oh-my-agent-web 0.2.0 → 0.4.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 (250) hide show
  1. package/README.md +86 -447
  2. package/dist/server/approval/read-only.js +107 -0
  3. package/dist/server/approval/rules.js +85 -0
  4. package/dist/server/approval/scope.js +150 -0
  5. package/dist/server/brand.js +2 -0
  6. package/dist/server/cli-config.js +52 -0
  7. package/dist/server/cli-info.js +101 -0
  8. package/dist/server/cli.js +304 -0
  9. package/dist/server/engine/builtin.js +61 -0
  10. package/dist/server/engine/catalog.js +8 -0
  11. package/dist/server/engine/codex-approvals.js +45 -0
  12. package/dist/server/engine/codex-map.js +298 -0
  13. package/dist/server/engine/codex-sessions.js +159 -0
  14. package/dist/server/engine/codex-transport.js +49 -0
  15. package/dist/server/engine/codex.js +314 -0
  16. package/dist/server/engine/dsh-history.js +195 -0
  17. package/dist/server/engine/dsh-log.js +245 -0
  18. package/dist/server/engine/dsh-map.js +221 -0
  19. package/dist/server/engine/dsh-sessions.js +207 -0
  20. package/dist/server/engine/dsh-transport.js +37 -0
  21. package/dist/server/engine/dsh.js +284 -0
  22. package/dist/server/engine/fake.js +231 -0
  23. package/dist/server/engine/jsonrpc-stdio.js +241 -0
  24. package/dist/server/engine/model-registry-reload.js +115 -0
  25. package/dist/server/engine/pi-events.js +148 -0
  26. package/dist/server/engine/pi-rename.js +14 -0
  27. package/dist/server/engine/pi-sdk.js +343 -0
  28. package/dist/server/engine/pi-session-model.js +23 -0
  29. package/dist/server/engine/pi-thinking.js +34 -0
  30. package/dist/server/engine/pi-transcript.js +225 -0
  31. package/dist/server/engine/pi-version.js +11 -0
  32. package/dist/server/engine/port.js +1 -0
  33. package/dist/server/engine/registry.js +47 -0
  34. package/dist/server/engine/scratch.js +13 -0
  35. package/dist/server/engine/suggestion-guidance.js +9 -0
  36. package/dist/server/engine/switchable.js +22 -0
  37. package/dist/server/engine/wiring.js +107 -0
  38. package/dist/server/files/dir-routes.js +40 -0
  39. package/dist/server/files/file-search.js +63 -0
  40. package/dist/server/files/file-service.js +232 -0
  41. package/dist/server/files/files-routes.js +182 -0
  42. package/dist/server/files/git-info.js +186 -0
  43. package/dist/server/http/auth-routes.js +99 -0
  44. package/dist/server/http/auth.js +191 -0
  45. package/dist/server/http/aux-routes.js +24 -0
  46. package/dist/server/http/compress.js +75 -0
  47. package/dist/server/http/health-and-static.js +33 -0
  48. package/dist/server/http/live-routes.js +113 -0
  49. package/dist/server/http/login-page.js +200 -0
  50. package/dist/server/index.js +342 -520
  51. package/dist/server/markers/actions.js +262 -0
  52. package/dist/server/protocol-parse.js +201 -0
  53. package/dist/server/protocol-settings.js +1 -0
  54. package/dist/server/protocol.js +6 -0
  55. package/dist/server/providers/catalog-store.js +85 -0
  56. package/dist/server/providers/catalog-writes.js +197 -0
  57. package/dist/server/providers/catalog.js +77 -0
  58. package/dist/server/providers/enabled-models.js +118 -0
  59. package/dist/server/providers/import-models.js +69 -0
  60. package/dist/server/providers/import-routes.js +136 -0
  61. package/dist/server/providers/model-test.js +52 -0
  62. package/dist/server/providers/probe.js +64 -0
  63. package/dist/server/providers/provider-handlers.js +203 -0
  64. package/dist/server/providers/provider-store.js +327 -0
  65. package/dist/server/providers/request-headers.js +7 -0
  66. package/dist/server/providers/validate.js +241 -0
  67. package/dist/server/push/push-routes.js +114 -0
  68. package/dist/server/push/push.js +120 -0
  69. package/dist/server/session/approval-bridge.js +103 -0
  70. package/dist/server/session/attachment-resolve.js +21 -0
  71. package/dist/server/session/client-view.js +82 -0
  72. package/dist/server/session/conversation-create.js +65 -0
  73. package/dist/server/session/delete-session.js +84 -0
  74. package/dist/server/session/engine-switch.js +108 -0
  75. package/dist/server/session/lock.js +82 -0
  76. package/dist/server/session/model-controller.js +111 -0
  77. package/dist/server/session/notices.js +31 -0
  78. package/dist/server/session/pi-opener.js +123 -0
  79. package/dist/server/session/pi-refresh.js +51 -0
  80. package/dist/server/session/pi-sessions-provider.js +46 -0
  81. package/dist/server/session/pi-sessions.js +272 -0
  82. package/dist/server/session/refresh-actions.js +42 -0
  83. package/dist/server/session/rename-session.js +23 -0
  84. package/dist/server/session/runtime-services.js +28 -0
  85. package/dist/server/session/runtime-types.js +1 -0
  86. package/dist/server/session/runtime.js +358 -0
  87. package/dist/server/session/session-stats.js +88 -0
  88. package/dist/server/session/settings.js +367 -0
  89. package/dist/server/session/snapshot-for.js +7 -0
  90. package/dist/server/session/stats-refresher.js +50 -0
  91. package/dist/server/session/switch-conversation.js +86 -0
  92. package/dist/server/session/throttle.js +14 -0
  93. package/dist/server/session/turn-events.js +126 -0
  94. package/dist/server/session/turn-finish.js +79 -0
  95. package/dist/server/session/turn-markers.js +26 -0
  96. package/dist/server/session/turn-queue.js +111 -0
  97. package/dist/server/session/turn-send.js +88 -0
  98. package/dist/server/session/turn-watchdog.js +77 -0
  99. package/dist/server/sessions/session-routes.js +17 -0
  100. package/dist/server/skills/skill-routes.js +28 -0
  101. package/dist/server/skills/skill-store.js +134 -0
  102. package/dist/server/snapshot/fan-out.js +10 -0
  103. package/dist/server/snapshot/messages.js +75 -0
  104. package/dist/server/snapshot/sender.js +62 -0
  105. package/dist/server/snapshot/state.js +193 -0
  106. package/dist/server/terminal/terminal-routes.js +176 -0
  107. package/dist/server/terminal/terminal-session.js +140 -0
  108. package/dist/server/uploads/upload-routes.js +64 -0
  109. package/dist/server/uploads/upload-store.js +115 -0
  110. package/dist/server/util/data-dir.js +36 -0
  111. package/dist/server/util/env.js +140 -0
  112. package/dist/server/util/ids.js +28 -0
  113. package/dist/server/util/log-file.js +67 -0
  114. package/dist/server/util/log.js +52 -0
  115. package/dist/server/util/origin.js +75 -0
  116. package/dist/server/util/paths.js +60 -0
  117. package/dist/server/util/pricing-index.js +121 -0
  118. package/dist/server/util/pricing.js +214 -0
  119. package/dist/server/util/startup.js +34 -0
  120. package/dist/server/util/upstream-version.js +140 -0
  121. package/dist/server/util/usage.js +19 -0
  122. package/dist/server/util/version.js +50 -0
  123. package/dist/server/ws/handle-message.js +95 -0
  124. package/package.json +49 -63
  125. package/web/dist/apple-touch-icon-precomposed.png +0 -0
  126. package/web/dist/apple-touch-icon.png +0 -0
  127. package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
  128. package/web/dist/assets/arc-DviL9o3c.js +1 -0
  129. package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
  130. package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
  131. package/web/dist/assets/array-BifhSqXX.js +1 -0
  132. package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
  133. package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
  134. package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
  135. package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
  136. package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  137. package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
  138. package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
  139. package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  140. package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
  141. package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
  142. package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
  143. package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  144. package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
  145. package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
  146. package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  147. package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  148. package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  149. package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
  150. package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
  151. package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
  152. package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
  153. package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
  154. package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  155. package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
  156. package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
  157. package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
  158. package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
  159. package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
  160. package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
  161. package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
  162. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  163. package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
  164. package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  165. package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
  166. package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
  167. package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
  168. package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
  169. package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
  170. package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
  171. package/web/dist/assets/dist-C86cKTuE.js +1 -0
  172. package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
  173. package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
  174. package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
  175. package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
  176. package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
  177. package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
  178. package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
  179. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
  180. package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  181. package/web/dist/assets/index-Bjn56a2i.js +76 -0
  182. package/web/dist/assets/index-Drp6e5iU.css +1 -0
  183. package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
  184. package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
  185. package/web/dist/assets/init-C-OQMol4.js +1 -0
  186. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
  187. package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
  188. package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
  189. package/web/dist/assets/katex-B_JcIwqj.js +257 -0
  190. package/web/dist/assets/line-CHV9_4lL.js +1 -0
  191. package/web/dist/assets/linear-C-eZsBTv.js +1 -0
  192. package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
  193. package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
  194. package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
  195. package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  196. package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
  197. package/web/dist/assets/path-fybaL0A-.js +1 -0
  198. package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
  199. package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
  200. package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
  201. package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
  202. package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
  203. package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
  204. package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
  205. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
  206. package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
  207. package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
  208. package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
  209. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  210. package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
  211. package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
  212. package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  213. package/web/dist/assets/src-Da91fG3g.js +1 -0
  214. package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
  215. package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
  216. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
  217. package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
  218. package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
  219. package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
  220. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
  221. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
  222. package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
  223. package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
  224. package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
  225. package/web/dist/favicon-32.png +0 -0
  226. package/web/dist/favicon.ico +0 -0
  227. package/web/dist/favicon.svg +20 -8
  228. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  229. package/web/dist/icons/icon-192.png +0 -0
  230. package/web/dist/icons/icon-512.png +0 -0
  231. package/web/dist/icons/icon-maskable-512.png +0 -0
  232. package/web/dist/index.html +21 -27
  233. package/web/dist/manifest.webmanifest +17 -0
  234. package/web/dist/maskable.svg +25 -0
  235. package/web/dist/sw.js +40 -0
  236. package/bin/oh-my-agent-web-ps.mjs +0 -203
  237. package/bin/oh-my-agent-web-ws.mjs +0 -593
  238. package/bin/oh-my-agent-web.mjs +0 -321
  239. package/bin/update-check.mjs +0 -142
  240. package/dist/server/codex-client.js +0 -310
  241. package/dist/server/codex-supervisor.js +0 -446
  242. package/dist/server/fs-service.js +0 -144
  243. package/dist/server/request-inspector.js +0 -458
  244. package/dist/shared/protocol.js +0 -112
  245. package/scripts/manage-server.mjs +0 -155
  246. package/scripts/omaw.mjs +0 -74
  247. package/shared/protocol.ts +0 -228
  248. package/web/dist/assets/index-B8-VzDf2.css +0 -1
  249. package/web/dist/assets/index-DeC2eiiq.js +0 -42
  250. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 「这条命令是不是只读」(ADR-0006 的补充):不要把读操作也丢给用户确认。
