@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,93 @@
1
+ /**
2
+ * Refreshing TokenProvider — kills the boot-time token snapshot (spec §4, W4).
3
+ *
4
+ * The extension used to capture `YAGNI_TOKEN` once at registration, so a long
5
+ * session outlived its token and every grounding tool started 401-ing with no
6
+ * recovery. The provider owns the CURRENT token + expiry instead:
7
+ *
8
+ * - `getToken()` always returns the current token (tools read it per call).
9
+ * - A proactive, unref'd timer rotates the token via `POST /auth/refresh`
10
+ * when it is within 24h of expiry (the CLI launcher covers the 7-day
11
+ * window at spawn; this covers sessions that outlive that).
12
+ * - On a tool 401, {@link makeAuthedFetch} asks for ONE refresh and retries
13
+ * the request once with the fresh token. ALL refresh calls (proactive and
14
+ * 401-triggered, from any number of concurrent tools) are serialized behind
15
+ * a single in-flight promise, so a burst of 401s costs one rotation.
16
+ * - On rotation the provider updates `process.env.YAGNI_TOKEN` (pi re-resolves
17
+ * `$YAGNI_TOKEN` per model request — see provider.ts) AND
18
+ * `process.env.YAGNI_TOKEN_EXPIRES_AT` (config.ts reads it), and persists
19
+ * the rotated token back to the launcher's profile file (`YAGNI_PROFILE_PATH`,
20
+ * atomic write, 0600) when set, so the next `yagni` launch reuses it.
21
+ *
22
+ * Everything is FAIL-SOFT: a refresh failure leaves the current token in place
23
+ * (the old token keeps a 5-minute server-side grace after a successful rotation,
24
+ * so in-flight requests never race the swap).
25
+ */
26
+ /** How close to expiry (24h) the proactive in-session refresh fires. */
27
+ export declare const PROACTIVE_REFRESH_WINDOW_MS: number;
28
+ /** How long after a failed proactive refresh the timer retries (fail-soft). */
29
+ export declare const REFRESH_RETRY_MS: number;
30
+ /** The rotated-token payload from `POST /api/yagni-code/auth/refresh` (§2). */
31
+ interface RefreshResponse {
32
+ token: string;
33
+ expiresAt?: string;
34
+ workspaceId?: string;
35
+ }
36
+ /** A cancellable timer seam: schedule `fn` after `ms`, return a cancel. */
37
+ export type ScheduleFn = (fn: () => void, ms: number) => () => void;
38
+ /** Injectable seams for {@link makeTokenProvider}. */
39
+ export interface TokenProviderDeps {
40
+ baseUrl: string;
41
+ /**
42
+ * The env object rotations are WRITTEN to (default: process.env). Seeding is
43
+ * the caller's job (registerYagni resolves the env itself) so the provider
44
+ * never silently re-reads a different env than the caller intended.
45
+ */
46
+ env?: NodeJS.ProcessEnv;
47
+ /** Seed token (undefined = not authenticated; refresh() is then a no-op). */
48
+ initialToken?: string;
49
+ /** Seed ISO expiry (undefined = unknown; the proactive timer stays off). */
50
+ initialExpiresAt?: string;
51
+ fetchImpl?: typeof fetch;
52
+ now?: () => number;
53
+ /** Timer seam (default: unref'd setTimeout); injected in tests. */
54
+ schedule?: ScheduleFn;
55
+ /**
56
+ * Persist a rotation to the active profile file. Default: atomic 0600 write
57
+ * to `env.YAGNI_PROFILE_PATH` (merging the existing profile JSON), no-op when
58
+ * the launcher did not forward a profile path. Always fail-soft.
59
+ */
60
+ persistProfile?: (rotation: RefreshResponse) => void;
61
+ }
62
+ /** The live token surface the tools and the model provider read through. */
63
+ export interface TokenProvider {
64
+ getToken(): string | undefined;
65
+ getExpiresAt(): string | undefined;
66
+ /**
67
+ * Rotate the token via `POST /auth/refresh`. Serialized: concurrent callers
68
+ * share one in-flight request. Resolves `true` only when a new token was
69
+ * installed. Never throws (fail-soft).
70
+ */
71
+ refresh(): Promise<boolean>;
72
+ /** Cancel the proactive timer (test hygiene; the default timer is unref'd). */
73
+ dispose(): void;
74
+ }
75
+ /**
76
+ * Atomic profile write: merge the rotation into the existing profile JSON,
77
+ * write a fresh 0600 temp file in the same directory, then rename over the
78
+ * original (rename is atomic on the same filesystem). Mirrors the launcher's
79
+ * profiles.ts format (2-space indent + trailing newline). Fail-soft.
80
+ */
81
+ export declare function persistRotationToProfile(profilePath: string, rotation: RefreshResponse): void;
82
+ export declare function makeTokenProvider(deps: TokenProviderDeps): TokenProvider;
83
+ /**
84
+ * The ONE 401 seam for every authed fetch: wrap the base fetch so a 401
85
+ * triggers a single serialized refresh and, when the token actually rotated,
86
+ * ONE retry with a fresh `authorization` header. Non-401 responses and
87
+ * failed refreshes pass through untouched; a body that cannot be replayed
88
+ * (anything but a string/undefined) is never retried. The wrapper adds no
89
+ * retry loops of its own — resilientFetch above it still owns transient retry.
90
+ */
91
+ export declare function makeAuthedFetch(provider: TokenProvider, baseFetch?: typeof fetch): typeof fetch;
92
+ export {};
93
+ //# sourceMappingURL=tokenProvider.d.ts.map
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Refreshing TokenProvider — kills the boot-time token snapshot (spec §4, W4).
3
+ *
4
+ * The extension used to capture `YAGNI_TOKEN` once at registration, so a long
5
+ * session outlived its token and every grounding tool started 401-ing with no
6
+ * recovery. The provider owns the CURRENT token + expiry instead:
7
+ *
8
+ * - `getToken()` always returns the current token (tools read it per call).
9
+ * - A proactive, unref'd timer rotates the token via `POST /auth/refresh`
10
+ * when it is within 24h of expiry (the CLI launcher covers the 7-day
11
+ * window at spawn; this covers sessions that outlive that).
12
+ * - On a tool 401, {@link makeAuthedFetch} asks for ONE refresh and retries
13
+ * the request once with the fresh token. ALL refresh calls (proactive and
14
+ * 401-triggered, from any number of concurrent tools) are serialized behind
15
+ * a single in-flight promise, so a burst of 401s costs one rotation.
16
+ * - On rotation the provider updates `process.env.YAGNI_TOKEN` (pi re-resolves
17
+ * `$YAGNI_TOKEN` per model request — see provider.ts) AND
18
+ * `process.env.YAGNI_TOKEN_EXPIRES_AT` (config.ts reads it), and persists
19
+ * the rotated token back to the launcher's profile file (`YAGNI_PROFILE_PATH`,
20
+ * atomic write, 0600) when set, so the next `yagni` launch reuses it.
21
+ *
22
+ * Everything is FAIL-SOFT: a refresh failure leaves the current token in place
23
+ * (the old token keeps a 5-minute server-side grace after a successful rotation,
24
+ * so in-flight requests never race the swap).
25
+ */
26
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
27
+ import { dirname, join } from "node:path";
28
+ /** How close to expiry (24h) the proactive in-session refresh fires. */
29
+ export const PROACTIVE_REFRESH_WINDOW_MS = 24 * 60 * 60 * 1000;
30
+ /** How long after a failed proactive refresh the timer retries (fail-soft). */
31
+ export const REFRESH_RETRY_MS = 60 * 60 * 1000;
32
+ /** Per-request wall-clock cap so a hung refresh never stalls a 401 retry. */
33
+ const REFRESH_REQUEST_TIMEOUT_MS = 10_000;
34
+ /** setTimeout overflows past 2^31-1 ms; clamp long delays and re-arm on fire. */
35
+ const MAX_TIMER_MS = 2 ** 31 - 1;
36
+ const defaultSchedule = (fn, ms) => {
37
+ const t = setTimeout(fn, ms);
38
+ // Never keep the process alive for a background refresh.
39
+ t.unref?.();
40
+ return () => clearTimeout(t);
41
+ };
42
+ /**
43
+ * Atomic profile write: merge the rotation into the existing profile JSON,
44
+ * write a fresh 0600 temp file in the same directory, then rename over the
45
+ * original (rename is atomic on the same filesystem). Mirrors the launcher's
46
+ * profiles.ts format (2-space indent + trailing newline). Fail-soft.
47
+ */
48
+ export function persistRotationToProfile(profilePath, rotation) {
49
+ try {
50
+ let existing = {};
51
+ try {
52
+ const parsed = JSON.parse(readFileSync(profilePath, "utf8"));
53
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
54
+ existing = parsed;
55
+ }
56
+ }
57
+ catch {
58
+ /* missing/corrupt profile — still persist the rotation fields */
59
+ }
60
+ const next = {
61
+ ...existing,
62
+ token: rotation.token,
63
+ ...(rotation.expiresAt ? { expiresAt: rotation.expiresAt } : {}),
64
+ ...(rotation.workspaceId ? { workspaceId: rotation.workspaceId } : {}),
65
+ };
66
+ mkdirSync(dirname(profilePath), { recursive: true, mode: 0o700 });
67
+ const tmp = join(dirname(profilePath), `.${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.tmp`);
68
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
69
+ // writeFile honors mode only on create; the tmp file IS a create, but be
70
+ // explicit so a umask can never widen a token-bearing file.
71
+ chmodSync(tmp, 0o600);
72
+ renameSync(tmp, profilePath);
73
+ }
74
+ catch {
75
+ /* fail-soft: a failed persist never breaks the session (memory + env win) */
76
+ }
77
+ }
78
+ export function makeTokenProvider(deps) {
79
+ const env = deps.env ?? process.env;
80
+ const fetchImpl = deps.fetchImpl ?? fetch;
81
+ const now = deps.now ?? (() => Date.now());
82
+ const schedule = deps.schedule ?? defaultSchedule;
83
+ const persistProfile = deps.persistProfile ??
84
+ ((rotation) => {
85
+ const profilePath = env.YAGNI_PROFILE_PATH?.trim();
86
+ if (profilePath)
87
+ persistRotationToProfile(profilePath, rotation);
88
+ });
89
+ let token = deps.initialToken;
90
+ let expiresAt = deps.initialExpiresAt;
91
+ let cancelTimer = null;
92
+ let inFlight = null;
93
+ function parsedExpiryMs() {
94
+ if (!expiresAt)
95
+ return null;
96
+ const ms = Date.parse(expiresAt);
97
+ return Number.isNaN(ms) ? null : ms;
98
+ }
99
+ /** (Re)arm the proactive timer from the current expiry. Unknown expiry → off. */
100
+ function armProactiveTimer() {
101
+ cancelTimer?.();
102
+ cancelTimer = null;
103
+ const expiryMs = parsedExpiryMs();
104
+ if (expiryMs === null)
105
+ return;
106
+ if (expiryMs <= now())
107
+ return; // already expired: refresh would 401 forever
108
+ const untilWindow = expiryMs - now() - PROACTIVE_REFRESH_WINDOW_MS;
109
+ const delay = Math.min(Math.max(untilWindow, 0), MAX_TIMER_MS);
110
+ cancelTimer = schedule(() => {
111
+ cancelTimer = null;
112
+ // A clamped long delay fired early: not in the window yet, just re-arm.
113
+ const nowExpiryMs = parsedExpiryMs();
114
+ if (nowExpiryMs !== null && nowExpiryMs - now() > PROACTIVE_REFRESH_WINDOW_MS) {
115
+ armProactiveTimer();
116
+ return;
117
+ }
118
+ void refresh().then((rotated) => {
119
+ // Success re-arms from the new expiry inside applyRotation; a failure
120
+ // retries later (fail-soft) as long as the token is not yet expired.
121
+ if (!rotated && cancelTimer === null) {
122
+ const expiry = parsedExpiryMs();
123
+ if (expiry !== null && expiry > now()) {
124
+ cancelTimer = schedule(() => {
125
+ cancelTimer = null;
126
+ void refresh();
127
+ }, Math.min(REFRESH_RETRY_MS, MAX_TIMER_MS));
128
+ }
129
+ }
130
+ });
131
+ }, delay);
132
+ }
133
+ function applyRotation(rotation) {
134
+ token = rotation.token;
135
+ if (rotation.expiresAt)
136
+ expiresAt = rotation.expiresAt;
137
+ // pi re-resolves $YAGNI_TOKEN per model request; config helpers read the
138
+ // expiry env. Both must see the rotation immediately.
139
+ env.YAGNI_TOKEN = rotation.token;
140
+ if (rotation.expiresAt)
141
+ env.YAGNI_TOKEN_EXPIRES_AT = rotation.expiresAt;
142
+ try {
143
+ persistProfile(rotation);
144
+ }
145
+ catch {
146
+ /* fail-soft */
147
+ }
148
+ armProactiveTimer();
149
+ }
150
+ async function doRefresh() {
151
+ const current = token;
152
+ if (!current)
153
+ return false;
154
+ try {
155
+ const res = await fetchImpl(`${deps.baseUrl}/api/yagni-code/auth/refresh`, {
156
+ method: "POST",
157
+ headers: {
158
+ "content-type": "application/json",
159
+ authorization: `Bearer ${current}`,
160
+ },
161
+ body: "{}",
162
+ signal: AbortSignal.timeout(REFRESH_REQUEST_TIMEOUT_MS),
163
+ });
164
+ if (!res.ok)
165
+ return false;
166
+ const data = (await res.json());
167
+ if (!data || typeof data.token !== "string" || data.token.length === 0)
168
+ return false;
169
+ applyRotation({
170
+ token: data.token,
171
+ expiresAt: typeof data.expiresAt === "string" ? data.expiresAt : undefined,
172
+ workspaceId: typeof data.workspaceId === "string" ? data.workspaceId : undefined,
173
+ });
174
+ return true;
175
+ }
176
+ catch {
177
+ return false; // network/timeout: keep the current token (fail-soft)
178
+ }
179
+ }
180
+ function refresh() {
181
+ // Single-flight: a proactive tick and N simultaneous 401 handlers share
182
+ // one POST, so the backend never sees a refresh stampede and the 5-minute
183
+ // grace window on the old token stays meaningful.
184
+ if (!inFlight) {
185
+ inFlight = doRefresh().finally(() => {
186
+ inFlight = null;
187
+ });
188
+ }
189
+ return inFlight;
190
+ }
191
+ armProactiveTimer();
192
+ return {
193
+ getToken: () => token,
194
+ getExpiresAt: () => expiresAt,
195
+ refresh,
196
+ dispose: () => {
197
+ cancelTimer?.();
198
+ cancelTimer = null;
199
+ },
200
+ };
201
+ }
202
+ /**
203
+ * The ONE 401 seam for every authed fetch: wrap the base fetch so a 401
204
+ * triggers a single serialized refresh and, when the token actually rotated,
205
+ * ONE retry with a fresh `authorization` header. Non-401 responses and
206
+ * failed refreshes pass through untouched; a body that cannot be replayed
207
+ * (anything but a string/undefined) is never retried. The wrapper adds no
208
+ * retry loops of its own — resilientFetch above it still owns transient retry.
209
+ */
210
+ export function makeAuthedFetch(provider, baseFetch) {
211
+ const underlying = baseFetch ?? fetch;
212
+ const wrapped = async (input, init) => {
213
+ const res = await underlying(input, init);
214
+ if (res.status !== 401)
215
+ return res;
216
+ // Only string (or absent) bodies are safely re-sendable.
217
+ if (init?.body !== undefined && typeof init.body !== "string")
218
+ return res;
219
+ let rotated = false;
220
+ try {
221
+ rotated = await provider.refresh();
222
+ }
223
+ catch {
224
+ rotated = false;
225
+ }
226
+ if (!rotated)
227
+ return res;
228
+ const headers = new Headers(init?.headers);
229
+ headers.set("authorization", `Bearer ${provider.getToken() ?? ""}`);
230
+ return underlying(input, { ...init, headers });
231
+ };
232
+ return wrapped;
233
+ }
234
+ //# sourceMappingURL=tokenProvider.js.map
package/dist/launch.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Build the configured pi launch. `buildLaunch` is pure (no spawn, no fs) so it
3
- * is fully unit-testable: it returns the child environment and the pi argv that
4
- * registers pi-extension-yagni and defaults the provider to `yagni`.
2
+ * Build the configured pi launch. `buildLaunch` has no spawn/fs side effects,
3
+ * so it stays fully unit-testable, though it is no longer strictly pure: it
4
+ * mints a fresh session id (`randomUUID()`) on every call. It returns the
5
+ * child environment and the pi argv that registers pi-extension-yagni and
6
+ * defaults the provider to `yagni`.
5
7
  */
