@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
package/src/config.js CHANGED
@@ -1,66 +1 @@
1
- // 配置读写:~/.huicai/config.json(含 baseUrl / apiKey / model),环境变量优先覆盖。
2
- // apiKey 敏感:写入时置文件权限 0600。
3
-
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import os from 'node:os';
7
-
8
- export const CONFIG_DIR = path.join(os.homedir(), '.huicai');
9
- export const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
10
-
11
- /** 平台默认地址(内置,用户无需输入网关地址;可用 HUICAI_BASE_URL / --base 覆盖,供自建/联调)。 */
12
- export const DEFAULT_BASE_URL = 'https://huicai-ai.sjht.com';
13
-
14
- /** 内置默认模型(用户未设置时用它;登录时若账号有它则优先,否则回退到账号的第一个原始模型)。 */
15
- export const DEFAULT_MODEL = 'deepseek-v4-flash';
16
-
17
- function readFileConfig() {
18
- try {
19
- return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
20
- } catch {
21
- return {};
22
- }
23
- }
24
-
25
- /**
26
- * 解析生效配置:环境变量 > 配置文件 > 缺省。
27
- * @returns {{baseUrl:string, apiKey:string, model:string}}
28
- */
29
- export function loadConfig() {
30
- const file = readFileConfig();
31
- const cfg = {
32
- // 网关地址内置默认,用户无需输入;仍可用环境变量/配置文件覆盖(自建或联调)
33
- baseUrl: (process.env.HUICAI_BASE_URL || file.baseUrl || DEFAULT_BASE_URL).trim(),
34
- apiKey: (process.env.HUICAI_API_KEY || file.apiKey || '').trim(),
35
- model: (process.env.HUICAI_MODEL || file.model || DEFAULT_MODEL).trim(),
36
- // 单回合最大工具轮数(0/缺省 = 用内置默认 60);可被 --max-turns 覆盖
37
- maxTurns: Number(process.env.HUICAI_MAX_TURNS || file.maxTurns) || 0,
38
- // 自动压缩触发的 prompt_tokens 上限(缺省用 compaction 内置默认 80000)
39
- autoCompactTokens: Number(file.autoCompactTokens) || 0,
40
- // 压缩摘要专用模型(缺省:当前模型;当前为伪模型时 compaction 内部回退 DEFAULT_MODEL——摘要绝不经智能体编排)
41
- compactModel: (process.env.HUICAI_COMPACT_MODEL || file.compactModel || '').trim(),
42
- // 长期记忆(memory_write + 系统提示词注入):默认开;配置 memory:false 或 HUICAI_MEMORY=0 关闭
43
- memoryEnabled: process.env.HUICAI_MEMORY === '0' ? false : file.memory !== false,
44
- // 思考过程流式展示:默认开;配置 showReasoning:false 或 HUICAI_REASONING=0 关闭
45
- showReasoning: process.env.HUICAI_REASONING === '0' ? false : file.showReasoning !== false,
46
- // 助手回复 Markdown 渲染(TUI 富文本):默认开;配置 markdown:false 或 HUICAI_MARKDOWN=0 关闭(显示原始文本)
47
- markdown: process.env.HUICAI_MARKDOWN === '0' ? false : file.markdown !== false,
48
- // 客户端来源标识(对话落库审计用):默认 cli;VSCode 扩展进程内置 HUICAI_CLIENT=vscode
49
- client: (process.env.HUICAI_CLIENT || 'cli').trim().toLowerCase(),
50
- };
51
- cfg.baseUrl = cfg.baseUrl.replace(/\/+$/, ''); // 去尾斜杠
52
- return cfg;
53
- }
54
-
55
- /** 合并写入配置文件(0600),返回合并结果。 */
56
- export function saveConfig(partial) {
57
- const merged = { ...readFileConfig(), ...partial };
58
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
59
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
60
- try {
61
- fs.chmodSync(CONFIG_PATH, 0o600);
62
- } catch {
63
- // 非 POSIX 忽略
64
- }
65
- return merged;
66
- }
1
+ import e from"node:fs";import o from"node:path";import r from"node:os";export const CONFIG_DIR=o.join(r.homedir(),".huicai");export const CONFIG_PATH=o.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function n(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=n(),o={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,autoCompactTokens:Number(e.autoCompactTokens)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),budgetTokens:Number(e.budgetTokens)||0,embeddingModel:(process.env.HUICAI_EMBED_MODEL||e.embeddingModel||"").trim()};return o.baseUrl=o.baseUrl.replace(/\/+$/,""),o}export function saveConfig(o){const r={...n(),...o};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(r,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return r}
package/src/consult.js CHANGED
@@ -1,65 +1 @@
1
- // 混合模式(对应 VSCode 扩展的「平台伪模型 + 工作区」):把平台智能体/工作流/数字人作为「咨询工具」
2
- // 暴露给本机智能体循环。本机用原始驱动模型 + 本地工具(读写文件/执行命令/子智能体…),遇到平台目标擅长的问题
3
- // 调用 consult_platform——由平台侧编排作答(服务端知识库 RAG / 工具 / 联网),再用本机工具把结论落地到项目。
4
-
5
- import { streamChat } from './api.js';
6
-
7
- const PSEUDO = /^(agent|workflow|datahuman)-/;
8
-
9
- /** 平台伪模型 id → 中文类别名。 */
10
- function kindOf(ref) {
11
- if (ref.startsWith('workflow-')) return '工作流';
12
- if (ref.startsWith('datahuman-')) return '业务数字人';
13
- return '智能体';
14
- }
15
-
16
- /**
17
- * 构造 consult_platform 工具;未配置或 ref 非平台伪模型时返回 null。
18
- * @param {object} p
19
- * @param {object} p.config 运行配置(读时取最新;含 baseUrl/apiKey/consult/consultLabel/client/sessionId)
20
- */
21
- export function makeConsultTool({ config }) {
22
- const ref = config && config.consult;
23
- if (!ref || !PSEUDO.test(String(ref))) return null;
24
- const kind = kindOf(String(ref));
25
- const label = config.consultLabel || ref;
26
-
27
- const spec = {
28
- type: 'function',
29
- function: {
30
- name: 'consult_platform',
31
- description:
32
- `咨询平台${kind}「${label}」:由平台侧编排作答(带其服务端知识库 RAG / 工具 / 联网检索)。`
33
- + `遇到它擅长领域的问题优先调用它获取权威答复,再用本机工具把结论落地到项目。可多次调用。`,
34
- parameters: {
35
- type: 'object',
36
- properties: {
37
- query: { type: 'string', description: '自然语言的咨询 / 查询 / 运行输入' },
38
- },
39
- required: ['query'],
40
- },
41
- },
42
- };
43
-
44
- const run = async (args) => {
45
- const q = String((args && args.query) || '').trim();
46
- if (!q) return '需要提供 query(要咨询平台的自然语言问题 / 输入)';
47
- try {
48
- // 走平台伪模型 → 服务端编排;client/sessionId 使这轮咨询也计入对话审计
49
- const r = await streamChat({
50
- baseUrl: config.baseUrl,
51
- apiKey: config.apiKey,
52
- model: ref,
53
- messages: [{ role: 'user', content: q }],
54
- client: config.client,
55
- sessionId: config.sessionId,
56
- }, {});
57
- return r.content || '(平台无返回)';
58
- } catch (e) {
59
- if (e && e.name === 'AbortError') throw e;
60
- return `平台调用失败:${e.message}`;
61
- }
62
- };
63
-
64
- return { readOnly: true, spec, run };
65
- }
1
+ import{streamChat as t}from"./api.js";const r=/^(agent|workflow|datahuman)-/;export function makeConsultTool({config:e}){const n=e&&e.consult;if(!n||!r.test(String(n)))return null;const o=function(t){return t.startsWith("workflow-")?"工作流":t.startsWith("datahuman-")?"业务数字人":"智能体"}(String(n));return{readOnly:!0,spec:{type:"function",function:{name:"consult_platform",description:`咨询平台${o}「${e.consultLabel||n}」:由平台侧编排作答(带其服务端知识库 RAG / 工具 / 联网检索)。遇到它擅长领域的问题优先调用它获取权威答复,再用本机工具把结论落地到项目。可多次调用。`,parameters:{type:"object",properties:{query:{type:"string",description:"自然语言的咨询 / 查询 / 运行输入"}},required:["query"]}}},run:async r=>{const o=String(r&&r.query||"").trim();if(!o)return"需要提供 query(要咨询平台的自然语言问题 / 输入)";try{return(await t({baseUrl:e.baseUrl,apiKey:e.apiKey,model:n,messages:[{role:"user",content:o}],client:e.client,sessionId:e.sessionId},{})).content||"(平台无返回)"}catch(t){if(t&&"AbortError"===t.name)throw t;return`平台调用失败:${t.message}`}}}}
@@ -0,0 +1 @@
1
+ import{buildModelMessages as t}from"./compaction.js";import{c as o}from"./colors.js";const e=/[぀-ヿ㐀-鿿가-힯豈-﫿＀-￯]/g;export function estimateTokens(t){if(!t)return 0;const o=String(t),s=(o.match(e)||[]).length,r=o.length-s;return Math.ceil(1.6*s+r/4)}function s(t){if("string"==typeof t.content)return estimateTokens(t.content);if(Array.isArray(t.content)){let o=0;for(const e of t.content)"text"===e?.type&&e.text?o+=estimateTokens(e.text):"image_url"===e?.type&&(o+=300);return o}return 0}function r(t){return(t=Math.round(Number(t)||0))>=1e3?(t/1e3).toFixed(1)+"k":String(t)}function n(t,o=14){const e=Math.max(0,Math.min(o,Math.round(t*o)));return"█".repeat(e)+"░".repeat(o-e)}export function contextReport({messages:e,compaction:a,specs:l,lastPromptTokens:u,autoCompactTokens:c,platform:i}){const m=i?e:t(e,a||{}),p=!!(a&&a.summary&&a.upTo>1),k={system:{label:"系统提示词",tok:0},tools:{label:"工具定义",tok:0},summary:{label:"压缩摘要",tok:0},user:{label:"用户消息",tok:0},assistant:{label:"助手回复",tok:0},toolcall:{label:"工具调用",tok:0},toolresult:{label:"工具结果",tok:0}};m.forEach((t,o)=>{0!==o||"system"!==t.role?1===o&&p&&"user"===t.role?k.summary.tok+=s(t):"user"===t.role?k.user.tok+=s(t):"assistant"===t.role?(k.assistant.tok+=s(t),t.tool_calls&&(k.toolcall.tok+=estimateTokens(JSON.stringify(t.tool_calls)))):"tool"===t.role&&(k.toolresult.tok+=s(t)):k.system.tok+=s(t)}),l&&l.length&&(k.tools.tok=estimateTokens(JSON.stringify(l)));const y=["system","tools","summary","user","assistant","toolcall","toolresult"];let h=y.reduce((t,o)=>t+k[o].tok,0),f=1;u>0&&h>0&&(f=u/h);const g=t=>k[t].tok*f,d=u>0?u:h,b=[];b.push(o.bold("上下文占用")+o.gray("(当前发给模型的视图 · 估算)"));for(const t of y){const e=g(t);if(e<1&&0===k[t].tok)continue;const s=d>0?e/d:0;b.push(" "+k[t].label.padEnd(6," ")+" "+("~"+r(e)).padStart(7)+" "+o.cyan(n(s))+" "+String(Math.round(100*s)).padStart(3)+"%")}b.push(o.gray(" "+"─".repeat(34)));const x=Number(c)||8e4,M=Math.min(100,Math.round(d/x*100));return b.push(" 合计   "+("~"+r(d)).padStart(7)+o.gray(` 自动压缩阈值 ${r(x)}(${M}%)`)),u>0?b.push(o.gray(` 实测上一轮 prompt_tokens:${r(u)}(分类按此校准)`)):b.push(o.gray(" (尚无实测值,纯估算;发生一轮对话后更准)")),p&&b.push(o.gray(" 已启用上下文压缩:更早的历史以摘要形式计入,原文完整保留。")),b.push(o.gray(" /compact 立即压缩 · /context 再次查看")),b.join("\n")}
package/src/context.js CHANGED
@@ -1,144 +1 @@
1
- // 组装系统提示词:人格 + 工作准则 + 运行环境(cwd/OS/git/顶层文件)
2
- // + 项目说明文件(HUICAI.md/AGENTS.md/CLAUDE.md)+ 全局用户说明(~/.huicai/HUICAI.md)。
3
-
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import os from 'node:os';
7
- import { execSync } from 'node:child_process';
8
- import { loadMemory } from './memory.js';
9
- import { skillCatalog } from './skills.js';
10
-
11
- function git(cwd) {
12
- const run = (cmd) => execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
13
- try {
14
- const branch = run('git rev-parse --abbrev-ref HEAD');
15
- let status = '';
16
- try {
17
- status = run('git status --short');
18
- } catch {
19
- /* ignore */
20
- }
21
- return { branch, status };
22
- } catch {
23
- return null;
24
- }
25
- }
26
-
27
- function topLevel(cwd) {
28
- try {
29
- return fs
30
- .readdirSync(cwd, { withFileTypes: true })
31
- .filter((e) => e.name !== '.git' && e.name !== 'node_modules')
32
- .slice(0, 60)
33
- .map((e) => (e.isDirectory() ? e.name + '/' : e.name))
34
- .join(' ');
35
- } catch {
36
- return '';
37
- }
38
- }
39
-
40
- function projectDoc(cwd) {
41
- for (const name of ['HUICAI.md', 'AGENTS.md', 'CLAUDE.md']) {
42
- try {
43
- const s = fs.readFileSync(path.join(cwd, name), 'utf8');
44
- if (s.trim()) return { name, content: s.slice(0, 8000) };
45
- } catch {
46
- /* next */
47
- }
48
- }
49
- return null;
50
- }
51
-
52
- /** 全局用户说明(~/.huicai/HUICAI.md):跨项目的个人偏好/约定。 */
53
- function globalDoc() {
54
- try {
55
- const s = fs.readFileSync(path.join(os.homedir(), '.huicai', 'HUICAI.md'), 'utf8');
56
- return s.trim() ? s.slice(0, 4000) : null;
57
- } catch {
58
- return null;
59
- }
60
- }
61
-
62
- const GUIDELINES = [
63
- '- 先了解现状再动手:并行使用 read_file/glob/grep/list_files 收集信息(一轮可同时调多个只读工具),不要臆测文件内容。',
64
- '- read_file 输出的「行号→」前缀仅供定位,不是文件内容;写 edit_file 的 old_string/new_string 时绝不能包含行号前缀。',
65
- '- 多步骤任务先用 todo_write 列出计划,并随进度更新状态(同一时刻至多一项 in_progress);单步小事不必用。',
66
- '- 改代码优先用 edit_file 做最小精确改动;新建文件才用 write_file。遵循项目既有风格与命名。',
67
- '- 用 run_bash 执行构建/测试/git 等;改动后尽量运行测试或构建自检,失败要定位修复而不是忽略。',
68
- '- 需要在线资料(文档/API 说明)可用 fetch_url;带明确目的使用,不要漫游。',
69
- '- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。',
70
- '- 不要把密钥、令牌等敏感值回显到输出或写进代码。',
71
- '- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。',
72
- ];
73
-
74
- /**
75
- * 组装系统提示词。
76
- * @param {string} cwd 工作目录
77
- * @param {{persona?:string, append?:string}} [opts]
78
- * persona 非空时以该智能体人格领衔(--agent 模式),但保留本机工具/环境说明;
79
- * append 追加在末尾(--append-system-prompt)。
80
- */
81
- export function buildSystemPrompt(cwd, opts = {}) {
82
- const g = git(cwd);
83
- const doc = projectDoc(cwd);
84
- const gdoc = globalDoc();
85
- let p = '';
86
- if (opts.persona && opts.persona.trim()) {
87
- p += '# 你的角色设定\n' + opts.persona.trim() + '\n\n';
88
- p += '# 运行方式\n';
89
- p += '你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n';
90
- } else {
91
- p += '你是 huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n';
92
- p += '你能通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令,从而真正完成任务,而不仅是给建议。\n\n';
93
- p += '# 工作准则\n';
94
- }
95
- p += GUIDELINES.join('\n') + '\n\n';
96
- p += '# 运行环境\n';
97
- p += `- 工作目录: ${cwd}\n`;
98
- p += `- 操作系统: ${os.platform()} ${os.release()} (${os.arch()})\n`;
99
- p += `- 日期: ${new Date().toISOString().slice(0, 10)}\n`;
100
- if (g) {
101
- p += `- Git 分支: ${g.branch}\n`;
102
- if (g.status) {
103
- const lines = g.status.split('\n').slice(0, 30).map((l) => ' ' + l).join('\n');
104
- p += `- 未提交改动:\n${lines}\n`;
105
- }
106
- }
107
- const top = topLevel(cwd);
108
- if (top) p += `- 顶层条目: ${top}\n`;
109
- const editor = editorContext();
110
- if (editor) p += '\n# 编辑器上下文(来自 IDE 集成)\n' + editor;
111
- if (gdoc) p += `\n# 全局用户说明(~/.huicai/HUICAI.md)\n${gdoc}\n`;
112
- if (doc) p += `\n# 项目说明(${doc.name})\n${doc.content}\n`;
113
- // 本地技能目录(渐进披露:仅名称+描述常驻,正文经 load_skill 按需载入)
114
- const skills = skillCatalog(cwd);
115
- if (skills) p += `\n# 可用技能(本地 Agent Skills)\n${skills}\n`;
116
- if (opts.memory !== false) {
117
- const mem = loadMemory(cwd);
118
- if (mem) {
119
- p += `\n# 长期记忆(此前会话经 memory_write 记录,跨会话生效)\n${mem}\n`;
120
- p += '(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用 memory_write 记录。)\n';
121
- }
122
- }
123
- if (opts.append && String(opts.append).trim()) p += '\n# 附加指示\n' + String(opts.append).trim() + '\n';
124
- return p;
125
- }
126
-
127
- /** VS Code 等 IDE 扩展经环境变量注入的当前编辑上下文:正在编辑的文件 + 选中片段 + 诊断(Problems)。 */
128
- function editorContext() {
129
- const file = process.env.HUICAI_IDE_FILE;
130
- const selection = process.env.HUICAI_IDE_SELECTION;
131
- const diagnostics = process.env.HUICAI_IDE_DIAGNOSTICS;
132
- if (!file && !selection && !diagnostics) return '';
133
- let s = '';
134
- if (file) s += `- 用户当前正在编辑的文件: ${file}\n`;
135
- if (selection) {
136
- const clipped = selection.length > 4000 ? selection.slice(0, 4000) + '\n…(选区已截断)' : selection;
137
- s += '- 用户当前选中的代码片段:\n```\n' + clipped + '\n```\n';
138
- }
139
- if (diagnostics) {
140
- const clipped = diagnostics.length > 4000 ? diagnostics.slice(0, 4000) + '\n…(诊断已截断)' : diagnostics;
141
- s += '- 编辑器诊断(错误/警告,来自 Problems 面板):\n```\n' + clipped + '\n```\n';
142
- }
143
- return s;
144
- }
1
+ import n from"node:fs";import e from"node:path";import t from"node:os";import{execSync as r}from"node:child_process";import{loadMemory as i}from"./memory.js";import{skillCatalog as o}from"./skills.js";import{buildRepoMap as s}from"./codeindex.js";const c=["- 先了解现状再动手:并行使用 read_file/glob/grep/list_files 收集信息(一轮可同时调多个只读工具),不要臆测文件内容。","- read_file 输出的「行号→」前缀仅供定位,不是文件内容;写 edit_file 的 old_string/new_string 时绝不能包含行号前缀。","- 多步骤任务先用 todo_write 列出计划,并随进度更新状态(同一时刻至多一项 in_progress);单步小事不必用。","- 改代码优先用 edit_file 做最小精确改动;新建文件才用 write_file。遵循项目既有风格与命名。","- 用 run_bash 执行构建/测试/git 等;改动后尽量运行测试或构建自检,失败要定位修复而不是忽略。","- 需要在线资料(文档/API 说明)可用 fetch_url;带明确目的使用,不要漫游。","- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。","- 不要把密钥、令牌等敏感值回显到输出或写进代码。","- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。"];export function buildSystemPrompt(l,m={}){const a=function(n){const e=e=>r(e,{cwd:n,stdio:["ignore","pipe","ignore"]}).toString().trim();try{const n=e("git rev-parse --abbrev-ref HEAD");let t="";try{t=e("git status --short")}catch{}return{branch:n,status:t}}catch{return null}}(l),f=function(t){for(const r of["HUICAI.md","AGENTS.md","CLAUDE.md"])try{const i=n.readFileSync(e.join(t,r),"utf8");if(i.trim())return{name:r,content:i.slice(0,8e3)}}catch{}return null}(l),p=function(){try{const r=n.readFileSync(e.join(t.homedir(),".huicai","HUICAI.md"),"utf8");return r.trim()?r.slice(0,4e3):null}catch{return null}}();let u="";if(m.persona&&m.persona.trim()?(u+="# 你的角色设定\n"+m.persona.trim()+"\n\n",u+="# 运行方式\n",u+="你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n"):(u+="你是 huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n",u+="你能通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令,从而真正完成任务,而不仅是给建议。\n\n",u+="# 工作准则\n"),u+=c.join("\n")+"\n\n",u+="# 运行环境\n",u+=`- 工作目录: ${l}\n`,u+=`- 操作系统: ${t.platform()} ${t.release()} (${t.arch()})\n`,u+=`- 日期: ${(new Date).toISOString().slice(0,10)}\n`,a&&(u+=`- Git 分支: ${a.branch}\n`,a.status)){u+=`- 未提交改动:\n${a.status.split("\n").slice(0,30).map(n=>" "+n).join("\n")}\n`}const d=function(e){try{return n.readdirSync(e,{withFileTypes:!0}).filter(n=>".git"!==n.name&&"node_modules"!==n.name).slice(0,60).map(n=>n.isDirectory()?n.name+"/":n.name).join(" ")}catch{return""}}(l);d&&(u+=`- 顶层条目: ${d}\n`);const h=function(){const n=process.env.HUICAI_IDE_FILE,e=process.env.HUICAI_IDE_SELECTION,t=process.env.HUICAI_IDE_DIAGNOSTICS;if(!n&&!e&&!t)return"";let r="";n&&(r+=`- 用户当前正在编辑的文件: ${n}\n`);if(e){r+="- 用户当前选中的代码片段:\n```\n"+(e.length>4e3?e.slice(0,4e3)+"\n…(选区已截断)":e)+"\n```\n"}if(t){r+="- 编辑器诊断(错误/警告,来自 Problems 面板):\n```\n"+(t.length>4e3?t.slice(0,4e3)+"\n…(诊断已截断)":t)+"\n```\n"}return r}();if(h&&(u+="\n# 编辑器上下文(来自 IDE 集成)\n"+h),p&&(u+=`\n# 全局用户说明(~/.huicai/HUICAI.md)\n${p}\n`),f&&(u+=`\n# 项目说明(${f.name})\n${f.content}\n`),!1!==m.repoMap)try{const n=s(l);n&&(u+=`\n# 项目地图(关键文件与主要符号;用 search_code 按概念检索相关实现)\n${n}\n`)}catch{}const _=o(l);if(_&&(u+=`\n# 可用技能(本地 Agent Skills)\n${_}\n`),!1!==m.memory){const n=i(l);n&&(u+=`\n# 长期记忆(此前会话经 memory_write 记录,跨会话生效)\n${n}\n`,u+="(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用 memory_write 记录。)\n")}return m.append&&String(m.append).trim()&&(u+="\n# 附加指示\n"+String(m.append).trim()+"\n"),u}
package/src/cron.js ADDED
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1 @@
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")}
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1 @@
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")}