@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.
- package/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,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 = "[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
|
+
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
|