6
8
  import type { Credentials } from "./credentials.js";
7
9
  export interface LaunchPlan {
@@ -59,10 +61,30 @@ export interface BuildLaunchOptions {
59
61
  * in-process only, still fail-soft).
60
62
  */
61
63
  profilePath?: string;
64
+ /** Distribution-specific root shared with the bundled extension. */
65
+ stateDir?: string;
66
+ /**
67
+ * Launcher version, forwarded as `YAGNI_CODE_VERSION` so the bundled
68
+ * extension (which has no package.json on disk to read) can label its crash
69
+ * reports with the shipping version. Optional: omitted, reports say "unknown".
70
+ */
71
+ cliVersion?: string;
62
72
  /** Base environment to extend (defaults to process.env at call sites). */
63
73
  baseEnv?: NodeJS.ProcessEnv;
64
74
  /** Clock seam for the token-expiry preflight (defaults to Date.now). */
65
75
  now?: () => number;
76
+ /**
77
+ * Launcher-computed agent flags (e.g. Claude-compat `--skill` /
78
+ * `--prompt-template` paths). Inserted before the user's passthrough args so
79
+ * anything the user passes explicitly still wins.
80
+ */
81
+ extraAgentArgs?: string[];
82
+ /**
83
+ * Launcher-computed child env (e.g. Claude-compat plugin agent dirs and
84
+ * rules dirs). Merged before the YAGNI-critical keys so it can never
85
+ * override the token, base URL, or hermetic agent dir.
86
+ */
87
+ extraEnv?: Record<string, string>;
66
88
  }
