doubao-cli 0.4.0 → 0.4.1

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 CHANGED
@@ -44,7 +44,11 @@ Every data-returning command supports `--json`. Select a non-default local profi
44
44
 
45
45
  Message automation requires Doubao to be launched with local Chrome DevTools Protocol enabled:
46
46
 
47
- Quit any running Doubao process first, then run `doubao cdp launch`. The equivalent manual command is `open -a /Applications/Doubao.app --args --remote-debugging-port=9225`.
47
+ ```bash
48
+ doubao cdp launch
49
+ ```
50
+
51
+ If Doubao is already running without CDP, the command asks for confirmation before quitting it and relaunching with the debugging port enabled. Scripts and `--json` mode never prompt; pass `doubao cdp launch --yes` to confirm the restart explicitly. The command returns only after both the CDP endpoint and authenticated chat renderer are ready. The equivalent manual sequence is to quit Doubao completely and run `open -a /Applications/Doubao.app --args --remote-debugging-port=9225`.
48
52
 
49
53
  Set `DOUBAO_CDP_ENDPOINT` if using another port. `sessions send --wait` waits for and returns the completed assistant reply.
50
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doubao-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Programmatic control for local Doubao desktop sessions on macOS",
5
5
  "author": "Fullstop000 <fullstop1005@gmail.com>",
6
6
  "type": "module",
