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,164 @@
1
+ // Transcript items (from the SDK, same as the web client) → pi-tui components, cached by item id.
2
+ // Only items whose content changed are rebuilt; the live assistant reuses its Markdown blocks.
3
+ import {
4
+ Container,
5
+ Markdown,
6
+ truncateToWidth,
7
+ wrapTextWithAnsi,
8
+ type Component,
9
+ } from "@earendil-works/pi-tui";
10
+ import type { ToolCallView, TranscriptItem } from "../../dpi-sdk/src/transcript.ts";
11
+ import { c, markdownTheme } from "./theme.ts";
12
+
13
+ const SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
14
+ /** Advanced by the app while a tool runs; running tool rows read it at render time. */
15
+ export const spinner = { frame: 0 };
16
+ const TAIL = 6;
17
+
18
+ /** One styled line, cut to the width. */
19
+ export class Line implements Component {
20
+ constructor(public text: string) {}
21
+ render(width: number) {
22
+ return [truncateToWidth(this.text, width)];
23
+ }
24
+ invalidate() {}
25
+ }
26
+
27
+ /** Wrapped text with a first-line prefix and a hanging indent. */
28
+ class Wrapped implements Component {
29
+ constructor(
30
+ private text: string,
31
+ private prefix: string,
32
+ private style: (s: string) => string,
33
+ private indent = " ",
34
+ ) {}
35
+ render(width: number) {
36
+ const inner = Math.max(1, width - 2);
37
+ const lines = this.text.split("\n").flatMap((l) => (l ? wrapTextWithAnsi(l, inner) : [""]));
38
+ return lines.map((l, i) => truncateToWidth((i ? this.indent : this.prefix) + this.style(l), width));
39
+ }
40
+ invalidate() {}
41
+ }
42
+
43
+ function argsPreview(call: ToolCallView) {
44
+ const args = call.args as Record<string, unknown> | undefined;
45
+ if (args && typeof args === "object") {
46
+ for (const key of ["command", "path", "file_path", "pattern", "task", "query"])
47
+ if (typeof args[key] === "string") return (args[key] as string).replace(/\s+/g, " ");
48
+ }
49
+ const json = JSON.stringify(call.args ?? {});
50
+ return json === "{}" ? "" : json;
51
+ }
52
+
53
+ export class ToolRow implements Component {
54
+ constructor(
55
+ private call: ToolCallView,
56
+ private expand: boolean,
57
+ ) {}
58
+ render(width: number) {
59
+ const s = this.call.status;
60
+ const icon =
61
+ s === "running" ? c.warn(SPIN[spinner.frame % SPIN.length]) : s === "done" ? c.ok("✓") : s === "error" ? c.err("✗") : c.muted("○");
62
+ const head = truncateToWidth(`${icon} ${c.bold(this.call.name)} ${c.muted(argsPreview(this.call))}`, width);
63
+ const body = (this.call.result ?? this.call.output ?? "").replace(/\s+$/, "");
64
+ if (!body) return [head];
65
+ const all = body.split("\n");
66
+ const shown = this.expand ? all : all.slice(-TAIL);
67
+ const gutter = c.muted(" │ ");
68
+ const lines = this.expand
69
+ ? shown.flatMap((l) => (l ? wrapTextWithAnsi(l, Math.max(1, width - 4)) : [""]))
70
+ : shown;
71
+ const out = [head, ...lines.map((l) => truncateToWidth(gutter + (s === "error" ? c.err(l) : l), width))];
72
+ if (all.length > shown.length)
73
+ out.splice(1, 0, truncateToWidth(c.muted(` │ … ${all.length - shown.length} earlier lines (ctrl+o)`), width));
74
+ return out;
75
+ }
76
+ invalidate() {}
77
+ }
78
+
79
+ class Stack implements Component {
80
+ constructor(public parts: Component[] = []) {}
81
+ render(width: number) {
82
+ return this.parts.flatMap((p) => p.render(width));
83
+ }
84
+ invalidate() {
85
+ for (const p of this.parts) p.invalidate();
86
+ }
87
+ }
88
+
89
+ /** Assistant message: Markdown text blocks (reused across deltas), thinking, tool rows, errors. */
90
+ class AssistantView extends Stack {
91
+ private markdown = new Map<number, Markdown>();
92
+ update(item: Extract<TranscriptItem, { kind: "assistant" }>, expand: boolean) {
93
+ const parts: Component[] = [new Line("")];
94
+ item.blocks.forEach((block, i) => {
95
+ if (block.type === "text") {
96
+ let md = this.markdown.get(i);
97
+ if (!md) {
98
+ md = new Markdown(block.text, 0, 0, markdownTheme);
99
+ this.markdown.set(i, md);
100
+ } else md.setText(block.text);
101
+ parts.push(md);
102
+ } else if (block.type === "thinking") {
103
+ parts.push(
104
+ expand
105
+ ? new Wrapped(block.text, c.muted("∴ "), (s) => c.italic(c.muted(s)))
106
+ : new Line(c.italic(c.muted(`∴ ${block.text.replace(/\s+/g, " ")}`))),
107
+ );
108
+ } else parts.push(new ToolRow(block.call, expand));
109
+ });
110
+ if (item.streaming && !item.blocks.length) parts.push(new Line(c.muted("…")));
111
+ if (item.error) parts.push(new Line(c.err(`! ${item.error}`)));
112
+ this.parts = parts;
113
+ }
114
+ }
115
+
116
+ export function itemComponent(item: TranscriptItem, expand: boolean): Component {
117
+ switch (item.kind) {
118
+ case "user":
119
+ return new Stack([new Line(""), new Wrapped(item.text, c.user("› "), c.user)]);
120
+ case "assistant": {
121
+ const view = new AssistantView();
122
+ view.update(item, expand);
123
+ return view;
124
+ }
125
+ case "tool-result":
126
+ return new ToolRow(
127
+ { callId: String(item.id), name: item.name, args: undefined, status: item.isError ? "error" : "done", result: item.text },
128
+ expand,
129
+ );
130
+ case "tool":
131
+ return new ToolRow(item.call, expand);
132
+ default:
133
+ return new Line(c.muted(`── ${item.text} `));
134
+ }
135
+ }
136
+
137
+ export class TranscriptView extends Container {
138
+ private cache = new Map<string, { sig: string; component: Component }>();
139
+ expand = false;
140
+
141
+ update(items: readonly TranscriptItem[]) {
142
+ const next = new Map<string, { sig: string; component: Component }>();
143
+ this.clear();
144
+ for (const item of items) {
145
+ const key = `${item.kind}:${item.id}`;
146
+ const sig = `${this.expand}|${JSON.stringify(item)}`;
147
+ let entry = this.cache.get(key);
148
+ if (!entry || entry.sig !== sig) {
149
+ if (entry && item.kind === "assistant" && entry.component instanceof AssistantView)
150
+ entry.component.update(item, this.expand);
151
+ else entry = { sig, component: itemComponent(item, this.expand) };
152
+ entry.sig = sig;
153
+ }
154
+ next.set(key, entry);
155
+ this.addChild(entry.component);
156
+ }
157
+ this.cache = next;
158
+ }
159
+
160
+ reset() {
161
+ this.cache.clear();
162
+ this.clear();
163
+ }
164
+ }
@@ -0,0 +1,57 @@
1
+ // Colors mirror the FE brand tokens exactly (apps/dpi-playground-fe/src/style.css :root):
2
+ // fg #f2eeee, accent #ff8997, secondary/muted #c9a87c, faint #8d7778, ok #6fcf97,
3
+ // warn #f2c94c, err #ff6b6b, line-soft #2a2021, primary #b9283c, primary-line #df5363.
4
+ import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
5
+
6
+ const rgb = (hex: string) => {
7
+ const n = parseInt(hex.slice(1), 16);
8
+ return `${(n >> 16) & 255};${(n >> 8) & 255};${n & 255}`;
9
+ };
10
+ const fg = (hex: string) => (s: string) => `\x1b[38;2;${rgb(hex)}m${s}\x1b[39m`;
11
+ const wrap = (on: string, off: string) => (s: string) => `\x1b[${on}m${s}\x1b[${off}m`;
12
+
13
+ export const c = {
14
+ accent: fg("#ff8997"),
15
+ muted: fg("#8d7778"),
16
+ secondary: fg("#c9a87c"),
17
+ text: fg("#f2eeee"),
18
+ ok: fg("#6fcf97"),
19
+ warn: fg("#f2c94c"),
20
+ err: fg("#ff6b6b"),
21
+ user: fg("#8ec5ff"),
22
+ primary: fg("#b9283c"),
23
+ primaryLine: fg("#df5363"),
24
+ bold: wrap("1", "22"),
25
+ dim: wrap("2", "22"),
26
+ italic: wrap("3", "23"),
27
+ underline: wrap("4", "24"),
28
+ strike: wrap("9", "29"),
29
+ userBg: (s: string) => `\x1b[48;2;42;32;33m${s}\x1b[49m`,
30
+ };
31
+
32
+ export const markdownTheme: MarkdownTheme = {
33
+ heading: (s) => c.bold(c.accent(s)),
34
+ link: c.user,
35
+ linkUrl: c.muted,
36
+ code: c.secondary,
37
+ codeBlock: c.text,
38
+ codeBlockBorder: c.muted,
39
+ quote: c.muted,
40
+ quoteBorder: c.muted,
41
+ hr: c.muted,
42
+ listBullet: c.accent,
43
+ bold: c.bold,
44
+ italic: c.italic,
45
+ strikethrough: c.strike,
46
+ underline: c.underline,
47
+ };
48
+
49
+ export const selectTheme: SelectListTheme = {
50
+ selectedPrefix: c.accent,
51
+ selectedText: (s) => c.bold(c.accent(s)),
52
+ description: c.muted,
53
+ scrollInfo: c.muted,
54
+ noMatch: c.muted,
55
+ };
56
+
57
+ export const editorTheme: EditorTheme = { borderColor: c.muted, selectList: selectTheme };
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "allowImportingTsExtensions": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src/**/*.ts", "tests/**/*.ts"]
14
+ }
package/bin/device.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Run the device daemon TypeScript entry through tsx so `my-durable-device` works without a build step.
3
+ import { register } from "tsx/esm/api";
4
+ register();
5
+ await import("../apps/dpi-backend/src/main.ts");
package/bin/hub.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Run the hub TypeScript entry through tsx so `my-durable-hub` works without a build step.
3
+ import { register } from "tsx/esm/api";
4
+ register();
5
+ await import("../apps/dpi-playground/apps/dpi-playground-hub/src/main.ts");
package/bin/tui.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Run the TUI TypeScript entry through tsx so `my-durable-tui` works without a build step.
3
+ import { register } from "tsx/esm/api";
4
+ register();
5
+ await import("../apps/dpi-tui/src/main.ts");
@@ -0,0 +1,120 @@
1
+ # Reference Caddy deployment — Durable Agent Network (P2-21, spec 05 §4).
2
+ #
3
+ # Placeholders (Caddy {$ENV} substitution, set by the operator environment):
4
+ # DPI_PUBLIC_HOST host:port Caddy listens on (the public origin)
5
+ # DPI_TLS_CERT full-chain PEM
6
+ # DPI_TLS_KEY private key (0600, managed outside the repo)
7
+ # DPI_HUB_ADDR loopback address of the hub process (e.g. 127.0.0.1:4781)
8
+ # DPI_ADMIN_HASH bcrypt hash for the single admin user (caddy hash-password;
9
+ # the PLAINTEXT never lives in this repo or in env logs)
10
+ #
11
+ # Auth matrix (05):
12
+ # exempt (exact path/method): GET /healthz · POST /api/pair/start ·
13
+ # POST /api/pair/verify · GET /api/tunnel (WS) — everything else requires
14
+ # HTTP Basic auth, and the Authorization header is STRIPPED before the hub
15
+ # so device tokens and admin credentials can never mix. Client-supplied
16
+ # XFF/XFP are overwritten from the real connection.
17
+ {
18
+ admin off
19
+ auto_https off
20
+ servers {
21
+ protocols h1 h2
22
+ }
23
+ }
24
+
25
+ https://{$DPI_PUBLIC_HOST} {
26
+ # TLS 1.2 is the declared floor (05 §4) rather than a Caddy default that a
27
+ # future build could change. Caddy adds a catch-all policy for handshakes whose
28
+ # SNI does not match; that one keeps Caddy's own default (also 1.2), and
29
+ # tests/caddy-tls.test.ts proves both paths refuse TLS 1.0/1.1.
30
+ tls {$DPI_TLS_CERT} {$DPI_TLS_KEY} {
31
+ protocols tls1.2 tls1.3
32
+ }
33
+
34
+ header {
35
+ Strict-Transport-Security "max-age=15552000; includeSubDomains"
36
+ X-Content-Type-Options "nosniff"
37
+ Referrer-Policy "no-referrer"
38
+ -Server
39
+ -X-Powered-By
40
+ }
41
+
42
+ # CSP is deliberately NOT set here. The hub owns content security per
43
+ # response: the app shell is served with a restrictive document CSP
44
+ # (script-src/style-src 'self' + fonts.googleapis.com, connect-src 'self',
45
+ # frame-ancestors 'none', X-Frame-Options: DENY — hub main.ts) while
46
+ # proxied device answers are locked down harder still (default-src 'none';
47
+ # sandbox — hub proxy.ts). This block runs after proxying, so a CSP line
48
+ # here would overwrite both of those per-route policies with one blanket
49
+ # value: either it weakens device responses or it breaks the app shell.
50
+ # Do not "harden" this file by adding one; the harness also runs without
51
+ # Caddy, so the hub must be correct on its own either way.
52
+
53
+ # Admin surface = everything except the four exact exemptions below.
54
+ @admin {
55
+ not path /api/tunnel /api/pair/start /api/pair/verify /healthz
56
+ }
57
+ @tunnel path /api/tunnel
58
+ @pairstart {
59
+ method POST
60
+ path /api/pair/start
61
+ }
62
+ @pairverify {
63
+ method POST
64
+ path /api/pair/verify
65
+ }
66
+ @healthz {
67
+ method GET
68
+ path /healthz
69
+ }
70
+
71
+ # 1. Tunnel WebSocket: exempt from basic auth, Bearer preserved.
72
+ handle @tunnel {
73
+ reverse_proxy {$DPI_HUB_ADDR} {
74
+ header_up X-Forwarded-For {remote_host}
75
+ header_up X-Forwarded-Proto https
76
+ flush_interval -1
77
+ }
78
+ }
79
+
80
+ # 2-4. Exempt pairing entry points — strip any caller Authorization so
81
+ # credentials never mix (04 §9).
82
+ handle @pairstart {
83
+ reverse_proxy {$DPI_HUB_ADDR} {
84
+ header_up X-Forwarded-For {remote_host}
85
+ header_up X-Forwarded-Proto https
86
+ header_up -Authorization
87
+ flush_interval -1
88
+ }
89
+ }
90
+
91
+ handle @pairverify {
92
+ reverse_proxy {$DPI_HUB_ADDR} {
93
+ header_up X-Forwarded-For {remote_host}
94
+ header_up X-Forwarded-Proto https
95
+ header_up -Authorization
96
+ flush_interval -1
97
+ }
98
+ }
99
+
100
+ handle @healthz {
101
+ reverse_proxy {$DPI_HUB_ADDR} {
102
+ header_up X-Forwarded-For {remote_host}
103
+ header_up X-Forwarded-Proto https
104
+ header_up -Authorization
105
+ }
106
+ }
107
+
108
+ # Everything else: basic auth, then STRIP Authorization before the hub.
109
+ handle @admin {
110
+ basic_auth {
111
+ admin {$DPI_ADMIN_HASH}
112
+ }
113
+ reverse_proxy {$DPI_HUB_ADDR} {
114
+ header_up X-Forwarded-For {remote_host}
115
+ header_up X-Forwarded-Proto https
116
+ header_up -Authorization
117
+ flush_interval -1
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,102 @@
1
+ # Local verification and release gates
2
+
3
+ Run from the repository root after installing the four apps:
4
+
5
+ ```sh
6
+ just test verify # full local matrix
7
+ just test verify P2 # core checks without lint/browser/PTY
8
+ node scripts/verify.mjs --phase all
9
+ node --test scripts/verify.test.mjs
10
+ ```
11
+
12
+ Phase names are case-insensitive. A missing value after `--phase`, an unknown
13
+ phase, a failed check or a check that cannot run produces a nonzero exit.
14
+ Every command runs with an explicit working directory.
15
+
16
+ ## What each command covers
17
+
18
+ All phases run the verification CLI regressions and five build/type/contract
19
+ checks. The CLI tests execute the real runner in an isolated filesystem and
20
+ replace only its external commands.
21
+
22
+ | Selector | Additional checks |
23
+ |---|---|
24
+ | `P0` | SDK tests (a focused selector, not a G0 decision) |
25
+ | `P1` | SDK and playground tests (not a G1 decision) |
26
+ | `P2` (default) | SDK, backend, playground and TUI tests |
27
+ | `all` | P2 plus the R-1/M4 SIGKILL recovery timing, both lint groups, the network/mobile browser, pairing/offline-credential browser and real PTY pairing/resize tests |
28
+
29
+ The Web build runs before browser tests. Lint/browser/PTY checks are **NOT_RUN**
30
+ in `P2`, not implicitly passed. `all` requires the installed playground lint
31
+ toolchain, system Chrome (`channel: "chrome"` in Playwright) and `tuistory`.
32
+ Core integration tests also need `tools/caddy`. Test providers are faux and
33
+ servers bind loopback; no paid-model or deployment commands run.
34
+
35
+ ## Recovery timing (R-1/M4)
36
+
37
+ ```sh
38
+ just test timing # or: cd apps/dpi-backend && npm run test:timing
39
+ DPI_M4_REPORT=/path/m4.json npm run test:timing # also keep the JSON report
40
+ ```
41
+
42
+ This suite SIGKILLs a real device server mid-run and measures how long it takes
43
+ to be back at verified durable progress. Every sample is asserted **strictly
44
+ under 2 s**, measured from *before* the kill (the interval includes a fixed
45
+ kill-position wait, so it is an upper bound, not a stopwatch on the restart
46
+ alone). The runner prints every sample, including the ones before a failure.
47
+
48
+ It is a **this-machine** measurement and it is load-sensitive: run it on a
49
+ quiet machine, never in parallel with the rest of the matrix. On the reference
50
+ laptop (i5-6300HQ ×4, Node 22) quiet samples land around 1.0–1.4 s; background
51
+ load pushes individual samples up, so a failure under load is rerun on an idle
52
+ machine before it counts. A green run here is **not** the P2-29 M4 acceptance:
53
+ that target is defined over NAT with the current daemon and is measured
54
+ separately.
55
+
56
+ ## Regression suites for the Gate A remediation
57
+
58
+ All run inside the phase suites above; each is also runnable on its own with
59
+ `npx tsx --test tests/<file>` from the owning app.
60
+
61
+ - `apps/dpi-backend`: `connector-backoff`, `connector-hardening`,
62
+ `connector-limits`, `connector-tls`, `pairing-lifecycle`, `private-dir`,
63
+ `rest-allowlist`, `token-script`, `timing/m4-resume.timing.ts`.
64
+ - `apps/dpi-playground`: `client-ip`, `http-errors`, `pairing-limits`,
65
+ `audit`, `guards-audit`, `hub-errors`, `tunnel-gate-a`, `pairing-gate-a`,
66
+ `device-lifecycle` (R-2 tunnel reconnect, T-2 bounded backoff), the
67
+ `fe-*` client suites, and the GA-* cases appended to `rotation`,
68
+ `rotation-ack`, `revoke` and `tls-policy`.
69
+ - `apps/dpi-sdk`: `registry-migration` with the `fixtures/registry-v0`
70
+ builder, `registry-writers` (writer exclusion), `backoff`, `private-dir`,
71
+ `sse-limits`.
72
+ - `apps/dpi-tui`: `pairing-ui.mjs` covers the offline-credential scan (S-7)
73
+ in a real PTY.
74
+
75
+ The runner prints command/status/count/duration summaries and bounded failure
76
+ output. It does **not** automatically save a report or source manifest.
77
+ Capture output to a new local evidence file and keep the command's exit code;
78
+ do not pipe it through a command that hides failure. Evidence needs the Git HEAD
79
+ and hashes of uncommitted source, not just a commit name.
80
+
81
+ ## What a green local run does not establish
82
+
83
+ - **P2-29:** real NAT pairing and current-daemon M1–M4 targets are
84
+ **NOT_MEASURED**. M3 includes human fingerprint approval; M4 starts before
85
+ SIGKILL and ends at verified durable resumed progress.
86
+ - **D-4 (P3):** the disaster restore/quarantine/re-pair drill is
87
+ **NOT_MEASURED**. D-4 is not the NAT test.
88
+ - **Gate A / Gate B:** **NOT_EVALUATED**. A green runner is neither an independent
89
+ security verdict nor owner authorization to expose or deploy anything.
90
+ - Soak/capacity, real-model/provider acceptance and untested platforms remain
91
+ separate work. Historical records do not become current acceptance results.
92
+
93
+ ## Memory units
94
+
95
+ The M1 contract is **150 MB = 150,000,000 bytes**, not 150 MiB.
96
+ Convert Linux `VmHWM` KiB to bytes by multiplying by 1024, then compare bytes
97
+ before rounding. Report both decimal MB and binary MiB.
98
+
99
+ The original P0 samples remain historical inputs. Their `targetMiB: 150` field
100
+ was incorrect contract metadata and must not determine the verdict. The first
101
+ sample is 153,256 KiB = 156,934,144 bytes, so that baseline misses M1 despite
102
+ rounding to 149.7 MiB. This correction is not a fresh P2 performance measurement.
@@ -0,0 +1,62 @@
1
+ # dpi-network.example.yaml → copy to dpi-network.yaml (gitignored) and add your own machines.
2
+ #
3
+ # cp dpi-network.example.yaml dpi-network.yaml
4
+ #
5
+ # Every device runs apps/dpi-backend. Clients (the web hub, dpi-tui, curl) connect with a URL and a
6
+ # bearer token. No code or recipe names a machine: `just net <verb> <id>` looks the id up here and
7
+ # runs the driver for its `kind`.
8
+ #
9
+ # just net up <id> provision → deploy → env → service → (expose) → register, idempotent
10
+ # just net status probe every device
11
+ # just net logs <id> | just net restart <id> | just net down <id> | just net deploy <id>
12
+ # just net destroy <id> exe-dev only; refuses any VM not named in this file
13
+ #
14
+ # Kinds
15
+ # local this machine; `just backend run` runs it in a terminal, or set `service: launchd|systemd`
16
+ # ssh any host reachable with `ssh <ssh>`; nvm + rsync + launchd (macOS) or systemd (Linux)
17
+ # exe-dev an exe.dev VM: the ssh driver at <vm>.exe.xyz plus VM create and HTTPS proxy exposure
18
+ # url connect only (someone else runs the backend); `just net register <id> --token <t>`
19
+ #
20
+ # Tokens never live here. Each device generates its own in <home_dir>/backend.env, and
21
+ # `just net register <id>` copies it into ~/.dpi/network.sqlite on this machine.
22
+
23
+ # Each device's <home_dir>/backend.env is written from this file and nothing else:
24
+ # DPI_TOKEN generated on the first `just net up`, kept after (new one: just net rotate <id>)
25
+ # DPI_DEVICE_NAME the device id
26
+ # DPI_PORT port
27
+ # DPI_HOST host (default 127.0.0.1 for kind local, 0.0.0.0 otherwise)
28
+ # DPI_START_DIRS start_dirs, colon-separated
29
+ # <NAME>=<value> for each name in env_keys, with its value read from this checkout's .env
30
+ # <NAME>=<value> for each entry in env (defaults.env merged with the device's env)
31
+
32
+ defaults:
33
+ port: 4780 # dpi-backend port on every device
34
+ node: 24 # Node major installed with nvm on ssh / exe-dev devices
35
+ code_dir: ~/dpi # where apps/dpi-backend + apps/dpi-sdk are synced (ssh / exe-dev)
36
+ home_dir: ~/.dpi # backend.env, data and logs; deploys never touch it
37
+ start_dirs: ["~/dpi-work"] # where new sessions start (created if missing); any existing folder works as a cwd
38
+ env_keys: [OPENROUTER_API_KEY] # secrets: copied by name from .env, never written here
39
+ # env: # plain settings, written as-is
40
+ # DPI_PROVIDER: faux # the free deterministic model, for tests
41
+
42
+ devices:
43
+ local:
44
+ name: Laptop
45
+ kind: local
46
+
47
+ # A Mac or Linux box you can reach with passwordless ssh.
48
+ # mac-mini:
49
+ # name: Mac mini
50
+ # kind: ssh
51
+ # ssh: me@my-mac-mini.local
52
+ # service: launchd # systemd on Linux (needs passwordless sudo)
53
+ # url: http://my-mac-mini.local:4780
54
+ # start_dirs: ["~/dpi-work"]
55
+
56
+ # An exe.dev VM (https://exe.dev). `just net up` creates it if missing and exposes it over HTTPS.
57
+ # vm-1:
58
+ # name: exe.dev VM
59
+ # kind: exe-dev
60
+ # vm: my-dpi-vm-1 # VM name, also the public host: https://my-dpi-vm-1.exe.xyz
61
+ # create: { cpu: 2, memory: 4GB, disk: 20GB }
62
+ # start_dirs: ["~/work"]