@fitlab-ai/agent-infra 0.8.1 → 0.8.3

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 (131) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/cli.ts +3 -3
  4. package/dist/bin/cli.js +4 -4
  5. package/dist/lib/cp.js +1 -1
  6. package/dist/lib/decide.js +2 -2
  7. package/dist/lib/defaults.json +2 -0
  8. package/dist/lib/init.js +7 -7
  9. package/dist/lib/merge.js +1 -1
  10. package/dist/lib/prompt.js +1 -1
  11. package/dist/lib/run/index.js +7 -7
  12. package/dist/lib/run/prompt.js +1 -1
  13. package/dist/lib/sandbox/capture.js +5 -5
  14. package/dist/lib/sandbox/clipboard/bridge.js +48 -13
  15. package/dist/lib/sandbox/clipboard/index.js +5 -6
  16. package/dist/lib/sandbox/clipboard/linux.js +91 -0
  17. package/dist/lib/sandbox/clipboard/paths.js +1 -1
  18. package/dist/lib/sandbox/clipboard/win32.js +144 -0
  19. package/dist/lib/sandbox/commands/create.js +101 -66
  20. package/dist/lib/sandbox/commands/enter.js +9 -9
  21. package/dist/lib/sandbox/commands/list-running.js +2 -2
  22. package/dist/lib/sandbox/commands/ls.js +7 -7
  23. package/dist/lib/sandbox/commands/prune.js +8 -8
  24. package/dist/lib/sandbox/commands/rebuild.js +38 -50
  25. package/dist/lib/sandbox/commands/refresh.js +2 -2
  26. package/dist/lib/sandbox/commands/rm.js +11 -11
  27. package/dist/lib/sandbox/commands/show.js +4 -4
  28. package/dist/lib/sandbox/commands/start.js +4 -4
  29. package/dist/lib/sandbox/commands/vm.js +4 -4
  30. package/dist/lib/sandbox/config.js +11 -5
  31. package/dist/lib/sandbox/constants.js +4 -1
  32. package/dist/lib/sandbox/credentials.js +1 -1
  33. package/dist/lib/sandbox/dotfiles.js +1 -1
  34. package/dist/lib/sandbox/engine.js +3 -3
  35. package/dist/lib/sandbox/engines/index.js +5 -5
  36. package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
  37. package/dist/lib/sandbox/image-build.js +80 -0
  38. package/dist/lib/sandbox/image-prune.js +2 -2
  39. package/dist/lib/sandbox/index.js +10 -10
  40. package/dist/lib/sandbox/managed-fs.js +1 -1
  41. package/dist/lib/sandbox/readme-scaffold.js +1 -1
  42. package/dist/lib/sandbox/runtimes/node20.dockerfile +1 -1
  43. package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
  44. package/dist/lib/sandbox/tools.js +11 -2
  45. package/dist/lib/server/adapters/feishu/index.js +12 -3
  46. package/dist/lib/server/adapters/feishu/renderer.js +88 -0
  47. package/dist/lib/server/adapters/feishu/transport.js +9 -2
  48. package/dist/lib/server/daemon.js +64 -43
  49. package/dist/lib/server/display.js +83 -0
  50. package/dist/lib/server/index.js +2 -2
  51. package/dist/lib/server/process-control.js +2 -2
  52. package/dist/lib/server/protocol.js +22 -6
  53. package/dist/lib/server/streamer.js +5 -4
  54. package/dist/lib/task/commands/cat.js +2 -2
  55. package/dist/lib/task/commands/decisions.js +4 -4
  56. package/dist/lib/task/commands/files.js +3 -3
  57. package/dist/lib/task/commands/grep.js +3 -3
  58. package/dist/lib/task/commands/issue-body.js +4 -4
  59. package/dist/lib/task/commands/log.js +5 -5
  60. package/dist/lib/task/commands/ls.js +3 -3
  61. package/dist/lib/task/commands/show.js +1 -1
  62. package/dist/lib/task/commands/status.js +71 -25
  63. package/dist/lib/task/index.js +9 -9
  64. package/dist/lib/task/resolve-ref.js +1 -1
  65. package/dist/lib/task/workflow-warnings.js +94 -0
  66. package/dist/lib/update.js +35 -4
  67. package/lib/defaults.json +2 -0
  68. package/lib/sandbox/clipboard/bridge.ts +50 -8
  69. package/lib/sandbox/clipboard/index.ts +5 -6
  70. package/lib/sandbox/clipboard/linux.ts +124 -0
  71. package/lib/sandbox/clipboard/win32.ts +173 -0
  72. package/lib/sandbox/commands/create.ts +123 -65
  73. package/lib/sandbox/commands/rebuild.ts +42 -54
  74. package/lib/sandbox/config.ts +14 -2
  75. package/lib/sandbox/constants.ts +4 -0
  76. package/lib/sandbox/image-build.ts +134 -0
  77. package/lib/sandbox/runtimes/node20.dockerfile +1 -1
  78. package/lib/sandbox/runtimes/node22.dockerfile +1 -1
  79. package/lib/sandbox/tools.ts +9 -0
  80. package/lib/server/adapters/_contract.ts +3 -0
  81. package/lib/server/adapters/feishu/index.ts +12 -2
  82. package/lib/server/adapters/feishu/renderer.ts +99 -0
  83. package/lib/server/adapters/feishu/transport.ts +19 -3
  84. package/lib/server/daemon.ts +74 -38
  85. package/lib/server/display.ts +136 -0
  86. package/lib/server/protocol.ts +20 -4
  87. package/lib/server/streamer.ts +5 -4
  88. package/lib/task/commands/log.ts +3 -3
  89. package/lib/task/commands/status.ts +102 -21
  90. package/lib/task/workflow-warnings.ts +121 -0
  91. package/lib/update.ts +34 -0
  92. package/package.json +3 -3
  93. package/templates/.agents/README.en.md +3 -3
  94. package/templates/.agents/README.zh-CN.md +3 -3
  95. package/templates/.agents/rules/create-issue.github.en.md +3 -3
  96. package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
  97. package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
  98. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
  99. package/templates/.agents/rules/issue-sync.github.en.md +13 -0
  100. package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
  101. package/templates/.agents/rules/next-step-output.en.md +15 -1
  102. package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
  103. package/templates/.agents/rules/pr-sync.github.en.md +17 -2
  104. package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
  105. package/templates/.agents/rules/task-management.en.md +1 -1
  106. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  107. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
  108. package/templates/.agents/scripts/validate-artifact.js +120 -0
  109. package/templates/.agents/scripts/workflow-warnings.js +290 -0
  110. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +118 -0
  111. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +118 -0
  112. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
  113. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
  114. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
  115. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
  116. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
  117. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
  118. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  119. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -0
  120. package/templates/.agents/skills/create-task/SKILL.en.md +4 -1
  121. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -1
  122. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +33 -1
  123. package/templates/.agents/templates/task.en.md +7 -0
  124. package/templates/.agents/templates/task.zh-CN.md +7 -0
  125. package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
  126. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
  127. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
  128. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
  129. package/templates/.github/workflows/metadata-sync.yml +5 -0
  130. package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
  131. package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
