@brunobracaioli/aibrain 0.4.0-beta.1 → 0.4.0-beta.10
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 +46 -3
- package/dist/assets/claude/hooks/context-compaction.ts +5 -1
- package/dist/assets/codex-context-compaction.mjs +98 -1
- package/dist/claude-observation.mjs +1 -1
- package/dist/cli.mjs +73 -4
- package/dist/codex-compatibility.mjs +52 -2
- package/dist/codex-engineering.mjs +165 -0
- package/dist/codex-protocol-fingerprint.json +10 -10
- package/dist/engineering-pack-public-key.json +10 -0
- package/dist/engineering-pack.mjs +845 -0
- package/dist/engineering.mjs +15 -11
- package/dist/extensions-catalog.mjs +310 -0
- package/dist/extensions-cli.mjs +486 -0
- package/dist/extensions-preferences.mjs +151 -0
- package/dist/guardrails/continuity.mjs +239 -0
- package/dist/guardrails/format.mjs +111 -0
- package/dist/guardrails/guards.mjs +397 -0
- package/dist/guardrails/run.mjs +211 -0
- package/dist/guardrails/shell-words.mjs +206 -0
- package/dist/guardrails-plugin.mjs +149 -0
- package/dist/launch.mjs +149 -9
- package/dist/mcp-servers.mjs +192 -0
- package/dist/monitor.mjs +33 -0
- package/dist/package.json +1 -1
- package/dist/platform.mjs +40 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# aisac
|
|
2
2
|
|
|
3
|
-
The AISAC client for Claude Code and Codex on Linux/WSL
|
|
3
|
+
The AISAC client for Claude Code and Codex on Linux/WSL, and for Claude Code on
|
|
4
|
+
macOS, with Node.js 22 or newer:
|
|
4
5
|
AI Brain memory, context management and, for AISAC Founding Cohort members,
|
|
5
6
|
AISAC Engineering.
|
|
6
7
|
|
|
@@ -14,6 +15,41 @@ both provide the `aisac` and `aibrain` commands. State stays in `~/.aibrain`.
|
|
|
14
15
|
shows the answer and sends the request only to the configured AI Brain address.
|
|
15
16
|
`aisac status` stays offline.
|
|
16
17
|
|
|
18
|
+
For licensed accounts, `aisac claude` downloads the AISAC Engineering pack for
|
|
19
|
+
the session, verifies its Ed25519 signature against the key pinned in this
|
|
20
|
+
release, and loads it as the `aisac-engineering` plugin from the session's
|
|
21
|
+
private folder, which is deleted on exit. Any failure starts Claude without the
|
|
22
|
+
pack. `aisac setup --lang pt-BR|en` picks the pack language without a network
|
|
23
|
+
call. The pack is never part of this npm package.
|
|
24
|
+
|
|
25
|
+
`aisac codex` (Codex 0.164.0 or later) loads the same verified pack for that
|
|
26
|
+
session only: its skills are added to the Codex app-server the launch owns
|
|
27
|
+
(call one with `$`, for example `$spec`) and its agents become session agent
|
|
28
|
+
roles. Nothing is written to `~/.codex`, `~/.agents` or the project, and the
|
|
29
|
+
session's folder is deleted on exit. An older Codex starts without the pack and
|
|
30
|
+
says `codex_outdated`.
|
|
31
|
+
|
|
32
|
+
Licensed `aisac claude` sessions also load guardrail hooks as the session plugin
|
|
33
|
+
`aisac-guardrails`.
|
|
34
|
+
- **Default on:** `secrets-guard` refuses secret files by name. Four more hooks
|
|
35
|
+
keep continuity: a repository snapshot at start and after a compaction, a
|
|
36
|
+
docs reminder, and the NOTES/spec/CHANGELOG pass.
|
|
37
|
+
- **Default off:** `git-guard`, `destructive-guard` and `format-on-edit`.
|
|
38
|
+
- **Changing them:** `aisac hooks` lists them, and `aisac hooks enable|disable
|
|
39
|
+
<id>` or `aisac hooks reset` changes the next session. The agent cannot change
|
|
40
|
+
them from inside a session.
|
|
41
|
+
- **Code:** it ships in this package and runs locally. Nothing comes from the
|
|
42
|
+
server.
|
|
43
|
+
|
|
44
|
+
Licensed sessions of `aisac claude` and `aisac codex` can also load reviewed MCP
|
|
45
|
+
servers: `github` (read-only, token in `GITHUB_PERSONAL_ACCESS_TOKEN`), `supabase`
|
|
46
|
+
(read-only), `vercel`, `playwright` (pinned, isolated) and `context7`.
|
|
47
|
+
- **Default:** every server is off. `aisac mcp` lists them, `aisac mcp enable|disable
|
|
48
|
+
<id>` or `aisac mcp reset` changes the next session, and `aisac mcp info <id>` says
|
|
49
|
+
what a server reaches and how to log in.
|
|
50
|
+
- **Login:** the host does it (`/mcp` in Claude Code, `aisac mcp login <id>` for Codex)
|
|
51
|
+
and keeps it in its own store. No token reaches `~/.aibrain` or AISAC.
|
|
52
|
+
|
|
17
53
|
Codex capture also requires the `flock` utility from `util-linux`, normally
|
|
18
54
|
included in Linux/WSL. The launcher and doctor check availability before a
|
|
19
55
|
monitored Codex session starts. Capture coordination is released by the kernel
|
|
@@ -23,7 +59,7 @@ notes or lock files to work around a reported storage error.
|
|
|
23
59
|
|
|
24
60
|
The reviewed native compatibility set is Claude Code 2.1.294, 2.1.295, 2.1.296,
|
|
25
61
|
2.1.299, 2.1.300, 2.1.301, 2.1.302 or 2.1.303, and Codex CLI 0.161.0,
|
|
26
|
-
0.162.0, 0.162.1, 0.163.0 or 0.
|
|
62
|
+
0.162.0, 0.162.1, 0.163.0, 0.164.0 or 0.166.0 (0.165.0 was never qualified). A newer stable Codex 0.x release may continue only
|
|
27
63
|
after its locally generated experimental protocol schemas and transport flags
|
|
28
64
|
match the bundled adapter contract; prereleases, older releases and new majors
|
|
29
65
|
remain rejected. Starting with beta14, newer stable Claude 2.1 patches can
|
|
@@ -39,6 +75,11 @@ record formats. Native launch/resume qualification is separate from a new
|
|
|
39
75
|
model/JEV compaction qualification. A later native release that changes the
|
|
40
76
|
consumed contract can still require an AI Brain compatibility update.
|
|
41
77
|
|
|
78
|
+
`0.4.0-beta.9` refreshes the bundled contract to Codex 0.166.0. Its delta is
|
|
79
|
+
additive: optional item timing, an image generation id, an attach-to-source fork
|
|
80
|
+
flag and two error codes. See the
|
|
81
|
+
[Codex 0.166 qualification record](../../docs/reference/codex-166-compatibility-2026-11-04.md).
|
|
82
|
+
|
|
42
83
|
The Codex terminal bridge permits the native session picker's separate read-only
|
|
43
84
|
connection while keeping the primary conversation and monitor intact. A picker
|
|
44
85
|
cannot compact, start a turn, inject history or take ownership of the primary
|
|
@@ -61,7 +102,9 @@ Installation does not contact AI Brain, change native client configuration, or
|
|
|
61
102
|
enable all paid accounts. Setup requires an existing AI Brain PAT, explicit
|
|
62
103
|
versioned disclosure, and successful provider-free pilot access. Claude and
|
|
63
104
|
Codex continue to own their own authentication. Windows native is unsupported;
|
|
64
|
-
run the beta inside WSL.
|
|
105
|
+
run the beta inside WSL. On macOS, `aisac setup` configures Claude Code, and
|
|
106
|
+
`aisac codex` is refused with `CODEX_MACOS_UNSUPPORTED`. Codex capture needs
|
|
107
|
+
Linux procfs and `flock`, so use Codex from Linux/WSL. Pricing and general customer activation are outside
|
|
65
108
|
this beta.
|
|
66
109
|
|
|
67
110
|
For reviewed noninteractive automation only, use a private token file and
|
|
@@ -151,7 +151,7 @@ type Register = (
|
|
|
151
151
|
) => void;
|
|
152
152
|
|
|
153
153
|
const MAX_RESULT_BYTES = 256 * 1024;
|
|
154
|
-
const CLAUDE_HOOK_VERSION = '0.4.0-beta.
|
|
154
|
+
const CLAUDE_HOOK_VERSION = '0.4.0-beta.7';
|
|
155
155
|
const MAX_DIAGNOSTIC_BYTE_COUNT = 128 * 1024 * 1024;
|
|
156
156
|
const COOLDOWN_MS = 60_000;
|
|
157
157
|
const MAX_TRUNCATED_CODE_POINTS = 300;
|
|
@@ -463,6 +463,10 @@ function v2WireOptions(options: V2Options): Record<string, unknown> {
|
|
|
463
463
|
|
|
464
464
|
function safeErrorCategory(error: unknown): string {
|
|
465
465
|
const record = isRecord(error) ? error : {};
|
|
466
|
+
// A coded error names its own reason; the keyword guess below would read
|
|
467
|
+
// "AI Brain plan does not meet the minimum estimated reduction" as invalid_plan.
|
|
468
|
+
if (COMPACT_ERROR_CODES.includes(record.compactErrorCode as CompactErrorCode))
|
|
469
|
+
return record.compactErrorCode as CompactErrorCode;
|
|
466
470
|
const code = typeof record.code === 'string' ? record.code.toLowerCase() : '';
|
|
467
471
|
const name = typeof record.name === 'string' ? record.name.toLowerCase() : '';
|
|
468
472
|
const message = typeof record.message === 'string' ? record.message.toLowerCase() : '';
|
|
@@ -54,6 +54,12 @@ const MAX_OWNED_REQUESTS = 1_000;
|
|
|
54
54
|
const MAX_RETIRED_REQUESTS = 2_000;
|
|
55
55
|
const MAX_CLIENT_REQUEST_BINDINGS = 128;
|
|
56
56
|
const MAX_OWNED_THREAD_IDS = 256;
|
|
57
|
+
// AISAC Engineering (ADR-0047 E4c): the launcher's skills root is added to the
|
|
58
|
+
// owned app-server before the client sees its initialize answer.
|
|
59
|
+
const ENGINEERING_SKILLS_ROOT_ENV = 'AISAC_ENGINEERING_SKILLS_ROOT';
|
|
60
|
+
const ENGINEERING_SKILLS_ACK_TIMEOUT_MS = 2_000;
|
|
61
|
+
const MAX_ENGINEERING_PATH_LENGTH = 4_096;
|
|
62
|
+
const MAX_TRACKED_INITIALIZE_REQUESTS = 4;
|
|
57
63
|
const MAX_RECOVERY_ITEMS = 128;
|
|
58
64
|
const MAX_RECOVERY_SOURCE_INDEX = 65_536;
|
|
59
65
|
const MAX_NOTE_BYTES = 128 * 1024;
|
|
@@ -2707,6 +2713,8 @@ export function createCodexContextProxy({
|
|
|
2707
2713
|
createDiagnosticSink = null,
|
|
2708
2714
|
brainServer = process.env.AI_BRAIN_MCP_SERVER ?? 'brain',
|
|
2709
2715
|
clientVersion = process.env.CODEX_CLIENT_VERSION ?? '0.161.0',
|
|
2716
|
+
engineeringSkillsRoot = null,
|
|
2717
|
+
engineeringAckTimeoutMs = ENGINEERING_SKILLS_ACK_TIMEOUT_MS,
|
|
2710
2718
|
}) {
|
|
2711
2719
|
const contextPolicy = normalizeContextPolicy(policy);
|
|
2712
2720
|
const effectiveThreshold = contextPolicy?.compactAtPercent ?? threshold;
|
|
@@ -2719,6 +2727,15 @@ export function createCodexContextProxy({
|
|
|
2719
2727
|
const telemetryByThread = new Map();
|
|
2720
2728
|
const diagnosticSinks = new Map();
|
|
2721
2729
|
let stopped = false;
|
|
2730
|
+
// AISAC Engineering skills root: 'off' without a root; 'waiting' for the
|
|
2731
|
+
// client's first successful initialize; 'holding' app-server output until
|
|
2732
|
+
// skills/extraRoots/set is answered (or times out); then 'done'.
|
|
2733
|
+
let engineeringState =
|
|
2734
|
+
typeof engineeringSkillsRoot === 'string' && engineeringSkillsRoot.length > 0
|
|
2735
|
+
? 'waiting'
|
|
2736
|
+
: 'off';
|
|
2737
|
+
const initializeRequestKeys = new Set();
|
|
2738
|
+
let engineeringHeld = null;
|
|
2722
2739
|
function emitDiagnostic(event) {
|
|
2723
2740
|
try {
|
|
2724
2741
|
const phase = typeof event?.phase === 'string' ? event.phase : '';
|
|
@@ -4131,6 +4148,11 @@ export function createCodexContextProxy({
|
|
|
4131
4148
|
const owned = ownedRequests.get(message.id);
|
|
4132
4149
|
ownedRequests.delete(message.id);
|
|
4133
4150
|
retire(message.id);
|
|
4151
|
+
if (owned.kind === 'engineering') {
|
|
4152
|
+
// An error leaves the session without the pack skills; it is never shown.
|
|
4153
|
+
releaseEngineering();
|
|
4154
|
+
return false;
|
|
4155
|
+
}
|
|
4134
4156
|
if (
|
|
4135
4157
|
owned.kind === 'read' ||
|
|
4136
4158
|
owned.kind === 'page' ||
|
|
@@ -4364,6 +4386,14 @@ export function createCodexContextProxy({
|
|
|
4364
4386
|
if (!validRequestId) return;
|
|
4365
4387
|
const requestKey = clientRequestKey(requestId);
|
|
4366
4388
|
if (requestKey === null) return;
|
|
4389
|
+
if (
|
|
4390
|
+
message.method === 'initialize' &&
|
|
4391
|
+
engineeringState === 'waiting' &&
|
|
4392
|
+
initializeRequestKeys.size < MAX_TRACKED_INITIALIZE_REQUESTS
|
|
4393
|
+
) {
|
|
4394
|
+
initializeRequestKeys.add(requestKey);
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4367
4397
|
if (['thread/start', 'thread/resume', 'thread/fork'].includes(message.method)) {
|
|
4368
4398
|
const threadId =
|
|
4369
4399
|
isRecord(message.params) && typeof message.params.threadId === 'string'
|
|
@@ -4428,7 +4458,7 @@ export function createCodexContextProxy({
|
|
|
4428
4458
|
let passthrough = false;
|
|
4429
4459
|
let discardOversized = false;
|
|
4430
4460
|
let pausedForBackpressure = false;
|
|
4431
|
-
const
|
|
4461
|
+
const writeOut = (text) => {
|
|
4432
4462
|
if (!stdout.write(text) && !pausedForBackpressure) {
|
|
4433
4463
|
pausedForBackpressure = true;
|
|
4434
4464
|
child.stdout.pause();
|
|
@@ -4438,6 +4468,54 @@ export function createCodexContextProxy({
|
|
|
4438
4468
|
});
|
|
4439
4469
|
}
|
|
4440
4470
|
};
|
|
4471
|
+
const forward = (text) => {
|
|
4472
|
+
if (engineeringHeld === null) {
|
|
4473
|
+
writeOut(text);
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
engineeringHeld.lines.push(text);
|
|
4477
|
+
engineeringHeld.bytes += Buffer.byteLength(text, 'utf8');
|
|
4478
|
+
if (engineeringHeld.bytes > MAX_LINE_BYTES) releaseEngineering();
|
|
4479
|
+
};
|
|
4480
|
+
/** The client's first successful initialize answer, which starts the hold. */
|
|
4481
|
+
function isEngineeringTrigger(message) {
|
|
4482
|
+
if (engineeringState !== 'waiting' || !isRecord(message)) return false;
|
|
4483
|
+
if (message.method !== undefined || !('result' in message)) return false;
|
|
4484
|
+
const key = clientRequestKey(message.id);
|
|
4485
|
+
return key !== null && initializeRequestKeys.has(key);
|
|
4486
|
+
}
|
|
4487
|
+
function holdForEngineering(text) {
|
|
4488
|
+
engineeringState = 'holding';
|
|
4489
|
+
initializeRequestKeys.clear();
|
|
4490
|
+
engineeringHeld = {
|
|
4491
|
+
lines: [text],
|
|
4492
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
4493
|
+
id: null,
|
|
4494
|
+
cancel: null,
|
|
4495
|
+
};
|
|
4496
|
+
try {
|
|
4497
|
+
engineeringHeld.id = sendOwned(
|
|
4498
|
+
'skills/extraRoots/set',
|
|
4499
|
+
{ extraRoots: [engineeringSkillsRoot] },
|
|
4500
|
+
'engineering',
|
|
4501
|
+
null,
|
|
4502
|
+
);
|
|
4503
|
+
} catch {
|
|
4504
|
+
releaseEngineering();
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
engineeringHeld.cancel = scheduleTimeout(() => releaseEngineering(), engineeringAckTimeoutMs);
|
|
4508
|
+
}
|
|
4509
|
+
/** Forward everything held, in order. Idempotent; the session never waits on the pack. */
|
|
4510
|
+
function releaseEngineering() {
|
|
4511
|
+
if (engineeringHeld === null) return;
|
|
4512
|
+
const held = engineeringHeld;
|
|
4513
|
+
engineeringHeld = null;
|
|
4514
|
+
engineeringState = 'done';
|
|
4515
|
+
held.cancel?.();
|
|
4516
|
+
if (held.id !== null && ownedRequests.has(held.id)) cleanupRequest(held.id);
|
|
4517
|
+
for (const text of held.lines) writeOut(text);
|
|
4518
|
+
}
|
|
4441
4519
|
const processLine = (line) => {
|
|
4442
4520
|
if (Buffer.byteLength(line, 'utf8') + 1 > MAX_LINE_BYTES) {
|
|
4443
4521
|
const ownedId = internalRequestId(line);
|
|
@@ -4451,6 +4529,10 @@ export function createCodexContextProxy({
|
|
|
4451
4529
|
try {
|
|
4452
4530
|
const message = JSON.parse(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
4453
4531
|
observeClientResponse(message);
|
|
4532
|
+
if (isEngineeringTrigger(message)) {
|
|
4533
|
+
holdForEngineering(`${line}\n`);
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4454
4536
|
forwardLine = handle(message);
|
|
4455
4537
|
} catch {
|
|
4456
4538
|
/* preserve malformed external protocol lines */
|
|
@@ -4536,6 +4618,7 @@ export function createCodexContextProxy({
|
|
|
4536
4618
|
}
|
|
4537
4619
|
});
|
|
4538
4620
|
child.stdout.on('end', () => {
|
|
4621
|
+
releaseEngineering();
|
|
4539
4622
|
const tail = inputBuffer + decoder.end();
|
|
4540
4623
|
if (tail && !discardOversized) forward(tail);
|
|
4541
4624
|
inputBuffer = '';
|
|
@@ -4565,6 +4648,7 @@ export function createCodexContextProxy({
|
|
|
4565
4648
|
observeClientRequest,
|
|
4566
4649
|
observeClientResponse,
|
|
4567
4650
|
async stop() {
|
|
4651
|
+
releaseEngineering();
|
|
4568
4652
|
if (stopped) {
|
|
4569
4653
|
await Promise.all(
|
|
4570
4654
|
[...diagnosticSinks.values()].map((sink) =>
|
|
@@ -4594,6 +4678,18 @@ export function createCodexContextProxy({
|
|
|
4594
4678
|
};
|
|
4595
4679
|
}
|
|
4596
4680
|
|
|
4681
|
+
/**
|
|
4682
|
+
* The launcher's AISAC Engineering skills root, or null. Only an absolute,
|
|
4683
|
+
* normalized path without control characters is used.
|
|
4684
|
+
*/
|
|
4685
|
+
export function engineeringSkillsRootFrom(value) {
|
|
4686
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
4687
|
+
if (value.length > MAX_ENGINEERING_PATH_LENGTH || /[\u0000-\u001f\u007f]/u.test(value))
|
|
4688
|
+
return null;
|
|
4689
|
+
if (!value.startsWith('/') || resolve(value) !== value) return null;
|
|
4690
|
+
return value;
|
|
4691
|
+
}
|
|
4692
|
+
|
|
4597
4693
|
async function main() {
|
|
4598
4694
|
if (process.argv.includes('--help')) {
|
|
4599
4695
|
process.stderr.write(
|
|
@@ -4648,6 +4744,7 @@ async function main() {
|
|
|
4648
4744
|
timeoutMs: numberEnv('CODEX_CONTEXT_COMPACT_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
|
|
4649
4745
|
policy,
|
|
4650
4746
|
createDiagnosticSink: diagnosticFactory,
|
|
4747
|
+
engineeringSkillsRoot: engineeringSkillsRootFrom(process.env[ENGINEERING_SKILLS_ROOT_ENV]),
|
|
4651
4748
|
});
|
|
4652
4749
|
const clientDecoder = new StringDecoder('utf8');
|
|
4653
4750
|
let clientInputBuffer = '';
|
|
@@ -16,7 +16,7 @@ export const MAX_LOG_FILES = 3;
|
|
|
16
16
|
const LOCK_WAIT_MS = 250;
|
|
17
17
|
const LOCK_STALE_MS = 30_000;
|
|
18
18
|
const ADMISSION_MS = 60 * 60 * 1000;
|
|
19
|
-
export const ADAPTER_VERSION = '0.4.0-beta.
|
|
19
|
+
export const ADAPTER_VERSION = '0.4.0-beta.10';
|
|
20
20
|
const MAX_DIAGNOSTIC_COUNT = 10_000_000;
|
|
21
21
|
const MAX_DIAGNOSTIC_BYTES = 128 * 1024 * 1024;
|
|
22
22
|
const CHECKPOINT_BACKENDS = new Set(['native_store', 'private_file']);
|
package/dist/cli.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from './state.mjs';
|
|
18
18
|
import {
|
|
19
19
|
captureLockPlatformCheck,
|
|
20
|
+
codexPlatformCheck,
|
|
21
|
+
defaultClientSelection,
|
|
20
22
|
nativeVersion,
|
|
21
23
|
platformCheck,
|
|
22
24
|
resolveNative,
|
|
@@ -29,8 +31,18 @@ import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.m
|
|
|
29
31
|
import { readDiagnostics } from './claude-observation.mjs';
|
|
30
32
|
import { readCodexDiagnostics } from './codex-diagnostics.mjs';
|
|
31
33
|
import { checkEngineeringLicense, licenseLine, unavailableLicense } from './engineering.mjs';
|
|
34
|
+
import {
|
|
35
|
+
isPackLocale,
|
|
36
|
+
readLastLaunch,
|
|
37
|
+
readStoredLocale,
|
|
38
|
+
resolvePackLocale,
|
|
39
|
+
writeStoredLocale,
|
|
40
|
+
} from './engineering-pack.mjs';
|
|
41
|
+
import { extensionsSummary, hooksCommand, mcpCommand } from './extensions-cli.mjs';
|
|
42
|
+
import { sessionServers } from './mcp-servers.mjs';
|
|
43
|
+
import { anyFormatter } from './guardrails/format.mjs';
|
|
32
44
|
|
|
33
|
-
const VERSION = '0.4.0-beta.
|
|
45
|
+
const VERSION = '0.4.0-beta.10';
|
|
34
46
|
// The command is `aisac`; `aibrain` is an alias installed from the same entry.
|
|
35
47
|
const COMMAND = 'aisac';
|
|
36
48
|
const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
|
|
@@ -48,7 +60,13 @@ function usage() {
|
|
|
48
60
|
'Claude shadow: setup --claude-shadow on|off and --claude-diagnostics on|off (preview stays applied=false).',
|
|
49
61
|
);
|
|
50
62
|
console.log(
|
|
51
|
-
'AISAC Engineering: setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
|
|
63
|
+
'AISAC Engineering: `aisac claude` and `aisac codex` (Codex 0.164+) load the licensed method; setup --lang pt-BR|en picks its language; setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
|
|
64
|
+
);
|
|
65
|
+
console.log(
|
|
66
|
+
'Guardrails: `aisac hooks` lists them (a checklist on a terminal); `aisac hooks enable|disable <id>...` and `aisac hooks reset` change the next `aisac claude` session.',
|
|
67
|
+
);
|
|
68
|
+
console.log(
|
|
69
|
+
'MCP catalog: `aisac mcp` lists the reviewed servers (GitHub, Supabase, Vercel, Playwright, Context7); `aisac mcp enable|disable <id>...`, `aisac mcp reset`, `aisac mcp info <id>` and `aisac mcp login <id>` (Codex) change or explain the next session.',
|
|
52
70
|
);
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -198,7 +216,7 @@ function selectedPolicy(args, previous) {
|
|
|
198
216
|
if (option(args, '--goal') === undefined && inherited.goal !== undefined)
|
|
199
217
|
overrides.goal = inherited.goal;
|
|
200
218
|
}
|
|
201
|
-
const clients = option(args, '--client') ??
|
|
219
|
+
const clients = option(args, '--client') ?? defaultClientSelection();
|
|
202
220
|
if (
|
|
203
221
|
clients === 'codex' &&
|
|
204
222
|
args.some(
|
|
@@ -310,6 +328,26 @@ async function updateLocalOptOut(previous, args) {
|
|
|
310
328
|
return true;
|
|
311
329
|
}
|
|
312
330
|
|
|
331
|
+
/** Store the AISAC Engineering pack language; local only, no network call. @param {'pt-BR' | 'en'} locale */
|
|
332
|
+
async function setPackLocale(locale) {
|
|
333
|
+
const root = await ensureStateRoot();
|
|
334
|
+
await writeStoredLocale(root, locale);
|
|
335
|
+
console.log(
|
|
336
|
+
`AISAC Engineering pack language set to ${locale} for future launches; no network call was made.`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Offline view of the pack language and the last launch (status, doctor). */
|
|
341
|
+
async function engineeringPackState() {
|
|
342
|
+
const root = stateRoot();
|
|
343
|
+
const stored = await readStoredLocale(root);
|
|
344
|
+
return {
|
|
345
|
+
locale: resolvePackLocale(stored),
|
|
346
|
+
localeSource: stored === null ? 'system' : 'setup',
|
|
347
|
+
lastLaunch: await readLastLaunch(root),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
313
351
|
/** @param {string} endpoint */
|
|
314
352
|
function validateEndpoint(endpoint) {
|
|
315
353
|
if (endpoint.length > 2048) throw new Error('endpoint_too_long');
|
|
@@ -385,6 +423,7 @@ async function setup(args) {
|
|
|
385
423
|
'--telemetry',
|
|
386
424
|
'--claude-shadow',
|
|
387
425
|
'--claude-diagnostics',
|
|
426
|
+
'--lang',
|
|
388
427
|
]),
|
|
389
428
|
new Set([
|
|
390
429
|
'--client',
|
|
@@ -396,10 +435,14 @@ async function setup(args) {
|
|
|
396
435
|
'--telemetry',
|
|
397
436
|
'--claude-shadow',
|
|
398
437
|
'--claude-diagnostics',
|
|
438
|
+
'--lang',
|
|
399
439
|
]),
|
|
400
440
|
);
|
|
401
441
|
if (args.includes('--undo') && args.length !== 1)
|
|
402
442
|
throw new Error('setup_undo_cannot_have_options');
|
|
443
|
+
const lang = option(args, '--lang');
|
|
444
|
+
if (lang !== undefined && !isPackLocale(lang)) throw new Error('invalid_lang');
|
|
445
|
+
if (lang !== undefined && args.length === 2) return setPackLocale(lang);
|
|
403
446
|
const previous = await readConfig();
|
|
404
447
|
if (await updateLocalOptOut(previous, args)) return;
|
|
405
448
|
const localOptOut =
|
|
@@ -408,8 +451,12 @@ async function setup(args) {
|
|
|
408
451
|
if (localOptOut) return disableTelemetryLocally(previous);
|
|
409
452
|
const platform = platformCheck();
|
|
410
453
|
if (!platform.ok) throw new Error(`${platform.code}:${platform.message}`);
|
|
411
|
-
const clients = option(args, '--client') ??
|
|
454
|
+
const clients = option(args, '--client') ?? defaultClientSelection();
|
|
412
455
|
if (!['claude', 'codex', 'both'].includes(clients)) throw new Error('invalid_client');
|
|
456
|
+
if (clients !== 'claude') {
|
|
457
|
+
const codexPlatform = codexPlatformCheck();
|
|
458
|
+
if (!codexPlatform.ok) throw new Error(`${codexPlatform.code}:${codexPlatform.message}`);
|
|
459
|
+
}
|
|
413
460
|
if (
|
|
414
461
|
clients === 'codex' &&
|
|
415
462
|
(option(args, '--claude-shadow') !== undefined ||
|
|
@@ -501,11 +548,27 @@ async function setup(args) {
|
|
|
501
548
|
console.log(
|
|
502
549
|
`Configured ${clients} in ${root}. Provider-free access verified; no provider batch was used.`,
|
|
503
550
|
);
|
|
551
|
+
if (lang !== undefined) await setPackLocale(lang);
|
|
504
552
|
// Display only: the server decides again on every pack request (ADR-0047).
|
|
505
553
|
const line = licenseLine(await checkEngineeringLicense(endpoint, token));
|
|
506
554
|
if (line !== null) console.log(line);
|
|
507
555
|
}
|
|
508
556
|
|
|
557
|
+
/** Extension state plus whether `format-on-edit` has a formatter in this folder (FR8). */
|
|
558
|
+
async function doctorExtensions() {
|
|
559
|
+
const summary = await extensionsSummary(stateRoot());
|
|
560
|
+
// A token server is skipped at launch when its variable is unset (names only).
|
|
561
|
+
const mcpMissingEnv = sessionServers(summary.mcp, 'claude', process.env).missingEnv.map(
|
|
562
|
+
(skipped) => skipped.id,
|
|
563
|
+
);
|
|
564
|
+
const formatOnEdit = !summary.hooks.includes('format-on-edit')
|
|
565
|
+
? 'off'
|
|
566
|
+
: (await anyFormatter(process.cwd()))
|
|
567
|
+
? 'ready'
|
|
568
|
+
: 'no_formatter_found';
|
|
569
|
+
return { ...summary, formatOnEdit, mcpMissingEnv };
|
|
570
|
+
}
|
|
571
|
+
|
|
509
572
|
/** @param {boolean} json */
|
|
510
573
|
async function doctor(json) {
|
|
511
574
|
const platform = platformCheck();
|
|
@@ -599,6 +662,8 @@ async function doctor(json) {
|
|
|
599
662
|
config?.claudeObservation?.disclosedAt !== undefined,
|
|
600
663
|
},
|
|
601
664
|
engineering,
|
|
665
|
+
engineeringPack: await engineeringPackState(),
|
|
666
|
+
extensions: await doctorExtensions(),
|
|
602
667
|
paidOrPilot: probe?.ok
|
|
603
668
|
? 'provider_free_probe_ok; paid entitlement or pilot invitation remains server-enforced'
|
|
604
669
|
: config
|
|
@@ -634,6 +699,8 @@ async function status(json) {
|
|
|
634
699
|
config?.claudeObservation?.disclosedAt !== null &&
|
|
635
700
|
config?.claudeObservation?.disclosedAt !== undefined,
|
|
636
701
|
},
|
|
702
|
+
engineeringPack: await engineeringPackState(),
|
|
703
|
+
extensions: await extensionsSummary(stateRoot()),
|
|
637
704
|
monitoredSession: null,
|
|
638
705
|
monitoringStatus: 'not_observed',
|
|
639
706
|
};
|
|
@@ -682,6 +749,8 @@ async function main(argv) {
|
|
|
682
749
|
else console.log(records);
|
|
683
750
|
return;
|
|
684
751
|
}
|
|
752
|
+
if (command === 'hooks') return hooksCommand(rest);
|
|
753
|
+
if (command === 'mcp') return mcpCommand(rest);
|
|
685
754
|
if (command === 'disable') {
|
|
686
755
|
if (rest.length > 0) throw new Error('disable_does_not_accept_options');
|
|
687
756
|
return removeOwnedSetup();
|
|
@@ -26,7 +26,14 @@ const PAGINATION_CONTRACT_VALID =
|
|
|
26
26
|
fingerprint.roots[PAGINATION_CONTRACT.response_schema]?.sha256 &&
|
|
27
27
|
typeof PAGINATION_CONTRACT.graph_sha256 === 'string' &&
|
|
28
28
|
PAGINATION_CONTRACT.graph_sha256.length === 64);
|
|
29
|
-
const QUALIFIED_VERSIONS = new Set([
|
|
29
|
+
const QUALIFIED_VERSIONS = new Set([
|
|
30
|
+
'0.161.0',
|
|
31
|
+
'0.162.0',
|
|
32
|
+
'0.162.1',
|
|
33
|
+
'0.163.0',
|
|
34
|
+
'0.164.0',
|
|
35
|
+
'0.166.0',
|
|
36
|
+
]);
|
|
30
37
|
const ANNOTATIONS = new Set([
|
|
31
38
|
'$comment',
|
|
32
39
|
'title',
|
|
@@ -475,7 +482,50 @@ export async function resolveCodexCompatibility(executable, baseEnvironment = pr
|
|
|
475
482
|
}
|
|
476
483
|
}
|
|
477
484
|
|
|
485
|
+
/** @param {string} left @param {string} right */
|
|
486
|
+
function compareStableVersions(left, right) {
|
|
487
|
+
const a = left.split('.').map(Number);
|
|
488
|
+
const b = right.split('.').map(Number);
|
|
489
|
+
for (let index = 0; index < 3; index += 1) {
|
|
490
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
491
|
+
if (difference !== 0) return difference;
|
|
492
|
+
}
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The newest Codex release this client was qualified against. */
|
|
497
|
+
export const LATEST_QUALIFIED_CODEX = /** @type {string} */ (
|
|
498
|
+
[...QUALIFIED_VERSIONS].sort(compareStableVersions).at(-1)
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Rejections a user fixes by installing a qualified Codex (or a newer aisac):
|
|
502
|
+
// the installed release's contract or transport differs from the bundled one.
|
|
503
|
+
const CONTRACT_REJECTIONS = new Set([
|
|
504
|
+
'protocol_schema_mismatch',
|
|
505
|
+
'protocol_method_missing',
|
|
506
|
+
'schema_root_missing_or_invalid',
|
|
507
|
+
'schema_set_too_large',
|
|
508
|
+
'schema_generation_failed',
|
|
509
|
+
'transport_flags_missing',
|
|
510
|
+
'protocol_probe_failed',
|
|
511
|
+
]);
|
|
512
|
+
|
|
513
|
+
/** @param {{reason:string, version:string|null}} decision @returns {string|null} */
|
|
514
|
+
export function codexCompatibilityRemedy(decision) {
|
|
515
|
+
const detected = decision.version === null ? 'The installed Codex' : `Codex ${decision.version}`;
|
|
516
|
+
const reinstall = `npm install -g @openai/codex@${LATEST_QUALIFIED_CODEX}`;
|
|
517
|
+
if (CONTRACT_REJECTIONS.has(decision.reason))
|
|
518
|
+
return `${detected} is not qualified by this aisac (newest qualified: ${LATEST_QUALIFIED_CODEX}). Install the qualified Codex with \`${reinstall}\`, or check for an aisac release that qualifies it with \`npm install -g @aisac/cli@beta\`.`;
|
|
519
|
+
if (decision.reason === 'version_unqualified')
|
|
520
|
+
return `${detected} is older than this aisac supports. Update it with \`${reinstall}\`.`;
|
|
521
|
+
if (decision.reason === 'version_not_stable_0x')
|
|
522
|
+
return `${detected} is not a stable 0.x release. Install a qualified one with \`${reinstall}\`.`;
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
478
526
|
/** @param {{reason:string, version:string|null}} decision @returns {string} */
|
|
479
527
|
export function codexCompatibilityError(decision) {
|
|
480
|
-
|
|
528
|
+
const coded = `codex_protocol_incompatible:${decision.reason}:detected=${decision.version ?? 'unavailable'};run aisac doctor --json`;
|
|
529
|
+
const remedy = codexCompatibilityRemedy(decision);
|
|
530
|
+
return remedy === null ? coded : `${coded}\n${remedy}`;
|
|
481
531
|
}
|