agentus 0.3.3 → 0.3.5
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 +18 -50
- package/README.zh-CN.md +12 -42
- package/bin/agentus.mjs +4 -4
- package/docs/dev/01-dev-loop.md +1 -2
- package/docs/dev/02-verify.md +0 -1
- package/docs/dev/04-recover.md +6 -26
- package/docs/dev/05-agent-driven.md +1 -1
- package/docs/dev/README.md +4 -5
- package/docs/installation.md +76 -30
- package/docs/installation.zh-CN.md +68 -29
- package/package.json +8 -3
- package/packages/server/mock/agent.mjs +38 -0
- package/packages/server/src/acp/registry.ts +28 -1
- package/packages/server/src/acp/session-manager.ts +192 -60
- package/packages/server/src/index.ts +44 -64
- package/packages/server/src/notify/center.ts +118 -26
- package/packages/server/src/notify/types.ts +25 -0
- package/packages/server/src/settings.ts +46 -0
- package/packages/shared/src/index.ts +40 -0
- package/packages/web/dist/assets/index-DQdijjaT.css +32 -0
- package/packages/web/dist/assets/index-eHg6Cshr.js +119 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +39 -0
- package/packages/web/dist/assets/index-CCvfL56d.css +0 -32
- package/packages/web/dist/assets/index-DCBs8dsw.js +0 -119
- package/scripts/make-cert.sh +0 -101
|
@@ -28,6 +28,57 @@ nvm install 22 && nvm use 22 # 或者
|
|
|
28
28
|
brew install node # 或者 https://nodejs.org/en/download
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
### 该跑哪份 Hermes
|
|
32
|
+
|
|
33
|
+
裸装的 Hermes 足够跑会话。但驾驶舱自己依赖的那几项能力**不在发布版里** —— 按会话切换的**思考深度**
|
|
34
|
+
(ACP `reasoning_effort`)、**跨重启 / `session/load` 存活的权限模式**,以及可自由填写的**上下文预算**
|
|
35
|
+
(带 256K/512K/768K/1M 预设)。它们在 fork
|
|
36
|
+
[`LiangYang666/hermes-agent`](https://github.com/LiangYang666/hermes-agent) 的 ACP 分支上,
|
|
37
|
+
按基座版本各有一个**集成构建的 tag**,自成命名空间:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
agentus/<hermes 内部版本>/merge-<YYYYMMDD-HHMM>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
用 tag 而不是分支是刻意的:同名分支(在 `yl-dev/` 下)是工作面、会被重建,而 tag 永不移动 —— 所以钉
|
|
44
|
+
tag。`agentus/` 这个前缀也把它和上游自己的 tag(`v0.21.7`、`v0.21.7+canary.*`)分开;最后一段是时间戳,
|
|
45
|
+
可排序,所以「最新」就是一次 `sort -V`。
|
|
46
|
+
|
|
47
|
+
先正常装好 Hermes(它的安装器会带上 Python/Node 运行时,并把 `hermes` 放进 PATH),再把那份构建
|
|
48
|
+
套在那个运行时上跑:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 1. Hermes 本体:槽位要用的运行时
|
|
52
|
+
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
|
|
53
|
+
|
|
54
|
+
# 2. 最新的集成构建,按 tag 取(写全 refs/tags/:永不歧义)
|
|
55
|
+
TAG=$(git ls-remote --tags --refs https://github.com/LiangYang666/hermes-agent.git \
|
|
56
|
+
'refs/tags/agentus/*' | awk '{print $2}' | sed 's#refs/tags/##' | sort -V | tail -1)
|
|
57
|
+
git clone --depth=1 --branch "$TAG" https://github.com/LiangYang666/hermes-agent.git ~/Project/hermes-agent
|
|
58
|
+
|
|
59
|
+
# 3. 包一层,让驾驶舱指向这个 wrapper
|
|
60
|
+
mkdir -p ~/.local/bin
|
|
61
|
+
cat > ~/.local/bin/hermes-acp-src <<'WRAP'
|
|
62
|
+
#!/bin/sh
|
|
63
|
+
exec env PYTHONPATH="$HOME/Project/hermes-agent" hermes "$@"
|
|
64
|
+
WRAP
|
|
65
|
+
chmod +x ~/.local/bin/hermes-acp-src
|
|
66
|
+
AGENTUS_HERMES_CMD=~/.local/bin/hermes-acp-src npm start
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`PYTHONPATH` 的作用是让这份 checkout **盖在**已安装的包前面,而解释器和依赖仍由装好的运行时提供 ——
|
|
70
|
+
所以第 1 步不能省。验证槽位实际会跑什么(就是 Agentus 缺后端时提示的那条自检):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
PYTHONPATH=~/Project/hermes-agent hermes acp --check # → Hermes ACP check OK
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
装成服务时,把 `AGENTUS_HERMES_CMD` 放进你交给服务端的环境里 —— 见「作为服务常驻」。保持默认
|
|
77
|
+
(`hermes`)就是一份纯安装,所以 fork 是升级项、不是必须项。
|
|
78
|
+
|
|
79
|
+
换到更新的构建只是在那棵树里 fetch + checkout(`git fetch --depth=1 origin tag <tag> && git checkout <tag>`),
|
|
80
|
+
不用重装;正在跑的槽位会在下次重启时用上。
|
|
81
|
+
|
|
31
82
|
## 安装
|
|
32
83
|
|
|
33
84
|
### 一条命令 —— 用 npm 上发布的包(对外)
|
|
@@ -89,7 +140,7 @@ npm run agentus # 与 `npx agentus` 同一个 CLI,直接
|
|
|
89
140
|
agentus [options]
|
|
90
141
|
|
|
91
142
|
-p, --port <n> 监听端口 (AGENTUS_PORT,默认 8788)
|
|
92
|
-
-d, --data <dir> 状态目录(sqlite
|
|
143
|
+
-d, --data <dir> 状态目录(sqlite、登录、设置)
|
|
93
144
|
(AGENTUS_DATA,默认 ~/.agentus)
|
|
94
145
|
-o, --open 就绪后自动在浏览器打开驾驶舱
|
|
95
146
|
--where 打印解析出的路径后退出(不启动任何东西)
|
|
@@ -97,8 +148,8 @@ agentus [options]
|
|
|
97
148
|
-v, --version 打印版本
|
|
98
149
|
```
|
|
99
150
|
|
|
100
|
-
`--where` 值得记住:它会打印服务端**实际**解析出的路径 —— 数据目录、前端产物、随附 APK、
|
|
101
|
-
|
|
151
|
+
`--where` 值得记住:它会打印服务端**实际**解析出的路径 —— 数据目录、前端产物、随附 APK、
|
|
152
|
+
Node 版本 —— 然后退出,什么都不启动。它是「我的数据去哪了」的唯一权威答案(见下文)。
|
|
102
153
|
|
|
103
154
|
## 首次运行
|
|
104
155
|
|
|
@@ -132,9 +183,6 @@ agentus [options]
|
|
|
132
183
|
| `AGENTUS_HERMES_HOME` | `~/.hermes` | 未在行里指定 home 时的默认 `HERMES_HOME` |
|
|
133
184
|
| `AGENTUS_PERM_TIMEOUT_MS` | `300000`(5 分钟) | 权限提示自动取消前的等待时间 |
|
|
134
185
|
| `AGENTUS_HISTORY_PAGE` | `500` | 对话记录分页大小 |
|
|
135
|
-
| `AGENTUS_TLS_PORT` | 有证书时 `8443`,否则关闭 | TLS 监听端口(`0` = 关闭) |
|
|
136
|
-
| `AGENTUS_TLS_CERT` / `AGENTUS_TLS_KEY` | `<DATA_DIR>/tls/{cert,key}.pem` | 该监听用的证书材料 |
|
|
137
|
-
| `AGENTUS_TLS_CA` | `<DATA_DIR>/tls/ca.pem` | 设备只需装一次的根证书 |
|
|
138
186
|
| `AGENTUS_TTS_BASE_URL` | 未设置 | 服务端语音合成的 OpenAI 兼容 base(`…/v1`);不设 = 只用浏览器语音 |
|
|
139
187
|
| `AGENTUS_TTS_API_KEY` / `AGENTUS_TTS_MODEL` / `AGENTUS_TTS_VOICE` | – / `tts-1` / `alloy` | 同上 |
|
|
140
188
|
| `AGENTUS_STT_BASE_URL` | 未设置 | 服务端语音识别的 OpenAI 兼容 base;不设 = 只用浏览器识别 |
|
|
@@ -161,7 +209,7 @@ agentus [options]
|
|
|
161
209
|
### 数据目录
|
|
162
210
|
|
|
163
211
|
默认 `~/.agentus`。里面有 `agentus.sqlite`(会话 + 记录)、`credentials.json`、`auth.token`、
|
|
164
|
-
`auth.secret`、`
|
|
212
|
+
`auth.secret`、`settings.json`。首次启动时以 `0700` 创建,启动日志会打印它**以及为什么是
|
|
165
213
|
这个目录胜出**:
|
|
166
214
|
|
|
167
215
|
| 顺序 | 规则 |
|
|
@@ -175,14 +223,13 @@ agentus [options]
|
|
|
175
223
|
会打印服务端解析的结果,不用靠猜。
|
|
176
224
|
|
|
177
225
|
任何东西都不会写进 `node_modules`:对已安装的包来说,`<pkg>/packages/server/.data` 会被下一次
|
|
178
|
-
`npm i -g agentus`
|
|
226
|
+
`npm i -g agentus` 删掉,连带记录和登录一起没了。
|
|
179
227
|
|
|
180
228
|
### 端口
|
|
181
229
|
|
|
182
230
|
| 端口 | 协议 | 用途 |
|
|
183
231
|
|---|---|---|
|
|
184
|
-
| `AGENTUS_PORT`(8788) | 纯 HTTP | 局域网、本机回环、`curl`、脚本 —— 无证书警告,CI
|
|
185
|
-
| `AGENTUS_TLS_PORT`(8443) | HTTPS(自签) | **隧道** —— 加密公网那一跳 |
|
|
232
|
+
| `AGENTUS_PORT`(8788) | 纯 HTTP | 局域网、本机回环、`curl`、脚本 —— 无证书警告,CI 不变;**唯一的监听** |
|
|
186
233
|
| 5173 | Vite 开发服务器 | 前端热更新,仅在 `npm run dev` 期间 |
|
|
187
234
|
|
|
188
235
|
两个监听的 handler、路由、鉴权完全相同,只有 socket 不同。
|
|
@@ -202,24 +249,15 @@ agentus [options]
|
|
|
202
249
|
|
|
203
250
|
### TLS
|
|
204
251
|
|
|
205
|
-
纯 TCP 隧道(SakuraFrp、`ssh -L`、多数 frp 配置)只是转发字节 —— 它**不会**替你终结 TLS
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
scripts/make-cert.sh # 自签根证书 + 短期叶证书,SAN = 你实际输入的名字
|
|
210
|
-
# -> $AGENTUS_DATA/tls/{ca.pem,cert.pem,key.pem} (key 0600,已被 git 忽略)
|
|
211
|
-
# 启动日志会写:https://0.0.0.0:8443 (self-signed …) ; 把隧道指向这个端口
|
|
212
|
-
```
|
|
252
|
+
纯 TCP 隧道(SakuraFrp、`ssh -L`、多数 frp 配置)只是转发字节 —— 它**不会**替你终结 TLS。所以要
|
|
253
|
+
在服务端**前面**终结 TLS:放一层反代(Caddy / nginx),或者用自带 `public_tls` 的隧道。
|
|
213
254
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
(设置 → 安全 → CA 证书);iOS 还需要再到 *关于本机 → 证书信任设置 → 启用*。这是公开材料 ——
|
|
217
|
-
私钥永远不会离开 `<DATA_DIR>/tls/`。
|
|
255
|
+
本进程自己只讲纯 HTTP,刻意如此:LAN 这条路不需要证书,`curl`/脚本保持不变;证书的生命周期(签发、
|
|
256
|
+
轮换、SAN)留在前置层里,而不是塞进应用。
|
|
218
257
|
|
|
219
|
-
|
|
220
|
-
(`
|
|
221
|
-
|
|
222
|
-
设备不受影响。
|
|
258
|
+
走 TLS 到达的请求,session cookie 照样是 `Secure`:handler 读 `X-Forwarded-Proto`
|
|
259
|
+
(`packages/server/src/auth.ts`),凡设置了这个头的前置层(Caddy 和 nginx 默认都设)都会拿到
|
|
260
|
+
`Secure` cookie,本地无需任何配置。
|
|
223
261
|
|
|
224
262
|
### 后端
|
|
225
263
|
|
|
@@ -314,7 +352,7 @@ npm i -g agentus@latest # 或者:npx agentus@latest … 跑一次
|
|
|
314
352
|
|
|
315
353
|
```bash
|
|
316
354
|
npm rm -g agentus # 删程序(状态保留)
|
|
317
|
-
rm -rf ~/.agentus # 删状态 —— SQLite
|
|
355
|
+
rm -rf ~/.agentus # 删状态 —— SQLite、登录、设置
|
|
318
356
|
```
|
|
319
357
|
|
|
320
358
|
装到用户级前缀的话用 `npm rm -g --prefix ~/.local agentus`。卸载**不会**碰 `~/.hermes`(你 agent
|
|
@@ -346,5 +384,6 @@ rm -rf ~/.agentus # 删状态 —— SQLite、登录、TLS 密
|
|
|
346
384
|
**页面能打开但一直显示「reconnecting」。** 某条隧道丢掉了 WebSocket 的 `Upgrade`。实时流走
|
|
347
385
|
`/ws`;请换成能透传 WebSocket 的隧道(SakuraFrp 的 TCP+auto-HTTPS 隧道已验证可用)。
|
|
348
386
|
|
|
349
|
-
**局域网里出现证书警告。**
|
|
350
|
-
|
|
387
|
+
**局域网里出现证书警告。** 服务端自己只讲纯 HTTP(`:8788`),所以出现警告说明你是用 IP 直接
|
|
388
|
+
访问了终结 TLS 的那层前置(它的证书签的是域名,不是 IP)。要么用纯 HTTP 的 `:8788`,要么用证书
|
|
389
|
+
签发的那个名字去访问前置层。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentus",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Multi-session web cockpit for ACP-speaking coding agents (Qoder CLI / Hermes). UI has no intelligence — the agent lives in spawned CLI subprocesses.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"packages/shared/src/",
|
|
24
24
|
"packages/shared/package.json",
|
|
25
25
|
"packages/web/dist/",
|
|
26
|
-
"scripts/make-cert.sh",
|
|
27
26
|
"docs/",
|
|
28
27
|
"NOTICE"
|
|
29
28
|
],
|
|
@@ -53,18 +52,24 @@
|
|
|
53
52
|
"workspace-smoke": "node scripts/workspace-smoke.mjs",
|
|
54
53
|
"voice-smoke": "node scripts/voice-smoke.mjs",
|
|
55
54
|
"sentence-units": "tsx scripts/qa/sentence-units.mts",
|
|
55
|
+
"token-format": "tsx scripts/qa/token-format.mts",
|
|
56
|
+
"terminal-palette": "tsx scripts/qa/terminal-palette.mts",
|
|
57
|
+
"android-trust-merge": "node scripts/qa/android-trust-merge.mjs",
|
|
58
|
+
"plan-hint-sweep": "node scripts/qa/plan-hint-sweep.mjs",
|
|
56
59
|
"plan-service-sweep": "node scripts/qa/plan-service-sweep.mjs",
|
|
57
60
|
"plan-mcp-sweep": "node scripts/qa/plan-mcp-sweep.mjs",
|
|
58
61
|
"plan-v2-sweep": "tsx scripts/qa/plan-v2-sweep.mts",
|
|
59
|
-
"tls-smoke": "node scripts/tls-smoke.mjs",
|
|
60
62
|
"notify-testbed": "tsx scripts/notify-testbed.mts",
|
|
61
63
|
"notify-smoke": "tsx scripts/notify-smoke.mts",
|
|
64
|
+
"web-notify-rules": "tsx scripts/qa/web-notify-rules.mts",
|
|
65
|
+
"web-notify-sweep": "node scripts/qa/web-notify-sweep.mjs",
|
|
62
66
|
"apk": "bash scripts/build-apk.sh",
|
|
63
67
|
"transcript-fold": "tsx scripts/qa/transcript-fold.mts",
|
|
64
68
|
"store-fold": "tsx scripts/qa/store-fold.mts",
|
|
65
69
|
"live-stream": "tsx scripts/qa/live-stream.mts",
|
|
66
70
|
"open-target": "tsx scripts/qa/open-target.mts",
|
|
67
71
|
"rail-recent": "tsx scripts/qa/rail-recent.mts",
|
|
72
|
+
"permission-echo": "node scripts/qa/permission-echo.mjs",
|
|
68
73
|
"turn-pulse": "tsx scripts/qa/turn-pulse.mts",
|
|
69
74
|
"resume-tail": "tsx scripts/qa/resume-tail.mts",
|
|
70
75
|
"render-lag-sweep": "node scripts/qa/render-lag-sweep.mjs",
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Env switches to simulate edge cases (append `=N` to env vars when spawning):
|
|
7
7
|
// MOCK_TOOL=1 -> prompt triggers a tool_call + requestPermission
|
|
8
|
+
// MOCK_TOOL2=1 -> TWO tool_calls + requestPermissions in one turn (a session holding more
|
|
9
|
+
// than one pending request; `[tool2]` in the prompt does the same)
|
|
8
10
|
// MOCK_THINK=1 -> emits agent_thought_chunk before the answer
|
|
9
11
|
// MOCK_SLOW_MS=n -> delay between chunks (default 60)
|
|
10
12
|
// MOCK_SPAWN_MS=n -> boot latency before the handshake answers (default 0). A real agent takes
|
|
@@ -506,6 +508,42 @@ const agent = () => ({
|
|
|
506
508
|
}
|
|
507
509
|
}
|
|
508
510
|
|
|
511
|
+
// `[tool2]` — TWO approvals from one turn, raised together (the shape an agent produces when it
|
|
512
|
+
// edits two files in parallel), with no diff payload so the two cards differ only by file name.
|
|
513
|
+
// This is the fixture for the state that has no other name: a session can hold more than one
|
|
514
|
+
// pending request, while the dialog draws only `perms[0]` — so answering the first swaps in the
|
|
515
|
+
// second, drawn identically, and the operator reads 「我点了允许,它还在那儿」 (measured
|
|
516
|
+
// 2026-10-11). QA only: scripts/qa/permission-echo.mjs.
|
|
517
|
+
if (process.env.MOCK_TOOL2 === "1" || /\btool2\b/.test(text)) {
|
|
518
|
+
const mode2 = s.currentModeId || "default";
|
|
519
|
+
if (mode2 !== "dont_ask" && mode2 !== "accept_edits") {
|
|
520
|
+
await Promise.all(["./two-a.txt", "./two-b.txt"].map(async (name, i) => {
|
|
521
|
+
const toolCallId = `tc-${sessionId}-pair-${i}`;
|
|
522
|
+
await send(agent._conn, sessionId, {
|
|
523
|
+
sessionUpdate: "tool_call", toolCallId,
|
|
524
|
+
title: `Write file: ${name}`, kind: "edit", status: "pending",
|
|
525
|
+
rawInput: { path: name, content: `hello from the mock agent (${i + 1})` },
|
|
526
|
+
});
|
|
527
|
+
const resp = await agent._conn.requestPermission({
|
|
528
|
+
sessionId,
|
|
529
|
+
toolCall: { toolCallId, title: `Write file: ${name}`, kind: "edit" },
|
|
530
|
+
options: [
|
|
531
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
532
|
+
{ optionId: "reject", name: "Reject", kind: "reject_once" },
|
|
533
|
+
],
|
|
534
|
+
});
|
|
535
|
+
const pick = resp?.outcome?.optionId;
|
|
536
|
+
await send(agent._conn, sessionId, {
|
|
537
|
+
sessionUpdate: "tool_call_update", toolCallId,
|
|
538
|
+
status: pick === "reject" ? "failed" : "completed",
|
|
539
|
+
rawOutput: pick === "reject"
|
|
540
|
+
? `rejected by the operator — nothing written (chosen option: ${pick})`
|
|
541
|
+
: `wrote ${name} (chosen option: ${pick})`,
|
|
542
|
+
});
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
509
547
|
if (wantTool) {
|
|
510
548
|
const toolCallId = `tc-${sessionId}-${Date.now()}`;
|
|
511
549
|
const edit = wantHermes ? MOCK_HERMES_EDIT : MOCK_EDIT;
|
|
@@ -204,7 +204,17 @@ export interface BackendHandshake {
|
|
|
204
204
|
/** agentCapabilities.sessionCapabilities.fork */
|
|
205
205
|
fork: boolean | null;
|
|
206
206
|
modes: { currentModeId: string | null; available: string[]; names?: Record<string, string> } | null;
|
|
207
|
-
|
|
207
|
+
/** The ACP `select`s, kept WHOLE rather than reduced to id/name/currentValue: the new-session
|
|
208
|
+
* start page has no session to read configOptions from, so the controls it draws come from
|
|
209
|
+
* here — and a control without its `options` is a button that cannot be turned. */
|
|
210
|
+
configOptions: {
|
|
211
|
+
id: string;
|
|
212
|
+
name: string | null;
|
|
213
|
+
currentValue: string | null;
|
|
214
|
+
type?: string | null;
|
|
215
|
+
category?: string | null;
|
|
216
|
+
options?: { value: string; name: string | null; description?: string | null }[] | null;
|
|
217
|
+
}[];
|
|
208
218
|
models: { currentModelId: string | null; available: string[] } | null;
|
|
209
219
|
/** slash commands the agent announced (session/update or newSession) */
|
|
210
220
|
commands: string[];
|
|
@@ -621,6 +631,23 @@ export function handshakeFrom(
|
|
|
621
631
|
id: o.id,
|
|
622
632
|
name: o.name ?? null,
|
|
623
633
|
currentValue: o.currentValue === undefined || o.currentValue === null ? null : String(o.currentValue),
|
|
634
|
+
// Passed through defensively like everything else here: the SDK types cover only part of what
|
|
635
|
+
// the wire carries, and a session's real control set differs per agent.
|
|
636
|
+
type: typeof (o as { type?: unknown }).type === "string" ? String((o as { type?: string }).type) : null,
|
|
637
|
+
category:
|
|
638
|
+
typeof (o as { category?: unknown }).category === "string"
|
|
639
|
+
? String((o as { category?: string }).category)
|
|
640
|
+
: null,
|
|
641
|
+
options: Array.isArray((o as { options?: unknown }).options)
|
|
642
|
+
? ((o as unknown as { options: { value?: unknown; name?: unknown; description?: unknown }[] }).options ?? []).map(
|
|
643
|
+
(opt) => ({
|
|
644
|
+
value: String(opt.value ?? ""),
|
|
645
|
+
name: opt.name === undefined || opt.name === null ? null : String(opt.name),
|
|
646
|
+
description:
|
|
647
|
+
opt.description === undefined || opt.description === null ? null : String(opt.description),
|
|
648
|
+
}),
|
|
649
|
+
)
|
|
650
|
+
: null,
|
|
624
651
|
})),
|
|
625
652
|
models,
|
|
626
653
|
commands: (s.availableCommands ?? []).map((c) => c.name),
|