nx-ce 0.1.7 → 0.2.1
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 +31 -60
- package/bin/nx-ce.js +7 -5
- package/package.json +6 -5
- package/src/cli.js +23 -95
- package/src/index.js +6 -4
- package/src/serve.js +99 -23
- package/src/query.js +0 -128
- package/src/skills.js +0 -56
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/nx-ce)
|
|
4
4
|
[](https://github.com/joke-lx/nx-ce/actions/workflows/npm-publish.yml)
|
|
5
5
|
|
|
6
|
-
**nx-ce** is a lightweight Node.js adapter for `@anthropic-ai/claude-agent-sdk`.
|
|
6
|
+
**nx-ce** is a lightweight Node.js adapter for `@anthropic-ai/claude-agent-sdk`. As of **v0.2**, it provides a single mode:
|
|
7
7
|
|
|
8
|
-
- **`nx-ce
|
|
9
|
-
- **`nx-ce serve`** — WebSocket multi-session server (persistent, concurrent clients)
|
|
8
|
+
- **`nx-ce serve`** — WebSocket multi-session server (persistent, concurrent clients). All consumers (CLI scripts, Chrome extensions, native_host) connect via this single WS endpoint.
|
|
10
9
|
|
|
11
|
-
**nx-ce** 是一个轻量级 Node.js 适配器,封装了 `@anthropic-ai/claude-agent-sdk
|
|
12
|
-
|
|
10
|
+
**nx-ce** 是一个轻量级 Node.js 适配器,封装了 `@anthropic-ai/claude-agent-sdk`。
|
|
11
|
+
**v0.2 起只提供一种模式**:`nx-ce serve` 启动 WebSocket 多会话服务器。
|
|
12
|
+
所有调用方(CLI 脚本 / Chrome 扩展 / native_host)都通过这个唯一的 WS 端点与 SDK 通信。
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -35,47 +35,16 @@ npm install -g nx-ce
|
|
|
35
35
|
## Quick Start / 快速开始
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
#
|
|
39
|
-
nx-ce query "用中文回答:1+1=?" --model claude-haiku-4-5
|
|
40
|
-
|
|
41
|
-
# Start WebSocket server (persistent, multi-session)
|
|
38
|
+
# Start WebSocket server (the only mode)
|
|
42
39
|
nx-ce serve --port 3100
|
|
43
40
|
|
|
44
41
|
# In another terminal, run tests
|
|
45
42
|
node test/serve-test.mjs
|
|
46
43
|
```
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## `nx-ce query` — One-Shot Cold-Start Query / 一次性冷启动查询
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
nx-ce query "解释这段代码" --model claude-sonnet-4-6
|
|
54
|
-
nx-ce query "继续之前的对话" --resume sess_abc123
|
|
55
|
-
nx-ce query "Analyze" --skill git-workflow,code-review
|
|
56
|
-
nx-ce query "Analyze" --skill all
|
|
57
|
-
```
|
|
45
|
+
> **v0.2 breaking change**: `nx-ce query` and `nx-ce skills` CLI subcommands removed. All consumers must use the WebSocket protocol. See the [Protocol](#websocket-protocol--websocket-协议) section below.
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
|------|-------------------|
|
|
61
|
-
| `--model <id>` | Model override (default `claude-sonnet-4-6`) / 模型 ID |
|
|
62
|
-
| `--claude-path <path>` | Path to Claude CLI binary / Claude CLI 路径 |
|
|
63
|
-
| `--system-prompt <text>` | System prompt override / 系统提示词覆盖 |
|
|
64
|
-
| `--resume <sessionId>` | Resume a prior session (long conversation) / 续接会话 |
|
|
65
|
-
| `--skill <name>[,<name>...]` | Load specific skills (comma-separated, or `all`) / 加载 Skill |
|
|
66
|
-
| `--include-metadata` | Include skills/tools/slashCommands in output / 附带元数据 |
|
|
67
|
-
| `--no-persist` | Don't persist session / 不持久化 |
|
|
68
|
-
| `--env "KEY=val,KEY2=val"` | Extra environment variables / 额外环境变量 |
|
|
69
|
-
|
|
70
|
-
### JSON output
|
|
71
|
-
|
|
72
|
-
```json
|
|
73
|
-
// Default
|
|
74
|
-
{ "text": "2", "sessionId": "sess_abc" }
|
|
75
|
-
|
|
76
|
-
// With --include-metadata
|
|
77
|
-
{ "text": "2", "sessionId": "sess_abc", "metadata": { "skills": [...], "tools": [...], ... } }
|
|
78
|
-
```
|
|
47
|
+
> **v0.2 破坏性变更**:移除 `nx-ce query` 和 `nx-ce skills` 子命令。所有调用方必须使用 WebSocket 协议。
|
|
79
48
|
|
|
80
49
|
---
|
|
81
50
|
|
|
@@ -148,18 +117,38 @@ proj-a:D~project-a~src → SDK 会话 C
|
|
|
148
117
|
|
|
149
118
|
### Idle auto-reclaim / 空闲自动回收
|
|
150
119
|
|
|
151
|
-
客户端断开连接 5 分钟后,session
|
|
120
|
+
客户端断开连接 5 分钟后,session 自动销毁。**历史会话保留**:
|
|
121
|
+
内存中的 session 销毁,但磁盘状态文件保留并标记为 `lifecycleState: 'stopped'`。
|
|
122
|
+
`listSessions` 会同时返回活跃 session(绿色)和历史 session(灰色虚线,可恢复)。
|
|
152
123
|
|
|
153
124
|
### List sessions / 查看会话
|
|
154
125
|
|
|
155
126
|
```javascript
|
|
156
127
|
→ { "type": "listSessions" }
|
|
157
128
|
← { "type": "session_list", "sessions": [
|
|
158
|
-
{ "name": "proj-a", "cwd": "D:/project-a", "sessionId": "sess_xxx",
|
|
159
|
-
|
|
129
|
+
{ "name": "proj-a", "cwd": "D:/project-a", "sessionId": "sess_xxx",
|
|
130
|
+
"processing": false, "lifecycleState": "active" },
|
|
131
|
+
{ "name": "proj-b", "cwd": "D:/project-b", "sessionId": "sess_yyy",
|
|
132
|
+
"lifecycleState": "stopped",
|
|
133
|
+
"startedAt": "...", "updatedAt": "..." }
|
|
160
134
|
]}
|
|
161
135
|
```
|
|
162
136
|
|
|
137
|
+
| lifecycleState | 含义 |
|
|
138
|
+
|----------------|------|
|
|
139
|
+
| `active` | 内存中活跃,可直接发 query |
|
|
140
|
+
| `stopped` | 已关闭但保留在磁盘,发 query 会自动 resume(`options.resume = sessionId`) |
|
|
141
|
+
|
|
142
|
+
### Resume a historical session / 恢复历史会话
|
|
143
|
+
|
|
144
|
+
直接对历史 session 发 `query`,服务端会自动用磁盘上保存的 `sessionId` 续接:
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
→ { "type": "query", "session": "proj-a", "cwd": "D:/project-a", "prompt": "继续上次的话题" }
|
|
148
|
+
// server: readState('proj-a:D~/project-a') → sessionId → options.resume
|
|
149
|
+
// 上下文自动恢复
|
|
150
|
+
```
|
|
151
|
+
|
|
163
152
|
---
|
|
164
153
|
|
|
165
154
|
## WebSocket Protocol / WebSocket 协议
|
|
@@ -278,21 +267,6 @@ Each session has its own `agentQuery()`, `MessageChannel`, `MonotonicClock`, and
|
|
|
278
267
|
|
|
279
268
|
---
|
|
280
269
|
|
|
281
|
-
## `nx-ce skills` — List Available Skills / 列出可用 Skill
|
|
282
|
-
|
|
283
|
-
```bash
|
|
284
|
-
nx-ce skills --cwd "D:/project"
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
```json
|
|
288
|
-
{ "skills": ["code-review", "browse", ...],
|
|
289
|
-
"tools": ["Read", "Edit", "Bash", ...],
|
|
290
|
-
"slashCommands": ["code-review", ...],
|
|
291
|
-
"agents": ["Explore", ...] }
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
270
|
## State Persistence / 状态持久化
|
|
297
271
|
|
|
298
272
|
State files at `~/.nx-ce/instances/{key}.json`. Key format: `{name}~{cwd}`.
|
|
@@ -323,9 +297,6 @@ State files at `~/.nx-ce/instances/{key}.json`. Key format: `{name}~{cwd}`.
|
|
|
323
297
|
## Development / 开发
|
|
324
298
|
|
|
325
299
|
```bash
|
|
326
|
-
# One-shot query
|
|
327
|
-
node ./bin/nx-ce.js query "你好"
|
|
328
|
-
|
|
329
300
|
# Start server
|
|
330
301
|
node ./bin/nx-ce.js serve --port 3100
|
|
331
302
|
|
package/bin/nx-ce.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* nx-ce — Claude Engine
|
|
4
|
+
* nx-ce — Claude Engine (v0.2 serve-only)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* nx-ce
|
|
6
|
+
* Single entry point — all consumers (CLI / Chrome extension / native_host)
|
|
7
|
+
* talk to the WebSocket server via the unified protocol.
|
|
8
|
+
*
|
|
9
|
+
* nx-ce serve [--port 3100] — start WebSocket server
|
|
10
|
+
* nx-ce status — list instance states
|
|
11
|
+
* nx-ce help — show help
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { runCli } from '../src/cli.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nx-ce",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Claude Engine —
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Claude Engine — WebSocket adapter for @anthropic-ai/claude-agent-sdk. Single serve mode serves all consumers (CLI / Chrome extension / native_host) over a unified JSON-over-WebSocket protocol.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"nx-ce": "node ./bin/nx-ce.js",
|
|
20
|
-
"test": "node test/
|
|
21
|
-
"start": "node ./bin/nx-ce.js"
|
|
20
|
+
"test": "node test/serve-test.mjs",
|
|
21
|
+
"start": "node ./bin/nx-ce.js serve"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"claude",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"anthropic",
|
|
27
27
|
"sdk",
|
|
28
28
|
"native-messaging",
|
|
29
|
-
"chrome-extension"
|
|
29
|
+
"chrome-extension",
|
|
30
|
+
"websocket"
|
|
30
31
|
],
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"dependencies": {
|
package/src/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI —
|
|
2
|
+
* CLI — 子命令路由器(v0.2 起仅 serve 模式)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 历史:
|
|
5
|
+
* - 旧版有 query(冷启动)/ skills(独立拉元数据)子命令
|
|
6
|
+
* - v0.2 起所有调用统一收敛到 serve:所有元数据通过 getSkills 消息获取
|
|
7
|
+
*
|
|
8
|
+
* 路由: serve | status | help
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { existsSync } from 'node:fs';
|
|
9
|
-
import { runQuery } from './query.js';
|
|
10
12
|
import { startServe } from './serve.js';
|
|
11
|
-
import { listSkills } from './skills.js';
|
|
12
13
|
import { readState, listStates } from './session-store.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -19,11 +20,9 @@ import { readState, listStates } from './session-store.js';
|
|
|
19
20
|
* @returns {{ cmd: string, flags: object, args: string[] }}
|
|
20
21
|
*/
|
|
21
22
|
export function parseArgs(argv = process.argv.slice(2)) {
|
|
22
|
-
const cmd = argv[0];
|
|
23
|
+
const cmd = argv[0];
|
|
23
24
|
const rest = argv.slice(1);
|
|
24
25
|
|
|
25
|
-
// 解析 --key=value 或 --key value 选项
|
|
26
|
-
// positional 只收集非 -- 开头的参数
|
|
27
26
|
const flags = {};
|
|
28
27
|
const positional = [];
|
|
29
28
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -31,13 +30,10 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
31
30
|
if (arg.startsWith('--')) {
|
|
32
31
|
const eqIdx = arg.indexOf('=');
|
|
33
32
|
if (eqIdx !== -1) {
|
|
34
|
-
// --key=value 格式
|
|
35
33
|
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
36
34
|
} else if (rest[i + 1] === undefined || rest[i + 1].startsWith('--')) {
|
|
37
|
-
// --key 后面没有值,或下一个参数也是 flag → boolean flag
|
|
38
35
|
flags[arg.slice(2)] = true;
|
|
39
36
|
} else {
|
|
40
|
-
// --key value 格式(下一个参数作为值)
|
|
41
37
|
flags[arg.slice(2)] = rest[++i] ?? true;
|
|
42
38
|
}
|
|
43
39
|
} else {
|
|
@@ -50,42 +46,14 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
50
46
|
|
|
51
47
|
/**
|
|
52
48
|
* 运行 CLI 入口。
|
|
53
|
-
* 根据子命令分发到对应的处理函数。
|
|
54
49
|
*/
|
|
55
50
|
export async function runCli() {
|
|
56
|
-
const { cmd, flags
|
|
51
|
+
const { cmd, flags } = parseArgs();
|
|
57
52
|
|
|
58
53
|
switch (cmd) {
|
|
59
|
-
case 'query': {
|
|
60
|
-
// 冷启动查询
|
|
61
|
-
const prompt = args[0] || flags.prompt;
|
|
62
|
-
if (!prompt) {
|
|
63
|
-
throw new Error('用法: nx-ce query <prompt> [--model ...] [--claude-path ...]');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const result = await runQuery({
|
|
67
|
-
prompt,
|
|
68
|
-
model: flags.model,
|
|
69
|
-
cwd: flags.cwd || process.cwd(),
|
|
70
|
-
claudePath: resolveClaudePath(flags['claude-path']),
|
|
71
|
-
systemPrompt: flags['system-prompt'],
|
|
72
|
-
persistSession: flags['no-persist'] ? false : undefined,
|
|
73
|
-
resumeSessionId: flags.resume,
|
|
74
|
-
skills: parseSkills(flags.skill),
|
|
75
|
-
env: flags.env ? parseEnvString(flags.env) : undefined,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// 默认只返回 text + sessionId,加 --include-metadata 才返回 metadata
|
|
79
|
-
if (flags['include-metadata']) {
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
return { text: result.text, sessionId: result.sessionId };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
54
|
case 'serve': {
|
|
86
|
-
// WebSocket
|
|
55
|
+
// WebSocket 持久化服务模式(唯一数据通路)
|
|
87
56
|
const name = flags.name || 'default';
|
|
88
|
-
|
|
89
57
|
const result = await startServe({
|
|
90
58
|
name,
|
|
91
59
|
claudePath: resolveClaudePath(flags['claude-path']),
|
|
@@ -94,57 +62,41 @@ export async function runCli() {
|
|
|
94
62
|
env: flags.env ? parseEnvString(flags.env) : undefined,
|
|
95
63
|
port: flags.port ? parseInt(flags.port, 10) : undefined,
|
|
96
64
|
});
|
|
97
|
-
|
|
98
65
|
return result;
|
|
99
66
|
}
|
|
100
67
|
|
|
101
68
|
case 'status': {
|
|
102
|
-
// 查询实例状态
|
|
103
69
|
const name = flags.name;
|
|
104
70
|
if (name) {
|
|
105
|
-
return readState(name);
|
|
71
|
+
return readState(name);
|
|
106
72
|
}
|
|
107
|
-
return listStates();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case 'skills': {
|
|
111
|
-
const result = await listSkills({
|
|
112
|
-
cwd: flags.cwd || process.cwd(),
|
|
113
|
-
claudePath: resolveClaudePath(flags['claude-path']),
|
|
114
|
-
env: flags.env ? parseEnvString(flags.env) : undefined,
|
|
115
|
-
});
|
|
116
|
-
return result;
|
|
73
|
+
return listStates();
|
|
117
74
|
}
|
|
118
75
|
|
|
119
76
|
case 'help':
|
|
77
|
+
case '--help':
|
|
78
|
+
case '-h':
|
|
120
79
|
default:
|
|
121
|
-
// 显示帮助信息
|
|
122
80
|
console.log(`
|
|
123
|
-
nx-ce — Claude Engine
|
|
81
|
+
nx-ce — Claude Engine (v0.2: serve-only)
|
|
124
82
|
|
|
125
83
|
用法:
|
|
126
|
-
nx-ce
|
|
127
|
-
--model <id> 模型覆盖
|
|
128
|
-
--claude-path <path> Claude CLI 路径
|
|
129
|
-
--system-prompt <text> 系统提示词覆盖
|
|
130
|
-
--resume <sessionId> 续接之前的会话(长对话)
|
|
131
|
-
--skill <name>[,<name>...] 加载指定 Skill(逗号分隔,传 "all" 加载全部)
|
|
132
|
-
--include-metadata 输出中附带 skills/tools/slash_commands 列表
|
|
133
|
-
--no-persist 不持久化会话
|
|
134
|
-
--env "KEY=value,KEY2=val" 额外环境变量
|
|
135
|
-
|
|
136
|
-
nx-ce serve WebSocket 持久化服务器(单一进程 / 多客户端)
|
|
84
|
+
nx-ce serve WebSocket 持久化服务器(唯一入口)
|
|
137
85
|
--name <name> 实例名称(默认: "default")
|
|
138
86
|
--port <port> WebSocket 端口(默认: 3100)
|
|
139
87
|
--model <id> 模型覆盖
|
|
140
88
|
--claude-path <path> Claude CLI 路径
|
|
89
|
+
--cwd <path> 默认工作目录
|
|
141
90
|
--env "KEY=value,..." 额外环境变量
|
|
142
91
|
|
|
143
92
|
nx-ce status [--name <name>] 查看实例状态
|
|
144
93
|
|
|
145
|
-
nx-ce skills [--cwd <path>] 列出 SDK 可用 skill/tool/agent
|
|
146
|
-
|
|
147
94
|
nx-ce help 显示此帮助
|
|
95
|
+
|
|
96
|
+
协议(ws://127.0.0.1:3100):
|
|
97
|
+
C→S: query / getSkills / getStatus / listSessions / closeSession / ping
|
|
98
|
+
S→C: connected / init / turn_start / text / thinking / tool_use / done /
|
|
99
|
+
error / pong / skills / status / session_list / session_closed
|
|
148
100
|
`);
|
|
149
101
|
return null;
|
|
150
102
|
}
|
|
@@ -152,32 +104,18 @@ export async function runCli() {
|
|
|
152
104
|
|
|
153
105
|
/**
|
|
154
106
|
* 解析 Claude CLI 路径。
|
|
155
|
-
* 优先使用命令行参数,然后检查环境变量。
|
|
156
|
-
*
|
|
157
|
-
* @param {string|undefined} flag - 命令行传入的路径
|
|
158
|
-
* @returns {string|undefined} 解析后的路径,未找到则返回 undefined(由 SDK 自动检测)
|
|
159
107
|
*/
|
|
160
108
|
function resolveClaudePath(flag) {
|
|
161
109
|
if (flag) return flag;
|
|
162
|
-
|
|
163
|
-
const candidates = [
|
|
164
|
-
process.env.CLAUDE_PATH,
|
|
165
|
-
process.env.CLAUDE_CLI_PATH,
|
|
166
|
-
// Windows: npx、npm 全局或用户本地安装
|
|
167
|
-
];
|
|
110
|
+
const candidates = [process.env.CLAUDE_PATH, process.env.CLAUDE_CLI_PATH];
|
|
168
111
|
for (const c of candidates) {
|
|
169
112
|
if (c && existsSync(c)) return c;
|
|
170
113
|
}
|
|
171
|
-
// 未找到,让 SDK 自动检测
|
|
172
114
|
return undefined;
|
|
173
115
|
}
|
|
174
116
|
|
|
175
117
|
/**
|
|
176
|
-
*
|
|
177
|
-
* 格式: "KEY=value,KEY2=val2"
|
|
178
|
-
*
|
|
179
|
-
* @param {string} str - 逗号分隔的 KEY=value 对
|
|
180
|
-
* @returns {object}
|
|
118
|
+
* 解析环境变量字符串 "KEY=val,KEY2=val2"
|
|
181
119
|
*/
|
|
182
120
|
function parseEnvString(str) {
|
|
183
121
|
const result = {};
|
|
@@ -189,13 +127,3 @@ function parseEnvString(str) {
|
|
|
189
127
|
}
|
|
190
128
|
return result;
|
|
191
129
|
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* 解析 --skill 参数。
|
|
195
|
-
* 逗号分隔的列表 → 数组;"all" → "all"(由 SDK 处理)。
|
|
196
|
-
*/
|
|
197
|
-
function parseSkills(value) {
|
|
198
|
-
if (value === undefined || value === null || value === '') return undefined;
|
|
199
|
-
if (value === 'all' || value === 'ALL') return 'all';
|
|
200
|
-
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
201
|
-
}
|
package/src/index.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* nx-ce — 公开 API 入口
|
|
3
3
|
*
|
|
4
4
|
* 用法:
|
|
5
|
-
* import {
|
|
6
|
-
*
|
|
5
|
+
* import { startServe, readState } from 'nx-ce';
|
|
6
|
+
* await startServe({ name: 'main', port: 3100 });
|
|
7
|
+
*
|
|
8
|
+
* v0.2 起:nx-ce 仅提供 WebSocket serve 模式,不再有冷启动 query。
|
|
9
|
+
* 所有调用方(CLI / Chrome 扩展 / native_host)都通过 WS 协议与 serve 通信。
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
export {
|
|
10
|
-
export { listSkills } from './skills.js';
|
|
12
|
+
export { startServe } from './serve.js';
|
|
11
13
|
export {
|
|
12
14
|
readState,
|
|
13
15
|
writeState,
|
package/src/serve.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { WebSocketServer } from 'ws';
|
|
19
19
|
import { query as agentQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
20
20
|
import { hostname, machine, platform, release } from 'node:os';
|
|
21
|
-
import { readState, writeState, deleteState, LifecycleState, createState } from './session-store.js';
|
|
21
|
+
import { readState, writeState, deleteState, listStates, LifecycleState, createState } from './session-store.js';
|
|
22
22
|
import { generateId, MonotonicClock, getMachineId } from './util.js';
|
|
23
23
|
|
|
24
24
|
/** 默认端口 */
|
|
@@ -365,8 +365,13 @@ class SessionManager {
|
|
|
365
365
|
/**
|
|
366
366
|
* 销毁一个 session。
|
|
367
367
|
* @param {string} key - 内部 key(name:cwd)
|
|
368
|
+
* @param {string} reason - 'shutdown' | 'client request' | 'idle timeout' | 'crash'
|
|
369
|
+
* @param {object} [opts]
|
|
370
|
+
* @param {boolean} [opts.keepHistory=true] - 是否保留磁盘状态(标记为 closed)
|
|
371
|
+
* 仅 'shutdown' 和 'crash' 会删除;其他情况保留为历史记录
|
|
368
372
|
*/
|
|
369
|
-
async destroy(key, reason = 'shutdown') {
|
|
373
|
+
async destroy(key, reason = 'shutdown', opts = {}) {
|
|
374
|
+
const { keepHistory = true } = opts;
|
|
370
375
|
const session = this.sessions.get(key);
|
|
371
376
|
if (!session || session.closed) return;
|
|
372
377
|
session.closed = true;
|
|
@@ -379,26 +384,39 @@ class SessionManager {
|
|
|
379
384
|
|
|
380
385
|
this.sessions.delete(key);
|
|
381
386
|
|
|
382
|
-
if (reason
|
|
387
|
+
if (reason === 'shutdown' || reason === 'crash' || !keepHistory) {
|
|
383
388
|
deleteState(key);
|
|
389
|
+
} else {
|
|
390
|
+
// 标记为 closed,保留历史供 listSessions / resume 使用
|
|
391
|
+
const prev = readState(key);
|
|
392
|
+
if (prev) {
|
|
393
|
+
writeState(key, {
|
|
394
|
+
...prev,
|
|
395
|
+
lifecycleState: LifecycleState.STOPPED,
|
|
396
|
+
closedAt: new Date().toISOString(),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
384
399
|
}
|
|
385
400
|
}
|
|
386
401
|
|
|
387
402
|
/**
|
|
388
403
|
* 按客户端 name 销毁匹配的所有 session(包括不同 cwd)。
|
|
389
404
|
* @param {string} name - 客户端传入的 session 名称
|
|
405
|
+
* @param {object} [opts] - 透传给 destroy()
|
|
390
406
|
*/
|
|
391
|
-
async destroyByName(name) {
|
|
407
|
+
async destroyByName(name, opts = {}) {
|
|
392
408
|
const keys = [...this.sessions.keys()].filter(k => baseName(k) === name);
|
|
393
|
-
await Promise.allSettled(keys.map(k => this.destroy(k, 'client request')));
|
|
409
|
+
await Promise.allSettled(keys.map(k => this.destroy(k, 'client request', opts)));
|
|
394
410
|
}
|
|
395
411
|
|
|
396
412
|
/**
|
|
397
413
|
* 销毁所有 session。
|
|
414
|
+
* @param {string} reason
|
|
415
|
+
* @param {object} [opts]
|
|
398
416
|
*/
|
|
399
|
-
async destroyAll(reason = 'shutdown') {
|
|
417
|
+
async destroyAll(reason = 'shutdown', opts = {}) {
|
|
400
418
|
const keys = [...this.sessions.keys()];
|
|
401
|
-
await Promise.allSettled(keys.map(k => this.destroy(k, reason)));
|
|
419
|
+
await Promise.allSettled(keys.map(k => this.destroy(k, reason, opts)));
|
|
402
420
|
}
|
|
403
421
|
}
|
|
404
422
|
|
|
@@ -497,19 +515,43 @@ export async function startServe(options) {
|
|
|
497
515
|
break;
|
|
498
516
|
|
|
499
517
|
case 'getSkills': {
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
518
|
+
// 解析目标 session
|
|
519
|
+
// 带 session/cwd → 取该 session 的元数据
|
|
520
|
+
// 不带 → 取任意已 init 的 session 的元数据(server 级)
|
|
521
|
+
let meta = null;
|
|
522
|
+
if (req.session || req.cwd) {
|
|
523
|
+
const key = sessionKey(sessionName, req.cwd);
|
|
524
|
+
const session = sessionManager.sessions.get(key);
|
|
525
|
+
meta = session?.metadata;
|
|
526
|
+
} else {
|
|
527
|
+
// server 级:找任意一个已 init 的 session
|
|
528
|
+
for (const s of sessionManager.sessions.values()) {
|
|
529
|
+
if (s.metadata) { meta = s.metadata; break; }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (meta) {
|
|
534
|
+
// 统一返回 type='skills',便于客户端按 type 路由
|
|
535
|
+
ws.send(JSON.stringify({
|
|
536
|
+
type: 'skills',
|
|
537
|
+
sessionId: meta.sessionId,
|
|
538
|
+
model: meta.model,
|
|
539
|
+
skills: meta.skills || [],
|
|
540
|
+
tools: meta.tools || [],
|
|
541
|
+
slashCommands: meta.slashCommands || [],
|
|
542
|
+
agents: meta.agents || [],
|
|
543
|
+
cwd: meta.cwd,
|
|
544
|
+
}));
|
|
505
545
|
} else {
|
|
546
|
+
// 无 init 元数据时:让客户端发一个 query 触发 init,或
|
|
547
|
+
// 等下一个 session 启动后再次 getSkills
|
|
506
548
|
ws.send(JSON.stringify({
|
|
507
549
|
type: 'skills',
|
|
508
550
|
skills: [],
|
|
509
551
|
tools: [],
|
|
510
552
|
slashCommands: [],
|
|
511
553
|
agents: [],
|
|
512
|
-
note: 'session
|
|
554
|
+
note: 'no session has been initialized yet — send a query first',
|
|
513
555
|
}));
|
|
514
556
|
}
|
|
515
557
|
break;
|
|
@@ -532,30 +574,64 @@ export async function startServe(options) {
|
|
|
532
574
|
|
|
533
575
|
case 'closeSession': {
|
|
534
576
|
if (req.cwd) {
|
|
535
|
-
// 精确关闭:name + cwd
|
|
577
|
+
// 精确关闭:name + cwd(保留历史)
|
|
536
578
|
const key = sessionKey(sessionName, req.cwd);
|
|
537
|
-
await sessionManager.destroy(key, 'client request');
|
|
579
|
+
await sessionManager.destroy(key, 'client request', { keepHistory: true });
|
|
538
580
|
ws.send(JSON.stringify({ type: 'session_closed', session: sessionName, cwd: req.cwd }));
|
|
539
581
|
} else {
|
|
540
|
-
// 关闭该 name 下所有 cwd
|
|
541
|
-
await sessionManager.destroyByName(sessionName);
|
|
582
|
+
// 关闭该 name 下所有 cwd 变体(保留历史)
|
|
583
|
+
await sessionManager.destroyByName(sessionName, { keepHistory: true });
|
|
542
584
|
ws.send(JSON.stringify({ type: 'session_closed', session: sessionName }));
|
|
543
585
|
}
|
|
544
586
|
break;
|
|
545
587
|
}
|
|
546
588
|
|
|
547
589
|
case 'listSessions': {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
590
|
+
// 合并:内存中活跃 session + 磁盘上历史 session
|
|
591
|
+
// 活跃优先(同 key 用活跃的元数据覆盖历史的)
|
|
592
|
+
const activeByKey = new Map();
|
|
593
|
+
for (const [key, s] of sessionManager.sessions) {
|
|
594
|
+
if (s.closed) continue;
|
|
595
|
+
activeByKey.set(key, {
|
|
552
596
|
key,
|
|
597
|
+
name: s.name,
|
|
553
598
|
cwd: s.cwd,
|
|
554
599
|
sessionId: s.sessionId,
|
|
600
|
+
model: s.sdkOptions?.model,
|
|
555
601
|
queueLength: s.queue.length,
|
|
556
602
|
processing: s.processing,
|
|
557
|
-
|
|
558
|
-
|
|
603
|
+
lifecycleState: 'active',
|
|
604
|
+
startedAt: s.existingState?.startedAt,
|
|
605
|
+
updatedAt: s.existingState?.updatedAt,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// 磁盘历史(每个 instance 文件对应一个 session)
|
|
610
|
+
// 跳过服务器级 entry(cwd === null 且 name 是 single token)
|
|
611
|
+
const historical = [];
|
|
612
|
+
for (const { name, state } of listStates()) {
|
|
613
|
+
// 服务器级 state 缺少 cwd 字段,跳过
|
|
614
|
+
if (!state || !state.cwd) continue;
|
|
615
|
+
// 已被活跃集合包含的,跳过
|
|
616
|
+
if (activeByKey.has(name)) continue;
|
|
617
|
+
historical.push({
|
|
618
|
+
key: name,
|
|
619
|
+
name: baseName(name),
|
|
620
|
+
cwd: state.cwd,
|
|
621
|
+
sessionId: state.sessionId,
|
|
622
|
+
model: state.model,
|
|
623
|
+
queueLength: 0,
|
|
624
|
+
processing: false,
|
|
625
|
+
lifecycleState: state.lifecycleState || 'closed',
|
|
626
|
+
startedAt: state.startedAt,
|
|
627
|
+
updatedAt: state.updatedAt,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
ws.send(JSON.stringify({
|
|
632
|
+
type: 'session_list',
|
|
633
|
+
sessions: [...activeByKey.values(), ...historical],
|
|
634
|
+
}));
|
|
559
635
|
break;
|
|
560
636
|
}
|
|
561
637
|
|
package/src/query.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 冷启动查询 — 对 @anthropic-ai/claude-agent-sdk 的一次性调用
|
|
3
|
-
*
|
|
4
|
-
* 参考 claudian 的 claudeColdStartQuery.ts(简单的非持久化路径)。
|
|
5
|
-
* 不含 MessageChannel、流式消费循环或 Electron 兼容代码。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { query as agentQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 执行一次冷启动查询并返回完整的文本结果。
|
|
12
|
-
*
|
|
13
|
-
* @param {object} options
|
|
14
|
-
* @param {string} options.prompt - 用户提示词
|
|
15
|
-
* @param {string} [options.systemPrompt] - 系统提示词覆盖
|
|
16
|
-
* @param {string} [options.model] - 模型 ID 覆盖
|
|
17
|
-
* @param {string} options.cwd - 工作目录
|
|
18
|
-
* @param {string} options.claudePath - Claude CLI 可执行文件路径
|
|
19
|
-
* @param {object} [options.env] - 额外的环境变量
|
|
20
|
-
* @param {string[]} [options.tools] - 工具白名单(省略则使用 SDK 默认值)
|
|
21
|
-
* @param {boolean} [options.persistSession] - 是否持久化会话(默认 true)
|
|
22
|
-
* @param {string} [options.resumeSessionId] - 恢复之前的会话
|
|
23
|
-
* @param {string[]|'all'} [options.skills] - 加载哪些 Skill(数组或 'all')
|
|
24
|
-
* @param {AbortController} [options.signal] - 中止信号
|
|
25
|
-
* @returns {Promise<{ text: string, sessionId: string | null, metadata: object | null }>}
|
|
26
|
-
*/
|
|
27
|
-
export async function runQuery(options) {
|
|
28
|
-
const {
|
|
29
|
-
prompt,
|
|
30
|
-
systemPrompt,
|
|
31
|
-
model,
|
|
32
|
-
cwd,
|
|
33
|
-
claudePath,
|
|
34
|
-
env = {},
|
|
35
|
-
tools,
|
|
36
|
-
persistSession,
|
|
37
|
-
resumeSessionId,
|
|
38
|
-
skills,
|
|
39
|
-
signal,
|
|
40
|
-
} = options;
|
|
41
|
-
|
|
42
|
-
// 组装 SDK 选项
|
|
43
|
-
const sdkOptions = {
|
|
44
|
-
cwd: cwd || process.cwd(),
|
|
45
|
-
model: model || 'claude-sonnet-4-6',
|
|
46
|
-
pathToClaudeCodeExecutable: claudePath,
|
|
47
|
-
permissionMode: 'bypassPermissions', // 跳过权限确认
|
|
48
|
-
allowDangerouslySkipPermissions: true,
|
|
49
|
-
env: {
|
|
50
|
-
...process.env,
|
|
51
|
-
...env, // 合并额外环境变量
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// 以下为可选参数的条件注入
|
|
56
|
-
if (systemPrompt) {
|
|
57
|
-
sdkOptions.systemPrompt = systemPrompt;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (tools !== undefined) {
|
|
61
|
-
sdkOptions.tools = tools;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (persistSession === false) {
|
|
65
|
-
sdkOptions.persistSession = false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (resumeSessionId) {
|
|
69
|
-
sdkOptions.resume = resumeSessionId;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (skills !== undefined) {
|
|
73
|
-
sdkOptions.skills = skills;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (signal) {
|
|
77
|
-
sdkOptions.abortController = signal;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 确保总有一个 abort controller
|
|
81
|
-
const abortController = signal || new AbortController();
|
|
82
|
-
sdkOptions.abortController = abortController;
|
|
83
|
-
|
|
84
|
-
// 发起 SDK 查询(返回异步迭代器)
|
|
85
|
-
const response = agentQuery({ prompt, options: sdkOptions });
|
|
86
|
-
|
|
87
|
-
let text = '';
|
|
88
|
-
let sessionId = null;
|
|
89
|
-
let metadata = null;
|
|
90
|
-
|
|
91
|
-
// 遍历 SDK 返回的流式消息
|
|
92
|
-
for await (const message of response) {
|
|
93
|
-
// 如果已中止,中断查询
|
|
94
|
-
if (abortController.signal.aborted) {
|
|
95
|
-
await response.interrupt();
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 从 init 消息中捕获 sessionId + 技能/工具/命令 元数据
|
|
100
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
101
|
-
sessionId = message.session_id;
|
|
102
|
-
metadata = {
|
|
103
|
-
model: message.model,
|
|
104
|
-
skills: message.skills || [],
|
|
105
|
-
tools: message.tools || [],
|
|
106
|
-
slashCommands: message.slash_commands || [],
|
|
107
|
-
agents: message.agents || [],
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 提取助手的文本回复内容
|
|
112
|
-
if (message.type === 'assistant' && message.message?.content) {
|
|
113
|
-
const content = message.message.content;
|
|
114
|
-
if (typeof content === 'string') {
|
|
115
|
-
text += content;
|
|
116
|
-
} else if (Array.isArray(content)) {
|
|
117
|
-
// 内容块数组,筛选出 text 块
|
|
118
|
-
for (const block of content) {
|
|
119
|
-
if (block.type === 'text') {
|
|
120
|
-
text += block.text;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { text, sessionId, metadata };
|
|
128
|
-
}
|
package/src/skills.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* skills — 查询 SDK 可用的 skill / tool / agent 列表
|
|
3
|
-
*
|
|
4
|
-
* 使用一次超轻量 agentQuery() 获取 init 元数据。
|
|
5
|
-
* 不发起真正对话,不持久化 session。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { query as agentQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 获取 SDK 可用的 skills/tools/slashCommands/agents 列表。
|
|
12
|
-
*
|
|
13
|
-
* @param {object} [options]
|
|
14
|
-
* @param {string} [options.cwd] - 工作目录
|
|
15
|
-
* @param {string} [options.claudePath] - Claude CLI 路径
|
|
16
|
-
* @param {object} [options.env] - 额外环境变量
|
|
17
|
-
* @returns {Promise<{ skills: string[], tools: string[], slashCommands: string[], agents: string[] }>}
|
|
18
|
-
*/
|
|
19
|
-
export async function listSkills(options = {}) {
|
|
20
|
-
const sdkOptions = {
|
|
21
|
-
cwd: options.cwd || process.cwd(),
|
|
22
|
-
model: 'claude-haiku-4-5',
|
|
23
|
-
permissionMode: 'bypassPermissions',
|
|
24
|
-
allowDangerouslySkipPermissions: true,
|
|
25
|
-
persistSession: false,
|
|
26
|
-
env: { ...process.env, ...options.env },
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
if (options.claudePath) {
|
|
30
|
-
sdkOptions.pathToClaudeCodeExecutable = options.claudePath;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 用空 prompt 做一次超轻量 init
|
|
34
|
-
const response = agentQuery({ prompt: ' ', options: sdkOptions });
|
|
35
|
-
|
|
36
|
-
const result = {
|
|
37
|
-
skills: [],
|
|
38
|
-
tools: [],
|
|
39
|
-
slashCommands: [],
|
|
40
|
-
agents: [],
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
for await (const message of response) {
|
|
44
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
45
|
-
if (Array.isArray(message.skills)) result.skills = message.skills;
|
|
46
|
-
if (Array.isArray(message.tools)) result.tools = message.tools;
|
|
47
|
-
if (Array.isArray(message.slash_commands)) result.slashCommands = message.slash_commands;
|
|
48
|
-
if (Array.isArray(message.agents)) result.agents = message.agents;
|
|
49
|
-
// init 消息后立即中断,不继续消耗资源
|
|
50
|
-
await response.interrupt().catch(() => {});
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return result;
|
|
56
|
-
}
|