doubao-cli 0.4.2 → 0.5.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.
package/README.md CHANGED
@@ -11,7 +11,7 @@ npm install --global doubao-cli@latest
11
11
  doubao --version
12
12
  ```
13
13
 
14
- Upgrade an existing installation with the same command. To run without a global install:
14
+ Upgrade an existing installation with `doubao update`. To run without a global install:
15
15
 
16
16
  ```bash
17
17
  npx --yes doubao-cli@latest status
@@ -37,6 +37,9 @@ doubao model select doubao-2.1-turbo
37
37
  doubao sessions send 38439138239851266 "hello" --model gpt-5.6-sol --wait
38
38
  doubao cdp status
39
39
  doubao cdp launch
40
+ doubao update check
41
+ doubao update
42
+ doubao update auto on
40
43
  doubao capabilities
41
44
  ```
42
45
 
@@ -52,6 +55,25 @@ If Doubao is already running without CDP, the command asks for confirmation befo
52
55
 
53
56
  Set `DOUBAO_CDP_ENDPOINT` if using another port. `sessions send --wait` waits for and returns the completed assistant reply.
54
57
 
58
+ ### Updates
59
+
60
+ `doubao update check` compares the running version with npm without changing the installation. `doubao update` installs the latest release globally through npm when an update is available:
61
+
62
+ ```bash
63
+ doubao update check --json
64
+ doubao update
65
+ ```
66
+
67
+ Automatic installation is opt-in and checks at most once every 24 hours:
68
+
69
+ ```bash
70
+ doubao update auto on
71
+ doubao update auto status
72
+ doubao update auto off
73
+ ```
74
+
75
+ An automatic update never blocks the requested Doubao command if npm or the network fails. Set `DOUBAO_CLI_DISABLE_AUTO_UPDATE=1` to skip configured automatic updates in CI or a one-off invocation. Settings are stored under `~/Library/Application Support/doubao-cli/update.json`; override that directory with `DOUBAO_CLI_CONFIG_DIR`.
76
+
55
77
  ### New sessions and attachments
56
78
 
57
79
  `sessions create` opens a clean composer. A numeric conversation id does not exist until the first message is sent, so `sessions create` without a message returns `conversationId: null`. Create and persist a session in one command by providing its first message:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doubao-cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
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/cli.mjs CHANGED
@@ -6,6 +6,14 @@ import { currentSession, getDataDir, listSessions, resolveProfile } from './stor
6
6
  import { cdpStatus } from './cdp.mjs';
7
7
  import { createConversation, openConversation, readConversation, sendMessage } from './automation.mjs';
8
8
  import { currentModel, listModels, selectModel } from './models.mjs';
9
+ import {
10
+ checkForUpdate,
11
+ installUpdate,
12
+ maybeAutoUpdate,
13
+ readUpdateState,
14
+ setAutoUpdate,
15
+ updateStatePath,
16
+ } from './update.mjs';
9
17
 
10
18
  const DEFAULT_APP = '/Applications/Doubao.app';
11
19
  const CLI_VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
@@ -24,12 +32,17 @@ const HELP = `Usage:
24
32
  doubao model select <model> [--json]
25
33
  doubao cdp status [--json]
26
34
  doubao cdp launch [--yes] [--json]
35
+ doubao update [--json]
36
+ doubao update check [--json]
37
+ doubao update auto <on|off|status> [--json]
27
38
  doubao capabilities [--json]
28
39
 
29
40
  Environment:
30
41
  DOUBAO_APP Override the Doubao.app path
31
42
  DOUBAO_DATA_DIR Override the Doubao user-data directory
