@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,377 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Send an HTML email with file attachment via Gmail SMTP.
|
|
3
|
+
|
|
4
|
+
Usage: python3 send-email-with-attachment.py <to> <subject> <body> [--cc email] [--attachment path] [--reply-to-subject "original subject"] [--in-reply-to "<message-id>"]
|
|
5
|
+
|
|
6
|
+
Sends as the running agent (identity from config/agent.json) with branded signature block.
|
|
7
|
+
Supports PNG, PDF, and other file attachments via MIME.
|
|
8
|
+
"""
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import imaplib
|
|
12
|
+
import email
|
|
13
|
+
import smtplib
|
|
14
|
+
import mimetypes
|
|
15
|
+
from email.mime.multipart import MIMEMultipart
|
|
16
|
+
from email.mime.text import MIMEText
|
|
17
|
+
from email.mime.base import MIMEBase
|
|
18
|
+
from email.mime.image import MIMEImage
|
|
19
|
+
from email.mime.application import MIMEApplication
|
|
20
|
+
from email import encoders
|
|
21
|
+
from email.utils import formatdate
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Outbound dedup — fail-open if module not available
|
|
25
|
+
try:
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
27
|
+
from outbound_dedup import acquire_email_lock, confirm_email_lock
|
|
28
|
+
_DEDUP_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
_DEDUP_AVAILABLE = False
|
|
31
|
+
|
|
32
|
+
# LLM-based dedup — replaces old subject+recipient lock per CEO directive ib-20260406-001
|
|
33
|
+
try:
|
|
34
|
+
from llm_email_dedup import llm_dedup_check
|
|
35
|
+
_LLM_DEDUP_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
_LLM_DEDUP_AVAILABLE = False
|
|
38
|
+
|
|
39
|
+
# Pre-draft context enrichment (Phase 7) — advisory, never blocks — fail-open
|
|
40
|
+
try:
|
|
41
|
+
from pre_draft_lookup import pre_draft_lookup
|
|
42
|
+
_PRE_DRAFT_AVAILABLE = True
|
|
43
|
+
except ImportError:
|
|
44
|
+
_PRE_DRAFT_AVAILABLE = False
|
|
45
|
+
|
|
46
|
+
import json
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
_AGENT_REPO_DIR = Path(os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent)))
|
|
49
|
+
try:
|
|
50
|
+
_AGENT = json.loads((_AGENT_REPO_DIR / "config" / "agent.json").read_text())
|
|
51
|
+
except Exception:
|
|
52
|
+
_AGENT = {"firstName": "Agent", "fullName": "Agent", "email": "agent@example.com", "title": "Agent", "phone": ""}
|
|
53
|
+
|
|
54
|
+
USER = _AGENT.get("email", "agent@example.com")
|
|
55
|
+
LOGO_URL = os.environ.get(
|
|
56
|
+
"EMAIL_SIGNATURE_LOGO_URL",
|
|
57
|
+
"https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_password():
|
|
62
|
+
"""Load Gmail app password from environment or .env file."""
|
|
63
|
+
pw = os.environ.get("GMAIL_APP_PASSWORD", "")
|
|
64
|
+
if pw:
|
|
65
|
+
return pw
|
|
66
|
+
# Try loading from .env file
|
|
67
|
+
env_path = Path(__file__).parent.parent / ".env"
|
|
68
|
+
if env_path.exists():
|
|
69
|
+
with open(env_path) as f:
|
|
70
|
+
for line in f:
|
|
71
|
+
line = line.strip()
|
|
72
|
+
if line.startswith("GMAIL_APP_PASSWORD="):
|
|
73
|
+
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
PASSWORD = load_password()
|
|
78
|
+
if not PASSWORD:
|
|
79
|
+
print("Error: GMAIL_APP_PASSWORD not set in environment or .env file.")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_thread_message_ids(subject_search):
|
|
84
|
+
"""Fetch real Message-IDs from Gmail via IMAP for threading.
|
|
85
|
+
|
|
86
|
+
Searches BOTH Inbox and Sent Mail to find the full thread chain.
|
|
87
|
+
Gmail threads by In-Reply-To/References headers, so we need all
|
|
88
|
+
Message-IDs from the conversation regardless of folder.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
mail = imaplib.IMAP4_SSL("imap.gmail.com")
|
|
92
|
+
mail.login(USER, PASSWORD)
|
|
93
|
+
|
|
94
|
+
clean_subject = subject_search.replace("Re: ", "").replace("Fwd: ", "").strip()
|
|
95
|
+
msg_ids = []
|
|
96
|
+
seen = set()
|
|
97
|
+
|
|
98
|
+
# Search across both INBOX and Sent Mail
|
|
99
|
+
for folder in ["INBOX", "[Gmail]/Sent Mail"]:
|
|
100
|
+
try:
|
|
101
|
+
mail.select(folder)
|
|
102
|
+
status, messages = mail.search(None, f'(SUBJECT "{clean_subject}")')
|
|
103
|
+
if status == "OK" and messages[0]:
|
|
104
|
+
for num in messages[0].split():
|
|
105
|
+
status, data = mail.fetch(num, '(RFC822.HEADER)')
|
|
106
|
+
if status == "OK":
|
|
107
|
+
hdr = email.message_from_string(data[0][1].decode('utf-8', errors='replace'))
|
|
108
|
+
mid = hdr.get('Message-ID', '')
|
|
109
|
+
if mid and mid not in seen:
|
|
110
|
+
msg_ids.append(mid)
|
|
111
|
+
seen.add(mid)
|
|
112
|
+
except Exception:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
mail.logout()
|
|
116
|
+
return msg_ids
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"Warning: Could not fetch thread IDs: {e}")
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def mark_as_read(sender):
|
|
123
|
+
"""Mark emails from sender as read."""
|
|
124
|
+
try:
|
|
125
|
+
mail = imaplib.IMAP4_SSL("imap.gmail.com")
|
|
126
|
+
mail.login(USER, PASSWORD)
|
|
127
|
+
mail.select("INBOX")
|
|
128
|
+
status, messages = mail.search(None, f'(UNSEEN FROM "{sender}")')
|
|
129
|
+
if status == "OK" and messages[0]:
|
|
130
|
+
for num in messages[0].split():
|
|
131
|
+
mail.store(num, '+FLAGS', '\\Seen')
|
|
132
|
+
mail.logout()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"Warning: Could not mark emails as read: {e}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_signature_html():
|
|
138
|
+
full_name = _AGENT.get("fullName", "Agent")
|
|
139
|
+
title = _AGENT.get("title", "")
|
|
140
|
+
phone = _AGENT.get("phone", "")
|
|
141
|
+
phone_line = f'<div style="font-size:x-small;color:#000;">{phone}</div>\n<div><br></div>\n' if phone else ""
|
|
142
|
+
address = os.environ.get(
|
|
143
|
+
"EMAIL_SIGNATURE_ADDRESS",
|
|
144
|
+
"Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE",
|
|
145
|
+
)
|
|
146
|
+
return f"""<div>
|
|
147
|
+
<div style="font-size:small;"><b>{full_name}</b></div>
|
|
148
|
+
<div style="font-size:small;">{title}</div>
|
|
149
|
+
<div><br></div>
|
|
150
|
+
<div><img src="{LOGO_URL}" width="125"></div>
|
|
151
|
+
<div><br></div>
|
|
152
|
+
{phone_line}<div style="font-size:x-small;color:#000;">{address}</div>
|
|
153
|
+
<div style="font-size:x-small;color:#666;">_________________________________________</div>
|
|
154
|
+
<div style="font-size:x-small;color:#999;">This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error please notify the sender. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this email. Please notify the sender immediately by email if you have received this email by mistake and delete this email from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing, or taking any action in reliance on the contents of this information is strictly prohibited.</div>
|
|
155
|
+
</div>"""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def attach_file(msg, filepath):
|
|
159
|
+
"""Attach a file to the email message."""
|
|
160
|
+
filepath = Path(filepath)
|
|
161
|
+
if not filepath.exists():
|
|
162
|
+
print(f"Error: Attachment file not found: {filepath}")
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
filename = filepath.name
|
|
166
|
+
content_type, encoding = mimetypes.guess_type(str(filepath))
|
|
167
|
+
|
|
168
|
+
if content_type is None:
|
|
169
|
+
content_type = 'application/octet-stream'
|
|
170
|
+
|
|
171
|
+
maintype, subtype = content_type.split('/', 1)
|
|
172
|
+
|
|
173
|
+
with open(filepath, 'rb') as f:
|
|
174
|
+
file_data = f.read()
|
|
175
|
+
|
|
176
|
+
if maintype == 'image':
|
|
177
|
+
attachment = MIMEImage(file_data, _subtype=subtype)
|
|
178
|
+
elif maintype == 'application':
|
|
179
|
+
attachment = MIMEApplication(file_data, _subtype=subtype)
|
|
180
|
+
else:
|
|
181
|
+
attachment = MIMEBase(maintype, subtype)
|
|
182
|
+
attachment.set_payload(file_data)
|
|
183
|
+
encoders.encode_base64(attachment)
|
|
184
|
+
|
|
185
|
+
attachment.add_header('Content-Disposition', 'attachment', filename=filename)
|
|
186
|
+
msg.attach(attachment)
|
|
187
|
+
print(f"Attached: {filename} ({len(file_data):,} bytes, {content_type})")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def check_already_sent(to, subject):
|
|
191
|
+
"""Check Gmail Sent Mail for a recent email to the same recipient with same subject.
|
|
192
|
+
Returns True if a matching email was sent in the last 24 hours."""
|
|
193
|
+
try:
|
|
194
|
+
mail = imaplib.IMAP4_SSL("imap.gmail.com")
|
|
195
|
+
mail.login(USER, PASSWORD)
|
|
196
|
+
mail.select('"[Gmail]/Sent Mail"')
|
|
197
|
+
clean_subject = subject.replace("RE: ", "").replace("Re: ", "").strip()
|
|
198
|
+
from datetime import datetime, timedelta
|
|
199
|
+
since_date = (datetime.now() - timedelta(hours=24)).strftime("%d-%b-%Y")
|
|
200
|
+
status, messages = mail.search(None, f'(SINCE "{since_date}" TO "{to}" SUBJECT "{clean_subject}")')
|
|
201
|
+
found = status == "OK" and messages[0] and len(messages[0].split()) > 0
|
|
202
|
+
count = len(messages[0].split()) if found else 0
|
|
203
|
+
mail.logout()
|
|
204
|
+
if found:
|
|
205
|
+
print(f"SENT_MAIL_DEDUP: Found {count} matching email(s) to {to} with subject containing '{clean_subject}' in last 24h", file=sys.stderr)
|
|
206
|
+
return found
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print(f"SENT_MAIL_CHECK_ERROR: {e}", file=sys.stderr)
|
|
209
|
+
return False # fail-open
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def send_email(to, subject, body, cc=None, attachment=None, reply_subject=None, in_reply_to=None):
|
|
213
|
+
# ── Pre-draft context enrichment (Phase 7) ─────────────────────
|
|
214
|
+
# Look up recipient in entity index. Advisory only — never blocks sending.
|
|
215
|
+
# Logs context to logs/audit/{date}-pre-draft-lookups.jsonl
|
|
216
|
+
if _PRE_DRAFT_AVAILABLE:
|
|
217
|
+
try:
|
|
218
|
+
_ctx = pre_draft_lookup(to, message_type="email")
|
|
219
|
+
if _ctx and _ctx.get("communication", {}).get("standing_instructions"):
|
|
220
|
+
print(f"PRE_DRAFT: {len(_ctx['communication']['standing_instructions'])} standing instruction(s) for {to}", file=sys.stderr)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
print(f"PRE_DRAFT_WARN (fail-open): {e}", file=sys.stderr)
|
|
223
|
+
|
|
224
|
+
# ── Layer 1: LLM-based dedup ──────────────────────────────────────
|
|
225
|
+
# Reads recent conversation history with the recipient, asks Claude Haiku
|
|
226
|
+
# whether this topic has already been addressed. Replaces the old
|
|
227
|
+
# subject+recipient lock per CEO directive ib-20260406-001.
|
|
228
|
+
if _LLM_DEDUP_AVAILABLE:
|
|
229
|
+
try:
|
|
230
|
+
llm_result = llm_dedup_check(to, subject, body)
|
|
231
|
+
if llm_result["skip"]:
|
|
232
|
+
print(f"DEDUP_SKIP (LLM): {llm_result['reason']} [confidence={llm_result['confidence']}, msgs_checked={llm_result['recent_messages']}]")
|
|
233
|
+
return
|
|
234
|
+
else:
|
|
235
|
+
print(f"LLM_DEDUP_OK: {llm_result['reason'][:100]} [msgs_checked={llm_result['recent_messages']}]", file=sys.stderr)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f"LLM_DEDUP_WARN (fail-open): {e}", file=sys.stderr)
|
|
238
|
+
|
|
239
|
+
# ── Layer 2: Gmail Sent folder check ─────────────────────────────
|
|
240
|
+
# Double-check against actual sent mail via IMAP
|
|
241
|
+
if check_already_sent(to, subject):
|
|
242
|
+
print(f"DEDUP_SKIP (sent mail check): Already sent to {to} with subject '{subject}' in last 24h")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# ── Layer 3: Content-hash dedup ──────────────────────────────────
|
|
246
|
+
# Outbound dedup: prevent concurrent sessions from sending the same email
|
|
247
|
+
_dedup_key = None
|
|
248
|
+
if _DEDUP_AVAILABLE:
|
|
249
|
+
try:
|
|
250
|
+
_dedup_key, _dedup_result = acquire_email_lock(to, subject, body)
|
|
251
|
+
if _dedup_result == "DEDUP_SKIP":
|
|
252
|
+
print("DEDUP_SKIP (content hash)")
|
|
253
|
+
return
|
|
254
|
+
except Exception:
|
|
255
|
+
pass # fail-open
|
|
256
|
+
|
|
257
|
+
msg = MIMEMultipart('mixed')
|
|
258
|
+
msg['From'] = f"{_AGENT.get('fullName', 'Agent')} <{USER}>"
|
|
259
|
+
msg['To'] = to
|
|
260
|
+
msg['Subject'] = subject
|
|
261
|
+
msg['Date'] = formatdate(localtime=True)
|
|
262
|
+
|
|
263
|
+
if cc:
|
|
264
|
+
msg['Cc'] = cc
|
|
265
|
+
|
|
266
|
+
# Add threading headers if replying
|
|
267
|
+
# Priority: direct --in-reply-to > subject-based search
|
|
268
|
+
if in_reply_to:
|
|
269
|
+
msg['In-Reply-To'] = in_reply_to
|
|
270
|
+
msg['References'] = in_reply_to
|
|
271
|
+
# Also search for full chain to build References
|
|
272
|
+
if reply_subject:
|
|
273
|
+
thread_ids = get_thread_message_ids(reply_subject)
|
|
274
|
+
if thread_ids:
|
|
275
|
+
all_refs = thread_ids + [in_reply_to]
|
|
276
|
+
seen = []
|
|
277
|
+
for r in all_refs:
|
|
278
|
+
if r not in seen:
|
|
279
|
+
seen.append(r)
|
|
280
|
+
msg.replace_header('References', ' '.join(seen))
|
|
281
|
+
elif reply_subject:
|
|
282
|
+
thread_ids = get_thread_message_ids(reply_subject)
|
|
283
|
+
if thread_ids:
|
|
284
|
+
msg['In-Reply-To'] = thread_ids[-1]
|
|
285
|
+
msg['References'] = ' '.join(thread_ids)
|
|
286
|
+
|
|
287
|
+
# Build HTML body with signature
|
|
288
|
+
body_html = body.replace('\n', '<br>')
|
|
289
|
+
html = f"""<html><body style="font-family:Arial,Helvetica,sans-serif;font-size:small;color:#000;">
|
|
290
|
+
<div>{body_html}</div>
|
|
291
|
+
<br>
|
|
292
|
+
{build_signature_html()}
|
|
293
|
+
</body></html>"""
|
|
294
|
+
|
|
295
|
+
# Create the alternative part for HTML body
|
|
296
|
+
body_part = MIMEMultipart('alternative')
|
|
297
|
+
body_part.attach(MIMEText(html, 'html'))
|
|
298
|
+
msg.attach(body_part)
|
|
299
|
+
|
|
300
|
+
# Attach file if provided
|
|
301
|
+
if attachment:
|
|
302
|
+
attach_file(msg, attachment)
|
|
303
|
+
|
|
304
|
+
# Build recipient list for sending
|
|
305
|
+
recipients = [to]
|
|
306
|
+
if cc:
|
|
307
|
+
recipients.extend([addr.strip() for addr in cc.split(',')])
|
|
308
|
+
|
|
309
|
+
# Send
|
|
310
|
+
with smtplib.SMTP('smtp.gmail.com', 587) as server:
|
|
311
|
+
server.starttls()
|
|
312
|
+
server.login(USER, PASSWORD)
|
|
313
|
+
server.send_message(msg, to_addrs=recipients)
|
|
314
|
+
|
|
315
|
+
# Mark sender's emails as read
|
|
316
|
+
sender_domain = to.split('@')[-1] if '@' in to else ''
|
|
317
|
+
if sender_domain:
|
|
318
|
+
mark_as_read(to)
|
|
319
|
+
|
|
320
|
+
cc_note = f" (CC: {cc})" if cc else ""
|
|
321
|
+
attachment_note = f" [Attachment: {Path(attachment).name}]" if attachment else ""
|
|
322
|
+
print(f"Sent to {to}{cc_note}, subject: {subject}{attachment_note}")
|
|
323
|
+
|
|
324
|
+
# Post-send: confirm content-hash lock (audit trail)
|
|
325
|
+
if _DEDUP_AVAILABLE:
|
|
326
|
+
try:
|
|
327
|
+
confirm_email_lock(_dedup_key, f"To:{to} Subj:{subject}", to=to, subject=subject)
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def parse_args():
|
|
333
|
+
"""Parse command-line arguments."""
|
|
334
|
+
if len(sys.argv) < 4:
|
|
335
|
+
print("Usage: send-email-with-attachment.py <to> <subject> <body> [--cc email] [--attachment path] [--reply-to-subject 'subject']")
|
|
336
|
+
print()
|
|
337
|
+
print("Arguments:")
|
|
338
|
+
print(" to Recipient email address")
|
|
339
|
+
print(" subject Email subject line")
|
|
340
|
+
print(" body Email body (plain text, newlines converted to <br>)")
|
|
341
|
+
print(" --cc CC email address(es), comma-separated")
|
|
342
|
+
print(" --attachment Path to file to attach (PNG, PDF, etc.)")
|
|
343
|
+
print(" --reply-to-subject Subject of original thread (for threading)")
|
|
344
|
+
sys.exit(1)
|
|
345
|
+
|
|
346
|
+
to = sys.argv[1]
|
|
347
|
+
subject = sys.argv[2]
|
|
348
|
+
body = sys.argv[3]
|
|
349
|
+
cc = None
|
|
350
|
+
attachment = None
|
|
351
|
+
reply_subject = None
|
|
352
|
+
in_reply_to = None
|
|
353
|
+
|
|
354
|
+
i = 4
|
|
355
|
+
while i < len(sys.argv):
|
|
356
|
+
if sys.argv[i] == '--cc' and i + 1 < len(sys.argv):
|
|
357
|
+
cc = sys.argv[i + 1]
|
|
358
|
+
i += 2
|
|
359
|
+
elif sys.argv[i] == '--attachment' and i + 1 < len(sys.argv):
|
|
360
|
+
attachment = sys.argv[i + 1]
|
|
361
|
+
i += 2
|
|
362
|
+
elif sys.argv[i] == '--reply-to-subject' and i + 1 < len(sys.argv):
|
|
363
|
+
reply_subject = sys.argv[i + 1]
|
|
364
|
+
i += 2
|
|
365
|
+
elif sys.argv[i] == '--in-reply-to' and i + 1 < len(sys.argv):
|
|
366
|
+
in_reply_to = sys.argv[i + 1]
|
|
367
|
+
i += 2
|
|
368
|
+
else:
|
|
369
|
+
print(f"Unknown argument: {sys.argv[i]}")
|
|
370
|
+
sys.exit(1)
|
|
371
|
+
|
|
372
|
+
return to, subject, body, cc, attachment, reply_subject, in_reply_to
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == '__main__':
|
|
376
|
+
to, subject, body, cc, attachment, reply_subject, in_reply_to = parse_args()
|
|
377
|
+
send_email(to, subject, body, cc, attachment, reply_subject, in_reply_to)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# send-email.sh — Send HTML email via SMTP as the running agent
|
|
3
|
+
# Usage: ./scripts/send-email.sh <to> <subject> <body> [cc] [in-reply-to] [references]
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR_INIT="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR_INIT/.." && pwd)}"
|
|
8
|
+
if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "$AGENT_REPO_DIR/config/agent.env"
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
TO="$1"
|
|
14
|
+
SUBJECT="$2"
|
|
15
|
+
BODY="$3"
|
|
16
|
+
CC="$4"
|
|
17
|
+
IN_REPLY_TO="$5"
|
|
18
|
+
REFERENCES="$6"
|
|
19
|
+
|
|
20
|
+
if [ -z "$TO" ] || [ -z "$SUBJECT" ] || [ -z "$BODY" ]; then
|
|
21
|
+
echo "Usage: send-email.sh <to> <subject> <body> [cc] [in-reply-to] [references]"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# --- Outbound dedup: content-hash only ---
|
|
26
|
+
# Subject+recipient lock removed per CEO directive ib-20260406-001.
|
|
27
|
+
# LLM-based dedup is now handled in the Python send scripts.
|
|
28
|
+
# This shell script uses content-hash dedup only (catches identical body sends).
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
30
|
+
DEDUP_ACQUIRED=""
|
|
31
|
+
if [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
|
|
32
|
+
# Generate content-hash key and acquire lock
|
|
33
|
+
CONTENT_KEY=$("$SCRIPT_DIR/outbound-dedup.sh" generate-key email "$TO" "$SUBJECT" "$BODY" 2>/dev/null) || true
|
|
34
|
+
if [ -n "$CONTENT_KEY" ]; then
|
|
35
|
+
ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire email "$CONTENT_KEY" "${AGENT_SESSION_ID:-${SOPHIE_SESSION_ID:-${RAVI_SESSION_ID:-$$}}}" 2>/dev/null) || true
|
|
36
|
+
if [ "$ACQUIRE_RESULT" = "DEDUP_SKIP" ]; then
|
|
37
|
+
echo "DEDUP_SKIP"
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
if [ "$ACQUIRE_RESULT" = "CLAIMED" ]; then
|
|
41
|
+
DEDUP_ACQUIRED="1"
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
# If ERROR or empty, fall through (fail-open) — send anyway
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Build headers
|
|
48
|
+
HEADERS="To: ${TO}
|
|
49
|
+
From: ${AGENT_FULL_NAME:-Agent} <${AGENT_EMAIL:-agent@example.com}>
|
|
50
|
+
Subject: ${SUBJECT}
|
|
51
|
+
MIME-Version: 1.0
|
|
52
|
+
Content-Type: text/html; charset=UTF-8"
|
|
53
|
+
|
|
54
|
+
if [ -n "$CC" ]; then
|
|
55
|
+
HEADERS="${HEADERS}
|
|
56
|
+
Cc: ${CC}"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if [ -n "$IN_REPLY_TO" ]; then
|
|
60
|
+
HEADERS="${HEADERS}
|
|
61
|
+
In-Reply-To: <${IN_REPLY_TO}>"
|
|
62
|
+
if [ -n "$REFERENCES" ]; then
|
|
63
|
+
HEADERS="${HEADERS}
|
|
64
|
+
References: ${REFERENCES}"
|
|
65
|
+
else
|
|
66
|
+
HEADERS="${HEADERS}
|
|
67
|
+
References: <${IN_REPLY_TO}>"
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
LOGO_URL="${EMAIL_SIGNATURE_LOGO_URL:-https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D}"
|
|
72
|
+
SIG_ADDRESS="${EMAIL_SIGNATURE_ADDRESS:-Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE}"
|
|
73
|
+
SIG_PHONE_HTML=""
|
|
74
|
+
if [ -n "${AGENT_PHONE:-}" ]; then
|
|
75
|
+
PHONE_TEL=$(echo "$AGENT_PHONE" | tr -d ' ')
|
|
76
|
+
SIG_PHONE_HTML="<div style=\"font-size:small;\"><a href=\"tel:${PHONE_TEL}\" style=\"color:#1155cc;\">${AGENT_PHONE}</a></div>
|
|
77
|
+
<div><br></div>
|
|
78
|
+
"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Build HTML body
|
|
82
|
+
HTML_BODY="<html><body>
|
|
83
|
+
<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:small;color:#000;\">
|
|
84
|
+
$(echo "$BODY" | sed 's/$/<br>/g')
|
|
85
|
+
</div>
|
|
86
|
+
<br>
|
|
87
|
+
<div style=\"font-family:Arial,Helvetica,sans-serif;color:#000;\">
|
|
88
|
+
<div style=\"font-size:small;\"><b>${AGENT_FULL_NAME:-Agent}</b></div>
|
|
89
|
+
<div style=\"font-size:small;\">${AGENT_TITLE:-}</div>
|
|
90
|
+
<div><br></div>
|
|
91
|
+
<div><img src=\"${LOGO_URL}\" width=\"125\"></div>
|
|
92
|
+
<div><br></div>
|
|
93
|
+
${SIG_PHONE_HTML}<div style=\"font-size:x-small;color:#000;\">${SIG_ADDRESS}</div>
|
|
94
|
+
<div style=\"font-size:x-small;color:#666;\">_________________________________________</div>
|
|
95
|
+
<div><br></div>
|
|
96
|
+
<div style=\"font-size:x-small;color:#999;\">This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error please notify the sender. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this email. Please notify the sender immediately by email if you have received this email by mistake and delete this email from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing, or taking any action in reliance on the contents of this information is strictly prohibited.</div>
|
|
97
|
+
</div>
|
|
98
|
+
</body></html>"
|
|
99
|
+
|
|
100
|
+
# ── Pre-send policy gate (shared send-gate chokepoint) ──────────────
|
|
101
|
+
# Unified policy layer ABOVE the Python Layer-0 validator: banned phrases,
|
|
102
|
+
# AI-disclosure, information barriers, recipient allowlist — all in
|
|
103
|
+
# lib/comms/send-gate.mjs. Email recipients are external by default, so an
|
|
104
|
+
# unreadable policy fails CLOSED here (mirrors validate_outbound semantics).
|
|
105
|
+
SEND_GATE="$AGENT_REPO_DIR/lib/comms/send-gate.mjs"
|
|
106
|
+
if [ ! -f "$SEND_GATE" ]; then
|
|
107
|
+
SEND_GATE="$AGENT_REPO_DIR/node_modules/@cohortapp/agent-sdk/lib/comms/send-gate.mjs"
|
|
108
|
+
fi
|
|
109
|
+
if [ -f "$SEND_GATE" ]; then
|
|
110
|
+
NODE_BIN="${MAESTRO_NODE_BIN:-/opt/homebrew/bin/node}"
|
|
111
|
+
command -v "$NODE_BIN" >/dev/null 2>&1 || NODE_BIN="node"
|
|
112
|
+
# Replies by default; MAESTRO_SEND_FIRST_CONTACT=true for first contact. The
|
|
113
|
+
# truthfulness invariant (never deny being an AI) applies regardless.
|
|
114
|
+
GATE_REASON=$(printf '%s' "$BODY" | AGENT_DIR="$AGENT_REPO_DIR" "$NODE_BIN" "$SEND_GATE" \
|
|
115
|
+
--channel email --recipient "$TO" --agent-root "$AGENT_REPO_DIR" \
|
|
116
|
+
--first-contact "${MAESTRO_SEND_FIRST_CONTACT:-false}" 2>&1) || GATE_EXIT=$?
|
|
117
|
+
GATE_EXIT=${GATE_EXIT:-0}
|
|
118
|
+
if [ "$GATE_EXIT" != "0" ]; then
|
|
119
|
+
echo "SEND_GATE_BLOCK: Email to $TO blocked by policy gate" >&2
|
|
120
|
+
echo "$GATE_REASON" >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
printf "%s\n\n%s" "$HEADERS" "$HTML_BODY" | msmtp -a northwind "$TO"
|
|
126
|
+
|
|
127
|
+
# Post-send: confirm content-hash dedup lock
|
|
128
|
+
if [ -n "$DEDUP_ACQUIRED" ] && [ -x "$SCRIPT_DIR/outbound-dedup.sh" ] && [ -n "$CONTENT_KEY" ]; then
|
|
129
|
+
PREVIEW=$(printf '%s' "$BODY" | head -c 200)
|
|
130
|
+
"$SCRIPT_DIR/outbound-dedup.sh" confirm email "$CONTENT_KEY" "To:$TO Subj:$SUBJECT $PREVIEW" 2>/dev/null || true
|
|
131
|
+
fi
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# send-sms.sh — Send outbound SMS via Twilio REST API as the running agent
|
|
3
|
+
# Usage: ./scripts/send-sms.sh --to "+1234567890" --body "message text"
|
|
4
|
+
#
|
|
5
|
+
# Environment variables (from .env):
|
|
6
|
+
# TWILIO_ACCOUNT_SID — Twilio account SID
|
|
7
|
+
# TWILIO_AUTH_TOKEN — Twilio auth token
|
|
8
|
+
# TWILIO_PHONE_NUMBER — The agent's Twilio phone number
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
# Load environment
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(dirname "$SCRIPT_DIR")}"
|
|
14
|
+
|
|
15
|
+
if [ -f "$AGENT_REPO_DIR/.env" ]; then
|
|
16
|
+
source "$AGENT_REPO_DIR/.env"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Defaults
|
|
20
|
+
FROM_NUMBER="${TWILIO_PHONE_NUMBER:-+16282656712}"
|
|
21
|
+
TO=""
|
|
22
|
+
BODY=""
|
|
23
|
+
|
|
24
|
+
# Parse arguments
|
|
25
|
+
while [[ $# -gt 0 ]]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--to)
|
|
28
|
+
TO="$2"
|
|
29
|
+
shift 2
|
|
30
|
+
;;
|
|
31
|
+
--body)
|
|
32
|
+
BODY="$2"
|
|
33
|
+
shift 2
|
|
34
|
+
;;
|
|
35
|
+
--from)
|
|
36
|
+
FROM_NUMBER="$2"
|
|
37
|
+
shift 2
|
|
38
|
+
;;
|
|
39
|
+
-h|--help)
|
|
40
|
+
echo "Usage: send-sms.sh --to \"+1234567890\" --body \"message text\""
|
|
41
|
+
echo ""
|
|
42
|
+
echo "Options:"
|
|
43
|
+
echo " --to Recipient phone number (E.164 format, required)"
|
|
44
|
+
echo " --body Message text (required)"
|
|
45
|
+
echo " --from Sender phone number (default: \$TWILIO_PHONE_NUMBER or +16282656712)"
|
|
46
|
+
echo " -h Show this help"
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
echo "ERROR: Unknown argument: $1" >&2
|
|
51
|
+
echo "Usage: send-sms.sh --to \"+1234567890\" --body \"message text\"" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
# Validate required arguments
|
|
58
|
+
if [ -z "$TO" ]; then
|
|
59
|
+
echo "ERROR: --to is required" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if [ -z "$BODY" ]; then
|
|
64
|
+
echo "ERROR: --body is required" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# --- Outbound dedup: prevent concurrent sessions from sending the same SMS ---
|
|
69
|
+
DEDUP_ACQUIRED=""
|
|
70
|
+
if [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
|
|
71
|
+
DEDUP_KEY=$("$SCRIPT_DIR/outbound-dedup.sh" generate-key sms "$TO" "" "$BODY" 2>/dev/null) || true
|
|
72
|
+
if [ -n "$DEDUP_KEY" ]; then
|
|
73
|
+
ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire sms "$DEDUP_KEY" "${SOPHIE_SESSION_ID:-$$}" 2>/dev/null) || true
|
|
74
|
+
if [ "$ACQUIRE_RESULT" = "DEDUP_SKIP" ]; then
|
|
75
|
+
echo "DEDUP_SKIP: Another session is already sending this SMS"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
if [ "$ACQUIRE_RESULT" = "CLAIMED" ]; then
|
|
79
|
+
DEDUP_ACQUIRED="1"
|
|
80
|
+
fi
|
|
81
|
+
# If ERROR or empty, fall through (fail-open) — send anyway
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Validate Twilio credentials
|
|
86
|
+
if [ -z "$TWILIO_ACCOUNT_SID" ]; then
|
|
87
|
+
echo "ERROR: TWILIO_ACCOUNT_SID not set" >&2
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if [ -z "$TWILIO_AUTH_TOKEN" ]; then
|
|
92
|
+
echo "ERROR: TWILIO_AUTH_TOKEN not set" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── Pre-send policy gate (shared send-gate chokepoint) ──────────────
|
|
97
|
+
# Unified policy layer: banned phrases, AI-disclosure, information barriers,
|
|
98
|
+
# recipient allowlist — all in lib/comms/send-gate.mjs. SMS recipients are phone
|
|
99
|
+
# numbers (external), so an unreadable policy fails CLOSED here.
|
|
100
|
+
SEND_GATE="$AGENT_REPO_DIR/lib/comms/send-gate.mjs"
|
|
101
|
+
if [ ! -f "$SEND_GATE" ]; then
|
|
102
|
+
SEND_GATE="$AGENT_REPO_DIR/node_modules/@cohortapp/agent-sdk/lib/comms/send-gate.mjs"
|
|
103
|
+
fi
|
|
104
|
+
if [ -f "$SEND_GATE" ]; then
|
|
105
|
+
NODE_BIN="${MAESTRO_NODE_BIN:-/opt/homebrew/bin/node}"
|
|
106
|
+
command -v "$NODE_BIN" >/dev/null 2>&1 || NODE_BIN="node"
|
|
107
|
+
# Replies by default; MAESTRO_SEND_FIRST_CONTACT=true for first contact. The
|
|
108
|
+
# truthfulness invariant (never deny being an AI) applies regardless.
|
|
109
|
+
GATE_REASON=$(printf '%s' "$BODY" | AGENT_DIR="$AGENT_REPO_DIR" "$NODE_BIN" "$SEND_GATE" \
|
|
110
|
+
--channel sms --recipient "$TO" --agent-root "$AGENT_REPO_DIR" \
|
|
111
|
+
--first-contact "${MAESTRO_SEND_FIRST_CONTACT:-false}" 2>&1) || GATE_EXIT=$?
|
|
112
|
+
GATE_EXIT=${GATE_EXIT:-0}
|
|
113
|
+
if [ "$GATE_EXIT" != "0" ]; then
|
|
114
|
+
echo "SEND_GATE_BLOCK: SMS to $TO blocked by policy gate" >&2
|
|
115
|
+
echo "$GATE_REASON" >&2
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Send SMS via Twilio REST API
|
|
121
|
+
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
122
|
+
-X POST "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json" \
|
|
123
|
+
-u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" \
|
|
124
|
+
--data-urlencode "To=${TO}" \
|
|
125
|
+
--data-urlencode "From=${FROM_NUMBER}" \
|
|
126
|
+
--data-urlencode "Body=${BODY}")
|
|
127
|
+
|
|
128
|
+
# Split response body and HTTP status code
|
|
129
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
130
|
+
RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d')
|
|
131
|
+
|
|
132
|
+
# Extract message SID from response
|
|
133
|
+
MESSAGE_SID=$(echo "$RESPONSE_BODY" | grep -o '"sid": *"[^"]*"' | head -1 | sed 's/"sid": *"//' | sed 's/"//')
|
|
134
|
+
STATUS=$(echo "$RESPONSE_BODY" | grep -o '"status": *"[^"]*"' | head -1 | sed 's/"status": *"//' | sed 's/"//')
|
|
135
|
+
|
|
136
|
+
# Log to SMS log
|
|
137
|
+
LOG_DIR="$AGENT_REPO_DIR/logs/sms/$(date +%Y-%m-%d)"
|
|
138
|
+
mkdir -p "$LOG_DIR"
|
|
139
|
+
LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d)-sms.jsonl"
|
|
140
|
+
|
|
141
|
+
LOG_ENTRY=$(cat <<LOGEOF
|
|
142
|
+
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)","direction":"outbound","messageSid":"${MESSAGE_SID}","from":"${FROM_NUMBER}","to":"${TO}","body":"$(echo "$BODY" | head -c 200 | sed 's/"/\\"/g' | tr '\n' ' ')","http_code":"${HTTP_CODE}","status":"${STATUS}","logged_at":"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"}
|
|
143
|
+
LOGEOF
|
|
144
|
+
)
|
|
145
|
+
echo "$LOG_ENTRY" >> "$LOG_FILE"
|
|
146
|
+
|
|
147
|
+
# Log to audit trail
|
|
148
|
+
AUDIT_DIR="$AGENT_REPO_DIR/logs/audit"
|
|
149
|
+
mkdir -p "$AUDIT_DIR"
|
|
150
|
+
AUDIT_FILE="$AUDIT_DIR/$(date +%Y-%m-%d)-actions.jsonl"
|
|
151
|
+
|
|
152
|
+
AUDIT_ENTRY=$(cat <<AUDITEOF
|
|
153
|
+
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)","agent":"send-sms","action":"sms_sent","to":"${TO}","from":"${FROM_NUMBER}","body_preview":"$(echo "$BODY" | head -c 100 | sed 's/"/\\"/g' | tr '\n' ' ')","messageSid":"${MESSAGE_SID}","http_code":"${HTTP_CODE}","status":"${STATUS}"}
|
|
154
|
+
AUDITEOF
|
|
155
|
+
)
|
|
156
|
+
echo "$AUDIT_ENTRY" >> "$AUDIT_FILE"
|
|
157
|
+
|
|
158
|
+
# Output result
|
|
159
|
+
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
|
160
|
+
# Post-send: confirm the dedup lock (audit trail)
|
|
161
|
+
if [ -n "$DEDUP_ACQUIRED" ] && [ -n "$DEDUP_KEY" ] && [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
|
|
162
|
+
PREVIEW=$(printf '%s' "$BODY" | head -c 200)
|
|
163
|
+
"$SCRIPT_DIR/outbound-dedup.sh" confirm sms "$DEDUP_KEY" "To:$TO SID:$MESSAGE_SID $PREVIEW" 2>/dev/null || true
|
|
164
|
+
fi
|
|
165
|
+
echo "SMS sent successfully"
|
|
166
|
+
echo " To: $TO"
|
|
167
|
+
echo " From: $FROM_NUMBER"
|
|
168
|
+
echo " SID: $MESSAGE_SID"
|
|
169
|
+
echo " Status: $STATUS"
|
|
170
|
+
exit 0
|
|
171
|
+
else
|
|
172
|
+
echo "ERROR: SMS send failed (HTTP $HTTP_CODE)" >&2
|
|
173
|
+
echo "$RESPONSE_BODY" >&2
|
|
174
|
+
exit 1
|
|
175
|
+
fi
|