aicodeman 1.35.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -7
- package/dist/attachment-registry.d.ts +6 -0
- package/dist/attachment-registry.d.ts.map +1 -1
- package/dist/attachment-registry.js +9 -3
- package/dist/attachment-registry.js.map +1 -1
- package/dist/cli-agent.d.ts +225 -0
- package/dist/cli-agent.d.ts.map +1 -0
- package/dist/cli-agent.js +767 -0
- package/dist/cli-agent.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -32
- package/dist/cli.js.map +1 -1
- package/dist/codeman-credentials.d.ts +27 -0
- package/dist/codeman-credentials.d.ts.map +1 -0
- package/dist/codeman-credentials.js +66 -0
- package/dist/codeman-credentials.js.map +1 -0
- package/dist/config/cli-registry/patterns.d.ts +6 -0
- package/dist/config/cli-registry/patterns.d.ts.map +1 -1
- package/dist/config/cli-registry/patterns.js +13 -0
- package/dist/config/cli-registry/patterns.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +15 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +63 -2
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +180 -7
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +105 -7
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/deepseek-route-config.d.ts +110 -0
- package/dist/deepseek-route-config.d.ts.map +1 -0
- package/dist/deepseek-route-config.js +477 -0
- package/dist/deepseek-route-config.js.map +1 -0
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +4 -0
- package/dist/git-clone.js.map +1 -1
- package/dist/git-workspace-status.d.ts +31 -5
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/git-workspace-status.js +49 -29
- package/dist/git-workspace-status.js.map +1 -1
- package/dist/image-watcher.d.ts.map +1 -1
- package/dist/image-watcher.js +6 -2
- package/dist/image-watcher.js.map +1 -1
- package/dist/mcp-sync-targets.d.ts +34 -0
- package/dist/mcp-sync-targets.d.ts.map +1 -0
- package/dist/mcp-sync-targets.js +55 -0
- package/dist/mcp-sync-targets.js.map +1 -0
- package/dist/mcp-sync.d.ts +2 -1
- package/dist/mcp-sync.d.ts.map +1 -1
- package/dist/mcp-sync.js +62 -1
- package/dist/mcp-sync.js.map +1 -1
- package/dist/model-config-resolvers.d.ts +29 -0
- package/dist/model-config-resolvers.d.ts.map +1 -0
- package/dist/model-config-resolvers.js +34 -0
- package/dist/model-config-resolvers.js.map +1 -0
- package/dist/session-display-model.d.ts +110 -0
- package/dist/session-display-model.d.ts.map +1 -0
- package/dist/session-display-model.js +154 -0
- package/dist/session-display-model.js.map +1 -0
- package/dist/session.d.ts +145 -8
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +290 -39
- package/dist/session.js.map +1 -1
- package/dist/subagent-watcher.d.ts.map +1 -1
- package/dist/subagent-watcher.js +29 -6
- package/dist/subagent-watcher.js.map +1 -1
- package/dist/templates/case-template.md +2 -0
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +47 -22
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tui/tui-client.d.ts +3 -17
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +3 -44
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/types/session.d.ts +28 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/tools.d.ts +1 -1
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/deepseek-cli-resolver.d.ts +8 -0
- package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
- package/dist/utils/deepseek-cli-resolver.js +18 -2
- package/dist/utils/deepseek-cli-resolver.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/process-exit-wait.d.ts +40 -0
- package/dist/utils/process-exit-wait.d.ts.map +1 -0
- package/dist/utils/process-exit-wait.js +68 -0
- package/dist/utils/process-exit-wait.js.map +1 -0
- package/dist/web/launch-defaults.d.ts +22 -0
- package/dist/web/launch-defaults.d.ts.map +1 -0
- package/dist/web/launch-defaults.js +46 -0
- package/dist/web/launch-defaults.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +46 -6
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +106 -32
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.6f8552e1.js +43 -0
- package/dist/web/public/app.6f8552e1.js.br +0 -0
- package/dist/web/public/app.6f8552e1.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.d95fade9.js → constants.bc9c1f63.js} +1276 -127
- package/dist/web/public/constants.bc9c1f63.js.br +0 -0
- package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js +491 -43
- package/dist/web/public/entrance-animations.js.br +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +55 -9
- package/dist/web/public/git-status-ui.js.br +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/home-sessions.js +13 -9
- package/dist/web/public/home-sessions.js.br +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.a9840ea2.js +2 -0
- package/dist/web/public/i18n.a9840ea2.js.br +0 -0
- package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
- package/dist/web/public/{image-input.a216cf9e.js → image-input.e00471c6.js} +21 -9
- package/dist/web/public/image-input.e00471c6.js.br +0 -0
- package/dist/web/public/image-input.e00471c6.js.gz +0 -0
- package/dist/web/public/index.html +250 -59
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.23a85db1.js → mobile-handlers.0ef9c311.js} +20 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.111b187d.css +1 -0
- package/dist/web/public/mobile.111b187d.css.br +0 -0
- package/dist/web/public/mobile.111b187d.css.gz +0 -0
- package/dist/web/public/{notification-manager.50bb87d8.js → notification-manager.feb82c04.js} +19 -2
- package/dist/web/public/notification-manager.feb82c04.js.br +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.6a5898d9.js → panels-ui.71846fcb.js} +4 -4
- package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js +229 -86
- package/dist/web/public/session-lineage.js.br +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.f731602c.js +100 -0
- package/dist/web/public/session-ui.f731602c.js.br +0 -0
- package/dist/web/public/session-ui.f731602c.js.gz +0 -0
- package/dist/web/public/settings-ui.4e33390f.js +97 -0
- package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
- package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js +290 -0
- package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js +555 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js +1006 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.81a72906.css +1 -0
- package/dist/web/public/styles.81a72906.css.br +0 -0
- package/dist/web/public/styles.81a72906.css.gz +0 -0
- package/dist/web/public/{subagent-windows.e6ca799f.js → subagent-windows.07277a58.js} +7 -5
- package/dist/web/public/subagent-windows.07277a58.js.br +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.94cd59f0.js → tab-rail-resize.a5867793.js} +1 -1
- package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
- package/dist/web/public/{tab-rail-resize.94cd59f0.js.gz → tab-rail-resize.a5867793.js.gz} +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +1 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js +146 -649
- package/dist/web/public/terminal-split.js.br +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +1688 -0
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.b7267005.js +2 -0
- package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
- package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
- package/dist/web/public/tile-grid.js +2895 -0
- package/dist/web/public/tile-grid.js.br +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js +9 -6
- package/dist/web/public/ultracode-windows.js.br +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js +44 -0
- package/dist/web/public/vendor/exceljs.min.js.br +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js +1 -0
- package/dist/web/public/vendor/fflate.min.js.br +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +4 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/{voice-input.c4b51eb6.js → voice-input.2342c37b.js} +64 -10
- package/dist/web/public/voice-input.2342c37b.js.br +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js +9 -1
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/routes/cli-registry-routes.d.ts +23 -4
- package/dist/web/routes/cli-registry-routes.d.ts.map +1 -1
- package/dist/web/routes/cli-registry-routes.js +95 -8
- package/dist/web/routes/cli-registry-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts +7 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +34 -4
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +33 -6
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/mcp-sync-routes.d.ts +7 -2
- package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -1
- package/dist/web/routes/mcp-sync-routes.js +10 -3
- package/dist/web/routes/mcp-sync-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +1 -1
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +18 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +154 -69
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts +3 -0
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +12 -0
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +13 -0
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +30 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +43 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +6 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +47 -21
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +1 -0
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +11 -0
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/package.json +6 -4
- package/scripts/postinstall.js +16 -0
- package/skills/codeman/SKILL.md +6 -0
- package/README.zh-CN.md +0 -1139
- package/dist/web/public/app.b9516c7b.js +0 -43
- package/dist/web/public/app.b9516c7b.js.br +0 -0
- package/dist/web/public/app.b9516c7b.js.gz +0 -0
- package/dist/web/public/constants.d95fade9.js.br +0 -0
- package/dist/web/public/constants.d95fade9.js.gz +0 -0
- package/dist/web/public/i18n.52f5b7cc.js +0 -1
- package/dist/web/public/i18n.52f5b7cc.js.br +0 -0
- package/dist/web/public/i18n.52f5b7cc.js.gz +0 -0
- package/dist/web/public/image-input.a216cf9e.js.br +0 -0
- package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
- package/dist/web/public/mobile-handlers.23a85db1.js.br +0 -0
- package/dist/web/public/mobile-handlers.23a85db1.js.gz +0 -0
- package/dist/web/public/mobile.1aa4952d.css +0 -1
- package/dist/web/public/mobile.1aa4952d.css.br +0 -0
- package/dist/web/public/mobile.1aa4952d.css.gz +0 -0
- package/dist/web/public/notification-manager.50bb87d8.js.br +0 -0
- package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
- package/dist/web/public/panels-ui.6a5898d9.js.br +0 -0
- package/dist/web/public/panels-ui.6a5898d9.js.gz +0 -0
- package/dist/web/public/session-ui.6bddf916.js +0 -89
- package/dist/web/public/session-ui.6bddf916.js.br +0 -0
- package/dist/web/public/session-ui.6bddf916.js.gz +0 -0
- package/dist/web/public/settings-ui.6e798b7b.js +0 -97
- package/dist/web/public/settings-ui.6e798b7b.js.br +0 -0
- package/dist/web/public/settings-ui.6e798b7b.js.gz +0 -0
- package/dist/web/public/styles.2d175c60.css +0 -1
- package/dist/web/public/styles.2d175c60.css.br +0 -0
- package/dist/web/public/styles.2d175c60.css.gz +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.br +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.94cd59f0.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js +0 -1
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
- package/dist/web/public/terminal-ui.a4535311.js +0 -2
- package/dist/web/public/terminal-ui.a4535311.js.br +0 -0
- package/dist/web/public/terminal-ui.a4535311.js.gz +0 -0
- package/dist/web/public/upload.html +0 -155
- package/dist/web/public/upload.html.br +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +0 -4
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.gz +0 -0
- package/dist/web/public/voice-input.c4b51eb6.js.br +0 -0
- package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `codeman agent …` — session-to-session verbs for the agent running
|
|
3
|
+
* inside a Codeman session, in every CLI mode.
|
|
4
|
+
*
|
|
5
|
+
* A thin HTTP client over endpoints that already exist (`quick-start`, `input`,
|
|
6
|
+
* `wait`, `wait-output`, `last-response`, `terminal`, `DELETE sessions/:id`). It
|
|
7
|
+
* invents no route and no transport: everything goes through `CODEMAN_API_URL`, so
|
|
8
|
+
* auth, ownership and the per-session waiter cap apply unchanged. The behaviour is
|
|
9
|
+
* the packaged agent skill's (`skills/codeman`), ported from shell prose into code
|
|
10
|
+
* with tests, so a `codex`/`opencode`/`pi` agent — which never gets the claude-only
|
|
11
|
+
* preamble — has the same verbs from one line in its AGENTS.md.
|
|
12
|
+
*
|
|
13
|
+
* Invariants (each asserted in `test/cli-agent.test.ts`):
|
|
14
|
+
* 1. Refuses outside a Codeman session (`CODEMAN_MUX=1` + `CODEMAN_API_URL`); it
|
|
15
|
+
* never guesses a URL — a server you are not part of is not yours to drive.
|
|
16
|
+
* 2. `send` transmits printable text plus `\r` only. ESC exists solely as
|
|
17
|
+
* `interrupt`, which never appends `\r`. A stray control byte is a dead session
|
|
18
|
+
* in the fullscreen TUIs (opencode's `Ctrl+C` is `app_exit`).
|
|
19
|
+
* 3. `rm` fails closed: empty id, a short self id, or a prefix match in EITHER
|
|
20
|
+
* direction refuses. Ids appear in full and 8-char form, so equality alone
|
|
21
|
+
* misses a real combination — and the miss deletes the caller.
|
|
22
|
+
* 4. An id shorter than 8 characters refuses (exit 4) before any request, on every
|
|
23
|
+
* verb. `9` would resolve to whichever session is alone with that first
|
|
24
|
+
* character, the user's own interactive tab included.
|
|
25
|
+
*
|
|
26
|
+
* Commands live here as functions returning an exit code, not calling
|
|
27
|
+
* `process.exit`, so the whole surface is unit-testable against a fake server.
|
|
28
|
+
*
|
|
29
|
+
* @module cli-agent
|
|
30
|
+
*/
|
|
31
|
+
import http from 'node:http';
|
|
32
|
+
import https from 'node:https';
|
|
33
|
+
import { basicAuthHeader, credentialsFrom, readCodemanEnvFile, } from './codeman-credentials.js';
|
|
34
|
+
import { getCli } from './config/cli-registry/registry.js';
|
|
35
|
+
import { GLYPH, palette, table } from './cli-style.js';
|
|
36
|
+
import { getErrorMessage } from './types.js';
|
|
37
|
+
import { stripAnsi as stripAnsiSequences } from './utils/regex-patterns.js';
|
|
38
|
+
/** Thrown when the process is not inside a Codeman-managed session. */
|
|
39
|
+
export class AgentGuardError extends Error {
|
|
40
|
+
}
|
|
41
|
+
/** Exit codes shared by every verb; a shell agent can branch on them. */
|
|
42
|
+
export const EXIT = {
|
|
43
|
+
ok: 0,
|
|
44
|
+
error: 1,
|
|
45
|
+
timeout: 2,
|
|
46
|
+
dead: 3,
|
|
47
|
+
refused: 4,
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the context from the environment, or throw `AgentGuardError`.
|
|
51
|
+
*
|
|
52
|
+
* Credentials in the order every client of the API uses (`credentialsFrom`, shared
|
|
53
|
+
* with `codeman attach` and the TUI): each field from the environment (a session
|
|
54
|
+
* inherits the server's), then the data dir's `.env`. No password means the server
|
|
55
|
+
* is open (single-user) — or it is not, and the 401 says so.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveAgentContext(env = process.env, envFile = readCodemanEnvFile) {
|
|
58
|
+
if (env.CODEMAN_MUX !== '1') {
|
|
59
|
+
throw new AgentGuardError('Not inside a Codeman-managed session (CODEMAN_MUX is not 1); refusing to act.');
|
|
60
|
+
}
|
|
61
|
+
const apiUrl = env.CODEMAN_API_URL?.trim();
|
|
62
|
+
if (!apiUrl) {
|
|
63
|
+
throw new AgentGuardError('CODEMAN_API_URL is not set; refusing to guess a server.');
|
|
64
|
+
}
|
|
65
|
+
const selfId = env.CODEMAN_SESSION_ID?.trim();
|
|
66
|
+
if (!selfId) {
|
|
67
|
+
throw new AgentGuardError('CODEMAN_SESSION_ID is not set; cannot tell which session is me.');
|
|
68
|
+
}
|
|
69
|
+
const credentials = credentialsFrom(env, envFile());
|
|
70
|
+
return { apiUrl, selfId, auth: credentials.password ? credentials : undefined };
|
|
71
|
+
}
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Pure helpers (the invariants)
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Is `id` this session? Prefix in BOTH directions, because ids appear in full and
|
|
77
|
+
* in 8-char form (mux names, UI surfaces, Docker's truncated `$SELF`). A self id
|
|
78
|
+
* shorter than 8 characters cannot prove anything and is treated as "maybe me".
|
|
79
|
+
*/
|
|
80
|
+
export function isSelfSession(selfId, id) {
|
|
81
|
+
if (!id || selfId.length < 8)
|
|
82
|
+
return true;
|
|
83
|
+
return id.startsWith(selfId) || selfId.startsWith(id);
|
|
84
|
+
}
|
|
85
|
+
/** Why `rm` refuses, or `undefined` when the delete may go ahead. */
|
|
86
|
+
export function deleteRefusal(selfId, id) {
|
|
87
|
+
if (!id)
|
|
88
|
+
return 'refusing: empty session id';
|
|
89
|
+
if (selfId.length < 8)
|
|
90
|
+
return 'refusing: own session id unset or too short to prove this is not me';
|
|
91
|
+
if (isSelfSession(selfId, id))
|
|
92
|
+
return `refusing: ${id} is me`;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The prompt `send` was given, which must be ONE argument. Joining several with spaces
|
|
97
|
+
* would turn an unquoted `$(cat notes.txt)`, which the shell splits on every newline,
|
|
98
|
+
* back into a single line, so the multi-line refusal below would never see it.
|
|
99
|
+
*/
|
|
100
|
+
export function sendPromptFromArgs(words) {
|
|
101
|
+
if (words.length === 1)
|
|
102
|
+
return { text: words[0] };
|
|
103
|
+
return {
|
|
104
|
+
error: `refusing: the prompt must be ONE argument, got ${words.length} — quote it (\`send <id> "…"\`; a prompt that starts with "-" goes after --: \`send <id> -- "- fix the bug"\`)`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Why `send` refuses this text, or `undefined` when it is printable. The composer
|
|
109
|
+
* takes one line; the server strips `\r`/`\n` but everything else below 0x20 (and
|
|
110
|
+
* DEL) reaches the pane as a keypress. None of that is a prompt.
|
|
111
|
+
*/
|
|
112
|
+
export function inputRefusal(text) {
|
|
113
|
+
if (text.length === 0)
|
|
114
|
+
return 'refusing: empty input (use `interrupt` for ESC, `send <id> ""` is never a prompt)';
|
|
115
|
+
// The composer is one line: the server strips newlines, which silently joins the
|
|
116
|
+
// lines into one prompt, and a tab reaches the pane as a keypress (claude: mode toggle).
|
|
117
|
+
if (/[\n\r\t]/.test(text)) {
|
|
118
|
+
return 'refusing: input must be a single line (the composer strips newlines and would join your lines) — join them yourself, or write a file into the workspace and send its path';
|
|
119
|
+
}
|
|
120
|
+
// C0, DEL and C1 (U+0080–U+009F: an 8-bit CSI is still a CSI to a terminal).
|
|
121
|
+
// eslint-disable-next-line no-control-regex
|
|
122
|
+
const control = text.match(/[\x00-\x1f\x7f-\x9f]/);
|
|
123
|
+
if (control) {
|
|
124
|
+
const code = control[0].charCodeAt(0).toString(16).padStart(2, '0');
|
|
125
|
+
return `refusing: input contains control byte 0x${code}; send transmits printable text only (ESC is \`interrupt\`)`;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Body for `POST /sessions/:id/input` on the send path: text plus `\r` unless the
|
|
131
|
+
* caller asked to type without submitting. Never anything else.
|
|
132
|
+
*/
|
|
133
|
+
export function buildSendBody(text, options) {
|
|
134
|
+
const body = {
|
|
135
|
+
input: options.enter ? `${text}\r` : text,
|
|
136
|
+
useMux: true,
|
|
137
|
+
clientId: options.clientId,
|
|
138
|
+
seq: options.seq,
|
|
139
|
+
};
|
|
140
|
+
if (options.wait !== undefined)
|
|
141
|
+
body.wait = options.wait;
|
|
142
|
+
if (options.waitTimeout !== undefined)
|
|
143
|
+
body.waitTimeout = options.waitTimeout;
|
|
144
|
+
return body;
|
|
145
|
+
}
|
|
146
|
+
/** Body for the interrupt path: a bare ESC, and nothing appended — ever. */
|
|
147
|
+
export function buildInterruptBody(clientId, seq) {
|
|
148
|
+
return { input: '\u001b', useMux: true, clientId, seq };
|
|
149
|
+
}
|
|
150
|
+
/** `clientId` for this caller: fixed per sending session, so `seq` stays monotonic. */
|
|
151
|
+
export function defaultClientId(selfId, suffix = '') {
|
|
152
|
+
return `codeman-agent-cli-${selfId.slice(0, 8)}${suffix ? `-${suffix}` : ''}`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Strip a terminal buffer for humans: the shared ANSI strip (CSI, OSC such as window
|
|
156
|
+
* titles, keypad modes) plus the charset designators (`ESC ( B`) it leaves in.
|
|
157
|
+
*/
|
|
158
|
+
export function stripAnsi(text) {
|
|
159
|
+
// eslint-disable-next-line no-control-regex
|
|
160
|
+
return stripAnsiSequences(text).replace(/\x1b[()][AB0]/g, '');
|
|
161
|
+
}
|
|
162
|
+
/** Parse a positive-integer option (`--timeout` ms, `--tail` bytes); the server rejects anything else. */
|
|
163
|
+
export function parsePositiveInt(raw, fallback, flag = '--timeout') {
|
|
164
|
+
if (raw === undefined)
|
|
165
|
+
return fallback;
|
|
166
|
+
const n = Number(raw);
|
|
167
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
168
|
+
throw new Error(`${flag} must be a positive integer, got "${raw}"`);
|
|
169
|
+
}
|
|
170
|
+
return n;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Exit code for a wait result: matched or a signal → ok, `exit` or `ended` → dead,
|
|
174
|
+
* timeout → timeout. `ended` is checked BEFORE the happy paths: a worker that dies
|
|
175
|
+
* during `--until stop` comes back as `ended:true, signal:null` (the registry only
|
|
176
|
+
* satisfies waiters that listed `exit`, then cancels the rest), and a `--match` on
|
|
177
|
+
* a dead worker as `ended:true, matched:false` — both are "dead", never "done".
|
|
178
|
+
*/
|
|
179
|
+
export function waitExitCode(wait) {
|
|
180
|
+
if (!wait)
|
|
181
|
+
return EXIT.error;
|
|
182
|
+
if (wait.signal === 'exit' || wait.ended)
|
|
183
|
+
return EXIT.dead;
|
|
184
|
+
if (wait.timedOut)
|
|
185
|
+
return EXIT.timeout;
|
|
186
|
+
if (wait.matched === false)
|
|
187
|
+
return EXIT.timeout;
|
|
188
|
+
return EXIT.ok;
|
|
189
|
+
}
|
|
190
|
+
/** Every request carries these; they are ignored on endpoints that do not read them. */
|
|
191
|
+
export function baseHeaders(ctx) {
|
|
192
|
+
const headers = {
|
|
193
|
+
Accept: 'application/json',
|
|
194
|
+
// Tags sessions this caller spawns as its children (lineage in the web UI).
|
|
195
|
+
// Cosmetic, never fails a call. NOT X-Codeman-Agent-Origin: that one marks a case
|
|
196
|
+
// directory as deletable agent scratch, so it rides only the spawn request that
|
|
197
|
+
// may create one (see agentSpawn), never anything else.
|
|
198
|
+
'X-Codeman-Parent-Session': ctx.selfId,
|
|
199
|
+
};
|
|
200
|
+
const authorization = ctx.auth ? basicAuthHeader(ctx.auth) : undefined;
|
|
201
|
+
if (authorization)
|
|
202
|
+
headers.Authorization = authorization;
|
|
203
|
+
return headers;
|
|
204
|
+
}
|
|
205
|
+
/** The real transport. `rejectUnauthorized:false` because the HTTPS install uses a self-signed cert. */
|
|
206
|
+
export const httpRequest = (ctx, options) => {
|
|
207
|
+
const url = new URL(options.path, ctx.apiUrl);
|
|
208
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
209
|
+
if (value !== undefined)
|
|
210
|
+
url.searchParams.set(key, String(value));
|
|
211
|
+
}
|
|
212
|
+
const bodyText = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
213
|
+
const headers = { ...baseHeaders(ctx), ...(options.headers ?? {}) };
|
|
214
|
+
if (bodyText !== undefined) {
|
|
215
|
+
headers['Content-Type'] = 'application/json';
|
|
216
|
+
headers['Content-Length'] = Buffer.byteLength(bodyText);
|
|
217
|
+
}
|
|
218
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const req = transport.request({
|
|
221
|
+
protocol: url.protocol,
|
|
222
|
+
hostname: url.hostname,
|
|
223
|
+
port: url.port,
|
|
224
|
+
method: options.method,
|
|
225
|
+
path: `${url.pathname}${url.search}`,
|
|
226
|
+
rejectUnauthorized: false,
|
|
227
|
+
headers,
|
|
228
|
+
timeout: options.timeoutMs ?? 30_000,
|
|
229
|
+
}, (res) => {
|
|
230
|
+
const chunks = [];
|
|
231
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
232
|
+
res.on('end', () => {
|
|
233
|
+
const text = Buffer.concat(chunks).toString('utf-8');
|
|
234
|
+
let json;
|
|
235
|
+
try {
|
|
236
|
+
json = JSON.parse(text);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
json = undefined;
|
|
240
|
+
}
|
|
241
|
+
resolve({ status: res.statusCode ?? 0, json, text });
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
req.on('timeout', () => req.destroy(new Error(`request timed out after ${options.timeoutMs ?? 30_000} ms`)));
|
|
245
|
+
req.on('error', reject);
|
|
246
|
+
if (bodyText !== undefined)
|
|
247
|
+
req.write(bodyText);
|
|
248
|
+
req.end();
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
/** One line describing a failed response, for humans. Plain-text guards (401/403/429) have no envelope. */
|
|
252
|
+
export function describeFailure(res) {
|
|
253
|
+
if (res.json && !res.json.success) {
|
|
254
|
+
return `${res.json.errorCode ?? 'ERROR'}: ${res.json.error ?? 'request failed'} (HTTP ${res.status})`;
|
|
255
|
+
}
|
|
256
|
+
const text = res.text.trim().split('\n')[0] ?? '';
|
|
257
|
+
if (res.status === 401)
|
|
258
|
+
return `HTTP 401 ${text}: the server wants a password (CODEMAN_PASSWORD, or the data dir's .env)`;
|
|
259
|
+
return `HTTP ${res.status}${text ? ` ${text}` : ''}`;
|
|
260
|
+
}
|
|
261
|
+
/** Print `data` as JSON (the `--json` path) — always the envelope's `data`, never a reshaped copy. */
|
|
262
|
+
function emitJson(deps, data) {
|
|
263
|
+
deps.io.out(JSON.stringify(data, null, 2));
|
|
264
|
+
}
|
|
265
|
+
function fail(deps, message, code = EXIT.error) {
|
|
266
|
+
if (deps.json) {
|
|
267
|
+
deps.io.out(JSON.stringify({ success: false, error: message }));
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
deps.io.err(palette.err(`${GLYPH.fail} ${message}`));
|
|
271
|
+
}
|
|
272
|
+
return code;
|
|
273
|
+
}
|
|
274
|
+
/** A full session id (the only form the routes accept); `ls` prints the 8-char prefix. */
|
|
275
|
+
const FULL_ID_LENGTH = 36;
|
|
276
|
+
/**
|
|
277
|
+
* Shortest prefix that may name a session: the 8-char form `ls` prints, and the floor
|
|
278
|
+
* the server's own resolver uses (`PARENT_SESSION_ID_MIN_PREFIX`, route-helpers.ts).
|
|
279
|
+
*/
|
|
280
|
+
export const MIN_ID_PREFIX_LENGTH = 8;
|
|
281
|
+
/**
|
|
282
|
+
* Turn the id a human typed into the one the routes accept. `ls` prints 8-char
|
|
283
|
+
* prefixes and the routes answer 404 to those (measured live), so anything shorter
|
|
284
|
+
* than a full id resolves through the session list; an ambiguous prefix refuses
|
|
285
|
+
* rather than picking one. Below 8 characters it refuses before the list: "unique"
|
|
286
|
+
* means nothing for `9` — it names whatever session happens to be alone with that
|
|
287
|
+
* first character, and `rm`/`send` would act on it.
|
|
288
|
+
*/
|
|
289
|
+
export async function resolveSessionId(deps, id) {
|
|
290
|
+
if (!id)
|
|
291
|
+
return { error: 'refusing: empty session id', code: EXIT.refused };
|
|
292
|
+
if (id.length < MIN_ID_PREFIX_LENGTH) {
|
|
293
|
+
return {
|
|
294
|
+
error: `refusing: "${id}" is shorter than ${MIN_ID_PREFIX_LENGTH} characters — use the 8-character id \`agent ls\` prints, or the full id`,
|
|
295
|
+
code: EXIT.refused,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (id.length >= FULL_ID_LENGTH)
|
|
299
|
+
return { id };
|
|
300
|
+
const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' });
|
|
301
|
+
if (!res.json?.success)
|
|
302
|
+
return { error: describeFailure(res), code: EXIT.error };
|
|
303
|
+
const matches = (res.json.data ?? []).filter((s) => s.id.startsWith(id));
|
|
304
|
+
if (matches.length === 1)
|
|
305
|
+
return { id: matches[0].id };
|
|
306
|
+
if (matches.length === 0)
|
|
307
|
+
return { error: `no session starts with "${id}" (see \`agent ls\`)`, code: EXIT.error };
|
|
308
|
+
return { error: `"${id}" is ambiguous: ${matches.map((s) => s.id.slice(0, 13)).join(', ')}`, code: EXIT.error };
|
|
309
|
+
}
|
|
310
|
+
/** `agent ls` — every session the caller can see, self marked. */
|
|
311
|
+
export async function agentLs(deps) {
|
|
312
|
+
const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' });
|
|
313
|
+
if (!res.json?.success)
|
|
314
|
+
return fail(deps, describeFailure(res));
|
|
315
|
+
const sessions = res.json.data ?? [];
|
|
316
|
+
if (deps.json) {
|
|
317
|
+
emitJson(deps, sessions.map((s) => ({ ...s, self: isSelfSession(deps.ctx.selfId, s.id) })));
|
|
318
|
+
return EXIT.ok;
|
|
319
|
+
}
|
|
320
|
+
if (sessions.length === 0) {
|
|
321
|
+
deps.io.out(palette.muted('(no sessions)'));
|
|
322
|
+
return EXIT.ok;
|
|
323
|
+
}
|
|
324
|
+
const rows = sessions.map((s) => [
|
|
325
|
+
isSelfSession(deps.ctx.selfId, s.id) ? '*' : ' ',
|
|
326
|
+
s.id.slice(0, 8),
|
|
327
|
+
s.mode ?? '?',
|
|
328
|
+
s.status ?? '?',
|
|
329
|
+
s.name || s.workingDir || '',
|
|
330
|
+
]);
|
|
331
|
+
deps.io.out(table([[' ', 'ID', 'MODE', 'STATUS', 'NAME'], ...rows], { gap: 2 }));
|
|
332
|
+
deps.io.out(palette.muted(`* = this session (${deps.ctx.selfId.slice(0, 8)}). status is a UI hint, never a sync signal.`));
|
|
333
|
+
return EXIT.ok;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Agent-scratch label for a case directory a spawn CREATES (the server applies it only
|
|
337
|
+
* when quick-start makes the directory). The Add Case UI offers a recursive delete for
|
|
338
|
+
* such directories, so this header must never ride any other request: mislabelling a
|
|
339
|
+
* real repo there is the one failure in this area that costs actual work.
|
|
340
|
+
*/
|
|
341
|
+
export const AGENT_ORIGIN_HEADER = { 'X-Codeman-Agent-Origin': 'codeman-agent-cli' };
|
|
342
|
+
/**
|
|
343
|
+
* What the mode's TUI draws once its composer can take a prompt, from the CLI registry
|
|
344
|
+
* (`capabilities.composerReadyMark`); undefined means the mode has no readiness wait.
|
|
345
|
+
*/
|
|
346
|
+
export function composerReadyMark(mode) {
|
|
347
|
+
return getCli(mode)?.capabilities.composerReadyMark;
|
|
348
|
+
}
|
|
349
|
+
/** `agent spawn` — quick-start with lineage, then the readiness ladder where the mode has one. */
|
|
350
|
+
export async function agentSpawn(deps, options) {
|
|
351
|
+
const body = {
|
|
352
|
+
caseName: options.caseName,
|
|
353
|
+
mode: options.mode,
|
|
354
|
+
parentSessionId: deps.ctx.selfId,
|
|
355
|
+
};
|
|
356
|
+
if (options.name)
|
|
357
|
+
body.sessionName = options.name;
|
|
358
|
+
const res = await deps.request(deps.ctx, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
path: '/api/v1/quick-start',
|
|
361
|
+
body,
|
|
362
|
+
headers: { ...AGENT_ORIGIN_HEADER },
|
|
363
|
+
});
|
|
364
|
+
const data = res.json?.data;
|
|
365
|
+
if (!res.json?.success || !data?.sessionId)
|
|
366
|
+
return fail(deps, describeFailure(res));
|
|
367
|
+
const sid = data.sessionId;
|
|
368
|
+
let ready;
|
|
369
|
+
let readinessError;
|
|
370
|
+
let dead = false;
|
|
371
|
+
const mark = composerReadyMark(options.mode);
|
|
372
|
+
if (options.ready && mark) {
|
|
373
|
+
const wait = await deps.request(deps.ctx, {
|
|
374
|
+
method: 'GET',
|
|
375
|
+
path: `/api/v1/sessions/${encodeURIComponent(sid)}/wait-output`,
|
|
376
|
+
query: { match: mark, from: 'buffer', timeout: options.timeoutMs },
|
|
377
|
+
timeoutMs: options.timeoutMs + 10_000,
|
|
378
|
+
});
|
|
379
|
+
// A failed readiness call (waiter cap, 400, network) is its own error, not "the
|
|
380
|
+
// composer never showed up": report the real reason instead of the trust-dialog hint.
|
|
381
|
+
if (!wait.json?.success)
|
|
382
|
+
readinessError = describeFailure(wait);
|
|
383
|
+
else {
|
|
384
|
+
const result = wait.json.data?.wait;
|
|
385
|
+
// A worker that died while we waited is exit 3 like every other wait, not a
|
|
386
|
+
// "composer not seen" timeout that sends the caller looking for a dialog.
|
|
387
|
+
dead = waitExitCode(result) === EXIT.dead;
|
|
388
|
+
ready = !dead && Boolean(result?.matched);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (deps.json) {
|
|
392
|
+
emitJson(deps, { ...data, ready, readinessError });
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Human lines go to stderr so `SID=$(codeman agent spawn …)` captures the id alone.
|
|
396
|
+
const say = (line) => deps.io.err(line);
|
|
397
|
+
say(palette.ok(`${GLYPH.ok} spawned ${sid} (${options.mode}, case ${data.caseName ?? options.caseName})`));
|
|
398
|
+
if (ready === true)
|
|
399
|
+
say(palette.muted(' composer up: the worker can take a prompt'));
|
|
400
|
+
if (dead)
|
|
401
|
+
say(palette.err(`${GLYPH.fail} the worker exited during the readiness wait`));
|
|
402
|
+
if (ready === false && !dead) {
|
|
403
|
+
say(palette.warn(`${GLYPH.warn} composer not seen within ${options.timeoutMs} ms — read \`agent read ${sid.slice(0, 8)} --tail 2000\` before sending (a startup dialog?)`));
|
|
404
|
+
}
|
|
405
|
+
if (readinessError)
|
|
406
|
+
say(palette.err(`${GLYPH.fail} readiness check failed: ${readinessError}`));
|
|
407
|
+
if (ready === undefined && !readinessError && options.ready) {
|
|
408
|
+
say(palette.muted(` ${options.mode} has no readiness mark; give it a moment, then use --match markers to synchronize`));
|
|
409
|
+
}
|
|
410
|
+
deps.io.out(sid);
|
|
411
|
+
}
|
|
412
|
+
if (readinessError)
|
|
413
|
+
return EXIT.error;
|
|
414
|
+
if (dead)
|
|
415
|
+
return EXIT.dead;
|
|
416
|
+
return ready === false ? EXIT.timeout : EXIT.ok;
|
|
417
|
+
}
|
|
418
|
+
/** `agent send` — printable text plus `\r`, exactly-once, optionally blocking on end of turn. */
|
|
419
|
+
export async function agentSend(deps, options) {
|
|
420
|
+
if (isSelfSession(deps.ctx.selfId, options.id)) {
|
|
421
|
+
return fail(deps, `refusing: ${options.id} is me — typing into my own composer is not a message`, EXIT.refused);
|
|
422
|
+
}
|
|
423
|
+
const refusal = inputRefusal(options.text);
|
|
424
|
+
if (refusal)
|
|
425
|
+
return fail(deps, refusal, EXIT.refused);
|
|
426
|
+
const target = await resolveSessionId(deps, options.id);
|
|
427
|
+
if ('error' in target)
|
|
428
|
+
return fail(deps, target.error, target.code);
|
|
429
|
+
const body = buildSendBody(options.text, {
|
|
430
|
+
enter: options.enter,
|
|
431
|
+
clientId: options.clientId ?? defaultClientId(deps.ctx.selfId),
|
|
432
|
+
seq: options.seq ?? (deps.now ?? Date.now)(),
|
|
433
|
+
wait: options.wait,
|
|
434
|
+
waitTimeout: options.wait !== undefined ? options.timeoutMs : undefined,
|
|
435
|
+
});
|
|
436
|
+
const res = await deps.request(deps.ctx, {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}/input`,
|
|
439
|
+
body,
|
|
440
|
+
timeoutMs: (options.timeoutMs ?? 60_000) + 10_000,
|
|
441
|
+
});
|
|
442
|
+
if (!res.json?.success)
|
|
443
|
+
return fail(deps, describeFailure(res));
|
|
444
|
+
const data = res.json.data;
|
|
445
|
+
if (deps.json)
|
|
446
|
+
emitJson(deps, data ?? {});
|
|
447
|
+
// Fire-and-forget to a remote session whose host is asleep (wake-on-LAN): the server
|
|
448
|
+
// holds the chunk and types it once the pane is back (`buffered`), or the chunk was
|
|
449
|
+
// over the wake buffer's cap and is gone (`dropped`). The seq is spent either way,
|
|
450
|
+
// so a retry needs a new one (the default, the clock, gives it that).
|
|
451
|
+
if (data?.dropped) {
|
|
452
|
+
if (!deps.json) {
|
|
453
|
+
deps.io.err(palette.err(`${GLYPH.fail} dropped: ${target.id}'s host is waking and its input buffer is full — nothing will be typed; send again once it is back`));
|
|
454
|
+
}
|
|
455
|
+
return EXIT.error;
|
|
456
|
+
}
|
|
457
|
+
// `delivered:false` without `duplicate` is the route's "the bytes went nowhere":
|
|
458
|
+
// the PTY exited or send-keys hit a dead pane. The field exists so a client does not
|
|
459
|
+
// say "wait longer" when the truth is "restart the worker" — so it is a failure here.
|
|
460
|
+
if (data?.delivered === false && !data.duplicate) {
|
|
461
|
+
if (!deps.json) {
|
|
462
|
+
deps.io.err(palette.err(`${GLYPH.fail} not delivered: ${target.id} has no live worker (pane exited) — restart it`));
|
|
463
|
+
}
|
|
464
|
+
return EXIT.dead;
|
|
465
|
+
}
|
|
466
|
+
if (!deps.json) {
|
|
467
|
+
const noEnter = options.enter ? '' : ' (no Enter)';
|
|
468
|
+
if (data?.duplicate) {
|
|
469
|
+
deps.io.out(palette.warn(`${GLYPH.warn} duplicate (clientId/seq already applied): nothing typed`));
|
|
470
|
+
}
|
|
471
|
+
else if (data?.buffered) {
|
|
472
|
+
deps.io.out(palette.ok(`${GLYPH.ok} buffered for ${target.id}${noEnter}: its host is asleep; Codeman is waking it and types this once the pane is back`));
|
|
473
|
+
}
|
|
474
|
+
else if (data?.delivered === true) {
|
|
475
|
+
deps.io.out(palette.ok(`${GLYPH.ok} delivered to ${target.id}${noEnter}`));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
// Fire-and-forget answers before the write, so there is no delivery report here.
|
|
479
|
+
deps.io.out(palette.ok(`${GLYPH.ok} accepted for ${target.id}${noEnter} (no delivery report without --wait)`));
|
|
480
|
+
}
|
|
481
|
+
if (data?.wait)
|
|
482
|
+
deps.io.out(describeWait(data.wait));
|
|
483
|
+
}
|
|
484
|
+
if (options.wait === undefined)
|
|
485
|
+
return EXIT.ok;
|
|
486
|
+
return waitExitCode(data?.wait);
|
|
487
|
+
}
|
|
488
|
+
function describeWait(wait) {
|
|
489
|
+
if (wait.signal === 'exit')
|
|
490
|
+
return palette.err(`${GLYPH.fail} the session exited`);
|
|
491
|
+
if (wait.ended) {
|
|
492
|
+
return palette.err(`${GLYPH.fail} the wait ended without an answer: the session went away (dead worker, deleted, or nothing was written)`);
|
|
493
|
+
}
|
|
494
|
+
// A timeout is a 200 with `timedOut`, an answer rather than a failure: exit 2 says it,
|
|
495
|
+
// so the line stays neutral instead of looking like an error to whoever reads the log.
|
|
496
|
+
if (wait.timedOut)
|
|
497
|
+
return palette.muted(`timed out after ${wait.timeoutMs ?? '?'} ms (exit 2)`);
|
|
498
|
+
if (wait.matched !== undefined) {
|
|
499
|
+
return wait.matched
|
|
500
|
+
? palette.ok(`${GLYPH.ok} matched "${wait.match}"${wait.snippet ? `: ${wait.snippet}` : ''}`)
|
|
501
|
+
: palette.muted('not matched (exit 2)');
|
|
502
|
+
}
|
|
503
|
+
return palette.ok(`${GLYPH.ok} signal: ${wait.signal}${wait.immediate ? ' (immediate: current state, not a transition)' : ''}`);
|
|
504
|
+
}
|
|
505
|
+
/** `agent wait` — a signal (`--until`) or a literal output marker (`--match`). */
|
|
506
|
+
export async function agentWait(deps, options) {
|
|
507
|
+
if (options.until && options.match)
|
|
508
|
+
return fail(deps, 'use either --until <signals> or --match <marker>, not both', EXIT.refused);
|
|
509
|
+
const target = await resolveSessionId(deps, options.id);
|
|
510
|
+
if ('error' in target)
|
|
511
|
+
return fail(deps, target.error, target.code);
|
|
512
|
+
const sid = encodeURIComponent(target.id);
|
|
513
|
+
const res = options.match
|
|
514
|
+
? await deps.request(deps.ctx, {
|
|
515
|
+
method: 'GET',
|
|
516
|
+
path: `/api/v1/sessions/${sid}/wait-output`,
|
|
517
|
+
query: {
|
|
518
|
+
match: options.match,
|
|
519
|
+
from: options.from ?? 'buffer',
|
|
520
|
+
nocase: options.nocase ? 1 : undefined,
|
|
521
|
+
timeout: options.timeoutMs,
|
|
522
|
+
},
|
|
523
|
+
timeoutMs: options.timeoutMs + 10_000,
|
|
524
|
+
})
|
|
525
|
+
: await deps.request(deps.ctx, {
|
|
526
|
+
method: 'GET',
|
|
527
|
+
path: `/api/v1/sessions/${sid}/wait`,
|
|
528
|
+
query: { until: options.until, fresh: options.fresh ? 1 : undefined, timeout: options.timeoutMs },
|
|
529
|
+
timeoutMs: options.timeoutMs + 10_000,
|
|
530
|
+
});
|
|
531
|
+
// A 400 here is the server saying "this mode has no such signal" (until=stop on an
|
|
532
|
+
// external CLI). Passed through, never papered over: the marker path is the answer.
|
|
533
|
+
if (!res.json?.success)
|
|
534
|
+
return fail(deps, describeFailure(res));
|
|
535
|
+
const data = res.json.data;
|
|
536
|
+
if (deps.json) {
|
|
537
|
+
emitJson(deps, data ?? {});
|
|
538
|
+
}
|
|
539
|
+
else if (data?.wait) {
|
|
540
|
+
deps.io.out(describeWait(data.wait));
|
|
541
|
+
if (data.limitPaused)
|
|
542
|
+
deps.io.out(palette.warn(`${GLYPH.warn} session is paused on a usage limit; a timeout is expected`));
|
|
543
|
+
}
|
|
544
|
+
return waitExitCode(data?.wait);
|
|
545
|
+
}
|
|
546
|
+
/** `agent read` — the last answer (the route picks the transcript reader or the pane segmenter) or a terminal tail. */
|
|
547
|
+
export async function agentRead(deps, options) {
|
|
548
|
+
const target = await resolveSessionId(deps, options.id);
|
|
549
|
+
if ('error' in target)
|
|
550
|
+
return fail(deps, target.error, target.code);
|
|
551
|
+
const sid = encodeURIComponent(target.id);
|
|
552
|
+
if (options.tail !== undefined) {
|
|
553
|
+
const res = await deps.request(deps.ctx, {
|
|
554
|
+
method: 'GET',
|
|
555
|
+
path: `/api/v1/sessions/${sid}/terminal`,
|
|
556
|
+
query: { tail: options.tail },
|
|
557
|
+
});
|
|
558
|
+
if (!res.json?.success)
|
|
559
|
+
return fail(deps, describeFailure(res));
|
|
560
|
+
const buffer = res.json.data?.terminalBuffer ?? '';
|
|
561
|
+
if (deps.json)
|
|
562
|
+
emitJson(deps, res.json.data);
|
|
563
|
+
else
|
|
564
|
+
deps.io.out(stripAnsi(buffer));
|
|
565
|
+
return EXIT.ok;
|
|
566
|
+
}
|
|
567
|
+
const res = await deps.request(deps.ctx, {
|
|
568
|
+
method: 'GET',
|
|
569
|
+
path: `/api/v1/sessions/${sid}/last-response`,
|
|
570
|
+
query: { context: options.full ? 'full' : undefined },
|
|
571
|
+
});
|
|
572
|
+
if (!res.json?.success)
|
|
573
|
+
return fail(deps, describeFailure(res));
|
|
574
|
+
const data = res.json.data;
|
|
575
|
+
if (deps.json) {
|
|
576
|
+
emitJson(deps, data ?? {});
|
|
577
|
+
return EXIT.ok;
|
|
578
|
+
}
|
|
579
|
+
if (options.full && data?.messages) {
|
|
580
|
+
for (const m of data.messages)
|
|
581
|
+
deps.io.out(`${palette.emph(m.role)}: ${m.text}`);
|
|
582
|
+
return EXIT.ok;
|
|
583
|
+
}
|
|
584
|
+
const text = data?.text ?? '';
|
|
585
|
+
if (!text) {
|
|
586
|
+
deps.io.err(palette.muted('(empty: nothing answered yet, or nothing the server could segment as an answer; try --tail 3000)'));
|
|
587
|
+
return EXIT.ok;
|
|
588
|
+
}
|
|
589
|
+
deps.io.out(text);
|
|
590
|
+
return EXIT.ok;
|
|
591
|
+
}
|
|
592
|
+
/** `agent interrupt` — a bare ESC keypress, no Enter, conversation intact. */
|
|
593
|
+
export async function agentInterrupt(deps, options) {
|
|
594
|
+
if (isSelfSession(deps.ctx.selfId, options.id))
|
|
595
|
+
return fail(deps, `refusing: ${options.id} is me`, EXIT.refused);
|
|
596
|
+
const target = await resolveSessionId(deps, options.id);
|
|
597
|
+
if ('error' in target)
|
|
598
|
+
return fail(deps, target.error, target.code);
|
|
599
|
+
const body = buildInterruptBody(defaultClientId(deps.ctx.selfId, 'interrupt'), (deps.now ?? Date.now)());
|
|
600
|
+
const res = await deps.request(deps.ctx, {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}/input`,
|
|
603
|
+
body,
|
|
604
|
+
});
|
|
605
|
+
if (!res.json?.success)
|
|
606
|
+
return fail(deps, describeFailure(res));
|
|
607
|
+
if (deps.json)
|
|
608
|
+
emitJson(deps, res.json.data ?? {});
|
|
609
|
+
else
|
|
610
|
+
deps.io.out(palette.ok(`${GLYPH.ok} ESC sent to ${target.id} — one Esc does not always land; read the tail before the next prompt`));
|
|
611
|
+
return EXIT.ok;
|
|
612
|
+
}
|
|
613
|
+
/** `agent rm` — delete a session that is provably not this one. */
|
|
614
|
+
export async function agentRm(deps, options) {
|
|
615
|
+
const refusal = deleteRefusal(deps.ctx.selfId, options.id);
|
|
616
|
+
if (refusal)
|
|
617
|
+
return fail(deps, refusal, EXIT.refused);
|
|
618
|
+
const target = await resolveSessionId(deps, options.id);
|
|
619
|
+
if ('error' in target)
|
|
620
|
+
return fail(deps, target.error, target.code);
|
|
621
|
+
// The guard again on the RESOLVED id: a prefix that is not me can still resolve
|
|
622
|
+
// to me only if the list is lying, but a delete is the one call worth the paranoia.
|
|
623
|
+
const resolvedRefusal = deleteRefusal(deps.ctx.selfId, target.id);
|
|
624
|
+
if (resolvedRefusal)
|
|
625
|
+
return fail(deps, resolvedRefusal, EXIT.refused);
|
|
626
|
+
const res = await deps.request(deps.ctx, {
|
|
627
|
+
method: 'DELETE',
|
|
628
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}`,
|
|
629
|
+
});
|
|
630
|
+
if (!res.json?.success)
|
|
631
|
+
return fail(deps, describeFailure(res));
|
|
632
|
+
if (deps.json)
|
|
633
|
+
emitJson(deps, res.json.data ?? {});
|
|
634
|
+
else
|
|
635
|
+
deps.io.out(palette.ok(`${GLYPH.ok} deleted ${target.id} (its case directory stays on disk)`));
|
|
636
|
+
return EXIT.ok;
|
|
637
|
+
}
|
|
638
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
639
|
+
// Commander wiring
|
|
640
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
641
|
+
const DEFAULT_WAIT_MS = 60_000;
|
|
642
|
+
/** Build deps from the live environment; the guard's message is the only thing a non-session caller sees. */
|
|
643
|
+
function liveDeps(json) {
|
|
644
|
+
try {
|
|
645
|
+
return {
|
|
646
|
+
ctx: resolveAgentContext(),
|
|
647
|
+
request: httpRequest,
|
|
648
|
+
json,
|
|
649
|
+
io: { out: (line) => console.log(line), err: (line) => console.error(line) },
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
if (err instanceof AgentGuardError) {
|
|
654
|
+
console.error(palette.err(`${GLYPH.fail} ${err.message}`));
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/** Run a verb with the live transport and turn its exit code into the process exit. */
|
|
661
|
+
async function run(json, verb) {
|
|
662
|
+
const deps = liveDeps(json);
|
|
663
|
+
if (!deps) {
|
|
664
|
+
process.exitCode = EXIT.refused;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
process.exitCode = await verb(deps);
|
|
669
|
+
}
|
|
670
|
+
catch (err) {
|
|
671
|
+
console.error(palette.err(`${GLYPH.fail} ${getErrorMessage(err)}`));
|
|
672
|
+
process.exitCode = EXIT.error;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** Register `codeman agent …` on the program. */
|
|
676
|
+
export function registerAgentCommands(program) {
|
|
677
|
+
const agent = program
|
|
678
|
+
.command('agent')
|
|
679
|
+
.description('Talk to other sessions from inside one (any CLI mode): list, spawn, send, wait, read, interrupt, rm');
|
|
680
|
+
agent
|
|
681
|
+
.command('ls')
|
|
682
|
+
.alias('list')
|
|
683
|
+
.description('List sessions; * marks this one')
|
|
684
|
+
.option('--json', 'Machine-readable output')
|
|
685
|
+
.action((options) => run(Boolean(options.json), agentLs));
|
|
686
|
+
agent
|
|
687
|
+
.command('spawn <case>')
|
|
688
|
+
.description('Start a worker session in a case (created if missing) and wait for its composer where the mode draws one')
|
|
689
|
+
.option('-m, --mode <mode>', 'Run mode id, as the Run menu names it', 'claude')
|
|
690
|
+
.option('-n, --name <name>', 'Session name shown in the UI')
|
|
691
|
+
.option('--no-ready', 'Return as soon as the session exists, without the readiness wait')
|
|
692
|
+
.option('-t, --timeout <ms>', 'Readiness budget in ms', String(DEFAULT_WAIT_MS))
|
|
693
|
+
.option('--json', 'Machine-readable output')
|
|
694
|
+
.action((caseName, options) => run(Boolean(options.json), (deps) => agentSpawn(deps, {
|
|
695
|
+
caseName,
|
|
696
|
+
mode: options.mode,
|
|
697
|
+
name: options.name,
|
|
698
|
+
ready: options.ready,
|
|
699
|
+
timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
|
|
700
|
+
})));
|
|
701
|
+
agent
|
|
702
|
+
.command('send <id> <text...>')
|
|
703
|
+
.description('Type a prompt into another session and press Enter (ONE quoted argument, printable text only; a prompt that starts with "-" goes after --: send <id> -- "- fix the bug")')
|
|
704
|
+
.option('-w, --wait', 'Block until end of turn (the default signal set; see --until)')
|
|
705
|
+
.option('-u, --until <signals>', 'Signals to wait for, comma list such as stop,exit (implies --wait)')
|
|
706
|
+
.option('-t, --timeout <ms>', 'Wait budget in ms (with --wait)', String(DEFAULT_WAIT_MS))
|
|
707
|
+
.option('--no-enter', 'Type the text without submitting it')
|
|
708
|
+
.option('--client-id <id>', 'Exactly-once tag (default: one per calling session)')
|
|
709
|
+
.option('--seq <n>', 'Sequence number for the tag (default: the current epoch ms). Must stay monotonic per client id: a reused or lower value is a silent duplicate, nothing is typed')
|
|
710
|
+
.option('--json', 'Machine-readable output')
|
|
711
|
+
.action((id, words, options) => run(Boolean(options.json), (deps) => {
|
|
712
|
+
const prompt = sendPromptFromArgs(words);
|
|
713
|
+
if ('error' in prompt)
|
|
714
|
+
return Promise.resolve(fail(deps, prompt.error, EXIT.refused));
|
|
715
|
+
return agentSend(deps, {
|
|
716
|
+
id,
|
|
717
|
+
text: prompt.text,
|
|
718
|
+
enter: options.enter,
|
|
719
|
+
wait: options.until ?? (options.wait ? true : undefined),
|
|
720
|
+
timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
|
|
721
|
+
clientId: options.clientId,
|
|
722
|
+
seq: options.seq === undefined ? undefined : parsePositiveInt(options.seq, 1, '--seq'),
|
|
723
|
+
});
|
|
724
|
+
}));
|
|
725
|
+
agent
|
|
726
|
+
.command('wait <id>')
|
|
727
|
+
.description('Block until a signal (--until) or an output marker (--match) — timeout exits 2, a dead worker exits 3')
|
|
728
|
+
.option('-u, --until <signals>', 'Comma list: stop,idle,exit,working,blocked (stop/blocked need hook signals for the session; where there are none the server answers 400, passed through)')
|
|
729
|
+
.option('-m, --match <marker>', 'Literal substring to wait for in the output (ANSI-stripped, no regex). The echo of your own prompt is output too, so never put the marker verbatim in the prompt: ask for it in halves ("print WORKDONE followed by _4711") and wait on the joined form (WORKDONE_4711)')
|
|
730
|
+
.option('--from <where>', 'buffer (scan existing output first, the default) or now', 'buffer')
|
|
731
|
+
.option('--nocase', 'Case-insensitive --match')
|
|
732
|
+
.option('--fresh', 'Require an actual transition (--until only)')
|
|
733
|
+
.option('-t, --timeout <ms>', 'Budget in ms', String(DEFAULT_WAIT_MS))
|
|
734
|
+
.option('--json', 'Machine-readable output')
|
|
735
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentWait(deps, {
|
|
736
|
+
id,
|
|
737
|
+
until: options.until,
|
|
738
|
+
match: options.match,
|
|
739
|
+
from: options.from === 'now' ? 'now' : 'buffer',
|
|
740
|
+
nocase: options.nocase,
|
|
741
|
+
fresh: options.fresh,
|
|
742
|
+
timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
|
|
743
|
+
})));
|
|
744
|
+
agent
|
|
745
|
+
.command('read <id>')
|
|
746
|
+
.description("Print a session's last answer (as the server reads it for that mode) or, with --tail, its terminal")
|
|
747
|
+
.option('--tail <bytes>', 'Raw terminal tail in bytes, ANSI stripped (works in every mode)')
|
|
748
|
+
.option('--full', 'The whole conversation instead of the last assistant message')
|
|
749
|
+
.option('--json', 'Machine-readable output')
|
|
750
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentRead(deps, {
|
|
751
|
+
id,
|
|
752
|
+
tail: options.tail === undefined ? undefined : parsePositiveInt(options.tail, 3000, '--tail'),
|
|
753
|
+
full: options.full,
|
|
754
|
+
})));
|
|
755
|
+
agent
|
|
756
|
+
.command('interrupt <id>')
|
|
757
|
+
.description('Send a bare ESC to stop the current turn (the conversation survives; deleting would not)')
|
|
758
|
+
.option('--json', 'Machine-readable output')
|
|
759
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentInterrupt(deps, { id })));
|
|
760
|
+
agent
|
|
761
|
+
.command('rm <id>')
|
|
762
|
+
.description('Delete any session except this one (refuses your own id)')
|
|
763
|
+
.option('--json', 'Machine-readable output')
|
|
764
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentRm(deps, { id })));
|
|
765
|
+
return agent;
|
|
766
|
+
}
|
|
767
|
+
//# sourceMappingURL=cli-agent.js.map
|