ai-project-manage-cli 8.0.2 → 8.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "8.0.2",
3
+ "version": "8.0.4",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -22,10 +22,7 @@
22
22
  "@types/node": "^22.0.0",
23
23
  "typescript": "~5.6.0",
24
24
  "esbuild": "~0.28.0",
25
- "vitest": "~4.1.5",
26
- "@types/dockerode": "~4.0.1",
27
- "@types/ssh2": "~1.15.0",
28
- "@types/ssh2-sftp-client": "~9.0.6"
25
+ "vitest": "~4.1.5"
29
26
  },
30
27
  "engines": {
31
28
  "node": ">=22.13.0"
@@ -38,10 +35,6 @@
38
35
  "ws": "~8.18.0",
39
36
  "listpage-http": "~0.0.318",
40
37
  "commander": "~14.0.3",
41
- "minio": "~8.0.7",
42
- "dockerode": "~5.0.0",
43
- "ssh2": "~1.16.0",
44
- "ssh2-sftp-client": "~12.0.1",
45
- "jszip": "~3.10.1"
38
+ "minio": "~8.0.7"
46
39
  }
47
40
  }
@@ -14,14 +14,14 @@
14
14
  - `webide_reply.md`:输出通道(每轮必守)
15
15
  - `webide_git_commit.md`:开发 / 修码时的 Git 提交与 push
16
16
  - `webide_sql_change.md`:有表结构或 SQL 变更时,须在 `AppendMessage` 中醒目提示
17
- - `webide_terminal.md`:本地 dev 必须用 WebIDE 终端工具,禁止用 Shell 起长驻进程
17
+ - `webide_terminal.md`:长驻进程禁止用 Shell;本地服务由顶栏「启动项目」触发,开发任务不要自行起服务
18
18
  - `webide_testcase.md`:生成测试用例并调用 `UpsertWebIdeTestCases`
19
19
  - `webide_merge.md`:验收通过后调用 `MergeWebIdePullRequests`
20
20
 
21
21
  ### 目录
22
22
 
23
- - 项目文档:`.apm/project/<项目ID>/`(索引 `manifest.json`;部署 / 本地 dev 命令以 `deploy.md` 为准,禁止猜测)
23
+ - 项目文档:`.apm/project/<项目ID>/`(索引 `manifest.json`;部署 / 本地 dev 命令以该目录下文件名或路径包含 `deploy` 的文档为准,不必恰好叫 `deploy.md`,禁止猜测)
24
24
  - 任务附件:`.apm/webide/<taskId>/attachments/`(prompt 出现「任务附件」时按路径查看)
25
25
  - 仓库清单:`.apm/workspace-repos.json`(先确认单仓 / 多仓;多仓必须写清改动范围并点名仓库)
26
26
 
27
- 计划、用例、终端、合并 PR 均走 WebIDE 专用工具(`AskQuestion`、`RecommendPlan`、`UpsertWebIdePlan`、`UpsertWebIdeTestCases`、`StartWebIdeTerminal`、`MergeWebIdePullRequests` 等)。
27
+ 计划、用例、合并 PR WebIDE 专用工具(`AskQuestion`、`RecommendPlan`、`UpsertWebIdePlan`、`UpsertWebIdeTestCases`、`MergeWebIdePullRequests` 等)。本地服务由用户在顶栏「启动项目」启动,开发任务不要自行起长驻进程。
@@ -1,37 +1,44 @@
1
1
  # WebIDE 长驻终端
2
2
 
3
- 长驻进程(如前后端 `dev`)必须用自定义工具管理,**禁止**用 Shell 启动会长期占用的 dev server。
3
+ 长驻进程(如前后端 `dev`)必须用 MCP `pty-session` 管理,**禁止**用 Shell 启动会长期占用的 dev server。
4
+
5
+ 会话与 WebIDE「终端」面板共用同一套 `pty-session-mcp serve`。工具名:`create` / `write` / `read` / `resize` / `kill` / `list`。
6
+
7
+ 本地服务由用户在顶栏「启动项目」触发(独立 Agent),**开发/修码任务不要自行起或重启长驻进程**。
4
8
 
