@huynle/hub 0.6.1 → 0.7.2

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 (355) hide show
  1. package/CHANGELOG.md +870 -0
  2. package/Dockerfile +33 -0
  3. package/README.md +1007 -66
  4. package/package.json +47 -2
  5. package/packages/claude-code/package.json +12 -0
  6. package/packages/claude-code/src/bridge.ts +79 -0
  7. package/packages/claude-code/src/daemon.ts +339 -0
  8. package/packages/claude-code/src/hook-input.ts +44 -0
  9. package/packages/claude-code/src/ids.ts +27 -0
  10. package/packages/claude-code/src/index.ts +18 -0
  11. package/packages/claude-code/src/launcher.ts +265 -0
  12. package/packages/claude-code/src/main.ts +127 -0
  13. package/packages/claude-code/src/mcp.ts +109 -0
  14. package/packages/claude-code/src/paths.ts +34 -0
  15. package/packages/claude-code/src/plugin.ts +99 -0
  16. package/packages/claude-code/src/references.ts +51 -0
  17. package/packages/claude-code/src/sessions.ts +600 -0
  18. package/packages/claude-code/src/tools.ts +66 -0
  19. package/packages/claude-code/src/wire.ts +71 -0
  20. package/packages/claude-code/src/wrapper.ts +156 -0
  21. package/packages/cli/dist/hub-cli.js +21647 -0
  22. package/packages/cli/package.json +11 -0
  23. package/packages/cli/src/acting.ts +70 -0
  24. package/packages/cli/src/args.ts +91 -0
  25. package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
  26. package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
  27. package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
  28. package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
  29. package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
  30. package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
  31. package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
  32. package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
  33. package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
  34. package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
  35. package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
  36. package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
  37. package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
  38. package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
  39. package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
  40. package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
  41. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
  42. package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
  43. package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
  44. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
  45. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
  46. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
  47. package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
  48. package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
  49. package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
  50. package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
  51. package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
  52. package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
  53. package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
  54. package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
  55. package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
  56. package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
  57. package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
  58. package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
  59. package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
  60. package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
  61. package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
  62. package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
  63. package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
  64. package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
  65. package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
  66. package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
  67. package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
  68. package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
  69. package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
  70. package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
  71. package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
  72. package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
  73. package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
  74. package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
  75. package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
  76. package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
  77. package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
  78. package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
  79. package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
  80. package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
  81. package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
  82. package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
  83. package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
  84. package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
  85. package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
  86. package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
  87. package/packages/cli/src/bootstrap/preview/index.ts +5 -0
  88. package/packages/cli/src/bootstrap/preview/links.ts +31 -0
  89. package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
  90. package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
  91. package/packages/cli/src/bootstrap/preview/render.ts +30 -0
  92. package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
  93. package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
  94. package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
  95. package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
  96. package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
  97. package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
  98. package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
  99. package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
  100. package/packages/cli/src/bootstrap/release/README.md +115 -0
  101. package/packages/cli/src/bootstrap/release/assets.ts +39 -0
  102. package/packages/cli/src/bootstrap/release/cli.ts +332 -0
  103. package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
  104. package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
  105. package/packages/cli/src/bootstrap/release/pins.ts +192 -0
  106. package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
  107. package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
  108. package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
  109. package/packages/cli/src/bootstrap/release/verify.ts +88 -0
  110. package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
  111. package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
  112. package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
  113. package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
  114. package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
  115. package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
  116. package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
  117. package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
  118. package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
  119. package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
  120. package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
  121. package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
  122. package/packages/cli/src/cli.ts +814 -0
  123. package/packages/cli/src/credentials.ts +75 -0
  124. package/packages/cli/src/decisions.ts +106 -0
  125. package/packages/cli/src/format.ts +185 -0
  126. package/packages/cli/src/grants.ts +105 -0
  127. package/packages/cli/src/http.ts +86 -0
  128. package/packages/cli/src/launches.ts +157 -0
  129. package/packages/cli/src/login.ts +160 -0
  130. package/packages/cli/src/main.ts +63 -0
  131. package/packages/cli/src/mcp.ts +879 -0
  132. package/packages/cli/src/messaging.ts +108 -0
  133. package/packages/cli/src/node.ts +279 -0
  134. package/packages/cli/src/openshell.ts +166 -0
  135. package/packages/cli/src/setup/commands.ts +809 -0
  136. package/packages/cli/src/setup/contributors.ts +171 -0
  137. package/packages/cli/src/setup/dispatch.ts +74 -0
  138. package/packages/cli/src/setup/launch-driver.ts +109 -0
  139. package/packages/cli/src/setup/runtime.ts +456 -0
  140. package/packages/cli/src/setup/spoke-probe.ts +120 -0
  141. package/packages/cli/src/setup/state.ts +244 -0
  142. package/packages/cli/src/setup/user-bin.ts +39 -0
  143. package/packages/cli/src/spoke-services.ts +109 -0
  144. package/packages/cli/src/watch.ts +28 -0
  145. package/packages/codex/package.json +12 -0
  146. package/packages/codex/src/app-server.ts +44 -0
  147. package/packages/codex/src/ids.ts +28 -0
  148. package/packages/codex/src/index.ts +11 -0
  149. package/packages/codex/src/jsonrpc.ts +168 -0
  150. package/packages/codex/src/main.ts +123 -0
  151. package/packages/codex/src/paths.ts +65 -0
  152. package/packages/codex/src/sessions.ts +578 -0
  153. package/packages/codex/src/spoke.ts +252 -0
  154. package/packages/codex/src/stdio.ts +101 -0
  155. package/packages/codex/src/websocket.ts +206 -0
  156. package/packages/codex/src/wrapper.ts +104 -0
  157. package/packages/node/package.json +12 -0
  158. package/packages/node/src/config.ts +184 -0
  159. package/packages/node/src/connection.ts +283 -0
  160. package/packages/node/src/daemon.ts +281 -0
  161. package/packages/node/src/git.ts +107 -0
  162. package/packages/node/src/harnesses.ts +95 -0
  163. package/packages/node/src/host.ts +319 -0
  164. package/packages/node/src/index.ts +17 -0
  165. package/packages/node/src/main.ts +11 -0
  166. package/packages/node/src/openshell.ts +73 -0
  167. package/packages/node/src/run.ts +106 -0
  168. package/packages/preset/package.json +12 -0
  169. package/packages/preset/presets/aider.json +8 -0
  170. package/packages/preset/presets/goose.json +8 -0
  171. package/packages/preset/src/index.ts +2 -0
  172. package/packages/preset/src/main.ts +195 -0
  173. package/packages/preset/src/presets.ts +87 -0
  174. package/packages/preset/src/spoke.ts +251 -0
  175. package/packages/preset/src/tmux.ts +160 -0
  176. package/packages/protocol/package.json +12 -0
  177. package/packages/protocol/src/harness-events.ts +207 -0
  178. package/packages/protocol/src/index.ts +1215 -0
  179. package/packages/protocol/src/launch.ts +172 -0
  180. package/packages/protocol/src/node.ts +409 -0
  181. package/packages/protocol/src/preset.ts +144 -0
  182. package/packages/protocol/src/webhook.ts +62 -0
  183. package/packages/protocol/src/wire.ts +54 -0
  184. package/packages/runtime-openshell/image/Dockerfile +36 -0
  185. package/packages/runtime-openshell/image/README.md +78 -0
  186. package/packages/runtime-openshell/image/build.ts +203 -0
  187. package/packages/runtime-openshell/image/entry.sh +163 -0
  188. package/packages/runtime-openshell/image/image.ts +119 -0
  189. package/packages/runtime-openshell/image/start.sh +24 -0
  190. package/packages/runtime-openshell/package.json +12 -0
  191. package/packages/runtime-openshell/src/cli.ts +584 -0
  192. package/packages/runtime-openshell/src/index.ts +6 -0
  193. package/packages/runtime-openshell/src/policy.ts +155 -0
  194. package/packages/runtime-openshell/src/profiles.ts +181 -0
  195. package/packages/runtime-openshell/src/runtime.ts +475 -0
  196. package/packages/runtime-sdk/package.json +12 -0
  197. package/packages/runtime-sdk/src/index.ts +87 -0
  198. package/packages/sdk/package.json +12 -0
  199. package/packages/sdk/src/address.ts +25 -0
  200. package/packages/sdk/src/client.ts +553 -0
  201. package/packages/sdk/src/envelope.ts +108 -0
  202. package/packages/sdk/src/errors.ts +107 -0
  203. package/packages/sdk/src/index.ts +10 -0
  204. package/packages/sdk/src/sse.ts +70 -0
  205. package/packages/sdk/src/types.ts +517 -0
  206. package/packages/sdk/src/webhook.ts +102 -0
  207. package/packages/spoke-sdk/package.json +12 -0
  208. package/packages/spoke-sdk/src/activity.ts +3 -0
  209. package/packages/spoke-sdk/src/connection.ts +295 -0
  210. package/packages/spoke-sdk/src/delivery.ts +119 -0
  211. package/packages/spoke-sdk/src/enroll.ts +101 -0
  212. package/packages/spoke-sdk/src/harness-hello.ts +79 -0
  213. package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
  214. package/packages/spoke-sdk/src/index.ts +12 -0
  215. package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
  216. package/packages/spoke-sdk/src/prompt-source.ts +8 -0
  217. package/packages/spoke-sdk/src/session-ids.ts +49 -0
  218. package/packages/spoke-sdk/src/session-tokens.ts +78 -0
  219. package/scripts/hub-claude-code.service +34 -0
  220. package/scripts/hub-codex-spoke.service +34 -0
  221. package/scripts/hub-node.service +35 -0
  222. package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
  223. package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
  224. package/scripts/io.github.huynle.hub-node.plist +52 -0
  225. package/src/central/actions.ts +233 -0
  226. package/src/central/agent/acting-routes.ts +133 -0
  227. package/src/central/agent/acting-switch.ts +93 -0
  228. package/src/central/agent/acting.ts +599 -0
  229. package/src/central/agent/actor.ts +193 -0
  230. package/src/central/agent/app-tokens.ts +152 -0
  231. package/src/central/agent/archive-inactive.ts +66 -0
  232. package/src/central/agent/audit.ts +185 -0
  233. package/src/central/agent/decisions.ts +530 -0
  234. package/src/central/agent/delivery.ts +678 -0
  235. package/src/central/agent/directory-routes.ts +328 -0
  236. package/src/central/agent/directory.ts +473 -0
  237. package/src/central/agent/envelope.ts +51 -0
  238. package/src/central/agent/errors.ts +74 -0
  239. package/src/central/agent/escalation.ts +219 -0
  240. package/src/central/agent/event-cursor.ts +49 -0
  241. package/src/central/agent/event-writers.ts +181 -0
  242. package/src/central/agent/events-routes.ts +148 -0
  243. package/src/central/agent/events.ts +277 -0
  244. package/src/central/agent/grant-requests.ts +221 -0
  245. package/src/central/agent/grant-routes.ts +70 -0
  246. package/src/central/agent/grants.ts +235 -0
  247. package/src/central/agent/launch-engine.ts +1025 -0
  248. package/src/central/agent/launch-routes.ts +252 -0
  249. package/src/central/agent/launch-spec.ts +289 -0
  250. package/src/central/agent/launch-store.ts +264 -0
  251. package/src/central/agent/launch-tokens.ts +206 -0
  252. package/src/central/agent/message-limits.ts +123 -0
  253. package/src/central/agent/message-routes.ts +385 -0
  254. package/src/central/agent/messages.ts +442 -0
  255. package/src/central/agent/names.ts +86 -0
  256. package/src/central/agent/placement.ts +419 -0
  257. package/src/central/agent/policy.ts +378 -0
  258. package/src/central/agent/read-routes.ts +385 -0
  259. package/src/central/agent/routes.ts +281 -0
  260. package/src/central/agent/session-tokens.ts +227 -0
  261. package/src/central/agent/versioned.ts +26 -0
  262. package/src/central/agent/watch.ts +112 -0
  263. package/src/central/agent/webhook-routes.ts +86 -0
  264. package/src/central/agent/webhooks.ts +493 -0
  265. package/src/central/agent-data.ts +177 -0
  266. package/src/central/auth.ts +350 -121
  267. package/src/central/backend/file-state.ts +240 -0
  268. package/src/central/backend/index.ts +116 -0
  269. package/src/central/backend/memory-bus.ts +93 -0
  270. package/src/central/backend/memory-catalog.ts +90 -0
  271. package/src/central/backend/memory-counters.ts +102 -0
  272. package/src/central/backend/memory-events.ts +45 -0
  273. package/src/central/backend/memory-kv.ts +38 -0
  274. package/src/central/backend/memory-state.ts +77 -0
  275. package/src/central/backend/postgres-migrate.ts +100 -0
  276. package/src/central/backend/postgres-state.ts +197 -0
  277. package/src/central/backend/types.ts +246 -0
  278. package/src/central/backend/valkey-bus.ts +425 -0
  279. package/src/central/backend/valkey-catalog.ts +190 -0
  280. package/src/central/backend/valkey-client.ts +36 -0
  281. package/src/central/backend/valkey-counters.ts +38 -0
  282. package/src/central/backend/valkey-events.ts +168 -0
  283. package/src/central/backend/valkey-kv.ts +54 -0
  284. package/src/central/backend/valkey-locks.ts +92 -0
  285. package/src/central/cluster/directory.ts +141 -0
  286. package/src/central/cluster/feed.ts +84 -0
  287. package/src/central/cluster/leader.ts +232 -0
  288. package/src/central/cluster/live.ts +151 -0
  289. package/src/central/cluster/recommend.ts +59 -0
  290. package/src/central/cluster/rpc.ts +150 -0
  291. package/src/central/cluster/view.ts +232 -0
  292. package/src/central/config.ts +197 -3
  293. package/src/central/done.ts +96 -0
  294. package/src/central/events-log.ts +100 -0
  295. package/src/central/http.ts +1149 -170
  296. package/src/central/import.ts +318 -0
  297. package/src/central/main.ts +211 -12
  298. package/src/central/mcp-http.ts +116 -0
  299. package/src/central/nodes.ts +314 -0
  300. package/src/central/oidc.ts +34 -14
  301. package/src/central/push-target.ts +118 -18
  302. package/src/central/push.ts +256 -82
  303. package/src/central/registry.ts +902 -92
  304. package/src/central/say-stream.ts +128 -0
  305. package/src/central/session-token-push.ts +233 -0
  306. package/src/central/speech/openai.ts +21 -14
  307. package/src/central/speech/openrouter.ts +27 -10
  308. package/src/central/speech/provider.ts +54 -0
  309. package/src/central/speech/service.ts +22 -7
  310. package/src/central/spoke-ws.ts +522 -44
  311. package/src/central/store-shared.ts +518 -0
  312. package/src/central/store.ts +625 -189
  313. package/src/central/token-keys.ts +165 -0
  314. package/src/config.ts +118 -7
  315. package/src/index.ts +195 -43
  316. package/src/protocol.ts +3 -650
  317. package/src/rpc.ts +27 -2
  318. package/src/spoke/client.ts +3 -272
  319. package/src/spoke/conversation.ts +16 -12
  320. package/src/spoke/deliver.ts +75 -0
  321. package/src/spoke/enroll.ts +2 -79
  322. package/src/spoke/env-inject.ts +160 -0
  323. package/src/spoke/hub-rpc.ts +115 -0
  324. package/src/spoke/hub-tool.ts +409 -0
  325. package/src/spoke/launch-source.ts +22 -0
  326. package/src/spoke/launchers.ts +174 -0
  327. package/src/spoke/locations.ts +3 -2
  328. package/src/spoke/node-roots.ts +61 -0
  329. package/src/spoke/prompt-refs.ts +116 -0
  330. package/src/spoke/session-tokens.ts +2 -0
  331. package/src/spoke/spoke.ts +465 -28
  332. package/src/spoke/taskboard.ts +243 -0
  333. package/src/spoke/timeline.ts +184 -6
  334. package/src/spoke/tracker.ts +79 -4
  335. package/src/status.ts +203 -10
  336. package/src/tui-commands.ts +13 -6
  337. package/src/tui-messaging.ts +93 -0
  338. package/src/tui.ts +15 -0
  339. package/web/dist/apple-touch-icon.png +0 -0
  340. package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
  341. package/web/dist/assets/index-DWnMhXk9.js +20 -0
  342. package/web/dist/assets/index-DmUXEnw9.css +1 -0
  343. package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
  344. package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
  345. package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
  346. package/web/dist/favicon.ico +0 -0
  347. package/web/dist/icon-192.png +0 -0
  348. package/web/dist/icon-512.png +0 -0
  349. package/web/dist/icon-maskable-512.png +0 -0
  350. package/web/dist/icon.svg +5 -5
  351. package/web/dist/index.html +17 -4
  352. package/web/dist/manifest.webmanifest +2 -2
  353. package/web/dist/sw.js +79 -15
  354. package/web/dist/assets/index-DClBvfLL.css +0 -1
  355. package/web/dist/assets/index-Djo_yF0a.js +0 -12
