dodo-mcp 1.2.0 → 1.3.0

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 (147) hide show
  1. package/README.md +45 -6
  2. package/dist/cli/dataRecovery.js +38 -0
  3. package/dist/cli/dataRecovery.js.map +1 -0
  4. package/dist/cli/deployment.js +61 -0
  5. package/dist/cli/deployment.js.map +1 -0
  6. package/dist/cli/main.js +35 -1
  7. package/dist/cli/main.js.map +1 -1
  8. package/dist/config/globalConfig.js +8 -0
  9. package/dist/config/globalConfig.js.map +1 -1
  10. package/dist/errors.js +1 -1
  11. package/dist/errors.js.map +1 -1
  12. package/dist/ipc/server.js +2 -0
  13. package/dist/ipc/server.js.map +1 -1
  14. package/dist/ipc/timeouts.js +5 -0
  15. package/dist/ipc/timeouts.js.map +1 -0
  16. package/dist/platform/privateFs.js +32 -3
  17. package/dist/platform/privateFs.js.map +1 -1
  18. package/dist/platform/windowsPrivateAcl.js +90 -0
  19. package/dist/platform/windowsPrivateAcl.js.map +1 -0
  20. package/dist/security/outboundAddress.js +20 -0
  21. package/dist/security/outboundAddress.js.map +1 -0
  22. package/dist/server/aiAdmin.js +35 -10
  23. package/dist/server/aiAdmin.js.map +1 -1
  24. package/dist/server/appServer.js +11 -3
  25. package/dist/server/appServer.js.map +1 -1
  26. package/dist/server/bootstrap.js +14 -0
  27. package/dist/server/bootstrap.js.map +1 -1
  28. package/dist/server/configUi/index.html +3 -0
  29. package/dist/server/configUi/ui/dataRecovery.js +90 -0
  30. package/dist/server/configUi/ui/deployment.js +111 -0
  31. package/dist/server/configUi/ui/recovery.js +167 -0
  32. package/dist/server/configUi/workbench.css +28 -1
  33. package/dist/server/configUi/workbench.js +136 -25
  34. package/dist/server/installationRuntime.js +3 -1
  35. package/dist/server/installationRuntime.js.map +1 -1
  36. package/dist/server/instructions.js +6 -2
  37. package/dist/server/instructions.js.map +1 -1
  38. package/dist/server/ipcDispatch.js +219 -0
  39. package/dist/server/ipcDispatch.js.map +1 -1
  40. package/dist/server/localConfig.js +1 -1
  41. package/dist/server/localConfig.js.map +1 -1
  42. package/dist/server/stdioServer.js +11 -3
  43. package/dist/server/stdioServer.js.map +1 -1
  44. package/dist/services/ai/network.js +1 -18
  45. package/dist/services/ai/network.js.map +1 -1
  46. package/dist/services/ai/subagents.js +5 -2
  47. package/dist/services/ai/subagents.js.map +1 -1
  48. package/dist/services/assistance/verification.js +43 -10
  49. package/dist/services/assistance/verification.js.map +1 -1
  50. package/dist/services/changes/applier.js +472 -297
  51. package/dist/services/changes/applier.js.map +1 -1
  52. package/dist/services/git/gitService.js +150 -34
  53. package/dist/services/git/gitService.js.map +1 -1
  54. package/dist/services/jobs/jobManager.js +59 -2
  55. package/dist/services/jobs/jobManager.js.map +1 -1
  56. package/dist/services/jobs/spool.js +9 -3
  57. package/dist/services/jobs/spool.js.map +1 -1
  58. package/dist/services/multimodal/mediaService.js +6 -6
  59. package/dist/services/multimodal/mediaService.js.map +1 -1
  60. package/dist/services/recovery/configKeys.js +115 -0
  61. package/dist/services/recovery/configKeys.js.map +1 -0
  62. package/dist/services/recovery/configVault.js +361 -0
  63. package/dist/services/recovery/configVault.js.map +1 -0
  64. package/dist/services/recovery/contracts.js +24 -0
  65. package/dist/services/recovery/contracts.js.map +1 -0
  66. package/dist/services/recovery/databaseAwareness.js +179 -0
  67. package/dist/services/recovery/databaseAwareness.js.map +1 -0
  68. package/dist/services/recovery/deploymentArchive.js +129 -0
  69. package/dist/services/recovery/deploymentArchive.js.map +1 -0
  70. package/dist/services/recovery/deploymentContracts.js +70 -0
  71. package/dist/services/recovery/deploymentContracts.js.map +1 -0
  72. package/dist/services/recovery/deploymentHealth.js +143 -0
  73. package/dist/services/recovery/deploymentHealth.js.map +1 -0
  74. package/dist/services/recovery/deploymentMaintenance.js +197 -0
  75. package/dist/services/recovery/deploymentMaintenance.js.map +1 -0
  76. package/dist/services/recovery/deploymentService.js +429 -0
  77. package/dist/services/recovery/deploymentService.js.map +1 -0
  78. package/dist/services/recovery/deploymentSource.js +36 -0
  79. package/dist/services/recovery/deploymentSource.js.map +1 -0
  80. package/dist/services/recovery/dockerDeployment.js +172 -0
  81. package/dist/services/recovery/dockerDeployment.js.map +1 -0
  82. package/dist/services/recovery/drift.js +178 -0
  83. package/dist/services/recovery/drift.js.map +1 -0
  84. package/dist/services/recovery/evidence.js +127 -0
  85. package/dist/services/recovery/evidence.js.map +1 -0
  86. package/dist/services/recovery/gitCopies.js +219 -0
  87. package/dist/services/recovery/gitCopies.js.map +1 -0
  88. package/dist/services/recovery/history.js +423 -0
  89. package/dist/services/recovery/history.js.map +1 -0
  90. package/dist/services/recovery/recoveryService.js +521 -0
  91. package/dist/services/recovery/recoveryService.js.map +1 -0
  92. package/dist/services/recovery/storage.js +344 -0
  93. package/dist/services/recovery/storage.js.map +1 -0
  94. package/dist/services/schedules/scheduleService.js +9 -10
  95. package/dist/services/schedules/scheduleService.js.map +1 -1
  96. package/dist/store/db.js +71 -0
  97. package/dist/store/db.js.map +1 -1
  98. package/dist/tools/catalog.js +4 -0
  99. package/dist/tools/catalog.js.map +1 -1
  100. package/dist/tools/changeTools.js +1 -1
  101. package/dist/tools/changeTools.js.map +1 -1
  102. package/dist/tools/context.js +23 -3
  103. package/dist/tools/context.js.map +1 -1
  104. package/dist/tools/deploymentTools.js +58 -0
  105. package/dist/tools/deploymentTools.js.map +1 -0
  106. package/dist/tools/directTools.js +9 -5
  107. package/dist/tools/directTools.js.map +1 -1
  108. package/dist/tools/gitTools.js +7 -2
  109. package/dist/tools/gitTools.js.map +1 -1
  110. package/dist/tools/jobTools.js +4 -4
  111. package/dist/tools/jobTools.js.map +1 -1
  112. package/dist/tools/projectTools.js +1 -0
  113. package/dist/tools/projectTools.js.map +1 -1
  114. package/dist/tools/recoveryTools.js +79 -0
  115. package/dist/tools/recoveryTools.js.map +1 -0
  116. package/dist/tools/runtimeTools.js +1 -1
  117. package/dist/tools/runtimeTools.js.map +1 -1
  118. package/dist/tools/surface.js +101 -41
  119. package/dist/tools/surface.js.map +1 -1
  120. package/dist/tunnel/credentials.js +1 -1
  121. package/dist/tunnel/credentials.js.map +1 -1
  122. package/docs/AI_PROVIDERS.md +6 -3
  123. package/docs/ARCHITECTURE.md +88 -4
  124. package/docs/CI.md +118 -0
  125. package/docs/COMPATIBILITY.md +7 -6
  126. package/docs/EVALUATION.md +17 -12
  127. package/docs/MANUAL_ACCEPTANCE.md +59 -2
  128. package/docs/RECOVERY.md +448 -0
  129. package/docs/RELEASE_1.2.1.md +47 -0
  130. package/docs/RELEASE_1.3.0.md +64 -0
  131. package/docs/RELEASE_NOTES.md +41 -0
  132. package/docs/SECURITY.md +122 -0
  133. package/docs/TEST_REPORT.md +327 -5
  134. package/docs/WEB_CLIENTS.md +6 -1
  135. package/docs/adr/051-source-recovery-foundation.md +45 -0
  136. package/docs/adr/052-reviewed-source-restore.md +50 -0
  137. package/docs/adr/053-content-drift-and-private-git-recovery.md +29 -0
  138. package/docs/adr/054-verified-source-checkpoints.md +58 -0
  139. package/docs/adr/055-reviewed-docker-deployments.md +42 -0
  140. package/docs/adr/056-owner-data-recovery.md +47 -0
  141. package/docs/adr/README.md +7 -0
  142. package/native/private-state-windows.cs +105 -0
  143. package/package.json +7 -1
  144. package/schemas/global-config.schema.json +44 -0
  145. package/schemas/tools.compact.json +175 -15
  146. package/schemas/tools.hybrid.json +350 -16
  147. package/schemas/tools.json +4883 -816
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - MCP ผ่าน HTTP ที่ `127.0.0.1:21730/mcp` พร้อม OAuth และ PKCE
8
8
  - Local Config แบบ loopback ที่ `127.0.0.1:21731` และ Remote Config ชั่วคราวผ่าน tunnel เมื่อเจ้าของสั่ง `dodo --web`
