@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which surface this process is painting for. The desktop driver sets
|
|
3
|
+
* YAGNI_SURFACE=desktop; the terminal CLI sets nothing. The two renderers are
|
|
4
|
+
* mutually exclusive: a desktop session never receives ANSI, which is why the
|
|
5
|
+
* escape-code leak is fixed by construction rather than stripped downstream.
|
|
6
|
+
* Desktop-facing widgets are structured single-line JSON records the app
|
|
7
|
+
* parses, not themed terminal lines.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDesktopSurface(): boolean;
|
|
10
|
+
//# sourceMappingURL=surface.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which surface this process is painting for. The desktop driver sets
|
|
3
|
+
* YAGNI_SURFACE=desktop; the terminal CLI sets nothing. The two renderers are
|
|
4
|
+
* mutually exclusive: a desktop session never receives ANSI, which is why the
|
|
5
|
+
* escape-code leak is fixed by construction rather than stripped downstream.
|
|
6
|
+
* Desktop-facing widgets are structured single-line JSON records the app
|
|
7
|
+
* parses, not themed terminal lines.
|
|
8
|
+
*/
|
|
9
|
+
export function isDesktopSurface() {
|
|
10
|
+
return process.env.YAGNI_SURFACE === "desktop";
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=surface.js.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session todo list — the visible checklist for multi-step work.
|
|
3
|
+
*
|
|
4
|
+
* A `todo_write` tool the model calls with the FULL list every time (replace,
|
|
5
|
+
* not patch: replacement is idempotent under retries and always renders a
|
|
6
|
+
* coherent board), a persistent above-editor widget while steps remain open,
|
|
7
|
+
* and a `/todos` command to pull the list on demand.
|
|
8
|
+
*
|
|
9
|
+
* State follows pi's branching model the same way the session does: the
|
|
10
|
+
* canonical list is the LAST `todo_write` tool result on the current branch,
|
|
11
|
+
* reconstructed on session_start/session_tree, so forking or rewinding a
|
|
12
|
+
* session automatically shows the list as it stood at that point. The
|
|
13
|
+
* in-memory copy is just a cache of that.
|
|
14
|
+
*/
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Type } from "typebox";
|
|
17
|
+
export declare const TODO_TOOL_NAME = "todo_write";
|
|
18
|
+
export declare const MAX_TODOS = 50;
|
|
19
|
+
export declare const MAX_TODO_TEXT = 300;
|
|
20
|
+
/**
|
|
21
|
+
* The desktop's structured state record rides its own widget key, like the
|
|
22
|
+
* `/go` run state: one JSON line the app parses and renders itself, never
|
|
23
|
+
* themed terminal lines. The desktop shell hand-declares the matching parser
|
|
24
|
+
* (`parseTodoState` in its piProtocol.ts); a key-set assertion in
|
|
25
|
+
* todos.test.ts guards the two against drift.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TODO_STATE_KEY = "yagni-todos:state";
|
|
28
|
+
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
29
|
+
export interface TodoItem {
|
|
30
|
+
text: string;
|
|
31
|
+
status: TodoStatus;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate a full replacement list. Strict: this is model input rendered
|
|
35
|
+
* straight into the terminal. An empty list is valid (it clears the board).
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeTodos(raw: unknown): {
|
|
38
|
+
ok: true;
|
|
39
|
+
todos: TodoItem[];
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function todoSummary(todos: TodoItem[]): {
|
|
45
|
+
done: number;
|
|
46
|
+
total: number;
|
|
47
|
+
active: string | null;
|
|
48
|
+
};
|
|
49
|
+
/** Plain-text checklist (tool results, /todos in headless contexts). */
|
|
50
|
+
export declare function formatTodoList(todos: TodoItem[]): string;
|
|
51
|
+
/** The slice of pi's Theme the widget styles with (matches the feed's pattern). */
|
|
52
|
+
export interface TodoTheme {
|
|
53
|
+
fg(color: string, s: string): string;
|
|
54
|
+
strikethrough?(s: string): string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The above-editor checklist. Empty (paint nothing) when there is no list or
|
|
58
|
+
* every step is completed — a finished board should leave the screen. Capped
|
|
59
|
+
* so header + items + overflow stays under pi's 10-line widget truncation.
|
|
60
|
+
*/
|
|
61
|
+
export declare function renderTodoWidget(todos: TodoItem[], theme: TodoTheme): string[];
|
|
62
|
+
/** The desktop state record: exactly one JSON line under TODO_STATE_KEY. */
|
|
63
|
+
export declare function todoStateLine(todos: TodoItem[]): string;
|
|
64
|
+
/** Replay the branch: the last todo_write result is the canonical list. */
|
|
65
|
+
export declare function reconstructTodos(entries: unknown[]): TodoItem[];
|
|
66
|
+
type TodoParams = {
|
|
67
|
+
todos: Array<{
|
|
68
|
+
text: string;
|
|
69
|
+
status: string;
|
|
70
|
+
}>;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Build the todo_write tool definition around a shared store. Separated from
|
|
74
|
+
* registration so tests can drive execute directly.
|
|
75
|
+
*/
|
|
76
|
+
export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoItem[]) => void): {
|
|
77
|
+
name: string;
|
|
78
|
+
label: string;
|
|
79
|
+
description: string;
|
|
80
|
+
promptSnippet: string;
|
|
81
|
+
parameters: Type.TObject<{
|
|
82
|
+
todos: Type.TArray<Type.TObject<{
|
|
83
|
+
text: Type.TString;
|
|
84
|
+
status: Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"in_progress">, Type.TLiteral<"completed">]>;
|
|
85
|
+
}>>;
|
|
86
|
+
}>;
|
|
87
|
+
execute(_toolCallId: string, params: TodoParams, _signal?: AbortSignal, _onUpdate?: unknown, ctx?: ExtensionContext): Promise<{
|
|
88
|
+
content: {
|
|
89
|
+
type: "text";
|
|
90
|
+
text: string;
|
|
91
|
+
}[];
|
|
92
|
+
details: {
|
|
93
|
+
todos: TodoItem[];
|
|
94
|
+
};
|
|
95
|
+
isError: boolean;
|
|
96
|
+
} | {
|
|
97
|
+
content: {
|
|
98
|
+
type: "text";
|
|
99
|
+
text: string;
|
|
100
|
+
}[];
|
|
101
|
+
details: {
|
|
102
|
+
todos: TodoItem[];
|
|
103
|
+
};
|
|
104
|
+
isError?: undefined;
|
|
105
|
+
}>;
|
|
106
|
+
};
|
|
107
|
+
/** Wire the tool, the branch-replay events, and the /todos command. */
|
|
108
|
+
export declare function registerTodos(pi: ExtensionAPI): void;
|
|
109
|
+
export {};
|
|
110
|
+
//# sourceMappingURL=todos.d.ts.map
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session todo list — the visible checklist for multi-step work.
|
|
3
|
+
*
|
|
4
|
+
* A `todo_write` tool the model calls with the FULL list every time (replace,
|
|
5
|
+
* not patch: replacement is idempotent under retries and always renders a
|
|
6
|
+
* coherent board), a persistent above-editor widget while steps remain open,
|
|
7
|
+
* and a `/todos` command to pull the list on demand.
|
|
8
|
+
*
|
|
9
|
+
* State follows pi's branching model the same way the session does: the
|
|
10
|
+
* canonical list is the LAST `todo_write` tool result on the current branch,
|
|
11
|
+
* reconstructed on session_start/session_tree, so forking or rewinding a
|
|
12
|
+
* session automatically shows the list as it stood at that point. The
|
|
13
|
+
* in-memory copy is just a cache of that.
|
|
14
|
+
*/
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import { isDesktopSurface } from "./surface.js";
|
|
17
|
+
export const TODO_TOOL_NAME = "todo_write";
|
|
18
|
+
export const MAX_TODOS = 50;
|
|
19
|
+
export const MAX_TODO_TEXT = 300;
|
|
20
|
+
const WIDGET_KEY = "yagni-todos";
|
|
21
|
+
/**
|
|
22
|
+
* The desktop's structured state record rides its own widget key, like the
|
|
23
|
+
* `/go` run state: one JSON line the app parses and renders itself, never
|
|
24
|
+
* themed terminal lines. The desktop shell hand-declares the matching parser
|
|
25
|
+
* (`parseTodoState` in its piProtocol.ts); a key-set assertion in
|
|
26
|
+
* todos.test.ts guards the two against drift.
|
|
27
|
+
*/
|
|
28
|
+
export const TODO_STATE_KEY = "yagni-todos:state";
|
|
29
|
+
/** Header + items + overflow line must stay under pi's 10-line widget cap. */
|
|
30
|
+
const MAX_WIDGET_ITEMS = 7;
|
|
31
|
+
const STATUSES = ["pending", "in_progress", "completed"];
|
|
32
|
+
/**
|
|
33
|
+
* Validate a full replacement list. Strict: this is model input rendered
|
|
34
|
+
* straight into the terminal. An empty list is valid (it clears the board).
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeTodos(raw) {
|
|
37
|
+
if (!Array.isArray(raw))
|
|
38
|
+
return { ok: false, error: "todos must be an array" };
|
|
39
|
+
if (raw.length > MAX_TODOS)
|
|
40
|
+
return { ok: false, error: `todos is capped at ${MAX_TODOS} items` };
|
|
41
|
+
const todos = [];
|
|
42
|
+
for (const item of raw) {
|
|
43
|
+
if (!item || typeof item !== "object")
|
|
44
|
+
return { ok: false, error: "each todo must be an object" };
|
|
45
|
+
const { text, status } = item;
|
|
46
|
+
if (typeof text !== "string" || text.trim().length === 0) {
|
|
47
|
+
return { ok: false, error: "each todo needs non-empty text" };
|
|
48
|
+
}
|
|
49
|
+
if (typeof status !== "string" || !STATUSES.includes(status)) {
|
|
50
|
+
return { ok: false, error: `status must be one of ${STATUSES.join(", ")}` };
|
|
51
|
+
}
|
|
52
|
+
todos.push({ text: text.trim().slice(0, MAX_TODO_TEXT), status: status });
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, todos };
|
|
55
|
+
}
|
|
56
|
+
export function todoSummary(todos) {
|
|
57
|
+
return {
|
|
58
|
+
done: todos.filter((t) => t.status === "completed").length,
|
|
59
|
+
total: todos.length,
|
|
60
|
+
active: todos.find((t) => t.status === "in_progress")?.text ?? null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const GLYPH = { pending: "[ ]", in_progress: "[>]", completed: "[x]" };
|
|
64
|
+
/** Plain-text checklist (tool results, /todos in headless contexts). */
|
|
65
|
+
export function formatTodoList(todos) {
|
|
66
|
+
if (todos.length === 0)
|
|
67
|
+
return "No todos.";
|
|
68
|
+
const { done, total } = todoSummary(todos);
|
|
69
|
+
const lines = todos.map((t) => `${GLYPH[t.status]} ${t.text}`);
|
|
70
|
+
return [`${done}/${total} done`, ...lines].join("\n");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The above-editor checklist. Empty (paint nothing) when there is no list or
|
|
74
|
+
* every step is completed — a finished board should leave the screen. Capped
|
|
75
|
+
* so header + items + overflow stays under pi's 10-line widget truncation.
|
|
76
|
+
*/
|
|
77
|
+
export function renderTodoWidget(todos, theme) {
|
|
78
|
+
const { done, total } = todoSummary(todos);
|
|
79
|
+
if (total === 0 || done === total)
|
|
80
|
+
return [];
|
|
81
|
+
const lines = [theme.fg("muted", `Todos ${done}/${total}`)];
|
|
82
|
+
const visible = todos.slice(0, MAX_WIDGET_ITEMS);
|
|
83
|
+
for (const todo of visible) {
|
|
84
|
+
if (todo.status === "completed") {
|
|
85
|
+
const text = theme.strikethrough ? theme.strikethrough(todo.text) : todo.text;
|
|
86
|
+
lines.push(`${theme.fg("success", "☑ ")}${theme.fg("dim", text)}`);
|
|
87
|
+
}
|
|
88
|
+
else if (todo.status === "in_progress") {
|
|
89
|
+
lines.push(`${theme.fg("accent", "▶ ")}${theme.fg("text", todo.text)}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lines.push(`${theme.fg("dim", "☐ ")}${theme.fg("muted", todo.text)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (todos.length > visible.length) {
|
|
96
|
+
lines.push(theme.fg("dim", `…and ${todos.length - visible.length} more`));
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
/** The desktop state record: exactly one JSON line under TODO_STATE_KEY. */
|
|
101
|
+
export function todoStateLine(todos) {
|
|
102
|
+
return JSON.stringify({ v: 1, todos });
|
|
103
|
+
}
|
|
104
|
+
/** Replay the branch: the last todo_write result is the canonical list. */
|
|
105
|
+
export function reconstructTodos(entries) {
|
|
106
|
+
let todos = [];
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const e = entry;
|
|
109
|
+
if (e?.type !== "message")
|
|
110
|
+
continue;
|
|
111
|
+
const msg = e.message;
|
|
112
|
+
if (msg?.role !== "toolResult" || msg.toolName !== TODO_TOOL_NAME)
|
|
113
|
+
continue;
|
|
114
|
+
const details = msg.details;
|
|
115
|
+
const normalized = normalizeTodos(details?.todos);
|
|
116
|
+
if (normalized.ok)
|
|
117
|
+
todos = normalized.todos;
|
|
118
|
+
}
|
|
119
|
+
return todos;
|
|
120
|
+
}
|
|
121
|
+
const parameters = Type.Object({
|
|
122
|
+
todos: Type.Array(Type.Object({
|
|
123
|
+
text: Type.String({ description: "The step, short and imperative." }),
|
|
124
|
+
status: Type.Union([
|
|
125
|
+
Type.Literal("pending"),
|
|
126
|
+
Type.Literal("in_progress"),
|
|
127
|
+
Type.Literal("completed"),
|
|
128
|
+
]),
|
|
129
|
+
}), { description: "The FULL list; it replaces the previous one." }),
|
|
130
|
+
});
|
|
131
|
+
function paintWidget(ctx, todos) {
|
|
132
|
+
if (!ctx?.hasUI)
|
|
133
|
+
return;
|
|
134
|
+
try {
|
|
135
|
+
if (isDesktopSurface()) {
|
|
136
|
+
// The desktop renders the checklist itself, so it gets the structured
|
|
137
|
+
// record instead of themed lines (RPC mode forwards widgetLines
|
|
138
|
+
// verbatim, and ANSI would land in the DOM). Painted on EVERY change —
|
|
139
|
+
// including an empty or finished list — so the app can clear its board,
|
|
140
|
+
// and on branch replay so resume/fork restores the list. The "todos
|
|
141
|
+
// n/m" status chip would be redundant next to the app's own rendering.
|
|
142
|
+
ctx.ui.setWidget?.(TODO_STATE_KEY, [todoStateLine(todos)], { placement: "aboveEditor" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const theme = ctx.ui.theme;
|
|
146
|
+
const lines = renderTodoWidget(todos, theme);
|
|
147
|
+
ctx.ui.setWidget?.(WIDGET_KEY, lines.length > 0 ? lines : undefined, {
|
|
148
|
+
placement: "aboveEditor",
|
|
149
|
+
});
|
|
150
|
+
const { done, total } = todoSummary(todos);
|
|
151
|
+
ctx.ui.setStatus?.(WIDGET_KEY, total > 0 && done < total ? `todos ${done}/${total}` : undefined);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Painting is chrome; it must never break the tool call.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build the todo_write tool definition around a shared store. Separated from
|
|
159
|
+
* registration so tests can drive execute directly.
|
|
160
|
+
*/
|
|
161
|
+
export function makeTodoTool(get, set) {
|
|
162
|
+
return {
|
|
163
|
+
name: TODO_TOOL_NAME,
|
|
164
|
+
label: "Todos",
|
|
165
|
+
description: "Track your plan for multi-step work as a checklist the user can see. " +
|
|
166
|
+
"Pass the FULL list every time — it replaces the previous list. Keep exactly one item " +
|
|
167
|
+
"in_progress at a time, mark items completed the moment they are done, and add newly " +
|
|
168
|
+
"discovered steps as pending. Use it for any task with three or more steps, updating as you go.",
|
|
169
|
+
promptSnippet: "todo_write: keep a user-visible checklist for multi-step work (full-list replacement).",
|
|
170
|
+
parameters,
|
|
171
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
172
|
+
const normalized = normalizeTodos(params.todos);
|
|
173
|
+
if (!normalized.ok) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: `Error: ${normalized.error}` }],
|
|
176
|
+
details: { todos: get() },
|
|
177
|
+
isError: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
set(normalized.todos);
|
|
181
|
+
paintWidget(ctx, normalized.todos);
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: formatTodoList(normalized.todos) }],
|
|
184
|
+
details: { todos: normalized.todos },
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Wire the tool, the branch-replay events, and the /todos command. */
|
|
190
|
+
export function registerTodos(pi) {
|
|
191
|
+
let todos = [];
|
|
192
|
+
const reconstruct = (ctx) => {
|
|
193
|
+
try {
|
|
194
|
+
todos = reconstructTodos(ctx.sessionManager.getBranch());
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
todos = [];
|
|
198
|
+
}
|
|
199
|
+
paintWidget(ctx, todos);
|
|
200
|
+
};
|
|
201
|
+
pi.on("session_start", async (_event, ctx) => reconstruct(ctx));
|
|
202
|
+
pi.on("session_tree", async (_event, ctx) => reconstruct(ctx));
|
|
203
|
+
pi.registerTool(makeTodoTool(() => todos, (next) => {
|
|
204
|
+
todos = next;
|
|
205
|
+
}));
|
|
206
|
+
pi.registerCommand("todos", {
|
|
207
|
+
description: "Show the agent's current task list for this session.",
|
|
208
|
+
handler: async (_args, ctx) => {
|
|
209
|
+
if (!ctx.hasUI)
|
|
210
|
+
return;
|
|
211
|
+
ctx.ui.notify(todos.length === 0
|
|
212
|
+
? "No todos. The agent keeps a checklist here when a task takes multiple steps."
|
|
213
|
+
: formatTodoList(todos), "info");
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=todos.js.map
|
|
@@ -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
|