package/src/status.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // Live status of a voice session and the subagent sessions it spawns, built from OpenCode events.
2
+ import type { JobInfo } from "./protocol"
2
3
 
3
4
  export type SessionState = "idle" | "working" | "blocked" | "error"
4
5
 
@@ -6,10 +7,26 @@ export interface Blocker {
6
7
  readonly kind: "permission" | "form"
7
8
  readonly id: string
8
9
  readonly summary: string
10
+ /** permission: what OpenCode asks to do ("shell", "edit", "read", an MCP tool), when it said. */
11
+ readonly action?: string
12
+ /** permission: what it applies to (commands, paths), at most BLOCKER_MAX_RESOURCES, each shortened. */
13
+ readonly resources?: readonly string[]
9
14
  /** form: asked by the agent's question tool (metadata.kind "question"). */
10
15
  readonly question?: true
11
16
  /** form: its fields, bounded, so a phone can render and answer it. */
12
17
  readonly fields?: readonly FormFieldSpec[]
18
+ /** permission: an agent escalated it (outside its grant) and recommends this (the hub adds it, P3-T8). */
19
+ readonly recommendation?: BlockerRecommendation
20
+ }
21
+
22
+ /** "@orchestrator recommends Approve: only runs tests in the worktree". */
23
+ export interface BlockerRecommendation {
24
+ /** Who recommends it, as the hub names the agent. */
25
+ readonly by: string
26
+ readonly recommends: "approve"
27
+ readonly why?: string
28
+ /** The Decision the user answers it in. */
29
+ readonly decisionID?: string
13
30
  }
