@brunobracaioli/aibrain 0.4.0-beta.5 → 0.4.0-beta.7
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 -0
- package/dist/assets/claude/hooks/context-compaction.ts +5 -1
- package/dist/claude-observation.mjs +1 -1
- package/dist/cli.mjs +17 -7
- package/dist/extensions-catalog.mjs +162 -3
- package/dist/extensions-cli.mjs +310 -78
- package/dist/extensions-preferences.mjs +30 -1
- package/dist/guardrails/guards.mjs +1 -1
- package/dist/launch.mjs +48 -11
- package/dist/mcp-servers.mjs +165 -0
- package/dist/monitor.mjs +12 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,15 @@ Licensed `aisac claude` sessions also load guardrail hooks as the session plugin
|
|
|
40
40
|
- **Code:** it ships in this package and runs locally. Nothing comes from the
|
|
41
41
|
server.
|
|
42
42
|
|
|
43
|
+
Licensed sessions of `aisac claude` and `aisac codex` can also load reviewed MCP
|
|
44
|
+
servers: `github` (read-only, token in `GITHUB_PERSONAL_ACCESS_TOKEN`), `supabase`
|
|
45
|
+
(read-only), `vercel`, `playwright` (pinned, isolated) and `context7`.
|
|
46
|
+
- **Default:** every server is off. `aisac mcp` lists them, `aisac mcp enable|disable
|
|
47
|
+
<id>` or `aisac mcp reset` changes the next session, and `aisac mcp info <id>` says
|
|
48
|
+
what a server reaches and how to log in.
|
|
49
|
+
- **Login:** the host does it (`/mcp` in Claude Code, `aisac mcp login <id>` for Codex)
|
|
50
|
+
and keeps it in its own store. No token reaches `~/.aibrain` or AISAC.
|
|
51
|
+
|
|
43
52
|
Codex capture also requires the `flock` utility from `util-linux`, normally
|
|
44
53
|
included in Linux/WSL. The launcher and doctor check availability before a
|
|
45
54
|
monitored Codex session starts. Capture coordination is released by the kernel
|
|
@@ -151,7 +151,7 @@ type Register = (
|
|
|
151
151
|
) => void;
|
|
152
152
|
|
|
153
153
|
const MAX_RESULT_BYTES = 256 * 1024;
|
|
154
|
-
const CLAUDE_HOOK_VERSION = '0.4.0-beta.
|
|
154
|
+
const CLAUDE_HOOK_VERSION = '0.4.0-beta.7';
|
|
155
155
|
const MAX_DIAGNOSTIC_BYTE_COUNT = 128 * 1024 * 1024;
|
|
156
156
|
const COOLDOWN_MS = 60_000;
|
|
157
157
|
const MAX_TRUNCATED_CODE_POINTS = 300;
|
|
@@ -463,6 +463,10 @@ function v2WireOptions(options: V2Options): Record<string, unknown> {
|
|
|
463
463
|
|
|
464
464
|
function safeErrorCategory(error: unknown): string {
|
|
465
465
|
const record = isRecord(error) ? error : {};
|
|
466
|
+
// A coded error names its own reason; the keyword guess below would read
|
|
467
|
+
// "AI Brain plan does not meet the minimum estimated reduction" as invalid_plan.
|
|
468
|
+
if (COMPACT_ERROR_CODES.includes(record.compactErrorCode as CompactErrorCode))
|
|
469
|
+
return record.compactErrorCode as CompactErrorCode;
|
|
466
470
|
const code = typeof record.code === 'string' ? record.code.toLowerCase() : '';
|
|
467
471
|
const name = typeof record.name === 'string' ? record.name.toLowerCase() : '';
|
|
468
472
|
const message = typeof record.message === 'string' ? record.message.toLowerCase() : '';
|
|
@@ -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.7';
|
|
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
|
@@ -36,10 +36,11 @@ import {
|
|
|
36
36
|
resolvePackLocale,
|
|
37
37
|
writeStoredLocale,
|
|
38
38
|
} from './engineering-pack.mjs';
|
|
39
|
-
import { extensionsSummary, hooksCommand } from './extensions-cli.mjs';
|
|
39
|
+
import { extensionsSummary, hooksCommand, mcpCommand } from './extensions-cli.mjs';
|
|
40
|
+
import { sessionServers } from './mcp-servers.mjs';
|
|
40
41
|
import { anyFormatter } from './guardrails/format.mjs';
|
|
41
42
|
|
|
42
|
-
const VERSION = '0.4.0-beta.
|
|
43
|
+
const VERSION = '0.4.0-beta.7';
|
|
43
44
|
// The command is `aisac`; `aibrain` is an alias installed from the same entry.
|
|
44
45
|
const COMMAND = 'aisac';
|
|
45
46
|
const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
|
|
@@ -62,6 +63,9 @@ function usage() {
|
|
|
62
63
|
console.log(
|
|
63
64
|
'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.',
|
|
64
65
|
);
|
|
66
|
+
console.log(
|
|
67
|
+
'MCP catalog: `aisac mcp` lists the reviewed servers (GitHub, Supabase, Vercel, Playwright, Context7); `aisac mcp enable|disable <id>...`, `aisac mcp reset`, `aisac mcp info <id>` and `aisac mcp login <id>` (Codex) change or explain the next session.',
|
|
68
|
+
);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
// Telemetry is opt-in; setup omits the flag to preserve the stored choice.
|
|
@@ -547,11 +551,16 @@ async function setup(args) {
|
|
|
547
551
|
/** Extension state plus whether `format-on-edit` has a formatter in this folder (FR8). */
|
|
548
552
|
async function doctorExtensions() {
|
|
549
553
|
const summary = await extensionsSummary(stateRoot());
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
554
|
+
// A token server is skipped at launch when its variable is unset (names only).
|
|
555
|
+
const mcpMissingEnv = sessionServers(summary.mcp, 'claude', process.env).missingEnv.map(
|
|
556
|
+
(skipped) => skipped.id,
|
|
557
|
+
);
|
|
558
|
+
const formatOnEdit = !summary.hooks.includes('format-on-edit')
|
|
559
|
+
? 'off'
|
|
560
|
+
: (await anyFormatter(process.cwd()))
|
|
561
|
+
? 'ready'
|
|
562
|
+
: 'no_formatter_found';
|
|
563
|
+
return { ...summary, formatOnEdit, mcpMissingEnv };
|
|
555
564
|
}
|
|
556
565
|
|
|
557
566
|
/** @param {boolean} json */
|
|
@@ -735,6 +744,7 @@ async function main(argv) {
|
|
|
735
744
|
return;
|
|
736
745
|
}
|
|
737
746
|
if (command === 'hooks') return hooksCommand(rest);
|
|
747
|
+
if (command === 'mcp') return mcpCommand(rest);
|
|
738
748
|
if (command === 'disable') {
|
|
739
749
|
if (rest.length > 0) throw new Error('disable_does_not_accept_options');
|
|
740
750
|
return removeOwnedSetup();
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* The AISAC Engineering extension catalog (spec aisac-engineering-extensions,
|
|
4
|
-
* FR1, FR2). It ships with the client and is the only source of hooks
|
|
5
|
-
* server and the licensed pack can neither add nor change
|
|
6
|
-
*
|
|
4
|
+
* FR1, FR2, FR7). It ships with the client and is the only source of hooks
|
|
5
|
+
* and MCP servers: the server and the licensed pack can neither add nor change
|
|
6
|
+
* entries (ADR-0048). Every change to MCP_CATALOG repeats the FR7 review
|
|
7
|
+
* recorded in the spec, and CODEOWNERS covers this file.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
/** @typedef {'pt-BR' | 'en'} CatalogLocale */
|
|
@@ -138,3 +139,161 @@ export function hookById(id) {
|
|
|
138
139
|
export function hookIds() {
|
|
139
140
|
return HOOK_CATALOG.map((entry) => entry.id);
|
|
140
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @typedef {object} McpHttpTransport
|
|
145
|
+
* @property {'http'} type
|
|
146
|
+
* @property {string} url Exact URL. Hosts key a stored login by server name and URL.
|
|
147
|
+
* @property {string} [bearerEnv] Environment variable that holds a bearer token. The
|
|
148
|
+
* catalog names it; its value is never stored or read by AISAC.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* @typedef {object} McpStdioTransport
|
|
152
|
+
* @property {'stdio'} type
|
|
153
|
+
* @property {'npx'} command
|
|
154
|
+
* @property {readonly string[]} args Pinned to an exact package version (FR7).
|
|
155
|
+
*/
|
|
156
|
+
/** @typedef {'host-oauth' | 'optional-oauth' | 'env-token' | 'none'} McpLogin */
|
|
157
|
+
/**
|
|
158
|
+
* @typedef {object} McpEntry
|
|
159
|
+
* @property {string} id
|
|
160
|
+
* @property {false} defaultOn Every catalog server starts off (FR7).
|
|
161
|
+
* @property {readonly ('claude' | 'codex')[]} clients
|
|
162
|
+
* @property {string} publisher
|
|
163
|
+
* @property {string} source The vendor's repository or documentation.
|
|
164
|
+
* @property {McpHttpTransport | McpStdioTransport} transport
|
|
165
|
+
* @property {McpLogin} login
|
|
166
|
+
* @property {string} [setup] A one-time command the user runs before the first use.
|
|
167
|
+
* @property {Readonly<Record<CatalogLocale, string>>} [loginNote] Why and how this login differs.
|
|
168
|
+
* @property {Readonly<Record<CatalogLocale, string>>} description
|
|
169
|
+
* @property {Readonly<Record<CatalogLocale, string>>} reaches What the server can reach (FR7).
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/** Session name of a catalog server, so it never collides with the user's own. */
|
|
173
|
+
export const MCP_SERVER_PREFIX = 'aisac-';
|
|
174
|
+
const PLAYWRIGHT_PACKAGE = '@playwright/mcp@0.0.83';
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* MCP catalog v1 (spec FR7; the owner chose the list on 2026-11-03). The review of
|
|
178
|
+
* each entry (source, transport, login, tools, reach) is recorded in the spec.
|
|
179
|
+
* @type {readonly McpEntry[]}
|
|
180
|
+
*/
|
|
181
|
+
export const MCP_CATALOG = Object.freeze([
|
|
182
|
+
{
|
|
183
|
+
id: 'github',
|
|
184
|
+
defaultOn: false,
|
|
185
|
+
clients: ['claude', 'codex'],
|
|
186
|
+
publisher: 'GitHub',
|
|
187
|
+
source: 'https://github.com/github/github-mcp-server',
|
|
188
|
+
transport: {
|
|
189
|
+
type: 'http',
|
|
190
|
+
url: 'https://api.githubcopilot.com/mcp/readonly',
|
|
191
|
+
bearerEnv: 'GITHUB_PERSONAL_ACCESS_TOKEN',
|
|
192
|
+
},
|
|
193
|
+
login: 'env-token',
|
|
194
|
+
loginNote: {
|
|
195
|
+
en: 'GitHub offers no OAuth login for Claude Code or Codex, so this server takes a personal access token. Use a fine-grained token with read-only access to only the repositories you need, and an expiry date: in Claude Code the session’s shell commands can also read this variable.',
|
|
196
|
+
'pt-BR':
|
|
197
|
+
'O GitHub não oferece login OAuth para o Claude Code nem para o Codex, então este servidor usa um token de acesso pessoal. Use um token fine-grained, somente leitura, só nos repositórios de que você precisa e com data de validade: no Claude Code os comandos de shell da sessão também conseguem ler essa variável.',
|
|
198
|
+
},
|
|
199
|
+
description: {
|
|
200
|
+
en: "GitHub's hosted server, read-only: repositories, issues, pull requests and users.",
|
|
201
|
+
'pt-BR':
|
|
202
|
+
'Servidor hospedado do GitHub, somente leitura: repositórios, issues, pull requests e usuários.',
|
|
203
|
+
},
|
|
204
|
+
reaches: {
|
|
205
|
+
en: 'Reads what your token can see: repository files, commits, branches, releases, issues, pull requests, and user and team profiles. This read-only URL exposes no write tool.',
|
|
206
|
+
'pt-BR':
|
|
207
|
+
'Lê o que o seu token enxerga: arquivos, commits, branches e releases dos repositórios, issues, pull requests e perfis de usuários e times. Esta URL somente leitura não expõe ferramenta de escrita.',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'supabase',
|
|
212
|
+
defaultOn: false,
|
|
213
|
+
clients: ['claude', 'codex'],
|
|
214
|
+
publisher: 'Supabase',
|
|
215
|
+
source: 'https://github.com/supabase-community/supabase-mcp',
|
|
216
|
+
transport: { type: 'http', url: 'https://mcp.supabase.com/mcp?read_only=true' },
|
|
217
|
+
login: 'host-oauth',
|
|
218
|
+
description: {
|
|
219
|
+
en: "Supabase's hosted server, read-only: tables, SQL, logs, advisors and docs.",
|
|
220
|
+
'pt-BR':
|
|
221
|
+
'Servidor hospedado da Supabase, somente leitura: tabelas, SQL, logs, advisors e documentação.',
|
|
222
|
+
},
|
|
223
|
+
reaches: {
|
|
224
|
+
en: 'Every organization and project of the Supabase account you log in with: database contents through read-only SQL, migrations, logs, advisors, Edge Functions, project URLs and publishable keys. read_only=true runs SQL as a read-only Postgres role and leaves out the write tools; the login grants the scopes you approve on Supabase.',
|
|
225
|
+
'pt-BR':
|
|
226
|
+
'Todas as organizações e projetos da conta Supabase em que você entrar: dados do banco por SQL somente leitura, migrations, logs, advisors, Edge Functions, URLs dos projetos e chaves publicáveis. read_only=true executa o SQL com um papel Postgres somente leitura e deixa de fora as ferramentas de escrita; o login concede os escopos que você aprovar na Supabase.',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'vercel',
|
|
231
|
+
defaultOn: false,
|
|
232
|
+
clients: ['claude', 'codex'],
|
|
233
|
+
publisher: 'Vercel',
|
|
234
|
+
source: 'https://vercel.com/docs/agent-resources/vercel-mcp',
|
|
235
|
+
transport: { type: 'http', url: 'https://mcp.vercel.com' },
|
|
236
|
+
login: 'host-oauth',
|
|
237
|
+
description: {
|
|
238
|
+
en: "Vercel's hosted server: projects, deployments, logs, domains and environment variables.",
|
|
239
|
+
'pt-BR':
|
|
240
|
+
'Servidor hospedado da Vercel: projetos, deploys, logs, domínios e variáveis de ambiente.',
|
|
241
|
+
},
|
|
242
|
+
reaches: {
|
|
243
|
+
en: 'The teams you grant at login, with the access of your Vercel user: projects, deployments, build and runtime logs, domains and DNS, and environment variables, including decrypted values. It also has write tools (deploy, change settings, DNS records) and no read-only mode, so keep the confirmation of each call on.',
|
|
244
|
+
'pt-BR':
|
|
245
|
+
'Os times que você liberar no login, com o acesso do seu usuário Vercel: projetos, deploys, logs de build e de execução, domínios e DNS, e variáveis de ambiente, inclusive valores decifrados. Também tem ferramentas de escrita (deploy, configurações, registros DNS) e não tem modo somente leitura, então mantenha a confirmação de cada chamada ligada.',
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'playwright',
|
|
250
|
+
defaultOn: false,
|
|
251
|
+
clients: ['claude', 'codex'],
|
|
252
|
+
publisher: 'Microsoft',
|
|
253
|
+
source: 'https://github.com/microsoft/playwright-mcp',
|
|
254
|
+
transport: {
|
|
255
|
+
type: 'stdio',
|
|
256
|
+
command: 'npx',
|
|
257
|
+
args: ['-y', PLAYWRIGHT_PACKAGE, '--browser', 'chromium', '--headless', '--isolated'],
|
|
258
|
+
},
|
|
259
|
+
login: 'none',
|
|
260
|
+
setup: `npx -y ${PLAYWRIGHT_PACKAGE} install-browser chrome-for-testing`,
|
|
261
|
+
description: {
|
|
262
|
+
en: 'A local headless browser driven by Playwright, for testing web pages.',
|
|
263
|
+
'pt-BR': 'Um navegador local sem janela, controlado pelo Playwright, para testar páginas.',
|
|
264
|
+
},
|
|
265
|
+
reaches: {
|
|
266
|
+
en: 'Runs on your machine with a fresh in-memory profile each session, so no saved logins. It opens any URL the agent asks for, including localhost and your local network, can run JavaScript in the page and upload files from the project. Page content is untrusted input.',
|
|
267
|
+
'pt-BR':
|
|
268
|
+
'Roda na sua máquina com um perfil novo em memória a cada sessão, sem logins salvos. Abre qualquer URL que o agente pedir, inclusive localhost e a sua rede local, pode executar JavaScript na página e enviar arquivos do projeto. O conteúdo das páginas é entrada não confiável.',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: 'context7',
|
|
273
|
+
defaultOn: false,
|
|
274
|
+
clients: ['claude', 'codex'],
|
|
275
|
+
publisher: 'Upstash',
|
|
276
|
+
source: 'https://github.com/upstash/context7',
|
|
277
|
+
transport: { type: 'http', url: 'https://mcp.context7.com/mcp' },
|
|
278
|
+
login: 'optional-oauth',
|
|
279
|
+
description: {
|
|
280
|
+
en: 'Up-to-date documentation and code examples for libraries, from Context7.',
|
|
281
|
+
'pt-BR': 'Documentação e exemplos de código atualizados de bibliotecas, do Context7.',
|
|
282
|
+
},
|
|
283
|
+
reaches: {
|
|
284
|
+
en: 'Public library documentation only. What the agent asks (library names and questions) goes to Upstash, so keep secrets and private code out of those questions. It works without login, with lower rate limits.',
|
|
285
|
+
'pt-BR':
|
|
286
|
+
'Só documentação pública de bibliotecas. O que o agente pergunta (nomes de bibliotecas e dúvidas) vai para a Upstash, então não coloque segredos nem código privado nessas perguntas. Funciona sem login, com limites de uso menores.',
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
/** @param {string} id @returns {McpEntry | undefined} */
|
|
292
|
+
export function mcpById(id) {
|
|
293
|
+
return MCP_CATALOG.find((entry) => entry.id === id);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** @returns {string[]} */
|
|
297
|
+
export function mcpIds() {
|
|
298
|
+
return MCP_CATALOG.map((entry) => entry.id);
|
|
299
|
+
}
|
package/dist/extensions-cli.mjs
CHANGED
|
@@ -1,35 +1,91 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* `aisac hooks` (spec aisac-engineering-extensions FR4): list,
|
|
4
|
-
* disable and reset
|
|
5
|
-
* opens a checklist.
|
|
6
|
-
*
|
|
3
|
+
* `aisac hooks` and `aisac mcp` (spec aisac-engineering-extensions FR4): list,
|
|
4
|
+
* enable, disable and reset catalog entries, offline. On a TTY without
|
|
5
|
+
* arguments each opens a checklist. `aisac mcp info` describes one server, and
|
|
6
|
+
* `aisac mcp login` runs Codex's own login for it. Choices apply to the next
|
|
7
|
+
* session; running sessions keep their set.
|
|
7
8
|
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
8
10
|
import { emitKeypressEvents } from 'node:readline';
|
|
9
|
-
import {
|
|
11
|
+
import { hookById, hookIds, mcpById, mcpIds } from './extensions-catalog.mjs';
|
|
10
12
|
import {
|
|
11
13
|
readPreferences,
|
|
12
14
|
resolveHooks,
|
|
15
|
+
resolveServers,
|
|
13
16
|
unknownHookIds,
|
|
17
|
+
unknownServerIds,
|
|
14
18
|
writePreferences,
|
|
15
19
|
} from './extensions-preferences.mjs';
|
|
16
20
|
import { readLastLaunch, readStoredLocale, resolvePackLocale } from './engineering-pack.mjs';
|
|
21
|
+
import { codexLoginArgs, serverName, supportsHostLogin } from './mcp-servers.mjs';
|
|
22
|
+
import { resolveNative } from './platform.mjs';
|
|
17
23
|
import { stateRoot } from './state.mjs';
|
|
18
24
|
|
|
25
|
+
/** @typedef {import('./extensions-catalog.mjs').CatalogLocale} CatalogLocale */
|
|
26
|
+
/** @typedef {import('./extensions-catalog.mjs').McpEntry} McpEntry */
|
|
27
|
+
/** @typedef {import('./extensions-preferences.mjs').ExtensionPreferences} ExtensionPreferences */
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} CatalogItem
|
|
30
|
+
* @property {{ id: string, description: Readonly<Record<CatalogLocale, string>> }} entry
|
|
31
|
+
* @property {boolean} enabled
|
|
32
|
+
* @property {'default' | 'choice'} source
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} ExtensionKind
|
|
36
|
+
* @property {'hooks' | 'mcp'} key The command name and the preferences key.
|
|
37
|
+
* @property {string} title
|
|
38
|
+
* @property {() => string[]} ids
|
|
39
|
+
* @property {(preferences: ExtensionPreferences) => CatalogItem[]} resolve
|
|
40
|
+
* @property {(preferences: ExtensionPreferences) => string[]} unknown
|
|
41
|
+
* @property {(id: string) => string} column The second column: hook mode or server login.
|
|
42
|
+
* @property {string} nextSession
|
|
43
|
+
*/
|
|
44
|
+
|
|
19
45
|
const USAGE_EXIT_CODE = 2;
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
'Applies to the next `aisac claude` session; running sessions keep their current set.';
|
|
23
|
-
const LICENSE_NOTE = 'Hooks load only with an active AISAC Engineering license.';
|
|
24
|
-
const CHECKLIST_HEADER =
|
|
25
|
-
'AISAC Engineering hooks: up/down moves, space toggles, enter saves, q cancels.';
|
|
46
|
+
const IO_EXIT_CODE = 1;
|
|
47
|
+
const LICENSE_NOTE = 'Extensions load only with an active AISAC Engineering license.';
|
|
26
48
|
const DEFAULT_COLUMNS = 80;
|
|
27
49
|
const MIN_COLUMNS = 20;
|
|
28
50
|
const ELLIPSIS = '…';
|
|
51
|
+
/** @type {Readonly<Record<McpEntry['login'], string>>} */
|
|
52
|
+
const LOGIN_LABEL = Object.freeze({
|
|
53
|
+
'host-oauth': 'oauth',
|
|
54
|
+
'optional-oauth': 'oauth-opt',
|
|
55
|
+
'env-token': 'env-token',
|
|
56
|
+
none: 'no-login',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** @type {ExtensionKind} */
|
|
60
|
+
const HOOKS = {
|
|
61
|
+
key: 'hooks',
|
|
62
|
+
title: 'AISAC Engineering hooks (Claude Code)',
|
|
63
|
+
ids: hookIds,
|
|
64
|
+
resolve: resolveHooks,
|
|
65
|
+
unknown: unknownHookIds,
|
|
66
|
+
column: (id) => hookById(id)?.mode ?? '',
|
|
67
|
+
nextSession:
|
|
68
|
+
'Applies to the next `aisac claude` session; running sessions keep their current set.',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** @type {ExtensionKind} */
|
|
72
|
+
const MCP = {
|
|
73
|
+
key: 'mcp',
|
|
74
|
+
title: 'AISAC Engineering MCP servers (Claude Code and Codex)',
|
|
75
|
+
ids: mcpIds,
|
|
76
|
+
resolve: resolveServers,
|
|
77
|
+
unknown: unknownServerIds,
|
|
78
|
+
column: (id) => {
|
|
79
|
+
const entry = mcpById(id);
|
|
80
|
+
return entry === undefined ? '' : LOGIN_LABEL[entry.login];
|
|
81
|
+
},
|
|
82
|
+
nextSession:
|
|
83
|
+
'Applies to the next `aisac claude` or `aisac codex` session; running sessions keep their current set.',
|
|
84
|
+
};
|
|
29
85
|
|
|
30
86
|
/**
|
|
31
87
|
* Cut a checklist row to the terminal width. The redraw moves the cursor up one
|
|
32
|
-
* row per
|
|
88
|
+
* row per item, which only holds while no row wraps; one column stays free so
|
|
33
89
|
* no terminal wraps at the last column either.
|
|
34
90
|
* @param {string} line
|
|
35
91
|
* @param {number | undefined} columns
|
|
@@ -73,13 +129,19 @@ export function checklistStep(state, key) {
|
|
|
73
129
|
return { state, done: null };
|
|
74
130
|
}
|
|
75
131
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
/**
|
|
133
|
+
* @param {ExtensionKind} kind @param {CatalogLocale} locale
|
|
134
|
+
* @param {CatalogItem[]} items @param {ChecklistState} [state]
|
|
135
|
+
*/
|
|
136
|
+
function renderLines(kind, locale, items, state) {
|
|
137
|
+
const idWidth = Math.max(...items.map((item) => item.entry.id.length));
|
|
138
|
+
const columnWidth = Math.max(...items.map((item) => kind.column(item.entry.id).length)) + 1;
|
|
139
|
+
return items.map((item, index) => {
|
|
140
|
+
const enabled = state === undefined ? item.enabled : state.enabled[index];
|
|
80
141
|
const pointer = state === undefined ? ' ' : index === state.cursor ? '>' : ' ';
|
|
81
|
-
const source = state === undefined ? ` ${
|
|
82
|
-
|
|
142
|
+
const source = state === undefined ? ` ${item.source.padEnd(7)}` : '';
|
|
143
|
+
const column = kind.column(item.entry.id).padEnd(columnWidth);
|
|
144
|
+
return `${pointer} [${enabled ? 'x' : ' '}] ${item.entry.id.padEnd(idWidth)} ${column}${source} ${item.entry.description[locale]}`;
|
|
83
145
|
});
|
|
84
146
|
}
|
|
85
147
|
|
|
@@ -90,43 +152,42 @@ async function licensedLastLaunch(root) {
|
|
|
90
152
|
}
|
|
91
153
|
|
|
92
154
|
/**
|
|
93
|
-
* Store explicit choices, keeping
|
|
94
|
-
* @param {Record<string, boolean>}
|
|
155
|
+
* Store one kind's explicit choices, keeping the other kind's as they are.
|
|
156
|
+
* @param {ExtensionKind} kind @param {Record<string, boolean>} choices
|
|
95
157
|
*/
|
|
96
|
-
async function
|
|
158
|
+
async function saveChoices(kind, choices) {
|
|
97
159
|
const current = await readPreferences(stateRoot());
|
|
98
|
-
|
|
160
|
+
const valid = current.status === 'valid';
|
|
161
|
+
await writePreferences({
|
|
162
|
+
hooks: kind.key === 'hooks' ? choices : valid ? current.hooks : {},
|
|
163
|
+
mcp: kind.key === 'mcp' ? choices : valid ? current.mcp : {},
|
|
164
|
+
});
|
|
99
165
|
if (current.status === 'invalid')
|
|
100
166
|
console.log('The previous extensions file was invalid and was replaced.');
|
|
101
167
|
}
|
|
102
168
|
|
|
103
|
-
/** @param {
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** @param {boolean} licensed */
|
|
109
|
-
function afterChange(licensed) {
|
|
110
|
-
console.log(NEXT_SESSION);
|
|
169
|
+
/** @param {ExtensionKind} kind @param {boolean} licensed */
|
|
170
|
+
function afterChange(kind, licensed) {
|
|
171
|
+
console.log(kind.nextSession);
|
|
111
172
|
if (!licensed) console.log(LICENSE_NOTE);
|
|
112
173
|
}
|
|
113
174
|
|
|
114
175
|
/**
|
|
115
176
|
* Interactive checklist. Resolves to the new enabled flags, or null on cancel.
|
|
116
|
-
* @param {
|
|
177
|
+
* @param {ExtensionKind} kind @param {CatalogLocale} locale @param {CatalogItem[]} items
|
|
117
178
|
* @returns {Promise<boolean[] | null>}
|
|
118
179
|
*/
|
|
119
|
-
function runChecklist(locale,
|
|
180
|
+
function runChecklist(kind, locale, items) {
|
|
120
181
|
return new Promise((resolve) => {
|
|
121
|
-
let state = { cursor: 0, enabled:
|
|
182
|
+
let state = { cursor: 0, enabled: items.map((item) => item.enabled) };
|
|
122
183
|
const draw = (/** @type {boolean} */ first) => {
|
|
123
|
-
if (!first) process.stdout.write(`\u001b[${
|
|
124
|
-
const rows = renderLines(locale,
|
|
184
|
+
if (!first) process.stdout.write(`\u001b[${items.length}A\u001b[J`);
|
|
185
|
+
const rows = renderLines(kind, locale, items, state).map((line) =>
|
|
125
186
|
fitLine(line, process.stdout.columns),
|
|
126
187
|
);
|
|
127
188
|
process.stdout.write(`${rows.join('\n')}\n`);
|
|
128
189
|
};
|
|
129
|
-
console.log(
|
|
190
|
+
console.log(`${kind.title}: up/down moves, space toggles, enter saves, q cancels.`);
|
|
130
191
|
draw(true);
|
|
131
192
|
emitKeypressEvents(process.stdin);
|
|
132
193
|
process.stdin.setRawMode(true);
|
|
@@ -146,17 +207,171 @@ function runChecklist(locale, hooks) {
|
|
|
146
207
|
}
|
|
147
208
|
|
|
148
209
|
/**
|
|
149
|
-
*
|
|
150
|
-
* @param {
|
|
210
|
+
* The JSON row of one item.
|
|
211
|
+
* @param {ExtensionKind} kind @param {CatalogLocale} locale @param {CatalogItem} item
|
|
212
|
+
*/
|
|
213
|
+
function jsonItem(kind, locale, item) {
|
|
214
|
+
const base = {
|
|
215
|
+
id: item.entry.id,
|
|
216
|
+
enabled: item.enabled,
|
|
217
|
+
source: item.source,
|
|
218
|
+
description: item.entry.description[locale],
|
|
219
|
+
};
|
|
220
|
+
if (kind.key === 'hooks') {
|
|
221
|
+
const hook = hookById(item.entry.id);
|
|
222
|
+
return { ...base, mode: hook?.mode, clients: hook?.clients };
|
|
223
|
+
}
|
|
224
|
+
const server = mcpById(item.entry.id);
|
|
225
|
+
return {
|
|
226
|
+
...base,
|
|
227
|
+
server: serverName(item.entry.id),
|
|
228
|
+
login: server?.login,
|
|
229
|
+
transport: server?.transport.type === 'stdio' ? 'local' : 'remote',
|
|
230
|
+
clients: server?.clients,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* What one server reaches and how to log in (FR4 `aisac mcp info`).
|
|
236
|
+
* @param {McpEntry} entry @param {CatalogLocale} locale @param {CatalogItem | undefined} item
|
|
151
237
|
*/
|
|
152
|
-
export
|
|
238
|
+
export function serverInfo(entry, locale, item) {
|
|
239
|
+
const name = serverName(entry.id);
|
|
240
|
+
const { transport } = entry;
|
|
241
|
+
const variable = transport.type === 'http' ? transport.bearerEnv : undefined;
|
|
242
|
+
/** @type {{ claude: string, codex: string }} */
|
|
243
|
+
const login =
|
|
244
|
+
entry.login === 'none'
|
|
245
|
+
? { claude: 'No login.', codex: 'No login.' }
|
|
246
|
+
: variable !== undefined
|
|
247
|
+
? {
|
|
248
|
+
claude: `Set ${variable} in the shell that runs aisac claude; without it the server is skipped.`,
|
|
249
|
+
codex: `Set ${variable} in the shell that runs aisac codex; without it the server is skipped.`,
|
|
250
|
+
}
|
|
251
|
+
: {
|
|
252
|
+
claude: `${entry.login === 'optional-oauth' ? 'Optional: ' : ''}in the session, /mcp, choose ${name}, then Authenticate.`,
|
|
253
|
+
codex: `${entry.login === 'optional-oauth' ? 'Optional: ' : ''}aisac mcp login ${entry.id} (runs Codex's own login with the session settings).`,
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
id: entry.id,
|
|
257
|
+
server: name,
|
|
258
|
+
enabled: item?.enabled ?? false,
|
|
259
|
+
choice: item?.source ?? 'default',
|
|
260
|
+
publisher: entry.publisher,
|
|
261
|
+
sourceUrl: entry.source,
|
|
262
|
+
description: entry.description[locale],
|
|
263
|
+
transport:
|
|
264
|
+
transport.type === 'stdio'
|
|
265
|
+
? {
|
|
266
|
+
type: 'local',
|
|
267
|
+
command: [transport.command, ...transport.args].join(' '),
|
|
268
|
+
note: 'Pinned to an exact package version; npx downloads it on first use.',
|
|
269
|
+
}
|
|
270
|
+
: {
|
|
271
|
+
type: 'remote',
|
|
272
|
+
url: transport.url,
|
|
273
|
+
note: 'Hosted by the vendor: its tools can change without a client release, so keep the confirmation of each call on.',
|
|
274
|
+
},
|
|
275
|
+
reaches: entry.reaches[locale],
|
|
276
|
+
login,
|
|
277
|
+
...(variable === undefined ? {} : { tokenVariable: variable }),
|
|
278
|
+
...(entry.loginNote === undefined ? {} : { loginNote: entry.loginNote[locale] }),
|
|
279
|
+
...(entry.setup === undefined ? {} : { setup: entry.setup }),
|
|
280
|
+
credentials:
|
|
281
|
+
'Logins stay in the host’s own credential store (Claude Code or Codex). AISAC never sees them: no token goes to ~/.aibrain or to AISAC.',
|
|
282
|
+
duplicate: `If you already run your own server for this service, keep one of them (aisac mcp disable ${entry.id}, or remove yours).`,
|
|
283
|
+
trifecta:
|
|
284
|
+
'Private data (GitHub, Supabase, Vercel) plus untrusted content (web pages, issues) plus a way out in one session lets injected text reach your data: keep the confirmation of each call on.',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** @param {ReturnType<typeof serverInfo>} info */
|
|
289
|
+
function printInfo(info) {
|
|
290
|
+
console.log(`${info.server}: ${info.description}`);
|
|
291
|
+
console.log(`State: ${info.enabled ? 'on' : 'off'} (${info.choice})`);
|
|
292
|
+
console.log(`Publisher: ${info.publisher} (${info.sourceUrl})`);
|
|
293
|
+
const where = info.transport.type === 'local' ? info.transport.command : info.transport.url;
|
|
294
|
+
console.log(`Transport: ${info.transport.type}, ${where}. ${info.transport.note}`);
|
|
295
|
+
console.log(`Reaches: ${info.reaches}`);
|
|
296
|
+
if (info.loginNote !== undefined) console.log(`Login: ${info.loginNote}`);
|
|
297
|
+
console.log(`Log in from Claude Code: ${info.login.claude}`);
|
|
298
|
+
console.log(`Log in from Codex: ${info.login.codex}`);
|
|
299
|
+
if (info.setup !== undefined) console.log(`Before the first use, run once: ${info.setup}`);
|
|
300
|
+
console.log(info.credentials);
|
|
301
|
+
console.log(info.trifecta);
|
|
302
|
+
console.log(info.duplicate);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* `aisac mcp login <id>`: Codex's own login, with the session's settings, so
|
|
307
|
+
* the next `aisac codex` session finds it. AISAC never sees the credential.
|
|
308
|
+
* @param {McpEntry} entry
|
|
309
|
+
*/
|
|
310
|
+
async function codexLogin(entry) {
|
|
311
|
+
const name = serverName(entry.id);
|
|
312
|
+
if (!supportsHostLogin(entry)) {
|
|
313
|
+
const variable = entry.transport.type === 'http' ? entry.transport.bearerEnv : undefined;
|
|
314
|
+
console.error(
|
|
315
|
+
variable === undefined
|
|
316
|
+
? `aisac: ${name} needs no login.`
|
|
317
|
+
: `aisac: ${name} has no host login; set ${variable} instead (aisac mcp info ${entry.id}).`,
|
|
318
|
+
);
|
|
319
|
+
process.exitCode = USAGE_EXIT_CODE;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const codex = resolveNative('codex');
|
|
323
|
+
if (codex === null) {
|
|
324
|
+
console.error('aisac: codex was not found on PATH.');
|
|
325
|
+
process.exitCode = IO_EXIT_CODE;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
console.log(
|
|
329
|
+
`Logging in to ${name} with Codex; the login stays in Codex's own store. Claude Code logs in inside the session: /mcp, choose ${name}, then Authenticate.`,
|
|
330
|
+
);
|
|
331
|
+
process.exitCode = await new Promise((resolve) => {
|
|
332
|
+
const child = spawn(codex, codexLoginArgs(entry), { stdio: 'inherit' });
|
|
333
|
+
child.on('error', () => resolve(IO_EXIT_CODE));
|
|
334
|
+
child.on('exit', (code) => resolve(code ?? IO_EXIT_CODE));
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @param {ExtensionKind} kind @param {string[]} ids
|
|
340
|
+
* @returns {boolean} true when every id is in the catalog
|
|
341
|
+
*/
|
|
342
|
+
function checkIds(kind, ids) {
|
|
343
|
+
const known = new Set(kind.ids());
|
|
344
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
345
|
+
if (unknown.length === 0) return true;
|
|
346
|
+
const noun = kind.key === 'hooks' ? 'hook' : 'MCP server';
|
|
347
|
+
console.error(
|
|
348
|
+
`aisac: unknown ${noun} id: ${unknown.join(', ')}. Valid: ${kind.ids().join(', ')}.`,
|
|
349
|
+
);
|
|
350
|
+
process.exitCode = USAGE_EXIT_CODE;
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** @param {ExtensionKind} kind */
|
|
355
|
+
function usage(kind) {
|
|
356
|
+
const extra = kind.key === 'mcp' ? ' | aisac mcp info <id> | aisac mcp login <id>' : '';
|
|
357
|
+
console.error(
|
|
358
|
+
`aisac: usage: aisac ${kind.key} [--json] | aisac ${kind.key} enable <id>... | aisac ${kind.key} disable <id>... | aisac ${kind.key} reset${extra}`,
|
|
359
|
+
);
|
|
360
|
+
process.exitCode = USAGE_EXIT_CODE;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* `aisac hooks|mcp [list|enable|disable|reset|info|login] [ids...] [--json]`.
|
|
365
|
+
* @param {ExtensionKind} kind @param {string[]} args
|
|
366
|
+
*/
|
|
367
|
+
async function extensionCommand(kind, args) {
|
|
153
368
|
const json = args.includes('--json');
|
|
154
369
|
const words = args.filter((arg) => arg !== '--json');
|
|
155
370
|
const [action = 'list', ...ids] = words;
|
|
156
371
|
const root = stateRoot();
|
|
157
372
|
const locale = resolvePackLocale(await readStoredLocale(root));
|
|
158
373
|
const preferences = await readPreferences(root);
|
|
159
|
-
const
|
|
374
|
+
const items = kind.resolve(preferences);
|
|
160
375
|
const licensed = await licensedLastLaunch(root);
|
|
161
376
|
|
|
162
377
|
if (action === 'list' && ids.length === 0) {
|
|
@@ -165,65 +380,79 @@ export async function hooksCommand(args) {
|
|
|
165
380
|
JSON.stringify({
|
|
166
381
|
preferences: preferences.status,
|
|
167
382
|
licensedLastLaunch: licensed,
|
|
168
|
-
hooks:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
mode: hook.entry.mode,
|
|
173
|
-
clients: hook.entry.clients,
|
|
174
|
-
description: hook.entry.description[locale],
|
|
175
|
-
})),
|
|
176
|
-
unknown: unknownHookIds(preferences),
|
|
383
|
+
[kind.key === 'hooks' ? 'hooks' : 'servers']: items.map((item) =>
|
|
384
|
+
jsonItem(kind, locale, item),
|
|
385
|
+
),
|
|
386
|
+
unknown: kind.unknown(preferences),
|
|
177
387
|
}),
|
|
178
388
|
);
|
|
179
389
|
return;
|
|
180
390
|
}
|
|
181
391
|
if (args.length === 0 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
182
|
-
const chosen = await runChecklist(locale,
|
|
392
|
+
const chosen = await runChecklist(kind, locale, items);
|
|
183
393
|
if (chosen === null) return console.log('No change.');
|
|
184
394
|
/** @type {Record<string, boolean>} */
|
|
185
|
-
const choices = preferences.status === 'valid' ? { ...preferences.
|
|
186
|
-
|
|
187
|
-
if (chosen[index] !==
|
|
395
|
+
const choices = preferences.status === 'valid' ? { ...preferences[kind.key] } : {};
|
|
396
|
+
items.forEach((item, index) => {
|
|
397
|
+
if (chosen[index] !== item.enabled) choices[item.entry.id] = chosen[index] === true;
|
|
188
398
|
});
|
|
189
|
-
await
|
|
190
|
-
return afterChange(licensed);
|
|
399
|
+
await saveChoices(kind, choices);
|
|
400
|
+
return afterChange(kind, licensed);
|
|
191
401
|
}
|
|
192
|
-
console.log(
|
|
193
|
-
console.log(renderLines(locale,
|
|
402
|
+
console.log(`${kind.title}:`);
|
|
403
|
+
console.log(renderLines(kind, locale, items).join('\n'));
|
|
194
404
|
if (preferences.status === 'invalid')
|
|
195
|
-
console.log(
|
|
405
|
+
console.log(
|
|
406
|
+
`The extensions file is invalid: ${kind.key === 'hooks' ? 'the defaults apply' : 'every server is off'}. Run \`aisac ${kind.key} reset\`.`,
|
|
407
|
+
);
|
|
196
408
|
console.log(
|
|
197
|
-
|
|
409
|
+
`Change with \`aisac ${kind.key} enable <id>\`, \`aisac ${kind.key} disable <id>\` or \`aisac ${kind.key} reset\`.${kind.key === 'mcp' ? ' Details: `aisac mcp info <id>`.' : ''}`,
|
|
198
410
|
);
|
|
199
411
|
if (!licensed) console.log(LICENSE_NOTE);
|
|
200
412
|
return;
|
|
201
413
|
}
|
|
202
414
|
if (action === 'reset' && ids.length === 0) {
|
|
203
|
-
await
|
|
204
|
-
console.log('Hooks reset to the defaults
|
|
205
|
-
return afterChange(licensed);
|
|
415
|
+
await saveChoices(kind, {});
|
|
416
|
+
console.log(`${kind.key === 'hooks' ? 'Hooks' : 'MCP servers'} reset to the defaults.`);
|
|
417
|
+
return afterChange(kind, licensed);
|
|
206
418
|
}
|
|
207
419
|
if ((action === 'enable' || action === 'disable') && ids.length > 0) {
|
|
208
|
-
|
|
209
|
-
if (unknown.length > 0) {
|
|
210
|
-
console.error(
|
|
211
|
-
`aisac: unknown hook id: ${unknown.join(', ')}. Valid: ${hookIds().join(', ')}.`,
|
|
212
|
-
);
|
|
213
|
-
process.exitCode = USAGE_EXIT_CODE;
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
420
|
+
if (!checkIds(kind, ids)) return;
|
|
216
421
|
/** @type {Record<string, boolean>} */
|
|
217
|
-
const choices = preferences.status === 'valid' ? { ...preferences.
|
|
422
|
+
const choices = preferences.status === 'valid' ? { ...preferences[kind.key] } : {};
|
|
218
423
|
for (const id of ids) choices[id] = action === 'enable';
|
|
219
|
-
await
|
|
424
|
+
await saveChoices(kind, choices);
|
|
220
425
|
console.log(`${action === 'enable' ? 'Enabled' : 'Disabled'}: ${ids.join(', ')}.`);
|
|
221
|
-
return afterChange(licensed);
|
|
426
|
+
return afterChange(kind, licensed);
|
|
222
427
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
428
|
+
const [id] = ids;
|
|
429
|
+
if (kind.key === 'mcp' && action === 'info' && ids.length === 1 && id !== undefined) {
|
|
430
|
+
if (!checkIds(kind, ids)) return;
|
|
431
|
+
const entry = /** @type {McpEntry} */ (mcpById(id));
|
|
432
|
+
const info = serverInfo(
|
|
433
|
+
entry,
|
|
434
|
+
locale,
|
|
435
|
+
items.find((item) => item.entry.id === id),
|
|
436
|
+
);
|
|
437
|
+
if (json) console.log(JSON.stringify(info));
|
|
438
|
+
else printInfo(info);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (kind.key === 'mcp' && action === 'login' && ids.length === 1 && id !== undefined && !json) {
|
|
442
|
+
if (!checkIds(kind, ids)) return;
|
|
443
|
+
return codexLogin(/** @type {McpEntry} */ (mcpById(id)));
|
|
444
|
+
}
|
|
445
|
+
usage(kind);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** @param {string[]} args */
|
|
449
|
+
export function hooksCommand(args) {
|
|
450
|
+
return extensionCommand(HOOKS, args);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** @param {string[]} args */
|
|
454
|
+
export function mcpCommand(args) {
|
|
455
|
+
return extensionCommand(MCP, args);
|
|
227
456
|
}
|
|
228
457
|
|
|
229
458
|
/** Offline extension summary for `status` and `doctor`. @param {string} root */
|
|
@@ -234,6 +463,9 @@ export async function extensionsSummary(root) {
|
|
|
234
463
|
hooks: resolveHooks(preferences)
|
|
235
464
|
.filter((hook) => hook.enabled)
|
|
236
465
|
.map((hook) => hook.entry.id),
|
|
237
|
-
|
|
466
|
+
mcp: resolveServers(preferences)
|
|
467
|
+
.filter((server) => server.enabled)
|
|
468
|
+
.map((server) => server.entry.id),
|
|
469
|
+
unknown: [...unknownHookIds(preferences), ...unknownServerIds(preferences)],
|
|
238
470
|
};
|
|
239
471
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { lstat, readFile } from 'node:fs/promises';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { atomicJson, ensureStateRoot } from './state.mjs';
|
|
12
|
-
import { CATALOG_ID, HOOK_CATALOG, hookById } from './extensions-catalog.mjs';
|
|
12
|
+
import { CATALOG_ID, HOOK_CATALOG, MCP_CATALOG, hookById, mcpById } from './extensions-catalog.mjs';
|
|
13
13
|
|
|
14
14
|
export const EXTENSIONS_FILE = 'extensions.json';
|
|
15
15
|
const SCHEMA_VERSION = 1;
|
|
@@ -120,3 +120,32 @@ export function enabledHookIds(preferences) {
|
|
|
120
120
|
export function unknownHookIds(preferences) {
|
|
121
121
|
return Object.keys(preferences.hooks).filter((id) => hookById(id) === undefined);
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Each catalog MCP server with its effective state. Every server defaults to
|
|
126
|
+
* off, and an invalid file switches them all off (FR3).
|
|
127
|
+
* @param {ExtensionPreferences} preferences
|
|
128
|
+
*/
|
|
129
|
+
export function resolveServers(preferences) {
|
|
130
|
+
const chosen = preferences.status === 'valid' ? preferences.mcp : {};
|
|
131
|
+
return MCP_CATALOG.map((entry) => {
|
|
132
|
+
const choice = chosen[entry.id];
|
|
133
|
+
return {
|
|
134
|
+
entry,
|
|
135
|
+
enabled: choice ?? entry.defaultOn,
|
|
136
|
+
source: /** @type {'default' | 'choice'} */ (choice === undefined ? 'default' : 'choice'),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @param {ExtensionPreferences} preferences @returns {string[]} */
|
|
142
|
+
export function enabledServerIds(preferences) {
|
|
143
|
+
return resolveServers(preferences)
|
|
144
|
+
.filter((server) => server.enabled)
|
|
145
|
+
.map((server) => server.entry.id);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Server ids stored in the file that this client's catalog does not know. @param {ExtensionPreferences} preferences */
|
|
149
|
+
export function unknownServerIds(preferences) {
|
|
150
|
+
return Object.keys(preferences.mcp).filter((id) => mcpById(id) === undefined);
|
|
151
|
+
}
|
|
@@ -36,7 +36,7 @@ const MAX_NESTING = 2;
|
|
|
36
36
|
const AISAC_PROGRAMS = new Set(['aisac', 'aibrain']);
|
|
37
37
|
const AISAC_PACKAGE = /^@(?:aisac\/cli|brunobracaioli\/aibrain)(?:@\S*)?$/u;
|
|
38
38
|
const EXTENSION_COMMANDS = new Set(['hooks', 'mcp']);
|
|
39
|
-
const EXTENSION_CHANGES = new Set(['enable', 'disable', 'reset']);
|
|
39
|
+
const EXTENSION_CHANGES = new Set(['enable', 'disable', 'reset', 'login']);
|
|
40
40
|
const GIT_OPTIONS_WITH_VALUE = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace']);
|
|
41
41
|
const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
42
42
|
const GIT_TIMEOUT_MS = 2_000;
|
package/dist/launch.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
writeLaunchMarker,
|
|
20
20
|
} from './engineering-pack.mjs';
|
|
21
21
|
import { prepareGuardrails } from './guardrails-plugin.mjs';
|
|
22
|
+
import { CODEX_MCP_ENV, claudeMcpEntries, mcpLaunchLines, sessionServers } from './mcp-servers.mjs';
|
|
23
|
+
import { enabledServerIds, readPreferences } from './extensions-preferences.mjs';
|
|
22
24
|
import {
|
|
23
25
|
CODEX_ENGINEERING_ENV,
|
|
24
26
|
CODEX_ENGINEERING_SKILLS_ROOT_ENV,
|
|
@@ -140,6 +142,7 @@ function childEnvironment(root) {
|
|
|
140
142
|
delete environment[CODEX_MONITOR_CWD];
|
|
141
143
|
delete environment[CODEX_ENGINEERING_ENV];
|
|
142
144
|
delete environment[CODEX_ENGINEERING_SKILLS_ROOT_ENV];
|
|
145
|
+
delete environment[CODEX_MCP_ENV];
|
|
143
146
|
environment.AIBRAIN_STATE_DIR = resolve(root);
|
|
144
147
|
return environment;
|
|
145
148
|
}
|
|
@@ -426,10 +429,22 @@ function waitForChild(child, directory) {
|
|
|
426
429
|
});
|
|
427
430
|
}
|
|
428
431
|
|
|
432
|
+
/**
|
|
433
|
+
* The catalog servers that join this session: the user's enabled choices from
|
|
434
|
+
* the preferences file. Called only after the license was confirmed.
|
|
435
|
+
* @param {string} root
|
|
436
|
+
* @param {'claude' | 'codex'} client
|
|
437
|
+
* @param {NodeJS.ProcessEnv} environment
|
|
438
|
+
*/
|
|
439
|
+
export async function readSessionServers(root, client, environment) {
|
|
440
|
+
return sessionServers(enabledServerIds(await readPreferences(root)), client, environment);
|
|
441
|
+
}
|
|
442
|
+
|
|
429
443
|
/**
|
|
430
444
|
* @typedef {object} LaunchDependencies
|
|
431
445
|
* @property {typeof prepareEngineeringPack} [preparePack] AISAC Engineering pack loader (E4b)
|
|
432
446
|
* @property {typeof prepareGuardrails} [prepareGuardrails] Guardrail hooks plugin writer (E6a)
|
|
447
|
+
* @property {typeof readSessionServers} [readSessionServers] MCP catalog servers to load (E6c)
|
|
433
448
|
*/
|
|
434
449
|
|
|
435
450
|
/**
|
|
@@ -441,6 +456,7 @@ function waitForChild(child, directory) {
|
|
|
441
456
|
export async function launchClaude(config, root, args, dependencies = {}) {
|
|
442
457
|
const preparePack = dependencies.preparePack ?? prepareEngineeringPack;
|
|
443
458
|
const writeGuardrails = dependencies.prepareGuardrails ?? prepareGuardrails;
|
|
459
|
+
const readServers = dependencies.readSessionServers ?? readSessionServers;
|
|
444
460
|
const validated = assertConfig(config);
|
|
445
461
|
const launchArgs = await parseClaudeLaunchArgs(args, process.cwd());
|
|
446
462
|
assertClaudeArgs(launchArgs.args);
|
|
@@ -556,18 +572,8 @@ export async function launchClaude(config, root, args, dependencies = {}) {
|
|
|
556
572
|
allow: mergedAllow,
|
|
557
573
|
},
|
|
558
574
|
};
|
|
559
|
-
const mcp = {
|
|
560
|
-
mcpServers: {
|
|
561
|
-
[MCP_ALIAS]: {
|
|
562
|
-
type: 'http',
|
|
563
|
-
url: validated.endpoint,
|
|
564
|
-
headers: { Authorization: `Bearer ${validated.token}` },
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
};
|
|
568
575
|
try {
|
|
569
576
|
await privateJson(settingsPath, settings);
|
|
570
|
-
await privateJson(mcpPath, mcp);
|
|
571
577
|
} catch (error) {
|
|
572
578
|
await rm(directory, { recursive: true, force: true });
|
|
573
579
|
throw error;
|
|
@@ -590,6 +596,29 @@ export async function launchClaude(config, root, args, dependencies = {}) {
|
|
|
590
596
|
() => null,
|
|
591
597
|
);
|
|
592
598
|
if (guardrails !== null) process.stderr.write(`${guardrails.line}\n`);
|
|
599
|
+
// Catalog servers need the same confirmed license, and never block the launch.
|
|
600
|
+
const servers =
|
|
601
|
+
engineering.pluginRoot === null
|
|
602
|
+
? null
|
|
603
|
+
: await readServers(root, 'claude', process.env).catch(() => null);
|
|
604
|
+
for (const line of servers === null ? [] : mcpLaunchLines(servers, 'claude'))
|
|
605
|
+
process.stderr.write(`${line}\n`);
|
|
606
|
+
const mcp = {
|
|
607
|
+
mcpServers: {
|
|
608
|
+
...(servers === null ? {} : claudeMcpEntries(servers.ready)),
|
|
609
|
+
[MCP_ALIAS]: {
|
|
610
|
+
type: 'http',
|
|
611
|
+
url: validated.endpoint,
|
|
612
|
+
headers: { Authorization: `Bearer ${validated.token}` },
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
try {
|
|
617
|
+
await privateJson(mcpPath, mcp);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
await rm(directory, { recursive: true, force: true });
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
593
622
|
const environment = childEnvironment(root);
|
|
594
623
|
environment.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS = '1';
|
|
595
624
|
/** @type {import('node:child_process').ChildProcess} */
|
|
@@ -630,6 +659,7 @@ export async function launchClaude(config, root, args, dependencies = {}) {
|
|
|
630
659
|
*/
|
|
631
660
|
export async function launchCodex(config, root, args, dependencies = {}) {
|
|
632
661
|
const preparePack = dependencies.preparePack ?? prepareEngineeringPack;
|
|
662
|
+
const readServers = dependencies.readSessionServers ?? readSessionServers;
|
|
633
663
|
const validated = assertConfig(config);
|
|
634
664
|
assertCodexArgs(args);
|
|
635
665
|
const captureLockPlatform = captureLockPlatformCheck();
|
|
@@ -691,8 +721,15 @@ export async function launchCodex(config, root, args, dependencies = {}) {
|
|
|
691
721
|
hostSupported: supportsCodexEngineering(compatibility.version),
|
|
692
722
|
}).catch(() => ({ pluginRoot: null, codex: null, line: null }));
|
|
693
723
|
if (engineering.line !== null) process.stderr.write(`${engineering.line}\n`);
|
|
694
|
-
if (engineering.codex != null)
|
|
724
|
+
if (engineering.codex != null) {
|
|
695
725
|
environment[CODEX_ENGINEERING_ENV] = JSON.stringify(engineering.codex);
|
|
726
|
+
// Catalog servers need the license the pack confirmed (spec extensions FR6).
|
|
727
|
+
const servers = await readServers(root, 'codex', environment).catch(() => null);
|
|
728
|
+
for (const line of servers === null ? [] : mcpLaunchLines(servers, 'codex'))
|
|
729
|
+
process.stderr.write(`${line}\n`);
|
|
730
|
+
if (servers !== null && servers.ready.length > 0)
|
|
731
|
+
environment[CODEX_MCP_ENV] = JSON.stringify(servers.ready.map((entry) => entry.id));
|
|
732
|
+
}
|
|
696
733
|
environment.AI_BRAIN_MCP_SERVER = MCP_ALIAS;
|
|
697
734
|
environment.CODEX_BIN = executable;
|
|
698
735
|
environment.CODEX_MONITOR_COMMAND = monitor;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Session configuration for the MCP catalog (spec aisac-engineering-extensions
|
|
4
|
+
* FR5.6, FR6, FR7): entries for Claude Code's `--mcp-config` file, and
|
|
5
|
+
* `-c mcp_servers.*` flags for the app-server that `aisac codex` owns. Only
|
|
6
|
+
* catalog ids reach this module from outside; every URL, command and argument
|
|
7
|
+
* comes from the catalog in this package. AISAC never reads or stores a
|
|
8
|
+
* third-party credential: logins live in the host's own store, and a token
|
|
9
|
+
* variable is only named, never read here.
|
|
10
|
+
*/
|
|
11
|
+
import { MCP_SERVER_PREFIX, mcpById } from './extensions-catalog.mjs';
|
|
12
|
+
import { tomlBasicString } from './codex-engineering.mjs';
|
|
13
|
+
|
|
14
|
+
/** @typedef {import('./extensions-catalog.mjs').McpEntry} McpEntry */
|
|
15
|
+
/** @typedef {'claude' | 'codex'} McpClient */
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} SessionServers
|
|
18
|
+
* @property {McpEntry[]} ready Servers that join the session.
|
|
19
|
+
* @property {{ id: string, variable: string }[]} missingEnv Skipped: the token variable is unset.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The launcher hands the Codex monitor the ready ids in this variable. */
|
|
23
|
+
export const CODEX_MCP_ENV = 'AISAC_EXTENSIONS_CODEX_MCP';
|
|
24
|
+
const STDIO_STARTUP_TIMEOUT_SEC = 60;
|
|
25
|
+
const MAX_IDS = 16;
|
|
26
|
+
const MAX_IDS_BYTES = 1024;
|
|
27
|
+
|
|
28
|
+
/** @param {string} id */
|
|
29
|
+
export function serverName(id) {
|
|
30
|
+
return `${MCP_SERVER_PREFIX}${id}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Catalog entries for these ids that support the client. A token server whose
|
|
35
|
+
* variable is unset is skipped with a reason instead of failing in the host.
|
|
36
|
+
* @param {readonly string[]} ids
|
|
37
|
+
* @param {McpClient} client
|
|
38
|
+
* @param {NodeJS.ProcessEnv} environment
|
|
39
|
+
* @returns {SessionServers}
|
|
40
|
+
*/
|
|
41
|
+
export function sessionServers(ids, client, environment) {
|
|
42
|
+
/** @type {SessionServers} */
|
|
43
|
+
const result = { ready: [], missingEnv: [] };
|
|
44
|
+
for (const id of new Set(ids)) {
|
|
45
|
+
const entry = mcpById(id);
|
|
46
|
+
if (entry === undefined || !entry.clients.includes(client)) continue;
|
|
47
|
+
const variable = entry.transport.type === 'http' ? entry.transport.bearerEnv : undefined;
|
|
48
|
+
if (variable !== undefined && !environment[variable]?.trim()) {
|
|
49
|
+
result.missingEnv.push({ id, variable });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
result.ready.push(entry);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `--mcp-config` entries. A token header uses Claude Code's `${VAR}` expansion,
|
|
59
|
+
* so the token never reaches the session file.
|
|
60
|
+
* @param {readonly McpEntry[]} entries
|
|
61
|
+
* @returns {Record<string, Record<string, unknown>>}
|
|
62
|
+
*/
|
|
63
|
+
export function claudeMcpEntries(entries) {
|
|
64
|
+
/** @type {Record<string, Record<string, unknown>>} */
|
|
65
|
+
const servers = {};
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const { transport } = entry;
|
|
68
|
+
servers[serverName(entry.id)] =
|
|
69
|
+
transport.type === 'stdio'
|
|
70
|
+
? { type: 'stdio', command: transport.command, args: [...transport.args] }
|
|
71
|
+
: {
|
|
72
|
+
type: 'http',
|
|
73
|
+
url: transport.url,
|
|
74
|
+
...(transport.bearerEnv === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { headers: { Authorization: `Bearer \${${transport.bearerEnv}}` } }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return servers;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Session-only `-c` overrides for the owned Codex app-server.
|
|
84
|
+
* @param {readonly McpEntry[]} entries
|
|
85
|
+
* @returns {string[]}
|
|
86
|
+
*/
|
|
87
|
+
export function codexMcpConfigArgs(entries) {
|
|
88
|
+
return entries.flatMap((entry) => {
|
|
89
|
+
const key = `mcp_servers.${serverName(entry.id)}`;
|
|
90
|
+
const { transport } = entry;
|
|
91
|
+
if (transport.type === 'stdio')
|
|
92
|
+
return [
|
|
93
|
+
'-c',
|
|
94
|
+
`${key}.command=${tomlBasicString(transport.command)}`,
|
|
95
|
+
'-c',
|
|
96
|
+
`${key}.args=[${transport.args.map(tomlBasicString).join(',')}]`,
|
|
97
|
+
'-c',
|
|
98
|
+
`${key}.startup_timeout_sec=${STDIO_STARTUP_TIMEOUT_SEC}`,
|
|
99
|
+
];
|
|
100
|
+
return [
|
|
101
|
+
'-c',
|
|
102
|
+
`${key}.url=${tomlBasicString(transport.url)}`,
|
|
103
|
+
...(transport.bearerEnv === undefined
|
|
104
|
+
? []
|
|
105
|
+
: ['-c', `${key}.bearer_token_env_var=${tomlBasicString(transport.bearerEnv)}`]),
|
|
106
|
+
];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The monitor's copy of the ready ids: a JSON array of catalog ids for Codex.
|
|
112
|
+
* Anything else throws, and the monitor then starts without catalog servers.
|
|
113
|
+
* @param {string | undefined} raw
|
|
114
|
+
* @returns {McpEntry[]}
|
|
115
|
+
*/
|
|
116
|
+
export function parseCodexMcp(raw) {
|
|
117
|
+
if (raw === undefined || raw === '') return [];
|
|
118
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_IDS_BYTES) throw new Error('codex_mcp_too_large');
|
|
119
|
+
const value = JSON.parse(raw);
|
|
120
|
+
if (!Array.isArray(value) || value.length > MAX_IDS) throw new Error('codex_mcp_invalid');
|
|
121
|
+
return value.map((id) => {
|
|
122
|
+
const entry = typeof id === 'string' ? mcpById(id) : undefined;
|
|
123
|
+
if (entry === undefined || !entry.clients.includes('codex'))
|
|
124
|
+
throw new Error('codex_mcp_invalid');
|
|
125
|
+
return entry;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Arguments for `codex` that log in to one catalog server with the same session
|
|
131
|
+
* flags `aisac codex` passes, so the next session finds the stored login.
|
|
132
|
+
* Spike Q5 (2026-11-03, Codex 0.164.0) measured this path end to end.
|
|
133
|
+
* @param {McpEntry} entry
|
|
134
|
+
*/
|
|
135
|
+
export function codexLoginArgs(entry) {
|
|
136
|
+
return [...codexMcpConfigArgs([entry]), 'mcp', 'login', serverName(entry.id)];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @param {McpEntry} entry */
|
|
140
|
+
export function supportsHostLogin(entry) {
|
|
141
|
+
return entry.login === 'host-oauth' || entry.login === 'optional-oauth';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Launch lines: counts and names only, never a value.
|
|
146
|
+
* @param {SessionServers} servers
|
|
147
|
+
* @param {McpClient} client
|
|
148
|
+
* @returns {string[]}
|
|
149
|
+
*/
|
|
150
|
+
export function mcpLaunchLines(servers, client) {
|
|
151
|
+
/** @type {string[]} */
|
|
152
|
+
const lines = [];
|
|
153
|
+
const count = servers.ready.length;
|
|
154
|
+
if (count > 0) {
|
|
155
|
+
const login = client === 'claude' ? 'log in with /mcp' : 'log in with `aisac mcp login <id>`';
|
|
156
|
+
lines.push(
|
|
157
|
+
`AISAC Engineering: ${count} MCP server${count === 1 ? '' : 's'} on (${login}; aisac mcp to change them).`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
for (const skipped of servers.missingEnv)
|
|
161
|
+
lines.push(
|
|
162
|
+
`AISAC Engineering: ${serverName(skipped.id)} skipped: set ${skipped.variable} (aisac mcp info ${skipped.id}).`,
|
|
163
|
+
);
|
|
164
|
+
return lines;
|
|
165
|
+
}
|
package/dist/monitor.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
codexEngineeringConfigArgs,
|
|
19
19
|
parseCodexEngineering,
|
|
20
20
|
} from './codex-engineering.mjs';
|
|
21
|
+
import { CODEX_MCP_ENV, codexMcpConfigArgs, parseCodexMcp } from './mcp-servers.mjs';
|
|
21
22
|
|
|
22
23
|
const root = dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const endpoint = process.env.AIBRAIN_MCP_URL?.trim();
|
|
@@ -49,6 +50,15 @@ try {
|
|
|
49
50
|
} catch {
|
|
50
51
|
engineering = null;
|
|
51
52
|
}
|
|
53
|
+
// MCP catalog servers (E6c) follow the pack: no license, no servers. A malformed
|
|
54
|
+
// value starts the session without them.
|
|
55
|
+
/** @type {import('./mcp-servers.mjs').McpEntry[]} */
|
|
56
|
+
let catalogServers = [];
|
|
57
|
+
try {
|
|
58
|
+
catalogServers = engineering === null ? [] : parseCodexMcp(process.env[CODEX_MCP_ENV]);
|
|
59
|
+
} catch {
|
|
60
|
+
catalogServers = [];
|
|
61
|
+
}
|
|
52
62
|
const policyText = process.env.CODEX_CONTEXT_POLICY_JSON;
|
|
53
63
|
let policy;
|
|
54
64
|
if (policyText === undefined) {
|
|
@@ -108,6 +118,7 @@ delete env[CODEX_LAUNCH_ENVELOPE];
|
|
|
108
118
|
delete env[CODEX_APP_SERVER_PROFILE];
|
|
109
119
|
delete env[CODEX_ENGINEERING_ENV];
|
|
110
120
|
delete env[CODEX_ENGINEERING_SKILLS_ROOT_ENV];
|
|
121
|
+
delete env[CODEX_MCP_ENV];
|
|
111
122
|
if (engineering !== null) env[CODEX_ENGINEERING_SKILLS_ROOT_ENV] = engineering.skillsRoot;
|
|
112
123
|
if (launchEnvelope?.profile !== undefined) env[CODEX_APP_SERVER_PROFILE] = launchEnvelope.profile;
|
|
113
124
|
if (policy !== null) env.CODEX_CONTEXT_POLICY_JSON = JSON.stringify(policy);
|
|
@@ -130,6 +141,7 @@ const child = spawn(
|
|
|
130
141
|
'mcp_servers.aibrain_context.bearer_token_env_var="CODEX_RECOVERY_MCP_TOKEN"',
|
|
131
142
|
...(launchEnvelope === null ? [] : ownedAppServerConfigArgs(launchEnvelope)),
|
|
132
143
|
...(engineering === null ? [] : codexEngineeringConfigArgs(engineering)),
|
|
144
|
+
...codexMcpConfigArgs(catalogServers),
|
|
133
145
|
],
|
|
134
146
|
{ cwd: process.cwd(), env, stdio: 'inherit' },
|
|
135
147
|
);
|
package/dist/package.json
CHANGED
package/package.json
CHANGED