subconscious-cli 5.0.3 → 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 +23 -5
- package/bin/agents.js +211 -71
- 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/README.md +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 +105 -34
- 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 +21 -6
package/bin/usage.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Usage display for `subc usage` — fetches billing/quota from the platform API.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { c } from './colors.js';
|
|
6
5
|
import { getApiKey, getPlatformUrl } from './auth.js';
|
|
6
|
+
import { c } from './colors.js';
|
|
7
7
|
import { DEFAULT_PROFILE } from './profiles.js';
|
|
8
8
|
import { printLoginUpgradeWarning } from './upgrade.js';
|
|
9
9
|
|
|
@@ -33,13 +33,18 @@ export function formatCurrency(value) {
|
|
|
33
33
|
|
|
34
34
|
export function shortModelSlug(slug) {
|
|
35
35
|
const text = String(slug ?? '').trim();
|
|
36
|
-
return text.startsWith('subconscious/')
|
|
36
|
+
return text.startsWith('subconscious/')
|
|
37
|
+
? text.slice('subconscious/'.length)
|
|
38
|
+
: text;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export function isLegacyUsagePayload(data) {
|
|
40
42
|
if (!data || typeof data !== 'object') return true;
|
|
41
|
-
if ('credits' in data && data.plan && typeof data.plan === 'object')
|
|
42
|
-
|
|
43
|
+
if ('credits' in data && data.plan && typeof data.plan === 'object')
|
|
44
|
+
return false;
|
|
45
|
+
return (
|
|
46
|
+
'dailyTokenCeiling' in data || 'consumedToday' in data || !('plan' in data)
|
|
47
|
+
);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export function meterColor(percentage) {
|
|
@@ -120,7 +125,9 @@ export function formatUsageDisplay(data) {
|
|
|
120
125
|
);
|
|
121
126
|
} else {
|
|
122
127
|
const label =
|
|
123
|
-
described.kind === 'credit'
|
|
128
|
+
described.kind === 'credit'
|
|
129
|
+
? 'of daily credit used'
|
|
130
|
+
: 'of daily tokens used';
|
|
124
131
|
lines.push(
|
|
125
132
|
` ${renderProgressBar(described.percent)} ${formatAllowancePercent(described.percent)} ${label}`,
|
|
126
133
|
);
|
|
@@ -155,9 +162,7 @@ export function formatUsageDisplay(data) {
|
|
|
155
162
|
);
|
|
156
163
|
for (const model of models) {
|
|
157
164
|
const label = shortModelSlug(model.slug).padEnd(slugWidth);
|
|
158
|
-
lines.push(
|
|
159
|
-
` ${label} ${formatTokens(model.consumedToday ?? 0)}`,
|
|
160
|
-
);
|
|
165
|
+
lines.push(` ${label} ${formatTokens(model.consumedToday ?? 0)}`);
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -166,10 +171,13 @@ export function formatUsageDisplay(data) {
|
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
export async function fetchUsage(apiKey, platformUrl, fetchImpl = fetch) {
|
|
169
|
-
const res = await fetchImpl(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
const res = await fetchImpl(
|
|
175
|
+
`${platformUrl.replace(/\/$/, '')}${USAGE_API_PATH}`,
|
|
176
|
+
{
|
|
177
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
178
|
+
signal: AbortSignal.timeout(5000),
|
|
179
|
+
},
|
|
180
|
+
);
|
|
173
181
|
return res;
|
|
174
182
|
}
|
|
175
183
|
|
|
@@ -183,7 +191,9 @@ export async function usageCommand(argv = [], options = {}) {
|
|
|
183
191
|
const auth = await getApiKey(options.profile);
|
|
184
192
|
if (!auth) {
|
|
185
193
|
console.log(`\n ${c.dim}Not logged in.${c.reset}`);
|
|
186
|
-
console.log(
|
|
194
|
+
console.log(
|
|
195
|
+
` Run ${c.cyan}subc ${profileFlag}login${c.reset} to get started.\n`,
|
|
196
|
+
);
|
|
187
197
|
return;
|
|
188
198
|
}
|
|
189
199
|
|
|
@@ -236,7 +246,9 @@ export async function usageCommand(argv = [], options = {}) {
|
|
|
236
246
|
console.log(formatUsageDisplay(data));
|
|
237
247
|
} catch (error) {
|
|
238
248
|
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
|
|
239
|
-
console.log(
|
|
249
|
+
console.log(
|
|
250
|
+
`\n ${c.yellow}Could not reach the platform (timed out).${c.reset}`,
|
|
251
|
+
);
|
|
240
252
|
console.log(` ${c.dim}Host: ${platformUrl}${c.reset}\n`);
|
|
241
253
|
return;
|
|
242
254
|
}
|
package/bin/windows/agents.js
CHANGED
|
@@ -1,79 +1,182 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
import readline from 'node:readline';
|
|
4
|
-
import { windowsEnv, windowsBinDirs, windowsPath, resolveWindowsExecutable, readWindowsVersion, runWindows } from './process.js';
|
|
5
|
-
import { windowsInstallSpec, installWindowsAgent } from './install.js';
|
|
6
|
-
import { windowsSetup } from './setup.js';
|
|
7
|
-
import { windowsLaunch, executeWindowsLaunch } from './launch.js';
|
|
8
4
|
import { parseOptions } from './common.js';
|
|
5
|
+
import { installWindowsAgent, windowsInstallSpec } from './install.js';
|
|
6
|
+
import { executeWindowsLaunch, windowsLaunch } from './launch.js';
|
|
7
|
+
import {
|
|
8
|
+
readWindowsVersion,
|
|
9
|
+
resolveWindowsExecutable,
|
|
10
|
+
runWindows,
|
|
11
|
+
windowsBinDirs,
|
|
12
|
+
windowsEnv,
|
|
13
|
+
windowsPath,
|
|
14
|
+
} from './process.js';
|
|
15
|
+
import { windowsSetup } from './setup.js';
|
|
9
16
|
|
|
10
|
-
const askInstall = name =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
const askInstall = (name) =>
|
|
18
|
+
new Promise((resolve) => {
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
rl.question(
|
|
24
|
+
` ${name} isn't installed. Install it now? [Y/n] `,
|
|
25
|
+
(answer) => {
|
|
26
|
+
rl.close();
|
|
27
|
+
resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase()));
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
});
|
|
14
31
|
|
|
15
32
|
export async function runWindowsAgent(agent, argv, dependencies) {
|
|
16
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
profile,
|
|
35
|
+
parseAgentAction,
|
|
36
|
+
extractModel,
|
|
37
|
+
requireApiKey,
|
|
38
|
+
resolvedModelsForLaunch,
|
|
39
|
+
selectLaunchModel,
|
|
40
|
+
runbookEnv,
|
|
41
|
+
} = dependencies;
|
|
17
42
|
const parsed = parseAgentAction(agent, argv);
|
|
18
|
-
let args =
|
|
43
|
+
let args =
|
|
44
|
+
parsed.action !== 'launch' && parsed.action === argv[0]
|
|
45
|
+
? argv.slice(1)
|
|
46
|
+
: argv;
|
|
19
47
|
const environment = windowsEnv(profile?.values, process.env);
|
|
20
48
|
const preferredDirs = agent.runbook?.installDir
|
|
21
|
-
? [
|
|
22
|
-
|
|
49
|
+
? [
|
|
50
|
+
path.resolve(
|
|
51
|
+
environment[agent.runbook.installDirEnv] ||
|
|
52
|
+
path.join(os.homedir(), agent.runbook.installDir),
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
: [];
|
|
56
|
+
environment.PATH = windowsPath([
|
|
57
|
+
...preferredDirs,
|
|
58
|
+
...(environment.PATH || '').split(';'),
|
|
59
|
+
...windowsBinDirs(environment),
|
|
60
|
+
]);
|
|
23
61
|
if (agent.id === 'subconscious-code' && parsed.action === 'install') {
|
|
24
62
|
if (args.length) throw new Error('Usage: subc marathon install');
|
|
25
63
|
return finish(await installWindowsAgent(agent.id, environment));
|
|
26
64
|
}
|
|
27
65
|
if (['status', 'uninstall'].includes(parsed.action)) {
|
|
28
|
-
return finish(
|
|
66
|
+
return finish(
|
|
67
|
+
await windowsSetup(agent.id, parsed.action, args, environment),
|
|
68
|
+
);
|
|
29
69
|
}
|
|
30
70
|
|
|
31
71
|
// Honor -- after which all arguments belong to the underlying agent.
|
|
32
72
|
const boundary = args.indexOf('--');
|
|
33
73
|
const tail = boundary < 0 ? [] : args.slice(boundary);
|
|
34
|
-
const extracted = extractModel(
|
|
74
|
+
const extracted = extractModel(
|
|
75
|
+
boundary < 0 ? args : args.slice(0, boundary),
|
|
76
|
+
profile,
|
|
77
|
+
);
|
|
35
78
|
args = [...extracted.rest, ...tail];
|
|
36
79
|
let explicit = {};
|
|
37
80
|
if (parsed.action === 'install' || agent.id === 'claude-code') {
|
|
38
|
-
const handled = parseOptions(
|
|
81
|
+
const handled = parseOptions(
|
|
82
|
+
boundary < 0 ? args : args.slice(0, args.indexOf('--')),
|
|
83
|
+
{ '--api-key': 1, '--gateway-url': 1 },
|
|
84
|
+
);
|
|
39
85
|
explicit = handled.options;
|
|
40
86
|
}
|
|
41
|
-
const key = explicit['--api-key'] || await requireApiKey(profile, agent);
|
|
87
|
+
const key = explicit['--api-key'] || (await requireApiKey(profile, agent));
|
|
42
88
|
if (!key) return finish(1);
|
|
43
|
-
const catalogProfile = explicit['--gateway-url']
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
const catalogProfile = explicit['--gateway-url']
|
|
90
|
+
? {
|
|
91
|
+
...profile,
|
|
92
|
+
values: { ...profile?.values, GATEWAY_URL: explicit['--gateway-url'] },
|
|
93
|
+
}
|
|
94
|
+
: profile;
|
|
95
|
+
const catalog = await resolvedModelsForLaunch(
|
|
96
|
+
catalogProfile,
|
|
97
|
+
key,
|
|
98
|
+
extracted.model,
|
|
99
|
+
);
|
|
100
|
+
const model = selectLaunchModel(
|
|
101
|
+
extracted.model,
|
|
102
|
+
extracted.modelSource,
|
|
103
|
+
catalog,
|
|
104
|
+
);
|
|
105
|
+
if (extracted.model && model !== extracted.model)
|
|
106
|
+
console.error(
|
|
107
|
+
`Configured model ${extracted.model} is not in the live catalog; using ${model}.`,
|
|
108
|
+
);
|
|
47
109
|
let executable;
|
|
48
110
|
if (parsed.action === 'launch') {
|
|
49
|
-
executable = resolveWindowsExecutable(agent.bin, {
|
|
111
|
+
executable = resolveWindowsExecutable(agent.bin, {
|
|
112
|
+
env: environment,
|
|
113
|
+
preferredDirs,
|
|
114
|
+
});
|
|
50
115
|
if (!executable) {
|
|
51
116
|
const installer = windowsInstallSpec(agent.id, environment);
|
|
52
|
-
if (
|
|
53
|
-
|
|
117
|
+
if (
|
|
118
|
+
!installer ||
|
|
119
|
+
!process.stdin.isTTY ||
|
|
120
|
+
!process.stdout.isTTY ||
|
|
121
|
+
!(await askInstall(agent.name))
|
|
122
|
+
) {
|
|
123
|
+
console.error(
|
|
124
|
+
`${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`,
|
|
125
|
+
);
|
|
54
126
|
return finish(127);
|
|
55
127
|
}
|
|
56
128
|
const installed = await installWindowsAgent(agent.id, environment);
|
|
57
129
|
if (installed) return finish(installed);
|
|
58
|
-
executable = resolveWindowsExecutable(agent.bin, {
|
|
130
|
+
executable = resolveWindowsExecutable(agent.bin, {
|
|
131
|
+
env: environment,
|
|
132
|
+
preferredDirs,
|
|
133
|
+
});
|
|
59
134
|
if (!executable) {
|
|
60
|
-
console.error(
|
|
135
|
+
console.error(
|
|
136
|
+
`Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`,
|
|
137
|
+
);
|
|
61
138
|
return finish(127);
|
|
62
139
|
}
|
|
63
140
|
}
|
|
64
141
|
if (agent.id === 'claude-code') {
|
|
65
|
-
const version = dependencies.parseClaudeVersion(
|
|
66
|
-
|
|
142
|
+
const version = dependencies.parseClaudeVersion(
|
|
143
|
+
readWindowsVersion(executable, environment),
|
|
144
|
+
);
|
|
145
|
+
if (dependencies.claudeVersionNeedsUpgrade(version))
|
|
146
|
+
console.error(
|
|
147
|
+
`Minimum supported Claude Code version is ${dependencies.minimumClaudeVersion}; installed version is ${version}. Please upgrade Claude Code.`,
|
|
148
|
+
);
|
|
67
149
|
}
|
|
68
150
|
}
|
|
69
151
|
// Reuse profile/catalog semantics, but use Windows path and environment rules.
|
|
70
|
-
const env = windowsEnv(
|
|
152
|
+
const env = windowsEnv(
|
|
153
|
+
runbookEnv(
|
|
154
|
+
key,
|
|
155
|
+
model,
|
|
156
|
+
executable && path.dirname(executable),
|
|
157
|
+
catalogProfile,
|
|
158
|
+
agent,
|
|
159
|
+
catalog.models,
|
|
160
|
+
),
|
|
161
|
+
{
|
|
162
|
+
PATH: windowsPath(
|
|
163
|
+
executable ? [path.dirname(executable)] : [],
|
|
164
|
+
environment.PATH,
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
71
168
|
if (explicit['--gateway-url']) env.GATEWAY_URL = explicit['--gateway-url'];
|
|
72
|
-
if (parsed.action === 'install')
|
|
169
|
+
if (parsed.action === 'install')
|
|
170
|
+
return finish(await windowsSetup(agent.id, 'install', args, env));
|
|
73
171
|
if (agent.id === 'pi') await windowsSetup('pi', 'install', [], env);
|
|
74
172
|
if (agent.id === 'codex') {
|
|
75
|
-
try {
|
|
76
|
-
|
|
173
|
+
try {
|
|
174
|
+
await windowsSetup('codex', 'install', [], env, { log: () => {} });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(
|
|
177
|
+
`Could not refresh Codex hooks (launch will continue): ${error.message}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
77
180
|
}
|
|
78
181
|
const spec = await windowsLaunch(agent.id, args, env);
|
|
79
182
|
if (spec.command === agent.bin) spec.command = executable;
|
package/bin/windows/claude.js
CHANGED
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
// Windows-only equivalent of the existing Claude shell launch.
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { resolvedModelSetting } from '../profiles.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { parseOptions, positiveInteger } from './common.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULTS = JSON.parse(
|
|
7
|
+
readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'),
|
|
8
|
+
).defaults;
|
|
6
9
|
function claudePickerSettings(models) {
|
|
7
|
-
return {
|
|
10
|
+
return {
|
|
11
|
+
availableModels: models,
|
|
12
|
+
modelPicker: {
|
|
13
|
+
replaceBuiltInOptions: true,
|
|
14
|
+
options: models.map((model) => ({
|
|
15
|
+
model,
|
|
16
|
+
label: model,
|
|
17
|
+
description: `Subconscious model ${model}`,
|
|
18
|
+
})),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
8
21
|
}
|
|
9
22
|
|
|
10
23
|
export function claudeNativeLaunch(argv, environment) {
|
|
11
24
|
const { options, rest: passthrough } = parseOptions(argv, {
|
|
12
|
-
'--gateway-url': 1,
|
|
13
|
-
'--
|
|
25
|
+
'--gateway-url': 1,
|
|
26
|
+
'--api-key': 1,
|
|
27
|
+
'--model': 1,
|
|
28
|
+
'--compact-window': 1,
|
|
29
|
+
'--max-context-tokens': 1,
|
|
14
30
|
});
|
|
15
|
-
const gatewayUrl =
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
31
|
+
const gatewayUrl =
|
|
32
|
+
options['--gateway-url'] || environment.GATEWAY_URL?.trim() || '';
|
|
33
|
+
const apiKey =
|
|
34
|
+
options['--api-key'] ||
|
|
35
|
+
environment.CLAUDE_CODE_API_KEY?.trim() ||
|
|
36
|
+
environment.API_KEY?.trim() ||
|
|
37
|
+
'';
|
|
38
|
+
const model =
|
|
39
|
+
options['--model'] || environment.MODEL?.trim() || DEFAULTS.model;
|
|
40
|
+
const compactWindow =
|
|
41
|
+
options['--compact-window'] ||
|
|
19
42
|
environment.CLAUDE_CODE_AUTO_COMPACT_WINDOW?.trim() ||
|
|
20
43
|
environment.COMPACT_WINDOW?.trim() ||
|
|
21
44
|
'1000000';
|
|
22
|
-
const maxContextTokens =
|
|
45
|
+
const maxContextTokens =
|
|
46
|
+
options['--max-context-tokens'] ||
|
|
23
47
|
environment.CLAUDE_CODE_MAX_CONTEXT_TOKENS?.trim() ||
|
|
24
48
|
environment.MAX_CONTEXT_TOKENS?.trim() ||
|
|
25
49
|
'3000000';
|
|
26
50
|
|
|
27
51
|
if (!gatewayUrl || !apiKey) {
|
|
28
|
-
throw new Error(
|
|
52
|
+
throw new Error(
|
|
53
|
+
'GATEWAY_URL and API_KEY are required to launch Claude Code',
|
|
54
|
+
);
|
|
29
55
|
}
|
|
30
56
|
positiveInteger(compactWindow, 'compact-window');
|
|
31
57
|
positiveInteger(maxContextTokens, 'max-context-tokens');
|
|
32
58
|
|
|
33
|
-
const effectiveGatewayUrl =
|
|
34
|
-
|
|
59
|
+
const effectiveGatewayUrl =
|
|
60
|
+
environment.CLAUDE_GATEWAY_URL?.trim() || gatewayUrl;
|
|
61
|
+
const subagentModel =
|
|
62
|
+
resolvedModelSetting(environment.CLAUDE_CODE_SUBAGENT_MODEL) || model;
|
|
35
63
|
const settings =
|
|
36
64
|
environment.SUBC_CLAUDE_SETTINGS?.trim() ||
|
|
37
65
|
JSON.stringify(claudePickerSettings([model], model));
|
|
@@ -41,7 +69,8 @@ export function claudeNativeLaunch(argv, environment) {
|
|
|
41
69
|
ANTHROPIC_BASE_URL: effectiveGatewayUrl,
|
|
42
70
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
43
71
|
ANTHROPIC_MODEL: environment.ANTHROPIC_MODEL?.trim() || model,
|
|
44
|
-
ANTHROPIC_SMALL_FAST_MODEL:
|
|
72
|
+
ANTHROPIC_SMALL_FAST_MODEL:
|
|
73
|
+
environment.ANTHROPIC_SMALL_FAST_MODEL?.trim() || model,
|
|
45
74
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:
|
|
46
75
|
environment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY?.trim() || '0',
|
|
47
76
|
CLAUDE_CODE_SUBAGENT_MODEL: subagentModel,
|
package/bin/windows/common.js
CHANGED
|
@@ -1,58 +1,95 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import { randomUUID } from 'node:crypto';
|
|
4
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
|
|
6
|
-
export const defaults = JSON.parse(
|
|
6
|
+
export const defaults = JSON.parse(
|
|
7
|
+
readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'),
|
|
8
|
+
).defaults;
|
|
7
9
|
export const value = (env, key, fallback = '') => env[key]?.trim() || fallback;
|
|
8
|
-
export const origin = url =>
|
|
10
|
+
export const origin = (url) =>
|
|
11
|
+
url
|
|
12
|
+
.replace(/\/+$/, '')
|
|
13
|
+
.replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
|
|
9
14
|
export function positiveInteger(input, name) {
|
|
10
|
-
if (
|
|
15
|
+
if (
|
|
16
|
+
!/^[1-9][0-9]*$/.test(String(input)) ||
|
|
17
|
+
!Number.isSafeInteger(Number(input))
|
|
18
|
+
)
|
|
19
|
+
throw new Error(`${name} must be a positive integer`);
|
|
11
20
|
return Number(input);
|
|
12
21
|
}
|
|
13
22
|
export function modelIds(env) {
|
|
14
|
-
const models = [
|
|
15
|
-
|
|
23
|
+
const models = [
|
|
24
|
+
...new Set(
|
|
25
|
+
[
|
|
26
|
+
value(env, 'MODEL', defaults.model),
|
|
27
|
+
...value(env, 'SUBCONSCIOUS_MODELS', defaults.models.join('\n')).split(
|
|
28
|
+
/\r?\n/,
|
|
29
|
+
),
|
|
30
|
+
].filter(Boolean),
|
|
31
|
+
),
|
|
32
|
+
];
|
|
33
|
+
for (const model of models)
|
|
34
|
+
if (!/^[-A-Za-z0-9._:/+]+$/.test(model))
|
|
35
|
+
throw new Error(`Invalid model id: ${model}`);
|
|
16
36
|
return models;
|
|
17
37
|
}
|
|
18
38
|
export function parseOptions(argv, schema, { strict = false } = {}) {
|
|
19
|
-
const options = {},
|
|
39
|
+
const options = {},
|
|
40
|
+
rest = [];
|
|
20
41
|
for (let i = 0; i < argv.length; i++) {
|
|
21
42
|
if (argv[i] === '--') {
|
|
22
|
-
if (strict && i + 1 < argv.length)
|
|
23
|
-
|
|
43
|
+
if (strict && i + 1 < argv.length)
|
|
44
|
+
throw new Error(`Unexpected setup argument: ${argv[i + 1]}`);
|
|
45
|
+
rest.push(...argv.slice(i + 1));
|
|
46
|
+
break;
|
|
24
47
|
}
|
|
25
48
|
const [flag, ...equals] = argv[i].split('=');
|
|
26
49
|
if (!Object.hasOwn(schema, flag)) {
|
|
27
50
|
if (strict) throw new Error(`Unknown setup option: ${argv[i]}`);
|
|
28
|
-
rest.push(argv[i]);
|
|
51
|
+
rest.push(argv[i]);
|
|
52
|
+
continue;
|
|
29
53
|
}
|
|
30
54
|
if (schema[flag] === true) {
|
|
31
55
|
if (equals.length) throw new Error(`${flag} does not take a value`);
|
|
32
|
-
options[flag] = true;
|
|
56
|
+
options[flag] = true;
|
|
57
|
+
continue;
|
|
33
58
|
}
|
|
34
59
|
const next = equals.length ? equals.join('=') : argv[++i];
|
|
35
|
-
if (!next || next.startsWith('--'))
|
|
60
|
+
if (!next || next.startsWith('--'))
|
|
61
|
+
throw new Error(`${flag} requires a value`);
|
|
36
62
|
options[flag] = next;
|
|
37
63
|
}
|
|
38
64
|
return { options, rest };
|
|
39
65
|
}
|
|
40
66
|
export async function readJson(file, fallback) {
|
|
41
|
-
try {
|
|
42
|
-
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse((await fs.readFile(file, 'utf8')).replace(/^\uFEFF/, ''));
|
|
69
|
+
} catch (error) {
|
|
43
70
|
if (error.code === 'ENOENT') return fallback;
|
|
44
|
-
throw new Error(
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Cannot read ${file}; leaving it unchanged: ${error.message}`,
|
|
73
|
+
);
|
|
45
74
|
}
|
|
46
75
|
}
|
|
47
76
|
export function object(value, description) {
|
|
48
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
77
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
78
|
+
throw new Error(
|
|
79
|
+
`${description} must be a JSON object; leaving it unchanged`,
|
|
80
|
+
);
|
|
49
81
|
return value;
|
|
50
82
|
}
|
|
51
83
|
export async function writeJson(file, data) {
|
|
52
84
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
53
85
|
const temp = `${file}.${randomUUID()}.tmp`;
|
|
54
86
|
try {
|
|
55
|
-
await fs.writeFile(temp, JSON.stringify(data, null, 2)
|
|
87
|
+
await fs.writeFile(temp, `${JSON.stringify(data, null, 2)}\n`, {
|
|
88
|
+
mode: 0o600,
|
|
89
|
+
flag: 'wx',
|
|
90
|
+
});
|
|
56
91
|
await fs.rename(temp, file);
|
|
57
|
-
} finally {
|
|
92
|
+
} finally {
|
|
93
|
+
await fs.rm(temp, { force: true });
|
|
94
|
+
}
|
|
58
95
|
}
|