3
+ *
4
+ * 真事:`sed -n 425,470p file` + `grep` 这种纯查看也会弹窗,用户被迫点几十次「允许一次」,
5
+ * 最后养成"闭眼允许"的习惯 —— 那比不问更危险。
6
+ *
7
+ * 判定偏保守(宁可多问一次,不要漏放一次写操作):
8
+ * 1. 命令里的每个可执行名都必须在白名单里;
9
+ * 2. 不能有 `>`、`>>`、`tee`、`&>` 这类写文件的重定向(`2>&1` 除外);
10
+ * 3. `sed`/`perl` 带 `-i`(原地改写)一律不算只读;
11
+ * 4. 不能有 `$(...)`、反引号、`rm/mv/cp/chmod/...` 这类明显的写操作。
12
+ */
13
+ /** 只读可执行名(能看、能过滤、能统计,但不落盘) */
14
+ const READ_ONLY_BINARIES = new Set([
15
+ "ls",
16
+ "cat",
17
+ "head",
18
+ "tail",
19
+ "wc",
20
+ "grep",
21
+ "egrep",
22
+ "rg",
23
+ "ag",
24
+ "find",
25
+ "fd",
26
+ "sed",
27
+ "awk",
28
+ "cut",
29
+ "sort",
30
+ "uniq",
31
+ "tr",
32
+ "echo",
33
+ "printf",
34
+ "pwd",
35
+ "whoami",
36
+ "date",
37
+ "which",
38
+ "type",
39
+ "file",
40
+ "stat",
41
+ "du",
42
+ "df",
43
+ "tree",
44
+ "jq",
45
+ "basename",
46
+ "dirname",
47
+ "realpath",
48
+ "md5sum",
49
+ "shasum",
50
+ "sha256sum",
51
+ "diff",
52
+ "comm",
53
+ "column",
54
+ "nl",
55
+ "rev",
56
+ "seq",
57
+ "expr",
58
+ "test",
59
+ "true",
60
+ "false",
61
+ ]);
62
+ /** 一旦出现这些词,直接判定「不是只读」 */
63
+ const WRITE_HINTS = /\b(rm|rmdir|mv|cp|mkdir|touch|chmod|chown|ln|install|dd|mkfs|truncate|tee|git|npm|pnpm|yarn|pip|brew|docker|kubectl|curl|wget|ssh|scp)\b/;
64
+ /** 把一个 shell 片段按 `&&`、`||`、`;`、`|`、换行拆开(不处理引号里的分隔符 —— 保守起见够了) */
65
+ function splitCommands(command) {
66
+ return command
67
+ .split(/\n|&&|\|\||;|\|/)
68
+ .map((part) => part.trim())
69
+ .filter((part) => part !== "");
70
+ }
71
+ /** 取一段命令的可执行名(跳过 `FOO=1 cmd` 这种前置赋值与 sudo/env 之类) */
72
+ function executableOf(part) {
73
+ const tokens = part.split(/\s+/).filter((token) => token !== "");
74
+ let index = 0;
75
+ // 跳过环境变量赋值
76
+ while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? ""))
77
+ index += 1;
78
+ const name = tokens[index];
79
+ if (!name)
80
+ return null;
81
+ return name.split("/").pop() ?? name;
82
+ }
83
+ export function isReadOnlyCommand(command) {
84
+ const text = command.trim();
85
+ if (text === "")
86
+ return false;
87
+ // 命令替换、反引号:内容不可预知 → 不算只读
88
+ if (text.includes("$(") || text.includes("`"))
89
+ return false;
90
+ if (WRITE_HINTS.test(text))
91
+ return false;
92
+ // 写文件的重定向(`2>&1`、`2> /dev/null` 这类放行)
93
+ if (/(^|[^0-9])>>?/.test(text.replace(/[0-9]?>&[0-9]/g, "").replace(/2>\s*\/dev\/null/g, "")))
94
+ return false;
95
+ // sed -i / perl -i 原地改写
96
+ if (/\bsed\b[^|;]*\s-i/.test(text) || /\bperl\b[^|;]*\s-i/.test(text))
97
+ return false;
98
+ // 逐个片段检查可执行名
99
+ for (const part of splitCommands(text)) {
100
+ const name = executableOf(part);
101
+ if (name === null)
102
+ return false;
103
+ if (!READ_ONLY_BINARIES.has(name))
104
+ return false;
105
+ }
106
+ return true;
107
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 审批规则落盘:`<dataDir>/approval-rules.json`(ADR-0006)。
3
+ *
4
+ * 规则形状刻意保持「人能手工编辑」的程度:
5
+ * { "v": 1, "rules": [{ "scopeKey": "bash|npm", "scopeHint": "命令 npm", "createdAt": 1699999999999 }] }
6
+ *
7
+ * 三条纪律:
8
+ * 1. 文件坏了/被删了 → 当作没有规则(**不能**因此拒绝启动;规则只是便利,不是安全边界);
9
+ * 2. 写盘原子(temp + rename),避免半截 JSON;
10
+ * 3. 规则只增不减(v1 没有管理 UI,要删就手工编辑文件)。
11
+ */
12
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
13
+ import { dirname, join } from "node:path";
14
+ export const RULES_FILE = "approval-rules.json";
15
+ export const RULES_VERSION = 1;
16
+ export function rulesPath(dataDir) {
17
+ return join(dataDir, RULES_FILE);
18
+ }
19
+ function isRule(value) {
20
+ if (typeof value !== "object" || value === null)
21
+ return false;
22
+ const candidate = value;
23
+ return typeof candidate.scopeKey === "string" && candidate.scopeKey !== "";
24
+ }
25
+ /** 纯函数:解析规则文件;坏内容一律返回空规则(不抛错) */
26
+ export function parseRules(text) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(text);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ if (typeof parsed !== "object" || parsed === null)
35
+ return [];
36
+ const rules = parsed.rules;
37
+ if (!Array.isArray(rules))
38
+ return [];
39
+ return rules.filter(isRule).map((rule) => ({
40
+ scopeKey: rule.scopeKey,
41
+ scopeHint: typeof rule.scopeHint === "string" ? rule.scopeHint : rule.scopeKey,
42
+ createdAt: typeof rule.createdAt === "number" ? rule.createdAt : 0,
43
+ }));
44
+ }
45
+ export function hasRule(rules, scopeKey) {
46
+ return rules.some((rule) => rule.scopeKey === scopeKey);
47
+ }
48
+ /** 纯函数:加一条规则(已存在则原样返回,保持幂等) */
49
+ export function withRule(rules, rule) {
50
+ if (hasRule(rules, rule.scopeKey))
51
+ return rules;
52
+ return [...rules, rule];
53
+ }
54
+ export function createFileRuleStore(dataDir) {
55
+ return {
56
+ async load() {
57
+ try {
58
+ return parseRules(await readFile(rulesPath(dataDir), "utf8"));
59
+ }
60
+ catch {
61
+ return []; // 文件不存在/读不了 = 没有规则
62
+ }
63
+ },
64
+ async save(rules) {
65
+ const target = rulesPath(dataDir);
66
+ await mkdir(dirname(target), { recursive: true });
67
+ const payload = { v: RULES_VERSION, rules };
68
+ const temporary = `${target}.tmp`;
69
+ await writeFile(temporary, `${JSON.stringify(payload, null, "\t")}\n`, "utf8");
70
+ await rename(temporary, target);
71
+ },
72
+ };
73
+ }
74
+ /** 内存版(单测 / 不落盘运行) */
75
+ export function createMemoryRuleStore(seed = []) {
76
+ let rules = [...seed];
77
+ return {
78
+ async load() {
79
+ return [...rules];
80
+ },
81
+ async save(next) {
82
+ rules = [...next];
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * 「这类操作」的归类(ADR-0006 审批粒度)。
3
+ *
4
+ * ADR-0006 的原话:「这类」= 工具名 + 一个稳定特征:命令类取**可执行名**,文件类取**目标目录**
5
+ * (不用完整命令行,否则永远命中不了)。
6
+ *
7
+ * 全部是纯函数:给定工具名与参数,产出
8
+ * - `dangerous`:要不要问(读类不问,写/改/跑命令要问)
9
+ * - `scopeHint`:落到规则文件里的稳定键
10
+ * - `riskNote`:给人看的一句话(为什么问)
11
+ */
12
+ import { isAbsolute, join, resolve } from "node:path";
13
+ import { isReadOnlyCommand } from "./read-only.js";
14
+ /** 不问的工具:只读类 */
15
+ export const READ_ONLY_TOOLS = new Set(["read", "grep", "find", "ls"]);
16
+ const SUMMARY_CHARS = 200;
17
+ const RISK_NOTES = {
18
+ bash: "跑命令会修改你的机器(装包、删文件、改配置都算),确认后才能执行",
19
+ write: "写文件会改动磁盘内容,确认后才能执行",
20
+ edit: "改文件会改动已有内容,确认后才能执行",
21
+ powershell: "跑 PowerShell 命令会修改你的机器,确认后才能执行",
22
+ };
23
+ function truncate(text, limit = SUMMARY_CHARS) {
24
+ return text.length > limit ? `${text.slice(0, limit)}…` : text;
25
+ }
26
+ /** 包装命令(跳过它们去找真正的可执行名) */
27
+ const WRAPPERS = new Set(["sudo", "env", "command", "nohup", "time", "nice", "stdbuf"]);
28
+ /** 这些选项后面还跟一个值(`sudo -u root …`)—— 跳选项时要一起跳 */
29
+ const VALUE_OPTIONS = new Set([
30
+ "-u",
31
+ "-g",
32
+ "-p",
33
+ "-C",
34
+ "-h",
35
+ "-U",
36
+ "-T",
37
+ "-r",
38
+ "-t",
39
+ "--user",
40
+ "--group",
41
+ "--prompt",
42
+ "--host",
43
+ "--chdir",
44
+ ]);
45
+ const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
46
+ /**
47
+ * 从命令行里取「可执行名」:跳过 `FOO=bar` 环境赋值、`sudo`/`env`/`nohup` 之类包装(含它们的选项与取值)、
48
+ * 以及路径前缀。
49
+ * 例:`FOO=1 npm run build` → `npm`;`sudo -u root npm ci` → `npm`;`/usr/bin/git push -f` → `git`
50
+ */
51
+ export function commandExecutable(command) {
52
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
53
+ let index = 0;
54
+ skipLeading: for (;;) {
55
+ const token = tokens[index];
56
+ if (token === undefined)
57
+ break;
58
+ if (ENV_ASSIGNMENT_RE.test(token)) {
59
+ index += 1;
60
+ continue;
61
+ }
62
+ if (WRAPPERS.has(token)) {
63
+ index += 1;
64
+ // 跳过包装命令自己的选项与取值(`sudo -u root` 里的 `-u root`)
65
+ while (index < tokens.length) {
66
+ const next = tokens[index];
67
+ if (ENV_ASSIGNMENT_RE.test(next)) {
68
+ index += 1;
69
+ continue;
70
+ }
71
+ if (!next.startsWith("-"))
72
+ break;
73
+ index += VALUE_OPTIONS.has(next) ? 2 : 1;
74
+ }
75
+ continue skipLeading;
76
+ }
77
+ break;
78
+ }
79
+ const token = tokens[index] ?? "";
80
+ const base = token.split(/[\\/]/).pop() ?? token;
81
+ return base === "" ? "(空命令)" : base;
82
+ }
83
+ /** 目标路径所在目录(相对路径按工作区根解析) */
84
+ export function targetDirectory(path, workspace) {
85
+ if (path === "")
86
+ return workspace;
87
+ const absolute = isAbsolute(path) ? path : resolve(join(workspace, path));
88
+ const index = absolute.lastIndexOf("/");
89
+ return index <= 0 ? absolute : absolute.slice(0, index);
90
+ }
91
+ /** 从工具调用参数里抠出最有信息量的那个字段(不同工具的 schema 不同,这里只做尽力而为) */
92
+ function pickString(args, keys) {
93
+ if (typeof args !== "object" || args === null)
94
+ return null;
95
+ const record = args;
96
+ for (const key of keys) {
97
+ const value = record[key];
98
+ if (typeof value === "string" && value !== "")
99
+ return value;
100
+ }
101
+ return null;
102
+ }
103
+ export function classifyScope(input) {
104
+ const { toolName, args, workspace } = input;
105
+ const dangerous = !READ_ONLY_TOOLS.has(toolName);
106
+ const argsSummary = truncate(typeof args === "string" ? args : JSON.stringify(args ?? {}));
107
+ if (toolName === "bash" || toolName === "powershell") {
108
+ const command = pickString(args, ["command", "cmd", "script"]) ?? "";
109
+ const executable = commandExecutable(command);
110
+ // 纯读命令(sed -n / grep / cat …)不问:把"闭眼允许"的口子关掉
111
+ if (isReadOnlyCommand(command)) {
112
+ return {
113
+ dangerous: false,
114
+ toolName,
115
+ scopeKey: `${toolName}|${executable}`,
116
+ scopeHint: `命令 ${executable}`,
117
+ riskNote: "",
118
+ argsSummary: truncate(command),
119
+ };
120
+ }
121
+ return {
122
+ dangerous,
123
+ toolName,
124
+ scopeKey: `${toolName}|${executable}`,
125
+ scopeHint: `命令 ${executable}`,
126
+ riskNote: RISK_NOTES[toolName] ?? "这条命令会改动你的机器,确认后才能执行",
127
+ argsSummary: truncate(command || argsSummary),
128
+ };
129
+ }
130
+ const path = pickString(args, ["path", "file", "filePath", "target"]);
131
+ if (path) {
132
+ const directory = targetDirectory(path, workspace);
133
+ return {
134
+ dangerous,
135
+ toolName,
136
+ scopeKey: `${toolName}|${directory}`,
137
+ scopeHint: `目录 ${directory}`,
138
+ riskNote: RISK_NOTES[toolName] ?? "这个操作会改动磁盘内容,确认后才能执行",
139
+ argsSummary,
140
+ };
141
+ }
142
+ return {
143
+ dangerous,
144
+ toolName,
145
+ scopeKey: `${toolName}|*`,
146
+ scopeHint: `所有 ${toolName} 调用`,
147
+ riskNote: RISK_NOTES[toolName] ?? "这个操作有副作用,确认后才能执行",
148
+ argsSummary,
149
+ };
150
+ }
@@ -0,0 +1,2 @@
1
+ /** 服务端侧的同一个自称(登录框 realm、推送通知兜底标题)——与 web/src/brand.ts 保持一致 */
2
+ export const BRAND_NAME = "oh-my-agent-web";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 命令行选项的「持久化兜底」:`<dataDir>/password` 与 `<dataDir>/allowed-hosts`。
3
+ *
4
+ * 为什么需要:`restart` 的参数是每次从命令行重新给的,忘了带 `-P` 就会把远程访问关掉
5
+ * (真实踩过一次:重启后远程立刻 403「未设置 AGENT_WEB_PASSWORD」)。
6
+ * 这两个文件让「设一次就一直在」,而命令行/环境变量仍然优先级最高。
7
+ *
8
+ * 纪律:读不到、读坏了都不致命 —— 没有密码文件就等同于「只允许本机」,和以前一致。
9
+ */
10
+ import { readFile } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ export const PASSWORD_FILE = "password";
13
+ export const ALLOWED_HOSTS_FILE = "allowed-hosts";
14
+ const readTrimmed = async (path) => {
15
+ try {
16
+ const text = await readFile(path, "utf8");
17
+ // 只取第一行:顺手忽略编辑器留下的空行/换行,但密码里不允许出现空白
18
+ const first = (text.split("\n")[0] ?? "").trim();
19
+ return first === "" ? null : first;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ };
25
+ /** 纯读取:把两个文件都试一遍(不存在就给空值) */
26
+ export async function readStoredOptions(dataDir) {
27
+ const password = await readTrimmed(join(dataDir, PASSWORD_FILE));
28
+ const rawHosts = await readFile(join(dataDir, ALLOWED_HOSTS_FILE), "utf8").catch(() => "");
29
+ const allowedHosts = rawHosts
30
+ .split(/[\n,]/)
31
+ .map((item) => item.trim())
32
+ .filter((item) => item !== "");
33
+ return { password, allowedHosts };
34
+ }
35
+ /**
36
+ * 把「存起来的选项」补进 env —— **只补缺**,命令行与环境变量优先。
37
+ * 返回补了哪些(日志/提示用,绝不回显密码本身)。
38
+ */
39
+ export async function applyStoredOptions(env, dataDir) {
40
+ const stored = await readStoredOptions(dataDir);
41
+ let password = false;
42
+ let allowedHosts = false;
43
+ if (stored.password && !env.AGENT_WEB_PASSWORD) {
44
+ env.AGENT_WEB_PASSWORD = stored.password;
45
+ password = true;
46
+ }
47
+ if (stored.allowedHosts.length > 0 && !env.AGENT_WEB_ALLOWED_HOSTS) {
48
+ env.AGENT_WEB_ALLOWED_HOSTS = stored.allowedHosts.join(",");
49
+ allowedHosts = true;
50
+ }
51
+ return { password, allowedHosts };
52
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `omaw` 的「查询类命令」:status / version / upgrade。
3
+ *
4
+ * 为什么单独成文件:cli.ts 已经要管参数解析、前后台切换、停止/重启,
5
+ * 再加状态查询就过 400 行护栏了(ADR-0010:一个文件只干一件事)。
6
+ * 这里只读锁文件、读 package.json、探一次 /api/health —— 不改任何状态。
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ import process from "node:process";
10
+ import { defaultIsAlive, readLock } from "./session/lock.js";
11
+ import { packageMeta } from "./util/version.js";
12
+ /** 探活:拿服务端自报的引擎 / 会话数 / 工作区;连不上就 null */
13
+ export async function fetchHealth(host, port, timeoutMs = 1500) {
14
+ try {
15
+ const url = `http://${host.includes(":") ? `[${host}]` : host}:${port}/api/health`;
16
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
17
+ return response.ok ? (await response.json()) : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /** 「已运行 2 天 3 小时」这种给人看的时间 */
24
+ function formatUptime(ms) {
25
+ const total = Math.max(0, Math.floor(ms / 1000));
26
+ const days = Math.floor(total / 86_400);
27
+ const hours = Math.floor((total % 86_400) / 3600);
28
+ const minutes = Math.floor((total % 3600) / 60);
29
+ if (days > 0)
30
+ return `${days} 天 ${hours} 小时`;
31
+ if (hours > 0)
32
+ return `${hours} 小时 ${minutes} 分`;
33
+ if (minutes > 0)
34
+ return `${minutes} 分 ${total % 60} 秒`;
35
+ return `${total} 秒`;
36
+ }
37
+ /** `omaw status`:进程状态 + 服务端自报的事实(引擎 / 会话数 / 工作区) */
38
+ export async function showStatus(options) {
39
+ const { dataDir, probeHost } = options;
40
+ const lock = await readLock(dataDir);
41
+ console.log(`oh-my-agent-web v${packageMeta().version} · 数据目录 ${dataDir}`);
42
+ if (!lock) {
43
+ console.log("状态:未运行(没有 server.lock)");
44
+ console.log(" 启动:omaw -d");
45
+ return;
46
+ }
47
+ if (!defaultIsAlive(lock.pid)) {
48
+ console.log(`状态:未运行(陈旧锁:pid ${lock.pid} 已不在,下次 start 会自动接管)`);
49
+ return;
50
+ }
51
+ const health = await fetchHealth(probeHost, lock.port);
52
+ const onDisk = packageMeta().version;
53
+ console.log(`状态:运行中(pid ${lock.pid},已运行 ${formatUptime(Date.now() - lock.startedAt)})`);
54
+ console.log(` 地址 http://${probeHost}:${lock.port}`);
55
+ if (health) {
56
+ const running = health.version ?? "unknown";
57
+ console.log(` 版本 v${running}(磁盘上是 v${onDisk}${running === onDisk ? "" : ",重启后生效:omaw restart -d"})`);
58
+ console.log(` 引擎 ${health.engine} · 协议 v${health.protocolVersion} · 会话 ${health.sessions} · 对话 ${health.conversations}`);
59
+ console.log(` 工作区 ${health.workspace}`);
60
+ }
61
+ else {
62
+ console.log(" ⚠ 进程在,但 /api/health 没通(可能还在启动,或端口没监听成功)");
63
+ }
64
+ }
65
+ /** `omaw version` */
66
+ export function showVersion() {
67
+ console.log(`oh-my-agent-web v${packageMeta().version}(node ${process.version} · ${process.platform}/${process.arch})`);
68
+ }
69
+ /**
70
+ * `omaw upgrade`:升级**全局安装**的这个包。
71
+ *
72
+ * 三条底线:
73
+ * 1. 只跑 `npm i -g <包名>@latest`,不做别的(不替你重启,重启会打断正在跑的回合);
74
+ * 2. `private: true` 的包**拒绝升级** —— 它根本没发布到 npm,照名字去装只会装到
75
+ * 别人的同名包(npm 上 `oh-my-agent-web` 确实已被别人占用,踩过一次);
76
+ * 3. 输出原样透传,成功/失败都给出下一步该做什么。
77
+ */
78
+ export async function upgrade() {
79
+ const meta = packageMeta();
80
+ if (meta.private) {
81
+ console.error(`这个包是 private(${meta.name}@${meta.version}),没有发布到 npm,无法从 npm 升级。`);
82
+ console.error(" · 本地 / git 安装:回到源码目录重新 `npm i -g .`,然后 `omaw restart -d`");
83
+ console.error(" · 想用 npm 升级:先把 package.json 的 name 改成自己 scope 下的名字(如 @you/omaw)并发布");
84
+ return 1;
85
+ }
86
+ console.log(`当前 v${meta.version},正在安装 npm 上的最新版 ${meta.name} …`);
87
+ const code = await new Promise((resolve) => {
88
+ const child = spawn("npm", ["install", "-g", `${meta.name}@latest`], { stdio: "inherit" });
89
+ child.on("error", (error) => {
90
+ console.error(`调用 npm 失败:${error.message}`);
91
+ resolve(1);
92
+ });
93
+ child.on("exit", (exitCode) => resolve(exitCode ?? 1));
94
+ });
95
+ if (code !== 0) {
96
+ console.error("升级失败(网络 / 权限 / 包名不存在都可能)。");
97
+ return code;
98
+ }
99
+ console.log(`已升级到 v${packageMeta().version}(新进程才用新版:omaw restart -d)`);
100
+ return 0;
101
+ }