@yagni-app/code-staging 0.3.0-staging.1059.1 → 0.3.0-staging.1064.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extension/crashReport.js +1 -3
- package/dist/extension/footer.d.ts +4 -1
- package/dist/extension/footer.js +7 -3
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +84 -4
- package/dist/extension/permission.d.ts +13 -0
- package/dist/extension/permission.js +8 -0
- package/dist/extension/tokenProvider.js +46 -5
- package/package.json +2 -2
|
@@ -125,8 +125,6 @@ export function makeCrashReporter(opts) {
|
|
|
125
125
|
if (crashReportsDisabled(env))
|
|
126
126
|
return;
|
|
127
127
|
const token = opts.getToken();
|
|
128
|
-
if (!token)
|
|
129
|
-
return;
|
|
130
128
|
const sanitized = sanitizeCrashError(error, { env, repoRoot });
|
|
131
129
|
const payload = {
|
|
132
130
|
client: isDesktopSurface() ? "desktop" : "cli",
|
|
@@ -148,7 +146,7 @@ export function makeCrashReporter(opts) {
|
|
|
148
146
|
method: "POST",
|
|
149
147
|
headers: {
|
|
150
148
|
"content-type": "application/json",
|
|
151
|
-
authorization: `Bearer ${token}
|
|
149
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
152
150
|
},
|
|
153
151
|
body: JSON.stringify(payload),
|
|
154
152
|
signal: controller.signal,
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* — useful reference for what data to replicate and how to format it.
|
|
40
40
|
*/
|
|
41
41
|
import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
42
|
+
import type { ModeHolder, PermissionMode } from "./permission.js";
|
|
42
43
|
export declare const BRANCH_MAX_WIDTH = 60;
|
|
43
44
|
/**
|
|
44
45
|
* Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
|
|
@@ -90,6 +91,8 @@ export declare function detectGitInfo(cwd: string, home: string | undefined): Gi
|
|
|
90
91
|
export declare function renderFooterLines(input: {
|
|
91
92
|
git: GitInfo;
|
|
92
93
|
model: string;
|
|
94
|
+
/** Current permission mode; shown on line 2 to the left of the model as "<mode> mode". */
|
|
95
|
+
mode?: PermissionMode | null;
|
|
93
96
|
usage: UsageTotals;
|
|
94
97
|
contextPercent: number | null;
|
|
95
98
|
statuses: string[];
|
|
@@ -99,7 +102,7 @@ export declare function renderFooterLines(input: {
|
|
|
99
102
|
* and returns the component `setFooter` expects. Called from the
|
|
100
103
|
* `session_start` handler in index.ts.
|
|
101
104
|
*/
|
|
102
|
-
export declare function createYagniFooterFactory(ctx: ExtensionContext): (_tui: unknown, theme: Theme, footerData: ReadonlyFooterDataProvider) => {
|
|
105
|
+
export declare function createYagniFooterFactory(ctx: ExtensionContext, modeHolder?: ModeHolder): (_tui: unknown, theme: Theme, footerData: ReadonlyFooterDataProvider) => {
|
|
103
106
|
render(width: number): string[];
|
|
104
107
|
invalidate(): void;
|
|
105
108
|
dispose(): void;
|
package/dist/extension/footer.js
CHANGED
|
@@ -221,7 +221,7 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
221
221
|
line1Parts.push(theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH)));
|
|
222
222
|
}
|
|
223
223
|
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
224
|
-
// Line 2: model · ↑in ↓out $cost · ctx%
|
|
224
|
+
// Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
|
|
225
225
|
const statParts = [];
|
|
226
226
|
if (input.usage.input)
|
|
227
227
|
statParts.push(`↑${formatTokens(input.usage.input)}`);
|
|
@@ -231,7 +231,10 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
231
231
|
statParts.push(`$${input.usage.cost.toFixed(3)}`);
|
|
232
232
|
const stats = statParts.join(" ");
|
|
233
233
|
const percentText = input.contextPercent === null ? "?" : `${Math.round(input.contextPercent)}%`;
|
|
234
|
-
const line2Parts = [
|
|
234
|
+
const line2Parts = [];
|
|
235
|
+
if (input.mode)
|
|
236
|
+
line2Parts.push(dim(`${input.mode} mode`));
|
|
237
|
+
line2Parts.push(dim(input.model));
|
|
235
238
|
if (stats)
|
|
236
239
|
line2Parts.push(dim(stats));
|
|
237
240
|
line2Parts.push(theme.fg(contextColor(input.contextPercent), percentText));
|
|
@@ -249,7 +252,7 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
249
252
|
* and returns the component `setFooter` expects. Called from the
|
|
250
253
|
* `session_start` handler in index.ts.
|
|
251
254
|
*/
|
|
252
|
-
export function createYagniFooterFactory(ctx) {
|
|
255
|
+
export function createYagniFooterFactory(ctx, modeHolder) {
|
|
253
256
|
return (_tui, theme, footerData) => {
|
|
254
257
|
// Recompute git/worktree info only when the branch actually changes. Optional-
|
|
255
258
|
// chained: onBranchChange is typed on ReadonlyFooterDataProvider, but the runtime
|
|
@@ -273,6 +276,7 @@ export function createYagniFooterFactory(ctx) {
|
|
|
273
276
|
return renderFooterLines({
|
|
274
277
|
git: gitInfo(),
|
|
275
278
|
model: ctx.model?.id ?? "no-model",
|
|
279
|
+
mode: modeHolder?.get() ?? null,
|
|
276
280
|
usage: collectUsage(ctx.sessionManager),
|
|
277
281
|
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
278
282
|
statuses,
|
|
@@ -63,6 +63,12 @@ export interface RegisterYagniDeps {
|
|
|
63
63
|
tokenProvider?: TokenProvider;
|
|
64
64
|
/** The spool flush (R4 write half), injectable so tests never touch disk. */
|
|
65
65
|
flushSpool?: (opts: SpoolClientOpts) => Promise<FlushOutcome>;
|
|
66
|
+
/**
|
|
67
|
+
* Non-fatal auth-event reporter (YAG-500 Fix E). Defaults to
|
|
68
|
+
* `makeCrashReporter` gated on `!evalMode`; inject a spy in tests to assert
|
|
69
|
+
* the report is fired with `context: "auth-failure"` and the refresh outcome.
|
|
70
|
+
*/
|
|
71
|
+
authReporter?: (error: unknown, context?: string) => Promise<void>;
|
|
66
72
|
env?: NodeJS.ProcessEnv;
|
|
67
73
|
}
|
|
68
74
|
/**
|
package/dist/extension/index.js
CHANGED
|
@@ -23,14 +23,14 @@ import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone }
|
|
|
23
23
|
import { fetchMcpServers as defaultFetchMcpServers, registerMcpCommand, registerMcpTools, } from "./mcpTools.js";
|
|
24
24
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
25
25
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
26
|
-
import { DEFAULT_PERMISSION_POLICY, registerPermissionGate } from "./permission.js";
|
|
26
|
+
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission.js";
|
|
27
27
|
import { registerSubagents } from "./subagents.js";
|
|
28
28
|
import { registerTodos } from "./todos.js";
|
|
29
29
|
import { registerDecisionCommands } from "./decisions.js";
|
|
30
30
|
import { makeDecisionCapture } from "./decisionCapture.js";
|
|
31
31
|
import { registerAmbientRecall } from "./recall.js";
|
|
32
32
|
import { resilientFetch } from "./resilientFetch.js";
|
|
33
|
-
import { installUncaughtExceptionMonitor } from "./crashReport.js";
|
|
33
|
+
import { installUncaughtExceptionMonitor, makeCrashReporter } from "./crashReport.js";
|
|
34
34
|
import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
35
35
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
36
36
|
import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
@@ -113,6 +113,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
113
113
|
if (!evalMode) {
|
|
114
114
|
installUncaughtExceptionMonitor({ baseUrl, getToken: getTokenFn, env: deps.env });
|
|
115
115
|
}
|
|
116
|
+
// YAG-500 Fix E: non-fatal auth-event reporter for 401s on the model path.
|
|
117
|
+
// Reuses the crash endpoint (/api/yagni-code/crash) with a distinct context
|
|
118
|
+
// so auth failures are visible in Sentry/backend logs. Gated on !evalMode
|
|
119
|
+
// like every other external side effect; injectable for tests.
|
|
120
|
+
const authReporter = deps.authReporter ??
|
|
121
|
+
(!evalMode
|
|
122
|
+
? makeCrashReporter({ baseUrl, getToken: getTokenFn, fetchImpl: deps.fetchImpl, env: deps.env })
|
|
123
|
+
: async () => { });
|
|
124
|
+
// YAG-500 Fix A+C: the model-path 401 recovery outcome, set by the
|
|
125
|
+
// message_end handler so it can produce the right user-facing message. The
|
|
126
|
+
// after_provider_response event does NOT fire on a 401 (the OpenAI SDK throws
|
|
127
|
+
// before onResponse is reached), so message_end is the only seam.
|
|
128
|
+
let lastAuthRecovery = null;
|
|
116
129
|
const fullCatalog = await fetchCatalog({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
117
130
|
// Lock the interactive session to the `advanced` tier only. The backend
|
|
118
131
|
// catalog returns all tiers, but only `advanced` is registered with the
|
|
@@ -220,7 +233,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
220
233
|
// drafts a decision on "don't ask again". Default auto, so still additive.
|
|
221
234
|
// Mutating MCP tools join write/edit/bash in the gate policy: plan mode
|
|
222
235
|
// holds them, review mode confirms them.
|
|
236
|
+
const modeHolder = createModeHolder();
|
|
223
237
|
registerPermissionGate(pi, {
|
|
238
|
+
modeHolder,
|
|
224
239
|
...(mcpMutatingTools.length > 0
|
|
225
240
|
? {
|
|
226
241
|
policy: {
|
|
@@ -433,7 +448,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
433
448
|
// renders. The message text deliberately matches neither pi's overflow nor
|
|
434
449
|
// retryable-error patterns: a deterministic empty response should not burn
|
|
435
450
|
// auto-retries or trigger compaction — the user decides what to do next.
|
|
436
|
-
pi.on("message_end", (event, ctx) => {
|
|
451
|
+
pi.on("message_end", async (event, ctx) => {
|
|
437
452
|
const msg = event.message;
|
|
438
453
|
if (msg.role !== "assistant")
|
|
439
454
|
return;
|
|
@@ -448,6 +463,71 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
448
463
|
// post-replacement message, and dropping the marker would defeat the very
|
|
449
464
|
// recovery this error exists to trigger.
|
|
450
465
|
if (msg.stopReason === "error" && msg.errorMessage) {
|
|
466
|
+
// YAG-500: a 401 from the model proxy means the session token expired
|
|
467
|
+
// (or was revoked). Unlike tool 401s (handled by makeAuthedFetch), the
|
|
468
|
+
// model completion path has no 401-retry seam — pi's retryProviderRequest
|
|
469
|
+
// treats 401 as non-retryable, and after_provider_response never fires
|
|
470
|
+
// (the SDK throws before onResponse is reached). So message_end is the
|
|
471
|
+
// only place to detect it and trigger recovery. The regex matches
|
|
472
|
+
// "yagni login" (the backend's auth-error message) but NOT
|
|
473
|
+
// "request_too_large" (YAG-460's overflow marker).
|
|
474
|
+
const isAuthError = /yagni login/i.test(msg.errorMessage)
|
|
475
|
+
&& !/request_too_large|context_too_large/i.test(msg.errorMessage);
|
|
476
|
+
if (isAuthError) {
|
|
477
|
+
lastAuthRecovery = null;
|
|
478
|
+
let rotated = false;
|
|
479
|
+
try {
|
|
480
|
+
rotated = await tokenProvider.refresh();
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
rotated = false;
|
|
484
|
+
}
|
|
485
|
+
lastAuthRecovery = rotated ? "refreshed" : "failed";
|
|
486
|
+
const explanation = rotated
|
|
487
|
+
? "Your session token expired but was refreshed automatically. Re-send your prompt to continue."
|
|
488
|
+
: "Your session token expired and could not be refreshed. Run `yagni login`, then re-send your prompt. If the issue persists, restart YAGNI Code.";
|
|
489
|
+
if (ctx.hasUI) {
|
|
490
|
+
try {
|
|
491
|
+
ctx.ui.notify(explanation, rotated ? "info" : "error");
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// Surfacing the problem must never break the session itself.
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// YAG-500 Fix E: fire a non-fatal crash report so auth failures are
|
|
498
|
+
// visible in Sentry/backend logs. Reuses the crash endpoint with a
|
|
499
|
+
// distinct context. Fire-and-forget, fail-soft. The crash endpoint is
|
|
500
|
+
// now public (no token required), so the report lands even when the
|
|
501
|
+
// session token is expired and refresh failed — the most critical
|
|
502
|
+
// failure signal is no longer silently dropped.
|
|
503
|
+
void authReporter(new Error(`auth_401 on model path; refresh=${rotated ? "succeeded" : "failed"}`), "auth-failure").catch(() => { });
|
|
504
|
+
// YAG-500 Fix F: local diagnostics log under YAGNI_DEBUG.
|
|
505
|
+
if (isDebug(env)) {
|
|
506
|
+
try {
|
|
507
|
+
const logPath = join(codeStateHome(null, env), "logs", "auth-events.log");
|
|
508
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
509
|
+
appendFileSync(logPath, JSON.stringify({
|
|
510
|
+
ts: new Date().toISOString(),
|
|
511
|
+
status: 401,
|
|
512
|
+
refresh: rotated ? "succeeded" : "failed",
|
|
513
|
+
}) + "\n", "utf8");
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// A diagnostic must never break the session.
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { message: { ...msg, errorMessage: explanation } };
|
|
520
|
+
}
|
|
521
|
+
// YAG-460: the backend proxy answers an oversized conversation with an
|
|
522
|
+
// OpenAI-format 413 whose type is `request_too_large`. pi's own overflow
|
|
523
|
+
// detection matches that marker and runs full recovery — compact, then
|
|
524
|
+
// auto-retry the failed turn — so this branch must NOT call ctx.compact()
|
|
525
|
+
// (it would race the built-in recovery and lose the retry). Its only job
|
|
526
|
+
// is UX: replace the raw `413: {"error":{...}}` JSON with a readable
|
|
527
|
+
// message and tell the user what is happening. The rewritten text KEEPS
|
|
528
|
+
// the `request_too_large` marker verbatim: pi's _checkCompaction reads the
|
|
529
|
+
// post-replacement message, and dropping the marker would defeat the very
|
|
530
|
+
// recovery this error exists to trigger.
|
|
451
531
|
const isContextTooLarge = /request_too_large|context_too_large/i.test(msg.errorMessage);
|
|
452
532
|
if (!isContextTooLarge)
|
|
453
533
|
return;
|
|
@@ -504,7 +584,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
504
584
|
// context % on line 2, and extension statuses (brand, todos, mode) on
|
|
505
585
|
// line 3. The factory captures ctx so the footer can read session data
|
|
506
586
|
// (token stats, context usage) that isn't on the footerData provider.
|
|
507
|
-
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx)(tui, theme, footerData));
|
|
587
|
+
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder)(tui, theme, footerData));
|
|
508
588
|
}
|
|
509
589
|
// Best-effort, once at session start: if the token is at or near expiry, say
|
|
510
590
|
// so via a single notice so a long session does not silently start 401-ing
|
|
@@ -29,6 +29,17 @@
|
|
|
29
29
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import { type BlessStore } from "./bless.js";
|
|
31
31
|
export type PermissionMode = "auto" | "plan" | "review";
|
|
32
|
+
/**
|
|
33
|
+
* A shared, mutable holder for the current permission mode. Both
|
|
34
|
+
* `registerPermissionGate` (which writes on /mode) and the footer factory
|
|
35
|
+
* (which reads on every render) hold a reference, so mode changes appear
|
|
36
|
+
* immediately in the footer's line 2.
|
|
37
|
+
*/
|
|
38
|
+
export interface ModeHolder {
|
|
39
|
+
get(): PermissionMode;
|
|
40
|
+
set(m: PermissionMode): void;
|
|
41
|
+
}
|
|
42
|
+
export declare function createModeHolder(initial?: PermissionMode): ModeHolder;
|
|
32
43
|
/** Which tools each tier acts on, plus the optional grounding-bless predicate. */
|
|
33
44
|
export interface PermissionPolicy {
|
|
34
45
|
/** Tools blocked outright in plan mode (write/exec). */
|
|
@@ -76,6 +87,8 @@ export interface RegisterPermissionDeps {
|
|
|
76
87
|
* never blocks the approved tool call.
|
|
77
88
|
*/
|
|
78
89
|
onBlessRemember?: (ctx: ExtensionContext, info: BlessRememberInfo) => void | Promise<void>;
|
|
90
|
+
/** Shared holder so the footer can read the live mode on every render. */
|
|
91
|
+
modeHolder?: ModeHolder;
|
|
79
92
|
}
|
|
80
93
|
/** The customType tag on injected plan-mode context (filterable later). */
|
|
81
94
|
export declare const PLAN_CONTEXT_TYPE = "yagni-plan-context";
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
* the context so the model doesn't keep believing it is restricted.
|
|
28
28
|
*/
|
|
29
29
|
import { makeBlessStore as defaultMakeBlessStore } from "./bless.js";
|
|
30
|
+
export function createModeHolder(initial = "auto") {
|
|
31
|
+
let current = initial;
|
|
32
|
+
return {
|
|
33
|
+
get: () => current,
|
|
34
|
+
set: (m) => { current = m; },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
30
37
|
export const DEFAULT_PERMISSION_POLICY = {
|
|
31
38
|
planBlockTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status"],
|
|
32
39
|
reviewConfirmTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status"],
|
|
@@ -232,6 +239,7 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
232
239
|
return { messages: filtered };
|
|
233
240
|
});
|
|
234
241
|
const paintMode = (ctx) => {
|
|
242
|
+
deps.modeHolder?.set(mode);
|
|
235
243
|
try {
|
|
236
244
|
if (ctx.hasUI)
|
|
237
245
|
ctx.ui.setStatus?.("yagni-mode", MODE_STATUS[mode]);
|
|
@@ -130,7 +130,7 @@ export function makeTokenProvider(deps) {
|
|
|
130
130
|
});
|
|
131
131
|
}, delay);
|
|
132
132
|
}
|
|
133
|
-
function applyRotation(rotation) {
|
|
133
|
+
function applyRotation(rotation, skipPersist = false) {
|
|
134
134
|
token = rotation.token;
|
|
135
135
|
if (rotation.expiresAt)
|
|
136
136
|
expiresAt = rotation.expiresAt;
|
|
@@ -139,13 +139,45 @@ export function makeTokenProvider(deps) {
|
|
|
139
139
|
env.YAGNI_TOKEN = rotation.token;
|
|
140
140
|
if (rotation.expiresAt)
|
|
141
141
|
env.YAGNI_TOKEN_EXPIRES_AT = rotation.expiresAt;
|
|
142
|
+
if (!skipPersist) {
|
|
143
|
+
try {
|
|
144
|
+
persistProfile(rotation);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* fail-soft */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
armProactiveTimer();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Read the token from the launcher's profile file on disk. Used as a fallback
|
|
154
|
+
* when the server-side refresh fails (the old token is also invalid for
|
|
155
|
+
* /auth/refresh): an external `yagni login` writes a fresh token to the same
|
|
156
|
+
* file, so re-reading it can recover a session that the server refresh cannot.
|
|
157
|
+
* Returns null when the file is missing, unreadable, or carries the same token
|
|
158
|
+
* already in memory.
|
|
159
|
+
*/
|
|
160
|
+
function readTokenFromDisk() {
|
|
161
|
+
const profilePath = env.YAGNI_PROFILE_PATH?.trim();
|
|
162
|
+
if (!profilePath)
|
|
163
|
+
return null;
|
|
142
164
|
try {
|
|
143
|
-
|
|
165
|
+
const parsed = JSON.parse(readFileSync(profilePath, "utf8"));
|
|
166
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
167
|
+
return null;
|
|
168
|
+
const obj = parsed;
|
|
169
|
+
const diskToken = typeof obj.token === "string" ? obj.token : undefined;
|
|
170
|
+
if (!diskToken || diskToken === token)
|
|
171
|
+
return null;
|
|
172
|
+
return {
|
|
173
|
+
token: diskToken,
|
|
174
|
+
expiresAt: typeof obj.expiresAt === "string" ? obj.expiresAt : undefined,
|
|
175
|
+
workspaceId: typeof obj.workspaceId === "string" ? obj.workspaceId : undefined,
|
|
176
|
+
};
|
|
144
177
|
}
|
|
145
178
|
catch {
|
|
146
|
-
|
|
179
|
+
return null;
|
|
147
180
|
}
|
|
148
|
-
armProactiveTimer();
|
|
149
181
|
}
|
|
150
182
|
async function doRefresh() {
|
|
151
183
|
const current = token;
|
|
@@ -161,8 +193,17 @@ export function makeTokenProvider(deps) {
|
|
|
161
193
|
body: "{}",
|
|
162
194
|
signal: AbortSignal.timeout(REFRESH_REQUEST_TIMEOUT_MS),
|
|
163
195
|
});
|
|
164
|
-
if (!res.ok)
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
// Server refresh failed (the old token is also invalid for /auth/refresh).
|
|
198
|
+
// Fall back to the profile file on disk: an external `yagni login` may
|
|
199
|
+
// have written a fresh token there that this running session hasn't seen.
|
|
200
|
+
const disk = readTokenFromDisk();
|
|
201
|
+
if (disk) {
|
|
202
|
+
applyRotation(disk, true);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
165
205
|
return false;
|
|
206
|
+
}
|
|
166
207
|
const data = (await res.json());
|
|
167
208
|
if (!data || typeof data.token !== "string" || data.token.length === 0)
|
|
168
209
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.3.0-staging.
|
|
3
|
+
"version": "0.3.0-staging.1064.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"@earendil-works/pi-tui": "0.84.1",
|
|
39
39
|
"typebox": "^1.3.11"
|
|
40
40
|
},
|
|
41
|
-
"yagniSourceSha": "
|
|
41
|
+
"yagniSourceSha": "e38a99de02a31e45c61d006943113caf8817e0bd"
|
|
42
42
|
}
|