subconscious-cli 6.0.0 → 6.0.1
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 +16 -2
- package/bin/agents.js +210 -70
- package/bin/auth.js +80 -32
- package/bin/branding.js +3 -1
- package/bin/cli.js +28 -20
- package/bin/clipboard-image.js +22 -5
- package/bin/colors.js +1 -1
- package/bin/feedback.js +57 -18
- package/bin/model-capabilities.js +4 -1
- package/bin/models.js +22 -6
- 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 +27 -5
- package/bin/profiles.js +126 -44
- package/bin/registry.generated.json +1 -1
- package/bin/runbook/opencode/opencode-plugin.d.ts +9 -0
- package/bin/runbook/opencode/subconscious-compaction.ts +26 -26
- package/bin/runbook/pi/subconscious-compaction.ts +69 -49
- package/bin/sessions.js +126 -41
- package/bin/tui.js +91 -32
- package/bin/update-check.js +49 -20
- package/bin/upgrade.js +49 -18
- package/bin/usage.js +26 -14
- package/bin/windows/agents.js +135 -32
- package/bin/windows/claude.js +43 -14
- package/bin/windows/common.js +56 -19
- package/bin/windows/install.js +120 -32
- package/bin/windows/launch.js +211 -49
- package/bin/windows/pi-compaction.ts +68 -13
- package/bin/windows/process.js +198 -56
- package/bin/windows/setup.js +231 -56
- package/package.json +9 -3
package/bin/auth.js
CHANGED
|
@@ -12,16 +12,23 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { exec } from 'node:child_process';
|
|
15
|
-
import { openWindowsBrowser } from './windows/process.js';
|
|
16
15
|
import fs from 'node:fs/promises';
|
|
17
16
|
import os from 'node:os';
|
|
18
17
|
import path from 'node:path';
|
|
19
18
|
import { c } from './colors.js';
|
|
20
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
clearProfileApiKey,
|
|
21
|
+
DEFAULT_PROFILE,
|
|
22
|
+
ensureProfile,
|
|
23
|
+
} from './profiles.js';
|
|
21
24
|
import { printLoginUpgradeWarning } from './upgrade.js';
|
|
25
|
+
import { openWindowsBrowser } from './windows/process.js';
|
|
22
26
|
|
|
23
27
|
function configDir() {
|
|
24
|
-
return
|
|
28
|
+
return (
|
|
29
|
+
process.env.SUBC_CONFIG_DIR?.trim() ||
|
|
30
|
+
path.join(os.homedir(), '.subconscious')
|
|
31
|
+
);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
function configFile() {
|
|
@@ -78,7 +85,10 @@ export async function getApiKey(profile) {
|
|
|
78
85
|
|
|
79
86
|
const config = await loadConfig();
|
|
80
87
|
if (config.subconscious_api_key) {
|
|
81
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
key: config.subconscious_api_key,
|
|
90
|
+
source: '~/.subconscious/config.json',
|
|
91
|
+
};
|
|
82
92
|
}
|
|
83
93
|
return null;
|
|
84
94
|
}
|
|
@@ -87,9 +97,11 @@ export async function getApiKey(profile) {
|
|
|
87
97
|
|
|
88
98
|
function openBrowser(url) {
|
|
89
99
|
if (process.platform === 'win32') {
|
|
90
|
-
void openWindowsBrowser(url)
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
void openWindowsBrowser(url)
|
|
101
|
+
.then((code) => {
|
|
102
|
+
if (code) throw new Error('Browser could not be opened');
|
|
103
|
+
})
|
|
104
|
+
.catch(() => console.log(`Please open this URL manually:\n\n ${url}\n`));
|
|
93
105
|
return;
|
|
94
106
|
}
|
|
95
107
|
const cmd =
|
|
@@ -131,7 +143,11 @@ export async function registerDeviceLogin(platformUrl, fetchImpl = fetch) {
|
|
|
131
143
|
return data;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
export async function pollDeviceLogin(
|
|
146
|
+
export async function pollDeviceLogin(
|
|
147
|
+
platformUrl,
|
|
148
|
+
deviceCode,
|
|
149
|
+
fetchImpl = fetch,
|
|
150
|
+
) {
|
|
135
151
|
const res = await fetchImpl(`${platformUrl}/api/cli/device/token`, {
|
|
136
152
|
method: 'POST',
|
|
137
153
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -151,7 +167,9 @@ function printDeviceInstructions(platformUrl, userCode) {
|
|
|
151
167
|
console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
|
|
152
168
|
console.log();
|
|
153
169
|
console.log(` ${c.dim}If a window doesn't open, go to:${c.reset}`);
|
|
154
|
-
console.log(
|
|
170
|
+
console.log(
|
|
171
|
+
` ${c.underline}${c.cyan}${terminalLink(verificationUrl)}${c.reset}`,
|
|
172
|
+
);
|
|
155
173
|
console.log();
|
|
156
174
|
console.log(
|
|
157
175
|
` ${c.dim}Or open ${c.reset}${c.underline}${c.cyan}${terminalLink(deviceUrl)}${c.reset}${c.dim} and enter:${c.reset} ${c.bold}${userCode}${c.reset}`,
|
|
@@ -161,7 +179,9 @@ function printDeviceInstructions(platformUrl, userCode) {
|
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
function printLoginFallback() {
|
|
164
|
-
console.error(
|
|
182
|
+
console.error(
|
|
183
|
+
` ${c.dim}You can also copy an API key from the dashboard and run${c.reset} ${c.cyan}subc update-key <your-api-key>${c.reset}${c.dim}.${c.reset}`,
|
|
184
|
+
);
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
async function saveLoginKey(profileName, token) {
|
|
@@ -181,17 +201,23 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
181
201
|
if (existing) {
|
|
182
202
|
const profile = await ensureProfile(profileName, existing.key);
|
|
183
203
|
const logout =
|
|
184
|
-
profileName === DEFAULT_PROFILE
|
|
185
|
-
|
|
204
|
+
profileName === DEFAULT_PROFILE
|
|
205
|
+
? 'subc logout'
|
|
206
|
+
: `subc --profile ${profileName} logout`;
|
|
207
|
+
const masked = `${existing.key.slice(0, 8)}...${existing.key.slice(-4)}`;
|
|
186
208
|
console.log(`\n${c.yellow}Already logged in.${c.reset}`);
|
|
187
209
|
console.log(` Key: ${c.dim}${masked}${c.reset}`);
|
|
188
210
|
console.log(` Profile: ${c.dim}${profile.path}${c.reset}`);
|
|
189
|
-
console.log(
|
|
211
|
+
console.log(
|
|
212
|
+
`\n Run ${c.cyan}${logout}${c.reset} first to switch accounts.\n`,
|
|
213
|
+
);
|
|
190
214
|
return;
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
console.log();
|
|
194
|
-
console.log(
|
|
218
|
+
console.log(
|
|
219
|
+
` ${c.magenta}${c.bold}Subconscious${c.reset} ${c.dim}— CLI Login${c.reset}`,
|
|
220
|
+
);
|
|
195
221
|
console.log();
|
|
196
222
|
|
|
197
223
|
const platformUrl = getPlatformUrl(options.profile);
|
|
@@ -206,7 +232,10 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
206
232
|
return;
|
|
207
233
|
}
|
|
208
234
|
|
|
209
|
-
const verificationUrl = printDeviceInstructions(
|
|
235
|
+
const verificationUrl = printDeviceInstructions(
|
|
236
|
+
platformUrl,
|
|
237
|
+
registered.user_code,
|
|
238
|
+
);
|
|
210
239
|
(options.openBrowser || openBrowser)(verificationUrl);
|
|
211
240
|
|
|
212
241
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -229,35 +258,47 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
229
258
|
if (Date.now() - started > DEVICE_POLL_TIMEOUT_MS) {
|
|
230
259
|
throw new Error('Authentication timed out (5 min). Please try again.');
|
|
231
260
|
}
|
|
232
|
-
const polled = await pollDeviceLogin(
|
|
261
|
+
const polled = await pollDeviceLogin(
|
|
262
|
+
platformUrl,
|
|
263
|
+
registered.device_code,
|
|
264
|
+
fetchImpl,
|
|
265
|
+
);
|
|
233
266
|
if (polled.status === 'approved') {
|
|
234
267
|
clearInterval(spinner);
|
|
235
|
-
process.stdout.write(
|
|
268
|
+
process.stdout.write(`\r${' '.repeat(50)}\r`);
|
|
236
269
|
const profile = await saveLoginKey(profileName, polled.key);
|
|
237
|
-
const masked = polled.key.slice(0, 8)
|
|
270
|
+
const masked = `${polled.key.slice(0, 8)}...${polled.key.slice(-4)}`;
|
|
238
271
|
console.log(` ${c.green}${c.bold}✓ Logged in successfully!${c.reset}`);
|
|
239
272
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
240
273
|
if (profileName === DEFAULT_PROFILE) {
|
|
241
|
-
console.log(
|
|
274
|
+
console.log(
|
|
275
|
+
` ${c.dim}Saved to ~/.subconscious/config.json${c.reset}`,
|
|
276
|
+
);
|
|
242
277
|
}
|
|
243
278
|
console.log(` ${c.dim}Runbook profile: ${profile.path}${c.reset}`);
|
|
244
|
-
console.log(
|
|
245
|
-
|
|
279
|
+
console.log(
|
|
280
|
+
` ${c.dim}Launch a terminal agent with ${c.reset}${c.cyan}subc claude${c.reset}${c.dim}, or install editor hooks with ${c.reset}${c.cyan}subc cursor install${c.reset}${c.dim}.${c.reset}`,
|
|
281
|
+
);
|
|
282
|
+
console.log(
|
|
283
|
+
` ${c.dim}Pi needs ${c.reset}${c.cyan}subc pi install${c.reset}${c.dim} first. List profiles with ${c.reset}${c.cyan}subc config${c.reset}${c.dim}.${c.reset}`,
|
|
284
|
+
);
|
|
246
285
|
console.log();
|
|
247
286
|
return;
|
|
248
287
|
}
|
|
249
288
|
if (polled.status === 'expired') {
|
|
250
289
|
throw new Error('Login code expired. Please try again.');
|
|
251
290
|
}
|
|
252
|
-
await new Promise((resolve) =>
|
|
291
|
+
await new Promise((resolve) =>
|
|
292
|
+
setTimeout(resolve, DEVICE_POLL_INTERVAL_MS),
|
|
293
|
+
);
|
|
253
294
|
}
|
|
254
295
|
clearInterval(spinner);
|
|
255
|
-
process.stdout.write(
|
|
296
|
+
process.stdout.write(`\r${' '.repeat(50)}\r`);
|
|
256
297
|
console.log(`\n ${c.dim}Login cancelled.${c.reset}\n`);
|
|
257
298
|
process.exitCode = 1;
|
|
258
299
|
} catch (error) {
|
|
259
300
|
clearInterval(spinner);
|
|
260
|
-
process.stdout.write(
|
|
301
|
+
process.stdout.write(`\r${' '.repeat(50)}\r`);
|
|
261
302
|
console.error(` ${c.red}✗ ${error.message}${c.reset}`);
|
|
262
303
|
printLoginFallback();
|
|
263
304
|
console.log();
|
|
@@ -282,7 +323,8 @@ export async function updateApiKeyCommand(argv = [], options = {}) {
|
|
|
282
323
|
await saveConfig(config);
|
|
283
324
|
}
|
|
284
325
|
|
|
285
|
-
const masked =
|
|
326
|
+
const masked =
|
|
327
|
+
key.length <= 12 ? '********' : `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
286
328
|
console.log(`\n ${c.green}${c.bold}✓ API key updated.${c.reset}`);
|
|
287
329
|
console.log(` ${c.dim}Profile: ${profile.path}${c.reset}`);
|
|
288
330
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
@@ -298,7 +340,8 @@ export async function logoutCommand(_argv = [], options = {}) {
|
|
|
298
340
|
const profileName = options.profileName || DEFAULT_PROFILE;
|
|
299
341
|
const config = await loadConfig();
|
|
300
342
|
const clearedProfile = await clearProfileApiKey(profileName);
|
|
301
|
-
const clearSavedConfig =
|
|
343
|
+
const clearSavedConfig =
|
|
344
|
+
profileName === DEFAULT_PROFILE && config.subconscious_api_key;
|
|
302
345
|
|
|
303
346
|
if (!clearSavedConfig && !clearedProfile) {
|
|
304
347
|
console.log(`\n ${c.dim}Not logged in.${c.reset}\n`);
|
|
@@ -331,16 +374,19 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
331
374
|
}
|
|
332
375
|
|
|
333
376
|
const { key, source } = auth;
|
|
334
|
-
const masked = key.slice(0, 8)
|
|
377
|
+
const masked = `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
335
378
|
|
|
336
379
|
console.log();
|
|
337
380
|
|
|
338
381
|
// Validate the key against the server; falls back to offline display if unreachable
|
|
339
382
|
try {
|
|
340
|
-
const res = await fetch(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
383
|
+
const res = await fetch(
|
|
384
|
+
`${getPlatformUrl(options.profile)}/api/cli/whoami`,
|
|
385
|
+
{
|
|
386
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
387
|
+
signal: AbortSignal.timeout(5000),
|
|
388
|
+
},
|
|
389
|
+
);
|
|
344
390
|
|
|
345
391
|
if (res.status === 404) {
|
|
346
392
|
printLoginUpgradeWarning();
|
|
@@ -365,7 +411,9 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
365
411
|
);
|
|
366
412
|
}
|
|
367
413
|
} catch {
|
|
368
|
-
console.log(
|
|
414
|
+
console.log(
|
|
415
|
+
` ${c.green}✓ Authenticated${c.reset} ${c.dim}(offline — key not verified)${c.reset}`,
|
|
416
|
+
);
|
|
369
417
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
370
418
|
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
371
419
|
}
|
package/bin/branding.js
CHANGED
|
@@ -28,6 +28,8 @@ export function renderBanner(options = {}) {
|
|
|
28
28
|
// still get the same logo without ANSI styling.
|
|
29
29
|
if (!isTTY || term === 'dumb') return ` ${title}`;
|
|
30
30
|
|
|
31
|
-
const logo = color
|
|
31
|
+
const logo = color
|
|
32
|
+
? `${BRAND_ORANGE}${LOGO_ART_SMALL}${RESET}`
|
|
33
|
+
: LOGO_ART_SMALL;
|
|
32
34
|
return `${logo}\n\n ${title}`;
|
|
33
35
|
}
|
package/bin/cli.js
CHANGED
|
@@ -10,43 +10,43 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs/promises';
|
|
13
|
-
import { c } from './colors.js';
|
|
14
|
-
import { renderBanner } from './branding.js';
|
|
15
13
|
import {
|
|
14
|
+
agentCommandName,
|
|
15
|
+
agentList,
|
|
16
|
+
isAgentHelpRequest,
|
|
17
|
+
parseAgentAction,
|
|
18
|
+
resolveAgent,
|
|
19
|
+
runAgent,
|
|
20
|
+
} from './agents.js';
|
|
21
|
+
import {
|
|
22
|
+
getApiKey,
|
|
16
23
|
loginCommand,
|
|
17
24
|
logoutCommand,
|
|
18
|
-
getApiKey,
|
|
19
25
|
updateApiKeyCommand,
|
|
20
26
|
whoamiCommand,
|
|
21
27
|
} from './auth.js';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
agentList,
|
|
27
|
-
agentCommandName,
|
|
28
|
-
isAgentHelpRequest,
|
|
29
|
-
parseAgentAction,
|
|
30
|
-
} from './agents.js';
|
|
28
|
+
import { renderBanner } from './branding.js';
|
|
29
|
+
import { c } from './colors.js';
|
|
30
|
+
import { feedbackCommand, printFeedbackHelp } from './feedback.js';
|
|
31
|
+
import { resolveModelCatalog } from './models.js';
|
|
31
32
|
import {
|
|
32
33
|
configCommand,
|
|
33
34
|
DEFAULT_PROFILE,
|
|
34
35
|
loadProfile,
|
|
35
36
|
modelsCommand,
|
|
37
|
+
SUPPORTED_MODELS as PACKAGED_MODELS,
|
|
36
38
|
printConfigHelp,
|
|
37
|
-
resolvedModelSetting,
|
|
38
39
|
RUNBOOK_DEFAULTS,
|
|
39
|
-
|
|
40
|
+
resolvedModelSetting,
|
|
40
41
|
updatePlatformUrlCommand,
|
|
41
42
|
updateUrlCommand,
|
|
42
43
|
validateProfileName,
|
|
43
44
|
} from './profiles.js';
|
|
44
|
-
import { resolveModelCatalog } from './models.js';
|
|
45
|
-
import { showUpdateNotice } from './update-check.js';
|
|
46
|
-
import { runTui } from './tui.js';
|
|
47
45
|
import { sessionsCommand } from './sessions.js';
|
|
46
|
+
import { runTui } from './tui.js';
|
|
47
|
+
import { showUpdateNotice } from './update-check.js';
|
|
48
|
+
import { upgradeCommand } from './upgrade.js';
|
|
48
49
|
import { printUsageHelp, usageCommand } from './usage.js';
|
|
49
|
-
import { feedbackCommand, printFeedbackHelp } from './feedback.js';
|
|
50
50
|
|
|
51
51
|
function isHelpArg(arg) {
|
|
52
52
|
return arg === 'help' || arg === '-h' || arg === '--help';
|
|
@@ -54,7 +54,10 @@ function isHelpArg(arg) {
|
|
|
54
54
|
|
|
55
55
|
function printHelp() {
|
|
56
56
|
const agents = agentList()
|
|
57
|
-
.map(
|
|
57
|
+
.map(
|
|
58
|
+
({ name, alias, action }) =>
|
|
59
|
+
` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}${action} ${name}${c.reset}`,
|
|
60
|
+
)
|
|
58
61
|
.join('\n');
|
|
59
62
|
|
|
60
63
|
console.log(`${renderBanner()}
|
|
@@ -330,7 +333,12 @@ async function main() {
|
|
|
330
333
|
}
|
|
331
334
|
}
|
|
332
335
|
|
|
333
|
-
if (
|
|
336
|
+
if (
|
|
337
|
+
!command ||
|
|
338
|
+
command === '--help' ||
|
|
339
|
+
command === '-h' ||
|
|
340
|
+
(command === 'help' && !args[1])
|
|
341
|
+
) {
|
|
334
342
|
printHelp();
|
|
335
343
|
return;
|
|
336
344
|
}
|
package/bin/clipboard-image.js
CHANGED
|
@@ -8,13 +8,23 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
|
|
11
|
-
const PNG_SIGNATURE = Buffer.from([
|
|
11
|
+
const PNG_SIGNATURE = Buffer.from([
|
|
12
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
13
|
+
]);
|
|
12
14
|
|
|
13
15
|
export function imageContentType(buffer) {
|
|
14
|
-
if (
|
|
16
|
+
if (
|
|
17
|
+
buffer.length >= 3 &&
|
|
18
|
+
buffer[0] === 0xff &&
|
|
19
|
+
buffer[1] === 0xd8 &&
|
|
20
|
+
buffer[2] === 0xff
|
|
21
|
+
) {
|
|
15
22
|
return 'image/jpeg';
|
|
16
23
|
}
|
|
17
|
-
if (
|
|
24
|
+
if (
|
|
25
|
+
buffer.length >= PNG_SIGNATURE.length &&
|
|
26
|
+
buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
|
|
27
|
+
) {
|
|
18
28
|
return 'image/png';
|
|
19
29
|
}
|
|
20
30
|
return null;
|
|
@@ -52,7 +62,10 @@ async function readLinuxClipboard() {
|
|
|
52
62
|
];
|
|
53
63
|
for (const [command, args] of commands) {
|
|
54
64
|
try {
|
|
55
|
-
const { stdout } = await execFileAsync(command, args, {
|
|
65
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
66
|
+
encoding: 'buffer',
|
|
67
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
68
|
+
});
|
|
56
69
|
if (stdout?.length) return stdout;
|
|
57
70
|
} catch {
|
|
58
71
|
// Try the next clipboard tool.
|
|
@@ -72,7 +85,11 @@ async function readWindowsClipboard() {
|
|
|
72
85
|
Write-Output $path
|
|
73
86
|
`;
|
|
74
87
|
try {
|
|
75
|
-
const { stdout } = await execFileAsync('powershell', [
|
|
88
|
+
const { stdout } = await execFileAsync('powershell', [
|
|
89
|
+
'-NoProfile',
|
|
90
|
+
'-Command',
|
|
91
|
+
script,
|
|
92
|
+
]);
|
|
76
93
|
const outPath = String(stdout).trim();
|
|
77
94
|
if (!outPath) return null;
|
|
78
95
|
const { readFile, rm } = await import('node:fs/promises');
|
package/bin/colors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ANSI color helpers shared across the CLI. Keep redirected output machine
|
|
2
2
|
// readable and honor the standard NO_COLOR opt-out.
|
|
3
|
-
const noColor = Object.
|
|
3
|
+
const noColor = Object.hasOwn(process.env, 'NO_COLOR');
|
|
4
4
|
const forceColor =
|
|
5
5
|
process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0';
|
|
6
6
|
|
package/bin/feedback.js
CHANGED
|
@@ -9,9 +9,9 @@ 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';
|
|
@@ -64,12 +64,19 @@ export async function collectFeedbackContext(options = {}) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export function buildFeedbackPayload({
|
|
67
|
+
export function buildFeedbackPayload({
|
|
68
|
+
subject,
|
|
69
|
+
message,
|
|
70
|
+
context,
|
|
71
|
+
attachments,
|
|
72
|
+
}) {
|
|
68
73
|
const payload = {
|
|
69
74
|
subject: subject || 'CLI feedback',
|
|
70
75
|
message,
|
|
71
76
|
context: Object.fromEntries(
|
|
72
|
-
Object.entries(context ?? {}).filter(([, value]) =>
|
|
77
|
+
Object.entries(context ?? {}).filter(([, value]) =>
|
|
78
|
+
Boolean(String(value).trim()),
|
|
79
|
+
),
|
|
73
80
|
),
|
|
74
81
|
};
|
|
75
82
|
if (attachments?.length) payload.attachments = attachments;
|
|
@@ -79,7 +86,8 @@ export function buildFeedbackPayload({ subject, message, context, attachments })
|
|
|
79
86
|
export async function loadFeedbackImages(paths) {
|
|
80
87
|
const attachments = [];
|
|
81
88
|
for (const filePath of paths) {
|
|
82
|
-
if (!filePath?.trim())
|
|
89
|
+
if (!filePath?.trim())
|
|
90
|
+
throw new Error('An image path is required after --image');
|
|
83
91
|
const buffer = await fs.readFile(filePath);
|
|
84
92
|
const contentType = imageContentType(buffer);
|
|
85
93
|
if (!contentType) throw new Error(`${filePath} is not a JPEG or PNG`);
|
|
@@ -88,7 +96,12 @@ export async function loadFeedbackImages(paths) {
|
|
|
88
96
|
return attachments;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
export async function submitFeedback(
|
|
99
|
+
export async function submitFeedback(
|
|
100
|
+
apiKey,
|
|
101
|
+
platformUrl,
|
|
102
|
+
payload,
|
|
103
|
+
fetchImpl = fetch,
|
|
104
|
+
) {
|
|
92
105
|
const res = await fetchImpl(
|
|
93
106
|
`${platformUrl.replace(/\/$/, '')}${FEEDBACK_API_PATH}`,
|
|
94
107
|
{
|
|
@@ -164,7 +177,9 @@ function readAttachmentLine(onPaste) {
|
|
|
164
177
|
async function promptForAttachments(already = 0) {
|
|
165
178
|
if (process.stdin.isTTY !== true || already >= MAX_FEEDBACK_IMAGES) return [];
|
|
166
179
|
const attachments = [];
|
|
167
|
-
console.log(
|
|
180
|
+
console.log(
|
|
181
|
+
`\n Attachments (optional). Ctrl+V pastes a screenshot from the clipboard.`,
|
|
182
|
+
);
|
|
168
183
|
console.log(` ${c.dim}Press Enter when you are done.${c.reset}`);
|
|
169
184
|
while (already + attachments.length < MAX_FEEDBACK_IMAGES) {
|
|
170
185
|
const line = await readAttachmentLine(async () => {
|
|
@@ -172,11 +187,15 @@ async function promptForAttachments(already = 0) {
|
|
|
172
187
|
const buffer = await readClipboardImage();
|
|
173
188
|
const contentType = buffer ? imageContentType(buffer) : null;
|
|
174
189
|
if (!contentType || !buffer) {
|
|
175
|
-
console.log(
|
|
190
|
+
console.log(
|
|
191
|
+
`\n ${c.yellow}No JPEG or PNG image on the clipboard.${c.reset}`,
|
|
192
|
+
);
|
|
176
193
|
return;
|
|
177
194
|
}
|
|
178
195
|
attachments.push({ contentType, data: buffer.toString('base64') });
|
|
179
|
-
console.log(
|
|
196
|
+
console.log(
|
|
197
|
+
` ${c.green}Added screenshot ${already + attachments.length}.${c.reset}`,
|
|
198
|
+
);
|
|
180
199
|
});
|
|
181
200
|
if (line == null || !line.trim()) break;
|
|
182
201
|
}
|
|
@@ -186,7 +205,8 @@ async function promptForAttachments(already = 0) {
|
|
|
186
205
|
async function promptForFeedback(argv) {
|
|
187
206
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
188
207
|
try {
|
|
189
|
-
const subject =
|
|
208
|
+
const subject =
|
|
209
|
+
argv.subject || (await rl.question(`\n Subject (optional): `)).trim();
|
|
190
210
|
let message = '';
|
|
191
211
|
while (!message) {
|
|
192
212
|
message = (await rl.question(` Message: `)).trim();
|
|
@@ -208,7 +228,9 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
208
228
|
const auth = await getApiKey(options.profile);
|
|
209
229
|
if (!auth) {
|
|
210
230
|
console.log(`\n ${c.dim}Not logged in.${c.reset}`);
|
|
211
|
-
console.log(
|
|
231
|
+
console.log(
|
|
232
|
+
` Run ${c.cyan}subc ${profileFlag}login${c.reset} to get started.\n`,
|
|
233
|
+
);
|
|
212
234
|
return;
|
|
213
235
|
}
|
|
214
236
|
|
|
@@ -216,7 +238,9 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
216
238
|
if (!feedback.message) {
|
|
217
239
|
if (process.stdin.isTTY !== true) {
|
|
218
240
|
console.error(`\n ${c.red}A message is required.${c.reset}`);
|
|
219
|
-
console.error(
|
|
241
|
+
console.error(
|
|
242
|
+
` Pass one with ${c.cyan}subc feedback --message "..."${c.reset}\n`,
|
|
243
|
+
);
|
|
220
244
|
process.exitCode = 1;
|
|
221
245
|
return;
|
|
222
246
|
}
|
|
@@ -224,7 +248,10 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
224
248
|
}
|
|
225
249
|
|
|
226
250
|
const fromFlags = await loadFeedbackImages(parsed.images);
|
|
227
|
-
const fromPrompt =
|
|
251
|
+
const fromPrompt =
|
|
252
|
+
feedback.message && parsed.message
|
|
253
|
+
? []
|
|
254
|
+
: await promptForAttachments(fromFlags.length);
|
|
228
255
|
const platformUrl = getPlatformUrl(options.profile);
|
|
229
256
|
const payload = buildFeedbackPayload({
|
|
230
257
|
subject: feedback.subject,
|
|
@@ -237,9 +264,13 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
237
264
|
const res = await submitFeedback(auth.key, platformUrl, payload);
|
|
238
265
|
|
|
239
266
|
if (res.status === 404) {
|
|
240
|
-
console.error(
|
|
267
|
+
console.error(
|
|
268
|
+
`\n ${c.yellow}This platform has no feedback endpoint (404).${c.reset}`,
|
|
269
|
+
);
|
|
241
270
|
console.error(` ${c.dim}Host: ${platformUrl}${c.reset}`);
|
|
242
|
-
console.error(
|
|
271
|
+
console.error(
|
|
272
|
+
` ${c.dim}Point PLATFORM_URL at a platform that includes /api/cli/feedback.${c.reset}\n`,
|
|
273
|
+
);
|
|
243
274
|
process.exitCode = 1;
|
|
244
275
|
return;
|
|
245
276
|
}
|
|
@@ -253,14 +284,18 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
253
284
|
|
|
254
285
|
if (!res.ok) {
|
|
255
286
|
const detail = typeof data?.error === 'string' ? data.error : '';
|
|
256
|
-
const invalidKey =
|
|
287
|
+
const invalidKey =
|
|
288
|
+
detail === 'Invalid or revoked API key' ||
|
|
289
|
+
detail === 'No API key provided';
|
|
257
290
|
console.log(`\n ${c.red}✗ Couldn't send your message.${c.reset}`);
|
|
258
291
|
if (invalidKey) {
|
|
259
292
|
console.log(
|
|
260
293
|
` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.`,
|
|
261
294
|
);
|
|
262
295
|
}
|
|
263
|
-
console.log(
|
|
296
|
+
console.log(
|
|
297
|
+
` Please email ${c.cyan}${SUPPORT_EMAIL}${c.reset} directly.\n`,
|
|
298
|
+
);
|
|
264
299
|
process.exitCode = 1;
|
|
265
300
|
return;
|
|
266
301
|
}
|
|
@@ -269,12 +304,16 @@ export async function feedbackCommand(argv = [], options = {}) {
|
|
|
269
304
|
console.log(` ${c.dim}Our support team will get back to you.\n`);
|
|
270
305
|
} catch (error) {
|
|
271
306
|
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
|
|
272
|
-
console.log(
|
|
307
|
+
console.log(
|
|
308
|
+
`\n ${c.yellow}Could not reach the platform (timed out).${c.reset}`,
|
|
309
|
+
);
|
|
273
310
|
console.log(` ${c.dim}Host: ${platformUrl}${c.reset}`);
|
|
274
311
|
} else {
|
|
275
312
|
console.log(`\n ${c.red}✗ Couldn't send your message.${c.reset}`);
|
|
276
313
|
}
|
|
277
|
-
console.log(
|
|
314
|
+
console.log(
|
|
315
|
+
` Please email ${c.cyan}${SUPPORT_EMAIL}${c.reset} directly.\n`,
|
|
316
|
+
);
|
|
278
317
|
process.exitCode = 1;
|
|
279
318
|
}
|
|
280
319
|
}
|
|
@@ -7,5 +7,8 @@ const { modelCapabilities = {} } = JSON.parse(
|
|
|
7
7
|
);
|
|
8
8
|
|
|
9
9
|
export function modelSupportsVision(modelId) {
|
|
10
|
-
return
|
|
10
|
+
return (
|
|
11
|
+
Object.hasOwn(modelCapabilities, modelId) &&
|
|
12
|
+
modelCapabilities[modelId].vision === true
|
|
13
|
+
);
|
|
11
14
|
}
|
package/bin/models.js
CHANGED
|
@@ -99,8 +99,10 @@ 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
|
});
|
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|