@lucascouts/claude-agent-tui 0.5.2 → 0.7.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/NOTICE +1 -1
- package/README.md +1 -1
- package/dist/acp-agent.d.ts +249 -21
- package/dist/acp-agent.js +573 -73
- package/dist/agent-catalog.d.ts +95 -0
- package/dist/agent-catalog.js +287 -0
- package/dist/ansi-mirror.d.ts +0 -1
- package/dist/besteffort.d.ts +0 -1
- package/dist/billing/entrypoint-guard.d.ts +0 -1
- package/dist/claude-path.d.ts +0 -1
- package/dist/claude-path.js +6 -0
- package/dist/command-catalog.d.ts +84 -0
- package/dist/command-catalog.js +339 -0
- package/dist/diff-enriched-reader.d.ts +0 -1
- package/dist/diff-source.d.ts +0 -1
- package/dist/drift-checks.d.ts +0 -1
- package/dist/end-of-turn.d.ts +6 -1
- package/dist/end-of-turn.js +8 -1
- package/dist/engine-lifecycle.d.ts +66 -2
- package/dist/engine-lifecycle.js +43 -4
- package/dist/engine-pty.d.ts +70 -3
- package/dist/engine-pty.js +80 -6
- package/dist/engine-watcher.d.ts +0 -1
- package/dist/engine.d.ts +0 -1
- package/dist/event-switch.d.ts +0 -1
- package/dist/gate/port.d.ts +0 -1
- package/dist/gate/settings-writer.d.ts +14 -1
- package/dist/gate/settings-writer.js +49 -0
- package/dist/image-input.d.ts +30 -0
- package/dist/image-input.js +79 -0
- package/dist/image-vision-smoke.d.ts +51 -0
- package/dist/image-vision-smoke.js +111 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +6 -0
- package/dist/jsonl.d.ts +0 -1
- package/dist/lib.d.ts +0 -1
- package/dist/linearize.d.ts +1 -2
- package/dist/linearize.js +1 -1
- package/dist/live-diff-env.d.ts +0 -1
- package/dist/live-subagent-env.d.ts +0 -1
- package/dist/mcp-config-writer.d.ts +60 -0
- package/dist/mcp-config-writer.js +172 -0
- package/dist/model-catalog.d.ts +68 -3
- package/dist/model-catalog.js +123 -13
- package/dist/permissions/allow-inject.d.ts +0 -1
- package/dist/permissions/deny.d.ts +12 -1
- package/dist/permissions/deny.js +18 -0
- package/dist/permissions/elicitation-bridge.d.ts +71 -0
- package/dist/permissions/elicitation-bridge.js +146 -0
- package/dist/permissions/gate-wiring.d.ts +23 -3
- package/dist/permissions/gate-wiring.js +123 -1
- package/dist/permissions/hook-server.d.ts +11 -3
- package/dist/permissions/hook-server.js +10 -1
- package/dist/permissions/permission-mode.d.ts +0 -1
- package/dist/permissions/request-permission.d.ts +0 -1
- package/dist/settings.d.ts +0 -1
- package/dist/settings.js +9 -0
- package/dist/stop-reason-map.d.ts +0 -1
- package/dist/subagent-gate.d.ts +0 -1
- package/dist/subagent-source.d.ts +0 -1
- package/dist/subagent-watcher.d.ts +0 -1
- package/dist/tools.d.ts +0 -1
- package/dist/tools.js +5 -1
- package/dist/usage-env.d.ts +0 -1
- package/dist/usage.d.ts +3 -1
- package/dist/usage.js +3 -0
- package/dist/utils.d.ts +0 -1
- package/dist/zed-register.d.ts +0 -1
- package/package.json +12 -9
- package/dist/acp-agent.d.ts.map +0 -1
- package/dist/ansi-mirror.d.ts.map +0 -1
- package/dist/besteffort.d.ts.map +0 -1
- package/dist/billing/entrypoint-guard.d.ts.map +0 -1
- package/dist/claude-path.d.ts.map +0 -1
- package/dist/diff-enriched-reader.d.ts.map +0 -1
- package/dist/diff-source.d.ts.map +0 -1
- package/dist/drift-checks.d.ts.map +0 -1
- package/dist/end-of-turn.d.ts.map +0 -1
- package/dist/engine-lifecycle.d.ts.map +0 -1
- package/dist/engine-pty.d.ts.map +0 -1
- package/dist/engine-watcher.d.ts.map +0 -1
- package/dist/engine.d.ts.map +0 -1
- package/dist/event-switch.d.ts.map +0 -1
- package/dist/gate/port.d.ts.map +0 -1
- package/dist/gate/settings-writer.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jsonl.d.ts.map +0 -1
- package/dist/lib.d.ts.map +0 -1
- package/dist/linearize.d.ts.map +0 -1
- package/dist/live-diff-env.d.ts.map +0 -1
- package/dist/live-subagent-env.d.ts.map +0 -1
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/permissions/allow-inject.d.ts.map +0 -1
- package/dist/permissions/deny.d.ts.map +0 -1
- package/dist/permissions/gate-wiring.d.ts.map +0 -1
- package/dist/permissions/hook-server.d.ts.map +0 -1
- package/dist/permissions/permission-mode.d.ts.map +0 -1
- package/dist/permissions/request-permission.d.ts.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/stop-reason-map.d.ts.map +0 -1
- package/dist/subagent-gate.d.ts.map +0 -1
- package/dist/subagent-source.d.ts.map +0 -1
- package/dist/subagent-watcher.d.ts.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- package/dist/usage-env.d.ts.map +0 -1
- package/dist/usage.d.ts.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/zed-register.d.ts.map +0 -1
package/dist/engine-pty.d.ts
CHANGED
|
@@ -49,19 +49,63 @@ export declare function resolveShell(baseEnv?: Record<string, string | undefined
|
|
|
49
49
|
* non-"default" mode is emitted as `--permission-mode <mode>`; "default"/undefined emit nothing;
|
|
50
50
|
* `permissionMode: "plan"` reproduces the old planMode=true path. Still interactive-only — adds no
|
|
51
51
|
* `-p`/`stream-json`, billing stays subscription `cli`.
|
|
52
|
+
*
|
|
53
|
+
* Story 056 (R3.3): an optional `agent` persona name is emitted as `--agent "<name>"` (DOUBLE-QUOTED,
|
|
54
|
+
* grouped with the other behavior flags). This is the SECOND layer of the two-layer command-injection
|
|
55
|
+
* defense — `agent-catalog.ts` already drops any name outside `SAFE_AGENT_REF`
|
|
56
|
+
* (`/^[A-Za-z0-9_-]+(?::[A-Za-z0-9_-]+)?$/`, allowing a namespaced `plugin:name`) at discovery, and
|
|
57
|
+
* here we re-assert via {@link isSafeAgentRef} so an unsafe name is silently DROPPED (no flag), never
|
|
58
|
+
* interpolated into the `-lc` shell string. Interactive-only — adds no `-p`/`stream-json`.
|
|
59
|
+
*
|
|
60
|
+
* Story 057 (R1.1/R1.2): an optional `additionalDirectories` list is emitted as ONE double-quoted
|
|
61
|
+
* `--add-dir "<dir>"` flag PER directory (never a single space-joined value — each dir is its own flag,
|
|
62
|
+
* matching the upstream/CLI shape). Each dir is re-asserted via {@link isSafeDir} (absolute + existing +
|
|
63
|
+
* free of shell metacharacters); an unsafe dir is DROPPED and a one-line warning is logged, never
|
|
64
|
+
* interpolated into the `-lc` shell string. Interactive-only — adds no `-p`/`stream-json`.
|
|
65
|
+
*
|
|
66
|
+
* Story 057 (R2.2): an optional `mcpConfigFile` PATH is emitted as the DOUBLE-QUOTED `--mcp-config
|
|
67
|
+
* "<file>"` flag (same style as `--settings` — a file path, so the secret-bearing JSON stays OFF the
|
|
68
|
+
* command line and survives the `-lc` shell word-splitting). The file is the fork-controlled
|
|
69
|
+
* uuid-named scratch from `mcp-config-writer.ts`. R2.2 is a HARD rule: `--strict-mcp-config` is NEVER
|
|
70
|
+
* emitted, so claude MERGES the scratch with the user's own `.mcp.json`/settings MCP servers (the
|
|
71
|
+
* parity target) instead of discarding them. Interactive-only — adds no `-p`/`stream-json`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildClaudeCmd(sessionId: string, permissionMode?: string, settingsFile?: string, effortLevel?: string, agent?: string, additionalDirectories?: string[], mcpConfigFile?: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Story 057 (R1.2). The directory-allowlist predicate, mirroring the discipline of
|
|
76
|
+
* {@link isSafeAgentRef}: a directory is safe to interpolate into a double-quoted `--add-dir "<dir>"`
|
|
77
|
+
* flag ONLY when it is an ABSOLUTE path, currently EXISTS on disk, and is FREE of the shell
|
|
78
|
+
* metacharacters that could break out of (or inject into) the `-lc` double-quoted argument — `"`, `$`,
|
|
79
|
+
* a backtick, or a newline/CR. Single source of truth — exported so engine-lifecycle.ts's resume path
|
|
80
|
+
* reuses it rather than re-implementing the rule.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isSafeDir(p: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Story 057 (R1.1/R1.2). Build the trailing ` --add-dir "<dir>"` fragment for a list of additional
|
|
85
|
+
* directories: ONE double-quoted flag per dir that passes {@link isSafeDir}. An unsafe dir (relative,
|
|
86
|
+
* non-existent, or containing a shell metacharacter) is DROPPED — never interpolated — and a one-line
|
|
87
|
+
* warning is logged to stderr (the fork's diagnostic channel, never the ACP stdout wire). An
|
|
88
|
+
* empty/undefined list emits the empty string (no flags). Shared by the fresh and resume spawn paths.
|
|
52
89
|
*/
|
|
53
|
-
export declare function
|
|
90
|
+
export declare function buildAddDirFlags(additionalDirectories?: string[]): string;
|
|
54
91
|
/**
|
|
55
92
|
* Login-shell argv (§5, R1.3). The `-lc` flag is MANDATORY: node-pty's `posix_spawnp`
|
|
56
93
|
* does not execute the `claude` npm launcher's `#!/usr/bin/env node` shebang, so the
|
|
57
94
|
* launcher must run *through* a login shell. (The compiled native-binary is the only
|
|
58
95
|
* thing that could be spawned directly — §5 / IMPLEMENTACAO-FORK-ACP.md §17.)
|
|
59
96
|
*/
|
|
60
|
-
export declare function buildSpawnArgv(sessionId: string, permissionMode?: string, settingsFile?: string, effortLevel?: string): [string, string];
|
|
97
|
+
export declare function buildSpawnArgv(sessionId: string, permissionMode?: string, settingsFile?: string, effortLevel?: string, agent?: string, additionalDirectories?: string[], mcpConfigFile?: string): [string, string];
|
|
61
98
|
/** Options for {@link spawnClaudePty}. */
|
|
62
99
|
export interface SpawnPtyOptions {
|
|
63
100
|
/** Host working directory the TUI runs in; passed straight through to the PTY (§5). */
|
|
64
101
|
cwd: string;
|
|
102
|
+
/**
|
|
103
|
+
* Story 056 v4 — an OPTIONAL pre-chosen session id for an in-place FRESH re-spawn (a selector change
|
|
104
|
+
* before the first interaction reuses the session's existing id). Absent → a new `randomUUID()` is
|
|
105
|
+
* generated (the normal createSession path). claude accepts a reused `--session-id` once the prior
|
|
106
|
+
* PTY for that id has exited (LIVE-VERIFIED).
|
|
107
|
+
*/
|
|
108
|
+
sessionId?: string;
|
|
65
109
|
/** Base environment to sanitize; defaults to the parent process env. */
|
|
66
110
|
baseEnv?: Record<string, string | undefined>;
|
|
67
111
|
/**
|
|
@@ -88,6 +132,30 @@ export interface SpawnPtyOptions {
|
|
|
88
132
|
* Absent → no flag (the ungated/`FORK_GATE=off` spawn is byte-for-byte pre-034).
|
|
89
133
|
*/
|
|
90
134
|
settingsFile?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Story 056 (R3.3): a main-thread agent persona name (from `agent-catalog.ts`) emitted as the
|
|
137
|
+
* interactive-only `--agent "<name>"` spawn flag. DOUBLE-QUOTED and injection-safe — re-asserted
|
|
138
|
+
* against the R3.3 allowlist (`/^[A-Za-z0-9_-]+$/`) so an unsafe name is dropped, never interpolated.
|
|
139
|
+
* Absent → no flag. Adds no `-p`/`stream-json`; billing stays subscription `cli`.
|
|
140
|
+
*/
|
|
141
|
+
agent?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Story 057 (R1.1/R1.2): extra workspace directories surfaced as ONE double-quoted
|
|
144
|
+
* `--add-dir "<dir>"` flag per dir. Each dir is re-asserted via {@link isSafeDir} (absolute +
|
|
145
|
+
* existing + no shell metacharacter) — an unsafe dir is dropped (logged), never interpolated.
|
|
146
|
+
* INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`, so billing stays subscription `cli`.
|
|
147
|
+
* Absent/empty → no flag.
|
|
148
|
+
*/
|
|
149
|
+
additionalDirectories?: string[];
|
|
150
|
+
/**
|
|
151
|
+
* Story 057 (R2.2): the fork-controlled uuid-named scratch MCP-config PATH (written by
|
|
152
|
+
* `mcp-config-writer.ts`), surfaced as the DOUBLE-QUOTED `--mcp-config "<file>"` flag. A file path —
|
|
153
|
+
* the secret-bearing JSON lives in the file, OFF the command line. NEVER paired with
|
|
154
|
+
* `--strict-mcp-config`, so claude MERGES it with the user's own `.mcp.json`/settings MCP servers
|
|
155
|
+
* (the R2.2 parity target) rather than discarding them. INTERACTIVE-ONLY: adds no
|
|
156
|
+
* `-p`/`--print`/`stream-json`, so billing stays subscription `cli`. Absent → no flag.
|
|
157
|
+
*/
|
|
158
|
+
mcpConfigFile?: string;
|
|
91
159
|
}
|
|
92
160
|
/** Handle returned by {@link spawnClaudePty}; story 014 manages its lifecycle. */
|
|
93
161
|
export interface PtyEngineHandle {
|
|
@@ -150,4 +218,3 @@ export declare const CLEAR_INPUT_DELAY_MS = 60;
|
|
|
150
218
|
* try/catch); the payload and `\r` writes are scheduled and post-exit-safe.
|
|
151
219
|
*/
|
|
152
220
|
export declare function sendPrompt(p: IPty, text: string, schedule?: (fn: () => void, ms: number) => void): void;
|
|
153
|
-
//# sourceMappingURL=engine-pty.d.ts.map
|
package/dist/engine-pty.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
// the ACP createSession rewrite that wires this engine in is story 023.
|
|
17
17
|
import pty from "node-pty";
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { isSafeAgentRef } from "./agent-catalog.js";
|
|
19
22
|
// PTY geometry pinned by §5.
|
|
20
23
|
const PTY_NAME = "xterm-256color";
|
|
21
24
|
const PTY_COLS = 120;
|
|
@@ -94,8 +97,28 @@ export function resolveShell(baseEnv = process.env) {
|
|
|
94
97
|
* non-"default" mode is emitted as `--permission-mode <mode>`; "default"/undefined emit nothing;
|
|
95
98
|
* `permissionMode: "plan"` reproduces the old planMode=true path. Still interactive-only — adds no
|
|
96
99
|
* `-p`/`stream-json`, billing stays subscription `cli`.
|
|
100
|
+
*
|
|
101
|
+
* Story 056 (R3.3): an optional `agent` persona name is emitted as `--agent "<name>"` (DOUBLE-QUOTED,
|
|
102
|
+
* grouped with the other behavior flags). This is the SECOND layer of the two-layer command-injection
|
|
103
|
+
* defense — `agent-catalog.ts` already drops any name outside `SAFE_AGENT_REF`
|
|
104
|
+
* (`/^[A-Za-z0-9_-]+(?::[A-Za-z0-9_-]+)?$/`, allowing a namespaced `plugin:name`) at discovery, and
|
|
105
|
+
* here we re-assert via {@link isSafeAgentRef} so an unsafe name is silently DROPPED (no flag), never
|
|
106
|
+
* interpolated into the `-lc` shell string. Interactive-only — adds no `-p`/`stream-json`.
|
|
107
|
+
*
|
|
108
|
+
* Story 057 (R1.1/R1.2): an optional `additionalDirectories` list is emitted as ONE double-quoted
|
|
109
|
+
* `--add-dir "<dir>"` flag PER directory (never a single space-joined value — each dir is its own flag,
|
|
110
|
+
* matching the upstream/CLI shape). Each dir is re-asserted via {@link isSafeDir} (absolute + existing +
|
|
111
|
+
* free of shell metacharacters); an unsafe dir is DROPPED and a one-line warning is logged, never
|
|
112
|
+
* interpolated into the `-lc` shell string. Interactive-only — adds no `-p`/`stream-json`.
|
|
113
|
+
*
|
|
114
|
+
* Story 057 (R2.2): an optional `mcpConfigFile` PATH is emitted as the DOUBLE-QUOTED `--mcp-config
|
|
115
|
+
* "<file>"` flag (same style as `--settings` — a file path, so the secret-bearing JSON stays OFF the
|
|
116
|
+
* command line and survives the `-lc` shell word-splitting). The file is the fork-controlled
|
|
117
|
+
* uuid-named scratch from `mcp-config-writer.ts`. R2.2 is a HARD rule: `--strict-mcp-config` is NEVER
|
|
118
|
+
* emitted, so claude MERGES the scratch with the user's own `.mcp.json`/settings MCP servers (the
|
|
119
|
+
* parity target) instead of discarding them. Interactive-only — adds no `-p`/`stream-json`.
|
|
97
120
|
*/
|
|
98
|
-
export function buildClaudeCmd(sessionId, permissionMode, settingsFile, effortLevel) {
|
|
121
|
+
export function buildClaudeCmd(sessionId, permissionMode, settingsFile, effortLevel, agent, additionalDirectories, mcpConfigFile) {
|
|
99
122
|
let cmd = `claude --session-id ${sessionId}`;
|
|
100
123
|
if (permissionMode && permissionMode !== "default")
|
|
101
124
|
cmd += ` --permission-mode ${permissionMode}`;
|
|
@@ -103,18 +126,69 @@ export function buildClaudeCmd(sessionId, permissionMode, settingsFile, effortLe
|
|
|
103
126
|
// non-"default" level is seeded/re-spawned with `--effort <level>`. Interactive-only — no credit path.
|
|
104
127
|
if (effortLevel && effortLevel !== "default")
|
|
105
128
|
cmd += ` --effort ${effortLevel}`;
|
|
129
|
+
// Story 056 (R3.3): the agent persona, double-quoted, emitted ONLY for a real persona — the "default"
|
|
130
|
+
// sentinel (= no persona, mirrors --effort/--permission-mode) emits nothing — and only when it passes
|
|
131
|
+
// the R3.3 allowlist re-assert (defense-in-depth — an unsafe name is dropped, never interpolated).
|
|
132
|
+
if (agent && agent !== "default" && isSafeAgentRef(agent))
|
|
133
|
+
cmd += ` --agent "${agent}"`;
|
|
106
134
|
if (settingsFile)
|
|
107
135
|
cmd += ` --settings "${settingsFile}"`;
|
|
136
|
+
// Story 057 (R1.1/R1.2): ONE double-quoted `--add-dir "<dir>"` per safe dir (never a space-joined
|
|
137
|
+
// value). Each dir is re-asserted via isSafeDir (absolute + existing + no shell metachar) — an unsafe
|
|
138
|
+
// dir is DROPPED with a one-line warning, never interpolated into the `-lc` shell string.
|
|
139
|
+
cmd += buildAddDirFlags(additionalDirectories);
|
|
140
|
+
// Story 057 (R2.2): the DOUBLE-QUOTED `--mcp-config "<file>"` (mirrors `--settings` above — a file path,
|
|
141
|
+
// so the secret-bearing JSON stays off the command line). Emitted ONLY when set. R2.2 HARD rule: NEVER
|
|
142
|
+
// `--strict-mcp-config` — without it claude MERGES the scratch with the user's own .mcp.json/settings
|
|
143
|
+
// MCP servers (the parity target) instead of discarding them.
|
|
144
|
+
if (mcpConfigFile)
|
|
145
|
+
cmd += ` --mcp-config "${mcpConfigFile}"`;
|
|
108
146
|
return cmd;
|
|
109
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Story 057 (R1.2). The directory-allowlist predicate, mirroring the discipline of
|
|
150
|
+
* {@link isSafeAgentRef}: a directory is safe to interpolate into a double-quoted `--add-dir "<dir>"`
|
|
151
|
+
* flag ONLY when it is an ABSOLUTE path, currently EXISTS on disk, and is FREE of the shell
|
|
152
|
+
* metacharacters that could break out of (or inject into) the `-lc` double-quoted argument — `"`, `$`,
|
|
153
|
+
* a backtick, or a newline/CR. Single source of truth — exported so engine-lifecycle.ts's resume path
|
|
154
|
+
* reuses it rather than re-implementing the rule.
|
|
155
|
+
*/
|
|
156
|
+
export function isSafeDir(p) {
|
|
157
|
+
return path.isAbsolute(p) && existsSync(p) && !/["$`\n\r]/.test(p);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Story 057 (R1.1/R1.2). Build the trailing ` --add-dir "<dir>"` fragment for a list of additional
|
|
161
|
+
* directories: ONE double-quoted flag per dir that passes {@link isSafeDir}. An unsafe dir (relative,
|
|
162
|
+
* non-existent, or containing a shell metacharacter) is DROPPED — never interpolated — and a one-line
|
|
163
|
+
* warning is logged to stderr (the fork's diagnostic channel, never the ACP stdout wire). An
|
|
164
|
+
* empty/undefined list emits the empty string (no flags). Shared by the fresh and resume spawn paths.
|
|
165
|
+
*/
|
|
166
|
+
export function buildAddDirFlags(additionalDirectories) {
|
|
167
|
+
if (!additionalDirectories || additionalDirectories.length === 0)
|
|
168
|
+
return "";
|
|
169
|
+
let fragment = "";
|
|
170
|
+
for (const dir of additionalDirectories) {
|
|
171
|
+
if (isSafeDir(dir)) {
|
|
172
|
+
fragment += ` --add-dir "${dir}"`;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.error(`engine-pty: dropping unsafe --add-dir entry (not absolute+existing, or contains a shell ` +
|
|
176
|
+
`metacharacter): ${JSON.stringify(dir)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return fragment;
|
|
180
|
+
}
|
|
110
181
|
/**
|
|
111
182
|
* Login-shell argv (§5, R1.3). The `-lc` flag is MANDATORY: node-pty's `posix_spawnp`
|
|
112
183
|
* does not execute the `claude` npm launcher's `#!/usr/bin/env node` shebang, so the
|
|
113
184
|
* launcher must run *through* a login shell. (The compiled native-binary is the only
|
|
114
185
|
* thing that could be spawned directly — §5 / IMPLEMENTACAO-FORK-ACP.md §17.)
|
|
115
186
|
*/
|
|
116
|
-
export function buildSpawnArgv(sessionId, permissionMode, settingsFile, effortLevel) {
|
|
117
|
-
return [
|
|
187
|
+
export function buildSpawnArgv(sessionId, permissionMode, settingsFile, effortLevel, agent, additionalDirectories, mcpConfigFile) {
|
|
188
|
+
return [
|
|
189
|
+
"-lc",
|
|
190
|
+
buildClaudeCmd(sessionId, permissionMode, settingsFile, effortLevel, agent, additionalDirectories, mcpConfigFile),
|
|
191
|
+
];
|
|
118
192
|
}
|
|
119
193
|
/**
|
|
120
194
|
* Spawn the interactive `claude` TUI under a real PTY with the §5 dimensions and a
|
|
@@ -133,10 +207,10 @@ export function buildSpawnArgv(sessionId, permissionMode, settingsFile, effortLe
|
|
|
133
207
|
* process-supervisor or non-PTY spawn variant is introduced.
|
|
134
208
|
*/
|
|
135
209
|
export function spawnClaudePty(opts) {
|
|
136
|
-
const { cwd, baseEnv = process.env, spawn = pty.spawn, permissionMode, settingsFile, effortLevel, } = opts;
|
|
137
|
-
const sessionId = randomUUID(); // pre-generated → correlates to the JSONL transcript
|
|
210
|
+
const { cwd, baseEnv = process.env, spawn = pty.spawn, permissionMode, settingsFile, effortLevel, agent, additionalDirectories, mcpConfigFile, } = opts;
|
|
211
|
+
const sessionId = opts.sessionId ?? randomUUID(); // pre-generated → correlates to the JSONL transcript (v4: reused on a fresh re-spawn)
|
|
138
212
|
const shell = resolveShell(baseEnv);
|
|
139
|
-
const argv = buildSpawnArgv(sessionId, permissionMode, settingsFile, effortLevel);
|
|
213
|
+
const argv = buildSpawnArgv(sessionId, permissionMode, settingsFile, effortLevel, agent, additionalDirectories, mcpConfigFile);
|
|
140
214
|
// Sanitized, subscription-billing env (§5/§10) via the single shared definition.
|
|
141
215
|
const env = buildSanitizedEnv(baseEnv);
|
|
142
216
|
// §10 no-spoof contract (R3.1/R3.3): the 'cli' entrypoint label MUST be produced
|
package/dist/engine-watcher.d.ts
CHANGED
|
@@ -80,4 +80,3 @@ export interface JsonlWatcher extends SessionWatcher {
|
|
|
80
80
|
* @returns a {@link JsonlWatcher} (a {@link SessionWatcher} plus `notifyEndOfTurn`).
|
|
81
81
|
*/
|
|
82
82
|
export declare function createJsonlWatcher(opts: JsonlWatcherOptions): JsonlWatcher;
|
|
83
|
-
//# sourceMappingURL=engine-watcher.d.ts.map
|
package/dist/engine.d.ts
CHANGED
package/dist/event-switch.d.ts
CHANGED
package/dist/gate/port.d.ts
CHANGED
|
@@ -144,4 +144,17 @@ export declare function injectHook(opts: {
|
|
|
144
144
|
* underlying error so teardown never silently strands the fork hook.
|
|
145
145
|
*/
|
|
146
146
|
export declare function restore(backup: Backup): Promise<RestoreResult>;
|
|
147
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Story 060 (R2.2/R2.3/R3.2) — toggle the ultracode keys in the per-session SCRATCH settings file the
|
|
149
|
+
* gate wrote (preserving the hook + EVERY other key). `active` → set `ultracode:true` (the per-session
|
|
150
|
+
* ACTIVATOR) + `ultracodeKeywordTrigger:true` (defensively ENABLE the keyword even if the user disabled
|
|
151
|
+
* it). `!active` → remove BOTH keys. Read tolerantly (JSONC) + written durably (temp+fsync+rename),
|
|
152
|
+
* mirroring {@link injectHook}. A missing/empty file is treated as an empty object. No-op-safe and
|
|
153
|
+
* idempotent: re-applying the same `active` produces a byte-equivalent document.
|
|
154
|
+
*
|
|
155
|
+
* @param settingsPath absolute path to the per-session scratch `settings.local.json` (the gate's file).
|
|
156
|
+
* @param active whether ultracode is selected (set the keys) or de-selected (remove them).
|
|
157
|
+
* @throws {Error} only on a genuine read/parse/write failure — NEVER on the benign absence of the file
|
|
158
|
+
* (ENOENT → treated as `{}`), mirroring the rest of the module's discipline.
|
|
159
|
+
*/
|
|
160
|
+
export declare function applyUltracodeSettings(settingsPath: string, active: boolean): Promise<void>;
|
|
@@ -353,3 +353,52 @@ export async function restore(backup) {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
|
+
// Story 060 (R2.2 / R2.3 / R3.2) — toggle the ultracode keys in the per-session SCRATCH settings file.
|
|
358
|
+
//
|
|
359
|
+
// The "ultracode" effort-selector sentinel (story 060) activates the binary's Workflow keyword-trigger.
|
|
360
|
+
// Its LIVE activation is a keyword prefix on the outgoing prompt (acp-agent.ts); THIS function is the
|
|
361
|
+
// declarative spawn-time complement — the keys `claude` reads only at (re-)spawn, so the scratch stays
|
|
362
|
+
// in sync for any (re-)spawn that DOES happen. It is NOT itself a re-spawn trigger (Option A).
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
364
|
+
/** The two settings keys that opt a session into the ultracode keyword-trigger (story 060). */
|
|
365
|
+
const ULTRACODE_SETTINGS_KEY = "ultracode";
|
|
366
|
+
const ULTRACODE_KEYWORD_TRIGGER_KEY = "ultracodeKeywordTrigger";
|
|
367
|
+
/**
|
|
368
|
+
* Story 060 (R2.2/R2.3/R3.2) — toggle the ultracode keys in the per-session SCRATCH settings file the
|
|
369
|
+
* gate wrote (preserving the hook + EVERY other key). `active` → set `ultracode:true` (the per-session
|
|
370
|
+
* ACTIVATOR) + `ultracodeKeywordTrigger:true` (defensively ENABLE the keyword even if the user disabled
|
|
371
|
+
* it). `!active` → remove BOTH keys. Read tolerantly (JSONC) + written durably (temp+fsync+rename),
|
|
372
|
+
* mirroring {@link injectHook}. A missing/empty file is treated as an empty object. No-op-safe and
|
|
373
|
+
* idempotent: re-applying the same `active` produces a byte-equivalent document.
|
|
374
|
+
*
|
|
375
|
+
* @param settingsPath absolute path to the per-session scratch `settings.local.json` (the gate's file).
|
|
376
|
+
* @param active whether ultracode is selected (set the keys) or de-selected (remove them).
|
|
377
|
+
* @throws {Error} only on a genuine read/parse/write failure — NEVER on the benign absence of the file
|
|
378
|
+
* (ENOENT → treated as `{}`), mirroring the rest of the module's discipline.
|
|
379
|
+
*/
|
|
380
|
+
export async function applyUltracodeSettings(settingsPath, active) {
|
|
381
|
+
// 1) Read the current bytes; a missing file (ENOENT) is treated as "no prior settings" (→ `{}`).
|
|
382
|
+
const priorBytes = await readPriorBytes(settingsPath);
|
|
383
|
+
let current = {};
|
|
384
|
+
if (priorBytes !== null) {
|
|
385
|
+
const text = priorBytes.toString("utf8").trim();
|
|
386
|
+
// An empty existing file is treated as an empty object rather than a parse error (mirrors inject).
|
|
387
|
+
if (text.length > 0) {
|
|
388
|
+
current = parsePriorSettings(text);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// 2) Clone (never mutate the parsed prior) and toggle ONLY the two ultracode keys — every other key
|
|
392
|
+
// (the gate hook, the user's own config) is preserved verbatim.
|
|
393
|
+
const next = structuredClone(current);
|
|
394
|
+
if (active) {
|
|
395
|
+
next[ULTRACODE_SETTINGS_KEY] = true;
|
|
396
|
+
next[ULTRACODE_KEYWORD_TRIGGER_KEY] = true;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
delete next[ULTRACODE_SETTINGS_KEY];
|
|
400
|
+
delete next[ULTRACODE_KEYWORD_TRIGGER_KEY];
|
|
401
|
+
}
|
|
402
|
+
// 3) Durably write (temp+fsync+rename) so a concurrent (re-)spawn read never sees a half-written file.
|
|
403
|
+
await durableWrite(settingsPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
404
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Filename prefix for every materialized image — shared so a later cleanup task can match it. */
|
|
2
|
+
export declare const MATERIALIZED_IMAGE_PREFIX = "fork-acp-img-";
|
|
3
|
+
/**
|
|
4
|
+
* Map an ACP `image` block `mimeType` to a file extension the TUI's Read tool recognizes.
|
|
5
|
+
*
|
|
6
|
+
* Tolerant of a `;`-parameterized mimeType (e.g. `image/png; charset=binary`): only the media type
|
|
7
|
+
* before the first `;` is matched. Anything unrecognized — including `undefined`/`null` — falls back
|
|
8
|
+
* to `.img` so a path is still produced (the Read tool sniffs the bytes regardless of extension).
|
|
9
|
+
* Never throws.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extFor(mimeType: string | undefined | null): string;
|
|
12
|
+
/**
|
|
13
|
+
* Decode a base64 `image` payload to a uniquely-named temp file and return its absolute path.
|
|
14
|
+
*
|
|
15
|
+
* The file lives under {@link os.tmpdir} with a uuid name (no user-controlled characters) and an
|
|
16
|
+
* extension derived from `mimeType` via {@link extFor}. The write is SYNCHRONOUS on purpose — the
|
|
17
|
+
* caller (`promptToClaude`) is sync. May throw if the decode or write fails; the caller isolates that
|
|
18
|
+
* in its per-block try/catch (R1.3), so a bad image is skipped rather than aborting the whole prompt.
|
|
19
|
+
*/
|
|
20
|
+
export declare function materializeImage(data: string, mimeType: string | undefined | null): string;
|
|
21
|
+
/**
|
|
22
|
+
* Story 058 / Task 2.1 (R2.1/R2.2) — best-effort unlink of every materialized temp image.
|
|
23
|
+
*
|
|
24
|
+
* The single unlink path shared by the turn-settle cleanup (prompt()'s catch + finally) and the
|
|
25
|
+
* session-teardown backstop, kept here so it stays OFFLINE-testable. NEVER throws: each unlink is
|
|
26
|
+
* isolated so a missing file (already removed, or a torn-down/raced turn) is a no-op rather than an
|
|
27
|
+
* error — which makes the teardown backstop idempotent with the prompt-finally cleanup. `undefined`
|
|
28
|
+
* (no image materialized this turn) returns immediately.
|
|
29
|
+
*/
|
|
30
|
+
export declare function cleanupMaterializedImages(paths: readonly string[] | undefined): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Story 058 / Task 1.1 — materialize an ACP `image` block to a temp file for the @path vision path.
|
|
2
|
+
//
|
|
3
|
+
// The fork drives the real `claude` TUI over a PTY; it has no API surface to push raw image bytes.
|
|
4
|
+
// The proven claude-2.1.195 path to get an image vision-encoded is to write the bytes to a file and
|
|
5
|
+
// reference it with `@<path>` in the prompt text, then ask the model to Read it — the TUI's Read tool
|
|
6
|
+
// then vision-encodes the file. This module is the standalone, OFFLINE-testable materialize seam:
|
|
7
|
+
//
|
|
8
|
+
// extFor(mimeType) → ".png" | ".jpg" | ".gif" | ".webp" | ".img" (PURE — mime → ext)
|
|
9
|
+
// materializeImage(data, mt) → <abs temp path> (DURABLE — base64 → temp file, returns the path)
|
|
10
|
+
//
|
|
11
|
+
// `promptToClaude` (acp-agent.ts) is SYNCHRONOUS and runs before the `await` at its call site, so the
|
|
12
|
+
// write here is synchronous (`writeFileSync`) by design — do NOT make it async. The temp path is
|
|
13
|
+
// uuid-named and fork-controlled (no user input in the filename), which is what lets a later cleanup
|
|
14
|
+
// task (Task 2) treat it as shell-safe. The decoded bytes are not logged.
|
|
15
|
+
import { writeFileSync, unlinkSync } from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
/** Filename prefix for every materialized image — shared so a later cleanup task can match it. */
|
|
20
|
+
export const MATERIALIZED_IMAGE_PREFIX = "fork-acp-img-";
|
|
21
|
+
/**
|
|
22
|
+
* Map an ACP `image` block `mimeType` to a file extension the TUI's Read tool recognizes.
|
|
23
|
+
*
|
|
24
|
+
* Tolerant of a `;`-parameterized mimeType (e.g. `image/png; charset=binary`): only the media type
|
|
25
|
+
* before the first `;` is matched. Anything unrecognized — including `undefined`/`null` — falls back
|
|
26
|
+
* to `.img` so a path is still produced (the Read tool sniffs the bytes regardless of extension).
|
|
27
|
+
* Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export function extFor(mimeType) {
|
|
30
|
+
// Strip any `;`-delimited parameters and normalize, so `image/JPEG; q=…` still matches.
|
|
31
|
+
const media = (mimeType ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
32
|
+
switch (media) {
|
|
33
|
+
case "image/png":
|
|
34
|
+
return ".png";
|
|
35
|
+
case "image/jpeg":
|
|
36
|
+
return ".jpg";
|
|
37
|
+
case "image/gif":
|
|
38
|
+
return ".gif";
|
|
39
|
+
case "image/webp":
|
|
40
|
+
return ".webp";
|
|
41
|
+
default:
|
|
42
|
+
return ".img";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decode a base64 `image` payload to a uniquely-named temp file and return its absolute path.
|
|
47
|
+
*
|
|
48
|
+
* The file lives under {@link os.tmpdir} with a uuid name (no user-controlled characters) and an
|
|
49
|
+
* extension derived from `mimeType` via {@link extFor}. The write is SYNCHRONOUS on purpose — the
|
|
50
|
+
* caller (`promptToClaude`) is sync. May throw if the decode or write fails; the caller isolates that
|
|
51
|
+
* in its per-block try/catch (R1.3), so a bad image is skipped rather than aborting the whole prompt.
|
|
52
|
+
*/
|
|
53
|
+
export function materializeImage(data, mimeType) {
|
|
54
|
+
const bytes = Buffer.from(data, "base64");
|
|
55
|
+
const tempPath = path.join(os.tmpdir(), MATERIALIZED_IMAGE_PREFIX + randomUUID() + extFor(mimeType));
|
|
56
|
+
writeFileSync(tempPath, bytes);
|
|
57
|
+
return tempPath;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Story 058 / Task 2.1 (R2.1/R2.2) — best-effort unlink of every materialized temp image.
|
|
61
|
+
*
|
|
62
|
+
* The single unlink path shared by the turn-settle cleanup (prompt()'s catch + finally) and the
|
|
63
|
+
* session-teardown backstop, kept here so it stays OFFLINE-testable. NEVER throws: each unlink is
|
|
64
|
+
* isolated so a missing file (already removed, or a torn-down/raced turn) is a no-op rather than an
|
|
65
|
+
* error — which makes the teardown backstop idempotent with the prompt-finally cleanup. `undefined`
|
|
66
|
+
* (no image materialized this turn) returns immediately.
|
|
67
|
+
*/
|
|
68
|
+
export function cleanupMaterializedImages(paths) {
|
|
69
|
+
if (!paths)
|
|
70
|
+
return;
|
|
71
|
+
for (const p of paths) {
|
|
72
|
+
try {
|
|
73
|
+
unlinkSync(p);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* best-effort: the file is already gone, or a raced teardown removed it first */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The claude version where `@<path>` → Read-vision was PROVEN to work (story 058 research).
|
|
3
|
+
*
|
|
4
|
+
* WHY this exact anchor: on claude 2.1.170 a `@path` image prompt did NOT vision-encode the picture —
|
|
5
|
+
* the TUI did not turn it into a Read the model could see (story 030). On claude 2.1.195 it DOES: the
|
|
6
|
+
* two-arm proof (a blue vs a green image under the same uuid filename, each described correctly) showed
|
|
7
|
+
* `@path` → Read vision-encodes. So 2.1.195 is the earliest version we are willing to call "confirmed";
|
|
8
|
+
* anything below is treated as unconfirmed and warned about.
|
|
9
|
+
*/
|
|
10
|
+
export declare const VISION_CONFIRMED_CLAUDE_VERSION = "2.1.195";
|
|
11
|
+
/** The verdict {@link imageVisionSmoke} returns. Observability only — there is NO capability toggle here. */
|
|
12
|
+
export interface VisionSmokeResult {
|
|
13
|
+
/** True iff the running `claude` version is one we have CONFIRMED vision-encodes `@image` via Read. */
|
|
14
|
+
confirmed: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Human-readable one-liner. On `confirmed=true` it is a positive note. On `confirmed=false` it is the
|
|
17
|
+
* fail-loud WARNING text and NAMES the detected version (or "undetected") plus the confirmed anchor
|
|
18
|
+
* (mirrors drift-checks.ts's "name the offending value" habit).
|
|
19
|
+
*/
|
|
20
|
+
detail: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compare two `x.y.z` semver strings: `true` iff `a >= b` componentwise (major, then minor, then patch).
|
|
24
|
+
*
|
|
25
|
+
* Splits on `.`, parses the three leading numeric components; a missing/non-numeric component counts as
|
|
26
|
+
* `0` (so `"2.1"` reads as `2.1.0`). Private to this module — exported only so the unit test can pin the
|
|
27
|
+
* ordering directly. Pure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function versionGte(a: string, b: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* PURE verdict: is the running `claude` `version` one we have CONFIRMED vision-encodes `@image` via Read?
|
|
32
|
+
*
|
|
33
|
+
* `confirmed: true` IFF `version` is a parseable leading `x.y.z` that is `>= {@link
|
|
34
|
+
* VISION_CONFIRMED_CLAUDE_VERSION}`. In that case `detail` is a positive one-liner.
|
|
35
|
+
*
|
|
36
|
+
* Otherwise (`null` / `undefined` / unparseable, OR a parseable version BELOW the anchor) `confirmed:
|
|
37
|
+
* false` and `detail` is the fail-loud WARNING text — it NAMES the detected version (or "undetected")
|
|
38
|
+
* and the confirmed anchor, and states that the image is NOT blocked (R3.1). No I/O; depends only on
|
|
39
|
+
* its argument.
|
|
40
|
+
*/
|
|
41
|
+
export declare function imageVisionSmoke(version: string | null | undefined): VisionSmokeResult;
|
|
42
|
+
/**
|
|
43
|
+
* Best-effort fail-loud reporter: compute {@link imageVisionSmoke} and, WHEN unconfirmed, log the
|
|
44
|
+
* verdict's `detail` EXACTLY ONCE via `log` with an `[image-vision]` prefix (mirrors claude-path.ts's
|
|
45
|
+
* reportVersionDrift `[claude-path] …` style). Returns the verdict.
|
|
46
|
+
*
|
|
47
|
+
* Logs NOTHING on a confirmed version (a confirmed `claude` is the expected case — no news is good news,
|
|
48
|
+
* matching reportVersionDrift which is silent when there is no drift). NEVER blocks and NEVER throws out
|
|
49
|
+
* of the caller's intent — it is pure aside from the single conditional `log` call (R3.1, R3.2).
|
|
50
|
+
*/
|
|
51
|
+
export declare function reportImageVisionSmoke(version: string | null | undefined, log?: (...args: unknown[]) => void): VisionSmokeResult;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// === Story 058 / Task 3.1 (R3.1, R3.2) — version-aware image-vision smoke (pure verdict + fail-loud) ==
|
|
2
|
+
//
|
|
3
|
+
// The whole "image input via @path" feature rests on a VERSION-FRAGILE claude behavior: a prompt
|
|
4
|
+
// containing `@<path>` makes the native TUI invoke the Read tool, and Read vision-encodes the image so
|
|
5
|
+
// the model can see it. This was PROVEN on claude 2.1.195 (story 058 research — two-arm proof: a blue
|
|
6
|
+
// image vs a green image under the same filename, each correctly described). It did NOT work on claude
|
|
7
|
+
// 2.1.170 (story 030): there, `@path` did not vision-encode, so an image prompt silently dropped its
|
|
8
|
+
// picture.
|
|
9
|
+
//
|
|
10
|
+
// Because the behavior is version-fragile, this module makes a too-old / unknown `claude` LOUD instead
|
|
11
|
+
// of silently dropping the image. It mirrors story 049's drift discipline:
|
|
12
|
+
// - the VERDICT is PURE (no I/O — like src/drift-checks.ts's `{surface, ok, detail}` checks), and
|
|
13
|
+
// - the REPORTER is best-effort + fail-loud one-liner (like src/claude-path.ts's reportVersionDrift,
|
|
14
|
+
// `[claude-path] …`); here the prefix is `[image-vision]`.
|
|
15
|
+
//
|
|
16
|
+
// CRITICAL — OBSERVABILITY ONLY, NEVER A GATE (R3.1): this smoke NEVER blocks the prompt and NEVER
|
|
17
|
+
// touches `promptCapabilities.image`, which stays literally `image: true` (acp-agent.ts:1413),
|
|
18
|
+
// UNCONDITIONALLY. The verdict's only output is `{confirmed, detail}` — there is deliberately NO field
|
|
19
|
+
// or return shape here that could disable a capability. A too-old `claude` produces a WARNING, not a
|
|
20
|
+
// degraded capability: the image still rides the @path delivery; we just warn that this `claude` is not
|
|
21
|
+
// a version we have confirmed vision-encodes it.
|
|
22
|
+
//
|
|
23
|
+
// LIMITATION (documented honestly): this is an OFFLINE `>= anchor` version-gate. It catches too-old and
|
|
24
|
+
// undetected/unparseable versions, but it CANNOT catch a *future regression* — a hypothetical 2.1.300
|
|
25
|
+
// that re-breaks @path-vision would still parse as `>= 2.1.195` and report confirmed. Catching a future
|
|
26
|
+
// regression needs the LIVE smoke (opt-in — story 058 R4 probe: actually send @image and check the
|
|
27
|
+
// model saw it), which this offline gate intentionally does NOT implement.
|
|
28
|
+
//
|
|
29
|
+
// node:test runner: `node --experimental-strip-types --test test/image-vision-smoke.test.ts`
|
|
30
|
+
/**
|
|
31
|
+
* The claude version where `@<path>` → Read-vision was PROVEN to work (story 058 research).
|
|
32
|
+
*
|
|
33
|
+
* WHY this exact anchor: on claude 2.1.170 a `@path` image prompt did NOT vision-encode the picture —
|
|
34
|
+
* the TUI did not turn it into a Read the model could see (story 030). On claude 2.1.195 it DOES: the
|
|
35
|
+
* two-arm proof (a blue vs a green image under the same uuid filename, each described correctly) showed
|
|
36
|
+
* `@path` → Read vision-encodes. So 2.1.195 is the earliest version we are willing to call "confirmed";
|
|
37
|
+
* anything below is treated as unconfirmed and warned about.
|
|
38
|
+
*/
|
|
39
|
+
export const VISION_CONFIRMED_CLAUDE_VERSION = "2.1.195";
|
|
40
|
+
/**
|
|
41
|
+
* Compare two `x.y.z` semver strings: `true` iff `a >= b` componentwise (major, then minor, then patch).
|
|
42
|
+
*
|
|
43
|
+
* Splits on `.`, parses the three leading numeric components; a missing/non-numeric component counts as
|
|
44
|
+
* `0` (so `"2.1"` reads as `2.1.0`). Private to this module — exported only so the unit test can pin the
|
|
45
|
+
* ordering directly. Pure.
|
|
46
|
+
*/
|
|
47
|
+
export function versionGte(a, b) {
|
|
48
|
+
const partsA = a.split(".");
|
|
49
|
+
const partsB = b.split(".");
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
const na = toComponent(partsA[i]);
|
|
52
|
+
const nb = toComponent(partsB[i]);
|
|
53
|
+
if (na !== nb) {
|
|
54
|
+
return na > nb;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true; // all three components equal → a >= b holds
|
|
58
|
+
}
|
|
59
|
+
/** Parse one semver component to a non-negative integer; missing / non-numeric → 0. */
|
|
60
|
+
function toComponent(part) {
|
|
61
|
+
if (part === undefined) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const n = Number.parseInt(part, 10);
|
|
65
|
+
return Number.isNaN(n) ? 0 : n;
|
|
66
|
+
}
|
|
67
|
+
/** Matches a leading `x.y.z` triple — same shape parseClaudeVersion extracts (claude-path.ts:58). */
|
|
68
|
+
const SEMVER_RE = /^(\d+\.\d+\.\d+)/;
|
|
69
|
+
/**
|
|
70
|
+
* PURE verdict: is the running `claude` `version` one we have CONFIRMED vision-encodes `@image` via Read?
|
|
71
|
+
*
|
|
72
|
+
* `confirmed: true` IFF `version` is a parseable leading `x.y.z` that is `>= {@link
|
|
73
|
+
* VISION_CONFIRMED_CLAUDE_VERSION}`. In that case `detail` is a positive one-liner.
|
|
74
|
+
*
|
|
75
|
+
* Otherwise (`null` / `undefined` / unparseable, OR a parseable version BELOW the anchor) `confirmed:
|
|
76
|
+
* false` and `detail` is the fail-loud WARNING text — it NAMES the detected version (or "undetected")
|
|
77
|
+
* and the confirmed anchor, and states that the image is NOT blocked (R3.1). No I/O; depends only on
|
|
78
|
+
* its argument.
|
|
79
|
+
*/
|
|
80
|
+
export function imageVisionSmoke(version) {
|
|
81
|
+
const parsed = typeof version === "string" ? SEMVER_RE.exec(version) : null;
|
|
82
|
+
if (parsed && versionGte(parsed[1], VISION_CONFIRMED_CLAUDE_VERSION)) {
|
|
83
|
+
return {
|
|
84
|
+
confirmed: true,
|
|
85
|
+
detail: `claude ${parsed[1]} >= ${VISION_CONFIRMED_CLAUDE_VERSION} — @image is confirmed to vision-encode via Read`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const named = parsed ? parsed[1] : "undetected";
|
|
89
|
+
return {
|
|
90
|
+
confirmed: false,
|
|
91
|
+
detail: `@image vision-encoding is UNCONFIRMED on this claude (detected ${named}; confirmed at ` +
|
|
92
|
+
`${VISION_CONFIRMED_CLAUDE_VERSION}+). Sending images anyway — image input is NOT blocked — but ` +
|
|
93
|
+
`if the picture is ignored, upgrade claude to ${VISION_CONFIRMED_CLAUDE_VERSION} or newer`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Best-effort fail-loud reporter: compute {@link imageVisionSmoke} and, WHEN unconfirmed, log the
|
|
98
|
+
* verdict's `detail` EXACTLY ONCE via `log` with an `[image-vision]` prefix (mirrors claude-path.ts's
|
|
99
|
+
* reportVersionDrift `[claude-path] …` style). Returns the verdict.
|
|
100
|
+
*
|
|
101
|
+
* Logs NOTHING on a confirmed version (a confirmed `claude` is the expected case — no news is good news,
|
|
102
|
+
* matching reportVersionDrift which is silent when there is no drift). NEVER blocks and NEVER throws out
|
|
103
|
+
* of the caller's intent — it is pure aside from the single conditional `log` call (R3.1, R3.2).
|
|
104
|
+
*/
|
|
105
|
+
export function reportImageVisionSmoke(version, log = console.error) {
|
|
106
|
+
const result = imageVisionSmoke(version);
|
|
107
|
+
if (!result.confirmed) {
|
|
108
|
+
log(`[image-vision] ${result.detail}`);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -88,6 +88,12 @@ else {
|
|
|
88
88
|
// (src/live-subagent-env.ts) so the truth table is unit-checkable; threaded through
|
|
89
89
|
// runAcp → AgentDeps → the agent.
|
|
90
90
|
const liveSubagentWatch = liveSubagentWatchEnabled();
|
|
91
|
+
// Story 056 / Task 7 (R3.5): enable the live agent-discovery probe in PRODUCTION only. The probe
|
|
92
|
+
// (agent-catalog.ts) spawns `claude --agent <sentinel>` once per createSession to read `claude`'s
|
|
93
|
+
// canonical persona list (enabled plugins + built-ins) for the 4th `agent` selector. Gated opt-in so
|
|
94
|
+
// the unit suite — which imports modules directly without this flag — stays on the hermetic glob
|
|
95
|
+
// fallback and never spawns the real binary. `??=` keeps a user opt-out (`FORK_AGENT_PROBE=0`).
|
|
96
|
+
process.env.FORK_AGENT_PROBE ??= "1";
|
|
91
97
|
const { connection, agent } = runAcp({ usageUpdate, gate, liveDiff, liveSubagentWatch });
|
|
92
98
|
async function shutdown() {
|
|
93
99
|
await agent.dispose().catch((err) => {
|
package/dist/jsonl.d.ts
CHANGED
|
@@ -264,4 +264,3 @@ export declare function stripHeavyImages(message: unknown, imageSkipBytes?: numb
|
|
|
264
264
|
* @returns the typed {@link JsonlEvent}.
|
|
265
265
|
*/
|
|
266
266
|
export declare function projectEvent(message: unknown, opts?: ProjectOptions): JsonlEvent;
|
|
267
|
-
//# sourceMappingURL=jsonl.d.ts.map
|
package/dist/lib.d.ts
CHANGED
|
@@ -3,4 +3,3 @@ export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./u
|
|
|
3
3
|
export { toolInfoFromToolUse, toDisplayPath, planEntries, toolUpdateFromToolResult, } from "./tools.js";
|
|
4
4
|
export { SettingsManager, type SettingsManagerOptions } from "./settings.js";
|
|
5
5
|
export type { ClaudePlanEntry } from "./tools.js";
|
|
6
|
-
//# sourceMappingURL=lib.d.ts.map
|