ai-project-manage-cli 6.0.33 → 6.0.35

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
@@ -1012,9 +1012,11 @@ async function runAppendMessage(options) {
1012
1012
  // src/commands/update-message-status.ts
1013
1013
  var VALID_STATUSES = [
1014
1014
  "CREATED",
1015
+ "QUEUED",
1015
1016
  "TYPING",
1016
1017
  "SUCCESS",
1017
- "FAILED"
1018
+ "FAILED",
1019
+ "CANCELLED"
1018
1020
  ];
1019
1021
  async function runUpdateMessageStatus(options) {
1020
1022
  const messageId = options.id?.trim();
@@ -1052,25 +1054,17 @@ function parseAgentWsMessage(raw) {
1052
1054
  function serializeAgentWsMessage(msg) {
1053
1055
  return JSON.stringify(msg);
1054
1056
  }
1055
- function validateAgentWsMessage(value, kind) {
1056
- if (typeof value !== "object" || value === null) {
1057
- return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
1058
- }
1059
- const o = value;
1060
- const type = o.type;
1061
- if (type !== "heartbeat" && type !== "message") {
1062
- return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
1057
+ function validateHeartbeat(o) {
1058
+ if (o.type !== "heartbeat") {
1059
+ return { ok: false, reason: "\u671F\u671B heartbeat" };
1063
1060
  }
1064
- if (kind === "heartbeat" || type === "heartbeat") {
1065
- if (type !== "heartbeat") {
1066
- return { ok: false, reason: "\u671F\u671B heartbeat" };
1067
- }
1068
- if (!nonEmptyString(o.userId)) {
1069
- return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
1070
- }
1071
- return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
1061
+ if (!nonEmptyString(o.userId)) {
1062
+ return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
1072
1063
  }
1073
- if (type !== "message") {
1064
+ return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
1065
+ }
1066
+ function validateMessagePush(o) {
1067
+ if (o.type !== "message") {
1074
1068
  return { ok: false, reason: "\u671F\u671B message" };
1075
1069
  }
1076
1070
  if (!nonEmptyString(o.messageId)) {
@@ -1108,6 +1102,35 @@ function validateAgentWsMessage(value, kind) {
1108
1102
  }
1109
1103
  };
1110
1104
  }
1105
+ function validateCancel(o) {
1106
+ if (o.type !== "cancel") {
1107
+ return { ok: false, reason: "\u671F\u671B cancel" };
1108
+ }
1109
+ if (!nonEmptyString(o.messageId)) {
1110
+ return { ok: false, reason: "cancel \u7F3A\u5C11 messageId" };
1111
+ }
1112
+ return {
1113
+ ok: true,
1114
+ data: { type: "cancel", messageId: o.messageId.trim() }
1115
+ };
1116
+ }
1117
+ function validateAgentWsMessage(value, kind) {
1118
+ if (typeof value !== "object" || value === null) {
1119
+ return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
1120
+ }
1121
+ const o = value;
1122
+ const type = o.type;
1123
+ if (type !== "heartbeat" && type !== "message" && type !== "cancel") {
1124
+ return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
1125
+ }
1126
+ if (kind === "heartbeat" || type === "heartbeat") {
1127
+ return validateHeartbeat(o);
1128
+ }
1129
+ if (type === "cancel") {
1130
+ return validateCancel(o);
1131
+ }
1132
+ return validateMessagePush(o);
1133
+ }
1111
1134
 
1112
1135
  // src/commands/connect/abort-signal-debug.ts
1113
1136
  import {
@@ -1496,6 +1519,54 @@ async function runCursorAgent(cfg, ctx, options) {
1496
1519
  }
1497
1520
  }
1498
1521
 
1522
+ // src/commands/connect/pre-step-cache.ts
1523
+ var PULL_TTL_MS = 3e4;
1524
+ var lastBranchSessionId = null;
1525
+ var lastPullAtBySession = /* @__PURE__ */ new Map();
1526
+ function shouldRunBranch(sessionId) {
1527
+ return lastBranchSessionId !== sessionId;
1528
+ }
1529
+ function markBranchDone(sessionId) {
1530
+ lastBranchSessionId = sessionId;
1531
+ }
1532
+ function shouldRunPull(sessionId) {
1533
+ const last = lastPullAtBySession.get(sessionId);
1534
+ if (last == null) {
1535
+ return true;
1536
+ }
1537
+ return Date.now() - last >= PULL_TTL_MS;
1538
+ }
1539
+ function markPullDone(sessionId) {
1540
+ lastPullAtBySession.set(sessionId, Date.now());
1541
+ }
1542
+
1543
+ // src/commands/connect/run-slot-pool.ts
1544
+ var DEFAULT_MAX_CONCURRENT = 2;
1545
+ function createRunSlotPool(maxConcurrent = DEFAULT_MAX_CONCURRENT) {
1546
+ let active = 0;
1547
+ const waiters = [];
1548
+ const acquire = () => {
1549
+ if (active < maxConcurrent) {
1550
+ active += 1;
1551
+ return Promise.resolve();
1552
+ }
1553
+ return new Promise((resolve5) => {
1554
+ waiters.push(() => {
1555
+ active += 1;
1556
+ resolve5();
1557
+ });
1558
+ });
1559
+ };
1560
+ const release = () => {
1561
+ active = Math.max(0, active - 1);
1562
+ const next = waiters.shift();
1563
+ if (next) {
1564
+ next();
1565
+ }
1566
+ };
1567
+ return { acquire, release };
1568
+ }
1569
+
1499
1570
  // src/commands/connect.ts
1500
1571
  var HEARTBEAT_MS = 3e4;
1501
1572
  async function updateMessageStatus(cfg, messageId, status) {
@@ -1509,55 +1580,62 @@ async function setMessageError(cfg, messageId, error) {
1509
1580
  console.log(`[apm] \u5DF2\u8BBE\u7F6E\u6D88\u606F\u9519\u8BEF: ${messageId}`);
1510
1581
  }
1511
1582
  var SHUTDOWN_DRAIN_MS = 3e3;
1512
- async function handleInboundMessage(cfg, raw, signal) {
1583
+ function isUserCancelled(ctx) {
1584
+ return ctx.perMessageSignal.aborted && !ctx.shutdownSignal.aborted;
1585
+ }
1586
+ async function handleInboundMessage(cfg, msg, signal, ctx) {
1587
+ if (isUserCancelled(ctx)) return;
1513
1588
  if (signal.aborted) return;
1514
- const parsed = parseAgentWsMessage(raw);
1515
- if (parsed === null) {
1516
- console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
1517
- return;
1518
- }
1519
- if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
1520
- return;
1521
- }
1522
- const validated = validateAgentWsMessage(parsed, "outbound");
1523
- if (!validated.ok) {
1524
- console.error(`[apm] \u6536\u5230\u65E0\u6548 message: ${validated.reason}`);
1525
- return;
1526
- }
1527
- if (validated.data.type !== "message") {
1528
- return;
1529
- }
1530
- const msg = validated.data;
1531
1589
  const messageId = msg.messageId;
1532
1590
  const runStep = async (step, fn) => {
1591
+ const startedAt = Date.now();
1533
1592
  try {
1534
- return await fn();
1593
+ const result = await fn();
1594
+ console.log(`[apm] step=${step} elapsed=${Date.now() - startedAt}ms`);
1595
+ return result;
1535
1596
  } catch (err) {
1536
1597
  const detail = err instanceof Error ? err.message : String(err);
1537
1598
  throw new Error(`[${step}] ${detail}`);
1538
1599
  }
1539
1600
  };
1540
1601
  try {
1541
- if (signal.aborted) return;
1542
- await runStep(
1543
- "branch",
1544
- () => runBranch(msg.sessionId, { cwd: msg.workdir })
1545
- );
1546
- if (signal.aborted) return;
1547
- await runStep(
1548
- "pull",
1549
- () => runPull(msg.sessionId, workspaceApmDir(msg.workdir))
1550
- );
1551
- if (signal.aborted) return;
1552
- await runStep(
1553
- "commit-pull",
1554
- () => commitWorkingTreeIfDirty(msg.workdir, "fix: apm pull")
1555
- );
1556
1602
  if (signal.aborted) return;
1557
1603
  await runStep(
1558
1604
  "status-typing",
1559
1605
  () => updateMessageStatus(cfg, messageId, "TYPING")
1560
1606
  );
1607
+ if (shouldRunBranch(msg.sessionId)) {
1608
+ if (signal.aborted) return;
1609
+ await runStep(
1610
+ "branch",
1611
+ () => runBranch(msg.sessionId, { cwd: msg.workdir })
1612
+ );
1613
+ markBranchDone(msg.sessionId);
1614
+ } else {
1615
+ console.log(`[apm] step=branch skipped sessionId=${msg.sessionId}`);
1616
+ }
1617
+ let pullRan = false;
1618
+ if (shouldRunPull(msg.sessionId)) {
1619
+ if (signal.aborted) return;
1620
+ await runStep(
1621
+ "pull",
1622
+ () => runPull(msg.sessionId, workspaceApmDir(msg.workdir))
1623
+ );
1624
+ markPullDone(msg.sessionId);
1625
+ pullRan = true;
1626
+ } else {
1627
+ console.log(`[apm] step=pull skipped sessionId=${msg.sessionId}`);
1628
+ }
1629
+ if (pullRan) {
1630
+ if (signal.aborted) return;
1631
+ await runStep(
1632
+ "commit-pull",
1633
+ () => commitWorkingTreeIfDirty(msg.workdir, "fix: apm pull")
1634
+ );
1635
+ } else {
1636
+ console.log(`[apm] step=commit-pull skipped sessionId=${msg.sessionId}`);
1637
+ }
1638
+ if (signal.aborted) return;
1561
1639
  await runStep(
1562
1640
  "cursor-agent",
1563
1641
  () => runCursorAgent(
@@ -1589,9 +1667,13 @@ async function handleInboundMessage(cfg, raw, signal) {
1589
1667
  () => updateMessageStatus(cfg, messageId, "SUCCESS")
1590
1668
  );
1591
1669
  } catch (err) {
1670
+ if (isUserCancelled(ctx)) {
1671
+ console.log(`[apm] \u6D88\u606F\u5DF2\u7EC8\u6B62 messageId=${messageId}`);
1672
+ return;
1673
+ }
1592
1674
  console.error(
1593
1675
  "[apm] \u5904\u7406\u6D88\u606F\u5931\u8D25:",
1594
- err instanceof Error ? err.message : err
1676
+ err instanceof Error ? err.message : String(err)
1595
1677
  );
1596
1678
  if (err instanceof Error && err.stack) {
1597
1679
  console.error(err.stack);
@@ -1643,7 +1725,10 @@ async function runConnect(options) {
1643
1725
  let stopHeartbeat;
1644
1726
  let shuttingDown = false;
1645
1727
  const shutdownAbort = new AbortController();
1646
- let inboundQueue = Promise.resolve();
1728
+ const runSlots = createRunSlotPool();
1729
+ const activeTasks = /* @__PURE__ */ new Set();
1730
+ const activeRuns = /* @__PURE__ */ new Map();
1731
+ const pendingCancels = /* @__PURE__ */ new Set();
1647
1732
  const shutdown = async (code = 0) => {
1648
1733
  if (shuttingDown) return;
1649
1734
  shuttingDown = true;
@@ -1659,7 +1744,7 @@ async function runConnect(options) {
1659
1744
  }
1660
1745
  try {
1661
1746
  await Promise.race([
1662
- inboundQueue,
1747
+ Promise.all(activeTasks),
1663
1748
  new Promise((r) => setTimeout(r, SHUTDOWN_DRAIN_MS))
1664
1749
  ]);
1665
1750
  } catch {
@@ -1674,9 +1759,58 @@ async function runConnect(options) {
1674
1759
  ws.on("message", (data) => {
1675
1760
  if (shuttingDown) return;
1676
1761
  const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
1677
- inboundQueue = inboundQueue.then(
1678
- () => handleInboundMessage(cfg, text, shutdownAbort.signal)
1679
- );
1762
+ const parsed = parseAgentWsMessage(text);
1763
+ if (parsed === null) {
1764
+ console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
1765
+ return;
1766
+ }
1767
+ if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
1768
+ return;
1769
+ }
1770
+ const validated = validateAgentWsMessage(parsed, "outbound");
1771
+ if (!validated.ok) {
1772
+ console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
1773
+ return;
1774
+ }
1775
+ if (validated.data.type === "cancel") {
1776
+ const { messageId } = validated.data;
1777
+ pendingCancels.add(messageId);
1778
+ activeRuns.get(messageId)?.abort();
1779
+ return;
1780
+ }
1781
+ if (validated.data.type !== "message") {
1782
+ return;
1783
+ }
1784
+ const msg = validated.data;
1785
+ const perMessageController = new AbortController();
1786
+ activeRuns.set(msg.messageId, perMessageController);
1787
+ if (pendingCancels.has(msg.messageId)) {
1788
+ activeRuns.delete(msg.messageId);
1789
+ pendingCancels.delete(msg.messageId);
1790
+ return;
1791
+ }
1792
+ const signal = AbortSignal.any([
1793
+ shutdownAbort.signal,
1794
+ perMessageController.signal
1795
+ ]);
1796
+ const ctx = {
1797
+ shutdownSignal: shutdownAbort.signal,
1798
+ perMessageSignal: perMessageController.signal
1799
+ };
1800
+ const task = (async () => {
1801
+ await runSlots.acquire();
1802
+ try {
1803
+ await handleInboundMessage(cfg, msg, signal, ctx);
1804
+ } finally {
1805
+ runSlots.release();
1806
+ activeRuns.delete(msg.messageId);
1807
+ pendingCancels.delete(msg.messageId);
1808
+ }
1809
+ })();
1810
+ activeTasks.add(task);
1811
+ void task.finally(() => {
1812
+ activeTasks.delete(task);
1813
+ });
1680
1814
  });
1681
1815
  ws.on("close", (code, reason) => {
1682
1816
  console.log(
@@ -2891,7 +3025,10 @@ function buildProgram() {
2891
3025
  program.command("append-message").description("\u5411\u5E73\u53F0\u4F1A\u8BDD\u6D88\u606F\u8FFD\u52A0\u5185\u5BB9\uFF08PUT /api/v1/cli/messages/content\uFF09").requiredOption("--id <messageId>", "\u6D88\u606F ID").requiredOption("--content <content>", "\u8981\u8FFD\u52A0\u7684\u6D88\u606F\u5185\u5BB9").action(async (opts) => {
2892
3026
  await runAppendMessage(opts);
2893
3027
  });
2894
- program.command("update-message-status").description("\u66F4\u65B0\u5E73\u53F0\u4F1A\u8BDD\u6D88\u606F\u72B6\u6001").requiredOption("--id <messageId>", "\u6D88\u606F ID").requiredOption("--status <status>", "CREATED | TYPING | SUCCESS | FAILED").action(async (opts) => {
3028
+ program.command("update-message-status").description("\u66F4\u65B0\u5E73\u53F0\u4F1A\u8BDD\u6D88\u606F\u72B6\u6001").requiredOption("--id <messageId>", "\u6D88\u606F ID").requiredOption(
3029
+ "--status <status>",
3030
+ "CREATED | QUEUED | TYPING | SUCCESS | FAILED | CANCELLED"
3031
+ ).action(async (opts) => {
2895
3032
  await runUpdateMessageStatus(opts);
2896
3033
  });
2897
3034
  program.command("connect").description(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.33",
3
+ "version": "6.0.35",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -2,11 +2,24 @@
2
2
 
3
3
  ### 工作流程
4
4
 
5
- 1. 读取 `.apm/sessions/<会话ID>/session.yaml`,必要时读取`messages.xml` 了解当前会话状态与历史消息
5
+ 按任务轻重选择路径:
6
+
7
+ #### 轻量回复(被 @ 询问、确认、同步进展)
8
+
9
+ 1. 读取 `.apm/sessions/<会话ID>/session.yaml`
10
+ 2. 读取 `.apm/rules/reply.md`
11
+ 3. **立即**用 `apm append-message` 回复(可先简短确认,再补充)
12
+ 4. 按需阅读 `docs/` 下的文档,有进展继续 append-message
13
+
14
+ #### 重任务(开发、写方案、评审、部署)
15
+
16
+ 1. 读取 `.apm/sessions/<会话ID>/session.yaml`,必要时读取 `messages.xml` 了解历史
6
17
  2. 根据你的名字从 `session.yaml` 的 `members` 中找到你对应的 **description**(智能体描述,非人设提示词)
7
18
  3. 根据角色描述完成用户指定的任务
8
- 4. 任务有阶段行进展或者任务完成后必须回复消息(具体见规则 `reply.md`)
9
- 5. 写工作日志(具体见规则 `write_doc.md`)
19
+ 4. 任务有阶段性进展或者任务完成后必须回复消息(具体见规则 `reply.md`)
20
+ 5. 写工作日志(具体见规则 `write_doc.md`)
21
+
22
+ **禁止**在未回复前先读完所有 docs。有进展就先 append-message。
10
23
 
11
24
  ### 目录指引
12
25
 
@@ -23,5 +36,5 @@
23
36
  - 附件列表: `attachments/*`,当有文档中提及附件时,从这里查找
24
37
  - 协作规则: `RULE.md`,在这里可以看到不同成员的协作规则,从而找其他成员协助你一起解决问题,按需阅读
25
38
  - 协作 TODO: `TODO.md`,跨轮次任务清单(待办与已完成项),按需阅读
26
- - 历史消息记录: `message.xml`,历史消息列表,数据量很大,按需阅读
39
+ - 历史消息记录: `messages.xml`,历史消息列表,数据量很大,按需阅读
27
40
  - 初始目标: `TASK.md`,最初的目标,不一定具体,团队成员会有人负责让这个任务变得具体,仅供参考。如果你是相关的角色,则你需要先读取这个目标,然后规划接下来的动作。
@@ -2,14 +2,14 @@
2
2
 
3
3
  1. 如果回复的内容包含文档地址,不要把前缀也写出来,直接用文档名称即可。例如: 我已经把待处理的内容更新到了 `PRD.md` 中,请查阅。
4
4
  2. 回复的内容并非对你工作的总结,要尽可能简洁,不要长篇大论,如果同步的内容确实较多,你可以先写文档,然后指引用户去阅读你写的文档。
5
- 3. 只要你有任何进展都想都可以调用下面的命令进行回复,你可以一直补充内容。
5
+ 3. 只要你有任何进展都可以调用下面的命令进行回复,你可以一直补充内容。**被 @ 提及时,收到后必须先回复一句简短确认(不超过 50 字),再开始读文档或执行任务。**
6
6
  4. 如果有需要指定相关人,必须在消息内容中@指定出来
7
7
 
8
8
  ## 执行回复的方法
9
9
 
10
10
  命令: `apm append-message --id=<消息ID> --content=<你要回复的消息内容>`
11
11
 
12
- 示例: apm append-message --id=xxxxx --content="我已经把待处理的内容更新到了 `PRD.md`,请查阅"
12
+ 示例: apm append-message --id=xxxxx --content="收到,正在分析后端改动范围。"
13
13
 
14
14
  ## 写文档的方法
15
15