aicodeman 1.44.0 → 1.46.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 +32 -8
- package/dist/agent-codex-identity.d.ts +26 -0
- package/dist/agent-codex-identity.d.ts.map +1 -0
- package/dist/agent-codex-identity.js +253 -0
- package/dist/agent-codex-identity.js.map +1 -0
- package/dist/agent-inspection.d.ts +32 -0
- package/dist/agent-inspection.d.ts.map +1 -0
- package/dist/agent-inspection.js +196 -0
- package/dist/agent-inspection.js.map +1 -0
- package/dist/agent-staging.d.ts +130 -0
- package/dist/agent-staging.d.ts.map +1 -0
- package/dist/agent-staging.js +312 -0
- package/dist/agent-staging.js.map +1 -0
- package/dist/case-rename.d.ts +65 -0
- package/dist/case-rename.d.ts.map +1 -0
- package/dist/case-rename.js +198 -0
- package/dist/case-rename.js.map +1 -0
- package/dist/cli-agent.d.ts +91 -3
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +501 -44
- package/dist/cli-agent.js.map +1 -1
- package/dist/cli-history.d.ts +20 -0
- package/dist/cli-history.d.ts.map +1 -0
- package/dist/cli-history.js +61 -0
- package/dist/cli-history.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex-rollout-format.d.ts +66 -0
- package/dist/codex-rollout-format.d.ts.map +1 -0
- package/dist/codex-rollout-format.js +111 -0
- package/dist/codex-rollout-format.js.map +1 -0
- package/dist/codex-transcript.d.ts +3 -0
- package/dist/codex-transcript.d.ts.map +1 -1
- package/dist/codex-transcript.js +13 -41
- package/dist/codex-transcript.js.map +1 -1
- package/dist/config/auth-config.d.ts +16 -1
- package/dist/config/auth-config.d.ts.map +1 -1
- package/dist/config/auth-config.js +16 -1
- package/dist/config/auth-config.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +12 -7
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +26 -0
- 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 +40 -1
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +57 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/server-timing.d.ts +6 -0
- package/dist/config/server-timing.d.ts.map +1 -1
- package/dist/config/server-timing.js +9 -0
- package/dist/config/server-timing.js.map +1 -1
- package/dist/daemon-control.d.ts +1 -0
- package/dist/daemon-control.d.ts.map +1 -1
- package/dist/daemon-control.js +2 -0
- package/dist/daemon-control.js.map +1 -1
- package/dist/deepseek-transcript.d.ts +9 -7
- package/dist/deepseek-transcript.d.ts.map +1 -1
- package/dist/deepseek-transcript.js +9 -7
- package/dist/deepseek-transcript.js.map +1 -1
- package/dist/docker-hosts.d.ts +8 -0
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +29 -0
- package/dist/docker-hosts.js.map +1 -1
- package/dist/git-fetch-scheduler.d.ts +124 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -0
- package/dist/git-fetch-scheduler.js +249 -0
- package/dist/git-fetch-scheduler.js.map +1 -0
- package/dist/git-fetch.d.ts +241 -0
- package/dist/git-fetch.d.ts.map +1 -0
- package/dist/git-fetch.js +392 -0
- package/dist/git-fetch.js.map +1 -0
- package/dist/git-workspace-status.d.ts +36 -1
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/git-workspace-status.js +42 -5
- package/dist/git-workspace-status.js.map +1 -1
- package/dist/hooks-config.d.ts +14 -2
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +39 -3
- package/dist/hooks-config.js.map +1 -1
- package/dist/host-sensors.d.ts +115 -0
- package/dist/host-sensors.d.ts.map +1 -0
- package/dist/host-sensors.js +457 -0
- package/dist/host-sensors.js.map +1 -0
- package/dist/local-history.d.ts +71 -0
- package/dist/local-history.d.ts.map +1 -0
- package/dist/local-history.js +750 -0
- package/dist/local-history.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/mux-interface.d.ts +5 -2
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.js +5 -3
- package/dist/prompt-cache-keepwarm.js.map +1 -1
- package/dist/remote-hosts.d.ts +7 -0
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +17 -3
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-pane-history.d.ts.map +1 -1
- package/dist/remote-pane-history.js +2 -2
- package/dist/remote-pane-history.js.map +1 -1
- package/dist/session.d.ts +113 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +354 -13
- package/dist/session.js.map +1 -1
- package/dist/terminal-input-classifier.d.ts +8 -0
- package/dist/terminal-input-classifier.d.ts.map +1 -0
- package/dist/terminal-input-classifier.js +60 -0
- package/dist/terminal-input-classifier.js.map +1 -0
- package/dist/tmux-manager.d.ts +30 -4
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +101 -21
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-prefix-table.d.ts +37 -5
- package/dist/tmux-prefix-table.d.ts.map +1 -1
- package/dist/tmux-prefix-table.js +40 -5
- package/dist/tmux-prefix-table.js.map +1 -1
- package/dist/tui/tui-app.d.ts +8 -1
- package/dist/tui/tui-app.d.ts.map +1 -1
- package/dist/tui/tui-app.js +49 -12
- package/dist/tui/tui-app.js.map +1 -1
- package/dist/tui/tui-client.d.ts +90 -3
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +195 -26
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/types/session.d.ts +15 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/agent-inbox-persistence.d.ts +52 -0
- package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
- package/dist/web/agent-inbox-persistence.js +139 -0
- package/dist/web/agent-inbox-persistence.js.map +1 -0
- package/dist/web/agent-inbox.d.ts +140 -0
- package/dist/web/agent-inbox.d.ts.map +1 -0
- package/dist/web/agent-inbox.js +258 -0
- package/dist/web/agent-inbox.js.map +1 -0
- package/dist/web/middleware/auth.d.ts +2 -0
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +145 -52
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/network-auth-policy.d.ts +13 -3
- package/dist/web/network-auth-policy.d.ts.map +1 -1
- package/dist/web/network-auth-policy.js +15 -3
- package/dist/web/network-auth-policy.js.map +1 -1
- package/dist/web/ports/infra-port.d.ts +2 -0
- package/dist/web/ports/infra-port.d.ts.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.3093527f.js +43 -0
- package/dist/web/public/app.3093527f.js.br +0 -0
- package/dist/web/public/app.3093527f.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.4866ecc3.js → constants.affcdde6.js} +622 -12
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +88 -5
- 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/header-divider.eb2d2463.js +1 -0
- package/dist/web/public/header-divider.eb2d2463.js.br +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +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.70265257.js +2 -0
- package/dist/web/public/i18n.70265257.js.br +0 -0
- package/dist/web/public/i18n.70265257.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/index.html +170 -16
- 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.4e8b20ac.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.44adaa07.js → mobile-handlers.f7c3f692.js} +26 -2
- package/dist/web/public/mobile-handlers.f7c3f692.js.br +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.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.1bbd2d67.css +1 -0
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +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.cbb24ba1.js → panels-ui.4a56ee63.js} +76 -70
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/panels-ui.4a56ee63.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.d7adcf6a.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.186f9419.js +106 -0
- package/dist/web/public/session-ui.186f9419.js.br +0 -0
- package/dist/web/public/session-ui.186f9419.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +106 -0
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.7238eae8.css +1 -0
- package/dist/web/public/styles.7238eae8.css.br +0 -0
- package/dist/web/public/styles.7238eae8.css.gz +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.2d6189f7.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.7157e0bd.js → tab-rail-resize.b5cdceaf.js} +1 -1
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.br +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +10 -1
- 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.7e787829.js +4 -0
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +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.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +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 → xterm-predictive-echo.276b11d9.js} +1 -1
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.br +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js +5 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.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.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/route-helpers.d.ts +57 -6
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +72 -4
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/agent-session-routes.d.ts +20 -0
- package/dist/web/routes/agent-session-routes.d.ts.map +1 -0
- package/dist/web/routes/agent-session-routes.js +160 -0
- package/dist/web/routes/agent-session-routes.js.map +1 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +203 -16
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.js +2 -2
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts +21 -7
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +78 -12
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/inbox-routes.d.ts +43 -0
- package/dist/web/routes/inbox-routes.d.ts.map +1 -0
- package/dist/web/routes/inbox-routes.js +148 -0
- package/dist/web/routes/inbox-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +7 -16
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +120 -74
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +107 -59
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/ws-routes.d.ts +5 -0
- package/dist/web/routes/ws-routes.d.ts.map +1 -1
- package/dist/web/routes/ws-routes.js +84 -47
- package/dist/web/routes/ws-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +56 -7
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +88 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +2 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +7 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +144 -16
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +48 -4
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +144 -6
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/settings-writer.d.ts +47 -0
- package/dist/web/settings-writer.d.ts.map +1 -0
- package/dist/web/settings-writer.js +110 -0
- package/dist/web/settings-writer.js.map +1 -0
- package/dist/web/sse-events.d.ts +4 -1
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +4 -1
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +30 -8
- package/skills/codeman/preamble.sh +3 -3
- package/skills/codeman/reference/endpoints.md +24 -3
- package/skills/codeman/reference/history.md +84 -0
- package/skills/codeman/reference/inspection-and-staging.md +152 -0
- package/skills/codeman/reference/messaging.md +1 -1
- package/skills/codeman/reference/verbs.md +65 -3
- package/dist/web/public/app.9314ee60.js +0 -43
- package/dist/web/public/app.9314ee60.js.br +0 -0
- package/dist/web/public/app.9314ee60.js.gz +0 -0
- package/dist/web/public/constants.4866ecc3.js.br +0 -0
- package/dist/web/public/constants.4866ecc3.js.gz +0 -0
- package/dist/web/public/i18n.22ca902b.js +0 -2
- package/dist/web/public/i18n.22ca902b.js.br +0 -0
- package/dist/web/public/i18n.22ca902b.js.gz +0 -0
- package/dist/web/public/mobile-handlers.44adaa07.js.br +0 -0
- package/dist/web/public/mobile-handlers.44adaa07.js.gz +0 -0
- package/dist/web/public/mobile.12fb18de.css +0 -1
- package/dist/web/public/mobile.12fb18de.css.br +0 -0
- package/dist/web/public/mobile.12fb18de.css.gz +0 -0
- package/dist/web/public/panels-ui.cbb24ba1.js.br +0 -0
- package/dist/web/public/panels-ui.cbb24ba1.js.gz +0 -0
- package/dist/web/public/session-ui.9c1f009b.js +0 -104
- package/dist/web/public/session-ui.9c1f009b.js.br +0 -0
- package/dist/web/public/session-ui.9c1f009b.js.gz +0 -0
- package/dist/web/public/settings-ui.fea9ddae.js +0 -106
- package/dist/web/public/settings-ui.fea9ddae.js.br +0 -0
- package/dist/web/public/settings-ui.fea9ddae.js.gz +0 -0
- package/dist/web/public/styles.e096fcd6.css +0 -1
- package/dist/web/public/styles.e096fcd6.css.br +0 -0
- package/dist/web/public/styles.e096fcd6.css.gz +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-ui.68dbd352.js +0 -2
- package/dist/web/public/terminal-ui.68dbd352.js.br +0 -0
- package/dist/web/public/terminal-ui.68dbd352.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.br +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 +0 -4
- 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/cli-agent.js
CHANGED
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
* @fileoverview `codeman agent …` — session-to-session verbs for the agent running
|
|
3
3
|
* inside a Codeman session, in every CLI mode.
|
|
4
4
|
*
|
|
5
|
-
* A thin HTTP client over
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* A thin HTTP client over the server's endpoints (`quick-start` for a named
|
|
6
|
+
* case, `POST sessions` + `sessions/:id/interactive|shell` for a worker in an
|
|
7
|
+
* existing directory, `input`, `wait`, `wait-output`, `last-response`, `terminal`,
|
|
8
|
+
* `DELETE sessions/:id`, the agent inbox: `inbox`, `inbox/ack`,
|
|
9
|
+
* `agent-inbox/summary`, and the experimental, opt-in
|
|
10
|
+
* `inspect`/`stage`/`submit` of `web/routes/agent-session-routes.ts`, which answer 403
|
|
11
|
+
* until `agentCoordinationEnabled` is on; that refusal is printed verbatim). It uses no
|
|
12
|
+
* transport of its own: everything goes through `CODEMAN_API_URL`, so auth, ownership
|
|
13
|
+
* and the per-session waiter cap apply unchanged. The behaviour is
|
|
9
14
|
* the packaged agent skill's (`skills/codeman`), ported from shell prose into code
|
|
10
15
|
* with tests, so a `codex`/`opencode`/`pi` agent — which never gets the claude-only
|
|
11
16
|
* preamble — has the same verbs from one line in its AGENTS.md.
|
|
12
17
|
*
|
|
13
|
-
* Invariants (each asserted in `test/cli-agent.test.ts`
|
|
18
|
+
* Invariants (each asserted in `test/cli-agent.test.ts` or
|
|
19
|
+
* `test/cli-agent-project-spawn.test.ts`):
|
|
14
20
|
* 1. Refuses outside a Codeman session (`CODEMAN_MUX=1` + `CODEMAN_API_URL`); it
|
|
15
21
|
* never guesses a URL — a server you are not part of is not yours to drive.
|
|
16
22
|
* 2. `send` transmits printable text plus `\r` only. ESC exists solely as
|
|
@@ -22,14 +28,26 @@
|
|
|
22
28
|
* 4. An id shorter than 8 characters refuses (exit 4) before any request, on every
|
|
23
29
|
* verb. `9` would resolve to whichever session is alone with that first
|
|
24
30
|
* character, the user's own interactive tab included.
|
|
31
|
+
* 5. `spawn` without a case starts the worker in an existing directory (cwd, or
|
|
32
|
+
* `--working-dir`) via `POST sessions` + `interactive|shell`, without the
|
|
33
|
+
* `X-Codeman-Agent-Origin` header, so no agent-case marker is written. It refuses
|
|
34
|
+
* (exit 4, before any request) an empty `--working-dir` and a bare spawn from a
|
|
35
|
+
* HOME cwd: it always sends an explicit `workingDir`, so the server's skip of
|
|
36
|
+
* workspace hooks for a request that names none cannot protect `~/.claude`.
|
|
25
37
|
*
|
|
26
38
|
* Commands live here as functions returning an exit code, not calling
|
|
27
39
|
* `process.exit`, so the whole surface is unit-testable against a fake server.
|
|
28
40
|
*
|
|
29
41
|
* @module cli-agent
|
|
30
42
|
*/
|
|
43
|
+
import { open, realpath, stat } from 'node:fs/promises';
|
|
44
|
+
import { homedir } from 'node:os';
|
|
45
|
+
import { resolve } from 'node:path';
|
|
31
46
|
import http from 'node:http';
|
|
32
47
|
import https from 'node:https';
|
|
48
|
+
import { isUtf8 } from 'node:buffer';
|
|
49
|
+
import { STAGE_MAX_BYTES } from './agent-staging.js';
|
|
50
|
+
import { INSPECT_MAX_BYTES } from './agent-inspection.js';
|
|
33
51
|
import { basicAuthHeader, credentialsFrom, readCodemanEnvFile, } from './codeman-credentials.js';
|
|
34
52
|
import { getCli } from './config/cli-registry/registry.js';
|
|
35
53
|
import { GLYPH, palette, table } from './cli-style.js';
|
|
@@ -161,6 +179,19 @@ export function stripAnsi(text) {
|
|
|
161
179
|
// eslint-disable-next-line no-control-regex
|
|
162
180
|
return stripAnsiSequences(text).replace(/\x1b[()][AB0]/g, '');
|
|
163
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Inbox message text for a terminal. Another session wrote it, so an escape sequence
|
|
184
|
+
* in it would otherwise reach the reader's terminal (retitle it, or move the cursor
|
|
185
|
+
* over the `from` line above it). ANSI sequences go as in `read --tail`, and every
|
|
186
|
+
* control character left (C0 other than newline and tab, DEL, C1) is shown as `\xHH`,
|
|
187
|
+
* so a bare `\r` or backspace cannot overwrite the header either. A CRLF line end
|
|
188
|
+
* stays a line end. `--json` prints the text as stored.
|
|
189
|
+
*/
|
|
190
|
+
export function terminalSafeText(text) {
|
|
191
|
+
return (stripAnsi(text.replace(/\r\n/g, '\n'))
|
|
192
|
+
// eslint-disable-next-line no-control-regex
|
|
193
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`));
|
|
194
|
+
}
|
|
164
195
|
/** Parse a positive-integer option (`--timeout` ms, `--tail` bytes); the server rejects anything else. */
|
|
165
196
|
export function parsePositiveInt(raw, fallback, flag = '--timeout') {
|
|
166
197
|
if (raw === undefined)
|
|
@@ -254,6 +285,18 @@ export const httpRequest = (ctx, options) => {
|
|
|
254
285
|
req.end();
|
|
255
286
|
});
|
|
256
287
|
};
|
|
288
|
+
/**
|
|
289
|
+
* One line describing a thrown request error. Node 22 rejects a refused connection to a host
|
|
290
|
+
* that resolves to both ::1 and 127.0.0.1 with an AggregateError whose `message` is empty, so
|
|
291
|
+
* fall back to its `code` (ECONNREFUSED) before a generic line rather than printing nothing.
|
|
292
|
+
*/
|
|
293
|
+
export function describeRequestError(error) {
|
|
294
|
+
const message = getErrorMessage(error);
|
|
295
|
+
if (message)
|
|
296
|
+
return message;
|
|
297
|
+
const code = error?.code;
|
|
298
|
+
return code ? String(code) : 'request failed without a message';
|
|
299
|
+
}
|
|
257
300
|
/** One line describing a failed response, for humans. Plain-text guards (401/403/429) have no envelope. */
|
|
258
301
|
export function describeFailure(res) {
|
|
259
302
|
if (res.json && !res.json.success) {
|
|
@@ -264,7 +307,13 @@ export function describeFailure(res) {
|
|
|
264
307
|
return `HTTP 401 ${text}: the server wants a password (CODEMAN_PASSWORD, or the data dir's .env)`;
|
|
265
308
|
return `HTTP ${res.status}${text ? ` ${text}` : ''}`;
|
|
266
309
|
}
|
|
267
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* Print `data` as JSON (the `--json` path): the envelope's `data`, never a reshaped copy,
|
|
312
|
+
* except a directory spawn, which prints `{ sessionId, workingDir, created: true }` plus
|
|
313
|
+
* `ready` when a readiness wait ran, `readinessError` when that call failed, and
|
|
314
|
+
* `startupError` when its start fails, because `POST /api/sessions` answers with the
|
|
315
|
+
* whole session row.
|
|
316
|
+
*/
|
|
268
317
|
function emitJson(deps, data) {
|
|
269
318
|
deps.io.out(JSON.stringify(data, null, 2));
|
|
270
319
|
}
|
|
@@ -313,29 +362,68 @@ export async function resolveSessionId(deps, id) {
|
|
|
313
362
|
return { error: `no session starts with "${id}" (see \`agent ls\`)`, code: EXIT.error };
|
|
314
363
|
return { error: `"${id}" is ambiguous: ${matches.map((s) => s.id.slice(0, 13)).join(', ')}`, code: EXIT.error };
|
|
315
364
|
}
|
|
316
|
-
/**
|
|
365
|
+
/**
|
|
366
|
+
* The inbox summary, or undefined when it cannot be had (a server without the route,
|
|
367
|
+
* a transient error). `ls` must still list sessions then, so this never fails the verb.
|
|
368
|
+
*/
|
|
369
|
+
async function fetchInboxSummary(deps) {
|
|
370
|
+
try {
|
|
371
|
+
const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/agent-inbox/summary' });
|
|
372
|
+
if (!res.json?.success)
|
|
373
|
+
return undefined;
|
|
374
|
+
return res.json.data?.sessions ?? {};
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** The INBOX cell of `ls`: `3 (1 new)`, `parked`, both, or empty. */
|
|
381
|
+
export function inboxCell(row) {
|
|
382
|
+
if (!row)
|
|
383
|
+
return '';
|
|
384
|
+
const parts = [];
|
|
385
|
+
if (row.pending > 0)
|
|
386
|
+
parts.push(row.unseen > 0 ? `${row.pending} (${row.unseen} new)` : String(row.pending));
|
|
387
|
+
if (row.parked)
|
|
388
|
+
parts.push('parked');
|
|
389
|
+
return parts.join(', ');
|
|
390
|
+
}
|
|
391
|
+
/** `agent ls` — every session the caller can see, self marked, with its inbox counts. */
|
|
317
392
|
export async function agentLs(deps) {
|
|
318
|
-
const res = await
|
|
393
|
+
const [res, inbox] = await Promise.all([
|
|
394
|
+
deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' }),
|
|
395
|
+
fetchInboxSummary(deps),
|
|
396
|
+
]);
|
|
319
397
|
if (!res.json?.success)
|
|
320
398
|
return fail(deps, describeFailure(res));
|
|
321
399
|
const sessions = res.json.data ?? [];
|
|
322
400
|
if (deps.json) {
|
|
323
|
-
emitJson(deps, sessions.map((s) => ({
|
|
401
|
+
emitJson(deps, sessions.map((s) => ({
|
|
402
|
+
...s,
|
|
403
|
+
self: isSelfSession(deps.ctx.selfId, s.id),
|
|
404
|
+
// null = empty inbox and not parked. The key is absent when the server did
|
|
405
|
+
// not answer the summary, so "no data" never reads as "no mail".
|
|
406
|
+
...(inbox ? { inbox: inbox[s.id] ?? null } : {}),
|
|
407
|
+
})));
|
|
324
408
|
return EXIT.ok;
|
|
325
409
|
}
|
|
326
410
|
if (sessions.length === 0) {
|
|
327
411
|
deps.io.out(palette.muted('(no sessions)'));
|
|
328
412
|
return EXIT.ok;
|
|
329
413
|
}
|
|
330
|
-
const rows = sessions.map((s) =>
|
|
331
|
-
isSelfSession(deps.ctx.selfId, s.id) ? '*' : ' ',
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
s.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
414
|
+
const rows = sessions.map((s) => {
|
|
415
|
+
const row = [isSelfSession(deps.ctx.selfId, s.id) ? '*' : ' ', s.id.slice(0, 8), s.mode ?? '?', s.status ?? '?'];
|
|
416
|
+
if (inbox)
|
|
417
|
+
row.push(inboxCell(inbox[s.id]));
|
|
418
|
+
row.push(s.name || s.workingDir || '');
|
|
419
|
+
return row;
|
|
420
|
+
});
|
|
421
|
+
const header = inbox ? [' ', 'ID', 'MODE', 'STATUS', 'INBOX', 'NAME'] : [' ', 'ID', 'MODE', 'STATUS', 'NAME'];
|
|
422
|
+
deps.io.out(table([header, ...rows], { gap: 2 }));
|
|
338
423
|
deps.io.out(palette.muted(`* = this session (${deps.ctx.selfId.slice(0, 8)}). status is a UI hint, never a sync signal.`));
|
|
424
|
+
if (inbox) {
|
|
425
|
+
deps.io.out(palette.muted('INBOX: pending (unseen), parked = the session is waiting on `agent inbox --wait` right now.'));
|
|
426
|
+
}
|
|
339
427
|
return EXIT.ok;
|
|
340
428
|
}
|
|
341
429
|
/**
|
|
@@ -352,46 +440,137 @@ export const AGENT_ORIGIN_HEADER = { 'X-Codeman-Agent-Origin': 'codeman-agent-cl
|
|
|
352
440
|
export function composerReadyMark(mode) {
|
|
353
441
|
return getCli(mode)?.capabilities.composerReadyMark;
|
|
354
442
|
}
|
|
355
|
-
/**
|
|
443
|
+
/** realpath, or the plain resolved path when it cannot be resolved; never throws. */
|
|
444
|
+
async function canonicalPath(path) {
|
|
445
|
+
try {
|
|
446
|
+
return await realpath(path);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return resolve(path);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Whether `directory` is the user's home directory, comparing realpaths so a symlinked
|
|
454
|
+
* HOME (or a cwd reached through a symlink) still matches. False when HOME is unknown.
|
|
455
|
+
*/
|
|
456
|
+
export async function isHomeDirectory(directory) {
|
|
457
|
+
let home;
|
|
458
|
+
try {
|
|
459
|
+
home = homedir();
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
if (!home)
|
|
465
|
+
return false;
|
|
466
|
+
const [a, b] = await Promise.all([canonicalPath(directory), canonicalPath(home)]);
|
|
467
|
+
return a === b;
|
|
468
|
+
}
|
|
469
|
+
/** `agent spawn`: start in a project directory or named case, preserving lineage and mode readiness. */
|
|
356
470
|
export async function agentSpawn(deps, options) {
|
|
471
|
+
const cli = getCli(options.mode);
|
|
472
|
+
if (!cli)
|
|
473
|
+
return fail(deps, `Unknown mode: ${options.mode}`, EXIT.refused);
|
|
474
|
+
if (options.caseName !== undefined && options.workingDir !== undefined)
|
|
475
|
+
return fail(deps, 'Use a case OR --working-dir, not both', EXIT.refused);
|
|
476
|
+
// Same rule for the flag: `--working-dir "$WT"` with WT empty (a failed
|
|
477
|
+
// `git worktree add`) must not fall through to the caller's own checkout, which
|
|
478
|
+
// `resolve('')` would make it.
|
|
479
|
+
if (options.workingDir !== undefined && options.workingDir.trim() === '')
|
|
480
|
+
return fail(deps, 'refusing: --working-dir is empty; pass an existing directory or omit the flag', EXIT.refused);
|
|
481
|
+
// Only an OMITTED case means "use a directory": an empty one (`spawn "$CASE"` with
|
|
482
|
+
// CASE unset) still goes to quick-start, whose schema refuses it.
|
|
483
|
+
const workingDir = options.caseName !== undefined ? undefined : resolve(options.workingDir ?? process.cwd());
|
|
484
|
+
if (workingDir) {
|
|
485
|
+
// eslint-disable-next-line no-control-regex
|
|
486
|
+
if (/[\x00-\x1f\x7f]/.test(workingDir))
|
|
487
|
+
return fail(deps, 'Working directory contains control characters', EXIT.refused);
|
|
488
|
+
try {
|
|
489
|
+
if (!(await stat(workingDir)).isDirectory())
|
|
490
|
+
throw new Error('not a directory');
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return fail(deps, 'Working directory must be an existing local directory', EXIT.refused);
|
|
494
|
+
}
|
|
495
|
+
// The server installs workspace hooks (and, with agent skill injection on, the
|
|
496
|
+
// skill) into the directory it is given, and it skips that only when the caller
|
|
497
|
+
// names no workingDir, because the cwd fallback is $HOME under installer-created
|
|
498
|
+
// services. This CLI always sends its directory explicitly, so an implicit cwd of
|
|
499
|
+
// HOME must be refused here or it would write ~/.claude/settings.local.json and
|
|
500
|
+
// ~/.claude/skills. An explicit `--working-dir ~` is the caller's choice and stays.
|
|
501
|
+
if (options.workingDir === undefined && (await isHomeDirectory(workingDir)))
|
|
502
|
+
return fail(deps, 'refusing: the current directory is your home directory; pass --working-dir ~ to start a worker there on purpose', EXIT.refused);
|
|
503
|
+
}
|
|
357
504
|
const body = {
|
|
358
|
-
caseName: options.caseName,
|
|
505
|
+
...(workingDir ? { workingDir } : { caseName: options.caseName }),
|
|
359
506
|
mode: options.mode,
|
|
360
507
|
parentSessionId: deps.ctx.selfId,
|
|
361
508
|
};
|
|
362
509
|
if (options.name)
|
|
363
|
-
body
|
|
510
|
+
body[workingDir ? 'name' : 'sessionName'] = options.name;
|
|
364
511
|
const res = await deps.request(deps.ctx, {
|
|
365
512
|
method: 'POST',
|
|
366
|
-
path: '/api/v1/quick-start',
|
|
513
|
+
path: workingDir ? '/api/v1/sessions' : '/api/v1/quick-start',
|
|
367
514
|
body,
|
|
368
|
-
headers: { ...AGENT_ORIGIN_HEADER },
|
|
515
|
+
headers: workingDir ? undefined : { ...AGENT_ORIGIN_HEADER },
|
|
369
516
|
});
|
|
370
|
-
const
|
|
517
|
+
const created = res.json?.data;
|
|
518
|
+
// `POST /api/sessions` answers `{ session: { id, ... } }`; quick-start answers `{ sessionId, ... }`.
|
|
519
|
+
const createdId = created?.session?.id;
|
|
520
|
+
const data = workingDir && createdId ? { sessionId: createdId, workingDir, created: true } : created;
|
|
371
521
|
if (!res.json?.success || !data?.sessionId)
|
|
372
522
|
return fail(deps, describeFailure(res));
|
|
373
523
|
const sid = data.sessionId;
|
|
524
|
+
if (workingDir) {
|
|
525
|
+
// Creation alone leaves a session without a pane. Report the exact created id
|
|
526
|
+
// on an ambiguous or rejected start; never retry a start or hide a leaked tab.
|
|
527
|
+
let startupError;
|
|
528
|
+
try {
|
|
529
|
+
const started = await deps.request(deps.ctx, {
|
|
530
|
+
method: 'POST',
|
|
531
|
+
path: `/api/v1/sessions/${encodeURIComponent(sid)}/${cli.capabilities.startMode}`,
|
|
532
|
+
});
|
|
533
|
+
if (!started.json?.success)
|
|
534
|
+
startupError = describeFailure(started);
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
startupError = getErrorMessage(error) || 'Startup request failed';
|
|
538
|
+
}
|
|
539
|
+
if (startupError) {
|
|
540
|
+
if (deps.json)
|
|
541
|
+
emitJson(deps, { ...data, ready: false, startupError });
|
|
542
|
+
else
|
|
543
|
+
deps.io.out(sid);
|
|
544
|
+
deps.io.err(`Session ${sid} was created but startup failed or is ambiguous: ${startupError}`);
|
|
545
|
+
return EXIT.error;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
374
548
|
let ready;
|
|
375
549
|
let readinessError;
|
|
376
550
|
let dead = false;
|
|
377
551
|
const mark = composerReadyMark(options.mode);
|
|
378
552
|
if (options.ready && mark) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
553
|
+
try {
|
|
554
|
+
const wait = await deps.request(deps.ctx, {
|
|
555
|
+
method: 'GET',
|
|
556
|
+
path: `/api/v1/sessions/${encodeURIComponent(sid)}/wait-output`,
|
|
557
|
+
query: { match: mark, from: 'buffer', timeout: options.timeoutMs },
|
|
558
|
+
timeoutMs: options.timeoutMs + 10_000,
|
|
559
|
+
});
|
|
560
|
+
// A failed readiness call (waiter cap, 400, network) is its own error, not "the
|
|
561
|
+
// composer never showed up": report the real reason instead of the trust-dialog hint.
|
|
562
|
+
if (!wait.json?.success)
|
|
563
|
+
readinessError = describeFailure(wait);
|
|
564
|
+
else {
|
|
565
|
+
const result = wait.json.data?.wait;
|
|
566
|
+
// A worker that died while we waited is exit 3 like every other wait, not a
|
|
567
|
+
// "composer not seen" timeout that sends the caller looking for a dialog.
|
|
568
|
+
dead = waitExitCode(result) === EXIT.dead;
|
|
569
|
+
ready = !dead && Boolean(result?.matched);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
readinessError = describeRequestError(error);
|
|
395
574
|
}
|
|
396
575
|
}
|
|
397
576
|
if (deps.json) {
|
|
@@ -400,7 +579,7 @@ export async function agentSpawn(deps, options) {
|
|
|
400
579
|
else {
|
|
401
580
|
// Human lines go to stderr so `SID=$(codeman agent spawn …)` captures the id alone.
|
|
402
581
|
const say = (line) => deps.io.err(line);
|
|
403
|
-
say(palette.ok(`${GLYPH.ok} spawned ${sid} (${options.mode},
|
|
582
|
+
say(palette.ok(`${GLYPH.ok} spawned ${sid} (${options.mode}, ${workingDir ?? data.caseName ?? options.caseName})`));
|
|
404
583
|
if (ready === true)
|
|
405
584
|
say(palette.muted(' composer up: the worker can take a prompt'));
|
|
406
585
|
if (dead)
|
|
@@ -549,6 +728,99 @@ export async function agentWait(deps, options) {
|
|
|
549
728
|
}
|
|
550
729
|
return waitExitCode(data?.wait);
|
|
551
730
|
}
|
|
731
|
+
/** Bounded incremental evidence, with an opaque server-owned cursor. */
|
|
732
|
+
export async function agentInspect(deps, options) {
|
|
733
|
+
if (!Number.isInteger(options.maxBytes) || options.maxBytes < 1 || options.maxBytes > INSPECT_MAX_BYTES)
|
|
734
|
+
return fail(deps, '--max-bytes must be between 1 and 65536', EXIT.refused);
|
|
735
|
+
const target = await resolveSessionId(deps, options.id);
|
|
736
|
+
if ('error' in target)
|
|
737
|
+
return fail(deps, target.error, target.code);
|
|
738
|
+
const response = await deps.request(deps.ctx, {
|
|
739
|
+
method: 'GET',
|
|
740
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}/inspect`,
|
|
741
|
+
query: { maxBytes: options.maxBytes, since: options.since },
|
|
742
|
+
});
|
|
743
|
+
if (!response.json?.success)
|
|
744
|
+
return fail(deps, describeFailure(response));
|
|
745
|
+
if (deps.json)
|
|
746
|
+
emitJson(deps, response.json.data);
|
|
747
|
+
else {
|
|
748
|
+
const data = response.json.data;
|
|
749
|
+
deps.io.out(`[${data.source ?? 'unknown'}]${data.truncated ? ' truncated' : ''}${data.blockedReason ? ` (${data.blockedReason})` : ''}`);
|
|
750
|
+
deps.io.out(data.evidence ?? '');
|
|
751
|
+
if (data.cursor)
|
|
752
|
+
deps.io.out(`cursor: ${data.cursor}`);
|
|
753
|
+
}
|
|
754
|
+
return EXIT.ok;
|
|
755
|
+
}
|
|
756
|
+
/** Stage exactly the printable UTF8 file contents; never sends Enter. */
|
|
757
|
+
export async function agentStage(deps, options) {
|
|
758
|
+
if (isSelfSession(deps.ctx.selfId, options.id))
|
|
759
|
+
return fail(deps, `refusing: ${options.id} is me`, EXIT.refused);
|
|
760
|
+
let message;
|
|
761
|
+
try {
|
|
762
|
+
const info = await stat(options.messageFile);
|
|
763
|
+
if (!info.isFile() || info.size > STAGE_MAX_BYTES)
|
|
764
|
+
return fail(deps, 'message file must be a regular file within 8192 UTF8 bytes', EXIT.refused);
|
|
765
|
+
const handle = await open(options.messageFile, 'r');
|
|
766
|
+
try {
|
|
767
|
+
const buffer = Buffer.alloc(STAGE_MAX_BYTES + 1);
|
|
768
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
769
|
+
const bytes = buffer.subarray(0, bytesRead);
|
|
770
|
+
if (bytesRead > STAGE_MAX_BYTES || !isUtf8(bytes))
|
|
771
|
+
return fail(deps, 'message file must contain at most 8192 bytes of UTF8', EXIT.refused);
|
|
772
|
+
message = bytes.toString('utf8');
|
|
773
|
+
}
|
|
774
|
+
finally {
|
|
775
|
+
await handle.close();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
return fail(deps, `cannot read message file: ${getErrorMessage(error)}`, EXIT.refused);
|
|
780
|
+
}
|
|
781
|
+
const refusal = inputRefusal(message);
|
|
782
|
+
if (refusal || message !== message.trimEnd())
|
|
783
|
+
return fail(deps, refusal ?? 'stage refuses trailing whitespace', EXIT.refused);
|
|
784
|
+
const target = await resolveSessionId(deps, options.id);
|
|
785
|
+
if ('error' in target)
|
|
786
|
+
return fail(deps, target.error, target.code);
|
|
787
|
+
const response = await deps.request(deps.ctx, {
|
|
788
|
+
method: 'POST',
|
|
789
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}/stage`,
|
|
790
|
+
body: { message },
|
|
791
|
+
});
|
|
792
|
+
if (!response.json?.success)
|
|
793
|
+
return fail(deps, describeFailure(response));
|
|
794
|
+
if (deps.json)
|
|
795
|
+
emitJson(deps, response.json.data);
|
|
796
|
+
else {
|
|
797
|
+
const data = response.json.data;
|
|
798
|
+
deps.io.out(`staged (no Enter): ${data.stageId}\nsha256: ${data.sha256}\nexpires: ${new Date(data.expiresAt).toISOString()}`);
|
|
799
|
+
}
|
|
800
|
+
return EXIT.ok;
|
|
801
|
+
}
|
|
802
|
+
/** Submit one verified staged composer; failure never automatically replays Enter. */
|
|
803
|
+
export async function agentSubmit(deps, options) {
|
|
804
|
+
if (isSelfSession(deps.ctx.selfId, options.id))
|
|
805
|
+
return fail(deps, `refusing: ${options.id} is me`, EXIT.refused);
|
|
806
|
+
if (!/^[a-f0-9]{64}$/.test(options.confirmSha) || !/^[a-f0-9-]{36}$/.test(options.stageId))
|
|
807
|
+
return fail(deps, 'submit requires a stage UUID and its exact SHA-256', EXIT.refused);
|
|
808
|
+
const target = await resolveSessionId(deps, options.id);
|
|
809
|
+
if ('error' in target)
|
|
810
|
+
return fail(deps, target.error, target.code);
|
|
811
|
+
const response = await deps.request(deps.ctx, {
|
|
812
|
+
method: 'POST',
|
|
813
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}/submit`,
|
|
814
|
+
body: { stageId: options.stageId, confirmSha: options.confirmSha },
|
|
815
|
+
});
|
|
816
|
+
if (!response.json?.success)
|
|
817
|
+
return fail(deps, describeFailure(response));
|
|
818
|
+
if (deps.json)
|
|
819
|
+
emitJson(deps, response.json.data);
|
|
820
|
+
else
|
|
821
|
+
deps.io.out('Enter delivered once; inspect or wait for acceptance (no automatic retry).');
|
|
822
|
+
return EXIT.ok;
|
|
823
|
+
}
|
|
552
824
|
/** `agent read` — the last answer (the route picks the transcript reader or the pane segmenter) or a terminal tail. */
|
|
553
825
|
export async function agentRead(deps, options) {
|
|
554
826
|
const target = await resolveSessionId(deps, options.id);
|
|
@@ -641,6 +913,136 @@ export async function agentRm(deps, options) {
|
|
|
641
913
|
deps.io.out(palette.ok(`${GLYPH.ok} deleted ${target.id} (its case directory stays on disk)`));
|
|
642
914
|
return EXIT.ok;
|
|
643
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* The message `post` was given, which must be ONE argument, as with `send`: several
|
|
918
|
+
* would be an unquoted `$(cat notes.md)` split by the shell, and joining the pieces
|
|
919
|
+
* with spaces loses its newlines. Unlike `send`, newlines inside the one argument are
|
|
920
|
+
* fine: the inbox stores text as data and never types it.
|
|
921
|
+
*/
|
|
922
|
+
export function postTextFromArgs(words) {
|
|
923
|
+
if (words.length === 1)
|
|
924
|
+
return { text: words[0] };
|
|
925
|
+
return { error: `refusing: the message must be ONE argument, got ${words.length} — quote it` };
|
|
926
|
+
}
|
|
927
|
+
/** `agent post` — store a message in another session's inbox. Never types into its pane. */
|
|
928
|
+
export async function agentPost(deps, options) {
|
|
929
|
+
if (options.text.length === 0)
|
|
930
|
+
return fail(deps, 'refusing: empty message', EXIT.refused);
|
|
931
|
+
const target = await resolveSessionId(deps, options.id);
|
|
932
|
+
if ('error' in target)
|
|
933
|
+
return fail(deps, target.error, target.code);
|
|
934
|
+
const res = await deps.request(deps.ctx, {
|
|
935
|
+
method: 'POST',
|
|
936
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}/inbox`,
|
|
937
|
+
body: { text: options.text },
|
|
938
|
+
});
|
|
939
|
+
// 409 = the receiver's inbox is full; nothing was stored. Passed through as an error.
|
|
940
|
+
if (!res.json?.success)
|
|
941
|
+
return fail(deps, describeFailure(res));
|
|
942
|
+
const data = res.json.data;
|
|
943
|
+
if (deps.json)
|
|
944
|
+
emitJson(deps, data ?? {});
|
|
945
|
+
else
|
|
946
|
+
deps.io.out(palette.ok(`${GLYPH.ok} posted to ${target.id.slice(0, 8)} (${data?.pending ?? '?'} pending, ${data?.unseen ?? '?'} unseen)`));
|
|
947
|
+
return EXIT.ok;
|
|
948
|
+
}
|
|
949
|
+
/** True only when the session list was read and does not contain `id`. */
|
|
950
|
+
async function sessionIsGone(deps, id) {
|
|
951
|
+
try {
|
|
952
|
+
const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' });
|
|
953
|
+
if (!res.json?.success)
|
|
954
|
+
return false;
|
|
955
|
+
return !(res.json.data ?? []).some((s) => s.id === id);
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/** Relative age for humans: `12s`, `4m`, `3h`. */
|
|
962
|
+
function age(createdAt, now) {
|
|
963
|
+
const s = Math.max(0, Math.round((now - createdAt) / 1000));
|
|
964
|
+
if (s < 60)
|
|
965
|
+
return `${s}s`;
|
|
966
|
+
if (s < 3600)
|
|
967
|
+
return `${Math.round(s / 60)}m`;
|
|
968
|
+
return `${Math.round(s / 3600)}h`;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* `agent inbox` — read this session's inbox (or peek at another's). Removes nothing:
|
|
972
|
+
* a message stays until `agent ack <id>`. Only reading your OWN inbox marks messages
|
|
973
|
+
* seen. Exit 0 with mail, 2 when a `--wait` ran out with nothing unseen or the server
|
|
974
|
+
* cut it short (restart, detach), 3 when the session is gone from the list.
|
|
975
|
+
*/
|
|
976
|
+
export async function agentInboxRead(deps, options) {
|
|
977
|
+
const target = await resolveSessionId(deps, options.id ?? deps.ctx.selfId);
|
|
978
|
+
if ('error' in target)
|
|
979
|
+
return fail(deps, target.error, target.code);
|
|
980
|
+
const res = await deps.request(deps.ctx, {
|
|
981
|
+
method: 'GET',
|
|
982
|
+
path: `/api/v1/sessions/${encodeURIComponent(target.id)}/inbox`,
|
|
983
|
+
query: { wait: options.waitMs },
|
|
984
|
+
timeoutMs: options.waitMs !== undefined ? options.waitMs + 10_000 : undefined,
|
|
985
|
+
});
|
|
986
|
+
if (!res.json?.success)
|
|
987
|
+
return fail(deps, describeFailure(res));
|
|
988
|
+
const data = res.json.data;
|
|
989
|
+
const messages = data?.messages ?? [];
|
|
990
|
+
// `ended` is also what a server restart or a detach answers. Exit 3 tells a receiver
|
|
991
|
+
// loop to stop, so it needs the session list to confirm the session is gone; while
|
|
992
|
+
// it is still listed (or the list cannot be had, as during a restart) the wait was
|
|
993
|
+
// only cut short, which is exit 2: loop again.
|
|
994
|
+
const gone = data?.wait?.ended ? await sessionIsGone(deps, target.id) : false;
|
|
995
|
+
const code = gone ? EXIT.dead : data?.wait?.timedOut || data?.wait?.ended ? EXIT.timeout : EXIT.ok;
|
|
996
|
+
if (deps.json) {
|
|
997
|
+
emitJson(deps, data ?? {});
|
|
998
|
+
return code;
|
|
999
|
+
}
|
|
1000
|
+
const now = (deps.now ?? Date.now)();
|
|
1001
|
+
for (const m of messages) {
|
|
1002
|
+
const sender = m.from ? m.from.slice(0, 8) : 'user';
|
|
1003
|
+
const tag = m.seen ? palette.muted('seen') : palette.emph('new');
|
|
1004
|
+
deps.io.out(palette.muted(`── ${m.id} from ${sender}, ${age(m.createdAt, now)} ago, `) + tag);
|
|
1005
|
+
deps.io.out(terminalSafeText(m.text));
|
|
1006
|
+
}
|
|
1007
|
+
if (gone) {
|
|
1008
|
+
deps.io.err(palette.err(`${GLYPH.fail} the session went away during the wait`));
|
|
1009
|
+
}
|
|
1010
|
+
else if (data?.wait?.ended) {
|
|
1011
|
+
deps.io.err(palette.muted('(the server ended the wait early, e.g. a restart; exit 2, wait again)'));
|
|
1012
|
+
}
|
|
1013
|
+
else if (messages.length === 0) {
|
|
1014
|
+
deps.io.err(palette.muted(data?.wait?.timedOut ? '(no mail; wait timed out, exit 2)' : '(no mail)'));
|
|
1015
|
+
}
|
|
1016
|
+
else if (data?.receiver) {
|
|
1017
|
+
deps.io.err(palette.muted(`${messages.length} pending. Nothing is removed by reading: when done, \`codeman agent ack <id>…\` (full ids above).`));
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
deps.io.err(palette.muted(`${messages.length} pending (peek: nothing was marked seen).`));
|
|
1021
|
+
}
|
|
1022
|
+
return code;
|
|
1023
|
+
}
|
|
1024
|
+
/** `agent ack` — remove messages from THIS session's inbox by id. Idempotent. */
|
|
1025
|
+
export async function agentAck(deps, options) {
|
|
1026
|
+
const ids = options.ids.map((id) => id.trim()).filter(Boolean);
|
|
1027
|
+
if (ids.length === 0)
|
|
1028
|
+
return fail(deps, 'refusing: no message ids', EXIT.refused);
|
|
1029
|
+
const self = await resolveSessionId(deps, deps.ctx.selfId);
|
|
1030
|
+
if ('error' in self)
|
|
1031
|
+
return fail(deps, self.error, self.code);
|
|
1032
|
+
const res = await deps.request(deps.ctx, {
|
|
1033
|
+
method: 'POST',
|
|
1034
|
+
path: `/api/v1/sessions/${encodeURIComponent(self.id)}/inbox/ack`,
|
|
1035
|
+
body: { ids },
|
|
1036
|
+
});
|
|
1037
|
+
if (!res.json?.success)
|
|
1038
|
+
return fail(deps, describeFailure(res));
|
|
1039
|
+
const data = res.json.data;
|
|
1040
|
+
if (deps.json)
|
|
1041
|
+
emitJson(deps, data ?? {});
|
|
1042
|
+
else
|
|
1043
|
+
deps.io.out(palette.ok(`${GLYPH.ok} acked ${data?.removed ?? 0} (${data?.pending ?? '?'} still pending)`));
|
|
1044
|
+
return EXIT.ok;
|
|
1045
|
+
}
|
|
644
1046
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
645
1047
|
// Commander wiring
|
|
646
1048
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -674,7 +1076,7 @@ async function run(json, verb) {
|
|
|
674
1076
|
process.exitCode = await verb(deps);
|
|
675
1077
|
}
|
|
676
1078
|
catch (err) {
|
|
677
|
-
console.error(palette.err(`${GLYPH.fail} ${
|
|
1079
|
+
console.error(palette.err(`${GLYPH.fail} ${describeRequestError(err)}`));
|
|
678
1080
|
process.exitCode = EXIT.error;
|
|
679
1081
|
}
|
|
680
1082
|
}
|
|
@@ -682,7 +1084,7 @@ async function run(json, verb) {
|
|
|
682
1084
|
export function registerAgentCommands(program) {
|
|
683
1085
|
const agent = program
|
|
684
1086
|
.command('agent')
|
|
685
|
-
.description('Talk to other sessions from inside one (any CLI mode): list, spawn, send, wait, read, interrupt, rm');
|
|
1087
|
+
.description('Talk to other sessions from inside one (any CLI mode): list, spawn, send, wait, read, interrupt, rm, the inbox: post, inbox, ack, and the experimental, opt-in inspect, stage, submit');
|
|
686
1088
|
agent
|
|
687
1089
|
.command('ls')
|
|
688
1090
|
.alias('list')
|
|
@@ -690,15 +1092,17 @@ export function registerAgentCommands(program) {
|
|
|
690
1092
|
.option('--json', 'Machine-readable output')
|
|
691
1093
|
.action((options) => run(Boolean(options.json), agentLs));
|
|
692
1094
|
agent
|
|
693
|
-
.command('spawn
|
|
694
|
-
.description('Start a worker
|
|
1095
|
+
.command('spawn [case]')
|
|
1096
|
+
.description('Start a worker in cwd, an existing directory, or a named case; wait for its composer where supported')
|
|
695
1097
|
.option('-m, --mode <mode>', 'Run mode id, as the Run menu names it', 'claude')
|
|
696
1098
|
.option('-n, --name <name>', 'Session name shown in the UI')
|
|
1099
|
+
.option('--working-dir <directory>', 'Existing project directory (defaults to cwd when no case is given; a cwd of HOME is refused, pass it explicitly)')
|
|
697
1100
|
.option('--no-ready', 'Return as soon as the session exists, without the readiness wait')
|
|
698
1101
|
.option('-t, --timeout <ms>', 'Readiness budget in ms', String(DEFAULT_WAIT_MS))
|
|
699
1102
|
.option('--json', 'Machine-readable output')
|
|
700
1103
|
.action((caseName, options) => run(Boolean(options.json), (deps) => agentSpawn(deps, {
|
|
701
1104
|
caseName,
|
|
1105
|
+
workingDir: options.workingDir,
|
|
702
1106
|
mode: options.mode,
|
|
703
1107
|
name: options.name,
|
|
704
1108
|
ready: options.ready,
|
|
@@ -747,6 +1151,30 @@ export function registerAgentCommands(program) {
|
|
|
747
1151
|
fresh: options.fresh,
|
|
748
1152
|
timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
|
|
749
1153
|
})));
|
|
1154
|
+
agent
|
|
1155
|
+
.command('inspect <id>')
|
|
1156
|
+
.description('Experimental, opt-in (agentCoordinationEnabled): bounded transcript evidence or a labeled pane snapshot; opaque incremental cursors')
|
|
1157
|
+
.option('--max-bytes <n>', 'UTF8 evidence budget (1–65536)', '8192')
|
|
1158
|
+
.option('--since <cursor>', 'Resume a cursor from the same owner, pane and transcript')
|
|
1159
|
+
.option('--json', 'Machine-readable output')
|
|
1160
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentInspect(deps, {
|
|
1161
|
+
id,
|
|
1162
|
+
maxBytes: parsePositiveInt(options.maxBytes, 8192, '--max-bytes'),
|
|
1163
|
+
since: options.since,
|
|
1164
|
+
})));
|
|
1165
|
+
agent
|
|
1166
|
+
.command('stage <id>')
|
|
1167
|
+
.description('Experimental, opt-in (agentCoordinationEnabled): type a printable single-line UTF8 file without Enter; requires a verifiable local Codex composer, and refuses slash or shell commands and a last word starting with @ or $ (use send)')
|
|
1168
|
+
.requiredOption('--message-file <path>', 'Regular UTF8 file, at most 8192 bytes, no trailing newline')
|
|
1169
|
+
.option('--json', 'Machine-readable output')
|
|
1170
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentStage(deps, { id, messageFile: options.messageFile })));
|
|
1171
|
+
agent
|
|
1172
|
+
.command('submit <id>')
|
|
1173
|
+
.description('Experimental, opt-in (agentCoordinationEnabled): send Enter once only when the staged composer, pane and hash still match')
|
|
1174
|
+
.requiredOption('--stage-id <receipt>', 'One-shot stage receipt')
|
|
1175
|
+
.requiredOption('--confirm-sha <hash>', 'Exact SHA-256 returned by stage')
|
|
1176
|
+
.option('--json', 'Machine-readable output')
|
|
1177
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentSubmit(deps, { id, stageId: options.stageId, confirmSha: options.confirmSha })));
|
|
750
1178
|
agent
|
|
751
1179
|
.command('read <id>')
|
|
752
1180
|
.description("Print a session's last answer (as the server reads it for that mode) or, with --tail, its terminal")
|
|
@@ -763,6 +1191,35 @@ export function registerAgentCommands(program) {
|
|
|
763
1191
|
.description('Send a bare ESC to stop the current turn (the conversation survives; deleting would not)')
|
|
764
1192
|
.option('--json', 'Machine-readable output')
|
|
765
1193
|
.action((id, options) => run(Boolean(options.json), (deps) => agentInterrupt(deps, { id })));
|
|
1194
|
+
agent
|
|
1195
|
+
.command('post <id> <text...>')
|
|
1196
|
+
.description("Leave a message in another session's inbox (ONE quoted argument; multi-line is fine). Nothing is typed into its pane: it reads with `agent inbox`")
|
|
1197
|
+
.option('--json', 'Machine-readable output')
|
|
1198
|
+
// A message is often a file (`"$(cat task.md)"`) and markdown starts with `- ` or
|
|
1199
|
+
// `---` often enough; without this commander takes it for an unknown option.
|
|
1200
|
+
// `--json` and `-h` are still options: text that is exactly one goes after `--`.
|
|
1201
|
+
.allowUnknownOption()
|
|
1202
|
+
.action((id, words, options) => run(Boolean(options.json), (deps) => {
|
|
1203
|
+
const message = postTextFromArgs(words);
|
|
1204
|
+
if ('error' in message)
|
|
1205
|
+
return Promise.resolve(fail(deps, message.error, EXIT.refused));
|
|
1206
|
+
return agentPost(deps, { id, text: message.text });
|
|
1207
|
+
}));
|
|
1208
|
+
agent
|
|
1209
|
+
.command('inbox [id]')
|
|
1210
|
+
.description("Read this session's inbox (or peek at another's with [id]). Reading removes nothing; `agent ack` does. --wait blocks while nothing is new: exit 2 on timeout")
|
|
1211
|
+
.option('-w, --wait', 'Block until a new (unseen) message arrives')
|
|
1212
|
+
.option('-t, --timeout <ms>', 'Wait budget in ms (with --wait)', String(DEFAULT_WAIT_MS))
|
|
1213
|
+
.option('--json', 'Machine-readable output')
|
|
1214
|
+
.action((id, options) => run(Boolean(options.json), (deps) => agentInboxRead(deps, {
|
|
1215
|
+
id,
|
|
1216
|
+
waitMs: options.wait ? parsePositiveInt(options.timeout, DEFAULT_WAIT_MS) : undefined,
|
|
1217
|
+
})));
|
|
1218
|
+
agent
|
|
1219
|
+
.command('ack <ids...>')
|
|
1220
|
+
.description("Remove messages from this session's inbox by id, once handled (unknown ids are not an error)")
|
|
1221
|
+
.option('--json', 'Machine-readable output')
|
|
1222
|
+
.action((ids, options) => run(Boolean(options.json), (deps) => agentAck(deps, { ids })));
|
|
766
1223
|
agent
|
|
767
1224
|
.command('rm <id>')
|
|
768
1225
|
.description('Delete any session except this one (refuses your own id)')
|