wave-code 1.0.0 → 1.0.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/dist/cli.js +20 -1
- package/dist/components/App.js +7 -0
- package/dist/components/BtwDisplay.js +13 -3
- package/dist/components/ChatInterface.js +21 -6
- package/dist/components/InputBox.d.ts +0 -3
- package/dist/components/InputBox.js +11 -9
- package/dist/components/LoadingIndicator.d.ts +1 -2
- package/dist/components/LoadingIndicator.js +2 -2
- package/dist/components/Markdown.js +13 -16
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.js +2 -2
- package/dist/components/StatusLine.d.ts +0 -2
- package/dist/components/StatusLine.js +6 -6
- package/dist/components/TaskList.js +2 -1
- package/dist/components/ToolDisplay.d.ts +1 -0
- package/dist/components/ToolDisplay.js +17 -9
- package/dist/constants/commands.js +0 -6
- package/dist/contexts/useChat.d.ts +3 -5
- package/dist/contexts/useChat.js +242 -82
- package/dist/daemon-cli.d.ts +10 -0
- package/dist/daemon-cli.js +15 -0
- package/dist/hooks/useInputManager.js +99 -22
- package/dist/index.js +10 -0
- package/dist/managers/inputHandlers.js +50 -22
- package/dist/managers/inputReducer.d.ts +12 -2
- package/dist/managers/inputReducer.js +57 -9
- package/dist/stdio/agentBridge.d.ts +23 -0
- package/dist/stdio/agentBridge.js +126 -16
- package/dist/stdio/daemonServer.d.ts +67 -0
- package/dist/stdio/daemonServer.js +191 -0
- package/dist/stdio/index.d.ts +2 -0
- package/dist/stdio/index.js +2 -0
- package/dist/stdio/jsonRpcConnection.d.ts +30 -0
- package/dist/stdio/jsonRpcConnection.js +127 -0
- package/dist/stdio/protocol.d.ts +2 -2
- package/dist/stdio/stdioServer.d.ts +2 -7
- package/dist/stdio/stdioServer.js +9 -100
- package/dist/utils/bracketedPaste.d.ts +39 -0
- package/dist/utils/bracketedPaste.js +122 -0
- package/dist/utils/markdownTable.d.ts +34 -0
- package/dist/utils/markdownTable.js +302 -0
- package/dist/utils/throttle.d.ts +3 -3
- package/package.json +4 -2
- package/src/cli.tsx +20 -1
- package/src/components/App.tsx +5 -0
- package/src/components/BtwDisplay.tsx +36 -12
- package/src/components/ChatInterface.tsx +25 -12
- package/src/components/InputBox.tsx +10 -18
- package/src/components/LoadingIndicator.tsx +1 -4
- package/src/components/Markdown.tsx +15 -18
- package/src/components/MessageList.tsx +6 -0
- package/src/components/StatusLine.tsx +0 -10
- package/src/components/TaskList.tsx +2 -1
- package/src/components/ToolDisplay.tsx +17 -6
- package/src/constants/commands.ts +0 -6
- package/src/contexts/useChat.tsx +310 -95
- package/src/daemon-cli.ts +17 -0
- package/src/hooks/useInputManager.ts +108 -22
- package/src/index.ts +12 -0
- package/src/managers/inputHandlers.ts +49 -22
- package/src/managers/inputReducer.ts +66 -11
- package/src/stdio/agentBridge.ts +188 -17
- package/src/stdio/daemonServer.ts +212 -0
- package/src/stdio/index.ts +2 -0
- package/src/stdio/jsonRpcConnection.ts +160 -0
- package/src/stdio/protocol.ts +5 -2
- package/src/stdio/stdioServer.ts +14 -120
- package/src/utils/bracketedPaste.ts +170 -0
- package/src/utils/markdownTable.ts +359 -0
- package/src/utils/throttle.ts +8 -8
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DaemonServer — JSON-RPC server over a unix socket for remote background
|
|
3
|
+
* sessions (spec: docs/specs/ui/desktop-app.md 「SSH 远程后台会话」).
|
|
4
|
+
*
|
|
5
|
+
* The desktop app launches `wave --daemon <socket>` on the remote host via
|
|
6
|
+
* nohup/setsid, then tunnels the socket back with `ssh -L`. All connections
|
|
7
|
+
* share one AgentBridge, so sessions and pending tool permissions survive
|
|
8
|
+
* client detach/attach — the daemon keeps running (and generating) while no
|
|
9
|
+
* desktop is connected. The daemon never exits on a client disconnect; it
|
|
10
|
+
* only exits when killed (app quit / 删除会话 / remote reboot).
|
|
11
|
+
*
|
|
12
|
+
* Idle auto-exit (spec: 「远程 daemon 空闲自动退出」): once every session has
|
|
13
|
+
* settled and no client is connected, the daemon mirrors `wave -p`'s exit
|
|
14
|
+
* semantics — after a grace period it destroys the sessions (saving their
|
|
15
|
+
* transcripts), closes the socket, and exits, so the remote process doesn't
|
|
16
|
+
* linger forever after background work completes.
|
|
17
|
+
*/
|
|
18
|
+
import net from "net";
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import { AgentBridge } from "./agentBridge.js";
|
|
21
|
+
import { JsonRpcConnection } from "./jsonRpcConnection.js";
|
|
22
|
+
export class DaemonServer {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.connections = new Set();
|
|
25
|
+
this.sockets = new Set();
|
|
26
|
+
this.shuttingDown = false;
|
|
27
|
+
this.stopped = false;
|
|
28
|
+
this.socketPath = options.socketPath;
|
|
29
|
+
this.graceMs = options.graceMs ?? DaemonServer.DEFAULT_IDLE_GRACE_MS;
|
|
30
|
+
this.bridge = new AgentBridge({
|
|
31
|
+
...options.bridgeOptions,
|
|
32
|
+
// Notifications go to every attached client; a fully detached daemon
|
|
33
|
+
// has none, and the write is dropped silently (the attach snapshot
|
|
34
|
+
// re-syncs state on reconnect).
|
|
35
|
+
emit: (method, params, sessionId) => {
|
|
36
|
+
for (const conn of this.connections) {
|
|
37
|
+
conn.sendNotification(method, params, sessionId);
|
|
38
|
+
}
|
|
39
|
+
// Any session activity can change the idle state — re-evaluate.
|
|
40
|
+
this.evaluateIdle();
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
this.server = net.createServer((socket) => {
|
|
44
|
+
if (this.shuttingDown) {
|
|
45
|
+
socket.destroy();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const conn = new JsonRpcConnection(socket, socket, this.bridge);
|
|
49
|
+
this.connections.add(conn);
|
|
50
|
+
this.sockets.add(socket);
|
|
51
|
+
// A (re)attached client cancels a pending idle exit.
|
|
52
|
+
this.evaluateIdle();
|
|
53
|
+
socket.on("error", () => {
|
|
54
|
+
// The client (ssh tunnel) can reset the socket mid-detach; the daemon
|
|
55
|
+
// must keep running — 'close' below cleans up the connection.
|
|
56
|
+
});
|
|
57
|
+
socket.on("close", () => {
|
|
58
|
+
this.connections.delete(conn);
|
|
59
|
+
this.sockets.delete(socket);
|
|
60
|
+
// A client detach may leave the daemon idle — re-evaluate.
|
|
61
|
+
this.evaluateIdle();
|
|
62
|
+
});
|
|
63
|
+
conn.start();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
get agentBridge() {
|
|
67
|
+
return this.bridge;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Listen on the socket path. Rejects when a live daemon already holds it. A
|
|
71
|
+
* stale socket file left by a crashed daemon would otherwise block the
|
|
72
|
+
* restart with EADDRINUSE, so it is cleaned up first: non-socket files are
|
|
73
|
+
* unlinked outright; socket files are probe-connected — ECONNREFUSED means
|
|
74
|
+
* no listener (stale → unlink and listen), anything else means a live
|
|
75
|
+
* daemon owns the path (reject).
|
|
76
|
+
*/
|
|
77
|
+
start() {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const server = this.server;
|
|
80
|
+
if (!server)
|
|
81
|
+
return resolve();
|
|
82
|
+
try {
|
|
83
|
+
const st = fs.statSync(this.socketPath);
|
|
84
|
+
if (st.isSocket()) {
|
|
85
|
+
const probe = net.connect(this.socketPath);
|
|
86
|
+
probe.once("connect", () => {
|
|
87
|
+
probe.destroy();
|
|
88
|
+
reject(new Error(`另一个 wave daemon 已在 ${this.socketPath} 监听`));
|
|
89
|
+
});
|
|
90
|
+
probe.once("error", (err) => {
|
|
91
|
+
probe.destroy();
|
|
92
|
+
if (err.code === "ECONNREFUSED") {
|
|
93
|
+
fs.unlinkSync(this.socketPath);
|
|
94
|
+
this.listen(server, resolve, reject);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
reject(err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
fs.unlinkSync(this.socketPath);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ENOENT — no stale socket, listen directly.
|
|
106
|
+
}
|
|
107
|
+
this.listen(server, resolve, reject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
listen(server, resolve, reject) {
|
|
111
|
+
server.once("error", reject);
|
|
112
|
+
server.listen(this.socketPath, () => {
|
|
113
|
+
server.removeListener("error", reject);
|
|
114
|
+
resolve();
|
|
115
|
+
// A freshly started daemon may already be idle (no sessions) — start the
|
|
116
|
+
// idle watch so a zero-session daemon also auto-exits.
|
|
117
|
+
this.evaluateIdle();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
stop() {
|
|
121
|
+
this.stopped = true;
|
|
122
|
+
this.clearIdleTimer();
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const server = this.server;
|
|
125
|
+
if (!server)
|
|
126
|
+
return resolve();
|
|
127
|
+
this.server = undefined;
|
|
128
|
+
server.close(() => resolve());
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// ── Idle auto-exit ────────────────────────────────────────────
|
|
132
|
+
clearIdleTimer() {
|
|
133
|
+
if (this.idleTimer) {
|
|
134
|
+
clearTimeout(this.idleTimer);
|
|
135
|
+
this.idleTimer = undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Re-evaluate the idle condition after any state transition: sessions busy
|
|
140
|
+
* (loading / pending messages / background work) or any client attached →
|
|
141
|
+
* cancel the timer. Fully idle + detached → arm the grace timer once; when
|
|
142
|
+
* it fires, shut the daemon down. Evaluation is event-driven (every
|
|
143
|
+
* busy→idle transition emits a notification, every attach/detach fires a
|
|
144
|
+
* connection event), so nothing is polled. The failure mode is
|
|
145
|
+
* conservative: a missed transition just leaves the daemon running.
|
|
146
|
+
*/
|
|
147
|
+
evaluateIdle() {
|
|
148
|
+
// A stopped/shutting-down daemon never (re)arms the idle timer — late
|
|
149
|
+
// socket 'close' events (which fire after server.close resolves) must not
|
|
150
|
+
// resurrect a timer after stop().
|
|
151
|
+
if (this.shuttingDown || this.stopped)
|
|
152
|
+
return;
|
|
153
|
+
if (this.connections.size > 0 || !this.bridge.isIdle()) {
|
|
154
|
+
this.clearIdleTimer();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (this.idleTimer)
|
|
158
|
+
return;
|
|
159
|
+
this.idleTimer = setTimeout(() => {
|
|
160
|
+
this.idleTimer = undefined;
|
|
161
|
+
void this.shutdown();
|
|
162
|
+
}, this.graceMs);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Destroy the sessions (each agent saves its transcript and drains
|
|
166
|
+
* auto-memory), close the listener, unlink the socket file, then exit.
|
|
167
|
+
* `shuttingDown` guards against re-entry: new connections are refused and
|
|
168
|
+
* further idle evaluations become no-ops.
|
|
169
|
+
*/
|
|
170
|
+
async shutdown() {
|
|
171
|
+
if (this.shuttingDown)
|
|
172
|
+
return;
|
|
173
|
+
this.shuttingDown = true;
|
|
174
|
+
this.clearIdleTimer();
|
|
175
|
+
// Destroy client sockets first so server.close() can complete (an open
|
|
176
|
+
// socket keeps the close callback pending).
|
|
177
|
+
for (const socket of this.sockets)
|
|
178
|
+
socket.destroy();
|
|
179
|
+
this.sockets.clear();
|
|
180
|
+
await this.bridge.destroyAll();
|
|
181
|
+
await this.stop();
|
|
182
|
+
try {
|
|
183
|
+
fs.unlinkSync(this.socketPath);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Already gone — a stale file would be probed/unlinked on next start.
|
|
187
|
+
}
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
DaemonServer.DEFAULT_IDLE_GRACE_MS = 60000;
|
package/dist/stdio/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { StdioServer, type StdioServerOptions } from "./stdioServer.js";
|
|
2
|
+
export { JsonRpcConnection } from "./jsonRpcConnection.js";
|
|
3
|
+
export { DaemonServer, type DaemonServerOptions } from "./daemonServer.js";
|
|
2
4
|
export { AgentBridge, type AgentBridgeOptions, RpcError, } from "./agentBridge.js";
|
|
3
5
|
export * from "./protocol.js";
|
package/dist/stdio/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonRpcConnection — one JSON-RPC connection over a Readable/Writable pair,
|
|
3
|
+
* dispatching to a shared AgentBridge.
|
|
4
|
+
*
|
|
5
|
+
* Shared by StdioServer (process stdin/stdout) and DaemonServer (each unix
|
|
6
|
+
* socket connection). The AgentBridge owns all session/agent state, so any
|
|
7
|
+
* number of connections can share it — a detached client loses nothing and
|
|
8
|
+
* every session keeps running on the daemon side.
|
|
9
|
+
*/
|
|
10
|
+
import type { Readable, Writable } from "stream";
|
|
11
|
+
import { AgentBridge } from "./agentBridge.js";
|
|
12
|
+
export declare class JsonRpcConnection {
|
|
13
|
+
private input;
|
|
14
|
+
private output;
|
|
15
|
+
private bridge;
|
|
16
|
+
private rl;
|
|
17
|
+
private started;
|
|
18
|
+
constructor(input: Readable, output: Writable, bridge: AgentBridge);
|
|
19
|
+
start(): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
handleLine(line: string): Promise<void>;
|
|
22
|
+
private handleRequest;
|
|
23
|
+
private handleNotification;
|
|
24
|
+
sendResponse(id: number | string | null, result?: unknown, error?: {
|
|
25
|
+
code: number;
|
|
26
|
+
message: string;
|
|
27
|
+
}): void;
|
|
28
|
+
sendNotification(method: string, params?: unknown, sessionId?: string): void;
|
|
29
|
+
private write;
|
|
30
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonRpcConnection — one JSON-RPC connection over a Readable/Writable pair,
|
|
3
|
+
* dispatching to a shared AgentBridge.
|
|
4
|
+
*
|
|
5
|
+
* Shared by StdioServer (process stdin/stdout) and DaemonServer (each unix
|
|
6
|
+
* socket connection). The AgentBridge owns all session/agent state, so any
|
|
7
|
+
* number of connections can share it — a detached client loses nothing and
|
|
8
|
+
* every session keeps running on the daemon side.
|
|
9
|
+
*/
|
|
10
|
+
import readline from "readline";
|
|
11
|
+
import { PARSE_ERROR, INVALID_REQUEST, INTERNAL_ERROR, isRequest, isNotification, } from "./protocol.js";
|
|
12
|
+
export class JsonRpcConnection {
|
|
13
|
+
constructor(input, output, bridge) {
|
|
14
|
+
this.input = input;
|
|
15
|
+
this.output = output;
|
|
16
|
+
this.bridge = bridge;
|
|
17
|
+
this.started = false;
|
|
18
|
+
}
|
|
19
|
+
start() {
|
|
20
|
+
if (this.started)
|
|
21
|
+
return;
|
|
22
|
+
this.started = true;
|
|
23
|
+
this.rl = readline.createInterface({
|
|
24
|
+
input: this.input,
|
|
25
|
+
crlfDelay: Infinity,
|
|
26
|
+
});
|
|
27
|
+
// readline re-emits input errors on the Interface; error handling on the
|
|
28
|
+
// underlying stream is the owner's job (the daemon keeps running on a
|
|
29
|
+
// client reset), so swallow them here.
|
|
30
|
+
this.rl.on("error", () => { });
|
|
31
|
+
this.rl.on("line", (line) => {
|
|
32
|
+
this.handleLine(line).catch((err) => {
|
|
33
|
+
// Should never reach here — handleLine catches internally
|
|
34
|
+
this.sendResponse(null, undefined, {
|
|
35
|
+
code: INTERNAL_ERROR,
|
|
36
|
+
message: `Unhandled error: ${err.message}`,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
this.rl.on("close", () => {
|
|
41
|
+
this.started = false;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
stop() {
|
|
45
|
+
this.rl?.close();
|
|
46
|
+
this.rl = undefined;
|
|
47
|
+
this.started = false;
|
|
48
|
+
}
|
|
49
|
+
async handleLine(line) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
return;
|
|
53
|
+
let msg;
|
|
54
|
+
try {
|
|
55
|
+
msg = JSON.parse(trimmed);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
this.sendResponse(null, undefined, {
|
|
59
|
+
code: PARSE_ERROR,
|
|
60
|
+
message: "Parse error: invalid JSON",
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (isRequest(msg)) {
|
|
65
|
+
await this.handleRequest(msg);
|
|
66
|
+
}
|
|
67
|
+
else if (isNotification(msg)) {
|
|
68
|
+
this.handleNotification(msg);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Echo back the id if the message has one, otherwise null
|
|
72
|
+
const id = typeof msg === "object" &&
|
|
73
|
+
msg !== null &&
|
|
74
|
+
"id" in msg &&
|
|
75
|
+
(typeof msg.id === "number" ||
|
|
76
|
+
typeof msg.id === "string")
|
|
77
|
+
? msg.id
|
|
78
|
+
: null;
|
|
79
|
+
this.sendResponse(id, undefined, {
|
|
80
|
+
code: INVALID_REQUEST,
|
|
81
|
+
message: "Invalid request: must have 'method' field",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async handleRequest(msg) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await this.bridge.handleRequest(msg.method, msg.params, msg.sessionId);
|
|
88
|
+
this.sendResponse(msg.id, result);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const code = err && typeof err === "object" && "code" in err
|
|
92
|
+
? err.code
|
|
93
|
+
: INTERNAL_ERROR;
|
|
94
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
+
this.sendResponse(msg.id, undefined, { code, message });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
handleNotification(msg) {
|
|
99
|
+
try {
|
|
100
|
+
this.bridge.handleNotification(msg.method, msg.params);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
// Notifications don't get responses, but we log to stderr
|
|
104
|
+
process.stderr.write(`Error handling notification ${msg.method}: ${err.message}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ── Output helpers ────────────────────────────────────────────
|
|
108
|
+
sendResponse(id, result, error) {
|
|
109
|
+
const response = { id };
|
|
110
|
+
if (error) {
|
|
111
|
+
response.error = error;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
response.result = result ?? null;
|
|
115
|
+
}
|
|
116
|
+
this.write(response);
|
|
117
|
+
}
|
|
118
|
+
sendNotification(method, params, sessionId) {
|
|
119
|
+
const notification = { method, params };
|
|
120
|
+
if (sessionId)
|
|
121
|
+
notification.sessionId = sessionId;
|
|
122
|
+
this.write(notification);
|
|
123
|
+
}
|
|
124
|
+
write(obj) {
|
|
125
|
+
this.output.write(JSON.stringify(obj) + "\n");
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/stdio/protocol.d.ts
CHANGED
|
@@ -34,8 +34,8 @@ export declare const INVALID_REQUEST = -32600;
|
|
|
34
34
|
export declare const METHOD_NOT_FOUND = -32601;
|
|
35
35
|
export declare const INVALID_PARAMS = -32602;
|
|
36
36
|
export declare const INTERNAL_ERROR = -32603;
|
|
37
|
-
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
37
|
+
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "listPendingPermissions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
38
38
|
export type ClientNotificationMethod = "permissionResponse";
|
|
39
|
-
export type ServerNotificationMethod = "
|
|
39
|
+
export type ServerNotificationMethod = "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange" | "btwContent";
|
|
40
40
|
export declare function isRequest(msg: unknown): msg is JsonRpcRequest;
|
|
41
41
|
export declare function isNotification(msg: unknown): msg is JsonRpcNotification;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* and writes responses / notifications to stdout.
|
|
4
4
|
*
|
|
5
5
|
* One JSON object per line. stderr is reserved for logger output.
|
|
6
|
+
* Line handling lives in JsonRpcConnection (shared with DaemonServer).
|
|
6
7
|
*/
|
|
7
8
|
import type { Readable, Writable } from "stream";
|
|
8
9
|
import { AgentBridge, type AgentBridgeOptions } from "./agentBridge.js";
|
|
@@ -12,22 +13,16 @@ export interface StdioServerOptions {
|
|
|
12
13
|
bridgeOptions?: AgentBridgeOptions;
|
|
13
14
|
}
|
|
14
15
|
export declare class StdioServer {
|
|
15
|
-
private rl;
|
|
16
16
|
private bridge;
|
|
17
|
-
private
|
|
18
|
-
private output;
|
|
19
|
-
private started;
|
|
17
|
+
private conn;
|
|
20
18
|
constructor(options?: StdioServerOptions);
|
|
21
19
|
get agentBridge(): AgentBridge;
|
|
22
20
|
start(): void;
|
|
23
21
|
stop(): void;
|
|
24
22
|
handleLine(line: string): Promise<void>;
|
|
25
|
-
private handleRequest;
|
|
26
|
-
private handleNotification;
|
|
27
23
|
sendResponse(id: number | string | null, result?: unknown, error?: {
|
|
28
24
|
code: number;
|
|
29
25
|
message: string;
|
|
30
26
|
}): void;
|
|
31
27
|
sendNotification(method: string, params?: unknown, sessionId?: string): void;
|
|
32
|
-
private write;
|
|
33
28
|
}
|
|
@@ -3,125 +3,34 @@
|
|
|
3
3
|
* and writes responses / notifications to stdout.
|
|
4
4
|
*
|
|
5
5
|
* One JSON object per line. stderr is reserved for logger output.
|
|
6
|
+
* Line handling lives in JsonRpcConnection (shared with DaemonServer).
|
|
6
7
|
*/
|
|
7
|
-
import readline from "readline";
|
|
8
8
|
import { AgentBridge } from "./agentBridge.js";
|
|
9
|
-
import {
|
|
9
|
+
import { JsonRpcConnection } from "./jsonRpcConnection.js";
|
|
10
10
|
export class StdioServer {
|
|
11
11
|
constructor(options = {}) {
|
|
12
|
-
this.started = false;
|
|
13
|
-
this.input = options.input ?? process.stdin;
|
|
14
|
-
this.output = options.output ?? process.stdout;
|
|
15
12
|
this.bridge = new AgentBridge({
|
|
16
13
|
...options.bridgeOptions,
|
|
17
14
|
emit: (method, params, sessionId) => this.sendNotification(method, params, sessionId),
|
|
18
15
|
});
|
|
16
|
+
this.conn = new JsonRpcConnection(options.input ?? process.stdin, options.output ?? process.stdout, this.bridge);
|
|
19
17
|
}
|
|
20
18
|
get agentBridge() {
|
|
21
19
|
return this.bridge;
|
|
22
20
|
}
|
|
23
21
|
start() {
|
|
24
|
-
|
|
25
|
-
return;
|
|
26
|
-
this.started = true;
|
|
27
|
-
this.rl = readline.createInterface({
|
|
28
|
-
input: this.input,
|
|
29
|
-
crlfDelay: Infinity,
|
|
30
|
-
});
|
|
31
|
-
this.rl.on("line", (line) => {
|
|
32
|
-
this.handleLine(line).catch((err) => {
|
|
33
|
-
// Should never reach here — handleLine catches internally
|
|
34
|
-
this.sendResponse(null, undefined, {
|
|
35
|
-
code: INTERNAL_ERROR,
|
|
36
|
-
message: `Unhandled error: ${err.message}`,
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
this.rl.on("close", () => {
|
|
41
|
-
this.started = false;
|
|
42
|
-
});
|
|
22
|
+
this.conn.start();
|
|
43
23
|
}
|
|
44
24
|
stop() {
|
|
45
|
-
this.
|
|
46
|
-
this.rl = undefined;
|
|
47
|
-
this.started = false;
|
|
25
|
+
this.conn.stop();
|
|
48
26
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!trimmed)
|
|
52
|
-
return;
|
|
53
|
-
let msg;
|
|
54
|
-
try {
|
|
55
|
-
msg = JSON.parse(trimmed);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
this.sendResponse(null, undefined, {
|
|
59
|
-
code: PARSE_ERROR,
|
|
60
|
-
message: "Parse error: invalid JSON",
|
|
61
|
-
});
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (isRequest(msg)) {
|
|
65
|
-
await this.handleRequest(msg);
|
|
66
|
-
}
|
|
67
|
-
else if (isNotification(msg)) {
|
|
68
|
-
this.handleNotification(msg);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
// Echo back the id if the message has one, otherwise null
|
|
72
|
-
const id = typeof msg === "object" &&
|
|
73
|
-
msg !== null &&
|
|
74
|
-
"id" in msg &&
|
|
75
|
-
(typeof msg.id === "number" ||
|
|
76
|
-
typeof msg.id === "string")
|
|
77
|
-
? msg.id
|
|
78
|
-
: null;
|
|
79
|
-
this.sendResponse(id, undefined, {
|
|
80
|
-
code: INVALID_REQUEST,
|
|
81
|
-
message: "Invalid request: must have 'method' field",
|
|
82
|
-
});
|
|
83
|
-
}
|
|
27
|
+
handleLine(line) {
|
|
28
|
+
return this.conn.handleLine(line);
|
|
84
29
|
}
|
|
85
|
-
async handleRequest(msg) {
|
|
86
|
-
try {
|
|
87
|
-
const result = await this.bridge.handleRequest(msg.method, msg.params, msg.sessionId);
|
|
88
|
-
this.sendResponse(msg.id, result);
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
const code = err && typeof err === "object" && "code" in err
|
|
92
|
-
? err.code
|
|
93
|
-
: INTERNAL_ERROR;
|
|
94
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
-
this.sendResponse(msg.id, undefined, { code, message });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
handleNotification(msg) {
|
|
99
|
-
try {
|
|
100
|
-
this.bridge.handleNotification(msg.method, msg.params);
|
|
101
|
-
}
|
|
102
|
-
catch (err) {
|
|
103
|
-
// Notifications don't get responses, but we log to stderr
|
|
104
|
-
process.stderr.write(`Error handling notification ${msg.method}: ${err.message}\n`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// ── Output helpers ────────────────────────────────────────────
|
|
108
30
|
sendResponse(id, result, error) {
|
|
109
|
-
|
|
110
|
-
if (error) {
|
|
111
|
-
response.error = error;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
response.result = result ?? null;
|
|
115
|
-
}
|
|
116
|
-
this.write(response);
|
|
31
|
+
this.conn.sendResponse(id, result, error);
|
|
117
32
|
}
|
|
118
33
|
sendNotification(method, params, sessionId) {
|
|
119
|
-
|
|
120
|
-
if (sessionId)
|
|
121
|
-
notification.sessionId = sessionId;
|
|
122
|
-
this.write(notification);
|
|
123
|
-
}
|
|
124
|
-
write(obj) {
|
|
125
|
-
this.output.write(JSON.stringify(obj) + "\n");
|
|
34
|
+
this.conn.sendNotification(method, params, sessionId);
|
|
126
35
|
}
|
|
127
36
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bracketed paste (DECSET 2004) detector.
|
|
3
|
+
*
|
|
4
|
+
* When bracketed paste is enabled, terminals wrap pasted text in
|
|
5
|
+
* `\x1b[200~` (start) and `\x1b[201~` (end) markers, which lets the input
|
|
6
|
+
* pipeline distinguish "text was pasted" from "user typed keys" — most
|
|
7
|
+
* importantly, a pasted trailing `\r` must NOT be treated as an Enter key.
|
|
8
|
+
*
|
|
9
|
+
* ink delivers each stdin chunk through useInput after stripping ONE leading
|
|
10
|
+
* ESC from the parsed sequence (see use-input.js), so markers may arrive in
|
|
11
|
+
* either the raw form (`\x1b[200~`) or the stripped form (`[200~`), and may
|
|
12
|
+
* be split across chunks. The detector normalizes both forms and buffers
|
|
13
|
+
* partial markers until they complete (deferral is safe: a deferred partial
|
|
14
|
+
* is flushed unchanged if the next chunk does not complete a marker).
|
|
15
|
+
*/
|
|
16
|
+
export type PasteProcessResult = {
|
|
17
|
+
kind: "input";
|
|
18
|
+
input: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "paste";
|
|
21
|
+
text: string;
|
|
22
|
+
leadingInput?: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "consume";
|
|
25
|
+
};
|
|
26
|
+
export interface BracketedPasteDetector {
|
|
27
|
+
/**
|
|
28
|
+
* Feed one input chunk (as delivered by ink's useInput callback).
|
|
29
|
+
* - `input`: regular keystrokes, pass through to normal handling.
|
|
30
|
+
* - `paste`: completed bracketed paste; insert `text` without submitting.
|
|
31
|
+
* `leadingInput` (rare) is content that preceded the start marker in the
|
|
32
|
+
* same chunk and should be handled as regular input first.
|
|
33
|
+
* - `consume`: content of an in-flight paste (or an empty paste); do
|
|
34
|
+
* nothing with this chunk.
|
|
35
|
+
*/
|
|
36
|
+
process(chunk: string): PasteProcessResult;
|
|
37
|
+
reset(): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createBracketedPasteDetector(): BracketedPasteDetector;
|