9
9
  - HTTP ใช้ Compact Tool Surface 20 tools เพื่อลดภาระการโหลด schema
10
- - STDIO ใช้ Full Tool Surface; มี capability definitions ทั้งหมด 138 รายการ แต่ค่าเริ่มต้นซ่อน Sub-agent 4 operations จาก MCP จึงเห็น 134 tools
10
+ - STDIO ใช้ Full Tool Surface; มี capability definitions ทั้งหมด 158 รายการ แต่ค่าเริ่มต้นซ่อน Sub-agent 4 operations จาก MCP จึงเห็น 154 tools
11
11
  - Hybrid Surface 49 tools สำหรับ client ที่รับ catalog ขนาดกลาง
12
12
  - อ่าน ค้นหา สร้าง แก้ ย้าย ลบไฟล์ พร้อม expected hash, journal และ rollback
13
13
  - รันคำสั่ง งานแบบขนาน jobs, Git, TypeScript/JavaScript intelligence, LSP และ task assistance
@@ -162,6 +162,11 @@ Chat & Tasks · Runs & Jobs · Approvals · Knowledge · Settings) หน้า
162
162
  ไม่มี CDN, CSP `'self'` เท่าเดิม) การลบ/ยกเลิก/เปลี่ยนโหมดต้องยืนยันก่อนเสมอ
