ai-project-manage-cli 7.0.9 → 7.1.2

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 (32) hide show
  1. package/README.md +31 -3
  2. package/dist/index.js +7601 -3971
  3. package/dist/webide-message-worker.js +2036 -0
  4. package/package.json +4 -2
  5. package/template/AGENTS.md +28 -25
  6. package/template/apm.config.json +1 -5
  7. package/template/project/.gitkeep +0 -0
  8. package/template/rules/reply.md +8 -8
  9. package/template/rules/write_doc.md +17 -5
  10. package/template/sessions/.gitkeep +0 -0
  11. package/template/skills/apm-apply-change/SKILL.md +116 -0
  12. package/template/skills/apm-confirm-assumptions/SKILL.md +3 -0
  13. package/template/skills/apm-dev/SKILL.md +80 -0
  14. package/template/skills/apm-diff-review/SKILL.md +12 -13
  15. package/template/skills/apm-propose/SKILL.md +52 -0
  16. package/template/skills/apm-propose/design.md +37 -0
  17. package/template/skills/apm-propose/proposal.md +31 -0
  18. package/template/skills/apm-propose/specs.md +39 -0
  19. package/template/skills/apm-propose/tasks.md +16 -0
  20. package/template/skills/apm-recap/SKILL.md +77 -0
  21. package/template/skills/apm-recap/recap-template.md +121 -0
  22. package/template/skills/apm-update-plan/SKILL.md +26 -0
  23. package/template/skills/apm-write-assumptions/SKILL.md +78 -0
  24. package/template/skills/apm-write-assumptions/assumptions-template.md +18 -0
  25. package/template/skills/apm-write-checklist/SKILL.md +5 -5
  26. package/template/skills/apm-write-frontend-plan/SKILL.md +27 -0
  27. package/template/skills/apm-write-frontend-plan/plan-template.md +40 -0
  28. package/template/skills/apm-write-plan/SKILL.md +51 -0
  29. package/template/skills/apm-write-plan/api-template.md +35 -0
  30. package/template/skills/apm-write-plan/plan-template.md +72 -0
  31. package/template/skills/apm-write-prd/SKILL.md +14 -0
  32. package/template/skills/apm-write-prd/template.md +134 -0
