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/agents.js
CHANGED
|
@@ -32,6 +32,15 @@ import { compareVersions } from './update-check.js';
|
|
|
32
32
|
|
|
33
33
|
export const MIN_CLAUDE_CODE_VERSION = '2.1.242';
|
|
34
34
|
|
|
35
|
+
// First Codex that honours `multi_agent_version: "v2"` in the model catalog,
|
|
36
|
+
// found by installing each release and reading what it advertises: 0.142.5 and
|
|
37
|
+
// below offer no subagent namespace at all, 0.143.0 offers `collaboration`.
|
|
38
|
+
//
|
|
39
|
+
// Below this the catalog field is not rejected, it is ignored - the run simply
|
|
40
|
+
// has no subagents and nothing says why. That silence is the reason for a
|
|
41
|
+
// version gate rather than a note in the docs.
|
|
42
|
+
export const MIN_CODEX_VERSION = '0.143.0';
|
|
43
|
+
|
|
35
44
|
// --- Registry (single source of truth, generated copy shipped in the package).
|
|
36
45
|
const registry = JSON.parse(
|
|
37
46
|
readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
|
|
@@ -193,10 +202,10 @@ const AGENT_HELP = {
|
|
|
193
202
|
codex: {
|
|
194
203
|
usage: 'subc [-p NAME] codex [help|install|status|uninstall] [Codex arguments...]',
|
|
195
204
|
behavior:
|
|
196
|
-
'Launches Codex with a temporary Subconscious provider catalog. Compaction hooks are merged into ~/.codex/hooks.json and removed with subc codex uninstall.',
|
|
205
|
+
'Launches Codex with a temporary Subconscious provider catalog. Compaction hooks are merged into ~/.codex/hooks.json on first launch and removed with subc codex uninstall; Codex will not run them until you trust them with /hooks once.',
|
|
197
206
|
options: [
|
|
198
207
|
['help', 'Show this help'],
|
|
199
|
-
['install', 'Install
|
|
208
|
+
['install', 'Install the Subconscious compaction hooks (then trust with /hooks)'],
|
|
200
209
|
['status', 'Inspect the installed compaction hooks'],
|
|
201
210
|
['uninstall', 'Remove only the Subconscious Codex hooks'],
|
|
202
211
|
['--model MODEL', 'Override the profile model for this launch'],
|
|
@@ -205,7 +214,9 @@ const AGENT_HELP = {
|
|
|
205
214
|
['--auto-compact-token-limit N', 'Override the automatic compaction threshold'],
|
|
206
215
|
['--reasoning-effort LEVEL', 'Use none, low, medium, high, or max'],
|
|
207
216
|
['--external-tools', 'Enable Codex apps/plugins for this launch'],
|
|
208
|
-
['--
|
|
217
|
+
['--stream-idle-timeout MS', 'Allow a longer silent think before timing out'],
|
|
218
|
+
['--max-subagents N', 'Subagents allowed to run at once (default 4)'],
|
|
219
|
+
['--subagent-effort LEVEL', 'Effort for subagents, separate from the parent'],
|
|
209
220
|
['-- ARGS...', 'Pass remaining arguments to Codex'],
|
|
210
221
|
],
|
|
211
222
|
},
|
|
@@ -625,6 +636,10 @@ export function parseClaudeVersion(text) {
|
|
|
625
636
|
}
|
|
626
637
|
|
|
627
638
|
export function claudeVersionNeedsUpgrade(installed, minimum = MIN_CLAUDE_CODE_VERSION) {
|
|
639
|
+
return versionNeedsUpgrade(installed, minimum);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function versionNeedsUpgrade(installed, minimum) {
|
|
628
643
|
return Boolean(installed && minimum && compareVersions(installed, minimum) < 0);
|
|
629
644
|
}
|
|
630
645
|
|
|
@@ -649,13 +664,20 @@ export function readClaudeVersion(bin, binDir, options = {}) {
|
|
|
649
664
|
}
|
|
650
665
|
}
|
|
651
666
|
|
|
667
|
+
const MINIMUM_VERSIONS = {
|
|
668
|
+
'claude-code': { minimum: MIN_CLAUDE_CODE_VERSION, label: 'Claude Code' },
|
|
669
|
+
// Below this, subagents are silently absent rather than broken.
|
|
670
|
+
codex: { minimum: MIN_CODEX_VERSION, label: 'Codex' },
|
|
671
|
+
};
|
|
672
|
+
|
|
652
673
|
async function ensureClaudeCompatible(agent, binDir) {
|
|
653
|
-
|
|
674
|
+
const requirement = MINIMUM_VERSIONS[agent.id];
|
|
675
|
+
if (!requirement) return;
|
|
654
676
|
const version = readClaudeVersion(agent.bin, binDir);
|
|
655
|
-
if (!
|
|
677
|
+
if (!versionNeedsUpgrade(version, requirement.minimum)) return;
|
|
656
678
|
|
|
657
679
|
console.error(
|
|
658
|
-
`\n Minimum supported
|
|
680
|
+
`\n Minimum supported ${requirement.label} version is ${requirement.minimum}. Your version is ${version}. Upgrade to get the best experience.\n`,
|
|
659
681
|
);
|
|
660
682
|
console.error(` Upgrade it with:`);
|
|
661
683
|
console.error(` ${c.cyan}${agent.install}${c.reset}`);
|
|
@@ -884,6 +906,7 @@ export function runbookEnv(
|
|
|
884
906
|
? {
|
|
885
907
|
SUBC_CLAUDE_SETTINGS: JSON.stringify(claudePickerSettings(models, model)),
|
|
886
908
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '0',
|
|
909
|
+
CLAUDE_CODE_AUTO_MODE_SERVER: '0',
|
|
887
910
|
}
|
|
888
911
|
: {}),
|
|
889
912
|
SUBC_ENV_FILE: os.devNull,
|
package/bin/cli.js
CHANGED
|
@@ -280,10 +280,20 @@ async function main() {
|
|
|
280
280
|
return;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
const
|
|
284
|
-
|
|
283
|
+
const launchTui =
|
|
284
|
+
!command &&
|
|
285
|
+
process.stdin.isTTY === true &&
|
|
286
|
+
process.stdout.isTTY === true &&
|
|
287
|
+
process.env.TERM !== 'dumb';
|
|
288
|
+
|
|
289
|
+
// The TUI opens immediately and surfaces catalog, session, and upgrade
|
|
290
|
+
// status itself. Keep the blocking npm prompt for explicit commands.
|
|
291
|
+
if (!launchTui) {
|
|
292
|
+
const update = await showUpdateNotice();
|
|
293
|
+
if (update?.action === 'updated' || update?.action === 'cancel') return;
|
|
294
|
+
}
|
|
285
295
|
|
|
286
|
-
if (
|
|
296
|
+
if (launchTui) {
|
|
287
297
|
const selection = await runTui({ profileName });
|
|
288
298
|
if (!selection?.args?.length) return;
|
|
289
299
|
if (selection.baseUrl?.trim()) {
|
package/bin/models.js
CHANGED
|
@@ -18,6 +18,27 @@ export function isLiveModelSource(source) {
|
|
|
18
18
|
return source === 'available' || source === 'public';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function isAbortError(error) {
|
|
22
|
+
return error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function linkAbortSignal(controller, signal) {
|
|
26
|
+
if (!signal) return () => {};
|
|
27
|
+
if (signal.aborted) {
|
|
28
|
+
controller.abort();
|
|
29
|
+
return () => {};
|
|
30
|
+
}
|
|
31
|
+
const onAbort = () => controller.abort();
|
|
32
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
33
|
+
return () => signal.removeEventListener('abort', onAbort);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function abortError(message) {
|
|
37
|
+
const error = new Error(message);
|
|
38
|
+
error.name = 'AbortError';
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
export function normalizeModelIds(modelIds = [], selectedModel) {
|
|
22
43
|
const models = [];
|
|
23
44
|
const seen = new Set();
|
|
@@ -53,6 +74,7 @@ export async function fetchGatewayModels({
|
|
|
53
74
|
path = PUBLIC_MODELS_PATH,
|
|
54
75
|
fetchImpl = globalThis.fetch,
|
|
55
76
|
timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
|
|
77
|
+
signal,
|
|
56
78
|
}) {
|
|
57
79
|
if (typeof fetchImpl !== 'function') {
|
|
58
80
|
throw new Error('This Node.js version does not support fetch');
|
|
@@ -71,14 +93,26 @@ export async function fetchGatewayModels({
|
|
|
71
93
|
const controller = new AbortController();
|
|
72
94
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
73
95
|
timeout.unref?.();
|
|
96
|
+
const unlink = linkAbortSignal(controller, signal);
|
|
74
97
|
|
|
75
98
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
if (signal?.aborted) throw abortError('Model discovery was cancelled');
|
|
100
|
+
const aborted = new Promise((_, reject) => {
|
|
101
|
+
const fail = () => {
|
|
102
|
+
if (signal?.aborted) reject(abortError('Model discovery was cancelled'));
|
|
103
|
+
else reject(new Error(`Model discovery timed out after ${timeoutMs}ms`));
|
|
104
|
+
};
|
|
105
|
+
controller.signal.addEventListener('abort', fail, { once: true });
|
|
81
106
|
});
|
|
107
|
+
const response = await Promise.race([
|
|
108
|
+
fetchImpl(endpoint, {
|
|
109
|
+
method: 'GET',
|
|
110
|
+
headers,
|
|
111
|
+
cache: 'no-store',
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
}),
|
|
114
|
+
aborted,
|
|
115
|
+
]);
|
|
82
116
|
|
|
83
117
|
if (!response.ok) {
|
|
84
118
|
throw new Error(`Model discovery returned HTTP ${response.status}`);
|
|
@@ -91,21 +125,24 @@ export async function fetchGatewayModels({
|
|
|
91
125
|
|
|
92
126
|
return normalizeModelIds(payload.data.map((model) => model?.id));
|
|
93
127
|
} catch (error) {
|
|
128
|
+
if (signal?.aborted) throw abortError('Model discovery was cancelled');
|
|
94
129
|
if (controller.signal.aborted) {
|
|
95
130
|
throw new Error(`Model discovery timed out after ${timeoutMs}ms`);
|
|
96
131
|
}
|
|
97
132
|
throw error;
|
|
98
133
|
} finally {
|
|
134
|
+
unlink();
|
|
99
135
|
clearTimeout(timeout);
|
|
100
136
|
}
|
|
101
137
|
}
|
|
102
138
|
|
|
103
|
-
async function fetchPublicModels({ baseUrl, fetchImpl, timeoutMs }) {
|
|
139
|
+
async function fetchPublicModels({ baseUrl, fetchImpl, timeoutMs, signal }) {
|
|
104
140
|
const models = await fetchGatewayModels({
|
|
105
141
|
baseUrl,
|
|
106
142
|
path: PUBLIC_MODELS_PATH,
|
|
107
143
|
fetchImpl,
|
|
108
144
|
timeoutMs,
|
|
145
|
+
signal,
|
|
109
146
|
});
|
|
110
147
|
if (!models.length) throw new Error('Model discovery returned no usable model IDs');
|
|
111
148
|
return models;
|
|
@@ -134,6 +171,7 @@ export async function resolveModelCatalog({
|
|
|
134
171
|
fallbackModels = [],
|
|
135
172
|
fetchImpl = globalThis.fetch,
|
|
136
173
|
timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
|
|
174
|
+
signal,
|
|
137
175
|
}) {
|
|
138
176
|
const key = apiKey?.trim() || '';
|
|
139
177
|
|
|
@@ -145,22 +183,26 @@ export async function resolveModelCatalog({
|
|
|
145
183
|
path: AVAILABLE_MODELS_PATH,
|
|
146
184
|
fetchImpl,
|
|
147
185
|
timeoutMs,
|
|
186
|
+
signal,
|
|
148
187
|
});
|
|
149
188
|
return liveCatalog(discovered, selectedModel, 'available');
|
|
150
189
|
} catch (availableError) {
|
|
190
|
+
if (isAbortError(availableError) || signal?.aborted) throw availableError;
|
|
151
191
|
try {
|
|
152
|
-
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs });
|
|
192
|
+
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs, signal });
|
|
153
193
|
return liveCatalog(discovered, selectedModel, 'public');
|
|
154
|
-
} catch {
|
|
194
|
+
} catch (publicError) {
|
|
195
|
+
if (isAbortError(publicError) || signal?.aborted) throw publicError;
|
|
155
196
|
return packagedCatalog(selectedModel, fallbackModels, availableError);
|
|
156
197
|
}
|
|
157
198
|
}
|
|
158
199
|
}
|
|
159
200
|
|
|
160
201
|
try {
|
|
161
|
-
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs });
|
|
202
|
+
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs, signal });
|
|
162
203
|
return liveCatalog(discovered, selectedModel, 'public');
|
|
163
204
|
} catch (error) {
|
|
205
|
+
if (isAbortError(error) || signal?.aborted) throw error;
|
|
164
206
|
return packagedCatalog(selectedModel, fallbackModels, error);
|
|
165
207
|
}
|
|
166
208
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { modelSupportsVision } from './model-capabilities.js';
|
|
2
|
+
|
|
3
|
+
export const OPENCODE_PROVIDER_ID = 'subconscious';
|
|
4
|
+
export const OPENCODE_PROVIDER_NAME = 'Subconscious Gateway';
|
|
5
|
+
|
|
6
|
+
const ACRONYMS = new Set(['gpt', 'oss', 'api', 'gguf', 'ggml', 'nomic', 'vl', 'it', 'mlx']);
|
|
7
|
+
|
|
8
|
+
function formatToken(token) {
|
|
9
|
+
const lower = token.toLowerCase();
|
|
10
|
+
if (ACRONYMS.has(lower)) return token.toUpperCase();
|
|
11
|
+
if (/^\d+[bkmg]$/i.test(token)) return token.toUpperCase();
|
|
12
|
+
if (/^q\d+$/i.test(token)) return token.toUpperCase();
|
|
13
|
+
if (/^\d+\.\d+/.test(token)) return token;
|
|
14
|
+
if (/^[a-z]\d+[a-z]$/i.test(token) || /^\d+[a-z]$/i.test(token)) return token.toUpperCase();
|
|
15
|
+
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Drop the subconscious/ prefix, then capitalize each dash-separated token. */
|
|
19
|
+
export function opencodeModelDisplayName(modelId) {
|
|
20
|
+
const text = String(modelId ?? '').trim();
|
|
21
|
+
const slug = text.startsWith('subconscious/') ? text.slice('subconscious/'.length) : text;
|
|
22
|
+
return slug.split(/[-_]/).filter(Boolean).map(formatToken).join(' ');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildOpenCodeModels(modelIds, { context, output }) {
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
modelIds.map((id) => [
|
|
28
|
+
id,
|
|
29
|
+
{
|
|
30
|
+
name: opencodeModelDisplayName(id),
|
|
31
|
+
tools: true,
|
|
32
|
+
limit: { context, output },
|
|
33
|
+
...(modelSupportsVision(id)
|
|
34
|
+
? { attachment: true, modalities: { input: ['text', 'image'], output: ['text'] } }
|
|
35
|
+
: {}),
|
|
36
|
+
},
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildOpenCodeConfig({ baseUrl, model, modelIds, context, output }) {
|
|
42
|
+
return {
|
|
43
|
+
$schema: 'https://opencode.ai/config.json',
|
|
44
|
+
disabled_providers: ['subconscious-cli'],
|
|
45
|
+
provider: {
|
|
46
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
47
|
+
npm: '@ai-sdk/openai-compatible',
|
|
48
|
+
name: OPENCODE_PROVIDER_NAME,
|
|
49
|
+
whitelist: [...modelIds],
|
|
50
|
+
options: {
|
|
51
|
+
baseURL: `${baseUrl}/v1`,
|
|
52
|
+
apiKey: '{env:SUBCONSCIOUS_API_KEY}',
|
|
53
|
+
headers: { 'x-subconscious-client': 'opencode' },
|
|
54
|
+
modelsDiscovery: { enabled: false },
|
|
55
|
+
},
|
|
56
|
+
models: buildOpenCodeModels(modelIds, { context, output }),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
model: `${OPENCODE_PROVIDER_ID}/${model}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/bin/profiles.js
CHANGED
|
@@ -144,6 +144,28 @@ const SETTINGS = {
|
|
|
144
144
|
type: 'integer',
|
|
145
145
|
min: 1,
|
|
146
146
|
},
|
|
147
|
+
CODEX_SUBAGENT_REASONING_EFFORT: {
|
|
148
|
+
key: 'CODEX_SUBAGENT_REASONING_EFFORT',
|
|
149
|
+
label: 'Subagent reasoning effort',
|
|
150
|
+
description:
|
|
151
|
+
'Effort for Codex subagents, set separately from the parent: a marathon model at max thinks for minutes without emitting a token, and a subagent turn is one uninterrupted think, so inheriting max makes every delegated subtask look like a hang.',
|
|
152
|
+
type: 'text',
|
|
153
|
+
},
|
|
154
|
+
CODEX_MULTI_AGENT_VERSION: {
|
|
155
|
+
key: 'CODEX_MULTI_AGENT_VERSION',
|
|
156
|
+
label: 'Codex multi-agent version',
|
|
157
|
+
description:
|
|
158
|
+
'Which subagent implementation Codex offers: v2 gives the collaboration tools. Leave empty to turn subagents off entirely.',
|
|
159
|
+
type: 'text',
|
|
160
|
+
},
|
|
161
|
+
CODEX_STREAM_IDLE_TIMEOUT_MS: {
|
|
162
|
+
key: 'CODEX_STREAM_IDLE_TIMEOUT_MS',
|
|
163
|
+
label: 'Stream idle timeout (ms)',
|
|
164
|
+
description:
|
|
165
|
+
'How long Codex waits on a silent stream before giving up. Needs to exceed the longest think the model will take.',
|
|
166
|
+
type: 'integer',
|
|
167
|
+
min: 1,
|
|
168
|
+
},
|
|
147
169
|
MAX_SUBAGENT_SPAWN_DEPTH: {
|
|
148
170
|
key: 'MAX_SUBAGENT_SPAWN_DEPTH',
|
|
149
171
|
label: 'Subagent spawn depth',
|
|
@@ -305,6 +327,9 @@ const AGENT_SETTING_KEYS = {
|
|
|
305
327
|
'CODEX_AUTO_COMPACT_TOKEN_LIMIT',
|
|
306
328
|
'CODEX_REASONING_EFFORT',
|
|
307
329
|
'CODEX_EXTERNAL_TOOLS',
|
|
330
|
+
'CODEX_STREAM_IDLE_TIMEOUT_MS',
|
|
331
|
+
'CODEX_MULTI_AGENT_VERSION',
|
|
332
|
+
'CODEX_SUBAGENT_REASONING_EFFORT',
|
|
308
333
|
'MAX_CONCURRENT_SUBAGENTS',
|
|
309
334
|
],
|
|
310
335
|
opencode: ['OPENCODE_API_KEY', 'OPENCODE_CONTEXT_LIMIT', 'OPENCODE_OUTPUT_LIMIT'],
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"uninstall"
|
|
168
168
|
]
|
|
169
169
|
},
|
|
170
|
-
"launch": "codex -c model={model} -c model_provider=subconscious -c web_search=disabled -c model_providers.subconscious.name=Subconscious -c model_providers.subconscious.base_url={baseUrlV1} -c model_providers.subconscious.wire_api=responses -c model_providers.subconscious.env_key=SUBCONSCIOUS_API_KEY -c model_providers.subconscious.stream_idle_timeout_ms=
|
|
170
|
+
"launch": "codex -c model={model} -c model_provider=subconscious -c web_search=disabled -c model_providers.subconscious.name=Subconscious -c model_providers.subconscious.base_url={baseUrlV1} -c model_providers.subconscious.wire_api=responses -c model_providers.subconscious.env_key=SUBCONSCIOUS_API_KEY -c model_providers.subconscious.stream_idle_timeout_ms=900000",
|
|
171
171
|
"env": {
|
|
172
172
|
"SUBCONSCIOUS_API_KEY": "{apiKey}",
|
|
173
173
|
"SUBCONSCIOUS_GATEWAY_URL": "{baseUrl}"
|
|
@@ -216,6 +216,11 @@ export ANTHROPIC_DEFAULT_FABLE_MODEL="${ANTHROPIC_DEFAULT_FABLE_MODEL:-$FABLE_MO
|
|
|
216
216
|
export ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="${ANTHROPIC_DEFAULT_FABLE_MODEL_NAME:-$FABLE_MODEL_NAME}"
|
|
217
217
|
export ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION:-$FABLE_MODEL_DESCRIPTION}"
|
|
218
218
|
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY="${CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:-0}"
|
|
219
|
+
# Gateway upstreams can't perform Anthropic's server-side auto mode classifier
|
|
220
|
+
# checks; opting out avoids the "isn't eligible for no-charge classifier
|
|
221
|
+
# requests" fallback and keeps classifier requests billed locally.
|
|
222
|
+
# https://code.claude.com/docs/en/auto-mode-classifier-billing
|
|
223
|
+
export CLAUDE_CODE_AUTO_MODE_SERVER="${CLAUDE_CODE_AUTO_MODE_SERVER:-0}"
|
|
219
224
|
export CLAUDE_CODE_SUBAGENT_MODEL="${CLAUDE_CODE_SUBAGENT_MODEL:-$MODEL}"
|
|
220
225
|
export CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS="${MAX_CONCURRENT_SUBAGENTS}"
|
|
221
226
|
export CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH="${MAX_SUBAGENT_SPAWN_DEPTH}"
|
|
@@ -61,6 +61,30 @@ EOF
|
|
|
61
61
|
chmod 600 "$HOOKS_ENV_FILE"
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
# True when our hooks, hook script and env file are all already in place and
|
|
65
|
+
# current, so there is nothing to write.
|
|
66
|
+
#
|
|
67
|
+
# This matters more than it looks. Codex asks the user to trust a hook before
|
|
68
|
+
# it will run one, and it decides what is "new" from what is on disk. Rewriting
|
|
69
|
+
# an identical hook on every single launch meant the trust prompt never went
|
|
70
|
+
# away and the compaction hooks never actually ran - we were re-introducing
|
|
71
|
+
# ourselves as a stranger several times a day.
|
|
72
|
+
codex_hooks_current() {
|
|
73
|
+
[[ -f "$HOOKS_JSON" ]] || return 1
|
|
74
|
+
codex_hooks_json_valid || return 1
|
|
75
|
+
[[ -x "$HOOK_DST" ]] || return 1
|
|
76
|
+
[[ -f "${HOOK_SRC:-}" ]] || return 1
|
|
77
|
+
cmp -s "$HOOK_SRC" "$HOOK_DST" || return 1
|
|
78
|
+
[[ -f "$HOOKS_ENV_FILE" ]] || return 1
|
|
79
|
+
grep -q "SUBCONSCIOUS_GATEWAY_URL='${GATEWAY_URL%/}'" "$HOOKS_ENV_FILE" || return 1
|
|
80
|
+
grep -q "SUBCONSCIOUS_API_KEY='${API_KEY}'" "$HOOKS_ENV_FILE" || return 1
|
|
81
|
+
# Our entry present under both compaction events, and exactly once each.
|
|
82
|
+
jq -e --arg m "$MARKER" '
|
|
83
|
+
def ours: [ .[]?.hooks[]? | select((.command // "") | tostring | contains($m)) ] | length;
|
|
84
|
+
(.hooks.PreCompact | ours) == 1 and (.hooks.PostCompact | ours) == 1
|
|
85
|
+
' "$HOOKS_JSON" >/dev/null 2>&1
|
|
86
|
+
}
|
|
87
|
+
|
|
64
88
|
# codex_ensure_hooks [strict|best-effort]
|
|
65
89
|
codex_ensure_hooks() {
|
|
66
90
|
local mode="${1:-strict}"
|
|
@@ -91,12 +115,18 @@ codex_ensure_hooks() {
|
|
|
91
115
|
if [[ ! -f "$HOOKS_JSON" ]]; then
|
|
92
116
|
printf '%s\n' '{"hooks":{}}' >"$HOOKS_JSON"
|
|
93
117
|
fi
|
|
118
|
+
if codex_hooks_current; then
|
|
119
|
+
# Already installed and unchanged. Saying nothing is the point: a message
|
|
120
|
+
# on every launch trains the user to ignore it, and rewriting the file
|
|
121
|
+
# would reset the trust they have already granted.
|
|
122
|
+
return 0
|
|
123
|
+
fi
|
|
94
124
|
codex_write_hook_script
|
|
95
125
|
codex_write_hooks_env
|
|
96
126
|
codex_strip_hook_entries
|
|
97
127
|
codex_append_hook_groups
|
|
98
|
-
echo "
|
|
99
|
-
echo "
|
|
128
|
+
echo "Installed Subconscious compaction hooks in $HOOKS_JSON"
|
|
129
|
+
echo "These do not run until you trust them: type /hooks inside Codex once."
|
|
100
130
|
}
|
|
101
131
|
|
|
102
132
|
codex_uninstall_hooks() {
|
|
@@ -33,6 +33,16 @@ Usage:
|
|
|
33
33
|
|
|
34
34
|
Merges Subconscious compaction hooks into ~/.codex/hooks.json. Does not write
|
|
35
35
|
config.toml. Launch Codex with subc codex.
|
|
36
|
+
|
|
37
|
+
The hooks report compaction start and end to the gateway, which is what lets a
|
|
38
|
+
conversation's turns stay correlated across a compaction boundary.
|
|
39
|
+
|
|
40
|
+
Codex will not run a hook until you trust it. After installing, type /hooks
|
|
41
|
+
inside Codex once and approve them; until then they are present but inert.
|
|
42
|
+
Non-interactive runs have nobody to ask, so `codex exec` can only run them with
|
|
43
|
+
--dangerously-bypass-hook-trust, which is yours to opt into and is never passed
|
|
44
|
+
for you. Fleet installs should ship the hooks through Codex's managed
|
|
45
|
+
(MDM) configuration, which is trusted without prompting.
|
|
36
46
|
EOF
|
|
37
47
|
}
|
|
38
48
|
|