5
9
  ## 工具
6
10
 
7
- - `StartWebIdeTerminal`:`{ key, name?, cwd, command, readyPattern?, portsHint? }`
8
- - `key` 稳定标识,如 `frontend` / `backend`;同 key 已在跑(RUNNING/READY)则幂等返回
9
- - 进程挂在 CLI 主进程,跨多轮消息存活
10
- - `StopWebIdeTerminal`:`{ key }` `{ terminalId }`;杀进程树,状态改为 EXITED(不删记录)
11
- - `StopAllWebIdeTerminals`:停掉本 task 全部仍存活的终端(重跑本地环境前使用)
12
- - `ListWebIdeTerminals`:列出**存活**终端(STARTING / RUNNING / READY / STOPPING),**不含** EXITED / FAILED
11
+ - `list`:列出当前仍存活的 PTY。用返回的 `name` 识别服务(如 `frontend` / `backend` / `mobile`)。
12
+ - `create`:`{ cwd, name, command?, args? }`
13
+ - `name` 用稳定 key(`frontend` / `backend` / `mobile`)
14
+ - 需要跑 shell 命令时:先 `create({ cwd, name })` 打开默认 shell,再 `write({ id, data: "<命令>" })`(`submit` 默认 true,会补换行)
15
+ - `command` 只能是可执行文件,不能是整句 shell(如不要把 `pnpm dev` 塞进 `command`)
16
+ - `write`:往会话写命令或输入
17
+ - `read`:`{ id, waitMs }` 轮询输出,直到出现就绪日志 / 端口
18
+ - `kill`:`{ id }` 结束该会话。只杀目标 `name`,不要误杀其它端
13
19
 
14
20
  ## 启动命令来源
15
21
 
16
- 必须 Read `.apm/project/<项目ID>/deploy.md`(完整路径以本轮 prompt 中给出的为准,勿省略项目 ID),按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
22
+ 必须在 `.apm/project/<项目ID>/` 下找到 **文件名或路径包含 deploy** 的文档(不必恰好叫 `deploy.md`,例如 `deploy前端.md`、`deploy/医务系统.md`)。完整目录以本轮 prompt 给出的项目 ID 为准,勿省略。
17
23
 
18
- - 文档不存在或未写明启动方式 AppendMessage 说明无法启动本地环境,**禁止猜测命令**。
24
+ 1. Read 该目录的 `manifest.json`,从 `documents[].path` 里挑包含 deploy 的条目,再 Read 该文件。
25
+ 2. 没有 manifest 时 Glob `**/*deploy*`。
26
+ 3. 按文档中的 cwd / command / 服务划分创建会话。
27
+ 4. 文档不存在或未写明启动方式 → 结束任务,**禁止猜测命令**。
19
28
 
20
- ## 时机(打开本地 dev)
29
+ ## 时机(顶栏启动 / 重启)
21
30
 
22
31
  固定顺序:
23
32
 
24
- 1. **`ListWebIdeTerminals`**
25
- 2. 若无活终端 Read prompt 中的 `.apm/project/<项目ID>/deploy.md` 完整路径 → 按前后端分别 `StartWebIdeTerminal`;已有活终端则复用
26
- 3. 等待 READY / 端口即可
27
- 4. AppendMessage:可在「终端」面板观察本地日志;「浏览器」面板由平台打开本客户机外网地址(勿粘贴整段 stdout;无需 Agent 打开浏览器)
28
-
29
- 后续改码(热更新):有活终端则**复用,不重起**。
33
+ 1. **`list`**
34
+ 2. 启动:无对应 `name` 的活会话 按部署文档 `create` + `write`;已有同名活会话则结束,不重起
35
+ 3. 重启:先 `kill` 目标 `name`,再按部署文档 `create` + `write`(不要杀掉其它端)
36
+ 4. `read` 等待就绪日志 / 端口即可
30
37
 
