my-durable 0.0.0-stage → 0.0.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 (259) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -2
  4. package/ai_docs/durable_pi/announcement.md +860 -0
  5. package/ai_docs/durable_pi/changelog.md +51 -0
  6. package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
  7. package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
  8. package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
  9. package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
  10. package/ai_docs/durable_pi/readme.md +617 -0
  11. package/ai_docs/durable_pi/sources.md +22 -0
  12. package/ai_docs/durable_pi/spec.md +4696 -0
  13. package/ai_docs/durable_pi/vacation-planner.md +57 -0
  14. package/ai_docs/pi/chord.md +268 -0
  15. package/ai_docs/pi/cli-integration.md +110 -0
  16. package/ai_docs/pi/cli.md +325 -0
  17. package/ai_docs/pi/codemode.md +211 -0
  18. package/ai_docs/pi/compaction.md +467 -0
  19. package/ai_docs/pi/configuration.md +51 -0
  20. package/ai_docs/pi/containerization.md +187 -0
  21. package/ai_docs/pi/custom-provider.md +177 -0
  22. package/ai_docs/pi/environment-variables.md +102 -0
  23. package/ai_docs/pi/extensions.md +278 -0
  24. package/ai_docs/pi/how-pi-works.md +53 -0
  25. package/ai_docs/pi/index.md +43 -0
  26. package/ai_docs/pi/json.md +230 -0
  27. package/ai_docs/pi/keybindings.md +198 -0
  28. package/ai_docs/pi/llama-cpp.md +118 -0
  29. package/ai_docs/pi/mcp.md +274 -0
  30. package/ai_docs/pi/message-types.md +265 -0
  31. package/ai_docs/pi/models.md +209 -0
  32. package/ai_docs/pi/overview.md +166 -0
  33. package/ai_docs/pi/packages.md +133 -0
  34. package/ai_docs/pi/pi-agent-core.md +568 -0
  35. package/ai_docs/pi/pi-ai.md +1942 -0
  36. package/ai_docs/pi/prompt-templates.md +63 -0
  37. package/ai_docs/pi/providers.md +204 -0
  38. package/ai_docs/pi/quickstart.md +140 -0
  39. package/ai_docs/pi/readme.md +99 -0
  40. package/ai_docs/pi/rpc-commands.md +864 -0
  41. package/ai_docs/pi/rpc-extension-ui.md +204 -0
  42. package/ai_docs/pi/rpc.md +197 -0
  43. package/ai_docs/pi/sdk.md +151 -0
  44. package/ai_docs/pi/security.md +103 -0
  45. package/ai_docs/pi/session-format.md +297 -0
  46. package/ai_docs/pi/sessions.md +70 -0
  47. package/ai_docs/pi/settings.md +173 -0
  48. package/ai_docs/pi/shell-aliases.md +97 -0
  49. package/ai_docs/pi/skills.md +97 -0
  50. package/ai_docs/pi/slash-commands.md +64 -0
  51. package/ai_docs/pi/sources.md +57 -0
  52. package/ai_docs/pi/terminal-setup.md +223 -0
  53. package/ai_docs/pi/termux.md +121 -0
  54. package/ai_docs/pi/themes.md +140 -0
  55. package/ai_docs/pi/tmux.md +59 -0
  56. package/ai_docs/pi/toolkit.md +168 -0
  57. package/ai_docs/pi/tui.md +129 -0
  58. package/ai_docs/pi/usage.md +98 -0
  59. package/ai_docs/pi/virtual-models.md +118 -0
  60. package/ai_docs/pi/windows.md +71 -0
  61. package/apps/dpi-backend/.nvmrc +1 -0
  62. package/apps/dpi-backend/API.md +99 -0
  63. package/apps/dpi-backend/README.md +119 -0
  64. package/apps/dpi-backend/deploy/backend.env.example +9 -0
  65. package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
  66. package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
  67. package/apps/dpi-backend/package.json +33 -0
  68. package/apps/dpi-backend/prompts/one_sentence.md +3 -0
  69. package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
  70. package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
  71. package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
  72. package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
  73. package/apps/dpi-backend/scripts/token.ts +57 -0
  74. package/apps/dpi-backend/src/addons.ts +139 -0
  75. package/apps/dpi-backend/src/auth.ts +20 -0
  76. package/apps/dpi-backend/src/config.ts +54 -0
  77. package/apps/dpi-backend/src/contract-check.ts +118 -0
  78. package/apps/dpi-backend/src/errors.ts +50 -0
  79. package/apps/dpi-backend/src/extensions.ts +510 -0
  80. package/apps/dpi-backend/src/faux.ts +80 -0
  81. package/apps/dpi-backend/src/history.ts +165 -0
  82. package/apps/dpi-backend/src/idempotency.ts +20 -0
  83. package/apps/dpi-backend/src/jev.ts +43 -0
  84. package/apps/dpi-backend/src/jobs.ts +790 -0
  85. package/apps/dpi-backend/src/main.ts +76 -0
  86. package/apps/dpi-backend/src/model-catalog.ts +180 -0
  87. package/apps/dpi-backend/src/pairing-files.ts +142 -0
  88. package/apps/dpi-backend/src/pairing.ts +459 -0
  89. package/apps/dpi-backend/src/routes.ts +217 -0
  90. package/apps/dpi-backend/src/shared-state.ts +85 -0
  91. package/apps/dpi-backend/src/store.ts +1240 -0
  92. package/apps/dpi-backend/src/streams.ts +256 -0
  93. package/apps/dpi-backend/src/tunnel.ts +955 -0
  94. package/apps/dpi-backend/tsconfig.json +13 -0
  95. package/apps/dpi-playground/README.md +83 -0
  96. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
  97. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
  98. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
  99. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
  100. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
  101. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
  102. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
  103. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
  104. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
  105. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
  106. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
  107. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
  108. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
  109. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
  110. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
  111. package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
  112. package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
  113. package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
  114. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
  115. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
  116. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
  117. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
  118. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
  119. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
  120. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
  121. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
  122. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
  123. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
  124. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
  125. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
  126. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
  127. package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
  128. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
  129. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
  130. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
  131. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
  132. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
  133. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
  134. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
  135. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
  136. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
  137. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
  138. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
  139. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
  140. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
  141. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
  142. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
  143. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
  144. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
  145. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
  146. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
  147. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
  148. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
  149. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
  150. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
  151. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
  152. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
  153. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
  154. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
  155. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
  156. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
  157. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
  158. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
  159. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
  160. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
  161. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
  162. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
  163. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
  164. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
  165. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
  166. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
  167. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
  168. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
  169. package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
  170. package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
  171. package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
  172. package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
  173. package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
  174. package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
  175. package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
  176. package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
  177. package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
  178. package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
  179. package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
  180. package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
  181. package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
  182. package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
  183. package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
  184. package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
  185. package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
  186. package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
  187. package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
  188. package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
  189. package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
  190. package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
  191. package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
  192. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
  193. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
  194. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
  195. package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
  196. package/apps/dpi-playground/package.json +37 -0
  197. package/apps/dpi-sdk/README.md +74 -0
  198. package/apps/dpi-sdk/package.json +20 -0
  199. package/apps/dpi-sdk/scripts/net.ts +73 -0
  200. package/apps/dpi-sdk/src/api.ts +631 -0
  201. package/apps/dpi-sdk/src/app-state.ts +111 -0
  202. package/apps/dpi-sdk/src/client.ts +330 -0
  203. package/apps/dpi-sdk/src/errors.ts +16 -0
  204. package/apps/dpi-sdk/src/jobs.ts +99 -0
  205. package/apps/dpi-sdk/src/models.ts +472 -0
  206. package/apps/dpi-sdk/src/node/chunk.ts +488 -0
  207. package/apps/dpi-sdk/src/node/config.ts +175 -0
  208. package/apps/dpi-sdk/src/node/drivers.ts +402 -0
  209. package/apps/dpi-sdk/src/node/ops.ts +114 -0
  210. package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
  211. package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
  212. package/apps/dpi-sdk/src/node/registry.ts +390 -0
  213. package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
  214. package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
  215. package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
  216. package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
  217. package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
  218. package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
  219. package/apps/dpi-sdk/src/sse.ts +117 -0
  220. package/apps/dpi-sdk/src/transcript.ts +291 -0
  221. package/apps/dpi-sdk/tsconfig.json +13 -0
  222. package/apps/dpi-sdk/w +20 -0
  223. package/apps/dpi-tui/README.md +54 -0
  224. package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
  225. package/apps/dpi-tui/package.json +20 -0
  226. package/apps/dpi-tui/src/app.ts +433 -0
  227. package/apps/dpi-tui/src/commands.ts +151 -0
  228. package/apps/dpi-tui/src/main.ts +122 -0
  229. package/apps/dpi-tui/src/pairing.ts +287 -0
  230. package/apps/dpi-tui/src/pickers.ts +238 -0
  231. package/apps/dpi-tui/src/render.ts +164 -0
  232. package/apps/dpi-tui/src/theme.ts +57 -0
  233. package/apps/dpi-tui/tsconfig.json +14 -0
  234. package/bin/device.mjs +5 -0
  235. package/bin/hub.mjs +5 -0
  236. package/bin/tui.mjs +5 -0
  237. package/deploy/Caddyfile +120 -0
  238. package/docs/verification.md +102 -0
  239. package/dpi-network.example.yaml +62 -0
  240. package/images/01_durable_agent_network.svg +185 -0
  241. package/images/02_pi_made_durable.svg +204 -0
  242. package/images/03_restart_mid_tool.svg +168 -0
  243. package/images/07_one_api_every_client.svg +123 -0
  244. package/images/08_device_session_agent.svg +123 -0
  245. package/images/09_compact_keep_history.svg +143 -0
  246. package/images/10_shared_app_state.svg +177 -0
  247. package/images/11_durable_jobs_wait.svg +141 -0
  248. package/images/12_tools_change_mid_run.svg +231 -0
  249. package/images/13_hook_blocks_tool.svg +122 -0
  250. package/images/14_fork_at_any_entry.svg +119 -0
  251. package/images/15_web_client.png +0 -0
  252. package/images/16_terminal_client.svg +43 -0
  253. package/images/17_devices_are_data.svg +182 -0
  254. package/images/18_should_every_agent_be_durable.svg +118 -0
  255. package/images/19_sqlite_on_every_device.svg +155 -0
  256. package/images/20_admission_and_effects.svg +134 -0
  257. package/justfile +33 -0
  258. package/package.json +41 -4
  259. package/scripts/verify.mjs +210 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * P2-23 — typed admin pairing client for the browser.
