@leviyuan/lodestar 0.17.1 → 0.17.3
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 +28 -176
- package/dist/dsh-bridge.js +30 -11
- package/dist/lodestar-setup.js +16 -11
- package/dist/lodestar-stop.js +2 -2
- package/dist/lodestar-update.js +10 -4
- package/dist/lodestar-version.js +1 -1
- package/dist/lodestar.js +172 -164
- package/docs/AGENTS.md +17 -0
- package/docs/claude-agent-backend.md +121 -0
- package/docs/configuration.md +79 -0
- package/docs/models.md +84 -0
- package/docs/usage.md +77 -0
- package/package.json +238 -245
- package/scripts/postinstall.cjs +2 -26
package/README.md
CHANGED
|
@@ -4,201 +4,53 @@
|
|
|
4
4
|
|
|
5
5
|
# 夜航星 (Lodestar)
|
|
6
6
|
|
|
7
|
-
AI
|
|
7
|
+
AI 不是帮手,是倍率。它放大的不是体力,是你 —— 你的直觉、判断和品味,每一样都被乘以一个你以前不敢想的系数。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
夜航星让这件事真正发生:在你思考的地方接住想法,在你转身之后继续推向终点。
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## 项目特色
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
- **发条消息,就能开工**:在飞书里把任务交给 Codex、Claude Code 或 DeepSeek Harness,让 Agent 在你的项目里读代码、改文件、跑命令。
|
|
16
|
+
- **一个群,一个项目**:工作目录和对话跟着项目走;手头几个项目来回切换,回到群里就能接着上次的思路继续。
|
|
17
|
+
- **进展看得见,问题直接答**:回复、工具调用、图片和后台进展都在卡片里,需要你补充信息时,就在群里回答。
|
|
18
|
+
- **趁手的模型,自己选**:Codex 订阅、GLM Coding Plan、DeepSeek、OpenRouter 和 Claude native 都能接入;在群里切换[账号、模型与推理档位](docs/models.md),每个项目记住自己的选择。
|
|
16
19
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
支持 Windows、macOS 和 Linux,需要 Node.js ≥ 18.15。Bun 用于源码开发和构建。
|
|
20
|
-
|
|
21
|
-
DeepSeek Harness 子进程另需 Node 22.19+(22.x)或 Node 24+;可通过其账号配置的 `bin` 指定 Node 可执行文件。
|
|
20
|
+
## 快速开始
|
|
22
21
|
|
|
23
22
|
```bash
|
|
24
23
|
npm i -g @leviyuan/lodestar
|
|
25
24
|
lodestar-setup
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
把机器人拉进群,群名设为 `projects_root` 下的目录名。目录不存在时会自动创建。首次消息默认使用 Claude 侧已配置的账号;发 `model` 可切换到其他账号。
|
|
31
|
-
|
|
32
|
-
安装后提供以下命令:
|
|
33
|
-
|
|
34
|
-
| 命令 | 作用 |
|
|
35
|
-
| --- | --- |
|
|
36
|
-
| `lodestar-setup` | 配置向导 |
|
|
37
|
-
| `lodestar-daemon` | 启动 daemon |
|
|
38
|
-
| `lodestar-stop` | 停止 daemon |
|
|
39
|
-
| `lodestar-update` | 升级 Lodestar、Codex CLI、Claude Code 和相关 SDK |
|
|
40
|
-
| `lodestar-version` | 查看 Lodestar、Claude Code、Codex CLI 和运行时版本 |
|
|
41
|
-
| `lodestar-agent` | 由会话中的 Agent 调用其他模型执行任务 |
|
|
42
|
-
|
|
43
|
-
长期运行可交给 Linux `systemd --user`、macOS `launchd` 或 Windows 任务计划程序。daemon 重启后会恢复上次活跃的会话。
|
|
27
|
+
按向导完成配置,把机器人拉进以项目目录命名的群,发消息即可开始工作。[安装要求与详细配置](docs/configuration.md)
|
|
44
28
|
|
|
45
|
-
##
|
|
29
|
+
## 常用指令
|
|
46
30
|
|
|
47
|
-
|
|
31
|
+
在群内直接发送,不加斜杠,大小写不敏感。
|
|
48
32
|
|
|
49
|
-
|
|
|
50
|
-
| --- | --- |
|
|
51
|
-
| `hi` |
|
|
52
|
-
| `
|
|
53
|
-
| `kill`
|
|
54
|
-
| `restart` / `rs` | 进程存活时打断并恢复当前会话;已停止时列出同目录的历史会话,选择后创建独立分支 |
|
|
55
|
-
| `clear` / `cl` | 关闭当前进程并开始新会话;已停止时提示先启动 |
|
|
56
|
-
| `compact` / `cm` | 压缩当前会话的上下文 |
|
|
57
|
-
| `model` / `md` | 按账号 → 模型 → effort 选择并保存 |
|
|
58
|
-
| `agents` | 查看可调用的 Agent 身份及可用状态 |
|
|
59
|
-
| `task` | 创建、查看或删除绑定的飞书任务清单 |
|
|
33
|
+
| 完整指令 | 缩写 / 别名 | 作用 |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `hi` | — | 打开控制台,查看会话与额度 |
|
|
36
|
+
| `model` | `md` | 选择账号、模型和推理档位 |
|
|
37
|
+
| `kill` | `kl` | 关闭当前 Agent 进程,保留会话记录 |
|
|
60
38
|
|
|
61
|
-
|
|
39
|
+
完整指令、参数与会话分支说明见[群内使用指南](docs/usage.md)。
|
|
62
40
|
|
|
63
|
-
|
|
41
|
+
## 附加能力
|
|
64
42
|
|
|
65
|
-
|
|
43
|
+
- [多模型协作](docs/usage.md#多模型任务):将任务交给一个或多个模型执行,支持并发协作与后续追问。
|
|
44
|
+
- [飞书任务清单](docs/usage.md#飞书任务清单):在项目群创建、绑定和删除任务清单。
|
|
45
|
+
- [脚本通知](docs/usage.md#本机通知):让本机脚本向群里推送通知,支持图片、交互按钮和文字回复。
|
|
66
46
|
|
|
67
|
-
|
|
47
|
+
## 详细文档
|
|
68
48
|
|
|
69
|
-
|
|
|
49
|
+
| 文档 | 内容 |
|
|
70
50
|
| --- | --- |
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
临时会话共享当前工作目录,适合在同一项目里另开一段对话:
|
|
77
|
-
|
|
78
|
-
| 指令 | 行为 |
|
|
79
|
-
| --- | --- |
|
|
80
|
-
| `btw` | 创建 `<session>*MMDD-HHMM` 临时群,继承账号、模型和工作目录,启动新会话 |
|
|
81
|
-
| `bye` | 停止并解散当前临时群,仅适用于 Lodestar 创建的临时群 |
|
|
82
|
-
| `fk` / `fork` | 选择一条用户输入,在临时群里从这条输入之前分叉 |
|
|
83
|
-
| `bk` / `back` | 选择一条用户输入,让本群接到这条输入之前的新分支 |
|
|
84
|
-
|
|
85
|
-
`fk`、`bk` 和从历史记录恢复的 `rs` 都使用后端原生 fork,保留源对话。选第 N 条输入表示回到它发出之前,选中的输入不包含在新分支内。Claude 的新分支在首条输入时获得会话 id,准备状态会持久保存。
|
|
86
|
-
|
|
87
|
-
**分叉和回退只改变对话历史,不回滚文件或撤销 Shell、MCP 等外部操作。** `bk` 会附上已观察到的文件变更记录;需要目录隔离时使用 `wt`。
|
|
88
|
-
|
|
89
|
-
## 多模型任务
|
|
90
|
-
|
|
91
|
-
主 Agent 可以通过 `lodestar-agent` 查询实时身份,再把任务交给一个或多个模型。同一个任务选择多个身份时并发执行;后续追问可继续使用各模型的原生会话。
|
|
92
|
-
|
|
93
|
-
被调用的 Agent 可以编辑文件、执行命令、使用 MCP 和 Skill;委派只允许一层,被调用的 Agent 不能继续派工。它们与主 Agent 共享工作区,修改会立即可见。运行状态和结果通过卡片展示;需要用户输入时暂停,由主 Agent 回填答案后继续。委派产生的会话不会混入主群的 `rs` 历史列表。
|
|
94
|
-
|
|
95
|
-
`lodestar-agent` 只能在 Lodestar 管理的 Agent 进程里调用。命令用法见 [Agent Skill](src/agent-skill.ts)。
|
|
96
|
-
|
|
97
|
-
## 飞书任务清单
|
|
98
|
-
|
|
99
|
-
发 `task`,点“启用”可创建并绑定 `<project>[lodestar]` 任务清单。删除需要在卡片上再次确认,会删除整个清单及其中任务。该面板管理清单绑定,任务执行由用户和 Agent 安排。
|
|
100
|
-
|
|
101
|
-
## 本机通知
|
|
102
|
-
|
|
103
|
-
脚本可通过 HTTP 向项目群发送通知,支持 `info`、`warn`、`error`,以及本地图片、按钮和点击回调:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
curl -sS -X POST http://127.0.0.1:9876/notify \
|
|
107
|
-
-H 'Content-Type: application/json' \
|
|
108
|
-
-d '{"project":"ops","level":"error","text":"构建失败,截图如下","images":["/abs/shot.png"]}'
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
按钮点击结果可以 POST 到指定的本机 `callback`,或通过 `GET /notify/result/<notify_id>` 查询。字段和协议见 [feishu-notify Skill](src/notify-skill.ts)。daemon 启动时会将该 Skill 同步到 Codex 和 Claude 的 Skill 目录。
|
|
112
|
-
|
|
113
|
-
## 配置
|
|
114
|
-
|
|
115
|
-
默认配置文件是 `~/.config/lodestar/config.toml`,可通过 `LODESTAR_CONFIG` 指定文件。日志和会话状态位于 `~/.local/share/lodestar/`;Windows 使用相应的应用数据目录。完整路径定义见 [src/paths.ts](src/paths.ts)。
|
|
116
|
-
|
|
117
|
-
```toml
|
|
118
|
-
[runtime]
|
|
119
|
-
projects_root = "/abs/projects"
|
|
120
|
-
live_elapsed = "bucket" # bucket 按档位刷新耗时;second 按秒刷新
|
|
121
|
-
|
|
122
|
-
[projects.calculator]
|
|
123
|
-
cwd = "/abs/projects/calculator" # 对两个后端均生效
|
|
124
|
-
setting_sources = "project" # Claude 后端;仅未绑定 Token Source 时使用
|
|
125
|
-
strict_mcp = "true" # 以下字段用于 Claude 主会话
|
|
126
|
-
load_project_mcp = "true"
|
|
127
|
-
tools = "Read,Write,Edit,Bash,Glob,Grep"
|
|
128
|
-
|
|
129
|
-
[claude]
|
|
130
|
-
bin = "/abs/path/to/claude-wrapper" # 可选的 Claude 可执行文件
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
账号配置使用 `[token_source.glm]`、`[token_source.deepseek]` 等节。GLM、DeepSeek 和 OpenRouter 也可以从本机 Claude settings 中识别;识别后由对应 Token Source 注入凭据,避免不同账号的环境变量串用。项目的工具限制只作用于主会话,委派 Agent 使用完整工具集。
|
|
134
|
-
|
|
135
|
-
`md` / `model` 首页用独立分组展示 `claude`、`codex`、`dsh`:每组都有浅蓝标题栏、边框和 `Agent · 名称` 标题,默认展开,Token Source 行放在组内。选账号后进入模型和 effort 选择。除 OpenRouter 的默认九项外,各来源默认显示接口目录。接口中的模型使用「隐藏 / 显示」,只调整面板可见性,接口新增模型仍会出现。
|
|
136
|
-
|
|
137
|
-
所有 Token Source 都提供「补录模型」,用于登记接口列表外的模型,并支持「删除」补录记录。记录保存在 `custom_models`;支持端点验证的来源会先验证,无法确认模型能力或 effort 的记录明确显示 `MISS`,不会凭空赋予运行能力。接口后来收录同名模型时自动转为接口项,不重复显示。删除补录记录需要先切换仍选用它的会话;默认模型和辅助模型中指向已删除补录项的配置也会清理。
|
|
138
|
-
|
|
139
|
-
所有对话卡的模型标识固定为 `claude · 模型名/effort`、`codex · 模型名/effort` 或 `dsh · 模型名/effort`。窗口额度沿用原来的紧凑格式,如 `4.1h·7%·[6.9d·17%]`(重置倒计时与已用百分比);余额显示 `余额 $12.34` 或 `余额 ¥12.34`。缺失或读取失败显示 `MISS`。
|
|
140
|
-
|
|
141
|
-
OpenRouter 通过 Claude Agent SDK 运行。在群内发送 `openrouter-setup <api_key>`,再通过 `model` 面板选择模型和 effort;自建兼容端点用 `openrouter-setup <base_url> <api_key>`。也可在配置文件中添加:
|
|
142
|
-
|
|
143
|
-
```toml
|
|
144
|
-
[token_source.openrouter]
|
|
145
|
-
agent = "claude"
|
|
146
|
-
api_key = "填写自己的 OpenRouter API key"
|
|
147
|
-
# base_url = "https://openrouter.ai/api" # SDK 自动追加 /v1/messages
|
|
148
|
-
# model = "anthropic/claude-fable-5.1" # 可选:默认运行模型,须获账号目录确认
|
|
149
|
-
# effort = "max" # 可选:仅覆盖默认运行模型的档位
|
|
150
|
-
# models = "anthropic/claude-fable-5.1,moonshotai/kimi-k3" # 可选:自定义可选列表
|
|
151
|
-
# slots = "haiku=anthropic/claude-fable-5.1" # 可选:辅助任务模型,须获账号目录确认且使用相同的 effort 参数模式
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
OpenRouter 默认可选列表按 [Arena Agent Labs 榜单](https://arena.ai/leaderboard/agent?rankBy=labs) 的 2026-09-08 快照设置:前 12 家各取排名最高的模型,排除 OpenAI、Z.ai / GLM、DeepSeek,保留以下 9 项。
|
|
155
|
-
|
|
156
|
-
| Labs 排名 | 厂商 | OpenRouter 模型 | 默认档位 |
|
|
157
|
-
| --- | --- | --- | --- |
|
|
158
|
-
| 1 | Anthropic | `anthropic/claude-fable-5.1` | max |
|
|
159
|
-
| 3 | Moonshot | `moonshotai/kimi-k3` | max |
|
|
160
|
-
| 4 | Tencent | `tencent/hy4-preview` | high |
|
|
161
|
-
| 7 | Google | `google/gemini-3.8-flash` | high |
|
|
162
|
-
| 8 | SpaceXAI | `x-ai/grok-4.5` | high |
|
|
163
|
-
| 9 | Alibaba | `qwen/qwen3.8-max-0902` | xhigh |
|
|
164
|
-
| 10 | Meta | `meta/muse-spark-1.2` | xhigh |
|
|
165
|
-
| 11 | Xiaomi | `xiaomi/mimo-v2.5-pro` | 模型默认 |
|
|
166
|
-
| 12 | MiniMax | `minimax/minimax-m3` | 模型默认 |
|
|
167
|
-
|
|
168
|
-
在 `md` / `model` → claude → OpenRouter 中,点「显示模型」选择账号目录中的模型,点每行「隐藏」移出面板列表。可见性自动持久化,不改当前运行模型;全部隐藏后也能继续显示或补录。未配置 `models` 时才使用上述九项;`models = ""` 明确表示没有已显示的接口模型,刷新或重启不会补回默认项。榜单变化不会覆盖用户维护的列表。
|
|
169
|
-
|
|
170
|
-
候选目录来自 `/api/v1/models/user`,按账号供应商和隐私设置筛选,仅纳入支持文本和工具调用的交互模型;OpenAI、GLM、DeepSeek 及无法保证厂商范围的自动路由不会出现在添加候选中。目录刷新失败显示 `MISS`。显式配置但已下线的模型保留为可删除的 `MISS` 项。
|
|
171
|
-
|
|
172
|
-
effort 按上游声明提供。小米、MiniMax 等没有 effort 选择器的模型使用「模型默认」,实际请求不携带 effort 参数。两种参数模式之间切换时,空闲进程会保存原生会话并在下一轮用新环境恢复;相同模式下继续使用 SDK 热切换。未配置 `model` 时需要通过面板明确选择运行模型。
|
|
173
|
-
|
|
174
|
-
OpenRouter 余额来自 `/api/v1/credits`,按 `total_credits - total_usage` 计算,卡片仅显示 `余额 $…`,不附加 Key 限额或累计消费。现有 Key 已实测返回 200;接口权限和错误以实际响应为准。SDK 使用 Bearer token,并显式清空 `ANTHROPIC_API_KEY`。OpenRouter 官方的兼容保证限于 Anthropic 第一方供应商;上述九项的工具调用、模型与档位参数已做真实验证,验证范围见 [后端说明](docs/claude-agent-backend.md)。
|
|
175
|
-
|
|
176
|
-
DeepSeek Harness 使用独立的 `[token_source.deepseek-harness]` 账号与原生会话。在群内发送 `deepseek-harness-setup <api_key>`,再通过 `model` 面板选择该来源即可启用。自建端点用 `deepseek-harness-setup <base_url> <api_key>`;这里使用原生 API 根地址,不带 `/anthropic`。
|
|
177
|
-
|
|
178
|
-
DSH 也支持 GLM Coding Plan:已有 `[token_source.glm]` 时自动复用该账号,在 `md` → dsh → GLM Coding Plan 中选择模型。独立凭据用 `dsh-glm-setup [base_url] <api_key>`,配置节为 `[token_source.dsh-glm]`。它调用 Coding Plan 的 OpenAI 端点,复用 GLM 额度查询;不经过 OpenRouter 或 Claude SDK。账号接口返回的模型都进入列表,当前 DSH 原生目录未声明能力的项显示 `MISS`,也可隐藏和恢复。
|
|
179
|
-
|
|
180
|
-
```toml
|
|
181
|
-
[token_source.deepseek-harness]
|
|
182
|
-
agent = "dsh"
|
|
183
|
-
api_key = "填写自己的 API key"
|
|
184
|
-
# bin = "/abs/path/to/node" # 可选:运行 DSH 的 Node 可执行文件
|
|
185
|
-
# model = "deepseek-v4-pro" # 可选:必须存在于 DSH 返回的模型目录
|
|
186
|
-
# effort = "high" # 可选:必须是该模型支持的推理强度
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
手动修改配置后需重启 daemon;群内账号启用和模型补录会自行重载相关配置。模型路由、配置优先级和后端差异见 [后端说明](docs/claude-agent-backend.md)。
|
|
190
|
-
|
|
191
|
-
## 开发
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
bun install
|
|
195
|
-
bun run typecheck
|
|
196
|
-
bun test
|
|
197
|
-
bun run build
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
从源码启动使用 `bun run start`。`scripts/` 中的 smoke 和探针会操作真实飞书群,使用前阅读 [脚本说明](scripts/AGENTS.md)。
|
|
201
|
-
|
|
202
|
-
## 许可
|
|
51
|
+
| [安装与配置](docs/configuration.md) | 安装要求、CLI、配置、自动更新与源码开发 |
|
|
52
|
+
| [群内使用指南](docs/usage.md) | 完整命令、会话恢复、worktree、多模型任务、文件及通知 |
|
|
53
|
+
| [账号与模型](docs/models.md) | 账号接入、模型选择、推理档位与额度 |
|
|
54
|
+
| [后端与路由](docs/claude-agent-backend.md) | 运行机制、接口差异和维护说明 |
|
|
203
55
|
|
|
204
|
-
[MIT](LICENSE)
|
|
56
|
+
[MIT License](LICENSE)
|
package/dist/dsh-bridge.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
// src/dsh-bridge.ts
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
4
6
|
import { realpath } from "node:fs/promises";
|
|
5
7
|
import { createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
6
8
|
import { SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
9
|
+
|
|
10
|
+
// src/dsh-protocol.ts
|
|
11
|
+
var DSH_PROTOCOL_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
// src/dsh-bridge.ts
|
|
7
14
|
var name = "lodestar-bridge";
|
|
8
15
|
var inject = ["agents", "llm", "agentDefaultModel", "sessionPersistence", "tools", "systemPrompt", "userQuestions", "compaction", "shellEnv", "attachments", "sdkAppStartup"];
|
|
9
16
|
var PROVIDER = process.env.LODESTAR_DSH_PROVIDER ?? "deepseek-official";
|
|
10
17
|
if (!["deepseek-official", "zai-coding-cn", "zai"].includes(PROVIDER))
|
|
11
18
|
throw new Error(`Unsupported DSH provider: ${PROVIDER}`);
|
|
12
19
|
var DELEGATION_TOOLS = ["subagent", "subagent_fork", "workflow", "ralph", "send_message"];
|
|
20
|
+
function nativeErrorMessage(error, depth = 0) {
|
|
21
|
+
if (!(error instanceof Error))
|
|
22
|
+
return String(error);
|
|
23
|
+
return error.cause && depth < 4 ? `${error.message}: ${nativeErrorMessage(error.cause, depth + 1)}` : error.message;
|
|
24
|
+
}
|
|
13
25
|
function apply(ctx) {
|
|
14
26
|
let root;
|
|
15
27
|
let rootId;
|
|
@@ -30,7 +42,7 @@ function apply(ctx) {
|
|
|
30
42
|
`);
|
|
31
43
|
};
|
|
32
44
|
const fail = (error) => {
|
|
33
|
-
const message =
|
|
45
|
+
const message = nativeErrorMessage(error);
|
|
34
46
|
process.stderr.write(`lodestar-dsh: ${message}
|
|
35
47
|
`);
|
|
36
48
|
notify("failure", { message });
|
|
@@ -91,7 +103,9 @@ function apply(ctx) {
|
|
|
91
103
|
settle();
|
|
92
104
|
});
|
|
93
105
|
ctx.on("agent/error", ({ agent, error }) => {
|
|
94
|
-
const message =
|
|
106
|
+
const message = nativeErrorMessage(error);
|
|
107
|
+
process.stderr.write(`lodestar-dsh agent error: ${message}
|
|
108
|
+
`);
|
|
95
109
|
if (agent.session.id === rootId)
|
|
96
110
|
turnFailure = message;
|
|
97
111
|
notify("agent.error", { sessionId: agent.session.id, message });
|
|
@@ -133,8 +147,13 @@ function apply(ctx) {
|
|
|
133
147
|
notify("question", { id, questions: request.questions });
|
|
134
148
|
});
|
|
135
149
|
});
|
|
136
|
-
async function catalog() {
|
|
137
|
-
const models = await ctx.llm.listModels(PROVIDER);
|
|
150
|
+
async function catalog(extra = []) {
|
|
151
|
+
const models = [...await ctx.llm.listModels(PROVIDER)];
|
|
152
|
+
const listed = new Set(models.map((model) => model.id));
|
|
153
|
+
for (const id of new Set(extra)) {
|
|
154
|
+
if (!listed.has(id))
|
|
155
|
+
models.push(await ctx.llm.resolveModelInfo(PROVIDER, id));
|
|
156
|
+
}
|
|
138
157
|
if (!models.length)
|
|
139
158
|
throw new Error("DSH model catalog is empty");
|
|
140
159
|
return Promise.all(models.map(async (model) => {
|
|
@@ -148,14 +167,12 @@ function apply(ctx) {
|
|
|
148
167
|
efforts: info.reasoning.efforts.map((e) => e.id),
|
|
149
168
|
defaultEffort: info.reasoning.defaultEffort,
|
|
150
169
|
contextWindow: info.context?.contextWindow ?? null,
|
|
151
|
-
isDefault: ctx.agentDefaultModel.currentSelection().provider === PROVIDER && ctx.agentDefaultModel.currentSelection().model === model.id
|
|
170
|
+
isDefault: ctx.agentDefaultModel.currentSelection().provider === PROVIDER && ctx.agentDefaultModel.currentSelection().model === model.id,
|
|
171
|
+
isCustom: !listed.has(model.id)
|
|
152
172
|
};
|
|
153
173
|
}));
|
|
154
174
|
}
|
|
155
175
|
async function route(model, effort) {
|
|
156
|
-
const info = (await catalog()).find((entry) => entry.model === model);
|
|
157
|
-
if (!info || !info.efforts.some((e) => e === effort))
|
|
158
|
-
throw new Error(`DSH model/effort unavailable: ${model}/${effort}`);
|
|
159
176
|
return ctx.llm.resolveCallConfig({ provider: PROVIDER, model, reasoningEffort: ReasoningEffortId(effort) });
|
|
160
177
|
}
|
|
161
178
|
async function open(input) {
|
|
@@ -231,7 +248,7 @@ function apply(ctx) {
|
|
|
231
248
|
}
|
|
232
249
|
await ctx.sessionPersistence.flush();
|
|
233
250
|
lifecycle.signal.throwIfAborted();
|
|
234
|
-
return { sessionId: rootId, models: await catalog() };
|
|
251
|
+
return { sessionId: rootId, models: await catalog([input.model]) };
|
|
235
252
|
} catch (error) {
|
|
236
253
|
if (!root)
|
|
237
254
|
rootId = undefined;
|
|
@@ -246,13 +263,15 @@ function apply(ctx) {
|
|
|
246
263
|
throw new Error("DSH already initialized");
|
|
247
264
|
await ctx.get("loader")?.await();
|
|
248
265
|
initialized = true;
|
|
249
|
-
|
|
266
|
+
const require2 = createRequire(import.meta.url);
|
|
267
|
+
const manifest = JSON.parse(readFileSync(require2.resolve("@deepseek-ai/dsh/package.json"), "utf8"));
|
|
268
|
+
return { protocolVersion: DSH_PROTOCOL_VERSION, runtimeVersion: manifest.version };
|
|
250
269
|
}
|
|
251
270
|
if (!initialized)
|
|
252
271
|
throw new Error("DSH not initialized");
|
|
253
272
|
switch (method) {
|
|
254
273
|
case "model/list":
|
|
255
|
-
return catalog();
|
|
274
|
+
return catalog(params.models ?? []);
|
|
256
275
|
case "session/open": {
|
|
257
276
|
if (opening)
|
|
258
277
|
throw new Error("DSH session initialization is already in progress");
|
package/dist/lodestar-setup.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{
|
|
3
|
-
${
|
|
4
|
-
${
|
|
5
|
-
`)}
|
|
6
|
-
${z.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
${
|
|
2
|
+
import{createRequire as P1}from"node:module";var N1=Object.create;var{getPrototypeOf:k1,defineProperty:K0,getOwnPropertyNames:O1}=Object;var F1=Object.prototype.hasOwnProperty;function _1(q){return this[q]}var x1,T1,U0=(q,z,J)=>{var Q=q!=null&&typeof q==="object";if(Q){var Z=z?x1??=new WeakMap:T1??=new WeakMap,X=Z.get(q);if(X)return X}J=q!=null?N1(k1(q)):{};let H=z||!q||!q.__esModule?K0(J,"default",{value:q,enumerable:!0}):J;for(let B of O1(q))if(!F1.call(H,B))K0(H,B,{get:_1.bind(q,B),enumerable:!0});if(Q)Z.set(q,H);return H};var k=(q,z)=>()=>(z||q((z={exports:{}}).exports,z),z.exports);var T=P1(import.meta.url);var k0=k((C8,N0)=>{N0.exports=M0;M0.sync=R1;var W0=T("fs");function b1(q,z){var J=z.pathExt!==void 0?z.pathExt:process.env.PATHEXT;if(!J)return!0;if(J=J.split(";"),J.indexOf("")!==-1)return!0;for(var Q=0;Q<J.length;Q++){var Z=J[Q].toLowerCase();if(Z&&q.substr(-Z.length).toLowerCase()===Z)return!0}return!1}function G0(q,z,J){if(!q.isSymbolicLink()&&!q.isFile())return!1;return b1(z,J)}function M0(q,z,J){W0.stat(q,function(Q,Z){J(Q,Q?!1:G0(Z,q,z))})}function R1(q,z){return G0(W0.statSync(q),q,z)}});var T0=k((l8,x0)=>{x0.exports=F0;F0.sync=I1;var O0=T("fs");function F0(q,z,J){O0.stat(q,function(Q,Z){J(Q,Q?!1:_0(Z,z))})}function I1(q,z){return _0(O0.statSync(q),z)}function _0(q,z){return q.isFile()&&j1(q,z)}function j1(q,z){var{mode:J,uid:Q,gid:Z}=q,X=z.uid!==void 0?z.uid:process.getuid&&process.getuid(),H=z.gid!==void 0?z.gid:process.getgid&&process.getgid(),B=parseInt("100",8),Y=parseInt("010",8),V=parseInt("001",8),U=B|Y,L=J&V||J&Y&&Z===H||J&B&&Q===X||J&U&&X===0;return L}});var b0=k((p8,P0)=>{var d8=T("fs"),A;if(process.platform==="win32"||global.TESTING_WINDOWS)A=k0();else A=T0();P0.exports=C;C.sync=D1;function C(q,z,J){if(typeof z==="function")J=z,z={};if(!J){if(typeof Promise!=="function")throw TypeError("callback not provided");return new Promise(function(Q,Z){C(q,z||{},function(X,H){if(X)Z(X);else Q(H)})})}A(q,z||{},function(Q,Z){if(Q){if(Q.code==="EACCES"||z&&z.ignoreErrors)Q=null,Z=!1}J(Q,Z)})}function D1(q,z){try{return A.sync(q,z||{})}catch(J){if(z&&z.ignoreErrors||J.code==="EACCES")return!1;else throw J}}});var w0=k((i8,S0)=>{var R=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",R0=T("path"),y1=R?";":":",I0=b0(),j0=(q)=>Object.assign(Error(`not found: ${q}`),{code:"ENOENT"}),D0=(q,z)=>{let J=z.colon||y1,Q=q.match(/\//)||R&&q.match(/\\/)?[""]:[...R?[process.cwd()]:[],...(z.path||process.env.PATH||"").split(J)],Z=R?z.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",X=R?Z.split(J):[""];if(R){if(q.indexOf(".")!==-1&&X[0]!=="")X.unshift("")}return{pathEnv:Q,pathExt:X,pathExtExe:Z}},y0=(q,z,J)=>{if(typeof z==="function")J=z,z={};if(!z)z={};let{pathEnv:Q,pathExt:Z,pathExtExe:X}=D0(q,z),H=[],B=(V)=>new Promise((U,L)=>{if(V===Q.length)return z.all&&H.length?U(H):L(j0(q));let K=Q[V],O=/^".*"$/.test(K)?K.slice(1,-1):K,M=R0.join(O,q),_=!O&&/^\.[\\\/]/.test(q)?q.slice(0,2)+M:M;U(Y(_,V,0))}),Y=(V,U,L)=>new Promise((K,O)=>{if(L===Z.length)return K(B(U+1));let M=Z[L];I0(V+M,{pathExt:X},(_,x)=>{if(!_&&x)if(z.all)H.push(V+M);else return K(V+M);return K(Y(V,U,L+1))})});return J?B(0).then((V)=>J(null,V),J):B(0)},S1=(q,z)=>{z=z||{};let{pathEnv:J,pathExt:Q,pathExtExe:Z}=D0(q,z),X=[];for(let H=0;H<J.length;H++){let B=J[H],Y=/^".*"$/.test(B)?B.slice(1,-1):B,V=R0.join(Y,q),U=!Y&&/^\.[\\\/]/.test(q)?q.slice(0,2)+V:V;for(let L=0;L<Q.length;L++){let K=U+Q[L];try{if(I0.sync(K,{pathExt:Z}))if(z.all)X.push(K);else return K}catch(O){}}}if(z.all&&X.length)return X;if(z.nothrow)return null;throw j0(q)};S0.exports=y0;y0.sync=S1});var f0=k((n8,l)=>{var v0=(q={})=>{let z=q.env||process.env;if((q.platform||process.platform)!=="win32")return"PATH";return Object.keys(z).reverse().find((Q)=>Q.toUpperCase()==="PATH")||"Path"};l.exports=v0;l.exports.default=v0});var h0=k((s8,u0)=>{var A0=T("path"),w1=w0(),v1=f0();function E0(q,z){let J=q.options.env||process.env,Q=process.cwd(),Z=q.options.cwd!=null,X=Z&&process.chdir!==void 0&&!process.chdir.disabled;if(X)try{process.chdir(q.options.cwd)}catch(B){}let H;try{H=w1.sync(q.command,{path:J[v1({env:J})],pathExt:z?A0.delimiter:void 0})}catch(B){}finally{if(X)process.chdir(Q)}if(H)H=A0.resolve(Z?q.options.cwd:"",H);return H}function f1(q){return E0(q)||E0(q,!0)}u0.exports=f1});var c0=k((u1,p)=>{var d=/([()\][%!^"`<>&|;, *?])/g;function A1(q){return q=q.replace(d,"^$1"),q}function E1(q,z){if(q=`${q}`,q=q.replace(/(?=(\\+?)?)\1"/g,"$1$1\\\""),q=q.replace(/(?=(\\+?)?)\1$/,"$1$1"),q=`"${q}"`,q=q.replace(d,"^$1"),z)q=q.replace(d,"^$1");return q}u1.command=A1;u1.argument=E1});var m0=k((a8,g0)=>{g0.exports=/^#!(.*)/});var l0=k((o8,C0)=>{var g1=m0();C0.exports=(q="")=>{let z=q.match(g1);if(!z)return null;let[J,Q]=z[0].replace(/#! ?/,"").split(" "),Z=J.split("/").pop();if(Z==="env")return Q;return Q?`${Z} ${Q}`:Z}});var p0=k((t8,d0)=>{var i=T("fs"),m1=l0();function C1(q){let J=Buffer.alloc(150),Q;try{Q=i.openSync(q,"r"),i.readSync(Q,J,0,150,0),i.closeSync(Q)}catch(Z){}return m1(J.toString())}d0.exports=C1});var a0=k((r8,s0)=>{var l1=T("path"),i0=h0(),n0=c0(),d1=p0(),p1=process.platform==="win32",i1=/\.(?:com|exe)$/i,n1=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function s1(q){q.file=i0(q);let z=q.file&&d1(q.file);if(z)return q.args.unshift(q.file),q.command=z,i0(q);return q.file}function a1(q){if(!p1)return q;let z=s1(q),J=!i1.test(z);if(q.options.forceShell||J){let Q=n1.test(z);q.command=l1.normalize(q.command),q.command=n0.command(q.command),q.args=q.args.map((X)=>n0.argument(X,Q));let Z=[q.command].concat(q.args).join(" ");q.args=["/d","/s","/c",`"${Z}"`],q.command=process.env.comspec||"cmd.exe",q.options.windowsVerbatimArguments=!0}return q}function o1(q,z,J){if(z&&!Array.isArray(z))J=z,z=null;z=z?z.slice(0):[],J=Object.assign({},J);let Q={command:q,args:z,options:J,file:void 0,original:{command:q,args:z}};return J.shell?Q:a1(Q)}s0.exports=o1});var r0=k((e8,t0)=>{var n=process.platform==="win32";function s(q,z){return Object.assign(Error(`${z} ${q.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${z} ${q.command}`,path:q.command,spawnargs:q.args})}function t1(q,z){if(!n)return;let J=q.emit;q.emit=function(Q,Z){if(Q==="exit"){let X=o0(Z,z);if(X)return J.call(q,"error",X)}return J.apply(q,arguments)}}function o0(q,z){if(n&&q===1&&!z.file)return s(z.original,"spawn");return null}function r1(q,z){if(n&&q===1&&!z.file)return s(z.original,"spawnSync");return null}t0.exports={hookChildProcess:t1,verifyENOENT:o0,verifyENOENTSync:r1,notFoundError:s}});var t=k(($$,I)=>{var e0=T("child_process"),a=a0(),o=r0();function $1(q,z,J){let Q=a(q,z,J),Z=e0.spawn(Q.command,Q.args,Q.options);return o.hookChildProcess(Z,Q),Z}function e1(q,z,J){let Q=a(q,z,J),Z=e0.spawnSync(Q.command,Q.args,Q.options);return Z.error=Z.error||o.verifyENOENTSync(Z.status,Q),Z}I.exports=$1;I.exports.spawn=$1;I.exports.sync=e1;I.exports._parse=a;I.exports._enoent=o});var W1=U0(t(),1);import{execFileSync as R8,spawn as g}from"node:child_process";import{existsSync as H0,mkdirSync as G1,readFileSync as I8}from"node:fs";import{createInterface as j8}from"node:readline/promises";import{dirname as D8,join as V0}from"node:path";import{homedir as y8}from"node:os";import{fileURLToPath as S8}from"node:url";import{homedir as $8}from"node:os";import{join as W}from"node:path";var E=$8(),q1=process.platform==="win32";function z1(q,z,J){if(q)return q;if(z)return W(z,"lodestar");return J}function q8(){if(q1)return W(process.env.APPDATA??W(E,"AppData","Roaming"),"Lodestar");return W(E,".config","lodestar")}function z8(){if(q1)return W(process.env.LOCALAPPDATA??W(E,"AppData","Local"),"Lodestar");return W(E,".local","share","lodestar")}var r=z1(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,q8()),G=z1(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,z8()),S=process.env.LODESTAR_CONFIG??W(r,"config.toml"),J$=W(G,"daemon.pid"),Q$=W(G,"daemon.log"),Z$=W(G,"session-chat-map.json"),X$=W(G,"session-resume-map.json"),Y$=W(G,"session-turns-map.json"),H$=W(G,"session-model-map.json"),V$=W(G,"temp-session-leases.json"),L$=W(G,"tasklist-map.json"),B$=W(G,"agent-runs"),K$=W(G,"agent-session-ids.json"),U$=W(G,"managed-claude-plugin"),W$=W(G,"dsh"),e=W(G,"agent-runtimes"),G$=W(G,"context-window-cache.json"),M$=W(G,"notify-callbacks.json"),N$=W(G,"alive-on-shutdown.json"),k$=W(G,"inbox"),O$=W(G,"debug.sock"),F$=W(G,"debug-context.json");import{chmodSync as J8,closeSync as $0,fsyncSync as J1,mkdirSync as Q8,openSync as Q1,renameSync as Z8,unlinkSync as X8,writeFileSync as Y8}from"node:fs";import{randomUUID as H8}from"node:crypto";import{dirname as Z1}from"node:path";var V8=384;var L8=448;function B8(q){let z=Z1(q);Q8(z,{recursive:!0,mode:L8})}function K8(q,z,J){B8(q);let Q=`${q}.${process.pid}.${H8()}.tmp`,Z=null;try{Z=Q1(Q,"wx",J),Y8(Z,z),J1(Z),$0(Z),Z=null,Z8(Q,q);try{J8(q,J)}catch{}if(process.platform!=="win32"){let X=null;try{X=Q1(Z1(q),"r"),J1(X)}catch{}finally{if(X!==null)try{$0(X)}catch{}}}}catch(X){if(Z!==null)try{$0(Z)}catch{}try{X8(Q)}catch{}throw X}}function q0(q,z){K8(q,z,V8)}import{createHash as M8,randomUUID as N8}from"node:crypto";import{existsSync as J0,readFileSync as H1}from"node:fs";import{mkdir as V1,mkdtemp as k8,readFile as L1,rename as B1,rm as h,writeFile as Q0}from"node:fs/promises";import{join as F,isAbsolute as O8}from"node:path";import{fileURLToPath as F8,pathToFileURL as v$}from"node:url";var X1=U0(t(),1);import{execFile as U8}from"node:child_process";var W8="https://registry.npmjs.org";class u extends Error{}function G8(q){if(!Number.isSafeInteger(q)||q<=0)return Promise.reject(Error(`Invalid npm installer PID: ${q}`));return new Promise((z,J)=>{U8("taskkill",["/PID",String(q),"/T","/F"],{windowsHide:!0,timeout:1e4},(Q,Z,X)=>{if(Q)J(Error(`taskkill npm installer PID ${q} failed: ${Q.message}
|
|
3
|
+
${Z}${X}`));else z()})})}async function z0(q,z,J={}){z?.throwIfAborted();let Q=J.platform??process.platform,Z=J.timeoutMs??300000,X=J.terminationTimeoutMs??1e4;return await new Promise((H,B)=>{let Y=(J.spawn??X1.spawn)("npm",["install","--prefix",q,"--include=optional","--no-fund","--no-audit",`--registry=${W8}`],{cwd:q,stdio:["ignore","pipe","pipe"],shell:!1,windowsHide:!0}),V="",U,L,K=!1,O=!1,M,_=(N)=>{V=(V+N.toString()).slice(-16384)};Y.stdout.on("data",_),Y.stderr.on("data",_);let x=(N)=>{if(K)return;if(K=!0,clearTimeout(B0),M)clearTimeout(M);if(z?.removeEventListener("abort",f),N)B(N);else H()},b=(N)=>new u(`${U?.message??"Agent runtime install failed"}; npm installer PID ${Y.pid??"MISS"} termination unconfirmed: ${N}; partial directory retained: ${q}
|
|
4
|
+
${V}`),L0=(N)=>{if(K||O||L)return;U??=Error(N),M=setTimeout(()=>x(b(`stdio did not close within ${X}ms`)),X),L=Promise.resolve().then(async()=>{if(O)throw Error("installer closed before process-tree termination started");if(!Y.pid)throw Error("installer PID unavailable");if(Q==="win32")await(J.terminateWindows??G8)(Y.pid);else if(!Y.kill("SIGTERM"))throw Error("npm installer rejected SIGTERM")}),L.catch((y)=>x(b(y instanceof Error?y.message:String(y))))},f=()=>L0("Agent runtime install aborted"),B0=setTimeout(()=>L0(`Agent runtime install timed out after ${Z}ms`),Z);if(z?.addEventListener("abort",f,{once:!0}),Y.on("error",(N)=>{if(U??=N,!Y.pid)x(Error(`${N.message}
|
|
5
|
+
${V}`))}),Y.once("close",async(N,y)=>{O=!0,clearTimeout(B0),z?.removeEventListener("abort",f);try{await L}catch(m){x(b(m instanceof Error?m.message:String(m)));return}if(U||N!==0)x(Error(`${U?.message??`npm install exited code=${N} signal=${y}`}
|
|
6
|
+
${V}`));else x()}),z?.aborted)f()})}var _8="https://registry.npmjs.org",w={codex:["@openai/codex"],claude:["@anthropic-ai/claude-code","@anthropic-ai/claude-agent-sdk","@anthropic-ai/sdk"],dsh:["@deepseek-ai/dsh","@deepseek-ai/dsh-llm-pi-ai","@deepseek-ai/dsh-tool-ask-user"]},j=(q)=>q instanceof Error?q.message:String(q);function K1(q,z=e){let J=F(z,q,"current.json");if(!J0(J))return null;let Q=JSON.parse(H1(J,"utf8"));if(!Number.isFinite(Q.checkedAt)||Q.directory!==void 0&&!O8(Q.directory))throw Error(`Invalid ${q} runtime state: ${J}`);return Q}function x8(q){let z=K1(q);if(z?.error)throw Error(`${q} 更新失败: ${z.error}`);if(!z?.directory)throw Error(`${q} runtime 未安装,请运行 lodestar-update --agents-only`);return z.directory}function Z0(q,z){let J=F(x8(q),"node_modules",".bin",`${z}${process.platform==="win32"?".cmd":""}`);if(!J0(J))throw Error(`${q} executable missing: ${J}`);return J}async function T8(q,z,J){let Q=new AbortController,Z=()=>Q.abort(J?.reason);if(J?.addEventListener("abort",Z,{once:!0}),J?.aborted)Z();let X=setTimeout(()=>Q.abort(Error("npm registry request timed out")),30000);try{let H=await fetch(`${_8}/${encodeURIComponent(q)}/${encodeURIComponent(z)}`,{signal:Q.signal});if(!H.ok)throw Error(`${q}@${z}: npm HTTP ${H.status}`);let B=await H.json();if(B.name!==q||typeof B.version!=="string"||!B.version)throw Error(`Invalid npm manifest: ${q}@${z}`);return B}finally{clearTimeout(X),J?.removeEventListener("abort",Z)}}async function P8(q,z){let J={};if(q!=="dsh"){let H=await Promise.all(w[q].map((B)=>z(B,"latest")));for(let B of H)J[B.name]=B.version;return J}let Q=await z("@deepseek-ai/dsh","latest"),Z=new Set(w.dsh),X=new Set;while(Z.size){let H=[...Z].slice(0,12);for(let Y of H)Z.delete(Y),X.add(Y);let B=await Promise.all(H.map((Y)=>Y===Q.name?Q:z(Y,Q.version)));for(let Y of B){if(Y.version!==Q.version)throw Error(`DSH release package mismatch: ${Y.name}@${Y.version}, expected ${Q.version}`);J[Y.name]=Y.version;for(let V of Object.keys({...Y.dependencies,...Y.peerDependencies,...Y.optionalDependencies}))if(V.startsWith("@deepseek-ai/dsh")&&!X.has(V))Z.add(V)}}return J}async function D(q,z=process.platform){for(let J=0;;J++)try{return await q()}catch(Q){let Z=Q.code;if(z!=="win32"||!["EPERM","EACCES","EBUSY","ENOTEMPTY"].includes(Z??"")||J>=5)throw Q;await new Promise((X)=>setTimeout(X,100*(J+1)))}}async function Y1(q,z){let J=F(q,`current-${N8()}.json`),Q;try{await Q0(J,JSON.stringify(z)+`
|
|
7
|
+
`,{mode:384}),await D(()=>B1(J,F(q,"current.json")))}catch(Z){throw Q=Z,Z}finally{try{await D(()=>h(J,{force:!0}))}catch(Z){if(Q)throw AggregateError([Q,Z],`${j(Q)}; state temporary file cleanup failed: ${j(Z)}`);throw Z}}}async function b8(q,z){let J=F(q,"update.lock"),Q=Date.now()+360000;for(;;){z?.throwIfAborted();try{return await V1(J),await Q0(F(J,"pid"),String(process.pid)),()=>D(()=>h(J,{recursive:!0}))}catch(Z){if(Z.code!=="EEXIST")throw Z;try{let X=Number(await L1(F(J,"pid"),"utf8"));if(!Number.isSafeInteger(X)||X<=0)throw Error(`Invalid runtime update lock: ${J}`);try{process.kill(X,0)}catch(H){if(H.code!=="ESRCH")throw H;await D(()=>h(J,{recursive:!0}));continue}}catch(X){if(X.code!=="ENOENT")throw X}if(Date.now()>=Q)throw Error(`Timed out waiting for runtime update lock: ${J}`);await new Promise((X)=>setTimeout(X,200))}}}async function X0(q,z={}){let J=z.root??e,Q=F(J,q);await V1(Q,{recursive:!0,mode:448});let Z=await b8(Q,z.signal),X,H,B=!1;try{z.report?.(`${q}: 检查 upstream latest`);let Y=K1(q,J),V=await P8(q,z.metadata??((M,_)=>T8(M,_,z.signal)));z.signal?.throwIfAborted();let U=JSON.parse(H1(F8(new URL("../package.json",import.meta.url)),"utf8")).overrides??{},L=M8("sha256").update(JSON.stringify({installFormat:2,versions:Object.entries(V).sort(),security:U,platform:process.platform,arch:process.arch})).digest("hex").slice(0,20),K=F(Q,L);if(!J0(K)){X=await k8(F(Q,".install-")),await Q0(F(X,"package.json"),JSON.stringify({name:`lodestar-runtime-${q}`,version:V[w[q][0]],private:!0,type:"module",dependencies:V,overrides:{...U,...q==="dsh"?V:{}}})+`
|
|
8
|
+
`,{mode:384}),await(z.install??z0)(X,z.signal);for(let[_,x]of Object.entries(V)){let b=JSON.parse(await L1(F(X,"node_modules",_,"package.json"),"utf8"));if(b.version!==x)throw Error(`npm installed ${_}@${b.version}, expected ${x}`)}z.signal?.throwIfAborted();let M=X;await D(()=>B1(M,K)),X=void 0}let O={directory:K,versions:V,checkedAt:Date.now()};return await Y1(Q,O),z.report?.(`${q}: ${w[q][0]}@${V[w[q][0]]}${Y?.directory===K?" 已是 latest":" 已更新,新进程使用新版"}`),O}catch(Y){H=Y,B=Y instanceof u;try{await Y1(Q,{checkedAt:Date.now(),error:j(Y)})}catch(V){throw H=AggregateError([Y,V],`${j(Y)}; recording update failure also failed: ${j(V)}`),H}throw Y}finally{let Y=[];if(X&&!B){let V=X;try{await D(()=>h(V,{recursive:!0}))}catch(U){Y.push(U)}}try{await Z()}catch(V){Y.push(V)}if(Y.length){let V=H?[H,...Y]:Y;throw AggregateError(V,V.map(j).join("; "))}}}var $={reset:"\x1B[0m",bold:"\x1B[1m",dim:"\x1B[2m",green:"\x1B[32m",yellow:"\x1B[33m",cyan:"\x1B[36m",red:"\x1B[31m"},v=j8({input:process.stdin,output:process.stdout});async function P(q,z={}){while(!0){let J=z.default?`${$.dim} [${z.default}]${$.reset}`:"",Q=(await v.question(`${$.cyan}? ${$.reset}${q}${J}
|
|
9
|
+
${$.green}>${$.reset} `)).trim();if(!Q&&z.default!==void 0)return z.default;if(!Q&&z.required){console.log(`${$.red}必填,请重新输入${$.reset}`);continue}return Q}}function w8(q){let z="═".repeat(58);console.log(`
|
|
10
|
+
${$.bold}${$.cyan}╔${z}╗${$.reset}`),console.log(`${$.bold}${$.cyan}║ ${q.padEnd(54)} ║${$.reset}`),console.log(`${$.bold}${$.cyan}╚${z}╝${$.reset}
|
|
11
|
+
`)}function c(q,z,J){console.log(`
|
|
12
|
+
${$.bold}${$.yellow}[${q}/${z}] ${J}${$.reset}
|
|
13
|
+
`)}function Y0(q){return q.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}function U1(q){try{let z=R8(q,["login","status"],{timeout:1e4,shell:process.platform==="win32"}).toString();return/Logged in using ChatGPT/i.test(z)}catch{return!1}}async function v8(q){return new Promise((z)=>{let J=W1.spawn(q,["login"],{stdio:"inherit",shell:!1});J.on("exit",(Q)=>z(Q===0)),J.on("error",()=>z(!1))})}function f8(){return process.env.CLAUDE_CONFIG_DIR||V0(y8(),".claude")}async function A8(q){try{let z=await fetch("https://open.bigmodel.cn/api/anthropic/v1/models",{headers:{Authorization:`Bearer ${q}`},signal:AbortSignal.timeout(1e4)});if(!z.ok)return null;let J=await z.json(),Q=(Array.isArray(J?.data)?J.data:[]).map((L)=>typeof L?.display_name==="string"&&L.display_name?L.display_name:String(L?.id??"")).filter(Boolean);if(!Q.length)return null;let Z=(L)=>{let K=L.match(/(\d+(?:\.\d+)?)/);return K?Number(K[1]):-1},X=(L)=>L.replace(/-Turbo|-Air$/i,""),B=[...Q.filter((L)=>!/-Turbo|-Air/i.test(L))].sort((L,K)=>Z(K)-Z(L)),Y=B[0],V=B[B.length-1],U={};if(Y)U.ANTHROPIC_DEFAULT_OPUS_MODEL=`${Y}[1m]`,U.ANTHROPIC_DEFAULT_SONNET_MODEL=`${Y}[1m]`;if(V&&X(V)!==X(Y))U.ANTHROPIC_DEFAULT_HAIKU_MODEL=V;return Object.keys(U).length?U:null}catch{return null}}async function E8(q){try{let z=f8();G1(z,{recursive:!0});let J=V0(z,"settings.json"),Q={};if(H0(J)){let B=I8(J,"utf8");try{let Y=JSON.parse(B);if(!Y||typeof Y!=="object"||Array.isArray(Y))return{error:`${J} 内容不是 JSON 对象, 拒绝覆盖, 请手动检查`};Q=Y}catch{return{error:`${J} 解析失败 (JSON 语法错), 拒绝覆盖, 请手动修复后重跑`}}}let Z=Q.env&&typeof Q.env==="object"&&!Array.isArray(Q.env)?Q.env:{},X={ANTHROPIC_AUTH_TOKEN:q,ANTHROPIC_BASE_URL:"https://open.bigmodel.cn/api/anthropic",API_TIMEOUT_MS:"3000000",CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:"1"},H=await A8(q);if(H)Object.assign(X,H);return Q.env={...Z,...X},q0(J,JSON.stringify(Q,null,2)+`
|
|
14
|
+
`),{path:J}}catch(z){return{error:z?.message??String(z)}}}function u8(q){try{if(process.platform==="win32")g(process.env.ComSpec??"cmd.exe",["/c","start",'""',q],{detached:!0,stdio:"ignore",windowsHide:!0}).unref();else if(process.platform==="darwin")g("open",[q],{detached:!0,stdio:"ignore"}).unref();else g("xdg-open",[q],{detached:!0,stdio:"ignore"}).unref()}catch{}}async function h8(q,z){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:q,app_secret:z}),signal:AbortSignal.timeout(15000)})).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 c8(){try{let q=D8(S8(import.meta.url)),z=V0(q,"lodestar.js");if(!H0(z))return{error:`找不到 daemon bundle: ${z}`};let J=g(process.execPath,[z],{detached:!0,stdio:"ignore",windowsHide:!0});return J.unref(),{pid:J.pid}}catch(q){return{error:q?.message??String(q)}}}async function M1(){if(!process.stdin.isTTY)console.error(`${$.red}lodestar-setup: stdin 不是 TTY,无法交互式输入。${$.reset}`),console.error("请直接在 cmd / PowerShell / Terminal 里跑,不要 pipe 或重定向 stdin。"),process.exit(1);if(H0(S)){if(console.log(`${$.yellow}发现已有配置: ${S}${$.reset}`),(await P("覆盖? (y/N)",{default:"n"})).toLowerCase()!=="y"){console.log("已取消"),v.close();return}}w8("Lodestar 安装向导"),console.log("Lodestar 把 Feishu (飞书) 群聊接到 AI agent。"),console.log("默认后端是 Claude Code; Codex 是可选第二后端, 群里发 model 一键切换。"),console.log("每个群对应一个项目目录, agent 在那里跑、能读写文件。"),console.log(),console.log(`${$.yellow}⚠ Claude 官方限制:${$.reset}`),console.log(` ${$.bold}Claude 订阅 (Pro/Max OAuth 登录) 不支持本项目${$.reset},`),console.log(" 必须走 API 方式 —— 推荐 GLM Coding Plan, 或自备 Anthropic API key。"),console.log(),console.log("本向导依次做 4 件事:"),console.log(` ${$.dim}1) 确保 Claude Code CLI 已装好${$.reset}`),console.log(` ${$.dim}2) GLM Coding Plan API key (推荐, 可选) + Codex (可选)${$.reset}`),console.log(` ${$.dim}3) Feishu 自建应用 (含权限 / 事件 / 发版 + 凭据测试)${$.reset}`),console.log(` ${$.dim}4) 工作目录, 自动启动 daemon${$.reset}`),console.log(),await v.question(`${$.dim}按 Enter 开始 (Ctrl+C 退出)...${$.reset}`),c(1,4,"准备 Claude Code CLI"),await X0("claude",{report:(L)=>console.log(L)}),console.log(`${$.green}✓ Claude Code / SDK 已更新${$.reset}: ${$.dim}${Z0("claude","claude")}${$.reset}`),console.log(),console.log(`${$.dim}下一步可选配 GLM Coding Plan 自动写入路由; 不配则用本机 Claude Code 现有配置。${$.reset}`),console.log(`${$.dim}记住: 别用 \`claude\` 走订阅 OAuth 登录 —— 订阅不支持本项目, 要用 API key。${$.reset}`),c(2,4,"GLM Coding Plan (推荐, 可选)"),console.log("GLM Coding Plan 给 Claude Code 接 GLM 系列模型 (支持 1M token 上下文, 中文友好)。"),console.log("订阅后在智谱开放平台拿一个 API key, 粘到下面 —— 向导自动写进 ~/.claude/settings.json。"),console.log(` ${$.dim}拿 key: https://open.bigmodel.cn → 控制台 → API Keys${$.reset}`),console.log(` ${$.dim}不给也行: 以本机 Claude Code 现有配置启动 (确保是 API key 方式, 非订阅)。${$.reset}`),console.log();let q=await P("GLM API key (直接回车跳过)",{});if(q){let L=await E8(q);if("path"in L)console.log(`${$.green}✓ GLM 路由已写入${$.reset}: ${$.dim}${L.path}${$.reset}`),console.log(`${$.dim}模型 slots 按端点当前列表自动选取(最新一代 opus/sonnet, 1M ctx)${$.reset}`);else console.log(`${$.red}✗ 写入失败:${$.reset} ${L.error}`),console.log(`${$.dim}跳过 GLM 配置, 以本机 Claude Code 现有配置启动。${$.reset}`)}else console.log(`${$.dim}已跳过 GLM, 以本机 Claude Code 现有配置启动。${$.reset}`);if(console.log(),console.log(`${$.bold}Codex (可选第二后端)${$.reset}`),console.log(` ${$.dim}想用 Codex 的话, 登录 ChatGPT 订阅即可, 模型/effort 走 ~/.codex/config.toml, 群里发 model 切换。${$.reset}`),(await P("现在顺便配置 Codex 后端吗?",{default:"n"})).toLowerCase()==="y"){await X0("codex",{report:(K)=>console.log(K)});let L=Z0("codex","codex");if(U1(L))console.log(`${$.green}✓ Codex 已登录 ChatGPT${$.reset}`);else if(console.log(`${$.dim}启动 \`codex login\`...${$.reset}`),await v8(L)&&U1(L))console.log(`${$.green}✓ Codex 已登录 ChatGPT${$.reset}`);else console.log(`${$.yellow}Codex 登录未完成 — 不影响默认 Claude 后端; 需要时随时跑 codex login。${$.reset}`)}else console.log(`${$.dim}已跳过 Codex (需要时随时跑 codex login, 群里发 model 切)。${$.reset}`);c(3,4,"Feishu 自建应用");let J="https://open.feishu.cn/app";console.log("打开飞书开放平台 (浏览器):"),console.log(` ${$.cyan}${J}${$.reset}`),u8(J),console.log(`${$.dim}(如果浏览器没自动开, 复制上面 URL 粘到浏览器)${$.reset}`),console.log(),console.log(`${$.bold}详细操作步骤:${$.reset}`),console.log(),console.log(` ${$.bold}① 创建应用${$.reset}`),console.log(` 点 "创建企业自建应用", 填名字 (如 ${$.dim}Lodestar${$.reset}), logo 随意。`),console.log(),console.log(` ${$.bold}② 添加机器人能力${$.reset}`),console.log(` 左侧菜单 "${$.cyan}添加应用能力${$.reset}" → 找到 "机器人" → 点 "${$.bold}添加${$.reset}" 按钮。`),console.log(),console.log(` ${$.bold}③ 申请权限 (左侧 "${$.cyan}权限管理${$.reset}" → "${$.bold}开通权限${$.reset}")${$.reset}`),console.log(` ${$.yellow}缺一个都会让 daemon 启动后默默丢消息, 一定要全开。${$.reset}`),console.log(` ${$.dim}消息类:${$.reset}`),console.log(` • ${$.bold}im:message:send_as_bot${$.reset} ${$.dim}# 以机器人身份发消息${$.reset}`),console.log(` • ${$.bold}im:message${$.reset} ${$.dim}# 接收/操作消息 (核心)${$.reset}`),console.log(` • ${$.bold}im:chat${$.reset} ${$.dim}# 读/写群信息 (匹配群名 ↔ 项目目录)${$.reset}`),console.log(` • ${$.bold}im:chat:create${$.reset} ${$.dim}# wt: 自动创建 worktree 群${$.reset}`),console.log(` • ${$.bold}im:chat:delete${$.reset} ${$.dim}# wt: 解散 worktree 群${$.reset}`),console.log(` • ${$.bold}im:chat.members:read${$.reset} ${$.dim}# wt: 判断发起人是否已在群内${$.reset}`),console.log(` • ${$.bold}im:chat.members:write_only${$.reset} ${$.dim}# wt: 把发起人拉进已有 worktree 群${$.reset}`),console.log(` • ${$.bold}im:resource${$.reset} ${$.dim}# 上传/下载附件 (图文双向)${$.reset}`),console.log(` • ${$.bold}im:message.urgent${$.reset} ${$.dim}# 加急推送 (锁屏通知 / Ask)${$.reset}`),console.log(` • ${$.bold}im:message.group_msg${$.reset} ${$.dim}# 敏感: 接收群里所有消息${$.reset}`),console.log(` ${$.dim}└ 关键: 没它机器人只收 @ 自己的消息, 拿不到群里其他对话, 一定要开${$.reset}`),console.log(` ${$.dim}└ 敏感权限要走审批: 申请时填用途, 个人开发者通常秒过${$.reset}`),console.log(` • ${$.bold}im:message.group_at_msg:readonly${$.reset} ${$.dim}# 读 @ 机器人消息 (兜底)${$.reset}`),console.log(` ${$.dim}卡片类 (Card Kit):${$.reset}`),console.log(` • ${$.bold}cardkit:card:read${$.reset} ${$.dim}# 读卡片状态${$.reset}`),console.log(` • ${$.bold}cardkit:card:write${$.reset} ${$.dim}# 创建/更新卡片 (流式渲染核心)${$.reset}`),console.log(),console.log(` ${$.bold}④ 订阅事件 (左侧 "${$.cyan}事件与回调${$.reset}", 拆两个子页:)${$.reset}`),console.log(` ${$.dim}a)${$.reset} ${$.bold}事件配置${$.reset} 页:`),console.log(` ${$.yellow}• "订阅方式" → 选 "长连接" → 点保存${$.reset}`),console.log(` • 添加事件: ${$.bold}im.message.receive_v1${$.reset} ${$.dim}# 收群消息${$.reset}`),console.log(` ${$.dim}b)${$.reset} ${$.bold}回调配置${$.reset} 页:`),console.log(` ${$.yellow}• "订阅方式" → 选 "长连接" → 点保存${$.reset}`),console.log(` • 添加事件: ${$.bold}card.action.trigger${$.reset} ${$.dim}# 卡片按钮点击回调${$.reset}`),console.log(),console.log(` ${$.bold}⑤ 发布版本${$.reset}`),console.log(` 页面顶部 "${$.bold}创建版本${$.reset}" → 滚到底点 "${$.bold}保存${$.reset}" → 弹框点 "${$.bold}发布${$.reset}"。`),console.log(` ${$.yellow}没发版的应用收不到任何事件 — 这步九成新手会忘!${$.reset}`),console.log(),console.log(` ${$.bold}⑥ 拿凭据${$.reset}`),console.log(` 左侧 "凭证与基础信息" → 顶部 ${$.bold}App ID${$.reset} (${$.dim}cli_...${$.reset}) 和 ${$.bold}App Secret${$.reset}, 待会粘到下面。`),console.log(),console.log(` ${$.bold}⑦ 把机器人拉进群${$.reset}`),console.log(" 想用的飞书群 → 群设置 → 群机器人 → 添加机器人 → 选你的应用。"),console.log(` ${$.yellow}群名要等于 projects_root 下的项目目录名${$.reset} (下一步设, 默认是用户主目录)。`),console.log();let Q="",Z="";while(!0){Q=await P("App ID (以 cli_ 开头)",{required:!0}),Z=await P("App Secret",{required:!0}),console.log(`${$.dim}测试中... (调 tenant_access_token endpoint)${$.reset}`);let L=await h8(Q,Z);if(L.ok){console.log(`${$.green}✓ Feishu 凭据测试通过${$.reset}`);break}if(console.log(`${$.red}✗ 测试失败:${$.reset} ${L.error}`),console.log(`${$.dim}最常见原因: app_id / app_secret 抄错, 或应用还没 "发布上线" (步骤 ⑤)。${$.reset}`),console.log(),(await P("重新填? (Y/n)",{default:"y"})).toLowerCase()==="n")console.log(`${$.yellow}已取消, 配置未写盘。${$.reset}`),v.close(),process.exit(1)}c(4,4,"工作目录 + 启动"),console.log("每个 Feishu 群对应 projects_root 下同名的目录。"),console.log();let X=process.platform==="win32"?process.env.USERPROFILE??"C:\\Users\\Default":process.env.HOME??"/root",H=await P("projects_root",{default:X});G1(r,{recursive:!0});let B=["# Lodestar config — generated by `lodestar-setup`","# Edit by hand or re-run setup to overwrite.","","[feishu]",`app_id = "${Y0(Q)}"`,`app_secret = "${Y0(Z)}"`,"","[runtime]",`projects_root = "${Y0(H)}"`,"","[runtime.agent_auto_update]","codex = false","claude = false","dsh = false",""];q0(S,B.join(`
|
|
10
15
|
`)),console.log(`
|
|
11
|
-
${
|
|
12
|
-
${
|
|
13
|
-
lodestar-setup: ${
|
|
16
|
+
${$.green}${$.bold}✓ 配置已写入${$.reset}`),console.log(` ${$.cyan}${S}${$.reset}`),console.log(`
|
|
17
|
+
${$.bold}启动 daemon...${$.reset}`);let Y=c8(),V=process.platform==="win32"?"\\":"/",U=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(Y.pid)console.log(`${$.green}✓ daemon 已在后台启动${$.reset} (pid ${Y.pid})`),console.log(),console.log(`${$.bold}最后一步: 在 Feishu 验证${$.reset}`),console.log(" ① 把机器人拉进任意飞书群"),console.log(` ② 群名 = ${$.cyan}${H}${V}<群名>${$.reset} 下的目录名 (新群第一条消息会自动建)`),console.log(" ③ 在群里发任意一条消息, 默认由 Claude 接管"),console.log(` ${$.dim}(群里发 model 可选择已配置账号、动态模型和 effort)${$.reset}`),console.log(),console.log("日志 (按日滚动, 保留近 7 天):"),console.log(` ${$.cyan}${U}${$.reset}`),console.log(),console.log(`${$.dim}若长期跑后台, Linux 配 systemd --user,macOS 配 launchd,Windows 配任务计划程序。${$.reset}`);else console.log(`${$.yellow}启动失败: ${Y.error}${$.reset}`),console.log(`手动运行: ${$.cyan}lodestar-daemon${$.reset}`);console.log(),v.close()}M1().catch((q)=>{console.error(`
|
|
18
|
+
lodestar-setup: ${q?.message??q}`),process.exit(1)});
|
package/dist/lodestar-stop.js
CHANGED
|
@@ -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 q}from"node:path";var X=W(),B=process.platform==="win32";function b(
|
|
3
|
-
`),Q=parseInt((
|
|
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 q}from"node:path";var X=W(),B=process.platform==="win32";function b(K,Q,Y){if(K)return K;if(Q)return q(Q,"lodestar");return Y}function H(){if(B)return q(process.env.APPDATA??q(X,"AppData","Roaming"),"Lodestar");return q(X,".config","lodestar")}function V(){if(B)return q(process.env.LOCALAPPDATA??q(X,"AppData","Local"),"Lodestar");return q(X,".local","share","lodestar")}var w=b(process.env.LODESTAR_CONFIG_DIR,process.env.XDG_CONFIG_HOME,H()),J=b(process.env.LODESTAR_DATA_DIR,process.env.XDG_DATA_HOME,V()),x=process.env.LODESTAR_CONFIG??q(w,"config.toml"),Z=q(J,"daemon.pid"),S=q(J,"daemon.log"),E=q(J,"session-chat-map.json"),v=q(J,"session-resume-map.json"),h=q(J,"session-turns-map.json"),k=q(J,"session-model-map.json"),F=q(J,"temp-session-leases.json"),R=q(J,"tasklist-map.json"),T=q(J,"agent-runs"),m=q(J,"agent-session-ids.json"),C=q(J,"managed-claude-plugin"),l=q(J,"dsh"),c=q(J,"agent-runtimes"),j=q(J,"context-window-cache.json"),t=q(J,"notify-callbacks.json"),_=q(J,"alive-on-shutdown.json"),A=q(J,"inbox"),I=q(J,"debug.sock"),p=q(J,"debug-context.json");import{execSync as G}from"node:child_process";import{readFileSync as L,writeFileSync as o}from"node:fs";function M(K){try{if(process.platform==="linux")return L(`/proc/${K}/cmdline`,"utf8").replace(/\0/g," ").trim();if(process.platform==="darwin")return G(`ps -p ${K} -o args=`,{encoding:"utf8",timeout:2000}).trim();if(process.platform==="win32")return G(`powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${K}').CommandLine"`,{encoding:"utf8",timeout:5000}).trim()||null;return null}catch{return null}}function N(K,Q){if(!Number.isFinite(K)||K<=0||!Q)return!1;let Y=M(K);if(!Y)return!1;if(process.platform==="win32")return Y.toLowerCase().includes(Q.toLowerCase());return Y.includes(Q)}var z={reset:"\x1B[0m",bold:"\x1B[1m",green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m",dim:"\x1B[2m"};function P(K){return new Promise((Q)=>setTimeout(Q,K))}async function u(){if(!U(Z)){console.log(`${z.yellow}Lodestar daemon 未运行${z.reset} ${z.dim}(${Z} 不存在)${z.reset}`);return}let K=y(Z,"utf8").split(`
|
|
3
|
+
`),Q=parseInt((K[0]??"").trim(),10),Y=(K[1]??"").trim();if(!Number.isFinite(Q)||Q<=0)console.error(`${z.red}PID 文件格式坏:${z.reset} ${Z}`),process.exit(1);if(Y&&!N(Q,Y)){console.log(`${z.yellow}PID ${Q} 上没有 daemon (stale 文件)${z.reset}`),console.log(`${z.dim}手删 ${Z} 后再试${z.reset}`);return}console.log(`${z.bold}停止 daemon${z.reset} (pid ${Q})...`);try{if(process.platform==="win32")f(`taskkill /PID ${Q} /T /F`,{stdio:"ignore"});else process.kill(Q,"SIGTERM")}catch($){console.error(`${z.red}发信号失败:${z.reset} ${$?.message??$}`),process.exit(1)}for(let $=0;$<50;$++){if(!U(Z)){console.log(`${z.green}✓ daemon 已停${z.reset}`);return}await P(100)}console.log(`${z.yellow}已发 SIGTERM, 但 5s 内 daemon 没清掉 ${Z}${z.reset}`),console.log(`${z.dim}它可能还在收尾 (落盘 alive marker / 关 WS); 下次启动 pid-guard 会自动判别。${z.reset}`)}u().catch((K)=>{console.error(`${z.red}lodestar-stop:${z.reset} ${K?.message??K}`),process.exit(1)});
|