ai-project-manage-cli 7.1.13 → 7.1.16

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 CHANGED
@@ -57,6 +57,6 @@ apm daemon start -f
57
57
 
58
58
  使用全局 PM2(未安装时会自动执行 `npm install -g pm2`);同一时刻仅允许一个 connect。启动时会自动检测 CLI 更新并刷新守护配置。
59
59
 
60
- `apm pull` 会自动将平台登记的**仓库项目文档**同步到 `.apm/project/`(含 `manifest.json`)。Agent 修改 `.apm/project/` 下文件后,`apm connect` 处理完消息会自动推回平台。
60
+ `apm pull` 会自动将平台登记的**项目文档**同步到 `.apm/docs/`(含 `manifest.json`)。Agent 修改 `.apm/docs/` 下文件后,`apm connect` 处理完消息会自动推回平台。
61
61
 
62
62
  详见仓库根目录 [docs/CLI.md](../../docs/CLI.md)。
package/dist/index.js CHANGED
@@ -184,6 +184,10 @@ var init_request_config = __esm({
184
184
  method: "GET",
185
185
  path: "/cli/webide/assumptions"
186
186
  }),
187
+ webideListAttachments: defineEndpoint({
188
+ method: "GET",
189
+ path: "/cli/webide/attachments"
190
+ }),
187
191
  webideRecommendPlan: defineEndpoint({
188
192
  method: "PUT",
189
193
  path: "/cli/webide/plan-recommendation"
@@ -212,10 +216,20 @@ var init_request_config = __esm({
212
216
  method: "PUT",
213
217
  path: "/cli/webide/terminals/meta"
214
218
  }),
219
+ acquireSteelBrowser: defineEndpoint({
220
+ method: "POST",
221
+ path: "/cli/steel-browser/acquire"
222
+ }),
215
223
  branchBaseline: defineEndpoint({
216
224
  method: "GET",
217
225
  path: "/cli/tasks/branch-baseline"
218
226
  }),
227
+ projectBaseBranch: defineEndpoint(
228
+ {
229
+ method: "GET",
230
+ path: "/cli/tasks/project-base-branch"
231
+ }
232
+ ),
219
233
  listSessionsForBranchCleanup: defineEndpoint({
220
234
  method: "GET",
221
235
  path: "/cli/sessions/branch-cleanup"
@@ -252,21 +266,21 @@ var init_request_config = __esm({
252
266
  method: "POST",
253
267
  path: "/cli/webide/pull-requests/merge"
254
268
  }),
255
- getRepositoryProjectDocumentManifest: defineEndpoint({
269
+ getProjectDocumentManifest: defineEndpoint({
256
270
  method: "GET",
257
- path: "/cli/repository-project-documents/manifest"
271
+ path: "/cli/project-documents/manifest"
258
272
  }),
259
- listRepositoryProjectDocuments: defineEndpoint({
273
+ listProjectDocuments: defineEndpoint({
260
274
  method: "GET",
261
- path: "/cli/repository-project-documents"
275
+ path: "/cli/project-documents"
262
276
  }),
263
- upsertRepositoryProjectDocument: defineEndpoint({
277
+ upsertProjectDocument: defineEndpoint({
264
278
  method: "PUT",
265
- path: "/cli/repository-project-documents/upsert"
279
+ path: "/cli/project-documents/upsert"
266
280
  }),
267
- removeRepositoryProjectDocument: defineEndpoint({
281
+ removeProjectDocument: defineEndpoint({
268
282
  method: "DELETE",
269
- path: "/cli/repository-project-documents"
283
+ path: "/cli/project-documents"
270
284
  }),
271
285
  createTaskDeployment: defineEndpoint({
272
286
  method: "POST",
@@ -648,11 +662,33 @@ function forceKillProcessTree(pid) {
648
662
  }
649
663
  }
650
664
  }
665
+ async function listProcessTreePids(rootPid) {
666
+ const pids = [rootPid];
667
+ const queue = [rootPid];
668
+ while (queue.length > 0) {
669
+ const parent = queue.shift();
670
+ try {
671
+ const { stdout } = await execFileAsync3("pgrep", ["-P", String(parent)], {
672
+ timeout: 2e3
673
+ });
674
+ for (const line of stdout.split("\n")) {
675
+ const child = Number(line.trim());
676
+ if (child > 0 && !pids.includes(child)) {
677
+ pids.push(child);
678
+ queue.push(child);
679
+ }
680
+ }
681
+ } catch {
682
+ }
683
+ }
684
+ return pids;
685
+ }
651
686
  async function probeListenPorts(pid) {
652
687
  try {
688
+ const treePids = await listProcessTreePids(pid);
653
689
  const { stdout } = await execFileAsync3(
654
690
  "lsof",
655
- ["-nP", `-p${pid}`, "-iTCP", "-sTCP:LISTEN"],
691
+ ["-nP", "-a", `-p${treePids.join(",")}`, "-iTCP", "-sTCP:LISTEN"],
656
692
  { timeout: 3e3, maxBuffer: 1024 * 1024 }
657
693
  );
658
694
  const ports = /* @__PURE__ */ new Set();
@@ -944,23 +980,16 @@ var init_webide_terminal_registry = __esm({
944
980
  if (!session.pid) return;
945
981
  const ports = await probeListenPorts(session.pid);
946
982
  if (ports.length === 0) return;
947
- let changed = false;
948
- for (const p of ports) {
949
- if (!session.ports.includes(p)) {
950
- session.ports.push(p);
951
- changed = true;
952
- }
983
+ const same = ports.length === session.ports.length && ports.every((p, i) => p === session.ports[i]);
984
+ if (same) return;
985
+ session.ports = ports;
986
+ if (!session.primaryUrl && session.ports[0]) {
987
+ session.primaryUrl = `http://127.0.0.1:${session.ports[0]}`;
953
988
  }
954
- if (changed) {
955
- session.ports.sort((a, b) => a - b);
956
- if (!session.primaryUrl && session.ports[0]) {
957
- session.primaryUrl = `http://127.0.0.1:${session.ports[0]}`;
958
- }
959
- if (session.status === "RUNNING" || session.status === "STARTING") {
960
- session.status = "READY";
961
- }
962
- void this.syncStatus(session);
989
+ if (session.status === "RUNNING" || session.status === "STARTING") {
990
+ session.status = "READY";
963
991
  }
992
+ void this.syncStatus(session);
964
993
  }
965
994
  async syncStatus(session) {
966
995
  const api = createApmApiClient(this.cfg);
@@ -1629,7 +1658,7 @@ ${diagnostic ?? ""}
1629
1658
  return { synced: true, repositoryId, configName: config.name };
1630
1659
  }
1631
1660
 
1632
- // src/repository-project-documents-sync.ts
1661
+ // src/project-documents-sync.ts
1633
1662
  init_client();
1634
1663
  init_config();
1635
1664
  import {
@@ -1643,7 +1672,7 @@ import { createHash } from "crypto";
1643
1672
  import { dirname as dirname2, join as join4, relative, sep } from "path";
1644
1673
  var MANIFEST_FILE = "manifest.json";
1645
1674
  function projectDocumentsDir(apmRoot) {
1646
- return join4(apmRoot ?? workspaceApmDir(), "project");
1675
+ return join4(apmRoot ?? workspaceApmDir(), "docs");
1647
1676
  }
1648
1677
  function projectDocumentLocalPath(apmRoot, documentPath) {
1649
1678
  const normalized = normalizeLocalDocumentPath(documentPath);
@@ -1720,41 +1749,65 @@ function diffManifestPaths(remote, local) {
1720
1749
  }
1721
1750
  return { download, deleteLocal };
1722
1751
  }
1723
- async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
1752
+ async function resolveProjectIdForSync(api, workdirPath) {
1753
+ try {
1754
+ const baseline = await api.cli.workspaceBaseline({ workdirPath });
1755
+ const projectId = baseline.projectId?.trim() || null;
1756
+ if (projectId) {
1757
+ return { projectId, diagnostic: null };
1758
+ }
1759
+ const projectIds = baseline.projectIds ?? [];
1760
+ if (projectIds.length === 0) {
1761
+ return {
1762
+ projectId: null,
1763
+ diagnostic: baseline.diagnostic?.message ?? `\u5DE5\u4F5C\u7A7A\u95F4\u672A\u5173\u8054\u9879\u76EE\uFF0C\u65E0\u6CD5\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF08\u76EE\u5F55\uFF1A${workdirPath}\uFF09\u3002
1764
+ \u8BF7\u5728\u5E73\u53F0\u5C06\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u5230\u552F\u4E00\u9879\u76EE\u3002`
1765
+ };
1766
+ }
1767
+ return {
1768
+ projectId: null,
1769
+ diagnostic: `\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u4E86 ${projectIds.length} \u4E2A\u9879\u76EE\uFF0C\u65E0\u6CD5\u786E\u5B9A\u540C\u6B65\u76EE\u6807\u3002
1770
+ \u8BF7\u786E\u4FDD\u8BE5\u5DE5\u4F5C\u7A7A\u95F4\u53EA\u5173\u8054\u4E00\u4E2A\u9879\u76EE\u540E\u518D\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002`
1771
+ };
1772
+ } catch (err) {
1773
+ const detail = err instanceof Error ? err.message : String(err);
1774
+ return {
1775
+ projectId: null,
1776
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
1777
+ };
1778
+ }
1779
+ }
1780
+ async function syncProjectDocumentsPull(workdirPath, apmDir) {
1724
1781
  const empty = {
1725
1782
  synced: false,
1726
- repositoryId: null,
1783
+ projectId: null,
1727
1784
  downloaded: 0,
1728
1785
  deleted: 0
1729
1786
  };
1730
1787
  const cfg = await tryReadApmConfig();
1731
1788
  if (!cfg || !resolveApiKey(cfg)) {
1732
1789
  console.log(
1733
- "[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"
1790
+ "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
1734
1791
  );
1735
1792
  return empty;
1736
1793
  }
1737
1794
  const api = createApmApiClient(cfg);
1738
- const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
1795
+ const { projectId, diagnostic } = await resolveProjectIdForSync(
1739
1796
  api,
1740
1797
  workdirPath
1741
1798
  );
1742
- if (!repositoryId) {
1743
- console.log(
1744
- `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
1745
- ${diagnostic ?? ""}`
1746
- );
1799
+ if (!projectId) {
1800
+ console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
1801
+ ${diagnostic ?? ""}`);
1747
1802
  return empty;
1748
1803
  }
1749
1804
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1750
- const projectDir = projectDocumentsDir(targetApmDir);
1751
- await ensureDirExists(projectDir);
1752
- const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
1805
+ const docsDir = projectDocumentsDir(targetApmDir);
1806
+ await ensureDirExists(docsDir);
1807
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest({ projectId });
1753
1808
  if (!remoteManifest) {
1754
- console.log(
1755
- `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
1756
- );
1757
- return { ...empty, repositoryId };
1809
+ console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
1810
+ return { ...empty, projectId };
1758
1811
  }
1759
1812
  const localManifest = readLocalManifest(targetApmDir);
1760
1813
  const { download, deleteLocal } = diffManifestPaths(
@@ -1763,12 +1816,14 @@ ${diagnostic ?? ""}`
1763
1816
  );
1764
1817
  let downloaded = 0;
1765
1818
  if (download.length > 0) {
1766
- const { list } = await api.cli.listRepositoryProjectDocuments({
1767
- repositoryId,
1819
+ const { list } = await api.cli.listProjectDocuments({
1820
+ projectId,
1768
1821
  paths: download.join(",")
1769
1822
  });
1770
1823
  for (const doc of list) {
1771
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
1824
+ const absPath = toFsPath(
1825
+ projectDocumentLocalPath(targetApmDir, doc.path)
1826
+ );
1772
1827
  await ensureDirExists(dirname2(absPath));
1773
1828
  writeFileSync4(absPath, doc.content, "utf8");
1774
1829
  downloaded += 1;
@@ -1783,34 +1838,34 @@ ${diagnostic ?? ""}`
1783
1838
  }
1784
1839
  }
1785
1840
  writeFileSync4(
1786
- toFsPath(join4(projectDir, MANIFEST_FILE)),
1841
+ toFsPath(join4(docsDir, MANIFEST_FILE)),
1787
1842
  `${JSON.stringify(remoteManifest, null, 2)}
1788
1843
  `,
1789
1844
  "utf8"
1790
1845
  );
1791
1846
  console.log(
1792
- `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1847
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1793
1848
  );
1794
1849
  return {
1795
1850
  synced: true,
1796
- repositoryId,
1851
+ projectId,
1797
1852
  downloaded,
1798
1853
  deleted
1799
1854
  };
1800
1855
  }
1801
- async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1856
+ async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1802
1857
  const api = createApmApiClient(cfg);
1803
- const { repositoryId } = await resolveRepositoryIdForSync(api, workdirPath);
1804
- if (!repositoryId) {
1858
+ const { projectId } = await resolveProjectIdForSync(api, workdirPath);
1859
+ if (!projectId) {
1805
1860
  return 0;
1806
1861
  }
1807
1862
  const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
1808
1863
  const localPaths = listLocalDocumentPaths(targetApmDir);
1809
1864
  if (localPaths.length === 0) {
1810
- console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1865
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1811
1866
  return 0;
1812
1867
  }
1813
- const remoteManifest = (await api.cli.getRepositoryProjectDocumentManifest({ repositoryId })).manifest ?? null;
1868
+ const remoteManifest = (await api.cli.getProjectDocumentManifest({ projectId })).manifest ?? null;
1814
1869
  const remoteHashByPath = new Map(
1815
1870
  (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1816
1871
  );
@@ -1828,17 +1883,17 @@ async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1828
1883
  if (remoteHashByPath.get(path19) === contentHash) {
1829
1884
  continue;
1830
1885
  }
1831
- await api.cli.upsertRepositoryProjectDocument({
1832
- repositoryId,
1886
+ await api.cli.upsertProjectDocument({
1887
+ projectId,
1833
1888
  path: path19,
1834
1889
  content,
1835
1890
  description: remoteDescriptionByPath.get(path19) ?? void 0
1836
1891
  });
1837
1892
  synced += 1;
1838
- console.log(`[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: ${path19}`);
1893
+ console.log(`[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: ${path19}`);
1839
1894
  }
1840
1895
  if (synced === 0) {
1841
- console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1896
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1842
1897
  }
1843
1898
  return synced;
1844
1899
  }
@@ -1859,7 +1914,7 @@ async function ensureWorkspaceInitialized(workdir, options) {
1859
1914
  const apmDir = workspaceApmDir(workdir);
1860
1915
  await copyTemplateFiles(apmDir, workdir);
1861
1916
  const syncResult = await syncRemoteDeploymentConfig(workdir, apmDir);
1862
- await syncRepositoryProjectDocumentsPull(workdir, apmDir);
1917
+ await syncProjectDocumentsPull(workdir, apmDir);
1863
1918
  const trimmedName = options?.name?.trim();
1864
1919
  if (trimmedName) {
1865
1920
  const apmConfigPath = toFsPath(join5(apmDir, "apm.config.json"));
@@ -2449,7 +2504,9 @@ import { join as join6 } from "path";
2449
2504
  var MANIFEST_FILE2 = ".sync-manifest.json";
2450
2505
  async function downloadAttachment(cfg, attachmentId) {
2451
2506
  const base = cfg.baseUrl.trim().replace(/\/+$/, "");
2452
- const url = `${base}/api/v1/tasks/attachments/file?${new URLSearchParams({ attachmentId })}`;
2507
+ const url = `${base}/api/v1/tasks/attachments/file?${new URLSearchParams({
2508
+ attachmentId
2509
+ })}`;
2453
2510
  const res = await fetch(url);
2454
2511
  if (!res.ok) {
2455
2512
  throw new Error(
@@ -2488,24 +2545,23 @@ function isAttachmentUpToDate(entry, item, dest) {
2488
2545
  const createdAt = item.createdAt ?? "";
2489
2546
  return entry.createdAt === createdAt;
2490
2547
  }
2491
- async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
2492
- const dir = join6(sessionDir(sessionId, apmRoot), SESSION_ATTACHMENTS_SUBDIR);
2548
+ async function syncAttachmentsToDirectory(cfg, attachments, dir, logLabel) {
2493
2549
  await ensureDirExists(dir);
2494
2550
  if (attachments.length === 0) {
2495
2551
  saveManifest(dir, { version: 1, attachments: {} });
2496
- return;
2552
+ return [];
2497
2553
  }
2498
2554
  const manifest = loadManifest(dir);
2499
2555
  const nextManifest = { version: 1, attachments: {} };
2556
+ const names = [];
2500
2557
  for (const item of attachments) {
2501
2558
  const dest = join6(dir, item.name);
2502
2559
  const entry = manifest.attachments[item.id];
2503
2560
  const createdAt = item.createdAt ?? "";
2561
+ names.push(item.name);
2504
2562
  if (isAttachmentUpToDate(entry, item, dest)) {
2505
2563
  nextManifest.attachments[item.id] = entry;
2506
- console.log(
2507
- `[apm] \u9644\u4EF6\u65E0\u53D8\u5316\uFF0C\u5DF2\u8DF3\u8FC7: ${SESSION_ATTACHMENTS_SUBDIR}/${item.name}`
2508
- );
2564
+ console.log(`[apm] \u9644\u4EF6\u65E0\u53D8\u5316\uFF0C\u5DF2\u8DF3\u8FC7: ${logLabel}/${item.name}`);
2509
2565
  continue;
2510
2566
  }
2511
2567
  const buffer = await downloadAttachment(cfg, item.id);
@@ -2514,9 +2570,19 @@ async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
2514
2570
  name: item.name,
2515
2571
  createdAt
2516
2572
  };
2517
- console.log(`[apm] \u5DF2\u4E0B\u8F7D\u9644\u4EF6: ${SESSION_ATTACHMENTS_SUBDIR}/${item.name}`);
2573
+ console.log(`[apm] \u5DF2\u4E0B\u8F7D\u9644\u4EF6: ${logLabel}/${item.name}`);
2518
2574
  }
2519
2575
  saveManifest(dir, nextManifest);
2576
+ return names;
2577
+ }
2578
+ async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
2579
+ const dir = join6(sessionDir(sessionId, apmRoot), SESSION_ATTACHMENTS_SUBDIR);
2580
+ return syncAttachmentsToDirectory(
2581
+ cfg,
2582
+ attachments,
2583
+ dir,
2584
+ SESSION_ATTACHMENTS_SUBDIR
2585
+ );
2520
2586
  }
2521
2587
 
2522
2588
  // src/rules-sync.ts
@@ -2773,7 +2839,7 @@ async function runPull(sessionId, remoteWorkdir) {
2773
2839
  await syncSessionAttachments(cfg, trimmedId, attachments, apmRoot);
2774
2840
  await syncPlatformRules(cfg, trimmedId, workdir, apmRoot);
2775
2841
  await syncRemoteDeploymentConfig(workdir, apmRoot);
2776
- await syncRepositoryProjectDocumentsPull(workdir, apmRoot);
2842
+ await syncProjectDocumentsPull(workdir, apmRoot);
2777
2843
  console.log(`[apm] \u5DF2\u540C\u6B65\u4F1A\u8BDD\u5DE5\u4F5C\u533A: ${dir}`);
2778
2844
  return dir;
2779
2845
  }
@@ -3026,17 +3092,21 @@ async function runSyncDeployConfig() {
3026
3092
  }
3027
3093
 
3028
3094
  // src/commands/sync-project-documents.ts
3095
+ init_config();
3029
3096
  async function runSyncProjectDocuments(options) {
3030
- const pull = options?.pull ?? !options?.push;
3031
- const push = options?.push ?? false;
3032
3097
  const workdir = resolveWorkdirPath();
3033
3098
  const apmRoot = workspaceApmDir(workdir);
3034
- if (pull) {
3035
- await syncRepositoryProjectDocumentsPull(workdir, apmRoot);
3099
+ const doPush = Boolean(options.push);
3100
+ const doPull = Boolean(options.pull) || !doPush;
3101
+ if (doPull) {
3102
+ await syncProjectDocumentsPull(workdir, apmRoot);
3036
3103
  }
3037
- if (push) {
3038
- const cfg = await ensureLoggedConfig();
3039
- await syncRepositoryProjectDocumentsPush(cfg, workdir, apmRoot);
3104
+ if (doPush) {
3105
+ const cfg = await tryReadApmConfig();
3106
+ if (!cfg) {
3107
+ throw new Error("[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8BF7\u5148\u6267\u884C apm login");
3108
+ }
3109
+ await syncProjectDocumentsPush(cfg, workdir, apmRoot);
3040
3110
  }
3041
3111
  }
3042
3112
 
@@ -7491,7 +7561,7 @@ function createRecommendPlanTool(options) {
7491
7561
  function createUpsertWebIdePlanTool(options) {
7492
7562
  const { cfg, taskId } = options;
7493
7563
  return {
7494
- description: "Persist the implementation plan markdown for this WebIDE task and mark the plan as ready for human confirmation. Call once with the full plan document.",
7564
+ description: "Persist or replace the implementation plan markdown for this WebIDE task and mark it ready for human confirmation. Call with the full plan document (not a diff). Use after generating or revising the plan.",
7495
7565
  inputSchema: {
7496
7566
  type: "object",
7497
7567
  properties: {
@@ -7878,8 +7948,8 @@ async function obtainAgent(ctx) {
7878
7948
  settingSources: ["project"]
7879
7949
  } : {}
7880
7950
  },
7881
- ...ctx.mode ? { mode: ctx.mode } : {}
7882
- // mcpServers: createPlaywrightMcpServers(),
7951
+ ...ctx.mode ? { mode: ctx.mode } : {},
7952
+ ...ctx.mcpServers ? { mcpServers: ctx.mcpServers } : {}
7883
7953
  };
7884
7954
  const explicitAgentId = ctx.resumeAgentId?.trim();
7885
7955
  const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
@@ -7976,7 +8046,6 @@ async function runCursorAgent(cfg, ctx, options) {
7976
8046
  try {
7977
8047
  const run = await agent.send(prompt, {
7978
8048
  ...ctx.mode ? { mode: ctx.mode } : {},
7979
- // mcpServers: createPlaywrightMcpServers(),
7980
8049
  local: {
7981
8050
  ...options?.forceSend ? { force: true } : {},
7982
8051
  customTools
@@ -8409,7 +8478,7 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
8409
8478
  if (!pullRan) {
8410
8479
  await runStep(
8411
8480
  "sync-project-documents-pull",
8412
- () => syncRepositoryProjectDocumentsPull(workdir, apmRoot)
8481
+ () => syncProjectDocumentsPull(workdir, apmRoot)
8413
8482
  );
8414
8483
  }
8415
8484
  const agentResult = await runStep(
@@ -8442,7 +8511,7 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
8442
8511
  );
8443
8512
  await runStep(
8444
8513
  "sync-project-documents",
8445
- () => syncRepositoryProjectDocumentsPush(cfg, workdir, apmRoot)
8514
+ () => syncProjectDocumentsPush(cfg, workdir, apmRoot)
8446
8515
  );
8447
8516
  await runStep(
8448
8517
  "commit-files",
@@ -9979,12 +10048,12 @@ function buildProgram() {
9979
10048
  await runSyncDeployConfig();
9980
10049
  });
9981
10050
  program.command("sync-project-documents").description(
9982
- "\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\uFF1A\u9ED8\u8BA4 pull \u5230 .apm/project/\uFF1B\u52A0 --push \u5C06\u672C\u5730\u53D8\u66F4\u63A8\u9001\u5230\u5E73\u53F0"
9983
- ).option("--push", "\u63A8\u9001\u672C\u5730 .apm/project/ \u5230\u5E73\u53F0").option("--pull", "\u4ECE\u5E73\u53F0\u62C9\u53D6\u5230 .apm/project/").action(async (opts) => {
10051
+ "\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF1A\u9ED8\u8BA4 pull \u5230 .apm/docs/\uFF1B\u52A0 --push \u5C06\u672C\u5730\u53D8\u66F4\u63A8\u9001\u5230\u5E73\u53F0"
10052
+ ).option("--push", "\u63A8\u9001\u672C\u5730 .apm/docs/ \u5230\u5E73\u53F0").option("--pull", "\u4ECE\u5E73\u53F0\u62C9\u53D6\u5230 .apm/docs/").action(async (opts) => {
9984
10053
  await runSyncProjectDocuments(opts);
9985
10054
  });
9986
10055
  program.command("pull").description(
9987
- "\u62C9\u53D6\u6C9F\u901A\u7FA4\u6570\u636E\u5230 .apm/sessions/<sessionId>/\uFF0C\u5E76\u540C\u6B65\u90E8\u7F72\u914D\u7F6E\u4E0E\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u5230 .apm/project/"
10056
+ "\u62C9\u53D6\u6C9F\u901A\u7FA4\u6570\u636E\u5230 .apm/sessions/<sessionId>/\uFF0C\u5E76\u540C\u6B65\u90E8\u7F72\u914D\u7F6E\u4E0E\u9879\u76EE\u6587\u6863\u5230 .apm/docs/"
9988
10057
  ).argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").action(async (sessionId) => {
9989
10058
  await runPull(sessionId);
9990
10059
  });
@@ -88,6 +88,10 @@ var requestConfig = {
88
88
  method: "GET",
89
89
  path: "/cli/webide/assumptions"
90
90
  }),
91
+ webideListAttachments: defineEndpoint({
92
+ method: "GET",
93
+ path: "/cli/webide/attachments"
94
+ }),
91
95
  webideRecommendPlan: defineEndpoint({
92
96
  method: "PUT",
93
97
  path: "/cli/webide/plan-recommendation"
@@ -116,10 +120,20 @@ var requestConfig = {
116
120
  method: "PUT",
117
121
  path: "/cli/webide/terminals/meta"
118
122
  }),
123
+ acquireSteelBrowser: defineEndpoint({
124
+ method: "POST",
125
+ path: "/cli/steel-browser/acquire"
126
+ }),
119
127
  branchBaseline: defineEndpoint({
120
128
  method: "GET",
121
129
  path: "/cli/tasks/branch-baseline"
122
130
  }),
131
+ projectBaseBranch: defineEndpoint(
132
+ {
133
+ method: "GET",
134
+ path: "/cli/tasks/project-base-branch"
135
+ }
136
+ ),
123
137
  listSessionsForBranchCleanup: defineEndpoint({
124
138
  method: "GET",
125
139
  path: "/cli/sessions/branch-cleanup"
@@ -156,21 +170,21 @@ var requestConfig = {
156
170
  method: "POST",
157
171
  path: "/cli/webide/pull-requests/merge"
158
172
  }),
159
- getRepositoryProjectDocumentManifest: defineEndpoint({
173
+ getProjectDocumentManifest: defineEndpoint({
160
174
  method: "GET",
161
- path: "/cli/repository-project-documents/manifest"
175
+ path: "/cli/project-documents/manifest"
162
176
  }),
163
- listRepositoryProjectDocuments: defineEndpoint({
177
+ listProjectDocuments: defineEndpoint({
164
178
  method: "GET",
165
- path: "/cli/repository-project-documents"
179
+ path: "/cli/project-documents"
166
180
  }),
167
- upsertRepositoryProjectDocument: defineEndpoint({
181
+ upsertProjectDocument: defineEndpoint({
168
182
  method: "PUT",
169
- path: "/cli/repository-project-documents/upsert"
183
+ path: "/cli/project-documents/upsert"
170
184
  }),
171
- removeRepositoryProjectDocument: defineEndpoint({
185
+ removeProjectDocument: defineEndpoint({
172
186
  method: "DELETE",
173
- path: "/cli/repository-project-documents"
187
+ path: "/cli/project-documents"
174
188
  }),
175
189
  createTaskDeployment: defineEndpoint({
176
190
  method: "POST",
@@ -207,6 +221,7 @@ import { homedir } from "os";
207
221
  import { join } from "path";
208
222
  var APM_CONFIG_DIR = join(homedir(), ".config", "apm");
209
223
  var APM_CONFIG_PATH = join(APM_CONFIG_DIR, "config.json");
224
+ var DEFAULT_BASE_URL = "http://127.0.0.1:3000";
210
225
  function resolveClientMachineId(cfg) {
211
226
  return (cfg.clientMachineId ?? cfg.userId ?? "").trim();
212
227
  }
@@ -237,6 +252,33 @@ async function tryReadApmConfig() {
237
252
  return null;
238
253
  }
239
254
  }
255
+ function defaultApmConfig() {
256
+ return {
257
+ baseUrl: DEFAULT_BASE_URL,
258
+ clientMachineId: "",
259
+ apiKey: ""
260
+ };
261
+ }
262
+ async function ensureApmConfig() {
263
+ const existing = await tryReadApmConfig();
264
+ if (existing) return existing;
265
+ const defaults = defaultApmConfig();
266
+ await writeApmConfig(defaults);
267
+ return defaults;
268
+ }
269
+ async function writeApmConfig(cfg) {
270
+ const clientMachineId = resolveClientMachineId(cfg);
271
+ const apiKey = resolveApiKey(cfg);
272
+ const normalized = {
273
+ baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
274
+ apiKey,
275
+ ...clientMachineId ? { clientMachineId } : {}
276
+ };
277
+ mkdirSync(APM_CONFIG_DIR, { recursive: true });
278
+ writeFileSync(APM_CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", {
279
+ mode: 384
280
+ });
281
+ }
240
282
 
241
283
  // src/api/client.ts
242
284
  function createApmApiClient(cfg) {
@@ -375,24 +417,6 @@ async function ensureRemoteBaselineBranch(cwd, baselineBranch) {
375
417
  `[apm] \u8FDC\u7A0B\u4E0D\u5B58\u5728\u57FA\u7EBF\u5206\u652F origin/${baselineBranch}\uFF0C\u8BF7\u786E\u8BA4\u4ED3\u5E93\u9ED8\u8BA4\u5206\u652F\u5DF2\u63A8\u9001\u5230 origin`
376
418
  );
377
419
  }
378
- async function resolveDefaultRemoteBranch(cwd) {
379
- try {
380
- const ref = (await execGit(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], true)).trim();
381
- const match = ref.match(/^refs\/remotes\/origin\/(.+)$/);
382
- if (match?.[1]) {
383
- return match[1];
384
- }
385
- } catch {
386
- }
387
- for (const candidate of ["main", "master"]) {
388
- if (await remoteBranchExists(cwd, candidate)) {
389
- return candidate;
390
- }
391
- }
392
- throw new Error(
393
- "[apm] \u65E0\u6CD5\u786E\u5B9A\u8FDC\u7A0B\u9ED8\u8BA4\u5206\u652F\uFF08origin/HEAD\u3001main\u3001master \u5747\u4E0D\u53EF\u7528\uFF09"
394
- );
395
- }
396
420
  async function hasUpstream(cwd) {
397
421
  try {
398
422
  await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
@@ -573,6 +597,24 @@ function assertApmGitignoredInRepo(workdir) {
573
597
  );
574
598
  }
575
599
  }
600
+ var SESSION_ATTACHMENTS_SUBDIR = "attachments";
601
+ var WEBIDE_SUBDIR = "webide";
602
+ function webideTaskDir(taskId, workdir) {
603
+ return join2(workdir, ".apm", WEBIDE_SUBDIR, taskId);
604
+ }
605
+ function webideAttachmentsDir(taskId, workdir) {
606
+ return join2(webideTaskDir(taskId, workdir), SESSION_ATTACHMENTS_SUBDIR);
607
+ }
608
+ async function ensureLoggedConfig() {
609
+ const cfg = await ensureApmConfig();
610
+ if (!resolveApiKey(cfg)) {
611
+ console.error(
612
+ "[apm] \u672A\u68C0\u6D4B\u5230 API Key\uFF0C\u8BF7\u5148\u6267\u884C: apm login --api-key cm_xxx"
613
+ );
614
+ process.exit(1);
615
+ }
616
+ return cfg;
617
+ }
576
618
  async function ensureDirExists(dir) {
577
619
  mkdirSync2(dir, { recursive: true });
578
620
  }
@@ -977,6 +1019,17 @@ async function runTaskBranch(taskId, options = {}) {
977
1019
  if (!trimmedTaskId) {
978
1020
  throw new Error("[apm] taskId \u4E0D\u80FD\u4E3A\u7A7A");
979
1021
  }
1022
+ const cfg = await ensureLoggedConfig();
1023
+ const api = createApmApiClient(cfg);
1024
+ const projectBaseline = await api.cli.projectBaseBranch({
1025
+ taskId: trimmedTaskId
1026
+ });
1027
+ const baselineBranch = projectBaseline.baseBranch?.trim() ?? "";
1028
+ if (!baselineBranch) {
1029
+ throw new Error(
1030
+ "[apm] \u9879\u76EE\u672A\u914D\u7F6E\u57FA\u7840\u5206\u652F\uFF0C\u65E0\u6CD5\u521B\u5EFA WebIDE \u7279\u6027\u5206\u652F"
1031
+ );
1032
+ }
980
1033
  const cwd = options.cwd ?? process.cwd();
981
1034
  const workdirPath = resolveWorkdirPath(cwd);
982
1035
  const manifest = loadWorkspaceReposCache(workdirPath);
@@ -984,9 +1037,8 @@ async function runTaskBranch(taskId, options = {}) {
984
1037
  const branch = branchNameForTask(trimmedTaskId);
985
1038
  for (const gitRoot of repoRoots) {
986
1039
  const label = formatRepoLabel(workdirPath, gitRoot);
987
- const baselineBranch = await resolveDefaultRemoteBranch(gitRoot);
988
1040
  console.log(
989
- `[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (baseline=${baselineBranch})`
1041
+ `[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (project baseline=${baselineBranch})`
990
1042
  );
991
1043
  await ensureFeatureBranch(branch, baselineBranch, {
992
1044
  ...options,
@@ -1643,7 +1695,7 @@ function createRecommendPlanTool(options) {
1643
1695
  function createUpsertWebIdePlanTool(options) {
1644
1696
  const { cfg, taskId } = options;
1645
1697
  return {
1646
- description: "Persist the implementation plan markdown for this WebIDE task and mark the plan as ready for human confirmation. Call once with the full plan document.",
1698
+ description: "Persist or replace the implementation plan markdown for this WebIDE task and mark it ready for human confirmation. Call with the full plan document (not a diff). Use after generating or revising the plan.",
1647
1699
  inputSchema: {
1648
1700
  type: "object",
1649
1701
  properties: {
@@ -2028,8 +2080,8 @@ async function obtainAgent(ctx) {
2028
2080
  settingSources: ["project"]
2029
2081
  } : {}
2030
2082
  },
2031
- ...ctx.mode ? { mode: ctx.mode } : {}
2032
- // mcpServers: createPlaywrightMcpServers(),
2083
+ ...ctx.mode ? { mode: ctx.mode } : {},
2084
+ ...ctx.mcpServers ? { mcpServers: ctx.mcpServers } : {}
2033
2085
  };
2034
2086
  const explicitAgentId = ctx.resumeAgentId?.trim();
2035
2087
  const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
@@ -2126,7 +2178,6 @@ async function runCursorAgent(cfg, ctx, options) {
2126
2178
  try {
2127
2179
  const run = await agent.send(prompt, {
2128
2180
  ...ctx.mode ? { mode: ctx.mode } : {},
2129
- // mcpServers: createPlaywrightMcpServers(),
2130
2181
  local: {
2131
2182
  ...options?.forceSend ? { force: true } : {},
2132
2183
  customTools
@@ -2622,7 +2673,7 @@ ${diagnostic ?? ""}
2622
2673
  return { synced: true, repositoryId, configName: config.name };
2623
2674
  }
2624
2675
 
2625
- // src/repository-project-documents-sync.ts
2676
+ // src/project-documents-sync.ts
2626
2677
  import {
2627
2678
  existsSync as existsSync5,
2628
2679
  readdirSync as readdirSync3,
@@ -2633,7 +2684,7 @@ import {
2633
2684
  import { dirname as dirname4, join as join6, relative as relative2, sep } from "path";
2634
2685
  var MANIFEST_FILE = "manifest.json";
2635
2686
  function projectDocumentsDir(apmRoot) {
2636
- return join6(apmRoot ?? workspaceApmDir(), "project");
2687
+ return join6(apmRoot ?? workspaceApmDir(), "docs");
2637
2688
  }
2638
2689
  function projectDocumentLocalPath(apmRoot, documentPath) {
2639
2690
  const normalized = normalizeLocalDocumentPath(documentPath);
@@ -2684,41 +2735,65 @@ function diffManifestPaths(remote, local) {
2684
2735
  }
2685
2736
  return { download, deleteLocal };
2686
2737
  }
2687
- async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
2738
+ async function resolveProjectIdForSync(api, workdirPath) {
2739
+ try {
2740
+ const baseline = await api.cli.workspaceBaseline({ workdirPath });
2741
+ const projectId = baseline.projectId?.trim() || null;
2742
+ if (projectId) {
2743
+ return { projectId, diagnostic: null };
2744
+ }
2745
+ const projectIds = baseline.projectIds ?? [];
2746
+ if (projectIds.length === 0) {
2747
+ return {
2748
+ projectId: null,
2749
+ diagnostic: baseline.diagnostic?.message ?? `\u5DE5\u4F5C\u7A7A\u95F4\u672A\u5173\u8054\u9879\u76EE\uFF0C\u65E0\u6CD5\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF08\u76EE\u5F55\uFF1A${workdirPath}\uFF09\u3002
2750
+ \u8BF7\u5728\u5E73\u53F0\u5C06\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u5230\u552F\u4E00\u9879\u76EE\u3002`
2751
+ };
2752
+ }
2753
+ return {
2754
+ projectId: null,
2755
+ diagnostic: `\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u4E86 ${projectIds.length} \u4E2A\u9879\u76EE\uFF0C\u65E0\u6CD5\u786E\u5B9A\u540C\u6B65\u76EE\u6807\u3002
2756
+ \u8BF7\u786E\u4FDD\u8BE5\u5DE5\u4F5C\u7A7A\u95F4\u53EA\u5173\u8054\u4E00\u4E2A\u9879\u76EE\u540E\u518D\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002`
2757
+ };
2758
+ } catch (err) {
2759
+ const detail = err instanceof Error ? err.message : String(err);
2760
+ return {
2761
+ projectId: null,
2762
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
2763
+ };
2764
+ }
2765
+ }
2766
+ async function syncProjectDocumentsPull(workdirPath, apmDir) {
2688
2767
  const empty = {
2689
2768
  synced: false,
2690
- repositoryId: null,
2769
+ projectId: null,
2691
2770
  downloaded: 0,
2692
2771
  deleted: 0
2693
2772
  };
2694
2773
  const cfg = await tryReadApmConfig();
2695
2774
  if (!cfg || !resolveApiKey(cfg)) {
2696
2775
  console.log(
2697
- "[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"
2776
+ "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
2698
2777
  );
2699
2778
  return empty;
2700
2779
  }
2701
2780
  const api = createApmApiClient(cfg);
2702
- const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
2781
+ const { projectId, diagnostic } = await resolveProjectIdForSync(
2703
2782
  api,
2704
2783
  workdirPath
2705
2784
  );
2706
- if (!repositoryId) {
2707
- console.log(
2708
- `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
2709
- ${diagnostic ?? ""}`
2710
- );
2785
+ if (!projectId) {
2786
+ console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
2787
+ ${diagnostic ?? ""}`);
2711
2788
  return empty;
2712
2789
  }
2713
2790
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
2714
- const projectDir = projectDocumentsDir(targetApmDir);
2715
- await ensureDirExists(projectDir);
2716
- const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
2791
+ const docsDir = projectDocumentsDir(targetApmDir);
2792
+ await ensureDirExists(docsDir);
2793
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest({ projectId });
2717
2794
  if (!remoteManifest) {
2718
- console.log(
2719
- `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
2720
- );
2721
- return { ...empty, repositoryId };
2795
+ console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
2796
+ return { ...empty, projectId };
2722
2797
  }
2723
2798
  const localManifest = readLocalManifest(targetApmDir);
2724
2799
  const { download, deleteLocal } = diffManifestPaths(
@@ -2727,12 +2802,14 @@ ${diagnostic ?? ""}`
2727
2802
  );
2728
2803
  let downloaded = 0;
2729
2804
  if (download.length > 0) {
2730
- const { list } = await api.cli.listRepositoryProjectDocuments({
2731
- repositoryId,
2805
+ const { list } = await api.cli.listProjectDocuments({
2806
+ projectId,
2732
2807
  paths: download.join(",")
2733
2808
  });
2734
2809
  for (const doc of list) {
2735
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
2810
+ const absPath = toFsPath(
2811
+ projectDocumentLocalPath(targetApmDir, doc.path)
2812
+ );
2736
2813
  await ensureDirExists(dirname4(absPath));
2737
2814
  writeFileSync7(absPath, doc.content, "utf8");
2738
2815
  downloaded += 1;
@@ -2747,17 +2824,17 @@ ${diagnostic ?? ""}`
2747
2824
  }
2748
2825
  }
2749
2826
  writeFileSync7(
2750
- toFsPath(join6(projectDir, MANIFEST_FILE)),
2827
+ toFsPath(join6(docsDir, MANIFEST_FILE)),
2751
2828
  `${JSON.stringify(remoteManifest, null, 2)}
2752
2829
  `,
2753
2830
  "utf8"
2754
2831
  );
2755
2832
  console.log(
2756
- `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
2833
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
2757
2834
  );
2758
2835
  return {
2759
2836
  synced: true,
2760
- repositoryId,
2837
+ projectId,
2761
2838
  downloaded,
2762
2839
  deleted
2763
2840
  };
@@ -2779,7 +2856,7 @@ async function ensureWorkspaceInitialized(workdir, options) {
2779
2856
  const apmDir = workspaceApmDir(workdir);
2780
2857
  await copyTemplateFiles(apmDir, workdir);
2781
2858
  const syncResult = await syncRemoteDeploymentConfig(workdir, apmDir);
2782
- await syncRepositoryProjectDocumentsPull(workdir, apmDir);
2859
+ await syncProjectDocumentsPull(workdir, apmDir);
2783
2860
  const trimmedName = options?.name?.trim();
2784
2861
  if (trimmedName) {
2785
2862
  const apmConfigPath = toFsPath(join7(apmDir, "apm.config.json"));
@@ -3024,21 +3101,108 @@ async function syncWorkspaceSkills(cfg, workdir) {
3024
3101
  );
3025
3102
  }
3026
3103
 
3027
- // src/commands/connect/cli-version-sync.ts
3104
+ // src/commands/sync-session-attachments.ts
3028
3105
  import { existsSync as existsSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
3029
3106
  import { join as join11 } from "path";
3107
+ var MANIFEST_FILE2 = ".sync-manifest.json";
3108
+ async function downloadAttachment(cfg, attachmentId) {
3109
+ const base = cfg.baseUrl.trim().replace(/\/+$/, "");
3110
+ const url = `${base}/api/v1/tasks/attachments/file?${new URLSearchParams({
3111
+ attachmentId
3112
+ })}`;
3113
+ const res = await fetch(url);
3114
+ if (!res.ok) {
3115
+ throw new Error(
3116
+ `[apm] \u4E0B\u8F7D\u9644\u4EF6\u5931\u8D25 (${res.status}): attachmentId=${attachmentId}`
3117
+ );
3118
+ }
3119
+ return Buffer.from(await res.arrayBuffer());
3120
+ }
3121
+ function loadManifest(dir) {
3122
+ const path = join11(dir, MANIFEST_FILE2);
3123
+ if (!existsSync8(path)) {
3124
+ return { version: 1, attachments: {} };
3125
+ }
3126
+ try {
3127
+ const parsed = JSON.parse(
3128
+ readFileSync9(path, "utf8")
3129
+ );
3130
+ if (parsed?.version === 1 && parsed.attachments && typeof parsed.attachments === "object") {
3131
+ return parsed;
3132
+ }
3133
+ } catch {
3134
+ }
3135
+ return { version: 1, attachments: {} };
3136
+ }
3137
+ function saveManifest(dir, manifest) {
3138
+ writeFileSync10(
3139
+ join11(dir, MANIFEST_FILE2),
3140
+ `${JSON.stringify(manifest, null, 2)}
3141
+ `,
3142
+ "utf8"
3143
+ );
3144
+ }
3145
+ function isAttachmentUpToDate(entry, item, dest) {
3146
+ if (!entry || !existsSync8(dest)) return false;
3147
+ if (entry.name !== item.name) return false;
3148
+ const createdAt = item.createdAt ?? "";
3149
+ return entry.createdAt === createdAt;
3150
+ }
3151
+ async function syncAttachmentsToDirectory(cfg, attachments, dir, logLabel) {
3152
+ await ensureDirExists(dir);
3153
+ if (attachments.length === 0) {
3154
+ saveManifest(dir, { version: 1, attachments: {} });
3155
+ return [];
3156
+ }
3157
+ const manifest = loadManifest(dir);
3158
+ const nextManifest = { version: 1, attachments: {} };
3159
+ const names = [];
3160
+ for (const item of attachments) {
3161
+ const dest = join11(dir, item.name);
3162
+ const entry = manifest.attachments[item.id];
3163
+ const createdAt = item.createdAt ?? "";
3164
+ names.push(item.name);
3165
+ if (isAttachmentUpToDate(entry, item, dest)) {
3166
+ nextManifest.attachments[item.id] = entry;
3167
+ console.log(`[apm] \u9644\u4EF6\u65E0\u53D8\u5316\uFF0C\u5DF2\u8DF3\u8FC7: ${logLabel}/${item.name}`);
3168
+ continue;
3169
+ }
3170
+ const buffer = await downloadAttachment(cfg, item.id);
3171
+ writeFileSync10(dest, buffer);
3172
+ nextManifest.attachments[item.id] = {
3173
+ name: item.name,
3174
+ createdAt
3175
+ };
3176
+ console.log(`[apm] \u5DF2\u4E0B\u8F7D\u9644\u4EF6: ${logLabel}/${item.name}`);
3177
+ }
3178
+ saveManifest(dir, nextManifest);
3179
+ return names;
3180
+ }
3181
+ async function syncWebIdeAttachments(cfg, taskId, workdir, attachments) {
3182
+ const dir = webideAttachmentsDir(taskId, workdir);
3183
+ return syncAttachmentsToDirectory(
3184
+ cfg,
3185
+ attachments,
3186
+ dir,
3187
+ `.apm/webide/${taskId}/attachments`
3188
+ );
3189
+ }
3190
+
3191
+ // src/commands/connect/cli-version-sync.ts
3192
+ import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync11 } from "fs";
3193
+ import { join as join12 } from "path";
3030
3194
  var CLI_VERSION_FILE = ".cli-version.json";
3031
3195
  function manifestPath2(apmDir) {
3032
- return join11(apmDir, CLI_VERSION_FILE);
3196
+ return join12(apmDir, CLI_VERSION_FILE);
3033
3197
  }
3034
- function loadManifest(apmDir) {
3198
+ function loadManifest2(apmDir) {
3035
3199
  const path = toFsPath(manifestPath2(apmDir));
3036
- if (!existsSync8(path)) {
3200
+ if (!existsSync9(path)) {
3037
3201
  return null;
3038
3202
  }
3039
3203
  try {
3040
3204
  const parsed = JSON.parse(
3041
- readFileSync9(path, "utf8")
3205
+ readFileSync10(path, "utf8")
3042
3206
  );
3043
3207
  if (parsed?.version === 1 && typeof parsed.cliVersion === "string" && parsed.cliVersion.trim()) {
3044
3208
  return parsed;
@@ -3047,9 +3211,9 @@ function loadManifest(apmDir) {
3047
3211
  }
3048
3212
  return null;
3049
3213
  }
3050
- function saveManifest(apmDir, cliVersion) {
3214
+ function saveManifest2(apmDir, cliVersion) {
3051
3215
  const manifest = { version: 1, cliVersion };
3052
- writeFileSync10(
3216
+ writeFileSync11(
3053
3217
  toFsPath(manifestPath2(apmDir)),
3054
3218
  `${JSON.stringify(manifest, null, 2)}
3055
3219
  `,
@@ -3062,7 +3226,7 @@ function shouldSyncSkillsForCliVersion(workdir, currentVersion) {
3062
3226
  if (cached === currentVersion) {
3063
3227
  return false;
3064
3228
  }
3065
- const stored = loadManifest(workspaceApmDir(workdir));
3229
+ const stored = loadManifest2(workspaceApmDir(workdir));
3066
3230
  if (stored?.cliVersion === currentVersion) {
3067
3231
  syncedInSession.set(workdir, currentVersion);
3068
3232
  return false;
@@ -3070,7 +3234,7 @@ function shouldSyncSkillsForCliVersion(workdir, currentVersion) {
3070
3234
  return true;
3071
3235
  }
3072
3236
  function markSkillsSyncedForCliVersion(workdir, cliVersion) {
3073
- saveManifest(workspaceApmDir(workdir), cliVersion);
3237
+ saveManifest2(workspaceApmDir(workdir), cliVersion);
3074
3238
  syncedInSession.set(workdir, cliVersion);
3075
3239
  }
3076
3240
 
@@ -3143,6 +3307,27 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3143
3307
  if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3144
3308
  await ensureWebIdePullRequests(cfg, taskId, workdir);
3145
3309
  }
3310
+ if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3311
+ const api = createApmApiClient(cfg);
3312
+ try {
3313
+ const attachments = await api.cli.webideListAttachments({ taskId });
3314
+ const names = await syncWebIdeAttachments(
3315
+ cfg,
3316
+ taskId,
3317
+ workdir,
3318
+ attachments ?? []
3319
+ );
3320
+ if (names.length > 0) {
3321
+ console.log(
3322
+ `[apm] webide \u9644\u4EF6\u5DF2\u5C31\u7EEA count=${names.length} dir=.apm/webide/${taskId}/attachments`
3323
+ );
3324
+ }
3325
+ } catch (err) {
3326
+ console.warn(
3327
+ "[apm] WebIDE \u9644\u4EF6\u540C\u6B65\u5931\u8D25:",
3328
+ err instanceof Error ? err.message : err
3329
+ );
3330
+ }
3146
3331
  const savedAgentId = loadWebIdeAgentId(workdir, taskId);
3147
3332
  const logSyncRef = { current: null };
3148
3333
  const outcome = await runCursorAgent(
@@ -3209,12 +3394,11 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3209
3394
  result: outcome.result,
3210
3395
  createPlan: outcome.createPlan
3211
3396
  });
3212
- const api = createApmApiClient(cfg);
3213
3397
  await api.cli.webideEnsureMessageContent({
3214
3398
  id: messageId,
3215
3399
  content: fallback
3216
3400
  });
3217
- if (msg.action === "write-plan" && outcome.createPlan && outcome.createPlan.trim()) {
3401
+ if ((msg.action === "write-plan" || msg.action === "revise-plan") && outcome.createPlan && outcome.createPlan.trim()) {
3218
3402
  try {
3219
3403
  await api.cli.webideUpsertPlan({
3220
3404
  taskId,
@@ -3222,7 +3406,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3222
3406
  });
3223
3407
  } catch (err) {
3224
3408
  console.warn(
3225
- "[apm] write-plan \u8865\u5199\u8BA1\u5212\u5931\u8D25:",
3409
+ `[apm] ${msg.action} \u8865\u5199\u8BA1\u5212\u5931\u8D25:`,
3226
3410
  err instanceof Error ? err.message : err
3227
3411
  );
3228
3412
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "7.1.13",
3
+ "version": "7.1.16",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -33,13 +33,14 @@
33
33
  - webide_testcase.md:WebIDE 生成测试用例时读取,按规范编写并提交用例
34
34
  - webide_merge.md:WebIDE 验收通过后读取;直接调用 MergeWebIdePullRequests 合并代码
35
35
 
36
- - 仓库项目上下文(`.apm/project/`):
36
+ - 项目文档(`.apm/docs/`):
37
37
 
38
- - 索引: `.apm/project/manifest.json` — 本仓库在平台登记的文档列表
39
- - 文档文件: `.apm/project/{path}` — 与 manifest 中 path 对应;`apm pull` 自动同步,`apm connect` 消息结束后自动推回平台
38
+ - 索引: `.apm/docs/manifest.json` — 本项目在平台登记的文档列表
39
+ - 文档文件: `.apm/docs/{path}` — 与 manifest 中 path 对应;`apm pull` 自动同步,`apm connect` 消息结束后自动推回平台
40
40
  - 任务涉及菜单名、路由、业务术语等时,**先 Read manifest 与相关文档**,禁止猜测路径
41
41
 
42
42
  - 本轮任务需要关注的文档指引(.apm/sessions/<会话 ID>/\*):
43
+
43
44
  - 本轮会话状态: `session.yaml`,从这里可以看到每个成员的信息,找到可以协助你一起解决问题的人,可以结合 `RULE.md`一起看
44
45
  - 群文档: `docs/xxxx.md`,当需要相关上下文可以在这里查找,按需阅读
45
46
  - 附件列表: `attachments/*`,当有文档中提及附件时,从这里查找
@@ -47,3 +48,6 @@
47
48
  - 协作 TODO: `TODO.md`,跨轮次任务清单(待办与已完成项),按需阅读
48
49
  - 历史消息记录: `messages.xml`,历史消息列表,数据量很大,按需阅读
49
50
  - 初始目标: `TASK.md`,最初的目标,不一定具体,团队成员会有人负责让这个任务变得具体,仅供参考。如果你是相关的角色,则你需要先读取这个目标,然后规划接下来的动作。
51
+
52
+ - WebIDE 任务附件(`.apm/webide/<taskId>/attachments/`):
53
+ - `apm connect` 处理 webide-message 时会自动同步;prompt 中若出现「任务附件」指引,请按路径查看相关文件
@@ -13,23 +13,25 @@
13
13
 
14
14
  ## 启动命令来源
15
15
 
16
- 必须 Read `.apm/project/deploy.md`,按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
16
+ 必须 Read `.apm/docs/deploy.md`,按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
17
17
 
18
18
  - 文档不存在或未写明启动方式 → 用 AppendMessage 说明无法启动本地环境,**禁止猜测命令**。
19
19
 
20
- ## 时机
20
+ ## 时机(打开本地 dev)
21
21
 
22
- 1. **首次**(`start-develop` / 跳过计划后的开发轮次):实现完成后需要本地环境时
23
- - 先 `ListWebIdeTerminals`
24
- - 若无活终端 → Read `deploy.md` → 按前后端分别 `StartWebIdeTerminal`
25
- - 等待 READY / 端口出现后,告知用户可在 WebIDE「终端」面板观察日志(勿粘贴整段 stdout)
26
- 2. **后续改码**(热更新):有活终端则**复用,不重起**
27
- 3. **显式重跑**(端口冲突、命令变更、或平台「重跑本地环境」指令):
28
- `StopAllWebIdeTerminals` → 再 Read `deploy.md` → 重新 Start
29
- 不要在已死进程上糊弄;需要换命令时也是先停再建
22
+ 固定顺序:
23
+
24
+ 1. **`ListWebIdeTerminals`**
25
+ 2. 若无活终端 Read `deploy.md` 按前后端分别 `StartWebIdeTerminal`;已有活终端则复用
26
+ 3. 等待 READY / 端口即可
27
+ 4. AppendMessage:可在「终端」面板观察本地日志;「浏览器」面板由平台打开本客户机外网地址(勿粘贴整段 stdout;无需 Agent 打开浏览器)
28
+
29
+ 后续改码(热更新):有活终端则**复用,不重起**。
30
+
31
+ 显式重跑:`StopAllWebIdeTerminals` → 再 Read `deploy.md` → 重新 Start。
30
32
 
31
33
  ## 规范
32
34
 
33
35
  1. 禁止用 Shell / 后台进程方式起 `dev` / `serve` 等长驻命令。
34
- 2. 观察日志请用户看 WebIDE「终端」面板。
36
+ 2. 观察日志请用户看 WebIDE「终端」面板;页面请用户看「浏览器」面板(客户机外网地址)。
35
37
  3. 任务结束或不再需要时可 Stop;不要留下孤儿进程。