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.
- package/README.md +49 -19
- package/dist/cli/main.js +108 -43
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/menu.js +5 -6
- package/dist/cli/menu.js.map +1 -1
- package/dist/config/globalConfig.js +23 -1
- package/dist/config/globalConfig.js.map +1 -1
- package/dist/config/stateImport.js +2 -2
- package/dist/config/stateImport.js.map +1 -1
- package/dist/config/tunnelConfig.js +2 -3
- package/dist/config/tunnelConfig.js.map +1 -1
- package/dist/server/appServer.js +89 -9
- package/dist/server/appServer.js.map +1 -1
- package/dist/server/configUi/app.js +54 -65
- package/dist/server/configUi/index.html +21 -22
- package/dist/server/configUi/workbench.js +7 -4
- package/dist/server/ipcDispatch.js +25 -0
- package/dist/server/ipcDispatch.js.map +1 -1
- package/dist/server/localConfig.js +50 -63
- package/dist/server/localConfig.js.map +1 -1
- package/dist/server/remoteConfig.js +317 -0
- package/dist/server/remoteConfig.js.map +1 -0
- package/dist/tunnel/control.js +4 -6
- package/dist/tunnel/control.js.map +1 -1
- package/dist/tunnel/credentials.js +5 -19
- package/dist/tunnel/credentials.js.map +1 -1
- package/dist/tunnel/runtime.js +3 -4
- package/dist/tunnel/runtime.js.map +1 -1
- package/dist/tunnel/supervisor.js +8 -10
- package/dist/tunnel/supervisor.js.map +1 -1
- package/docs/AI_PROVIDERS.md +1 -1
- package/docs/ARCHITECTURE.md +29 -14
- package/docs/COMPATIBILITY.md +7 -3
- package/docs/MANUAL_ACCEPTANCE.md +39 -10
- package/docs/RELEASE_1.0.0.md +3 -0
- package/docs/RELEASE_1.0.2.md +46 -0
- package/docs/RELEASE_1.0.3.md +57 -0
- package/docs/RELEASE_NOTES.md +37 -0
- package/docs/SECURITY.md +39 -13
- package/docs/TEST_REPORT.md +61 -2
- package/docs/TUNNEL.md +114 -53
- package/docs/WINDOWS.md +1 -1
- package/docs/adr/031-owner-selected-tunnel-supervision.md +8 -2
- package/docs/adr/044-global-launcher-and-cli-menu.md +3 -3
- package/docs/adr/047-temporary-remote-config.md +47 -0
- package/docs/adr/048-persistent-connection-mode.md +58 -0
- package/docs/adr/README.md +2 -0
- package/package.json +3 -1
- 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.
|
|
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` โดยไม่ต้อง
|
|
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
|
|
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
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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`
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
`dodo tunnel
|
|
271
|
-
`dodo tunnel
|
|
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:**
|
|
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
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
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 (
|
|
188
|
+
if (connectionMode === 'tunnel') {
|
|
179
189
|
try {
|
|
180
|
-
const
|
|
190
|
+
const selectedConfig = GlobalConfigSchema.parse({
|
|
181
191
|
...config,
|
|
182
192
|
...(publicUrl ? { publicUrl } : {}),
|
|
183
|
-
tunnel: { ...config.tunnel,
|
|
193
|
+
tunnel: { ...config.tunnel, connectionMode: 'tunnel' },
|
|
184
194
|
});
|
|
185
|
-
const status = await tunnelRuntime.start(
|
|
186
|
-
console.log(`[dodo]
|
|
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
|
|
198
|
-
|
|
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('
|
|
362
|
-
.option('--
|
|
363
|
-
.option('--
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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.
|
|
422
|
-
fail('
|
|
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 = {
|
|
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(`
|
|
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.
|
|
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
|
|
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
|
-
|
|
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('--
|
|
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(
|
|
711
|
-
console.log(' 3.
|
|
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
|
|
1380
|
-
|
|
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;
|