17
17
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
18
18
  <a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
19
- <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
19
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22.9.0-brightgreen?logo=node.js" alt="Node.js >= 22.9.0"></a>
20
20
  <a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
21
21
  <a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
22
22
  <a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
package/README.zh-CN.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
17
17
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
18
18
  <a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
19
- <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
19
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22.9.0-brightgreen?logo=node.js" alt="Node.js >= 22.9.0"></a>
20
20
  <a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
21
21
  <a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
22
22
  <a href="CONTRIBUTING.zh-CN.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
package/bin/cli.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  import { VERSION } from '../lib/version.ts';
3
3
 
4
4
  // Node.js version check
5
- const [major = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
6
- if (major < 22) {
5
+ const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
6
+ if (major < 22 || (major === 22 && minor < 9)) {
7
7
  process.stderr.write(
8
- `agent-infra requires Node.js >= 22 (current: ${process.version})\n`
8
+ `agent-infra requires Node.js >= 22.9.0 (current: ${process.version})\n`
9
9
  );
10
10
  process.exit(1);
11
11
  }
package/dist/bin/cli.js CHANGED
@@ -7,11 +7,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  }
8
8
  return path;
9
9
  };
10
- import { VERSION } from "../lib/version.js";
10
+ import { VERSION } from '../lib/version.js';
11
11
  // Node.js version check
