@huicai/cli 0.5.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/bin/huicai.js ADDED
@@ -0,0 +1,578 @@
1
+ #!/usr/bin/env node
2
+ // huicai —— 终端智能体入口:解析参数并分发(交互 / 一次性 / login / models / help)。
3
+
4
+ import readline from 'node:readline';
5
+ import path from 'node:path';
6
+ import { c } from '../src/colors.js';
7
+ import { loadConfig, saveConfig, CONFIG_PATH, DEFAULT_MODEL } from '../src/config.js';
8
+ import { listModels, getAgentPersona, fetchUsage, listPlatformSkills, getPlatformSkill, pushPlatformSkill } from '../src/api.js';
9
+ import { deviceLogin } from '../src/device.js';
10
+ import { startRepl, runOnce } from '../src/repl.js';
11
+ import { buildSystemPrompt } from '../src/context.js';
12
+ import * as sessions from '../src/session.js';
13
+ import { loadLocalSkills, skillDirs, writeSkillToDir, readSkillDirForPush } from '../src/skills.js';
14
+
15
+ const VERSION = '0.5.1';
16
+
17
+ function parse(argv) {
18
+ const flags = {};
19
+ const positional = [];
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const a = argv[i];
22
+ switch (a) {
23
+ case '--yolo':
24
+ flags.yolo = true;
25
+ break;
26
+ case '-p':
27
+ case '--print':
28
+ flags.print = true;
29
+ break;
30
+ case '-h':
31
+ case '--help':
32
+ flags.help = true;
33
+ break;
34
+ case '-v':
35
+ case '--version':
36
+ flags.version = true;
37
+ break;
38
+ case '-m':
39
+ case '--model':
40
+ flags.model = argv[++i];
41
+ break;
42
+ case '--base':
43
+ flags.base = argv[++i];
44
+ break;
45
+ case '--key':
46
+ flags.key = argv[++i];
47
+ break;
48
+ case '--cwd':
49
+ flags.cwd = argv[++i];
50
+ break;
51
+ case '--agent':
52
+ flags.agent = argv[++i];
53
+ break;
54
+ case '--project':
55
+ flags.project = true;
56
+ break;
57
+ case '--overwrite':
58
+ flags.overwrite = true;
59
+ break;
60
+ case '--paste':
61
+ flags.paste = true;
62
+ break;
63
+ case '-c':
64
+ case '--continue':
65
+ flags.continue = true;
66
+ break;
67
+ case '-r':
68
+ case '--resume':
69
+ flags.resume = true;
70
+ break;
71
+ case '-a':
72
+ case '--all':
73
+ flags.all = true;
74
+ break;
75
+ case '--tui':
76
+ flags.tui = true;
77
+ break;
78
+ case '--plain':
79
+ flags.plain = true;
80
+ break;
81
+ case '--file':
82
+ flags.file = argv[++i];
83
+ break;
84
+ case '--prompt':
85
+ flags.prompt = argv[++i];
86
+ break;
87
+ case '--output-format':
88
+ flags.outputFormat = argv[++i];
89
+ break;
90
+ case '--max-turns':
91
+ flags.maxTurns = parseInt(argv[++i], 10);
92
+ break;
93
+ case '--system-prompt':
94
+ flags.systemPrompt = argv[++i];
95
+ break;
96
+ case '--append-system-prompt':
97
+ flags.appendSystemPrompt = argv[++i];
98
+ break;
99
+ default:
100
+ positional.push(a);
101
+ }
102
+ }
103
+ return { flags, positional };
104
+ }
105
+
106
+ function usage() {
107
+ process.stdout.write(`
108
+ ${c.bold('huicai')} — 终端智能体,以 HuiCai_AI 平台为模型后端,在本机执行工具。
109
+
110
+ 用法:
111
+ huicai 进入交互式会话(当前目录)
112
+ huicai "帮我修复登录bug" 进入会话并先执行该指令
113
+ huicai -p "运行测试并总结" 一次性执行后退出(脚本/CI;写/执行需 --yolo)
114
+ cat err.log | huicai -p "分析这段日志" 管道内容自动作为上下文
115
+ huicai --agent <id> 采用某已发布智能体的人格(工具仍在本机执行)
116
+ huicai -c ["prompt"] 继续当前目录最近一次会话
117
+ huicai -r 从当前目录历史会话中选择恢复
118
+ huicai resume <id> 恢复指定会话
119
+ huicai sessions [-a] 列出历史会话(-a 跨目录全部)
120
+ huicai login 浏览器授权登录(内置平台地址,无需输入网关)
121
+ huicai login --paste 无浏览器环境:手动粘贴 API 令牌
122
+ huicai models 列出可用模型
123
+ huicai usage 查看 Token 用量窗口(5小时/7天,按会员等级)
124
+ huicai skills 列出本地 + 平台技能(Agent Skills)
125
+ huicai skills pull [name] 平台技能 → 本地(默认 ~/.huicai/skills;--project 放入项目)
126
+ huicai skills push <name> 本地技能 → 平台(默认同名跳过;--overwrite 覆盖)
127
+ huicai help 显示本帮助
128
+
129
+ 选项:
130
+ -m, --model <id> 指定模型(覆盖配置)
131
+ --agent <id> 采用平台智能体人格(分享令牌或 agent-xxx)
132
+ -c, --continue 继续最近一次会话(当前目录)
133
+ -r, --resume 选择并恢复历史会话(当前目录)
134
+ --base <url> 覆盖内置平台地址(自建 / 联调用)
135
+ --key <sk-...> API 令牌(覆盖配置)
136
+ --cwd <dir> 指定工作目录(默认当前目录)
137
+ --yolo 自动放行写/执行工具(谨慎)
138
+ --tui / --plain 强制富 TUI(ink)/ 普通 REPL(默认 TTY 下自动用 TUI)
139
+ -p, --print 一次性非交互模式
140
+ --output-format <fmt> -p 输出格式:text(默认)| json(结构化,脚本集成)
141
+ --max-turns <n> 单回合最大工具轮数(默认 60)
142
+ --system-prompt <s> 完全替换系统提示词(-p/新会话)
143
+ --append-system-prompt <s> 在系统提示词末尾追加指示
144
+ -h, --help 帮助
145
+ -v, --version 版本
146
+
147
+ 交互技巧:
148
+ /help 查看命令 · @文件路径 引用文件内容 · !命令 直通 shell(输出进上下文)
149
+ 自定义命令:.huicai/commands/<名>.md($ARGUMENTS 为参数占位符)
150
+ 权限规则:.huicai/settings.json {"permissions":{"allow":["run_bash(git *)"]}}
151
+
152
+ 会话默认自动保存到 ~/.huicai/sessions(按项目目录归属),随时可 -c/-r 恢复。
153
+
154
+ 配置: ${CONFIG_PATH}(或环境变量 HUICAI_BASE_URL / HUICAI_API_KEY / HUICAI_MODEL / HUICAI_MAX_TURNS)
155
+ `);
156
+ }
157
+
158
+ async function login(flags = {}) {
159
+ const cur = loadConfig();
160
+ // 网关地址内置默认(huicai-ai.sjht.com);仅在显式 --base / 环境变量时覆盖,用户无需输入
161
+ const normBase = (flags.base ? String(flags.base) : cur.baseUrl).replace(/\/+$/, '');
162
+
163
+ let key = cur.apiKey;
164
+ if (!flags.paste) {
165
+ // 默认:直接打开浏览器授权(设备码流),无需任何输入
166
+ process.stdout.write(c.bold('\n登录 huicai') + c.gray(` · ${normBase}`) + '\n');
167
+ try {
168
+ key = await deviceLogin(normBase);
169
+ } catch (e) {
170
+ process.stdout.write(c.yellow(`浏览器授权未完成(${e.message}),可改用 huicai login --paste 手动粘贴令牌。`) + '\n');
171
+ return;
172
+ }
173
+ } else {
174
+ // 显式 --paste:无浏览器环境下手动粘贴令牌
175
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
176
+ key = (await new Promise((r) => rl.question(`API 令牌 (sk-huicai-…)${cur.apiKey ? ' [回车保留已保存]' : ''}: `, r))).trim()
177
+ || cur.apiKey;
178
+ rl.close();
179
+ if (!key) {
180
+ process.stdout.write(c.red('未提供令牌。\n'));
181
+ return;
182
+ }
183
+ }
184
+
185
+ // 校验令牌并挑一个「原始渠道模型」作默认(智能体伪模型 agent-/workflow-/datahuman- 走服务端编排,
186
+ // 不适合 CLI 的本机工具循环——用 `huicai --agent <ref>` 才是采用智能体的正确方式)。
187
+ let model = cur.model;
188
+ let tokenOk = true;
189
+ try {
190
+ const models = await listModels(normBase, key);
191
+ const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman)-/.test(id));
192
+ if (models.length === 0) {
193
+ process.stdout.write(c.yellow('⚠ 已拿到令牌,但平台没返回任何可用模型——通常是令牌无效或账号无模型权限,请联系管理员。') + '\n');
194
+ } else if (raw.length === 0) {
195
+ process.stdout.write(c.yellow('⚠ 你的账号没有可直连的原始模型(本机工具循环不可用)。仍可 `huicai -m agent-xxx` 与智能体/数字人/工作流做平台编排对话,或 `huicai --agent <分享令牌>`。') + '\n');
196
+ } else if (!raw.includes(model)) {
197
+ // 当前(默认)模型不在账号可用清单里 → 优先内置默认模型,否则退回第一个原始模型
198
+ model = raw.includes(DEFAULT_MODEL) ? DEFAULT_MODEL : raw[0];
199
+ }
200
+ } catch (e) {
201
+ tokenOk = false;
202
+ process.stdout.write(c.red('⚠ 令牌校验失败:' + e.message) + '\n');
203
+ }
204
+
205
+ saveConfig({ baseUrl: normBase, apiKey: key, model });
206
+ if (tokenOk) {
207
+ process.stdout.write(c.green('登录成功') + (model ? c.gray(` · 默认模型 ${model}`) : '') + '\n');
208
+ process.stdout.write(c.gray(`已保存到 ${CONFIG_PATH},直接运行 huicai 开始。`) + '\n');
209
+ } else {
210
+ process.stdout.write(c.yellow(`令牌已保存到 ${CONFIG_PATH},但校验未通过——请重试 huicai login 或联系管理员。`) + '\n');
211
+ }
212
+ }
213
+
214
+ function validateAuth(config) {
215
+ if (!config.baseUrl) return c.red('未配置平台地址。运行 `huicai login` 或设置 HUICAI_BASE_URL。');
216
+ if (!config.apiKey) return c.red('未配置 API 令牌。运行 `huicai login` 或设置 HUICAI_API_KEY。');
217
+ return null;
218
+ }
219
+
220
+ async function cmdModels(config) {
221
+ const problem = validateAuth(config);
222
+ if (problem) {
223
+ process.stderr.write(problem + '\n');
224
+ process.exit(1);
225
+ }
226
+ try {
227
+ const models = await listModels(config.baseUrl, config.apiKey);
228
+ process.stdout.write(models.map((m) => m.id).join('\n') + '\n');
229
+ } catch (e) {
230
+ process.stderr.write(c.red('拉取失败:' + e.message) + '\n');
231
+ process.exit(1);
232
+ }
233
+ }
234
+
235
+ /** usage 子命令:显示当前令牌用户的 5 小时 / 7 天 Token 用量窗口(按会员等级)。 */
236
+ async function cmdUsage(config) {
237
+ const problem = validateAuth(config);
238
+ if (problem) {
239
+ process.stderr.write(problem + '\n');
240
+ process.exit(1);
241
+ }
242
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
243
+ const fmt = (used, limit) => {
244
+ used = num(used);
245
+ limit = num(limit);
246
+ if (limit < 0) return `${used.toLocaleString()} / 无限`;
247
+ if (limit === 0) return '该档未开放';
248
+ const pct = Math.min(100, Math.round((used / limit) * 100));
249
+ return `${used.toLocaleString()} / ${limit.toLocaleString()} (${pct}%)`;
250
+ };
251
+ try {
252
+ const u = await fetchUsage(config.baseUrl, config.apiKey);
253
+ // 平台未部署 /v1/usage 或返回异常结构时给出清晰提示,而非崩栈
254
+ if (!u || (typeof u.used5h !== 'number' && typeof u.used7d !== 'number')) {
255
+ process.stderr.write(c.red('拉取用量失败:平台未返回用量数据(请确认平台已支持 /v1/usage)') + '\n');
256
+ process.exit(1);
257
+ }
258
+ process.stdout.write(c.bold('Token 用量窗口(按会员等级)') + '\n');
259
+ process.stdout.write(' 近 5 小时:' + fmt(u.used5h, u.limit5h) + '\n');
260
+ process.stdout.write(' 近 7 天 :' + fmt(u.used7d, u.limit7d) + '\n');
261
+ if (!u.allowed && u.message) {
262
+ process.stdout.write(c.yellow(' ' + u.message) + '\n');
263
+ } else if (u.resetAtEpochMs) {
264
+ process.stdout.write(c.gray(' 最早释放:' + new Date(u.resetAtEpochMs).toLocaleString()) + '\n');
265
+ }
266
+ } catch (e) {
267
+ process.stderr.write(c.red('拉取用量失败:' + e.message) + '\n');
268
+ process.exit(1);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * skills 子命令:本地技能与平台云端技能库的查看/同步。
274
+ * huicai skills 列出本地 + 平台技能
275
+ * huicai skills pull [name] 平台 → 本地(默认 ~/.huicai/skills;--project 放入项目 .huicai/skills)
276
+ * huicai skills push <name|目录> 本地 → 平台(默认同名跳过;--overwrite 覆盖)
277
+ */
278
+ async function cmdSkills(config, args, flags, cwd) {
279
+ const action = args[0] || 'list';
280
+ const locals = loadLocalSkills(cwd);
281
+
282
+ if (action === 'list') {
283
+ process.stdout.write(c.bold('本地技能') + c.gray('(.huicai/skills 与 ~/.huicai/skills)') + '\n');
284
+ if (!locals.length) {
285
+ process.stdout.write(c.gray(' (无。huicai skills pull 可从平台拉取)') + '\n');
286
+ } else {
287
+ for (const s of locals) {
288
+ process.stdout.write(` ${s.name}${s.version ? c.gray(' v' + s.version) : ''}`
289
+ + c.gray(`(${s.scope === 'project' ? '项目' : '全局'}${s.files.length ? ` · ${s.files.length} 附件` : ''})`)
290
+ + (s.description ? c.gray(' ' + s.description.slice(0, 60)) : '') + '\n');
291
+ }
292
+ }
293
+ const problem = validateAuth(config);
294
+ if (problem) {
295
+ process.stdout.write(c.gray('(未登录,跳过平台技能列表;huicai login 后可 pull/push)') + '\n');
296
+ return;
297
+ }
298
+ try {
299
+ const remote = await listPlatformSkills(config.baseUrl, config.apiKey);
300
+ process.stdout.write(c.bold('平台技能') + c.gray('(文档技能,' + config.baseUrl + ')') + '\n');
301
+ if (!remote.length) {
302
+ process.stdout.write(c.gray(' (无)') + '\n');
303
+ return;
304
+ }
305
+ const localNames = new Set(locals.map((s) => s.name));
306
+ for (const s of remote) {
307
+ process.stdout.write(` ${s.name}${s.version ? c.gray(' v' + s.version) : ''}`
308
+ + (localNames.has(s.name) ? c.green(' [已在本地]') : '')
309
+ + (s.description ? c.gray(' ' + String(s.description).slice(0, 60)) : '') + '\n');
310
+ }
311
+ process.stdout.write(c.gray('(huicai skills pull [name] 拉取到本地)') + '\n');
312
+ } catch (e) {
313
+ process.stderr.write(c.red('拉取平台技能失败:' + e.message) + '\n');
314
+ }
315
+ return;
316
+ }
317
+
318
+ const problem = validateAuth(config);
319
+ if (problem) {
320
+ process.stderr.write(problem + '\n');
321
+ process.exit(1);
322
+ }
323
+
324
+ if (action === 'pull') {
325
+ const wanted = args[1] || '';
326
+ const baseDir = flags.project ? skillDirs(cwd)[0] : skillDirs(cwd)[skillDirs(cwd).length - 1];
327
+ try {
328
+ const remote = await listPlatformSkills(config.baseUrl, config.apiKey);
329
+ const targets = wanted ? remote.filter((s) => s.name === wanted) : remote;
330
+ if (!targets.length) {
331
+ process.stderr.write(c.red(wanted ? `平台上没有技能:${wanted}` : '平台上没有可拉取的文档技能') + '\n');
332
+ process.exit(1);
333
+ }
334
+ for (const s of targets) {
335
+ const detail = await getPlatformSkill(config.baseUrl, config.apiKey, s.id);
336
+ const dir = writeSkillToDir(baseDir, detail);
337
+ process.stdout.write(c.green('已拉取 ') + s.name
338
+ + c.gray(`(${detail.files?.length || 0} 附件)→ ${dir}`) + '\n');
339
+ }
340
+ process.stdout.write(c.gray('新会话中生效(系统提示词注入技能目录,load_skill 按需载入)。') + '\n');
341
+ } catch (e) {
342
+ process.stderr.write(c.red('拉取失败:' + e.message) + '\n');
343
+ process.exit(1);
344
+ }
345
+ return;
346
+ }
347
+
348
+ if (action === 'push') {
349
+ const target = args[1];
350
+ if (!target) {
351
+ process.stderr.write(c.red('用法:huicai skills push <本地技能名|技能目录> [--overwrite]') + '\n');
352
+ process.exit(1);
353
+ }
354
+ // 按本地技能名解析(项目级优先),否则视为目录路径
355
+ const local = locals.find((s) => s.name === target);
356
+ const dir = local ? local.dir : path.resolve(cwd, target);
357
+ let payload;
358
+ try {
359
+ payload = readSkillDirForPush(dir);
360
+ } catch (e) {
361
+ process.stderr.write(c.red(`读取技能失败(目录须含 SKILL.md):${e.message}`) + '\n');
362
+ process.exit(1);
363
+ }
364
+ try {
365
+ const r = await pushPlatformSkill(config.baseUrl, config.apiKey, {
366
+ ...payload, conflict: flags.overwrite ? 'overwrite' : 'skip',
367
+ });
368
+ const label = r.status === 'imported' ? '已推送(新建)'
369
+ : r.status === 'overwritten' ? '已推送(覆盖同名)' : '已跳过:平台已有同名技能(--overwrite 可覆盖)';
370
+ process.stdout.write((r.status === 'skipped' ? c.yellow(label) : c.green(label))
371
+ + c.gray(` · ${payload.filename} → ${config.baseUrl}`) + '\n');
372
+ for (const w of r.warnings || []) {
373
+ process.stdout.write(c.yellow(' 规范提示:' + w) + '\n');
374
+ }
375
+ } catch (e) {
376
+ process.stderr.write(c.red('推送失败:' + e.message) + '\n');
377
+ process.exit(1);
378
+ }
379
+ return;
380
+ }
381
+
382
+ process.stderr.write(c.red(`未知子命令:skills ${action}(可用:list / pull / push)`) + '\n');
383
+ process.exit(1);
384
+ }
385
+
386
+ /** -p 模式下读取管道 stdin(若有):作为提示词本体或附加上下文。上限 5MB。 */
387
+ async function readPipedStdin() {
388
+ if (process.stdin.isTTY) return '';
389
+ const chunks = [];
390
+ let size = 0;
391
+ for await (const chunk of process.stdin) {
392
+ chunks.push(chunk);
393
+ size += chunk.length;
394
+ if (size > 5 * 1024 * 1024) break;
395
+ }
396
+ return Buffer.concat(chunks).toString('utf8').trim();
397
+ }
398
+
399
+ async function main() {
400
+ const { flags, positional } = parse(process.argv.slice(2));
401
+ if (flags.version) {
402
+ process.stdout.write('huicai ' + VERSION + '\n');
403
+ return;
404
+ }
405
+ const sub = positional[0];
406
+ if (flags.help || sub === 'help') {
407
+ usage();
408
+ return;
409
+ }
410
+ if (sub === 'login') {
411
+ await login(flags);
412
+ return;
413
+ }
414
+
415
+ const config = loadConfig();
416
+ if (flags.base) config.baseUrl = flags.base.replace(/\/+$/, '');
417
+ if (flags.key) config.apiKey = flags.key;
418
+ if (flags.model) config.model = flags.model;
419
+ if (Number.isFinite(flags.maxTurns) && flags.maxTurns > 0) config.maxTurns = flags.maxTurns;
420
+ const cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd();
421
+
422
+ // 会话列表:纯本地,无需鉴权
423
+ if (sub === 'sessions') {
424
+ cmdSessions(cwd, flags.all || positional.includes('--all') || positional.includes('-a'));
425
+ return;
426
+ }
427
+
428
+ if (sub === 'models') {
429
+ await cmdModels(config);
430
+ return;
431
+ }
432
+
433
+ if (sub === 'usage') {
434
+ await cmdUsage(config);
435
+ return;
436
+ }
437
+
438
+ if (sub === 'skills') {
439
+ await cmdSkills(config, positional.slice(1), flags, cwd);
440
+ return;
441
+ }
442
+
443
+ const problem = validateAuth(config);
444
+ if (problem) {
445
+ process.stderr.write(problem + '\n');
446
+ process.exit(1);
447
+ }
448
+
449
+ // --agent:采用平台智能体人格(其模型/系统提示词),工具仍在本机执行
450
+ let persona = null;
451
+ if (flags.agent) {
452
+ try {
453
+ const p = await getAgentPersona(config.baseUrl, config.apiKey, flags.agent);
454
+ persona = p?.systemPrompt || null;
455
+ if (p?.model && !flags.model) config.model = p.model; // 未显式 -m 时采用智能体的模型
456
+ process.stdout.write(c.gray(`已采用智能体人格:${p?.name || flags.agent}`) + '\n');
457
+ } catch (e) {
458
+ process.stderr.write(c.red('获取智能体人格失败:' + e.message) + '\n');
459
+ process.exit(1);
460
+ }
461
+ }
462
+
463
+ // 恢复会话:resume <id> 子命令 / -r 选择器 / -c 最近一次
464
+ let resumeId = null;
465
+ let positionalPrompt = positional;
466
+ if (sub === 'resume') {
467
+ resumeId = positional[1] || null;
468
+ positionalPrompt = positional.slice(2);
469
+ if (!resumeId) resumeId = await pickSession(cwd);
470
+ if (!resumeId) return; // 无会话/取消
471
+ } else if (flags.resume) {
472
+ resumeId = await pickSession(cwd);
473
+ if (!resumeId) return;
474
+ } else if (flags.continue) {
475
+ resumeId = sessions.mostRecent(cwd);
476
+ if (!resumeId) process.stdout.write(c.gray('当前目录暂无历史会话,将开启新会话。') + '\n');
477
+ }
478
+
479
+ let session = resumeId ? sessions.loadSession(resumeId) : null;
480
+ if (resumeId && !session) {
481
+ process.stderr.write(c.red('未找到会话: ' + resumeId) + '\n');
482
+ process.exit(1);
483
+ }
484
+ const resuming = !!session;
485
+ if (session && !flags.model && !flags.agent && session.model) {
486
+ config.model = session.model; // 恢复会话默认沿用其模型(-m/--agent 可覆盖)
487
+ }
488
+
489
+ if (!config.model) {
490
+ process.stderr.write(
491
+ c.red('未设置模型。用 `huicai -m <模型>`、`huicai login` 设置,或先 `huicai models` 查看可用模型。') + '\n',
492
+ );
493
+ process.exit(1);
494
+ }
495
+
496
+ // 新会话:用当前 cwd/persona 构建系统提示词(--system-prompt 完全替换、--append-system-prompt 追加)
497
+ if (!session) {
498
+ const sys = flags.systemPrompt
499
+ ? flags.systemPrompt
500
+ : buildSystemPrompt(cwd, { persona, append: flags.appendSystemPrompt });
501
+ session = sessions.createSession({ cwd, model: config.model, messages: [{ role: 'system', content: sys }] });
502
+ }
503
+
504
+ // 编辑器上下文(VS Code 扩展经 --file / 环境变量注入);context.js 会据此加入系统提示词
505
+ if (flags.file) process.env.HUICAI_IDE_FILE = flags.file;
506
+
507
+ let prompt = (flags.prompt || positionalPrompt.join(' ')).trim();
508
+
509
+ if (flags.print) {
510
+ // 管道 stdin:有提示词则作为附加上下文,无提示词则直接作为提示词(cat log | huicai -p)
511
+ const piped = await readPipedStdin();
512
+ if (piped) {
513
+ prompt = prompt ? `${prompt}\n\n【以下为管道输入(stdin)】\n${piped}` : piped;
514
+ }
515
+ await runOnce({
516
+ config, cwd, yolo: flags.yolo, prompt, session,
517
+ persist: resuming,
518
+ outputFormat: flags.outputFormat || 'text',
519
+ maxTurns: config.maxTurns || undefined,
520
+ });
521
+ return;
522
+ }
523
+
524
+ // 交互式:默认富 TUI(ink,需已安装依赖 + TTY);--plain 强制普通 REPL;TUI 不可用则自动回退
525
+ const wantTui = flags.tui || (!flags.plain && process.stdout.isTTY && process.stdin.isTTY);
526
+ if (wantTui) {
527
+ try {
528
+ const { runInk } = await import('../src/tui/app.js');
529
+ await runInk({ config, cwd, session, yolo: flags.yolo, initialPrompt: prompt || null, version: VERSION });
530
+ return;
531
+ } catch (e) {
532
+ process.stderr.write(c.gray('(未启用 TUI:' + e.message + ',回退普通模式)') + '\n');
533
+ }
534
+ }
535
+ await startRepl({ config, cwd, yolo: flags.yolo, initialPrompt: prompt || null, session, version: VERSION });
536
+ }
537
+
538
+ /** 交互式选择要恢复的会话(当前目录);返回选中的会话 id,或 null(无会话/取消)。 */
539
+ async function pickSession(cwd) {
540
+ const list = sessions.listSessions(cwd);
541
+ if (!list.length) {
542
+ process.stdout.write(c.gray('当前目录暂无历史会话。') + '\n');
543
+ return null;
544
+ }
545
+ process.stdout.write(c.bold('\n历史会话(当前目录):') + '\n');
546
+ list.slice(0, 20).forEach((s, i) => {
547
+ process.stdout.write(
548
+ ` ${c.cyan(String(i + 1).padStart(2))}) ${c.gray(sessions.relTime(s.updatedAt).padStart(8))} ${s.title} ${c.gray(s.turns + '轮')}\n`,
549
+ );
550
+ });
551
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
552
+ const ans = await new Promise((r) => rl.question(c.cyan('\n选择编号恢复(回车取消): '), r));
553
+ rl.close();
554
+ const idx = parseInt(String(ans).trim(), 10);
555
+ if (!ans || Number.isNaN(idx) || idx < 1 || idx > list.length) return null;
556
+ return list[idx - 1].id;
557
+ }
558
+
559
+ /** 列出会话(huicai sessions)。 */
560
+ function cmdSessions(cwd, all) {
561
+ const list = sessions.listSessions(all ? null : cwd);
562
+ if (!list.length) {
563
+ process.stdout.write(all ? '暂无历史会话。\n' : '当前目录暂无历史会话(-a/--all 查看全部)。\n');
564
+ return;
565
+ }
566
+ for (const s of list) {
567
+ process.stdout.write(
568
+ `${c.gray(sessions.relTime(s.updatedAt).padStart(9))} ${c.bold(s.id)} ${c.gray(s.turns + '轮')} ${s.title}\n`,
569
+ );
570
+ if (all) process.stdout.write(c.gray(` ${s.cwd || ''}`) + '\n');
571
+ }
572
+ process.stdout.write(c.gray('\n恢复:huicai resume <id> 或 huicai -c(最近一次)\n'));
573
+ }
574
+
575
+ main().catch((e) => {
576
+ process.stderr.write(c.red('\n致命错误:' + (e?.stack || e?.message || e)) + '\n');
577
+ process.exit(1);
578
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@huicai/cli",
3
+ "version": "0.5.1",
4
+ "description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
5
+ "type": "module",
6
+ "bin": {
7
+ "huicai": "bin/huicai.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "start": "node bin/huicai.js",
20
+ "check": "node --check bin/huicai.js",
21
+ "test": "node --test --test-concurrency=1 test/*.test.mjs",
22
+ "prepublishOnly": "node --check bin/huicai.js"
23
+ },
24
+ "keywords": [
25
+ "cli",
26
+ "agent",
27
+ "ai",
28
+ "terminal", "huicai",
29
+ "openai-compatible",
30
+ "coding-agent"
31
+ ],
32
+ "author": "ChenBo",
33
+ "license": "MIT",
34
+ "homepage": "https://github.com/SJHT-SDK/huicai-cli/tree/master#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/SJHT-SDK/huicai-cli.git/",
38
+ "directory": "huicai-cli"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/SJHT-SDK/huicai-cli/issues"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "ink": "^7.1.0",
48
+ "react": "^19.2.7"
49
+ }
50
+ }