@huicai/cli 2026.710.97950 → 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 (50) hide show
  1. package/LICENSE +43 -21
  2. package/bin/huicai.js +93 -2
  3. package/package.json +4 -3
  4. package/src/advisor.js +1 -0
  5. package/src/agent.js +1 -385
  6. package/src/api.js +1 -326
  7. package/src/autofix.js +1 -0
  8. package/src/banner.js +1 -83
  9. package/src/checkpoints.js +1 -0
  10. package/src/clipboard.js +1 -0
  11. package/src/codeindex.js +1 -0
  12. package/src/colors.js +1 -22
  13. package/src/commands.js +1 -339
  14. package/src/compaction.js +1 -140
  15. package/src/config.js +1 -66
  16. package/src/consult.js +1 -65
  17. package/src/context-report.js +1 -0
  18. package/src/context.js +1 -144
  19. package/src/cron.js +1 -0
  20. package/src/daemon.js +1 -0
  21. package/src/device.js +1 -88
  22. package/src/diffview.js +1 -0
  23. package/src/doctor.js +1 -0
  24. package/src/executors.js +1 -391
  25. package/src/forks.js +1 -0
  26. package/src/gitflow.js +1 -0
  27. package/src/hooks.js +1 -0
  28. package/src/ide.js +1 -53
  29. package/src/input.js +1 -182
  30. package/src/insights.js +1 -0
  31. package/src/kbsync.js +1 -0
  32. package/src/markdown.js +0 -0
  33. package/src/markup-filter.js +1 -85
  34. package/src/mcp.js +1 -207
  35. package/src/memory.js +1 -110
  36. package/src/permissions.js +1 -82
  37. package/src/render.js +1 -134
  38. package/src/repl.js +1 -205
  39. package/src/scan.js +1 -0
  40. package/src/schedule.js +1 -0
  41. package/src/session.js +1 -140
  42. package/src/settings.js +0 -0
  43. package/src/skills.js +1 -248
  44. package/src/subagent.js +1 -82
  45. package/src/todo.js +1 -75
  46. package/src/tools.js +1 -132
  47. package/src/transcript.js +1 -0
  48. package/src/tui/app.js +1 -566
  49. package/src/usage.js +1 -43
  50. package/src/watch.js +1 -0
