@minko-ai/minko 0.1.0 → 0.1.1

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 (189) hide show
  1. package/README.md +577 -2
  2. package/assets/mascot-preview.png +0 -0
  3. package/assets/mascot-preview.txt +46 -0
  4. package/assets/mascot-source.png +0 -0
  5. package/assets/skills/README.md +21 -0
  6. package/assets/skills/brainstorming/LICENSE +21 -0
  7. package/assets/skills/brainstorming/SKILL.md +316 -0
  8. package/assets/skills/buddhist-method/LICENSE +21 -0
  9. package/assets/skills/buddhist-method/SKILL.md +137 -0
  10. package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
  11. package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
  12. package/assets/skills/code-humanizer/LICENSE +21 -0
  13. package/assets/skills/code-humanizer/SKILL.md +174 -0
  14. package/assets/skills/code-review/LICENSE +21 -0
  15. package/assets/skills/code-review/SKILL.md +232 -0
  16. package/assets/skills/code-review/reference/angular.md +768 -0
  17. package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
  18. package/assets/skills/code-review/reference/c.md +890 -0
  19. package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
  20. package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
  21. package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
  22. package/assets/skills/code-review/reference/cpp.md +893 -0
  23. package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  24. package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  25. package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  26. package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  27. package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  28. package/assets/skills/code-review/reference/csharp.md +525 -0
  29. package/assets/skills/code-review/reference/css-less-sass.md +661 -0
  30. package/assets/skills/code-review/reference/dart.md +670 -0
  31. package/assets/skills/code-review/reference/django.md +985 -0
  32. package/assets/skills/code-review/reference/fastapi.md +580 -0
  33. package/assets/skills/code-review/reference/go.md +993 -0
  34. package/assets/skills/code-review/reference/java.md +409 -0
  35. package/assets/skills/code-review/reference/java8.md +586 -0
  36. package/assets/skills/code-review/reference/kotlin.md +1018 -0
  37. package/assets/skills/code-review/reference/nestjs.md +593 -0
  38. package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
  39. package/assets/skills/code-review/reference/php.md +684 -0
  40. package/assets/skills/code-review/reference/python.md +1073 -0
  41. package/assets/skills/code-review/reference/qt.md +757 -0
  42. package/assets/skills/code-review/reference/react.md +871 -0
  43. package/assets/skills/code-review/reference/ruby.md +964 -0
  44. package/assets/skills/code-review/reference/rust.md +846 -0
  45. package/assets/skills/code-review/reference/security-review-guide.md +494 -0
  46. package/assets/skills/code-review/reference/svelte.md +1064 -0
  47. package/assets/skills/code-review/reference/swift.md +936 -0
  48. package/assets/skills/code-review/reference/typescript.md +1016 -0
  49. package/assets/skills/code-review/reference/vue.md +924 -0
  50. package/assets/skills/code-review/reference/zig.md +440 -0
  51. package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
  52. package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
  53. package/assets/skills/five-whys/LICENSE +21 -0
  54. package/assets/skills/five-whys/SKILL.md +247 -0
  55. package/assets/skills/five-whys/examples/performance-regression.md +161 -0
  56. package/assets/skills/five-whys/examples/production-incident.md +134 -0
  57. package/assets/skills/five-whys/references/software-patterns.md +284 -0
  58. package/assets/skills/five-whys/references/toyota-origins.md +257 -0
  59. package/assets/skills/systematic-debugging/LICENSE +21 -0
  60. package/assets/skills/systematic-debugging/SKILL.md +283 -0
  61. package/assets/skills/test-driven-development/LICENSE +21 -0
  62. package/assets/skills/test-driven-development/SKILL.md +330 -0
  63. package/assets/skills/verification-before-completion/LICENSE +21 -0
  64. package/assets/skills/verification-before-completion/SKILL.md +120 -0
  65. package/assets/skills/vibe-sec/LICENSE +201 -0
  66. package/assets/skills/vibe-sec/SKILL.md +758 -0
  67. package/assets/skills/writing-plans/LICENSE +21 -0
  68. package/assets/skills/writing-plans/SKILL.md +204 -0
  69. package/bin/minko.js +2 -2
  70. package/dist/agent/approvalTokens.js +224 -0
  71. package/dist/agent/branchLock.js +315 -0
  72. package/dist/agent/compact.js +182 -0
  73. package/dist/agent/context.js +183 -0
  74. package/dist/agent/cron.js +326 -0
  75. package/dist/agent/hooks.js +211 -0
  76. package/dist/agent/lanes.js +511 -0
  77. package/dist/agent/loop.js +337 -0
  78. package/dist/agent/memory.js +159 -0
  79. package/dist/agent/permissions.js +71 -0
  80. package/dist/agent/plan.js +118 -0
  81. package/dist/agent/prompt.js +120 -0
  82. package/dist/agent/subagent.js +67 -0
  83. package/dist/agent/tasks.js +449 -0
  84. package/dist/agent/transcript.js +62 -0
  85. package/dist/agent/workerStore.js +172 -0
  86. package/dist/agent/workerTypes.js +1 -0
  87. package/dist/agent/workers.js +503 -0
  88. package/dist/cli/app.js +261 -0
  89. package/dist/cli/argv.js +206 -0
  90. package/dist/cli/commandsCommand.js +12 -0
  91. package/dist/cli/configCommand.js +170 -0
  92. package/dist/cli/doctorCommand.js +112 -0
  93. package/dist/cli/exportCommand.js +85 -0
  94. package/dist/cli/forkCommand.js +24 -0
  95. package/dist/cli/linkCommand.js +272 -0
  96. package/dist/cli/loginCommand.js +69 -0
  97. package/dist/cli/mcpLoginCommand.js +68 -0
  98. package/dist/cli/pluginCommand.js +113 -0
  99. package/dist/cli/powerCommand.js +174 -0
  100. package/dist/cli/promptFooter.js +74 -0
  101. package/dist/cli/relayCommand.js +38 -0
  102. package/dist/cli/sandboxCommand.js +31 -0
  103. package/dist/cli/serveCommand.js +104 -0
  104. package/dist/cli/statusCommand.js +120 -0
  105. package/dist/cli/tui.js +1104 -0
  106. package/dist/commands/registry.js +99 -0
  107. package/dist/config/paths.js +81 -0
  108. package/dist/config/store.js +605 -0
  109. package/dist/index.js +199 -0
  110. package/dist/link/protocol.js +159 -0
  111. package/dist/link/relay.js +184 -0
  112. package/dist/link/server.js +566 -0
  113. package/dist/link/tunnel.js +90 -0
  114. package/dist/lsp/client.js +340 -0
  115. package/dist/lsp/tool.js +177 -0
  116. package/dist/mcp/client.js +469 -0
  117. package/dist/mcp/httpClient.js +191 -0
  118. package/dist/mcp/server.js +206 -0
  119. package/dist/mcp/tools.js +173 -0
  120. package/dist/oauth/device.js +111 -0
  121. package/dist/oauth/store.js +56 -0
  122. package/dist/plugins/loader.js +291 -0
  123. package/dist/plugins/market.js +98 -0
  124. package/dist/providers/anthropic.js +279 -0
  125. package/dist/providers/factory.js +109 -0
  126. package/dist/providers/openai.js +182 -0
  127. package/dist/providers/retry.js +95 -0
  128. package/dist/providers/sse.js +67 -0
  129. package/dist/providers/types.js +10 -0
  130. package/dist/rag/embeddings.js +171 -0
  131. package/dist/rag/index.js +373 -0
  132. package/dist/rag/server.js +167 -0
  133. package/dist/rag/tool.js +148 -0
  134. package/dist/sandbox/index.js +55 -0
  135. package/dist/sdk/server.js +150 -0
  136. package/dist/sdk/session.js +104 -0
  137. package/dist/session/liveness.js +148 -0
  138. package/dist/session/recorder.js +49 -0
  139. package/dist/session/store.js +171 -0
  140. package/dist/skills/index.js +428 -0
  141. package/dist/theme.js +30 -0
  142. package/dist/tools/fs.js +195 -0
  143. package/dist/tools/notebook.js +109 -0
  144. package/dist/tools/registry.js +32 -0
  145. package/dist/tools/search.js +130 -0
  146. package/dist/tools/shell.js +200 -0
  147. package/dist/tools/structured.js +83 -0
  148. package/dist/tools/types.js +1 -0
  149. package/dist/ui/ansi.js +135 -0
  150. package/dist/ui/banner.js +103 -0
  151. package/dist/ui/input.js +49 -0
  152. package/dist/ui/inputModel.js +113 -0
  153. package/dist/ui/keyboard.js +142 -0
  154. package/dist/ui/mascot.js +34 -0
  155. package/dist/ui/mascotData.js +96 -0
  156. package/dist/ui/md.js +183 -0
  157. package/dist/ui/screen.js +49 -0
  158. package/dist/ui/spinner.js +52 -0
  159. package/dist/ui/transcript.js +287 -0
  160. package/dist/util/clipboard.js +53 -0
  161. package/dist/util/errors.js +50 -0
  162. package/dist/util/version.js +12 -0
  163. package/dist/web/fetch.js +168 -0
  164. package/dist/web/search.js +97 -0
  165. package/package.json +45 -36
  166. package/src/agent/loop.ts +0 -159
  167. package/src/agent/prompt.ts +0 -45
  168. package/src/cli/app.ts +0 -396
  169. package/src/cli/argv.ts +0 -139
  170. package/src/cli/configCommand.ts +0 -175
  171. package/src/config/paths.ts +0 -20
  172. package/src/config/store.ts +0 -149
  173. package/src/index.ts +0 -120
  174. package/src/providers/anthropic.ts +0 -215
  175. package/src/providers/openai.ts +0 -158
  176. package/src/providers/sse.ts +0 -57
  177. package/src/providers/types.ts +0 -59
  178. package/src/session/store.ts +0 -125
  179. package/src/theme.ts +0 -41
  180. package/src/tools/fs.ts +0 -143
  181. package/src/tools/registry.ts +0 -19
  182. package/src/tools/shell.ts +0 -120
  183. package/src/tools/types.ts +0 -26
  184. package/src/ui/ansi.ts +0 -110
  185. package/src/ui/banner.ts +0 -39
  186. package/src/ui/input.ts +0 -54
  187. package/src/ui/md.ts +0 -160
  188. package/src/ui/spinner.ts +0 -58
  189. package/src/util/errors.ts +0 -48