32
43
  DOUBAO_CDP_ENDPOINT CDP endpoint (default: http://127.0.0.1:9225)
44
+ DOUBAO_CLI_CONFIG_DIR Override the doubao-cli settings directory
45
+ DOUBAO_CLI_DISABLE_AUTO_UPDATE Set to 1 to skip configured automatic updates
33
46
  `;
34
47
 
35
48
  export function parseOptions(argv) {
@@ -169,6 +182,16 @@ function sessionWithTitle(profilePath, id) {
169
182
  return listSessions(profilePath).find((session) => session.id === id) || { id, title: null };
170
183
  }
171
184
 
185
+ async function runConfiguredAutoUpdate(command, json) {
186
+ if (['help', '--help', '-h', 'version', '--version', '-v', 'update'].includes(command)) return;
187
+ const result = await maybeAutoUpdate(CLI_VERSION);
188
+ if (result.updated) {
189
+ if (!json) console.error(`doubao: automatically updated to ${result.latestVersion}; the new version applies next run`);
190
+ } else if (result.error && !json) {
191
+ console.error(`doubao: automatic update failed: ${result.error}`);
192
+ }
193
+ }
194
+
172
195
  export async function main(argv) {
173
196
  const { args, profile: requestedProfile, json, yes, wait, timeoutMs, limit, model, attachments } = parseOptions(argv);
174
197
  const [command, subcommand, operand] = args;
@@ -183,6 +206,54 @@ export async function main(argv) {
183
206
  return;
184
207
  }
185
208
 
209
+ if (command === 'update') {
210
+ if (subcommand === 'auto') {
211
+ const action = operand || 'status';
212
+ if (!['on', 'off', 'status'].includes(action)) {
213
+ throw new Error('update auto requires on, off, or status');
214
+ }
215
+ const state = action === 'status'
216
+ ? await readUpdateState()
217
+ : await setAutoUpdate(action === 'on');
218
+ const result = {
219
+ enabled: state.autoUpdate,
220
+ lastCheckedAt: state.lastCheckedAt,
221
+ lastUpdatedVersion: state.lastUpdatedVersion,
222
+ settingsPath: updateStatePath(),
223
+ };
224
+ if (json) output(result, true);
225
+ else {
226
+ console.log(`automatic updates\t${result.enabled ? 'on' : 'off'}`);
227
+ if (result.lastCheckedAt) console.log(`last checked\t${result.lastCheckedAt}`);
228
+ if (result.lastUpdatedVersion) console.log(`last updated\t${result.lastUpdatedVersion}`);
229
+ }
230
+ return;
231
+ }
232
+
233
+ if (subcommand && subcommand !== 'check') {
234
+ throw new Error(`unknown update command "${subcommand}". Run "doubao help".`);
235
+ }
236
+ const check = await checkForUpdate(CLI_VERSION);
237
+ if (subcommand === 'check') {
238
+ if (json) output(check, true);
239
+ else {
240
+ console.log(`current\t${check.currentVersion}`);
241
+ console.log(`latest\t${check.latestVersion}`);
242
+ console.log(`update available\t${check.updateAvailable ? 'yes' : 'no'}`);
243
+ }
244
+ return;
245
+ }
246
+ const result = check.updateAvailable
247
+ ? { ...check, ...installUpdate(check.latestVersion, { inherit: !json }) }
248
+ : { ...check, updated: false };
249
+ if (json) output(result, true);
250
+ else if (result.updated) console.log(`updated\t${result.latestVersion}`);
251
+ else console.log(`up to date\t${result.currentVersion}`);
252
+ return;
253
+ }
254
+
255
+ await runConfiguredAutoUpdate(command, json);
256
+
186
257
  if (command === 'profiles') {
187
258
  const { readProfiles } = await import('./storage.mjs');
188
259
  const profiles = readProfiles(dataDir);
@@ -208,6 +279,8 @@ export async function main(argv) {
208
279
  sendMessages: cdp.available,
209
280
  uploadAttachments: cdp.available,
210
281
  selectModels: cdp.available,
282
+ selfUpdate: true,
283
+ automaticUpdates: true,
211
284
  cdp,
212
285
  note: cdp.available
213
286
  ? 'Message automation is available through the authenticated Doubao renderer over local CDP.'
@@ -224,6 +297,8 @@ export async function main(argv) {
224
297
  console.log(`messages send\t${capabilities.sendMessages ? 'yes' : 'no'}`);
225
298
  console.log(`attachments upload\t${capabilities.uploadAttachments ? 'yes' : 'no'}`);
226
299
  console.log(`models select\t${capabilities.selectModels ? 'yes' : 'no'}`);
300
+ console.log('self update\tyes');
301
+ console.log('automatic updates\tyes');
227
302
  console.log(`note\t${capabilities.note}`);
228
303
  }
229
304
  return;
package/src/update.mjs ADDED
@@ -0,0 +1,165 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const PACKAGE_NAME = 'doubao-cli';
7
+ const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
8
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
9
+ const DEFAULT_STATE = Object.freeze({
10
+ autoUpdate: false,
11
+ lastCheckedAt: null,
12
+ lastUpdatedVersion: null,
13
+ });
14
+
15
+ function parseVersion(version) {
16
+ const match = /^(?:v)?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u.exec(String(version || '').trim());
17
+ if (!match) throw new Error(`invalid semantic version "${version}"`);
18
+ return {
19
+ numbers: match.slice(1, 4).map(Number),
20
+ prerelease: match[4]?.split('.') || [],
21
+ };
22
+ }
23
+
24
+ function comparePrerelease(left, right) {
25
+ if (!left.length && !right.length) return 0;
26
+ if (!left.length) return 1;
27
+ if (!right.length) return -1;
28
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
29
+ if (left[index] === undefined) return -1;
30
+ if (right[index] === undefined) return 1;
31
+ if (left[index] === right[index]) continue;
32
+ const leftNumber = /^\d+$/u.test(left[index]) ? Number(left[index]) : null;
33
+ const rightNumber = /^\d+$/u.test(right[index]) ? Number(right[index]) : null;
34
+ if (leftNumber !== null && rightNumber !== null) return Math.sign(leftNumber - rightNumber);
35
+ if (leftNumber !== null) return -1;
36
+ if (rightNumber !== null) return 1;
37
+ return Math.sign(left[index].localeCompare(right[index], 'en-US'));
38
+ }
39
+ return 0;
40
+ }
41
+
42
+ export function compareVersions(leftVersion, rightVersion) {
43
+ const left = parseVersion(leftVersion);
44
+ const right = parseVersion(rightVersion);
45
+ for (let index = 0; index < left.numbers.length; index += 1) {
46
+ if (left.numbers[index] !== right.numbers[index]) {
47
+ return Math.sign(left.numbers[index] - right.numbers[index]);
48
+ }
49
+ }
50
+ return comparePrerelease(left.prerelease, right.prerelease);
51
+ }
52
+
53
+ export function updateStatePath(env = process.env) {
54
+ const directory = env.DOUBAO_CLI_CONFIG_DIR
55
+ || path.join(os.homedir(), 'Library', 'Application Support', PACKAGE_NAME);
56
+ return path.join(directory, 'update.json');
57
+ }
58
+
59
+ export async function readUpdateState(env = process.env) {
60
+ const statePath = updateStatePath(env);
61
+ try {
62
+ const parsed = JSON.parse(await fs.readFile(statePath, 'utf8'));
63
+ return { ...DEFAULT_STATE, ...parsed };
64
+ } catch (error) {
65
+ if (error.code === 'ENOENT') return { ...DEFAULT_STATE };
66
+ throw new Error(`cannot read update settings at ${statePath}: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ export async function writeUpdateState(state, env = process.env) {
71
+ const statePath = updateStatePath(env);
72
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
73
+ const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
74
+ try {
75
+ await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
76
+ await fs.rename(temporaryPath, statePath);
77
+ } finally {
78
+ await fs.rm(temporaryPath, { force: true }).catch(() => {});
79
+ }
80
+ return state;
81
+ }
82
+
83
+ export async function setAutoUpdate(enabled, env = process.env) {
84
+ const state = await readUpdateState(env);
85
+ return writeUpdateState({
86
+ ...state,
87
+ autoUpdate: Boolean(enabled),
88
+ ...(enabled ? { lastCheckedAt: null } : {}),
89
+ }, env);
90
+ }
91
+
92
+ export async function checkForUpdate(currentVersion, options = {}) {
93
+ parseVersion(currentVersion);
94
+ const fetchImpl = options.fetchImpl || fetch;
95
+ const controller = new AbortController();
96
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs || 5000);
97
+ try {
98
+ const response = await fetchImpl(REGISTRY_URL, {
99
+ headers: { accept: 'application/json' },
100
+ signal: controller.signal,
101
+ });
102
+ if (!response.ok) throw new Error(`npm registry returned HTTP ${response.status}`);
103
+ const payload = await response.json();
104
+ parseVersion(payload.version);
105
+ return {
106
+ currentVersion,
107
+ latestVersion: payload.version,
108
+ updateAvailable: compareVersions(currentVersion, payload.version) < 0,
109
+ };
110
+ } finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
114
+
115
+ export function installUpdate(version, options = {}) {
116
+ parseVersion(version);
117
+ const spawnImpl = options.spawnImpl || spawnSync;
118
+ const result = spawnImpl('npm', ['install', '--global', `${PACKAGE_NAME}@${version}`], {
119
+ encoding: 'utf8',
120
+ env: options.env || process.env,
121
+ stdio: options.inherit ? 'inherit' : 'pipe',
122
+ });
123
+ if (result.error) throw new Error(`failed to run npm: ${result.error.message}`);
124
+ if (result.status !== 0) {
125
+ throw new Error(result.stderr?.trim() || `npm exited with status ${result.status}`);
126
+ }
127
+ return { updated: true, version };
128
+ }
129
+
130
+ function automaticUpdatesDisabled(env) {
131
+ return /^(?:1|true|yes)$/iu.test(env.DOUBAO_CLI_DISABLE_AUTO_UPDATE || '');
132
+ }
133
+
134
+ export async function maybeAutoUpdate(currentVersion, options = {}) {
135
+ const env = options.env || process.env;
136
+ if (automaticUpdatesDisabled(env)) return { skipped: 'disabled' };
137
+ try {
138
+ const state = await readUpdateState(env);
139
+ if (!state.autoUpdate) return { skipped: 'not-enabled' };
140
+
141
+ const now = options.now || Date.now();
142
+ const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : 0;
143
+ if (Number.isFinite(lastCheckedAt) && now - lastCheckedAt < (options.intervalMs || CHECK_INTERVAL_MS)) {
144
+ return { skipped: 'not-due', state };
145
+ }
146
+
147
+ const check = await checkForUpdate(currentVersion, options);
148
+ const nextState = {
149
+ ...state,
150
+ lastCheckedAt: new Date(now).toISOString(),
151
+ };
152
+ await writeUpdateState(nextState, env);
153
+ if (!check.updateAvailable) {
154
+ return { ...check, updated: false };
155
+ }
156
+ installUpdate(check.latestVersion, options);
157
+ await writeUpdateState({
158
+ ...nextState,
159
+ lastUpdatedVersion: check.latestVersion,
160
+ }, env);
161
+ return { ...check, updated: true };
162
+ } catch (error) {
163
+ return { error: error.message };
164
+ }
165
+ }