@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.
Files changed (92) hide show
  1. package/README.md +91 -46
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -5
  5. package/dist/claudePlugins.js +129 -21
  6. package/dist/cli.js +16 -4
  7. package/dist/doctor.d.ts +21 -8
  8. package/dist/doctor.js +53 -28
  9. package/dist/extension/askUserQuestionTool.js +7 -2
  10. package/dist/extension/condensedTools.d.ts +12 -1
  11. package/dist/extension/condensedTools.js +17 -9
  12. package/dist/extension/config.d.ts +6 -0
  13. package/dist/extension/hooks.d.ts +3 -3
  14. package/dist/extension/hooks.js +30 -5
  15. package/dist/extension/index.d.ts +19 -0
  16. package/dist/extension/index.js +248 -43
  17. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  18. package/dist/extension/mcp/cliConfig.js +1 -1
  19. package/dist/extension/mcp/config.d.ts +24 -2
  20. package/dist/extension/mcp/config.js +75 -3
  21. package/dist/extension/mcp/manager.d.ts +3 -1
  22. package/dist/extension/mcp/manager.js +2 -2
  23. package/dist/extension/mcp/panel.d.ts +0 -1
  24. package/dist/extension/mcp/panel.js +13 -3
  25. package/dist/extension/mcp/startup.js +8 -6
  26. package/dist/extension/permission/gate.d.ts +101 -2
  27. package/dist/extension/permission/gate.js +485 -42
  28. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  29. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  30. package/dist/extension/permissionRules/engine.d.ts +50 -0
  31. package/dist/extension/permissionRules/engine.js +238 -0
  32. package/dist/extension/permissionRules/loadConfig.d.ts +64 -0
  33. package/dist/extension/permissionRules/loadConfig.js +105 -0
  34. package/dist/extension/permissionRules/parser.d.ts +38 -0
  35. package/dist/extension/permissionRules/parser.js +136 -0
  36. package/dist/extension/permissionRules/pathRules.d.ts +60 -0
  37. package/dist/extension/permissionRules/pathRules.js +122 -0
  38. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  39. package/dist/extension/permissionRules/shellRules.js +221 -0
  40. package/dist/extension/pipeline/invocation.d.ts +3 -6
  41. package/dist/extension/pipeline/invocation.js +3 -6
  42. package/dist/extension/pipeline/runner.d.ts +0 -1
  43. package/dist/extension/pipeline/runner.js +6 -14
  44. package/dist/extension/plugins/inventory.d.ts +88 -0
  45. package/dist/extension/plugins/inventory.js +144 -0
  46. package/dist/extension/plugins/panel.d.ts +45 -0
  47. package/dist/extension/plugins/panel.js +293 -0
  48. package/dist/extension/sandbox/bash.d.ts +99 -0
  49. package/dist/extension/sandbox/bash.js +190 -0
  50. package/dist/extension/sandbox/config.d.ts +115 -0
  51. package/dist/extension/sandbox/config.js +388 -0
  52. package/dist/extension/sandbox/manager.d.ts +108 -0
  53. package/dist/extension/sandbox/manager.js +243 -0
  54. package/dist/extension/sandbox/panel.d.ts +111 -0
  55. package/dist/extension/sandbox/panel.js +342 -0
  56. package/dist/extension/sandbox/session.d.ts +85 -0
  57. package/dist/extension/sandbox/session.js +829 -0
  58. package/dist/extension/settingsFiles.d.ts +50 -0
  59. package/dist/extension/settingsFiles.js +206 -0
  60. package/dist/extension/telemetry/attrs.d.ts +96 -0
  61. package/dist/extension/telemetry/attrs.js +149 -0
  62. package/dist/extension/telemetry/config.d.ts +103 -0
  63. package/dist/extension/telemetry/config.js +193 -0
  64. package/dist/extension/telemetry/index.d.ts +7 -0
  65. package/dist/extension/telemetry/index.js +7 -0
  66. package/dist/extension/telemetry/probe.d.ts +29 -0
  67. package/dist/extension/telemetry/probe.js +122 -0
  68. package/dist/extension/telemetry/register.d.ts +47 -0
  69. package/dist/extension/telemetry/register.js +207 -0
  70. package/dist/extension/telemetry/sdk.d.ts +63 -0
  71. package/dist/extension/telemetry/sdk.js +207 -0
  72. package/dist/extension/telemetry/tracker.d.ts +131 -0
  73. package/dist/extension/telemetry/tracker.js +551 -0
  74. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  75. package/dist/extension/vendor/ignore.d.ts +86 -0
  76. package/dist/extension/vendor/ignore.js +788 -0
  77. package/dist/goHeadless.d.ts +1 -1
  78. package/dist/goHeadless.js +2 -2
  79. package/dist/launch.d.ts +4 -3
  80. package/dist/launch.js +7 -4
  81. package/dist/mcpCommand.d.ts +10 -1
  82. package/dist/mcpCommand.js +42 -10
  83. package/dist/otel.d.ts +67 -90
  84. package/dist/otel.js +152 -195
  85. package/dist/paths.d.ts +13 -0
  86. package/dist/paths.js +18 -0
  87. package/dist/pluginCommand.d.ts +43 -0
  88. package/dist/pluginCommand.js +499 -0
  89. package/dist/pluginStore.d.ts +170 -0
  90. package/dist/pluginStore.js +554 -0
  91. package/dist/upgrade.js +10 -1
  92. package/package.json +19 -3