67
89
  export declare function buildLaunch(creds: Credentials | null, passthroughArgs: string[], opts: BuildLaunchOptions): LaunchPlan;
68
90
  //# sourceMappingURL=launch.d.ts.map
package/dist/launch.js CHANGED
@@ -1,8 +1,11 @@
1
1
  /**
2
- * Build the configured pi launch. `buildLaunch` is pure (no spawn, no fs) so it
3
- * is fully unit-testable: it returns the child environment and the pi argv that
4
- * registers pi-extension-yagni and defaults the provider to `yagni`.
2
+ * Build the configured pi launch. `buildLaunch` has no spawn/fs side effects,
3
+ * so it stays fully unit-testable, though it is no longer strictly pure: it
4
+ * mints a fresh session id (`randomUUID()`) on every call. It returns the
5
+ * child environment and the pi argv that registers pi-extension-yagni and
6
+ * defaults the provider to `yagni`.
5
7
  */
8
+ import { randomUUID } from "node:crypto";
6
9
  import { agentDirEnvVar } from "./branding.js";
7
10
  /**
8
11
  * How close to expiry the token can be before launch warns. A coding session
@@ -50,6 +53,13 @@ export function buildLaunch(creds, passthroughArgs, opts) {
50
53
  }
51
54
  const env = {
52
55
  ...(opts.baseEnv ?? {}),
56
+ ...(opts.extraEnv ?? {}),
57
+ // One session id per launch (YAG-471 attribution). The extension reads this
58
+ // as YAGNI_SESSION_ID and forwards it as the model proxy's x-yagni-session-id
59
+ // header on the driver's own completions. /go's children override
60
+ // YAGNI_CALLER with their own stage/subagent/advisor label but inherit this
61
+ // session id (unless they also carry a YAGNI_RUN_ID, which wins server-side).
62
+ YAGNI_SESSION_ID: randomUUID(),
53
63
  YAGNI_TOKEN: creds.token,
54
64
  YAGNI_BASE_URL: creds.baseUrl,
55
65
  // Forward the token's expiry (when known) so the extension can surface a
@@ -62,6 +72,10 @@ export function buildLaunch(creds, passthroughArgs, opts) {
62
72
  // atomically persist a mid-session token rotation back to the same profile
63
73
  // the launcher read (0600). Absent when the caller can't resolve it.
64
74
  ...(opts.profilePath ? { YAGNI_PROFILE_PATH: opts.profilePath } : {}),
75
+ ...(opts.stateDir ? { YAGNI_CODE_HOME: opts.stateDir } : {}),
76
+ // Forward the launcher's version so the extension's crash reports carry
77
+ // the shipping version (the bundled extension has no package.json to read).
78
+ ...(opts.cliVersion ? { YAGNI_CODE_VERSION: opts.cliVersion } : {}),
65
79
  // Rebrand pi's chrome ("pi"/"π" → "YAGNI Code"): pi reads piConfig.name from
66
80
  // the package at PI_PACKAGE_DIR (a shadow package the launcher generates).
67
81
  ...(opts.piPackageDir ? { PI_PACKAGE_DIR: opts.piPackageDir } : {}),
@@ -85,20 +99,24 @@ export function buildLaunch(creds, passthroughArgs, opts) {
85
99
  // Always load our extension. Default the provider to `yagni` unless the user
86
100
  // explicitly chose one (so power users can still point pi elsewhere).
87
101
  const userChoseProvider = passthroughArgs.some(arg => arg === "--provider" || arg.startsWith("--provider="));
88
- // Default the model to the `advanced` tier the strongest judgment-grade
89
- // model, so interactive sessions get the best reasoning by default. A user
90
- // `--model` (e.g. `standard` or `efficient`) still wins. Without this, pi's
91
- // default-model heuristic could land an interactive session on a weaker tier.
102
+ // Default the model to the `balanced` tier (YAG-380). Balanced drives on the
103
+ // judgment-grade `advanced` model and additionally lets the session escalate
104
+ // hard calls to `peak` through the capped ask_advisor tool. So it is strictly
105
+ // additive: no session reasons worse than plain advanced, and the hard ones
106
+ // can reason better. A user `--model` (e.g. `standard` or `efficient`) still
107
+ // wins. Without this, pi's default-model heuristic could land an interactive
108
+ // session on a weaker tier.
92
109
  // Detect if the user explicitly set a model, whether via "--model" as a separate
93
110
  // argument or using the equals form "--model=efficient". The previous check only
94
- // caught the separate form, causing a duplicate "--model advanced" to be added
111
+ // caught the separate form, causing a duplicate "--model balanced" to be added
95
112
  // when the equals form was used.
96
113
  const userChoseModel = passthroughArgs.some(arg => arg === "--model" || arg.startsWith("--model="));
97
114
  const argv = [
98
115
  "-e",
99
116
  opts.extensionPath,
100
117
  ...(userChoseProvider ? [] : ["--provider", "yagni"]),
101
- ...(userChoseModel ? [] : ["--model", "advanced"]),
118
+ ...(userChoseModel ? [] : ["--model", "balanced"]),
119
+ ...(opts.extraAgentArgs ?? []),
102
120
  ...passthroughArgs,
103
121
  ];
104
122
  return { env, argv, warnings };
package/dist/login.d.ts CHANGED
@@ -9,6 +9,13 @@ export interface LoginDeps {
9
9
  baseUrl?: string;
10
10
  /** Which profile to store the minted token in. Defaults to the active one. */