3
+ *
4
+ * Contract (plan §P2-23):
5
+ * - Every admin pairing/rotation/revoke call is typed with the SDK DTOs.
6
+ * - Wizard enrollment state lives in ISOLATED per-session RAM (created via
7
+ * createPairingSession()); it never writes into the network/pairings
8
+ * metadata or the monitor six-status feed.
9
+ * - The browser only ever talks to hub /api endpoints — never to a device
10
+ * URL directly (the hub proxy injects device tokens; the browser holds none).
11
+ * - A closed dialog/tab aborts its polls; a stale session (superseded by a
12
+ * newer wizard) can never update anything.
13
+ * - HTTP 401 maps to a typed error (needsAuth) so the UI can ask for
14
+ * re-authentication without any redirect to a login screen.
15
+ * - No secret (code/token/hash) is ever logged or persisted here.
16
+ */
17
+ import { DpiError } from "../../../../../dpi-sdk/src/errors";
18
+ import type {
19
+ PairCodeIssue,
20
+ PairCodeState,
21
+ PairApproval,
22
+ PublicPairing,
23
+ } from "../../../../../dpi-sdk/src/api";
24
+
25
+ /** One hub REST call. 401 surfaces as DpiError(status=401) → needsAuth(). */
26
+ async function call<T>(
27
+ method: string,
28
+ path: string,
29
+ body?: unknown,
30
+ signal?: AbortSignal,
31
+ keepalive?: boolean,
32
+ ): Promise<T> {
33
+ const r = await fetch(`/api${path}`, {
34
+ method,
35
+ headers: body === undefined ? {} : { "content-type": "application/json" },
36
+ body: body === undefined ? undefined : JSON.stringify(body),
37
+ signal,
38
+ ...(keepalive ? { keepalive } : {}),
39
+ });
40
+ const data = (await r.json().catch(() => undefined)) as
41
+ | T
42
+ | { error?: { code?: string; message?: string } }
43
+ | undefined;
44
+ if (!r.ok) {
45
+ const error = (data as { error?: { code?: string; message?: string } } | undefined)?.error;
46
+ throw new DpiError(
47
+ (error?.code as never) ?? "internal",
48
+ error?.message ?? `${r.status} ${r.statusText}`,
49
+ r.status,
50
+ );
51
+ }
52
+ return data as T;
53
+ }
54
+
55
+ /** The only auth failure the browser treats as "authenticate again". */
56
+ export const needsAuth = (e: unknown): boolean => e instanceof DpiError && e.status === 401;
57
+
58
+ export type RevokeReason = "user" | "compromise";
59
+
60
+ export const pairingAdmin = {
61
+ pairings: (signal?: AbortSignal) => call<PublicPairing[]>("GET", "/pairings", undefined, signal),
62
+ /** Operator opens the wizard: mint a code (admin-gated upstream by Caddy). */
63
+ createCode: (signal?: AbortSignal) => call<PairCodeIssue>("POST", "/pair/code", {}, signal),
64
+ /** Resolve an existing handoff through a same-origin JSON body, never a code-bearing API URL. */
65
+ resolveCode: (code: string, signal?: AbortSignal) =>
66
+ call<PairCodeIssue>("POST", "/pair/resolve", { code }, signal),
67
+ /** Phase metadata only — the issued token is NEVER in this response. */
68
+ codeState: (codeId: string, signal?: AbortSignal) =>
69
+ call<PairCodeState>("GET", `/pair/codes/${encodeURIComponent(codeId)}`, undefined, signal),
70
+ /** Full-fingerprint approval (P-8/P-14): tuple must match the enrollment exactly. */
71
+ approve: (codeId: string, approval: PairApproval, signal?: AbortSignal) =>
72
+ call<PairCodeState>(
73
+ "POST",
74
+ `/pair/codes/${encodeURIComponent(codeId)}/approve`,
75
+ approval,
76
+ signal,
77
+ ),
78
+ /** `keepalive` lets the request outlive the page; use it only when the wizard is going away. */
79
+ cancel: (codeId: string, signal?: AbortSignal, keepalive?: boolean) =>
80
+ call<PairCodeState>(
81
+ "POST",
82
+ `/pair/codes/${encodeURIComponent(codeId)}/cancel`,
83
+ {},
84
+ signal,
85
+ keepalive,
86
+ ),
87
+ /** Ask the hub to rotate a device credential (202 — async via tunnel). */
88
+ rotate: (deviceId: string, signal?: AbortSignal) =>
89
+ call<{ ok: boolean }>("POST", `/pairings/${encodeURIComponent(deviceId)}/rotate`, {}, signal),
90
+ /** Durable revoke (P2-20): every hash dies at commit; live transport is cut. */
91
+ revoke: (deviceId: string, reason: RevokeReason = "user", signal?: AbortSignal) =>
92
+ call<{ ok: boolean }>(
93
+ "POST",
94
+ `/pairings/${encodeURIComponent(deviceId)}/revoke`,
95
+ { reason },
96
+ signal,
97
+ ),
98
+ };
99
+
100
+ export type PairPhase = PairCodeState["phase"];
101
+
102
+ /**
103
+ * Isolated wizard session. `gen` increments per session; a poll from an old
104
+ * session (stale closure) sees `closed` and stops without touching anything.
105
+ */
106
+ export interface PairingSession {
107
+ readonly gen: number;
108
+ /** Latest codeState snapshot (RAM only — never persisted). */
109
+ state?: PairCodeState;
110
+ readonly close: () => void;
111
+ readonly closed: () => boolean;
112
+ }
113
+
114
+ let sessionGen = 0;
115
+
116
+ export function createPairingSession(): PairingSession {
117
+ const gen = ++sessionGen;
118
+ let isClosed = false;
119
+ return {
120
+ gen,
121
+ close: () => (isClosed = true),
122
+ closed: () => isClosed,
123
+ };
124
+ }
125
+
126
+ export interface PollOptions {
127
+ signal?: AbortSignal;
128
+ /** Overall budget; the hub's own code TTL (5 min) bounds reality. */
129
+ timeoutMs?: number;
130
+ intervalMs?: number;
131
+ onState?: (state: PairCodeState) => void;
132
+ }
133
+
134
+ export class PollTimeout extends Error {
135
+ constructor(readonly codeId: string) {
136
+ super(`pairing poll timed out for ${codeId}`);
137
+ this.name = "PollTimeout";
138
+ }
139
+ }
140
+
141
+ const terminal = (phase: PairPhase) =>
142
+ phase === "issued" || phase === "cancelled" || phase === "expired";
143
+
144
+ /**
145
+ * Poll a code until it reaches a terminal phase. Aborting (signal or
146
+ * session.close()) stops cleanly; superseded sessions never write state.
147
+ */
148
+ export async function pollCode(
149
+ session: PairingSession,
150
+ codeId: string,
151
+ opts: PollOptions = {},
152
+ ): Promise<PairCodeState | undefined> {
153
+ const interval = opts.intervalMs ?? 1_000; // PAIR_POLL_MS cadence
154
+ const deadline = Date.now() + (opts.timeoutMs ?? 330_000);
155
+ for (;;) {
156
+ if (session.closed()) return undefined; // dialog/tab closed — stop silently
157
+ if (opts.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
158
+ if (Date.now() > deadline) throw new PollTimeout(codeId);
159
+ const state = await pairingAdmin.codeState(codeId, opts.signal);
160
+ if (session.closed()) return undefined; // closed while in flight — discard
161
+ session.state = state;
162
+ opts.onState?.(state);
163
+ if (terminal(state.phase)) return state;
164
+ await new Promise((r) => setTimeout(r, interval));
165
+ }
166
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Minimal local QR encoder for the PairDeviceWizard (P2-24: "QR render local;
3
+ * no remote QR service"). Byte mode, EC level L, versions 1–5 (pair URLs are
4
+ * ≤ ~106 bytes). Pure function: text → boolean[size][size] module matrix.
5
+ * Cross-validated against the reference `python-qrcode` implementation.
6
+ */
7
+
8
+ // -- GF(256), poly 0x11D ------------------------------------------------------
9
+
10
+ const EXP = new Uint8Array(512);
11
+ const LOG = new Uint8Array(256);
12
+ (() => {
13
+ let x = 1;
14
+ for (let i = 0; i < 255; i++) {
15
+ EXP[i] = x;
16
+ LOG[x] = i;
17
+ x <<= 1;
18
+ if (x & 0x100) x ^= 0x11d;
19
+ }
20
+ for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
21
+ })();
22
+ const gmul = (a: number, b: number) => (a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]]);
23
+
24
+ /** Generator polynomial, HIGH→low coefficients, monic (gen[0] = 1). */
25
+ function rsGenerator(degree: number): number[] {
26
+ let g = [1];
27
+ for (let i = 0; i < degree; i++) {
28
+ const next = new Array<number>(g.length + 1).fill(0);
29
+ for (let j = 0; j < g.length; j++) {
30
+ next[j] ^= g[j]; // multiply by x
31
+ next[j + 1] ^= gmul(g[j], EXP[i]); // multiply by α^i
32
+ }
33
+ g = next;
34
+ }
35
+ return g;
36
+ }
37
+
38
+ function rsEncode(data: number[], ecLen: number): number[] {
39
+ const gen = rsGenerator(ecLen);
40
+ const rem = new Array<number>(ecLen).fill(0);
41
+ for (const byte of data) {
42
+ const factor = byte ^ rem[0];
43
+ rem.shift();
44
+ rem.push(0);
45
+ // gen[0] is the x^ecLen term (cancels against the shifted-out lead); gen[i+1] is x^{ecLen-1-i}.
46
+ for (let i = 0; i < ecLen; i++) rem[i] ^= gmul(gen[i + 1], factor);
47
+ }
48
+ return rem;
49
+ }
50
+
51
+ // -- version tables (EC level L, single data block for v1–5) -------------------
52
+
53
+ const SIZES = [21, 25, 29, 33, 37];
54
+ const DATA_CODEWORDS = [19, 34, 55, 80, 108];
55
+ const EC_PER_BLOCK = [7, 10, 15, 20, 26];
56
+ const ALIGN = [[], [6, 18], [6, 22], [6, 26], [6, 30]]; // alignment centers
57
+ const REMAINDER_BITS = [0, 7, 7, 7, 7];
58
+
59
+ function selectVersion(byteLen: number): number {
60
+ for (let v = 1; v <= 5; v++) {
61
+ // byte mode: 4-bit indicator + 8-bit count (v1–9) + payload
62
+ if (byteLen + 2 <= DATA_CODEWORDS[v - 1]) return v;
63
+ }
64
+ throw new Error(`qr: payload too long (${byteLen} bytes; v5L max ${DATA_CODEWORDS[4] - 2})`);
65
+ }
66
+
67
+ // -- bit stream ---------------------------------------------------------------
68
+
69
+ function buildCodewords(text: string, version: number): number[] {
70
+ const bytes = [...new TextEncoder().encode(text)];
71
+ const bits: number[] = [];
72
+ const push = (value: number, len: number) => {
73
+ for (let i = len - 1; i >= 0; i--) bits.push((value >> i) & 1);
74
+ };
75
+ push(0b0100, 4); // byte mode
76
+ push(bytes.length, 8);
77
+ for (const b of bytes) push(b, 8);
78
+ const dataCodewords = DATA_CODEWORDS[version - 1];
79
+ const capacityBits = dataCodewords * 8;
80
+ push(0, Math.min(4, capacityBits - bits.length)); // terminator
81
+ while (bits.length % 8 !== 0) bits.push(0);
82
+ const padBytes = [0xec, 0x11];
83
+ let pi = 0;
84
+ while (bits.length < capacityBits) push(padBytes[pi++ % 2], 8);
85
+ const out: number[] = [];
86
+ for (let i = 0; i < dataCodewords; i++) {
87
+ let byte = 0;
88
+ for (let j = 0; j < 8; j++) byte = (byte << 1) | bits[i * 8 + j];
89
+ out.push(byte);
90
+ }
91
+ return out;
92
+ }
93
+
94
+ // -- matrix scaffolding ---------------------------------------------------------
95
+
96
+ function makeMatrix(size: number): boolean[][] {
97
+ return Array.from({ length: size }, () => new Array<boolean>(size).fill(false));
98
+ }
99
+
100
+ function placeFunctionPatterns(m: boolean[][], version: number): void {
101
+ const size = SIZES[version - 1];
102
+ const finder = (row: number, col: number) => {
103
+ for (let r = -1; r <= 7; r++)
104
+ for (let c = -1; c <= 7; c++) {
105
+ const rr = row + r;
106
+ const cc = col + c;
107
+ if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue;
108
+ const inRing =
109
+ r >= 0 && r <= 6 && c >= 0 && c <= 6 && (r === 0 || r === 6 || c === 0 || c === 6);
110
+ const inCore = r >= 2 && r <= 4 && c >= 2 && c <= 4;
111
+ m[rr][cc] = inRing || inCore;
112
+ }
113
+ };
114
+ finder(0, 0);
115
+ finder(0, size - 7);
116
+ finder(size - 7, 0);
117
+ // timing patterns
118
+ for (let i = 8; i < size - 8; i++) {
119
+ m[6][i] = i % 2 === 0;
120
+ m[i][6] = i % 2 === 0;
121
+ }
122
+ // alignment patterns (skip those overlapping finders)
123
+ const centers = ALIGN[version - 1];
124
+ for (const r of centers)
125
+ for (const c of centers) {
126
+ if ((r <= 8 && c <= 8) || (r <= 8 && c >= size - 9) || (r >= size - 9 && c <= 8)) continue;
127
+ for (let rr = -2; rr <= 2; rr++)
128
+ for (let cc = -2; cc <= 2; cc++)
129
+ m[r + rr][c + cc] = Math.max(Math.abs(rr), Math.abs(cc)) !== 1;
130
+ }
131
+ // dark module
132
+ m[size - 8][8] = true;
133
+ }
134
+
135
+ // format info: EC level L (01) + mask, BCH(15,5), XOR 0x5412
136
+ function formatBits(mask: number): boolean[] {
137
+ const data = (0b01 << 3) | mask;
138
+ let rem = data;
139
+ for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >> 9) & 1 ? 0x537 : 0);
140
+ const bits15 = ((data << 10) | rem) ^ 0x5412;
141
+ const out: boolean[] = [];
142
+ for (let i = 14; i >= 0; i--) out.push(((bits15 >> i) & 1) === 1);
143
+ return out;
144
+ }
145
+
146
+ function placeFormat(m: boolean[][], mask: number): void {
147
+ const size = SIZES[0] * 0 + m.length;
148
+ const bits = formatBits(mask);
149
+ // around top-left finder
150
+ for (let i = 0; i <= 5; i++) m[8][i] = bits[i];
151
+ m[8][7] = bits[6];
152
+ m[8][8] = bits[7];
153
+ m[7][8] = bits[8];
154
+ for (let i = 9; i < 15; i++) m[14 - i][8] = bits[i];
155
+ // second copy: bottom-left (col 8, rows size-7..size-1) = bits 6..0; the
156
+ // dark module at (size-8, 8) is NOT part of the format string.
157
+ for (let i = 0; i < 7; i++) m[size - 1 - i][8] = bits[i];
158
+ m[8][size - 8] = bits[7];
159
+ // top-right (row 8, cols size-7..size-1) = bits 8..14 left→right
160
+ for (let i = 8; i < 15; i++) m[8][size - 15 + i] = bits[i];
161
+ // the dark module cell (size-8,8) is NOT part of format
162
+ m[size - 8][8] = true;
163
+ }
164
+
165
+ function placeData(m: boolean[][], codewords: number[], version: number, mask: number): void {
166
+ const size = m.length;
167
+ const total = SIZES[version - 1] * SIZES[version - 1];
168
+ const bitAt = (i: number): boolean => {
169
+ const cw = codewords[i >> 3] ?? 0;
170
+ return ((cw >> (7 - (i & 7))) & 1) === 1;
171
+ };
172
+ let bitIndex = 0;
173
+ let upward = true;
174
+ for (let col = size - 1; col > 0; col -= 2) {
175
+ if (col === 6) col--; // skip timing column
176
+ for (let step = 0; step < size; step++) {
177
+ const row = upward ? size - 1 - step : step;
178
+ for (const c of [col, col - 1]) {
179
+ if (isFunctionModule(row, c, version)) continue;
180
+ if (bitIndex >= (DATA_CODEWORDS[version - 1] + EC_PER_BLOCK[version - 1]) * 8) {
181
+ // past data+ec bits → remainder modules (decoders ignore them)
182
+ bitIndex++;
183
+ continue;
184
+ }
185
+ let bit = bitAt(bitIndex++);
186
+ if (maskModule(row, c, mask)) bit = !bit;
187
+ m[row][c] = bit;
188
+ }
189
+ }
190
+ upward = !upward;
191
+ }
192
+ }
193
+
194
+ function maskModule(row: number, col: number, mask: number): boolean {
195
+ switch (mask) {
196
+ case 0:
197
+ return (row + col) % 2 === 0;
198
+ case 1:
199
+ return row % 2 === 0;
200
+ case 2:
201
+ return col % 3 === 0;
202
+ case 3:
203
+ return (row + col) % 3 === 0;
204
+ case 4:
205
+ return (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0;
206
+ case 5:
207
+ return ((row * col) % 2) + ((row * col) % 3) === 0;
208
+ case 6:
209
+ return (((row * col) % 2) + ((row * col) % 3)) % 2 === 0;
210
+ default:
211
+ return (((row + col) % 2) + ((row * col) % 3)) % 2 === 0;
212
+ }
213
+ }
214
+
215
+ function isFunctionModule(row: number, col: number, version: number): boolean {
216
+ const size = SIZES[version - 1];
217
+ // finders + separators
218
+ if (row <= 8 && col <= 8) return true;
219
+ if (row <= 8 && col >= size - 8) return true;
220
+ if (row >= size - 8 && col <= 8) return true;
221
+ // timing
222
+ if (row === 6 || col === 6) return true;
223
+ // alignment
224
+ const centers = ALIGN[version - 1];
225
+ for (const r of centers)
226
+ for (const c of centers) {
227
+ if ((r <= 8 && c <= 8) || (r <= 8 && c >= size - 9) || (r >= size - 9 && c <= 8)) continue;
228
+ if (Math.abs(row - r) <= 2 && Math.abs(col - c) <= 2) return true;
229
+ }
230
+ return false;
231
+ }
232
+
233
+ // -- penalty scoring -------------------------------------------------------------
234
+
235
+ function penalty(m: boolean[][]): number {
236
+ const size = m.length;
237
+ let score = 0;
238
+ // N1: runs ≥5 same colour in rows/cols
239
+ for (let axis = 0; axis < 2; axis++)
240
+ for (let i = 0; i < size; i++) {
241
+ let run = 1;
242
+ for (let j = 1; j < size; j++) {
243
+ const cur = axis === 0 ? m[i][j] : m[j][i];
244
+ const prev = axis === 0 ? m[i][j - 1] : m[j - 1][i];
245
+ if (cur === prev) {
246
+ run++;
247
+ if (j === size - 1 && run >= 5) score += 3 + (run - 5);
248
+ } else {
249
+ if (run >= 5) score += 3 + (run - 5);
250
+ run = 1;
251
+ }
252
+ }
253
+ }
254
+ // N2: 2×2 blocks
255
+ for (let r = 0; r < size - 1; r++)
256
+ for (let c = 0; c < size - 1; c++) {
257
+ const v = m[r][c];
258
+ if (m[r][c + 1] === v && m[r + 1][c] === v && m[r + 1][c + 1] === v) score += 3;
259
+ }
260
+ // N3: finder-like 1:1:3:1:1 patterns (simplified: exact pattern with quiet edges)
261
+ const pat = [true, false, true, true, true, false, true, false, false, false, false];
262
+ const matches = (get: (i: number) => boolean, len: number) => {
263
+ for (let start = 0; start + 11 <= len; start++) {
264
+ let ok = true;
265
+ for (let k = 0; k < 11; k++)
266
+ if (get(start + k) !== pat[k]) {
267
+ ok = false;
268
+ break;
269
+ }
270
+ if (ok) score += 40;
271
+ }
272
+ };
273
+ for (let r = 0; r < size; r++) matches((c) => m[r][c], size);
274
+ for (let c = 0; c < size; c++) matches((r) => m[r][c], size);
275
+ // N4: dark ratio
276
+ let dark = 0;
277
+ for (const row of m) for (const v of row) if (v) dark++;
278
+ const pct = (dark * 100) / (size * size);
279
+ score += Math.floor(Math.abs(pct - 50) / 5) * 10;
280
+ return score;
281
+ }
282
+
283
+ // -- public API -------------------------------------------------------------------
284
+
285
+ function matrixWithMask(text: string, mask: number): boolean[][] {
286
+ const version = selectVersion(new TextEncoder().encode(text).length);
287
+ const size = SIZES[version - 1];
288
+ const data = buildCodewords(text, version);
289
+ const ec = rsEncode(data, EC_PER_BLOCK[version - 1]);
290
+ const all = [...data, ...ec];
291
+ const m = makeMatrix(size);
292
+ placeFunctionPatterns(m, version);
293
+ placeData(m, all, version, mask);
294
+ placeFormat(m, mask);
295
+ return m;
296
+ }
297
+
298
+ /** Encode text into a QR matrix (true = dark module), EC level L, versions 1–5. */
299
+ export function qrMatrix(text: string, maskOverride?: number): boolean[][] {
300
+ if (maskOverride !== undefined) return matrixWithMask(text, maskOverride);
301
+ let best: { m: boolean[][]; score: number } | undefined;
302
+ for (let mask = 0; mask < 8; mask++) {
303
+ const m = matrixWithMask(text, mask);
304
+ const score = penalty(m);
305
+ if (!best || score < best.score) best = { m, score };
306
+ }
307
+ return best!.m;
308
+ }
309
+
310
+ /** Render the matrix as an SVG string (quiet zone 4 modules, black on white). */
311
+ export function qrSvg(text: string, pixelSize = 160): string {
312
+ const m = qrMatrix(text);
313
+ const n = m.length;
314
+ const quiet = 4;
315
+ const total = n + quiet * 2;
316
+ const cell = pixelSize / total;
317
+ let rects = "";
318
+ for (let r = 0; r < n; r++)
319
+ for (let c = 0; c < n; c++)
320
+ if (m[r][c])
321
+ rects += `<rect x="${((c + quiet) * cell).toFixed(2)}" y="${((r + quiet) * cell).toFixed(2)}" width="${cell.toFixed(3)}" height="${cell.toFixed(3)}"/>`;
322
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${pixelSize} ${pixelSize}" shape-rendering="crispEdges"><rect width="${pixelSize}" height="${pixelSize}" fill="#fff"/><g fill="#000">${rects}</g></svg>`;
323
+ }
@@ -0,0 +1,146 @@
1
+ // A tool's JSON Schema as a TypeScript function declaration, already split into highlight tokens.
2
+ // We generate the code, so we know every token's kind without a highlighter.
3
+
4
+ export type TokenKind =
5
+ | "keyword"
6
+ | "function"
7
+ | "property"
8
+ | "type"
9
+ | "string"
10
+ | "number"
11
+ | "comment"
12
+ | "punct"
13
+ | "text";
14
+ export interface Token {
15
+ text: string;
16
+ kind: TokenKind;
17
+ }
18
+
19
+ interface Schema {
20
+ type?: string | string[];
21
+ properties?: Record<string, Schema>;
22
+ required?: string[];
23
+ additionalProperties?: boolean | Schema;
24
+ items?: Schema;
25
+ anyOf?: Schema[];
26
+ oneOf?: Schema[];
27
+ enum?: unknown[];
28
+ const?: unknown;
29
+ description?: string;
30
+ default?: unknown;
31
+ minimum?: number;
32
+ maximum?: number;
33
+ }
34
+
35
+ type Emit = (text: string, kind?: TokenKind) => void;
36
+ const IDENT = /^[A-Za-z_$][\w$]*$/;
37
+ const indent = (depth: number) => " ".repeat(depth);
38
+
39
+ export function toolSignature(name: string, parameters: unknown): Token[] {
40
+ const out: Token[] = [];
41
+ const emit: Emit = (text, kind = "text") => out.push({ text, kind });
42
+ emit("function", "keyword");
43
+ emit(" ");
44
+ emit(name, "function");
45
+ emit("(", "punct");
46
+ emit("args");
47
+ emit(": ", "punct");
48
+ type(parameters as Schema, 0, emit);
49
+ emit("): ", "punct");
50
+ emit("Promise", "type");
51
+ emit("<", "punct");
52
+ emit("ToolResult", "type");
53
+ emit(">;", "punct");
54
+ return out;
55
+ }
56
+
57
+ function type(s: Schema | undefined, depth: number, emit: Emit) {
58
+ if (!s || typeof s !== "object") return emit("unknown", "type");
59
+ if ("const" in s) return literal(s.const, emit);
60
+ if (s.enum)
61
+ return union(
62
+ s.enum.map((value) => ({ const: value })),
63
+ depth,
64
+ emit,
65
+ );
66
+ if (s.anyOf ?? s.oneOf) return union((s.anyOf ?? s.oneOf)!, depth, emit);
67
+ if (Array.isArray(s.type))
68
+ return union(
69
+ s.type.map((t) => ({ ...s, type: t })),
70
+ depth,
71
+ emit,
72
+ );
73
+ switch (s.type) {
74
+ case "string":
75
+ case "boolean":
76
+ case "null":
77
+ return emit(s.type, "type");
78
+ case "number":
79
+ case "integer":
80
+ return emit("number", "type");
81
+ case "array": {
82
+ const grouped = !!(s.items?.anyOf ?? s.items?.oneOf ?? s.items?.enum);
83
+ if (grouped) emit("(", "punct");
84
+ type(s.items, depth, emit);
85
+ if (grouped) emit(")", "punct");
86
+ return emit("[]", "punct");
87
+ }
88
+ case "object":
89
+ return object(s, depth, emit);
90
+ default:
91
+ return emit("unknown", "type");
92
+ }
93
+ }
94
+
95
+ function object(s: Schema, depth: number, emit: Emit) {
96
+ const props = Object.entries(s.properties ?? {});
97
+ if (!props.length) {
98
+ if (s.additionalProperties && typeof s.additionalProperties === "object") {
99
+ emit("Record", "type");
100
+ emit("<", "punct");
101
+ emit("string", "type");
102
+ emit(", ", "punct");
103
+ type(s.additionalProperties, depth, emit);
104
+ return emit(">", "punct");
105
+ }
106
+ return emit("{}", "punct");
107
+ }
108
+ const required = new Set(s.required ?? []);
109
+ emit("{", "punct");
110
+ emit("\n");
111
+ for (const [key, prop] of props) {
112
+ const doc = docComment(prop);
113
+ if (doc) emit(`${indent(depth + 1)}${doc}\n`, "comment");
114
+ emit(indent(depth + 1));
115
+ emit(IDENT.test(key) ? key : JSON.stringify(key), "property");
116
+ emit(required.has(key) ? ": " : "?: ", "punct");
117
+ type(prop, depth + 1, emit);
118
+ emit(";", "punct");
119
+ emit("\n");
120
+ }
121
+ emit(indent(depth));
122
+ emit("}", "punct");
123
+ }
124
+
125
+ function union(variants: Schema[], depth: number, emit: Emit) {
126
+ variants.forEach((variant, i) => {
127
+ if (i) emit(" | ", "punct");
128
+ type(variant, depth, emit);
129
+ });
130
+ }
131
+
132
+ function literal(value: unknown, emit: Emit) {
133
+ if (typeof value === "string") return emit(JSON.stringify(value), "string");
134
+ if (typeof value === "number") return emit(String(value), "number");
135
+ emit(String(value), "type");
136
+ }
137
+
138
+ function docComment(s: Schema) {
139
+ const parts = [
140
+ s.description,
141
+ s.minimum !== undefined ? `@minimum ${s.minimum}` : "",
142
+ s.maximum !== undefined ? `@maximum ${s.maximum}` : "",
143
+ s.default !== undefined ? `@default ${JSON.stringify(s.default)}` : "",
144
+ ].filter(Boolean);
145
+ return parts.length ? `/** ${parts.join(" ")} */` : "";
146
+ }