subconscious-cli 6.0.0 → 6.1.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.
Files changed (76) hide show
  1. package/README.md +135 -2
  2. package/bin/agent-data.js +242 -0
  3. package/bin/agent-help.js +106 -0
  4. package/bin/agent-install.js +354 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +284 -840
  8. package/bin/auth.js +89 -33
  9. package/bin/branding.js +3 -1
  10. package/bin/claude-picker.js +143 -0
  11. package/bin/cli.js +87 -20
  12. package/bin/clipboard-image.js +24 -5
  13. package/bin/colors.js +1 -1
  14. package/bin/feedback.js +71 -21
  15. package/bin/harness-manifest.js +286 -0
  16. package/bin/headless-args.js +35 -0
  17. package/bin/launch-values.js +25 -0
  18. package/bin/model-capabilities.js +12 -8
  19. package/bin/models.js +35 -19
  20. package/bin/native/subc-tui-darwin-amd64 +0 -0
  21. package/bin/native/subc-tui-darwin-arm64 +0 -0
  22. package/bin/native/subc-tui-linux-amd64 +0 -0
  23. package/bin/native/subc-tui-linux-arm64 +0 -0
  24. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  25. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  26. package/bin/opencode-provider.js +48 -5
  27. package/bin/profile-settings.js +129 -0
  28. package/bin/profiles.js +136 -449
  29. package/bin/runbook/README.md +66 -23
  30. package/bin/runbook/claude-code/agent.json +300 -0
  31. package/bin/runbook/claude-code/install.sh +9 -252
  32. package/bin/runbook/codex/agent.json +461 -0
  33. package/bin/runbook/codex/hooks-lib.sh +17 -4
  34. package/bin/runbook/codex/install.sh +0 -4
  35. package/bin/runbook/codex/run.sh +16 -235
  36. package/bin/runbook/copilot/agent.json +185 -0
  37. package/bin/runbook/copilot/install.sh +45 -63
  38. package/bin/runbook/cursor/agent.json +137 -0
  39. package/bin/runbook/cursor/install.sh +8 -35
  40. package/bin/runbook/deepseek-harness/agent.json +163 -0
  41. package/bin/runbook/deepseek-harness/run.sh +20 -59
  42. package/bin/runbook/image-window/index.ts +14 -0
  43. package/bin/runbook/image-window/window.d.ts +19 -0
  44. package/bin/runbook/image-window/window.js +140 -0
  45. package/bin/runbook/lib.sh +41 -0
  46. package/bin/runbook/opencode/agent.json +158 -0
  47. package/bin/runbook/opencode/install.sh +9 -183
  48. package/bin/runbook/opencode/opencode-plugin.d.ts +16 -0
  49. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  50. package/bin/runbook/pi/agent.json +216 -0
  51. package/bin/runbook/pi/install.sh +18 -44
  52. package/bin/runbook/pi/run.sh +6 -18
  53. package/bin/runbook/pi/subconscious-compaction.ts +71 -49
  54. package/bin/runbook/shared.json +58 -0
  55. package/bin/runbook/subconscious-code/agent.json +156 -0
  56. package/bin/sessions.js +173 -58
  57. package/bin/tui.js +104 -32
  58. package/bin/update-check.js +54 -21
  59. package/bin/upgrade.js +67 -43
  60. package/bin/usage.js +47 -24
  61. package/bin/windows/agents.js +141 -37
  62. package/bin/windows/common.js +36 -25
  63. package/bin/windows/install.js +116 -34
  64. package/bin/windows/launch.js +260 -77
  65. package/bin/windows/pi-compaction.ts +68 -13
  66. package/bin/windows/process.js +210 -57
  67. package/bin/windows/setup.js +251 -56
  68. package/package.json +9 -4
  69. package/bin/registry.generated.json +0 -406
  70. package/bin/runbook/claude-code/run.sh +0 -254
  71. package/bin/runbook/model-capabilities.generated.sh +0 -9
  72. package/bin/runbook/opencode/provider.sh +0 -93
  73. package/bin/runbook/opencode/run.sh +0 -99
  74. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  75. package/bin/runbook/subconscious-code/run.sh +0 -21
  76. package/bin/windows/claude.js +0 -71
package/bin/profiles.js CHANGED
@@ -6,24 +6,29 @@
6
6
  * ~/.subconscious/profiles/<name>.env
7
7
  */
8
8
 
9
- import { spawn, execFileSync } from 'node:child_process';
10
- import { windowsEditor, runWindows } from './windows/process.js';
9
+ import { execFileSync, spawn } from 'node:child_process';
11
10
  import fs from 'node:fs/promises';
12
11
  import os from 'node:os';
13
12
  import path from 'node:path';
14
- import { readFileSync } from 'node:fs';
15
13
  import { c } from './colors.js';
16
14
  import { PUBLIC_CATALOG_FALLBACK_MESSAGE } from './models.js';
17
-
18
- const registry = JSON.parse(
19
- readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
20
- );
15
+ import {
16
+ profileTemplate,
17
+ RUNBOOK_DEFAULTS,
18
+ SUPPORTED_MODELS,
19
+ } from './profile-settings.js';
20
+
21
+ export {
22
+ PROFILE_SETTING_GROUPS,
23
+ profileSettingsForAgent,
24
+ RUNBOOK_DEFAULTS,
25
+ resolvedProfileValues,
26
+ SUPPORTED_MODELS,
27
+ } from './profile-settings.js';
28
+
29
+ import { runWindows, windowsEditor } from './windows/process.js';
21
30
 
