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/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) return { command: override, args: [], cwd: undefined };
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
- if (await pathExists(packaged)) {
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 (await pathExists(path.join(TUI_SOURCE_DIR, 'go.mod'))) {
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
- export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}) {
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: catalog.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: catalog.error?.message || '',
152
- modelSource: catalog.source,
153
- sessions: sessions.map((session) => ({
154
- key: session.key,
155
- harness: session.harness,
156
- harnessName: session.harnessName,
157
- title: session.title,
158
- cwd: session.cwd,
159
- updatedAt: session.updatedAt,
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 = spawn(command, args, {
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 state = options.state || (await createTuiState(options.profileName, options));
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
  }
@@ -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 response = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
127
- headers: {
128
- Accept: 'application/json',
129
- 'Cache-Control': 'no-cache, no-store',
130
- Pragma: 'no-cache',
131
- },
132
- cache: 'no-store',
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 if (allowance && allowance.amount > 0) {
85
- const consumed = Number(allowance.consumed) || 0;
86
- const amount = Number(allowance.amount) || 0;
87
- const remaining = Number(allowance.remaining) || 0;
88
- const pct = amount > 0 ? Math.min((consumed / amount) * 100, 100) : 0;
89
- const overage = Math.max(0, consumed - amount);
90
-
91
- lines.push('');
92
- lines.push(` ${c.dim}Daily allowance${c.reset}`);
93
- lines.push(
94
- ` ${renderProgressBar(pct)} ${formatTokens(consumed)} / ${formatTokens(amount)} tokens`,
95
- );
96
- const resetNote = allowance.resetAt
97
- ? `${formatTokens(remaining)} remaining · resets midnight UTC`
98
- : `${formatTokens(remaining)} remaining`;
99
- lines.push(` ${c.dim}${resetNote}${c.reset}`);
100
- if (overage > 0) {
101
- lines.push(` ${c.yellow}+${formatTokens(overage)} over daily allowance${c.reset}`);
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 token allowance, credit balance, and per-model usage
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
@@ -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
- const models = Object.fromEntries(modelIds(env).map(id => [id, {
30
- name: id, tools: true, limit: { context, output },
31
- ...(modelSupportsVision(id) ? { attachment: true, modalities: { input: ['text', 'image'], output: ['text'] } } : {}),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "4.1.3",
3
+ "version": "4.2.0",
4
4
  "description": "CLI for Subconscious — run Subconscious Code, Claude Code, Codex, OpenCode, DeepSeek Harness, Cursor, Copilot, and Pi",
5
5
  "bin": {
6
6
  "subc": "bin/cli.js"