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,76 @@
1
+ // dpi-backend: one device of the Durable Agent Network.
2
+ import express from "express";
3
+ import { assertLimitsConfig } from "../../dpi-sdk/src/protocol/limits.js";
4
+ import { createServer } from "node:http";
5
+ import { networkInterfaces } from "node:os";
6
+ import { loadConfig } from "./config.js";
7
+ import { Store } from "./store.js";
8
+ import { bearerAuth } from "./auth.js";
9
+ import { errorMiddleware } from "./errors.js";
10
+ import { v1 } from "./routes.js";
11
+ import { DevicePairing } from "./pairing.js";
12
+ import { DeviceConnector, readCaPem } from "./tunnel.js";
13
+
14
+ assertLimitsConfig();
15
+
16
+ const config = loadConfig();
17
+ const store = await Store.open({
18
+ dataDir: config.dataDir,
19
+ startDirs: config.startDirs,
20
+ name: config.name,
21
+ provider: config.provider,
22
+ });
23
+
24
+ const app = express();
25
+ app.disable("x-powered-by");
26
+ app.get("/healthz", (_req, res) => res.json({ ok: true }));
27
+ const pairing = new DevicePairing(store, config.home, config.name, process.env.DPI_HUB_URL, {
28
+ // Forced hub change (P-5): once the new pairing is persisted the OLD
29
+ // connector is stopped — never two live tunnels for one device, and the old
30
+ // hub keeps its tunnel until a replacement exists.
31
+ stopOldConnector: () => connector.stop("re-pairing: hub changed"),
32
+ // Pairing success (P2-15/20 glue, F05): adopt the freshly persisted pairing
33
+ // file — reload() swaps the RUNNING connector onto the new epoch instead of
34
+ // no-op'ing with stale RAM credential/tuple.
35
+ onPaired: () => connector.reload("re-paired"),
36
+ });
37
+ // One connector per daemon, from the persisted pairing file (P2-13). DPI_HUB_CA
38
+ // narrows TLS trust for the reference profile; verification is never disabled.
39
+ const connector = new DeviceConnector(
40
+ config.home,
41
+ readCaPem(process.env.DPI_HUB_CA),
42
+ {
43
+ // The local pairing view follows what the hub did to this pairing.
44
+ onRevoked: (epoch, reason) => pairing.markRevoked(reason, epoch),
45
+ onRotated: (file) => pairing.markRotated(file),
46
+ },
47
+ { baseUrl: `http://${config.host === "0.0.0.0" ? "127.0.0.1" : config.host}:${config.port}`, token: config.token },
48
+ );
49
+ connector.start();
50
+ app.use("/v1", bearerAuth(config.token), express.json({ limit: "256kb" }), v1(store, pairing, connector));
51
+ app.use((_req, res) => res.status(404).json({ error: { code: "not_found", message: "Route not found" } }));
52
+ app.use(errorMiddleware);
53
+
54
+ const server = createServer(app);
55
+ server.keepAliveTimeout = 65_000;
56
+ server.listen(config.port, config.host, () => {
57
+ console.log(`dpi-backend "${config.name}" (uid ${store.uid}) on http://${config.host}:${config.port}`);
58
+ console.log(`data: ${config.dataDir} start dirs: ${config.startDirs.join(", ")} provider: ${config.provider}`);
59
+ if (config.host === "0.0.0.0")
60
+ for (const addresses of Object.values(networkInterfaces()))
61
+ for (const a of addresses ?? [])
62
+ if (a.family === "IPv4" && !a.internal) console.log(`LAN: http://${a.address}:${config.port}`);
63
+ });
64
+
65
+ let closing = false;
66
+ const shutdown = async () => {
67
+ if (closing) return;
68
+ closing = true;
69
+ connector.stop("backend shutdown"); // readers/pending ops before storage closes
70
+ server.close();
71
+ server.closeAllConnections();
72
+ await store.close();
73
+ process.exit(0);
74
+ };
75
+ process.on("SIGTERM", () => void shutdown());
76
+ process.on("SIGINT", () => void shutdown());
@@ -0,0 +1,180 @@
1
+ // Source: https://agenticengineer.com/model-stack, retrieved 2026-10-05.
2
+ // Active S+, S, A and B models, matched to https://openrouter.ai/api/v1/models.
3
+ // Gemini 4 Argon is rolling out and has no OpenRouter listing at this snapshot.
4
+ export const curatedModels = [
5
+ {
6
+ id: "anthropic/claude-opus-5.5",
7
+ name: "Claude Opus 5.5",
8
+ tier: "S+",
9
+ icon: "anthropic.png",
10
+ },
11
+ {
12
+ id: "anthropic/claude-sonnet-5.5",
13
+ name: "Claude Sonnet 5.5",
14
+ tier: "S+",
15
+ icon: "anthropic.png",
16
+ },
17
+ {
18
+ id: "anthropic/claude-fable-5.1",
19
+ name: "Claude Fable 5.1",
20
+ tier: "S+",
21
+ icon: "anthropic.png",
22
+ },
23
+ {
24
+ id: "openai/gpt-6-astra",
25
+ name: "GPT-6 Astra",
26
+ tier: "S+",
27
+ icon: "openai.png",
28
+ },
29
+ {
30
+ id: "anthropic/claude-opus-5",
31
+ name: "Opus 5",
32
+ tier: "S+",
33
+ icon: "anthropic.png",
34
+ },
35
+ {
36
+ id: "openai/gpt-6.1-sol",
37
+ name: "GPT-6.1 Sol",
38
+ tier: "S+",
39
+ icon: "openai.png",
40
+ },
41
+ {
42
+ id: "anthropic/claude-fable-5",
43
+ name: "Fable 5",
44
+ tier: "S",
45
+ icon: "anthropic.png",
46
+ },
47
+ {
48
+ id: "meta/muse-spark-1.3",
49
+ name: "Muse Spark 1.3",
50
+ tier: "S",
51
+ icon: "meta.png",
52
+ },
53
+ {
54
+ id: "openai/gpt-6-sol",
55
+ name: "GPT-6 Sol",
56
+ tier: "S",
57
+ icon: "openai.png",
58
+ },
59
+ {
60
+ id: "x-ai/grok-4.7",
61
+ name: "Grok 4.7",
62
+ tier: "S",
63
+ icon: "grok.png",
64
+ },
65
+ {
66
+ id: "xiaomi/mimo-v2.6-pro",
67
+ name: "MiMo-V2.6-Pro",
68
+ tier: "S",
69
+ icon: "xiaomi.png",
70
+ },
71
+ {
72
+ id: "x-ai/grok-4.6",
73
+ name: "Grok 4.6",
74
+ tier: "S",
75
+ icon: "grok.png",
76
+ },
77
+ {
78
+ id: "z-ai/glm-5.3",
79
+ name: "GLM-5.3",
80
+ tier: "A",
81
+ icon: "zai.png",
82
+ },
83
+ {
84
+ id: "moonshotai/kimi-k3",
85
+ name: "Kimi K3",
86
+ tier: "A",
87
+ icon: "kimi.png",
88
+ },
89
+ {
90
+ id: "openai/gpt-5.6-terra",
91
+ name: "GPT 5.6 Terra",
92
+ tier: "A",
93
+ icon: "openai.png",
94
+ },
95
+ {
96
+ id: "google/gemini-3.8-flash",
97
+ name: "Gemini 3.8 Flash",
98
+ tier: "A",
99
+ icon: "gemini.png",
100
+ },
101
+ {
102
+ id: "qwen/qwen3.8-2.4t-a95b",
103
+ name: "Qwen3.8-2.4T-A95B",
104
+ tier: "A",
105
+ icon: "qwen.png",
106
+ },
107
+ {
108
+ id: "deepseek/deepseek-v4.1-flash",
109
+ name: "DeepSeek V4.1 Flash",
110
+ tier: "A",
111
+ icon: "deepseek.png",
112
+ },
113
+ {
114
+ id: "google/gemini-3.7-flash",
115
+ name: "Gemini 3.7 Flash",
116
+ tier: "A",
117
+ icon: "gemini.png",
118
+ },
119
+ {
120
+ id: "anthropic/claude-sonnet-5",
121
+ name: "Sonnet 5",
122
+ tier: "A",
123
+ icon: "anthropic.png",
124
+ },
125
+ {
126
+ id: "openai/gpt-6-luna",
127
+ name: "GPT-6 Luna",
128
+ tier: "A",
129
+ icon: "openai.png",
130
+ },
131
+ {
132
+ id: "xiaomi/mimo-v2.6-flash",
133
+ name: "MiMo-V2.6-Flash",
134
+ tier: "A",
135
+ icon: "xiaomi.png",
136
+ },
137
+ {
138
+ id: "openai/gpt-5.6-luna",
139
+ name: "GPT 5.6 Luna",
140
+ tier: "A",
141
+ icon: "openai.png",
142
+ },
143
+ {
144
+ id: "deepseek/deepseek-v4-pro-0813",
145
+ name: "DeepSeek V4 Pro 0813",
146
+ tier: "A",
147
+ icon: "deepseek.png",
148
+ },
149
+ {
150
+ id: "z-ai/glm-5.2",
151
+ name: "GLM-5.2",
152
+ tier: "A",
153
+ icon: "zai.png",
154
+ },
155
+ {
156
+ id: "z-ai/glm-5.3-flash",
157
+ name: "GLM-5.3-Flash",
158
+ tier: "B",
159
+ icon: "zai.png",
160
+ },
161
+ {
162
+ id: "google/gemini-3.1-pro-preview",
163
+ name: "Gemini 3.1 Pro",
164
+ tier: "B",
165
+ icon: "gemini.png",
166
+ },
167
+ {
168
+ id: "minimax/minimax-m3",
169
+ name: "MiniMax-M3",
170
+ tier: "B",
171
+ icon: "minimax.png",
172
+ },
173
+ {
174
+ id: "google/gemini-3.5-flash-lite",
175
+ name: "Gemini 3.5 Flash-Lite",
176
+ tier: "B",
177
+ icon: "gemini.png",
178
+ },
179
+ ] as const;
180
+ export const defaultModelId = "anthropic/claude-sonnet-5.5";
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Atomic secret-file primitives for the device pairing state (spec 03 §7).
3
+ *
4
+ * Order is normative: temp file 0600 in the SAME directory → write + fsync →
5
+ * rename → fsync the directory. A file is only reported as persisted after
6
+ * every step. Callers must never ACK or use a credential before writeAtomic
7
+ * resolves, and async latecomers must re-check the active operation before
8
+ * committing (see pairing.ts).
9
+ */
10
+ import { chmodSync, closeSync, fsyncSync, openSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { randomBytes } from "node:crypto";
13
+ import { ensurePrivateDir } from "../../dpi-sdk/src/node/private-dir.js";
14
+
15
+ export { ensurePrivateDir };
16
+
17
+ /** Write `contents` to `path` atomically (temp → fsync → rename → dir fsync). */
18
+ export function writeAtomic(path: string, contents: string): void {
19
+ ensurePrivateDir(dirname(path));
20
+ replaceFileAtomic(path, contents);
21
+ }
22
+
23
+ /** The write itself, for callers that have already judged the directory (and must not have it chmod'ed). */
24
+ export function replaceFileAtomic(path: string, contents: string): void {
25
+ const dir = dirname(path);
26
+ const temp = join(dir, `.${basename(path)}.tmp-${randomBytes(6).toString("hex")}`);
27
+ let fd: number | undefined;
28
+ try {
29
+ fd = openSync(temp, "w", 0o600);
30
+ writeSync(fd, Buffer.from(contents, "utf8"));
31
+ fsyncSync(fd);
32
+ closeSync(fd);
33
+ fd = undefined;
34
+ renameSync(temp, path);
35
+ // fsync the directory so the rename itself is durable.
36
+ const dirFd = openSync(dir, "r");
37
+ try {
38
+ fsyncSync(dirFd);
39
+ } finally {
40
+ closeSync(dirFd);
41
+ }
42
+ } catch (e) {
43
+ if (fd !== undefined) {
44
+ try {
45
+ closeSync(fd);
46
+ } catch {}
47
+ }
48
+ try {
49
+ unlinkSync(temp); // cleanup ONLY the temp this function created
50
+ } catch {}
51
+ throw e;
52
+ }
53
+ }
54
+
55
+ const basename = (p: string) => p.split(/[\\/]/).pop() ?? p;
56
+
57
+ /** Exactly the names `replaceFileAtomic` gives its temp files for the two state files in the home directory. */
58
+ const STATE_TEMP = /^\.(?:pairing|device-keypair)\.json\.tmp-[0-9a-f]{12}$/;
59
+
60
+ /**
61
+ * A writer killed between creating its temp file and renaming it leaves that file behind, holding a credential.
62
+ * Run before anything in this process writes: it must not race a live writer.
63
+ */
64
+ export function removeStaleTemps(dir: string): void {
65
+ for (const name of readdirSync(dir)) {
66
+ if (!STATE_TEMP.test(name)) continue;
67
+ const path = join(dir, name);
68
+ try {
69
+ unlinkSync(path); // never follows a symlink; refuses a directory
70
+ } catch {
71
+ /* raced away or not removable: the next start retries */
72
+ }
73
+ }
74
+ }
75
+
76
+ /** Tolerant JSON load for state files: malformed → undefined (caller decides). */
77
+ export function loadJson<T>(path: string): T | undefined {
78
+ try {
79
+ return JSON.parse(readFileSync(path, "utf8")) as T;
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * The device key, or undefined when there is none yet. Anything else that keeps it from being read (permissions,
87
+ * I/O error, truncated or damaged contents) throws: the caller must not answer that by minting a new identity
88
+ * over the old file.
89
+ */
90
+ export function readKeypairFile(path: string): DeviceKeypair | undefined {
91
+ let raw: string;
92
+ try {
93
+ raw = readFileSync(path, "utf8");
94
+ } catch (e) {
95
+ if ((e as NodeJS.ErrnoException).code === "ENOENT") return undefined;
96
+ throw keypairUnusable(path, (e as Error).message);
97
+ }
98
+ let parsed: Partial<DeviceKeypair> | null;
99
+ try {
100
+ parsed = JSON.parse(raw) as Partial<DeviceKeypair> | null;
101
+ } catch {
102
+ throw keypairUnusable(path, "not valid JSON");
103
+ }
104
+ if (!parsed?.privateKeyPem || !parsed.publicKeyPem || !parsed.fingerprint) throw keypairUnusable(path, "key fields are missing");
105
+ return parsed as DeviceKeypair;
106
+ }
107
+
108
+ const keypairUnusable = (path: string, why: string) =>
109
+ new Error(`device keypair ${path} is unusable (${why}); refusing to replace it. Restore it from a backup, or move it away to enroll this device again.`);
110
+
111
+ /** Tighten a file that must hold secrets if a copy/restore loosened it. */
112
+ export function ensureFileMode0600(path: string): void {
113
+ try {
114
+ if ((statSync(path).mode & 0o777) !== 0o600) chmodSync(path, 0o600);
115
+ } catch {
116
+ /* file may not exist yet */
117
+ }
118
+ }
119
+
120
+ export interface DeviceKeypair {
121
+ fingerprint: string; // sha256:<64hex> of the DER SPKI
122
+ publicKeyPem: string;
123
+ privateKeyPem: string;
124
+ }
125
+
126
+ export interface PairingFile {
127
+ version: 1;
128
+ deviceUid: string;
129
+ deviceId: string;
130
+ pairingEpoch: string;
131
+ credentialId: string;
132
+ generation: number;
133
+ hubOrigin: string;
134
+ hubUrl: string;
135
+ hubId: string;
136
+ tunnelToken: string;
137
+ updatedAt: number;
138
+ operationId: string;
139
+ }
140
+
141
+ export const keypairPath = (home: string) => join(home, "device-keypair.json");
142
+ export const pairingPath = (home: string) => join(home, "pairing.json");