aicodeman 1.33.2 → 1.34.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/dist/config/cli-registry/schema.d.ts +18 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +27 -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 +35 -0
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +33 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +5 -1
- package/dist/hooks-config.js.map +1 -1
- package/dist/mcp-sync.d.ts +105 -0
- package/dist/mcp-sync.d.ts.map +1 -0
- package/dist/mcp-sync.js +619 -0
- package/dist/mcp-sync.js.map +1 -0
- package/dist/mux-interface.d.ts +4 -0
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/session-cli-builder.d.ts +20 -1
- package/dist/session-cli-builder.d.ts.map +1 -1
- package/dist/session-cli-builder.js +33 -3
- package/dist/session-cli-builder.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts +2 -0
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +15 -8
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session.d.ts +19 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +29 -2
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +2 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +4 -2
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/mcp-sync.d.ts +40 -0
- package/dist/types/mcp-sync.d.ts.map +1 -0
- package/dist/types/mcp-sync.js +8 -0
- package/dist/types/mcp-sync.js.map +1 -0
- package/dist/types/push.d.ts +37 -2
- package/dist/types/push.d.ts.map +1 -1
- package/dist/types/push.js +10 -3
- package/dist/types/push.js.map +1 -1
- package/dist/types/session.d.ts +34 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js +25 -1
- package/dist/types/session.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.cf0db8ce.js +43 -0
- package/dist/web/public/app.cf0db8ce.js.br +0 -0
- package/dist/web/public/app.cf0db8ce.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.cc927eb8.js → constants.d95fade9.js} +19 -2
- package/dist/web/public/constants.d95fade9.js.br +0 -0
- package/dist/web/public/constants.d95fade9.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/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.398b8331.js +1 -0
- package/dist/web/public/i18n.398b8331.js.br +0 -0
- package/dist/web/public/i18n.398b8331.js.gz +0 -0
- package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
- package/dist/web/public/index.html +125 -14
- 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.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js +1 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.br +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.1aa4952d.css.gz +0 -0
- package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.67646701.js → panels-ui.6a5898d9.js} +69 -67
- 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/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/{ralph-wizard.13a1831e.js → ralph-wizard.b6c0dd19.js} +2 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.br +0 -0
- package/dist/web/public/{ralph-wizard.13a1831e.js.gz → 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 +1 -0
- package/dist/web/public/sanitize-html.e7147e76.js.br +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js +27 -11
- 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.7575a231.js → session-ui.bf66c13b.js} +4 -4
- package/dist/web/public/session-ui.bf66c13b.js.br +0 -0
- package/dist/web/public/session-ui.bf66c13b.js.gz +0 -0
- package/dist/web/public/{settings-ui.c68e7f59.js → settings-ui.331d3b1d.js} +12 -10
- package/dist/web/public/settings-ui.331d3b1d.js.br +0 -0
- package/dist/web/public/settings-ui.331d3b1d.js.gz +0 -0
- package/dist/web/public/styles.8c485d26.css +1 -0
- package/dist/web/public/styles.8c485d26.css.br +0 -0
- package/dist/web/public/styles.8c485d26.css.gz +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.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.4e0ed50c.js +1 -0
- package/dist/web/public/tab-layout-browser.4e0ed50c.js.br +0 -0
- package/dist/web/public/tab-layout-browser.4e0ed50c.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.dcdc5772.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
- package/dist/web/public/terminal-split.js +248 -15
- 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-ui.a4535311.js +2 -0
- 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/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.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.bd6882b8.js → xterm-predictive-echo.ae5e3d5d.js} +1 -1
- package/dist/web/public/vendor/{xterm-predictive-echo.bd6882b8.js.br → xterm-predictive-echo.ae5e3d5d.js.br} +0 -0
- package/dist/web/public/vendor/{xterm-predictive-echo.bd6882b8.js.gz → xterm-predictive-echo.ae5e3d5d.js.gz} +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +4 -0
- 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/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.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js +36 -14
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/reboot-restore-registry.d.ts +2 -1
- package/dist/web/reboot-restore-registry.d.ts.map +1 -1
- package/dist/web/reboot-restore-registry.js +2 -1
- package/dist/web/reboot-restore-registry.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +2 -1
- package/dist/web/routes/file-routes.js.map +1 -1
- 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/mcp-sync-routes.d.ts +29 -0
- package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -0
- package/dist/web/routes/mcp-sync-routes.js +83 -0
- package/dist/web/routes/mcp-sync-routes.js.map +1 -0
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +2 -1
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +2 -0
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +40 -17
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/webhook-routes.d.ts +21 -0
- package/dist/web/routes/webhook-routes.d.ts.map +1 -0
- package/dist/web/routes/webhook-routes.js +87 -0
- package/dist/web/routes/webhook-routes.js.map +1 -0
- package/dist/web/schemas.d.ts +47 -2
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +64 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +4 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +55 -25
- package/dist/web/server.js.map +1 -1
- package/dist/webhook-notify.d.ts +71 -0
- package/dist/webhook-notify.d.ts.map +1 -0
- package/dist/webhook-notify.js +282 -0
- package/dist/webhook-notify.js.map +1 -0
- package/package.json +2 -1
- package/skills/codeman/SKILL.md +26 -8
- package/skills/codeman/preamble.sh +16 -4
- package/skills/codeman/reference/endpoints.md +11 -3
- package/skills/codeman/reference/recipes.md +1 -1
- package/dist/web/public/app.91131d2c.js +0 -43
- package/dist/web/public/app.91131d2c.js.br +0 -0
- package/dist/web/public/app.91131d2c.js.gz +0 -0
- package/dist/web/public/constants.cc927eb8.js.br +0 -0
- package/dist/web/public/constants.cc927eb8.js.gz +0 -0
- package/dist/web/public/i18n.f094d01f.js +0 -1
- package/dist/web/public/i18n.f094d01f.js.br +0 -0
- package/dist/web/public/i18n.f094d01f.js.gz +0 -0
- package/dist/web/public/panels-ui.67646701.js.br +0 -0
- package/dist/web/public/panels-ui.67646701.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.br +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js +0 -1
- package/dist/web/public/sanitize-html.bc7078d6.js.br +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.7575a231.js.br +0 -0
- package/dist/web/public/session-ui.7575a231.js.gz +0 -0
- package/dist/web/public/settings-ui.c68e7f59.js.br +0 -0
- package/dist/web/public/settings-ui.c68e7f59.js.gz +0 -0
- package/dist/web/public/styles.3fe3312d.css +0 -1
- package/dist/web/public/styles.3fe3312d.css.br +0 -0
- package/dist/web/public/styles.3fe3312d.css.gz +0 -0
- package/dist/web/public/terminal-ui.d5223c0e.js +0 -2
- package/dist/web/public/terminal-ui.d5223c0e.js.br +0 -0
- package/dist/web/public/terminal-ui.d5223c0e.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js +0 -4
- package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Webhook notifications (ntfy, Slack, Discord, generic JSON) for the events that
|
|
3
|
+
* already trigger Web Push, so a headless server can reach a phone without a browser tab or a
|
|
4
|
+
* push subscription.
|
|
5
|
+
*
|
|
6
|
+
* Split in three, so the parts that matter are testable without a network:
|
|
7
|
+
* - pure: `webhookUrlProblem`, `maskWebhookUrl`, `shouldSendWebhook`, `buildWebhookRequest`
|
|
8
|
+
* - store: `~/.codeman/webhook.json`, written 0600 via tmp+rename (the URL is a bearer secret:
|
|
9
|
+
* anyone holding a Slack/Discord webhook URL can post as it)
|
|
10
|
+
* - IO: `sendWebhook` (injected fetch) and `WebhookNotifier` (dedupe, in-flight cap, last result)
|
|
11
|
+
*
|
|
12
|
+
* Rules the code keeps and the tests pin:
|
|
13
|
+
* - The URL is configured only through the admin-only `/api/webhook` routes and kept OUT of
|
|
14
|
+
* `settings.json`, which every logged-in user can read through `GET /api/settings`.
|
|
15
|
+
* - Delivery goes through `webviewFetch`: link-local and cloud-metadata targets are refused on
|
|
16
|
+
* the RESOLVED address at connect time, redirects are not followed, and the call is bounded
|
|
17
|
+
* by a timeout. Loopback and LAN stay allowed on purpose (a local ntfy is the feature).
|
|
18
|
+
* - The URL never appears in a log line, a result, or an error message.
|
|
19
|
+
* - Session names and error text are user/agent-controlled, so they cannot ping a channel:
|
|
20
|
+
* Discord gets `allowed_mentions: { parse: [] }` and Slack control characters are escaped.
|
|
21
|
+
*
|
|
22
|
+
* @module webhook-notify
|
|
23
|
+
*/
|
|
24
|
+
import { type WebhookConfig, type WebhookKind, type WebhookResult, type WebhookUrgency } from './types/push.js';
|
|
25
|
+
export declare const DEFAULT_WEBHOOK_CONFIG: WebhookConfig;
|
|
26
|
+
export interface WebhookMessage {
|
|
27
|
+
event: string;
|
|
28
|
+
title: string;
|
|
29
|
+
body: string;
|
|
30
|
+
urgency: WebhookUrgency;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
sessionName?: string;
|
|
33
|
+
/** The Codeman instance's window title, so several machines are told apart. */
|
|
34
|
+
host?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Why `raw` cannot be a webhook URL, or null. Used at save time; delivery re-checks the resolved address. */
|
|
37
|
+
export declare function webhookUrlProblem(raw: string): string | null;
|
|
38
|
+
/** Scheme + host only: the path and query of a webhook URL are the secret. */
|
|
39
|
+
export declare function maskWebhookUrl(raw: string): string;
|
|
40
|
+
export declare function shouldSendWebhook(cfg: WebhookConfig, urgency: WebhookUrgency): boolean;
|
|
41
|
+
export interface WebhookRequest {
|
|
42
|
+
method: 'POST';
|
|
43
|
+
headers: Record<string, string>;
|
|
44
|
+
body: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function buildWebhookRequest(kind: WebhookKind, msg: WebhookMessage, now?: Date): WebhookRequest;
|
|
47
|
+
export declare function webhookConfigPath(configDir: string): string;
|
|
48
|
+
export declare function readWebhookConfig(configDir: string): Promise<WebhookConfig>;
|
|
49
|
+
/** 0600 via tmp+rename: `mode` on writeFile only applies to a file being created. */
|
|
50
|
+
export declare function writeWebhookConfig(configDir: string, cfg: WebhookConfig): Promise<void>;
|
|
51
|
+
export type WebhookFetch = (target: URL, init: RequestInit) => Promise<Response>;
|
|
52
|
+
export declare function sendWebhook(cfg: Pick<WebhookConfig, 'kind' | 'url'>, msg: WebhookMessage, fetchImpl: WebhookFetch): Promise<WebhookResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Sends the notifications the server decides on. Fire-and-forget by design (a slow webhook must
|
|
55
|
+
* never delay Web Push or a request), so it dedupes, caps what is in flight, and remembers only
|
|
56
|
+
* the last result for the Settings status line.
|
|
57
|
+
*/
|
|
58
|
+
export declare class WebhookNotifier {
|
|
59
|
+
private readonly load;
|
|
60
|
+
private readonly fetchImpl;
|
|
61
|
+
private readonly now;
|
|
62
|
+
private lastSent;
|
|
63
|
+
private inFlight;
|
|
64
|
+
private last;
|
|
65
|
+
constructor(load: () => Promise<WebhookConfig>, fetchImpl: WebhookFetch, now?: () => number);
|
|
66
|
+
get lastResult(): WebhookResult | null;
|
|
67
|
+
notify(msg: WebhookMessage): Promise<void>;
|
|
68
|
+
/** A deliberate test send: bypasses `enabled`, scope and dedupe, and records the result. */
|
|
69
|
+
sendTest(cfg: Pick<WebhookConfig, 'kind' | 'url'>, host?: string): Promise<WebhookResult>;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=webhook-notify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-notify.d.ts","sourceRoot":"","sources":["../src/webhook-notify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAUzB,eAAO,MAAM,sBAAsB,EAAE,aAA6E,CAAC;AAEnH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa5D;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAGtF;AAoBD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,GAAE,IAAiB,GAAG,cAAc,CAgDlH;AAMD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAgBD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAMjF;AAED,qFAAqF;AACrF,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAW7F;AAMD,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAgBjF,wBAAsB,WAAW,CAC/B,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,KAAK,CAAC,EACxC,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,YAAY,GACtB,OAAO,CAAC,aAAa,CAAC,CAuBxB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAMxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,IAAI,CAA8B;gBAGvB,IAAI,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,EAClC,SAAS,EAAE,YAAY,EACvB,GAAG,GAAE,MAAM,MAAiB;IAG/C,IAAI,UAAU,IAAI,aAAa,GAAG,IAAI,CAErC;IAEK,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBhD,4FAA4F;IACtF,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;CAehG"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Webhook notifications (ntfy, Slack, Discord, generic JSON) for the events that
|
|
3
|
+
* already trigger Web Push, so a headless server can reach a phone without a browser tab or a
|
|
4
|
+
* push subscription.
|
|
5
|
+
*
|
|
6
|
+
* Split in three, so the parts that matter are testable without a network:
|
|
7
|
+
* - pure: `webhookUrlProblem`, `maskWebhookUrl`, `shouldSendWebhook`, `buildWebhookRequest`
|
|
8
|
+
* - store: `~/.codeman/webhook.json`, written 0600 via tmp+rename (the URL is a bearer secret:
|
|
9
|
+
* anyone holding a Slack/Discord webhook URL can post as it)
|
|
10
|
+
* - IO: `sendWebhook` (injected fetch) and `WebhookNotifier` (dedupe, in-flight cap, last result)
|
|
11
|
+
*
|
|
12
|
+
* Rules the code keeps and the tests pin:
|
|
13
|
+
* - The URL is configured only through the admin-only `/api/webhook` routes and kept OUT of
|
|
14
|
+
* `settings.json`, which every logged-in user can read through `GET /api/settings`.
|
|
15
|
+
* - Delivery goes through `webviewFetch`: link-local and cloud-metadata targets are refused on
|
|
16
|
+
* the RESOLVED address at connect time, redirects are not followed, and the call is bounded
|
|
17
|
+
* by a timeout. Loopback and LAN stay allowed on purpose (a local ntfy is the feature).
|
|
18
|
+
* - The URL never appears in a log line, a result, or an error message.
|
|
19
|
+
* - Session names and error text are user/agent-controlled, so they cannot ping a channel:
|
|
20
|
+
* Discord gets `allowed_mentions: { parse: [] }` and Slack control characters are escaped.
|
|
21
|
+
*
|
|
22
|
+
* @module webhook-notify
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
25
|
+
import fs from 'node:fs/promises';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { blockedWebviewHostReason } from './web/webview-egress-policy.js';
|
|
28
|
+
import { isEgressBlockedError } from './web/webview-egress.js';
|
|
29
|
+
import { WEBHOOK_KINDS, WEBHOOK_SCOPES, } from './types/push.js';
|
|
30
|
+
const WEBHOOK_FILE = 'webhook.json';
|
|
31
|
+
const MAX_URL_LENGTH = 2048;
|
|
32
|
+
const SEND_TIMEOUT_MS = 5000;
|
|
33
|
+
const MAX_BODY_CHARS = 500;
|
|
34
|
+
/** Same event + session within this window is sent once: a flapping prompt must not flood a channel. */
|
|
35
|
+
const DEDUPE_WINDOW_MS = 3000;
|
|
36
|
+
const MAX_IN_FLIGHT = 5;
|
|
37
|
+
export const DEFAULT_WEBHOOK_CONFIG = { enabled: false, kind: 'ntfy', url: '', scope: 'attention' };
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Pure
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/** Why `raw` cannot be a webhook URL, or null. Used at save time; delivery re-checks the resolved address. */
|
|
42
|
+
export function webhookUrlProblem(raw) {
|
|
43
|
+
if (raw.length > MAX_URL_LENGTH)
|
|
44
|
+
return 'URL is too long';
|
|
45
|
+
let url;
|
|
46
|
+
try {
|
|
47
|
+
url = new URL(raw);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return 'Not a valid URL';
|
|
51
|
+
}
|
|
52
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
53
|
+
return 'Only http and https URLs are allowed';
|
|
54
|
+
if (url.username || url.password)
|
|
55
|
+
return 'Put credentials in the path or a header-less token, not user:password@';
|
|
56
|
+
const blocked = blockedWebviewHostReason(url.hostname);
|
|
57
|
+
if (blocked)
|
|
58
|
+
return `Refused: ${blocked}`;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** Scheme + host only: the path and query of a webhook URL are the secret. */
|
|
62
|
+
export function maskWebhookUrl(raw) {
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(raw);
|
|
65
|
+
return `${url.protocol}//${url.host}/•••`;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function shouldSendWebhook(cfg, urgency) {
|
|
72
|
+
if (!cfg.enabled || !cfg.url)
|
|
73
|
+
return false;
|
|
74
|
+
return cfg.scope === 'all' || urgency !== 'info';
|
|
75
|
+
}
|
|
76
|
+
const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
77
|
+
/** A header value must be single-line printable ASCII; anything else goes out RFC 2047 encoded. */
|
|
78
|
+
function headerSafe(value) {
|
|
79
|
+
const oneLine = value.replace(/[\r\n]+/g, ' ').trim();
|
|
80
|
+
return /^[\x20-\x7e]*$/.test(oneLine) ? oneLine : `=?UTF-8?B?${Buffer.from(oneLine, 'utf8').toString('base64')}?=`;
|
|
81
|
+
}
|
|
82
|
+
/** Slack parses `<!channel>`, `<@U123>` and `<url|text>`; escaping the three control characters turns them to text. */
|
|
83
|
+
const slackEscape = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
84
|
+
const NTFY_PRIORITY = { critical: '5', warning: '4', info: '3' };
|
|
85
|
+
const NTFY_TAGS = {
|
|
86
|
+
critical: 'rotating_light',
|
|
87
|
+
warning: 'bell',
|
|
88
|
+
info: 'white_check_mark',
|
|
89
|
+
};
|
|
90
|
+
export function buildWebhookRequest(kind, msg, now = new Date()) {
|
|
91
|
+
const title = clip(msg.title, 120);
|
|
92
|
+
const body = clip(msg.body, MAX_BODY_CHARS);
|
|
93
|
+
const prefix = msg.host ? `${clip(msg.host, 60)}: ` : '';
|
|
94
|
+
switch (kind) {
|
|
95
|
+
case 'ntfy':
|
|
96
|
+
return {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
100
|
+
Title: headerSafe(`${prefix}${title}`),
|
|
101
|
+
Priority: NTFY_PRIORITY[msg.urgency],
|
|
102
|
+
Tags: NTFY_TAGS[msg.urgency],
|
|
103
|
+
},
|
|
104
|
+
body: body || title,
|
|
105
|
+
};
|
|
106
|
+
case 'slack':
|
|
107
|
+
return {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({ text: `*${slackEscape(`${prefix}${title}`)}*${body ? `\n${slackEscape(body)}` : ''}` }),
|
|
111
|
+
};
|
|
112
|
+
case 'discord':
|
|
113
|
+
return {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
content: clip(`**${prefix}${title}**${body ? `\n${body}` : ''}`, 1900),
|
|
118
|
+
// Agent output and session names are not trusted to @everyone a channel.
|
|
119
|
+
allowed_mentions: { parse: [] },
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
case 'generic':
|
|
123
|
+
return {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
event: msg.event,
|
|
128
|
+
title,
|
|
129
|
+
body,
|
|
130
|
+
urgency: msg.urgency,
|
|
131
|
+
sessionId: msg.sessionId ?? null,
|
|
132
|
+
sessionName: msg.sessionName ?? null,
|
|
133
|
+
host: msg.host ?? null,
|
|
134
|
+
at: now.toISOString(),
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Store
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
export function webhookConfigPath(configDir) {
|
|
143
|
+
return join(configDir, WEBHOOK_FILE);
|
|
144
|
+
}
|
|
145
|
+
function coerce(raw) {
|
|
146
|
+
const r = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
147
|
+
return {
|
|
148
|
+
enabled: r.enabled === true,
|
|
149
|
+
kind: WEBHOOK_KINDS.includes(r.kind)
|
|
150
|
+
? r.kind
|
|
151
|
+
: DEFAULT_WEBHOOK_CONFIG.kind,
|
|
152
|
+
url: typeof r.url === 'string' ? r.url : '',
|
|
153
|
+
scope: WEBHOOK_SCOPES.includes(r.scope)
|
|
154
|
+
? r.scope
|
|
155
|
+
: DEFAULT_WEBHOOK_CONFIG.scope,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export async function readWebhookConfig(configDir) {
|
|
159
|
+
try {
|
|
160
|
+
return coerce(JSON.parse(await fs.readFile(webhookConfigPath(configDir), 'utf-8')));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { ...DEFAULT_WEBHOOK_CONFIG };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** 0600 via tmp+rename: `mode` on writeFile only applies to a file being created. */
|
|
167
|
+
export async function writeWebhookConfig(configDir, cfg) {
|
|
168
|
+
if (!existsSync(configDir))
|
|
169
|
+
mkdirSync(configDir, { recursive: true });
|
|
170
|
+
const target = webhookConfigPath(configDir);
|
|
171
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
172
|
+
try {
|
|
173
|
+
await fs.writeFile(tmp, JSON.stringify(coerce(cfg), null, 2), { mode: 0o600 });
|
|
174
|
+
await fs.rename(tmp, target);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
await fs.unlink(tmp).catch(() => undefined);
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* What went wrong, without the URL: blocked / timed out / refused / an HTTP status. An egress
|
|
183
|
+
* refusal is recognised by its `CODEMAN_EGRESS_BLOCKED` code anywhere in the cause chain (undici
|
|
184
|
+
* wraps the lookup's error as `TypeError('fetch failed', { cause })`), never by message text.
|
|
185
|
+
*/
|
|
186
|
+
function describeError(err) {
|
|
187
|
+
const e = err;
|
|
188
|
+
if (e?.name === 'TimeoutError' || e?.name === 'AbortError')
|
|
189
|
+
return 'Timed out';
|
|
190
|
+
if (isEgressBlockedError(err))
|
|
191
|
+
return 'Refused: target is a link-local or cloud-metadata address';
|
|
192
|
+
if (e?.cause?.code === 'ENOTFOUND')
|
|
193
|
+
return 'Host not found';
|
|
194
|
+
if (e?.cause?.code === 'ECONNREFUSED')
|
|
195
|
+
return 'Connection refused';
|
|
196
|
+
return 'Network error';
|
|
197
|
+
}
|
|
198
|
+
export async function sendWebhook(cfg, msg, fetchImpl) {
|
|
199
|
+
const at = Date.now();
|
|
200
|
+
const problem = webhookUrlProblem(cfg.url);
|
|
201
|
+
if (problem)
|
|
202
|
+
return { ok: false, error: problem, at };
|
|
203
|
+
const req = buildWebhookRequest(cfg.kind, msg);
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetchImpl(new URL(cfg.url), {
|
|
206
|
+
method: req.method,
|
|
207
|
+
headers: req.headers,
|
|
208
|
+
body: req.body,
|
|
209
|
+
redirect: 'manual',
|
|
210
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
211
|
+
});
|
|
212
|
+
void res.body?.cancel().catch(() => undefined);
|
|
213
|
+
if (res.status >= 300 && res.status < 400) {
|
|
214
|
+
return { ok: false, status: res.status, error: 'The URL redirects; use the final URL', at };
|
|
215
|
+
}
|
|
216
|
+
return res.ok
|
|
217
|
+
? { ok: true, status: res.status, at }
|
|
218
|
+
: { ok: false, status: res.status, error: `HTTP ${res.status}`, at };
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return { ok: false, error: describeError(err), at };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Sends the notifications the server decides on. Fire-and-forget by design (a slow webhook must
|
|
226
|
+
* never delay Web Push or a request), so it dedupes, caps what is in flight, and remembers only
|
|
227
|
+
* the last result for the Settings status line.
|
|
228
|
+
*/
|
|
229
|
+
export class WebhookNotifier {
|
|
230
|
+
load;
|
|
231
|
+
fetchImpl;
|
|
232
|
+
now;
|
|
233
|
+
lastSent = new Map();
|
|
234
|
+
inFlight = 0;
|
|
235
|
+
last = null;
|
|
236
|
+
constructor(load, fetchImpl, now = Date.now) {
|
|
237
|
+
this.load = load;
|
|
238
|
+
this.fetchImpl = fetchImpl;
|
|
239
|
+
this.now = now;
|
|
240
|
+
}
|
|
241
|
+
get lastResult() {
|
|
242
|
+
return this.last;
|
|
243
|
+
}
|
|
244
|
+
async notify(msg) {
|
|
245
|
+
const cfg = await this.load();
|
|
246
|
+
if (!shouldSendWebhook(cfg, msg.urgency))
|
|
247
|
+
return;
|
|
248
|
+
const key = `${msg.event}:${msg.sessionId ?? ''}`;
|
|
249
|
+
const t = this.now();
|
|
250
|
+
const prev = this.lastSent.get(key);
|
|
251
|
+
if (prev !== undefined && t - prev < DEDUPE_WINDOW_MS)
|
|
252
|
+
return;
|
|
253
|
+
if (this.inFlight >= MAX_IN_FLIGHT)
|
|
254
|
+
return;
|
|
255
|
+
this.lastSent.set(key, t);
|
|
256
|
+
if (this.lastSent.size > 256) {
|
|
257
|
+
for (const [k, v] of this.lastSent)
|
|
258
|
+
if (t - v > DEDUPE_WINDOW_MS)
|
|
259
|
+
this.lastSent.delete(k);
|
|
260
|
+
}
|
|
261
|
+
this.inFlight++;
|
|
262
|
+
try {
|
|
263
|
+
this.last = await sendWebhook(cfg, msg, this.fetchImpl);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
this.inFlight--;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** A deliberate test send: bypasses `enabled`, scope and dedupe, and records the result. */
|
|
270
|
+
async sendTest(cfg, host) {
|
|
271
|
+
const result = await sendWebhook(cfg, {
|
|
272
|
+
event: 'webhook:test',
|
|
273
|
+
title: 'Codeman test notification',
|
|
274
|
+
body: 'If you can read this, webhook notifications are working.',
|
|
275
|
+
urgency: 'info',
|
|
276
|
+
host,
|
|
277
|
+
}, this.fetchImpl);
|
|
278
|
+
this.last = result;
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=webhook-notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-notify.js","sourceRoot":"","sources":["../src/webhook-notify.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicodeman",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"jpeg-js": "^0.4.4",
|
|
106
106
|
"node-pty": "^1.1.0",
|
|
107
107
|
"qrcode": "^1.5.4",
|
|
108
|
+
"smol-toml": "^1.9.0",
|
|
108
109
|
"undici": "^6.28.0",
|
|
109
110
|
"uuid": "^14.0.0",
|
|
110
111
|
"web-push": "^3.6.7",
|
package/skills/codeman/SKILL.md
CHANGED
|
@@ -47,7 +47,7 @@ later call opens with, and your first REAL call performs them anyway:
|
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
|
50
|
-
[ "${CODEMAN_PREAMBLE:-}" = 1.
|
|
50
|
+
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
|
|
@@ -75,8 +75,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh"
|
|
|
75
75
|
mkdir -p "$(dirname "$PRE")"
|
|
76
76
|
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
|
|
77
77
|
# half-written file self-heals here instead of costing you a round trip to rm it.
|
|
78
|
-
grep -qs '^CODEMAN_PREAMBLE=1.
|
|
79
|
-
# ---- Codeman agent preamble 1.
|
|
78
|
+
grep -qs '^CODEMAN_PREAMBLE=1.33.4$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
|
|
79
|
+
# ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
|
80
80
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
|
81
81
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
|
82
82
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
|
@@ -196,6 +196,11 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
|
|
|
196
196
|
# rather than handed back, because a worker that never drew its composer would eat the
|
|
197
197
|
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
|
198
198
|
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
|
199
|
+
# CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
|
|
200
|
+
# it is set Claude Code's advisor tool: a stronger model the worker consults before
|
|
201
|
+
# committing to an approach, on a recurring error and before declaring the task done.
|
|
202
|
+
# Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
|
|
203
|
+
# advisor that ranks below the worker's model is accepted but never attached by claude.
|
|
199
204
|
spawn_worker() {
|
|
200
205
|
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
|
201
206
|
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
|
@@ -207,12 +212,19 @@ spawn_worker() {
|
|
|
207
212
|
# server clamps this back to `workspace-write` for an owner without the grant.
|
|
208
213
|
# Spawn by hand (§5.1) when you want a worker that asks.
|
|
209
214
|
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
|
210
|
-
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
|
|
215
|
+
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
|
|
211
216
|
'{caseName:$n,mode:$m,parentSessionId:$p}
|
|
212
|
-
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
|
217
|
+
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
|
218
|
+
+ (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
|
|
213
219
|
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
|
214
220
|
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
|
215
221
|
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
|
222
|
+
# A server without advisor support DROPS the field instead of refusing it, so read it
|
|
223
|
+
# back: a worker silently missing the advisor it was asked for is worth one line.
|
|
224
|
+
if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
|
|
225
|
+
[ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
|
|
226
|
+
echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
|
|
227
|
+
fi
|
|
216
228
|
if [ "$mode" = deepseek ]; then
|
|
217
229
|
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
|
218
230
|
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
|
@@ -372,10 +384,10 @@ last_text() {
|
|
|
372
384
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
|
373
385
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
|
374
386
|
# here would fail that match and rewrite this file on every single bootstrap.
|
|
375
|
-
CODEMAN_PREAMBLE=1.
|
|
387
|
+
CODEMAN_PREAMBLE=1.33.4
|
|
376
388
|
PREAMBLE
|
|
377
389
|
)
|
|
378
|
-
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.
|
|
390
|
+
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
|
|
379
391
|
```
|
|
380
392
|
|
|
381
393
|
Every later Bash call that touches the API starts with the same two loader lines from
|
|
@@ -426,7 +438,7 @@ and no per-call body to hand-build.
|
|
|
426
438
|
|
|
427
439
|
```bash
|
|
428
440
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
|
|
429
|
-
[ "${CODEMAN_PREAMBLE:-}" = 1.
|
|
441
|
+
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
|
430
442
|
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
|
|
431
443
|
# (a name may carry a mode: `beta:deepseek`, see below)
|
|
432
444
|
T=('reply with one line: the absolute path of your working directory'
|
|
@@ -493,6 +505,12 @@ Four things this block leans on, each one link away, no detour needed to run it:
|
|
|
493
505
|
`sendwait` reads the composer and keeps pressing Enter until the prompt has left it.
|
|
494
506
|
All three are reasons to let `sendwait` build the call rather than hand-rolling it.
|
|
495
507
|
- Each `sendwait` costs that worker one billed turn, as does every prompt you send it.
|
|
508
|
+
- For long or high-stakes worker tasks, `CODEMAN_WORKER_ADVISOR=opus spawn_workers "${N[@]}"`
|
|
509
|
+
(`fable`, `opus` or `sonnet`) gives each claude worker Claude Code's advisor tool: a
|
|
510
|
+
stronger model it consults before committing to an approach, on a recurring error and
|
|
511
|
+
before declaring the task done. Advisor calls bill extra tokens, and an advisor ranked
|
|
512
|
+
below the worker's own model is never attached (on an Opus worker only `opus` and
|
|
513
|
+
`fable` do anything).
|
|
496
514
|
- Deleting the sessions does **not** remove the case directories. They are marked as
|
|
497
515
|
agent-created, so `GET /api/v1/cases/agent-created` lists them for cleanup: §5.14.
|
|
498
516
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ---- Codeman agent preamble 1.
|
|
1
|
+
# ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
|
2
2
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
|
3
3
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
|
4
4
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
|
@@ -118,6 +118,11 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
|
|
|
118
118
|
# rather than handed back, because a worker that never drew its composer would eat the
|
|
119
119
|
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
|
120
120
|
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
|
121
|
+
# CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
|
|
122
|
+
# it is set Claude Code's advisor tool: a stronger model the worker consults before
|
|
123
|
+
# committing to an approach, on a recurring error and before declaring the task done.
|
|
124
|
+
# Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
|
|
125
|
+
# advisor that ranks below the worker's model is accepted but never attached by claude.
|
|
121
126
|
spawn_worker() {
|
|
122
127
|
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
|
123
128
|
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
|
@@ -129,12 +134,19 @@ spawn_worker() {
|
|
|
129
134
|
# server clamps this back to `workspace-write` for an owner without the grant.
|
|
130
135
|
# Spawn by hand (§5.1) when you want a worker that asks.
|
|
131
136
|
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
|
132
|
-
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
|
|
137
|
+
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
|
|
133
138
|
'{caseName:$n,mode:$m,parentSessionId:$p}
|
|
134
|
-
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
|
139
|
+
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
|
140
|
+
+ (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
|
|
135
141
|
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
|
136
142
|
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
|
137
143
|
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
|
144
|
+
# A server without advisor support DROPS the field instead of refusing it, so read it
|
|
145
|
+
# back: a worker silently missing the advisor it was asked for is worth one line.
|
|
146
|
+
if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
|
|
147
|
+
[ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
|
|
148
|
+
echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
|
|
149
|
+
fi
|
|
138
150
|
if [ "$mode" = deepseek ]; then
|
|
139
151
|
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
|
140
152
|
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
|
@@ -294,4 +306,4 @@ last_text() {
|
|
|
294
306
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
|
295
307
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
|
296
308
|
# here would fail that match and rewrite this file on every single bootstrap.
|
|
297
|
-
CODEMAN_PREAMBLE=1.
|
|
309
|
+
CODEMAN_PREAMBLE=1.33.4
|
|
@@ -345,6 +345,13 @@ ESC=$(printf '\033')
|
|
|
345
345
|
`.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
|
|
346
346
|
on the user's disk) if missing, do not retry it in a loop, and remember the name.
|
|
347
347
|
|
|
348
|
+
A claude worker also takes `"advisorModel":"opus"` (`fable`, `opus`, `sonnet` or a full
|
|
349
|
+
model id): Claude Code's advisor tool, a stronger model the worker consults before
|
|
350
|
+
committing to an approach, on a recurring error and before declaring the task done. It is
|
|
351
|
+
a soft default the worker can change with `/advisor`. Remote and docker cases refuse it
|
|
352
|
+
(400), as they refuse `effort`. `spawn_worker` and `spawn_workers` send it for you when
|
|
353
|
+
`CODEMAN_WORKER_ADVISOR` is set.
|
|
354
|
+
|
|
348
355
|
⚠️ A `mode` whose CLI is **not installed on the server** fails the spawn with
|
|
349
356
|
`OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick
|
|
350
357
|
the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`,
|
|
@@ -384,17 +391,18 @@ every claude create path installs them, so a linked case and a raw path both get
|
|
|
384
391
|
|
|
385
392
|
**The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in
|
|
386
393
|
a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`,
|
|
387
|
-
`envOverrides
|
|
394
|
+
`advisorModel`, `envOverrides`, and for claude a per-session `model` passed as `--model`). Three
|
|
395
|
+
differences that break copied code:
|
|
388
396
|
|
|
389
397
|
- The id is at **`.data.session.id`**, not quick-start's `.data.sessionId`
|
|
390
|
-
(`session-routes.ts
|
|
398
|
+
(the `POST /api/sessions` handler in `session-routes.ts` returns `{ session: lightState }`).
|
|
391
399
|
- **It spawns no PTY.** The session exists with `pid:null` and nothing running, so
|
|
392
400
|
`wait?until=exit` answers `exit` immediately. Follow it with
|
|
393
401
|
`POST /api/v1/sessions/:id/interactive` (claude and the other agent CLIs) or
|
|
394
402
|
`POST /api/v1/sessions/:id/shell` (shell mode) to actually start the worker.
|
|
395
403
|
- Its capacity failure is **`OPERATION_FAILED` (422)**, not quick-start's
|
|
396
404
|
`SESSION_BUSY` (409), from the same global-50 / per-user-25 caps
|
|
397
|
-
(`
|
|
405
|
+
(`sessionCapacityMessage()` in `route-helpers.ts`).
|
|
398
406
|
|
|
399
407
|
⚠️ `POST .../interactive` accepts `{"clearBreaker":true}`, which resets the **PTY-exit
|
|
400
408
|
circuit breaker**. That breaker exists to stop a session that keeps crashing on spawn
|
|
@@ -21,7 +21,7 @@ by sourcing the preamble file the §0 bootstrap wrote, and checking its version
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
|
24
|
-
[ "${CODEMAN_PREAMBLE:-}" = 1.
|
|
24
|
+
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Do **not** re-paste the preamble body into each call. Sourcing it is what retires the
|