@brunobracaioli/aibrain 0.3.0-beta.7 → 0.4.0-beta.2
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 +80 -15
- package/dist/assets/claude/.claude-plugin/plugin.json +1 -1
- package/dist/assets/claude/hooks/context-compaction.ts +1100 -77
- package/dist/assets/claude/hooks/native-host-contract-runtime.ts +18 -0
- package/dist/assets/claude/hooks/native-host-contract.test.ts +51 -0
- package/dist/assets/codex-context-compaction.mjs +2179 -194
- package/dist/assets/jev-paid-pilot-terminal.mjs +334 -35
- package/dist/claude-compatibility.mjs +506 -0
- package/dist/claude-observation.mjs +117 -3
- package/dist/cli.mjs +105 -28
- package/dist/codex-compatibility.mjs +481 -0
- package/dist/codex-diagnostics.mjs +39 -3
- package/dist/codex-protocol-fingerprint.json +131 -0
- package/dist/engineering-pack-public-key.json +10 -0
- package/dist/engineering-pack.mjs +722 -0
- package/dist/engineering.mjs +194 -0
- package/dist/launch.mjs +71 -14
- package/dist/mcp.mjs +8 -8
- package/dist/package.json +1 -1
- package/dist/platform.mjs +47 -3
- package/package.json +3 -2
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
cp,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readdir,
|
|
10
|
+
realpath,
|
|
11
|
+
rm,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from 'node:fs/promises';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
const MAX_OUTPUT_BYTES = 96 * 1024;
|
|
19
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
20
|
+
const MAX_PLUGIN_ASSET_BYTES = 4 * 1024 * 1024;
|
|
21
|
+
const MAX_PLUGIN_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const HOST_MAJOR = 2;
|
|
23
|
+
const HOST_MINOR = 1;
|
|
24
|
+
const QUALIFIED_VERSIONS = new Set([
|
|
25
|
+
'2.1.294',
|
|
26
|
+
'2.1.295',
|
|
27
|
+
'2.1.296',
|
|
28
|
+
'2.1.299',
|
|
29
|
+
'2.1.300',
|
|
30
|
+
'2.1.301',
|
|
31
|
+
'2.1.302',
|
|
32
|
+
'2.1.303',
|
|
33
|
+
]);
|
|
34
|
+
const REQUIRED_FLAGS = ['--settings', '--mcp-config', '--allowedTools', '--plugin-dir'];
|
|
35
|
+
const REQUIRED_INVENTORY = [
|
|
36
|
+
'prompt.submit',
|
|
37
|
+
'tool.call',
|
|
38
|
+
'session.end',
|
|
39
|
+
'turn.complete',
|
|
40
|
+
'session.compact',
|
|
41
|
+
'$.mcp.call',
|
|
42
|
+
'$.process.run',
|
|
43
|
+
'$.session.id',
|
|
44
|
+
'$.session.messages',
|
|
45
|
+
'$.session.usage',
|
|
46
|
+
'$.session.append',
|
|
47
|
+
'$.session.compact',
|
|
48
|
+
'$.store.get',
|
|
49
|
+
'$.store.set',
|
|
50
|
+
'$.store.delete',
|
|
51
|
+
'$.store.keys',
|
|
52
|
+
'$.ui.log',
|
|
53
|
+
'$.ui.toast',
|
|
54
|
+
];
|
|
55
|
+
const ALLOWED_PLUGIN_FILES = Object.freeze([
|
|
56
|
+
'.claude-plugin/plugin.json',
|
|
57
|
+
'hooks/hooks.json',
|
|
58
|
+
'hooks/context-compaction.ts',
|
|
59
|
+
'hooks/provider-window.js',
|
|
60
|
+
'hooks/state-fitting.mjs',
|
|
61
|
+
'hooks/state-fitting.d.mts',
|
|
62
|
+
'hooks/native-host-contract.test.ts',
|
|
63
|
+
'hooks/native-host-contract-runtime.ts',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/** @param {string} executable */
|
|
67
|
+
function safePath(executable) {
|
|
68
|
+
return [...new Set([dirname(executable), dirname(process.execPath), '/usr/bin', '/bin'])].join(
|
|
69
|
+
':',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @param {string} executable @returns {Promise<string>} */
|
|
74
|
+
async function readNativeVersion(executable) {
|
|
75
|
+
const root = await mkdtemp(join(tmpdir(), 'aibrain-claude-version-'));
|
|
76
|
+
const cwd = join(root, 'cwd');
|
|
77
|
+
const home = join(root, 'home');
|
|
78
|
+
const config = join(root, 'config');
|
|
79
|
+
await mkdir(cwd, { recursive: true, mode: 0o700 });
|
|
80
|
+
try {
|
|
81
|
+
/** @type {NodeJS.ProcessEnv} */
|
|
82
|
+
const environment = {
|
|
83
|
+
PATH: safePath(executable),
|
|
84
|
+
HOME: home,
|
|
85
|
+
CLAUDE_CONFIG_DIR: config,
|
|
86
|
+
CLAUDE_COMPAT_CWD: cwd,
|
|
87
|
+
NO_COLOR: '1',
|
|
88
|
+
CI: '1',
|
|
89
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
90
|
+
};
|
|
91
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
92
|
+
await mkdir(config, { recursive: true, mode: 0o700 });
|
|
93
|
+
const result = await runBounded(executable, ['--version'], environment, {
|
|
94
|
+
timeoutMs: 10_000,
|
|
95
|
+
maxOutputBytes: 2_048,
|
|
96
|
+
});
|
|
97
|
+
if (result.code !== 0 || result.overflow || result.timedOut) return '';
|
|
98
|
+
return result.stdout.split(/\r?\n/u, 1)[0]?.trim() ?? '';
|
|
99
|
+
} finally {
|
|
100
|
+
try {
|
|
101
|
+
await rm(root, { recursive: true, force: true });
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('probe_cleanup_failed');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @typedef {'qualified'|'host_checked'|'rejected'} ClaudeCompatibilityStatus */
|
|
109
|
+
|
|
110
|
+
/** @typedef {object} ClaudeCompatibility
|
|
111
|
+
* @property {ClaudeCompatibilityStatus} status
|
|
112
|
+
* @property {string|null} executable
|
|
113
|
+
* @property {string|null} version
|
|
114
|
+
* @property {string} reason
|
|
115
|
+
* @property {string[]} [checks]
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/** @param {string} output */
|
|
119
|
+
export function parseClaudeStableVersion(output) {
|
|
120
|
+
const match = output
|
|
121
|
+
.trim()
|
|
122
|
+
.match(
|
|
123
|
+
/^(?:(?:claude(?:\s+code)?)\s+)?(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})(-[A-Za-z0-9.-]+)?(?:\s+\(Claude\s+Code\))?$/iu,
|
|
124
|
+
);
|
|
125
|
+
if (!match) return null;
|
|
126
|
+
return {
|
|
127
|
+
major: Number(match[1]),
|
|
128
|
+
minor: Number(match[2]),
|
|
129
|
+
patch: Number(match[3]),
|
|
130
|
+
prerelease: match[4] !== undefined,
|
|
131
|
+
text: `${match[1]}.${match[2]}.${match[3]}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {string} output */
|
|
136
|
+
function parseVersion(output) {
|
|
137
|
+
return parseClaudeStableVersion(output) ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @param {string} executable @param {string[]} args @param {NodeJS.ProcessEnv} environment @param {{timeoutMs?:number,maxOutputBytes?:number}} [limits] */
|
|
141
|
+
function runBounded(executable, args, environment, limits = {}) {
|
|
142
|
+
const timeoutMs = limits.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
143
|
+
const maxOutputBytes = limits.maxOutputBytes ?? MAX_OUTPUT_BYTES;
|
|
144
|
+
return new Promise((resolvePromise) => {
|
|
145
|
+
const child = spawn(executable, args, {
|
|
146
|
+
cwd: environment.CLAUDE_COMPAT_CWD,
|
|
147
|
+
env: environment,
|
|
148
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
149
|
+
detached: process.platform !== 'win32',
|
|
150
|
+
});
|
|
151
|
+
let stdout = '';
|
|
152
|
+
let stderr = '';
|
|
153
|
+
let bytes = 0;
|
|
154
|
+
let overflow = false;
|
|
155
|
+
let settled = false;
|
|
156
|
+
let timedOut = false;
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
timedOut = true;
|
|
159
|
+
if (process.platform !== 'win32' && child.pid !== undefined) {
|
|
160
|
+
try {
|
|
161
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
162
|
+
} catch {
|
|
163
|
+
child.kill('SIGKILL');
|
|
164
|
+
}
|
|
165
|
+
} else child.kill('SIGKILL');
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
/** @param {string} target @param {Buffer} chunk */
|
|
168
|
+
const append = (target, chunk) => {
|
|
169
|
+
bytes += chunk.byteLength;
|
|
170
|
+
if (bytes > maxOutputBytes) {
|
|
171
|
+
overflow = true;
|
|
172
|
+
return target;
|
|
173
|
+
}
|
|
174
|
+
return `${target}${chunk.toString('utf8')}`;
|
|
175
|
+
};
|
|
176
|
+
/** @param {number|null} code */
|
|
177
|
+
const finish = (code) => {
|
|
178
|
+
if (settled) return;
|
|
179
|
+
settled = true;
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
resolvePromise({ code, stdout, stderr, overflow, timedOut });
|
|
182
|
+
};
|
|
183
|
+
child.stdout.on('data', (chunk) => {
|
|
184
|
+
stdout = append(stdout, chunk);
|
|
185
|
+
});
|
|
186
|
+
child.stderr.on('data', (chunk) => {
|
|
187
|
+
stderr = append(stderr, chunk);
|
|
188
|
+
});
|
|
189
|
+
child.once('error', () => finish(null));
|
|
190
|
+
child.once('close', (code) => finish(code));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @param {string} root @param {string} relativePath */
|
|
195
|
+
async function assertNoSymlinkPath(root, relativePath) {
|
|
196
|
+
let current = resolve(root);
|
|
197
|
+
const parts = relativePath.split('/');
|
|
198
|
+
for (const part of parts) {
|
|
199
|
+
current = join(current, part);
|
|
200
|
+
const info = await lstat(current);
|
|
201
|
+
if (info.isSymbolicLink()) throw new Error('plugin_asset_symlink_rejected');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {string} source @param {string} destination */
|
|
206
|
+
async function copyAllowlistedPlugin(source, destination) {
|
|
207
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
208
|
+
let totalBytes = 0;
|
|
209
|
+
for (const file of ALLOWED_PLUGIN_FILES) {
|
|
210
|
+
const input = join(source, file);
|
|
211
|
+
await assertNoSymlinkPath(source, file);
|
|
212
|
+
const info = await lstat(input);
|
|
213
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error('plugin_asset_invalid');
|
|
214
|
+
const uid = process.getuid?.();
|
|
215
|
+
if (uid !== undefined && info.uid !== uid) throw new Error('plugin_asset_owner_mismatch');
|
|
216
|
+
if (info.size > MAX_PLUGIN_ASSET_BYTES) throw new Error('plugin_asset_too_large');
|
|
217
|
+
totalBytes += info.size;
|
|
218
|
+
if (totalBytes > MAX_PLUGIN_TOTAL_BYTES) throw new Error('plugin_assets_too_large');
|
|
219
|
+
const output = join(destination, file);
|
|
220
|
+
await mkdir(dirname(output), { recursive: true, mode: 0o700 });
|
|
221
|
+
await cp(input, output, { force: true, errorOnExist: true, verbatimSymlinks: true });
|
|
222
|
+
await chmod(output, 0o600);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** @param {string} directory */
|
|
227
|
+
async function assertEmptyDirectory(directory) {
|
|
228
|
+
const entries = await readdir(directory);
|
|
229
|
+
if (entries.length !== 0) throw new Error('probe_directory_not_empty');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
233
|
+
function record(value) {
|
|
234
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @param {unknown} value @returns {value is string[]} */
|
|
238
|
+
function stringArray(value) {
|
|
239
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @param {string} executable @param {string} pluginRoot @param {{timeoutMs?:number,maxOutputBytes?:number}} [limits] */
|
|
243
|
+
async function runHostProbe(executable, pluginRoot, limits = {}) {
|
|
244
|
+
const root = await mkdtemp(join(tmpdir(), 'aibrain-claude-host-'));
|
|
245
|
+
const home = join(root, 'home');
|
|
246
|
+
const config = join(root, 'config');
|
|
247
|
+
const cwd = join(root, 'cwd');
|
|
248
|
+
const copiedPlugin = join(root, 'plugin');
|
|
249
|
+
const contractPlugin = join(root, 'contract-plugin');
|
|
250
|
+
await Promise.all([
|
|
251
|
+
mkdir(home, { recursive: true, mode: 0o700 }),
|
|
252
|
+
mkdir(config, { recursive: true, mode: 0o700 }),
|
|
253
|
+
mkdir(cwd, { recursive: true, mode: 0o700 }),
|
|
254
|
+
]);
|
|
255
|
+
try {
|
|
256
|
+
await copyAllowlistedPlugin(pluginRoot, copiedPlugin);
|
|
257
|
+
await copyAllowlistedPlugin(pluginRoot, contractPlugin);
|
|
258
|
+
await writeFile(
|
|
259
|
+
join(contractPlugin, 'hooks', 'hooks.json'),
|
|
260
|
+
JSON.stringify({ modules: ['./native-host-contract-runtime.ts'] }),
|
|
261
|
+
{ mode: 0o600 },
|
|
262
|
+
);
|
|
263
|
+
/** @type {NodeJS.ProcessEnv} */
|
|
264
|
+
const environment = {
|
|
265
|
+
PATH: safePath(executable),
|
|
266
|
+
HOME: home,
|
|
267
|
+
CLAUDE_CONFIG_DIR: config,
|
|
268
|
+
CLAUDE_COMPAT_CWD: cwd,
|
|
269
|
+
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: '1',
|
|
270
|
+
AIBRAIN_NATIVE_CONTRACT: '1',
|
|
271
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
|
|
272
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
273
|
+
NO_COLOR: '1',
|
|
274
|
+
CI: '1',
|
|
275
|
+
};
|
|
276
|
+
const help = await runBounded(executable, ['--help'], environment, limits);
|
|
277
|
+
if (
|
|
278
|
+
help.code !== 0 ||
|
|
279
|
+
help.overflow ||
|
|
280
|
+
REQUIRED_FLAGS.some((flag) => !help.stdout.includes(flag))
|
|
281
|
+
)
|
|
282
|
+
return { ok: false, reason: 'launch_flags_missing' };
|
|
283
|
+
const validate = await runBounded(
|
|
284
|
+
executable,
|
|
285
|
+
['plugin', 'validate', '--strict', '--json', copiedPlugin],
|
|
286
|
+
environment,
|
|
287
|
+
limits,
|
|
288
|
+
);
|
|
289
|
+
if (validate.code !== 0 || validate.overflow)
|
|
290
|
+
return { ok: false, reason: 'plugin_validation_failed' };
|
|
291
|
+
/** @type {Record<string, unknown>} */
|
|
292
|
+
let report;
|
|
293
|
+
try {
|
|
294
|
+
const parsed = JSON.parse(validate.stdout);
|
|
295
|
+
if (!record(parsed)) return { ok: false, reason: 'plugin_validation_malformed' };
|
|
296
|
+
report = parsed;
|
|
297
|
+
} catch {
|
|
298
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
299
|
+
}
|
|
300
|
+
if (report.success !== true || report.strict !== true)
|
|
301
|
+
return { ok: false, reason: 'plugin_validation_failed' };
|
|
302
|
+
if (
|
|
303
|
+
!record(report.manifest) ||
|
|
304
|
+
!stringArray(report.manifest.errors) ||
|
|
305
|
+
!stringArray(report.manifest.warnings)
|
|
306
|
+
)
|
|
307
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
308
|
+
if (
|
|
309
|
+
!Array.isArray(report.contents) ||
|
|
310
|
+
report.contents.length !== 1 ||
|
|
311
|
+
!report.contents.every(record)
|
|
312
|
+
)
|
|
313
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
314
|
+
const contents = report.contents;
|
|
315
|
+
if (
|
|
316
|
+
!contents.every(
|
|
317
|
+
(item) => stringArray(item.errors) && stringArray(item.warnings) && stringArray(item.notes),
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
321
|
+
const manifest = report.manifest;
|
|
322
|
+
const errors = [
|
|
323
|
+
...(Array.isArray(manifest.errors) ? manifest.errors : []),
|
|
324
|
+
...(Array.isArray(manifest.warnings) ? manifest.warnings : []),
|
|
325
|
+
...contents.flatMap((item) => [
|
|
326
|
+
...(Array.isArray(item.errors) ? item.errors : []),
|
|
327
|
+
...(Array.isArray(item.warnings) ? item.warnings : []),
|
|
328
|
+
]),
|
|
329
|
+
];
|
|
330
|
+
if (errors.length > 0) return { ok: false, reason: 'plugin_validation_failed' };
|
|
331
|
+
const notes = contents.flatMap((item) => item.notes);
|
|
332
|
+
const hookNote = notes.find((note) => typeof note === 'string' && note.includes(' hooks: '));
|
|
333
|
+
const callNote = notes.find((note) => typeof note === 'string' && note.includes(' calls: '));
|
|
334
|
+
const hooks = new Set(
|
|
335
|
+
typeof hookNote === 'string'
|
|
336
|
+
? hookNote
|
|
337
|
+
.split(' hooks: ', 2)[1]
|
|
338
|
+
.split(', ')
|
|
339
|
+
.map((item) => item.trim())
|
|
340
|
+
: [],
|
|
341
|
+
);
|
|
342
|
+
const calls = new Set(
|
|
343
|
+
typeof callNote === 'string'
|
|
344
|
+
? callNote
|
|
345
|
+
.split(' calls: ', 2)[1]
|
|
346
|
+
.replace(/\s+\(via[^)]*\)/gu, '')
|
|
347
|
+
.split(', ')
|
|
348
|
+
.map((item) => item.trim())
|
|
349
|
+
: [],
|
|
350
|
+
);
|
|
351
|
+
const inventory = new Set([...hooks, ...calls]);
|
|
352
|
+
if (inventory.size === 0 || REQUIRED_INVENTORY.some((item) => !inventory.has(item)))
|
|
353
|
+
return { ok: false, reason: 'host_inventory_incomplete' };
|
|
354
|
+
const test = await runBounded(
|
|
355
|
+
executable,
|
|
356
|
+
['plugin', 'test', contractPlugin],
|
|
357
|
+
environment,
|
|
358
|
+
limits,
|
|
359
|
+
);
|
|
360
|
+
const passMatch = (test.stdout + test.stderr).match(/(?:^|\s)(\d+)\s+pass(?:ed)?(?:\s|$)/iu);
|
|
361
|
+
const failMatch = (test.stdout + test.stderr).match(/(?:^|\s)(\d+)\s+fail(?:ed)?(?:\s|$)/iu);
|
|
362
|
+
if (test.timedOut) return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
363
|
+
if (test.code !== 0) return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
364
|
+
if (test.overflow) return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
365
|
+
if (passMatch === null || Number(passMatch[1]) < 1)
|
|
366
|
+
return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
367
|
+
if (failMatch === null || Number(failMatch[1]) !== 0)
|
|
368
|
+
return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
369
|
+
if (
|
|
370
|
+
!test.stdout.includes(
|
|
371
|
+
'AI Brain enabled host contract invokes usage and native compaction through the dispatcher',
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
375
|
+
await assertEmptyDirectory(cwd);
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
checks: [
|
|
379
|
+
'launch_flags',
|
|
380
|
+
'plugin_validate_strict',
|
|
381
|
+
'native_plugin_test',
|
|
382
|
+
'isolated_filesystem',
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
const reason = error instanceof Error ? error.message : '';
|
|
387
|
+
const allowedReasons = new Set([
|
|
388
|
+
'plugin_asset_invalid',
|
|
389
|
+
'plugin_asset_symlink_rejected',
|
|
390
|
+
'plugin_asset_owner_mismatch',
|
|
391
|
+
'plugin_asset_too_large',
|
|
392
|
+
'plugin_assets_too_large',
|
|
393
|
+
'probe_directory_not_empty',
|
|
394
|
+
]);
|
|
395
|
+
return { ok: false, reason: allowedReasons.has(reason) ? reason : 'host_probe_failed' };
|
|
396
|
+
} finally {
|
|
397
|
+
try {
|
|
398
|
+
await rm(root, { recursive: true, force: true });
|
|
399
|
+
} catch {
|
|
400
|
+
throw new Error('probe_cleanup_failed');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** @param {string} executable @param {string} packageRoot */
|
|
406
|
+
/** @param {string} executable @param {string} packageRoot @returns {Promise<ClaudeCompatibility>} */
|
|
407
|
+
export async function resolveClaudeCompatibility(executable, packageRoot) {
|
|
408
|
+
let pinnedExecutable;
|
|
409
|
+
try {
|
|
410
|
+
pinnedExecutable = await realpath(executable);
|
|
411
|
+
} catch {
|
|
412
|
+
return {
|
|
413
|
+
status: 'rejected',
|
|
414
|
+
executable: null,
|
|
415
|
+
version: null,
|
|
416
|
+
reason: 'executable_unavailable',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
let raw;
|
|
420
|
+
try {
|
|
421
|
+
raw = await readNativeVersion(pinnedExecutable);
|
|
422
|
+
} catch {
|
|
423
|
+
return {
|
|
424
|
+
status: 'rejected',
|
|
425
|
+
executable: pinnedExecutable,
|
|
426
|
+
version: null,
|
|
427
|
+
reason: 'version_probe_cleanup_failed',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const version = parseVersion(raw);
|
|
431
|
+
if (!version)
|
|
432
|
+
return {
|
|
433
|
+
status: 'rejected',
|
|
434
|
+
executable: pinnedExecutable,
|
|
435
|
+
version: null,
|
|
436
|
+
reason: 'version_unavailable',
|
|
437
|
+
};
|
|
438
|
+
if (version.prerelease)
|
|
439
|
+
return {
|
|
440
|
+
status: 'rejected',
|
|
441
|
+
executable: pinnedExecutable,
|
|
442
|
+
version: version.text,
|
|
443
|
+
reason: 'prerelease',
|
|
444
|
+
};
|
|
445
|
+
if (version.major !== HOST_MAJOR || version.minor !== HOST_MINOR) {
|
|
446
|
+
return {
|
|
447
|
+
status: 'rejected',
|
|
448
|
+
executable: pinnedExecutable,
|
|
449
|
+
version: version.text,
|
|
450
|
+
reason: 'major_minor_unsupported',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (QUALIFIED_VERSIONS.has(version.text))
|
|
454
|
+
return {
|
|
455
|
+
status: 'qualified',
|
|
456
|
+
executable: pinnedExecutable,
|
|
457
|
+
version: version.text,
|
|
458
|
+
reason: 'known_stable',
|
|
459
|
+
};
|
|
460
|
+
if (version.patch < 303)
|
|
461
|
+
return {
|
|
462
|
+
status: 'rejected',
|
|
463
|
+
executable: pinnedExecutable,
|
|
464
|
+
version: version.text,
|
|
465
|
+
reason: 'unsupported_stable_patch',
|
|
466
|
+
};
|
|
467
|
+
let probe;
|
|
468
|
+
try {
|
|
469
|
+
probe = await runHostProbe(pinnedExecutable, join(resolve(packageRoot), 'assets', 'claude'));
|
|
470
|
+
} catch {
|
|
471
|
+
return {
|
|
472
|
+
status: 'rejected',
|
|
473
|
+
executable: pinnedExecutable,
|
|
474
|
+
version: version.text,
|
|
475
|
+
reason: 'host_probe_cleanup_failed',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (!probe.ok)
|
|
479
|
+
return {
|
|
480
|
+
status: 'rejected',
|
|
481
|
+
executable: pinnedExecutable,
|
|
482
|
+
version: version.text,
|
|
483
|
+
reason: probe.reason ?? 'host_probe_failed',
|
|
484
|
+
};
|
|
485
|
+
return {
|
|
486
|
+
status: 'host_checked',
|
|
487
|
+
executable: pinnedExecutable,
|
|
488
|
+
version: version.text,
|
|
489
|
+
reason: 'native_host_contract',
|
|
490
|
+
...(probe.checks === undefined ? {} : { checks: probe.checks }),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** @param {ClaudeCompatibility} decision */
|
|
495
|
+
export function claudeCompatibilityError(decision) {
|
|
496
|
+
const detected = decision.version ?? 'unavailable';
|
|
497
|
+
return `claude_version_unverified: Claude Code detected version ${detected}; reason ${decision.reason}; run aisac doctor --json or install a compatible stable native version.`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const claudeCompatibilityInternals = Object.freeze({
|
|
501
|
+
REQUIRED_FLAGS,
|
|
502
|
+
REQUIRED_INVENTORY,
|
|
503
|
+
ALLOWED_PLUGIN_FILES,
|
|
504
|
+
parseVersion,
|
|
505
|
+
runHostProbe,
|
|
506
|
+
});
|