@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,287 @@
1
+ import { charWidth, stripAnsi, truncateVisible, visibleWidth, wrapText } from "./ansi.js";
2
+ import { StreamRenderer } from "./md.js";
3
+ import { PURPLE } from "../theme.js";
4
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+ const TOOL_OUTPUT_TAIL = 2;
6
+ const TOOL_OUTPUT_ERROR_TAIL = 6;
7
+ const TOOL_OUTPUT_EXPANDED = 20;
8
+ const REASONING_PREVIEW_LINES = 2;
9
+ const REASONING_EXPANDED_LINES = 30;
10
+ export function renderEntry(entry, width, painter, spinnerFrame = 0, expandDetails = false) {
11
+ if (entry.kind === 'tool') {
12
+ return renderTool(entry, width, painter, spinnerFrame, expandDetails);
13
+ }
14
+ if (entry.kind === 'reasoning') {
15
+ return renderReasoning(entry, width, painter, spinnerFrame, expandDetails);
16
+ }
17
+ if (entry.kind === 'permission') {
18
+ return renderPermission(entry, width, painter);
19
+ }
20
+ const rail = painter.fg(PURPLE.rail, '│ ');
21
+ if (entry.kind === 'banner') {
22
+ return entry.text.split('\n');
23
+ }
24
+ if (entry.kind === 'stats') {
25
+ return [' ' + painter.dimmed(truncateVisible(entry.text, Math.max(10, width - 2)))];
26
+ }
27
+ if (entry.kind === 'user') {
28
+ const inner = Math.max(10, width - 4);
29
+ const lines = wrapText(entry.text, inner);
30
+ const out = [rail + painter.primary('› ') + painter.bold(lines[0] ?? '')];
31
+ for (let index = 1; index < lines.length; index += 1) {
32
+ out.push(rail + ' ' + painter.bold(lines[index]));
33
+ }
34
+ return out;
35
+ }
36
+ return renderAssistant(entry.text, Math.max(10, width - 4), painter)
37
+ .map((line) => rail + line);
38
+ }
39
+ /** Permission prompt shown inside the conversation, Claude-Code style. */
40
+ function renderPermission(entry, width, painter) {
41
+ if (entry.state !== 'pending') {
42
+ const mark = entry.state === 'allowed' ? '✓ 已允许' : '✗ 已拒绝';
43
+ return [' ' + painter.dimmed(mark + ' · ' + entry.title)];
44
+ }
45
+ const boxWidth = Math.max(34, Math.min(width - 2, 68));
46
+ const inner = boxWidth - 4;
47
+ const border = (text) => painter.fg(PURPLE.rail, text);
48
+ const row = (content) => {
49
+ const padding = ' '.repeat(Math.max(0, inner - visibleWidth(content)));
50
+ return border('│') + ' ' + content + padding + ' ' + border('│');
51
+ };
52
+ const lines = [border('╭' + '─'.repeat(boxWidth - 2) + '╮')];
53
+ lines.push(row(painter.bold(painter.soft(entry.title))));
54
+ lines.push(row(''));
55
+ for (const line of entry.detail.split('\n').slice(0, 4)) {
56
+ lines.push(row(painter.dim(' ' + truncateVisible(line, inner - 2))));
57
+ }
58
+ lines.push(row(''));
59
+ lines.push(row(painter.muted('是否继续?')));
60
+ const options = ['允许', '允许,本次会话不再询问', '拒绝(Esc)'];
61
+ options.forEach((label, index) => {
62
+ const active = index === entry.choice;
63
+ const pointer = active ? painter.primary('❯ ') : ' ';
64
+ const text = index + 1 + '. ' + label;
65
+ lines.push(row(pointer + (active ? painter.primary(text) : painter.dim(text))));
66
+ });
67
+ lines.push(border('╰' + '─'.repeat(boxWidth - 2) + '╯'));
68
+ return lines;
69
+ }
70
+ /** A tool call as one compact status line plus a short output tail.
71
+ * Visual grammar modeled on Gemini CLI's DenseToolMessage / ToolShared
72
+ * (Apache-2.0): status icon, name, one-line metadata, collapsed output. */
73
+ function renderTool(entry, width, painter, spinnerFrame, expandDetails) {
74
+ const room = Math.max(10, width - 24);
75
+ const args = entry.args.length > room ? entry.args.slice(0, room) + '…' : entry.args;
76
+ const icon = entry.status === 'running'
77
+ ? painter.primary(SPINNER[spinnerFrame % SPINNER.length])
78
+ : entry.status === 'ok' ? painter.ok('●') : painter.error('✕');
79
+ const head = ' ' + icon + ' ' + painter.soft(entry.name)
80
+ + (args.length > 0 ? ' ' + painter.dim(args) : '');
81
+ const elapsed = entry.status === 'running'
82
+ ? Math.max(0, Math.round((Date.now() - entry.started) / 1000)) + 's'
83
+ : formatMs(entry.ms);
84
+ const bytes = outputBytes(entry.output);
85
+ const lines = [head + painter.dimmed(' · ' + elapsed + (bytes > 0 ? ' · ' + formatBytes(bytes) : ''))];
86
+ appendOutputTail(lines, entry, width, painter, outputLineLimit(entry, expandDetails), expandDetails);
87
+ return lines;
88
+ }
89
+ /** Collapses consecutive calls of the same finished tool into one line. */
90
+ function renderToolGroup(entries, width, painter) {
91
+ const failed = entries.some((entry) => entry.status === 'error');
92
+ const ms = entries.reduce((sum, entry) => sum + entry.ms, 0);
93
+ const bytes = entries.reduce((sum, entry) => sum + outputBytes(entry.output), 0);
94
+ const head = ' ' + (failed ? painter.error('✕') : painter.ok('●')) + ' '
95
+ + painter.soft(entries[0].name) + painter.muted(' ×' + entries.length)
96
+ + painter.dimmed(' · ' + formatMs(ms) + (bytes > 0 ? ' · ' + formatBytes(bytes) : ''));
97
+ const lines = [head];
98
+ const source = failed
99
+ ? [...entries].reverse().find((entry) => entry.status === 'error') ?? entries[entries.length - 1]
100
+ : entries[entries.length - 1];
101
+ appendOutputTail(lines, source, width, painter, TOOL_OUTPUT_TAIL, false);
102
+ return lines;
103
+ }
104
+ function outputLineLimit(entry, expandDetails) {
105
+ if (expandDetails) {
106
+ return TOOL_OUTPUT_EXPANDED;
107
+ }
108
+ return entry.status === 'error' ? TOOL_OUTPUT_ERROR_TAIL : TOOL_OUTPUT_TAIL;
109
+ }
110
+ function appendOutputTail(lines, entry, width, painter, limit, expanded) {
111
+ if (entry.output.length === 0) {
112
+ return;
113
+ }
114
+ const output = entry.output.replace(/\r\n/g, '\n').split('\n').filter((line) => line.length > 0);
115
+ const shown = output.slice(-limit);
116
+ const hidden = output.length - shown.length;
117
+ for (const line of shown) {
118
+ lines.push(' ' + painter.fg(PURPLE.rail, '│ ')
119
+ + painter.dim(truncateVisible(line, Math.max(10, width - 8))));
120
+ }
121
+ if (hidden > 0) {
122
+ lines.push(' ' + painter.fg(PURPLE.rail, '│ ')
123
+ + painter.dimmed('… 还有 ' + hidden + ' 行' + (expanded ? '' : ' · Ctrl+O 展开')));
124
+ }
125
+ }
126
+ /** Thinking is streamed dim/italic, then folded into a one-line duration.
127
+ * Layout modeled on Gemini CLI's ThinkingMessage (Apache-2.0): left border
128
+ * rail, first lines visible while streaming, collapsed once finished. */
129
+ function renderReasoning(entry, width, painter, spinnerFrame, expandDetails) {
130
+ const elapsed = entry.done ? entry.ms : Math.max(0, Date.now() - entry.started);
131
+ const icon = entry.done ? painter.primary('✻') : painter.primary(SPINNER[spinnerFrame % SPINNER.length]);
132
+ const label = entry.done ? '思考了 ' : '思考中';
133
+ const hint = entry.done
134
+ ? (expandDetails ? ' · Ctrl+O 收起' : ' · Ctrl+O 展开')
135
+ : '';
136
+ const lines = [' ' + icon + ' ' + painter.muted(label + formatMs(elapsed))
137
+ + (entry.text.trim().length > 0 ? painter.dimmed(hint) : '')];
138
+ if (entry.done && !expandDetails) {
139
+ return lines;
140
+ }
141
+ const text = entry.text.replace(/\r\n/g, '\n').split('\n').filter((line) => line.trim().length > 0);
142
+ const limit = expandDetails ? REASONING_EXPANDED_LINES : REASONING_PREVIEW_LINES;
143
+ for (const line of text.slice(-limit)) {
144
+ lines.push(' ' + painter.fg(PURPLE.rail, '│ ')
145
+ + painter.italic(painter.dim(truncateVisible(line, Math.max(10, width - 6)))));
146
+ }
147
+ return lines;
148
+ }
149
+ function outputBytes(output) {
150
+ return Buffer.byteLength(output, 'utf8');
151
+ }
152
+ function formatBytes(bytes) {
153
+ if (bytes < 1024) {
154
+ return bytes + ' B';
155
+ }
156
+ if (bytes < 1024 * 1024) {
157
+ return (bytes / 1024).toFixed(1) + ' KB';
158
+ }
159
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
160
+ }
161
+ function formatMs(ms) {
162
+ return ms < 1000 ? ms + 'ms' : (ms / 1000).toFixed(1) + 's';
163
+ }
164
+ /** Renders assistant markdown (code bands included) into display lines. */ export function renderAssistant(text, width, painter) {
165
+ const lines = [];
166
+ let current = '';
167
+ const sink = (chunk) => {
168
+ for (const char of chunk) {
169
+ if (char === '\n') {
170
+ lines.push(current);
171
+ current = '';
172
+ }
173
+ else {
174
+ current += char;
175
+ }
176
+ }
177
+ };
178
+ const renderer = new StreamRenderer(painter, sink, { width, indent: '' });
179
+ renderer.push(text);
180
+ renderer.finish();
181
+ if (current.length > 0) {
182
+ lines.push(current);
183
+ }
184
+ return lines.map((line) => ' ' + line);
185
+ }
186
+ export function renderEntries(entries, width, painter, spinnerFrame = 0, expandDetails = false) {
187
+ const out = [];
188
+ for (let index = 0; index < entries.length; index += 1) {
189
+ const entry = entries[index];
190
+ if (!expandDetails && entry.kind === 'tool' && entry.status !== 'running') {
191
+ let end = index + 1;
192
+ while (end < entries.length) {
193
+ const next = entries[end];
194
+ if (next.kind !== 'tool' || next.name !== entry.name || next.status === 'running') {
195
+ break;
196
+ }
197
+ end += 1;
198
+ }
199
+ if (end - index >= 2) {
200
+ out.push(...renderToolGroup(entries.slice(index, end), width, painter));
201
+ index = end - 1;
202
+ continue;
203
+ }
204
+ }
205
+ out.push(...renderEntry(entry, width, painter, spinnerFrame, expandDetails));
206
+ }
207
+ return out;
208
+ }
209
+ /** Scroll position measured in lines from the bottom of the transcript. */
210
+ export class Viewport {
211
+ scroll = 0;
212
+ get atBottom() {
213
+ return this.scroll === 0;
214
+ }
215
+ clamp(total, height) {
216
+ const max = Math.max(0, total - height);
217
+ this.scroll = Math.max(0, Math.min(max, this.scroll));
218
+ }
219
+ scrollBy(delta, total, height) {
220
+ const max = Math.max(0, total - height);
221
+ this.scroll = Math.max(0, Math.min(max, this.scroll + delta));
222
+ }
223
+ visible(all, height) {
224
+ this.clamp(all.length, height);
225
+ const start = Math.max(0, all.length - height - this.scroll);
226
+ return all.slice(start, start + height);
227
+ }
228
+ }
229
+ export function lineWidth(line) {
230
+ return visibleWidth(line);
231
+ }
232
+ /** Plain text of the lines between anchor and cursor, inclusive. */
233
+ export function selectionText(lines, anchor, cursor) {
234
+ const start = Math.max(0, Math.min(anchor, cursor));
235
+ const end = Math.min(lines.length - 1, Math.max(anchor, cursor));
236
+ const out = [];
237
+ for (let index = start; index <= end; index += 1) {
238
+ out.push(stripAnsi(lines[index]).replace(/\s+$/, ''));
239
+ }
240
+ return out.join('\n');
241
+ }
242
+ function columnToIndex(text, column) {
243
+ let width = 0;
244
+ let index = 0;
245
+ for (const char of text) {
246
+ if (width >= column) {
247
+ return index;
248
+ }
249
+ width += charWidth(char);
250
+ index += char.length;
251
+ }
252
+ return text.length;
253
+ }
254
+ /** Plain text between two display columns of one line. */
255
+ export function sliceColumns(text, from, to) {
256
+ const plain = stripAnsi(text);
257
+ if (to <= from) {
258
+ return '';
259
+ }
260
+ const start = columnToIndex(plain, from);
261
+ const end = to >= Number.MAX_SAFE_INTEGER ? plain.length : columnToIndex(plain, to);
262
+ return plain.slice(start, end);
263
+ }
264
+ /** Text selected by dragging between two screen cells. */
265
+ export function selectionByCells(lines, a, b) {
266
+ const startLine = Math.max(0, Math.min(a.line, b.line));
267
+ const endLine = Math.min(lines.length - 1, Math.max(a.line, b.line));
268
+ const first = a.line <= b.line ? a : b;
269
+ const last = a.line <= b.line ? b : a;
270
+ const out = [];
271
+ for (let line = startLine; line <= endLine; line += 1) {
272
+ const from = line === first.line ? first.col : 0;
273
+ const to = line === last.line ? last.col : Number.MAX_SAFE_INTEGER;
274
+ out.push(sliceColumns(lines[line] ?? '', from, to).replace(/\s+$/, ''));
275
+ }
276
+ return out.join('\n');
277
+ }
278
+ /** Rebuilds a line with the given column range shown in reverse video. */
279
+ export function highlightColumns(line, from, to) {
280
+ const plain = stripAnsi(line);
281
+ const start = columnToIndex(plain, from);
282
+ const end = to >= Number.MAX_SAFE_INTEGER ? plain.length : columnToIndex(plain, to);
283
+ if (end <= start) {
284
+ return plain;
285
+ }
286
+ return plain.slice(0, start) + '\x1b[7m' + plain.slice(start, end) + '\x1b[27m' + plain.slice(end);
287
+ }
@@ -0,0 +1,53 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ /**
3
+ * Puts text on the system clipboard without touching the console code page.
4
+ *
5
+ * On Windows, clip.exe decodes piped stdin with the OEM code page, which turns
6
+ * Chinese into mojibake; PowerShell's Set-Clipboard gets the text as base64 so
7
+ * the encoding never depends on the console.
8
+ */
9
+ export function copyToClipboard(text) {
10
+ if (process.platform === 'win32' && copyWindows(text)) {
11
+ return true;
12
+ }
13
+ if (process.platform === 'darwin') {
14
+ if (run('pbcopy', [], text)) {
15
+ return true;
16
+ }
17
+ }
18
+ else if (process.platform !== 'win32') {
19
+ if (run('wl-copy', [], text)) {
20
+ return true;
21
+ }
22
+ if (run('xclip', ['-selection', 'clipboard'], text)) {
23
+ return true;
24
+ }
25
+ }
26
+ // OSC 52: the terminal itself writes the clipboard, so UTF-8 stays intact.
27
+ try {
28
+ process.stdout.write('\x1b]52;c;' + Buffer.from(text, 'utf8').toString('base64') + '\x07');
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ function copyWindows(text) {
36
+ const encoded = Buffer.from(text, 'utf8').toString('base64');
37
+ const script = 'Set-Clipboard -Value ([Text.Encoding]::UTF8.GetString('
38
+ + '[Convert]::FromBase64String(\'' + encoded + '\')))';
39
+ if (run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], '')) {
40
+ return true;
41
+ }
42
+ // Older systems without PowerShell: clip.exe still works for plain ASCII.
43
+ return run('clip', [], text);
44
+ }
45
+ function run(command, args, input) {
46
+ try {
47
+ const result = spawnSync(command, args, { input, timeout: 4000, windowsHide: true });
48
+ return result.status === 0;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
@@ -0,0 +1,50 @@
1
+ export class HttpError extends Error {
2
+ status;
3
+ body;
4
+ /** Parsed Retry-After, when the server sent one. */
5
+ retryAfterMs;
6
+ constructor(status, body, retryAfterMs) {
7
+ super('HTTP ' + status + ':' + summarize(body));
8
+ this.name = 'HttpError';
9
+ this.status = status;
10
+ this.body = body;
11
+ this.retryAfterMs = retryAfterMs;
12
+ }
13
+ }
14
+ function summarize(body) {
15
+ const compact = body.replace(/\s+/g, ' ').trim();
16
+ return compact.length > 400 ? compact.slice(0, 400) + '…' : compact;
17
+ }
18
+ /** Turns provider failures into something a person can act on. */
19
+ export function friendlyError(error) {
20
+ if (error instanceof HttpError) {
21
+ if (error.status === 401 || error.status === 403) {
22
+ return 'API Key 被拒绝(HTTP ' + error.status + ')。运行 minko config --api-key <key> 更新。';
23
+ }
24
+ if (error.status === 404) {
25
+ return '接口地址或模型名不对(HTTP 404)。检查 minko config --model 与 --base-url。';
26
+ }
27
+ if (error.status === 429) {
28
+ return '请求太频繁或额度不足(HTTP 429)。稍后重试。';
29
+ }
30
+ if (error.status === 400) {
31
+ return '服务端拒绝了这次请求(HTTP 400):' + summarize(error.body);
32
+ }
33
+ if (error.status >= 500) {
34
+ return '服务端出错(HTTP ' + error.status + '):' + summarize(error.body);
35
+ }
36
+ return error.message;
37
+ }
38
+ if (error instanceof Error) {
39
+ if (error.name === 'AbortError') {
40
+ return '已中断';
41
+ }
42
+ const cause = error.cause;
43
+ if (error.message.includes('fetch failed')) {
44
+ const detail = cause instanceof Error ? cause.message : '';
45
+ return '连不上服务地址' + (detail ? '(' + detail + ')' : '') + ',检查网络和 --base-url。';
46
+ }
47
+ return error.message;
48
+ }
49
+ return String(error);
50
+ }
@@ -0,0 +1,12 @@
1
+ import fs from 'node:fs';
2
+ /** Version string straight from package.json. */
3
+ export function readVersion() {
4
+ try {
5
+ const file = new URL('../../package.json', import.meta.url);
6
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
7
+ return pkg.version ?? '0.0.0';
8
+ }
9
+ catch {
10
+ return '0.0.0';
11
+ }
12
+ }
@@ -0,0 +1,168 @@
1
+ import { readVersion } from "../util/version.js";
2
+ const MAX_BYTES = 2 * 1024 * 1024;
3
+ const MAX_OUTPUT = 20_000;
4
+ const TIMEOUT_MS = 20_000;
5
+ /** Fetches a URL with a timeout and a hard response-size cap. */
6
+ export async function fetchWeb(url, signal) {
7
+ let parsed;
8
+ try {
9
+ parsed = new URL(url);
10
+ }
11
+ catch {
12
+ throw new Error('URL 不合法:' + url);
13
+ }
14
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
15
+ throw new Error('只支持 http/https URL');
16
+ }
17
+ if (signal.aborted) {
18
+ throw new Error('已中断');
19
+ }
20
+ const controller = new AbortController();
21
+ let timedOut = false;
22
+ const timer = setTimeout(() => {
23
+ timedOut = true;
24
+ controller.abort();
25
+ }, TIMEOUT_MS);
26
+ timer.unref?.();
27
+ const onAbort = () => controller.abort();
28
+ signal.addEventListener('abort', onAbort, { once: true });
29
+ try {
30
+ const response = await fetch(parsed.href, {
31
+ redirect: 'follow',
32
+ signal: controller.signal,
33
+ headers: {
34
+ 'user-agent': 'Minko/' + readVersion(),
35
+ accept: 'text/html,application/xhtml+xml,text/plain,application/json;q=0.9,*/*;q=0.5',
36
+ },
37
+ });
38
+ if (!response.ok) {
39
+ throw new Error('HTTP ' + response.status);
40
+ }
41
+ const contentType = response.headers.get('content-type') ?? '';
42
+ const body = await readLimited(response);
43
+ return { body, contentType, finalUrl: response.url || parsed.href };
44
+ }
45
+ catch (error) {
46
+ if (timedOut) {
47
+ throw new Error('抓取超时(20 秒)');
48
+ }
49
+ if (signal.aborted) {
50
+ throw new Error('已中断');
51
+ }
52
+ throw error;
53
+ }
54
+ finally {
55
+ clearTimeout(timer);
56
+ signal.removeEventListener('abort', onAbort);
57
+ }
58
+ }
59
+ export const webFetchTool = {
60
+ readOnly: true,
61
+ spec: {
62
+ name: 'web_fetch',
63
+ description: '抓取一个网页并转成可读文本(HTML 会去掉脚本/样式)。需要联网。',
64
+ parameters: {
65
+ type: 'object',
66
+ properties: {
67
+ url: { type: 'string', description: 'http/https 地址' },
68
+ },
69
+ required: ['url'],
70
+ additionalProperties: false,
71
+ },
72
+ },
73
+ async run(args, ctx) {
74
+ const raw = typeof args.url === 'string' ? args.url.trim() : '';
75
+ if (raw.length === 0) {
76
+ throw new Error('缺少 url 参数');
77
+ }
78
+ const page = await fetchWeb(raw, ctx.signal);
79
+ const type = page.contentType.toLowerCase();
80
+ let text;
81
+ if (type.includes('text/html') || type.includes('application/xhtml')) {
82
+ text = htmlToText(page.body);
83
+ }
84
+ else if (type.startsWith('text/') || type.includes('json')
85
+ || type.includes('xml') || type.includes('javascript') || type.length === 0) {
86
+ text = page.body;
87
+ }
88
+ else {
89
+ throw new Error('不支持的内容类型:' + (page.contentType || 'unknown'));
90
+ }
91
+ text = text.trim();
92
+ if (text.length === 0) {
93
+ return '(页面没有可读文本)';
94
+ }
95
+ if (text.length > MAX_OUTPUT) {
96
+ return text.slice(0, MAX_OUTPUT) + '\n…(内容过长,已截断)';
97
+ }
98
+ return text;
99
+ },
100
+ };
101
+ export function htmlToText(html) {
102
+ let text = html
103
+ .replace(/<!--[\s\S]*?-->/g, '')
104
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
105
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '')
106
+ .replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, '')
107
+ .replace(/<\/(p|div|section|article|header|footer|h[1-6]|tr|ul|ol|table)>/gi, '\n')
108
+ .replace(/<br\s*\/?>/gi, '\n')
109
+ .replace(/<li\b[^>]*>/gi, '- ')
110
+ .replace(/<[^>]+>/g, ' ');
111
+ text = decodeEntities(text);
112
+ return text
113
+ .replace(/[ \t]+\n/g, '\n')
114
+ .replace(/\n{3,}/g, '\n\n')
115
+ .replace(/[ \t]{2,}/g, ' ')
116
+ .trim();
117
+ }
118
+ export function decodeEntities(text) {
119
+ const named = {
120
+ amp: '&',
121
+ lt: '<',
122
+ gt: '>',
123
+ quot: '"',
124
+ apos: "'",
125
+ nbsp: ' ',
126
+ };
127
+ return text
128
+ .replace(/&#(\d+);/g, (_, value) => safeCodePoint(Number(value)))
129
+ .replace(/&#x([0-9a-f]+);/gi, (_, value) => safeCodePoint(parseInt(value, 16)))
130
+ .replace(/&([a-z]+);/gi, (match, name) => named[name.toLowerCase()] ?? match);
131
+ }
132
+ function safeCodePoint(value) {
133
+ try {
134
+ return Number.isFinite(value) ? String.fromCodePoint(value) : '';
135
+ }
136
+ catch {
137
+ return '';
138
+ }
139
+ }
140
+ async function readLimited(response) {
141
+ if (!response.body) {
142
+ return '';
143
+ }
144
+ const reader = response.body.getReader();
145
+ const chunks = [];
146
+ let total = 0;
147
+ try {
148
+ for (;;) {
149
+ const { done, value } = await reader.read();
150
+ if (done) {
151
+ break;
152
+ }
153
+ total += value.byteLength;
154
+ if (total > MAX_BYTES) {
155
+ await reader.cancel().catch(() => { });
156
+ throw new Error('页面太大(超过 2MB)');
157
+ }
158
+ chunks.push(value);
159
+ }
160
+ }
161
+ finally {
162
+ try {
163
+ reader.releaseLock();
164
+ }
165
+ catch { /* already released */ }
166
+ }
167
+ return new TextDecoder('utf-8').decode(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))));
168
+ }
@@ -0,0 +1,97 @@
1
+ import { decodeEntities, fetchWeb, htmlToText } from "./fetch.js";
2
+ const DEFAULT_SEARCH_URL = 'https://html.duckduckgo.com/html/';
3
+ export const webSearchTool = {
4
+ readOnly: true,
5
+ spec: {
6
+ name: 'web_search',
7
+ description: '用 HTML 搜索页查资料(默认 DuckDuckGo),返回标题、链接和摘要。需要联网。',
8
+ parameters: {
9
+ type: 'object',
10
+ properties: {
11
+ query: { type: 'string' },
12
+ max_results: { type: 'number', description: '1-10,默认 5' },
13
+ },
14
+ required: ['query'],
15
+ additionalProperties: false,
16
+ },
17
+ },
18
+ async run(args, ctx) {
19
+ const query = typeof args.query === 'string' ? args.query.trim() : '';
20
+ if (query.length === 0) {
21
+ throw new Error('缺少 query 参数');
22
+ }
23
+ const maxResults = typeof args.max_results === 'number' && Number.isFinite(args.max_results)
24
+ ? Math.min(10, Math.max(1, Math.floor(args.max_results)))
25
+ : 5;
26
+ const base = ctx.config?.web?.searchUrl || DEFAULT_SEARCH_URL;
27
+ let url;
28
+ try {
29
+ url = new URL(base);
30
+ }
31
+ catch {
32
+ throw new Error('web.searchUrl 不是合法 URL:' + base);
33
+ }
34
+ url.searchParams.set('q', query);
35
+ const page = await fetchWeb(url.href, ctx.signal);
36
+ const results = parseResults(page.body).slice(0, maxResults);
37
+ if (results.length === 0) {
38
+ return '没有找到搜索结果。';
39
+ }
40
+ return results.map((result, index) => (index + 1) + '. ' + result.title
41
+ + '\n' + result.url + (result.snippet ? '\n' + result.snippet : '')).join('\n\n');
42
+ },
43
+ };
44
+ function parseResults(html) {
45
+ const titles = [];
46
+ const snippets = [];
47
+ for (const match of html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi)) {
48
+ const attrs = match[1];
49
+ const inner = match[2];
50
+ const className = attribute(attrs, 'class');
51
+ if (/result__a/i.test(className)) {
52
+ const href = attribute(attrs, 'href');
53
+ const url = cleanUrl(href);
54
+ const title = stripText(inner);
55
+ if (title.length > 0 && url.length > 0) {
56
+ titles.push({ title, url });
57
+ }
58
+ }
59
+ else if (/result__snippet/i.test(className)) {
60
+ snippets.push(stripText(inner));
61
+ }
62
+ }
63
+ return titles.map((entry, index) => ({
64
+ ...entry,
65
+ snippet: snippets[index] ?? '',
66
+ }));
67
+ }
68
+ function attribute(attrs, name) {
69
+ const doubleQuoted = new RegExp('\\b' + name + '\\s*=\\s*"([^"]*)"', 'i').exec(attrs);
70
+ if (doubleQuoted) {
71
+ return decodeEntities(doubleQuoted[1]);
72
+ }
73
+ const singleQuoted = new RegExp("\\b" + name + "\\s*=\\s*'([^']*)'", 'i').exec(attrs);
74
+ return singleQuoted ? decodeEntities(singleQuoted[1]) : '';
75
+ }
76
+ function cleanUrl(href) {
77
+ if (href.length === 0) {
78
+ return '';
79
+ }
80
+ try {
81
+ const parsed = new URL(href, 'https://duckduckgo.com');
82
+ const redirect = parsed.searchParams.get('uddg');
83
+ if (redirect) {
84
+ return decodeURIComponent(redirect);
85
+ }
86
+ if (href.startsWith('//')) {
87
+ return 'https:' + href;
88
+ }
89
+ return parsed.href;
90
+ }
91
+ catch {
92
+ return href;
93
+ }
94
+ }
95
+ function stripText(html) {
96
+ return htmlToText(html).replace(/\s+/g, ' ').trim();
97
+ }