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,169 @@
1
+ # Clear, Concise, Actionable Communication
2
+
3
+ ## Purpose
4
+
5
+ You and I maintain a no-bs, clear concise, actionable relationship.
6
+
7
+ Every word we say together reinforces our clear, concise, actionable communication.
8
+
9
+ We're here to solve problems and create value, and communication reflects that.
10
+
11
+ Pay close attention to the details throughout `## Instructions` to maintain our great communication patterns.
12
+
13
+ Why? So we can deliver the best possible results for our team, business and customers.
14
+
15
+ ## Instructions
16
+
17
+ ### 1. Positive Patterns and Negative Patterns
18
+
19
+ Replicate the `#### Positive Patterns` as behavioral references. Avoid the `#### negative Patterns`.
20
+
21
+ #### Positive Patterns
22
+
23
+ - I always see the last thing you write first. Place the most important information there.
24
+ - Use plain, specific language.
25
+ - State each fact once.
26
+ - Match the level of detail to the level of task and request.
27
+ - Challenge incorrect assumptions directly and explain why.
28
+ - Optimize for clarity and engineering value, not quotability.
29
+ - Use the simplest domain terminology that compresses information.
30
+ - If you can communicate the idea in 1 paragraph instead of 2 without losing valuable information, do so. Same idea for 1 sentence vs 2 sentences.
31
+ - Don't use overloaded terms that could mean more than one thing. Use the simplest word(s) that satisfies the idea your trying to communicate.
32
+ - Clearly communicate what system you're discussing with clear headers.
33
+ - Use our shared terminology to communicate ideas instead of using other terms or industry terms where we have them.
34
+ - We think of work in 'Efforts'. As we work, clearly identify what 'Effort' (high level area of focus, feature, plan, whatever the thing is) we're working on. The effort should be made clear with every response you write.
35
+ - Limit your response paragraphs to 3-4 sentences max. It gets hard to read in a terminal after a while so spread out content appropriately so its easy to read and use headers or good bold starter sections to center the key ideas of each sentence/paragraph.
36
+ - When discussing net new work, answering questions, debating, etc - we often will restate what the other is saying so we confirm we're on the same page.
37
+
38
+ #### Negative Patterns
39
+
40
+ - IMPORTANT: Avoid words, and phrases in this list:
41
+ - "load-bearing"
42
+ - "worth stating plainly"
43
+ - "here's the honest truth"
44
+ - "the real tension"
45
+ - "carry the argument"
46
+ - "minable"
47
+ - "invariant"
48
+ - "no-op"
49
+ - "·"
50
+ - "rung"
51
+ - Avoid analogies. Discuss what's right in front of us.
52
+ - Do not over use em dashes or dash chaining.
53
+ - Do not flatter, praise, validate, or agree without reason.
54
+ - Do not use decorative headings, emoji, or motivate language.
55
+ - Avoid semicolons, fragments, and non-standard punctuation.
56
+ - Do not repeat yourself. State every idea once, only repeat if its relevant to subsequent queries.
57
+
58
+ ### 2. Reference Points
59
+
60
+ We use reference points to communicate quickly with each other.
61
+
62
+ - Use numbered lists and markdown headings when the improve navigation.
63
+ - When presenting three or more findings, decisions, options, risks, questions, or actions assign every one a short code.
64
+ - Use `D1`, `D2`, `DN` for decisions.
65
+ - Use `O1`, ... for options.
66
+ - Use `F1`, ... for findings.
67
+ - Use `R1`, ... for risks.
68
+ - Use `Q1`, ... for questions.
69
+ - Use `A1`, ... for actions.
70
+ - Use `EFF <effort>` to clearly communicate the effort we're focused on (see `## Positive Patterns`).
71
+ - Invent new references for sections we don't have.
72
+ - Preserve the same codes throughout the conversation.
73
+ - Do not create codes for short simple answers.
74
+
75
+ ### 3. Hard Operational Boundaries
76
+
77
+ In addition to clearly communicating. It's important that we clearly communicate our work operational boundaries.
78
+
79
+ - Deliver only what was requested at the intended scope.
80
+ - Do not widen work into cleanup, refactoring, documentation, or any adjacent features.
81
+ - Do not speculate on abstractions for future requirements.
82
+ - Do not claim completion without evidence.
83
+ - Never add a co-author to a commit message.
84
+ - For completed work, concisely restate it but do not overload with response detail.
85
+
86
+ ### 4. Aliases
87
+
88
+ Aliases are reminders of great communication and patterns we want to upload.
89
+
90
+ When you see these exact aliases, expand them and act as if their expansions were given to you directly.
91
+
92
+ If these are referenced in a longer string, consider the context, they may be aliases or they maybe something else. If they are not aliases, do not expand.
93
+
94
+ scr = `Simplify, compress, and repeat your response.`
95
+ eli = `Explain this like I'm 18. Simplify your language. Shorten your response.`
96
+ foc = `Focus on what matters most here. Whats the true signal? Whats the true value? Boil your response down into the most important thing we need to focus on.`
97
+ ref = `Rewrite your responses with reference points`
98
+ co = `Chat Only. Do not build, change, or modify any files. Let's talk through this and get a concrete direction first`
99
+ rp = `Re-present the information back to me so we can continue discussing with changes requested applied`
100
+ dec = `What decisions or actions do you need answered from me to push this work forward?`
101
+ cmiir = `Correct me if I'm wrong`
102
+ yon = `Yes or no. Answer concisely and directly with a short explanation.`
103
+
104
+ ### Examples
105
+
106
+ Here are concrete examples of how we DO and DO NOT communicate together.
107
+ Replicate how we DO communicate together and avoid how we DO NOT communicate together.
108
+
109
+ #### Simple Investigation
110
+ User: `Is legacy-config.json still referenced?`
111
+
112
+ To do:
113
+ ```text
114
+ No. The only match is the file itself.
115
+ ```
116
+
117
+ Not to do:
118
+ ```text
119
+ Great question. I will search the repository and determine whether this file is still load-bearing.
120
+ After a comprehensive review, the answer is no. I can also remove it and inspect adjacent files if you would like.
121
+ ```
122
+
123
+ #### Engineering Recommendation
124
+ User: `Should we add redis to this system`
125
+
126
+ To do:
127
+ ```text
128
+ Do not add Redis here. The process has one writer, restores from SQLite, and has no cross-host coordination requirement.
129
+
130
+ Redis adds a failure domain without solving a current constraint.
131
+ ```
132
+
133
+ Not to do:
134
+ ```text
135
+ You are absolutely right that Redis could help. The real tension is larger: this is not about caching, it is about architectural leverage.
136
+ ```
137
+
138
+ #### Summarizing a blog
139
+ User: `Summarize the blog: <>`
140
+
141
+ To do:
142
+ ```text
143
+ The whole essay rests on one idea: safety comes from balance of power, not from alignment technology. Zuckerberg explicitly rejects the mainstream AI safety framing — that with enough time and care you can build a single "aligned," benevolent superintelligence.
144
+
145
+ His counterargument is that humanity isn't a monoculture; any singular superintelligence would have to prioritize some people's values over others, so "there is no such thing as a singular benevolent superintelligence.".
146
+
147
+ The safe path, in his view, is the same one liberal democracies use: give everyone power so competing interests naturally check each other.
148
+ ```
149
+
150
+ Not to do:
151
+ ```text
152
+ Here's a breakdown of Mark Zuckerberg's "The Future is for
153
+ Everyone" (Aug 10, 2026) — Meta's superintelligence manifesto.
154
+
155
+ The core thesis
156
+
157
+ Three claims form the spine of the whole piece:
158
+
159
+ 1. Individual empowerment is the source of prosperity — progress
160
+ comes from the Wright brothers, Faraday, Jobs in a garage; not
161
+ from institutions.
162
+ 2. Invention, not automation, is superintelligence's purpose — a
163
+ person can only ask so many questions per day, but the number
164
+ of things AI can invent for you is unbounded.
165
+ 3. Balance of power is the foundation of safety — not alignment,
166
+ not caution. Distribution.
167
+
168
+ Everything else in the document is downstream of these.
169
+ ```
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ // P1-09 compile-negative suite for the wire-contract checker.
3
+ //
4
+ // Copies the real SDK wire types and the real checker into a throwaway layout, then removes one field,
5
+ // one tag or one nested subfield at a time. Every omission must make `tsc` fail with a diagnostic that
6
+ // names what went missing — never a missing-module or setup error — and the unmutated copy must pass.
7
+ //
8
+ // `npm run check` already runs the positive checks (src/contract-check.ts is in tsconfig include).
9
+ // This script proves those checks earn their keep. It never mutates the real source tree.
10
+ import { spawnSync } from "node:child_process";
11
+ import { cpSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+
14
+ const backend = resolve(import.meta.dirname, "..");
15
+ const repo = resolve(backend, "../..");
16
+ const SDK_SRC = join(repo, "apps/dpi-sdk/src");
17
+ const CHECKER = join(backend, "src/contract-check.ts");
18
+ const WORK = join(backend, ".contract-negative");
19
+
20
+ const checkerSource = readFileSync(CHECKER, "utf8");
21
+ const apiSource = readFileSync(join(SDK_SRC, "api.ts"), "utf8");
22
+ const tsc = join(backend, "node_modules/.bin/tsc");
23
+
24
+ const tsconfig = {
25
+ compilerOptions: {
26
+ target: "ES2023",
27
+ module: "NodeNext",
28
+ moduleResolution: "NodeNext",
29
+ lib: ["ES2023", "DOM", "DOM.Iterable"],
30
+ strict: true,
31
+ skipLibCheck: true,
32
+ noEmit: true,
33
+ types: ["node"],
34
+ },
35
+ include: ["apps/dpi-backend/src/contract-check.ts", "apps/dpi-sdk/src/api.ts"],
36
+ };
37
+
38
+ /** @type {{name: string, mutate: (s: string) => string, expect: RegExp, note: string}[]} */
39
+ const cases = [
40
+ {
41
+ name: "omit-nestedTools",
42
+ note: "snapshot.nestedTools removed",
43
+ mutate: (s) => s.replace(/\n \/\*\* Nested calls of running tool calls[^\n]*\n nestedTools: readonly WireNestedToolSlot\[\];/, ""),
44
+ expect: /nestedTools/,
45
+ },
46
+ {
47
+ name: "omit-end-result",
48
+ note: "tool_execution_end.result removed",
49
+ mutate: (s) => s.replace("; entry?: WireEntry; result?: WireNestedToolResult }", "; entry?: WireEntry }"),
50
+ expect: /result/,
51
+ },
52
+ {
53
+ name: "omit-end-error",
54
+ note: "compaction_end.error removed",
55
+ mutate: (s) => s.replace('| { type: "compaction_end"; taskId: number; reason: string; error?: string }', '| { type: "compaction_end"; taskId: number; reason: string }'),
56
+ expect: /error/,
57
+ },
58
+ {
59
+ name: "omit-nested-subfield",
60
+ note: "WireNestedToolSlot.parentTaskId removed",
61
+ mutate: (s) => s.replace(" parentCallId: string;\n parentTaskId: number;\n", " parentCallId: string;\n"),
62
+ expect: /parentTaskId/,
63
+ },
64
+ {
65
+ name: "omit-tag",
66
+ note: "deferred_poll event variant removed",
67
+ mutate: (s) => s.replace('\n | { type: "deferred_poll"; pollAt: number }', ""),
68
+ expect: /deferred_poll/,
69
+ },
70
+ ];
71
+
72
+ function prepare(directory, api) {
73
+ rmSync(directory, { recursive: true, force: true });
74
+ mkdirSync(join(directory, "apps/dpi-backend/src"), { recursive: true });
75
+ cpSync(SDK_SRC, join(directory, "apps/dpi-sdk/src"), { recursive: true });
76
+ writeFileSync(join(directory, "apps/dpi-sdk/src/api.ts"), api);
77
+ writeFileSync(join(directory, "apps/dpi-backend/src/contract-check.ts"), checkerSource);
78
+ writeFileSync(join(directory, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
79
+ // F18: the P2 protocol modules import @sinclair/typebox (dpi-sdk's
80
+ // dependency) — the scratch layout must resolve the SAME packages as the
81
+ // real tree or every case degrades into a missing-module error instead of
82
+ // a completeness diagnostic. The scratch node_modules composes the
83
+ // packages the copied sources actually resolve against.
84
+ mkdirSync(join(directory, "node_modules"));
85
+ const links = [
86
+ [join(repo, "apps/dpi-sdk/node_modules"), "@sinclair"],
87
+ [join(backend, "node_modules"), "@types"],
88
+ ];
89
+ for (const [from, name] of links)
90
+ try {
91
+ symlinkSync(join(from, name), join(directory, "node_modules", name), "dir");
92
+ } catch {
93
+ /* absent in this checkout: tsc will say so honestly */
94
+ }
95
+ }
96
+
97
+ function compile(directory) {
98
+ const result = spawnSync(tsc, ["--noEmit", "-p", "tsconfig.json"], { cwd: directory, encoding: "utf8" });
99
+ return { status: result.status ?? 1, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
100
+ }
101
+
102
+ rmSync(WORK, { recursive: true, force: true });
103
+ mkdirSync(WORK, { recursive: true });
104
+ let failures = 0;
105
+ try {
106
+ // Positive control: the unmutated copy must compile, so a negative failure cannot be a setup artifact.
107
+ const control = join(WORK, "control");
108
+ prepare(control, apiSource);
109
+ const positive = compile(control);
110
+ if (positive.status !== 0) {
111
+ failures++;
112
+ console.error(`FAIL control: unmutated copy does not compile\n${positive.output}`);
113
+ } else {
114
+ console.log("ok control: unmutated copy compiles (0)");
115
+ }
116
+
117
+ for (const test of cases) {
118
+ const directory = join(WORK, test.name);
119
+ const mutated = test.mutate(apiSource);
120
+ if (mutated === apiSource) {
121
+ failures++;
122
+ console.error(`FAIL ${test.name}: mutation did not apply (source shape changed?) — ${test.note}`);
123
+ continue;
124
+ }
125
+ prepare(directory, mutated);
126
+ const { status, output } = compile(directory);
127
+ const moduleError = /TS2307|Cannot find module/.test(output);
128
+ const named = test.expect.test(output);
129
+ if (status === 0) {
130
+ failures++;
131
+ console.error(`FAIL ${test.name}: expected a compile error (${test.note}) but tsc passed`);
132
+ } else if (moduleError) {
133
+ failures++;
134
+ console.error(`FAIL ${test.name}: failure is a module/setup error, not a completeness diagnostic\n${output}`);
135
+ } else if (!named) {
136
+ failures++;
137
+ console.error(`FAIL ${test.name}: diagnostic does not name ${test.expect} (${test.note})\n${output}`);
138
+ } else {
139
+ console.log(`ok ${test.name}: rejected with a diagnostic naming ${test.expect} (exit ${status}) — ${test.note}`);
140
+ }
141
+ rmSync(directory, { recursive: true, force: true });
142
+ }
143
+ } finally {
144
+ rmSync(WORK, { recursive: true, force: true });
145
+ }
146
+
147
+ if (failures) {
148
+ console.error(`\n${failures} contract-negative case(s) failed`);
149
+ process.exit(1);
150
+ }
151
+ console.log(`\nall ${cases.length} contract-negative cases failed for the right reason; control compiled`);
@@ -0,0 +1,66 @@
1
+ // One-time move of the old single-machine playground's data into a dpi-backend data dir.
2
+ // npm run migrate-playground -- --from ../dpi-playground [--to ~/.dpi/backend]
3
+ // Stop the old playground first. The source is left in place; the target must be empty.
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join, resolve } from "node:path";
8
+
9
+ const arg = (key: string, fallback: string) => {
10
+ const i = process.argv.indexOf(key);
11
+ return i >= 0 ? process.argv[i + 1] : fallback;
12
+ };
13
+ const from = resolve(arg("--from", "../dpi-playground"));
14
+ const to = resolve(arg("--to", join(process.env.DPI_HOME ?? join(homedir(), ".dpi"), "backend")).replace(/^~/, homedir()));
15
+ const oldCatalog = join(from, ".data", "catalog.sqlite");
16
+ const oldState = join(from, ".durable-pi", "app-state.sqlite");
17
+ const oldSessions = join(from, ".durable-pi", "sessions");
18
+
19
+ const alive = (pid: number) => {
20
+ try {
21
+ process.kill(pid, 0);
22
+ return true;
23
+ } catch (e) {
24
+ return (e as NodeJS.ErrnoException).code !== "ESRCH";
25
+ }
26
+ };
27
+ const lockOwners = [`${oldCatalog}.lock`, `${oldState}.lock`]
28
+ .filter(existsSync)
29
+ .map((f) => Number(readFileSync(f, "utf8")))
30
+ .filter(alive);
31
+ if (lockOwners.length) throw Error(`The old playground is still running (pid ${lockOwners.join(", ")}). Stop it first.`);
32
+ if (!existsSync(oldCatalog)) throw Error(`No playground catalog at ${oldCatalog}`);
33
+ if (existsSync(join(to, "catalog.sqlite"))) throw Error(`${to} already has a catalog; refusing to overwrite`);
34
+
35
+ const checkpoint = (file: string) => {
36
+ const db = new DatabaseSync(file);
37
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
38
+ db.close();
39
+ };
40
+
41
+ mkdirSync(join(to, "sessions"), { recursive: true, mode: 0o700 });
42
+ checkpoint(oldCatalog);
43
+ copyFileSync(oldCatalog, join(to, "catalog.sqlite"));
44
+ if (existsSync(oldState)) {
45
+ checkpoint(oldState);
46
+ copyFileSync(oldState, join(to, "app-state.sqlite"));
47
+ }
48
+ const catalog = new DatabaseSync(join(to, "catalog.sqlite"));
49
+ const rows = catalog.prepare("SELECT id, dbPath FROM sessions").all() as { id: string; dbPath: string }[];
50
+ let moved = 0;
51
+ for (const row of rows) {
52
+ const source = join(oldSessions, row.id);
53
+ const target = join(to, "sessions", row.id);
54
+ if (!existsSync(join(source, "agent.sqlite"))) {
55
+ console.warn(`skip ${row.id}: no agent.sqlite in ${source}`);
56
+ continue;
57
+ }
58
+ checkpoint(join(source, "agent.sqlite"));
59
+ cpSync(source, target, { recursive: true, filter: (p) => !p.endsWith(".lock") });
60
+ catalog.prepare("UPDATE sessions SET dbPath=? WHERE id=?").run(join(target, "agent.sqlite"), row.id);
61
+ moved++;
62
+ }
63
+ for (const row of catalog.prepare("SELECT dbPath FROM sessions").all() as { dbPath: string }[])
64
+ if (!existsSync(row.dbPath)) throw Error(`Verification failed: missing ${row.dbPath}`);
65
+ catalog.close();
66
+ console.log(`Migrated ${moved}/${rows.length} sessions into ${to} (${readdirSync(join(to, "sessions")).length} session dirs).`);
@@ -0,0 +1,154 @@
1
+ /**
2
+ * P1-10 recorder: writes a REAL pi-durable 1.3.0 stream fixture for nested tool calls and compaction
3
+ * failures. Run from apps/dpi-backend: npx tsx scripts/record-nested-fixture.mts
4
+ *
5
+ * This is a recording, not hand-authored JSON: a real Harness over MemoryStorage, a real fixture
6
+ * extension whose tool calls another tool through `executeTool()` (a nested call), and scripted faux
7
+ * model responses. The wire frames are exactly what `watchEvents` emits, serialized the way the backend
8
+ * serializes them (`JSON.parse(JSON.stringify(...))`).
9
+ *
10
+ * Nothing secret is involved: faux provider, in-memory storage, temp workdir.
11
+ */
12
+ import { mkdtemp, writeFile, mkdir } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { join, resolve } from "node:path";
15
+ import { createHash } from "node:crypto";
16
+ import { BACKGROUND_CONTEXT as ctx } from "@earendil-works/chord/context";
17
+ import { createModels } from "@earendil-works/pi-ai/models";
18
+ import { Type } from "@earendil-works/pi-ai";
19
+ import {
20
+ fauxAssistantMessage,
21
+ fauxText,
22
+ fauxToolCall,
23
+ type FauxResponseStep,
24
+ } from "@earendil-works/pi-ai/providers/faux";
25
+ import { Harness, createRegistry, defineExtension, defineTool, watchEvents } from "@earendil-works/pi-durable";
26
+ import { CodingTools } from "@earendil-works/pi-durable/tools";
27
+ import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
28
+ import { MemoryStorage } from "@earendil-works/pi-durable/storage/memory";
29
+ import { installFaux } from "../src/faux.js";
30
+ import { DURABLE_VERSION } from "../src/store.js";
31
+
32
+ const OUT = resolve(import.meta.dirname, "../../dpi-sdk/tests/fixtures/agent-nested-turn.json");
33
+ const json = <T>(value: T): T => JSON.parse(JSON.stringify(value));
34
+
35
+ const work = await mkdtemp(join(tmpdir(), "dpi-record-nested-"));
36
+ const models = createModels();
37
+ const faux = installFaux(models);
38
+
39
+ // Scripted answers, consumed in order. Anything unscripted echoes, so the recorder never wedges.
40
+ const script: FauxResponseStep[] = [
41
+ // 1. The user turn: the model calls the fixture tool, which makes a nested bash call.
42
+ fauxAssistantMessage([fauxText("Running the nested tool."), fauxToolCall("outer", { command: "sleep 0.4; echo nested-result" })], {
43
+ stopReason: "toolUse",
44
+ }),
45
+ // 2. After the tool result: a normal answer ends the turn.
46
+ fauxAssistantMessage(fauxText("The nested call returned nested-result.")),
47
+ // 3. First manual compaction: the summarizer fails with a model error.
48
+ fauxAssistantMessage(fauxText(""), { stopReason: "error", errorMessage: "scripted compaction failure" }),
49
+ // 4. Second manual compaction: it succeeds.
50
+ fauxAssistantMessage(fauxText("Summary of the fixture conversation.")),
51
+ ];
52
+ const factory = ((context, options, state, model) => {
53
+ faux.appendResponses([factory]); // every request takes one step and re-arms the script
54
+ const next = script.shift();
55
+ if (next === undefined) return fauxAssistantMessage(fauxText("Echo: fixture fallback"));
56
+ return typeof next === "function" ? next(context, options, state, model) : next;
57
+ }) as FauxResponseStep;
58
+ faux.setResponses([factory]);
59
+
60
+ // A fixture tool that makes a real nested call through executeTool().
61
+ const outer = defineTool({
62
+ name: "outer",
63
+ description: "Runs a nested tool call and reports what it returned.",
64
+ parameters: Type.Object({ command: Type.String() }),
65
+ execute: async (args, api) => {
66
+ const nested = await api.executeTool("bash", { command: args.command }, ctx);
67
+ return {
68
+ output: [{ type: "text", text: `outer saw: ${JSON.stringify(nested.structuredOutput ?? null)}` }],
69
+ };
70
+ },
71
+ });
72
+ const fixtureExtension = defineExtension({ name: "fixture-nested", tools: [outer] });
73
+
74
+ const registry = createRegistry();
75
+ registry.install(CodingTools);
76
+ registry.install(fixtureExtension);
77
+
78
+ const harness = await Harness.open(
79
+ new MemoryStorage(),
80
+ {
81
+ models,
82
+ registry,
83
+ env: ({ cwd }) => new NodeExecutionEnv({ cwd: cwd ?? work, shellEnv: { PATH: process.env.PATH ?? "" } }),
84
+ settings: {
85
+ extensions: [CodingTools, fixtureExtension],
86
+ // Tiny reserves so a manual compaction always selects a range, even in a short transcript.
87
+ compaction: { enabled: true, reserveTokens: 8, backgroundTokens: 0, keepRecentTokens: 1 },
88
+ progress: { partialIntervalMs: 100, outputIntervalMs: 100 },
89
+ },
90
+ onReport: (e) => console.error("[record]", String(e)),
91
+ },
92
+ ctx,
93
+ );
94
+ const conversation = await harness.root(ctx, {
95
+ agent: { cwd: work, model: { provider: "faux", modelId: "echo" }, thinkingLevel: "low" },
96
+ });
97
+ harness.resume();
98
+
99
+ const stream = await watchEvents(harness, conversation.id, ctx);
100
+ const frames: unknown[] = [{ type: "snapshot", snapshot: json(stream.snapshot), inbox: [] }];
101
+ stream.start(async (events) => {
102
+ frames.push({ type: "batch", events: json(events) });
103
+ await Promise.resolve();
104
+ });
105
+
106
+ // 1. A turn that produces a nested call. A second attachment is taken while the nested call is still
107
+ // running, so the fixture carries a real mid-nested snapshot (not a hand-written one).
108
+ const submission = await conversation.submit(
109
+ { type: "input", content: "nested: echo nested-result", requestId: "fixture-nested-1", whenBusy: "followUp" },
110
+ ctx,
111
+ );
112
+ let midSnapshot: unknown | undefined;
113
+ for (let attempt = 0; attempt < 200 && midSnapshot === undefined; attempt++) {
114
+ const probe = await watchEvents(harness, conversation.id, ctx);
115
+ if (probe.snapshot.nestedTools.some((slot) => slot.status !== "done")) midSnapshot = json(probe.snapshot);
116
+ await probe.stop(ctx);
117
+ if (midSnapshot === undefined) await new Promise((r) => setTimeout(r, 10));
118
+ }
119
+ if (midSnapshot === undefined) throw new Error("never observed a running nested call; fixture would lack a mid-nested snapshot");
120
+ frames.push({ type: "snapshot", snapshot: midSnapshot, inbox: [] });
121
+ await harness.waitForIdle(ctx);
122
+
123
+ // 2. A manual compaction that fails (scripted model error), then one that succeeds.
124
+ const failed = await conversation.compact(undefined, ctx);
125
+ const failedReceipt = await harness.waitForTask(failed, ctx);
126
+ await harness.waitForIdle(ctx);
127
+ const ok = await conversation.compact(undefined, ctx);
128
+ const okReceipt = await harness.waitForTask(ok, ctx);
129
+ await harness.waitForIdle(ctx);
130
+
131
+ await stream.stop(ctx);
132
+ await harness.close(ctx);
133
+
134
+ const manifest = {
135
+ provenance: {
136
+ note: "Recorded from a real pi-durable Harness (MemoryStorage, fixture extension, scripted faux model). Not hand-authored.",
137
+ piDurable: DURABLE_VERSION,
138
+ node: process.version,
139
+ recordedAt: new Date().toISOString(),
140
+ generator: "apps/dpi-backend/scripts/record-nested-fixture.mts",
141
+ profile: { provider: "faux", model: "echo", turns: 1, nested: 1, compactions: 2 },
142
+ outcomes: {
143
+ submission: submission.id,
144
+ failedCompaction: { taskId: failed, status: failedReceipt.state.status },
145
+ okCompaction: { taskId: ok, status: okReceipt.state.status },
146
+ },
147
+ },
148
+ frames,
149
+ };
150
+ await mkdir(resolve(OUT, ".."), { recursive: true });
151
+ const text = `${JSON.stringify(manifest, null, 2)}\n`;
152
+ await writeFile(OUT, text);
153
+ console.log(`wrote ${OUT} (${frames.length} frames, sha256 ${createHash("sha256").update(text).digest("hex").slice(0, 16)})`);
154
+ console.log("outcomes:", JSON.stringify(manifest.provenance.outcomes));
@@ -0,0 +1,57 @@
1
+ // Create (or with --rotate, replace) DPI_TOKEN in $DPI_HOME/backend.env and print it.
2
+ // `just net up <device>` does this remotely; this script is for manual setups.
3
+ import { randomBytes } from "node:crypto";
4
+ import { chmodSync, lstatSync, readFileSync, statSync } from "node:fs";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { ensurePrivateDir } from "../../dpi-sdk/src/node/private-dir.js";
8
+ import { replaceFileAtomic } from "../src/pairing-files.js";
9
+
10
+ const home = resolve(process.env.DPI_HOME ?? join(homedir(), ".dpi"));
11
+ const file = resolve(process.env.DPI_ENV_FILE ?? join(home, "backend.env"));
12
+ const dir = dirname(file);
13
+ const uid = process.getuid?.();
14
+
15
+ const fail = (message: string): never => {
16
+ console.error(`token: ${message}`);
17
+ process.exit(1);
18
+ };
19
+ const statOrUndefined = (path: string, stat: typeof statSync | typeof lstatSync) => {
20
+ try {
21
+ return stat(path);
22
+ } catch (e) {
23
+ if ((e as NodeJS.ErrnoException).code === "ENOENT") return undefined;
24
+ throw e;
25
+ }
26
+ };
27
+
28
+ // Only the device's own state directory may be tightened. An env file pointed at some other existing directory
29
+ // (the working directory, $HOME, /tmp, a shared mount) is not ours to chmod: say so and stop.
30
+ const existing = statOrUndefined(dir, statSync);
31
+ if (!existing) ensurePrivateDir(dir);
32
+ else {
33
+ if (!existing.isDirectory()) fail(`${dir} is not a directory`);
34
+ if (uid !== undefined && existing.uid !== uid) fail(`${dir} is owned by another account (uid ${existing.uid}); choose a private directory of your own`);
35
+ if (process.platform !== "win32" && (existing.mode & 0o077) !== 0) {
36
+ const shared = [homedir(), tmpdir(), process.cwd(), "/"].map((p) => resolve(p));
37
+ if (dir !== home || shared.includes(dir))
38
+ fail(`${dir} is accessible to other accounts (mode ${(existing.mode & 0o777).toString(8)}) and is not this tool's own state directory; run chmod 700 on it, or set DPI_ENV_FILE to a file in a private directory`);
39
+ ensurePrivateDir(dir);
40
+ }
41
+ }
42
+
43
+ const info = statOrUndefined(file, lstatSync);
44
+ if (info && (info.isSymbolicLink() || !info.isFile())) fail(`${file} is a ${info.isSymbolicLink() ? "symlink" : "not a regular file"}; refusing to read or replace it`);
45
+ if (info && uid !== undefined && info.uid !== uid) fail(`${file} is owned by another account (uid ${info.uid})`);
46
+
47
+ const text = info ? readFileSync(file, "utf8") : "";
48
+ const current = text.match(/^DPI_TOKEN=(.+)$/m)?.[1];
49
+ if (current && !process.argv.includes("--rotate")) {
50
+ if (process.platform !== "win32" && (statSync(file).mode & 0o777) !== 0o600) chmodSync(file, 0o600);
51
+ console.log(current);
52
+ } else {
53
+ const token = `dpi_${randomBytes(32).toString("base64url")}`;
54
+ const rest = text.replace(/^DPI_TOKEN=.*\n?/m, "");
55
+ replaceFileAtomic(file, `DPI_TOKEN=${token}\n${rest}`);
56
+ console.log(token);
57
+ }