14
31
 
15
32
  export interface FormOptionSpec {
@@ -53,6 +70,75 @@ const FIELD_TYPES = new Set<FormFieldSpec["type"]>(["string", "number", "integer
53
70
  const text = (v: unknown, max: number) => (typeof v === "string" && v ? v.slice(0, max) : undefined)
54
71
  const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined)
55
72
 
73
+ /**
74
+ * What a permission request asks, as OpenCode sent it (permission.asked), kept so a policy can judge it.
75
+ * metadata (diff previews and the like) is dropped. When anything had to be cut or dropped, `truncated` is
76
+ * set: a judgement on what is left does not cover the whole request.
77
+ */
78
+ export interface PermissionRequest {
79
+ /** "shell", "read", "edit", "external_directory", an MCP tool ("<server>_<tool>"), … */
80
+ readonly action: string
81
+ /** What the action applies to (one per parsed shell command, paths, "*"). */
82
+ readonly resources: readonly string[]
83
+ /** The patterns an "always" reply would save as rules. */
84
+ readonly save?: readonly string[]
85
+ /** The tool call that asked. */
86
+ readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
87
+ readonly truncated?: true
88
+ }
89
+
90
+ /** Resources (and save patterns) kept per request, and the length kept of each. */
91
+ export const PERMISSION_MAX_RESOURCES = 100
92
+ export const PERMISSION_TEXT_MAX = 4000
93
+ /** Longest action kept; a longer one makes the request unusable (undefined). */
94
+ const PERMISSION_ACTION_MAX = 200
95
+ const SOURCE_ID_MAX = 200
96
+ /** Resources shown on a blocker (catalog and status go to every console), and the length of each. */
97
+ export const BLOCKER_MAX_RESOURCES = 10
98
+ const BLOCKER_RESOURCE_MAX = 500
99
+
100
+ /** Strings of `list`, bounded; `cut` is set when anything was dropped or shortened. */
101
+ function boundedStrings(list: unknown, maxItems: number, maxLength: number): { items: string[]; cut: boolean } {
102
+ if (!Array.isArray(list)) return { items: [], cut: list !== undefined }
103
+ const strings = list.filter((v): v is string => typeof v === "string")
104
+ const items = strings.slice(0, maxItems).map((v) => v.slice(0, maxLength))
105
+ const cut = strings.length !== list.length || strings.length > maxItems || strings.some((v) => v.length > maxLength)
106
+ return { items, cut }
107
+ }
108
+
109
+ /** A permission.asked event's request, bounded; undefined without a usable action. */
110
+ export function permissionRequest(raw: any): PermissionRequest | undefined {
111
+ const action = raw?.action
112
+ if (typeof action !== "string" || !action || action.length > PERMISSION_ACTION_MAX) return undefined
113
+ const resources = boundedStrings(raw.resources, PERMISSION_MAX_RESOURCES, PERMISSION_TEXT_MAX)
114
+ const save = raw.save === undefined ? undefined : boundedStrings(raw.save, PERMISSION_MAX_RESOURCES, PERMISSION_TEXT_MAX)
115
+ const src = raw.source
116
+ const source =
117
+ src?.type === "tool" && typeof src.messageID === "string" && typeof src.id === "string"
118
+ ? { type: "tool" as const, messageID: src.messageID.slice(0, SOURCE_ID_MAX), id: src.id.slice(0, SOURCE_ID_MAX) }
119
+ : undefined
120
+ return {
121
+ action,
122
+ resources: resources.items,
123
+ ...(save ? { save: save.items } : {}),
124
+ ...(source ? { source } : {}),
125
+ ...(resources.cut || save?.cut ? { truncated: true as const } : {}),
126
+ }
127
+ }
128
+
129
+ /** A permission.asked event as a blocker: its summary, and what it asks (bounded for display). */
130
+ function permissionBlocker(data: Record<string, any>): Blocker {
131
+ const action = typeof data.action === "string" && data.action ? data.action.slice(0, PERMISSION_ACTION_MAX) : undefined
132
+ const resources = boundedStrings(data.resources, BLOCKER_MAX_RESOURCES, BLOCKER_RESOURCE_MAX).items
133
+ return {
134
+ kind: "permission",
135
+ id: data.id,
136
+ summary: [data.action, (data.resources ?? []).join(", ")].filter(Boolean).join(": "),
137
+ ...(action ? { action } : {}),
138
+ ...(resources.length ? { resources } : {}),
139
+ }
140
+ }
141
+
56
142
  function optionSpec(raw: any): FormOptionSpec | undefined {
57
143
  const value = text(raw?.value, FORM_LABEL_MAX)
58
144
  if (value === undefined) return undefined
@@ -112,15 +198,28 @@ export interface SessionStatus {
112
198
  readonly state: SessionState
113
199
  readonly detail?: string
114
200
  readonly blockers: ReadonlyArray<Blocker>
201
+ /** OpenCode is retrying the model request (state stays "working"). */
202
+ readonly retry?: { readonly attempt?: number; readonly message?: string }
203
+ /** Shells and terminals of the session: running ones, then the last few that ended (oldest first). */
204
+ readonly jobs?: readonly JobInfo[]
115
205
  }
116
206
 
207
+ /** Ended jobs kept per session (shown briefly with their exit code). */
208
+ export const ENDED_JOBS_KEPT = 5
209
+ /** Jobs kept per session at most (running ones beyond it are dropped oldest first). */
210
+ const JOBS_MAX = 50
211
+
117
212
  interface Entry {
118
213
  id: string
119
214
  parentID?: string
120
215
  title: string
121
216
  running: boolean
217
+ /** session.status: busy or retry (OpenCode's own view of the session). */
218
+ busy: boolean
219
+ retry?: { attempt?: number; message?: string }
122
220
  error?: string
123
221
  blockers: Blocker[]
222
+ jobs: JobInfo[]
124
223
  }
125
224
 
126
225
  interface EventLike {
@@ -132,7 +231,7 @@ export class StatusBoard {
132
231
  private readonly sessions = new Map<string, Entry>()
133
232
 
134
233
  constructor(rootID: string, title: string) {
135
- this.sessions.set(rootID, { id: rootID, title, running: false, blockers: [] })
234
+ this.sessions.set(rootID, { id: rootID, title, running: false, busy: false, blockers: [], jobs: [] })
136
235
  }
137
236
 
138
237
  /** Applies an event. Returns true when the visible status changed. */
@@ -146,7 +245,9 @@ export class StatusBoard {
146
245
  parentID: data.parentID,
147
246
  title: data.title ?? data.agent ?? data.slug ?? "subagent",
148
247
  running: false,
248
+ busy: false,
149
249
  blockers: [],
250
+ jobs: [],
150
251
  })
151
252
  return true
152
253
  }
@@ -161,20 +262,16 @@ export class StatusBoard {
161
262
  })
162
263
  case "session.execution.succeeded":
163
264
  case "session.execution.interrupted":
164
- return this.update(data.sessionID, (entry) => (entry.running = false))
265
+ return this.update(data.sessionID, (entry) => endRun(entry))
165
266
  case "session.execution.failed":
166
267
  return this.update(data.sessionID, (entry) => {
167
- entry.running = false
268
+ endRun(entry)
168
269
  entry.error = String(data.error?.message ?? "failed")
169
270
  })
170
271
  case "permission.asked":
171
272
  return this.update(data.sessionID, (entry) => {
172
273
  if (entry.blockers.some((b) => b.id === data.id)) return
173
- entry.blockers.push({
174
- kind: "permission",
175
- id: data.id,
176
- summary: [data.action, (data.resources ?? []).join(", ")].filter(Boolean).join(": "),
177
- })
274
+ entry.blockers.push(permissionBlocker(data))
178
275
  })
179
276
  case "permission.replied":
180
277
  return this.unblock(data.sessionID, data.requestID)
@@ -186,6 +283,30 @@ export class StatusBoard {
186
283
  case "form.replied":
187
284
  case "form.cancelled":
188
285
  return this.unblock(data.sessionID, data.id)
286
+ case "session.status":
287
+ return this.update(data.sessionID, (entry) => setStatus(entry, data.status))
288
+ case "session.seed":
289
+ return this.update(data.sessionID, (entry) => {
290
+ setStatus(entry, data.status)
291
+ entry.jobs = entry.jobs.filter((job) => job.status !== "running")
292
+ for (const job of Array.isArray(data.jobs) ? data.jobs : []) if (isJob(job)) upsertJob(entry, job)
293
+ })
294
+ case "shell.created":
295
+ case "session.shell.started":
296
+ case "pty.created":
297
+ case "pty.updated":
298
+ case "shell.exited":
299
+ case "session.shell.ended":
300
+ case "pty.exited":
301
+ return this.update(data.sessionID, (entry) => {
302
+ const job = jobOf(data, entry.jobs.find((j) => j.id === data.id))
303
+ if (job) upsertJob(entry, job)
304
+ })
305
+ case "shell.deleted":
306
+ case "pty.deleted":
307
+ return this.update(data.sessionID, (entry) => {
308
+ entry.jobs = entry.jobs.filter((job) => job.id !== data.id)
309
+ })
189
310
  default:
190
311
  return false
191
312
  }
@@ -200,6 +321,18 @@ export class StatusBoard {
200
321
  return [...this.sessions.keys()]
201
322
  }
202
323
 
324
+ /**
325
+ * True when nothing in the tree runs (no run, busy or retrying status), waits for an answer, or has a running
326
+ * shell job. Terminals (pty) do not count: an open terminal is not work in progress.
327
+ */
328
+ settled(): boolean {
329
+ for (const entry of this.sessions.values()) {
330
+ if (entry.running || entry.busy || entry.blockers.length) return false
331
+ if (entry.jobs.some((job) => job.status === "running" && job.kind === "shell")) return false
332
+ }
333
+ return true
334
+ }
335
+
203
336
  snapshot(): { sessions: SessionStatus[] } {
204
337
  return { sessions: [...this.sessions.values()].map(view) }
205
338
  }
@@ -218,15 +351,75 @@ export class StatusBoard {
218
351
  }
219
352
  }
220
353
 
354
+ const JOB_STATUSES = new Set(["running", "exited", "timeout", "killed"])
355
+
356
+ function isJob(v: any): v is JobInfo {
357
+ return typeof v?.id === "string" && v.id && (v.kind === "shell" || v.kind === "pty") && JOB_STATUSES.has(v.status)
358
+ }
359
+
360
+ /** A job event's data (compact, see compactActivity) merged over what is known of the job. */
361
+ function jobOf(data: Record<string, any>, known: JobInfo | undefined): JobInfo | undefined {
362
+ if (typeof data.id !== "string" || !JOB_STATUSES.has(data.status)) return undefined
363
+ const kind = data.kind === "pty" ? "pty" : "shell"
364
+ const ended = data.status !== "running"
365
+ const at = num(data.at)
366
+ const command = text(data.command, 200) ?? known?.command
367
+ const exit = num(data.exit) ?? (ended ? known?.exit : undefined)
368
+ const started = known?.started ?? (!ended ? at : undefined)
369
+ return {
370
+ id: data.id,
371
+ kind,
372
+ ...(command ? { command } : {}),
373
+ status: data.status,
374
+ ...(exit !== undefined ? { exit } : {}),
375
+ ...(data.background === true || known?.background ? { background: true as const } : {}),
376
+ ...(started !== undefined ? { started } : {}),
377
+ ...(ended && at !== undefined ? { ended: at } : {}),
378
+ }
379
+ }
380
+
381
+ function upsertJob(entry: Entry, job: JobInfo): void {
382
+ const rest = entry.jobs.filter((j) => j.id !== job.id)
383
+ rest.push({ ...job })
384
+ const running = rest.filter((j) => j.status === "running")
385
+ const endedJobs = rest.filter((j) => j.status !== "running").slice(-ENDED_JOBS_KEPT)
386
+ const keep = new Set([...running.slice(-JOBS_MAX), ...endedJobs])
387
+ entry.jobs = rest.filter((j) => keep.has(j))
388
+ }
389
+
390
+ /** A run ended: OpenCode v2 sends no idle status, so a busy or retrying status (e.g. seeded) ends with it. */
391
+ function endRun(entry: Entry): void {
392
+ entry.running = false
393
+ entry.busy = false
394
+ entry.retry = undefined
395
+ }
396
+
397
+ function setStatus(entry: Entry, status: any): void {
398
+ const type = status?.type
399
+ if (type !== "idle" && type !== "busy" && type !== "retry") return
400
+ entry.busy = type !== "idle"
401
+ if (type === "retry") {
402
+ const attempt = num(status.attempt)
403
+ const message = text(status.message, 200)
404
+ entry.retry = { ...(attempt !== undefined ? { attempt } : {}), ...(message ? { message } : {}) }
405
+ } else entry.retry = undefined
406
+ if (type === "busy") entry.error = undefined
407
+ }
408
+
221
409
  function view(entry: Entry): SessionStatus {
410
+ const working = entry.running || entry.busy || entry.jobs.some((job) => job.status === "running")
222
411
  const state: SessionState =
223
- entry.blockers.length > 0 ? "blocked" : entry.error ? "error" : entry.running ? "working" : "idle"
412
+ entry.blockers.length > 0 ? "blocked" : entry.error && !working ? "error" : working ? "working" : "idle"
413
+ const retry = entry.retry && state === "working" ? entry.retry : undefined
414
+ const detail = state === "error" ? entry.error : retry ? (retry.message ? `Retrying: ${retry.message}` : "Retrying") : undefined
224
415
  return {
225
416
  id: entry.id,
226
417
  ...(entry.parentID ? { parentID: entry.parentID } : {}),
227
418
  title: entry.title,
228
419
  state,
229
- ...(entry.error && state === "error" ? { detail: entry.error } : {}),
420
+ ...(detail ? { detail } : {}),
230
421
  blockers: entry.blockers.map((blocker) => structuredClone(blocker)),
422
+ ...(retry ? { retry: { ...retry } } : {}),
423
+ ...(entry.jobs.length ? { jobs: entry.jobs.map((job) => ({ ...job })) } : {}),
231
424
  }
232
425
  }
@@ -14,7 +14,13 @@ export interface HubCommand {
14
14
  readonly slash: string
15
15
  /** Shown in the command palette after "Hub: ". */
16
16
  readonly title: string
17
- readonly run: () => Promise<unknown>
17
+ /**
18
+ * The command takes arguments: `/<slash> text` runs it with the raw text after the name (quotes and inner
19
+ * spacing kept). Without it, any text after the name sends the whole line to the model instead.
20
+ */
21
+ readonly arguments?: boolean
22
+ /** `input`: the raw arguments; undefined (palette, key binding) and "" (no text) both mean none. */
23
+ readonly run: (input?: string) => Promise<unknown>
18
24
  }
19
25
 
20
26
  /** The shape OpenCode's keymap layer takes for one command. */
@@ -23,8 +29,8 @@ export interface KeymapCommand {
23
29
  readonly title: string
24
30
  readonly group: string
25
31
  readonly palette: true
26
- readonly slash: { readonly name: string }
27
- readonly run: () => void
32
+ readonly slash: { readonly name: string; readonly arguments?: true }
33
+ readonly run: (input?: string) => void
28
34
  }
29
35
 
30
36
  /**
@@ -44,9 +50,10 @@ export function hubCommands(features: readonly HubCommand[], onError: (cause: un
44
50
  title: `${COMMAND_GROUP}: ${feature.title}`,
45
51
  group: COMMAND_GROUP,
46
52
  palette: true,
47
- slash: { name: feature.slash },
48
- run: () => {
49
- void feature.run().catch(onError)
53
+ slash: feature.arguments ? { name: feature.slash, arguments: true } : { name: feature.slash },
54
+ // Keyboard dispatch may pass something other than text; only a string is the command's input.
55
+ run: (input?: unknown) => {
56
+ void feature.run(typeof input === "string" ? input : undefined).catch(onError)
50
57
  },
51
58
  }
52
59
  })
@@ -0,0 +1,93 @@
1
+ // /who and /msg in the TUI: what the user typed after the slash name, and what the TUI shows back. The server side
2
+ // (src/spoke/hub-rpc.ts) talks to the hub as the TUI's current session; labels arrive sanitised from there.
3
+ import type { WhoRow } from "./spoke/hub-rpc"
4
+
5
+ /** One session in /who, as the server's hubWho RPC returns it (type only: the TUI never loads the SDK). */
6
+ export type { WhoRow }
7
+
8
+ export const MSG_USAGE = "Usage: /msg @name text (or @@role, @name.project, ses_…, @me)"
9
+
10
+ /** @name, @@role, @name.project, @me, or a session ID. */
11
+ const ADDRESS = /^(@@?[^\s@]\S*|ses_\S+)$/
12
+
13
+ /**
14
+ * `/msg` arguments: the first word is the address, everything after the whitespace that follows it is the text,
15
+ * word for word (spacing, quotes and new lines kept). Undefined when there are no arguments at all.
16
+ */
17
+ export function parseMsgInput(input?: string): { to: string; text: string } | { error: string } | undefined {
18
+ if (input === undefined || !input.trim()) return undefined
19
+ const match = /^\s*(\S+)(?:\s+([\s\S]*))?$/.exec(input)
20
+ const to = match?.[1] ?? ""
21
+ const text = match?.[2] ?? ""
22
+ if (!ADDRESS.test(to)) return { error: `Start with the address. ${MSG_USAGE}` }
23
+ if (!text.trim()) return { error: `What should the message say? ${MSG_USAGE}` }
24
+ return { to, text }
25
+ }
26
+
27
+ /** `/who` arguments: nothing (everyone), a project, or a @@role. */
28
+ export function parseWhoInput(input?: string): { project?: string; role?: string } {
29
+ const value = input?.trim() ?? ""
30
+ if (!value) return {}
31
+ if (value.startsWith("@@") && value.length > 2) return { role: value.slice(2) }
32
+ return { project: value }
33
+ }
34
+
35
+ /** One line per session: name (else ID) · project · state · online/offline · roles. */
36
+ export function formatWho(rows: readonly WhoRow[]): string {
37
+ if (!rows.length) return "No named or role-holding sessions."
38
+ return rows
39
+ .map((s) =>
40
+ [s.name ? `@${s.name}` : s.sessionID, s.project ?? "", s.state ?? "", s.online ? "online" : "offline", s.roles.map((r) => `@@${r}`).join(" ")]
41
+ .filter(Boolean)
42
+ .join(" · "),
43
+ )
44
+ .join("\n")
45
+ }
46
+
47
+ /** The toast after /msg: who it reached and how ("stored" when the hub only stored it). */
48
+ export function formatSent(sent: { id: string; to: string; delivery?: string }): string {
49
+ return `Sent to ${sent.to} (${sent.delivery ?? "stored"})`
50
+ }
51
+
52
+ /** What /who and /msg need from the TUI (src/tui.ts): the current session, the RPC calls and the UI. */
53
+ export interface MessagingDeps {
54
+ /** The session the TUI shows; undefined on the home screen. */
55
+ sessionID(): string | undefined
56
+ who(input: { sessionID: string; project?: string; role?: string }): Promise<{ sessions: WhoRow[] }>
57
+ msg(input: { sessionID: string; to: string; text: string }): Promise<{ id: string; to: string; delivery?: string }>
58
+ prompt(options: { title: string; description?: string; placeholder?: string; value?: string }): Promise<string | null | undefined>
59
+ alert(options: { title: string; message: string }): Promise<unknown>
60
+ toast(message: string, variant?: "info" | "success" | "warning" | "error"): unknown
61
+ }
62
+
63
+ const NO_SESSION = (what: string) => `Open a session first: ${what} acts as the current session.`
64
+
65
+ /**
66
+ * /msg: sends as the current session. Without arguments (palette, bare /msg) it asks for "@name text"; RPC failures
67
+ * are thrown (the command registry shows them as a toast).
68
+ */
69
+ export async function runMsg(deps: MessagingDeps, input?: string): Promise<void> {
70
+ const sessionID = deps.sessionID()
71
+ if (!sessionID) return void deps.toast(NO_SESSION("/msg"), "warning")
72
+ let parsed = parseMsgInput(input)
73
+ if (!parsed) {
74
+ const typed = await deps.prompt({ title: "Message a session", description: `Sent as this session. ${MSG_USAGE}`, placeholder: "@name text" })
75
+ if (typeof typed !== "string") return
76
+ parsed = parseMsgInput(typed)
77
+ if (!parsed) return
78
+ }
79
+ if ("error" in parsed) return void deps.toast(parsed.error, "error")
80
+ const sent = await deps.msg({ sessionID, to: parsed.to, text: parsed.text })
81
+ deps.toast(formatSent(sent), "success")
82
+ }
83
+
84
+ /** /who [project | @@role]: the hub's named and role-holding sessions in a dialog, asked as the current session. */
85
+ export async function runWho(deps: MessagingDeps, input?: string): Promise<void> {
86
+ const sessionID = deps.sessionID()
87
+ if (!sessionID) return void deps.toast(NO_SESSION("/who"), "warning")
88
+ const filter = parseWhoInput(input)
89
+ const { sessions } = await deps.who({ sessionID, ...filter })
90
+ if (!sessions.length) return void deps.toast(formatWho(sessions), "info")
91
+ const scope = filter.role ? ` · @@${filter.role}` : filter.project ? ` · ${filter.project}` : ""
92
+ await deps.alert({ title: `Hub · who${scope}`, message: formatWho(sessions) })
93
+ }
package/src/tui.ts CHANGED
@@ -5,6 +5,7 @@ import { HubRpc } from "./rpc"
5
5
  import { localServerFor, modeFor, needsSetup, parseServerInput, setupOptions, type SetupChoice } from "./setup"
6
6
  import { hubCommands } from "./tui-commands"
7
7
  import { FormRelay } from "./tui-forms"
8
+ import { runMsg, runWho, type MessagingDeps } from "./tui-messaging"
8
9
  import { PresenceReporter, shownSessions } from "./tui-presence"
9
10
  import { startVoice } from "./voice-command"
10
11
 
@@ -15,6 +16,8 @@ import { startVoice } from "./voice-command"
15
16
  // /voice: no menu — connect (setting up first when needed), share this session, and open it in the browser
16
17
  // with Voice mode starting (src/voice-command.ts). Links are shown here, never posted into the session, so
17
18
  // they do not reach the model. Decisions live in src/setup.ts and src/voice-command.ts (tested).
19
+ // /who [project | @@role] and /msg @name text: other sessions on the hub, as the current session
20
+ // (src/tui-messaging.ts; server side src/spoke/hub-rpc.ts).
18
21
 
19
22
  type Mode = "off" | "always" | "session"
20
23
  interface Status {
@@ -408,6 +411,16 @@ export default Plugin.define({
408
411
  toast,
409
412
  })
410
413
 
414
+ // /who and /msg: the server sends as this TUI's current session, with that session's own hub token.
415
+ const messaging: MessagingDeps = {
416
+ sessionID: currentSession,
417
+ who: (input) => call("hubWho", input),
418
+ msg: (input) => call("hubMsg", input),
419
+ prompt: (options) => context.ui.dialog.prompt(options),
420
+ alert: (options) => context.ui.dialog.alert(options),
421
+ toast: (message, variant) => toast(message, variant),
422
+ }
423
+
411
424
  // Presence: tell the server-side spoke which sessions this TUI shows, so the phone lists them as open.
412
425
  const reporter = `tui_${Math.random().toString(36).slice(2, 12)}`
413
426
  const presence = new PresenceReporter({
@@ -436,6 +449,8 @@ export default Plugin.define({
436
449
  [
437
450
  { id: "open", slash: "hub", title: "server, connection, sharing and phones", run: open },
438
451
  { id: "voice", slash: "voice", title: "talk to this session (Voice mode in the browser)", run: voice },
452
+ { id: "who", slash: "who", title: "who is on the hub (names and roles)", arguments: true, run: (input) => runWho(messaging, input) },
453
+ { id: "msg", slash: "msg", title: "message a session as this session (/msg @name text)", arguments: true, run: (input) => runMsg(messaging, input) },
439
454
  ],
440
455
  (cause) => toast(errorMessage(cause), "error"),
441
456
  )
Binary file
@@ -0,0 +1,5 @@
1
+ var e={ts:`ts`,typescript:`ts`,tsx:`ts`,mts:`ts`,cts:`ts`,js:`ts`,javascript:`ts`,jsx:`ts`,mjs:`ts`,cjs:`ts`,node:`ts`,json:`json`,jsonc:`json`,json5:`json`,bash:`bash`,sh:`bash`,shell:`bash`,zsh:`bash`,console:`bash`,shellsession:`bash`,fish:`bash`,python:`python`,py:`python`,python3:`python`,go:`go`,golang:`go`,rust:`rust`,rs:`rust`,yaml:`yaml`,yml:`yaml`,diff:`diff`,patch:`diff`,sql:`sql`,postgresql:`sql`,postgres:`sql`,mysql:`sql`,sqlite:`sql`,css:`css`,scss:`css`,less:`css`,html:`html`,htm:`html`,xml:`html`,svg:`html`,vue:`html`,md:`md`,markdown:`md`};function t(t){return e[String(t??``).trim().toLowerCase()]}var n=e=>new Set(e.split(` `)),r=e=>e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`||e===`_`||e===`$`,i=e=>e!==void 0&&(r(e)||e>=`0`&&e<=`9`);function a(e,t){let n=t+1;for(;i(e[n]);)n++;return e.slice(t,n)}var o=/(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)[a-zA-Z]{0,3}\b/y,s=/"(?:[^"\\\n]|\\.)*"?/y,c=/'(?:[^'\\\n]|\\.)*'?/y,l=/\/\*[\s\S]*?(?:\*\/|$)/y,u=/\/\/[^\n]*/y,d=/#[^\n]*/y,f={ts:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/`(?:[^`\\]|\\[\s\S])*`?/y,first:"`"},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`abstract as async await break case catch class const continue debugger declare default delete do else enum export extends false finally for from function get if implements import in instanceof interface keyof let new null of private protected public readonly return satisfies set static super switch this throw true try type typeof undefined var void while with yield`),types:n(`string number boolean bigint symbol object unknown never any void Promise Array Record Map Set Partial Readonly`)},json:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`prop`,re:/"(?:[^"\\\n]|\\.)*"(?=\s*:)/y,first:`"`},{kind:`string`,re:s,first:`"`},{kind:`number`,re:/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y,first:`-0123456789`}],keywords:n(`true false null`)},bash:{rules:[{kind:`comment`,re:d,at:`word`,first:`#`},{kind:`string`,re:/"(?:[^"\\]|\\[\s\S])*"?/y,first:`"`},{kind:`string`,re:/'[^']*'?/y,first:`'`},{kind:`prop`,re:/\$(?:\{[^}\n]*\}?|[\w@#?$!*-]+)/y,first:`$`},{kind:`fn`,re:/[\w./-]+/y,at:`command`},{kind:`attr`,re:/--?[\w-]+/y,at:`word`,first:`-`}],keywords:n(`if then else elif fi for do done case esac while until function in export local return select time sudo`)},python:{rules:[{kind:`comment`,re:d,first:`#`},{kind:`string`,re:/[rRbBfFuU]{0,2}(?:"""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$))/y,first:`rRbBfFuU"'`},{kind:`string`,re:/[rRbBfFuU]{0,2}"(?:[^"\\\n]|\\.)*"?/y,first:`rRbBfFuU"'`},{kind:`string`,re:/[rRbBfFuU]{0,2}'(?:[^'\\\n]|\\.)*'?/y,first:`rRbBfFuU"'`},{kind:`meta`,re:/@[\w.]+/y,first:`@`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return self True try while with yield match case`),types:n(`int str float bool list dict set tuple bytes object type`)},go:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/`[^`]*`?/y,first:"`"},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false iota`),types:n(`string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte rune float32 float64 bool error any`)},rust:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/b?r#*"[\s\S]*?(?:"#*|$)/y,first:`br`},{kind:`string`,re:/b?"(?:[^"\\]|\\[\s\S])*"?/y,first:`b"`},{kind:`string`,re:/b?'(?:[^'\\\n]|\\.){1,8}'/y,first:`b'`},{kind:`meta`,re:/#!?\[[^\]\n]*\]?/y,first:`#`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while`),types:n(`i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 bool char str String Vec Option Result Box Some None Ok Err`),macros:!0},yaml:{rules:[{kind:`comment`,re:d,at:`word`,first:`#`},{kind:`prop`,re:/[^\s#:'"][^\n:#]*?(?=\s*:(?:\s|$))/y,at:`line`},{kind:`string`,re:s,first:`"`},{kind:`string`,re:/'(?:[^'\n]|'')*'?/y,first:`'`},{kind:`meta`,re:/---|\.\.\./y,at:`line`,first:`-.`},{kind:`number`,re:/-?\d+(?:\.\d+)?\b/y,first:`-0123456789`}],keywords:n(`true false null yes no on off`)},sql:{rules:[{kind:`comment`,re:/--[^\n]*/y,first:`-`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/'(?:[^']|'')*'?/y,first:`'`},{kind:`prop`,re:/"[^"\n]*"?/y,first:`"`},{kind:`number`,re:/\d+(?:\.\d+)?\b/y,first:`0123456789`}],keywords:n(`select from where and or not insert into values update set delete create table drop alter add column index primary key foreign references join left right inner outer full on as group by order having limit offset distinct union all null is in like between case when then else end exists returning with default unique check if begin commit rollback asc desc cascade view trigger`),types:n(`int integer bigint smallint serial text varchar char boolean bool date timestamp timestamptz json jsonb uuid real numeric float double decimal`),ci:!0},css:{rules:[{kind:`comment`,re:l,first:`/`},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`keyword`,re:/@[\w-]+/y,first:`@`},{kind:`prop`,re:/--?[a-zA-Z][\w-]*(?=\s*:)|[a-zA-Z][\w-]*(?=\s*:[^:{]*[;}\n])/y},{kind:`number`,re:/#[\da-fA-F]{3,8}\b|-?\d*\.?\d+(?:%|[a-zA-Z]{1,4})?/y}],keywords:n(`important inherit initial unset none auto`)},md:{rules:[{kind:`keyword`,re:/#{1,6}\s[^\n]*/y,at:`line`,first:`#`},{kind:`meta`,re:/(?:[-*+]|\d+[.)])(?=\s)/y,at:`line`,first:`-*+0123456789`},{kind:`string`,re:/`[^`\n]*`?/y,first:"`"},{kind:`type`,re:/\*\*[^*\n]+\*\*|__[^_\n]+__/y,first:`*_`},{kind:`attr`,re:/\[[^\]\n]*\]\([^)\n]*\)/y,first:`[`},{kind:`comment`,re:/>[^\n]*/y,at:`line`,first:`>`}]}};function p(e,t){for(let n=t-1;n>=0;n--){let t=e[n];if(t===`
2
+ `)return!0;if(t!==` `&&t!==` `&&t!==`-`)return!1}return!0}function m(e,t){for(let n=t-1;n>=0;n--){let t=e[n];if(t!==` `&&t!==` `)return t===`
3
+ `||t===`|`||t===`;`||t===`&`||t===`(`||t==="`"||t===`$`}return!0}var h=(e,t)=>t===0||/\s/.test(e[t-1]);function g(e,t,n){e.lastIndex=n;let r=e.exec(t);return r&&r[0]?r[0]:void 0}function _(e){let t=[];for(let[n,r]of e.split(`
4
+ `).entries()){if(n&&t.push({text:`
5
+ `}),!r)continue;let e=/^(@@|\+\+\+ |--- |diff |index )/.test(r)?`meta`:r[0]===`+`?`add`:r[0]===`-`?`del`:void 0;t.push(e?{text:r,kind:e}:{text:r})}return t}function v(e){let t=[],n=0;for(;n<e.length;){let r=g(/<!--[\s\S]*?(?:-->|$)/y,e,n);if(r){t.push({text:r,kind:`comment`}),n+=r.length;continue}let i=g(/<\/?[A-Za-z][\w:.-]*|<![A-Za-z]+/y,e,n);if(i)for(t.push({text:i,kind:`tag`}),n+=i.length;n<e.length;){let r=g(/\/?>/y,e,n);if(r){t.push({text:r,kind:`tag`}),n+=r.length;break}let i=g(/\s+/y,e,n),a=i?void 0:g(/[^\s=>/"']+/y,e,n),o=i||a?void 0:g(/"[^"]*"?|'[^']*'?/y,e,n);i?t.push({text:i}):a?t.push({text:a,kind:`attr`}):o?t.push({text:o,kind:`string`}):t.push({text:e[n]}),n+=(i??a??o??e[n]).length}else{let r=g(/[^<]+|</y,e,n);t.push({text:r}),n+=r.length}}return t}function y(e,t){let n=[],i=``,o=(e,t)=>{t?(i&&n.push({text:i}),i=``,n.push({text:e,kind:t})):i+=e},s=0;outer:for(;s<e.length;){let n=e[s];for(let r of t.rules){if(r.first&&!r.first.includes(n)||r.at===`line`&&!p(e,s)||r.at===`word`&&!h(e,s)||r.at===`command`&&!m(e,s))continue;let i=g(r.re,e,s);if(i){o(i,r.kind===`fn`&&t.keywords?.has(i)?`keyword`:r.kind),s+=i.length;continue outer}}if(r(n)){let n=a(e,s),r=t.ci?n.toLowerCase():n,i=e[s+n.length],c;t.keywords?.has(r)?c=`keyword`:t.types?.has(r)?c=`type`:(i===`(`||t.macros&&i===`!`&&e[s+n.length+1]!==`=`)&&(c=`fn`),o(n,c),s+=n.length}else i+=n,s++}return i&&n.push({text:i}),n}function b(e){let t=[];for(let n of e){let e=t.at(-1);e&&!e.kind&&!n.kind?e.text+=n.text:t.push({...n})}return t}function x(e,n){let r=String(e??``);if(!r)return[];let i=t(n);if(i===`diff`)return b(_(r));if(i===`html`)return b(v(r));let a=i?f[i]:void 0;return a?y(r,a):[{text:r}]}export{x as highlight,t as languageOf};