@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/cli.ts +3 -3
- package/dist/bin/cli.js +4 -4
- package/dist/lib/cp.js +1 -1
- package/dist/lib/decide.js +2 -2
- package/dist/lib/defaults.json +2 -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 +101 -66
- 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 +11 -5
- 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/runtimes/node20.dockerfile +1 -1
- package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/dist/lib/sandbox/tools.js +11 -2
- package/dist/lib/server/adapters/feishu/index.js +12 -3
- package/dist/lib/server/adapters/feishu/renderer.js +88 -0
- package/dist/lib/server/adapters/feishu/transport.js +9 -2
- 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 +22 -6
- 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 +35 -4
- package/lib/defaults.json +2 -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 +123 -65
- package/lib/sandbox/commands/rebuild.ts +42 -54
- package/lib/sandbox/config.ts +14 -2
- package/lib/sandbox/constants.ts +4 -0
- package/lib/sandbox/image-build.ts +134 -0
- package/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/lib/sandbox/tools.ts +9 -0
- package/lib/server/adapters/_contract.ts +3 -0
- package/lib/server/adapters/feishu/index.ts +12 -2
- package/lib/server/adapters/feishu/renderer.ts +99 -0
- package/lib/server/adapters/feishu/transport.ts +19 -3
- package/lib/server/daemon.ts +74 -38
- package/lib/server/display.ts +136 -0
- package/lib/server/protocol.ts +20 -4
- 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/lib/update.ts +34 -0
- package/package.json +3 -3
- package/templates/.agents/README.en.md +3 -3
- package/templates/.agents/README.zh-CN.md +3 -3
- 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 +33 -1
- 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
|
@@ -30,8 +30,13 @@ type BridgeOptions = {
|
|
|
30
30
|
createDetector?: () => CtrlVDetector;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
type TextImageClipboardAdapter = ClipboardAdapter & {
|
|
34
|
+
readImageFromText?: (text: string) => Buffer | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
33
37
|
const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
|
|
34
38
|
const PARTIAL_ESCAPE_FLUSH_MS = 30;
|
|
39
|
+
const BRACKETED_PASTE_RE = /^\x1b\[200~([\s\S]*)\x1b\[201~$/u;
|
|
35
40
|
|
|
36
41
|
// Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
|
|
37
42
|
// cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
|
|
@@ -147,7 +152,7 @@ async function runBridge({
|
|
|
147
152
|
clearFlushTimer();
|
|
148
153
|
for (const token of detector.feed(inputDecoder.write(chunk))) {
|
|
149
154
|
if (token.kind === 'text') {
|
|
150
|
-
|
|
155
|
+
handleText(token.raw, child);
|
|
151
156
|
} else {
|
|
152
157
|
handleCtrlV(token, child);
|
|
153
158
|
}
|
|
@@ -157,7 +162,7 @@ async function runBridge({
|
|
|
157
162
|
flushTimer = null;
|
|
158
163
|
for (const token of detector.flush()) {
|
|
159
164
|
if (token.kind === 'text') {
|
|
160
|
-
|
|
165
|
+
handleText(token.raw, child);
|
|
161
166
|
} else {
|
|
162
167
|
handleCtrlV(token, child);
|
|
163
168
|
}
|
|
@@ -169,6 +174,37 @@ async function runBridge({
|
|
|
169
174
|
const onSigint = () => child.kill('SIGINT');
|
|
170
175
|
const onSigterm = () => child.kill('SIGTERM');
|
|
171
176
|
|
|
177
|
+
function writeImagePaste(png: Buffer, target: PtyProcess): void {
|
|
178
|
+
const filename = pngClipboardFilename(png);
|
|
179
|
+
writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
|
|
180
|
+
pruneClipboardDir(clipboardHostDir(home));
|
|
181
|
+
target.write(buildBracketedPaste(containerClipboardPath(filename)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleText(raw: string, target: PtyProcess): void {
|
|
185
|
+
const textAdapter = adapter as TextImageClipboardAdapter;
|
|
186
|
+
const pastedText = extractSinglePathPaste(raw);
|
|
187
|
+
if (!textAdapter.readImageFromText || pastedText === null) {
|
|
188
|
+
target.write(raw);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const png = textAdapter.readImageFromText(pastedText);
|
|
194
|
+
if (!png) {
|
|
195
|
+
target.write(raw);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
writeImagePaste(png, target);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
target.write(raw);
|
|
201
|
+
if (!warnedPasteFailure) {
|
|
202
|
+
warnedPasteFailure = true;
|
|
203
|
+
writeStderr(`Warning: clipboard image paste failed; forwarded pasted text (${error instanceof Error ? error.message : 'unknown error'})\n`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
172
208
|
function handleCtrlV(match: CtrlVMatch, target: PtyProcess): void {
|
|
173
209
|
try {
|
|
174
210
|
// readImagePng returns null both for "no image on clipboard" and for
|
|
@@ -181,10 +217,7 @@ async function runBridge({
|
|
|
181
217
|
target.write(match.raw);
|
|
182
218
|
return;
|
|
183
219
|
}
|
|
184
|
-
|
|
185
|
-
writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
|
|
186
|
-
pruneClipboardDir(clipboardHostDir(home));
|
|
187
|
-
target.write(buildBracketedPaste(containerClipboardPath(filename)));
|
|
220
|
+
writeImagePaste(png, target);
|
|
188
221
|
} catch (error) {
|
|
189
222
|
target.write(match.raw);
|
|
190
223
|
if (!warnedPasteFailure) {
|
|
@@ -219,14 +252,14 @@ async function runBridge({
|
|
|
219
252
|
try {
|
|
220
253
|
for (const token of detector.feed(inputDecoder.end())) {
|
|
221
254
|
if (token.kind === 'text') {
|
|
222
|
-
|
|
255
|
+
handleText(token.raw, child);
|
|
223
256
|
} else {
|
|
224
257
|
handleCtrlV(token, child);
|
|
225
258
|
}
|
|
226
259
|
}
|
|
227
260
|
for (const token of detector.flush()) {
|
|
228
261
|
if (token.kind === 'text') {
|
|
229
|
-
|
|
262
|
+
handleText(token.raw, child);
|
|
230
263
|
}
|
|
231
264
|
}
|
|
232
265
|
} catch {
|
|
@@ -244,6 +277,15 @@ async function runBridge({
|
|
|
244
277
|
}
|
|
245
278
|
}
|
|
246
279
|
|
|
280
|
+
function extractSinglePathPaste(raw: string): string | null {
|
|
281
|
+
const match = raw.match(BRACKETED_PASTE_RE);
|
|
282
|
+
const text = (match ? match[1] : raw)?.trim();
|
|
283
|
+
if (!text || /[\r\n]/u.test(text)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return text;
|
|
287
|
+
}
|
|
288
|
+
|
|
247
289
|
function onceExit(
|
|
248
290
|
child: PtyProcess,
|
|
249
291
|
stdin: NodeJS.ReadStream
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { platform } from 'node:os';
|
|
2
2
|
import { createDarwinClipboardAdapter, type DarwinClipboardAdapter } from './darwin.ts';
|
|
3
|
+
import { createLinuxClipboardAdapter, type LinuxClipboardAdapter } from './linux.ts';
|
|
4
|
+
import { createWin32ClipboardAdapter, type Win32ClipboardAdapter } from './win32.ts';
|
|
3
5
|
|
|
4
|
-
export type ClipboardAdapter = DarwinClipboardAdapter;
|
|
6
|
+
export type ClipboardAdapter = DarwinClipboardAdapter | LinuxClipboardAdapter | Win32ClipboardAdapter;
|
|
5
7
|
|
|
6
8
|
export function createClipboardAdapter({
|
|
7
9
|
platformName = platform()
|
|
@@ -10,12 +12,9 @@ export function createClipboardAdapter({
|
|
|
10
12
|
case 'darwin':
|
|
11
13
|
return createDarwinClipboardAdapter();
|
|
12
14
|
case 'linux':
|
|
13
|
-
|
|
14
|
-
// see Issue #386 follow-up. Returning null disables the bridge for now.
|
|
15
|
-
return null;
|
|
15
|
+
return createLinuxClipboardAdapter();
|
|
16
16
|
case 'win32':
|
|
17
|
-
|
|
18
|
-
return null;
|
|
17
|
+
return createWin32ClipboardAdapter();
|
|
19
18
|
default:
|
|
20
19
|
return null;
|
|
21
20
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
import type { ExecFileSyncOptions } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const PROBE_TIMEOUT_MS = 2_000;
|
|
8
|
+
const READ_IMAGE_TIMEOUT_MS = 5_000;
|
|
9
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
10
|
+
|
|
11
|
+
type ExecFn = (cmd: string, args: string[], options?: ExecFileSyncOptions) => Buffer | string;
|
|
12
|
+
type ExecToFileFn = (cmd: string, args: string[], outputPath: string, timeout: number) => void;
|
|
13
|
+
|
|
14
|
+
type LinuxBackend =
|
|
15
|
+
| { kind: 'wayland'; command: 'wl-paste' }
|
|
16
|
+
| { kind: 'x11'; command: 'xclip' };
|
|
17
|
+
|
|
18
|
+
export type LinuxClipboardAdapter = {
|
|
19
|
+
available(): { ok: true } | { ok: false; reason: string };
|
|
20
|
+
readImagePng(): Buffer | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createLinuxClipboardAdapter({
|
|
24
|
+
env = process.env,
|
|
25
|
+
execFn = execFileSync,
|
|
26
|
+
execToFileFn = execToFile,
|
|
27
|
+
mkdtempFn = fs.mkdtempSync,
|
|
28
|
+
readFileFn = fs.readFileSync,
|
|
29
|
+
rmFn = fs.rmSync
|
|
30
|
+
}: {
|
|
31
|
+
env?: NodeJS.ProcessEnv;
|
|
32
|
+
execFn?: ExecFn;
|
|
33
|
+
execToFileFn?: ExecToFileFn;
|
|
34
|
+
mkdtempFn?: typeof fs.mkdtempSync;
|
|
35
|
+
readFileFn?: typeof fs.readFileSync;
|
|
36
|
+
rmFn?: typeof fs.rmSync;
|
|
37
|
+
} = {}): LinuxClipboardAdapter {
|
|
38
|
+
return {
|
|
39
|
+
available() {
|
|
40
|
+
const backend = selectBackend(env);
|
|
41
|
+
try {
|
|
42
|
+
execFn(backend.command, versionArgs(backend), {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
timeout: PROBE_TIMEOUT_MS
|
|
45
|
+
});
|
|
46
|
+
return { ok: true };
|
|
47
|
+
} catch {
|
|
48
|
+
return { ok: false, reason: unavailableReason(backend) };
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
readImagePng() {
|
|
52
|
+
const backend = selectBackend(env);
|
|
53
|
+
try {
|
|
54
|
+
const mimeTypes = String(execFn(backend.command, mimeArgs(backend), {
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: PROBE_TIMEOUT_MS
|
|
57
|
+
}));
|
|
58
|
+
if (!hasPngMime(mimeTypes)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
|
|
63
|
+
const outputPath = path.join(tmpDir, 'clipboard.png');
|
|
64
|
+
try {
|
|
65
|
+
execToFileFn(backend.command, imageArgs(backend), outputPath, READ_IMAGE_TIMEOUT_MS);
|
|
66
|
+
const png = Buffer.from(readFileFn(outputPath));
|
|
67
|
+
return isPng(png) ? png : null;
|
|
68
|
+
} finally {
|
|
69
|
+
rmFn(tmpDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function selectBackend(env: NodeJS.ProcessEnv): LinuxBackend {
|
|
79
|
+
return env.WAYLAND_DISPLAY?.trim()
|
|
80
|
+
? { kind: 'wayland', command: 'wl-paste' }
|
|
81
|
+
: { kind: 'x11', command: 'xclip' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function versionArgs(backend: LinuxBackend): string[] {
|
|
85
|
+
return backend.kind === 'wayland' ? ['--version'] : ['-version'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mimeArgs(backend: LinuxBackend): string[] {
|
|
89
|
+
return backend.kind === 'wayland'
|
|
90
|
+
? ['--list-types']
|
|
91
|
+
: ['-selection', 'clipboard', '-t', 'TARGETS', '-o'];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function imageArgs(backend: LinuxBackend): string[] {
|
|
95
|
+
return backend.kind === 'wayland'
|
|
96
|
+
? ['-t', 'image/png']
|
|
97
|
+
: ['-selection', 'clipboard', '-t', 'image/png', '-o'];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function unavailableReason(backend: LinuxBackend): string {
|
|
101
|
+
return backend.kind === 'wayland'
|
|
102
|
+
? 'Wayland clipboard tool wl-paste is unavailable; install wl-clipboard to enable image paste'
|
|
103
|
+
: 'X11 clipboard tool xclip is unavailable; install xclip to enable image paste';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasPngMime(output: string): boolean {
|
|
107
|
+
return output.split(/\s+/u).some((type) => type === 'image/png');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function execToFile(cmd: string, args: string[], outputPath: string, timeout: number): void {
|
|
111
|
+
const fd = fs.openSync(outputPath, 'w', 0o600);
|
|
112
|
+
try {
|
|
113
|
+
execFileSync(cmd, args, {
|
|
114
|
+
timeout,
|
|
115
|
+
stdio: ['ignore', fd, 'pipe']
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
fs.closeSync(fd);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPng(buffer: Buffer): boolean {
|
|
123
|
+
return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
|
|
124
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
import type { ExecFileSyncOptions } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const PROBE_TIMEOUT_MS = 2_000;
|
|
8
|
+
const READ_IMAGE_TIMEOUT_MS = 5_000;
|
|
9
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
10
|
+
const POWERSHELL_ARGS = ['-NoProfile', '-NonInteractive', '-STA', '-Command'];
|
|
11
|
+
|
|
12
|
+
type ExecFn = (cmd: string, args: string[], options?: ExecFileSyncOptions) => Buffer | string;
|
|
13
|
+
type MkdtempFn = (prefix: string) => string;
|
|
14
|
+
type ReadFileFn = (filePath: fs.PathOrFileDescriptor) => Buffer | string;
|
|
15
|
+
type RmFn = (filePath: fs.PathLike, options: { recursive?: boolean; force?: boolean }) => void;
|
|
16
|
+
type ExistsFn = (filePath: fs.PathLike) => boolean;
|
|
17
|
+
|
|
18
|
+
export type Win32ClipboardAdapter = {
|
|
19
|
+
available(): { ok: true } | { ok: false; reason: string };
|
|
20
|
+
readImagePng(): Buffer | null;
|
|
21
|
+
readImageFromPath(imagePath: string): Buffer | null;
|
|
22
|
+
readImageFromText(text: string): Buffer | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createWin32ClipboardAdapter({
|
|
26
|
+
execFn = execFileSync,
|
|
27
|
+
mkdtempFn = fs.mkdtempSync,
|
|
28
|
+
readFileFn = fs.readFileSync,
|
|
29
|
+
rmFn = fs.rmSync,
|
|
30
|
+
existsFn = fs.existsSync
|
|
31
|
+
}: {
|
|
32
|
+
execFn?: ExecFn;
|
|
33
|
+
mkdtempFn?: MkdtempFn;
|
|
34
|
+
readFileFn?: ReadFileFn;
|
|
35
|
+
rmFn?: RmFn;
|
|
36
|
+
existsFn?: ExistsFn;
|
|
37
|
+
} = {}): Win32ClipboardAdapter {
|
|
38
|
+
return {
|
|
39
|
+
available() {
|
|
40
|
+
try {
|
|
41
|
+
execFn('powershell.exe', [...POWERSHELL_ARGS, clipboardProbeScript()], {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
timeout: PROBE_TIMEOUT_MS
|
|
44
|
+
});
|
|
45
|
+
return { ok: true };
|
|
46
|
+
} catch {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: 'Windows PowerShell Get-Clipboard is unavailable; install or enable powershell.exe to use image paste'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
readImagePng() {
|
|
54
|
+
const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
|
|
55
|
+
const outputPath = path.join(tmpDir, 'clipboard.png');
|
|
56
|
+
try {
|
|
57
|
+
execFn('powershell.exe', [...POWERSHELL_ARGS, pngWriteScript(outputPath)], {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
timeout: READ_IMAGE_TIMEOUT_MS
|
|
60
|
+
});
|
|
61
|
+
const png = Buffer.from(readFileFn(outputPath));
|
|
62
|
+
return isPng(png) ? png : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
} finally {
|
|
66
|
+
rmFn(tmpDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
readImageFromPath(imagePath) {
|
|
70
|
+
const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
|
|
71
|
+
const outputPath = path.join(tmpDir, 'clipboard.png');
|
|
72
|
+
try {
|
|
73
|
+
execFn('powershell.exe', [...POWERSHELL_ARGS, pngFromPathScript(imagePath, outputPath)], {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
timeout: READ_IMAGE_TIMEOUT_MS
|
|
76
|
+
});
|
|
77
|
+
const png = Buffer.from(readFileFn(outputPath));
|
|
78
|
+
return isPng(png) ? png : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
} finally {
|
|
82
|
+
rmFn(tmpDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
readImageFromText(text) {
|
|
86
|
+
const trimmed = text.trim();
|
|
87
|
+
if (!isWindowsImagePath(trimmed)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!existsFn(trimmed)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return this.readImageFromPath(trimmed);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function clipboardProbeScript(): string {
|
|
99
|
+
return 'if ($null -eq (Get-Command Get-Clipboard -ErrorAction SilentlyContinue)) { exit 1 }';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Embed a filesystem path in a PowerShell single-quoted string literal.
|
|
103
|
+
// Single-quoted strings in PowerShell are verbatim (no variable expansion,
|
|
104
|
+
// no escape sequences). The only special character is a single quote itself,
|
|
105
|
+
// which is escaped by doubling it.
|
|
106
|
+
function psLiteral(value: string): string {
|
|
107
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function pngWriteScript(outputPath: string): string {
|
|
111
|
+
const psOutputPath = psLiteral(outputPath);
|
|
112
|
+
return [
|
|
113
|
+
'$ErrorActionPreference = "Stop"',
|
|
114
|
+
`$outputPath = ${psOutputPath}`,
|
|
115
|
+
'$image = $null',
|
|
116
|
+
'try {',
|
|
117
|
+
' Add-Type -AssemblyName System.Drawing',
|
|
118
|
+
' $image = Get-Clipboard -Format Image -ErrorAction SilentlyContinue',
|
|
119
|
+
' if ($null -ne $image) {',
|
|
120
|
+
' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
121
|
+
' return',
|
|
122
|
+
' }',
|
|
123
|
+
' $files = Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue',
|
|
124
|
+
' if ($null -ne $files) {',
|
|
125
|
+
' $allowed = @(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".ico")',
|
|
126
|
+
' foreach ($file in $files) {',
|
|
127
|
+
' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue }',
|
|
128
|
+
' $ext = [System.IO.Path]::GetExtension($file)',
|
|
129
|
+
' if ($allowed -notcontains $ext) { continue }',
|
|
130
|
+
' try {',
|
|
131
|
+
' $image = [System.Drawing.Image]::FromFile($file)',
|
|
132
|
+
' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
133
|
+
' return',
|
|
134
|
+
' } catch {}',
|
|
135
|
+
' }',
|
|
136
|
+
' }',
|
|
137
|
+
' throw "clipboard has no image"',
|
|
138
|
+
'} finally {',
|
|
139
|
+
' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
|
|
140
|
+
'}'
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pngFromPathScript(imagePath: string, outputPath: string): string {
|
|
145
|
+
const psImagePath = psLiteral(imagePath);
|
|
146
|
+
const psOutputPath = psLiteral(outputPath);
|
|
147
|
+
return [
|
|
148
|
+
'$ErrorActionPreference = "Stop"',
|
|
149
|
+
`$imagePath = ${psImagePath}`,
|
|
150
|
+
`$outputPath = ${psOutputPath}`,
|
|
151
|
+
'$image = $null',
|
|
152
|
+
'try {',
|
|
153
|
+
' Add-Type -AssemblyName System.Drawing',
|
|
154
|
+
' if (-not (Test-Path -LiteralPath $imagePath -PathType Leaf)) { throw "file not found: $imagePath" }',
|
|
155
|
+
' $image = [System.Drawing.Image]::FromFile($imagePath)',
|
|
156
|
+
' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
157
|
+
'} finally {',
|
|
158
|
+
' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
|
|
159
|
+
'}'
|
|
160
|
+
].join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const IMAGE_EXTENSIONS_RE = /\.(png|jpg|jpeg|gif|bmp|webp|tiff?|ico)$/i;
|
|
164
|
+
|
|
165
|
+
function isWindowsImagePath(text: string): boolean {
|
|
166
|
+
// Must be a Windows absolute path: drive letter + colon + backslash/slash
|
|
167
|
+
// followed by a path ending with a known image extension.
|
|
168
|
+
return /^[A-Za-z]:[\\/].+$/.test(text) && IMAGE_EXTENSIONS_RE.test(text);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isPng(buffer: Buffer): boolean {
|
|
172
|
+
return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
|
|
173
|
+
}
|