@fitlab-ai/agent-infra 0.8.2 → 0.8.4
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.zh-CN.md +10 -0
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +2 -2
- package/dist/lib/cp.js +57 -16
- 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 +10 -8
- package/dist/lib/sandbox/clipboard/bridge.js +59 -21
- package/dist/lib/sandbox/clipboard/inbox.js +82 -0
- 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 +106 -88
- 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/runtimes/base.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +4 -4
- 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/cp.ts +56 -15
- package/lib/defaults.json +2 -0
- package/lib/sandbox/capture.ts +5 -3
- package/lib/sandbox/clipboard/bridge.ts +62 -18
- package/lib/sandbox/clipboard/inbox.ts +83 -0
- 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 +132 -88
- 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/sandbox/runtimes/base.dockerfile +3 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- 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/README.en.md +3 -0
- package/templates/.agents/README.zh-CN.md +3 -0
- package/templates/.agents/rules/create-issue.github.en.md +4 -4
- package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -4
- 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/review-handshake.en.md +2 -2
- package/templates/.agents/rules/review-handshake.zh-CN.md +2 -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/rules/testing-discipline.en.md +3 -41
- package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
- package/templates/.agents/scripts/validate-artifact.js +213 -0
- package/templates/.agents/scripts/workflow-warnings.js +290 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +6 -6
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +142 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +142 -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/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +6 -3
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +9 -6
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -2
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +165 -41
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +36 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -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
package/README.zh-CN.md
CHANGED
|
@@ -207,3 +207,13 @@ my-project/
|
|
|
207
207
|
## 许可协议
|
|
208
208
|
|
|
209
209
|
[MIT](License.txt)
|
|
210
|
+
|
|
211
|
+
## 社区
|
|
212
|
+
|
|
213
|
+
- 微信公众号:`FitFramework`
|
|
214
|
+
- QQ 技术交流群:`1029802553`
|
|
215
|
+
|
|
216
|
+
<p align="center">
|
|
217
|
+
<img src="./assets/qrcode_for_wechat_gh.png" alt="FitFramework 微信公众号二维码" width="250">
|
|
218
|
+
<img src="./assets/qrcode_for_qq_01.png" alt="FitFramework QQ 技术交流群二维码" width="250">
|
|
219
|
+
</p>
|
package/bin/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructur
|
|
|
15
15
|
Usage: ai <command> [options]
|
|
16
16
|
|
|
17
17
|
Commands:
|
|
18
|
-
cp <ssh-alias> Copy local clipboard image to a remote macOS
|
|
18
|
+
cp <ssh-alias> Copy local clipboard image to a remote macOS clipboard or Linux sandbox over SSH
|
|
19
19
|
decide Record a human decision for a task HD item
|
|
20
20
|
help Show this help message
|
|
21
21
|
init Initialize a new project with update-agent-infra seed command
|
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)) {
|
|
@@ -19,7 +19,7 @@ const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructur
|
|
|
19
19
|
Usage: ai <command> [options]
|
|
20
20
|
|
|
21
21
|
Commands:
|
|
22
|
-
cp <ssh-alias> Copy local clipboard image to a remote macOS
|
|
22
|
+
cp <ssh-alias> Copy local clipboard image to a remote macOS clipboard or Linux sandbox over SSH
|
|
23
23
|
decide Record a human decision for a task HD item
|
|
24
24
|
help Show this help message
|
|
25
25
|
init Initialize a new project with update-agent-infra seed command
|
package/dist/lib/cp.js
CHANGED
|
@@ -4,8 +4,9 @@ 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
|
|
8
|
-
|
|
7
|
+
import { createClipboardAdapter } from './sandbox/clipboard/index.js';
|
|
8
|
+
import { RECEIVER_CAPABILITY, receiveRemoteClipboardPng } from './sandbox/clipboard/inbox.js';
|
|
9
|
+
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS clipboard or Linux sandbox over SSH.\n';
|
|
9
10
|
const COMMAND_TIMEOUT_MS = 30_000;
|
|
10
11
|
export function runCommand(cmd, args, input) {
|
|
11
12
|
const result = spawnSync(cmd, args, {
|
|
@@ -23,6 +24,22 @@ export function runCommand(cmd, args, input) {
|
|
|
23
24
|
}
|
|
24
25
|
export async function cmdCp(args, deps = {}) {
|
|
25
26
|
const { platform = currentPlatform(), createAdapter = createClipboardAdapter, spawnFn = runCommand, randomId = randomUUID, mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, rmFn = fs.rmSync, tmpdir = defaultTmpdir, writeStdout = (chunk) => process.stdout.write(chunk), writeStderr = (chunk) => process.stderr.write(chunk) } = deps;
|
|
27
|
+
if (args.length === 1 && args[0] === '--remote-receive-capability') {
|
|
28
|
+
if (platform === 'linux')
|
|
29
|
+
writeStdout(`${RECEIVER_CAPABILITY}\n`);
|
|
30
|
+
return platform === 'linux' ? 0 : 1;
|
|
31
|
+
}
|
|
32
|
+
if (args.length === 2 && args[0] === '--remote-receive-v1') {
|
|
33
|
+
try {
|
|
34
|
+
const result = receiveRemoteClipboardPng(process.env.HOME || process.env.USERPROFILE || '', args[1], platform);
|
|
35
|
+
writeStdout(`${result}\n`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
writeStderr(`${error instanceof Error ? error.message : 'clipboard receive failed'}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
26
43
|
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
27
44
|
writeStdout(USAGE);
|
|
28
45
|
return 0;
|
|
@@ -62,6 +79,34 @@ export async function cmdCp(args, deps = {}) {
|
|
|
62
79
|
const localPng = path.join(localTmpDir, 'clipboard.png');
|
|
63
80
|
writeFileFn(localPng, png);
|
|
64
81
|
remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
|
|
82
|
+
const remoteSystem = spawnFn('ssh', [
|
|
83
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'uname', '-s'
|
|
84
|
+
]);
|
|
85
|
+
if (remoteSystem.status !== 0) {
|
|
86
|
+
writeStderr(`failed to detect remote platform on ${alias}:\n${commandDetail(remoteSystem)}\n`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const remotePlatform = remoteSystem.stdout.trim();
|
|
90
|
+
if (remotePlatform !== 'Darwin' && remotePlatform !== 'Linux') {
|
|
91
|
+
writeStderr(`unsupported remote platform on ${alias}: ${remotePlatform || 'unknown'}\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
if (remotePlatform === 'Linux') {
|
|
95
|
+
const homeCheck = spawnFn('ssh', [
|
|
96
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'test', '-d', '$HOME/.agent-infra'
|
|
97
|
+
]);
|
|
98
|
+
if (homeCheck.status !== 0) {
|
|
99
|
+
writeStderr(`remote agent-infra installation is missing on ${alias}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const capability = spawnFn('ssh', [
|
|
103
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-capability'
|
|
104
|
+
]);
|
|
105
|
+
if (capability.status !== 0 || capability.stderr !== '' || capability.stdout.replace(/\n$/u, '') !== RECEIVER_CAPABILITY) {
|
|
106
|
+
writeStderr(`remote agent-infra clipboard receiver is unavailable or incompatible on ${alias}; update agent-infra\n`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
65
110
|
const upload = spawnFn('scp', [
|
|
66
111
|
'-o',
|
|
67
112
|
'BatchMode=yes',
|
|
@@ -75,24 +120,20 @@ export async function cmdCp(args, deps = {}) {
|
|
|
75
120
|
return 1;
|
|
76
121
|
}
|
|
77
122
|
uploaded = true;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'-o',
|
|
86
|
-
'ConnectTimeout=10',
|
|
87
|
-
alias,
|
|
88
|
-
'osascript',
|
|
89
|
-
'-'
|
|
90
|
-
], remoteSetScript(remotePath));
|
|
123
|
+
const setRemote = remotePlatform === 'Darwin'
|
|
124
|
+
? spawnFn('ssh', [
|
|
125
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'osascript', '-'
|
|
126
|
+
], remoteSetScript(remotePath))
|
|
127
|
+
: spawnFn('ssh', [
|
|
128
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-v1', remotePath
|
|
129
|
+
]);
|
|
91
130
|
if (setRemote.status !== 0) {
|
|
92
131
|
writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
|
|
93
132
|
return 1;
|
|
94
133
|
}
|
|
95
|
-
writeStdout(
|
|
134
|
+
writeStdout(remotePlatform === 'Linux'
|
|
135
|
+
? `uploaded clipboard image to ${alias}; return to the SSH session and press Ctrl+V\n`
|
|
136
|
+
: `copied clipboard image to ${alias}\n`);
|
|
96
137
|
return 0;
|
|
97
138
|
}
|
|
98
139
|
finally {
|
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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"gemini-cli",
|
|
15
15
|
"opencode"
|
|
16
16
|
],
|
|
17
|
+
"refreshIntervalDays": 7,
|
|
17
18
|
"dockerfile": null,
|
|
18
19
|
"vm": {
|
|
19
20
|
"cpu": null,
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"**/test-integration.*",
|
|
52
53
|
"**/test.*",
|
|
53
54
|
"**/upgrade-dependency.*",
|
|
55
|
+
".agents/rules/testing-discipline.*",
|
|
54
56
|
".agents/skills/post-release/SKILL.*",
|
|
55
57
|
".agents/skills/release/SKILL.*",
|
|
56
58
|
".agents/skills/test-integration/SKILL.*",
|
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,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { loadConfig } from
|
|
3
|
-
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from
|
|
4
|
-
import { detectEngine } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.js';
|
|
4
|
+
import { detectEngine } from './engine.js';
|
|
5
|
+
import { commandForEngine } from './shell.js';
|
|
6
|
+
import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.js';
|
|
7
|
+
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
|
|
7
8
|
async function spawnCapture(file, args) {
|
|
8
9
|
return new Promise((resolve, reject) => {
|
|
9
10
|
const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -49,12 +50,12 @@ function buildRunScript(params) {
|
|
|
49
50
|
return `#!/bin/sh
|
|
50
51
|
set -u
|
|
51
52
|
cd /workspace
|
|
52
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
53
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/started_at
|
|
53
54
|
printf '%s\\n' running > ${runDir}/status
|
|
54
55
|
${command}
|
|
55
56
|
code=$?
|
|
56
57
|
printf '%s\\n' "$code" > ${runDir}/exit_code
|
|
57
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
58
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/finished_at
|
|
58
59
|
if [ "$code" -eq 0 ]; then
|
|
59
60
|
printf '%s\\n' completed > ${runDir}/status
|
|
60
61
|
else
|
|
@@ -144,7 +145,8 @@ export async function runInSandbox(request, options = {}) {
|
|
|
144
145
|
'-lc',
|
|
145
146
|
buildTmuxLauncher({ request, runId })
|
|
146
147
|
];
|
|
147
|
-
const
|
|
148
|
+
const command = commandForEngine(engine, 'docker', dockerArgs);
|
|
149
|
+
const result = await (options.spawn ?? spawnCapture)(command.cmd, command.args);
|
|
148
150
|
return {
|
|
149
151
|
...result,
|
|
150
152
|
run: {
|
|
@@ -1,12 +1,14 @@
|
|
|
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 {
|
|
7
|
-
import {
|
|
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 { isClipboardInboxReadable, readPendingClipboardPath } from './inbox.js';
|
|
7
|
+
import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from '../shell.js';
|
|
8
|
+
import { loadNodePty } from './node-pty.js';
|
|
8
9
|
const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
|
|
9
10
|
const PARTIAL_ESCAPE_FLUSH_MS = 30;
|
|
11
|
+
const BRACKETED_PASTE_RE = /^\x1b\[200~([\s\S]*)\x1b\[201~$/u;
|
|
10
12
|
// Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
|
|
11
13
|
// cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
|
|
12
14
|
// set on the shared host TTY. With ONLCR on, the kernel rewrites the bare \n
|
|
@@ -36,13 +38,10 @@ export async function runInteractiveWithClipboardBridge(options) {
|
|
|
36
38
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
37
39
|
return fallback('host stdin/stdout is not a TTY');
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const available = adapter.available();
|
|
43
|
-
if (!available.ok) {
|
|
41
|
+
const available = adapter?.available() ?? { ok: false, reason: 'no clipboard adapter available on this platform' };
|
|
42
|
+
const inboxReady = isClipboardInboxReadable(home);
|
|
43
|
+
if (!available.ok && !inboxReady)
|
|
44
44
|
return fallback(available.reason);
|
|
45
|
-
}
|
|
46
45
|
if (!runOk(engine, 'docker', ['exec', container, 'sh', '-c', '[ -d /clipboard ] && [ -r /clipboard ]'])) {
|
|
47
46
|
return fallback('container /clipboard mount is missing; rebuild the sandbox to enable image paste');
|
|
48
47
|
}
|
|
@@ -68,7 +67,7 @@ export async function runInteractiveWithClipboardBridge(options) {
|
|
|
68
67
|
return runBridge({
|
|
69
68
|
child,
|
|
70
69
|
home,
|
|
71
|
-
adapter,
|
|
70
|
+
adapter: available.ok ? adapter : null,
|
|
72
71
|
writeStderr,
|
|
73
72
|
stdin,
|
|
74
73
|
stdout,
|
|
@@ -83,7 +82,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
83
82
|
clearFlushTimer();
|
|
84
83
|
for (const token of detector.feed(inputDecoder.write(chunk))) {
|
|
85
84
|
if (token.kind === 'text') {
|
|
86
|
-
|
|
85
|
+
handleText(token.raw, child);
|
|
87
86
|
}
|
|
88
87
|
else {
|
|
89
88
|
handleCtrlV(token, child);
|
|
@@ -94,7 +93,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
94
93
|
flushTimer = null;
|
|
95
94
|
for (const token of detector.flush()) {
|
|
96
95
|
if (token.kind === 'text') {
|
|
97
|
-
|
|
96
|
+
handleText(token.raw, child);
|
|
98
97
|
}
|
|
99
98
|
else {
|
|
100
99
|
handleCtrlV(token, child);
|
|
@@ -106,22 +105,53 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
106
105
|
const onResize = () => child.resize(stdout.columns || 120, stdout.rows || 40);
|
|
107
106
|
const onSigint = () => child.kill('SIGINT');
|
|
108
107
|
const onSigterm = () => child.kill('SIGTERM');
|
|
108
|
+
function writeImagePaste(png, target) {
|
|
109
|
+
const filename = pngClipboardFilename(png);
|
|
110
|
+
writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
|
|
111
|
+
pruneClipboardDir(clipboardHostDir(home));
|
|
112
|
+
target.write(buildBracketedPaste(containerClipboardPath(filename)));
|
|
113
|
+
}
|
|
114
|
+
function handleText(raw, target) {
|
|
115
|
+
const textAdapter = adapter;
|
|
116
|
+
const pastedText = extractSinglePathPaste(raw);
|
|
117
|
+
if (!textAdapter || !textAdapter.readImageFromText || pastedText === null) {
|
|
118
|
+
target.write(raw);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const png = textAdapter.readImageFromText(pastedText);
|
|
123
|
+
if (!png) {
|
|
124
|
+
target.write(raw);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
writeImagePaste(png, target);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
target.write(raw);
|
|
131
|
+
if (!warnedPasteFailure) {
|
|
132
|
+
warnedPasteFailure = true;
|
|
133
|
+
writeStderr(`Warning: clipboard image paste failed; forwarded pasted text (${error instanceof Error ? error.message : 'unknown error'})\n`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
109
137
|
function handleCtrlV(match, target) {
|
|
110
138
|
try {
|
|
139
|
+
const pendingPath = readPendingClipboardPath(home);
|
|
140
|
+
if (pendingPath) {
|
|
141
|
+
target.write(buildBracketedPaste(pendingPath));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
111
144
|
// readImagePng returns null both for "no image on clipboard" and for
|
|
112
145
|
// unexpected read failures; both cases forward the original Ctrl+V so
|
|
113
146
|
// the container app handles it as a regular keystroke. The throw branch
|
|
114
147
|
// below only fires on truly unexpected exceptions (e.g. fs write
|
|
115
148
|
// errors writing to the host clipboard dir).
|
|
116
|
-
const png = adapter
|
|
149
|
+
const png = adapter?.readImagePng() ?? null;
|
|
117
150
|
if (!png) {
|
|
118
151
|
target.write(match.raw);
|
|
119
152
|
return;
|
|
120
153
|
}
|
|
121
|
-
|
|
122
|
-
writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
|
|
123
|
-
pruneClipboardDir(clipboardHostDir(home));
|
|
124
|
-
target.write(buildBracketedPaste(containerClipboardPath(filename)));
|
|
154
|
+
writeImagePaste(png, target);
|
|
125
155
|
}
|
|
126
156
|
catch (error) {
|
|
127
157
|
target.write(match.raw);
|
|
@@ -155,7 +185,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
155
185
|
try {
|
|
156
186
|
for (const token of detector.feed(inputDecoder.end())) {
|
|
157
187
|
if (token.kind === 'text') {
|
|
158
|
-
|
|
188
|
+
handleText(token.raw, child);
|
|
159
189
|
}
|
|
160
190
|
else {
|
|
161
191
|
handleCtrlV(token, child);
|
|
@@ -163,7 +193,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
163
193
|
}
|
|
164
194
|
for (const token of detector.flush()) {
|
|
165
195
|
if (token.kind === 'text') {
|
|
166
|
-
|
|
196
|
+
handleText(token.raw, child);
|
|
167
197
|
}
|
|
168
198
|
}
|
|
169
199
|
}
|
|
@@ -181,6 +211,14 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
181
211
|
restoreTerminal();
|
|
182
212
|
}
|
|
183
213
|
}
|
|
214
|
+
function extractSinglePathPaste(raw) {
|
|
215
|
+
const match = raw.match(BRACKETED_PASTE_RE);
|
|
216
|
+
const text = (match ? match[1] : raw)?.trim();
|
|
217
|
+
if (!text || /[\r\n]/u.test(text)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
184
222
|
function onceExit(child, stdin) {
|
|
185
223
|
return new Promise((resolve) => {
|
|
186
224
|
let settled = false;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from './paths.js';
|
|
4
|
+
export const RECEIVER_CAPABILITY = 'AGENT_INFRA_CLIPBOARD_RECEIVER:v1';
|
|
5
|
+
const MARKER = '.pending.json';
|
|
6
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
7
|
+
const HASHED_PNG = /^[a-f0-9]{16}\.png$/u;
|
|
8
|
+
const REMOTE_TEMP = /^agent-infra-cp-[0-9a-f-]{36}\.png$/u;
|
|
9
|
+
export function isClipboardInboxReadable(home) {
|
|
10
|
+
const dir = clipboardHostDir(home);
|
|
11
|
+
try {
|
|
12
|
+
fs.accessSync(dir, fs.constants.R_OK);
|
|
13
|
+
return fs.statSync(dir).isDirectory();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function receiveRemoteClipboardPng(home, tempPath, platformName = process.platform) {
|
|
20
|
+
if (platformName !== 'linux')
|
|
21
|
+
throw new Error('remote clipboard receiver requires Linux');
|
|
22
|
+
if (path.dirname(tempPath) !== '/tmp' || !REMOTE_TEMP.test(path.basename(tempPath))) {
|
|
23
|
+
throw new Error('invalid clipboard upload path');
|
|
24
|
+
}
|
|
25
|
+
const signature = Buffer.alloc(PNG_SIGNATURE.length);
|
|
26
|
+
const fd = fs.openSync(tempPath, 'r');
|
|
27
|
+
let signatureBytes;
|
|
28
|
+
try {
|
|
29
|
+
signatureBytes = fs.readSync(fd, signature, 0, signature.length, 0);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
fs.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
if (signatureBytes !== PNG_SIGNATURE.length || !signature.equals(PNG_SIGNATURE)) {
|
|
35
|
+
throw new Error('uploaded clipboard file is not a PNG');
|
|
36
|
+
}
|
|
37
|
+
const png = fs.readFileSync(tempPath);
|
|
38
|
+
const dir = clipboardHostDir(home);
|
|
39
|
+
if (!fs.existsSync(path.dirname(dir)))
|
|
40
|
+
throw new Error('agent-infra is not installed on this host');
|
|
41
|
+
const filename = pngClipboardFilename(png);
|
|
42
|
+
writeClipboardPngAtomic(dir, filename, png);
|
|
43
|
+
pruneClipboardDir(dir);
|
|
44
|
+
const markerTmp = path.join(dir, `.${MARKER}.${process.pid}.tmp`);
|
|
45
|
+
fs.writeFileSync(markerTmp, JSON.stringify({ version: 1, filename, createdAt: new Date().toISOString() }) + '\n', { mode: 0o600 });
|
|
46
|
+
fs.renameSync(markerTmp, path.join(dir, MARKER));
|
|
47
|
+
return containerClipboardPath(filename);
|
|
48
|
+
}
|
|
49
|
+
export function readPendingClipboardPath(home) {
|
|
50
|
+
const dir = clipboardHostDir(home);
|
|
51
|
+
const markerPath = path.join(dir, MARKER);
|
|
52
|
+
let markerText;
|
|
53
|
+
try {
|
|
54
|
+
markerText = fs.readFileSync(markerPath, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === 'ENOENT')
|
|
58
|
+
return null;
|
|
59
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
60
|
+
}
|
|
61
|
+
let marker;
|
|
62
|
+
try {
|
|
63
|
+
marker = JSON.parse(markerText);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
67
|
+
}
|
|
68
|
+
if (marker.version !== 1 || typeof marker.filename !== 'string' || !HASHED_PNG.test(marker.filename)) {
|
|
69
|
+
throw new Error('invalid clipboard pending marker');
|
|
70
|
+
}
|
|
71
|
+
const filePath = path.join(dir, marker.filename);
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.lstatSync(filePath);
|
|
74
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
75
|
+
throw new Error('pending clipboard image is not a regular file');
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
79
|
+
}
|
|
80
|
+
return containerClipboardPath(marker.filename);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -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;
|