subconscious-cli 6.0.1 → 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.
- package/README.md +119 -0
- package/bin/agent-data.js +242 -0
- package/bin/agent-help.js +106 -0
- package/bin/agent-install.js +354 -0
- package/bin/agent-launch.js +199 -0
- package/bin/agent-spawn.js +137 -0
- package/bin/agents.js +236 -932
- package/bin/auth.js +9 -1
- package/bin/claude-picker.js +143 -0
- package/bin/cli.js +60 -1
- package/bin/clipboard-image.js +2 -0
- package/bin/feedback.js +14 -3
- package/bin/harness-manifest.js +286 -0
- package/bin/headless-args.js +35 -0
- package/bin/launch-values.js +25 -0
- package/bin/model-capabilities.js +10 -9
- package/bin/models.js +13 -13
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/opencode-provider.js +21 -0
- package/bin/profile-settings.js +129 -0
- package/bin/profiles.js +29 -424
- package/bin/runbook/README.md +66 -23
- package/bin/runbook/claude-code/agent.json +300 -0
- package/bin/runbook/claude-code/install.sh +9 -252
- package/bin/runbook/codex/agent.json +461 -0
- package/bin/runbook/codex/hooks-lib.sh +17 -4
- package/bin/runbook/codex/install.sh +0 -4
- package/bin/runbook/codex/run.sh +16 -235
- package/bin/runbook/copilot/agent.json +185 -0
- package/bin/runbook/copilot/install.sh +45 -63
- package/bin/runbook/cursor/agent.json +137 -0
- package/bin/runbook/cursor/install.sh +8 -35
- package/bin/runbook/deepseek-harness/agent.json +163 -0
- package/bin/runbook/deepseek-harness/run.sh +20 -59
- package/bin/runbook/image-window/index.ts +14 -0
- package/bin/runbook/image-window/window.d.ts +19 -0
- package/bin/runbook/image-window/window.js +140 -0
- package/bin/runbook/lib.sh +41 -0
- package/bin/runbook/opencode/agent.json +158 -0
- package/bin/runbook/opencode/install.sh +9 -183
- package/bin/runbook/opencode/opencode-plugin.d.ts +10 -3
- package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
- package/bin/runbook/pi/agent.json +216 -0
- package/bin/runbook/pi/install.sh +18 -44
- package/bin/runbook/pi/run.sh +6 -18
- package/bin/runbook/pi/subconscious-compaction.ts +3 -1
- package/bin/runbook/shared.json +58 -0
- package/bin/runbook/subconscious-code/agent.json +156 -0
- package/bin/sessions.js +52 -22
- package/bin/tui.js +13 -0
- package/bin/update-check.js +5 -1
- package/bin/upgrade.js +24 -31
- package/bin/usage.js +21 -10
- package/bin/windows/agents.js +18 -17
- package/bin/windows/common.js +2 -28
- package/bin/windows/install.js +11 -17
- package/bin/windows/launch.js +252 -231
- package/bin/windows/process.js +12 -1
- package/bin/windows/setup.js +29 -9
- package/package.json +2 -3
- package/bin/registry.generated.json +0 -406
- package/bin/runbook/claude-code/run.sh +0 -254
- package/bin/runbook/model-capabilities.generated.sh +0 -9
- package/bin/runbook/opencode/provider.sh +0 -93
- package/bin/runbook/opencode/run.sh +0 -99
- package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
- package/bin/runbook/subconscious-code/run.sh +0 -21
- package/bin/windows/claude.js +0 -100
package/bin/agents.js
CHANGED
|
@@ -1,126 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AGENTS,
|
|
3
|
+
agentCommandName,
|
|
4
|
+
agentKeyName,
|
|
5
|
+
agentSetupActions,
|
|
6
|
+
DEFAULTS,
|
|
7
|
+
PACKAGED_MODELS,
|
|
8
|
+
} from './agent-data.js';
|
|
9
|
+
import { isAgentHelpRequest, printAgentHelp } from './agent-help.js';
|
|
10
|
+
import {
|
|
11
|
+
augmentPath,
|
|
12
|
+
candidateBinDirs,
|
|
13
|
+
claudeVersionNeedsUpgrade,
|
|
14
|
+
ensureInstalled,
|
|
15
|
+
ensureMinimumVersion,
|
|
16
|
+
MIN_CLAUDE_CODE_VERSION,
|
|
17
|
+
parseClaudeVersion,
|
|
18
|
+
} from './agent-install.js';
|
|
19
|
+
import { launchPlan, resolveInputs } from './agent-launch.js';
|
|
20
|
+
import { planCommand, runCommand, scriptCommand } from './agent-spawn.js';
|
|
20
21
|
import { getApiKey } from './auth.js';
|
|
22
|
+
import {
|
|
23
|
+
applyClaudePickerCatalog,
|
|
24
|
+
CLAUDE_PICKER_ENV_DESCRIPTION,
|
|
25
|
+
claudeModelPickerEnv,
|
|
26
|
+
claudePickerSettings,
|
|
27
|
+
} from './claude-picker.js';
|
|
21
28
|
import { c } from './colors.js';
|
|
29
|
+
import {
|
|
30
|
+
headlessPromptIndex,
|
|
31
|
+
separatorIndex,
|
|
32
|
+
takesModelValue,
|
|
33
|
+
} from './headless-args.js';
|
|
22
34
|
import {
|
|
23
35
|
isLiveModelSource,
|
|
24
36
|
PUBLIC_CATALOG_FALLBACK_MESSAGE,
|
|
25
37
|
resolveModelCatalog,
|
|
26
38
|
} from './models.js';
|
|
27
|
-
import {
|
|
28
|
-
isUnsetSetting,
|
|
29
|
-
profileSettingsForAgent,
|
|
30
|
-
resolvedModelSetting,
|
|
31
|
-
resolvedProfileValues,
|
|
32
|
-
} from './profiles.js';
|
|
33
|
-
import { compareVersions } from './update-check.js';
|
|
39
|
+
import { isUnsetSetting, resolvedModelSetting } from './profiles.js';
|
|
34
40
|
import { runWindowsAgent } from './windows/agents.js';
|
|
35
41
|
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
export { agentCommandName, agentSetupActions } from './agent-data.js';
|
|
43
|
+
export { isAgentHelpRequest, printAgentHelp } from './agent-help.js';
|
|
44
|
+
export {
|
|
45
|
+
augmentPath,
|
|
46
|
+
claudeVersionNeedsUpgrade,
|
|
47
|
+
MIN_CLAUDE_CODE_VERSION,
|
|
48
|
+
MIN_CODEX_VERSION,
|
|
49
|
+
parseClaudeVersion,
|
|
50
|
+
preferredBinDirsForAgent,
|
|
51
|
+
readClaudeVersion,
|
|
52
|
+
versionNeedsUpgrade,
|
|
53
|
+
} from './agent-install.js';
|
|
54
|
+
export { claudePickerSettings } from './claude-picker.js';
|
|
55
|
+
|
|
56
|
+
const CLAUDE_CODE = 'claude-code';
|
|
46
57
|
|
|
47
|
-
// --- Registry (single source of truth, generated copy shipped in the package).
|
|
48
|
-
const registry = JSON.parse(
|
|
49
|
-
readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
|
|
50
|
-
);
|
|
51
|
-
const DEFAULTS = registry.defaults;
|
|
52
|
-
const PACKAGED_MODELS =
|
|
53
|
-
Array.isArray(DEFAULTS.models) && DEFAULTS.models.length
|
|
54
|
-
? DEFAULTS.models
|
|
55
|
-
: [DEFAULTS.model];
|
|
56
|
-
const RUNBOOK_DIR = fileURLToPath(new URL('./runbook/', import.meta.url));
|
|
57
|
-
|
|
58
|
-
// --- Token substitution — same rules as scripts/lib/registry.js.
|
|
59
|
-
// Replaces {apiKey}, {model}, {baseUrl}, {baseUrlV1}. NEVER touches {env:...}.
|
|
60
|
-
const TOKENS = ['apiKey', 'model', 'baseUrl', 'baseUrlV1'];
|
|
61
|
-
|
|
62
|
-
function substituteString(str, ctx) {
|
|
63
|
-
let out = str;
|
|
64
|
-
for (const token of TOKENS) {
|
|
65
|
-
if (ctx[token] === undefined) continue;
|
|
66
|
-
out = out.split(`{${token}}`).join(ctx[token]);
|
|
67
|
-
}
|
|
68
|
-
return out;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function substitute(value, ctx) {
|
|
72
|
-
if (typeof value === 'string') return substituteString(value, ctx);
|
|
73
|
-
if (Array.isArray(value)) return value.map((v) => substitute(v, ctx));
|
|
74
|
-
if (value && typeof value === 'object') {
|
|
75
|
-
const keys = Object.keys(value);
|
|
76
|
-
if (keys.length === 1 && keys[0] === '$json') {
|
|
77
|
-
return JSON.stringify(substitute(value.$json, ctx));
|
|
78
|
-
}
|
|
79
|
-
const out = {};
|
|
80
|
-
for (const key of keys)
|
|
81
|
-
out[substituteString(key, ctx)] = substitute(value[key], ctx);
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Resolve the install command for the current OS from a per-OS install object.
|
|
89
|
-
* Falls back to the linux command, then any string value present, if the exact
|
|
90
|
-
* `process.platform` key is missing. Tolerates a legacy plain-string `install`.
|
|
91
|
-
* Returns `{ command, fallback }` where `fallback` may be undefined.
|
|
92
|
-
*/
|
|
93
|
-
function resolveInstall(install) {
|
|
94
|
-
if (typeof install === 'string')
|
|
95
|
-
return { command: install, fallback: undefined };
|
|
96
|
-
if (!install || typeof install !== 'object')
|
|
97
|
-
return { command: undefined, fallback: undefined };
|
|
98
|
-
const command =
|
|
99
|
-
install[process.platform] ||
|
|
100
|
-
install.linux ||
|
|
101
|
-
Object.values(install).find((v) => typeof v === 'string');
|
|
102
|
-
return { command, fallback: install.fallback };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// --- Build the in-memory registry + alias index.
|
|
106
|
-
// Each agent gets a resolved per-OS `install` (string) plus optional
|
|
107
|
-
// `installFallback`, while keeping the original per-OS object available.
|
|
108
|
-
const AGENTS = registry.agents
|
|
109
|
-
.filter((agent) => agent.cli !== false)
|
|
110
|
-
.map((agent) => {
|
|
111
|
-
const { command, fallback } = resolveInstall(agent.install);
|
|
112
|
-
return {
|
|
113
|
-
...agent,
|
|
114
|
-
bin: agent.binByPlatform?.[process.platform] || agent.bin,
|
|
115
|
-
install: command,
|
|
116
|
-
installFallback: fallback,
|
|
117
|
-
};
|
|
118
|
-
});
|
|
119
58
|
const BY_ALIAS = new Map();
|
|
120
59
|
for (const agent of AGENTS) {
|
|
121
60
|
BY_ALIAS.set(agent.id, agent);
|
|
122
|
-
|
|
123
|
-
for (const alias of agent.aliases
|
|
61
|
+
BY_ALIAS.set(agentCommandName(agent), agent);
|
|
62
|
+
for (const alias of agent.aliases) BY_ALIAS.set(alias, agent);
|
|
124
63
|
}
|
|
125
64
|
|
|
126
65
|
export function resolveAgent(name) {
|
|
@@ -131,26 +70,16 @@ export function agentList() {
|
|
|
131
70
|
return AGENTS.map((a) => ({
|
|
132
71
|
id: a.id,
|
|
133
72
|
name: a.name,
|
|
134
|
-
alias: a
|
|
135
|
-
action: a.runbook
|
|
136
|
-
description: a.description
|
|
137
|
-
launch: a.runbook
|
|
73
|
+
alias: agentCommandName(a),
|
|
74
|
+
action: a.runbook.mode === 'setup' ? 'Configure' : 'Launch',
|
|
75
|
+
description: a.description,
|
|
76
|
+
launch: a.runbook.mode !== 'setup',
|
|
138
77
|
}));
|
|
139
78
|
}
|
|
140
79
|
|
|
141
80
|
const SETUP_ACTIONS = new Set(['install', 'status', 'uninstall']);
|
|
142
81
|
const DROPPED_SETUP_HELPERS = new Set(['use', 'env', 'unset']);
|
|
143
82
|
|
|
144
|
-
export function agentSetupActions(agent) {
|
|
145
|
-
return Array.isArray(agent.runbook?.setupActions)
|
|
146
|
-
? agent.runbook.setupActions
|
|
147
|
-
: [];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function agentCommandName(agent) {
|
|
151
|
-
return agent.command || agent.id;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
83
|
export function parseAgentAction(agent, argv = []) {
|
|
155
84
|
const command = agentCommandName(agent);
|
|
156
85
|
const first = argv[0];
|
|
@@ -163,7 +92,7 @@ export function parseAgentAction(agent, argv = []) {
|
|
|
163
92
|
}
|
|
164
93
|
|
|
165
94
|
if (SETUP_ACTIONS.has(first)) {
|
|
166
|
-
if (!agent.runbook
|
|
95
|
+
if (!agent.runbook.setup_script || !actions.includes(first)) {
|
|
167
96
|
if (first === 'install') {
|
|
168
97
|
const uninstallHint = actions.includes('uninstall')
|
|
169
98
|
? ` To remove leftover files: subc ${command} uninstall.`
|
|
@@ -179,234 +108,37 @@ export function parseAgentAction(agent, argv = []) {
|
|
|
179
108
|
return { action: first, args: argv };
|
|
180
109
|
}
|
|
181
110
|
|
|
182
|
-
if (
|
|
111
|
+
if (first === 'headless') {
|
|
112
|
+
if (!agent.launch?.headless_argv) {
|
|
113
|
+
throw new Error(`${agent.name} does not support headless mode.`);
|
|
114
|
+
}
|
|
115
|
+
if (!argv[1]?.trim() || ['-h', '--help'].includes(argv[1])) {
|
|
116
|
+
throw new Error(`Usage: subc ${command} headless PROMPT [args...]`);
|
|
117
|
+
}
|
|
118
|
+
if (argv.slice(2).some((arg) => arg === '-h' || arg === '--help')) {
|
|
119
|
+
throw new Error('help is not available in a headless run');
|
|
120
|
+
}
|
|
121
|
+
return { action: 'headless', args: argv };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (agent.runbook.mode === 'setup') {
|
|
183
125
|
return { action: 'install', args: argv };
|
|
184
126
|
}
|
|
185
127
|
|
|
186
128
|
return { action: 'launch', args: argv };
|
|
187
129
|
}
|
|
188
130
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
['help', 'Show this help'],
|
|
196
|
-
[
|
|
197
|
-
'install',
|
|
198
|
-
process.platform === 'win32'
|
|
199
|
-
? 'Install a verified Windows release, when published upstream'
|
|
200
|
-
: 'Install the latest precompiled Linux or macOS release',
|
|
201
|
-
],
|
|
202
|
-
['--model MODEL', 'Override the profile model for this launch'],
|
|
203
|
-
['ARGS...', 'Pass arguments directly to Marathon'],
|
|
204
|
-
],
|
|
205
|
-
},
|
|
206
|
-
'claude-code': {
|
|
207
|
-
usage:
|
|
208
|
-
'subc [-p NAME] claude [help|status|uninstall] [Claude arguments...]',
|
|
209
|
-
behavior:
|
|
210
|
-
'Launches Claude Code with the active Subconscious profile and model picker. Persistent files are leftover-only: subc claude uninstall.',
|
|
211
|
-
options: [
|
|
212
|
-
['help', 'Show this help'],
|
|
213
|
-
['status', 'Inspect leftover ~/.claude/subconscious-gateway.env'],
|
|
214
|
-
['uninstall', 'Remove leftover ~/.claude/subconscious-gateway.env'],
|
|
215
|
-
['--model MODEL', 'Override the profile model for this launch'],
|
|
216
|
-
['--compact-window N', 'Override the Claude auto-compact window'],
|
|
217
|
-
['--max-context-tokens N', 'Override the maximum context tokens'],
|
|
218
|
-
['-- ARGS...', 'Pass remaining arguments to Claude Code'],
|
|
219
|
-
],
|
|
220
|
-
},
|
|
221
|
-
codex: {
|
|
222
|
-
usage:
|
|
223
|
-
'subc [-p NAME] codex [help|install|status|uninstall] [Codex arguments...]',
|
|
224
|
-
behavior:
|
|
225
|
-
'Launches Codex with a temporary Subconscious provider catalog. Compaction hooks are merged into ~/.codex/hooks.json on first launch and removed with subc codex uninstall; Codex will not run them until you trust them with /hooks once.',
|
|
226
|
-
options: [
|
|
227
|
-
['help', 'Show this help'],
|
|
228
|
-
[
|
|
229
|
-
'install',
|
|
230
|
-
'Install the Subconscious compaction hooks (then trust with /hooks)',
|
|
231
|
-
],
|
|
232
|
-
['status', 'Inspect the installed compaction hooks'],
|
|
233
|
-
['uninstall', 'Remove only the Subconscious Codex hooks'],
|
|
234
|
-
['--model MODEL', 'Override the profile model for this launch'],
|
|
235
|
-
['--context-window N', 'Override catalog context_window'],
|
|
236
|
-
['--max-context-window N', 'Override catalog max_context_window'],
|
|
237
|
-
[
|
|
238
|
-
'--auto-compact-token-limit N',
|
|
239
|
-
'Override the automatic compaction threshold',
|
|
240
|
-
],
|
|
241
|
-
['--reasoning-effort LEVEL', 'Use none, low, medium, high, or max'],
|
|
242
|
-
['--external-tools', 'Enable Codex apps/plugins for this launch'],
|
|
243
|
-
[
|
|
244
|
-
'--stream-idle-timeout MS',
|
|
245
|
-
'Allow a longer silent think before timing out',
|
|
246
|
-
],
|
|
247
|
-
['--max-subagents N', 'Subagents allowed to run at once (default 4)'],
|
|
248
|
-
[
|
|
249
|
-
'--subagent-effort LEVEL',
|
|
250
|
-
'Effort for subagents, separate from the parent',
|
|
251
|
-
],
|
|
252
|
-
['-- ARGS...', 'Pass remaining arguments to Codex'],
|
|
253
|
-
],
|
|
254
|
-
},
|
|
255
|
-
opencode: {
|
|
256
|
-
usage:
|
|
257
|
-
'subc [-p NAME] opencode [help|status|uninstall] [OpenCode arguments...]',
|
|
258
|
-
behavior:
|
|
259
|
-
'Launches OpenCode with an ephemeral provider containing every Subconscious model. Persistent files are leftover-only: subc opencode uninstall.',
|
|
260
|
-
options: [
|
|
261
|
-
['help', 'Show this help'],
|
|
262
|
-
['status', 'Inspect leftover OpenCode Subconscious config'],
|
|
263
|
-
[
|
|
264
|
-
'uninstall',
|
|
265
|
-
'Remove only the Subconscious OpenCode provider and plugin',
|
|
266
|
-
],
|
|
267
|
-
['--model MODEL', 'Override the profile model for this launch'],
|
|
268
|
-
['ARGS...', 'Pass arguments directly to OpenCode'],
|
|
269
|
-
],
|
|
270
|
-
},
|
|
271
|
-
cursor: {
|
|
272
|
-
usage: 'subc [-p NAME] cursor [help|install|status|uninstall]',
|
|
273
|
-
behavior:
|
|
274
|
-
'Manages Cursor correlation hooks; model endpoint setup is completed in Cursor Settings.',
|
|
275
|
-
options: [
|
|
276
|
-
['help', 'Show this help'],
|
|
277
|
-
['install', 'Install or update the Cursor hooks (default action)'],
|
|
278
|
-
['status', 'Inspect the installed hook configuration'],
|
|
279
|
-
['uninstall', 'Remove only the Subconscious Cursor hooks'],
|
|
280
|
-
],
|
|
281
|
-
},
|
|
282
|
-
copilot: {
|
|
283
|
-
usage: 'subc [-p NAME] copilot [help|install|status|uninstall]',
|
|
284
|
-
behavior:
|
|
285
|
-
'Manages the VS Code model provider and Copilot correlation hooks.',
|
|
286
|
-
options: [
|
|
287
|
-
['help', 'Show this help'],
|
|
288
|
-
['install', 'Install or update the provider and hooks (default action)'],
|
|
289
|
-
['status', 'Inspect the installed provider and hooks'],
|
|
290
|
-
['uninstall', 'Remove the Subconscious provider and hooks'],
|
|
291
|
-
],
|
|
292
|
-
},
|
|
293
|
-
pi: {
|
|
294
|
-
usage:
|
|
295
|
-
'subc [-p NAME] pi [help|install|status|uninstall] [Pi arguments...]',
|
|
296
|
-
behavior:
|
|
297
|
-
'Refreshes the persistent Subconscious provider from the live catalog, then launches Pi.',
|
|
298
|
-
options: [
|
|
299
|
-
['help', 'Show this help'],
|
|
300
|
-
['install', 'Refresh the Subconscious provider without launching Pi'],
|
|
301
|
-
['status', 'Inspect the persistent Pi provider'],
|
|
302
|
-
['uninstall', 'Remove only the Subconscious Pi provider and extension'],
|
|
303
|
-
['--model MODEL', 'Override the profile model for this launch'],
|
|
304
|
-
['ARGS...', 'Pass arguments directly to Pi'],
|
|
305
|
-
],
|
|
306
|
-
},
|
|
307
|
-
'deepseek-harness': {
|
|
308
|
-
usage:
|
|
309
|
-
'subc [-p NAME] dsh [web|headless] [--model MODEL] [Harness arguments...]',
|
|
310
|
-
behavior:
|
|
311
|
-
'Launches DeepSeek Harness with a temporary Subconscious provider containing every live gateway model. Web mode is the default.',
|
|
312
|
-
options: [
|
|
313
|
-
['help', 'Show this help'],
|
|
314
|
-
['web', 'Launch the DeepSeek Harness Web UI (default)'],
|
|
315
|
-
['headless PROMPT', 'Run one headless task and exit'],
|
|
316
|
-
['--model MODEL', 'Set the initial model for new Harness sessions'],
|
|
317
|
-
['ARGS...', 'Pass remaining arguments to the selected Harness mode'],
|
|
318
|
-
],
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
export function isAgentHelpRequest(argv = []) {
|
|
323
|
-
return ['help', '-h', '--help'].includes(argv[0]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function displayProfileValue(setting, value, values) {
|
|
327
|
-
if (setting.type === 'secret') {
|
|
328
|
-
if (value) return '(set)';
|
|
329
|
-
return setting.key !== 'API_KEY' && values.API_KEY
|
|
330
|
-
? '(shared key)'
|
|
331
|
-
: '(not set)';
|
|
332
|
-
}
|
|
333
|
-
if (
|
|
334
|
-
!value &&
|
|
335
|
-
(setting.key === 'MODEL' || setting.key === 'CLAUDE_CODE_SUBAGENT_MODEL')
|
|
336
|
-
) {
|
|
337
|
-
return 'UNSET';
|
|
338
|
-
}
|
|
339
|
-
return value || '(auto)';
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export function printAgentHelp(agent, profile) {
|
|
343
|
-
const details = AGENT_HELP[agent.id] || {
|
|
344
|
-
usage: `subc [--profile NAME] ${agent.command || agent.id} [arguments...]`,
|
|
345
|
-
behavior: agent.description,
|
|
346
|
-
options: [],
|
|
347
|
-
};
|
|
348
|
-
const settings = profileSettingsForAgent(agent.id);
|
|
349
|
-
const values = resolvedProfileValues(profile);
|
|
350
|
-
const optionWidth = Math.max(
|
|
351
|
-
0,
|
|
352
|
-
...details.options.map(([option]) => option.length),
|
|
353
|
-
);
|
|
354
|
-
const settingWidth = Math.max(
|
|
355
|
-
0,
|
|
356
|
-
...settings.map((setting) => setting.key.length),
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
console.log(`\n ${c.bold}${agent.name} + Subconscious${c.reset}\n`);
|
|
360
|
-
console.log(` ${details.behavior}\n`);
|
|
361
|
-
console.log(` ${c.bold}Usage${c.reset}\n ${details.usage}\n`);
|
|
362
|
-
if (details.options.length) {
|
|
363
|
-
console.log(` ${c.bold}Commands and options${c.reset}`);
|
|
364
|
-
for (const [option, description] of details.options) {
|
|
365
|
-
console.log(
|
|
366
|
-
` ${c.cyan}${option.padEnd(optionWidth)}${c.reset} ${description}`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
console.log();
|
|
370
|
-
}
|
|
371
|
-
console.log(
|
|
372
|
-
` ${c.bold}Profile settings${c.reset} ${c.dim}(${profile?.name || 'default'})${c.reset}`,
|
|
373
|
-
);
|
|
374
|
-
for (const setting of settings) {
|
|
375
|
-
const value = displayProfileValue(setting, values[setting.key], values);
|
|
376
|
-
console.log(
|
|
377
|
-
` ${c.cyan}${setting.key.padEnd(settingWidth)}${c.reset} ${value}`,
|
|
378
|
-
);
|
|
379
|
-
console.log(
|
|
380
|
-
` ${' '.repeat(settingWidth)} ${c.dim}${setting.description}${c.reset}`,
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
const command = agentCommandName(agent);
|
|
384
|
-
const profileFlag = profile?.name || 'default';
|
|
385
|
-
console.log(
|
|
386
|
-
`\n Edit the env file with ${c.cyan}subc -p ${profileFlag} config edit${c.reset}.`,
|
|
387
|
-
);
|
|
388
|
-
const actions = agentSetupActions(agent);
|
|
389
|
-
if (actions.includes('install')) {
|
|
390
|
-
const installDescription = agent.runbook?.binaryInstallScript
|
|
391
|
-
? 'Install the agent binary with'
|
|
392
|
-
: 'Install the persistent integration with';
|
|
393
|
-
console.log(
|
|
394
|
-
` ${installDescription} ${c.cyan}subc ${command} install${c.reset}.`,
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
if (actions.includes('uninstall')) {
|
|
398
|
-
console.log(
|
|
399
|
-
` Remove it with ${c.cyan}subc ${command} uninstall${c.reset}.`,
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
console.log();
|
|
131
|
+
/** True when argv asks for a headless run; throws when that run is invalid. */
|
|
132
|
+
export function isHeadlessRequest(agent, argv = []) {
|
|
133
|
+
const prompt = headlessPromptIndex(argv);
|
|
134
|
+
if (prompt < 0) return false;
|
|
135
|
+
parseAgentAction(agent, argv.slice(prompt - 1));
|
|
136
|
+
return true;
|
|
403
137
|
}
|
|
404
138
|
|
|
405
139
|
/**
|
|
406
|
-
* Resolve the
|
|
407
|
-
*
|
|
408
|
-
* baseUrl — SUBCONSCIOUS_BASE_URL → registry default
|
|
409
|
-
* baseUrlV1 — `${baseUrl}/v1` (so an override flows to both)
|
|
140
|
+
* Resolve the gateway for a launch: SUBCONSCIOUS_BASE_URL, then the profile
|
|
141
|
+
* GATEWAY_URL, then the packaged default, without trailing slashes.
|
|
410
142
|
*/
|
|
411
143
|
function buildContext(apiKey, model, profile) {
|
|
412
144
|
const baseUrl = (
|
|
@@ -414,7 +146,7 @@ function buildContext(apiKey, model, profile) {
|
|
|
414
146
|
profile?.values?.GATEWAY_URL?.trim() ||
|
|
415
147
|
DEFAULTS.baseUrl
|
|
416
148
|
).replace(/\/+$/, '');
|
|
417
|
-
return { apiKey, model, baseUrl
|
|
149
|
+
return { apiKey, model, baseUrl };
|
|
418
150
|
}
|
|
419
151
|
|
|
420
152
|
/**
|
|
@@ -433,12 +165,18 @@ export function extractModel(argv, profile) {
|
|
|
433
165
|
? 'profile'
|
|
434
166
|
: 'catalog';
|
|
435
167
|
const rest = [];
|
|
168
|
+
// The headless prompt is opaque, even when it looks like --model.
|
|
169
|
+
const promptIndex = headlessPromptIndex(argv);
|
|
436
170
|
for (let i = 0; i < argv.length; i++) {
|
|
437
171
|
const a = argv[i];
|
|
172
|
+
if (i === promptIndex) {
|
|
173
|
+
rest.push(a);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
438
176
|
if (a === '--model') {
|
|
439
177
|
const v = argv[i + 1];
|
|
440
|
-
if (v
|
|
441
|
-
if (isUnsetSetting(v)) {
|
|
178
|
+
if (takesModelValue(v)) {
|
|
179
|
+
if (v === '' || isUnsetSetting(v)) {
|
|
442
180
|
model = '';
|
|
443
181
|
modelSource = 'catalog';
|
|
444
182
|
} else {
|
|
@@ -465,363 +203,6 @@ export function extractModel(argv, profile) {
|
|
|
465
203
|
return { model, modelSource, rest };
|
|
466
204
|
}
|
|
467
205
|
|
|
468
|
-
/**
|
|
469
|
-
* Common locations a freshly-installed coding-agent binary lands in but which
|
|
470
|
-
* are often NOT on the current process's PATH (e.g. aider/claude install into
|
|
471
|
-
* `~/.local/bin`; npm globals into the npm prefix bin). Best-effort, deduped.
|
|
472
|
-
*/
|
|
473
|
-
function candidateBinDirs() {
|
|
474
|
-
const home = os.homedir();
|
|
475
|
-
const dirs = [];
|
|
476
|
-
|
|
477
|
-
if (process.platform === 'win32') {
|
|
478
|
-
if (process.env.APPDATA) dirs.push(path.join(process.env.APPDATA, 'npm'));
|
|
479
|
-
if (process.env.USERPROFILE) {
|
|
480
|
-
dirs.push(path.join(process.env.USERPROFILE, '.local', 'bin'));
|
|
481
|
-
}
|
|
482
|
-
if (home) dirs.push(path.join(home, '.local', 'bin'));
|
|
483
|
-
} else {
|
|
484
|
-
dirs.push(path.join(home, '.local', 'bin'));
|
|
485
|
-
dirs.push(path.join(home, '.cargo', 'bin'));
|
|
486
|
-
dirs.push('/opt/homebrew/bin');
|
|
487
|
-
dirs.push('/usr/local/bin');
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// npm global bin (best-effort — npm may be absent).
|
|
491
|
-
try {
|
|
492
|
-
const prefix = execFileSync('npm', ['prefix', '-g'], {
|
|
493
|
-
encoding: 'utf-8',
|
|
494
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
495
|
-
}).trim();
|
|
496
|
-
if (prefix) {
|
|
497
|
-
dirs.push(
|
|
498
|
-
process.platform === 'win32' ? prefix : path.join(prefix, 'bin'),
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
} catch {
|
|
502
|
-
// npm not available — skip.
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Dedupe, drop empties.
|
|
506
|
-
return [...new Set(dirs.filter(Boolean))];
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** Resolve an agent-managed binary directory declared by the registry. */
|
|
510
|
-
export function preferredBinDirsForAgent(
|
|
511
|
-
agent,
|
|
512
|
-
environment = process.env,
|
|
513
|
-
home = os.homedir(),
|
|
514
|
-
) {
|
|
515
|
-
const envName = agent.runbook?.installDirEnv;
|
|
516
|
-
const configured = envName ? environment[envName]?.trim() : '';
|
|
517
|
-
if (configured) return [path.resolve(configured)];
|
|
518
|
-
|
|
519
|
-
const installDir = agent.runbook?.installDir?.trim();
|
|
520
|
-
return installDir ? [path.resolve(home, installDir)] : [];
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/** Executable extensions to probe (Windows uses PATHEXT). */
|
|
524
|
-
function binExts() {
|
|
525
|
-
return process.platform === 'win32'
|
|
526
|
-
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
527
|
-
: [''];
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Resolve `bin` against PATH plus the candidate bin dirs. Returns the directory
|
|
532
|
-
* containing the executable if found, otherwise null. Searching the candidate
|
|
533
|
-
* dirs lets us find binaries installed this session that aren't on PATH yet.
|
|
534
|
-
*/
|
|
535
|
-
async function resolveBinPath(bin, preferredDirs = []) {
|
|
536
|
-
const pathDirs = (process.env.PATH || '')
|
|
537
|
-
.split(path.delimiter)
|
|
538
|
-
.filter(Boolean);
|
|
539
|
-
const dirs = [
|
|
540
|
-
...new Set([...preferredDirs, ...pathDirs, ...candidateBinDirs()]),
|
|
541
|
-
];
|
|
542
|
-
const exts = binExts();
|
|
543
|
-
for (const dir of dirs) {
|
|
544
|
-
for (const ext of exts) {
|
|
545
|
-
const candidate = path.join(dir, bin + ext);
|
|
546
|
-
try {
|
|
547
|
-
await fs.access(candidate, fsConstants.F_OK);
|
|
548
|
-
return dir;
|
|
549
|
-
} catch {
|
|
550
|
-
// keep scanning
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return null;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Build a PATH string with `extraDirs` prepended (deduped against PATH).
|
|
559
|
-
* Returns the augmented PATH value for use in a child env.
|
|
560
|
-
*/
|
|
561
|
-
export function augmentPath(
|
|
562
|
-
extraDirs,
|
|
563
|
-
preferredDir,
|
|
564
|
-
currentPath = process.env.PATH || '',
|
|
565
|
-
) {
|
|
566
|
-
const current = currentPath.split(path.delimiter).filter(Boolean);
|
|
567
|
-
const seen = new Set(current);
|
|
568
|
-
const prepend = extraDirs.filter(
|
|
569
|
-
(dir, index) =>
|
|
570
|
-
dir &&
|
|
571
|
-
dir !== preferredDir &&
|
|
572
|
-
!seen.has(dir) &&
|
|
573
|
-
extraDirs.indexOf(dir) === index,
|
|
574
|
-
);
|
|
575
|
-
const remaining = current.filter((dir) => dir !== preferredDir);
|
|
576
|
-
return [
|
|
577
|
-
...(preferredDir ? [preferredDir] : []),
|
|
578
|
-
...prepend,
|
|
579
|
-
...remaining,
|
|
580
|
-
].join(path.delimiter);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/** Ask a yes/no question on the TTY. Empty answer counts as yes. */
|
|
584
|
-
function askYesNo(question) {
|
|
585
|
-
return new Promise((resolve) => {
|
|
586
|
-
const rl = readline.createInterface({
|
|
587
|
-
input: process.stdin,
|
|
588
|
-
output: process.stdout,
|
|
589
|
-
});
|
|
590
|
-
rl.question(question, (answer) => {
|
|
591
|
-
rl.close();
|
|
592
|
-
const a = answer.trim().toLowerCase();
|
|
593
|
-
resolve(a === '' || a === 'y' || a === 'yes');
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function waitForEnter(prompt) {
|
|
599
|
-
return new Promise((resolve) => {
|
|
600
|
-
const rl = readline.createInterface({
|
|
601
|
-
input: process.stdin,
|
|
602
|
-
output: process.stdout,
|
|
603
|
-
});
|
|
604
|
-
rl.question(prompt, () => {
|
|
605
|
-
rl.close();
|
|
606
|
-
resolve();
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/** Run the agent's packaged binary installer, or its shell install command. */
|
|
612
|
-
function runInstaller(
|
|
613
|
-
agent,
|
|
614
|
-
install = agent.install,
|
|
615
|
-
usePackagedScript = true,
|
|
616
|
-
) {
|
|
617
|
-
return new Promise((resolve) => {
|
|
618
|
-
const installScript =
|
|
619
|
-
usePackagedScript && agent.runbook?.binaryInstallScript;
|
|
620
|
-
const child = installScript
|
|
621
|
-
? spawn('bash', [runbookScriptPath(agent, installScript), 'install'], {
|
|
622
|
-
stdio: 'inherit',
|
|
623
|
-
})
|
|
624
|
-
: spawn(install, { shell: true, stdio: 'inherit' });
|
|
625
|
-
child.on('error', () => resolve(false));
|
|
626
|
-
child.on('exit', (code) => resolve(code === 0));
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/** Print the resolved install command (plus any fallback) for an agent. */
|
|
631
|
-
function printInstallCommands(agent) {
|
|
632
|
-
console.error(` ${c.cyan}${agent.install}${c.reset}`);
|
|
633
|
-
if (agent.installFallback) {
|
|
634
|
-
console.error(` ${c.dim}or, as a fallback:${c.reset}`);
|
|
635
|
-
console.error(` ${c.cyan}${agent.installFallback}${c.reset}`);
|
|
636
|
-
}
|
|
637
|
-
console.error('');
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Ensure the agent's binary is resolvable. If missing:
|
|
642
|
-
* - interactive TTY: offer to run the per-OS installer (with fallback), then
|
|
643
|
-
* re-resolve against PATH + candidate dirs.
|
|
644
|
-
* - non-interactive: print the resolved install command (+ fallback) and
|
|
645
|
-
* exit 127 without running anything.
|
|
646
|
-
*
|
|
647
|
-
* Returns the directory containing the bin (to prepend to the child's PATH) on
|
|
648
|
-
* success. May exit the process on failure or when manual action is needed.
|
|
649
|
-
*/
|
|
650
|
-
async function ensureInstalled(agent) {
|
|
651
|
-
const preferredDirs = preferredBinDirsForAgent(agent);
|
|
652
|
-
const existing = await resolveBinPath(agent.bin, preferredDirs);
|
|
653
|
-
if (existing) return existing;
|
|
654
|
-
|
|
655
|
-
// Agents without an installer are launch-only. Their setup integration may
|
|
656
|
-
// configure the provider, but `subc <agent>` must never install the binary.
|
|
657
|
-
if (!agent.install) {
|
|
658
|
-
console.error(
|
|
659
|
-
`\n ${c.red}${agent.name} isn't installed${c.reset} ${c.dim}(\`${agent.bin}\` not found on PATH).${c.reset}`,
|
|
660
|
-
);
|
|
661
|
-
console.error(
|
|
662
|
-
` Install ${agent.name} separately, then re-run ${c.cyan}subc ${agent.command || agent.id}${c.reset}.\n`,
|
|
663
|
-
);
|
|
664
|
-
process.exit(127);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
668
|
-
|
|
669
|
-
if (!interactive) {
|
|
670
|
-
console.error(
|
|
671
|
-
`\n ${c.red}${agent.name} isn't installed${c.reset} ${c.dim}(\`${agent.bin}\` not found on PATH).${c.reset}`,
|
|
672
|
-
);
|
|
673
|
-
console.error(` Install it with:\n`);
|
|
674
|
-
printInstallCommands(agent);
|
|
675
|
-
process.exit(127);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
console.error(`\n ${c.bold}${agent.name}${c.reset} isn't installed.`);
|
|
679
|
-
const ok = await askYesNo(` Install it now? ${c.dim}[Y/n]${c.reset} `);
|
|
680
|
-
if (!ok) {
|
|
681
|
-
console.error(`\n No problem. Install it yourself with:\n`);
|
|
682
|
-
printInstallCommands(agent);
|
|
683
|
-
process.exit(127);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
console.error(
|
|
687
|
-
`\n ${c.dim}Running ${c.reset}${c.cyan}${agent.install}${c.reset}\n`,
|
|
688
|
-
);
|
|
689
|
-
let installed = await runInstaller(agent);
|
|
690
|
-
|
|
691
|
-
// Primary failed and a fallback exists — try it once.
|
|
692
|
-
if (!installed && agent.installFallback) {
|
|
693
|
-
console.error(
|
|
694
|
-
`\n ${c.dim}That didn't work. Trying the fallback: ${c.reset}${c.cyan}${agent.installFallback}${c.reset}\n`,
|
|
695
|
-
);
|
|
696
|
-
installed = await runInstaller(agent, agent.installFallback, false);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
if (!installed) {
|
|
700
|
-
console.error(`\n ${c.red}Install failed.${c.reset} Try it manually:\n`);
|
|
701
|
-
printInstallCommands(agent);
|
|
702
|
-
process.exit(127);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// PATH hardening: the freshly-installed binary is often not on the current
|
|
706
|
-
// process's PATH. Re-resolve against PATH + candidate dirs.
|
|
707
|
-
const found = await resolveBinPath(agent.bin, preferredDirs);
|
|
708
|
-
if (found) return found;
|
|
709
|
-
|
|
710
|
-
console.error(
|
|
711
|
-
`\n ${c.dim}Installed ${agent.name}, but it isn't on this shell's PATH yet. ` +
|
|
712
|
-
`Open a new terminal (or add a bin dir to PATH) and re-run \`subc ${agent.command || agent.id}\`.${c.reset}\n`,
|
|
713
|
-
);
|
|
714
|
-
process.exit(0);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
export function parseClaudeVersion(text) {
|
|
718
|
-
const match = String(text ?? '').match(/v?(\d+\.\d+\.\d+)/);
|
|
719
|
-
return match?.[1] ?? null;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
export function claudeVersionNeedsUpgrade(
|
|
723
|
-
installed,
|
|
724
|
-
minimum = MIN_CLAUDE_CODE_VERSION,
|
|
725
|
-
) {
|
|
726
|
-
return versionNeedsUpgrade(installed, minimum);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
export function versionNeedsUpgrade(installed, minimum) {
|
|
730
|
-
return Boolean(
|
|
731
|
-
installed && minimum && compareVersions(installed, minimum) < 0,
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
export function readClaudeVersion(bin, binDir, options = {}) {
|
|
736
|
-
const execFile = options.execFileSync || execFileSync;
|
|
737
|
-
try {
|
|
738
|
-
const stdout = execFile(bin, ['--version'], {
|
|
739
|
-
encoding: 'utf-8',
|
|
740
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
741
|
-
env: {
|
|
742
|
-
...process.env,
|
|
743
|
-
PATH: augmentPath(
|
|
744
|
-
[binDir, ...candidateBinDirs()].filter(Boolean),
|
|
745
|
-
binDir,
|
|
746
|
-
),
|
|
747
|
-
},
|
|
748
|
-
timeout: options.timeoutMs ?? 3000,
|
|
749
|
-
});
|
|
750
|
-
return parseClaudeVersion(
|
|
751
|
-
typeof stdout === 'string' ? stdout : stdout?.toString?.(),
|
|
752
|
-
);
|
|
753
|
-
} catch {
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const MINIMUM_VERSIONS = {
|
|
759
|
-
'claude-code': { minimum: MIN_CLAUDE_CODE_VERSION, label: 'Claude Code' },
|
|
760
|
-
// Below this, subagents are silently absent rather than broken.
|
|
761
|
-
codex: { minimum: MIN_CODEX_VERSION, label: 'Codex' },
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
async function ensureClaudeCompatible(agent, binDir) {
|
|
765
|
-
const requirement = MINIMUM_VERSIONS[agent.id];
|
|
766
|
-
if (!requirement) return;
|
|
767
|
-
const version = readClaudeVersion(agent.bin, binDir);
|
|
768
|
-
if (!versionNeedsUpgrade(version, requirement.minimum)) return;
|
|
769
|
-
|
|
770
|
-
console.error(
|
|
771
|
-
`\n Minimum supported ${requirement.label} version is ${requirement.minimum}. Your version is ${version}. Upgrade to get the best experience.\n`,
|
|
772
|
-
);
|
|
773
|
-
console.error(` Upgrade it with:`);
|
|
774
|
-
console.error(` ${c.cyan}${agent.install}${c.reset}`);
|
|
775
|
-
if (agent.installFallback) {
|
|
776
|
-
console.error(` or`);
|
|
777
|
-
console.error(` ${c.cyan}${agent.installFallback}${c.reset}`);
|
|
778
|
-
}
|
|
779
|
-
console.error('');
|
|
780
|
-
if (process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
781
|
-
await waitForEnter(` ${c.dim}Press Enter to continue.${c.reset} `);
|
|
782
|
-
console.error('');
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
/** Resolve and validate a script inside the packaged runbook directory. */
|
|
787
|
-
function runbookScriptPath(agent, relativeScript = agent.runbook.script) {
|
|
788
|
-
const script = path.resolve(RUNBOOK_DIR, relativeScript);
|
|
789
|
-
const relative = path.relative(RUNBOOK_DIR, script);
|
|
790
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
791
|
-
throw new Error(`Invalid runbook script path for ${agent.name}`);
|
|
792
|
-
}
|
|
793
|
-
return script;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/** Spawn a runbook script and mirror its exit status/signals. */
|
|
797
|
-
function spawnRunbook(agent, args, env, relativeScript) {
|
|
798
|
-
return new Promise((resolve, reject) => {
|
|
799
|
-
const script = runbookScriptPath(agent, relativeScript);
|
|
800
|
-
const child = spawn('bash', [script, ...args], { stdio: 'inherit', env });
|
|
801
|
-
|
|
802
|
-
child.on('error', (error) => {
|
|
803
|
-
if (error.code === 'ENOENT') {
|
|
804
|
-
reject(
|
|
805
|
-
new Error(
|
|
806
|
-
'These coding-agent integrations require `bash`, but it was not found on PATH.',
|
|
807
|
-
),
|
|
808
|
-
);
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
reject(error);
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
child.on('exit', (code, signal) => {
|
|
815
|
-
if (signal) {
|
|
816
|
-
process.kill(process.pid, signal);
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
if (code) process.exitCode = code;
|
|
820
|
-
resolve(code ?? 0);
|
|
821
|
-
});
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
|
|
825
206
|
function isSetupWithoutAuth(argv) {
|
|
826
207
|
return ['status', 'uninstall', '-h', '--help', 'help'].includes(argv[0]);
|
|
827
208
|
}
|
|
@@ -831,15 +212,8 @@ function optionValue(argv, name) {
|
|
|
831
212
|
return index >= 0 ? argv[index + 1]?.trim() || null : null;
|
|
832
213
|
}
|
|
833
214
|
|
|
834
|
-
function agentApiKeySetting(agent) {
|
|
835
|
-
return profileSettingsForAgent(agent.id).find(
|
|
836
|
-
(setting) => setting.key !== 'API_KEY' && setting.key.endsWith('_API_KEY'),
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
215
|
export async function getAgentApiKey(profile, agent) {
|
|
841
|
-
const
|
|
842
|
-
const specificKey = specificSetting?.key;
|
|
216
|
+
const specificKey = agentKeyName(agent);
|
|
843
217
|
const specificEnvKey = specificKey && process.env[specificKey]?.trim();
|
|
844
218
|
if (specificEnvKey)
|
|
845
219
|
return { key: specificEnvKey, source: `${specificKey} env var` };
|
|
@@ -871,113 +245,79 @@ async function requireApiKey(profile, agent) {
|
|
|
871
245
|
return null;
|
|
872
246
|
}
|
|
873
247
|
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
|
|
883
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
|
884
|
-
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
885
|
-
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
|
|
886
|
-
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
|
|
887
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
888
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
|
|
889
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
|
|
890
|
-
];
|
|
891
|
-
|
|
892
|
-
const CLAUDE_MODEL_PICKER_ID_KEYS = [
|
|
893
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
894
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
895
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
896
|
-
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
897
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
898
|
-
];
|
|
899
|
-
|
|
900
|
-
function uniqueCatalogModels(models, fallbackModel) {
|
|
901
|
-
const unique = [];
|
|
902
|
-
for (const model of models) {
|
|
903
|
-
if (model && !unique.includes(model)) unique.push(model);
|
|
904
|
-
}
|
|
905
|
-
if (unique.length === 0 && fallbackModel) unique.push(fallbackModel);
|
|
906
|
-
return unique;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
export function claudePickerSettings(models, fallbackModel) {
|
|
910
|
-
const unique = uniqueCatalogModels(models, fallbackModel);
|
|
911
|
-
return {
|
|
912
|
-
availableModels: unique,
|
|
913
|
-
modelPicker: {
|
|
914
|
-
replaceBuiltInOptions: true,
|
|
915
|
-
options: unique.map((model) => ({
|
|
916
|
-
model,
|
|
917
|
-
label: model,
|
|
918
|
-
description: `Subconscious model ${model}`,
|
|
919
|
-
})),
|
|
248
|
+
export const RUNBOOK_ENV_DESCRIPTION = Object.freeze({
|
|
249
|
+
order:
|
|
250
|
+
'Later layers win: Claude picker env (claude-code only), then profile values, then process.env, then the fixed values. For claude-code, picker slots whose value is not in the live catalog are then reset to the catalog. Each agent then applies its own inputs and env entries.',
|
|
251
|
+
inherited: [
|
|
252
|
+
{
|
|
253
|
+
source: 'profile',
|
|
254
|
+
description:
|
|
255
|
+
'Every key in the selected profile env file, for example CODEX_CONTEXT_WINDOW.',
|
|
920
256
|
},
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
}
|
|
257
|
+
{
|
|
258
|
+
source: 'process.env',
|
|
259
|
+
description:
|
|
260
|
+
"The caller's whole environment. It overrides profile values.",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
fixed: [
|
|
264
|
+
{
|
|
265
|
+
name: 'GATEWAY_URL',
|
|
266
|
+
value: '{baseUrl}',
|
|
267
|
+
description:
|
|
268
|
+
'SUBCONSCIOUS_BASE_URL, then profile GATEWAY_URL, then the packaged default. Trailing slashes are removed.',
|
|
269
|
+
},
|
|
270
|
+
{ name: 'API_KEY', value: '{apiKey}', description: 'Gateway credential.' },
|
|
271
|
+
{
|
|
272
|
+
name: '<agent key>',
|
|
273
|
+
value: '{apiKey}',
|
|
274
|
+
description:
|
|
275
|
+
'The agent-specific key input (for example CODEX_API_KEY), set to the same credential.',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'MODEL',
|
|
279
|
+
value: '{model}',
|
|
280
|
+
description:
|
|
281
|
+
'--model, then SUBCONSCIOUS_MODEL, then profile MODEL; UNSET or blank uses the first catalog model. A profile or default model missing from a live catalog is replaced by the first live model.',
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'SUBCONSCIOUS_MODELS',
|
|
285
|
+
value: '{catalog}',
|
|
286
|
+
description: 'Newline-separated live model catalog.',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'PATH',
|
|
290
|
+
value: '{binDir}:<install dirs>:{PATH}',
|
|
291
|
+
description:
|
|
292
|
+
"The caller's PATH with the agent's install directory and common install directories prepended.",
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
runbook_scripts: [
|
|
296
|
+
{
|
|
297
|
+
name: 'SUBC_MODEL_IDS',
|
|
298
|
+
value: '{model} then {catalog}',
|
|
299
|
+
description: 'The launch model first, then the catalog, deduplicated.',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'SUBC_VISION_MODELS',
|
|
303
|
+
value: 'model IDs',
|
|
304
|
+
description: 'Models that accept images, newline-separated.',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'SUBC_TEMPLATE_WORDS',
|
|
308
|
+
value: 'count',
|
|
309
|
+
description:
|
|
310
|
+
'How many leading argv words came from the argv template; only those may hold {tempFile}.',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
per_harness: { 'claude-code': CLAUDE_PICKER_ENV_DESCRIPTION },
|
|
314
|
+
});
|
|
980
315
|
|
|
316
|
+
/**
|
|
317
|
+
* The env every launch and setup starts from: profile values, then the
|
|
318
|
+
* caller's env, then the resolved gateway, key, model, and catalog. Claude
|
|
319
|
+
* Code also gets its model picker, kept inside the live catalog.
|
|
320
|
+
*/
|
|
981
321
|
export function runbookEnv(
|
|
982
322
|
apiKey,
|
|
983
323
|
model,
|
|
@@ -988,8 +328,9 @@ export function runbookEnv(
|
|
|
988
328
|
) {
|
|
989
329
|
const ctx = buildContext(apiKey, model, profile);
|
|
990
330
|
const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
|
|
991
|
-
const
|
|
992
|
-
const
|
|
331
|
+
const keyName = agentKeyName(agent);
|
|
332
|
+
const claude = agent.id === CLAUDE_CODE;
|
|
333
|
+
const picker = claude ? claudeModelPickerEnv(model, models) : {};
|
|
993
334
|
return applyClaudePickerCatalog(
|
|
994
335
|
{
|
|
995
336
|
...picker,
|
|
@@ -997,19 +338,16 @@ export function runbookEnv(
|
|
|
997
338
|
...process.env,
|
|
998
339
|
GATEWAY_URL: ctx.baseUrl,
|
|
999
340
|
API_KEY: apiKey,
|
|
1000
|
-
...(
|
|
341
|
+
...(keyName ? { [keyName]: apiKey } : {}),
|
|
1001
342
|
MODEL: model,
|
|
1002
343
|
SUBCONSCIOUS_MODELS: models.join('\n'),
|
|
1003
|
-
...(
|
|
344
|
+
...(claude
|
|
1004
345
|
? {
|
|
1005
346
|
SUBC_CLAUDE_SETTINGS: JSON.stringify(
|
|
1006
347
|
claudePickerSettings(models, model),
|
|
1007
348
|
),
|
|
1008
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '0',
|
|
1009
|
-
CLAUDE_CODE_AUTO_MODE_SERVER: '0',
|
|
1010
349
|
}
|
|
1011
350
|
: {}),
|
|
1012
|
-
SUBC_ENV_FILE: os.devNull,
|
|
1013
351
|
PATH: augmentPath(extraDirs, binDir),
|
|
1014
352
|
},
|
|
1015
353
|
picker,
|
|
@@ -1049,23 +387,15 @@ export function selectLaunchModel(requestedModel, modelSource, catalog) {
|
|
|
1049
387
|
return useLiveDefault ? first : requestedModel;
|
|
1050
388
|
}
|
|
1051
389
|
|
|
1052
|
-
async function runRunbookSetup(
|
|
1053
|
-
agent
|
|
1054
|
-
argv
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
argv,
|
|
1062
|
-
{
|
|
1063
|
-
...(profile?.values || {}),
|
|
1064
|
-
...process.env,
|
|
1065
|
-
SUBC_ENV_FILE: os.devNull,
|
|
1066
|
-
},
|
|
1067
|
-
relativeScript,
|
|
1068
|
-
);
|
|
390
|
+
async function runRunbookSetup(agent, argv, profile) {
|
|
391
|
+
const script = agent.runbook.setup_script;
|
|
392
|
+
if (isSetupWithoutAuth(argv) || agent.runbook.setup_needs_auth === false) {
|
|
393
|
+
// Status, uninstall, and Marathon's binary install read no model or
|
|
394
|
+
// agent setting, so an invalid saved one must not block them.
|
|
395
|
+
const lenient = { strict: false };
|
|
396
|
+
const base = { ...(profile?.values || {}), ...process.env };
|
|
397
|
+
const env = resolveInputs(agent, base, {}, lenient);
|
|
398
|
+
return runCommand(scriptCommand(agent, script, argv, env, lenient));
|
|
1069
399
|
}
|
|
1070
400
|
|
|
1071
401
|
const {
|
|
@@ -1087,29 +417,56 @@ async function runRunbookSetup(
|
|
|
1087
417
|
` ${c.yellow}Configured model ${requestedModel} is not in the live catalog; using ${model}.${c.reset}\n`,
|
|
1088
418
|
);
|
|
1089
419
|
}
|
|
1090
|
-
const ctx = buildContext(apiKey, model, profile);
|
|
1091
|
-
const authArgs = substitute(agent.runbook.authArgs || [], ctx);
|
|
1092
420
|
|
|
1093
421
|
console.log(
|
|
1094
422
|
` ${c.dim}Configuring ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}for Subconscious (${model})${c.reset}\n`,
|
|
1095
423
|
);
|
|
1096
|
-
|
|
424
|
+
// Setup scripts read no strict-choice setting; only the model list matters.
|
|
425
|
+
const env = resolveInputs(
|
|
1097
426
|
agent,
|
|
1098
|
-
[...authArgs, ...rest],
|
|
1099
427
|
runbookEnv(apiKey, model, undefined, profile, agent, catalog.models),
|
|
1100
|
-
|
|
428
|
+
{},
|
|
429
|
+
{ strict: false },
|
|
1101
430
|
);
|
|
431
|
+
const code = await runCommand(scriptCommand(agent, script, rest, env));
|
|
1102
432
|
const installed = !['status', 'uninstall'].includes(rest[0]);
|
|
1103
433
|
if (code !== 0 || !installed) return code;
|
|
1104
434
|
|
|
1105
|
-
if (agent.
|
|
1106
|
-
console.log(
|
|
1107
|
-
|
|
1108
|
-
|
|
435
|
+
if (agent.runbook.after_install) {
|
|
436
|
+
console.log(`\n ${c.dim}${agent.runbook.after_install}${c.reset}\n`);
|
|
437
|
+
}
|
|
438
|
+
return code;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function runSetupAction(agent, parsed, profile) {
|
|
442
|
+
if (!agent.runbook.setup_script) {
|
|
443
|
+
throw new Error(`No persistent integration is available for ${agent.name}`);
|
|
444
|
+
}
|
|
445
|
+
const setupArgs =
|
|
446
|
+
parsed.args[0] === parsed.action
|
|
447
|
+
? parsed.args
|
|
448
|
+
: [parsed.action, ...parsed.args];
|
|
449
|
+
const code = await runRunbookSetup(agent, setupArgs, profile);
|
|
450
|
+
if (code === 0) {
|
|
451
|
+
const message =
|
|
452
|
+
parsed.action === 'status'
|
|
453
|
+
? `${agent.name} status check complete.`
|
|
454
|
+
: parsed.action === 'uninstall'
|
|
455
|
+
? `${agent.name} integration removed.`
|
|
456
|
+
: `${agent.name} setup complete.`;
|
|
457
|
+
console.log(`\n ${c.green}${c.bold}✓ ${message}${c.reset}\n`);
|
|
1109
458
|
}
|
|
1110
459
|
return code;
|
|
1111
460
|
}
|
|
1112
461
|
|
|
462
|
+
/** Split `headless PROMPT args...` from the words after subc's own flags. */
|
|
463
|
+
function headlessParts(rest) {
|
|
464
|
+
const index = headlessPromptIndex(rest);
|
|
465
|
+
return index < 0
|
|
466
|
+
? { args: rest, prompt: undefined }
|
|
467
|
+
: { args: rest.slice(index + 1), prompt: rest[index] };
|
|
468
|
+
}
|
|
469
|
+
|
|
1113
470
|
/**
|
|
1114
471
|
* Launch a coding agent against Subconscious. `argv` is everything after the
|
|
1115
472
|
* agent name; unknown flags pass straight through to the underlying CLI.
|
|
@@ -1121,6 +478,13 @@ export async function runAgent(agent, argv, options = {}) {
|
|
|
1121
478
|
return 0;
|
|
1122
479
|
}
|
|
1123
480
|
|
|
481
|
+
const headless = isHeadlessRequest(agent, argv);
|
|
482
|
+
if (headless && !agent.launch.headless_platforms.includes(process.platform)) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`${agent.name} headless mode is not available on ${process.platform === 'win32' ? 'Windows' : process.platform} yet.`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
1124
488
|
if (process.platform === 'win32') {
|
|
1125
489
|
return runWindowsAgent(agent, argv, {
|
|
1126
490
|
profile,
|
|
@@ -1131,50 +495,35 @@ export async function runAgent(agent, argv, options = {}) {
|
|
|
1131
495
|
selectLaunchModel,
|
|
1132
496
|
runbookEnv,
|
|
1133
497
|
minimumClaudeVersion: MIN_CLAUDE_CODE_VERSION,
|
|
498
|
+
headless,
|
|
1134
499
|
parseClaudeVersion,
|
|
1135
500
|
claudeVersionNeedsUpgrade,
|
|
1136
501
|
});
|
|
1137
502
|
}
|
|
1138
503
|
|
|
1139
504
|
const parsed = parseAgentAction(agent, argv);
|
|
1140
|
-
if (parsed.action !== 'launch') {
|
|
1141
|
-
|
|
1142
|
-
throw new Error(
|
|
1143
|
-
`No persistent integration is available for ${agent.name}`,
|
|
1144
|
-
);
|
|
1145
|
-
}
|
|
1146
|
-
const setupArgs =
|
|
1147
|
-
parsed.args[0] === parsed.action
|
|
1148
|
-
? parsed.args
|
|
1149
|
-
: [parsed.action, ...parsed.args];
|
|
1150
|
-
const code = await runRunbookSetup(
|
|
1151
|
-
agent,
|
|
1152
|
-
setupArgs,
|
|
1153
|
-
profile,
|
|
1154
|
-
agent.runbook.setupScript,
|
|
1155
|
-
);
|
|
1156
|
-
if (code === 0) {
|
|
1157
|
-
const message =
|
|
1158
|
-
parsed.action === 'status'
|
|
1159
|
-
? `${agent.name} status check complete.`
|
|
1160
|
-
: parsed.action === 'uninstall'
|
|
1161
|
-
? `${agent.name} integration removed.`
|
|
1162
|
-
: `${agent.name} setup complete.`;
|
|
1163
|
-
console.log(`\n ${c.green}${c.bold}✓ ${message}${c.reset}\n`);
|
|
1164
|
-
}
|
|
1165
|
-
return code;
|
|
505
|
+
if (parsed.action !== 'launch' && !headless) {
|
|
506
|
+
return runSetupAction(agent, parsed, profile);
|
|
1166
507
|
}
|
|
1167
508
|
|
|
509
|
+
return launchAgent(agent, argv, profile, headless);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function launchAgent(agent, argv, profile, headless) {
|
|
513
|
+
// Arguments after -- belong to the agent, including its own --model.
|
|
514
|
+
const boundary = separatorIndex(argv);
|
|
1168
515
|
const {
|
|
1169
516
|
model: requestedModel,
|
|
1170
517
|
modelSource,
|
|
1171
|
-
rest,
|
|
1172
|
-
} = extractModel(argv, profile);
|
|
518
|
+
rest: subcRest,
|
|
519
|
+
} = extractModel(boundary < 0 ? argv : argv.slice(0, boundary), profile);
|
|
520
|
+
const rest = boundary < 0 ? subcRest : [...subcRest, ...argv.slice(boundary)];
|
|
1173
521
|
const apiKey = await requireApiKey(profile, agent);
|
|
1174
522
|
if (!apiKey) return 1;
|
|
1175
523
|
|
|
1176
|
-
const
|
|
1177
|
-
await
|
|
524
|
+
const mayPrompt = !headless;
|
|
525
|
+
const binDir = await ensureInstalled(agent, { mayPrompt });
|
|
526
|
+
await ensureMinimumVersion(agent, binDir, { mayPrompt });
|
|
1178
527
|
const catalog = await resolvedModelsForLaunch(
|
|
1179
528
|
profile,
|
|
1180
529
|
apiKey,
|
|
@@ -1187,58 +536,13 @@ export async function runAgent(agent, argv, options = {}) {
|
|
|
1187
536
|
);
|
|
1188
537
|
}
|
|
1189
538
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
1193
|
-
);
|
|
1194
|
-
return spawnRunbook(
|
|
1195
|
-
agent,
|
|
1196
|
-
rest,
|
|
1197
|
-
runbookEnv(apiKey, model, binDir, profile, agent, catalog.models),
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const ctx = buildContext(apiKey, model, profile);
|
|
1202
|
-
const launch = substituteString(agent.launch, ctx);
|
|
1203
|
-
const [bin, ...launchArgs] = launch.split(' ').filter(Boolean);
|
|
1204
|
-
const envMap = substitute(agent.env, ctx);
|
|
1205
|
-
|
|
1206
|
-
// Prepend the resolved bin dir + candidate dirs to the child's PATH so the
|
|
1207
|
-
// agent (and any subprocess it spawns) resolves correctly this session, even
|
|
1208
|
-
// if it was installed into a dir not yet on the parent shell's PATH.
|
|
1209
|
-
const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
|
|
1210
|
-
const env = {
|
|
1211
|
-
...envMap,
|
|
1212
|
-
...(profile?.values || {}),
|
|
1213
|
-
...process.env,
|
|
1214
|
-
PATH: augmentPath(extraDirs, binDir),
|
|
1215
|
-
};
|
|
1216
|
-
const args = [...launchArgs, ...rest];
|
|
1217
|
-
|
|
1218
|
-
console.log(
|
|
539
|
+
// Headless stdout belongs to the harness alone.
|
|
540
|
+
(headless ? console.error : console.log)(
|
|
1219
541
|
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
1220
542
|
);
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
console.error(
|
|
1227
|
-
`\n ${c.red}Could not launch \`${bin}\`.${c.reset} Install it with:\n`,
|
|
1228
|
-
);
|
|
1229
|
-
printInstallCommands(agent);
|
|
1230
|
-
process.exit(127);
|
|
1231
|
-
}
|
|
1232
|
-
console.error(`\n ${c.red}${err.message}${c.reset}\n`);
|
|
1233
|
-
process.exit(1);
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
// Mirror the child's exit status so callers/scripts see the real result.
|
|
1237
|
-
child.on('exit', (code, signal) => {
|
|
1238
|
-
if (signal) {
|
|
1239
|
-
process.kill(process.pid, signal);
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
process.exit(code ?? 0);
|
|
1243
|
-
});
|
|
543
|
+
const env = runbookEnv(apiKey, model, binDir, profile, agent, catalog.models);
|
|
544
|
+
// The headless endpoint is the one endpoint for every harness, Claude too.
|
|
545
|
+
const launchEnv = headless ? { ...env, CLAUDE_GATEWAY_URL: '' } : env;
|
|
546
|
+
const plan = launchPlan(agent, { env: launchEnv, ...headlessParts(rest) });
|
|
547
|
+
return runCommand(planCommand(agent, plan, binDir));
|
|
1244
548
|
}
|