@fitlab-ai/agent-infra 0.8.2 → 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.
- package/dist/bin/cli.js +1 -1
- package/dist/lib/cp.js +1 -1
- package/dist/lib/decide.js +2 -2
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/init.js +7 -7
- package/dist/lib/merge.js +1 -1
- package/dist/lib/prompt.js +1 -1
- package/dist/lib/run/index.js +7 -7
- package/dist/lib/run/prompt.js +1 -1
- package/dist/lib/sandbox/capture.js +5 -5
- package/dist/lib/sandbox/clipboard/bridge.js +48 -13
- package/dist/lib/sandbox/clipboard/index.js +5 -6
- package/dist/lib/sandbox/clipboard/linux.js +91 -0
- package/dist/lib/sandbox/clipboard/paths.js +1 -1
- package/dist/lib/sandbox/clipboard/win32.js +144 -0
- package/dist/lib/sandbox/commands/create.js +67 -65
- package/dist/lib/sandbox/commands/enter.js +9 -9
- package/dist/lib/sandbox/commands/list-running.js +2 -2
- package/dist/lib/sandbox/commands/ls.js +7 -7
- package/dist/lib/sandbox/commands/prune.js +8 -8
- package/dist/lib/sandbox/commands/rebuild.js +38 -50
- package/dist/lib/sandbox/commands/refresh.js +2 -2
- package/dist/lib/sandbox/commands/rm.js +11 -11
- package/dist/lib/sandbox/commands/show.js +4 -4
- package/dist/lib/sandbox/commands/start.js +4 -4
- package/dist/lib/sandbox/commands/vm.js +4 -4
- package/dist/lib/sandbox/config.js +10 -4
- package/dist/lib/sandbox/constants.js +4 -1
- package/dist/lib/sandbox/credentials.js +1 -1
- package/dist/lib/sandbox/dotfiles.js +1 -1
- package/dist/lib/sandbox/engine.js +3 -3
- package/dist/lib/sandbox/engines/index.js +5 -5
- package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
- package/dist/lib/sandbox/image-build.js +80 -0
- package/dist/lib/sandbox/image-prune.js +2 -2
- package/dist/lib/sandbox/index.js +10 -10
- package/dist/lib/sandbox/managed-fs.js +1 -1
- package/dist/lib/sandbox/readme-scaffold.js +1 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/dist/lib/server/adapters/feishu/index.js +10 -3
- package/dist/lib/server/adapters/feishu/renderer.js +88 -0
- package/dist/lib/server/adapters/feishu/transport.js +2 -13
- package/dist/lib/server/daemon.js +64 -43
- package/dist/lib/server/display.js +83 -0
- package/dist/lib/server/index.js +2 -2
- package/dist/lib/server/process-control.js +2 -2
- package/dist/lib/server/protocol.js +2 -2
- package/dist/lib/server/streamer.js +5 -4
- package/dist/lib/task/commands/cat.js +2 -2
- package/dist/lib/task/commands/decisions.js +4 -4
- package/dist/lib/task/commands/files.js +3 -3
- package/dist/lib/task/commands/grep.js +3 -3
- package/dist/lib/task/commands/issue-body.js +4 -4
- package/dist/lib/task/commands/log.js +5 -5
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/commands/show.js +1 -1
- package/dist/lib/task/commands/status.js +71 -25
- package/dist/lib/task/index.js +9 -9
- package/dist/lib/task/resolve-ref.js +1 -1
- package/dist/lib/task/workflow-warnings.js +94 -0
- package/dist/lib/update.js +4 -4
- package/lib/defaults.json +1 -0
- package/lib/sandbox/clipboard/bridge.ts +50 -8
- package/lib/sandbox/clipboard/index.ts +5 -6
- package/lib/sandbox/clipboard/linux.ts +124 -0
- package/lib/sandbox/clipboard/win32.ts +173 -0
- package/lib/sandbox/commands/create.ts +84 -64
- package/lib/sandbox/commands/rebuild.ts +42 -54
- package/lib/sandbox/config.ts +13 -1
- package/lib/sandbox/constants.ts +4 -0
- package/lib/sandbox/image-build.ts +134 -0
- package/lib/server/adapters/_contract.ts +3 -0
- package/lib/server/adapters/feishu/index.ts +11 -3
- package/lib/server/adapters/feishu/renderer.ts +99 -0
- package/lib/server/adapters/feishu/transport.ts +5 -18
- package/lib/server/daemon.ts +74 -38
- package/lib/server/display.ts +136 -0
- package/lib/server/streamer.ts +5 -4
- package/lib/task/commands/log.ts +3 -3
- package/lib/task/commands/status.ts +102 -21
- package/lib/task/workflow-warnings.ts +121 -0
- package/package.json +2 -2
- package/templates/.agents/rules/create-issue.github.en.md +3 -3
- package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
- package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
- package/templates/.agents/rules/issue-sync.github.en.md +13 -0
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
- package/templates/.agents/rules/next-step-output.en.md +15 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
- package/templates/.agents/rules/pr-sync.github.en.md +17 -2
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
- package/templates/.agents/scripts/validate-artifact.js +120 -0
- package/templates/.agents/scripts/workflow-warnings.js +290 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +118 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +118 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +7 -0
- package/templates/.agents/templates/task.en.md +7 -0
- package/templates/.agents/templates/task.zh-CN.md +7 -0
- package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
- package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
- package/templates/.github/workflows/metadata-sync.yml +5 -0
- package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
package/dist/bin/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
}
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
|
-
import { VERSION } from
|
|
10
|
+
import { VERSION } from '../lib/version.js';
|
|
11
11
|
// Node.js version check
|
|
12
12
|
const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
|
|
13
13
|
if (major < 22 || (major === 22 && minor < 9)) {
|
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
|
|
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) {
|
package/dist/lib/decide.js
CHANGED
|
@@ -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
|
|
5
|
-
import { resolveTaskRef } from
|
|
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'], {
|
package/dist/lib/defaults.json
CHANGED
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
|
|
6
|
-
import { prompt, select, multiSelect, closePrompt } from
|
|
7
|
-
import { resolveTemplateDir } from
|
|
8
|
-
import { renderFile, copySkillDir, KNOWN_PLATFORMS } from
|
|
9
|
-
import { enginesForPlatform } from
|
|
10
|
-
import { VERSION } from
|
|
11
|
-
import { BUILTIN_TUI_IDS, BUILTIN_TUI_DISPLAY, isPathOwnedByDisabledTUI, resolveEnabledTUIs } from
|
|
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
|
|
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;
|
package/dist/lib/prompt.js
CHANGED
package/dist/lib/run/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { loadServerConfig } from
|
|
4
|
-
import { resolveTaskBranch } from
|
|
5
|
-
import { createRunId, runInSandbox } from
|
|
6
|
-
import { loadShortIdByTaskId, normalizeShortIdInput } from
|
|
7
|
-
import { buildTuiCommand, renderPrompt, selectTui } from
|
|
8
|
-
import { getSkillRunSpec } from
|
|
9
|
-
import { runHostCommand } from
|
|
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
|
package/dist/lib/run/prompt.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { renderPrompt } from
|
|
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
|
|
3
|
-
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from
|
|
4
|
-
import { detectEngine } from
|
|
5
|
-
import { hostTimezoneEnvFlags, terminalEnvFlags } from
|
|
6
|
-
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from
|
|
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
|
|
4
|
-
import { buildBracketedPaste, CtrlVDetector } from
|
|
5
|
-
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from
|
|
6
|
-
import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from
|
|
7
|
-
import { loadNodePty } from
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9
|
-
// see Issue #386 follow-up. Returning null disables the bridge for now.
|
|
10
|
-
return null;
|
|
10
|
+
return createLinuxClipboardAdapter();
|
|
11
11
|
case 'win32':
|
|
12
|
-
|
|
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
|
|
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
|