@leviyuan/lodestar 0.11.18 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ lodestar-setup
57
57
  | `hi` | 未运行时同一张卡动态启动并收束为控制台;运行中弹控制台 |
58
58
  | `stop` / `st` | 软打断当前 turn,子进程保活,排队消息打 ❌ |
59
59
  | `kill` / `kl` | 用状态卡展示关闭 Codex 进程,`threadId` 落盘 |
60
- | `restart` / `rs` | 用状态卡展示按上次 `threadId` 重启(保留上下文)|
60
+ | `restart` / `rs` | 进行中:打断 + 放弃后台进程 + 恢复;空闲:列出项目最近 24h 会话(不足 10 条补更早),选一个在本群接续恢复 |
61
61
  | `clear` / `cl` | 用状态卡展示杀进程并开新 thread(等价 `/clear`)|
62
62
  | `compact` / `cm` | 主动触发当前 thread 的上下文压缩,完成后状态卡收束 |
63
63
  | `model` / `md` | 展示固定两项面板(Codex·GPT-5.5 / Claude·GLM-5.2),一键生效,按群持久化 |
@@ -74,6 +74,19 @@ lodestar-setup
74
74
 
75
75
  `删` 会先确认对应 worktree 群没有正在运行的 Codex session,再检查 worktree 没有未提交变更,然后解散群并删除 worktree 目录;分支保留,合并和分支清理由 agent 处理。
76
76
 
77
+ **临时会话 / 分叉 / 回滚**(Claude 后端)
78
+
79
+ 在同一个项目目录里多开 Claude 会话,加上语义化的分叉/回滚——基于 Claude 原生的会话 fork(派生新会话 id,原对话不动):
80
+
81
+ | 指令 | 行为 |
82
+ | --- | --- |
83
+ | `btw` | 开一个临时群 `<project>*MMDD-HHMM`(同一个项目目录,自动启动一个干净 Claude) |
84
+ | `bye` | 解散当前临时群(只能在 `*` 开头的临时群里用) |
85
+ | `fk` / `fork` | 列出当前会话的每条用户输入(倒序),选一条 → 从这条**之前**开临时群分叉,原会话不动 |
86
+ | `bk` / `back` | 立刻终止当前 + 列用户输入,选一条 → 当前会话回退到这条**之前**,并发一张回滚段 Write 记录卡(代码块,可复制后编辑重发) |
87
+
88
+ 分叉/回滚点以「用户输入」为分界:选第 N 条 = 回到这条发出之前的状态。`rs` 空闲模式的历史列表数据源是 Claude 自己的会话存档(`~/.claude/projects/<本项目目录编码>/*.jsonl`),同一工作目录的全部会话都列出来,worktree(不同目录)不会混进来。
89
+
77
90
  ---
78
91
 
79
92
  ## 🎁 附加能力
@@ -94,101 +107,69 @@ lodestar-setup
94
107
 
95
108
  `task` 面板里的 `删` 会二次确认,确认后删除整个清单和清单内任务。这个能力需要飞书应用开通任务清单/任务/评论相关权限;缺权限时面板会显示 Open API 返回的失败原因和缺失 scope。
96
109
 
