@aexol/spectral 0.7.1 → 0.7.5

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 (219) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/agent/agents.js +1 -1
  3. package/dist/agent/index.js +199 -184
  4. package/dist/commands/serve.js +0 -3
  5. package/dist/designer/data/systems/renault/DESIGN.md +1 -1
  6. package/dist/designer/philosophies.js +668 -0
  7. package/dist/mcp/sampling-handler.js +1 -1
  8. package/dist/memory/commands/status.js +1 -1
  9. package/dist/memory/compaction.js +2 -2
  10. package/dist/memory/config.js +1 -1
  11. package/dist/memory/debug-log.js +1 -1
  12. package/dist/memory/hooks/compaction-hook.js +29 -0
  13. package/dist/memory/index.js +2 -0
  14. package/dist/memory/observer.js +2 -2
  15. package/dist/memory/project-observations-store.js +14 -0
  16. package/dist/memory/tokens.js +1 -1
  17. package/dist/memory/tools/read-project-observations.js +82 -0
  18. package/dist/memory/tools/recall-observation.js +2 -2
  19. package/dist/pi/agent-core/agent-loop.js +501 -0
  20. package/dist/pi/agent-core/agent.js +401 -0
  21. package/dist/pi/agent-core/harness/agent-harness.js +899 -0
  22. package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
  23. package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
  24. package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
  25. package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
  26. package/dist/pi/agent-core/harness/messages.js +101 -0
  27. package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
  28. package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
  29. package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
  30. package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
  31. package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
  32. package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
  33. package/dist/pi/agent-core/harness/session/session.js +196 -0
  34. package/dist/pi/agent-core/harness/session/uuid.js +49 -0
  35. package/dist/pi/agent-core/harness/skills.js +310 -0
  36. package/dist/pi/agent-core/harness/system-prompt.js +29 -0
  37. package/dist/pi/agent-core/harness/types.js +93 -0
  38. package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
  39. package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
  40. package/dist/pi/agent-core/index.js +24 -0
  41. package/dist/pi/agent-core/node.js +2 -0
  42. package/dist/pi/agent-core/proxy.js +277 -0
  43. package/dist/pi/agent-core/types.js +1 -0
  44. package/dist/pi/ai/api-registry.js +43 -0
  45. package/dist/pi/ai/cli.js +120 -0
  46. package/dist/pi/ai/env-api-keys.js +169 -0
  47. package/dist/pi/ai/image-models.generated.js +441 -0
  48. package/dist/pi/ai/image-models.js +22 -0
  49. package/dist/pi/ai/images-api-registry.js +21 -0
  50. package/dist/pi/ai/images.js +13 -0
  51. package/dist/pi/ai/index.js +18 -0
  52. package/dist/pi/ai/models.generated.js +16220 -0
  53. package/dist/pi/ai/models.js +70 -0
  54. package/dist/pi/ai/oauth.js +1 -0
  55. package/dist/pi/ai/providers/anthropic.js +945 -0
  56. package/dist/pi/ai/providers/faux.js +367 -0
  57. package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
  58. package/dist/pi/ai/providers/openai-completions.js +945 -0
  59. package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
  60. package/dist/pi/ai/providers/register-builtins.js +97 -0
  61. package/dist/pi/ai/providers/simple-options.js +40 -0
  62. package/dist/pi/ai/providers/transform-messages.js +183 -0
  63. package/dist/pi/ai/session-resources.js +21 -0
  64. package/dist/pi/ai/stream.js +26 -0
  65. package/dist/pi/ai/types.js +1 -0
  66. package/dist/pi/ai/utils/diagnostics.js +24 -0
  67. package/dist/pi/ai/utils/event-stream.js +80 -0
  68. package/dist/pi/ai/utils/hash.js +13 -0
  69. package/dist/pi/ai/utils/headers.js +7 -0
  70. package/dist/pi/ai/utils/json-parse.js +112 -0
  71. package/dist/pi/ai/utils/node-http-proxy.js +96 -0
  72. package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
  73. package/dist/pi/ai/utils/oauth/device-code.js +54 -0
  74. package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
  75. package/dist/pi/ai/utils/oauth/index.js +121 -0
  76. package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
  77. package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
  78. package/dist/pi/ai/utils/oauth/pkce.js +30 -0
  79. package/dist/pi/ai/utils/oauth/types.js +1 -0
  80. package/dist/pi/ai/utils/overflow.js +150 -0
  81. package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
  82. package/dist/pi/ai/utils/typebox-helpers.js +20 -0
  83. package/dist/pi/ai/utils/validation.js +280 -0
  84. package/dist/pi/coding-agent/bun/cli.js +7 -0
  85. package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
  86. package/dist/pi/coding-agent/cli/args.js +340 -0
  87. package/dist/pi/coding-agent/cli/file-processor.js +82 -0
  88. package/dist/pi/coding-agent/cli/initial-message.js +21 -0
  89. package/dist/pi/coding-agent/cli.js +17 -0
  90. package/dist/pi/coding-agent/config.js +414 -0
  91. package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
  92. package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
  93. package/dist/pi/coding-agent/core/agent-session.js +2498 -0
  94. package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
  95. package/dist/pi/coding-agent/core/auth-storage.js +441 -0
  96. package/dist/pi/coding-agent/core/bash-executor.js +110 -0
  97. package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
  98. package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
  99. package/dist/pi/coding-agent/core/compaction/index.js +6 -0
  100. package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
  101. package/dist/pi/coding-agent/core/defaults.js +1 -0
  102. package/dist/pi/coding-agent/core/diagnostics.js +1 -0
  103. package/dist/pi/coding-agent/core/event-bus.js +24 -0
  104. package/dist/pi/coding-agent/core/exec.js +74 -0
  105. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
  106. package/dist/pi/coding-agent/core/export-html/index.js +225 -0
  107. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
  108. package/dist/pi/coding-agent/core/extensions/index.js +8 -0
  109. package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
  110. package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
  111. package/dist/pi/coding-agent/core/extensions/types.js +44 -0
  112. package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
  113. package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
  114. package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
  115. package/dist/pi/coding-agent/core/index.js +11 -0
  116. package/dist/pi/coding-agent/core/keybindings.js +294 -0
  117. package/dist/pi/coding-agent/core/messages.js +122 -0
  118. package/dist/pi/coding-agent/core/model-registry.js +728 -0
  119. package/dist/pi/coding-agent/core/model-resolver.js +494 -0
  120. package/dist/pi/coding-agent/core/output-guard.js +58 -0
  121. package/dist/pi/coding-agent/core/package-manager.js +2020 -0
  122. package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
  123. package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
  124. package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
  125. package/dist/pi/coding-agent/core/resource-loader.js +733 -0
  126. package/dist/pi/coding-agent/core/sdk.js +282 -0
  127. package/dist/pi/coding-agent/core/session-cwd.js +37 -0
  128. package/dist/pi/coding-agent/core/session-manager.js +1146 -0
  129. package/dist/pi/coding-agent/core/settings-manager.js +794 -0
  130. package/dist/pi/coding-agent/core/skills.js +386 -0
  131. package/dist/pi/coding-agent/core/slash-commands.js +24 -0
  132. package/dist/pi/coding-agent/core/source-info.js +18 -0
  133. package/dist/pi/coding-agent/core/system-prompt.js +122 -0
  134. package/dist/pi/coding-agent/core/telemetry.js +8 -0
  135. package/dist/pi/coding-agent/core/timings.js +30 -0
  136. package/dist/pi/coding-agent/core/tools/bash.js +341 -0
  137. package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
  138. package/dist/pi/coding-agent/core/tools/edit.js +324 -0
  139. package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
  140. package/dist/pi/coding-agent/core/tools/find.js +297 -0
  141. package/dist/pi/coding-agent/core/tools/grep.js +303 -0
  142. package/dist/pi/coding-agent/core/tools/index.js +111 -0
  143. package/dist/pi/coding-agent/core/tools/ls.js +168 -0
  144. package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
  145. package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
  146. package/dist/pi/coding-agent/core/tools/read.js +288 -0
  147. package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
  148. package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
  149. package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
  150. package/dist/pi/coding-agent/core/tools/write.js +212 -0
  151. package/dist/pi/coding-agent/index.js +41 -0
  152. package/dist/pi/coding-agent/main.js +5 -0
  153. package/dist/pi/coding-agent/migrations.js +280 -0
  154. package/dist/pi/coding-agent/modes/index.js +7 -0
  155. package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
  156. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
  157. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
  158. package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
  159. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
  160. package/dist/pi/coding-agent/modes/print-mode.js +130 -0
  161. package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
  162. package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
  163. package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
  164. package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
  165. package/dist/pi/coding-agent/utils/ansi.js +51 -0
  166. package/dist/pi/coding-agent/utils/changelog.js +86 -0
  167. package/dist/pi/coding-agent/utils/child-process.js +87 -0
  168. package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
  169. package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
  170. package/dist/pi/coding-agent/utils/clipboard.js +116 -0
  171. package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
  172. package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
  173. package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
  174. package/dist/pi/coding-agent/utils/git.js +162 -0
  175. package/dist/pi/coding-agent/utils/html.js +39 -0
  176. package/dist/pi/coding-agent/utils/image-convert.js +38 -0
  177. package/dist/pi/coding-agent/utils/image-resize.js +136 -0
  178. package/dist/pi/coding-agent/utils/mime.js +68 -0
  179. package/dist/pi/coding-agent/utils/paths.js +91 -0
  180. package/dist/pi/coding-agent/utils/photon.js +120 -0
  181. package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
  182. package/dist/pi/coding-agent/utils/shell.js +194 -0
  183. package/dist/pi/coding-agent/utils/sleep.js +16 -0
  184. package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
  185. package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
  186. package/dist/pi/coding-agent/utils/version-check.js +81 -0
  187. package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
  188. package/dist/pi/tui/autocomplete.js +631 -0
  189. package/dist/pi/tui/components/box.js +103 -0
  190. package/dist/pi/tui/components/cancellable-loader.js +34 -0
  191. package/dist/pi/tui/components/editor.js +1915 -0
  192. package/dist/pi/tui/components/image.js +88 -0
  193. package/dist/pi/tui/components/input.js +425 -0
  194. package/dist/pi/tui/components/loader.js +68 -0
  195. package/dist/pi/tui/components/markdown.js +633 -0
  196. package/dist/pi/tui/components/select-list.js +158 -0
  197. package/dist/pi/tui/components/settings-list.js +184 -0
  198. package/dist/pi/tui/components/spacer.js +22 -0
  199. package/dist/pi/tui/components/text.js +88 -0
  200. package/dist/pi/tui/components/truncated-text.js +50 -0
  201. package/dist/pi/tui/editor-component.js +1 -0
  202. package/dist/pi/tui/fuzzy.js +109 -0
  203. package/dist/pi/tui/index.js +31 -0
  204. package/dist/pi/tui/keybindings.js +173 -0
  205. package/dist/pi/tui/keys.js +1172 -0
  206. package/dist/pi/tui/kill-ring.js +43 -0
  207. package/dist/pi/tui/stdin-buffer.js +360 -0
  208. package/dist/pi/tui/terminal-image.js +335 -0
  209. package/dist/pi/tui/terminal.js +324 -0
  210. package/dist/pi/tui/tui.js +1076 -0
  211. package/dist/pi/tui/undo-stack.js +24 -0
  212. package/dist/pi/tui/utils.js +1016 -0
  213. package/dist/relay/dispatcher.js +30 -0
  214. package/dist/server/handlers/queue.js +52 -0
  215. package/dist/server/pi-bridge.js +9 -1
  216. package/dist/server/session-stream.js +76 -111
  217. package/dist/server/storage.js +154 -2
  218. package/dist/server/title-generator.js +14 -153
  219. package/package.json +24 -6
