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.
- package/README.md +31 -3
- package/dist/index.js +7601 -3971
- package/dist/webide-message-worker.js +2036 -0
- package/package.json +4 -2
- package/template/AGENTS.md +28 -25
- package/template/apm.config.json +1 -5
- package/template/project/.gitkeep +0 -0
- package/template/rules/reply.md +8 -8
- package/template/rules/write_doc.md +17 -5
- package/template/sessions/.gitkeep +0 -0
- package/template/skills/apm-apply-change/SKILL.md +116 -0
- package/template/skills/apm-confirm-assumptions/SKILL.md +3 -0
- package/template/skills/apm-dev/SKILL.md +80 -0
- package/template/skills/apm-diff-review/SKILL.md +12 -13
- package/template/skills/apm-propose/SKILL.md +52 -0
- package/template/skills/apm-propose/design.md +37 -0
- package/template/skills/apm-propose/proposal.md +31 -0
- package/template/skills/apm-propose/specs.md +39 -0
- package/template/skills/apm-propose/tasks.md +16 -0
- package/template/skills/apm-recap/SKILL.md +77 -0
- package/template/skills/apm-recap/recap-template.md +121 -0
- package/template/skills/apm-update-plan/SKILL.md +26 -0
- package/template/skills/apm-write-assumptions/SKILL.md +78 -0
- package/template/skills/apm-write-assumptions/assumptions-template.md +18 -0
- package/template/skills/apm-write-checklist/SKILL.md +5 -5
- package/template/skills/apm-write-frontend-plan/SKILL.md +27 -0
- package/template/skills/apm-write-frontend-plan/plan-template.md +40 -0
- package/template/skills/apm-write-plan/SKILL.md +51 -0
- package/template/skills/apm-write-plan/api-template.md +35 -0
- package/template/skills/apm-write-plan/plan-template.md +72 -0
- package/template/skills/apm-write-prd/SKILL.md +14 -0
- 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
|
+
}
|