@cohortapp/agent-sdk 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Gmail Poller — polls the agent's inbox via native Node.js IMAP.
|
|
2
|
+
// Uses imapflow + mailparser to fetch unseen emails from the agent's
|
|
3
|
+
// configured email address (config/agent.json#/email) and write them as
|
|
4
|
+
// .json files to state/inbox/gmail/.
|
|
5
|
+
//
|
|
6
|
+
// All IMAP logic is in imap-client.mjs, shared with the principal poller.
|
|
7
|
+
|
|
8
|
+
import { readFileSync, readdirSync, renameSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { readCursor, writeCursor, AGENT_REPO_DIR } from "./utils.mjs";
|
|
11
|
+
import { pollImapInbox } from "./imap-client.mjs";
|
|
12
|
+
|
|
13
|
+
const GMAIL_INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "gmail");
|
|
14
|
+
const MAX_ITEMS_PER_POLL = 10;
|
|
15
|
+
|
|
16
|
+
// Load agent identity once. Email + first-name used as account labels.
|
|
17
|
+
let _agent = null;
|
|
18
|
+
function loadAgent() {
|
|
19
|
+
if (_agent) return _agent;
|
|
20
|
+
try {
|
|
21
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
22
|
+
} catch {
|
|
23
|
+
_agent = { firstName: "agent", email: "" };
|
|
24
|
+
}
|
|
25
|
+
return _agent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load the agent's Gmail app password from env or the agent's .env file.
|
|
30
|
+
*/
|
|
31
|
+
function loadGmailPassword() {
|
|
32
|
+
if (process.env.GMAIL_APP_PASSWORD) return process.env.GMAIL_APP_PASSWORD;
|
|
33
|
+
try {
|
|
34
|
+
const envFile = readFileSync(join(AGENT_REPO_DIR, ".env"), "utf-8");
|
|
35
|
+
for (const line of envFile.split("\n")) {
|
|
36
|
+
if (line.startsWith("GMAIL_APP_PASSWORD=")) {
|
|
37
|
+
let val = line.split("=", 2)[1].trim();
|
|
38
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
39
|
+
val = val.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
return val;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch { /* no .env file */ }
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract sender name from email "From" header.
|
|
50
|
+
*/
|
|
51
|
+
function extractSenderName(from) {
|
|
52
|
+
const match = from.match(/^"?([^"<]+)"?\s*</);
|
|
53
|
+
return match ? match[1].trim() : from.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract sender email from "From" header.
|
|
58
|
+
*/
|
|
59
|
+
function extractSenderEmail(from) {
|
|
60
|
+
const match = from.match(/<([^>]+)>/);
|
|
61
|
+
return match ? match[1].trim() : from.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Determine sender privilege from email address. The principal (CEO) is
|
|
66
|
+
* recognised by matching against the agent's `principal.email` or
|
|
67
|
+
* `principal.firstName` from config/agent.json — never hardcode names.
|
|
68
|
+
*
|
|
69
|
+
* INTERNAL classification comes from CONFIG, never a hardcoded domain: the
|
|
70
|
+
* optional `internal_domains[]` array in config/agent.json, defaulting to the
|
|
71
|
+
* domain of the agent's own `email` there. Exact domain match (not substring)
|
|
72
|
+
* so `evil-corp.example.attacker.net` never rides an `example` allowance.
|
|
73
|
+
*/
|
|
74
|
+
function internalDomains(me) {
|
|
75
|
+
const configured = Array.isArray(me.internal_domains)
|
|
76
|
+
? me.internal_domains.map((d) => String(d).toLowerCase().replace(/^@/, "")).filter(Boolean)
|
|
77
|
+
: [];
|
|
78
|
+
if (configured.length) return configured;
|
|
79
|
+
const own = String(me.email || "");
|
|
80
|
+
const at = own.lastIndexOf("@");
|
|
81
|
+
return at > 0 ? [own.slice(at + 1).toLowerCase()] : [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveSenderPrivilege(email) {
|
|
85
|
+
const lower = email.toLowerCase();
|
|
86
|
+
const me = loadAgent();
|
|
87
|
+
const principalEmail = (me.principal?.email || "").toLowerCase();
|
|
88
|
+
const principalFirst = (me.principal?.firstName || "").toLowerCase();
|
|
89
|
+
if (principalEmail && lower === principalEmail) return "ceo";
|
|
90
|
+
if (principalFirst && lower.includes(principalFirst)) return "ceo";
|
|
91
|
+
const domain = lower.includes("@") ? lower.slice(lower.lastIndexOf("@") + 1) : "";
|
|
92
|
+
if (domain && internalDomains(me).includes(domain)) return "internal";
|
|
93
|
+
return "external";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function pollGmail() {
|
|
97
|
+
const items = [];
|
|
98
|
+
const errors = [];
|
|
99
|
+
|
|
100
|
+
// Run native IMAP poll for the agent's configured email address.
|
|
101
|
+
const me = loadAgent();
|
|
102
|
+
const agentSlug = (me.firstName || "agent").toLowerCase();
|
|
103
|
+
try {
|
|
104
|
+
const result = await pollImapInbox({
|
|
105
|
+
email: me.email,
|
|
106
|
+
password: loadGmailPassword(),
|
|
107
|
+
account: agentSlug,
|
|
108
|
+
fileSuffix: "email",
|
|
109
|
+
eventType: `${agentSlug}_email`,
|
|
110
|
+
logPrefix: `[${agentSlug}-gmail]`,
|
|
111
|
+
});
|
|
112
|
+
if (result.errors.length > 0) {
|
|
113
|
+
errors.push(...result.errors);
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const msg = `Agent Gmail IMAP poll failed: ${err.message}`;
|
|
117
|
+
console.error(`[gmail] ${msg}`);
|
|
118
|
+
errors.push(msg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Read resulting JSON files (same as before — the IMAP client writes them)
|
|
122
|
+
try {
|
|
123
|
+
const files = readdirSync(GMAIL_INBOX_DIR)
|
|
124
|
+
.filter((f) => f.endsWith("-email.json") && !f.endsWith(".processed"))
|
|
125
|
+
.sort()
|
|
126
|
+
.slice(0, MAX_ITEMS_PER_POLL);
|
|
127
|
+
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
try {
|
|
130
|
+
const raw = readFileSync(join(GMAIL_INBOX_DIR, file), "utf-8");
|
|
131
|
+
const data = JSON.parse(raw);
|
|
132
|
+
const email = data.email || {};
|
|
133
|
+
const senderEmail = extractSenderEmail(email.from || "unknown");
|
|
134
|
+
const senderName = extractSenderName(email.from || "unknown");
|
|
135
|
+
const privilege = resolveSenderPrivilege(senderEmail);
|
|
136
|
+
const id = file.replace(/-email\.json$/, "");
|
|
137
|
+
|
|
138
|
+
// Pass through attachment metadata
|
|
139
|
+
const attachments = data.attachments
|
|
140
|
+
? data.attachments.map((a) => ({
|
|
141
|
+
name: a.name || "unnamed",
|
|
142
|
+
mimetype: a.mimetype || "unknown",
|
|
143
|
+
size: a.size || 0,
|
|
144
|
+
local_path: a.local_path || null,
|
|
145
|
+
}))
|
|
146
|
+
: undefined;
|
|
147
|
+
|
|
148
|
+
items.push({
|
|
149
|
+
id,
|
|
150
|
+
service: "gmail",
|
|
151
|
+
channel: `email/${senderEmail}`,
|
|
152
|
+
sender: senderName,
|
|
153
|
+
sender_email: senderEmail,
|
|
154
|
+
sender_privilege: privilege,
|
|
155
|
+
timestamp: data.received_at || email.date || new Date().toISOString(),
|
|
156
|
+
subject: (email.subject || "").replace(/\r\n/g, " ").trim(),
|
|
157
|
+
content: `Email from ${email.from || "unknown"}\nSubject: ${email.subject || "(no subject)"}\nDate: ${email.date || "unknown"}`,
|
|
158
|
+
thread_id: email.message_id || "",
|
|
159
|
+
is_reply: (email.subject || "").toLowerCase().startsWith("re:"),
|
|
160
|
+
attachments,
|
|
161
|
+
priority_signals: {
|
|
162
|
+
from_ceo: privilege === "ceo",
|
|
163
|
+
tagged_urgent: /urgent|asap|critical/i.test(email.subject || ""),
|
|
164
|
+
contains_deadline: /deadline|due|by\s+(monday|tuesday|wednesday|thursday|friday|tomorrow|end of)/i.test(email.subject || ""),
|
|
165
|
+
mentions_agent: new RegExp(agentSlug, "i").test(email.subject || ""),
|
|
166
|
+
},
|
|
167
|
+
raw_ref: `gmail:${id}`,
|
|
168
|
+
_inbox_file: file,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Mark as processed after reading
|
|
172
|
+
try {
|
|
173
|
+
renameSync(
|
|
174
|
+
join(GMAIL_INBOX_DIR, file),
|
|
175
|
+
join(GMAIL_INBOX_DIR, file + ".processed"),
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
// Already processed
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
errors.push(`Gmail file ${file}: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err.code !== "ENOENT") {
|
|
186
|
+
errors.push(`Gmail inbox scan: ${err.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
writeCursor("gmail", new Date().toISOString());
|
|
191
|
+
return { items, errors };
|
|
192
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// imap-client.mjs — Shared native IMAP polling for the agent's inbox and
|
|
2
|
+
// any other inbox the agent has explicit access to (e.g. principal-gmail).
|
|
3
|
+
// Replaces external Python scripts with direct Node.js IMAP via imapflow.
|
|
4
|
+
//
|
|
5
|
+
// Each inbox gets its own credentials and file-naming convention, but the
|
|
6
|
+
// connect → search UNSEEN → fetch headers+body → write JSON → disconnect
|
|
7
|
+
// flow is identical and lives here.
|
|
8
|
+
|
|
9
|
+
import { ImapFlow } from "imapflow";
|
|
10
|
+
import { writeFileSync, existsSync, readFileSync, mkdirSync } from "fs";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
import { AGENT_REPO_DIR } from "./utils.mjs";
|
|
14
|
+
|
|
15
|
+
const INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "gmail");
|
|
16
|
+
const ATTACHMENTS_DIR = join(AGENT_REPO_DIR, "state", "inbox", "attachments");
|
|
17
|
+
const MAX_EMAILS_PER_CYCLE = 20;
|
|
18
|
+
const SNIPPET_LENGTH = 500;
|
|
19
|
+
const IMAP_TIMEOUT_MS = 25_000;
|
|
20
|
+
|
|
21
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function messageIdHash(messageId, fromAddr = "", subject = "", date = "") {
|
|
24
|
+
const key = messageId || `${fromAddr}${subject}${date}`;
|
|
25
|
+
return createHash("md5").update(key).digest("hex").slice(0, 16);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stripHtml(html) {
|
|
29
|
+
let text = html.replace(/<(style|script)[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
30
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
31
|
+
text = text.replace(/<\/(p|div|tr|li)>/gi, "\n");
|
|
32
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
33
|
+
text = text.replace(/ /g, " ").replace(/&/g, "&");
|
|
34
|
+
text = text.replace(/</g, "<").replace(/>/g, ">");
|
|
35
|
+
text = text.replace(/"/g, '"').replace(/'/g, "'");
|
|
36
|
+
text = text.replace(/&\w+;/g, "");
|
|
37
|
+
text = text.replace(/[ \t]+/g, " ");
|
|
38
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
39
|
+
return text.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractSnippet(textBody, htmlBody, maxLen = SNIPPET_LENGTH) {
|
|
43
|
+
let body = (textBody || "").trim();
|
|
44
|
+
if (!body && htmlBody) body = stripHtml(htmlBody);
|
|
45
|
+
return body.length > maxLen ? body.slice(0, maxLen) + "..." : body;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isAlreadyProcessed(midHash, suffix) {
|
|
49
|
+
const baseName = `${midHash}-${suffix}.json`;
|
|
50
|
+
return (
|
|
51
|
+
existsSync(join(INBOX_DIR, baseName)) ||
|
|
52
|
+
existsSync(join(INBOX_DIR, baseName + ".processed"))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Cursor Management ───────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function cursorPath(account) {
|
|
59
|
+
return join(AGENT_REPO_DIR, "state", "polling", `${account}-gmail-cursor.yaml`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readImapCursor(account) {
|
|
63
|
+
const path = cursorPath(account);
|
|
64
|
+
try {
|
|
65
|
+
const content = readFileSync(path, "utf-8");
|
|
66
|
+
const cursor = {};
|
|
67
|
+
for (const line of content.split("\n")) {
|
|
68
|
+
const m = line.match(/^(\w+):\s*"?([^"\n]*)"?/);
|
|
69
|
+
if (m) {
|
|
70
|
+
let val = m[2].trim();
|
|
71
|
+
if (val === "null" || val === "") val = null;
|
|
72
|
+
cursor[m[1]] = val;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
last_poll: cursor.last_poll || null,
|
|
77
|
+
last_message_id: cursor.last_message_id || null,
|
|
78
|
+
messages_processed: parseInt(cursor.messages_processed || "0", 10) || 0,
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return { last_poll: null, last_message_id: null, messages_processed: 0 };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeImapCursor(account, lastPoll, lastMessageId, messagesProcessed) {
|
|
86
|
+
const path = cursorPath(account);
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
88
|
+
const lp = lastPoll ? `"${lastPoll}"` : "null";
|
|
89
|
+
const lm = lastMessageId ? `"${lastMessageId}"` : "null";
|
|
90
|
+
writeFileSync(
|
|
91
|
+
path,
|
|
92
|
+
`last_poll: ${lp}\nlast_message_id: ${lm}\nmessages_processed: ${messagesProcessed}\n`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Attachment Extraction ───────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function saveAttachments(parsed, midHash) {
|
|
99
|
+
const attachments = [];
|
|
100
|
+
if (!parsed.attachments || parsed.attachments.length === 0) return attachments;
|
|
101
|
+
|
|
102
|
+
mkdirSync(ATTACHMENTS_DIR, { recursive: true });
|
|
103
|
+
for (const att of parsed.attachments) {
|
|
104
|
+
if (!att.filename) continue;
|
|
105
|
+
// Skip tiny inline images (likely signatures)
|
|
106
|
+
if ((att.contentType || "").startsWith("image/") && att.content && att.content.length < 1024) continue;
|
|
107
|
+
|
|
108
|
+
const safeName = att.filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
109
|
+
const localName = `${midHash}-${safeName}`;
|
|
110
|
+
const localPath = join(ATTACHMENTS_DIR, localName);
|
|
111
|
+
try {
|
|
112
|
+
writeFileSync(localPath, att.content);
|
|
113
|
+
attachments.push({
|
|
114
|
+
name: att.filename,
|
|
115
|
+
mimetype: att.contentType || "unknown",
|
|
116
|
+
size: att.content.length,
|
|
117
|
+
local_path: localPath,
|
|
118
|
+
});
|
|
119
|
+
} catch {
|
|
120
|
+
// Non-fatal
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return attachments;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Core IMAP Poll ──────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Poll a Gmail inbox via IMAP and write new messages as JSON to the inbox dir.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} opts
|
|
132
|
+
* @param {string} opts.email — Email address to poll
|
|
133
|
+
* @param {string} opts.password — Gmail app password
|
|
134
|
+
* @param {string} opts.account — Account slug for cursor/file naming (e.g. agent's first name)
|
|
135
|
+
* @param {string} opts.fileSuffix — JSON file suffix
|
|
136
|
+
* @param {string} opts.eventType — Event type tag
|
|
137
|
+
* @param {string} opts.logPrefix — Console log prefix
|
|
138
|
+
* @returns {Promise<{newCount: number, errors: string[]}>}
|
|
139
|
+
*/
|
|
140
|
+
export async function pollImapInbox({
|
|
141
|
+
email: emailAddr,
|
|
142
|
+
password,
|
|
143
|
+
account,
|
|
144
|
+
fileSuffix,
|
|
145
|
+
eventType,
|
|
146
|
+
logPrefix,
|
|
147
|
+
}) {
|
|
148
|
+
const errors = [];
|
|
149
|
+
|
|
150
|
+
if (!password) {
|
|
151
|
+
console.log(`${logPrefix} App password not set, skipping`);
|
|
152
|
+
return { newCount: 0, errors };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
mkdirSync(INBOX_DIR, { recursive: true });
|
|
156
|
+
|
|
157
|
+
const cursor = readImapCursor(account);
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
let newCount = 0;
|
|
160
|
+
let skipped = 0;
|
|
161
|
+
let unseenCount = 0;
|
|
162
|
+
let lastMid = cursor.last_message_id;
|
|
163
|
+
let totalProcessed = cursor.messages_processed;
|
|
164
|
+
|
|
165
|
+
const client = new ImapFlow({
|
|
166
|
+
host: "imap.gmail.com",
|
|
167
|
+
port: 993,
|
|
168
|
+
secure: true,
|
|
169
|
+
auth: { user: emailAddr, pass: password },
|
|
170
|
+
logger: false, // Suppress imapflow debug logs
|
|
171
|
+
socketTimeout: IMAP_TIMEOUT_MS,
|
|
172
|
+
greetTimeout: IMAP_TIMEOUT_MS,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await client.connect();
|
|
177
|
+
const lock = await client.getMailboxLock("INBOX");
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Search for unseen messages
|
|
181
|
+
const unseen = await client.search({ seen: false });
|
|
182
|
+
if (!unseen || unseen.length === 0) {
|
|
183
|
+
console.log(`${logPrefix} No unseen messages`);
|
|
184
|
+
writeImapCursor(account, now, lastMid, totalProcessed);
|
|
185
|
+
lock.release();
|
|
186
|
+
await client.logout();
|
|
187
|
+
return { newCount: 0, errors };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
unseenCount = unseen.length;
|
|
191
|
+
console.log(`${logPrefix} Found ${unseenCount} unseen messages`);
|
|
192
|
+
|
|
193
|
+
// Rate limit: process most recent N
|
|
194
|
+
const toFetch = unseen.length > MAX_EMAILS_PER_CYCLE
|
|
195
|
+
? unseen.slice(-MAX_EMAILS_PER_CYCLE)
|
|
196
|
+
: unseen;
|
|
197
|
+
|
|
198
|
+
if (unseen.length > MAX_EMAILS_PER_CYCLE) {
|
|
199
|
+
console.log(`${logPrefix} Rate limited to last ${MAX_EMAILS_PER_CYCLE}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Fetch messages using PEEK (don't mark as read)
|
|
203
|
+
for (const seq of toFetch) {
|
|
204
|
+
try {
|
|
205
|
+
const msg = await client.fetchOne(seq, {
|
|
206
|
+
envelope: true,
|
|
207
|
+
bodyStructure: true,
|
|
208
|
+
source: true, // Full raw source for body/attachment extraction
|
|
209
|
+
}, { uid: false });
|
|
210
|
+
|
|
211
|
+
if (!msg || !msg.envelope) continue;
|
|
212
|
+
|
|
213
|
+
const env = msg.envelope;
|
|
214
|
+
const fromAddr = env.from?.[0]?.address || "";
|
|
215
|
+
const fromName = env.from?.[0]?.name || fromAddr;
|
|
216
|
+
const subject = env.subject || "";
|
|
217
|
+
const dateStr = env.date ? env.date.toISOString() : "";
|
|
218
|
+
const messageId = env.messageId || "";
|
|
219
|
+
|
|
220
|
+
const midHash = messageIdHash(messageId, fromAddr, subject, dateStr);
|
|
221
|
+
|
|
222
|
+
if (isAlreadyProcessed(midHash, fileSuffix)) {
|
|
223
|
+
skipped++;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Parse the full message for body and attachments
|
|
228
|
+
let snippet = "";
|
|
229
|
+
let emailAttachments = [];
|
|
230
|
+
if (msg.source) {
|
|
231
|
+
try {
|
|
232
|
+
const { simpleParser } = await import("mailparser");
|
|
233
|
+
const parsed = await simpleParser(msg.source);
|
|
234
|
+
snippet = extractSnippet(parsed.text, parsed.html);
|
|
235
|
+
emailAttachments = saveAttachments(parsed, midHash);
|
|
236
|
+
} catch {
|
|
237
|
+
// Fall back to envelope-only data
|
|
238
|
+
snippet = "";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const record = {
|
|
243
|
+
received_at: now,
|
|
244
|
+
event_type: eventType,
|
|
245
|
+
source: `${account}_gmail_poll`,
|
|
246
|
+
email: {
|
|
247
|
+
from: `${fromName} <${fromAddr}>`,
|
|
248
|
+
from_name: fromName,
|
|
249
|
+
from_addr: fromAddr,
|
|
250
|
+
to: emailAddr,
|
|
251
|
+
subject,
|
|
252
|
+
date: dateStr,
|
|
253
|
+
message_id: messageId,
|
|
254
|
+
snippet,
|
|
255
|
+
},
|
|
256
|
+
attachments: emailAttachments.length > 0 ? emailAttachments : null,
|
|
257
|
+
triage_hint: null,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const filepath = join(INBOX_DIR, `${midHash}-${fileSuffix}.json`);
|
|
261
|
+
writeFileSync(filepath, JSON.stringify(record, null, 2));
|
|
262
|
+
|
|
263
|
+
newCount++;
|
|
264
|
+
lastMid = messageId;
|
|
265
|
+
console.log(`${logPrefix} New: ${fromName} <${fromAddr}> — ${subject.slice(0, 60)}`);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
errors.push(`${logPrefix} Message fetch error: ${err.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
lock.release();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
totalProcessed += newCount;
|
|
275
|
+
writeImapCursor(account, now, lastMid, totalProcessed);
|
|
276
|
+
await client.logout();
|
|
277
|
+
|
|
278
|
+
console.log(
|
|
279
|
+
`${logPrefix} Poll complete: ${newCount} new, ${skipped} skipped, ${unseenCount} unseen total`,
|
|
280
|
+
);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
errors.push(`${logPrefix} IMAP error: ${err.message}`);
|
|
283
|
+
console.error(`${logPrefix} IMAP error: ${err.message}`);
|
|
284
|
+
writeImapCursor(account, now, lastMid, totalProcessed);
|
|
285
|
+
try { await client.logout(); } catch { /* already disconnected */ }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { newCount, errors };
|
|
289
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inbox-scan-poller.mjs — generic poller for channel-bus inbox items.
|
|
3
|
+
*
|
|
4
|
+
* WS2 channels (Telegram, Baileys WhatsApp, …) deliver events through the
|
|
5
|
+
* daemon's channel bus, which serializes each to a byte-compatible inbox item
|
|
6
|
+
* via `eventToInboxItem` → `writeInboxItem("<service>", item)` under
|
|
7
|
+
* `state/inbox/<service>/*.yaml`. Unlike Slack/Gmail/Calendar/Voice, these
|
|
8
|
+
* channels have no dedicated API poller — so this scanner reads the YAML items
|
|
9
|
+
* back into the in-memory `{ items, errors }` shape the daemon's `poll()` loop
|
|
10
|
+
* already consumes, and the rest of the pipeline (classify → directed-gate →
|
|
11
|
+
* locks → dispatch) runs unchanged.
|
|
12
|
+
*
|
|
13
|
+
* The scanner is the inverse of `inboxItemToYaml` (scripts/poller/utils.mjs):
|
|
14
|
+
* it parses only the fields the daemon needs and ignores the rest. Items that
|
|
15
|
+
* have already been processed/deferred (renamed by the daemon) are skipped via
|
|
16
|
+
* the same sibling-file convention `writeInboxItem` uses, so the scanner never
|
|
17
|
+
* re-emits a handled item.
|
|
18
|
+
*
|
|
19
|
+
* Durable admission (F1 / H2): an item admitted-but-not-yet-closed is parked at
|
|
20
|
+
* `<name>.dispatched` (see `markDispatched` in utils.mjs). The scanner skips a
|
|
21
|
+
* fresh `.dispatched` file (no double-delivery on the happy path) but RECLAIMS a
|
|
22
|
+
* stale one (older than the reclaim window) — so a crash/restart that loses the
|
|
23
|
+
* daemon's in-memory queue still re-delivers the user's message instead of
|
|
24
|
+
* silently dropping it. `markInboxProcessed` retires it to `.processed` on
|
|
25
|
+
* success, which is terminal and never reclaimed.
|
|
26
|
+
*
|
|
27
|
+
* @module scripts/poller/inbox-scan-poller
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { AGENT_REPO_DIR } from "./utils.mjs";
|
|
33
|
+
|
|
34
|
+
// Reclaim window for in-flight `.dispatched` items (F1 / H2 durable admission).
|
|
35
|
+
// An item parked at `.dispatched` while its session is in-flight must NOT be
|
|
36
|
+
// re-delivered on the happy path, but a crash/restart that loses the in-memory
|
|
37
|
+
// queue must leave it re-deliverable. We reclaim a `.dispatched` file only once
|
|
38
|
+
// it is older than this window — 2h matches the daemon's STALE_LOCK_MS and
|
|
39
|
+
// comfortably exceeds every inbox session timeout, so a still-running session
|
|
40
|
+
// is never double-delivered while a crashed one is recovered within one window.
|
|
41
|
+
const DEFAULT_RECLAIM_MS = 2 * 60 * 60 * 1000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse a single inbox YAML body into the daemon's item shape. Tolerant of
|
|
45
|
+
* missing optional fields. Pure.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} body
|
|
48
|
+
* @returns {object} item
|
|
49
|
+
*/
|
|
50
|
+
export function parseInboxItemYaml(body) {
|
|
51
|
+
const scalar = (key) => {
|
|
52
|
+
const m = body.match(new RegExp(`^${key}:\\s*"?([^"\\n]*)"?\\s*$`, "m"));
|
|
53
|
+
return m ? m[1] : "";
|
|
54
|
+
};
|
|
55
|
+
const bool = (key) => /true/i.test((body.match(new RegExp(`^${key}:\\s*(\\S+)`, "m")) || [])[1] || "");
|
|
56
|
+
const nestedBool = (key) => {
|
|
57
|
+
const m = body.match(new RegExp(`^\\s{2}${key}:\\s*(\\S+)`, "m"));
|
|
58
|
+
return /true/i.test((m || [])[1] || "");
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Block scalar `content: |` — capture the indented block.
|
|
62
|
+
const blockScalar = (key) => {
|
|
63
|
+
const re = new RegExp(`^${key}:\\s*\\|\\s*\\n((?:\\s{2}.*\\n?)*)`, "m");
|
|
64
|
+
const m = body.match(re);
|
|
65
|
+
if (!m) return scalar(key);
|
|
66
|
+
return m[1].replace(/^ {2}/gm, "").replace(/\n+$/, "");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
id: scalar("id"),
|
|
71
|
+
service: scalar("service"),
|
|
72
|
+
channel: scalar("channel"),
|
|
73
|
+
channel_id: scalar("channel_id"),
|
|
74
|
+
sender: scalar("sender"),
|
|
75
|
+
sender_privilege: scalar("sender_privilege"),
|
|
76
|
+
timestamp: scalar("timestamp"),
|
|
77
|
+
subject: scalar("subject"),
|
|
78
|
+
content: blockScalar("content"),
|
|
79
|
+
thread_id: scalar("thread_id"),
|
|
80
|
+
thread_context: (() => { const tc = blockScalar("thread_context"); return tc || null; })(),
|
|
81
|
+
is_reply: bool("is_reply"),
|
|
82
|
+
kind: scalar("kind") || "message",
|
|
83
|
+
priority_signals: {
|
|
84
|
+
from_ceo: nestedBool("from_ceo"),
|
|
85
|
+
tagged_urgent: nestedBool("tagged_urgent"),
|
|
86
|
+
contains_deadline: nestedBool("contains_deadline"),
|
|
87
|
+
mentions_agent: nestedBool("mentions_agent"),
|
|
88
|
+
},
|
|
89
|
+
raw_ref: scalar("raw_ref"),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Scan `state/inbox/<service>/` for unprocessed `*.yaml` items, plus any stale
|
|
95
|
+
* `*.dispatched` items left in-flight by a crashed session (F1 / H2 reclaim).
|
|
96
|
+
*
|
|
97
|
+
* @param {string} service
|
|
98
|
+
* @param {object} [opts]
|
|
99
|
+
* @param {string} [opts.agentRoot]
|
|
100
|
+
* @param {number} [opts.reclaimMs] age (ms) after which a `.dispatched` item is
|
|
101
|
+
* considered crash-orphaned and re-delivered. Defaults to 2h (STALE_LOCK_MS).
|
|
102
|
+
* @param {number} [opts.now] injectable clock (epoch ms) for tests.
|
|
103
|
+
* @returns {{ items: object[], errors: string[] }}
|
|
104
|
+
*/
|
|
105
|
+
export function scanInbox(service, opts = {}) {
|
|
106
|
+
const agentRoot = opts.agentRoot || AGENT_REPO_DIR;
|
|
107
|
+
const reclaimMs = typeof opts.reclaimMs === "number" ? opts.reclaimMs : DEFAULT_RECLAIM_MS;
|
|
108
|
+
const now = typeof opts.now === "number" ? opts.now : Date.now();
|
|
109
|
+
const dir = join(agentRoot, "state", "inbox", service);
|
|
110
|
+
const items = [];
|
|
111
|
+
const errors = [];
|
|
112
|
+
if (!existsSync(dir)) return { items, errors };
|
|
113
|
+
|
|
114
|
+
let files;
|
|
115
|
+
try {
|
|
116
|
+
files = readdirSync(dir);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { items, errors: [`readdir ${service}: ${err.message}`] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const f of files) {
|
|
122
|
+
// Live items always emit. A `.dispatched` item is in-flight: emit it only
|
|
123
|
+
// once it is stale (crash-orphaned) so the happy path never double-delivers.
|
|
124
|
+
const isLive = f.endsWith(".yaml");
|
|
125
|
+
const isDispatched = f.endsWith(".dispatched");
|
|
126
|
+
if (!isLive && !isDispatched) continue; // skip .processed/.deferred/.bundled
|
|
127
|
+
const path = join(dir, f);
|
|
128
|
+
if (isDispatched) {
|
|
129
|
+
let ageMs;
|
|
130
|
+
try {
|
|
131
|
+
// Clamp at 0: filesystem mtime can read a hair ahead of the captured
|
|
132
|
+
// clock, which would otherwise make a just-dispatched file look like a
|
|
133
|
+
// negative (i.e. "future") age and skew the reclaim comparison.
|
|
134
|
+
ageMs = Math.max(0, now - statSync(path).mtimeMs);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
errors.push(`stat ${f}: ${err.message}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ageMs < reclaimMs) continue; // still in-flight — do not re-deliver
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const item = parseInboxItemYaml(readFileSync(path, "utf-8"));
|
|
143
|
+
if (item.id) items.push(item);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
errors.push(`parse ${f}: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { items, errors };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Build a bound poller for a service, matching the poller fn contract. */
|
|
152
|
+
export function makeInboxScanPoller(service, opts = {}) {
|
|
153
|
+
return async () => scanInbox(service, opts);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default { scanInbox, parseInboxItemYaml, makeInboxScanPoller };
|