11
11
  profileName?: string;
12
+ /**
13
+ * Client identifier sent with the token poll so the backend can attribute
14
+ * the minted credential to the app that requested it. Defaults to "cli";
15
+ * the desktop shell (which reuses this flow) passes "desktop". The server
16
+ * treats unknown values as absent, so this can never break a login.
17
+ */
18
+ client?: string;
12
19
  fetchImpl?: typeof fetch;
13
20
  now?: () => number;
14
21
  sleep?: (ms: number) => Promise<void>;
package/dist/login.js CHANGED
@@ -100,7 +100,9 @@ export async function login(deps = {}) {
100
100
  res = await fetchImpl(`${baseUrl}/api/yagni-code/auth/token`, {
101
101
  method: "POST",
102
102
  headers: { "content-type": "application/json" },
103
- body: JSON.stringify({ device_code: start.device_code }),
103
+ // `client` attributes the minted token to the requesting app; servers
104
+ // that predate the field simply ignore it.
105
+ body: JSON.stringify({ device_code: start.device_code, client: deps.client ?? "cli" }),
104
106
  signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS),
105
107
  });
106
108
  }
package/dist/paths.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Resolve the bundled dependency entry points the launcher needs: pi's CLI and
3
- * the pi-extension-yagni extension. Both are package dependencies, so we resolve
4
- * them from this module rather than assuming a global install.
2
+ * Resolve the dependency entry points the launcher needs: pi's CLI and the
3
+ * YAGNI extension. pi is a package dependency, so we resolve it from this module
4
+ * rather than assuming a global install; the extension ships inside our own dist.
5
5
  *
