@brunobracaioli/aibrain 0.3.0-beta.18 → 0.4.0-beta.1

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 CHANGED
@@ -1,6 +1,18 @@
1
- # aibrain
1
+ # aisac
2
2
 
3
- Assisted AI Brain context beta client for Linux/WSL with Node.js 22 or newer.
3
+ The AISAC client for Claude Code and Codex on Linux/WSL with Node.js 22 or newer:
4
+ AI Brain memory, context management and, for AISAC Founding Cohort members,
5
+ AISAC Engineering.
6
+
7
+ The command is `aisac`. `aibrain` is an alias for the same program, so existing
8
+ scripts and habits keep working. The same files are published as `@aisac/cli`
9
+ and, for existing installs, `@brunobracaioli/aibrain`. Install only one of them:
10
+ both provide the `aisac` and `aibrain` commands. State stays in `~/.aibrain`.
11
+
12
+ `aisac setup` and `aisac doctor` report the AISAC Engineering license
13
+ (`active`, `inactive` or `unavailable`). The server decides; the client only
14
+ shows the answer and sends the request only to the configured AI Brain address.
15
+ `aisac status` stays offline.
4
16
 
5
17
  Codex capture also requires the `flock` utility from `util-linux`, normally
6
18
  included in Linux/WSL. The launcher and doctor check availability before a
@@ -34,13 +46,17 @@ connection. Its failure or timeout closes only that auxiliary connection.
34
46
 
35
47
  ```sh
36
48
  # Install the published beta:
37
- npm install -g @brunobracaioli/aibrain@beta
38
- aibrain setup --client both
39
- aibrain doctor --json
40
- aibrain claude
41
- aibrain codex
49
+ npm install -g @aisac/cli@beta
50
+ aisac setup --client both
51
+ aisac doctor --json
52
+ aisac claude
53
+ aisac codex
42
54
  ```
43
55
 
56
+ To move from `@brunobracaioli/aibrain` to `@aisac/cli`, close running sessions,
57
+ then run `npm uninstall -g @brunobracaioli/aibrain` before
58
+ `npm install -g @aisac/cli@beta`. Setup, notes and checkpoints are kept.
59
+
44
60
  Installation does not contact AI Brain, change native client configuration, or
45
61
  enable all paid accounts. Setup requires an existing AI Brain PAT, explicit
46
62
  versioned disclosure, and successful provider-free pilot access. Claude and
@@ -53,10 +69,10 @@ explicitly acknowledge the displayed disclosure with
53
69
  `aibrain setup --client both --token-file /private/path/brain.pat
54
70
  --accept-disclosure`.
55
71
 
56
- For a CI-reviewed tarball, use
57
- `brunobracaioli-aibrain-*.tgz` with
58
- `npm install -g ./brunobracaioli-aibrain-*.tgz`; registry installation is gated on
59
- the operator's npm review and authentication.
72
+ For a CI-reviewed tarball, use `aisac-cli-*.tgz` with
73
+ `npm install -g ./aisac-cli-*.tgz` (the same files are also packed as
74
+ `brunobracaioli-aibrain-*.tgz`); registry installation is gated on the
75
+ operator's npm review and authentication.
60
76
 
61
77
  For users of an earlier unscoped review tarball, close its client sessions,
62
78
  run `npm uninstall -g aibrain`, and install this scoped package. Local