@@ -0,0 +1,2036 @@
1
+ // src/commands/connect/webide-message-worker.ts
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+
4
+ // src/api/client.ts
5
+ import { createApiClient } from "listpage-http";
6
+
7
+ // src/api/request-config.ts
8
+ import { defineEndpoint } from "listpage-http";
9
+ var requestConfig = {
10
+ cli: {
11
+ me: defineEndpoint(
12
+ {
13
+ method: "GET",
14
+ path: "/cli/me"
15
+ }
16
+ ),
17
+ sessionDetail: defineEndpoint({
18
+ method: "GET",
19
+ path: "/cli/sessions/detail"
20
+ }),
21
+ sessionMembers: defineEndpoint({
22
+ method: "GET",
23
+ path: "/cli/sessions/members"
24
+ }),
25
+ listSessionMessages: defineEndpoint({
26
+ method: "GET",
27
+ path: "/cli/messages"
28
+ }),
29
+ listDocuments: defineEndpoint({
30
+ method: "GET",
31
+ path: "/cli/documents"
32
+ }),
33
+ getDevGate: defineEndpoint({
34
+ method: "GET",
35
+ path: "/cli/dev-gate"
36
+ }),
37
+ listAttachments: defineEndpoint(
38
+ {
39
+ method: "GET",
40
+ path: "/cli/attachments"
41
+ }
42
+ ),
43
+ upsertDocument: defineEndpoint({
44
+ method: "PUT",
45
+ path: "/cli/documents/upsert"
46
+ }),
47
+ appendMessageContent: defineEndpoint({
48
+ method: "PUT",
49
+ path: "/cli/messages/content"
50
+ }),
51
+ setMessageError: defineEndpoint({
52
+ method: "PUT",
53
+ path: "/cli/messages/error"
54
+ }),
55
+ upsertCursorMessageLog: defineEndpoint({
56
+ method: "PUT",
57
+ path: "/cli/cursor-message-logs"
58
+ }),
59
+ updateMessageStatus: defineEndpoint({
60
+ method: "PUT",
61
+ path: "/cli/messages/status"
62
+ }),
63
+ webideAppendMessageContent: defineEndpoint({
64
+ method: "PUT",
65
+ path: "/cli/webide/messages/content"
66
+ }),
67
+ webideEnsureMessageContent: defineEndpoint({
68
+ method: "PUT",
69
+ path: "/cli/webide/messages/ensure-content"
70
+ }),
71
+ webideSetMessageError: defineEndpoint({
72
+ method: "PUT",
73
+ path: "/cli/webide/messages/error"
74
+ }),
75
+ webideUpdateMessageStatus: defineEndpoint({
76
+ method: "PUT",
77
+ path: "/cli/webide/messages/status"
78
+ }),
79
+ webideUpsertMessageLog: defineEndpoint({
80
+ method: "PUT",
81
+ path: "/cli/webide/message-logs"
82
+ }),
83
+ webideReplaceAssumptions: defineEndpoint({
84
+ method: "PUT",
85
+ path: "/cli/webide/assumptions"
86
+ }),
87
+ webideListAssumptions: defineEndpoint({
88
+ method: "GET",
89
+ path: "/cli/webide/assumptions"
90
+ }),
91
+ branchBaseline: defineEndpoint({
92
+ method: "GET",
93
+ path: "/cli/tasks/branch-baseline"
94
+ }),
95
+ listSessionsForBranchCleanup: defineEndpoint({
96
+ method: "GET",
97
+ path: "/cli/sessions/branch-cleanup"
98
+ }),
99
+ workspaceBaseline: defineEndpoint({
100
+ method: "GET",
101
+ path: "/cli/workspaces/baseline"
102
+ }),
103
+ getDeploymentConfiguration: defineEndpoint({
104
+ method: "GET",
105
+ path: "/cli/deployment-configurations"
106
+ }),
107
+ matchRepository: defineEndpoint({
108
+ method: "GET",
109
+ path: "/cli/repositories/match"
110
+ }),
111
+ listSkills: defineEndpoint({
112
+ method: "GET",
113
+ path: "/cli/skills"
114
+ }),
115
+ listRules: defineEndpoint({
116
+ method: "GET",
117
+ path: "/cli/rules"
118
+ }),
119
+ createPullRequest: defineEndpoint({
120
+ method: "POST",
121
+ path: "/cli/pull-requests"
122
+ }),
123
+ getRepositoryProjectDocumentManifest: defineEndpoint({
124
+ method: "GET",
125
+ path: "/cli/repository-project-documents/manifest"
126
+ }),
127
+ listRepositoryProjectDocuments: defineEndpoint({
128
+ method: "GET",
129
+ path: "/cli/repository-project-documents"
130
+ }),
131
+ upsertRepositoryProjectDocument: defineEndpoint({
132
+ method: "PUT",
133
+ path: "/cli/repository-project-documents/upsert"
134
+ }),
135
+ removeRepositoryProjectDocument: defineEndpoint({
136
+ method: "DELETE",
137
+ path: "/cli/repository-project-documents"
138
+ }),
139
+ createTaskDeployment: defineEndpoint({
140
+ method: "POST",
141
+ path: "/cli/task-deployments"
142
+ }),
143
+ updateTaskDeploymentStatus: defineEndpoint({
144
+ method: "PUT",
145
+ path: "/cli/task-deployments/status"
146
+ }),
147
+ syncTaskDeploymentLog: defineEndpoint({
148
+ method: "PUT",
149
+ path: "/cli/task-deployments/log"
150
+ }),
151
+ completeTaskDeployment: defineEndpoint({
152
+ method: "PUT",
153
+ path: "/cli/task-deployments/complete"
154
+ }),
155
+ getDeployArtifactStorage: defineEndpoint(
156
+ {
157
+ method: "GET",
158
+ path: "/cli/deploy-artifact-storage"
159
+ }
160
+ ),
161
+ attachTaskDeploymentArtifact: defineEndpoint({
162
+ method: "PUT",
163
+ path: "/cli/task-deployments/artifact"
164
+ })
165
+ }
166
+ };
167
+
168
+ // src/config.ts
169
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
170
+ import { homedir } from "os";
171
+ import { join } from "path";
172
+ var APM_CONFIG_DIR = join(homedir(), ".config", "apm");
173
+ var APM_CONFIG_PATH = join(APM_CONFIG_DIR, "config.json");
174
+ function resolveClientMachineId(cfg) {
175
+ return (cfg.clientMachineId ?? cfg.userId ?? "").trim();
176
+ }
177
+ function resolveApiKey(cfg) {
178
+ return (cfg.apiKey ?? cfg.token ?? "").trim();
179
+ }
180
+ async function tryReadApmConfig() {
181
+ try {
182
+ const raw = readFileSync(APM_CONFIG_PATH, "utf8");
183
+ const v = JSON.parse(raw);
184
+ if (typeof v !== "object" || v === null) {
185
+ return null;
186
+ }
187
+ const rawCfg = v;
188
+ if (typeof rawCfg.baseUrl !== "string") {
189
+ return null;
190
+ }
191
+ const apiKey = resolveApiKey(rawCfg);
192
+ if (!apiKey) return null;
193
+ const cfg = v;
194
+ const clientMachineId = resolveClientMachineId(cfg);
195
+ return {
196
+ baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
197
+ apiKey,
198
+ ...clientMachineId ? { clientMachineId } : {}
199
+ };
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ // src/api/client.ts
206
+ function createApmApiClient(cfg) {
207
+ const baseURL = `${cfg.baseUrl.trim().replace(/\/+$/, "")}/api/v1`;
208
+ return createApiClient(requestConfig, {
209
+ baseURL,
210
+ getToken: () => resolveApiKey(cfg) || void 0,
211
+ successCodes: [0],
212
+ unauthorizedCodes: [401]
213
+ });
214
+ }
215
+
216
+ // src/workdir-path.ts
217
+ import { realpathSync } from "fs";
218
+ import { platform } from "os";
219
+ import { resolve } from "path";
220
+ function toFsPath(inputPath) {
221
+ const absolute = resolve(inputPath);
222
+ if (platform() !== "win32") return absolute;
223
+ if (absolute.startsWith("\\\\?\\")) return absolute;
224
+ const normalized = absolute.replace(/\//g, "\\");
225
+ if (normalized.startsWith("\\\\")) {
226
+ return `\\\\?\\UNC\\${normalized.slice(2)}`;
227
+ }
228
+ return `\\\\?\\${normalized}`;
229
+ }
230
+ function normalizeWorkdirPath(path) {
231
+ let normalized = path.trim().replace(/\\/g, "/").normalize("NFC");
232
+ if (normalized.startsWith("//?/")) {
233
+ normalized = normalized.slice(4);
234
+ }
235
+ const windowsDrive = /^([A-Za-z]:)\/*(.*)$/.exec(normalized);
236
+ if (windowsDrive) {
237
+ const drive = windowsDrive[1].toLowerCase();
238
+ const rest = windowsDrive[2].replace(/\/+/g, "/").replace(/\/$/, "");
239
+ return rest ? `${drive}/${rest}` : drive;
240
+ }
241
+ normalized = normalized.replace(/\/+/g, "/");
242
+ if (normalized.length > 1 && normalized.endsWith("/")) {
243
+ normalized = normalized.slice(0, -1);
244
+ }
245
+ return normalized;
246
+ }
247
+ function resolveWorkdirPath(cwd = process.cwd()) {
248
+ const absolute = resolve(cwd);
249
+ try {
250
+ return normalizeWorkdirPath(realpathSync.native(absolute));
251
+ } catch {
252
+ return normalizeWorkdirPath(absolute);
253
+ }
254
+ }
255
+ function requireRemoteWorkdir(workdir) {
256
+ const trimmed = typeof workdir === "string" ? workdir.trim() : "";
257
+ if (!trimmed) {
258
+ throw new Error("[apm] \u8FDC\u7A0B\u6D88\u606F\u7F3A\u5C11\u5DE5\u4F5C\u76EE\u5F55 workdir");
259
+ }
260
+ return resolveWorkdirPath(trimmed);
261
+ }
262
+
263
+ // src/commands/connect/cursor-agent.ts
264
+ import {
265
+ Agent,
266
+ CursorAgentError
267
+ } from "@cursor/sdk";
268
+ import { setMaxListeners as setMaxListeners2 } from "node:events";
269
+
270
+ // src/plan-format.ts
271
+ function formatPlanMarkdown(raw) {
272
+ let text = raw.trim();
273
+ if (!text) {
274
+ return text;
275
+ }
276
+ if (text.startsWith("{") && text.endsWith("}")) {
277
+ try {
278
+ const parsed = JSON.parse(text);
279
+ if (typeof parsed.plan === "string") {
280
+ return formatPlanMarkdown(parsed.plan);
281
+ }
282
+ } catch {
283
+ }
284
+ }
285
+ if (!text.includes("\n") && text.includes("\\n")) {
286
+ text = text.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
287
+ }
288
+ return text.replace(/\r\n/g, "\n").trimEnd() + "\n";
289
+ }
290
+
291
+ // src/session-utils.ts
292
+ var EventSession = class {
293
+ events = [];
294
+ dirtyIndices = /* @__PURE__ */ new Set();
295
+ constructor(prompt) {
296
+ this.events.push({
297
+ type: "input",
298
+ content: prompt
299
+ });
300
+ this.markDirty(0);
301
+ }
302
+ markDirty(index) {
303
+ this.dirtyIndices.add(index);
304
+ }
305
+ addEvent(event) {
306
+ const latestEvent = this.events[this.events.length - 1];
307
+ const formatedEvent = this.formatEvent(event);
308
+ if (!formatedEvent) {
309
+ return;
310
+ }
311
+ if (formatedEvent.type === "tool_call") {
312
+ const existingIndex = this.events.findIndex(
313
+ (e) => e.type === "tool_call" && e.call_id === formatedEvent.call_id
314
+ );
315
+ if (existingIndex >= 0) {
316
+ const existingToolCall = this.events[existingIndex];
317
+ existingToolCall.args = formatedEvent.args;
318
+ existingToolCall.result = formatedEvent.result;
319
+ existingToolCall.status = formatedEvent.status;
320
+ this.markDirty(existingIndex);
321
+ return;
322
+ }
323
+ this.events.push(formatedEvent);
324
+ this.markDirty(this.events.length - 1);
325
+ return;
326
+ }
327
+ if (formatedEvent.type === "status") {
328
+ return;
329
+ }
330
+ if (formatedEvent.type === "request") {
331
+ this.events.push(formatedEvent);
332
+ this.markDirty(this.events.length - 1);
333
+ return;
334
+ }
335
+ if (latestEvent?.type === formatedEvent.type) {
336
+ switch (formatedEvent.type) {
337
+ case "assistant":
338
+ latestEvent.content += formatedEvent.content;
339
+ break;
340
+ case "thinking":
341
+ latestEvent.content += formatedEvent.content;
342
+ break;
343
+ case "task":
344
+ latestEvent.status = formatedEvent.status;
345
+ latestEvent.text = formatedEvent.text;
346
+ break;
347
+ }
348
+ this.markDirty(this.events.length - 1);
349
+ return;
350
+ }
351
+ this.events.push(formatedEvent);
352
+ this.markDirty(this.events.length - 1);
353
+ }
354
+ formatEvent(event) {
355
+ switch (event.type) {
356
+ case "assistant": {
357
+ let content = "";
358
+ for (const block of event.message.content) {
359
+ if (block.type === "text" && block.text) {
360
+ content += block.text;
361
+ }
362
+ }
363
+ return {
364
+ type: "assistant",
365
+ content: content || event.content || ""
366
+ };
367
+ }
368
+ case "thinking":
369
+ return {
370
+ type: "thinking",
371
+ content: event.text || event.content || ""
372
+ };
373
+ case "tool_call":
374
+ return {
375
+ type: "tool_call",
376
+ args: event.args,
377
+ result: event.result,
378
+ status: event.status,
379
+ call_id: event.call_id,
380
+ name: event.name
381
+ };
382
+ case "task":
383
+ return {
384
+ type: "task",
385
+ status: event.status,
386
+ text: event.text
387
+ };
388
+ case "request":
389
+ return {
390
+ ...event,
391
+ type: "request"
392
+ };
393
+ case "status":
394
+ return { type: "status", status: event.status, message: event.message };
395
+ }
396
+ }
397
+ getDirtyEvents() {
398
+ return [...this.dirtyIndices].sort((a, b) => a - b).map((index) => {
399
+ const event = this.events[index];
400
+ return {
401
+ index,
402
+ type: event.type,
403
+ data: JSON.stringify(event)
404
+ };
405
+ });
406
+ }
407
+ clearDirty(indices) {
408
+ for (const index of indices) {
409
+ this.dirtyIndices.delete(index);
410
+ }
411
+ }
412
+ getEventCount() {
413
+ return this.events.length;
414
+ }
415
+ /** 合并所有 assistant 片段,供剧场成员回传等场景使用 */
416
+ getAssistantText() {
417
+ return this.events.filter((e) => e.type === "assistant").map((e) => String(e.content ?? "")).join("\n").trim();
418
+ }
419
+ /** plan 模式下 createPlan 工具 completed 时的 plan 字段(取最后一次) */
420
+ getCreatePlanContent() {
421
+ for (let i = this.events.length - 1; i >= 0; i--) {
422
+ const event = this.events[i];
423
+ if (event.type !== "tool_call") {
424
+ continue;
425
+ }
426
+ if (event.name !== "createPlan" || event.status !== "completed") {
427
+ continue;
428
+ }
429
+ const plan = event.args?.plan;
430
+ if (typeof plan === "string" && plan.trim()) {
431
+ return formatPlanMarkdown(plan);
432
+ }
433
+ }
434
+ return void 0;
435
+ }
436
+ resolveLogContent() {
437
+ return this.events.map((event) => formatLogEvent(event.type, event)).join("\n");
438
+ }
439
+ };
440
+ function formatLogEvent(type, event) {
441
+ if (type === "input") {
442
+ return `## \u7528\u6237\u8F93\u5165
443
+
444
+ ${String(event.content ?? "")}
445
+ `;
446
+ }
447
+ if (type === "assistant") {
448
+ return `## \u6A21\u578B\u8F93\u51FA
449
+
450
+ ${String(event.content ?? "")}
451
+ `;
452
+ }
453
+ if (type === "thinking") {
454
+ return `## \u6A21\u578B\u601D\u8003
455
+
456
+ ${String(event.content ?? "")}
457
+ `;
458
+ }
459
+ if (type === "tool_call") {
460
+ return "````toolcall\n" + JSON.stringify(event, null, 2) + "\n````\n";
461
+ }
462
+ return `## \u672A\u77E5\u4E8B\u4EF6\uFF1A${type}
463
+
464
+ \`\`\`json
465
+ ${JSON.stringify(event, null, 2)}
466
+ \`\`\``;
467
+ }
468
+
469
+ // src/commands/connect/agent-session-registry.ts
470
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
471
+ import { dirname, resolve as resolve2 } from "node:path";
472
+ function registryPath(workdir, sessionId) {
473
+ return resolve2(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
474
+ }
475
+ function readRegistry(path) {
476
+ if (!existsSync(path)) {
477
+ return {};
478
+ }
479
+ try {
480
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
481
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
482
+ const result = {};
483
+ for (const [key, value] of Object.entries(
484
+ parsed
485
+ )) {
486
+ if (typeof value === "string" && value.trim()) {
487
+ result[key] = value.trim();
488
+ }
489
+ }
490
+ return result;
491
+ }
492
+ } catch {
493
+ }
494
+ return {};
495
+ }
496
+ function writeRegistry(path, registry) {
497
+ mkdirSync2(dirname(path), { recursive: true });
498
+ writeFileSync2(path, `${JSON.stringify(registry, null, 2)}
499
+ `, "utf8");
500
+ }
501
+ function loadSessionAgentId(workdir, sessionId, user) {
502
+ const registry = readRegistry(registryPath(workdir, sessionId));
503
+ return registry[user];
504
+ }
505
+ function saveSessionAgentId(workdir, sessionId, user, agentId) {
506
+ const path = registryPath(workdir, sessionId);
507
+ const registry = readRegistry(path);
508
+ registry[user] = agentId;
509
+ writeRegistry(path, registry);
510
+ }
511
+ function clearSessionAgentId(workdir, sessionId, user) {
512
+ const path = registryPath(workdir, sessionId);
513
+ const registry = readRegistry(path);
514
+ if (!(user in registry)) {
515
+ return;
516
+ }
517
+ delete registry[user];
518
+ writeRegistry(path, registry);
519
+ }
520
+
521
+ // src/commands/connect/cursor-message-log.ts
522
+ var CURSOR_MESSAGE_LOG_SYNC_INTERVAL_MS = 2e3;
523
+ function createThrottledCursorMessageLogSync(cfg, ctx, onError) {
524
+ let lastRunAt = 0;
525
+ let timer;
526
+ let latestSession;
527
+ let syncChain = Promise.resolve();
528
+ const syncDirtyEventsOnce = async (session) => {
529
+ const events = session.getDirtyEvents();
530
+ if (events.length === 0) {
531
+ return;
532
+ }
533
+ lastRunAt = Date.now();
534
+ try {
535
+ await syncCursorMessageLog(cfg, ctx, events);
536
+ session.clearDirty(events.map((event) => event.index));
537
+ } catch (err) {
538
+ onError(err);
539
+ }
540
+ };
541
+ const drainDirtyEvents = async (session) => {
542
+ while (session.getDirtyEvents().length > 0) {
543
+ await syncDirtyEventsOnce(session);
544
+ }
545
+ };
546
+ const enqueueSync = (session) => {
547
+ syncChain = syncChain.then(() => drainDirtyEvents(session));
548
+ };
549
+ return {
550
+ schedule(session) {
551
+ latestSession = session;
552
+ const now = Date.now();
553
+ const elapsed = now - lastRunAt;
554
+ if (elapsed >= CURSOR_MESSAGE_LOG_SYNC_INTERVAL_MS) {
555
+ if (timer) {
556
+ clearTimeout(timer);
557
+ timer = void 0;
558
+ }
559
+ enqueueSync(session);
560
+ return;
561
+ }
562
+ if (timer) {
563
+ return;
564
+ }
565
+ timer = setTimeout(() => {
566
+ timer = void 0;
567
+ enqueueSync(latestSession);
568
+ }, CURSOR_MESSAGE_LOG_SYNC_INTERVAL_MS - elapsed);
569
+ },
570
+ async flush(session) {
571
+ latestSession = session;
572
+ if (timer) {
573
+ clearTimeout(timer);
574
+ timer = void 0;
575
+ }
576
+ await syncChain;
577
+ await drainDirtyEvents(session);
578
+ }
579
+ };
580
+ }
581
+ async function syncCursorMessageLog(cfg, ctx, events) {
582
+ const agentId = ctx.agentId.trim();
583
+ if (!agentId || events.length === 0) {
584
+ return;
585
+ }
586
+ const api = createApmApiClient(cfg);
587
+ await api.cli.upsertCursorMessageLog({
588
+ sessionId: ctx.sessionId,
589
+ messageId: ctx.messageId,
590
+ agentId,
591
+ events
592
+ });
593
+ }
594
+
595
+ // src/commands/connect/abort-signal-debug.ts
596
+ import {
597
+ getEventListeners,
598
+ getMaxListeners,
599
+ setMaxListeners
600
+ } from "node:events";
601
+ function isAbortSignalDebugEnabled() {
602
+ const v = process.env.APM_DEBUG_ABORT_SIGNAL?.trim().toLowerCase();
603
+ return v === "1" || v === "true" || v === "yes";
604
+ }
605
+ function formatAbortSignalStats(signal, label) {
606
+ if (!signal) {
607
+ return `[apm:abort-debug] ${label}: (no signal)`;
608
+ }
609
+ const listeners = getEventListeners(signal, "abort");
610
+ const max = getMaxListeners(signal);
611
+ return `[apm:abort-debug] ${label}: abortListeners=${listeners.length} maxListeners=${max} aborted=${signal.aborted}`;
612
+ }
613
+ function logAbortSignalStats(signal, label) {
614
+ if (!isAbortSignalDebugEnabled()) return;
615
+ console.log(formatAbortSignalStats(signal, label));
616
+ }
617
+ var installed = false;
618
+ function installAbortSignalDebug() {
619
+ if (!isAbortSignalDebugEnabled() || installed) return;
620
+ installed = true;
621
+ const maxFromEnv = Number.parseInt(
622
+ process.env.APM_ABORT_SIGNAL_MAX_LISTENERS ?? "",
623
+ 10
624
+ );
625
+ if (Number.isFinite(maxFromEnv) && maxFromEnv > 0) {
626
+ setMaxListeners(maxFromEnv);
627
+ console.log(
628
+ `[apm:abort-debug] setMaxListeners(${maxFromEnv}) via APM_ABORT_SIGNAL_MAX_LISTENERS`
629
+ );
630
+ }
631
+ process.on("warning", (warning) => {
632
+ if (warning.name !== "MaxListenersExceededWarning") return;
633
+ console.warn(`[apm:abort-debug] ${warning.name}: ${warning.message}`);
634
+ if (warning.stack) {
635
+ console.warn(warning.stack);
636
+ }
637
+ });
638
+ const proto = AbortSignal.prototype;
639
+ const original = proto.addEventListener;
640
+ proto.addEventListener = function(type, listener, options) {
641
+ if (type === "abort") {
642
+ const sig = this;
643
+ const before = getEventListeners(sig, "abort").length;
644
+ const max = getMaxListeners(sig);
645
+ const stack = new Error("[apm:abort-debug] addEventListener stack").stack?.split("\n").slice(2, 8).join("\n") ?? "";
646
+ console.log(
647
+ `[apm:abort-debug] addEventListener("abort") before=${before} max=${max}
648
+ ${stack}`
649
+ );
650
+ }
651
+ return original.call(this, type, listener, options);
652
+ };
653
+ console.log(
654
+ "[apm:abort-debug] \u5DF2\u542F\u7528 AbortSignal \u8C03\u8BD5\uFF08APM_DEBUG_ABORT_SIGNAL\uFF09"
655
+ );
656
+ }
657
+
658
+ // src/command-utils.ts
659
+ import {
660
+ copyFileSync,
661
+ existsSync as existsSync2,
662
+ mkdirSync as mkdirSync3,
663
+ readFileSync as readFileSync3,
664
+ readdirSync,
665
+ statSync,
666
+ writeFileSync as writeFileSync3
667
+ } from "fs";
668
+ import { basename, dirname as dirname2, extname, join as join2, resolve as resolve3 } from "path";
669
+ import { fileURLToPath } from "url";
670
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
671
+ var CLI_TEMPLATE_DIR = resolve3(__dirname, "../template");
672
+ function workspaceApmDir(cwd = resolveWorkdirPath()) {
673
+ return resolve3(resolve3(cwd), ".apm");
674
+ }
675
+ function isWorkspaceApmInitialized(workdir) {
676
+ const apmDir = workspaceApmDir(workdir);
677
+ const fsApmDir = toFsPath(apmDir);
678
+ if (!existsSync2(fsApmDir)) {
679
+ return false;
680
+ }
681
+ const st = statSync(fsApmDir);
682
+ if (!st.isDirectory()) {
683
+ throw new Error(
684
+ `\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u4E0B\u7684 .apm \u4E0D\u662F\u76EE\u5F55\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u63A5\u5165\u72B6\u6001\u3002`
685
+ );
686
+ }
687
+ return readdirSync(fsApmDir).length > 0;
688
+ }
689
+ var APM_GITIGNORE_PATTERNS = [
690
+ /^\.apm\/?$/,
691
+ /^\.apm\/\*\*$/,
692
+ /^\*\*\/\.apm\/?$/,
693
+ /^\*\*\/\.apm\/\*\*$/,
694
+ /^\/\.apm\/?$/,
695
+ /^\/\.apm\/\*\*$/
696
+ ];
697
+ function normalizeGitignorePattern(line) {
698
+ const trimmed = line.trim();
699
+ if (!trimmed || trimmed.startsWith("#")) return "";
700
+ if (trimmed.startsWith("!")) return "";
701
+ const hashIndex = trimmed.indexOf("#");
702
+ return (hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed).trim();
703
+ }
704
+ function gitignoreIgnoresApm(line) {
705
+ const pattern = normalizeGitignorePattern(line);
706
+ if (!pattern) return false;
707
+ return APM_GITIGNORE_PATTERNS.some((re) => re.test(pattern));
708
+ }
709
+ var APM_GITIGNORE_LINE = "**/.apm/**";
710
+ function ensureApmGitignoredInRepo(workdir) {
711
+ const gitignorePath = join2(workdir, ".gitignore");
712
+ const fsGitignorePath = toFsPath(gitignorePath);
713
+ if (!existsSync2(fsGitignorePath)) {
714
+ writeFileSync3(fsGitignorePath, `${APM_GITIGNORE_LINE}
715
+ `, "utf8");
716
+ return true;
717
+ }
718
+ const content = readFileSync3(fsGitignorePath, "utf8");
719
+ if (content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
720
+ return false;
721
+ }
722
+ const suffix = content.endsWith("\n") || content.length === 0 ? "" : "\n";
723
+ writeFileSync3(
724
+ fsGitignorePath,
725
+ `${content}${suffix}${APM_GITIGNORE_LINE}
726
+ `,
727
+ "utf8"
728
+ );
729
+ return true;
730
+ }
731
+ function assertApmGitignoredInRepo(workdir) {
732
+ const gitignorePath = join2(workdir, ".gitignore");
733
+ const fsGitignorePath = toFsPath(gitignorePath);
734
+ if (!existsSync2(fsGitignorePath)) {
735
+ throw new Error(
736
+ `\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u7F3A\u5C11 .gitignore \u6587\u4EF6\uFF0C\u8BF7\u6DFB\u52A0\u5BF9 .apm \u7684\u5FFD\u7565\u89C4\u5219\uFF08\u4F8B\u5982 **/.apm/**\uFF09\uFF0C\u907F\u514D\u672C\u5730\u4F1A\u8BDD\u4E0E\u90E8\u7F72\u51ED\u636E\u88AB\u63D0\u4EA4\u3002`
737
+ );
738
+ }
739
+ const content = readFileSync3(fsGitignorePath, "utf8");
740
+ if (!content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
741
+ throw new Error(
742
+ `\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u7684 .gitignore \u672A\u5FFD\u7565 .apm\uFF0C\u8BF7\u6DFB\u52A0 **/.apm/** \u6216 .apm/\uFF0C\u907F\u514D\u672C\u5730\u4F1A\u8BDD\u4E0E\u90E8\u7F72\u51ED\u636E\u88AB\u63D0\u4EA4\u3002`
743
+ );
744
+ }
745
+ }
746
+ async function ensureDirExists(dir) {
747
+ mkdirSync3(dir, { recursive: true });
748
+ }
749
+ async function ensureWorkspaceApmDirForInit(cwd = resolveWorkdirPath()) {
750
+ const dir = workspaceApmDir(cwd);
751
+ const fsDir = toFsPath(dir);
752
+ if (!existsSync2(fsDir)) {
753
+ mkdirSync3(fsDir, { recursive: true });
754
+ return;
755
+ }
756
+ const st = statSync(fsDir);
757
+ if (!st.isDirectory()) {
758
+ throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${dir}`);
759
+ }
760
+ if (readdirSync(fsDir).length > 0) {
761
+ throw new Error(
762
+ "[apm] .apm \u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A\uFF0C\u8BF7\u5148\u5907\u4EFD\u3001\u6E05\u7A7A\u6216\u5220\u9664\u540E\u518D\u6267\u884C init"
763
+ );
764
+ }
765
+ }
766
+ var WORKSPACE_TEMPLATE_SUBDIRS = [
767
+ "sessions",
768
+ "skills",
769
+ "rules",
770
+ "deploy"
771
+ ];
772
+ function shouldSkipTemplateEntry(name) {
773
+ return name === ".DS_Store" || name === "Thumbs.db";
774
+ }
775
+ function copyTemplateEntry(src, dest) {
776
+ const fsSrc = toFsPath(src);
777
+ const fsDest = toFsPath(dest);
778
+ const st = statSync(fsSrc);
779
+ if (st.isDirectory()) {
780
+ mkdirSync3(fsDest, { recursive: true });
781
+ for (const name of readdirSync(fsSrc)) {
782
+ if (shouldSkipTemplateEntry(name)) continue;
783
+ copyTemplateEntry(join2(src, name), join2(dest, name));
784
+ }
785
+ return;
786
+ }
787
+ if (!st.isFile()) return;
788
+ mkdirSync3(toFsPath(dirname2(dest)), { recursive: true });
789
+ copyFileSync(fsSrc, fsDest);
790
+ }
791
+ function assertTemplateCopiedToApm(apmDir, workdir) {
792
+ const required = [
793
+ "AGENTS.md",
794
+ "apm.config.json",
795
+ "rules",
796
+ "skills",
797
+ "sessions"
798
+ ];
799
+ for (const item of required) {
800
+ const path = join2(apmDir, item);
801
+ if (!existsSync2(toFsPath(path))) {
802
+ throw new Error(`[apm] \u521D\u59CB\u5316\u4E0D\u5B8C\u6574\uFF0C\u7F3A\u5C11: ${path}`);
803
+ }
804
+ }
805
+ const leakedRules = join2(workdir, "rules");
806
+ const apmRules = join2(apmDir, "rules");
807
+ if (existsSync2(toFsPath(leakedRules)) && !existsSync2(toFsPath(join2(apmRules, "reply.md")))) {
808
+ throw new Error(
809
+ `[apm] \u6A21\u677F\u88AB\u590D\u5236\u5230\u9519\u8BEF\u4F4D\u7F6E: ${leakedRules}\uFF08\u5E94\u5728 ${apmRules}\uFF09`
810
+ );
811
+ }
812
+ }
813
+ async function copyTemplateFiles(targetDir, workdir = resolveWorkdirPath()) {
814
+ const resolvedTarget = resolve3(targetDir);
815
+ const templateDir = resolve3(CLI_TEMPLATE_DIR);
816
+ const fsTemplateDir = toFsPath(templateDir);
817
+ if (!existsSync2(fsTemplateDir)) {
818
+ throw new Error(`[apm] \u672A\u627E\u5230 CLI \u6A21\u677F\u76EE\u5F55: ${templateDir}`);
819
+ }
820
+ const dirStat = statSync(fsTemplateDir);
821
+ if (!dirStat.isDirectory()) {
822
+ throw new Error(`[apm] CLI \u6A21\u677F\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55: ${templateDir}`);
823
+ }
824
+ const entries = readdirSync(fsTemplateDir).filter(
825
+ (name) => !shouldSkipTemplateEntry(name)
826
+ );
827
+ if (entries.length === 0) {
828
+ throw new Error(`[apm] CLI \u6A21\u677F\u76EE\u5F55\u4E3A\u7A7A: ${templateDir}`);
829
+ }
830
+ mkdirSync3(toFsPath(resolvedTarget), { recursive: true });
831
+ for (const name of entries) {
832
+ copyTemplateEntry(join2(templateDir, name), join2(resolvedTarget, name));
833
+ }
834
+ for (const subdir of WORKSPACE_TEMPLATE_SUBDIRS) {
835
+ mkdirSync3(toFsPath(join2(resolvedTarget, subdir)), { recursive: true });
836
+ }
837
+ assertTemplateCopiedToApm(resolvedTarget, resolve3(workdir));
838
+ }
839
+
840
+ // src/commands/append-message.ts
841
+ async function appendMessageContent(cfg, messageId, content) {
842
+ const trimmedId = messageId.trim();
843
+ if (!trimmedId) {
844
+ throw new Error("messageId \u4E0D\u80FD\u4E3A\u7A7A");
845
+ }
846
+ if (!content) {
847
+ throw new Error("content \u4E0D\u80FD\u4E3A\u7A7A");
848
+ }
849
+ const api = createApmApiClient(cfg);
850
+ await api.cli.appendMessageContent({ id: trimmedId, content });
851
+ }
852
+
853
+ // src/commands/connect/append-message-tool.ts
854
+ function createAppendMessageCustomTools(cfg, messageId, appendContent2) {
855
+ const doAppend = appendContent2 ?? ((content) => appendMessageContent(cfg, messageId, content));
856
+ return {
857
+ append_message: {
858
+ description: "\u5411\u5F53\u524D\u4F1A\u8BDD\u6D88\u606F\u8FFD\u52A0\u56DE\u590D\u5185\u5BB9\u3002\u53EF\u591A\u6B21\u8C03\u7528\u8865\u5145\u8FDB\u5C55\uFF1B\u88AB @ \u65F6\u6536\u5230\u540E\u5E94\u5148\u7B80\u77ED\u786E\u8BA4\u518D\u6267\u884C\u4EFB\u52A1\u3002",
859
+ inputSchema: {
860
+ type: "object",
861
+ properties: {
862
+ content: {
863
+ type: "string",
864
+ description: "\u8981\u53D1\u9001\u5230\u7FA4\u91CC\u7684\u56DE\u590D\u5185\u5BB9"
865
+ }
866
+ },
867
+ required: ["content"]
868
+ },
869
+ execute: async (args) => {
870
+ const content = typeof args.content === "string" ? args.content.trim() : "";
871
+ if (!content) {
872
+ return {
873
+ content: [{ type: "text", text: "content \u4E0D\u80FD\u4E3A\u7A7A" }],
874
+ isError: true
875
+ };
876
+ }
877
+ try {
878
+ await doAppend(content);
879
+ console.log(`[apm] append_message \u5DF2\u8FFD\u52A0: messageId=${messageId}`);
880
+ return "\u5DF2\u8FFD\u52A0\u6D88\u606F\u5185\u5BB9";
881
+ } catch (err) {
882
+ const detail = err instanceof Error ? err.message : String(err);
883
+ return {
884
+ content: [{ type: "text", text: `\u8FFD\u52A0\u6D88\u606F\u5931\u8D25: ${detail}` }],
885
+ isError: true
886
+ };
887
+ }
888
+ }
889
+ }
890
+ };
891
+ }
892
+
893
+ // src/commands/connect/ask-question-tool.ts
894
+ var INPUT_SCHEMA = {
895
+ type: "object",
896
+ properties: {
897
+ title: {
898
+ type: "string",
899
+ description: "Optional title for the questions form"
900
+ },
901
+ questions: {
902
+ type: "array",
903
+ minItems: 1,
904
+ items: {
905
+ type: "object",
906
+ properties: {
907
+ id: { type: "string" },
908
+ prompt: { type: "string" },
909
+ allow_multiple: { type: "boolean" },
910
+ options: {
911
+ type: "array",
912
+ minItems: 2,
913
+ items: {
914
+ type: "object",
915
+ properties: {
916
+ id: { type: "string" },
917
+ label: { type: "string" }
918
+ },
919
+ required: ["id", "label"]
920
+ }
921
+ }
922
+ },
923
+ required: ["id", "prompt", "options"]
924
+ }
925
+ }
926
+ },
927
+ required: ["questions"]
928
+ };
929
+ function createAskQuestionTool(options) {
930
+ return {
931
+ description: "Collect structured multiple-choice answers from the user. Use when blocked on a decision that is genuinely the user's to make.",
932
+ inputSchema: INPUT_SCHEMA,
933
+ execute: async (args) => {
934
+ options?.onInvoke?.(args);
935
+ if (options?.execute) {
936
+ return options.execute(args);
937
+ }
938
+ const payload = JSON.stringify(args, null, 2);
939
+ console.log(`[apm] AskQuestion\uFF08\u672C\u5730\u5360\u4F4D\uFF0C\u672A\u7B49\u5F85\u7528\u6237\uFF09:
940
+ ${payload}`);
941
+ return `[local] AskQuestion \u5DF2\u8BB0\u5F55\uFF08\u672A\u521B\u5EFA\u4EFB\u52A1\u95EE\u9898\u3001\u672A\u7B49\u5F85\u7528\u6237\u56DE\u7B54\uFF09\u3002\u53C2\u6570:
942
+ ${payload}`;
943
+ }
944
+ };
945
+ }
946
+
947
+ // src/commands/connect/cursor-custom-tools.ts
948
+ var PLAN_MODE_ASK_QUESTION_HINT = `[SDK \u73AF\u5883\u8BF4\u660E]
949
+ AskQuestion \u5DF2\u901A\u8FC7 MCP \u670D\u52A1\u5668 custom-user-tools \u6CE8\u518C\uFF0C\u5DE5\u5177\u540D\u4E3A AskQuestion\u3002
950
+ \u9700\u8981\u5411\u7528\u6237\u786E\u8BA4\u65F6\uFF0C\u8BF7\u8C03\u7528 AskQuestion\uFF08\u7ECF CallMcpTool / custom-user-tools\uFF09\uFF0C\u4E0D\u8981\u5047\u8BBE IDE \u5185\u7F6E AskQuestion \u4E0D\u53EF\u7528\u3002
951
+ \u975E\u5FC5\u987B\u7684\u95EE\u9898\u53EF\u8DF3\u8FC7\uFF0C\u76F4\u63A5\u5B8C\u6210 createPlan\u3002`;
952
+ function createCursorCustomTools(cfg, messageId, options) {
953
+ return {
954
+ ...createAppendMessageCustomTools(
955
+ cfg,
956
+ messageId,
957
+ options?.appendMessageContent
958
+ ),
959
+ AskQuestion: createAskQuestionTool({
960
+ onInvoke: options?.onAskQuestion,
961
+ execute: options?.askQuestionExecute
962
+ })
963
+ };
964
+ }
965
+ function withPlanModeToolHint(prompt, mode) {
966
+ if (mode !== "plan") {
967
+ return prompt;
968
+ }
969
+ return `${prompt.trim()}
970
+
971
+ ${PLAN_MODE_ASK_QUESTION_HINT}`;
972
+ }
973
+
974
+ // src/commands/connect/cursor-agent.ts
975
+ setMaxListeners2(100);
976
+ installAbortSignalDebug();
977
+ var noopRemoteLogSync = {
978
+ schedule(_session) {
979
+ },
980
+ async flush(_session) {
981
+ }
982
+ };
983
+ var logCtx = (ctx, agentId) => ({
984
+ sessionId: ctx.sessionId,
985
+ messageId: ctx.messageId,
986
+ agentId
987
+ });
988
+ function formatCursorRunFailure(runId, options) {
989
+ const details = [
990
+ options?.statusError?.trim(),
991
+ options?.resultText?.trim()
992
+ ].filter((value, index, arr) => {
993
+ if (!value) return false;
994
+ return arr.indexOf(value) === index;
995
+ });
996
+ if (details.length === 0) {
997
+ return `Cursor run \u5931\u8D25: ${runId}`;
998
+ }
999
+ return `Cursor run \u5931\u8D25: ${runId} \u2014 ${details.join("\uFF1B")}`;
1000
+ }
1001
+ async function obtainAgent(ctx) {
1002
+ const agentOptions = {
1003
+ apiKey: ctx.apiKey,
1004
+ model: { id: ctx.model || "default" },
1005
+ local: {
1006
+ cwd: ctx.cwd,
1007
+ ...ctx.customTools ? { customTools: ctx.customTools } : {}
1008
+ },
1009
+ ...ctx.mode ? { mode: ctx.mode } : {}
1010
+ // mcpServers: createPlaywrightMcpServers(),
1011
+ };
1012
+ const explicitAgentId = ctx.resumeAgentId?.trim();
1013
+ const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
1014
+ if (savedAgentId) {
1015
+ try {
1016
+ const agent2 = await Agent.resume(savedAgentId, agentOptions);
1017
+ console.log(
1018
+ `[apm] \u590D\u7528 Agent user=${ctx.user} agentId=${savedAgentId}${explicitAgentId ? "\uFF08\u53C2\u6570\u6307\u5B9A\uFF09" : ""}`
1019
+ );
1020
+ if (ctx.user) {
1021
+ saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent2.agentId);
1022
+ }
1023
+ return { agent: agent2, resumed: true };
1024
+ } catch (err) {
1025
+ console.warn(
1026
+ `[apm] \u590D\u7528 Agent \u5931\u8D25\uFF08agentId=${savedAgentId}\uFF09\uFF0C\u56DE\u9000\u4E3A\u65B0\u5EFA:`,
1027
+ err instanceof Error ? err.message : err
1028
+ );
1029
+ if (!explicitAgentId && ctx.user) {
1030
+ clearSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user);
1031
+ }
1032
+ }
1033
+ }
1034
+ const agent = await Agent.create(agentOptions);
1035
+ if (ctx.user) {
1036
+ saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent.agentId);
1037
+ }
1038
+ return { agent, resumed: false };
1039
+ }
1040
+ async function runCursorAgent(cfg, ctx, options) {
1041
+ const signal = options?.signal;
1042
+ logAbortSignalStats(signal, "runCursorAgent:start");
1043
+ if (signal?.aborted) {
1044
+ throw new Error("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u4EFB\u52A1\u4E2D\u65AD");
1045
+ }
1046
+ const apiKey = ctx.apiKey.trim();
1047
+ if (!apiKey) {
1048
+ throw new Error("\u7F3A\u5C11 apiKey\uFF0C\u65E0\u6CD5\u8C03\u7528 Cursor SDK");
1049
+ }
1050
+ const workdir = resolveWorkdirPath(ctx.workdir);
1051
+ const customTools = createCursorCustomTools(cfg, ctx.messageId, {
1052
+ onAskQuestion: options?.onAskQuestion,
1053
+ appendMessageContent: options?.appendMessageContent,
1054
+ askQuestionExecute: options?.askQuestionExecute
1055
+ });
1056
+ const prompt = withPlanModeToolHint(ctx.prompt, ctx.mode);
1057
+ console.log(
1058
+ `[apm] Cursor Agent \u5F00\u59CB messageId=${ctx.messageId} sessionId=${ctx.sessionId} cwd=${workdir}`
1059
+ );
1060
+ const { agent, resumed } = await obtainAgent({
1061
+ apiKey,
1062
+ model: ctx.model,
1063
+ cwd: workdir,
1064
+ workdir,
1065
+ sessionId: ctx.sessionId,
1066
+ user: ctx.user,
1067
+ mode: ctx.mode,
1068
+ resumeAgentId: ctx.resumeAgentId,
1069
+ customTools
1070
+ });
1071
+ const eventSession = new EventSession(prompt);
1072
+ const syncRemoteLog = options?.createRemoteLogSync ? options.createRemoteLogSync(agent.agentId) : options?.skipRemoteLogSync ? noopRemoteLogSync : createThrottledCursorMessageLogSync(
1073
+ cfg,
1074
+ logCtx(ctx, agent.agentId),
1075
+ (err) => {
1076
+ console.warn(
1077
+ "[apm] \u540C\u6B65 Cursor \u6D88\u606F\u65E5\u5FD7\u5931\u8D25:",
1078
+ err instanceof Error ? err.message : err
1079
+ );
1080
+ }
1081
+ );
1082
+ let activeRun;
1083
+ const abortRun = () => {
1084
+ if (!activeRun?.supports("cancel")) return;
1085
+ void activeRun.cancel().catch(() => void 0);
1086
+ };
1087
+ signal?.addEventListener("abort", abortRun, { once: true });
1088
+ logAbortSignalStats(signal, "runCursorAgent:after-addListener");
1089
+ try {
1090
+ const run = await agent.send(prompt, {
1091
+ ...ctx.mode ? { mode: ctx.mode } : {},
1092
+ // mcpServers: createPlaywrightMcpServers(),
1093
+ local: {
1094
+ ...options?.forceSend ? { force: true } : {},
1095
+ customTools
1096
+ }
1097
+ });
1098
+ activeRun = run;
1099
+ logAbortSignalStats(signal, "runCursorAgent:after-send");
1100
+ console.log(`[apm] Cursor run id=${run.id} agentId=${agent.agentId}`);
1101
+ await options?.onRunStarted?.({ agentId: agent.agentId, runId: run.id });
1102
+ let lastRunErrorStatus;
1103
+ for await (const event of run.stream()) {
1104
+ if (signal?.aborted) {
1105
+ abortRun();
1106
+ throw new Error("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u4EFB\u52A1\u4E2D\u65AD");
1107
+ }
1108
+ if (event.type === "status" && event.status === "ERROR") {
1109
+ const message = event.message?.trim();
1110
+ if (message) {
1111
+ lastRunErrorStatus = message;
1112
+ console.error(
1113
+ `[apm] Cursor run status=ERROR runId=${run.id}: ${message}`
1114
+ );
1115
+ }
1116
+ }
1117
+ options?.onStreamEvent?.(event);
1118
+ eventSession.addEvent(event);
1119
+ syncRemoteLog.schedule(eventSession);
1120
+ }
1121
+ await syncRemoteLog.flush(eventSession);
1122
+ const result = await run.wait();
1123
+ if (result.status === "error") {
1124
+ const failureMessage = formatCursorRunFailure(result.id, {
1125
+ statusError: lastRunErrorStatus,
1126
+ resultText: result.result
1127
+ });
1128
+ console.error(`[apm] ${failureMessage}`);
1129
+ if (resumed) {
1130
+ clearSessionAgentId(workdir, ctx.sessionId, ctx.user);
1131
+ }
1132
+ throw new Error(failureMessage);
1133
+ }
1134
+ if (result.status === "cancelled") {
1135
+ throw new Error(`Cursor run \u5DF2\u53D6\u6D88: ${result.id}`);
1136
+ }
1137
+ console.log(`[apm] Cursor Agent \u5B8C\u6210 messageId=${ctx.messageId}`);
1138
+ const artifacts = await agent.listArtifacts().catch(() => []);
1139
+ const artifactDocuments = [];
1140
+ for (const artifact of artifacts) {
1141
+ try {
1142
+ const content = (await agent.downloadArtifact(artifact.path)).toString(
1143
+ "utf8"
1144
+ );
1145
+ artifactDocuments.push({ path: artifact.path, content });
1146
+ } catch (err) {
1147
+ console.warn(
1148
+ `[apm] \u8BFB\u53D6\u4EA7\u7269\u5931\u8D25 path=${artifact.path}:`,
1149
+ err instanceof Error ? err.message : err
1150
+ );
1151
+ }
1152
+ }
1153
+ return {
1154
+ runId: result.id,
1155
+ agentId: agent.agentId,
1156
+ status: result.status,
1157
+ result: result.result,
1158
+ durationMs: result.durationMs,
1159
+ assistantText: eventSession.getAssistantText(),
1160
+ createPlan: eventSession.getCreatePlanContent(),
1161
+ artifacts,
1162
+ artifactDocuments
1163
+ };
1164
+ } catch (err) {
1165
+ if (err instanceof CursorAgentError) {
1166
+ if (resumed) {
1167
+ clearSessionAgentId(workdir, ctx.sessionId, ctx.user);
1168
+ }
1169
+ throw new Error(
1170
+ `Cursor \u542F\u52A8\u5931\u8D25: ${err.message}${err.isRetryable ? "\uFF08\u53EF\u91CD\u8BD5\uFF09" : ""}`
1171
+ );
1172
+ }
1173
+ throw err;
1174
+ } finally {
1175
+ logAbortSignalStats(signal, "runCursorAgent:finally-before-cleanup");
1176
+ signal?.removeEventListener("abort", abortRun);
1177
+ logAbortSignalStats(signal, "runCursorAgent:finally-after-cleanup");
1178
+ await agent[Symbol.asyncDispose]();
1179
+ }
1180
+ }
1181
+
1182
+ // src/commands/connect/webide-agent-registry.ts
1183
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1184
+ import { dirname as dirname3, resolve as resolve4 } from "node:path";
1185
+ function registryPath2(workdir, taskId) {
1186
+ return resolve4(workdir, ".apm", "webide", taskId, "cursor-agent.json");
1187
+ }
1188
+ function readRegistry2(path) {
1189
+ if (!existsSync3(path)) {
1190
+ return {};
1191
+ }
1192
+ try {
1193
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
1194
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1195
+ const agentId = parsed.agentId;
1196
+ if (typeof agentId === "string" && agentId.trim()) {
1197
+ return { agentId: agentId.trim() };
1198
+ }
1199
+ }
1200
+ } catch {
1201
+ }
1202
+ return {};
1203
+ }
1204
+ function writeRegistry2(path, registry) {
1205
+ mkdirSync4(dirname3(path), { recursive: true });
1206
+ writeFileSync4(path, `${JSON.stringify(registry, null, 2)}
1207
+ `, "utf8");
1208
+ }
1209
+ function loadWebIdeAgentId(workdir, taskId) {
1210
+ return readRegistry2(registryPath2(workdir, taskId)).agentId;
1211
+ }
1212
+ function saveWebIdeAgentId(workdir, taskId, agentId) {
1213
+ writeRegistry2(registryPath2(workdir, taskId), { agentId });
1214
+ }
1215
+ function clearWebIdeAgentId(workdir, taskId) {
1216
+ const path = registryPath2(workdir, taskId);
1217
+ if (!existsSync3(path)) return;
1218
+ writeRegistry2(path, {});
1219
+ }
1220
+
1221
+ // src/commands/connect/webide-ask-question.ts
1222
+ import { setTimeout as delay } from "node:timers/promises";
1223
+ var POLL_INTERVAL_MS = 2e3;
1224
+ function asString(value) {
1225
+ return typeof value === "string" ? value.trim() : "";
1226
+ }
1227
+ function parseQuestions(args) {
1228
+ const title = asString(args.title) || void 0;
1229
+ const raw = args.questions;
1230
+ if (!Array.isArray(raw) || raw.length === 0) {
1231
+ throw new Error("AskQuestion \u7F3A\u5C11 questions");
1232
+ }
1233
+ const questions = [];
1234
+ for (const item of raw) {
1235
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
1236
+ const row = item;
1237
+ const id = asString(row.id);
1238
+ const prompt = asString(row.prompt);
1239
+ const optionsRaw = row.options;
1240
+ if (!id || !prompt || !Array.isArray(optionsRaw)) continue;
1241
+ const options = [];
1242
+ for (const opt of optionsRaw) {
1243
+ if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
1244
+ const o = opt;
1245
+ const oid = asString(o.id);
1246
+ const label = asString(o.label);
1247
+ if (oid && label) options.push({ id: oid, label });
1248
+ }
1249
+ if (options.length < 2) {
1250
+ throw new Error(`AskQuestion \u95EE\u9898 ${id} \u81F3\u5C11\u9700\u8981 2 \u4E2A\u9009\u9879`);
1251
+ }
1252
+ questions.push({ id, prompt, options });
1253
+ }
1254
+ if (questions.length === 0) {
1255
+ throw new Error("AskQuestion \u65E0\u6709\u6548\u95EE\u9898");
1256
+ }
1257
+ return { title, questions };
1258
+ }
1259
+ function createWebIdeAskQuestionExecute(options) {
1260
+ const { cfg, taskId, signal } = options;
1261
+ return async (args) => {
1262
+ const parsed = parseQuestions(args);
1263
+ const api = createApmApiClient(cfg);
1264
+ console.log(
1265
+ `[apm] AskQuestion \u521B\u5EFA\u5F85\u786E\u8BA4\u9879 taskId=${taskId} count=${parsed.questions.length}`
1266
+ );
1267
+ await api.cli.webideReplaceAssumptions({
1268
+ taskId,
1269
+ title: parsed.title,
1270
+ questions: parsed.questions
1271
+ });
1272
+ while (true) {
1273
+ if (signal?.aborted) {
1274
+ throw new Error("AskQuestion \u5DF2\u53D6\u6D88");
1275
+ }
1276
+ const list = await api.cli.webideListAssumptions({ taskId });
1277
+ const byId = new Map(
1278
+ list.assumptions.map((a) => [a.questionId, a])
1279
+ );
1280
+ const pending = parsed.questions.filter((q) => {
1281
+ const row = byId.get(q.id);
1282
+ return !row || row.status !== "RESOLVED";
1283
+ });
1284
+ if (pending.length === 0) {
1285
+ const answers = parsed.questions.map((q) => {
1286
+ const row = byId.get(q.id);
1287
+ return {
1288
+ questionId: q.id,
1289
+ prompt: q.prompt,
1290
+ selectedOptionId: row.selectedOptionId,
1291
+ customAnswer: row.customAnswer,
1292
+ resolution: row.resolution
1293
+ };
1294
+ });
1295
+ console.log(
1296
+ `[apm] AskQuestion \u7528\u6237\u5DF2\u786E\u8BA4 taskId=${taskId} count=${answers.length}`
1297
+ );
1298
+ return JSON.stringify(
1299
+ {
1300
+ title: parsed.title,
1301
+ answers
1302
+ },
1303
+ null,
1304
+ 2
1305
+ );
1306
+ }
1307
+ await delay(POLL_INTERVAL_MS);
1308
+ }
1309
+ };
1310
+ }
1311
+
1312
+ // src/commands/deploy/internal/minio.ts
1313
+ import * as Minio from "minio";
1314
+ var MinioClient = class {
1315
+ inner;
1316
+ constructor(opts) {
1317
+ const endPoint = opts.endPoint.replace(/^https?:\/\//i, "").split("/")[0] ?? opts.endPoint;
1318
+ this.inner = new Minio.Client({
1319
+ endPoint,
1320
+ port: opts.port,
1321
+ useSSL: opts.useSSL,
1322
+ accessKey: opts.accessKey,
1323
+ secretKey: opts.secretKey
1324
+ });
1325
+ }
1326
+ async ensureBucket(bucket) {
1327
+ const exists = await this.inner.bucketExists(bucket);
1328
+ if (!exists) {
1329
+ await this.inner.makeBucket(bucket);
1330
+ }
1331
+ }
1332
+ async deleteObjectsByPrefix(bucket, prefix) {
1333
+ const objectsStream = this.inner.listObjectsV2(bucket, prefix, true);
1334
+ const keys = [];
1335
+ await new Promise((resolve5, reject) => {
1336
+ objectsStream.on("data", (obj) => {
1337
+ if (obj.name) {
1338
+ keys.push(obj.name);
1339
+ }
1340
+ });
1341
+ objectsStream.on("error", reject);
1342
+ objectsStream.on("end", resolve5);
1343
+ });
1344
+ const chunkSize = 500;
1345
+ for (let i = 0; i < keys.length; i += chunkSize) {
1346
+ const chunk = keys.slice(i, i + chunkSize);
1347
+ await this.inner.removeObjects(
1348
+ bucket,
1349
+ chunk.map((name) => name)
1350
+ );
1351
+ }
1352
+ }
1353
+ async putObject(bucket, objectKey, body, meta) {
1354
+ await this.inner.putObject(bucket, objectKey, body, body.length, meta);
1355
+ }
1356
+ /** 匿名可读当前桶全部对象(便于静态站点直链) */
1357
+ async setBucketPublicRead(bucket) {
1358
+ const policy = {
1359
+ Version: "2012-10-17",
1360
+ Statement: [
1361
+ {
1362
+ Effect: "Allow",
1363
+ Principal: { AWS: ["*"] },
1364
+ Action: ["s3:GetObject"],
1365
+ Resource: [`arn:aws:s3:::${bucket}/*`]
1366
+ }
1367
+ ]
1368
+ };
1369
+ await this.inner.setBucketPolicy(bucket, JSON.stringify(policy));
1370
+ }
1371
+ };
1372
+
1373
+ // src/commands/deploy/internal/deploy-artifact-minio.ts
1374
+ async function fetchDeployArtifactStorage(api) {
1375
+ return api.cli.getDeployArtifactStorage(void 0);
1376
+ }
1377
+
1378
+ // src/commands/connect/webide-message-log.ts
1379
+ var SYNC_INTERVAL_MS = 2e3;
1380
+ function webIdeEventsObjectPrefix(taskId, messageId) {
1381
+ return `events/webide/${taskId}/${messageId}/`;
1382
+ }
1383
+ async function putDirtyEvents(minio, bucket, prefix, events) {
1384
+ for (const event of events) {
1385
+ const key = `${prefix}${event.index}.json`;
1386
+ const body = Buffer.from(
1387
+ JSON.stringify({
1388
+ index: event.index,
1389
+ type: event.type,
1390
+ data: event.data
1391
+ }),
1392
+ "utf8"
1393
+ );
1394
+ await minio.putObject(bucket, key, body, {
1395
+ "Content-Type": "application/json"
1396
+ });
1397
+ }
1398
+ }
1399
+ async function upsertLogHeader(cfg, ctx, patch) {
1400
+ const api = createApmApiClient(cfg);
1401
+ await api.cli.webideUpsertMessageLog({
1402
+ messageId: ctx.messageId,
1403
+ agentId: ctx.agentId,
1404
+ objectPrefix: webIdeEventsObjectPrefix(ctx.taskId, ctx.messageId),
1405
+ ...patch
1406
+ });
1407
+ }
1408
+ function createThrottledWebIdeMessageLogSync(cfg, ctx, onError) {
1409
+ let lastRunAt = 0;
1410
+ let timer;
1411
+ let latestSession;
1412
+ let syncChain = Promise.resolve();
1413
+ let minio;
1414
+ let bucket = "";
1415
+ const ensureMinio = async () => {
1416
+ if (minio) return;
1417
+ const storage = await fetchDeployArtifactStorage(createApmApiClient(cfg));
1418
+ minio = new MinioClient({
1419
+ endPoint: storage.endpoint,
1420
+ port: storage.port,
1421
+ useSSL: storage.useSsl,
1422
+ accessKey: storage.accessKey,
1423
+ secretKey: storage.secretKey
1424
+ });
1425
+ bucket = storage.bucket;
1426
+ await minio.ensureBucket(bucket);
1427
+ };
1428
+ const syncDirtyEventsOnce = async (session) => {
1429
+ const events = session.getDirtyEvents();
1430
+ if (events.length === 0) return;
1431
+ lastRunAt = Date.now();
1432
+ try {
1433
+ await ensureMinio();
1434
+ const prefix = webIdeEventsObjectPrefix(ctx.taskId, ctx.messageId);
1435
+ await putDirtyEvents(minio, bucket, prefix, events);
1436
+ await upsertLogHeader(cfg, ctx, {
1437
+ eventCount: session.getEventCount()
1438
+ });
1439
+ session.clearDirty(events.map((e) => e.index));
1440
+ } catch (err) {
1441
+ onError(err);
1442
+ }
1443
+ };
1444
+ const drainDirtyEvents = async (session) => {
1445
+ while (session.getDirtyEvents().length > 0) {
1446
+ await syncDirtyEventsOnce(session);
1447
+ }
1448
+ };
1449
+ const enqueueSync = (session) => {
1450
+ syncChain = syncChain.then(() => drainDirtyEvents(session));
1451
+ };
1452
+ return {
1453
+ async markRun(runId, runStatus, lastError) {
1454
+ try {
1455
+ await upsertLogHeader(cfg, ctx, {
1456
+ runId,
1457
+ runStatus,
1458
+ lastError: lastError ?? null
1459
+ });
1460
+ } catch (err) {
1461
+ onError(err);
1462
+ }
1463
+ },
1464
+ schedule(session) {
1465
+ latestSession = session;
1466
+ const now = Date.now();
1467
+ const elapsed = now - lastRunAt;
1468
+ if (elapsed >= SYNC_INTERVAL_MS) {
1469
+ if (timer) {
1470
+ clearTimeout(timer);
1471
+ timer = void 0;
1472
+ }
1473
+ enqueueSync(session);
1474
+ return;
1475
+ }
1476
+ if (timer) return;
1477
+ timer = setTimeout(() => {
1478
+ timer = void 0;
1479
+ if (latestSession) enqueueSync(latestSession);
1480
+ }, SYNC_INTERVAL_MS - elapsed);
1481
+ },
1482
+ async flush(session) {
1483
+ if (timer) {
1484
+ clearTimeout(timer);
1485
+ timer = void 0;
1486
+ }
1487
+ await syncChain;
1488
+ await drainDirtyEvents(session);
1489
+ }
1490
+ };
1491
+ }
1492
+
1493
+ // src/commands/connect/ensure-message-reply.ts
1494
+ var DEFAULT_REPLY = "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u3002";
1495
+ function resolveMessageReplyFallback(fallback) {
1496
+ for (const candidate of [
1497
+ fallback.assistantText,
1498
+ fallback.result,
1499
+ fallback.createPlan
1500
+ ]) {
1501
+ const trimmed = candidate?.trim();
1502
+ if (trimmed) {
1503
+ return trimmed;
1504
+ }
1505
+ }
1506
+ return DEFAULT_REPLY;
1507
+ }
1508
+
1509
+ // src/commands/init.ts
1510
+ import { join as join5 } from "path";
1511
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
1512
+
1513
+ // src/deployment-config-sync.ts
1514
+ import { join as join3 } from "path";
1515
+ import { writeFileSync as writeFileSync5 } from "fs";
1516
+
1517
+ // src/git-remote.ts
1518
+ import { execFile } from "child_process";
1519
+ import { promisify } from "util";
1520
+ var execFileAsync = promisify(execFile);
1521
+ async function tryReadGitOriginUrl(cwd) {
1522
+ try {
1523
+ const { stdout } = await execFileAsync(
1524
+ "git",
1525
+ ["config", "--get", "remote.origin.url"],
1526
+ { cwd, encoding: "utf8", maxBuffer: 1024 * 1024 }
1527
+ );
1528
+ const url = stdout.trim();
1529
+ return url || null;
1530
+ } catch {
1531
+ return null;
1532
+ }
1533
+ }
1534
+
1535
+ // src/git-utils.ts
1536
+ import { execFile as execFile2 } from "child_process";
1537
+ import { promisify as promisify2 } from "util";
1538
+ var execFileAsync2 = promisify2(execFile2);
1539
+ async function execGit(cwd, args, quiet = false) {
1540
+ try {
1541
+ const { stdout, stderr } = await execFileAsync2("git", args, {
1542
+ cwd,
1543
+ encoding: "utf8",
1544
+ maxBuffer: 10 * 1024 * 1024
1545
+ });
1546
+ if (!quiet && stderr.trim()) {
1547
+ process.stderr.write(stderr);
1548
+ }
1549
+ return stdout;
1550
+ } catch (err) {
1551
+ const e = err;
1552
+ const detail = (e.stderr ?? e.message ?? String(err)).trim();
1553
+ throw new Error(
1554
+ `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
1555
+ );
1556
+ }
1557
+ }
1558
+ async function isGitRepo(cwd) {
1559
+ try {
1560
+ await execGit(cwd, ["rev-parse", "--git-dir"], true);
1561
+ return true;
1562
+ } catch {
1563
+ return false;
1564
+ }
1565
+ }
1566
+ async function resolveGitRepoRoot(cwd) {
1567
+ return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
1568
+ }
1569
+ async function hasUpstream(cwd) {
1570
+ try {
1571
+ await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
1572
+ return true;
1573
+ } catch {
1574
+ return false;
1575
+ }
1576
+ }
1577
+ var GITIGNORE_COMMIT_MESSAGE = "chore(apm): ignore .apm directory";
1578
+ async function commitAndPushGitignore(workdir) {
1579
+ if (!await isGitRepo(workdir)) {
1580
+ console.log("[apm] \u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u8BF7\u624B\u52A8\u63D0\u4EA4 .gitignore");
1581
+ return;
1582
+ }
1583
+ await execGit(workdir, ["add", "--", ".gitignore"]);
1584
+ await execGit(workdir, ["commit", "-m", GITIGNORE_COMMIT_MESSAGE]);
1585
+ console.log(`[apm] \u5DF2\u63D0\u4EA4 .gitignore: ${GITIGNORE_COMMIT_MESSAGE}`);
1586
+ const originUrl = await tryReadGitOriginUrl(workdir);
1587
+ if (!originUrl) {
1588
+ console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8 push .gitignore");
1589
+ return;
1590
+ }
1591
+ if (await hasUpstream(workdir)) {
1592
+ await execGit(workdir, ["push"]);
1593
+ } else {
1594
+ await execGit(workdir, ["push", "-u", "origin", "HEAD"]);
1595
+ }
1596
+ console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
1597
+ }
1598
+
1599
+ // src/baseline-resolve.ts
1600
+ function formatBaselineDiagnostic(workdirPath, baselineWorkdirPath, diagnostic) {
1601
+ return diagnostic?.message ?? `\u672A\u5728\u5E73\u53F0\u627E\u5230\u4E0E\u5F53\u524D\u76EE\u5F55\u5339\u914D\u7684\u5DE5\u4F5C\u76EE\u5F55\u767B\u8BB0\uFF1A${workdirPath}\uFF08\u89C4\u8303\u5316\uFF1A${baselineWorkdirPath}\uFF09
1602
+ \u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
1603
+ }
1604
+ async function matchRepositoryByGitRemote(api, workdirPath) {
1605
+ const gitRoot = await resolveGitRepoRoot(workdirPath);
1606
+ const gitUrl = await tryReadGitOriginUrl(gitRoot);
1607
+ if (!gitUrl) {
1608
+ return null;
1609
+ }
1610
+ const matched = await api.cli.matchRepository({ url: gitUrl });
1611
+ const repositoryId = matched.repositoryId?.trim();
1612
+ const defaultBranch = matched.defaultBranch?.trim();
1613
+ if (!repositoryId || !defaultBranch) {
1614
+ return null;
1615
+ }
1616
+ console.log(
1617
+ `[apm] \u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u672A\u5339\u914D\uFF0C\u5DF2\u901A\u8FC7 git remote \u5173\u8054\u4ED3\u5E93: ${gitUrl}`
1618
+ );
1619
+ return { repositoryId, defaultBranch };
1620
+ }
1621
+ async function resolveWorkspaceBaseline(api, workdirPath) {
1622
+ const baseline = await api.cli.workspaceBaseline({ workdirPath });
1623
+ const repositoryId = baseline.repositoryId?.trim();
1624
+ const defaultBranch = baseline.defaultBranch?.trim();
1625
+ if (repositoryId && defaultBranch) {
1626
+ return {
1627
+ repositoryId,
1628
+ defaultBranch,
1629
+ workdirPath: baseline.workdirPath,
1630
+ matchedViaGitRemote: false
1631
+ };
1632
+ }
1633
+ const viaGit = await matchRepositoryByGitRemote(api, workdirPath);
1634
+ if (viaGit) {
1635
+ return {
1636
+ ...viaGit,
1637
+ workdirPath: baseline.workdirPath,
1638
+ matchedViaGitRemote: true
1639
+ };
1640
+ }
1641
+ throw new Error(
1642
+ `[apm] ${formatBaselineDiagnostic(
1643
+ workdirPath,
1644
+ baseline.workdirPath,
1645
+ baseline.diagnostic
1646
+ )}`
1647
+ );
1648
+ }
1649
+
1650
+ // src/deployment-config-sync.ts
1651
+ var TEMPLATE_HINT = "\u4FDD\u7559\u6A21\u677F .apm/apm.config.json";
1652
+ var SYNC_HINT = "\u767B\u8BB0\u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u3001\u7ED1\u5B9A\u4ED3\u5E93\u540E\uFF0C\u53EF\u6267\u884C: apm sync-deploy-config";
1653
+ async function resolveRepositoryIdForSync(api, workdirPath) {
1654
+ try {
1655
+ const baseline = await resolveWorkspaceBaseline(api, workdirPath);
1656
+ return { repositoryId: baseline.repositoryId, diagnostic: null };
1657
+ } catch (err) {
1658
+ const detail = err instanceof Error ? err.message : String(err);
1659
+ return {
1660
+ repositoryId: null,
1661
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
1662
+ };
1663
+ }
1664
+ }
1665
+ async function syncRemoteDeploymentConfig(workdirPath, apmDir) {
1666
+ const cfg = await tryReadApmConfig();
1667
+ if (!cfg || !resolveApiKey(cfg)) {
1668
+ console.log(
1669
+ `[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u5E73\u53F0\u90E8\u7F72\u914D\u7F6E\u540C\u6B65\uFF08${TEMPLATE_HINT}\uFF09\u3002
1670
+ [apm] \u8BF7\u5148\u6267\u884C apm login\uFF0C\u518D\u6267\u884C apm sync-deploy-config \u62C9\u53D6\u6700\u65B0\u914D\u7F6E\u3002`
1671
+ );
1672
+ return { synced: false, repositoryId: null };
1673
+ }
1674
+ const api = createApmApiClient(cfg);
1675
+ const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
1676
+ api,
1677
+ workdirPath
1678
+ );
1679
+ if (!repositoryId) {
1680
+ console.log(
1681
+ `[apm] \u672A\u80FD\u540C\u6B65\u5E73\u53F0\u90E8\u7F72\u914D\u7F6E\uFF08${TEMPLATE_HINT}\uFF09\u3002
1682
+ ${diagnostic ?? ""}
1683
+ [apm] ${SYNC_HINT}`
1684
+ );
1685
+ return { synced: false, repositoryId: null };
1686
+ }
1687
+ const { config } = await api.cli.getDeploymentConfiguration({ repositoryId });
1688
+ if (!config) {
1689
+ console.log(
1690
+ `[apm] \u672A\u627E\u5230\u5173\u8054\u4ED3\u5E93\u7684\u90E8\u7F72\u914D\u7F6E\uFF08${TEMPLATE_HINT}\uFF0CrepositoryId\uFF1A${repositoryId}\uFF09\u3002
1691
+ [apm] \u8BF7\u5728\u5E73\u53F0\u300C\u90E8\u7F72\u914D\u7F6E\u300D\u4E2D\u521B\u5EFA\u914D\u7F6E\u5E76\u5173\u8054\u8BE5\u4ED3\u5E93\uFF0C\u7136\u540E\u6267\u884C: apm sync-deploy-config`
1692
+ );
1693
+ return { synced: false, repositoryId };
1694
+ }
1695
+ let parsed;
1696
+ try {
1697
+ parsed = JSON.parse(config.content);
1698
+ } catch {
1699
+ console.warn(
1700
+ `[apm] \u8FDC\u7A0B\u90E8\u7F72\u914D\u7F6E\u300C${config.name}\u300DJSON \u65E0\u6548\uFF08${TEMPLATE_HINT}\uFF09`
1701
+ );
1702
+ return { synced: false, repositoryId };
1703
+ }
1704
+ const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1705
+ const apmConfigPath = toFsPath(join3(targetApmDir, "apm.config.json"));
1706
+ writeFileSync5(apmConfigPath, `${JSON.stringify(parsed, null, 2)}
1707
+ `, "utf8");
1708
+ console.log(`[apm] \u5DF2\u540C\u6B65\u5E73\u53F0\u90E8\u7F72\u914D\u7F6E: ${config.name}`);
1709
+ console.log("[apm] \u5DF2\u5199\u5165 .apm/apm.config.json");
1710
+ return { synced: true, repositoryId, configName: config.name };
1711
+ }
1712
+
1713
+ // src/repository-project-documents-sync.ts
1714
+ import {
1715
+ existsSync as existsSync4,
1716
+ readdirSync as readdirSync2,
1717
+ readFileSync as readFileSync5,
1718
+ rmSync,
1719
+ writeFileSync as writeFileSync6
1720
+ } from "fs";
1721
+ import { dirname as dirname4, join as join4, relative, sep } from "path";
1722
+ var MANIFEST_FILE = "manifest.json";
1723
+ function projectDocumentsDir(apmRoot) {
1724
+ return join4(apmRoot ?? workspaceApmDir(), "project");
1725
+ }
1726
+ function projectDocumentLocalPath(apmRoot, documentPath) {
1727
+ const normalized = normalizeLocalDocumentPath(documentPath);
1728
+ return join4(projectDocumentsDir(apmRoot), ...normalized.split("/"));
1729
+ }
1730
+ function normalizeLocalDocumentPath(path) {
1731
+ const trimmed = path.trim().replace(/\\/g, "/");
1732
+ if (!trimmed || trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) {
1733
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path}`);
1734
+ }
1735
+ const segments = trimmed.split("/").filter(Boolean);
1736
+ if (segments.some((segment) => segment === ".." || segment === ".")) {
1737
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path}`);
1738
+ }
1739
+ return segments.join("/");
1740
+ }
1741
+ function readLocalManifest(apmRoot) {
1742
+ const manifestPath = join4(projectDocumentsDir(apmRoot), MANIFEST_FILE);
1743
+ if (!existsSync4(manifestPath)) {
1744
+ return null;
1745
+ }
1746
+ try {
1747
+ return JSON.parse(
1748
+ readFileSync5(manifestPath, "utf8")
1749
+ );
1750
+ } catch {
1751
+ return null;
1752
+ }
1753
+ }
1754
+ function diffManifestPaths(remote, local) {
1755
+ const remoteMap = new Map(
1756
+ (remote?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1757
+ );
1758
+ const localMap = new Map(
1759
+ (local?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1760
+ );
1761
+ const download = [];
1762
+ for (const [path, hash] of remoteMap) {
1763
+ if (localMap.get(path) !== hash) {
1764
+ download.push(path);
1765
+ }
1766
+ }
1767
+ const deleteLocal = [];
1768
+ for (const path of localMap.keys()) {
1769
+ if (!remoteMap.has(path)) {
1770
+ deleteLocal.push(path);
1771
+ }
1772
+ }
1773
+ return { download, deleteLocal };
1774
+ }
1775
+ async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
1776
+ const empty = {
1777
+ synced: false,
1778
+ repositoryId: null,
1779
+ downloaded: 0,
1780
+ deleted: 0
1781
+ };
1782
+ const cfg = await tryReadApmConfig();
1783
+ if (!cfg || !resolveApiKey(cfg)) {
1784
+ console.log(
1785
+ "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
1786
+ );
1787
+ return empty;
1788
+ }
1789
+ const api = createApmApiClient(cfg);
1790
+ const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
1791
+ api,
1792
+ workdirPath
1793
+ );
1794
+ if (!repositoryId) {
1795
+ console.log(
1796
+ `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
1797
+ ${diagnostic ?? ""}`
1798
+ );
1799
+ return empty;
1800
+ }
1801
+ const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1802
+ const projectDir = projectDocumentsDir(targetApmDir);
1803
+ await ensureDirExists(projectDir);
1804
+ const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
1805
+ if (!remoteManifest) {
1806
+ console.log(
1807
+ `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
1808
+ );
1809
+ return { ...empty, repositoryId };
1810
+ }
1811
+ const localManifest = readLocalManifest(targetApmDir);
1812
+ const { download, deleteLocal } = diffManifestPaths(
1813
+ remoteManifest,
1814
+ localManifest
1815
+ );
1816
+ let downloaded = 0;
1817
+ if (download.length > 0) {
1818
+ const { list } = await api.cli.listRepositoryProjectDocuments({
1819
+ repositoryId,
1820
+ paths: download.join(",")
1821
+ });
1822
+ for (const doc of list) {
1823
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
1824
+ await ensureDirExists(dirname4(absPath));
1825
+ writeFileSync6(absPath, doc.content, "utf8");
1826
+ downloaded += 1;
1827
+ }
1828
+ }
1829
+ let deleted = 0;
1830
+ for (const path of deleteLocal) {
1831
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path));
1832
+ if (existsSync4(absPath)) {
1833
+ rmSync(absPath, { force: true });
1834
+ deleted += 1;
1835
+ }
1836
+ }
1837
+ writeFileSync6(
1838
+ toFsPath(join4(projectDir, MANIFEST_FILE)),
1839
+ `${JSON.stringify(remoteManifest, null, 2)}
1840
+ `,
1841
+ "utf8"
1842
+ );
1843
+ console.log(
1844
+ `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1845
+ );
1846
+ return {
1847
+ synced: true,
1848
+ repositoryId,
1849
+ downloaded,
1850
+ deleted
1851
+ };
1852
+ }
1853
+
1854
+ // src/commands/init.ts
1855
+ async function ensureWorkspaceInitialized(workdir, options) {
1856
+ if (isWorkspaceApmInitialized(workdir)) {
1857
+ return { didInit: false };
1858
+ }
1859
+ console.log(
1860
+ `[apm] \u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u672A\u68C0\u6D4B\u5230\u5DF2\u521D\u59CB\u5316\u7684 .apm\uFF0C\u6B63\u5728\u81EA\u52A8\u521D\u59CB\u5316\u2026`
1861
+ );
1862
+ await ensureWorkspaceApmDirForInit(workdir);
1863
+ if (ensureApmGitignoredInRepo(workdir)) {
1864
+ console.log("[apm] \u5DF2\u5728 .gitignore \u4E2D\u6DFB\u52A0 **/.apm/**");
1865
+ await commitAndPushGitignore(workdir);
1866
+ }
1867
+ const apmDir = workspaceApmDir(workdir);
1868
+ await copyTemplateFiles(apmDir, workdir);
1869
+ const syncResult = await syncRemoteDeploymentConfig(workdir, apmDir);
1870
+ await syncRepositoryProjectDocumentsPull(workdir, apmDir);
1871
+ const trimmedName = options?.name?.trim();
1872
+ if (trimmedName) {
1873
+ const apmConfigPath = toFsPath(join5(apmDir, "apm.config.json"));
1874
+ const config = readFileSync6(apmConfigPath, "utf8");
1875
+ const configJson = JSON.parse(config);
1876
+ configJson.name = trimmedName;
1877
+ writeFileSync7(
1878
+ apmConfigPath,
1879
+ `${JSON.stringify(configJson, null, 2)}
1880
+ `,
1881
+ "utf8"
1882
+ );
1883
+ }
1884
+ console.log(`[apm] \u5DF2\u521D\u59CB\u5316\u5DE5\u4F5C\u533A\uFF1A${apmDir}`);
1885
+ return { didInit: true, syncResult };
1886
+ }
1887
+
1888
+ // src/commands/connect/handle-webide-message.ts
1889
+ async function updateStatus(cfg, messageId, status) {
1890
+ const api = createApmApiClient(cfg);
1891
+ await api.cli.webideUpdateMessageStatus({ id: messageId, status });
1892
+ }
1893
+ async function setError(cfg, messageId, error) {
1894
+ const api = createApmApiClient(cfg);
1895
+ await api.cli.webideSetMessageError({ id: messageId, error });
1896
+ }
1897
+ async function appendContent(cfg, messageId, content) {
1898
+ const api = createApmApiClient(cfg);
1899
+ await api.cli.webideAppendMessageContent({ id: messageId, content });
1900
+ }
1901
+ async function handleWebIdeInboundMessage(cfg, msg, signal) {
1902
+ const workdir = requireRemoteWorkdir(msg.workdir);
1903
+ const messageId = msg.messageId;
1904
+ const taskId = msg.taskId;
1905
+ console.log(
1906
+ `[apm] webide-message action=${msg.action} taskId=${taskId} messageId=${messageId}`
1907
+ );
1908
+ await updateStatus(cfg, messageId, "TYPING");
1909
+ try {
1910
+ if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
1911
+ const { didInit } = await ensureWorkspaceInitialized(workdir);
1912
+ if (!didInit) {
1913
+ assertApmGitignoredInRepo(workdir);
1914
+ }
1915
+ const savedAgentId = loadWebIdeAgentId(workdir, taskId);
1916
+ const logSyncRef = { current: null };
1917
+ const outcome = await runCursorAgent(
1918
+ cfg,
1919
+ {
1920
+ messageId,
1921
+ sessionId: `webide:${taskId}`,
1922
+ prompt: msg.content,
1923
+ model: msg.model,
1924
+ apiKey: msg.apiKey,
1925
+ workdir,
1926
+ user: msg.user || "webide",
1927
+ resumeAgentId: savedAgentId
1928
+ },
1929
+ {
1930
+ signal,
1931
+ appendMessageContent: (content) => appendContent(cfg, messageId, content),
1932
+ askQuestionExecute: createWebIdeAskQuestionExecute({
1933
+ cfg,
1934
+ taskId,
1935
+ signal
1936
+ }),
1937
+ createRemoteLogSync: (agentId) => {
1938
+ saveWebIdeAgentId(workdir, taskId, agentId);
1939
+ logSyncRef.current = createThrottledWebIdeMessageLogSync(
1940
+ cfg,
1941
+ { taskId, messageId, agentId },
1942
+ (err) => {
1943
+ console.warn(
1944
+ "[apm] WebIDE \u65E5\u5FD7\u540C\u6B65\u5931\u8D25:",
1945
+ err instanceof Error ? err.message : err
1946
+ );
1947
+ }
1948
+ );
1949
+ return logSyncRef.current;
1950
+ },
1951
+ onRunStarted: async ({ agentId, runId }) => {
1952
+ saveWebIdeAgentId(workdir, taskId, agentId);
1953
+ await logSyncRef.current?.markRun(runId, "running");
1954
+ }
1955
+ }
1956
+ );
1957
+ saveWebIdeAgentId(workdir, taskId, outcome.agentId);
1958
+ if (outcome.status === "error") {
1959
+ const detail = formatCursorRunFailure(outcome.runId, {
1960
+ resultText: outcome.result
1961
+ });
1962
+ await logSyncRef.current?.markRun(outcome.runId, "error", detail);
1963
+ await setError(cfg, messageId, detail);
1964
+ return;
1965
+ }
1966
+ if (outcome.status === "cancelled" || signal.aborted) {
1967
+ await logSyncRef.current?.markRun(outcome.runId, "cancelled", "\u5DF2\u53D6\u6D88");
1968
+ await updateStatus(cfg, messageId, "CANCELLED");
1969
+ return;
1970
+ }
1971
+ const fallback = resolveMessageReplyFallback({
1972
+ assistantText: outcome.assistantText,
1973
+ result: outcome.result,
1974
+ createPlan: outcome.createPlan
1975
+ });
1976
+ const api = createApmApiClient(cfg);
1977
+ await api.cli.webideEnsureMessageContent({
1978
+ id: messageId,
1979
+ content: fallback
1980
+ });
1981
+ await logSyncRef.current?.markRun(outcome.runId, "finished");
1982
+ await updateStatus(cfg, messageId, "SUCCESS");
1983
+ console.log(
1984
+ `[apm] webide-message \u5B8C\u6210 action=${msg.action} messageId=${messageId} agentId=${outcome.agentId}`
1985
+ );
1986
+ } catch (err) {
1987
+ const detail = err instanceof Error ? err.message : String(err);
1988
+ console.error(`[apm] webide-message \u5931\u8D25: ${detail}`);
1989
+ if (detail.includes("\u590D\u7528 Agent") || detail.includes("resume")) {
1990
+ clearWebIdeAgentId(workdir, taskId);
1991
+ }
1992
+ try {
1993
+ await setError(cfg, messageId, detail);
1994
+ } catch (statusErr) {
1995
+ console.error(
1996
+ "[apm] \u5199\u5165 WebIDE FAILED \u5931\u8D25:",
1997
+ statusErr instanceof Error ? statusErr.message : statusErr
1998
+ );
1999
+ }
2000
+ }
2001
+ }
2002
+
2003
+ // src/commands/connect/webide-message-worker.ts
2004
+ var controllers = /* @__PURE__ */ new Map();
2005
+ async function runJob(cfg, msg) {
2006
+ const controller = new AbortController();
2007
+ controllers.set(msg.messageId, controller);
2008
+ try {
2009
+ await handleWebIdeInboundMessage(cfg, msg, controller.signal);
2010
+ parentPort?.postMessage({
2011
+ type: "done",
2012
+ messageId: msg.messageId
2013
+ });
2014
+ } catch (err) {
2015
+ parentPort?.postMessage({
2016
+ type: "error",
2017
+ messageId: msg.messageId,
2018
+ error: err instanceof Error ? err.message : String(err)
2019
+ });
2020
+ } finally {
2021
+ controllers.delete(msg.messageId);
2022
+ }
2023
+ }
2024
+ parentPort?.on("message", (raw) => {
2025
+ if (raw.type === "cancel") {
2026
+ controllers.get(raw.messageId)?.abort();
2027
+ return;
2028
+ }
2029
+ if (raw.type === "run") {
2030
+ void runJob(raw.cfg, raw.msg);
2031
+ }
2032
+ });
2033
+ var boot = workerData;
2034
+ if (boot?.cfg && boot?.msg) {
2035
+ void runJob(boot.cfg, boot.msg);
2036
+ }