@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,194 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AISAC Engineering license check (ADR-0047 wave E3).
|
|
5
|
+
*
|
|
6
|
+
* The server is the only authority. This answer is shown to the user and
|
|
7
|
+
* never unlocks anything locally: every failure is reported as
|
|
8
|
+
* `unavailable`, never as licensed. The account token goes only to the
|
|
9
|
+
* configured AI Brain origin, redirects are refused, and the response is
|
|
10
|
+
* size-bounded and validated before any field is displayed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const LICENSE_PATH = '/engineering/v1/license';
|
|
14
|
+
export const LICENSE_TIMEOUT_MS = 5_000;
|
|
15
|
+
const MAX_LICENSE_BODY_BYTES = 16 * 1024;
|
|
16
|
+
const MAX_URL_LENGTH = 2_048;
|
|
17
|
+
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
|
|
18
|
+
// Reasons are displayed, so only a plain identifier is accepted. New server
|
|
19
|
+
// reasons stay readable by old clients without an allowlist update.
|
|
20
|
+
const SAFE_REASON = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
21
|
+
/** @type {ReadonlyMap<number, string>} */
|
|
22
|
+
const UNAVAILABLE_BY_STATUS = new Map([
|
|
23
|
+
[401, 'unauthorized'],
|
|
24
|
+
[404, 'not_offered'],
|
|
25
|
+
[429, 'rate_limited'],
|
|
26
|
+
[503, 'license_unavailable'],
|
|
27
|
+
]);
|
|
28
|
+
const SERVER_ERROR_STATUS = 500;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {{
|
|
32
|
+
* state: 'active' | 'inactive' | 'unavailable',
|
|
33
|
+
* reason: string,
|
|
34
|
+
* validUntil: string | null,
|
|
35
|
+
* purchaseUrl: string | null
|
|
36
|
+
* }} EngineeringLicense
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** @param {string} reason @returns {EngineeringLicense} */
|
|
40
|
+
export function unavailableLicense(reason) {
|
|
41
|
+
return { state: 'unavailable', reason, validUntil: null, purchaseUrl: null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {string} endpoint */
|
|
45
|
+
export function engineeringLicenseUrl(endpoint) {
|
|
46
|
+
return new URL(LICENSE_PATH, endpoint).toString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
50
|
+
export function isRecord(value) {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {unknown} value @returns {value is string} */
|
|
55
|
+
export function isPurchaseUrl(value) {
|
|
56
|
+
if (typeof value !== 'string' || value.length > MAX_URL_LENGTH) return false;
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(value);
|
|
59
|
+
return url.protocol === 'https:' && url.username === '' && url.password === '';
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {unknown} value */
|
|
66
|
+
function isValidUntil(value) {
|
|
67
|
+
return (
|
|
68
|
+
value === null ||
|
|
69
|
+
(typeof value === 'string' && ISO_INSTANT.test(value) && Number.isFinite(Date.parse(value)))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate the 200 body of the license route (OpenAPI `EngineeringLicense`).
|
|
75
|
+
* Unknown extra fields are ignored; anything malformed rejects the whole body.
|
|
76
|
+
* @param {string} text
|
|
77
|
+
* @returns {EngineeringLicense | null}
|
|
78
|
+
*/
|
|
79
|
+
export function parseLicenseBody(text) {
|
|
80
|
+
let body;
|
|
81
|
+
try {
|
|
82
|
+
body = JSON.parse(text);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (!isRecord(body) || body.schema_version !== 1 || body.capability !== 'engineering')
|
|
87
|
+
return null;
|
|
88
|
+
const { state, reason, valid_until: validUntil, purchase_url: purchaseUrl } = body;
|
|
89
|
+
if (state !== 'active' && state !== 'inactive') return null;
|
|
90
|
+
if (typeof reason !== 'string' || !SAFE_REASON.test(reason)) return null;
|
|
91
|
+
if (!isValidUntil(validUntil) || !isPurchaseUrl(purchaseUrl)) return null;
|
|
92
|
+
return {
|
|
93
|
+
state,
|
|
94
|
+
reason,
|
|
95
|
+
validUntil: /** @type {string | null} */ (validUntil),
|
|
96
|
+
purchaseUrl: /** @type {string} */ (purchaseUrl),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read a response body as UTF-8, refusing anything above `limit` bytes.
|
|
102
|
+
* @param {Response} response
|
|
103
|
+
* @param {number} [limit]
|
|
104
|
+
*/
|
|
105
|
+
export async function readBoundedText(response, limit = MAX_LICENSE_BODY_BYTES) {
|
|
106
|
+
const declared = Number(response.headers.get('content-length') ?? '0');
|
|
107
|
+
if (declared > limit) throw new Error('body_too_large');
|
|
108
|
+
if (response.body === null) return '';
|
|
109
|
+
const reader = response.body.getReader();
|
|
110
|
+
/** @type {Uint8Array[]} */
|
|
111
|
+
const chunks = [];
|
|
112
|
+
let size = 0;
|
|
113
|
+
for (;;) {
|
|
114
|
+
const { done, value } = await reader.read();
|
|
115
|
+
if (done) break;
|
|
116
|
+
size += value.byteLength;
|
|
117
|
+
if (size > limit) {
|
|
118
|
+
await reader.cancel().catch(() => undefined);
|
|
119
|
+
throw new Error('body_too_large');
|
|
120
|
+
}
|
|
121
|
+
chunks.push(value);
|
|
122
|
+
}
|
|
123
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @param {unknown} error */
|
|
127
|
+
export function isTimeout(error) {
|
|
128
|
+
return error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @param {number} status */
|
|
132
|
+
export function unavailableReasonFor(status) {
|
|
133
|
+
return (
|
|
134
|
+
UNAVAILABLE_BY_STATUS.get(status) ??
|
|
135
|
+
(status >= SERVER_ERROR_STATUS ? 'server_error' : 'unexpected_status')
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ask the server whether this account holds the AISAC Engineering license.
|
|
141
|
+
* @param {string} endpoint configured AI Brain MCP endpoint (already validated at setup)
|
|
142
|
+
* @param {string} token account PAT
|
|
143
|
+
* @param {{ fetchImpl?: typeof fetch, timeoutMs?: number }} [options]
|
|
144
|
+
* @returns {Promise<EngineeringLicense>}
|
|
145
|
+
*/
|
|
146
|
+
export async function checkEngineeringLicense(endpoint, token, options = {}) {
|
|
147
|
+
const { fetchImpl = fetch, timeoutMs = LICENSE_TIMEOUT_MS } = options;
|
|
148
|
+
let url;
|
|
149
|
+
try {
|
|
150
|
+
url = engineeringLicenseUrl(endpoint);
|
|
151
|
+
} catch {
|
|
152
|
+
return unavailableLicense('invalid_endpoint');
|
|
153
|
+
}
|
|
154
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
155
|
+
let response;
|
|
156
|
+
try {
|
|
157
|
+
response = await fetchImpl(url, {
|
|
158
|
+
method: 'GET',
|
|
159
|
+
redirect: 'error',
|
|
160
|
+
signal,
|
|
161
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return unavailableLicense(isTimeout(error) ? 'timeout' : 'unreachable');
|
|
165
|
+
}
|
|
166
|
+
if (response.status !== 200) {
|
|
167
|
+
await response.body?.cancel().catch(() => undefined);
|
|
168
|
+
return unavailableLicense(unavailableReasonFor(response.status));
|
|
169
|
+
}
|
|
170
|
+
let text;
|
|
171
|
+
try {
|
|
172
|
+
text = await readBoundedText(response);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return unavailableLicense(isTimeout(error) ? 'timeout' : 'invalid_response');
|
|
175
|
+
}
|
|
176
|
+
return parseLicenseBody(text) ?? unavailableLicense('invalid_response');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One line for `aisac setup`. Returns null while the server does not offer the
|
|
181
|
+
* capability yet, so memory-only users see no new noise.
|
|
182
|
+
* @param {EngineeringLicense} license
|
|
183
|
+
* @returns {string | null}
|
|
184
|
+
*/
|
|
185
|
+
export function licenseLine(license) {
|
|
186
|
+
if (license.state === 'active') {
|
|
187
|
+
const until = license.validUntil === null ? '' : ` until ${license.validUntil.slice(0, 10)}`;
|
|
188
|
+
return `AISAC Engineering: active${until}.`;
|
|
189
|
+
}
|
|
190
|
+
if (license.state === 'inactive')
|
|
191
|
+
return `AISAC Engineering: inactive (${license.reason}) — ${license.purchaseUrl ?? ''}`.trimEnd();
|
|
192
|
+
if (license.reason === 'not_offered') return null;
|
|
193
|
+
return `AISAC Engineering: unavailable (${license.reason}); memory and context are unaffected.`;
|
|
194
|
+
}
|
package/dist/launch.mjs
CHANGED
|
@@ -4,13 +4,20 @@ import { access, chmod, lstat, mkdir, mkdtemp, open, rm, writeFile } from 'node:
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { constants } from 'node:fs';
|
|
6
6
|
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { captureLockPlatformCheck, nativeVersion, resolveNative } from './platform.mjs';
|
|
8
|
+
import { claudeCompatibilityError, resolveClaudeCompatibility } from './claude-compatibility.mjs';
|
|
9
|
+
import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
|
|
8
10
|
import {
|
|
9
11
|
claudeCheckpointConfigBinding,
|
|
10
12
|
claudeObservationConfigBinding,
|
|
11
13
|
telemetryConfigBinding,
|
|
12
14
|
} from './state.mjs';
|
|
13
15
|
import { codexPolicyJson, makePolicy } from './policy.mjs';
|
|
16
|
+
import {
|
|
17
|
+
prepareEngineeringPack,
|
|
18
|
+
sweepStaleLaunches,
|
|
19
|
+
writeLaunchMarker,
|
|
20
|
+
} from './engineering-pack.mjs';
|
|
14
21
|
import {
|
|
15
22
|
CODEX_LAUNCH_ENVELOPE,
|
|
16
23
|
CODEX_MONITOR_CWD,
|
|
@@ -359,14 +366,26 @@ async function assertClaudeCheckpointHelper(packageRoot) {
|
|
|
359
366
|
return helperPath;
|
|
360
367
|
}
|
|
361
368
|
|
|
362
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* A fresh private directory for one launch. Directories left by launches whose
|
|
371
|
+
* processes are gone are swept first; this one records its launcher.
|
|
372
|
+
* @param {string} root @param {string} prefix
|
|
373
|
+
*/
|
|
363
374
|
async function launchDirectory(root, prefix) {
|
|
375
|
+
await sweepStaleLaunches(root);
|
|
364
376
|
const parent = await privateDirectory(join(root, 'runtime'));
|
|
365
377
|
const directory = await mkdtemp(join(parent, `${prefix}-`));
|
|
366
378
|
await chmod(directory, 0o700);
|
|
379
|
+
await writeLaunchMarker(directory, { launcherPid: process.pid, childPid: null });
|
|
367
380
|
return directory;
|
|
368
381
|
}
|
|
369
382
|
|
|
383
|
+
/** @param {string} directory @param {number | undefined} childPid */
|
|
384
|
+
async function recordChild(directory, childPid) {
|
|
385
|
+
if (childPid === undefined) return;
|
|
386
|
+
await writeLaunchMarker(directory, { launcherPid: process.pid, childPid }).catch(() => undefined);
|
|
387
|
+
}
|
|
388
|
+
|
|
370
389
|
/** @param {import('node:child_process').ChildProcess} child @param {string} directory */
|
|
371
390
|
function waitForChild(child, directory) {
|
|
372
391
|
return new Promise((resolvePromise) => {
|
|
@@ -399,15 +418,32 @@ function waitForChild(child, directory) {
|
|
|
399
418
|
});
|
|
400
419
|
}
|
|
401
420
|
|
|
402
|
-
/**
|
|
403
|
-
|
|
421
|
+
/**
|
|
422
|
+
* @typedef {object} LaunchDependencies
|
|
423
|
+
* @property {typeof prepareEngineeringPack} [preparePack] AISAC Engineering pack loader (E4b)
|
|
424
|
+
*/
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @param {LaunchConfig} config
|
|
428
|
+
* @param {string} root
|
|
429
|
+
* @param {string[]} args
|
|
430
|
+
* @param {LaunchDependencies} [dependencies]
|
|
431
|
+
*/
|
|
432
|
+
export async function launchClaude(config, root, args, dependencies = {}) {
|
|
433
|
+
const preparePack = dependencies.preparePack ?? prepareEngineeringPack;
|
|
404
434
|
const validated = assertConfig(config);
|
|
405
435
|
const launchArgs = await parseClaudeLaunchArgs(args, process.cwd());
|
|
406
436
|
assertClaudeArgs(launchArgs.args);
|
|
407
437
|
const executable = resolveNative('claude');
|
|
408
438
|
if (!executable) throw new Error('claude_not_found');
|
|
409
|
-
const
|
|
410
|
-
|
|
439
|
+
const compatibility = await resolveClaudeCompatibility(executable, validated.packageRoot);
|
|
440
|
+
if (compatibility.status === 'rejected') throw new Error(claudeCompatibilityError(compatibility));
|
|
441
|
+
const pinnedExecutable = compatibility.executable ?? executable;
|
|
442
|
+
if (compatibility.status === 'host_checked')
|
|
443
|
+
process.stderr.write(
|
|
444
|
+
`aisac: Claude ${compatibility.version ?? 'unknown'} accepted by the local native host contract; end-to-end recovery remains separately qualified.\n`,
|
|
445
|
+
);
|
|
446
|
+
const currentVersion = compatibility.version ?? nativeVersion(pinnedExecutable);
|
|
411
447
|
const pluginRoot = await assertClaudeAssets(validated.packageRoot);
|
|
412
448
|
const telemetryActive =
|
|
413
449
|
validated.telemetry?.enabled === true && validated.telemetry?.disclosedAt != null;
|
|
@@ -526,13 +562,22 @@ export async function launchClaude(config, root, args) {
|
|
|
526
562
|
await rm(directory, { recursive: true, force: true });
|
|
527
563
|
throw error;
|
|
528
564
|
}
|
|
565
|
+
// The pack never blocks the launch: every failure starts Claude without it.
|
|
566
|
+
const engineering = await preparePack({
|
|
567
|
+
endpoint: validated.endpoint,
|
|
568
|
+
token: validated.token,
|
|
569
|
+
root,
|
|
570
|
+
directory,
|
|
571
|
+
packageRoot: validated.packageRoot,
|
|
572
|
+
}).catch(() => ({ pluginRoot: null, line: null }));
|
|
573
|
+
if (engineering.line !== null) process.stderr.write(`${engineering.line}\n`);
|
|
529
574
|
const environment = childEnvironment(root);
|
|
530
575
|
environment.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS = '1';
|
|
531
576
|
/** @type {import('node:child_process').ChildProcess} */
|
|
532
577
|
let child;
|
|
533
578
|
try {
|
|
534
579
|
child = spawn(
|
|
535
|
-
|
|
580
|
+
pinnedExecutable,
|
|
536
581
|
[
|
|
537
582
|
'--settings',
|
|
538
583
|
settingsPath,
|
|
@@ -542,6 +587,7 @@ export async function launchClaude(config, root, args) {
|
|
|
542
587
|
requiredAllow.join(','),
|
|
543
588
|
'--plugin-dir',
|
|
544
589
|
pluginRoot,
|
|
590
|
+
...(engineering.pluginRoot === null ? [] : ['--plugin-dir', engineering.pluginRoot]),
|
|
545
591
|
...launchArgs.args,
|
|
546
592
|
],
|
|
547
593
|
{ cwd: process.cwd(), env: environment, stdio: 'inherit' },
|
|
@@ -550,20 +596,28 @@ export async function launchClaude(config, root, args) {
|
|
|
550
596
|
await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
551
597
|
throw error;
|
|
552
598
|
}
|
|
553
|
-
|
|
599
|
+
// Listen for exit before any await, so a child that exits at once is not missed.
|
|
600
|
+
const finished = waitForChild(child, directory);
|
|
601
|
+
await recordChild(directory, child.pid);
|
|
602
|
+
return finished;
|
|
554
603
|
}
|
|
555
604
|
|
|
556
605
|
/** @param {LaunchConfig} config @param {string} root @param {string[]} args */
|
|
557
606
|
export async function launchCodex(config, root, args) {
|
|
558
607
|
const validated = assertConfig(config);
|
|
559
608
|
assertCodexArgs(args);
|
|
609
|
+
const captureLockPlatform = captureLockPlatformCheck();
|
|
610
|
+
if (!captureLockPlatform.ok) throw new Error(captureLockPlatform.code);
|
|
560
611
|
const executable = resolveNative('codex');
|
|
561
612
|
const node = resolveNative('node');
|
|
562
613
|
if (!executable || !node) throw new Error('codex_or_node_not_found');
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (
|
|
614
|
+
const compatibility = await resolveCodexCompatibility(executable);
|
|
615
|
+
if (compatibility.status === 'rejected') throw new Error(codexCompatibilityError(compatibility));
|
|
616
|
+
if (compatibility.version === null) throw new Error('codex_version_unverified');
|
|
617
|
+
if (compatibility.status === 'protocol_match')
|
|
618
|
+
process.stderr.write(
|
|
619
|
+
`aisac: Codex ${compatibility.version} matched the local protocol contract; native behavioral qualification is not established.\n`,
|
|
620
|
+
);
|
|
567
621
|
const launchPlan = await parseCodexLaunchArgs(args, process.cwd());
|
|
568
622
|
const packageRoot = resolve(validated.packageRoot);
|
|
569
623
|
const terminal = join(packageRoot, 'assets', 'jev-paid-pilot-terminal.mjs');
|
|
@@ -613,7 +667,7 @@ export async function launchCodex(config, root, args) {
|
|
|
613
667
|
environment.CODEX_CONTEXT_DIAGNOSTICS_ENABLED = '1';
|
|
614
668
|
environment.AIBRAIN_MCP_URL = validated.endpoint;
|
|
615
669
|
environment.AIBRAIN_MCP_TOKEN = validated.token;
|
|
616
|
-
environment.CODEX_CLIENT_VERSION =
|
|
670
|
+
environment.CODEX_CLIENT_VERSION = compatibility.version;
|
|
617
671
|
environment.CODEX_CONTEXT_TELEMETRY_ENABLED = validated.telemetry?.enabled === true ? '1' : '0';
|
|
618
672
|
environment.CODEX_CONTEXT_TELEMETRY_DISCLOSED =
|
|
619
673
|
validated.telemetry?.enabled === true && validated.telemetry?.disclosedAt != null ? '1' : '0';
|
|
@@ -627,7 +681,10 @@ export async function launchCodex(config, root, args) {
|
|
|
627
681
|
env: environment,
|
|
628
682
|
stdio: 'inherit',
|
|
629
683
|
});
|
|
630
|
-
|
|
684
|
+
// Listen for exit before any await, so a child that exits at once is not missed.
|
|
685
|
+
const finished = waitForChild(child, directory);
|
|
686
|
+
await recordChild(directory, child.pid);
|
|
687
|
+
return finished;
|
|
631
688
|
}
|
|
632
689
|
|
|
633
690
|
export const launchInternals = { MCP_ALIAS, PLUGIN_NAME, PLUGIN_INLINE_NAME };
|
package/dist/mcp.mjs
CHANGED
|
@@ -252,7 +252,7 @@ export async function reportContextTelemetry(endpoint, token, report, deadlineMs
|
|
|
252
252
|
params: {
|
|
253
253
|
protocolVersion: '2025-03-26',
|
|
254
254
|
capabilities: {},
|
|
255
|
-
clientInfo: { name: 'aibrain-telemetry-report', version: '0.3.0-beta.
|
|
255
|
+
clientInfo: { name: 'aibrain-telemetry-report', version: '0.3.0-beta.9' },
|
|
256
256
|
},
|
|
257
257
|
},
|
|
258
258
|
undefined,
|
|
@@ -344,7 +344,7 @@ export async function selectContextShadow(endpoint, token, request, deadlineMs =
|
|
|
344
344
|
params: {
|
|
345
345
|
protocolVersion: '2025-03-26',
|
|
346
346
|
capabilities: {},
|
|
347
|
-
clientInfo: { name: 'aibrain-claude-shadow', version: '0.3.0-beta.
|
|
347
|
+
clientInfo: { name: 'aibrain-claude-shadow', version: '0.3.0-beta.9' },
|
|
348
348
|
},
|
|
349
349
|
},
|
|
350
350
|
undefined,
|
|
@@ -428,7 +428,7 @@ export async function readContextBudget(endpoint, token, deadlineMs = 8_000) {
|
|
|
428
428
|
params: {
|
|
429
429
|
protocolVersion: '2025-03-26',
|
|
430
430
|
capabilities: {},
|
|
431
|
-
clientInfo: { name: 'aibrain-context-budget', version: '0.3.0-beta.
|
|
431
|
+
clientInfo: { name: 'aibrain-context-budget', version: '0.3.0-beta.9' },
|
|
432
432
|
},
|
|
433
433
|
},
|
|
434
434
|
undefined,
|
|
@@ -578,7 +578,7 @@ async function providerFreeProbeV1(endpoint, token) {
|
|
|
578
578
|
params: {
|
|
579
579
|
protocolVersion: '2025-03-26',
|
|
580
580
|
capabilities: {},
|
|
581
|
-
clientInfo: { name: 'aibrain', version: '0.3.0-beta.
|
|
581
|
+
clientInfo: { name: 'aibrain', version: '0.3.0-beta.9' },
|
|
582
582
|
},
|
|
583
583
|
},
|
|
584
584
|
undefined,
|
|
@@ -632,7 +632,7 @@ async function providerFreeProbeV1(endpoint, token) {
|
|
|
632
632
|
arguments: {
|
|
633
633
|
schema_version: 1,
|
|
634
634
|
client: 'codex_app_server',
|
|
635
|
-
client_version: '0.3.0-beta.
|
|
635
|
+
client_version: '0.3.0-beta.9',
|
|
636
636
|
session_ref: sessionRef,
|
|
637
637
|
revision,
|
|
638
638
|
observed_at: new Date().toISOString(),
|
|
@@ -724,7 +724,7 @@ export async function providerFreeProbeV2(endpoint, token, policy, clients) {
|
|
|
724
724
|
params: {
|
|
725
725
|
protocolVersion: '2025-03-26',
|
|
726
726
|
capabilities: {},
|
|
727
|
-
clientInfo: { name: 'aibrain', version: '0.3.0-beta.
|
|
727
|
+
clientInfo: { name: 'aibrain', version: '0.3.0-beta.9' },
|
|
728
728
|
},
|
|
729
729
|
},
|
|
730
730
|
undefined,
|
|
@@ -928,7 +928,7 @@ export async function providerFreeProbeV2(endpoint, token, policy, clients) {
|
|
|
928
928
|
await callTool(2, 'brain_context_select', {
|
|
929
929
|
schema_version: 2,
|
|
930
930
|
client: clients === 'claude' ? 'claude_code' : 'codex_app_server',
|
|
931
|
-
client_version: '0.3.0-beta.
|
|
931
|
+
client_version: '0.3.0-beta.9',
|
|
932
932
|
session_ref: sessionRef,
|
|
933
933
|
revision,
|
|
934
934
|
observed_at: now,
|
|
@@ -941,7 +941,7 @@ export async function providerFreeProbeV2(endpoint, token, policy, clients) {
|
|
|
941
941
|
await callTool(3, 'brain_context_compact', {
|
|
942
942
|
schema_version: 2,
|
|
943
943
|
client: 'claude_code',
|
|
944
|
-
client_version: '0.3.0-beta.
|
|
944
|
+
client_version: '0.3.0-beta.9',
|
|
945
945
|
session_ref: sessionRef,
|
|
946
946
|
revision,
|
|
947
947
|
observed_at: now,
|
package/dist/package.json
CHANGED
package/dist/platform.mjs
CHANGED
|
@@ -23,6 +23,19 @@ export function platformCheck() {
|
|
|
23
23
|
return { ok: true };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** @param {NodeJS.ProcessEnv} [environment] */
|
|
27
|
+
export function captureLockPlatformCheck(environment = process.env) {
|
|
28
|
+
const platform = platformCheck();
|
|
29
|
+
if (!platform.ok) return platform;
|
|
30
|
+
if (resolveNative('flock', environment) === null)
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
code: 'CODEX_CAPTURE_LOCK_BACKEND_UNAVAILABLE',
|
|
34
|
+
message: 'Codex capture requires util-linux flock on Linux/WSL.',
|
|
35
|
+
};
|
|
36
|
+
return { ok: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/** @param {string} name @param {NodeJS.ProcessEnv} [environment] */
|
|
27
40
|
export function resolveNative(name, environment = process.env) {
|
|
28
41
|
if (!/^[A-Za-z0-9._+-]+$/u.test(name)) return null;
|
|
@@ -70,13 +83,44 @@ export function nativeVersion(executable, environment = process.env) {
|
|
|
70
83
|
export function isTestedNativeVersion(client, version) {
|
|
71
84
|
const value = version.trim();
|
|
72
85
|
if (client === 'claude')
|
|
73
|
-
return /^(?:(?:claude(?:\s+code)?)\s+)?2\.1\.(?:294|295|296|299|300)(?:\s+\(Claude\s+Code\))?$/iu.test(
|
|
86
|
+
return /^(?:(?:claude(?:\s+code)?)\s+)?2\.1\.(?:294|295|296|299|300|301|302|303)(?:\s+\(Claude\s+Code\))?$/iu.test(
|
|
74
87
|
value,
|
|
75
88
|
);
|
|
76
|
-
return /^(?:codex(?:-cli)?\s+)?0\.16(?:1\.0|2\.0|2\.1)$/iu.test(value);
|
|
89
|
+
return /^(?:codex(?:-cli)?\s+)?0\.16(?:1\.0|2\.0|2\.1|3\.0|4\.0)$/iu.test(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const TESTED_NATIVE_VERSIONS = Object.freeze({
|
|
93
|
+
claude: Object.freeze([
|
|
94
|
+
'2.1.294',
|
|
95
|
+
'2.1.295',
|
|
96
|
+
'2.1.296',
|
|
97
|
+
'2.1.299',
|
|
98
|
+
'2.1.300',
|
|
99
|
+
'2.1.301',
|
|
100
|
+
'2.1.302',
|
|
101
|
+
'2.1.303',
|
|
102
|
+
]),
|
|
103
|
+
codex: Object.freeze(['0.161.0', '0.162.0', '0.162.1', '0.163.0', '0.164.0']),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** @param {'claude'|'codex'} client @param {string} version */
|
|
107
|
+
export function nativeVersionDiagnostic(client, version) {
|
|
108
|
+
const detected = version.trim();
|
|
109
|
+
const versionPattern =
|
|
110
|
+
client === 'claude'
|
|
111
|
+
? /^(?:(?:claude(?:\s+code)?)\s+)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})(-[A-Za-z0-9.-]+)?(?:\s+\(Claude\s+Code\))?$/iu
|
|
112
|
+
: /^(?:codex(?:-cli)?\s+)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})(-[A-Za-z0-9.-]+)?$/iu;
|
|
113
|
+
const versionMatch = detected.match(versionPattern);
|
|
114
|
+
const safeDetected = versionMatch
|
|
115
|
+
? `${versionMatch[1]}.${versionMatch[2]}.${versionMatch[3]}${versionMatch[4] ? '-prerelease' : ''}`
|
|
116
|
+
: 'unavailable';
|
|
117
|
+
const label = client === 'claude' ? 'Claude Code' : 'Codex CLI';
|
|
118
|
+
const tested = TESTED_NATIVE_VERSIONS[client].join(', ');
|
|
119
|
+
return `${client}_version_unverified: ${label} detected version ${safeDetected}; tested stable versions: ${tested}. Update AI Brain to a compatible version, then run aisac doctor --json or install a qualified stable native version.`;
|
|
77
120
|
}
|
|
78
121
|
|
|
79
122
|
/** @param {'claude'|'codex'} client @param {string} version */
|
|
80
123
|
export function assertTestedNativeVersion(client, version) {
|
|
81
|
-
if (!isTestedNativeVersion(client, version))
|
|
124
|
+
if (!isTestedNativeVersion(client, version))
|
|
125
|
+
throw new Error(nativeVersionDiagnostic(client, version));
|
|
82
126
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brunobracaioli/aibrain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-beta.2",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "AISAC client for Claude Code and Codex: AI Brain memory, context management and AISAC Engineering",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
+
"aisac": "./dist/cli.mjs",
|
|
9
10
|
"aibrain": "./dist/cli.mjs"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|