dodo-mcp 1.0.1 → 1.0.3

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 (49) hide show
  1. package/README.md +49 -19
  2. package/dist/cli/main.js +108 -43
  3. package/dist/cli/main.js.map +1 -1
  4. package/dist/cli/menu.js +5 -6
  5. package/dist/cli/menu.js.map +1 -1
  6. package/dist/config/globalConfig.js +23 -1
  7. package/dist/config/globalConfig.js.map +1 -1
  8. package/dist/config/stateImport.js +2 -2
  9. package/dist/config/stateImport.js.map +1 -1
  10. package/dist/config/tunnelConfig.js +2 -3
  11. package/dist/config/tunnelConfig.js.map +1 -1
  12. package/dist/server/appServer.js +89 -9
  13. package/dist/server/appServer.js.map +1 -1
  14. package/dist/server/configUi/app.js +54 -65
  15. package/dist/server/configUi/index.html +21 -22
  16. package/dist/server/configUi/workbench.js +7 -4
  17. package/dist/server/ipcDispatch.js +25 -0
  18. package/dist/server/ipcDispatch.js.map +1 -1
  19. package/dist/server/localConfig.js +50 -63
  20. package/dist/server/localConfig.js.map +1 -1
  21. package/dist/server/remoteConfig.js +317 -0
  22. package/dist/server/remoteConfig.js.map +1 -0
  23. package/dist/tunnel/control.js +4 -6
  24. package/dist/tunnel/control.js.map +1 -1
  25. package/dist/tunnel/credentials.js +5 -19
  26. package/dist/tunnel/credentials.js.map +1 -1
  27. package/dist/tunnel/runtime.js +3 -4
  28. package/dist/tunnel/runtime.js.map +1 -1
  29. package/dist/tunnel/supervisor.js +8 -10
  30. package/dist/tunnel/supervisor.js.map +1 -1
  31. package/docs/AI_PROVIDERS.md +1 -1
  32. package/docs/ARCHITECTURE.md +29 -14
  33. package/docs/COMPATIBILITY.md +7 -3
  34. package/docs/MANUAL_ACCEPTANCE.md +39 -10
  35. package/docs/RELEASE_1.0.0.md +3 -0
  36. package/docs/RELEASE_1.0.2.md +46 -0
  37. package/docs/RELEASE_1.0.3.md +57 -0
  38. package/docs/RELEASE_NOTES.md +37 -0
  39. package/docs/SECURITY.md +39 -13
  40. package/docs/TEST_REPORT.md +61 -2
  41. package/docs/TUNNEL.md +114 -53
  42. package/docs/WINDOWS.md +1 -1
  43. package/docs/adr/031-owner-selected-tunnel-supervision.md +8 -2
  44. package/docs/adr/044-global-launcher-and-cli-menu.md +3 -3
  45. package/docs/adr/047-temporary-remote-config.md +47 -0
  46. package/docs/adr/048-persistent-connection-mode.md +58 -0
  47. package/docs/adr/README.md +2 -0
  48. package/package.json +3 -1
  49. package/schemas/global-config.schema.json +6 -12
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # DODO MCP
2
2
 
3
- **DODO MCP 1.0.1** คือ MCP server แบบ local-first สำหรับให้ AI ช่วยพัฒนา software โดยทำงานกับ workspace ที่เจ้าของเลือก ค่าเริ่มต้นเป็นโหมดส่วนตัวแบบเพิ่มโปรเจกต์แล้วใช้ได้ทันที และยังมีโหมด managed สำหรับแยก workspace ACL/trust แบบละเอียด
3
+ **DODO MCP 1.0.3** คือ MCP server แบบ local-first สำหรับให้ AI ช่วยพัฒนา software โดยทำงานกับ workspace ที่เจ้าของเลือก ค่าเริ่มต้นเป็นโหมดส่วนตัวแบบเพิ่มโปรเจกต์แล้วใช้ได้ทันที และยังมีโหมด managed สำหรับแยก workspace ACL/trust แบบละเอียด
4
4
 
5
5
  ## จุดเด่น
6
6
 