@@ -0,0 +1,109 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const MAX_BYTES = 5 * 1024 * 1024;
4
+ export const notebookEditTool = {
5
+ readOnly: false,
6
+ needsApproval: (args, cwd) => {
7
+ const file = typeof args.path === 'string' ? args.path : '';
8
+ return file.length === 0 || outsideWorkspace(file, cwd);
9
+ },
10
+ spec: {
11
+ name: 'notebook_edit',
12
+ description: '编辑 Jupyter .ipynb 的单元格:replace / insert / delete。',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ path: { type: 'string' },
17
+ action: { type: 'string', enum: ['replace', 'insert', 'delete'] },
18
+ cell_index: { type: 'number', description: '0-based;insert 时是插入位置' },
19
+ cell_id: { type: 'string', description: '优先用 cell id 定位' },
20
+ cell_type: { type: 'string', enum: ['code', 'markdown', 'raw'] },
21
+ source: { type: 'string' },
22
+ },
23
+ required: ['path', 'action'],
24
+ additionalProperties: false,
25
+ },
26
+ },
27
+ async run(args, ctx) {
28
+ const rawPath = typeof args.path === 'string' ? args.path.trim() : '';
29
+ if (rawPath.length === 0) {
30
+ throw new Error('缺少 path 参数');
31
+ }
32
+ const file = path.resolve(ctx.cwd, rawPath);
33
+ const stat = fs.statSync(file);
34
+ if (stat.size > MAX_BYTES) {
35
+ throw new Error('notebook 太大(超过 5MB)');
36
+ }
37
+ const notebook = JSON.parse(fs.readFileSync(file, 'utf8'));
38
+ if (!notebook || !Array.isArray(notebook.cells)) {
39
+ throw new Error('不是合法的 Jupyter notebook');
40
+ }
41
+ const action = typeof args.action === 'string' ? args.action : '';
42
+ const index = resolveIndex(notebook, args);
43
+ if (action === 'replace') {
44
+ if (index < 0 || index >= notebook.cells.length) {
45
+ throw new Error('cell_index 越界');
46
+ }
47
+ const source = requireSource(args);
48
+ notebook.cells[index] = {
49
+ ...notebook.cells[index],
50
+ source: toLines(source),
51
+ ...(typeof args.cell_type === 'string'
52
+ ? { cell_type: args.cell_type }
53
+ : {}),
54
+ };
55
+ write(file, notebook);
56
+ return '已替换 cell ' + index + '(' + notebook.cells[index].cell_type + ')';
57
+ }
58
+ if (action === 'insert') {
59
+ const at = index < 0 || index > notebook.cells.length ? notebook.cells.length : index;
60
+ const cellType = args.cell_type === 'markdown' || args.cell_type === 'raw'
61
+ ? args.cell_type
62
+ : 'code';
63
+ const cell = {
64
+ cell_type: cellType,
65
+ source: toLines(requireSource(args)),
66
+ metadata: {},
67
+ ...(cellType === 'code' ? { execution_count: null, outputs: [] } : {}),
68
+ };
69
+ notebook.cells.splice(at, 0, cell);
70
+ write(file, notebook);
71
+ return '已在位置 ' + at + ' 插入 ' + cellType + ' cell';
72
+ }
73
+ if (action === 'delete') {
74
+ if (index < 0 || index >= notebook.cells.length) {
75
+ throw new Error('cell_index 越界');
76
+ }
77
+ notebook.cells.splice(index, 1);
78
+ write(file, notebook);
79
+ return '已删除 cell ' + index;
80
+ }
81
+ throw new Error('不支持的操作:' + action);
82
+ },
83
+ };
84
+ function resolveIndex(notebook, args) {
85
+ if (typeof args.cell_id === 'string' && args.cell_id.trim().length > 0) {
86
+ return notebook.cells.findIndex((cell) => cell.id === args.cell_id);
87
+ }
88
+ return typeof args.cell_index === 'number' && Number.isFinite(args.cell_index)
89
+ ? Math.floor(args.cell_index)
90
+ : -1;
91
+ }
92
+ function requireSource(args) {
93
+ if (typeof args.source !== 'string' || args.source.length === 0) {
94
+ throw new Error('缺少 source 参数');
95
+ }
96
+ return args.source;
97
+ }
98
+ function toLines(source) {
99
+ const lines = source.split(/\r?\n/);
100
+ return lines.map((line, index) => index < lines.length - 1 ? line + '\n' : line);
101
+ }
102
+ function write(file, notebook) {
103
+ fs.writeFileSync(file, JSON.stringify(notebook, null, 1) + '\n', 'utf8');
104
+ }
105
+ function outsideWorkspace(file, cwd) {
106
+ const target = path.resolve(cwd, file);
107
+ const relative = path.relative(cwd, target);
108
+ return relative.startsWith('..') || path.isAbsolute(relative);
109
+ }
@@ -0,0 +1,32 @@
1
+ import { editFileTool, listDirTool, readFileTool, writeFileTool } from "./fs.js";
2
+ import { runShellTool } from "./shell.js";
3
+ import { searchFilesTool } from "./search.js";
4
+ import { retrieveContextTool } from "../rag/tool.js";
5
+ import { lspTool } from "../lsp/tool.js";
6
+ import { webFetchTool } from "../web/fetch.js";
7
+ import { webSearchTool } from "../web/search.js";
8
+ import { skillListTool, skillTool } from "../skills/index.js";
9
+ import { notebookEditTool } from "./notebook.js";
10
+ import { structuredOutputTool } from "./structured.js";
11
+ export const ALL_TOOLS = [
12
+ readFileTool,
13
+ editFileTool,
14
+ writeFileTool,
15
+ listDirTool,
16
+ searchFilesTool,
17
+ retrieveContextTool,
18
+ lspTool,
19
+ webFetchTool,
20
+ webSearchTool,
21
+ skillListTool,
22
+ skillTool,
23
+ notebookEditTool,
24
+ structuredOutputTool,
25
+ runShellTool,
26
+ ];
27
+ export function toolSpecs(tools = ALL_TOOLS) {
28
+ return tools.map((tool) => tool.spec);
29
+ }
30
+ export function findTool(name, tools = ALL_TOOLS) {
31
+ return tools.find((tool) => tool.spec.name === name);
32
+ }
@@ -0,0 +1,130 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.minko', '__pycache__', '.venv']);
4
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
5
+ const MAX_RESULTS = 100;
6
+ function resolvePath(cwd, raw) {
7
+ const value = typeof raw === 'string' ? raw.trim() : '';
8
+ if (value.length === 0) {
9
+ throw new Error('缺少 path 参数');
10
+ }
11
+ return path.resolve(cwd, value);
12
+ }
13
+ function isBinary(file) {
14
+ try {
15
+ const fd = fs.openSync(file, 'r');
16
+ const buffer = Buffer.alloc(4096);
17
+ const read = fs.readSync(fd, buffer, 0, buffer.length, 0);
18
+ fs.closeSync(fd);
19
+ return buffer.subarray(0, read).includes(0);
20
+ }
21
+ catch {
22
+ return true;
23
+ }
24
+ }
25
+ export const searchFilesTool = {
26
+ readOnly: true,
27
+ spec: {
28
+ name: 'search_files',
29
+ description: '在目录里递归搜索文件内容,返回匹配的「文件:行号: 内容」。'
30
+ + 'pattern 按正则解析,正则写错时按普通文本匹配。用于快速定位代码,不必自己 shell 拼接。',
31
+ parameters: {
32
+ type: 'object',
33
+ properties: {
34
+ pattern: { type: 'string', description: '要搜索的文本或正则' },
35
+ path: { type: 'string', description: '搜索起点目录,默认当前工作目录' },
36
+ max_results: { type: 'number', description: '最多返回多少条,默认 100' },
37
+ },
38
+ required: ['pattern'],
39
+ additionalProperties: false,
40
+ },
41
+ },
42
+ async run(args, ctx) {
43
+ const pattern = typeof args.pattern === 'string' ? args.pattern : '';
44
+ if (pattern.length === 0) {
45
+ throw new Error('缺少 pattern 参数');
46
+ }
47
+ const root = args.path === undefined ? ctx.cwd : resolvePath(ctx.cwd, args.path);
48
+ const limit = typeof args.max_results === 'number' && args.max_results > 0
49
+ ? Math.min(Math.floor(args.max_results), MAX_RESULTS)
50
+ : MAX_RESULTS;
51
+ let matcher;
52
+ try {
53
+ const regex = new RegExp(pattern, 'i');
54
+ matcher = (line) => regex.test(line);
55
+ }
56
+ catch {
57
+ const needle = pattern.toLowerCase();
58
+ matcher = (line) => line.toLowerCase().includes(needle);
59
+ }
60
+ const results = [];
61
+ let scanned = 0;
62
+ const walk = (dir) => {
63
+ if (results.length >= limit) {
64
+ return;
65
+ }
66
+ let entries;
67
+ try {
68
+ entries = fs.readdirSync(dir, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ return;
72
+ }
73
+ for (const entry of entries) {
74
+ if (results.length >= limit) {
75
+ return;
76
+ }
77
+ const full = path.join(dir, entry.name);
78
+ if (entry.isDirectory()) {
79
+ if (SKIP_DIRS.has(entry.name)) {
80
+ continue;
81
+ }
82
+ walk(full);
83
+ continue;
84
+ }
85
+ if (!entry.isFile()) {
86
+ continue;
87
+ }
88
+ let stat;
89
+ try {
90
+ stat = fs.statSync(full);
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ if (stat.size > MAX_FILE_BYTES) {
96
+ continue;
97
+ }
98
+ scanned += 1;
99
+ if (isBinary(full)) {
100
+ continue;
101
+ }
102
+ let lines;
103
+ try {
104
+ lines = fs.readFileSync(full, 'utf8').replace(/\r\n/g, '\n').split('\n');
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ for (let index = 0; index < lines.length; index += 1) {
110
+ if (!matcher(lines[index])) {
111
+ continue;
112
+ }
113
+ const relative = path.relative(ctx.cwd, full) || full;
114
+ const text = lines[index].trim();
115
+ results.push(relative + ':' + (index + 1) + ': '
116
+ + (text.length > 160 ? text.slice(0, 160) + '…' : text));
117
+ if (results.length >= limit) {
118
+ return;
119
+ }
120
+ }
121
+ }
122
+ };
123
+ walk(root);
124
+ if (results.length === 0) {
125
+ return '没有匹配(扫描了 ' + scanned + ' 个文件)';
126
+ }
127
+ const more = results.length >= limit ? '\n… 结果已达上限 ' + limit + ' 条' : '';
128
+ return results.join('\n') + more;
129
+ },
130
+ };
@@ -0,0 +1,200 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { dockerArgs, dockerAvailable, resolveSandbox } from "../sandbox/index.js";
3
+ const MAX_OUTPUT = 32 * 1024;
4
+ const DEFAULT_TIMEOUT_MS = 120000;
5
+ /** Read-only commands that never need a confirmation. */
6
+ const SAFE_PREFIXES = [
7
+ 'dir', 'ls', 'cd', 'pwd', 'echo', 'type', 'cat', 'head', 'tail',
8
+ 'where', 'which', 'ver', 'set ', 'systeminfo', 'tasklist', 'hostname', 'whoami',
9
+ 'findstr', 'find ', 'sort', 'more', 'tree', 'cls',
10
+ 'node -v', 'node --version', 'npm -v', 'npm --version', 'npm ls', 'npm list',
11
+ 'npm test', 'npm run test', 'npm run build', 'npm run lint', 'npm run typecheck',
12
+ 'git status', 'git diff', 'git log', 'git show', 'git branch', 'git remote',
13
+ 'python --version', 'python -v', 'py --version',
14
+ ];
15
+ /** Commands that are always risky, whatever else they contain. */
16
+ const DANGEROUS = [
17
+ 'del ', 'erase ', 'rd ', 'rmdir', 'rm ', 'format ', 'mkfs',
18
+ 'shutdown', 'reg delete', 'taskkill', 'sc delete',
19
+ 'git push', 'git reset --hard', 'git clean',
20
+ 'npm install -g', 'npm i -g', 'npm publish',
21
+ 'curl ', 'wget ', 'invoke-webrequest', 'iwr ',
22
+ ];
23
+ export function isSafeCommand(command) {
24
+ const text = command.trim().toLowerCase().replace(/\s+/g, ' ');
25
+ if (text.length === 0) {
26
+ return true;
27
+ }
28
+ if (DANGEROUS.some((prefix) => text.includes(prefix))) {
29
+ return false;
30
+ }
31
+ // Chaining, redirection, substitution: never treat as a read-only command.
32
+ if (/[>|&;`]/.test(text) || text.includes('$(') || text.includes('\n')) {
33
+ return false;
34
+ }
35
+ return SAFE_PREFIXES.some((prefix) => text === prefix.trim() || text.startsWith(prefix));
36
+ }
37
+ export const runShellTool = {
38
+ readOnly: false,
39
+ needsApproval: (args) => !isSafeCommand(typeof args.command === 'string' ? args.command : ''),
40
+ spec: {
41
+ name: 'run_shell',
42
+ description: '在用户的电脑上执行一条 shell 命令并返回输出。Windows 用 cmd.exe,macOS/Linux 用 sh。'
43
+ + '命令会在当前工作目录里执行。不要执行交互式命令(会一直等下一条输入)。',
44
+ parameters: {
45
+ type: 'object',
46
+ properties: {
47
+ command: { type: 'string', description: '要执行的命令' },
48
+ timeout_ms: { type: 'number', description: '超时毫秒数,默认 120000' },
49
+ },
50
+ required: ['command'],
51
+ additionalProperties: false,
52
+ },
53
+ },
54
+ async run(args, ctx) {
55
+ const command = typeof args.command === 'string' ? args.command.trim() : '';
56
+ if (command.length === 0) {
57
+ throw new Error('缺少 command 参数');
58
+ }
59
+ const timeout = typeof args.timeout_ms === 'number' && args.timeout_ms > 0
60
+ ? Math.min(Math.floor(args.timeout_ms), 600000)
61
+ : DEFAULT_TIMEOUT_MS;
62
+ return await execute(command, ctx.cwd, timeout, ctx.signal, resolveSandbox(ctx.config));
63
+ },
64
+ };
65
+ export async function execute(command, cwd, timeoutMs, signal, sandbox) {
66
+ let prefix = '';
67
+ let useDocker = false;
68
+ if (sandbox?.mode === 'docker') {
69
+ if (dockerAvailable()) {
70
+ useDocker = true;
71
+ }
72
+ else if (sandbox.allowFallback) {
73
+ prefix = '(沙箱不可用,已回退普通 shell)\n';
74
+ }
75
+ else {
76
+ return '沙箱不可用:Docker 没有运行。请启动 Docker,或关闭 sandbox.mode / 设置 allowFallback。';
77
+ }
78
+ }
79
+ // cmd.exe writes pipes in the console code page (GBK on Chinese Windows).
80
+ // Decode as UTF-8 first; if that fails, fall back to GB18030.
81
+ return await new Promise((resolve) => {
82
+ const child = useDocker && sandbox
83
+ ? spawn('docker', dockerArgs(sandbox, command, cwd), {
84
+ cwd,
85
+ shell: false,
86
+ windowsHide: true,
87
+ env: { ...process.env, MINKO: '1' },
88
+ })
89
+ : spawn(command, {
90
+ cwd,
91
+ shell: true,
92
+ windowsHide: true,
93
+ env: { ...process.env, MINKO: '1' },
94
+ });
95
+ let stdout = '';
96
+ let truncated = false;
97
+ let settled = false;
98
+ const outChunks = [];
99
+ const errChunks = [];
100
+ let outBytes = 0;
101
+ let errBytes = 0;
102
+ const append = (target, chunk) => {
103
+ if (target === 'out') {
104
+ if (outBytes < MAX_OUTPUT) {
105
+ outChunks.push(chunk);
106
+ outBytes += chunk.length;
107
+ }
108
+ else {
109
+ truncated = true;
110
+ }
111
+ }
112
+ else {
113
+ if (errBytes < MAX_OUTPUT) {
114
+ errChunks.push(chunk);
115
+ errBytes += chunk.length;
116
+ }
117
+ else {
118
+ truncated = true;
119
+ }
120
+ }
121
+ };
122
+ child.stdout?.on('data', (chunk) => append('out', chunk));
123
+ child.stderr?.on('data', (chunk) => append('err', chunk));
124
+ const finish = (tail) => {
125
+ if (settled) {
126
+ return;
127
+ }
128
+ settled = true;
129
+ clearTimeout(timer);
130
+ signal?.removeEventListener('abort', onAbort);
131
+ const stdout = decodeOutput(Buffer.concat(outChunks));
132
+ const stderr = decodeOutput(Buffer.concat(errChunks));
133
+ const parts = [];
134
+ if (stdout.trim().length > 0) {
135
+ parts.push(stdout.trimEnd());
136
+ }
137
+ if (stderr.trim().length > 0) {
138
+ parts.push('--- stderr ---\n' + stderr.trimEnd());
139
+ }
140
+ if (truncated) {
141
+ parts.push('(输出超过 ' + Math.floor(MAX_OUTPUT / 1024) + ' KB,已截断)');
142
+ }
143
+ if (parts.length === 0) {
144
+ parts.push('(没有输出)');
145
+ }
146
+ parts.push(tail);
147
+ resolve(prefix + parts.join('\n'));
148
+ };
149
+ const timer = setTimeout(() => {
150
+ killTree(child);
151
+ finish('退出码:-1(超过 ' + Math.round(timeoutMs / 1000) + ' 秒被终止)');
152
+ }, timeoutMs);
153
+ const onAbort = () => {
154
+ killTree(child);
155
+ finish('退出码:-1(用户中断)');
156
+ };
157
+ signal?.addEventListener('abort', onAbort, { once: true });
158
+ child.on('error', (error) => {
159
+ finish('无法启动命令:' + error.message);
160
+ });
161
+ child.on('close', (code) => {
162
+ finish('退出码:' + (code === null ? -1 : code));
163
+ });
164
+ });
165
+ }
166
+ /** UTF-8 first, GB18030 when cmd.exe hands us OEM-encoded bytes. */
167
+ export function decodeOutput(bytes) {
168
+ if (bytes.length === 0) {
169
+ return '';
170
+ }
171
+ const utf8 = bytes.toString('utf8');
172
+ if (!utf8.includes('�')) {
173
+ return utf8;
174
+ }
175
+ try {
176
+ return new TextDecoder('gb18030').decode(bytes);
177
+ }
178
+ catch {
179
+ return utf8;
180
+ }
181
+ }
182
+ function killTree(child) {
183
+ if (child.pid === undefined) {
184
+ return;
185
+ }
186
+ try {
187
+ if (process.platform === 'win32') {
188
+ spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
189
+ }
190
+ else {
191
+ child.kill('SIGTERM');
192
+ }
193
+ }
194
+ catch {
195
+ try {
196
+ child.kill();
197
+ }
198
+ catch { /* already gone */ }
199
+ }
200
+ }
@@ -0,0 +1,83 @@
1
+ /** Validates the small JSON Schema subset used by tool parameters. */
2
+ export function validateValue(value, schema, pathName = '$') {
3
+ const errors = [];
4
+ if (typeof schema.type === 'string' && !matchesType(value, schema.type)) {
5
+ errors.push(pathName + ' 需要是 ' + schema.type);
6
+ return errors;
7
+ }
8
+ if (Array.isArray(schema.enum) && !schema.enum.some((item) => deepEqual(item, value))) {
9
+ errors.push(pathName + ' 必须是 ' + JSON.stringify(schema.enum) + ' 之一');
10
+ }
11
+ if (schema.type === 'object' && value !== null && typeof value === 'object' && !Array.isArray(value)) {
12
+ const record = value;
13
+ if (Array.isArray(schema.required)) {
14
+ for (const key of schema.required) {
15
+ if (typeof key === 'string' && !(key in record)) {
16
+ errors.push(pathName + ' 缺少字段 ' + key);
17
+ }
18
+ }
19
+ }
20
+ const properties = schema.properties;
21
+ if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
22
+ for (const [key, child] of Object.entries(properties)) {
23
+ if (!(key in record)) {
24
+ continue;
25
+ }
26
+ if (child !== null && typeof child === 'object') {
27
+ errors.push(...validateValue(record[key], child, pathName + '.' + key));
28
+ }
29
+ }
30
+ }
31
+ }
32
+ if (schema.type === 'array' && Array.isArray(value) && schema.items !== null
33
+ && typeof schema.items === 'object') {
34
+ value.forEach((item, index) => {
35
+ errors.push(...validateValue(item, schema.items, pathName + '[' + index + ']'));
36
+ });
37
+ }
38
+ return errors;
39
+ }
40
+ export const structuredOutputTool = {
41
+ readOnly: true,
42
+ spec: {
43
+ name: 'structured_output',
44
+ description: '把结果作为结构化 JSON 输出,并按可选 JSON Schema 校验(object/array/string/number/boolean/null、required、properties、items、enum)。',
45
+ parameters: {
46
+ type: 'object',
47
+ properties: {
48
+ data: { type: 'object', description: '要输出的 JSON 值' },
49
+ schema: { type: 'object', description: '可选的 JSON Schema' },
50
+ },
51
+ required: ['data'],
52
+ additionalProperties: false,
53
+ },
54
+ },
55
+ async run(args) {
56
+ if (!('data' in args)) {
57
+ throw new Error('缺少 data 参数');
58
+ }
59
+ const schema = args.schema;
60
+ if (schema !== null && typeof schema === 'object' && !Array.isArray(schema)) {
61
+ const errors = validateValue(args.data, schema);
62
+ if (errors.length > 0) {
63
+ throw new Error('结构校验失败:\n- ' + errors.join('\n- '));
64
+ }
65
+ }
66
+ return '结构化输出(已通过校验):\n' + JSON.stringify(args.data, null, 2);
67
+ },
68
+ };
69
+ function matchesType(value, type) {
70
+ switch (type) {
71
+ case 'object': return value !== null && typeof value === 'object' && !Array.isArray(value);
72
+ case 'array': return Array.isArray(value);
73
+ case 'string': return typeof value === 'string';
74
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
75
+ case 'integer': return typeof value === 'number' && Number.isInteger(value);
76
+ case 'boolean': return typeof value === 'boolean';
77
+ case 'null': return value === null;
78
+ default: return true;
79
+ }
80
+ }
81
+ function deepEqual(a, b) {
82
+ return JSON.stringify(a) === JSON.stringify(b);
83
+ }
@@ -0,0 +1 @@
1
+ export {};