@realtimex/sdk 1.4.3 → 1.4.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/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: realtimex-moderator-sdk
|
|
3
3
|
description: Control and interact with the RealTimeX application through its Node.js SDK. This skill should be used when users want to manage workspaces, threads, agents, activities, LLM chat, vector store, MCP tools, ACP agent sessions, TTS/STT, or any other RealTimeX platform feature via the API. All method signatures are verified against the SDK source code.
|
|
4
|
-
generated: 2026-03-
|
|
5
|
-
sdk_version: 1.4.
|
|
4
|
+
generated: 2026-03-27
|
|
5
|
+
sdk_version: 1.4.5
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# RealTimeX Moderator (SDK Source-Verified)
|
|
9
9
|
|
|
10
|
-
Interact with the RealTimeX desktop app (`http://localhost:3001`) using `@realtimex/sdk` **v1.4.
|
|
10
|
+
Interact with the RealTimeX desktop app (`http://localhost:3001`) using `@realtimex/sdk` **v1.4.5** in Developer Mode (API Key).
|
|
11
11
|
|
|
12
12
|
> Auto-generated from the `@realtimex/sdk` TypeScript source.
|
|
13
13
|
> Refresh: `node scripts/generate-skill.mjs --force` from the SDK repo root.
|
|
@@ -41,7 +41,7 @@ node "$SKILL" agents $ENV
|
|
|
41
41
|
node "$SKILL" workspaces $ENV
|
|
42
42
|
node "$SKILL" threads <workspace-slug> $ENV
|
|
43
43
|
node "$SKILL" trigger-agent <agent> <workspace> <msg> $ENV
|
|
44
|
-
node "$SKILL" acp-chat qwen "question" --cwd=<path>
|
|
44
|
+
node "$SKILL" acp-chat qwen-cli "question" --cwd=<path> $ENV
|
|
45
45
|
node "$SKILL" llm-chat "message" $ENV
|
|
46
46
|
node "$SKILL" activities --status=pending $ENV
|
|
47
47
|
node "$SKILL" mcp-servers $ENV
|
|
@@ -58,6 +58,125 @@ const { sdk, apiKey } = await initSDK({ envDir: process.cwd() });
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
+
## ACP Session Management
|
|
62
|
+
|
|
63
|
+
ACP sessions are persistent agent processes. **Always reuse sessions** across turns instead of spawning a new process for every message — it preserves context and is far more efficient.
|
|
64
|
+
|
|
65
|
+
### Smart `acp-chat` (recommended)
|
|
66
|
+
|
|
67
|
+
`acp-chat` automatically finds or creates a session using this priority:
|
|
68
|
+
|
|
69
|
+
1. `--session=<key>` → validate and reuse that exact session
|
|
70
|
+
2. `listSessions()` → find a compatible active session (same `agent_id`, optional `cwd` match)
|
|
71
|
+
3. `createSession()` → spawn a new agent process only if none available
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# First call — creates a new session, prints session key at end
|
|
75
|
+
node "$SKILL" acp-chat qwen-cli "build a website" --cwd=~/projects/myapp $ENV
|
|
76
|
+
|
|
77
|
+
# Subsequent calls — reuses the existing session automatically
|
|
78
|
+
node "$SKILL" acp-chat qwen-cli "add a login page" $ENV
|
|
79
|
+
|
|
80
|
+
# Pin to a specific session
|
|
81
|
+
node "$SKILL" acp-chat qwen-cli "fix the bug" --session=<key> $ENV
|
|
82
|
+
|
|
83
|
+
# Force a fresh session
|
|
84
|
+
node "$SKILL" acp-chat qwen-cli "start over" --new $ENV
|
|
85
|
+
|
|
86
|
+
# Close session after this turn
|
|
87
|
+
node "$SKILL" acp-chat qwen-cli "done for now" --close $ENV
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Manual Session Lifecycle
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Spawn a session explicitly — save the session_key
|
|
94
|
+
node "$SKILL" acp-session-create qwen-cli --cwd=~/projects/myapp $ENV
|
|
95
|
+
|
|
96
|
+
# Inspect session state
|
|
97
|
+
node "$SKILL" acp-session-get <session-key> $ENV
|
|
98
|
+
|
|
99
|
+
# List all active sessions
|
|
100
|
+
node "$SKILL" acp-sessions $ENV
|
|
101
|
+
|
|
102
|
+
# Patch runtime options (applied on next turn)
|
|
103
|
+
node "$SKILL" acp-session-patch <session-key> --cwd=~/projects/other $ENV
|
|
104
|
+
|
|
105
|
+
# Send a turn on an existing session (sync, no streaming)
|
|
106
|
+
node "$SKILL" acp-send <session-key> "what files did you create?" $ENV
|
|
107
|
+
|
|
108
|
+
# Stream a turn on an existing session (with permission handling)
|
|
109
|
+
node "$SKILL" acp-stream <session-key> "run the tests" $ENV
|
|
110
|
+
|
|
111
|
+
# Cancel the active turn
|
|
112
|
+
node "$SKILL" acp-cancel <session-key> $ENV
|
|
113
|
+
|
|
114
|
+
# Manually resolve a permission request (while stream is active in another process)
|
|
115
|
+
node "$SKILL" acp-resolve <session-key> <request-id> <option-id> $ENV
|
|
116
|
+
|
|
117
|
+
# Close/terminate the session
|
|
118
|
+
node "$SKILL" acp-session-close <session-key> $ENV
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Permission Handling
|
|
122
|
+
|
|
123
|
+
`acp-chat` and `acp-stream` handle `permission_request` SSE events inline via `resolvePermission()`.
|
|
124
|
+
|
|
125
|
+
Control with `--policy-override`:
|
|
126
|
+
- `approve-all` *(default)* — auto-approve (picks the approve/allow/yes option)
|
|
127
|
+
- `deny-all` — auto-deny (picks the deny/cancel/no option)
|
|
128
|
+
- `prompt` — pause and ask you interactively via stdin
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Auto-approve all tool permissions (default)
|
|
132
|
+
node "$SKILL" acp-chat qwen-cli "delete temp files" --policy-override=approve-all $ENV
|
|
133
|
+
|
|
134
|
+
# Ask before approving each permission
|
|
135
|
+
node "$SKILL" acp-chat qwen-cli "run npm install" --policy-override=prompt $ENV
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Custom Script Pattern
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const { initSDK } = require('<SKILL_DIR>/scripts/lib/sdk-init');
|
|
142
|
+
const { sdk } = await initSDK({ envDir: process.cwd() });
|
|
143
|
+
|
|
144
|
+
// Find or reuse a session
|
|
145
|
+
let sessionKey;
|
|
146
|
+
const sessions = await sdk.acpAgent.listSessions();
|
|
147
|
+
const match = sessions.find(s => s.agent_id === 'qwen-cli' && s.state !== 'closed');
|
|
148
|
+
if (match) {
|
|
149
|
+
sessionKey = match.session_key;
|
|
150
|
+
} else {
|
|
151
|
+
const session = await sdk.acpAgent.createSession({
|
|
152
|
+
agent_id: 'qwen-cli',
|
|
153
|
+
cwd: '/path/to/project',
|
|
154
|
+
approvalPolicy: 'approve-all',
|
|
155
|
+
});
|
|
156
|
+
sessionKey = session.session_key;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Stream a turn, resolving permissions inline
|
|
160
|
+
for await (const event of sdk.acpAgent.streamChat(sessionKey, 'build a website')) {
|
|
161
|
+
if (event.type === 'text_delta' && event.data.type !== 'thinking') {
|
|
162
|
+
process.stdout.write(event.data.text ?? '');
|
|
163
|
+
}
|
|
164
|
+
if (event.type === 'permission_request') {
|
|
165
|
+
const req = event.data;
|
|
166
|
+
const opt = req.options?.[0];
|
|
167
|
+
if (opt) {
|
|
168
|
+
await sdk.acpAgent.resolvePermission(sessionKey, {
|
|
169
|
+
requestId: req.requestId,
|
|
170
|
+
optionId: opt.id || opt.optionId,
|
|
171
|
+
outcome: 'approved',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
61
180
|
## Critical Rules (source-detected)
|
|
62
181
|
|
|
63
182
|
| # | Issue |
|
|
@@ -84,7 +203,8 @@ Full fixes in `references/known-issues.md`.
|
|
|
84
203
|
- **Metadata methods** (`getAgents`, `getWorkspaces`, etc.) live on `sdk.api.*`, not `sdk.*`
|
|
85
204
|
- **`sdk.webhook.triggerAgent()`** sends wrong event type — always use raw fetch with `event: "trigger-agent"`
|
|
86
205
|
- **`sdk.task`** methods: `start(uuid)`, `complete(uuid, result)`, `fail(uuid, "error")` — positional args
|
|
87
|
-
- **ACP sessions**
|
|
206
|
+
- **ACP sessions** are persistent — reuse them across turns via `listSessions()` + `streamChat()`
|
|
207
|
+
- **`resolvePermission()`** must be called while the `streamChat` SSE stream is still active
|
|
88
208
|
- **SDK env vars:** `RTX_API_KEY` (dev), `RTX_APP_ID` (prod), `RTX_APP_NAME`
|
|
89
209
|
|
|
90
210
|
## References
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RealTimeX SDK — API Reference
|
|
2
2
|
|
|
3
|
-
> Auto-generated from `@realtimex/sdk` source · v**1.4.
|
|
3
|
+
> Auto-generated from `@realtimex/sdk` source · v**1.4.5** · 2026-03-27
|
|
4
4
|
|
|
5
5
|
**Package:** `@realtimex/sdk` (CJS) · **Server:** `http://localhost:3001`
|
|
6
6
|
**Developer Mode auth:** `Authorization: Bearer <apiKey>`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Known Issues — Source-Detected
|
|
2
2
|
|
|
3
|
-
> Auto-generated by `scripts/generate-skill.mjs` · SDK **1.4.
|
|
3
|
+
> Auto-generated by `scripts/generate-skill.mjs` · SDK **1.4.5** · 2026-03-27
|
|
4
4
|
|
|
5
5
|
Run `node scripts/generate-skill.mjs --force` after SDK source changes to refresh.
|
|
6
6
|
|
|
@@ -311,42 +311,386 @@ CMD['acp-agents'] = async () => {
|
|
|
311
311
|
const { sdk } = await getSDK();
|
|
312
312
|
printTable(await sdk.acpAgent.listAgents({ includeModels: flags.models === 'true' }), ['id', 'label', 'status', 'authReady', 'installed']);
|
|
313
313
|
};
|
|
314
|
+
|
|
315
|
+
// -- acp-sessions -----------------------------------------------------------
|
|
316
|
+
// Source: AcpAgentModule.listSessions() → AcpSessionStatus[]
|
|
314
317
|
CMD['acp-sessions'] = async () => {
|
|
315
318
|
const { sdk } = await getSDK();
|
|
316
319
|
print(await sdk.acpAgent.listSessions());
|
|
317
320
|
};
|
|
318
321
|
|
|
322
|
+
// -- acp-session-create -----------------------------------------------------
|
|
323
|
+
// Source: AcpAgentModule.createSession(options) → AcpSession { session_key, agent_id, state, ... }
|
|
324
|
+
// Prints session_key so callers can pass --session=<key> on subsequent calls.
|
|
325
|
+
CMD['acp-session-create'] = async () => {
|
|
326
|
+
const [agentId] = cmdArgs;
|
|
327
|
+
if (!agentId) {
|
|
328
|
+
console.error('Usage: rtx.js acp-session-create <agent-id> [--cwd=<path>] [--model=<m>] [--policy=approve-all|approve-reads|deny-all]');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
const { sdk } = await getSDK();
|
|
332
|
+
const opts = {
|
|
333
|
+
agent_id: agentId,
|
|
334
|
+
cwd: flags.cwd || process.cwd(),
|
|
335
|
+
approvalPolicy: flags.policy || 'approve-all',
|
|
336
|
+
};
|
|
337
|
+
if (flags.model) opts.model = flags.model;
|
|
338
|
+
const session = await sdk.acpAgent.createSession(opts);
|
|
339
|
+
process.stderr.write('Session created.\n');
|
|
340
|
+
print(session);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// -- acp-session-get --------------------------------------------------------
|
|
344
|
+
// Source: AcpAgentModule.getSession(sessionKey) → AcpSessionStatus
|
|
345
|
+
CMD['acp-session-get'] = async () => {
|
|
346
|
+
const [sessionKey] = cmdArgs;
|
|
347
|
+
if (!sessionKey) { console.error('Usage: rtx.js acp-session-get <session-key>'); process.exit(1); }
|
|
348
|
+
const { sdk } = await getSDK();
|
|
349
|
+
print(await sdk.acpAgent.getSession(sessionKey));
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// -- acp-session-patch ------------------------------------------------------
|
|
353
|
+
// Source: AcpAgentModule.patchSession(sessionKey, patch) → void
|
|
354
|
+
// patch fields: model?, cwd?, timeoutSeconds?, runtimeMode?, approvalPolicy?
|
|
355
|
+
CMD['acp-session-patch'] = async () => {
|
|
356
|
+
const [sessionKey] = cmdArgs;
|
|
357
|
+
if (!sessionKey) {
|
|
358
|
+
console.error('Usage: rtx.js acp-session-patch <session-key> [--cwd=<path>] [--model=<m>] [--policy=<p>] [--timeout=<s>]');
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
const { sdk } = await getSDK();
|
|
362
|
+
const patch = {};
|
|
363
|
+
if (flags.cwd) patch.cwd = flags.cwd;
|
|
364
|
+
if (flags.model) patch.model = flags.model;
|
|
365
|
+
if (flags.policy) patch.approvalPolicy = flags.policy;
|
|
366
|
+
if (flags.timeout) patch.timeoutSeconds = Number(flags.timeout);
|
|
367
|
+
await sdk.acpAgent.patchSession(sessionKey, patch);
|
|
368
|
+
console.log('Session patched.');
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// -- acp-session-close ------------------------------------------------------
|
|
372
|
+
// Source: AcpAgentModule.closeSession(sessionKey, reason?) → void
|
|
373
|
+
CMD['acp-session-close'] = async () => {
|
|
374
|
+
const [sessionKey, ...reasonParts] = cmdArgs;
|
|
375
|
+
if (!sessionKey) { console.error('Usage: rtx.js acp-session-close <session-key> [<reason>]'); process.exit(1); }
|
|
376
|
+
const { sdk } = await getSDK();
|
|
377
|
+
await sdk.acpAgent.closeSession(sessionKey, reasonParts.join(' ') || undefined);
|
|
378
|
+
console.log('Session closed.');
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// -- acp-cancel -------------------------------------------------------------
|
|
382
|
+
// Source: AcpAgentModule.cancelTurn(sessionKey, reason?) → void
|
|
383
|
+
CMD['acp-cancel'] = async () => {
|
|
384
|
+
const [sessionKey, ...reasonParts] = cmdArgs;
|
|
385
|
+
if (!sessionKey) { console.error('Usage: rtx.js acp-cancel <session-key> [<reason>]'); process.exit(1); }
|
|
386
|
+
const { sdk } = await getSDK();
|
|
387
|
+
await sdk.acpAgent.cancelTurn(sessionKey, reasonParts.join(' ') || undefined);
|
|
388
|
+
console.log('Turn cancelled.');
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// -- acp-resolve ------------------------------------------------------------
|
|
392
|
+
// Source: AcpAgentModule.resolvePermission(sessionKey, { requestId, optionId, outcome? }) → void
|
|
393
|
+
// Must be called while streamChat SSE is still active for that session.
|
|
394
|
+
CMD['acp-resolve'] = async () => {
|
|
395
|
+
const [sessionKey, requestId, optionId] = cmdArgs;
|
|
396
|
+
if (!sessionKey || !requestId || !optionId) {
|
|
397
|
+
console.error('Usage: rtx.js acp-resolve <session-key> <request-id> <option-id> [--outcome=approved]');
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const { sdk } = await getSDK();
|
|
401
|
+
await sdk.acpAgent.resolvePermission(sessionKey, {
|
|
402
|
+
requestId,
|
|
403
|
+
optionId,
|
|
404
|
+
outcome: flags.outcome || 'approved',
|
|
405
|
+
});
|
|
406
|
+
console.log('Permission resolved.');
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// -- acp-send ---------------------------------------------------------------
|
|
410
|
+
// Source: AcpAgentModule.chat(sessionKey, message) → AcpChatResponse (sync, waits for full reply)
|
|
411
|
+
// Reuses existing session; never creates a new one.
|
|
412
|
+
CMD['acp-send'] = async () => {
|
|
413
|
+
const [sessionKey, ...msgParts] = cmdArgs;
|
|
414
|
+
const message = msgParts.join(' ');
|
|
415
|
+
if (!sessionKey || !message) {
|
|
416
|
+
console.error('Usage: rtx.js acp-send <session-key> <message>');
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
const { sdk } = await getSDK();
|
|
420
|
+
const res = await sdk.acpAgent.chat(sessionKey, message);
|
|
421
|
+
console.log(res.text ?? JSON.stringify(res));
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// -- acp-stream -------------------------------------------------------------
|
|
425
|
+
// Source: AcpAgentModule.streamChat(sessionKey, message) — SSE stream on existing session.
|
|
426
|
+
// Handles permission_request events automatically via resolvePermission().
|
|
427
|
+
// Reuses existing session; never creates a new one.
|
|
428
|
+
CMD['acp-stream'] = async () => {
|
|
429
|
+
const [sessionKey, ...msgParts] = cmdArgs;
|
|
430
|
+
const message = msgParts.join(' ');
|
|
431
|
+
if (!sessionKey || !message) {
|
|
432
|
+
console.error('Usage: rtx.js acp-stream <session-key> <message> [--thoughts] [--quiet]');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const { sdk } = await getSDK();
|
|
436
|
+
await acpStreamWithPermissions(sdk, sessionKey, message);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// ACP helpers
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Extract a stable string id from a permission option entry.
|
|
445
|
+
* Options can be raw numbers, strings, or objects with numeric/string id fields.
|
|
446
|
+
* IMPORTANT: must use != null (not ||) to handle id === 0 (falsy but valid).
|
|
447
|
+
*/
|
|
448
|
+
function optionId(opt, index) {
|
|
449
|
+
if (opt == null) return String(index);
|
|
450
|
+
if (typeof opt === 'number') return String(opt);
|
|
451
|
+
if (typeof opt === 'string') return opt;
|
|
452
|
+
if (opt.id != null) return String(opt.id);
|
|
453
|
+
if (opt.optionId != null) return String(opt.optionId);
|
|
454
|
+
if (opt.value != null) return String(opt.value);
|
|
455
|
+
return String(index);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Extract a human-readable label from a permission option entry. */
|
|
459
|
+
function optionLabel(opt, index) {
|
|
460
|
+
if (opt == null) return String(index);
|
|
461
|
+
if (typeof opt === 'number') return String(opt);
|
|
462
|
+
if (typeof opt === 'string') return opt;
|
|
463
|
+
return opt.label || opt.name || opt.description || optionId(opt, index);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Stream a chat turn on an existing session, auto-resolving permission_request
|
|
468
|
+
* events via sdk.acpAgent.resolvePermission().
|
|
469
|
+
*
|
|
470
|
+
* Permission resolution strategy (--policy-override flag):
|
|
471
|
+
* approve-all (default) — approve: label-match → last option (typically "Yes/Always")
|
|
472
|
+
* deny-all — deny: label-match → first option (typically "No/Cancel")
|
|
473
|
+
* prompt — pause and ask the user interactively via stdin
|
|
474
|
+
*
|
|
475
|
+
* Why last-option for approve-all fallback:
|
|
476
|
+
* Qwen/Claude-style dialogs are ordered [Deny=0, Approve-once=1, Approve-always=2].
|
|
477
|
+
* Picking options[0] = Deny. We want the last approve variant when no label matches.
|
|
478
|
+
*/
|
|
479
|
+
async function acpStreamWithPermissions(sdk, sessionKey, message) {
|
|
480
|
+
const policyOverride = flags['policy-override'] || 'approve-all';
|
|
481
|
+
|
|
482
|
+
for await (const event of sdk.acpAgent.streamChat(sessionKey, message)) {
|
|
483
|
+
switch (event.type) {
|
|
484
|
+
|
|
485
|
+
case 'text_delta': {
|
|
486
|
+
if (event.data && event.data.type === 'thinking') {
|
|
487
|
+
if (flags.thoughts) process.stderr.write('[thought] ' + String(event.data.text ?? '') + '\n');
|
|
488
|
+
} else {
|
|
489
|
+
process.stdout.write(String(event.data?.text ?? event.data ?? ''));
|
|
490
|
+
}
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
case 'permission_request': {
|
|
495
|
+
const req = event.data || {};
|
|
496
|
+
const requestId = req.requestId || req.id || req.request_id;
|
|
497
|
+
const options = Array.isArray(req.options) ? req.options : [];
|
|
498
|
+
|
|
499
|
+
// Always show the full raw payload — critical for diagnosing option structure
|
|
500
|
+
if (!flags.quiet) {
|
|
501
|
+
process.stderr.write('\n[Permission Request] id=' + requestId + '\n');
|
|
502
|
+
if (req.title) process.stderr.write(' title: ' + req.title + '\n');
|
|
503
|
+
if (req.description) process.stderr.write(' details: ' + req.description + '\n');
|
|
504
|
+
options.forEach((o, i) => {
|
|
505
|
+
process.stderr.write(' [' + optionId(o, i) + '] ' + optionLabel(o, i) + '\n');
|
|
506
|
+
});
|
|
507
|
+
if (flags.debug) {
|
|
508
|
+
process.stderr.write(' raw: ' + JSON.stringify(req) + '\n');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
let chosenId = null;
|
|
513
|
+
|
|
514
|
+
if (policyOverride === 'prompt') {
|
|
515
|
+
const readline = require('readline');
|
|
516
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
517
|
+
const answer = await new Promise(resolve => {
|
|
518
|
+
rl.question(' Choose option id (Enter = last/approve): ', ans => { rl.close(); resolve(ans.trim()); });
|
|
519
|
+
});
|
|
520
|
+
// Use typed answer, or fall back to last option (approve-always)
|
|
521
|
+
chosenId = answer || (options.length ? optionId(options[options.length - 1], options.length - 1) : null);
|
|
522
|
+
|
|
523
|
+
} else if (policyOverride === 'deny-all') {
|
|
524
|
+
// Label-match for deny keywords first, then fall back to first option (index 0 = No/Cancel)
|
|
525
|
+
const idx = options.findIndex(o => /deny|cancel|no|reject/i.test(optionLabel(o, 0)));
|
|
526
|
+
const pick = idx >= 0 ? options[idx] : options[0];
|
|
527
|
+
const pickIdx = idx >= 0 ? idx : 0;
|
|
528
|
+
if (pick != null) chosenId = optionId(pick, pickIdx);
|
|
529
|
+
|
|
530
|
+
} else {
|
|
531
|
+
// approve-all: label-match first, then LAST option (not first — first is typically Deny)
|
|
532
|
+
const idx = options.findIndex(o => /approve|allow|yes|confirm|ok|always/i.test(optionLabel(o, 0)));
|
|
533
|
+
if (idx >= 0) {
|
|
534
|
+
chosenId = optionId(options[idx], idx);
|
|
535
|
+
} else if (options.length > 0) {
|
|
536
|
+
// Last option is safest approve fallback (e.g. "Yes, don't ask again" / "Approve always")
|
|
537
|
+
const lastIdx = options.length - 1;
|
|
538
|
+
chosenId = optionId(options[lastIdx], lastIdx);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (chosenId !== null && requestId) {
|
|
543
|
+
if (!flags.quiet) process.stderr.write(' → resolving with option: ' + chosenId + '\n');
|
|
544
|
+
await sdk.acpAgent.resolvePermission(sessionKey, {
|
|
545
|
+
requestId,
|
|
546
|
+
optionId: chosenId,
|
|
547
|
+
outcome: policyOverride === 'deny-all' ? 'denied' : 'approved',
|
|
548
|
+
});
|
|
549
|
+
} else {
|
|
550
|
+
if (!flags.quiet) process.stderr.write(' → no options to resolve\n');
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
case 'tool_call': {
|
|
556
|
+
if (!flags.quiet) {
|
|
557
|
+
const tool = event.data?.tool || event.data?.name || JSON.stringify(event.data);
|
|
558
|
+
process.stderr.write('\n[tool: ' + tool + ']\n');
|
|
559
|
+
}
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
case 'status': {
|
|
564
|
+
if (!flags.quiet && event.data?.message) {
|
|
565
|
+
process.stderr.write('[status] ' + event.data.message + '\n');
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
case 'error': {
|
|
571
|
+
process.stderr.write('\n[Error] ' + (event.data?.message || JSON.stringify(event.data)) + '\n');
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case 'done':
|
|
576
|
+
case 'close':
|
|
577
|
+
process.stdout.write('\n');
|
|
578
|
+
break;
|
|
579
|
+
|
|
580
|
+
default:
|
|
581
|
+
if (flags.debug) process.stderr.write('[event:' + event.type + '] ' + JSON.stringify(event.data) + '\n');
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Find a reusable session for agentId+cwd, or create a fresh one.
|
|
589
|
+
*
|
|
590
|
+
* Resolution order:
|
|
591
|
+
* 1. --session=<key> → validate it exists and is not closed/stale, use it
|
|
592
|
+
* 2. listSessions() → find first session matching agent_id (and cwd if --cwd given)
|
|
593
|
+
* whose state is not 'closed' or 'stale'
|
|
594
|
+
* 3. createSession() → spawn a new process
|
|
595
|
+
*
|
|
596
|
+
* Pass --new to always create a fresh session regardless.
|
|
597
|
+
*/
|
|
598
|
+
async function findOrCreateSession(sdk, agentId) {
|
|
599
|
+
const cwd = flags.cwd || process.cwd();
|
|
600
|
+
const policy = flags.policy || 'approve-all';
|
|
601
|
+
|
|
602
|
+
// Force-new
|
|
603
|
+
if (flags.new) {
|
|
604
|
+
process.stderr.write('Creating new ACP session for "' + agentId + '"...\n');
|
|
605
|
+
const s = await sdk.acpAgent.createSession({ agent_id: agentId, cwd, approvalPolicy: policy, ...(flags.model ? { model: flags.model } : {}) });
|
|
606
|
+
process.stderr.write('Session: ' + s.session_key + '\n');
|
|
607
|
+
return s.session_key;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Explicit key
|
|
611
|
+
if (flags.session) {
|
|
612
|
+
try {
|
|
613
|
+
const status = await sdk.acpAgent.getSession(flags.session);
|
|
614
|
+
const state = status?.state || status?.runtime_options?.state;
|
|
615
|
+
if (state !== 'closed' && state !== 'stale') {
|
|
616
|
+
process.stderr.write('Reusing session (--session): ' + flags.session + '\n');
|
|
617
|
+
return flags.session;
|
|
618
|
+
}
|
|
619
|
+
process.stderr.write('Session ' + flags.session + ' is ' + state + ', will create new.\n');
|
|
620
|
+
} catch (e) {
|
|
621
|
+
process.stderr.write('Session lookup failed (' + (e.message || e) + '), will create new.\n');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Search existing sessions
|
|
626
|
+
try {
|
|
627
|
+
const sessions = await sdk.acpAgent.listSessions();
|
|
628
|
+
if (Array.isArray(sessions)) {
|
|
629
|
+
for (const s of sessions) {
|
|
630
|
+
const key = s.session_key || s.key;
|
|
631
|
+
const sid = s.agent_id || s.agentId;
|
|
632
|
+
const state = s.state || s.runtime_options?.state;
|
|
633
|
+
const sCwd = s.cwd || s.runtime_options?.cwd;
|
|
634
|
+
if (state === 'closed' || state === 'stale') continue;
|
|
635
|
+
if (sid && sid !== agentId) continue;
|
|
636
|
+
if (flags.cwd && sCwd && sCwd !== cwd) continue;
|
|
637
|
+
if (key) {
|
|
638
|
+
process.stderr.write('Reusing existing session: ' + key + (sid ? ' (' + sid + ')' : '') + '\n');
|
|
639
|
+
return key;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch (_) { /* listSessions not supported or empty — fall through */ }
|
|
644
|
+
|
|
645
|
+
// Create new
|
|
646
|
+
process.stderr.write('Creating new ACP session for "' + agentId + '"...\n');
|
|
647
|
+
const session = await sdk.acpAgent.createSession({
|
|
648
|
+
agent_id: agentId,
|
|
649
|
+
cwd,
|
|
650
|
+
approvalPolicy: policy,
|
|
651
|
+
...(flags.model ? { model: flags.model } : {}),
|
|
652
|
+
});
|
|
653
|
+
process.stderr.write('Session: ' + session.session_key + '\n');
|
|
654
|
+
return session.session_key;
|
|
655
|
+
}
|
|
656
|
+
|
|
319
657
|
// -- acp-chat ---------------------------------------------------------------
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
658
|
+
// Smart version: reuses existing sessions, handles permission_request via resolvePermission().
|
|
659
|
+
//
|
|
660
|
+
// Flags:
|
|
661
|
+
// --session=<key> Reuse this specific session key
|
|
662
|
+
// --new Always create a fresh session
|
|
663
|
+
// --cwd=<path> Working directory (used for new session + matching)
|
|
664
|
+
// --model=<m> Model override
|
|
665
|
+
// --policy=approve-all approvalPolicy for new sessions
|
|
666
|
+
// --policy-override=<p> Runtime permission decision: approve-all|deny-all|prompt
|
|
667
|
+
// --close Close the session after this turn
|
|
668
|
+
// --thoughts Print reasoning/thinking tokens to stderr
|
|
669
|
+
// --quiet Suppress tool/status/permission stderr output
|
|
670
|
+
// --debug Print all unhandled SSE event types
|
|
324
671
|
CMD['acp-chat'] = async () => {
|
|
325
672
|
const [agentId, ...msgParts] = cmdArgs;
|
|
326
673
|
const message = msgParts.join(' ');
|
|
327
674
|
if (!agentId || !message) {
|
|
328
|
-
console.error('Usage: rtx.js acp-chat <agent-id> <message
|
|
675
|
+
console.error('Usage: rtx.js acp-chat <agent-id> <message>\n' +
|
|
676
|
+
' [--session=<key>] [--new] [--cwd=<path>] [--model=<m>]\n' +
|
|
677
|
+
' [--policy=approve-all] [--policy-override=approve-all|deny-all|prompt]\n' +
|
|
678
|
+
' [--close] [--thoughts] [--quiet] [--debug]');
|
|
329
679
|
process.exit(1);
|
|
330
680
|
}
|
|
331
681
|
const { sdk } = await getSDK();
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
process.stderr.write('Creating ACP session for "' + agentId + '"...\n');
|
|
335
|
-
const session = await sdk.acpAgent.createSession(sessionOpts);
|
|
682
|
+
const sessionKey = await findOrCreateSession(sdk, agentId);
|
|
683
|
+
|
|
336
684
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
process.stderr.write('\nError: ' + event.data.message + '\n');
|
|
345
|
-
} else if (event.type === 'done' || event.type === 'close') {
|
|
346
|
-
process.stdout.write('\n');
|
|
347
|
-
}
|
|
685
|
+
await acpStreamWithPermissions(sdk, sessionKey, message);
|
|
686
|
+
} finally {
|
|
687
|
+
if (flags.close) {
|
|
688
|
+
await sdk.acpAgent.closeSession(sessionKey, 'chat complete').catch(() => {});
|
|
689
|
+
process.stderr.write('Session closed.\n');
|
|
690
|
+
} else {
|
|
691
|
+
process.stderr.write('Session key (reuse with --session=' + sessionKey + ')\n');
|
|
348
692
|
}
|
|
349
|
-
}
|
|
693
|
+
}
|
|
350
694
|
};
|
|
351
695
|
|
|
352
696
|
// -- tts-providers / stt-providers ------------------------------------------
|
|
@@ -453,11 +797,55 @@ sdk.mcp.*:
|
|
|
453
797
|
mcp-tools <server> [--provider]
|
|
454
798
|
mcp-exec <server> <tool> [<args-json>] [--provider]
|
|
455
799
|
|
|
456
|
-
sdk.acpAgent
|
|
800
|
+
sdk.acpAgent.* — Session Management:
|
|
457
801
|
acp-agents [--models=true]
|
|
802
|
+
List available ACP CLI agents.
|
|
803
|
+
|
|
458
804
|
acp-sessions
|
|
805
|
+
List all active sessions owned by this app.
|
|
806
|
+
|
|
807
|
+
acp-session-create <agent-id>
|
|
808
|
+
[--cwd=<path>] [--model=<m>] [--policy=approve-all|approve-reads|deny-all]
|
|
809
|
+
Spawn a new agent process. Prints session_key for reuse.
|
|
810
|
+
|
|
811
|
+
acp-session-get <session-key>
|
|
812
|
+
Get session status and runtime options.
|
|
813
|
+
|
|
814
|
+
acp-session-patch <session-key>
|
|
815
|
+
[--cwd=<path>] [--model=<m>] [--policy=<p>] [--timeout=<s>]
|
|
816
|
+
Update runtime options (applied on next turn).
|
|
817
|
+
|
|
818
|
+
acp-session-close <session-key> [<reason>]
|
|
819
|
+
Stop the agent process and close the session.
|
|
820
|
+
|
|
821
|
+
acp-cancel <session-key> [<reason>]
|
|
822
|
+
Cancel the currently active turn on a session.
|
|
823
|
+
|
|
824
|
+
acp-resolve <session-key> <request-id> <option-id> [--outcome=approved]
|
|
825
|
+
Manually resolve a pending permission request (while stream is active).
|
|
826
|
+
|
|
827
|
+
acp-send <session-key> <message>
|
|
828
|
+
Synchronous chat on an existing session (waits for full reply).
|
|
829
|
+
|
|
830
|
+
acp-stream <session-key> <message>
|
|
831
|
+
[--policy-override=approve-all|deny-all|prompt] [--thoughts] [--quiet] [--debug]
|
|
832
|
+
Streaming chat on an existing session. Auto-resolves permission_request events.
|
|
833
|
+
|
|
459
834
|
acp-chat <agent-id> <message>
|
|
460
|
-
[--
|
|
835
|
+
[--session=<key>] Reuse a specific session key
|
|
836
|
+
[--new] Force-create a new session
|
|
837
|
+
[--cwd=<path>] Working directory (for new session and session matching)
|
|
838
|
+
[--model=<m>] Model override
|
|
839
|
+
[--policy=approve-all] approvalPolicy for newly created sessions
|
|
840
|
+
[--policy-override=approve-all|deny-all|prompt] Runtime permission handling
|
|
841
|
+
[--close] Close session after this turn
|
|
842
|
+
[--thoughts] Print reasoning tokens to stderr
|
|
843
|
+
[--quiet] Suppress tool/status/permission stderr output
|
|
844
|
+
[--debug] Print all unhandled SSE events
|
|
845
|
+
|
|
846
|
+
Smart session reuse: checks --session flag → searches listSessions() for a
|
|
847
|
+
compatible active session → creates new only if none found.
|
|
848
|
+
Handles permission_request events inline via resolvePermission().
|
|
461
849
|
|
|
462
850
|
sdk.tts.* / sdk.stt.*:
|
|
463
851
|
tts-providers / stt-providers
|