package/src/cdp.mjs CHANGED
@@ -140,7 +140,7 @@ export class CdpClient {
140
140
  export async function withChatClient(callback, endpoint = cdpEndpoint()) {
141
141
  const status = await cdpStatus(endpoint);
142
142
  if (!status.available) {
143
- throw new Error(`Doubao CDP is unavailable at ${endpoint}. Restart Doubao with --remote-debugging-port=9225.`);
143
+ throw new Error(`Doubao CDP is unavailable at ${endpoint}. Run "doubao cdp launch" to restart Doubao with CDP enabled.`);
144
144
  }
145
145
  const target = await findChatTarget(endpoint);
146
146
  const client = await new CdpClient(target.webSocketDebuggerUrl).connect();
package/src/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawnSync } from 'node:child_process';
4
+ import { createInterface } from 'node:readline/promises';
4
5
  import { currentSession, getDataDir, listSessions, resolveProfile } from './storage.mjs';
5
6
  import { cdpStatus } from './cdp.mjs';
6
7
  import { createConversation, openConversation, readConversation, sendMessage } from './automation.mjs';
@@ -22,7 +23,7 @@ const HELP = `Usage:
22
23
  doubao model [--json]
23
24
  doubao model select <model> [--json]
24
25
  doubao cdp status [--json]
25
- doubao cdp launch [--json]
26
+ doubao cdp launch [--yes] [--json]
26
27
  doubao capabilities [--json]
27
28
 
28
29
  Environment:
@@ -35,6 +36,7 @@ export function parseOptions(argv) {
35
36
  const args = [];
36
37
  let profile;
37
38
  let json = false;
39
+ let yes = false;
38
40
  let wait = false;
39
41
  let timeoutSeconds = 120;
40
42
  let limit = 20;
@@ -46,6 +48,8 @@ export function parseOptions(argv) {
46
48
  break;
47
49
  } else if (argv[index] === '--json') {
48
50
  json = true;
51
+ } else if (argv[index] === '--yes') {
52
+ yes = true;
49
53
  } else if (argv[index] === '--wait') {
50
54
  wait = true;
51
55
  } else if (argv[index] === '--profile') {
@@ -73,7 +77,7 @@ export function parseOptions(argv) {
73
77
  args.push(argv[index]);
74
78
  }
75
79
  }
76
- return { args, profile, json, wait, timeoutMs: timeoutSeconds * 1000, limit, model, attachments };
80
+ return { args, profile, json, yes, wait, timeoutMs: timeoutSeconds * 1000, limit, model, attachments };
77
81
  }
78
82
 
79
83
  function output(value, json) {
@@ -89,9 +93,71 @@ function appVersion(appPath) {
89
93
  return result.status === 0 ? result.stdout.trim() : null;
90
94
  }
91
95
 
92
- function appRunning(appPath) {
96
+ function appProcessPattern(appPath) {
93
97
  const executable = path.join(appPath, 'Contents', 'MacOS', 'Doubao');
94
- return spawnSync('/usr/bin/pgrep', ['-f', executable]).status === 0;
98
+ const escaped = executable.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
99
+ return `^${escaped}([[:space:]]|$)`;
100
+ }
101
+
102
+ function appRunning(appPath) {
103
+ return spawnSync('/usr/bin/pgrep', ['-f', appProcessPattern(appPath)]).status === 0;
104
+ }
105
+
106
+ async function waitForAppExit(appPath, timeoutMs) {
107
+ const deadline = Date.now() + timeoutMs;
108
+ while (Date.now() < deadline) {
109
+ if (!appRunning(appPath)) return true;
110
+ await new Promise((resolve) => setTimeout(resolve, 250));
111
+ }
112
+ return !appRunning(appPath);
113
+ }
114
+
115
+ async function quitAppForCdp(appPath) {
116
+ const result = spawnSync('/usr/bin/osascript', [
117
+ '-e',
118
+ 'tell application id "com.bot.pc.doubao" to quit',
119
+ ], { encoding: 'utf8' });
120
+ if (result.status === 0 && await waitForAppExit(appPath, 5000)) return;
121
+
122
+ const terminated = spawnSync('/usr/bin/pkill', ['-TERM', '-f', appProcessPattern(appPath)], { encoding: 'utf8' });
123
+ if (terminated.status !== 0 && terminated.status !== 1) {
124
+ throw new Error(terminated.stderr.trim() || result.stderr.trim() || 'failed to stop Doubao before enabling CDP');
125
+ }
126
+ if (!await waitForAppExit(appPath, 10_000)) {
127
+ throw new Error('Doubao did not quit after SIGTERM. Quit it manually, then run "doubao cdp launch" again.');
128
+ }
129
+ }
130
+
131
+ async function confirmCdpRestart({ json, yes }) {
132
+ if (yes) return;
133
+ if (json || !process.stdin.isTTY || !process.stdout.isTTY) {
134
+ throw new Error('Doubao must restart to enable CDP. Re-run "doubao cdp launch --yes" to confirm.');
135
+ }
136
+
137
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
138
+ try {
139
+ const answer = await prompt.question('Doubao must restart to enable CDP. Continue? [y/N] ');
140
+ if (!/^(?:y|yes)$/iu.test(answer.trim())) {
141
+ throw new Error('CDP launch cancelled; Doubao was not restarted.');
142
+ }
143
+ } finally {
144
+ prompt.close();
145
+ }
146
+ }
147
+
148
+ async function waitForAutomationReady(timeoutMs = 20_000) {
149
+ const deadline = Date.now() + timeoutMs;
150
+ let lastError;
151
+ while (Date.now() < deadline) {
152
+ try {
153
+ const result = await listModels();
154
+ if (result.models.length) return;
155
+ } catch (error) {
156
+ lastError = error;
157
+ }
158
+ await new Promise((resolve) => setTimeout(resolve, 250));
159
+ }
160
+ throw new Error(`Doubao CDP is listening, but the chat renderer is not ready: ${lastError?.message || 'timed out'}`);
95
161
  }
96
162
 
97
163
  function validateId(value) {
@@ -104,7 +170,7 @@ function sessionWithTitle(profilePath, id) {
104
170
  }
105
171
 
106
172
  export async function main(argv) {
107
- const { args, profile: requestedProfile, json, wait, timeoutMs, limit, model, attachments } = parseOptions(argv);
173
+ const { args, profile: requestedProfile, json, yes, wait, timeoutMs, limit, model, attachments } = parseOptions(argv);
108
174
  const [command, subcommand, operand] = args;
109
175
  const dataDir = getDataDir();
110
176
 
@@ -213,17 +279,22 @@ export async function main(argv) {
213
279
  if (command === 'cdp' && subcommand === 'launch') {
214
280
  const existing = await cdpStatus();
215
281
  if (existing.available) {
282
+ await waitForAutomationReady();
216
283
  if (json) output(existing, true);
217
284
  else console.log(`available\tyes\nendpoint\t${existing.endpoint}`);
218
285
  return;
219
286
  }
220
287
  const appPath = process.env.DOUBAO_APP || DEFAULT_APP;
221
- if (appRunning(appPath)) throw new Error('Doubao is already running without CDP. Quit it completely, then run this command again.');
222
288
  const endpoint = new URL(existing.endpoint);
223
289
  if (endpoint.hostname !== '127.0.0.1' && endpoint.hostname !== 'localhost') {
224
290
  throw new Error('cdp launch only supports a localhost DOUBAO_CDP_ENDPOINT');
225
291
  }
226
292
  const port = endpoint.port || '9225';
293
+ const restarted = appRunning(appPath);
294
+ if (restarted) {
295
+ await confirmCdpRestart({ json, yes });
296
+ await quitAppForCdp(appPath);
297
+ }
227
298
  const result = spawnSync('/usr/bin/open', ['-a', appPath, '--args', `--remote-debugging-port=${port}`], { encoding: 'utf8' });
228
299
  if (result.status !== 0) throw new Error(result.stderr.trim() || 'failed to launch Doubao with CDP');
229
300
  let launched = existing;
@@ -232,8 +303,10 @@ export async function main(argv) {
232
303
  launched = await cdpStatus();
233
304
  }
234
305
  if (!launched.available) throw new Error(`Doubao launched, but CDP did not become available at ${existing.endpoint}`);
235
- if (json) output(launched, true);
236
- else console.log(`available\tyes\nendpoint\t${launched.endpoint}`);
306
+ await waitForAutomationReady();
307
+ const launchResult = { ...launched, launched: true, restarted };
308
+ if (json) output(launchResult, true);
309
+ else console.log(`available\tyes\nendpoint\t${launched.endpoint}\nrestarted\t${restarted ? 'yes' : 'no'}`);
237
310
  return;
238
311
  }
239
312