@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
package/src/tools/fs.ts DELETED
@@ -1,143 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import type { Tool, ToolContext } from './types.ts';
4
-
5
- const MAX_READ_BYTES = 1024 * 1024;
6
- const MAX_LINES = 2000;
7
-
8
- function resolvePath(cwd: string, raw: unknown): string {
9
- const value = typeof raw === 'string' ? raw.trim() : '';
10
- if (value.length === 0) { throw new Error('缺少 path 参数'); }
11
- return path.resolve(cwd, value);
12
- }
13
-
14
- function inside(parent: string, child: string): boolean {
15
- const relative = path.relative(parent, child);
16
- return relative.length === 0 || (!relative.startsWith('..') && !path.isAbsolute(relative));
17
- }
18
-
19
- export const readFileTool: Tool = {
20
- readOnly: true,
21
- spec: {
22
- name: 'read_file',
23
- description: '读取一个文本文件的内容,返回带行号的文本。可用 offset/limit 读取其中一段。',
24
- parameters: {
25
- type: 'object',
26
- properties: {
27
- path: { type: 'string', description: '文件路径,相对路径按当前工作目录解析' },
28
- offset: { type: 'number', description: '从第几行开始(从 1 开始),默认 1' },
29
- limit: { type: 'number', description: '最多读多少行,默认 2000' },
30
- },
31
- required: ['path'],
32
- additionalProperties: false,
33
- },
34
- },
35
- async run(args: Record<string, unknown>, ctx: ToolContext): Promise<string> {
36
- const target = resolvePath(ctx.cwd, args.path);
37
- const stat = fs.statSync(target);
38
- if (stat.isDirectory()) { throw new Error(target + ' 是目录,用 list_dir'); }
39
- if (stat.size > MAX_READ_BYTES) {
40
- throw new Error('文件太大(' + stat.size + ' 字节),请用 offset/limit 分段读取');
41
- }
42
- const buffer = fs.readFileSync(target);
43
- if (buffer.subarray(0, 8192).includes(0)) {
44
- throw new Error('这是二进制文件,不能按文本读取');
45
- }
46
- const all = buffer.toString('utf8').replace(/\r\n/g, '\n').split('\n');
47
- const offset = positiveInt(args.offset, 1);
48
- const limit = Math.min(positiveInt(args.limit, MAX_LINES), MAX_LINES);
49
- const slice = all.slice(offset - 1, offset - 1 + limit);
50
- const width = String(offset + slice.length - 1).length;
51
- const lines = slice.map((line, index) =>
52
- String(offset + index).padStart(width, ' ') + ' ' + line);
53
- const more = offset - 1 + limit < all.length
54
- ? '\n… 还有 ' + (all.length - (offset - 1 + limit)) + ' 行,用 offset 继续'
55
- : '';
56
- return lines.join('\n') + more;
57
- },
58
- };
59
-
60
- export const writeFileTool: Tool = {
61
- readOnly: false,
62
- spec: {
63
- name: 'write_file',
64
- description: '把内容写入文件(不存在则创建,存在则整份覆盖)。会自动创建父目录。',
65
- parameters: {
66
- type: 'object',
67
- properties: {
68
- path: { type: 'string', description: '文件路径,相对路径按当前工作目录解析' },
69
- content: { type: 'string', description: '完整的文件内容' },
70
- },
71
- required: ['path', 'content'],
72
- additionalProperties: false,
73
- },
74
- },
75
- async run(args: Record<string, unknown>, ctx: ToolContext): Promise<string> {
76
- const target = resolvePath(ctx.cwd, args.path);
77
- const content = typeof args.content === 'string' ? args.content : '';
78
- const exists = fs.existsSync(target);
79
- if (exists && fs.statSync(target).isDirectory()) {
80
- throw new Error(target + ' 是目录,不能写文件');
81
- }
82
- const insideCwd = inside(path.resolve(ctx.cwd), target);
83
- if (!insideCwd) {
84
- const allowed = await ctx.approve({
85
- tool: 'write_file',
86
- title: exists ? '覆盖工作区外的文件' : '在工作区外新建文件',
87
- detail: target,
88
- danger: 'write',
89
- });
90
- if (!allowed) { return '用户拒绝了这次写入,文件没有改动。'; }
91
- }
92
- fs.mkdirSync(path.dirname(target), { recursive: true });
93
- fs.writeFileSync(target, content, 'utf8');
94
- return '已写入 ' + target + '(' + Buffer.byteLength(content, 'utf8') + ' 字节)';
95
- },
96
- };
97
-
98
- export const listDirTool: Tool = {
99
- readOnly: true,
100
- spec: {
101
- name: 'list_dir',
102
- description: '列出目录里的文件和子目录,目录名以 / 结尾。',
103
- parameters: {
104
- type: 'object',
105
- properties: {
106
- path: { type: 'string', description: '目录路径,默认当前工作目录' },
107
- },
108
- additionalProperties: false,
109
- },
110
- },
111
- async run(args: Record<string, unknown>, ctx: ToolContext): Promise<string> {
112
- const target = args.path === undefined ? ctx.cwd : resolvePath(ctx.cwd, args.path);
113
- const entries = fs.readdirSync(target, { withFileTypes: true });
114
- if (entries.length === 0) { return target + ' 是空目录'; }
115
- entries.sort((a, b) => {
116
- const aDir = a.isDirectory() ? 0 : 1;
117
- const bDir = b.isDirectory() ? 0 : 1;
118
- return aDir - bDir || a.name.localeCompare(b.name);
119
- });
120
- const shown = entries.slice(0, 500).map((entry) => {
121
- if (entry.isDirectory()) { return entry.name + '/'; }
122
- try {
123
- const stat = fs.statSync(path.join(target, entry.name));
124
- return entry.name + ' ' + formatSize(stat.size);
125
- } catch {
126
- return entry.name;
127
- }
128
- });
129
- const more = entries.length > shown.length ? '\n… 还有 ' + (entries.length - shown.length) + ' 项' : '';
130
- return target + '\n' + shown.map((line) => ' ' + line).join('\n') + more;
131
- },
132
- };
133
-
134
- function positiveInt(value: unknown, fallback: number): number {
135
- if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) { return fallback; }
136
- return Math.floor(value);
137
- }
138
-
139
- export function formatSize(bytes: number): string {
140
- if (bytes < 1024) { return bytes + ' B'; }
141
- if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(1) + ' KB'; }
142
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
143
- }
@@ -1,19 +0,0 @@
1
- import type { Tool } from './types.ts';
2
- import type { ToolSpec } from '../providers/types.ts';
3
- import { listDirTool, readFileTool, writeFileTool } from './fs.ts';
4
- import { runShellTool } from './shell.ts';
5
-
6
- export const ALL_TOOLS: Tool[] = [
7
- readFileTool,
8
- writeFileTool,
9
- listDirTool,
10
- runShellTool,
11
- ];
12
-
13
- export function toolSpecs(tools: Tool[] = ALL_TOOLS): ToolSpec[] {
14
- return tools.map((tool) => tool.spec);
15
- }
16
-
17
- export function findTool(name: string, tools: Tool[] = ALL_TOOLS): Tool | undefined {
18
- return tools.find((tool) => tool.spec.name === name);
19
- }
@@ -1,120 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import type { ChildProcess } from 'node:child_process';
3
- import type { Tool, ToolContext } from './types.ts';
4
-
5
- const MAX_OUTPUT = 32 * 1024;
6
- const DEFAULT_TIMEOUT_MS = 120000;
7
-
8
- export const runShellTool: Tool = {
9
- readOnly: false,
10
- spec: {
11
- name: 'run_shell',
12
- description: '在用户的电脑上执行一条 shell 命令并返回输出。Windows 用 cmd.exe,macOS/Linux 用 sh。'
13
- + '命令会在当前工作目录里执行。不要执行交互式命令(会一直等下一条输入)。',
14
- parameters: {
15
- type: 'object',
16
- properties: {
17
- command: { type: 'string', description: '要执行的命令' },
18
- timeout_ms: { type: 'number', description: '超时毫秒数,默认 120000' },
19
- },
20
- required: ['command'],
21
- additionalProperties: false,
22
- },
23
- },
24
- async run(args: Record<string, unknown>, ctx: ToolContext): Promise<string> {
25
- const command = typeof args.command === 'string' ? args.command.trim() : '';
26
- if (command.length === 0) { throw new Error('缺少 command 参数'); }
27
- const timeout = typeof args.timeout_ms === 'number' && args.timeout_ms > 0
28
- ? Math.min(Math.floor(args.timeout_ms), 600000)
29
- : DEFAULT_TIMEOUT_MS;
30
-
31
- const allowed = await ctx.approve({
32
- tool: 'run_shell',
33
- title: '执行命令',
34
- detail: command,
35
- danger: 'exec',
36
- });
37
- if (!allowed) { return '用户拒绝了这条命令,没有执行。'; }
38
-
39
- return await execute(command, ctx.cwd, timeout, ctx.signal);
40
- },
41
- };
42
-
43
- export async function execute(
44
- command: string,
45
- cwd: string,
46
- timeoutMs: number,
47
- signal?: AbortSignal,
48
- ): Promise<string> {
49
- return await new Promise<string>((resolve) => {
50
- const child = spawn(command, {
51
- cwd,
52
- shell: true,
53
- windowsHide: true,
54
- env: { ...process.env, MINKO: '1' },
55
- });
56
- let stdout = '';
57
- let stderr = '';
58
- let truncated = false;
59
- let settled = false;
60
-
61
- const append = (target: 'out' | 'err', chunk: Buffer): void => {
62
- const text = chunk.toString('utf8');
63
- if (target === 'out') {
64
- if (stdout.length < MAX_OUTPUT) { stdout += text; }
65
- else { truncated = true; }
66
- } else {
67
- if (stderr.length < MAX_OUTPUT) { stderr += text; }
68
- else { truncated = true; }
69
- }
70
- };
71
-
72
- child.stdout?.on('data', (chunk: Buffer) => append('out', chunk));
73
- child.stderr?.on('data', (chunk: Buffer) => append('err', chunk));
74
-
75
- const finish = (tail: string): void => {
76
- if (settled) { return; }
77
- settled = true;
78
- clearTimeout(timer);
79
- signal?.removeEventListener('abort', onAbort);
80
- const parts: string[] = [];
81
- if (stdout.trim().length > 0) { parts.push(stdout.trimEnd()); }
82
- if (stderr.trim().length > 0) { parts.push('--- stderr ---\n' + stderr.trimEnd()); }
83
- if (truncated) { parts.push('(输出超过 ' + Math.floor(MAX_OUTPUT / 1024) + ' KB,已截断)'); }
84
- if (parts.length === 0) { parts.push('(没有输出)'); }
85
- parts.push(tail);
86
- resolve(parts.join('\n'));
87
- };
88
-
89
- const timer = setTimeout(() => {
90
- killTree(child);
91
- finish('退出码:-1(超过 ' + Math.round(timeoutMs / 1000) + ' 秒被终止)');
92
- }, timeoutMs);
93
-
94
- const onAbort = (): void => {
95
- killTree(child);
96
- finish('退出码:-1(用户中断)');
97
- };
98
- signal?.addEventListener('abort', onAbort, { once: true });
99
-
100
- child.on('error', (error) => {
101
- finish('无法启动命令:' + error.message);
102
- });
103
- child.on('close', (code) => {
104
- finish('退出码:' + (code === null ? -1 : code));
105
- });
106
- });
107
- }
108
-
109
- function killTree(child: ChildProcess): void {
110
- if (child.pid === undefined) { return; }
111
- try {
112
- if (process.platform === 'win32') {
113
- spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
114
- } else {
115
- child.kill('SIGTERM');
116
- }
117
- } catch {
118
- try { child.kill(); } catch { /* already gone */ }
119
- }
120
- }
@@ -1,26 +0,0 @@
1
- import type { ToolSpec } from '../providers/types.ts';
2
-
3
- export type ApprovalRequest = {
4
- tool: string;
5
- title: string;
6
- detail: string;
7
- danger: 'write' | 'exec' | 'network';
8
- };
9
-
10
- export type ToolContext = {
11
- cwd: string;
12
- signal: AbortSignal;
13
- /** Returns true when the user (or an earlier "always" answer) allows it. */
14
- approve: (request: ApprovalRequest) => Promise<boolean>;
15
- };
16
-
17
- export type Tool = {
18
- spec: ToolSpec;
19
- readOnly: boolean;
20
- run: (args: Record<string, unknown>, ctx: ToolContext) => Promise<string>;
21
- };
22
-
23
- export type ToolResult = {
24
- ok: boolean;
25
- output: string;
26
- };
package/src/ui/ansi.ts DELETED
@@ -1,110 +0,0 @@
1
- import { colorEnabled, PURPLE } from '../theme.ts';
2
-
3
- const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
4
-
5
- export function hexToRgb(hex: string): [number, number, number] {
6
- const raw = hex.replace('#', '');
7
- const full = raw.length === 3 ? raw.split('').map((c) => c + c).join('') : raw;
8
- const value = parseInt(full, 16);
9
- return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
10
- }
11
-
12
- /** A small painter: every method is a no-op when colour is disabled. */
13
- export class Painter {
14
- enabled: boolean;
15
-
16
- constructor(enabled: boolean = colorEnabled()) {
17
- this.enabled = enabled;
18
- }
19
-
20
- fg(hex: string, text: string): string {
21
- if (!this.enabled) { return text; }
22
- const [r, g, b] = hexToRgb(hex);
23
- return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
24
- }
25
-
26
- bg(hex: string, text: string): string {
27
- if (!this.enabled) { return text; }
28
- const [r, g, b] = hexToRgb(hex);
29
- return `\x1b[48;2;${r};${g};${b}m${text}\x1b[49m`;
30
- }
31
-
32
- bold(text: string): string {
33
- if (!this.enabled) { return text; }
34
- return `\x1b[1m${text}\x1b[22m`;
35
- }
36
-
37
- dim(text: string): string {
38
- if (!this.enabled) { return text; }
39
- return `\x1b[2m${text}\x1b[22m`;
40
- }
41
-
42
- italic(text: string): string {
43
- if (!this.enabled) { return text; }
44
- return `\x1b[3m${text}\x1b[23m`;
45
- }
46
-
47
- underline(text: string): string {
48
- if (!this.enabled) { return text; }
49
- return `\x1b[4m${text}\x1b[24m`;
50
- }
51
-
52
- primary(text: string): string { return this.fg(PURPLE.primary, text); }
53
- soft(text: string): string { return this.fg(PURPLE.soft, text); }
54
- deep(text: string): string { return this.fg(PURPLE.deep, text); }
55
- muted(text: string): string { return this.fg(PURPLE.muted, text); }
56
- dimmed(text: string): string { return this.dim(this.fg(PURPLE.dim, text)); }
57
- error(text: string): string { return this.fg(PURPLE.error, text); }
58
- ok(text: string): string { return this.fg(PURPLE.ok, text); }
59
- warn(text: string): string { return this.fg(PURPLE.warn, text); }
60
- }
61
-
62
- export function stripAnsi(text: string): string {
63
- return text.replace(ANSI_PATTERN, '');
64
- }
65
-
66
- /** Terminal columns occupied by a string (CJK and emoji count double). */
67
- export function visibleWidth(text: string): number {
68
- let width = 0;
69
- for (const char of stripAnsi(text)) {
70
- const code = char.codePointAt(0) ?? 0;
71
- width += isWide(code) ? 2 : 1;
72
- }
73
- return width;
74
- }
75
-
76
- function isWide(code: number): boolean {
77
- return (code >= 0x1100 && code <= 0x115f)
78
- || (code >= 0x2e80 && code <= 0xa4cf)
79
- || (code >= 0xac00 && code <= 0xd7a3)
80
- || (code >= 0xf900 && code <= 0xfaff)
81
- || (code >= 0xfe30 && code <= 0xfe4f)
82
- || (code >= 0xff00 && code <= 0xff60)
83
- || (code >= 0xffe0 && code <= 0xffe6)
84
- || (code >= 0x1f300 && code <= 0x1faff)
85
- || (code >= 0x20000 && code <= 0x3fffd);
86
- }
87
-
88
- export function truncateVisible(text: string, max: number): string {
89
- if (visibleWidth(text) <= max) { return text; }
90
- let out = '';
91
- let width = 0;
92
- for (const char of stripAnsi(text)) {
93
- const code = char.codePointAt(0) ?? 0;
94
- const w = isWide(code) ? 2 : 1;
95
- if (width + w > max - 1) { break; }
96
- out += char;
97
- width += w;
98
- }
99
- return out + '…';
100
- }
101
-
102
- export function terminalWidth(fallback: number = 80): number {
103
- const columns = process.stdout.columns;
104
- return typeof columns === 'number' && columns > 20 ? columns : fallback;
105
- }
106
-
107
- /** Text with an even two-column grid, used by the banner. */
108
- export function spaced(text: string, gap: string = ' '): string {
109
- return text.split('').join(gap);
110
- }
package/src/ui/banner.ts DELETED
@@ -1,39 +0,0 @@
1
- import type { Painter } from './ansi.ts';
2
- import { terminalWidth } from './ansi.ts';
3
-
4
- const LETTERS = ['#f3e8ff', '#e9d5ff', '#d8b4fe', '#c4b5fd', '#a78bfa'];
5
-
6
- export function printBanner(painter: Painter, info: {
7
- model: string;
8
- cwd: string;
9
- session: string;
10
- continued: boolean;
11
- }): void {
12
- const width = Math.min(terminalWidth(), 72);
13
- const title = 'Minko'.split('').map((letter, index) =>
14
- painter.fg(LETTERS[index % LETTERS.length], painter.bold(letter))).join('');
15
- const lines = [
16
- '',
17
- ' ' + painter.primary('▌') + ' ' + title + ' ' + painter.muted('终端里的 AI 助手'),
18
- ' ' + painter.primary('▌') + ' ' + painter.dimmed('─'.repeat(Math.max(8, width - 6))),
19
- ' ' + painter.primary('▌') + ' ' + painter.muted(info.model + ' · ' + info.cwd),
20
- ' ' + painter.primary('▌') + ' ' + painter.dimmed('会话 ' + info.session + (info.continued ? '(已继续)' : '(新)')),
21
- ' ' + painter.primary('▌') + ' ' + painter.dimmed('/help 查看命令,Ctrl+C 中断'),
22
- '',
23
- ];
24
- process.stdout.write(lines.join('\n') + '\n');
25
- }
26
-
27
- export function printDivider(painter: Painter): void {
28
- process.stdout.write(painter.dimmed(' ' + '─'.repeat(Math.min(terminalWidth() - 4, 64))) + '\n');
29
- }
30
-
31
- export function formatTokens(value: number): string {
32
- if (value < 1000) { return String(value); }
33
- return (value / 1000).toFixed(1) + 'k';
34
- }
35
-
36
- export function formatDuration(ms: number): string {
37
- if (ms < 1000) { return ms + 'ms'; }
38
- return (ms / 1000).toFixed(1) + 's';
39
- }
package/src/ui/input.ts DELETED
@@ -1,54 +0,0 @@
1
- import type { Interface } from 'node:readline/promises';
2
-
3
- /**
4
- * Reads a line with echo disabled (API keys). Falls back to a normal question
5
- * when stdin is not a TTY.
6
- */
7
- export async function askHidden(
8
- rl: Interface,
9
- question: string,
10
- ): Promise<string> {
11
- if (!process.stdin.isTTY) {
12
- const answer = await rl.question(question);
13
- return answer.trim();
14
- }
15
- rl.pause();
16
- process.stdout.write(question);
17
-
18
- const stdin = process.stdin;
19
- stdin.setRawMode(true);
20
- stdin.resume();
21
-
22
- return await new Promise<string>((resolve, reject) => {
23
- let value = '';
24
- const cleanup = (): void => {
25
- stdin.setRawMode(false);
26
- stdin.pause();
27
- stdin.removeListener('data', onData);
28
- rl.resume();
29
- };
30
- const onData = (chunk: Buffer): void => {
31
- const text = chunk.toString('utf8');
32
- for (const char of text) {
33
- if (char === '\r' || char === '\n') {
34
- cleanup();
35
- process.stdout.write('\n');
36
- resolve(value);
37
- return;
38
- }
39
- if (char === '\u0003') {
40
- cleanup();
41
- process.stdout.write('\n');
42
- reject(new Error('已取消'));
43
- return;
44
- }
45
- if (char === '\u007f' || char === '\b') {
46
- value = value.slice(0, -1);
47
- continue;
48
- }
49
- if (char >= ' ') { value += char; }
50
- }
51
- };
52
- stdin.on('data', onData);
53
- });
54
- }
package/src/ui/md.ts DELETED
@@ -1,160 +0,0 @@
1
- import type { Painter } from './ansi.ts';
2
- import { visibleWidth } from './ansi.ts';
3
-
4
- export type Sink = (text: string) => void;
5
-
6
- /**
7
- * Streams model text to the terminal.
8
- *
9
- * Formatting happens line by line: fenced code gets a violet gutter, everything
10
- * else is printed as-is while it streams (never re-rendered, so nothing flickers).
11
- */
12
- export class StreamRenderer {
13
- private painter: Painter;
14
- private sink: Sink;
15
- private pending = '';
16
- private midLine = false;
17
- private inFence = false;
18
- private fenceLang = '';
19
- private fenceWidth = 0;
20
-
21
- constructor(painter: Painter, sink: Sink) {
22
- this.painter = painter;
23
- this.sink = sink;
24
- }
25
-
26
- push(chunk: string): void {
27
- this.pending += chunk.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
28
- this.drain(false);
29
- }
30
-
31
- finish(): void {
32
- this.drain(true);
33
- if (this.midLine) {
34
- this.sink('\n');
35
- this.midLine = false;
36
- }
37
- if (this.inFence) {
38
- this.closeFence();
39
- }
40
- }
41
-
42
- private drain(final: boolean): void {
43
- for (;;) {
44
- const index = this.pending.indexOf('\n');
45
- if (index >= 0) {
46
- const line = this.pending.slice(0, index);
47
- this.pending = this.pending.slice(index + 1);
48
- this.emitLine(line, true);
49
- continue;
50
- }
51
- if (final && this.pending.length > 0) {
52
- const line = this.pending;
53
- this.pending = '';
54
- this.emitLine(line, true);
55
- continue;
56
- }
57
- // Long single line: show it rather than waiting for a newline.
58
- if (this.pending.length > 160) {
59
- this.sink(this.body(this.pending));
60
- this.pending = '';
61
- this.midLine = true;
62
- }
63
- return;
64
- }
65
- }
66
-
67
- private emitLine(line: string, newline: boolean): void {
68
- if (!this.midLine && this.fenceMarker(line)) {
69
- if (this.inFence) { this.closeFence(); } else { this.openFence(line.trim().slice(3).trim()); }
70
- this.midLine = false;
71
- return;
72
- }
73
- if (!this.midLine && this.inFence) {
74
- this.sink(this.painter.fg('#8f7fb8', ' │ ') + this.painter.dim(line) + (newline ? '\n' : ''));
75
- this.midLine = !newline;
76
- return;
77
- }
78
- this.sink(this.body(line) + (newline ? '\n' : ''));
79
- this.midLine = !newline;
80
- }
81
-
82
- private body(line: string): string {
83
- return line;
84
- }
85
-
86
- private fenceMarker(line: string): boolean {
87
- return line.trimStart().startsWith('```');
88
- }
89
-
90
- private openFence(lang: string): void {
91
- this.inFence = true;
92
- this.fenceLang = lang;
93
- const width = Math.max(24, Math.min(visibleWidth(lang) + 16, 64));
94
- this.fenceWidth = width;
95
- const label = lang.length > 0 ? this.painter.soft(lang) : this.painter.muted('代码');
96
- const head = ' ╭─ ' + label + ' ';
97
- const rest = Math.max(0, width - visibleWidth(head) + 1);
98
- this.sink(this.painter.fg('#8f7fb8', head + '─'.repeat(rest)) + '\n');
99
- }
100
-
101
- private closeFence(): void {
102
- this.inFence = false;
103
- this.sink(this.painter.fg('#8f7fb8', ' ╰' + '─'.repeat(Math.max(2, this.fenceWidth - 2))) + '\n');
104
- }
105
- }
106
-
107
- /** One-off markdown rendering for help text and tool output. */
108
- export function renderInline(painter: Painter, line: string): string {
109
- let text = line;
110
- text = text.replace(/`([^`]+)`/g, (_all, code: string) => painter.fg('#d8b4fe', code));
111
- text = text.replace(/\*\*([^*]+)\*\*/g, (_all, bold: string) => painter.bold(bold));
112
- text = text.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, (_all, pre: string, em: string) => pre + painter.italic(em));
113
- text = text.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, (_all, label: string, url: string) =>
114
- painter.primary(label) + painter.dimmed(' ' + url));
115
- return text;
116
- }
117
-
118
- export function renderMarkdown(painter: Painter, source: string): string {
119
- const out: string[] = [];
120
- let fence = false;
121
- let lang = '';
122
- for (const line of source.replace(/\r\n/g, '\n').split('\n')) {
123
- if (line.trimStart().startsWith('```')) {
124
- if (fence) {
125
- out.push(painter.fg('#8f7fb8', ' ╰' + '─'.repeat(40)));
126
- fence = false;
127
- lang = '';
128
- } else {
129
- fence = true;
130
- lang = line.trim().slice(3).trim();
131
- out.push(painter.fg('#8f7fb8', ' ╭─ ' + (lang || '代码') + ' ' + '─'.repeat(30)));
132
- }
133
- continue;
134
- }
135
- if (fence) {
136
- out.push(painter.fg('#8f7fb8', ' │ ') + line);
137
- continue;
138
- }
139
- const heading = /^(#{1,4})\s+(.*)$/.exec(line);
140
- if (heading) {
141
- out.push(painter.soft(painter.bold(heading[2])));
142
- continue;
143
- }
144
- if (/^\s*(---|\*\*\*)\s*$/.test(line)) {
145
- out.push(painter.dimmed('─'.repeat(40)));
146
- continue;
147
- }
148
- const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
149
- if (bullet) {
150
- out.push(bullet[1] + painter.primary('• ') + renderInline(painter, bullet[2]));
151
- continue;
152
- }
153
- if (line.startsWith('> ')) {
154
- out.push(painter.fg('#8f7fb8', '│ ') + painter.dim(renderInline(painter, line.slice(2))));
155
- continue;
156
- }
157
- out.push(renderInline(painter, line));
158
- }
159
- return out.join('\n');
160
- }