@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,340 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { decodeOutput } from "../tools/shell.js";
6
+ const INIT_TIMEOUT_MS = 15_000;
7
+ const REQUEST_TIMEOUT_MS = 20_000;
8
+ /** Minimal LSP client using Content-Length framed JSON-RPC over stdio. */
9
+ export class LspClient {
10
+ options;
11
+ child = null;
12
+ buffer = Buffer.alloc(0);
13
+ nextId = 0;
14
+ pending = new Map();
15
+ documents = new Set();
16
+ stderr = '';
17
+ closed = false;
18
+ constructor(options) {
19
+ this.options = options;
20
+ }
21
+ async start() {
22
+ const launch = chooseSpawn(this.options.command);
23
+ const child = spawn(launch.command, this.options.args ?? [], {
24
+ cwd: this.options.cwd,
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ windowsHide: true,
27
+ shell: launch.shell,
28
+ });
29
+ this.child = child;
30
+ child.stdout.on('data', (chunk) => this.onData(chunk));
31
+ child.stderr.on('data', (chunk) => {
32
+ // cmd.exe writes its own errors in the console code page (GBK on zh-CN).
33
+ this.stderr = (this.stderr + decodeOutput(chunk)).slice(-4_000);
34
+ });
35
+ child.stdin.on('error', () => { });
36
+ child.on('error', (error) => this.failAll('LSP 进程启动失败:' + error.message));
37
+ child.on('exit', (code) => {
38
+ if (!this.closed) {
39
+ const detail = this.stderr.trim();
40
+ this.failAll('LSP 进程退出(code ' + String(code) + ')' + (detail ? ':' + detail : ''));
41
+ }
42
+ });
43
+ const rootUri = pathToFileURL(this.options.cwd).href;
44
+ await this.request('initialize', {
45
+ processId: process.pid,
46
+ rootUri,
47
+ capabilities: {
48
+ textDocument: {
49
+ definition: {},
50
+ references: {},
51
+ hover: {},
52
+ documentSymbol: {},
53
+ },
54
+ },
55
+ workspaceFolders: [{ uri: rootUri, name: path.basename(this.options.cwd) || 'workspace' }],
56
+ }, INIT_TIMEOUT_MS);
57
+ this.notify('initialized', {});
58
+ }
59
+ async openDocument(file, languageId) {
60
+ const resolved = path.resolve(file);
61
+ if (this.documents.has(resolved)) {
62
+ return;
63
+ }
64
+ const text = await fs.promises.readFile(resolved, 'utf8');
65
+ const uri = pathToFileURL(resolved).href;
66
+ this.documents.add(resolved);
67
+ this.notify('textDocument/didOpen', {
68
+ textDocument: { uri, languageId, version: 1, text },
69
+ });
70
+ }
71
+ async definition(file, languageId, line, column) {
72
+ await this.openDocument(file, languageId);
73
+ const result = await this.request('textDocument/definition', {
74
+ textDocument: { uri: pathToFileURL(path.resolve(file)).href },
75
+ position: { line: Math.max(0, line - 1), character: Math.max(0, column - 1) },
76
+ });
77
+ return normalizeLocations(result);
78
+ }
79
+ async references(file, languageId, line, column) {
80
+ await this.openDocument(file, languageId);
81
+ const result = await this.request('textDocument/references', {
82
+ textDocument: { uri: pathToFileURL(path.resolve(file)).href },
83
+ position: { line: Math.max(0, line - 1), character: Math.max(0, column - 1) },
84
+ context: { includeDeclaration: true },
85
+ });
86
+ return normalizeLocations(result);
87
+ }
88
+ async hover(file, languageId, line, column) {
89
+ await this.openDocument(file, languageId);
90
+ const result = await this.request('textDocument/hover', {
91
+ textDocument: { uri: pathToFileURL(path.resolve(file)).href },
92
+ position: { line: Math.max(0, line - 1), character: Math.max(0, column - 1) },
93
+ });
94
+ if (result === null || typeof result !== 'object') {
95
+ return '';
96
+ }
97
+ return renderHover(result.contents);
98
+ }
99
+ async symbols(file, languageId) {
100
+ await this.openDocument(file, languageId);
101
+ const result = await this.request('textDocument/documentSymbol', {
102
+ textDocument: { uri: pathToFileURL(path.resolve(file)).href },
103
+ });
104
+ return flattenSymbols(result);
105
+ }
106
+ async close() {
107
+ if (this.closed) {
108
+ return;
109
+ }
110
+ try {
111
+ await this.request('shutdown', null, 2_000);
112
+ }
113
+ catch {
114
+ // The server may already be gone.
115
+ }
116
+ this.closed = true;
117
+ try {
118
+ this.notify('exit', null);
119
+ }
120
+ catch { /* ignore */ }
121
+ const child = this.child;
122
+ this.child = null;
123
+ if (child) {
124
+ try {
125
+ child.kill();
126
+ }
127
+ catch { /* already gone */ }
128
+ }
129
+ this.failAll('LSP 已关闭');
130
+ }
131
+ request(method, params, timeoutMs = REQUEST_TIMEOUT_MS) {
132
+ if (!this.child || this.closed) {
133
+ return Promise.reject(new Error('LSP 未连接'));
134
+ }
135
+ const id = this.nextId + 1;
136
+ this.nextId = id;
137
+ return new Promise((resolve, reject) => {
138
+ const timer = setTimeout(() => {
139
+ this.pending.delete(id);
140
+ reject(new Error('LSP 请求超时:' + method));
141
+ }, timeoutMs);
142
+ this.pending.set(id, { resolve, reject, timer });
143
+ this.write({ jsonrpc: '2.0', id, method, params });
144
+ });
145
+ }
146
+ notify(method, params) {
147
+ this.write({ jsonrpc: '2.0', method, params });
148
+ }
149
+ write(message) {
150
+ const child = this.child;
151
+ if (!child || this.closed || !child.stdin) {
152
+ return;
153
+ }
154
+ const body = JSON.stringify(message);
155
+ child.stdin.write('Content-Length: ' + Buffer.byteLength(body, 'utf8') + '\r\n\r\n' + body);
156
+ }
157
+ onData(chunk) {
158
+ this.buffer = Buffer.concat([this.buffer, chunk]);
159
+ for (;;) {
160
+ const headerEnd = this.buffer.indexOf('\r\n\r\n');
161
+ if (headerEnd < 0) {
162
+ return;
163
+ }
164
+ const header = this.buffer.subarray(0, headerEnd).toString('utf8');
165
+ const match = /Content-Length:\s*(\d+)/i.exec(header);
166
+ if (!match) {
167
+ this.buffer = this.buffer.subarray(headerEnd + 4);
168
+ continue;
169
+ }
170
+ const length = Number(match[1]);
171
+ const start = headerEnd + 4;
172
+ if (this.buffer.length < start + length) {
173
+ return;
174
+ }
175
+ const body = this.buffer.subarray(start, start + length).toString('utf8');
176
+ this.buffer = this.buffer.subarray(start + length);
177
+ this.onMessage(body);
178
+ }
179
+ }
180
+ onMessage(body) {
181
+ let message;
182
+ try {
183
+ message = JSON.parse(body);
184
+ }
185
+ catch {
186
+ return;
187
+ }
188
+ if (typeof message.id === 'number' && this.pending.has(message.id)) {
189
+ const entry = this.pending.get(message.id);
190
+ if (!entry) {
191
+ return;
192
+ }
193
+ this.pending.delete(message.id);
194
+ clearTimeout(entry.timer);
195
+ if (message.error) {
196
+ entry.reject(new Error('LSP 错误:' + message.error.message));
197
+ }
198
+ else {
199
+ entry.resolve(message.result);
200
+ }
201
+ return;
202
+ }
203
+ // Server-to-client request: answer so it does not hang.
204
+ if (message.method !== undefined && message.id !== undefined) {
205
+ this.write({
206
+ jsonrpc: '2.0',
207
+ id: message.id,
208
+ error: { code: -32601, message: 'method not found' },
209
+ });
210
+ }
211
+ }
212
+ failAll(message) {
213
+ for (const [id, entry] of this.pending) {
214
+ clearTimeout(entry.timer);
215
+ entry.reject(new Error(message));
216
+ this.pending.delete(id);
217
+ }
218
+ }
219
+ }
220
+ function normalizeLocations(value) {
221
+ const items = Array.isArray(value) ? value : value ? [value] : [];
222
+ const locations = [];
223
+ for (const item of items) {
224
+ if (item === null || typeof item !== 'object') {
225
+ continue;
226
+ }
227
+ const record = item;
228
+ const uri = typeof record.uri === 'string'
229
+ ? record.uri
230
+ : typeof record.targetUri === 'string' ? record.targetUri : '';
231
+ const range = record.range ?? record.targetSelectionRange ?? record.targetRange;
232
+ if (uri.length === 0 || range === null || typeof range !== 'object') {
233
+ continue;
234
+ }
235
+ const start = range.start;
236
+ const end = range.end;
237
+ const startRecord = start !== null && typeof start === 'object'
238
+ ? start : {};
239
+ const endRecord = end !== null && typeof end === 'object'
240
+ ? end : {};
241
+ locations.push({
242
+ path: uriToPath(uri),
243
+ line: numberOr(startRecord.line, 0) + 1,
244
+ column: numberOr(startRecord.character, 0) + 1,
245
+ endLine: numberOr(endRecord.line, 0) + 1,
246
+ endColumn: numberOr(endRecord.character, 0) + 1,
247
+ });
248
+ }
249
+ return locations;
250
+ }
251
+ function flattenSymbols(value) {
252
+ if (!Array.isArray(value)) {
253
+ return [];
254
+ }
255
+ const symbols = [];
256
+ const walk = (items) => {
257
+ for (const item of items) {
258
+ if (item === null || typeof item !== 'object') {
259
+ continue;
260
+ }
261
+ const record = item;
262
+ if (typeof record.name === 'string') {
263
+ const range = record.selectionRange ?? record.range ?? record.location;
264
+ let line = 1;
265
+ let column = 1;
266
+ if (range !== null && typeof range === 'object') {
267
+ const rangeRecord = range;
268
+ const start = rangeRecord.start;
269
+ const location = rangeRecord.range;
270
+ if (start !== null && typeof start === 'object') {
271
+ const startRecord = start;
272
+ line = numberOr(startRecord.line, 0) + 1;
273
+ column = numberOr(startRecord.character, 0) + 1;
274
+ }
275
+ else if (location !== null && typeof location === 'object') {
276
+ const locationRecord = location;
277
+ const locationStart = locationRecord.start;
278
+ if (locationStart !== null && typeof locationStart === 'object') {
279
+ const startRecord = locationStart;
280
+ line = numberOr(startRecord.line, 0) + 1;
281
+ column = numberOr(startRecord.character, 0) + 1;
282
+ }
283
+ }
284
+ }
285
+ symbols.push({
286
+ name: record.name,
287
+ kind: numberOr(record.kind, 0),
288
+ line,
289
+ column,
290
+ });
291
+ }
292
+ if (Array.isArray(record.children)) {
293
+ walk(record.children);
294
+ }
295
+ }
296
+ };
297
+ walk(value);
298
+ return symbols;
299
+ }
300
+ function renderHover(contents) {
301
+ if (typeof contents === 'string') {
302
+ return contents.trim();
303
+ }
304
+ if (Array.isArray(contents)) {
305
+ return contents.map((item) => renderHover(item)).filter((item) => item.length > 0).join('\n');
306
+ }
307
+ if (contents !== null && typeof contents === 'object') {
308
+ const record = contents;
309
+ if (typeof record.value === 'string') {
310
+ return record.value.trim();
311
+ }
312
+ }
313
+ return '';
314
+ }
315
+ function uriToPath(uri) {
316
+ if (!uri.startsWith('file://')) {
317
+ return uri;
318
+ }
319
+ try {
320
+ return fileURLToPath(uri);
321
+ }
322
+ catch {
323
+ return uri;
324
+ }
325
+ }
326
+ function numberOr(value, fallback) {
327
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
328
+ }
329
+ function chooseSpawn(command) {
330
+ if (process.platform !== 'win32') {
331
+ return { command, shell: false };
332
+ }
333
+ if (/\.(cmd|bat)$/i.test(command)) {
334
+ return { command, shell: true };
335
+ }
336
+ if (!command.includes('/') && !command.includes('\\')) {
337
+ return { command, shell: true };
338
+ }
339
+ return { command, shell: false };
340
+ }
@@ -0,0 +1,177 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { LspClient } from "./client.js";
5
+ const clients = new Map();
6
+ const DEFAULT_SERVERS = {
7
+ typescript: { command: 'typescript-language-server', args: ['--stdio'] },
8
+ javascript: { command: 'typescript-language-server', args: ['--stdio'] },
9
+ python: { command: 'pyright-langserver', args: ['--stdio'] },
10
+ rust: { command: 'rust-analyzer', args: [] },
11
+ go: { command: 'gopls', args: [] },
12
+ };
13
+ const INSTALL_HINTS = {
14
+ typescript: 'npm install -g typescript-language-server typescript',
15
+ javascript: 'npm install -g typescript-language-server typescript',
16
+ python: 'npm install -g pyright(或 pip install pyright)',
17
+ rust: 'rustup component add rust-analyzer',
18
+ go: 'go install golang.org/x/tools/gopls@latest',
19
+ };
20
+ /**
21
+ * Code navigation through a language server. Read-only and safe for plan mode
22
+ * and workers. Servers are started lazily and reused per workspace/language.
23
+ */
24
+ export const lspTool = {
25
+ readOnly: true,
26
+ spec: {
27
+ name: 'lsp',
28
+ description: '用语言服务器查定义、引用、悬停信息和文件符号。需要可用的 language server;可在 lsp.servers 配置命令。',
29
+ parameters: {
30
+ type: 'object',
31
+ properties: {
32
+ action: {
33
+ type: 'string',
34
+ enum: ['definition', 'references', 'hover', 'symbols'],
35
+ },
36
+ path: { type: 'string', description: '文件路径,相对于工作目录' },
37
+ line: { type: 'number', description: '1-based 行号' },
38
+ column: { type: 'number', description: '1-based 列号' },
39
+ },
40
+ required: ['action', 'path'],
41
+ additionalProperties: false,
42
+ },
43
+ },
44
+ async run(args, ctx) {
45
+ const action = typeof args.action === 'string' ? args.action : '';
46
+ const rawPath = typeof args.path === 'string' ? args.path.trim() : '';
47
+ if (rawPath.length === 0) {
48
+ throw new Error('缺少 path 参数');
49
+ }
50
+ const file = path.resolve(ctx.cwd, rawPath);
51
+ if (!fs.existsSync(file)) {
52
+ throw new Error('文件不存在:' + rawPath);
53
+ }
54
+ const languageId = languageIdFor(file);
55
+ if (!languageId) {
56
+ throw new Error('不支持的语言:' + path.extname(file)
57
+ + '(可在 ~/.minko/config.json 的 lsp.servers 配置)');
58
+ }
59
+ const server = ctx.config?.lsp?.servers?.[languageId] ?? DEFAULT_SERVERS[languageId];
60
+ if (!server) {
61
+ throw new Error('没有为 ' + languageId + ' 配置语言服务器(lsp.servers)');
62
+ }
63
+ const client = await lspClient(server, languageId, ctx.cwd);
64
+ const line = positiveInt(args.line, 0);
65
+ const column = positiveInt(args.column, 0);
66
+ if (action === 'symbols') {
67
+ const symbols = await client.symbols(file, languageId);
68
+ return formatSymbols(symbols, displayPath(ctx.cwd, file));
69
+ }
70
+ if (action !== 'definition' && action !== 'references' && action !== 'hover') {
71
+ throw new Error('不支持的操作:' + action);
72
+ }
73
+ if (line === 0) {
74
+ throw new Error('这个操作需要 line 和 column(从 1 开始)');
75
+ }
76
+ if (action === 'definition') {
77
+ const locations = await client.definition(file, languageId, line, column || 1);
78
+ return locations.length === 0
79
+ ? '没有找到定义。'
80
+ : '定义位置:\n' + formatLocations(locations, ctx.cwd);
81
+ }
82
+ if (action === 'references') {
83
+ const locations = await client.references(file, languageId, line, column || 1);
84
+ return locations.length === 0
85
+ ? '没有找到引用。'
86
+ : '引用(' + locations.length + '):\n' + formatLocations(locations, ctx.cwd);
87
+ }
88
+ const hover = await client.hover(file, languageId, line, column || 1);
89
+ return hover.length === 0 ? '没有悬停信息。' : '悬停信息:\n' + hover;
90
+ },
91
+ };
92
+ /** Closes every language server started in this process. */
93
+ export async function closeLspClients() {
94
+ const all = [...clients.values()];
95
+ clients.clear();
96
+ await Promise.all(all.map((client) => client.close().catch(() => { })));
97
+ }
98
+ async function lspClient(server, languageId, cwd) {
99
+ const key = path.resolve(cwd) + '|' + languageId + '|' + server.command + '|'
100
+ + (server.args ?? []).join(' ');
101
+ const existing = clients.get(key);
102
+ if (existing) {
103
+ return existing;
104
+ }
105
+ // Spawning through cmd.exe on Windows makes a missing server look like a
106
+ // crashed one ("不是内部或外部命令"). Check first and give an install hint.
107
+ if (!commandExists(server.command)) {
108
+ throw new Error('没有找到语言服务器 ' + server.command + '。安装:'
109
+ + (INSTALL_HINTS[languageId] ?? '请安装 ' + languageId + ' 的 language server')
110
+ + ';或在 ~/.minko/config.json 的 lsp.servers 里指定 command。');
111
+ }
112
+ const client = new LspClient({ ...server, cwd });
113
+ try {
114
+ await client.start();
115
+ }
116
+ catch (error) {
117
+ await client.close().catch(() => { });
118
+ const reason = error instanceof Error ? error.message : String(error);
119
+ throw new Error('启动 LSP 失败(' + server.command + '):' + reason
120
+ + '。可在 ~/.minko/config.json 的 lsp.servers 配置命令。');
121
+ }
122
+ clients.set(key, client);
123
+ return client;
124
+ }
125
+ function commandExists(command) {
126
+ if (/[\\/]/.test(command)) {
127
+ return fs.existsSync(command);
128
+ }
129
+ const probe = process.platform === 'win32' ? 'where.exe' : 'which';
130
+ const result = spawnSync(probe, [command], { stdio: 'ignore', windowsHide: true });
131
+ return result.status === 0;
132
+ }
133
+ function languageIdFor(file) {
134
+ switch (path.extname(file).toLowerCase()) {
135
+ case '.ts':
136
+ case '.tsx':
137
+ case '.mts':
138
+ case '.cts':
139
+ return 'typescript';
140
+ case '.js':
141
+ case '.jsx':
142
+ case '.mjs':
143
+ case '.cjs':
144
+ return 'javascript';
145
+ case '.py':
146
+ case '.pyi':
147
+ return 'python';
148
+ case '.rs':
149
+ return 'rust';
150
+ case '.go':
151
+ return 'go';
152
+ default:
153
+ return null;
154
+ }
155
+ }
156
+ function formatLocations(locations, cwd) {
157
+ return locations.map((location) => '- ' + displayPath(cwd, location.path)
158
+ + ':' + location.line + ':' + location.column).join('\n');
159
+ }
160
+ function formatSymbols(symbols, file) {
161
+ if (symbols.length === 0) {
162
+ return '没有找到符号。';
163
+ }
164
+ return '符号(' + symbols.length + '):\n'
165
+ + symbols.map((symbol) => '- ' + symbol.name + ' (kind ' + symbol.kind + ') @ '
166
+ + file + ':' + symbol.line + ':' + symbol.column).join('\n');
167
+ }
168
+ function displayPath(cwd, file) {
169
+ const relative = path.relative(path.resolve(cwd), file);
170
+ const shown = relative.length > 0 && !relative.startsWith('..') ? relative : file;
171
+ return shown.split(path.sep).join('/');
172
+ }
173
+ function positiveInt(value, fallback) {
174
+ return typeof value === 'number' && Number.isFinite(value) && value > 0
175
+ ? Math.floor(value)
176
+ : fallback;
177
+ }