163
163
  และงานที่มีค่าใช้จ่ายหรือผลไม่แน่นอนจะบอกชัดว่า "ไม่ retry อัตโนมัติ"
164
164
 
165
+ การสร้าง Agent ใช้หน้า **Providers & Profiles**: เลือกประเภท Coding/Review/Research,
166
+ Provider connection และ Model ID แล้วกด **สร้าง Agent** รายชื่อโมเดลที่โหลดจาก
167
+ Provider จะเข้าแบบฟอร์มให้อัตโนมัติ ส่วน permission, tool calling, token limits และราคา
168
+ อยู่ในหัวข้อ **ตั้งค่าขั้นสูง** หาก endpoint ไม่รองรับการลิสต์โมเดลยังกรอก Model ID เองได้
169
+
165
170
  หน้า **Settings → Sub-agent tools ใน MCP** มีสวิตช์เปิด/ปิดการ expose
166
171
  `subagent_spawn`, `subagent_status`, `subagent_result` และ `subagent_control` ให้ AI
167
172
  ภายนอก ค่าเริ่มต้นปิดเพื่อให้ catalog สำหรับงานทั่วไปกระชับขึ้น แต่หน้า Chat & Tasks
@@ -169,6 +174,13 @@ Chat & Tasks · Runs & Jobs · Approvals · Knowledge · Settings) หน้า
169
174
  สร้าง MCP app ใหม่ตามพฤติกรรมของ client การตั้งค่านี้เปลี่ยนเฉพาะการมองเห็น tools
170
175
  และไม่เพิ่ม OAuth scope, trust, approval หรือสิทธิ์ของ profile
171
176
 
177
+ หน้า **Settings → Tools ที่ AI มองเห็น** เลือกเปิด/ปิดแต่ละ operation ได้ พร้อมค้นหา
178
+ และเปิด/ปิดทั้งหมวด ค่าใช้กับ Compact/Hybrid: operation ที่ปิดจะหายจาก
179
+ `dodo_discover`, gateway enum และ direct duplicate ใน Hybrid หากปิดครบทั้งหมวด gateway
180
+ นั้นจะไม่ถูกส่งให้ client จึงลดทั้งจำนวน schema และ context ที่ AI ต้องอ่าน Full/STDIO
181
+ ยังคง definitions รายตัวครบตามเดิม เมื่อบันทึกต้อง restart DODO แล้ว rescan หรือสร้าง
182
+ MCP app ใหม่ การตั้งค่านี้เป็น visibility เท่านั้นและไม่ grant สิทธิ์ใด ๆ
183
+
172
184
  ### เพิ่มโปรเจกต์ครั้งเดียว แล้วสั่งงานด้วยชื่อ
