ai-project-manage-cli 6.0.27 → 6.0.29

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 CHANGED
@@ -885,7 +885,73 @@ async function runUpdateSkills() {
885
885
  }
886
886
 
887
887
  // src/commands/sync-document.ts
888
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
888
+ import { existsSync as existsSync6 } from "fs";
889
+ import { basename as basename2 } from "path";
890
+
891
+ // src/commands/sync-session-documents.ts
892
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
893
+ import { join as join9 } from "path";
894
+ function listLocalMarkdownFiles(docsDir) {
895
+ if (!existsSync5(docsDir)) {
896
+ return [];
897
+ }
898
+ return readdirSync3(docsDir).filter(
899
+ (name) => name.toLowerCase().endsWith(".md")
900
+ );
901
+ }
902
+ function remoteDocumentByLocalName(remoteDocuments, localFileName) {
903
+ const platformName = documentPlatformName(localFileName);
904
+ return remoteDocuments.find((doc) => {
905
+ const remoteLocalName = documentLocalFileName(doc.name);
906
+ return remoteLocalName === localFileName || documentPlatformName(doc.name) === platformName;
907
+ });
908
+ }
909
+ async function upsertLocalDocumentFile(api, sessionId, docsDir, fileName) {
910
+ const absPath = join9(docsDir, fileName);
911
+ const content = readFileSync5(absPath, "utf8");
912
+ const name = documentPlatformName(absPath);
913
+ return api.cli.upsertDocument({
914
+ sessionId,
915
+ name,
916
+ content
917
+ });
918
+ }
919
+ async function syncSessionDocuments(cfg, sessionId, apmRoot, options) {
920
+ const trimmedSessionId = sessionId.trim();
921
+ if (!trimmedSessionId) {
922
+ return 0;
923
+ }
924
+ const docsDir = sessionDocsDir(trimmedSessionId, apmRoot);
925
+ const localFiles = listLocalMarkdownFiles(docsDir);
926
+ if (localFiles.length === 0) {
927
+ return 0;
928
+ }
929
+ const api = options?.api ?? createApmApiClient(cfg);
930
+ const remoteDocuments = options?.remoteDocuments ?? await api.cli.listDocuments({ sessionId: trimmedSessionId });
931
+ let synced = 0;
932
+ for (const fileName of localFiles) {
933
+ const absPath = join9(docsDir, fileName);
934
+ const content = readFileSync5(absPath, "utf8");
935
+ const remote = remoteDocumentByLocalName(remoteDocuments, fileName);
936
+ if (remote && remote.content === content) {
937
+ continue;
938
+ }
939
+ const doc = await upsertLocalDocumentFile(
940
+ api,
941
+ trimmedSessionId,
942
+ docsDir,
943
+ fileName
944
+ );
945
+ synced += 1;
946
+ console.log(`[apm] \u5DF2\u540C\u6B65\u6587\u6863: ${doc.name} (id=${doc.id})`);
947
+ }
948
+ if (synced === 0) {
949
+ console.log("[apm] \u4F1A\u8BDD\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
950
+ }
951
+ return synced;
952
+ }
953
+
954
+ // src/commands/sync-document.ts
889
955
  async function runSyncDocument(sessionId, options) {
890
956
  const trimmedSessionId = sessionId.trim();
891
957
  if (!trimmedSessionId) {
@@ -898,24 +964,24 @@ async function runSyncDocument(sessionId, options) {
898
964
  process.exit(1);
899
965
  }
900
966
  const absPath = resolveSessionDocumentPath(trimmedSessionId, fileArg);
901
- if (!existsSync5(absPath)) {
902
- const docsDir = sessionDocsDir(trimmedSessionId);
967
+ if (!existsSync6(absPath)) {
968
+ const docsDir2 = sessionDocsDir(trimmedSessionId);
903
969
  console.error(
904
970
  `[apm] \u6587\u6863\u4E0D\u5B58\u5728: ${absPath}
905
- [apm] \u8BF7\u786E\u8BA4\u5DF2 pull\uFF0C\u4E14 ${docsDir} \u4E0B\u5B58\u5728\u5BF9\u5E94\u6587\u4EF6`
971
+ [apm] \u8BF7\u786E\u8BA4\u5DF2 pull\uFF0C\u4E14 ${docsDir2} \u4E0B\u5B58\u5728\u5BF9\u5E94\u6587\u4EF6`
906
972
  );
907
973
  process.exit(1);
908
974
  }
909
- const content = readFileSync5(absPath, "utf8");
910
- const name = documentPlatformName(absPath);
911
975
  const cfg = await ensureLoggedConfig();
912
976
  const api = createApmApiClient(cfg);
913
- const doc = await api.cli.upsertDocument({
914
- sessionId: trimmedSessionId,
915
- name,
916
- content
917
- });
918
- console.log(`[apm] \u5DF2\u540C\u6B65\u6587\u6863: ${name} (id=${doc.id})`);
977
+ const docsDir = sessionDocsDir(trimmedSessionId);
978
+ const doc = await upsertLocalDocumentFile(
979
+ api,
980
+ trimmedSessionId,
981
+ docsDir,
982
+ basename2(absPath)
983
+ );
984
+ console.log(`[apm] \u5DF2\u540C\u6B65\u6587\u6863: ${doc.name} (id=${doc.id})`);
919
985
  }
920
986
 
921
987
  // src/commands/append-message.ts
@@ -1038,6 +1104,7 @@ function validateAgentWsMessage(value, kind) {
1038
1104
 
1039
1105
  // src/commands/connect/cursor-agent.ts
1040
1106
  import { Agent, CursorAgentError } from "@cursor/sdk";
1107
+ import { setMaxListeners } from "node:events";
1041
1108
  import { resolve as resolve3 } from "path";
1042
1109
 
1043
1110
  // src/session-utils.ts
@@ -1199,18 +1266,6 @@ ${JSON.stringify(event, null, 2)}
1199
1266
  }
1200
1267
 
1201
1268
  // src/commands/connect/cursor-message-log.ts
1202
- function assistantStreamChunk(event) {
1203
- if (event.type !== "assistant") {
1204
- return "";
1205
- }
1206
- let text = "";
1207
- for (const block of event.message.content) {
1208
- if (block.type === "text" && block.text) {
1209
- text += block.text;
1210
- }
1211
- }
1212
- return text;
1213
- }
1214
1269
  var CURSOR_MESSAGE_LOG_SYNC_INTERVAL_MS = 2e3;
1215
1270
  function createThrottledCursorMessageLogSync(cfg, ctx, onError) {
1216
1271
  let lastRunAt = 0;
@@ -1285,12 +1340,17 @@ async function syncCursorMessageLog(cfg, ctx, events) {
1285
1340
  }
1286
1341
 
1287
1342
  // src/commands/connect/cursor-agent.ts
1343
+ setMaxListeners(50);
1288
1344
  var logCtx = (ctx, agentId) => ({
1289
1345
  sessionId: ctx.sessionId,
1290
1346
  messageId: ctx.messageId,
1291
1347
  agentId
1292
1348
  });
1293
- async function runCursorAgent(cfg, ctx) {
1349
+ async function runCursorAgent(cfg, ctx, options) {
1350
+ const signal = options?.signal;
1351
+ if (signal?.aborted) {
1352
+ throw new Error("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u4EFB\u52A1\u4E2D\u65AD");
1353
+ }
1294
1354
  const apiKey = ctx.apiKey.trim();
1295
1355
  if (!apiKey) {
1296
1356
  throw new Error("\u7F3A\u5C11 apiKey\uFF0C\u65E0\u6CD5\u8C03\u7528 Cursor SDK");
@@ -1320,12 +1380,22 @@ async function runCursorAgent(cfg, ctx) {
1320
1380
  );
1321
1381
  }
1322
1382
  );
1383
+ let activeRun;
1384
+ const abortRun = () => {
1385
+ if (!activeRun?.supports("cancel")) return;
1386
+ void activeRun.cancel().catch(() => void 0);
1387
+ };
1388
+ signal?.addEventListener("abort", abortRun, { once: true });
1323
1389
  try {
1324
1390
  const run = await agent.send(prompt);
1391
+ activeRun = run;
1325
1392
  console.log(`[apm] Cursor run id=${run.id} agentId=${agent.agentId}`);
1326
1393
  for await (const event of run.stream()) {
1394
+ if (signal?.aborted) {
1395
+ abortRun();
1396
+ throw new Error("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u4EFB\u52A1\u4E2D\u65AD");
1397
+ }
1327
1398
  eventSession.addEvent(event);
1328
- const chunk = assistantStreamChunk(event);
1329
1399
  syncRemoteLog.schedule(eventSession);
1330
1400
  }
1331
1401
  await syncRemoteLog.flush(eventSession);
@@ -1345,6 +1415,7 @@ async function runCursorAgent(cfg, ctx) {
1345
1415
  }
1346
1416
  throw err;
1347
1417
  } finally {
1418
+ signal?.removeEventListener("abort", abortRun);
1348
1419
  await agent[Symbol.asyncDispose]();
1349
1420
  }
1350
1421
  }
@@ -1361,7 +1432,9 @@ async function appendMessageContent(cfg, messageId, content) {
1361
1432
  await api.cli.appendMessageContent({ id: messageId, content });
1362
1433
  console.log(`[apm] \u5DF2\u8FFD\u52A0\u6D88\u606F\u5185\u5BB9: ${messageId}`);
1363
1434
  }
1364
- async function handleInboundMessage(cfg, raw) {
1435
+ var SHUTDOWN_DRAIN_MS = 3e3;
1436
+ async function handleInboundMessage(cfg, raw, signal) {
1437
+ if (signal.aborted) return;
1365
1438
  const parsed = parseAgentWsMessage(raw);
1366
1439
  if (parsed === null) {
1367
1440
  console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
@@ -1381,18 +1454,35 @@ async function handleInboundMessage(cfg, raw) {
1381
1454
  const msg = validated.data;
1382
1455
  const messageId = msg.messageId;
1383
1456
  try {
1457
+ if (signal.aborted) return;
1384
1458
  await runBranch(msg.sessionId, { cwd: msg.workdir });
1459
+ if (signal.aborted) return;
1385
1460
  await runPull(msg.sessionId, workspaceApmDir(msg.workdir));
1461
+ if (signal.aborted) return;
1386
1462
  await commitWorkingTreeIfDirty(msg.workdir, "fix: apm pull");
1463
+ if (signal.aborted) return;
1387
1464
  await updateMessageStatus(cfg, messageId, "TYPING");
1388
- await runCursorAgent(cfg, {
1389
- messageId: msg.messageId,
1390
- sessionId: msg.sessionId,
1391
- prompt: msg.content,
1392
- model: msg.model,
1393
- apiKey: msg.apiKey,
1394
- workdir: msg.workdir
1395
- });
1465
+ await runCursorAgent(
1466
+ cfg,
1467
+ {
1468
+ messageId: msg.messageId,
1469
+ sessionId: msg.sessionId,
1470
+ prompt: msg.content,
1471
+ model: msg.model,
1472
+ apiKey: msg.apiKey,
1473
+ workdir: msg.workdir
1474
+ },
1475
+ { signal }
1476
+ );
1477
+ await syncSessionDocuments(
1478
+ cfg,
1479
+ msg.sessionId,
1480
+ workspaceApmDir(msg.workdir)
1481
+ );
1482
+ await commitWorkingTreeIfDirty(
1483
+ msg.workdir,
1484
+ "chore(apm): sync session documents"
1485
+ );
1396
1486
  await updateMessageStatus(cfg, messageId, "SUCCESS");
1397
1487
  } catch (err) {
1398
1488
  console.error(
@@ -1446,32 +1536,42 @@ async function runConnect(options) {
1446
1536
  const ws = new WebSocket(url);
1447
1537
  let stopHeartbeat;
1448
1538
  let shuttingDown = false;
1539
+ const shutdownAbort = new AbortController();
1449
1540
  let inboundQueue = Promise.resolve();
1450
- const shutdown = (code = 0) => {
1541
+ const shutdown = async (code = 0) => {
1451
1542
  if (shuttingDown) return;
1452
1543
  shuttingDown = true;
1544
+ shutdownAbort.abort();
1453
1545
  stopHeartbeat?.();
1454
1546
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
1455
- ws.close();
1547
+ ws.terminate();
1456
1548
  }
1457
- resolve5();
1458
- if (code !== 0) {
1459
- process.exit(code);
1549
+ try {
1550
+ await Promise.race([
1551
+ inboundQueue,
1552
+ new Promise((r) => setTimeout(r, SHUTDOWN_DRAIN_MS))
1553
+ ]);
1554
+ } catch {
1460
1555
  }
1556
+ resolve5();
1557
+ process.exit(code);
1461
1558
  };
1462
1559
  ws.on("open", () => {
1463
1560
  console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
1464
1561
  stopHeartbeat = startHeartbeat(ws, clientMachineId);
1465
1562
  });
1466
1563
  ws.on("message", (data) => {
1564
+ if (shuttingDown) return;
1467
1565
  const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
1468
- inboundQueue = inboundQueue.then(() => handleInboundMessage(cfg, text));
1566
+ inboundQueue = inboundQueue.then(
1567
+ () => handleInboundMessage(cfg, text, shutdownAbort.signal)
1568
+ );
1469
1569
  });
1470
1570
  ws.on("close", (code, reason) => {
1471
1571
  console.log(
1472
1572
  `[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
1473
1573
  );
1474
- shutdown();
1574
+ void shutdown();
1475
1575
  });
1476
1576
  ws.on("error", (err) => {
1477
1577
  console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
@@ -1479,9 +1579,11 @@ async function runConnect(options) {
1479
1579
  });
1480
1580
  process.on("SIGINT", () => {
1481
1581
  console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
1482
- shutdown();
1582
+ void shutdown();
1583
+ });
1584
+ process.on("SIGTERM", () => {
1585
+ void shutdown();
1483
1586
  });
1484
- process.on("SIGTERM", () => shutdown());
1485
1587
  });
1486
1588
  }
1487
1589
 
@@ -1489,14 +1591,14 @@ async function runConnect(options) {
1489
1591
  import path5 from "node:path";
1490
1592
 
1491
1593
  // src/commands/deploy/internal/apm-config.ts
1492
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
1594
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
1493
1595
  import { resolve as resolve4 } from "node:path";
1494
1596
  function loadApmConfig(options) {
1495
1597
  const p = resolve4(
1496
1598
  process.cwd(),
1497
1599
  options?.configPath ?? resolve4(workspaceApmDir(), "apm.config.json")
1498
1600
  );
1499
- if (!existsSync6(p)) {
1601
+ if (!existsSync7(p)) {
1500
1602
  console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
1501
1603
  process.exit(1);
1502
1604
  }
@@ -1623,7 +1725,7 @@ import path4 from "node:path";
1623
1725
  import Docker from "dockerode";
1624
1726
 
1625
1727
  // src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
1626
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
1728
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
1627
1729
  import path from "node:path";
1628
1730
  function asOptionalTlsBuffer(value) {
1629
1731
  if (typeof value !== "string") {
@@ -1635,7 +1737,7 @@ function asOptionalTlsBuffer(value) {
1635
1737
  if (normalized === "") {
1636
1738
  return void 0;
1637
1739
  }
1638
- if (existsSync7(normalized)) {
1740
+ if (existsSync8(normalized)) {
1639
1741
  return readFileSync7(normalized);
1640
1742
  }
1641
1743
  const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
@@ -1846,7 +1948,7 @@ var DockerodeClient = class {
1846
1948
  var createDockerodeClient = (config) => new DockerodeClient(config);
1847
1949
 
1848
1950
  // src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
1849
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
1951
+ import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
1850
1952
  import path2 from "node:path";
1851
1953
  function stripSurroundingQuotes(value) {
1852
1954
  const t = value.trim();
@@ -1863,7 +1965,7 @@ function loadEnvFromFile(envFilePath) {
1863
1965
  return {};
1864
1966
  }
1865
1967
  const targetPath = path2.resolve(envFilePath);
1866
- if (!existsSync8(targetPath) || !statSync4(targetPath).isFile()) {
1968
+ if (!existsSync9(targetPath) || !statSync4(targetPath).isFile()) {
1867
1969
  return {};
1868
1970
  }
1869
1971
  const raw = readFileSync8(targetPath, "utf-8");
@@ -2037,12 +2139,12 @@ function dockerPushImage(params, cwd) {
2037
2139
  }
2038
2140
 
2039
2141
  // src/commands/deploy/internal/backend-deploy/resolve-dockerfile.ts
2040
- import { existsSync as existsSync9 } from "node:fs";
2142
+ import { existsSync as existsSync10 } from "node:fs";
2041
2143
  import path3 from "node:path";
2042
2144
  function resolveDockerBuildPaths(cwd) {
2043
2145
  const dockerfilePath = path3.join(cwd, "Dockerfile");
2044
2146
  Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
2045
- if (!existsSync9(dockerfilePath)) {
2147
+ if (!existsSync10(dockerfilePath)) {
2046
2148
  throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
2047
2149
  }
2048
2150
  Logger.info("\u2713 Dockerfile \u5B58\u5728");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.27",
3
+ "version": "6.0.29",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -18,9 +18,11 @@
18
18
 
19
19
  ## 在保存完成之后需要同步到远程
20
20
 
21
- 同步命令: `apm sync-document <会话 ID> --file=<文档名称>
21
+ 通过 `apm connect` 连接平台时,每轮 Agent 结束后会自动推送 `docs/` 下变更的 Markdown,一般无需手动同步。
22
22
 
23
- 示例: `apm sync-document <会话ID> --file=张三.md`
23
+ 手动同步命令: `apm sync-document <会话 ID> --file=<文档名称>`
24
+
25
+ 示例: `apm sync-document <会话ID> --file=张三-工作日志.md`
24
26
 
25
27
  ## 注意事项
26
28