6
6
  * We use the ESM-native `import.meta.resolve` (NOT `createRequire().resolve`):
7
7
  * pi is an ESM-only package whose `exports` map declares only the `import`
@@ -9,7 +9,16 @@
9
9
  * on it. pi's package.json is also not an exported subpath, so we read it from
10
10
  * disk after resolving the package's main entry.
11
11
  */
12
- /** Absolute path to pi-extension-yagni's built entry (dist/index.js). */
12
+ /**
13
+ * Absolute path to the extension's built entry.
14
+ *
15
+ * Published installs read the copy bundled into this package's own
16
+ * `dist/extension/` (see scripts/bundle-extension.mjs) — pi-extension-yagni is a
17
+ * private workspace package and is never on the registry, so module resolution
18
+ * would fail. In the monorepo the bundled copy only exists after a build, so we
19
+ * fall back to resolving the workspace-linked package, which keeps `pnpm test`
20
+ * and a fresh checkout working before anything is bundled.
21
+ */
13
22
  export declare function resolveExtensionPath(): string;
14
23
  /**
15
24
  * Absolute path to pi's package root — the dir whose package.json names the
package/dist/paths.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Resolve the bundled dependency entry points the launcher needs: pi's CLI and
3
- * the pi-extension-yagni extension. Both are package dependencies, so we resolve
4
- * them from this module rather than assuming a global install.
2
+ * Resolve the dependency entry points the launcher needs: pi's CLI and the
3
+ * YAGNI extension. pi is a package dependency, so we resolve it from this module
4
+ * rather than assuming a global install; the extension ships inside our own dist.
5
5
  *
6
6
  * We use the ESM-native `import.meta.resolve` (NOT `createRequire().resolve`):
7
7
  * pi is an ESM-only package whose `exports` map declares only the `import`
@@ -9,14 +9,26 @@
9
9
  * on it. pi's package.json is also not an exported subpath, so we read it from
10
10
  * disk after resolving the package's main entry.
11
11
  */
