@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,520 @@
1
+ import { countNodes, isSafeViewUrl } from '@moxxy/sdk';
2
+ import type {
3
+ AttrSpec,
4
+ ViewDoc,
5
+ ViewNode,
6
+ ViewParseError,
7
+ ViewParseResult,
8
+ ViewTagSpec,
9
+ } from '@moxxy/sdk';
10
+
11
+ /**
12
+ * Hand-rolled, zero-dependency parser for the JSX/XML-like view-spec, in the
13
+ * style of the TUI markdown block parser. Pipeline:
14
+ * tokenize → tree-build → allow-list validate → action extract → expand.
15
+ *
16
+ * Security posture: the allow-list is the wall. Unknown tags/attributes are
17
+ * hard errors (never silently dropped); `on*` handler attributes and unsafe URL
18
+ * schemes are rejected; raw HTML never survives because only known tags build
19
+ * nodes. The browser renderer applies the same allow-list as defense in depth.
20
+ */
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Tokenizer
24
+ // ---------------------------------------------------------------------------
25
+
26
+ interface RawAttr {
27
+ readonly name: string;
28
+ readonly value: string | true;
29
+ }
30
+
31
+ type Token =
32
+ | { readonly t: 'open'; readonly tag: string; readonly attrs: ReadonlyArray<RawAttr>; readonly selfClose: boolean; readonly pos: number }
33
+ | { readonly t: 'close'; readonly tag: string; readonly pos: number }
34
+ | { readonly t: 'text'; readonly value: string; readonly pos: number };
35
+
36
+ class ParseFail extends Error {
37
+ constructor(readonly info: ViewParseError) {
38
+ super(info.message);
39
+ }
40
+ }
41
+
42
+ function lineColAt(src: string, pos: number): { line: number; col: number } {
43
+ let line = 1;
44
+ let col = 1;
45
+ for (let i = 0; i < pos && i < src.length; i++) {
46
+ if (src[i] === '\n') {
47
+ line++;
48
+ col = 1;
49
+ } else {
50
+ col++;
51
+ }
52
+ }
53
+ return { line, col };
54
+ }
55
+
56
+ function fail(src: string, pos: number, message: string): never {
57
+ throw new ParseFail({ message, ...lineColAt(src, pos) });
58
+ }
59
+
60
+ function tokenize(src: string): Token[] {
61
+ const tokens: Token[] = [];
62
+ let i = 0;
63
+ const n = src.length;
64
+ while (i < n) {
65
+ const lt = src.indexOf('<', i);
66
+ if (lt === -1) {
67
+ if (src.slice(i).trim()) tokens.push({ t: 'text', value: src.slice(i), pos: i });
68
+ break;
69
+ }
70
+ if (lt > i && src.slice(i, lt).trim()) {
71
+ tokens.push({ t: 'text', value: src.slice(i, lt), pos: i });
72
+ }
73
+
74
+ // Comment: <!-- ... -->
75
+ if (src.startsWith('<!--', lt)) {
76
+ const end = src.indexOf('-->', lt + 4);
77
+ i = end === -1 ? n : end + 3;
78
+ continue;
79
+ }
80
+
81
+ // Closing tag
82
+ if (src[lt + 1] === '/') {
83
+ const gt = src.indexOf('>', lt);
84
+ if (gt === -1) fail(src, lt, 'unterminated closing tag');
85
+ tokens.push({ t: 'close', tag: src.slice(lt + 2, gt).trim(), pos: lt });
86
+ i = gt + 1;
87
+ continue;
88
+ }
89
+
90
+ // Opening tag — read to the matching top-level '>' (quotes/braces aware)
91
+ const { end, inner } = readRawTag(src, lt);
92
+ let body = inner.trim();
93
+ const selfClose = body.endsWith('/');
94
+ if (selfClose) body = body.slice(0, -1).trim();
95
+ const sp = body.search(/\s/);
96
+ const tag = (sp === -1 ? body : body.slice(0, sp)).trim();
97
+ if (!tag || !/^[A-Za-z][A-Za-z0-9-]*$/.test(tag)) fail(src, lt, `invalid tag name "${tag}"`);
98
+ const attrs = sp === -1 ? [] : parseAttrs(body.slice(sp));
99
+ tokens.push({ t: 'open', tag, attrs, selfClose, pos: lt });
100
+ i = end + 1;
101
+ }
102
+ return tokens;
103
+ }
104
+
105
+ function readRawTag(src: string, lt: number): { end: number; inner: string } {
106
+ let i = lt + 1;
107
+ const n = src.length;
108
+ let quote: string | null = null;
109
+ let brace = 0;
110
+ while (i < n) {
111
+ const c = src[i]!;
112
+ if (quote) {
113
+ if (c === quote) quote = null;
114
+ } else if (c === '"' || c === "'") {
115
+ quote = c;
116
+ } else if (c === '{') {
117
+ brace++;
118
+ } else if (c === '}') {
119
+ if (brace > 0) brace--;
120
+ } else if (c === '>' && brace === 0) {
121
+ return { end: i, inner: src.slice(lt + 1, i) };
122
+ }
123
+ i++;
124
+ }
125
+ fail(src, lt, 'unterminated tag');
126
+ }
127
+
128
+ function parseAttrs(s: string): RawAttr[] {
129
+ const attrs: RawAttr[] = [];
130
+ let i = 0;
131
+ const n = s.length;
132
+ while (i < n) {
133
+ while (i < n && /\s/.test(s[i]!)) i++;
134
+ if (i >= n) break;
135
+ let name = '';
136
+ while (i < n && /[A-Za-z0-9\-_:]/.test(s[i]!)) {
137
+ name += s[i]!;
138
+ i++;
139
+ }
140
+ if (!name) {
141
+ i++; // skip a stray separator char
142
+ continue;
143
+ }
144
+ let j = i;
145
+ while (j < n && /\s/.test(s[j]!)) j++;
146
+ if (s[j] === '=') {
147
+ i = j + 1;
148
+ while (i < n && /\s/.test(s[i]!)) i++;
149
+ const q = s[i];
150
+ let value = '';
151
+ if (q === '"' || q === "'") {
152
+ i++;
153
+ while (i < n && s[i] !== q) {
154
+ value += s[i]!;
155
+ i++;
156
+ }
157
+ i++; // closing quote
158
+ } else if (q === '{') {
159
+ const start = i;
160
+ let depth = 0;
161
+ while (i < n) {
162
+ if (s[i] === '{') depth++;
163
+ else if (s[i] === '}') {
164
+ depth--;
165
+ if (depth === 0) {
166
+ i++;
167
+ break;
168
+ }
169
+ }
170
+ i++;
171
+ }
172
+ value = s.slice(start, i); // keep braces; raw
173
+ } else {
174
+ while (i < n && !/\s/.test(s[i]!)) {
175
+ value += s[i]!;
176
+ i++;
177
+ }
178
+ }
179
+ attrs.push({ name, value });
180
+ } else {
181
+ attrs.push({ name, value: true });
182
+ i = j;
183
+ }
184
+ }
185
+ return attrs;
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Tree builder (mutable builder nodes → validated ViewNode)
190
+ // ---------------------------------------------------------------------------
191
+
192
+ interface BNode {
193
+ readonly tag: string;
194
+ readonly attrs: ReadonlyArray<RawAttr>;
195
+ readonly children: BChild[];
196
+ readonly pos: number;
197
+ }
198
+ type BChild = BNode | { readonly text: string };
199
+
200
+ function isBNode(c: BChild): c is BNode {
201
+ return 'tag' in c;
202
+ }
203
+
204
+ function build(tokens: Token[], src: string): BNode {
205
+ const top: BChild[] = [];
206
+ const stack: BNode[] = [];
207
+ for (const tok of tokens) {
208
+ const target = stack.length ? stack[stack.length - 1]!.children : top;
209
+ if (tok.t === 'text') {
210
+ target.push({ text: decodeEntities(tok.value) });
211
+ } else if (tok.t === 'open') {
212
+ const node: BNode = { tag: tok.tag, attrs: tok.attrs, children: [], pos: tok.pos };
213
+ target.push(node);
214
+ if (!tok.selfClose) stack.push(node);
215
+ } else {
216
+ const open = stack.pop();
217
+ if (!open) fail(src, tok.pos, `unexpected closing tag </${tok.tag}>`);
218
+ if (open.tag !== tok.tag) {
219
+ fail(src, tok.pos, `mismatched closing tag: expected </${open.tag}>, got </${tok.tag}>`);
220
+ }
221
+ }
222
+ }
223
+ if (stack.length) fail(src, stack[stack.length - 1]!.pos, `unclosed tag <${stack[stack.length - 1]!.tag}>`);
224
+ const elements = top.filter(isBNode);
225
+ if (elements.length !== 1) {
226
+ throw new ParseFail({ message: `expected exactly one root element, found ${elements.length}` });
227
+ }
228
+ return elements[0]!;
229
+ }
230
+
231
+ function decodeEntities(s: string): string {
232
+ return s
233
+ .replace(/&lt;/g, '<')
234
+ .replace(/&gt;/g, '>')
235
+ .replace(/&quot;/g, '"')
236
+ .replace(/&#39;/g, "'")
237
+ .replace(/&apos;/g, "'")
238
+ .replace(/&amp;/g, '&');
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Validation + coercion (BNode → ViewNode), collecting all errors
243
+ // ---------------------------------------------------------------------------
244
+
245
+ // URL safety: the canonical scheme allow-list is `isSafeViewUrl` in
246
+ // @moxxy/sdk (view-renderer.ts) — shared with `validateDoc` below and
247
+ // mirrored by the web frontend's render-time re-check.
248
+
249
+ function coerceValue(
250
+ tag: string,
251
+ name: string,
252
+ value: string | true,
253
+ spec: AttrSpec,
254
+ errors: ViewParseError[],
255
+ ): string | number | boolean {
256
+ if (spec.type === 'boolean') {
257
+ if (value === true) return true;
258
+ return value !== 'false';
259
+ }
260
+ if (value === true) {
261
+ errors.push({ message: `<${tag}> attribute "${name}" expects a value` });
262
+ return '';
263
+ }
264
+ if (spec.type === 'number') {
265
+ const num = Number(value);
266
+ if (!Number.isFinite(num)) {
267
+ errors.push({ message: `<${tag}> attribute "${name}" must be a number` });
268
+ return 0;
269
+ }
270
+ if (spec.min !== undefined && num < spec.min) {
271
+ errors.push({ message: `<${tag}> attribute "${name}" must be ≥ ${spec.min}` });
272
+ }
273
+ if (spec.max !== undefined && num > spec.max) {
274
+ errors.push({ message: `<${tag}> attribute "${name}" must be ≤ ${spec.max}` });
275
+ }
276
+ return num;
277
+ }
278
+ // Decode HTML entities in attribute string values so they match the
279
+ // contract of text nodes (which build() decodes), e.g. a label of
280
+ // `a &amp; b` renders `a & b` instead of the literal `a &amp; b`. Decoding
281
+ // happens BEFORE the URL-safety check below, so an entity-encoded scheme
282
+ // (e.g. `href="javascript&#58;…"`) is normalized to `javascript:` and still
283
+ // rejected — decoding can never smuggle a disallowed scheme past the check.
284
+ const decoded = decodeEntities(value);
285
+ if (spec.type === 'enum') {
286
+ if (!spec.values?.includes(decoded)) {
287
+ errors.push({ message: `<${tag}> attribute "${name}" must be one of: ${spec.values?.join(', ')}` });
288
+ }
289
+ return decoded;
290
+ }
291
+ // string
292
+ if ((name === 'href' || name === 'src') && !isSafeViewUrl(decoded, name)) {
293
+ errors.push({ message: `<${tag}> attribute "${name}" has a disallowed URL scheme` });
294
+ }
295
+ return decoded;
296
+ }
297
+
298
+ function coerceAttrs(b: BNode, spec: ViewTagSpec | undefined, errors: ViewParseError[]): Record<string, string | number | boolean> {
299
+ const out: Record<string, string | number | boolean> = {};
300
+ const seen = new Set<string>();
301
+ for (const a of b.attrs) {
302
+ if (/^on/i.test(a.name)) {
303
+ errors.push({ message: `<${b.tag}> forbidden attribute "${a.name}"` });
304
+ continue;
305
+ }
306
+ const aspec = spec?.attrs[a.name];
307
+ if (!aspec) {
308
+ // When the TAG itself is unknown (spec === undefined) convert() already
309
+ // pushed one `unknown tag` error — flagging every attribute as `unknown
310
+ // attribute` on top of it is redundant noise the agent reads back. The
311
+ // on* security check above still fires either way. Only emit the
312
+ // per-attribute error when the tag is known and the attribute isn't.
313
+ if (spec) errors.push({ message: `<${b.tag}> unknown attribute "${a.name}"` });
314
+ continue;
315
+ }
316
+ seen.add(a.name);
317
+ out[a.name] = coerceValue(b.tag, a.name, a.value, aspec, errors);
318
+ }
319
+ if (spec) {
320
+ for (const [name, aspec] of Object.entries(spec.attrs)) {
321
+ if (aspec.required && !seen.has(name)) {
322
+ errors.push({ message: `<${b.tag}> missing required attribute "${name}"` });
323
+ }
324
+ }
325
+ }
326
+ return out;
327
+ }
328
+
329
+ function convert(b: BNode, specByTag: Map<string, ViewTagSpec>, errors: ViewParseError[]): ViewNode {
330
+ const spec = specByTag.get(b.tag);
331
+ if (!spec) errors.push({ message: `unknown tag <${b.tag}>` });
332
+ const props = coerceAttrs(b, spec, errors);
333
+ const children: ViewNode[] = [];
334
+ for (const c of b.children) {
335
+ if (!isBNode(c)) {
336
+ const v = c.text.replace(/\s+/g, ' ').trim();
337
+ if (!v) continue; // drop whitespace-only text
338
+ if (spec && spec.allowedChildren !== 'any') {
339
+ errors.push({ message: `<${b.tag}> may not contain text` });
340
+ }
341
+ children.push({ kind: 'text', value: v });
342
+ } else {
343
+ if (spec) {
344
+ if (spec.allowedChildren === 'none') {
345
+ errors.push({ message: `<${b.tag}> may not have children` });
346
+ } else if (Array.isArray(spec.allowedChildren) && !spec.allowedChildren.includes(c.tag)) {
347
+ errors.push({ message: `<${b.tag}> may not contain <${c.tag}>` });
348
+ }
349
+ }
350
+ children.push(convert(c, specByTag, errors));
351
+ }
352
+ }
353
+ return finalizeNode({ kind: 'element', tag: b.tag, props, children }, spec);
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Action + navigation extraction
358
+ // ---------------------------------------------------------------------------
359
+
360
+ function collectFieldNames(node: ViewNode): string[] {
361
+ const names: string[] = [];
362
+ const walk = (n: ViewNode) => {
363
+ if (n.kind !== 'element') return;
364
+ if ((n.tag === 'input' || n.tag === 'select' || n.tag === 'checkbox') && typeof n.props.name === 'string') {
365
+ names.push(n.props.name);
366
+ }
367
+ n.children.forEach(walk);
368
+ };
369
+ walk(node);
370
+ return names;
371
+ }
372
+
373
+ function finalizeNode(node: ViewNode, spec: ViewTagSpec | undefined): ViewNode {
374
+ if (node.kind !== 'element') return node;
375
+ let out = node;
376
+ // Client-side navigation: any element carrying a `to` (link/button).
377
+ if (typeof node.props.to === 'string' && node.props.to) {
378
+ out = { ...out, nav: node.props.to };
379
+ }
380
+ if (spec?.interactive) {
381
+ if (node.tag === 'form') {
382
+ const name = typeof node.props.action === 'string' ? node.props.action : '';
383
+ out = { ...out, action: { name, fields: collectFieldNames(node) } };
384
+ } else if (node.tag === 'button' && typeof node.props.action === 'string' && node.props.action) {
385
+ const fields =
386
+ typeof node.props.fields === 'string' && node.props.fields
387
+ ? node.props.fields.split(',').map((f) => f.trim()).filter(Boolean)
388
+ : [];
389
+ out = { ...out, action: { name: node.props.action, fields } };
390
+ }
391
+ }
392
+ return out;
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // Rich-component expansion (results → primitive card stack)
397
+ // ---------------------------------------------------------------------------
398
+
399
+ function el(
400
+ tag: string,
401
+ props: Record<string, string | number | boolean>,
402
+ children: ViewNode[],
403
+ action?: { name: string; fields: string[] },
404
+ nav?: string,
405
+ ): ViewNode {
406
+ return { kind: 'element', tag, props, children, ...(action ? { action } : {}), ...(nav ? { nav } : {}) };
407
+ }
408
+ function txt(value: string): ViewNode {
409
+ return { kind: 'text', value };
410
+ }
411
+
412
+ /**
413
+ * `results` → a stack of selectable cards (one per `result`). Each card shows
414
+ * title/subtitle/badge and an "Open" affordance: `to` navigates client-side to a
415
+ * named view; else `action` (or `open:<id>` when only `id` is given) drives a turn.
416
+ */
417
+ function expandResults(node: Extract<ViewNode, { kind: 'element' }>): ViewNode {
418
+ const cards = node.children
419
+ .filter((c): c is Extract<ViewNode, { kind: 'element' }> => c.kind === 'element' && c.tag === 'result')
420
+ .map((r) => {
421
+ const p = r.props;
422
+ const id = p.id != null ? String(p.id) : '';
423
+ const title = String(p.title ?? '');
424
+ const subtitle = p.subtitle != null ? String(p.subtitle) : '';
425
+ const badge = p.badge != null ? String(p.badge) : '';
426
+ const to = typeof p.to === 'string' && p.to ? p.to : undefined;
427
+ const action = typeof p.action === 'string' && p.action ? p.action : id ? `open:${id}` : undefined;
428
+
429
+ const left = el('stack', { gap: 'none' }, [
430
+ el('text', { weight: 'bold' }, [txt(title)]),
431
+ ...(subtitle ? [el('text', { tone: 'muted' }, [txt(subtitle)])] : []),
432
+ ]);
433
+ const right: ViewNode[] = [];
434
+ if (badge) right.push(el('badge', {}, [txt(badge)]));
435
+ if (to) right.push(el('button', { to, label: 'Open', variant: 'secondary' }, [], undefined, to));
436
+ else if (action) right.push(el('button', { action, label: 'Open', variant: 'primary' }, [], { name: action, fields: [] }));
437
+
438
+ return el('card', {}, [
439
+ el('row', { justify: 'between', align: 'center' }, [left, el('row', { gap: 'md', align: 'center' }, right)]),
440
+ ]);
441
+ });
442
+ return el('stack', { gap: 'md' }, cards);
443
+ }
444
+
445
+ function expand(node: ViewNode): ViewNode {
446
+ if (node.kind !== 'element') return node;
447
+ if (node.tag === 'results') return expandResults(node);
448
+ return { ...node, children: node.children.map(expand) };
449
+ }
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // Public API
453
+ // ---------------------------------------------------------------------------
454
+
455
+ export function parseView(source: string, allowList: ReadonlyArray<ViewTagSpec>): ViewParseResult {
456
+ try {
457
+ const tokens = tokenize(source);
458
+ const rootB = build(tokens, source);
459
+ const specByTag = new Map(allowList.map((s) => [s.tag, s]));
460
+ const errors: ViewParseError[] = [];
461
+ let root = convert(rootB, specByTag, errors);
462
+ if (errors.length) return { ok: false, errors };
463
+ root = expand(root);
464
+ const title = root.kind === 'element' && root.tag === 'view' && typeof root.props.title === 'string' ? root.props.title : undefined;
465
+ return { ok: true, doc: { root, ...(title ? { title } : {}) } };
466
+ } catch (e) {
467
+ if (e instanceof ParseFail) return { ok: false, errors: [e.info] };
468
+ return { ok: false, errors: [{ message: e instanceof Error ? e.message : String(e) }] };
469
+ }
470
+ }
471
+
472
+ /** Validate an already-built (expanded) AST against the allow-list. */
473
+ export function validateDoc(doc: ViewDoc, allowList: ReadonlyArray<ViewTagSpec>): ViewParseError[] {
474
+ const specByTag = new Map(allowList.map((s) => [s.tag, s]));
475
+ const errors: ViewParseError[] = [];
476
+ const walk = (node: ViewNode) => {
477
+ if (node.kind !== 'element') return;
478
+ const spec = specByTag.get(node.tag);
479
+ if (!spec) {
480
+ errors.push({ message: `unknown tag <${node.tag}>` });
481
+ return;
482
+ }
483
+ for (const [key, value] of Object.entries(node.props)) {
484
+ if (!spec.attrs[key]) {
485
+ errors.push({ message: `<${node.tag}> unknown attribute "${key}"` });
486
+ continue;
487
+ }
488
+ // URL-scheme allow-list — an AST handed straight to validateDoc (i.e.
489
+ // one that never went through parseView's coerceValue) must not smuggle
490
+ // a javascript:/data:text href past the renderer.
491
+ if ((key === 'href' || key === 'src') && typeof value === 'string' && !isSafeViewUrl(value, key)) {
492
+ errors.push({ message: `<${node.tag}> attribute "${key}" has a disallowed URL scheme` });
493
+ }
494
+ }
495
+ for (const [name, aspec] of Object.entries(spec.attrs)) {
496
+ if (aspec.required && !(name in node.props)) {
497
+ errors.push({ message: `<${node.tag}> missing required attribute "${name}"` });
498
+ }
499
+ }
500
+ for (const c of node.children) {
501
+ if (c.kind === 'element') {
502
+ if (spec.allowedChildren === 'none') {
503
+ errors.push({ message: `<${node.tag}> may not have children` });
504
+ } else if (Array.isArray(spec.allowedChildren) && !spec.allowedChildren.includes(c.tag)) {
505
+ errors.push({ message: `<${node.tag}> may not contain <${c.tag}>` });
506
+ }
507
+ } else if (spec.allowedChildren !== 'any' && c.value.trim()) {
508
+ errors.push({ message: `<${node.tag}> may not contain text` });
509
+ }
510
+ walk(c);
511
+ }
512
+ };
513
+ walk(doc.root);
514
+ return errors;
515
+ }
516
+
517
+ // Count element + text nodes (for tool result `nodeCount`). The recursion now
518
+ // lives in @moxxy/sdk so core's parser and the plugin-view tool share one
519
+ // implementation; re-exported here to keep core's public export shape.
520
+ export { countNodes };