173
185
 
174
186
  เพิ่มโปรเจกต์จากหน้า Projects (หรือ CLI) โดยกรอกแค่ **ชื่อ**, **absolute path** และ
@@ -470,6 +482,10 @@ dodo_read(operation="read_files", args={...})
470
482
 
471
483
  `dodo_discover` คืน schema ของ operation ที่เลือกและ hash ที่ deterministic ส่วน gateway จะส่ง request ผ่าน policy pipeline เดียวกับ tool รายตัว จึงยังตรวจ scope, ACL, workspace ID/epoch, trust, approval, path guard, secret guard, hash และ audit ครบทุกชั้น
472
484
 
485
+ เจ้าของลดรายการที่ AI เห็นได้จาก **Local Config → Settings → Tools ที่ AI มองเห็น**
486
+ โดยไม่แตะ permission เมื่อปิด operation แล้ว schema ของ Compact/Hybrid จะตัดรายการนั้น
487
+ ออกจริงหลัง restart/rescan ส่วน Full surface ไม่เปลี่ยน
488
+
473
489
  ชื่อ gateway `dodo_*` เป็นชื่อ MCP protocol ที่คงไว้เพื่อ compatibility กับ client ที่เชื่อมต่ออยู่ การเปลี่ยนชื่อ gateway ต้องทำเป็น protocol migration แยกต่างหาก
474
490
 
475
491
  ## การเปลี่ยน workspace
@@ -551,19 +567,23 @@ dodo desktop allow --app com.google.Chrome --mode control --persist --yes
551
567
 
552
568
  ## Development
553
569
 
554
- Platform CI ใช้ dedicated self-hosted runners ที่เจ้าของควบคุม: Linux X64 รันผ่าน
555
- Docker พร้อม Playwright Chromium และ Windows X64 รัน native candidate gate ส่วน
570
+ Platform CI ใช้ dedicated self-hosted runners ที่เจ้าของควบคุม: Linux X64 และ
571
+ Windows X64 รัน native candidate gate บนเครื่อง runner โดยตรง ไม่ใช้ Docker ส่วน
556
572
  macOS ยังรัน release gate บนเครื่องพัฒนา Workflow รับเฉพาะ push ที่ `main` และ manual
557
573
  dispatch ไม่รัน pull request จากภายนอกบน self-hosted runner
558
574
 
559
- รัน regression benchmark, macOS candidate gate และ Linux Docker gate ได้ด้วย:
575
+ รัน regression benchmark และ macOS candidate gate ได้ด้วย:
560
576
 
561
577
  ```bash
562
578
  npm run bench
563
579
  npm run release:gate
564
- npm run test:linux:docker
565
580
  ```
566
581
 
582
+ สั่ง Linux/Windows CI ผ่าน GitHub Actions → `platform-gates` → Run workflow →
583
+ เลือก `all` หรือใช้ `gh workflow run platform-gates.yml --ref main -f platform=all`
584
+ Linux รันเมื่อสั่ง workflow; Windows รันทั้งเมื่อ push ไป main และเมื่อสั่ง workflow
585
+ อ่าน [การเตรียม runner และวิธีแก้ CI](docs/CI.md) ก่อนรันครั้งแรก
586
+
567
587
  GitHub workflow รัน Node 22 และ 24 ทั้ง Linux/Windows โดยไม่ publish npm Windows จะ
568
588
  ยังไม่ถูกประกาศเป็น supported platform จนกว่า automated native gate และ manual
569
589
  Windows 11 acceptance จะผ่านตามเอกสาร
@@ -572,8 +592,9 @@ Release gate สร้างหลักฐาน non-secret ใน ignored `rel
572
592
  exact-tarball ผ่าน STDIO Full กับ HTTP/OAuth Compact โดยไม่ publish npm ดูรายละเอียดที่
573
593
  [DodoBench และ Release Gate](docs/EVALUATION.md)
574
594
 
575
- เมื่อทั้งสอง report มาจาก Git revision และ `package-lock.json` เดียวกัน ให้รวมหลักฐาน
595
+ เมื่อทั้งสอง report มาจาก clean Git revision, source fingerprint และ `package-lock.json` เดียวกัน ให้รวมหลักฐาน
576
596
  บน macOS ด้วย `node scripts/release-gate.mjs --release --platform-evidence /absolute/path/to/linux/gate-report.json`
597
+ Linux report ต้องมาจาก native GitHub Actions; หลักฐาน Docker เก่าไม่แทน CI รอบใหม่
577
598
  คำสั่งนี้ไม่ publish package
578
599
 
579
600
  ```bash
