@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,291 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { minkoHome } from "../config/paths.js";
5
+ import { normalizeHookConfig } from "../config/store.js";
6
+ const MAX_TOOLS = 50;
7
+ const DEFAULT_TIMEOUT_MS = 30_000;
8
+ const MAX_OUTPUT = 64 * 1024;
9
+ export function pluginsDir() {
10
+ return path.join(minkoHome(), 'plugins');
11
+ }
12
+ /**
13
+ * Loads every `<plugins>/<name>/plugin.json`. A broken plugin is reported and
14
+ * skipped without affecting the others.
15
+ */
16
+ export function loadPlugins(dir = pluginsDir(), disabled = []) {
17
+ const plugins = [];
18
+ const errors = [];
19
+ let entries = [];
20
+ try {
21
+ entries = fs.readdirSync(dir, { withFileTypes: true });
22
+ }
23
+ catch {
24
+ return { plugins, errors };
25
+ }
26
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
27
+ if (!entry.isDirectory()) {
28
+ continue;
29
+ }
30
+ const pluginDir = path.join(dir, entry.name);
31
+ try {
32
+ const manifest = readManifest(pluginDir);
33
+ const name = manifest.name?.trim() || entry.name;
34
+ validateName(name);
35
+ if (disabled.includes(name)) {
36
+ continue;
37
+ }
38
+ plugins.push({
39
+ name,
40
+ dir: pluginDir,
41
+ version: manifest.version,
42
+ description: manifest.description,
43
+ tools: normalizeTools(manifest.tools, pluginDir, name),
44
+ hooks: pluginHooks(manifest.hooks, pluginDir),
45
+ });
46
+ }
47
+ catch (error) {
48
+ errors.push(entry.name + ':' + (error instanceof Error ? error.message : String(error)));
49
+ }
50
+ }
51
+ return { plugins, errors };
52
+ }
53
+ export function listPlugins(dir = pluginsDir(), disabled = []) {
54
+ const plugins = [];
55
+ const errors = [];
56
+ let entries = [];
57
+ try {
58
+ entries = fs.readdirSync(dir, { withFileTypes: true });
59
+ }
60
+ catch {
61
+ return { plugins, errors };
62
+ }
63
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
64
+ if (!entry.isDirectory()) {
65
+ continue;
66
+ }
67
+ const pluginDir = path.join(dir, entry.name);
68
+ try {
69
+ const manifest = readManifest(pluginDir);
70
+ const name = manifest.name?.trim() || entry.name;
71
+ validateName(name);
72
+ plugins.push({
73
+ name,
74
+ version: manifest.version,
75
+ description: manifest.description,
76
+ toolCount: Array.isArray(manifest.tools) ? manifest.tools.length : 0,
77
+ dir: pluginDir,
78
+ enabled: !disabled.includes(name),
79
+ });
80
+ }
81
+ catch (error) {
82
+ errors.push(entry.name + ':' + (error instanceof Error ? error.message : String(error)));
83
+ }
84
+ }
85
+ return { plugins, errors };
86
+ }
87
+ export function installPlugin(source, dir = pluginsDir()) {
88
+ const sourceDir = path.resolve(source);
89
+ const manifest = readManifest(sourceDir);
90
+ const name = manifest.name?.trim() || path.basename(sourceDir);
91
+ validateName(name);
92
+ const target = path.join(dir, name);
93
+ fs.mkdirSync(dir, { recursive: true });
94
+ fs.cpSync(sourceDir, target, { recursive: true, force: true });
95
+ return { name, path: target };
96
+ }
97
+ export function uninstallPlugin(name, dir = pluginsDir()) {
98
+ validateName(name);
99
+ const target = path.join(dir, name);
100
+ if (!fs.existsSync(target)) {
101
+ return false;
102
+ }
103
+ fs.rmSync(target, { recursive: true, force: true });
104
+ return true;
105
+ }
106
+ export function mergeHooks(...configs) {
107
+ const merged = { PreToolUse: [], PostToolUse: [], PostToolUseFailure: [] };
108
+ for (const event of ['PreToolUse', 'PostToolUse', 'PostToolUseFailure']) {
109
+ merged[event] = configs.flatMap((config) => config?.[event] ?? []);
110
+ }
111
+ return merged;
112
+ }
113
+ export function readPluginManifest(pluginDir) {
114
+ return readManifest(pluginDir);
115
+ }
116
+ function readManifest(pluginDir) {
117
+ const file = path.join(pluginDir, 'plugin.json');
118
+ let raw;
119
+ try {
120
+ raw = fs.readFileSync(file, 'utf8');
121
+ }
122
+ catch {
123
+ throw new Error('缺少 plugin.json');
124
+ }
125
+ let parsed;
126
+ try {
127
+ parsed = JSON.parse(raw);
128
+ }
129
+ catch {
130
+ throw new Error('plugin.json 不是合法 JSON');
131
+ }
132
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
133
+ throw new Error('plugin.json 必须是对象');
134
+ }
135
+ return parsed;
136
+ }
137
+ function normalizeTools(raw, pluginDir, pluginName) {
138
+ if (!Array.isArray(raw)) {
139
+ return [];
140
+ }
141
+ const tools = [];
142
+ const seen = new Set();
143
+ for (const item of raw.slice(0, MAX_TOOLS)) {
144
+ if (item === null || typeof item !== 'object') {
145
+ continue;
146
+ }
147
+ const record = item;
148
+ if (typeof record.name !== 'string' || record.name.trim().length === 0) {
149
+ continue;
150
+ }
151
+ if (typeof record.command !== 'string' || record.command.trim().length === 0) {
152
+ continue;
153
+ }
154
+ const toolName = 'plugin__' + sanitize(pluginName) + '__' + sanitize(record.name.trim());
155
+ if (seen.has(toolName)) {
156
+ continue;
157
+ }
158
+ seen.add(toolName);
159
+ const readOnly = record.readOnly !== false;
160
+ const timeoutMs = clampInt(record.timeoutMs, 1_000, 300_000) ?? DEFAULT_TIMEOUT_MS;
161
+ const parameters = isObjectSchema(record.parameters)
162
+ ? record.parameters
163
+ : { type: 'object', properties: {} };
164
+ const description = typeof record.description === 'string' && record.description.trim().length > 0
165
+ ? record.description.trim()
166
+ : record.name.trim();
167
+ tools.push({
168
+ readOnly,
169
+ needsApproval: readOnly ? undefined : () => true,
170
+ spec: {
171
+ name: toolName,
172
+ description: '[' + pluginName + '] ' + description,
173
+ parameters,
174
+ },
175
+ async run(args, ctx) {
176
+ return await runPluginTool(pluginDir, pluginName, record.command, record.name, args, ctx.signal, timeoutMs);
177
+ },
178
+ });
179
+ }
180
+ return tools;
181
+ }
182
+ function pluginHooks(raw, pluginDir) {
183
+ const normalized = normalizeHookConfig(raw);
184
+ const out = { PreToolUse: [], PostToolUse: [], PostToolUseFailure: [] };
185
+ for (const event of ['PreToolUse', 'PostToolUse', 'PostToolUseFailure']) {
186
+ out[event] = normalized[event].map((entry) => {
187
+ if (typeof entry === 'string') {
188
+ return {
189
+ hooks: [{ type: 'command', command: entry, cwd: pluginDir }],
190
+ };
191
+ }
192
+ return {
193
+ ...(entry.matcher ? { matcher: entry.matcher } : {}),
194
+ hooks: entry.hooks.map((hook) => ({
195
+ ...hook,
196
+ cwd: hook.cwd ?? pluginDir,
197
+ })),
198
+ };
199
+ });
200
+ }
201
+ return out;
202
+ }
203
+ function runPluginTool(pluginDir, pluginName, command, toolName, args, signal, timeoutMs) {
204
+ return new Promise((resolve, reject) => {
205
+ if (signal.aborted) {
206
+ reject(new Error('插件工具已中断'));
207
+ return;
208
+ }
209
+ const child = spawn(command, {
210
+ shell: true,
211
+ cwd: pluginDir,
212
+ windowsHide: true,
213
+ env: {
214
+ ...process.env,
215
+ MINKO: '1',
216
+ MINKO_PLUGIN: pluginName,
217
+ MINKO_PLUGIN_DIR: pluginDir,
218
+ MINKO_TOOL: toolName,
219
+ },
220
+ });
221
+ let stdout = '';
222
+ let stderr = '';
223
+ let settled = false;
224
+ const finish = (error, output) => {
225
+ if (settled) {
226
+ return;
227
+ }
228
+ settled = true;
229
+ clearTimeout(timer);
230
+ signal.removeEventListener('abort', onAbort);
231
+ if (error) {
232
+ reject(error);
233
+ }
234
+ else {
235
+ resolve(output ?? '');
236
+ }
237
+ };
238
+ const onAbort = () => {
239
+ child.kill();
240
+ finish(new Error('插件工具已中断'));
241
+ };
242
+ const timer = setTimeout(() => {
243
+ child.kill();
244
+ finish(new Error('插件工具超时(' + timeoutMs + 'ms):' + toolName));
245
+ }, timeoutMs);
246
+ timer.unref?.();
247
+ signal.addEventListener('abort', onAbort, { once: true });
248
+ child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
249
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
250
+ child.stdin.on('error', () => { });
251
+ child.on('error', (error) => {
252
+ finish(new Error('插件工具启动失败:' + error.message));
253
+ });
254
+ child.on('close', (code) => {
255
+ if (code === 0) {
256
+ finish(undefined, cap(stdout.trim(), MAX_OUTPUT));
257
+ return;
258
+ }
259
+ const detail = stderr.trim() || stdout.trim() || 'exit ' + String(code);
260
+ finish(new Error('插件工具失败(' + toolName + '):' + detail));
261
+ });
262
+ child.stdin.end(JSON.stringify(args));
263
+ });
264
+ }
265
+ function isObjectSchema(value) {
266
+ if (value === null || typeof value !== 'object') {
267
+ return false;
268
+ }
269
+ const record = value;
270
+ return record.type === 'object' && record.properties !== null && typeof record.properties === 'object';
271
+ }
272
+ function sanitize(value) {
273
+ return value.replace(/[^A-Za-z0-9_-]/g, '_');
274
+ }
275
+ function validateName(name) {
276
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
277
+ throw new Error('插件名只能包含字母、数字、点、下划线和短横线');
278
+ }
279
+ }
280
+ function clampInt(value, min, max) {
281
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
282
+ return undefined;
283
+ }
284
+ return Math.min(max, Math.max(min, Math.floor(value)));
285
+ }
286
+ function cap(text, max) {
287
+ if (text.length <= max) {
288
+ return text;
289
+ }
290
+ return text.slice(0, max - 20) + '\n…(插件输出过长,已截断)';
291
+ }
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { pluginsDir, readPluginManifest } from "./loader.js";
6
+ /** Fetches and validates a plugin marketplace JSON index. */
7
+ export async function fetchMarket(registryUrl) {
8
+ const response = await fetch(registryUrl, {
9
+ headers: { accept: 'application/json' },
10
+ signal: AbortSignal.timeout(10_000),
11
+ });
12
+ if (!response.ok) {
13
+ throw new Error('插件市场请求失败(HTTP ' + response.status + ')');
14
+ }
15
+ const payload = await response.json();
16
+ const raw = Array.isArray(payload)
17
+ ? payload
18
+ : payload !== null && typeof payload === 'object'
19
+ ? payload.plugins
20
+ : null;
21
+ if (!Array.isArray(raw)) {
22
+ throw new Error('插件市场索引格式不对');
23
+ }
24
+ const entries = [];
25
+ const seen = new Set();
26
+ for (const item of raw.slice(0, 200)) {
27
+ if (item === null || typeof item !== 'object') {
28
+ continue;
29
+ }
30
+ const record = item;
31
+ if (typeof record.name !== 'string' || record.name.trim().length === 0) {
32
+ continue;
33
+ }
34
+ if (typeof record.git !== 'string' || record.git.trim().length === 0) {
35
+ continue;
36
+ }
37
+ const name = record.name.trim();
38
+ if (seen.has(name)) {
39
+ continue;
40
+ }
41
+ seen.add(name);
42
+ entries.push({
43
+ name,
44
+ description: typeof record.description === 'string' ? record.description : undefined,
45
+ version: typeof record.version === 'string' ? record.version : undefined,
46
+ git: record.git.trim(),
47
+ });
48
+ }
49
+ return entries;
50
+ }
51
+ export function searchMarket(entries, query) {
52
+ const wanted = query.trim().toLowerCase();
53
+ if (wanted.length === 0) {
54
+ return entries;
55
+ }
56
+ return entries.filter((entry) => entry.name.toLowerCase().includes(wanted)
57
+ || (entry.description ?? '').toLowerCase().includes(wanted));
58
+ }
59
+ /** Clones a git repository into the plugin directory. */
60
+ export function installFromGit(url, dir = pluginsDir()) {
61
+ if (url.trim().length === 0) {
62
+ throw new Error('缺少 git 地址');
63
+ }
64
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'minko-plugin-'));
65
+ try {
66
+ try {
67
+ execFileSync('git', ['clone', '--depth', '1', url, temporary], {
68
+ stdio: ['ignore', 'pipe', 'pipe'],
69
+ timeout: 60_000,
70
+ windowsHide: true,
71
+ });
72
+ }
73
+ catch (error) {
74
+ const stderr = error.stderr;
75
+ const detail = stderr ? stderr.toString().trim() : error instanceof Error ? error.message : '';
76
+ throw new Error('git clone 失败:' + (detail || url));
77
+ }
78
+ const manifest = readPluginManifest(temporary);
79
+ const name = manifest.name?.trim()
80
+ || path.basename(url).replace(/\.git$/i, '').replace(/[^A-Za-z0-9._-]/g, '-');
81
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
82
+ throw new Error('插件名不合法:' + name);
83
+ }
84
+ const target = path.join(dir, name);
85
+ if (fs.existsSync(target)) {
86
+ throw new Error('插件已安装:' + name + '(先 minko plugin uninstall ' + name + ')');
87
+ }
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ fs.cpSync(temporary, target, { recursive: true, force: true });
90
+ return { name, path: target };
91
+ }
92
+ finally {
93
+ try {
94
+ fs.rmSync(temporary, { recursive: true, force: true });
95
+ }
96
+ catch { /* best effort */ }
97
+ }
98
+ }
@@ -0,0 +1,279 @@
1
+ import { HttpError } from "../util/errors.js";
2
+ import { retryAfterFromHeaders, withRetry } from "./retry.js";
3
+ import { sseEvents } from "./sse.js";
4
+ export function toAnthropicMessages(messages) {
5
+ const systemParts = [];
6
+ const out = [];
7
+ const push = (role, blocks) => {
8
+ if (blocks.length === 0) {
9
+ return;
10
+ }
11
+ const last = out.length > 0 ? out[out.length - 1] : undefined;
12
+ if (last && last.role === role) {
13
+ last.content.push(...blocks);
14
+ return;
15
+ }
16
+ out.push({ role, content: blocks });
17
+ };
18
+ for (const message of messages) {
19
+ if (message.role === 'system') {
20
+ systemParts.push(message.content);
21
+ continue;
22
+ }
23
+ if (message.role === 'tool') {
24
+ push('user', [{ type: 'tool_result', tool_use_id: message.toolCallId, content: message.content }]);
25
+ continue;
26
+ }
27
+ if (message.role === 'assistant') {
28
+ const blocks = [];
29
+ if (message.content.length > 0) {
30
+ blocks.push({ type: 'text', text: message.content });
31
+ }
32
+ for (const call of message.toolCalls) {
33
+ blocks.push({ type: 'tool_use', id: call.id, name: call.name, input: safeParse(call.arguments) });
34
+ }
35
+ push('assistant', blocks);
36
+ continue;
37
+ }
38
+ push('user', [{ type: 'text', text: message.content }]);
39
+ }
40
+ return { system: systemParts.join('\n\n'), messages: out };
41
+ }
42
+ function safeParse(raw) {
43
+ if (raw.trim().length === 0) {
44
+ return {};
45
+ }
46
+ try {
47
+ return JSON.parse(raw);
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ }
53
+ export class AnthropicProvider {
54
+ config;
55
+ retry;
56
+ cacheEnabled = true;
57
+ constructor(config, retry = {}) {
58
+ this.config = config;
59
+ this.retry = retry;
60
+ }
61
+ async stream(messages, tools, handlers, signal) {
62
+ const converted = toAnthropicMessages(messages);
63
+ let response;
64
+ try {
65
+ response = await this.send(converted, tools, handlers, signal, this.cacheEnabled);
66
+ }
67
+ catch (error) {
68
+ // Some Anthropic-compatible gateways reject cache_control. Retry once
69
+ // without markers so caching is an optimization, never a hard failure.
70
+ if (this.cacheEnabled
71
+ && error instanceof HttpError
72
+ && error.status === 400
73
+ && /cache_control|prompt cach/i.test(error.body)) {
74
+ this.cacheEnabled = false;
75
+ response = await this.send(converted, tools, handlers, signal, false);
76
+ }
77
+ else {
78
+ throw error;
79
+ }
80
+ }
81
+ if (!response.body) {
82
+ throw new Error('服务端没有返回可读的流');
83
+ }
84
+ const textParts = [];
85
+ const reasoningParts = [];
86
+ const toolCalls = [];
87
+ let usage = { input: 0, output: 0 };
88
+ let current = null;
89
+ for await (const event of sseEvents(response.body)) {
90
+ let payload;
91
+ try {
92
+ payload = JSON.parse(event.data);
93
+ }
94
+ catch {
95
+ continue;
96
+ }
97
+ switch (event.event) {
98
+ case 'message_start': {
99
+ const message = payload.message;
100
+ const rawUsage = message?.usage;
101
+ if (rawUsage) {
102
+ const next = {
103
+ input: numberOrZero(rawUsage.input_tokens),
104
+ output: numberOrZero(rawUsage.output_tokens),
105
+ };
106
+ const cached = numberOrZero(rawUsage.cache_read_input_tokens);
107
+ if (cached > 0) {
108
+ next.cached = cached;
109
+ }
110
+ usage = next;
111
+ handlers.onUsage?.(usage);
112
+ }
113
+ break;
114
+ }
115
+ case 'content_block_start': {
116
+ const block = payload.content_block;
117
+ const index = typeof payload.index === 'number' ? payload.index : -1;
118
+ if (!block) {
119
+ break;
120
+ }
121
+ if (block.type === 'tool_use') {
122
+ current = {
123
+ kind: 'tool',
124
+ id: typeof block.id === 'string' ? block.id : 'tool_' + index,
125
+ name: typeof block.name === 'string' ? block.name : '',
126
+ json: '',
127
+ };
128
+ }
129
+ else if (block.type === 'thinking') {
130
+ current = { kind: 'thinking', id: '', name: '', json: '' };
131
+ }
132
+ else {
133
+ current = { kind: 'text', id: '', name: '', json: '' };
134
+ const initial = block.text;
135
+ if (typeof initial === 'string' && initial.length > 0) {
136
+ textParts.push(initial);
137
+ handlers.onText?.(initial);
138
+ }
139
+ }
140
+ break;
141
+ }
142
+ case 'content_block_delta': {
143
+ const delta = payload.delta;
144
+ if (!delta) {
145
+ break;
146
+ }
147
+ if (delta.type === 'text_delta' && typeof delta.text === 'string') {
148
+ textParts.push(delta.text);
149
+ handlers.onText?.(delta.text);
150
+ }
151
+ else if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') {
152
+ reasoningParts.push(delta.thinking);
153
+ handlers.onReasoning?.(delta.thinking);
154
+ }
155
+ else if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string' && current) {
156
+ current.json += delta.partial_json;
157
+ }
158
+ break;
159
+ }
160
+ case 'content_block_stop': {
161
+ if (current && current.kind === 'tool') {
162
+ const call = {
163
+ id: current.id,
164
+ name: current.name,
165
+ arguments: current.json.trim().length > 0 ? current.json : '{}',
166
+ };
167
+ toolCalls.push(call);
168
+ handlers.onToolCall?.(call);
169
+ }
170
+ current = null;
171
+ break;
172
+ }
173
+ case 'message_delta': {
174
+ const rawUsage = payload.usage;
175
+ if (rawUsage && typeof rawUsage.output_tokens === 'number') {
176
+ const next = { input: usage.input, output: rawUsage.output_tokens };
177
+ if (usage.cached !== undefined) {
178
+ next.cached = usage.cached;
179
+ }
180
+ usage = next;
181
+ handlers.onUsage?.(usage);
182
+ }
183
+ break;
184
+ }
185
+ default:
186
+ break;
187
+ }
188
+ }
189
+ return {
190
+ text: textParts.join(''),
191
+ reasoning: reasoningParts.join(''),
192
+ toolCalls,
193
+ usage,
194
+ };
195
+ }
196
+ async send(converted, tools, handlers, signal, cacheEnabled) {
197
+ const body = buildAnthropicBody(this.config, converted, tools, cacheEnabled);
198
+ const headers = {
199
+ 'content-type': 'application/json',
200
+ 'anthropic-version': '2023-06-01',
201
+ };
202
+ if (this.config.auth === 'bearer') {
203
+ headers.authorization = 'Bearer ' + this.config.apiKey;
204
+ }
205
+ else {
206
+ headers['x-api-key'] = this.config.apiKey;
207
+ }
208
+ return await withRetry(async () => {
209
+ const result = await fetch(messagesUrl(this.config.baseUrl), {
210
+ method: 'POST',
211
+ headers,
212
+ body: JSON.stringify(body),
213
+ signal,
214
+ });
215
+ if (!result.ok) {
216
+ throw new HttpError(result.status, await result.text(), retryAfterFromHeaders(result.headers));
217
+ }
218
+ return result;
219
+ }, { ...this.retry, signal, onRetry: handlers.onRetry });
220
+ }
221
+ }
222
+ function buildAnthropicBody(config, converted, tools, cacheEnabled) {
223
+ const toolDefs = tools.map((tool) => ({
224
+ name: tool.name,
225
+ description: tool.description,
226
+ input_schema: tool.parameters,
227
+ }));
228
+ if (cacheEnabled && toolDefs.length > 0) {
229
+ toolDefs[toolDefs.length - 1].cache_control = { type: 'ephemeral' };
230
+ }
231
+ // A stable order matters: system → tools → conversation. The last block is
232
+ // the moving breakpoint, so the whole prefix before it can be reused.
233
+ const messages = cacheEnabled ? addMessageCache(converted.messages) : converted.messages;
234
+ const system = cacheEnabled && converted.system.length > 0
235
+ ? [{ type: 'text', text: converted.system, cache_control: { type: 'ephemeral' } }]
236
+ : converted.system;
237
+ const body = {
238
+ model: config.model,
239
+ max_tokens: config.maxTokens && config.maxTokens > 0 ? config.maxTokens : 8192,
240
+ system,
241
+ messages,
242
+ stream: true,
243
+ };
244
+ if (toolDefs.length > 0) {
245
+ body.tools = toolDefs;
246
+ }
247
+ return body;
248
+ }
249
+ function addMessageCache(messages) {
250
+ if (messages.length === 0) {
251
+ return messages;
252
+ }
253
+ const out = messages.map((message) => ({
254
+ role: message.role,
255
+ content: message.content.slice(),
256
+ }));
257
+ const last = out[out.length - 1];
258
+ if (last.content.length > 0) {
259
+ const index = last.content.length - 1;
260
+ last.content[index] = {
261
+ ...last.content[index],
262
+ cache_control: { type: 'ephemeral' },
263
+ };
264
+ }
265
+ return out;
266
+ }
267
+ function messagesUrl(baseUrl) {
268
+ const base = baseUrl.replace(/\/+$/, '');
269
+ if (/\/v1$/i.test(base)) {
270
+ return base + '/messages';
271
+ }
272
+ return base + '/v1/messages';
273
+ }
274
+ function numberOrZero(value) {
275
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
276
+ }
277
+ export function createAnthropicProvider(config) {
278
+ return new AnthropicProvider(config);
279
+ }