dodo-mcp 1.0.2 → 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 (43) hide show
  1. package/README.md +31 -18
  2. package/dist/cli/main.js +63 -66
  3. package/dist/cli/main.js.map +1 -1
  4. package/dist/cli/menu.js +9 -16
  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 +38 -26
  13. package/dist/server/appServer.js.map +1 -1
  14. package/dist/server/configUi/app.js +35 -58
  15. package/dist/server/configUi/index.html +21 -22
  16. package/dist/server/localConfig.js +50 -63
  17. package/dist/server/localConfig.js.map +1 -1
  18. package/dist/tunnel/control.js +4 -6
  19. package/dist/tunnel/control.js.map +1 -1
  20. package/dist/tunnel/credentials.js +5 -19
  21. package/dist/tunnel/credentials.js.map +1 -1
  22. package/dist/tunnel/runtime.js +3 -4
  23. package/dist/tunnel/runtime.js.map +1 -1
  24. package/dist/tunnel/supervisor.js +8 -10
  25. package/dist/tunnel/supervisor.js.map +1 -1
  26. package/docs/AI_PROVIDERS.md +1 -1
  27. package/docs/ARCHITECTURE.md +14 -14
  28. package/docs/COMPATIBILITY.md +6 -2
  29. package/docs/MANUAL_ACCEPTANCE.md +21 -16
  30. package/docs/RELEASE_1.0.0.md +3 -0
  31. package/docs/RELEASE_1.0.3.md +57 -0
  32. package/docs/RELEASE_NOTES.md +19 -0
  33. package/docs/SECURITY.md +20 -10
  34. package/docs/TEST_REPORT.md +33 -1
  35. package/docs/TUNNEL.md +102 -80
  36. package/docs/WINDOWS.md +1 -1
  37. package/docs/adr/031-owner-selected-tunnel-supervision.md +8 -2
  38. package/docs/adr/044-global-launcher-and-cli-menu.md +3 -3
  39. package/docs/adr/047-temporary-remote-config.md +4 -4
  40. package/docs/adr/048-persistent-connection-mode.md +58 -0
  41. package/docs/adr/README.md +1 -0
  42. package/package.json +2 -1
  43. package/schemas/global-config.schema.json +6 -12
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DODO MCP
2
2
 
3
- **DODO MCP 1.0.2** คือ 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
 
@@ -250,20 +250,34 @@ command sandbox, idempotency และ audit เดิม `agent_exec` ใช้
250
250
 
251
251
  ### เชื่อม Remote MCP ผ่าน Cloudflare Tunnel
252
252
 
253
- 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:
254
265
 
255
266
  ```bash
256
267
  dodo setup --check --components cloudflared
257
- dodo init --public-url https://mcp.example.com
268
+ dodo tunnel configure \
269
+ --tunnel \
270
+ --public-url https://mcp.example.com \
271
+ --os-credential
258
272
  dodo start
259
- # Cloudflare Tunnel token (temporary; Enter = local only):
260
273
  ```
261
274
 
262
- เมื่อกรอก token DODO จะเริ่ม `cloudflared` เป็น child process ของรอบนั้นและหยุดพร้อม
263
- DODO Token อยู่ในหน่วยความจำระหว่างรอบและ environment ของ child เท่านั้น ไม่อยู่ใน
264
- argv, config, Keychain, Credential Manager, Secret Service, tunnel log, MCP response
265
- หรือ environment ของ MCP jobs กด Enter หรือใช้ `dodo start --no-tunnel` เพื่อเปิด
266
- เฉพาะ 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
267
281
 
268
282
  หากต้องตั้งค่าจากอุปกรณ์อื่น ให้เปิดหน้าเจ้าของแบบชั่วคราวผ่าน public listener เดียวกัน:
269
283
 
@@ -277,15 +291,14 @@ dodo web --close # ปิดหน้า Remote Config แต่ MCP/Tunnel
277
291
  terminal หลังจับคู่ browser จะใช้ cookie ที่เป็น `Secure`, `HttpOnly`,
278
292
  `SameSite=Strict` และจำกัด path ที่ `/config` หน้า Remote Config ปิดอัตโนมัติภายใน
279
293
  1 ชั่วโมง การเปิดใหม่ออก code/session ใหม่และไม่ต้อง restart MCP ไม่มี token หรือ
