@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,566 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import { runAgent } from "../agent/loop.js";
4
+ import { loadProjectContext } from "../agent/context.js";
5
+ import { createSubagentTool } from "../agent/subagent.js";
6
+ import { createPlanTools } from "../agent/plan.js";
7
+ import { createMemoryTools, defaultMemoryDir, loadMemoryContext, } from "../agent/memory.js";
8
+ import { WorkerRegistry, createWorkerTools } from "../agent/workers.js";
9
+ import { WorkerStore } from "../agent/workerStore.js";
10
+ import { LaneRegistry, createLaneTools } from "../agent/lanes.js";
11
+ import { TaskRegistry, createTaskTools } from "../agent/tasks.js";
12
+ import { CronScheduler, createCronTools } from "../agent/cron.js";
13
+ import { BranchLockStore, createBranchTools } from "../agent/branchLock.js";
14
+ import { ApprovalTokenStore, createApprovalTools } from "../agent/approvalTokens.js";
15
+ import { closeLspClients } from "../lsp/tool.js";
16
+ import { loadPlugins, mergeHooks } from "../plugins/loader.js";
17
+ import { systemPrompt } from "../agent/prompt.js";
18
+ import { ALL_TOOLS } from "../tools/registry.js";
19
+ import { titleFromMessages } from "../session/store.js";
20
+ import { TurnRecorder } from "../session/recorder.js";
21
+ import { friendlyError } from "../util/errors.js";
22
+ import { isLive, readStatus, writeStatus } from "../session/liveness.js";
23
+ const MAX_BODY = 1024 * 1024;
24
+ /** LAN API used by the phone: list/create sessions and stream replies as SSE. */
25
+ export class LinkServer {
26
+ options;
27
+ server = null;
28
+ running = new Map();
29
+ agentTool;
30
+ memoryDir = defaultMemoryDir();
31
+ memoryTools;
32
+ workers;
33
+ workerTools;
34
+ workerStore;
35
+ lanes;
36
+ laneTools;
37
+ taskTools;
38
+ crons;
39
+ cronTools;
40
+ branchTools;
41
+ approvalTokens;
42
+ approvalTools;
43
+ pluginTools;
44
+ effectiveHooks;
45
+ approvals = new Map();
46
+ nextApproval = 0;
47
+ onLog = () => { };
48
+ constructor(options) {
49
+ this.options = options;
50
+ this.agentTool = createSubagentTool({
51
+ getProvider: () => this.options.provider,
52
+ getConfig: () => this.options.config,
53
+ });
54
+ this.memoryTools = createMemoryTools(this.memoryDir);
55
+ this.workerStore = new WorkerStore(options.workerStoreDir);
56
+ this.approvalTokens = new ApprovalTokenStore(options.approvalStoreDir);
57
+ this.workers = new WorkerRegistry({
58
+ getProvider: () => this.options.provider,
59
+ getConfig: () => this.options.config,
60
+ cwd: this.options.cwd,
61
+ store: this.workerStore,
62
+ approvalStore: this.approvalTokens,
63
+ // Remote worker writes need an explicit full-power mode; there is no
64
+ // per-worker approval UI yet.
65
+ approve: async () => this.options.power === 'full',
66
+ });
67
+ this.workerTools = createWorkerTools(this.workers);
68
+ const branchLocks = new BranchLockStore(options.branchLockDir);
69
+ this.branchTools = createBranchTools(branchLocks);
70
+ this.lanes = new LaneRegistry({
71
+ getProvider: () => this.options.provider,
72
+ getConfig: () => this.options.config,
73
+ cwd: this.options.cwd,
74
+ lockStore: branchLocks,
75
+ approvalStore: this.approvalTokens,
76
+ approve: async () => this.options.power === 'full',
77
+ });
78
+ this.laneTools = createLaneTools(this.lanes);
79
+ this.taskTools = createTaskTools(new TaskRegistry({
80
+ workers: this.workers,
81
+ storeDir: options.taskStoreDir,
82
+ }));
83
+ this.crons = new CronScheduler({
84
+ workers: this.workers,
85
+ storeDir: options.cronStoreDir,
86
+ });
87
+ this.cronTools = createCronTools(this.crons);
88
+ this.approvalTools = createApprovalTools(this.approvalTokens);
89
+ const pluginLoad = loadPlugins(undefined, options.config?.plugins.disabled ?? []);
90
+ this.pluginTools = pluginLoad.plugins.flatMap((plugin) => plugin.tools);
91
+ this.effectiveHooks = mergeHooks(options.toolHooks, ...pluginLoad.plugins.map((plugin) => plugin.hooks));
92
+ }
93
+ async start(host, port) {
94
+ this.server = http.createServer((request, response) => {
95
+ void this.handle(request, response).catch((error) => {
96
+ writeJson(response, 500, { error: friendlyError(error) });
97
+ });
98
+ });
99
+ await new Promise((resolve, reject) => {
100
+ this.server?.once('error', reject);
101
+ this.server?.listen(port, host, () => resolve());
102
+ });
103
+ const address = this.server.address();
104
+ this.crons.start();
105
+ return typeof address === 'object' && address ? address.port : port;
106
+ }
107
+ requestApproval(sessionId, meta, request, send, signal) {
108
+ const id = sessionId + ':' + (this.nextApproval + 1).toString(36);
109
+ this.nextApproval += 1;
110
+ this.markStatus(sessionId, 'waiting_approval', meta);
111
+ return new Promise((resolve) => {
112
+ let settled = false;
113
+ const finish = (allowed) => {
114
+ if (settled) {
115
+ return;
116
+ }
117
+ settled = true;
118
+ const pending = this.approvals.get(id);
119
+ if (pending) {
120
+ clearTimeout(pending.timer);
121
+ this.approvals.delete(id);
122
+ }
123
+ signal.removeEventListener('abort', onAbort);
124
+ this.markStatus(sessionId, 'running', meta);
125
+ try {
126
+ send('approval_resolved', { id, allowed });
127
+ }
128
+ catch {
129
+ // The SSE connection may already be closed.
130
+ }
131
+ resolve(allowed);
132
+ };
133
+ const onAbort = () => finish(false);
134
+ signal.addEventListener('abort', onAbort, { once: true });
135
+ const timer = setTimeout(() => finish(false), 120_000);
136
+ timer.unref?.();
137
+ this.approvals.set(id, { resolve: finish, timer, sessionId });
138
+ try {
139
+ send('approval_request', {
140
+ id,
141
+ tool: request.tool,
142
+ title: request.title,
143
+ detail: request.detail,
144
+ danger: request.danger,
145
+ });
146
+ }
147
+ catch {
148
+ finish(false);
149
+ }
150
+ });
151
+ }
152
+ markStatus(id, status, meta) {
153
+ writeStatus(this.options.store.dir, {
154
+ sessionId: id,
155
+ status,
156
+ pid: process.pid,
157
+ updatedAt: new Date().toISOString(),
158
+ title: meta.title,
159
+ cwd: meta.cwd,
160
+ });
161
+ }
162
+ async stop() {
163
+ this.crons.stop();
164
+ this.workers.stopAll();
165
+ this.lanes.closeAll();
166
+ await closeLspClients();
167
+ for (const pending of [...this.approvals.values()]) {
168
+ pending.resolve(false);
169
+ }
170
+ this.approvals.clear();
171
+ for (const controller of this.running.values()) {
172
+ controller.abort();
173
+ }
174
+ this.running.clear();
175
+ await new Promise((resolve) => {
176
+ if (!this.server) {
177
+ resolve();
178
+ return;
179
+ }
180
+ const server = this.server;
181
+ this.server = null;
182
+ server.close(() => resolve());
183
+ server.closeAllConnections?.();
184
+ });
185
+ }
186
+ async handle(request, response) {
187
+ const url = new URL(request.url ?? '/', 'http://localhost');
188
+ const path = url.pathname;
189
+ if (path === '/health') {
190
+ writeJson(response, 200, {
191
+ ok: true,
192
+ name: this.options.name,
193
+ deviceId: this.options.deviceId,
194
+ version: this.options.version,
195
+ });
196
+ return;
197
+ }
198
+ const token = (request.headers.authorization ?? '').replace(/^Bearer\s+/i, '');
199
+ if (token !== this.options.token) {
200
+ writeJson(response, 401, { error: 'unauthorized' });
201
+ return;
202
+ }
203
+ if (path === '/pair') {
204
+ if (request.method === 'GET') {
205
+ writeJson(response, 200, { phones: this.options.getPhones ? this.options.getPhones() : [] });
206
+ return;
207
+ }
208
+ if (request.method === 'POST') {
209
+ const body = await readJson(request);
210
+ const name = typeof body.name === 'string' ? body.name.trim().slice(0, 24) : '手机';
211
+ this.options.onPair?.(name);
212
+ this.onLog('手机已配对:' + name);
213
+ writeJson(response, 200, { ok: true });
214
+ return;
215
+ }
216
+ }
217
+ if (path === '/roots' && request.method === 'GET') {
218
+ writeJson(response, 200, { roots: fileRoots() });
219
+ return;
220
+ }
221
+ if (path === '/workers' && request.method === 'GET') {
222
+ writeJson(response, 200, { workers: this.workerStore.list() });
223
+ return;
224
+ }
225
+ if (path === '/approvals' && request.method === 'GET') {
226
+ writeJson(response, 200, { tokens: this.approvalTokens.list() });
227
+ return;
228
+ }
229
+ if (path === '/approvals' && request.method === 'POST') {
230
+ const body = await readJson(request);
231
+ const token = this.approvalTokens.create({
232
+ tool: typeof body.tool === 'string' ? body.tool : '',
233
+ pattern: typeof body.pattern === 'string' ? body.pattern : undefined,
234
+ maxUses: typeof body.max_uses === 'number' ? body.max_uses : undefined,
235
+ expiresMinutes: typeof body.expires_minutes === 'number' ? body.expires_minutes : undefined,
236
+ reason: typeof body.reason === 'string' ? body.reason : undefined,
237
+ });
238
+ writeJson(response, 201, { token });
239
+ return;
240
+ }
241
+ const approvalMatch = /^\/approvals\/([A-Za-z0-9._-]+)$/.exec(path);
242
+ if (approvalMatch && request.method === 'DELETE') {
243
+ const removed = this.approvalTokens.revoke(approvalMatch[1]);
244
+ writeJson(response, removed ? 200 : 404, { ok: removed });
245
+ return;
246
+ }
247
+ const workerStopMatch = /^\/workers\/([A-Za-z0-9._-]+)\/stop$/.exec(path);
248
+ if (workerStopMatch && request.method === 'POST') {
249
+ const stopped = this.workers.stop(workerStopMatch[1]);
250
+ if (!stopped) {
251
+ writeJson(response, 404, { error: 'worker 不在当前进程中运行' });
252
+ return;
253
+ }
254
+ writeJson(response, 200, { ok: true });
255
+ return;
256
+ }
257
+ const workerMatch = /^\/workers\/([A-Za-z0-9._-]+)$/.exec(path);
258
+ if (workerMatch && request.method === 'GET') {
259
+ const record = this.workerStore.get(workerMatch[1]);
260
+ if (!record) {
261
+ writeJson(response, 404, { error: 'worker not found' });
262
+ return;
263
+ }
264
+ writeJson(response, 200, record);
265
+ return;
266
+ }
267
+ if (path === '/sessions' && request.method === 'GET') {
268
+ const sessions = this.options.store.list().map((meta) => {
269
+ const live = readStatus(this.options.store.dir, meta.id);
270
+ const active = isLive(live);
271
+ return {
272
+ ...meta,
273
+ status: active ? live?.status ?? 'idle' : 'idle',
274
+ live: active && live?.status !== 'idle',
275
+ };
276
+ });
277
+ const workers = this.workerStore.list();
278
+ writeJson(response, 200, {
279
+ sessions,
280
+ workers: {
281
+ total: workers.length,
282
+ active: workers.filter((worker) => worker.state === 'running' || worker.state === 'queued').length,
283
+ },
284
+ });
285
+ return;
286
+ }
287
+ if (path === '/sessions' && request.method === 'POST') {
288
+ const body = await readJson(request);
289
+ const cwd = typeof body.cwd === 'string' && body.cwd.length > 0 ? body.cwd : this.options.cwd;
290
+ const meta = this.options.store.create(cwd, 'remote');
291
+ if (typeof body.title === 'string' && body.title.length > 0) {
292
+ meta.title = body.title.slice(0, 60);
293
+ this.options.store.save(meta, []);
294
+ }
295
+ this.onLog('新建会话 ' + meta.id + '(' + cwd + ')');
296
+ writeJson(response, 201, { session: meta });
297
+ return;
298
+ }
299
+ const approveMatch = /^\/sessions\/([A-Za-z0-9._-]+)\/approve$/.exec(path);
300
+ if (approveMatch && request.method === 'POST') {
301
+ const body = await readJson(request);
302
+ const approvalId = typeof body.id === 'string' ? body.id : '';
303
+ const pending = this.approvals.get(approvalId);
304
+ if (!pending || pending.sessionId !== approveMatch[1]) {
305
+ writeJson(response, 404, { error: 'approval not found' });
306
+ return;
307
+ }
308
+ pending.resolve(body.allow === true);
309
+ writeJson(response, 200, { ok: true });
310
+ return;
311
+ }
312
+ const sessionMatch = /^\/sessions\/([A-Za-z0-9._-]+)$/.exec(path);
313
+ if (sessionMatch) {
314
+ const id = sessionMatch[1];
315
+ if (request.method === 'GET') {
316
+ const record = this.options.store.load(id);
317
+ if (!record) {
318
+ writeJson(response, 404, { error: 'not found' });
319
+ return;
320
+ }
321
+ writeJson(response, 200, record);
322
+ return;
323
+ }
324
+ if (request.method === 'DELETE') {
325
+ const removed = this.options.store.delete(id);
326
+ writeJson(response, removed ? 200 : 404, { ok: removed });
327
+ return;
328
+ }
329
+ }
330
+ const messageMatch = /^\/sessions\/([A-Za-z0-9._-]+)\/messages$/.exec(path);
331
+ if (messageMatch && request.method === 'POST') {
332
+ await this.handleMessage(messageMatch[1], request, response);
333
+ return;
334
+ }
335
+ writeJson(response, 404, { error: 'not found' });
336
+ }
337
+ async handleMessage(id, request, response) {
338
+ const record = this.options.store.load(id);
339
+ if (!record) {
340
+ writeJson(response, 404, { error: 'not found' });
341
+ return;
342
+ }
343
+ if (this.running.has(id)) {
344
+ writeJson(response, 409, { error: 'busy' });
345
+ return;
346
+ }
347
+ const body = await readJson(request);
348
+ const text = typeof body.text === 'string' ? body.text.trim() : '';
349
+ if (text.length === 0) {
350
+ writeJson(response, 400, { error: 'text is required' });
351
+ return;
352
+ }
353
+ const wantsStream = new URL(request.url ?? '/', 'http://localhost').searchParams.get('stream') !== '0';
354
+ if (!wantsStream) {
355
+ await this.replyOnce(id, record, text, response);
356
+ return;
357
+ }
358
+ response.writeHead(200, {
359
+ 'content-type': 'text/event-stream; charset=utf-8',
360
+ 'cache-control': 'no-cache',
361
+ connection: 'keep-alive',
362
+ });
363
+ const send = (event, data) => {
364
+ response.write('event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n');
365
+ };
366
+ const controller = new AbortController();
367
+ this.running.set(id, controller);
368
+ response.on('close', () => {
369
+ controller.abort();
370
+ this.running.delete(id);
371
+ });
372
+ const history = [...record.messages, { role: 'user', content: text }];
373
+ const recorder = new TurnRecorder(this.options.store, record.meta, history);
374
+ const planTools = createPlanTools(record.meta);
375
+ this.markStatus(id, 'running', record.meta);
376
+ send('accepted', { session: id });
377
+ try {
378
+ const outcome = await runAgent({
379
+ provider: this.options.provider,
380
+ tools: [
381
+ ...ALL_TOOLS,
382
+ ...(this.options.mcpTools ?? []),
383
+ ...planTools,
384
+ ...this.memoryTools,
385
+ ...this.workerTools,
386
+ ...this.pluginTools,
387
+ ...this.laneTools,
388
+ ...this.taskTools,
389
+ ...this.cronTools,
390
+ ...this.branchTools,
391
+ ...this.approvalTools,
392
+ this.agentTool,
393
+ ],
394
+ messages: [{
395
+ role: 'system',
396
+ content: systemPrompt({
397
+ cwd: record.meta.cwd,
398
+ project: loadProjectContext(record.meta.cwd),
399
+ planMode: record.meta.planMode,
400
+ memory: loadMemoryContext(this.memoryDir),
401
+ }),
402
+ }, ...history],
403
+ cwd: record.meta.cwd,
404
+ signal: controller.signal,
405
+ approval: 'ask',
406
+ power: this.options.power,
407
+ permissions: this.options.permissions,
408
+ toolHooks: this.effectiveHooks,
409
+ planState: record.meta,
410
+ config: this.options.config,
411
+ approvalStore: this.approvalTokens,
412
+ hooks: {
413
+ onText: (delta) => send('text', { text: delta }),
414
+ onToolStart: (call, args) => send('tool_start', { name: call.name, args }),
415
+ onToolEnd: (call, result) => send('tool_end', {
416
+ name: call.name, ok: result.ok, ms: result.ms,
417
+ }),
418
+ approve: (request) => this.requestApproval(id, record.meta, request, send, controller.signal),
419
+ onNotice: (notice) => send('notice', { text: notice }),
420
+ onAssistantMessage: (message) => recorder.record(message),
421
+ },
422
+ });
423
+ const messages = outcome.messages.filter((message) => message.role !== 'system');
424
+ record.meta.title = titleFromMessages(messages);
425
+ record.meta.updatedAt = new Date().toISOString();
426
+ recorder.commit(messages);
427
+ this.markStatus(id, 'idle', record.meta);
428
+ send('done', { usage: outcome.usage, session: record.meta });
429
+ }
430
+ catch (error) {
431
+ record.meta.title = titleFromMessages(recorder.snapshot());
432
+ record.meta.updatedAt = new Date().toISOString();
433
+ recorder.recover();
434
+ this.markStatus(id, 'idle', record.meta);
435
+ send('error', { message: friendlyError(error) });
436
+ }
437
+ finally {
438
+ this.running.delete(id);
439
+ response.end();
440
+ }
441
+ }
442
+ /** Non-streaming variant for simple clients: waits and returns one JSON. */
443
+ async replyOnce(id, record, text, response) {
444
+ const controller = new AbortController();
445
+ this.running.set(id, controller);
446
+ const history = [...record.messages, { role: 'user', content: text }];
447
+ const recorder = new TurnRecorder(this.options.store, record.meta, history);
448
+ const planTools = createPlanTools(record.meta);
449
+ this.markStatus(id, 'running', record.meta);
450
+ try {
451
+ const outcome = await runAgent({
452
+ provider: this.options.provider,
453
+ tools: [
454
+ ...ALL_TOOLS,
455
+ ...(this.options.mcpTools ?? []),
456
+ ...planTools,
457
+ ...this.memoryTools,
458
+ ...this.workerTools,
459
+ ...this.pluginTools,
460
+ ...this.laneTools,
461
+ ...this.taskTools,
462
+ ...this.cronTools,
463
+ ...this.branchTools,
464
+ ...this.approvalTools,
465
+ this.agentTool,
466
+ ],
467
+ messages: [{
468
+ role: 'system',
469
+ content: systemPrompt({
470
+ cwd: record.meta.cwd,
471
+ project: loadProjectContext(record.meta.cwd),
472
+ planMode: record.meta.planMode,
473
+ memory: loadMemoryContext(this.memoryDir),
474
+ }),
475
+ }, ...history],
476
+ cwd: record.meta.cwd,
477
+ signal: controller.signal,
478
+ approval: 'ask',
479
+ power: this.options.power,
480
+ permissions: this.options.permissions,
481
+ toolHooks: this.effectiveHooks,
482
+ planState: record.meta,
483
+ config: this.options.config,
484
+ approvalStore: this.approvalTokens,
485
+ hooks: {
486
+ onText: () => { },
487
+ approve: async () => false,
488
+ onAssistantMessage: (message) => recorder.record(message),
489
+ },
490
+ });
491
+ const messages = outcome.messages.filter((message) => message.role !== 'system');
492
+ record.meta.title = titleFromMessages(messages);
493
+ record.meta.updatedAt = new Date().toISOString();
494
+ recorder.commit(messages);
495
+ this.markStatus(id, 'idle', record.meta);
496
+ const last = messages[messages.length - 1];
497
+ writeJson(response, 200, {
498
+ text: last && last.role === 'assistant' ? last.content : '',
499
+ usage: outcome.usage,
500
+ session: record.meta,
501
+ });
502
+ }
503
+ catch (error) {
504
+ record.meta.title = titleFromMessages(recorder.snapshot());
505
+ record.meta.updatedAt = new Date().toISOString();
506
+ recorder.recover();
507
+ this.markStatus(id, 'idle', record.meta);
508
+ writeJson(response, 500, { error: friendlyError(error) });
509
+ }
510
+ finally {
511
+ this.running.delete(id);
512
+ }
513
+ }
514
+ }
515
+ /** Drives (Windows) or / so the phone can pick where a session lives. */
516
+ export function fileRoots() {
517
+ if (process.platform !== 'win32') {
518
+ return [{ name: '/', path: '/' }];
519
+ }
520
+ const roots = [];
521
+ for (let code = 65; code <= 90; code += 1) {
522
+ const letter = String.fromCharCode(code);
523
+ const path = letter + ':\\';
524
+ try {
525
+ if (fs.existsSync(path)) {
526
+ roots.push({ name: letter + ':', path });
527
+ }
528
+ }
529
+ catch {
530
+ // unreadable drive, skip
531
+ }
532
+ }
533
+ return roots;
534
+ }
535
+ function writeJson(response, status, body) {
536
+ if (response.headersSent) {
537
+ return;
538
+ }
539
+ const payload = JSON.stringify(body);
540
+ response.writeHead(status, {
541
+ 'content-type': 'application/json; charset=utf-8',
542
+ 'content-length': Buffer.byteLength(payload),
543
+ });
544
+ response.end(payload);
545
+ }
546
+ async function readJson(request) {
547
+ let size = 0;
548
+ const chunks = [];
549
+ for await (const chunk of request) {
550
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
551
+ size += buffer.length;
552
+ if (size > MAX_BODY) {
553
+ throw new Error('请求体过大');
554
+ }
555
+ chunks.push(buffer);
556
+ }
557
+ if (chunks.length === 0) {
558
+ return {};
559
+ }
560
+ try {
561
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
562
+ }
563
+ catch {
564
+ return {};
565
+ }
566
+ }
@@ -0,0 +1,90 @@
1
+ import { sseEvents } from "../providers/sse.js";
2
+ /** Keeps an outbound SSE tunnel to the relay and forwards calls to the local API. */
3
+ export class TunnelClient {
4
+ relay;
5
+ room;
6
+ token;
7
+ localPort;
8
+ stopped = false;
9
+ controller = null;
10
+ onLog = () => { };
11
+ constructor(relay, room, token, localPort) {
12
+ this.relay = relay.replace(/\/+$/, '');
13
+ this.room = room;
14
+ this.token = token;
15
+ this.localPort = localPort;
16
+ }
17
+ start() {
18
+ void this.loop();
19
+ }
20
+ stop() {
21
+ this.stopped = true;
22
+ this.controller?.abort();
23
+ this.controller = null;
24
+ }
25
+ async loop() {
26
+ while (!this.stopped) {
27
+ try {
28
+ this.controller = new AbortController();
29
+ const response = await fetch(this.relay + '/tunnel/' + this.room, {
30
+ headers: { authorization: 'Bearer ' + this.token },
31
+ signal: this.controller.signal,
32
+ });
33
+ if (!response.ok || !response.body) {
34
+ throw new Error('relay ' + response.status);
35
+ }
36
+ this.onLog('已连接中转 ' + this.relay);
37
+ for await (const event of sseEvents(response.body)) {
38
+ if (event.event === 'request') {
39
+ void this.forward(event.data);
40
+ }
41
+ }
42
+ }
43
+ catch {
44
+ if (this.stopped) {
45
+ return;
46
+ }
47
+ }
48
+ await new Promise((resolve) => setTimeout(resolve, 3000));
49
+ }
50
+ }
51
+ async forward(raw) {
52
+ let message;
53
+ try {
54
+ message = JSON.parse(raw);
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ const id = message.id ?? '';
60
+ const method = message.method ?? 'GET';
61
+ const path = message.path ?? '/sessions';
62
+ try {
63
+ const init = {
64
+ method,
65
+ headers: { authorization: 'Bearer ' + this.token, 'content-type': 'application/json' },
66
+ };
67
+ if (method !== 'GET' && method !== 'HEAD') {
68
+ init.body = message.body ?? '';
69
+ }
70
+ const local = await fetch('http://127.0.0.1:' + this.localPort + path, init);
71
+ const text = await local.text();
72
+ await this.respond(id, local.status, text);
73
+ }
74
+ catch {
75
+ await this.respond(id, 502, JSON.stringify({ error: '电脑处理失败' }));
76
+ }
77
+ }
78
+ async respond(id, status, body) {
79
+ try {
80
+ await fetch(this.relay + '/tunnel/' + this.room + '/response', {
81
+ method: 'POST',
82
+ headers: { authorization: 'Bearer ' + this.token, 'content-type': 'application/json' },
83
+ body: JSON.stringify({ id, status, body }),
84
+ });
85
+ }
86
+ catch {
87
+ // the relay will time this call out
88
+ }
89
+ }
90
+ }