macro-agent 0.0.11 → 0.0.13
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/.macro-agent/teams/self-driving/prompts/grinder.md +27 -0
- package/.macro-agent/teams/self-driving/prompts/judge.md +27 -0
- package/.macro-agent/teams/self-driving/prompts/planner.md +33 -0
- package/.macro-agent/teams/self-driving/roles/grinder.yaml +17 -0
- package/.macro-agent/teams/self-driving/roles/judge.yaml +24 -0
- package/.macro-agent/teams/self-driving/roles/planner.yaml +18 -0
- package/.macro-agent/teams/self-driving/team.yaml +103 -0
- package/.macro-agent/teams/structured/prompts/developer.md +26 -0
- package/.macro-agent/teams/structured/prompts/lead.md +25 -0
- package/.macro-agent/teams/structured/prompts/reviewer.md +24 -0
- package/.macro-agent/teams/structured/roles/developer.yaml +12 -0
- package/.macro-agent/teams/structured/roles/lead.yaml +11 -0
- package/.macro-agent/teams/structured/roles/reviewer.yaml +19 -0
- package/.macro-agent/teams/structured/team.yaml +89 -0
- package/.sudocode/issues.jsonl +6 -0
- package/.sudocode/specs.jsonl +7 -0
- package/CLAUDE.md +110 -30
- package/README.md +60 -3
- package/dist/acp/macro-agent.d.ts +4 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +50 -4
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/session-mapper.d.ts +20 -1
- package/dist/acp/session-mapper.d.ts.map +1 -1
- package/dist/acp/session-mapper.js +90 -1
- package/dist/acp/session-mapper.js.map +1 -1
- package/dist/acp/types.d.ts +24 -1
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +25 -1
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +93 -7
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +22 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/agent-detection/command-builder.d.ts +30 -0
- package/dist/agent-detection/command-builder.d.ts.map +1 -0
- package/dist/agent-detection/command-builder.js +71 -0
- package/dist/agent-detection/command-builder.js.map +1 -0
- package/dist/agent-detection/detector.d.ts +84 -0
- package/dist/agent-detection/detector.d.ts.map +1 -0
- package/dist/agent-detection/detector.js +240 -0
- package/dist/agent-detection/detector.js.map +1 -0
- package/dist/agent-detection/index.d.ts +12 -0
- package/dist/agent-detection/index.d.ts.map +1 -0
- package/dist/agent-detection/index.js +14 -0
- package/dist/agent-detection/index.js.map +1 -0
- package/dist/agent-detection/registry.d.ts +53 -0
- package/dist/agent-detection/registry.d.ts.map +1 -0
- package/dist/agent-detection/registry.js +177 -0
- package/dist/agent-detection/registry.js.map +1 -0
- package/dist/agent-detection/types.d.ts +121 -0
- package/dist/agent-detection/types.d.ts.map +1 -0
- package/dist/agent-detection/types.js +20 -0
- package/dist/agent-detection/types.js.map +1 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +95 -0
- package/dist/api/server.js.map +1 -1
- package/dist/cli/index.js +29 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.js +38 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/project-config.d.ts +46 -0
- package/dist/config/project-config.d.ts.map +1 -0
- package/dist/config/project-config.js +68 -0
- package/dist/config/project-config.js.map +1 -0
- package/dist/lifecycle/cascade.d.ts +1 -1
- package/dist/lifecycle/cascade.d.ts.map +1 -1
- package/dist/lifecycle/handlers/index.d.ts +4 -0
- package/dist/lifecycle/handlers/index.d.ts.map +1 -1
- package/dist/lifecycle/handlers/index.js +2 -0
- package/dist/lifecycle/handlers/index.js.map +1 -1
- package/dist/lifecycle/handlers/worker.d.ts +4 -0
- package/dist/lifecycle/handlers/worker.d.ts.map +1 -1
- package/dist/lifecycle/handlers/worker.js +35 -3
- package/dist/lifecycle/handlers/worker.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +32 -2
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/event-translator.d.ts.map +1 -1
- package/dist/map/adapter/event-translator.js +1 -0
- package/dist/map/adapter/event-translator.js.map +1 -1
- package/dist/map/adapter/extensions/agent-detection.d.ts +49 -0
- package/dist/map/adapter/extensions/agent-detection.d.ts.map +1 -0
- package/dist/map/adapter/extensions/agent-detection.js +91 -0
- package/dist/map/adapter/extensions/agent-detection.js.map +1 -0
- package/dist/map/adapter/extensions/index.d.ts +10 -1
- package/dist/map/adapter/extensions/index.d.ts.map +1 -1
- package/dist/map/adapter/extensions/index.js +39 -0
- package/dist/map/adapter/extensions/index.js.map +1 -1
- package/dist/map/adapter/extensions/resume.d.ts +47 -0
- package/dist/map/adapter/extensions/resume.d.ts.map +1 -0
- package/dist/map/adapter/extensions/resume.js +59 -0
- package/dist/map/adapter/extensions/resume.js.map +1 -0
- package/dist/map/adapter/extensions/workspace-files.d.ts +42 -0
- package/dist/map/adapter/extensions/workspace-files.d.ts.map +1 -0
- package/dist/map/adapter/extensions/workspace-files.js +338 -0
- package/dist/map/adapter/extensions/workspace-files.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +6 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +45 -0
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/tools/claim_task.d.ts +35 -0
- package/dist/mcp/tools/claim_task.d.ts.map +1 -0
- package/dist/mcp/tools/claim_task.js +58 -0
- package/dist/mcp/tools/claim_task.js.map +1 -0
- package/dist/mcp/tools/done.d.ts +11 -2
- package/dist/mcp/tools/done.d.ts.map +1 -1
- package/dist/mcp/tools/done.js +15 -10
- package/dist/mcp/tools/done.js.map +1 -1
- package/dist/mcp/tools/list_claimable_tasks.d.ts +38 -0
- package/dist/mcp/tools/list_claimable_tasks.d.ts.map +1 -0
- package/dist/mcp/tools/list_claimable_tasks.js +63 -0
- package/dist/mcp/tools/list_claimable_tasks.js.map +1 -0
- package/dist/mcp/tools/unclaim_task.d.ts +31 -0
- package/dist/mcp/tools/unclaim_task.d.ts.map +1 -0
- package/dist/mcp/tools/unclaim_task.js +47 -0
- package/dist/mcp/tools/unclaim_task.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +2 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/metrics.d.ts +79 -0
- package/dist/metrics/metrics.d.ts.map +1 -0
- package/dist/metrics/metrics.js +166 -0
- package/dist/metrics/metrics.js.map +1 -0
- package/dist/roles/capabilities.d.ts +1 -0
- package/dist/roles/capabilities.d.ts.map +1 -1
- package/dist/roles/capabilities.js +3 -0
- package/dist/roles/capabilities.js.map +1 -1
- package/dist/roles/types.d.ts +1 -1
- package/dist/roles/types.d.ts.map +1 -1
- package/dist/router/message-router.d.ts +41 -0
- package/dist/router/message-router.d.ts.map +1 -1
- package/dist/router/message-router.js +136 -5
- package/dist/router/message-router.js.map +1 -1
- package/dist/store/event-store.d.ts +8 -1
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +120 -4
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/types/agents.d.ts +1 -1
- package/dist/store/types/agents.d.ts.map +1 -1
- package/dist/store/types/events.d.ts +1 -1
- package/dist/store/types/events.d.ts.map +1 -1
- package/dist/store/types/events.js.map +1 -1
- package/dist/store/types/index.d.ts +1 -0
- package/dist/store/types/index.d.ts.map +1 -1
- package/dist/store/types/index.js +1 -0
- package/dist/store/types/index.js.map +1 -1
- package/dist/store/types/sessions.d.ts +44 -0
- package/dist/store/types/sessions.d.ts.map +1 -0
- package/dist/store/types/sessions.js +9 -0
- package/dist/store/types/sessions.js.map +1 -0
- package/dist/store/types/tasks.d.ts +2 -0
- package/dist/store/types/tasks.d.ts.map +1 -1
- package/dist/task/backend/memory.d.ts +4 -1
- package/dist/task/backend/memory.d.ts.map +1 -1
- package/dist/task/backend/memory.js +81 -0
- package/dist/task/backend/memory.js.map +1 -1
- package/dist/task/backend/types.d.ts +30 -0
- package/dist/task/backend/types.d.ts.map +1 -1
- package/dist/task/backend/types.js.map +1 -1
- package/dist/teams/index.d.ts +4 -0
- package/dist/teams/index.d.ts.map +1 -0
- package/dist/teams/index.js +4 -0
- package/dist/teams/index.js.map +1 -0
- package/dist/teams/team-loader.d.ts +20 -0
- package/dist/teams/team-loader.d.ts.map +1 -0
- package/dist/teams/team-loader.js +293 -0
- package/dist/teams/team-loader.js.map +1 -0
- package/dist/teams/team-runtime.d.ts +139 -0
- package/dist/teams/team-runtime.d.ts.map +1 -0
- package/dist/teams/team-runtime.js +613 -0
- package/dist/teams/team-runtime.js.map +1 -0
- package/dist/teams/types.d.ts +266 -0
- package/dist/teams/types.d.ts.map +1 -0
- package/dist/teams/types.js +20 -0
- package/dist/teams/types.js.map +1 -0
- package/dist/workspace/dataplane-adapter.d.ts +1 -1
- package/dist/workspace/dataplane-adapter.d.ts.map +1 -1
- package/dist/workspace/dataplane-adapter.js +1 -1
- package/dist/workspace/dataplane-adapter.js.map +1 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/strategies/index.d.ts +6 -0
- package/dist/workspace/strategies/index.d.ts.map +1 -0
- package/dist/workspace/strategies/index.js +5 -0
- package/dist/workspace/strategies/index.js.map +1 -0
- package/dist/workspace/strategies/optimistic.d.ts +26 -0
- package/dist/workspace/strategies/optimistic.d.ts.map +1 -0
- package/dist/workspace/strategies/optimistic.js +121 -0
- package/dist/workspace/strategies/optimistic.js.map +1 -0
- package/dist/workspace/strategies/queue.d.ts +26 -0
- package/dist/workspace/strategies/queue.d.ts.map +1 -0
- package/dist/workspace/strategies/queue.js +67 -0
- package/dist/workspace/strategies/queue.js.map +1 -0
- package/dist/workspace/strategies/registry.d.ts +37 -0
- package/dist/workspace/strategies/registry.d.ts.map +1 -0
- package/dist/workspace/strategies/registry.js +63 -0
- package/dist/workspace/strategies/registry.js.map +1 -0
- package/dist/workspace/strategies/trunk.d.ts +20 -0
- package/dist/workspace/strategies/trunk.d.ts.map +1 -0
- package/dist/workspace/strategies/trunk.js +108 -0
- package/dist/workspace/strategies/trunk.js.map +1 -0
- package/dist/workspace/strategies/types.d.ts +104 -0
- package/dist/workspace/strategies/types.d.ts.map +1 -0
- package/dist/workspace/strategies/types.js +11 -0
- package/dist/workspace/strategies/types.js.map +1 -0
- package/dist/workspace/types.d.ts +1 -1
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +1 -1
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/docs/implementation-details.md +1127 -0
- package/docs/implementation-summary.md +448 -0
- package/docs/plan-self-driving-support.md +433 -0
- package/docs/spec-self-driving-support.md +462 -0
- package/docs/team-templates.md +860 -0
- package/docs/teams.md +233 -0
- package/package.json +5 -3
- package/src/acp/__tests__/integration.test.ts +161 -1
- package/src/acp/__tests__/macro-agent.test.ts +95 -0
- package/src/acp/__tests__/session-persistence.test.ts +276 -0
- package/src/acp/macro-agent.ts +79 -7
- package/src/acp/session-mapper.ts +108 -1
- package/src/acp/types.ts +33 -1
- package/src/agent/agent-manager.ts +158 -6
- package/src/agent/types.ts +27 -0
- package/src/agent-detection/__tests__/command-builder.test.ts +336 -0
- package/src/agent-detection/__tests__/detector.test.ts +768 -0
- package/src/agent-detection/__tests__/registry.test.ts +254 -0
- package/src/agent-detection/command-builder.ts +90 -0
- package/src/agent-detection/detector.ts +307 -0
- package/src/agent-detection/index.ts +36 -0
- package/src/agent-detection/registry.ts +200 -0
- package/src/agent-detection/types.ts +184 -0
- package/src/api/server.ts +110 -0
- package/src/cli/index.ts +44 -0
- package/src/cli/mcp.ts +47 -0
- package/src/config/index.ts +9 -0
- package/src/config/project-config.ts +107 -0
- package/src/lifecycle/cascade.ts +1 -1
- package/src/lifecycle/handlers/index.ts +8 -0
- package/src/lifecycle/handlers/worker.ts +48 -3
- package/src/map/adapter/__tests__/extensions.test.ts +359 -0
- package/src/map/adapter/__tests__/workspace-files.test.ts +673 -0
- package/src/map/adapter/acp-over-map.ts +45 -2
- package/src/map/adapter/event-translator.ts +1 -0
- package/src/map/adapter/extensions/agent-detection.ts +201 -0
- package/src/map/adapter/extensions/index.ts +63 -0
- package/src/map/adapter/extensions/resume.ts +114 -0
- package/src/map/adapter/extensions/workspace-files.ts +449 -0
- package/src/mcp/mcp-server.ts +67 -0
- package/src/mcp/tools/claim_task.ts +86 -0
- package/src/mcp/tools/done.ts +24 -10
- package/src/mcp/tools/list_claimable_tasks.ts +93 -0
- package/src/mcp/tools/unclaim_task.ts +71 -0
- package/src/metrics/index.ts +9 -0
- package/src/metrics/metrics.ts +280 -0
- package/src/roles/capabilities.ts +3 -0
- package/src/roles/types.ts +2 -1
- package/src/router/__tests__/message-router.test.ts +561 -0
- package/src/router/message-router.ts +223 -6
- package/src/store/event-store.ts +151 -3
- package/src/store/types/agents.ts +1 -1
- package/src/store/types/events.ts +2 -1
- package/src/store/types/index.ts +1 -0
- package/src/store/types/sessions.ts +53 -0
- package/src/store/types/tasks.ts +3 -0
- package/src/task/backend/memory.ts +116 -0
- package/src/task/backend/types.ts +43 -0
- package/src/teams/__tests__/cross-subsystem.integration.test.ts +983 -0
- package/src/teams/__tests__/e2e/team-runtime.e2e.test.ts +553 -0
- package/src/teams/__tests__/team-system.test.ts +1280 -0
- package/src/teams/index.ts +13 -0
- package/src/teams/team-loader.ts +434 -0
- package/src/teams/team-runtime.ts +727 -0
- package/src/teams/types.ts +377 -0
- package/src/workspace/dataplane-adapter.ts +1 -1
- package/src/workspace/index.ts +1 -1
- package/src/workspace/strategies/index.ts +18 -0
- package/src/workspace/strategies/optimistic.ts +136 -0
- package/src/workspace/strategies/queue.ts +81 -0
- package/src/workspace/strategies/registry.ts +89 -0
- package/src/workspace/strategies/trunk.ts +123 -0
- package/src/workspace/strategies/types.ts +145 -0
- package/src/workspace/types.ts +1 -1
- package/src/workspace/workspace-manager.ts +1 -1
- package/.claude/settings.local.json +0 -59
- package/dist/map/utils/address-translation.d.ts +0 -99
- package/dist/map/utils/address-translation.d.ts.map +0 -1
- package/dist/map/utils/address-translation.js +0 -285
- package/dist/map/utils/address-translation.js.map +0 -1
- package/dist/map/utils/index.d.ts +0 -7
- package/dist/map/utils/index.d.ts.map +0 -1
- package/dist/map/utils/index.js +0 -7
- package/dist/map/utils/index.js.map +0 -1
- package/references/acp-factory-ref/CHANGELOG.md +0 -33
- package/references/acp-factory-ref/LICENSE +0 -21
- package/references/acp-factory-ref/README.md +0 -341
- package/references/acp-factory-ref/package-lock.json +0 -3102
- package/references/acp-factory-ref/package.json +0 -96
- package/references/acp-factory-ref/python/CHANGELOG.md +0 -33
- package/references/acp-factory-ref/python/LICENSE +0 -21
- package/references/acp-factory-ref/python/Makefile +0 -57
- package/references/acp-factory-ref/python/README.md +0 -253
- package/references/acp-factory-ref/python/pyproject.toml +0 -73
- package/references/acp-factory-ref/python/tests/__init__.py +0 -0
- package/references/acp-factory-ref/python/tests/e2e/__init__.py +0 -1
- package/references/acp-factory-ref/python/tests/e2e/test_codex_e2e.py +0 -349
- package/references/acp-factory-ref/python/tests/e2e/test_gemini_e2e.py +0 -165
- package/references/acp-factory-ref/python/tests/e2e/test_opencode_e2e.py +0 -296
- package/references/acp-factory-ref/python/tests/test_client_handler.py +0 -543
- package/references/acp-factory-ref/python/tests/test_pushable.py +0 -199
- package/references/claude-code-acp/.github/workflows/ci.yml +0 -45
- package/references/claude-code-acp/.github/workflows/publish.yml +0 -34
- package/references/claude-code-acp/.prettierrc.json +0 -4
- package/references/claude-code-acp/CHANGELOG.md +0 -249
- package/references/claude-code-acp/LICENSE +0 -222
- package/references/claude-code-acp/README.md +0 -53
- package/references/claude-code-acp/docs/RELEASES.md +0 -24
- package/references/claude-code-acp/eslint.config.js +0 -48
- package/references/claude-code-acp/package-lock.json +0 -4570
- package/references/claude-code-acp/package.json +0 -88
- package/references/claude-code-acp/scripts/release.sh +0 -119
- package/references/claude-code-acp/src/acp-agent.ts +0 -2065
- package/references/claude-code-acp/src/index.ts +0 -26
- package/references/claude-code-acp/src/lib.ts +0 -38
- package/references/claude-code-acp/src/mcp-server.ts +0 -911
- package/references/claude-code-acp/src/settings.ts +0 -522
- package/references/claude-code-acp/src/tests/.claude/commands/quick-math.md +0 -5
- package/references/claude-code-acp/src/tests/.claude/commands/say-hello.md +0 -6
- package/references/claude-code-acp/src/tests/acp-agent-fork.test.ts +0 -479
- package/references/claude-code-acp/src/tests/acp-agent.test.ts +0 -1502
- package/references/claude-code-acp/src/tests/extract-lines.test.ts +0 -103
- package/references/claude-code-acp/src/tests/fork-session.test.ts +0 -335
- package/references/claude-code-acp/src/tests/replace-and-calculate-location.test.ts +0 -334
- package/references/claude-code-acp/src/tests/settings.test.ts +0 -617
- package/references/claude-code-acp/src/tests/skills-options.test.ts +0 -187
- package/references/claude-code-acp/src/tests/tools.test.ts +0 -318
- package/references/claude-code-acp/src/tests/typescript-declarations.test.ts +0 -558
- package/references/claude-code-acp/src/tools.ts +0 -819
- package/references/claude-code-acp/src/utils.ts +0 -171
- package/references/claude-code-acp/tsconfig.json +0 -18
- package/references/claude-code-acp/vitest.config.ts +0 -19
- package/references/multi-agent-protocol/.sudocode/issues.jsonl +0 -111
- package/references/multi-agent-protocol/.sudocode/specs.jsonl +0 -13
- package/references/multi-agent-protocol/LICENSE +0 -21
- package/references/multi-agent-protocol/README.md +0 -113
- package/references/multi-agent-protocol/docs/00-design-specification.md +0 -496
- package/references/multi-agent-protocol/docs/01-open-questions.md +0 -1050
- package/references/multi-agent-protocol/docs/02-wire-protocol.md +0 -296
- package/references/multi-agent-protocol/docs/03-streaming-semantics.md +0 -252
- package/references/multi-agent-protocol/docs/04-error-handling.md +0 -231
- package/references/multi-agent-protocol/docs/05-connection-model.md +0 -244
- package/references/multi-agent-protocol/docs/06-visibility-permissions.md +0 -243
- package/references/multi-agent-protocol/docs/07-federation.md +0 -259
- package/references/multi-agent-protocol/docs/08-macro-agent-migration.md +0 -253
- package/references/multi-agent-protocol/docs/09-authentication.md +0 -680
- package/references/multi-agent-protocol/docs/10-mail-protocol.md +0 -553
- package/references/multi-agent-protocol/docs/agent-iam-integration.md +0 -877
- package/references/multi-agent-protocol/docs/agentic-mesh-integration-draft.md +0 -459
- package/references/multi-agent-protocol/docs/git-transport-draft.md +0 -251
- package/references/multi-agent-protocol/docs-site/Gemfile +0 -22
- package/references/multi-agent-protocol/docs-site/README.md +0 -82
- package/references/multi-agent-protocol/docs-site/_config.yml +0 -91
- package/references/multi-agent-protocol/docs-site/_includes/head_custom.html +0 -20
- package/references/multi-agent-protocol/docs-site/_sass/color_schemes/map.scss +0 -42
- package/references/multi-agent-protocol/docs-site/_sass/custom/custom.scss +0 -34
- package/references/multi-agent-protocol/docs-site/examples/full-integration.md +0 -510
- package/references/multi-agent-protocol/docs-site/examples/index.md +0 -138
- package/references/multi-agent-protocol/docs-site/examples/simple-chat.md +0 -282
- package/references/multi-agent-protocol/docs-site/examples/task-queue.md +0 -399
- package/references/multi-agent-protocol/docs-site/getting-started/index.md +0 -98
- package/references/multi-agent-protocol/docs-site/getting-started/installation.md +0 -219
- package/references/multi-agent-protocol/docs-site/getting-started/overview.md +0 -172
- package/references/multi-agent-protocol/docs-site/getting-started/quickstart.md +0 -237
- package/references/multi-agent-protocol/docs-site/index.md +0 -136
- package/references/multi-agent-protocol/docs-site/protocol/authentication.md +0 -391
- package/references/multi-agent-protocol/docs-site/protocol/connection-model.md +0 -376
- package/references/multi-agent-protocol/docs-site/protocol/design.md +0 -284
- package/references/multi-agent-protocol/docs-site/protocol/error-handling.md +0 -312
- package/references/multi-agent-protocol/docs-site/protocol/federation.md +0 -449
- package/references/multi-agent-protocol/docs-site/protocol/index.md +0 -129
- package/references/multi-agent-protocol/docs-site/protocol/permissions.md +0 -398
- package/references/multi-agent-protocol/docs-site/protocol/streaming.md +0 -353
- package/references/multi-agent-protocol/docs-site/protocol/wire-protocol.md +0 -369
- package/references/multi-agent-protocol/docs-site/sdk/api/agent.md +0 -357
- package/references/multi-agent-protocol/docs-site/sdk/api/client.md +0 -380
- package/references/multi-agent-protocol/docs-site/sdk/api/index.md +0 -62
- package/references/multi-agent-protocol/docs-site/sdk/api/server.md +0 -453
- package/references/multi-agent-protocol/docs-site/sdk/api/types.md +0 -468
- package/references/multi-agent-protocol/docs-site/sdk/guides/agent.md +0 -375
- package/references/multi-agent-protocol/docs-site/sdk/guides/authentication.md +0 -405
- package/references/multi-agent-protocol/docs-site/sdk/guides/client.md +0 -352
- package/references/multi-agent-protocol/docs-site/sdk/guides/index.md +0 -89
- package/references/multi-agent-protocol/docs-site/sdk/guides/server.md +0 -360
- package/references/multi-agent-protocol/docs-site/sdk/guides/testing.md +0 -446
- package/references/multi-agent-protocol/docs-site/sdk/guides/transports.md +0 -363
- package/references/multi-agent-protocol/docs-site/sdk/index.md +0 -206
- package/references/multi-agent-protocol/package-lock.json +0 -3886
- package/references/multi-agent-protocol/package.json +0 -56
- package/references/multi-agent-protocol/schema/meta.json +0 -467
- package/references/multi-agent-protocol/schema/schema.json +0 -2558
|
@@ -1,2065 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Agent,
|
|
3
|
-
AgentSideConnection,
|
|
4
|
-
AuthenticateRequest,
|
|
5
|
-
AvailableCommand,
|
|
6
|
-
CancelNotification,
|
|
7
|
-
ClientCapabilities,
|
|
8
|
-
ForkSessionRequest,
|
|
9
|
-
ForkSessionResponse,
|
|
10
|
-
InitializeRequest,
|
|
11
|
-
InitializeResponse,
|
|
12
|
-
LoadSessionRequest,
|
|
13
|
-
LoadSessionResponse,
|
|
14
|
-
ndJsonStream,
|
|
15
|
-
NewSessionRequest,
|
|
16
|
-
NewSessionResponse,
|
|
17
|
-
PromptRequest,
|
|
18
|
-
PromptResponse,
|
|
19
|
-
ReadTextFileRequest,
|
|
20
|
-
ReadTextFileResponse,
|
|
21
|
-
RequestError,
|
|
22
|
-
ResumeSessionRequest,
|
|
23
|
-
ResumeSessionResponse,
|
|
24
|
-
SessionModelState,
|
|
25
|
-
SessionNotification,
|
|
26
|
-
SetSessionModelRequest,
|
|
27
|
-
SetSessionModelResponse,
|
|
28
|
-
SetSessionModeRequest,
|
|
29
|
-
SetSessionModeResponse,
|
|
30
|
-
TerminalHandle,
|
|
31
|
-
TerminalOutputResponse,
|
|
32
|
-
WriteTextFileRequest,
|
|
33
|
-
WriteTextFileResponse,
|
|
34
|
-
} from "@agentclientprotocol/sdk";
|
|
35
|
-
import { SettingsManager } from "./settings.js";
|
|
36
|
-
import {
|
|
37
|
-
CanUseTool,
|
|
38
|
-
McpServerConfig,
|
|
39
|
-
Options,
|
|
40
|
-
PermissionMode,
|
|
41
|
-
Query,
|
|
42
|
-
query,
|
|
43
|
-
SDKPartialAssistantMessage,
|
|
44
|
-
SDKUserMessage,
|
|
45
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
46
|
-
import * as fs from "node:fs";
|
|
47
|
-
import * as path from "node:path";
|
|
48
|
-
import * as os from "node:os";
|
|
49
|
-
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
50
|
-
import { createMcpServer } from "./mcp-server.js";
|
|
51
|
-
import { EDIT_TOOL_NAMES, acpToolNames } from "./tools.js";
|
|
52
|
-
import {
|
|
53
|
-
toolInfoFromToolUse,
|
|
54
|
-
planEntries,
|
|
55
|
-
toolUpdateFromToolResult,
|
|
56
|
-
ClaudePlanEntry,
|
|
57
|
-
registerHookCallback,
|
|
58
|
-
createPostToolUseHook,
|
|
59
|
-
createPreToolUseHook,
|
|
60
|
-
} from "./tools.js";
|
|
61
|
-
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
|
62
|
-
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
|
|
63
|
-
import packageJson from "../package.json" with { type: "json" };
|
|
64
|
-
import { randomUUID } from "node:crypto";
|
|
65
|
-
import { fileURLToPath } from "node:url";
|
|
66
|
-
|
|
67
|
-
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE ?? path.join(os.homedir(), ".claude");
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Logger interface for customizing logging output
|
|
71
|
-
*/
|
|
72
|
-
export interface Logger {
|
|
73
|
-
log: (...args: any[]) => void;
|
|
74
|
-
error: (...args: any[]) => void;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Internal state for tracking compaction.
|
|
79
|
-
*/
|
|
80
|
-
type CompactionState = {
|
|
81
|
-
/** Whether auto-compaction is enabled for this session */
|
|
82
|
-
enabled: boolean;
|
|
83
|
-
/** Token threshold that triggers compaction */
|
|
84
|
-
threshold: number;
|
|
85
|
-
/** Custom instructions for compaction summary */
|
|
86
|
-
customInstructions?: string;
|
|
87
|
-
/** Current total token count for this session */
|
|
88
|
-
currentTokens: number;
|
|
89
|
-
/** Whether a compaction is currently in progress */
|
|
90
|
-
isCompacting: boolean;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Information about a skill available in the session.
|
|
95
|
-
*/
|
|
96
|
-
export type SkillInfo = {
|
|
97
|
-
/** Skill name */
|
|
98
|
-
name: string;
|
|
99
|
-
/** Skill description */
|
|
100
|
-
description?: string;
|
|
101
|
-
/** Source of the skill */
|
|
102
|
-
source?: "user" | "project" | "plugin";
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
type Session = {
|
|
106
|
-
query: Query;
|
|
107
|
-
input: Pushable<SDKUserMessage>;
|
|
108
|
-
cancelled: boolean;
|
|
109
|
-
permissionMode: PermissionMode;
|
|
110
|
-
settingsManager: SettingsManager;
|
|
111
|
-
abortController: AbortController;
|
|
112
|
-
cwd: string;
|
|
113
|
-
/** Optional: the actual session file path (for forked sessions where filename differs from sessionId) */
|
|
114
|
-
sessionFilePath?: string;
|
|
115
|
-
/** Auto-compaction state tracking */
|
|
116
|
-
compaction: CompactionState;
|
|
117
|
-
/** Skills loaded for this session (populated from SDK init message) */
|
|
118
|
-
skills?: SkillInfo[];
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
type BackgroundTerminal =
|
|
122
|
-
| {
|
|
123
|
-
handle: TerminalHandle;
|
|
124
|
-
status: "started";
|
|
125
|
-
lastOutput: TerminalOutputResponse | null;
|
|
126
|
-
}
|
|
127
|
-
| {
|
|
128
|
-
status: "aborted" | "exited" | "killed" | "timedOut";
|
|
129
|
-
pendingOutput: TerminalOutputResponse;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Configuration for automatic context compaction.
|
|
134
|
-
* When enabled, the session will automatically trigger compaction when token usage exceeds the threshold.
|
|
135
|
-
*/
|
|
136
|
-
export type CompactionConfig = {
|
|
137
|
-
/**
|
|
138
|
-
* Whether automatic compaction is enabled.
|
|
139
|
-
* @default false
|
|
140
|
-
*/
|
|
141
|
-
enabled: boolean;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Token threshold that triggers automatic compaction.
|
|
145
|
-
* When the total token count exceeds this value, a /compact command is automatically sent.
|
|
146
|
-
* @default 100000
|
|
147
|
-
*/
|
|
148
|
-
contextTokenThreshold?: number;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Optional custom instructions for the compaction summary.
|
|
152
|
-
* These instructions guide how Claude summarizes the conversation.
|
|
153
|
-
*/
|
|
154
|
-
customInstructions?: string;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Extra metadata that can be given to Claude Code when creating a new session.
|
|
159
|
-
*/
|
|
160
|
-
export type NewSessionMeta = {
|
|
161
|
-
claudeCode?: {
|
|
162
|
-
/**
|
|
163
|
-
* Options forwarded to Claude Code when starting a new session.
|
|
164
|
-
* Those parameters will be ignored and managed by ACP:
|
|
165
|
-
* - cwd
|
|
166
|
-
* - includePartialMessages
|
|
167
|
-
* - allowDangerouslySkipPermissions
|
|
168
|
-
* - permissionMode
|
|
169
|
-
* - canUseTool
|
|
170
|
-
* - executable
|
|
171
|
-
* Those parameters will be used and updated to work with ACP:
|
|
172
|
-
* - hooks (merged with ACP's hooks)
|
|
173
|
-
* - mcpServers (merged with ACP's mcpServers)
|
|
174
|
-
*/
|
|
175
|
-
options?: Options;
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Configuration for automatic context compaction.
|
|
179
|
-
* When enabled, automatically triggers /compact when token usage exceeds the threshold.
|
|
180
|
-
*/
|
|
181
|
-
compaction?: CompactionConfig;
|
|
182
|
-
};
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Extra metadata that the agent provides for each tool_call / tool_update update.
|
|
187
|
-
*/
|
|
188
|
-
export type ToolUpdateMeta = {
|
|
189
|
-
claudeCode?: {
|
|
190
|
-
/* The name of the tool that was used in Claude Code. */
|
|
191
|
-
toolName: string;
|
|
192
|
-
/* The structured output provided by Claude Code. */
|
|
193
|
-
toolResponse?: unknown;
|
|
194
|
-
};
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
export type ToolUseCache = {
|
|
198
|
-
[key: string]: {
|
|
199
|
-
type: "tool_use" | "server_tool_use" | "mcp_tool_use";
|
|
200
|
-
id: string;
|
|
201
|
-
name: string;
|
|
202
|
-
input: unknown;
|
|
203
|
-
};
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// Bypass Permissions doesn't work if we are a root/sudo user
|
|
207
|
-
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
208
|
-
|
|
209
|
-
// Implement the ACP Agent interface
|
|
210
|
-
export class ClaudeAcpAgent implements Agent {
|
|
211
|
-
sessions: {
|
|
212
|
-
[key: string]: Session;
|
|
213
|
-
};
|
|
214
|
-
client: AgentSideConnection;
|
|
215
|
-
toolUseCache: ToolUseCache;
|
|
216
|
-
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
|
|
217
|
-
clientCapabilities?: ClientCapabilities;
|
|
218
|
-
logger: Logger;
|
|
219
|
-
|
|
220
|
-
constructor(client: AgentSideConnection, logger?: Logger) {
|
|
221
|
-
this.sessions = {};
|
|
222
|
-
this.client = client;
|
|
223
|
-
this.toolUseCache = {};
|
|
224
|
-
this.logger = logger ?? console;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async initialize(request: InitializeRequest): Promise<InitializeResponse> {
|
|
228
|
-
this.clientCapabilities = request.clientCapabilities;
|
|
229
|
-
|
|
230
|
-
// Default authMethod
|
|
231
|
-
const authMethod: any = {
|
|
232
|
-
description: "Run `claude /login` in the terminal",
|
|
233
|
-
name: "Log in with Claude Code",
|
|
234
|
-
id: "claude-login",
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// If client supports terminal-auth capability, use that instead.
|
|
238
|
-
if (request.clientCapabilities?._meta?.["terminal-auth"] === true) {
|
|
239
|
-
const cliPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk/cli.js"));
|
|
240
|
-
|
|
241
|
-
authMethod._meta = {
|
|
242
|
-
"terminal-auth": {
|
|
243
|
-
command: "node",
|
|
244
|
-
args: [cliPath, "/login"],
|
|
245
|
-
label: "Claude Code Login",
|
|
246
|
-
},
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
protocolVersion: 1,
|
|
252
|
-
agentCapabilities: {
|
|
253
|
-
promptCapabilities: {
|
|
254
|
-
image: true,
|
|
255
|
-
embeddedContext: true,
|
|
256
|
-
},
|
|
257
|
-
mcpCapabilities: {
|
|
258
|
-
http: true,
|
|
259
|
-
sse: true,
|
|
260
|
-
},
|
|
261
|
-
sessionCapabilities: {
|
|
262
|
-
fork: {},
|
|
263
|
-
resume: {},
|
|
264
|
-
},
|
|
265
|
-
loadSession: true,
|
|
266
|
-
},
|
|
267
|
-
agentInfo: {
|
|
268
|
-
name: packageJson.name,
|
|
269
|
-
title: "Claude Code",
|
|
270
|
-
version: packageJson.version,
|
|
271
|
-
},
|
|
272
|
-
authMethods: [authMethod],
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
|
277
|
-
if (
|
|
278
|
-
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
|
279
|
-
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
|
|
280
|
-
) {
|
|
281
|
-
throw RequestError.authRequired();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return await this.createSession(params, {
|
|
285
|
-
// Revisit these meta values once we support resume
|
|
286
|
-
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Fork an existing session to create a new independent session.
|
|
292
|
-
* This is the ACP protocol method handler for session/fork.
|
|
293
|
-
* Named unstable_forkSession to match SDK expectations (session/fork routes to this method).
|
|
294
|
-
*/
|
|
295
|
-
async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
|
|
296
|
-
// Get the session directory to track new files
|
|
297
|
-
const sessionDir = this.getSessionDirPath(params.cwd);
|
|
298
|
-
const beforeFiles = new Set(
|
|
299
|
-
fs.existsSync(sessionDir)
|
|
300
|
-
? fs.readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"))
|
|
301
|
-
: [],
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
const result = await this.createSession(
|
|
305
|
-
{
|
|
306
|
-
cwd: params.cwd,
|
|
307
|
-
mcpServers: params.mcpServers ?? [],
|
|
308
|
-
_meta: params._meta,
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
resume: params.sessionId,
|
|
312
|
-
forkSession: true,
|
|
313
|
-
},
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
// Wait briefly for CLI to create the session file
|
|
317
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
318
|
-
|
|
319
|
-
// Find the CLI-assigned session ID by looking for new session files
|
|
320
|
-
const cliSessionId = await this.discoverCliSessionId(sessionDir, beforeFiles, result.sessionId);
|
|
321
|
-
|
|
322
|
-
// If no new file was created, create one by copying the parent session file
|
|
323
|
-
if (cliSessionId === result.sessionId) {
|
|
324
|
-
// cliSessionId equals fallback, meaning no new file was found
|
|
325
|
-
const parentFilePath = path.join(sessionDir, `${params.sessionId}.jsonl`);
|
|
326
|
-
const forkFilePath = path.join(sessionDir, `${result.sessionId}.jsonl`);
|
|
327
|
-
|
|
328
|
-
if (fs.existsSync(parentFilePath) && !fs.existsSync(forkFilePath)) {
|
|
329
|
-
try {
|
|
330
|
-
// Copy parent session file to fork session file
|
|
331
|
-
fs.copyFileSync(parentFilePath, forkFilePath);
|
|
332
|
-
// Update the internal session ID in the copied file
|
|
333
|
-
this.updateSessionIdInFile(forkFilePath, result.sessionId);
|
|
334
|
-
// Promote to full session (not sidechain)
|
|
335
|
-
this.promoteToFullSession(forkFilePath);
|
|
336
|
-
this.logger.log(
|
|
337
|
-
`[claude-code-acp] Fork: created fork file by copying parent: ${params.sessionId}.jsonl -> ${result.sessionId}.jsonl`,
|
|
338
|
-
);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
this.logger.error(`[claude-code-acp] Failed to create fork file from parent: ${err}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (cliSessionId && cliSessionId !== result.sessionId) {
|
|
346
|
-
// Check if the CLI assigned a non-UUID session ID (e.g., "agent-xxx")
|
|
347
|
-
// If so, we need to extract the internal sessionId from the file
|
|
348
|
-
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
349
|
-
cliSessionId,
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
if (!isUuid) {
|
|
353
|
-
// Read the session file to extract the internal sessionId
|
|
354
|
-
const oldFilePath = path.join(sessionDir, `${cliSessionId}.jsonl`);
|
|
355
|
-
const internalSessionId = this.extractInternalSessionId(oldFilePath);
|
|
356
|
-
|
|
357
|
-
if (internalSessionId) {
|
|
358
|
-
this.logger.log(
|
|
359
|
-
`[claude-code-acp] Fork: extracted internal sessionId ${internalSessionId} from ${cliSessionId}`,
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
// Check if target file already exists (CLI reuses session IDs for forks from same parent)
|
|
363
|
-
// If so, generate a new unique session ID to avoid collisions
|
|
364
|
-
let finalSessionId = internalSessionId;
|
|
365
|
-
let newFilePath = path.join(sessionDir, `${finalSessionId}.jsonl`);
|
|
366
|
-
|
|
367
|
-
if (fs.existsSync(newFilePath)) {
|
|
368
|
-
// Session ID collision - CLI created a fork with the same internal ID
|
|
369
|
-
// Generate a new UUID and update the file's internal session ID
|
|
370
|
-
finalSessionId = randomUUID();
|
|
371
|
-
newFilePath = path.join(sessionDir, `${finalSessionId}.jsonl`);
|
|
372
|
-
this.logger.log(
|
|
373
|
-
`[claude-code-acp] Fork: session ID collision detected, using new ID: ${finalSessionId}`,
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
// Update the internal session ID in the file before renaming
|
|
377
|
-
this.updateSessionIdInFile(oldFilePath, finalSessionId);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Rename the file to match the session ID so CLI can find it
|
|
381
|
-
try {
|
|
382
|
-
fs.renameSync(oldFilePath, newFilePath);
|
|
383
|
-
this.logger.log(
|
|
384
|
-
`[claude-code-acp] Fork: renamed ${cliSessionId}.jsonl -> ${finalSessionId}.jsonl`,
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
// Promote sidechain to full session so it can be resumed/forked again
|
|
388
|
-
this.promoteToFullSession(newFilePath);
|
|
389
|
-
} catch (err) {
|
|
390
|
-
this.logger.error(`[claude-code-acp] Failed to rename session file: ${err}`);
|
|
391
|
-
// Continue anyway - the session might still work
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Re-register session with the final session ID
|
|
395
|
-
const session = this.sessions[result.sessionId];
|
|
396
|
-
this.sessions[finalSessionId] = session;
|
|
397
|
-
delete this.sessions[result.sessionId];
|
|
398
|
-
return { ...result, sessionId: finalSessionId };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Fall through if we couldn't extract the internal ID
|
|
402
|
-
this.logger.error(
|
|
403
|
-
`[claude-code-acp] Could not extract internal sessionId from ${oldFilePath}`,
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Re-register session with the CLI's session ID (if it's already a UUID or extraction failed)
|
|
408
|
-
this.logger.log(
|
|
409
|
-
`[claude-code-acp] Fork: remapping session ${result.sessionId} -> ${cliSessionId}`,
|
|
410
|
-
);
|
|
411
|
-
this.sessions[cliSessionId] = this.sessions[result.sessionId];
|
|
412
|
-
delete this.sessions[result.sessionId];
|
|
413
|
-
return { ...result, sessionId: cliSessionId };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return result;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Get the directory where session files are stored for a given cwd.
|
|
421
|
-
*/
|
|
422
|
-
private getSessionDirPath(cwd: string): string {
|
|
423
|
-
const realCwd = fs.realpathSync(cwd);
|
|
424
|
-
const cwdHash = realCwd.replace(/[/_]/g, "-");
|
|
425
|
-
return path.join(os.homedir(), ".claude", "projects", cwdHash);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Extract the internal sessionId from a session JSONL file.
|
|
430
|
-
* The CLI stores the actual session ID inside the file, which may differ from the filename.
|
|
431
|
-
* For forked sessions, the filename is "agent-xxx" but the internal sessionId is a UUID.
|
|
432
|
-
*/
|
|
433
|
-
private extractInternalSessionId(filePath: string): string | null {
|
|
434
|
-
try {
|
|
435
|
-
if (!fs.existsSync(filePath)) {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
440
|
-
const firstLine = content.split("\n").find((line) => line.trim().length > 0);
|
|
441
|
-
|
|
442
|
-
if (!firstLine) {
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const parsed = JSON.parse(firstLine);
|
|
447
|
-
if (parsed.sessionId && typeof parsed.sessionId === "string") {
|
|
448
|
-
// Verify it's a UUID format
|
|
449
|
-
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
450
|
-
parsed.sessionId,
|
|
451
|
-
);
|
|
452
|
-
if (isUuid) {
|
|
453
|
-
return parsed.sessionId;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return null;
|
|
458
|
-
} catch (err) {
|
|
459
|
-
this.logger.error(`[claude-code-acp] Failed to extract sessionId from ${filePath}: ${err}`);
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Promote a sidechain session to a regular session by modifying the session file.
|
|
466
|
-
* Forked sessions have "isSidechain": true which prevents them from being resumed.
|
|
467
|
-
* This method changes it to false so the session can be resumed/forked again.
|
|
468
|
-
*/
|
|
469
|
-
private promoteToFullSession(filePath: string): boolean {
|
|
470
|
-
try {
|
|
471
|
-
if (!fs.existsSync(filePath)) {
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
476
|
-
const lines = content.split("\n");
|
|
477
|
-
const modifiedLines: string[] = [];
|
|
478
|
-
|
|
479
|
-
for (const line of lines) {
|
|
480
|
-
if (!line.trim()) {
|
|
481
|
-
modifiedLines.push(line);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
const parsed = JSON.parse(line);
|
|
487
|
-
// Change isSidechain from true to false
|
|
488
|
-
if (parsed.isSidechain === true) {
|
|
489
|
-
parsed.isSidechain = false;
|
|
490
|
-
}
|
|
491
|
-
modifiedLines.push(JSON.stringify(parsed));
|
|
492
|
-
} catch {
|
|
493
|
-
// Keep line as-is if it can't be parsed
|
|
494
|
-
modifiedLines.push(line);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
fs.writeFileSync(filePath, modifiedLines.join("\n"), "utf-8");
|
|
499
|
-
this.logger.log(`[claude-code-acp] Promoted sidechain to full session: ${filePath}`);
|
|
500
|
-
return true;
|
|
501
|
-
} catch (err) {
|
|
502
|
-
this.logger.error(`[claude-code-acp] Failed to promote session: ${err}`);
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Update the sessionId in all lines of a session JSONL file.
|
|
509
|
-
* This is used when we need to assign a new unique session ID to avoid collisions.
|
|
510
|
-
*/
|
|
511
|
-
private updateSessionIdInFile(filePath: string, newSessionId: string): boolean {
|
|
512
|
-
try {
|
|
513
|
-
if (!fs.existsSync(filePath)) {
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
518
|
-
const lines = content.split("\n");
|
|
519
|
-
const modifiedLines: string[] = [];
|
|
520
|
-
|
|
521
|
-
for (const line of lines) {
|
|
522
|
-
if (!line.trim()) {
|
|
523
|
-
modifiedLines.push(line);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
try {
|
|
528
|
-
const parsed = JSON.parse(line);
|
|
529
|
-
// Update the sessionId in each line
|
|
530
|
-
if (parsed.sessionId && typeof parsed.sessionId === "string") {
|
|
531
|
-
parsed.sessionId = newSessionId;
|
|
532
|
-
}
|
|
533
|
-
modifiedLines.push(JSON.stringify(parsed));
|
|
534
|
-
} catch {
|
|
535
|
-
// Keep line as-is if it can't be parsed
|
|
536
|
-
modifiedLines.push(line);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
fs.writeFileSync(filePath, modifiedLines.join("\n"), "utf-8");
|
|
541
|
-
this.logger.log(`[claude-code-acp] Updated session ID in file: ${filePath}`);
|
|
542
|
-
return true;
|
|
543
|
-
} catch (err) {
|
|
544
|
-
this.logger.error(`[claude-code-acp] Failed to update session ID in file: ${err}`);
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Discover the CLI-assigned session ID by looking for new session files.
|
|
551
|
-
* Returns the CLI's session ID if found, or the original sessionId if not.
|
|
552
|
-
*/
|
|
553
|
-
private async discoverCliSessionId(
|
|
554
|
-
sessionDir: string,
|
|
555
|
-
beforeFiles: Set<string>,
|
|
556
|
-
fallbackId: string,
|
|
557
|
-
timeout: number = 2000,
|
|
558
|
-
): Promise<string> {
|
|
559
|
-
const start = Date.now();
|
|
560
|
-
// Pattern for CLI-assigned fork session IDs (agent-xxxxxxx)
|
|
561
|
-
const agentPattern = /^agent-[a-f0-9]+\.jsonl$/;
|
|
562
|
-
|
|
563
|
-
while (Date.now() - start < timeout) {
|
|
564
|
-
if (fs.existsSync(sessionDir)) {
|
|
565
|
-
const currentFiles = fs.readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
566
|
-
// Only look for new files that match the agent-xxx pattern
|
|
567
|
-
// This prevents picking up renamed UUID files from previous forks
|
|
568
|
-
const newFiles = currentFiles.filter((f) => !beforeFiles.has(f) && agentPattern.test(f));
|
|
569
|
-
|
|
570
|
-
if (newFiles.length === 1) {
|
|
571
|
-
// Found exactly one new agent session file - this is our fork
|
|
572
|
-
this.logger.log(`[claude-code-acp] Discovered fork session file: ${newFiles[0]}`);
|
|
573
|
-
return newFiles[0].replace(".jsonl", "");
|
|
574
|
-
} else if (newFiles.length > 1) {
|
|
575
|
-
// Multiple new agent files - try to find the most recent one
|
|
576
|
-
let newestFile = "";
|
|
577
|
-
let newestMtime = 0;
|
|
578
|
-
for (const file of newFiles) {
|
|
579
|
-
const filePath = path.join(sessionDir, file);
|
|
580
|
-
const stat = fs.statSync(filePath);
|
|
581
|
-
if (stat.mtimeMs > newestMtime) {
|
|
582
|
-
newestMtime = stat.mtimeMs;
|
|
583
|
-
newestFile = file;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
if (newestFile) {
|
|
587
|
-
this.logger.log(
|
|
588
|
-
`[claude-code-acp] Discovered fork session file (newest of ${newFiles.length}): ${newestFile}`,
|
|
589
|
-
);
|
|
590
|
-
return newestFile.replace(".jsonl", "");
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Timeout - return fallback
|
|
599
|
-
this.logger.log(
|
|
600
|
-
`[claude-code-acp] Could not discover CLI session ID, using fallback: ${fallbackId}`,
|
|
601
|
-
);
|
|
602
|
-
return fallbackId;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Alias for unstable_forkSession for convenience.
|
|
607
|
-
*/
|
|
608
|
-
async forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
|
|
609
|
-
return this.unstable_forkSession(params);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Load an existing session to resume a previous conversation.
|
|
614
|
-
* This is the ACP protocol method handler for session/load.
|
|
615
|
-
*/
|
|
616
|
-
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
|
617
|
-
const response = await this.createSession(
|
|
618
|
-
{
|
|
619
|
-
cwd: params.cwd,
|
|
620
|
-
mcpServers: params.mcpServers ?? [],
|
|
621
|
-
_meta: params._meta,
|
|
622
|
-
},
|
|
623
|
-
{
|
|
624
|
-
resume: params.sessionId,
|
|
625
|
-
},
|
|
626
|
-
);
|
|
627
|
-
|
|
628
|
-
return response;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Resume an existing session. This delegates to loadSession for enhanced functionality.
|
|
633
|
-
*/
|
|
634
|
-
async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
|
|
635
|
-
const response = await this.createSession(
|
|
636
|
-
{
|
|
637
|
-
cwd: params.cwd,
|
|
638
|
-
mcpServers: params.mcpServers ?? [],
|
|
639
|
-
_meta: params._meta,
|
|
640
|
-
},
|
|
641
|
-
{
|
|
642
|
-
resume: params.sessionId,
|
|
643
|
-
},
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
return response;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async authenticate(_params: AuthenticateRequest): Promise<void> {
|
|
650
|
-
throw new Error("Method not implemented.");
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
654
|
-
if (!this.sessions[params.sessionId]) {
|
|
655
|
-
throw new Error("Session not found");
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
this.sessions[params.sessionId].cancelled = false;
|
|
659
|
-
|
|
660
|
-
const { query, input } = this.sessions[params.sessionId];
|
|
661
|
-
|
|
662
|
-
input.push(promptToClaude(params));
|
|
663
|
-
while (true) {
|
|
664
|
-
const { value: message, done } = await query.next();
|
|
665
|
-
if (done || !message) {
|
|
666
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
667
|
-
return { stopReason: "cancelled" };
|
|
668
|
-
}
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
switch (message.type) {
|
|
673
|
-
case "system":
|
|
674
|
-
switch (message.subtype) {
|
|
675
|
-
case "init": {
|
|
676
|
-
// Capture skills from the SDK init message if available
|
|
677
|
-
const session = this.sessions[params.sessionId];
|
|
678
|
-
if (session && (message as any).skills) {
|
|
679
|
-
session.skills = ((message as any).skills as any[]).map((s: any) => ({
|
|
680
|
-
name: s.name ?? s,
|
|
681
|
-
description: s.description,
|
|
682
|
-
source: s.source,
|
|
683
|
-
}));
|
|
684
|
-
}
|
|
685
|
-
break;
|
|
686
|
-
}
|
|
687
|
-
case "compact_boundary": {
|
|
688
|
-
// Reset token count after compaction
|
|
689
|
-
const session = this.sessions[params.sessionId];
|
|
690
|
-
if (session) {
|
|
691
|
-
const preTokens = message.compact_metadata.pre_tokens;
|
|
692
|
-
const trigger = message.compact_metadata.trigger;
|
|
693
|
-
|
|
694
|
-
session.compaction.currentTokens = 0;
|
|
695
|
-
session.compaction.isCompacting = false;
|
|
696
|
-
|
|
697
|
-
this.logger.log(
|
|
698
|
-
`[auto-compaction] Compaction completed, token count reset. Previous tokens: ${preTokens}`,
|
|
699
|
-
);
|
|
700
|
-
|
|
701
|
-
// Emit compaction_completed event to client via extension notification
|
|
702
|
-
// (compaction events are not part of the standard ACP SessionUpdate schema)
|
|
703
|
-
// Note: Using "_" prefix for SDK 0.13+ compatibility with older client SDKs
|
|
704
|
-
await this.client.extNotification("_compaction_completed", {
|
|
705
|
-
sessionId: params.sessionId,
|
|
706
|
-
trigger: trigger,
|
|
707
|
-
preTokens: preTokens,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
break;
|
|
711
|
-
}
|
|
712
|
-
case "hook_started":
|
|
713
|
-
case "task_notification":
|
|
714
|
-
case "hook_progress":
|
|
715
|
-
case "hook_response":
|
|
716
|
-
case "status":
|
|
717
|
-
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
718
|
-
break;
|
|
719
|
-
default:
|
|
720
|
-
unreachable(message, this.logger);
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
break;
|
|
724
|
-
case "result": {
|
|
725
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
726
|
-
return { stopReason: "cancelled" };
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Track token usage for auto-compaction
|
|
730
|
-
const session = this.sessions[params.sessionId];
|
|
731
|
-
if (session && message.usage) {
|
|
732
|
-
const totalTokens =
|
|
733
|
-
message.usage.input_tokens +
|
|
734
|
-
message.usage.output_tokens +
|
|
735
|
-
(message.usage.cache_creation_input_tokens ?? 0) +
|
|
736
|
-
(message.usage.cache_read_input_tokens ?? 0);
|
|
737
|
-
session.compaction.currentTokens += totalTokens;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
switch (message.subtype) {
|
|
741
|
-
case "success": {
|
|
742
|
-
if (message.result.includes("Please run /login")) {
|
|
743
|
-
throw RequestError.authRequired();
|
|
744
|
-
}
|
|
745
|
-
if (message.is_error) {
|
|
746
|
-
throw RequestError.internalError(undefined, message.result);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Check if auto-compaction should be triggered
|
|
750
|
-
if (session && (await this.shouldTriggerCompaction(params.sessionId))) {
|
|
751
|
-
await this.triggerAutoCompaction(params.sessionId);
|
|
752
|
-
// Continue processing - the compaction will happen in the background
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return { stopReason: "end_turn" };
|
|
756
|
-
}
|
|
757
|
-
case "error_during_execution":
|
|
758
|
-
if (message.is_error) {
|
|
759
|
-
throw RequestError.internalError(
|
|
760
|
-
undefined,
|
|
761
|
-
message.errors.join(", ") || message.subtype,
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
return { stopReason: "end_turn" };
|
|
765
|
-
case "error_max_budget_usd":
|
|
766
|
-
case "error_max_turns":
|
|
767
|
-
case "error_max_structured_output_retries":
|
|
768
|
-
if (message.is_error) {
|
|
769
|
-
throw RequestError.internalError(
|
|
770
|
-
undefined,
|
|
771
|
-
message.errors.join(", ") || message.subtype,
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
return { stopReason: "max_turn_requests" };
|
|
775
|
-
default:
|
|
776
|
-
unreachable(message, this.logger);
|
|
777
|
-
break;
|
|
778
|
-
}
|
|
779
|
-
break;
|
|
780
|
-
}
|
|
781
|
-
case "stream_event": {
|
|
782
|
-
for (const notification of streamEventToAcpNotifications(
|
|
783
|
-
message,
|
|
784
|
-
params.sessionId,
|
|
785
|
-
this.toolUseCache,
|
|
786
|
-
this.client,
|
|
787
|
-
this.logger,
|
|
788
|
-
)) {
|
|
789
|
-
await this.client.sessionUpdate(notification);
|
|
790
|
-
}
|
|
791
|
-
break;
|
|
792
|
-
}
|
|
793
|
-
case "user":
|
|
794
|
-
case "assistant": {
|
|
795
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
796
|
-
break;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Slash commands like /compact can generate invalid output... doesn't match
|
|
800
|
-
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
|
|
801
|
-
if (
|
|
802
|
-
typeof message.message.content === "string" &&
|
|
803
|
-
message.message.content.includes("<local-command-stdout>")
|
|
804
|
-
) {
|
|
805
|
-
// Handle /context by sending its reply as regular agent message.
|
|
806
|
-
if (message.message.content.includes("Context Usage")) {
|
|
807
|
-
for (const notification of toAcpNotifications(
|
|
808
|
-
message.message.content
|
|
809
|
-
.replace("<local-command-stdout>", "")
|
|
810
|
-
.replace("</local-command-stdout>", ""),
|
|
811
|
-
"assistant",
|
|
812
|
-
params.sessionId,
|
|
813
|
-
this.toolUseCache,
|
|
814
|
-
this.client,
|
|
815
|
-
this.logger,
|
|
816
|
-
)) {
|
|
817
|
-
await this.client.sessionUpdate(notification);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
this.logger.log(message.message.content);
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (
|
|
825
|
-
typeof message.message.content === "string" &&
|
|
826
|
-
message.message.content.includes("<local-command-stderr>")
|
|
827
|
-
) {
|
|
828
|
-
this.logger.error(message.message.content);
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
// Skip these user messages for now, since they seem to just be messages we don't want in the feed
|
|
832
|
-
if (
|
|
833
|
-
message.type === "user" &&
|
|
834
|
-
(typeof message.message.content === "string" ||
|
|
835
|
-
(Array.isArray(message.message.content) &&
|
|
836
|
-
message.message.content.length === 1 &&
|
|
837
|
-
message.message.content[0].type === "text"))
|
|
838
|
-
) {
|
|
839
|
-
break;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (
|
|
843
|
-
message.type === "assistant" &&
|
|
844
|
-
message.message.model === "<synthetic>" &&
|
|
845
|
-
Array.isArray(message.message.content) &&
|
|
846
|
-
message.message.content.length === 1 &&
|
|
847
|
-
message.message.content[0].type === "text" &&
|
|
848
|
-
message.message.content[0].text.includes("Please run /login")
|
|
849
|
-
) {
|
|
850
|
-
throw RequestError.authRequired();
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
const content =
|
|
854
|
-
message.type === "assistant"
|
|
855
|
-
? // Handled by stream events above
|
|
856
|
-
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
857
|
-
: message.message.content;
|
|
858
|
-
|
|
859
|
-
for (const notification of toAcpNotifications(
|
|
860
|
-
content,
|
|
861
|
-
message.message.role,
|
|
862
|
-
params.sessionId,
|
|
863
|
-
this.toolUseCache,
|
|
864
|
-
this.client,
|
|
865
|
-
this.logger,
|
|
866
|
-
)) {
|
|
867
|
-
await this.client.sessionUpdate(notification);
|
|
868
|
-
}
|
|
869
|
-
break;
|
|
870
|
-
}
|
|
871
|
-
case "tool_progress":
|
|
872
|
-
case "tool_use_summary":
|
|
873
|
-
break;
|
|
874
|
-
case "auth_status":
|
|
875
|
-
break;
|
|
876
|
-
default:
|
|
877
|
-
unreachable(message);
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
throw new Error("Session did not end in result");
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
async cancel(params: CancelNotification): Promise<void> {
|
|
885
|
-
if (!this.sessions[params.sessionId]) {
|
|
886
|
-
throw new Error("Session not found");
|
|
887
|
-
}
|
|
888
|
-
this.sessions[params.sessionId].cancelled = true;
|
|
889
|
-
await this.sessions[params.sessionId].query.interrupt();
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* Handle extension methods from the client.
|
|
894
|
-
*
|
|
895
|
-
* Currently supports:
|
|
896
|
-
* - `_session/inject`: Inject a message into an active session mid-execution
|
|
897
|
-
* - `_session/flush`: Flush a session to disk for fork-with-flush support
|
|
898
|
-
* - `_session/setCompaction`: Configure automatic context compaction for a session
|
|
899
|
-
* - `_session/listSkills`: List available skills for a session
|
|
900
|
-
*/
|
|
901
|
-
async extMethod(
|
|
902
|
-
method: string,
|
|
903
|
-
params: Record<string, unknown>,
|
|
904
|
-
): Promise<Record<string, unknown>> {
|
|
905
|
-
if (method === "_session/inject") {
|
|
906
|
-
return this.handleSessionInject(
|
|
907
|
-
params as { sessionId: string; message: string | PromptRequest["prompt"] },
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
if (method === "_session/flush") {
|
|
911
|
-
return this.handleSessionFlush(
|
|
912
|
-
params as { sessionId: string; idleTimeout?: number; persistTimeout?: number },
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
if (method === "_session/setCompaction") {
|
|
916
|
-
return this.handleSessionSetCompaction(
|
|
917
|
-
params as {
|
|
918
|
-
sessionId: string;
|
|
919
|
-
enabled: boolean;
|
|
920
|
-
contextTokenThreshold?: number;
|
|
921
|
-
customInstructions?: string;
|
|
922
|
-
},
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
if (method === "_session/listSkills") {
|
|
926
|
-
return this.handleSessionListSkills(params as { sessionId: string });
|
|
927
|
-
}
|
|
928
|
-
throw RequestError.methodNotFound(method);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* Inject a message into an active session.
|
|
933
|
-
*
|
|
934
|
-
* This allows sending additional user input while a prompt() call is actively
|
|
935
|
-
* processing. The injected message will be queued and processed by the agent
|
|
936
|
-
* as part of the current conversation turn.
|
|
937
|
-
*
|
|
938
|
-
* Use cases:
|
|
939
|
-
* - Providing clarification while the agent is working
|
|
940
|
-
* - Adding context mid-execution
|
|
941
|
-
* - Sending corrections before a tool completes
|
|
942
|
-
*
|
|
943
|
-
* @param params.sessionId - The session to inject the message into
|
|
944
|
-
* @param params.message - Either a string or an array of ContentBlocks (same format as prompt)
|
|
945
|
-
* @returns Success status and any error message
|
|
946
|
-
*/
|
|
947
|
-
private handleSessionInject(params: {
|
|
948
|
-
sessionId: string;
|
|
949
|
-
message: string | PromptRequest["prompt"];
|
|
950
|
-
}): Record<string, unknown> {
|
|
951
|
-
const { sessionId, message } = params;
|
|
952
|
-
const session = this.sessions[sessionId];
|
|
953
|
-
|
|
954
|
-
if (!session) {
|
|
955
|
-
return { success: false, error: `Session ${sessionId} not found` };
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (session.cancelled) {
|
|
959
|
-
return { success: false, error: `Session ${sessionId} is cancelled` };
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
try {
|
|
963
|
-
// Convert string to ContentBlock array if needed
|
|
964
|
-
const prompt: PromptRequest["prompt"] =
|
|
965
|
-
typeof message === "string" ? [{ type: "text", text: message }] : message;
|
|
966
|
-
|
|
967
|
-
// Create a PromptRequest-like object to reuse promptToClaude
|
|
968
|
-
const promptRequest: PromptRequest = {
|
|
969
|
-
sessionId,
|
|
970
|
-
prompt,
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
// Convert to SDK format and push to the session's input queue
|
|
974
|
-
const sdkMessage = promptToClaude(promptRequest);
|
|
975
|
-
session.input.push(sdkMessage);
|
|
976
|
-
|
|
977
|
-
this.logger.log(`[claude-code-acp] Injected message into session ${sessionId}`);
|
|
978
|
-
return { success: true };
|
|
979
|
-
} catch (error) {
|
|
980
|
-
this.logger.error(
|
|
981
|
-
`[claude-code-acp] Failed to inject message into session ${sessionId}:`,
|
|
982
|
-
error,
|
|
983
|
-
);
|
|
984
|
-
return { success: false, error: String(error) };
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* Configure automatic context compaction for a session.
|
|
990
|
-
*
|
|
991
|
-
* When enabled, the session will automatically trigger compaction when token usage
|
|
992
|
-
* exceeds the configured threshold. This helps manage context window limits during
|
|
993
|
-
* long-running conversations.
|
|
994
|
-
*
|
|
995
|
-
* @param params.sessionId - The session to configure
|
|
996
|
-
* @param params.enabled - Whether automatic compaction is enabled
|
|
997
|
-
* @param params.contextTokenThreshold - Token count that triggers compaction (default: 100000)
|
|
998
|
-
* @param params.customInstructions - Optional instructions for the compaction summary
|
|
999
|
-
* @returns Success status and any error message
|
|
1000
|
-
*/
|
|
1001
|
-
private handleSessionSetCompaction(params: {
|
|
1002
|
-
sessionId: string;
|
|
1003
|
-
enabled: boolean;
|
|
1004
|
-
contextTokenThreshold?: number;
|
|
1005
|
-
customInstructions?: string;
|
|
1006
|
-
}): Record<string, unknown> {
|
|
1007
|
-
const { sessionId, enabled, contextTokenThreshold, customInstructions } = params;
|
|
1008
|
-
const session = this.sessions[sessionId];
|
|
1009
|
-
|
|
1010
|
-
if (!session) {
|
|
1011
|
-
return { success: false, error: `Session ${sessionId} not found` };
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
try {
|
|
1015
|
-
// Update the session's compaction configuration
|
|
1016
|
-
session.compaction.enabled = enabled;
|
|
1017
|
-
|
|
1018
|
-
if (contextTokenThreshold !== undefined) {
|
|
1019
|
-
session.compaction.threshold = contextTokenThreshold;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
if (customInstructions !== undefined) {
|
|
1023
|
-
session.compaction.customInstructions = customInstructions;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
this.logger.log(
|
|
1027
|
-
`[claude-code-acp] Updated compaction config for session ${sessionId}: ` +
|
|
1028
|
-
`enabled=${enabled}, threshold=${session.compaction.threshold}`,
|
|
1029
|
-
);
|
|
1030
|
-
|
|
1031
|
-
return { success: true };
|
|
1032
|
-
} catch (error) {
|
|
1033
|
-
this.logger.error(
|
|
1034
|
-
`[claude-code-acp] Failed to set compaction config for session ${sessionId}:`,
|
|
1035
|
-
error,
|
|
1036
|
-
);
|
|
1037
|
-
return { success: false, error: String(error) };
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
/**
|
|
1042
|
-
* List available skills for a session.
|
|
1043
|
-
*
|
|
1044
|
-
* Skills are loaded based on the session's settingSources and plugins configuration.
|
|
1045
|
-
* This method returns the skills that were discovered during session initialization.
|
|
1046
|
-
*
|
|
1047
|
-
* @param params.sessionId - The session to query skills for
|
|
1048
|
-
* @returns Success status, skills array, and any error message
|
|
1049
|
-
*/
|
|
1050
|
-
private handleSessionListSkills(params: { sessionId: string }): Record<string, unknown> {
|
|
1051
|
-
const { sessionId } = params;
|
|
1052
|
-
const session = this.sessions[sessionId];
|
|
1053
|
-
|
|
1054
|
-
if (!session) {
|
|
1055
|
-
return { success: false, error: `Session ${sessionId} not found` };
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
try {
|
|
1059
|
-
return {
|
|
1060
|
-
success: true,
|
|
1061
|
-
skills: session.skills ?? [],
|
|
1062
|
-
};
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
this.logger.error(`[claude-code-acp] Failed to list skills for session ${sessionId}:`, error);
|
|
1065
|
-
return { success: false, error: String(error) };
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* Flush a session to disk by aborting its query subprocess.
|
|
1071
|
-
*
|
|
1072
|
-
* This is used by the fork-with-flush mechanism to ensure session data
|
|
1073
|
-
* is persisted to disk before forking. When the Claude SDK subprocess
|
|
1074
|
-
* exits (via abort), it writes the session data to:
|
|
1075
|
-
* ~/.claude/projects/<cwd-hash>/<sessionId>.jsonl
|
|
1076
|
-
*
|
|
1077
|
-
* After this method completes, the session is removed from memory and
|
|
1078
|
-
* must be reloaded via loadSession() to continue using it.
|
|
1079
|
-
*/
|
|
1080
|
-
private async handleSessionFlush(params: {
|
|
1081
|
-
sessionId: string;
|
|
1082
|
-
idleTimeout?: number;
|
|
1083
|
-
persistTimeout?: number;
|
|
1084
|
-
}): Promise<Record<string, unknown>> {
|
|
1085
|
-
const { sessionId, persistTimeout = 5000 } = params;
|
|
1086
|
-
const session = this.sessions[sessionId];
|
|
1087
|
-
|
|
1088
|
-
if (!session) {
|
|
1089
|
-
return { success: false, error: `Session ${sessionId} not found` };
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
try {
|
|
1093
|
-
// Step 1: Mark session as cancelled to stop processing
|
|
1094
|
-
session.cancelled = true;
|
|
1095
|
-
|
|
1096
|
-
// Step 2: Interrupt any ongoing query work
|
|
1097
|
-
await session.query.interrupt();
|
|
1098
|
-
|
|
1099
|
-
// Step 3: End the input stream to signal no more input
|
|
1100
|
-
session.input.end();
|
|
1101
|
-
|
|
1102
|
-
// Step 4: Abort the session using the AbortController
|
|
1103
|
-
// This forces the Claude SDK subprocess to exit, which triggers disk persistence
|
|
1104
|
-
session.abortController.abort();
|
|
1105
|
-
|
|
1106
|
-
// Step 5: Wait for the session file to appear on disk
|
|
1107
|
-
// Use stored sessionFilePath for forked sessions (where filename differs from sessionId)
|
|
1108
|
-
const sessionFilePath =
|
|
1109
|
-
session.sessionFilePath ?? this.getSessionFilePath(sessionId, session.cwd);
|
|
1110
|
-
this.logger.log(`[claude-code-acp] Waiting for session file at: ${sessionFilePath}`);
|
|
1111
|
-
this.logger.log(`[claude-code-acp] Session cwd: ${session.cwd}`);
|
|
1112
|
-
const persisted = await this.waitForSessionFile(sessionFilePath, persistTimeout);
|
|
1113
|
-
|
|
1114
|
-
if (!persisted) {
|
|
1115
|
-
this.logger.error(
|
|
1116
|
-
`[claude-code-acp] Session file not found at ${sessionFilePath} after ${persistTimeout}ms`,
|
|
1117
|
-
);
|
|
1118
|
-
// Check if file exists at the path
|
|
1119
|
-
const exists = fs.existsSync(sessionFilePath);
|
|
1120
|
-
this.logger.error(`[claude-code-acp] File exists check: ${exists}`);
|
|
1121
|
-
// Still remove the session from memory
|
|
1122
|
-
delete this.sessions[sessionId];
|
|
1123
|
-
return { success: false, error: `Session file not created within timeout` };
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Step 6: Remove session from our map
|
|
1127
|
-
// The client will call loadSession() to reload it from disk
|
|
1128
|
-
delete this.sessions[sessionId];
|
|
1129
|
-
|
|
1130
|
-
this.logger.log(
|
|
1131
|
-
`[claude-code-acp] Session ${sessionId} flushed to disk at ${sessionFilePath}`,
|
|
1132
|
-
);
|
|
1133
|
-
return { success: true, filePath: sessionFilePath };
|
|
1134
|
-
} catch (error) {
|
|
1135
|
-
this.logger.error(`[claude-code-acp] Failed to flush session ${sessionId}:`, error);
|
|
1136
|
-
// Clean up session on error
|
|
1137
|
-
delete this.sessions[sessionId];
|
|
1138
|
-
return { success: false, error: String(error) };
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Get the file path where Claude Code stores session data.
|
|
1144
|
-
*
|
|
1145
|
-
* Claude Code stores sessions at:
|
|
1146
|
-
* ~/.claude/projects/<cwd-hash>/<sessionId>.jsonl
|
|
1147
|
-
*
|
|
1148
|
-
* Where <cwd-hash> is the cwd with `/` replaced by `-`
|
|
1149
|
-
* Note: We resolve the real path to handle macOS symlinks like /var -> /private/var
|
|
1150
|
-
*/
|
|
1151
|
-
private getSessionFilePath(sessionId: string, cwd: string): string {
|
|
1152
|
-
// Resolve the real path to handle macOS symlinks like /var -> /private/var
|
|
1153
|
-
const realCwd = fs.realpathSync(cwd);
|
|
1154
|
-
// Claude Code replaces both / and _ with - in the cwd hash
|
|
1155
|
-
const cwdHash = realCwd.replace(/[/_]/g, "-");
|
|
1156
|
-
return path.join(os.homedir(), ".claude", "projects", cwdHash, `${sessionId}.jsonl`);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
/**
|
|
1160
|
-
* Wait for a session file to appear on disk.
|
|
1161
|
-
*
|
|
1162
|
-
* @param filePath - Path to the session file
|
|
1163
|
-
* @param timeout - Maximum time to wait in milliseconds
|
|
1164
|
-
* @returns true if file appears, false if timeout
|
|
1165
|
-
*/
|
|
1166
|
-
private async waitForSessionFile(filePath: string, timeout: number): Promise<boolean> {
|
|
1167
|
-
const start = Date.now();
|
|
1168
|
-
while (Date.now() - start < timeout) {
|
|
1169
|
-
if (fs.existsSync(filePath)) {
|
|
1170
|
-
return true;
|
|
1171
|
-
}
|
|
1172
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1173
|
-
}
|
|
1174
|
-
return false;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
async unstable_setSessionModel(
|
|
1178
|
-
params: SetSessionModelRequest,
|
|
1179
|
-
): Promise<SetSessionModelResponse | void> {
|
|
1180
|
-
if (!this.sessions[params.sessionId]) {
|
|
1181
|
-
throw new Error("Session not found");
|
|
1182
|
-
}
|
|
1183
|
-
await this.sessions[params.sessionId].query.setModel(params.modelId);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse> {
|
|
1187
|
-
if (!this.sessions[params.sessionId]) {
|
|
1188
|
-
throw new Error("Session not found");
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
switch (params.modeId) {
|
|
1192
|
-
case "default":
|
|
1193
|
-
case "acceptEdits":
|
|
1194
|
-
case "bypassPermissions":
|
|
1195
|
-
case "dontAsk":
|
|
1196
|
-
case "plan":
|
|
1197
|
-
this.sessions[params.sessionId].permissionMode = params.modeId;
|
|
1198
|
-
try {
|
|
1199
|
-
await this.sessions[params.sessionId].query.setPermissionMode(params.modeId);
|
|
1200
|
-
} catch (error) {
|
|
1201
|
-
const errorMessage =
|
|
1202
|
-
error instanceof Error && error.message ? error.message : "Invalid Mode";
|
|
1203
|
-
|
|
1204
|
-
throw new Error(errorMessage);
|
|
1205
|
-
}
|
|
1206
|
-
return {};
|
|
1207
|
-
default:
|
|
1208
|
-
throw new Error("Invalid Mode");
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
1213
|
-
const response = await this.client.readTextFile(params);
|
|
1214
|
-
return response;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
async writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
|
1218
|
-
const response = await this.client.writeTextFile(params);
|
|
1219
|
-
return response;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
canUseTool(sessionId: string): CanUseTool {
|
|
1223
|
-
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
|
|
1224
|
-
const session = this.sessions[sessionId];
|
|
1225
|
-
if (!session) {
|
|
1226
|
-
return {
|
|
1227
|
-
behavior: "deny",
|
|
1228
|
-
message: "Session not found",
|
|
1229
|
-
interrupt: true,
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
if (toolName === "ExitPlanMode") {
|
|
1234
|
-
const response = await this.client.requestPermission({
|
|
1235
|
-
options: [
|
|
1236
|
-
{
|
|
1237
|
-
kind: "allow_always",
|
|
1238
|
-
name: "Yes, and auto-accept edits",
|
|
1239
|
-
optionId: "acceptEdits",
|
|
1240
|
-
},
|
|
1241
|
-
{ kind: "allow_once", name: "Yes, and manually approve edits", optionId: "default" },
|
|
1242
|
-
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
|
1243
|
-
],
|
|
1244
|
-
sessionId,
|
|
1245
|
-
toolCall: {
|
|
1246
|
-
toolCallId: toolUseID,
|
|
1247
|
-
rawInput: toolInput,
|
|
1248
|
-
title: toolInfoFromToolUse({ name: toolName, input: toolInput }).title,
|
|
1249
|
-
},
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
1253
|
-
throw new Error("Tool use aborted");
|
|
1254
|
-
}
|
|
1255
|
-
if (
|
|
1256
|
-
response.outcome?.outcome === "selected" &&
|
|
1257
|
-
(response.outcome.optionId === "default" || response.outcome.optionId === "acceptEdits")
|
|
1258
|
-
) {
|
|
1259
|
-
session.permissionMode = response.outcome.optionId;
|
|
1260
|
-
await this.client.sessionUpdate({
|
|
1261
|
-
sessionId,
|
|
1262
|
-
update: {
|
|
1263
|
-
sessionUpdate: "current_mode_update",
|
|
1264
|
-
currentModeId: response.outcome.optionId,
|
|
1265
|
-
},
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
return {
|
|
1269
|
-
behavior: "allow",
|
|
1270
|
-
updatedInput: toolInput,
|
|
1271
|
-
updatedPermissions: suggestions ?? [
|
|
1272
|
-
{ type: "setMode", mode: response.outcome.optionId, destination: "session" },
|
|
1273
|
-
],
|
|
1274
|
-
};
|
|
1275
|
-
} else {
|
|
1276
|
-
return {
|
|
1277
|
-
behavior: "deny",
|
|
1278
|
-
message: "User rejected request to exit plan mode.",
|
|
1279
|
-
interrupt: true,
|
|
1280
|
-
};
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
if (
|
|
1285
|
-
session.permissionMode === "bypassPermissions" ||
|
|
1286
|
-
(session.permissionMode === "acceptEdits" && EDIT_TOOL_NAMES.includes(toolName))
|
|
1287
|
-
) {
|
|
1288
|
-
return {
|
|
1289
|
-
behavior: "allow",
|
|
1290
|
-
updatedInput: toolInput,
|
|
1291
|
-
updatedPermissions: suggestions ?? [
|
|
1292
|
-
{ type: "addRules", rules: [{ toolName }], behavior: "allow", destination: "session" },
|
|
1293
|
-
],
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
const response = await this.client.requestPermission({
|
|
1298
|
-
options: [
|
|
1299
|
-
{
|
|
1300
|
-
kind: "allow_always",
|
|
1301
|
-
name: "Always Allow",
|
|
1302
|
-
optionId: "allow_always",
|
|
1303
|
-
},
|
|
1304
|
-
{ kind: "allow_once", name: "Allow", optionId: "allow" },
|
|
1305
|
-
{ kind: "reject_once", name: "Reject", optionId: "reject" },
|
|
1306
|
-
],
|
|
1307
|
-
sessionId,
|
|
1308
|
-
toolCall: {
|
|
1309
|
-
toolCallId: toolUseID,
|
|
1310
|
-
rawInput: toolInput,
|
|
1311
|
-
title: toolInfoFromToolUse({ name: toolName, input: toolInput }).title,
|
|
1312
|
-
},
|
|
1313
|
-
});
|
|
1314
|
-
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
1315
|
-
throw new Error("Tool use aborted");
|
|
1316
|
-
}
|
|
1317
|
-
if (
|
|
1318
|
-
response.outcome?.outcome === "selected" &&
|
|
1319
|
-
(response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")
|
|
1320
|
-
) {
|
|
1321
|
-
// If Claude Code has suggestions, it will update their settings already
|
|
1322
|
-
if (response.outcome.optionId === "allow_always") {
|
|
1323
|
-
return {
|
|
1324
|
-
behavior: "allow",
|
|
1325
|
-
updatedInput: toolInput,
|
|
1326
|
-
updatedPermissions: suggestions ?? [
|
|
1327
|
-
{
|
|
1328
|
-
type: "addRules",
|
|
1329
|
-
rules: [{ toolName }],
|
|
1330
|
-
behavior: "allow",
|
|
1331
|
-
destination: "session",
|
|
1332
|
-
},
|
|
1333
|
-
],
|
|
1334
|
-
};
|
|
1335
|
-
}
|
|
1336
|
-
return {
|
|
1337
|
-
behavior: "allow",
|
|
1338
|
-
updatedInput: toolInput,
|
|
1339
|
-
};
|
|
1340
|
-
} else {
|
|
1341
|
-
return {
|
|
1342
|
-
behavior: "deny",
|
|
1343
|
-
message: "User refused permission to run tool",
|
|
1344
|
-
interrupt: true,
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
/**
|
|
1351
|
-
* Check if auto-compaction should be triggered based on current token usage.
|
|
1352
|
-
*/
|
|
1353
|
-
private async shouldTriggerCompaction(sessionId: string): Promise<boolean> {
|
|
1354
|
-
const session = this.sessions[sessionId];
|
|
1355
|
-
if (!session) return false;
|
|
1356
|
-
|
|
1357
|
-
const { compaction } = session;
|
|
1358
|
-
|
|
1359
|
-
// Check if auto-compaction is enabled and not already in progress
|
|
1360
|
-
if (!compaction.enabled || compaction.isCompacting) {
|
|
1361
|
-
return false;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// Check if current tokens exceed threshold
|
|
1365
|
-
return compaction.currentTokens >= compaction.threshold;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
/**
|
|
1369
|
-
* Trigger automatic compaction by sending a /compact command.
|
|
1370
|
-
*/
|
|
1371
|
-
private async triggerAutoCompaction(sessionId: string): Promise<void> {
|
|
1372
|
-
const session = this.sessions[sessionId];
|
|
1373
|
-
if (!session) return;
|
|
1374
|
-
|
|
1375
|
-
const { compaction, input } = session;
|
|
1376
|
-
|
|
1377
|
-
// Mark compaction as in progress to prevent multiple triggers
|
|
1378
|
-
compaction.isCompacting = true;
|
|
1379
|
-
|
|
1380
|
-
const preTokens = compaction.currentTokens;
|
|
1381
|
-
|
|
1382
|
-
this.logger.log(
|
|
1383
|
-
`[auto-compaction] Triggering compaction. Current tokens: ${preTokens}, Threshold: ${compaction.threshold}`,
|
|
1384
|
-
);
|
|
1385
|
-
|
|
1386
|
-
// Emit compaction_started event to client via extension notification
|
|
1387
|
-
// (compaction events are not part of the standard ACP SessionUpdate schema)
|
|
1388
|
-
// Note: Using "_" prefix for SDK 0.13+ compatibility with older client SDKs
|
|
1389
|
-
await this.client.extNotification("_compaction_started", {
|
|
1390
|
-
sessionId,
|
|
1391
|
-
trigger: "auto",
|
|
1392
|
-
preTokens: preTokens,
|
|
1393
|
-
threshold: compaction.threshold,
|
|
1394
|
-
});
|
|
1395
|
-
|
|
1396
|
-
// Build the compact command with optional custom instructions
|
|
1397
|
-
const compactCommand = compaction.customInstructions
|
|
1398
|
-
? `/compact ${compaction.customInstructions}`
|
|
1399
|
-
: "/compact";
|
|
1400
|
-
|
|
1401
|
-
// Inject the compact command as a user message
|
|
1402
|
-
const compactMessage: SDKUserMessage = {
|
|
1403
|
-
type: "user",
|
|
1404
|
-
message: {
|
|
1405
|
-
role: "user",
|
|
1406
|
-
content: [{ type: "text", text: compactCommand }],
|
|
1407
|
-
},
|
|
1408
|
-
session_id: sessionId,
|
|
1409
|
-
parent_tool_use_id: null,
|
|
1410
|
-
};
|
|
1411
|
-
|
|
1412
|
-
input.push(compactMessage);
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
private async createSession(
|
|
1416
|
-
params: NewSessionRequest,
|
|
1417
|
-
creationOpts: { resume?: string; forkSession?: boolean } = {},
|
|
1418
|
-
): Promise<NewSessionResponse> {
|
|
1419
|
-
// We want to create a new session id unless it is resume,
|
|
1420
|
-
// but not resume + forkSession.
|
|
1421
|
-
let sessionId;
|
|
1422
|
-
if (creationOpts.forkSession) {
|
|
1423
|
-
sessionId = randomUUID();
|
|
1424
|
-
} else if (creationOpts.resume) {
|
|
1425
|
-
sessionId = creationOpts.resume;
|
|
1426
|
-
} else {
|
|
1427
|
-
sessionId = randomUUID();
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
const input = new Pushable<SDKUserMessage>();
|
|
1431
|
-
|
|
1432
|
-
const settingsManager = new SettingsManager(params.cwd, {
|
|
1433
|
-
logger: this.logger,
|
|
1434
|
-
});
|
|
1435
|
-
await settingsManager.initialize();
|
|
1436
|
-
|
|
1437
|
-
const mcpServers: Record<string, McpServerConfig> = {};
|
|
1438
|
-
if (Array.isArray(params.mcpServers)) {
|
|
1439
|
-
for (const server of params.mcpServers) {
|
|
1440
|
-
if ("type" in server) {
|
|
1441
|
-
mcpServers[server.name] = {
|
|
1442
|
-
type: server.type,
|
|
1443
|
-
url: server.url,
|
|
1444
|
-
headers: server.headers
|
|
1445
|
-
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
|
1446
|
-
: undefined,
|
|
1447
|
-
};
|
|
1448
|
-
} else {
|
|
1449
|
-
mcpServers[server.name] = {
|
|
1450
|
-
type: "stdio",
|
|
1451
|
-
command: server.command,
|
|
1452
|
-
args: server.args,
|
|
1453
|
-
env: server.env
|
|
1454
|
-
? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
|
|
1455
|
-
: undefined,
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// Only add the acp MCP server if built-in tools are not disabled
|
|
1462
|
-
if (!params._meta?.disableBuiltInTools) {
|
|
1463
|
-
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
|
1464
|
-
mcpServers["acp"] = {
|
|
1465
|
-
type: "sdk",
|
|
1466
|
-
name: "acp",
|
|
1467
|
-
instance: server,
|
|
1468
|
-
};
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
let systemPrompt: Options["systemPrompt"] = { type: "preset", preset: "claude_code" };
|
|
1472
|
-
if (params._meta?.systemPrompt) {
|
|
1473
|
-
const customPrompt = params._meta.systemPrompt;
|
|
1474
|
-
if (typeof customPrompt === "string") {
|
|
1475
|
-
systemPrompt = customPrompt;
|
|
1476
|
-
} else if (
|
|
1477
|
-
typeof customPrompt === "object" &&
|
|
1478
|
-
"append" in customPrompt &&
|
|
1479
|
-
typeof customPrompt.append === "string"
|
|
1480
|
-
) {
|
|
1481
|
-
systemPrompt.append = customPrompt.append;
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
const permissionMode = "default";
|
|
1486
|
-
|
|
1487
|
-
// Extract options from _meta if provided
|
|
1488
|
-
const userProvidedOptions = (params._meta as NewSessionMeta | undefined)?.claudeCode?.options;
|
|
1489
|
-
const extraArgs = { ...userProvidedOptions?.extraArgs };
|
|
1490
|
-
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
|
1491
|
-
// Set our own session id if not resuming an existing session.
|
|
1492
|
-
extraArgs["session-id"] = sessionId;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// Configure thinking tokens from environment variable
|
|
1496
|
-
const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
|
|
1497
|
-
? parseInt(process.env.MAX_THINKING_TOKENS, 10)
|
|
1498
|
-
: undefined;
|
|
1499
|
-
|
|
1500
|
-
const options: Options = {
|
|
1501
|
-
systemPrompt,
|
|
1502
|
-
// Use user-provided settingSources or default to all sources
|
|
1503
|
-
settingSources: userProvidedOptions?.settingSources ?? ["user", "project", "local"],
|
|
1504
|
-
stderr: (err) => this.logger.error(err),
|
|
1505
|
-
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
|
|
1506
|
-
...userProvidedOptions,
|
|
1507
|
-
// Override certain fields that must be controlled by ACP
|
|
1508
|
-
cwd: params.cwd,
|
|
1509
|
-
includePartialMessages: true,
|
|
1510
|
-
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
|
|
1511
|
-
extraArgs,
|
|
1512
|
-
// If we want bypassPermissions to be an option, we have to allow it here.
|
|
1513
|
-
// But it doesn't work in root mode, so we only activate it if it will work.
|
|
1514
|
-
allowDangerouslySkipPermissions: !IS_ROOT,
|
|
1515
|
-
permissionMode,
|
|
1516
|
-
canUseTool: this.canUseTool(sessionId),
|
|
1517
|
-
// note: although not documented by the types, passing an absolute path
|
|
1518
|
-
// here works to find zed's managed node version.
|
|
1519
|
-
executable: process.execPath as any,
|
|
1520
|
-
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
|
1521
|
-
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
|
1522
|
-
}),
|
|
1523
|
-
tools: { type: "preset", preset: "claude_code" },
|
|
1524
|
-
hooks: {
|
|
1525
|
-
...userProvidedOptions?.hooks,
|
|
1526
|
-
PreToolUse: [
|
|
1527
|
-
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
|
1528
|
-
{
|
|
1529
|
-
hooks: [createPreToolUseHook(settingsManager, this.logger)],
|
|
1530
|
-
},
|
|
1531
|
-
],
|
|
1532
|
-
PostToolUse: [
|
|
1533
|
-
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
1534
|
-
{
|
|
1535
|
-
hooks: [createPostToolUseHook(this.logger)],
|
|
1536
|
-
},
|
|
1537
|
-
],
|
|
1538
|
-
},
|
|
1539
|
-
...creationOpts,
|
|
1540
|
-
};
|
|
1541
|
-
|
|
1542
|
-
// Start with user-provided tools lists, then add ACP-managed tools
|
|
1543
|
-
const allowedTools = [...(userProvidedOptions?.allowedTools ?? [])];
|
|
1544
|
-
// Disable AskUserQuestion for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
|
1545
|
-
const disallowedTools = ["AskUserQuestion", ...(userProvidedOptions?.disallowedTools ?? [])];
|
|
1546
|
-
|
|
1547
|
-
// Check if built-in tools should be disabled
|
|
1548
|
-
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
|
1549
|
-
|
|
1550
|
-
if (!disableBuiltInTools) {
|
|
1551
|
-
if (this.clientCapabilities?.fs?.readTextFile) {
|
|
1552
|
-
allowedTools.push(acpToolNames.read);
|
|
1553
|
-
disallowedTools.push("Read");
|
|
1554
|
-
}
|
|
1555
|
-
if (this.clientCapabilities?.fs?.writeTextFile) {
|
|
1556
|
-
disallowedTools.push("Write", "Edit");
|
|
1557
|
-
}
|
|
1558
|
-
if (this.clientCapabilities?.terminal) {
|
|
1559
|
-
allowedTools.push(acpToolNames.bashOutput, acpToolNames.killShell);
|
|
1560
|
-
disallowedTools.push("Bash", "BashOutput", "KillShell");
|
|
1561
|
-
}
|
|
1562
|
-
} else {
|
|
1563
|
-
// When built-in tools are disabled, explicitly disallow all of them
|
|
1564
|
-
disallowedTools.push(
|
|
1565
|
-
acpToolNames.read,
|
|
1566
|
-
acpToolNames.write,
|
|
1567
|
-
acpToolNames.edit,
|
|
1568
|
-
acpToolNames.bash,
|
|
1569
|
-
acpToolNames.bashOutput,
|
|
1570
|
-
acpToolNames.killShell,
|
|
1571
|
-
"Read",
|
|
1572
|
-
"Write",
|
|
1573
|
-
"Edit",
|
|
1574
|
-
"Bash",
|
|
1575
|
-
"BashOutput",
|
|
1576
|
-
"KillShell",
|
|
1577
|
-
"Glob",
|
|
1578
|
-
"Grep",
|
|
1579
|
-
"Task",
|
|
1580
|
-
"TodoWrite",
|
|
1581
|
-
"ExitPlanMode",
|
|
1582
|
-
"WebSearch",
|
|
1583
|
-
"WebFetch",
|
|
1584
|
-
"AskUserQuestion",
|
|
1585
|
-
"SlashCommand",
|
|
1586
|
-
"Skill",
|
|
1587
|
-
"NotebookEdit",
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
if (allowedTools.length > 0) {
|
|
1592
|
-
options.allowedTools = allowedTools;
|
|
1593
|
-
}
|
|
1594
|
-
if (disallowedTools.length > 0) {
|
|
1595
|
-
options.disallowedTools = disallowedTools;
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
// Create our own AbortController for session management
|
|
1599
|
-
const sessionAbortController = new AbortController();
|
|
1600
|
-
|
|
1601
|
-
// Handle abort controller from meta options (user can still provide one)
|
|
1602
|
-
const userAbortController = userProvidedOptions?.abortController;
|
|
1603
|
-
if (userAbortController?.signal.aborted) {
|
|
1604
|
-
throw new Error("Cancelled");
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
// Pass the abort controller to the query options
|
|
1608
|
-
options.abortController = sessionAbortController;
|
|
1609
|
-
|
|
1610
|
-
const q = query({
|
|
1611
|
-
prompt: input,
|
|
1612
|
-
options,
|
|
1613
|
-
});
|
|
1614
|
-
|
|
1615
|
-
// Extract compaction config from _meta if provided
|
|
1616
|
-
const compactionConfig = (params._meta as NewSessionMeta | undefined)?.claudeCode?.compaction;
|
|
1617
|
-
const compactionState: CompactionState = {
|
|
1618
|
-
enabled: compactionConfig?.enabled ?? false,
|
|
1619
|
-
threshold: compactionConfig?.contextTokenThreshold ?? 100000,
|
|
1620
|
-
customInstructions: compactionConfig?.customInstructions,
|
|
1621
|
-
currentTokens: 0,
|
|
1622
|
-
isCompacting: false,
|
|
1623
|
-
};
|
|
1624
|
-
|
|
1625
|
-
this.sessions[sessionId] = {
|
|
1626
|
-
query: q,
|
|
1627
|
-
input: input,
|
|
1628
|
-
cancelled: false,
|
|
1629
|
-
permissionMode,
|
|
1630
|
-
settingsManager,
|
|
1631
|
-
abortController: sessionAbortController,
|
|
1632
|
-
cwd: params.cwd,
|
|
1633
|
-
compaction: compactionState,
|
|
1634
|
-
};
|
|
1635
|
-
|
|
1636
|
-
const availableCommands = await getAvailableSlashCommands(q);
|
|
1637
|
-
const models = await getAvailableModels(q);
|
|
1638
|
-
|
|
1639
|
-
// Needs to happen after we return the session
|
|
1640
|
-
setTimeout(() => {
|
|
1641
|
-
this.client.sessionUpdate({
|
|
1642
|
-
sessionId,
|
|
1643
|
-
update: {
|
|
1644
|
-
sessionUpdate: "available_commands_update",
|
|
1645
|
-
availableCommands,
|
|
1646
|
-
},
|
|
1647
|
-
});
|
|
1648
|
-
}, 0);
|
|
1649
|
-
|
|
1650
|
-
const availableModes = [
|
|
1651
|
-
{
|
|
1652
|
-
id: "default",
|
|
1653
|
-
name: "Default",
|
|
1654
|
-
description: "Standard behavior, prompts for dangerous operations",
|
|
1655
|
-
},
|
|
1656
|
-
{
|
|
1657
|
-
id: "acceptEdits",
|
|
1658
|
-
name: "Accept Edits",
|
|
1659
|
-
description: "Auto-accept file edit operations",
|
|
1660
|
-
},
|
|
1661
|
-
{
|
|
1662
|
-
id: "plan",
|
|
1663
|
-
name: "Plan Mode",
|
|
1664
|
-
description: "Planning mode, no actual tool execution",
|
|
1665
|
-
},
|
|
1666
|
-
{
|
|
1667
|
-
id: "dontAsk",
|
|
1668
|
-
name: "Don't Ask",
|
|
1669
|
-
description: "Don't prompt for permissions, deny if not pre-approved",
|
|
1670
|
-
},
|
|
1671
|
-
];
|
|
1672
|
-
// Only works in non-root mode
|
|
1673
|
-
if (!IS_ROOT) {
|
|
1674
|
-
availableModes.push({
|
|
1675
|
-
id: "bypassPermissions",
|
|
1676
|
-
name: "Bypass Permissions",
|
|
1677
|
-
description: "Bypass all permission checks",
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
return {
|
|
1682
|
-
sessionId,
|
|
1683
|
-
models,
|
|
1684
|
-
modes: {
|
|
1685
|
-
currentModeId: permissionMode,
|
|
1686
|
-
availableModes,
|
|
1687
|
-
},
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
async function getAvailableModels(query: Query): Promise<SessionModelState> {
|
|
1693
|
-
const models = await query.supportedModels();
|
|
1694
|
-
|
|
1695
|
-
// Query doesn't give us access to the currently selected model, so we just choose the first model in the list.
|
|
1696
|
-
const currentModel = models[0];
|
|
1697
|
-
await query.setModel(currentModel.value);
|
|
1698
|
-
|
|
1699
|
-
const availableModels = models.map((model) => ({
|
|
1700
|
-
modelId: model.value,
|
|
1701
|
-
name: model.displayName,
|
|
1702
|
-
description: model.description,
|
|
1703
|
-
}));
|
|
1704
|
-
|
|
1705
|
-
return {
|
|
1706
|
-
availableModels,
|
|
1707
|
-
currentModelId: currentModel.value,
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
|
|
1712
|
-
const UNSUPPORTED_COMMANDS = [
|
|
1713
|
-
"cost",
|
|
1714
|
-
"login",
|
|
1715
|
-
"logout",
|
|
1716
|
-
"output-style:new",
|
|
1717
|
-
"release-notes",
|
|
1718
|
-
"todos",
|
|
1719
|
-
];
|
|
1720
|
-
const commands = await query.supportedCommands();
|
|
1721
|
-
|
|
1722
|
-
return commands
|
|
1723
|
-
.map((command) => {
|
|
1724
|
-
const input = command.argumentHint
|
|
1725
|
-
? {
|
|
1726
|
-
hint: Array.isArray(command.argumentHint)
|
|
1727
|
-
? command.argumentHint.join(" ")
|
|
1728
|
-
: command.argumentHint,
|
|
1729
|
-
}
|
|
1730
|
-
: null;
|
|
1731
|
-
let name = command.name;
|
|
1732
|
-
if (command.name.endsWith(" (MCP)")) {
|
|
1733
|
-
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
1734
|
-
}
|
|
1735
|
-
return {
|
|
1736
|
-
name,
|
|
1737
|
-
description: command.description || "",
|
|
1738
|
-
input,
|
|
1739
|
-
};
|
|
1740
|
-
})
|
|
1741
|
-
.filter((command: AvailableCommand) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
function formatUriAsLink(uri: string): string {
|
|
1745
|
-
try {
|
|
1746
|
-
if (uri.startsWith("file://")) {
|
|
1747
|
-
const path = uri.slice(7); // Remove "file://"
|
|
1748
|
-
const name = path.split("/").pop() || path;
|
|
1749
|
-
return `[@${name}](${uri})`;
|
|
1750
|
-
} else if (uri.startsWith("zed://")) {
|
|
1751
|
-
const parts = uri.split("/");
|
|
1752
|
-
const name = parts[parts.length - 1] || uri;
|
|
1753
|
-
return `[@${name}](${uri})`;
|
|
1754
|
-
}
|
|
1755
|
-
return uri;
|
|
1756
|
-
} catch {
|
|
1757
|
-
return uri;
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
|
|
1762
|
-
const content: any[] = [];
|
|
1763
|
-
const context: any[] = [];
|
|
1764
|
-
|
|
1765
|
-
for (const chunk of prompt.prompt) {
|
|
1766
|
-
switch (chunk.type) {
|
|
1767
|
-
case "text": {
|
|
1768
|
-
let text = chunk.text;
|
|
1769
|
-
// change /mcp:server:command args -> /server:command (MCP) args
|
|
1770
|
-
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
|
|
1771
|
-
if (mcpMatch) {
|
|
1772
|
-
const [, server, command, args] = mcpMatch;
|
|
1773
|
-
text = `/${server}:${command} (MCP)${args || ""}`;
|
|
1774
|
-
}
|
|
1775
|
-
content.push({ type: "text", text });
|
|
1776
|
-
break;
|
|
1777
|
-
}
|
|
1778
|
-
case "resource_link": {
|
|
1779
|
-
const formattedUri = formatUriAsLink(chunk.uri);
|
|
1780
|
-
content.push({
|
|
1781
|
-
type: "text",
|
|
1782
|
-
text: formattedUri,
|
|
1783
|
-
});
|
|
1784
|
-
break;
|
|
1785
|
-
}
|
|
1786
|
-
case "resource": {
|
|
1787
|
-
if ("text" in chunk.resource) {
|
|
1788
|
-
const formattedUri = formatUriAsLink(chunk.resource.uri);
|
|
1789
|
-
content.push({
|
|
1790
|
-
type: "text",
|
|
1791
|
-
text: formattedUri,
|
|
1792
|
-
});
|
|
1793
|
-
context.push({
|
|
1794
|
-
type: "text",
|
|
1795
|
-
text: `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>`,
|
|
1796
|
-
});
|
|
1797
|
-
}
|
|
1798
|
-
// Ignore blob resources (unsupported)
|
|
1799
|
-
break;
|
|
1800
|
-
}
|
|
1801
|
-
case "image":
|
|
1802
|
-
if (chunk.data) {
|
|
1803
|
-
content.push({
|
|
1804
|
-
type: "image",
|
|
1805
|
-
source: {
|
|
1806
|
-
type: "base64",
|
|
1807
|
-
data: chunk.data,
|
|
1808
|
-
media_type: chunk.mimeType,
|
|
1809
|
-
},
|
|
1810
|
-
});
|
|
1811
|
-
} else if (chunk.uri && chunk.uri.startsWith("http")) {
|
|
1812
|
-
content.push({
|
|
1813
|
-
type: "image",
|
|
1814
|
-
source: {
|
|
1815
|
-
type: "url",
|
|
1816
|
-
url: chunk.uri,
|
|
1817
|
-
},
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
break;
|
|
1821
|
-
// Ignore audio and other unsupported types
|
|
1822
|
-
default:
|
|
1823
|
-
break;
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
content.push(...context);
|
|
1828
|
-
|
|
1829
|
-
return {
|
|
1830
|
-
type: "user",
|
|
1831
|
-
message: {
|
|
1832
|
-
role: "user",
|
|
1833
|
-
content: content,
|
|
1834
|
-
},
|
|
1835
|
-
session_id: prompt.sessionId,
|
|
1836
|
-
parent_tool_use_id: null,
|
|
1837
|
-
};
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
/**
|
|
1841
|
-
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
1842
|
-
* Only handles text, image, and thinking chunks for now.
|
|
1843
|
-
*/
|
|
1844
|
-
export function toAcpNotifications(
|
|
1845
|
-
content: string | ContentBlockParam[] | BetaContentBlock[] | BetaRawContentBlockDelta[],
|
|
1846
|
-
role: "assistant" | "user",
|
|
1847
|
-
sessionId: string,
|
|
1848
|
-
toolUseCache: ToolUseCache,
|
|
1849
|
-
client: AgentSideConnection,
|
|
1850
|
-
logger: Logger,
|
|
1851
|
-
): SessionNotification[] {
|
|
1852
|
-
if (typeof content === "string") {
|
|
1853
|
-
return [
|
|
1854
|
-
{
|
|
1855
|
-
sessionId,
|
|
1856
|
-
update: {
|
|
1857
|
-
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1858
|
-
content: {
|
|
1859
|
-
type: "text",
|
|
1860
|
-
text: content,
|
|
1861
|
-
},
|
|
1862
|
-
},
|
|
1863
|
-
},
|
|
1864
|
-
];
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
const output = [];
|
|
1868
|
-
// Only handle the first chunk for streaming; extend as needed for batching
|
|
1869
|
-
for (const chunk of content) {
|
|
1870
|
-
let update: SessionNotification["update"] | null = null;
|
|
1871
|
-
switch (chunk.type) {
|
|
1872
|
-
case "text":
|
|
1873
|
-
case "text_delta":
|
|
1874
|
-
update = {
|
|
1875
|
-
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1876
|
-
content: {
|
|
1877
|
-
type: "text",
|
|
1878
|
-
text: chunk.text,
|
|
1879
|
-
},
|
|
1880
|
-
};
|
|
1881
|
-
break;
|
|
1882
|
-
case "image":
|
|
1883
|
-
update = {
|
|
1884
|
-
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1885
|
-
content: {
|
|
1886
|
-
type: "image",
|
|
1887
|
-
data: chunk.source.type === "base64" ? chunk.source.data : "",
|
|
1888
|
-
mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
|
|
1889
|
-
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
|
|
1890
|
-
},
|
|
1891
|
-
};
|
|
1892
|
-
break;
|
|
1893
|
-
case "thinking":
|
|
1894
|
-
case "thinking_delta":
|
|
1895
|
-
update = {
|
|
1896
|
-
sessionUpdate: "agent_thought_chunk",
|
|
1897
|
-
content: {
|
|
1898
|
-
type: "text",
|
|
1899
|
-
text: chunk.thinking,
|
|
1900
|
-
},
|
|
1901
|
-
};
|
|
1902
|
-
break;
|
|
1903
|
-
case "tool_use":
|
|
1904
|
-
case "server_tool_use":
|
|
1905
|
-
case "mcp_tool_use": {
|
|
1906
|
-
toolUseCache[chunk.id] = chunk;
|
|
1907
|
-
if (chunk.name === "TodoWrite") {
|
|
1908
|
-
// @ts-expect-error - sometimes input is empty object
|
|
1909
|
-
if (Array.isArray(chunk.input.todos)) {
|
|
1910
|
-
update = {
|
|
1911
|
-
sessionUpdate: "plan",
|
|
1912
|
-
entries: planEntries(chunk.input as { todos: ClaudePlanEntry[] }),
|
|
1913
|
-
};
|
|
1914
|
-
}
|
|
1915
|
-
} else {
|
|
1916
|
-
// Register hook callback to receive the structured output from the hook
|
|
1917
|
-
registerHookCallback(chunk.id, {
|
|
1918
|
-
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
|
|
1919
|
-
const toolUse = toolUseCache[toolUseId];
|
|
1920
|
-
if (toolUse) {
|
|
1921
|
-
const update: SessionNotification["update"] = {
|
|
1922
|
-
_meta: {
|
|
1923
|
-
claudeCode: {
|
|
1924
|
-
toolResponse,
|
|
1925
|
-
toolName: toolUse.name,
|
|
1926
|
-
},
|
|
1927
|
-
} satisfies ToolUpdateMeta,
|
|
1928
|
-
toolCallId: toolUseId,
|
|
1929
|
-
sessionUpdate: "tool_call_update",
|
|
1930
|
-
};
|
|
1931
|
-
await client.sessionUpdate({
|
|
1932
|
-
sessionId,
|
|
1933
|
-
update,
|
|
1934
|
-
});
|
|
1935
|
-
} else {
|
|
1936
|
-
logger.error(
|
|
1937
|
-
`[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`,
|
|
1938
|
-
);
|
|
1939
|
-
}
|
|
1940
|
-
},
|
|
1941
|
-
});
|
|
1942
|
-
|
|
1943
|
-
let rawInput;
|
|
1944
|
-
try {
|
|
1945
|
-
rawInput = JSON.parse(JSON.stringify(chunk.input));
|
|
1946
|
-
} catch {
|
|
1947
|
-
// ignore if we can't turn it to JSON
|
|
1948
|
-
}
|
|
1949
|
-
update = {
|
|
1950
|
-
_meta: {
|
|
1951
|
-
claudeCode: {
|
|
1952
|
-
toolName: chunk.name,
|
|
1953
|
-
},
|
|
1954
|
-
} satisfies ToolUpdateMeta,
|
|
1955
|
-
toolCallId: chunk.id,
|
|
1956
|
-
sessionUpdate: "tool_call",
|
|
1957
|
-
rawInput,
|
|
1958
|
-
status: "pending",
|
|
1959
|
-
...toolInfoFromToolUse(chunk),
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
break;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
case "tool_result":
|
|
1966
|
-
case "tool_search_tool_result":
|
|
1967
|
-
case "web_fetch_tool_result":
|
|
1968
|
-
case "web_search_tool_result":
|
|
1969
|
-
case "code_execution_tool_result":
|
|
1970
|
-
case "bash_code_execution_tool_result":
|
|
1971
|
-
case "text_editor_code_execution_tool_result":
|
|
1972
|
-
case "mcp_tool_result": {
|
|
1973
|
-
const toolUse = toolUseCache[chunk.tool_use_id];
|
|
1974
|
-
if (!toolUse) {
|
|
1975
|
-
logger.error(
|
|
1976
|
-
`[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`,
|
|
1977
|
-
);
|
|
1978
|
-
break;
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
if (toolUse.name !== "TodoWrite") {
|
|
1982
|
-
update = {
|
|
1983
|
-
_meta: {
|
|
1984
|
-
claudeCode: {
|
|
1985
|
-
toolName: toolUse.name,
|
|
1986
|
-
},
|
|
1987
|
-
} satisfies ToolUpdateMeta,
|
|
1988
|
-
toolCallId: chunk.tool_use_id,
|
|
1989
|
-
sessionUpdate: "tool_call_update",
|
|
1990
|
-
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
1991
|
-
rawOutput: chunk.content,
|
|
1992
|
-
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
|
|
1993
|
-
};
|
|
1994
|
-
}
|
|
1995
|
-
break;
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
case "document":
|
|
1999
|
-
case "search_result":
|
|
2000
|
-
case "redacted_thinking":
|
|
2001
|
-
case "input_json_delta":
|
|
2002
|
-
case "citations_delta":
|
|
2003
|
-
case "signature_delta":
|
|
2004
|
-
case "container_upload":
|
|
2005
|
-
break;
|
|
2006
|
-
|
|
2007
|
-
default:
|
|
2008
|
-
unreachable(chunk, logger);
|
|
2009
|
-
break;
|
|
2010
|
-
}
|
|
2011
|
-
if (update) {
|
|
2012
|
-
output.push({ sessionId, update });
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
return output;
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
export function streamEventToAcpNotifications(
|
|
2020
|
-
message: SDKPartialAssistantMessage,
|
|
2021
|
-
sessionId: string,
|
|
2022
|
-
toolUseCache: ToolUseCache,
|
|
2023
|
-
client: AgentSideConnection,
|
|
2024
|
-
logger: Logger,
|
|
2025
|
-
): SessionNotification[] {
|
|
2026
|
-
const event = message.event;
|
|
2027
|
-
switch (event.type) {
|
|
2028
|
-
case "content_block_start":
|
|
2029
|
-
return toAcpNotifications(
|
|
2030
|
-
[event.content_block],
|
|
2031
|
-
"assistant",
|
|
2032
|
-
sessionId,
|
|
2033
|
-
toolUseCache,
|
|
2034
|
-
client,
|
|
2035
|
-
logger,
|
|
2036
|
-
);
|
|
2037
|
-
case "content_block_delta":
|
|
2038
|
-
return toAcpNotifications(
|
|
2039
|
-
[event.delta],
|
|
2040
|
-
"assistant",
|
|
2041
|
-
sessionId,
|
|
2042
|
-
toolUseCache,
|
|
2043
|
-
client,
|
|
2044
|
-
logger,
|
|
2045
|
-
);
|
|
2046
|
-
// No content
|
|
2047
|
-
case "message_start":
|
|
2048
|
-
case "message_delta":
|
|
2049
|
-
case "message_stop":
|
|
2050
|
-
case "content_block_stop":
|
|
2051
|
-
return [];
|
|
2052
|
-
|
|
2053
|
-
default:
|
|
2054
|
-
unreachable(event, logger);
|
|
2055
|
-
return [];
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
export function runAcp() {
|
|
2060
|
-
const input = nodeToWebWritable(process.stdout);
|
|
2061
|
-
const output = nodeToWebReadable(process.stdin);
|
|
2062
|
-
|
|
2063
|
-
const stream = ndJsonStream(input, output);
|
|
2064
|
-
new AgentSideConnection((client) => new ClaudeAcpAgent(client), stream);
|
|
2065
|
-
}
|