Skip to content

Commit ab2e41f

Browse files
pacphiclaude
andauthored
fix(codex): multi-line TOML args, and sync that can't leave a host CLI broken (#232)
* fix(codex): read multi-line TOML args and flag non-executable hosts Codex (toml_edit) and `aqe platform setup codex` write MCP `args` as multi-line arrays. Three readers accepted only single-line arrays, so: - rufloCodexMcpStatus saw args=null, reported the canonical `ak x ruflo-mcp` entry as the legacy launcher, and sync re-ran the migration on every invocation; - the AQE embedding TOML editor rejected the project .codex/config.toml as "unsupported AQE arguments encoding", leaving a permanent projection conflict after sync; - codexMcpTopology missed args for recursive-codex / legacy-ruflo tables, hiding duplicate-transport repairs. A shared string-array reader in codex-toml-safety.mjs now handles both forms; comments or non-string elements inside the array stay unsupported. `ak status` also reported an npm-installed host as healthy from its package.json alone. A launcher that fails `--version` (e.g. codex missing @openai/codex-darwin-arm64) now shows as installed but not executable with a reinstall fix, which also explains downstream `codex mcp` failures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sync): verify host CLIs after upgrade and repair broken npm hosts `ak sync`'s versions step upgraded @openai/codex 0.156.0 -> 0.156.1 about eight minutes after OpenAI published it. npm resolved the platform alias (@openai/codex-darwin-arm64 -> @openai/codex@0.156.1-darwin-arm64, published ~5 minutes after the main version), failed to install it, and exited 0: npm silently drops a failed optional dependency. heal.upgradePackage trusted the exit code, reported "upgraded", and left `codex` unable to start, which then broke every later `codex mcp` operation in sync. - installGlobalCli (npm-global-install.mjs): install, prove `bin --version`, and retry once with --prefer-online when the CLI cannot start. installHost and heal.upgradePackage (for host packages, via sync's hostUpgradeOptions) share it, so a broken upgrade is reported and usually repaired in place. - sync `hosts` step: also reinstalls an npm-owned host whose CLI cannot start (external installs untouched), and now runs before the Codex MCP and provider steps that shell out to the host CLIs. - status: the not-executable host row's fix is now `sync reinstalls <pkg>`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 9f0ffad commit ab2e41f

14 files changed

Lines changed: 352 additions & 88 deletions

