@bolloon/bolloon-agent 0.1.8 → 0.1.10

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.
@@ -114,10 +114,17 @@ async function startCLI(additionalArgs) {
114
114
  }
115
115
 
116
116
  async function main() {
117
+ // Get version from package.json
118
+ const binPath = require.main.filename;
119
+ const binDir = path.dirname(binPath);
120
+ const packageDir = path.dirname(binDir);
121
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf-8"));
122
+ const version = packageJson.version;
123
+
117
124
  const { mode, args } = parseArgs();
118
125
  switch (mode) {
119
126
  case "version":
120
- console.log("Bolloon Agent v0.1.2");
127
+ console.log("Bolloon Agent v" + version);
121
128
  break;
122
129
  case "help":
123
130
  printBanner();
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ const path = require("path");
3
+ const { spawn } = require("child_process");
4
+ const fs = require("fs");
5
+
6
+ const RESET = "\x1b[0m";
7
+ const BOLD = "\x1b[1m";
8
+ const CYAN = "\x1b[36m";
9
+ const GREEN = "\x1b[32m";
10
+ const MAGENTA = "\x1b[35m";
11
+
12
+ function log(msg, color) {
13
+ console.log((color || RESET) + msg + RESET);
14
+ }
15
+
16
+ function printBanner() {
17
+ console.log("\n" + CYAN + BOLD + [
18
+ " ╔═══════════════════════════════════════════╗",
19
+ " ║ 🤖 Bolloon Agent ║",
20
+ " ║ P2P AI Document Processor ║",
21
+ " ╚═══════════════════════════════════════════╝"
22
+ ].join("\n") + RESET + "\n");
23
+ }
24
+
25
+ function getMainEntry() {
26
+ const distDir = path.dirname(require.main.filename);
27
+ const distIndex = path.join(distDir, "index.js");
28
+ if (fs.existsSync(distIndex)) {
29
+ return distIndex;
30
+ }
31
+ return path.join(process.cwd(), "src", "index.ts");
32
+ }
33
+
34
+ function parseArgs() {
35
+ const args = process.argv.slice(2);
36
+ if (args.length === 0) {
37
+ return { mode: "gui", args: [] };
38
+ }
39
+ const first = args[0];
40
+ switch (first) {
41
+ case "-v":
42
+ case "--version":
43
+ return { mode: "version", args: [] };
44
+ case "-h":
45
+ case "--help":
46
+ return { mode: "help", args: [] };
47
+ case "-g":
48
+ case "--gui":
49
+ return { mode: "gui", args: args.slice(1) };
50
+ case "-w":
51
+ case "--web":
52
+ return { mode: "web", args: args.slice(1) };
53
+ case "-c":
54
+ case "--cli":
55
+ return { mode: "cli", args: args.slice(1) };
56
+ default:
57
+ return { mode: "passthrough", args };
58
+ }
59
+ }
60
+
61
+ async function startElectron(additionalArgs) {
62
+ try {
63
+ const electron = require("electron");
64
+ const distDir = path.dirname(require.main.filename);
65
+ let mainPath = path.join(distDir, "electron.js");
66
+ if (!fs.existsSync(mainPath)) {
67
+ mainPath = path.join(process.cwd(), "src", "electron.ts");
68
+ }
69
+ log("启动 Electron...", CYAN);
70
+ const child = spawn(electron, [mainPath, ...additionalArgs], {
71
+ stdio: "inherit",
72
+ env: { ...process.env, NODE_ENV: "development" }
73
+ });
74
+ child.on("error", (err) => {
75
+ log("Electron 启动失败: " + err.message, MAGENTA);
76
+ process.exit(1);
77
+ });
78
+ child.on("exit", (code) => process.exit(code || 0));
79
+ } catch (err) {
80
+ log("Electron 不可用,切换到 Web 模式...", CYAN);
81
+ await startWebServer(additionalArgs);
82
+ }
83
+ }
84
+
85
+ async function startWebServer(additionalArgs) {
86
+ const mainPath = getMainEntry();
87
+ const webArgs = ["--web", ...additionalArgs];
88
+ log("启动 Web 服务...", CYAN);
89
+ const child = spawn(process.execPath, [mainPath, ...webArgs], { stdio: "inherit" });
90
+ child.on("error", (err) => {
91
+ log("Web 服务启动失败: " + err.message, MAGENTA);
92
+ process.exit(1);
93
+ });
94
+ child.on("exit", (code) => process.exit(code || 0));
95
+ }
96
+
97
+ async function startCLI(additionalArgs) {
98
+ const mainPath = getMainEntry();
99
+ log("启动命令行界面...", CYAN);
100
+ const child = spawn(process.execPath, [mainPath, ...additionalArgs], { stdio: "inherit" });
101
+ child.on("error", (err) => {
102
+ log("CLI 启动失败: " + err.message, MAGENTA);
103
+ process.exit(1);
104
+ });
105
+ child.on("exit", (code) => process.exit(code || 0));
106
+ }
107
+
108
+ async function main() {
109
+ const { mode, args } = parseArgs();
110
+ switch (mode) {
111
+ case "version":
112
+ console.log("Bolloon Agent v0.1.1");
113
+ break;
114
+ case "help":
115
+ printBanner();
116
+ console.log(BOLD + "用法:" + RESET + " bolloon [选项] [命令] [参数]");
117
+ console.log(BOLD + "选项:" + RESET + " --gui, -g 启动图形界面");
118
+ console.log(" --web, -w 启动 Web UI");
119
+ console.log(" --cli, -c 启动命令行界面");
120
+ console.log(" --version, -v 显示版本");
121
+ console.log(" --help, -h 显示帮助");
122
+ console.log(BOLD + "示例:" + RESET + " bolloon # 启动图形界面");
123
+ console.log(" bolloon --web # 启动 Web UI");
124
+ console.log(" bolloon --read file # 读取文档");
125
+ break;
126
+ case "gui":
127
+ printBanner();
128
+ await startElectron(args);
129
+ break;
130
+ case "web":
131
+ printBanner();
132
+ await startWebServer(args);
133
+ break;
134
+ case "cli":
135
+ await startCLI(args);
136
+ break;
137
+ case "passthrough":
138
+ const mainPath = getMainEntry();
139
+ const child = spawn(process.execPath, [mainPath, ...args], { stdio: "inherit" });
140
+ child.on("error", (err) => {
141
+ log("执行失败: " + err.message, MAGENTA);
142
+ process.exit(1);
143
+ });
144
+ child.on("exit", (code) => process.exit(code || 0));
145
+ break;
146
+ default:
147
+ log("未知模式: " + mode, MAGENTA);
148
+ printBanner();
149
+ console.log("输入 --help 查看帮助");
150
+ process.exit(1);
151
+ }
152
+ }
153
+
154
+ main().catch((err) => {
155
+ console.error("Fatal error:", err);
156
+ process.exit(1);
157
+ });
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { createSubAgentManager } from './agents/subagent-manager.js';
11
11
  import { getGlobalSharedContext } from './social/global-shared-context.js';
12
12
  import { createBollharnessIntegration } from './bollharness-integration/index.js';
13
13
  import * as readline from 'readline';
14
+ import { checkAndUpdate } from './utils/auto-update.js';
14
15
  const RESET = '\x1b[0m';
15
16
  const BOLD = '\x1b[1m';
16
17
  const DIM = '\x1b[2m';
@@ -496,6 +497,8 @@ const AVAILABLE_TOOLS = [
496
497
  { name: 'harness_classify', description: '分类变更类型', example: '--harness-classify <description>' },
497
498
  { name: 'harness_context', description: '获取文件上下文', example: '--harness-context <file>' },
498
499
  { name: 'harness_check', description: '执行 Guard 检查', example: '--harness-check <file>' },
500
+ { name: 'update_check', description: '检查 npm 包更新', example: '--update-check' },
501
+ { name: 'update_now', description: '立即更新到最新版本', example: '--update-now [package]' },
499
502
  ];
500
503
  async function runToolCommand(tool, args, outputJson, comm) {
501
504
  const a = await getAgent();
@@ -891,6 +894,38 @@ async function runToolCommand(tool, args, outputJson, comm) {
891
894
  response = `✅ 已添加用户行动: ${content.substring(0, 50)}...`;
892
895
  break;
893
896
  }
897
+ // ==================== Update Commands ====================
898
+ case 'update-check': {
899
+ const { checkForUpdates } = await import('./utils/auto-update.js');
900
+ const info = await checkForUpdates();
901
+ if (info && info.outdated) {
902
+ response = `📦 发现更新可用:\n\n` +
903
+ `当前版本: ${info.version}\n` +
904
+ `最新版本: ${info.latest}\n\n` +
905
+ `待更新包:\n${info.packages.map(p => ` - ${p.name}: ${p.current} → ${p.latest}`).join('\n')}\n\n` +
906
+ `运行 --update-now 进行更新`;
907
+ }
908
+ else {
909
+ response = `✅ 已是最新版本 (${info?.version || 'unknown'})`;
910
+ }
911
+ break;
912
+ }
913
+ case 'update-now': {
914
+ const { performUpdate } = await import('./utils/auto-update.js');
915
+ const packages = args.length > 0 ? args : undefined;
916
+ const result = await performUpdate(packages);
917
+ if (result.success) {
918
+ response = `✅ 更新成功${result.updatedPackages ? `: ${result.updatedPackages.join(', ')}` : ''}`;
919
+ if (result.updated) {
920
+ response += `\n\n${YELLOW}请重新启动应用以使用新版本${RESET}`;
921
+ }
922
+ }
923
+ else {
924
+ response = `❌ 更新失败: ${result.error}`;
925
+ error = response;
926
+ }
927
+ break;
928
+ }
894
929
  default:
895
930
  response = `错误: 未知工具 "${tool}"`;
896
931
  error = response;
@@ -1155,6 +1190,19 @@ function parseArgs() {
1155
1190
  }
1156
1191
  result.toolArgs = sessionArgs;
1157
1192
  break;
1193
+ case '--update-check':
1194
+ result.updateCheck = true;
1195
+ result.tool = 'update-check';
1196
+ break;
1197
+ case '--update-now':
1198
+ result.updateNow = true;
1199
+ result.tool = 'update-now';
1200
+ const updateArgs = [];
1201
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1202
+ updateArgs.push(args[++i]);
1203
+ }
1204
+ result.toolArgs = updateArgs;
1205
+ break;
1158
1206
  case '--tui':
1159
1207
  result.tui = true;
1160
1208
  break;
@@ -1224,6 +1272,10 @@ function printHelp() {
1224
1272
  --harness-sessions 列出 Session 归档记录
1225
1273
  --harness-session-context [id] 获取 Session 上下文
1226
1274
 
1275
+ # 自动更新
1276
+ --update-check 检查 npm 包更新
1277
+ --update-now [pkg] 更新到最新版本
1278
+
1227
1279
  # AI 对话
1228
1280
  --prompt, -p <text> 通用 AI 对话(默认)
1229
1281
  --model <name> 指定使用的模型
@@ -1272,6 +1324,13 @@ async function main() {
1272
1324
  printHelp();
1273
1325
  process.exit(0);
1274
1326
  }
1327
+ // 启动时检查 npm 包更新
1328
+ const updateResult = await checkAndUpdate();
1329
+ if (updateResult.hasUpdate && updateResult.updated) {
1330
+ console.log(`\n${YELLOW}更新完成,请重新启动 bolloon${RESET}\n`);
1331
+ // 更新完成后退出,让用户重新启动
1332
+ process.exit(0);
1333
+ }
1275
1334
  const mode = args.web ? 'web' : 'cli';
1276
1335
  const isNonInteractive = !!(args.tool || args.prompt);
1277
1336
  const isTuiMode = mode === 'cli' && !isNonInteractive && args.tui;
@@ -0,0 +1,310 @@
1
+ /**
2
+ * npm 自动更新检查器
3
+ *
4
+ * 功能:
5
+ * - 启动时检查 npm 注册表是否有新版本
6
+ * - 自动下载并安装最新版本
7
+ * - 支持增量更新(只更新新增的包)
8
+ */
9
+ import { execSync } from 'child_process';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import * as https from 'https';
13
+ import * as http from 'http';
14
+ // ANSI 颜色
15
+ const RESET = '\x1b[0m';
16
+ const BOLD = '\x1b[1m';
17
+ const CYAN = '\x1b[36m';
18
+ const GREEN = '\x1b[32m';
19
+ const YELLOW = '\x1b[33m';
20
+ const MAGENTA = '\x1b[35m';
21
+ function log(msg, color = RESET) {
22
+ process.stdout.write(`${color}${msg}${RESET}`);
23
+ }
24
+ function httpGet(url) {
25
+ return new Promise((resolve, reject) => {
26
+ const client = url.startsWith('https') ? https : http;
27
+ const req = client.get(url, { timeout: 10000 }, (res) => {
28
+ let data = '';
29
+ res.on('data', (chunk) => data += chunk);
30
+ res.on('end', () => resolve(data));
31
+ });
32
+ req.on('error', reject);
33
+ req.on('timeout', () => {
34
+ req.destroy();
35
+ reject(new Error('请求超时'));
36
+ });
37
+ });
38
+ }
39
+ /**
40
+ * 获取当前安装的包版本
41
+ */
42
+ function getInstalledVersion(packageName) {
43
+ try {
44
+ const packageJsonPath = findPackageJson(packageName);
45
+ if (packageJsonPath) {
46
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
47
+ return pkg.version || null;
48
+ }
49
+ }
50
+ catch (e) {
51
+ // 忽略错误
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * 查找包的 package.json 路径
57
+ */
58
+ function findPackageJson(packageName) {
59
+ // 检查当前项目的 node_modules
60
+ const checkPaths = [
61
+ path.join(process.cwd(), 'node_modules', packageName, 'package.json'),
62
+ path.join(process.cwd(), 'node_modules', '@' + packageName.split('/')[0], packageName.split('/')[1] || '', 'package.json'),
63
+ ];
64
+ for (const p of checkPaths) {
65
+ if (fs.existsSync(p))
66
+ return p;
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * 检查 npm 注册表获取最新版本
72
+ */
73
+ async function getLatestVersion(packageName) {
74
+ try {
75
+ const encodedName = encodeURIComponent(packageName);
76
+ const url = `https://registry.npmjs.org/${encodedName}`;
77
+ const response = await httpGet(url);
78
+ const data = JSON.parse(response);
79
+ return data['dist-tags']?.latest || null;
80
+ }
81
+ catch (e) {
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * 比较版本号
87
+ */
88
+ function compareVersions(current, latest) {
89
+ const currentParts = current.split('.').map(Number);
90
+ const latestParts = latest.split('.').map(Number);
91
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
92
+ const c = currentParts[i] || 0;
93
+ const l = latestParts[i] || 0;
94
+ if (c < l)
95
+ return -1;
96
+ if (c > l)
97
+ return 1;
98
+ }
99
+ return 0;
100
+ }
101
+ /**
102
+ * 检查 @bolloon 相关的包是否有更新
103
+ */
104
+ async function checkBolloonUpdates() {
105
+ const packagesToCheck = [
106
+ '@bolloon/bolloon-agent',
107
+ '@bolloon/constraint-runtime',
108
+ ];
109
+ const outdatedPackages = [];
110
+ let hasUpdate = false;
111
+ let currentVersion = '';
112
+ let latestVersion = '';
113
+ for (const pkg of packagesToCheck) {
114
+ const installed = getInstalledVersion(pkg);
115
+ if (!installed)
116
+ continue;
117
+ currentVersion = installed;
118
+ const latest = await getLatestVersion(pkg);
119
+ if (latest && compareVersions(installed, latest) < 0) {
120
+ hasUpdate = true;
121
+ latestVersion = latest;
122
+ outdatedPackages.push({
123
+ name: pkg,
124
+ current: installed,
125
+ wanted: latest,
126
+ latest,
127
+ location: pkg
128
+ });
129
+ }
130
+ }
131
+ if (outdatedPackages.length === 0) {
132
+ return {
133
+ name: '@bolloon/bolloon-agent',
134
+ version: currentVersion,
135
+ latest: currentVersion,
136
+ outdated: false,
137
+ packages: []
138
+ };
139
+ }
140
+ return {
141
+ name: '@bolloon/bolloon-agent',
142
+ version: currentVersion,
143
+ latest: latestVersion,
144
+ outdated: hasUpdate,
145
+ packages: outdatedPackages
146
+ };
147
+ }
148
+ /**
149
+ * 使用 npm outdated 检查所有包的更新
150
+ */
151
+ function checkNpmOutdated() {
152
+ try {
153
+ const output = execSync('npm outdated --json', {
154
+ encoding: 'utf-8',
155
+ timeout: 30000,
156
+ maxBuffer: 10 * 1024 * 1024,
157
+ cwd: process.cwd()
158
+ });
159
+ if (!output.trim())
160
+ return [];
161
+ const data = JSON.parse(output);
162
+ const packages = [];
163
+ for (const [name, info] of Object.entries(data)) {
164
+ const pkg = info;
165
+ packages.push({
166
+ name,
167
+ current: pkg.current || '0.0.0',
168
+ wanted: pkg.wanted || '0.0.0',
169
+ latest: pkg.latest || '0.0.0',
170
+ location: pkg.location || name
171
+ });
172
+ }
173
+ return packages;
174
+ }
175
+ catch (e) {
176
+ // npm outdated 在没有过时包时会返回非零退出码
177
+ if (e.status !== 0 && !e.message?.includes('npm outdated')) {
178
+ return [];
179
+ }
180
+ return [];
181
+ }
182
+ }
183
+ /**
184
+ * 自动更新 npm 包
185
+ */
186
+ async function updatePackages(packages) {
187
+ try {
188
+ const args = packages && packages.length > 0
189
+ ? ['npm', 'install', ...packages, '--save']
190
+ : ['npm', 'install', '-g', '@bolloon/bolloon-agent'];
191
+ log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
192
+ // 执行 npm install
193
+ const result = execSync(args.join(' '), {
194
+ encoding: 'utf-8',
195
+ timeout: 300000, // 5分钟超时
196
+ stdio: 'inherit',
197
+ cwd: process.cwd()
198
+ });
199
+ return {
200
+ success: true,
201
+ updated: true,
202
+ message: '更新成功',
203
+ updatedPackages: packages
204
+ };
205
+ }
206
+ catch (e) {
207
+ return {
208
+ success: false,
209
+ updated: false,
210
+ message: '更新失败',
211
+ error: e.message
212
+ };
213
+ }
214
+ }
215
+ /**
216
+ * 检查并自动更新(启动时调用)
217
+ */
218
+ export async function checkAndUpdate() {
219
+ // 检查是否有 --no-update 标志
220
+ if (process.argv.includes('--no-update') || process.argv.includes('--skip-update')) {
221
+ return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查' };
222
+ }
223
+ // 检查环境变量
224
+ if (process.env.BOLLOON_SKIP_UPDATE === 'true') {
225
+ return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查(环境变量)' };
226
+ }
227
+ log(`\n${CYAN}🔍 检查更新...${RESET}`, RESET);
228
+ try {
229
+ // 检查 @bolloon 包的更新
230
+ const bolloonInfo = await checkBolloonUpdates();
231
+ if (bolloonInfo && bolloonInfo.outdated) {
232
+ log(`\n${YELLOW}⚠️ 发现新版本: ${bolloonInfo.latest}${RESET}\n`, RESET);
233
+ log(` 当前版本: ${bolloonInfo.version}\n`, RESET);
234
+ log(` 最新版本: ${bolloonInfo.latest}\n\n`, RESET);
235
+ // 自动更新
236
+ const result = await updatePackages(bolloonInfo.packages.map(p => p.name));
237
+ if (result.success) {
238
+ log(`\n${GREEN}✅ 更新成功!请重新启动应用${RESET}\n`, RESET);
239
+ // 提示用户重启
240
+ log(`${YELLOW}💡 请重新运行 bolloon 以使用新版本${RESET}\n\n`, RESET);
241
+ // 通知主进程更新完成
242
+ process.emit('bolloon-update-complete', result);
243
+ return {
244
+ hasUpdate: true,
245
+ info: bolloonInfo,
246
+ updated: true,
247
+ message: `已更新到 ${bolloonInfo.latest}`
248
+ };
249
+ }
250
+ else {
251
+ return {
252
+ hasUpdate: true,
253
+ info: bolloonInfo,
254
+ updated: false,
255
+ message: `更新失败: ${result.error}`
256
+ };
257
+ }
258
+ }
259
+ else {
260
+ log(` ${GREEN}✓${RESET} 已是最新版本 (${bolloonInfo?.version || 'unknown'})\n`, RESET);
261
+ return {
262
+ hasUpdate: false,
263
+ info: bolloonInfo,
264
+ updated: false,
265
+ message: '已是最新版本'
266
+ };
267
+ }
268
+ }
269
+ catch (e) {
270
+ log(` ${YELLOW}⚠${RESET} 更新检查失败: ${e.message}\n`, RESET);
271
+ return {
272
+ hasUpdate: false,
273
+ info: null,
274
+ updated: false,
275
+ message: `检查失败: ${e.message}`
276
+ };
277
+ }
278
+ }
279
+ /**
280
+ * 仅检查更新,不自动安装
281
+ */
282
+ export async function checkForUpdates() {
283
+ return await checkBolloonUpdates();
284
+ }
285
+ /**
286
+ * 手动触发更新
287
+ */
288
+ export async function performUpdate(packages) {
289
+ return await updatePackages(packages);
290
+ }
291
+ // CLI 入口
292
+ if (import.meta.url === `file://${process.argv[1]}`) {
293
+ (async () => {
294
+ const command = process.argv[2];
295
+ switch (command) {
296
+ case 'check':
297
+ const info = await checkForUpdates();
298
+ if (info) {
299
+ console.log(JSON.stringify(info, null, 2));
300
+ }
301
+ break;
302
+ case 'update':
303
+ const result = await performUpdate(process.argv.slice(3));
304
+ console.log(JSON.stringify(result, null, 2));
305
+ break;
306
+ default:
307
+ await checkAndUpdate();
308
+ }
309
+ })();
310
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
@@ -32,7 +32,7 @@
32
32
  "src/constraint-runtime"
33
33
  ],
34
34
  "dependencies": {
35
- "@bolloon/bolloon-agent": "^0.1.5",
35
+ "@bolloon/bolloon-agent": "^0.1.9",
36
36
  "@bolloon/constraint-runtime": "0.1.0",
37
37
  "@chainsafe/libp2p-noise": "^17.0.0",
38
38
  "@chainsafe/libp2p-yamux": "^8.0.1",
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import { createSubAgentManager } from './agents/subagent-manager.js';
22
22
  import { getGlobalSharedContext } from './social/global-shared-context.js';
23
23
  import { BollharnessIntegration, createBollharnessIntegration } from './bollharness-integration/index.js';
24
24
  import * as readline from 'readline';
25
+ import { checkAndUpdate } from './utils/auto-update.js';
25
26
 
26
27
  const RESET = '\x1b[0m';
27
28
  const BOLD = '\x1b[1m';
@@ -601,6 +602,8 @@ const AVAILABLE_TOOLS = [
601
602
  { name: 'harness_classify', description: '分类变更类型', example: '--harness-classify <description>' },
602
603
  { name: 'harness_context', description: '获取文件上下文', example: '--harness-context <file>' },
603
604
  { name: 'harness_check', description: '执行 Guard 检查', example: '--harness-check <file>' },
605
+ { name: 'update_check', description: '检查 npm 包更新', example: '--update-check' },
606
+ { name: 'update_now', description: '立即更新到最新版本', example: '--update-now [package]' },
604
607
  ];
605
608
 
606
609
  async function runToolCommand(
@@ -1036,6 +1039,39 @@ async function runToolCommand(
1036
1039
  break;
1037
1040
  }
1038
1041
 
1042
+ // ==================== Update Commands ====================
1043
+
1044
+ case 'update-check': {
1045
+ const { checkForUpdates } = await import('./utils/auto-update.js');
1046
+ const info = await checkForUpdates();
1047
+ if (info && info.outdated) {
1048
+ response = `📦 发现更新可用:\n\n` +
1049
+ `当前版本: ${info.version}\n` +
1050
+ `最新版本: ${info.latest}\n\n` +
1051
+ `待更新包:\n${info.packages.map(p => ` - ${p.name}: ${p.current} → ${p.latest}`).join('\n')}\n\n` +
1052
+ `运行 --update-now 进行更新`;
1053
+ } else {
1054
+ response = `✅ 已是最新版本 (${info?.version || 'unknown'})`;
1055
+ }
1056
+ break;
1057
+ }
1058
+
1059
+ case 'update-now': {
1060
+ const { performUpdate } = await import('./utils/auto-update.js');
1061
+ const packages = args.length > 0 ? args : undefined;
1062
+ const result = await performUpdate(packages as string[] | undefined);
1063
+ if (result.success) {
1064
+ response = `✅ 更新成功${result.updatedPackages ? `: ${result.updatedPackages.join(', ')}` : ''}`;
1065
+ if (result.updated) {
1066
+ response += `\n\n${YELLOW}请重新启动应用以使用新版本${RESET}`;
1067
+ }
1068
+ } else {
1069
+ response = `❌ 更新失败: ${result.error}`;
1070
+ error = response;
1071
+ }
1072
+ break;
1073
+ }
1074
+
1039
1075
  default:
1040
1076
  response = `错误: 未知工具 "${tool}"`;
1041
1077
  error = response;
@@ -1151,6 +1187,8 @@ interface ParsedArgs {
1151
1187
  globalAgents?: boolean;
1152
1188
  addAction?: boolean;
1153
1189
  tui?: boolean;
1190
+ updateCheck?: boolean;
1191
+ updateNow?: boolean;
1154
1192
  }
1155
1193
 
1156
1194
  function parseArgs(): ParsedArgs {
@@ -1338,6 +1376,19 @@ function parseArgs(): ParsedArgs {
1338
1376
  }
1339
1377
  result.toolArgs = sessionArgs;
1340
1378
  break;
1379
+ case '--update-check':
1380
+ result.updateCheck = true;
1381
+ result.tool = 'update-check';
1382
+ break;
1383
+ case '--update-now':
1384
+ result.updateNow = true;
1385
+ result.tool = 'update-now';
1386
+ const updateArgs: string[] = [];
1387
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1388
+ updateArgs.push(args[++i]);
1389
+ }
1390
+ result.toolArgs = updateArgs;
1391
+ break;
1341
1392
  case '--tui':
1342
1393
  result.tui = true;
1343
1394
  break;
@@ -1409,6 +1460,10 @@ function printHelp(): void {
1409
1460
  --harness-sessions 列出 Session 归档记录
1410
1461
  --harness-session-context [id] 获取 Session 上下文
1411
1462
 
1463
+ # 自动更新
1464
+ --update-check 检查 npm 包更新
1465
+ --update-now [pkg] 更新到最新版本
1466
+
1412
1467
  # AI 对话
1413
1468
  --prompt, -p <text> 通用 AI 对话(默认)
1414
1469
  --model <name> 指定使用的模型
@@ -1461,6 +1516,14 @@ async function main() {
1461
1516
  process.exit(0);
1462
1517
  }
1463
1518
 
1519
+ // 启动时检查 npm 包更新
1520
+ const updateResult = await checkAndUpdate();
1521
+ if (updateResult.hasUpdate && updateResult.updated) {
1522
+ console.log(`\n${YELLOW}更新完成,请重新启动 bolloon${RESET}\n`);
1523
+ // 更新完成后退出,让用户重新启动
1524
+ process.exit(0);
1525
+ }
1526
+
1464
1527
  const mode = args.web ? 'web' : 'cli';
1465
1528
  const isNonInteractive = !!(args.tool || args.prompt);
1466
1529
  const isTuiMode = mode === 'cli' && !isNonInteractive && args.tui;
@@ -0,0 +1,369 @@
1
+ /**
2
+ * npm 自动更新检查器
3
+ *
4
+ * 功能:
5
+ * - 启动时检查 npm 注册表是否有新版本
6
+ * - 自动下载并安装最新版本
7
+ * - 支持增量更新(只更新新增的包)
8
+ */
9
+
10
+ import { execSync } from 'child_process';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import * as https from 'https';
14
+ import * as http from 'http';
15
+
16
+ // ANSI 颜色
17
+ const RESET = '\x1b[0m';
18
+ const BOLD = '\x1b[1m';
19
+ const CYAN = '\x1b[36m';
20
+ const GREEN = '\x1b[32m';
21
+ const YELLOW = '\x1b[33m';
22
+ const MAGENTA = '\x1b[35m';
23
+
24
+ interface PackageInfo {
25
+ name: string;
26
+ version: string;
27
+ latest: string;
28
+ outdated: boolean;
29
+ packages: OutdatedPackage[];
30
+ }
31
+
32
+ interface OutdatedPackage {
33
+ name: string;
34
+ current: string;
35
+ wanted: string;
36
+ latest: string;
37
+ location: string;
38
+ }
39
+
40
+ interface UpdateResult {
41
+ success: boolean;
42
+ updated: boolean;
43
+ message: string;
44
+ updatedPackages?: string[];
45
+ error?: string;
46
+ }
47
+
48
+ function log(msg: string, color: string = RESET) {
49
+ process.stdout.write(`${color}${msg}${RESET}`);
50
+ }
51
+
52
+ function httpGet(url: string): Promise<string> {
53
+ return new Promise((resolve, reject) => {
54
+ const client = url.startsWith('https') ? https : http;
55
+ const req = client.get(url, { timeout: 10000 }, (res: any) => {
56
+ let data = '';
57
+ res.on('data', (chunk: string) => data += chunk);
58
+ res.on('end', () => resolve(data));
59
+ });
60
+ req.on('error', reject);
61
+ req.on('timeout', () => {
62
+ req.destroy();
63
+ reject(new Error('请求超时'));
64
+ });
65
+ });
66
+ }
67
+
68
+ /**
69
+ * 获取当前安装的包版本
70
+ */
71
+ function getInstalledVersion(packageName: string): string | null {
72
+ try {
73
+ const packageJsonPath = findPackageJson(packageName);
74
+ if (packageJsonPath) {
75
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
76
+ return pkg.version || null;
77
+ }
78
+ } catch (e) {
79
+ // 忽略错误
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * 查找包的 package.json 路径
86
+ */
87
+ function findPackageJson(packageName: string): string | null {
88
+ // 检查当前项目的 node_modules
89
+ const checkPaths = [
90
+ path.join(process.cwd(), 'node_modules', packageName, 'package.json'),
91
+ path.join(process.cwd(), 'node_modules', '@' + packageName.split('/')[0], packageName.split('/')[1] || '', 'package.json'),
92
+ ];
93
+
94
+ for (const p of checkPaths) {
95
+ if (fs.existsSync(p)) return p;
96
+ }
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * 检查 npm 注册表获取最新版本
102
+ */
103
+ async function getLatestVersion(packageName: string): Promise<string | null> {
104
+ try {
105
+ const encodedName = encodeURIComponent(packageName);
106
+ const url = `https://registry.npmjs.org/${encodedName}`;
107
+ const response = await httpGet(url);
108
+ const data = JSON.parse(response);
109
+ return data['dist-tags']?.latest || null;
110
+ } catch (e) {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 比较版本号
117
+ */
118
+ function compareVersions(current: string, latest: string): -1 | 0 | 1 {
119
+ const currentParts = current.split('.').map(Number);
120
+ const latestParts = latest.split('.').map(Number);
121
+
122
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
123
+ const c = currentParts[i] || 0;
124
+ const l = latestParts[i] || 0;
125
+ if (c < l) return -1;
126
+ if (c > l) return 1;
127
+ }
128
+ return 0;
129
+ }
130
+
131
+ /**
132
+ * 检查 @bolloon 相关的包是否有更新
133
+ */
134
+ async function checkBolloonUpdates(): Promise<PackageInfo | null> {
135
+ const packagesToCheck = [
136
+ '@bolloon/bolloon-agent',
137
+ '@bolloon/constraint-runtime',
138
+ ];
139
+
140
+ const outdatedPackages: OutdatedPackage[] = [];
141
+ let hasUpdate = false;
142
+ let currentVersion = '';
143
+ let latestVersion = '';
144
+
145
+ for (const pkg of packagesToCheck) {
146
+ const installed = getInstalledVersion(pkg);
147
+ if (!installed) continue;
148
+
149
+ currentVersion = installed;
150
+ const latest = await getLatestVersion(pkg);
151
+
152
+ if (latest && compareVersions(installed, latest) < 0) {
153
+ hasUpdate = true;
154
+ latestVersion = latest;
155
+ outdatedPackages.push({
156
+ name: pkg,
157
+ current: installed,
158
+ wanted: latest,
159
+ latest,
160
+ location: pkg
161
+ });
162
+ }
163
+ }
164
+
165
+ if (outdatedPackages.length === 0) {
166
+ return {
167
+ name: '@bolloon/bolloon-agent',
168
+ version: currentVersion,
169
+ latest: currentVersion,
170
+ outdated: false,
171
+ packages: []
172
+ };
173
+ }
174
+
175
+ return {
176
+ name: '@bolloon/bolloon-agent',
177
+ version: currentVersion,
178
+ latest: latestVersion,
179
+ outdated: hasUpdate,
180
+ packages: outdatedPackages
181
+ };
182
+ }
183
+
184
+ /**
185
+ * 使用 npm outdated 检查所有包的更新
186
+ */
187
+ function checkNpmOutdated(): OutdatedPackage[] {
188
+ try {
189
+ const output = execSync('npm outdated --json', {
190
+ encoding: 'utf-8',
191
+ timeout: 30000,
192
+ maxBuffer: 10 * 1024 * 1024,
193
+ cwd: process.cwd()
194
+ });
195
+
196
+ if (!output.trim()) return [];
197
+
198
+ const data = JSON.parse(output);
199
+ const packages: OutdatedPackage[] = [];
200
+
201
+ for (const [name, info] of Object.entries(data)) {
202
+ const pkg = info as any;
203
+ packages.push({
204
+ name,
205
+ current: pkg.current || '0.0.0',
206
+ wanted: pkg.wanted || '0.0.0',
207
+ latest: pkg.latest || '0.0.0',
208
+ location: pkg.location || name
209
+ });
210
+ }
211
+
212
+ return packages;
213
+ } catch (e: any) {
214
+ // npm outdated 在没有过时包时会返回非零退出码
215
+ if (e.status !== 0 && !e.message?.includes('npm outdated')) {
216
+ return [];
217
+ }
218
+ return [];
219
+ }
220
+ }
221
+
222
+ /**
223
+ * 自动更新 npm 包
224
+ */
225
+ async function updatePackages(packages?: string[]): Promise<UpdateResult> {
226
+ try {
227
+ const args = packages && packages.length > 0
228
+ ? ['npm', 'install', ...packages, '--save']
229
+ : ['npm', 'install', '-g', '@bolloon/bolloon-agent'];
230
+
231
+ log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
232
+
233
+ // 执行 npm install
234
+ const result = execSync(args.join(' '), {
235
+ encoding: 'utf-8',
236
+ timeout: 300000, // 5分钟超时
237
+ stdio: 'inherit',
238
+ cwd: process.cwd()
239
+ });
240
+
241
+ return {
242
+ success: true,
243
+ updated: true,
244
+ message: '更新成功',
245
+ updatedPackages: packages
246
+ };
247
+ } catch (e: any) {
248
+ return {
249
+ success: false,
250
+ updated: false,
251
+ message: '更新失败',
252
+ error: e.message
253
+ };
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 检查并自动更新(启动时调用)
259
+ */
260
+ export async function checkAndUpdate(): Promise<{
261
+ hasUpdate: boolean;
262
+ info: PackageInfo | null;
263
+ updated: boolean;
264
+ message: string;
265
+ }> {
266
+ // 检查是否有 --no-update 标志
267
+ if (process.argv.includes('--no-update') || process.argv.includes('--skip-update')) {
268
+ return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查' };
269
+ }
270
+
271
+ // 检查环境变量
272
+ if (process.env.BOLLOON_SKIP_UPDATE === 'true') {
273
+ return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查(环境变量)' };
274
+ }
275
+
276
+ log(`\n${CYAN}🔍 检查更新...${RESET}`, RESET);
277
+
278
+ try {
279
+ // 检查 @bolloon 包的更新
280
+ const bolloonInfo = await checkBolloonUpdates();
281
+
282
+ if (bolloonInfo && bolloonInfo.outdated) {
283
+ log(`\n${YELLOW}⚠️ 发现新版本: ${bolloonInfo.latest}${RESET}\n`, RESET);
284
+ log(` 当前版本: ${bolloonInfo.version}\n`, RESET);
285
+ log(` 最新版本: ${bolloonInfo.latest}\n\n`, RESET);
286
+
287
+ // 自动更新
288
+ const result = await updatePackages(bolloonInfo.packages.map(p => p.name));
289
+
290
+ if (result.success) {
291
+ log(`\n${GREEN}✅ 更新成功!请重新启动应用${RESET}\n`, RESET);
292
+
293
+ // 提示用户重启
294
+ log(`${YELLOW}💡 请重新运行 bolloon 以使用新版本${RESET}\n\n`, RESET);
295
+
296
+ // 通知主进程更新完成
297
+ process.emit('bolloon-update-complete', result);
298
+
299
+ return {
300
+ hasUpdate: true,
301
+ info: bolloonInfo,
302
+ updated: true,
303
+ message: `已更新到 ${bolloonInfo.latest}`
304
+ };
305
+ } else {
306
+ return {
307
+ hasUpdate: true,
308
+ info: bolloonInfo,
309
+ updated: false,
310
+ message: `更新失败: ${result.error}`
311
+ };
312
+ }
313
+ } else {
314
+ log(` ${GREEN}✓${RESET} 已是最新版本 (${bolloonInfo?.version || 'unknown'})\n`, RESET);
315
+ return {
316
+ hasUpdate: false,
317
+ info: bolloonInfo,
318
+ updated: false,
319
+ message: '已是最新版本'
320
+ };
321
+ }
322
+ } catch (e: any) {
323
+ log(` ${YELLOW}⚠${RESET} 更新检查失败: ${e.message}\n`, RESET);
324
+ return {
325
+ hasUpdate: false,
326
+ info: null,
327
+ updated: false,
328
+ message: `检查失败: ${e.message}`
329
+ };
330
+ }
331
+ }
332
+
333
+ /**
334
+ * 仅检查更新,不自动安装
335
+ */
336
+ export async function checkForUpdates(): Promise<PackageInfo | null> {
337
+ return await checkBolloonUpdates();
338
+ }
339
+
340
+ /**
341
+ * 手动触发更新
342
+ */
343
+ export async function performUpdate(packages?: string[]): Promise<UpdateResult> {
344
+ return await updatePackages(packages);
345
+ }
346
+
347
+ // CLI 入口
348
+ if (process.argv[1]?.includes('auto-update')) {
349
+ (async () => {
350
+ const command = process.argv[2];
351
+
352
+ switch (command) {
353
+ case 'check':
354
+ const info = await checkForUpdates();
355
+ if (info) {
356
+ console.log(JSON.stringify(info, null, 2));
357
+ }
358
+ break;
359
+
360
+ case 'update':
361
+ const result = await performUpdate(process.argv.slice(3));
362
+ console.log(JSON.stringify(result, null, 2));
363
+ break;
364
+
365
+ default:
366
+ await checkAndUpdate();
367
+ }
368
+ })();
369
+ }