@yagni-app/code 1.0.5 → 1.0.6
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 +30 -6
- package/dist/claudePlugins.d.ts +3 -1
- package/dist/claudePlugins.js +3 -1
- package/dist/cli.js +12 -0
- package/dist/doctor.d.ts +28 -3
- package/dist/doctor.js +117 -7
- package/dist/extension/index.d.ts +5 -5
- package/dist/extension/index.js +89 -29
- package/dist/extension/mcp/approval.d.ts +45 -0
- package/dist/extension/mcp/approval.js +164 -0
- package/dist/extension/mcp/auth.d.ts +124 -0
- package/dist/extension/mcp/auth.js +560 -0
- package/dist/extension/mcp/authStore.d.ts +61 -0
- package/dist/extension/mcp/authStore.js +105 -0
- package/dist/extension/mcp/callbackPage.d.ts +31 -0
- package/dist/extension/mcp/callbackPage.js +222 -0
- package/dist/extension/mcp/cliConfig.d.ts +12 -0
- package/dist/extension/mcp/cliConfig.js +12 -0
- package/dist/extension/mcp/config.d.ts +131 -0
- package/dist/extension/mcp/config.js +309 -0
- package/dist/extension/mcp/log.d.ts +28 -0
- package/dist/extension/mcp/log.js +82 -0
- package/dist/extension/mcp/manager.d.ts +98 -0
- package/dist/extension/mcp/manager.js +273 -0
- package/dist/extension/mcp/names.d.ts +25 -0
- package/dist/extension/mcp/names.js +40 -0
- package/dist/extension/mcp/panel.d.ts +34 -0
- package/dist/extension/mcp/panel.js +258 -0
- package/dist/extension/mcp/prompts.d.ts +23 -0
- package/dist/extension/mcp/prompts.js +93 -0
- package/dist/extension/mcp/startup.d.ts +55 -0
- package/dist/extension/mcp/startup.js +150 -0
- package/dist/extension/mcp/tools.d.ts +31 -0
- package/dist/extension/mcp/tools.js +117 -0
- package/dist/extension/mcp/transports.d.ts +17 -0
- package/dist/extension/mcp/transports.js +44 -0
- package/dist/extension/permission/gate.d.ts +7 -0
- package/dist/extension/permission/gate.js +12 -5
- package/dist/extension/permission/guardian.d.ts +24 -5
- package/dist/extension/permission/guardian.js +162 -24
- package/dist/extension/pipeline/personas.js +5 -0
- package/dist/mcpCommand.d.ts +113 -0
- package/dist/mcpCommand.js +755 -0
- package/dist/otel.d.ts +36 -7
- package/dist/otel.js +90 -12
- package/dist/upgrade.d.ts +11 -2
- package/dist/upgrade.js +48 -8
- package/package.json +3 -2
- package/dist/extension/mcpTools.d.ts +0 -57
- package/dist/extension/mcpTools.js +0 -132
package/README.md
CHANGED
|
@@ -179,11 +179,16 @@ children and subagents — emits a per-prompt span tree: interaction → LLM
|
|
|
179
179
|
request → tool calls, following the OTel GenAI semantic conventions. Nothing is
|
|
180
180
|
exported unless you configure an endpoint.
|
|
181
181
|
|
|
182
|
-
Enable it one of
|
|
183
|
-
|
|
184
|
-
- `OTEL_EXPORTER_OTLP_ENDPOINT
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
Enable it one of four ways (first match wins):
|
|
183
|
+
|
|
184
|
+
- `OTEL_EXPORTER_OTLP_ENDPOINT=…` in the environment (a personal override —
|
|
185
|
+
handy for pointing one session at a scratch collector), or
|
|
186
|
+
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=…` in the environment — the per-signal
|
|
187
|
+
form Claude Code setups use (e.g.
|
|
188
|
+
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://otlp.datadoghq.com/v1/traces`
|
|
189
|
+
with `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf`); the launcher
|
|
190
|
+
bridges it to the generic var pi-otel reads, so a Claude Code OTel env
|
|
191
|
+
lights up YAGNI Code traces with no extra config, or
|
|
187
192
|
- **workspace settings** (the zero-setup path): a workspace admin sets the
|
|
188
193
|
endpoint, protocol, and any collector headers (e.g. a Datadog API key) once
|
|
189
194
|
in the web app under Settings → YAGNI Code → Trace export. Every session in
|
|
@@ -197,7 +202,26 @@ Enable it one of three ways (first match wins):
|
|
|
197
202
|
Standard OTel env vars are honored (`OTEL_EXPORTER_OTLP_PROTOCOL`,
|
|
198
203
|
`OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME` — defaults to `yagni-code`),
|
|
199
204
|
and `PI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
|
|
200
|
-
export state
|
|
205
|
+
export state and probes the endpoint live (unreachable → a warning with a
|
|
206
|
+
hint, never an exit-code flip).
|
|
207
|
+
|
|
208
|
+
Two recipes for common collectors:
|
|
209
|
+
|
|
210
|
+
- **Datadog, direct SaaS (no agent):** endpoint
|
|
211
|
+
`https://otlp.datadoghq.com/v1/traces` (US1; other sites have their own
|
|
212
|
+
host — `otlp.us3.datadoghq.com`, `otlp.us5.datadoghq.com`,
|
|
213
|
+
`otlp.datadoghq.eu`, `otlp.ap1.datadoghq.com`), protocol `http/protobuf`
|
|
214
|
+
(Datadog's direct trace intake does not accept gRPC), header
|
|
215
|
+
`dd-api-key=<your Datadog API key>`. Use the full path, not `/v1/` — the
|
|
216
|
+
endpoint is passed through verbatim, no path is appended for you.
|
|
217
|
+
- **Datadog Agent:** endpoint `http://<agent-host>:4317`, protocol `grpc`, no
|
|
218
|
+
key (the agent forwards with its own). OTLP ingest ships in the agent but
|
|
219
|
+
is off by default — enable it with
|
|
220
|
+
`otlp_config.receiver.protocols.grpc.endpoint: 0.0.0.0:4317` in
|
|
221
|
+
`datadog.yaml`.
|
|
222
|
+
- **Grafana Cloud:** endpoint `https://otlp-gateway-prod-…-0.grafana.net:443/otlp`,
|
|
223
|
+
protocol `http/protobuf`, header `authorization=<the raw MTgx… value>` (the
|
|
224
|
+
`Basic ` scheme is added for you).
|
|
201
225
|
|
|
202
226
|
Two things are enforced and not configurable:
|
|
203
227
|
|
package/dist/claudePlugins.d.ts
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
20
|
* Deliberately NOT here: network installation of any kind, plugin hooks, MCP
|
|
21
|
-
* servers
|
|
21
|
+
* servers (standard MCP support lives in `src/mcpCommand.ts` + the extension's
|
|
22
|
+
* `mcp/` module — see `yagni mcp --help`), LSP servers, themes, output styles.
|
|
23
|
+
* Discovery is read-only.
|
|
22
24
|
*
|
|
23
25
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
24
26
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
package/dist/claudePlugins.js
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
20
|
* Deliberately NOT here: network installation of any kind, plugin hooks, MCP
|
|
21
|
-
* servers
|
|
21
|
+
* servers (standard MCP support lives in `src/mcpCommand.ts` + the extension's
|
|
22
|
+
* `mcp/` module — see `yagni mcp --help`), LSP servers, themes, output styles.
|
|
23
|
+
* Discovery is read-only.
|
|
22
24
|
*
|
|
23
25
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
24
26
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
|
|
|
23
23
|
import { DISTRIBUTION } from "./distribution.js";
|
|
24
24
|
import { connectCommand } from "./connectClaudeCode.js";
|
|
25
25
|
import { goCommand } from "./goHeadless.js";
|
|
26
|
+
import { mcpCommand } from "./mcpCommand.js";
|
|
26
27
|
import { login } from "./login.js";
|
|
27
28
|
import { logout } from "./logout.js";
|
|
28
29
|
import { tokenCommand } from "./token.js";
|
|
@@ -515,6 +516,10 @@ export const HELP_TEXT = [
|
|
|
515
516
|
" yagni connect claude-code Route Claude Code through the YAGNI model proxy",
|
|
516
517
|
" (--project scopes to this repo; --off disconnects).",
|
|
517
518
|
" yagni connect codex Route Codex CLI through the YAGNI model proxy.",
|
|
519
|
+
" yagni mcp <subcommand> Manage MCP servers: add, remove, list, get,",
|
|
520
|
+
" add-from-claude, and more — run `yagni mcp` for",
|
|
521
|
+
" details. OAuth servers authenticate via the",
|
|
522
|
+
" /mcp panel in a session.",
|
|
518
523
|
" yagni token Output the active environment's API token (for helpers).",
|
|
519
524
|
" yagni use <name> Switch the active environment (sticky).",
|
|
520
525
|
" Presets: prod, local. Others need --base-url <url>.",
|
|
@@ -536,6 +541,7 @@ export const HELP_TEXT = [
|
|
|
536
541
|
" /mode plan|review|auto Hold writes for planning, or confirm each change.",
|
|
537
542
|
" /todos Show the agent's live task list.",
|
|
538
543
|
" /cost Session usage and credit headroom.",
|
|
544
|
+
" /mcp Manage MCP servers; authenticate OAuth servers.",
|
|
539
545
|
"",
|
|
540
546
|
"The active environment is sticky; `use` switches it (prod is the default).",
|
|
541
547
|
"Set YAGNI_BASE_URL to override the base URL for a single run.",
|
|
@@ -653,6 +659,12 @@ export async function main(argv) {
|
|
|
653
659
|
if (command === "connect") {
|
|
654
660
|
return connectCommand(rest);
|
|
655
661
|
}
|
|
662
|
+
// MCP server configuration: same three-scope model and file homes the
|
|
663
|
+
// in-session /mcp panel uses, shared code via the bundled extension's
|
|
664
|
+
// config entry. `list` runs real health-check connections.
|
|
665
|
+
if (command === "mcp") {
|
|
666
|
+
return mcpCommand(rest);
|
|
667
|
+
}
|
|
656
668
|
// The headless pipeline entry. `go` is a real subcommand, not passthrough:
|
|
657
669
|
// the interactive run stays `/go` inside a session, and a `yagni go` without
|
|
658
670
|
// --headless is refused with usage rather than launched as an agent prompt.
|
package/dist/doctor.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Advisory checks (loose perms, missing `gh`) never flip the exit code.
|
|
14
14
|
*/
|
|
15
15
|
import { type TokenExpiryStatus } from "./launch.js";
|
|
16
|
-
import { type OtelLaunchConfig } from "./otel.js";
|
|
16
|
+
import { probeOtelEndpoint, type OtelLaunchConfig } from "./otel.js";
|
|
17
17
|
import { type Profile } from "./profiles.js";
|
|
18
18
|
export type CheckStatus = "ok" | "warn" | "fail";
|
|
19
19
|
export interface CheckResult {
|
|
@@ -63,10 +63,33 @@ export declare function checkCliUpdate(probe: {
|
|
|
63
63
|
export declare function checkGh(onPath: boolean): CheckResult;
|
|
64
64
|
/**
|
|
65
65
|
* Advisory OTel-export line: says whether sessions will stream traces to an
|
|
66
|
-
* OTLP collector,
|
|
66
|
+
* OTLP collector, from which config source, and — when a collector is
|
|
67
|
+
* configured — whether the endpoint answers TCP right now (scheme-default
|
|
68
|
+
* ports included: https with no port probes 443). Never flips the exit code —
|
|
67
69
|
* most machines have no collector, and that is the healthy default.
|
|
68
70
|
*/
|
|
69
|
-
export declare function checkOtelExport(config: OtelLaunchConfig | undefined
|
|
71
|
+
export declare function checkOtelExport(config: OtelLaunchConfig | undefined, deps?: {
|
|
72
|
+
probeEndpoint?: typeof probeOtelEndpoint;
|
|
73
|
+
probeTimeoutMs?: number;
|
|
74
|
+
}): Promise<CheckResult>;
|
|
75
|
+
/** Config-only MCP snapshot (no connections — health checks live in `mcp list`/`get`). */
|
|
76
|
+
export interface McpProbe {
|
|
77
|
+
/** YAGNI_CODE_MCP_DISABLED kill-switch is active. */
|
|
78
|
+
disabled: boolean;
|
|
79
|
+
/** Count of configured servers (post-merge, before the approval gate). */
|
|
80
|
+
serverCount: number;
|
|
81
|
+
/** Project-scope servers that are neither enabled nor disabled. */
|
|
82
|
+
undecidedProjectServers: string[];
|
|
83
|
+
/** Config parse/validation errors (already human-readable). */
|
|
84
|
+
errors: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Advisory (never required) MCP line: reports config health without connecting
|
|
88
|
+
* to anything — `yagni mcp list`/`get` already run the real health checks, and
|
|
89
|
+
* doctor must never spawn an unapproved server process. The kill-switch and
|
|
90
|
+
* pending-approval cases read as `warn`, config errors as `fail` (non-required).
|
|
91
|
+
*/
|
|
92
|
+
export declare function checkMcpConfig(probe: McpProbe): CheckResult;
|
|
70
93
|
/** What the Windows bash probe found (pi needs a bash — Git Bash — on win32). */
|
|
71
94
|
export interface BashProbe {
|
|
72
95
|
found: boolean;
|
|
@@ -95,6 +118,8 @@ export interface DoctorDeps {
|
|
|
95
118
|
probeBash?: () => BashProbe;
|
|
96
119
|
currentVersion?: string;
|
|
97
120
|
probeLatestVersion?: () => Promise<string | null>;
|
|
121
|
+
/** MCP config snapshot (config-only, no connections). */
|
|
122
|
+
probeMcp?: () => Promise<McpProbe>;
|
|
98
123
|
log?: (msg: string) => void;
|
|
99
124
|
}
|
|
100
125
|
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
package/dist/doctor.js
CHANGED
|
@@ -17,9 +17,10 @@ import { delimiter, join } from "node:path";
|
|
|
17
17
|
import { credentialsDir } from "./credentials.js";
|
|
18
18
|
import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
|
|
19
19
|
import { classifyTokenExpiry } from "./launch.js";
|
|
20
|
-
import { resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
20
|
+
import { probeOtelEndpoint, resolveOtelLaunchWithWorkspace, } from "./otel.js";
|
|
21
21
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
22
22
|
import { readActiveProfile } from "./profiles.js";
|
|
23
|
+
import { resolveMcpConfigPath } from "./mcpCommand.js";
|
|
23
24
|
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
24
25
|
export function checkPiEngine(probe) {
|
|
25
26
|
if (!probe.binPath || !probe.binExists) {
|
|
@@ -201,10 +202,12 @@ export function checkGh(onPath) {
|
|
|
201
202
|
}
|
|
202
203
|
/**
|
|
203
204
|
* Advisory OTel-export line: says whether sessions will stream traces to an
|
|
204
|
-
* OTLP collector,
|
|
205
|
+
* OTLP collector, from which config source, and — when a collector is
|
|
206
|
+
* configured — whether the endpoint answers TCP right now (scheme-default
|
|
207
|
+
* ports included: https with no port probes 443). Never flips the exit code —
|
|
205
208
|
* most machines have no collector, and that is the healthy default.
|
|
206
209
|
*/
|
|
207
|
-
export function checkOtelExport(config) {
|
|
210
|
+
export async function checkOtelExport(config, deps = {}) {
|
|
208
211
|
if (!config) {
|
|
209
212
|
return {
|
|
210
213
|
name: "otel export (optional)",
|
|
@@ -214,14 +217,88 @@ export function checkOtelExport(config) {
|
|
|
214
217
|
};
|
|
215
218
|
}
|
|
216
219
|
const source = config.source === "env"
|
|
217
|
-
? "OTEL_EXPORTER_OTLP_ENDPOINT"
|
|
220
|
+
? (config.envVar ?? "OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
218
221
|
: config.source === "workspace"
|
|
219
222
|
? "workspace settings"
|
|
220
223
|
: ".pi/settings.json";
|
|
224
|
+
const base = `on → ${config.endpoint} (${source}, metadata-only)`;
|
|
225
|
+
const doProbe = deps.probeEndpoint ?? probeOtelEndpoint;
|
|
226
|
+
try {
|
|
227
|
+
const reachable = await doProbe(config.endpoint, deps.probeTimeoutMs ?? 1_000);
|
|
228
|
+
return reachable
|
|
229
|
+
? {
|
|
230
|
+
name: "otel export (optional)",
|
|
231
|
+
status: "ok",
|
|
232
|
+
detail: `${base}, TCP reachable`,
|
|
233
|
+
required: false,
|
|
234
|
+
}
|
|
235
|
+
: {
|
|
236
|
+
name: "otel export (optional)",
|
|
237
|
+
status: "warn",
|
|
238
|
+
detail: `${base}, endpoint not reachable`,
|
|
239
|
+
hint: "the collector is down, unreachable from this machine, or the URL is wrong — traces will not export until it answers",
|
|
240
|
+
required: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// The probe itself errored (not "endpoint down" — the check could not
|
|
245
|
+
// run). Distinct from a verified-healthy line so nobody reads an
|
|
246
|
+
// unverified config as checked-and-good.
|
|
247
|
+
return {
|
|
248
|
+
name: "otel export (optional)",
|
|
249
|
+
status: "warn",
|
|
250
|
+
detail: `${base}, reachability probe failed`,
|
|
251
|
+
hint: "the reachability check could not run (network error) — export is configured but unverified; traces may still export normally",
|
|
252
|
+
required: false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Advisory (never required) MCP line: reports config health without connecting
|
|
258
|
+
* to anything — `yagni mcp list`/`get` already run the real health checks, and
|
|
259
|
+
* doctor must never spawn an unapproved server process. The kill-switch and
|
|
260
|
+
* pending-approval cases read as `warn`, config errors as `fail` (non-required).
|
|
261
|
+
*/
|
|
262
|
+
export function checkMcpConfig(probe) {
|
|
263
|
+
if (probe.disabled) {
|
|
264
|
+
return {
|
|
265
|
+
name: "mcp servers",
|
|
266
|
+
status: "warn",
|
|
267
|
+
detail: "disabled (YAGNI_CODE_MCP_DISABLED=1)",
|
|
268
|
+
hint: "unset YAGNI_CODE_MCP_DISABLED to re-enable MCP servers",
|
|
269
|
+
required: false,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (probe.errors.length > 0) {
|
|
273
|
+
return {
|
|
274
|
+
name: "mcp servers",
|
|
275
|
+
status: "fail",
|
|
276
|
+
detail: `${probe.serverCount} configured, ${probe.errors.length} config error${probe.errors.length === 1 ? "" : "s"}`,
|
|
277
|
+
hint: `run \`yagni mcp list\` (first error: ${probe.errors[0]})`,
|
|
278
|
+
required: false,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (probe.serverCount === 0) {
|
|
282
|
+
return {
|
|
283
|
+
name: "mcp servers",
|
|
284
|
+
status: "ok",
|
|
285
|
+
detail: "none configured",
|
|
286
|
+
required: false,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (probe.undecidedProjectServers.length > 0) {
|
|
290
|
+
return {
|
|
291
|
+
name: "mcp servers",
|
|
292
|
+
status: "warn",
|
|
293
|
+
detail: `${probe.serverCount} configured, ${probe.undecidedProjectServers.length} awaiting approval`,
|
|
294
|
+
hint: "run /mcp to approve the project servers",
|
|
295
|
+
required: false,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
221
298
|
return {
|
|
222
|
-
name: "
|
|
299
|
+
name: "mcp servers",
|
|
223
300
|
status: "ok",
|
|
224
|
-
detail:
|
|
301
|
+
detail: `${probe.serverCount} configured`,
|
|
225
302
|
required: false,
|
|
226
303
|
};
|
|
227
304
|
}
|
|
@@ -312,6 +389,37 @@ function defaultProbeStateDir() {
|
|
|
312
389
|
return { path, exists: false, mode: null };
|
|
313
390
|
}
|
|
314
391
|
}
|
|
392
|
+
async function defaultProbeMcp(env = process.env) {
|
|
393
|
+
const disabled = env.YAGNI_CODE_MCP_DISABLED === "1";
|
|
394
|
+
if (disabled)
|
|
395
|
+
return { disabled: true, serverCount: 0, undecidedProjectServers: [], errors: [] };
|
|
396
|
+
try {
|
|
397
|
+
const mod = (await import(resolveMcpConfigPath()));
|
|
398
|
+
if (typeof mod?.loadMcpServers !== "function") {
|
|
399
|
+
return { disabled: false, serverCount: 0, undecidedProjectServers: [], errors: ["extension MCP config entry missing"] };
|
|
400
|
+
}
|
|
401
|
+
const loaded = mod.loadMcpServers(process.cwd(), env);
|
|
402
|
+
const repoRoot = mod.resolveProjectRoot(process.cwd());
|
|
403
|
+
const { state } = mod.readProjectApproval(repoRoot);
|
|
404
|
+
const undecided = loaded.servers
|
|
405
|
+
.filter((s) => s.scope === "project" && mod.decisionFor(state, s.name) === "undecided")
|
|
406
|
+
.map((s) => s.name);
|
|
407
|
+
return {
|
|
408
|
+
disabled: false,
|
|
409
|
+
serverCount: loaded.servers.length,
|
|
410
|
+
undecidedProjectServers: undecided,
|
|
411
|
+
errors: loaded.errors.map((e) => (e.serverName ? `${e.serverName}: ${e.message}` : e.message)),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
return {
|
|
416
|
+
disabled: false,
|
|
417
|
+
serverCount: 0,
|
|
418
|
+
undecidedProjectServers: [],
|
|
419
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
315
423
|
async function defaultProbeBackend(baseUrl, token) {
|
|
316
424
|
if (!token)
|
|
317
425
|
return { kind: "skipped" };
|
|
@@ -378,6 +486,7 @@ export async function gatherChecks(deps = {}) {
|
|
|
378
486
|
const readProfile = deps.readActiveProfile ?? readActiveProfile;
|
|
379
487
|
const probeBackend = deps.probeBackend ?? defaultProbeBackend;
|
|
380
488
|
const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
|
|
489
|
+
const probeMcp = deps.probeMcp ?? (() => defaultProbeMcp());
|
|
381
490
|
const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
|
|
382
491
|
const platform = deps.platform ?? process.platform;
|
|
383
492
|
const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
|
|
@@ -406,7 +515,8 @@ export async function gatherChecks(deps = {}) {
|
|
|
406
515
|
checks.push(checkBackend(backend));
|
|
407
516
|
checks.push(checkStateDir(probeStateDir()));
|
|
408
517
|
checks.push(checkGh(ghOnPath()));
|
|
409
|
-
checks.push(
|
|
518
|
+
checks.push(checkMcpConfig(await probeMcp()));
|
|
519
|
+
checks.push(await checkOtelExport(await resolveOtelLaunchWithWorkspace({
|
|
410
520
|
env: process.env,
|
|
411
521
|
cwd: process.cwd(),
|
|
412
522
|
creds: profile.token ? { baseUrl: profile.baseUrl, token: profile.token } : null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import {
|
|
4
|
+
import { startMcp as defaultStartMcp } from "./mcp/startup.js";
|
|
5
5
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
6
6
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
7
7
|
import { type CatalogResult, type ContextBrief } from "./config.js";
|
|
@@ -53,11 +53,11 @@ export interface RegisterYagniDeps {
|
|
|
53
53
|
/** The beat's once-per-repo marker store. Injectable (no disk in tests). */
|
|
54
54
|
mineBeatMarkers?: MineBeatMarkers;
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Local-MCP startup seam: injectable so tests assert the wiring (approval
|
|
57
|
+
* skip, kill-switch, mutating-tool gate) without touching real config
|
|
58
|
+
* homes or spawning servers. Default runs the real startup.
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
startMcp?: typeof defaultStartMcp;
|
|
61
61
|
/**
|
|
62
62
|
* The CLI init pass (Onramp Door B). Injectable so tests assert it fires on a
|
|
63
63
|
* fresh-workspace first-run and is skipped otherwise, without a network or disk.
|
package/dist/extension/index.js
CHANGED
|
@@ -29,7 +29,8 @@ import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_
|
|
|
29
29
|
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
30
30
|
import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
31
31
|
import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
|
|
32
|
-
import {
|
|
32
|
+
import { looksMutating } from "./mcp/tools.js";
|
|
33
|
+
import { startMcp as defaultStartMcp, wireShutdown, deriveStartupConnectivityNotices } from "./mcp/startup.js";
|
|
33
34
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
34
35
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
35
36
|
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
|
|
@@ -293,16 +294,65 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
293
294
|
const decisionCapture = evalMode ? undefined : makeDecisionCapture(decisionClientOpts);
|
|
294
295
|
if (!evalMode)
|
|
295
296
|
registerDecisionCommands(pi, decisionClientOpts);
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
297
|
+
// Local MCP servers (standard client, replaces the backend-proxy /mcp of
|
|
298
|
+
// the YAG-446 era): load user/project/local config, approval-gate project
|
|
299
|
+
// servers, connect, register one tool per server tool plus /mcp panel.
|
|
300
|
+
// Fail-soft everywhere; skipped entirely in eval mode (external side
|
|
301
|
+
// effects) and under the YAGNI_CODE_MCP_DISABLED kill-switch.
|
|
302
|
+
const startMcpImpl = deps.startMcp ?? defaultStartMcp;
|
|
303
|
+
// One-shot notices queued by startMcp at session start (the `.mcp.json`
|
|
304
|
+
// approval prompt); flushed once on the first provider response. Connectivity
|
|
305
|
+
// notices are NOT here — they are recomputed live at flush (see below).
|
|
306
|
+
const mcpApprovalNotices = [];
|
|
307
|
+
const mcpOutcome = evalMode
|
|
308
|
+
? { manager: undefined, mutatingToolNames: [] }
|
|
309
|
+
: await startMcpImpl(pi, {
|
|
310
|
+
cwd: process.cwd(),
|
|
311
|
+
env,
|
|
312
|
+
hasUI: true,
|
|
313
|
+
notify: (msg) => mcpApprovalNotices.push(msg),
|
|
314
|
+
});
|
|
315
|
+
const mcpMutatingSet = new Set(mcpOutcome.mutatingToolNames);
|
|
316
|
+
// Wire names carry the original tool name after the second `__`: parse it
|
|
317
|
+
// out and re-run the heuristic for tools registered after startup (a
|
|
318
|
+
// reconnect can re-register tools the original classification never saw).
|
|
319
|
+
const mcpStartupIsMutating = (wireName) => {
|
|
320
|
+
const parts = wireName.split("__");
|
|
321
|
+
const original = parts.slice(2).join("__");
|
|
322
|
+
if (!original)
|
|
323
|
+
return false;
|
|
324
|
+
return looksMutating(original);
|
|
325
|
+
};
|
|
326
|
+
if (!evalMode && mcpOutcome.manager) {
|
|
327
|
+
wireShutdown(pi, mcpOutcome.manager);
|
|
328
|
+
// Startup notify (documented CC deviation): one quiet line per notice on
|
|
329
|
+
// the first provider response — never raw stdout, never blocking.
|
|
330
|
+
//
|
|
331
|
+
// Connectivity notices are recomputed against LIVE manager state at flush
|
|
332
|
+
// time (not the frozen startMcp snapshot): if the user healed a server via
|
|
333
|
+
// /mcp before the first turn, it must no longer read as broken/unauth.
|
|
334
|
+
// The static queue only carries non-connectivity notices (e.g. the
|
|
335
|
+
// `.mcp.json` approval prompt), which stay one-shot.
|
|
336
|
+
let connectivityNoticesFlushed = false;
|
|
337
|
+
pi.on("after_provider_response", (_event, ctx) => {
|
|
338
|
+
try {
|
|
339
|
+
if (!ctx.hasUI)
|
|
340
|
+
return;
|
|
341
|
+
const connectivity = connectivityNoticesFlushed
|
|
342
|
+
? []
|
|
343
|
+
: deriveStartupConnectivityNotices(mcpOutcome.manager);
|
|
344
|
+
connectivityNoticesFlushed = true;
|
|
345
|
+
for (const notice of connectivity)
|
|
346
|
+
ctx.ui.notify(notice, "info");
|
|
347
|
+
while (mcpApprovalNotices.length > 0) {
|
|
348
|
+
ctx.ui.notify(mcpApprovalNotices.shift(), "info");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// A notice must never break a turn.
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
306
356
|
// P3 + W4: interactive permission tiers + plan mode via /mode, now with a
|
|
307
357
|
// session bless store (three-way review-mode select) and a capture hook that
|
|
308
358
|
// drafts a decision on "don't ask again". Default auto, so still additive.
|
|
@@ -336,12 +386,18 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
336
386
|
const guardianLogSink = (payload) => {
|
|
337
387
|
const event = typeof payload.event === "string" ? payload.event : "guardian_event";
|
|
338
388
|
const { event: _ignored, ...fields } = payload;
|
|
389
|
+
// `malformed` (repair failed — still really failing) and a failed
|
|
390
|
+
// telemetry POST are errors. `repaired: true` (the ladder salvaged the
|
|
391
|
+
// verdict) is a warn — it worked, but we keep counting it. Everything
|
|
392
|
+
// else is routine info.
|
|
393
|
+
const level = event === "guardian_event_post_failed" || payload.outcome === "malformed"
|
|
394
|
+
? "error"
|
|
395
|
+
: payload.repaired === true && payload.outcome !== undefined
|
|
396
|
+
? "warn"
|
|
397
|
+
: "info";
|
|
339
398
|
logEvent({
|
|
340
399
|
source: "guardian",
|
|
341
|
-
|
|
342
|
-
// whole capture exists to diagnose) — an error, not routine info. The
|
|
343
|
-
// failed-telemetry event is already an error.
|
|
344
|
-
level: event === "guardian_event_post_failed" || payload.outcome === "malformed" ? "error" : "info",
|
|
400
|
+
level,
|
|
345
401
|
event,
|
|
346
402
|
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
347
403
|
fields,
|
|
@@ -369,15 +425,20 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
369
425
|
guardianReview: (command, deps) => reviewCommand(command, { ...deps, modelTier: guardianTier }),
|
|
370
426
|
onGuardianReview: (ev) => {
|
|
371
427
|
guardianLogSink(ev);
|
|
372
|
-
// A malformed verdict
|
|
373
|
-
//
|
|
374
|
-
//
|
|
428
|
+
// A malformed verdict (repair FAILED — still really failing) is a real
|
|
429
|
+
// failure on our side that must reach Sentry for EVERY workspace
|
|
430
|
+
// (independent of the opt-in storage tier). A REPAIRED verdict (repair
|
|
431
|
+
// salvaged it) posts too, with outcome "malformed_repaired" so the
|
|
432
|
+
// backend can count salvage hits on their own Sentry issue. Both
|
|
433
|
+
// fire-and-forget to the dedicated telemetry endpoint; fail-soft, and
|
|
375
434
|
// suppressed under test/eval so a CI run never phones prod.
|
|
376
|
-
|
|
435
|
+
const isMalformed = ev.outcome === "malformed";
|
|
436
|
+
const isRepaired = ev.outcome !== "malformed" && ev.repaired === true;
|
|
437
|
+
if ((isMalformed || isRepaired) && !evalMode && !runningUnderTest(env)) {
|
|
377
438
|
void (async () => {
|
|
378
439
|
try {
|
|
379
440
|
const body = {
|
|
380
|
-
outcome: ev.outcome,
|
|
441
|
+
outcome: isRepaired ? "malformed_repaired" : ev.outcome,
|
|
381
442
|
...(ev.rawOutput ? { rawOutput: ev.rawOutput } : {}),
|
|
382
443
|
...(ev.durationMs !== undefined ? { durationMs: ev.durationMs } : {}),
|
|
383
444
|
...(ev.tier ? { tier: ev.tier } : {}),
|
|
@@ -461,18 +522,17 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
461
522
|
}
|
|
462
523
|
})();
|
|
463
524
|
},
|
|
464
|
-
...(
|
|
525
|
+
...(mcpMutatingSet.size > 0 || mcpOutcome.manager
|
|
465
526
|
? {
|
|
466
527
|
policy: {
|
|
467
|
-
planBlockTools: [
|
|
468
|
-
|
|
469
|
-
...mcpMutatingTools,
|
|
470
|
-
],
|
|
471
|
-
reviewConfirmTools: [
|
|
472
|
-
...DEFAULT_PERMISSION_POLICY.reviewConfirmTools,
|
|
473
|
-
...mcpMutatingTools,
|
|
474
|
-
],
|
|
528
|
+
planBlockTools: [...DEFAULT_PERMISSION_POLICY.planBlockTools],
|
|
529
|
+
reviewConfirmTools: [...DEFAULT_PERMISSION_POLICY.reviewConfirmTools],
|
|
475
530
|
alwaysConfirmTools: DEFAULT_PERMISSION_POLICY.alwaysConfirmTools,
|
|
531
|
+
// Predicate (not a static spread): matches any registered
|
|
532
|
+
// mcp__* tool that startup classified as mutating, including
|
|
533
|
+
// tools re-registered after a /mcp reconnect.
|
|
534
|
+
isMutating: (toolName) => mcpMutatingSet.has(toolName) ||
|
|
535
|
+
(toolName.startsWith("mcp__") && mcpStartupIsMutating(toolName)),
|
|
476
536
|
},
|
|
477
537
|
}
|
|
478
538
|
: {}),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval gate for project-scope `.mcp.json` servers, mirroring Claude Code's
|
|
3
|
+
* decision shape: a `.mcp.json` in the repo (shared, VCS-controlled) cannot
|
|
4
|
+
* spawn processes until the user has approved it. Choices live in the
|
|
5
|
+
* per-project entry of `~/.yagni-code/mcp.json` under Claude Code's exact key
|
|
6
|
+
* names (`enabledMcpjsonServers` / `disabledMcpjsonServers` /
|
|
7
|
+
* `enableAllProjectMcpServers`), so a decision made in one tool is respected
|
|
8
|
+
* by the other.
|
|
9
|
+
*/
|
|
10
|
+
import { McpConfigError, UserMcpProjectEntry } from "./config.js";
|
|
11
|
+
export type ProjectApprovalDecision = "enabled" | "disabled" | "undecided";
|
|
12
|
+
export interface ProjectApprovalState {
|
|
13
|
+
enabledMcpjsonServers: string[];
|
|
14
|
+
disabledMcpjsonServers: string[];
|
|
15
|
+
enableAllProjectMcpServers: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Read the approval state for one project (abs path). Missing file/entry → fresh state. */
|
|
18
|
+
export declare function readProjectApproval(repoRoot: string): {
|
|
19
|
+
state: ProjectApprovalState;
|
|
20
|
+
errors: McpConfigError[];
|
|
21
|
+
};
|
|
22
|
+
/** Effective decision for one project-scope server. Undecided by default. */
|
|
23
|
+
export declare function decisionFor(state: ProjectApprovalState, serverName: string): ProjectApprovalDecision;
|
|
24
|
+
/** Which configured project servers await a decision (drives the session-start prompt). */
|
|
25
|
+
export declare function undecidedProjectServers(state: ProjectApprovalState, projectServerNames: string[]): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Record a decision. `enableAll: true` mirrors Claude Code's "Yes, approve all
|
|
28
|
+
* project servers in this repo" (sets the blanket flag); a single enable/disable
|
|
29
|
+
* appends to the corresponding list. Idempotent.
|
|
30
|
+
*/
|
|
31
|
+
export declare function recordProjectDecision(repoRoot: string, serverName: string | "all", decision: "enabled" | "disabled"): {
|
|
32
|
+
errors: McpConfigError[];
|
|
33
|
+
};
|
|
34
|
+
/** Clear this project's `.mcp.json` decisions (`yagni mcp reset-project-choices`). */
|
|
35
|
+
export declare function resetProjectChoices(repoRoot: string): {
|
|
36
|
+
errors: McpConfigError[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Mutate `~/.yagni-code/mcp.json`'s per-project entry under a read-modify-write
|
|
40
|
+
* helper shared with CLI commands (add/remove at local scope).
|
|
41
|
+
*/
|
|
42
|
+
export declare function updateProjectEntry(repoRoot: string, mutate: (entry: UserMcpProjectEntry) => void): {
|
|
43
|
+
errors: McpConfigError[];
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=approval.d.ts.map
|