@levr-one/cli 0.11.9 → 0.12.0
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 +17 -16
- package/dist/{addHandler-B_3UmMbj.js → addHandler-BIKNXipz.js} +700 -168
- package/dist/{appendHandler-KVsJMxpX.js → appendHandler-DtxLpPu6.js} +9 -9
- package/dist/{artifact-upload-BzLxue32.js → artifact-upload-BvBSevIx.js} +2 -2
- package/dist/{attachHandler-DQpK_pVa.js → attachHandler-DXZi2ILT.js} +9 -9
- package/dist/cli.js +56 -26
- package/dist/{connect-BgQmrL_0.js → connect-DVjgyfls.js} +3 -3
- package/dist/{currentHandler-u89l068v.js → currentHandler-QhCywr3S.js} +1 -1
- package/dist/{find-workspace-T4xuXwtS.js → find-workspace-Bl4rfr73.js} +1 -1
- package/dist/{importHandler-zw5GKb-S.js → importHandler-uFdalKtw.js} +6 -6
- package/dist/inspect-B8tG9g0q.js +1122 -0
- package/dist/inspect-BzBqSBO3.js +3 -0
- package/dist/{listHandler-YA-0RkwS.js → listHandler-BW92JLCl.js} +5 -5
- package/dist/{loginHandler-DE0L1ehF.js → loginHandler-C9Oi92IY.js} +15 -11
- package/dist/{logoutHandler-S4TFYFwz.js → logoutHandler-Cf5aI1wq.js} +2 -2
- package/dist/{pushHandler-DdNyVQ5t.js → pushHandler-CXFZt3SO.js} +8 -8
- package/dist/{resolve-token-7xb7kG7h.js → resolve-token-ByZq05Us.js} +2 -2
- package/dist/{resolve-workspace-CStHisUb.js → resolve-workspace-BbK3uYQc.js} +2 -2
- package/dist/{runHandler-BFDjtxbt.js → runHandler-itaAVn4U.js} +215 -25
- package/dist/{sdk-client-DYv-0CfA.js → sdk-client-B4FMd_Fa.js} +1 -1
- package/dist/{selectHandler-Cejme5nY.js → selectHandler-D6cvq60S.js} +5 -5
- package/dist/{statusHandler-BNiWlC7e.js → statusHandler-BQXn93-r.js} +3 -3
- package/dist/{token-refresh-DeusLK8H.js → token-refresh-CJB7Qlxy.js} +1 -1
- package/package.json +1 -1
- /package/dist/{env-CdwyPHGV.js → env-CvjMSRd7.js} +0 -0
- /package/dist/{sleep-BpJ39Ypm.js → sleep-CGBHjsj5.js} +0 -0
- /package/dist/{workspace-store-CnrxyYRB.js → workspace-store-Bmw104Qa.js} +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { configureClient } from "./sdk-client-
|
|
2
|
-
import { getApiUrl } from "./env-
|
|
3
|
-
import "./token-refresh-
|
|
4
|
-
import { resolveToken } from "./resolve-token-
|
|
5
|
-
import "./workspace-store-
|
|
6
|
-
import { fetchSites } from "./resolve-workspace-
|
|
7
|
-
import { describeWorkspaces, findWorkspaceByKeyOrName, isScopedMcpUrl, resolveMcpUrl, scopedMcpUrl, servedBySessionApi } from "./find-workspace-
|
|
1
|
+
import { configureClient } from "./sdk-client-B4FMd_Fa.js";
|
|
2
|
+
import { getApiUrl } from "./env-CvjMSRd7.js";
|
|
3
|
+
import "./token-refresh-CJB7Qlxy.js";
|
|
4
|
+
import { resolveToken } from "./resolve-token-ByZq05Us.js";
|
|
5
|
+
import "./workspace-store-Bmw104Qa.js";
|
|
6
|
+
import { fetchSites } from "./resolve-workspace-BbK3uYQc.js";
|
|
7
|
+
import { describeWorkspaces, findWorkspaceByKeyOrName, isScopedMcpUrl, resolveMcpUrl, scopedMcpUrl, servedBySessionApi } from "./find-workspace-Bl4rfr73.js";
|
|
8
8
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
|
12
12
|
import { TomlError, parse as parse$1 } from "smol-toml";
|
|
13
|
-
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
14
14
|
|
|
15
15
|
//#region ../mcp-harnesses/dist/format/toml-render.js
|
|
16
16
|
/**
|
|
@@ -105,6 +105,12 @@ function renderTomlBodyLines(value, pathPrefix) {
|
|
|
105
105
|
|
|
106
106
|
//#endregion
|
|
107
107
|
//#region ../mcp-harnesses/dist/catalog.js
|
|
108
|
+
/**
|
|
109
|
+
* Sentinel for {@link AutoApproveDescriptor.file}: the permission key lives in
|
|
110
|
+
* the same file this scope's MCP server entry is written to (Zed keeps both in
|
|
111
|
+
* `settings.json`), so the path is resolved exactly as the install resolves it.
|
|
112
|
+
*/
|
|
113
|
+
const SAME_FILE_AS_MCP_CONFIG = "<mcp-config-file>";
|
|
108
114
|
/** The `{ url, type: "http" }` shape Claude Code and Gemini CLI write. */
|
|
109
115
|
const DEFAULT_NATIVE_HTTP_ENTRY = {
|
|
110
116
|
urlKey: "url",
|
|
@@ -163,7 +169,11 @@ const CLAUDE_CODE_LOCATIONS = [
|
|
|
163
169
|
}
|
|
164
170
|
];
|
|
165
171
|
/**
|
|
166
|
-
* The catalog. Order is presentation order
|
|
172
|
+
* The catalog. Order is presentation order: the agent CLIs first (Claude
|
|
173
|
+
* Desktop, Claude Code, Codex CLI, Grok Build, Antigravity, Gemini CLI), then
|
|
174
|
+
* the IDEs (Cursor, Windsurf, Zed, VS Code) — ENG-7100. Every surface that
|
|
175
|
+
* lists clients iterates this array unsorted, so this is the one place order
|
|
176
|
+
* is decided; `catalog.test.ts` pins it.
|
|
167
177
|
*
|
|
168
178
|
* Every entry is installable, and there is no deferral flag: the old
|
|
169
179
|
* coming-soon flag was retired in ENG-5558 once its last member left. Codex
|
|
@@ -237,6 +247,20 @@ const HARNESSES = [
|
|
|
237
247
|
verifiedVersion: UNVERIFIED_LEGACY,
|
|
238
248
|
verifiedOn: UNVERIFIED_LEGACY,
|
|
239
249
|
configDirEnv: "CLAUDE_CONFIG_DIR",
|
|
250
|
+
autoApprove: {
|
|
251
|
+
file: {
|
|
252
|
+
user: "~/.claude/settings.json",
|
|
253
|
+
project: ".claude/settings.json",
|
|
254
|
+
local: ".claude/settings.local.json"
|
|
255
|
+
},
|
|
256
|
+
format: "json",
|
|
257
|
+
keyPath: ["permissions", "allow"],
|
|
258
|
+
shape: "allow-list",
|
|
259
|
+
entry: (serverName, tool) => `mcp__${serverName}__${tool}`,
|
|
260
|
+
configDirReplaces: "~/.claude",
|
|
261
|
+
docsUrl: "https://code.claude.com/docs/en/permissions",
|
|
262
|
+
verifiedOn: "2026-10-09"
|
|
263
|
+
},
|
|
240
264
|
detectSignals: [
|
|
241
265
|
{
|
|
242
266
|
platform: "darwin",
|
|
@@ -285,30 +309,30 @@ const HARNESSES = [
|
|
|
285
309
|
]
|
|
286
310
|
},
|
|
287
311
|
{
|
|
288
|
-
id: "
|
|
289
|
-
label: "
|
|
290
|
-
matchers: ["
|
|
291
|
-
serverPropertyName: "
|
|
292
|
-
transport: "
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
312
|
+
id: "codex",
|
|
313
|
+
label: "Codex CLI",
|
|
314
|
+
matchers: ["codex"],
|
|
315
|
+
serverPropertyName: "mcp_servers",
|
|
316
|
+
transport: "native-http",
|
|
317
|
+
nativeHttpEntry: { urlKey: "url" },
|
|
318
|
+
configFormat: "toml",
|
|
319
|
+
docsUrl: "https://github.com/openai/codex",
|
|
320
|
+
verifiedVersion: "0.162.1",
|
|
321
|
+
verifiedOn: "2026-10-16",
|
|
322
|
+
configDirEnv: "CODEX_HOME",
|
|
323
|
+
configDirReplaces: "~/.codex",
|
|
296
324
|
detectSignals: [
|
|
297
325
|
{
|
|
298
326
|
platform: "darwin",
|
|
299
|
-
signals: [
|
|
300
|
-
"~/.cursor",
|
|
301
|
-
"which:cursor",
|
|
302
|
-
"/Applications/Cursor.app"
|
|
303
|
-
]
|
|
327
|
+
signals: ["~/.codex", "which:codex"]
|
|
304
328
|
},
|
|
305
329
|
{
|
|
306
|
-
platform: "
|
|
307
|
-
signals: ["~/.
|
|
330
|
+
platform: "linux",
|
|
331
|
+
signals: ["~/.codex", "which:codex"]
|
|
308
332
|
},
|
|
309
333
|
{
|
|
310
|
-
platform: "
|
|
311
|
-
signals: ["~/.
|
|
334
|
+
platform: "win32",
|
|
335
|
+
signals: ["~/.codex", "which:codex"]
|
|
312
336
|
}
|
|
313
337
|
],
|
|
314
338
|
scopes: [{
|
|
@@ -317,48 +341,50 @@ const HARNESSES = [
|
|
|
317
341
|
locations: [
|
|
318
342
|
{
|
|
319
343
|
platform: "darwin",
|
|
320
|
-
configPath: "~/.
|
|
344
|
+
configPath: "~/.codex/config.toml"
|
|
321
345
|
},
|
|
322
346
|
{
|
|
323
|
-
platform: "
|
|
324
|
-
configPath: "~/.
|
|
347
|
+
platform: "linux",
|
|
348
|
+
configPath: "~/.codex/config.toml"
|
|
325
349
|
},
|
|
326
350
|
{
|
|
327
|
-
platform: "
|
|
328
|
-
configPath: "~/.
|
|
351
|
+
platform: "win32",
|
|
352
|
+
configPath: "~/.codex/config.toml"
|
|
329
353
|
}
|
|
330
354
|
]
|
|
331
355
|
}, {
|
|
332
356
|
scope: "project",
|
|
333
357
|
installKind: "config-file",
|
|
334
|
-
projectPath: ".
|
|
358
|
+
projectPath: ".codex/config.toml",
|
|
359
|
+
note: "Codex loads .codex/config.toml only once it trusts this repo: accept the trust prompt on first launch, or set trust_level = \"trusted\" under [projects.\"<repo path>\"] in your Codex config (~/.codex/config.toml, or $CODEX_HOME/config.toml when CODEX_HOME is set)"
|
|
335
360
|
}]
|
|
336
361
|
},
|
|
337
362
|
{
|
|
338
|
-
id: "
|
|
339
|
-
label: "
|
|
340
|
-
matchers: ["
|
|
341
|
-
serverPropertyName: "
|
|
342
|
-
transport: "
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
363
|
+
id: "grok",
|
|
364
|
+
label: "Grok Build",
|
|
365
|
+
matchers: ["grok build", "grok-build"],
|
|
366
|
+
serverPropertyName: "mcp_servers",
|
|
367
|
+
transport: "native-http",
|
|
368
|
+
nativeHttpEntry: {
|
|
369
|
+
urlKey: "url",
|
|
370
|
+
constants: { enabled: true }
|
|
371
|
+
},
|
|
372
|
+
configFormat: "toml",
|
|
373
|
+
docsUrl: "https://docs.x.ai/build/overview",
|
|
374
|
+
verifiedVersion: "1.0.34",
|
|
375
|
+
verifiedOn: "2026-09-17",
|
|
346
376
|
detectSignals: [
|
|
347
377
|
{
|
|
348
378
|
platform: "darwin",
|
|
349
|
-
signals: [
|
|
350
|
-
"~/.codeium/windsurf",
|
|
351
|
-
"/Applications/Windsurf.app",
|
|
352
|
-
"which:windsurf"
|
|
353
|
-
]
|
|
379
|
+
signals: ["~/.grok/config.toml", "~/.grok/bin/grok"]
|
|
354
380
|
},
|
|
355
381
|
{
|
|
356
|
-
platform: "
|
|
357
|
-
signals: ["~/.
|
|
382
|
+
platform: "linux",
|
|
383
|
+
signals: ["~/.grok/config.toml", "~/.grok/bin/grok"]
|
|
358
384
|
},
|
|
359
385
|
{
|
|
360
|
-
platform: "
|
|
361
|
-
signals: ["~/.
|
|
386
|
+
platform: "win32",
|
|
387
|
+
signals: ["~/.grok/config.toml", "~/.grok/bin/grok.exe"]
|
|
362
388
|
}
|
|
363
389
|
],
|
|
364
390
|
scopes: [{
|
|
@@ -367,44 +393,57 @@ const HARNESSES = [
|
|
|
367
393
|
locations: [
|
|
368
394
|
{
|
|
369
395
|
platform: "darwin",
|
|
370
|
-
configPath: "~/.
|
|
396
|
+
configPath: "~/.grok/config.toml"
|
|
371
397
|
},
|
|
372
398
|
{
|
|
373
|
-
platform: "
|
|
374
|
-
configPath: "~/.
|
|
399
|
+
platform: "linux",
|
|
400
|
+
configPath: "~/.grok/config.toml"
|
|
375
401
|
},
|
|
376
402
|
{
|
|
377
|
-
platform: "
|
|
378
|
-
configPath: "~/.
|
|
403
|
+
platform: "win32",
|
|
404
|
+
configPath: "~/.grok/config.toml"
|
|
379
405
|
}
|
|
380
406
|
]
|
|
407
|
+
}, {
|
|
408
|
+
scope: "project",
|
|
409
|
+
installKind: "config-file",
|
|
410
|
+
projectPath: ".grok/config.toml"
|
|
381
411
|
}]
|
|
382
412
|
},
|
|
383
413
|
{
|
|
384
|
-
id: "
|
|
385
|
-
label: "
|
|
386
|
-
matchers: ["
|
|
387
|
-
serverPropertyName: "
|
|
388
|
-
transport: "
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
414
|
+
id: "antigravity",
|
|
415
|
+
label: "Antigravity",
|
|
416
|
+
matchers: ["antigravity"],
|
|
417
|
+
serverPropertyName: "mcpServers",
|
|
418
|
+
transport: "native-http",
|
|
419
|
+
nativeHttpEntry: {
|
|
420
|
+
urlKey: "serverUrl",
|
|
421
|
+
constants: { disabled: false }
|
|
422
|
+
},
|
|
423
|
+
docsUrl: "https://antigravity.google/docs/mcp",
|
|
424
|
+
verifiedVersion: "1.4.0",
|
|
425
|
+
verifiedOn: "2026-10-16",
|
|
392
426
|
detectSignals: [
|
|
393
427
|
{
|
|
394
428
|
platform: "darwin",
|
|
395
429
|
signals: [
|
|
396
|
-
"
|
|
397
|
-
"/
|
|
398
|
-
"
|
|
430
|
+
"which:agy",
|
|
431
|
+
"~/.local/bin/agy",
|
|
432
|
+
"/Applications/Antigravity IDE.app",
|
|
433
|
+
"~/.gemini/config/mcp_config.json"
|
|
399
434
|
]
|
|
400
435
|
},
|
|
401
436
|
{
|
|
402
437
|
platform: "linux",
|
|
403
|
-
signals: [
|
|
438
|
+
signals: [
|
|
439
|
+
"which:agy",
|
|
440
|
+
"~/.local/bin/agy",
|
|
441
|
+
"~/.gemini/config/mcp_config.json"
|
|
442
|
+
]
|
|
404
443
|
},
|
|
405
444
|
{
|
|
406
445
|
platform: "win32",
|
|
407
|
-
signals: ["
|
|
446
|
+
signals: ["which:agy", "~/.gemini/config/mcp_config.json"]
|
|
408
447
|
}
|
|
409
448
|
],
|
|
410
449
|
scopes: [{
|
|
@@ -413,21 +452,21 @@ const HARNESSES = [
|
|
|
413
452
|
locations: [
|
|
414
453
|
{
|
|
415
454
|
platform: "darwin",
|
|
416
|
-
configPath: "~/.config/
|
|
455
|
+
configPath: "~/.gemini/config/mcp_config.json"
|
|
417
456
|
},
|
|
418
457
|
{
|
|
419
458
|
platform: "linux",
|
|
420
|
-
configPath: "~/.config/
|
|
459
|
+
configPath: "~/.gemini/config/mcp_config.json"
|
|
421
460
|
},
|
|
422
461
|
{
|
|
423
462
|
platform: "win32",
|
|
424
|
-
configPath: "
|
|
463
|
+
configPath: "~/.gemini/config/mcp_config.json"
|
|
425
464
|
}
|
|
426
465
|
]
|
|
427
466
|
}, {
|
|
428
467
|
scope: "project",
|
|
429
468
|
installKind: "config-file",
|
|
430
|
-
projectPath: ".
|
|
469
|
+
projectPath: ".agents/mcp_config.json"
|
|
431
470
|
}]
|
|
432
471
|
},
|
|
433
472
|
{
|
|
@@ -477,31 +516,44 @@ const HARNESSES = [
|
|
|
477
516
|
}]
|
|
478
517
|
},
|
|
479
518
|
{
|
|
480
|
-
id: "
|
|
481
|
-
label: "
|
|
482
|
-
matchers: ["
|
|
483
|
-
serverPropertyName: "
|
|
484
|
-
transport: "
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
519
|
+
id: "cursor",
|
|
520
|
+
label: "Cursor",
|
|
521
|
+
matchers: ["cursor"],
|
|
522
|
+
serverPropertyName: "mcpServers",
|
|
523
|
+
transport: "mcp-remote",
|
|
524
|
+
docsUrl: "https://cursor.com/docs/mcp",
|
|
525
|
+
verifiedVersion: UNVERIFIED_LEGACY,
|
|
526
|
+
verifiedOn: UNVERIFIED_LEGACY,
|
|
527
|
+
autoApprove: {
|
|
528
|
+
file: {
|
|
529
|
+
user: "~/.cursor/permissions.json",
|
|
530
|
+
project: ".cursor/permissions.json"
|
|
531
|
+
},
|
|
532
|
+
format: "json",
|
|
533
|
+
keyPath: ["mcpAllowlist"],
|
|
534
|
+
shape: "allow-list",
|
|
535
|
+
entry: (serverName, tool) => `${serverName}:${tool}`,
|
|
536
|
+
appendOnlyToExistingKey: true,
|
|
537
|
+
caveat: "Cursor: Levr only adds to an mcpAllowlist this file already has. It never creates one, because once mcpAllowlist exists here it replaces the MCP allowlist kept in Cursor Settings. The entries only take effect when Run Mode in Cursor Settings uses an allowlist.",
|
|
538
|
+
docsUrl: "https://cursor.com/docs/reference/permissions",
|
|
539
|
+
verifiedOn: "2026-10-09"
|
|
488
540
|
},
|
|
489
|
-
configFormat: "toml",
|
|
490
|
-
docsUrl: "https://docs.x.ai/build/overview",
|
|
491
|
-
verifiedVersion: "1.0.34",
|
|
492
|
-
verifiedOn: "2026-09-17",
|
|
493
541
|
detectSignals: [
|
|
494
542
|
{
|
|
495
543
|
platform: "darwin",
|
|
496
|
-
signals: [
|
|
544
|
+
signals: [
|
|
545
|
+
"~/.cursor",
|
|
546
|
+
"which:cursor",
|
|
547
|
+
"/Applications/Cursor.app"
|
|
548
|
+
]
|
|
497
549
|
},
|
|
498
550
|
{
|
|
499
|
-
platform: "
|
|
500
|
-
signals: ["~/.
|
|
551
|
+
platform: "win32",
|
|
552
|
+
signals: ["~/.cursor", "which:cursor"]
|
|
501
553
|
},
|
|
502
554
|
{
|
|
503
|
-
platform: "
|
|
504
|
-
signals: ["~/.
|
|
555
|
+
platform: "linux",
|
|
556
|
+
signals: ["~/.cursor", "which:cursor"]
|
|
505
557
|
}
|
|
506
558
|
],
|
|
507
559
|
scopes: [{
|
|
@@ -510,57 +562,109 @@ const HARNESSES = [
|
|
|
510
562
|
locations: [
|
|
511
563
|
{
|
|
512
564
|
platform: "darwin",
|
|
513
|
-
configPath: "~/.
|
|
565
|
+
configPath: "~/.cursor/mcp.json"
|
|
514
566
|
},
|
|
515
567
|
{
|
|
516
|
-
platform: "
|
|
517
|
-
configPath: "~/.
|
|
568
|
+
platform: "win32",
|
|
569
|
+
configPath: "~/.cursor/mcp.json"
|
|
518
570
|
},
|
|
519
571
|
{
|
|
520
|
-
platform: "
|
|
521
|
-
configPath: "~/.
|
|
572
|
+
platform: "linux",
|
|
573
|
+
configPath: "~/.cursor/mcp.json"
|
|
522
574
|
}
|
|
523
575
|
]
|
|
524
576
|
}, {
|
|
525
577
|
scope: "project",
|
|
526
578
|
installKind: "config-file",
|
|
527
|
-
projectPath: ".
|
|
579
|
+
projectPath: ".cursor/mcp.json"
|
|
528
580
|
}]
|
|
529
581
|
},
|
|
530
582
|
{
|
|
531
|
-
id: "
|
|
532
|
-
label: "
|
|
533
|
-
matchers: ["
|
|
583
|
+
id: "windsurf",
|
|
584
|
+
label: "Windsurf",
|
|
585
|
+
matchers: ["windsurf", "codeium"],
|
|
534
586
|
serverPropertyName: "mcpServers",
|
|
535
|
-
transport: "
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
},
|
|
540
|
-
docsUrl: "https://antigravity.google/docs/mcp",
|
|
541
|
-
verifiedVersion: "1.2.5",
|
|
542
|
-
verifiedOn: "2026-09-17",
|
|
587
|
+
transport: "mcp-remote",
|
|
588
|
+
docsUrl: "https://docs.windsurf.com/windsurf/mcp",
|
|
589
|
+
verifiedVersion: UNVERIFIED_LEGACY,
|
|
590
|
+
verifiedOn: UNVERIFIED_LEGACY,
|
|
543
591
|
detectSignals: [
|
|
544
592
|
{
|
|
545
593
|
platform: "darwin",
|
|
546
594
|
signals: [
|
|
547
|
-
"
|
|
548
|
-
"
|
|
549
|
-
"
|
|
550
|
-
"~/.gemini/config/mcp_config.json"
|
|
595
|
+
"~/.codeium/windsurf",
|
|
596
|
+
"/Applications/Windsurf.app",
|
|
597
|
+
"which:windsurf"
|
|
551
598
|
]
|
|
552
599
|
},
|
|
600
|
+
{
|
|
601
|
+
platform: "win32",
|
|
602
|
+
signals: ["~/.codeium/windsurf", "which:windsurf"]
|
|
603
|
+
},
|
|
553
604
|
{
|
|
554
605
|
platform: "linux",
|
|
606
|
+
signals: ["~/.codeium/windsurf", "which:windsurf"]
|
|
607
|
+
}
|
|
608
|
+
],
|
|
609
|
+
scopes: [{
|
|
610
|
+
scope: "user",
|
|
611
|
+
installKind: "config-file",
|
|
612
|
+
locations: [
|
|
613
|
+
{
|
|
614
|
+
platform: "darwin",
|
|
615
|
+
configPath: "~/.codeium/windsurf/mcp_config.json"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
platform: "win32",
|
|
619
|
+
configPath: "~/.codeium/windsurf/mcp_config.json"
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
platform: "linux",
|
|
623
|
+
configPath: "~/.codeium/windsurf/mcp_config.json"
|
|
624
|
+
}
|
|
625
|
+
]
|
|
626
|
+
}]
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
id: "zed",
|
|
630
|
+
label: "Zed",
|
|
631
|
+
matchers: ["zed"],
|
|
632
|
+
serverPropertyName: "context_servers",
|
|
633
|
+
transport: "mcp-remote",
|
|
634
|
+
docsUrl: "https://zed.dev/docs/ai/mcp",
|
|
635
|
+
verifiedVersion: UNVERIFIED_LEGACY,
|
|
636
|
+
verifiedOn: UNVERIFIED_LEGACY,
|
|
637
|
+
autoApprove: {
|
|
638
|
+
file: { user: SAME_FILE_AS_MCP_CONFIG },
|
|
639
|
+
format: "json",
|
|
640
|
+
keyPath: [
|
|
641
|
+
"agent",
|
|
642
|
+
"tool_permissions",
|
|
643
|
+
"tools"
|
|
644
|
+
],
|
|
645
|
+
shape: "tool-map",
|
|
646
|
+
entry: (serverName, tool) => `mcp:${serverName}:${tool}`,
|
|
647
|
+
toolValue: { default: "allow" },
|
|
648
|
+
caveat: "Zed: per-tool permissions need Zed v0.224.0 or later.",
|
|
649
|
+
docsUrl: "https://zed.dev/docs/ai/tool-permissions",
|
|
650
|
+
verifiedOn: "2026-10-09"
|
|
651
|
+
},
|
|
652
|
+
detectSignals: [
|
|
653
|
+
{
|
|
654
|
+
platform: "darwin",
|
|
555
655
|
signals: [
|
|
556
|
-
"
|
|
557
|
-
"
|
|
558
|
-
"
|
|
656
|
+
"~/.config/zed",
|
|
657
|
+
"/Applications/Zed.app",
|
|
658
|
+
"which:zed"
|
|
559
659
|
]
|
|
560
660
|
},
|
|
661
|
+
{
|
|
662
|
+
platform: "linux",
|
|
663
|
+
signals: ["~/.config/zed", "which:zed"]
|
|
664
|
+
},
|
|
561
665
|
{
|
|
562
666
|
platform: "win32",
|
|
563
|
-
signals: ["
|
|
667
|
+
signals: ["~/AppData/Roaming/Zed"]
|
|
564
668
|
}
|
|
565
669
|
],
|
|
566
670
|
scopes: [{
|
|
@@ -569,17 +673,21 @@ const HARNESSES = [
|
|
|
569
673
|
locations: [
|
|
570
674
|
{
|
|
571
675
|
platform: "darwin",
|
|
572
|
-
configPath: "~/.
|
|
676
|
+
configPath: "~/.config/zed/settings.json"
|
|
573
677
|
},
|
|
574
678
|
{
|
|
575
679
|
platform: "linux",
|
|
576
|
-
configPath: "~/.
|
|
680
|
+
configPath: "~/.config/zed/settings.json"
|
|
577
681
|
},
|
|
578
682
|
{
|
|
579
683
|
platform: "win32",
|
|
580
|
-
configPath: "
|
|
684
|
+
configPath: "~/AppData/Roaming/Zed/settings.json"
|
|
581
685
|
}
|
|
582
686
|
]
|
|
687
|
+
}, {
|
|
688
|
+
scope: "project",
|
|
689
|
+
installKind: "config-file",
|
|
690
|
+
projectPath: ".zed/settings.json"
|
|
583
691
|
}]
|
|
584
692
|
},
|
|
585
693
|
{
|
|
@@ -631,52 +739,6 @@ const HARNESSES = [
|
|
|
631
739
|
installKind: "config-file",
|
|
632
740
|
projectPath: ".vscode/mcp.json"
|
|
633
741
|
}]
|
|
634
|
-
},
|
|
635
|
-
{
|
|
636
|
-
id: "codex",
|
|
637
|
-
label: "Codex CLI",
|
|
638
|
-
matchers: ["codex"],
|
|
639
|
-
serverPropertyName: "mcp_servers",
|
|
640
|
-
transport: "native-http",
|
|
641
|
-
nativeHttpEntry: { urlKey: "url" },
|
|
642
|
-
configFormat: "toml",
|
|
643
|
-
docsUrl: "https://github.com/openai/codex",
|
|
644
|
-
verifiedVersion: "0.154.0",
|
|
645
|
-
verifiedOn: "2026-09-17",
|
|
646
|
-
configDirEnv: "CODEX_HOME",
|
|
647
|
-
configDirReplaces: "~/.codex",
|
|
648
|
-
detectSignals: [
|
|
649
|
-
{
|
|
650
|
-
platform: "darwin",
|
|
651
|
-
signals: ["~/.codex", "which:codex"]
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
platform: "linux",
|
|
655
|
-
signals: ["~/.codex", "which:codex"]
|
|
656
|
-
},
|
|
657
|
-
{
|
|
658
|
-
platform: "win32",
|
|
659
|
-
signals: ["~/.codex", "which:codex"]
|
|
660
|
-
}
|
|
661
|
-
],
|
|
662
|
-
scopes: [{
|
|
663
|
-
scope: "user",
|
|
664
|
-
installKind: "config-file",
|
|
665
|
-
locations: [
|
|
666
|
-
{
|
|
667
|
-
platform: "darwin",
|
|
668
|
-
configPath: "~/.codex/config.toml"
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
platform: "linux",
|
|
672
|
-
configPath: "~/.codex/config.toml"
|
|
673
|
-
},
|
|
674
|
-
{
|
|
675
|
-
platform: "win32",
|
|
676
|
-
configPath: "~/.codex/config.toml"
|
|
677
|
-
}
|
|
678
|
-
]
|
|
679
|
-
}]
|
|
680
742
|
}
|
|
681
743
|
];
|
|
682
744
|
/** Look up a harness by id. Returns `undefined` for unknown ids. */
|
|
@@ -810,6 +872,76 @@ function besideName(serverName, url) {
|
|
|
810
872
|
return name.length <= BESIDE_NAME_MAX && isValidEntryName(name) ? name : void 0;
|
|
811
873
|
}
|
|
812
874
|
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region ../mcp-harnesses/dist/levr-tool-allowlist.js
|
|
877
|
+
/**
|
|
878
|
+
* The Levr MCP tools `levr mcp add` may offer to pre-approve in a harness's
|
|
879
|
+
* own permission file (ENG-6392, plan ENG-6341 §D9).
|
|
880
|
+
*
|
|
881
|
+
* SINGLE SOURCE. Every harness auto-approve descriptor in `catalog.ts`
|
|
882
|
+
* renders its entries from this list, and the backender parity spec
|
|
883
|
+
* (`apps/backender/src/mcp/levr-tool-allowlist.parity.spec.ts`) asserts that
|
|
884
|
+
* every name here is a public tool and none is in
|
|
885
|
+
* `McpToolAdapterService.DESTRUCTIVE_TOOL_NAMES`.
|
|
886
|
+
*
|
|
887
|
+
* Rules for this list:
|
|
888
|
+
* - Only the non-destructive tools a first task actually calls. A tool that
|
|
889
|
+
* can delete, merge away or permanently unlink anything never belongs here.
|
|
890
|
+
* - Nor does a tool whose PURPOSE is to waive a check (waive_gate): see
|
|
891
|
+
* {@link LEVR_NEVER_PREAPPROVED_TOOLS}.
|
|
892
|
+
* - `deploy_gates` and `transition_issue` ARE listed (user decision
|
|
893
|
+
* 2026-10-15: neither is destructive, and the first task must run without
|
|
894
|
+
* permission prompts). deploy_gates can also change or withdraw a gate,
|
|
895
|
+
* and transition_issue can waive a required gate through
|
|
896
|
+
* `verification_override`. The installer's disclosure says so rather than
|
|
897
|
+
* promising otherwise, and every such change is recorded on the gate (code
|
|
898
|
+
* review F-027). The disclosure must stay literally true for every entry.
|
|
899
|
+
* - One entry per tool, by its exact registered name. Never a server-wide
|
|
900
|
+
* entry and never a glob: a server-wide grant would also pre-approve the
|
|
901
|
+
* destructive tools the server exposes.
|
|
902
|
+
*
|
|
903
|
+
* Isomorphic and import-free, like the catalog, so the client SPA and the
|
|
904
|
+
* backender can both read it.
|
|
905
|
+
*/
|
|
906
|
+
/** The tools offered for pre-approval, in the order a first task uses them. */
|
|
907
|
+
const LEVR_PREAPPROVED_TOOLS = [
|
|
908
|
+
"get_context",
|
|
909
|
+
"start_onboarding",
|
|
910
|
+
"get_onboarding_step",
|
|
911
|
+
"get_issue",
|
|
912
|
+
"get_issues",
|
|
913
|
+
"search_issues",
|
|
914
|
+
"get_issue_comments",
|
|
915
|
+
"add_issue_comment",
|
|
916
|
+
"deploy_gates",
|
|
917
|
+
"verify_deliverable_gates",
|
|
918
|
+
"report_gate_results",
|
|
919
|
+
"get_gate_status",
|
|
920
|
+
"transition_issue",
|
|
921
|
+
"get_practice_app"
|
|
922
|
+
];
|
|
923
|
+
/**
|
|
924
|
+
* A plain description of what each tool does, printed next to its entry in
|
|
925
|
+
* the installer's disclosure (plan R1-23) so the user can judge each line
|
|
926
|
+
* rather than a list of opaque identifiers.
|
|
927
|
+
*/
|
|
928
|
+
const LEVR_PREAPPROVED_TOOL_DESCRIPTIONS = {
|
|
929
|
+
get_context: "reads your workspace, teams and projects",
|
|
930
|
+
start_onboarding: "starts a guided first task",
|
|
931
|
+
get_onboarding_step: "reads the current step of the guided first task",
|
|
932
|
+
get_issue: "reads one task",
|
|
933
|
+
get_issues: "lists tasks",
|
|
934
|
+
search_issues: "searches tasks",
|
|
935
|
+
get_issue_comments: "reads a task's comments",
|
|
936
|
+
add_issue_comment: "adds a comment to a task",
|
|
937
|
+
deploy_gates: "writes the task's gates; it can also change or withdraw a gate (recorded)",
|
|
938
|
+
transition_issue: "moves a task to another status; Done is refused while a required check fails, unless your agent waives that check (the waiver is recorded)",
|
|
939
|
+
verify_deliverable_gates: "runs the task's checklist",
|
|
940
|
+
report_gate_results: "records the checklist results your agent ran",
|
|
941
|
+
get_gate_status: "reads the task's checklist results",
|
|
942
|
+
get_practice_app: "reads the sample practice app"
|
|
943
|
+
};
|
|
944
|
+
|
|
813
945
|
//#endregion
|
|
814
946
|
//#region ../mcp-harnesses/dist/node/paths.js
|
|
815
947
|
function defaultEnv() {
|
|
@@ -1212,6 +1344,27 @@ const jsoncAdapter = {
|
|
|
1212
1344
|
},
|
|
1213
1345
|
removeAt(text, path$1) {
|
|
1214
1346
|
return edit(text, path$1, void 0);
|
|
1347
|
+
},
|
|
1348
|
+
appendToArray(text, path$1, values) {
|
|
1349
|
+
const doc = parseDocument$1(text);
|
|
1350
|
+
if (!doc.ok) throw new UnsupportedConfigShapeError(doc.detail);
|
|
1351
|
+
const current = getAtPath(doc.root, path$1);
|
|
1352
|
+
if (!Array.isArray(current)) throw new UnsupportedConfigShapeError(`${path$1.join(".")} is not an array — refusing to restructure it`);
|
|
1353
|
+
let next = text;
|
|
1354
|
+
let index = current.length;
|
|
1355
|
+
for (const value of values) {
|
|
1356
|
+
try {
|
|
1357
|
+
next = applyEdits(next, modify(next, [...path$1, index], value, {
|
|
1358
|
+
formattingOptions: FORMAT,
|
|
1359
|
+
isArrayInsertion: true
|
|
1360
|
+
}));
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1363
|
+
throw new UnsupportedConfigShapeError(`cannot append to ${path$1.join(".")}: ${message}`);
|
|
1364
|
+
}
|
|
1365
|
+
index++;
|
|
1366
|
+
}
|
|
1367
|
+
return next;
|
|
1215
1368
|
}
|
|
1216
1369
|
};
|
|
1217
1370
|
/** Shared by set and remove: jsonc-parser removes a key when `value` is `undefined`. */
|
|
@@ -2104,6 +2257,207 @@ function writeConfigAtomic(path$1, text) {
|
|
|
2104
2257
|
throw err;
|
|
2105
2258
|
}
|
|
2106
2259
|
}
|
|
2260
|
+
/** Resolve the descriptor's permission file for a scope, or why it cannot be. */
|
|
2261
|
+
function resolvePermissionFile(harness, desc, scope, env) {
|
|
2262
|
+
const file = desc.file[scope];
|
|
2263
|
+
if (!file || !scopeDef(harness, scope)) return { reason: "unsupported-scope" };
|
|
2264
|
+
if (file === SAME_FILE_AS_MCP_CONFIG) {
|
|
2265
|
+
const target = resolveTarget(harness, env, scope);
|
|
2266
|
+
if ("reason" in target) return { reason: target.reason === "not-a-repo" || target.reason === "no-location" ? target.reason : "unsupported-scope" };
|
|
2267
|
+
return target;
|
|
2268
|
+
}
|
|
2269
|
+
if (file.startsWith("~")) {
|
|
2270
|
+
if (harness.configDirEnv && desc.configDirReplaces) return { path: expandConfigPath(file, {
|
|
2271
|
+
...harness,
|
|
2272
|
+
configDirReplaces: desc.configDirReplaces
|
|
2273
|
+
}, env) };
|
|
2274
|
+
return { path: expandTilde(file, env.homedir) };
|
|
2275
|
+
}
|
|
2276
|
+
const root = resolveProjectRoot(env);
|
|
2277
|
+
if (!root.isRepo) return { reason: "not-a-repo" };
|
|
2278
|
+
return {
|
|
2279
|
+
path: join(root.root, ...file.split("/")),
|
|
2280
|
+
repoRoot: root.root
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Whether git ignores `file` inside the work tree at `repoRoot`: `true` /
|
|
2285
|
+
* `false` from `git check-ignore`, `undefined` when git cannot say (not
|
|
2286
|
+
* installed, not a work tree). Never throws. Git runs in the injected machine
|
|
2287
|
+
* environment — `HOME` is `env.homedir`, so the user's global excludes file
|
|
2288
|
+
* counts and a test's temp home isolates it — and the `GIT_*` per-process
|
|
2289
|
+
* variables are dropped so a parent git (a hook) cannot redirect the check to
|
|
2290
|
+
* another repository.
|
|
2291
|
+
*/
|
|
2292
|
+
function isGitIgnored(env, repoRoot, file) {
|
|
2293
|
+
const childEnv = {
|
|
2294
|
+
...env.vars ?? {},
|
|
2295
|
+
HOME: env.homedir,
|
|
2296
|
+
PATH: env.pathVar || process.env.PATH
|
|
2297
|
+
};
|
|
2298
|
+
for (const key of [
|
|
2299
|
+
"GIT_DIR",
|
|
2300
|
+
"GIT_WORK_TREE",
|
|
2301
|
+
"GIT_INDEX_FILE",
|
|
2302
|
+
"GIT_COMMON_DIR"
|
|
2303
|
+
]) delete childEnv[key];
|
|
2304
|
+
try {
|
|
2305
|
+
const r = spawnSync("git", [
|
|
2306
|
+
"check-ignore",
|
|
2307
|
+
"-q",
|
|
2308
|
+
"--",
|
|
2309
|
+
file
|
|
2310
|
+
], {
|
|
2311
|
+
cwd: repoRoot,
|
|
2312
|
+
env: childEnv,
|
|
2313
|
+
stdio: "ignore",
|
|
2314
|
+
timeout: 5e3
|
|
2315
|
+
});
|
|
2316
|
+
if (r.status === 0) return true;
|
|
2317
|
+
if (r.status === 1) return false;
|
|
2318
|
+
return;
|
|
2319
|
+
} catch {
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
function permissionAdapter(desc) {
|
|
2324
|
+
return desc.format === "toml" ? tomlAdapter : jsoncAdapter;
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Pre-approve the non-destructive Levr tools in a harness's own permission
|
|
2328
|
+
* file, so its agent can call them without asking each time.
|
|
2329
|
+
*
|
|
2330
|
+
* MERGE-ONLY and NON-DESTRUCTIVE: entries are appended to what the file
|
|
2331
|
+
* already holds; an existing entry is reported in `alreadyPresent` and never
|
|
2332
|
+
* duplicated; a `tool-map` key the user already set to something else is
|
|
2333
|
+
* reported in `conflicting` and left alone. Comments survive (the edit is an
|
|
2334
|
+
* in-place text patch, never a re-serialize), and the write is atomic
|
|
2335
|
+
* ({@link writeConfigAtomic}: a sibling temp file, then rename), so an
|
|
2336
|
+
* interrupted write leaves the original file intact.
|
|
2337
|
+
*
|
|
2338
|
+
* Error direction: fail_closed. An existing file that cannot be read or
|
|
2339
|
+
* parsed, or that holds the key in a shape we will not restructure (e.g.
|
|
2340
|
+
* `permissions.allow` is a string), aborts with
|
|
2341
|
+
* `unsupported-config-shape` and writes NOTHING — never a clobber. Every
|
|
2342
|
+
* failure is returned, not thrown, so the caller can report "permissions not
|
|
2343
|
+
* added" and carry on (the MCP install itself already succeeded).
|
|
2344
|
+
*/
|
|
2345
|
+
function preapproveTools(harness, scope, serverName = DEFAULT_SERVER_NAME, opts = {}) {
|
|
2346
|
+
const env = opts.env ?? defaultEnv();
|
|
2347
|
+
const dryRun = opts.dryRun ?? false;
|
|
2348
|
+
const desc = harness.autoApprove;
|
|
2349
|
+
const base = {
|
|
2350
|
+
ok: false,
|
|
2351
|
+
file: "",
|
|
2352
|
+
scope,
|
|
2353
|
+
fileExists: false,
|
|
2354
|
+
added: [],
|
|
2355
|
+
alreadyPresent: [],
|
|
2356
|
+
conflicting: [],
|
|
2357
|
+
addedEntries: [],
|
|
2358
|
+
wrote: false,
|
|
2359
|
+
dryRun,
|
|
2360
|
+
keyPath: desc ? desc.keyPath.join(".") : "",
|
|
2361
|
+
...desc?.toolValue ? { toolValue: desc.toolValue } : {},
|
|
2362
|
+
...desc?.caveat ? { caveat: desc.caveat } : {}
|
|
2363
|
+
};
|
|
2364
|
+
if (!desc) return {
|
|
2365
|
+
...base,
|
|
2366
|
+
reason: "no-descriptor"
|
|
2367
|
+
};
|
|
2368
|
+
const resolved = resolvePermissionFile(harness, desc, scope, env);
|
|
2369
|
+
if ("reason" in resolved) return {
|
|
2370
|
+
...base,
|
|
2371
|
+
reason: resolved.reason
|
|
2372
|
+
};
|
|
2373
|
+
const file = resolved.path;
|
|
2374
|
+
base.fileExists = existsSync(file);
|
|
2375
|
+
if (resolved.repoRoot !== void 0) {
|
|
2376
|
+
const ignored = isGitIgnored(env, resolved.repoRoot, file);
|
|
2377
|
+
if (ignored !== void 0) base.gitIgnored = ignored;
|
|
2378
|
+
}
|
|
2379
|
+
const fail = (reason, detail) => ({
|
|
2380
|
+
...base,
|
|
2381
|
+
file,
|
|
2382
|
+
reason,
|
|
2383
|
+
...detail ? { detail } : {}
|
|
2384
|
+
});
|
|
2385
|
+
const adapter = permissionAdapter(desc);
|
|
2386
|
+
if (desc.shape === "allow-list" && !adapter.appendToArray) return fail("unsupported-format", `${desc.format} permission files cannot be appended to in place`);
|
|
2387
|
+
const existing = readTextOrNull(file);
|
|
2388
|
+
if (existing === null && existsSync(file)) return fail("unsupported-config-shape", "file exists but cannot be read");
|
|
2389
|
+
const baseText = existing && existing.trim() ? existing : adapter.empty;
|
|
2390
|
+
const whole = adapter.readAt(baseText, []);
|
|
2391
|
+
if (whole.kind === "unsupported") return fail("unsupported-config-shape", whole.detail);
|
|
2392
|
+
const root = whole.kind === "value" ? whole.value : {};
|
|
2393
|
+
const keyPath = [...desc.keyPath];
|
|
2394
|
+
const currentAtKey = getAtPath(root, keyPath);
|
|
2395
|
+
const wanted = (opts.tools ?? LEVR_PREAPPROVED_TOOLS).map((tool) => ({
|
|
2396
|
+
tool,
|
|
2397
|
+
entry: desc.entry(serverName, tool)
|
|
2398
|
+
}));
|
|
2399
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2400
|
+
const unique = wanted.filter((w) => seen.has(w.entry) ? false : (seen.add(w.entry), true));
|
|
2401
|
+
const addedEntries = [];
|
|
2402
|
+
const alreadyPresent = [];
|
|
2403
|
+
const conflicting = [];
|
|
2404
|
+
let nextText = baseText;
|
|
2405
|
+
try {
|
|
2406
|
+
if (desc.shape === "allow-list") {
|
|
2407
|
+
if (currentAtKey !== void 0 && !Array.isArray(currentAtKey)) return fail("unsupported-config-shape", `${base.keyPath} exists but is not a list — refusing to restructure it`);
|
|
2408
|
+
if (currentAtKey === void 0 && desc.appendOnlyToExistingKey) return fail("allowlist-key-absent", `${harness.label} uses the allowlist in its own Settings while ${base.keyPath} is absent from ${file}, and creating it would replace that allowlist. To pre-approve, add these in ${harness.label} Settings (or to an existing ${base.keyPath} in that file): ${unique.map((w) => w.entry).join(", ")}`);
|
|
2409
|
+
const have = new Set((currentAtKey ?? []).filter((v) => typeof v === "string"));
|
|
2410
|
+
for (const w of unique) if (have.has(w.entry)) alreadyPresent.push(w.entry);
|
|
2411
|
+
else addedEntries.push(w);
|
|
2412
|
+
const values = addedEntries.map((w) => w.entry);
|
|
2413
|
+
if (values.length > 0) nextText = currentAtKey === void 0 ? adapter.setAt(baseText, keyPath, values) : adapter.appendToArray(baseText, keyPath, values);
|
|
2414
|
+
} else {
|
|
2415
|
+
if (currentAtKey !== void 0 && (currentAtKey === null || typeof currentAtKey !== "object" || Array.isArray(currentAtKey))) return fail("unsupported-config-shape", `${base.keyPath} exists but is not an object — refusing to restructure it`);
|
|
2416
|
+
const map = currentAtKey ?? {};
|
|
2417
|
+
const value = desc.toolValue ?? {};
|
|
2418
|
+
for (const w of unique) {
|
|
2419
|
+
const cur = map[w.entry];
|
|
2420
|
+
if (cur === void 0) {
|
|
2421
|
+
addedEntries.push(w);
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
const curObj = cur !== null && typeof cur === "object" && !Array.isArray(cur) ? cur : void 0;
|
|
2425
|
+
if (curObj !== void 0 && Object.entries(value).every(([k, v]) => curObj[k] === v)) alreadyPresent.push(w.entry);
|
|
2426
|
+
else conflicting.push(w.entry);
|
|
2427
|
+
}
|
|
2428
|
+
for (const w of addedEntries) nextText = adapter.setAt(nextText, [...keyPath, w.entry], { ...value });
|
|
2429
|
+
}
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
if (err instanceof UnsupportedConfigShapeError) return fail("unsupported-config-shape", err.detail);
|
|
2432
|
+
throw err;
|
|
2433
|
+
}
|
|
2434
|
+
const added = addedEntries.map((w) => w.entry);
|
|
2435
|
+
const result = {
|
|
2436
|
+
...base,
|
|
2437
|
+
ok: true,
|
|
2438
|
+
file,
|
|
2439
|
+
added,
|
|
2440
|
+
addedEntries,
|
|
2441
|
+
alreadyPresent,
|
|
2442
|
+
conflicting
|
|
2443
|
+
};
|
|
2444
|
+
if (dryRun || added.length === 0) return result;
|
|
2445
|
+
const finalText = nextText.endsWith("\n") ? nextText : `${nextText}\n`;
|
|
2446
|
+
try {
|
|
2447
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
2448
|
+
writeConfigAtomic(file, finalText);
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
return {
|
|
2451
|
+
...fail("write-failed", err instanceof Error ? err.message : String(err)),
|
|
2452
|
+
added: [],
|
|
2453
|
+
addedEntries: []
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
return {
|
|
2457
|
+
...result,
|
|
2458
|
+
wrote: true
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2107
2461
|
|
|
2108
2462
|
//#endregion
|
|
2109
2463
|
//#region src/mcp/run.ts
|
|
@@ -2347,6 +2701,9 @@ function restoreText(r) {
|
|
|
2347
2701
|
/** One human-readable status line per outcome. */
|
|
2348
2702
|
function outcomeLine(o, dryRun, switchCommand, entryName) {
|
|
2349
2703
|
const r = o.result;
|
|
2704
|
+
const harness = getHarness(o.id);
|
|
2705
|
+
const scopeNote = harness ? scopeDef(harness, r.scope)?.note : void 0;
|
|
2706
|
+
const caveat = scopeNote ? ` [${scopeNote}]` : "";
|
|
2350
2707
|
const note = (o.entryName ? ` [as ${o.entryName}]` : "") + (o.fallbackFrom ? ` [${o.fallbackFrom} scope unsupported — used ${r.scope}]` : "");
|
|
2351
2708
|
const where = r.path ? ` → ${r.path}` : "";
|
|
2352
2709
|
const backup = r.backupPath ? dryRun ? ` [original would be backed up to ${r.backupPath}]` : ` [original backed up to ${r.backupPath}]` : "";
|
|
@@ -2355,14 +2712,14 @@ function outcomeLine(o, dryRun, switchCommand, entryName) {
|
|
|
2355
2712
|
if (o.entryName) return `${o.label}: failed [as ${o.entryName}] — ${failureText(o, void 0, o.entryName)}`;
|
|
2356
2713
|
return `${o.label}: failed — ${failureText(o, switchCommand, entryName)}`;
|
|
2357
2714
|
}
|
|
2358
|
-
if (r.alreadyConfigured) return `${o.label}: already set up (${r.scope})${where}${note}`;
|
|
2715
|
+
if (r.alreadyConfigured) return `${o.label}: already set up (${r.scope})${where}${note}${caveat}`;
|
|
2359
2716
|
if (r.command) {
|
|
2360
|
-
if (r.executed) return `${o.label}: installed (${r.scope}) via \`${r.command}\`${replaced}${note}`;
|
|
2361
|
-
return `${o.label} (${r.scope}): run \`${r.command}\`${replaced}${note}`;
|
|
2717
|
+
if (r.executed) return `${o.label}: installed (${r.scope}) via \`${r.command}\`${replaced}${note}${caveat}`;
|
|
2718
|
+
return `${o.label} (${r.scope}): run \`${r.command}\`${replaced}${note}${caveat}`;
|
|
2362
2719
|
}
|
|
2363
|
-
if (dryRun) return `${o.label}: would update (${r.scope})${where} (dry run — no changes)${backup}${replaced}${note}`;
|
|
2364
|
-
if (r.wrote) return `${o.label}: installed (${r.scope})${where}${backup}${replaced}${note}`;
|
|
2365
|
-
return `${o.label}: no change (${r.scope})${where}${note}`;
|
|
2720
|
+
if (dryRun) return `${o.label}: would update (${r.scope})${where} (dry run — no changes)${backup}${replaced}${note}${caveat}`;
|
|
2721
|
+
if (r.wrote) return `${o.label}: installed (${r.scope})${where}${backup}${replaced}${note}${caveat}`;
|
|
2722
|
+
return `${o.label}: no change (${r.scope})${where}${note}${caveat}`;
|
|
2366
2723
|
}
|
|
2367
2724
|
/** Render a report as a plain multi-line summary (used by the CLI + tests). */
|
|
2368
2725
|
function formatReport(report) {
|
|
@@ -2542,11 +2899,180 @@ async function mcpAddHandler(flags) {
|
|
|
2542
2899
|
}
|
|
2543
2900
|
this.process.stdout.write(`\n${nextStepsText(report)}\n`);
|
|
2544
2901
|
if (report.unknownClients.length > 0 || hasFailure(report)) this.process.exitCode = 1;
|
|
2902
|
+
await offerPreapproval(this, report.outcomes, {
|
|
2903
|
+
mode: "non-interactive",
|
|
2904
|
+
dryRun: options.dryRun,
|
|
2905
|
+
flag: flags["preapprove-tools"] ?? false,
|
|
2906
|
+
serverName: entry.serverName
|
|
2907
|
+
});
|
|
2545
2908
|
return;
|
|
2546
2909
|
}
|
|
2547
2910
|
await interactive(this, options.dryRun, url, source, flags, entry, urlKey);
|
|
2548
2911
|
}
|
|
2549
2912
|
/**
|
|
2913
|
+
* The sentence every disclosure carries, verbatim. It must stay literally
|
|
2914
|
+
* true of every tool in `LEVR_PREAPPROVED_TOOLS` (code review F-027).
|
|
2915
|
+
* deploy_gates and transition_issue stay pre-approved (user decision
|
|
2916
|
+
* 2026-10-15): deploy_gates can change or withdraw a gate and
|
|
2917
|
+
* transition_issue can waive one, so the disclosure says that instead of
|
|
2918
|
+
* promising otherwise.
|
|
2919
|
+
*/
|
|
2920
|
+
const PREAPPROVE_EXPLANATION = "These let your agent call these Levr tools without asking each time. None of them can delete anything. deploy_gates can change or withdraw a task's gates, and transition_issue can waive a failing required gate to finish a task; every change and waiver is recorded on the gate.";
|
|
2921
|
+
/** The confirmation question. The default is No. */
|
|
2922
|
+
const PREAPPROVE_QUESTION = "Add these permissions? (y/N)";
|
|
2923
|
+
/** Clients this run installed into that can take a pre-approval in that scope. */
|
|
2924
|
+
function preapproveCandidates(outcomes) {
|
|
2925
|
+
const out = [];
|
|
2926
|
+
for (const o of outcomes) {
|
|
2927
|
+
if (!o.result.ok) continue;
|
|
2928
|
+
const harness = getHarness(o.id);
|
|
2929
|
+
if (!harness?.autoApprove?.file[o.result.scope]) continue;
|
|
2930
|
+
out.push({
|
|
2931
|
+
harness,
|
|
2932
|
+
scope: o.result.scope
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
return out;
|
|
2936
|
+
}
|
|
2937
|
+
/** "get_issue: reads one task" for an entry's tool. */
|
|
2938
|
+
function describeTool(tool) {
|
|
2939
|
+
const description = LEVR_PREAPPROVED_TOOL_DESCRIPTIONS[tool];
|
|
2940
|
+
return description ? `${tool}: ${description}` : tool;
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Who the permission file reaches (code review F-030): a `project` file sits
|
|
2944
|
+
* in the repository and is usually committed, so it grants the tools to
|
|
2945
|
+
* everyone on the repo; a `local` file is personal, and when it is about to
|
|
2946
|
+
* be created somewhere git does not ignore, the user is told so (we never
|
|
2947
|
+
* edit their .gitignore).
|
|
2948
|
+
*/
|
|
2949
|
+
function sharingNote(plan) {
|
|
2950
|
+
if (plan.scope === "project") return "This file is in your repository and is usually committed: these permissions are shared via git with everyone who uses this repo.";
|
|
2951
|
+
if (plan.scope === "local") {
|
|
2952
|
+
const note = "This file is personal to you in this repository.";
|
|
2953
|
+
if (!plan.fileExists && plan.gitIgnored === false) return `${note} It does not exist yet and git does not ignore it, so it would be committed by a "git add" of that folder: add it to .gitignore if you do not want to share it.`;
|
|
2954
|
+
return note;
|
|
2955
|
+
}
|
|
2956
|
+
return null;
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* The full disclosure for one client: the exact file, who it is shared with,
|
|
2960
|
+
* every entry that would be added (each with what its tool does), what is
|
|
2961
|
+
* already there, what the entries mean, any caveat, and how to undo it.
|
|
2962
|
+
* Printed BEFORE anything is written, in every mode.
|
|
2963
|
+
*/
|
|
2964
|
+
function buildPreapproveDisclosure(label, plan) {
|
|
2965
|
+
const lines = [];
|
|
2966
|
+
lines.push(`${label}: pre-approve Levr tools`);
|
|
2967
|
+
lines.push(`File: ${plan.file}`);
|
|
2968
|
+
const sharing = sharingNote(plan);
|
|
2969
|
+
if (sharing) lines.push(sharing);
|
|
2970
|
+
const valueNote = plan.toolValue ? ` (each set to ${JSON.stringify(plan.toolValue)})` : "";
|
|
2971
|
+
lines.push(`Entries to add under "${plan.keyPath}"${valueNote}:`);
|
|
2972
|
+
for (const e of plan.addedEntries) lines.push(` ${e.entry} (${describeTool(e.tool)})`);
|
|
2973
|
+
if (plan.alreadyPresent.length > 0) lines.push(`Already present, left as is: ${plan.alreadyPresent.join(", ")}`);
|
|
2974
|
+
if (plan.conflicting.length > 0) lines.push(`Set differently by you, left as is: ${plan.conflicting.join(", ")}`);
|
|
2975
|
+
lines.push(PREAPPROVE_EXPLANATION);
|
|
2976
|
+
if (plan.caveat) lines.push(plan.caveat);
|
|
2977
|
+
lines.push(`To undo: remove these entries from "${plan.keyPath}" in ${plan.file}: ` + plan.added.join(", "));
|
|
2978
|
+
return lines.join("\n");
|
|
2979
|
+
}
|
|
2980
|
+
function preapproveFailureText(r) {
|
|
2981
|
+
if (r.reason === "allowlist-key-absent" && r.detail) return `permissions not added (${r.detail})`;
|
|
2982
|
+
return `permissions not added (${r.detail ? `${r.reason}: ${r.detail}` : r.reason ?? "unknown"})`;
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* Whether a not-`ok` result counts against an explicit `--preapprove-tools`
|
|
2986
|
+
* (code review F-031). `allowlist-key-absent` is the writer declining on
|
|
2987
|
+
* purpose to protect the user's own allowlist — the safe outcome, so it is
|
|
2988
|
+
* reported but never fails the run.
|
|
2989
|
+
*/
|
|
2990
|
+
function isPreapproveFailure(r) {
|
|
2991
|
+
return !r.ok && r.reason !== "allowlist-key-absent";
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Nothing to add (code review F-052): "already pre-approved" only when every
|
|
2995
|
+
* entry is already there. Entries the user set differently (a Zed deny) are
|
|
2996
|
+
* named as theirs and left alone — never claimed as approved.
|
|
2997
|
+
*/
|
|
2998
|
+
function nothingToAddText(label, plan) {
|
|
2999
|
+
if (plan.conflicting.length === 0) return `${label}: Levr tools are already pre-approved in ${plan.file}.`;
|
|
3000
|
+
const lines = [`${label}: nothing added to ${plan.file}.`, `Set differently by you, left as is: ${plan.conflicting.join(", ")}`];
|
|
3001
|
+
if (plan.alreadyPresent.length > 0) lines.push(`Already pre-approved: ${plan.alreadyPresent.join(", ")}`);
|
|
3002
|
+
return lines.join("\n");
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* After a successful install, offer to pre-approve Levr's non-destructive
|
|
3006
|
+
* tools in each client that has a documented per-tool allow key.
|
|
3007
|
+
*
|
|
3008
|
+
* - Interactive: print the disclosure, then ask with No as the default.
|
|
3009
|
+
* - Non-interactive: never write unless `--preapprove-tools` was passed; with
|
|
3010
|
+
* it, print the same disclosure to stderr before writing.
|
|
3011
|
+
* - `--dry-run`: print the disclosure and write nothing.
|
|
3012
|
+
*
|
|
3013
|
+
* Error direction: fail_fallback(continue). A failure is reported as
|
|
3014
|
+
* "permissions not added" and the run carries on to the next client. When
|
|
3015
|
+
* `--preapprove-tools` was passed, the user ASKED for the write, so a
|
|
3016
|
+
* failure sets exit code 1 once every client has been tried (code review
|
|
3017
|
+
* F-031). An interactive "No" and `--dry-run` are never failures.
|
|
3018
|
+
*
|
|
3019
|
+
* Cancelling a prompt (Ctrl+C) stops the whole offer: no further client is
|
|
3020
|
+
* asked, and each is reported "permissions not added" (code review F-063).
|
|
3021
|
+
*/
|
|
3022
|
+
async function offerPreapproval(ctx, outcomes, opts) {
|
|
3023
|
+
const candidates = preapproveCandidates(outcomes);
|
|
3024
|
+
if (candidates.length === 0) return;
|
|
3025
|
+
const out = (s) => ctx.process.stdout.write(`${s}\n`);
|
|
3026
|
+
if (opts.mode === "non-interactive" && !opts.flag && !opts.dryRun) {
|
|
3027
|
+
out("\nTip: re-run with --preapprove-tools to let your agent call Levr's non-destructive tools without asking each time (add --dry-run to see exactly what it would write).");
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
let failed = false;
|
|
3031
|
+
for (const [index, { harness, scope }] of candidates.entries()) {
|
|
3032
|
+
const plan = preapproveTools(harness, scope, opts.serverName, { dryRun: true });
|
|
3033
|
+
if (!plan.ok) {
|
|
3034
|
+
out(`\n${harness.label}: ${preapproveFailureText(plan)}`);
|
|
3035
|
+
if (!opts.dryRun && isPreapproveFailure(plan)) failed = true;
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
if (plan.added.length === 0) {
|
|
3039
|
+
out(`\n${nothingToAddText(harness.label, plan)}`);
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
const disclosure = buildPreapproveDisclosure(harness.label, plan);
|
|
3043
|
+
if (opts.dryRun) {
|
|
3044
|
+
out(`\n${disclosure}\n(dry run: nothing written)`);
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
if (opts.mode === "non-interactive") ctx.process.stderr.write(`\n${disclosure}\n`);
|
|
3048
|
+
else {
|
|
3049
|
+
out(`\n${disclosure}`);
|
|
3050
|
+
const p = await import("@clack/prompts");
|
|
3051
|
+
const answer = await p.confirm({
|
|
3052
|
+
message: PREAPPROVE_QUESTION,
|
|
3053
|
+
initialValue: false
|
|
3054
|
+
});
|
|
3055
|
+
if (p.isCancel(answer)) {
|
|
3056
|
+
out(`${harness.label}: permissions not added.`);
|
|
3057
|
+
for (const rest of candidates.slice(index + 1)) out(`${rest.harness.label}: permissions not added (cancelled).`);
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
3060
|
+
if (answer !== true) {
|
|
3061
|
+
out(`${harness.label}: permissions not added.`);
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
const written = preapproveTools(harness, scope, opts.serverName, { dryRun: false });
|
|
3066
|
+
if (!written.ok) {
|
|
3067
|
+
out(`${harness.label}: ${preapproveFailureText(written)}`);
|
|
3068
|
+
if (isPreapproveFailure(written)) failed = true;
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
out(`${harness.label}: added ${written.added.length} permission ${written.added.length === 1 ? "entry" : "entries"} to ${written.file}.`);
|
|
3072
|
+
}
|
|
3073
|
+
if (opts.flag && failed) ctx.process.exitCode = 1;
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
2550
3076
|
* For an unpinned entry: the `--workspace` choices. Without a session there is
|
|
2551
3077
|
* no list to show, so it says how to get one. Never fails the run.
|
|
2552
3078
|
*/
|
|
@@ -2722,6 +3248,12 @@ async function interactive(ctx, dryRun, baseUrl, baseSource, flags, entry, resol
|
|
|
2722
3248
|
left
|
|
2723
3249
|
};
|
|
2724
3250
|
p.note(formatReport(report), "Results");
|
|
3251
|
+
await offerPreapproval(ctx, outcomes, {
|
|
3252
|
+
mode: "interactive",
|
|
3253
|
+
dryRun,
|
|
3254
|
+
flag: flags["preapprove-tools"] ?? false,
|
|
3255
|
+
serverName: entry.serverName
|
|
3256
|
+
});
|
|
2725
3257
|
p.outro(nextStepsText(report));
|
|
2726
3258
|
if (hasFailure(report)) ctx.process.exitCode = 1;
|
|
2727
3259
|
}
|