subconscious-cli 6.0.1 → 6.2.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 +167 -43
- package/bin/agent-data.js +278 -0
- package/bin/agent-help.js +114 -0
- package/bin/agent-install.js +238 -0
- package/bin/agent-launch.js +199 -0
- package/bin/agent-spawn.js +137 -0
- package/bin/agents.js +236 -931
- package/bin/auth.js +9 -1
- package/bin/claude-picker.js +143 -0
- package/bin/cli.js +62 -2
- 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 +25 -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 +77 -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 +462 -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 +186 -0
- package/bin/runbook/copilot/install.sh +45 -63
- package/bin/runbook/cursor/agent.json +138 -0
- package/bin/runbook/cursor/install.sh +8 -35
- package/bin/runbook/deepseek-harness/agent.json +164 -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 +159 -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 +217 -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 +57 -0
- package/bin/sessions.js +53 -48
- package/bin/tui.js +13 -0
- package/bin/update-check.js +35 -1
- package/bin/upgrade.js +27 -34
- package/bin/usage.js +21 -10
- package/bin/windows/agents.js +28 -60
- package/bin/windows/common.js +2 -28
- 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 -4
- 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/install.sh +0 -154
- package/bin/runbook/subconscious-code/run.sh +0 -21
- package/bin/windows/claude.js +0 -100
- package/bin/windows/install.js +0 -188
package/bin/auth.js
CHANGED
|
@@ -401,7 +401,7 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
401
401
|
}
|
|
402
402
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
403
403
|
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
404
|
-
} else {
|
|
404
|
+
} else if (res.status === 401) {
|
|
405
405
|
console.log(` ${c.red}✗ Key is invalid or revoked${c.reset}`);
|
|
406
406
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
407
407
|
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
@@ -409,6 +409,14 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
409
409
|
console.log(
|
|
410
410
|
` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.`,
|
|
411
411
|
);
|
|
412
|
+
} else {
|
|
413
|
+
// Only a 401 means the key was rejected. A 5xx or 429 says nothing
|
|
414
|
+
// about the key, so don't tell the user to log out.
|
|
415
|
+
console.log(
|
|
416
|
+
` ${c.yellow}Could not verify the key (platform returned HTTP ${res.status})${c.reset}`,
|
|
417
|
+
);
|
|
418
|
+
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
419
|
+
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
412
420
|
}
|
|
413
421
|
} catch {
|
|
414
422
|
console.log(
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// The env claudeModelPickerEnv sets, described for readers that cannot run it.
|
|
2
|
+
export const CLAUDE_PICKER_ENV_DESCRIPTION = [
|
|
3
|
+
...['OPUS', 'SONNET', 'HAIKU', 'FABLE'].flatMap((role, index) => {
|
|
4
|
+
const alias = role.toLowerCase();
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
name: `ANTHROPIC_DEFAULT_${role}_MODEL`,
|
|
8
|
+
value: `{catalog[${index}]}`,
|
|
9
|
+
override: [`ANTHROPIC_DEFAULT_${role}_MODEL`],
|
|
10
|
+
description: `Model behind the ${alias} alias. Set from the catalog, padded to four entries; a profile or environment value is kept only when it is in the live catalog.`,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: `ANTHROPIC_DEFAULT_${role}_MODEL_NAME`,
|
|
14
|
+
value: `{catalog[${index}]}`,
|
|
15
|
+
override: [`ANTHROPIC_DEFAULT_${role}_MODEL_NAME`],
|
|
16
|
+
description: `Picker label for the ${alias} alias.`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: `ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`,
|
|
20
|
+
value: `Subconscious model {catalog[${index}]}`,
|
|
21
|
+
override: [`ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`],
|
|
22
|
+
description: `Picker description for the ${alias} alias.`,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
}),
|
|
26
|
+
{
|
|
27
|
+
name: 'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
28
|
+
value: '{catalog[4]}',
|
|
29
|
+
description:
|
|
30
|
+
'Fifth picker entry, with _NAME and _DESCRIPTION. Only when the catalog has five or more models.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'SUBC_CLAUDE_SETTINGS',
|
|
34
|
+
value: '{claudeSettings}',
|
|
35
|
+
description: 'Picker settings JSON passed to --settings.',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const CLAUDE_MODEL_PICKER_KEYS = [
|
|
40
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
41
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
|
|
42
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
|
|
43
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
44
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
|
45
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
|
46
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
47
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
|
|
48
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
|
49
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
50
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
|
|
51
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
|
|
52
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
53
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
|
|
54
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const CLAUDE_MODEL_PICKER_ID_KEYS = [
|
|
58
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
59
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
60
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
61
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
62
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function uniqueCatalogModels(models, fallbackModel) {
|
|
66
|
+
const unique = [];
|
|
67
|
+
for (const model of models) {
|
|
68
|
+
if (model && !unique.includes(model)) unique.push(model);
|
|
69
|
+
}
|
|
70
|
+
if (unique.length === 0 && fallbackModel) unique.push(fallbackModel);
|
|
71
|
+
return unique;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function claudePickerSettings(models, fallbackModel) {
|
|
75
|
+
const unique = uniqueCatalogModels(models, fallbackModel);
|
|
76
|
+
return {
|
|
77
|
+
availableModels: unique,
|
|
78
|
+
modelPicker: {
|
|
79
|
+
replaceBuiltInOptions: true,
|
|
80
|
+
options: unique.map((model) => ({
|
|
81
|
+
model,
|
|
82
|
+
label: model,
|
|
83
|
+
description: `Subconscious model ${model}`,
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function claudeModelPickerEnv(model, models) {
|
|
90
|
+
// Only the live catalog belongs in Claude's picker. Packaged registry slots
|
|
91
|
+
// (Haiku → DeepSeek, etc.) must not be unioned back in — that advertised
|
|
92
|
+
// models the key cannot call.
|
|
93
|
+
//
|
|
94
|
+
// Pad through Fable so the built-in `fable` alias cannot resolve to Anthropic
|
|
95
|
+
// Fable 5. The /model menu itself is replaced via claudePickerSettings.
|
|
96
|
+
const pickerModels = uniqueCatalogModels(models, model);
|
|
97
|
+
while (pickerModels.length < 4)
|
|
98
|
+
pickerModels.push(pickerModels.at(-1) || model);
|
|
99
|
+
|
|
100
|
+
const roles = ['OPUS', 'SONNET', 'HAIKU', 'FABLE'];
|
|
101
|
+
const env = {};
|
|
102
|
+
for (let index = 0; index < roles.length; index++) {
|
|
103
|
+
const role = roles[index];
|
|
104
|
+
const model = pickerModels[index];
|
|
105
|
+
if (!model) continue;
|
|
106
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL`] = model;
|
|
107
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL_NAME`] = model;
|
|
108
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`] =
|
|
109
|
+
`Subconscious model ${model}`;
|
|
110
|
+
}
|
|
111
|
+
const customModel = pickerModels[4];
|
|
112
|
+
if (customModel) {
|
|
113
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION = customModel;
|
|
114
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME = customModel;
|
|
115
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION = `Subconscious model ${customModel}`;
|
|
116
|
+
}
|
|
117
|
+
return Object.fromEntries(
|
|
118
|
+
CLAUDE_MODEL_PICKER_KEYS.map((key) => [key, env[key]]).filter(
|
|
119
|
+
([, value]) => value,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function applyClaudePickerCatalog(env, picker, models) {
|
|
125
|
+
if (Object.keys(picker).length === 0) return env;
|
|
126
|
+
const allowed = new Set(models);
|
|
127
|
+
|
|
128
|
+
for (const key of CLAUDE_MODEL_PICKER_KEYS) {
|
|
129
|
+
if (!picker[key]) {
|
|
130
|
+
delete env[key];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const key of CLAUDE_MODEL_PICKER_ID_KEYS) {
|
|
135
|
+
if (!picker[key] || !env[key] || allowed.has(env[key])) continue;
|
|
136
|
+
env[key] = picker[key];
|
|
137
|
+
const nameKey = `${key}_NAME`;
|
|
138
|
+
const descriptionKey = `${key}_DESCRIPTION`;
|
|
139
|
+
if (picker[nameKey]) env[nameKey] = picker[nameKey];
|
|
140
|
+
if (picker[descriptionKey]) env[descriptionKey] = picker[descriptionKey];
|
|
141
|
+
}
|
|
142
|
+
return env;
|
|
143
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
agentCommandName,
|
|
15
15
|
agentList,
|
|
16
16
|
isAgentHelpRequest,
|
|
17
|
+
isHeadlessRequest,
|
|
17
18
|
parseAgentAction,
|
|
18
19
|
resolveAgent,
|
|
19
20
|
runAgent,
|
|
@@ -28,6 +29,11 @@ import {
|
|
|
28
29
|
import { renderBanner } from './branding.js';
|
|
29
30
|
import { c } from './colors.js';
|
|
30
31
|
import { feedbackCommand, printFeedbackHelp } from './feedback.js';
|
|
32
|
+
import {
|
|
33
|
+
harnessManifestCommand,
|
|
34
|
+
printHarnessManifestHelp,
|
|
35
|
+
} from './harness-manifest.js';
|
|
36
|
+
import { headlessPromptIndex } from './headless-args.js';
|
|
31
37
|
import { resolveModelCatalog } from './models.js';
|
|
32
38
|
import {
|
|
33
39
|
configCommand,
|
|
@@ -86,6 +92,7 @@ function printHelp() {
|
|
|
86
92
|
|
|
87
93
|
${c.bold}Coding agents${c.reset}
|
|
88
94
|
${agents}
|
|
95
|
+
${c.cyan}harness-manifest${c.reset} Print how each agent is installed and launched (JSON)
|
|
89
96
|
|
|
90
97
|
${c.bold}Options${c.reset}
|
|
91
98
|
${c.dim}--model <id>${c.reset} Model to use (profile MODEL, or first live catalog model if UNSET)
|
|
@@ -111,6 +118,8 @@ ${agents}
|
|
|
111
118
|
${c.dim}$${c.reset} subc cursor install
|
|
112
119
|
${c.dim}$${c.reset} subc cursor uninstall
|
|
113
120
|
${c.dim}$${c.reset} subc pi install
|
|
121
|
+
${c.dim}$${c.reset} subc harness-manifest --json
|
|
122
|
+
${c.dim}$${c.reset} subc codex headless "fix the failing test"
|
|
114
123
|
${c.dim}$${c.reset} subc -p staging codex
|
|
115
124
|
|
|
116
125
|
${c.dim}Use subc <command> help for command-specific usage.${c.reset}
|
|
@@ -150,7 +159,8 @@ Usage:
|
|
|
150
159
|
subc upgrade --latest
|
|
151
160
|
subc upgrade help
|
|
152
161
|
|
|
153
|
-
Upgrade subconscious-cli to the latest published version
|
|
162
|
+
Upgrade subconscious-cli to the latest published version, the same way it
|
|
163
|
+
was installed: npm (or pnpm, yarn, bun), or the curl installer.
|
|
154
164
|
|
|
155
165
|
subc upgrade Prompt "Do you want to upgrade?" then install @latest
|
|
156
166
|
subc upgrade --latest Skip the prompt and install @latest
|
|
@@ -245,12 +255,45 @@ const authCommands = {
|
|
|
245
255
|
whoami: whoamiCommand,
|
|
246
256
|
};
|
|
247
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Position of the headless prompt in the full argv, or -1. It is decided on
|
|
260
|
+
* the words left once profile flags are removed, which is what the agent
|
|
261
|
+
* launcher sees, so both agree on whether the run is headless.
|
|
262
|
+
*/
|
|
263
|
+
function headlessPromptPosition(argv) {
|
|
264
|
+
const kept = [];
|
|
265
|
+
for (let i = 0; i < argv.length; i++) {
|
|
266
|
+
const arg = argv[i];
|
|
267
|
+
const afterCommand = kept.slice(1).map((index) => argv[index]);
|
|
268
|
+
if (
|
|
269
|
+
kept.length > 0 &&
|
|
270
|
+
headlessPromptIndex(afterCommand) === afterCommand.length
|
|
271
|
+
) {
|
|
272
|
+
return i;
|
|
273
|
+
}
|
|
274
|
+
if (arg === '--') return -1;
|
|
275
|
+
if (arg === '--profile' || arg === '-p') {
|
|
276
|
+
i++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (arg.startsWith('--profile=')) continue;
|
|
280
|
+
kept.push(i);
|
|
281
|
+
}
|
|
282
|
+
return -1;
|
|
283
|
+
}
|
|
284
|
+
|
|
248
285
|
function extractProfile(argv) {
|
|
249
286
|
let profileName = process.env.SUBC_PROFILE?.trim() || DEFAULT_PROFILE;
|
|
250
287
|
let profileExplicit = false;
|
|
251
288
|
const args = [];
|
|
289
|
+
// The headless prompt is opaque, even when it looks like --profile.
|
|
290
|
+
const promptIndex = headlessPromptPosition(argv);
|
|
252
291
|
for (let i = 0; i < argv.length; i++) {
|
|
253
292
|
const arg = argv[i];
|
|
293
|
+
if (i === promptIndex) {
|
|
294
|
+
args.push(arg);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
254
297
|
if (arg === '--') {
|
|
255
298
|
args.push(...argv.slice(i));
|
|
256
299
|
break;
|
|
@@ -296,15 +339,32 @@ async function main() {
|
|
|
296
339
|
return;
|
|
297
340
|
}
|
|
298
341
|
|
|
342
|
+
// Machine-readable output: skip the update check so nothing else reaches
|
|
343
|
+
// stdout and no npm request is made.
|
|
344
|
+
if (command === 'harness-manifest') {
|
|
345
|
+
if (isHelpArg(args[1])) {
|
|
346
|
+
printHarnessManifestHelp();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
harnessManifestCommand(args.slice(1));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
299
353
|
const launchTui =
|
|
300
354
|
!command &&
|
|
301
355
|
process.stdin.isTTY === true &&
|
|
302
356
|
process.stdout.isTTY === true &&
|
|
303
357
|
process.env.TERM !== 'dumb';
|
|
304
358
|
|
|
359
|
+
// Headless runs are scripted: no npm request and no update prompt.
|
|
360
|
+
const headlessAgent = resolveAgent(command);
|
|
361
|
+
const headless = Boolean(
|
|
362
|
+
headlessAgent && isHeadlessRequest(headlessAgent, args.slice(1)),
|
|
363
|
+
);
|
|
364
|
+
|
|
305
365
|
// Explicit commands can wait on npm. The TUI opens immediately and prompts
|
|
306
366
|
// only after a newer version is found.
|
|
307
|
-
if (!launchTui) {
|
|
367
|
+
if (!launchTui && !headless) {
|
|
308
368
|
const update = await showUpdateNotice();
|
|
309
369
|
if (update?.action === 'updated' || update?.action === 'cancel') return;
|
|
310
370
|
}
|
package/bin/clipboard-image.js
CHANGED
|
@@ -58,7 +58,9 @@ async function readDarwinClipboard() {
|
|
|
58
58
|
async function readLinuxClipboard() {
|
|
59
59
|
const commands = [
|
|
60
60
|
['wl-paste', ['--type', 'image/png']],
|
|
61
|
+
['wl-paste', ['--type', 'image/jpeg']],
|
|
61
62
|
['xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o']],
|
|
63
|
+
['xclip', ['-selection', 'clipboard', '-t', 'image/jpeg', '-o']],
|
|
62
64
|
];
|
|
63
65
|
for (const [command, args] of commands) {
|
|
64
66
|
try {
|
package/bin/feedback.js
CHANGED
|
@@ -17,6 +17,17 @@ import { DEFAULT_PROFILE } from './profiles.js';
|
|
|
17
17
|
export const FEEDBACK_API_PATH = '/api/cli/feedback';
|
|
18
18
|
export const SUPPORT_EMAIL = 'support@subconscious.dev';
|
|
19
19
|
|
|
20
|
+
function feedbackOptionValue(argv, index) {
|
|
21
|
+
const value = argv[index + 1];
|
|
22
|
+
if (
|
|
23
|
+
value === undefined ||
|
|
24
|
+
/^(?:--[A-Za-z][\w-]*|-[A-Za-z])(?:=|$)/.test(value)
|
|
25
|
+
) {
|
|
26
|
+
throw new Error(`${argv[index]} requires a value`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
/**
|
|
21
32
|
* Parses feedback command arguments. Shared with tests.
|
|
22
33
|
*
|
|
@@ -31,15 +42,15 @@ export function parseFeedbackArgs(argv = []) {
|
|
|
31
42
|
for (let i = 0; i < argv.length; i++) {
|
|
32
43
|
const arg = argv[i];
|
|
33
44
|
if (arg === '-s' || arg === '--subject') {
|
|
34
|
-
subject = argv
|
|
45
|
+
subject = feedbackOptionValue(argv, i++);
|
|
35
46
|
} else if (arg.startsWith('--subject=')) {
|
|
36
47
|
subject = arg.slice('--subject='.length);
|
|
37
48
|
} else if (arg === '-m' || arg === '--message') {
|
|
38
|
-
message = argv
|
|
49
|
+
message = feedbackOptionValue(argv, i++);
|
|
39
50
|
} else if (arg.startsWith('--message=')) {
|
|
40
51
|
message = arg.slice('--message='.length);
|
|
41
52
|
} else if (arg === '--image') {
|
|
42
|
-
images.push(argv
|
|
53
|
+
images.push(feedbackOptionValue(argv, i++));
|
|
43
54
|
} else if (arg.startsWith('--image=')) {
|
|
44
55
|
images.push(arg.slice('--image='.length));
|
|
45
56
|
} else if (arg !== '') {
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
AGENTS,
|
|
5
|
+
agentBinary,
|
|
6
|
+
agentCommandName,
|
|
7
|
+
agentInputs,
|
|
8
|
+
installCommands,
|
|
9
|
+
RUNBOOK_DIR,
|
|
10
|
+
resolveRefs,
|
|
11
|
+
staticLookup,
|
|
12
|
+
} from './agent-data.js';
|
|
13
|
+
import { RUNBOOK_ENV_DESCRIPTION } from './agents.js';
|
|
14
|
+
import { CLAUDE_PICKER_ENV_DESCRIPTION } from './claude-picker.js';
|
|
15
|
+
import { c } from './colors.js';
|
|
16
|
+
import { buildOpenCodeConfig } from './opencode-provider.js';
|
|
17
|
+
|
|
18
|
+
// Bump on any change that can break a reader: a renamed or removed field, or a
|
|
19
|
+
// changed meaning. Adding a field does not need a bump.
|
|
20
|
+
export const HARNESS_MANIFEST_SCHEMA_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
export const MANIFEST_TOKENS = {
|
|
23
|
+
'{baseUrl}': 'Gateway origin, without a trailing slash.',
|
|
24
|
+
'{baseUrlV1}': 'Gateway origin followed by /v1.',
|
|
25
|
+
'{apiKey}': "The agent's API key input when set, otherwise API_KEY.",
|
|
26
|
+
'{model}': 'Launch model ID.',
|
|
27
|
+
'${NAME}':
|
|
28
|
+
'Value of env var NAME at launch. The manifest replaces it with the default when NAME is an input or env entry; a reference followed by / drops trailing slashes.',
|
|
29
|
+
'{catalog}': 'Every live catalog model ID, newline-separated.',
|
|
30
|
+
'{catalog[N]}':
|
|
31
|
+
'Catalog model at index N, clamped to the last entry; {model} when the catalog is empty.',
|
|
32
|
+
'{args}': 'Arguments passed through from subc, after its own flags.',
|
|
33
|
+
'{config}':
|
|
34
|
+
'config_flag NAME=VALUE for each cli-config entry whose condition holds.',
|
|
35
|
+
'{prompt}': 'The task given to subc <agent> headless PROMPT.',
|
|
36
|
+
'{sessionId}': 'A local session ID, for launch.resume.',
|
|
37
|
+
'{sessionFile}': "A local session's file, or its ID when it has none.",
|
|
38
|
+
'{tempFile}':
|
|
39
|
+
'Temporary file the runbook writes; the matching config entry says whether it is removed.',
|
|
40
|
+
'{tmp}': 'System temporary directory.',
|
|
41
|
+
'{opencodeConfig}':
|
|
42
|
+
'The OpenCode config document shown in the env-json config entry.',
|
|
43
|
+
'{claudeSettings}': 'JSON document described by the --settings config entry.',
|
|
44
|
+
'{target}':
|
|
45
|
+
'Release target triple, one of install.targets or install.windows_targets.',
|
|
46
|
+
'{binDir}': "Directory the agent's binary was found in.",
|
|
47
|
+
'{PATH}': "The caller's PATH.",
|
|
48
|
+
'<install dirs>': 'Common install directories subc adds to PATH.',
|
|
49
|
+
'<agent key>': 'The agent-specific API key input, for example CODEX_API_KEY.',
|
|
50
|
+
'<id>': 'Any model ID in the catalog; the field repeats for each model.',
|
|
51
|
+
'models[]': 'Every element of the models array.',
|
|
52
|
+
'<runbook>': 'Absolute path of the installed bin/runbook directory.',
|
|
53
|
+
'<VS Code user dir>':
|
|
54
|
+
'The VS Code user settings directory, for example ~/Library/Application Support/Code/User.',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const PACKAGE_URL = new URL('../package.json', import.meta.url);
|
|
58
|
+
|
|
59
|
+
function conditionText(when) {
|
|
60
|
+
return Object.entries(when)
|
|
61
|
+
.map(([name, test]) =>
|
|
62
|
+
test === 'set'
|
|
63
|
+
? `${name} non-empty`
|
|
64
|
+
: test === 'empty'
|
|
65
|
+
? `${name} empty`
|
|
66
|
+
: `${name} != ${test.not}`,
|
|
67
|
+
)
|
|
68
|
+
.join(' and ');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The OpenCode config with placeholders, built by the launch code itself. */
|
|
72
|
+
function openCodeConfigDocument(lookup) {
|
|
73
|
+
const document = buildOpenCodeConfig({
|
|
74
|
+
baseUrl: '{baseUrl}',
|
|
75
|
+
model: '{model}',
|
|
76
|
+
modelIds: ['<id>'],
|
|
77
|
+
context: lookup('OPENCODE_CONTEXT_LIMIT'),
|
|
78
|
+
output: lookup('OPENCODE_OUTPUT_LIMIT'),
|
|
79
|
+
});
|
|
80
|
+
// The manifest is the same on every platform, so a Windows path keeps `/`.
|
|
81
|
+
const portable = (file) =>
|
|
82
|
+
file.startsWith(RUNBOOK_DIR)
|
|
83
|
+
? `<runbook>/${path.relative(RUNBOOK_DIR, file).split(path.sep).join('/')}`
|
|
84
|
+
: file;
|
|
85
|
+
return { ...document, plugin: document.plugin.map(portable) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolver(agent) {
|
|
89
|
+
const lookup = staticLookup(agent);
|
|
90
|
+
return (value) =>
|
|
91
|
+
value === '{opencodeConfig}'
|
|
92
|
+
? openCodeConfigDocument(lookup)
|
|
93
|
+
: resolveRefs(value, lookup);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inputEntry(input, resolve) {
|
|
97
|
+
return input.default === undefined
|
|
98
|
+
? input
|
|
99
|
+
: { ...input, default: resolve(input.default) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function exportedEnv(input, resolve) {
|
|
103
|
+
return {
|
|
104
|
+
name: input.name,
|
|
105
|
+
value: String(resolve(input.default)),
|
|
106
|
+
override: [input.name, ...(input.aliases || [])],
|
|
107
|
+
description: input.description,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function envEntries(agent, resolve) {
|
|
112
|
+
const inputs = agentInputs(agent).filter((input) => input.export);
|
|
113
|
+
const own = agent.env.map((entry) => ({
|
|
114
|
+
...entry,
|
|
115
|
+
value:
|
|
116
|
+
entry.value === '{opencodeConfig}'
|
|
117
|
+
? entry.value
|
|
118
|
+
: String(resolve(entry.value)),
|
|
119
|
+
override: entry.override || [],
|
|
120
|
+
}));
|
|
121
|
+
const launcher =
|
|
122
|
+
agent.id === 'claude-code' ? CLAUDE_PICKER_ENV_DESCRIPTION : [];
|
|
123
|
+
return [
|
|
124
|
+
...inputs.map((input) => exportedEnv(input, resolve)),
|
|
125
|
+
...own,
|
|
126
|
+
...launcher,
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function inputBehindRef(agent, value) {
|
|
131
|
+
const name = /^\$\{([A-Z_][A-Z0-9_]*)\}$/.exec(value ?? '')?.[1];
|
|
132
|
+
return name && agentInputs(agent).find((input) => input.name === name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Where a value that reads an input can be changed: its env names and flag. */
|
|
136
|
+
function overrideFor(input) {
|
|
137
|
+
return [
|
|
138
|
+
input.name,
|
|
139
|
+
...(input.aliases || []),
|
|
140
|
+
...(input.flag ? [input.flag] : []),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function configEntry(agent, entry, resolve) {
|
|
145
|
+
const { when, ...rest } = entry;
|
|
146
|
+
const out = { ...rest };
|
|
147
|
+
if ('value' in entry) out.value = resolve(entry.value);
|
|
148
|
+
const input = inputBehindRef(agent, entry.value);
|
|
149
|
+
if (input) out.override = overrideFor(input);
|
|
150
|
+
if (when) out.condition = conditionText(when);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A knob that reads an input says where the value comes from and its bounds. */
|
|
155
|
+
function knob(agent, setting, resolve) {
|
|
156
|
+
const input = inputBehindRef(agent, setting.value);
|
|
157
|
+
const value = resolve(setting.value);
|
|
158
|
+
const out = {
|
|
159
|
+
...setting,
|
|
160
|
+
value: setting.kind === 'env' ? String(value) : value,
|
|
161
|
+
};
|
|
162
|
+
if (input && !setting.override) out.override = overrideFor(input);
|
|
163
|
+
if (input && setting.range)
|
|
164
|
+
out.range = { min: input.min, max: input.max, ...setting.range };
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function capabilities(agent, resolve) {
|
|
169
|
+
const { compaction, ...rest } = agent.capabilities;
|
|
170
|
+
const knobs = Object.fromEntries(
|
|
171
|
+
['on', 'off', 'threshold'].map((mode) => [
|
|
172
|
+
mode,
|
|
173
|
+
knob(agent, compaction[mode], resolve),
|
|
174
|
+
]),
|
|
175
|
+
);
|
|
176
|
+
return { ...resolve(rest), compaction: { ...compaction, ...knobs } };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function harnessEntry(agent) {
|
|
180
|
+
const resolve = resolver(agent);
|
|
181
|
+
const { runbook } = agent;
|
|
182
|
+
const script = (name) => (name ? `${agent.id}/${name}` : null);
|
|
183
|
+
return {
|
|
184
|
+
id: agent.id,
|
|
185
|
+
name: agent.name,
|
|
186
|
+
command: `subc ${agentCommandName(agent)}`,
|
|
187
|
+
aliases: agent.aliases,
|
|
188
|
+
mode: runbook.mode,
|
|
189
|
+
protocol: agent.protocol,
|
|
190
|
+
runbook: {
|
|
191
|
+
dir: agent.id,
|
|
192
|
+
launch: script(runbook.script),
|
|
193
|
+
setup: script(runbook.setup_script),
|
|
194
|
+
setup_actions: runbook.setup_actions || [],
|
|
195
|
+
},
|
|
196
|
+
binary: agentBinary(agent),
|
|
197
|
+
install: { ...agent.install, commands: installCommands(agent) },
|
|
198
|
+
prerequisites: agent.prerequisites,
|
|
199
|
+
launch: agent.launch ?? null,
|
|
200
|
+
inputs: agentInputs(agent).map((input) => inputEntry(input, resolve)),
|
|
201
|
+
env: envEntries(agent, resolve),
|
|
202
|
+
config: agent.config.map((entry) => configEntry(agent, entry, resolve)),
|
|
203
|
+
capabilities: capabilities(agent, resolve),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The manifest, computed from the agent files this subc ships. */
|
|
208
|
+
export function loadHarnessManifest() {
|
|
209
|
+
return {
|
|
210
|
+
schema_version: HARNESS_MANIFEST_SCHEMA_VERSION,
|
|
211
|
+
cli_version: JSON.parse(readFileSync(PACKAGE_URL, 'utf-8')).version,
|
|
212
|
+
cli_package: 'subconscious-cli',
|
|
213
|
+
tokens: MANIFEST_TOKENS,
|
|
214
|
+
runbook_env: RUNBOOK_ENV_DESCRIPTION,
|
|
215
|
+
harnesses: Object.fromEntries(
|
|
216
|
+
AGENTS.map((agent) => [agent.id, harnessEntry(agent)]),
|
|
217
|
+
),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function printHarnessManifestHelp() {
|
|
222
|
+
console.log(`
|
|
223
|
+
Usage:
|
|
224
|
+
subc harness-manifest [--json]
|
|
225
|
+
subc harness-manifest help
|
|
226
|
+
|
|
227
|
+
Print how this subc version installs, configures, and launches each coding
|
|
228
|
+
harness: install source, binary, env vars, config files, compaction knobs,
|
|
229
|
+
MCP, headers, and hooks.
|
|
230
|
+
|
|
231
|
+
--json Print the full manifest as JSON. This is the default when stdout
|
|
232
|
+
is not a terminal; in a terminal, a summary table is printed.
|
|
233
|
+
`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function compactionSummary(compaction) {
|
|
237
|
+
const supported = ['on', 'off', 'threshold'].filter(
|
|
238
|
+
(mode) => compaction[mode]?.set_by_subc,
|
|
239
|
+
);
|
|
240
|
+
return supported.length ? supported.join(',') : '-';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function tableRows(manifest) {
|
|
244
|
+
return Object.values(manifest.harnesses).map((harness) => [
|
|
245
|
+
harness.id,
|
|
246
|
+
harness.binary || '-',
|
|
247
|
+
harness.install.package || harness.install.repository || '-',
|
|
248
|
+
String(harness.env.length),
|
|
249
|
+
compactionSummary(harness.capabilities.compaction),
|
|
250
|
+
String(harness.capabilities.hooks.length),
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function printTable(manifest) {
|
|
255
|
+
const header = ['harness', 'binary', 'install', 'env', 'compaction', 'hooks'];
|
|
256
|
+
const rows = tableRows(manifest);
|
|
257
|
+
const widths = header.map((title, index) =>
|
|
258
|
+
Math.max(title.length, ...rows.map((row) => row[index].length)),
|
|
259
|
+
);
|
|
260
|
+
const line = (cells) =>
|
|
261
|
+
` ${cells.map((cell, index) => cell.padEnd(widths[index])).join(' ')}`;
|
|
262
|
+
console.log(
|
|
263
|
+
`\n ${c.bold}Harness manifest${c.reset} ${c.dim}schema ${manifest.schema_version}, subc ${manifest.cli_version}${c.reset}\n`,
|
|
264
|
+
);
|
|
265
|
+
console.log(`${c.dim}${line(header)}${c.reset}`);
|
|
266
|
+
for (const row of rows) console.log(line(row));
|
|
267
|
+
console.log(
|
|
268
|
+
`\n ${c.dim}compaction lists the modes subc sets. Use --json for every field.${c.reset}\n`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function harnessManifestCommand(args = [], options = {}) {
|
|
273
|
+
const unknown = args.filter((arg) => arg !== '--json');
|
|
274
|
+
if (unknown.length) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Unknown argument: ${unknown[0]}. Try subc harness-manifest help.`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const isTTY = options.isTTY ?? process.stdout.isTTY === true;
|
|
280
|
+
const manifest = loadHarnessManifest();
|
|
281
|
+
if (args.includes('--json') || !isTTY) {
|
|
282
|
+
process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
printTable(manifest);
|
|
286
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const SUBC_PROFILE_FLAGS = new Set(['-p', '--profile']);
|
|
2
|
+
const SUBC_INLINE_FLAGS = ['--model=', '--profile='];
|
|
3
|
+
|
|
4
|
+
/** Whether `--model` consumes the word after it. An empty word is a value. */
|
|
5
|
+
export function takesModelValue(value) {
|
|
6
|
+
return value !== undefined && !value.startsWith('-');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Index of the headless prompt when `argv` (the words after the agent name)
|
|
11
|
+
* asks for a headless run, otherwise -1. Only subc's own flags may come
|
|
12
|
+
* before "headless"; later on it is an ordinary agent argument.
|
|
13
|
+
*/
|
|
14
|
+
export function headlessPromptIndex(argv) {
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (SUBC_PROFILE_FLAGS.has(arg)) {
|
|
18
|
+
i++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === '--model') {
|
|
22
|
+
if (takesModelValue(argv[i + 1])) i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (SUBC_INLINE_FLAGS.some((prefix) => arg.startsWith(prefix))) continue;
|
|
26
|
+
return arg === 'headless' ? i + 1 : -1;
|
|
27
|
+
}
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Index of the first "--" separator; a headless prompt of "--" is not one. */
|
|
32
|
+
export function separatorIndex(argv) {
|
|
33
|
+
const prompt = headlessPromptIndex(argv);
|
|
34
|
+
return argv.findIndex((arg, index) => arg === '--' && index !== prompt);
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULTS, PACKAGED_MODELS } from './agent-data.js';
|
|
2
|
+
|
|
3
|
+
const MODEL_ID = /^[-A-Za-z0-9._:/+]+$/;
|
|
4
|
+
|
|
5
|
+
/** The launch model first, then the catalog, deduplicated and validated. */
|
|
6
|
+
export function modelIds(env) {
|
|
7
|
+
const catalog = (
|
|
8
|
+
env.SUBCONSCIOUS_MODELS?.trim() || PACKAGED_MODELS.join('\n')
|
|
9
|
+
).split(/\r?\n/);
|
|
10
|
+
const models = [
|
|
11
|
+
...new Set([env.MODEL?.trim() || DEFAULTS.model, ...catalog]),
|
|
12
|
+
].filter(Boolean);
|
|
13
|
+
for (const model of models)
|
|
14
|
+
if (!MODEL_ID.test(model)) throw new Error(`Invalid model id: ${model}`);
|
|
15
|
+
return models;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function positiveInteger(input, name) {
|
|
19
|
+
if (
|
|
20
|
+
!/^[1-9][0-9]*$/.test(String(input)) ||
|
|
21
|
+
!Number.isSafeInteger(Number(input))
|
|
22
|
+
)
|
|
23
|
+
throw new Error(`${name} must be a positive integer`);
|
|
24
|
+
return Number(input);
|
|
25
|
+
}
|