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