7
7
  - MCP ผ่าน HTTP ที่ `127.0.0.1:21730/mcp` พร้อม OAuth และ PKCE
8
- - Local Config แบบ loopback ที่ `127.0.0.1:21731`
8
+ - Local Config แบบ loopback ที่ `127.0.0.1:21731` และ Remote Config ชั่วคราวผ่าน tunnel เมื่อเจ้าของสั่ง `dodo --web`
9
9
  - HTTP ใช้ Compact Tool Surface 19 tools เพื่อลดภาระการโหลด schema
10
10
  - STDIO ใช้ Full Tool Surface; มี capability definitions ทั้งหมด 125 รายการ แต่ค่าเริ่มต้นซ่อน Sub-agent 4 operations จาก MCP จึงเห็น 121 tools
11
11
  - Hybrid Surface 49 tools สำหรับ client ที่รับ catalog ขนาดกลาง
@@ -36,7 +36,8 @@ dodo --cli
36
36
  ```
37
37
 
38
38
  `dodo --cli` เปิดเมนู local owner สำหรับเลือก/เพิ่มโปรเจกต์ เปิด MCP + Local Config +
39
- Tunnel และรัน setup รวม `cloudflared` โดยไม่ต้อง `cd` เข้าโปรเจกต์
39
+ Tunnel, เปิด Remote Config ชั่วคราว 1 ชั่วโมง และรัน setup รวม `cloudflared` โดยไม่ต้อง
40
+ `cd` เข้าโปรเจกต์
40
41
  ก่อน หากเรียก `dodo` หรือ `dodo start` จากโฟลเดอร์ใดก็ตาม ระบบจะเปิดโปรเจกต์ที่
41
42
  เจ้าของเลือกล่าสุด หากยังไม่มีโปรเจกต์ใน registry จะเปิด control plane, OAuth และ
42
43
  authenticated tool catalog ได้โดยไม่ใช้ CWD เป็น workspace เมื่อเจ้าของเพิ่มโปรเจกต์
@@ -249,27 +250,55 @@ command sandbox, idempotency และ audit เดิม `agent_exec` ใช้
249
250
 
250
251
  ### เชื่อม Remote MCP ผ่าน Cloudflare Tunnel
251
252
 
252
- DODO ไม่สร้าง Tunnel, DNS หรือ Cloudflare account ให้ ผู้ใช้สร้าง remotely-managed Tunnel และตั้ง public hostname ให้ route **ทุก path** มาที่ `http://127.0.0.1:21730` ก่อน จากนั้นตั้ง public origin ครั้งเดียว:
253
+ DODO เลือกวิธีเชื่อมต่อระดับ installation ได้หนึ่งแบบ: `local` หรือ `tunnel` ค่าที่เลือก
254
+ มีผลกับทุก `dodo start` และไม่มี fallback อัตโนมัติ หากเลือก Tunnel แล้ว credential หรือ
255
+ readiness ไม่พร้อม DODO จะหยุด startup แทนการเปิด local โดยไม่แจ้ง
256
+
257
+ Local mode:
258
+
259
+ ```bash
260
+ dodo tunnel configure --local
261
+ dodo start
262
+ ```
263
+
264
+ DODO Tunnel mode:
253
265
 
254
266
  ```bash
255
267
  dodo setup --check --components cloudflared
256
- dodo init --public-url https://mcp.example.com
268
+ dodo tunnel configure \
269
+ --tunnel \
270
+ --public-url https://mcp.example.com \
271
+ --os-credential
257
272
  dodo start
258
- # Cloudflare Tunnel token (temporary; Enter = local only):
259
273
  ```
260
274
 
