diff --git a/Cargo.toml b/Cargo.toml
index aa062b4..90815c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ thiserror = "2"
# Logging
tracing = "0.1"
+chrono = "0.4"
# Async (optional, for client)
tokio = { version = "1", features = ["rt", "net", "io-util", "time"], optional = true }
diff --git a/logs/user_prompt_submit.json b/logs/user_prompt_submit.json
new file mode 100644
index 0000000..a404f98
--- /dev/null
+++ b/logs/user_prompt_submit.json
@@ -0,0 +1,90 @@
+[
+ {
+ "session_id": "10296741-0b3a-4bd0-95ec-07fc3a823bb1",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/10296741-0b3a-4bd0-95ec-07fc3a823bb1.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "are all the FGP modules published to github? please publish"
+ },
+ {
+ "session_id": "10296741-0b3a-4bd0-95ec-07fc3a823bb1",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/10296741-0b3a-4bd0-95ec-07fc3a823bb1.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "commit the fly client.rs fix"
+ },
+ {
+ "session_id": "57ed67ac-2bd7-4367-9402-225a5ad61c75",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/57ed67ac-2bd7-4367-9402-225a5ad61c75.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "commit the protocol compliance tests"
+ },
+ {
+ "session_id": "57ed67ac-2bd7-4367-9402-225a5ad61c75",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/57ed67ac-2bd7-4367-9402-225a5ad61c75.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "push to github"
+ },
+ {
+ "session_id": "57ed67ac-2bd7-4367-9402-225a5ad61c75",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/57ed67ac-2bd7-4367-9402-225a5ad61c75.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "what's next on the fgp plan"
+ },
+ {
+ "session_id": "57ed67ac-2bd7-4367-9402-225a5ad61c75",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/57ed67ac-2bd7-4367-9402-225a5ad61c75.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "do the gmail attachments support"
+ },
+ {
+ "session_id": "57ed67ac-2bd7-4367-9402-225a5ad61c75",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/57ed67ac-2bd7-4367-9402-225a5ad61c75.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "do the gmail attachments support"
+ },
+ {
+ "session_id": "57ed67ac-2bd7-4367-9402-225a5ad61c75",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/57ed67ac-2bd7-4367-9402-225a5ad61c75.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "create PR for gmail attachment change and set a reminder for 10 minutes for me to check on it"
+ },
+ {
+ "session_id": "b084daa3-6131-406b-a78c-8246feeb00fe",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/b084daa3-6131-406b-a78c-8246feeb00fe.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "yes push to origin"
+ },
+ {
+ "session_id": "b084daa3-6131-406b-a78c-8246feeb00fe",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/b084daa3-6131-406b-a78c-8246feeb00fe.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "plan",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "what should we do next in terms of polishing the repos/org and adding documentation?"
+ },
+ {
+ "session_id": "b084daa3-6131-406b-a78c-8246feeb00fe",
+ "transcript_path": "/Users/wolfgangschoenberger/.claude/projects/-Users-wolfgangschoenberger-Projects-fgp/b084daa3-6131-406b-a78c-8246feeb00fe.jsonl",
+ "cwd": "/Users/wolfgangschoenberger/Projects/fgp/daemon",
+ "permission_mode": "bypassPermissions",
+ "hook_event_name": "UserPromptSubmit",
+ "prompt": "\na080b89\ncompleted\nAgent \"Audit FGP repo documentation\" completed\nNow let me compile my findings into a comprehensive audit report. Based on my exploration, I have enough information to provide a detailed assessment.\n\n## FGP Documentation Audit Report\n\nI've completed a comprehensive audit of the FGP repository documentation. Here's the detailed analysis:\n\n---\n\n## Executive Summary\n\n**Overall Status:** Good foundation with significant gaps in service-specific documentation\n\n**Key Findings:**\n- Strong: Core README, protocol spec, contribution guidelines, browser daemon\n- Weak: 5 service daemons (Gmail, Calendar, GitHub, Fly, Neon, Vercel) lack depth\n- Missing: Troubleshooting guides, architecture diagrams, API method documentation, tests\n\n---\n\n## Repository-by-Repository Audit\n\n### Root Level (`/fgp/`)\n**Status:** Strong\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | Excellent - 324 lines, comprehensive overview, performance tables, quick start for all services |\n| Architecture | Well-documented with ASCII diagram |\n| Installation | Clear per-service instructions |\n| Protocol Overview | Concise explanation of NDJSON format |\n| CLAUDE.md | Excellent - detailed working guidelines |\n| CHANGELOG.md | Present but minimal (1 entry) |\n| Contributing Guide | Good - covers dev setup, testing, commit style |\n| Status Table | Excellent - clear component status matrix |\n\n**Gaps:**\n- No troubleshooting/FAQ section\n- No architecture decision records (ADRs)\n- No integration examples (how to use multiple daemons together)\n\n---\n\n### Core SDKs\n\n#### `daemon/` (Rust SDK)\n**Status:** Strong\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 324 lines - Good overview, performance highlights |\n| Inline Docs | Excellent - service.rs has 50+ lines of detailed rustdoc examples |\n| Examples | 1 example (echo_daemon.rs) |\n| CHANGELOG.md | Present with detailed entries |\n| Code Structure | Well-organized (protocol.rs, server.rs, service.rs, client.rs, lifecycle.rs) |\n\n**Gaps:**\n- Only 1 example (should have: Python module, auth flow, multi-method service)\n- No API documentation comments on server.rs\n- No performance tuning guide\n- No error handling guide\n\n#### `daemon-py/` (Python SDK)\n**Status:** Good\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 164 lines - Comprehensive with code examples |\n| Examples | 1 example (echo_daemon.py) |\n| Inline Docs | Good docstrings but sparse |\n| API Stability | Unclear (no changelog) |\n\n**Gaps:**\n- No CHANGELOG.md\n- Only 1 example (needs: async patterns, error handling, integration examples)\n- No type stub documentation (.pyi files)\n- Missing migration guide from daemon-sdk to daemon-py\n\n#### `protocol/` (Protocol Spec)\n**Status:** Excellent\n\n| Aspect | Assessment |\n|--------|-----------|\n| FGP-PROTOCOL.md | Excellent - 80+ lines of formal specification |\n| Section Coverage | Complete (transport, framing, versioning, error handling) |\n| Examples | Multiple JSON request/response examples |\n| README.md | Basic reference |\n\n**Gaps:**\n- No versioning history (how to handle protocol evolution)\n- No backward compatibility guarantees\n- No performance benchmarks for protocol overhead\n\n#### `cli/` (FGP CLI)\n**Status:** Moderate\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 110 lines - Good command reference |\n| Commands Documented | Yes - table format |\n| Examples | 1 example (workflow.sh) |\n| CHANGELOG.md | Minimal entries |\n| Installation | Clear |\n\n**Gaps:**\n- No configuration file documentation (where does .fgp/config.json go?)\n- No troubleshooting (what if daemon won't start?)\n- No shell completion setup\n- Missing: daemon management (restart, logs, debug)\n\n---\n\n### Browser Daemon\n**Status:** Excellent\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 224 lines - Very comprehensive |\n| Quick Start | Detailed with 5 code examples |\n| CLI Commands | Complete table with all commands |\n| Architecture | Clear ASCII diagram |\n| Performance Tips | 4 specific tips |\n| Examples | 1 quickstart.rs example |\n| CHANGELOG.md | Present |\n| Inline Docs | Good (service.rs has clear structure comments) |\n\n**Gaps:**\n- No error handling guide (what if Chrome crashes?)\n- No session lifecycle documentation\n- No ARIA tree format specification\n- No CSS selector vs element ref (@e1) performance comparison\n\n---\n\n### Gmail Daemon\n**Status:** Moderate\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 178 lines - Basic coverage |\n| Installation | Covers automated + manual setup |\n| OAuth Setup | Clear steps (Google Console \u2192 credentials.json) |\n| Methods Documented | 6 methods with examples |\n| Performance | Comparison table present |\n| Response Format | 1 JSON example |\n| CHANGELOG.md | Present in main.rs but not separate file |\n\n**Gaps:**\n- **No examples directory** (needs: OAuth flow, batch operations, error handling, attachment handling)\n- **No inline documentation** in main.rs beyond top comment\n- No troubleshooting guide (OAuth failures, quota limits, authentication refresh)\n- No attachment handling details (size limits, types)\n- No search query syntax guide (only brief examples)\n- No threading/conversation handling guide\n- Missing: What happens to tokens? How long do they last?\n\n---\n\n### Calendar Daemon\n**Status:** Moderate\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 83 lines - Basic but incomplete |\n| Installation | Covered |\n| Methods | Table of 5 methods |\n| Setup | Clear |\n| Performance | Metrics present |\n\n**Gaps:**\n- **No examples directory**\n- **Extremely sparse** - only 83 lines for entire daemon\n- No free slot algorithm explanation\n- No timezone handling documentation\n- No calendar selection (primary vs other calendars)\n- No recurring event handling\n- No inline code documentation\n- Missing: oauth setup details (points to generic Google Calendar API docs)\n- No event creation schema (date format, time zones, attendees)\n\n---\n\n### GitHub Daemon\n**Status:** Moderate\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 151 lines - Good foundation |\n| Installation | Clear (requires gh CLI auth) |\n| Methods | 5 methods documented with params |\n| Quick Start | 6 examples |\n| Architecture | Flow diagram present |\n| Protocol | Example request/response |\n\n**Gaps:**\n- **No examples directory**\n- No gh CLI auth setup guide (assumes user already knows)\n- No GraphQL query examples (implementation detail)\n- No pagination handling\n- No rate limiting guidance\n- No error scenarios (e.g., private repos, no access)\n- Missing: notification filtering options\n- No PR status schema (what's returned?)\n- No repository filtering (forks, templates, archived)\n\n---\n\n### Fly.io Daemon\n**Status:** Moderate-Good\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 74 lines - Basic but organized |\n| Installation | Clear with Rust version requirement |\n| Quick Start | 4 examples |\n| Methods | 4 methods with param details |\n| Performance | Comparison vs MCP |\n| Use Cases | Listed (good for context) |\n\n**Gaps:**\n- **No examples directory**\n- No FLY_API_TOKEN creation guide (assumes knowledge)\n- No machine status constants (what values possible?)\n- No deployment history access\n- No environment variable handling\n- No error handling (what if token is invalid?)\n- Missing: volume/database management methods\n\n---\n\n### Neon Daemon\n**Status:** Moderate-Good\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 87 lines - More complete than Fly |\n| Installation | Clear requirements |\n| Quick Start | 4 examples with credentials setup |\n| Methods | 8 methods (most comprehensive of services) |\n| Use Cases | Listed (AI agents, schema exploration, data validation) |\n| SQL Support | Specific example included |\n\n**Gaps:**\n- **No examples directory**\n- No API key creation guide (step-by-step from Neon console)\n- No SQL error handling (what if query fails?)\n- No transaction support documented\n- No branch strategy documentation\n- No connection pooling documentation\n- Missing: table size limits, query timeouts\n- No backup/restore methods\n\n---\n\n### Vercel Daemon\n**Status:** Moderate-Good\n\n| Aspect | Assessment |\n|--------|-----------|\n| README.md | 86 lines - Well-structured |\n| Installation | Clear |\n| Quick Start | 4 examples |\n| Methods | 6 methods documented |\n| Use Cases | Listed (deployment monitoring, log debugging) |\n| Performance | Comparison table |\n\n**Gaps:**\n- **No examples directory**\n- No VERCEL_TOKEN creation guide\n- No log format specification\n- No deployment status constants (what states?)\n- No environment/secret management\n- No preview deployment handling\n- Missing: project domain management\n\n---\n\n### MCP Bridge\n**Status:** Good\n\n| Aspect | Assessment |\n|--------|========|\n| README.md | 92 lines - Clear purpose |\n| Architecture | Good flow diagram |\n| Tool Discovery | Explained well |\n| Configuration | Claude Desktop example provided |\n| Testing | Manual test examples |\n| Use Cases | Explained clearly |\n\n**Gaps:**\n- No examples directory\n- No error handling (what if daemon crashes while bridge runs?)\n- No performance overhead analysis\n- No fallback behavior documentation\n- No JSON Schema generation details\n- Missing: compatibility matrix (which MCP clients tested?)\n\n---\n\n## Documentation Gaps Summary\n\n### Critical Gaps (Affects Usability)\n\n1. **Examples Directories - 7 of 12 repos missing examples**\n - Missing: gmail/, calendar/, github/, fly/, neon/, vercel/, mcp-bridge/\n - Impact: Users must reverse-engineer from README snippets\n - Needed: Actual runnable code for each method\n\n2. **Troubleshooting Guides - 0 of 12 have them**\n - Missing: All daemons\n - Common issues: OAuth failures, API quota limits, token refresh, socket errors\n - Needed: FAQ with solutions to common problems\n\n3. **API Method Documentation**\n - Gmail: No schema for attachment handling, threading\n - Calendar: No timezone/recurring event docs\n - GitHub: No pagination, rate limiting, filtering details\n - Neon: No transaction, timeout, size limit docs\n - Vercel: No deployment status constants, secret handling\n\n4. **Error Handling Patterns**\n - Missing: How to handle specific errors (404, 403, timeout, etc.)\n - Missing: Retry strategies\n - Missing: Error response schema\n\n### Major Gaps (Affects Implementation)\n\n5. **Credential/Auth Setup**\n - Each daemon has different auth (OAuth, env vars, gh CLI)\n - Missing: Step-by-step console setup for each service\n - Missing: Token lifecycle (expiration, refresh, storage)\n - Missing: Unified auth documentation\n\n6. **Inline Code Documentation**\n - Browser: Excellent (~50 lines of examples in service.rs)\n - Gmail: Good (top-level comments)\n - Others: Minimal to none\n - Missing: Docstring comments on key functions\n\n7. **Test Coverage Documentation**\n - Only 3 repos have tests: browser (1), cli (1), daemon (3)\n - No test examples in README\n - Missing: How to write tests for custom daemons\n\n8. **Configuration Files**\n - Missing: ~/.fgp/config.json schema and examples\n - Missing: Service manifest.json format\n - Missing: Authentication token storage details\n\n### Moderate Gaps (Nice to Have)\n\n9. **Architecture Decision Records (ADRs)**\n - Why UNIX sockets vs TCP?\n - Why NDJSON vs Protocol Buffers?\n - Why separate daemons vs monolith?\n - Missing: Decision history\n\n10. **Integration Guides**\n - How to use multiple daemons together?\n - How to coordinate state across services?\n - Missing: Multi-daemon workflows\n\n11. **Performance Tuning**\n - Connection pooling strategies\n - Caching guidance\n - Batch operation patterns\n - Missing: Best practices guide\n\n12. **Deployment Guides**\n - How to deploy daemons to production?\n - Docker, systemd, launchd examples?\n - Missing: DevOps documentation\n\n---\n\n## Quality Metrics\n\n| Category | Score | Notes |\n|----------|-------|-------|\n| README Coverage | 7/10 | All repos have READMEs but varying quality |\n| Code Examples | 3/10 | Only 3 repos have examples (browser, cli, daemon-py) |\n| Inline Documentation | 6/10 | Rust code better than others; Python minimal |\n| Getting Started Guides | 6/10 | Installation covered; OAuth setup varies |\n| API Documentation | 5/10 | Methods listed, but no schemas or error docs |\n| Troubleshooting | 1/10 | Almost completely missing |\n| Tests | 2/10 | Only 3 repos have tests; no test guides |\n| Architecture Docs | 7/10 | Good at high level; missing deep dives |\n\n**Overall Documentation Score: 4.3/10**\n\n---\n\n## Recommendations by Priority\n\n### T0 (Critical - Blocks Users)\n\n1. **Create examples/ directories for all 7 service daemons**\n - gmail/examples/: OAuth flow, inbox read, search, send, attachment handling\n - calendar/examples/: list events, create event, find free slots\n - github/examples/: list repos, create issue, check PR status\n - fly/examples/: list apps, check status, update machine\n - neon/examples/: query database, manage branches, schema inspection\n - vercel/examples/: list projects, get deployment logs, deployment status\n - mcp-bridge/examples/: configure Claude Desktop, test tool discovery\n\n2. **Add troubleshooting sections to all service READMEs**\n - Common errors and solutions\n - OAuth timeout handling\n - API quota/rate limiting\n - Socket permission issues\n - Token refresh behavior\n\n3. **Document API method schemas for each service**\n - Request parameters with types\n - Response objects with examples\n - Error responses\n - Limits and constraints\n\n### T1 (Important - Improves DX)\n\n4. **Comprehensive auth setup guides**\n - Step-by-step credential creation for each service\n - Token lifecycle and refresh\n - Unified auth documentation\n - Security best practices\n\n5. **Add inline code documentation**\n - Service struct rustdocs\n - Method dispatch comments\n - Python method docstrings\n - Parameter validation details\n\n6. **Test examples and guides**\n - How to write tests for daemons\n - Example test files for each service\n - CI/CD testing patterns\n\n### T2 (Nice to Have - Polish)\n\n7. **Architecture documentation**\n - ADRs for design decisions\n - Performance design patterns\n - Concurrency models\n - Connection pooling strategies\n\n8. **Integration guides**\n - Multi-daemon workflows\n - State coordination\n - Dependency management\n\n9. **Deployment documentation**\n - Docker containers\n - systemd services\n - macOS launchd setup\n - Process management\n\n10. **Performance tuning guides**\n - Batch operation patterns\n - Caching strategies\n - Metrics and monitoring\n - Load testing\n\n---\n\n## By the Numbers\n\n| Metric | Status |\n|--------|--------|\n| Repos with README | 12/12 (100%) |\n| Repos with examples | 3/12 (25%) |\n| Repos with tests | 3/12 (25%) |\n| Repos with CHANGELOG | 5/12 (42%) |\n| Services with OAuth docs | 2/7 (29%) |\n| Services with error handling docs | 0/7 (0%) |\n| Services with integration examples | 0/7 (0%) |\n| Services with API schemas | 0/7 (0%) |\n\n---\n\n## File Paths for Reference\n\n**Well-documented:**\n- `/Users/wolfgangschoenberger/Projects/fgp/README.md` - Root overview\n- `/Users/wolfgangschoenberger/Projects/fgp/protocol/FGP-PROTOCOL.md` - Spec\n- `/Users/wolfgangschoenberger/Projects/fgp/browser/README.md` - Best service example\n- `/Users/wolfgangschoenberger/Projects/fgp/.github/CONTRIBUTING.md` - Contribution guide\n\n**Needs Improvement:**\n- `/Users/wolfgangschoenberger/Projects/fgp/calendar/README.md` - Only 83 lines\n- `/Users/wolfgangschoenberger/Projects/fgp/gmail/README.md` - Missing error handling\n- `/Users/wolfgangschoenberger/Projects/fgp/github/README.md` - Missing auth setup guide\n- `/Users/wolfgangschoenberger/Projects/fgp/daemon-py/README.md` - No CHANGELOG\n- `/Users/wolfgangschoenberger/Projects/fgp/cli/README.md` - No config documentation\n\n---\n\n## Conclusion\n\nThe FGP project has **strong foundation documentation** (root README, protocol spec, contribution guide) but **significant gaps in service-specific documentation**. The browser daemon exemplifies what complete documentation looks like. Most other services need:\n\n1. Practical examples (runnable code)\n2. Error handling and troubleshooting\n3. Detailed API schemas\n4. OAuth/credential setup guides\n5. Inline code documentation\n\nAddressing T0 items would significantly improve user experience and reduce support burden. Current documentation could block users from successfully using 5 of 7 service daemons without external research.\n\nFull transcript available at: /tmp/claude/-Users-wolfgangschoenberger-Projects-fgp/tasks/a080b89.output"
+ }
+]
\ No newline at end of file
diff --git a/src/client.rs b/src/client.rs
index 5a70eb5..bc9a9b6 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -29,9 +29,25 @@ use crate::protocol::{Request, Response};
/// println!("Health: {:?}", health);
/// # Ok::<(), anyhow::Error>(())
/// ```
+///
+/// ## Auto-Start
+///
+/// Use `FgpClient::for_service()` to create a client that automatically starts
+/// the daemon if it's not running:
+///
+/// ```rust,no_run
+/// use fgp_daemon::FgpClient;
+///
+/// // This will auto-start the gmail daemon if needed
+/// let client = FgpClient::for_service("gmail")?;
+/// let response = client.call("gmail.inbox", serde_json::json!({}))?;
+/// # Ok::<(), anyhow::Error>(())
+/// ```
pub struct FgpClient {
socket_path: PathBuf,
timeout: Duration,
+ /// Service name for auto-start support
+ auto_start_service: Option,
}
impl FgpClient {
@@ -44,6 +60,34 @@ impl FgpClient {
Ok(Self {
socket_path,
timeout: Duration::from_secs(30),
+ auto_start_service: None,
+ })
+ }
+
+ /// Create a client for a named service with auto-start enabled.
+ ///
+ /// This is the recommended way to create a client. If the daemon is not running,
+ /// it will be automatically started on the first call.
+ ///
+ /// # Arguments
+ /// * `service_name` - Name of the service (e.g., "gmail", "browser", "calendar")
+ ///
+ /// # Example
+ ///
+ /// ```rust,no_run
+ /// use fgp_daemon::FgpClient;
+ ///
+ /// let client = FgpClient::for_service("gmail")?;
+ /// // If gmail daemon isn't running, it will be auto-started
+ /// let response = client.call("gmail.inbox", serde_json::json!({}))?;
+ /// # Ok::<(), anyhow::Error>(())
+ /// ```
+ pub fn for_service(service_name: &str) -> Result {
+ let socket_path = crate::lifecycle::service_socket_path(service_name);
+ Ok(Self {
+ socket_path,
+ timeout: Duration::from_secs(30),
+ auto_start_service: Some(service_name.to_string()),
})
}
@@ -53,6 +97,26 @@ impl FgpClient {
self
}
+ /// Enable auto-start for a specific service.
+ ///
+ /// When auto-start is enabled and the daemon is not running, the client
+ /// will attempt to start it automatically on the first call.
+ ///
+ /// # Arguments
+ /// * `service_name` - Name of the service to auto-start
+ pub fn with_auto_start(mut self, service_name: &str) -> Self {
+ self.auto_start_service = Some(service_name.to_string());
+ self
+ }
+
+ /// Disable auto-start.
+ ///
+ /// Calls will fail immediately if the daemon is not running.
+ pub fn without_auto_start(mut self) -> Self {
+ self.auto_start_service = None;
+ self
+ }
+
/// Call a daemon method.
///
/// # Arguments
@@ -105,10 +169,35 @@ impl FgpClient {
/// Send a request and receive a response.
fn send_request(&self, request: &Request) -> Result {
- // Connect to socket
- let mut stream = UnixStream::connect(&self.socket_path)
- .with_context(|| format!("Cannot connect to daemon at {:?}", self.socket_path))?;
+ // Try to connect to socket
+ let stream = match UnixStream::connect(&self.socket_path) {
+ Ok(stream) => stream,
+ Err(e) => {
+ // Connection failed - try auto-start if configured
+ if let Some(ref service_name) = self.auto_start_service {
+ tracing::info!(
+ "Daemon not running, auto-starting service '{}'...",
+ service_name
+ );
+
+ // Start the service
+ crate::lifecycle::start_service(service_name)
+ .with_context(|| format!("Failed to auto-start service '{}'", service_name))?;
+ // Retry connection
+ UnixStream::connect(&self.socket_path)
+ .with_context(|| format!("Cannot connect to daemon at {:?} after auto-start", self.socket_path))?
+ } else {
+ return Err(e).with_context(|| format!("Cannot connect to daemon at {:?}", self.socket_path));
+ }
+ }
+ };
+
+ self.send_request_on_stream(stream, request)
+ }
+
+ /// Send request on an already-connected stream.
+ fn send_request_on_stream(&self, mut stream: UnixStream, request: &Request) -> Result {
stream.set_read_timeout(Some(self.timeout))?;
stream.set_write_timeout(Some(self.timeout))?;
@@ -135,11 +224,15 @@ fn expand_path(path: &Path) -> Result {
/// Convenience function to call a method on a daemon.
///
+/// This does NOT auto-start the daemon. If the daemon is not running, the call
+/// will fail. Use `call_auto_start()` if you want automatic daemon startup.
+///
/// # Example
///
/// ```rust,no_run
/// use fgp_daemon::client::call;
///
+/// // Fails if gmail daemon is not running
/// let response = call("gmail", "gmail.list", serde_json::json!({"limit": 5}))?;
/// # Ok::<(), anyhow::Error>(())
/// ```
@@ -149,12 +242,27 @@ pub fn call(service_name: &str, method: &str, params: serde_json::Value) -> Resu
client.call(method, params)
}
+/// Call a method with auto-start enabled.
+///
+/// If the daemon is not running, it will be started automatically.
+///
+/// # Example
+///
+/// ```rust,no_run
+/// use fgp_daemon::client::call_auto_start;
+///
+/// // Auto-starts gmail daemon if not running
+/// let response = call_auto_start("gmail", "gmail.list", serde_json::json!({"limit": 5}))?;
+/// # Ok::<(), anyhow::Error>(())
+/// ```
+pub fn call_auto_start(service_name: &str, method: &str, params: serde_json::Value) -> Result {
+ let client = FgpClient::for_service(service_name)?;
+ client.call(method, params)
+}
+
/// Check if a daemon service is running.
pub fn is_running(service_name: &str) -> bool {
- let socket_path = crate::lifecycle::service_socket_path(service_name);
- FgpClient::new(socket_path)
- .map(|c| c.is_running())
- .unwrap_or(false)
+ crate::lifecycle::is_service_running(service_name)
}
#[cfg(test)]
diff --git a/src/lib.rs b/src/lib.rs
index b4aacba..008df78 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -61,7 +61,11 @@ pub use protocol::{Request, Response, ErrorInfo, ResponseMeta};
pub use server::FgpServer;
pub use service::FgpService;
pub use client::FgpClient;
-pub use lifecycle::{daemonize, write_pid_file, cleanup_socket};
+pub use lifecycle::{
+ daemonize, write_pid_file, cleanup_socket,
+ start_service, start_service_with_timeout, stop_service, is_service_running,
+ fgp_services_dir, service_socket_path, service_pid_path
+};
#[cfg(feature = "python")]
pub use python::PythonModule;
diff --git a/src/lifecycle.rs b/src/lifecycle.rs
index 9497888..a85d49f 100644
--- a/src/lifecycle.rs
+++ b/src/lifecycle.rs
@@ -1,11 +1,14 @@
//! Daemon lifecycle utilities.
//!
-//! Helpers for daemonizing processes, managing PID files, and socket cleanup.
+//! Helpers for daemonizing processes, managing PID files, socket cleanup,
+//! and on-demand service starting.
-use anyhow::{Context, Result};
+use anyhow::{bail, Context, Result};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::time::{Duration, Instant};
/// Daemonize the current process.
///
@@ -133,6 +136,41 @@ fn expand_path(path: &Path) -> Result {
Ok(PathBuf::from(expanded.as_ref()))
}
+/// Validate that an entrypoint is safe to execute.
+///
+/// Checks:
+/// - File has executable permission
+/// - File is owned by current user or root (not world-writable)
+fn validate_entrypoint(path: &Path) -> Result<()> {
+ use std::os::unix::fs::PermissionsExt;
+
+ let metadata = fs::metadata(path)
+ .with_context(|| format!("Cannot read entrypoint metadata: {}", path.display()))?;
+
+ let permissions = metadata.permissions();
+ let mode = permissions.mode();
+
+ // Check if file is executable (user, group, or other)
+ if mode & 0o111 == 0 {
+ bail!(
+ "Entrypoint is not executable: {}. Run: chmod +x {}",
+ path.display(),
+ path.display()
+ );
+ }
+
+ // Security check: warn if world-writable (but don't block)
+ if mode & 0o002 != 0 {
+ tracing::warn!(
+ "Security warning: entrypoint {} is world-writable. Consider: chmod o-w {}",
+ path.display(),
+ path.display()
+ );
+ }
+
+ Ok(())
+}
+
/// Standard socket path for a service.
pub fn service_socket_path(service_name: &str) -> PathBuf {
let base = shellexpand::tilde("~/.fgp/services");
@@ -149,6 +187,217 @@ pub fn service_pid_path(service_name: &str) -> PathBuf {
.join("daemon.pid")
}
+/// Get the FGP services base directory.
+pub fn fgp_services_dir() -> PathBuf {
+ let base = shellexpand::tilde("~/.fgp/services");
+ PathBuf::from(base.as_ref())
+}
+
+/// Start a daemon service on-demand.
+///
+/// This function:
+/// 1. Reads the service manifest from `~/.fgp/services/{service}/manifest.json`
+/// 2. Spawns the daemon entrypoint process
+/// 3. Waits for the socket to appear (with timeout)
+///
+/// # Arguments
+/// * `service_name` - Name of the service to start (e.g., "gmail", "browser")
+///
+/// # Example
+///
+/// ```rust,no_run
+/// use fgp_daemon::lifecycle::start_service;
+///
+/// // Start the gmail daemon
+/// start_service("gmail")?;
+/// # Ok::<(), anyhow::Error>(())
+/// ```
+pub fn start_service(service_name: &str) -> Result<()> {
+ start_service_with_timeout(service_name, Duration::from_secs(5))
+}
+
+/// Start a daemon service with a custom timeout.
+///
+/// # Arguments
+/// * `service_name` - Name of the service to start
+/// * `timeout` - Maximum time to wait for socket to appear
+pub fn start_service_with_timeout(service_name: &str, timeout: Duration) -> Result<()> {
+ let service_dir = fgp_services_dir().join(service_name);
+
+ // Check if service is installed
+ let manifest_path = service_dir.join("manifest.json");
+ if !manifest_path.exists() {
+ bail!(
+ "Service '{}' is not installed. Run 'fgp install ' first.",
+ service_name
+ );
+ }
+
+ // Check if already running
+ let socket_path = service_socket_path(service_name);
+ if socket_path.exists() {
+ // Try to connect to see if it's actually running
+ if std::os::unix::net::UnixStream::connect(&socket_path).is_ok() {
+ tracing::debug!("Service '{}' is already running", service_name);
+ return Ok(());
+ } else {
+ // Stale socket, remove it
+ let _ = fs::remove_file(&socket_path);
+ }
+ }
+
+ // Read manifest to get entrypoint
+ let manifest_content = fs::read_to_string(&manifest_path)
+ .context("Failed to read manifest.json")?;
+ let manifest: serde_json::Value = serde_json::from_str(&manifest_content)
+ .context("Failed to parse manifest.json")?;
+
+ let entrypoint = manifest["daemon"]["entrypoint"]
+ .as_str()
+ .context("manifest.json missing daemon.entrypoint")?;
+
+ let entrypoint_path = service_dir.join(entrypoint);
+ if !entrypoint_path.exists() {
+ bail!("Daemon entrypoint not found: {}", entrypoint_path.display());
+ }
+
+ // Security: Validate entrypoint is executable
+ validate_entrypoint(&entrypoint_path)?;
+
+ tracing::info!("Starting service '{}'...", service_name);
+
+ // Start as background process
+ let _child = Command::new(&entrypoint_path)
+ .current_dir(&service_dir)
+ .spawn()
+ .context("Failed to start daemon")?;
+
+ // Wait for socket to appear with timeout
+ let start = Instant::now();
+ while start.elapsed() < timeout {
+ if socket_path.exists() {
+ // Verify we can connect
+ if std::os::unix::net::UnixStream::connect(&socket_path).is_ok() {
+ tracing::info!("Service '{}' started successfully", service_name);
+ return Ok(());
+ }
+ }
+ std::thread::sleep(Duration::from_millis(50));
+ }
+
+ bail!(
+ "Service '{}' started but socket not ready within {:?}",
+ service_name,
+ timeout
+ )
+}
+
+/// Stop a daemon service.
+///
+/// Sends SIGTERM to the daemon process and cleans up socket/PID files.
+///
+/// # Arguments
+/// * `service_name` - Name of the service to stop
+pub fn stop_service(service_name: &str) -> Result<()> {
+ let socket_path = service_socket_path(service_name);
+ let pid_path = service_pid_path(service_name);
+
+ if socket_path.exists() {
+ if let Ok(client) = crate::client::FgpClient::new(&socket_path) {
+ if let Ok(response) = client.stop() {
+ if response.ok {
+ return Ok(());
+ }
+ }
+ }
+ }
+
+ // Check if PID file exists
+ if let Some(pid) = read_pid_file(&pid_path) {
+ if is_process_running(pid) {
+ tracing::info!("Stopping service '{}' (PID: {})...", service_name, pid);
+
+ let expected = read_entrypoint_name(service_name)?;
+ if !pid_matches_process(pid, expected.as_deref()) {
+ bail!(
+ "Refusing to stop PID {}: process does not match expected entrypoint '{}'",
+ pid,
+ expected.unwrap_or_else(|| "unknown".to_string())
+ );
+ }
+
+ // Send SIGTERM
+ unsafe {
+ libc::kill(pid as i32, libc::SIGTERM);
+ }
+
+ // Wait a moment for graceful shutdown
+ std::thread::sleep(Duration::from_millis(500));
+ }
+ }
+
+ // Clean up files
+ let _ = fs::remove_file(&socket_path);
+ let _ = fs::remove_file(&pid_path);
+
+ tracing::info!("Service '{}' stopped", service_name);
+ Ok(())
+}
+
+fn read_entrypoint_name(service_name: &str) -> Result