@huicai/cli 2026.711.0 → 2026.711.24616

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.
Files changed (49) hide show
  1. package/LICENSE +43 -21
  2. package/package.json +4 -3
  3. package/src/advisor.js +1 -46
  4. package/src/agent.js +1 -518
  5. package/src/api.js +1 -359
  6. package/src/autofix.js +1 -90
  7. package/src/banner.js +1 -83
  8. package/src/checkpoints.js +1 -99
  9. package/src/clipboard.js +1 -34
  10. package/src/codeindex.js +1 -229
  11. package/src/colors.js +1 -47
  12. package/src/commands.js +1 -1294
  13. package/src/compaction.js +1 -144
  14. package/src/config.js +1 -86
  15. package/src/consult.js +1 -65
  16. package/src/context-report.js +1 -119
  17. package/src/context.js +1 -152
  18. package/src/cron.js +1 -64
  19. package/src/daemon.js +1 -62
  20. package/src/device.js +1 -88
  21. package/src/diffview.js +1 -115
  22. package/src/doctor.js +1 -72
  23. package/src/executors.js +1 -391
  24. package/src/forks.js +1 -59
  25. package/src/gitflow.js +1 -140
  26. package/src/hooks.js +1 -207
  27. package/src/ide.js +1 -53
  28. package/src/input.js +1 -182
  29. package/src/insights.js +1 -102
  30. package/src/kbsync.js +1 -141
  31. package/src/markdown.js +0 -0
  32. package/src/markup-filter.js +1 -85
  33. package/src/mcp.js +1 -207
  34. package/src/memory.js +1 -161
  35. package/src/permissions.js +1 -120
  36. package/src/render.js +1 -138
  37. package/src/repl.js +1 -271
  38. package/src/scan.js +1 -84
  39. package/src/schedule.js +1 -48
  40. package/src/session.js +1 -140
  41. package/src/settings.js +0 -0
  42. package/src/skills.js +1 -248
  43. package/src/subagent.js +1 -183
  44. package/src/todo.js +1 -75
  45. package/src/tools.js +1 -147
  46. package/src/transcript.js +1 -94
  47. package/src/tui/app.js +1 -666
  48. package/src/usage.js +1 -43
  49. package/src/watch.js +1 -74
