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.
- package/README.md +135 -2
- 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 +284 -840
- package/bin/auth.js +89 -33
- package/bin/branding.js +3 -1
- package/bin/claude-picker.js +143 -0
- package/bin/cli.js +87 -20
- package/bin/clipboard-image.js +24 -5
- package/bin/colors.js +1 -1
- package/bin/feedback.js +71 -21
- 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 +12 -8
- package/bin/models.js +35 -19
- 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 +48 -5
- package/bin/profile-settings.js +129 -0
- package/bin/profiles.js +136 -449
- 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 +16 -0
- 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 +71 -49
- package/bin/runbook/shared.json +58 -0
- package/bin/runbook/subconscious-code/agent.json +156 -0
- package/bin/sessions.js +173 -58
- package/bin/tui.js +104 -32
- package/bin/update-check.js +54 -21
- package/bin/upgrade.js +67 -43
- package/bin/usage.js +47 -24
- package/bin/windows/agents.js +141 -37
- package/bin/windows/common.js +36 -25
- package/bin/windows/install.js +116 -34
- package/bin/windows/launch.js +260 -77
- package/bin/windows/pi-compaction.ts +68 -13
- package/bin/windows/process.js +210 -57
- package/bin/windows/setup.js +251 -56
- package/package.json +9 -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/run.sh +0 -21
- package/bin/windows/claude.js +0 -71
package/bin/feedback.js
CHANGED
|
@@ -9,14 +9,25 @@ import fs from 'node:fs/promises';
|
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import readline from 'node:readline';
|
|
11
11
|
import { createInterface } from 'node:readline/promises';
|
|
12
|
-
import { c } from './colors.js';
|
|
13
12
|
import { getApiKey, getPlatformUrl } from './auth.js';
|
|
14
13
|
import { imageContentType, readClipboardImage } from './clipboard-image.js';
|
|
14
|
+
import { c } from './colors.js';
|
|
15
15
|
import { DEFAULT_PROFILE } from './profiles.js';
|
|
16
16
|
|
|
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 !== '') {
|
|
@@ -64,12 +75,19 @@ export async function collectFeedbackContext(options = {}) {
|
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
export function buildFeedbackPayload({
|
|
78
|
+
export function buildFeedbackPayload({
|
|
79
|
+
subject,
|
|
80
|
+
message,
|
|
81
|
+
context,
|
|
82
|
+
attachments,
|
|
83
|
+
}) {
|
|
68
84
|
const payload = {
|
|
69
85
|
subject: subject || 'CLI feedback',
|
|
70
86
|
message,
|
|
71
87
|
context: Object.fromEntries(
|
|
72
|
-
Object.entries(context ?? {}).filter(([, value]) =>
|
|
88
|
+
Object.entries(context ?? {}).filter(([, value]) =>
|
|
89
|
+
Boolean(String(value).trim()),
|
|
90
|
+
),
|
|
73
91
|
),
|
|
74
92
|
};
|
|
75
93
|
if (attachments?.length) payload.attachments = attachments;
|
|
@@ -79,7 +97,8 @@ export function buildFeedbackPayload({ subject, message, context, attachments })
|
|
|
79
97
|
export async function loadFeedbackImages(paths) {
|
|
80
98
|
const attachments = [];
|
|
81
99
|
for (const filePath of paths) {
|
|
82
|
-
if (!filePath?.trim())
|
|
100
|
+
if (!filePath?.trim())
|
|
101
|
+
throw new Error('An image path is required after --image');
|
|
83
102
|
const buffer = await fs.readFile(filePath);
|
|
84
103
|
const contentType = imageContentType(buffer);
|
|
85
104
|
if (!contentType) throw new Error(`${filePath} is not a JPEG or PNG`);
|
|
@@ -88,7 +107,12 @@ export async function loadFeedbackImages(paths) {
|
|
|
88
107
|
return attachments;
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
export async function submitFeedback(
|
|
110
|
+
export async function submitFeedback(
|
|
111
|
+
apiKey,
|
|
112
|
+
platformUrl,
|
|
113
|
+
payload,
|
|
114
|
+
fetchImpl = fetch,
|
|
115
|
+
) {
|
|
92
116
|
const res = await fetchImpl(
|
|
93
117
|
`${platformUrl.replace(/\/$/, '')}${FEEDBACK_API_PATH}`,
|
|
94
118
|
{
|
|
@@ -164,7 +188,9 @@ function readAttachmentLine(onPaste) {
|
|
|
164
188
|
async function promptForAttachments(already = 0) {
|
|
165
189
|
if (process.stdin.isTTY !== true || already >= MAX_FEEDBACK_IMAGES) return [];
|
|
166
190
|
const attachments = [];
|
|
167
|
-
console.log(
|
|
191
|
+
console.log(
|
|
192
|
+
`\n Attachments (optional). Ctrl+V pastes a screenshot from the clipboard.`,
|
|
193
|
+
);
|
|
168
194
|
console.log(` ${c.dim}Press Enter when you are done.${c.reset}`);
|
|
169
195
|
while (already + attachments.length < MAX_FEEDBACK_IMAGES) {
|
|
170
196
|
const line = await readAttachmentLine(async () => {
|
|
@@ -172,11 +198,15 @@ async function promptForAttachments(already = 0) {
|
|
|
172
198
|
const buffer = await readClipboardImage();
|
|
173
199
|
const contentType = buffer ? imageContentType(buffer) : null;
|
|
174
200
|
if (!contentType || !buffer) {
|
|
175
|
-
console.log(
|
|
201
|
+
console.log(
|
|
202
|
+
`\n ${c.yellow}No JPEG or PNG image on the clipboard.${c.reset}`,
|
|
203
|
+
);
|
|
176
204
|
return;
|
|
177
205
|
}
|
|
178
206
|
attachments.push({ contentType, data: buffer.toString('base64') });
|
|
179
|
-
console.log(
|
|
207
|
+
console.log(
|
|
208
|
+
` ${c.green}Added screenshot ${already + attachments.length}.${c.reset}`,
|
|
209
|
+
);
|
|
180
210
|
});
|
|
181
211
|
if (line == null || !line.trim()) break;
|
|
182
212
|
}
|
|
@@ -186,7 +216,8 @@ async function promptForAttachments(already = 0) {
|
|
|
186
216
|
async function promptForFeedback(argv) {
|
|
187
217
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
188
218
|
try {
|
|
189
|
-
const subject =
|
|
219
|
+
const subject =
|
|
220
|
+
argv.subject || (await rl.question(`\n Subject (optional): `)).trim();
|
|
190
221
|
let message = '';
|
|
191
222
|
while (!message) {
|
|
192
223
|
message = (await rl.question(` Message: `)).trim();
|
|
@@ -208,7 +239,9 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
208
239
|
const auth = await getApiKey(options.profile);
|
|
209
240
|
if (!auth) {
|
|
210
241
|
console.log(`\n ${c.dim}Not logged in.${c.reset}`);
|
|
211
|
-
console.log(
|
|
242
|
+
console.log(
|
|
243
|
+
` Run ${c.cyan}subc ${profileFlag}login${c.reset} to get started.\n`,
|
|
244
|
+
);
|
|
212
245
|
return;
|
|
213
246
|
}
|
|
214
247
|
|
|
@@ -216,7 +249,9 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
216
249
|
if (!feedback.message) {
|
|
217
250
|
if (process.stdin.isTTY !== true) {
|
|
218
251
|
console.error(`\n ${c.red}A message is required.${c.reset}`);
|
|
219
|
-
console.error(
|
|
252
|
+
console.error(
|
|
253
|
+
` Pass one with ${c.cyan}subc feedback --message "..."${c.reset}\n`,
|
|
254
|
+
);
|
|
220
255
|
process.exitCode = 1;
|
|
221
256
|
return;
|
|
222
257
|
}
|
|
@@ -224,7 +259,10 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
224
259
|
}
|
|
225
260
|
|
|
226
261
|
const fromFlags = await loadFeedbackImages(parsed.images);
|
|
227
|
-
const fromPrompt =
|
|
262
|
+
const fromPrompt =
|
|
263
|
+
feedback.message && parsed.message
|
|
264
|
+
? []
|
|
265
|
+
: await promptForAttachments(fromFlags.length);
|
|
228
266
|
const platformUrl = getPlatformUrl(options.profile);
|
|
229
267
|
const payload = buildFeedbackPayload({
|
|
230
268
|
subject: feedback.subject,
|
|
@@ -237,9 +275,13 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
237
275
|
const res = await submitFeedback(auth.key, platformUrl, payload);
|
|
238
276
|
|
|
239
277
|
if (res.status === 404) {
|
|
240
|
-
console.error(
|
|
278
|
+
console.error(
|
|
279
|
+
`\n ${c.yellow}This platform has no feedback endpoint (404).${c.reset}`,
|
|
280
|
+
);
|
|
241
281
|
console.error(` ${c.dim}Host: ${platformUrl}${c.reset}`);
|
|
242
|
-
console.error(
|
|
282
|
+
console.error(
|
|
283
|
+
` ${c.dim}Point PLATFORM_URL at a platform that includes /api/cli/feedback.${c.reset}\n`,
|
|
284
|
+
);
|
|
243
285
|
process.exitCode = 1;
|
|
244
286
|
return;
|
|
245
287
|
}
|
|
@@ -253,14 +295,18 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
253
295
|
|
|
254
296
|
if (!res.ok) {
|
|
255
297
|
const detail = typeof data?.error === 'string' ? data.error : '';
|
|
256
|
-
const invalidKey =
|
|
298
|
+
const invalidKey =
|
|
299
|
+
detail === 'Invalid or revoked API key' ||
|
|
300
|
+
detail === 'No API key provided';
|
|
257
301
|
console.log(`\n ${c.red}✗ Couldn't send your message.${c.reset}`);
|
|
258
302
|
if (invalidKey) {
|
|
259
303
|
console.log(
|
|
260
304
|
` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.`,
|
|
261
305
|
);
|
|
262
306
|
}
|
|
263
|
-
console.log(
|
|
307
|
+
console.log(
|
|
308
|
+
` Please email ${c.cyan}${SUPPORT_EMAIL}${c.reset} directly.\n`,
|
|
309
|
+
);
|
|
264
310
|
process.exitCode = 1;
|
|
265
311
|
return;
|
|
266
312
|
}
|
|
@@ -269,12 +315,16 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
269
315
|
console.log(` ${c.dim}Our support team will get back to you.\n`);
|
|
270
316
|
} catch (error) {
|
|
271
317
|
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
|
|
272
|
-
console.log(
|
|
318
|
+
console.log(
|
|
319
|
+
`\n ${c.yellow}Could not reach the platform (timed out).${c.reset}`,
|
|
320
|
+
);
|
|
273
321
|
console.log(` ${c.dim}Host: ${platformUrl}${c.reset}`);
|
|
274
322
|
} else {
|
|
275
323
|
console.log(`\n ${c.red}✗ Couldn't send your message.${c.reset}`);
|
|
276
324
|
}
|
|
277
|
-
console.log(
|
|
325
|
+
console.log(
|
|
326
|
+
` Please email ${c.cyan}${SUPPORT_EMAIL}${c.reset} directly.\n`,
|
|
327
|
+
);
|
|
278
328
|
process.exitCode = 1;
|
|
279
329
|
}
|
|
280
330
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// The models endpoint currently supplies IDs, not modality metadata. Keep
|
|
4
|
-
// explicit capabilities in the same registry used to generate the Unix lookup.
|
|
5
|
-
const { modelCapabilities = {} } = JSON.parse(
|
|
6
|
-
readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf8'),
|
|
7
|
-
);
|
|
1
|
+
import { MODEL_CAPABILITIES } from './agent-data.js';
|
|
8
2
|
|
|
3
|
+
// The models endpoint supplies IDs, not modality metadata, so capabilities
|
|
4
|
+
// are listed by exact model ID: similarly named models inherit nothing.
|
|
9
5
|
export function modelSupportsVision(modelId) {
|
|
10
|
-
return
|
|
6
|
+
return (
|
|
7
|
+
Object.hasOwn(MODEL_CAPABILITIES, modelId) &&
|
|
8
|
+
MODEL_CAPABILITIES[modelId].vision === true
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Vision model IDs, newline-separated, for the runbooks' shell lookup. */
|
|
13
|
+
export function visionModelList() {
|
|
14
|
+
return Object.keys(MODEL_CAPABILITIES).filter(modelSupportsVision).join('\n');
|
|
11
15
|
}
|
package/bin/models.js
CHANGED
|
@@ -99,31 +99,33 @@ export async function fetchGatewayModels({
|
|
|
99
99
|
if (signal?.aborted) throw abortError('Model discovery was cancelled');
|
|
100
100
|
const aborted = new Promise((_, reject) => {
|
|
101
101
|
const fail = () => {
|
|
102
|
-
if (signal?.aborted)
|
|
103
|
-
|
|
102
|
+
if (signal?.aborted)
|
|
103
|
+
reject(abortError('Model discovery was cancelled'));
|
|
104
|
+
else
|
|
105
|
+
reject(new Error(`Model discovery timed out after ${timeoutMs}ms`));
|
|
104
106
|
};
|
|
105
107
|
controller.signal.addEventListener('abort', fail, { once: true });
|
|
106
108
|
});
|
|
107
|
-
const
|
|
108
|
-
fetchImpl(endpoint, {
|
|
109
|
+
const discover = async () => {
|
|
110
|
+
const response = await fetchImpl(endpoint, {
|
|
109
111
|
method: 'GET',
|
|
110
112
|
headers,
|
|
111
113
|
cache: 'no-store',
|
|
112
114
|
signal: controller.signal,
|
|
113
|
-
})
|
|
114
|
-
aborted,
|
|
115
|
-
]);
|
|
115
|
+
});
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`Model discovery returned HTTP ${response.status}`);
|
|
119
|
+
}
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
const payload = await response.json();
|
|
122
|
+
if (!Array.isArray(payload?.data)) {
|
|
123
|
+
throw new Error('Model discovery returned an invalid response');
|
|
124
|
+
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
return normalizeModelIds(payload.data.map((model) => model?.id));
|
|
127
|
+
};
|
|
128
|
+
return await Promise.race([discover(), aborted]);
|
|
127
129
|
} catch (error) {
|
|
128
130
|
if (signal?.aborted) throw abortError('Model discovery was cancelled');
|
|
129
131
|
if (controller.signal.aborted) {
|
|
@@ -144,13 +146,17 @@ async function fetchPublicModels({ baseUrl, fetchImpl, timeoutMs, signal }) {
|
|
|
144
146
|
timeoutMs,
|
|
145
147
|
signal,
|
|
146
148
|
});
|
|
147
|
-
if (!models.length)
|
|
149
|
+
if (!models.length)
|
|
150
|
+
throw new Error('Model discovery returned no usable model IDs');
|
|
148
151
|
return models;
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
function packagedCatalog(selectedModel, fallbackModels, error) {
|
|
152
155
|
return {
|
|
153
|
-
models: normalizeModelIds(
|
|
156
|
+
models: normalizeModelIds(
|
|
157
|
+
[selectedModel, ...fallbackModels],
|
|
158
|
+
selectedModel,
|
|
159
|
+
),
|
|
154
160
|
source: 'packaged',
|
|
155
161
|
error,
|
|
156
162
|
};
|
|
@@ -189,7 +195,12 @@ export async function resolveModelCatalog({
|
|
|
189
195
|
} catch (availableError) {
|
|
190
196
|
if (isAbortError(availableError) || signal?.aborted) throw availableError;
|
|
191
197
|
try {
|
|
192
|
-
const discovered = await fetchPublicModels({
|
|
198
|
+
const discovered = await fetchPublicModels({
|
|
199
|
+
baseUrl,
|
|
200
|
+
fetchImpl,
|
|
201
|
+
timeoutMs,
|
|
202
|
+
signal,
|
|
203
|
+
});
|
|
193
204
|
return liveCatalog(discovered, selectedModel, 'public');
|
|
194
205
|
} catch (publicError) {
|
|
195
206
|
if (isAbortError(publicError) || signal?.aborted) throw publicError;
|
|
@@ -199,7 +210,12 @@ export async function resolveModelCatalog({
|
|
|
199
210
|
}
|
|
200
211
|
|
|
201
212
|
try {
|
|
202
|
-
const discovered = await fetchPublicModels({
|
|
213
|
+
const discovered = await fetchPublicModels({
|
|
214
|
+
baseUrl,
|
|
215
|
+
fetchImpl,
|
|
216
|
+
timeoutMs,
|
|
217
|
+
signal,
|
|
218
|
+
});
|
|
203
219
|
return liveCatalog(discovered, selectedModel, 'public');
|
|
204
220
|
} catch (error) {
|
|
205
221
|
if (isAbortError(error) || signal?.aborted) throw error;
|
|
Binary file
|
|
Binary file
|