subconscious-cli 4.1.3 → 4.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/bin/agents.js +29 -6
- package/bin/cli.js +13 -3
- package/bin/models.js +51 -9
- 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 +61 -0
- package/bin/profiles.js +25 -0
- package/bin/registry.generated.json +1 -1
- package/bin/runbook/claude-code/run.sh +5 -0
- package/bin/runbook/codex/hooks-lib.sh +32 -2
- package/bin/runbook/codex/install.sh +10 -0
- package/bin/runbook/codex/run.sh +111 -55
- package/bin/runbook/opencode/install.sh +10 -34
- package/bin/runbook/opencode/provider.sh +93 -0
- package/bin/runbook/opencode/run.sh +3 -18
- package/bin/tui.js +247 -51
- package/bin/update-check.js +29 -8
- package/bin/usage.js +51 -19
- package/bin/windows/launch.js +4 -10
- package/package.json +1 -1
package/bin/tui.js
CHANGED
|
@@ -6,8 +6,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
|
|
7
7
|
import { agentList } from './agents.js';
|
|
8
8
|
import { DEFAULT_PLATFORM_URL, getApiKey, getPlatformUrl } from './auth.js';
|
|
9
|
-
import { resolveModelCatalog } from './models.js';
|
|
9
|
+
import { isAbortError, normalizeModelIds, resolveModelCatalog } from './models.js';
|
|
10
10
|
import { discoverSessions, SESSION_HARNESSES } from './sessions.js';
|
|
11
|
+
import { compareVersions, fetchLatestVersion } from './update-check.js';
|
|
11
12
|
import {
|
|
12
13
|
DEFAULT_PROFILE,
|
|
13
14
|
listProfiles,
|
|
@@ -47,27 +48,43 @@ async function pathExists(file) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export async function tuiSourceIsNewerThan(binary, sourceFile = path.join(TUI_SOURCE_DIR, 'cmd/subc-tui/main.go')) {
|
|
52
|
+
try {
|
|
53
|
+
const [binaryStat, sourceStat] = await Promise.all([fs.stat(binary), fs.stat(sourceFile)]);
|
|
54
|
+
return sourceStat.mtimeMs > binaryStat.mtimeMs;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sourceTuiCommand() {
|
|
61
|
+
return {
|
|
62
|
+
command: 'go',
|
|
63
|
+
args: ['run', './cmd/subc-tui'],
|
|
64
|
+
cwd: TUI_SOURCE_DIR,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
export async function resolveTuiExecutable(options = {}) {
|
|
51
69
|
const override = options.binary || process.env.SUBC_TUI_BIN?.trim();
|
|
52
|
-
if (override)
|
|
70
|
+
if (override) {
|
|
71
|
+
return { command: override, args: options.binaryArgs || [], cwd: undefined };
|
|
72
|
+
}
|
|
53
73
|
|
|
74
|
+
const hasSource = await pathExists(path.join(TUI_SOURCE_DIR, 'go.mod'));
|
|
54
75
|
const target = nativeTargetName(options.platform, options.arch);
|
|
55
76
|
if (target) {
|
|
56
77
|
const packaged = path.join(BIN_DIR, 'native', target);
|
|
57
|
-
|
|
78
|
+
// Source checkouts keep a cached host binary for speed. If TUI source is
|
|
79
|
+
// newer, that cache is stale (for example it will reject --updates).
|
|
80
|
+
if (await pathExists(packaged) && !(hasSource && (await tuiSourceIsNewerThan(packaged)))) {
|
|
58
81
|
return { command: packaged, args: [], cwd: undefined };
|
|
59
82
|
}
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
// Source checkouts can run the TUI without committing native build output.
|
|
63
86
|
// Published packages always contain a prebuilt platform binary.
|
|
64
|
-
if (
|
|
65
|
-
return {
|
|
66
|
-
command: 'go',
|
|
67
|
-
args: ['run', './cmd/subc-tui'],
|
|
68
|
-
cwd: TUI_SOURCE_DIR,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
87
|
+
if (hasSource) return sourceTuiCommand();
|
|
71
88
|
return null;
|
|
72
89
|
}
|
|
73
90
|
|
|
@@ -98,7 +115,71 @@ async function packageVersion() {
|
|
|
98
115
|
return pkg.version;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
|
-
|
|
118
|
+
function serializeSessions(sessions = []) {
|
|
119
|
+
return sessions.map((session) => ({
|
|
120
|
+
key: session.key,
|
|
121
|
+
harness: session.harness,
|
|
122
|
+
harnessName: session.harnessName,
|
|
123
|
+
title: session.title,
|
|
124
|
+
cwd: session.cwd,
|
|
125
|
+
updatedAt: session.updatedAt,
|
|
126
|
+
model: session.model,
|
|
127
|
+
portable: session.portable,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sessionHarnessList() {
|
|
132
|
+
return Object.entries(SESSION_HARNESSES).map(([id, harness]) => ({
|
|
133
|
+
id,
|
|
134
|
+
name: harness.name,
|
|
135
|
+
portable: harness.portable,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function agentMenuItems() {
|
|
140
|
+
return agentList().map((agent) => ({
|
|
141
|
+
command: agent.alias,
|
|
142
|
+
name: agent.name,
|
|
143
|
+
action: agent.action,
|
|
144
|
+
description: agent.description,
|
|
145
|
+
launch: agent.launch,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ATOMIC_REPLACE_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
|
|
150
|
+
|
|
151
|
+
function delay(ms) {
|
|
152
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function replaceFile(tmp, file) {
|
|
156
|
+
try {
|
|
157
|
+
await fs.rename(tmp, file);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (!ATOMIC_REPLACE_CODES.has(error.code)) throw error;
|
|
160
|
+
// Windows rejects a rename over a file another process still has open.
|
|
161
|
+
await fs.rm(file, { force: true });
|
|
162
|
+
await fs.rename(tmp, file);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function writeAtomicJson(file, value) {
|
|
167
|
+
const payload = `${JSON.stringify(value)}\n`;
|
|
168
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
169
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
170
|
+
try {
|
|
171
|
+
await fs.writeFile(tmp, payload, { mode: 0o600 });
|
|
172
|
+
await replaceFile(tmp, file);
|
|
173
|
+
return;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
await fs.rm(tmp, { force: true }).catch(() => {});
|
|
176
|
+
if (!ATOMIC_REPLACE_CODES.has(error.code) || attempt === 7) throw error;
|
|
177
|
+
await delay(20 * (attempt + 1));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function createLocalTuiState(profileName = DEFAULT_PROFILE, options = {}) {
|
|
102
183
|
const activeProfile = options.profile || (await loadProfile(profileName));
|
|
103
184
|
const names = [...new Set([profileName, ...(await listProfiles())])].sort((a, b) => {
|
|
104
185
|
if (a === profileName) return -1;
|
|
@@ -118,26 +199,17 @@ export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}
|
|
|
118
199
|
}),
|
|
119
200
|
);
|
|
120
201
|
|
|
121
|
-
const auth = await getApiKey(activeProfile);
|
|
122
202
|
const requestedModel = selectedModelFor(activeProfile);
|
|
123
203
|
const gatewayUrl = gatewayFor(activeProfile);
|
|
124
204
|
const platformUrl = platformFor(activeProfile);
|
|
125
|
-
const catalog = await resolveModelCatalog({
|
|
126
|
-
baseUrl: gatewayUrl,
|
|
127
|
-
apiKey: auth?.key,
|
|
128
|
-
selectedModel: requestedModel,
|
|
129
|
-
fallbackModels: PACKAGED_MODELS,
|
|
130
|
-
});
|
|
131
|
-
const selectedModel = requestedModel;
|
|
132
|
-
const sessions = await discoverSessions({ max: options.maxSessions || 500 });
|
|
133
205
|
|
|
134
206
|
return {
|
|
135
207
|
version: await packageVersion(),
|
|
136
208
|
activeProfile: profileName,
|
|
137
209
|
profilePath: activeProfile.path,
|
|
138
210
|
profiles,
|
|
139
|
-
models:
|
|
140
|
-
selectedModel,
|
|
211
|
+
models: normalizeModelIds([requestedModel, ...PACKAGED_MODELS], requestedModel),
|
|
212
|
+
selectedModel: requestedModel,
|
|
141
213
|
subagentModel: subagentModelFor(activeProfile),
|
|
142
214
|
gatewayUrl,
|
|
143
215
|
savedGatewayUrl:
|
|
@@ -148,39 +220,138 @@ export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}
|
|
|
148
220
|
savedPlatformUrl:
|
|
149
221
|
activeProfile.values.PLATFORM_URL?.trim().replace(/\/+$/, '') || DEFAULT_PLATFORM_URL,
|
|
150
222
|
platformOverridden: Boolean(process.env.SUBCONSCIOUS_URL?.trim()),
|
|
151
|
-
modelError:
|
|
152
|
-
modelSource:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
model: session.model,
|
|
161
|
-
portable: session.portable,
|
|
162
|
-
})),
|
|
163
|
-
sessionHarnesses: Object.entries(SESSION_HARNESSES).map(([id, harness]) => ({
|
|
164
|
-
id,
|
|
165
|
-
name: harness.name,
|
|
166
|
-
portable: harness.portable,
|
|
167
|
-
})),
|
|
168
|
-
agents: agentList().map((agent) => ({
|
|
169
|
-
command: agent.alias,
|
|
170
|
-
name: agent.name,
|
|
171
|
-
action: agent.action,
|
|
172
|
-
description: agent.description,
|
|
173
|
-
launch: agent.launch,
|
|
174
|
-
})),
|
|
223
|
+
modelError: '',
|
|
224
|
+
modelSource: 'packaged',
|
|
225
|
+
modelsLoading: true,
|
|
226
|
+
sessionsLoading: true,
|
|
227
|
+
sessions: [],
|
|
228
|
+
sessionHarnesses: sessionHarnessList(),
|
|
229
|
+
agents: agentMenuItems(),
|
|
230
|
+
updateAvailable: false,
|
|
231
|
+
latestVersion: '',
|
|
175
232
|
};
|
|
176
233
|
}
|
|
177
234
|
|
|
235
|
+
function whenAborted(signal) {
|
|
236
|
+
return new Promise((_, reject) => {
|
|
237
|
+
const fail = () => {
|
|
238
|
+
const error = new Error('cancelled');
|
|
239
|
+
error.name = 'AbortError';
|
|
240
|
+
reject(error);
|
|
241
|
+
};
|
|
242
|
+
if (signal.aborted) {
|
|
243
|
+
fail();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
signal.addEventListener('abort', fail, { once: true });
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function raceAbort(signal, promise) {
|
|
251
|
+
if (!signal) return promise;
|
|
252
|
+
return Promise.race([promise, whenAborted(signal)]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function loadRemoteTuiUpdates(state, options = {}) {
|
|
256
|
+
const signal = options.signal;
|
|
257
|
+
const writePatch = options.writePatch || (async () => {});
|
|
258
|
+
const resolveCatalog = options.resolveCatalog || resolveModelCatalog;
|
|
259
|
+
const discover = options.discoverSessions || discoverSessions;
|
|
260
|
+
const fetchLatest = options.fetchLatestVersion || fetchLatestVersion;
|
|
261
|
+
const readApiKey = options.getApiKey || getApiKey;
|
|
262
|
+
const updateDisabled =
|
|
263
|
+
options.disableUpdateCheck ?? process.env.SUBC_DISABLE_UPDATE_CHECK?.trim() === '1';
|
|
264
|
+
|
|
265
|
+
const tasks = [
|
|
266
|
+
(async () => {
|
|
267
|
+
const profile = options.profile || (await loadProfile(state.activeProfile));
|
|
268
|
+
if (signal?.aborted) return;
|
|
269
|
+
const auth = await readApiKey(profile);
|
|
270
|
+
if (signal?.aborted) return;
|
|
271
|
+
const catalog = await resolveCatalog({
|
|
272
|
+
baseUrl: state.gatewayUrl,
|
|
273
|
+
apiKey: auth?.key,
|
|
274
|
+
selectedModel: state.selectedModel,
|
|
275
|
+
fallbackModels: PACKAGED_MODELS,
|
|
276
|
+
fetchImpl: options.fetchImpl,
|
|
277
|
+
timeoutMs: options.timeoutMs,
|
|
278
|
+
signal,
|
|
279
|
+
});
|
|
280
|
+
if (signal?.aborted) return;
|
|
281
|
+
await writePatch({
|
|
282
|
+
models: catalog.models,
|
|
283
|
+
modelError: catalog.error?.message || '',
|
|
284
|
+
modelSource: catalog.source,
|
|
285
|
+
modelsLoading: false,
|
|
286
|
+
});
|
|
287
|
+
})(),
|
|
288
|
+
(async () => {
|
|
289
|
+
const sessions = await raceAbort(
|
|
290
|
+
signal,
|
|
291
|
+
discover({
|
|
292
|
+
max: options.maxSessions || 500,
|
|
293
|
+
home: options.home,
|
|
294
|
+
roots: options.roots,
|
|
295
|
+
indexes: options.indexes,
|
|
296
|
+
execute: options.execute,
|
|
297
|
+
signal,
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
if (signal?.aborted) return;
|
|
301
|
+
await writePatch({
|
|
302
|
+
sessions: serializeSessions(sessions),
|
|
303
|
+
sessionsLoading: false,
|
|
304
|
+
});
|
|
305
|
+
})(),
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
if (!updateDisabled) {
|
|
309
|
+
tasks.push(
|
|
310
|
+
(async () => {
|
|
311
|
+
try {
|
|
312
|
+
const latest = await fetchLatest({
|
|
313
|
+
fetchImpl: options.updateFetchImpl || options.fetchImpl,
|
|
314
|
+
timeoutMs: options.updateTimeoutMs,
|
|
315
|
+
signal,
|
|
316
|
+
});
|
|
317
|
+
if (signal?.aborted || !latest) return;
|
|
318
|
+
if (compareVersions(latest, state.version) > 0) {
|
|
319
|
+
await writePatch({ updateAvailable: true, latestVersion: latest });
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (isAbortError(error) || signal?.aborted) return;
|
|
323
|
+
}
|
|
324
|
+
})(),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const results = await Promise.allSettled(tasks);
|
|
329
|
+
for (const result of results) {
|
|
330
|
+
if (result.status !== 'rejected') continue;
|
|
331
|
+
if (isAbortError(result.reason) || signal?.aborted) continue;
|
|
332
|
+
throw result.reason;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}) {
|
|
337
|
+
const state = await createLocalTuiState(profileName, options);
|
|
338
|
+
let next = { ...state };
|
|
339
|
+
await loadRemoteTuiUpdates(state, {
|
|
340
|
+
...options,
|
|
341
|
+
writePatch: async (partial) => {
|
|
342
|
+
next = { ...next, ...partial };
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
return next;
|
|
346
|
+
}
|
|
347
|
+
|
|
178
348
|
function spawnAndWait(command, args, options = {}) {
|
|
349
|
+
const spawnImpl = options.spawn || spawn;
|
|
179
350
|
return new Promise((resolve, reject) => {
|
|
180
|
-
const child =
|
|
351
|
+
const child = spawnImpl(command, args, {
|
|
181
352
|
cwd: options.cwd,
|
|
182
353
|
env: process.env,
|
|
183
|
-
stdio: 'inherit',
|
|
354
|
+
stdio: options.stdio ?? 'inherit',
|
|
184
355
|
});
|
|
185
356
|
child.once('error', reject);
|
|
186
357
|
child.once('exit', (code, signal) => {
|
|
@@ -197,10 +368,28 @@ export async function runTui(options = {}) {
|
|
|
197
368
|
const executable = await resolveTuiExecutable(options);
|
|
198
369
|
if (!executable) return null;
|
|
199
370
|
|
|
200
|
-
const
|
|
371
|
+
const providedState = options.state;
|
|
372
|
+
const state = providedState || (await createLocalTuiState(options.profileName, options));
|
|
201
373
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'subc-tui-'));
|
|
202
374
|
const statePath = path.join(tempDir, 'state.json');
|
|
203
375
|
const resultPath = path.join(tempDir, 'result.json');
|
|
376
|
+
const updatesPath = path.join(tempDir, 'updates.json');
|
|
377
|
+
const controller = new AbortController();
|
|
378
|
+
|
|
379
|
+
let patch = {};
|
|
380
|
+
const writePatch = async (partial) => {
|
|
381
|
+
if (controller.signal.aborted) return;
|
|
382
|
+
patch = { ...patch, ...partial };
|
|
383
|
+
try {
|
|
384
|
+
await writeAtomicJson(updatesPath, patch);
|
|
385
|
+
} catch {
|
|
386
|
+
// The TUI may have already exited and removed the working directory.
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const remote = providedState
|
|
391
|
+
? Promise.resolve()
|
|
392
|
+
: loadRemoteTuiUpdates(state, { ...options, signal: controller.signal, writePatch });
|
|
204
393
|
|
|
205
394
|
try {
|
|
206
395
|
// This state intentionally contains only display data. API keys are used
|
|
@@ -208,9 +397,11 @@ export async function runTui(options = {}) {
|
|
|
208
397
|
await fs.writeFile(statePath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
|
|
209
398
|
const code = await spawnAndWait(
|
|
210
399
|
executable.command,
|
|
211
|
-
[...executable.args, '--state', statePath, '--result', resultPath],
|
|
212
|
-
{ cwd: executable.cwd },
|
|
400
|
+
[...executable.args, '--state', statePath, '--result', resultPath, '--updates', updatesPath],
|
|
401
|
+
{ cwd: executable.cwd, spawn: options.spawn, stdio: options.stdio },
|
|
213
402
|
);
|
|
403
|
+
controller.abort();
|
|
404
|
+
await remote.catch(() => {});
|
|
214
405
|
if (code !== 0) throw new Error(`Subconscious TUI exited with status ${code}`);
|
|
215
406
|
|
|
216
407
|
try {
|
|
@@ -220,7 +411,12 @@ export async function runTui(options = {}) {
|
|
|
220
411
|
if (error.code === 'ENOENT') return null;
|
|
221
412
|
throw error;
|
|
222
413
|
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
controller.abort();
|
|
416
|
+
await remote.catch(() => {});
|
|
417
|
+
throw error;
|
|
223
418
|
} finally {
|
|
419
|
+
controller.abort();
|
|
224
420
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
225
421
|
}
|
|
226
422
|
}
|
package/bin/update-check.js
CHANGED
|
@@ -116,27 +116,48 @@ export async function currentVersion() {
|
|
|
116
116
|
export async function fetchLatestVersion(options = {}) {
|
|
117
117
|
const fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
118
118
|
const timeoutMs = options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS;
|
|
119
|
+
const signal = options.signal;
|
|
119
120
|
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable');
|
|
120
121
|
|
|
121
122
|
const controller = new AbortController();
|
|
122
123
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
123
124
|
timeout.unref?.();
|
|
125
|
+
const onAbort = () => controller.abort();
|
|
126
|
+
if (signal?.aborted) {
|
|
127
|
+
const error = new Error('Update check was cancelled');
|
|
128
|
+
error.name = 'AbortError';
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
124
133
|
|
|
125
134
|
try {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
signal: controller.signal,
|
|
135
|
+
const aborted = new Promise((_, reject) => {
|
|
136
|
+
const fail = () => {
|
|
137
|
+
const error = new Error(signal?.aborted ? 'Update check was cancelled' : 'Update check timed out');
|
|
138
|
+
error.name = 'AbortError';
|
|
139
|
+
reject(error);
|
|
140
|
+
};
|
|
141
|
+
controller.signal.addEventListener('abort', fail, { once: true });
|
|
134
142
|
});
|
|
143
|
+
const response = await Promise.race([
|
|
144
|
+
fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
145
|
+
headers: {
|
|
146
|
+
Accept: 'application/json',
|
|
147
|
+
'Cache-Control': 'no-cache, no-store',
|
|
148
|
+
Pragma: 'no-cache',
|
|
149
|
+
},
|
|
150
|
+
cache: 'no-store',
|
|
151
|
+
signal: controller.signal,
|
|
152
|
+
}),
|
|
153
|
+
aborted,
|
|
154
|
+
]);
|
|
135
155
|
if (!response.ok) throw new Error(`npm returned HTTP ${response.status}`);
|
|
136
156
|
const payload = await response.json();
|
|
137
157
|
if (!parseVersion(payload?.version)) throw new Error('npm returned an invalid version');
|
|
138
158
|
return payload.version;
|
|
139
159
|
} finally {
|
|
160
|
+
signal?.removeEventListener('abort', onAbort);
|
|
140
161
|
clearTimeout(timeout);
|
|
141
162
|
}
|
|
142
163
|
}
|
package/bin/usage.js
CHANGED
|
@@ -56,6 +56,33 @@ export function renderProgressBar(percentage, width = 20) {
|
|
|
56
56
|
return `${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Whole percent. Never rounds up to 100 while allowance remains — a meter
|
|
61
|
+
* that reads 100% claims the day is spent.
|
|
62
|
+
*/
|
|
63
|
+
export function formatAllowancePercent(percent) {
|
|
64
|
+
const n = Number(percent);
|
|
65
|
+
if (!Number.isFinite(n) || n <= 0) return '0%';
|
|
66
|
+
const whole = Math.round(n);
|
|
67
|
+
if (n >= 100) return '100%';
|
|
68
|
+
if (whole >= 100) return '99%';
|
|
69
|
+
return `${whole}%`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** New percent payloads, or the pre-basis token shape (consumed / amount). */
|
|
73
|
+
export function describeDailyAllowance(allowance) {
|
|
74
|
+
if (!allowance || typeof allowance !== 'object') return null;
|
|
75
|
+
if (allowance.basis === 'unavailable') return { kind: 'unavailable' };
|
|
76
|
+
if (allowance.basis === 'credit' || allowance.basis === 'tokens') {
|
|
77
|
+
return { kind: allowance.basis, percent: Number(allowance.percent) || 0 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const consumed = Number(allowance.consumed) || 0;
|
|
81
|
+
const amount = Number(allowance.amount) || 0;
|
|
82
|
+
if (amount <= 0) return null;
|
|
83
|
+
return { kind: 'tokens', percent: (consumed / amount) * 100 };
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
export function formatUsageDisplay(data) {
|
|
60
87
|
const lines = [];
|
|
61
88
|
lines.push(`\n ${c.bold}Subconscious Usage${c.reset}\n`);
|
|
@@ -81,24 +108,29 @@ export function formatUsageDisplay(data) {
|
|
|
81
108
|
lines.push('');
|
|
82
109
|
lines.push(` ${c.dim}Daily allowance${c.reset}`);
|
|
83
110
|
lines.push(` ${c.green}Unlimited${c.reset} ${c.dim}(comp)${c.reset}`);
|
|
84
|
-
} else
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
} else {
|
|
112
|
+
const described = describeDailyAllowance(allowance);
|
|
113
|
+
if (described) {
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push(` ${c.dim}Daily allowance${c.reset}`);
|
|
116
|
+
if (described.kind === 'unavailable') {
|
|
117
|
+
lines.push(` ${c.dim}Daily credit unavailable${c.reset}`);
|
|
118
|
+
lines.push(
|
|
119
|
+
` ${c.dim}Today's credit usage could not be read just now. Your plan and access are unaffected, and the figure returns on its own.${c.reset}`,
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
const label =
|
|
123
|
+
described.kind === 'credit' ? 'of daily credit used' : 'of daily tokens used';
|
|
124
|
+
lines.push(
|
|
125
|
+
` ${renderProgressBar(described.percent)} ${formatAllowancePercent(described.percent)} ${label}`,
|
|
126
|
+
);
|
|
127
|
+
lines.push(` ${c.dim}Resets midnight UTC${c.reset}`);
|
|
128
|
+
if (described.kind === 'credit' && described.percent >= 100) {
|
|
129
|
+
lines.push(
|
|
130
|
+
` ${c.yellow}Past today's allowance - further usage bills from your balance.${c.reset}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
102
134
|
}
|
|
103
135
|
}
|
|
104
136
|
|
|
@@ -221,7 +253,7 @@ Usage:
|
|
|
221
253
|
subc usage --json
|
|
222
254
|
subc usage help
|
|
223
255
|
|
|
224
|
-
Show billing mode, daily
|
|
256
|
+
Show billing mode, daily allowance, credit balance, and per-model usage
|
|
225
257
|
for the authenticated organization.
|
|
226
258
|
|
|
227
259
|
--json Print the raw platform JSON response
|
package/bin/windows/launch.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { claudeNativeLaunch } from './claude.js';
|
|
6
|
+
import { buildOpenCodeConfig } from '../opencode-provider.js';
|
|
6
7
|
import { modelSupportsVision } from '../model-capabilities.js';
|
|
7
8
|
import { defaults, value, origin, modelIds, positiveInteger, parseOptions, writeJson } from './common.js';
|
|
8
9
|
|
|
@@ -26,16 +27,9 @@ export async function windowsLaunch(id, argv, env, { tempRoot = os.tmpdir() } =
|
|
|
26
27
|
if (id === 'opencode') {
|
|
27
28
|
const context = positiveInteger(value(env, 'OPENCODE_CONTEXT_LIMIT', '5000000'), 'OPENCODE_CONTEXT_LIMIT');
|
|
28
29
|
const output = positiveInteger(value(env, 'OPENCODE_OUTPUT_LIMIT', '65536'), 'OPENCODE_OUTPUT_LIMIT');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}]));
|
|
33
|
-
childEnv.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
34
|
-
$schema: 'https://opencode.ai/config.json', disabled_providers: ['subconscious'],
|
|
35
|
-
provider: { 'subconscious-cli': { npm: '@ai-sdk/openai-compatible', name: 'Subconscious Gateway', options: {
|
|
36
|
-
baseURL: `${base}/v1`, apiKey: '{env:SUBCONSCIOUS_API_KEY}', headers: { 'x-subconscious-client': 'opencode' },
|
|
37
|
-
}, models } }, model: `subconscious-cli/${model}`,
|
|
38
|
-
});
|
|
30
|
+
childEnv.OPENCODE_CONFIG_CONTENT = JSON.stringify(
|
|
31
|
+
buildOpenCodeConfig({ baseUrl: base, model, modelIds: modelIds(env), context, output }),
|
|
32
|
+
);
|
|
39
33
|
return { command: 'opencode', args: parseOptions(argv, {}).rest, env: childEnv };
|
|
40
34
|
}
|
|
41
35
|
if (id === 'codex') {
|
package/package.json
CHANGED