‎docs/TROUBLESHOOTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ ak sync # apply it
3636
| Codex receives automatic deja-vu recall while Agentic Kit says MCP mode | A user-owned Codex deja-vu plugin can contribute session/per-prompt/precompaction hooks independently of Agentic Kit's mode | disable/remove that plugin through Codex if MCP-only behavior is required. `ak sync` preserves external plugins and reports the effective auto surface without claiming a fix |
3737
| `--purge-deja-vu-data` refuses the index path | The observed path is broad, relative, outside an approved data root, overlaps config/transcript sources, or crosses a symlink | move/reconfigure the derived index safely, run `deja doctor --offline`, then retry. Never bypass the guard by deleting a host transcript root |
3838
| Just upgraded ruflo/agentic-qe (`npm i -g …`) and things feel off | Upgrades re-resolve dependencies: native SQLite bindings and the aidefence package get dropped, and ruflo's helper auto-refresh regenerates the statusline without the footer | `ak sync` (this is its main job) |
39+
| `status` shows a host `installed but not executable` | npm exits 0 even when an optional dependency fails, so a package can be recorded without its platform binary. Codex ships its binary as per-platform versions (for example `@openai/codex-darwin-arm64`) published minutes after the main version, so an upgrade in that window can leave `codex` unable to start | `ak sync` reinstalls an npm-owned host and verifies it starts; upgrades and installs already retry once with `--prefer-online`. An external (mise/native/brew) install is reinstalled with its own tool |
3940
| `status` shows `natives … WASM fallback` | agentdb resolved a non-native better-sqlite3 — on this path **memory writes can silently vanish**. Common causes are npm ≥11.17 blocking install scripts during upgrades, or a stale better-sqlite3 ≤12.9 pin on Node 26 | `ak sync` selects a Node-compatible release and installs the native binding |
4041
| `status` shows `aidefence missing` | ruflo ≥3.28 stopped shipping `@claude-flow/aidefence` but `ruflo security defend` still imports it — injection defense is silently non-functional ([ruvnet/ruflo#2670](https://github.com/ruvnet/ruflo/issues/2670)) | `ak sync` reinstalls it; `ak x verify security` proves defend works (exit 1=threat / 0=clean) |
4142
| `status` shows oversized RVF store(s) | A runaway append after a hard exit grew a `.rvf` past the 2 GB cap (seen at ~277 GB once) | `ak sync` quarantines the oversized store; agentic-qe rebuilds it |

‎src/commands/status/sections/hosts.mjs‎

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,48 @@
11
// hosts (install-if-missing) — cheap: file read + `which`, no network.
22
// An enabled host that is entirely absent is installable by sync; an external
3-
// install (mise/native/brew) is reported but never touched.
4-
import { HOSTS, hostInstallState, hostAuthState } from '../../../lib/providers.mjs';
3+
// install (mise/native/brew) is reported but never touched. An npm install is
4+
// also launched once (`--version`): its package.json can outlive the binary.
5+
import { HOSTS, hostInstallState, hostAuthState, hostExecutable } from '../../../lib/providers.mjs';
56
import { row } from '../row.mjs';
67

8+
const DEFAULT_DEPS = { installState: hostInstallState, executable: hostExecutable, authState: hostAuthState };
9+
10+
// Install row + auth row for a host that is on disk.
11+
async function installedHostRows(h, st, primary, deps) {
12+
const label = `${h.id} ${st.version ?? ''} (${st.method}${st.method === 'external' ? ' — self-managed' : ''})`;
13+
const launch = st.method === 'npm' ? await deps.executable(h) : { ok: true, detail: null };
14+
const install = launch.ok ? row('hosts', 'ok', label)
15+
: row('hosts', primary ? 'fail' : 'warn', `${label} installed but not executable: ${launch.detail}`,
16+
`sync reinstalls ${h.pkg}`);
17+
// auth mode (billing axis): oauth/subscription ($0) vs metered api-key.
18+
// A distinct row so `ak status --json` (and the dashboard) can badge it.
19+
const auth = deps.authState(h.id, { present: true });
20+
const billing = auth.billing === 'subscription' ? 'subscription, $0'
21+
: auth.billing === 'metered' ? 'metered' : auth.billing;
22+
return [install, row('hosts', auth.mode === 'none' ? 'warn' : 'ok',
23+
`${h.id} auth: ${auth.mode} (${billing})${auth.source ? ` · ${auth.source}` : ''}${auth.note ? ` — ${auth.note}` : ''}`,
24+
auth.mode === 'none' ? `${h.id} login` : null)];
25+
}
26+
727
export default {
828
id: 'hosts',
9-
async collect({ cfg, integrationFacts }) {
29+
/** @param {{ cfg: any, integrationFacts: any, hostDeps?: Partial<typeof DEFAULT_DEPS> }} ctx */
30+
async collect({ cfg, integrationFacts, hostDeps = {} }) {
31+
const deps = { ...DEFAULT_DEPS, ...hostDeps };
1032
const rows = [];
1133
try {
1234
// primary host absent = fail (nothing can drive); alternate absent = warn.
1335
const primaryHost = cfg.routing?.primaryHost ?? 'claude';
1436
for (const h of HOSTS) {
1537
if (!cfg.integrations.hosts[h.id]) continue;
16-
const detected = integrationFacts.hosts[h.id];
17-
if (detected?.present === false) {
18-
rows.push(row('hosts', h.id === primaryHost ? 'fail' : 'warn',
19-
`${h.id} enabled but not installed${h.id === primaryHost ? ' (primary)' : ''}`, `sync installs ${h.pkg}`));
20-
continue;
21-
}
22-
const st = await hostInstallState(h);
38+
const primary = h.id === primaryHost;
39+
const st = integrationFacts.hosts[h.id]?.present === false
40+
? { method: 'absent', version: null } : await deps.installState(h);
2341
if (st.method === 'absent') {
24-
rows.push(row('hosts', h.id === primaryHost ? 'fail' : 'warn',
25-
`${h.id} enabled but not installed${h.id === primaryHost ? ' (primary)' : ''}`, `sync installs ${h.pkg}`));
42+
rows.push(row('hosts', primary ? 'fail' : 'warn',
43+
`${h.id} enabled but not installed${primary ? ' (primary)' : ''}`, `sync installs ${h.pkg}`));
2644
} else {
27-
rows.push(row('hosts', 'ok', `${h.id} ${st.version ?? ''} (${st.method}${st.method === 'external' ? ' — self-managed' : ''})`));
28-
// auth mode (billing axis): oauth/subscription ($0) vs metered api-key.
29-
// A distinct row so `ak status --json` (and the dashboard) can badge it.
30-
const auth = hostAuthState(h.id, { present: true });
31-
const billing = auth.billing === 'subscription' ? 'subscription, $0'
32-
: auth.billing === 'metered' ? 'metered' : auth.billing;
33-
rows.push(row('hosts', auth.mode === 'none' ? 'warn' : 'ok',
34-
`${h.id} auth: ${auth.mode} (${billing})${auth.source ? ` · ${auth.source}` : ''}${auth.note ? ` — ${auth.note}` : ''}`,
35-
auth.mode === 'none' ? `${h.id} login` : null));
45+
rows.push(...await installedHostRows(h, st, primary, deps));
3646
}
3747
}
3848
} catch (e) {

‎src/commands/sync.mjs‎

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { companionLifecycleFor } from '../lib/adapters/companion-lifecycle-regis
1919
import { renderApplyReport } from '../lib/adapters/lifecycle-render.mjs';
2020
import { listDaemons, staleDaemons, reap } from '../lib/daemons.mjs';
2121
import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
22-
import { commandHosts, hostInstallState, installHost, convergeProviderStack, guidanceContext, reportRetiredRouteChanges } from '../lib/providers.mjs';
22+
import { HOSTS, commandHosts, hostInstallState, hostExecutable, installHost, convergeProviderStack, guidanceContext, reportRetiredRouteChanges } from '../lib/providers.mjs';
2323
import { driftReport, selfDrift } from '../lib/versions.mjs';
2424
import { drift as ruvnetBrainDrift } from '../lib/ruvnet-brain.mjs';
2525
import { RUVECTOR_PKG, managed as ruvectorManaged } from '../lib/ruvector.mjs';
@@ -126,6 +126,14 @@ Examples:
126126
// report, step, state}. `state` carries the two cross-step signals
127127
// (`dejaVuApplyFailed`, `aqeRouterApplyFailure`) the final convergence check
128128
// needs — the only state that survives past its own step.
129+
const HOST_LIFECYCLE = { installState: hostInstallState, executable: hostExecutable, install: installHost };
130+
131+
/** A host package's CLI must start after an upgrade, not just extract. */
132+
export function hostUpgradeOptions(pkg) {
133+
const host = HOSTS.find((h) => h.pkg === pkg);
134+
return host ? { bin: host.bin } : {};
135+
}
136+
129137
export const SYNC_STEPS = [
130138
{
131139
id: 'agent-browser',
@@ -141,6 +149,24 @@ export const SYNC_STEPS = [
141149
return result;
142150
},
143151
},
152+
// hosts: install any ENABLED host that is entirely absent, and reinstall an
153+
// npm-owned host whose CLI cannot start (npm exits 0 after dropping a failed
154+
// optional platform binary). External installs are never touched; updates
155+
// ride the `versions` step via driftReport. Runs before the Codex MCP and
156+
// provider steps, which shell out to the host CLIs.
157+
{
158+
id: 'hosts',
159+
when: (subs) => subs.has('hosts'),
160+
run: async (ctx) => {
161+
const { installState, executable, install } = { ...HOST_LIFECYCLE, ...ctx.hostLifecycle };
162+
for (const h of commandHosts()) {
163+
if (!ctx.cfg.integrations.hosts[h.id]) continue;
164+
const { method } = await installState(h);
165+
if (method === 'absent') await ctx.step(`install ${h.id}`, () => install(h.id));
166+
else if (method === 'npm' && !(await executable(h)).ok) await ctx.step(`repair ${h.id}`, () => install(h.id));
167+
}
168+
},
169+
},
144170
{
145171
id: 'codex-mcp-repair',
146172
when: (subs) => subs.has('codex-mcp'),
@@ -170,7 +196,7 @@ export const SYNC_STEPS = [
170196
// No force here: the pre-plan refresh in run() already ran for every
171197
// non-dry-run, non-no-upgrade sync, so this read hits that fresh cache.
172198
for (const d of await driftReport()) {
173-
if (d.outdated || !d.installed) await ctx.step(`upgrade ${d.pkg}`, () => heal.upgradePackage(d.pkg));
199+
if (d.outdated || !d.installed) await ctx.step(`upgrade ${d.pkg}`, () => heal.upgradePackage(d.pkg, hostUpgradeOptions(d.pkg)));
174200
}
175201
},
176202
},
@@ -283,19 +309,6 @@ export const SYNC_STEPS = [
283309
}
284310
},
285311
},
286-
// hosts: install any ENABLED host that is entirely absent (updates to
287-
// npm-managed hosts ride the `versions` step above via driftReport).
288-
{
289-
id: 'hosts',
290-
when: (subs) => subs.has('hosts'),
291-
run: async (ctx) => {
292-
for (const h of commandHosts()) {
293-
if (!ctx.cfg.integrations.hosts[h.id]) continue;
294-
if ((await hostInstallState(h)).method !== 'absent') continue;
295-
await ctx.step(`install ${h.id}`, () => installHost(h.id));
296-
}
297-
},
298-
},
299312
// Managed companion convergence is independent from host lifecycle
300313
// adapters. The adapter owns exact package/target/index ordering and mutates
301314
// only its in-memory ownership ledger; this command owns persistence. Save a

‎src/lib/aqe-embedding-toml.mjs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Deliberately narrow TOML editor: unsupported encodings remain user-owned.
2-
import { inspectCodexTomlStructure, isTomlTableLine } from './codex-toml-safety.mjs';
2+
import { inspectCodexTomlStructure, isTomlTableLine, tomlStringArrayAt } from './codex-toml-safety.mjs';
33
import { recognizedAqeTransport, parseEmbeddingJson } from './aqe-embedding-transport.mjs';
44
const BASE = 'mcp_servers.agentic-qe';
55
const ENV = `${BASE}.env`;
@@ -32,18 +32,18 @@ function scalar(text, key) {
3232
return parseEmbeddingJson(match[1]);
3333
}
3434

35-
function transportAssignment(text, transport) {
35+
function transportAssignment(text, transport, rest) {
3636
if (/^env\s*=/.test(text)) throw new Error('inline AQE environment requires manual embedding configuration');
3737
if (/^command\s*=/.test(text)) {
3838
if (transport.command !== null) throw new Error('duplicate AQE command');
3939
transport.command = scalar(text, 'command');
4040
}
4141
if (/^args\s*=/.test(text)) {
4242
if (transport.args !== null) throw new Error('duplicate AQE arguments');
43-
const match = /^args\s*=\s*(\[[^\n]*\])\s*(?:#.*)?$/.exec(text);
44-
if (!match) throw new Error('unsupported AQE arguments encoding');
45-
transport.args = parseEmbeddingJson(match[1]);
46-
if (!Array.isArray(transport.args)) throw new Error('unsupported AQE arguments shape');
43+
// `rest` starts at this line so a multi-line array is read whole.
44+
const args = tomlStringArrayAt(rest, 'args');
45+
if (!args?.value) throw new Error('unsupported AQE arguments encoding');
46+
transport.args = args.value;
4747
}
4848
}
4949

@@ -73,7 +73,7 @@ export function aqeTomlEnvironment(source) {
7373
if (table === 'unrelated') continue;
7474
// Dotted/quoted keys can alias a managed table: refuse rather than guessing.
7575
if (!/^[A-Za-z0-9_-]+\s*=/.test(text)) throw new Error('dotted or quoted TOML assignments require manual embedding configuration');
76-
if (table === BASE) transportAssignment(text, transport);
76+
if (table === BASE) transportAssignment(text, transport, source.slice(line.start));
7777
if (table === ENV && new RegExp(`^${KEY}\\s*=`).test(text)) {
7878
if (endpoint) throw new Error('duplicate AQE endpoint');
7979
endpoint = { ...line, value: scalar(text, KEY) };

‎src/lib/codex-toml-safety.mjs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,30 @@ export function inspectCodexTomlStructure(source) {
197197
}
198198

199199
export const isTomlTableLine = (line) => TABLE.test(line) || ARRAY_TABLE.test(line);
200+
201+
// Arrays of basic strings, single- or multi-line (toml_edit, which Codex and
202+
// AQE write through, puts each element on its own line with a trailing comma).
203+
// Comments and non-string elements inside the array stay unsupported.
204+
const ARRAY_STRING = '"(?:[^"\\\\\\r\\n]|\\\\.)*"';
205+
const STRING_ARRAY = `\\[\\s*(?:${ARRAY_STRING}(?:\\s*,\\s*${ARRAY_STRING})*\\s*,?\\s*)?\\]`;
206+
207+
function parseStringArray(value) {
208+
try {
209+
const parsed = JSON.parse(value.replace(/,\s*\]$/, ']'));
210+
return Array.isArray(parsed) && parsed.every((entry) => typeof entry === 'string') ? parsed : null;
211+
} catch { return null; }
212+
}
213+
214+
/** Read `key = [..strings..]` at the very start of `text`. Returns
215+
* { value, text } where value is null for an unsupported encoding, or null when
216+
* `text` does not start with that assignment. */
217+
export function tomlStringArrayAt(text, key) {
218+
const match = new RegExp(`^[\\t ]*${key}[\\t ]*=[\\t ]*(${STRING_ARRAY})[\\t ]*(?:#[^\\r\\n]*)?(?=\\r?\\n|$)`).exec(text);
219+
return match ? { value: parseStringArray(match[1]), text: match[0] } : null;
220+
}
221+
222+
/** Find the first `key = [..strings..]` line in a table body (see tomlStringArrayAt). */
223+
export function findTomlStringArray(body, key) {
224+
const match = new RegExp(`^[\\t ]*${key}[\\t ]*=[\\t ]*(${STRING_ARRAY})[\\t ]*(?:#[^\\r\\n]*)?$`, 'm').exec(body);
225+
return match ? { value: parseStringArray(match[1]), text: match[0] } : null;
226+
}

‎src/lib/heal.mjs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { KIT_PKG } from './versions.mjs';
1414
import { scanRvf, quarantine } from './rvf.mjs';
1515
import { INSTALL_SPEC, INSTALL_ARGS, RELEASE_ASSET as RB_RELEASE_ASSET, NIGHTLY_LABEL as RB_NIGHTLY_LABEL, nightlyAgentPlist as rbNightlyPlist, present as rbPresent, latestRelease as rbLatestRelease, recordInstalledRelease as rbRecord } from './ruvnet-brain.mjs';
1616
import { PKG as ADB_PKG, present as adbPresent, coherence as adbCoherence } from './agentdb.mjs';
17-
import { globalInstallArgs } from './npm-global-install.mjs';
17+
import { globalInstallArgs, installGlobalCli } from './npm-global-install.mjs';
1818

1919
// NB: `--allow-scripts` is rejected for project-scoped installs (EALLOWSCRIPTS,
2020
// npm >=11.17) — it is a global-install flag only. Plain installs still get
@@ -153,9 +153,16 @@ export function healRvf(projectAqeDir) {
153153
return { ok: true, detail: removed.length ? `quarantined: ${removed.join(', ')}` : 'healthy' };
154154
}
155155

156-
/** Upgrade a global package to latest (with allow-scripts). */
157-
export async function upgradePackage(pkg) {
158-
const r = await run('npm', globalInstallArgs(`${pkg}@latest`),
156+
/** Upgrade a global package to latest (with allow-scripts). With `bin`, the
157+
* package's CLI must also start afterwards (see installGlobalCli).
158+
* @param {string} pkg
159+
* @param {{ bin?: string|null, runner?: typeof run, sleep?: (ms: number) => Promise<void> }} [opts] */
160+
export async function upgradePackage(pkg, { bin = null, runner = run, sleep } = {}) {
161+
if (bin) {
162+
const r = await installGlobalCli(`${pkg}@latest`, bin, { runner, sleep });
163+
return { ok: r.ok, detail: r.ok ? (r.retried ? 'upgraded (missing platform files repaired on retry)' : 'upgraded') : r.detail };
164+
}
165+
const r = await runner('npm', globalInstallArgs(`${pkg}@latest`),
159166
{ timeout: 600_000 });
160167
return { ok: r.code === 0, detail: r.code === 0 ? 'upgraded' : r.stderr.split('\n').slice(-3).join(' ') };
161168
}

‎src/lib/mcp.mjs‎

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { writeFileWithBackup } from './file-write.mjs';
1313
import { managedAgentBrowserEnv } from './agent-browser.mjs';
1414
import { isRufloMcpTransport } from './ruflo-mcp-transport.mjs';
1515
import { retiredCodexTransport } from './host-alignment.mjs';
16+
import { findTomlStringArray } from './codex-toml-safety.mjs';
1617

1718
/** Enumerate MCP tool names from the installed package's mcp-tools modules,
1819
* grouped by name prefix (family). Returns Map<family, string[]>. */
@@ -166,14 +167,6 @@ function tomlString(value) {
166167
try { return JSON.parse(value); } catch { return null; }
167168
}
168169

169-
function tomlStringArray(value) {
170-
if (!value) return null;
171-
try {
172-
const parsed = JSON.parse(value);
173-
return Array.isArray(parsed) && parsed.every((entry) => typeof entry === 'string') ? parsed : null;
174-
} catch { return null; }
175-
}
176-
177170
const sameArgs = (left, right) => JSON.stringify(left) === JSON.stringify(right);
178171
const fingerprint = (value) => createHash('sha256').update(value).digest('hex');
179172

@@ -184,7 +177,7 @@ function mcpTableName(table) {
184177

185178
/** Read the bounded base-table sections behind Codex MCP registrations. This
186179
* is deliberately not a general TOML parser: only a base
187-
* `[mcp_servers.<name>]` table with single-line string command/args facts is
180+
* `[mcp_servers.<name>]` table with string command and string-array args facts is
188181
* observed. Extra fields or child tables preserve ownership, except for the
189182
* exact kit-managed browser environment on the retired Ruflo transport. */
190183
function codexMcpSections(file, scope) {
@@ -203,10 +196,13 @@ function codexMcpSections(file, scope) {
203196
const bodyEnd = headers[index + 1]?.index ?? source.length;
204197
const body = source.slice(bodyStart, bodyEnd);
205198
const command = tomlString(/^\s*command\s*=\s*("(?:[^"\\]|\\.)*")\s*$/m.exec(body)?.[1]);
206-
const args = tomlStringArray(/^\s*args\s*=\s*(\[[^\n]*\])\s*$/m.exec(body)?.[1]);
199+
const argsAssignment = findTomlStringArray(body, 'args');
200+
const args = argsAssignment?.value ?? null;
207201
const enabledValue = /^\s*enabled\s*=\s*(true|false)\s*(?:#.*)?$/m.exec(body)?.[1];
208202
const enabled = enabledValue == null ? undefined : enabledValue === 'true';
209-
const meaningful = body.split(/\r?\n/).map((line) => line.trim())
203+
// Count a multi-line args array as the single field it is.
204+
const fieldBody = argsAssignment ? body.replace(argsAssignment.text, 'args = []') : body;
205+
const meaningful = fieldBody.split(/\r?\n/).map((line) => line.trim())
210206
.filter((line) => line && !line.startsWith('#'));
211207
const exactFields = meaningful.length === 2
212208
&& meaningful.some((line) => /^command\s*=/.test(line))
@@ -445,9 +441,8 @@ export function rufloCodexMcpStatus(cfg, { home = os.homedir() } = {}) {
445441
const next = rest.search(/^\s*\[/m);
446442
const body = rest.slice(0, next < 0 ? rest.length : next);
447443
const commandMatch = /^\s*command\s*=\s*("(?:[^"\\]|\\.)*")\s*$/m.exec(body);
448-
const argsMatch = /^\s*args\s*=\s*(\[[^\n]*\])\s*$/m.exec(body);
449444
try { if (commandMatch) command = JSON.parse(commandMatch[1]); } catch { /* non-canonical TOML */ }
450-
try { if (argsMatch) args = JSON.parse(argsMatch[1]); } catch { /* non-canonical TOML */ }
445+
args = findTomlStringArray(body, 'args')?.value ?? null;
451446
}
452447
} catch { /* config absent → not registered */ }
453448
return {

0 commit comments

Comments
 (0)