@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.
- package/README.md +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- 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 +194 -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 +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -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 +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -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 +205 -0
- package/dist/extension/pipeline/eval.js +226 -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 +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -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 +645 -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/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -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 +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -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 +53 -0
- package/dist/extension/pipeline/stages.js +115 -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 +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -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 +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -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/rerouteNotice.d.ts +42 -0
- package/dist/extension/rerouteNotice.js +67 -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/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -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 +145 -0
- package/dist/extension/subagents.js +326 -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 +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- 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 = "[7m";
|
|
36
|
+
const CHIP_OFF = "[27m";
|
|
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
|