@yagni-app/code 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +65 -11
  2. package/dist/claudeCompat.d.ts +109 -0
  3. package/dist/claudeCompat.js +260 -0
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.d.ts +1 -5
  7. package/dist/cli.js +79 -20
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/crashReport.d.ts +135 -0
  11. package/dist/crashReport.js +291 -0
  12. package/dist/credentials.js +2 -1
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +27 -0
  16. package/dist/doctor.js +81 -3
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +194 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +87 -0
  25. package/dist/extension/boostCommand.d.ts +144 -0
  26. package/dist/extension/boostCommand.js +263 -0
  27. package/dist/extension/branding.d.ts +79 -0
  28. package/dist/extension/branding.js +156 -0
  29. package/dist/extension/chipEditor.d.ts +117 -0
  30. package/dist/extension/chipEditor.js +373 -0
  31. package/dist/extension/claudeRules.d.ts +54 -0
  32. package/dist/extension/claudeRules.js +180 -0
  33. package/dist/extension/config.d.ts +173 -0
  34. package/dist/extension/config.js +194 -0
  35. package/dist/extension/costHud.d.ts +186 -0
  36. package/dist/extension/costHud.js +293 -0
  37. package/dist/extension/crashReport.d.ts +89 -0
  38. package/dist/extension/crashReport.js +241 -0
  39. package/dist/extension/decisionCapture.d.ts +52 -0
  40. package/dist/extension/decisionCapture.js +66 -0
  41. package/dist/extension/decisions.d.ts +83 -0
  42. package/dist/extension/decisions.js +200 -0
  43. package/dist/extension/diagnostics.d.ts +41 -0
  44. package/dist/extension/diagnostics.js +110 -0
  45. package/dist/extension/index.d.ts +155 -0
  46. package/dist/extension/index.js +597 -0
  47. package/dist/extension/initDone.d.ts +28 -0
  48. package/dist/extension/initDone.js +66 -0
  49. package/dist/extension/initPass.d.ts +170 -0
  50. package/dist/extension/initPass.js +394 -0
  51. package/dist/extension/mcpTools.d.ts +57 -0
  52. package/dist/extension/mcpTools.js +132 -0
  53. package/dist/extension/nextWorkTool.d.ts +51 -0
  54. package/dist/extension/nextWorkTool.js +80 -0
  55. package/dist/extension/permission.d.ts +91 -0
  56. package/dist/extension/permission.js +236 -0
  57. package/dist/extension/pipeline/activity.d.ts +37 -0
  58. package/dist/extension/pipeline/activity.js +151 -0
  59. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  60. package/dist/extension/pipeline/activityFeed.js +175 -0
  61. package/dist/extension/pipeline/budget.d.ts +48 -0
  62. package/dist/extension/pipeline/budget.js +68 -0
  63. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  64. package/dist/extension/pipeline/checkpoint.js +176 -0
  65. package/dist/extension/pipeline/eval.d.ts +205 -0
  66. package/dist/extension/pipeline/eval.js +226 -0
  67. package/dist/extension/pipeline/events.d.ts +56 -0
  68. package/dist/extension/pipeline/events.js +147 -0
  69. package/dist/extension/pipeline/findings.d.ts +42 -0
  70. package/dist/extension/pipeline/findings.js +144 -0
  71. package/dist/extension/pipeline/finish.d.ts +128 -0
  72. package/dist/extension/pipeline/finish.js +307 -0
  73. package/dist/extension/pipeline/goCommand.d.ts +146 -0
  74. package/dist/extension/pipeline/goCommand.js +1085 -0
  75. package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
  76. package/dist/extension/pipeline/goCompareCommand.js +203 -0
  77. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  78. package/dist/extension/pipeline/goFlags.js +46 -0
  79. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  80. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  81. package/dist/extension/pipeline/invocation.d.ts +45 -0
  82. package/dist/extension/pipeline/invocation.js +64 -0
  83. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  84. package/dist/extension/pipeline/orchestrator.js +645 -0
  85. package/dist/extension/pipeline/personas.d.ts +44 -0
  86. package/dist/extension/pipeline/personas.js +248 -0
  87. package/dist/extension/pipeline/resilience.d.ts +85 -0
  88. package/dist/extension/pipeline/resilience.js +166 -0
  89. package/dist/extension/pipeline/resume.d.ts +18 -0
  90. package/dist/extension/pipeline/resume.js +106 -0
  91. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  92. package/dist/extension/pipeline/runCostTable.js +165 -0
  93. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  94. package/dist/extension/pipeline/runRegistry.js +202 -0
  95. package/dist/extension/pipeline/runSession.d.ts +152 -0
  96. package/dist/extension/pipeline/runSession.js +167 -0
  97. package/dist/extension/pipeline/runState.d.ts +177 -0
  98. package/dist/extension/pipeline/runState.js +275 -0
  99. package/dist/extension/pipeline/runner.d.ts +98 -0
  100. package/dist/extension/pipeline/runner.js +310 -0
  101. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  102. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  103. package/dist/extension/pipeline/stages.d.ts +53 -0
  104. package/dist/extension/pipeline/stages.js +115 -0
  105. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  106. package/dist/extension/pipeline/ticketResolution.js +75 -0
  107. package/dist/extension/pipeline/types.d.ts +439 -0
  108. package/dist/extension/pipeline/types.js +48 -0
  109. package/dist/extension/pipeline/verify.d.ts +277 -0
  110. package/dist/extension/pipeline/verify.js +763 -0
  111. package/dist/extension/pipeline/workspace.d.ts +32 -0
  112. package/dist/extension/pipeline/workspace.js +73 -0
  113. package/dist/extension/pipeline/worktree.d.ts +107 -0
  114. package/dist/extension/pipeline/worktree.js +202 -0
  115. package/dist/extension/provider.d.ts +17 -0
  116. package/dist/extension/provider.js +33 -0
  117. package/dist/extension/recall.d.ts +93 -0
  118. package/dist/extension/recall.js +190 -0
  119. package/dist/extension/recordContextTool.d.ts +38 -0
  120. package/dist/extension/recordContextTool.js +85 -0
  121. package/dist/extension/recordDecisionTool.d.ts +52 -0
  122. package/dist/extension/recordDecisionTool.js +102 -0
  123. package/dist/extension/repoDocs.d.ts +81 -0
  124. package/dist/extension/repoDocs.js +260 -0
  125. package/dist/extension/rerouteNotice.d.ts +42 -0
  126. package/dist/extension/rerouteNotice.js +67 -0
  127. package/dist/extension/resilientFetch.d.ts +60 -0
  128. package/dist/extension/resilientFetch.js +133 -0
  129. package/dist/extension/reviewTool.d.ts +34 -0
  130. package/dist/extension/reviewTool.js +81 -0
  131. package/dist/extension/sessionRuns.d.ts +45 -0
  132. package/dist/extension/sessionRuns.js +77 -0
  133. package/dist/extension/spool.d.ts +92 -0
  134. package/dist/extension/spool.js +266 -0
  135. package/dist/extension/stateHome.d.ts +2 -0
  136. package/dist/extension/stateHome.js +6 -0
  137. package/dist/extension/subagents.d.ts +145 -0
  138. package/dist/extension/subagents.js +326 -0
  139. package/dist/extension/surface.d.ts +10 -0
  140. package/dist/extension/surface.js +12 -0
  141. package/dist/extension/todos.d.ts +110 -0
  142. package/dist/extension/todos.js +217 -0
  143. package/dist/extension/tokenProvider.d.ts +93 -0
  144. package/dist/extension/tokenProvider.js +234 -0
  145. package/dist/launch.d.ts +25 -3
  146. package/dist/launch.js +27 -9
  147. package/dist/login.d.ts +7 -0
  148. package/dist/login.js +3 -1
  149. package/dist/paths.d.ts +13 -4
  150. package/dist/paths.js +17 -5
  151. package/dist/profiles.d.ts +1 -1
  152. package/dist/profiles.js +5 -2
  153. package/dist/upgrade.d.ts +97 -0
  154. package/dist/upgrade.js +284 -0
  155. package/package.json +11 -15
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Claude Code plugin + marketplace discovery (local content only).
3
+ *
4
+ * Finds the plugin content a Claude Code user already has on disk and exposes
5
+ * it as asset paths the launcher can feed through the existing compat seams:
6
+ * skills → `--skill`, commands → `--prompt-template`, agents → the subagent
7
+ * discovery env var. Two sources:
8
+ *
9
+ * 1. Installed plugins: `~/.claude/plugins/installed_plugins.json` (version-2
10
+ * ledger of `name@marketplace` → cache checkouts), enabled through
11
+ * `enabledPlugins` in the user's `~/.claude/settings.json` (user scope) or
12
+ * the repo's `.claude/settings{,.local}.json` (project scope). When a
13
+ * ledger entry is gone, the marketplace checkout recorded in
14
+ * `known_marketplaces.json` is tried as a fallback.
15
+ * 2. The repo's own `.claude-plugin/marketplace.json` (the internal-
16
+ * marketplace pattern): every plugin it defines with a locally-present
17
+ * relative-path source loads on the project side, unless explicitly
18
+ * disabled via `enabledPlugins`.
19
+ *
20
+ * Deliberately NOT here: network installation of any kind, plugin hooks, MCP
21
+ * servers, LSP servers, themes, output styles. Discovery is read-only.
22
+ *
23
+ * Everything is fail-soft: malformed JSON, missing dirs, or hostile path
24
+ * entries degrade to "that plugin absent" — never a failed launch. Path
25
+ * entries from marketplace/plugin manifests are containment-checked (realpath
26
+ * inside the expected root) so a malicious `source` or component override
27
+ * cannot reach outside its repo/plugin.
28
+ */
29
+ import { readFileSync, realpathSync, statSync } from "node:fs";
30
+ import { isAbsolute, join, resolve, sep } from "node:path";
31
+ // ── fail-soft fs/json primitives ────────────────────────────────────────────
32
+ function isDirectory(path) {
33
+ try {
34
+ return statSync(path).isDirectory();
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ function isFile(path) {
41
+ try {
42
+ return statSync(path).isFile();
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ function readJsonObject(path) {
49
+ try {
50
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
51
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
52
+ return null;
53
+ return parsed;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ function realOrNull(path) {
60
+ try {
61
+ return realpathSync(path);
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /**
68
+ * Resolve `candidate` (a relative path from untrusted JSON) against `root` and
69
+ * return its realpath only when it exists AND stays inside `root` after
70
+ * symlink resolution. Absolute candidates and any `../` escape are rejected.
71
+ */
72
+ export function containedExistingPath(root, candidate) {
73
+ if (typeof candidate !== "string" || candidate.length === 0)
74
+ return null;
75
+ if (isAbsolute(candidate))
76
+ return null;
77
+ const realRoot = realOrNull(root);
78
+ if (!realRoot)
79
+ return null;
80
+ const real = realOrNull(resolve(root, candidate));
81
+ if (!real)
82
+ return null;
83
+ if (real !== realRoot && !real.startsWith(realRoot + sep))
84
+ return null;
85
+ return real;
86
+ }
87
+ /** `~/.claude/plugins/installed_plugins.json` → `name@marketplace` → entries. */
88
+ export function readInstalledPlugins(homeDir) {
89
+ const out = new Map();
90
+ const parsed = readJsonObject(join(homeDir, ".claude", "plugins", "installed_plugins.json"));
91
+ const plugins = parsed?.plugins;
92
+ if (!plugins || typeof plugins !== "object" || Array.isArray(plugins))
93
+ return out;
94
+ for (const [key, value] of Object.entries(plugins)) {
95
+ if (!Array.isArray(value))
96
+ continue;
97
+ const entries = [];
98
+ for (const item of value) {
99
+ if (!item || typeof item !== "object")
100
+ continue;
101
+ const installPath = item.installPath;
102
+ if (typeof installPath !== "string" || installPath.length === 0)
103
+ continue;
104
+ const projectPath = item.projectPath;
105
+ entries.push({
106
+ installPath,
107
+ ...(typeof projectPath === "string" ? { projectPath } : {}),
108
+ });
109
+ }
110
+ if (entries.length > 0)
111
+ out.set(key, entries);
112
+ }
113
+ return out;
114
+ }
115
+ function collectEnabled(path, scope, into) {
116
+ const parsed = readJsonObject(path);
117
+ const enabled = parsed?.enabledPlugins;
118
+ if (!enabled || typeof enabled !== "object" || Array.isArray(enabled))
119
+ return;
120
+ for (const [key, value] of Object.entries(enabled)) {
121
+ if (value === true || value === false)
122
+ into.set(key, { enabled: value, scope });
123
+ }
124
+ }
125
+ /**
126
+ * `enabledPlugins` merged across scopes, later (more specific) winning:
127
+ * user `~/.claude/settings.json` < project `.claude/settings.json` <
128
+ * project `.claude/settings.local.json`.
129
+ */
130
+ export function readEnabledPlugins(cwd, homeDir) {
131
+ const out = new Map();
132
+ collectEnabled(join(homeDir, ".claude", "settings.json"), "user", out);
133
+ collectEnabled(join(cwd, ".claude", "settings.json"), "project", out);
134
+ collectEnabled(join(cwd, ".claude", "settings.local.json"), "project", out);
135
+ return out;
136
+ }
137
+ /** `~/.claude/plugins/known_marketplaces.json` → marketplace name → checkout dir. */
138
+ export function readKnownMarketplaces(homeDir) {
139
+ const out = new Map();
140
+ const parsed = readJsonObject(join(homeDir, ".claude", "plugins", "known_marketplaces.json"));
141
+ if (!parsed)
142
+ return out;
143
+ for (const [name, value] of Object.entries(parsed)) {
144
+ if (!value || typeof value !== "object")
145
+ continue;
146
+ const loc = value.installLocation;
147
+ if (typeof loc === "string" && loc.length > 0)
148
+ out.set(name, loc);
149
+ }
150
+ return out;
151
+ }
152
+ /** Parse `<root>/.claude-plugin/marketplace.json`; null when absent/unusable. */
153
+ export function readMarketplace(root) {
154
+ const parsed = readJsonObject(join(root, ".claude-plugin", "marketplace.json"));
155
+ if (!parsed || typeof parsed.name !== "string" || parsed.name.length === 0)
156
+ return null;
157
+ if (!Array.isArray(parsed.plugins))
158
+ return null;
159
+ const metadata = parsed.metadata && typeof parsed.metadata === "object" && !Array.isArray(parsed.metadata)
160
+ ? parsed.metadata
161
+ : {};
162
+ const pluginRoot = typeof metadata.pluginRoot === "string" ? metadata.pluginRoot : null;
163
+ const plugins = [];
164
+ for (const item of parsed.plugins) {
165
+ if (!item || typeof item !== "object" || Array.isArray(item))
166
+ continue;
167
+ const name = item.name;
168
+ if (typeof name !== "string" || name.length === 0)
169
+ continue;
170
+ plugins.push({ name, source: item.source });
171
+ }
172
+ return { name: parsed.name, root, pluginRoot, plugins };
173
+ }
174
+ /**
175
+ * The local directory a marketplace entry's source points at, when it is a
176
+ * relative-path source that exists inside the marketplace root. Object
177
+ * sources (github/url/npm/...) are network installs → null here.
178
+ */
179
+ export function resolveLocalPluginRoot(mp, entry) {
180
+ const src = entry.source;
181
+ if (typeof src !== "string" || src.length === 0)
182
+ return null;
183
+ if (isAbsolute(src))
184
+ return null;
185
+ // Docs: `metadata.pluginRoot` lets a bare "formatter" mean "./plugins/formatter".
186
+ // Try the pluginRoot-prefixed form first, then the source as written.
187
+ const candidates = [];
188
+ if (mp.pluginRoot)
189
+ candidates.push(`${mp.pluginRoot}/${src}`);
190
+ candidates.push(src);
191
+ for (const candidate of candidates) {
192
+ const contained = containedExistingPath(mp.root, candidate);
193
+ if (contained && isDirectory(contained))
194
+ return contained;
195
+ }
196
+ return null;
197
+ }
198
+ // ── plugin component assets ─────────────────────────────────────────────────
199
+ function asStringArray(value) {
200
+ if (typeof value === "string")
201
+ return [value];
202
+ if (Array.isArray(value))
203
+ return value.filter((v) => typeof v === "string");
204
+ return [];
205
+ }
206
+ /**
207
+ * The bridgeable assets inside one plugin checkout, per Claude Code's
208
+ * component rules: `skills/` (manifest `skills` ADDS dirs), `commands/`
209
+ * (manifest REPLACES), `agents/` (manifest REPLACES; dirs only — pi's
210
+ * discovery reads whole dirs), root `SKILL.md` fallback. Null when the plugin
211
+ * has nothing we can bridge.
212
+ */
213
+ export function pluginAssets(root, name) {
214
+ if (!isDirectory(root))
215
+ return null;
216
+ const manifest = readJsonObject(join(root, ".claude-plugin", "plugin.json")) ?? {};
217
+ const skillPaths = [];
218
+ const defaultSkills = join(root, "skills");
219
+ if (isDirectory(defaultSkills))
220
+ skillPaths.push(defaultSkills);
221
+ const manifestSkills = asStringArray(manifest.skills);
222
+ for (const rel of manifestSkills) {
223
+ const contained = containedExistingPath(root, rel);
224
+ if (contained)
225
+ skillPaths.push(contained);
226
+ }
227
+ if (skillPaths.length === 0 && manifestSkills.length === 0) {
228
+ const rootSkill = join(root, "SKILL.md");
229
+ if (isFile(rootSkill))
230
+ skillPaths.push(rootSkill);
231
+ }
232
+ const componentPaths = (manifestValue, defaultDir) => {
233
+ const entries = asStringArray(manifestValue);
234
+ if (entries.length > 0) {
235
+ const out = [];
236
+ for (const rel of entries) {
237
+ const contained = containedExistingPath(root, rel);
238
+ if (contained)
239
+ out.push(contained);
240
+ }
241
+ return out;
242
+ }
243
+ const def = join(root, defaultDir);
244
+ return isDirectory(def) ? [def] : [];
245
+ };
246
+ const commandPaths = componentPaths(manifest.commands, "commands");
247
+ const agentDirs = componentPaths(manifest.agents, "agents").filter(isDirectory);
248
+ if (skillPaths.length === 0 && commandPaths.length === 0 && agentDirs.length === 0)
249
+ return null;
250
+ return { name, root, skillPaths, commandPaths, agentDirs };
251
+ }
252
+ function resolveInstalledRoot(key, installed, marketplaces, realCwd) {
253
+ const entries = (installed.get(key) ?? []).filter((e) => isDirectory(e.installPath));
254
+ const projectMatch = entries.find((e) => {
255
+ if (typeof e.projectPath !== "string")
256
+ return false;
257
+ const real = realOrNull(e.projectPath);
258
+ if (!real)
259
+ return false;
260
+ return realCwd === real || realCwd.startsWith(real + sep);
261
+ });
262
+ if (projectMatch)
263
+ return projectMatch.installPath;
264
+ if (entries[0])
265
+ return entries[0].installPath;
266
+ // Cache entry gone: try the marketplace checkout's own copy of the plugin.
267
+ const at = key.lastIndexOf("@");
268
+ if (at <= 0)
269
+ return null;
270
+ const pluginName = key.slice(0, at);
271
+ const marketplaceName = key.slice(at + 1);
272
+ const location = marketplaces.get(marketplaceName);
273
+ if (!location)
274
+ return null;
275
+ const mp = readMarketplace(location);
276
+ if (!mp)
277
+ return null;
278
+ const entry = mp.plugins.find((p) => p.name === pluginName);
279
+ if (!entry)
280
+ return null;
281
+ return resolveLocalPluginRoot(mp, entry);
282
+ }
283
+ /**
284
+ * All locally-present Claude Code plugin content relevant to `cwd`, split by
285
+ * the trust posture the launcher must apply. Never throws.
286
+ */
287
+ export function discoverClaudePlugins(deps) {
288
+ try {
289
+ const installed = readInstalledPlugins(deps.homeDir);
290
+ const enabled = readEnabledPlugins(deps.cwd, deps.homeDir);
291
+ const marketplaces = readKnownMarketplaces(deps.homeDir);
292
+ const realCwd = realOrNull(deps.cwd) ?? resolve(deps.cwd);
293
+ const user = [];
294
+ const project = [];
295
+ const seen = new Set();
296
+ for (const [key, state] of enabled) {
297
+ if (!state.enabled)
298
+ continue;
299
+ const root = resolveInstalledRoot(key, installed, marketplaces, realCwd);
300
+ if (!root)
301
+ continue;
302
+ const at = key.lastIndexOf("@");
303
+ const assets = pluginAssets(root, at > 0 ? key.slice(0, at) : key);
304
+ if (!assets)
305
+ continue;
306
+ seen.add(key);
307
+ (state.scope === "user" ? user : project).push(assets);
308
+ }
309
+ // The internal-marketplace pattern: working inside the marketplace repo
310
+ // itself, its locally-present plugins load (trust-gated) with zero config.
311
+ const repoMarketplace = readMarketplace(deps.cwd);
312
+ if (repoMarketplace) {
313
+ for (const entry of repoMarketplace.plugins) {
314
+ const key = `${entry.name}@${repoMarketplace.name}`;
315
+ if (seen.has(key))
316
+ continue;
317
+ if (enabled.get(key)?.enabled === false)
318
+ continue;
319
+ const root = resolveLocalPluginRoot(repoMarketplace, entry) ??
320
+ resolveInstalledRoot(key, installed, marketplaces, realCwd);
321
+ if (!root)
322
+ continue;
323
+ const assets = pluginAssets(root, entry.name);
324
+ if (!assets)
325
+ continue;
326
+ seen.add(key);
327
+ project.push(assets);
328
+ }
329
+ }
330
+ return { user, project };
331
+ }
332
+ catch {
333
+ return { user: [], project: [] };
334
+ }
335
+ }
336
+ //# sourceMappingURL=claudePlugins.js.map
package/dist/cli.d.ts CHANGED
@@ -37,11 +37,7 @@ export declare function wantsHelp(argv: string[]): boolean;
37
37
  * leaks the agent's branded version line (or a login error before it).
38
38
  */
39
39
  export declare function wantsVersion(argv: string[]): boolean;
40
- /**
41
- * The CLI's own version, read from this package's package.json. Resolved
42
- * relative to the module (src/ and dist/ both sit one level below the package
43
- * root), so it is correct under tsx in tests and in the published tarball.
44
- */
40
+ /** The CLI's own version (see `currentCliVersion` — upgrade.ts owns it). */
45
41
  export declare function cliVersion(): string;
46
42
  export declare function main(argv: string[]): Promise<number>;
47
43
  /**
package/dist/cli.js CHANGED
@@ -13,22 +13,40 @@
13
13
  * pi-extension-yagni and the YAGNI backend.
14
14
  */
15
15
  import { spawn } from "node:child_process";
16
- import { mkdirSync, readFileSync, realpathSync } from "node:fs";
17
- import { dirname, join } from "node:path";
16
+ import { mkdirSync, realpathSync } from "node:fs";
17
+ import { createInterface } from "node:readline/promises";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { PI_CONFIG_NAME } from "./branding.js";
20
- import { agentDir, piPackageDir } from "./credentials.js";
20
+ import { claudeCompatArgs } from "./claudeCompat.js";
21
+ import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
22
+ import { DISTRIBUTION } from "./distribution.js";
21
23
  import { login } from "./login.js";
22
24
  import { logout } from "./logout.js";
23
25
  import { buildLaunch } from "./launch.js";
24
26
  import { runDoctor } from "./doctor.js";
27
+ import { installProcessCrashHandlers } from "./crashReport.js";
28
+ import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
25
29
  import { maybeRefreshAtLaunch } from "./refresh.js";
26
30
  import { ensureShadowPiPackage } from "./piPackage.js";
27
31
  import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
28
32
  import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
29
33
  // Present as "yagni" in process listings, not "node".
30
- process.title = "yagni";
34
+ process.title = DISTRIBUTION.commandName;
35
+ /** The one-time Claude-compat trust question, asked on the launch TTY. */
36
+ async function confirmOnTty(question) {
37
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
38
+ try {
39
+ const answer = await rl.question(`${question} [Y/n] `);
40
+ return !/^n/i.test(answer.trim());
41
+ }
42
+ finally {
43
+ rl.close();
44
+ }
45
+ }
31
46
  async function runDefault(passthroughArgs) {
47
+ // Cache-backed update nudge (never a network wait), then a background cache
48
+ // refresh that completes while the session runs. Both fail soft.
49
+ await maybeNudgeAndRefresh({ current: cliVersion() });
32
50
  // Load the active environment's credentials (base URL respects a per-run
33
51
  // YAGNI_BASE_URL override). The token must belong to this environment.
34
52
  const profile = await readActiveProfile();
@@ -68,6 +86,21 @@ async function runDefault(passthroughArgs) {
68
86
  catch {
69
87
  shadowPiDir = undefined;
70
88
  }
89
+ // Zero-config Claude Code compat: wire this repo's (and the user's)
90
+ // .claude/skills and .claude/commands into the agent, asking the one-time
91
+ // per-folder trust question when needed. Fail-soft: compat can only add
92
+ // flags, never block a launch.
93
+ let compat = { argv: [], env: {} };
94
+ try {
95
+ compat = await claudeCompatArgs({
96
+ cwd: process.cwd(),
97
+ agentDir: piAgentDir,
98
+ confirm: confirmOnTty,
99
+ });
100
+ }
101
+ catch {
102
+ compat = { argv: [], env: {} };
103
+ }
71
104
  // buildLaunch runs the token-expiry preflight: it throws (with an actionable
72
105
  // login prompt) on an already-expired token so we never spawn a session that
73
106
  // immediately 401s, and returns non-fatal warnings (e.g. expiry approaching).
@@ -79,9 +112,13 @@ async function runDefault(passthroughArgs) {
79
112
  extensionPath: resolveExtensionPath(),
80
113
  agentDir: piAgentDir,
81
114
  piPackageDir: shadowPiDir,
115
+ extraAgentArgs: compat.argv,
116
+ extraEnv: compat.env,
82
117
  // Forward the active profile's file path so the extension can persist a
83
118
  // mid-session token rotation back to the same profile the launcher read.
84
119
  profilePath: profilePath(profile.name),
120
+ stateDir: credentialsDir(),
121
+ cliVersion: cliVersion(),
85
122
  baseEnv: process.env,
86
123
  });
87
124
  }
@@ -110,17 +147,37 @@ export const HELP_TEXT = [
110
147
  "YAGNI Code — a business-context-grounded terminal coding agent.",
111
148
  "",
112
149
  "Usage:",
113
- " yagni login Authorize the active environment (device-code flow).",
114
- " yagni logout Revoke and clear the active environment's token.",
115
- " yagni doctor Check that everything is ready (green/red checklist).",
116
- " yagni use <name> Switch the active environment (sticky).",
117
- " Presets: prod, local. Others need --base-url <url>.",
118
- " yagni profiles List saved environments; the active one is marked.",
119
- " yagni version Print the CLI version.",
120
- " yagni [args…] Launch the agent in the current repo.",
150
+ " yagni [args…] Launch the agent in the current repo.",
151
+ " yagni -c Continue the most recent session.",
152
+ " yagni -r Browse and resume a previous session.",
153
+ ' yagni -p "prompt" Print one response and exit (reads piped stdin too).',
154
+ " yagni login Authorize the active environment (device-code flow).",
155
+ " yagni logout Revoke and clear the active environment's token.",
156
+ " yagni doctor Check that everything is ready (green/red checklist).",
157
+ " yagni use <name> Switch the active environment (sticky).",
158
+ " Presets: prod, local. Others need --base-url <url>.",
159
+ " yagni profiles List saved environments; the active one is marked.",
160
+ " yagni upgrade [x.y.z] Upgrade the CLI to the latest (or a given) version.",
161
+ " yagni version Print the CLI version.",
162
+ "",
163
+ "Common agent flags (passed straight through):",
164
+ " --model <tier> Pick the model tier (balanced by default).",
165
+ " --thinking <level> off | minimal | low | medium | high | xhigh | max",
166
+ " --session <id> Open a specific session; --fork <id> branches one.",
167
+ " --mode json Emit machine-readable events (for scripts and CI).",
168
+ "",
169
+ "In a session:",
170
+ " /name Runs this repo's .claude/commands/<name>.md template.",
171
+ " /mode plan|review|auto Hold writes for planning, or confirm each change.",
172
+ " /todos Show the agent's live task list.",
173
+ " /cost Session usage and credit headroom.",
121
174
  "",
122
175
  "The active environment is sticky; `use` switches it (prod is the default).",
123
176
  "Set YAGNI_BASE_URL to override the base URL for a single run.",
177
+ "Set YAGNI_DISABLE_UPDATE_CHECK=1 to silence the new-version notice.",
178
+ "Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to skip loading .claude assets.",
179
+ "Set YAGNI_DISABLE_BRANDING=1 to pass the system prompt through unmodified.",
180
+ "Set YAGNI_DISABLE_CRASH_REPORTS=1 to turn off sanitized crash reports.",
124
181
  ].join("\n");
125
182
  /** Parse `use <name> [--base-url <url>]` argv into its parts. */
126
183
  export function parseUseArgs(args) {
@@ -194,15 +251,9 @@ export function wantsHelp(argv) {
194
251
  export function wantsVersion(argv) {
195
252
  return argv[0] === "version" || argv.includes("--version") || argv.includes("-v");
196
253
  }
197
- /**
198
- * The CLI's own version, read from this package's package.json. Resolved
199
- * relative to the module (src/ and dist/ both sit one level below the package
200
- * root), so it is correct under tsx in tests and in the published tarball.
201
- */
254
+ /** The CLI's own version (see `currentCliVersion` — upgrade.ts owns it). */
202
255
  export function cliVersion() {
203
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
204
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
205
- return pkg.version ?? "0.0.0";
256
+ return currentCliVersion();
206
257
  }
207
258
  export async function main(argv) {
208
259
  const [command, ...rest] = argv;
@@ -232,6 +283,9 @@ export async function main(argv) {
232
283
  if (command === "doctor") {
233
284
  return runDoctor();
234
285
  }
286
+ if (command === "upgrade") {
287
+ return upgradeCommand(rest, { current: cliVersion() });
288
+ }
235
289
  if (command === "use") {
236
290
  return useCommand(rest);
237
291
  }
@@ -267,6 +321,11 @@ export function isEntrypoint(argv1, moduleUrl) {
267
321
  // Only auto-run when invoked as the CLI entry, so tests can import this module
268
322
  // (e.g. to exercise wantsHelp) without spawning the agent.
269
323
  if (isEntrypoint(process.argv[1], import.meta.url)) {
324
+ // Crash reporting for the LAUNCHER process only (pi runs as a child and the
325
+ // extension covers the session side). Fire-and-forget, sanitized, bounded;
326
+ // YAGNI_DISABLE_CRASH_REPORTS=1 turns it off. Registered before main() so a
327
+ // crash anywhere in the launcher is covered.
328
+ installProcessCrashHandlers({ client: "cli", clientVersion: cliVersion() });
270
329
  main(process.argv.slice(2))
271
330
  .then((code) => process.exit(code))
272
331
  .catch((err) => {
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Shared CLI config: where the YAGNI backend lives + the environment presets. */
2
- export declare const DEFAULT_BASE_URL = "https://yagni.app";
2
+ export declare const DEFAULT_BASE_URL: string;
3
3
  /**
4
4
  * Built-in environment presets. These only SEED a profile's base URL when you
5
5
  * `yagni use <name>` without `--base-url`; once stored, the profile's own
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Shared CLI config: where the YAGNI backend lives + the environment presets. */
2
- export const DEFAULT_BASE_URL = "https://yagni.app";
2
+ import { DISTRIBUTION } from "./distribution.js";
3
+ export const DEFAULT_BASE_URL = DISTRIBUTION.defaultBaseUrl;
3
4
  /**
4
5
  * Built-in environment presets. These only SEED a profile's base URL when you
5
6
  * `yagni use <name>` without `--base-url`; once stored, the profile's own
@@ -8,7 +9,8 @@ export const DEFAULT_BASE_URL = "https://yagni.app";
8
9
  * (pass `--base-url`).
9
10
  */
10
11
  export const PRESET_BASE_URLS = {
11
- prod: DEFAULT_BASE_URL,
12
+ prod: "https://yagni.app",
13
+ staging: "https://yagni-staging.app",
12
14
  local: "http://localhost:3456",
13
15
  };
14
16
  /** The base URL a named preset resolves to, or undefined for a custom name. */
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Crash reporting for the YAGNI Code launcher (and, via deep import, the
3
+ * desktop driver): a sanitized, fire-and-forget POST to the customer's own
4
+ * YAGNI backend at /api/yagni-code/crash. Deliberately no third-party
5
+ * telemetry SDK — crash reports are the ONLY thing this CLI phones home, they
6
+ * contain no code and no prompts, and `YAGNI_DISABLE_CRASH_REPORTS=1` turns
7
+ * them off entirely (same switch family as YAGNI_DISABLE_UPDATE_CHECK).
8
+ *
9
+ * Everything here is fail-soft: reporting never throws, never retries, never
10
+ * blocks longer than the short timeout, and never changes how the process
11
+ * would otherwise crash.
12
+ *
13
+ * The sanitizer core (scrub patterns + env-value redaction + path collapse +
14
+ * caps) is duplicated in `pi-extension-yagni/src/crashReport.ts` (the
15
+ * extension is bundled standalone and cannot import across packages) and
16
+ * re-run server-side in `backend/src/yagniCode/crashReports.ts`. Keep the
17
+ * three in sync. Spec:
18
+ * docs/superpowers/specs/2026-08-08-crash-reporting-design.md
19
+ */
20
+ export declare const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
21
+ export declare const CRASH_REPORT_TIMEOUT_MS = 1500;
22
+ /** Hard ceiling the process-exit path waits for a report before exiting. */
23
+ export declare const CRASH_REPORT_EXIT_RACE_MS = 1600;
24
+ export declare const MAX_CRASH_MESSAGE = 512;
25
+ export declare const MAX_CRASH_ERROR_CLASS = 128;
26
+ export declare const MAX_CRASH_STACK = 8000;
27
+ export declare const MAX_CRASH_STACK_FRAMES = 40;
28
+ /** If the serialized payload would exceed this, the stack is dropped. */
29
+ export declare const MAX_CRASH_PAYLOAD_BYTES = 16384;
30
+ /** Same truthiness rule as updateChecksDisabled: set and not "" / "0". */
31
+ export declare function crashReportsDisabled(env?: NodeJS.ProcessEnv): boolean;
32
+ export interface SanitizeCrashOptions {
33
+ /** Environment whose values get redacted (defaults to process.env). */
34
+ env?: NodeJS.ProcessEnv;
35
+ /** Repo root; paths under it become repo-relative instead of basenames. */
36
+ repoRoot?: string;
37
+ }
38
+ /**
39
+ * Sanitize one piece of crash text. Order matters:
40
+ * 1. env-value redaction (non-path values, length >= 8) → `[ENV:NAME]`
41
+ * 2. secret patterns (keys, connection strings, base64 blobs)
42
+ * 3. home-dir collapse to `~`
43
+ * 4. repo-root strip (repo files stay readable as relative paths)
44
+ * 5. every remaining absolute/`~` path → basename (node_modules kept from
45
+ * `node_modules/` on, so dependency frames stay diagnosable)
46
+ * Over-redacts rather than under-redacts; pure; never throws.
47
+ */
48
+ export declare function sanitizeCrashText(text: string, opts?: SanitizeCrashOptions): string;
49
+ export interface SanitizedCrash {
50
+ errorClass: string;
51
+ message: string;
52
+ stack?: string;
53
+ }
54
+ /**
55
+ * Reduce an arbitrary thrown value to sanitized error facts. Reads ONLY
56
+ * `name`, `message`, and `stack` — custom fields (response bodies, request
57
+ * payloads…) are never touched.
58
+ */
59
+ export declare function sanitizeCrashError(err: unknown, opts?: SanitizeCrashOptions): SanitizedCrash;
60
+ export type CrashClient = "cli" | "desktop" | "desktop-driver";
61
+ export interface CrashReportInput {
62
+ client: CrashClient;
63
+ clientVersion: string;
64
+ error: unknown;
65
+ /** Command/phase label, e.g. "uncaught-exception" or "go:implement". */
66
+ context?: string;
67
+ baseUrl: string;
68
+ token: string;
69
+ env?: NodeJS.ProcessEnv;
70
+ repoRoot?: string;
71
+ fetchImpl?: typeof fetch;
72
+ timeoutMs?: number;
73
+ }
74
+ /**
75
+ * Pre-extracted crash facts (e.g. relayed from the desktop webview or the
76
+ * Rust panic hook, where no live Error object crosses the boundary). Each
77
+ * text field is sanitized and capped before sending.
78
+ */
79
+ export interface CrashFieldsInput {
80
+ client: CrashClient;
81
+ clientVersion: string;
82
+ errorClass: string;
83
+ message: string;
84
+ stack?: string;
85
+ context?: string;
86
+ baseUrl: string;
87
+ token: string;
88
+ env?: NodeJS.ProcessEnv;
89
+ repoRoot?: string;
90
+ fetchImpl?: typeof fetch;
91
+ timeoutMs?: number;
92
+ }
93
+ /**
94
+ * Sanitize + POST one crash report from pre-extracted fields. Resolves on
95
+ * every outcome — timeout, network error, non-2xx, disabled — and never
96
+ * throws; there is nobody upstream who could act on a failure, and a
97
+ * reporting bug must never re-crash the process.
98
+ */
99
+ export declare function sendCrashReport(input: CrashFieldsInput): Promise<void>;
100
+ /**
101
+ * Fire one crash report from a live thrown value. Same never-throws contract
102
+ * as {@link sendCrashReport}, which does the sending.
103
+ */
104
+ export declare function reportCrash(input: CrashReportInput): Promise<void>;
105
+ export interface InstallCrashHandlersOptions {
106
+ client: CrashClient;
107
+ clientVersion: string;
108
+ /** Credentials source; defaults to the active profile. Fail-soft. */
109
+ resolveTarget?: () => Promise<{
110
+ baseUrl: string;
111
+ token: string;
112
+ } | null>;
113
+ env?: NodeJS.ProcessEnv;
114
+ repoRoot?: string;
115
+ fetchImpl?: typeof fetch;
116
+ /** Ceiling on how long the exit path waits for the report. */
117
+ raceMs?: number;
118
+ /** Test seams. */
119
+ exit?: (code: number) => void;
120
+ writeStderr?: (text: string) => void;
121
+ proc?: Pick<NodeJS.Process, "on">;
122
+ }
123
+ /**
124
+ * The handler behind uncaughtException/unhandledRejection: report (bounded by
125
+ * `raceMs`), then preserve today's crash behavior — print the error to stderr
126
+ * and exit 1. Exposed for tests; `installProcessCrashHandlers` registers it.
127
+ */
128
+ export declare function makeProcessCrashHandler(context: "uncaught-exception" | "unhandled-rejection", opts: InstallCrashHandlersOptions): (err: unknown) => void;
129
+ /**
130
+ * Install process-level crash reporting. Call once, before main() runs, from
131
+ * a process YAGNI owns (the launcher, the desktop driver) — never from inside
132
+ * pi's process, where adding these listeners would change crash semantics.
133
+ */
134
+ export declare function installProcessCrashHandlers(opts: InstallCrashHandlersOptions): void;
135
+ //# sourceMappingURL=crashReport.d.ts.map