31
- 显式重跑:`StopAllWebIdeTerminals` → 再 Read `.apm/project/<项目ID>/deploy.md` → 重新 Start。
38
+ 用户在「终端」面板看日志,在「浏览器」面板看客户机外网地址。
32
39
 
33
40
  ## 规范
34
41
 
35
42
  1. 禁止用 Shell / 后台进程方式起 `dev` / `serve` 等长驻命令。
36
43
  2. 观察日志请用户看 WebIDE「终端」面板;页面请用户看「浏览器」面板(客户机外网地址)。
37
- 3. 任务结束或不再需要时可 Stop;不要留下孤儿进程。
44
+ 3. 不再需要时可 kill;不要留下孤儿进程。
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: webide-run-retro
3
+ description: >-
4
+ Resolve a Cursor WebIDE run-id or agent-id to the local conversation
5
+ transcript, then produce two documents: conversation replay and defect
6
+ retrospective attribution (unclear requirements vs AI vs environment). Use
7
+ when the user pastes a run-id (run-<uuid>), agent-id (agent-<uuid>), asks to
8
+ 查找对话, 对话回放, 复盘, 缺陷归因, or reviews WebIDE report-defect rounds.
9
+ ---
10
+
11
+ # WebIDE Run / Agent 复盘
12
+
13
+ 用户给出 `run-id` 或 `agent-id` 时:先还原对话,再只对 **report-defect**(及等价的用户缺陷反馈)做归因。回复用中文。
14
+
15
+ 产出必须是 **两份独立文档**(两份 Canvas),缺一不可:
16
+
17
+ 1. **对话回放** — 整段 agent 对话按轮次可浏览
18
+ 2. **复盘归因** — 对用户提出的缺陷做主因判定与改进建议
19
+
20
+ ## 1. 解析 ID
21
+
22
+ 执行(不要手写等价逻辑):
23
+
24
+ ```bash
25
+ node ~/.cursor/skills/webide-run-retro/scripts/inspect-run.mjs --id '<用户给的ID>'
26
+ ```
27
+
28
+ 脚本会:
29
+
30
+ - 读 `AGENT_TRANSCRIPTS` 与 `~/.cursor/projects/*/agent-transcripts`
31
+ - 查 SDK store `index.db`(活表 + 已删除页的字符串残留,因为 WebIDE 常 `purge` store)
32
+ - 读 `.apm/webide/<taskId>/cursor-agent.json`
33
+ - 打印 `agentId`、`matchedRunId`、`transcriptPath`、按轮次的 `conversation`
34
+
35
+ `run-<uuid>` 是一次 `send()`;`agent-<uuid>` 是整段多轮会话。有 run-id 时仍要拉**整个 agent** 对话,否则看不到原需求。
36
+
37
+ 找不到 transcript 就停,说明缺了哪一层(store / transcripts / 已清理),不要编对话。
38
+
39
+ ## 2. 读对话
40
+
41
+ 用脚本输出的 `transcriptPath` 做补充阅读:
42
+
43
+ - 第一轮 `action=start`(或首条用户需求)= **原需求原文**
44
+ - `action=write-plan` / `start-develop` = AI 理解与实现范围
45
+ - `action=report-defect` = 待归因缺陷(`【现状】` / `【预期】`)
46
+ - 助手收尾句与 git 提交(`feat:` / `fix:`)用来核对手改了什么
47
+
48
+ 需要核对代码时,对比特性分支相对基线的首次实现提交 vs 后续 `fix:` 提交,避免把基线旧逻辑算成 AI 新引入。
49
+
50
+ ## 3. 归因(必做)
51
+
52
+ 细则见 [attribution.md](attribution.md)。对每条缺陷标**一个主因**:
53
+
54
+ 1. **需求没写清** — 原文没有该行为/边界/状态机
55
+ 2. **AI 理解或实现问题** — 原文或缺陷期望已写清,但理解偏了、修不彻底、工作流做半截
56
+ 3. **环境 / 工程** — 启动、构建、端口,与业务无关
57
+
58
+ 总判断先写:**原需求有没有被理解对**。后面的缺陷经常是测试挖出未写进需求的业务,不要误判成「字段理解反了」。
59
+
60
+ 给出两个动作建议:
61
+
62
+ - 需求没写清 → 培训用户(过滤包含/排除、空值样例、暂存/提交状态机、字段可空、验收反例)
63
+ - AI 理解/实现问题 → 补业务文档(组件默认过滤、审批暂存/startFlow、deploy 启动命令)
64
+
65
+ ## 4. 输出
66
+
67
+ 聊天里用 3–8 句中文给出:agent-id、任务名、回放轮次数、缺陷条数、总判断、两条动作建议。然后给出两份 Canvas 的 markdown 链接。不要用大 markdown 表代替 Canvas。
68
+
69
+ 先 Read `~/.cursor/skills-cursor/canvas/SKILL.md`。两份都写在 `~/.cursor/projects/<workspace>/canvases/`,文件名 kebab-case。
70
+
71
+ ### 文档 1:对话回放
72
+
73
+ 文件:`<agent-short-id>-conversation.canvas.tsx`
74
+
75
+ 必须包含:
76
+
77
+ - ID 映射:run-id、agent-id、task-id
78
+ - 任务标题与时间范围
79
+ - 轮次一览表:序号、时间、动作、用户意图、工具次数
80
+ - 对话正文:每轮可展开,用户请求摘要 + 助手收尾回复(不要堆工具调用细节)
81
+ - 中断后重试的同一缺陷合并说明(若有)
82
+
83
+ ### 文档 2:复盘归因
84
+
85
+ 文件:`<agent-short-id>-retro.canvas.tsx`
86
+
87
+ 必须包含:
88
+
89
+ - 总判断:原需求有没有被理解对
90
+ - 主因分布(饼图或统计):需求没写清 / AI 理解或实现 / 环境
91
+ - 原需求实际覆盖了什么(对照评估与首轮实现)
92
+ - 缺陷连锁关系(若有)
93
+ - 逐条归因表:缺陷、主因、原需求是否覆盖、原因
94
+ - 两块建议:培训用户写需求 vs 补 AI 业务文档
95
+
96
+ 无缺陷时:仍产出文档 2,写明「无 report-defect 轮次,不归因」,不要编缺陷。
97
+
98
+ ## 5. 禁止
99
+
100
+ - 不要把对话回放和复盘归因合成一份 Canvas
101
+ - 不要把缺陷单里新补的期望,回溯成「原需求 AI 理解错」
102
+ - 不要只复述工具调用;归因要对照原文、评估结论、代码/提交
103
+ - 不要输出密钥、API Key、MinIO 凭证
@@ -0,0 +1,26 @@
1
+ # 缺陷归因细则
2
+
3
+ 主因只标一个。对照物永远是**该 agent 第一轮用户需求原文**,不是后续缺陷单里补充的期望。
4
+
5
+ ## 三类主因
6
+
7
+ | 主因 | 何时用 | 对应动作 |
8
+ |------|--------|----------|
9
+ | 需求没写清 | 缺陷所要的行为不在原文(含空值、过滤白名单、按钮/状态机、验收反例) | 培训用户把需求写完整 |
10
+ | AI 理解或实现问题 | 原文已覆盖,或缺陷单已写清期望,但 AI 理解偏了、修不彻底、把工作流做半截 | 补业务/组件/工作流文档 |
11
+ | 环境 / 工程 | 启动失败、端口、构建命令、与业务无关 | 补 deploy/启动文档 |
12
+
13
+ 不要把「测试中途新提出的暂存/提交」算成「AI 把原需求理解错」。原需求理解对、范围外行为在测试里才出现 → 需求缺口。
14
+
15
+ ## 连锁缺陷
16
+
17
+ 同一缺口连续爆出来时,在表里分开写,但复盘正文要标明因果链。例如:未写「无级别术式可申请」→ 选中后必填失败 → 库非空失败 → 改成暂存 → 列表/工作流半截。
18
+
19
+ 只把链尾的「做半截」标成 AI 实现问题;链头标需求缺口。
20
+
21
+ ## 评估阶段检查
22
+
23
+ 若第一轮评估写了「无开放问题」,但测试缺陷正好落在未提问的边界上,复盘里必须点出本该问的问题,并同时改进:
24
+
25
+ - 用户需求模板(过滤包含/排除、空值样例、按钮状态机、字段可空、验收反例)
26
+ - AI 评估检查单(空值、默认过滤、暂存 vs 提交、流程何时启动)
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Resolve a Cursor SDK run-id or agent-id to the local transcript and a
4
+ * structured conversation summary. Execute this script; do not reimplement.
5
+ *
6
+ * Usage:
7
+ * node inspect-run.mjs --id run-<uuid>
8
+ * node inspect-run.mjs --id agent-<uuid>
9
+ * node inspect-run.mjs --id <uuid>
10
+ */
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import { DatabaseSync } from "node:sqlite";
15
+
16
+ const UUID_RE =
17
+ "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
18
+ const RUN_RE = new RegExp(`run-${UUID_RE}`, "gi");
19
+ const AGENT_RE = new RegExp(`agent-${UUID_RE}`, "gi");
20
+
21
+ function arg(name, fallback) {
22
+ const idx = process.argv.indexOf(name);
23
+ if (idx >= 0 && process.argv[idx + 1]) return process.argv[idx + 1];
24
+ return fallback;
25
+ }
26
+
27
+ function walkFiles(root, matcher, acc = [], depth = 0) {
28
+ if (!root || depth > 6 || acc.length > 80) return acc;
29
+ let entries = [];
30
+ try {
31
+ entries = fs.readdirSync(root, { withFileTypes: true });
32
+ } catch {
33
+ return acc;
34
+ }
35
+ for (const ent of entries) {
36
+ const full = path.join(root, ent.name);
37
+ if (ent.isDirectory()) {
38
+ if (["node_modules", ".git", "target"].includes(ent.name)) continue;
39
+ walkFiles(full, matcher, acc, depth + 1);
40
+ } else if (matcher(full, ent.name)) {
41
+ acc.push(full);
42
+ }
43
+ }
44
+ return acc;
45
+ }
46
+
47
+ function normalizeId(raw) {
48
+ const id = String(raw || "").trim();
49
+ if (!id) throw new Error("missing --id");
50
+ if (id.startsWith("run-") || id.startsWith("agent-")) return id;
51
+ if (new RegExp(`^${UUID_RE}$`, "i").test(id)) {
52
+ return { uuid: id, ambiguous: true };
53
+ }
54
+ return id;
55
+ }
56
+
57
+ function texts(rec) {
58
+ const content = rec?.message?.content;
59
+ if (!Array.isArray(content)) return [];
60
+ return content
61
+ .filter((c) => c?.type === "text" && c.text)
62
+ .map((c) => String(c.text).replace(/\[REDACTED\]/g, "").trim())
63
+ .filter(Boolean);
64
+ }
65
+
66
+ function tools(rec) {
67
+ const content = rec?.message?.content;
68
+ if (!Array.isArray(content)) return [];
69
+ return content.filter((c) => c?.type === "tool_use").map((c) => c.name);
70
+ }
71
+
72
+ function parseTranscript(filePath) {
73
+ const lines = fs.readFileSync(filePath, "utf8").trim().split("\n").filter(Boolean);
74
+ const records = lines.map((line, i) => {
75
+ try {
76
+ return { i, ...JSON.parse(line) };
77
+ } catch {
78
+ return { i, role: "unknown", parseError: true };
79
+ }
80
+ });
81
+
82
+ const rounds = [];
83
+ let cur = null;
84
+ for (const rec of records) {
85
+ if (rec.role === "user") {
86
+ const raw = texts(rec).join("\n");
87
+ if (raw.startsWith("<dynamic_tools>")) continue;
88
+ if (cur) rounds.push(cur);
89
+ const ts = (raw.match(/<timestamp>(.*?)<\/timestamp>/) || [])[1] || "";
90
+ const action = (raw.match(/\baction=([^\s]+)/) || [])[1] || "";
91
+ const taskId = (raw.match(/\btaskId=([^\s]+)/) || [])[1] || "";
92
+ const title = (raw.match(/标题:(.+)/) || [])[1]?.trim() || "";
93
+ const dm = raw.match(/【现状】\s*([\s\S]*?)\s*【预期】/);
94
+ const em = raw.match(/【预期】\s*([\s\S]*?)(?:\n\n##|\npayload=|$)/);
95
+ cur = {
96
+ ts,
97
+ action,
98
+ taskId,
99
+ title,
100
+ defect: dm ? dm[1].trim().replace(/\s+/g, " ") : "",
101
+ expected: em ? em[1].trim().replace(/\s+/g, " ") : "",
102
+ userPreview: raw.replace(/\s+/g, " ").slice(0, 280),
103
+ lastReply: "",
104
+ assistantNotes: [],
105
+ toolCount: 0,
106
+ };
107
+ } else if (cur && rec.role === "assistant") {
108
+ const t = texts(rec).join(" ").trim();
109
+ cur.toolCount += tools(rec).length;
110
+ if (t) {
111
+ cur.assistantNotes.push(t.slice(0, 240));
112
+ cur.lastReply = t.slice(0, 400);
113
+ }
114
+ }
115
+ }
116
+ if (cur) rounds.push(cur);
117
+
118
+ const first = rounds[0] || {};
119
+ return {
120
+ recordCount: records.length,
121
+ taskId: rounds.map((r) => r.taskId).find(Boolean) || "",
122
+ title: rounds.map((r) => r.title).find(Boolean) || "",
123
+ rounds,
124
+ defects: rounds
125
+ .filter((r) => r.action === "report-defect" || r.defect)
126
+ .map((r, idx) => ({
127
+ n: idx + 1,
128
+ ts: r.ts,
129
+ defect: r.defect,
130
+ expected: r.expected,
131
+ lastReply: r.lastReply,
132
+ toolCount: r.toolCount,
133
+ })),
134
+ actions: rounds.map((r) => r.action).filter(Boolean),
135
+ firstAction: first.action || "",
136
+ };
137
+ }
138
+
139
+ function readLiveSqlite(dbPath) {
140
+ const out = { agents: [], runs: [] };
141
+ try {
142
+ const db = new DatabaseSync(dbPath);
143
+ try {
144
+ out.agents = db.prepare("SELECT * FROM agents").all();
145
+ out.runs = db.prepare("SELECT * FROM runs").all();
146
+ } finally {
147
+ db.close();
148
+ }
149
+ } catch (err) {
150
+ out.error = err.message;
151
+ }
152
+ return out;
153
+ }
154
+
155
+ function scanSqliteBinary(dbPath) {
156
+ const buf = fs.readFileSync(dbPath);
157
+ const text = buf.toString("latin1");
158
+ return {
159
+ agentIds: [...new Set(text.match(AGENT_RE) || [])],
160
+ runIds: [...new Set(text.match(RUN_RE) || [])],
161
+ };
162
+ }
163
+
164
+ function discoverRoots() {
165
+ const cwd = process.cwd();
166
+ const home = os.homedir();
167
+ const transcriptsEnv = process.env.AGENT_TRANSCRIPTS;
168
+ const cursorHome = fs.existsSync(path.join(home, ".cursor"))
169
+ ? path.join(home, ".cursor")
170
+ : path.join("/data", "cursor-home");
171
+ const projectsDir = path.join(cursorHome, "projects");
172
+ return { cwd, home, transcriptsEnv, cursorHome, projectsDir };
173
+ }
174
+
175
+ function findTranscripts(roots) {
176
+ const dirs = [];
177
+ if (roots.transcriptsEnv && fs.existsSync(roots.transcriptsEnv)) {
178
+ dirs.push(roots.transcriptsEnv);
179
+ }
180
+ if (fs.existsSync(roots.projectsDir)) {
181
+ for (const name of fs.readdirSync(roots.projectsDir)) {
182
+ const p = path.join(roots.projectsDir, name, "agent-transcripts");
183
+ if (fs.existsSync(p)) dirs.push(p);
184
+ }
185
+ }
186
+ return [...new Set(dirs)];
187
+ }
188
+
189
+ function findStores(roots) {
190
+ const files = [];
191
+ if (fs.existsSync(roots.projectsDir)) {
192
+ for (const name of fs.readdirSync(roots.projectsDir)) {
193
+ const store = path.join(roots.projectsDir, name, "sdk-agent-store");
194
+ walkFiles(store, (full, base) => base === "index.db", files, 0);
195
+ }
196
+ }
197
+ walkFiles(
198
+ path.join(roots.cwd, ".apm"),
199
+ (full, base) => base === "cursor-agent.json",
200
+ files,
201
+ 0,
202
+ );
203
+ return files;
204
+ }
205
+
206
+ function findTranscriptFile(transcriptDirs, agentId) {
207
+ const stems = [agentId, agentId.replace(/^agent-/, ""), `agent-${agentId.replace(/^agent-/, "")}`];
208
+ for (const dir of transcriptDirs) {
209
+ for (const stem of stems) {
210
+ const nested = path.join(dir, stem, `${stem}.jsonl`);
211
+ if (fs.existsSync(nested)) return nested;
212
+ const flat = path.join(dir, `${stem}.jsonl`);
213
+ if (fs.existsSync(flat)) return flat;
214
+ }
215
+ try {
216
+ for (const name of fs.readdirSync(dir)) {
217
+ if (name.includes(agentId.replace(/^agent-/, ""))) {
218
+ const candidate = path.join(dir, name, `${name}.jsonl`);
219
+ if (fs.existsSync(candidate)) return candidate;
220
+ }
221
+ }
222
+ } catch {
223
+ /* ignore */
224
+ }
225
+ }
226
+ return null;
227
+ }
228
+
229
+ function main() {
230
+ const rawId = arg("--id", process.argv[2]);
231
+ const normalized = normalizeId(rawId);
232
+ const roots = discoverRoots();
233
+ const transcriptDirs = findTranscripts(roots);
234
+ const storeFiles = findStores(roots);
235
+
236
+ const recovered = { agentIds: new Set(), runIds: new Set(), liveRuns: [] };
237
+ for (const file of storeFiles.filter((f) => f.endsWith("index.db"))) {
238
+ const live = readLiveSqlite(file);
239
+ for (const row of live.runs || []) {
240
+ recovered.liveRuns.push({
241
+ store: file,
242
+ runId: row.run_id || row.runId,
243
+ agentId: row.agent_id || row.agentId,
244
+ status: row.status,
245
+ turnNumber: row.turn_number || row.turnNumber,
246
+ });
247
+ if (row.run_id) recovered.runIds.add(row.run_id);
248
+ if (row.agent_id) recovered.agentIds.add(row.agent_id);
249
+ }
250
+ for (const row of live.agents || []) {
251
+ if (row.agent_id) recovered.agentIds.add(row.agent_id);
252
+ }
253
+ const scanned = scanSqliteBinary(file);
254
+ scanned.agentIds.forEach((id) => recovered.agentIds.add(id));
255
+ scanned.runIds.forEach((id) => recovered.runIds.add(id));
256
+ }
257
+
258
+ for (const file of storeFiles.filter((f) => f.endsWith("cursor-agent.json"))) {
259
+ try {
260
+ const j = JSON.parse(fs.readFileSync(file, "utf8"));
261
+ if (j.agentId) recovered.agentIds.add(j.agentId);
262
+ } catch {
263
+ /* ignore */
264
+ }
265
+ }
266
+
267
+ let input = typeof normalized === "string" ? normalized : `uuid:${normalized.uuid}`;
268
+ let kind = "unknown";
269
+ let agentId = null;
270
+ let matchedRunId = null;
271
+
272
+ const asString = typeof normalized === "string" ? normalized : normalized.uuid;
273
+ const wantRun = asString.startsWith("run-")
274
+ ? asString
275
+ : recovered.runIds.has(`run-${asString}`)
276
+ ? `run-${asString}`
277
+ : null;
278
+ const wantAgent = asString.startsWith("agent-")
279
+ ? asString
280
+ : recovered.agentIds.has(`agent-${asString}`)
281
+ ? `agent-${asString}`
282
+ : asString.startsWith("agent-")
283
+ ? asString
284
+ : `agent-${asString.replace(/^agent-/, "")}`;
285
+
286
+ if (asString.startsWith("run-") || recovered.runIds.has(wantRun)) {
287
+ kind = "run";
288
+ matchedRunId = wantRun || asString;
289
+ const live = recovered.liveRuns.find((r) => r.runId === matchedRunId);
290
+ if (live?.agentId) agentId = live.agentId;
291
+ if (!agentId && recovered.agentIds.size === 1) {
292
+ agentId = [...recovered.agentIds][0];
293
+ }
294
+ } else if (asString.startsWith("agent-") || recovered.agentIds.has(wantAgent)) {
295
+ kind = "agent";
296
+ agentId = wantAgent.startsWith("agent-") ? wantAgent : `agent-${wantAgent}`;
297
+ } else if (asString.startsWith("agent-") || asString.length === 36) {
298
+ kind = asString.startsWith("run-") ? "run" : "agent";
299
+ agentId = kind === "agent" ? (asString.startsWith("agent-") ? asString : `agent-${asString}`) : null;
300
+ matchedRunId = kind === "run" ? asString : null;
301
+ }
302
+
303
+ if (!agentId && kind === "agent") {
304
+ agentId = asString.startsWith("agent-") ? asString : `agent-${asString}`;
305
+ }
306
+
307
+ let transcriptPath = agentId ? findTranscriptFile(transcriptDirs, agentId) : null;
308
+ if (!transcriptPath && kind === "agent") {
309
+ transcriptPath = findTranscriptFile(transcriptDirs, asString);
310
+ }
311
+
312
+ const conversation = transcriptPath ? parseTranscript(transcriptPath) : null;
313
+
314
+ const result = {
315
+ input: rawId,
316
+ kind,
317
+ agentId,
318
+ matchedRunId,
319
+ recoveredAgentIds: [...recovered.agentIds],
320
+ recoveredRunIds: [...recovered.runIds],
321
+ liveRuns: recovered.liveRuns,
322
+ transcriptPath,
323
+ transcriptDirs,
324
+ storeFiles,
325
+ conversation,
326
+ note: !transcriptPath
327
+ ? "No transcript found. Check AGENT_TRANSCRIPTS or ~/.cursor/projects/*/agent-transcripts."
328
+ : recovered.liveRuns.length === 0 && matchedRunId
329
+ ? "Run tables were empty; mapping used leftover SQLite pages plus the only local agent-id."
330
+ : undefined,
331
+ };
332
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
333
+ }
334
+
335
+ main();