@co0ontty/wand 4.91.1-beta.357.1.g3bfbf35 → 4.92.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 +26 -5
- package/dist/build-info.json +4 -4
- package/dist/cli.js +6 -0
- package/dist/core-runner.js +0 -7
- package/dist/core-status-cli.d.ts +2 -0
- package/dist/core-status-cli.js +13 -16
- package/dist/daemon-connection.d.ts +5 -0
- package/dist/daemon-connection.js +8 -0
- package/dist/decision-tool.js +2 -8
- package/dist/middleware/api-scope.js +1 -0
- package/dist/models/shared.d.ts +1 -1
- package/dist/models/shared.js +1 -3
- package/dist/npm-update-utils.d.ts +18 -1
- package/dist/npm-update-utils.js +153 -11
- package/dist/object-utils.d.ts +2 -0
- package/dist/object-utils.js +4 -0
- package/dist/pi-event-projection.d.ts +8 -0
- package/dist/pi-event-projection.js +28 -0
- package/dist/pi-model-discovery.js +1 -3
- package/dist/pi-native-event-bridge.d.ts +2 -0
- package/dist/pi-native-event-bridge.js +63 -0
- package/dist/pi-output-file.d.ts +44 -0
- package/dist/pi-output-file.js +159 -0
- package/dist/pi-protocol-reader.d.ts +33 -0
- package/dist/pi-protocol-reader.js +277 -0
- package/dist/pi-tmux-host.d.ts +44 -0
- package/dist/pi-tmux-host.js +385 -0
- package/dist/pi-tmux-worker.d.ts +1 -0
- package/dist/pi-tmux-worker.js +71 -0
- package/dist/render-daemon-client.js +2 -6
- package/dist/render-structured-client.js +2 -6
- package/dist/server-ai-team-routes.js +4 -14
- package/dist/server-employee-routes.js +5 -14
- package/dist/server-request.js +2 -1
- package/dist/server-session-context.js +1 -1
- package/dist/server-session-input-routes.js +1 -1
- package/dist/server-session-lifecycle-routes.js +2 -2
- package/dist/server-session-list-routes.js +3 -3
- package/dist/server-session-worktree-routes.js +3 -3
- package/dist/server-update-routes.d.ts +6 -1
- package/dist/server-update-routes.js +51 -26
- package/dist/server.js +12 -67
- package/dist/session-logger.d.ts +4 -0
- package/dist/session-logger.js +9 -0
- package/dist/session-service.js +4 -4
- package/dist/storage/mappers.d.ts +2 -1
- package/dist/storage/mappers.js +2 -3
- package/dist/storage/missions-repository.d.ts +1 -2
- package/dist/storage/missions-repository.js +1 -3
- package/dist/storage/preferences-repository.d.ts +1 -2
- package/dist/storage/preferences-repository.js +1 -3
- package/dist/structured-content.js +2 -3
- package/dist/structured-exec-host.d.ts +8 -0
- package/dist/structured-exec-pump.d.ts +10 -0
- package/dist/structured-exec-pump.js +88 -18
- package/dist/structured-pi-adapter.d.ts +5 -1
- package/dist/structured-pi-adapter.js +20 -2
- package/dist/structured-session-manager-replay.js +8 -0
- package/dist/structured-session-manager.d.ts +1 -5
- package/dist/structured-session-manager.js +103 -29
- package/dist/task-agent-parsing.d.ts +4 -0
- package/dist/task-agent-parsing.js +20 -0
- package/dist/team-dispatch.js +1 -4
- package/dist/terminal-daemon-client.js +2 -6
- package/dist/text-utils.d.ts +2 -0
- package/dist/text-utils.js +14 -0
- package/dist/tui/commands.js +0 -1
- package/dist/update-health.d.ts +7 -0
- package/dist/update-health.js +77 -0
- package/dist/update-helper.d.ts +17 -0
- package/dist/update-helper.js +222 -34
- package/dist/update-legacy.d.ts +5 -0
- package/dist/update-legacy.js +198 -0
- package/dist/update-repair-cli.d.ts +2 -0
- package/dist/update-repair-cli.js +17 -0
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chat-render-QXOBJZYF.js → 2548dbe0d0783577/chat-render-EODYMQF6.js} +12 -12
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-NRYBDZJQ.js → 2548dbe0d0783577/chunk-3TCUG766.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-AN34B6OX.js → 2548dbe0d0783577/chunk-47EVOSJS.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-A3PREDLN.js → 2548dbe0d0783577/chunk-A33YPCY6.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-LKQKEUHR.js → 2548dbe0d0783577/chunk-I3UYDQLS.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-4TWMU7ZU.js → 2548dbe0d0783577/chunk-QNUWUMKW.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-2SZRSM2C.js → 2548dbe0d0783577/chunk-WGADUEIT.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-4RVBPCZ2.js → 2548dbe0d0783577/chunk-ZTEQSXPA.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/employee-card-JRCEGKOW.js → 2548dbe0d0783577/employee-card-LJ3TX72B.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-HSG7HCKW.js → 2548dbe0d0783577/host-2PU6TV2I.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-XTI24R46.js → 2548dbe0d0783577/host-4E4H4JZJ.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-PULYRKUO.js → 2548dbe0d0783577/host-BPYNMKFC.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-ZTK4NJBZ.js → 2548dbe0d0783577/host-CXDAFMXX.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-O3FQO2WF.js → 2548dbe0d0783577/host-E3SCPOIU.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-VQKHZ4XY.js → 2548dbe0d0783577/host-HKXE2DOT.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-UHPSRXOS.js → 2548dbe0d0783577/host-HZIKPWYY.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-QKLN6BQT.js → 2548dbe0d0783577/host-ITV2QZ4A.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-EAFCE2FW.js → 2548dbe0d0783577/host-J5XRAQFP.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-TFWV4EL3.js → 2548dbe0d0783577/host-OGD6AWHL.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-3YSTASO7.js → 2548dbe0d0783577/host-OWD32FZ3.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-VSUTKITC.js → 2548dbe0d0783577/host-XNWC7KPR.js} +10 -10
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-TOKT2KEL.js → 2548dbe0d0783577/host-YELXZID4.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-bridge-YU4CDKM6.js → 2548dbe0d0783577/host-bridge-G5HEVADT.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/library-bridge-E4G3TTAM.js → 2548dbe0d0783577/library-bridge-Y3Y24CPQ.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/page-EL25SU2L.js → 2548dbe0d0783577/page-GQBUU7T7.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/panel-5FP5SRNS.js → 2548dbe0d0783577/panel-PG662OC5.js} +1 -1
- package/dist/web-ui/content/modules/2548dbe0d0783577/runtime.js +946 -0
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/task-board-host-EYEPEKBJ.js → 2548dbe0d0783577/task-board-host-RAN2Z2KR.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/workspace-agent-picker-TA2VJ2QW.js → 2548dbe0d0783577/workspace-agent-picker-FXLH6N5K.js} +1 -1
- package/dist/web-ui/content/modules/{d70a22b9c0aa5244/workspace-window-QDI7YABJ.js → 2548dbe0d0783577/workspace-window-HV4T3WKR.js} +1 -1
- package/dist/web-ui/embedded-assets.d.ts +27 -27
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +7 -1
- package/scripts/recover-web-update.mjs +262 -0
- package/scripts/recover-web-update.sh +123 -0
- package/scripts/update-node.sh +65 -0
- package/scripts/update-target.js +36 -0
- package/dist/web-ui/content/modules/d70a22b9c0aa5244/runtime.js +0 -923
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-LROINIFZ.js → 2548dbe0d0783577/chunk-C3K34WJD.js} +0 -0
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-PFPY4NTX.js → 2548dbe0d0783577/chunk-HK7RBU3M.js} +0 -0
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244/chunk-4OECFENI.js → 2548dbe0d0783577/chunk-NP35GDQ3.js} +0 -0
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244 → 2548dbe0d0783577}/chunk-XGVT4ZWT.js +0 -0
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244 → 2548dbe0d0783577}/chunk-YOXXNXOX.js +0 -0
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244 → 2548dbe0d0783577}/chunk-YX475LXO.js +0 -0
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244/host-5QOHKHNC.js → 2548dbe0d0783577/host-J6HBDK4A.js} +0 -0
- /package/dist/web-ui/content/modules/{d70a22b9c0aa5244 → 2548dbe0d0783577}/sha256-ANXV72UC.js +0 -0
package/README.md
CHANGED
|
@@ -68,6 +68,13 @@ wand config:password
|
|
|
68
68
|
- **Update channels** — built-in stable/beta update checks and upgrade prompts
|
|
69
69
|
- **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, Pi, and Gemini CLI from server settings, with optional automatic updates
|
|
70
70
|
|
|
71
|
+
Stable subscribers receive npm `latest`; Beta subscribers receive the newer of `latest`
|
|
72
|
+
and `beta`, including a stable release that supersedes its prerelease. Installing stable
|
|
73
|
+
keeps the Beta subscription enabled. Updates pin the checked version, wait for Core turns,
|
|
74
|
+
and restart only the Web service. If an old updater cannot run, rerun `install.sh` for a
|
|
75
|
+
staged recovery that preserves the previous installation and service scope. Node.js must
|
|
76
|
+
meet the new package's requirement. See [Web update and recovery](docs/web-updates.md).
|
|
77
|
+
|
|
71
78
|
### Configuration
|
|
72
79
|
|
|
73
80
|
The default config file is `~/.wand/config.json`; it is created by `wand init`.
|
|
@@ -251,16 +258,30 @@ npm install -g @google/gemini-cli@latest
|
|
|
251
258
|
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
252
259
|
- **版本管理** — 内置更新检查与升级提示,支持 stable/beta 双通道
|
|
253
260
|
|
|
254
|
-
Web 设置中的「Beta 通道」决定服务端 npm
|
|
255
|
-
`@co0ontty/wand@latest
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
Web 设置中的「Beta 通道」决定服务端 npm 更新候选:关闭时仅检查
|
|
262
|
+
`@co0ontty/wand@latest`,开启时同时检查 `latest` 与 `beta`,选择版本较新的一个;
|
|
263
|
+
同号正式版高于其 beta。「自动更新 Web 服务」独立决定是否自动安装。
|
|
264
|
+
检查后安装锁定具体版本,避免 dist-tag 在安装期间移动。Beta 用户安装正式版后
|
|
265
|
+
仍保留 Beta 订阅,下一次有更新的 beta 时继续收到更新。关闭 Beta 不会自动降级;
|
|
266
|
+
需要立即返回较旧正式版时,点击「更新或重新安装」。
|
|
267
|
+
|
|
268
|
+
手动与自动更新共用独立 helper:等待 Core 回合完成后安装,失败保留旧服务;
|
|
269
|
+
成功后只重启 Web 服务,Render/terminald 的会话继续运行。Web 更新不能升级 Node.js;
|
|
270
|
+
Node 版本不足会在改动安装前拒绝。页面同时核对新服务实例和目标版本后才刷新。
|
|
271
|
+
旧版本无法自更新时,重新执行本文的 `install.sh`:恢复流程先下载验证完整包,
|
|
272
|
+
保留旧安装,等待 Core 排空,再修复全局入口与原 service scope,支持残缺安装。
|
|
273
|
+
默认保留已保存的 Beta 订阅;切换 Node 后已有配置/数据库仍进入升级流程。
|
|
274
|
+
入口会复用兼容的已有 Node,必要时使用已配置 nvm 安装;npm 目录不可写时迁入用户可写的运行时目录,
|
|
275
|
+
后续更新始终绑定这份实际安装。新版也附带 `wand update:repair` 恢复命令,避免依赖 GitHub 可达。
|
|
276
|
+
可用 `WAND_UPDATE_CHANNEL=stable` 或 `beta` 指定本次恢复目标,
|
|
277
|
+
`WAND_CONFIG_PATH` 指定自定义配置路径。详细流程、失败原因与补救见 [Web 更新说明](docs/web-updates.md)。
|
|
258
278
|
|
|
259
279
|
正式 npm 发布由 `vX.Y.Z` Git tag 触发,只写入 `latest`,拒绝预发布版本。
|
|
260
280
|
master 的 Beta 发布只写入 `beta`,版本为下一补丁号加
|
|
261
281
|
`-beta.<构建序号>.<重试序号>.g<提交>`,例如 `4.89.5-beta.100.1.gabcdef0`。
|
|
262
282
|
版本基数同时检查正式 Git tags 和 npm latest,兼容未推送 Git 的本地正式发布。
|
|
263
|
-
Beta
|
|
283
|
+
Beta 标签缺失或无效时,Beta 订阅仍可接收有效正式版;正式通道始终不选择 beta。
|
|
284
|
+
两个候选都无效或 npm 不可达时不安装。
|
|
264
285
|
- **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI、Pi 和 Gemini CLI,也可开启自动更新
|
|
265
286
|
|
|
266
287
|
### 配置
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-10-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "f9fff36feba5836645f1eff3c7605c2d3c794755",
|
|
3
|
+
"builtAt": "2026-10-14T02:19:26.662Z",
|
|
4
|
+
"version": "4.92.0",
|
|
5
|
+
"channel": "stable"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -180,6 +180,11 @@ async function main() {
|
|
|
180
180
|
await runAgentCliCommand(command, args, configPath);
|
|
181
181
|
break;
|
|
182
182
|
}
|
|
183
|
+
case "update:repair": {
|
|
184
|
+
const { repairInstalledUpdate } = await import("./update-repair-cli.js");
|
|
185
|
+
process.exitCode = await repairInstalledUpdate(configPath);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
183
188
|
case "core:status": {
|
|
184
189
|
const { checkCoreStatus } = await import("./core-status-cli.js");
|
|
185
190
|
await checkCoreStatus(configPath);
|
|
@@ -297,6 +302,7 @@ Agent runtime:
|
|
|
297
302
|
|
|
298
303
|
System service (default = system-wide; pass --user for user-level):
|
|
299
304
|
wand service:install Register and start the background service (needs sudo for system)
|
|
305
|
+
wand update:repair Recover an old/broken installation while retaining data and update channel
|
|
300
306
|
wand service:uninstall Stop and remove the service
|
|
301
307
|
wand service:start Start the service
|
|
302
308
|
wand service:stop Stop the service
|
package/dist/core-runner.js
CHANGED
|
@@ -632,13 +632,6 @@ export function compactionSettings(config, defaults) {
|
|
|
632
632
|
keepRecentTokens: override?.keepRecentTokens ?? defaults.keepRecentTokens,
|
|
633
633
|
};
|
|
634
634
|
}
|
|
635
|
-
/** 一条只含文本的用户消息才算“真正的用户输入”:只在该边界切,保证 tool_use / tool_result 不被切开。 */
|
|
636
|
-
function currentUserTurnIndex(session, prompt) {
|
|
637
|
-
const turns = session.messages ?? [];
|
|
638
|
-
const last = turns.at(-1);
|
|
639
|
-
const text = last?.content?.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
640
|
-
return last?.role === "user" && text === prompt ? turns.length - 1 : turns.length;
|
|
641
|
-
}
|
|
642
635
|
function isUserTextTurn(turn) {
|
|
643
636
|
if (turn.role !== "user")
|
|
644
637
|
return false;
|
|
@@ -4,6 +4,8 @@ export interface CoreStatus {
|
|
|
4
4
|
activeTurnCount: number;
|
|
5
5
|
activeTurnIds?: string[];
|
|
6
6
|
}
|
|
7
|
+
/** A single request spends only the original overall deadline, never a fresh timeout. */
|
|
8
|
+
export declare function remainingCoreRequestBudget(deadline: number): number;
|
|
7
9
|
export declare function checkCoreStatus(configPath: string): Promise<void>;
|
|
8
10
|
/** Zero means wait indefinitely. A timeout/error NEVER runs the stop command. */
|
|
9
11
|
export declare function waitForCoreTurnsComplete(configPath: string, timeoutSeconds?: number, stopCommand?: string[], allowStopped?: boolean): Promise<number>;
|
package/dist/core-status-cli.js
CHANGED
|
@@ -141,17 +141,18 @@ async function readLegacyStatus(configPath, timeoutMs = 5000) {
|
|
|
141
141
|
});
|
|
142
142
|
return parseStatus(result);
|
|
143
143
|
}
|
|
144
|
+
/** A single request spends only the original overall deadline, never a fresh timeout. */
|
|
145
|
+
export function remainingCoreRequestBudget(deadline) {
|
|
146
|
+
const remaining = deadline - Date.now();
|
|
147
|
+
if (remaining <= 0)
|
|
148
|
+
throw new Error("等待 Core 回合超时,服务未停止,已取消重启。");
|
|
149
|
+
return Math.min(5000, remaining);
|
|
150
|
+
}
|
|
144
151
|
async function openControl(configPath, deadline = Infinity) {
|
|
145
152
|
const control = new CoreControl(configPath);
|
|
146
|
-
const budget = () => {
|
|
147
|
-
const remaining = deadline - Date.now();
|
|
148
|
-
if (remaining <= 0)
|
|
149
|
-
throw new Error("等待 Core 回合超时,服务未停止,已取消重启。");
|
|
150
|
-
return Math.min(5000, remaining);
|
|
151
|
-
};
|
|
152
153
|
try {
|
|
153
|
-
await control.connect(
|
|
154
|
-
await control.request("core-status",
|
|
154
|
+
await control.connect(remainingCoreRequestBudget(deadline));
|
|
155
|
+
await control.request("core-status", remainingCoreRequestBudget(deadline));
|
|
155
156
|
return control;
|
|
156
157
|
}
|
|
157
158
|
catch (error) {
|
|
@@ -183,12 +184,6 @@ export async function waitForCoreTurnsComplete(configPath, timeoutSeconds = 0, s
|
|
|
183
184
|
if (stopCommand && !stopCommand.length)
|
|
184
185
|
throw new Error("缺少等待完成后要执行的停止命令。");
|
|
185
186
|
const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
|
|
186
|
-
const budget = () => {
|
|
187
|
-
const remaining = deadline - Date.now();
|
|
188
|
-
if (remaining <= 0)
|
|
189
|
-
throw new Error("等待 Core 回合超时,服务未停止,已取消重启。");
|
|
190
|
-
return Math.min(5000, remaining);
|
|
191
|
-
};
|
|
192
187
|
const control = await openControl(configPath, deadline);
|
|
193
188
|
try {
|
|
194
189
|
const readStatus = async (budget = 5000) => {
|
|
@@ -222,7 +217,9 @@ export async function waitForCoreTurnsComplete(configPath, timeoutSeconds = 0, s
|
|
|
222
217
|
return { hasActiveTurns: false, activeTurnCount: 0 };
|
|
223
218
|
}
|
|
224
219
|
};
|
|
225
|
-
let status = control && stopCommand
|
|
220
|
+
let status = control && stopCommand
|
|
221
|
+
? await control.request("core-drain", remainingCoreRequestBudget(deadline))
|
|
222
|
+
: await readStatus(remainingCoreRequestBudget(deadline));
|
|
226
223
|
if (!control && stopCommand)
|
|
227
224
|
console.error("[wand] 旧服务尚不支持重启排空;正在读取真实服务状态。首次升级等待期间请勿启动新任务。");
|
|
228
225
|
let lastCount = -1;
|
|
@@ -235,7 +232,7 @@ export async function waitForCoreTurnsComplete(configPath, timeoutSeconds = 0, s
|
|
|
235
232
|
if (remaining <= 0)
|
|
236
233
|
throw new Error("等待 Core 回合超时,服务未停止,已取消重启。");
|
|
237
234
|
await new Promise((resolve) => setTimeout(resolve, Math.min(1000, remaining)));
|
|
238
|
-
status = await readStatus(
|
|
235
|
+
status = await readStatus(remainingCoreRequestBudget(deadline));
|
|
239
236
|
}
|
|
240
237
|
// The draining connection stays open until the stop command finishes.
|
|
241
238
|
// New Pi inputs are persisted in the queue, not started in the check/stop gap.
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type net from "node:net";
|
|
2
|
+
/** Reject this client's pending generation without changing its connection lifecycle. */
|
|
3
|
+
export declare function rejectPendingRequests<T extends {
|
|
4
|
+
timer: NodeJS.Timeout;
|
|
5
|
+
reject(error: Error): void;
|
|
6
|
+
}>(pending: Map<number, T>, error: Error): void;
|
|
2
7
|
/** Bound connect attempts, including sockets that close before the handshake. */
|
|
3
8
|
export declare function waitForDaemonSocket(socket: net.Socket): Promise<void>;
|
|
4
9
|
/** Probe the application, not just the fd: a half-open socket may still be writable. */
|
|
@@ -2,6 +2,14 @@ import { lstatSync, utimesSync } from "node:fs";
|
|
|
2
2
|
const CONNECT_TIMEOUT_MS = 10_000;
|
|
3
3
|
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
4
4
|
const HEARTBEAT_TIMEOUT_MS = 5_000;
|
|
5
|
+
/** Reject this client's pending generation without changing its connection lifecycle. */
|
|
6
|
+
export function rejectPendingRequests(pending, error) {
|
|
7
|
+
for (const request of pending.values()) {
|
|
8
|
+
clearTimeout(request.timer);
|
|
9
|
+
request.reject(error);
|
|
10
|
+
}
|
|
11
|
+
pending.clear();
|
|
12
|
+
}
|
|
5
13
|
/** Bound connect attempts, including sockets that close before the handshake. */
|
|
6
14
|
export function waitForDaemonSocket(socket) {
|
|
7
15
|
return new Promise((resolve, reject) => {
|
package/dist/decision-tool.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isRecord } from "./object-utils.js";
|
|
2
|
+
import { collapseWhitespaceAndClip as collapse } from "./text-utils.js";
|
|
1
3
|
/** core 会话里决策是进程内工具(`src/core-runner.ts`);CLI 会话是命令行调用。 */
|
|
2
4
|
export const DECISION_TOOL_NAME = "decision_evaluate";
|
|
3
5
|
/** Small shell lexer: quoted examples are one argument, not executable commands; heredoc bodies are data. */
|
|
@@ -116,14 +118,6 @@ const MODE_HINTS = {
|
|
|
116
118
|
choice: "选择", score: "评分", noul: "是非判断", mixed: "混合判断",
|
|
117
119
|
};
|
|
118
120
|
const GENERIC_HINT = "选择 / 评分 / 是非判断";
|
|
119
|
-
function isRecord(value) {
|
|
120
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
121
|
-
}
|
|
122
|
-
/** 折叠空白并截断:摘要永远单行、有界。 */
|
|
123
|
-
function collapse(text, max) {
|
|
124
|
-
const flat = text.replace(/\s+/g, " ").trim();
|
|
125
|
-
return flat.length > max ? `${flat.slice(0, Math.max(1, max - 1))}…` : flat;
|
|
126
|
-
}
|
|
127
121
|
function modeOf(types) {
|
|
128
122
|
const unique = new Set(types);
|
|
129
123
|
if (!unique.size)
|
|
@@ -18,6 +18,7 @@ export const API_ROUTE_RULES = [
|
|
|
18
18
|
{ prefix: "/api/connectors", scope: "admin" },
|
|
19
19
|
{ prefix: "/api/github", scope: "admin" },
|
|
20
20
|
{ prefix: "/api/update", scope: "admin" },
|
|
21
|
+
{ prefix: "/api/update-status", scope: "admin" },
|
|
21
22
|
{ prefix: "/api/update-channel", scope: "admin" },
|
|
22
23
|
{ prefix: "/api/check-update", scope: "admin" },
|
|
23
24
|
{ prefix: "/api/auto-update", scope: "admin" },
|
package/dist/models/shared.d.ts
CHANGED
|
@@ -15,4 +15,4 @@ export declare const PI_FALLBACK_MODELS: ClaudeModelInfo[];
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const GEMINI_FALLBACK_MODELS: ClaudeModelInfo[];
|
|
17
17
|
export declare function cloneModels(models: readonly ClaudeModelInfo[]): ClaudeModelInfo[];
|
|
18
|
-
export
|
|
18
|
+
export { isRecord } from "../object-utils.js";
|
package/dist/models/shared.js
CHANGED
|
@@ -20,18 +20,31 @@ export interface PackageUpdateInfo {
|
|
|
20
20
|
updateAvailable: boolean;
|
|
21
21
|
distTag: "latest" | "beta";
|
|
22
22
|
installSpec: string;
|
|
23
|
+
/** Candidate metadata is distinct from the user's persistent subscription. */
|
|
24
|
+
stableVersion?: string | null;
|
|
25
|
+
betaVersion?: string | null;
|
|
23
26
|
}
|
|
24
27
|
export declare function normalizeUpdateChannel(value: unknown): UpdateChannel;
|
|
25
28
|
export declare function getInstallSpecForChannel(channel: UpdateChannel): string;
|
|
26
29
|
export declare function buildPackageUpdateInfo(currentVersion: string, channel: UpdateChannel, latestVersion: string | null): PackageUpdateInfo;
|
|
30
|
+
export declare function selectPackageUpdateInfo(currentVersion: string, channel: UpdateChannel, stableVersion: string | null, betaVersion?: string | null): PackageUpdateInfo;
|
|
27
31
|
export declare function checkPackageUpdateAsync(currentVersion: string, channel: UpdateChannel, timeoutMs?: number): Promise<PackageUpdateInfo>;
|
|
28
32
|
export declare function checkPackageUpdateSync(currentVersion: string, channel: UpdateChannel, timeoutMs?: number): PackageUpdateInfo;
|
|
33
|
+
export declare function resolveInstalledNpmPrefix(moduleUrl?: string, platform?: NodeJS.Platform): {
|
|
34
|
+
root: string;
|
|
35
|
+
prefix: string;
|
|
36
|
+
} | null;
|
|
29
37
|
/**
|
|
30
38
|
* 更新时同时存在旧包备份、npm 正在解包的新版本和下载/回滚余量。
|
|
31
39
|
* 旧包大小按三倍计(安全备份、新包、回滚 staging),再保留 512 MiB,
|
|
32
40
|
* 避免 ENOSPC 把全局 CLI 拆成半包。
|
|
33
41
|
*/
|
|
34
42
|
export declare function requiredUpdateFreeBytes(currentInstallBytes: number): number;
|
|
43
|
+
export declare function assertNodeVersionForUpdate(range: string, current?: string): void;
|
|
44
|
+
export declare function checkGlobalUpdateAccess(): boolean;
|
|
45
|
+
export declare function isGlobalUpdateLocked(): boolean;
|
|
46
|
+
/** Only used in the independent helper process; holds the install lock through handoff. */
|
|
47
|
+
export declare function withGlobalUpdateLock<T>(action: () => Promise<T>): Promise<T>;
|
|
35
48
|
/**
|
|
36
49
|
* 异步版本的全局安装:
|
|
37
50
|
* 1. 清理残留
|
|
@@ -44,7 +57,11 @@ export declare function requiredUpdateFreeBytes(currentInstallBytes: number): nu
|
|
|
44
57
|
* @param log 可选 logger,用来把过程写入控制台或前端日志
|
|
45
58
|
* @returns 与本次安装使用同一个 npm root 校验出的 dist/cli.js 绝对路径
|
|
46
59
|
*/
|
|
47
|
-
export declare function installPackageGloballyAsync(pkg: string, timeoutMs: number, log?: (line: string) => void
|
|
60
|
+
export declare function installPackageGloballyAsync(pkg: string, timeoutMs: number, log?: (line: string) => void, options?: {
|
|
61
|
+
backupReceiptPath?: string;
|
|
62
|
+
}): Promise<string>;
|
|
63
|
+
/** Keep the old runtime until the new instance actually responds. */
|
|
64
|
+
export declare function finalizeDeferredGlobalUpdate(receiptPath: string, rollback?: boolean, log?: (line: string) => void): boolean;
|
|
48
65
|
/**
|
|
49
66
|
* 同步版本,给 TUI installUpdate 用。
|
|
50
67
|
*
|
package/dist/npm-update-utils.js
CHANGED
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { execFile, spawnSync } from "node:child_process";
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
|
-
import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, readdirSync, renameSync, rmSync, statfsSync, statSync, symlinkSync, } from "node:fs";
|
|
16
|
+
import { accessSync, constants as fsConstants, chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, readdirSync, renameSync, rmSync, statfsSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { promisify } from "node:util";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
20
21
|
import { buildChildEnv } from "./env-utils.js";
|
|
21
22
|
import { whichSync } from "./path-repair.js";
|
|
22
23
|
import { getErrorMessage } from "./error-utils.js";
|
|
23
|
-
import { compareWandInstallOrder, extractSemver } from "./version-utils.js";
|
|
24
|
+
import { compareSemver, compareWandInstallOrder, extractSemver } from "./version-utils.js";
|
|
24
25
|
import { acquireDeploymentLock, releaseDeploymentLock } from "./deployment-lock.js";
|
|
25
26
|
const execFileAsync = promisify(execFile);
|
|
26
27
|
export const PACKAGE_NAME = "@co0ontty/wand";
|
|
@@ -95,14 +96,48 @@ function viewPackageVersionSync(channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
|
|
|
95
96
|
const version = res.stdout.trim();
|
|
96
97
|
return version || null;
|
|
97
98
|
}
|
|
99
|
+
export function selectPackageUpdateInfo(currentVersion, channel, stableVersion, betaVersion = null) {
|
|
100
|
+
const stable = acceptedVersion(stableVersion, "stable");
|
|
101
|
+
const beta = channel === "beta" ? acceptedVersion(betaVersion, "beta") : null;
|
|
102
|
+
const useBeta = beta !== null && (stable === null || compareWandInstallOrder(beta, stable) > 0);
|
|
103
|
+
const info = buildPackageUpdateInfo(currentVersion, useBeta ? "beta" : "stable", useBeta ? beta : stable);
|
|
104
|
+
return { ...info, channel, stableVersion: stable, betaVersion: beta };
|
|
105
|
+
}
|
|
98
106
|
export async function checkPackageUpdateAsync(currentVersion, channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
|
|
99
|
-
const
|
|
100
|
-
|
|
107
|
+
const [stable, beta] = await Promise.all([
|
|
108
|
+
viewPackageVersionAsync("stable", timeoutMs),
|
|
109
|
+
channel === "beta" ? viewPackageVersionAsync("beta", timeoutMs) : null,
|
|
110
|
+
]);
|
|
111
|
+
return selectPackageUpdateInfo(currentVersion, channel, stable, beta);
|
|
101
112
|
}
|
|
102
113
|
export function checkPackageUpdateSync(currentVersion, channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
|
|
103
|
-
const
|
|
104
|
-
|
|
114
|
+
const stable = viewPackageVersionSync("stable", timeoutMs);
|
|
115
|
+
const beta = channel === "beta" ? viewPackageVersionSync("beta", timeoutMs) : null;
|
|
116
|
+
return selectPackageUpdateInfo(currentVersion, channel, stable, beta);
|
|
117
|
+
}
|
|
118
|
+
export function resolveInstalledNpmPrefix(moduleUrl = import.meta.url, platform = process.platform) {
|
|
119
|
+
try {
|
|
120
|
+
const packageDir = path.dirname(path.dirname(fileURLToPath(moduleUrl)));
|
|
121
|
+
let scope = path.dirname(packageDir);
|
|
122
|
+
// Rollback may briefly boot through the recovery shim. Its retained backup
|
|
123
|
+
// still belongs to the original npm prefix, not the shell's default prefix.
|
|
124
|
+
if (/^\.wand-safe-backup-[A-Za-z0-9]+$/.test(path.basename(scope)))
|
|
125
|
+
scope = path.dirname(scope);
|
|
126
|
+
const root = path.dirname(scope);
|
|
127
|
+
if (path.basename(packageDir) !== PACKAGE_BASENAME || path.basename(scope) !== PACKAGE_SCOPE
|
|
128
|
+
|| path.basename(root) !== "node_modules"
|
|
129
|
+
|| JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8")).name !== PACKAGE_NAME)
|
|
130
|
+
return null;
|
|
131
|
+
const parent = path.dirname(root);
|
|
132
|
+
if (platform !== "win32" && path.basename(parent) !== "lib")
|
|
133
|
+
return null;
|
|
134
|
+
return { root, prefix: platform === "win32" ? parent : path.dirname(parent) };
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
105
139
|
}
|
|
140
|
+
const INSTALLED_NPM_BINDING = resolveInstalledNpmPrefix();
|
|
106
141
|
function getChildEnv() {
|
|
107
142
|
const entries = [
|
|
108
143
|
path.dirname(process.execPath),
|
|
@@ -120,6 +155,7 @@ function getChildEnv() {
|
|
|
120
155
|
return {
|
|
121
156
|
...buildChildEnv(true),
|
|
122
157
|
PATH: pathEntries.join(path.delimiter),
|
|
158
|
+
...(INSTALLED_NPM_BINDING ? { npm_config_prefix: INSTALLED_NPM_BINDING.prefix } : {}),
|
|
123
159
|
};
|
|
124
160
|
}
|
|
125
161
|
function runNpmSync(args, timeoutMs) {
|
|
@@ -143,6 +179,8 @@ async function runNpmAsync(args, timeoutMs) {
|
|
|
143
179
|
* 解析当前 `npm root -g` 的目录。失败返回 null。
|
|
144
180
|
*/
|
|
145
181
|
function getNpmGlobalRoot() {
|
|
182
|
+
if (INSTALLED_NPM_BINDING)
|
|
183
|
+
return INSTALLED_NPM_BINDING.root;
|
|
146
184
|
try {
|
|
147
185
|
const res = runNpmSync(["root", "-g"], 10_000);
|
|
148
186
|
if (res.status !== 0)
|
|
@@ -155,6 +193,8 @@ function getNpmGlobalRoot() {
|
|
|
155
193
|
}
|
|
156
194
|
}
|
|
157
195
|
function getNpmGlobalPrefix() {
|
|
196
|
+
if (INSTALLED_NPM_BINDING)
|
|
197
|
+
return INSTALLED_NPM_BINDING.prefix;
|
|
158
198
|
try {
|
|
159
199
|
const res = runNpmSync(["prefix", "-g"], 10_000);
|
|
160
200
|
if (res.status !== 0)
|
|
@@ -584,18 +624,71 @@ function restoreGlobalInstallBackup(backup, note) {
|
|
|
584
624
|
return false;
|
|
585
625
|
}
|
|
586
626
|
}
|
|
627
|
+
export function assertNodeVersionForUpdate(range, current = process.versions.node) {
|
|
628
|
+
const minimum = range.trim().match(/^>=(\d+\.\d+\.\d+)$/)?.[1];
|
|
629
|
+
if (minimum && compareSemver(current, minimum) < 0) {
|
|
630
|
+
throw new Error(`新版本要求 Node.js >= ${minimum}(当前 ${current})。请先升级 Node.js,再执行 README 的 install.sh 恢复更新;旧安装未修改。`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function checkUpdateNodeVersion(pkg, timeoutMs) {
|
|
634
|
+
// Registry metadata is checked before creating a backup or replacing any files.
|
|
635
|
+
// npm --engine-strict remains the authority for other engine range formats.
|
|
636
|
+
const { stdout } = await execFileAsync(npmBin(), ["view", pkg, "engines.node"], {
|
|
637
|
+
timeout: Math.min(timeoutMs, NPM_VIEW_TIMEOUT_MS), env: getChildEnv(), maxBuffer: INSTALL_MAX_BUFFER,
|
|
638
|
+
});
|
|
639
|
+
assertNodeVersionForUpdate(String(stdout));
|
|
640
|
+
}
|
|
587
641
|
async function npmInstallGlobalAsync(pkg, timeoutMs, extra = []) {
|
|
588
|
-
await runNpmAsync(["install", "-g", ...extra, pkg], timeoutMs);
|
|
642
|
+
await runNpmAsync(["install", "-g", "--engine-strict", ...extra, pkg], timeoutMs);
|
|
589
643
|
}
|
|
590
644
|
function isRecoverableInstallError(message) {
|
|
591
645
|
return /ENOTEMPTY|EEXIST|全局 wand 安装不完整|无法解析 npm 全局安装目录|全局 wand CLI 无法设置执行权限|全局 wand 命令入口缺失/.test(message);
|
|
592
646
|
}
|
|
647
|
+
export function checkGlobalUpdateAccess() {
|
|
648
|
+
const root = getNpmGlobalRoot();
|
|
649
|
+
const prefix = getNpmGlobalPrefix();
|
|
650
|
+
if (!root || !prefix)
|
|
651
|
+
return false;
|
|
652
|
+
try {
|
|
653
|
+
for (const dir of [root, path.join(root, PACKAGE_SCOPE), path.join(prefix, "bin")]) {
|
|
654
|
+
accessSync(nearestExistingAncestor(dir), fsConstants.W_OK);
|
|
655
|
+
}
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
export function isGlobalUpdateLocked() {
|
|
663
|
+
const root = getNpmGlobalRoot();
|
|
664
|
+
return !!root && existsSync(path.join(root, ".wand-update-lock"));
|
|
665
|
+
}
|
|
593
666
|
function acquireGlobalUpdateLock() {
|
|
594
667
|
const root = getNpmGlobalRoot();
|
|
595
668
|
if (!root)
|
|
596
669
|
throw new Error("无法解析 npm 全局安装目录,不能建立更新锁。");
|
|
597
670
|
mkdirSync(path.join(root, PACKAGE_SCOPE), { recursive: true });
|
|
598
|
-
|
|
671
|
+
const inherited = process.env.WAND_DEPLOYMENT_LOCKS
|
|
672
|
+
? JSON.parse(process.env.WAND_DEPLOYMENT_LOCKS) : [];
|
|
673
|
+
if (!Array.isArray(inherited))
|
|
674
|
+
throw new Error("无效的部署锁回执。");
|
|
675
|
+
return acquireDeploymentLock(path.join(root, ".wand-update-lock"), { inherited });
|
|
676
|
+
}
|
|
677
|
+
/** Only used in the independent helper process; holds the install lock through handoff. */
|
|
678
|
+
export async function withGlobalUpdateLock(action) {
|
|
679
|
+
const lock = acquireGlobalUpdateLock();
|
|
680
|
+
const previous = process.env.WAND_DEPLOYMENT_LOCKS;
|
|
681
|
+
try {
|
|
682
|
+
process.env.WAND_DEPLOYMENT_LOCKS = JSON.stringify([lock]);
|
|
683
|
+
return await action();
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
if (previous === undefined)
|
|
687
|
+
delete process.env.WAND_DEPLOYMENT_LOCKS;
|
|
688
|
+
else
|
|
689
|
+
process.env.WAND_DEPLOYMENT_LOCKS = previous;
|
|
690
|
+
releaseGlobalUpdateLock(lock);
|
|
691
|
+
}
|
|
599
692
|
}
|
|
600
693
|
function releaseGlobalUpdateLock(lock) {
|
|
601
694
|
releaseDeploymentLock(lock);
|
|
@@ -612,11 +705,12 @@ function releaseGlobalUpdateLock(lock) {
|
|
|
612
705
|
* @param log 可选 logger,用来把过程写入控制台或前端日志
|
|
613
706
|
* @returns 与本次安装使用同一个 npm root 校验出的 dist/cli.js 绝对路径
|
|
614
707
|
*/
|
|
615
|
-
export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
|
|
708
|
+
export async function installPackageGloballyAsync(pkg, timeoutMs, log, options = {}) {
|
|
616
709
|
const note = (line) => {
|
|
617
710
|
if (log)
|
|
618
711
|
log(line);
|
|
619
712
|
};
|
|
713
|
+
await checkUpdateNodeVersion(pkg, timeoutMs);
|
|
620
714
|
const updateLock = acquireGlobalUpdateLock();
|
|
621
715
|
let backup = null;
|
|
622
716
|
let success = false;
|
|
@@ -683,7 +777,11 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
|
|
|
683
777
|
}
|
|
684
778
|
}
|
|
685
779
|
// 恢复失败时绝不能再删唯一完整备份,留给人工恢复。
|
|
686
|
-
if (backup &&
|
|
780
|
+
if (backup && success && options.backupReceiptPath) {
|
|
781
|
+
writeFileSync(options.backupReceiptPath, JSON.stringify(backup), { mode: 0o600 });
|
|
782
|
+
note(`[wand] 旧安装备份保留到启动验证完成: ${backup.backupDir ?? "首次安装"}`);
|
|
783
|
+
}
|
|
784
|
+
else if (backup && (success || restored))
|
|
687
785
|
cleanupGlobalInstallBackup(backup);
|
|
688
786
|
}
|
|
689
787
|
finally {
|
|
@@ -691,6 +789,46 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
|
|
|
691
789
|
}
|
|
692
790
|
}
|
|
693
791
|
}
|
|
792
|
+
function readDeferredGlobalInstallBackup(receiptPath) {
|
|
793
|
+
const value = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
794
|
+
const packageDir = getGlobalPackageDir();
|
|
795
|
+
if (!packageDir || value.packageDir !== packageDir || !Array.isArray(value.binEntries)) {
|
|
796
|
+
throw new Error("更新备份回执与当前安装不一致。");
|
|
797
|
+
}
|
|
798
|
+
if (value.backupRoot === null && value.backupDir === null && value.binEntries.length === 0)
|
|
799
|
+
return value;
|
|
800
|
+
if (typeof value.backupRoot !== "string" || path.dirname(value.backupRoot) !== path.dirname(packageDir)
|
|
801
|
+
|| !/^\.wand-safe-backup-[A-Za-z0-9]+$/.test(path.basename(value.backupRoot))
|
|
802
|
+
|| value.backupDir !== path.join(value.backupRoot, PACKAGE_BASENAME)
|
|
803
|
+
|| !lstatSync(value.backupRoot).isDirectory() || lstatSync(value.backupRoot).isSymbolicLink()) {
|
|
804
|
+
throw new Error("更新备份回执无效。");
|
|
805
|
+
}
|
|
806
|
+
const expectedBins = new Set(getGlobalWandBinPaths());
|
|
807
|
+
for (const entry of value.binEntries) {
|
|
808
|
+
if (!expectedBins.has(entry.originalPath) || path.dirname(entry.backupPath) !== path.join(value.backupRoot, "bin")) {
|
|
809
|
+
throw new Error("更新入口备份回执无效。");
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return value;
|
|
813
|
+
}
|
|
814
|
+
/** Keep the old runtime until the new instance actually responds. */
|
|
815
|
+
export function finalizeDeferredGlobalUpdate(receiptPath, rollback = false, log) {
|
|
816
|
+
const lock = acquireGlobalUpdateLock();
|
|
817
|
+
try {
|
|
818
|
+
const backup = readDeferredGlobalInstallBackup(receiptPath);
|
|
819
|
+
if (rollback && !restoreGlobalInstallBackup(backup, log))
|
|
820
|
+
return false;
|
|
821
|
+
// A manager may already be starting the old runtime through the recovery
|
|
822
|
+
// shim. Retain that backup so its late imports remain available.
|
|
823
|
+
if (!rollback)
|
|
824
|
+
cleanupGlobalInstallBackup(backup);
|
|
825
|
+
rmSync(receiptPath, { force: true });
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
finally {
|
|
829
|
+
releaseGlobalUpdateLock(lock);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
694
832
|
/**
|
|
695
833
|
* 同步版本,给 TUI installUpdate 用。
|
|
696
834
|
*
|
|
@@ -701,6 +839,10 @@ export function installPackageGloballySync(pkg, timeoutMs) {
|
|
|
701
839
|
const backupNotes = [];
|
|
702
840
|
let updateLock;
|
|
703
841
|
try {
|
|
842
|
+
const metadata = runNpmSync(["view", pkg, "engines.node"], Math.min(timeoutMs, NPM_VIEW_TIMEOUT_MS));
|
|
843
|
+
if (metadata.status === 0)
|
|
844
|
+
assertNodeVersionForUpdate(metadata.stdout);
|
|
845
|
+
// Fixtures/old registries may not expose engines; --engine-strict still checks installation.
|
|
704
846
|
updateLock = acquireGlobalUpdateLock();
|
|
705
847
|
}
|
|
706
848
|
catch (err) {
|
|
@@ -732,7 +874,7 @@ export function installPackageGloballySync(pkg, timeoutMs) {
|
|
|
732
874
|
};
|
|
733
875
|
};
|
|
734
876
|
const tryInstall = (extra) => {
|
|
735
|
-
const args = ["install", "-g", ...extra, pkg];
|
|
877
|
+
const args = ["install", "-g", "--engine-strict", ...extra, pkg];
|
|
736
878
|
attempts.push(`npm ${args.join(" ")}`);
|
|
737
879
|
const r = runNpmSync(args, timeoutMs);
|
|
738
880
|
return withValidation({
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Fields consumed by Wand's Pi reducer. Context/entries are CLI-owned history,
|
|
2
|
+
* not new output. Never journal repeated copies of that history in native mode. */
|
|
3
|
+
export declare const PI_PROTOCOL_FIELDS: Set<string>;
|
|
4
|
+
/** agent_end is a reconciliation snapshot, not a second replay of every turn.
|
|
5
|
+
* The latest assistant owns the final answer, usage and stop/error facts. This
|
|
6
|
+
* also supports CLIs that emit only the final snapshot (without deltas). */
|
|
7
|
+
export declare function lastPiAssistantMessage(event: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
8
|
+
export declare function projectPiProtocolEvent(event: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Fields consumed by Wand's Pi reducer. Context/entries are CLI-owned history,
|
|
2
|
+
* not new output. Never journal repeated copies of that history in native mode. */
|
|
3
|
+
export const PI_PROTOCOL_FIELDS = new Set([
|
|
4
|
+
"type", "id", "message", "assistantMessageEvent", "toolCallId", "toolName",
|
|
5
|
+
"args", "result", "partialResult", "isError", "messages",
|
|
6
|
+
]);
|
|
7
|
+
/** agent_end is a reconciliation snapshot, not a second replay of every turn.
|
|
8
|
+
* The latest assistant owns the final answer, usage and stop/error facts. This
|
|
9
|
+
* also supports CLIs that emit only the final snapshot (without deltas). */
|
|
10
|
+
export function lastPiAssistantMessage(event) {
|
|
11
|
+
const messages = event.type === "agent_end" && Array.isArray(event.messages) ? event.messages
|
|
12
|
+
: event.type === "message_end" || event.type === "turn_end" ? [event.message] : [];
|
|
13
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
14
|
+
const message = messages[index];
|
|
15
|
+
if (message && typeof message === "object" && !Array.isArray(message)
|
|
16
|
+
&& message.role === "assistant")
|
|
17
|
+
return message;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
export function projectPiProtocolEvent(event) {
|
|
22
|
+
const projected = Object.fromEntries(Object.entries(event).filter(([key]) => PI_PROTOCOL_FIELDS.has(key)));
|
|
23
|
+
if (event.type === "agent_end" && Array.isArray(event.messages)) {
|
|
24
|
+
const last = lastPiAssistantMessage(event);
|
|
25
|
+
projected.messages = last ? [last] : [];
|
|
26
|
+
}
|
|
27
|
+
return projected;
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isRecord } from "./object-utils.js";
|
|
4
5
|
const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
|
|
5
6
|
const SUPPORTED_APIS = new Set([
|
|
6
7
|
"anthropic-messages",
|
|
@@ -12,9 +13,6 @@ const MAX_PAGES_PER_ENDPOINT = 40;
|
|
|
12
13
|
const MAX_MODELS_PER_ENDPOINT = 2000;
|
|
13
14
|
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
14
15
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
15
|
-
function isRecord(value) {
|
|
16
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
17
|
-
}
|
|
18
16
|
function resolveConfigValue(value, env) {
|
|
19
17
|
if (typeof value !== "string")
|
|
20
18
|
return null;
|