open-claude-p 1.1.3 → 1.2.2
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/CHANGELOG.md +342 -0
- package/README.ja.md +5 -5
- package/README.ko.md +5 -5
- package/README.md +10 -5
- package/README.zh.md +5 -5
- package/bin/cli.js +117 -55
- package/package.json +1 -1
- package/src/chat/index.js +88 -3
- package/src/index.js +97 -12
- package/src/options/forward.js +70 -0
- package/src/options/index.js +1 -0
- package/src/options/parse-argv.js +73 -2
- package/src/options/spec.js +146 -6
- package/src/options/validate.js +41 -1
- package/src/parsers/ansi-strip.js +149 -42
package/src/options/index.js
CHANGED
|
@@ -26,15 +26,20 @@ import { OPTION_SPEC, getOption } from './spec.js';
|
|
|
26
26
|
* @property {string[]} positional
|
|
27
27
|
* @property {string[]} unknown Tokens that look like flags but were not in the spec.
|
|
28
28
|
* @property {string[]} errors Human-readable error strings.
|
|
29
|
+
* @property {Set<string>} supplied Canonical names actually present in argv.
|
|
30
|
+
* Defaults are pre-seeded into `options`, so a value alone cannot tell
|
|
31
|
+
* "user passed `--flag=false`" apart from "user passed nothing" — this
|
|
32
|
+
* set is the only way to distinguish them.
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* @param {string[]} argv
|
|
37
|
+
* @param {Record<string, string|undefined>} [env] defaults to `process.env`
|
|
33
38
|
* @returns {ParseResult}
|
|
34
39
|
*/
|
|
35
|
-
export function parseArgv(argv) {
|
|
40
|
+
export function parseArgv(argv, env = process.env) {
|
|
36
41
|
/** @type {ParseResult} */
|
|
37
|
-
const out = { options: {}, positional: [], unknown: [], errors: [] };
|
|
42
|
+
const out = { options: {}, positional: [], unknown: [], errors: [], supplied: new Set() };
|
|
38
43
|
|
|
39
44
|
// Pre-seed defaults so the validator sees them.
|
|
40
45
|
for (const spec of OPTION_SPEC) {
|
|
@@ -43,6 +48,14 @@ export function parseArgv(argv) {
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
// Canonical names supplied on the command line. Used only by the
|
|
52
|
+
// env-var pass at the end: argv always wins over the environment, and
|
|
53
|
+
// that has to include an explicit opt-out like `--print-mode=false`,
|
|
54
|
+
// whose value is indistinguishable from the default if you compare
|
|
55
|
+
// values instead of tracking what was actually seen.
|
|
56
|
+
/** @type {Set<string>} */
|
|
57
|
+
const supplied = out.supplied;
|
|
58
|
+
|
|
46
59
|
let i = 0;
|
|
47
60
|
while (i < argv.length) {
|
|
48
61
|
const tok = argv[i];
|
|
@@ -63,6 +76,7 @@ export function parseArgv(argv) {
|
|
|
63
76
|
i++;
|
|
64
77
|
continue;
|
|
65
78
|
}
|
|
79
|
+
supplied.add(spec.name);
|
|
66
80
|
i = consumeOption(spec, argv, i, inline, out);
|
|
67
81
|
continue;
|
|
68
82
|
}
|
|
@@ -93,11 +107,13 @@ export function parseArgv(argv) {
|
|
|
93
107
|
);
|
|
94
108
|
break;
|
|
95
109
|
}
|
|
110
|
+
supplied.add(spec.name);
|
|
96
111
|
i = consumeOption(spec, argv, i, undefined, out);
|
|
97
112
|
consumedByValue = true;
|
|
98
113
|
break;
|
|
99
114
|
}
|
|
100
115
|
// Boolean short: just set.
|
|
116
|
+
supplied.add(spec.name);
|
|
101
117
|
out.options[spec.name] = true;
|
|
102
118
|
}
|
|
103
119
|
if (!consumedByValue) i++;
|
|
@@ -109,9 +125,64 @@ export function parseArgv(argv) {
|
|
|
109
125
|
i++;
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
applyEnvFallbacks(out, supplied, env);
|
|
129
|
+
|
|
112
130
|
return out;
|
|
113
131
|
}
|
|
114
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Fill in options that were not supplied on the command line from their
|
|
135
|
+
* declared `env:` variable.
|
|
136
|
+
*
|
|
137
|
+
* OPTION_SPEC has always documented an `env` field, but nothing read it —
|
|
138
|
+
* the two entries that used it (`--print-mode`, `--no-session-persistence`)
|
|
139
|
+
* were wired up by hand at their call sites instead, so any new entry that
|
|
140
|
+
* declared `env` silently did nothing. This makes the declaration real, and
|
|
141
|
+
* keeps the precedence the hand-rolled versions already used:
|
|
142
|
+
*
|
|
143
|
+
* explicit argv > environment variable > spec default
|
|
144
|
+
*
|
|
145
|
+
* Booleans accept `1` / `true` / `yes` / `on` (case-insensitive); anything
|
|
146
|
+
* else, including the empty string, leaves the option at its default so
|
|
147
|
+
* `VAR=` and `VAR=0` both read as "off". Value-taking options are coerced
|
|
148
|
+
* through the same `coerce()` path as argv, and a malformed value produces
|
|
149
|
+
* the same style of error rather than being silently ignored.
|
|
150
|
+
*
|
|
151
|
+
* @param {ParseResult} out
|
|
152
|
+
* @param {Set<string>} supplied canonical names seen on the command line
|
|
153
|
+
* @param {Record<string, string|undefined>} env
|
|
154
|
+
*/
|
|
155
|
+
function applyEnvFallbacks(out, supplied, env) {
|
|
156
|
+
for (const spec of OPTION_SPEC) {
|
|
157
|
+
if (!spec.env) continue;
|
|
158
|
+
const raw = Object.prototype.hasOwnProperty.call(env ?? {}, spec.env)
|
|
159
|
+
? env[spec.env]
|
|
160
|
+
: undefined;
|
|
161
|
+
// Library callers can pass any object as `env`; only honour real
|
|
162
|
+
// string values so a stray number/object cannot throw in `.trim()`.
|
|
163
|
+
if (typeof raw !== 'string' || raw === '') continue;
|
|
164
|
+
// Supplied on the command line — argv always wins.
|
|
165
|
+
if (supplied.has(spec.name)) continue;
|
|
166
|
+
|
|
167
|
+
if (spec.kind === 'boolean') {
|
|
168
|
+
const v = raw.trim().toLowerCase();
|
|
169
|
+
if (v === '1' || v === 'true' || v === 'yes' || v === 'on') {
|
|
170
|
+
out.options[spec.name] = true;
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (spec.kind === 'array') {
|
|
175
|
+
// Comma-separated, so a single env var can carry a repeatable flag.
|
|
176
|
+
const items = raw.split(',').map((x) => x.trim()).filter(Boolean);
|
|
177
|
+
if (items.length > 0) out.options[spec.name] = items;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const { value, error } = coerce(spec, raw);
|
|
181
|
+
if (error) out.errors.push(`${spec.env}: ${error}`);
|
|
182
|
+
else out.options[spec.name] = value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
115
186
|
/**
|
|
116
187
|
* Consume the value(s) for `spec` starting at argv index `i` (where argv[i]
|
|
117
188
|
* is the flag token itself). `inline` is the post-`=` value when present.
|
package/src/options/spec.js
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
// times (occurrences accumulate); array kind also collects
|
|
18
18
|
// variadic values until the next flag-looking token.
|
|
19
19
|
// env: Optional environment-variable fallback.
|
|
20
|
+
// printModeOnly: Only forward this flag when running under
|
|
21
|
+
// `--print-mode`. For upstream flags that require
|
|
22
|
+
// `--print` and abort the process without it.
|
|
20
23
|
// description: One-line help text.
|
|
21
24
|
// forward: How this option reaches the upstream `claude` process.
|
|
22
25
|
// { type: 'argv', flag: '--name' } pass to spawn argv
|
|
@@ -72,7 +75,11 @@ export const OPTION_SPEC = [
|
|
|
72
75
|
{
|
|
73
76
|
name: 'model',
|
|
74
77
|
kind: 'string',
|
|
75
|
-
description:
|
|
78
|
+
description:
|
|
79
|
+
'Model alias or full name. Aliases resolve to the latest model in ' +
|
|
80
|
+
'that family (e.g. `fable`, `opus`, `sonnet`, `haiku`); full names ' +
|
|
81
|
+
'are passed through verbatim (e.g. `claude-opus-5`, ' +
|
|
82
|
+
'`claude-fable-5-1`, `claude-sonnet-5`).',
|
|
76
83
|
forward: { type: 'argv', flag: '--model' },
|
|
77
84
|
},
|
|
78
85
|
{
|
|
@@ -225,6 +232,24 @@ export const OPTION_SPEC = [
|
|
|
225
232
|
'(stream-json input + output only).',
|
|
226
233
|
forward: { type: 'shim' },
|
|
227
234
|
},
|
|
235
|
+
{
|
|
236
|
+
name: 'forward-subagent-text',
|
|
237
|
+
kind: 'boolean',
|
|
238
|
+
default: false,
|
|
239
|
+
description:
|
|
240
|
+
'Forward subagent text and thinking blocks as assistant/user messages ' +
|
|
241
|
+
'with `parent_tool_use_id` set. Requires `--output-format=stream-json`.',
|
|
242
|
+
forward: { type: 'argv', flag: '--forward-subagent-text' },
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'prompt-suggestions',
|
|
246
|
+
kind: 'boolean',
|
|
247
|
+
default: false,
|
|
248
|
+
description:
|
|
249
|
+
'Emit a `prompt_suggestion` message after each turn carrying a ' +
|
|
250
|
+
'predicted next user prompt (stream-json output).',
|
|
251
|
+
forward: { type: 'argv', flag: '--prompt-suggestions' },
|
|
252
|
+
},
|
|
228
253
|
{
|
|
229
254
|
name: 'include-hook-events',
|
|
230
255
|
kind: 'boolean',
|
|
@@ -236,7 +261,18 @@ export const OPTION_SPEC = [
|
|
|
236
261
|
name: 'include-partial-messages',
|
|
237
262
|
kind: 'boolean',
|
|
238
263
|
default: false,
|
|
239
|
-
|
|
264
|
+
// Upstream refuses this flag unless it is also given `--print`
|
|
265
|
+
// ("Error: --include-partial-messages requires --print and
|
|
266
|
+
// --output-format=stream-json") and exits immediately. The default
|
|
267
|
+
// PTY path drives the interactive TUI, so forwarding it there kills
|
|
268
|
+
// the spawn. ocp already synthesises `assistant-partial` events from
|
|
269
|
+
// the TUI frames on that path, so the flag has nothing to add there
|
|
270
|
+
// anyway — it is forwarded only under `--print-mode`.
|
|
271
|
+
printModeOnly: true,
|
|
272
|
+
description:
|
|
273
|
+
'Include partial assistant message chunks in stream-json output. ' +
|
|
274
|
+
'Forwarded upstream only under `--print-mode`; on the default PTY ' +
|
|
275
|
+
'path ocp emits `assistant-partial` events itself.',
|
|
240
276
|
forward: { type: 'argv', flag: '--include-partial-messages' },
|
|
241
277
|
},
|
|
242
278
|
|
|
@@ -244,8 +280,11 @@ export const OPTION_SPEC = [
|
|
|
244
280
|
{
|
|
245
281
|
name: 'effort',
|
|
246
282
|
kind: 'enum',
|
|
247
|
-
choices: ['low', 'medium', 'high', 'max'],
|
|
248
|
-
description:
|
|
283
|
+
choices: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
284
|
+
description:
|
|
285
|
+
'Effort level the model should apply (`low` | `medium` | `high` | ' +
|
|
286
|
+
'`xhigh` | `max`). `xhigh` sits between `high` and `max` and is the ' +
|
|
287
|
+
'upstream default for coding and agentic work.',
|
|
249
288
|
forward: { type: 'argv', flag: '--effort' },
|
|
250
289
|
},
|
|
251
290
|
{
|
|
@@ -281,6 +320,14 @@ export const OPTION_SPEC = [
|
|
|
281
320
|
description: 'API-side task budget in tokens. Shim-enforced.',
|
|
282
321
|
forward: { type: 'shim' },
|
|
283
322
|
},
|
|
323
|
+
{
|
|
324
|
+
name: 'autocompact',
|
|
325
|
+
kind: 'string',
|
|
326
|
+
description:
|
|
327
|
+
'Auto-compact window size: `auto`, or an explicit token budget ' +
|
|
328
|
+
'between 100k and 1M (e.g. `200000`, `500k`).',
|
|
329
|
+
forward: { type: 'argv', flag: '--autocompact' },
|
|
330
|
+
},
|
|
284
331
|
{
|
|
285
332
|
name: 'fallback-model',
|
|
286
333
|
kind: 'string',
|
|
@@ -293,8 +340,10 @@ export const OPTION_SPEC = [
|
|
|
293
340
|
name: 'permission-mode',
|
|
294
341
|
kind: 'string',
|
|
295
342
|
description:
|
|
296
|
-
'Permission mode (`
|
|
297
|
-
'`
|
|
343
|
+
'Permission mode (`acceptEdits` | `auto` | `bypassPermissions` | ' +
|
|
344
|
+
'`manual` | `dontAsk` | `plan`). `manual` is the current name for ' +
|
|
345
|
+
"the interactive prompt-on-every-tool mode; upstream still accepts " +
|
|
346
|
+
'the older `default` spelling, so ocp keeps accepting it too.',
|
|
298
347
|
forward: { type: 'argv', flag: '--permission-mode' },
|
|
299
348
|
},
|
|
300
349
|
{
|
|
@@ -305,6 +354,35 @@ export const OPTION_SPEC = [
|
|
|
305
354
|
'Allow `--dangerously-skip-permissions` as a choice without enabling it.',
|
|
306
355
|
forward: { type: 'argv', flag: '--allow-dangerously-skip-permissions' },
|
|
307
356
|
},
|
|
357
|
+
{
|
|
358
|
+
name: 'permission-prompts',
|
|
359
|
+
kind: 'enum',
|
|
360
|
+
choices: ['host', 'none'],
|
|
361
|
+
description:
|
|
362
|
+
'Who answers permission prompts: `host` (the SDK host, or the tool ' +
|
|
363
|
+
'named by `--permission-prompt-tool`) or `none` (nobody — anything ' +
|
|
364
|
+
'that would prompt is denied automatically; the permission mode ' +
|
|
365
|
+
'still decides everything else). `none` suits PTY automation, which ' +
|
|
366
|
+
'has no way to answer a prompt, when denying is preferable to ' +
|
|
367
|
+
'bypassing checks outright.',
|
|
368
|
+
forward: { type: 'argv', flag: '--permission-prompts' },
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: 'restricted',
|
|
372
|
+
kind: 'boolean',
|
|
373
|
+
default: false,
|
|
374
|
+
description:
|
|
375
|
+
'Restricted mode: drop the built-in tools that run commands or code ' +
|
|
376
|
+
'(Bash, PowerShell, REPL, …) and WebFetch unless `--tools` names ' +
|
|
377
|
+
'them, ignore user/project/local settings files (managed settings ' +
|
|
378
|
+
'and `--settings` still apply), confine the file tools to the ' +
|
|
379
|
+
'working directories, and refuse `bypassPermissions`. Pair with ' +
|
|
380
|
+
'`--strict-mcp-config` to skip MCP servers too. Note that ocp does ' +
|
|
381
|
+
'not get its usual clean transcript in this mode — no session ' +
|
|
382
|
+
'JSONL is written for the run, so the reply is reconstructed from ' +
|
|
383
|
+
'the TUI frames and may carry leftover box-border chrome.',
|
|
384
|
+
forward: { type: 'argv', flag: '--restricted' },
|
|
385
|
+
},
|
|
308
386
|
{
|
|
309
387
|
name: 'tools',
|
|
310
388
|
kind: 'string',
|
|
@@ -353,6 +431,29 @@ export const OPTION_SPEC = [
|
|
|
353
431
|
description: 'Append to the system prompt from a file path.',
|
|
354
432
|
forward: { type: 'argv', flag: '--append-system-prompt-file' },
|
|
355
433
|
},
|
|
434
|
+
{
|
|
435
|
+
name: 'system-prompt-snapshot',
|
|
436
|
+
kind: 'enum',
|
|
437
|
+
choices: ['on', 'off'],
|
|
438
|
+
description:
|
|
439
|
+
'Record the system prompt once per conversation and reuse it ' +
|
|
440
|
+
'verbatim on every later request and resume (`on`, the upstream ' +
|
|
441
|
+
'default), or render it fresh every request (`off`, for iterating ' +
|
|
442
|
+
'on prompt text). With `on`, a later run passing different prompt ' +
|
|
443
|
+
'text is ignored until the conversation is compacted.',
|
|
444
|
+
forward: { type: 'argv', flag: '--system-prompt-snapshot' },
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'exclude-dynamic-system-prompt-sections',
|
|
448
|
+
kind: 'boolean',
|
|
449
|
+
default: false,
|
|
450
|
+
description:
|
|
451
|
+
'Move per-machine sections (cwd, env info, memory paths, git status) ' +
|
|
452
|
+
'out of the system prompt and into the first user message, so the ' +
|
|
453
|
+
'cached prompt prefix is identical across machines and users. Ignored ' +
|
|
454
|
+
'when `--system-prompt` replaces the default prompt.',
|
|
455
|
+
forward: { type: 'argv', flag: '--exclude-dynamic-system-prompt-sections' },
|
|
456
|
+
},
|
|
356
457
|
{
|
|
357
458
|
name: 'add-dir',
|
|
358
459
|
kind: 'array',
|
|
@@ -387,6 +488,13 @@ export const OPTION_SPEC = [
|
|
|
387
488
|
description: 'Load plugins from directory (repeatable).',
|
|
388
489
|
forward: { type: 'argv', flag: '--plugin-dir' },
|
|
389
490
|
},
|
|
491
|
+
{
|
|
492
|
+
name: 'plugin-url',
|
|
493
|
+
kind: 'array',
|
|
494
|
+
repeatable: true,
|
|
495
|
+
description: 'Fetch a plugin `.zip` from a URL for this session only (repeatable).',
|
|
496
|
+
forward: { type: 'argv', flag: '--plugin-url' },
|
|
497
|
+
},
|
|
390
498
|
{
|
|
391
499
|
name: 'disable-slash-commands',
|
|
392
500
|
kind: 'boolean',
|
|
@@ -455,6 +563,38 @@ export const OPTION_SPEC = [
|
|
|
455
563
|
description: 'Run Setup hooks with the `maintenance` trigger, then continue.',
|
|
456
564
|
forward: { type: 'argv', flag: '--maintenance' },
|
|
457
565
|
},
|
|
566
|
+
{
|
|
567
|
+
name: 'safe-mode',
|
|
568
|
+
kind: 'boolean',
|
|
569
|
+
default: false,
|
|
570
|
+
description:
|
|
571
|
+
'Start with all customizations disabled (CLAUDE.md, skills, plugins, ' +
|
|
572
|
+
'hooks, MCP servers, custom commands and agents, output styles, ' +
|
|
573
|
+
'workflows, themes, keybindings). Admin policy settings still apply. ' +
|
|
574
|
+
'Sets `CLAUDE_CODE_SAFE_MODE=1`. Useful for reproducing a failure ' +
|
|
575
|
+
'without local configuration in the way.',
|
|
576
|
+
forward: { type: 'argv', flag: '--safe-mode' },
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: 'ax-screen-reader',
|
|
580
|
+
kind: 'boolean',
|
|
581
|
+
default: false,
|
|
582
|
+
env: 'OCP_AX_SCREEN_READER',
|
|
583
|
+
description:
|
|
584
|
+
'Ask the upstream TUI for screen-reader friendly output: flat text, ' +
|
|
585
|
+
'no decorative borders, no animations. Because ocp reconstructs its ' +
|
|
586
|
+
'answer by scraping TUI frames, this markedly reduces the amount of ' +
|
|
587
|
+
'chrome the parsers have to strip. Opt-in — it changes the captured ' +
|
|
588
|
+
'frame shape, so it is never enabled by default.',
|
|
589
|
+
forward: { type: 'argv', flag: '--ax-screen-reader' },
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: 'brief',
|
|
593
|
+
kind: 'boolean',
|
|
594
|
+
default: false,
|
|
595
|
+
description: 'Enable the SendUserMessage tool for agent-to-user communication.',
|
|
596
|
+
forward: { type: 'argv', flag: '--brief' },
|
|
597
|
+
},
|
|
458
598
|
{
|
|
459
599
|
name: 'debug-file',
|
|
460
600
|
kind: 'string',
|
package/src/options/validate.js
CHANGED
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
// Rules are appended to `CROSS_RULES`; the runner returns the concatenated
|
|
9
9
|
// list of human-readable errors.
|
|
10
10
|
|
|
11
|
+
// Upstream (`claude --permission-mode`) advertises: acceptEdits, auto,
|
|
12
|
+
// bypassPermissions, manual, dontAsk, plan. `manual` is the current name
|
|
13
|
+
// for the prompt-on-every-tool mode that used to be spelled `default`;
|
|
14
|
+
// upstream still accepts `default`, so we keep it here rather than
|
|
15
|
+
// breaking callers that pass it.
|
|
11
16
|
const PERMISSION_MODES = new Set([
|
|
12
|
-
'
|
|
17
|
+
'acceptEdits', 'auto', 'bypassPermissions', 'manual', 'dontAsk', 'plan',
|
|
18
|
+
'default',
|
|
13
19
|
]);
|
|
14
20
|
|
|
15
21
|
/** @type {Array<(options: Record<string, unknown>) => string | undefined>} */
|
|
@@ -85,6 +91,40 @@ export const CROSS_RULES = [
|
|
|
85
91
|
return '`--fork-session` requires `--resume <id>` or `--continue`.';
|
|
86
92
|
}
|
|
87
93
|
},
|
|
94
|
+
// R11. forward-subagent-text => stream-json output. Upstream only emits
|
|
95
|
+
// the extra assistant/user messages on the stream-json channel.
|
|
96
|
+
(o) => {
|
|
97
|
+
if (o['forward-subagent-text'] === true && o['output-format'] !== 'stream-json') {
|
|
98
|
+
return '`--forward-subagent-text` requires `--output-format=stream-json`.';
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
// R12. prompt-suggestions => stream-json output. The suggestion arrives
|
|
102
|
+
// as its own `prompt_suggestion` event; the text and json adapters
|
|
103
|
+
// have nowhere to put it.
|
|
104
|
+
(o) => {
|
|
105
|
+
if (o['prompt-suggestions'] === true && o['output-format'] !== 'stream-json') {
|
|
106
|
+
return '`--prompt-suggestions` requires `--output-format=stream-json`.';
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
// R13. autocompact takes `auto` or a token budget in the 100k–1M range.
|
|
110
|
+
// Accepts a bare integer or a `k`/`m` suffix.
|
|
111
|
+
(o) => {
|
|
112
|
+
const v = o.autocompact;
|
|
113
|
+
if (v === undefined) return undefined;
|
|
114
|
+
if (typeof v !== 'string' || v === '') {
|
|
115
|
+
return '`--autocompact` must be `auto` or a token budget between 100k and 1M.';
|
|
116
|
+
}
|
|
117
|
+
if (v === 'auto') return undefined;
|
|
118
|
+
const m = /^(\d+(?:\.\d+)?)([km])?$/i.exec(v.trim());
|
|
119
|
+
if (!m) {
|
|
120
|
+
return `\`--autocompact\` must be \`auto\` or a token budget between 100k and 1M; got ${JSON.stringify(v)}.`;
|
|
121
|
+
}
|
|
122
|
+
const mult = m[2] ? { k: 1e3, m: 1e6 }[m[2].toLowerCase()] : 1;
|
|
123
|
+
const tokens = Number(m[1]) * mult;
|
|
124
|
+
if (!(tokens >= 100_000 && tokens <= 1_000_000)) {
|
|
125
|
+
return `\`--autocompact\` token budget must be between 100k and 1M; got ${JSON.stringify(v)}.`;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
88
128
|
];
|
|
89
129
|
|
|
90
130
|
/**
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
// Baseline parser: strip ANSI escape sequences.
|
|
2
2
|
//
|
|
3
|
-
// We do not attempt to reconstruct a terminal screen here.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// We do not attempt to reconstruct a terminal screen here. Colors, clears
|
|
4
|
+
// and vertical moves are simply removed. The one thing we DO model is the
|
|
5
|
+
// cursor's horizontal position, because the upstream TUI uses horizontal
|
|
6
|
+
// moves in place of literal spaces:
|
|
7
|
+
//
|
|
8
|
+
// A\x1b[5Gpseudoterminal\x1b[20Gis\x1b[23Ga\x1b[25Gsoftware
|
|
9
|
+
//
|
|
10
|
+
// Every inter-word gap there is an absolute column move, not a space. Strip
|
|
11
|
+
// the sequences blindly and the words run together — "Apseudoterminalisa…".
|
|
12
|
+
// So we track the column and translate a forward move into the run of
|
|
13
|
+
// spaces it would have left blank on screen.
|
|
14
|
+
//
|
|
15
|
+
// Two sequences produce those gaps:
|
|
16
|
+
// CUF — `ESC [ Pn C` move right Pn columns (relative, default 1)
|
|
17
|
+
// CHA — `ESC [ Pn G` move to column Pn (absolute, 1-based, default 1)
|
|
18
|
+
//
|
|
19
|
+
// CUF alone used to be enough; the current CLI emits CHA for nearly every
|
|
20
|
+
// word, which is what made this worth modelling properly rather than
|
|
21
|
+
// special-casing one sequence.
|
|
6
22
|
//
|
|
7
23
|
// Buffering rule for partial chunks: if the chunk ends with an ESC that has
|
|
8
24
|
// not yet been terminated (e.g. mid-CSI), hold it until the next chunk.
|
|
@@ -10,37 +26,9 @@
|
|
|
10
26
|
const ESC = '\x1b';
|
|
11
27
|
const BEL = '\x07';
|
|
12
28
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// rendering optimization). Stripping the sequence collapses words
|
|
17
|
-
// together — `each<CUF1>other` becomes `eachother`. We translate CUF
|
|
18
|
-
// into the equivalent run of spaces BEFORE the general CSI strip so the
|
|
19
|
-
// visible text is preserved.
|
|
20
|
-
const RE_CSI_CURSOR_FORWARD = /\x1b\[(\d*)C/g;
|
|
21
|
-
function expandCursorForward(text) {
|
|
22
|
-
return text.replace(RE_CSI_CURSOR_FORWARD, (_match, n) => {
|
|
23
|
-
const count = parseInt(n || '1', 10);
|
|
24
|
-
return ' '.repeat(Math.min(count, 200)); // cap defensively
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Complete-sequence regexes — used only on input slices we have proven to be
|
|
29
|
-
// complete (i.e. without a dangling partial at the end).
|
|
30
|
-
const RE_CSI = /\x1b\[[0-?]*[ -/]*[@-~]/g; // ESC [ params intermediates final
|
|
31
|
-
// OSC-style sequences: ESC introducer then arbitrary content then BEL or ST.
|
|
32
|
-
// Covers OSC (`]`), DCS (`P`), SOS (`X`), PM (`^`), APC (`_`).
|
|
33
|
-
const RE_STRING_TERMINATED =
|
|
34
|
-
/\x1b[\]PX^_][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
35
|
-
// Two-byte ESC forms — covers C1 7-bit aliases (`ESC @`..`ESC _` except the
|
|
36
|
-
// CSI/OSC/etc. introducers, which are handled above) AND the DEC private
|
|
37
|
-
// single-letter sequences in the 0x30..0x3F range (`ESC 7` = DECSC save
|
|
38
|
-
// cursor, `ESC 8` = DECRC restore, `ESC =` / `ESC >` keypad modes, etc.).
|
|
39
|
-
// Order is: CSI -> string-terminated -> this, so by the time we reach
|
|
40
|
-
// RE_C1 the only `\x1b[`/`\x1b]`/`\x1b_`/... left in the input is either
|
|
41
|
-
// complete-and-already-stripped or partial-and-held-back by the pending
|
|
42
|
-
// buffer.
|
|
43
|
-
const RE_C1 = /\x1b[0-?@-_]/g;
|
|
29
|
+
// Defensive ceiling on generated padding. A corrupt or hostile column
|
|
30
|
+
// target must not let one escape sequence expand into megabytes.
|
|
31
|
+
const MAX_PAD = 200;
|
|
44
32
|
|
|
45
33
|
/** Introducer characters for OSC-style sequences (BEL/ST terminated). */
|
|
46
34
|
const STRING_INTRODUCERS = new Set([']', 'P', 'X', '^', '_']);
|
|
@@ -56,11 +44,136 @@ function isCompleteEscapeTail(tail) {
|
|
|
56
44
|
return tail.length >= 2;
|
|
57
45
|
}
|
|
58
46
|
|
|
47
|
+
/** First numeric parameter of a CSI parameter string, or `fallback`. */
|
|
48
|
+
function firstParam(params, fallback) {
|
|
49
|
+
const n = parseInt(params, 10);
|
|
50
|
+
return Number.isNaN(n) ? fallback : n;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Nth (0-based) numeric parameter of a CSI parameter string. */
|
|
54
|
+
function nthParam(params, index, fallback) {
|
|
55
|
+
const raw = params.split(';')[index];
|
|
56
|
+
if (raw === undefined || raw === '') return fallback;
|
|
57
|
+
const n = parseInt(raw, 10);
|
|
58
|
+
return Number.isNaN(n) ? fallback : n;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Strip escapes from `input` while tracking the cursor column, expanding
|
|
63
|
+
* forward horizontal moves into spaces.
|
|
64
|
+
*
|
|
65
|
+
* `state.col` is the 0-based column and persists across chunks, since a
|
|
66
|
+
* rendered line routinely spans several PTY reads.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} input
|
|
69
|
+
* @param {{col: number}} state
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function stripTrackingColumn(input, state) {
|
|
73
|
+
let out = '';
|
|
74
|
+
let i = 0;
|
|
75
|
+
const len = input.length;
|
|
76
|
+
|
|
77
|
+
while (i < len) {
|
|
78
|
+
const ch = input[i];
|
|
79
|
+
|
|
80
|
+
if (ch === '\r' || ch === '\n') {
|
|
81
|
+
out += ch;
|
|
82
|
+
state.col = 0;
|
|
83
|
+
i += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ch !== ESC) {
|
|
88
|
+
out += ch;
|
|
89
|
+
state.col += 1;
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const c1 = input[i + 1];
|
|
95
|
+
|
|
96
|
+
// ── CSI: ESC [ params intermediates final ────────────────────────────
|
|
97
|
+
if (c1 === '[') {
|
|
98
|
+
let j = i + 2;
|
|
99
|
+
while (j < len) {
|
|
100
|
+
const c = input.charCodeAt(j);
|
|
101
|
+
if (c >= 0x30 && c <= 0x3f) j += 1; // parameter bytes
|
|
102
|
+
else break;
|
|
103
|
+
}
|
|
104
|
+
const paramsEnd = j;
|
|
105
|
+
while (j < len) {
|
|
106
|
+
const c = input.charCodeAt(j);
|
|
107
|
+
if (c >= 0x20 && c <= 0x2f) j += 1; // intermediate bytes
|
|
108
|
+
else break;
|
|
109
|
+
}
|
|
110
|
+
if (j >= len) {
|
|
111
|
+
// Incomplete despite the pending-buffer guard; drop the remainder.
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const final = input[j];
|
|
115
|
+
const params = input.slice(i + 2, paramsEnd);
|
|
116
|
+
i = j + 1;
|
|
117
|
+
|
|
118
|
+
// Private-parameter sequences (`ESC [ ? 25 h`) never move the cursor
|
|
119
|
+
// horizontally in a way we can model; strip them.
|
|
120
|
+
if (params.charCodeAt(0) >= 0x3c && params.charCodeAt(0) <= 0x3f) continue;
|
|
121
|
+
|
|
122
|
+
if (final === 'C') {
|
|
123
|
+
// CUF — relative forward move.
|
|
124
|
+
const n = Math.max(0, firstParam(params, 1));
|
|
125
|
+
const pad = Math.min(n, MAX_PAD);
|
|
126
|
+
out += ' '.repeat(pad);
|
|
127
|
+
state.col += pad;
|
|
128
|
+
} else if (final === 'G' || final === '`') {
|
|
129
|
+
// CHA / HPA — absolute column, 1-based.
|
|
130
|
+
const target = Math.max(1, firstParam(params, 1)) - 1;
|
|
131
|
+
if (target > state.col) {
|
|
132
|
+
const pad = Math.min(target - state.col, MAX_PAD);
|
|
133
|
+
out += ' '.repeat(pad);
|
|
134
|
+
state.col += pad;
|
|
135
|
+
}
|
|
136
|
+
// A backwards move would overwrite already-emitted text. We cannot
|
|
137
|
+
// represent that without a screen model, so we neither pad nor
|
|
138
|
+
// rewind — the text simply continues.
|
|
139
|
+
} else if (final === 'H' || final === 'f') {
|
|
140
|
+
// CUP — absolute row;col. This is a jump to another part of the
|
|
141
|
+
// screen (status line, input box), so it never means "gap here";
|
|
142
|
+
// adopt the column but emit no padding.
|
|
143
|
+
state.col = Math.max(1, nthParam(params, 1, 1)) - 1;
|
|
144
|
+
}
|
|
145
|
+
// Everything else (SGR, erase, vertical moves, scroll regions) is
|
|
146
|
+
// zero-width for our purposes: stripped, column untouched.
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── OSC and friends: ESC ] … BEL | ST ────────────────────────────────
|
|
151
|
+
if (STRING_INTRODUCERS.has(c1)) {
|
|
152
|
+
let j = i + 2;
|
|
153
|
+
while (j < len) {
|
|
154
|
+
if (input[j] === BEL) { j += 1; break; }
|
|
155
|
+
if (input[j] === ESC && input[j + 1] === '\\') { j += 2; break; }
|
|
156
|
+
j += 1;
|
|
157
|
+
}
|
|
158
|
+
i = j;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Two-byte ESC forms (ESC 7, ESC 8, ESC =, C1 aliases) ─────────────
|
|
163
|
+
if (c1 === undefined) break; // dangling ESC; pending guard should catch it
|
|
164
|
+
i += 2;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
59
170
|
export const ansiStripParser = {
|
|
60
171
|
name: 'ansi-strip',
|
|
61
172
|
priority: 10,
|
|
62
173
|
create() {
|
|
63
174
|
let pending = '';
|
|
175
|
+
// Cursor column persists across feeds — a line spans many PTY reads.
|
|
176
|
+
const state = { col: 0 };
|
|
64
177
|
return {
|
|
65
178
|
feed(chunk) {
|
|
66
179
|
let input = pending + chunk;
|
|
@@ -77,17 +190,11 @@ export const ansiStripParser = {
|
|
|
77
190
|
}
|
|
78
191
|
}
|
|
79
192
|
|
|
80
|
-
|
|
81
|
-
// literal spaces BEFORE the general CSI strip eats them.
|
|
82
|
-
const expanded = expandCursorForward(input);
|
|
83
|
-
const out = expanded
|
|
84
|
-
.replace(RE_CSI, '')
|
|
85
|
-
.replace(RE_STRING_TERMINATED, '')
|
|
86
|
-
.replace(RE_C1, '');
|
|
87
|
-
return { text: out, events: [] };
|
|
193
|
+
return { text: stripTrackingColumn(input, state), events: [] };
|
|
88
194
|
},
|
|
89
195
|
reset() {
|
|
90
196
|
pending = '';
|
|
197
|
+
state.col = 0;
|
|
91
198
|
},
|
|
92
199
|
};
|
|
93
200
|
},
|