@brunobracaioli/aibrain 0.2.0-beta.1 → 0.3.0-beta.13

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/dist/cli.mjs CHANGED
@@ -15,12 +15,21 @@ import {
15
15
  stateRoot,
16
16
  tokenDigest,
17
17
  } from './state.mjs';
18
- import { isTestedNativeVersion, nativeVersion, platformCheck, resolveNative } from './platform.mjs';
18
+ import {
19
+ isTestedNativeVersion,
20
+ nativeVersion,
21
+ nativeVersionDiagnostic,
22
+ platformCheck,
23
+ resolveNative,
24
+ } from './platform.mjs';
19
25
  import { launchClaude, launchCodex } from './launch.mjs';
26
+ import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
20
27
  import { providerFreeProbe } from './mcp.mjs';
21
28
  import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.mjs';
29
+ import { readDiagnostics } from './claude-observation.mjs';
30
+ import { readCodexDiagnostics } from './codex-diagnostics.mjs';
22
31
 
23
- const VERSION = '0.2.0-beta.1';
32
+ const VERSION = '0.3.0-beta.13';
24
33
  const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
25
34
  const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url));
26
35
  const LEGACY_DISCLOSURE_VERSION = 'aibrain-paid-context-v1-2026-10-09';
@@ -29,8 +38,15 @@ function usage() {
29
38
  console.log(
30
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.`,
31
40
  );
41
+ console.log(
42
+ 'Telemetry: setup --telemetry on|off (omitted preserves the stored choice; fresh defaults off).',
43
+ );
44
+ console.log(
45
+ 'Claude shadow: setup --claude-shadow on|off and --claude-diagnostics on|off (preview stays applied=false).',
46
+ );
32
47
  }
33
48
 
49
+ // Telemetry is opt-in; setup omits the flag to preserve the stored choice.
34
50
  function version() {
35
51
  console.log(`aibrain ${VERSION}`);
36
52
  }
@@ -141,7 +157,13 @@ function selectedPolicy(args, previous) {
141
157
  const explicitProfile = option(args, '--context-profile');
142
158
  const explicitV2 =
143
159
  explicitProfile !== undefined ||
144
- args.some((item) => POLICY_OPTION_NAMES.has(item) || item === '--goal');
160
+ args.some(
161
+ (item) =>
162
+ POLICY_OPTION_NAMES.has(item) ||
163
+ item === '--goal' ||
164
+ item === '--claude-shadow' ||
165
+ item === '--claude-diagnostics',
166
+ );
145
167
  if (previous?.schemaVersion === 1 && !explicitV2)
146
168
  return { v2: false, policy: makePolicy('conservative') };
147
169
  const inherited = previous?.schemaVersion === 2 ? previous.context : undefined;
@@ -188,6 +210,100 @@ function policySummary(policy) {
188
210
  return JSON.stringify({ effective: publicPolicy(policy) });
189
211
  }
190
212
 
213
+ /** @param {string[]} args @param {Awaited<ReturnType<typeof readConfig>>} previous */
214
+ function telemetrySelection(args, previous) {
215
+ const requested = option(args, '--telemetry');
216
+ if (requested !== undefined && requested !== 'on' && requested !== 'off')
217
+ throw new Error('invalid_telemetry');
218
+ const current = previous?.telemetry;
219
+ const enabled = requested === undefined ? current?.enabled === true : requested === 'on';
220
+ return {
221
+ enabled,
222
+ disclosedAt: current?.disclosedAt ?? null,
223
+ };
224
+ }
225
+
226
+ /** @param {string[]} args @param {Awaited<ReturnType<typeof readConfig>>} previous */
227
+ function claudeObservationSelection(args, previous) {
228
+ const shadow = option(args, '--claude-shadow');
229
+ const diagnostics = option(args, '--claude-diagnostics');
230
+ for (const value of [shadow, diagnostics]) {
231
+ if (value !== undefined && value !== 'on' && value !== 'off')
232
+ throw new Error('invalid_claude_observation');
233
+ }
234
+ const current = previous?.claudeObservation;
235
+ const shadowEnabled = shadow === undefined ? current?.shadowEnabled === true : shadow === 'on';
236
+ const diagnosticsEnabled =
237
+ diagnostics === undefined ? current?.diagnosticsEnabled === true : diagnostics === 'on';
238
+ const enabled = shadowEnabled || diagnosticsEnabled;
239
+ return {
240
+ shadowEnabled,
241
+ diagnosticsEnabled,
242
+ disclosedAt: enabled
243
+ ? (current?.disclosedAt ?? new Date().toISOString())
244
+ : (current?.disclosedAt ?? null),
245
+ };
246
+ }
247
+
248
+ /** @param {Awaited<ReturnType<typeof readConfig>>} previous Disable future-launch egress locally without probing the server or requiring native clients. */
249
+ async function disableTelemetryLocally(previous) {
250
+ if (!previous) {
251
+ console.log('Context dashboard telemetry is already disabled; no local setup exists.');
252
+ return;
253
+ }
254
+ const root = await ensureStateRoot();
255
+ await atomicJson(`${root}/config.json`, {
256
+ ...previous,
257
+ telemetry: {
258
+ enabled: false,
259
+ disclosedAt: previous.telemetry?.disclosedAt ?? null,
260
+ },
261
+ });
262
+ console.log(
263
+ `Context dashboard telemetry disabled for future launches in ${root}. Close and restart currently monitored sessions to stop their launch-captured reporting settings.`,
264
+ );
265
+ }
266
+
267
+ /** @param {Awaited<ReturnType<typeof readConfig>>} previous @param {string[]} args */
268
+ async function updateLocalOptOut(previous, args) {
269
+ const allowed = new Set([
270
+ '--accept-disclosure',
271
+ '--telemetry',
272
+ 'off',
273
+ '--claude-shadow',
274
+ '--claude-diagnostics',
275
+ ]);
276
+ const hasOptOut =
277
+ args.includes('--telemetry') ||
278
+ args.includes('--claude-shadow') ||
279
+ args.includes('--claude-diagnostics');
280
+ if (!hasOptOut || args.some((argument) => !allowed.has(argument))) return false;
281
+ const requestedOff =
282
+ option(args, '--telemetry') === 'off' ||
283
+ option(args, '--claude-shadow') === 'off' ||
284
+ option(args, '--claude-diagnostics') === 'off';
285
+ if (!requestedOff) return false;
286
+ if (!previous) {
287
+ console.log(
288
+ 'AI Brain local opt-out already has no configured account; no provider call was made.',
289
+ );
290
+ return true;
291
+ }
292
+ const root = await ensureStateRoot();
293
+ const observation = claudeObservationSelection(args, previous);
294
+ const telemetry = telemetrySelection(args, previous);
295
+ await atomicJson(`${root}/config.json`, {
296
+ ...previous,
297
+ telemetry: {
298
+ enabled: telemetry.enabled,
299
+ disclosedAt: telemetry.disclosedAt,
300
+ },
301
+ claudeObservation: observation,
302
+ });
303
+ console.log(`Local observation settings updated in ${root}; no provider call was made.`);
304
+ return true;
305
+ }
306
+
191
307
  /** @param {string} endpoint */
192
308
  function validateEndpoint(endpoint) {
193
309
  if (endpoint.length > 2048) throw new Error('endpoint_too_long');
@@ -209,9 +325,28 @@ function validateEndpoint(endpoint) {
209
325
  throw new Error('endpoint_credentials_query_or_fragment_rejected');
210
326
  }
211
327
 
212
- /** @param {string[]} args @param {string} endpoint @param {import('./policy.mjs').ContextPolicy} policy @param {string} disclosureVersion */
213
- async function disclosureAccepted(args, endpoint, policy, disclosureVersion) {
214
- const disclosure = `AI Brain sends selected task/message/tool-input projections through AI Brain to JEV/TypeSafe at https://api.typesafe.ai/v1/systemone. Retention profile: ${policy.profile}; compact at ${policy.compactAtPercent}%; keep threshold ${policy.keepThreshold}; recent messages ${policy.preserveRecentMessages}; Claude tool policy ${policy.toolPolicy}. Selected projections are bounded and filtered; private local recovery files remain on this machine. Hidden fields and redaction limits apply; no zero-retention or complete-redaction claim. Goal present: ${policy.goal !== undefined}. Endpoint: ${endpoint}. Disclosure: ${disclosureVersion}.`;
328
+ /** @param {string[]} args @param {string} endpoint @param {import('./policy.mjs').ContextPolicy} policy @param {string} disclosureVersion @param {boolean} telemetryEnabled @param {boolean} shadowEnabled @param {boolean} diagnosticsEnabled */
329
+ async function disclosureAccepted(
330
+ args,
331
+ endpoint,
332
+ policy,
333
+ disclosureVersion,
334
+ telemetryEnabled,
335
+ shadowEnabled,
336
+ diagnosticsEnabled,
337
+ ) {
338
+ const telemetryDisclosure = telemetryEnabled
339
+ ? ' Operational telemetry is enabled: only a hashed native session key, random lifecycle identifiers, client version, phase, timestamps, numeric counts and allowed measurement/reason enums are sent for the authenticated Contexto dashboard. Prompts, notes, paths, tool names, credentials and raw errors are excluded; the dashboard shows a 30-day visible window and physical cleanup runs on the next successful daily cleanup; delivery failure does not affect compaction.'
340
+ : '';
341
+ const shadowDisclosure =
342
+ shadowEnabled || option(args, '--claude-shadow') === 'on'
343
+ ? ' Claude shadow observation is enabled: bounded redacted task and tool metadata may be sent through AI Brain to the existing JEV selection service for hypothetical decisions; native Claude results remain unchanged and private diagnostics exclude prompts, paths, inputs, result bodies, credentials and raw errors.'
344
+ : '';
345
+ const diagnosticsDisclosure =
346
+ diagnosticsEnabled || option(args, '--claude-diagnostics') === 'on'
347
+ ? ' Claude compact diagnostics are enabled locally: bounded stages, enums, pseudonymous identifiers, timestamps and numeric measurements are stored in private rotated logs; prompts, paths, inputs, result bodies, credentials and raw errors are excluded.'
348
+ : '';
349
+ const disclosure = `AI Brain sends selected task/message/tool-input projections through AI Brain to JEV/TypeSafe at https://api.typesafe.ai/v1/systemone. Retention profile: ${policy.profile}; compact at ${policy.compactAtPercent}%; keep threshold ${policy.keepThreshold}; recent messages ${policy.preserveRecentMessages}; Claude tool policy ${policy.toolPolicy}. Selected projections are bounded and filtered; private local recovery files remain on this machine. Hidden fields and redaction limits apply; no zero-retention or complete-redaction claim. Goal present: ${policy.goal !== undefined}.${telemetryDisclosure}${shadowDisclosure}${diagnosticsDisclosure} Endpoint: ${endpoint}. Disclosure: ${disclosureVersion}.`;
215
350
  if (args.includes('--accept-disclosure')) {
216
351
  console.log(disclosure);
217
352
  return true;
@@ -241,6 +376,9 @@ async function setup(args) {
241
376
  '--context-profile',
242
377
  ...POLICY_OPTION_NAMES,
243
378
  '--goal',
379
+ '--telemetry',
380
+ '--claude-shadow',
381
+ '--claude-diagnostics',
244
382
  ]),
245
383
  new Set([
246
384
  '--client',
@@ -249,25 +387,51 @@ async function setup(args) {
249
387
  '--context-profile',
250
388
  ...POLICY_OPTION_NAMES,
251
389
  '--goal',
390
+ '--telemetry',
391
+ '--claude-shadow',
392
+ '--claude-diagnostics',
252
393
  ]),
253
394
  );
254
395
  if (args.includes('--undo') && args.length !== 1)
255
396
  throw new Error('setup_undo_cannot_have_options');
256
397
  const previous = await readConfig();
398
+ if (await updateLocalOptOut(previous, args)) return;
399
+ const localOptOut =
400
+ option(args, '--telemetry') === 'off' &&
401
+ args.every((argument) => ['--telemetry', 'off', '--accept-disclosure'].includes(argument));
402
+ if (localOptOut) return disableTelemetryLocally(previous);
257
403
  const platform = platformCheck();
258
404
  if (!platform.ok) throw new Error(`${platform.code}:${platform.message}`);
259
405
  const clients = option(args, '--client') ?? 'both';
260
406
  if (!['claude', 'codex', 'both'].includes(clients)) throw new Error('invalid_client');
407
+ if (
408
+ clients === 'codex' &&
409
+ (option(args, '--claude-shadow') !== undefined ||
410
+ option(args, '--claude-diagnostics') !== undefined)
411
+ )
412
+ throw new Error('claude_observation_requires_claude_client');
261
413
  const endpoint = option(args, '--endpoint') ?? ENDPOINT;
262
414
  validateEndpoint(endpoint);
263
415
  const selection = selectedPolicy(args, previous);
416
+ const telemetry = telemetrySelection(args, previous);
417
+ const claudeObservation = claudeObservationSelection(args, previous);
264
418
  const policy = selection.policy;
265
419
  const disclosureVersion = selection.v2
266
420
  ? DISCLOSURE_VERSION
267
421
  : (previous?.disclosureVersion ?? LEGACY_DISCLOSURE_VERSION);
268
422
  if (selection.v2 && (clients === 'codex' || clients === 'both')) codexPolicyJson(policy);
269
423
  console.log(`Effective context policy ${policySummary(policy)}`);
270
- if (!(await disclosureAccepted(args, endpoint, policy, disclosureVersion)))
424
+ if (
425
+ !(await disclosureAccepted(
426
+ args,
427
+ endpoint,
428
+ policy,
429
+ disclosureVersion,
430
+ telemetry.enabled,
431
+ claudeObservation.shadowEnabled,
432
+ claudeObservation.diagnosticsEnabled,
433
+ ))
434
+ )
271
435
  throw new Error('disclosure_not_accepted');
272
436
  const root = await ensureStateRoot();
273
437
  const tokenPath = option(args, '--token-file');
@@ -281,13 +445,16 @@ async function setup(args) {
281
445
  if ((clients === 'codex' || clients === 'both') && (!codex || !node))
282
446
  throw new Error('codex_or_node_not_found');
283
447
  const claudeVersion = claude ? nativeVersion(claude) : null;
284
- const codexVersion = codex ? nativeVersion(codex) : null;
285
448
  const claudeSupported = claudeVersion !== null && isTestedNativeVersion('claude', claudeVersion);
286
- const codexSupported = codexVersion !== null && isTestedNativeVersion('codex', codexVersion);
287
449
  if ((clients === 'claude' || clients === 'both') && !claudeSupported)
288
- throw new Error('claude_version_unverified');
289
- if ((clients === 'codex' || clients === 'both') && !codexSupported)
290
- throw new Error('codex_version_unverified');
450
+ throw new Error(nativeVersionDiagnostic('claude', claudeVersion ?? ''));
451
+ let codexCompatibility = null;
452
+ if (clients === 'codex' || clients === 'both') {
453
+ codexCompatibility = codex ? await resolveCodexCompatibility(codex) : null;
454
+ if (codexCompatibility?.status === 'rejected')
455
+ throw new Error(codexCompatibilityError(codexCompatibility));
456
+ }
457
+ const codexVersion = codexCompatibility?.version ?? (codex ? nativeVersion(codex) : null);
291
458
  const config = {
292
459
  schemaVersion: selection.v2 ? 2 : 1,
293
460
  packageVersion: VERSION,
@@ -299,6 +466,13 @@ async function setup(args) {
299
466
  codexVersion,
300
467
  packageRoot: PACKAGE_ROOT,
301
468
  ...(selection.v2 ? { context: policy } : {}),
469
+ telemetry: {
470
+ enabled: telemetry.enabled,
471
+ disclosedAt: telemetry.enabled
472
+ ? (telemetry.disclosedAt ?? new Date().toISOString())
473
+ : telemetry.disclosedAt,
474
+ },
475
+ claudeObservation,
302
476
  };
303
477
  let probe;
304
478
  try {
@@ -345,10 +519,16 @@ async function doctor(json) {
345
519
  codex: resolveNative('codex'),
346
520
  node: resolveNative('node'),
347
521
  };
348
- const nativeVersions = {
522
+ const rawNativeVersions = {
349
523
  claude: native.claude ? nativeVersion(native.claude) : '',
350
524
  codex: native.codex ? nativeVersion(native.codex) : '',
351
525
  };
526
+ const codexCompatibility =
527
+ native.codex !== null ? await resolveCodexCompatibility(native.codex) : null;
528
+ const nativeVersions = {
529
+ ...rawNativeVersions,
530
+ codex: codexCompatibility?.version ?? rawNativeVersions.codex,
531
+ };
352
532
  const needsClaude = config?.clientSelection === 'claude' || config?.clientSelection === 'both';
353
533
  const needsCodex = config?.clientSelection === 'codex' || config?.clientSelection === 'both';
354
534
  const nativeReady =
@@ -357,7 +537,8 @@ async function doctor(json) {
357
537
  (!needsCodex ||
358
538
  (native.codex !== null &&
359
539
  native.node !== null &&
360
- isTestedNativeVersion('codex', nativeVersions.codex)));
540
+ (codexCompatibility?.status === 'qualified' ||
541
+ codexCompatibility?.status === 'protocol_match')));
361
542
  const result = {
362
543
  version: VERSION,
363
544
  platform: platform.ok ? process.platform : platform.code,
@@ -371,8 +552,23 @@ async function doctor(json) {
371
552
  node: native.node !== null,
372
553
  },
373
554
  nativeVersions,
555
+ codexCompatibility: codexCompatibility?.status ?? 'unavailable',
556
+ codexCompatibilityReason: codexCompatibility?.reason ?? 'codex_not_found',
557
+ codexCompatibilityVersion: codexCompatibility?.version ?? null,
374
558
  nativeReady,
375
559
  context: publicPolicy(policy),
560
+ telemetry: {
561
+ enabled: config?.telemetry?.enabled === true,
562
+ disclosed:
563
+ config?.telemetry?.disclosedAt !== null && config?.telemetry?.disclosedAt !== undefined,
564
+ },
565
+ claudeObservation: {
566
+ shadowEnabled: config?.claudeObservation?.shadowEnabled === true,
567
+ diagnosticsEnabled: config?.claudeObservation?.diagnosticsEnabled === true,
568
+ disclosed:
569
+ config?.claudeObservation?.disclosedAt !== null &&
570
+ config?.claudeObservation?.disclosedAt !== undefined,
571
+ },
376
572
  paidOrPilot: probe?.ok
377
573
  ? 'provider_free_probe_ok; paid entitlement or pilot invitation remains server-enforced'
378
574
  : config
@@ -396,6 +592,18 @@ async function status(json) {
396
592
  disclosed: config?.disclosureVersion !== undefined,
397
593
  schemaVersion: config?.schemaVersion ?? null,
398
594
  context: publicPolicy(policy),
595
+ telemetry: {
596
+ enabled: config?.telemetry?.enabled === true,
597
+ disclosed:
598
+ config?.telemetry?.disclosedAt !== null && config?.telemetry?.disclosedAt !== undefined,
599
+ },
600
+ claudeObservation: {
601
+ shadowEnabled: config?.claudeObservation?.shadowEnabled === true,
602
+ diagnosticsEnabled: config?.claudeObservation?.diagnosticsEnabled === true,
603
+ disclosed:
604
+ config?.claudeObservation?.disclosedAt !== null &&
605
+ config?.claudeObservation?.disclosedAt !== undefined,
606
+ },
399
607
  monitoredSession: null,
400
608
  monitoringStatus: 'not_observed',
401
609
  };
@@ -423,6 +631,27 @@ async function main(argv) {
423
631
  validateOptions(rest, new Set(['--json']), new Set());
424
632
  return status(rest.includes('--json'));
425
633
  }
634
+ if (command === 'diagnostics') {
635
+ validateOptions(
636
+ rest,
637
+ new Set(['--client', '--tail', '--json']),
638
+ new Set(['--client', '--tail']),
639
+ );
640
+ const requestedClient = option(rest, '--client');
641
+ if (requestedClient !== undefined && !['claude', 'codex'].includes(requestedClient))
642
+ throw new Error('diagnostics_client_unsupported');
643
+ const tail = Number(option(rest, '--tail') ?? '20');
644
+ if (!Number.isSafeInteger(tail) || tail < 1 || tail > 100)
645
+ throw new Error('invalid_diagnostics_tail');
646
+ const client = requestedClient ?? 'claude';
647
+ const records =
648
+ client === 'codex'
649
+ ? await readCodexDiagnostics(stateRoot(), tail)
650
+ : await readDiagnostics(stateRoot(), tail);
651
+ if (rest.includes('--json')) console.log(JSON.stringify({ client, records }));
652
+ else console.log(records);
653
+ return;
654
+ }
426
655
  if (command === 'disable') {
427
656
  if (rest.length > 0) throw new Error('disable_does_not_accept_options');
428
657
  return removeOwnedSetup();
@@ -439,12 +668,13 @@ async function main(argv) {
439
668
  (typeof config.claudeVersion !== 'string' ||
440
669
  !isTestedNativeVersion('claude', config.claudeVersion))
441
670
  )
442
- throw new Error('claude_version_unverified');
443
- if (
444
- command === 'codex' &&
445
- (typeof config.codexVersion !== 'string' ||
446
- !isTestedNativeVersion('codex', config.codexVersion))
447
- )
671
+ throw new Error(
672
+ nativeVersionDiagnostic(
673
+ 'claude',
674
+ typeof config.claudeVersion === 'string' ? config.claudeVersion : '',
675
+ ),
676
+ );
677
+ if (command === 'codex' && typeof config.codexVersion !== 'string')
448
678
  throw new Error('codex_version_unverified');
449
679
  const root = stateRoot();
450
680
  const result = await (command === 'claude'