Imported from Elc-2077/apicode (
AGENTS.md). Install upstream withnpx skills add Elc-2077/apicode. Copyright stays with the author.
AGENTS.md
api-code-cli(命令 apicode):一个 Node.js 终端 CLI,从「AI API 用量追踪工具(apistat)」演进为「类 Claude Code 的 AI 编码助手 REPL」,同时保留用量统计与代理监控功能。npm 包名 api-code-cli,当前版本 1.2.1。
常用命令
npm start或node bin/cli.js— 启动默认 REPL 对话模式(apicode)- REPL 内斜杠命令:
/help、/clear、/model、/style(回车后在面板里 ↑↓ 选风格,也可/style <id>)、/skills(技能状态;/skills <名称>直接加载、/skills all重扫列前 40)、/exit、/quit node bin/cli.js serve [--port N]— 启动抓包代理服务器(apicode serve)node bin/cli.js monitor— 全屏监控仪表盘(apicode monitor)node bin/cli.js update— 自更新(npm install -g api-code-cli@latest)- 无构建步骤、无 TypeScript、无 lint 配置;
npm test是占位(直接报错),没有测试框架
入口与命令分发(bin/)
bin/cli.js— 主入口。按第一个参数分发:update/serve/monitor,默认进入 REPLbin/cli-monitor.js— 监控仪表盘(terminal-kit 全屏 UI),消费src/modules/*bin/cli-old.js— cli-monitor 的遗留副本(勿改,保持兼容)bin/cli-agent.js— 旧的独立 agent 入口(已被 REPL 内置工具取代)bin/proxy-server.js— 代理服务器独立入口
架构分层(src/)
- REPL / Agent 层(核心)
repl-agent-engine.js— REPLAgentEngine:包装 Agent 工具循环 + 会话 token 统计,完成后写记录到 trackeragent.js— agentic loop:模型输出 tool_calls → 本地执行 → 结果回灌,直到最终回答;同时支持 OpenAI function calling 和 Anthropic tool_useagent-tools.js— 工具 schema(OpenAI function 格式,Anthropic 由 agent.js 转换)+ 执行器:read_file / write_file / edit_file / create_dir / list_dir / glob / grep / run_shell / read_image / load_skillskills.js— 本机技能发现:扫项目与主目录下的.zcode|.agents|.claude/skills,手写极简 frontmatter 解析(只认顶格 name/description,零 yaml 依赖),buildSkillIndex生成进系统提示的技能索引,matchSkills/countSkillMatches供 REPL 的/实时筛选面板,findSkill/readSkillBody供load_skill工具与手动加载取正文styles.js— Agent 表达风格注册表(code默认、neko猫娘):每项只有blocks文案与 UI 前缀,buildStylePrompt拼在 system prompt 最末尾;默认风格返回空串以保证零差异term-width.js/skill-panel.js— 面板的两块地基:前者按终端显示列量宽度(CJK 与「东亚模糊宽度」字符一律算 2 列,宁可估宽不可估窄),后者是候选面板的纯函数层(buildItems挑条目、buildLines出行文本、fitsOneRowEach校验每行只占一行)。bin/cli.js只管游标移动与按键分派。条目有三种type:cmd/skill/choice(二级选择面板用,如/style);提示行固定为「第 N/M 项 · 窗口 a-b · …按键说明」,传hint只替换按键说明那一段,位置信息永远排在最前(窄终端截尾时先丢的是文案,不是位置)repl-fixed-ui.js— 当前 REPL UI(固定输入行);repl-ui.js、repl-scroll-ui.js是旧 UI 变体;repl-engine.js是无工具的旧对话引擎
- 用量追踪层
tracker.js— JSON 记录存储(~/.api-usage-tracker/records.json),addRecord / getStatsinterceptor.js— wrapOpenAI / wrapAnthropic / createTrackedFetch / setupAxiosInterceptor,供第三方以库的方式自动追踪(由根index.js导出)data-manager.js— 多数据源检测(cc-switch 的 sqlite + 本地 records.json),用 sql.js 读库
- 配置 / API 层
config.js— API 配置存于~/.api-usage-tracker/config.json(README 里写的~/.apicode/config.json是错的,以代码为准)api.js— 连接测试、GET /models 拉模型列表、余额查询presets.js— 服务商预设(自动填 baseUrl);constants.js— 平台/模型常量
- 监控模块层
src/modules/*— 仪表盘各功能页:api-manage、group-manage、manual-log、query-status、settings、usage-stats、export-data - 代理层
src/proxy/*— HTTP/HTTPS 拦截代理记录 API 用量;database.js用 sql.js(纯 JS)存~/.api-usage-tracker/apistat.db(package.json 里声明的 better-sqlite3 实际未在此使用)
编辑时需要知道的规则与坑
- 全部 CommonJS(require/module.exports),chalk 用 v4(CJS 兼容);不要引入 ESM 语法
- baseUrl 规范化因协议而异:OpenAI 兼容 → 必须以
/v1结尾(SDK 再拼/chat/completions);Anthropic → 必须去掉/v1(SDK 自己拼/v1/messages)。见agent.js/ai-client.js的normalizeBaseUrl - 危险工具操作(write_file / edit_file / create_dir / run_shell)必须经过 confirm 回调由用户 y/n/a 把关;agent-tools 不做目录牢笼(有意设计),路径按 rootDir(process.cwd())解析
- 技能索引进 system prompt(
skills.js生成 →agent.js的buildSystemPrompt(model, skills)):按 10K 字符预算自适应降级(描述 24 字 → 12 字 → 只列名字;本机 330 个技能 ≈ +3K token/请求,而 agentic loop 每一步都重发 system prompt,成本要按步数估)。Agent.setSkills()热刷新会改写 OpenAI 分支的messages[0].content;Anthropic 分支 system 每次请求现取、天然生效;switchModel重建 Agent 时必须继续带config.skills。读正文前一律先过skills.js的isSkillRecordSafe(skill)(readSkillBody内自检 +queueSkill/load_skill调用点各一道):只允许读「当下真实的技能根 /<技能目录>/ 固定文件名SKILL.md」,文件名不对、../逃逸、root谎报、手搓对象一律拒读——新链路会readFileSync(skill.file),不加这道边界就退化成任意文件读取(提交前扫描已报过,别撤)。load_skill是只读工具、不进 DANGEROUS;技能自带脚本一律由模型经run_shell在技能目录内跑(有意不设专用脚本执行器,避免重复实现与二次确认)。技能目录常是符号链接 / Windows junction,判定要用fs.statSync(跟随链接)而不是 Dirent 的isDirectory()。bin/cli.js的/skills走ui.print/ui.showInfo(内部自动重绘输入行),不要往handleScrollCommand/handleCommand那两段死代码加 case。 - 技能手动加载走「队列 → 随下一条消息发出」:
repl-agent-engine.js的queueSkill()把正文放进pendingSkills,_composeMessage()在sendMessage时拼成【已加载技能:X】…\n\n【用户消息】\n<原文>发出去并移入loadedSkillNames(无队列时原样返回,老行为不变);clearSession()同时清空两者。别改成直接往agent.messages塞 user 消息:Anthropic 分支历史上连续同角色消息与「用户还没说话就出现 user 块」都会改变对话形状。技能正文入历史后每轮重发,卸载手段只有/clear。 - Agent 风格(
styles.js+/style)只允许动表达层:buildSystemPrompt的拼接顺序固定为 base → 技能索引 → 风格段(风格在最后,近因最强);默认code的blocks必须保持为空数组,让buildStylePrompt('code') === '',否则「未启用功能时的提示词」就不再逐字节一致(已有断言)。运行时切换走Agent.setStyle()→_syncSystemPrompt()(与setSkills共用同一套:重建 prompt 并在 OpenAI 分支改写messages[0]);engine.switchModel()重建 Agent 时必须继续带config.style。新风格的文案不得削弱 base prompt 的纪律(危险操作确认、如实回答模型名、报错原样呈现),也不要写${...}这类占位符——blocks 是普通字符串,会被原样塞进提示词。UI 侧uiPrefix只加在showInfo;showError与危险确认文案保持原样,风格标签只出现在统计栏(纯console.log,不影响面板游标记账)。选择持久化在config.json顶层style字段,loadConfig/saveConfig整对象读写可直接复用;无效值必须回落DEFAULT_STYLE而不是报错。落点只有一处:面板确认(acceptPanelItem的choice分支)与/style <id>都调用bin/cli.js的applyStyleChoice(id, engine, ui, config)(setStyle+setUiStyle+config.style+saveConfig,保存失败只降级提示),不要再复制第二份。 - REPL 的
/候选面板(bin/cli.js的panel/renderPanel/refreshPanel):terminal-kit 装的是 3.x,没有eraseLineThenEnd、moveCursor、cursorLocation这些 1.x 名字——可用的是eraseLine()(ESC[2K)、eraseDisplayBelow()(ESC[0J)、eraseLineAfter()(ESC[0K)、up(n)/down(n)(只接正数)、move(dx,dy)(带符号)、column(n)、deleteLine(n)。面板画在输入行下方、不永久占行,做法是逐行term('\n') → column(1) → term(行文本) → styleReset()(styleReset 必须在换行前,否则屏底新造的行继承背景色),画完term.up(rows) + column(1)回输入行;重绘靠column(1) + eraseLine + eraseDisplayBelow一次清掉「输入行及其下方」(下方只可能是面板)。因此:绝不能用getCursorLocation()定行号(异步、200ms 超时、Windows 有 6 处兜底会跳到底行),每行必须按term.width - 1自己截断(终端物理换行会把行数记账打乱),非 TTY(process.stdout.isTTY为假,termconfig 退化成空串/空格)直接不画,resize时收起面板。按键按name白名单分派(UP/DOWN/TAB/SHIFT_TAB/ENTER/ESCAPE);data.isControl在 terminal-kit 里从未被赋值,bin/cli.js现有的if (data.isCharacter && !data.isControl)是死条件,别继续依赖。singleColumnMenu不适合做实时过滤(无输入过滤、字符键被吞、没有setItems、逐键重建会在屏底反复造行),它只留给/model这类静态列表。 - 面板防漂移的铁律:
term.up(n)的前提是「记账行数 == 真实下移行数」,而只要有一行触到右边界物理换行,真实行数就会比记账多,表现为每按一次 ↑↓ 多出一行、上面留下旧的>残渣(已踩过:按String.length裁中文描述,100 列的终端里一行实际吃掉 155 列)。所以任何往面板里画的文本都必须经term-width.dispWidth/clipW按显示列裁剪,行预算取term.width - 4,出图前用skillPanel.fitsOneRowEach(lines, term.width)校验;不通过就整块不画(fail-closed),绝不能画歪。改skill-panel.js后请跑一遍行宽断言(纯函数、无需 TTY;已覆盖 12–400 列 × 6–50 行 × 12 组查询 × 三种选中位 → 35 万余行零超宽)。 - 面板是「完整列表 + 滚动窗口」,不是「屏上那几条」:
panel.items存全部匹配项(skillPanel.buildItems,命令全部 + 技能上限maxItems=400),panel.sel是绝对选中下标、panel.win是可见窗口起点,buildLines依据{selected, winStart, rows}切片渲染。可见正文行数 =skillPanel.panelRowBudget(term.height) - 1(减掉提示行)。按键:↑↓逐条(越界即滚动窗口 = 翻页)、PgUp/PgDn整页、Home/End首尾、Tab/Enter接受、Esc收起;moveSel/jumpSel负责把 sel 保持在 [win, win+rows) 内。别再退回「只取前 N 条 + hi 取模」——那样几百个技能永远选不到后面的(已踩过)。查询词变化时refreshPanel会把sel=0 / win=0复位。 - max_tokens 为 32000,不做自动续写(v1.2.1 行为):截断时提示用户输入「继续」接续;会话历史跨轮保留。改动这块前先看
repl-agent-engine.js/agent.js里的截断处理 - 面板有两种模式(
panel.mode):'slash'= 输入/的实时搜索;'pick'= 命令回车后的二级选择(目前只有/style无参数)。两种模式共用同一套renderPanel / moveSel / jumpSel与up(n)记账,所以打开二级面板只能经注入的skillUi.pickStyle()→beginPick(items, hint)(它先hidePanel()复位、再立条目),绝不能直接给panel.items赋值。不变式:hidePanel()把 mode 复位成'slash'并清hint,refreshPanel()(搜索路径)也强制复位,renderPanel()的两条 fail-closed/异常分支同样复位——所以 mode 与 items 永远不会互相打架。beginPick在非 TTY、条目为空、或行宽放不下(被 fail-closed 清空)时返回false,handleFixedCommand的/style据此退回原来的静态列表文案。pick 模式按键:数字1-9直接选中该项,其余字符先收起面板再按普通输入走;Enter/Tab走acceptPanelItem(按item.type === 'choice'分派);Esc收起并回报「仍使用 X」(wasPick要在hidePanel()之前取)。别把二级面板写成singleColumnMenu:它自带阻塞、要临时摘掉 REPL 的 keyHandler,只有/model那种长静态列表值得(见下一条)。 - terminal-kit 按键监听:弹出菜单时要临时
term.removeListener('key', keyHandler),结束后term.grabInput({ mouse: false })再装回,否则菜单和 REPL 抢按键(参考 bin/cli.js 的pickModelInteractive) - UI 文案、代码注释、系统提示词全部为中文;README 部分内容已过时(其自述「以文件里面的为准」),改动行为后应同步更新 README
- 用户数据目录
~/.api-usage-tracker/(records.json / config.json / apistat.db)已在 .gitignore 排除,勿提交 - read_image 返回 base64 图像内容,走 OpenAI / Anthropic 的视觉消息格式(见 agent.js 中的消息构造)
动手前建议先读
PLAN.md— REPL 改造的完整设计文档(架构图、分层、状态栏设计)src/repl-agent-engine.js+src/agent.js— 理解一次对话从输入到统计落盘的完整链路
