@yagni-app/code 1.0.6 → 1.0.8
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 +91 -46
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -5
- package/dist/claudePlugins.js +129 -21
- package/dist/cli.js +16 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +53 -28
- package/dist/extension/askUserQuestionTool.js +7 -2
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/config.d.ts +6 -0
- package/dist/extension/hooks.d.ts +3 -3
- package/dist/extension/hooks.js +30 -5
- package/dist/extension/index.d.ts +19 -0
- package/dist/extension/index.js +248 -43
- package/dist/extension/mcp/cliConfig.d.ts +1 -1
- package/dist/extension/mcp/cliConfig.js +1 -1
- package/dist/extension/mcp/config.d.ts +24 -2
- package/dist/extension/mcp/config.js +75 -3
- package/dist/extension/mcp/manager.d.ts +3 -1
- package/dist/extension/mcp/manager.js +2 -2
- package/dist/extension/mcp/panel.d.ts +0 -1
- package/dist/extension/mcp/panel.js +13 -3
- package/dist/extension/mcp/startup.js +8 -6
- package/dist/extension/permission/gate.d.ts +101 -2
- package/dist/extension/permission/gate.js +485 -42
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +64 -0
- package/dist/extension/permissionRules/loadConfig.js +105 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +60 -0
- package/dist/extension/permissionRules/pathRules.js +122 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +115 -0
- package/dist/extension/sandbox/config.js +388 -0
- package/dist/extension/sandbox/manager.d.ts +108 -0
- package/dist/extension/sandbox/manager.js +243 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +829 -0
- package/dist/extension/settingsFiles.d.ts +50 -0
- package/dist/extension/settingsFiles.js +206 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +103 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +47 -0
- package/dist/extension/telemetry/register.js +207 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- package/dist/upgrade.js +10 -1
- package/package.json +19 -3
|
@@ -2,6 +2,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { type SpendResponse } from "./costHud.js";
|
|
3
3
|
import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
4
4
|
import { startMcp as defaultStartMcp } from "./mcp/startup.js";
|
|
5
|
+
import { type GuardianGateEvent } from "./permission/gate.js";
|
|
6
|
+
import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
|
|
7
|
+
import { type PluginCliRunner } from "./plugins/panel.js";
|
|
5
8
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
6
9
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
7
10
|
import { type CatalogResult, type ContextBrief } from "./config.js";
|
|
@@ -58,11 +61,18 @@ export interface RegisterYagniDeps {
|
|
|
58
61
|
* homes or spawning servers. Default runs the real startup.
|
|
59
62
|
*/
|
|
60
63
|
startMcp?: typeof defaultStartMcp;
|
|
64
|
+
/** Test seam for the /plugin panel: runs `yagni plugin …` (defaults to spawning YAGNI_CLI_PATH). */
|
|
65
|
+
runPluginCli?: PluginCliRunner;
|
|
61
66
|
/**
|
|
62
67
|
* The CLI init pass (Onramp Door B). Injectable so tests assert it fires on a
|
|
63
68
|
* fresh-workspace first-run and is skipped otherwise, without a network or disk.
|
|
64
69
|
*/
|
|
65
70
|
runInitPass?: typeof defaultRunInitPass;
|
|
71
|
+
/**
|
|
72
|
+
* OTel export registration. Injectable so tests assert the boot handoff of
|
|
73
|
+
* the /context `userEmail` into the telemetry identity without an SDK.
|
|
74
|
+
*/
|
|
75
|
+
registerTelemetry?: typeof defaultRegisterTelemetry;
|
|
66
76
|
/** The workspace id the token is bound to (keys the one-time init marker). */
|
|
67
77
|
getWorkspaceId?: () => string | undefined;
|
|
68
78
|
/** Has the init pass already run once for this workspace? Injectable (no disk in tests). */
|
|
@@ -115,6 +125,15 @@ export declare function buildRunSpendUrl(baseUrl: string, runId: string): string
|
|
|
115
125
|
* Returns null on anything that doesn't look like the real shape.
|
|
116
126
|
*/
|
|
117
127
|
export declare function parseSpendResponse(data: unknown): SpendResponse | null;
|
|
128
|
+
/**
|
|
129
|
+
* Local trail for EVERY terminal gate outcome (sandbox auto-allow included),
|
|
130
|
+
* always on so e2e and support can see what the gate did even when the
|
|
131
|
+
* storage tier is off. Decision K: the always-on line carries outcome
|
|
132
|
+
* metadata ONLY — no command content. The command prefix and the exec
|
|
133
|
+
* justification are command/model content and ride the debug tier (never
|
|
134
|
+
* uploaded by /feedback) plus the opt-in storage stream.
|
|
135
|
+
*/
|
|
136
|
+
export declare function logGateOutcomeTrail(ev: GuardianGateEvent, env: NodeJS.ProcessEnv): void;
|
|
118
137
|
export declare function registerYagni(pi: ExtensionAPI, deps?: RegisterYagniDeps): Promise<void>;
|
|
119
138
|
export default function (pi: ExtensionAPI): Promise<void>;
|
|
120
139
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { logEvent } from "./errorSink.js";
|
|
|
24
24
|
import { registerFeedbackCommands } from "./feedbackCommand.js";
|
|
25
25
|
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
26
26
|
import { codeStateHome } from "./stateHome.js";
|
|
27
|
+
import { mutateLocalConfig } from "./settingsFiles.js";
|
|
27
28
|
import { logTurnLifecycle } from "./turnLog.js";
|
|
28
29
|
import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_PATTERN, isShiftTab } from "./footer.js";
|
|
29
30
|
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
@@ -34,7 +35,12 @@ import { startMcp as defaultStartMcp, wireShutdown, deriveStartupConnectivityNot
|
|
|
34
35
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
35
36
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
36
37
|
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
|
|
38
|
+
import { registerSandbox } from "./sandbox/session.js";
|
|
39
|
+
import { sandboxAutoAllowDecision } from "./sandbox/bash.js";
|
|
40
|
+
import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
|
|
37
41
|
import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
|
|
42
|
+
import { loadPermissionRules } from "./permissionRules/loadConfig.js";
|
|
43
|
+
import { registerPluginPanel } from "./plugins/panel.js";
|
|
38
44
|
import { registerSubagents } from "./subagents.js";
|
|
39
45
|
import { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
40
46
|
import { registerTodos } from "./todos.js";
|
|
@@ -99,6 +105,39 @@ export function parseSpendResponse(data) {
|
|
|
99
105
|
return null;
|
|
100
106
|
return d;
|
|
101
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Local trail for EVERY terminal gate outcome (sandbox auto-allow included),
|
|
110
|
+
* always on so e2e and support can see what the gate did even when the
|
|
111
|
+
* storage tier is off. Decision K: the always-on line carries outcome
|
|
112
|
+
* metadata ONLY — no command content. The command prefix and the exec
|
|
113
|
+
* justification are command/model content and ride the debug tier (never
|
|
114
|
+
* uploaded by /feedback) plus the opt-in storage stream.
|
|
115
|
+
*/
|
|
116
|
+
export function logGateOutcomeTrail(ev, env) {
|
|
117
|
+
logEvent({
|
|
118
|
+
source: "guardian",
|
|
119
|
+
level: "info",
|
|
120
|
+
event: "gate_outcome",
|
|
121
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
122
|
+
fields: {
|
|
123
|
+
outcome: ev.outcome,
|
|
124
|
+
mode: ev.mode,
|
|
125
|
+
consulted: ev.consulted,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
if (isDebug(env)) {
|
|
129
|
+
logEvent({
|
|
130
|
+
source: "guardian",
|
|
131
|
+
level: "debug",
|
|
132
|
+
event: "gate_outcome",
|
|
133
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
134
|
+
fields: {
|
|
135
|
+
commandPrefix: storagePrefix(ev.command),
|
|
136
|
+
...(ev.execJustification ? { execJustification: ev.execJustification } : {}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
102
141
|
export async function registerYagni(pi, deps = {}) {
|
|
103
142
|
const baseUrl = deps.baseUrl ?? resolveBaseUrl();
|
|
104
143
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -239,6 +278,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
239
278
|
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
240
279
|
const ultraHolder = createUltraHolder();
|
|
241
280
|
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine, childUsage });
|
|
281
|
+
// /plugin + /reload-plugins: manage Claude Code marketplace plugins from the
|
|
282
|
+
// session, driving the launcher's `yagni plugin …` (YAGNI_CLI_PATH).
|
|
283
|
+
registerPluginPanel(pi, { env: deps.env, runCli: deps.runPluginCli });
|
|
242
284
|
registerUltraCommand(pi, ultraHolder);
|
|
243
285
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
244
286
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
@@ -334,6 +376,8 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
334
376
|
// The static queue only carries non-connectivity notices (e.g. the
|
|
335
377
|
// `.mcp.json` approval prompt), which stay one-shot.
|
|
336
378
|
let connectivityNoticesFlushed = false;
|
|
379
|
+
// permission-rule notices are one-shot like connectivity notices.
|
|
380
|
+
let rulesNoticesFlushed = false;
|
|
337
381
|
pi.on("after_provider_response", (_event, ctx) => {
|
|
338
382
|
try {
|
|
339
383
|
if (!ctx.hasUI)
|
|
@@ -347,6 +391,21 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
347
391
|
while (mcpApprovalNotices.length > 0) {
|
|
348
392
|
ctx.ui.notify(mcpApprovalNotices.shift(), "info");
|
|
349
393
|
}
|
|
394
|
+
// one-shot permission-rule config warnings (bad files,
|
|
395
|
+
// never-consulted tools). One consolidated line, never a wall.
|
|
396
|
+
if (!rulesNoticesFlushed) {
|
|
397
|
+
rulesNoticesFlushed = true;
|
|
398
|
+
const parts = [];
|
|
399
|
+
if (loadedRules.diagnostics.warnings.length > 0) {
|
|
400
|
+
parts.push(`${loadedRules.diagnostics.warnings.length} permission-rule config warning(s)`);
|
|
401
|
+
}
|
|
402
|
+
if (loadedRules.diagnostics.neverConsultedTools.length > 0) {
|
|
403
|
+
parts.push(`rules for unknown tools: ${loadedRules.diagnostics.neverConsultedTools.join(", ")} (ignored)`);
|
|
404
|
+
}
|
|
405
|
+
if (parts.length > 0) {
|
|
406
|
+
ctx.ui.notify(`Permission rules: ${parts.join(" · ")}`, "warning");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
350
409
|
}
|
|
351
410
|
catch {
|
|
352
411
|
// A notice must never break a turn.
|
|
@@ -359,6 +418,18 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
359
418
|
// Mutating MCP tools join write/edit/bash in the gate policy: plan mode
|
|
360
419
|
// holds them, review mode confirms them.
|
|
361
420
|
const modeHolder = createModeHolder();
|
|
421
|
+
// OTel export (traces, metrics, log events; Claude Code parity). A no-op
|
|
422
|
+
// unless the launcher's gate set YAGNI_OTEL_EXPORT=1 — see the CLI's
|
|
423
|
+
// otel.ts for the sources and src/telemetry for the exporter.
|
|
424
|
+
const telemetry = (deps.registerTelemetry ?? defaultRegisterTelemetry)(pi, { env });
|
|
425
|
+
{
|
|
426
|
+
let previousMode = modeHolder.get();
|
|
427
|
+
modeHolder.onSet((m) => {
|
|
428
|
+
if (m !== previousMode)
|
|
429
|
+
telemetry.permissionModeChanged(previousMode, m);
|
|
430
|
+
previousMode = m;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
362
433
|
const footerInvalidateHandle = { invalidateGit: () => { }, requestRender: () => { } };
|
|
363
434
|
const guardianState = makeGuardianState();
|
|
364
435
|
// Disabled by the local env override OR the workspace kill switch
|
|
@@ -408,6 +479,25 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
408
479
|
// the startup load is the trust boundary; live reload was reviewed and
|
|
409
480
|
// rejected as a same-session self-authorization path, PR #1698).
|
|
410
481
|
const sessionGrants = evalMode ? [] : loadGrants();
|
|
482
|
+
// settings-based permission rules, loaded once at startup (same
|
|
483
|
+
// trust-boundary posture as grants: no mid-session reload). User config
|
|
484
|
+
// (~/.yagni-code/config.json) + project config (.yagni-code/config.json);
|
|
485
|
+
// project allow rules are trust-gated at evaluation time, deny/ask always.
|
|
486
|
+
const loadedRules = evalMode
|
|
487
|
+
? { rules: [], diagnostics: { warnings: [], neverConsultedTools: [] } }
|
|
488
|
+
: loadPermissionRules({ cwd: process.cwd() });
|
|
489
|
+
for (const w of loadedRules.diagnostics.warnings) {
|
|
490
|
+
logEvent({ source: "permission-rules", level: "warn", event: "config_warning", fields: { warning: w } });
|
|
491
|
+
}
|
|
492
|
+
if (loadedRules.diagnostics.neverConsultedTools.length > 0) {
|
|
493
|
+
logEvent({
|
|
494
|
+
source: "permission-rules",
|
|
495
|
+
level: "warn",
|
|
496
|
+
event: "never_consulted_tools",
|
|
497
|
+
fields: { tools: loadedRules.diagnostics.neverConsultedTools.join(", ") },
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
const rulesStateHome = codeStateHome(null, env);
|
|
411
501
|
const GUARDIAN_EVENT_TIMEOUT_MS = 5_000;
|
|
412
502
|
// YAG-506: load user-configurable lifecycle hooks config and create the
|
|
413
503
|
// hook runner for the permission gate. Skipped in eval mode.
|
|
@@ -415,11 +505,56 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
415
505
|
const hookRunner = evalMode ? null : makeHookRunner({ config: hooksConfig });
|
|
416
506
|
if (!evalMode)
|
|
417
507
|
registerHooks(pi, { config: hooksConfig });
|
|
508
|
+
// OS-level bash sandboxing. Registers the sandbox-aware bash tool
|
|
509
|
+
// (dangerouslyDisableSandbox param + wrap + violation annotation) and the
|
|
510
|
+
// session lifecycle (init/reset, fail-soft unless failIfUnavailable).
|
|
511
|
+
// Null on unsupported platforms — stock bash stays. Shares the loaded
|
|
512
|
+
// permission rules so sandbox config and the gate read one rule source.
|
|
513
|
+
const sandboxHandle = evalMode
|
|
514
|
+
? null
|
|
515
|
+
: registerSandbox(pi, {
|
|
516
|
+
cwd: process.cwd(),
|
|
517
|
+
env,
|
|
518
|
+
hasUI: (ctx) => ctx.hasUI,
|
|
519
|
+
// Anchors project-protected paths (.yagni-code + its config.json in
|
|
520
|
+
// denyWrite) and project-sourced permission rules to the repo the
|
|
521
|
+
// session runs in — same root the gate uses for its rule anchoring.
|
|
522
|
+
projectRoot: process.cwd(),
|
|
523
|
+
});
|
|
524
|
+
if (sandboxHandle)
|
|
525
|
+
sandboxHandle.setRules(loadedRules.rules);
|
|
418
526
|
registerPermissionGate(pi, {
|
|
527
|
+
...(sandboxHandle
|
|
528
|
+
? {
|
|
529
|
+
sandboxAutoAllow: (toolName, params) => sandboxAutoAllowDecision(toolName, params, sandboxHandle.manager, sandboxHandle.settings()),
|
|
530
|
+
}
|
|
531
|
+
: {}),
|
|
419
532
|
modeHolder,
|
|
420
533
|
guardianState,
|
|
421
534
|
guardianLimits,
|
|
422
535
|
guardianTier,
|
|
536
|
+
onToolDecision: (ev) => telemetry.toolDecision(ev),
|
|
537
|
+
...(loadedRules.rules.length > 0
|
|
538
|
+
? {
|
|
539
|
+
permissionRules: loadedRules.rules,
|
|
540
|
+
rulesUserStateHome: rulesStateHome,
|
|
541
|
+
rulesProjectRoot: process.cwd(),
|
|
542
|
+
onRuleVerdict: (ev) => {
|
|
543
|
+
logEvent({
|
|
544
|
+
source: "permission-rules",
|
|
545
|
+
level: "debug",
|
|
546
|
+
event: `rule_${ev.verdict}`,
|
|
547
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
548
|
+
fields: {
|
|
549
|
+
tool: ev.toolName,
|
|
550
|
+
via: ev.matchedVia,
|
|
551
|
+
source: ev.rule.source,
|
|
552
|
+
rule: ev.rule.raw,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
: {}),
|
|
423
558
|
guardianDisabled,
|
|
424
559
|
childUsage,
|
|
425
560
|
guardianReview: (command, deps) => reviewCommand(command, { ...deps, modelTier: guardianTier }),
|
|
@@ -471,57 +606,91 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
471
606
|
if (!evalMode)
|
|
472
607
|
appendGrant(grant);
|
|
473
608
|
},
|
|
609
|
+
// persist a project-local allow rule (Guardian ask dialog's third
|
|
610
|
+
// option — Claude Code's own default save destination for personal
|
|
611
|
+
// rules is localSettings; ours matches). Atomic write, never overwrites
|
|
612
|
+
// other keys. THROWS on failure — the gate's catch owns the fail-soft
|
|
613
|
+
// behavior (notify the user, record ask_approved not remembered); a
|
|
614
|
+
// catch HERE would swallow the throw and make that path unreachable,
|
|
615
|
+
// leaving the user believing a rule saved that will not survive restart.
|
|
616
|
+
// Local writes fire-and-forget the global gitignore helper so the file
|
|
617
|
+
// is never committed by accident.
|
|
618
|
+
//
|
|
619
|
+
// The cwd is the GATE's per-call session cwd (a session can switch
|
|
620
|
+
// directories; process.cwd() would silently land the rule in another
|
|
621
|
+
// project's local settings while the dialog claims this one). Not
|
|
622
|
+
// registered in eval mode: the gate hides the save option when the dep
|
|
623
|
+
// is absent, so a no-op implementation that would still emit
|
|
624
|
+
// ask_approved_remembered never exists.
|
|
625
|
+
...(evalMode ? {} : {
|
|
626
|
+
persistUserRule: (ruleString, cwd) => {
|
|
627
|
+
mutateLocalConfig(cwd, (config) => {
|
|
628
|
+
const perms = (config.permissions ?? {});
|
|
629
|
+
const allow = Array.isArray(perms.allow) ? [...perms.allow] : [];
|
|
630
|
+
if (!allow.includes(ruleString))
|
|
631
|
+
allow.push(ruleString);
|
|
632
|
+
perms.allow = allow;
|
|
633
|
+
config.permissions = perms;
|
|
634
|
+
});
|
|
635
|
+
logEvent({
|
|
636
|
+
source: "permission-rules",
|
|
637
|
+
level: "info",
|
|
638
|
+
event: "rule_saved",
|
|
639
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
640
|
+
fields: { rule: ruleString, destination: "local" },
|
|
641
|
+
});
|
|
642
|
+
},
|
|
643
|
+
}),
|
|
474
644
|
// Opt-in storage stream (YAG-510). Tier decides what leaves the machine:
|
|
475
645
|
// "off" → nothing (not even sent); "hash" → sha256 + family prefix +
|
|
476
646
|
// metadata, no command content; "raw" → adds client-REDACTED command and
|
|
477
647
|
// rationale. Fire-and-forget: one attempt, short timeout, failures logged
|
|
478
648
|
// fail-soft to the unified sink (source:"guardian") — a storage outage
|
|
479
649
|
// never touches the session.
|
|
480
|
-
onGuardianEvent:
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
method: "POST",
|
|
503
|
-
headers: {
|
|
504
|
-
"content-type": "application/json",
|
|
505
|
-
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
506
|
-
...attributionHeaders(deps.env),
|
|
507
|
-
},
|
|
508
|
-
body: JSON.stringify(body),
|
|
509
|
-
}, {
|
|
510
|
-
fetchImpl: authedFetch,
|
|
511
|
-
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
512
|
-
});
|
|
513
|
-
if (!res.ok) {
|
|
514
|
-
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
515
|
-
}
|
|
650
|
+
onGuardianEvent: (ev) => {
|
|
651
|
+
logGateOutcomeTrail(ev, env);
|
|
652
|
+
// Opt-in storage stream (YAG-510): tier decides what leaves the machine.
|
|
653
|
+
if (guardianStorageTier === "off" || evalMode)
|
|
654
|
+
return;
|
|
655
|
+
void (async () => {
|
|
656
|
+
try {
|
|
657
|
+
const body = {
|
|
658
|
+
sessionId: env.YAGNI_SESSION_ID ?? null,
|
|
659
|
+
commandHash: createHash("sha256").update(ev.command).digest("hex"),
|
|
660
|
+
commandPrefix: storagePrefix(ev.command),
|
|
661
|
+
outcome: ev.outcome,
|
|
662
|
+
mode: ev.mode,
|
|
663
|
+
...(ev.execJustification ? { execJustification: ev.execJustification } : {}),
|
|
664
|
+
...(ev.riskLevel ? { riskLevel: ev.riskLevel } : {}),
|
|
665
|
+
...(ev.tier ? { tier: ev.tier } : {}),
|
|
666
|
+
...(ev.durationMs !== undefined ? { durationMs: ev.durationMs } : {}),
|
|
667
|
+
};
|
|
668
|
+
if (guardianStorageTier === "raw") {
|
|
669
|
+
body.command = redactCommand(ev.command);
|
|
670
|
+
if (ev.rationale)
|
|
671
|
+
body.rationale = redactCommand(ev.rationale);
|
|
516
672
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
673
|
+
const res = await resilientFetch(`${baseUrl}/api/yagni-code/guardian-events`, {
|
|
674
|
+
method: "POST",
|
|
675
|
+
headers: {
|
|
676
|
+
"content-type": "application/json",
|
|
677
|
+
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
678
|
+
...attributionHeaders(deps.env),
|
|
679
|
+
},
|
|
680
|
+
body: JSON.stringify(body),
|
|
681
|
+
}, {
|
|
682
|
+
fetchImpl: authedFetch,
|
|
683
|
+
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
684
|
+
});
|
|
685
|
+
if (!res.ok) {
|
|
686
|
+
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
522
687
|
}
|
|
523
|
-
}
|
|
524
|
-
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
guardianLogSink({ event: "guardian_event_post_failed", kind: "network" });
|
|
691
|
+
}
|
|
692
|
+
})();
|
|
693
|
+
},
|
|
525
694
|
...(mcpMutatingSet.size > 0 || mcpOutcome.manager
|
|
526
695
|
? {
|
|
527
696
|
policy: {
|
|
@@ -671,6 +840,29 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
671
840
|
contextBrief = undefined;
|
|
672
841
|
}
|
|
673
842
|
}
|
|
843
|
+
// The developer's account email for the OTel export (`user.email`), so a
|
|
844
|
+
// collector can attribute sessions per person. Set before session_start
|
|
845
|
+
// fires, so even the session.count metric carries it. No-op when export is
|
|
846
|
+
// off or the launcher already forwarded YAGNI_USER_EMAIL.
|
|
847
|
+
const bootUserEmail = typeof briefResult?.userEmail === "string" ? briefResult.userEmail : undefined;
|
|
848
|
+
telemetry.setUserEmail(bootUserEmail);
|
|
849
|
+
if (telemetry.enabled && !bootUserEmail && !telemetry.config.identity.userEmail) {
|
|
850
|
+
// Attribution would otherwise degrade silently: leave a trail in the local
|
|
851
|
+
// error sink so "why is user.email missing in the collector" is answerable.
|
|
852
|
+
// Info, not warn: one line per session start, and an account with no
|
|
853
|
+
// email hits it by design. The fields name each way the email can be
|
|
854
|
+
// absent — the boot fetch failed or lacked the field, and whether the
|
|
855
|
+
// launcher's YAGNI_USER_EMAIL was unset or forwarded blank.
|
|
856
|
+
logEvent({
|
|
857
|
+
source: "telemetry",
|
|
858
|
+
level: "info",
|
|
859
|
+
event: "user_email_missing",
|
|
860
|
+
fields: {
|
|
861
|
+
contextFetch: briefResult === null ? "failed_or_skipped" : "no_email_field",
|
|
862
|
+
launcherEmail: env.YAGNI_USER_EMAIL === undefined ? "unset" : "blank",
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
}
|
|
674
866
|
// M1 ambient judgment recall: after a `read`, append the recorded judgment for
|
|
675
867
|
// that path to the tool result (result modification is supported on pi's
|
|
676
868
|
// `tool_result` event — verified against 0.80.2). No-op when the corpus is thin
|
|
@@ -753,12 +945,25 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
753
945
|
try {
|
|
754
946
|
registerCondensedTools(pi, {
|
|
755
947
|
...(scratchpadDirPath ? { scratchpadDir: scratchpadDirPath } : {}),
|
|
948
|
+
// condensed owns the FINAL bash registration (it re-registers
|
|
949
|
+
// built-ins last), so the sandbox composes INTO it here. Identity when
|
|
950
|
+
// the sandbox is off.
|
|
951
|
+
...(sandboxHandle ? { wrapBash: sandboxHandle.composeBash } : {}),
|
|
756
952
|
});
|
|
757
953
|
}
|
|
758
954
|
catch {
|
|
759
955
|
// Rendering must never break activation; pi's built-ins remain.
|
|
760
956
|
}
|
|
761
957
|
}
|
|
958
|
+
else if (sandboxHandle && env.YAGNI_CLASSIC_TOOL_ROWS !== "1") {
|
|
959
|
+
// Condensed inactive (eval mode / desktop): the sandbox's own
|
|
960
|
+
// registration is the one that reaches the tool registry. NOT under the
|
|
961
|
+
// classic-rows escape hatch — classic bash must stay pi's stock
|
|
962
|
+
// registration (the hatch's contract); a competing one would replace
|
|
963
|
+
// classic rendering with the sandbox's wrap. Desktop/eval still need
|
|
964
|
+
// the sandbox to reach bash, so the hatch is the one carve-out.
|
|
965
|
+
sandboxHandle.registerOwnBash();
|
|
966
|
+
}
|
|
762
967
|
// Own the identity + inject live company context (and repo rules) on every
|
|
763
968
|
// turn. The extension loads identically in every pi process this app spawns —
|
|
764
969
|
// the interactive driver AND every `/go` stage child, subagent, and advisor
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* from its bundled copy). Pure config I/O — no pi imports, no TUI, no network —
|
|
5
5
|
* so importing it from the launcher is side-effect-free.
|
|
6
6
|
*/
|
|
7
|
-
export { McpServerConfig, McpScope, PROJECT_CONFIG_FILENAME, ScopedMcpServerConfig, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
7
|
+
export { McpServerConfig, McpScope, PLUGIN_MCP_ENV, PROJECT_CONFIG_FILENAME, ScopedMcpServerConfig, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readPluginMcpServers, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
8
8
|
export { ProjectApprovalState, decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
|
|
9
9
|
export { McpAuthFile, StoredOAuthEntry, deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
|
|
10
10
|
export { revokeTokensOnRemove } from "./auth.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* from its bundled copy). Pure config I/O — no pi imports, no TUI, no network —
|
|
5
5
|
* so importing it from the launcher is side-effect-free.
|
|
6
6
|
*/
|
|
7
|
-
export { PROJECT_CONFIG_FILENAME, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
7
|
+
export { PLUGIN_MCP_ENV, PROJECT_CONFIG_FILENAME, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readPluginMcpServers, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
8
8
|
export { decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
|
|
9
9
|
export { deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
|
|
10
10
|
export { revokeTokensOnRemove } from "./auth.js";
|
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* A repo configured for Claude Code works here with zero changes.
|
|
7
7
|
* - user: top-level `mcpServers` in `~/.yagni-code/mcp.json`.
|
|
8
8
|
* - local: `projects[<absPath>].mcpServers` in the same file, keyed by cwd.
|
|
9
|
+
* - plugin: servers bundled in Claude Code plugins (`.mcp.json` at the plugin
|
|
10
|
+
* root), handed over by the yagni launcher as JSON in
|
|
11
|
+
* `YAGNI_CLAUDE_PLUGIN_MCP` (see yagni-code-cli claudeCompat.ts). Entries
|
|
12
|
+
* marked `gated` came from repo config and ride the project approval gate.
|
|
9
13
|
*
|
|
10
|
-
* Merge precedence (Claude Code parity): user < project < local —
|
|
14
|
+
* Merge precedence (Claude Code parity): plugin < user < project < local —
|
|
15
|
+
* later wins, so an explicit user entry beats a plugin's.
|
|
11
16
|
* Writes are atomic (temp file + rename) so two concurrent sessions can never
|
|
12
17
|
* interleave into a corrupt config. Validation errors are collected and
|
|
13
18
|
* surfaced, never fatal: one malformed server entry must not hide the rest.
|
|
14
19
|
*/
|
|
15
|
-
export type McpScope = "user" | "project" | "local";
|
|
20
|
+
export type McpScope = "user" | "project" | "local" | "plugin";
|
|
16
21
|
export interface McpStdioServerConfig {
|
|
17
22
|
type?: "stdio";
|
|
18
23
|
command: string;
|
|
@@ -39,7 +44,24 @@ export interface ScopedMcpServerConfig {
|
|
|
39
44
|
scope: McpScope;
|
|
40
45
|
/** Where this entry came from (file path) — surfaced in /mcp and `yagni mcp get`. */
|
|
41
46
|
sourcePath: string;
|
|
47
|
+
/** `name@marketplace` of the plugin that bundles this server (plugin scope). */
|
|
48
|
+
plugin?: string;
|
|
49
|
+
/** Plugin server that came from repo config: needs the project approval gate. */
|
|
50
|
+
gated?: boolean;
|
|
42
51
|
}
|
|
52
|
+
/** Launcher → extension: plugin-bundled MCP servers (JSON array). */
|
|
53
|
+
export declare const PLUGIN_MCP_ENV = "YAGNI_CLAUDE_PLUGIN_MCP";
|
|
54
|
+
export interface PluginMcpEnvEntry {
|
|
55
|
+
plugin: string;
|
|
56
|
+
sourcePath: string;
|
|
57
|
+
gated: boolean;
|
|
58
|
+
servers: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
/** Parse the launcher's plugin MCP payload; malformed input degrades to errors, never a throw. */
|
|
61
|
+
export declare function readPluginMcpServers(env?: NodeJS.ProcessEnv): {
|
|
62
|
+
servers: ScopedMcpServerConfig[];
|
|
63
|
+
errors: McpConfigError[];
|
|
64
|
+
};
|
|
43
65
|
export interface McpConfigError {
|
|
44
66
|
/** File the problem was found in. */
|
|
45
67
|
sourcePath: string;
|
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
* A repo configured for Claude Code works here with zero changes.
|
|
7
7
|
* - user: top-level `mcpServers` in `~/.yagni-code/mcp.json`.
|
|
8
8
|
* - local: `projects[<absPath>].mcpServers` in the same file, keyed by cwd.
|
|
9
|
+
* - plugin: servers bundled in Claude Code plugins (`.mcp.json` at the plugin
|
|
10
|
+
* root), handed over by the yagni launcher as JSON in
|
|
11
|
+
* `YAGNI_CLAUDE_PLUGIN_MCP` (see yagni-code-cli claudeCompat.ts). Entries
|
|
12
|
+
* marked `gated` came from repo config and ride the project approval gate.
|
|
9
13
|
*
|
|
10
|
-
* Merge precedence (Claude Code parity): user < project < local —
|
|
14
|
+
* Merge precedence (Claude Code parity): plugin < user < project < local —
|
|
15
|
+
* later wins, so an explicit user entry beats a plugin's.
|
|
11
16
|
* Writes are atomic (temp file + rename) so two concurrent sessions can never
|
|
12
17
|
* interleave into a corrupt config. Validation errors are collected and
|
|
13
18
|
* surfaced, never fatal: one malformed server entry must not hide the rest.
|
|
@@ -15,6 +20,70 @@
|
|
|
15
20
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
21
|
import { dirname, join } from "node:path";
|
|
17
22
|
import { codeStateHome } from "../stateHome.js";
|
|
23
|
+
/** Launcher → extension: plugin-bundled MCP servers (JSON array). */
|
|
24
|
+
export const PLUGIN_MCP_ENV = "YAGNI_CLAUDE_PLUGIN_MCP";
|
|
25
|
+
const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
|
|
26
|
+
/** Parse the launcher's plugin MCP payload; malformed input degrades to errors, never a throw. */
|
|
27
|
+
export function readPluginMcpServers(env = process.env) {
|
|
28
|
+
const disabled = env[CLAUDE_COMPAT_DISABLE_ENV];
|
|
29
|
+
if (disabled !== undefined && disabled !== "" && disabled !== "0")
|
|
30
|
+
return { servers: [], errors: [] };
|
|
31
|
+
const raw = env[PLUGIN_MCP_ENV];
|
|
32
|
+
if (!raw)
|
|
33
|
+
return { servers: [], errors: [] };
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
servers: [],
|
|
41
|
+
errors: [{ sourcePath: PLUGIN_MCP_ENV, message: `could not parse plugin MCP payload (${err instanceof Error ? err.message : String(err)})` }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(parsed))
|
|
45
|
+
return { servers: [], errors: [{ sourcePath: PLUGIN_MCP_ENV, message: "plugin MCP payload is not an array" }] };
|
|
46
|
+
const servers = [];
|
|
47
|
+
const errors = [];
|
|
48
|
+
const seen = new Map();
|
|
49
|
+
for (const item of parsed) {
|
|
50
|
+
if (!item || typeof item !== "object")
|
|
51
|
+
continue;
|
|
52
|
+
const entry = item;
|
|
53
|
+
if (typeof entry.plugin !== "string" || !entry.servers || typeof entry.servers !== "object")
|
|
54
|
+
continue;
|
|
55
|
+
const sourcePath = typeof entry.sourcePath === "string" ? entry.sourcePath : entry.plugin;
|
|
56
|
+
for (const [name, value] of Object.entries(entry.servers)) {
|
|
57
|
+
const validation = validateServerConfig(value);
|
|
58
|
+
if (!validation.ok) {
|
|
59
|
+
errors.push({ sourcePath, serverName: name, message: `${validation.message} (plugin ${entry.plugin})` });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const owner = seen.get(name);
|
|
63
|
+
if (owner) {
|
|
64
|
+
errors.push({
|
|
65
|
+
sourcePath,
|
|
66
|
+
serverName: name,
|
|
67
|
+
message: `also defined by plugin ${owner} — keeping that one, skipping ${entry.plugin}'s`,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const config = value;
|
|
72
|
+
const expanded = expandServerEnv(config, env);
|
|
73
|
+
if (expanded.missingVars.length > 0) {
|
|
74
|
+
errors.push({
|
|
75
|
+
sourcePath,
|
|
76
|
+
serverName: name,
|
|
77
|
+
message: `missing environment variable(s): ${expanded.missingVars.join(", ")} — server skipped`,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
seen.set(name, entry.plugin);
|
|
82
|
+
servers.push({ name, config, scope: "plugin", sourcePath, plugin: entry.plugin, gated: entry.gated === true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { servers, errors };
|
|
86
|
+
}
|
|
18
87
|
export const PROJECT_CONFIG_FILENAME = ".mcp.json";
|
|
19
88
|
/**
|
|
20
89
|
* Expand `${VAR}` / `${VAR:-default}` in a config string (Claude Code parity —
|
|
@@ -239,7 +308,10 @@ function defaultExists(p) {
|
|
|
239
308
|
export function loadMcpServers(cwd, env = process.env) {
|
|
240
309
|
const repoRoot = resolveProjectRoot(cwd);
|
|
241
310
|
const errors = [];
|
|
242
|
-
const byScope = { user: [], project: [], local: [] };
|
|
311
|
+
const byScope = { plugin: [], user: [], project: [], local: [] };
|
|
312
|
+
const pluginLoad = readPluginMcpServers(env);
|
|
313
|
+
errors.push(...pluginLoad.errors);
|
|
314
|
+
byScope.plugin.push(...pluginLoad.servers);
|
|
243
315
|
const { file: userFile, errors: userErrors } = readUserMcpConfig();
|
|
244
316
|
errors.push(...userErrors);
|
|
245
317
|
const userServers = userFile.mcpServers ?? {};
|
|
@@ -300,7 +372,7 @@ export function loadMcpServers(cwd, env = process.env) {
|
|
|
300
372
|
}
|
|
301
373
|
}
|
|
302
374
|
const merged = new Map();
|
|
303
|
-
for (const scope of ["user", "project", "local"]) {
|
|
375
|
+
for (const scope of ["plugin", "user", "project", "local"]) {
|
|
304
376
|
for (const server of byScope[scope])
|
|
305
377
|
merged.set(server.name, server);
|
|
306
378
|
}
|
|
@@ -20,6 +20,8 @@ export interface ManagedServer {
|
|
|
20
20
|
scope: McpScope;
|
|
21
21
|
config: McpStdioServerConfig | McpHttpServerConfig;
|
|
22
22
|
status: McpServerStatus;
|
|
23
|
+
/** Plugin server sourced from repo config: rides the project approval gate. */
|
|
24
|
+
gated?: boolean;
|
|
23
25
|
/** Failure detail for the failed/needs_auth states (panel + logs). */
|
|
24
26
|
error?: string;
|
|
25
27
|
client?: Client;
|
|
@@ -51,7 +53,7 @@ export declare class McpManager {
|
|
|
51
53
|
list(): ManagedServer[];
|
|
52
54
|
get(name: string): ManagedServer | undefined;
|
|
53
55
|
/** Registers a server without connecting (panel listing, kill-switch states). */
|
|
54
|
-
register(name: string, scope: McpScope, config: McpStdioServerConfig | McpHttpServerConfig, status?: McpServerStatus): ManagedServer;
|
|
56
|
+
register(name: string, scope: McpScope, config: McpStdioServerConfig | McpHttpServerConfig, status?: McpServerStatus, gated?: boolean): ManagedServer;
|
|
55
57
|
connect(name: string): Promise<ManagedServer | undefined>;
|
|
56
58
|
disconnect(name: string): Promise<void>;
|
|
57
59
|
/** Reconnect with bounded attempts, panel-visible progress. */
|
|
@@ -47,11 +47,11 @@ export class McpManager {
|
|
|
47
47
|
return this.servers.get(name);
|
|
48
48
|
}
|
|
49
49
|
/** Registers a server without connecting (panel listing, kill-switch states). */
|
|
50
|
-
register(name, scope, config, status = "disabled") {
|
|
50
|
+
register(name, scope, config, status = "disabled", gated = false) {
|
|
51
51
|
const existing = this.servers.get(name);
|
|
52
52
|
if (existing)
|
|
53
53
|
return existing;
|
|
54
|
-
const server = { name, scope, config, status };
|
|
54
|
+
const server = { name, scope, config, status, ...(gated ? { gated } : {}) };
|
|
55
55
|
this.servers.set(name, server);
|
|
56
56
|
return server;
|
|
57
57
|
}
|
|
@@ -21,7 +21,6 @@ export interface PanelServerInfo {
|
|
|
21
21
|
}
|
|
22
22
|
export declare function statusIcon(status: string): string;
|
|
23
23
|
export declare function statusText(server: ManagedServer): string;
|
|
24
|
-
/** Plain-text listing, mirroring CC's `claude mcp list` and panel states. */
|
|
25
24
|
export declare function renderPanelListing(servers: ManagedServer[], errors: string[]): string;
|
|
26
25
|
export interface PanelDeps {
|
|
27
26
|
cwd: string;
|