261
- เมื่อกรอก token DODO จะเริ่ม `cloudflared` เป็น child process ของรอบนั้นและหยุดพร้อม
262
- DODO Token อยู่ในหน่วยความจำระหว่างรอบและ environment ของ child เท่านั้น ไม่อยู่ใน
263
- argv, config, Keychain, Credential Manager, Secret Service, tunnel log, MCP response
264
- หรือ environment ของ MCP jobs กด Enter หรือใช้ `dodo start --no-tunnel` เพื่อเปิด
265
- เฉพาะ local MCP
275
+ เจ้าของสร้าง remotely-managed Tunnel, public hostname และ DNS ใน Cloudflare และ route
276
+ **ทุก path** มาที่ `http://127.0.0.1:21730` DODO ไม่จัดการ Cloudflare account/DNS
277
+ Token ถูกบันทึกใน macOS Keychain, Windows Credential Manager หรือ Linux Secret Service;
278
+ global config เก็บเพียง opaque reference ทุกครั้งที่เปิด DODO ระบบจะเริ่ม
279
+ `cloudflared` เป็น child และหยุด child พร้อม DODO Token ไม่อยู่ใน argv, config, log,
280
+ audit, browser storage, MCP response หรือ environment ของ MCP jobs
281
+
282
+ หากต้องตั้งค่าจากอุปกรณ์อื่น ให้เปิดหน้าเจ้าของแบบชั่วคราวผ่าน public listener เดียวกัน:
283
+
284
+ ```bash
285
+ dodo --web # เริ่ม DODO/Tunnel หรือเปิด lease ใหม่ให้ process ที่รันอยู่
286
+ dodo web --status # ดูสถานะแบบไม่มี secret
287
+ dodo web --close # ปิดหน้า Remote Config แต่ MCP/Tunnel ยังทำงาน
288
+ ```
289
+
290
+ คำสั่งจะแสดง `https://mcp.example.com/config` กับ pairing code แบบใช้ครั้งเดียวใน
291
+ terminal หลังจับคู่ browser จะใช้ cookie ที่เป็น `Secure`, `HttpOnly`,
292
+ `SameSite=Strict` และจำกัด path ที่ `/config` หน้า Remote Config ปิดอัตโนมัติภายใน
293
+ 1 ชั่วโมง การเปิดใหม่ออก code/session ใหม่และไม่ต้อง restart MCP ไม่มี token หรือ
294
+ credential อยู่ใน URL คำสั่งนี้ใช้ได้เฉพาะเมื่อ persistent mode เป็น `tunnel` และ
295
+ DODO-owned Tunnel กำลังทำงาน; มันไม่รับ token ใหม่ผ่าน IPC และไม่เปลี่ยนจาก Local เอง
266
296
 
267
- หน้า Local Config ที่ `127.0.0.1:21731` มีช่อง **Temporary Tunnel token** สำหรับเริ่ม
268
- และหยุด tunnel ของ DODO process ปัจจุบันได้จริง ช่องจะถูกล้างหลังส่งและ backend ไม่
269
- persist ค่า ดูสถานะด้วย `dodo tunnel status`, ตรวจ connectivity ด้วย
270
- `dodo tunnel doctor` และดู log ที่ redacted ด้วย `dodo tunnel logs` คำสั่ง
271
- `dodo tunnel configure` เดิมยังมีสำหรับ advanced/headless compatibility และต้องเรียก
272
- โดยเจ้าของอย่างชัดเจน; เส้นทาง `dodo start` และหน้าเว็บไม่ใช้ credential store
297
+ หน้า Local Config ที่ `127.0.0.1:21731` เลือก Local/Tunnel, ตั้ง public origin และบันทึก
298
+ token แบบ write-only เข้า OS credential store ได้ ค่าใหม่มีผลหลัง restart หน้าเว็บแสดง
299
+ Active MCP URL จาก runtime จริงและไม่กล่าวว่า Tunnel หรือ AI client เชื่อมแล้วหากไม่มี
300
+ หลักฐาน ดูสถานะด้วย `dodo tunnel status`, ตรวจ connectivity ด้วย `dodo tunnel doctor`
301
+ และดู log ที่ redacted ด้วย `dodo tunnel logs`
273
302
 
274
303
  บน macOS `dodo setup --yes` เลือก component `cloudflared` อยู่ในชุด `all` แล้ว
275
304
  Tunnel route ต้องชี้เฉพาะ MCP/OAuth listener `21730` ห้ามชี้ Local Config `21731`
@@ -326,7 +355,8 @@ dodo_read(operation="read_files", args={...})
326
355
 
327
356
  - **Local MCP:** `http://127.0.0.1:21730/mcp` สำหรับ process ในเครื่อง