97
- ### 🔀 自定义 Claude Code 可执行文件(reclaude 等)
98
-
99
- 默认自动查找 `claude`(`~/.local/npm-global/bin` → `~/.local/bin` → PATH,都没有则用 SDK 自带二进制)。要换成 [reclaude](https://docs.reclaude.ai) 这类"参数原样透传给 claude"的包装器,在 `config.toml` 显式指定:
100
-
101
- ```toml
102
- [claude]
103
- bin = "~/.local/bin/reclaude"
104
- ```
105
-
106
- 配置后跳过自动查找;路径不存在会在会话启动时直接报错,不会静默回退。日志里 `executable=config:<路径>` 可确认生效。
107
-
108
- 迁移到 reclaude 时注意:`[claude.env]` 或 `~/.claude/settings.json` 里遗留的 GLM `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` 必须清掉 —— base URL 指向 GLM 时流量不经官方域名,reclaude 的拦截不会生效,烧的还是 GLM 额度。`[claude.models.*]` 里的 GLM profile 也需换回官方模型档位。
109
-
110
110
  ### 🔔 HTTP 通知端点
111
111
 
112
- 本机任何脚本一行 curl 就能往群里推一张 markdown 卡片(info / warn / error 三档染色):
112
+ 本机任意脚本一行 curl 就能往群里推一张 markdown 卡片:`info` / `warn` / `error` 三档染色,正文支持飞书 markdown,还能附本地图片、加交互按钮、把点击结果 POST 回你自己的回调。这条能力对应的 skill,daemon 每次启动会自动装进 `~/.claude/skills/` 和 `~/.codex/skills/`(不用自己放文件),完整字段、按钮和回调协议看 [`feishu-notify` skill](src/notify-skill.ts)。
113
113
 
114
- ```bash
115
- curl -sS -X POST http://127.0.0.1:9876/notify \
116
- -H 'Content-Type: application/json' \
117
- -d '{"project":"xxx","text":"build done","level":"info"}'
118
- ```
119
-
120
- `/notify` 还支持可选的 `images` 字段,传本地图片绝对路径,夜航星上传到飞书后渲染在正文之前(上传失败会在卡片里显式标红,不会静默丢):
114
+ 推一条带图的告警:
121
115
 
122
116
  ```bash
123
117
  curl -sS -X POST http://127.0.0.1:9876/notify \
124
118
  -H 'Content-Type: application/json' \
125
- -d '{"project":"xxx","level":"warn","text":"卡点截图如下","images":["/abs/shot.png"]}'
119
+ -d '{"project":"ops","level":"error","text":"卡点了,截图如下","images":["/abs/shot.png"]}'
126
120
  ```
127
121
 
128
- `/notify` 还支持**交互按钮**:带上 `buttons`(最多 5 个)和一个本机 `callback` URL,群里点按钮后夜航星会把选择 POST 回这个 URL,整条回路都在本机 loopback 上。
122
+ 发一张带按钮的审批卡,点了按钮 daemon 把选择 POST 回你本机的 callback:
129
123
 
130
124
  ```bash
131
125
  curl -sS -X POST http://127.0.0.1:9876/notify \
132
126
  -H 'Content-Type: application/json' \
133
- -d '{"project":"ops","text":"deploy ready — approve?",
127
+ -d '{"project":"ops","text":"deploy ready — 审批?",
134
128
  "buttons":[
135
129
  {"id":"approve","text":"✅ 通过","type":"primary"},
136
130
  {"id":"reject","text":"❌ 拒绝","type":"danger"}
137
131
  ],
138
132
  "callback":"http://127.0.0.1:9999/hook"}'
139
- # → 200 {"ok":true,"chat_id":"oc_…","message_id":"om_…","notify_id":"nf_…"}
140
133
  ```
141
134
 
142
- 有人在飞书点了按钮,daemon 就向 `callback` POST 这样一个 JSON,你的本机服务 2xx 应答即可。卡片是**两段式**反馈:点击瞬间立刻冻结成「⏳ 已选择:X · 推送中…」,推送送达后再刷成「✅ … · 反馈已送达」(失败则「⚠️ … · 回调失败:原因」,可再点重试)。调用方还可以在 2xx **响应体**里回一段文本(JSON `{"text":"…"}` 或纯文本,≤500 字,支持飞书 markdown),daemon 会把它显示在最终卡片「反馈已送达」下面那一行 —— 比如回 `{"text":"已发布 v1.2.3,提交 abc123"}`:
135
+ ---
136
+
137
+ ## ⚙️ 配置参考
138
+
139
+ 配置在 `~/.config/lodestar/config.toml`,`lodestar-setup` 会帮你生成,手改也行。两个常被问的:
140
+
141
+ ### 想让某个外部项目跑在别的目录?
142
+
143
+ 默认群名就是 `projects_root` 下的目录名。但如果你的项目放在别处、不想搬进来,在 config.toml 指一下它的目录就行,其它项目不受影响:
143
144
 
144
- ```json
145
- {"notify_id":"nf_…","message_id":"om_…","chat_id":"oc_…","project":"ops",
146
- "button":{"id":"approve","text":"✅ 通过","type":"primary"},
147
- "operator":{"open_id":"ou_…"},"timestamp":1700000000}
145
+ ```toml
146
+ [projects.calculator2]
147
+ cwd = "/abs/path/to/calculator2"
148
148
  ```
149
149
 
150
- 约束:`button.id` 需匹配 `^[A-Za-z0-9_-]{1,64}$` 且不重复;`button.text` ≤ 64 字(每个按钮独占一整行,放短句也行);`type` 为 `default`/`primary`/`danger`;按钮数量不限,有几个就往下排几行;`callback` 可选 —— 给了就是 push(必须是 `http://` 且 host 为 `127.0.0.1`/`localhost`/`::1`,否则 400;回调需在 ~2.5s 内 2xx,否则点选显示"回调失败"并保持可重试)。**不给 `callback` 就走 pull**:daemon 照样把卡片冻结在已选项上并记下结果,调用方拿 `notify_id` 去轮询:
150
+ 下面几个开关是给"想在这个项目里跑个更受限的 Claude session"的人准备的(限定工具、只挂项目自己的 MCP、只读项目级配置之类)。普通用法用不着,默认全不开,也只对 Claude 后端有效。**要开就整节配齐,配一半对话会卡死**:
151
151
 
152
- ```bash
153
- curl -sS http://127.0.0.1:9876/notify/result/<notify_id>
154
- # 未点 → {"notify_id":"nf_…","project":"ops","message_id":"om_…","resolved":false}
155
- # 点后 → {"notify_id":"nf_…",...,"resolved":true,
156
- # "button":{"id":"approve","text":"✅ 通过","type":"primary"},
157
- # "resolved_at":1783242381786,"resolved_by":"ou_…"}
152
+ ```toml
153
+ [projects.calculator2]
154
+ cwd = "/abs/path/to/calculator2"
155
+ setting_sources = "project" # 只读项目级配置(会丢全局)
156
+ strict_mcp = "true" # 只挂项目 .mcp.json,挡掉全局 MCP
157
+ tools = "Read,Write,Edit,Bash,Glob,Grep"
158
+ load_project_mcp = "true" # 读 <cwd>/.mcp.json
158
159
  ```
159
160
 
160
- 绑定持久化在 `~/.local/share/lodestar/notify-callbacks.json`,daemon 重启不丢,7 天后自动清理。
161
+ > 最常踩的坑:`setting_sources = "project"` 会把 `~/.claude/settings.json` 里的 GLM 路由一起丢掉 —— 走 project 模式时,GLM 路由得改落在 config.toml 的 `[claude.env]` 里。要是看到卡片一直 `Thinking...`、model 显示 `<synthetic>`,先把整节注释掉重启,基本就是这几个开关没配齐。
161
162
 
162
- ### 🧩 项目级隔离配置(外部项目接入)
163
+ ### 想换成 reclaude 之类的 claude 包装器?
163
164
 
164
- 默认每个飞书群对应 `projects_root` 下同名目录,跑 Claude Code 默认工具集。当一个外部项目(不在 `projects_root` 下、且需要干净隔离的 agent)想接入时,在 `~/.config/lodestar/config.toml` 加一个 `[projects.<群名>]` 节即可 —— **未配置的项目行为完全不变**:
165
+ 默认 lodestar 自己找 `claude`。想让它改用 [reclaude](https://docs.reclaude.ai)(或别的"参数原样透传"的包装器),指定一下路径:
165
166
 
166
167
  ```toml
167
- [projects.calculator2]
168
- cwd = "/abs/path/to/evolving_data/calculator2"
169
- setting_sources = "project"
170
- strict_mcp = "true"
171
- tools = "Read,Write,Edit,Bash,Glob,Grep"
172
- load_project_mcp = "true"
173
- keep_lodestar_instructions = "true"
168
+ [claude]
169
+ bin = "~/.local/bin/reclaude"
174
170
  ```
175
171
 
176
- | 字段 | 作用 | 默认 |
177
- | --- | --- | --- |
178
- | `cwd` | agent 工作目录(绝对路径) | `projects_root/<群名>` |
179
- | `setting_sources` | 显式列表如 `"project"`(只项目级,会丢全局)或 `"user,project"`;不配则**对齐裸 `claude` CLI**,读项目 `CLAUDE.md`/skills/agents/`settings.json` | `user,project,local` |
180
- | `strict_mcp` | 只挂下方项目 MCP,忽略全局 MCP | 关 |
181
- | `tools` | 允许的内置工具(逗号分隔);MCP 工具由 `load_project_mcp` 自动可用,不用列在这里 | claude_code 全套 |
182
- | `load_project_mcp` | 读取 `<cwd>/.mcp.json` 并挂载其 MCP 服务(对齐裸 `claude` 自动发现;无则无效果) | 开 |
183
- | `keep_lodestar_instructions` | 保留夜航星卡片/输出约定系统提示 | 开 |
184
-
185
- `strict_mcp = "true"` 时,项目 `.mcp.json` 是 agent 能挂上 MCP 的唯一通路 —— 全局插件/技能被全部挡掉,agent 干净专注。典型用法:外部自动化引擎在自己的目录里维护规则文件,夜航星负责飞书通道和卡片渲染,两者通过群消息驱动协作。
186
-
187
- > ⚠️ **这组配置必须完整,配一半会把对话卡死。** `[projects.*]` 的字段是联动的,任一项开启后,它依赖的链路都要一起配齐:
188
- >
189
- > - **`setting_sources = "project"`** 会排除用户级 `~/.claude/settings.json`。如果你的 GLM 路由 / `ANTHROPIC_BASE_URL` 是写在 `~/.claude/settings.json`(而不是 lodestar 的 `[claude.env]`),会被丢掉、请求发不出去 → 卡死。**走 `project` 模式时,GLM 路由必须落在 `config.toml` 的 `[claude.env]`**(`env` 不受 `setting_sources` 影响)。
190
- > - 项目 `<cwd>/.mcp.json` 现在**默认读取**(对齐裸 `claude`)。若它存在,其声明的 MCP server 必须能秒级启动并完成 stdio 握手 —— server 卡住(路径错、二进制不存在、stdio 不响应)会让对话卡在真正调用模型之前,表现是卡片底部一直 `Thinking...` 且 model 显示成 `<synthetic>`。要跳过:显式 `load_project_mcp = "false"`。
191
- > - **排查卡死**:看到 `model=<synthetic>` + 长时间 `Thinking`,先把 `[projects.*]` 整节注释掉重启 daemon;不卡了就是 profile 没配齐,按上面两条逐项检查。
172
+ 路径填错会直接报错,不会偷偷回退到自动查找。换成 reclaude 的话,记得把 `~/.claude/settings.json` `[claude.env]` 里残留的 GLM 地址 / Token 清掉,否则流量还走 GLM、reclaude 的拦截不生效。更多细节看 `docs/claude-agent-backend.md`。
192
173
 
193
174
  ---
194
175
 
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import{execSync as m,spawn as f}from"node:child_process";import{existsSync as y,mkdirSync as u,readFileSync as I,writeFileSync as v}from"node:fs";import{createInterface as h}from"node:readline/promises";import{delimiter as c,dirname as p,join as G}from"node:path";import{homedir as l}from"node:os";import{fileURLToPath as r}from"node:url";import{homedir as j}from"node:os";import{join as Y}from"node:path";var U=j(),R=process.platform==="win32";function w($,q,J){if($)return $;if(q)return Y(q,"lodestar");return J}function g(){if(R)return Y(process.env.APPDATA??Y(U,"AppData","Roaming"),"Lodestar");return Y(U,".config","lodestar")}function D(){if(R)return Y(process.env.LOCALAPPDATA??Y(U,"AppData","Local"),"Lodestar");return Y(U,".local","share","lodestar")}var O=w(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,g()),X=w(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,D()),H=process.env.LODESTAR_CONFIG??Y(O,"config.toml"),zz=Y(X,"daemon.pid"),$z=Y(X,"daemon.log"),qz=Y(X,"session-chat-map.json"),Jz=Y(X,"session-resume-map.json"),Qz=Y(X,"session-model-map.json"),Yz=Y(X,"tasklist-map.json"),Zz=Y(X,"notify-callbacks.json"),Xz=Y(X,"alive-on-shutdown.json"),bz=Y(X,"inbox"),Wz=Y(X,"debug.sock"),Kz=Y(X,"debug-context.json");var z={reset:"\x1B[0m",bold:"\x1B[1m",dim:"\x1B[2m",green:"\x1B[32m",yellow:"\x1B[33m",cyan:"\x1B[36m",red:"\x1B[31m"},M=h({input:process.stdin,output:process.stdout});async function V($,q={}){while(!0){let J=q.default?`${z.dim} [${q.default}]${z.reset}`:"",Q=(await M.question(`${z.cyan}? ${z.reset}${$}${J}
2
+ import{execSync as D,spawn as f}from"node:child_process";import{existsSync as y,mkdirSync as u,readFileSync as I,writeFileSync as v}from"node:fs";import{createInterface as h}from"node:readline/promises";import{delimiter as c,dirname as p,join as G}from"node:path";import{homedir as l}from"node:os";import{fileURLToPath as r}from"node:url";import{homedir as g}from"node:os";import{join as Y}from"node:path";var U=g(),w=process.platform==="win32";function R($,q,J){if($)return $;if(q)return Y(q,"lodestar");return J}function j(){if(w)return Y(process.env.APPDATA??Y(U,"AppData","Roaming"),"Lodestar");return Y(U,".config","lodestar")}function m(){if(w)return Y(process.env.LOCALAPPDATA??Y(U,"AppData","Local"),"Lodestar");return Y(U,".local","share","lodestar")}var O=R(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,j()),X=R(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,m()),H=process.env.LODESTAR_CONFIG??Y(O,"config.toml"),zz=Y(X,"daemon.pid"),$z=Y(X,"daemon.log"),qz=Y(X,"session-chat-map.json"),Jz=Y(X,"session-resume-map.json"),Qz=Y(X,"session-turns-map.json"),Yz=Y(X,"session-model-map.json"),Zz=Y(X,"tasklist-map.json"),Xz=Y(X,"notify-callbacks.json"),bz=Y(X,"alive-on-shutdown.json"),Wz=Y(X,"inbox"),Kz=Y(X,"debug.sock"),Vz=Y(X,"debug-context.json");var z={reset:"\x1B[0m",bold:"\x1B[1m",dim:"\x1B[2m",green:"\x1B[32m",yellow:"\x1B[33m",cyan:"\x1B[36m",red:"\x1B[31m"},M=h({input:process.stdin,output:process.stdout});async function V($,q={}){while(!0){let J=q.default?`${z.dim} [${q.default}]${z.reset}`:"",Q=(await M.question(`${z.cyan}? ${z.reset}${$}${J}
3
3
  ${z.green}>${z.reset} `)).trim();if(!Q&&q.default!==void 0)return q.default;if(!Q&&q.required){console.log(`${z.red}必填,请重新输入${z.reset}`);continue}return Q}}function o($){let q="═".repeat(58);console.log(`
4
4
  ${z.bold}${z.cyan}╔${q}╗${z.reset}`),console.log(`${z.bold}${z.cyan}║ ${$.padEnd(54)} ║${z.reset}`),console.log(`${z.bold}${z.cyan}╚${q}╝${z.reset}
5
5
  `)}function N($,q,J){console.log(`
6
6
  ${z.bold}${z.yellow}[${$}/${q}] ${J}${z.reset}
7
- `)}function k($){return $.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}function L($){let q=process.env.PATH??"";if(!q)return null;let J=process.platform==="win32"?[`${$}.cmd`,`${$}.bat`,`${$}.exe`,$]:[$];for(let Q of q.split(c)){if(!Q)continue;for(let b of J){let W=G(Q,b);if(y(W))return W}}return null}function S($){let q=process.platform==="win32"?"npm.cmd":"npm";return new Promise((J)=>{let Q=f(q,["install","-g",`${$}@latest`],{stdio:"inherit",shell:process.platform==="win32"});Q.on("exit",(b)=>J(b===0)),Q.on("error",()=>J(!1))})}function T($){try{let q=m(`"${$}" login status 2>&1`,{timeout:1e4}).toString();return/Logged in using ChatGPT/i.test(q)}catch{return!1}}async function d($){return new Promise((q)=>{let J=f($,["login"],{stdio:"inherit",shell:process.platform==="win32"});J.on("exit",(Q)=>q(Q===0)),J.on("error",()=>q(!1))})}function a(){return process.env.CLAUDE_CONFIG_DIR||G(l(),".claude")}function s($){try{let q=a();u(q,{recursive:!0});let J=G(q,"settings.json"),Q={};if(y(J)){let P=I(J,"utf8");try{let K=JSON.parse(P);if(!K||typeof K!=="object"||Array.isArray(K))return{error:`${J} 内容不是 JSON 对象, 拒绝覆盖, 请手动检查`};Q=K}catch{return{error:`${J} 解析失败 (JSON 语法错), 拒绝覆盖, 请手动修复后重跑`}}}let b=Q.env&&typeof Q.env==="object"&&!Array.isArray(Q.env)?Q.env:{},W={ANTHROPIC_AUTH_TOKEN:$,ANTHROPIC_BASE_URL:"https://open.bigmodel.cn/api/anthropic",API_TIMEOUT_MS:"3000000",CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:"1",ANTHROPIC_DEFAULT_OPUS_MODEL:"GLM-5.2[1m]",ANTHROPIC_DEFAULT_SONNET_MODEL:"GLM-5.2[1m]",ANTHROPIC_DEFAULT_HAIKU_MODEL:"GLM-4.7"};return Q.env={...b,...W},v(J,JSON.stringify(Q,null,2)+`
7
+ `)}function k($){return $.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}function L($){let q=process.env.PATH??"";if(!q)return null;let J=process.platform==="win32"?[`${$}.cmd`,`${$}.bat`,`${$}.exe`,$]:[$];for(let Q of q.split(c)){if(!Q)continue;for(let b of J){let W=G(Q,b);if(y(W))return W}}return null}function S($){let q=process.platform==="win32"?"npm.cmd":"npm";return new Promise((J)=>{let Q=f(q,["install","-g",`${$}@latest`],{stdio:"inherit",shell:process.platform==="win32"});Q.on("exit",(b)=>J(b===0)),Q.on("error",()=>J(!1))})}function T($){try{let q=D(`"${$}" login status 2>&1`,{timeout:1e4}).toString();return/Logged in using ChatGPT/i.test(q)}catch{return!1}}async function d($){return new Promise((q)=>{let J=f($,["login"],{stdio:"inherit",shell:process.platform==="win32"});J.on("exit",(Q)=>q(Q===0)),J.on("error",()=>q(!1))})}function a(){return process.env.CLAUDE_CONFIG_DIR||G(l(),".claude")}function s($){try{let q=a();u(q,{recursive:!0});let J=G(q,"settings.json"),Q={};if(y(J)){let P=I(J,"utf8");try{let K=JSON.parse(P);if(!K||typeof K!=="object"||Array.isArray(K))return{error:`${J} 内容不是 JSON 对象, 拒绝覆盖, 请手动检查`};Q=K}catch{return{error:`${J} 解析失败 (JSON 语法错), 拒绝覆盖, 请手动修复后重跑`}}}let b=Q.env&&typeof Q.env==="object"&&!Array.isArray(Q.env)?Q.env:{},W={ANTHROPIC_AUTH_TOKEN:$,ANTHROPIC_BASE_URL:"https://open.bigmodel.cn/api/anthropic",API_TIMEOUT_MS:"3000000",CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:"1",ANTHROPIC_DEFAULT_OPUS_MODEL:"GLM-5.2[1m]",ANTHROPIC_DEFAULT_SONNET_MODEL:"GLM-5.2[1m]",ANTHROPIC_DEFAULT_HAIKU_MODEL:"GLM-4.7"};return Q.env={...b,...W},v(J,JSON.stringify(Q,null,2)+`
8
8
  `,{mode:384}),{path:J}}catch(q){return{error:q?.message??String(q)}}}function t($){try{if(process.platform==="win32")f(process.env.ComSpec??"cmd.exe",["/c","start",'""',$],{detached:!0,stdio:"ignore",windowsHide:!0}).unref();else if(process.platform==="darwin")f("open",[$],{detached:!0,stdio:"ignore"}).unref();else f("xdg-open",[$],{detached:!0,stdio:"ignore"}).unref()}catch{}}async function i($,q){try{let Q=await(await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({app_id:$,app_secret:q})})).json();if(Q.tenant_access_token)return{ok:!0};return{ok:!1,error:`飞书拒绝: code=${Q.code} msg=${Q.msg??"(no msg)"}`}}catch(J){return{ok:!1,error:`网络错误: ${J?.message??String(J)}`}}}function n(){try{let $=p(r(import.meta.url)),q=G($,"lodestar.js");if(!y(q))return{error:`找不到 daemon bundle: ${q}`};let J=f(process.execPath,[q],{detached:!0,stdio:"ignore",windowsHide:!0});return J.unref(),{pid:J.pid}}catch($){return{error:$?.message??String($)}}}async function B(){if(!process.stdin.isTTY)console.error(`${z.red}lodestar-setup: stdin 不是 TTY,无法交互式输入。${z.reset}`),console.error("请直接在 cmd / PowerShell / Terminal 里跑,不要 pipe 或重定向 stdin。"),process.exit(1);if(y(H)){if(console.log(`${z.yellow}发现已有配置: ${H}${z.reset}`),(await V("覆盖? (y/N)",{default:"n"})).toLowerCase()!=="y"){console.log("已取消"),M.close();return}}o("Lodestar 安装向导"),console.log("Lodestar 把 Feishu (飞书) 群聊接到 AI agent。"),console.log("默认后端是 Claude Code; Codex 是可选第二后端, 群里发 model 一键切换。"),console.log("每个群对应一个项目目录, agent 在那里跑、能读写文件。"),console.log(),console.log(`${z.yellow}⚠ Claude 官方限制:${z.reset}`),console.log(` ${z.bold}Claude 订阅 (Pro/Max OAuth 登录) 不支持本项目${z.reset},`),console.log(" 必须走 API 方式 —— 推荐 GLM Coding Plan, 或自备 Anthropic API key。"),console.log(),console.log("本向导依次做 4 件事:"),console.log(` ${z.dim}1) 确保 Claude Code CLI 已装好${z.reset}`),console.log(` ${z.dim}2) GLM Coding Plan API key (推荐, 可选) + Codex (可选)${z.reset}`),console.log(` ${z.dim}3) Feishu 自建应用 (含权限 / 事件 / 发版 + 凭据测试)${z.reset}`),console.log(` ${z.dim}4) 工作目录, 自动启动 daemon${z.reset}`),console.log(),await M.question(`${z.dim}按 Enter 开始 (Ctrl+C 退出)...${z.reset}`),N(1,4,"准备 Claude Code CLI");let $=L("claude");if($)console.log(`${z.green}✓ claude CLI 已就位${z.reset}: ${z.dim}${$}${z.reset}`);else{if(console.log(`${z.yellow}未在 PATH 找到 claude CLI, 自动安装...${z.reset}`),console.log(`${z.dim}运行: npm install -g @anthropic-ai/claude-code@latest${z.reset}`),console.log(),!await S("@anthropic-ai/claude-code"))console.error(`
9
- ${z.red}安装失败。${z.reset}`),console.error("请手动运行后再开向导:"),console.error(` ${z.cyan}npm install -g @anthropic-ai/claude-code@latest${z.reset}`),console.error(` ${z.cyan}lodestar-setup${z.reset}`),M.close(),process.exit(1);$=L("claude"),console.log(`${z.green}✓ 安装完成${z.reset}: ${z.dim}${$??"(应该装好了, 但 PATH 找不到 — 重开终端再试)"}${z.reset}`)}console.log(),console.log(`${z.dim}下一步可选配 GLM Coding Plan 自动写入路由; 不配则用本机 Claude Code 现有配置。${z.reset}`),console.log(`${z.dim}记住: 别用 \`claude\` 走订阅 OAuth 登录 —— 订阅不支持本项目, 要用 API key。${z.reset}`),N(2,4,"GLM Coding Plan (推荐, 可选)"),console.log("GLM Coding Plan 给 Claude Code 接 GLM-5.2 (开放 1M token 上下文, 中文友好)。"),console.log("订阅后在智谱开放平台拿一个 API key, 粘到下面 —— 向导自动写进 ~/.claude/settings.json。"),console.log(` ${z.dim}拿 key: https://open.bigmodel.cn → 控制台 → API Keys${z.reset}`),console.log(` ${z.dim}不给也行: 以本机 Claude Code 现有配置启动 (确保是 API key 方式, 非订阅)。${z.reset}`),console.log();let q=await V("GLM API key (直接回车跳过)",{});if(q){let Z=s(q);if("path"in Z)console.log(`${z.green}✓ GLM 路由已写入${z.reset}: ${z.dim}${Z.path}${z.reset}`),console.log(`${z.dim}opus/sonnet → GLM-5.2[1m] (1M ctx), haiku → GLM-4.7${z.reset}`);else console.log(`${z.red}✗ 写入失败:${z.reset} ${Z.error}`),console.log(`${z.dim}跳过 GLM 配置, 以本机 Claude Code 现有配置启动。${z.reset}`)}else console.log(`${z.dim}已跳过 GLM, 以本机 Claude Code 现有配置启动。${z.reset}`);if(console.log(),console.log(`${z.bold}Codex (可选第二后端)${z.reset}`),console.log(` ${z.dim}想用 Codex·GPT-5.5 的话, 登录 ChatGPT 订阅即可, 群里发 model 切换。${z.reset}`),(await V("现在顺便配置 Codex 后端吗?",{default:"n"})).toLowerCase()==="y"){let Z=L("codex");if(!Z){if(console.log(`${z.dim}未找到 codex CLI, 安装 @openai/codex...${z.reset}`),await S("@openai/codex"))Z=L("codex")}if(Z&&T(Z))console.log(`${z.green}✓ Codex 已登录 ChatGPT${z.reset}`);else if(Z)if(console.log(`${z.dim}启动 \`codex login\`...${z.reset}`),await d(Z)&&T(Z))console.log(`${z.green}✓ Codex 已登录 ChatGPT${z.reset}`);else console.log(`${z.yellow}Codex 登录未完成 — 不影响默认 Claude 后端; 需要时随时跑 codex login。${z.reset}`);else console.log(`${z.yellow}Codex 安装失败 — 不影响默认 Claude 后端; 需要时手动 npm i -g @openai/codex。${z.reset}`)}else console.log(`${z.dim}已跳过 Codex (需要时随时跑 codex login, 群里发 model 切)。${z.reset}`);N(3,4,"Feishu 自建应用");let Q="https://open.feishu.cn/app";console.log("打开飞书开放平台 (浏览器):"),console.log(` ${z.cyan}${Q}${z.reset}`),t(Q),console.log(`${z.dim}(如果浏览器没自动开, 复制上面 URL 粘到浏览器)${z.reset}`),console.log(),console.log(`${z.bold}详细操作步骤:${z.reset}`),console.log(),console.log(` ${z.bold}① 创建应用${z.reset}`),console.log(` 点 "创建企业自建应用", 填名字 (如 ${z.dim}Lodestar${z.reset}), logo 随意。`),console.log(),console.log(` ${z.bold}② 添加机器人能力${z.reset}`),console.log(` 左侧菜单 "${z.cyan}添加应用能力${z.reset}" → 找到 "机器人" → 点 "${z.bold}添加${z.reset}" 按钮。`),console.log(),console.log(` ${z.bold}③ 申请权限 (左侧 "${z.cyan}权限管理${z.reset}" → "${z.bold}开通权限${z.reset}")${z.reset}`),console.log(` ${z.yellow}缺一个都会让 daemon 启动后默默丢消息, 一定要全开。${z.reset}`),console.log(` ${z.dim}消息类:${z.reset}`),console.log(` • ${z.bold}im:message:send_as_bot${z.reset} ${z.dim}# 以机器人身份发消息${z.reset}`),console.log(` • ${z.bold}im:message${z.reset} ${z.dim}# 接收/操作消息 (核心)${z.reset}`),console.log(` • ${z.bold}im:chat${z.reset} ${z.dim}# 读/写群信息 (匹配群名 ↔ 项目目录)${z.reset}`),console.log(` • ${z.bold}im:chat:create${z.reset} ${z.dim}# wt: 自动创建 worktree 群${z.reset}`),console.log(` • ${z.bold}im:chat:delete${z.reset} ${z.dim}# wt: 解散 worktree 群${z.reset}`),console.log(` • ${z.bold}im:chat.members:read${z.reset} ${z.dim}# wt: 判断发起人是否已在群内${z.reset}`),console.log(` • ${z.bold}im:chat.members:write_only${z.reset} ${z.dim}# wt: 把发起人拉进已有 worktree 群${z.reset}`),console.log(` • ${z.bold}im:resource${z.reset} ${z.dim}# 上传/下载附件 (图文双向)${z.reset}`),console.log(` • ${z.bold}im:message.urgent${z.reset} ${z.dim}# 加急推送 (锁屏通知 / Ask)${z.reset}`),console.log(` • ${z.bold}im:message.group_msg${z.reset} ${z.dim}# 敏感: 接收群里所有消息${z.reset}`),console.log(` ${z.dim}└ 关键: 没它机器人只收 @ 自己的消息, 拿不到群里其他对话, 一定要开${z.reset}`),console.log(` ${z.dim}└ 敏感权限要走审批: 申请时填用途, 个人开发者通常秒过${z.reset}`),console.log(` • ${z.bold}im:message.group_at_msg:readonly${z.reset} ${z.dim}# 读 @ 机器人消息 (兜底)${z.reset}`),console.log(` ${z.dim}卡片类 (Card Kit):${z.reset}`),console.log(` • ${z.bold}cardkit:card:read${z.reset} ${z.dim}# 读卡片状态${z.reset}`),console.log(` • ${z.bold}cardkit:card:write${z.reset} ${z.dim}# 创建/更新卡片 (流式渲染核心)${z.reset}`),console.log(),console.log(` ${z.bold}④ 订阅事件 (左侧 "${z.cyan}事件与回调${z.reset}", 拆两个子页:)${z.reset}`),console.log(` ${z.dim}a)${z.reset} ${z.bold}事件配置${z.reset} 页:`),console.log(` ${z.yellow}• "订阅方式" → 选 "长连接" → 点保存${z.reset}`),console.log(` • 添加事件: ${z.bold}im.message.receive_v1${z.reset} ${z.dim}# 收群消息${z.reset}`),console.log(` ${z.dim}b)${z.reset} ${z.bold}回调配置${z.reset} 页:`),console.log(` ${z.yellow}• "订阅方式" → 选 "长连接" → 点保存${z.reset}`),console.log(` • 添加事件: ${z.bold}card.action.trigger${z.reset} ${z.dim}# 卡片按钮点击回调${z.reset}`),console.log(),console.log(` ${z.bold}⑤ 发布版本${z.reset}`),console.log(` 页面顶部 "${z.bold}创建版本${z.reset}" → 滚到底点 "${z.bold}保存${z.reset}" → 弹框点 "${z.bold}发布${z.reset}"。`),console.log(` ${z.yellow}没发版的应用收不到任何事件 — 这步九成新手会忘!${z.reset}`),console.log(),console.log(` ${z.bold}⑥ 拿凭据${z.reset}`),console.log(` 左侧 "凭证与基础信息" → 顶部 ${z.bold}App ID${z.reset} (${z.dim}cli_...${z.reset}) 和 ${z.bold}App Secret${z.reset}, 待会粘到下面。`),console.log(),console.log(` ${z.bold}⑦ 把机器人拉进群${z.reset}`),console.log(" 想用的飞书群 → 群设置 → 群机器人 → 添加机器人 → 选你的应用。"),console.log(` ${z.yellow}群名要等于 projects_root 下的项目目录名${z.reset} (下一步设, 默认是用户主目录)。`),console.log();let b="",W="";while(!0){b=await V("App ID (以 cli_ 开头)",{required:!0}),W=await V("App Secret",{required:!0}),console.log(`${z.dim}测试中... (调 tenant_access_token endpoint)${z.reset}`);let Z=await i(b,W);if(Z.ok){console.log(`${z.green}✓ Feishu 凭据测试通过${z.reset}`);break}if(console.log(`${z.red}✗ 测试失败:${z.reset} ${Z.error}`),console.log(`${z.dim}最常见原因: app_id / app_secret 抄错, 或应用还没 "发布上线" (步骤 ⑤)。${z.reset}`),console.log(),(await V("重新填? (Y/n)",{default:"y"})).toLowerCase()==="n")console.log(`${z.yellow}已取消, 配置未写盘。${z.reset}`),M.close(),process.exit(1)}N(4,4,"工作目录 + 启动"),console.log("每个 Feishu 群对应 projects_root 下同名的目录。"),console.log();let P=process.platform==="win32"?process.env.USERPROFILE??"C:\\Users\\Default":process.env.HOME??"/root",K=await V("projects_root",{default:P});u(O,{recursive:!0});let _=["# Lodestar config — generated by `lodestar-setup`","# Edit by hand or re-run setup to overwrite.","","[feishu]",`app_id = "${k(b)}"`,`app_secret = "${k(W)}"`,"","[runtime]",`projects_root = "${k(K)}"`,""];v(H,_.join(`
9
+ ${z.red}安装失败。${z.reset}`),console.error("请手动运行后再开向导:"),console.error(` ${z.cyan}npm install -g @anthropic-ai/claude-code@latest${z.reset}`),console.error(` ${z.cyan}lodestar-setup${z.reset}`),M.close(),process.exit(1);$=L("claude"),console.log(`${z.green}✓ 安装完成${z.reset}: ${z.dim}${$??"(应该装好了, 但 PATH 找不到 — 重开终端再试)"}${z.reset}`)}console.log(),console.log(`${z.dim}下一步可选配 GLM Coding Plan 自动写入路由; 不配则用本机 Claude Code 现有配置。${z.reset}`),console.log(`${z.dim}记住: 别用 \`claude\` 走订阅 OAuth 登录 —— 订阅不支持本项目, 要用 API key。${z.reset}`),N(2,4,"GLM Coding Plan (推荐, 可选)"),console.log("GLM Coding Plan 给 Claude Code 接 GLM-5.2 (开放 1M token 上下文, 中文友好)。"),console.log("订阅后在智谱开放平台拿一个 API key, 粘到下面 —— 向导自动写进 ~/.claude/settings.json。"),console.log(` ${z.dim}拿 key: https://open.bigmodel.cn → 控制台 → API Keys${z.reset}`),console.log(` ${z.dim}不给也行: 以本机 Claude Code 现有配置启动 (确保是 API key 方式, 非订阅)。${z.reset}`),console.log();let q=await V("GLM API key (直接回车跳过)",{});if(q){let Z=s(q);if("path"in Z)console.log(`${z.green}✓ GLM 路由已写入${z.reset}: ${z.dim}${Z.path}${z.reset}`),console.log(`${z.dim}opus/sonnet → GLM-5.2[1m] (1M ctx), haiku → GLM-4.7${z.reset}`);else console.log(`${z.red}✗ 写入失败:${z.reset} ${Z.error}`),console.log(`${z.dim}跳过 GLM 配置, 以本机 Claude Code 现有配置启动。${z.reset}`)}else console.log(`${z.dim}已跳过 GLM, 以本机 Claude Code 现有配置启动。${z.reset}`);if(console.log(),console.log(`${z.bold}Codex (可选第二后端)${z.reset}`),console.log(` ${z.dim}想用 Codex·GPT-5.5 的话, 登录 ChatGPT 订阅即可, 群里发 model 切换。${z.reset}`),(await V("现在顺便配置 Codex 后端吗?",{default:"n"})).toLowerCase()==="y"){let Z=L("codex");if(!Z){if(console.log(`${z.dim}未找到 codex CLI, 安装 @openai/codex...${z.reset}`),await S("@openai/codex"))Z=L("codex")}if(Z&&T(Z))console.log(`${z.green}✓ Codex 已登录 ChatGPT${z.reset}`);else if(Z)if(console.log(`${z.dim}启动 \`codex login\`...${z.reset}`),await d(Z)&&T(Z))console.log(`${z.green}✓ Codex 已登录 ChatGPT${z.reset}`);else console.log(`${z.yellow}Codex 登录未完成 — 不影响默认 Claude 后端; 需要时随时跑 codex login。${z.reset}`);else console.log(`${z.yellow}Codex 安装失败 — 不影响默认 Claude 后端; 需要时手动 npm i -g @openai/codex。${z.reset}`)}else console.log(`${z.dim}已跳过 Codex (需要时随时跑 codex login, 群里发 model 切)。${z.reset}`);N(3,4,"Feishu 自建应用");let Q="https://open.feishu.cn/app";console.log("打开飞书开放平台 (浏览器):"),console.log(` ${z.cyan}${Q}${z.reset}`),t(Q),console.log(`${z.dim}(如果浏览器没自动开, 复制上面 URL 粘到浏览器)${z.reset}`),console.log(),console.log(`${z.bold}详细操作步骤:${z.reset}`),console.log(),console.log(` ${z.bold}① 创建应用${z.reset}`),console.log(` 点 "创建企业自建应用", 填名字 (如 ${z.dim}Lodestar${z.reset}), logo 随意。`),console.log(),console.log(` ${z.bold}② 添加机器人能力${z.reset}`),console.log(` 左侧菜单 "${z.cyan}添加应用能力${z.reset}" → 找到 "机器人" → 点 "${z.bold}添加${z.reset}" 按钮。`),console.log(),console.log(` ${z.bold}③ 申请权限 (左侧 "${z.cyan}权限管理${z.reset}" → "${z.bold}开通权限${z.reset}")${z.reset}`),console.log(` ${z.yellow}缺一个都会让 daemon 启动后默默丢消息, 一定要全开。${z.reset}`),console.log(` ${z.dim}消息类:${z.reset}`),console.log(` • ${z.bold}im:message:send_as_bot${z.reset} ${z.dim}# 以机器人身份发消息${z.reset}`),console.log(` • ${z.bold}im:message${z.reset} ${z.dim}# 接收/操作消息 (核心)${z.reset}`),console.log(` • ${z.bold}im:chat${z.reset} ${z.dim}# 读/写群信息 (匹配群名 ↔ 项目目录)${z.reset}`),console.log(` • ${z.bold}im:chat:create${z.reset} ${z.dim}# wt: 自动创建 worktree 群${z.reset}`),console.log(` • ${z.bold}im:chat:delete${z.reset} ${z.dim}# wt: 解散 worktree 群${z.reset}`),console.log(` • ${z.bold}im:chat.members:read${z.reset} ${z.dim}# wt: 判断发起人是否已在群内${z.reset}`),console.log(` • ${z.bold}im:chat.members:write_only${z.reset} ${z.dim}# wt: 把发起人拉进已有 worktree 群${z.reset}`),console.log(` • ${z.bold}im:resource${z.reset} ${z.dim}# 上传/下载附件 (图文双向)${z.reset}`),console.log(` • ${z.bold}im:message.urgent${z.reset} ${z.dim}# 加急推送 (锁屏通知 / Ask)${z.reset}`),console.log(` • ${z.bold}im:message.group_msg${z.reset} ${z.dim}# 敏感: 接收群里所有消息${z.reset}`),console.log(` ${z.dim}└ 关键: 没它机器人只收 @ 自己的消息, 拿不到群里其他对话, 一定要开${z.reset}`),console.log(` ${z.dim}└ 敏感权限要走审批: 申请时填用途, 个人开发者通常秒过${z.reset}`),console.log(` • ${z.bold}im:message.group_at_msg:readonly${z.reset} ${z.dim}# 读 @ 机器人消息 (兜底)${z.reset}`),console.log(` ${z.dim}卡片类 (Card Kit):${z.reset}`),console.log(` • ${z.bold}cardkit:card:read${z.reset} ${z.dim}# 读卡片状态${z.reset}`),console.log(` • ${z.bold}cardkit:card:write${z.reset} ${z.dim}# 创建/更新卡片 (流式渲染核心)${z.reset}`),console.log(),console.log(` ${z.bold}④ 订阅事件 (左侧 "${z.cyan}事件与回调${z.reset}", 拆两个子页:)${z.reset}`),console.log(` ${z.dim}a)${z.reset} ${z.bold}事件配置${z.reset} 页:`),console.log(` ${z.yellow}• "订阅方式" → 选 "长连接" → 点保存${z.reset}`),console.log(` • 添加事件: ${z.bold}im.message.receive_v1${z.reset} ${z.dim}# 收群消息${z.reset}`),console.log(` ${z.dim}b)${z.reset} ${z.bold}回调配置${z.reset} 页:`),console.log(` ${z.yellow}• "订阅方式" → 选 "长连接" → 点保存${z.reset}`),console.log(` • 添加事件: ${z.bold}card.action.trigger${z.reset} ${z.dim}# 卡片按钮点击回调${z.reset}`),console.log(),console.log(` ${z.bold}⑤ 发布版本${z.reset}`),console.log(` 页面顶部 "${z.bold}创建版本${z.reset}" → 滚到底点 "${z.bold}保存${z.reset}" → 弹框点 "${z.bold}发布${z.reset}"。`),console.log(` ${z.yellow}没发版的应用收不到任何事件 — 这步九成新手会忘!${z.reset}`),console.log(),console.log(` ${z.bold}⑥ 拿凭据${z.reset}`),console.log(` 左侧 "凭证与基础信息" → 顶部 ${z.bold}App ID${z.reset} (${z.dim}cli_...${z.reset}) 和 ${z.bold}App Secret${z.reset}, 待会粘到下面。`),console.log(),console.log(` ${z.bold}⑦ 把机器人拉进群${z.reset}`),console.log(" 想用的飞书群 → 群设置 → 群机器人 → 添加机器人 → 选你的应用。"),console.log(` ${z.yellow}群名要等于 projects_root 下的项目目录名${z.reset} (下一步设, 默认是用户主目录)。`),console.log();let b="",W="";while(!0){b=await V("App ID (以 cli_ 开头)",{required:!0}),W=await V("App Secret",{required:!0}),console.log(`${z.dim}测试中... (调 tenant_access_token endpoint)${z.reset}`);let Z=await i(b,W);if(Z.ok){console.log(`${z.green}✓ Feishu 凭据测试通过${z.reset}`);break}if(console.log(`${z.red}✗ 测试失败:${z.reset} ${Z.error}`),console.log(`${z.dim}最常见原因: app_id / app_secret 抄错, 或应用还没 "发布上线" (步骤 ⑤)。${z.reset}`),console.log(),(await V("重新填? (Y/n)",{default:"y"})).toLowerCase()==="n")console.log(`${z.yellow}已取消, 配置未写盘。${z.reset}`),M.close(),process.exit(1)}N(4,4,"工作目录 + 启动"),console.log("每个 Feishu 群对应 projects_root 下同名的目录。"),console.log();let P=process.platform==="win32"?process.env.USERPROFILE??"C:\\Users\\Default":process.env.HOME??"/root",K=await V("projects_root",{default:P});u(O,{recursive:!0});let x=["# Lodestar config — generated by `lodestar-setup`","# Edit by hand or re-run setup to overwrite.","","[feishu]",`app_id = "${k(b)}"`,`app_secret = "${k(W)}"`,"","[runtime]",`projects_root = "${k(K)}"`,""];v(H,x.join(`
10
10
  `),{mode:384}),console.log(`
11
11
  ${z.green}${z.bold}✓ 配置已写入${z.reset}`),console.log(` ${z.cyan}${H}${z.reset}`),console.log(`
12
- ${z.bold}启动 daemon...${z.reset}`);let E=n(),x=process.platform==="win32"?"\\":"/",A=process.platform==="win32"?`${process.env.LOCALAPPDATA??"%LOCALAPPDATA%"}\\Lodestar\\daemon-YYYY-MM-DD.log`:`${process.env.HOME??"~"}/.local/share/lodestar/daemon-YYYY-MM-DD.log`;if(E.pid)console.log(`${z.green}✓ daemon 已在后台启动${z.reset} (pid ${E.pid})`),console.log(),console.log(`${z.bold}最后一步: 在 Feishu 验证${z.reset}`),console.log(" ① 把机器人拉进任意飞书群"),console.log(` ② 群名 = ${z.cyan}${K}${x}<群名>${z.reset} 下的目录名 (新群第一条消息会自动建)`),console.log(" ③ 在群里发任意一条消息, 默认由 Claude 接管"),console.log(` ${z.dim}(群里发 model 可切到 Codex·GPT-5.5)${z.reset}`),console.log(),console.log("日志 (按日滚动, 保留近 7 天):"),console.log(` ${z.cyan}${A}${z.reset}`),console.log(),console.log(`${z.dim}若长期跑后台, 参考 README "7×24 守护" 一节配 systemd / Windows 后台托管。${z.reset}`);else console.log(`${z.yellow}启动失败: ${E.error}${z.reset}`),console.log(`手动运行: ${z.cyan}lodestar-daemon${z.reset}`);console.log(),M.close()}B().catch(($)=>{console.error(`
12
+ ${z.bold}启动 daemon...${z.reset}`);let E=n(),_=process.platform==="win32"?"\\":"/",A=process.platform==="win32"?`${process.env.LOCALAPPDATA??"%LOCALAPPDATA%"}\\Lodestar\\daemon-YYYY-MM-DD.log`:`${process.env.HOME??"~"}/.local/share/lodestar/daemon-YYYY-MM-DD.log`;if(E.pid)console.log(`${z.green}✓ daemon 已在后台启动${z.reset} (pid ${E.pid})`),console.log(),console.log(`${z.bold}最后一步: 在 Feishu 验证${z.reset}`),console.log(" ① 把机器人拉进任意飞书群"),console.log(` ② 群名 = ${z.cyan}${K}${_}<群名>${z.reset} 下的目录名 (新群第一条消息会自动建)`),console.log(" ③ 在群里发任意一条消息, 默认由 Claude 接管"),console.log(` ${z.dim}(群里发 model 可切到 Codex·GPT-5.5)${z.reset}`),console.log(),console.log("日志 (按日滚动, 保留近 7 天):"),console.log(` ${z.cyan}${A}${z.reset}`),console.log(),console.log(`${z.dim}若长期跑后台, 参考 README "7×24 守护" 一节配 systemd / Windows 后台托管。${z.reset}`);else console.log(`${z.yellow}启动失败: ${E.error}${z.reset}`),console.log(`手动运行: ${z.cyan}lodestar-daemon${z.reset}`);console.log(),M.close()}B().catch(($)=>{console.error(`
13
13
  lodestar-setup: ${$?.message??$}`),process.exit(1)});
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{execSync as f}from"node:child_process";import{existsSync as U,readFileSync as y}from"node:fs";import{homedir as W}from"node:os";import{join as z}from"node:path";var X=W(),B=process.platform==="win32";function b(J,K,Y){if(J)return J;if(K)return z(K,"lodestar");return Y}function H(){if(B)return z(process.env.APPDATA??z(X,"AppData","Roaming"),"Lodestar");return z(X,".config","lodestar")}function V(){if(B)return z(process.env.LOCALAPPDATA??z(X,"AppData","Local"),"Lodestar");return z(X,".local","share","lodestar")}var w=b(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,H()),Q=b(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,V()),g=process.env.LODESTAR_CONFIG??z(w,"config.toml"),Z=z(Q,"daemon.pid"),x=z(Q,"daemon.log"),S=z(Q,"session-chat-map.json"),T=z(Q,"session-resume-map.json"),E=z(Q,"session-model-map.json"),v=z(Q,"tasklist-map.json"),h=z(Q,"notify-callbacks.json"),k=z(Q,"alive-on-shutdown.json"),F=z(Q,"inbox"),j=z(Q,"debug.sock"),A=z(Q,"debug-context.json");import{execSync as G}from"node:child_process";import{readFileSync as L,writeFileSync as _}from"node:fs";function M(J){try{if(process.platform==="linux")return L(`/proc/${J}/cmdline`,"utf8").replace(/\0/g," ").trim();if(process.platform==="darwin")return G(`ps -p ${J} -o args=`,{encoding:"utf8",timeout:2000}).trim();if(process.platform==="win32")return G(`powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${J}').CommandLine"`,{encoding:"utf8",timeout:5000}).trim()||null;return null}catch{return null}}function N(J,K){if(!Number.isFinite(J)||J<=0||!K)return!1;let Y=M(J);if(!Y)return!1;if(process.platform==="win32")return Y.toLowerCase().includes(K.toLowerCase());return Y.includes(K)}var q={reset:"\x1B[0m",bold:"\x1B[1m",green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m",dim:"\x1B[2m"};function P(J){return new Promise((K)=>setTimeout(K,J))}async function u(){if(!U(Z)){console.log(`${q.yellow}Lodestar daemon 未运行${q.reset} ${q.dim}(${Z} 不存在)${q.reset}`);return}let J=y(Z,"utf8").split(`
2
+ import{execSync as f}from"node:child_process";import{existsSync as U,readFileSync as y}from"node:fs";import{homedir as W}from"node:os";import{join as z}from"node:path";var X=W(),B=process.platform==="win32";function b(J,K,Y){if(J)return J;if(K)return z(K,"lodestar");return Y}function H(){if(B)return z(process.env.APPDATA??z(X,"AppData","Roaming"),"Lodestar");return z(X,".config","lodestar")}function V(){if(B)return z(process.env.LOCALAPPDATA??z(X,"AppData","Local"),"Lodestar");return z(X,".local","share","lodestar")}var w=b(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,H()),Q=b(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,V()),x=process.env.LODESTAR_CONFIG??z(w,"config.toml"),Z=z(Q,"daemon.pid"),R=z(Q,"daemon.log"),S=z(Q,"session-chat-map.json"),E=z(Q,"session-resume-map.json"),T=z(Q,"session-turns-map.json"),v=z(Q,"session-model-map.json"),h=z(Q,"tasklist-map.json"),k=z(Q,"notify-callbacks.json"),F=z(Q,"alive-on-shutdown.json"),j=z(Q,"inbox"),m=z(Q,"debug.sock"),A=z(Q,"debug-context.json");import{execSync as G}from"node:child_process";import{readFileSync as L,writeFileSync as c}from"node:fs";function M(J){try{if(process.platform==="linux")return L(`/proc/${J}/cmdline`,"utf8").replace(/\0/g," ").trim();if(process.platform==="darwin")return G(`ps -p ${J} -o args=`,{encoding:"utf8",timeout:2000}).trim();if(process.platform==="win32")return G(`powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${J}').CommandLine"`,{encoding:"utf8",timeout:5000}).trim()||null;return null}catch{return null}}function N(J,K){if(!Number.isFinite(J)||J<=0||!K)return!1;let Y=M(J);if(!Y)return!1;if(process.platform==="win32")return Y.toLowerCase().includes(K.toLowerCase());return Y.includes(K)}var q={reset:"\x1B[0m",bold:"\x1B[1m",green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m",dim:"\x1B[2m"};function P(J){return new Promise((K)=>setTimeout(K,J))}async function u(){if(!U(Z)){console.log(`${q.yellow}Lodestar daemon 未运行${q.reset} ${q.dim}(${Z} 不存在)${q.reset}`);return}let J=y(Z,"utf8").split(`
3
3
  `),K=parseInt((J[0]??"").trim(),10),Y=(J[1]??"").trim();if(!Number.isFinite(K)||K<=0)console.error(`${q.red}PID 文件格式坏:${q.reset} ${Z}`),process.exit(1);if(Y&&!N(K,Y)){console.log(`${q.yellow}PID ${K} 上没有 daemon (stale 文件)${q.reset}`),console.log(`${q.dim}手删 ${Z} 后再试${q.reset}`);return}console.log(`${q.bold}停止 daemon${q.reset} (pid ${K})...`);try{if(process.platform==="win32")f(`taskkill /PID ${K} /T /F`,{stdio:"ignore"});else process.kill(K,"SIGTERM")}catch($){console.error(`${q.red}发信号失败:${q.reset} ${$?.message??$}`),process.exit(1)}for(let $=0;$<50;$++){if(!U(Z)){console.log(`${q.green}✓ daemon 已停${q.reset}`);return}await P(100)}console.log(`${q.yellow}已发 SIGTERM, 但 5s 内 daemon 没清掉 ${Z}${q.reset}`),console.log(`${q.dim}它可能还在收尾 (落盘 alive marker / 关 WS); 下次启动 pid-guard 会自动判别。${q.reset}`)}u().catch((J)=>{console.error(`${q.red}lodestar-stop:${q.reset} ${J?.message??J}`),process.exit(1)});
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as b}from"node:child_process";import{existsSync as g}from"node:fs";import{homedir as d}from"node:os";import{join as o}from"node:path";var r=d(),a=process.platform==="win32";function n(t,l,m){if(t)return t;if(l)return o(l,"lodestar");return m}function y(){if(a)return o(process.env.APPDATA??o(r,"AppData","Roaming"),"Lodestar");return o(r,".config","lodestar")}function C(){if(a)return o(process.env.LOCALAPPDATA??o(r,"AppData","Local"),"Lodestar");return o(r,".local","share","lodestar")}var u=n(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,y()),e=n(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,C()),K=process.env.LODESTAR_CONFIG??o(u,"config.toml"),$=o(e,"daemon.pid"),f=o(e,"daemon.log"),S=o(e,"session-chat-map.json"),i=o(e,"session-resume-map.json"),q=o(e,"session-model-map.json"),z=o(e,"tasklist-map.json"),J=o(e,"notify-callbacks.json"),Q=o(e,"alive-on-shutdown.json"),Y=o(e,"inbox"),Z=o(e,"debug.sock"),W=o(e,"debug-context.json");var s={reset:"\x1B[0m",bold:"\x1B[1m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m",dim:"\x1B[2m"},p=["@leviyuan/lodestar@latest","@openai/codex@latest","@anthropic-ai/claude-code@latest","@anthropic-ai/claude-agent-sdk@latest","@anthropic-ai/sdk@latest"];function w(){let t=process.platform==="win32"?"npm.cmd":"npm";return new Promise((l)=>{let m=b(t,["install","-g",...p],{stdio:"inherit",shell:process.platform==="win32"});m.on("exit",(c)=>l(c??1)),m.on("error",(c)=>{console.error(`${s.red}spawn npm 失败:${s.reset} ${c.message}`),l(1)})})}async function x(){console.log(`${s.bold}更新 Lodestar + Codex CLI + Claude Code / SDK${s.reset}`),console.log(`${s.dim}npm i -g ${p.join(" ")}${s.reset}
3
- `);let t=await w();if(t!==0)console.error(`
4
- ${s.red}更新失败 (npm exit ${t})${s.reset}`),process.exit(t);if(console.log(`
5
- ${s.green}✓ 更新完成${s.reset}`),g($))console.log(),console.log(`${s.yellow}检测到 daemon 仍在跑老版本进程, 用新版本需要重启:${s.reset}`),console.log(` ${s.dim}# systemd / Windows 后台托管的:${s.reset}`),console.log(` ${s.cyan}systemctl --user restart lodestar${s.reset} ${s.dim}# Linux/macOS${s.reset}`),console.log(` ${s.dim}# 或手动重启:${s.reset}`),console.log(` ${s.cyan}lodestar-stop && lodestar-daemon${s.reset}`)}x().catch((t)=>{console.error(`${s.red}lodestar-update:${s.reset} ${t?.message??t}`),process.exit(1)});
2
+ import{spawn as b}from"node:child_process";import{existsSync as g}from"node:fs";import{homedir as d}from"node:os";import{join as o}from"node:path";var r=d(),a=process.platform==="win32";function $(e,l,m){if(e)return e;if(l)return o(l,"lodestar");return m}function y(){if(a)return o(process.env.APPDATA??o(r,"AppData","Roaming"),"Lodestar");return o(r,".config","lodestar")}function C(){if(a)return o(process.env.LOCALAPPDATA??o(r,"AppData","Local"),"Lodestar");return o(r,".local","share","lodestar")}var u=$(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,y()),t=$(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,C()),K=process.env.LODESTAR_CONFIG??o(u,"config.toml"),n=o(t,"daemon.pid"),f=o(t,"daemon.log"),S=o(t,"session-chat-map.json"),q=o(t,"session-resume-map.json"),z=o(t,"session-turns-map.json"),J=o(t,"session-model-map.json"),Q=o(t,"tasklist-map.json"),Y=o(t,"notify-callbacks.json"),Z=o(t,"alive-on-shutdown.json"),i=o(t,"inbox"),W=o(t,"debug.sock"),X=o(t,"debug-context.json");var s={reset:"\x1B[0m",bold:"\x1B[1m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m",dim:"\x1B[2m"},p=["@leviyuan/lodestar@latest","@openai/codex@latest","@anthropic-ai/claude-code@latest","@anthropic-ai/claude-agent-sdk@latest","@anthropic-ai/sdk@latest"];function w(){let e=process.platform==="win32"?"npm.cmd":"npm";return new Promise((l)=>{let m=b(e,["install","-g",...p],{stdio:"inherit",shell:process.platform==="win32"});m.on("exit",(c)=>l(c??1)),m.on("error",(c)=>{console.error(`${s.red}spawn npm 失败:${s.reset} ${c.message}`),l(1)})})}async function x(){console.log(`${s.bold}更新 Lodestar + Codex CLI + Claude Code / SDK${s.reset}`),console.log(`${s.dim}npm i -g ${p.join(" ")}${s.reset}
3
+ `);let e=await w();if(e!==0)console.error(`
4
+ ${s.red}更新失败 (npm exit ${e})${s.reset}`),process.exit(e);if(console.log(`
5
+ ${s.green}✓ 更新完成${s.reset}`),g(n))console.log(),console.log(`${s.yellow}检测到 daemon 仍在跑老版本进程, 用新版本需要重启:${s.reset}`),console.log(` ${s.dim}# systemd / Windows 后台托管的:${s.reset}`),console.log(` ${s.cyan}systemctl --user restart lodestar${s.reset} ${s.dim}# Linux/macOS${s.reset}`),console.log(` ${s.dim}# 或手动重启:${s.reset}`),console.log(` ${s.cyan}lodestar-stop && lodestar-daemon${s.reset}`)}x().catch((e)=>{console.error(`${s.red}lodestar-update:${s.reset} ${e?.message??e}`),process.exit(1)});