@yagni-app/code-staging 0.0.0 → 0.1.0-staging.1002.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.
Files changed (147) hide show
  1. package/LICENSE.md +55 -0
  2. package/README.md +166 -0
  3. package/dist/branding.d.ts +25 -0
  4. package/dist/branding.js +27 -0
  5. package/dist/claudeCompat.d.ts +78 -0
  6. package/dist/claudeCompat.js +198 -0
  7. package/dist/cli.d.ts +55 -0
  8. package/dist/cli.js +326 -0
  9. package/dist/config.d.ts +19 -0
  10. package/dist/config.js +28 -0
  11. package/dist/credentials.d.ts +41 -0
  12. package/dist/credentials.js +75 -0
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +94 -0
  16. package/dist/doctor.js +343 -0
  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 +188 -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 +74 -0
  25. package/dist/extension/branding.d.ts +48 -0
  26. package/dist/extension/branding.js +119 -0
  27. package/dist/extension/chipEditor.d.ts +117 -0
  28. package/dist/extension/chipEditor.js +369 -0
  29. package/dist/extension/config.d.ts +112 -0
  30. package/dist/extension/config.js +108 -0
  31. package/dist/extension/costHud.d.ts +73 -0
  32. package/dist/extension/costHud.js +123 -0
  33. package/dist/extension/decisionCapture.d.ts +52 -0
  34. package/dist/extension/decisionCapture.js +66 -0
  35. package/dist/extension/decisions.d.ts +83 -0
  36. package/dist/extension/decisions.js +200 -0
  37. package/dist/extension/diagnostics.d.ts +41 -0
  38. package/dist/extension/diagnostics.js +110 -0
  39. package/dist/extension/index.d.ts +116 -0
  40. package/dist/extension/index.js +388 -0
  41. package/dist/extension/initDone.d.ts +28 -0
  42. package/dist/extension/initDone.js +66 -0
  43. package/dist/extension/initPass.d.ts +152 -0
  44. package/dist/extension/initPass.js +394 -0
  45. package/dist/extension/nextWorkTool.d.ts +51 -0
  46. package/dist/extension/nextWorkTool.js +80 -0
  47. package/dist/extension/permission.d.ts +91 -0
  48. package/dist/extension/permission.js +236 -0
  49. package/dist/extension/pipeline/activity.d.ts +37 -0
  50. package/dist/extension/pipeline/activity.js +151 -0
  51. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  52. package/dist/extension/pipeline/activityFeed.js +175 -0
  53. package/dist/extension/pipeline/budget.d.ts +48 -0
  54. package/dist/extension/pipeline/budget.js +68 -0
  55. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  56. package/dist/extension/pipeline/checkpoint.js +176 -0
  57. package/dist/extension/pipeline/eval.d.ts +168 -0
  58. package/dist/extension/pipeline/eval.js +182 -0
  59. package/dist/extension/pipeline/events.d.ts +56 -0
  60. package/dist/extension/pipeline/events.js +147 -0
  61. package/dist/extension/pipeline/findings.d.ts +42 -0
  62. package/dist/extension/pipeline/findings.js +144 -0
  63. package/dist/extension/pipeline/finish.d.ts +128 -0
  64. package/dist/extension/pipeline/finish.js +307 -0
  65. package/dist/extension/pipeline/goCommand.d.ts +128 -0
  66. package/dist/extension/pipeline/goCommand.js +972 -0
  67. package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
  68. package/dist/extension/pipeline/goCompareCommand.js +184 -0
  69. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  70. package/dist/extension/pipeline/goFlags.js +46 -0
  71. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  72. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  73. package/dist/extension/pipeline/invocation.d.ts +45 -0
  74. package/dist/extension/pipeline/invocation.js +64 -0
  75. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  76. package/dist/extension/pipeline/orchestrator.js +636 -0
  77. package/dist/extension/pipeline/personas.d.ts +44 -0
  78. package/dist/extension/pipeline/personas.js +248 -0
  79. package/dist/extension/pipeline/resilience.d.ts +85 -0
  80. package/dist/extension/pipeline/resilience.js +166 -0
  81. package/dist/extension/pipeline/resume.d.ts +18 -0
  82. package/dist/extension/pipeline/resume.js +106 -0
  83. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  84. package/dist/extension/pipeline/runRegistry.js +202 -0
  85. package/dist/extension/pipeline/runSession.d.ts +152 -0
  86. package/dist/extension/pipeline/runSession.js +167 -0
  87. package/dist/extension/pipeline/runState.d.ts +158 -0
  88. package/dist/extension/pipeline/runState.js +264 -0
  89. package/dist/extension/pipeline/runner.d.ts +79 -0
  90. package/dist/extension/pipeline/runner.js +298 -0
  91. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  92. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  93. package/dist/extension/pipeline/stages.d.ts +51 -0
  94. package/dist/extension/pipeline/stages.js +113 -0
  95. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  96. package/dist/extension/pipeline/ticketResolution.js +75 -0
  97. package/dist/extension/pipeline/types.d.ts +436 -0
  98. package/dist/extension/pipeline/types.js +48 -0
  99. package/dist/extension/pipeline/verify.d.ts +277 -0
  100. package/dist/extension/pipeline/verify.js +758 -0
  101. package/dist/extension/pipeline/workspace.d.ts +32 -0
  102. package/dist/extension/pipeline/workspace.js +73 -0
  103. package/dist/extension/pipeline/worktree.d.ts +107 -0
  104. package/dist/extension/pipeline/worktree.js +200 -0
  105. package/dist/extension/provider.d.ts +11 -0
  106. package/dist/extension/provider.js +26 -0
  107. package/dist/extension/recall.d.ts +93 -0
  108. package/dist/extension/recall.js +187 -0
  109. package/dist/extension/recordContextTool.d.ts +38 -0
  110. package/dist/extension/recordContextTool.js +85 -0
  111. package/dist/extension/recordDecisionTool.d.ts +52 -0
  112. package/dist/extension/recordDecisionTool.js +102 -0
  113. package/dist/extension/repoDocs.d.ts +81 -0
  114. package/dist/extension/repoDocs.js +260 -0
  115. package/dist/extension/resilientFetch.d.ts +60 -0
  116. package/dist/extension/resilientFetch.js +133 -0
  117. package/dist/extension/reviewTool.d.ts +34 -0
  118. package/dist/extension/reviewTool.js +81 -0
  119. package/dist/extension/spool.d.ts +92 -0
  120. package/dist/extension/spool.js +266 -0
  121. package/dist/extension/stateHome.d.ts +2 -0
  122. package/dist/extension/stateHome.js +6 -0
  123. package/dist/extension/subagents.d.ts +135 -0
  124. package/dist/extension/subagents.js +281 -0
  125. package/dist/extension/surface.d.ts +10 -0
  126. package/dist/extension/surface.js +12 -0
  127. package/dist/extension/todos.d.ts +110 -0
  128. package/dist/extension/todos.js +217 -0
  129. package/dist/extension/tokenProvider.d.ts +93 -0
  130. package/dist/extension/tokenProvider.js +234 -0
  131. package/dist/launch.d.ts +76 -0
  132. package/dist/launch.js +111 -0
  133. package/dist/login.d.ts +45 -0
  134. package/dist/login.js +142 -0
  135. package/dist/logout.d.ts +14 -0
  136. package/dist/logout.js +34 -0
  137. package/dist/paths.d.ts +31 -0
  138. package/dist/paths.js +87 -0
  139. package/dist/piPackage.d.ts +33 -0
  140. package/dist/piPackage.js +71 -0
  141. package/dist/profiles.d.ts +80 -0
  142. package/dist/profiles.js +222 -0
  143. package/dist/refresh.d.ts +70 -0
  144. package/dist/refresh.js +117 -0
  145. package/dist/upgrade.d.ts +97 -0
  146. package/dist/upgrade.js +284 -0
  147. package/package.json +39 -3
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Chip-editor for image paste.
3
+ *
4
+ * Goal: pasting a screenshot into the `yagni` prompt should drop an inline
5
+ * `[Image #N]` chip at the cursor (like Claude Code), not a raw temp-file path.
6
+ *
7
+ * pi already implements cross-platform clipboard-image paste, but its default
8
+ * editor writes the image to a temp file and inserts the *path* as literal
9
+ * text. This module replaces the editor (via `ctx.ui.setEditorComponent`) with
10
+ * a `CustomEditor` subclass that owns the paste handler, stashes the image, and
11
+ * inserts a styled `[Image #N]` token as the text source-of-truth. An `input`
12
+ * transform (registered alongside) parses those tokens back into real
13
+ * `ImageContent[]` attachments so the image reaches the model.
14
+ *
15
+ * No fork, no patch: both the editor override and the input transform are
16
+ * first-class pi extension surfaces. The chip is a *styled text token* — the
17
+ * literal text is the source of truth, so undo/cursor/history and the
18
+ * chip→model mapping all share one string and can't desync.
19
+ *
20
+ * Validated against pi 0.83.0 (spike, 5/5): the subclass owns `onPasteImage`
21
+ * because `setCustomEditorComponent` only fills it when unset, and jiti aliases
22
+ * the extension's pi imports to the same module instance pi uses.
23
+ */
24
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
+ import { CustomEditor } from "@earendil-works/pi-coding-agent";
26
+ import type { EditorOptions, EditorTheme, TUI } from "@earendil-works/pi-tui";
27
+ export interface ClipboardImage {
28
+ bytes: Uint8Array;
29
+ mimeType: string;
30
+ }
31
+ /** Injectable clipboard-image reader seam (tests stub it; prod reads the OS). */
32
+ export type ClipboardImageReader = () => Promise<ClipboardImage | null>;
33
+ interface StashedImage {
34
+ n: number;
35
+ bytes: Uint8Array;
36
+ mimeType: string;
37
+ }
38
+ /** The attachment shape pi's `input` transform forwards to the model. */
39
+ export interface ImageAttachment {
40
+ type: "image";
41
+ data: string;
42
+ mimeType: string;
43
+ }
44
+ /**
45
+ * If `data` is a complete bracketed-paste payload, return its inner content;
46
+ * otherwise null. pi's Terminal re-wraps every paste with these markers before
47
+ * the editor sees it, so a whole-payload match is the unambiguous paste signal.
48
+ */
49
+ export declare function unwrapBracketedPaste(data: string): string | null;
50
+ /**
51
+ * Recognize a pasted temp-image path written by a Ghostty-based terminal
52
+ * (cmux, Ghostty). Those terminals name the file `clipboard-<ts>-<id>.<ext>`
53
+ * in the system temp dir. We match strictly — clipboard- prefix, image
54
+ * extension, real file, and genuine image magic bytes — so a path the user
55
+ * typed by hand (e.g. /Users/me/photo.png) is never silently converted.
56
+ * Returns the decoded image, or null when the pasted text is not one such path.
57
+ */
58
+ export declare function readPastedImagePath(pastedText: string): ClipboardImage | null;
59
+ /**
60
+ * Read an image from the system clipboard without forking pi. Best-effort and
61
+ * cross-platform: native module if present, else osascript/pngpaste (macOS),
62
+ * wl-paste (Wayland), xclip (X11), PowerShell (Windows/WSL). Returns null when
63
+ * the clipboard holds no image (caller then pastes text instead).
64
+ */
65
+ export declare const defaultClipboardImageReader: ClipboardImageReader;
66
+ /**
67
+ * A CustomEditor that turns clipboard image paste into numbered `[Image #N]`
68
+ * chips. The image bytes live here, keyed by chip number; the editor text holds
69
+ * only the token, which is the durable, undo-friendly source of truth.
70
+ */
71
+ export declare class ChipEditor extends CustomEditor {
72
+ private stashed;
73
+ private chipCounter;
74
+ private readImage;
75
+ constructor(tui: TUI, theme: EditorTheme, keybindings: ConstructorParameters<typeof CustomEditor>[2], readImage?: ClipboardImageReader, options?: EditorOptions);
76
+ /**
77
+ * Paste-image must fire on Cmd+V too (Claude Code parity on macOS). Claude
78
+ * Code's own approach is iTerm2-specific — "Cmd+V ... maps to Ctrl+V" — i.e.
79
+ * it relies on the terminal delivering Cmd+V AS Ctrl+V. We do the same: match
80
+ * ctrl+v + alt+v (win32/WSL) explicitly, and super+v only as a bonus for
81
+ * Kitty-protocol terminals that genuinely deliver the Cmd modifier.
82
+ */
83
+ handleInput(data: string): void;
84
+ private pasteChip;
85
+ private dropChip;
86
+ /** Every image stashed on this editor (used by the input transform). */
87
+ allStashed(): StashedImage[];
88
+ /** Drop all stashed images. Called after a successful submit so a sent image
89
+ * is not re-attached to the next message. */
90
+ clearStash(): void;
91
+ /** Re-style the chip tokens so they read as chips; layout is untouched. */
92
+ render(width: number): string[];
93
+ }
94
+ /**
95
+ * Build the `input` transform handler: parse `[Image #N]` markers in the
96
+ * submitted text into `ImageAttachment[]` for the model. Unknown/removed chip
97
+ * numbers are ignored (the image was deleted from the prompt), and a prompt
98
+ * with no live chips passes through untouched.
99
+ */
100
+ export declare function makeChipInputTransform(getImages: () => StashedImage[]): (event: {
101
+ text: string;
102
+ images?: unknown;
103
+ }) => {
104
+ action: "transform";
105
+ text: string;
106
+ images: ImageAttachment[];
107
+ } | {
108
+ action: "continue";
109
+ };
110
+ /**
111
+ * Register the chip editor + the input transform on a session. Called on
112
+ * `session_start` in TUI mode. The factory registers each constructed editor in
113
+ * the shared registry; the transform reads the union of live editors' stashes.
114
+ */
115
+ export declare function registerChipEditor(pi: ExtensionAPI): void;
116
+ export {};
117
+ //# sourceMappingURL=chipEditor.d.ts.map
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Chip-editor for image paste.
3
+ *
4
+ * Goal: pasting a screenshot into the `yagni` prompt should drop an inline
5
+ * `[Image #N]` chip at the cursor (like Claude Code), not a raw temp-file path.
6
+ *
7
+ * pi already implements cross-platform clipboard-image paste, but its default
8
+ * editor writes the image to a temp file and inserts the *path* as literal
9
+ * text. This module replaces the editor (via `ctx.ui.setEditorComponent`) with
10
+ * a `CustomEditor` subclass that owns the paste handler, stashes the image, and
11
+ * inserts a styled `[Image #N]` token as the text source-of-truth. An `input`
12
+ * transform (registered alongside) parses those tokens back into real
13
+ * `ImageContent[]` attachments so the image reaches the model.
14
+ *
15
+ * No fork, no patch: both the editor override and the input transform are
16
+ * first-class pi extension surfaces. The chip is a *styled text token* — the
17
+ * literal text is the source of truth, so undo/cursor/history and the
18
+ * chip→model mapping all share one string and can't desync.
19
+ *
20
+ * Validated against pi 0.83.0 (spike, 5/5): the subclass owns `onPasteImage`
21
+ * because `setCustomEditorComponent` only fills it when unset, and jiti aliases
22
+ * the extension's pi imports to the same module instance pi uses.
23
+ */
24
+ import { CustomEditor } from "@earendil-works/pi-coding-agent";
25
+ import { matchesKey } from "@earendil-works/pi-tui";
26
+ import { spawnSync } from "node:child_process";
27
+ import { readFileSync, unlinkSync, existsSync } from "node:fs";
28
+ import { tmpdir } from "node:os";
29
+ import { join, basename } from "node:path";
30
+ import { randomUUID } from "node:crypto";
31
+ import { logImagePaste } from "./diagnostics.js";
32
+ /** Matches the `[Image #N]` chip token in the editor text. */
33
+ const CHIP_RE = /\[Image #(\d+)\]/g;
34
+ /** Inverse-video wrap so the chip token reads as a chip, not plain text. */
35
+ const CHIP_ON = "";
36
+ const CHIP_OFF = "";
37
+ const PASTE_START = "\x1b[200~";
38
+ const PASTE_END = "\x1b[201~";
39
+ /**
40
+ * If `data` is a complete bracketed-paste payload, return its inner content;
41
+ * otherwise null. pi's Terminal re-wraps every paste with these markers before
42
+ * the editor sees it, so a whole-payload match is the unambiguous paste signal.
43
+ */
44
+ export function unwrapBracketedPaste(data) {
45
+ if (!data.startsWith(PASTE_START) || !data.endsWith(PASTE_END))
46
+ return null;
47
+ return data.slice(PASTE_START.length, data.length - PASTE_END.length);
48
+ }
49
+ const IMAGE_EXT_MIME = {
50
+ png: "image/png",
51
+ jpg: "image/jpeg",
52
+ jpeg: "image/jpeg",
53
+ gif: "image/gif",
54
+ webp: "image/webp",
55
+ bmp: "image/bmp",
56
+ tiff: "image/tiff",
57
+ tif: "image/tiff",
58
+ };
59
+ // PNG/JPEG/GIF/WEBP magic bytes, so we only convert a path that is genuinely a
60
+ // decodable image — never a same-named text file or a path the user typed.
61
+ const IMAGE_MAGIC = [
62
+ [[0x89, 0x50, 0x4e, 0x47], "image/png"],
63
+ [[0xff, 0xd8, 0xff], "image/jpeg"],
64
+ [[0x47, 0x49, 0x46, 0x38], "image/gif"], // GIF8
65
+ [[0x52, 0x49, 0x46, 0x46], "image/webp"], // RIFF (webp container)
66
+ ];
67
+ /**
68
+ * Recognize a pasted temp-image path written by a Ghostty-based terminal
69
+ * (cmux, Ghostty). Those terminals name the file `clipboard-<ts>-<id>.<ext>`
70
+ * in the system temp dir. We match strictly — clipboard- prefix, image
71
+ * extension, real file, and genuine image magic bytes — so a path the user
72
+ * typed by hand (e.g. /Users/me/photo.png) is never silently converted.
73
+ * Returns the decoded image, or null when the pasted text is not one such path.
74
+ */
75
+ export function readPastedImagePath(pastedText) {
76
+ const trimmed = pastedText.trim();
77
+ if (trimmed.includes("\n") || trimmed.includes(" "))
78
+ return null; // one token only
79
+ // Validate the RAW input first: an attacker could smuggle the `clipboard-`
80
+ // prefix through backslash escapes (e.g. `\c\l\i\p\b\o\a\r\d-`), so we
81
+ // must confirm the un-escaped form is a well-formed tmp image path before any
82
+ // unescaping or path resolution. cmux tmp paths contain no escapable chars
83
+ // (no spaces/metacharacters), so a legitimate paste has NO backslashes at all.
84
+ if (trimmed.includes("\\"))
85
+ return null; // escapes => not a plain cmux tmp path
86
+ const path = trimmed;
87
+ if (!path.startsWith("/"))
88
+ return null;
89
+ const name = basename(path);
90
+ if (!name.startsWith("clipboard-"))
91
+ return null;
92
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
93
+ if (!(ext in IMAGE_EXT_MIME))
94
+ return null;
95
+ if (!existsSync(path))
96
+ return null;
97
+ let bytes;
98
+ try {
99
+ bytes = readFileSync(path);
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ if (bytes.length === 0)
105
+ return null;
106
+ for (const [magic, mime] of IMAGE_MAGIC) {
107
+ if (magic.every((b, i) => bytes[i] === b))
108
+ return { bytes, mimeType: mime };
109
+ }
110
+ return null;
111
+ }
112
+ /**
113
+ * Read an image from the system clipboard without forking pi. Best-effort and
114
+ * cross-platform: native module if present, else osascript/pngpaste (macOS),
115
+ * wl-paste (Wayland), xclip (X11), PowerShell (Windows/WSL). Returns null when
116
+ * the clipboard holds no image (caller then pastes text instead).
117
+ */
118
+ export const defaultClipboardImageReader = async () => {
119
+ const platform = process.platform;
120
+ try {
121
+ if (platform === "darwin") {
122
+ return readMacClipboardImage();
123
+ }
124
+ if (platform === "linux") {
125
+ return readLinuxClipboardImage();
126
+ }
127
+ if (platform === "win32") {
128
+ return readWindowsClipboardImage();
129
+ }
130
+ }
131
+ catch {
132
+ // Fall through — a clipboard read failure must never break the prompt.
133
+ }
134
+ return null;
135
+ };
136
+ function runBase64(command, args, timeoutMs = 3000) {
137
+ const res = spawnSync(command, args, { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
138
+ if (res.error || res.status !== 0)
139
+ return null;
140
+ const out = Buffer.isBuffer(res.stdout) ? res.stdout : Buffer.from(res.stdout ?? "");
141
+ return out.length > 0 ? out : null;
142
+ }
143
+ function readMacClipboardImage() {
144
+ // pngpaste writes the clipboard image to a PNG file (cleanest on macOS).
145
+ const dest = join(tmpdir(), `yagni-clip-${randomUUID()}.png`);
146
+ try {
147
+ const res = spawnSync("pngpaste", [dest], { timeout: 3000 });
148
+ if (res.error || res.status !== 0)
149
+ return null;
150
+ const bytes = readFileSync(dest);
151
+ return bytes.length > 0 ? { bytes, mimeType: "image/png" } : null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ finally {
157
+ try {
158
+ unlinkSync(dest);
159
+ }
160
+ catch {
161
+ /* best effort */
162
+ }
163
+ }
164
+ }
165
+ function readLinuxClipboardImage() {
166
+ // Wayland: list types, prefer a supported image type, then read it.
167
+ const types = runBase64("wl-paste", ["--list-types"], 1000);
168
+ if (types) {
169
+ const list = types.toString("utf8").split(/\r?\n/).map((t) => t.trim());
170
+ const preferred = ["image/png", "image/jpeg", "image/webp", "image/gif"].find((t) => list.includes(t));
171
+ if (preferred) {
172
+ const data = runBase64("wl-paste", ["--type", preferred, "--no-newline"]);
173
+ if (data)
174
+ return { bytes: data, mimeType: preferred };
175
+ }
176
+ }
177
+ // X11 fallback.
178
+ const xclip = runBase64("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]);
179
+ return xclip ? { bytes: xclip, mimeType: "image/png" } : null;
180
+ }
181
+ function readWindowsClipboardImage() {
182
+ // PowerShell: get the clipboard image and emit it as base64 PNG.
183
+ const script = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing;" +
184
+ "$i=[System.Windows.Forms.Clipboard]::GetImage();" +
185
+ "if($i -eq $null){exit 1};" +
186
+ "$ms=New-Object System.IO.MemoryStream;" +
187
+ "$i.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png);" +
188
+ "[Console]::Out.Write([Convert]::ToBase64String($ms.ToArray()))";
189
+ const out = runBase64("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], 5000);
190
+ if (!out)
191
+ return null;
192
+ const b64 = out.toString("utf8").trim();
193
+ if (!b64)
194
+ return null;
195
+ return { bytes: Buffer.from(b64, "base64"), mimeType: "image/png" };
196
+ }
197
+ /**
198
+ * A CustomEditor that turns clipboard image paste into numbered `[Image #N]`
199
+ * chips. The image bytes live here, keyed by chip number; the editor text holds
200
+ * only the token, which is the durable, undo-friendly source of truth.
201
+ */
202
+ export class ChipEditor extends CustomEditor {
203
+ stashed = [];
204
+ chipCounter = 0;
205
+ readImage;
206
+ constructor(tui, theme, keybindings, readImage = defaultClipboardImageReader, options) {
207
+ super(tui, theme, keybindings, options);
208
+ this.readImage = readImage;
209
+ // Own the paste handler. pi's setCustomEditorComponent only fills
210
+ // onPasteImage when unset, so this assignment wins over pi's default.
211
+ this.onPasteImage = () => void this.pasteChip();
212
+ }
213
+ /**
214
+ * Paste-image must fire on Cmd+V too (Claude Code parity on macOS). Claude
215
+ * Code's own approach is iTerm2-specific — "Cmd+V ... maps to Ctrl+V" — i.e.
216
+ * it relies on the terminal delivering Cmd+V AS Ctrl+V. We do the same: match
217
+ * ctrl+v + alt+v (win32/WSL) explicitly, and super+v only as a bonus for
218
+ * Kitty-protocol terminals that genuinely deliver the Cmd modifier.
219
+ */
220
+ handleInput(data) {
221
+ // Cmd+V on Ghostty-based terminals (cmux, Ghostty): the terminal writes the
222
+ // clipboard image to a temp file and pastes its PATH as bracketed text, so
223
+ // the payload arrives here as "\x1b[200~<path>\x1b[201~" (pi's Terminal
224
+ // re-wraps it). handlePaste is private and consumed inside super.handleInput,
225
+ // so we strip the markers here and convert a bare image path to a chip.
226
+ const unwrapped = unwrapBracketedPaste(data);
227
+ if (unwrapped !== null) {
228
+ const image = readPastedImagePath(unwrapped);
229
+ logImagePaste({
230
+ event: "paste_path",
231
+ outcome: image ? "ok" : "passthrough",
232
+ mimeType: image?.mimeType,
233
+ bytes: image?.bytes.length,
234
+ file: image ? unwrapped : undefined,
235
+ detail: image ? undefined : `not a lone image path: ${unwrapped.slice(0, 60)}`,
236
+ });
237
+ if (image) {
238
+ this.dropChip(image.bytes, image.mimeType);
239
+ return;
240
+ }
241
+ // Not a lone image path — re-wrap and let pi handle the paste normally.
242
+ super.handleInput(data);
243
+ return;
244
+ }
245
+ // iTerm2 / Terminal.app: Cmd+V arrives as the raw Ctrl+V bytes.
246
+ if (matchesKey(data, "ctrl+v") || matchesKey(data, "alt+v") || matchesKey(data, "super+v")) {
247
+ this.onPasteImage?.();
248
+ return;
249
+ }
250
+ super.handleInput(data);
251
+ }
252
+ async pasteChip() {
253
+ const image = await this.readImage().catch(() => null);
254
+ logImagePaste({
255
+ event: "paste_key",
256
+ outcome: image ? "ok" : "passthrough",
257
+ mimeType: image?.mimeType,
258
+ bytes: image?.bytes.length,
259
+ detail: image ? undefined : "clipboard held no image",
260
+ });
261
+ if (!image)
262
+ return; // no image on the clipboard — leave the prompt untouched
263
+ this.dropChip(image.bytes, image.mimeType);
264
+ }
265
+ dropChip(bytes, mimeType) {
266
+ const n = ++this.chipCounter;
267
+ this.stashed.push({ n, bytes, mimeType });
268
+ logImagePaste({ event: "chip_drop", outcome: "ok", mimeType, bytes: bytes.length, imageCount: n });
269
+ this.insertTextAtCursor(`[Image #${n}]`);
270
+ }
271
+ /** Every image stashed on this editor (used by the input transform). */
272
+ allStashed() {
273
+ return this.stashed;
274
+ }
275
+ /** Drop all stashed images. Called after a successful submit so a sent image
276
+ * is not re-attached to the next message. */
277
+ clearStash() {
278
+ this.stashed = [];
279
+ }
280
+ /** Re-style the chip tokens so they read as chips; layout is untouched. */
281
+ render(width) {
282
+ return super
283
+ .render(width)
284
+ .map((line) => line.replace(CHIP_RE, (m) => `${CHIP_ON}${m}${CHIP_OFF}`));
285
+ }
286
+ }
287
+ /**
288
+ * Build the `input` transform handler: parse `[Image #N]` markers in the
289
+ * submitted text into `ImageAttachment[]` for the model. Unknown/removed chip
290
+ * numbers are ignored (the image was deleted from the prompt), and a prompt
291
+ * with no live chips passes through untouched.
292
+ */
293
+ export function makeChipInputTransform(getImages) {
294
+ return (event) => {
295
+ const byN = new Map(getImages().map((s) => [s.n, s]));
296
+ const attached = [];
297
+ for (const m of event.text.matchAll(CHIP_RE)) {
298
+ const img = byN.get(Number(m[1]));
299
+ if (img) {
300
+ attached.push({
301
+ type: "image",
302
+ mimeType: img.mimeType,
303
+ data: Buffer.from(img.bytes).toString("base64"),
304
+ });
305
+ }
306
+ }
307
+ if (attached.length === 0)
308
+ return { action: "continue" };
309
+ return { action: "transform", text: event.text, images: attached };
310
+ };
311
+ }
312
+ /**
313
+ * Every ChipEditor instance pi has live. pi may recreate the editor (session
314
+ * reset, restore-default, model switch), so the input transform must read from
315
+ * ALL live instances, not a single captured reference that can go stale. Each
316
+ * editor registers itself on construction. The registry holds each editor by
317
+ * weak reference so an orphaned editor (and its stashed image bytes) can be
318
+ * garbage-collected once pi drops it; dead refs are pruned on each read. A
319
+ * plain Set would leak them across session resets / model switches.
320
+ */
321
+ const liveChipEditors = new Set();
322
+ /** Live editors with dead weak refs pruned. */
323
+ function currentChipEditors() {
324
+ const live = [];
325
+ for (const ref of liveChipEditors) {
326
+ const editor = ref.deref();
327
+ if (editor)
328
+ live.push(editor);
329
+ else
330
+ liveChipEditors.delete(ref);
331
+ }
332
+ return live;
333
+ }
334
+ /**
335
+ * Register the chip editor + the input transform on a session. Called on
336
+ * `session_start` in TUI mode. The factory registers each constructed editor in
337
+ * the shared registry; the transform reads the union of live editors' stashes.
338
+ */
339
+ export function registerChipEditor(pi) {
340
+ pi.on("session_start", (_event, ctx) => {
341
+ if (ctx.mode !== "tui" || !ctx.hasUI)
342
+ return;
343
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => {
344
+ const editor = new ChipEditor(tui, theme, keybindings);
345
+ liveChipEditors.add(new WeakRef(editor));
346
+ return editor;
347
+ });
348
+ });
349
+ pi.on("input", (event) => {
350
+ // pi clears the editor (getText() === "") before emitting `input`, so the
351
+ // transform must match chip markers against the EVENT text, not the editor.
352
+ // Union every live editor's stash and resolve markers by chip number.
353
+ const editors = currentChipEditors();
354
+ const stashed = editors.flatMap((e) => e.allStashed());
355
+ const result = makeChipInputTransform(() => stashed)(event);
356
+ // A successful transform means the images were handed to the model; clear
357
+ // the stash so they are not re-attached to the next message.
358
+ if (result.action === "transform")
359
+ editors.forEach((e) => e.clearStash());
360
+ logImagePaste({
361
+ event: "transform",
362
+ outcome: result.action === "transform" ? "ok" : "passthrough",
363
+ imageCount: result.action === "transform" ? result.images.length : 0,
364
+ detail: `stashed=${stashed.length} action=${result.action}`,
365
+ });
366
+ return result;
367
+ });
368
+ }
369
+ //# sourceMappingURL=chipEditor.js.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Configuration + catalog-fetch helpers for the YAGNI pi extension.
3
+ *
4
+ * `YAGNI_BASE_URL` and `YAGNI_TOKEN` are read from the process environment,
5
+ * which the `yagni` launcher is responsible for populating. `YAGNI_CODE_DEBUG`
6
+ * (any truthy value) is an optional operator switch that surfaces fail-soft
7
+ * diagnostics on stderr, e.g. why a /go run could not be recorded (HTTP status +
8
+ * a token-free body excerpt); off by default and never required.
9
+ */
10
+ /** A single model entry as returned by the YAGNI backend catalog endpoint. */
11
+ export interface ModelEntry {
12
+ id: string;
13
+ name: string;
14
+ reasoning: boolean;
15
+ contextWindow: number;
16
+ maxTokens: number;
17
+ /**
18
+ * Supported input modalities for this tier. Catalog-driven so a vision-capable
19
+ * tier can receive images with no pi change (pi does the image plumbing via
20
+ * ProviderModelConfig.input). Optional; defaults to text-only when absent.
21
+ */
22
+ input?: ("text" | "image")[];
23
+ cost: {
24
+ input: number;
25
+ output: number;
26
+ cacheRead: number;
27
+ cacheWrite: number;
28
+ };
29
+ }
30
+ /**
31
+ * Resolve the YAGNI backend base URL from the environment.
32
+ *
33
+ * Falls back to {@link DEFAULT_BASE_URL}, trims whitespace, and strips a single
34
+ * trailing slash so callers can safely append paths.
35
+ */
36
+ export declare function resolveBaseUrl(env?: NodeJS.ProcessEnv): string;
37
+ /** Read the bearer token used to authenticate with the YAGNI backend. */
38
+ export declare function getToken(env?: NodeJS.ProcessEnv): string | undefined;
39
+ /**
40
+ * Read the workspace id the token is bound to, forwarded by the launcher as
41
+ * `YAGNI_WORKSPACE_ID` (from the stored credentials). Used to key the one-time
42
+ * init-pass marker ({@link initDone}). Absent on older logins (treated as
43
+ * unknown → the marker gate degrades to freshness-only, still fail-closed).
44
+ */
45
+ export declare function getWorkspaceId(env?: NodeJS.ProcessEnv): string | undefined;
46
+ /**
47
+ * Read the token's ISO expiry, forwarded by the launcher as
48
+ * `YAGNI_TOKEN_EXPIRES_AT`. Absent on older logins (treated as unknown).
49
+ */
50
+ export declare function getTokenExpiresAt(env?: NodeJS.ProcessEnv): string | undefined;
51
+ /**
52
+ * How close to expiry the token can be before the session-start notice fires.
53
+ * A long session can outlive its token, so a single heads-up lets the user
54
+ * re-login before completions and grounding tools start 401-ing mid-flight.
55
+ */
56
+ export declare const EXPIRY_NOTICE_THRESHOLD_MS: number;
57
+ /** A one-line expiry notice to surface at session start, or null when none. */
58
+ export interface ExpiryNotice {
59
+ message: string;
60
+ type: "warning" | "error";
61
+ }
62
+ /**
63
+ * Pure helper: given the token's ISO expiry and the current time, return the
64
+ * notice to surface at session start, or null when there is nothing to say
65
+ * (missing/unparseable expiry, or comfortably valid). Never throws. This only
66
+ * reads the expiry; it never refreshes the token (no new auth surface).
67
+ */
68
+ export declare function tokenExpiryNotice(expiresAt: string | undefined, nowMs: number, thresholdMs?: number): ExpiryNotice | null;
69
+ /** Options for {@link fetchCatalog}. */
70
+ export interface FetchCatalogOptions {
71
+ baseUrl: string;
72
+ getToken: () => string | undefined;
73
+ fetchImpl?: typeof fetch;
74
+ }
75
+ /**
76
+ * Fetch the YAGNI model catalog at startup.
77
+ *
78
+ * @throws an actionable Error (mentioning `yagni login`) on any non-2xx
79
+ * response so the launcher can surface a clear re-authentication prompt.
80
+ */
81
+ export declare function fetchCatalog(opts: FetchCatalogOptions): Promise<ModelEntry[]>;
82
+ /** The startup company brief returned by GET /api/yagni-code/context. */
83
+ export interface ContextBrief {
84
+ brief: string;
85
+ sources: Array<{
86
+ title: string;
87
+ url: string;
88
+ }>;
89
+ /**
90
+ * Judgment-corpus sizes (additive; §2). Absent on older backends. The
91
+ * extension skips ambient recall entirely when `counts.decisions < 3`.
92
+ */
93
+ counts?: {
94
+ decisions: number;
95
+ corrections: number;
96
+ };
97
+ }
98
+ /** Options for {@link fetchContextBrief}. */
99
+ export interface FetchContextBriefOptions {
100
+ baseUrl: string;
101
+ getToken: () => string | undefined;
102
+ fetchImpl?: typeof fetch;
103
+ }
104
+ /**
105
+ * Fetch the workspace company brief at startup.
106
+ *
107
+ * Unlike {@link fetchCatalog}, this is BEST-EFFORT: a missing or failed brief
108
+ * must never block the agent (it still works, just without boot-time grounding).
109
+ * Returns `null` on any non-2xx response or network error.
110
+ */
111
+ export declare function fetchContextBrief(opts: FetchContextBriefOptions): Promise<ContextBrief | null>;
112
+ //# sourceMappingURL=config.d.ts.map