328
357
  - **Public MCP:** origin HTTPS ของ tunnel ที่เจ้าของสร้างและกำหนด route เอง; DODO อาจ supervise เฉพาะ `cloudflared` process ที่เจ้าของสั่ง
329
- - **Local Config:** loopback เท่านั้น ใช้ owner capability token และ Host/Origin checks
358
+ - **Local Config:** `127.0.0.1:21731` เท่านั้น ใช้ owner capability token และ Host/Origin checks
359
+ - **Remote Config:** ปิดเป็น 404 โดยค่าเริ่มต้น; เจ้าของเปิด `/config` บน public origin ได้ครั้งละไม่เกิน 1 ชั่วโมงด้วย `dodo --web`
330
360
 
331
361
  DODO รายงาน `connected` เฉพาะเมื่อ managed `cloudflared` ตอบ readiness จริง และ `doctor` แยก local/public health ออกจากกัน สถานะนี้ไม่ใช่หลักฐานว่า ChatGPT หรือ AI client เชื่อมต่อแล้ว
332
362
 
@@ -354,7 +384,7 @@ dodo desktop allow --app com.google.Chrome --mode control --persist --yes
354
384
  ## ความปลอดภัย
355
385
 
356
386
  - public MCP ใช้ OAuth เสมอ ไม่มี NoAuth fallback
357
- - Local Config bind loopback และมี capability token, expiration, Host/Origin checks และ rate limit
387
+ - Local Config bind loopback และมี capability token, expiration, Host/Origin checks และ rate limit; Remote Config ต้องจับคู่ด้วย code ใช้ครั้งเดียวและหมดอายุภายใน 1 ชั่วโมง
358
388
  - client secret/access token ไม่แสดงใน UI หรือ audit
359
389
  - path traversal, symlink/hardlink, secret paths และ stale hashes ถูกปฏิเสธ
360
390
  - command environment ใช้ allowlist และไม่ส่ง local auth state ให้ child process
