minimal-agent 0.18.13 → 0.18.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/package.json +1 -3
  2. package/plugins/workflow-runner/src/runtime/context.js +1 -1
  3. package/plugins/workflow-runner/src/runtime/context.ts +1 -1
  4. package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
  5. package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
  6. package/skills/ssh-remote-dev/SKILL.md +61 -0
  7. package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
  8. package/src/advisor/README.md +62 -24
  9. package/src/advisor/advisorNudge.js +199 -0
  10. package/src/advisor/advisorNudge.ts +232 -0
  11. package/src/advisor/deliveryReview.js +35 -12
  12. package/src/advisor/deliveryReview.ts +35 -11
  13. package/src/advisor/presentation.js +7 -10
  14. package/src/advisor/presentation.ts +10 -13
  15. package/src/advisor/reviewContext.js +22 -58
  16. package/src/advisor/reviewContext.ts +24 -67
  17. package/src/cli/configMenu.js +17 -128
  18. package/src/cli/configMenu.tsx +17 -193
  19. package/src/cli/print.js +2 -0
  20. package/src/cli/print.ts +2 -0
  21. package/src/cli/streamJson.js +2 -0
  22. package/src/cli/streamJson.ts +2 -0
  23. package/src/cli/wizards/advisorWizard.js +9 -7
  24. package/src/cli/wizards/advisorWizard.tsx +9 -7
  25. package/src/cli/wizards/contextAdvisorWizard.js +9 -7
  26. package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
  27. package/src/cli/wizards/providerPresets.js +27 -1
  28. package/src/cli/wizards/providerPresets.ts +44 -2
  29. package/src/cli/wizards/providerWizard.js +51 -10
  30. package/src/cli/wizards/providerWizard.tsx +59 -9
  31. package/src/config/configDefaults.js +1 -1
  32. package/src/config/configDefaults.ts +1 -1
  33. package/src/config/configDiagnostics.js +1 -21
  34. package/src/config/configDiagnostics.ts +3 -23
  35. package/src/config/configFile.js +11 -4
  36. package/src/config/configFile.ts +27 -7
  37. package/src/config/configManagement.js +1 -0
  38. package/src/config/configManagement.ts +2 -0
  39. package/src/config/configService.js +12 -8
  40. package/src/config/configService.ts +16 -11
  41. package/src/config/numericLimits.js +0 -4
  42. package/src/config/numericLimits.ts +0 -4
  43. package/src/config/reasoningEffort.js +16 -0
  44. package/src/config/reasoningEffort.ts +18 -0
  45. package/src/context/budgetPlan.js +1 -1
  46. package/src/context/budgetPlan.ts +1 -1
  47. package/src/context/optimizeConversation.js +1 -1
  48. package/src/context/optimizeConversation.ts +1 -1
  49. package/src/harness/stallGuard.js +8 -13
  50. package/src/harness/stallGuard.ts +8 -12
  51. package/src/llm/client.js +64 -17
  52. package/src/llm/client.ts +62 -7
  53. package/src/llm/providers/openaiFormat.js +14 -3
  54. package/src/llm/providers/openaiFormat.ts +17 -3
  55. package/src/llm/providers/responsesFormat.js +275 -0
  56. package/src/llm/providers/responsesFormat.ts +306 -0
  57. package/src/loop/README.md +18 -3
  58. package/src/loop/liveDiagramGate.js +63 -0
  59. package/src/loop/liveDiagramGate.ts +61 -0
  60. package/src/loop/messages.js +12 -0
  61. package/src/loop/messages.ts +13 -0
  62. package/src/loop/options.ts +7 -2
  63. package/src/loop/toolBatch.js +0 -1
  64. package/src/loop/toolBatch.ts +0 -1
  65. package/src/loop/toolExecutor.js +11 -46
  66. package/src/loop/toolExecutor.ts +12 -55
  67. package/src/loop/toolPresentation.js +2 -13
  68. package/src/loop/toolPresentation.ts +2 -13
  69. package/src/loop/toolSemantics.js +3 -4
  70. package/src/loop/toolSemantics.ts +4 -6
  71. package/src/loop.js +222 -214
  72. package/src/loop.ts +226 -232
  73. package/src/memory/dependencies.js +11 -3
  74. package/src/memory/dependencies.ts +11 -3
  75. package/src/memory/episodicMemory.js +59 -18
  76. package/src/memory/episodicMemory.ts +58 -18
  77. package/src/memory/experienceStore.js +4 -3
  78. package/src/memory/experienceStore.ts +4 -3
  79. package/src/memory/factCandidates.js +53 -1
  80. package/src/memory/factCandidates.ts +54 -1
  81. package/src/memory/factLearning.js +2 -2
  82. package/src/memory/factLearning.ts +2 -2
  83. package/src/memory/jobPolicy.js +24 -5
  84. package/src/memory/jobPolicy.ts +25 -5
  85. package/src/memory/learning.js +2 -2
  86. package/src/memory/learning.ts +2 -2
  87. package/src/memory/learningModel.js +7 -0
  88. package/src/memory/learningModel.ts +7 -0
  89. package/src/memory/managementService.js +8 -7
  90. package/src/memory/managementService.ts +16 -8
  91. package/src/memory/memoryService.js +104 -33
  92. package/src/memory/memoryService.ts +132 -28
  93. package/src/memory/projectKnowledge.js +39 -3
  94. package/src/memory/projectKnowledge.ts +45 -3
  95. package/src/memory/recallQuery.js +56 -0
  96. package/src/memory/recallQuery.ts +58 -0
  97. package/src/memory/retrieval.js +39 -10
  98. package/src/memory/retrieval.ts +46 -11
  99. package/src/memory/turnJournal.js +75 -10
  100. package/src/memory/turnJournal.ts +66 -10
  101. package/src/memory/types.ts +7 -1
  102. package/src/prompts/delegation.js +2 -1
  103. package/src/prompts/delegation.ts +3 -1
  104. package/src/prompts/policyRegistry.js +9 -12
  105. package/src/prompts/policyRegistry.ts +9 -12
  106. package/src/prompts/promptAssembly.js +6 -5
  107. package/src/prompts/promptAssembly.ts +8 -6
  108. package/src/prompts/visualPresentation.js +39 -10
  109. package/src/prompts/visualPresentation.ts +38 -9
  110. package/src/runtime/answerPreparation.js +1 -1
  111. package/src/runtime/answerPreparation.ts +1 -1
  112. package/src/runtime/fileLock.js +19 -3
  113. package/src/runtime/fileLock.ts +19 -3
  114. package/src/runtime/invocationPolicy.js +1 -1
  115. package/src/runtime/invocationPolicy.ts +1 -1
  116. package/src/runtime/outputContract.js +0 -4
  117. package/src/runtime/outputContract.ts +0 -7
  118. package/src/session/runAgentTurn.js +3 -0
  119. package/src/session/runAgentTurn.ts +3 -0
  120. package/src/team/onlineToolContract.js +0 -1
  121. package/src/team/onlineToolContract.ts +0 -1
  122. package/src/tools/advisor/advisor.js +207 -365
  123. package/src/tools/advisor/advisor.ts +211 -383
  124. package/src/tools/agent/agentTypes.js +0 -1
  125. package/src/tools/agent/agentTypes.ts +0 -1
  126. package/src/tools/agent/coordinator.js +4 -1
  127. package/src/tools/agent/coordinator.ts +4 -1
  128. package/src/tools/bash/semantics.js +4 -5
  129. package/src/tools/bash/semantics.ts +4 -5
  130. package/src/tools/capabilities.js +2 -3
  131. package/src/tools/capabilities.ts +2 -4
  132. package/src/tools/index.js +0 -16
  133. package/src/tools/index.ts +0 -16
  134. package/src/tools/memory/maintenance.js +8 -9
  135. package/src/tools/memory/maintenance.ts +13 -11
  136. package/src/tools/memory/memory.js +5 -1
  137. package/src/tools/memory/memory.ts +5 -0
  138. package/src/tools/optimizeConversation/README.md +4 -6
  139. package/src/tools/toolPromptGuides.js +2 -9
  140. package/src/tools/toolPromptGuides.ts +2 -9
  141. package/src/types.ts +28 -105
  142. package/src/ui/App.js +2 -0
  143. package/src/ui/App.tsx +1 -0
  144. package/src/ui/MessageList.js +8 -44
  145. package/src/ui/MessageList.tsx +3 -81
  146. package/src/ui/hooks/useChat.js +2 -0
  147. package/src/ui/hooks/useChat.ts +2 -0
  148. package/src/ui/toolProgress.js +1 -1
  149. package/src/ui/toolProgress.ts +1 -1
  150. package/src/web/protocol.ts +10 -3
  151. package/src/advisor/advisorCoordinator.js +0 -734
  152. package/src/advisor/advisorCoordinator.ts +0 -825
  153. package/src/cli/wizards/sshHostWizard.js +0 -156
  154. package/src/cli/wizards/sshHostWizard.tsx +0 -214
  155. package/src/context/sshHosts.js +0 -290
  156. package/src/context/sshHosts.ts +0 -312
  157. package/src/loop/publication.js +0 -94
  158. package/src/loop/publication.ts +0 -124
  159. package/src/runtime/finalAnswer.js +0 -9
  160. package/src/runtime/finalAnswer.ts +0 -13
  161. package/src/tools/advisor/advisorDecision.js +0 -79
  162. package/src/tools/advisor/advisorDecision.ts +0 -92
  163. package/src/tools/ssh/ssh.js +0 -152
  164. package/src/tools/ssh/ssh.ts +0 -167
  165. package/src/tools/ssh/sshAvailability.js +0 -35
  166. package/src/tools/ssh/sshAvailability.ts +0 -39
  167. package/src/tools/ssh/sshClient.js +0 -177
  168. package/src/tools/ssh/sshClient.ts +0 -189
  169. package/src/tools/ssh/sshDownload.js +0 -77
  170. package/src/tools/ssh/sshDownload.ts +0 -92
  171. package/src/tools/ssh/sshHostsTool.js +0 -36
  172. package/src/tools/ssh/sshHostsTool.ts +0 -48
  173. package/src/tools/ssh/sshUpload.js +0 -72
  174. package/src/tools/ssh/sshUpload.ts +0 -85