@@ -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.3.0-beta.18';
154
+ const CLAUDE_HOOK_VERSION = '0.4.0-beta.1';
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;
@@ -494,7 +494,7 @@ export async function resolveClaudeCompatibility(executable, packageRoot) {
494
494
  /** @param {ClaudeCompatibility} decision */
495
495
  export function claudeCompatibilityError(decision) {
496
496
  const detected = decision.version ?? 'unavailable';
497
- return `claude_version_unverified: Claude Code detected version ${detected}; reason ${decision.reason}; run aibrain doctor --json or install a compatible stable native version.`;
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
498
  }
499
499
 
500
500
  export const claudeCompatibilityInternals = Object.freeze({
@@ -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.3.0-beta.18';
19
+ export const ADAPTER_VERSION = '0.4.0-beta.1';
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
@@ -28,15 +28,18 @@ import { providerFreeProbe } from './mcp.mjs';
28
28
  import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.mjs';
29
29
  import { readDiagnostics } from './claude-observation.mjs';
30
30
  import { readCodexDiagnostics } from './codex-diagnostics.mjs';
31
+ import { checkEngineeringLicense, licenseLine, unavailableLicense } from './engineering.mjs';
31
32
 
32
- const VERSION = '0.3.0-beta.18';
33
+ const VERSION = '0.4.0-beta.1';
34
+ // The command is `aisac`; `aibrain` is an alias installed from the same entry.
35
+ const COMMAND = 'aisac';
33
36
  const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
34
37
  const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url));
35
38
  const LEGACY_DISCLOSURE_VERSION = 'aibrain-paid-context-v1-2026-10-09';
36
39
 
37
40
  function usage() {
38
41
  console.log(
39
- `aibrain ${VERSION}\n\nUsage:\n aibrain setup [--client claude|codex|both] [--context-profile upstream|conservative]\n [--compact-at 1..95] [--keep-threshold 0..1] [--preserve-recent 1..100]\n [--min-reduction 0..1] [--truncate-head-chars 0..4096]\n [--max-state-tokens 1000..25000] [--max-request-tokens 1000..30000]\n [--tool-policy compatible|read_only] [--goal TEXT]\n aibrain doctor [--json]\n aibrain status [--json]\n aibrain disable\n aibrain setup --undo\n aibrain claude [-- native-args...]\n aibrain codex [-- native-args...]\n\nPolicy notes:\n Automatic monitoring asks for compaction at compact-at percent; state/request\n limits are estimated upper bounds and do not override server quotas.\n min-reduction, truncate-head-chars and tool-policy apply only to Claude;\n Codex keeps native compaction and same-session recovery.`,
42
+ `${COMMAND} ${VERSION}\n\nUsage:\n ${COMMAND} setup [--client claude|codex|both] [--context-profile upstream|conservative]\n [--compact-at 1..95] [--keep-threshold 0..1] [--preserve-recent 1..100]\n [--min-reduction 0..1] [--truncate-head-chars 0..4096]\n [--max-state-tokens 1000..25000] [--max-request-tokens 1000..30000]\n [--tool-policy compatible|read_only] [--goal TEXT]\n ${COMMAND} doctor [--json]\n ${COMMAND} status [--json]\n ${COMMAND} disable\n ${COMMAND} setup --undo\n ${COMMAND} claude [-- native-args...]\n ${COMMAND} codex [-- native-args...]\n\nPolicy notes:\n Automatic monitoring asks for compaction at compact-at percent; state/request\n limits are estimated upper bounds and do not override server quotas.\n min-reduction, truncate-head-chars and tool-policy apply only to Claude;\n Codex keeps native compaction and same-session recovery.`,
40
43
  );
41
44
  console.log(
42
45
  'Telemetry: setup --telemetry on|off (omitted preserves the stored choice; fresh defaults off).',
@@ -44,11 +47,14 @@ function usage() {
44
47
  console.log(
45
48
  'Claude shadow: setup --claude-shadow on|off and --claude-diagnostics on|off (preview stays applied=false).',
46
49
  );
50
+ console.log(
51
+ 'AISAC Engineering: setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
52
+ );
47
53
  }
48
54
 
49
55
  // Telemetry is opt-in; setup omits the flag to preserve the stored choice.
50
56
  function version() {
51
- console.log(`aibrain ${VERSION}`);
57
+ console.log(`${COMMAND} ${VERSION}`);
52
58
  }
53
59
 
54
60
  async function maskedPrompt() {
@@ -495,6 +501,9 @@ async function setup(args) {
495
501
  console.log(
496
502
  `Configured ${clients} in ${root}. Provider-free access verified; no provider batch was used.`,
497
503
  );
504
+ // Display only: the server decides again on every pack request (ADR-0047).
505
+ const line = licenseLine(await checkEngineeringLicense(endpoint, token));
506
+ if (line !== null) console.log(line);
498
507
  }
499
508
 
500
509
  /** @param {boolean} json */
@@ -534,6 +543,10 @@ async function doctor(json) {
534
543
  ...rawNativeVersions,
535
544
  codex: codexCompatibility?.version ?? rawNativeVersions.codex,
536
545
  };
546
+ // Optional capability: its state never changes doctor's exit code.
547
+ const engineering = config
548
+ ? await checkEngineeringLicense(config.endpoint, config.token)
549
+ : unavailableLicense('not_configured');
537
550
  const needsClaude = config?.clientSelection === 'claude' || config?.clientSelection === 'both';
538
551
  const needsCodex = config?.clientSelection === 'codex' || config?.clientSelection === 'both';
539
552
  const captureLockPlatform = needsCodex ? captureLockPlatformCheck() : null;
@@ -585,6 +598,7 @@ async function doctor(json) {
585
598
  config?.claudeObservation?.disclosedAt !== null &&
586
599
  config?.claudeObservation?.disclosedAt !== undefined,
587
600
  },
601
+ engineering,
588
602
  paidOrPilot: probe?.ok
589
603
  ? 'provider_free_probe_ok; paid entitlement or pilot invitation remains server-enforced'
590
604
  : config
@@ -716,7 +730,7 @@ main(cliArgs).catch((error) => {
716
730
  }),
717
731
  );
718
732
  } else {
719
- console.error(`aibrain: ${errorMessage(error)}`);
733
+ console.error(`${COMMAND}: ${errorMessage(error)}`);
720
734
  }
721
735
  process.exitCode = 1;
722
736
  });