280
- credential อยู่ใน URL หาก process เดิมเริ่มแบบ local-only `dodo --web` จะถาม Tunnel
281
- token แบบซ่อนและเปิด `cloudflared` ของ process นั้นก่อน
282
-
283
- หน้า Local Config ที่ `127.0.0.1:21731` มีช่อง **Temporary Tunnel token** สำหรับเริ่ม
284
- และหยุด tunnel ของ DODO process ปัจจุบันได้จริง ช่องจะถูกล้างหลังส่งและ backend ไม่
285
- persist ค่า ดูสถานะด้วย `dodo tunnel status`, ตรวจ connectivity ด้วย
286
- `dodo tunnel doctor` และดู log ที่ redacted ด้วย `dodo tunnel logs` คำสั่ง
287
- `dodo tunnel configure` เดิมยังมีสำหรับ advanced/headless compatibility และต้องเรียก
288
- โดยเจ้าของอย่างชัดเจน; เส้นทาง `dodo start` และหน้าเว็บไม่ใช้ credential store
294
+ credential อยู่ใน URL คำสั่งนี้ใช้ได้เฉพาะเมื่อ persistent mode เป็น `tunnel` และ
295
+ DODO-owned Tunnel กำลังทำงาน; มันไม่รับ token ใหม่ผ่าน IPC และไม่เปลี่ยนจาก Local เอง
296
+
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`
289
302
 
290
303
  บน macOS `dodo setup --yes` เลือก component `cloudflared` อยู่ในชุด `all` แล้ว
291
304
  Tunnel route ต้องชี้เฉพาะ MCP/OAuth listener `21730` ห้ามชี้ Local Config `21731`
package/dist/cli/main.js CHANGED
@@ -142,21 +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
- if (opts.web && !publicUrl)
147
- fail('dodo --web requires a public HTTPS origin; run dodo init --public-url https://your-host first');
148
- let temporaryTunnelToken;
149
- const interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true;
150
- if (opts.web && opts.tunnel !== false && !interactiveTerminal) {
151
- fail('dodo --web needs an interactive terminal for temporary Tunnel token entry; use dodo start --web --no-tunnel only with an already-running external tunnel');
152
- }
153
- if (opts.tunnel !== false && (config.tunnel.startWithDodo || opts.web) && publicUrl && interactiveTerminal) {
154
- const { readTemporaryTunnelToken } = await import('../tunnel/credentials.js');
155
- temporaryTunnelToken = await readTemporaryTunnelToken();
156
- if (opts.web && !temporaryTunnelToken) {
157
- fail('Remote Config was not opened because no temporary Tunnel token was entered');
158
- }
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');
159
160
  }
161
+ const endpointOrigin = connectionMode === 'tunnel' ? publicUrl : `http://127.0.0.1:${selectedPort}`;
160
162
  const startOpts = {
161
163
  configPort: config.configPort,
162
164
  ...(opts.bypass ? { runMode: 'bypass' } : opts.allow ? { runMode: 'allow-all' } : {}),
@@ -165,12 +167,11 @@ async function launchHttp(opts) {
165
167
  allowUnsafeRoot: opts.allowUnsafeRoot ?? false,
166
168
  quiet: opts.quiet ?? false,
167
169
  onLog: (line) => console.log(formatTerminalLine(line)),
168
- ...(opts.web ? { remoteConfig: true } : {}),
170
+ connectionMode,
171
+ publicUrlOverride: endpointOrigin,
169
172
  };
170
173
  if (opts.port !== undefined && !Number.isNaN(opts.port))
171
174
  startOpts.portOverride = opts.port;
172
- if (opts.publicUrl !== undefined)
173
- startOpts.publicUrlOverride = opts.publicUrl;
174
175
  if (opts.tools !== undefined)
175
176
  startOpts.toolSurface = opts.tools;
176
177
  const tunnelRuntime = new TunnelRuntime(configDir, line => console.log(`[dodo:tunnel] ${line}`));
@@ -184,32 +185,26 @@ async function launchHttp(opts) {
184
185
  void stop('server stop');
185
186
  },
186
187
  });
