oh-my-agent-web 0.4.0 → 0.5.0

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.
Files changed (142) hide show
  1. package/README.md +29 -2
  2. package/dist/server/engine/builtin.js +23 -0
  3. package/dist/server/engine/jsonrpc-stdio.js +1 -1
  4. package/dist/server/engine/narrate-guidance.js +22 -0
  5. package/dist/server/engine/opencode-map.js +499 -0
  6. package/dist/server/engine/opencode-projects.js +61 -0
  7. package/dist/server/engine/opencode-sessions.js +195 -0
  8. package/dist/server/engine/opencode-transport.js +48 -0
  9. package/dist/server/engine/opencode.js +394 -0
  10. package/dist/server/engine/pi-plugins.js +310 -0
  11. package/dist/server/engine/pi-sdk.js +163 -3
  12. package/dist/server/engine/pi-subagent-tool.js +166 -0
  13. package/dist/server/engine/pi-subagents.js +372 -0
  14. package/dist/server/engine/registry.js +1 -1
  15. package/dist/server/engine/subagent-guidance.js +22 -0
  16. package/dist/server/engine/wiring.js +59 -0
  17. package/dist/server/files/files-routes.js +29 -1
  18. package/dist/server/files/git-info.js +176 -1
  19. package/dist/server/http/aux-routes.js +16 -0
  20. package/dist/server/http/compress.js +2 -0
  21. package/dist/server/index.js +22 -1
  22. package/dist/server/mcp/mcp-auth.js +184 -0
  23. package/dist/server/mcp/mcp-command.js +94 -0
  24. package/dist/server/mcp/mcp-import.js +290 -0
  25. package/dist/server/mcp/mcp-market-sources.js +192 -0
  26. package/dist/server/mcp/mcp-market-types.js +5 -0
  27. package/dist/server/mcp/mcp-market.js +175 -0
  28. package/dist/server/mcp/mcp-probe.js +134 -0
  29. package/dist/server/mcp/mcp-routes.js +70 -0
  30. package/dist/server/mcp/mcp-store.js +286 -0
  31. package/dist/server/mcp/pi-mcp-config.js +280 -0
  32. package/dist/server/plugins/plugin-routes.js +21 -0
  33. package/dist/server/protocol-parse.js +64 -3
  34. package/dist/server/providers/codex-models.js +96 -0
  35. package/dist/server/providers/codex-test.js +124 -0
  36. package/dist/server/providers/import-routes.js +99 -32
  37. package/dist/server/providers/provider-handlers.js +26 -6
  38. package/dist/server/providers/provider-store.js +49 -17
  39. package/dist/server/providers/subscription-routes.js +181 -0
  40. package/dist/server/session/model-controller.js +9 -1
  41. package/dist/server/session/runtime.js +23 -1
  42. package/dist/server/session/session-stats.js +3 -0
  43. package/dist/server/session/subagents.js +116 -0
  44. package/dist/server/session/turn-queue.js +1 -1
  45. package/dist/server/session/turn-send.js +23 -3
  46. package/dist/server/skills/skill-install.js +186 -0
  47. package/dist/server/skills/skill-routes.js +107 -1
  48. package/dist/server/skills/skill-store.js +33 -4
  49. package/dist/server/skills/skillhub.js +214 -0
  50. package/dist/server/skills/zip.js +121 -0
  51. package/dist/server/snapshot/state.js +10 -1
  52. package/dist/server/subagents/profile-store.js +287 -0
  53. package/dist/server/util/env.js +6 -0
  54. package/dist/server/ws/handle-message.js +12 -0
  55. package/package.json +2 -1
  56. package/web/dist/assets/{abnfDiagram-WEYOIA3C-NLp09iEC.js → abnfDiagram-WEYOIA3C-VUKLHHSn.js} +1 -1
  57. package/web/dist/assets/{arc-DviL9o3c.js → arc-BeUkaHhk.js} +1 -1
  58. package/web/dist/assets/{architecture-WOLXFQ4H-fjsM_Sy3.js → architecture-WOLXFQ4H-B9I4s0NV.js} +1 -1
  59. package/web/dist/assets/{architectureDiagram-ZHVYKT53-Cn3B4TE8.js → architectureDiagram-ZHVYKT53-CaEkLhfq.js} +1 -1
  60. package/web/dist/assets/{blockDiagram-RJAE7QA3-CIuvxWvi.js → blockDiagram-RJAE7QA3-IR49UNE_.js} +1 -1
  61. package/web/dist/assets/{c4Diagram-XDPPPE54-DvdKmEpF.js → c4Diagram-XDPPPE54-B-81CloB.js} +1 -1
  62. package/web/dist/assets/channel-B_mTTjGt.js +1 -0
  63. package/web/dist/assets/{chunk-2E7WHVMD-HNNASGCS.js → chunk-2E7WHVMD-CaWVF7qv.js} +1 -1
  64. package/web/dist/assets/{chunk-4EMNKR2T-Cg-eRwBE.js → chunk-4EMNKR2T-BvXf2tq-.js} +1 -1
  65. package/web/dist/assets/{chunk-55XCU76K-BweiisZL.js → chunk-55XCU76K-Bz_d2e2D.js} +1 -1
  66. package/web/dist/assets/{chunk-7S6XSTFO-BCzIiP-6.js → chunk-7S6XSTFO-B_SPMiU8.js} +1 -1
  67. package/web/dist/assets/{chunk-AQDTSKZB-Dl0NRFuU.js → chunk-AQDTSKZB-Cen-_xej.js} +1 -1
  68. package/web/dist/assets/{chunk-DSNLSHJZ-zhHCjjN9.js → chunk-DSNLSHJZ-DEw1kFwj.js} +1 -1
  69. package/web/dist/assets/{chunk-HAGFOTED-BDQqU2EH.js → chunk-HAGFOTED-BwpOOcw8.js} +1 -1
  70. package/web/dist/assets/{chunk-HNE4Q36P-B-qRzMjX.js → chunk-HNE4Q36P-BjCwi2uW.js} +1 -1
  71. package/web/dist/assets/{chunk-NQOKRVIF-CNcvhMn_.js → chunk-NQOKRVIF-WcqhWFS5.js} +1 -1
  72. package/web/dist/assets/{chunk-UHW7PFBP-Dwr8LUzr.js → chunk-UHW7PFBP-quDvQby8.js} +1 -1
  73. package/web/dist/assets/{chunk-VQ5W62QI-O8sN6jJu.js → chunk-VQ5W62QI-DZBwmhzq.js} +1 -1
  74. package/web/dist/assets/{chunk-WQQL4CDK-B6-59Zg2.js → chunk-WQQL4CDK-wYyHYuui.js} +1 -1
  75. package/web/dist/assets/{chunk-XXDRQBXY-DvSbeUlZ.js → chunk-XXDRQBXY-DfJzC7nt.js} +1 -1
  76. package/web/dist/assets/{chunk-YZO67HFG-BXpKYKsw.js → chunk-YZO67HFG-CWLraGk_.js} +1 -1
  77. package/web/dist/assets/{chunk-ZAUX2LTX-B1Z5VSkz.js → chunk-ZAUX2LTX-ChBUt_FJ.js} +1 -1
  78. package/web/dist/assets/{chunk-ZCHMQSOR-h_5mBLIA.js → chunk-ZCHMQSOR-C57UFT60.js} +1 -1
  79. package/web/dist/assets/{classDiagram-v2-4MYWR3UA-Dr6JnaaV.js → classDiagram-v2-4MYWR3UA-Bo4i7SNY.js} +1 -1
  80. package/web/dist/assets/{cose-bilkent-JH36ORCC-36ShkR7R.js → cose-bilkent-JH36ORCC-CK360NdH.js} +1 -1
  81. package/web/dist/assets/{cynefin-EF2NZ3EQ-Dg6bI7pC.js → cynefin-EF2NZ3EQ-DlXFjIFm.js} +1 -1
  82. package/web/dist/assets/{cynefinDiagram-WF24UDRP-B0YHpBlR.js → cynefinDiagram-WF24UDRP-zvZIqA1c.js} +1 -1
  83. package/web/dist/assets/{dagre-NK4GIZVX-Cx3mn7YS.js → dagre-NK4GIZVX-CnKhCu-v.js} +1 -1
  84. package/web/dist/assets/{diagram-CBK3MWTC-l8aZG_Bm.js → diagram-CBK3MWTC-3QTR8Fjx.js} +1 -1
  85. package/web/dist/assets/{diagram-I5XIIV2D-Bd5f14_h.js → diagram-I5XIIV2D-D9lSd7z6.js} +1 -1
  86. package/web/dist/assets/{diagram-ODIL2UCW-hBsxCYUj.js → diagram-ODIL2UCW-DWJD6lPM.js} +1 -1
  87. package/web/dist/assets/{diagram-PFBEJUXE-BgpIBUYl.js → diagram-PFBEJUXE-CWJmoUg4.js} +1 -1
  88. package/web/dist/assets/{diagram-RJA3I4VV-CDU1gUVD.js → diagram-RJA3I4VV-D1SZHQcQ.js} +1 -1
  89. package/web/dist/assets/{diagram-TYUNFSNR-Iuxh7Tqf.js → diagram-TYUNFSNR-DCCjGi1x.js} +1 -1
  90. package/web/dist/assets/{dist-C86cKTuE.js → dist-VOk_Iv17.js} +1 -1
  91. package/web/dist/assets/{ebnfDiagram-V4P3TTPT-Cms0HFKh.js → ebnfDiagram-V4P3TTPT-CQLdqEiW.js} +1 -1
  92. package/web/dist/assets/{elk-QONTBCTQ-B7yEy0PQ.js → elk-QONTBCTQ-Dp2pEp6b.js} +1 -1
  93. package/web/dist/assets/{erDiagram-KYQIGQAA-CL48j4aU.js → erDiagram-KYQIGQAA-BFGNoFQ-.js} +1 -1
  94. package/web/dist/assets/{eventmodeling-K75KTNOO-CFvkaejh.js → eventmodeling-K75KTNOO-CFfAemGw.js} +1 -1
  95. package/web/dist/assets/flowDiagram-IUGLLWBU-DmSp3cbW.js +1 -0
  96. package/web/dist/assets/{ganttDiagram-R7GPNTJY-vdMr-0ZV.js → ganttDiagram-R7GPNTJY-vK3jPGDq.js} +1 -1
  97. package/web/dist/assets/{gitGraph-VSP46ZUC-2z85Wuwo.js → gitGraph-VSP46ZUC-rXBgHTdq.js} +1 -1
  98. package/web/dist/assets/{gitGraphDiagram-PDYL7XHY-BY4GPiqC.js → gitGraphDiagram-PDYL7XHY-CBbGhago.js} +1 -1
  99. package/web/dist/assets/index-DBOF_OJe.js +104 -0
  100. package/web/dist/assets/index-nlY_-OaR.css +1 -0
  101. package/web/dist/assets/{info-OHQRW6UA-ECzkYTx-.js → info-OHQRW6UA-Cd9vp-6C.js} +1 -1
  102. package/web/dist/assets/{infoDiagram-MVDRKAX5-DDk5EAeb.js → infoDiagram-MVDRKAX5-Ce-Sg_qu.js} +1 -1
  103. package/web/dist/assets/{ishikawaDiagram-HPEHV2GE-9yPH4J94.js → ishikawaDiagram-HPEHV2GE-DWlmR6BY.js} +1 -1
  104. package/web/dist/assets/{journeyDiagram-BOSHW7O7-BPTIap3l.js → journeyDiagram-BOSHW7O7-DmDG97ae.js} +1 -1
  105. package/web/dist/assets/{kanban-definition-FNSA4HC5-BbADHrA8.js → kanban-definition-FNSA4HC5-1Z4sDrgV.js} +1 -1
  106. package/web/dist/assets/{line-CHV9_4lL.js → line-DGM1-9KW.js} +1 -1
  107. package/web/dist/assets/{linear-C-eZsBTv.js → linear-B6hyeGHu.js} +1 -1
  108. package/web/dist/assets/{mermaid-parser.core-BiAhX1l1.js → mermaid-parser.core-D1HmdkA4.js} +3 -3
  109. package/web/dist/assets/{mermaid.core-BxP_A_zj.js → mermaid.core-DeUJrhO1.js} +4 -4
  110. package/web/dist/assets/{mindmap-definition-6OY57UG3-BMWlit95.js → mindmap-definition-6OY57UG3-C3-FByy8.js} +1 -1
  111. package/web/dist/assets/{packet-JDAUHWVQ-FVApuKOp.js → packet-JDAUHWVQ-BoOr7DaW.js} +1 -1
  112. package/web/dist/assets/{pegDiagram-ECIUJNBX-WjWJvDHM.js → pegDiagram-ECIUJNBX-Bn7m75TZ.js} +1 -1
  113. package/web/dist/assets/{pie-XZMESJXO-BGCZnvhp.js → pie-XZMESJXO-CGuMTDBp.js} +1 -1
  114. package/web/dist/assets/{pieDiagram-GNMPY74T-DJpAC_JC.js → pieDiagram-GNMPY74T-DHhaPwZe.js} +1 -1
  115. package/web/dist/assets/{quadrantDiagram-D57K4MC3-hxx5c7oq.js → quadrantDiagram-D57K4MC3-DxTgFHLK.js} +1 -1
  116. package/web/dist/assets/{radar-ABXABTNO-BU9Shc-O.js → radar-ABXABTNO-BiKxmALL.js} +1 -1
  117. package/web/dist/assets/{railroad-I3PHUGI6-pWj8d-G4.js → railroad-I3PHUGI6-BKYufgg4.js} +1 -1
  118. package/web/dist/assets/{railroad-abnf-LNEFI6M7-D7HZBTAj.js → railroad-abnf-LNEFI6M7-DmOrjlyL.js} +1 -1
  119. package/web/dist/assets/{railroad-ebnf-DWYD2UWJ-CKiXJSuM.js → railroad-ebnf-DWYD2UWJ-BgMyVDd1.js} +1 -1
  120. package/web/dist/assets/{railroad-peg-JR7BVNR7-DB3VJHgR.js → railroad-peg-JR7BVNR7-BN6NL8S-.js} +1 -1
  121. package/web/dist/assets/{railroadDiagram-Y72NRBL7-Cev7Y65k.js → railroadDiagram-Y72NRBL7-BsbA-9gP.js} +1 -1
  122. package/web/dist/assets/{requirementDiagram-EQYSW5RA-CqbwMLD8.js → requirementDiagram-EQYSW5RA-HUw4YqWd.js} +1 -1
  123. package/web/dist/assets/{sankeyDiagram-SY24JHO5-V2cyVi8q.js → sankeyDiagram-SY24JHO5-Dx1MOFRu.js} +1 -1
  124. package/web/dist/assets/{sequenceDiagram-BLEHNHQU-By6NJOPM.js → sequenceDiagram-BLEHNHQU-C0-Rzg3g.js} +1 -1
  125. package/web/dist/assets/{src-Da91fG3g.js → src-CuHygcpj.js} +1 -1
  126. package/web/dist/assets/{stateDiagram-v2-A6IESKWZ-BnPGX5UI.js → stateDiagram-v2-A6IESKWZ-BnEICGtg.js} +1 -1
  127. package/web/dist/assets/{swimlanes-B3ENRKFQ-C58BpIpm.js → swimlanes-B3ENRKFQ-CwDRCHZD.js} +1 -1
  128. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-DZ_aAXmG.js +8 -0
  129. package/web/dist/assets/{timeline-definition-JXYKGLGB-Hgo1DhnS.js → timeline-definition-JXYKGLGB-C4BVpgzc.js} +1 -1
  130. package/web/dist/assets/{treeView-D4JQ5SDB-DF0dfjqB.js → treeView-D4JQ5SDB-BBsvEMP3.js} +1 -1
  131. package/web/dist/assets/{treemap-SAJKECNS-CSSM5FqB.js → treemap-SAJKECNS-BYw6cxC8.js} +1 -1
  132. package/web/dist/assets/{usecaseDiagram-WPOLBNNX-BkBBD1NY.js → usecaseDiagram-WPOLBNNX-BkOXLgwP.js} +1 -1
  133. package/web/dist/assets/{vennDiagram-Q3Y3IDCW-Bm28keX_.js → vennDiagram-Q3Y3IDCW-Kgjk_n6B.js} +1 -1
  134. package/web/dist/assets/{wardley-7MLQ67FV-By7KgEQX.js → wardley-7MLQ67FV-BGVObhvJ.js} +1 -1
  135. package/web/dist/assets/{wardleyDiagram-IVAIDKWA-oVtIrnGa.js → wardleyDiagram-IVAIDKWA-i97uD6tL.js} +1 -1
  136. package/web/dist/assets/{xychartDiagram-4URNHDU4-aI8FH4g2.js → xychartDiagram-4URNHDU4-awJi5TfJ.js} +1 -1
  137. package/web/dist/index.html +15 -2
  138. package/web/dist/assets/channel-C3AQ6nAs.js +0 -1
  139. package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +0 -1
  140. package/web/dist/assets/index-Bjn56a2i.js +0 -76
  141. package/web/dist/assets/index-Drp6e5iU.css +0 -1
  142. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +0 -8
