ai-project-manage-cli 6.0.63 → 6.0.64
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/dist/index.js +545 -79
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -484,6 +484,22 @@ var requestConfig = {
|
|
|
484
484
|
completeCoordinatorDeployment: defineEndpoint({
|
|
485
485
|
method: "PUT",
|
|
486
486
|
path: "/cli/coordinator-deployments/complete"
|
|
487
|
+
}),
|
|
488
|
+
updateCoordinatorDispatchStatus: defineEndpoint({
|
|
489
|
+
method: "PUT",
|
|
490
|
+
path: "/cli/coordinator-dispatches/status"
|
|
491
|
+
}),
|
|
492
|
+
appendCoordinatorDispatchResponse: defineEndpoint({
|
|
493
|
+
method: "PUT",
|
|
494
|
+
path: "/cli/coordinator-dispatches/append-response"
|
|
495
|
+
}),
|
|
496
|
+
completeCoordinatorDispatch: defineEndpoint({
|
|
497
|
+
method: "PUT",
|
|
498
|
+
path: "/cli/coordinator-dispatches/complete"
|
|
499
|
+
}),
|
|
500
|
+
createCoordinatorDispatchQuestions: defineEndpoint({
|
|
501
|
+
method: "POST",
|
|
502
|
+
path: "/cli/coordinator-dispatches/questions"
|
|
487
503
|
})
|
|
488
504
|
}
|
|
489
505
|
};
|
|
@@ -2152,13 +2168,113 @@ function validateDeployPush(o) {
|
|
|
2152
2168
|
}
|
|
2153
2169
|
};
|
|
2154
2170
|
}
|
|
2171
|
+
function validateCoordinatorDispatchMode(mode) {
|
|
2172
|
+
return mode === "plan" || mode === "agent";
|
|
2173
|
+
}
|
|
2174
|
+
function validateCoordinatorDispatchPush(o) {
|
|
2175
|
+
if (o.type !== "coordinator_dispatch") {
|
|
2176
|
+
return { ok: false, reason: "\u671F\u671B coordinator_dispatch" };
|
|
2177
|
+
}
|
|
2178
|
+
if (!nonEmptyString(o.dispatchId)) {
|
|
2179
|
+
return { ok: false, reason: "coordinator_dispatch \u7F3A\u5C11 dispatchId" };
|
|
2180
|
+
}
|
|
2181
|
+
if (!nonEmptyString(o.dispatchSessionId)) {
|
|
2182
|
+
return { ok: false, reason: "coordinator_dispatch \u7F3A\u5C11 dispatchSessionId" };
|
|
2183
|
+
}
|
|
2184
|
+
if (!validateCoordinatorDispatchMode(o.mode)) {
|
|
2185
|
+
return { ok: false, reason: "coordinator_dispatch.mode \u65E0\u6548" };
|
|
2186
|
+
}
|
|
2187
|
+
if (!nonEmptyString(o.content)) {
|
|
2188
|
+
return { ok: false, reason: "coordinator_dispatch \u7F3A\u5C11 content" };
|
|
2189
|
+
}
|
|
2190
|
+
if (!nonEmptyString(o.model)) {
|
|
2191
|
+
return { ok: false, reason: "coordinator_dispatch \u7F3A\u5C11 model" };
|
|
2192
|
+
}
|
|
2193
|
+
if (!nonEmptyString(o.apiKey)) {
|
|
2194
|
+
return { ok: false, reason: "coordinator_dispatch \u7F3A\u5C11 apiKey" };
|
|
2195
|
+
}
|
|
2196
|
+
if (!nonEmptyString(o.workdir)) {
|
|
2197
|
+
return { ok: false, reason: "coordinator_dispatch \u7F3A\u5C11 workdir" };
|
|
2198
|
+
}
|
|
2199
|
+
if (!nonEmptyString(o.user)) {
|
|
2200
|
+
return { ok: false, reason: "coordinator_dispatch \u7F3A\u5C11 user" };
|
|
2201
|
+
}
|
|
2202
|
+
const data = {
|
|
2203
|
+
type: "coordinator_dispatch",
|
|
2204
|
+
dispatchId: o.dispatchId.trim(),
|
|
2205
|
+
dispatchSessionId: o.dispatchSessionId.trim(),
|
|
2206
|
+
mode: o.mode,
|
|
2207
|
+
content: o.content,
|
|
2208
|
+
model: o.model.trim(),
|
|
2209
|
+
apiKey: o.apiKey.trim(),
|
|
2210
|
+
workdir: o.workdir.trim(),
|
|
2211
|
+
user: o.user.trim()
|
|
2212
|
+
};
|
|
2213
|
+
if (nonEmptyString(o.resumeAgentId)) {
|
|
2214
|
+
data.resumeAgentId = o.resumeAgentId.trim();
|
|
2215
|
+
}
|
|
2216
|
+
return { ok: true, data };
|
|
2217
|
+
}
|
|
2218
|
+
function validateCoordinatorDispatchResumePush(o) {
|
|
2219
|
+
if (o.type !== "coordinator_dispatch_resume") {
|
|
2220
|
+
return { ok: false, reason: "\u671F\u671B coordinator_dispatch_resume" };
|
|
2221
|
+
}
|
|
2222
|
+
if (!nonEmptyString(o.dispatchId)) {
|
|
2223
|
+
return { ok: false, reason: "coordinator_dispatch_resume \u7F3A\u5C11 dispatchId" };
|
|
2224
|
+
}
|
|
2225
|
+
if (!nonEmptyString(o.dispatchSessionId)) {
|
|
2226
|
+
return {
|
|
2227
|
+
ok: false,
|
|
2228
|
+
reason: "coordinator_dispatch_resume \u7F3A\u5C11 dispatchSessionId"
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
if (!validateCoordinatorDispatchMode(o.mode)) {
|
|
2232
|
+
return { ok: false, reason: "coordinator_dispatch_resume.mode \u65E0\u6548" };
|
|
2233
|
+
}
|
|
2234
|
+
if (!nonEmptyString(o.content)) {
|
|
2235
|
+
return { ok: false, reason: "coordinator_dispatch_resume \u7F3A\u5C11 content" };
|
|
2236
|
+
}
|
|
2237
|
+
if (!nonEmptyString(o.resumeAgentId)) {
|
|
2238
|
+
return {
|
|
2239
|
+
ok: false,
|
|
2240
|
+
reason: "coordinator_dispatch_resume \u7F3A\u5C11 resumeAgentId"
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
if (!nonEmptyString(o.model)) {
|
|
2244
|
+
return { ok: false, reason: "coordinator_dispatch_resume \u7F3A\u5C11 model" };
|
|
2245
|
+
}
|
|
2246
|
+
if (!nonEmptyString(o.apiKey)) {
|
|
2247
|
+
return { ok: false, reason: "coordinator_dispatch_resume \u7F3A\u5C11 apiKey" };
|
|
2248
|
+
}
|
|
2249
|
+
if (!nonEmptyString(o.workdir)) {
|
|
2250
|
+
return { ok: false, reason: "coordinator_dispatch_resume \u7F3A\u5C11 workdir" };
|
|
2251
|
+
}
|
|
2252
|
+
if (!nonEmptyString(o.user)) {
|
|
2253
|
+
return { ok: false, reason: "coordinator_dispatch_resume \u7F3A\u5C11 user" };
|
|
2254
|
+
}
|
|
2255
|
+
return {
|
|
2256
|
+
ok: true,
|
|
2257
|
+
data: {
|
|
2258
|
+
type: "coordinator_dispatch_resume",
|
|
2259
|
+
dispatchId: o.dispatchId.trim(),
|
|
2260
|
+
dispatchSessionId: o.dispatchSessionId.trim(),
|
|
2261
|
+
mode: o.mode,
|
|
2262
|
+
content: o.content,
|
|
2263
|
+
resumeAgentId: o.resumeAgentId.trim(),
|
|
2264
|
+
model: o.model.trim(),
|
|
2265
|
+
apiKey: o.apiKey.trim(),
|
|
2266
|
+
workdir: o.workdir.trim(),
|
|
2267
|
+
user: o.user.trim()
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2155
2271
|
function validateAgentWsMessage(value, kind) {
|
|
2156
2272
|
if (typeof value !== "object" || value === null) {
|
|
2157
2273
|
return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
|
|
2158
2274
|
}
|
|
2159
2275
|
const o = value;
|
|
2160
2276
|
const type = o.type;
|
|
2161
|
-
if (type !== "heartbeat" && type !== "message" && type !== "cancel" && type !== "deploy") {
|
|
2277
|
+
if (type !== "heartbeat" && type !== "message" && type !== "cancel" && type !== "deploy" && type !== "coordinator_dispatch" && type !== "coordinator_dispatch_resume") {
|
|
2162
2278
|
return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
|
|
2163
2279
|
}
|
|
2164
2280
|
if (kind === "heartbeat" || type === "heartbeat") {
|
|
@@ -2170,6 +2286,12 @@ function validateAgentWsMessage(value, kind) {
|
|
|
2170
2286
|
if (type === "deploy") {
|
|
2171
2287
|
return validateDeployPush(o);
|
|
2172
2288
|
}
|
|
2289
|
+
if (type === "coordinator_dispatch") {
|
|
2290
|
+
return validateCoordinatorDispatchPush(o);
|
|
2291
|
+
}
|
|
2292
|
+
if (type === "coordinator_dispatch_resume") {
|
|
2293
|
+
return validateCoordinatorDispatchResumePush(o);
|
|
2294
|
+
}
|
|
2173
2295
|
return validateMessagePush(o);
|
|
2174
2296
|
}
|
|
2175
2297
|
|
|
@@ -2341,69 +2463,6 @@ async function handleInboundDeploy(cfg, msg, signal) {
|
|
|
2341
2463
|
}
|
|
2342
2464
|
}
|
|
2343
2465
|
|
|
2344
|
-
// src/commands/connect/abort-signal-debug.ts
|
|
2345
|
-
import {
|
|
2346
|
-
getEventListeners,
|
|
2347
|
-
getMaxListeners,
|
|
2348
|
-
setMaxListeners
|
|
2349
|
-
} from "node:events";
|
|
2350
|
-
function isAbortSignalDebugEnabled() {
|
|
2351
|
-
const v = process.env.APM_DEBUG_ABORT_SIGNAL?.trim().toLowerCase();
|
|
2352
|
-
return v === "1" || v === "true" || v === "yes";
|
|
2353
|
-
}
|
|
2354
|
-
function formatAbortSignalStats(signal, label) {
|
|
2355
|
-
if (!signal) {
|
|
2356
|
-
return `[apm:abort-debug] ${label}: (no signal)`;
|
|
2357
|
-
}
|
|
2358
|
-
const listeners = getEventListeners(signal, "abort");
|
|
2359
|
-
const max = getMaxListeners(signal);
|
|
2360
|
-
return `[apm:abort-debug] ${label}: abortListeners=${listeners.length} maxListeners=${max} aborted=${signal.aborted}`;
|
|
2361
|
-
}
|
|
2362
|
-
function logAbortSignalStats(signal, label) {
|
|
2363
|
-
if (!isAbortSignalDebugEnabled()) return;
|
|
2364
|
-
console.log(formatAbortSignalStats(signal, label));
|
|
2365
|
-
}
|
|
2366
|
-
var installed = false;
|
|
2367
|
-
function installAbortSignalDebug() {
|
|
2368
|
-
if (!isAbortSignalDebugEnabled() || installed) return;
|
|
2369
|
-
installed = true;
|
|
2370
|
-
const maxFromEnv = Number.parseInt(
|
|
2371
|
-
process.env.APM_ABORT_SIGNAL_MAX_LISTENERS ?? "",
|
|
2372
|
-
10
|
|
2373
|
-
);
|
|
2374
|
-
if (Number.isFinite(maxFromEnv) && maxFromEnv > 0) {
|
|
2375
|
-
setMaxListeners(maxFromEnv);
|
|
2376
|
-
console.log(
|
|
2377
|
-
`[apm:abort-debug] setMaxListeners(${maxFromEnv}) via APM_ABORT_SIGNAL_MAX_LISTENERS`
|
|
2378
|
-
);
|
|
2379
|
-
}
|
|
2380
|
-
process.on("warning", (warning) => {
|
|
2381
|
-
if (warning.name !== "MaxListenersExceededWarning") return;
|
|
2382
|
-
console.warn(`[apm:abort-debug] ${warning.name}: ${warning.message}`);
|
|
2383
|
-
if (warning.stack) {
|
|
2384
|
-
console.warn(warning.stack);
|
|
2385
|
-
}
|
|
2386
|
-
});
|
|
2387
|
-
const proto = AbortSignal.prototype;
|
|
2388
|
-
const original = proto.addEventListener;
|
|
2389
|
-
proto.addEventListener = function(type, listener, options) {
|
|
2390
|
-
if (type === "abort") {
|
|
2391
|
-
const sig = this;
|
|
2392
|
-
const before = getEventListeners(sig, "abort").length;
|
|
2393
|
-
const max = getMaxListeners(sig);
|
|
2394
|
-
const stack = new Error("[apm:abort-debug] addEventListener stack").stack?.split("\n").slice(2, 8).join("\n") ?? "";
|
|
2395
|
-
console.log(
|
|
2396
|
-
`[apm:abort-debug] addEventListener("abort") before=${before} max=${max}
|
|
2397
|
-
${stack}`
|
|
2398
|
-
);
|
|
2399
|
-
}
|
|
2400
|
-
return original.call(this, type, listener, options);
|
|
2401
|
-
};
|
|
2402
|
-
console.log(
|
|
2403
|
-
"[apm:abort-debug] \u5DF2\u542F\u7528 AbortSignal \u8C03\u8BD5\uFF08APM_DEBUG_ABORT_SIGNAL\uFF09"
|
|
2404
|
-
);
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
2466
|
// src/commands/connect/cursor-agent.ts
|
|
2408
2467
|
import {
|
|
2409
2468
|
Agent,
|
|
@@ -2411,6 +2470,27 @@ import {
|
|
|
2411
2470
|
} from "@cursor/sdk";
|
|
2412
2471
|
import { setMaxListeners as setMaxListeners2 } from "node:events";
|
|
2413
2472
|
|
|
2473
|
+
// src/plan-format.ts
|
|
2474
|
+
function formatPlanMarkdown(raw) {
|
|
2475
|
+
let text = raw.trim();
|
|
2476
|
+
if (!text) {
|
|
2477
|
+
return text;
|
|
2478
|
+
}
|
|
2479
|
+
if (text.startsWith("{") && text.endsWith("}")) {
|
|
2480
|
+
try {
|
|
2481
|
+
const parsed = JSON.parse(text);
|
|
2482
|
+
if (typeof parsed.plan === "string") {
|
|
2483
|
+
return formatPlanMarkdown(parsed.plan);
|
|
2484
|
+
}
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (!text.includes("\n") && text.includes("\\n")) {
|
|
2489
|
+
text = text.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
2490
|
+
}
|
|
2491
|
+
return text.replace(/\r\n/g, "\n").trimEnd() + "\n";
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2414
2494
|
// src/session-utils.ts
|
|
2415
2495
|
var EventSession = class {
|
|
2416
2496
|
events = [];
|
|
@@ -2536,6 +2616,23 @@ var EventSession = class {
|
|
|
2536
2616
|
getAssistantText() {
|
|
2537
2617
|
return this.events.filter((e) => e.type === "assistant").map((e) => String(e.content ?? "")).join("\n").trim();
|
|
2538
2618
|
}
|
|
2619
|
+
/** plan 模式下 createPlan 工具 completed 时的 plan 字段(取最后一次) */
|
|
2620
|
+
getCreatePlanContent() {
|
|
2621
|
+
for (let i = this.events.length - 1; i >= 0; i--) {
|
|
2622
|
+
const event = this.events[i];
|
|
2623
|
+
if (event.type !== "tool_call") {
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
if (event.name !== "createPlan" || event.status !== "completed") {
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
const plan = event.args?.plan;
|
|
2630
|
+
if (typeof plan === "string" && plan.trim()) {
|
|
2631
|
+
return formatPlanMarkdown(plan);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
return void 0;
|
|
2635
|
+
}
|
|
2539
2636
|
resolveLogContent() {
|
|
2540
2637
|
return this.events.map((event) => formatLogEvent(event.type, event)).join("\n");
|
|
2541
2638
|
}
|
|
@@ -2695,6 +2792,97 @@ async function syncCursorMessageLog(cfg, ctx, events) {
|
|
|
2695
2792
|
});
|
|
2696
2793
|
}
|
|
2697
2794
|
|
|
2795
|
+
// src/commands/connect/abort-signal-debug.ts
|
|
2796
|
+
import {
|
|
2797
|
+
getEventListeners,
|
|
2798
|
+
getMaxListeners,
|
|
2799
|
+
setMaxListeners
|
|
2800
|
+
} from "node:events";
|
|
2801
|
+
function isAbortSignalDebugEnabled() {
|
|
2802
|
+
const v = process.env.APM_DEBUG_ABORT_SIGNAL?.trim().toLowerCase();
|
|
2803
|
+
return v === "1" || v === "true" || v === "yes";
|
|
2804
|
+
}
|
|
2805
|
+
function formatAbortSignalStats(signal, label) {
|
|
2806
|
+
if (!signal) {
|
|
2807
|
+
return `[apm:abort-debug] ${label}: (no signal)`;
|
|
2808
|
+
}
|
|
2809
|
+
const listeners = getEventListeners(signal, "abort");
|
|
2810
|
+
const max = getMaxListeners(signal);
|
|
2811
|
+
return `[apm:abort-debug] ${label}: abortListeners=${listeners.length} maxListeners=${max} aborted=${signal.aborted}`;
|
|
2812
|
+
}
|
|
2813
|
+
function logAbortSignalStats(signal, label) {
|
|
2814
|
+
if (!isAbortSignalDebugEnabled()) return;
|
|
2815
|
+
console.log(formatAbortSignalStats(signal, label));
|
|
2816
|
+
}
|
|
2817
|
+
var installed = false;
|
|
2818
|
+
function installAbortSignalDebug() {
|
|
2819
|
+
if (!isAbortSignalDebugEnabled() || installed) return;
|
|
2820
|
+
installed = true;
|
|
2821
|
+
const maxFromEnv = Number.parseInt(
|
|
2822
|
+
process.env.APM_ABORT_SIGNAL_MAX_LISTENERS ?? "",
|
|
2823
|
+
10
|
|
2824
|
+
);
|
|
2825
|
+
if (Number.isFinite(maxFromEnv) && maxFromEnv > 0) {
|
|
2826
|
+
setMaxListeners(maxFromEnv);
|
|
2827
|
+
console.log(
|
|
2828
|
+
`[apm:abort-debug] setMaxListeners(${maxFromEnv}) via APM_ABORT_SIGNAL_MAX_LISTENERS`
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2831
|
+
process.on("warning", (warning) => {
|
|
2832
|
+
if (warning.name !== "MaxListenersExceededWarning") return;
|
|
2833
|
+
console.warn(`[apm:abort-debug] ${warning.name}: ${warning.message}`);
|
|
2834
|
+
if (warning.stack) {
|
|
2835
|
+
console.warn(warning.stack);
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
const proto = AbortSignal.prototype;
|
|
2839
|
+
const original = proto.addEventListener;
|
|
2840
|
+
proto.addEventListener = function(type, listener, options) {
|
|
2841
|
+
if (type === "abort") {
|
|
2842
|
+
const sig = this;
|
|
2843
|
+
const before = getEventListeners(sig, "abort").length;
|
|
2844
|
+
const max = getMaxListeners(sig);
|
|
2845
|
+
const stack = new Error("[apm:abort-debug] addEventListener stack").stack?.split("\n").slice(2, 8).join("\n") ?? "";
|
|
2846
|
+
console.log(
|
|
2847
|
+
`[apm:abort-debug] addEventListener("abort") before=${before} max=${max}
|
|
2848
|
+
${stack}`
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
return original.call(this, type, listener, options);
|
|
2852
|
+
};
|
|
2853
|
+
console.log(
|
|
2854
|
+
"[apm:abort-debug] \u5DF2\u542F\u7528 AbortSignal \u8C03\u8BD5\uFF08APM_DEBUG_ABORT_SIGNAL\uFF09"
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// src/commands/connect/append-dispatch-response-tool.ts
|
|
2859
|
+
function createAppendDispatchResponseTools(cfg, dispatchId) {
|
|
2860
|
+
return {
|
|
2861
|
+
append_dispatch_response: {
|
|
2862
|
+
description: "Append content to the coordinator member dispatch response for the coordinator Agent to read.",
|
|
2863
|
+
inputSchema: {
|
|
2864
|
+
type: "object",
|
|
2865
|
+
properties: {
|
|
2866
|
+
content: { type: "string", description: "Response text to append" }
|
|
2867
|
+
},
|
|
2868
|
+
required: ["content"]
|
|
2869
|
+
},
|
|
2870
|
+
execute: async (args) => {
|
|
2871
|
+
const content = String(args.content ?? "").trim();
|
|
2872
|
+
if (!content) {
|
|
2873
|
+
return "content \u4E0D\u80FD\u4E3A\u7A7A";
|
|
2874
|
+
}
|
|
2875
|
+
const api = createApmApiClient(cfg);
|
|
2876
|
+
await api.cli.appendCoordinatorDispatchResponse({
|
|
2877
|
+
id: dispatchId,
|
|
2878
|
+
content
|
|
2879
|
+
});
|
|
2880
|
+
return `\u5DF2\u8FFD\u52A0\u6D3E\u53D1\u54CD\u5E94\uFF08${content.length} \u5B57\u7B26\uFF09`;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2698
2886
|
// src/commands/connect/append-message-tool.ts
|
|
2699
2887
|
function createAppendMessageCustomTools(cfg, messageId) {
|
|
2700
2888
|
return {
|
|
@@ -2734,9 +2922,153 @@ function createAppendMessageCustomTools(cfg, messageId) {
|
|
|
2734
2922
|
};
|
|
2735
2923
|
}
|
|
2736
2924
|
|
|
2925
|
+
// src/commands/connect/ask-question-tool.ts
|
|
2926
|
+
import { randomUUID } from "crypto";
|
|
2927
|
+
function createAskQuestionTool(options) {
|
|
2928
|
+
return {
|
|
2929
|
+
description: "Collect structured multiple-choice answers from the user. Use when blocked on a decision that is genuinely the user's to make.",
|
|
2930
|
+
inputSchema: {
|
|
2931
|
+
type: "object",
|
|
2932
|
+
properties: {
|
|
2933
|
+
title: {
|
|
2934
|
+
type: "string",
|
|
2935
|
+
description: "Optional title for the questions form"
|
|
2936
|
+
},
|
|
2937
|
+
questions: {
|
|
2938
|
+
type: "array",
|
|
2939
|
+
minItems: 1,
|
|
2940
|
+
items: {
|
|
2941
|
+
type: "object",
|
|
2942
|
+
properties: {
|
|
2943
|
+
id: { type: "string" },
|
|
2944
|
+
prompt: { type: "string" },
|
|
2945
|
+
allow_multiple: { type: "boolean" },
|
|
2946
|
+
options: {
|
|
2947
|
+
type: "array",
|
|
2948
|
+
minItems: 2,
|
|
2949
|
+
items: {
|
|
2950
|
+
type: "object",
|
|
2951
|
+
properties: {
|
|
2952
|
+
id: { type: "string" },
|
|
2953
|
+
label: { type: "string" }
|
|
2954
|
+
},
|
|
2955
|
+
required: ["id", "label"]
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
},
|
|
2959
|
+
required: ["id", "prompt", "options"]
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
},
|
|
2963
|
+
required: ["questions"]
|
|
2964
|
+
},
|
|
2965
|
+
execute: async (args) => {
|
|
2966
|
+
const record = args;
|
|
2967
|
+
const questions = record.questions;
|
|
2968
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
2969
|
+
return "questions \u4E0D\u80FD\u4E3A\u7A7A";
|
|
2970
|
+
}
|
|
2971
|
+
const api = createApmApiClient(options.cfg);
|
|
2972
|
+
await api.cli.createCoordinatorDispatchQuestions({
|
|
2973
|
+
dispatchId: options.dispatchId,
|
|
2974
|
+
batchId: randomUUID(),
|
|
2975
|
+
title: typeof record.title === "string" ? record.title : void 0,
|
|
2976
|
+
questions
|
|
2977
|
+
});
|
|
2978
|
+
options.onSubmitted?.();
|
|
2979
|
+
return "\u5DF2\u5411\u7528\u6237\u63D0\u4EA4\u95EE\u9898\uFF0C\u8BF7\u7ED3\u675F\u672C\u8F6E run\uFF0C\u7B49\u5F85\u7528\u6237\u5728\u534F\u8C03\u4F1A\u8BDD\u9875\u56DE\u7B54\u540E\u7EE7\u7EED\u3002";
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// src/commands/connect/ask-question-tool.mock.ts
|
|
2985
|
+
function createAskQuestionMockTool(options) {
|
|
2986
|
+
return {
|
|
2987
|
+
description: "Collect structured multiple-choice answers from the user. Use when blocked on a decision that is genuinely the user's to make.",
|
|
2988
|
+
inputSchema: {
|
|
2989
|
+
type: "object",
|
|
2990
|
+
properties: {
|
|
2991
|
+
title: { type: "string" },
|
|
2992
|
+
questions: {
|
|
2993
|
+
type: "array",
|
|
2994
|
+
minItems: 1,
|
|
2995
|
+
items: {
|
|
2996
|
+
type: "object",
|
|
2997
|
+
properties: {
|
|
2998
|
+
id: { type: "string" },
|
|
2999
|
+
prompt: { type: "string" },
|
|
3000
|
+
allow_multiple: { type: "boolean" },
|
|
3001
|
+
options: {
|
|
3002
|
+
type: "array",
|
|
3003
|
+
minItems: 2,
|
|
3004
|
+
items: {
|
|
3005
|
+
type: "object",
|
|
3006
|
+
properties: {
|
|
3007
|
+
id: { type: "string" },
|
|
3008
|
+
label: { type: "string" }
|
|
3009
|
+
},
|
|
3010
|
+
required: ["id", "label"]
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
},
|
|
3014
|
+
required: ["id", "prompt", "options"]
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
},
|
|
3018
|
+
required: ["questions"]
|
|
3019
|
+
},
|
|
3020
|
+
execute: async (args) => {
|
|
3021
|
+
const payload = JSON.stringify(args, null, 2);
|
|
3022
|
+
console.log(`[apm] AskQuestion mock \u8C03\u7528:
|
|
3023
|
+
${payload}`);
|
|
3024
|
+
options?.onInvoke?.(args);
|
|
3025
|
+
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:
|
|
3026
|
+
${payload}`;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// src/commands/connect/cursor-custom-tools.ts
|
|
3032
|
+
var PLAN_MODE_ASK_QUESTION_HINT = `[SDK \u73AF\u5883\u8BF4\u660E]
|
|
3033
|
+
AskQuestion \u5DF2\u901A\u8FC7 MCP \u670D\u52A1\u5668 custom-user-tools \u6CE8\u518C\uFF0C\u5DE5\u5177\u540D\u4E3A AskQuestion\u3002
|
|
3034
|
+
\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
|
|
3035
|
+
\u975E\u5FC5\u987B\u7684\u95EE\u9898\u53EF\u8DF3\u8FC7\uFF0C\u76F4\u63A5\u5B8C\u6210 createPlan\u3002`;
|
|
3036
|
+
function createCursorCustomTools(cfg, messageId, options) {
|
|
3037
|
+
return {
|
|
3038
|
+
...createAppendMessageCustomTools(cfg, messageId),
|
|
3039
|
+
AskQuestion: createAskQuestionMockTool({
|
|
3040
|
+
onInvoke: options?.onAskQuestion
|
|
3041
|
+
})
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
function createCursorDispatchCustomTools(cfg, options) {
|
|
3045
|
+
return {
|
|
3046
|
+
...createAppendDispatchResponseTools(cfg, options.dispatchId),
|
|
3047
|
+
AskQuestion: createAskQuestionTool({
|
|
3048
|
+
cfg,
|
|
3049
|
+
dispatchId: options.dispatchId,
|
|
3050
|
+
onSubmitted: options.onAskQuestionSubmitted
|
|
3051
|
+
})
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
function withPlanModeToolHint(prompt, mode) {
|
|
3055
|
+
if (mode !== "plan") {
|
|
3056
|
+
return prompt;
|
|
3057
|
+
}
|
|
3058
|
+
return `${prompt.trim()}
|
|
3059
|
+
|
|
3060
|
+
${PLAN_MODE_ASK_QUESTION_HINT}`;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2737
3063
|
// src/commands/connect/cursor-agent.ts
|
|
2738
3064
|
setMaxListeners2(50);
|
|
2739
3065
|
installAbortSignalDebug();
|
|
3066
|
+
var noopRemoteLogSync = {
|
|
3067
|
+
schedule(_session) {
|
|
3068
|
+
},
|
|
3069
|
+
async flush(_session) {
|
|
3070
|
+
}
|
|
3071
|
+
};
|
|
2740
3072
|
var logCtx = (ctx, agentId) => ({
|
|
2741
3073
|
sessionId: ctx.sessionId,
|
|
2742
3074
|
messageId: ctx.messageId,
|
|
@@ -2760,24 +3092,32 @@ async function obtainAgent(ctx) {
|
|
|
2760
3092
|
apiKey: ctx.apiKey,
|
|
2761
3093
|
model: { id: ctx.model || "default" },
|
|
2762
3094
|
local: {
|
|
2763
|
-
cwd: ctx.cwd
|
|
2764
|
-
|
|
3095
|
+
cwd: ctx.cwd,
|
|
3096
|
+
...ctx.customTools ? { customTools: ctx.customTools } : {}
|
|
3097
|
+
},
|
|
3098
|
+
...ctx.mode ? { mode: ctx.mode } : {}
|
|
2765
3099
|
// mcpServers: createPlaywrightMcpServers(),
|
|
2766
3100
|
};
|
|
2767
|
-
const
|
|
3101
|
+
const explicitAgentId = ctx.resumeAgentId?.trim();
|
|
3102
|
+
const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
|
|
2768
3103
|
if (savedAgentId) {
|
|
2769
3104
|
try {
|
|
2770
3105
|
const agent2 = await Agent.resume(savedAgentId, agentOptions);
|
|
2771
3106
|
console.log(
|
|
2772
|
-
`[apm] \u590D\u7528
|
|
3107
|
+
`[apm] \u590D\u7528 Agent user=${ctx.user} agentId=${savedAgentId}${explicitAgentId ? "\uFF08\u53C2\u6570\u6307\u5B9A\uFF09" : ""}`
|
|
2773
3108
|
);
|
|
3109
|
+
if (ctx.user) {
|
|
3110
|
+
saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent2.agentId);
|
|
3111
|
+
}
|
|
2774
3112
|
return { agent: agent2, resumed: true };
|
|
2775
3113
|
} catch (err) {
|
|
2776
3114
|
console.warn(
|
|
2777
3115
|
`[apm] \u590D\u7528 Agent \u5931\u8D25\uFF08agentId=${savedAgentId}\uFF09\uFF0C\u56DE\u9000\u4E3A\u65B0\u5EFA:`,
|
|
2778
3116
|
err instanceof Error ? err.message : err
|
|
2779
3117
|
);
|
|
2780
|
-
|
|
3118
|
+
if (!explicitAgentId && ctx.user) {
|
|
3119
|
+
clearSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user);
|
|
3120
|
+
}
|
|
2781
3121
|
}
|
|
2782
3122
|
}
|
|
2783
3123
|
const agent = await Agent.create(agentOptions);
|
|
@@ -2797,7 +3137,10 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
2797
3137
|
throw new Error("\u7F3A\u5C11 apiKey\uFF0C\u65E0\u6CD5\u8C03\u7528 Cursor SDK");
|
|
2798
3138
|
}
|
|
2799
3139
|
const workdir = resolveWorkdirPath(ctx.workdir);
|
|
2800
|
-
const
|
|
3140
|
+
const customTools = options?.customTools ?? createCursorCustomTools(cfg, ctx.messageId, {
|
|
3141
|
+
onAskQuestion: options?.onAskQuestion
|
|
3142
|
+
});
|
|
3143
|
+
const prompt = withPlanModeToolHint(ctx.prompt, ctx.mode);
|
|
2801
3144
|
console.log(
|
|
2802
3145
|
`[apm] Cursor Agent \u5F00\u59CB messageId=${ctx.messageId} sessionId=${ctx.sessionId} cwd=${workdir}`
|
|
2803
3146
|
);
|
|
@@ -2807,13 +3150,15 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
2807
3150
|
cwd: workdir,
|
|
2808
3151
|
workdir,
|
|
2809
3152
|
sessionId: ctx.sessionId,
|
|
2810
|
-
user: ctx.user
|
|
3153
|
+
user: ctx.user,
|
|
3154
|
+
mode: ctx.mode,
|
|
3155
|
+
resumeAgentId: ctx.resumeAgentId,
|
|
3156
|
+
customTools
|
|
2811
3157
|
});
|
|
2812
3158
|
const eventSession = new EventSession(prompt);
|
|
2813
|
-
const
|
|
2814
|
-
const syncRemoteLog = createThrottledCursorMessageLogSync(
|
|
3159
|
+
const syncRemoteLog = options?.skipRemoteLogSync ? noopRemoteLogSync : createThrottledCursorMessageLogSync(
|
|
2815
3160
|
cfg,
|
|
2816
|
-
|
|
3161
|
+
logCtx(ctx, agent.agentId),
|
|
2817
3162
|
(err) => {
|
|
2818
3163
|
console.warn(
|
|
2819
3164
|
"[apm] \u540C\u6B65 Cursor \u6D88\u606F\u65E5\u5FD7\u5931\u8D25:",
|
|
@@ -2830,9 +3175,11 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
2830
3175
|
logAbortSignalStats(signal, "runCursorAgent:after-addListener");
|
|
2831
3176
|
try {
|
|
2832
3177
|
const run = await agent.send(prompt, {
|
|
3178
|
+
...ctx.mode ? { mode: ctx.mode } : {},
|
|
2833
3179
|
// mcpServers: createPlaywrightMcpServers(),
|
|
2834
3180
|
local: {
|
|
2835
|
-
|
|
3181
|
+
...options?.forceSend ? { force: true } : {},
|
|
3182
|
+
customTools
|
|
2836
3183
|
}
|
|
2837
3184
|
});
|
|
2838
3185
|
activeRun = run;
|
|
@@ -2853,6 +3200,7 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
2853
3200
|
);
|
|
2854
3201
|
}
|
|
2855
3202
|
}
|
|
3203
|
+
options?.onStreamEvent?.(event);
|
|
2856
3204
|
eventSession.addEvent(event);
|
|
2857
3205
|
syncRemoteLog.schedule(eventSession);
|
|
2858
3206
|
}
|
|
@@ -2873,6 +3221,32 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
2873
3221
|
throw new Error(`Cursor run \u5DF2\u53D6\u6D88: ${result.id}`);
|
|
2874
3222
|
}
|
|
2875
3223
|
console.log(`[apm] Cursor Agent \u5B8C\u6210 messageId=${ctx.messageId}`);
|
|
3224
|
+
const artifacts = await agent.listArtifacts().catch(() => []);
|
|
3225
|
+
const artifactDocuments = [];
|
|
3226
|
+
for (const artifact of artifacts) {
|
|
3227
|
+
try {
|
|
3228
|
+
const content = (await agent.downloadArtifact(artifact.path)).toString(
|
|
3229
|
+
"utf8"
|
|
3230
|
+
);
|
|
3231
|
+
artifactDocuments.push({ path: artifact.path, content });
|
|
3232
|
+
} catch (err) {
|
|
3233
|
+
console.warn(
|
|
3234
|
+
`[apm] \u8BFB\u53D6\u4EA7\u7269\u5931\u8D25 path=${artifact.path}:`,
|
|
3235
|
+
err instanceof Error ? err.message : err
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
return {
|
|
3240
|
+
runId: result.id,
|
|
3241
|
+
agentId: agent.agentId,
|
|
3242
|
+
status: result.status,
|
|
3243
|
+
result: result.result,
|
|
3244
|
+
durationMs: result.durationMs,
|
|
3245
|
+
assistantText: eventSession.getAssistantText(),
|
|
3246
|
+
createPlan: eventSession.getCreatePlanContent(),
|
|
3247
|
+
artifacts,
|
|
3248
|
+
artifactDocuments
|
|
3249
|
+
};
|
|
2876
3250
|
} catch (err) {
|
|
2877
3251
|
if (err instanceof CursorAgentError) {
|
|
2878
3252
|
if (resumed) {
|
|
@@ -2891,6 +3265,79 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
2891
3265
|
}
|
|
2892
3266
|
}
|
|
2893
3267
|
|
|
3268
|
+
// src/commands/connect/coordinator-dispatch-handler.ts
|
|
3269
|
+
async function handleInboundCoordinatorDispatch(cfg, msg, signal) {
|
|
3270
|
+
const workdir = requireRemoteWorkdir(msg.workdir);
|
|
3271
|
+
assertApmGitignoredInRepo(workdir);
|
|
3272
|
+
await ensureWorkspaceInitialized(workdir);
|
|
3273
|
+
const api = createApmApiClient(cfg);
|
|
3274
|
+
let askQuestionSubmitted = false;
|
|
3275
|
+
try {
|
|
3276
|
+
await api.cli.updateCoordinatorDispatchStatus({
|
|
3277
|
+
id: msg.dispatchId,
|
|
3278
|
+
status: "DISPATCHING"
|
|
3279
|
+
});
|
|
3280
|
+
const result = await runCursorAgent(
|
|
3281
|
+
cfg,
|
|
3282
|
+
{
|
|
3283
|
+
messageId: msg.dispatchId,
|
|
3284
|
+
sessionId: msg.dispatchSessionId,
|
|
3285
|
+
prompt: msg.content,
|
|
3286
|
+
model: msg.model,
|
|
3287
|
+
apiKey: msg.apiKey,
|
|
3288
|
+
workdir: msg.workdir,
|
|
3289
|
+
user: msg.user,
|
|
3290
|
+
mode: msg.mode,
|
|
3291
|
+
resumeAgentId: msg.type === "coordinator_dispatch_resume" ? msg.resumeAgentId : msg.resumeAgentId
|
|
3292
|
+
},
|
|
3293
|
+
{
|
|
3294
|
+
signal,
|
|
3295
|
+
forceSend: true,
|
|
3296
|
+
skipRemoteLogSync: true,
|
|
3297
|
+
customTools: createCursorDispatchCustomTools(cfg, {
|
|
3298
|
+
dispatchId: msg.dispatchId,
|
|
3299
|
+
onAskQuestionSubmitted: () => {
|
|
3300
|
+
askQuestionSubmitted = true;
|
|
3301
|
+
}
|
|
3302
|
+
})
|
|
3303
|
+
}
|
|
3304
|
+
);
|
|
3305
|
+
if (askQuestionSubmitted) {
|
|
3306
|
+
if (result.agentId) {
|
|
3307
|
+
await api.cli.completeCoordinatorDispatch({
|
|
3308
|
+
id: msg.dispatchId,
|
|
3309
|
+
agentId: result.agentId
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
console.log(
|
|
3313
|
+
`[apm] \u534F\u8C03\u6D3E\u53D1 ${msg.dispatchId} \u5DF2\u63D0\u4EA4 AskQuestion\uFF0C\u7B49\u5F85\u7528\u6237\u7B54\u9898`
|
|
3314
|
+
);
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
await api.cli.completeCoordinatorDispatch({
|
|
3318
|
+
id: msg.dispatchId,
|
|
3319
|
+
agentId: result.agentId,
|
|
3320
|
+
createPlan: result.createPlan,
|
|
3321
|
+
artifacts: result.artifactDocuments.map((doc) => ({
|
|
3322
|
+
name: doc.path.split("/").pop() ?? doc.path,
|
|
3323
|
+
content: doc.content
|
|
3324
|
+
})),
|
|
3325
|
+
status: "SUCCESS"
|
|
3326
|
+
});
|
|
3327
|
+
console.log(`[apm] \u534F\u8C03\u6D3E\u53D1\u5B8C\u6210 dispatchId=${msg.dispatchId}`);
|
|
3328
|
+
} catch (error) {
|
|
3329
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3330
|
+
console.error(`[apm] \u534F\u8C03\u6D3E\u53D1\u5931\u8D25 dispatchId=${msg.dispatchId}:`, message);
|
|
3331
|
+
if (!askQuestionSubmitted) {
|
|
3332
|
+
await api.cli.completeCoordinatorDispatch({
|
|
3333
|
+
id: msg.dispatchId,
|
|
3334
|
+
status: "FAILED",
|
|
3335
|
+
error: message
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
|
|
2894
3341
|
// src/commands/connect/cli-version-sync.ts
|
|
2895
3342
|
import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
2896
3343
|
import { join as join14 } from "path";
|
|
@@ -3266,6 +3713,27 @@ async function runConnect(options) {
|
|
|
3266
3713
|
});
|
|
3267
3714
|
return;
|
|
3268
3715
|
}
|
|
3716
|
+
if (validated.data.type === "coordinator_dispatch" || validated.data.type === "coordinator_dispatch_resume") {
|
|
3717
|
+
const msg2 = validated.data;
|
|
3718
|
+
const perDispatchController = new AbortController();
|
|
3719
|
+
const signal2 = AbortSignal.any([
|
|
3720
|
+
shutdownAbort.signal,
|
|
3721
|
+
perDispatchController.signal
|
|
3722
|
+
]);
|
|
3723
|
+
const task2 = (async () => {
|
|
3724
|
+
await runSlots.acquire();
|
|
3725
|
+
try {
|
|
3726
|
+
await handleInboundCoordinatorDispatch(cfg, msg2, signal2);
|
|
3727
|
+
} finally {
|
|
3728
|
+
runSlots.release();
|
|
3729
|
+
}
|
|
3730
|
+
})();
|
|
3731
|
+
activeTasks.add(task2);
|
|
3732
|
+
void task2.finally(() => {
|
|
3733
|
+
activeTasks.delete(task2);
|
|
3734
|
+
});
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3269
3737
|
if (validated.data.type !== "message") {
|
|
3270
3738
|
return;
|
|
3271
3739
|
}
|
|
@@ -4403,14 +4871,12 @@ function buildClearRemoteDirExceptZipCommand(target) {
|
|
|
4403
4871
|
const script = [
|
|
4404
4872
|
`T=${quotedTarget}`,
|
|
4405
4873
|
"S=$(mktemp -d)",
|
|
4406
|
-
'while IFS= read -r -d "" z; do',
|
|
4407
|
-
'r="${z#${T}/}"',
|
|
4874
|
+
'while IFS= read -r -d "" z; do r="${z#${T}/}"',
|
|
4408
4875
|
'mkdir -p "${S}/$(dirname "$r")"',
|
|
4409
4876
|
'mv "$z" "${S}/${r}"',
|
|
4410
4877
|
'done < <(find "$T" -mindepth 1 -type f -iname "*.zip" -print0)',
|
|
4411
4878
|
'rm -rf "${T}"/*',
|
|
4412
|
-
'while IFS= read -r -d "" r; do',
|
|
4413
|
-
'r="${r#./}"',
|
|
4879
|
+
'while IFS= read -r -d "" r; do r="${r#./}"',
|
|
4414
4880
|
'mkdir -p "${T}/$(dirname "$r")"',
|
|
4415
4881
|
'mv "${S}/${r}" "${T}/${r}"',
|
|
4416
4882
|
'done < <(cd "$S" 2>/dev/null && find . -type f -print0)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-project-manage-cli",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.64",
|
|
4
4
|
"description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/index.js && tsc --noEmit -p tsconfig.json",
|
|
18
|
+
"test:plan-cursor": "esbuild scripts/test-plan-cursor.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/test-plan-cursor.mjs && node dist/test-plan-cursor.mjs",
|
|
18
19
|
"prepublishOnly": "npm run build"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|