@@ -477,5 +477,5 @@ export async function resolveCodexCompatibility(executable, baseEnvironment = pr
477
477
 
478
478
  /** @param {{reason:string, version:string|null}} decision @returns {string} */
479
479
  export function codexCompatibilityError(decision) {
480
- return `codex_protocol_incompatible:${decision.reason}:detected=${decision.version ?? 'unavailable'};run aibrain doctor --json`;
480
+ return `codex_protocol_incompatible:${decision.reason}:detected=${decision.version ?? 'unavailable'};run aisac doctor --json`;
481
481
  }
@@ -0,0 +1,190 @@
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
+ function isRecord(value) {
51
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
52
+ }
53
+
54
+ /** @param {unknown} value */
55
+ 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
+ /** @param {Response} response */
101
+ async function boundedText(response) {
102
+ const declared = Number(response.headers.get('content-length') ?? '0');
103
+ if (declared > MAX_LICENSE_BODY_BYTES) throw new Error('license_body_too_large');
104
+ if (response.body === null) return '';
105
+ const reader = response.body.getReader();
106
+ /** @type {Uint8Array[]} */
107
+ const chunks = [];
108
+ let size = 0;
109
+ for (;;) {
110
+ const { done, value } = await reader.read();
111
+ if (done) break;
112
+ size += value.byteLength;
113
+ if (size > MAX_LICENSE_BODY_BYTES) {
114
+ await reader.cancel().catch(() => undefined);
115
+ throw new Error('license_body_too_large');
116
+ }
117
+ chunks.push(value);
118
+ }
119
+ return Buffer.concat(chunks).toString('utf8');
120
+ }
121
+
122
+ /** @param {unknown} error */
123
+ function isTimeout(error) {
124
+ return error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError');
125
+ }
126
+
127
+ /** @param {number} status */
128
+ function unavailableReasonFor(status) {
129
+ return (
130
+ UNAVAILABLE_BY_STATUS.get(status) ??
131
+ (status >= SERVER_ERROR_STATUS ? 'server_error' : 'unexpected_status')
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Ask the server whether this account holds the AISAC Engineering license.
137
+ * @param {string} endpoint configured AI Brain MCP endpoint (already validated at setup)
138
+ * @param {string} token account PAT
139
+ * @param {{ fetchImpl?: typeof fetch, timeoutMs?: number }} [options]
140
+ * @returns {Promise<EngineeringLicense>}
141
+ */
142
+ export async function checkEngineeringLicense(endpoint, token, options = {}) {
143
+ const { fetchImpl = fetch, timeoutMs = LICENSE_TIMEOUT_MS } = options;
144
+ let url;
145
+ try {
146
+ url = engineeringLicenseUrl(endpoint);
147
+ } catch {
148
+ return unavailableLicense('invalid_endpoint');
149
+ }
150
+ const signal = AbortSignal.timeout(timeoutMs);
151
+ let response;
152
+ try {
153
+ response = await fetchImpl(url, {
154
+ method: 'GET',
155
+ redirect: 'error',
156
+ signal,
157
+ headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
158
+ });
159
+ } catch (error) {
160
+ return unavailableLicense(isTimeout(error) ? 'timeout' : 'unreachable');
161
+ }
162
+ if (response.status !== 200) {
163
+ await response.body?.cancel().catch(() => undefined);
164
+ return unavailableLicense(unavailableReasonFor(response.status));
165
+ }
166
+ let text;
167
+ try {
168
+ text = await boundedText(response);
169
+ } catch (error) {
170
+ return unavailableLicense(isTimeout(error) ? 'timeout' : 'invalid_response');
171
+ }
172
+ return parseLicenseBody(text) ?? unavailableLicense('invalid_response');
173
+ }
174
+
175
+ /**
176
+ * One line for `aisac setup`. Returns null while the server does not offer the
177
+ * capability yet, so memory-only users see no new noise.
178
+ * @param {EngineeringLicense} license
179
+ * @returns {string | null}
180
+ */
181
+ export function licenseLine(license) {
182
+ if (license.state === 'active') {
183
+ const until = license.validUntil === null ? '' : ` until ${license.validUntil.slice(0, 10)}`;
184
+ return `AISAC Engineering: active${until}.`;
185
+ }
186
+ if (license.state === 'inactive')
187
+ return `AISAC Engineering: inactive (${license.reason}) — ${license.purchaseUrl ?? ''}`.trimEnd();
188
+ if (license.reason === 'not_offered') return null;
189
+ return `AISAC Engineering: unavailable (${license.reason}); memory and context are unaffected.`;
190
+ }
package/dist/launch.mjs CHANGED
@@ -413,7 +413,7 @@ export async function launchClaude(config, root, args) {
413
413
  const pinnedExecutable = compatibility.executable ?? executable;
414
414
  if (compatibility.status === 'host_checked')
415
415
  process.stderr.write(
416
- `aibrain: Claude ${compatibility.version ?? 'unknown'} accepted by the local native host contract; end-to-end recovery remains separately qualified.\n`,
416
+ `aisac: Claude ${compatibility.version ?? 'unknown'} accepted by the local native host contract; end-to-end recovery remains separately qualified.\n`,
417
417
  );
418
418
  const currentVersion = compatibility.version ?? nativeVersion(pinnedExecutable);
419
419
  const pluginRoot = await assertClaudeAssets(validated.packageRoot);
@@ -575,7 +575,7 @@ export async function launchCodex(config, root, args) {
575
575
  if (compatibility.version === null) throw new Error('codex_version_unverified');
576
576
  if (compatibility.status === 'protocol_match')
577
577
  process.stderr.write(
578
- `aibrain: Codex ${compatibility.version} matched the local protocol contract; native behavioral qualification is not established.\n`,
578
+ `aisac: Codex ${compatibility.version} matched the local protocol contract; native behavioral qualification is not established.\n`,
579
579
  );
580
580
  const launchPlan = await parseCodexLaunchArgs(args, process.cwd());
581
581
  const packageRoot = resolve(validated.packageRoot);
package/dist/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.3.0-beta.18",
3
+ "version": "0.4.0-beta.1",
4
4
  "type": "module"
5
5
  }
package/dist/platform.mjs CHANGED
@@ -116,7 +116,7 @@ export function nativeVersionDiagnostic(client, version) {
116
116
  : 'unavailable';
117
117
  const label = client === 'claude' ? 'Claude Code' : 'Codex CLI';
118
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 aibrain doctor --json or install a qualified stable native version.`;
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.`;
120
120
  }
121
121
 
122
122
  /** @param {'claude'|'codex'} client @param {string} version */
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.3.0-beta.18",
3
+ "version": "0.4.0-beta.1",
4
4
  "private": false,
5
- "description": "AI Brain assisted context beta client for Claude Code and Codex",
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": [