airaknit 1.1.2-rc.9
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/LICENSE +84 -0
- package/README.md +202 -0
- package/bin/airaknit +9 -0
- package/bin/airaknit-project +14 -0
- package/bin/kanna +9 -0
- package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
- package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
- package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
- package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
- package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
- package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
- package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
- package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
- package/dist/client/assets/arc-B07zg7ol.js +1 -0
- package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
- package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
- package/dist/client/assets/array-BGFCBI0e.js +1 -0
- package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
- package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
- package/dist/client/assets/button-DO50qOGv.js +1 -0
- package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
- package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
- package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
- package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
- package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
- package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
- package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
- package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
- package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
- package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
- package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
- package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
- package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
- package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
- package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
- package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
- package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
- package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
- package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
- package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
- package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
- package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
- package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
- package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
- package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
- package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
- package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
- package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
- package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
- package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
- package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
- package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
- package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
- package/dist/client/assets/client-BrKWI4CM.js +1 -0
- package/dist/client/assets/client-CGgNRU9w.js +1 -0
- package/dist/client/assets/client-DMSLRzg9.js +6 -0
- package/dist/client/assets/clone-DWcL7whJ.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
- package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
- package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
- package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
- package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
- package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
- package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
- package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
- package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
- package/dist/client/assets/dist-CuB4kiSK.js +1 -0
- package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
- package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
- package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
- package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
- package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
- package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
- package/dist/client/assets/index-CetCiuqP.js +105 -0
- package/dist/client/assets/index-N29Mip7A.css +1 -0
- package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
- package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
- package/dist/client/assets/init-B8gtcn7T.js +1 -0
- package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
- package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
- package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
- package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
- package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
- package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
- package/dist/client/assets/katex-B94qP8b6.js +265 -0
- package/dist/client/assets/lib--QVjyxmL.js +29 -0
- package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
- package/dist/client/assets/line-DCrYfLBn.js +1 -0
- package/dist/client/assets/linear-_4upLmeo.js +1 -0
- package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
- package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
- package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
- package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
- package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
- package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
- package/dist/client/assets/path-DZF-JdEe.js +1 -0
- package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
- package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
- package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
- package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
- package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
- package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
- package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
- package/dist/client/assets/react-CClhXMB2.js +1 -0
- package/dist/client/assets/react-Dd6D81m0.js +1 -0
- package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
- package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
- package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
- package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
- package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
- package/dist/client/assets/src-DeTlMJAU.js +1 -0
- package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
- package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
- package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
- package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
- package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
- package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
- package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
- package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
- package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/favicon.svg +15 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +31 -0
- package/dist/client/manifest.webmanifest +12 -0
- package/dist/client/sw.js +32 -0
- package/package.json +122 -0
- package/src/nats/auth-callout/callout-config.test.ts +93 -0
- package/src/nats/auth-callout/callout-config.ts +109 -0
- package/src/nats/auth-callout/callout.integration.test.ts +332 -0
- package/src/nats/auth-callout/keys.ts +103 -0
- package/src/nats/auth-callout/responder.ts +241 -0
- package/src/nats/auth-callout/scope-policy.test.ts +159 -0
- package/src/nats/auth-callout/scope-policy.ts +210 -0
- package/src/nats/auth-callout/token.test.ts +163 -0
- package/src/nats/auth-callout/token.ts +157 -0
- package/src/nats/nats-daemon-callout.ts +194 -0
- package/src/nats/nats-daemon.test.ts +77 -0
- package/src/nats/nats-daemon.ts +50 -0
- package/src/nats/nats-token.test.ts +61 -0
- package/src/nats/nats-token.ts +59 -0
- package/src/runner/coordination-mcp-integration.test.ts +134 -0
- package/src/runner/nats-coordination-client.test.ts +49 -0
- package/src/runner/nats-coordination-client.ts +94 -0
- package/src/runner/runner-agent.test.ts +469 -0
- package/src/runner/runner-agent.ts +453 -0
- package/src/runner/runner-credential.test.ts +93 -0
- package/src/runner/runner-credential.ts +82 -0
- package/src/runner/runner-nats.test.ts +495 -0
- package/src/runner/runner-nats.ts +323 -0
- package/src/runner/runner-pair.test.ts +107 -0
- package/src/runner/runner-pair.ts +81 -0
- package/src/runner/runner.test.ts +135 -0
- package/src/runner/runner.ts +212 -0
- package/src/runner/turn-factories.test.ts +97 -0
- package/src/runner/turn-factories.ts +475 -0
- package/src/server/agent-config-journey.test.ts +106 -0
- package/src/server/agent.ts +8 -0
- package/src/server/auto-continue/auth-error-detector.ts +66 -0
- package/src/server/auto-continue/limit-detector.ts +194 -0
- package/src/server/bm25.test.ts +92 -0
- package/src/server/bm25.ts +101 -0
- package/src/server/chat-events-jetstream.test.ts +135 -0
- package/src/server/claude-harness.ts +360 -0
- package/src/server/claude-pty/agent-normalizers.ts +309 -0
- package/src/server/claude-pty/auth.test.ts +38 -0
- package/src/server/claude-pty/auth.ts +32 -0
- package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
- package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
- package/src/server/claude-pty/driver.test.ts +902 -0
- package/src/server/claude-pty/driver.ts +807 -0
- package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
- package/src/server/claude-pty/jsonl-path.test.ts +114 -0
- package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
- package/src/server/claude-pty/jsonl-to-event.ts +174 -0
- package/src/server/claude-pty/output-ring.test.ts +35 -0
- package/src/server/claude-pty/output-ring.ts +25 -0
- package/src/server/claude-pty/parity-matrix.test.ts +227 -0
- package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
- package/src/server/claude-pty/pid-registry.test.ts +122 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
- package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
- package/src/server/claude-pty/pty-instance-registry.ts +166 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
- package/src/server/claude-pty/pty-process.adapter.ts +66 -0
- package/src/server/claude-pty/pty-process.test.ts +49 -0
- package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
- package/src/server/claude-pty/resolve-binary.test.ts +118 -0
- package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
- package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
- package/src/server/claude-pty/settings-writer.test.ts +22 -0
- package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
- package/src/server/claude-pty/smoke-test.test.ts +191 -0
- package/src/server/claude-pty/smoke-test.ts +185 -0
- package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
- package/src/server/claude-pty/tool-callback.ts +274 -0
- package/src/server/claude-pty/tui-control.test.ts +272 -0
- package/src/server/claude-pty/tui-control.ts +182 -0
- package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
- package/src/server/claude-pty/tui-source.adapter.ts +343 -0
- package/src/server/claude-pty/tunnel-gateway.ts +12 -0
- package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
- package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
- package/src/server/claude-pty-mcp/history-primer.ts +90 -0
- package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
- package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
- package/src/server/claude-pty-mcp/mcp.ts +412 -0
- package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
- package/src/server/claude-pty-mcp/paths.ts +24 -0
- package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
- package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
- package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
- package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
- package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
- package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
- package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
- package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
- package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
- package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
- package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
- package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
- package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
- package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
- package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
- package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
- package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
- package/src/server/claude-pty-mcp/uploads.ts +38 -0
- package/src/server/claude-turn.test.ts +176 -0
- package/src/server/cli-runtime.test.ts +456 -0
- package/src/server/cli-runtime.ts +374 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +78 -0
- package/src/server/client-log-forwarder.test.ts +74 -0
- package/src/server/client-log-forwarder.ts +75 -0
- package/src/server/codex-app-server-protocol.ts +449 -0
- package/src/server/codex-app-server.test.ts +2990 -0
- package/src/server/codex-app-server.ts +1713 -0
- package/src/server/coordination-integration.test.ts +63 -0
- package/src/server/coordination-mcp.test.ts +149 -0
- package/src/server/coordination-mcp.ts +197 -0
- package/src/server/delegation-coordinator.test.ts +675 -0
- package/src/server/delegation-coordinator.ts +454 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store-agent-config.test.ts +124 -0
- package/src/server/event-store-coordination.test.ts +149 -0
- package/src/server/event-store-profile.test.ts +132 -0
- package/src/server/event-store-repo.test.ts +154 -0
- package/src/server/event-store-runner-team.test.ts +104 -0
- package/src/server/event-store.test.ts +342 -0
- package/src/server/event-store.ts +2208 -0
- package/src/server/events.ts +379 -0
- package/src/server/extension-router.test.ts +183 -0
- package/src/server/extension-router.ts +114 -0
- package/src/server/extensions/agents/server.test.ts +191 -0
- package/src/server/extensions/agents/server.ts +108 -0
- package/src/server/extensions/c3/server.test.ts +284 -0
- package/src/server/extensions/c3/server.ts +212 -0
- package/src/server/extensions/code/server.test.ts +200 -0
- package/src/server/extensions/code/server.ts +150 -0
- package/src/server/extensions.config.ts +10 -0
- package/src/server/external-open.ts +69 -0
- package/src/server/generate-fork-context.ts +58 -0
- package/src/server/generate-merge-context.test.ts +290 -0
- package/src/server/generate-merge-context.ts +141 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-clone-policy.test.ts +138 -0
- package/src/server/git-clone-policy.ts +27 -0
- package/src/server/harness-types.ts +1 -0
- package/src/server/journey-verification.test.ts +640 -0
- package/src/server/journey-verification.ts +195 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/nats-auth.test.ts +92 -0
- package/src/server/nats-auth.ts +6 -0
- package/src/server/nats-bind-guard.test.ts +71 -0
- package/src/server/nats-bind-guard.ts +42 -0
- package/src/server/nats-bridge.test.ts +141 -0
- package/src/server/nats-bridge.ts +111 -0
- package/src/server/nats-connector.test.ts +56 -0
- package/src/server/nats-connector.ts +71 -0
- package/src/server/nats-daemon-manager.test.ts +76 -0
- package/src/server/nats-daemon-manager.ts +174 -0
- package/src/server/nats-publisher.test.ts +356 -0
- package/src/server/nats-publisher.ts +271 -0
- package/src/server/nats-responders.test.ts +1018 -0
- package/src/server/nats-responders.ts +925 -0
- package/src/server/nats-streams.test.ts +112 -0
- package/src/server/nats-streams.ts +129 -0
- package/src/server/oauth-pool/oauth-responders.ts +185 -0
- package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
- package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
- package/src/server/orchestration.test.ts +1063 -0
- package/src/server/orchestration.ts +716 -0
- package/src/server/pairing-endpoints.test.ts +171 -0
- package/src/server/pairing-store.test.ts +154 -0
- package/src/server/pairing-store.ts +124 -0
- package/src/server/paths.ts +27 -0
- package/src/server/pr3-liveness.test.ts +252 -0
- package/src/server/process-utils.ts +10 -0
- package/src/server/project-cli.ts +180 -0
- package/src/server/provider-catalog.test.ts +177 -0
- package/src/server/provider-catalog.ts +146 -0
- package/src/server/pty-responders.ts +345 -0
- package/src/server/push-notifications.test.ts +234 -0
- package/src/server/push-notifications.ts +188 -0
- package/src/server/quick-response.test.ts +196 -0
- package/src/server/quick-response.ts +154 -0
- package/src/server/read-models-agent-config.test.ts +59 -0
- package/src/server/read-models-coordination.test.ts +69 -0
- package/src/server/read-models.test.ts +332 -0
- package/src/server/read-models.ts +258 -0
- package/src/server/repo-journey.test.ts +96 -0
- package/src/server/repo-manager.test.ts +118 -0
- package/src/server/repo-manager.ts +97 -0
- package/src/server/repo-status.test.ts +54 -0
- package/src/server/repo-status.ts +82 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/runner-incompatible-gate.test.ts +383 -0
- package/src/server/runner-manager.test.ts +72 -0
- package/src/server/runner-manager.ts +312 -0
- package/src/server/runner-pairing-urls.test.ts +59 -0
- package/src/server/runner-pairing-urls.ts +67 -0
- package/src/server/runner-proxy.test.ts +456 -0
- package/src/server/runner-proxy.ts +494 -0
- package/src/server/runner-router.test.ts +429 -0
- package/src/server/runner-router.ts +212 -0
- package/src/server/runner-routing.test.ts +584 -0
- package/src/server/runtime-registry.test.ts +436 -0
- package/src/server/runtime-registry.ts +307 -0
- package/src/server/sandbox-health.test.ts +127 -0
- package/src/server/sandbox-health.ts +94 -0
- package/src/server/sandbox-journey.test.ts +232 -0
- package/src/server/sandbox-manager.test.ts +146 -0
- package/src/server/sandbox-manager.ts +159 -0
- package/src/server/server.test.ts +287 -0
- package/src/server/server.ts +1108 -0
- package/src/server/session-discovery.test.ts +129 -0
- package/src/server/session-discovery.ts +475 -0
- package/src/server/session-index.test.ts +362 -0
- package/src/server/session-index.ts +119 -0
- package/src/server/session-seed.ts +288 -0
- package/src/server/share.test.ts +108 -0
- package/src/server/share.ts +113 -0
- package/src/server/skill-discovery.test.ts +201 -0
- package/src/server/skill-discovery.ts +77 -0
- package/src/server/storage/test-helpers.ts +67 -0
- package/src/server/terminal-manager.test.ts +309 -0
- package/src/server/terminal-manager.ts +354 -0
- package/src/server/transcript-consumer.test.ts +339 -0
- package/src/server/transcript-consumer.ts +162 -0
- package/src/server/transcript-search.test.ts +193 -0
- package/src/server/transcript-search.ts +83 -0
- package/src/server/transcript-utils.ts +52 -0
- package/src/server/update-manager.test.ts +107 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/workflow-engine.test.ts +251 -0
- package/src/server/workflow-engine.ts +169 -0
- package/src/server/workflow-mcp.ts +49 -0
- package/src/server/workflow-store.test.ts +155 -0
- package/src/server/workflow-store.ts +139 -0
- package/src/server/workspace-agent-integration.test.ts +167 -0
- package/src/server/workspace-agent-routes.test.ts +127 -0
- package/src/server/workspace-agent-routes.ts +89 -0
- package/src/server/workspace-agent.test.ts +103 -0
- package/src/server/workspace-agent.ts +102 -0
- package/src/server/workspace-cli.test.ts +79 -0
- package/src/server/workspace-config-manager.test.ts +109 -0
- package/src/server/workspace-config-manager.ts +83 -0
- package/src/server/workspace-directory-policy.test.ts +109 -0
- package/src/server/workspace-directory-policy.ts +56 -0
- package/src/shared/agent-config-types.ts +25 -0
- package/src/shared/branding.test.ts +42 -0
- package/src/shared/branding.ts +54 -0
- package/src/shared/compression.test.ts +85 -0
- package/src/shared/compression.ts +42 -0
- package/src/shared/coordination-store.test.ts +24 -0
- package/src/shared/coordination-store.ts +26 -0
- package/src/shared/dev-ports.test.ts +84 -0
- package/src/shared/dev-ports.ts +100 -0
- package/src/shared/extension-types.ts +45 -0
- package/src/shared/fork-presets.ts +54 -0
- package/src/shared/harness-types.test.ts +15 -0
- package/src/shared/harness-types.ts +21 -0
- package/src/shared/log-sink.test.ts +112 -0
- package/src/shared/log-sink.ts +185 -0
- package/src/shared/mention-pattern.ts +7 -0
- package/src/shared/merge-presets.ts +41 -0
- package/src/shared/nats-subjects.test.ts +61 -0
- package/src/shared/nats-subjects.ts +131 -0
- package/src/shared/permission-policy.ts +136 -0
- package/src/shared/ports.ts +3 -0
- package/src/shared/preset-types.ts +15 -0
- package/src/shared/profile-types.ts +49 -0
- package/src/shared/projectFileUrl.ts +36 -0
- package/src/shared/protocol.ts +153 -0
- package/src/shared/pty-instance.ts +43 -0
- package/src/shared/puggy/diagnostics/result.ts +18 -0
- package/src/shared/puggy/expressions/evaluate.ts +292 -0
- package/src/shared/puggy/index.test.ts +101 -0
- package/src/shared/puggy/index.ts +69 -0
- package/src/shared/puggy/parser/ast.ts +110 -0
- package/src/shared/puggy/parser/parser.ts +624 -0
- package/src/shared/puggy/renderer/html.ts +447 -0
- package/src/shared/runner-protocol.test.ts +277 -0
- package/src/shared/runner-protocol.ts +210 -0
- package/src/shared/runner-team-types.ts +73 -0
- package/src/shared/runtime-types.ts +48 -0
- package/src/shared/sandbox-types.ts +53 -0
- package/src/shared/tailwind-build.test.ts +12 -0
- package/src/shared/tinkaria-system-prompt.ts +101 -0
- package/src/shared/tools.test.ts +335 -0
- package/src/shared/tools.ts +397 -0
- package/src/shared/transcript-entries.ts +27 -0
- package/src/shared/transcript-render.test.ts +225 -0
- package/src/shared/transcript-render.ts +467 -0
- package/src/shared/types.ts +1031 -0
- package/src/shared/vite-config.test.ts +47 -0
- package/src/shared/web-context.test.ts +110 -0
- package/src/shared/web-context.ts +76 -0
- package/src/shared/workflow-types.ts +51 -0
- package/src/shared/workspace-types.ts +100 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { randomUUID } from "node:crypto"
|
|
4
|
+
import { createRuntimeDir, writeRuntimeFile, removeRuntimeDir } from "./runtime-dir.adapter"
|
|
5
|
+
import { verifyPtyAuth } from "./auth"
|
|
6
|
+
import { startKannaMcpHttpServer, buildMcpConfigJson, type KannaMcpHttpHandle } from "../claude-pty-mcp/mcp-http"
|
|
7
|
+
import type { KannaMcpDelegationContext } from "../claude-pty-mcp/mcp"
|
|
8
|
+
import type { SubagentOrchestrator } from "./subagent-orchestrator"
|
|
9
|
+
import { parseConfiguredContextWindowFromModelId, timestamped } from "./agent-normalizers"
|
|
10
|
+
import { KANNA_SYSTEM_PROMPT_APPEND } from "../../shared/tinkaria-system-prompt"
|
|
11
|
+
import { resolveClaudeBinary } from "./resolve-binary.adapter"
|
|
12
|
+
import { createJsonlEventParser } from "./jsonl-to-event"
|
|
13
|
+
import { OutputRing, OUTPUT_RING_DEFAULT_BYTES } from "./output-ring"
|
|
14
|
+
import { createSmokeTestGate, createFileSmokeTestCache, buildLiveSmokeProbe, type SmokeTestGate } from "./smoke-test"
|
|
15
|
+
import { computeBinarySha256 } from "./preflight/binary-fingerprint.adapter"
|
|
16
|
+
import { spawnPtyProcess as defaultSpawnPtyProcess, type PtyProcess, type SpawnPtyProcessArgs } from "./pty-process.adapter"
|
|
17
|
+
import type { ClaudePtyRegistry } from "./pid-registry.adapter"
|
|
18
|
+
import type { PtyInstanceRegistry } from "./pty-instance-registry"
|
|
19
|
+
import { sampleProcessTreeUsage as defaultSampleProcessTreeUsage, type ProcessTreeSample } from "./pty-memory-sampler.adapter"
|
|
20
|
+
import { waitForTuiReady, waitForTuiReadyWithTrustDismiss, sendUserPrompt, sendExitCommand } from "./tui-control"
|
|
21
|
+
import { startTranscriptStream } from "./tui-source.adapter"
|
|
22
|
+
import { computeJsonlPath, computeProjectDir } from "./jsonl-path.adapter"
|
|
23
|
+
import type { ClaudeSessionHandle } from "./agent-normalizers"
|
|
24
|
+
import type { HarnessEvent, HarnessToolRequest } from "../harness-types"
|
|
25
|
+
import type { AccountInfo, McpServerConfig, SlashCommand } from "../../shared/types"
|
|
26
|
+
import type { ToolCallbackService } from "./tool-callback"
|
|
27
|
+
import type { TunnelGateway } from "./tunnel-gateway"
|
|
28
|
+
import type { ChatPermissionPolicy } from "../../shared/permission-policy"
|
|
29
|
+
|
|
30
|
+
// Fallback list returned by getSupportedCommands() if claude's system_init
|
|
31
|
+
// JSONL message hasn't been observed yet (cold start before first spawn).
|
|
32
|
+
// Names follow claude's own format — no leading "/" — so the chat input
|
|
33
|
+
// renders `/clear` (not `//clear`) after `applyCommandToInput` prefixes the
|
|
34
|
+
// slash. The driver overwrites this with the full live list as soon as
|
|
35
|
+
// the spawned claude subprocess emits its system_init entry.
|
|
36
|
+
const STATIC_SUPPORTED_COMMANDS: SlashCommand[] = [
|
|
37
|
+
{ name: "model", description: "Switch model", argumentHint: "model name" },
|
|
38
|
+
{ name: "exit", description: "Exit the session", argumentHint: "" },
|
|
39
|
+
{ name: "clear", description: "Clear context", argumentHint: "" },
|
|
40
|
+
{ name: "help", description: "List commands", argumentHint: "" },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
export interface StartClaudeSessionPtyArgs {
|
|
44
|
+
chatId: string
|
|
45
|
+
projectId: string
|
|
46
|
+
localPath: string
|
|
47
|
+
model: string
|
|
48
|
+
effort?: string
|
|
49
|
+
planMode: boolean
|
|
50
|
+
forkSession: boolean
|
|
51
|
+
oauthToken: string | null
|
|
52
|
+
sessionToken: string | null
|
|
53
|
+
additionalDirectories?: string[]
|
|
54
|
+
onToolRequest: (request: HarnessToolRequest) => Promise<unknown>
|
|
55
|
+
/**
|
|
56
|
+
* Append text for `--append-system-prompt`. Defaults to the static
|
|
57
|
+
* {@link KANNA_SYSTEM_PROMPT_APPEND} blurb for back-compat with older
|
|
58
|
+
* callers; production callers in `agent.ts` pass the dynamic value
|
|
59
|
+
* from `buildKannaSystemPromptAppend` so the subagent roster is
|
|
60
|
+
* embedded.
|
|
61
|
+
*/
|
|
62
|
+
systemPromptAppend?: string
|
|
63
|
+
systemPromptOverride?: string
|
|
64
|
+
initialPrompt?: string
|
|
65
|
+
homeDir?: string
|
|
66
|
+
env?: NodeJS.ProcessEnv
|
|
67
|
+
/** Routes AskUserQuestion/ExitPlanMode + built-in shims through durable approval when KANNA_MCP_TOOL_CALLBACKS=1. */
|
|
68
|
+
toolCallback?: ToolCallbackService
|
|
69
|
+
/** Tunnel gateway for kanna-mcp expose_port. */
|
|
70
|
+
tunnelGateway?: TunnelGateway | null
|
|
71
|
+
/** Per-chat permission policy for kanna-mcp built-in shims. */
|
|
72
|
+
chatPolicy?: ChatPermissionPolicy
|
|
73
|
+
/** Orchestrator for delegate_subagent. Omit to hide the tool from the model. */
|
|
74
|
+
subagentOrchestrator?: SubagentOrchestrator
|
|
75
|
+
/** Per-spawn delegation context (depth / ancestor chain / parentUserMessageId resolver). */
|
|
76
|
+
delegationContext?: KannaMcpDelegationContext
|
|
77
|
+
/** Enabled user-defined MCP servers, written into mcp-config.json. */
|
|
78
|
+
customMcpServers?: readonly McpServerConfig[]
|
|
79
|
+
/** Optional override used by tests to inject a fake HTTP MCP starter. */
|
|
80
|
+
startKannaMcpHttpServer?: typeof startKannaMcpHttpServer
|
|
81
|
+
/** Optional smoke-test gate override (used by tests to inject a fake gate). */
|
|
82
|
+
smokeTestGate?: SmokeTestGate
|
|
83
|
+
/** Optional PTY spawn override (used by tests to inject a fake PTY). */
|
|
84
|
+
spawnPtyProcess?: (args: SpawnPtyProcessArgs) => Promise<PtyProcess>
|
|
85
|
+
/** Optional transcript stream factory override (used by tests). */
|
|
86
|
+
startTranscriptStreamFn?: typeof startTranscriptStream
|
|
87
|
+
/**
|
|
88
|
+
* One-shot semantics: after the first `result` entry, close stdin so
|
|
89
|
+
* the subprocess exits. Mirrors the SDK driver's prompt-queue close
|
|
90
|
+
* for single-turn subagent runs.
|
|
91
|
+
*/
|
|
92
|
+
oneShot?: boolean
|
|
93
|
+
/** Label of the OAuth-pool token. Surfaces in AccountInfo since the CLI doesn't emit account info in stream-json. */
|
|
94
|
+
oauthLabel?: string
|
|
95
|
+
/** Masked OAuth-pool token (e.g. `sk-ant-oat01...XXXX`). Computed by AgentCoordinator; never the raw token. */
|
|
96
|
+
oauthKeyMasked?: string
|
|
97
|
+
/**
|
|
98
|
+
* Optional on-disk registry of claude PTY children so a non-graceful
|
|
99
|
+
* server crash can reap orphan processes on the next boot. When set,
|
|
100
|
+
* the driver registers the spawn's pid + runtimeDir before sending the
|
|
101
|
+
* first prompt and unregisters during cleanup.
|
|
102
|
+
*/
|
|
103
|
+
ptyRegistry?: ClaudePtyRegistry
|
|
104
|
+
/**
|
|
105
|
+
* Optional in-memory live-status registry surfaced to the client UI.
|
|
106
|
+
* Driver upserts phase transitions; ws-router fans deltas out to
|
|
107
|
+
* subscribed sockets.
|
|
108
|
+
*/
|
|
109
|
+
ptyInstanceRegistry?: PtyInstanceRegistry
|
|
110
|
+
/** Optional sampler override (tests inject deterministic values). */
|
|
111
|
+
sampleProcessTreeUsage?: (pid: number) => Promise<ProcessTreeSample | null>
|
|
112
|
+
/** Optional poll-interval override (ms). Defaults to 2000. */
|
|
113
|
+
memorySamplerIntervalMs?: number
|
|
114
|
+
/**
|
|
115
|
+
* Optional usage-sample sink. Invoked on every memory-sampler tick with the
|
|
116
|
+
* process-tree resident memory and CPU% plus their session peaks. Lets the
|
|
117
|
+
* runner path (which publishes its own deltas instead of wiring a
|
|
118
|
+
* PtyInstanceRegistry) surface cpu/mem to the UI.
|
|
119
|
+
*/
|
|
120
|
+
onUsageSample?: (usage: PtyUsageSample) => void
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PtyUsageSample {
|
|
124
|
+
rssBytes: number
|
|
125
|
+
rssPeakBytes: number
|
|
126
|
+
cpuPercent: number
|
|
127
|
+
cpuPeakPercent: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Derive an AccountInfo from the picked OAuth-pool token. The claude CLI
|
|
132
|
+
* never emits account info in stream-json, so the user-configured token
|
|
133
|
+
* label and the coordinator-computed masked key are the only account
|
|
134
|
+
* signals PTY has.
|
|
135
|
+
*/
|
|
136
|
+
export function deriveAccountInfoFromOauth(args: { label?: string; oauthKeyMasked?: string }): AccountInfo | null {
|
|
137
|
+
const hasLabel = Boolean(args.label && args.label.length > 0)
|
|
138
|
+
const hasMasked = Boolean(args.oauthKeyMasked && args.oauthKeyMasked.length > 0)
|
|
139
|
+
if (!hasLabel && !hasMasked) return null
|
|
140
|
+
const info: AccountInfo = { tokenSource: "kanna-oauth-pool" }
|
|
141
|
+
if (hasLabel) info.organization = args.label
|
|
142
|
+
if (hasMasked) info.oauthKeyMasked = args.oauthKeyMasked
|
|
143
|
+
return info
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** VT100 Shift+Tab sequence sent to exit plan mode (one press cycles back to acceptEdits). */
|
|
147
|
+
export const SHIFT_TAB_KEY = "\x1b[Z"
|
|
148
|
+
|
|
149
|
+
export const PLAN_MODE_EXIT_UNSUPPORTED =
|
|
150
|
+
"[claude-pty] cannot exit plan mode: driver-tracked plan mode is inactive "
|
|
151
|
+
+ "(plan mode may have been toggled externally via Shift+Tab). "
|
|
152
|
+
+ "Restart the session to return to acceptEdits."
|
|
153
|
+
|
|
154
|
+
/** Backward-compat re-exports — callers that import from driver.ts continue to work. */
|
|
155
|
+
export const PTY_STDERR_RING_BYTES = OUTPUT_RING_DEFAULT_BYTES
|
|
156
|
+
export { OutputRing }
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Native CLI built-ins removed from the model's context under PTY (issue
|
|
160
|
+
* #215). The SDK driver intercepts these via the `canUseTool` hook
|
|
161
|
+
* (`buildCanUseTool` in agent.ts); PTY has no such hook, so the CLI
|
|
162
|
+
* auto-rejects them with `is_error: "Answer questions?"` and the model
|
|
163
|
+
* mis-reads it as a user cancel. Disallowing the natives forces the model
|
|
164
|
+
* onto the `mcp__kanna__ask_user_question` / `mcp__kanna__exit_plan_mode`
|
|
165
|
+
* shims, which the PTY driver always registers (forceInteractiveToolCallbacks)
|
|
166
|
+
* and which route through the durable approval protocol to the UI.
|
|
167
|
+
* `EnterPlanMode` is intentionally excluded — it has no user round-trip and
|
|
168
|
+
* the SDK hook never intercepts it, so leaving it native preserves parity.
|
|
169
|
+
*/
|
|
170
|
+
export const PTY_DISALLOWED_NATIVE_TOOLS = ["AskUserQuestion", "ExitPlanMode"] as const
|
|
171
|
+
|
|
172
|
+
export interface BuildPtyCliArgsInput {
|
|
173
|
+
sessionId: string
|
|
174
|
+
model: string
|
|
175
|
+
effort?: string
|
|
176
|
+
planMode: boolean
|
|
177
|
+
sessionToken: string | null
|
|
178
|
+
forkSession: boolean
|
|
179
|
+
additionalDirectories?: string[]
|
|
180
|
+
systemPromptOverride?: string
|
|
181
|
+
systemPromptAppend?: string
|
|
182
|
+
/** Absolute path to kanna's own mcp-config JSON. Merged with user's MCP configs (no --strict-mcp-config). */
|
|
183
|
+
mcpConfigPath?: string
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build claude CLI args for TUI driver mode.
|
|
188
|
+
*
|
|
189
|
+
* Kanna spawns the claude CLI under a real PTY and watches the on-disk
|
|
190
|
+
* transcript JSONL file as the event source. The CLI runs interactively
|
|
191
|
+
* with `--dangerously-skip-permissions` so tool calls are auto-approved.
|
|
192
|
+
*
|
|
193
|
+
* • No `--print` / `--output-format` / `--input-format` / `--verbose` —
|
|
194
|
+
* TUI mode does NOT use the stream-json headless transport.
|
|
195
|
+
* • No `--session-id` for new sessions — TUI claude generates its own UUID
|
|
196
|
+
* on first prompt; kanna identifies the session via the transcript file.
|
|
197
|
+
* • `--strict-mcp-config` — CLI ignores user MCP config; kanna provides
|
|
198
|
+
* its own via `--mcp-config` so the MCP surface is fully controlled.
|
|
199
|
+
* • `--setting-sources user,project,local` — user's installed skills,
|
|
200
|
+
* slash commands, plugins, agents, and project / local settings layers
|
|
201
|
+
* all load normally.
|
|
202
|
+
* • `--dangerously-skip-permissions` — auto-run tools because the CLI's
|
|
203
|
+
* own interactive permission prompt is not routed through kanna's UI.
|
|
204
|
+
*/
|
|
205
|
+
export function buildPtyCliArgs(args: BuildPtyCliArgsInput): string[] {
|
|
206
|
+
const cliArgs: string[] = [
|
|
207
|
+
"--model", args.model,
|
|
208
|
+
"--setting-sources", "user,project,local",
|
|
209
|
+
"--permission-mode", args.planMode ? "plan" : "acceptEdits",
|
|
210
|
+
"--dangerously-skip-permissions",
|
|
211
|
+
]
|
|
212
|
+
// TUI mode session handling:
|
|
213
|
+
// • New session (no sessionToken) → no --session-id (TUI ignores it; claude generates its own UUID)
|
|
214
|
+
// • Resume existing session (sessionToken set) → --resume <token>
|
|
215
|
+
// • Fork existing session (sessionToken + fork) → --session-id <newUuid> --resume <token> --fork-session
|
|
216
|
+
//
|
|
217
|
+
// Interactive TUI claude ignores `--session-id` for new sessions and
|
|
218
|
+
// always generates its own UUID. Watcher uses an mtime filter on the
|
|
219
|
+
// project dir instead — only JSONLs created at or after spawn start are
|
|
220
|
+
// candidates, so stale JSONLs from prior sessions cannot win the race.
|
|
221
|
+
if (args.sessionToken && !args.forkSession) {
|
|
222
|
+
cliArgs.push("--resume", args.sessionToken)
|
|
223
|
+
} else if (args.sessionToken && args.forkSession) {
|
|
224
|
+
cliArgs.push("--session-id", args.sessionId, "--resume", args.sessionToken, "--fork-session")
|
|
225
|
+
}
|
|
226
|
+
if (args.mcpConfigPath) {
|
|
227
|
+
cliArgs.push("--mcp-config", args.mcpConfigPath, "--strict-mcp-config")
|
|
228
|
+
}
|
|
229
|
+
if (args.effort && args.effort.length > 0) cliArgs.push("--effort", args.effort)
|
|
230
|
+
if (args.additionalDirectories) {
|
|
231
|
+
for (const dir of args.additionalDirectories) cliArgs.push("--add-dir", dir)
|
|
232
|
+
}
|
|
233
|
+
if (args.systemPromptOverride) {
|
|
234
|
+
cliArgs.push("--system-prompt", args.systemPromptOverride)
|
|
235
|
+
} else {
|
|
236
|
+
cliArgs.push("--append-system-prompt", args.systemPromptAppend ?? KANNA_SYSTEM_PROMPT_APPEND)
|
|
237
|
+
}
|
|
238
|
+
// `--disallowedTools` is variadic in the claude CLI (space-separated tool
|
|
239
|
+
// strings as separate argv — code.claude.com/docs/en/cli-reference). Push
|
|
240
|
+
// it LAST so it cannot greedily swallow a subsequent flag value.
|
|
241
|
+
cliArgs.push("--disallowedTools", ...PTY_DISALLOWED_NATIVE_TOOLS)
|
|
242
|
+
return cliArgs
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function buildPtyEnv(args: {
|
|
246
|
+
baseEnv: NodeJS.ProcessEnv
|
|
247
|
+
homeDir: string
|
|
248
|
+
oauthToken: string | null
|
|
249
|
+
}): NodeJS.ProcessEnv {
|
|
250
|
+
const spawnEnv: NodeJS.ProcessEnv = { ...args.baseEnv }
|
|
251
|
+
delete spawnEnv.ANTHROPIC_API_KEY
|
|
252
|
+
spawnEnv.HOME = args.homeDir
|
|
253
|
+
spawnEnv.DISABLE_AUTOUPDATER = "1"
|
|
254
|
+
if (args.oauthToken && args.oauthToken.length > 0) {
|
|
255
|
+
spawnEnv.CLAUDE_CODE_OAUTH_TOKEN = args.oauthToken
|
|
256
|
+
}
|
|
257
|
+
return spawnEnv
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function startClaudeSessionPTY(args: StartClaudeSessionPtyArgs): Promise<ClaudeSessionHandle> {
|
|
261
|
+
const home = args.homeDir ?? homedir()
|
|
262
|
+
const env = args.env ?? process.env
|
|
263
|
+
|
|
264
|
+
console.log("[kanna/pty] startClaudeSessionPTY begin", {
|
|
265
|
+
chatId: args.chatId,
|
|
266
|
+
projectId: args.projectId,
|
|
267
|
+
localPath: args.localPath,
|
|
268
|
+
model: args.model,
|
|
269
|
+
planMode: args.planMode,
|
|
270
|
+
forkSession: args.forkSession,
|
|
271
|
+
hasOauthToken: Boolean(args.oauthToken),
|
|
272
|
+
oauthLabel: args.oauthLabel ?? null,
|
|
273
|
+
sandboxEnvOverride: env.KANNA_PTY_SANDBOX ?? null,
|
|
274
|
+
platform: process.platform,
|
|
275
|
+
anthropicApiKeySet: Boolean(env.ANTHROPIC_API_KEY),
|
|
276
|
+
claudeExecutable: env.CLAUDE_EXECUTABLE ?? null,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const spawnStartedAt = Date.now()
|
|
280
|
+
args.ptyInstanceRegistry?.upsert(args.chatId, {
|
|
281
|
+
cwd: args.localPath,
|
|
282
|
+
model: args.model,
|
|
283
|
+
accountLabel: args.oauthLabel ?? null,
|
|
284
|
+
oauthMasked: args.oauthKeyMasked ?? null,
|
|
285
|
+
phase: "spawning",
|
|
286
|
+
startedAt: spawnStartedAt,
|
|
287
|
+
lastEventAt: spawnStartedAt,
|
|
288
|
+
planMode: args.planMode,
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const auth = await verifyPtyAuth({ env, oauthToken: args.oauthToken })
|
|
292
|
+
if (!auth.ok) {
|
|
293
|
+
console.error("[kanna/pty] verifyPtyAuth failed", {
|
|
294
|
+
chatId: args.chatId,
|
|
295
|
+
error: auth.error,
|
|
296
|
+
hasOauthToken: Boolean(args.oauthToken),
|
|
297
|
+
anthropicApiKeySet: Boolean(env.ANTHROPIC_API_KEY),
|
|
298
|
+
})
|
|
299
|
+
throw new Error(auth.error)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const resolved = await resolveClaudeBinary({ env, homeDir: home })
|
|
303
|
+
console.log("[kanna/pty] resolved claude binary", {
|
|
304
|
+
chatId: args.chatId,
|
|
305
|
+
path: resolved.path,
|
|
306
|
+
source: resolved.source,
|
|
307
|
+
})
|
|
308
|
+
const claudeBinAbs = resolved.path
|
|
309
|
+
|
|
310
|
+
const binarySha256 = await computeBinarySha256(claudeBinAbs)
|
|
311
|
+
const smokeGate = args.smokeTestGate ?? createSmokeTestGate({
|
|
312
|
+
probe: buildLiveSmokeProbe({
|
|
313
|
+
claudeBinPath: claudeBinAbs,
|
|
314
|
+
model: args.model,
|
|
315
|
+
oauthToken: args.oauthToken ?? "",
|
|
316
|
+
homeDir: home,
|
|
317
|
+
}),
|
|
318
|
+
cache: createFileSmokeTestCache({ cacheDir: path.join(home, ".tinkaria", "cache", "smoke-test") }),
|
|
319
|
+
ttlMs: 24 * 3600 * 1000,
|
|
320
|
+
now: () => Date.now(),
|
|
321
|
+
})
|
|
322
|
+
const smoke = await smokeGate.canSpawn({ binarySha256, model: args.model })
|
|
323
|
+
if (!smoke.ok) {
|
|
324
|
+
console.error("[kanna/pty] smoke-test refused spawn", { chatId: args.chatId, reason: smoke.reason })
|
|
325
|
+
throw new Error(`PTY smoke-test refused spawn: ${smoke.reason}`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const spawnEnv = buildPtyEnv({
|
|
329
|
+
baseEnv: env,
|
|
330
|
+
homeDir: home,
|
|
331
|
+
oauthToken: args.oauthToken,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const sessionId = args.sessionToken ?? randomUUID()
|
|
335
|
+
|
|
336
|
+
const runtimeDir = await createRuntimeDir(`kanna-pty-${sessionId.slice(0, 8)}-`)
|
|
337
|
+
|
|
338
|
+
const mcpConfigPath = path.join(runtimeDir, "mcp-config.json")
|
|
339
|
+
let mcpHandle: KannaMcpHttpHandle
|
|
340
|
+
const startMcp = args.startKannaMcpHttpServer ?? startKannaMcpHttpServer
|
|
341
|
+
try {
|
|
342
|
+
mcpHandle = await startMcp({
|
|
343
|
+
args: {
|
|
344
|
+
projectId: args.projectId,
|
|
345
|
+
localPath: args.localPath,
|
|
346
|
+
chatId: args.chatId,
|
|
347
|
+
sessionId,
|
|
348
|
+
tunnelGateway: args.tunnelGateway ?? null,
|
|
349
|
+
toolCallback: args.toolCallback,
|
|
350
|
+
chatPolicy: args.chatPolicy,
|
|
351
|
+
subagentOrchestrator: args.subagentOrchestrator,
|
|
352
|
+
delegationContext: args.delegationContext,
|
|
353
|
+
// PTY has no canUseTool hook — the durable approval protocol is the
|
|
354
|
+
// only host path for AskUserQuestion/ExitPlanMode. Force the shims
|
|
355
|
+
// on regardless of KANNA_MCP_TOOL_CALLBACKS (issue #215). Paired
|
|
356
|
+
// with --disallowedTools AskUserQuestion ExitPlanMode above so the
|
|
357
|
+
// model uses the shim instead of the auto-rejected native built-in.
|
|
358
|
+
forceInteractiveToolCallbacks: true,
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
await writeRuntimeFile(
|
|
362
|
+
mcpConfigPath,
|
|
363
|
+
buildMcpConfigJson(mcpHandle, args.customMcpServers ?? []),
|
|
364
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
365
|
+
)
|
|
366
|
+
} catch (err) {
|
|
367
|
+
try { await (mcpHandle! as KannaMcpHttpHandle | undefined)?.close() } catch { /* swallow */ }
|
|
368
|
+
try { await removeRuntimeDir(runtimeDir) } catch { /* swallow */ }
|
|
369
|
+
throw err
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const claudeBin = claudeBinAbs
|
|
373
|
+
const cliArgs = buildPtyCliArgs({
|
|
374
|
+
sessionId,
|
|
375
|
+
model: args.model,
|
|
376
|
+
effort: args.effort,
|
|
377
|
+
planMode: args.planMode,
|
|
378
|
+
sessionToken: args.sessionToken,
|
|
379
|
+
forkSession: args.forkSession,
|
|
380
|
+
additionalDirectories: args.additionalDirectories,
|
|
381
|
+
systemPromptOverride: args.systemPromptOverride,
|
|
382
|
+
systemPromptAppend: args.systemPromptAppend,
|
|
383
|
+
mcpConfigPath,
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
let closed = false
|
|
387
|
+
let cleanedUp = false
|
|
388
|
+
let cachedAccountInfo: AccountInfo | null = deriveAccountInfoFromOauth({ label: args.oauthLabel, oauthKeyMasked: args.oauthKeyMasked })
|
|
389
|
+
let sawResultEntry = false
|
|
390
|
+
let cachedSlashCommands: SlashCommand[] | null = null
|
|
391
|
+
let localPlanModeActive = args.planMode
|
|
392
|
+
const mergedQueue: HarnessEvent[] = []
|
|
393
|
+
const mergedWaiters: Array<(r: IteratorResult<HarnessEvent>) => void> = []
|
|
394
|
+
|
|
395
|
+
async function cleanupResources() {
|
|
396
|
+
if (cleanedUp) return
|
|
397
|
+
cleanedUp = true
|
|
398
|
+
stopMemorySampler()
|
|
399
|
+
args.ptyInstanceRegistry?.upsert(args.chatId, {
|
|
400
|
+
phase: "exited",
|
|
401
|
+
exitedAt: Date.now(),
|
|
402
|
+
lastEventAt: Date.now(),
|
|
403
|
+
})
|
|
404
|
+
if (args.toolCallback) {
|
|
405
|
+
try { await args.toolCallback.cancelAllForSession(sessionId, "session_closed") } catch (err) {
|
|
406
|
+
console.warn("[kanna/pty] toolCallback.cancelAllForSession failed", { chatId: args.chatId, sessionId, err })
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
try { await mcpHandle.close() } catch (err) {
|
|
410
|
+
// Logged because a swallowed mcpHandle close error means the loopback
|
|
411
|
+
// HTTP server may still be listening — a real resource leak.
|
|
412
|
+
console.warn("[kanna/pty] mcpHandle.close failed (HTTP server may leak)", { chatId: args.chatId, sessionId, err })
|
|
413
|
+
}
|
|
414
|
+
try { await removeRuntimeDir(runtimeDir) } catch (err) {
|
|
415
|
+
console.warn("[kanna/pty] runtimeDir cleanup failed", { chatId: args.chatId, runtimeDir, err })
|
|
416
|
+
}
|
|
417
|
+
if (args.ptyRegistry) {
|
|
418
|
+
try { await args.ptyRegistry.unregister(sessionId) } catch (err) {
|
|
419
|
+
// A stale entry on disk only matters across server restarts — log
|
|
420
|
+
// for observability but do not fail cleanup.
|
|
421
|
+
console.warn("[kanna/pty] ptyRegistry.unregister failed", { chatId: args.chatId, sessionId, err })
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function pushMerged(ev: HarnessEvent) {
|
|
427
|
+
if (ev.type === "transcript" && ev.entry) {
|
|
428
|
+
const entry = ev.entry as { kind?: string; accountInfo?: unknown; slashCommands?: unknown }
|
|
429
|
+
if (entry.kind === "account_info" && entry.accountInfo !== undefined) {
|
|
430
|
+
cachedAccountInfo = entry.accountInfo as AccountInfo
|
|
431
|
+
}
|
|
432
|
+
if (entry.kind === "result") {
|
|
433
|
+
sawResultEntry = true
|
|
434
|
+
}
|
|
435
|
+
// system_init carries the full slash-command list the spawned claude
|
|
436
|
+
// CLI knows about — including every skill, plugin command, project
|
|
437
|
+
// command, and built-in. Cache it so getSupportedCommands() returns
|
|
438
|
+
// the live set instead of the cold-start fallback.
|
|
439
|
+
if (entry.kind === "system_init" && Array.isArray(entry.slashCommands)) {
|
|
440
|
+
cachedSlashCommands = (entry.slashCommands as string[]).map((name) => ({
|
|
441
|
+
name,
|
|
442
|
+
description: "",
|
|
443
|
+
argumentHint: "",
|
|
444
|
+
}))
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const w = mergedWaiters.shift()
|
|
448
|
+
if (w) w({ value: ev, done: false })
|
|
449
|
+
else mergedQueue.push(ev)
|
|
450
|
+
|
|
451
|
+
if (
|
|
452
|
+
args.oneShot
|
|
453
|
+
&& ev.type === "transcript"
|
|
454
|
+
&& (ev.entry as { kind?: string } | undefined)?.kind === "result"
|
|
455
|
+
) {
|
|
456
|
+
void oneShotClose()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let oneShotClosing = false
|
|
461
|
+
// pty is declared before use; assigned in the spawn try-block below.
|
|
462
|
+
let pty: PtyProcess
|
|
463
|
+
|
|
464
|
+
let memorySamplerHandle: ReturnType<typeof setInterval> | null = null
|
|
465
|
+
let rssPeakBytes = 0
|
|
466
|
+
let cpuPeakPercent = 0
|
|
467
|
+
|
|
468
|
+
function stopMemorySampler(): void {
|
|
469
|
+
if (memorySamplerHandle !== null) {
|
|
470
|
+
clearInterval(memorySamplerHandle)
|
|
471
|
+
memorySamplerHandle = null
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function startMemorySampler(rootPid: number): void {
|
|
476
|
+
if (memorySamplerHandle !== null) return
|
|
477
|
+
const sampler = args.sampleProcessTreeUsage ?? defaultSampleProcessTreeUsage
|
|
478
|
+
const intervalMs = args.memorySamplerIntervalMs ?? 2000
|
|
479
|
+
const tick = async (): Promise<void> => {
|
|
480
|
+
let sample: ProcessTreeSample | null
|
|
481
|
+
try {
|
|
482
|
+
sample = await sampler(rootPid)
|
|
483
|
+
} catch {
|
|
484
|
+
sample = null
|
|
485
|
+
}
|
|
486
|
+
if (sample === null) return
|
|
487
|
+
if (sample.rssBytes > rssPeakBytes) rssPeakBytes = sample.rssBytes
|
|
488
|
+
if (sample.cpuPercent > cpuPeakPercent) cpuPeakPercent = sample.cpuPercent
|
|
489
|
+
args.ptyInstanceRegistry?.upsert(args.chatId, {
|
|
490
|
+
rssBytes: sample.rssBytes,
|
|
491
|
+
rssPeakBytes,
|
|
492
|
+
cpuPercent: sample.cpuPercent,
|
|
493
|
+
cpuPeakPercent,
|
|
494
|
+
})
|
|
495
|
+
args.onUsageSample?.({
|
|
496
|
+
rssBytes: sample.rssBytes,
|
|
497
|
+
rssPeakBytes,
|
|
498
|
+
cpuPercent: sample.cpuPercent,
|
|
499
|
+
cpuPeakPercent,
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
memorySamplerHandle = setInterval(() => { void tick() }, intervalMs)
|
|
503
|
+
void tick()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const ring = new OutputRing()
|
|
507
|
+
const spawnPty = args.spawnPtyProcess ?? defaultSpawnPtyProcess
|
|
508
|
+
try {
|
|
509
|
+
console.log("[kanna/pty] spawn begin", {
|
|
510
|
+
chatId: args.chatId,
|
|
511
|
+
command: claudeBin,
|
|
512
|
+
cwd: args.localPath,
|
|
513
|
+
})
|
|
514
|
+
pty = await spawnPty({
|
|
515
|
+
command: claudeBin,
|
|
516
|
+
args: cliArgs,
|
|
517
|
+
cwd: args.localPath,
|
|
518
|
+
env: spawnEnv,
|
|
519
|
+
onOutput: (chunk) => { ring.append(chunk) },
|
|
520
|
+
})
|
|
521
|
+
console.log("[kanna/pty] pty spawned", { chatId: args.chatId, sessionId, pid: pty.pid })
|
|
522
|
+
args.ptyInstanceRegistry?.upsert(args.chatId, {
|
|
523
|
+
sessionId,
|
|
524
|
+
pid: pty.pid,
|
|
525
|
+
phase: "trust-dialog",
|
|
526
|
+
lastEventAt: Date.now(),
|
|
527
|
+
})
|
|
528
|
+
startMemorySampler(pty.pid)
|
|
529
|
+
// Record the live PTY in the on-disk registry so a non-graceful
|
|
530
|
+
// server crash can reap this orphan on the next boot. Persistence is
|
|
531
|
+
// best-effort — failure to write must not block the spawn.
|
|
532
|
+
if (args.ptyRegistry) {
|
|
533
|
+
try {
|
|
534
|
+
await args.ptyRegistry.register({
|
|
535
|
+
chatId: args.chatId,
|
|
536
|
+
sessionId,
|
|
537
|
+
pid: pty.pid,
|
|
538
|
+
cwd: args.localPath,
|
|
539
|
+
runtimeDir,
|
|
540
|
+
})
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.warn("[kanna/pty] ptyRegistry.register failed (orphan reap on crash disabled for this session)", { chatId: args.chatId, sessionId, err })
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.error("[kanna/pty] spawn failed", {
|
|
547
|
+
chatId: args.chatId,
|
|
548
|
+
sessionId,
|
|
549
|
+
error: err instanceof Error ? err.message : String(err),
|
|
550
|
+
})
|
|
551
|
+
try { await mcpHandle.close() } catch { /* swallow */ }
|
|
552
|
+
try { await removeRuntimeDir(runtimeDir) } catch { /* swallow */ }
|
|
553
|
+
throw err
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Wait for TUI to render its input box, dismissing the trust dialog if
|
|
557
|
+
// present. The combined helper handles the ANSI-encoded trust dialog text
|
|
558
|
+
// and keeps polling until the real "❯ " input box appears after dismiss.
|
|
559
|
+
const tuiReadyMs = Number((args.env ?? process.env).KANNA_PTY_TUI_BOOT_MS ?? 3000)
|
|
560
|
+
const tuiReadyQuietRaw = (args.env ?? process.env).KANNA_PTY_TUI_READY_QUIET_MS
|
|
561
|
+
const tuiReadyQuietMs = tuiReadyQuietRaw !== undefined ? Number(tuiReadyQuietRaw) : undefined
|
|
562
|
+
const trustDismiss = (args.env ?? process.env).KANNA_PTY_TRUST_DISMISS ?? "enabled"
|
|
563
|
+
if (trustDismiss !== "disabled") {
|
|
564
|
+
// +5 s over the base cap to absorb trust-dialog dismiss + project reload.
|
|
565
|
+
const readyResult = await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: tuiReadyMs + 5_000, quietPeriodMs: tuiReadyQuietMs })
|
|
566
|
+
if (readyResult === "timeout") {
|
|
567
|
+
console.warn("[kanna/pty] TUI ready marker not detected after trust dismiss", { chatId: args.chatId, hardCapMs: tuiReadyMs + 5_000 })
|
|
568
|
+
} else {
|
|
569
|
+
console.log("[kanna/pty] TUI ready", { chatId: args.chatId })
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
const readyResult = await waitForTuiReady(ring, { hardCapMs: tuiReadyMs, quietPeriodMs: tuiReadyQuietMs })
|
|
573
|
+
if (readyResult === "timeout") {
|
|
574
|
+
console.warn("[kanna/pty] TUI ready marker not detected within hard cap", { chatId: args.chatId, hardCapMs: tuiReadyMs })
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
args.ptyInstanceRegistry?.upsert(args.chatId, {
|
|
579
|
+
phase: "ready",
|
|
580
|
+
lastEventAt: Date.now(),
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
// Open transcript-file event stream.
|
|
584
|
+
const projectDir = computeProjectDir({ homeDir: home, cwd: args.localPath })
|
|
585
|
+
// knownFilePath: only known up-front when resuming (we know the
|
|
586
|
+
// sessionToken). For new sessions interactive TUI claude generates its
|
|
587
|
+
// own UUID and ignores `--session-id`, so the path is unknown — fall
|
|
588
|
+
// back to discovery via `findLatestTranscript` with an mtime floor at
|
|
589
|
+
// spawn-start time to filter out stale JSONLs from prior sessions in
|
|
590
|
+
// the same project dir.
|
|
591
|
+
const knownFilePath = args.sessionToken && !args.forkSession
|
|
592
|
+
? computeJsonlPath({ homeDir: home, cwd: args.localPath, sessionId: args.sessionToken })
|
|
593
|
+
: undefined
|
|
594
|
+
const spawnStartedAtMs = Date.now()
|
|
595
|
+
const startStream = args.startTranscriptStreamFn ?? startTranscriptStream
|
|
596
|
+
const transcriptStream = await startStream({
|
|
597
|
+
projectDir,
|
|
598
|
+
knownFilePath,
|
|
599
|
+
minMtimeMs: spawnStartedAtMs,
|
|
600
|
+
pollMode: (args.env ?? process.env).KANNA_PTY_TRANSCRIPT_WATCH === "poll",
|
|
601
|
+
// Race-free discovery via claude's per-PID session registry at
|
|
602
|
+
// `${home}/.claude/sessions/<pid>.json`. Falls back to the mtime
|
|
603
|
+
// heuristic if the registry file does not appear in time (older
|
|
604
|
+
// claude versions, broken HOME, etc).
|
|
605
|
+
claudeChildPid: pty.pid,
|
|
606
|
+
homeDir: home,
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
const parser = createJsonlEventParser({
|
|
610
|
+
configuredContextWindow: parseConfiguredContextWindowFromModelId(args.model),
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
// Pipe transcript JSONL lines through the parser into the merged event queue.
|
|
614
|
+
void (async () => {
|
|
615
|
+
try {
|
|
616
|
+
for await (const line of transcriptStream.lines) {
|
|
617
|
+
try {
|
|
618
|
+
const events = parser.parse(line)
|
|
619
|
+
for (const ev of events) pushMerged(ev)
|
|
620
|
+
} catch (err) {
|
|
621
|
+
console.warn("[kanna/pty] parser threw on line", { chatId: args.chatId, sessionId, err })
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
console.log("[kanna/pty] transcript stream ended", { chatId: args.chatId, sessionId })
|
|
625
|
+
} catch (err) {
|
|
626
|
+
console.warn("[kanna/pty] transcript stream errored", { chatId: args.chatId, sessionId, err })
|
|
627
|
+
}
|
|
628
|
+
})()
|
|
629
|
+
|
|
630
|
+
function drainTerminate(exitCode: number | null) {
|
|
631
|
+
console.log("[kanna/pty] drainTerminate", {
|
|
632
|
+
chatId: args.chatId,
|
|
633
|
+
sessionId,
|
|
634
|
+
exitCode,
|
|
635
|
+
closed,
|
|
636
|
+
oneShotClosing,
|
|
637
|
+
sawResultEntry,
|
|
638
|
+
oneShot: Boolean(args.oneShot),
|
|
639
|
+
waitersAwaitingEvent: mergedWaiters.length,
|
|
640
|
+
})
|
|
641
|
+
if (closed || oneShotClosing) {
|
|
642
|
+
while (mergedWaiters.length > 0) {
|
|
643
|
+
const w = mergedWaiters.shift()
|
|
644
|
+
if (w) w({ value: undefined as unknown as HarnessEvent, done: true })
|
|
645
|
+
}
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
if (!sawResultEntry) {
|
|
649
|
+
const tail = ring.tail().trim()
|
|
650
|
+
const codeNote = exitCode === null ? "signal" : `exit code ${exitCode}`
|
|
651
|
+
const resultText = tail.length > 0
|
|
652
|
+
? tail
|
|
653
|
+
: `claude PTY process exited (${codeNote}) before producing a result.`
|
|
654
|
+
console.warn("[kanna/pty] synthesizing error-result for early PTY exit (no turn_duration / result row seen)", {
|
|
655
|
+
chatId: args.chatId,
|
|
656
|
+
sessionId,
|
|
657
|
+
exitCode,
|
|
658
|
+
ringTailBytes: tail.length,
|
|
659
|
+
})
|
|
660
|
+
pushMerged({
|
|
661
|
+
type: "transcript",
|
|
662
|
+
entry: timestamped({
|
|
663
|
+
kind: "result",
|
|
664
|
+
subtype: "error",
|
|
665
|
+
isError: true,
|
|
666
|
+
durationMs: 0,
|
|
667
|
+
result: resultText,
|
|
668
|
+
debugRaw: JSON.stringify({ source: "pty-exit", exitCode }),
|
|
669
|
+
}),
|
|
670
|
+
})
|
|
671
|
+
}
|
|
672
|
+
void cleanupResources()
|
|
673
|
+
while (mergedWaiters.length > 0) {
|
|
674
|
+
const w = mergedWaiters.shift()
|
|
675
|
+
if (w) w({ value: undefined as unknown as HarnessEvent, done: true })
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
void pty.exited
|
|
680
|
+
.then((code) => {
|
|
681
|
+
console.log("[kanna/pty] pty.exited resolved", { chatId: args.chatId, sessionId, pid: pty.pid, code })
|
|
682
|
+
drainTerminate(typeof code === "number" ? code : null)
|
|
683
|
+
})
|
|
684
|
+
.catch((err) => {
|
|
685
|
+
console.warn("[kanna/pty] pty.exited rejected", { chatId: args.chatId, sessionId, pid: pty.pid, err })
|
|
686
|
+
drainTerminate(null)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
async function oneShotClose() {
|
|
690
|
+
if (oneShotClosing || closed) return
|
|
691
|
+
oneShotClosing = true
|
|
692
|
+
console.log("[kanna/pty] oneShotClose start", { chatId: args.chatId, sessionId, sawResultEntry })
|
|
693
|
+
try { await sendExitCommand(pty) } catch (err) {
|
|
694
|
+
console.warn("[kanna/pty] oneShotClose sendExitCommand failed", { chatId: args.chatId, sessionId, err })
|
|
695
|
+
}
|
|
696
|
+
try { await pty.exited } catch { /* swallow */ }
|
|
697
|
+
try { transcriptStream.close() } catch { /* swallow */ }
|
|
698
|
+
await cleanupResources()
|
|
699
|
+
console.log("[kanna/pty] oneShotClose finished", { chatId: args.chatId, sessionId })
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (args.initialPrompt) {
|
|
703
|
+
try {
|
|
704
|
+
await sendUserPrompt(pty, ring, args.initialPrompt)
|
|
705
|
+
} catch (err) {
|
|
706
|
+
console.warn("[kanna/pty] initialPrompt write failed", err)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const stream: AsyncIterable<HarnessEvent> = {
|
|
711
|
+
[Symbol.asyncIterator]() {
|
|
712
|
+
return {
|
|
713
|
+
next(): Promise<IteratorResult<HarnessEvent>> {
|
|
714
|
+
if (mergedQueue.length > 0) {
|
|
715
|
+
const ev = mergedQueue.shift()
|
|
716
|
+
if (ev) return Promise.resolve({ value: ev, done: false })
|
|
717
|
+
}
|
|
718
|
+
if (closed) {
|
|
719
|
+
return Promise.resolve({ value: undefined as unknown as HarnessEvent, done: true })
|
|
720
|
+
}
|
|
721
|
+
return new Promise((resolve) => {
|
|
722
|
+
mergedWaiters.push(resolve)
|
|
723
|
+
})
|
|
724
|
+
},
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
provider: "claude",
|
|
731
|
+
stream,
|
|
732
|
+
interrupt: async () => {
|
|
733
|
+
try { await pty.sendInput("\x03") } catch { /* swallow */ }
|
|
734
|
+
},
|
|
735
|
+
sendPrompt: async (content) => {
|
|
736
|
+
const text = typeof content === "string"
|
|
737
|
+
? content
|
|
738
|
+
: Array.isArray(content)
|
|
739
|
+
? (content as Array<{ type?: string; text?: string }>)
|
|
740
|
+
.filter((c) => c.type === "text")
|
|
741
|
+
.map((c) => c.text ?? "")
|
|
742
|
+
.join("\n")
|
|
743
|
+
: String(content)
|
|
744
|
+
await sendUserPrompt(pty, ring, text)
|
|
745
|
+
},
|
|
746
|
+
setModel: async (model) => {
|
|
747
|
+
try {
|
|
748
|
+
await pty.sendInput(`/model ${model}\r`)
|
|
749
|
+
} catch (err) {
|
|
750
|
+
console.warn("[kanna/pty] setModel via /model slash command failed", err)
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
setPermissionMode: async (planMode) => {
|
|
754
|
+
if (planMode) {
|
|
755
|
+
try {
|
|
756
|
+
await pty.sendInput("/plan\r")
|
|
757
|
+
localPlanModeActive = true
|
|
758
|
+
} catch (err) {
|
|
759
|
+
console.warn("[kanna/pty] /plan slash command failed", err)
|
|
760
|
+
}
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
if (localPlanModeActive) {
|
|
764
|
+
try {
|
|
765
|
+
await pty.sendInput(SHIFT_TAB_KEY)
|
|
766
|
+
localPlanModeActive = false
|
|
767
|
+
} catch (err) {
|
|
768
|
+
console.warn("[kanna/pty] Shift+Tab exit-plan failed", err)
|
|
769
|
+
}
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
console.warn(PLAN_MODE_EXIT_UNSUPPORTED)
|
|
773
|
+
},
|
|
774
|
+
getSupportedCommands: async () => cachedSlashCommands ?? STATIC_SUPPORTED_COMMANDS,
|
|
775
|
+
getAccountInfo: async () => cachedAccountInfo,
|
|
776
|
+
close: () => {
|
|
777
|
+
if (closed) return
|
|
778
|
+
closed = true
|
|
779
|
+
void (async () => {
|
|
780
|
+
// 3-stage shutdown escalation:
|
|
781
|
+
// 1. /exit (graceful REPL exit) — 2 s grace
|
|
782
|
+
// 2. SIGTERM (terminal.close + proc.kill) — 3 s grace
|
|
783
|
+
// 3. SIGKILL (force kill, unblocks hung TUI)
|
|
784
|
+
// Each timer is cleared if pty.exited resolves before the deadline.
|
|
785
|
+
try { await sendExitCommand(pty) } catch { /* swallow */ }
|
|
786
|
+
const sigkillTimer = { ref: null as ReturnType<typeof setTimeout> | null }
|
|
787
|
+
const termTimer = setTimeout(() => {
|
|
788
|
+
try { pty.close() } catch { /* swallow */ }
|
|
789
|
+
sigkillTimer.ref = setTimeout(() => {
|
|
790
|
+
try { pty.kill("SIGKILL") } catch { /* swallow */ }
|
|
791
|
+
}, 3000)
|
|
792
|
+
}, 2000)
|
|
793
|
+
try {
|
|
794
|
+
await pty.exited
|
|
795
|
+
} catch { /* swallow */ }
|
|
796
|
+
clearTimeout(termTimer)
|
|
797
|
+
if (sigkillTimer.ref !== null) clearTimeout(sigkillTimer.ref)
|
|
798
|
+
try { transcriptStream.close() } catch { /* swallow */ }
|
|
799
|
+
await cleanupResources()
|
|
800
|
+
while (mergedWaiters.length > 0) {
|
|
801
|
+
const w = mergedWaiters.shift()
|
|
802
|
+
if (w) w({ value: undefined as unknown as HarnessEvent, done: true })
|
|
803
|
+
}
|
|
804
|
+
})()
|
|
805
|
+
},
|
|
806
|
+
}
|
|
807
|
+
}
|