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 CHANGED
@@ -3,13 +3,13 @@
3
3
  [![npm version](https://img.shields.io/npm/v/nx-ce)](https://www.npmjs.com/package/nx-ce)
4
4
  [![CI](https://github.com/joke-lx/nx-ce/actions/workflows/npm-publish.yml/badge.svg)](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`. It provides two modes:
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 query`** — one-shot cold-start queries (stateless, CLI-friendly)
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
- 一次性冷启动查询与多会话 WebSocket 持久化服务器。
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
- # One-shot query (stateless)
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
- | Flag | Description / 说明 |
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", "processing": false },
159
- { "name": "proj-b", "cwd": "D:/project-b", "sessionId": "sess_yyy", "processing": true }
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
- * CLI entry point. Routes to subcommands:
7
- * nx-ce query "prompt" — one-shot cold-start query
8
- * nx-ce serve — persistent manager process (stdin/stdout protocol)
9
- * nx-ce status show instance state
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.7",
4
- "description": "Claude Engine — SDK adapter layer for native messaging host. Bridges @anthropic-ai/claude-agent-sdk calls over a length-prefixed JSON protocol.",
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/basic.js",
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
- * 模式:nx-sx/src/cli.js
5
- * 路由到 query | serve | status | help
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, args } = parseArgs();
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 query <prompt> 一次性冷启动查询
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 { runQuery } from 'nx-ce';
6
- * const { text, sessionId } = await runQuery({ prompt: 'hello', ... });
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 { runQuery } from './query.js';
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 !== 'crash') {
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
- // (name, cwd) 查 session
501
- const key = sessionKey(sessionName, req.cwd);
502
- const session = sessionManager.sessions.get(key);
503
- if (session?.metadata) {
504
- ws.send(JSON.stringify(session.metadata));
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 not yet initialized',
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
- const sessions = [...sessionManager.sessions.entries()]
549
- .filter(([_, s]) => !s.closed)
550
- .map(([key, s]) => ({
551
- name: s.name,
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
- ws.send(JSON.stringify({ type: 'session_list', sessions }));
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
- }