@@ -0,0 +1,86 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ /**
3
+ * Parse changelog entries from CHANGELOG.md
4
+ * Scans for ## lines and collects content until next ## or EOF
5
+ */
6
+ export function parseChangelog(changelogPath) {
7
+ if (!existsSync(changelogPath)) {
8
+ return [];
9
+ }
10
+ try {
11
+ const content = readFileSync(changelogPath, "utf-8");
12
+ const lines = content.split("\n");
13
+ const entries = [];
14
+ let currentLines = [];
15
+ let currentVersion = null;
16
+ for (const line of lines) {
17
+ // Check if this is a version header (## [x.y.z] ...)
18
+ if (line.startsWith("## ")) {
19
+ // Save previous entry if exists
20
+ if (currentVersion && currentLines.length > 0) {
21
+ entries.push({
22
+ ...currentVersion,
23
+ content: currentLines.join("\n").trim(),
24
+ });
25
+ }
26
+ // Try to parse version from this line
27
+ const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
28
+ if (versionMatch) {
29
+ currentVersion = {
30
+ major: Number.parseInt(versionMatch[1], 10),
31
+ minor: Number.parseInt(versionMatch[2], 10),
32
+ patch: Number.parseInt(versionMatch[3], 10),
33
+ };
34
+ currentLines = [line];
35
+ }
36
+ else {
37
+ // Reset if we can't parse version
38
+ currentVersion = null;
39
+ currentLines = [];
40
+ }
41
+ }
42
+ else if (currentVersion) {
43
+ // Collect lines for current version
44
+ currentLines.push(line);
45
+ }
46
+ }
47
+ // Save last entry
48
+ if (currentVersion && currentLines.length > 0) {
49
+ entries.push({
50
+ ...currentVersion,
51
+ content: currentLines.join("\n").trim(),
52
+ });
53
+ }
54
+ return entries;
55
+ }
56
+ catch (error) {
57
+ console.error(`Warning: Could not parse changelog: ${error}`);
58
+ return [];
59
+ }
60
+ }
61
+ /**
62
+ * Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
63
+ */
64
+ export function compareVersions(v1, v2) {
65
+ if (v1.major !== v2.major)
66
+ return v1.major - v2.major;
67
+ if (v1.minor !== v2.minor)
68
+ return v1.minor - v2.minor;
69
+ return v1.patch - v2.patch;
70
+ }
71
+ /**
72
+ * Get entries newer than lastVersion
73
+ */
74
+ export function getNewEntries(entries, lastVersion) {
75
+ // Parse lastVersion
76
+ const parts = lastVersion.split(".").map(Number);
77
+ const last = {
78
+ major: parts[0] || 0,
79
+ minor: parts[1] || 0,
80
+ patch: parts[2] || 0,
81
+ content: "",
82
+ };
83
+ return entries.filter((entry) => compareVersions(entry, last) > 0);
84
+ }
85
+ // Re-export getChangelogPath from paths.ts for convenience
86
+ export { getChangelogPath } from "../config.js";
@@ -0,0 +1,87 @@
1
+ import { spawn as nodeSpawn, spawnSync as nodeSpawnSync, } from "node:child_process";
2
+ import crossSpawn from "cross-spawn";
3
+ const EXIT_STDIO_GRACE_MS = 100;
4
+ export function spawnProcess(command, args, options) {
5
+ return process.platform === "win32" ? crossSpawn(command, args, options) : nodeSpawn(command, args, options);
6
+ }
7
+ export function spawnProcessSync(command, args, options) {
8
+ return process.platform === "win32"
9
+ ? crossSpawn.sync(command, args, options)
10
+ : nodeSpawnSync(command, args, options);
11
+ }
12
+ /**
13
+ * Wait for a child process to terminate without hanging on inherited stdio handles.
14
+ *
15
+ * On Windows, daemonized descendants can inherit the child's stdout/stderr pipe
16
+ * handles. In that case the child emits `exit`, but `close` can hang forever even
17
+ * though the original process is already gone. We wait briefly for stdio to end,
18
+ * then forcibly stop tracking the inherited handles.
19
+ */
20
+ export function waitForChildProcess(child) {
21
+ return new Promise((resolve, reject) => {
22
+ let settled = false;
23
+ let exited = false;
24
+ let exitCode = null;
25
+ let postExitTimer;
26
+ let stdoutEnded = child.stdout === null;
27
+ let stderrEnded = child.stderr === null;
28
+ const cleanup = () => {
29
+ if (postExitTimer) {
30
+ clearTimeout(postExitTimer);
31
+ postExitTimer = undefined;
32
+ }
33
+ child.removeListener("error", onError);
34
+ child.removeListener("exit", onExit);
35
+ child.removeListener("close", onClose);
36
+ child.stdout?.removeListener("end", onStdoutEnd);
37
+ child.stderr?.removeListener("end", onStderrEnd);
38
+ };
39
+ const finalize = (code) => {
40
+ if (settled)
41
+ return;
42
+ settled = true;
43
+ cleanup();
44
+ child.stdout?.destroy();
45
+ child.stderr?.destroy();
46
+ resolve(code);
47
+ };
48
+ const maybeFinalizeAfterExit = () => {
49
+ if (!exited || settled)
50
+ return;
51
+ if (stdoutEnded && stderrEnded) {
52
+ finalize(exitCode);
53
+ }
54
+ };
55
+ const onStdoutEnd = () => {
56
+ stdoutEnded = true;
57
+ maybeFinalizeAfterExit();
58
+ };
59
+ const onStderrEnd = () => {
60
+ stderrEnded = true;
61
+ maybeFinalizeAfterExit();
62
+ };
63
+ const onError = (err) => {
64
+ if (settled)
65
+ return;
66
+ settled = true;
67
+ cleanup();
68
+ reject(err);
69
+ };
70
+ const onExit = (code) => {
71
+ exited = true;
72
+ exitCode = code;
73
+ maybeFinalizeAfterExit();
74
+ if (!settled) {
75
+ postExitTimer = setTimeout(() => finalize(code), EXIT_STDIO_GRACE_MS);
76
+ }
77
+ };
78
+ const onClose = (code) => {
79
+ finalize(code);
80
+ };
81
+ child.stdout?.once("end", onStdoutEnd);
82
+ child.stderr?.once("end", onStderrEnd);
83
+ child.once("error", onError);
84
+ child.once("exit", onExit);
85
+ child.once("close", onClose);
86
+ });
87
+ }
@@ -0,0 +1,244 @@
1
+ import { spawnSync } from "child_process";
2
+ import { randomUUID } from "crypto";
3
+ import { readFileSync, unlinkSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
6
+ import { clipboard } from "./clipboard-native.js";
7
+ import { loadPhoton } from "./photon.js";
8
+ const SUPPORTED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
9
+ const DEFAULT_LIST_TIMEOUT_MS = 1000;
10
+ const DEFAULT_READ_TIMEOUT_MS = 3000;
11
+ const DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;
12
+ const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
13
+ export function isWaylandSession(env = process.env) {
14
+ return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland";
15
+ }
16
+ function baseMimeType(mimeType) {
17
+ return mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
18
+ }
19
+ export function extensionForImageMimeType(mimeType) {
20
+ switch (baseMimeType(mimeType)) {
21
+ case "image/png":
22
+ return "png";
23
+ case "image/jpeg":
24
+ return "jpg";
25
+ case "image/webp":
26
+ return "webp";
27
+ case "image/gif":
28
+ return "gif";
29
+ default:
30
+ return null;
31
+ }
32
+ }
33
+ function selectPreferredImageMimeType(mimeTypes) {
34
+ const normalized = mimeTypes
35
+ .map((t) => t.trim())
36
+ .filter(Boolean)
37
+ .map((t) => ({ raw: t, base: baseMimeType(t) }));
38
+ for (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {
39
+ const match = normalized.find((t) => t.base === preferred);
40
+ if (match) {
41
+ return match.raw;
42
+ }
43
+ }
44
+ const anyImage = normalized.find((t) => t.base.startsWith("image/"));
45
+ return anyImage?.raw ?? null;
46
+ }
47
+ function isSupportedImageMimeType(mimeType) {
48
+ const base = baseMimeType(mimeType);
49
+ return SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);
50
+ }
51
+ /**
52
+ * Convert unsupported image formats to PNG using Photon.
53
+ * Returns null if conversion is unavailable or fails.
54
+ */
55
+ async function convertToPng(bytes) {
56
+ const photon = await loadPhoton();
57
+ if (!photon) {
58
+ return null;
59
+ }
60
+ try {
61
+ const image = photon.PhotonImage.new_from_byteslice(bytes);
62
+ try {
63
+ return image.get_bytes();
64
+ }
65
+ finally {
66
+ image.free();
67
+ }
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ function runCommand(command, args, options) {
74
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
75
+ const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
76
+ const result = spawnSync(command, args, {
77
+ timeout: timeoutMs,
78
+ maxBuffer: maxBufferBytes,
79
+ env: options?.env,
80
+ });
81
+ if (result.error) {
82
+ return { ok: false, stdout: Buffer.alloc(0) };
83
+ }
84
+ if (result.status !== 0) {
85
+ return { ok: false, stdout: Buffer.alloc(0) };
86
+ }
87
+ const stdout = Buffer.isBuffer(result.stdout)
88
+ ? result.stdout
89
+ : Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf-8" : undefined);
90
+ return { ok: true, stdout };
91
+ }
92
+ function readClipboardImageViaWlPaste() {
93
+ const list = runCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
94
+ if (!list.ok) {
95
+ return null;
96
+ }
97
+ const types = list.stdout
98
+ .toString("utf-8")
99
+ .split(/\r?\n/)
100
+ .map((t) => t.trim())
101
+ .filter(Boolean);
102
+ const selectedType = selectPreferredImageMimeType(types);
103
+ if (!selectedType) {
104
+ return null;
105
+ }
106
+ const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
107
+ if (!data.ok || data.stdout.length === 0) {
108
+ return null;
109
+ }
110
+ return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
111
+ }
112
+ function isWSL(env = process.env) {
113
+ if (env.WSL_DISTRO_NAME || env.WSLENV) {
114
+ return true;
115
+ }
116
+ try {
117
+ const release = readFileSync("/proc/version", "utf-8");
118
+ return /microsoft|wsl/i.test(release);
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ }
124
+ /**
125
+ * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from
126
+ * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard
127
+ * directly, so we use it as a fallback.
128
+ */
129
+ function readClipboardImageViaPowerShell() {
130
+ const tmpFile = join(tmpdir(), `pi-wsl-clip-${randomUUID()}.png`);
131
+ try {
132
+ const winPathResult = runCommand("wslpath", ["-w", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
133
+ if (!winPathResult.ok) {
134
+ return null;
135
+ }
136
+ const winPath = winPathResult.stdout.toString("utf-8").trim();
137
+ if (!winPath) {
138
+ return null;
139
+ }
140
+ const psQuotedWinPath = winPath.replaceAll("'", "''");
141
+ const psScript = [
142
+ "Add-Type -AssemblyName System.Windows.Forms",
143
+ "Add-Type -AssemblyName System.Drawing",
144
+ `$path = '${psQuotedWinPath}'`,
145
+ "$img = [System.Windows.Forms.Clipboard]::GetImage()",
146
+ "if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }",
147
+ ].join("; ");
148
+ const result = runCommand("powershell.exe", ["-NoProfile", "-Command", psScript], {
149
+ timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,
150
+ });
151
+ if (!result.ok) {
152
+ return null;
153
+ }
154
+ const output = result.stdout.toString("utf-8").trim();
155
+ if (output !== "ok") {
156
+ return null;
157
+ }
158
+ const bytes = readFileSync(tmpFile);
159
+ if (bytes.length === 0) {
160
+ return null;
161
+ }
162
+ return { bytes: new Uint8Array(bytes), mimeType: "image/png" };
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ finally {
168
+ try {
169
+ unlinkSync(tmpFile);
170
+ }
171
+ catch {
172
+ // Ignore cleanup errors.
173
+ }
174
+ }
175
+ }
176
+ function readClipboardImageViaXclip() {
177
+ const targets = runCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], {
178
+ timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
179
+ });
180
+ let candidateTypes = [];
181
+ if (targets.ok) {
182
+ candidateTypes = targets.stdout
183
+ .toString("utf-8")
184
+ .split(/\r?\n/)
185
+ .map((t) => t.trim())
186
+ .filter(Boolean);
187
+ }
188
+ const preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;
189
+ const tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];
190
+ for (const mimeType of tryTypes) {
191
+ const data = runCommand("xclip", ["-selection", "clipboard", "-t", mimeType, "-o"]);
192
+ if (data.ok && data.stdout.length > 0) {
193
+ return { bytes: data.stdout, mimeType: baseMimeType(mimeType) };
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ async function readClipboardImageViaNativeClipboard() {
199
+ if (!clipboard || !clipboard.hasImage()) {
200
+ return null;
201
+ }
202
+ const imageData = await clipboard.getImageBinary();
203
+ if (!imageData || imageData.length === 0) {
204
+ return null;
205
+ }
206
+ const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
207
+ return { bytes, mimeType: "image/png" };
208
+ }
209
+ export async function readClipboardImage(options) {
210
+ const env = options?.env ?? process.env;
211
+ const platform = options?.platform ?? process.platform;
212
+ if (env.TERMUX_VERSION) {
213
+ return null;
214
+ }
215
+ let image = null;
216
+ if (platform === "linux") {
217
+ const wsl = isWSL(env);
218
+ const wayland = isWaylandSession(env);
219
+ if (wayland || wsl) {
220
+ image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
221
+ }
222
+ if (!image && wsl) {
223
+ image = readClipboardImageViaPowerShell();
224
+ }
225
+ if (!image && !wayland) {
226
+ image = await readClipboardImageViaNativeClipboard();
227
+ }
228
+ }
229
+ else {
230
+ image = await readClipboardImageViaNativeClipboard();
231
+ }
232
+ if (!image) {
233
+ return null;
234
+ }
235
+ // Convert unsupported formats (e.g., BMP from WSLg) to PNG
236
+ if (!isSupportedImageMimeType(image.mimeType)) {
237
+ const pngBytes = await convertToPng(image.bytes);
238
+ if (!pngBytes) {
239
+ return null;
240
+ }
241
+ return { bytes: pngBytes, mimeType: "image/png" };
242
+ }
243
+ return image;
244
+ }
@@ -0,0 +1,13 @@
1
+ import { createRequire } from "module";
2
+ const require = createRequire(import.meta.url);
3
+ let clipboard = null;
4
+ const hasDisplay = process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
5
+ if (!process.env.TERMUX_VERSION && hasDisplay) {
6
+ try {
7
+ clipboard = require("@mariozechner/clipboard");
8
+ }
9
+ catch {
10
+ clipboard = null;
11
+ }
12
+ }
13
+ export { clipboard };
@@ -0,0 +1,116 @@
1
+ import { execSync, spawn } from "child_process";
2
+ import { platform } from "os";
3
+ import { isWaylandSession } from "./clipboard-image.js";
4
+ import { clipboard } from "./clipboard-native.js";
5
+ function copyToX11Clipboard(options) {
6
+ try {
7
+ execSync("xclip -selection clipboard", options);
8
+ }
9
+ catch {
10
+ execSync("xsel --clipboard --input", options);
11
+ }
12
+ }
13
+ const MAX_OSC52_ENCODED_LENGTH = 100_000;
14
+ function isRemoteSession(env = process.env) {
15
+ return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);
16
+ }
17
+ function emitOsc52(text) {
18
+ const encoded = Buffer.from(text).toString("base64");
19
+ if (encoded.length > MAX_OSC52_ENCODED_LENGTH) {
20
+ return false;
21
+ }
22
+ process.stdout.write(`\x1b]52;c;${encoded}\x07`);
23
+ return true;
24
+ }
25
+ export async function copyToClipboard(text) {
26
+ let copied = false;
27
+ const p = platform();
28
+ // Prefer direct clipboard writes. Emitting OSC 52 first can make terminals
29
+ // write the same native clipboard concurrently with the addon, and very large
30
+ // OSC 52 payloads can desynchronize terminal rendering.
31
+ //
32
+ // On Linux, skip the native addon. The underlying `clipboard-rs` crate is
33
+ // X11-only and does not retain selection ownership after `set_text`
34
+ // resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even
35
+ // some X11 sessions the call resolves successfully without populating the
36
+ // clipboard. The platform tools below (wl-copy, xclip, xsel) properly
37
+ // daemonize and keep ownership.
38
+ try {
39
+ if (clipboard && p !== "linux") {
40
+ await clipboard.setText(text);
41
+ copied = true;
42
+ }
43
+ }
44
+ catch {
45
+ // Fall through to platform-specific clipboard tools.
46
+ }
47
+ const remote = isRemoteSession();
48
+ if (copied && !remote) {
49
+ return;
50
+ }
51
+ const options = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] };
52
+ if (!copied) {
53
+ try {
54
+ if (p === "darwin") {
55
+ execSync("pbcopy", options);
56
+ copied = true;
57
+ }
58
+ else if (p === "win32") {
59
+ execSync("clip", options);
60
+ copied = true;
61
+ }
62
+ else {
63
+ // Linux. Try Termux, Wayland, or X11 clipboard tools.
64
+ if (process.env.TERMUX_VERSION) {
65
+ try {
66
+ execSync("termux-clipboard-set", options);
67
+ copied = true;
68
+ }
69
+ catch {
70
+ // Fall back to Wayland or X11 tools.
71
+ }
72
+ }
73
+ if (!copied) {
74
+ const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
75
+ const hasX11Display = Boolean(process.env.DISPLAY);
76
+ const isWayland = isWaylandSession();
77
+ if (isWayland && hasWaylandDisplay) {
78
+ try {
79
+ // Verify wl-copy exists (spawn errors are async and won't be caught)
80
+ execSync("which wl-copy", { stdio: "ignore" });
81
+ // wl-copy with execSync hangs due to fork behavior; use spawn instead
82
+ const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] });
83
+ proc.stdin.on("error", () => {
84
+ // Ignore EPIPE errors if wl-copy exits early
85
+ });
86
+ proc.stdin.write(text);
87
+ proc.stdin.end();
88
+ proc.unref();
89
+ copied = true;
90
+ }
91
+ catch {
92
+ if (hasX11Display) {
93
+ copyToX11Clipboard(options);
94
+ copied = true;
95
+ }
96
+ }
97
+ }
98
+ else if (hasX11Display) {
99
+ copyToX11Clipboard(options);
100
+ copied = true;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch {
106
+ // Fall through to OSC 52 fallback.
107
+ }
108
+ }
109
+ if (remote || !copied) {
110
+ const osc52Copied = emitOsc52(text);
111
+ copied = copied || osc52Copied;
112
+ }
113
+ if (!copied) {
114
+ throw new Error("Failed to copy to clipboard");
115
+ }
116
+ }