12
- const [major = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
13
- if (major < 22) {
14
- process.stderr.write(`agent-infra requires Node.js >= 22 (current: ${process.version})\n`);
12
+ const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
13
+ if (major < 22 || (major === 22 && minor < 9)) {
14
+ process.stderr.write(`agent-infra requires Node.js >= 22.9.0 (current: ${process.version})\n`);
15
15
  process.exit(1);
16
16
  }
17
17
  const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
package/dist/lib/cp.js CHANGED
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
- import { createClipboardAdapter } from "./sandbox/clipboard/index.js";
7
+ import { createClipboardAdapter } from './sandbox/clipboard/index.js';
8
8
  const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS NSPasteboard over ssh/scp.\n';
9
9
  const COMMAND_TIMEOUT_MS = 30_000;
10
10
  export function runCommand(cmd, args, input) {
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
- import { VERSION } from "./version.js";
5
- import { resolveTaskRef } from "./task/resolve-ref.js";
4
+ import { VERSION } from './version.js';
5
+ import { resolveTaskRef } from './task/resolve-ref.js';
6
6
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
7
7
  function detectRepoRoot() {
8
8
  return execFileSync('git', ['rev-parse', '--show-toplevel'], {
@@ -8,11 +8,13 @@
8
8
  "node22"
9
9
  ],
10
10
  "tools": [
11
+ "agent-infra",
11
12
  "claude-code",
12
13
  "codex",
13
14
  "gemini-cli",
14
15
  "opencode"
15
16
  ],
17
+ "refreshIntervalDays": 7,
16
18
  "dockerfile": null,
17
19
  "vm": {
18
20
  "cpu": null,
package/dist/lib/init.js CHANGED
@@ -2,13 +2,13 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { platform } from 'node:os';
5
- import { info, ok, err } from "./log.js";
6
- import { prompt, select, multiSelect, closePrompt } from "./prompt.js";
7
- import { resolveTemplateDir } from "./paths.js";
8
- import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
9
- import { enginesForPlatform } from "./sandbox/engines/index.js";
10
- import { VERSION } from "./version.js";
11
- import { BUILTIN_TUI_IDS, BUILTIN_TUI_DISPLAY, isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
5
+ import { info, ok, err } from './log.js';
6
+ import { prompt, select, multiSelect, closePrompt } from './prompt.js';
7
+ import { resolveTemplateDir } from './paths.js';
8
+ import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.js';
9
+ import { enginesForPlatform } from './sandbox/engines/index.js';
10
+ import { VERSION } from './version.js';
11
+ import { BUILTIN_TUI_IDS, BUILTIN_TUI_DISPLAY, isPathOwnedByDisabledTUI, resolveEnabledTUIs } from './builtin-tuis.js';
12
12
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
13
13
  const PLATFORM_DEFAULT_ENGINES = Object.freeze({
14
14
  linux: 'native',
package/dist/lib/merge.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { info, ok } from "./log.js";
3
+ import { info, ok } from './log.js';
4
4
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
5
5
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
6
6
  const TITLE_RE = /^# (.+)$/m;
@@ -1,5 +1,5 @@
1
1
  import readline from 'node:readline';
2
- import { ask } from "./log.js";
2
+ import { ask } from './log.js';
3
3
  let _rl = null;
4
4
  let _lines = [];
5
5
  let _lineResolve = null;
@@ -1,12 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { loadServerConfig } from "../server/config.js";
4
- import { resolveTaskBranch } from "../sandbox/task-resolver.js";
5
- import { createRunId, runInSandbox } from "../sandbox/capture.js";
6
- import { loadShortIdByTaskId, normalizeShortIdInput } from "../task/short-id.js";
7
- import { buildTuiCommand, renderPrompt, selectTui } from "./tui.js";
8
- import { getSkillRunSpec } from "./skills.js";
9
- import { runHostCommand } from "./host.js";
3
+ import { loadServerConfig } from '../server/config.js';
4
+ import { resolveTaskBranch } from '../sandbox/task-resolver.js';
5
+ import { createRunId, runInSandbox } from '../sandbox/capture.js';
6
+ import { loadShortIdByTaskId, normalizeShortIdInput } from '../task/short-id.js';
7
+ import { buildTuiCommand, renderPrompt, selectTui } from './tui.js';
8
+ import { getSkillRunSpec } from './skills.js';
9
+ import { runHostCommand } from './host.js';
10
10
  const USAGE = `Usage: ai run <skill> [task-ref] [args...] [--tui <name>]
11
11
 
12
12
  Task skills are scheduled inside the sandbox tmux session; ai run returns once
@@ -1,2 +1,2 @@
1
- export { renderPrompt } from "./tui.js";
1
+ export { renderPrompt } from './tui.js';
2
2
  //# sourceMappingURL=prompt.js.map
@@ -1,9 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { loadConfig } from "./config.js";
3
- import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "./constants.js";
4
- import { detectEngine } from "./engine.js";
5
- import { hostTimezoneEnvFlags, terminalEnvFlags } from "./commands/enter.js";
6
- import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from "./commands/list-running.js";
2
+ import { loadConfig } from './config.js';
3
+ import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.js';
4
+ import { detectEngine } from './engine.js';
5
+ import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.js';
6
+ import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
7
7
  async function spawnCapture(file, args) {
8
8
  return new Promise((resolve, reject) => {
9
9
  const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -1,12 +1,13 @@
1
1
  import { StringDecoder } from 'node:string_decoder';
2
2
  import { spawnSync } from 'node:child_process';
3
- import { createClipboardAdapter } from "./index.js";
4
- import { buildBracketedPaste, CtrlVDetector } from "./keys.js";
5
- import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from "./paths.js";
6
- import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from "../shell.js";
7
- import { loadNodePty } from "./node-pty.js";
3
+ import { createClipboardAdapter } from './index.js';
4
+ import { buildBracketedPaste, CtrlVDetector } from './keys.js';
5
+ import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from './paths.js';
6
+ import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from '../shell.js';
7
+ import { loadNodePty } from './node-pty.js';
8
8
  const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
9
9
  const PARTIAL_ESCAPE_FLUSH_MS = 30;
10
+ const BRACKETED_PASTE_RE = /^\x1b\[200~([\s\S]*)\x1b\[201~$/u;
10
11
  // Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
11
12
  // cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
12
13
  // set on the shared host TTY. With ONLCR on, the kernel rewrites the bare \n
@@ -83,7 +84,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
83
84
  clearFlushTimer();
84
85
  for (const token of detector.feed(inputDecoder.write(chunk))) {
85
86
  if (token.kind === 'text') {
86
- child.write(token.raw);
87
+ handleText(token.raw, child);
87
88
  }
88
89
  else {
89
90
  handleCtrlV(token, child);
@@ -94,7 +95,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
94
95
  flushTimer = null;
95
96
  for (const token of detector.flush()) {
96
97
  if (token.kind === 'text') {
97
- child.write(token.raw);
98
+ handleText(token.raw, child);
98
99
  }
99
100
  else {
100
101
  handleCtrlV(token, child);
@@ -106,6 +107,35 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
106
107
  const onResize = () => child.resize(stdout.columns || 120, stdout.rows || 40);
107
108
  const onSigint = () => child.kill('SIGINT');
108
109
  const onSigterm = () => child.kill('SIGTERM');
110
+ function writeImagePaste(png, target) {
111
+ const filename = pngClipboardFilename(png);
112
+ writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
113
+ pruneClipboardDir(clipboardHostDir(home));
114
+ target.write(buildBracketedPaste(containerClipboardPath(filename)));
115
+ }
116
+ function handleText(raw, target) {
117
+ const textAdapter = adapter;
118
+ const pastedText = extractSinglePathPaste(raw);
119
+ if (!textAdapter.readImageFromText || pastedText === null) {
120
+ target.write(raw);
121
+ return;
122
+ }
123
+ try {
124
+ const png = textAdapter.readImageFromText(pastedText);
125
+ if (!png) {
126
+ target.write(raw);
127
+ return;
128
+ }
129
+ writeImagePaste(png, target);
130
+ }
131
+ catch (error) {
132
+ target.write(raw);
133
+ if (!warnedPasteFailure) {
134
+ warnedPasteFailure = true;
135
+ writeStderr(`Warning: clipboard image paste failed; forwarded pasted text (${error instanceof Error ? error.message : 'unknown error'})\n`);
136
+ }
137
+ }
138
+ }
109
139
  function handleCtrlV(match, target) {
110
140
  try {
111
141
  // readImagePng returns null both for "no image on clipboard" and for
@@ -118,10 +148,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
118
148
  target.write(match.raw);
119
149
  return;
120
150
  }
121
- const filename = pngClipboardFilename(png);
122
- writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
123
- pruneClipboardDir(clipboardHostDir(home));
124
- target.write(buildBracketedPaste(containerClipboardPath(filename)));
151
+ writeImagePaste(png, target);
125
152
  }
126
153
  catch (error) {
127
154
  target.write(match.raw);
@@ -155,7 +182,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
155
182
  try {
156
183
  for (const token of detector.feed(inputDecoder.end())) {
157
184
  if (token.kind === 'text') {
158
- child.write(token.raw);
185
+ handleText(token.raw, child);
159
186
  }
160
187
  else {
161
188
  handleCtrlV(token, child);
@@ -163,7 +190,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
163
190
  }
164
191
  for (const token of detector.flush()) {
165
192
  if (token.kind === 'text') {
166
- child.write(token.raw);
193
+ handleText(token.raw, child);
167
194
  }
168
195
  }
169
196
  }
@@ -181,6 +208,14 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
181
208
  restoreTerminal();
182
209
  }
183
210
  }
211
+ function extractSinglePathPaste(raw) {
212
+ const match = raw.match(BRACKETED_PASTE_RE);
213
+ const text = (match ? match[1] : raw)?.trim();
214
+ if (!text || /[\r\n]/u.test(text)) {
215
+ return null;
216
+ }
217
+ return text;
218
+ }
184
219
  function onceExit(child, stdin) {
185
220
  return new Promise((resolve) => {
186
221
  let settled = false;
@@ -1,16 +1,15 @@
1
1
  import { platform } from 'node:os';
2
- import { createDarwinClipboardAdapter } from "./darwin.js";
2
+ import { createDarwinClipboardAdapter } from './darwin.js';
3
+ import { createLinuxClipboardAdapter } from './linux.js';
4
+ import { createWin32ClipboardAdapter } from './win32.js';
3
5
  export function createClipboardAdapter({ platformName = platform() } = {}) {
4
6
  switch (platformName) {
5
7
  case 'darwin':
6
8
  return createDarwinClipboardAdapter();
7
9
  case 'linux':
8
- // Future work: dispatch based on $WAYLAND_DISPLAY (wl-paste) or $DISPLAY (xclip);
9
- // see Issue #386 follow-up. Returning null disables the bridge for now.
10
- return null;
10
+ return createLinuxClipboardAdapter();
11
11
  case 'win32':
12
- // Future work: native Win32 clipboard reader. Returning null disables the bridge.
13
- return null;
12
+ return createWin32ClipboardAdapter();
14
13
  default:
15
14
  return null;
16
15
  }
@@ -0,0 +1,91 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ const PROBE_TIMEOUT_MS = 2_000;
6
+ const READ_IMAGE_TIMEOUT_MS = 5_000;
7
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
8
+ export function createLinuxClipboardAdapter({ env = process.env, execFn = execFileSync, execToFileFn = execToFile, mkdtempFn = fs.mkdtempSync, readFileFn = fs.readFileSync, rmFn = fs.rmSync } = {}) {
9
+ return {
10
+ available() {
11
+ const backend = selectBackend(env);
12
+ try {
13
+ execFn(backend.command, versionArgs(backend), {
14
+ encoding: 'utf8',
15
+ timeout: PROBE_TIMEOUT_MS
16
+ });
17
+ return { ok: true };
18
+ }
19
+ catch {
20
+ return { ok: false, reason: unavailableReason(backend) };
21
+ }
22
+ },
23
+ readImagePng() {
24
+ const backend = selectBackend(env);
25
+ try {
26
+ const mimeTypes = String(execFn(backend.command, mimeArgs(backend), {
27
+ encoding: 'utf8',
28
+ timeout: PROBE_TIMEOUT_MS
29
+ }));
30
+ if (!hasPngMime(mimeTypes)) {
31
+ return null;
32
+ }
33
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
34
+ const outputPath = path.join(tmpDir, 'clipboard.png');
35
+ try {
36
+ execToFileFn(backend.command, imageArgs(backend), outputPath, READ_IMAGE_TIMEOUT_MS);
37
+ const png = Buffer.from(readFileFn(outputPath));
38
+ return isPng(png) ? png : null;
39
+ }
40
+ finally {
41
+ rmFn(tmpDir, { recursive: true, force: true });
42
+ }
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ };
49
+ }
50
+ function selectBackend(env) {
51
+ return env.WAYLAND_DISPLAY?.trim()
52
+ ? { kind: 'wayland', command: 'wl-paste' }
53
+ : { kind: 'x11', command: 'xclip' };
54
+ }
55
+ function versionArgs(backend) {
56
+ return backend.kind === 'wayland' ? ['--version'] : ['-version'];
57
+ }
58
+ function mimeArgs(backend) {
59
+ return backend.kind === 'wayland'
60
+ ? ['--list-types']
61
+ : ['-selection', 'clipboard', '-t', 'TARGETS', '-o'];
62
+ }
63
+ function imageArgs(backend) {
64
+ return backend.kind === 'wayland'
65
+ ? ['-t', 'image/png']
66
+ : ['-selection', 'clipboard', '-t', 'image/png', '-o'];
67
+ }
68
+ function unavailableReason(backend) {
69
+ return backend.kind === 'wayland'
70
+ ? 'Wayland clipboard tool wl-paste is unavailable; install wl-clipboard to enable image paste'
71
+ : 'X11 clipboard tool xclip is unavailable; install xclip to enable image paste';
72
+ }
73
+ function hasPngMime(output) {
74
+ return output.split(/\s+/u).some((type) => type === 'image/png');
75
+ }
76
+ function execToFile(cmd, args, outputPath, timeout) {
77
+ const fd = fs.openSync(outputPath, 'w', 0o600);
78
+ try {
79
+ execFileSync(cmd, args, {
80
+ timeout,
81
+ stdio: ['ignore', fd, 'pipe']
82
+ });
83
+ }
84
+ finally {
85
+ fs.closeSync(fd);
86
+ }
87
+ }
88
+ function isPng(buffer) {
89
+ return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
90
+ }
91
+ //# sourceMappingURL=linux.js.map
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { hostJoin } from "../engines/wsl2-paths.js";
4
+ import { hostJoin } from '../engines/wsl2-paths.js';
5
5
  export const CONTAINER_CLIPBOARD_MOUNT = '/clipboard';
6
6
  const DEFAULT_KEEP = 20;
7
7
  const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
@@ -0,0 +1,144 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ const PROBE_TIMEOUT_MS = 2_000;
6
+ const READ_IMAGE_TIMEOUT_MS = 5_000;
7
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
8
+ const POWERSHELL_ARGS = ['-NoProfile', '-NonInteractive', '-STA', '-Command'];
9
+ export function createWin32ClipboardAdapter({ execFn = execFileSync, mkdtempFn = fs.mkdtempSync, readFileFn = fs.readFileSync, rmFn = fs.rmSync, existsFn = fs.existsSync } = {}) {
10
+ return {
11
+ available() {
12
+ try {
13
+ execFn('powershell.exe', [...POWERSHELL_ARGS, clipboardProbeScript()], {
14
+ encoding: 'utf8',
15
+ timeout: PROBE_TIMEOUT_MS
16
+ });
17
+ return { ok: true };
18
+ }
19
+ catch {
20
+ return {
21
+ ok: false,
22
+ reason: 'Windows PowerShell Get-Clipboard is unavailable; install or enable powershell.exe to use image paste'
23
+ };
24
+ }
25
+ },
26
+ readImagePng() {
27
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
28
+ const outputPath = path.join(tmpDir, 'clipboard.png');
29
+ try {
30
+ execFn('powershell.exe', [...POWERSHELL_ARGS, pngWriteScript(outputPath)], {
31
+ encoding: 'utf8',
32
+ timeout: READ_IMAGE_TIMEOUT_MS
33
+ });
34
+ const png = Buffer.from(readFileFn(outputPath));
35
+ return isPng(png) ? png : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ finally {
41
+ rmFn(tmpDir, { recursive: true, force: true });
42
+ }
43
+ },
44
+ readImageFromPath(imagePath) {
45
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
46
+ const outputPath = path.join(tmpDir, 'clipboard.png');
47
+ try {
48
+ execFn('powershell.exe', [...POWERSHELL_ARGS, pngFromPathScript(imagePath, outputPath)], {
49
+ encoding: 'utf8',
50
+ timeout: READ_IMAGE_TIMEOUT_MS
51
+ });
52
+ const png = Buffer.from(readFileFn(outputPath));
53
+ return isPng(png) ? png : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ finally {
59
+ rmFn(tmpDir, { recursive: true, force: true });
60
+ }
61
+ },
62
+ readImageFromText(text) {
63
+ const trimmed = text.trim();
64
+ if (!isWindowsImagePath(trimmed)) {
65
+ return null;
66
+ }
67
+ if (!existsFn(trimmed)) {
68
+ return null;
69
+ }
70
+ return this.readImageFromPath(trimmed);
71
+ }
72
+ };
73
+ }
74
+ function clipboardProbeScript() {
75
+ return 'if ($null -eq (Get-Command Get-Clipboard -ErrorAction SilentlyContinue)) { exit 1 }';
76
+ }
77
+ // Embed a filesystem path in a PowerShell single-quoted string literal.
78
+ // Single-quoted strings in PowerShell are verbatim (no variable expansion,
79
+ // no escape sequences). The only special character is a single quote itself,
80
+ // which is escaped by doubling it.
81
+ function psLiteral(value) {
82
+ return `'${value.replace(/'/g, "''")}'`;
83
+ }
84
+ function pngWriteScript(outputPath) {
85
+ const psOutputPath = psLiteral(outputPath);
86
+ return [
87
+ '$ErrorActionPreference = "Stop"',
88
+ `$outputPath = ${psOutputPath}`,
89
+ '$image = $null',
90
+ 'try {',
91
+ ' Add-Type -AssemblyName System.Drawing',
92
+ ' $image = Get-Clipboard -Format Image -ErrorAction SilentlyContinue',
93
+ ' if ($null -ne $image) {',
94
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
95
+ ' return',
96
+ ' }',
97
+ ' $files = Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue',
98
+ ' if ($null -ne $files) {',
99
+ ' $allowed = @(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".ico")',
100
+ ' foreach ($file in $files) {',
101
+ ' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue }',
102
+ ' $ext = [System.IO.Path]::GetExtension($file)',
103
+ ' if ($allowed -notcontains $ext) { continue }',
104
+ ' try {',
105
+ ' $image = [System.Drawing.Image]::FromFile($file)',
106
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
107
+ ' return',
108
+ ' } catch {}',
109
+ ' }',
110
+ ' }',
111
+ ' throw "clipboard has no image"',
112
+ '} finally {',
113
+ ' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
114
+ '}'
115
+ ].join('\n');
116
+ }
117
+ function pngFromPathScript(imagePath, outputPath) {
118
+ const psImagePath = psLiteral(imagePath);
119
+ const psOutputPath = psLiteral(outputPath);
120
+ return [
121
+ '$ErrorActionPreference = "Stop"',
122
+ `$imagePath = ${psImagePath}`,
123
+ `$outputPath = ${psOutputPath}`,
124
+ '$image = $null',
125
+ 'try {',
126
+ ' Add-Type -AssemblyName System.Drawing',
127
+ ' if (-not (Test-Path -LiteralPath $imagePath -PathType Leaf)) { throw "file not found: $imagePath" }',
128
+ ' $image = [System.Drawing.Image]::FromFile($imagePath)',
129
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
130
+ '} finally {',
131
+ ' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
132
+ '}'
133
+ ].join('\n');
134
+ }
135
+ const IMAGE_EXTENSIONS_RE = /\.(png|jpg|jpeg|gif|bmp|webp|tiff?|ico)$/i;
136
+ function isWindowsImagePath(text) {
137
+ // Must be a Windows absolute path: drive letter + colon + backslash/slash
138
+ // followed by a path ending with a known image extension.
139
+ return /^[A-Za-z]:[\\/].+$/.test(text) && IMAGE_EXTENSIONS_RE.test(text);
140
+ }
141
+ function isPng(buffer) {
142
+ return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
143
+ }
144
+ //# sourceMappingURL=win32.js.map