@@ -1,82 +1 @@
1
- // 权限/审批:只读工具自动放行;写/执行需授权(y 一次 / a 本会话该工具 / A 永久写入项目设置 / 否可附原因)。
2
- // 另支持 settings.json 允许规则(全局 ~/.huicai + 项目 .huicai,见 settings.js)。
3
- // yolo 模式全放行;非交互(无 ask)且非 yolo 时拒绝写/执行并把原因回灌模型,让它调整策略。
4
-
5
- import { c } from './colors.js';
6
- import { toolLabel, renderEditDiff, indent } from './render.js';
7
- import { loadAllowRules, matchAllowRules, suggestRule, addProjectAllowRule, riskWarning } from './settings.js';
8
-
9
- export function createPermissions({ yolo = false, ask = null, cwd = null } = {}) {
10
- const sessionAllowed = new Set();
11
- let autoAll = !!yolo;
12
- // 仅在给定 cwd 时读取 settings 允许规则(测试等场景不受用户本机全局配置影响)
13
- let rules = cwd ? loadAllowRules(cwd) : [];
14
-
15
- // readOnly 由调用方判定(内置工具查 READONLY_TOOLS;MCP 工具看 readOnlyHint;子智能体只读)
16
- async function request(name, args, _cwd, readOnly = false) {
17
- if (readOnly) return { allow: true };
18
- if (autoAll || sessionAllowed.has(name)) return { allow: true };
19
- if (rules.length && matchAllowRules(rules, name, args)) return { allow: true };
20
- if (!ask) {
21
- return { allow: false, reason: '当前为非交互模式,未授权写/执行操作(加 --yolo 可自动放行)。' };
22
- }
23
- printPreview(name, args);
24
- const raw = (await ask(
25
- `${c.yellow('允许执行?')} [y]是 · [a]本会话该工具 · [A]永久允许(写入项目设置) · [N]否(可附原因): `,
26
- )) || '';
27
- const answer = raw.trim();
28
- if (answer === 'a') {
29
- sessionAllowed.add(name);
30
- return { allow: true };
31
- }
32
- if (answer === 'A') {
33
- sessionAllowed.add(name);
34
- if (cwd) {
35
- const rule = suggestRule(name, args);
36
- try {
37
- const p = addProjectAllowRule(cwd, rule);
38
- rules = loadAllowRules(cwd);
39
- process.stdout.write(c.gray(` 已写入规则 ${rule} → ${p}`) + '\n');
40
- } catch (e) {
41
- process.stdout.write(c.yellow(` 规则写入失败(本会话仍生效):${e.message}`) + '\n');
42
- }
43
- }
44
- return { allow: true };
45
- }
46
- const lower = answer.toLowerCase();
47
- if (lower === 'y' || lower === 'yes') return { allow: true };
48
- if (answer === '' || lower === 'n' || lower === 'no') {
49
- return { allow: false, reason: '用户拒绝了该操作。' };
50
- }
51
- return { allow: false, reason: `用户拒绝并说明:${answer}` };
52
- }
53
-
54
- return {
55
- request,
56
- setYolo(v) {
57
- autoAll = !!v;
58
- },
59
- isYolo() {
60
- return autoAll;
61
- },
62
- };
63
- }
64
-
65
- function printPreview(name, args) {
66
- process.stdout.write('\n' + c.bold(c.yellow('● 需要授权:')) + toolLabel(name, args) + '\n');
67
- const risk = riskWarning(name, args);
68
- if (risk) process.stdout.write(' ' + c.bold(c.red('⚠ ' + risk)) + '\n');
69
- if (name === 'run_bash') {
70
- process.stdout.write(c.gray(' $ ') + String(args.command ?? '') + '\n');
71
- } else if (name === 'write_file') {
72
- const preview = String(args.content ?? '')
73
- .split('\n')
74
- .slice(0, 12)
75
- .join('\n');
76
- process.stdout.write(c.gray(` 将写入 ${args.path}:`) + '\n' + indent(preview) + '\n');
77
- } else if (name === 'edit_file') {
78
- process.stdout.write(c.gray(` 编辑 ${args.path}:`) + '\n' + indent(renderEditDiff(args.old_string, args.new_string)) + '\n');
79
- } else if (name === 'fetch_url') {
80
- process.stdout.write(c.gray(' GET ') + String(args.url ?? '') + '\n');
81
- }
82
- }
1
+ import{c as o}from"./colors.js";import{toolLabel as t,renderEditDiff as r,indent as e}from"./render.js";import{loadAllowRules as s,loadDenyRules as n,matchAllowRules as l,suggestRule as a,addProjectAllowRule as i,riskWarning as u}from"./settings.js";export const PLAN_DENY_REASON="当前处于计划模式:只做只读调研,不执行任何改动。请先产出完整方案交用户确认;用户 /plan 关闭计划模式后再执行改动。";export const SANDBOX_DENY_REASON="当前处于沙箱模式:禁止执行 shell 命令(run_bash)与联网(fetch_url)。可继续读写本地文件;如需执行/联网,请用户 /sandbox off 关闭沙箱。";export function createPermissions({yolo:c=!1,ask:w=null,cwd:d=null}={}){const f=new Set;let p=!!c,_=!1,y=!1,g=d?s(d):[],h=d?n(d):[];return{request:async function(n,c,N,m=!1){if(h.length&&l(h,n,c))return{allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"};if(_&&!m)return{allow:!1,reason:PLAN_DENY_REASON};if(y&&("run_bash"===n||"fetch_url"===n))return{allow:!1,reason:SANDBOX_DENY_REASON};if(m)return{allow:!0};if(p||f.has(n))return{allow:!0};if(g.length&&l(g,n,c))return{allow:!0};if(!w)return{allow:!1,reason:"当前为非交互模式,未授权写/执行操作(加 --yolo 可自动放行)。"};!function(s,n){process.stdout.write("\n"+o.bold(o.yellow("● 需要授权:"))+t(s,n)+"\n");const l=u(s,n);l&&process.stdout.write(" "+o.bold(o.red("⚠ "+l))+"\n");if("run_bash"===s)process.stdout.write(o.gray(" $ ")+String(n.command??"")+"\n");else if("write_file"===s){const t=String(n.content??"").split("\n").slice(0,12).join("\n");process.stdout.write(o.gray(` 将写入 ${n.path}:`)+"\n"+e(t)+"\n")}else"edit_file"===s?process.stdout.write(o.gray(` 编辑 ${n.path}:`)+"\n"+e(r(n.old_string,n.new_string))+"\n"):"fetch_url"===s&&process.stdout.write(o.gray(" GET ")+String(n.url??"")+"\n")}(n,c);const S=(await w(`${o.yellow("允许执行?")} [y]是 · [a]本会话该工具 · [A]永久允许(写入项目设置) · [N]否(可附原因): `)||"").trim();if("a"===S)return f.add(n),{allow:!0};if("A"===S){if(f.add(n),d){const t=a(n,c);try{const r=i(d,t);g=s(d),process.stdout.write(o.gray(` 已写入规则 ${t} → ${r}`)+"\n")}catch(t){process.stdout.write(o.yellow(` 规则写入失败(本会话仍生效):${t.message}`)+"\n")}}return{allow:!0}}const A=S.toLowerCase();return"y"===A||"yes"===A?{allow:!0}:""===S||"n"===A||"no"===A?{allow:!1,reason:"用户拒绝了该操作。"}:{allow:!1,reason:`用户拒绝并说明:${S}`}},setYolo(o){p=!!o},isYolo:()=>p,setPlanMode(o){_=!!o},isPlanMode:()=>_,setSandbox(o){y=!!o},isSandbox:()=>y,reloadRules(){g=d?s(d):[],h=d?n(d):[]}}}
package/src/render.js CHANGED
@@ -1,134 +1 @@
1
- // 终端渲染小工具:工具调用标签、编辑 diff。
2
-
3
- import { c } from './colors.js';
4
-
5
- /** 把一次工具调用渲染成简洁的一行标签。 */
6
- export function toolLabel(name, args = {}) {
7
- switch (name) {
8
- case 'read_file':
9
- return `read_file ${args.path ?? ''}`;
10
- case 'list_files':
11
- return `list_files ${args.path ?? '.'}`;
12
- case 'glob':
13
- return `glob ${args.pattern ?? ''}`;
14
- case 'grep':
15
- return `grep ${JSON.stringify(args.pattern ?? '')}${args.path ? ' in ' + args.path : ''}`;
16
- case 'write_file':
17
- return `write_file ${args.path ?? ''}`;
18
- case 'edit_file':
19
- return `edit_file ${args.path ?? ''}`;
20
- case 'run_bash':
21
- return `run_bash: ${String(args.command ?? '').split('\n')[0]}`;
22
- case 'fetch_url':
23
- return `fetch_url ${args.url ?? ''}`;
24
- case 'todo_write':
25
- return 'todo_write(更新任务清单)';
26
- case 'dispatch_agent': {
27
- const o = String(args.objective ?? '').split('\n')[0];
28
- return `dispatch_agent: ${o.length > 60 ? o.slice(0, 60) + '…' : o}`;
29
- }
30
- default:
31
- return `${name}(${Object.keys(args).join(', ')})`;
32
- }
33
- }
34
-
35
- /** edit_file 的简易 diff:old 红、new 绿。 */
36
- export function renderEditDiff(oldStr, newStr) {
37
- const out = [];
38
- for (const l of String(oldStr ?? '').split('\n')) out.push(c.red('- ' + l));
39
- for (const l of String(newStr ?? '').split('\n')) out.push(c.green('+ ' + l));
40
- return out.join('\n');
41
- }
42
-
43
- /**
44
- * 逐行 LCS diff:返回 [{type:' '|'-'|'+', text}]。超大文本退化为「全删+全增」以防 O(n·m) 爆内存。
45
- */
46
- export function diffLines(oldText, newText) {
47
- // 空文本视为「零行」(新建文件时旧内容为空,不应显示一条幽灵空行删除)
48
- const split = (t) => (t == null || t === '' ? [] : String(t).split('\n'));
49
- const a = split(oldText);
50
- const b = split(newText);
51
- const n = a.length, m = b.length;
52
- if (n * m > 4_000_000) {
53
- return [...a.map((t) => ({ type: '-', text: t })), ...b.map((t) => ({ type: '+', text: t }))];
54
- }
55
- // dp[i][j] = a[i..] 与 b[j..] 的最长公共子序列长度
56
- const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
57
- for (let i = n - 1; i >= 0; i--) {
58
- for (let j = m - 1; j >= 0; j--) {
59
- dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
60
- }
61
- }
62
- const ops = [];
63
- let i = 0, j = 0;
64
- while (i < n && j < m) {
65
- if (a[i] === b[j]) { ops.push({ type: ' ', text: a[i] }); i++; j++; }
66
- else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push({ type: '-', text: a[i] }); i++; }
67
- else { ops.push({ type: '+', text: b[j] }); j++; }
68
- }
69
- while (i < n) ops.push({ type: '-', text: a[i++] });
70
- while (j < m) ops.push({ type: '+', text: b[j++] });
71
- return ops;
72
- }
73
-
74
- /**
75
- * 把一次文件更改渲染为「带行号的统一 diff」(对标 Claude Code 编辑后展示更改的行):
76
- * 增行绿色 `+`、删行红色 `-`、上下文灰色;仅显示变更行及其上下 context 行,中间大段用 ⋯ 省略。
77
- * 无变化返回空串。返回的是带 ANSI 的字符串(可直接 write,也可放进 ink <Text>)。
78
- * @param {string} oldText
79
- * @param {string} newText
80
- * @param {{path?:string, context?:number, maxLines?:number}} [opts]
81
- */
82
- export function renderFileDiff(oldText, newText, opts = {}) {
83
- const { path = '', context = 3, maxLines = 80 } = opts;
84
- const ops = diffLines(oldText, newText);
85
- let adds = 0, dels = 0;
86
- for (const o of ops) { if (o.type === '+') adds++; else if (o.type === '-') dels++; }
87
- if (adds === 0 && dels === 0) return '';
88
-
89
- // 标注每行的旧/新行号
90
- const rows = [];
91
- let oldNo = 0, newNo = 0;
92
- for (const o of ops) {
93
- if (o.type === ' ') { oldNo++; newNo++; rows.push({ type: ' ', text: o.text, no: oldNo }); }
94
- else if (o.type === '-') { oldNo++; rows.push({ type: '-', text: o.text, no: oldNo }); }
95
- else { newNo++; rows.push({ type: '+', text: o.text, no: newNo }); }
96
- }
97
- // 需展示的行 = 变更行及其上下 context 行
98
- const show = new Array(rows.length).fill(false);
99
- rows.forEach((r, idx) => {
100
- if (r.type === ' ') return;
101
- for (let k = Math.max(0, idx - context); k <= Math.min(rows.length - 1, idx + context); k++) show[k] = true;
102
- });
103
-
104
- const width = String(Math.max(oldNo, newNo)).length;
105
- const pad = (no) => String(no == null ? '' : no).padStart(width);
106
- const out = [(path ? c.bold(path) + ' ' : '') + c.green('+' + adds) + ' ' + c.red('−' + dels)];
107
- let elided = false, shown = 0, truncated = false;
108
- for (let idx = 0; idx < rows.length; idx++) {
109
- if (!show[idx]) { if (!elided) { out.push(c.gray(' ' + ' '.repeat(width) + ' ⋯')); elided = true; } continue; }
110
- elided = false;
111
- if (shown >= maxLines) { truncated = true; break; }
112
- const r = rows[idx];
113
- if (r.type === '+') out.push(c.green(' ' + pad(r.no) + ' + ') + c.green(r.text));
114
- else if (r.type === '-') out.push(c.red(' ' + pad(r.no) + ' - ') + c.red(r.text));
115
- else out.push(c.gray(' ' + pad(r.no) + ' ' + r.text));
116
- shown++;
117
- }
118
- if (truncated) out.push(c.gray(` …(差异较大,仅显示前 ${maxLines} 行;共 +${adds} −${dels})`));
119
- return out.join('\n');
120
- }
121
-
122
- export function indent(s, pad = ' ') {
123
- return String(s)
124
- .split('\n')
125
- .map((l) => pad + l)
126
- .join('\n');
127
- }
128
-
129
- /** 结果摘要(首行 + 行数提示),避免把整段工具输出刷屏。 */
130
- export function summarize(text, maxLines = 6) {
131
- const lines = String(text).split('\n');
132
- if (lines.length <= maxLines) return text;
133
- return lines.slice(0, maxLines).join('\n') + c.gray(`\n …(共 ${lines.length} 行)`);
134
- }
1
+ import{c as t}from"./colors.js";export function toolLabel(t,e={}){switch(t){case"read_file":return`read_file ${e.path??""}`;case"list_files":return`list_files ${e.path??"."}`;case"glob":return`glob ${e.pattern??""}`;case"grep":return`grep ${JSON.stringify(e.pattern??"")}${e.path?" in "+e.path:""}`;case"search_code":return`search_code ${JSON.stringify(e.query??"")}`;case"write_file":return`write_file ${e.path??""}`;case"edit_file":return`edit_file ${e.path??""}`;case"run_bash":return`run_bash: ${String(e.command??"").split("\n")[0]}`;case"fetch_url":return`fetch_url ${e.url??""}`;case"todo_write":return"todo_write(更新任务清单)";case"dispatch_agent":{const t=String(e.objective??"").split("\n")[0];return`dispatch_agent: ${t.length>60?t.slice(0,60)+"…":t}`}case"dispatch_agents":return`dispatch_agents(并行 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个子任务)`;default:return`${t}(${Object.keys(e).join(", ")})`}}export function renderEditDiff(e,n){const r=[];for(const n of String(e??"").split("\n"))r.push(t.red("- "+n));for(const e of String(n??"").split("\n"))r.push(t.green("+ "+e));return r.join("\n")}export function diffLines(t,e){const n=t=>null==t||""===t?[]:String(t).split("\n"),r=n(t),s=n(e),i=r.length,o=s.length;if(i*o>4e6)return[...r.map(t=>({type:"-",text:t})),...s.map(t=>({type:"+",text:t}))];const a=Array.from({length:i+1},()=>new Uint32Array(o+1));for(let t=i-1;t>=0;t--)for(let e=o-1;e>=0;e--)a[t][e]=r[t]===s[e]?a[t+1][e+1]+1:Math.max(a[t+1][e],a[t][e+1]);const p=[];let l=0,c=0;for(;l<i&&c<o;)r[l]===s[c]?(p.push({type:" ",text:r[l]}),l++,c++):a[l+1][c]>=a[l][c+1]?(p.push({type:"-",text:r[l]}),l++):(p.push({type:"+",text:s[c]}),c++);for(;l<i;)p.push({type:"-",text:r[l++]});for(;c<o;)p.push({type:"+",text:s[c++]});return p}export function renderFileDiff(e,n,r={}){const{path:s="",context:i=3,maxLines:o=80}=r,a=diffLines(e,n);let p=0,l=0;for(const t of a)"+"===t.type?p++:"-"===t.type&&l++;if(0===p&&0===l)return"";const c=[];let u=0,f=0;for(const t of a)" "===t.type?(u++,f++,c.push({type:" ",text:t.text,no:u})):"-"===t.type?(u++,c.push({type:"-",text:t.text,no:u})):(f++,c.push({type:"+",text:t.text,no:f}));const h=new Array(c.length).fill(!1);c.forEach((t,e)=>{if(" "!==t.type)for(let t=Math.max(0,e-i);t<=Math.min(c.length-1,e+i);t++)h[t]=!0});const g=String(Math.max(u,f)).length,y=t=>String(null==t?"":t).padStart(g),x=[(s?t.bold(s)+" ":"")+t.green("+"+p)+" "+t.red("−"+l)];let d=!1,_=0,$=!1;for(let e=0;e<c.length;e++){if(!h[e]){d||(x.push(t.gray(" "+" ".repeat(g)+" ⋯")),d=!0);continue}if(d=!1,_>=o){$=!0;break}const n=c[e];"+"===n.type?x.push(t.green(" "+y(n.no)+" + ")+t.green(n.text)):"-"===n.type?x.push(t.red(" "+y(n.no)+" - ")+t.red(n.text)):x.push(t.gray(" "+y(n.no)+" "+n.text)),_++}return $&&x.push(t.gray(` …(差异较大,仅显示前 ${o} 行;共 +${p} −${l})`)),x.join("\n")}export function indent(t,e=" "){return String(t).split("\n").map(t=>e+t).join("\n")}export function summarize(e,n=6){const r=String(e).split("\n");return r.length<=n?e:r.slice(0,n).join("\n")+t.gray(`\n …(共 ${r.length} 行)`)}
package/src/repl.js CHANGED
@@ -1,205 +1 @@
1
- // 交互式 REPL + 一次性(print)模式。用同一个 readline 处理「主输入」与「工具授权」(顺序进行,不冲突)。
2
- // 斜杠命令走 commands.js 统一注册表(与 ink TUI 共用);支持 @文件 引用与 !命令 直通。
3
-
4
- import readline from 'node:readline';
5
- import { c } from './colors.js';
6
- import { createPermissions } from './permissions.js';
7
- import { runTurn } from './agent.js';
8
- import { createSession, saveSession, turnCount } from './session.js';
9
- import { loadMcpTools, closeMcp } from './mcp.js';
10
- import { makeSubagentTool } from './subagent.js';
11
- import { makeConsultTool } from './consult.js';
12
- import { runSlash } from './commands.js';
13
- import { expandAtMentions, buildUserContent, runBangCommand, loadHistory, appendHistory } from './input.js';
14
- import { makeSkillTools } from './skills.js';
15
- import { renderWelcome } from './banner.js';
16
-
17
- /** 汇集动态工具:本地技能(load_skill)+ 本地 MCP 工具 + dispatch_agent 子智能体。返回 {extraTools, clients}。 */
18
- export async function buildExtraTools(config, cwd) {
19
- let mcp = { tools: [], clients: [] };
20
- try {
21
- mcp = await loadMcpTools();
22
- } catch (e) {
23
- process.stdout.write(c.yellow('MCP 初始化失败(忽略):' + e.message) + '\n');
24
- }
25
- // 本地技能工具(.huicai/skills 与 ~/.huicai/skills;无技能时为空数组,零开销)
26
- const extraTools = [...makeSkillTools(cwd), ...mcp.tools];
27
- const dispatch = makeSubagentTool({ config, cwd, extraTools: mcp.tools, depth: 0 });
28
- if (dispatch) extraTools.push(dispatch);
29
- // 混合模式:配置了 --consult 平台伪模型时,暴露 consult_platform 工具(本机驱动 + 平台编排咨询)
30
- const consult = makeConsultTool({ config });
31
- if (consult) extraTools.push(consult);
32
- return { extraTools, clients: mcp.clients };
33
- }
34
-
35
- function banner(config, cwd, permissions, version) {
36
- process.stdout.write(renderWelcome({
37
- model: config.model,
38
- cwd,
39
- version,
40
- platform: /^(agent|workflow|datahuman)-/.test(String(config.model || '')),
41
- yolo: permissions.isYolo(),
42
- consult: config.consult ? (config.consultLabel || config.consult) : null,
43
- }) + '\n');
44
- }
45
-
46
- export async function startRepl({ config, cwd, yolo, initialPrompt, session, version }) {
47
- const rl = readline.createInterface({
48
- input: process.stdin,
49
- output: process.stdout,
50
- history: loadHistory(cwd).reverse(), // readline 的 history 最近的在前
51
- historySize: 200,
52
- });
53
- const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
54
- const permissions = createPermissions({ yolo, ask, cwd });
55
- const usage = { prompt: 0, completion: 0 };
56
- const todoState = { list: [] };
57
- let currentAbort = null;
58
- // ctx 持有可变的 session(/clear 会替换为新会话),各处统一读 ctx.session
59
- const ctx = { config, cwd, permissions, usage, session, version };
60
-
61
- rl.on('SIGINT', () => {
62
- if (currentAbort) currentAbort.abort();
63
- else process.stdout.write('\n' + c.gray('输入 /exit 退出。') + '\n');
64
- });
65
-
66
- banner(config, cwd, permissions, version);
67
- echoResumed(session);
68
- const { extraTools, clients } = await buildExtraTools(config, cwd); // 连接 MCP(若已配置)
69
-
70
- const doTurn = async (text) => {
71
- const { text: expanded, mentions, images } = await expandAtMentions(text, cwd, config);
72
- if (mentions.length) process.stdout.write(c.gray(' · 已展开引用:' + mentions.join('、')
73
- + (images.length ? `(含 ${images.length} 张图片)` : '')) + '\n');
74
- ctx.session.messages.push({ role: 'user', content: buildUserContent(expanded, images) });
75
- currentAbort = new AbortController();
76
- if (!ctx.session.compaction) ctx.session.compaction = {};
77
- config.sessionId = ctx.session.id; // 对话落库审计:当前会话号(/clear 换新会话后随之更新)
78
- const r = await runTurn({
79
- config,
80
- messages: ctx.session.messages,
81
- permissions,
82
- cwd,
83
- signal: currentAbort.signal,
84
- extraTools,
85
- todoState,
86
- compaction: ctx.session.compaction,
87
- });
88
- if (r?.usage) {
89
- usage.prompt += r.usage.prompt;
90
- usage.completion += r.usage.completion;
91
- }
92
- if (r?.lastPromptTokens) ctx.session.lastPromptTokens = r.lastPromptTokens;
93
- currentAbort = null;
94
- saveSession(ctx.session); // 每轮结束自动保存,可随时 --resume 恢复
95
- };
96
-
97
- const cmdCtx = () => ({
98
- ...ctx,
99
- session: ctx.session,
100
- extraTools,
101
- print: (t) => process.stdout.write(String(t).endsWith('\n') ? String(t) : String(t) + '\n'),
102
- });
103
-
104
- if (initialPrompt) {
105
- process.stdout.write(c.cyan('\nhuicai> ') + initialPrompt + '\n');
106
- await doTurn(initialPrompt);
107
- }
108
-
109
- while (true) {
110
- let input;
111
- try {
112
- input = (await ask(c.cyan('\nhuicai> '))).trim();
113
- } catch {
114
- break; // stdin 关闭
115
- }
116
- if (input === '') continue;
117
- appendHistory(cwd, input);
118
- if (input === '/exit' || input === '/quit') break;
119
-
120
- if (input.startsWith('!') && input.length > 1) {
121
- // shell 直通:本机执行,输出加入上下文(不消耗模型调用)
122
- const command = input.slice(1).trim();
123
- const { output, record } = await runBangCommand(command, cwd);
124
- process.stdout.write(c.gray(output.split('\n').map((l) => ' ' + l).join('\n')) + '\n');
125
- ctx.session.messages.push({ role: 'user', content: record });
126
- saveSession(ctx.session);
127
- continue;
128
- }
129
-
130
- if (input.startsWith('/')) {
131
- const r = await runSlash(input, cmdCtx());
132
- if (r.action === 'exit') break;
133
- if (r.action === 'clear') {
134
- ctx.session = createSession({ cwd, model: config.model, messages: [ctx.session.messages[0]] });
135
- todoState.list = [];
136
- process.stdout.write(c.gray('已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n'));
137
- }
138
- if (r.prompt) await doTurn(r.prompt);
139
- continue;
140
- }
141
- await doTurn(input);
142
- }
143
- rl.close();
144
- closeMcp(clients);
145
- process.stdout.write(c.gray('\n再见。\n'));
146
- }
147
-
148
- /** 恢复会话时的提示:标题 + 轮数 + 上一条助手回复摘要,帮用户续上下文。 */
149
- function echoResumed(session) {
150
- const turns = turnCount(session.messages);
151
- if (turns === 0) return;
152
- process.stdout.write(c.gray(` 已恢复会话「${session.title || ''}」(${turns} 轮)`) + '\n');
153
- const lastAssistant = [...session.messages].reverse().find((m) => m.role === 'assistant' && m.content);
154
- if (lastAssistant) {
155
- const text = String(lastAssistant.content).trim();
156
- const snippet = text.length > 400 ? text.slice(0, 400) + '…' : text;
157
- process.stdout.write(c.gray(' 上次回复:') + snippet.replace(/\n/g, '\n ') + '\n');
158
- }
159
- }
160
-
161
- /**
162
- * 一次性(非交互)模式:跑一个指令即退出。无 ask → 写/执行需 --yolo 才放行。
163
- * persist=true(配合 --continue/--resume)时把本轮追加保存回会话,供后续续接。
164
- * outputFormat='json' 时静默过程输出,最后打印一行结构化 JSON(脚本/CI 集成)。
165
- */
166
- export async function runOnce({ config, cwd, yolo, prompt, session, persist, outputFormat, maxTurns }) {
167
- if (!prompt) {
168
- process.stderr.write('用法: huicai -p "你的指令"\n');
169
- process.exit(1);
170
- }
171
- const json = outputFormat === 'json';
172
- const permissions = createPermissions({ yolo, ask: null, cwd });
173
- const { extraTools, clients } = await buildExtraTools(config, cwd);
174
- const { text: expanded, images } = await expandAtMentions(prompt, cwd, config);
175
- session.messages.push({ role: 'user', content: buildUserContent(expanded, images) });
176
- if (!session.compaction) session.compaction = {};
177
- config.sessionId = session.id; // 对话落库审计:会话号
178
- const r = await runTurn({
179
- config,
180
- messages: session.messages,
181
- permissions,
182
- cwd,
183
- extraTools,
184
- maxIters: maxTurns,
185
- compaction: session.compaction,
186
- io: json ? { write: () => {} } : undefined,
187
- });
188
- if (r?.lastPromptTokens) session.lastPromptTokens = r.lastPromptTokens;
189
- if (persist) saveSession(session);
190
- closeMcp(clients);
191
- if (json) {
192
- const lastAssistant = [...session.messages].reverse().find((m) => m.role === 'assistant' && m.content);
193
- process.stdout.write(JSON.stringify({
194
- type: 'result',
195
- is_error: !!r?.error,
196
- ...(r?.error ? { error: r.error } : {}),
197
- result: lastAssistant ? String(lastAssistant.content) : '',
198
- usage: { input_tokens: r?.usage?.prompt ?? 0, output_tokens: r?.usage?.completion ?? 0 },
199
- session_id: session.id,
200
- model: config.model,
201
- num_turns: turnCount(session.messages),
202
- }) + '\n');
203
- }
204
- if (r?.error) process.exit(1);
205
- }
1
+ import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as n,saveSession as r,turnCount as i}from"./session.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as c}from"./context.js";import{runVerifyLoop as l}from"./autofix.js";import{loadMcpTools as m,closeMcp as p}from"./mcp.js";import{makeSubagentTool as u,makeDispatchAgentsTool as d}from"./subagent.js";import{makeConsultTool as g}from"./consult.js";import{makeAdvisorTool as w}from"./advisor.js";import{makeRecallTool as f,readMemoryRaw as h}from"./memory.js";import{detectScanners as y,makeScanTool as x}from"./scan.js";import{runSlash as T}from"./commands.js";import{expandAtMentions as j,buildUserContent as k,runBangCommand as b,loadHistory as I,appendHistory as v}from"./input.js";import{makeSkillTools as S}from"./skills.js";import{renderWelcome as P}from"./banner.js";export async function buildExtraTools(s,t){let e={tools:[],clients:[]};try{e=await m()}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const n=[...S(t),...e.tools],r=u({config:s,cwd:t,extraTools:e.tools,depth:0});r&&n.push(r);const i=d({config:s,cwd:t,extraTools:e.tools,depth:0});i&&n.push(i);const a=g({config:s});a&&n.push(a);const c=w({config:s});return c&&n.push(c),!1!==s.memoryEnabled&&h(t)&&n.push(f({cwd:t})),y(t).length&&n.push(x({cwd:t})),{extraTools:n,clients:e.clients}}export async function startRepl({config:i,cwd:m,yolo:u,initialPrompt:d,session:g,version:w}){const f=s.createInterface({input:process.stdin,output:process.stdout,history:I(m).reverse(),historySize:200}),h=s=>new Promise(o=>f.question(s,o));let y=t({yolo:u,ask:h,cwd:m});const x={prompt:0,completion:0},S={list:[]},$=a();let _=null,O=null;const W={config:i,cwd:m,permissions:y,usage:x,session:g,version:w};f.on("SIGINT",()=>{_?_.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(P({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,m,y,w),E(g);let{extraTools:q,clients:C}=await buildExtraTools(i,m);const J=async s=>{const{text:t,mentions:n,images:a}=await j(s,m,i);n.length&&process.stdout.write(o.gray(" · 已展开引用:"+n.join("、")+(a.length?`(含 ${a.length} 张图片)`:""))+"\n");const c=$.snapshot({session:W.session,label:s});W.session.messages.push({role:"user",content:k(t,a)}),_=new AbortController,W.session.compaction||(W.session.compaction={}),i.sessionId=W.session.id;const l=await e({config:i,messages:W.session.messages,permissions:y,cwd:m,signal:_.signal,extraTools:q,todoState:S,compaction:W.session.compaction,fileJournal:c});return l?.usage&&(x.prompt+=l.usage.prompt,x.completion+=l.usage.completion),l?.lastPromptTokens&&(W.session.lastPromptTokens=l.lastPromptTokens),_=null,r(W.session),l},N=()=>({...W,session:W.session,extraTools:q,checkpoints:$,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(d&&(process.stdout.write(o.cyan("\nhuicai> ")+d+"\n"),await J(d));;){let s;try{s=(await h(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(v(m,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:n}=await b(t,m);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),W.session.messages.push({role:"user",content:n}),r(W.session);continue}if(s.startsWith("/")){const e=await T(s,N());if("exit"===e.action)break;"clear"===e.action&&(W.session=n({cwd:m,model:i.model,messages:[W.session.messages[0]]}),S.list=[],$.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&E(W.session),"cd"===e.action&&(m=e.cwd,W.cwd=m,y=t({yolo:y.isYolo(),ask:h,cwd:m}),W.permissions=y,p(C),({extraTools:q,clients:C}=await buildExtraTools(i,m)),W.session=n({cwd:m,model:i.model,messages:[{role:"system",content:c(m,{memory:!1!==i.memoryEnabled})}]}),S.list=[],$.list.length=0,i.sessionId=W.session.id,process.stdout.write(o.gray("已切换到 "+m+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await l({command:e.command,maxIters:e.maxIters,cwd:m,runTurnOnce:s=>J(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"loop"===e.action&&(O&&(clearInterval(O),O=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),O=setInterval(()=>{_||J(e.prompt)},1e3*e.seconds))),e.prompt&&await J(e.prompt);continue}await J(s)}}O&&clearInterval(O),f.close(),p(C),process.stdout.write(o.gray("\n再见。\n"))}function E(s){const t=i(s.messages);if(0===t)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${t} 轮)`)+"\n");const e=[...s.messages].reverse().find(s=>"assistant"===s.role&&s.content);if(e){const s=String(e.content).trim(),t=s.length>400?s.slice(0,400)+"…":s;process.stdout.write(o.gray(" 上次回复:")+t.replace(/\n/g,"\n ")+"\n")}}export async function runOnce({config:s,cwd:o,yolo:n,prompt:a,session:c,persist:l,outputFormat:m,maxTurns:u}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const d="json"===m,g=t({yolo:n,ask:null,cwd:o}),{extraTools:w,clients:f}=await buildExtraTools(s,o),{text:h,images:y}=await j(a,o,s);c.messages.push({role:"user",content:k(h,y)}),c.compaction||(c.compaction={}),s.sessionId=c.id;const x=await e({config:s,messages:c.messages,permissions:g,cwd:o,extraTools:w,maxIters:u,compaction:c.compaction,io:d?{write:()=>{}}:void 0});if(x?.lastPromptTokens&&(c.lastPromptTokens=x.lastPromptTokens),l&&r(c),p(f),d){const o=[...c.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!x?.error,...x?.error?{error:x.error}:{},result:o?String(o.content):"",usage:{input_tokens:x?.usage?.prompt??0,output_tokens:x?.usage?.completion??0},session_id:c.id,model:s.model,num_turns:i(c.messages)})+"\n")}x?.error&&process.exit(1)}
package/src/scan.js ADDED
@@ -0,0 +1 @@
1
+ import n from"node:fs";import e from"node:path";import{execSync as t}from"node:child_process";import{captureCommand as o}from"./autofix.js";function r(t,o){try{return n.existsSync(e.join(t,o))}catch{return!1}}function a(n){try{return t("win32"===process.platform?`where ${n}`:`command -v ${n}`,{stdio:"ignore"}),!0}catch{return!1}}export function detectScanners(n){const e=[];return r(n,"package.json")&&a("npm")&&e.push({name:"npm audit",command:"npm audit --audit-level=high",why:"Node 依赖漏洞"}),(r(n,"requirements.txt")||r(n,"pyproject.toml"))&&a("pip-audit")&&e.push({name:"pip-audit",command:"pip-audit",why:"Python 依赖漏洞"}),r(n,"go.mod")&&a("govulncheck")&&e.push({name:"govulncheck",command:"govulncheck ./...",why:"Go 漏洞"}),r(n,"Cargo.toml")&&a("cargo-audit")&&e.push({name:"cargo audit",command:"cargo audit",why:"Rust 依赖漏洞"}),a("semgrep")&&e.push({name:"semgrep",command:"semgrep --config auto --error -q .",why:"代码静态安全(SAST)"}),a("gitleaks")&&e.push({name:"gitleaks",command:"gitleaks detect --no-banner -r /dev/stdout",why:"密钥泄漏"}),a("trivy")&&e.push({name:"trivy fs",command:"trivy fs --scanners vuln,secret -q .",why:"依赖漏洞+密钥"}),e}export async function runScanners(n,e){const t=[];for(const r of e){const{code:e,output:a}=await o(n,r.command,18e4);t.push({name:r.name,why:r.why,command:r.command,code:e,output:a})}return t}export function makeScanTool({cwd:n}){return{readOnly:!0,spec:{type:"function",function:{name:"security_scan",description:"自动检测并运行本项目适用的安全/质量扫描器(npm audit / semgrep / trivy / gitleaks / 依赖体检等,仅跑本机已装的),返回发现的问题。用于安全审查或提交前自检;发现问题后你应逐条修复再重扫确认。",parameters:{type:"object",properties:{},required:[]}}},run:async()=>{const e=detectScanners(n);if(!e.length)return"未检测到可用的扫描器(可安装 semgrep/trivy/gitleaks,或项目含 package.json 等)。";return(await runScanners(n,e)).map(n=>`## ${n.name}(${0===n.code?"无高危":"有发现"})\n${n.output.slice(0,4e3)}`).join("\n\n")}}}
@@ -0,0 +1 @@
1
+ import r from"node:fs";import n from"node:path";import t from"node:os";export const SCHEDULE_FILE=n.join(t.homedir(),".huicai","schedule.json");function o(){try{const n=JSON.parse(r.readFileSync(SCHEDULE_FILE,"utf8"));return Array.isArray(n)?n:[]}catch{return[]}}function i(t){r.mkdirSync(n.dirname(SCHEDULE_FILE),{recursive:!0}),r.writeFileSync(SCHEDULE_FILE,JSON.stringify(t,null,2)+"\n",{mode:384})}export function listTasks(){return o()}export function addTask({cron:r,prompt:n,cwd:t}){const e=o(),c=Date.now().toString(36)+Math.random().toString(36).slice(2,4);return e.push({id:c,cron:r,prompt:n,cwd:t,created:Date.now()}),i(e),{id:c}}export function removeTask(r){const n=o(),t=n.filter(n=>n.id!==r);return i(t),t.length<n.length}export function crontabLine(r,o="huicai"){return`${r.cron} cd ${JSON.stringify(r.cwd)} && ${o} -p ${JSON.stringify(r.prompt)} --yolo --plain >> ${JSON.stringify(n.join(t.homedir(),".huicai","schedule-"+r.id+".log"))} 2>&1`}
package/src/session.js CHANGED
@@ -1,140 +1 @@
1
- // 会话持久化:把对话(含系统提示词 + 各轮消息)落盘到 ~/.huicai/sessions/<id>.json,
2
- // 支持列出、恢复、删除。按项目目录(cwd)归属,使 --continue/--resume 只看当前项目的会话。
3
- // 所有磁盘操作 best-effort:失败仅告警,绝不让保存/读取异常中断对话本身。
4
-
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import { CONFIG_DIR } from './config.js';
8
-
9
- export const SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
10
-
11
- function ensureDir() {
12
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
13
- }
14
-
15
- function fileOf(id) {
16
- return path.join(SESSIONS_DIR, id + '.json');
17
- }
18
-
19
- function genId() {
20
- return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
21
- }
22
-
23
- /** 由首条用户消息派生标题(截断)。 */
24
- function deriveTitle(messages) {
25
- const firstUser = (messages || []).find((m) => m.role === 'user');
26
- if (!firstUser || !firstUser.content) return '(空会话)';
27
- return String(firstUser.content).replace(/\s+/g, ' ').trim().slice(0, 60) || '(空会话)';
28
- }
29
-
30
- /** 会话中的用户轮数(对话往返次数的近似)。 */
31
- export function turnCount(messages) {
32
- return (messages || []).filter((m) => m.role === 'user').length;
33
- }
34
-
35
- /**
36
- * 新建会话对象(内存态);messages 需已含 system 消息。落盘发生在首次 saveSession。
37
- */
38
- export function createSession({ cwd, model, messages }) {
39
- const now = Date.now();
40
- return {
41
- id: genId(),
42
- cwd,
43
- model,
44
- createdAt: now,
45
- updatedAt: now,
46
- title: '',
47
- messages: messages || [],
48
- // 非破坏式上下文压缩状态(摘要独立存储,messages 永远保留全量原文)
49
- compaction: {},
50
- };
51
- }
52
-
53
- /** 保存会话(覆盖写,0600)。best-effort。 */
54
- export function saveSession(session) {
55
- if (!session || !session.id) return;
56
- try {
57
- ensureDir();
58
- session.updatedAt = Date.now();
59
- if (!session.title) session.title = deriveTitle(session.messages);
60
- fs.writeFileSync(fileOf(session.id), JSON.stringify(session), { mode: 0o600 });
61
- } catch (e) {
62
- process.stderr.write(`(会话保存失败,不影响对话:${e.message})\n`);
63
- }
64
- }
65
-
66
- /** 读取会话;不存在或损坏返回 null。 */
67
- export function loadSession(id) {
68
- if (!id) return null;
69
- try {
70
- const j = JSON.parse(fs.readFileSync(fileOf(id), 'utf8'));
71
- if (!j || !Array.isArray(j.messages)) return null;
72
- if (!j.id) j.id = id;
73
- if (!j.compaction || typeof j.compaction !== 'object') j.compaction = {}; // 旧会话兼容
74
- return j;
75
- } catch {
76
- return null;
77
- }
78
- }
79
-
80
- /**
81
- * 列出会话(按更新时间倒序)。cwd 非空时仅列该项目目录的会话。
82
- * @returns {{id,title,cwd,model,updatedAt,turns}[]}
83
- */
84
- export function listSessions(cwd) {
85
- let files;
86
- try {
87
- ensureDir();
88
- files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json'));
89
- } catch {
90
- return [];
91
- }
92
- const out = [];
93
- for (const f of files) {
94
- try {
95
- const j = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8'));
96
- if (cwd && j.cwd !== cwd) continue;
97
- out.push({
98
- id: j.id || f.replace(/\.json$/, ''),
99
- title: j.title || deriveTitle(j.messages),
100
- cwd: j.cwd,
101
- model: j.model,
102
- updatedAt: j.updatedAt || 0,
103
- turns: turnCount(j.messages),
104
- });
105
- } catch {
106
- // 跳过损坏文件
107
- }
108
- }
109
- out.sort((a, b) => b.updatedAt - a.updatedAt);
110
- return out;
111
- }
112
-
113
- /** 当前项目最近一次会话的 id;无则 null。 */
114
- export function mostRecent(cwd) {
115
- const list = listSessions(cwd);
116
- return list.length ? list[0].id : null;
117
- }
118
-
119
- /** 删除会话文件。 */
120
- export function deleteSession(id) {
121
- try {
122
- fs.unlinkSync(fileOf(id));
123
- return true;
124
- } catch {
125
- return false;
126
- }
127
- }
128
-
129
- /** 相对时间(如「3 分钟前」)。 */
130
- export function relTime(ts) {
131
- if (!ts) return '未知时间';
132
- const s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
133
- if (s < 60) return s + ' 秒前';
134
- const m = Math.floor(s / 60);
135
- if (m < 60) return m + ' 分钟前';
136
- const h = Math.floor(m / 60);
137
- if (h < 24) return h + ' 小时前';
138
- const d = Math.floor(h / 24);
139
- return d + ' 天前';
140
- }
1
+ import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";export const SESSIONS_DIR=e.join(n,"sessions");function r(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function o(t){return e.join(SESSIONS_DIR,t+".json")}function i(t){const e=(t||[]).find(t=>"user"===t.role);return e&&e.content&&String(e.content).replace(/\s+/g," ").trim().slice(0,60)||"(空会话)"}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n}){const r=Date.now();return{id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:r,updatedAt:r,title:"",messages:n||[],compaction:{}}}export function saveSession(e){if(e&&e.id)try{r(),e.updatedAt=Date.now(),e.title||(e.title=i(e.messages)),t.writeFileSync(o(e.id),JSON.stringify(e),{mode:384})}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(o(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let o;try{r(),o=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const s=[];for(const r of o)try{const o=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,r),"utf8"));if(n&&o.cwd!==n)continue;s.push({id:o.id||r.replace(/\.json$/,""),title:o.title||i(o.messages),cwd:o.cwd,model:o.model,updatedAt:o.updatedAt||0,turns:turnCount(o.messages)})}catch{}return s.sort((t,e)=>e.updatedAt-t.updatedAt),s}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e){try{return t.unlinkSync(o(e)),!0}catch{return!1}}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
package/src/settings.js CHANGED
Binary file