@moxxy/core 0.3.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 (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/events/factory.d.ts +6 -0
  4. package/dist/events/factory.d.ts.map +1 -0
  5. package/dist/events/factory.js +14 -0
  6. package/dist/events/factory.js.map +1 -0
  7. package/dist/events/log.d.ts +92 -0
  8. package/dist/events/log.d.ts.map +1 -0
  9. package/dist/events/log.js +244 -0
  10. package/dist/events/log.js.map +1 -0
  11. package/dist/index.d.ts +40 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +41 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/logger.d.ts +16 -0
  16. package/dist/logger.d.ts.map +1 -0
  17. package/dist/logger.js +27 -0
  18. package/dist/logger.js.map +1 -0
  19. package/dist/permissions/engine.d.ts +94 -0
  20. package/dist/permissions/engine.d.ts.map +1 -0
  21. package/dist/permissions/engine.js +203 -0
  22. package/dist/permissions/engine.js.map +1 -0
  23. package/dist/permissions/resolvers.d.ts +8 -0
  24. package/dist/permissions/resolvers.d.ts.map +1 -0
  25. package/dist/permissions/resolvers.js +8 -0
  26. package/dist/permissions/resolvers.js.map +1 -0
  27. package/dist/plugins/discovery.d.ts +9 -0
  28. package/dist/plugins/discovery.d.ts.map +1 -0
  29. package/dist/plugins/discovery.js +112 -0
  30. package/dist/plugins/discovery.js.map +1 -0
  31. package/dist/plugins/host-options.d.ts +72 -0
  32. package/dist/plugins/host-options.d.ts.map +1 -0
  33. package/dist/plugins/host-options.js +2 -0
  34. package/dist/plugins/host-options.js.map +1 -0
  35. package/dist/plugins/host.d.ts +53 -0
  36. package/dist/plugins/host.d.ts.map +1 -0
  37. package/dist/plugins/host.js +269 -0
  38. package/dist/plugins/host.js.map +1 -0
  39. package/dist/plugins/lifecycle.d.ts +31 -0
  40. package/dist/plugins/lifecycle.d.ts.map +1 -0
  41. package/dist/plugins/lifecycle.js +100 -0
  42. package/dist/plugins/lifecycle.js.map +1 -0
  43. package/dist/plugins/loader.d.ts +7 -0
  44. package/dist/plugins/loader.d.ts.map +1 -0
  45. package/dist/plugins/loader.js +68 -0
  46. package/dist/plugins/loader.js.map +1 -0
  47. package/dist/plugins/package-requirements.d.ts +16 -0
  48. package/dist/plugins/package-requirements.d.ts.map +1 -0
  49. package/dist/plugins/package-requirements.js +52 -0
  50. package/dist/plugins/package-requirements.js.map +1 -0
  51. package/dist/plugins/registry-kinds.d.ts +58 -0
  52. package/dist/plugins/registry-kinds.d.ts.map +1 -0
  53. package/dist/plugins/registry-kinds.js +139 -0
  54. package/dist/plugins/registry-kinds.js.map +1 -0
  55. package/dist/plugins/toposort.d.ts +24 -0
  56. package/dist/plugins/toposort.d.ts.map +1 -0
  57. package/dist/plugins/toposort.js +67 -0
  58. package/dist/plugins/toposort.js.map +1 -0
  59. package/dist/preferences.d.ts +40 -0
  60. package/dist/preferences.d.ts.map +1 -0
  61. package/dist/preferences.js +58 -0
  62. package/dist/preferences.js.map +1 -0
  63. package/dist/registries/active-backend-registry.d.ts +65 -0
  64. package/dist/registries/active-backend-registry.d.ts.map +1 -0
  65. package/dist/registries/active-backend-registry.js +108 -0
  66. package/dist/registries/active-backend-registry.js.map +1 -0
  67. package/dist/registries/active-def-registry.d.ts +59 -0
  68. package/dist/registries/active-def-registry.d.ts.map +1 -0
  69. package/dist/registries/active-def-registry.js +63 -0
  70. package/dist/registries/active-def-registry.js.map +1 -0
  71. package/dist/registries/agents.d.ts +16 -0
  72. package/dist/registries/agents.d.ts.map +1 -0
  73. package/dist/registries/agents.js +17 -0
  74. package/dist/registries/agents.js.map +1 -0
  75. package/dist/registries/cache-strategies.d.ts +12 -0
  76. package/dist/registries/cache-strategies.d.ts.map +1 -0
  77. package/dist/registries/cache-strategies.js +13 -0
  78. package/dist/registries/cache-strategies.js.map +1 -0
  79. package/dist/registries/channels.d.ts +16 -0
  80. package/dist/registries/channels.d.ts.map +1 -0
  81. package/dist/registries/channels.js +35 -0
  82. package/dist/registries/channels.js.map +1 -0
  83. package/dist/registries/commands.d.ts +29 -0
  84. package/dist/registries/commands.d.ts.map +1 -0
  85. package/dist/registries/commands.js +72 -0
  86. package/dist/registries/commands.js.map +1 -0
  87. package/dist/registries/compactors.d.ts +12 -0
  88. package/dist/registries/compactors.d.ts.map +1 -0
  89. package/dist/registries/compactors.js +13 -0
  90. package/dist/registries/compactors.js.map +1 -0
  91. package/dist/registries/def-map-registry.d.ts +33 -0
  92. package/dist/registries/def-map-registry.d.ts.map +1 -0
  93. package/dist/registries/def-map-registry.js +36 -0
  94. package/dist/registries/def-map-registry.js.map +1 -0
  95. package/dist/registries/embedders.d.ts +16 -0
  96. package/dist/registries/embedders.d.ts.map +1 -0
  97. package/dist/registries/embedders.js +17 -0
  98. package/dist/registries/embedders.js.map +1 -0
  99. package/dist/registries/isolators.d.ts +21 -0
  100. package/dist/registries/isolators.d.ts.map +1 -0
  101. package/dist/registries/isolators.js +32 -0
  102. package/dist/registries/isolators.js.map +1 -0
  103. package/dist/registries/modes.d.ts +28 -0
  104. package/dist/registries/modes.d.ts.map +1 -0
  105. package/dist/registries/modes.js +81 -0
  106. package/dist/registries/modes.js.map +1 -0
  107. package/dist/registries/providers.d.ts +44 -0
  108. package/dist/registries/providers.d.ts.map +1 -0
  109. package/dist/registries/providers.js +95 -0
  110. package/dist/registries/providers.js.map +1 -0
  111. package/dist/registries/skills.d.ts +24 -0
  112. package/dist/registries/skills.d.ts.map +1 -0
  113. package/dist/registries/skills.js +81 -0
  114. package/dist/registries/skills.js.map +1 -0
  115. package/dist/registries/surfaces.d.ts +13 -0
  116. package/dist/registries/surfaces.d.ts.map +1 -0
  117. package/dist/registries/surfaces.js +14 -0
  118. package/dist/registries/surfaces.js.map +1 -0
  119. package/dist/registries/synthesizers.d.ts +26 -0
  120. package/dist/registries/synthesizers.d.ts.map +1 -0
  121. package/dist/registries/synthesizers.js +34 -0
  122. package/dist/registries/synthesizers.js.map +1 -0
  123. package/dist/registries/tools.d.ts +49 -0
  124. package/dist/registries/tools.d.ts.map +1 -0
  125. package/dist/registries/tools.js +98 -0
  126. package/dist/registries/tools.js.map +1 -0
  127. package/dist/registries/transcribers.d.ts +17 -0
  128. package/dist/registries/transcribers.d.ts.map +1 -0
  129. package/dist/registries/transcribers.js +18 -0
  130. package/dist/registries/transcribers.js.map +1 -0
  131. package/dist/registries/tunnel-providers.d.ts +12 -0
  132. package/dist/registries/tunnel-providers.d.ts.map +1 -0
  133. package/dist/registries/tunnel-providers.js +13 -0
  134. package/dist/registries/tunnel-providers.js.map +1 -0
  135. package/dist/registries/view-renderers.d.ts +13 -0
  136. package/dist/registries/view-renderers.d.ts.map +1 -0
  137. package/dist/registries/view-renderers.js +14 -0
  138. package/dist/registries/view-renderers.js.map +1 -0
  139. package/dist/registries/workflow-executors.d.ts +13 -0
  140. package/dist/registries/workflow-executors.d.ts.map +1 -0
  141. package/dist/registries/workflow-executors.js +14 -0
  142. package/dist/registries/workflow-executors.js.map +1 -0
  143. package/dist/requirements.d.ts +59 -0
  144. package/dist/requirements.d.ts.map +1 -0
  145. package/dist/requirements.js +149 -0
  146. package/dist/requirements.js.map +1 -0
  147. package/dist/run-turn.d.ts +6 -0
  148. package/dist/run-turn.d.ts.map +1 -0
  149. package/dist/run-turn.js +141 -0
  150. package/dist/run-turn.js.map +1 -0
  151. package/dist/session-runtime.d.ts +67 -0
  152. package/dist/session-runtime.d.ts.map +1 -0
  153. package/dist/session-runtime.js +2 -0
  154. package/dist/session-runtime.js.map +1 -0
  155. package/dist/session.d.ts +203 -0
  156. package/dist/session.d.ts.map +1 -0
  157. package/dist/session.js +399 -0
  158. package/dist/session.js.map +1 -0
  159. package/dist/sessions/persistence.d.ts +184 -0
  160. package/dist/sessions/persistence.d.ts.map +1 -0
  161. package/dist/sessions/persistence.js +442 -0
  162. package/dist/sessions/persistence.js.map +1 -0
  163. package/dist/setup-agent.d.ts +91 -0
  164. package/dist/setup-agent.d.ts.map +1 -0
  165. package/dist/setup-agent.js +138 -0
  166. package/dist/setup-agent.js.map +1 -0
  167. package/dist/skills/index.d.ts +4 -0
  168. package/dist/skills/index.d.ts.map +1 -0
  169. package/dist/skills/index.js +4 -0
  170. package/dist/skills/index.js.map +1 -0
  171. package/dist/skills/loader.d.ts +16 -0
  172. package/dist/skills/loader.d.ts.map +1 -0
  173. package/dist/skills/loader.js +99 -0
  174. package/dist/skills/loader.js.map +1 -0
  175. package/dist/skills/parse.d.ts +5 -0
  176. package/dist/skills/parse.d.ts.map +1 -0
  177. package/dist/skills/parse.js +8 -0
  178. package/dist/skills/parse.js.map +1 -0
  179. package/dist/skills/synthesize-draft.d.ts +8 -0
  180. package/dist/skills/synthesize-draft.d.ts.map +1 -0
  181. package/dist/skills/synthesize-draft.js +32 -0
  182. package/dist/skills/synthesize-draft.js.map +1 -0
  183. package/dist/skills/synthesize.d.ts +33 -0
  184. package/dist/skills/synthesize.d.ts.map +1 -0
  185. package/dist/skills/synthesize.js +241 -0
  186. package/dist/skills/synthesize.js.map +1 -0
  187. package/dist/subagents/events.d.ts +20 -0
  188. package/dist/subagents/events.d.ts.map +1 -0
  189. package/dist/subagents/events.js +146 -0
  190. package/dist/subagents/events.js.map +1 -0
  191. package/dist/subagents/registry.d.ts +40 -0
  192. package/dist/subagents/registry.d.ts.map +1 -0
  193. package/dist/subagents/registry.js +42 -0
  194. package/dist/subagents/registry.js.map +1 -0
  195. package/dist/subagents/run-child.d.ts +43 -0
  196. package/dist/subagents/run-child.d.ts.map +1 -0
  197. package/dist/subagents/run-child.js +276 -0
  198. package/dist/subagents/run-child.js.map +1 -0
  199. package/dist/subagents/spawn.d.ts +8 -0
  200. package/dist/subagents/spawn.d.ts.map +1 -0
  201. package/dist/subagents/spawn.js +8 -0
  202. package/dist/subagents/spawn.js.map +1 -0
  203. package/dist/subagents/tools.d.ts +4 -0
  204. package/dist/subagents/tools.d.ts.map +1 -0
  205. package/dist/subagents/tools.js +13 -0
  206. package/dist/subagents/tools.js.map +1 -0
  207. package/dist/subagents.d.ts +3 -0
  208. package/dist/subagents.d.ts.map +1 -0
  209. package/dist/subagents.js +3 -0
  210. package/dist/subagents.js.map +1 -0
  211. package/dist/surfaces/host.d.ts +53 -0
  212. package/dist/surfaces/host.d.ts.map +1 -0
  213. package/dist/surfaces/host.js +177 -0
  214. package/dist/surfaces/host.js.map +1 -0
  215. package/dist/tunnel/localhost.d.ts +7 -0
  216. package/dist/tunnel/localhost.d.ts.map +1 -0
  217. package/dist/tunnel/localhost.js +21 -0
  218. package/dist/tunnel/localhost.js.map +1 -0
  219. package/dist/usage-stats.d.ts +42 -0
  220. package/dist/usage-stats.d.ts.map +1 -0
  221. package/dist/usage-stats.js +83 -0
  222. package/dist/usage-stats.js.map +1 -0
  223. package/dist/view/default-renderer.d.ts +7 -0
  224. package/dist/view/default-renderer.d.ts.map +1 -0
  225. package/dist/view/default-renderer.js +14 -0
  226. package/dist/view/default-renderer.js.map +1 -0
  227. package/dist/view/parse.d.ts +7 -0
  228. package/dist/view/parse.d.ts.map +1 -0
  229. package/dist/view/parse.js +479 -0
  230. package/dist/view/parse.js.map +1 -0
  231. package/package.json +61 -0
  232. package/src/events/factory.ts +27 -0
  233. package/src/events/log.test.ts +470 -0
  234. package/src/events/log.ts +256 -0
  235. package/src/index.ts +102 -0
  236. package/src/logger.ts +46 -0
  237. package/src/permissions/engine.test.ts +206 -0
  238. package/src/permissions/engine.ts +247 -0
  239. package/src/permissions/resolvers.test.ts +40 -0
  240. package/src/permissions/resolvers.ts +17 -0
  241. package/src/plugins/discovery.test.ts +115 -0
  242. package/src/plugins/discovery.ts +124 -0
  243. package/src/plugins/host-options.ts +73 -0
  244. package/src/plugins/host-view-tunnel.test.ts +86 -0
  245. package/src/plugins/host.test.ts +434 -0
  246. package/src/plugins/host.ts +327 -0
  247. package/src/plugins/lifecycle.test.ts +187 -0
  248. package/src/plugins/lifecycle.ts +136 -0
  249. package/src/plugins/loader.ts +79 -0
  250. package/src/plugins/package-requirements.ts +50 -0
  251. package/src/plugins/registry-kinds.test.ts +162 -0
  252. package/src/plugins/registry-kinds.ts +210 -0
  253. package/src/plugins/toposort.test.ts +66 -0
  254. package/src/plugins/toposort.ts +69 -0
  255. package/src/preferences.test.ts +105 -0
  256. package/src/preferences.ts +84 -0
  257. package/src/registries/active-backend-registry.ts +142 -0
  258. package/src/registries/active-def-registry.test.ts +103 -0
  259. package/src/registries/active-def-registry.ts +97 -0
  260. package/src/registries/agents.ts +18 -0
  261. package/src/registries/cache-strategies.ts +14 -0
  262. package/src/registries/channels.ts +42 -0
  263. package/src/registries/commands.test.ts +71 -0
  264. package/src/registries/commands.ts +78 -0
  265. package/src/registries/compactors.ts +14 -0
  266. package/src/registries/def-map-registry.test.ts +67 -0
  267. package/src/registries/def-map-registry.ts +59 -0
  268. package/src/registries/embedders.test.ts +54 -0
  269. package/src/registries/embedders.ts +18 -0
  270. package/src/registries/isolators.test.ts +43 -0
  271. package/src/registries/isolators.ts +38 -0
  272. package/src/registries/modes.test.ts +66 -0
  273. package/src/registries/modes.ts +80 -0
  274. package/src/registries/providers.test.ts +50 -0
  275. package/src/registries/providers.ts +97 -0
  276. package/src/registries/semantics.test.ts +117 -0
  277. package/src/registries/skills.ts +87 -0
  278. package/src/registries/surfaces.ts +18 -0
  279. package/src/registries/synthesizers.test.ts +63 -0
  280. package/src/registries/synthesizers.ts +36 -0
  281. package/src/registries/tools.test.ts +142 -0
  282. package/src/registries/tools.ts +145 -0
  283. package/src/registries/transcribers.test.ts +70 -0
  284. package/src/registries/transcribers.ts +19 -0
  285. package/src/registries/tunnel-providers.test.ts +30 -0
  286. package/src/registries/tunnel-providers.ts +14 -0
  287. package/src/registries/view-renderers.test.ts +68 -0
  288. package/src/registries/view-renderers.ts +15 -0
  289. package/src/registries/workflow-executors.test.ts +53 -0
  290. package/src/registries/workflow-executors.ts +15 -0
  291. package/src/requirements.targetinfo.test.ts +70 -0
  292. package/src/requirements.test.ts +111 -0
  293. package/src/requirements.ts +262 -0
  294. package/src/run-turn.test.ts +178 -0
  295. package/src/run-turn.ts +156 -0
  296. package/src/session-runtime.ts +75 -0
  297. package/src/session.test.ts +216 -0
  298. package/src/session.ts +491 -0
  299. package/src/sessions/persistence.flush.test.ts +100 -0
  300. package/src/sessions/persistence.ready.test.ts +91 -0
  301. package/src/sessions/persistence.test.ts +274 -0
  302. package/src/sessions/persistence.ts +498 -0
  303. package/src/setup-agent.test.ts +72 -0
  304. package/src/setup-agent.ts +189 -0
  305. package/src/skills/index.ts +14 -0
  306. package/src/skills/loader.test.ts +102 -0
  307. package/src/skills/loader.ts +114 -0
  308. package/src/skills/parse.test.ts +59 -0
  309. package/src/skills/parse.ts +11 -0
  310. package/src/skills/synthesize-draft.test.ts +79 -0
  311. package/src/skills/synthesize-draft.ts +46 -0
  312. package/src/skills/synthesize.test.ts +170 -0
  313. package/src/skills/synthesize.ts +304 -0
  314. package/src/subagents/events.test.ts +56 -0
  315. package/src/subagents/events.ts +194 -0
  316. package/src/subagents/registry.ts +62 -0
  317. package/src/subagents/run-child.test.ts +185 -0
  318. package/src/subagents/run-child.ts +425 -0
  319. package/src/subagents/spawn.ts +12 -0
  320. package/src/subagents/tools.ts +19 -0
  321. package/src/subagents.ts +2 -0
  322. package/src/surfaces/host.test.ts +330 -0
  323. package/src/surfaces/host.ts +192 -0
  324. package/src/tunnel/localhost.test.ts +22 -0
  325. package/src/tunnel/localhost.ts +21 -0
  326. package/src/usage-stats.test.ts +77 -0
  327. package/src/usage-stats.ts +119 -0
  328. package/src/view/default-renderer.test.ts +29 -0
  329. package/src/view/default-renderer.ts +14 -0
  330. package/src/view/parse-extended.test.ts +255 -0
  331. package/src/view/parse.test.ts +232 -0
  332. package/src/view/parse.ts +520 -0
@@ -0,0 +1,42 @@
1
+ import { type ModelUsageTotals } from '@moxxy/sdk';
2
+ /**
3
+ * Cross-session token usage, persisted at ~/.moxxy/usage.json. A forward-going
4
+ * aggregate keyed by `"<provider>/<model>"`: each session folds its own
5
+ * `provider_response` usage and merges the delta in on shutdown (see
6
+ * `@moxxy/plugin-usage-stats`). Purely additive counters — no derived/cost
7
+ * fields — so a session's contribution is added once and the file stays a sum.
8
+ *
9
+ * Like preferences, this is best-effort: a missing or malformed file reads as
10
+ * empty, and a write failure never blocks shutdown.
11
+ */
12
+ export interface StoredModelUsage extends ModelUsageTotals {
13
+ /** ISO timestamp of the first session that recorded usage for this model. */
14
+ readonly firstSeen: string;
15
+ /** ISO timestamp of the most recent session that recorded usage. */
16
+ readonly lastSeen: string;
17
+ }
18
+ export interface UsageStatsFile {
19
+ readonly version: 1;
20
+ /** ISO timestamp of the last merge/clear. */
21
+ readonly updatedAt: string;
22
+ /** Per-`provider/model` lifetime totals. */
23
+ readonly models: Record<string, StoredModelUsage>;
24
+ }
25
+ export declare function usageStatsPath(): string;
26
+ /**
27
+ * Read the usage aggregate. Returns an empty file when missing or unparseable —
28
+ * usage stats are an optional, non-load-blocking layer.
29
+ */
30
+ export declare function loadUsageStats(filePath?: string): Promise<UsageStatsFile>;
31
+ /**
32
+ * Merge a session's per-model delta into the persisted aggregate and write it
33
+ * back. Reads the current file, adds each delta field-wise, refreshes
34
+ * first/lastSeen, and writes atomically. Returns the updated file.
35
+ *
36
+ * Best-effort: a write failure logs to stderr but does not throw — losing one
37
+ * session's stats must never block shutdown.
38
+ */
39
+ export declare function mergeUsageStats(delta: Record<string, ModelUsageTotals>, filePath?: string): Promise<UsageStatsFile>;
40
+ /** Reset the aggregate to empty (the user-facing `/usage clear`). */
41
+ export declare function clearUsageStats(filePath?: string): Promise<void>;
42
+ //# sourceMappingURL=usage-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-stats.d.ts","sourceRoot":"","sources":["../src/usage-stats.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGhF;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACnD;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAMD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,QAAQ,GAAE,MAAyB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgBjG;AAWD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACvC,QAAQ,GAAE,MAAyB,GAClC,OAAO,CAAC,cAAc,CAAC,CAgCzB;AAED,qEAAqE;AACrE,wBAAsB,eAAe,CAAC,QAAQ,GAAE,MAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAExF"}
@@ -0,0 +1,83 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { addModelTotals, createMutex } from '@moxxy/sdk';
5
+ import { writeFileAtomic } from '@moxxy/sdk/server';
6
+ export function usageStatsPath() {
7
+ return path.join(os.homedir(), '.moxxy', 'usage.json');
8
+ }
9
+ function emptyStats() {
10
+ return { version: 1, updatedAt: new Date().toISOString(), models: {} };
11
+ }
12
+ /**
13
+ * Read the usage aggregate. Returns an empty file when missing or unparseable —
14
+ * usage stats are an optional, non-load-blocking layer.
15
+ */
16
+ export async function loadUsageStats(filePath = usageStatsPath()) {
17
+ try {
18
+ const raw = await fs.readFile(filePath, 'utf8');
19
+ const parsed = JSON.parse(raw);
20
+ if (parsed &&
21
+ typeof parsed === 'object' &&
22
+ typeof parsed.models === 'object' &&
23
+ parsed.models !== null) {
24
+ return parsed;
25
+ }
26
+ }
27
+ catch {
28
+ // missing or malformed — start fresh
29
+ }
30
+ return emptyStats();
31
+ }
32
+ async function writeAtomic(file, filePath) {
33
+ await writeFileAtomic(filePath, JSON.stringify(file, null, 2) + '\n');
34
+ }
35
+ // Serializes the read-modify-write in `mergeUsageStats` so two concurrent
36
+ // merges (e.g. overlapping shutdowns) can't both read the same snapshot and
37
+ // have the second write clobber the first's delta.
38
+ const mergeMutex = createMutex();
39
+ /**
40
+ * Merge a session's per-model delta into the persisted aggregate and write it
41
+ * back. Reads the current file, adds each delta field-wise, refreshes
42
+ * first/lastSeen, and writes atomically. Returns the updated file.
43
+ *
44
+ * Best-effort: a write failure logs to stderr but does not throw — losing one
45
+ * session's stats must never block shutdown.
46
+ */
47
+ export async function mergeUsageStats(delta, filePath = usageStatsPath()) {
48
+ // An empty delta writes nothing, so there's no read-modify-write to
49
+ // serialize: skip the mutex entirely and read the current file directly. A
50
+ // session with no recorded usage produces an empty delta on every shutdown,
51
+ // so this avoids needless I/O + contention on the common no-op path.
52
+ const keys = Object.keys(delta);
53
+ if (keys.length === 0)
54
+ return loadUsageStats(filePath);
55
+ return mergeMutex.run(async () => {
56
+ const current = await loadUsageStats(filePath);
57
+ const now = new Date().toISOString();
58
+ const models = { ...current.models };
59
+ for (const key of keys) {
60
+ const d = delta[key];
61
+ if (d.calls === 0)
62
+ continue;
63
+ const existing = models[key];
64
+ models[key] = existing
65
+ ? { ...addModelTotals(existing, d), firstSeen: existing.firstSeen, lastSeen: now }
66
+ : { ...d, firstSeen: now, lastSeen: now };
67
+ }
68
+ const next = { version: 1, updatedAt: now, models };
69
+ try {
70
+ await writeAtomic(next, filePath);
71
+ }
72
+ catch (err) {
73
+ process.stderr.write(`moxxy: failed to persist usage stats to ${filePath}: ` +
74
+ `${err instanceof Error ? err.message : String(err)}\n`);
75
+ }
76
+ return next;
77
+ });
78
+ }
79
+ /** Reset the aggregate to empty (the user-facing `/usage clear`). */
80
+ export async function clearUsageStats(filePath = usageStatsPath()) {
81
+ await writeAtomic(emptyStats(), filePath);
82
+ }
83
+ //# sourceMappingURL=usage-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-stats.js","sourceRoot":"","sources":["../src/usage-stats.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The built-in view-spec renderer, seeded into every session's
3
+ * {@link ViewRendererRegistry}. Plugins may register/replace and `setActive`
4
+ * an alternative renderer with a different vocabulary.
5
+ */
6
+ export declare const defaultViewRenderer: import("@moxxy/sdk").ViewRendererDef;
7
+ //# sourceMappingURL=default-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-renderer.d.ts","sourceRoot":"","sources":["../../src/view/default-renderer.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,sCAK9B,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { DEFAULT_VIEW_TAGS, defineViewRenderer } from '@moxxy/sdk';
2
+ import { parseView, validateDoc } from './parse.js';
3
+ /**
4
+ * The built-in view-spec renderer, seeded into every session's
5
+ * {@link ViewRendererRegistry}. Plugins may register/replace and `setActive`
6
+ * an alternative renderer with a different vocabulary.
7
+ */
8
+ export const defaultViewRenderer = defineViewRenderer({
9
+ name: 'moxxy/default',
10
+ allowList: DEFAULT_VIEW_TAGS,
11
+ parse: (source) => parseView(source, DEFAULT_VIEW_TAGS),
12
+ validate: (doc) => validateDoc(doc, DEFAULT_VIEW_TAGS),
13
+ });
14
+ //# sourceMappingURL=default-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-renderer.js","sourceRoot":"","sources":["../../src/view/default-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;IACpD,IAAI,EAAE,eAAe;IACrB,SAAS,EAAE,iBAAiB;IAC5B,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACvD,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,iBAAiB,CAAC;CACvD,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { countNodes } from '@moxxy/sdk';
2
+ import type { ViewDoc, ViewParseError, ViewParseResult, ViewTagSpec } from '@moxxy/sdk';
3
+ export declare function parseView(source: string, allowList: ReadonlyArray<ViewTagSpec>): ViewParseResult;
4
+ /** Validate an already-built (expanded) AST against the allow-list. */
5
+ export declare function validateDoc(doc: ViewDoc, allowList: ReadonlyArray<ViewTagSpec>): ViewParseError[];
6
+ export { countNodes };
7
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/view/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAEV,OAAO,EAEP,cAAc,EACd,eAAe,EACf,WAAW,EACZ,MAAM,YAAY,CAAC;AA8bpB,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,eAAe,CAehG;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,cAAc,EAAE,CA0CjG;AAKD,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,479 @@
1
+ import { countNodes, isSafeViewUrl } from '@moxxy/sdk';
2
+ class ParseFail extends Error {
3
+ info;
4
+ constructor(info) {
5
+ super(info.message);
6
+ this.info = info;
7
+ }
8
+ }
9
+ function lineColAt(src, pos) {
10
+ let line = 1;
11
+ let col = 1;
12
+ for (let i = 0; i < pos && i < src.length; i++) {
13
+ if (src[i] === '\n') {
14
+ line++;
15
+ col = 1;
16
+ }
17
+ else {
18
+ col++;
19
+ }
20
+ }
21
+ return { line, col };
22
+ }
23
+ function fail(src, pos, message) {
24
+ throw new ParseFail({ message, ...lineColAt(src, pos) });
25
+ }
26
+ function tokenize(src) {
27
+ const tokens = [];
28
+ let i = 0;
29
+ const n = src.length;
30
+ while (i < n) {
31
+ const lt = src.indexOf('<', i);
32
+ if (lt === -1) {
33
+ if (src.slice(i).trim())
34
+ tokens.push({ t: 'text', value: src.slice(i), pos: i });
35
+ break;
36
+ }
37
+ if (lt > i && src.slice(i, lt).trim()) {
38
+ tokens.push({ t: 'text', value: src.slice(i, lt), pos: i });
39
+ }
40
+ // Comment: <!-- ... -->
41
+ if (src.startsWith('<!--', lt)) {
42
+ const end = src.indexOf('-->', lt + 4);
43
+ i = end === -1 ? n : end + 3;
44
+ continue;
45
+ }
46
+ // Closing tag
47
+ if (src[lt + 1] === '/') {
48
+ const gt = src.indexOf('>', lt);
49
+ if (gt === -1)
50
+ fail(src, lt, 'unterminated closing tag');
51
+ tokens.push({ t: 'close', tag: src.slice(lt + 2, gt).trim(), pos: lt });
52
+ i = gt + 1;
53
+ continue;
54
+ }
55
+ // Opening tag — read to the matching top-level '>' (quotes/braces aware)
56
+ const { end, inner } = readRawTag(src, lt);
57
+ let body = inner.trim();
58
+ const selfClose = body.endsWith('/');
59
+ if (selfClose)
60
+ body = body.slice(0, -1).trim();
61
+ const sp = body.search(/\s/);
62
+ const tag = (sp === -1 ? body : body.slice(0, sp)).trim();
63
+ if (!tag || !/^[A-Za-z][A-Za-z0-9-]*$/.test(tag))
64
+ fail(src, lt, `invalid tag name "${tag}"`);
65
+ const attrs = sp === -1 ? [] : parseAttrs(body.slice(sp));
66
+ tokens.push({ t: 'open', tag, attrs, selfClose, pos: lt });
67
+ i = end + 1;
68
+ }
69
+ return tokens;
70
+ }
71
+ function readRawTag(src, lt) {
72
+ let i = lt + 1;
73
+ const n = src.length;
74
+ let quote = null;
75
+ let brace = 0;
76
+ while (i < n) {
77
+ const c = src[i];
78
+ if (quote) {
79
+ if (c === quote)
80
+ quote = null;
81
+ }
82
+ else if (c === '"' || c === "'") {
83
+ quote = c;
84
+ }
85
+ else if (c === '{') {
86
+ brace++;
87
+ }
88
+ else if (c === '}') {
89
+ if (brace > 0)
90
+ brace--;
91
+ }
92
+ else if (c === '>' && brace === 0) {
93
+ return { end: i, inner: src.slice(lt + 1, i) };
94
+ }
95
+ i++;
96
+ }
97
+ fail(src, lt, 'unterminated tag');
98
+ }
99
+ function parseAttrs(s) {
100
+ const attrs = [];
101
+ let i = 0;
102
+ const n = s.length;
103
+ while (i < n) {
104
+ while (i < n && /\s/.test(s[i]))
105
+ i++;
106
+ if (i >= n)
107
+ break;
108
+ let name = '';
109
+ while (i < n && /[A-Za-z0-9\-_:]/.test(s[i])) {
110
+ name += s[i];
111
+ i++;
112
+ }
113
+ if (!name) {
114
+ i++; // skip a stray separator char
115
+ continue;
116
+ }
117
+ let j = i;
118
+ while (j < n && /\s/.test(s[j]))
119
+ j++;
120
+ if (s[j] === '=') {
121
+ i = j + 1;
122
+ while (i < n && /\s/.test(s[i]))
123
+ i++;
124
+ const q = s[i];
125
+ let value = '';
126
+ if (q === '"' || q === "'") {
127
+ i++;
128
+ while (i < n && s[i] !== q) {
129
+ value += s[i];
130
+ i++;
131
+ }
132
+ i++; // closing quote
133
+ }
134
+ else if (q === '{') {
135
+ const start = i;
136
+ let depth = 0;
137
+ while (i < n) {
138
+ if (s[i] === '{')
139
+ depth++;
140
+ else if (s[i] === '}') {
141
+ depth--;
142
+ if (depth === 0) {
143
+ i++;
144
+ break;
145
+ }
146
+ }
147
+ i++;
148
+ }
149
+ value = s.slice(start, i); // keep braces; raw
150
+ }
151
+ else {
152
+ while (i < n && !/\s/.test(s[i])) {
153
+ value += s[i];
154
+ i++;
155
+ }
156
+ }
157
+ attrs.push({ name, value });
158
+ }
159
+ else {
160
+ attrs.push({ name, value: true });
161
+ i = j;
162
+ }
163
+ }
164
+ return attrs;
165
+ }
166
+ function isBNode(c) {
167
+ return 'tag' in c;
168
+ }
169
+ function build(tokens, src) {
170
+ const top = [];
171
+ const stack = [];
172
+ for (const tok of tokens) {
173
+ const target = stack.length ? stack[stack.length - 1].children : top;
174
+ if (tok.t === 'text') {
175
+ target.push({ text: decodeEntities(tok.value) });
176
+ }
177
+ else if (tok.t === 'open') {
178
+ const node = { tag: tok.tag, attrs: tok.attrs, children: [], pos: tok.pos };
179
+ target.push(node);
180
+ if (!tok.selfClose)
181
+ stack.push(node);
182
+ }
183
+ else {
184
+ const open = stack.pop();
185
+ if (!open)
186
+ fail(src, tok.pos, `unexpected closing tag </${tok.tag}>`);
187
+ if (open.tag !== tok.tag) {
188
+ fail(src, tok.pos, `mismatched closing tag: expected </${open.tag}>, got </${tok.tag}>`);
189
+ }
190
+ }
191
+ }
192
+ if (stack.length)
193
+ fail(src, stack[stack.length - 1].pos, `unclosed tag <${stack[stack.length - 1].tag}>`);
194
+ const elements = top.filter(isBNode);
195
+ if (elements.length !== 1) {
196
+ throw new ParseFail({ message: `expected exactly one root element, found ${elements.length}` });
197
+ }
198
+ return elements[0];
199
+ }
200
+ function decodeEntities(s) {
201
+ return s
202
+ .replace(/&lt;/g, '<')
203
+ .replace(/&gt;/g, '>')
204
+ .replace(/&quot;/g, '"')
205
+ .replace(/&#39;/g, "'")
206
+ .replace(/&apos;/g, "'")
207
+ .replace(/&amp;/g, '&');
208
+ }
209
+ // ---------------------------------------------------------------------------
210
+ // Validation + coercion (BNode → ViewNode), collecting all errors
211
+ // ---------------------------------------------------------------------------
212
+ // URL safety: the canonical scheme allow-list is `isSafeViewUrl` in
213
+ // @moxxy/sdk (view-renderer.ts) — shared with `validateDoc` below and
214
+ // mirrored by the web frontend's render-time re-check.
215
+ function coerceValue(tag, name, value, spec, errors) {
216
+ if (spec.type === 'boolean') {
217
+ if (value === true)
218
+ return true;
219
+ return value !== 'false';
220
+ }
221
+ if (value === true) {
222
+ errors.push({ message: `<${tag}> attribute "${name}" expects a value` });
223
+ return '';
224
+ }
225
+ if (spec.type === 'number') {
226
+ const num = Number(value);
227
+ if (!Number.isFinite(num)) {
228
+ errors.push({ message: `<${tag}> attribute "${name}" must be a number` });
229
+ return 0;
230
+ }
231
+ if (spec.min !== undefined && num < spec.min) {
232
+ errors.push({ message: `<${tag}> attribute "${name}" must be ≥ ${spec.min}` });
233
+ }
234
+ if (spec.max !== undefined && num > spec.max) {
235
+ errors.push({ message: `<${tag}> attribute "${name}" must be ≤ ${spec.max}` });
236
+ }
237
+ return num;
238
+ }
239
+ // Decode HTML entities in attribute string values so they match the
240
+ // contract of text nodes (which build() decodes), e.g. a label of
241
+ // `a &amp; b` renders `a & b` instead of the literal `a &amp; b`. Decoding
242
+ // happens BEFORE the URL-safety check below, so an entity-encoded scheme
243
+ // (e.g. `href="javascript&#58;…"`) is normalized to `javascript:` and still
244
+ // rejected — decoding can never smuggle a disallowed scheme past the check.
245
+ const decoded = decodeEntities(value);
246
+ if (spec.type === 'enum') {
247
+ if (!spec.values?.includes(decoded)) {
248
+ errors.push({ message: `<${tag}> attribute "${name}" must be one of: ${spec.values?.join(', ')}` });
249
+ }
250
+ return decoded;
251
+ }
252
+ // string
253
+ if ((name === 'href' || name === 'src') && !isSafeViewUrl(decoded, name)) {
254
+ errors.push({ message: `<${tag}> attribute "${name}" has a disallowed URL scheme` });
255
+ }
256
+ return decoded;
257
+ }
258
+ function coerceAttrs(b, spec, errors) {
259
+ const out = {};
260
+ const seen = new Set();
261
+ for (const a of b.attrs) {
262
+ if (/^on/i.test(a.name)) {
263
+ errors.push({ message: `<${b.tag}> forbidden attribute "${a.name}"` });
264
+ continue;
265
+ }
266
+ const aspec = spec?.attrs[a.name];
267
+ if (!aspec) {
268
+ // When the TAG itself is unknown (spec === undefined) convert() already
269
+ // pushed one `unknown tag` error — flagging every attribute as `unknown
270
+ // attribute` on top of it is redundant noise the agent reads back. The
271
+ // on* security check above still fires either way. Only emit the
272
+ // per-attribute error when the tag is known and the attribute isn't.
273
+ if (spec)
274
+ errors.push({ message: `<${b.tag}> unknown attribute "${a.name}"` });
275
+ continue;
276
+ }
277
+ seen.add(a.name);
278
+ out[a.name] = coerceValue(b.tag, a.name, a.value, aspec, errors);
279
+ }
280
+ if (spec) {
281
+ for (const [name, aspec] of Object.entries(spec.attrs)) {
282
+ if (aspec.required && !seen.has(name)) {
283
+ errors.push({ message: `<${b.tag}> missing required attribute "${name}"` });
284
+ }
285
+ }
286
+ }
287
+ return out;
288
+ }
289
+ function convert(b, specByTag, errors) {
290
+ const spec = specByTag.get(b.tag);
291
+ if (!spec)
292
+ errors.push({ message: `unknown tag <${b.tag}>` });
293
+ const props = coerceAttrs(b, spec, errors);
294
+ const children = [];
295
+ for (const c of b.children) {
296
+ if (!isBNode(c)) {
297
+ const v = c.text.replace(/\s+/g, ' ').trim();
298
+ if (!v)
299
+ continue; // drop whitespace-only text
300
+ if (spec && spec.allowedChildren !== 'any') {
301
+ errors.push({ message: `<${b.tag}> may not contain text` });
302
+ }
303
+ children.push({ kind: 'text', value: v });
304
+ }
305
+ else {
306
+ if (spec) {
307
+ if (spec.allowedChildren === 'none') {
308
+ errors.push({ message: `<${b.tag}> may not have children` });
309
+ }
310
+ else if (Array.isArray(spec.allowedChildren) && !spec.allowedChildren.includes(c.tag)) {
311
+ errors.push({ message: `<${b.tag}> may not contain <${c.tag}>` });
312
+ }
313
+ }
314
+ children.push(convert(c, specByTag, errors));
315
+ }
316
+ }
317
+ return finalizeNode({ kind: 'element', tag: b.tag, props, children }, spec);
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // Action + navigation extraction
321
+ // ---------------------------------------------------------------------------
322
+ function collectFieldNames(node) {
323
+ const names = [];
324
+ const walk = (n) => {
325
+ if (n.kind !== 'element')
326
+ return;
327
+ if ((n.tag === 'input' || n.tag === 'select' || n.tag === 'checkbox') && typeof n.props.name === 'string') {
328
+ names.push(n.props.name);
329
+ }
330
+ n.children.forEach(walk);
331
+ };
332
+ walk(node);
333
+ return names;
334
+ }
335
+ function finalizeNode(node, spec) {
336
+ if (node.kind !== 'element')
337
+ return node;
338
+ let out = node;
339
+ // Client-side navigation: any element carrying a `to` (link/button).
340
+ if (typeof node.props.to === 'string' && node.props.to) {
341
+ out = { ...out, nav: node.props.to };
342
+ }
343
+ if (spec?.interactive) {
344
+ if (node.tag === 'form') {
345
+ const name = typeof node.props.action === 'string' ? node.props.action : '';
346
+ out = { ...out, action: { name, fields: collectFieldNames(node) } };
347
+ }
348
+ else if (node.tag === 'button' && typeof node.props.action === 'string' && node.props.action) {
349
+ const fields = typeof node.props.fields === 'string' && node.props.fields
350
+ ? node.props.fields.split(',').map((f) => f.trim()).filter(Boolean)
351
+ : [];
352
+ out = { ...out, action: { name: node.props.action, fields } };
353
+ }
354
+ }
355
+ return out;
356
+ }
357
+ // ---------------------------------------------------------------------------
358
+ // Rich-component expansion (results → primitive card stack)
359
+ // ---------------------------------------------------------------------------
360
+ function el(tag, props, children, action, nav) {
361
+ return { kind: 'element', tag, props, children, ...(action ? { action } : {}), ...(nav ? { nav } : {}) };
362
+ }
363
+ function txt(value) {
364
+ return { kind: 'text', value };
365
+ }
366
+ /**
367
+ * `results` → a stack of selectable cards (one per `result`). Each card shows
368
+ * title/subtitle/badge and an "Open" affordance: `to` navigates client-side to a
369
+ * named view; else `action` (or `open:<id>` when only `id` is given) drives a turn.
370
+ */
371
+ function expandResults(node) {
372
+ const cards = node.children
373
+ .filter((c) => c.kind === 'element' && c.tag === 'result')
374
+ .map((r) => {
375
+ const p = r.props;
376
+ const id = p.id != null ? String(p.id) : '';
377
+ const title = String(p.title ?? '');
378
+ const subtitle = p.subtitle != null ? String(p.subtitle) : '';
379
+ const badge = p.badge != null ? String(p.badge) : '';
380
+ const to = typeof p.to === 'string' && p.to ? p.to : undefined;
381
+ const action = typeof p.action === 'string' && p.action ? p.action : id ? `open:${id}` : undefined;
382
+ const left = el('stack', { gap: 'none' }, [
383
+ el('text', { weight: 'bold' }, [txt(title)]),
384
+ ...(subtitle ? [el('text', { tone: 'muted' }, [txt(subtitle)])] : []),
385
+ ]);
386
+ const right = [];
387
+ if (badge)
388
+ right.push(el('badge', {}, [txt(badge)]));
389
+ if (to)
390
+ right.push(el('button', { to, label: 'Open', variant: 'secondary' }, [], undefined, to));
391
+ else if (action)
392
+ right.push(el('button', { action, label: 'Open', variant: 'primary' }, [], { name: action, fields: [] }));
393
+ return el('card', {}, [
394
+ el('row', { justify: 'between', align: 'center' }, [left, el('row', { gap: 'md', align: 'center' }, right)]),
395
+ ]);
396
+ });
397
+ return el('stack', { gap: 'md' }, cards);
398
+ }
399
+ function expand(node) {
400
+ if (node.kind !== 'element')
401
+ return node;
402
+ if (node.tag === 'results')
403
+ return expandResults(node);
404
+ return { ...node, children: node.children.map(expand) };
405
+ }
406
+ // ---------------------------------------------------------------------------
407
+ // Public API
408
+ // ---------------------------------------------------------------------------
409
+ export function parseView(source, allowList) {
410
+ try {
411
+ const tokens = tokenize(source);
412
+ const rootB = build(tokens, source);
413
+ const specByTag = new Map(allowList.map((s) => [s.tag, s]));
414
+ const errors = [];
415
+ let root = convert(rootB, specByTag, errors);
416
+ if (errors.length)
417
+ return { ok: false, errors };
418
+ root = expand(root);
419
+ const title = root.kind === 'element' && root.tag === 'view' && typeof root.props.title === 'string' ? root.props.title : undefined;
420
+ return { ok: true, doc: { root, ...(title ? { title } : {}) } };
421
+ }
422
+ catch (e) {
423
+ if (e instanceof ParseFail)
424
+ return { ok: false, errors: [e.info] };
425
+ return { ok: false, errors: [{ message: e instanceof Error ? e.message : String(e) }] };
426
+ }
427
+ }
428
+ /** Validate an already-built (expanded) AST against the allow-list. */
429
+ export function validateDoc(doc, allowList) {
430
+ const specByTag = new Map(allowList.map((s) => [s.tag, s]));
431
+ const errors = [];
432
+ const walk = (node) => {
433
+ if (node.kind !== 'element')
434
+ return;
435
+ const spec = specByTag.get(node.tag);
436
+ if (!spec) {
437
+ errors.push({ message: `unknown tag <${node.tag}>` });
438
+ return;
439
+ }
440
+ for (const [key, value] of Object.entries(node.props)) {
441
+ if (!spec.attrs[key]) {
442
+ errors.push({ message: `<${node.tag}> unknown attribute "${key}"` });
443
+ continue;
444
+ }
445
+ // URL-scheme allow-list — an AST handed straight to validateDoc (i.e.
446
+ // one that never went through parseView's coerceValue) must not smuggle
447
+ // a javascript:/data:text href past the renderer.
448
+ if ((key === 'href' || key === 'src') && typeof value === 'string' && !isSafeViewUrl(value, key)) {
449
+ errors.push({ message: `<${node.tag}> attribute "${key}" has a disallowed URL scheme` });
450
+ }
451
+ }
452
+ for (const [name, aspec] of Object.entries(spec.attrs)) {
453
+ if (aspec.required && !(name in node.props)) {
454
+ errors.push({ message: `<${node.tag}> missing required attribute "${name}"` });
455
+ }
456
+ }
457
+ for (const c of node.children) {
458
+ if (c.kind === 'element') {
459
+ if (spec.allowedChildren === 'none') {
460
+ errors.push({ message: `<${node.tag}> may not have children` });
461
+ }
462
+ else if (Array.isArray(spec.allowedChildren) && !spec.allowedChildren.includes(c.tag)) {
463
+ errors.push({ message: `<${node.tag}> may not contain <${c.tag}>` });
464
+ }
465
+ }
466
+ else if (spec.allowedChildren !== 'any' && c.value.trim()) {
467
+ errors.push({ message: `<${node.tag}> may not contain text` });
468
+ }
469
+ walk(c);
470
+ }
471
+ };
472
+ walk(doc.root);
473
+ return errors;
474
+ }
475
+ // Count element + text nodes (for tool result `nodeCount`). The recursion now
476
+ // lives in @moxxy/sdk so core's parser and the plugin-view tool share one
477
+ // implementation; re-exported here to keep core's public export shape.
478
+ export { countNodes };
479
+ //# sourceMappingURL=parse.js.map