@@ -10,6 +10,7 @@
10
10
  * a `disabledMcpServers` list under projects[absPath]). A disable/enable
11
11
  * toggle reconnects or disconnects the live client and refreshes tools.
12
12
  */
13
+ import { recordProjectDecision } from "./approval.js";
13
14
  import { readUserMcpConfig, resolveProjectRoot, writeUserMcpConfig } from "./config.js";
14
15
  import { registerServerPrompts } from "./prompts.js";
15
16
  import { registerServerTools } from "./tools.js";
@@ -39,6 +40,7 @@ export function statusText(server) {
39
40
  return STATUS_TEXT[server.status] ?? server.status;
40
41
  }
41
42
  /** Plain-text listing, mirroring CC's `claude mcp list` and panel states. */
43
+ const SCOPE_LABELS = { local: "Local", project: "Project", user: "User", plugin: "Plugin" };
42
44
  export function renderPanelListing(servers, errors) {
43
45
  if (servers.length === 0 && errors.length === 0) {
44
46
  return [
@@ -47,14 +49,14 @@ export function renderPanelListing(servers, errors) {
47
49
  ].join("\n");
48
50
  }
49
51
  const lines = [];
50
- const byScope = { user: [], project: [], local: [] };
52
+ const byScope = { user: [], project: [], local: [], plugin: [] };
51
53
  for (const s of servers)
52
54
  (byScope[s.scope] ??= []).push(s);
53
- for (const scope of ["local", "project", "user"]) {
55
+ for (const scope of ["local", "project", "user", "plugin"]) {
54
56
  const group = byScope[scope];
55
57
  if (!group?.length)
56
58
  continue;
57
- lines.push(`${scope === "local" ? "Local" : scope === "project" ? "Project" : "User"} servers:`);
59
+ lines.push(`${SCOPE_LABELS[scope]} servers:`);
58
60
  for (const s of group) {
59
61
  lines.push(` ${s.name} · ${statusIcon(s.status)} ${statusText(s)}${s.error ? ` — ${s.error}` : ""}`);
60
62
  }
@@ -163,13 +165,21 @@ export function registerMcpPanel(pi, manager, configErrors) {
163
165
  ui.notify(`No MCP server named "${name}".`, "error");
164
166
  continue;
165
167
  }
168
+ // Repo-sourced servers (.mcp.json, or a repo plugin's bundle) carry an
169
+ // approval decision that must outlive this session — record it, so the
170
+ // startup gate stops re-asking.
171
+ const approvalGated = server.scope === "project" || server.gated === true;
166
172
  if (action === "disable") {
167
173
  await manager.disconnect(name);
168
174
  persistDisabled(name, true);
175
+ if (approvalGated)
176
+ recordProjectDecision(repoRoot(), name, "disabled");
169
177
  ui.notify(`○ ${name} disabled`, "info");
170
178
  }
171
179
  else {
172
180
  persistDisabled(name, false);
181
+ if (approvalGated)
182
+ recordProjectDecision(repoRoot(), name, "enabled");
173
183
  const result = await manager.connect(name);
174
184
  if (result?.status === "connected") {
175
185
  await refreshForServer(name);
@@ -53,32 +53,34 @@ export async function startMcp(pi, opts) {
53
53
  registerMcpPanel(pi, outcome.manager, () => outcome.configErrors);
54
54
  if (servers.length === 0 && errors.length === 0)
55
55
  return outcome;
56
- // Approval gate for project-scope servers (fail-closed: undecided = skip).
56
+ // Approval gate for project-scope servers and repo-sourced plugin servers
57
+ // (fail-closed: undecided = skip).
57
58
  const { state } = readProjectApproval(repoRoot);
58
- const projectNames = servers.filter((s) => s.scope === "project").map((s) => s.name);
59
+ const needsApproval = (s) => s.scope === "project" || s.gated === true;
60
+ const projectNames = servers.filter(needsApproval).map((s) => s.name);
59
61
  const undecided = undecidedProjectServers(state, projectNames);
60
62
  outcome.skippedApproval = undecided;
61
63
  const gate = (server) => {
62
- if (server.scope !== "project")
64
+ if (!needsApproval(server))
63
65
  return true;
64
66
  return !undecided.includes(server.name);
65
67
  };
66
68
  // Ask once, interactively, about undecided project servers (CC parity).
67
69
  if (undecided.length > 0 && opts.hasUI && opts.notify) {
68
- opts.notify(`.mcp.json in this repo defines MCP server(s) ${undecided.join(", ")} — not yet approved. ` +
70
+ opts.notify(`This repo's config (.mcp.json or a repo plugin) defines MCP server(s) ${undecided.join(", ")} — not yet approved. ` +
69
71
  `Run /mcp to approve them individually, or \`yagni mcp reset-project-choices\` to clear old choices.`);
70
72
  }
71
73
  const connectable = [];
72
74
  for (const server of servers) {
73
75
  if (!gate(server)) {
74
- outcome.manager.register(server.name, server.scope, server.config, "disabled");
76
+ outcome.manager.register(server.name, server.scope, server.config, "disabled", server.gated === true);
75
77
  continue;
76
78
  }
77
79
  connectable.push(server);
78
80
  }
79
81
  // Connect in parallel; each server fails soft.
80
82
  const results = await Promise.all(connectable.map(async (server) => {
81
- const managed = outcome.manager.register(server.name, server.scope, server.config, "connecting");
83
+ const managed = outcome.manager.register(server.name, server.scope, server.config, "connecting", server.gated === true);
82
84
  const connected = await outcome.manager.connect(server.name);
83
85
  if (connected?.status === "connected") {
84
86
  const tools = await registerServerTools(pi, outcome.manager, server.name, env);
@@ -26,7 +26,7 @@
26
26
  * When the mode leaves plan, stale plan-context messages are filtered out of
27
27
  * the context so the model doesn't keep believing it is restricted.
28
28
  */
29
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
29
+ import type { ExtensionAPI, ExtensionContext, ToolCallEvent, ToolCallEventResult } from "@earendil-works/pi-coding-agent";
30
30
  import { type ApprovedPrefixGrant } from "./approvedPrefixes.js";
31
31
  import type { HookRunner } from "../hooks.js";
32
32
  import { type BlessStore } from "../bless.js";
@@ -94,6 +94,33 @@ export interface GateDecision {
94
94
  */
95
95
  classifyJustification?: string;
96
96
  }
97
+ /**
98
+ * What the hard floors say about an ALLOW-rule verdict — the three invariants
99
+ * that outrank any user/project permission rule. Extracted from the inline
100
+ * allow path so the gate reads linearly and the floors are directly testable.
101
+ * PURE — no I/O, no session state:
102
+ *
103
+ * "allow" no floor applies — the rule's allow short-circuits everything
104
+ * "block" the exec-policy forbidden band holds despite the allow rule
105
+ * "confirm" alwaysConfirmTools keeps its fresh-consent contract
106
+ * "hold" plan mode's no-mutation contract outranks the allow rule —
107
+ * fall through to the normal plan-mode gate
108
+ *
109
+ * Order is deliberate: plan-mode first (its outcome routes to the EXISTING
110
+ * plan-mode machinery, not a bespoke block); then the terminal forbidden-band
111
+ * block; then the confirm deferral.
112
+ */
113
+ export type AllowRuleFloorOutcome = {
114
+ kind: "allow";
115
+ } | {
116
+ kind: "block";
117
+ reason: string;
118
+ } | {
119
+ kind: "confirm";
120
+ } | {
121
+ kind: "hold";
122
+ };
123
+ export declare function allowRuleFloorVerdict(toolName: string, params: Record<string, unknown>, mode: PermissionMode, policy: PermissionPolicy): AllowRuleFloorOutcome;
97
124
  /**
98
125
  * Pure permission decision for one tool call under a mode + policy. Auto allows
99
126
  * ordinary tools; plan blocks the write/exec set; review marks writes for confirmation
@@ -105,7 +132,7 @@ export declare function decideGate(toolName: string, params: Record<string, unkn
105
132
  * is emitted per terminal outcome; `consulted` says whether a Guardian LLM
106
133
  * call actually happened (grants/cache hits skip it).
107
134
  */
108
- export type GuardianGateOutcome = "prefix_allow" | "cached_allow" | "allow" | "deny" | "ask_approved" | "ask_approved_remembered" | "ask_denied" | "ask_headless_blocked" | "breaker_ask_approved" | "breaker_blocked" | GuardianError;
135
+ export type GuardianGateOutcome = "prefix_allow" | "cached_allow" | "sandbox_auto_allow" | "allow" | "deny" | "ask_approved" | "ask_approved_remembered" | "ask_denied" | "ask_headless_blocked" | "breaker_ask_approved" | "breaker_blocked" | GuardianError;
109
136
  /**
110
137
  * Rich per-decision event for opt-in storage (YAG-510). Carries the RAW
111
138
  * command — the wiring layer (index.ts) hashes/redacts per the workspace's
@@ -129,6 +156,17 @@ export interface GuardianGateEvent {
129
156
  * error-fallback ask that the user then approved). */
130
157
  guardianError?: GuardianError;
131
158
  }
159
+ /** Claude Code's decision sources for a tool permission outcome. */
160
+ export type ToolDecisionSource = "config" | "hook" | "user_permanent" | "user_temporary" | "user_abort" | "user_reject";
161
+ /** One terminal tool_call outcome, in Claude Code's telemetry shape. */
162
+ export interface ToolDecisionEvent {
163
+ toolName: string;
164
+ toolCallId?: string;
165
+ decision: "accept" | "reject";
166
+ source: ToolDecisionSource;
167
+ /** For edit/write tools: the target path (language attribution). */
168
+ filePath?: string;
169
+ }
132
170
  /** What was blessed with "don't ask again", handed to the capture hook. */
133
171
  export interface BlessRememberInfo {
134
172
  tool: string;
@@ -187,12 +225,59 @@ export interface RegisterPermissionDeps {
187
225
  /** Persist a new grant (fire-and-forget; the in-memory list is updated
188
226
  * either way). index.ts wires approvedPrefixes.appendGrant. */
189
227
  persistGrant?: (grant: ApprovedPrefixGrant) => void;
228
+ /**
229
+ * persist a user-level permission rule string (e.g.
230
+ * `Bash(git push:*)`) into ~/.yagni-code/config.json permissions.allow.
231
+ * Wired by index.ts; offered as a third option on Guardian ask dialogs.
232
+ * Fail-soft: the in-session approval applies even if the write fails.
233
+ */
234
+ /** Persist an allow rule for the caller; the cwd is the SESSION cwd of
235
+ * the gate call (may differ from process.cwd() when the session switches
236
+ * directories) — the rule must land in the project the user is working
237
+ * in, the same cwd the gate uses for grants and approvals. */
238
+ persistUserRule?: (ruleString: string, cwd: string) => void;
190
239
  /**
191
240
  * Called (fire-and-forget) at every terminal prompt-band outcome with the
192
241
  * rich storage event (raw command — the wiring layer redacts/hashes).
193
242
  * Fail-soft; never blocks.
194
243
  */
195
244
  onGuardianEvent?: (event: GuardianGateEvent) => void;
245
+ /**
246
+ * Called (fire-and-forget) at EVERY terminal tool_call outcome, for every
247
+ * tool, with the Claude Code-shaped decision (accept/reject + source).
248
+ * Feeds the telemetry `tool_decision` event and the
249
+ * `code_edit_tool.decision` counter. Fail-soft; never blocks.
250
+ */
251
+ onToolDecision?: (event: ToolDecisionEvent) => void;
252
+ /**
253
+ * settings-based permission rules (user + project config.json).
254
+ * When present, evaluated at the TOP of the tool_call handler, before
255
+ * hooks: deny → ask → allow, first match wins. Deny/ask are final; allow
256
+ * short-circuits the Guardian but can never lift the exec-policy forbidden
257
+ * band or the alwaysConfirmTools contract (both re-checked below).
258
+ */
259
+ permissionRules?: readonly import("../permissionRules/loadConfig.js").PermissionRule[];
260
+ /** ~/.yagni-code — the user `/`-anchor base for path rules. */
261
+ rulesUserStateHome?: string;
262
+ /** Project root for project-source `/`-anchored path rules; null outside a repo. */
263
+ rulesProjectRoot?: string | null;
264
+ /** Overrides ~ expansion for path rules (tests). */
265
+ rulesHomeDir?: string;
266
+ /** Called (fire-and-soft) after every rule verdict for debug logging. */
267
+ onRuleVerdict?: (event: import("../permissionRules/engine.js").RuleEvaluation & {
268
+ toolName: string;
269
+ cwd: string;
270
+ }) => void;
271
+ /**
272
+ * Sandbox auto-allow (YAGNI Code sandbox): when present and true for this bash call,
273
+ * a prompt-band command runs sandboxed WITHOUT a Guardian consult or user
274
+ * prompt (Claude's autoAllowBashIfSandboxed). Consulted AFTER deny/ask
275
+ * rules, the exec-policy forbidden band, grants, and the session cache —
276
+ * never in plan mode (plan's no-mutation contract outranks it). The
277
+ * caller (session.ts) owns the decision: sandbox initialized + this
278
+ * command will be wrapped + autoAllow enabled.
279
+ */
280
+ sandboxAutoAllow?: (toolName: string, params: Record<string, unknown>) => boolean;
196
281
  /**
197
282
  * User-configurable lifecycle hooks (YAG-506). When present, PreToolUse
198
283
  * hooks run before decideGate and can short-circuit (allow/deny/ask),
@@ -217,9 +302,23 @@ export declare function buildModeContextMessage(mode: PermissionMode): string;
217
302
  export declare function filterStaleModeContext<T>(messages: T[], currentMode?: PermissionMode): T[];
218
303
  /** Legacy alias — the original plan-mode filter name. */
219
304
  export declare const filterStalePlanContext: typeof filterStaleModeContext;
305
+ /** The decision source of ONE tool_call (telemetry), carried per invocation
306
+ * because tool calls run concurrently: refined by the user dialog, hooks,
307
+ * and Guardian outcomes as the call moves through the gate. */
308
+ interface DecisionSlot {
309
+ source: ToolDecisionSource;
310
+ }
311
+ /**
312
+ * The pi tool_call handler wiring (crash-honest attribution). Extracted so
313
+ * the telemetry contract is testable against the production path: a thrown
314
+ * gate records reject — never accept — logs a gate_crashed line (metadata
315
+ * only), and rethrows so pi's handling of a crashed handler is unchanged.
316
+ */
317
+ export declare function wireToolCallGate(pi: Pick<ExtensionAPI, "on">, run: (event: ToolCallEvent, ctx: ExtensionContext, slot: DecisionSlot) => Promise<ToolCallEventResult | undefined>, onToolDecision?: (ev: ToolDecisionEvent) => void): void;
220
318
  /**
221
319
  * Wire the tool_call gate + the /mode command onto a shared mode holder. Default
222
320
  * auto, so absent any /mode this is a no-op over today's behavior.
223
321
  */
224
322
  export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): void;
323
+ export {};
225
324
  //# sourceMappingURL=gate.d.ts.map