12
- import { readFileSync } from "node:fs";
12
+ import { existsSync, readFileSync } from "node:fs";
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  function resolveModulePath(specifier) {
16
16
  return fileURLToPath(import.meta.resolve(specifier));
17
17
  }
18
- /** Absolute path to pi-extension-yagni's built entry (dist/index.js). */
18
+ /**
19
+ * Absolute path to the extension's built entry.
20
+ *
21
+ * Published installs read the copy bundled into this package's own
22
+ * `dist/extension/` (see scripts/bundle-extension.mjs) — pi-extension-yagni is a
23
+ * private workspace package and is never on the registry, so module resolution
24
+ * would fail. In the monorepo the bundled copy only exists after a build, so we
25
+ * fall back to resolving the workspace-linked package, which keeps `pnpm test`
26
+ * and a fresh checkout working before anything is bundled.
27
+ */
19
28
  export function resolveExtensionPath() {
29
+ const bundled = fileURLToPath(new URL("./extension/index.js", import.meta.url));
30
+ if (existsSync(bundled))
31
+ return bundled;
20
32
  return resolveModulePath("pi-extension-yagni");
21
33
  }
22
34
  /**
@@ -22,7 +22,7 @@ export interface Profile {
22
22
  expiresAt?: string;
23
23
  }
24
24
  /** The default active profile when nothing has been configured yet. */