@@ -0,0 +1,61 @@
1
+ /**
2
+ * opencode 的「会话目录清单」——只回答一个问题:侧栏该向哪些目录点名。
3
+ *
4
+ * 为什么需要它:ACP 的 `session/list` 是**目录作用域**的(opencode-sessions.ts 文件头有实测):
5
+ * - 不带 cwd:只拿 acp 子进程自己那个目录的会话;
6
+ * - 带 `{cwd}`:只拿 `directory` **精确等于** cwd 的会话(不是整棵项目树)。
7
+ * 所以「全部历史」= 把「有过会话的目录」逐个报给它。目录从哪来?
8
+ * - 工作台自己的工作区 + pi 的历史会话项目:只覆盖「在 pi 里也用过」的目录;
9
+ * - **opencode 自己的 `session.directory`**:这是全量答案(只在 opencode 里用过的目录差别很大)。
10
+ * 所以这里读一次 SQLite —— **只读、只取目录一列、失败就算了**(返回空数组)。
11
+ *
12
+ * 为什么可以读:`export` 是会话正文的稳定出口(正文结构会变,所以走 CLI),
13
+ * 而「有哪些目录有过会话」只是个索引,读不到就退化到前两个来源,不会让侧栏整体失败。
14
+ * 依赖也现成:Node ≥22.19 自带 `node:sqlite`(实验性,但不需要装包)。
15
+ */
16
+ import { existsSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+ /** opencode 的库文件:跟随 XDG(它自己也是这么找数据的) */
20
+ export function opencodeDbFile(env = process.env, home = homedir()) {
21
+ const xdg = env.XDG_DATA_HOME?.trim();
22
+ const dataHome = xdg && xdg !== "" ? xdg : join(home, ".local", "share");
23
+ return join(dataHome, "opencode", "opencode.db");
24
+ }
25
+ /**
26
+ * 读「有过会话的目录」(`SELECT DISTINCT directory FROM session`)。
27
+ *
28
+ * 失败一律返回空数组:库文件不存在、被锁(opencode 正在写 WAL)、表结构变了、`node:sqlite` 用不了……
29
+ * 这些都只该让「目录发现」退化成「只知道工作区 + pi 项目」,不该冒泡。
30
+ */
31
+ export async function readOpencodeSessionDirs(options = {}) {
32
+ const dbFile = options.dbFile ?? opencodeDbFile();
33
+ if (!existsSync(dbFile))
34
+ return [];
35
+ try {
36
+ const { DatabaseSync } = await import("node:sqlite");
37
+ const db = new DatabaseSync(dbFile, { readOnly: true });
38
+ try {
39
+ // 只读连接也可能撞上写锁(opencode 正在写 WAL):等一下就走,别把侧栏刷新卡住
40
+ try {
41
+ db.exec("PRAGMA busy_timeout = 500");
42
+ }
43
+ catch {
44
+ // PRAGMA 不被允许也不影响这次只读查询
45
+ }
46
+ const rows = db
47
+ .prepare("SELECT DISTINCT directory FROM session WHERE directory IS NOT NULL AND directory <> ''")
48
+ .all();
49
+ return rows
50
+ .map((row) => (typeof row.directory === "string" ? row.directory.trim() : ""))
51
+ .filter((directory) => directory !== "");
52
+ }
53
+ finally {
54
+ db.close();
55
+ }
56
+ }
57
+ catch (error) {
58
+ options.log?.(`读取 opencode 会话目录清单失败(退回工作区 + pi 项目):${error instanceof Error ? error.message : String(error)}`);
59
+ return [];
60
+ }
61
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * opencode 的「项目 / 会话清单」提供者 + 历史数据的读取口(export / delete)。
3
+ *
4
+ * 数据来源两条,各管一件事:
5
+ * 1. **清单**:走 ACP 的 `session/list`(同一个子进程,零额外开销),返回
6
+ * `{sessionId, cwd, title, updatedAt}` —— 没有 startedAt / model(这两个只能给 null)。
7
+ *
8
+ * **`session/list` 是「目录作用域」的(opencode 1.19.0 实测)**:
9
+ * - 不带参数:只返回 acp 子进程 cwd 那个目录的会话;
10
+ * - 带 `{cwd}`:只返回 `directory` **精确等于**它的会话(不是整棵项目树,也不按 git 根合并)。
11
+ * 曾经以为不带参就是全局的,于是侧栏只看得到「宿主从哪个目录启动」那一个目录,其余全丢。
12
+ * 现在的做法是**逐个目录点名**:宿主给出「有过会话的目录集合」
13
+ * (见 wiring.ts 的 opencodeWorkspaces / opencode-projects.ts),这里并发点名 + 按 sessionId 去重。
14
+ * 2. **转录与统计**:走 CLI `opencode export <sid>`(stdout 是纯 JSON,日志走 stderr)。
15
+ * 为什么不自己读 SQLite:会话正文的结构会变,`export` 是它自己给的稳定出口
16
+ * (「有哪些目录有过会话」这种索引才敢只读一次,见 opencode-projects.ts)。
17
+ *
18
+ * 分组规则与 codex / dsh 保持一致:按 cwd 分组、组名取目录名、临时目录里的会话不进侧栏。
19
+ */
20
+ import { spawn } from "node:child_process";
21
+ import { basename } from "node:path";
22
+ import { isScratchCwd } from "./scratch.js";
23
+ /** 单个项目的 `session/list` 超时:侧栏刷新不该被一个卡住的项目拖死 */
24
+ const PROJECT_LIST_TIMEOUT_MS = 10_000;
25
+ const timestampOf = (value) => {
26
+ if (!value)
27
+ return 0;
28
+ const parsed = Date.parse(value);
29
+ return Number.isFinite(parsed) ? parsed : 0;
30
+ };
31
+ /**
32
+ * 纯分组:`session/list` 的行 → 侧栏形状(单测直接覆盖)。
33
+ *
34
+ * - 临时目录(/tmp、/var/folders…)里的一次性会话丢掉,别塞噪音进侧栏;
35
+ * - 同 cwd 归一组,组内按更新时间倒序;组之间按「最近一条会话」倒序;
36
+ * - 没有标题的会话不编名字(opencode 自己会给「New session - …」,照它的原文显示)。
37
+ */
38
+ export function groupOpencodeSessions(rows) {
39
+ // 逐个项目点名时同一条会话会被问到多次(项目根与它的子目录都解析到同一个项目),先去重:
40
+ // 同一个 sessionId 只留 updatedAt 最新的一条。
41
+ const byId = new Map();
42
+ for (const row of rows) {
43
+ const id = row.sessionId;
44
+ if (!id)
45
+ continue;
46
+ const previous = byId.get(id);
47
+ if (!previous || timestampOf(row.updatedAt) >= timestampOf(previous.updatedAt))
48
+ byId.set(id, row);
49
+ }
50
+ const groups = new Map();
51
+ for (const row of byId.values()) {
52
+ const id = row.sessionId;
53
+ if (!id)
54
+ continue;
55
+ const cwd = row.cwd ?? "";
56
+ if (isScratchCwd(cwd))
57
+ continue;
58
+ const updatedAt = timestampOf(row.updatedAt);
59
+ const group = groups.get(cwd) ?? {
60
+ cwd,
61
+ name: basename(cwd) || cwd || "未分项目",
62
+ sessions: [],
63
+ };
64
+ group.sessions.push({
65
+ id,
66
+ engineSessionId: id,
67
+ title: (row.title ?? "").trim() || "(未命名会话)",
68
+ // ACP 不给创建时间:用更新时间兜底(侧栏只按 updatedAt 排序,不会显示错)
69
+ startedAt: updatedAt,
70
+ updatedAt,
71
+ cwd,
72
+ model: null,
73
+ });
74
+ groups.set(cwd, group);
75
+ }
76
+ return [...groups.values()]
77
+ .map((group) => ({
78
+ cwd: group.cwd,
79
+ name: group.name,
80
+ sessions: [...group.sessions].sort((left, right) => right.updatedAt - left.updatedAt),
81
+ }))
82
+ .sort((left, right) => (right.sessions[0]?.updatedAt ?? 0) - (left.sessions[0]?.updatedAt ?? 0));
83
+ }
84
+ export function createOpencodeSessionsProvider(options) {
85
+ const cacheMs = options.cacheMs ?? 0;
86
+ const now = options.now ?? Date.now;
87
+ let cached = null;
88
+ /**
89
+ * 候选目录:去掉空串与重复。
90
+ *
91
+ * 故意**不**过滤「目录已经不存在」的:历史会话还在,点不开是用户自己删了目录的事;
92
+ * pi / codex / dsh 的侧栏也是这个口径(按会话里记的 cwd 分组)。实测点名不存在的目录
93
+ * 也不会在 opencode 的库里留下任何记录(就是空结果)。
94
+ */
95
+ const targets = async () => {
96
+ let raw = [];
97
+ try {
98
+ raw = (await options.workspaces?.()) ?? [];
99
+ }
100
+ catch (error) {
101
+ options.log?.(`枚举 opencode 候选项目目录失败:${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ const dirs = [...new Set(raw.map((dir) => dir.trim()).filter((dir) => dir !== ""))];
104
+ return dirs.length > 0 ? dirs : [undefined];
105
+ };
106
+ /** 并发点名所有项目。单个项目失败不影响其它项目(侧栏能显示多少是多少) */
107
+ const fetchAll = async () => {
108
+ const list = await targets();
109
+ const settled = await Promise.allSettled(list.map((cwd) => options.client.request("session/list", cwd ? { cwd } : {}, {
110
+ timeoutMs: PROJECT_LIST_TIMEOUT_MS,
111
+ })));
112
+ const rows = [];
113
+ for (const [index, item] of settled.entries()) {
114
+ if (item.status === "fulfilled") {
115
+ rows.push(...(item.value?.sessions ?? []));
116
+ continue;
117
+ }
118
+ const cwd = list[index] ?? "(默认)";
119
+ const reason = item.reason instanceof Error ? item.reason.message : String(item.reason);
120
+ options.log?.(`读取 opencode 会话清单失败(${cwd}):${reason}`);
121
+ }
122
+ return groupOpencodeSessions(rows);
123
+ };
124
+ return async () => {
125
+ if (cacheMs > 0 && cached && now() - cached.at < cacheMs)
126
+ return cached.value;
127
+ let value;
128
+ try {
129
+ value = await fetchAll();
130
+ }
131
+ catch (error) {
132
+ // 清单读不到不该让侧栏整体失败:给出上一次的结果(可能为空),并留一条日志
133
+ options.log?.(`读取 opencode 会话清单失败:${error instanceof Error ? error.message : String(error)}`);
134
+ return cached?.value ?? [];
135
+ }
136
+ cached = { at: now(), value };
137
+ return value;
138
+ };
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // CLI 出口:export / delete
142
+ // ---------------------------------------------------------------------------
143
+ /**
144
+ * 跑一次 `opencode <args>`,收 stdout(`export` 的日志走 stderr,不混进来)。
145
+ * 失败返回 null / 抛错由调用方决定 —— 历史读不到不该让对话区崩掉。
146
+ */
147
+ function runOpencode(bin, args, options = {}) {
148
+ return new Promise((resolve, reject) => {
149
+ const child = spawn(bin, args, { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
150
+ let stdout = "";
151
+ let stderr = "";
152
+ const timer = setTimeout(() => {
153
+ child.kill("SIGTERM");
154
+ reject(new Error(`opencode ${args[0]} 超时`));
155
+ }, options.timeoutMs ?? 30_000);
156
+ child.stdout?.setEncoding("utf8");
157
+ child.stdout?.on("data", (chunk) => {
158
+ stdout += chunk;
159
+ });
160
+ child.stderr?.setEncoding("utf8");
161
+ child.stderr?.on("data", (chunk) => {
162
+ stderr = `${stderr}${chunk}`.slice(-2000);
163
+ });
164
+ child.on("error", (error) => {
165
+ clearTimeout(timer);
166
+ reject(new Error(`opencode ${args[0]} 启动失败:${error.message}`));
167
+ });
168
+ child.on("exit", (code) => {
169
+ clearTimeout(timer);
170
+ if (code === 0) {
171
+ resolve(stdout);
172
+ return;
173
+ }
174
+ reject(new Error(`opencode ${args[0]} 退出(code ${code ?? "?"})${stderr.trim() ? `:${stderr.trim()}` : ""}`));
175
+ });
176
+ });
177
+ }
178
+ /** 读一条会话的完整导出(JSON)。读不到返回 null(界面上就是「暂无历史」) */
179
+ export async function readOpencodeExport(bin, sessionId, options = {}) {
180
+ try {
181
+ const stdout = await runOpencode(bin, ["export", sessionId], { timeoutMs: options.timeoutMs });
182
+ const start = stdout.indexOf("{");
183
+ if (start < 0)
184
+ return null;
185
+ return JSON.parse(stdout.slice(start));
186
+ }
187
+ catch (error) {
188
+ options.log?.(`读取 opencode 会话导出失败(${sessionId}):${error instanceof Error ? error.message : String(error)}`);
189
+ return null;
190
+ }
191
+ }
192
+ /** 删一条会话(opencode 原生删除:`opencode session delete <sid>`) */
193
+ export async function deleteOpencodeSession(bin, sessionId) {
194
+ await runOpencode(bin, ["session", "delete", sessionId], { timeoutMs: 20_000 });
195
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * opencode ACP 客户端 = 通用 JSON-RPC stdio + ACP 握手。
3
+ *
4
+ * 为什么走 ACP(`opencode acp`)而不是 SDK / `opencode run --format json`:
5
+ * - `opencode run` 是「一次性回合」:没有权限问询、没有 `session/cancel`(打断只能杀进程)、
6
+ * 也没法在一次进程里换模型 —— 只能当兜底,不能当引擎;
7
+ * - HTTP(`opencode serve`)+ `@opencode-ai/sdk` 能力最全,但要多管一个带鉴权的子进程 +
8
+ * 自己解析 SSE,且审批要在 HTTP 上回包;
9
+ * - ACP 原生给了我们端口需要的全部动作:`session/new|list|resume|load|close`、
10
+ * `session/set_config_option`(模型 / 思考档 / 模式)、`session/cancel`、
11
+ * `session/request_permission`,外加带分桶的 usage。与 dsh 走的是同一套协议,
12
+ * 所以复用 `jsonrpc-stdio.ts`(换的只有可执行文件与握手参数)。
13
+ *
14
+ * 实测(opencode 1.19.0):
15
+ * initialize → `{protocolVersion:1, agentCapabilities:{loadSession:true, sessionCapabilities:{list,resume,fork,close},
16
+ * promptCapabilities:{image:true, embeddedContext:true}}, agentInfo:{name:"OpenCode", version:"1.19.0"}}`
17
+ */
18
+ import { createJsonRpcStdioClient } from "./jsonrpc-stdio.js";
19
+ /**
20
+ * 握手时声明的客户端能力。
21
+ *
22
+ * 全部 `false`:工具都在 opencode 自己的进程里跑,我们不当它的 fs / terminal 桥
23
+ * (与 dsh 一致)。opencode 的 `promptCapabilities.image` 是**它**收图,不需要我们这边声明。
24
+ */
25
+ const CLIENT_CAPABILITIES = {
26
+ fs: { readTextFile: false, writeTextFile: false },
27
+ terminal: false,
28
+ };
29
+ export function createOpencodeClient(options = {}) {
30
+ return createJsonRpcStdioClient({
31
+ bin: options.bin ?? "opencode",
32
+ args: ["acp", ...(options.args ?? [])],
33
+ env: options.env,
34
+ cwd: options.cwd,
35
+ name: "opencode",
36
+ log: options.log ?? (() => { }),
37
+ timeoutMs: options.timeoutMs,
38
+ // ACP 是标准 JSON-RPC 2.0:帧里必须带 jsonrpc 字段
39
+ jsonrpc: true,
40
+ async handshake(call) {
41
+ const result = (await call("initialize", {
42
+ protocolVersion: 1,
43
+ clientCapabilities: CLIENT_CAPABILITIES,
44
+ }));
45
+ return { version: result?.agentInfo?.version ?? null };
46
+ },
47
+ });
48
+ }