@brunobracaioli/aibrain 0.4.0-beta.10 → 0.4.0-beta.11
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 +9 -8
- package/dist/assets/codex-context-compaction.mjs +145 -4
- package/dist/assets/jev-paid-pilot-terminal.mjs +6 -0
- package/dist/claude-observation.mjs +1 -1
- package/dist/cli.mjs +29 -12
- package/dist/launch.mjs +1 -6
- package/dist/package.json +1 -1
- package/dist/platform.mjs +33 -23
- package/dist/token-guide.mjs +105 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# aisac
|
|
2
2
|
|
|
3
|
-
The AISAC client for Claude Code and Codex on Linux
|
|
4
|
-
|
|
3
|
+
The AISAC client for Claude Code and Codex on Linux, WSL and macOS, with
|
|
4
|
+
Node.js 22 or newer:
|
|
5
5
|
AI Brain memory, context management and, for AISAC Founding Cohort members,
|
|
6
6
|
AISAC Engineering.
|
|
7
7
|
|
|
@@ -50,8 +50,9 @@ servers: `github` (read-only, token in `GITHUB_PERSONAL_ACCESS_TOKEN`), `supabas
|
|
|
50
50
|
- **Login:** the host does it (`/mcp` in Claude Code, `aisac mcp login <id>` for Codex)
|
|
51
51
|
and keeps it in its own store. No token reaches `~/.aibrain` or AISAC.
|
|
52
52
|
|
|
53
|
-
Codex capture also requires the `flock` utility from `util-linux`,
|
|
54
|
-
included
|
|
53
|
+
On Linux/WSL, Codex capture also requires the `flock` utility from `util-linux`,
|
|
54
|
+
normally included. On macOS it uses `/usr/bin/perl`, `/usr/sbin/sysctl` and
|
|
55
|
+
`/bin/ps` instead. The launcher and doctor check availability before a
|
|
55
56
|
monitored Codex session starts. Capture coordination is released by the kernel
|
|
56
57
|
after a process exits unexpectedly. An unidentifiable empty lock left by an
|
|
57
58
|
older client requires a verified support migration; do not delete checkpoints,
|
|
@@ -102,10 +103,10 @@ Installation does not contact AI Brain, change native client configuration, or
|
|
|
102
103
|
enable all paid accounts. Setup requires an existing AI Brain PAT, explicit
|
|
103
104
|
versioned disclosure, and successful provider-free pilot access. Claude and
|
|
104
105
|
Codex continue to own their own authentication. Windows native is unsupported;
|
|
105
|
-
run the beta inside WSL. On macOS,
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
this beta.
|
|
106
|
+
run the beta inside WSL. On macOS, Codex capture locks with the system
|
|
107
|
+
`/usr/bin/perl` (`flock(2)`) and identifies the lock owner with `sysctl` and
|
|
108
|
+
`ps`, so nothing extra needs installing. Pricing and general customer
|
|
109
|
+
activation are outside this beta.
|
|
109
110
|
|
|
110
111
|
For reviewed noninteractive automation only, use a private token file and
|
|
111
112
|
explicitly acknowledge the displayed disclosure with
|
|
@@ -1007,6 +1007,128 @@ async function processIdentity(pid) {
|
|
|
1007
1007
|
}
|
|
1008
1008
|
}
|
|
1009
1009
|
|
|
1010
|
+
// macOS has flock(2) but no util-linux helper and no /proc (ADR-0049, spec
|
|
1011
|
+
// codex-macos). The guard helper and the owner identity use fixed system paths:
|
|
1012
|
+
// never PATH lookups, never a shell.
|
|
1013
|
+
const DARWIN_CAPTURE_TOOLS = Object.freeze({
|
|
1014
|
+
perl: '/usr/bin/perl',
|
|
1015
|
+
sysctl: '/usr/sbin/sysctl',
|
|
1016
|
+
ps: '/bin/ps',
|
|
1017
|
+
});
|
|
1018
|
+
const DARWIN_FLOCK_ARGS = Object.freeze([
|
|
1019
|
+
'-MFcntl=:flock',
|
|
1020
|
+
'-e',
|
|
1021
|
+
'open(my $f, "+<&=", 3) or exit 2; exit(flock($f, LOCK_EX | LOCK_NB) ? 0 : 1)',
|
|
1022
|
+
]);
|
|
1023
|
+
const DARWIN_TOOL_ENV = Object.freeze({ LC_ALL: 'C', TZ: 'UTC', PATH: '/usr/bin:/bin:/usr/sbin' });
|
|
1024
|
+
const DARWIN_TOOL_TIMEOUT_MS = 2_000;
|
|
1025
|
+
const DARWIN_TOOL_MAX_OUTPUT_BYTES = 4096;
|
|
1026
|
+
const DARWIN_START_TIME = /^[\x20-\x7e]{1,64}$/u;
|
|
1027
|
+
const BOOT_SESSION = /^[a-f0-9-]{36}$/iu;
|
|
1028
|
+
const PS_NO_MATCHING_PROCESS = 1;
|
|
1029
|
+
|
|
1030
|
+
/** Platform and tool paths of the capture lock; replaced only by tests. */
|
|
1031
|
+
let captureLockHost = { platform: process.platform, tools: DARWIN_CAPTURE_TOOLS };
|
|
1032
|
+
|
|
1033
|
+
export function __testSetCaptureLockHost(host) {
|
|
1034
|
+
const previous = captureLockHost;
|
|
1035
|
+
captureLockHost = host;
|
|
1036
|
+
return previous;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function guardHelperCommand() {
|
|
1040
|
+
return captureLockHost.platform === 'darwin'
|
|
1041
|
+
? [captureLockHost.tools.perl, [...DARWIN_FLOCK_ARGS]]
|
|
1042
|
+
: ['flock', ['-x', '-n', '3']];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** @returns {Promise<{ code: number | null, stdout: string }>} */
|
|
1046
|
+
function runDarwinTool(command, args) {
|
|
1047
|
+
return new Promise((resolvePromise) => {
|
|
1048
|
+
const child = spawn(command, args, {
|
|
1049
|
+
env: DARWIN_TOOL_ENV,
|
|
1050
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1051
|
+
});
|
|
1052
|
+
let stdout = '';
|
|
1053
|
+
let settled = false;
|
|
1054
|
+
const finish = (code) => {
|
|
1055
|
+
if (settled) return;
|
|
1056
|
+
settled = true;
|
|
1057
|
+
clearTimeout(timer);
|
|
1058
|
+
resolvePromise({ code, stdout });
|
|
1059
|
+
};
|
|
1060
|
+
const timer = setTimeout(() => {
|
|
1061
|
+
child.kill('SIGKILL');
|
|
1062
|
+
finish(null);
|
|
1063
|
+
}, DARWIN_TOOL_TIMEOUT_MS);
|
|
1064
|
+
child.stdout.setEncoding('utf8');
|
|
1065
|
+
child.stdout.on('data', (chunk) => {
|
|
1066
|
+
stdout += chunk;
|
|
1067
|
+
if (Buffer.byteLength(stdout) > DARWIN_TOOL_MAX_OUTPUT_BYTES) {
|
|
1068
|
+
child.kill('SIGKILL');
|
|
1069
|
+
finish(null);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
child.once('error', () => finish(null));
|
|
1073
|
+
child.once('close', (code) => finish(code));
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function darwinBootSession() {
|
|
1078
|
+
const result = await runDarwinTool(captureLockHost.tools.sysctl, ['-n', 'kern.bootsessionuuid']);
|
|
1079
|
+
const value = result.stdout.trim();
|
|
1080
|
+
return result.code === 0 && BOOT_SESSION.test(value) ? value.toUpperCase() : null;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function darwinProcessIdentity(pid) {
|
|
1084
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return { status: 'invalid' };
|
|
1085
|
+
const bootSession = await darwinBootSession();
|
|
1086
|
+
if (bootSession === null) return { status: 'unverifiable' };
|
|
1087
|
+
const result = await runDarwinTool(captureLockHost.tools.ps, [
|
|
1088
|
+
'-o',
|
|
1089
|
+
'lstart=',
|
|
1090
|
+
'-p',
|
|
1091
|
+
String(pid),
|
|
1092
|
+
]);
|
|
1093
|
+
const startTime = result.stdout.trim();
|
|
1094
|
+
// BSD ps exits 1 with no output when no process matches the pid.
|
|
1095
|
+
if (result.code === PS_NO_MATCHING_PROCESS && startTime === '') return { status: 'dead' };
|
|
1096
|
+
if (result.code !== 0 || !DARWIN_START_TIME.test(startTime)) return { status: 'unverifiable' };
|
|
1097
|
+
return { status: 'live', startTime, bootSession };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async function darwinSentinelOwner(value) {
|
|
1101
|
+
if (
|
|
1102
|
+
!isRecord(value) ||
|
|
1103
|
+
!hasOnlyKeys(value, [
|
|
1104
|
+
'schema',
|
|
1105
|
+
'backend',
|
|
1106
|
+
'platform',
|
|
1107
|
+
'pid',
|
|
1108
|
+
'startTime',
|
|
1109
|
+
'bootSession',
|
|
1110
|
+
'nonce',
|
|
1111
|
+
]) ||
|
|
1112
|
+
value.schema !== 1 ||
|
|
1113
|
+
value.backend !== 'flock' ||
|
|
1114
|
+
value.platform !== 'darwin' ||
|
|
1115
|
+
!isSafeBoundedInteger(value.pid, 1, 4_000_000_000) ||
|
|
1116
|
+
typeof value.startTime !== 'string' ||
|
|
1117
|
+
!DARWIN_START_TIME.test(value.startTime) ||
|
|
1118
|
+
typeof value.bootSession !== 'string' ||
|
|
1119
|
+
!BOOT_SESSION.test(value.bootSession) ||
|
|
1120
|
+
typeof value.nonce !== 'string' ||
|
|
1121
|
+
!isUuid(value.nonce)
|
|
1122
|
+
)
|
|
1123
|
+
return { status: 'unverifiable' };
|
|
1124
|
+
const observer = await darwinBootSession();
|
|
1125
|
+
if (observer === null) return { status: 'unverifiable' };
|
|
1126
|
+
if (observer !== value.bootSession.toUpperCase()) return { status: 'dead' };
|
|
1127
|
+
const current = await darwinProcessIdentity(value.pid);
|
|
1128
|
+
if (current.status !== 'live') return { status: current.status };
|
|
1129
|
+
return current.startTime === value.startTime ? { status: 'live' } : { status: 'dead' };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1010
1132
|
async function captureSentinelOwner(lockPath, fsOps) {
|
|
1011
1133
|
let info;
|
|
1012
1134
|
try {
|
|
@@ -1025,6 +1147,7 @@ async function captureSentinelOwner(lockPath, fsOps) {
|
|
|
1025
1147
|
} catch {
|
|
1026
1148
|
return { status: 'unverifiable' };
|
|
1027
1149
|
}
|
|
1150
|
+
if (captureLockHost.platform === 'darwin') return darwinSentinelOwner(value);
|
|
1028
1151
|
if (
|
|
1029
1152
|
!isRecord(value) ||
|
|
1030
1153
|
!hasOnlyKeys(value, [
|
|
@@ -1060,10 +1183,23 @@ async function captureSentinelOwner(lockPath, fsOps) {
|
|
|
1060
1183
|
return current.startTime === value.startTime ? { status: 'live' } : { status: 'dead' };
|
|
1061
1184
|
}
|
|
1062
1185
|
|
|
1063
|
-
async function
|
|
1186
|
+
async function ownSentinelRecord() {
|
|
1187
|
+
if (captureLockHost.platform === 'darwin') {
|
|
1188
|
+
const identity = await darwinProcessIdentity(process.pid);
|
|
1189
|
+
if (identity.status !== 'live') throw captureLockError('capture_lock_owner_unverifiable');
|
|
1190
|
+
return {
|
|
1191
|
+
schema: 1,
|
|
1192
|
+
backend: 'flock',
|
|
1193
|
+
platform: 'darwin',
|
|
1194
|
+
pid: process.pid,
|
|
1195
|
+
startTime: identity.startTime,
|
|
1196
|
+
bootSession: identity.bootSession,
|
|
1197
|
+
nonce: randomUUID(),
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1064
1200
|
const identity = await processIdentity(process.pid);
|
|
1065
1201
|
if (identity.status !== 'live') throw captureLockError('capture_lock_owner_unverifiable');
|
|
1066
|
-
|
|
1202
|
+
return {
|
|
1067
1203
|
schema: 1,
|
|
1068
1204
|
backend: 'flock',
|
|
1069
1205
|
pid: process.pid,
|
|
@@ -1071,7 +1207,11 @@ async function ensureCaptureSentinel(lockPath, task) {
|
|
|
1071
1207
|
bootId: identity.bootId,
|
|
1072
1208
|
pidNamespace: identity.pidNamespace,
|
|
1073
1209
|
nonce: randomUUID(),
|
|
1074
|
-
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async function ensureCaptureSentinel(lockPath, task) {
|
|
1214
|
+
const metadata = `${JSON.stringify(await ownSentinelRecord())}\n`;
|
|
1075
1215
|
while (true) {
|
|
1076
1216
|
const temporary = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1077
1217
|
let contention = false;
|
|
@@ -1149,7 +1289,8 @@ async function withAdvisoryCaptureLock(directory, task) {
|
|
|
1149
1289
|
while (true) {
|
|
1150
1290
|
let helper;
|
|
1151
1291
|
try {
|
|
1152
|
-
|
|
1292
|
+
const [command, args] = guardHelperCommand();
|
|
1293
|
+
helper = spawn(command, args, {
|
|
1153
1294
|
stdio: ['ignore', 'ignore', 'ignore', guardHandle.fd],
|
|
1154
1295
|
});
|
|
1155
1296
|
const acquired = await new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -20,6 +20,8 @@ const MAX_AUXILIARY_IN_FLIGHT = 32;
|
|
|
20
20
|
const MAX_AUXILIARY_NOTIFICATIONS = 256;
|
|
21
21
|
const DEFAULT_AUXILIARY_IDLE_TIMEOUT_MS = 120_000;
|
|
22
22
|
const NORMAL_DISCOVERY_CLOSE_CODES = new Set([1000, 1001, 1005]);
|
|
23
|
+
// Smallest sun_path among supported hosts: macOS 104 bytes, minus the terminator.
|
|
24
|
+
const UNIX_SOCKET_PATH_MAX_BYTES = 103;
|
|
23
25
|
|
|
24
26
|
function repoRoot() {
|
|
25
27
|
return fileURLToPath(new URL('..', import.meta.url)).replace(/\/$/u, '');
|
|
@@ -1022,6 +1024,10 @@ async function main() {
|
|
|
1022
1024
|
});
|
|
1023
1025
|
});
|
|
1024
1026
|
try {
|
|
1027
|
+
// macOS sun_path holds 104 bytes including the terminator (Linux 108); refuse a
|
|
1028
|
+
// path that the kernel would truncate or reject instead of binding elsewhere.
|
|
1029
|
+
if (Buffer.byteLength(socketPath) > UNIX_SOCKET_PATH_MAX_BYTES)
|
|
1030
|
+
throw new Error('terminal_bridge_socket_path_too_long');
|
|
1025
1031
|
await new Promise((resolve, reject) => {
|
|
1026
1032
|
server.once('error', reject);
|
|
1027
1033
|
server.listen(socketPath, async () => {
|
|
@@ -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.11';
|
|
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,8 +17,6 @@ import {
|
|
|
17
17
|
} from './state.mjs';
|
|
18
18
|
import {
|
|
19
19
|
captureLockPlatformCheck,
|
|
20
|
-
codexPlatformCheck,
|
|
21
|
-
defaultClientSelection,
|
|
22
20
|
nativeVersion,
|
|
23
21
|
platformCheck,
|
|
24
22
|
resolveNative,
|
|
@@ -27,6 +25,7 @@ import { launchClaude, launchCodex } from './launch.mjs';
|
|
|
27
25
|
import { claudeCompatibilityError, resolveClaudeCompatibility } from './claude-compatibility.mjs';
|
|
28
26
|
import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
|
|
29
27
|
import { providerFreeProbe } from './mcp.mjs';
|
|
28
|
+
import { tokenFailureGuide, tokenPromptGuide } from './token-guide.mjs';
|
|
30
29
|
import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.mjs';
|
|
31
30
|
import { readDiagnostics } from './claude-observation.mjs';
|
|
32
31
|
import { readCodexDiagnostics } from './codex-diagnostics.mjs';
|
|
@@ -42,7 +41,7 @@ import { extensionsSummary, hooksCommand, mcpCommand } from './extensions-cli.mj
|
|
|
42
41
|
import { sessionServers } from './mcp-servers.mjs';
|
|
43
42
|
import { anyFormatter } from './guardrails/format.mjs';
|
|
44
43
|
|
|
45
|
-
const VERSION = '0.4.0-beta.
|
|
44
|
+
const VERSION = '0.4.0-beta.11';
|
|
46
45
|
// The command is `aisac`; `aibrain` is an alias installed from the same entry.
|
|
47
46
|
const COMMAND = 'aisac';
|
|
48
47
|
const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
|
|
@@ -117,6 +116,18 @@ async function maskedPrompt() {
|
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
|
|
119
|
+
/**
|
|
120
|
+
* A setup error that carries the PAT step-by-step guide when the failure means
|
|
121
|
+
* the subscriber needs a (different) token.
|
|
122
|
+
* @param {string} code @param {'pt-BR' | 'en'} locale @param {string} endpoint
|
|
123
|
+
*/
|
|
124
|
+
function setupTokenError(code, locale, endpoint) {
|
|
125
|
+
const error = new Error(code);
|
|
126
|
+
const guide = tokenFailureGuide(code, locale, endpoint);
|
|
127
|
+
if (guide !== null) Object.defineProperty(error, 'guide', { value: guide });
|
|
128
|
+
return error;
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
/** @param {string[]} args @param {string} name */
|
|
121
132
|
function option(args, name) {
|
|
122
133
|
const index = args.indexOf(name);
|
|
@@ -216,7 +227,7 @@ function selectedPolicy(args, previous) {
|
|
|
216
227
|
if (option(args, '--goal') === undefined && inherited.goal !== undefined)
|
|
217
228
|
overrides.goal = inherited.goal;
|
|
218
229
|
}
|
|
219
|
-
const clients = option(args, '--client') ??
|
|
230
|
+
const clients = option(args, '--client') ?? 'both';
|
|
220
231
|
if (
|
|
221
232
|
clients === 'codex' &&
|
|
222
233
|
args.some(
|
|
@@ -451,12 +462,8 @@ async function setup(args) {
|
|
|
451
462
|
if (localOptOut) return disableTelemetryLocally(previous);
|
|
452
463
|
const platform = platformCheck();
|
|
453
464
|
if (!platform.ok) throw new Error(`${platform.code}:${platform.message}`);
|
|
454
|
-
const clients = option(args, '--client') ??
|
|
465
|
+
const clients = option(args, '--client') ?? 'both';
|
|
455
466
|
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
|
-
}
|
|
460
467
|
if (
|
|
461
468
|
clients === 'codex' &&
|
|
462
469
|
(option(args, '--claude-shadow') !== undefined ||
|
|
@@ -487,10 +494,18 @@ async function setup(args) {
|
|
|
487
494
|
)
|
|
488
495
|
throw new Error('disclosure_not_accepted');
|
|
489
496
|
const root = await ensureStateRoot();
|
|
497
|
+
const locale = resolvePackLocale(lang ?? (await readStoredLocale(root)));
|
|
490
498
|
const tokenPath = option(args, '--token-file');
|
|
491
|
-
|
|
499
|
+
if (!tokenPath && stdin.isTTY && stdout.isTTY)
|
|
500
|
+
console.log(`\n${tokenPromptGuide(locale, endpoint)}\n`);
|
|
501
|
+
let token;
|
|
502
|
+
try {
|
|
503
|
+
token = tokenPath ? await readPrivateToken(tokenPath) : await maskedPrompt();
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw setupTokenError(errorMessage(error), locale, endpoint);
|
|
506
|
+
}
|
|
492
507
|
token = token.trim();
|
|
493
|
-
if (!token) throw
|
|
508
|
+
if (!token) throw setupTokenError('empty_token', locale, endpoint);
|
|
494
509
|
const claude = resolveNative('claude');
|
|
495
510
|
const codex = resolveNative('codex');
|
|
496
511
|
const node = resolveNative('node');
|
|
@@ -536,7 +551,7 @@ async function setup(args) {
|
|
|
536
551
|
? await providerFreeProbe(endpoint, token, policy, clients)
|
|
537
552
|
: await providerFreeProbe(endpoint, token);
|
|
538
553
|
} catch (error) {
|
|
539
|
-
throw
|
|
554
|
+
throw setupTokenError(`provider_free_probe_failed:${errorMessage(error)}`, locale, endpoint);
|
|
540
555
|
}
|
|
541
556
|
try {
|
|
542
557
|
const configWithToken = { ...config, token, tokenSha256: await tokenDigest(token), probe };
|
|
@@ -800,6 +815,8 @@ main(cliArgs).catch((error) => {
|
|
|
800
815
|
);
|
|
801
816
|
} else {
|
|
802
817
|
console.error(`${COMMAND}: ${errorMessage(error)}`);
|
|
818
|
+
const guide = /** @type {{ guide?: unknown }} */ (error ?? {}).guide;
|
|
819
|
+
if (typeof guide === 'string') console.error(`\n${guide}`);
|
|
803
820
|
}
|
|
804
821
|
process.exitCode = 1;
|
|
805
822
|
});
|
package/dist/launch.mjs
CHANGED
|
@@ -678,12 +678,7 @@ export async function launchCodex(config, root, args, dependencies = {}) {
|
|
|
678
678
|
const validated = assertConfig(config);
|
|
679
679
|
assertCodexArgs(args);
|
|
680
680
|
const captureLockPlatform = captureLockPlatformCheck();
|
|
681
|
-
if (!captureLockPlatform.ok)
|
|
682
|
-
throw new Error(
|
|
683
|
-
captureLockPlatform.code === 'CODEX_MACOS_UNSUPPORTED'
|
|
684
|
-
? `${captureLockPlatform.code}:${captureLockPlatform.message}`
|
|
685
|
-
: captureLockPlatform.code,
|
|
686
|
-
);
|
|
681
|
+
if (!captureLockPlatform.ok) throw new Error(captureLockPlatform.code);
|
|
687
682
|
const executable = resolveNative('codex');
|
|
688
683
|
const node = resolveNative('node');
|
|
689
684
|
if (!executable || !node) throw new Error('codex_or_node_not_found');
|
package/dist/package.json
CHANGED
package/dist/platform.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { delimiter, isAbsolute, join } from 'node:path';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Host operating system gate shared by every command
|
|
9
|
-
* and macOS
|
|
8
|
+
* Host operating system gate shared by every command: Linux (including WSL)
|
|
9
|
+
* and macOS. Codex capture adds its lock-backend check below.
|
|
10
10
|
* @param {NodeJS.Platform} [platform]
|
|
11
11
|
*/
|
|
12
12
|
export function platformCheck(platform = process.platform) {
|
|
@@ -29,34 +29,44 @@ export function platformCheck(platform = process.platform) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
* System tools the macOS capture lock runs by absolute path (ADR-0049): perl
|
|
33
|
+
* takes flock(2) on the guard descriptor; sysctl and ps identify the owner.
|
|
34
|
+
*/
|
|
35
|
+
export const DARWIN_CAPTURE_TOOLS = Object.freeze(['/usr/bin/perl', '/usr/sbin/sysctl', '/bin/ps']);
|
|
36
|
+
|
|
37
|
+
/** @param {string} path */
|
|
38
|
+
function isExecutableFile(path) {
|
|
39
|
+
try {
|
|
40
|
+
if (!statSync(path).isFile()) return false;
|
|
41
|
+
accessSync(path, constants.X_OK);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Codex capture needs a kernel-owned guard lock: util-linux flock on Linux/WSL,
|
|
50
|
+
* the system perl's flock(2) on macOS (spec codex-macos).
|
|
51
|
+
* @param {NodeJS.ProcessEnv} [environment]
|
|
36
52
|
* @param {NodeJS.Platform} [platform]
|
|
53
|
+
* @param {readonly string[]} [darwinTools]
|
|
37
54
|
*/
|
|
38
|
-
export function
|
|
55
|
+
export function captureLockPlatformCheck(
|
|
56
|
+
environment = process.env,
|
|
57
|
+
platform = process.platform,
|
|
58
|
+
darwinTools = DARWIN_CAPTURE_TOOLS,
|
|
59
|
+
) {
|
|
39
60
|
const host = platformCheck(platform);
|
|
40
61
|
if (!host.ok) return host;
|
|
41
|
-
if (platform === 'darwin')
|
|
62
|
+
if (platform === 'darwin') {
|
|
63
|
+
if (darwinTools.every(isExecutableFile)) return { ok: true };
|
|
42
64
|
return {
|
|
43
65
|
ok: false,
|
|
44
|
-
code: '
|
|
45
|
-
message:
|
|
46
|
-
'aisac codex is not available on macOS yet. Use `aisac setup --client claude` and `aisac claude`, or run Codex from Linux/WSL.',
|
|
66
|
+
code: 'CODEX_CAPTURE_LOCK_BACKEND_UNAVAILABLE',
|
|
67
|
+
message: `Codex capture on macOS requires ${darwinTools.join(', ')}.`,
|
|
47
68
|
};
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Clients configured when `--client` is omitted: only those this OS can run. */
|
|
52
|
-
export function defaultClientSelection(platform = process.platform) {
|
|
53
|
-
return codexPlatformCheck(platform).ok ? 'both' : 'claude';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** @param {NodeJS.ProcessEnv} [environment] @param {NodeJS.Platform} [platform] */
|
|
57
|
-
export function captureLockPlatformCheck(environment = process.env, platform = process.platform) {
|
|
58
|
-
const codex = codexPlatformCheck(platform);
|
|
59
|
-
if (!codex.ok) return codex;
|
|
69
|
+
}
|
|
60
70
|
if (resolveNative('flock', environment) === null)
|
|
61
71
|
return {
|
|
62
72
|
ok: false,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Step-by-step guidance for obtaining an AI Brain PAT during `aisac setup`.
|
|
2
|
+
// Spec aibrain-subscriber-beta § Setup step 3: guide users to the existing
|
|
3
|
+
// dashboard token flow instead of failing with a bare error code. The text
|
|
4
|
+
// never contains the token or any account data; the URL is the endpoint origin.
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DASHBOARD = 'https://mcp.ai-brain.aisac.cloud';
|
|
7
|
+
|
|
8
|
+
/** @typedef {'pt-BR' | 'en'} GuideLocale */
|
|
9
|
+
|
|
10
|
+
/** Setup failures that mean "you need a (different) token", mapped to a reason key. */
|
|
11
|
+
const TOKEN_FAILURES = Object.freeze({
|
|
12
|
+
empty_token: 'empty',
|
|
13
|
+
token_file_required_in_noninteractive_mode: 'noninteractive',
|
|
14
|
+
'provider_free_probe_failed:mcp_auth_denied': 'denied',
|
|
15
|
+
'provider_free_probe_failed:mcp_paid_plan_required': 'unlicensed',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const TEXT = Object.freeze({
|
|
19
|
+
'pt-BR': {
|
|
20
|
+
intro: 'O aisac precisa de um token pessoal (PAT) do seu AI Brain.',
|
|
21
|
+
missing: 'Não tem um? Gere em um minuto:',
|
|
22
|
+
login: (/** @type {string} */ url) =>
|
|
23
|
+
` 1. Abra ${url} e entre com o MESMO e-mail usado na compra\n (chega um código de 6 dígitos). A licença fica vinculada a esse e-mail.`,
|
|
24
|
+
label:
|
|
25
|
+
' 2. No painel, dê um rótulo (ex.: meu-mac) e escolha o agente\n (Anthropic para Claude Code, OpenAI para Codex). Isso só organiza a atribuição.',
|
|
26
|
+
generate: ' 3. Clique em "Gerar token" e copie na hora: ele aparece uma única vez.',
|
|
27
|
+
pasteHere: ' 4. Cole abaixo em "AI Brain PAT". A digitação fica oculta.',
|
|
28
|
+
rerun: ' 4. Rode `aisac setup` de novo e cole o token em "AI Brain PAT" (fica oculto).',
|
|
29
|
+
safety:
|
|
30
|
+
'Nunca coloque o token em argumento de comando, arquivo do repositório ou perfil do shell.',
|
|
31
|
+
support: 'Se o e-mail da compra não entrar ou não tiver a licença, fale com o suporte AISAC.',
|
|
32
|
+
reasons: {
|
|
33
|
+
empty: 'Nenhum token foi informado.',
|
|
34
|
+
noninteractive:
|
|
35
|
+
'Sem um terminal interativo, o token precisa vir de `--token-file` (um arquivo privado, modo 600).',
|
|
36
|
+
denied:
|
|
37
|
+
'O AI Brain recusou este token: ele está incompleto, foi revogado ou não existe. Gere um novo.',
|
|
38
|
+
unlicensed:
|
|
39
|
+
'Este token pertence a uma conta sem a licença. Gere o token entrando com o e-mail usado na compra.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
en: {
|
|
43
|
+
intro: 'aisac needs a personal access token (PAT) from your AI Brain.',
|
|
44
|
+
missing: "Don't have one? Create it in a minute:",
|
|
45
|
+
login: (/** @type {string} */ url) =>
|
|
46
|
+
` 1. Open ${url} and sign in with the SAME email you used to buy\n (a 6-digit code arrives by email). The license is tied to that email.`,
|
|
47
|
+
label:
|
|
48
|
+
' 2. In the dashboard, enter a label (e.g. my-mac) and pick the agent\n (Anthropic for Claude Code, OpenAI for Codex). It only organizes attribution.',
|
|
49
|
+
generate: ' 3. Click "Gerar token" and copy it right away: it is shown only once.',
|
|
50
|
+
pasteHere: ' 4. Paste it below at "AI Brain PAT". Typing stays hidden.',
|
|
51
|
+
rerun: ' 4. Run `aisac setup` again and paste the token at "AI Brain PAT" (hidden).',
|
|
52
|
+
safety: 'Never put the token in command arguments, a repository file or a shell profile.',
|
|
53
|
+
support: 'If the purchase email cannot sign in or has no license, contact AISAC support.',
|
|
54
|
+
reasons: {
|
|
55
|
+
empty: 'No token was entered.',
|
|
56
|
+
noninteractive:
|
|
57
|
+
'Without an interactive terminal the token must come from `--token-file` (a private file, mode 600).',
|
|
58
|
+
denied:
|
|
59
|
+
'AI Brain rejected this token: it is incomplete, revoked or unknown. Create a new one.',
|
|
60
|
+
unlicensed:
|
|
61
|
+
'This token belongs to an account without the license. Create the token signed in with the purchase email.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** The dashboard origin derived from an https endpoint, else the production dashboard. @param {string} endpoint */
|
|
67
|
+
export function dashboardUrl(endpoint) {
|
|
68
|
+
try {
|
|
69
|
+
const url = new URL(endpoint);
|
|
70
|
+
return url.protocol === 'https:' ? url.origin : DEFAULT_DASHBOARD;
|
|
71
|
+
} catch {
|
|
72
|
+
return DEFAULT_DASHBOARD;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {GuideLocale} locale @param {string} endpoint @param {'prompt' | 'rerun'} step */
|
|
77
|
+
function steps(locale, endpoint, step) {
|
|
78
|
+
const text = TEXT[locale];
|
|
79
|
+
return [
|
|
80
|
+
text.intro,
|
|
81
|
+
text.missing,
|
|
82
|
+
text.login(dashboardUrl(endpoint)),
|
|
83
|
+
text.label,
|
|
84
|
+
text.generate,
|
|
85
|
+
step === 'prompt' ? text.pasteHere : text.rerun,
|
|
86
|
+
text.safety,
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Guidance printed right before the masked PAT prompt. @param {GuideLocale} locale @param {string} endpoint */
|
|
91
|
+
export function tokenPromptGuide(locale, endpoint) {
|
|
92
|
+
return steps(locale, endpoint, 'prompt');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Guidance for a setup failure caused by a missing, rejected or unlicensed token,
|
|
97
|
+
* or null when the failure has another cause.
|
|
98
|
+
* @param {string} code @param {GuideLocale} locale @param {string} endpoint
|
|
99
|
+
*/
|
|
100
|
+
export function tokenFailureGuide(code, locale, endpoint) {
|
|
101
|
+
const reason = /** @type {Record<string, keyof typeof TEXT.en.reasons>} */ (TOKEN_FAILURES)[code];
|
|
102
|
+
if (reason === undefined) return null;
|
|
103
|
+
const text = TEXT[locale];
|
|
104
|
+
return [text.reasons[reason], '', steps(locale, endpoint, 'rerun'), text.support].join('\n');
|
|
105
|
+
}
|
package/package.json
CHANGED