mixdog 1.2.6 → 1.3.1
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/LICENSES/simple-icons-CC0-1.0.txt +30 -0
- package/NOTICE.md +71 -9
- package/package.json +4 -1
- package/scripts/check-model-recommendations.mjs +89 -0
- package/scripts/gate-local.mjs +7 -0
- package/scripts/lib/isolated-test-env.mjs +16 -0
- package/src/agents/reviewer/AGENT.md +4 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/browser-use/SKILL.md +5 -0
- package/src/defaults/skills/memory-management/SKILL.md +4 -1
- package/src/defaults/skills/setup/references/actions.md +14 -4
- package/src/defaults/skills/setup-guide/SKILL.md +178 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent/AGENT.md +4 -0
- package/src/rules/agent-mode/AGENT_MODE.md +24 -0
- package/src/rules/agent-mode/MIXY.md +28 -0
- package/src/rules/agent-mode/ROUTINE.md +6 -0
- package/src/rules/lead/LEAD.md +7 -3
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
- package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
- package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
- package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
- package/src/runtime/attachments/store.mjs +6 -0
- package/src/runtime/browser-bridge/client.mjs +24 -0
- package/src/runtime/channels/lib/scheduler.mjs +40 -12
- package/src/runtime/channels/lib/schedules-db.mjs +71 -12
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
- package/src/runtime/channels/lib/webhook.mjs +0 -1
- package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
- package/src/runtime/channels/lib/worker-main.mjs +17 -20
- package/src/runtime/computer-bridge/client.mjs +28 -1
- package/src/runtime/connectors/gmail-app-password.mjs +690 -0
- package/src/runtime/connectors/google-mcp.mjs +446 -0
- package/src/runtime/effort-judge/judge-client.mjs +1 -1
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
- package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
- package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
- package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
- package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
- package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
- package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
- package/src/runtime/memory/tool-defs.mjs +5 -0
- package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
- package/src/runtime/shared/agent-avatar.d.mts +64 -0
- package/src/runtime/shared/agent-avatar.mjs +320 -0
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-route-config.d.mts +4 -0
- package/src/runtime/shared/agent-route-config.mjs +30 -0
- package/src/runtime/shared/agent-store.mjs +132 -0
- package/src/runtime/shared/atomic-file.mjs +14 -1
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/config.mjs +8 -1
- package/src/runtime/shared/connector-accounts.mjs +15 -0
- package/src/runtime/shared/connector-catalog.d.mts +51 -0
- package/src/runtime/shared/connector-catalog.mjs +228 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +45 -0
- package/src/runtime/shared/err-text.mjs +3 -1
- package/src/runtime/shared/error-presentation.mjs +91 -6
- package/src/runtime/shared/google-oauth-client.mjs +24 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/held-request-host.d.mts +12 -0
- package/src/runtime/shared/held-request-host.mjs +71 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +33 -0
- package/src/runtime/shared/routine-limits.d.mts +11 -0
- package/src/runtime/shared/routine-limits.mjs +21 -0
- package/src/runtime/shared/routine-trigger.d.mts +10 -0
- package/src/runtime/shared/routine-trigger.mjs +44 -0
- package/src/runtime/shared/schedule-model-ref.mjs +9 -10
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/slow-span.mjs +4 -12
- package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
- package/src/runtime/shared/transcript-metadata.mjs +10 -0
- package/src/runtime/shared/turn-changes.mjs +0 -8
- package/src/runtime/shared/usage-readout-totals.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +119 -38
- package/src/session-runtime/agent-home-delivery.mjs +826 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +133 -0
- package/src/session-runtime/agent-task-sessions.mjs +460 -0
- package/src/session-runtime/boot/apis.mjs +63 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/providers.mjs +14 -4
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- package/src/session-runtime/config-lifecycle.mjs +43 -2
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/held-requests.mjs +228 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
- package/src/session-runtime/internal-tool-executor.mjs +2 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
- package/src/session-runtime/lifecycle/shared.mjs +6 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
- package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +112 -15
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +7 -1
- package/src/session-runtime/model-route/set-route.mjs +35 -4
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/owned-automation-run.mjs +229 -0
- package/src/session-runtime/resource-api.mjs +5 -0
- package/src/session-runtime/resource-mcp-api.mjs +107 -13
- package/src/session-runtime/resource-skills-api.mjs +12 -0
- package/src/session-runtime/room-member-gate.mjs +48 -0
- package/src/session-runtime/route-resolution.mjs +5 -11
- package/src/session-runtime/routine-home-turn.mjs +156 -0
- package/src/session-runtime/routine-owner.mjs +48 -0
- package/src/session-runtime/schedule-session-run.mjs +30 -64
- package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
- package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/services/channel-admin.mjs +93 -24
- package/src/session-runtime/services/memory-access.mjs +37 -0
- package/src/session-runtime/services/provider-admin.mjs +74 -39
- package/src/session-runtime/services/schedule-access.mjs +91 -0
- package/src/session-runtime/session-create/session-options.mjs +34 -7
- package/src/session-runtime/session-hooks.mjs +16 -8
- package/src/session-runtime/setup-tool/executor.mjs +79 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
- package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/skills-api.mjs +16 -1
- package/src/session-runtime/tool-policy-refresh.mjs +249 -29
- package/src/session-runtime/turn/session-ops.mjs +8 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
- package/src/session-runtime/webhook-session-run.mjs +49 -67
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
- package/src/session-runtime/workflow-agents-api.mjs +4 -1
- package/src/standalone/daemon-agent-control.mjs +1 -8
- package/src/standalone/daemon-session-control.mjs +21 -4
- package/src/standalone/daemon.mjs +31 -5
- package/src/standalone/session-protocol.mjs +27 -0
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +234 -95
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +34 -4
- package/src/tui/session/queue-helpers.mjs +2 -1
- package/src/tui/session/session-api/extensions.mjs +4 -0
- package/src/tui/session/session-api/intake/submission.mjs +5 -0
- package/src/tui/session/session-api/integrations.mjs +8 -0
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
- package/src/tui/session/session-api/lifecycle.mjs +17 -1
- package/src/tui/session/session-api/routes.mjs +61 -5
- package/src/tui/session/session-flow/drain.mjs +12 -3
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
- package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
- package/src/tui/session/session-flow/steering.mjs +58 -24
- package/src/tui/session/tool-approval.mjs +26 -5
- package/src/tui/session/transcript-intake.mjs +8 -2
- package/src/tui/session/transcript-spill.mjs +5 -1
- package/src/tui/session/tui-steering-persist.mjs +65 -42
- package/src/tui/session/turn-live-call.mjs +64 -0
- package/src/tui/session/turn.mjs +34 -8
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
// Gmail with a Google app password instead of the Google sign-in: a small MCP
|
|
2
|
+
// server answered in-process at a reserved address (connector-catalog.mjs
|
|
3
|
+
// GMAIL_APP_PASSWORD; RFC 2606 .invalid, so no request can leave for it),
|
|
4
|
+
// like the built-in Google connectors (google-mcp.mjs). It speaks IMAP
|
|
5
|
+
// (imap.gmail.com:993, TLS) and SMTP (smtp.gmail.com:465, TLS) as the account
|
|
6
|
+
// named in the server's header, with the app password read from the keychain
|
|
7
|
+
// for each call. The password is checked with Gmail before it is saved, and
|
|
8
|
+
// never appears in a config, log, error or tool result.
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import keychain from '../../lib/keychain-cjs.cjs';
|
|
11
|
+
import { GMAIL_APP_PASSWORD, gmailAddress } from '../shared/connector-catalog.mjs';
|
|
12
|
+
import { accountInstructions } from '../shared/connector-accounts.mjs';
|
|
13
|
+
|
|
14
|
+
const PROTOCOL_VERSION = '2025-06-18';
|
|
15
|
+
export const GMAIL_IMAP = Object.freeze({ host: 'imap.gmail.com', port: 993, secure: true });
|
|
16
|
+
export const GMAIL_SMTP = Object.freeze({ host: 'smtp.gmail.com', port: 465, secure: true });
|
|
17
|
+
const TIMEOUTS = { connectionTimeout: 20_000, greetingTimeout: 15_000, socketTimeout: 60_000 };
|
|
18
|
+
const SEARCH_DEFAULT = 10;
|
|
19
|
+
const SEARCH_MAX = 25;
|
|
20
|
+
const SNIPPET_BYTES = 2048;
|
|
21
|
+
const SNIPPET_CHARS = 200;
|
|
22
|
+
const LINE_CHARS = 300;
|
|
23
|
+
const READ_BYTES = 262_144;
|
|
24
|
+
const READ_CHARS = 50_000;
|
|
25
|
+
|
|
26
|
+
const json = (status, body) =>
|
|
27
|
+
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
28
|
+
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
29
|
+
const clip = (value, max) => (value.length > max ? `${value.slice(0, max)}…` : value);
|
|
30
|
+
|
|
31
|
+
class ToolInputError extends Error {}
|
|
32
|
+
|
|
33
|
+
function requireText(args, key) {
|
|
34
|
+
const value = text(args?.[key]);
|
|
35
|
+
if (!value) throw new ToolInputError(`${key} is required`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Errors: one fixed sentence per cause, never the password ────────────────
|
|
40
|
+
const MESSAGES = {
|
|
41
|
+
REJECTED: 'Google did not accept this address and app password.',
|
|
42
|
+
UNAVAILABLE:
|
|
43
|
+
'Google refused app-password access for this account: app passwords need 2-Step Verification, and a Workspace admin can block them or IMAP.',
|
|
44
|
+
NETWORK: "Couldn't reach Gmail. Check the network connection.",
|
|
45
|
+
MISSING: 'No Gmail app password is saved on this computer. Connect Gmail again with an app password.',
|
|
46
|
+
FAILED: 'Gmail refused the request.',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** `GMAIL_APP_PASSWORD_<CODE>: <sentence>` — the code is what Desktop shows
|
|
50
|
+
* in the user's language. */
|
|
51
|
+
export class GmailAppPasswordError extends Error {
|
|
52
|
+
constructor(code, detail = '') {
|
|
53
|
+
super(`GMAIL_APP_PASSWORD_${code}: ${MESSAGES[code]}${detail ? ` (${detail})` : ''}`);
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const redact = (value, password) => (password ? String(value).split(password).join('[redacted]') : String(value));
|
|
59
|
+
|
|
60
|
+
const NETWORK_CODES = new Set([
|
|
61
|
+
'ENOTFOUND',
|
|
62
|
+
'EAI_AGAIN',
|
|
63
|
+
'ECONNREFUSED',
|
|
64
|
+
'ECONNRESET',
|
|
65
|
+
'ETIMEDOUT',
|
|
66
|
+
'EHOSTUNREACH',
|
|
67
|
+
'ENETUNREACH',
|
|
68
|
+
'EPIPE',
|
|
69
|
+
'CONNECT_TIMEOUT',
|
|
70
|
+
'GREETING_TIMEOUT',
|
|
71
|
+
'UPGRADE_TIMEOUT',
|
|
72
|
+
'NoConnection',
|
|
73
|
+
// nodemailer
|
|
74
|
+
'ESOCKET',
|
|
75
|
+
'ECONNECTION',
|
|
76
|
+
'EDNS',
|
|
77
|
+
]);
|
|
78
|
+
// Gmail's answers when an app password cannot be used here: the account's own
|
|
79
|
+
// password was typed (2-Step Verification on), a browser sign-in is demanded,
|
|
80
|
+
// or a Workspace admin turned IMAP or app passwords off.
|
|
81
|
+
const UNAVAILABLE =
|
|
82
|
+
/application-specific password|web ?login required|WEBALERT|IMAP access is disabled|not enabled for IMAP|domain administrator|5\.7\.9|5\.7\.14/i;
|
|
83
|
+
const REJECTED = /AUTHENTICATIONFAILED|Invalid credentials|Username and Password not accepted|5\.7\.8/i;
|
|
84
|
+
|
|
85
|
+
/** An IMAP/SMTP failure as a GmailAppPasswordError. */
|
|
86
|
+
function gmailError(error, password) {
|
|
87
|
+
if (error instanceof GmailAppPasswordError || error instanceof ToolInputError) return error;
|
|
88
|
+
const detail = redact([error?.response, error?.responseText, error?.message].filter(Boolean).join(' '), password);
|
|
89
|
+
const code = String(error?.code || '');
|
|
90
|
+
if (UNAVAILABLE.test(detail) || error?.responseCode === 534) return new GmailAppPasswordError('UNAVAILABLE');
|
|
91
|
+
if (
|
|
92
|
+
error?.authenticationFailed === true ||
|
|
93
|
+
code === 'EAUTH' ||
|
|
94
|
+
error?.responseCode === 535 ||
|
|
95
|
+
REJECTED.test(detail)
|
|
96
|
+
) {
|
|
97
|
+
return new GmailAppPasswordError('REJECTED');
|
|
98
|
+
}
|
|
99
|
+
if (NETWORK_CODES.has(code)) return new GmailAppPasswordError('NETWORK');
|
|
100
|
+
return new GmailAppPasswordError('FAILED', clip(detail.replace(/\s+/g, ' ').trim(), 200));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Clients (tests pass fakes with the same surface) ────────────────────────
|
|
104
|
+
export const gmailClients = {
|
|
105
|
+
async imap({ email, password }) {
|
|
106
|
+
const { ImapFlow } = await import('imapflow');
|
|
107
|
+
const client = new ImapFlow({
|
|
108
|
+
...GMAIL_IMAP,
|
|
109
|
+
auth: { user: email, pass: password },
|
|
110
|
+
logger: false,
|
|
111
|
+
disableAutoIdle: true,
|
|
112
|
+
...TIMEOUTS,
|
|
113
|
+
});
|
|
114
|
+
// A socket failure rejects the pending command; it must never surface as
|
|
115
|
+
// an unhandled 'error' event in the runtime.
|
|
116
|
+
client.on('error', () => {});
|
|
117
|
+
return client;
|
|
118
|
+
},
|
|
119
|
+
async smtp({ email, password }) {
|
|
120
|
+
const { default: nodemailer } = await import('nodemailer');
|
|
121
|
+
return nodemailer.createTransport({
|
|
122
|
+
...GMAIL_SMTP,
|
|
123
|
+
auth: { user: email, pass: password },
|
|
124
|
+
logger: false,
|
|
125
|
+
debug: false,
|
|
126
|
+
...TIMEOUTS,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Runs `work` on a logged-in IMAP connection, closed afterwards. */
|
|
132
|
+
async function withImap(ctx, work) {
|
|
133
|
+
const client = await ctx.clients.imap(ctx);
|
|
134
|
+
try {
|
|
135
|
+
await client.connect();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw gmailError(error, ctx.password);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return await work(client);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw gmailError(error, ctx.password);
|
|
143
|
+
} finally {
|
|
144
|
+
await client.logout().catch(() => client.close?.());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Throws a GmailAppPasswordError unless Gmail accepts the app password over
|
|
149
|
+
* both IMAP and SMTP. */
|
|
150
|
+
export async function verifyGmailAppPassword({ email, password }, clients = gmailClients) {
|
|
151
|
+
const ctx = { email, password, clients };
|
|
152
|
+
await withImap(ctx, async () => {});
|
|
153
|
+
const transport = await clients.smtp(ctx);
|
|
154
|
+
try {
|
|
155
|
+
await transport.verify();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw gmailError(error, password);
|
|
158
|
+
} finally {
|
|
159
|
+
transport.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The keychain account of the app password of Gmail address `email`. */
|
|
164
|
+
export function gmailAppPasswordSecretAccount(email) {
|
|
165
|
+
return `gmail-app-password.${createHash('sha256').update(gmailAddress(email)).digest('hex')}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Checks the app password with Gmail, then keeps it in the keychain only. */
|
|
169
|
+
export async function saveGmailAppPassword(
|
|
170
|
+
email,
|
|
171
|
+
password,
|
|
172
|
+
{ clients = gmailClients, credentialStore = keychain } = {}
|
|
173
|
+
) {
|
|
174
|
+
await verifyGmailAppPassword({ email, password }, clients);
|
|
175
|
+
await credentialStore.setSecretAsync(gmailAppPasswordSecretAccount(email), password);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Deletes the stored app password of `email`. */
|
|
179
|
+
export async function forgetGmailAppPassword(email, { credentialStore = keychain } = {}) {
|
|
180
|
+
await credentialStore.deleteSecretAsync(gmailAppPasswordSecretAccount(email));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Messages ────────────────────────────────────────────────────────────────
|
|
184
|
+
const addresses = (list) =>
|
|
185
|
+
(Array.isArray(list) ? list : [])
|
|
186
|
+
.map((entry) => (entry?.name ? `${entry.name} <${entry.address || ''}>` : entry?.address || ''))
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.join(', ');
|
|
189
|
+
const isoDate = (value) => {
|
|
190
|
+
const date = value ? new Date(value) : null;
|
|
191
|
+
return date && !Number.isNaN(date.getTime()) ? date.toISOString() : '';
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** All Mail (every label; Spam and Trash are not in it), read-only unless
|
|
195
|
+
* `readOnly` is false. Returns the mailbox list (its special-use folders). */
|
|
196
|
+
async function openAllMail(client, { readOnly = true } = {}) {
|
|
197
|
+
const boxes = await client.list();
|
|
198
|
+
const all = boxes.find((box) => box.specialUse === '\\All');
|
|
199
|
+
await client.mailboxOpen(all?.path || '[Gmail]/All Mail', { readOnly });
|
|
200
|
+
return boxes;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const MESSAGE_ID = /^\d{1,20}$/;
|
|
204
|
+
|
|
205
|
+
/** The UID in All Mail of Gmail message id `id` (X-GM-MSGID), or null. */
|
|
206
|
+
async function uidOf(client, id) {
|
|
207
|
+
if (!MESSAGE_ID.test(id)) throw new ToolInputError('id must be an id from search_emails');
|
|
208
|
+
const uids = (await client.search({ emailId: id }, { uid: true })) || [];
|
|
209
|
+
return uids[0] ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isAttachment = (node) =>
|
|
213
|
+
node.disposition === 'attachment' ||
|
|
214
|
+
String(node.type || '').startsWith('message/') ||
|
|
215
|
+
Boolean(node.dispositionParameters?.filename || node.parameters?.name);
|
|
216
|
+
|
|
217
|
+
/** The text/plain part (else text/html) that is the message's body. */
|
|
218
|
+
function bodyPart(root) {
|
|
219
|
+
let html = null;
|
|
220
|
+
const walk = (node) => {
|
|
221
|
+
if (!node || String(node.type || '').startsWith('message/')) return null;
|
|
222
|
+
if (node.childNodes?.length) {
|
|
223
|
+
for (const child of node.childNodes) {
|
|
224
|
+
const found = walk(child);
|
|
225
|
+
if (found) return found;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (node.disposition === 'attachment') return null;
|
|
230
|
+
const part = node.part || '1';
|
|
231
|
+
if (node.type === 'text/plain') return { part, html: false };
|
|
232
|
+
if (node.type === 'text/html' && !html) html = { part, html: true };
|
|
233
|
+
return null;
|
|
234
|
+
};
|
|
235
|
+
return walk(root) || html;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Attachments by name, type and (decoded, approximate) size only. */
|
|
239
|
+
function attachmentsOf(node, out = []) {
|
|
240
|
+
if (!node) return out;
|
|
241
|
+
if (node.childNodes?.length && !String(node.type || '').startsWith('message/')) {
|
|
242
|
+
for (const child of node.childNodes) attachmentsOf(child, out);
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
if (isAttachment(node)) {
|
|
246
|
+
const size = Number(node.size) || 0;
|
|
247
|
+
out.push({
|
|
248
|
+
name: node.dispositionParameters?.filename || node.parameters?.name || '(unnamed)',
|
|
249
|
+
type: node.type,
|
|
250
|
+
size: String(node.encoding || '').toLowerCase() === 'base64' ? Math.floor((size * 3) / 4) : size,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const ENTITIES = { nbsp: ' ', lt: '<', gt: '>', quot: '"', apos: "'", amp: '&', '#39': "'" };
|
|
257
|
+
const htmlText = (html) =>
|
|
258
|
+
html
|
|
259
|
+
.replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, ' ')
|
|
260
|
+
.replace(/<br\s*\/?>|<\/(?:p|div|tr|li|h\d)>/gi, '\n')
|
|
261
|
+
.replace(/<[^>]*>/g, ' ')
|
|
262
|
+
.replace(/&(nbsp|lt|gt|quot|apos|amp|#39);/g, (_, name) => ENTITIES[name])
|
|
263
|
+
.replace(/[ \t]+/g, ' ')
|
|
264
|
+
.replace(/ *\n\s*/g, '\n')
|
|
265
|
+
.trim();
|
|
266
|
+
|
|
267
|
+
/** Up to `maxBytes` of the body text, decoded to UTF-8. */
|
|
268
|
+
async function bodyText(client, uid, structure, maxBytes) {
|
|
269
|
+
const picked = bodyPart(structure);
|
|
270
|
+
if (!picked) return '';
|
|
271
|
+
const download = await client.download(String(uid), picked.part, {
|
|
272
|
+
uid: true,
|
|
273
|
+
maxBytes,
|
|
274
|
+
chunkSize: Math.min(maxBytes, 65_536),
|
|
275
|
+
});
|
|
276
|
+
if (!download?.content) return '';
|
|
277
|
+
const chunks = [];
|
|
278
|
+
let size = 0;
|
|
279
|
+
for await (const chunk of download.content) {
|
|
280
|
+
chunks.push(chunk);
|
|
281
|
+
size += chunk.length;
|
|
282
|
+
if (size >= maxBytes) break;
|
|
283
|
+
}
|
|
284
|
+
const value = Buffer.concat(chunks).subarray(0, maxBytes).toString('utf8');
|
|
285
|
+
return picked.html ? htmlText(value) : value.trim();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const references = (headers) =>
|
|
289
|
+
String(headers || '')
|
|
290
|
+
.replace(/\r?\n[ \t]+/g, ' ')
|
|
291
|
+
.match(/<[^<>\s]+>/g) || [];
|
|
292
|
+
|
|
293
|
+
/** The Message-ID, References and subject a reply to `value` threads on. */
|
|
294
|
+
async function threadOf(ctx, value) {
|
|
295
|
+
if (/^<[^<>\s]+>$/.test(value)) return { messageId: value, references: [value], subject: '' };
|
|
296
|
+
return withImap(ctx, async (client) => {
|
|
297
|
+
await openAllMail(client);
|
|
298
|
+
const uid = await uidOf(client, value);
|
|
299
|
+
const [message] = uid
|
|
300
|
+
? await client.fetchAll(String(uid), { uid: true, envelope: true, headers: ['references'] }, { uid: true })
|
|
301
|
+
: [];
|
|
302
|
+
const messageId = text(message?.envelope?.messageId);
|
|
303
|
+
if (!messageId) throw new ToolInputError(`No email with id ${value}.`);
|
|
304
|
+
return {
|
|
305
|
+
messageId,
|
|
306
|
+
references: [...references(message.headers).filter((ref) => ref !== messageId), messageId],
|
|
307
|
+
subject: text(message.envelope.subject),
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const NO_HEADER_BREAKS = /[\r\n]/;
|
|
313
|
+
|
|
314
|
+
// ── Mailbox management ──────────────────────────────────────────────────────
|
|
315
|
+
const MANAGE_MAX = 50;
|
|
316
|
+
const LABEL_MAX = 225;
|
|
317
|
+
|
|
318
|
+
/** The distinct message ids of `args.ids` (an array, or one id). */
|
|
319
|
+
function messageIds(args) {
|
|
320
|
+
const raw = Array.isArray(args.ids) ? args.ids : args.ids == null ? [] : [args.ids];
|
|
321
|
+
const ids = [...new Set(raw.map((id) => text(id)))];
|
|
322
|
+
if (!ids.length || ids.some((id) => !id)) throw new ToolInputError('ids must list ids from search_emails');
|
|
323
|
+
if (ids.length > MANAGE_MAX) throw new ToolInputError(`At most ${MANAGE_MAX} emails per call; got ${ids.length}.`);
|
|
324
|
+
return ids;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** A user label name: one line, not a system label (`\\Inbox`, …). */
|
|
328
|
+
function labelName(args) {
|
|
329
|
+
const label = requireText(args, 'label');
|
|
330
|
+
if (
|
|
331
|
+
label.length > LABEL_MAX ||
|
|
332
|
+
[...label].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) ||
|
|
333
|
+
label.startsWith('\\')
|
|
334
|
+
) {
|
|
335
|
+
throw new ToolInputError('label must be a plain label name');
|
|
336
|
+
}
|
|
337
|
+
return label;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Runs `act(client, range, boxes)` on the All Mail UIDs of the message ids
|
|
341
|
+
* (opened read-write); returns the summary line. */
|
|
342
|
+
async function manageEmails(ctx, args, verb, act, extra = () => undefined) {
|
|
343
|
+
const ids = messageIds(args);
|
|
344
|
+
const detail = extra(args);
|
|
345
|
+
return withImap(ctx, async (client) => {
|
|
346
|
+
const boxes = await openAllMail(client, { readOnly: false });
|
|
347
|
+
const uids = [];
|
|
348
|
+
const missing = [];
|
|
349
|
+
for (const id of ids) {
|
|
350
|
+
const uid = await uidOf(client, id);
|
|
351
|
+
if (uid) uids.push(uid);
|
|
352
|
+
else missing.push(id);
|
|
353
|
+
}
|
|
354
|
+
if (!uids.length) throw new ToolInputError(`No email with id ${missing.join(', ')}.`);
|
|
355
|
+
await act(client, uids.join(','), boxes, detail);
|
|
356
|
+
const done = `${verb} ${uids.length} email${uids.length === 1 ? '' : 's'}.`;
|
|
357
|
+
return missing.length ? `${done} Not found: ${missing.join(', ')}.` : done;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const idsSchema = {
|
|
362
|
+
type: 'array',
|
|
363
|
+
items: { type: 'string' },
|
|
364
|
+
minItems: 1,
|
|
365
|
+
maxItems: MANAGE_MAX,
|
|
366
|
+
description: `Email ids from search_emails (at most ${MANAGE_MAX}).`,
|
|
367
|
+
};
|
|
368
|
+
const manageTool = (name, description, verb, act, { label = false, destructive = false } = {}) => ({
|
|
369
|
+
name,
|
|
370
|
+
description,
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
properties: {
|
|
374
|
+
ids: idsSchema,
|
|
375
|
+
...(label ? { label: { type: 'string', description: 'A label name; created when new.' } } : {}),
|
|
376
|
+
},
|
|
377
|
+
required: label ? ['ids', 'label'] : ['ids'],
|
|
378
|
+
},
|
|
379
|
+
annotations: { readOnlyHint: false, destructiveHint: destructive, idempotentHint: true, openWorldHint: true },
|
|
380
|
+
run: (ctx, args) => manageEmails(ctx, args, verb, act, label ? labelName : undefined),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const byUid = { uid: true };
|
|
384
|
+
const flag = (name, add) => (client, range) =>
|
|
385
|
+
add ? client.messageFlagsAdd(range, [name], byUid) : client.messageFlagsRemove(range, [name], byUid);
|
|
386
|
+
const gmailLabel = (add) => (client, range, _boxes, label) =>
|
|
387
|
+
add
|
|
388
|
+
? client.messageFlagsAdd(range, [label], { ...byUid, useLabels: true })
|
|
389
|
+
: client.messageFlagsRemove(range, [label], { ...byUid, useLabels: true });
|
|
390
|
+
|
|
391
|
+
const MANAGE_TOOLS = [
|
|
392
|
+
manageTool(
|
|
393
|
+
'archive_emails',
|
|
394
|
+
'Archive emails (remove them from the Inbox; they stay in All Mail).',
|
|
395
|
+
'Archived',
|
|
396
|
+
(client, range) => client.messageFlagsRemove(range, ['\\Inbox'], { ...byUid, useLabels: true })
|
|
397
|
+
),
|
|
398
|
+
manageTool(
|
|
399
|
+
'trash_emails',
|
|
400
|
+
'Move emails to Gmail Trash (recoverable for 30 days; never a permanent delete).',
|
|
401
|
+
'Moved to Trash:',
|
|
402
|
+
async (client, range, boxes) => {
|
|
403
|
+
const trash = boxes.find((box) => box.specialUse === '\\Trash');
|
|
404
|
+
await client.messageMove(range, trash?.path || '[Gmail]/Trash', byUid);
|
|
405
|
+
},
|
|
406
|
+
{ destructive: true }
|
|
407
|
+
),
|
|
408
|
+
manageTool('add_email_label', 'Add a Gmail label to emails.', 'Labeled', gmailLabel(true), { label: true }),
|
|
409
|
+
manageTool('remove_email_label', 'Remove a Gmail label from emails.', 'Unlabeled', gmailLabel(false), {
|
|
410
|
+
label: true,
|
|
411
|
+
}),
|
|
412
|
+
manageTool('mark_emails_read', 'Mark emails as read.', 'Marked read:', flag('\\Seen', true)),
|
|
413
|
+
manageTool('mark_emails_unread', 'Mark emails as unread.', 'Marked unread:', flag('\\Seen', false)),
|
|
414
|
+
manageTool('star_emails', 'Star emails.', 'Starred', flag('\\Flagged', true)),
|
|
415
|
+
manageTool('unstar_emails', 'Remove the star from emails.', 'Unstarred', flag('\\Flagged', false)),
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
const TOOLS = [
|
|
419
|
+
{
|
|
420
|
+
name: 'send_email',
|
|
421
|
+
description: 'Send a plain-text email from the connected Gmail account.',
|
|
422
|
+
inputSchema: {
|
|
423
|
+
type: 'object',
|
|
424
|
+
properties: {
|
|
425
|
+
to: { type: 'string', description: 'Recipients, comma-separated.' },
|
|
426
|
+
subject: { type: 'string', description: 'Empty in a reply: "Re: " and the original subject.' },
|
|
427
|
+
body: { type: 'string' },
|
|
428
|
+
cc: { type: 'string' },
|
|
429
|
+
bcc: { type: 'string' },
|
|
430
|
+
inReplyTo: {
|
|
431
|
+
type: 'string',
|
|
432
|
+
description: 'Reply in the thread of this email: an id from search_emails, or a Message-ID like <…>.',
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
required: ['to', 'subject', 'body'],
|
|
436
|
+
},
|
|
437
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
438
|
+
async run(ctx, args) {
|
|
439
|
+
const fields = {
|
|
440
|
+
to: requireText(args, 'to'),
|
|
441
|
+
cc: text(args.cc),
|
|
442
|
+
bcc: text(args.bcc),
|
|
443
|
+
subject: text(args.subject),
|
|
444
|
+
};
|
|
445
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
446
|
+
if (value && NO_HEADER_BREAKS.test(value)) throw new ToolInputError(`${key} must be one line`);
|
|
447
|
+
}
|
|
448
|
+
const message = { from: ctx.email, to: fields.to, subject: fields.subject, text: String(args.body ?? '') };
|
|
449
|
+
if (fields.cc) message.cc = fields.cc;
|
|
450
|
+
if (fields.bcc) message.bcc = fields.bcc;
|
|
451
|
+
const replyTo = text(args.inReplyTo);
|
|
452
|
+
if (replyTo) {
|
|
453
|
+
const original = await threadOf(ctx, replyTo);
|
|
454
|
+
message.inReplyTo = original.messageId;
|
|
455
|
+
message.references = original.references;
|
|
456
|
+
if (!message.subject && original.subject) {
|
|
457
|
+
message.subject = /^re:/i.test(original.subject) ? original.subject : `Re: ${original.subject}`;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const transport = await ctx.clients.smtp(ctx);
|
|
461
|
+
try {
|
|
462
|
+
const sent = await transport.sendMail(message);
|
|
463
|
+
return `Sent (message ${sent?.messageId || 'accepted'}).`;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw gmailError(error, ctx.password);
|
|
466
|
+
} finally {
|
|
467
|
+
transport.close();
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'search_emails',
|
|
473
|
+
description:
|
|
474
|
+
'Search the Gmail account with Gmail search syntax (from:, subject:, newer_than:7d, has:attachment, label:…), newest first. Spam and Trash are not searched.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
query: { type: 'string', description: 'Gmail search; default in:inbox.' },
|
|
479
|
+
maxResults: { type: 'number', description: `1-${SEARCH_MAX}, default ${SEARCH_DEFAULT}.` },
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
483
|
+
async run(ctx, args) {
|
|
484
|
+
const query = text(args.query) || 'in:inbox';
|
|
485
|
+
const limit = Math.min(SEARCH_MAX, Math.max(1, Math.trunc(Number(args.maxResults)) || SEARCH_DEFAULT));
|
|
486
|
+
return withImap(ctx, async (client) => {
|
|
487
|
+
await openAllMail(client);
|
|
488
|
+
const found = (await client.search({ gmraw: query }, { uid: true })) || [];
|
|
489
|
+
const uids = [...found].sort((a, b) => b - a).slice(0, limit);
|
|
490
|
+
if (!uids.length) return 'No emails.';
|
|
491
|
+
const messages = await client.fetchAll(
|
|
492
|
+
uids.join(','),
|
|
493
|
+
{ uid: true, envelope: true, internalDate: true, bodyStructure: true, threadId: true },
|
|
494
|
+
{ uid: true }
|
|
495
|
+
);
|
|
496
|
+
messages.sort((a, b) => b.uid - a.uid);
|
|
497
|
+
const lines = [];
|
|
498
|
+
for (const message of messages) {
|
|
499
|
+
const snippet = (await bodyText(client, message.uid, message.bodyStructure, SNIPPET_BYTES)).replace(
|
|
500
|
+
/\s+/g,
|
|
501
|
+
' '
|
|
502
|
+
);
|
|
503
|
+
lines.push(
|
|
504
|
+
JSON.stringify({
|
|
505
|
+
id: message.emailId,
|
|
506
|
+
threadId: message.threadId,
|
|
507
|
+
from: clip(addresses(message.envelope?.from), LINE_CHARS),
|
|
508
|
+
subject: clip(text(message.envelope?.subject), LINE_CHARS),
|
|
509
|
+
date: isoDate(message.envelope?.date || message.internalDate),
|
|
510
|
+
snippet: clip(snippet.trim(), SNIPPET_CHARS),
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (found.length > uids.length)
|
|
515
|
+
lines.push(`(${uids.length} of ${found.length} shown; narrow the query for others)`);
|
|
516
|
+
return lines.join('\n');
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'read_email',
|
|
522
|
+
description: 'Read one email by its id from search_emails: headers, text body, and attachment names and sizes.',
|
|
523
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
524
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
525
|
+
async run(ctx, args) {
|
|
526
|
+
const id = requireText(args, 'id');
|
|
527
|
+
return withImap(ctx, async (client) => {
|
|
528
|
+
await openAllMail(client);
|
|
529
|
+
const uid = await uidOf(client, id);
|
|
530
|
+
const [message] = uid
|
|
531
|
+
? await client.fetchAll(
|
|
532
|
+
String(uid),
|
|
533
|
+
{ uid: true, envelope: true, internalDate: true, bodyStructure: true, threadId: true },
|
|
534
|
+
{ uid: true }
|
|
535
|
+
)
|
|
536
|
+
: [];
|
|
537
|
+
if (!message) throw new ToolInputError(`No email with id ${id}.`);
|
|
538
|
+
const { envelope = {} } = message;
|
|
539
|
+
const body = await bodyText(client, message.uid, message.bodyStructure, READ_BYTES);
|
|
540
|
+
const head = {
|
|
541
|
+
id: message.emailId || id,
|
|
542
|
+
threadId: message.threadId,
|
|
543
|
+
messageId: envelope.messageId,
|
|
544
|
+
from: addresses(envelope.from),
|
|
545
|
+
to: addresses(envelope.to),
|
|
546
|
+
cc: addresses(envelope.cc) || undefined,
|
|
547
|
+
date: isoDate(envelope.date || message.internalDate),
|
|
548
|
+
subject: text(envelope.subject),
|
|
549
|
+
attachments: attachmentsOf(message.bodyStructure),
|
|
550
|
+
};
|
|
551
|
+
const clipped = body.length > READ_CHARS ? `${body.slice(0, READ_CHARS)}\n[truncated]` : body;
|
|
552
|
+
return `${JSON.stringify(head)}\n\n${clipped || '(no text body)'}`;
|
|
553
|
+
});
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: 'describe_emails',
|
|
558
|
+
description:
|
|
559
|
+
'The connected account and the sender, subject and date of emails by id (what an approval card shows). Without ids, only the account.',
|
|
560
|
+
// Callable by the approval gate only: not in tools/list, so never in the model's tool surface.
|
|
561
|
+
hidden: true,
|
|
562
|
+
inputSchema: { type: 'object', properties: { ids: idsSchema } },
|
|
563
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
564
|
+
async run(ctx, args) {
|
|
565
|
+
const ids = args.ids == null ? [] : messageIds(args);
|
|
566
|
+
const emails = [];
|
|
567
|
+
if (ids.length) {
|
|
568
|
+
await withImap(ctx, async (client) => {
|
|
569
|
+
await openAllMail(client);
|
|
570
|
+
for (const id of ids) {
|
|
571
|
+
const uid = await uidOf(client, id);
|
|
572
|
+
const [message] = uid
|
|
573
|
+
? await client.fetchAll(String(uid), { uid: true, envelope: true, internalDate: true }, { uid: true })
|
|
574
|
+
: [];
|
|
575
|
+
if (!message) continue;
|
|
576
|
+
emails.push({
|
|
577
|
+
id,
|
|
578
|
+
from: clip(addresses(message.envelope?.from), LINE_CHARS),
|
|
579
|
+
subject: clip(text(message.envelope?.subject), LINE_CHARS),
|
|
580
|
+
date: isoDate(message.envelope?.date || message.internalDate),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return JSON.stringify({ account: ctx.email, emails });
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
...MANAGE_TOOLS,
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
// ── The MCP server at the reserved address ──────────────────────────────────
|
|
592
|
+
const GMAIL_HOST = new URL(GMAIL_APP_PASSWORD.url).host;
|
|
593
|
+
|
|
594
|
+
/** The URL when it addresses the Gmail app-password server, else null. */
|
|
595
|
+
export function gmailAppPasswordService(input) {
|
|
596
|
+
let url;
|
|
597
|
+
try {
|
|
598
|
+
url = new URL(String(input));
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return url.protocol === 'https:' && url.host === GMAIL_HOST ? url : null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const textResult = (value) => ({ content: [{ type: 'text', text: value }] });
|
|
606
|
+
const errorResult = (message) => ({ content: [{ type: 'text', text: message }], isError: true });
|
|
607
|
+
|
|
608
|
+
async function rpc(session, message) {
|
|
609
|
+
const { id, method, params } = message || {};
|
|
610
|
+
const reply = (result) => ({ jsonrpc: '2.0', id, result });
|
|
611
|
+
if (method === 'initialize') {
|
|
612
|
+
if (!(await session.password())) {
|
|
613
|
+
return { jsonrpc: '2.0', id, error: { code: -32001, message: new GmailAppPasswordError('MISSING').message } };
|
|
614
|
+
}
|
|
615
|
+
return reply({
|
|
616
|
+
protocolVersion: text(params?.protocolVersion) || PROTOCOL_VERSION,
|
|
617
|
+
capabilities: { tools: { listChanged: false } },
|
|
618
|
+
serverInfo: { name: 'mixdog-gmail-app-password', version: '1.0.0' },
|
|
619
|
+
instructions: accountInstructions('Gmail', session.email),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
if (method === 'ping') return reply({});
|
|
623
|
+
if (method === 'tools/list') {
|
|
624
|
+
return reply({
|
|
625
|
+
tools: TOOLS.filter((tool) => !tool.hidden).map(({ run, hidden, ...tool }) => ({
|
|
626
|
+
...tool,
|
|
627
|
+
description: `${tool.description} (Gmail account: ${session.email})`,
|
|
628
|
+
})),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (method === 'tools/call') {
|
|
632
|
+
const tool = TOOLS.find((candidate) => candidate.name === params?.name);
|
|
633
|
+
if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } };
|
|
634
|
+
const password = await session.password();
|
|
635
|
+
if (!password) return reply(errorResult(new GmailAppPasswordError('MISSING').message));
|
|
636
|
+
const ctx = { email: session.email, password, clients: session.clients };
|
|
637
|
+
try {
|
|
638
|
+
return reply(textResult(redact(await tool.run(ctx, params?.arguments || {}), password)));
|
|
639
|
+
} catch (error) {
|
|
640
|
+
return reply(errorResult(redact(error?.message || String(error), password)));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function answer(url, init, { credentialStore, clients }) {
|
|
647
|
+
if (url.pathname !== '/mcp') return json(404, { error: 'not_found' });
|
|
648
|
+
if (String(init?.method || 'GET').toUpperCase() !== 'POST') {
|
|
649
|
+
return new Response(null, { status: 405, headers: { allow: 'POST' } });
|
|
650
|
+
}
|
|
651
|
+
let message;
|
|
652
|
+
try {
|
|
653
|
+
message = JSON.parse(typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body));
|
|
654
|
+
} catch {
|
|
655
|
+
return json(400, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
656
|
+
}
|
|
657
|
+
const email = gmailAddress(new Headers(init?.headers).get(GMAIL_APP_PASSWORD.accountHeader));
|
|
658
|
+
let stored;
|
|
659
|
+
const session = {
|
|
660
|
+
email,
|
|
661
|
+
clients,
|
|
662
|
+
password: () => {
|
|
663
|
+
stored ??= email
|
|
664
|
+
? credentialStore.getSecretAsync(gmailAppPasswordSecretAccount(email)).then((value) => value || '')
|
|
665
|
+
: Promise.resolve('');
|
|
666
|
+
return stored;
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
const batch = Array.isArray(message) ? message : [message];
|
|
670
|
+
const replies = (await Promise.all(batch.map((one) => (one?.id === undefined ? null : rpc(session, one))))).filter(
|
|
671
|
+
Boolean
|
|
672
|
+
);
|
|
673
|
+
if (!replies.length) return new Response(null, { status: 202 });
|
|
674
|
+
return json(200, Array.isArray(message) ? replies : replies[0]);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** A `fetch` that answers the Gmail app-password server in-process and sends
|
|
678
|
+
* every other request to `next`. */
|
|
679
|
+
export function withGmailAppPasswordMcp({ credentialStore = keychain, clients = gmailClients } = {}, next = fetch) {
|
|
680
|
+
return (input, init) => {
|
|
681
|
+
const url = gmailAppPasswordService(input instanceof Request ? input.url : input);
|
|
682
|
+
return url
|
|
683
|
+
? answer(
|
|
684
|
+
url,
|
|
685
|
+
input instanceof Request ? { method: input.method, headers: input.headers, body: init?.body } : init,
|
|
686
|
+
{ credentialStore, clients }
|
|
687
|
+
)
|
|
688
|
+
: next(input, init);
|
|
689
|
+
};
|
|
690
|
+
}
|