@yagni-app/code 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +65 -11
  2. package/dist/claudeCompat.d.ts +109 -0
  3. package/dist/claudeCompat.js +260 -0
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.d.ts +1 -5
  7. package/dist/cli.js +79 -20
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/crashReport.d.ts +135 -0
  11. package/dist/crashReport.js +291 -0
  12. package/dist/credentials.js +2 -1
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +27 -0
  16. package/dist/doctor.js +81 -3
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +194 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +87 -0
  25. package/dist/extension/boostCommand.d.ts +144 -0
  26. package/dist/extension/boostCommand.js +263 -0
  27. package/dist/extension/branding.d.ts +79 -0
  28. package/dist/extension/branding.js +156 -0
  29. package/dist/extension/chipEditor.d.ts +117 -0
  30. package/dist/extension/chipEditor.js +373 -0
  31. package/dist/extension/claudeRules.d.ts +54 -0
  32. package/dist/extension/claudeRules.js +180 -0
  33. package/dist/extension/config.d.ts +173 -0
  34. package/dist/extension/config.js +194 -0
  35. package/dist/extension/costHud.d.ts +186 -0
  36. package/dist/extension/costHud.js +293 -0
  37. package/dist/extension/crashReport.d.ts +89 -0
  38. package/dist/extension/crashReport.js +241 -0
  39. package/dist/extension/decisionCapture.d.ts +52 -0
  40. package/dist/extension/decisionCapture.js +66 -0
  41. package/dist/extension/decisions.d.ts +83 -0
  42. package/dist/extension/decisions.js +200 -0
  43. package/dist/extension/diagnostics.d.ts +41 -0
  44. package/dist/extension/diagnostics.js +110 -0
  45. package/dist/extension/index.d.ts +155 -0
  46. package/dist/extension/index.js +597 -0
  47. package/dist/extension/initDone.d.ts +28 -0
  48. package/dist/extension/initDone.js +66 -0
  49. package/dist/extension/initPass.d.ts +170 -0
  50. package/dist/extension/initPass.js +394 -0
  51. package/dist/extension/mcpTools.d.ts +57 -0
  52. package/dist/extension/mcpTools.js +132 -0
  53. package/dist/extension/nextWorkTool.d.ts +51 -0
  54. package/dist/extension/nextWorkTool.js +80 -0
  55. package/dist/extension/permission.d.ts +91 -0
  56. package/dist/extension/permission.js +236 -0
  57. package/dist/extension/pipeline/activity.d.ts +37 -0
  58. package/dist/extension/pipeline/activity.js +151 -0
  59. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  60. package/dist/extension/pipeline/activityFeed.js +175 -0
  61. package/dist/extension/pipeline/budget.d.ts +48 -0
  62. package/dist/extension/pipeline/budget.js +68 -0
  63. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  64. package/dist/extension/pipeline/checkpoint.js +176 -0
  65. package/dist/extension/pipeline/eval.d.ts +205 -0
  66. package/dist/extension/pipeline/eval.js +226 -0
  67. package/dist/extension/pipeline/events.d.ts +56 -0
  68. package/dist/extension/pipeline/events.js +147 -0
  69. package/dist/extension/pipeline/findings.d.ts +42 -0
  70. package/dist/extension/pipeline/findings.js +144 -0
  71. package/dist/extension/pipeline/finish.d.ts +128 -0
  72. package/dist/extension/pipeline/finish.js +307 -0
  73. package/dist/extension/pipeline/goCommand.d.ts +146 -0
  74. package/dist/extension/pipeline/goCommand.js +1085 -0
  75. package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
  76. package/dist/extension/pipeline/goCompareCommand.js +203 -0
  77. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  78. package/dist/extension/pipeline/goFlags.js +46 -0
  79. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  80. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  81. package/dist/extension/pipeline/invocation.d.ts +45 -0
  82. package/dist/extension/pipeline/invocation.js +64 -0
  83. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  84. package/dist/extension/pipeline/orchestrator.js +645 -0
  85. package/dist/extension/pipeline/personas.d.ts +44 -0
  86. package/dist/extension/pipeline/personas.js +248 -0
  87. package/dist/extension/pipeline/resilience.d.ts +85 -0
  88. package/dist/extension/pipeline/resilience.js +166 -0
  89. package/dist/extension/pipeline/resume.d.ts +18 -0
  90. package/dist/extension/pipeline/resume.js +106 -0
  91. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  92. package/dist/extension/pipeline/runCostTable.js +165 -0
  93. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  94. package/dist/extension/pipeline/runRegistry.js +202 -0
  95. package/dist/extension/pipeline/runSession.d.ts +152 -0
  96. package/dist/extension/pipeline/runSession.js +167 -0
  97. package/dist/extension/pipeline/runState.d.ts +177 -0
  98. package/dist/extension/pipeline/runState.js +275 -0
  99. package/dist/extension/pipeline/runner.d.ts +98 -0
  100. package/dist/extension/pipeline/runner.js +310 -0
  101. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  102. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  103. package/dist/extension/pipeline/stages.d.ts +53 -0
  104. package/dist/extension/pipeline/stages.js +115 -0
  105. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  106. package/dist/extension/pipeline/ticketResolution.js +75 -0
  107. package/dist/extension/pipeline/types.d.ts +439 -0
  108. package/dist/extension/pipeline/types.js +48 -0
  109. package/dist/extension/pipeline/verify.d.ts +277 -0
  110. package/dist/extension/pipeline/verify.js +763 -0
  111. package/dist/extension/pipeline/workspace.d.ts +32 -0
  112. package/dist/extension/pipeline/workspace.js +73 -0
  113. package/dist/extension/pipeline/worktree.d.ts +107 -0
  114. package/dist/extension/pipeline/worktree.js +202 -0
  115. package/dist/extension/provider.d.ts +17 -0
  116. package/dist/extension/provider.js +33 -0
  117. package/dist/extension/recall.d.ts +93 -0
  118. package/dist/extension/recall.js +190 -0
  119. package/dist/extension/recordContextTool.d.ts +38 -0
  120. package/dist/extension/recordContextTool.js +85 -0
  121. package/dist/extension/recordDecisionTool.d.ts +52 -0
  122. package/dist/extension/recordDecisionTool.js +102 -0
  123. package/dist/extension/repoDocs.d.ts +81 -0
  124. package/dist/extension/repoDocs.js +260 -0
  125. package/dist/extension/rerouteNotice.d.ts +42 -0
  126. package/dist/extension/rerouteNotice.js +67 -0
  127. package/dist/extension/resilientFetch.d.ts +60 -0
  128. package/dist/extension/resilientFetch.js +133 -0
  129. package/dist/extension/reviewTool.d.ts +34 -0
  130. package/dist/extension/reviewTool.js +81 -0
  131. package/dist/extension/sessionRuns.d.ts +45 -0
  132. package/dist/extension/sessionRuns.js +77 -0
  133. package/dist/extension/spool.d.ts +92 -0
  134. package/dist/extension/spool.js +266 -0
  135. package/dist/extension/stateHome.d.ts +2 -0
  136. package/dist/extension/stateHome.js +6 -0
  137. package/dist/extension/subagents.d.ts +145 -0
  138. package/dist/extension/subagents.js +326 -0
  139. package/dist/extension/surface.d.ts +10 -0
  140. package/dist/extension/surface.js +12 -0
  141. package/dist/extension/todos.d.ts +110 -0
  142. package/dist/extension/todos.js +217 -0
  143. package/dist/extension/tokenProvider.d.ts +93 -0
  144. package/dist/extension/tokenProvider.js +234 -0
  145. package/dist/launch.d.ts +25 -3
  146. package/dist/launch.js +27 -9
  147. package/dist/login.d.ts +7 -0
  148. package/dist/login.js +3 -1
  149. package/dist/paths.d.ts +13 -4
  150. package/dist/paths.js +17 -5
  151. package/dist/profiles.d.ts +1 -1
  152. package/dist/profiles.js +5 -2
  153. package/dist/upgrade.d.ts +97 -0
  154. package/dist/upgrade.js +284 -0
  155. package/package.json +11 -15