25
- export declare const DEFAULT_PROFILE = "prod";
25
+ export declare const DEFAULT_PROFILE: string;
26
26
  export declare function isValidProfileName(name: string): boolean;
27
27
  export declare function profilesDir(): string;
28
28
  export declare function profilePath(name: string): string;
package/dist/profiles.js CHANGED
@@ -17,8 +17,9 @@ import { chmod, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises
17
17
  import { dirname, join } from "node:path";
18
18
  import { credentialsDir, deleteCredentials, readCredentials } from "./credentials.js";
19
19
  import { DEFAULT_BASE_URL, PRESET_BASE_URLS, presetBaseUrl } from "./config.js";
20
+ import { DISTRIBUTION } from "./distribution.js";
20
21
  /** The default active profile when nothing has been configured yet. */
21
- export const DEFAULT_PROFILE = "prod";
22
+ export const DEFAULT_PROFILE = DISTRIBUTION.defaultProfile;
22
23
  /**
23
24
  * Allowed profile-name shape. Restricts to a safe filename so a name can never
24
25
  * traverse out of the profiles dir or collide with `config.json`.
@@ -211,7 +212,9 @@ export async function migrateLegacyCredentials() {
211
212
  const legacy = await readCredentials();
212
213
  if (!legacy?.token)
213
214
  return;
214
- const name = legacy.baseUrl.replace(/\/$/, "") === DEFAULT_BASE_URL ? "prod" : "default";
215
+ const name = legacy.baseUrl.replace(/\/$/, "") === DEFAULT_BASE_URL
216
+ ? DISTRIBUTION.defaultProfile
217
+ : "default";
215
218
  await persistProfileCredentials(name, legacy);
216
219
  await setActiveProfileName(name);
217
220
  await deleteCredentials();