@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,112 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { activeProvider, loadConfig } from "../config/store.js";
3
+ import { minkoHome } from "../config/paths.js";
4
+ import { listPlugins, pluginsDir } from "../plugins/loader.js";
5
+ import { dockerAvailable, resolveSandbox } from "../sandbox/index.js";
6
+ import { Painter } from "../ui/ansi.js";
7
+ export function collectChecks(config, error, deps = {}) {
8
+ const checks = [];
9
+ const nodeVersion = deps.nodeVersion ?? process.version;
10
+ const parts = nodeVersion.replace(/^v/, '').split('.').map(Number);
11
+ const nodeOk = parts[0] > 18 || (parts[0] === 18 && parts[1] >= 17);
12
+ checks.push({
13
+ name: 'node',
14
+ ok: nodeOk,
15
+ detail: nodeVersion + '(需要 >=18.17)',
16
+ });
17
+ checks.push({
18
+ name: 'config',
19
+ ok: config !== null,
20
+ detail: error ?? (config ? '已加载' : '不可用'),
21
+ });
22
+ if (config) {
23
+ const active = activeProvider(config);
24
+ checks.push({
25
+ name: 'provider',
26
+ ok: Boolean(active && active.provider.apiKey.length > 0),
27
+ detail: active
28
+ ? active.name + ' · ' + active.provider.model
29
+ + (active.provider.apiKey.length > 0 ? ' · key ✓' : ' · key 缺失')
30
+ : '还没有配置模型',
31
+ });
32
+ const plugins = listPlugins(pluginsDir(), config.plugins.disabled);
33
+ checks.push({
34
+ name: 'plugins',
35
+ ok: plugins.errors.length === 0,
36
+ detail: plugins.plugins.length + ' 个插件'
37
+ + (plugins.errors.length > 0 ? ',' + plugins.errors.length + ' 个错误' : ''),
38
+ });
39
+ checks.push({
40
+ name: 'mcp',
41
+ ok: true,
42
+ detail: Object.keys(config.mcpServers).length + ' 个 server 配置',
43
+ });
44
+ checks.push({
45
+ name: 'lsp',
46
+ ok: true,
47
+ detail: Object.keys(config.lsp.servers ?? {}).length + ' 个自定义 server'
48
+ + '(未配置时使用内置默认)',
49
+ });
50
+ const sandbox = resolveSandbox(config);
51
+ if (sandbox) {
52
+ const docker = dockerAvailable();
53
+ checks.push({
54
+ name: 'sandbox',
55
+ ok: docker || sandbox.allowFallback,
56
+ detail: docker
57
+ ? 'Docker 可用 · ' + sandbox.image
58
+ : sandbox.allowFallback
59
+ ? 'Docker 不可用,将回退普通 shell'
60
+ : 'Docker 不可用,run_shell 会拒绝执行',
61
+ });
62
+ }
63
+ }
64
+ try {
65
+ const home = deps.home ? deps.home() : minkoHome();
66
+ checks.push({ name: 'data-dir', ok: true, detail: home });
67
+ }
68
+ catch (error) {
69
+ checks.push({
70
+ name: 'data-dir',
71
+ ok: false,
72
+ detail: error instanceof Error ? error.message : String(error),
73
+ });
74
+ }
75
+ try {
76
+ const version = deps.gitVersion
77
+ ? deps.gitVersion()
78
+ : execFileSync('git', ['--version'], {
79
+ encoding: 'utf8',
80
+ timeout: 5_000,
81
+ windowsHide: true,
82
+ }).trim();
83
+ checks.push({ name: 'git', ok: version.length > 0, detail: version || 'git 不可用' });
84
+ }
85
+ catch {
86
+ checks.push({ name: 'git', ok: false, detail: '未安装 git(lane 功能不可用)' });
87
+ }
88
+ return checks;
89
+ }
90
+ export async function runDoctorCommand(json) {
91
+ const painter = new Painter();
92
+ let config = null;
93
+ let error = null;
94
+ try {
95
+ config = loadConfig();
96
+ }
97
+ catch (caught) {
98
+ error = caught instanceof Error ? caught.message : String(caught);
99
+ }
100
+ const checks = collectChecks(config, error);
101
+ const ok = checks.every((check) => check.ok);
102
+ if (json) {
103
+ process.stdout.write(JSON.stringify({ ok, checks }, null, 2) + '\n');
104
+ return ok ? 0 : 1;
105
+ }
106
+ for (const check of checks) {
107
+ process.stdout.write(' ' + (check.ok ? painter.ok('✓ ') : painter.error('✗ '))
108
+ + painter.soft(check.name.padEnd(10)) + painter.dimmed(check.detail) + '\n');
109
+ }
110
+ process.stdout.write('\n ' + (ok ? painter.ok('全部通过') : painter.error('有项目需要处理')) + '\n');
111
+ return ok ? 0 : 1;
112
+ }
@@ -0,0 +1,85 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { SessionStore } from "../session/store.js";
4
+ import { Painter } from "../ui/ansi.js";
5
+ export function sessionToMarkdown(record) {
6
+ const lines = [
7
+ '# ' + record.meta.title,
8
+ '',
9
+ '- 会话:' + record.meta.id,
10
+ '- 目录:' + record.meta.cwd,
11
+ '- 模型:' + record.meta.model,
12
+ '- 时间:' + record.meta.createdAt + ' → ' + record.meta.updatedAt,
13
+ ];
14
+ if (record.meta.forkedFrom) {
15
+ lines.push('- 分支自:' + record.meta.forkedFrom);
16
+ }
17
+ lines.push('', '---', '');
18
+ for (const message of record.messages) {
19
+ if (message.role === 'user') {
20
+ lines.push('## 用户', '', message.content, '');
21
+ }
22
+ else if (message.role === 'assistant') {
23
+ lines.push('## 助手', '');
24
+ if (message.content.length > 0) {
25
+ lines.push(message.content, '');
26
+ }
27
+ for (const call of message.toolCalls) {
28
+ lines.push('- 工具调用:`' + call.name + '` `' + oneLine(call.arguments, 300) + '`');
29
+ }
30
+ if (message.toolCalls.length > 0) {
31
+ lines.push('');
32
+ }
33
+ }
34
+ else if (message.role === 'system') {
35
+ lines.push('## 系统', '', message.content, '');
36
+ }
37
+ else {
38
+ lines.push('## 工具 · ' + message.name, '');
39
+ const fence = fenceFor(message.content);
40
+ lines.push(fence + 'text', message.content, fence, '');
41
+ }
42
+ }
43
+ return lines.join('\n').trimEnd() + '\n';
44
+ }
45
+ export async function runExportCommand(id, format = 'md', out) {
46
+ const painter = new Painter();
47
+ const store = new SessionStore();
48
+ const sourceId = !id || id.trim() === 'latest' ? store.latest()?.id : id.trim();
49
+ if (!sourceId) {
50
+ process.stderr.write(painter.error('找不到可用的会话') + '\n');
51
+ return 1;
52
+ }
53
+ const record = store.load(sourceId);
54
+ if (!record) {
55
+ process.stderr.write(painter.error('找不到会话:' + sourceId) + '\n');
56
+ return 1;
57
+ }
58
+ const payload = format === 'json'
59
+ ? JSON.stringify(record, null, 2) + '\n'
60
+ : sessionToMarkdown(record);
61
+ if (out && out.trim().length > 0) {
62
+ const target = path.resolve(out.trim());
63
+ try {
64
+ fs.writeFileSync(target, payload, 'utf8');
65
+ }
66
+ catch (error) {
67
+ process.stderr.write(painter.error('写入失败:'
68
+ + (error instanceof Error ? error.message : String(error))) + '\n');
69
+ return 1;
70
+ }
71
+ process.stdout.write(painter.ok('✓ ') + '已导出到 ' + painter.soft(target) + '\n');
72
+ return 0;
73
+ }
74
+ process.stdout.write(payload);
75
+ return 0;
76
+ }
77
+ function fenceFor(text) {
78
+ const matches = text.match(/`{3,}/g) ?? [];
79
+ const longest = matches.reduce((max, fence) => Math.max(max, fence.length), 2);
80
+ return '`'.repeat(Math.max(3, longest + 1));
81
+ }
82
+ function oneLine(text, max) {
83
+ const compact = text.replace(/\s+/g, ' ').trim();
84
+ return compact.length > max ? compact.slice(0, max) + '…' : compact;
85
+ }
@@ -0,0 +1,24 @@
1
+ import { SessionStore } from "../session/store.js";
2
+ import { Painter } from "../ui/ansi.js";
3
+ export async function runForkCommand(id, name) {
4
+ const painter = new Painter();
5
+ if (!id || id.trim().length === 0) {
6
+ process.stderr.write(painter.error('用法:minko fork <会话id|latest> [名字]') + '\n');
7
+ return 1;
8
+ }
9
+ const store = new SessionStore();
10
+ const sourceId = id.trim() === 'latest' ? store.latest()?.id : id.trim();
11
+ if (!sourceId) {
12
+ process.stderr.write(painter.error('找不到可用的会话:' + id) + '\n');
13
+ return 1;
14
+ }
15
+ const forked = store.fork(sourceId, name);
16
+ if (!forked) {
17
+ process.stderr.write(painter.error('找不到会话:' + sourceId) + '\n');
18
+ return 1;
19
+ }
20
+ process.stdout.write(painter.ok('✓ ') + '已创建分支 ' + painter.soft(forked.meta.id)
21
+ + painter.dimmed('(来自 ' + sourceId + ')') + '\n');
22
+ process.stdout.write(' ' + painter.dimmed('继续这个分支:minko -r ' + forked.meta.id) + '\n');
23
+ return 0;
24
+ }
@@ -0,0 +1,272 @@
1
+ import QRCode from 'qrcode';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { Painter } from "../ui/ansi.js";
9
+ import { configProblems, loadConfig } from "../config/store.js";
10
+ import { encodeLink, lanAddress, loadOrCreateDevice, newToken, saveDevice, pidFilePath } from "../link/protocol.js";
11
+ import { minkoHome } from "../config/paths.js";
12
+ import { readVersion } from "../util/version.js";
13
+ import { SERVE_PORT } from "./serveCommand.js";
14
+ /** Default public relay so cross-network works without extra flags. */
15
+ const DEFAULT_RELAY = 'http://81.70.41.250:8787';
16
+ /**
17
+ * One-time device pairing: makes sure the background service is running,
18
+ * shows the QR, then exits once the phone confirms.
19
+ */
20
+ export async function runLinkCommand(_portArg, hostArg, relayArg, lanOnly) {
21
+ const painter = new Painter();
22
+ const config = loadConfig();
23
+ const problems = configProblems(config);
24
+ if (problems.length > 0) {
25
+ for (const problem of problems) {
26
+ process.stderr.write(painter.error('· ' + problem + '\n'));
27
+ }
28
+ return 1;
29
+ }
30
+ const device = loadOrCreateDevice();
31
+ const relay = lanOnly
32
+ ? ''
33
+ : ((relayArg && relayArg.trim().length > 0)
34
+ ? relayArg.trim().replace(/\/+$/, '')
35
+ : (device.relay ?? DEFAULT_RELAY));
36
+ if (relay.length > 0) {
37
+ device.relay = relay;
38
+ if (!device.room) {
39
+ device.room = crypto.randomBytes(4).toString('hex');
40
+ }
41
+ }
42
+ else {
43
+ delete device.relay;
44
+ delete device.room;
45
+ }
46
+ if (!device.token) {
47
+ device.token = newToken();
48
+ }
49
+ saveDevice(device);
50
+ let running = await isServeRunning();
51
+ const version = await serveVersion();
52
+ if (running && version !== readVersion()) {
53
+ // Older daemon from a previous install: restart it so it serves new APIs.
54
+ stopServe();
55
+ running = await waitForServeDown(4000);
56
+ }
57
+ if (!running) {
58
+ startServeDetached();
59
+ running = await waitForServe(8000);
60
+ }
61
+ if (!running) {
62
+ process.stderr.write(painter.error('后台服务没有启动,原因如下:\n'));
63
+ process.stderr.write(serveLogTail());
64
+ process.stderr.write(painter.dimmed('也可以手动运行 minko serve 查看完整输出\n'));
65
+ return 1;
66
+ }
67
+ const autostart = ensureAutostart();
68
+ const host = hostArg && hostArg.trim().length > 0 ? hostArg.trim() : lanAddress();
69
+ const payload = encodeLink({
70
+ v: 1,
71
+ host,
72
+ port: SERVE_PORT,
73
+ token: device.token,
74
+ name: device.name.slice(0, 10),
75
+ relay: device.relay,
76
+ room: device.room,
77
+ });
78
+ const qr = await QRCode.toString(payload, {
79
+ type: 'terminal',
80
+ small: true,
81
+ margin: 1,
82
+ errorCorrectionLevel: 'L',
83
+ });
84
+ const lines = [
85
+ '',
86
+ ' ' + painter.bold(painter.soft('手机配对')) + painter.dimmed(' · 仅绑定设备,后台服务已常驻'),
87
+ ' ' + painter.dimmed('─'.repeat(46)),
88
+ ' ' + painter.muted('电脑 ') + painter.primary(device.name),
89
+ ' ' + painter.muted(relay.length > 0 ? '中转 ' : '地址 ')
90
+ + painter.primary(relay.length > 0 ? relay : host + ':' + SERVE_PORT),
91
+ relay.length > 0
92
+ ? ' ' + painter.muted('房间 ') + painter.primary(device.room ?? '')
93
+ : '',
94
+ ' ' + painter.muted('常驻 ') + painter.soft('后台服务已启动' + (autostart ? ',登录时自动运行' : '')),
95
+ '',
96
+ ' ' + painter.dimmed('用手机 Minko 的「远程」扫码;令牌:') + painter.muted(device.token),
97
+ '',
98
+ ].filter((line) => line.length > 0);
99
+ process.stdout.write(lines.join('\n') + '\n');
100
+ process.stdout.write(qr + '\n');
101
+ process.stdout.write(' ' + painter.dimmed('等待手机扫码… 配对完成后自动退出,服务继续在后台运行') + '\n\n');
102
+ const before = (device.phones ?? []).length;
103
+ return await new Promise((resolve) => {
104
+ let done = false;
105
+ const finish = (code) => {
106
+ if (done) {
107
+ return;
108
+ }
109
+ done = true;
110
+ clearInterval(timer);
111
+ process.removeListener('SIGINT', onSigint);
112
+ resolve(code);
113
+ };
114
+ const onSigint = () => {
115
+ process.stdout.write('\n ' + painter.dimmed('已退出配对界面,后台服务继续运行') + '\n');
116
+ finish(0);
117
+ };
118
+ process.on('SIGINT', onSigint);
119
+ const timer = setInterval(() => {
120
+ void fetchPhones(device).then((phones) => {
121
+ if (phones.length > before) {
122
+ const latest = phones[phones.length - 1];
123
+ process.stdout.write('\n ' + painter.ok('✓ ') + '已与 ' + painter.soft(latest.name)
124
+ + ' 配对,后台服务继续运行\n\n');
125
+ finish(0);
126
+ }
127
+ });
128
+ }, 1000);
129
+ });
130
+ }
131
+ async function isServeRunning() {
132
+ try {
133
+ const response = await fetch('http://127.0.0.1:' + SERVE_PORT + '/health', {
134
+ signal: AbortSignal.timeout(1500),
135
+ });
136
+ return response.ok;
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ async function serveVersion() {
143
+ try {
144
+ const response = await fetch('http://127.0.0.1:' + SERVE_PORT + '/health', {
145
+ signal: AbortSignal.timeout(1500),
146
+ });
147
+ const data = await response.json();
148
+ return data.version ?? null;
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ function stopServe() {
155
+ try {
156
+ const pid = Number(fs.readFileSync(pidFilePath(), 'utf8').trim());
157
+ if (Number.isInteger(pid) && pid > 0) {
158
+ process.kill(pid);
159
+ }
160
+ }
161
+ catch {
162
+ // no pid file, nothing to stop
163
+ }
164
+ }
165
+ async function waitForServeDown(ms) {
166
+ const deadline = Date.now() + ms;
167
+ while (Date.now() < deadline) {
168
+ if (!(await isServeRunning())) {
169
+ return true;
170
+ }
171
+ await new Promise((resolve) => setTimeout(resolve, 300));
172
+ }
173
+ return false;
174
+ }
175
+ async function waitForServe(ms) {
176
+ const deadline = Date.now() + ms;
177
+ while (Date.now() < deadline) {
178
+ if (await isServeRunning()) {
179
+ return true;
180
+ }
181
+ await new Promise((resolve) => setTimeout(resolve, 400));
182
+ }
183
+ return false;
184
+ }
185
+ function binPath() {
186
+ return fileURLToPath(new URL('../../bin/minko.js', import.meta.url));
187
+ }
188
+ function startServeDetached() {
189
+ if (process.platform === 'win32') {
190
+ try {
191
+ // `start /min` hands the process to the shell: it is not a child of this
192
+ // window, so closing the pairing terminal cannot kill it. wscript was
193
+ // silently blocked on some machines, so this is the reliable path.
194
+ const child = spawn('cmd.exe', [
195
+ '/c', 'start', '', '/min', process.execPath, binPath(), 'serve',
196
+ ], { detached: true, stdio: 'ignore', windowsHide: true });
197
+ child.unref();
198
+ return;
199
+ }
200
+ catch {
201
+ // fall through to a plain detached spawn
202
+ }
203
+ }
204
+ try {
205
+ const child = spawn(process.execPath, [binPath(), 'serve'], {
206
+ detached: true,
207
+ stdio: 'ignore',
208
+ windowsHide: true,
209
+ cwd: process.cwd(),
210
+ });
211
+ child.unref();
212
+ }
213
+ catch {
214
+ // reported by waitForServe
215
+ }
216
+ }
217
+ function serveLogTail() {
218
+ try {
219
+ const file = path.join(minkoHome(), 'serve.log');
220
+ if (!fs.existsSync(file)) {
221
+ return ' (没有 serve.log:守护进程可能根本没启动,检查 ' + process.execPath + ' 是否存在)\n';
222
+ }
223
+ const text = fs.readFileSync(file, 'utf8').trimEnd();
224
+ const tail = text.length > 1500 ? text.slice(text.length - 1500) : text;
225
+ return tail.split('\n').map((line) => ' ' + line).join('\n') + '\n';
226
+ }
227
+ catch {
228
+ return ' (读取 serve.log 失败)\n';
229
+ }
230
+ }
231
+ async function fetchPhones(device) {
232
+ try {
233
+ const response = await fetch('http://127.0.0.1:' + SERVE_PORT + '/pair', {
234
+ headers: { authorization: 'Bearer ' + (device.token ?? '') },
235
+ signal: AbortSignal.timeout(2000),
236
+ });
237
+ const data = await response.json();
238
+ return data.phones ?? [];
239
+ }
240
+ catch {
241
+ return [];
242
+ }
243
+ }
244
+ /** Adds a login-time starter so the daemon comes back after a reboot. */
245
+ function ensureAutostart() {
246
+ if (process.platform !== 'win32') {
247
+ return false;
248
+ }
249
+ const appData = process.env.APPDATA ?? '';
250
+ if (appData.length === 0) {
251
+ return false;
252
+ }
253
+ const dir = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
254
+ if (!fs.existsSync(dir)) {
255
+ return false;
256
+ }
257
+ const file = path.join(dir, 'minko-serve.cmd');
258
+ const content = '@echo off\r\nstart "" /min "' + process.execPath + '" "'
259
+ + binPath() + '" serve\r\n';
260
+ try {
261
+ fs.writeFileSync(file, content, 'utf8');
262
+ // Remove the older VBS starter if a previous version wrote one.
263
+ const legacy = path.join(dir, 'minko-serve.vbs');
264
+ if (fs.existsSync(legacy)) {
265
+ fs.unlinkSync(legacy);
266
+ }
267
+ return true;
268
+ }
269
+ catch {
270
+ return false;
271
+ }
272
+ }
@@ -0,0 +1,69 @@
1
+ import { loadConfig, saveConfig } from "../config/store.js";
2
+ import { deviceLogin } from "../oauth/device.js";
3
+ import { saveOAuthToken } from "../oauth/store.js";
4
+ import { Painter } from "../ui/ansi.js";
5
+ export async function runLoginCommand() {
6
+ const painter = new Painter();
7
+ const config = loadConfig();
8
+ const oauth = config.oauth;
9
+ if (!oauth.deviceAuthorizationUrl || !oauth.tokenUrl || !oauth.clientId) {
10
+ process.stderr.write(painter.error('还没有配置 OAuth。请在 ~/.minko/config.json 添加:') + '\n');
11
+ process.stderr.write(' "oauth": {\n'
12
+ + ' "deviceAuthorizationUrl": "https://id.example/device",\n'
13
+ + ' "tokenUrl": "https://id.example/token",\n'
14
+ + ' "clientId": "...",\n'
15
+ + ' "scopes": ["openid", "profile"],\n'
16
+ + ' "provider": "你的服务商名"\n'
17
+ + ' }\n');
18
+ return 1;
19
+ }
20
+ try {
21
+ const tokens = await deviceLogin({
22
+ deviceAuthorizationUrl: oauth.deviceAuthorizationUrl,
23
+ tokenUrl: oauth.tokenUrl,
24
+ clientId: oauth.clientId,
25
+ scopes: oauth.scopes,
26
+ }, {
27
+ onUserCode: (info) => {
28
+ process.stdout.write('\n ' + painter.bold('OAuth 登录') + '\n');
29
+ process.stdout.write(' 1. 打开:' + painter.primary(info.verificationUri) + '\n');
30
+ if (info.verificationUriComplete) {
31
+ process.stdout.write(' 或直接打开:' + painter.soft(info.verificationUriComplete) + '\n');
32
+ }
33
+ process.stdout.write(' 2. 输入代码:' + painter.primary(info.userCode) + '\n');
34
+ process.stdout.write(' ' + painter.dimmed('等待授权…(Ctrl+C 取消)') + '\n\n');
35
+ },
36
+ });
37
+ const profile = oauth.name || oauth.provider || config.active || 'default';
38
+ saveOAuthToken(profile, tokens);
39
+ const providerName = oauth.provider || config.active;
40
+ if (providerName && config.providers[providerName]) {
41
+ saveConfig({
42
+ ...config,
43
+ providers: {
44
+ ...config.providers,
45
+ [providerName]: {
46
+ ...config.providers[providerName],
47
+ apiKey: tokens.accessToken,
48
+ auth: 'bearer',
49
+ },
50
+ },
51
+ });
52
+ process.stdout.write(painter.ok('✓ ') + '已登录,并更新服务商 '
53
+ + painter.soft(providerName) + painter.dimmed('(auth: bearer)') + '\n');
54
+ }
55
+ else {
56
+ process.stdout.write(painter.ok('✓ ') + '已保存 OAuth 令牌(profile '
57
+ + painter.soft(profile) + '),但没有可更新的服务商\n');
58
+ }
59
+ if (tokens.expiresAt) {
60
+ process.stdout.write(' ' + painter.dimmed('令牌过期时间:'
61
+ + new Date(tokens.expiresAt).toISOString().slice(0, 16).replace('T', ' ')) + '\n');
62
+ }
63
+ return 0;
64
+ }
65
+ catch (error) {
66
+ process.stderr.write(painter.error(error instanceof Error ? error.message : String(error)) + '\n');
67
+ return 1;
68
+ }
69
+ }
@@ -0,0 +1,68 @@
1
+ import { loadConfig, saveConfig } from "../config/store.js";
2
+ import { deviceLogin } from "../oauth/device.js";
3
+ import { saveOAuthToken } from "../oauth/store.js";
4
+ import { Painter } from "../ui/ansi.js";
5
+ export function withMcpToken(config, server, token) {
6
+ const current = config.mcpServers[server];
7
+ if (!current) {
8
+ return config;
9
+ }
10
+ return {
11
+ ...config,
12
+ mcpServers: {
13
+ ...config.mcpServers,
14
+ [server]: { ...current, token },
15
+ },
16
+ };
17
+ }
18
+ export async function runMcpLoginCommand(serverName) {
19
+ const painter = new Painter();
20
+ const config = loadConfig();
21
+ const names = Object.keys(config.mcpServers);
22
+ if (!serverName || serverName.trim().length === 0) {
23
+ process.stderr.write(painter.error('用法:minko mcp-login <server>') + '\n');
24
+ if (names.length > 0) {
25
+ process.stderr.write(' 可用 server:' + names.join('、') + '\n');
26
+ }
27
+ return 1;
28
+ }
29
+ const name = serverName.trim();
30
+ const server = config.mcpServers[name];
31
+ if (!server) {
32
+ process.stderr.write(painter.error('找不到 MCP server:' + name) + '\n');
33
+ return 1;
34
+ }
35
+ if (!server.oauth) {
36
+ process.stderr.write(painter.error('这个 MCP server 没有配置 oauth:' + name) + '\n');
37
+ return 1;
38
+ }
39
+ try {
40
+ const tokens = await deviceLogin({
41
+ deviceAuthorizationUrl: server.oauth.deviceAuthorizationUrl,
42
+ tokenUrl: server.oauth.tokenUrl,
43
+ clientId: server.oauth.clientId,
44
+ scopes: server.oauth.scopes,
45
+ }, {
46
+ onUserCode: (info) => {
47
+ process.stdout.write('\n ' + painter.bold('MCP OAuth 登录') + ' · ' + name + '\n');
48
+ process.stdout.write(' 1. 打开:' + painter.primary(info.verificationUri) + '\n');
49
+ if (info.verificationUriComplete) {
50
+ process.stdout.write(' 或直接打开:' + painter.soft(info.verificationUriComplete) + '\n');
51
+ }
52
+ process.stdout.write(' 2. 输入代码:' + painter.primary(info.userCode) + '\n');
53
+ process.stdout.write(' ' + painter.dimmed('等待授权…(Ctrl+C 取消)') + '\n\n');
54
+ },
55
+ });
56
+ saveConfig(withMcpToken(config, name, tokens.accessToken));
57
+ saveOAuthToken('mcp:' + name, tokens);
58
+ process.stdout.write(painter.ok('✓ ') + '已为 MCP server ' + painter.soft(name)
59
+ + ' 保存令牌' + (tokens.expiresAt
60
+ ? painter.dimmed('(' + new Date(tokens.expiresAt).toISOString().slice(0, 16).replace('T', ' ') + ' 过期)')
61
+ : '') + '\n');
62
+ return 0;
63
+ }
64
+ catch (error) {
65
+ process.stderr.write(painter.error(error instanceof Error ? error.message : String(error)) + '\n');
66
+ return 1;
67
+ }
68
+ }