@@ -1,189 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/tools/ssh/sshClient.ts —— ssh2 的薄封装(Promise 化 exec / sftp)
4
- * ------------------------------------------------------------
5
- * 只做协议层:连接、执行命令、传文件。主机解析(别名→凭据)、危险命令拦截、
6
- * 后台模式语义全部在调用方(ssh.ts / sshUpload.ts / sshDownload.ts)处理,
7
- * 这里不掺业务判断,方便测试用假实现替换(见 createSshTool 系列的 DI 参数)。
8
- * ============================================================
9
- */
10
-
11
- import { readFile } from 'node:fs/promises';
12
- import { posix } from 'node:path';
13
- import { Client, type ConnectConfig, type SFTPWrapper } from 'ssh2';
14
-
15
- import type { SshHostConfig } from '../../context/sshHosts.js';
16
- import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
17
-
18
- /** 三个 SSH 工具(exec/upload/download)共用的默认超时——唯一真值。 */
19
- export const DEFAULT_SSH_TIMEOUT_MS = 30_000;
20
-
21
- /**
22
- * exec() 累积 stdout/stderr 时的内存安全上限(字符,每路各自)——远程命令狂输出
23
- * (如误连到日志尾/`cat` 大文件)时防止字符串无限增长撑爆内存。跟 Bash 工具的
24
- * `MAX_STREAM_BUFFER_CHARS` 同一思路:取展示层截断阈值(`ssh.ts` 里做)的 2 倍,
25
- * 正常输出不受影响。这里只做内存兜底,不做背压/归档——那是业务逻辑,本文件按头注
26
- * 原则不掺(真要背压得像 Bash 那样接 `ToolOutputStore`,成本对协议薄封装层不划算)。
27
- */
28
- const MAX_EXEC_BUFFER_CHARS = DEFAULT_MAX_RESULT_SIZE_CHARS * 2;
29
-
30
- function appendCapped(current: string, chunk: string, capChars: number): string {
31
- if (current.length >= capChars) return current;
32
- const combined = current + chunk;
33
- return combined.length > capChars ? combined.slice(0, capChars) : combined;
34
- }
35
-
36
- /** 专门区分「执行超时」与其它失败(认证/网络错误)——background 模式要能识别前者。 */
37
- export class SshExecTimeoutError extends Error {
38
- /** 超时发生前已经收到的部分输出(exec 用)——调用方可选择性地把它带给模型,而不是整段丢弃。 */
39
- readonly partialStdout?: string;
40
- readonly partialStderr?: string;
41
-
42
- constructor(timeoutMs: number, partial?: { stdout: string; stderr: string }) {
43
- super(`执行超时(${timeoutMs}ms)`);
44
- this.name = 'SshExecTimeoutError';
45
- this.partialStdout = partial?.stdout;
46
- this.partialStderr = partial?.stderr;
47
- }
48
- }
49
-
50
- export interface SshExecResult {
51
- stdout: string;
52
- stderr: string;
53
- code: number | null;
54
- signal: string | null;
55
- }
56
-
57
- export interface SshConnection {
58
- exec(command: string, timeoutMs: number): Promise<SshExecResult>;
59
- /** 上传本地文件到远程路径,返回写入的字节数。超时会中断连接并抛 SshExecTimeoutError(与 exec 同语义)。 */
60
- put(localPath: string, remotePath: string, timeoutMs: number): Promise<number>;
61
- /** 从远程路径下载到本地文件,返回写入的字节数。超时会中断连接并抛 SshExecTimeoutError(与 exec 同语义)。 */
62
- get(remotePath: string, localPath: string, timeoutMs: number): Promise<number>;
63
- end(): void;
64
- }
65
-
66
- /** 主循环/工具注入的连接工厂类型——测试用假实现替换,避免真的打网络。 */
67
- export type SshConnectImpl = (host: SshHostConfig, connectTimeoutMs: number) => Promise<SshConnection>;
68
-
69
- async function buildConnectConfig(host: SshHostConfig, connectTimeoutMs: number): Promise<ConnectConfig> {
70
- const base: ConnectConfig = {
71
- host: host.host,
72
- port: host.port,
73
- username: host.username,
74
- readyTimeout: connectTimeoutMs,
75
- };
76
- if (host.authMethod === 'password') return { ...base, password: host.password };
77
- const privateKey = await readFile(host.privateKeyPath!);
78
- return { ...base, privateKey, ...(host.passphrase ? { passphrase: host.passphrase } : {}) };
79
- }
80
-
81
- /**
82
- * 逐级 mkdir 出 remotePath 的父目录(SFTP 没有 `mkdir -p`,ssh2 的 `sftp.mkdir` 一次只建一层)。
83
- * 每一级都直接 mkdir、忽略错误(多数情况下是"已存在")——不去解析具体 SFTP 错误码区分
84
- * "已存在" vs "无权限":无权限时 mkdir 静默失败,紧随其后的 fastPut 会用清楚得多的错误
85
- * 报出真实原因,没必要在这里重复判断一遍。
86
- */
87
- function sftpMkdirParents(sftp: SFTPWrapper, remotePath: string): Promise<void> {
88
- const dir = posix.dirname(remotePath);
89
- if (dir === '.' || dir === '/' || dir === '') return Promise.resolve();
90
- const absolute = dir.startsWith('/');
91
- const segments = dir.split('/').filter(Boolean);
92
- let current = '';
93
- let chain: Promise<void> = Promise.resolve();
94
- for (const seg of segments) {
95
- current += `/${seg}`;
96
- const path = absolute ? current : current.slice(1);
97
- chain = chain.then(() => new Promise<void>((resolve) => sftp.mkdir(path, () => resolve())));
98
- }
99
- return chain;
100
- }
101
-
102
- /** 导出仅供单测直接驱动(绕开真实 ssh2.Client 网络连接,喂假 exec/sftp 回调验证超时与 mkdir-parents 行为)。 */
103
- export function wrapConnection(conn: Client): SshConnection {
104
- return {
105
- exec(command, timeoutMs) {
106
- return new Promise((resolvePromise, rejectPromise) => {
107
- let stdout = '';
108
- let stderr = '';
109
- const timer = setTimeout(() => {
110
- conn.end();
111
- rejectPromise(new SshExecTimeoutError(timeoutMs, { stdout, stderr }));
112
- }, timeoutMs);
113
- conn.exec(command, (execError, stream) => {
114
- if (execError) {
115
- clearTimeout(timer);
116
- rejectPromise(execError);
117
- return;
118
- }
119
- stream
120
- .on('close', (code: number | null, signal: string | null) => {
121
- clearTimeout(timer);
122
- resolvePromise({ stdout, stderr, code, signal: signal ?? null });
123
- })
124
- .on('data', (data: Buffer) => { stdout = appendCapped(stdout, data.toString('utf8'), MAX_EXEC_BUFFER_CHARS); })
125
- .stderr.on('data', (data: Buffer) => { stderr = appendCapped(stderr, data.toString('utf8'), MAX_EXEC_BUFFER_CHARS); });
126
- });
127
- });
128
- },
129
- put(localPath, remotePath, timeoutMs) {
130
- return new Promise((resolvePromise, rejectPromise) => {
131
- const timer = setTimeout(() => {
132
- conn.end();
133
- rejectPromise(new SshExecTimeoutError(timeoutMs));
134
- }, timeoutMs);
135
- conn.sftp((sftpError, sftp) => {
136
- if (sftpError) { clearTimeout(timer); rejectPromise(sftpError); return; }
137
- sftpMkdirParents(sftp, remotePath)
138
- .then(() => {
139
- sftp.fastPut(localPath, remotePath, (putError) => {
140
- if (putError) { clearTimeout(timer); rejectPromise(putError); return; }
141
- sftp.stat(remotePath, (statError, stats) => {
142
- clearTimeout(timer);
143
- resolvePromise(statError ? 0 : stats.size);
144
- });
145
- });
146
- })
147
- // sftp.mkdir 的回调本身吞错误恒 resolve(见 sftpMkdirParents 头注),这里防的是
148
- // 更早一层:sftp 连接在 mkdir 期间掉线导致同步抛出——没有这个 catch,链路会挂成
149
- // 未处理 rejection,外层 Promise 只能干等到 timer 触发,模型看到的是「执行超时」
150
- // 而不是真实的连接错误。
151
- .catch((mkdirError: unknown) => { clearTimeout(timer); rejectPromise(mkdirError); });
152
- });
153
- });
154
- },
155
- get(remotePath, localPath, timeoutMs) {
156
- return new Promise((resolvePromise, rejectPromise) => {
157
- const timer = setTimeout(() => {
158
- conn.end();
159
- rejectPromise(new SshExecTimeoutError(timeoutMs));
160
- }, timeoutMs);
161
- conn.sftp((sftpError, sftp) => {
162
- if (sftpError) { clearTimeout(timer); rejectPromise(sftpError); return; }
163
- sftp.fastGet(remotePath, localPath, (getError) => {
164
- if (getError) { clearTimeout(timer); rejectPromise(getError); return; }
165
- sftp.stat(remotePath, (statError, stats) => {
166
- clearTimeout(timer);
167
- resolvePromise(statError ? 0 : stats.size);
168
- });
169
- });
170
- });
171
- });
172
- },
173
- end() {
174
- conn.end();
175
- },
176
- };
177
- }
178
-
179
- /** 生产环境的真实连接实现——测试通过 DI 参数注入假实现,不经过这里。 */
180
- export const defaultSshConnect: SshConnectImpl = async (host, connectTimeoutMs) => {
181
- const config = await buildConnectConfig(host, connectTimeoutMs);
182
- return new Promise((resolvePromise, rejectPromise) => {
183
- const conn = new Client();
184
- conn
185
- .on('ready', () => resolvePromise(wrapConnection(conn)))
186
- .on('error', (error) => rejectPromise(error))
187
- .connect(config);
188
- });
189
- };
@@ -1,77 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/tools/ssh/sshDownload.ts —— SSHDownload:远程主机 → 本地文件(SFTP)
4
- * ------------------------------------------------------------
5
- * 拆成独立工具名的理由同 sshUpload.ts 头注。落盘用临时文件 + rename 做到
6
- * 原子性——SFTP 流式写入中途失败时不留半个文件覆盖已有内容。
7
- * ============================================================
8
- */
9
- import { randomUUID } from 'node:crypto';
10
- import { mkdir, rename, rm } from 'node:fs/promises';
11
- import { dirname } from 'node:path';
12
- import { z } from 'zod';
13
- import { resolveSshHost } from '../../context/sshHosts.js';
14
- import { CONFIG_LIMITS } from '../../config/numericLimits.js';
15
- import { resolveToolPath } from '../shared/fileUtils.js';
16
- import { toToolParameters } from '../../utils/zodToJson.js';
17
- import { resolveSshToolAvailability } from './sshAvailability.js';
18
- import { DEFAULT_SSH_TIMEOUT_MS, defaultSshConnect, SshExecTimeoutError } from './sshClient.js';
19
- const inputSchema = z.object({
20
- host: z.string().optional().describe('已配置的 SSH 主机别名;不填则使用该工作区的默认主机'),
21
- remotePath: z.string().min(1).describe('远程源路径(远程文件系统语义,不做本地锚定)'),
22
- localPath: z.string().min(1).describe('本地落盘路径(相对路径基于 working dir 解析;会覆盖已存在的同名文件)'),
23
- timeoutMs: z.number().int().min(CONFIG_LIMITS.sshTimeoutMs.min).max(CONFIG_LIMITS.sshTimeoutMs.max).optional()
24
- .describe('本次传输超时(毫秒),默认 30000'),
25
- });
26
- async function callWith(input, signal, context, resolveHost, connectImpl) {
27
- const workspaceId = context?.workspaceId;
28
- if (!workspaceId) {
29
- return { ok: false, error: '当前运行环境没有工作区身份,无法解析 SSH 主机配置(SSH 主机按工作区隔离存储)。', recovery: 'none' };
30
- }
31
- const local = resolveToolPath(input.localPath);
32
- if (!local.ok)
33
- return local;
34
- const resolved = await resolveHost(workspaceId, input.host);
35
- if (!resolved.ok)
36
- return { ok: false, error: resolved.error, recovery: 'change-input' };
37
- const host = resolved.host;
38
- const timeoutMs = input.timeoutMs ?? host.timeoutMs ?? DEFAULT_SSH_TIMEOUT_MS;
39
- let connection;
40
- try {
41
- connection = await connectImpl(host, timeoutMs);
42
- }
43
- catch (error) {
44
- return { ok: false, error: `连接主机 "${host.alias}" 失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
45
- }
46
- const tmpPath = `${local.resolvedPath}.${randomUUID()}.minimal-agent-ssh.tmp`;
47
- try {
48
- await mkdir(dirname(local.resolvedPath), { recursive: true });
49
- const bytes = await connection.get(input.remotePath, tmpPath, timeoutMs);
50
- await rename(tmpPath, local.resolvedPath);
51
- return { ok: true, content: `已下载 ${host.alias}:${input.remotePath} → ${input.localPath}(${bytes} 字节)`, sensitivity: 'local' };
52
- }
53
- catch (error) {
54
- await rm(tmpPath, { force: true }).catch(() => { });
55
- if (signal?.aborted)
56
- return { ok: false, error: 'SSH 下载已取消', recovery: 'none' };
57
- if (error instanceof SshExecTimeoutError)
58
- return { ok: false, error: error.message, recovery: 'retry' };
59
- return { ok: false, error: `从 "${host.alias}" 下载失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
60
- }
61
- finally {
62
- connection.end();
63
- }
64
- }
65
- export function createSshDownloadTool(resolveHost = resolveSshHost, connectImpl = defaultSshConnect) {
66
- return {
67
- name: 'SSHDownload',
68
- description: '通过 SFTP 把远程 SSH 主机上的文件下载到本地。主机由用户在 /config → 远程主机(SSH) 中配置,按当前工作区隔离;不确定有哪些主机时先调用 SSHHosts。',
69
- inputSchema,
70
- parameters: toToolParameters(inputSchema),
71
- isReadOnly: false,
72
- resolveAvailability: resolveSshToolAvailability,
73
- call: (input, signal) => callWith(input, signal, undefined, resolveHost, connectImpl),
74
- callWithContext: (input, signal, context) => callWith(input, signal, context, resolveHost, connectImpl),
75
- };
76
- }
77
- export const sshDownloadTool = createSshDownloadTool();
@@ -1,92 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/tools/ssh/sshDownload.ts —— SSHDownload:远程主机 → 本地文件(SFTP)
4
- * ------------------------------------------------------------
5
- * 拆成独立工具名的理由同 sshUpload.ts 头注。落盘用临时文件 + rename 做到
6
- * 原子性——SFTP 流式写入中途失败时不留半个文件覆盖已有内容。
7
- * ============================================================
8
- */
9
-
10
- import { randomUUID } from 'node:crypto';
11
- import { mkdir, rename, rm } from 'node:fs/promises';
12
- import { dirname } from 'node:path';
13
-
14
- import { z } from 'zod';
15
-
16
- import { resolveSshHost } from '../../context/sshHosts.js';
17
- import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
18
- import { CONFIG_LIMITS } from '../../config/numericLimits.js';
19
- import { resolveToolPath } from '../shared/fileUtils.js';
20
- import { toToolParameters } from '../../utils/zodToJson.js';
21
- import { resolveSshToolAvailability } from './sshAvailability.js';
22
- import { DEFAULT_SSH_TIMEOUT_MS, defaultSshConnect, SshExecTimeoutError, type SshConnectImpl } from './sshClient.js';
23
-
24
- const inputSchema = z.object({
25
- host: z.string().optional().describe('已配置的 SSH 主机别名;不填则使用该工作区的默认主机'),
26
- remotePath: z.string().min(1).describe('远程源路径(远程文件系统语义,不做本地锚定)'),
27
- localPath: z.string().min(1).describe('本地落盘路径(相对路径基于 working dir 解析;会覆盖已存在的同名文件)'),
28
- timeoutMs: z.number().int().min(CONFIG_LIMITS.sshTimeoutMs.min).max(CONFIG_LIMITS.sshTimeoutMs.max).optional()
29
- .describe('本次传输超时(毫秒),默认 30000'),
30
- });
31
-
32
- type SshDownloadInput = z.infer<typeof inputSchema>;
33
-
34
- async function callWith(
35
- input: SshDownloadInput,
36
- signal: AbortSignal | undefined,
37
- context: ToolCallContext | undefined,
38
- resolveHost: typeof resolveSshHost,
39
- connectImpl: SshConnectImpl,
40
- ): Promise<ToolResult> {
41
- const workspaceId = context?.workspaceId;
42
- if (!workspaceId) {
43
- return { ok: false, error: '当前运行环境没有工作区身份,无法解析 SSH 主机配置(SSH 主机按工作区隔离存储)。', recovery: 'none' };
44
- }
45
- const local = resolveToolPath(input.localPath);
46
- if (!local.ok) return local;
47
-
48
- const resolved = await resolveHost(workspaceId, input.host);
49
- if (!resolved.ok) return { ok: false, error: resolved.error, recovery: 'change-input' };
50
- const host = resolved.host;
51
- const timeoutMs = input.timeoutMs ?? host.timeoutMs ?? DEFAULT_SSH_TIMEOUT_MS;
52
-
53
- let connection;
54
- try {
55
- connection = await connectImpl(host, timeoutMs);
56
- } catch (error) {
57
- return { ok: false, error: `连接主机 "${host.alias}" 失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
58
- }
59
-
60
- const tmpPath = `${local.resolvedPath}.${randomUUID()}.minimal-agent-ssh.tmp`;
61
- try {
62
- await mkdir(dirname(local.resolvedPath), { recursive: true });
63
- const bytes = await connection.get(input.remotePath, tmpPath, timeoutMs);
64
- await rename(tmpPath, local.resolvedPath);
65
- return { ok: true, content: `已下载 ${host.alias}:${input.remotePath} → ${input.localPath}(${bytes} 字节)`, sensitivity: 'local' };
66
- } catch (error) {
67
- await rm(tmpPath, { force: true }).catch(() => {});
68
- if (signal?.aborted) return { ok: false, error: 'SSH 下载已取消', recovery: 'none' };
69
- if (error instanceof SshExecTimeoutError) return { ok: false, error: error.message, recovery: 'retry' };
70
- return { ok: false, error: `从 "${host.alias}" 下载失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
71
- } finally {
72
- connection.end();
73
- }
74
- }
75
-
76
- export function createSshDownloadTool(
77
- resolveHost: typeof resolveSshHost = resolveSshHost,
78
- connectImpl: SshConnectImpl = defaultSshConnect,
79
- ): Tool<SshDownloadInput> {
80
- return {
81
- name: 'SSHDownload',
82
- description: '通过 SFTP 把远程 SSH 主机上的文件下载到本地。主机由用户在 /config → 远程主机(SSH) 中配置,按当前工作区隔离;不确定有哪些主机时先调用 SSHHosts。',
83
- inputSchema,
84
- parameters: toToolParameters(inputSchema),
85
- isReadOnly: false,
86
- resolveAvailability: resolveSshToolAvailability,
87
- call: (input, signal) => callWith(input, signal, undefined, resolveHost, connectImpl),
88
- callWithContext: (input, signal, context) => callWith(input, signal, context, resolveHost, connectImpl),
89
- };
90
- }
91
-
92
- export const sshDownloadTool = createSshDownloadTool();
@@ -1,36 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/tools/ssh/sshHostsTool.ts —— SSHHosts:列出当前工作区已配置的 SSH 主机
4
- * ------------------------------------------------------------
5
- * 存在的理由:Tool.description 只能是字符串或无参函数(同步、拿不到
6
- * workspaceId),没法把"当前工作区配置了哪些主机"实时渲染进静态描述——
7
- * 主机列表按工作区隔离且可随时增删,唯一正确的做法是让模型按需查询一次,
8
- * 而不是塞一份可能已经过期、还可能读到别的工作区数据的静态文案。
9
- * 只读、不带密码,随时可调用。
10
- * ============================================================
11
- */
12
- import { z } from 'zod';
13
- import { describeSshHosts } from '../../context/sshHosts.js';
14
- import { toToolParameters } from '../../utils/zodToJson.js';
15
- import { resolveSshToolAvailability } from './sshAvailability.js';
16
- const inputSchema = z.object({});
17
- async function callWith(_input, context, describe) {
18
- const workspaceId = context?.workspaceId;
19
- if (!workspaceId) {
20
- return { ok: false, error: '当前运行环境没有工作区身份,无法解析 SSH 主机配置(SSH 主机按工作区隔离存储)。', recovery: 'none' };
21
- }
22
- return { ok: true, content: await describe(workspaceId), sensitivity: 'local' };
23
- }
24
- export function createSshHostsTool(describe = describeSshHosts) {
25
- return {
26
- name: 'SSHHosts',
27
- description: '列出当前工作区在 /config → 远程主机(SSH) 中配置的 SSH 主机别名(不含密码),用于在调用 SSH/SSHUpload/SSHDownload 前确认可用主机。',
28
- inputSchema,
29
- parameters: toToolParameters(inputSchema),
30
- isReadOnly: true,
31
- resolveAvailability: resolveSshToolAvailability,
32
- call: (input) => callWith(input, undefined, describe),
33
- callWithContext: (input, _signal, context) => callWith(input, context, describe),
34
- };
35
- }
36
- export const sshHostsTool = createSshHostsTool();
@@ -1,48 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/tools/ssh/sshHostsTool.ts —— SSHHosts:列出当前工作区已配置的 SSH 主机
4
- * ------------------------------------------------------------
5
- * 存在的理由:Tool.description 只能是字符串或无参函数(同步、拿不到
6
- * workspaceId),没法把"当前工作区配置了哪些主机"实时渲染进静态描述——
7
- * 主机列表按工作区隔离且可随时增删,唯一正确的做法是让模型按需查询一次,
8
- * 而不是塞一份可能已经过期、还可能读到别的工作区数据的静态文案。
9
- * 只读、不带密码,随时可调用。
10
- * ============================================================
11
- */
12
-
13
- import { z } from 'zod';
14
-
15
- import { describeSshHosts } from '../../context/sshHosts.js';
16
- import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
17
- import { toToolParameters } from '../../utils/zodToJson.js';
18
- import { resolveSshToolAvailability } from './sshAvailability.js';
19
-
20
- const inputSchema = z.object({});
21
- type SshHostsInput = z.infer<typeof inputSchema>;
22
-
23
- async function callWith(
24
- _input: SshHostsInput,
25
- context: ToolCallContext | undefined,
26
- describe: typeof describeSshHosts,
27
- ): Promise<ToolResult> {
28
- const workspaceId = context?.workspaceId;
29
- if (!workspaceId) {
30
- return { ok: false, error: '当前运行环境没有工作区身份,无法解析 SSH 主机配置(SSH 主机按工作区隔离存储)。', recovery: 'none' };
31
- }
32
- return { ok: true, content: await describe(workspaceId), sensitivity: 'local' };
33
- }
34
-
35
- export function createSshHostsTool(describe: typeof describeSshHosts = describeSshHosts): Tool<SshHostsInput> {
36
- return {
37
- name: 'SSHHosts',
38
- description: '列出当前工作区在 /config → 远程主机(SSH) 中配置的 SSH 主机别名(不含密码),用于在调用 SSH/SSHUpload/SSHDownload 前确认可用主机。',
39
- inputSchema,
40
- parameters: toToolParameters(inputSchema),
41
- isReadOnly: true,
42
- resolveAvailability: resolveSshToolAvailability,
43
- call: (input) => callWith(input, undefined, describe),
44
- callWithContext: (input, _signal, context) => callWith(input, context, describe),
45
- };
46
- }
47
-
48
- export const sshHostsTool = createSshHostsTool();
@@ -1,72 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/tools/ssh/sshUpload.ts —— SSHUpload:本地文件 → 远程主机(SFTP)
4
- * ------------------------------------------------------------
5
- * 与 SSH(exec)/SSHDownload 拆成独立工具名而不是一个 action 字段:
6
- * 本地/远程路径的语义在上传和下载之间是反的,合并成一个 schema 会引入
7
- * 「当前 action 下 localPath 到底是读还是写」的歧义;拆开还让宿主 toolGate
8
- * 能按操作粒度分别放行(例如允许下载但拦截上传)。
9
- * ============================================================
10
- */
11
- import { z } from 'zod';
12
- import { resolveSshHost } from '../../context/sshHosts.js';
13
- import { CONFIG_LIMITS } from '../../config/numericLimits.js';
14
- import { resolveToolPath } from '../shared/fileUtils.js';
15
- import { toToolParameters } from '../../utils/zodToJson.js';
16
- import { resolveSshToolAvailability } from './sshAvailability.js';
17
- import { DEFAULT_SSH_TIMEOUT_MS, defaultSshConnect, SshExecTimeoutError } from './sshClient.js';
18
- const inputSchema = z.object({
19
- host: z.string().optional().describe('已配置的 SSH 主机别名;不填则使用该工作区的默认主机'),
20
- localPath: z.string().min(1).describe('本地文件路径(相对路径基于 working dir 解析)'),
21
- remotePath: z.string().min(1).describe('远程目标路径(远程文件系统语义,不做本地锚定)'),
22
- timeoutMs: z.number().int().min(CONFIG_LIMITS.sshTimeoutMs.min).max(CONFIG_LIMITS.sshTimeoutMs.max).optional()
23
- .describe('本次传输超时(毫秒),默认 30000'),
24
- });
25
- async function callWith(input, signal, context, resolveHost, connectImpl) {
26
- const workspaceId = context?.workspaceId;
27
- if (!workspaceId) {
28
- return { ok: false, error: '当前运行环境没有工作区身份,无法解析 SSH 主机配置(SSH 主机按工作区隔离存储)。', recovery: 'none' };
29
- }
30
- const local = resolveToolPath(input.localPath);
31
- if (!local.ok)
32
- return local;
33
- const resolved = await resolveHost(workspaceId, input.host);
34
- if (!resolved.ok)
35
- return { ok: false, error: resolved.error, recovery: 'change-input' };
36
- const host = resolved.host;
37
- const timeoutMs = input.timeoutMs ?? host.timeoutMs ?? DEFAULT_SSH_TIMEOUT_MS;
38
- let connection;
39
- try {
40
- connection = await connectImpl(host, timeoutMs);
41
- }
42
- catch (error) {
43
- return { ok: false, error: `连接主机 "${host.alias}" 失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
44
- }
45
- try {
46
- const bytes = await connection.put(local.resolvedPath, input.remotePath, timeoutMs);
47
- return { ok: true, content: `已上传 ${input.localPath} → ${host.alias}:${input.remotePath}(${bytes} 字节)`, sensitivity: 'local' };
48
- }
49
- catch (error) {
50
- if (signal?.aborted)
51
- return { ok: false, error: 'SSH 上传已取消', recovery: 'none' };
52
- if (error instanceof SshExecTimeoutError)
53
- return { ok: false, error: error.message, recovery: 'retry' };
54
- return { ok: false, error: `上传到 "${host.alias}" 失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
55
- }
56
- finally {
57
- connection.end();
58
- }
59
- }
60
- export function createSshUploadTool(resolveHost = resolveSshHost, connectImpl = defaultSshConnect) {
61
- return {
62
- name: 'SSHUpload',
63
- description: '通过 SFTP 把本地文件上传到已配置的远程 SSH 主机。主机由用户在 /config → 远程主机(SSH) 中配置,按当前工作区隔离;不确定有哪些主机时先调用 SSHHosts。',
64
- inputSchema,
65
- parameters: toToolParameters(inputSchema),
66
- isReadOnly: false,
67
- resolveAvailability: resolveSshToolAvailability,
68
- call: (input, signal) => callWith(input, signal, undefined, resolveHost, connectImpl),
69
- callWithContext: (input, signal, context) => callWith(input, signal, context, resolveHost, connectImpl),
70
- };
71
- }
72
- export const sshUploadTool = createSshUploadTool();
@@ -1,85 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/tools/ssh/sshUpload.ts —— SSHUpload:本地文件 → 远程主机(SFTP)
4
- * ------------------------------------------------------------
5
- * 与 SSH(exec)/SSHDownload 拆成独立工具名而不是一个 action 字段:
6
- * 本地/远程路径的语义在上传和下载之间是反的,合并成一个 schema 会引入
7
- * 「当前 action 下 localPath 到底是读还是写」的歧义;拆开还让宿主 toolGate
8
- * 能按操作粒度分别放行(例如允许下载但拦截上传)。
9
- * ============================================================
10
- */
11
-
12
- import { z } from 'zod';
13
-
14
- import { resolveSshHost } from '../../context/sshHosts.js';
15
- import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
16
- import { CONFIG_LIMITS } from '../../config/numericLimits.js';
17
- import { resolveToolPath } from '../shared/fileUtils.js';
18
- import { toToolParameters } from '../../utils/zodToJson.js';
19
- import { resolveSshToolAvailability } from './sshAvailability.js';
20
- import { DEFAULT_SSH_TIMEOUT_MS, defaultSshConnect, SshExecTimeoutError, type SshConnectImpl } from './sshClient.js';
21
-
22
- const inputSchema = z.object({
23
- host: z.string().optional().describe('已配置的 SSH 主机别名;不填则使用该工作区的默认主机'),
24
- localPath: z.string().min(1).describe('本地文件路径(相对路径基于 working dir 解析)'),
25
- remotePath: z.string().min(1).describe('远程目标路径(远程文件系统语义,不做本地锚定)'),
26
- timeoutMs: z.number().int().min(CONFIG_LIMITS.sshTimeoutMs.min).max(CONFIG_LIMITS.sshTimeoutMs.max).optional()
27
- .describe('本次传输超时(毫秒),默认 30000'),
28
- });
29
-
30
- type SshUploadInput = z.infer<typeof inputSchema>;
31
-
32
- async function callWith(
33
- input: SshUploadInput,
34
- signal: AbortSignal | undefined,
35
- context: ToolCallContext | undefined,
36
- resolveHost: typeof resolveSshHost,
37
- connectImpl: SshConnectImpl,
38
- ): Promise<ToolResult> {
39
- const workspaceId = context?.workspaceId;
40
- if (!workspaceId) {
41
- return { ok: false, error: '当前运行环境没有工作区身份,无法解析 SSH 主机配置(SSH 主机按工作区隔离存储)。', recovery: 'none' };
42
- }
43
- const local = resolveToolPath(input.localPath);
44
- if (!local.ok) return local;
45
-
46
- const resolved = await resolveHost(workspaceId, input.host);
47
- if (!resolved.ok) return { ok: false, error: resolved.error, recovery: 'change-input' };
48
- const host = resolved.host;
49
- const timeoutMs = input.timeoutMs ?? host.timeoutMs ?? DEFAULT_SSH_TIMEOUT_MS;
50
-
51
- let connection;
52
- try {
53
- connection = await connectImpl(host, timeoutMs);
54
- } catch (error) {
55
- return { ok: false, error: `连接主机 "${host.alias}" 失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
56
- }
57
- try {
58
- const bytes = await connection.put(local.resolvedPath, input.remotePath, timeoutMs);
59
- return { ok: true, content: `已上传 ${input.localPath} → ${host.alias}:${input.remotePath}(${bytes} 字节)`, sensitivity: 'local' };
60
- } catch (error) {
61
- if (signal?.aborted) return { ok: false, error: 'SSH 上传已取消', recovery: 'none' };
62
- if (error instanceof SshExecTimeoutError) return { ok: false, error: error.message, recovery: 'retry' };
63
- return { ok: false, error: `上传到 "${host.alias}" 失败:${error instanceof Error ? error.message : String(error)}`, recovery: 'retry' };
64
- } finally {
65
- connection.end();
66
- }
67
- }
68
-
69
- export function createSshUploadTool(
70
- resolveHost: typeof resolveSshHost = resolveSshHost,
71
- connectImpl: SshConnectImpl = defaultSshConnect,
72
- ): Tool<SshUploadInput> {
73
- return {
74
- name: 'SSHUpload',
75
- description: '通过 SFTP 把本地文件上传到已配置的远程 SSH 主机。主机由用户在 /config → 远程主机(SSH) 中配置,按当前工作区隔离;不确定有哪些主机时先调用 SSHHosts。',
76
- inputSchema,
77
- parameters: toToolParameters(inputSchema),
78
- isReadOnly: false,
79
- resolveAvailability: resolveSshToolAvailability,
80
- call: (input, signal) => callWith(input, signal, undefined, resolveHost, connectImpl),
81
- callWithContext: (input, signal, context) => callWith(input, signal, context, resolveHost, connectImpl),
82
- };
83
- }
84
-
85
- export const sshUploadTool = createSshUploadTool();