@brunobracaioli/aibrain 0.4.0-beta.10 → 0.4.0-beta.12
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 +13 -11
- 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 +67 -18
- 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
|
|
|
@@ -19,8 +19,9 @@ For licensed accounts, `aisac claude` downloads the AISAC Engineering pack for
|
|
|
19
19
|
the session, verifies its Ed25519 signature against the key pinned in this
|
|
20
20
|
release, and loads it as the `aisac-engineering` plugin from the session's
|
|
21
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
|
|
23
|
-
call
|
|
22
|
+
pack. Once set up, `aisac setup --lang pt-BR|en` picks the pack language without a
|
|
23
|
+
network call; on a machine that was never set up it runs the full setup in that
|
|
24
|
+
language. The pack is never part of this npm package.
|
|
24
25
|
|
|
25
26
|
`aisac codex` (Codex 0.164.0 or later) loads the same verified pack for that
|
|
26
27
|
session only: its skills are added to the Codex app-server the launch owns
|
|
@@ -50,8 +51,9 @@ servers: `github` (read-only, token in `GITHUB_PERSONAL_ACCESS_TOKEN`), `supabas
|
|
|
50
51
|
- **Login:** the host does it (`/mcp` in Claude Code, `aisac mcp login <id>` for Codex)
|
|
51
52
|
and keeps it in its own store. No token reaches `~/.aibrain` or AISAC.
|
|
52
53
|
|
|
53
|
-
Codex capture also requires the `flock` utility from `util-linux`,
|
|
54
|
-
included
|
|
54
|
+
On Linux/WSL, Codex capture also requires the `flock` utility from `util-linux`,
|
|
55
|
+
normally included. On macOS it uses `/usr/bin/perl`, `/usr/sbin/sysctl` and
|
|
56
|
+
`/bin/ps` instead. The launcher and doctor check availability before a
|
|
55
57
|
monitored Codex session starts. Capture coordination is released by the kernel
|
|
56
58
|
after a process exits unexpectedly. An unidentifiable empty lock left by an
|
|
57
59
|
older client requires a verified support migration; do not delete checkpoints,
|
|
@@ -88,7 +90,7 @@ connection. Its failure or timeout closes only that auxiliary connection.
|
|
|
88
90
|
```sh
|
|
89
91
|
# Install the published beta:
|
|
90
92
|
npm install -g @aisac/cli@beta
|
|
91
|
-
aisac setup --client both
|
|
93
|
+
aisac setup # configures the clients on PATH; --client claude|codex|both to choose
|
|
92
94
|
aisac doctor --json
|
|
93
95
|
aisac claude
|
|
94
96
|
aisac codex
|
|
@@ -102,10 +104,10 @@ Installation does not contact AI Brain, change native client configuration, or
|
|
|
102
104
|
enable all paid accounts. Setup requires an existing AI Brain PAT, explicit
|
|
103
105
|
versioned disclosure, and successful provider-free pilot access. Claude and
|
|
104
106
|
Codex continue to own their own authentication. Windows native is unsupported;
|
|
105
|
-
run the beta inside WSL. On macOS,
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
this beta.
|
|
107
|
+
run the beta inside WSL. On macOS, Codex capture locks with the system
|
|
108
|
+
`/usr/bin/perl` (`flock(2)`) and identifies the lock owner with `sysctl` and
|
|
109
|
+
`ps`, so nothing extra needs installing. Pricing and general customer
|
|
110
|
+
activation are outside this beta.
|
|
109
111
|
|
|
110
112
|
For reviewed noninteractive automation only, use a private token file and
|
|
111
113
|
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.12';
|
|
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.12';
|
|
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';
|
|
@@ -53,6 +52,9 @@ function usage() {
|
|
|
53
52
|
console.log(
|
|
54
53
|
`${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.`,
|
|
55
54
|
);
|
|
55
|
+
console.log(
|
|
56
|
+
'Clients: setup without --client configures the clients installed on PATH (Claude Code, Codex, or both).',
|
|
57
|
+
);
|
|
56
58
|
console.log(
|
|
57
59
|
'Telemetry: setup --telemetry on|off (omitted preserves the stored choice; fresh defaults off).',
|
|
58
60
|
);
|
|
@@ -60,7 +62,7 @@ function usage() {
|
|
|
60
62
|
'Claude shadow: setup --claude-shadow on|off and --claude-diagnostics on|off (preview stays applied=false).',
|
|
61
63
|
);
|
|
62
64
|
console.log(
|
|
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`.',
|
|
65
|
+
'AISAC Engineering: `aisac claude` and `aisac codex` (Codex 0.164+) load the licensed method; setup --lang pt-BR|en picks its language (before the first setup it runs the full setup); setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
|
|
64
66
|
);
|
|
65
67
|
console.log(
|
|
66
68
|
'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.',
|
|
@@ -117,6 +119,18 @@ async function maskedPrompt() {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A setup error that carries the PAT step-by-step guide when the failure means
|
|
124
|
+
* the subscriber needs a (different) token.
|
|
125
|
+
* @param {string} code @param {'pt-BR' | 'en'} locale @param {string} endpoint
|
|
126
|
+
*/
|
|
127
|
+
function setupTokenError(code, locale, endpoint) {
|
|
128
|
+
const error = new Error(code);
|
|
129
|
+
const guide = tokenFailureGuide(code, locale, endpoint);
|
|
130
|
+
if (guide !== null) Object.defineProperty(error, 'guide', { value: guide });
|
|
131
|
+
return error;
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
/** @param {string[]} args @param {string} name */
|
|
121
135
|
function option(args, name) {
|
|
122
136
|
const index = args.indexOf(name);
|
|
@@ -176,8 +190,11 @@ function policyOverrides(args) {
|
|
|
176
190
|
return values;
|
|
177
191
|
}
|
|
178
192
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
193
|
+
/**
|
|
194
|
+
* @param {string[]} args @param {Awaited<ReturnType<typeof readConfig>>} previous
|
|
195
|
+
* @param {'claude' | 'codex' | 'both'} clients
|
|
196
|
+
*/
|
|
197
|
+
function selectedPolicy(args, previous, clients) {
|
|
181
198
|
const explicitProfile = option(args, '--context-profile');
|
|
182
199
|
const explicitV2 =
|
|
183
200
|
explicitProfile !== undefined ||
|
|
@@ -216,7 +233,6 @@ function selectedPolicy(args, previous) {
|
|
|
216
233
|
if (option(args, '--goal') === undefined && inherited.goal !== undefined)
|
|
217
234
|
overrides.goal = inherited.goal;
|
|
218
235
|
}
|
|
219
|
-
const clients = option(args, '--client') ?? defaultClientSelection();
|
|
220
236
|
if (
|
|
221
237
|
clients === 'codex' &&
|
|
222
238
|
args.some(
|
|
@@ -405,6 +421,32 @@ async function disclosureAccepted(
|
|
|
405
421
|
}
|
|
406
422
|
}
|
|
407
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Client selection when `--client` is omitted: the native clients found on PATH. Codex
|
|
426
|
+
* also needs `node`, the same pair the explicit `--client codex` path checks.
|
|
427
|
+
* @returns {'claude' | 'codex' | 'both' | null}
|
|
428
|
+
*/
|
|
429
|
+
function detectedClients() {
|
|
430
|
+
const hasClaude = resolveNative('claude') !== null;
|
|
431
|
+
const hasCodex = resolveNative('codex') !== null && resolveNative('node') !== null;
|
|
432
|
+
if (hasClaude && hasCodex) return 'both';
|
|
433
|
+
if (hasClaude) return 'claude';
|
|
434
|
+
return hasCodex ? 'codex' : null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** @param {string | undefined} requested @returns {'claude' | 'codex' | 'both'} */
|
|
438
|
+
function setupClients(requested) {
|
|
439
|
+
if (requested !== undefined) {
|
|
440
|
+
if (requested !== 'claude' && requested !== 'codex' && requested !== 'both')
|
|
441
|
+
throw new Error('invalid_client');
|
|
442
|
+
return requested;
|
|
443
|
+
}
|
|
444
|
+
const detected = detectedClients();
|
|
445
|
+
if (detected === null) throw new Error('no_native_client_found');
|
|
446
|
+
console.log(`Clients detected on PATH: ${detected} (pass --client to choose).`);
|
|
447
|
+
return detected;
|
|
448
|
+
}
|
|
449
|
+
|
|
408
450
|
/** @param {string[]} args */
|
|
409
451
|
async function setup(args) {
|
|
410
452
|
if (args.includes('--yes')) throw new Error('explicit_disclosure_flag_required');
|
|
@@ -442,8 +484,10 @@ async function setup(args) {
|
|
|
442
484
|
throw new Error('setup_undo_cannot_have_options');
|
|
443
485
|
const lang = option(args, '--lang');
|
|
444
486
|
if (lang !== undefined && !isPackLocale(lang)) throw new Error('invalid_lang');
|
|
445
|
-
if (lang !== undefined && args.length === 2) return setPackLocale(lang);
|
|
446
487
|
const previous = await readConfig();
|
|
488
|
+
// `--lang` alone only switches the language of an existing setup. Before the first
|
|
489
|
+
// setup it runs the full setup (token included) and stores the language at the end.
|
|
490
|
+
if (lang !== undefined && args.length === 2 && previous !== null) return setPackLocale(lang);
|
|
447
491
|
if (await updateLocalOptOut(previous, args)) return;
|
|
448
492
|
const localOptOut =
|
|
449
493
|
option(args, '--telemetry') === 'off' &&
|
|
@@ -451,12 +495,7 @@ async function setup(args) {
|
|
|
451
495
|
if (localOptOut) return disableTelemetryLocally(previous);
|
|
452
496
|
const platform = platformCheck();
|
|
453
497
|
if (!platform.ok) throw new Error(`${platform.code}:${platform.message}`);
|
|
454
|
-
const clients = option(args, '--client')
|
|
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
|
-
}
|
|
498
|
+
const clients = setupClients(option(args, '--client'));
|
|
460
499
|
if (
|
|
461
500
|
clients === 'codex' &&
|
|
462
501
|
(option(args, '--claude-shadow') !== undefined ||
|
|
@@ -465,7 +504,7 @@ async function setup(args) {
|
|
|
465
504
|
throw new Error('claude_observation_requires_claude_client');
|
|
466
505
|
const endpoint = option(args, '--endpoint') ?? ENDPOINT;
|
|
467
506
|
validateEndpoint(endpoint);
|
|
468
|
-
const selection = selectedPolicy(args, previous);
|
|
507
|
+
const selection = selectedPolicy(args, previous, clients);
|
|
469
508
|
const telemetry = telemetrySelection(args, previous);
|
|
470
509
|
const claudeObservation = claudeObservationSelection(args, previous);
|
|
471
510
|
const policy = selection.policy;
|
|
@@ -487,10 +526,18 @@ async function setup(args) {
|
|
|
487
526
|
)
|
|
488
527
|
throw new Error('disclosure_not_accepted');
|
|
489
528
|
const root = await ensureStateRoot();
|
|
529
|
+
const locale = resolvePackLocale(lang ?? (await readStoredLocale(root)));
|
|
490
530
|
const tokenPath = option(args, '--token-file');
|
|
491
|
-
|
|
531
|
+
if (!tokenPath && stdin.isTTY && stdout.isTTY)
|
|
532
|
+
console.log(`\n${tokenPromptGuide(locale, endpoint)}\n`);
|
|
533
|
+
let token;
|
|
534
|
+
try {
|
|
535
|
+
token = tokenPath ? await readPrivateToken(tokenPath) : await maskedPrompt();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
throw setupTokenError(errorMessage(error), locale, endpoint);
|
|
538
|
+
}
|
|
492
539
|
token = token.trim();
|
|
493
|
-
if (!token) throw
|
|
540
|
+
if (!token) throw setupTokenError('empty_token', locale, endpoint);
|
|
494
541
|
const claude = resolveNative('claude');
|
|
495
542
|
const codex = resolveNative('codex');
|
|
496
543
|
const node = resolveNative('node');
|
|
@@ -536,7 +583,7 @@ async function setup(args) {
|
|
|
536
583
|
? await providerFreeProbe(endpoint, token, policy, clients)
|
|
537
584
|
: await providerFreeProbe(endpoint, token);
|
|
538
585
|
} catch (error) {
|
|
539
|
-
throw
|
|
586
|
+
throw setupTokenError(`provider_free_probe_failed:${errorMessage(error)}`, locale, endpoint);
|
|
540
587
|
}
|
|
541
588
|
try {
|
|
542
589
|
const configWithToken = { ...config, token, tokenSha256: await tokenDigest(token), probe };
|
|
@@ -800,6 +847,8 @@ main(cliArgs).catch((error) => {
|
|
|
800
847
|
);
|
|
801
848
|
} else {
|
|
802
849
|
console.error(`${COMMAND}: ${errorMessage(error)}`);
|
|
850
|
+
const guide = /** @type {{ guide?: unknown }} */ (error ?? {}).guide;
|
|
851
|
+
if (typeof guide === 'string') console.error(`\n${guide}`);
|
|
803
852
|
}
|
|
804
853
|
process.exitCode = 1;
|
|
805
854
|
});
|
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