@@ -586,3 +607,21 @@ npm pack
586
607
  ```
587
608
 
588
609
  โปรเจกต์นี้ใช้ GitHub repository [arthittakun/dodo-mcp](https://github.com/arthittakun/dodo-mcp) และ package `dodo-mcp`
610
+
611
+ ### Recovery — สำรองและกู้คืนงาน
612
+
613
+ โปรเจกต์ที่ลงทะเบียนเปิด source backup เป็นค่าเริ่มต้น ก่อนแก้ไฟล์หรือรันคำสั่ง
614
+ หน้า **Projects → Recovery** ใช้ตรวจประวัติ เปรียบเทียบ และยืนยันแผนกู้คืนได้จริง
615
+ หรือใช้ `dodo recovery --help` ผ่าน terminal สำเนาอยู่ใน private state แยกจาก Git
616
+ ของโปรเจกต์ และไม่ push ขึ้น GitHub อัตโนมัติ
617
+
618
+ - กู้คืน checkpoint หรือ session ที่เลือก พร้อม expected hashes, สำเนาก่อนกู้ และ receipt หลัง reconnect
619
+ - ตรวจไฟล์ที่ถูกแก้จากภายนอก และแยก `SAVED`, `VERIFIED`, `FAILED`, `INCONCLUSIVE`, `STALE` ให้เห็นตามหลักฐาน
620
+ - ตั้งชื่อ/pin จุดกู้คืน และตรวจ retention ก่อนล้าง โดยรักษางานที่ไม่เกี่ยวข้อง
621
+ - เพิ่ม Docker deployment target ได้โดยเจ้าของ: source ที่ตรวจแล้ว → image → health/stabilization → known-good พร้อม reviewed rollback
622
+ - ตรวจ SQLite migration compatibility แบบอ่านอย่างเดียว และสำรอง private config แบบเข้ารหัสเมื่อเจ้าของเปิดแยก
623
+
624
+ **Source restore ไม่ย้อนฐานข้อมูล, secrets, volumes หรือระบบภายนอก** การสำรอง config
625
+ ใช้กุญแจจาก OS credential store และไม่ส่งเนื้อหาให้ AI; ไม่มี generic database rollback
626
+ หรือการ restart service อัตโนมัติ ดู [คู่มือ Recovery](docs/RECOVERY.md),
627
+ [การเปลี่ยนแปลง 1.3.0](docs/RELEASE_1.3.0.md) และ [ผลตรวจรับจริง](docs/TEST_REPORT.md)
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import { DodoError } from '../errors.js';
3
+ export function registerDataRecoveryCommands(recovery, ipc) {
4
+ const database = recovery.command('database').description('owner-only read-only migration compatibility; never database rollback');
5
+ const print = (value) => console.log(JSON.stringify(value, null, 2));
6
+ database.command('targets').action(async () => print(await ipc('recovery.database.list')));
7
+ const review = (cmd) => cmd.requiredOption('--workspace <id>', 'current workspace ID').requiredOption('--epoch <epoch>', 'current epoch').option('--yes', 'confirm the exact owner operation', false);
8
+ const envelope = (opts, input) => {
9
+ if (!opts.yes)
10
+ throw new DodoError('INVALID_INPUT', 'review the owner operation and add --yes');
11
+ return { workspaceId: opts.workspace, workspaceEpoch: opts.epoch, confirm: true, input };
12
+ };
13
+ review(database.command('inspect <targetId>')).action(async (targetId, opts) => print(await ipc('recovery.database.inspect', envelope(opts, { targetId }))));
14
+ const jsonOperation = (group, operation, method) => review(group.command(operation).requiredOption('--file <path>', 'owner-reviewed bounded JSON; never secret values'))
15
+ .action(async (opts) => {
16
+ const st = fs.lstatSync(opts.file);
17
+ if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > 128 * 1024)
18
+ throw new DodoError('INVALID_INPUT', 'expected a bounded regular JSON file');
19
+ let input;
20
+ try {
21
+ input = JSON.parse(fs.readFileSync(opts.file, 'utf8'));
22
+ }
23
+ catch {
24
+ throw new DodoError('INVALID_INPUT', 'invalid owner JSON');
25
+ }
26
+ print(await ipc(method, envelope(opts, input)));
27
+ });
28
+ for (const op of ['configure', 'bind', 'unbind'])
29
+ jsonOperation(database, op, 'recovery.database.' + op);
30
+ const vault = recovery.command('private-config').description('owner-only encrypted secret-file recovery; explicit opt-in and OS key store required');
31
+ vault.command('list').action(async () => print(await ipc('recovery.config.list')));
32
+ jsonOperation(vault, 'configure', 'recovery.config.configure');
33
+ review(vault.command('backup <targetId>')).action(async (targetId, opts) => print(await ipc('recovery.config.backup', envelope(opts, { targetId }))));
34
+ review(vault.command('preview <backupId>')).action(async (backupId, opts) => print(await ipc('recovery.config.preview', envelope(opts, { backupId }))));
35
+ review(vault.command('apply <planId>').requiredOption('--hash <hash>', 'exact reviewed plan hash')).action(async (planId, opts) => print(await ipc('recovery.config.apply', envelope(opts, { planId, planHash: opts.hash }))));
36
+ review(vault.command('rotate <targetId>').requiredOption('--revision <n>', 'current target revision', Number)).action(async (targetId, opts) => print(await ipc('recovery.config.rotate', envelope(opts, { targetId, expectedRevision: opts.revision }))));
37
+ }
38
+ //# sourceMappingURL=dataRecovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataRecovery.js","sourceRoot":"","sources":["../../src/cli/dataRecovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import { DodoError } from '../errors.js';
3
+ /** Private owner IPC only; reviewed files are bounded JSON, never executable repository configuration. */
4
+ export function registerDeploymentCommands(program, ipc) {
5
+ const group = program.command('deployment').description('review registered Docker deployment targets and durable build/deploy outcomes');
6
+ const print = (value) => console.log(JSON.stringify(value, null, 2));
7
+ for (const op of ['targets', 'list'])
8
+ group.command(op).action(async () => print(await ipc(`deployment.${op}`)));
9
+ for (const op of ['inspect', 'compare'])
10
+ group.command(`${op} <deploymentId>`).action(async (deploymentId) => print(await ipc(`deployment.${op}`, { deploymentId })));
11
+ const reviewed = (cmd) => cmd.requiredOption('--workspace <id>', 'reviewed workspace ID')
12
+ .requiredOption('--epoch <epoch>', 'reviewed workspace epoch').option('--yes', 'confirm the exact reviewed operation', false);
13
+ const envelope = (opts, input) => {
14
+ if (!opts.yes)
15
+ throw new DodoError('INVALID_INPUT', 'review target/source and add --yes; Docker commands may affect resources outside the workspace');
16
+ return { workspaceId: opts.workspace, workspaceEpoch: opts.epoch, confirm: true, input };
17
+ };
18
+ reviewed(group.command('configure').requiredOption('--file <path>', 'owner-reviewed target JSON: expectedRevision, enabled, definition, confirmDaemonAccess'))
19
+ .action(async (opts) => {
20
+ const st = fs.lstatSync(opts.file);
21
+ if (!st.isFile() || st.isSymbolicLink() || st.size > 128 * 1024)
22
+ throw new DodoError('INVALID_INPUT', 'target configuration must be a regular JSON file at most 128 KiB');
23
+ let input;
24
+ try {
25
+ input = JSON.parse(fs.readFileSync(opts.file, 'utf8'));
26
+ }
27
+ catch {
28
+ throw new DodoError('INVALID_INPUT', 'invalid target JSON');
29
+ }
30
+ print(await ipc('deployment.configure', envelope(opts, input)));
31
+ });
32
+ reviewed(group.command('prepare').requiredOption('--target <id>', 'owner-registered target ID').requiredOption('--revision <n>', 'target revision')
33
+ .requiredOption('--verification <id>', 'current VERIFIED verification ID').requiredOption('--key <key>', 'idempotency key; reuse for retries'))
34
+ .action(async (opts) => print(await ipc('deployment.prepare', envelope(opts, {
35
+ targetId: opts.target, expectedTargetRevision: Number(opts.revision), verificationId: opts.verification, idempotencyKey: opts.key,
36
+ }))));
37
+ reviewed(group.command('build <deploymentId>').requiredOption('--hash <hash>', 'reviewed plan hash'))
38
+ .action(async (deploymentId, opts) => print(await ipc('deployment.build', envelope(opts, { deploymentId, planHash: opts.hash }))));
39
+ reviewed(group.command('apply <deploymentId>').requiredOption('--hash <hash>', 'reviewed plan hash').requiredOption('--image <digest>', 'reviewed immutable image digest'))
40
+ .action(async (deploymentId, opts) => print(await ipc('deployment.apply', envelope(opts, { deploymentId, planHash: opts.hash, imageDigest: opts.image }))));
41
+ for (const op of ['observe', 'source-preview'])
42
+ reviewed(group.command(`${op} <deploymentId>`))
43
+ .action(async (deploymentId, opts) => print(await ipc('deployment.' + op.replaceAll('-', '_'), envelope(opts, { deploymentId }))));
44
+ reviewed(group.command('maintenance').requiredOption('--file <path>', 'owner-reviewed maintenance JSON; preview returns a review ID/hash for apply'))
45
+ .action(async (opts) => {
46
+ const st = fs.lstatSync(opts.file);
47
+ if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > 65536)
48
+ throw new DodoError('INVALID_INPUT', 'maintenance JSON must be a bounded regular file');
49
+ let input;
50
+ try {
51
+ input = JSON.parse(fs.readFileSync(opts.file, 'utf8'));
52
+ }
53
+ catch {
54
+ throw new DodoError('INVALID_INPUT', 'invalid maintenance JSON');
55
+ }
56
+ print(await ipc('deployment.maintenance', envelope(opts, input)));
57
+ });
58
+ reviewed(group.command('rollback-prepare <deploymentId>').requiredOption('--key <key>', 'idempotency key for this rollback review'))
59
+ .action(async (deploymentId, opts) => print(await ipc('deployment.rollback_prepare', envelope(opts, { deploymentId, idempotencyKey: opts.key }))));
60
+ }
61
+ //# sourceMappingURL=deployment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployment.js","sourceRoot":"","sources":["../../src/cli/deployment.ts"],"names":[],"mappings":"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"}
package/dist/cli/main.js CHANGED
@@ -7,6 +7,8 @@
7
7
  const invokedCwd = process.cwd();
8
8
  import fs from 'node:fs';
9
9
  import { Command } from 'commander';
10
+ import { registerDataRecoveryCommands } from './dataRecovery.js';
11
+ import { registerDeploymentCommands } from './deployment.js';
10
12
  import { startServer, DODO_VERSION } from '../server/appServer.js';
11
13
  import { startStdioServer } from '../server/stdioServer.js';
12
14
  import { resolveConfigDir, ensureConfigDir, statePaths, ipcSocketPath } from '../config/paths.js';
@@ -16,6 +18,7 @@ import { mintWorkspaceId } from '../workspace/identity.js';
16
18
  import { openDatabase, openDatabaseReadonly } from '../store/db.js';
17
19
  import { Store } from '../store/store.js';
18
20
  import { ipcCall, IpcError } from '../ipc/client.js';
21
+ import { ownerCommandTimeout } from '../ipc/timeouts.js';
19
22
  import { installationIpcCall } from '../ipc/installationClient.js';
20
23
  import { formatTerminalLine } from './terminal.js';
21
24
  import { TRUST_MODE_DESCRIPTIONS } from '../security/policy.js';
@@ -31,6 +34,7 @@ import { runCliMenu } from './menu.js';
31
34
  import { TunnelRuntime } from '../tunnel/runtime.js';
32
35
  import { INSTALLATION_AUTHORITY_ID } from '../auth/constants.js';
33
36
  const program = new Command();
37
+ registerDeploymentCommands(program, ipcForCwd);
34
38
  program.name('dodo').description('DODO — local-first, single-owner, project-scoped coding MCP server').version(DODO_VERSION);
35
39
  function fail(message, code = 1) {
36
40
  console.error(`dodo: ${message}`);
@@ -61,7 +65,7 @@ function workspaceIpcPath(root) {
61
65
  }
62
66
  async function ipcForCwd(cmd, args = {}) {
63
67
  const { ipcPath } = workspaceIpcPath();
64
- return ipcCall(ipcPath, cmd, args);
68
+ return ipcCall(ipcPath, cmd, args, ownerCommandTimeout(cmd));
65
69
  }
66
70
  /** OAuth identity belongs to the installation, not the shell's CWD. */
67
71
  async function ipcForInstallation(cmd, args = {}) {
@@ -1268,6 +1272,36 @@ auth
1268
1272
  throw err;
1269
1273
  }
1270
1274
  });
1275
+ // -------------------------------------------------------------- recovery ---
1276
+ const recovery = program.command('recovery').description('inspect or configure automatic source backups for the current registered project');
1277
+ registerDataRecoveryCommands(recovery, ipcForCwd);
1278
+ recovery.command('status').action(async () => console.log(JSON.stringify(await ipcForCwd('recovery.status'), null, 2)));
1279
+ recovery.command('scan').option('--cursor <n>', 'page offset', '0').option('--limit <n>', 'page size (1–100)', '50').action(async (opts) => console.log(JSON.stringify(await ipcForCwd('recovery.drift.scan', { cursor: Number(opts.cursor), limit: Number(opts.limit) }), null, 2)));
1280
+ recovery.command('acknowledge <digest>').requiredOption('--workspace <id>', 'reviewed workspace ID').requiredOption('--epoch <epoch>', 'reviewed workspace epoch').option('--yes', 'accept exactly the reviewed observed state', false)
1281
+ .action(async (digest, opts) => { if (!opts.yes)
1282
+ throw new DodoError('INVALID_INPUT', 'review drift then add --yes'); console.log(JSON.stringify(await ipcForCwd('recovery.drift.acknowledge', { digest, workspaceId: opts.workspace, workspaceEpoch: opts.epoch, confirm: true }), null, 2)); });
1283
+ recovery.command('evidence').option('--id <verificationId>', 'refresh evidence for a specific verification').action(async (opts) => console.log(JSON.stringify(await ipcForCwd(opts.id ? 'recovery.evidence.inspect' : 'recovery.evidence.list', opts.id ? { verificationId: opts.id } : {}), null, 2)));
1284
+ recovery.command('cleanup-preview').action(async () => console.log(JSON.stringify(await ipcForCwd('recovery.cleanup.preview'), null, 2)));
1285
+ recovery.command('mark <name> <checkpointId>').requiredOption('--revision <n>', 'reviewed pointer revision; 0 for a new name').requiredOption('--workspace <id>', 'reviewed workspace ID').requiredOption('--epoch <epoch>', 'reviewed epoch').option('--yes', 'confirm owner label (does not certify tests)', false)
1286
+ .action(async (name, checkpointId, opts) => { if (!opts.yes)
1287
+ throw new DodoError('INVALID_INPUT', 'review checkpoint and add --yes'); console.log(JSON.stringify(await ipcForCwd('recovery.mark', { name, snapshotId: checkpointId, expectedRevision: Number(opts.revision), workspaceId: opts.workspace, workspaceEpoch: opts.epoch, confirm: true }), null, 2)); });
1288
+ recovery.command('checkpoint').action(async () => console.log(JSON.stringify(await ipcForCwd('recovery.checkpoint'), null, 2)));
1289
+ recovery.command('list').option('--sessions', 'list sessions instead of checkpoints', false).action(async (opts) => console.log(JSON.stringify(await ipcForCwd(opts.sessions ? 'recovery.recovery_session_list' : 'recovery.checkpoint_list'), null, 2)));
1290
+ recovery.command('preview <id>').option('--session', 'undo caller session receipts', false).option('--path <relative>', 'select a file or directory').option('--exact-mirror', 'explicitly include extra source-file deletions', false)
1291
+ .action(async (id, opts) => console.log(JSON.stringify(await ipcForCwd('recovery.restore_preview', { ...(opts.session ? { sessionId: id } : { checkpointId: id }), ...(opts.path ? { paths: [opts.path] } : {}), exactMirror: opts.exactMirror }), null, 2)));
1292
+ recovery.command('apply <planId>').requiredOption('--hash <hash>', 'exact reviewed planHash').requiredOption('--key <key>', 'durable idempotency key; reuse for retries').requiredOption('--workspace <id>', 'reviewed workspace ID').requiredOption('--epoch <epoch>', 'reviewed workspace epoch').option('--yes', 'confirm exactly this reviewed plan', false)
1293
+ .action(async (planId, opts) => {
1294
+ if (!opts.yes)
1295
+ fail('review the preview, then pass --yes with the same plan/hash and workspace context');
1296
+ console.log(JSON.stringify(await ipcForCwd('recovery.restore_apply', { planId, planHash: opts.hash, idempotencyKey: opts.key, workspaceId: opts.workspace, workspaceEpoch: opts.epoch, confirm: true }), null, 2));
1297
+ });
1298
+ recovery.command('configure').option('--enabled <value>', 'true or false').option('--yes', 'confirm the owner policy change', false)
1299
+ .action(async (opts) => {
1300
+ if (!opts.yes || !['true', 'false'].includes(opts.enabled ?? ''))
1301
+ fail('use --enabled true|false --yes to confirm');
1302
+ const current = await ipcForCwd('recovery.status');
1303
+ console.log(JSON.stringify(await ipcForCwd('recovery.configure', { policy: { ...current.policy, enabled: opts.enabled === 'true' }, confirm: true }), null, 2));
1304
+ });
1271
1305
  // --------------------------------------------------------------- recover ---
1272
1306
  program
1273
1307
  .command('recover')