package/src/cron.js CHANGED
@@ -1,64 +1 @@
1
- // 极简 5 字段 cron 匹配器(分 时 日 月 周):支持 * / N / N-M / N-M/S / */S / 列表(A,B,C)。
2
- // 周字段 0 与 7 均表示周日。日与周的「或/与」语义遵循标准 cron。
3
-
4
- function matchPart(part, value, min, max) {
5
- part = part.trim();
6
- let step = 1;
7
- let range = part;
8
- const slash = part.indexOf('/');
9
- if (slash >= 0) {
10
- step = parseInt(part.slice(slash + 1), 10) || 1;
11
- range = part.slice(0, slash);
12
- }
13
- let lo;
14
- let hi;
15
- if (range === '*') {
16
- lo = min;
17
- hi = max;
18
- } else if (range.includes('-')) {
19
- const [a, b] = range.split('-');
20
- lo = parseInt(a, 10);
21
- hi = parseInt(b, 10);
22
- } else {
23
- lo = parseInt(range, 10);
24
- hi = slash >= 0 ? max : lo; // "N/S" 表示从 N 起步长 S 到 max
25
- }
26
- if (Number.isNaN(lo) || Number.isNaN(hi)) return false;
27
- if (value < lo || value > hi) return false;
28
- return ((value - lo) % step) === 0;
29
- }
30
-
31
- function matchField(expr, value, min, max) {
32
- return String(expr).split(',').some((p) => matchPart(p, value, min, max));
33
- }
34
-
35
- /** date 是否匹配 cron 表达式。 */
36
- export function cronMatches(expr, date = new Date()) {
37
- const fields = String(expr).trim().split(/\s+/);
38
- if (fields.length !== 5) return false;
39
- const [minF, hourF, domF, monF, dowF] = fields;
40
- const minute = date.getMinutes();
41
- const hour = date.getHours();
42
- const dom = date.getDate();
43
- const mon = date.getMonth() + 1;
44
- const dow = date.getDay(); // 0=周日
45
-
46
- if (!matchField(minF, minute, 0, 59)) return false;
47
- if (!matchField(hourF, hour, 0, 23)) return false;
48
- if (!matchField(monF, mon, 1, 12)) return false;
49
-
50
- const domStar = domF.trim() === '*';
51
- const dowStar = dowF.trim() === '*';
52
- const domOk = matchField(domF, dom, 1, 31);
53
- const dowOk = matchField(dowF, dow, 0, 7) || (dow === 0 && matchField(dowF, 7, 0, 7));
54
- // 标准 cron:日与周都非 * 时取「或」;否则取「与」
55
- const dayOk = (domStar && dowStar) ? true
56
- : (!domStar && !dowStar) ? (domOk || dowOk)
57
- : (domStar ? dowOk : domOk);
58
- return dayOk;
59
- }
60
-
61
- /** 校验一个 cron 表达式是否 5 字段(不校验字段合法性细节)。 */
62
- export function isValidCron(expr) {
63
- return String(expr).trim().split(/\s+/).length === 5;
64
- }
1
+ function t(t,e,n,r){return String(t).split(",").some(t=>function(t,e,n,r){let i=1,s=t=t.trim();const u=t.indexOf("/");let o,c;if(u>=0&&(i=parseInt(t.slice(u+1),10)||1,s=t.slice(0,u)),"*"===s)o=n,c=r;else if(s.includes("-")){const[t,e]=s.split("-");o=parseInt(t,10),c=parseInt(e,10)}else o=parseInt(s,10),c=u>=0?r:o;return!(Number.isNaN(o)||Number.isNaN(c)||e<o||e>c||(e-o)%i!==0)}(t,e,n,r))}export function cronMatches(e,n=new Date){const r=String(e).trim().split(/\s+/);if(5!==r.length)return!1;const[i,s,u,o,c]=r,l=n.getMinutes(),a=n.getHours(),f=n.getDate(),g=n.getMonth()+1,p=n.getDay();if(!t(i,l,0,59))return!1;if(!t(s,a,0,23))return!1;if(!t(o,g,1,12))return!1;const m="*"===u.trim(),N="*"===c.trim(),h=t(u,f,1,31),I=t(c,p,0,7)||0===p&&t(c,7,0,7);return!(!m||!N)||(m||N?m?I:h:h||I)}export function isValidCron(t){return 5===String(t).trim().split(/\s+/).length}
package/src/daemon.js CHANGED
@@ -1,62 +1 @@
1
- // huicai daemon:内置调度守护——按 cron 真正运行 /schedule 登记的任务(不再依赖系统 cron)。
2
- // 每分钟检查一次,对命中的任务派生 `huicai -p <prompt> --yolo --plain`(非交互自动放行),输出写日志。
3
-
4
- import { spawn } from 'node:child_process';
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import os from 'node:os';
8
- import { listTasks } from './schedule.js';
9
- import { cronMatches } from './cron.js';
10
- import { huicaiBin } from './forks.js';
11
-
12
- export const DAEMON_LOG_DIR = path.join(os.homedir(), '.huicai', 'daemon');
13
-
14
- /**
15
- * 运行调度守护。
16
- * @param {{intervalMs?:number, log?:(s:string)=>void, once?:boolean, spawnImpl?:Function}} [opts]
17
- * once=true 只跑一次(供测试/单次触发);spawnImpl 可注入(测试用)。
18
- */
19
- export async function runDaemon({ intervalMs = 60000, log = (s) => process.stdout.write(s + '\n'), once = false, spawnImpl } = {}) {
20
- const bin = huicaiBin();
21
- if (!bin && !spawnImpl) throw new Error('无法定位 huicai 可执行文件(daemon 需在 CLI 环境运行)');
22
- fs.mkdirSync(DAEMON_LOG_DIR, { recursive: true });
23
- const doSpawn = spawnImpl || ((task) => {
24
- const out = fs.openSync(path.join(DAEMON_LOG_DIR, task.id + '.log'), 'a');
25
- const child = spawn(process.execPath, [bin, '-p', task.prompt, '--yolo', '--plain'],
26
- { cwd: task.cwd, detached: true, stdio: ['ignore', out, out], env: process.env });
27
- child.unref();
28
- return child.pid;
29
- });
30
- const lastRun = new Map(); // id -> 'YYYY-MM-DDTHH:MM',防同一分钟重复触发
31
-
32
- const tick = () => {
33
- const now = new Date();
34
- const stamp = now.toISOString().slice(0, 16);
35
- let fired = 0;
36
- for (const t of listTasks()) {
37
- if (!t.cron || !t.prompt) continue;
38
- if (lastRun.get(t.id) === stamp) continue;
39
- let due = false;
40
- try { due = cronMatches(t.cron, now); } catch { due = false; }
41
- if (!due) continue;
42
- lastRun.set(t.id, stamp);
43
- const pid = doSpawn(t);
44
- fired++;
45
- log(`[${stamp}] 触发 #${t.id}(pid ${pid}):${String(t.prompt).replace(/\s+/g, ' ').slice(0, 60)}`);
46
- }
47
- return fired;
48
- };
49
-
50
- const n = tick();
51
- if (once) return { fired: n };
52
- log(`huicai 调度守护已启动:每 ${Math.round(intervalMs / 1000)}s 检查 ~/.huicai/schedule.json(${listTasks().length} 个任务)· Ctrl-C 退出`);
53
- const timer = setInterval(tick, intervalMs);
54
- await new Promise((resolve) => {
55
- process.on('SIGINT', () => {
56
- clearInterval(timer);
57
- log('\n调度守护已停止。');
58
- resolve();
59
- });
60
- });
61
- return { fired: n };
62
- }
1
+ import{spawn as o}from"node:child_process";import e from"node:fs";import r from"node:path";import n from"node:os";import{listTasks as t}from"./schedule.js";import{cronMatches as i}from"./cron.js";import{huicaiBin as c}from"./forks.js";export const DAEMON_LOG_DIR=r.join(n.homedir(),".huicai","daemon");export async function runDaemon({intervalMs:n=6e4,log:s=o=>process.stdout.write(o+"\n"),once:p=!1,spawnImpl:d}={}){const a=c();if(!a&&!d)throw new Error("无法定位 huicai 可执行文件(daemon 需在 CLI 环境运行)");e.mkdirSync(DAEMON_LOG_DIR,{recursive:!0});const m=d||(n=>{const t=e.openSync(r.join(DAEMON_LOG_DIR,n.id+".log"),"a"),i=o(process.execPath,[a,"-p",n.prompt,"--yolo","--plain"],{cwd:n.cwd,detached:!0,stdio:["ignore",t,t],env:process.env});return i.unref(),i.pid}),f=new Map,l=()=>{const o=new Date,e=o.toISOString().slice(0,16);let r=0;for(const n of t()){if(!n.cron||!n.prompt)continue;if(f.get(n.id)===e)continue;let t=!1;try{t=i(n.cron,o)}catch{t=!1}if(!t)continue;f.set(n.id,e);const c=m(n);r++,s(`[${e}] 触发 #${n.id}(pid ${c}):${String(n.prompt).replace(/\s+/g," ").slice(0,60)}`)}return r},u=l();if(p)return{fired:u};s(`huicai 调度守护已启动:每 ${Math.round(n/1e3)}s 检查 ~/.huicai/schedule.json(${t().length} 个任务)· Ctrl-C 退出`);const h=setInterval(l,n);return await new Promise(o=>{process.on("SIGINT",()=>{clearInterval(h),s("\n调度守护已停止。"),o()})}),{fired:u}}
package/src/device.js CHANGED
@@ -1,88 +1 @@
1
- // 设备授权码流(浏览器授权登录):发起 → 引导用户在浏览器确认 → 轮询换取授权登录令牌。
2
- // 服务端在用户确认时签发带时限的登录令牌(JWT,默认 7 天)——不再为每次登录铸造 API 令牌,
3
- // 不占用会员 API 令牌数量额度;令牌过期或改密后需重新授权。响应字段名保留 apiKey 以兼容旧版。
4
- // 全程只用平台网关的 /v1/device/*(免签名/免登录),CLI 无需预置任何凭据。
5
-
6
- import { spawn } from 'node:child_process';
7
- import { c } from './colors.js';
8
-
9
- /** 尽力打开系统浏览器(失败静默,用户可手动打开)。HUICAI_NO_BROWSER 置位时跳过(无头/测试环境)。 */
10
- function openBrowser(url) {
11
- if (!url || process.env.HUICAI_NO_BROWSER) return;
12
- const platform = process.platform;
13
- const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
14
- const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
15
- try {
16
- const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
17
- child.on('error', () => {});
18
- child.unref();
19
- } catch {
20
- /* 用户可手动打开 */
21
- }
22
- }
23
-
24
- async function postJson(baseUrl, path, body) {
25
- const res = await fetch(`${baseUrl}${path}`, {
26
- method: 'POST',
27
- headers: { 'Content-Type': 'application/json' },
28
- body: JSON.stringify(body || {}),
29
- });
30
- const text = await res.text();
31
- let json;
32
- try {
33
- json = JSON.parse(text);
34
- } catch {
35
- throw new Error(`服务返回异常(${res.status}):${text.slice(0, 200)}`);
36
- }
37
- if (!res.ok) throw new Error(json?.message || `HTTP ${res.status}`);
38
- // 平台统一包装 {code,message,data}
39
- if (json && typeof json === 'object' && 'data' in json && 'code' in json) {
40
- if (json.code !== 0 && json.code !== 200) throw new Error(json.message || '请求失败');
41
- return json.data;
42
- }
43
- return json;
44
- }
45
-
46
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
47
-
48
- /**
49
- * 执行设备授权流,成功返回授权登录令牌(JWT,服务端响应字段仍名 apiKey);失败/超时抛错。
50
- * @param {string} baseUrl 平台网关地址
51
- */
52
- export async function deviceLogin(baseUrl) {
53
- const start = await postJson(baseUrl, '/v1/device/code', { client_name: `huicai-cli (${process.platform})` });
54
- const { deviceCode, userCode, verificationUri, verificationUriComplete } = start;
55
- const interval = Math.max(2, start.interval || 5);
56
- const expiresIn = start.expiresIn || 600;
57
-
58
- // 授权页地址:优先服务端返回;否则按同域部署由平台地址推导 {baseUrl}/device
59
- const vUri = verificationUri || baseUrl + '/device';
60
- const vComplete = verificationUriComplete || vUri + (vUri.includes('?') ? '&' : '?') + 'code=' + encodeURIComponent(userCode);
61
-
62
- process.stdout.write('\n' + c.bold('请在浏览器中完成授权:') + '\n');
63
- process.stdout.write(' 1) 打开 ' + c.cyan(vUri) + '\n');
64
- process.stdout.write(' 2) 确认授权码 ' + c.bold(c.yellow(userCode)) + '\n');
65
- process.stdout.write(c.gray(' (已尝试自动打开浏览器;未弹出请手动访问上面的地址)') + '\n');
66
- openBrowser(vComplete);
67
- process.stdout.write(c.gray(`\n等待授权中…(有效期约 ${Math.round(expiresIn / 60)} 分钟,Ctrl+C 取消)`) + '\n');
68
-
69
- const deadline = Date.now() + expiresIn * 1000;
70
- while (Date.now() < deadline) {
71
- await sleep(interval * 1000);
72
- let r;
73
- try {
74
- r = await postJson(baseUrl, '/v1/device/token', { device_code: deviceCode });
75
- } catch (e) {
76
- process.stdout.write(c.gray(` 轮询出错(将重试):${e.message}`) + '\n');
77
- continue;
78
- }
79
- if (r.status === 'approved' && r.apiKey) {
80
- process.stdout.write(c.green('\n✓ 授权成功') + '\n');
81
- return r.apiKey;
82
- }
83
- if (r.status === 'denied') throw new Error('用户在浏览器中拒绝了授权');
84
- if (r.status === 'expired') throw new Error('授权码已过期,请重新登录');
85
- process.stdout.write(c.gray(' · 仍在等待浏览器确认…') + '\n');
86
- }
87
- throw new Error('授权超时,请重新运行 huicai login');
88
- }
1
+ import{spawn as e}from"node:child_process";import{c as t}from"./colors.js";async function o(e,t,o){const r=await fetch(`${e}${t}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o||{})}),n=await r.text();let i;try{i=JSON.parse(n)}catch{throw new Error(`服务返回异常(${r.status}):${n.slice(0,200)}`)}if(!r.ok)throw new Error(i?.message||`HTTP ${r.status}`);if(i&&"object"==typeof i&&"data"in i&&"code"in i){if(0!==i.code&&200!==i.code)throw new Error(i.message||"请求失败");return i.data}return i}const r=e=>new Promise(t=>setTimeout(t,e));export async function deviceLogin(n){const i=await o(n,"/v1/device/code",{client_name:`huicai-cli (${process.platform})`}),{deviceCode:s,userCode:c,verificationUri:a,verificationUriComplete:d}=i,p=Math.max(2,i.interval||5),w=i.expiresIn||600,u=a||n+"/device",f=d||u+(u.includes("?")?"&":"?")+"code="+encodeURIComponent(c);process.stdout.write("\n"+t.bold("请在浏览器中完成授权:")+"\n"),process.stdout.write(" 1) 打开 "+t.cyan(u)+"\n"),process.stdout.write(" 2) 确认授权码 "+t.bold(t.yellow(c))+"\n"),process.stdout.write(t.gray(" (已尝试自动打开浏览器;未弹出请手动访问上面的地址)")+"\n"),function(t){if(!t||process.env.HUICAI_NO_BROWSER)return;const o=process.platform,r="darwin"===o?"open":"win32"===o?"cmd":"xdg-open",n="win32"===o?["/c","start","",t]:[t];try{const t=e(r,n,{stdio:"ignore",detached:!0});t.on("error",()=>{}),t.unref()}catch{}}(f),process.stdout.write(t.gray(`\n等待授权中…(有效期约 ${Math.round(w/60)} 分钟,Ctrl+C 取消)`)+"\n");const l=Date.now()+1e3*w;for(;Date.now()<l;){let e;await r(1e3*p);try{e=await o(n,"/v1/device/token",{device_code:s})}catch(e){process.stdout.write(t.gray(` 轮询出错(将重试):${e.message}`)+"\n");continue}if("approved"===e.status&&e.apiKey)return process.stdout.write(t.green("\n✓ 授权成功")+"\n"),e.apiKey;if("denied"===e.status)throw new Error("用户在浏览器中拒绝了授权");if("expired"===e.status)throw new Error("授权码已过期,请重新登录");process.stdout.write(t.gray(" · 仍在等待浏览器确认…")+"\n")}throw new Error("授权超时,请重新运行 huicai login")}
package/src/diffview.js CHANGED
@@ -1,115 +1 @@
1
- // /diff:显示未提交改动(git status + git diff,含未跟踪文件),并汇总本会话智能体改过的文件。
2
- // 非 git 仓库时退化为「仅列出本会话改过的文件」(无基线无法给 diff,建议配合 /rewind)。
3
-
4
- import { execSync } from 'node:child_process';
5
- import { c } from './colors.js';
6
-
7
- const MAX_DIFF_LINES = 500;
8
-
9
- function git(cwd, args) {
10
- return execSync(`git ${args}`, { cwd, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 8 * 1024 * 1024 })
11
- .toString();
12
- }
13
-
14
- function isRepo(cwd) {
15
- try {
16
- return git(cwd, 'rev-parse --is-inside-work-tree').trim() === 'true';
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- /** 本会话中智能体写过/编辑过的文件(从 write_file/edit_file 的工具调用参数提取,去重保序)。 */
23
- export function sessionEditedFiles(messages) {
24
- const seen = new Set();
25
- const out = [];
26
- for (const m of messages || []) {
27
- if (m.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue;
28
- for (const tc of m.tool_calls) {
29
- const name = tc.function?.name;
30
- if (name !== 'write_file' && name !== 'edit_file') continue;
31
- let p;
32
- try {
33
- p = JSON.parse(tc.function?.arguments || '{}').path;
34
- } catch {
35
- continue;
36
- }
37
- if (p && !seen.has(p)) {
38
- seen.add(p);
39
- out.push(p);
40
- }
41
- }
42
- }
43
- return out;
44
- }
45
-
46
- /** 给 unified diff 上色。 */
47
- export function colorizeDiff(text) {
48
- return String(text)
49
- .split('\n')
50
- .map((l) => {
51
- if (l.startsWith('diff --git') || l.startsWith('index ') || l.startsWith('--- ') || l.startsWith('+++ ')) return c.bold(l);
52
- if (l.startsWith('@@')) return c.cyan(l);
53
- if (l.startsWith('+')) return c.green(l);
54
- if (l.startsWith('-')) return c.red(l);
55
- return l;
56
- })
57
- .join('\n');
58
- }
59
-
60
- function clip(text) {
61
- const lines = String(text).split('\n');
62
- if (lines.length <= MAX_DIFF_LINES) return text;
63
- return lines.slice(0, MAX_DIFF_LINES).join('\n') + `\n…(diff 过长,仅显示前 ${MAX_DIFF_LINES} 行,用 git diff 查看完整)`;
64
- }
65
-
66
- /** 组装 /diff 输出文本。 */
67
- export function renderDiff(cwd, messages) {
68
- const edited = sessionEditedFiles(messages);
69
- const lines = [];
70
- if (edited.length) {
71
- lines.push(c.bold(`本会话智能体改过的文件(${edited.length})`));
72
- for (const f of edited) lines.push(' · ' + f);
73
- lines.push('');
74
- }
75
-
76
- if (!isRepo(cwd)) {
77
- if (!edited.length) return '当前目录无 git 仓库,且本会话尚无文件改动。';
78
- lines.push(c.gray('(非 git 仓库,无法给出 diff;可用 /rewind 回滚本会话改动。)'));
79
- return lines.join('\n');
80
- }
81
-
82
- let status = '';
83
- try {
84
- status = git(cwd, 'status --short').trim();
85
- } catch { /* ignore */ }
86
- if (!status) {
87
- lines.push('工作区干净,无未提交改动。');
88
- return lines.join('\n');
89
- }
90
- lines.push(c.bold('git 状态'));
91
- lines.push(status.split('\n').map((l) => ' ' + l).join('\n'));
92
-
93
- let diff = '';
94
- try {
95
- diff = git(cwd, 'diff --stat').trim();
96
- } catch { /* ignore */ }
97
- if (diff) {
98
- lines.push('');
99
- lines.push(c.bold('改动统计'));
100
- lines.push(diff.split('\n').map((l) => ' ' + l).join('\n'));
101
- }
102
-
103
- let full = '';
104
- try {
105
- full = git(cwd, 'diff').trim(); // 已跟踪文件的未暂存改动
106
- } catch { /* ignore */ }
107
- if (full) {
108
- lines.push('');
109
- lines.push(c.bold('改动明细(未暂存)'));
110
- lines.push(colorizeDiff(clip(full)));
111
- }
112
- lines.push('');
113
- lines.push(c.gray('(未跟踪文件见上方 ?? 标记;已暂存改动用 git diff --staged 查看。)'));
114
- return lines.join('\n');
115
- }
1
+ import{execSync as t}from"node:child_process";import{c as i}from"./colors.js";function n(i,n){return t(`git ${n}`,{cwd:i,stdio:["ignore","pipe","ignore"],maxBuffer:8388608}).toString()}export function sessionEditedFiles(t){const i=new Set,n=[];for(const r of t||[])if("assistant"===r.role&&Array.isArray(r.tool_calls))for(const t of r.tool_calls){const r=t.function?.name;if("write_file"!==r&&"edit_file"!==r)continue;let s;try{s=JSON.parse(t.function?.arguments||"{}").path}catch{continue}s&&!i.has(s)&&(i.add(s),n.push(s))}return n}export function colorizeDiff(t){return String(t).split("\n").map(t=>t.startsWith("diff --git")||t.startsWith("index ")||t.startsWith("--- ")||t.startsWith("+++ ")?i.bold(t):t.startsWith("@@")?i.cyan(t):t.startsWith("+")?i.green(t):t.startsWith("-")?i.red(t):t).join("\n")}export function renderDiff(t,r){const s=sessionEditedFiles(r),o=[];if(s.length){o.push(i.bold(`本会话智能体改过的文件(${s.length})`));for(const t of s)o.push(" · "+t);o.push("")}if(!function(t){try{return"true"===n(t,"rev-parse --is-inside-work-tree").trim()}catch{return!1}}(t))return s.length?(o.push(i.gray("(非 git 仓库,无法给出 diff;可用 /rewind 回滚本会话改动。)")),o.join("\n")):"当前目录无 git 仓库,且本会话尚无文件改动。";let e="";try{e=n(t,"status --short").trim()}catch{}if(!e)return o.push("工作区干净,无未提交改动。"),o.join("\n");o.push(i.bold("git 状态")),o.push(e.split("\n").map(t=>" "+t).join("\n"));let f="";try{f=n(t,"diff --stat").trim()}catch{}f&&(o.push(""),o.push(i.bold("改动统计")),o.push(f.split("\n").map(t=>" "+t).join("\n")));let u="";try{u=n(t,"diff").trim()}catch{}return u&&(o.push(""),o.push(i.bold("改动明细(未暂存)")),o.push(colorizeDiff(function(t){const i=String(t).split("\n");return i.length<=500?t:i.slice(0,500).join("\n")+"\n…(diff 过长,仅显示前 500 行,用 git diff 查看完整)"}(u)))),o.push(""),o.push(i.gray("(未跟踪文件见上方 ?? 标记;已暂存改动用 git diff --staged 查看。)")),o.join("\n")}
package/src/doctor.js CHANGED
@@ -1,72 +1 @@
1
- // /doctor:本机环境体检(对齐 VSCode diagnostics)——node/登录/平台连通/模型/git/工作目录/钩子。
2
- // 网络类检查 best-effort,失败给出可读提示,绝不抛出中断命令。
3
-
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { execSync } from 'node:child_process';
7
- import { c } from './colors.js';
8
- import { CONFIG_PATH } from './config.js';
9
- import { listModels } from './api.js';
10
- import { loadHooks } from './hooks.js';
11
-
12
- const OK = () => c.green('✓');
13
- const NO = () => c.red('✗');
14
- const DOT = () => c.yellow('•');
15
-
16
- function isGitRepo(cwd) {
17
- try {
18
- return execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() === 'true';
19
- } catch {
20
- return false;
21
- }
22
- }
23
-
24
- function cwdWritable(cwd) {
25
- try {
26
- fs.accessSync(cwd, fs.constants.W_OK);
27
- return true;
28
- } catch {
29
- return false;
30
- }
31
- }
32
-
33
- /** 生成体检报告文本(async:含一次平台连通探测)。 */
34
- export async function runDoctor(ctx) {
35
- const { config, cwd } = ctx;
36
- const lines = [c.bold('huicai 体检')];
37
-
38
- const nodeMajor = Number(String(process.versions.node).split('.')[0]);
39
- lines.push(` ${nodeMajor >= 18 ? OK() : NO()} Node ${process.version}${nodeMajor >= 18 ? '' : c.yellow('(需 ≥18)')}`);
40
- lines.push(` ${DOT()} 配置文件 ${CONFIG_PATH}`);
41
- lines.push(` ${DOT()} 工作目录 ${cwd}${cwdWritable(cwd) ? '' : c.yellow('(不可写)')}`);
42
-
43
- const loggedIn = !!(config.apiKey && config.baseUrl);
44
- lines.push(` ${loggedIn ? OK() : NO()} 登录 ${loggedIn ? '已登录 · ' + config.baseUrl : '未登录(/login 或 huicai login)'}`);
45
-
46
- if (loggedIn) {
47
- try {
48
- const models = await listModels(config.baseUrl, config.apiKey);
49
- lines.push(` ${OK()} 平台连通 · ${models.length} 个模型/智能体可用`);
50
- const m = config.model;
51
- const known = m === 'auto' || models.some((x) => x.id === m);
52
- lines.push(` ${known ? OK() : NO()} 当前模型 ${m}${known ? '' : c.yellow('(不在账号可用列表,建议 /model auto)')}`);
53
- } catch (e) {
54
- lines.push(` ${NO()} 平台连通失败:${e.message}`);
55
- }
56
- }
57
-
58
- const git = isGitRepo(cwd);
59
- lines.push(` ${git ? OK() : DOT()} Git ${git ? '仓库' : c.yellow('当前目录非 git 仓库(/diff 与 /rewind code 在 git 下体验更佳)')}`);
60
-
61
- try {
62
- const hooks = loadHooks(cwd);
63
- if (hooks.all.length) lines.push(` ${DOT()} 钩子 已配置 ${hooks.all.length} 个(/hooks 查看)`);
64
- if (hooks.projectIgnored) lines.push(` ${DOT()} 钩子 ${hooks.projectIgnored} 个项目钩子未信任(HUICAI_PROJECT_HOOKS=1 启用)`);
65
- } catch { /* 忽略 */ }
66
-
67
- const mcpPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.huicai', 'mcp.json');
68
- lines.push(` ${DOT()} MCP ${fs.existsSync(mcpPath) ? '已配置 ~/.huicai/mcp.json' : '未配置(可选)'}`);
69
-
70
- lines.push(c.gray(' (体检只读,不改动任何东西)'));
71
- return lines.join('\n');
72
- }
1
+ import o from"node:fs";import r from"node:path";import{execSync as s}from"node:child_process";import{c as e}from"./colors.js";import{CONFIG_PATH as t}from"./config.js";import{listModels as n}from"./api.js";import{loadHooks as i}from"./hooks.js";const c=()=>e.green("✓"),p=()=>e.red("✗"),u=()=>e.yellow("•");export async function runDoctor(l){const{config:a,cwd:h}=l,$=[e.bold("huicai 体检")],m=Number(String(process.versions.node).split(".")[0]);$.push(` ${m>=18?c():p()} Node ${process.version}${m>=18?"":e.yellow("(需 ≥18)")}`),$.push(` ${u()} 配置文件 ${t}`),$.push(` ${u()} 工作目录 ${h}${function(r){try{return o.accessSync(r,o.constants.W_OK),!0}catch{return!1}}(h)?"":e.yellow("(不可写)")}`);const d=!(!a.apiKey||!a.baseUrl);if($.push(` ${d?c():p()} 登录 ${d?"已登录 · "+a.baseUrl:"未登录(/login 或 huicai login)"}`),d)try{const o=await n(a.baseUrl,a.apiKey);$.push(` ${c()} 平台连通 · ${o.length} 个模型/智能体可用`);const r=a.model,s="auto"===r||o.some(o=>o.id===r);$.push(` ${s?c():p()} 当前模型 ${r}${s?"":e.yellow("(不在账号可用列表,建议 /model auto)")}`)}catch(o){$.push(` ${p()} 平台连通失败:${o.message}`)}const g=function(o){try{return"true"===s("git rev-parse --is-inside-work-tree",{cwd:o,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{return!1}}(h);$.push(` ${g?c():u()} Git ${g?"仓库":e.yellow("当前目录非 git 仓库(/diff 与 /rewind code 在 git 下体验更佳)")}`);try{const o=i(h);o.all.length&&$.push(` ${u()} 钩子 已配置 ${o.all.length} 个(/hooks 查看)`),o.projectIgnored&&$.push(` ${u()} 钩子 ${o.projectIgnored} 个项目钩子未信任(HUICAI_PROJECT_HOOKS=1 启用)`)}catch{}const f=r.join(process.env.HOME||process.env.USERPROFILE||"",".huicai","mcp.json");return $.push(` ${u()} MCP ${o.existsSync(f)?"已配置 ~/.huicai/mcp.json":"未配置(可选)"}`),$.push(e.gray(" (体检只读,不改动任何东西)")),$.join("\n")}