187
- if (temporaryTunnelToken) {
188
+ if (connectionMode === 'tunnel') {
188
189
  try {
189
- const transientConfig = GlobalConfigSchema.parse({
190
+ const selectedConfig = GlobalConfigSchema.parse({
190
191
  ...config,
191
192
  ...(publicUrl ? { publicUrl } : {}),
192
- tunnel: { ...config.tunnel, mode: 'managed' },
193
+ tunnel: { ...config.tunnel, connectionMode: 'tunnel' },
193
194
  });
194
- const status = await tunnelRuntime.start(transientConfig, temporaryTunnelToken);
195
- 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'));
196
199
  }
197
200
  catch (error) {
198
201
  suppressOnStopped = true;
199
202
  await server.close();
200
203
  throw error;
201
204
  }
202
- finally {
203
- temporaryTunnelToken = undefined;
204
- }
205
205
  }
206
- else if (!opts.quiet && opts.tunnel !== false && config.tunnel.startWithDodo) {
207
- if (!publicUrl)
208
- console.log('[dodo] Tunnel skipped: configure the public HTTPS origin, then restart DODO.');
209
- else if (!interactiveTerminal)
210
- console.log('[dodo] Tunnel skipped: temporary token entry requires an interactive terminal.');
211
- else
212
- 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`);
213
208
  }
214
209
  async function stop(signal) {
215
210
  if (shutdownPromise)
@@ -367,9 +362,12 @@ project.command('remove <projectId>')
367
362
  // --------------------------------------------------------------- tunnel ----
368
363
  const tunnel = program.command('tunnel').description('manage or inspect a Cloudflare remotely-managed Tunnel without managing DNS or the Cloudflare account');
369
364
  tunnel.command('configure')
370
- .description('save local tunnel mode and a credential reference; tokens are never written to config')
371
- .option('--managed', 'DODO supervises cloudflared in the foreground', false)
372
- .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')
373
371
  .option('--os-credential', 'prompt through the reviewed OS credential provider', false)
374
372
  .option('--token-env <name>', 'reference an uppercase environment variable that already contains the token')
375
373
  .option('--token-file <absolute-path>', 'reference an owner-private absolute token file')
@@ -381,8 +379,10 @@ tunnel.command('configure')
381
379
  .option('--json', 'machine-readable non-secret result', false)
382
380
  .action(async (opts) => {
383
381
  try {
384
- if (opts.managed && opts.external)
385
- 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');
386
386
  const methods = [opts.osCredential, opts.tokenEnv !== undefined, opts.tokenFile !== undefined].filter(Boolean).length;
387
387
  if (methods > 1)
388
388
  fail('choose only one of --os-credential, --token-env or --token-file');
@@ -393,6 +393,16 @@ tunnel.command('configure')
393
393
  const { dir } = resolveConfigDir(process.env);
394
394
  ensureConfigDir(dir);
395
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');
396
406
  const credentials = await import('../tunnel/credentials.js');
397
407
  const { resolveCloudflared } = await import('../tunnel/supervisor.js');
398
408
  const previousCredentialRef = current.tunnel.credentialRef;
@@ -401,7 +411,7 @@ tunnel.command('configure')
401
411
  credentialRef = undefined;
402
412
  const baseTunnelInput = {
403
413
  ...current.tunnel,
404
- mode: opts.managed ? 'managed' : opts.external ? 'external' : current.tunnel.mode,
414
+ connectionMode,
405
415
  ...(credentialRef ? { credentialRef } : {}),
406
416
  ...(opts.metricsPort !== undefined ? { metricsPort: opts.metricsPort } : {}),
407
417
  ...(opts.maxRestarts !== undefined ? { maxRestarts: opts.maxRestarts } : {}),
@@ -409,7 +419,7 @@ tunnel.command('configure')
409
419
  };
410
420
  if (!credentialRef)
411
421
  delete baseTunnelInput['credentialRef'];
412
- let next = GlobalConfigSchema.parse({ ...current, tunnel: baseTunnelInput });
422
+ let next = GlobalConfigSchema.parse({ ...current, ...(publicUrl ? { publicUrl } : {}), tunnel: baseTunnelInput });
413
423
  if (opts.cloudflared !== undefined) {
414
424
  const canonical = resolveCloudflared(next);
415
425
  next = GlobalConfigSchema.parse({ ...next, tunnel: { ...next.tunnel, executable: canonical } });
@@ -427,19 +437,19 @@ tunnel.command('configure')
427
437
  if (!credentialRef)
428
438
  delete withCredential['credentialRef'];
429
439
  next = GlobalConfigSchema.parse({ ...next, tunnel: withCredential });
430
- if (next.tunnel.mode === 'managed' && !next.tunnel.credentialRef)
431
- 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');
432
442
  saveGlobalConfig(paths.configFile, next);
433
443
  if (opts.removeCredential && previousCredentialRef)
434
444
  credentials.deleteOsTunnelCredential(previousCredentialRef);
435
- 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 };
436
446
  if (opts.json)
437
447
  console.log(JSON.stringify(result, null, 2));
438
448
  else {
439
- console.log(`Tunnel mode: ${result.mode}`);
449
+ console.log(`Connection mode: ${result.connectionMode}`);
440
450
  console.log(`Credential: ${result.credential} (value is not stored in config or displayed)`);
441
451
  console.log(`Readiness: 127.0.0.1:${result.metricsPort} Restarts: ${result.maxRestarts}`);
442
- 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.');
443
453
  }
444
454
  }
445
455
  catch (error) {
@@ -570,7 +580,7 @@ program.command('cli')
570
580
  startupProject,
571
581
  selectProject: selectStartupProject,
572
582
  addProject: addAndSelectProject,
573
- start: async (root, tunnel = true) => launchHttp({ ...(root === undefined ? {} : { root }), tunnel }),
583
+ start: async (root) => launchHttp({ ...(root === undefined ? {} : { root }) }),
574
584
  openRemoteConfig: openRemoteConfigFromCli,
575
585
  setupAll: () => setupFromMenu(false),
576
586
  checkSetup: () => setupFromMenu(true),
@@ -590,20 +600,7 @@ function printRemoteConfig(result) {
590
600
  }
591
601
  async function openRemoteConfigFromCli() {
592
602
  try {
593
- const status = await ipcForInstallation('remoteConfig.status');
594
- let tunnelToken;
595
- if (status.tunnel?.available === true && status.tunnel.running !== true) {
596
- const { readTemporaryTunnelToken } = await import('../tunnel/credentials.js');
597
- tunnelToken = await readTemporaryTunnelToken('Cloudflare Tunnel token (temporary; required for Remote Config): ');
598
- if (!tunnelToken)
599
- fail('Remote Config needs a temporary Tunnel token; use dodo start --no-tunnel for local-only MCP');
600
- }
601
- try {
602
- printRemoteConfig(await ipcForInstallation('remoteConfig.open', tunnelToken ? { tunnelToken } : {}));
603
- }
604
- finally {
605
- tunnelToken = undefined;
606
- }
603
+ printRemoteConfig(await ipcForInstallation('remoteConfig.open'));
607
604
  }
608
605
  catch (error) {
609
606
  if (!(error instanceof IpcError) || !error.message.includes('no running DODO installation'))
@@ -641,13 +638,11 @@ program
641
638
  .description('start the MCP server for the saved project, or wait for a project selection (foreground)')
642
639
  .option('--root <path>', 'select and remember an explicit workspace root (local CLI only)')
643
640
  .option('--port <n>', 'listen port (default from config, initially 21730)', (v) => Number.parseInt(v, 10))
644
- .option('--public-url <url>', 'public origin override for this run (local CLI only)')
645
641
  .option('--allow-unsafe-root', 'permit filesystem root / home / other unusually broad workspace roots', false)
646
642
  .option('--allow', 'allow task execution for this run (requires --all)', false)
647
643
  .option('--all', 'all local action classes (requires --allow)', false)
648
644
  .option('--bypass', 'trusted actions and no default job sandbox for this run; OAuth and file guards remain', false)
649
645
  .option('--tools <surface>', 'tool exposure for this run: compact (HTTP default), full, or hybrid (49 tools: gateways + common direct tools); permissions are unchanged')
650
- .option('--no-tunnel', 'start local MCP only; do not ask for a temporary Cloudflare Tunnel token')
651
646
  .option('--web', 'open Remote Config through the public tunnel for at most one hour', false)
652
647
  .option('--quiet', 'suppress the startup banner', false)
653
648
  .action(async (opts) => {
@@ -771,8 +766,8 @@ program
771
766
  console.log('');
772
767
  console.log('Next steps:');
773
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)`);
774
- console.log(' 2. Register your client callback: dodo auth add-client --redirect-uri <exact callback URL from your client UI>');
775
- 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>');
776
771
  console.log(' 4. dodo start');
777
772
  }
778
773
  catch (err) {
@@ -1440,10 +1435,12 @@ Desktop (platform helper and explicit app permission required):
1440
1435
  dodo desktop disable revoke desktop access
1441
1436
  Remote setup:
1442
1437
  dodo init --public-url https://... configure public origin once
1443
- dodo start prompt for a temporary Tunnel token
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
1444
1442
  dodo --web start/renew Remote Config through that tunnel for 1 hour
1445
- dodo web --close close Remote Config; keep MCP/Tunnel running
1446
- dodo start --no-tunnel run local MCP only`);
1443
+ dodo web --close close Remote Config; keep MCP/Tunnel running`);
1447
1444
  function effectiveArgv() {
1448
1445
  if (process.argv.length <= 2)
1449
1446
  return [...process.argv, 'start'];