nexus-agentd 0.1.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 ADDED
@@ -0,0 +1,253 @@
1
+ # nexus-agentd
2
+
3
+ `nexus-agentd` 运行在 Coding Agent 所在机器,通过 Bearer Token 保护的 HTTP/SSE Gateway
4
+ 向 AgentNexus 暴露本机白名单 Agent。当前支持 OpenCode、Claude Code、Codex、Pi 和
5
+ OpenClaw ACP Driver;Hermes 使用其原生 A2A Server,不经过 agentd。
6
+
7
+ 当前部署目标是 Linux/Unix Agent 主机。
8
+
9
+ ```text
10
+ AgentNexus
11
+ │ HTTP/SSE
12
+
13
+ nexus-agentd
14
+ │ ACP stdio
15
+
16
+ OpenCode / Claude Code / Codex / Pi / OpenClaw
17
+ ```
18
+
19
+ ## 安装与启动
20
+
21
+ ```bash
22
+ npm install -g nexus-agentd
23
+ curl -fsSL \
24
+ https://raw.githubusercontent.com/lumia1998/koishi-plugin-agent-nexus/main/packages/nexus-agentd/nexus-agentd.example.json \
25
+ -o nexus-agentd.json
26
+ export NEXUS_AGENTD_TOKEN='TOKEN'
27
+ nexus-agentd --config nexus-agentd.json
28
+ ```
29
+
30
+ OpenCode 和 OpenClaw 自带 ACP 入口。Claude Code、Codex、Pi 需要先安装对应 adapter:
31
+
32
+ ```bash
33
+ npm install -g \
34
+ @agentclientprotocol/claude-agent-acp \
35
+ @agentclientprotocol/codex-acp \
36
+ pi-acp
37
+ ```
38
+
39
+ 还需要分别完成各 Agent 自身的登录或 API Key 配置。`claude-agent-acp` 当前要求 Node.js 22
40
+ 或更高版本;其他 adapter 的版本要求以各自发布包为准。
41
+
42
+ 也可以设置配置路径:
43
+
44
+ ```bash
45
+ export NEXUS_AGENTD_CONFIG=/etc/agent-nexus/nexus-agentd.json
46
+ nexus-agentd
47
+ ```
48
+
49
+ 从仓库开发:
50
+
51
+ ```bash
52
+ npm install
53
+ npm run build
54
+ node dist/cli.js --config nexus-agentd.json
55
+ ```
56
+
57
+ ## 配置
58
+
59
+ ```json
60
+ {
61
+ "listen": {
62
+ "host": "127.0.0.1",
63
+ "port": 8787
64
+ },
65
+ "authToken": "env:NEXUS_AGENTD_TOKEN",
66
+ "workspaceRoots": [
67
+ "/data/repos"
68
+ ],
69
+ "agents": {
70
+ "opencode": {
71
+ "driver": "opencode",
72
+ "command": "opencode",
73
+ "args": ["acp"],
74
+ "permissionPolicy": "ask",
75
+ "inheritEnv": [
76
+ "OPENAI_API_KEY",
77
+ "ANTHROPIC_API_KEY"
78
+ ]
79
+ },
80
+ "claude": {
81
+ "driver": "claude",
82
+ "command": "claude-agent-acp",
83
+ "permissionPolicy": "ask"
84
+ },
85
+ "codex": {
86
+ "driver": "codex",
87
+ "command": "codex-acp",
88
+ "permissionPolicy": "ask"
89
+ },
90
+ "pi": {
91
+ "driver": "pi",
92
+ "command": "pi-acp",
93
+ "permissionPolicy": "ask"
94
+ },
95
+ "openclaw": {
96
+ "driver": "openclaw",
97
+ "command": "openclaw",
98
+ "args": ["acp"],
99
+ "permissionPolicy": "ask"
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ 主要字段:
106
+
107
+ | 字段 | 说明 |
108
+ |---|---|
109
+ | `listen.host` | 默认 `127.0.0.1`;LAN 访问需显式改为内网地址或 `0.0.0.0` |
110
+ | `listen.port` | HTTP/SSE 监听端口,默认 `8787` |
111
+ | `authToken` | 必填 Bearer Token,支持 `env:VAR` |
112
+ | `workspaceRoots` | 允许启动 ACP Session 的目录根列表 |
113
+ | `agents` | daemon 本地 Agent 白名单 |
114
+ | `driver` | `opencode`、`claude`、`codex`、`pi` 或 `openclaw` |
115
+ | `permissionPolicy` | `ask` 或 `deny`,默认 `ask` |
116
+ | `permissionTimeoutMs` | 权限或输入请求等待时间 |
117
+ | `inheritEnv` | 额外允许传给 Agent 子进程的本机环境变量名 |
118
+ | `env` | 显式环境变量,值支持 `env:VAR` |
119
+
120
+ 默认入口:
121
+
122
+ | Driver | 默认命令 | 说明 |
123
+ |---|---|---|
124
+ | `opencode` | `opencode acp` | OpenCode 原生 ACP Server |
125
+ | `claude` | `claude-agent-acp` | `@agentclientprotocol/claude-agent-acp` |
126
+ | `codex` | `codex-acp` | `@agentclientprotocol/codex-acp`,自带兼容 Codex 依赖 |
127
+ | `pi` | `pi-acp` | adapter 再启动本机 `pi --mode rpc` |
128
+ | `openclaw` | `openclaw acp` | OpenClaw 原生 Gateway ACP Bridge |
129
+
130
+ OpenClaw Gateway 必须已运行并可被 `openclaw acp` 访问。可通过 OpenClaw 本机配置保存 Gateway
131
+ 地址和凭据,也可在 `args` 中使用 `--url` 与 `--token-file`。不要把明文 Token 提交到仓库。
132
+
133
+ Pi Driver 会同时探测 `pi-acp` 和底层 `pi`。如果 Pi 可执行文件不是默认名称,通过
134
+ `env.PI_ACP_PI_COMMAND` 指定。
135
+
136
+ `command` 和 `args` 只从 agentd 本机配置读取。HTTP 客户端不能覆盖它们。
137
+
138
+ ## API
139
+
140
+ ```text
141
+ GET /health
142
+ GET /v1/agents
143
+ POST /v1/sessions
144
+ GET /v1/sessions/:id
145
+ POST /v1/sessions/:id/message
146
+ POST /v1/sessions/:id/cancel
147
+ GET /v1/sessions/:id/events
148
+ ```
149
+
150
+ 除 `/health` 外都要求:
151
+
152
+ ```http
153
+ Authorization: Bearer TOKEN
154
+ ```
155
+
156
+ 创建 Session 的请求只接受:
157
+
158
+ ```json
159
+ {
160
+ "agentId": "claude",
161
+ "workspace": "/data/repos/project"
162
+ }
163
+ ```
164
+
165
+ 发送消息只接受:
166
+
167
+ ```json
168
+ {
169
+ "message": "检查并修复测试"
170
+ }
171
+ ```
172
+
173
+ 额外字段会返回 `400`,因此客户端不能传入 command、argv 或 shell payload。
174
+
175
+ ## Session 与事件
176
+
177
+ 一个 Gateway Session 对应一个 ACP 子进程和一个 ACP Session。Session 完成后再次发送消息,
178
+ 会续接同一 ACP Session;每轮输出单独保存,不与上一轮文本拼接。
179
+
180
+ 状态:
181
+
182
+ ```text
183
+ created
184
+ running
185
+ input_required
186
+ permission_required
187
+ completed
188
+ failed
189
+ canceled
190
+ ```
191
+
192
+ SSE 事件包括:
193
+
194
+ ```text
195
+ session_state
196
+ assistant_chunk
197
+ thought_chunk
198
+ plan
199
+ tool_call
200
+ tool_update
201
+ terminal_output
202
+ file_activity
203
+ permission_required
204
+ input_required
205
+ completed
206
+ failed
207
+ canceled
208
+ ```
209
+
210
+ `/events` 支持 `after` 查询参数和 `Last-Event-ID`,用于重放内存事件日志中的后续事件。
211
+
212
+ Session 当前保存在内存中。agentd 重启会终止 ACP 子进程,旧 Gateway Session 不可恢复。
213
+
214
+ ## 权限与输入
215
+
216
+ `permissionPolicy=ask` 时,ACP permission request 会进入 `permission_required`,Session 响应
217
+ 包含问题和选项。AgentNexus 通过 `message` 返回:
218
+
219
+ - 选项序号,例如 `1`
220
+ - option ID
221
+ - option name
222
+ - `deny`、`cancel`、`拒绝` 或 `取消`
223
+
224
+ ACP elicitation 会进入 `input_required`。简单单字段表单可直接回复文本、数字、布尔值或列表;
225
+ 多字段结构化输入使用 JSON object。
226
+
227
+ `permissionPolicy=deny` 会选择协议提供的拒绝选项,或在没有拒绝选项时取消请求。
228
+
229
+ ## Workspace 安全
230
+
231
+ workspace 在启动 ACP 子进程前执行:
232
+
233
+ 1. `realpath(workspace)`。
234
+ 2. `realpath` 每个 `workspaceRoots`。
235
+ 3. 使用路径边界检查确认 workspace 位于允许根内。
236
+
237
+ 这会拒绝 `../` 逃逸、允许根之外的绝对路径和 symlink/junction 逃逸。workspace 必须已经存在。
238
+
239
+ ## 网络建议
240
+
241
+ - 默认保持 `127.0.0.1`。
242
+ - LAN 监听时使用强随机 Token 和主机防火墙限制 Koishi 来源 IP。
243
+ - 不要直接暴露到公网;需要跨网络时在前面部署 HTTPS/mTLS 反向代理。
244
+ - 使用权限受限的专用系统账号运行 agentd。
245
+
246
+ ## 验证
247
+
248
+ ```bash
249
+ npm test
250
+ npm run typecheck
251
+ npm run build
252
+ npm pack --dry-run --json
253
+ ```
@@ -0,0 +1,26 @@
1
+ import type { AgentDriver } from '../drivers/index.js';
2
+ import type { AcpSessionSink } from '../session-contract.js';
3
+ export declare class AcpProcessRuntime {
4
+ private readonly driver;
5
+ private readonly sink;
6
+ private process?;
7
+ private connection?;
8
+ private pending?;
9
+ private pendingInput?;
10
+ private disposed;
11
+ private prompting;
12
+ constructor(driver: AgentDriver, sink: AcpSessionSink);
13
+ start(workspace: string): Promise<void>;
14
+ prompt(message: string): Promise<void>;
15
+ respondPending(message: string): Promise<void>;
16
+ cancel(): Promise<void>;
17
+ dispose(): Promise<void>;
18
+ private requestPermission;
19
+ private finishPermission;
20
+ private requestInput;
21
+ private finishInput;
22
+ private sessionUpdate;
23
+ private captureStderr;
24
+ private onProcessFailure;
25
+ private requireSessionId;
26
+ }
@@ -0,0 +1,420 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { Readable, Writable } from 'node:stream';
3
+ import * as acp from '@agentclientprotocol/sdk';
4
+ export class AcpProcessRuntime {
5
+ driver;
6
+ sink;
7
+ process;
8
+ connection;
9
+ pending;
10
+ pendingInput;
11
+ disposed = false;
12
+ prompting = false;
13
+ constructor(driver, sink) {
14
+ this.driver = driver;
15
+ this.sink = sink;
16
+ }
17
+ async start(workspace) {
18
+ if (this.process)
19
+ throw new Error('ACP runtime is already started');
20
+ const child = this.driver.spawn(workspace);
21
+ this.process = child;
22
+ this.captureStderr(child);
23
+ child.once('error', (error) => this.onProcessFailure(error));
24
+ child.once('exit', (code, signal) => {
25
+ if (this.disposed)
26
+ return;
27
+ if (this.sink.state === 'canceled' || this.sink.state === 'completed')
28
+ return;
29
+ this.onProcessFailure(new Error(`ACP agent exited unexpectedly (${signal || code || 'unknown'})`));
30
+ });
31
+ const app = acp
32
+ .client({ name: 'nexus-agentd' })
33
+ .onRequest(acp.methods.client.session.requestPermission, ({ params }) => this.requestPermission(params))
34
+ .onRequest(acp.methods.client.elicitation.create, ({ params }) => this.requestInput(params))
35
+ .onNotification(acp.methods.client.session.update, ({ params }) => {
36
+ this.sessionUpdate(params);
37
+ });
38
+ const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
39
+ this.connection = app.connect(stream);
40
+ const initialize = await this.connection.agent.request(acp.methods.agent.initialize, {
41
+ protocolVersion: acp.PROTOCOL_VERSION,
42
+ clientCapabilities: {
43
+ elicitation: {
44
+ form: {},
45
+ url: {}
46
+ }
47
+ },
48
+ clientInfo: {
49
+ name: 'nexus-agentd',
50
+ version: '0.1.0'
51
+ }
52
+ });
53
+ if (initialize.protocolVersion !== acp.PROTOCOL_VERSION) {
54
+ this.sink.emit('terminal_output', {
55
+ stream: 'system',
56
+ text: `ACP negotiated protocol ${initialize.protocolVersion}`
57
+ });
58
+ }
59
+ const session = await this.connection.agent.request(acp.methods.agent.session.new, {
60
+ cwd: workspace,
61
+ mcpServers: []
62
+ });
63
+ this.sink.setAcpSessionId(String(session.sessionId));
64
+ this.sink.setState('created');
65
+ }
66
+ async prompt(message) {
67
+ if (!this.connection)
68
+ throw new Error('ACP runtime is not connected');
69
+ if (this.prompting)
70
+ throw new Error('ACP session is already processing a prompt');
71
+ const sessionId = this.requireSessionId();
72
+ this.prompting = true;
73
+ this.sink.clearPending();
74
+ this.sink.setState('running');
75
+ try {
76
+ const response = await this.connection.agent.request(acp.methods.agent.session.prompt, {
77
+ sessionId,
78
+ prompt: [{ type: 'text', text: message }]
79
+ });
80
+ if (this.sink.state === 'canceled')
81
+ return;
82
+ if (response.stopReason === 'cancelled') {
83
+ this.sink.setState('canceled');
84
+ }
85
+ else if (response.stopReason === 'refusal') {
86
+ this.sink.setState('failed', 'ACP agent refused the prompt');
87
+ }
88
+ else {
89
+ this.sink.setState('completed');
90
+ }
91
+ }
92
+ catch (error) {
93
+ if (this.sink.state !== 'canceled') {
94
+ this.sink.setState('failed', error instanceof Error ? error.message : String(error));
95
+ }
96
+ }
97
+ finally {
98
+ this.prompting = false;
99
+ }
100
+ }
101
+ async respondPending(message) {
102
+ if (this.pendingInput) {
103
+ this.finishInput(message);
104
+ return;
105
+ }
106
+ const pending = this.pending;
107
+ if (!pending)
108
+ throw new Error('ACP session is not waiting for input');
109
+ const normalized = message.trim().toLowerCase();
110
+ const options = pending.request.options || [];
111
+ if (['cancel', 'deny', 'reject', '拒绝', '取消', '不同意'].includes(normalized)) {
112
+ this.finishPermission({ outcome: { outcome: 'cancelled' } });
113
+ return;
114
+ }
115
+ const numeric = Number(normalized);
116
+ const option = Number.isInteger(numeric) && numeric >= 1
117
+ ? options[numeric - 1]
118
+ : options.find((item) => item.id.toLowerCase() === normalized ||
119
+ item.name.toLowerCase() === normalized);
120
+ if (!option) {
121
+ throw new Error(`Permission answer must be an option id/name or index: ${options
122
+ .map((item, index) => `${index + 1}. ${item.name} (${item.id})`)
123
+ .join('; ')}`);
124
+ }
125
+ this.finishPermission({
126
+ outcome: {
127
+ outcome: 'selected',
128
+ optionId: option.id
129
+ }
130
+ });
131
+ }
132
+ async cancel() {
133
+ if (this.pending) {
134
+ this.finishPermission({ outcome: { outcome: 'cancelled' } });
135
+ }
136
+ if (this.pendingInput) {
137
+ this.finishInput('cancel');
138
+ }
139
+ if (this.connection && this.sink.state !== 'canceled') {
140
+ try {
141
+ await this.connection.agent.notify(acp.methods.agent.session.cancel, {
142
+ sessionId: this.requireSessionId()
143
+ });
144
+ }
145
+ catch (error) {
146
+ this.sink.emit('terminal_output', {
147
+ stream: 'system',
148
+ text: `ACP cancellation notification failed: ${error instanceof Error ? error.message : String(error)}`
149
+ });
150
+ }
151
+ }
152
+ this.sink.setState('canceled');
153
+ await this.dispose();
154
+ }
155
+ async dispose() {
156
+ if (this.disposed)
157
+ return;
158
+ this.disposed = true;
159
+ if (this.pending) {
160
+ clearTimeout(this.pending.timer);
161
+ this.pending.reject(new Error('ACP runtime disposed'));
162
+ this.pending = undefined;
163
+ }
164
+ if (this.pendingInput) {
165
+ clearTimeout(this.pendingInput.timer);
166
+ this.pendingInput.resolve({ action: 'cancel' });
167
+ this.pendingInput = undefined;
168
+ }
169
+ this.connection?.close();
170
+ this.connection = undefined;
171
+ const child = this.process;
172
+ this.process = undefined;
173
+ if (child && child.exitCode === null && child.signalCode === null) {
174
+ child.kill();
175
+ }
176
+ }
177
+ requestPermission(params) {
178
+ if (this.driver.permissionPolicy === 'deny') {
179
+ const reject = params.options.find((option) => option.kind.startsWith('reject'));
180
+ return reject
181
+ ? {
182
+ outcome: {
183
+ outcome: 'selected',
184
+ optionId: reject.optionId
185
+ }
186
+ }
187
+ : { outcome: { outcome: 'cancelled' } };
188
+ }
189
+ if (this.pending) {
190
+ return { outcome: { outcome: 'cancelled' } };
191
+ }
192
+ const request = {
193
+ id: randomUUID(),
194
+ kind: 'permission',
195
+ prompt: params.toolCall.title ||
196
+ `Permission requested for tool ${params.toolCall.toolCallId}`,
197
+ options: params.options.map((option) => ({
198
+ id: option.optionId,
199
+ name: option.name,
200
+ kind: option.kind
201
+ }))
202
+ };
203
+ this.sink.setPending(request);
204
+ return new Promise((resolve, reject) => {
205
+ const timer = setTimeout(() => {
206
+ if (this.pending?.request.id !== request.id)
207
+ return;
208
+ this.pending = undefined;
209
+ this.sink.clearPending();
210
+ this.sink.setState('failed', 'ACP permission request timed out');
211
+ resolve({ outcome: { outcome: 'cancelled' } });
212
+ }, this.driver.permissionTimeoutMs);
213
+ this.pending = { request, resolve, reject, timer };
214
+ });
215
+ }
216
+ finishPermission(response) {
217
+ const pending = this.pending;
218
+ if (!pending)
219
+ return;
220
+ this.pending = undefined;
221
+ clearTimeout(pending.timer);
222
+ this.sink.clearPending();
223
+ this.sink.setState('running');
224
+ pending.resolve(response);
225
+ }
226
+ requestInput(params) {
227
+ if (this.pending || this.pendingInput) {
228
+ return Promise.resolve({ action: 'cancel' });
229
+ }
230
+ const request = {
231
+ id: randomUUID(),
232
+ kind: 'input',
233
+ prompt: params.mode === 'url'
234
+ ? `${params.message}\n${params.url}`
235
+ : params.message,
236
+ options: elicitationOptions(params)
237
+ };
238
+ this.sink.setPending(request);
239
+ return new Promise((resolve) => {
240
+ const timer = setTimeout(() => {
241
+ if (this.pendingInput?.request.id !== request.id)
242
+ return;
243
+ this.pendingInput = undefined;
244
+ this.sink.clearPending();
245
+ this.sink.setState('failed', 'ACP input request timed out');
246
+ resolve({ action: 'cancel' });
247
+ }, this.driver.permissionTimeoutMs);
248
+ this.pendingInput = { request, params, resolve, timer };
249
+ });
250
+ }
251
+ finishInput(message) {
252
+ const pending = this.pendingInput;
253
+ if (!pending)
254
+ return;
255
+ const normalized = message.trim().toLowerCase();
256
+ let response;
257
+ if (['cancel', '取消'].includes(normalized)) {
258
+ response = { action: 'cancel' };
259
+ }
260
+ else if (['decline', '拒绝', '不同意'].includes(normalized)) {
261
+ response = { action: 'decline' };
262
+ }
263
+ else if (!isFormElicitation(pending.params)) {
264
+ response = { action: 'accept' };
265
+ }
266
+ else {
267
+ response = {
268
+ action: 'accept',
269
+ content: elicitationContent(pending.params, message)
270
+ };
271
+ }
272
+ this.pendingInput = undefined;
273
+ clearTimeout(pending.timer);
274
+ this.sink.clearPending();
275
+ this.sink.setState('running');
276
+ pending.resolve(response);
277
+ }
278
+ sessionUpdate(params) {
279
+ const update = params.update;
280
+ switch (update.sessionUpdate) {
281
+ case 'agent_message_chunk':
282
+ if (update.content.type === 'text') {
283
+ this.sink.appendOutput(update.content.text);
284
+ this.sink.emit('assistant_chunk', {
285
+ messageId: update.messageId,
286
+ content: update.content
287
+ });
288
+ }
289
+ break;
290
+ case 'agent_thought_chunk':
291
+ this.sink.emit('thought_chunk', update);
292
+ break;
293
+ case 'tool_call':
294
+ this.sink.emit('tool_call', update);
295
+ if (update.locations?.length) {
296
+ this.sink.emit('file_activity', {
297
+ toolCallId: update.toolCallId,
298
+ locations: update.locations
299
+ });
300
+ }
301
+ break;
302
+ case 'tool_call_update':
303
+ this.sink.emit('tool_update', update);
304
+ if (update.locations?.length) {
305
+ this.sink.emit('file_activity', {
306
+ toolCallId: update.toolCallId,
307
+ locations: update.locations
308
+ });
309
+ }
310
+ break;
311
+ case 'plan':
312
+ case 'plan_update':
313
+ case 'plan_removed':
314
+ this.sink.emit('plan', update);
315
+ break;
316
+ default:
317
+ break;
318
+ }
319
+ }
320
+ captureStderr(child) {
321
+ let buffer = '';
322
+ child.stderr.on('data', (chunk) => {
323
+ buffer += String(chunk);
324
+ const lines = buffer.split(/\r?\n/);
325
+ buffer = lines.pop() || '';
326
+ for (const line of lines) {
327
+ if (line) {
328
+ this.sink.emit('terminal_output', {
329
+ stream: 'stderr',
330
+ text: line
331
+ });
332
+ }
333
+ }
334
+ });
335
+ }
336
+ onProcessFailure(error) {
337
+ if (this.disposed)
338
+ return;
339
+ this.sink.setState('failed', error instanceof Error ? error.message : String(error));
340
+ }
341
+ requireSessionId() {
342
+ const value = this.sink.acpSessionId;
343
+ if (typeof value !== 'string' || !value) {
344
+ throw new Error('ACP session has not been initialized');
345
+ }
346
+ return value;
347
+ }
348
+ }
349
+ function elicitationOptions(params) {
350
+ if (!isFormElicitation(params))
351
+ return undefined;
352
+ const properties = params.requestedSchema.properties || {};
353
+ if (Object.keys(properties).length !== 1)
354
+ return undefined;
355
+ const schema = Object.values(properties)[0];
356
+ if (schema.type !== 'string')
357
+ return undefined;
358
+ const oneOf = Array.isArray(schema.oneOf)
359
+ ? schema.oneOf
360
+ : [];
361
+ if (oneOf.length) {
362
+ return oneOf.map((option) => ({
363
+ id: String(option.const),
364
+ name: String(option.title),
365
+ kind: 'input'
366
+ }));
367
+ }
368
+ const values = Array.isArray(schema.enum)
369
+ ? schema.enum
370
+ : [];
371
+ return values.map((value) => ({
372
+ id: String(value),
373
+ name: String(value),
374
+ kind: 'input'
375
+ }));
376
+ }
377
+ function elicitationContent(params, message) {
378
+ try {
379
+ const parsed = JSON.parse(message);
380
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
381
+ return parsed;
382
+ }
383
+ }
384
+ catch { }
385
+ const properties = params.requestedSchema.properties || {};
386
+ const entries = Object.entries(properties);
387
+ if (entries.length !== 1) {
388
+ throw new Error('Structured ACP input requires a JSON object');
389
+ }
390
+ const [key, schema] = entries[0];
391
+ if (schema.type === 'boolean') {
392
+ const value = message.trim().toLowerCase();
393
+ if (['true', 'yes', '1', '是'].includes(value))
394
+ return { [key]: true };
395
+ if (['false', 'no', '0', '否'].includes(value))
396
+ return { [key]: false };
397
+ throw new Error('Boolean ACP input must be true/false or yes/no');
398
+ }
399
+ if (schema.type === 'number' || schema.type === 'integer') {
400
+ const value = Number(message);
401
+ if (!Number.isFinite(value))
402
+ throw new Error('ACP input must be a number');
403
+ return { [key]: schema.type === 'integer' ? Math.trunc(value) : value };
404
+ }
405
+ if (schema.type === 'array') {
406
+ return {
407
+ [key]: message
408
+ .split(/[,,\n]/)
409
+ .map((item) => item.trim())
410
+ .filter(Boolean)
411
+ };
412
+ }
413
+ return { [key]: message };
414
+ }
415
+ function isFormElicitation(params) {
416
+ return (params.mode === 'form' &&
417
+ Boolean(params.requestedSchema &&
418
+ typeof params.requestedSchema ===
419
+ 'object'));
420
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};