@@ -0,0 +1,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,373 @@
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, isAbsolute } 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
+ // On Windows the backslash IS the path separator (and cmd/PowerShell do no
85
+ // backslash-escaping), so the escape-smuggling rejection applies only where
86
+ // a backslash could be an escape: POSIX shells.
87
+ const isWindows = process.platform === "win32";
88
+ if (!isWindows && trimmed.includes("\\"))
89
+ return null; // escapes => not a plain cmux tmp path
90
+ const path = trimmed;
91
+ if (isWindows ? !isAbsolute(path) : !path.startsWith("/"))
92
+ return null;
93
+ const name = basename(path);
94
+ if (!name.startsWith("clipboard-"))
95
+ return null;
96
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
97
+ if (!(ext in IMAGE_EXT_MIME))
98
+ return null;
99
+ if (!existsSync(path))
100
+ return null;
101
+ let bytes;
102
+ try {
103
+ bytes = readFileSync(path);
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ if (bytes.length === 0)
109
+ return null;
110
+ for (const [magic, mime] of IMAGE_MAGIC) {
111
+ if (magic.every((b, i) => bytes[i] === b))
112
+ return { bytes, mimeType: mime };
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Read an image from the system clipboard without forking pi. Best-effort and
118
+ * cross-platform: native module if present, else osascript/pngpaste (macOS),
119
+ * wl-paste (Wayland), xclip (X11), PowerShell (Windows/WSL). Returns null when
120
+ * the clipboard holds no image (caller then pastes text instead).
121
+ */
122
+ export const defaultClipboardImageReader = async () => {
123
+ const platform = process.platform;
124
+ try {
125
+ if (platform === "darwin") {
126
+ return readMacClipboardImage();
127
+ }
128
+ if (platform === "linux") {
129
+ return readLinuxClipboardImage();
130
+ }
131
+ if (platform === "win32") {
132
+ return readWindowsClipboardImage();
133
+ }
134
+ }
135
+ catch {
136
+ // Fall through — a clipboard read failure must never break the prompt.
137
+ }
138
+ return null;
139
+ };
140
+ function runBase64(command, args, timeoutMs = 3000) {
141
+ const res = spawnSync(command, args, { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
142
+ if (res.error || res.status !== 0)
143
+ return null;
144
+ const out = Buffer.isBuffer(res.stdout) ? res.stdout : Buffer.from(res.stdout ?? "");
145
+ return out.length > 0 ? out : null;
146
+ }
147
+ function readMacClipboardImage() {
148
+ // pngpaste writes the clipboard image to a PNG file (cleanest on macOS).
149
+ const dest = join(tmpdir(), `yagni-clip-${randomUUID()}.png`);
150
+ try {
151
+ const res = spawnSync("pngpaste", [dest], { timeout: 3000 });
152
+ if (res.error || res.status !== 0)
153
+ return null;
154
+ const bytes = readFileSync(dest);
155
+ return bytes.length > 0 ? { bytes, mimeType: "image/png" } : null;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ finally {
161
+ try {
162
+ unlinkSync(dest);
163
+ }
164
+ catch {
165
+ /* best effort */
166
+ }
167
+ }
168
+ }
169
+ function readLinuxClipboardImage() {
170
+ // Wayland: list types, prefer a supported image type, then read it.
171
+ const types = runBase64("wl-paste", ["--list-types"], 1000);
172
+ if (types) {
173
+ const list = types.toString("utf8").split(/\r?\n/).map((t) => t.trim());
174
+ const preferred = ["image/png", "image/jpeg", "image/webp", "image/gif"].find((t) => list.includes(t));
175
+ if (preferred) {
176
+ const data = runBase64("wl-paste", ["--type", preferred, "--no-newline"]);
177
+ if (data)
178
+ return { bytes: data, mimeType: preferred };
179
+ }
180
+ }
181
+ // X11 fallback.
182
+ const xclip = runBase64("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]);
183
+ return xclip ? { bytes: xclip, mimeType: "image/png" } : null;
184
+ }
185
+ function readWindowsClipboardImage() {
186
+ // PowerShell: get the clipboard image and emit it as base64 PNG.
187
+ const script = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing;" +
188
+ "$i=[System.Windows.Forms.Clipboard]::GetImage();" +
189
+ "if($i -eq $null){exit 1};" +
190
+ "$ms=New-Object System.IO.MemoryStream;" +
191
+ "$i.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png);" +
192
+ "[Console]::Out.Write([Convert]::ToBase64String($ms.ToArray()))";
193
+ const out = runBase64("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], 5000);
194
+ if (!out)
195
+ return null;
196
+ const b64 = out.toString("utf8").trim();
197
+ if (!b64)
198
+ return null;
199
+ return { bytes: Buffer.from(b64, "base64"), mimeType: "image/png" };
200
+ }
201
+ /**
202
+ * A CustomEditor that turns clipboard image paste into numbered `[Image #N]`
203
+ * chips. The image bytes live here, keyed by chip number; the editor text holds
204
+ * only the token, which is the durable, undo-friendly source of truth.
205
+ */
206
+ export class ChipEditor extends CustomEditor {
207
+ stashed = [];
208
+ chipCounter = 0;
209
+ readImage;
210
+ constructor(tui, theme, keybindings, readImage = defaultClipboardImageReader, options) {
211
+ super(tui, theme, keybindings, options);
212
+ this.readImage = readImage;
213
+ // Own the paste handler. pi's setCustomEditorComponent only fills
214
+ // onPasteImage when unset, so this assignment wins over pi's default.
215
+ this.onPasteImage = () => void this.pasteChip();
216
+ }
217
+ /**
218
+ * Paste-image must fire on Cmd+V too (Claude Code parity on macOS). Claude
219
+ * Code's own approach is iTerm2-specific — "Cmd+V ... maps to Ctrl+V" — i.e.
220
+ * it relies on the terminal delivering Cmd+V AS Ctrl+V. We do the same: match
221
+ * ctrl+v + alt+v (win32/WSL) explicitly, and super+v only as a bonus for
222
+ * Kitty-protocol terminals that genuinely deliver the Cmd modifier.
223
+ */
224
+ handleInput(data) {
225
+ // Cmd+V on Ghostty-based terminals (cmux, Ghostty): the terminal writes the
226
+ // clipboard image to a temp file and pastes its PATH as bracketed text, so
227
+ // the payload arrives here as "\x1b[200~<path>\x1b[201~" (pi's Terminal
228
+ // re-wraps it). handlePaste is private and consumed inside super.handleInput,
229
+ // so we strip the markers here and convert a bare image path to a chip.
230
+ const unwrapped = unwrapBracketedPaste(data);
231
+ if (unwrapped !== null) {
232
+ const image = readPastedImagePath(unwrapped);
233
+ logImagePaste({
234
+ event: "paste_path",
235
+ outcome: image ? "ok" : "passthrough",
236
+ mimeType: image?.mimeType,
237
+ bytes: image?.bytes.length,
238
+ file: image ? unwrapped : undefined,
239
+ detail: image ? undefined : `not a lone image path: ${unwrapped.slice(0, 60)}`,
240
+ });
241
+ if (image) {
242
+ this.dropChip(image.bytes, image.mimeType);
243
+ return;
244
+ }
245
+ // Not a lone image path — re-wrap and let pi handle the paste normally.
246
+ super.handleInput(data);
247
+ return;
248
+ }
249
+ // iTerm2 / Terminal.app: Cmd+V arrives as the raw Ctrl+V bytes.
250
+ if (matchesKey(data, "ctrl+v") || matchesKey(data, "alt+v") || matchesKey(data, "super+v")) {
251
+ this.onPasteImage?.();
252
+ return;
253
+ }
254
+ super.handleInput(data);
255
+ }
256
+ async pasteChip() {
257
+ const image = await this.readImage().catch(() => null);
258
+ logImagePaste({
259
+ event: "paste_key",
260
+ outcome: image ? "ok" : "passthrough",
261
+ mimeType: image?.mimeType,
262
+ bytes: image?.bytes.length,
263
+ detail: image ? undefined : "clipboard held no image",
264
+ });
265
+ if (!image)
266
+ return; // no image on the clipboard — leave the prompt untouched
267
+ this.dropChip(image.bytes, image.mimeType);
268
+ }
269
+ dropChip(bytes, mimeType) {
270
+ const n = ++this.chipCounter;
271
+ this.stashed.push({ n, bytes, mimeType });
272
+ logImagePaste({ event: "chip_drop", outcome: "ok", mimeType, bytes: bytes.length, imageCount: n });
273
+ this.insertTextAtCursor(`[Image #${n}]`);
274
+ }
275
+ /** Every image stashed on this editor (used by the input transform). */
276
+ allStashed() {
277
+ return this.stashed;
278
+ }
279
+ /** Drop all stashed images. Called after a successful submit so a sent image
280
+ * is not re-attached to the next message. */
281
+ clearStash() {
282
+ this.stashed = [];
283
+ }
284
+ /** Re-style the chip tokens so they read as chips; layout is untouched. */
285
+ render(width) {
286
+ return super
287
+ .render(width)
288
+ .map((line) => line.replace(CHIP_RE, (m) => `${CHIP_ON}${m}${CHIP_OFF}`));
289
+ }
290
+ }
291
+ /**
292
+ * Build the `input` transform handler: parse `[Image #N]` markers in the
293
+ * submitted text into `ImageAttachment[]` for the model. Unknown/removed chip
294
+ * numbers are ignored (the image was deleted from the prompt), and a prompt
295
+ * with no live chips passes through untouched.
296
+ */
297
+ export function makeChipInputTransform(getImages) {
298
+ return (event) => {
299
+ const byN = new Map(getImages().map((s) => [s.n, s]));
300
+ const attached = [];
301
+ for (const m of event.text.matchAll(CHIP_RE)) {
302
+ const img = byN.get(Number(m[1]));
303
+ if (img) {
304
+ attached.push({
305
+ type: "image",
306
+ mimeType: img.mimeType,
307
+ data: Buffer.from(img.bytes).toString("base64"),
308
+ });
309
+ }
310
+ }
311
+ if (attached.length === 0)
312
+ return { action: "continue" };
313
+ return { action: "transform", text: event.text, images: attached };
314
+ };
315
+ }
316
+ /**
317
+ * Every ChipEditor instance pi has live. pi may recreate the editor (session
318
+ * reset, restore-default, model switch), so the input transform must read from
319
+ * ALL live instances, not a single captured reference that can go stale. Each
320
+ * editor registers itself on construction. The registry holds each editor by
321
+ * weak reference so an orphaned editor (and its stashed image bytes) can be
322
+ * garbage-collected once pi drops it; dead refs are pruned on each read. A
323
+ * plain Set would leak them across session resets / model switches.
324
+ */
325
+ const liveChipEditors = new Set();
326
+ /** Live editors with dead weak refs pruned. */
327
+ function currentChipEditors() {
328
+ const live = [];
329
+ for (const ref of liveChipEditors) {
330
+ const editor = ref.deref();
331
+ if (editor)
332
+ live.push(editor);
333
+ else
334
+ liveChipEditors.delete(ref);
335
+ }
336
+ return live;
337
+ }
338
+ /**
339
+ * Register the chip editor + the input transform on a session. Called on
340
+ * `session_start` in TUI mode. The factory registers each constructed editor in
341
+ * the shared registry; the transform reads the union of live editors' stashes.
342
+ */
343
+ export function registerChipEditor(pi) {
344
+ pi.on("session_start", (_event, ctx) => {
345
+ if (ctx.mode !== "tui" || !ctx.hasUI)
346
+ return;
347
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => {
348
+ const editor = new ChipEditor(tui, theme, keybindings);
349
+ liveChipEditors.add(new WeakRef(editor));
350
+ return editor;
351
+ });
352
+ });
353
+ pi.on("input", (event) => {
354
+ // pi clears the editor (getText() === "") before emitting `input`, so the
355
+ // transform must match chip markers against the EVENT text, not the editor.
356
+ // Union every live editor's stash and resolve markers by chip number.
357
+ const editors = currentChipEditors();
358
+ const stashed = editors.flatMap((e) => e.allStashed());
359
+ const result = makeChipInputTransform(() => stashed)(event);
360
+ // A successful transform means the images were handed to the model; clear
361
+ // the stash so they are not re-attached to the next message.
362
+ if (result.action === "transform")
363
+ editors.forEach((e) => e.clearStash());
364
+ logImagePaste({
365
+ event: "transform",
366
+ outcome: result.action === "transform" ? "ok" : "passthrough",
367
+ imageCount: result.action === "transform" ? result.images.length : 0,
368
+ detail: `stashed=${stashed.length} action=${result.action}`,
369
+ });
370
+ return result;
371
+ });
372
+ }
373
+ //# sourceMappingURL=chipEditor.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Claude Code `.claude/rules/*.md` compat: fold rule files into the system
3
+ * prompt the extension already owns.
4
+ *
5
+ * Claude Code loads a rule without `paths` frontmatter at launch (same weight
6
+ * as CLAUDE.md) and injects a `paths`-scoped rule when the agent reads a
7
+ * matching file. pi has no read-triggered injection, so the faithful bounded
8
+ * mapping (see docs/superpowers/specs/2026-08-08-claude-plugins-design.md) is:
9
+ *
10
+ * - unscoped rules: content inlined verbatim into one system-prompt section
11
+ * (per-file and total character caps, truncation markers) — exactly the
12
+ * load-at-launch behavior;
13
+ * - path-scoped rules: a pointer line — "when working with files matching
14
+ * these patterns, read and follow <file>" — so the prompt stays bounded on
15
+ * rule-heavy repos and scoping stays honest.
16
+ *
17
+ * The launcher decides WHICH dirs load (user `~/.claude/rules` freely, repo
18
+ * `.claude/rules` only once the folder is trusted) and passes them via
19
+ * `YAGNI_CLAUDE_RULES_DIRS`, user dir first so project rules land last and
20
+ * win. Everything here is fail-soft and honors the compat kill switch.
21
+ */
22
+ export declare const CLAUDE_RULES_DIRS_ENV = "YAGNI_CLAUDE_RULES_DIRS";
23
+ export declare const CLAUDE_RULES_HEADER = "## Repository rules (.claude/rules)";
24
+ export interface ClaudeRule {
25
+ /** Absolute file path — the pointer target for scoped rules. */
26
+ path: string;
27
+ /** Glob patterns from `paths` frontmatter; empty means always-on. */
28
+ paths: string[];
29
+ /** Markdown body (frontmatter stripped). */
30
+ body: string;
31
+ }
32
+ export interface CollectRulesOptions {
33
+ /** Total file cap across all dirs (default 40). */
34
+ maxFiles?: number;
35
+ /** Recursion depth cap per dir (default 5). */
36
+ maxDepth?: number;
37
+ /** Skip files larger than this (default 64 KiB). */
38
+ maxFileBytes?: number;
39
+ }
40
+ /** Enumerate + parse rule files from `dirs`, in dir order. Never throws. */
41
+ export declare function collectClaudeRules(dirs: string[], opts?: CollectRulesOptions): ClaudeRule[];
42
+ export interface BuildSectionOptions {
43
+ /** Per-file inline cap in characters (default 8000). */
44
+ maxInlineChars?: number;
45
+ /** Total inline budget across unscoped rules (default 32000). */
46
+ maxTotalChars?: number;
47
+ }
48
+ /** The system-prompt section for `rules`, or null when there is nothing to say. */
49
+ export declare function buildClaudeRulesSection(rules: ClaudeRule[], opts?: BuildSectionOptions): string | null;
50
+ /** Env-driven entry: dirs from YAGNI_CLAUDE_RULES_DIRS, null when off/empty. */
51
+ export declare function claudeRulesSection(env: NodeJS.ProcessEnv, opts?: CollectRulesOptions & BuildSectionOptions): string | null;
52
+ /** Append `section` to a system prompt exactly once; no-op without a section. */
53
+ export declare function appendClaudeRules(systemPrompt: string, section: string | null): string;
54
+ //# sourceMappingURL=claudeRules.d.ts.map