package/dist/cli/main.js CHANGED
@@ -142,13 +142,23 @@ async function launchHttp(opts) {
142
142
  const configDir = resolveConfigDir(process.env).dir;
143
143
  const configFile = statePaths(configDir).configFile;
144
144
  const config = loadGlobalConfig(configFile);
145
- const publicUrl = opts.publicUrl ?? config.publicUrl;
146
- let temporaryTunnelToken;
147
- const interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true;
148
- if (opts.tunnel !== false && config.tunnel.startWithDodo && publicUrl && interactiveTerminal) {
149
- const { readTemporaryTunnelToken } = await import('../tunnel/credentials.js');
150
- temporaryTunnelToken = await readTemporaryTunnelToken();
145
+ const connectionMode = config.tunnel.connectionMode;
146
+ const publicUrl = config.publicUrl;
147
+ const selectedPort = opts.port ?? config.port;
148
+ if (connectionMode === 'tunnel') {
149
+ if (!publicUrl)
150
+ fail('Tunnel mode requires a public HTTPS origin; run dodo tunnel configure --tunnel --os-credential --public-url https://your-host');
151
+ validatePublicUrl(publicUrl, false);
152
+ if (!config.tunnel.credentialRef)
153
+ fail('Tunnel mode requires a saved credential; run dodo tunnel configure --tunnel --os-credential --public-url https://your-host');
154
+ }
155
+ else {
156
+ if (opts.web)
157
+ fail('Remote Config requires Tunnel mode; configure Tunnel, restart DODO, then run dodo --web');
158
+ if (selectedPort === 0)
159
+ fail('local connection mode requires a concrete MCP port');
151
160
  }
161
+ const endpointOrigin = connectionMode === 'tunnel' ? publicUrl : `http://127.0.0.1:${selectedPort}`;
152
162
  const startOpts = {
153
163
  configPort: config.configPort,
154
164
  ...(opts.bypass ? { runMode: 'bypass' } : opts.allow ? { runMode: 'allow-all' } : {}),
@@ -157,11 +167,11 @@ async function launchHttp(opts) {
157
167
  allowUnsafeRoot: opts.allowUnsafeRoot ?? false,
158
168
  quiet: opts.quiet ?? false,
159
169
  onLog: (line) => console.log(formatTerminalLine(line)),
170
+ connectionMode,
171
+ publicUrlOverride: endpointOrigin,
160
172
  };
161
173
  if (opts.port !== undefined && !Number.isNaN(opts.port))
162
174
  startOpts.portOverride = opts.port;
163
- if (opts.publicUrl !== undefined)
164
- startOpts.publicUrlOverride = opts.publicUrl;
165
175
  if (opts.tools !== undefined)
166
176
  startOpts.toolSurface = opts.tools;
167
177
  const tunnelRuntime = new TunnelRuntime(configDir, line => console.log(`[dodo:tunnel] ${line}`));
@@ -175,32 +185,26 @@ async function launchHttp(opts) {
175
185
  void stop('server stop');
176
186
  },
177
187
  });
178
- if (temporaryTunnelToken) {
188
+ if (connectionMode === 'tunnel') {
179
189
  try {
180
- const transientConfig = GlobalConfigSchema.parse({
190
+ const selectedConfig = GlobalConfigSchema.parse({
181
191
  ...config,
182
192
  ...(publicUrl ? { publicUrl } : {}),
183
- tunnel: { ...config.tunnel, mode: 'managed' },
193
+ tunnel: { ...config.tunnel, connectionMode: 'tunnel' },
184
194
  });
185
- const status = await tunnelRuntime.start(transientConfig, temporaryTunnelToken);
186
- console.log(`[dodo] Cloudflare Tunnel attached for this run: ${status.publicOrigin}/mcp`);
195
+ const status = await tunnelRuntime.start(selectedConfig);
196
+ console.log(`[dodo] DODO Tunnel started: ${status.publicOrigin}/mcp`);
197
+ if (opts.web)
198
+ printRemoteConfig(await ipcForInstallation('remoteConfig.open'));
187
199
  }
188
200
  catch (error) {
189
201
  suppressOnStopped = true;
190
202
  await server.close();
191
203
  throw error;
192
204
  }
193
- finally {
194
- temporaryTunnelToken = undefined;
195
- }
196
205
  }
197
- else if (!opts.quiet && opts.tunnel !== false && config.tunnel.startWithDodo) {
198
- if (!publicUrl)
199
- console.log('[dodo] Tunnel skipped: configure the public HTTPS origin, then restart DODO.');
200
- else if (!interactiveTerminal)
201
- console.log('[dodo] Tunnel skipped: temporary token entry requires an interactive terminal.');
202
- else
203
- console.log('[dodo] Tunnel skipped for this run; local MCP remains available.');
206
+ else if (!opts.quiet) {
207
+ console.log(`[dodo] Local connection selected: ${endpointOrigin}/mcp`);
204
208
  }
205
209
  async function stop(signal) {
206
210
  if (shutdownPromise)
@@ -358,9 +362,12 @@ project.command('remove <projectId>')
358
362
  // --------------------------------------------------------------- tunnel ----
359
363
  const tunnel = program.command('tunnel').description('manage or inspect a Cloudflare remotely-managed Tunnel without managing DNS or the Cloudflare account');
360
364
  tunnel.command('configure')
361
- .description('save local tunnel mode and a credential reference; tokens are never written to config')
362
- .option('--managed', 'DODO supervises cloudflared in the foreground', false)
363
- .option('--external', 'an owner/system service manages the tunnel', false)
365
+ .description('select persistent local or DODO-owned Tunnel mode; tokens are never written to config')
366
+ .option('--tunnel', 'select the DODO-owned Tunnel for every dodo start', false)
367
+ .option('--local', 'select local-only MCP for every dodo start', false)
368
+ .option('--managed', 'deprecated alias for --tunnel', false)
369
+ .option('--external', 'deprecated alias for --local', false)
370
+ .option('--public-url <url>', 'public HTTPS origin used by the selected DODO Tunnel')
364
371
  .option('--os-credential', 'prompt through the reviewed OS credential provider', false)
365
372
  .option('--token-env <name>', 'reference an uppercase environment variable that already contains the token')
366
373
  .option('--token-file <absolute-path>', 'reference an owner-private absolute token file')
@@ -372,8 +379,10 @@ tunnel.command('configure')
372
379
  .option('--json', 'machine-readable non-secret result', false)
373
380
  .action(async (opts) => {
374
381
  try {
375
- if (opts.managed && opts.external)
376
- fail('choose --managed or --external');
382
+ const chooseTunnel = opts.tunnel || opts.managed;
383
+ const chooseLocal = opts.local || opts.external;
384
+ if (chooseTunnel && chooseLocal)
385
+ fail('choose --tunnel or --local');
377
386
  const methods = [opts.osCredential, opts.tokenEnv !== undefined, opts.tokenFile !== undefined].filter(Boolean).length;
378
387
  if (methods > 1)
379
388
  fail('choose only one of --os-credential, --token-env or --token-file');
@@ -384,6 +393,16 @@ tunnel.command('configure')
384
393
  const { dir } = resolveConfigDir(process.env);
385
394
  ensureConfigDir(dir);
386
395
  const paths = statePaths(dir), current = loadGlobalConfig(paths.configFile);
396
+ const connectionMode = chooseTunnel ? 'tunnel' : chooseLocal ? 'local' : methods > 0 ? 'tunnel' : current.tunnel.connectionMode;
397
+ const publicUrl = opts.publicUrl !== undefined
398
+ ? validatePublicUrl(opts.publicUrl, false).origin
399
+ : current.publicUrl;
400
+ if (connectionMode === 'tunnel' && !publicUrl)
401
+ fail('Tunnel mode requires --public-url https://your-host (or a previously saved public origin)');
402
+ if (connectionMode === 'tunnel' && publicUrl)
403
+ validatePublicUrl(publicUrl, false);
404
+ if (connectionMode === 'local' && opts.publicUrl !== undefined)
405
+ fail('--public-url is used only with --tunnel');
387
406
  const credentials = await import('../tunnel/credentials.js');
388
407
  const { resolveCloudflared } = await import('../tunnel/supervisor.js');
389
408
  const previousCredentialRef = current.tunnel.credentialRef;
@@ -392,7 +411,7 @@ tunnel.command('configure')
392
411
  credentialRef = undefined;
393
412
  const baseTunnelInput = {
394
413
  ...current.tunnel,
395
- mode: opts.managed ? 'managed' : opts.external ? 'external' : current.tunnel.mode,
414
+ connectionMode,
396
415
  ...(credentialRef ? { credentialRef } : {}),
397
416
  ...(opts.metricsPort !== undefined ? { metricsPort: opts.metricsPort } : {}),
398
417
  ...(opts.maxRestarts !== undefined ? { maxRestarts: opts.maxRestarts } : {}),
@@ -400,7 +419,7 @@ tunnel.command('configure')
400
419
  };
401
420
  if (!credentialRef)
402
421
  delete baseTunnelInput['credentialRef'];
403
- let next = GlobalConfigSchema.parse({ ...current, tunnel: baseTunnelInput });
422
+ let next = GlobalConfigSchema.parse({ ...current, ...(publicUrl ? { publicUrl } : {}), tunnel: baseTunnelInput });
404
423
  if (opts.cloudflared !== undefined) {
405
424
  const canonical = resolveCloudflared(next);
406
425
  next = GlobalConfigSchema.parse({ ...next, tunnel: { ...next.tunnel, executable: canonical } });
@@ -418,19 +437,19 @@ tunnel.command('configure')
418
437
  if (!credentialRef)
419
438
  delete withCredential['credentialRef'];
420
439
  next = GlobalConfigSchema.parse({ ...next, tunnel: withCredential });
421
- if (next.tunnel.mode === 'managed' && !next.tunnel.credentialRef)
422
- fail('managed mode requires --os-credential, --token-env or --token-file');
440
+ if (next.tunnel.connectionMode === 'tunnel' && !next.tunnel.credentialRef)
441
+ fail('Tunnel mode requires --os-credential, --token-env or --token-file');
423
442
  saveGlobalConfig(paths.configFile, next);
424
443
  if (opts.removeCredential && previousCredentialRef)
425
444
  credentials.deleteOsTunnelCredential(previousCredentialRef);
426
- const result = { mode: next.tunnel.mode, credential: next.tunnel.credentialRef ? 'configured' : 'not-configured', metricsPort: next.tunnel.metricsPort, maxRestarts: next.tunnel.maxRestarts, cloudflared: next.tunnel.executable ? 'owner-selected' : 'trusted-PATH', started: false };
445
+ const result = { connectionMode: next.tunnel.connectionMode, publicOrigin: next.tunnel.connectionMode === 'tunnel' ? next.publicUrl : null, credential: next.tunnel.credentialRef ? 'configured' : 'not-configured', metricsPort: next.tunnel.metricsPort, maxRestarts: next.tunnel.maxRestarts, cloudflared: next.tunnel.executable ? 'owner-selected' : 'trusted-PATH', started: false };
427
446
  if (opts.json)
428
447
  console.log(JSON.stringify(result, null, 2));
429
448
  else {
430
- console.log(`Tunnel mode: ${result.mode}`);
449
+ console.log(`Connection mode: ${result.connectionMode}`);
431
450
  console.log(`Credential: ${result.credential} (value is not stored in config or displayed)`);
432
451
  console.log(`Readiness: 127.0.0.1:${result.metricsPort} Restarts: ${result.maxRestarts}`);
433
- console.log(result.mode === 'managed' ? 'Configuration saved. Start explicitly with: dodo tunnel start --yes' : 'External mode saved. DODO will observe health but will not start cloudflared.');
452
+ console.log(result.connectionMode === 'tunnel' ? 'Saved. Every dodo start now owns this Tunnel and fails closed if it cannot start.' : 'Saved. Every dodo start now uses only the local MCP endpoint.');
434
453
  }
435
454
  }
436
455
  catch (error) {
@@ -561,7 +580,8 @@ program.command('cli')
561
580
  startupProject,
562
581
  selectProject: selectStartupProject,
563
582
  addProject: addAndSelectProject,
564
- start: async (root, tunnel = true) => launchHttp({ ...(root === undefined ? {} : { root }), tunnel }),
583
+ start: async (root) => launchHttp({ ...(root === undefined ? {} : { root }) }),
584
+ openRemoteConfig: openRemoteConfigFromCli,
565
585
  setupAll: () => setupFromMenu(false),
566
586
  checkSetup: () => setupFromMenu(true),
567
587
  }, { input: process.stdin, output: process.stdout });
@@ -572,19 +592,58 @@ program.command('cli')
572
592
  throw error;
573
593
  }
574
594
  });
575
- // ---------------------------------------------------------------- start ----
595
+ function printRemoteConfig(result) {
596
+ console.log(`Remote Config: ${result.url}`);
597
+ console.log(`Pairing code (shown once): ${result.pairingCode}`);
598
+ console.log(`Expires: ${new Date(result.expiresAt).toISOString()} (ไม่เกิน 1 ชั่วโมง)`);
599
+ console.log('เปิด URL แล้วกรอก code นี้ ระบบจะแลกเป็น Secure/HttpOnly cookie; ไม่มี secret อยู่ใน URL');
600
+ }
601
+ async function openRemoteConfigFromCli() {
602
+ try {
603
+ printRemoteConfig(await ipcForInstallation('remoteConfig.open'));
604
+ }
605
+ catch (error) {
606
+ if (!(error instanceof IpcError) || !error.message.includes('no running DODO installation'))
607
+ throw error;
608
+ await launchHttp({ web: true });
609
+ }
610
+ }
611
+ const web = program.command('web')
612
+ .description('open or renew the one-hour Remote Config page through the public MCP tunnel')
613
+ .option('--close', 'close the Remote Config lease without stopping MCP or Tunnel', false)
614
+ .option('--status', 'show the current non-secret Remote Config lease status', false)
615
+ .action(async (opts) => {
616
+ if (opts.close && opts.status)
617
+ fail('choose --close or --status');
618
+ try {
619
+ if (opts.close) {
620
+ console.log(JSON.stringify(await ipcForInstallation('remoteConfig.close'), null, 2));
621
+ return;
622
+ }
623
+ if (opts.status) {
624
+ console.log(JSON.stringify(await ipcForInstallation('remoteConfig.status'), null, 2));
625
+ return;
626
+ }
627
+ await openRemoteConfigFromCli();
628
+ }
629
+ catch (error) {
630
+ if (error instanceof DodoError || error instanceof IpcError)
631
+ fail(error.message);
632
+ throw error;
633
+ }
634
+ });
635
+ void web;
576
636
  program
577
637
  .command('start')
578
638
  .description('start the MCP server for the saved project, or wait for a project selection (foreground)')
579
639
  .option('--root <path>', 'select and remember an explicit workspace root (local CLI only)')
580
640
  .option('--port <n>', 'listen port (default from config, initially 21730)', (v) => Number.parseInt(v, 10))
581
- .option('--public-url <url>', 'public origin override for this run (local CLI only)')
582
641
  .option('--allow-unsafe-root', 'permit filesystem root / home / other unusually broad workspace roots', false)
583
642
  .option('--allow', 'allow task execution for this run (requires --all)', false)
584
643
  .option('--all', 'all local action classes (requires --allow)', false)
585
644
  .option('--bypass', 'trusted actions and no default job sandbox for this run; OAuth and file guards remain', false)
586
645
  .option('--tools <surface>', 'tool exposure for this run: compact (HTTP default), full, or hybrid (49 tools: gateways + common direct tools); permissions are unchanged')
587
- .option('--no-tunnel', 'start local MCP only; do not ask for a temporary Cloudflare Tunnel token')
646
+ .option('--web', 'open Remote Config through the public tunnel for at most one hour', false)
588
647
  .option('--quiet', 'suppress the startup banner', false)
589
648
  .action(async (opts) => {
590
649
  try {
@@ -707,8 +766,8 @@ program
707
766
  console.log('');
708
767
  console.log('Next steps:');
709
768
  console.log(` 1. Point your own tunnel at 127.0.0.1:${next.port} for host ${url.hostname} (see docs/TUNNEL.md — route ALL paths, not just /mcp)`);
710
- console.log(' 2. Register your client callback: dodo auth add-client --redirect-uri <exact callback URL from your client UI>');
711
- console.log(' 3. cd into a project, set trust: dodo trust --mode edit');
769
+ console.log(` 2. Select it persistently: dodo tunnel configure --tunnel --public-url ${url.origin} --os-credential`);
770
+ console.log(' 3. Register your client callback: dodo auth add-client --redirect-uri <exact callback URL from your client UI>');
712
771
  console.log(' 4. dodo start');
713
772
  }
714
773
  catch (err) {
@@ -1376,13 +1435,19 @@ Desktop (platform helper and explicit app permission required):
1376
1435
  dodo desktop disable revoke desktop access
1377
1436
  Remote setup:
1378
1437
  dodo init --public-url https://... configure public origin once
1379
- dodo start prompt for a temporary Tunnel token
1380
- dodo start --no-tunnel run local MCP only`);
1438
+ dodo tunnel configure --tunnel --os-credential --public-url https://...
1439
+ select the persistent DODO Tunnel
1440
+ dodo tunnel configure --local select local-only MCP
1441
+ dodo start use exactly the saved connection mode
1442
+ dodo --web start/renew Remote Config through that tunnel for 1 hour
1443
+ dodo web --close close Remote Config; keep MCP/Tunnel running`);
1381
1444
  function effectiveArgv() {
1382
1445
  if (process.argv.length <= 2)
1383
1446
  return [...process.argv, 'start'];
1384
1447
  if (process.argv.length === 3 && process.argv[2] === '--cli')
1385
1448
  return [...process.argv.slice(0, 2), 'cli'];
1449
+ if (process.argv.length === 3 && process.argv[2] === '--web')
1450
+ return [...process.argv.slice(0, 2), 'web'];
1386
1451
  if (process.argv[2] === '--bypass')
1387
1452
  return [...process.argv.slice(0, 2), 'start', ...process.argv.slice(2)];
1388
1453
  return process.argv;