22
31
  export const DEFAULT_PROFILE = 'default';
23
- export const SUPPORTED_MODELS =
24
- Array.isArray(registry.defaults.models) && registry.defaults.models.length
25
- ? registry.defaults.models
26
- : [registry.defaults.model];
27
32
  const PREVIOUS_DEFAULT_GATEWAYS = new Set([
28
33
  'https://api.subconscious.dev',
29
34
  'https://api.subconscious.dev/',
@@ -35,407 +40,11 @@ const LEGACY_PROFILES_DIR = CONFIG_OVERRIDE
35
40
  ? null
36
41
  : path.join(os.homedir(), '.subcon', 'profiles');
37
42
 
38
- export const RUNBOOK_DEFAULTS = {
39
- GATEWAY_URL: registry.defaults.baseUrl,
40
- API_KEY: '',
41
- MODEL: registry.defaults.model,
42
- SC_API_KEY: '',
43
- CLAUDE_CODE_SUBAGENT_MODEL: '',
44
- CLAUDE_GATEWAY_URL: '',
45
- CLAUDE_CODE_API_KEY: '',
46
- CODEX_API_KEY: '',
47
- OPENCODE_API_KEY: '',
48
- CURSOR_API_KEY: '',
49
- COPILOT_API_KEY: '',
50
- PI_API_KEY: '',
51
- DEEPSEEK_HARNESS_API_KEY: '',
52
- CLAUDE_CODE_AUTO_COMPACT_WINDOW: '1000000',
53
- CLAUDE_CODE_MAX_CONTEXT_TOKENS: '3000000',
54
- MAX_CONCURRENT_SUBAGENTS: '4',
55
- MAX_SUBAGENT_SPAWN_DEPTH: '1',
56
- CODEX_CONTEXT_WINDOW: '5000000',
57
- CODEX_MAX_CONTEXT_WINDOW: '5000000',
58
- CODEX_AUTO_COMPACT_TOKEN_LIMIT: '4500000',
59
- CODEX_REASONING_EFFORT: 'max',
60
- CODEX_EXTERNAL_TOOLS: 'false',
61
- OPENCODE_CONTEXT_LIMIT: '5000000',
62
- OPENCODE_OUTPUT_LIMIT: '65536',
63
- PI_CONTEXT_WINDOW: '5000000',
64
- PI_MAX_TOKENS: '65536',
65
- DEEPSEEK_HARNESS_CONTEXT_WINDOW: '5000000',
66
- DEEPSEEK_HARNESS_MAX_TOKENS: '65536',
67
- COPILOT_MAX_INPUT_TOKENS: '5000000',
68
- COPILOT_MAX_OUTPUT_TOKENS: '65536',
69
- VSCODE_APP: '',
70
- };
71
-
72
- const SETTINGS = {
73
- GATEWAY_URL: {
74
- key: 'GATEWAY_URL',
75
- label: 'Gateway URL',
76
- description: 'Subconscious gateway origin shared by every integration.',
77
- type: 'url',
78
- required: true,
79
- },
80
- PLATFORM_URL: {
81
- key: 'PLATFORM_URL',
82
- label: 'Platform URL',
83
- description: 'Subconscious platform origin for login, whoami, and usage.',
84
- type: 'url',
85
- },
86
- API_KEY: {
87
- key: 'API_KEY',
88
- label: 'Shared API key',
89
- description: 'Default credential used when an agent-specific key is blank.',
90
- type: 'secret',
91
- },
92
- MODEL: {
93
- key: 'MODEL',
94
- label: 'Default model',
95
- description:
96
- 'Initial model used by coding-agent launches and setup. UNSET uses the first live catalog model.',
97
- type: 'choice',
98
- choices: SUPPORTED_MODELS,
99
- },
100
- SC_API_KEY: {
101
- key: 'SC_API_KEY',
102
- label: 'Subconscious Code API key',
103
- description: 'Subconscious Code credential override; blank uses API_KEY.',
104
- type: 'secret',
105
- },
106
- CLAUDE_CODE_SUBAGENT_MODEL: {
107
- key: 'CLAUDE_CODE_SUBAGENT_MODEL',
108
- label: 'Subagent model',
109
- description: 'Model used by Claude Code subagents. UNSET follows the default model.',
110
- type: 'choice',
111
- choices: SUPPORTED_MODELS,
112
- },
113
- CLAUDE_GATEWAY_URL: {
114
- key: 'CLAUDE_GATEWAY_URL',
115
- label: 'Claude gateway override',
116
- description: 'Optional Claude-only gateway origin; blank uses GATEWAY_URL.',
117
- type: 'url',
118
- },
119
- CLAUDE_CODE_API_KEY: {
120
- key: 'CLAUDE_CODE_API_KEY',
121
- label: 'Claude API key',
122
- description: 'Claude-specific credential override; blank uses API_KEY.',
123
- type: 'secret',
124
- },
125
- CLAUDE_CODE_AUTO_COMPACT_WINDOW: {
126
- key: 'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
127
- label: 'Auto-compact window',
128
- description: 'Claude token window before compaction (100000–1000000).',
129
- type: 'integer',
130
- min: 100000,
131
- max: 1000000,
132
- },
133
- CLAUDE_CODE_MAX_CONTEXT_TOKENS: {
134
- key: 'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
135
- label: 'Maximum context tokens',
136
- description: 'Maximum context advertised to Claude Code.',
137
- type: 'integer',
138
- min: 1,
139
- },
140
- MAX_CONCURRENT_SUBAGENTS: {
141
- key: 'MAX_CONCURRENT_SUBAGENTS',
142
- label: 'Concurrent subagents',
143
- description: 'Maximum concurrent Claude/Codex subagent threads.',
144
- type: 'integer',
145
- min: 1,
146
- },
147
- CODEX_SUBAGENT_REASONING_EFFORT: {
148
- key: 'CODEX_SUBAGENT_REASONING_EFFORT',
149
- label: 'Subagent reasoning effort',
150
- description:
151
- 'Effort for Codex subagents, set separately from the parent: a marathon model at max thinks for minutes without emitting a token, and a subagent turn is one uninterrupted think, so inheriting max makes every delegated subtask look like a hang.',
152
- type: 'text',
153
- },
154
- CODEX_MULTI_AGENT_VERSION: {
155
- key: 'CODEX_MULTI_AGENT_VERSION',
156
- label: 'Codex multi-agent version',
157
- description:
158
- 'Which subagent implementation Codex offers: v2 gives the collaboration tools. Leave empty to turn subagents off entirely.',
159
- type: 'text',
160
- },
161
- CODEX_STREAM_IDLE_TIMEOUT_MS: {
162
- key: 'CODEX_STREAM_IDLE_TIMEOUT_MS',
163
- label: 'Stream idle timeout (ms)',
164
- description:
165
- 'How long Codex waits on a silent stream before giving up. Needs to exceed the longest think the model will take.',
166
- type: 'integer',
167
- min: 1,
168
- },
169
- MAX_SUBAGENT_SPAWN_DEPTH: {
170
- key: 'MAX_SUBAGENT_SPAWN_DEPTH',
171
- label: 'Subagent spawn depth',
172
- description: 'Maximum Claude subagent nesting depth.',
173
- type: 'integer',
174
- min: 0,
175
- },
176
- CODEX_API_KEY: {
177
- key: 'CODEX_API_KEY',
178
- label: 'Codex API key',
179
- description: 'Codex-specific credential override; blank uses API_KEY.',
180
- type: 'secret',
181
- },
182
- CODEX_CONTEXT_WINDOW: {
183
- key: 'CODEX_CONTEXT_WINDOW',
184
- label: 'Context window',
185
- description: 'Context window written into the Codex model catalog.',
186
- type: 'integer',
187
- min: 1,
188
- },
189
- CODEX_MAX_CONTEXT_WINDOW: {
190
- key: 'CODEX_MAX_CONTEXT_WINDOW',
191
- label: 'Maximum context window',
192
- description: 'Maximum context window written into the Codex catalog.',
193
- type: 'integer',
194
- min: 1,
195
- },
196
- CODEX_AUTO_COMPACT_TOKEN_LIMIT: {
197
- key: 'CODEX_AUTO_COMPACT_TOKEN_LIMIT',
198
- label: 'Auto-compact token limit',
199
- description: 'Codex token threshold that triggers automatic compaction.',
200
- type: 'integer',
201
- min: 1,
202
- },
203
- CODEX_REASONING_EFFORT: {
204
- key: 'CODEX_REASONING_EFFORT',
205
- label: 'Reasoning effort',
206
- description: 'Default Codex reasoning effort.',
207
- type: 'choice',
208
- choices: ['none', 'low', 'medium', 'high', 'max'],
209
- },
210
- CODEX_EXTERNAL_TOOLS: {
211
- key: 'CODEX_EXTERNAL_TOOLS',
212
- label: 'External tools',
213
- description: 'Enable Codex apps/plugins; may exceed the gateway tool limit.',
214
- type: 'choice',
215
- choices: ['false', 'true'],
216
- },
217
- OPENCODE_API_KEY: {
218
- key: 'OPENCODE_API_KEY',
219
- label: 'OpenCode API key',
220
- description: 'OpenCode-specific credential override; blank uses API_KEY.',
221
- type: 'secret',
222
- },
223
- OPENCODE_CONTEXT_LIMIT: {
224
- key: 'OPENCODE_CONTEXT_LIMIT',
225
- label: 'Context limit',
226
- description: 'OpenCode provider context limit used for compaction.',
227
- type: 'integer',
228
- min: 1,
229
- },
230
- OPENCODE_OUTPUT_LIMIT: {
231
- key: 'OPENCODE_OUTPUT_LIMIT',
232
- label: 'Output limit',
233
- description: 'Maximum output tokens advertised to OpenCode.',
234
- type: 'integer',
235
- min: 1,
236
- },
237
- CURSOR_API_KEY: {
238
- key: 'CURSOR_API_KEY',
239
- label: 'Cursor hook API key',
240
- description: 'Cursor hook credential override; blank uses API_KEY.',
241
- type: 'secret',
242
- },
243
- COPILOT_API_KEY: {
244
- key: 'COPILOT_API_KEY',
245
- label: 'Copilot hook API key',
246
- description: 'Copilot hook credential override; blank uses API_KEY.',
247
- type: 'secret',
248
- },
249
- COPILOT_MAX_INPUT_TOKENS: {
250
- key: 'COPILOT_MAX_INPUT_TOKENS',
251
- label: 'Maximum input tokens',
252
- description: 'Input-token capacity advertised to VS Code.',
253
- type: 'integer',
254
- min: 1,
255
- },
256
- COPILOT_MAX_OUTPUT_TOKENS: {
257
- key: 'COPILOT_MAX_OUTPUT_TOKENS',
258
- label: 'Maximum output tokens',
259
- description: 'Output-token capacity advertised to VS Code.',
260
- type: 'integer',
261
- min: 1,
262
- },
263
- VSCODE_APP: {
264
- key: 'VSCODE_APP',
265
- label: 'VS Code application',
266
- description: 'Optional application override; blank enables auto-detection.',
267
- type: 'choice',
268
- choices: ['', 'Code', 'Code - Insiders', 'VSCodium'],
269
- },
270
- PI_API_KEY: {
271
- key: 'PI_API_KEY',
272
- label: 'Pi API key',
273
- description: 'Pi-specific credential override; blank uses API_KEY.',
274
- type: 'secret',
275
- },
276
- PI_CONTEXT_WINDOW: {
277
- key: 'PI_CONTEXT_WINDOW',
278
- label: 'Context window',
279
- description: 'Pi context window used to determine compaction.',
280
- type: 'integer',
281
- min: 1,
282
- },
283
- PI_MAX_TOKENS: {
284
- key: 'PI_MAX_TOKENS',
285
- label: 'Maximum output tokens',
286
- description: 'Maximum output tokens advertised to Pi.',
287
- type: 'integer',
288
- min: 1,
289
- },
290
- DEEPSEEK_HARNESS_API_KEY: {
291
- key: 'DEEPSEEK_HARNESS_API_KEY',
292
- label: 'DeepSeek Harness API key',
293
- description: 'DeepSeek Harness credential override; blank uses API_KEY.',
294
- type: 'secret',
295
- },
296
- DEEPSEEK_HARNESS_CONTEXT_WINDOW: {
297
- key: 'DEEPSEEK_HARNESS_CONTEXT_WINDOW',
298
- label: 'Context window',
299
- description: 'Context capacity advertised to DeepSeek Harness.',
300
- type: 'integer',
301
- min: 1,
302
- },
303
- DEEPSEEK_HARNESS_MAX_TOKENS: {
304
- key: 'DEEPSEEK_HARNESS_MAX_TOKENS',
305
- label: 'Maximum output tokens',
306
- description: 'Output capacity advertised to DeepSeek Harness.',
307
- type: 'integer',
308
- min: 1,
309
- },
310
- };
311
-
312
- const AGENT_SETTING_KEYS = {
313
- 'subconscious-code': ['SC_API_KEY'],
314
- 'claude-code': [
315
- 'CLAUDE_GATEWAY_URL',
316
- 'CLAUDE_CODE_API_KEY',
317
- 'CLAUDE_CODE_SUBAGENT_MODEL',
318
- 'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
319
- 'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
320
- 'MAX_CONCURRENT_SUBAGENTS',
321
- 'MAX_SUBAGENT_SPAWN_DEPTH',
322
- ],
323
- codex: [
324
- 'CODEX_API_KEY',
325
- 'CODEX_CONTEXT_WINDOW',
326
- 'CODEX_MAX_CONTEXT_WINDOW',
327
- 'CODEX_AUTO_COMPACT_TOKEN_LIMIT',
328
- 'CODEX_REASONING_EFFORT',
329
- 'CODEX_EXTERNAL_TOOLS',
330
- 'CODEX_STREAM_IDLE_TIMEOUT_MS',
331
- 'CODEX_MULTI_AGENT_VERSION',
332
- 'CODEX_SUBAGENT_REASONING_EFFORT',
333
- 'MAX_CONCURRENT_SUBAGENTS',
334
- ],
335
- opencode: ['OPENCODE_API_KEY', 'OPENCODE_CONTEXT_LIMIT', 'OPENCODE_OUTPUT_LIMIT'],
336
- cursor: ['CURSOR_API_KEY'],
337
- copilot: [
338
- 'COPILOT_API_KEY',
339
- 'COPILOT_MAX_INPUT_TOKENS',
340
- 'COPILOT_MAX_OUTPUT_TOKENS',
341
- 'VSCODE_APP',
342
- ],
343
- pi: ['PI_API_KEY', 'PI_CONTEXT_WINDOW', 'PI_MAX_TOKENS'],
344
- 'deepseek-harness': [
345
- 'DEEPSEEK_HARNESS_API_KEY',
346
- 'DEEPSEEK_HARNESS_CONTEXT_WINDOW',
347
- 'DEEPSEEK_HARNESS_MAX_TOKENS',
348
- ],
349
- };
350
-
351
- const GROUP_KEYS = [
352
- { id: 'shared', label: 'Shared settings', keys: ['GATEWAY_URL', 'API_KEY', 'MODEL'] },
353
- ...registry.agents
354
- .filter((agent) => agent.cli !== false)
355
- .map((agent) => ({
356
- id: agent.id,
357
- label: agent.name,
358
- keys: AGENT_SETTING_KEYS[agent.id] || [],
359
- })),
360
- ];
361
-
362
- export const PROFILE_SETTING_GROUPS = GROUP_KEYS.map((group) => ({
363
- ...group,
364
- settings: group.keys.map((key) => SETTINGS[key]),
365
- }));
366
-
367
- export function profileSettingsForAgent(agentId) {
368
- const shared = PROFILE_SETTING_GROUPS.find((group) => group.id === 'shared');
369
- const agent = PROFILE_SETTING_GROUPS.find((group) => group.id === agentId);
370
- return [...(shared?.settings || []), ...(agent?.settings || [])];
371
- }
372
-
373
- export function resolvedProfileValues(profile) {
374
- return { ...RUNBOOK_DEFAULTS, ...(profile?.values || {}) };
375
- }
376
-
377
- function profileTemplate() {
378
- const modelComments = SUPPORTED_MODELS.map((model) => `# ${model}`).join('\n');
379
- return `# Subconscious coding-agent profile
380
- # Generated by subc. Extra KEY=value lines are passed through to launches.
381
-
382
- GATEWAY_URL={GATEWAY_URL}
383
- API_KEY={API_KEY}
384
- # Available models (MODEL=UNSET uses the first live catalog model):
385
- ${modelComments}
386
- MODEL={MODEL}
387
-
388
- # Optional per-agent keys. Leave blank to use API_KEY above.
389
- SC_API_KEY={SC_API_KEY}
390
- CLAUDE_CODE_API_KEY={CLAUDE_CODE_API_KEY}
391
- CODEX_API_KEY={CODEX_API_KEY}
392
- OPENCODE_API_KEY={OPENCODE_API_KEY}
393
- CURSOR_API_KEY={CURSOR_API_KEY}
394
- COPILOT_API_KEY={COPILOT_API_KEY}
395
- PI_API_KEY={PI_API_KEY}
396
- DEEPSEEK_HARNESS_API_KEY={DEEPSEEK_HARNESS_API_KEY}
397
-
398
- # Claude Code
399
- # Leave CLAUDE_GATEWAY_URL blank to use GATEWAY_URL above.
400
- CLAUDE_GATEWAY_URL={CLAUDE_GATEWAY_URL}
401
- # CLAUDE_CODE_SUBAGENT_MODEL=UNSET follows MODEL above.
402
- CLAUDE_CODE_SUBAGENT_MODEL={CLAUDE_CODE_SUBAGENT_MODEL}
403
- CLAUDE_CODE_AUTO_COMPACT_WINDOW={CLAUDE_CODE_AUTO_COMPACT_WINDOW}
404
- CLAUDE_CODE_MAX_CONTEXT_TOKENS={CLAUDE_CODE_MAX_CONTEXT_TOKENS}
405
- MAX_CONCURRENT_SUBAGENTS={MAX_CONCURRENT_SUBAGENTS}
406
- MAX_SUBAGENT_SPAWN_DEPTH={MAX_SUBAGENT_SPAWN_DEPTH}
407
-
408
- # Codex
409
- CODEX_CONTEXT_WINDOW={CODEX_CONTEXT_WINDOW}
410
- CODEX_MAX_CONTEXT_WINDOW={CODEX_MAX_CONTEXT_WINDOW}
411
- CODEX_AUTO_COMPACT_TOKEN_LIMIT={CODEX_AUTO_COMPACT_TOKEN_LIMIT}
412
- CODEX_REASONING_EFFORT={CODEX_REASONING_EFFORT}
413
- CODEX_EXTERNAL_TOOLS={CODEX_EXTERNAL_TOOLS}
414
-
415
- # OpenCode
416
- OPENCODE_CONTEXT_LIMIT={OPENCODE_CONTEXT_LIMIT}
417
- OPENCODE_OUTPUT_LIMIT={OPENCODE_OUTPUT_LIMIT}
418
-
419
- # Pi
420
- PI_CONTEXT_WINDOW={PI_CONTEXT_WINDOW}
421
- PI_MAX_TOKENS={PI_MAX_TOKENS}
422
-
423
- # DeepSeek Harness
424
- DEEPSEEK_HARNESS_CONTEXT_WINDOW={DEEPSEEK_HARNESS_CONTEXT_WINDOW}
425
- DEEPSEEK_HARNESS_MAX_TOKENS={DEEPSEEK_HARNESS_MAX_TOKENS}
426
-
427
- # GitHub Copilot
428
- COPILOT_MAX_INPUT_TOKENS={COPILOT_MAX_INPUT_TOKENS}
429
- COPILOT_MAX_OUTPUT_TOKENS={COPILOT_MAX_OUTPUT_TOKENS}
430
- VSCODE_APP={VSCODE_APP}
431
-
432
- # Extra KEY=value lines are passed through to launches.
433
- `;
434
- }
435
-
436
43
  export function validateProfileName(name) {
437
44
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
438
- throw new Error(`Invalid profile name '${name}' (use letters, digits, _ or -)`);
45
+ throw new Error(
46
+ `Invalid profile name '${name}' (use letters, digits, _ or -)`,
47
+ );
439
48
  }
440
49
  return name;
441
50
  }
@@ -462,7 +71,9 @@ export function parseProfile(text) {
462
71
  for (const line of text.split('\n')) {
463
72
  const trimmed = line.trim();
464
73
  if (!trimmed || trimmed.startsWith('#')) continue;
465
- const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
74
+ const match = trimmed.match(
75
+ /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/,
76
+ );
466
77
  if (!match) continue;
467
78
  values[match[1]] = decodeValue(match[2]);
468
79
  }
@@ -478,7 +89,9 @@ function encodeValue(value) {
478
89
  }
479
90
 
480
91
  function renderTemplate(values) {
481
- return profileTemplate().replace(/\{([A-Z0-9_]+)\}/g, (_, key) => encodeValue(values[key]));
92
+ return profileTemplate().replace(/\{([A-Z0-9_]+)\}/g, (_, key) =>
93
+ encodeValue(values[key]),
94
+ );
482
95
  }
483
96
 
484
97
  function upsertValues(text, updates) {
@@ -491,7 +104,8 @@ function upsertValues(text, updates) {
491
104
  remaining.delete(match[2]);
492
105
  return `${match[1]}${match[2]}=${encodeValue(value)}`;
493
106
  });
494
- for (const [key, value] of remaining) output.push(`${key}=${encodeValue(value)}`);
107
+ for (const [key, value] of remaining)
108
+ output.push(`${key}=${encodeValue(value)}`);
495
109
  return `${output.join('\n')}\n`;
496
110
  }
497
111
 
@@ -506,10 +120,12 @@ function migrateCopilotTokenDefaults(text) {
506
120
  const values = parseProfile(text);
507
121
  const updates = {};
508
122
  if (values.COPILOT_MAX_INPUT_TOKENS === '12288') {
509
- updates.COPILOT_MAX_INPUT_TOKENS = RUNBOOK_DEFAULTS.COPILOT_MAX_INPUT_TOKENS;
123
+ updates.COPILOT_MAX_INPUT_TOKENS =
124
+ RUNBOOK_DEFAULTS.COPILOT_MAX_INPUT_TOKENS;
510
125
  }
511
126
  if (values.COPILOT_MAX_OUTPUT_TOKENS === '4096') {
512
- updates.COPILOT_MAX_OUTPUT_TOKENS = RUNBOOK_DEFAULTS.COPILOT_MAX_OUTPUT_TOKENS;
127
+ updates.COPILOT_MAX_OUTPUT_TOKENS =
128
+ RUNBOOK_DEFAULTS.COPILOT_MAX_OUTPUT_TOKENS;
513
129
  }
514
130
  return Object.keys(updates).length ? upsertValues(text, updates) : text;
515
131
  }
@@ -530,7 +146,12 @@ export async function loadProfile(name = DEFAULT_PROFILE) {
530
146
  const text = await fs.readFile(file, 'utf-8');
531
147
  const migratedText = migrateDefaults(text);
532
148
  if (migratedText !== text) await writeProfile(file, migratedText);
533
- return { name, path: file, exists: true, values: parseProfile(migratedText) };
149
+ return {
150
+ name,
151
+ path: file,
152
+ exists: true,
153
+ values: parseProfile(migratedText),
154
+ };
534
155
  } catch (error) {
535
156
  if (error.code !== 'ENOENT') throw error;
536
157
  }
@@ -539,7 +160,10 @@ export async function loadProfile(name = DEFAULT_PROFILE) {
539
160
  if (LEGACY_PROFILES_DIR) {
540
161
  try {
541
162
  const text = migrateDefaults(
542
- await fs.readFile(path.join(LEGACY_PROFILES_DIR, `${name}.env`), 'utf-8'),
163
+ await fs.readFile(
164
+ path.join(LEGACY_PROFILES_DIR, `${name}.env`),
165
+ 'utf-8',
166
+ ),
543
167
  );
544
168
  await writeProfile(file, text);
545
169
  return { name, path: file, exists: true, values: parseProfile(text) };
@@ -562,7 +186,8 @@ export async function ensureProfile(name = DEFAULT_PROFILE, apiKey) {
562
186
  Object.fromEntries(
563
187
  Object.entries(values).filter(
564
188
  ([key]) =>
565
- profile.values[key] === undefined || (key === 'API_KEY' && apiKey !== undefined),
189
+ profile.values[key] === undefined ||
190
+ (key === 'API_KEY' && apiKey !== undefined),
566
191
  ),
567
192
  ),
568
193
  )
@@ -586,11 +211,21 @@ export async function clearProfileApiKey(name = DEFAULT_PROFILE) {
586
211
  return true;
587
212
  }
588
213
 
589
- function isSecretKey(key) {
214
+ // PI_MAX_TOKENS=65536 is a budget, not a credential. Anything else under a
215
+ // *_TOKENS name, such as a key pasted onto the wrong line, stays redacted.
216
+ function isTokenBudget(key, value) {
217
+ return (
218
+ /_TOKENS$|_TOKEN_LIMIT$/i.test(key) &&
219
+ /^\d+$/.test(value) &&
220
+ Number.isSafeInteger(Number(value))
221
+ );
222
+ }
223
+
224
+ function isSecretKey(key, value) {
590
225
  const upper = key.toUpperCase();
591
226
  return (
592
227
  upper.endsWith('API_KEY') ||
593
- upper.includes('TOKEN') ||
228
+ (upper.includes('TOKEN') && !isTokenBudget(key, value)) ||
594
229
  upper.includes('SECRET') ||
595
230
  upper.includes('PASSWORD') ||
596
231
  upper.includes('AUTH')
@@ -598,16 +233,20 @@ function isSecretKey(key) {
598
233
  }
599
234
 
600
235
  function redact(key, value) {
601
- if (!isSecretKey(key) || !value) return value;
602
- return value.length <= 8 ? '********' : `${value.slice(0, 4)}…${value.slice(-4)}`;
236
+ if (!value || !isSecretKey(key, value)) return value;
237
+ return value.length <= 8
238
+ ? '********'
239
+ : `${value.slice(0, 4)}…${value.slice(-4)}`;
603
240
  }
604
241
 
605
242
  function redactEnvLine(line) {
606
- const match = line.match(/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
243
+ const match = line.match(
244
+ /^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)=(.*)$/,
245
+ );
607
246
  if (!match) return line;
608
247
  const [, prefix, key, raw] = match;
609
248
  const value = decodeValue(raw);
610
- if (!isSecretKey(key) || !value) return line;
249
+ if (!value || !isSecretKey(key, value)) return line;
611
250
  return `${prefix}${key}=${redact(key, value)}`;
612
251
  }
613
252
 
@@ -633,18 +272,20 @@ async function printProfile(profile) {
633
272
  }
634
273
  const text = await fs.readFile(profile.path, 'utf-8');
635
274
  console.log(
636
- text
637
- .replace(/\n$/, '')
638
- .split('\n')
639
- .map(redactEnvLine)
640
- .join('\n'),
275
+ text.replace(/\n$/, '').split('\n').map(redactEnvLine).join('\n'),
641
276
  );
642
277
  console.log();
643
278
  }
644
279
 
645
280
  export function isUnsetSetting(value) {
646
- const normalized = String(value ?? '').trim().toUpperCase();
647
- return normalized === 'UNSET' || normalized === 'FOLLOW-GATEWAY' || normalized === 'FOLLOW-DEFAULT';
281
+ const normalized = String(value ?? '')
282
+ .trim()
283
+ .toUpperCase();
284
+ return (
285
+ normalized === 'UNSET' ||
286
+ normalized === 'FOLLOW-GATEWAY' ||
287
+ normalized === 'FOLLOW-DEFAULT'
288
+ );
648
289
  }
649
290
 
650
291
  export function resolvedModelSetting(value) {
@@ -684,7 +325,11 @@ export function validateSettingValue(setting, value) {
684
325
  return null;
685
326
  }
686
327
 
687
- const ALLOWED_EDITORS = new Set(process.platform === 'win32' ? ['notepad', 'code', 'code-insiders', 'vim', 'nano'] : ['vim', 'nano']);
328
+ const ALLOWED_EDITORS = new Set(
329
+ process.platform === 'win32'
330
+ ? ['notepad', 'code', 'code-insiders', 'vim', 'nano']
331
+ : ['vim', 'nano'],
332
+ );
688
333
 
689
334
  function commandExists(bin) {
690
335
  try {
@@ -709,7 +354,9 @@ function resolveEditor(requested) {
709
354
  if (fromEnv) return parseEditorCommand(fromEnv);
710
355
  if (commandExists('vim')) return { cmd: 'vim', args: [] };
711
356
  if (commandExists('nano')) return { cmd: 'nano', args: [] };
712
- throw new Error('No editor found. Install vim or nano, or set $VISUAL / $EDITOR.');
357
+ throw new Error(
358
+ 'No editor found. Install vim or nano, or set $VISUAL / $EDITOR.',
359
+ );
713
360
  }
714
361
 
715
362
  async function editProfile(profileName, requestedEditor) {
@@ -776,12 +423,18 @@ async function printProfileList(activeName) {
776
423
  console.log(`\n ${c.bold}Profiles${c.reset}`);
777
424
  for (const name of profiles) {
778
425
  const active = name === activeName ? ` ${c.green}(active)${c.reset}` : '';
779
- console.log(` ${c.cyan}${name}${c.reset}${active} ${c.dim}${profilePath(name)}${c.reset}`);
426
+ console.log(
427
+ ` ${c.cyan}${name}${c.reset}${active} ${c.dim}${profilePath(name)}${c.reset}`,
428
+ );
780
429
  }
781
430
  console.log();
782
431
  }
783
432
 
784
- export async function configCommand(argv, profileName = DEFAULT_PROFILE, options = {}) {
433
+ export async function configCommand(
434
+ argv,
435
+ profileName = DEFAULT_PROFILE,
436
+ options = {},
437
+ ) {
785
438
  let action = 'default';
786
439
  let editor;
787
440
  const updates = {};
@@ -791,7 +444,17 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
791
444
  if (arg === 'help' || arg === '-h' || arg === '--help') {
792
445
  printConfigHelp();
793
446
  return;
794
- } else if (['show', 'path', 'list', 'create', 'delete', 'edit', 'interactive'].includes(arg)) {
447
+ } else if (
448
+ [
449
+ 'show',
450
+ 'path',
451
+ 'list',
452
+ 'create',
453
+ 'delete',
454
+ 'edit',
455
+ 'interactive',
456
+ ].includes(arg)
457
+ ) {
795
458
  action = arg === 'interactive' ? 'edit' : arg;
796
459
  } else if (
797
460
  arg === '--gateway-url' ||
@@ -810,12 +473,17 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
810
473
  '--subagent-model': 'CLAUDE_CODE_SUBAGENT_MODEL',
811
474
  }[arg];
812
475
  updates[key] =
813
- (arg === '--model' || arg === '--subagent-model') && isUnsetSetting(value) ? '' : value;
476
+ (arg === '--model' || arg === '--subagent-model') &&
477
+ isUnsetSetting(value)
478
+ ? ''
479
+ : value;
814
480
  } else if (action === 'edit' && ALLOWED_EDITORS.has(arg)) {
815
- if (editor) throw new Error('Specify only one editor (vim or nano)');
481
+ if (editor) throw new Error('Specify only one editor');
816
482
  editor = arg;
817
483
  } else if (action === 'edit') {
818
- throw new Error(`Unknown editor '${arg}' (use vim or nano)`);
484
+ throw new Error(
485
+ `Unknown editor '${arg}' (use ${[...ALLOWED_EDITORS].join(', ')})`,
486
+ );
819
487
  } else {
820
488
  throw new Error(`Unknown config argument: ${arg}`);
821
489
  }
@@ -831,7 +499,12 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
831
499
  return;
832
500
  }
833
501
 
834
- if (action === 'list' || (action === 'default' && !options.profileExplicit && !Object.keys(updates).length)) {
502
+ if (
503
+ action === 'list' ||
504
+ (action === 'default' &&
505
+ !options.profileExplicit &&
506
+ !Object.keys(updates).length)
507
+ ) {
835
508
  await printProfileList(profileName);
836
509
  return;
837
510
  }
@@ -846,7 +519,8 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
846
519
  throw new Error('config create cannot be combined with profile updates');
847
520
  }
848
521
  const existing = await loadProfile(profileName);
849
- if (existing.exists) throw new Error(`Profile '${profileName}' already exists`);
522
+ if (existing.exists)
523
+ throw new Error(`Profile '${profileName}' already exists`);
850
524
  const profile = await ensureProfile(profileName);
851
525
  console.log(`Created profile '${profileName}'.`);
852
526
  await printProfile(profile);
@@ -855,7 +529,9 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
855
529
 
856
530
  if (action === 'delete') {
857
531
  if (profileName === DEFAULT_PROFILE) {
858
- throw new Error('Refusing to delete the default profile; use subc logout to clear its key');
532
+ throw new Error(
533
+ 'Refusing to delete the default profile; use subc logout to clear its key',
534
+ );
859
535
  }
860
536
  const profile = await loadProfile(profileName);
861
537
  if (!profile.exists) {
@@ -878,10 +554,12 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
878
554
  }
879
555
 
880
556
  export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
881
- const selectedModel = options.selectedModel || models[0] || registry.defaults.model;
557
+ const selectedModel =
558
+ options.selectedModel || models[0] || RUNBOOK_DEFAULTS.MODEL;
882
559
  console.log(`\n ${c.bold}Available models${c.reset}\n`);
883
560
  for (const model of models) {
884
- const suffix = model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
561
+ const suffix =
562
+ model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
885
563
  console.log(` ${c.cyan}${model}${c.reset}${suffix}`);
886
564
  }
887
565
  if (options.source === 'public' && options.hasApiKey) {
@@ -896,7 +574,9 @@ export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
896
574
  }
897
575
 
898
576
  export function validateOriginUrl(rawUrl, label = 'URL') {
899
- const normalized = String(rawUrl ?? '').trim().replace(/\/+$/, '');
577
+ const normalized = String(rawUrl ?? '')
578
+ .trim()
579
+ .replace(/\/+$/, '');
900
580
  if (!normalized) {
901
581
  throw new Error(`${label} cannot be blank`);
902
582
  }
@@ -929,7 +609,9 @@ export async function updateUrlCommand(argv = [], options = {}) {
929
609
  const profile = await updateProfile(profileName, { GATEWAY_URL: gatewayUrl });
930
610
 
931
611
  console.log(`\n ${c.green}${c.bold}✓ Gateway URL updated.${c.reset}`);
932
- console.log(` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`);
612
+ console.log(
613
+ ` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`,
614
+ );
933
615
  console.log(` ${c.dim}URL: ${gatewayUrl}${c.reset}`);
934
616
  if (process.env.SUBCONSCIOUS_BASE_URL?.trim()) {
935
617
  console.log(
@@ -937,7 +619,8 @@ export async function updateUrlCommand(argv = [], options = {}) {
937
619
  );
938
620
  }
939
621
  const claudeOverride =
940
- process.env.CLAUDE_GATEWAY_URL?.trim() || profile.values.CLAUDE_GATEWAY_URL?.trim();
622
+ process.env.CLAUDE_GATEWAY_URL?.trim() ||
623
+ profile.values.CLAUDE_GATEWAY_URL?.trim();
941
624
  if (claudeOverride) {
942
625
  console.log(
943
626
  ` ${c.yellow}CLAUDE_GATEWAY_URL is set, so Claude Code will continue using ${claudeOverride}.${c.reset}`,
@@ -954,10 +637,14 @@ export async function updatePlatformUrlCommand(argv = [], options = {}) {
954
637
  const platformUrl = validateOriginUrl(argv[0], 'Platform URL');
955
638
 
956
639
  const profileName = options.profileName || DEFAULT_PROFILE;
957
- const profile = await updateProfile(profileName, { PLATFORM_URL: platformUrl });
640
+ const profile = await updateProfile(profileName, {
641
+ PLATFORM_URL: platformUrl,
642
+ });
958
643
 
959
644
  console.log(`\n ${c.green}${c.bold}✓ Platform URL updated.${c.reset}`);
960
- console.log(` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`);
645
+ console.log(
646
+ ` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`,
647
+ );
961
648
  console.log(` ${c.dim}URL: ${platformUrl}${c.reset}`);
962
649
  if (process.env.SUBCONSCIOUS_URL?.trim()) {
963
650
  console.log(