@draht/coding-agent 2026.3.5 → 2026.3.11-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/CHANGELOG.md +55 -0
- package/README.md +6 -2
- package/agents/architect.md +1 -0
- package/agents/debugger.md +1 -0
- package/agents/git-committer.md +1 -0
- package/agents/implementer.md +1 -0
- package/agents/reviewer.md +1 -0
- package/agents/security-auditor.md +1 -0
- package/agents/verifier.md +1 -0
- package/dist/agents/architect.md +1 -0
- package/dist/agents/debugger.md +1 -0
- package/dist/agents/git-committer.md +1 -0
- package/dist/agents/implementer.md +1 -0
- package/dist/agents/reviewer.md +1 -0
- package/dist/agents/security-auditor.md +1 -0
- package/dist/agents/verifier.md +1 -0
- package/dist/cli/args.d.ts +6 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +24 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/attach-mode.d.ts +13 -0
- package/dist/cli/attach-mode.d.ts.map +1 -0
- package/dist/cli/attach-mode.js +97 -0
- package/dist/cli/attach-mode.js.map +1 -0
- package/dist/cli/list-sessions.d.ts +8 -0
- package/dist/cli/list-sessions.d.ts.map +1 -0
- package/dist/cli/list-sessions.js +52 -0
- package/dist/cli/list-sessions.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +50 -17
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +2 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +25 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +3 -0
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +16 -1
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/export-html/index.d.ts +5 -2
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +4 -3
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/template.js +11 -14
- package/dist/core/export-html/tool-renderer.d.ts +5 -2
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +12 -5
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -6
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +3 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +32 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +21 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +2 -2
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +8 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +4 -4
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +10 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +7 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +38 -4
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +3 -3
- package/dist/core/skills.js.map +1 -1
- package/dist/core/socket-server/discovery.d.ts +19 -0
- package/dist/core/socket-server/discovery.d.ts.map +1 -0
- package/dist/core/socket-server/discovery.js +91 -0
- package/dist/core/socket-server/discovery.js.map +1 -0
- package/dist/core/socket-server/index.d.ts +13 -0
- package/dist/core/socket-server/index.d.ts.map +1 -0
- package/dist/core/socket-server/index.js +11 -0
- package/dist/core/socket-server/index.js.map +1 -0
- package/dist/core/socket-server/session-integration.d.ts +17 -0
- package/dist/core/socket-server/session-integration.d.ts.map +1 -0
- package/dist/core/socket-server/session-integration.js +77 -0
- package/dist/core/socket-server/session-integration.js.map +1 -0
- package/dist/core/socket-server/socket-client.d.ts +65 -0
- package/dist/core/socket-server/socket-client.d.ts.map +1 -0
- package/dist/core/socket-server/socket-client.js +197 -0
- package/dist/core/socket-server/socket-client.js.map +1 -0
- package/dist/core/socket-server/socket-server.d.ts +60 -0
- package/dist/core/socket-server/socket-server.d.ts.map +1 -0
- package/dist/core/socket-server/socket-server.js +273 -0
- package/dist/core/socket-server/socket-server.js.map +1 -0
- package/dist/core/socket-server/types.d.ts +81 -0
- package/dist/core/socket-server/types.d.ts.map +1 -0
- package/dist/core/socket-server/types.js +8 -0
- package/dist/core/socket-server/types.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +76 -11
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +2 -2
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +3 -3
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +1 -0
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -23
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +14 -4
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +21 -2
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +115 -9
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +64 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/jsonl.d.ts +17 -0
- package/dist/modes/rpc/jsonl.d.ts.map +1 -0
- package/dist/modes/rpc/jsonl.js +49 -0
- package/dist/modes/rpc/jsonl.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -11
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +9 -11
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/prompts/commands/discuss-phase.md +10 -0
- package/dist/prompts/commands/execute-phase.md +51 -34
- package/dist/prompts/commands/fix.md +8 -6
- package/dist/prompts/commands/init-project.md +12 -0
- package/dist/prompts/commands/map-codebase.md +17 -18
- package/dist/prompts/commands/new-project.md +12 -0
- package/dist/prompts/commands/next-milestone.md +5 -3
- package/dist/prompts/commands/plan-phase.md +27 -5
- package/dist/prompts/commands/quick.md +12 -5
- package/dist/prompts/commands/review.md +10 -10
- package/dist/prompts/commands/verify-work.md +31 -17
- package/docs/compaction.md +2 -0
- package/docs/custom-provider.md +11 -7
- package/docs/extensions.md +55 -3
- package/docs/keybindings.md +9 -1
- package/docs/models.md +5 -1
- package/docs/rpc.md +40 -3
- package/docs/session.md +2 -2
- package/docs/settings.md +1 -0
- package/docs/terminal-setup.md +28 -3
- package/docs/tmux.md +61 -0
- package/docs/tree.md +9 -0
- package/examples/extensions/overlay-qa-tests.ts +468 -1
- package/examples/extensions/provider-payload.ts +14 -0
- package/examples/extensions/with-deps/index.ts +1 -5
- package/package.json +7 -5
- package/prompts/commands/discuss-phase.md +10 -0
- package/prompts/commands/execute-phase.md +51 -34
- package/prompts/commands/fix.md +8 -6
- package/prompts/commands/init-project.md +12 -0
- package/prompts/commands/map-codebase.md +17 -18
- package/prompts/commands/new-project.md +12 -0
- package/prompts/commands/next-milestone.md +5 -3
- package/prompts/commands/plan-phase.md +27 -5
- package/prompts/commands/quick.md +12 -5
- package/prompts/commands/review.md +10 -10
- package/prompts/commands/verify-work.md +31 -17
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocketClient - Client for attaching to a draht socket session
|
|
3
|
+
*
|
|
4
|
+
* Connects to a Unix domain socket, sends input, and receives output.
|
|
5
|
+
* Used when running `draht --attach <session-id>`.
|
|
6
|
+
*/
|
|
7
|
+
import { connect } from "node:net";
|
|
8
|
+
/**
|
|
9
|
+
* SocketClient connects to a socket-based draht session.
|
|
10
|
+
*
|
|
11
|
+
* Provides callbacks for output, metadata, and connection events.
|
|
12
|
+
*/
|
|
13
|
+
export class SocketClient {
|
|
14
|
+
#socketPath;
|
|
15
|
+
#clientId;
|
|
16
|
+
#mode;
|
|
17
|
+
#socket = null;
|
|
18
|
+
#buffer = "";
|
|
19
|
+
/** Callbacks */
|
|
20
|
+
#onOutput = null;
|
|
21
|
+
#onMetadata = null;
|
|
22
|
+
#onClientJoined = null;
|
|
23
|
+
#onClientLeft = null;
|
|
24
|
+
#onInputEcho = null;
|
|
25
|
+
#onError = null;
|
|
26
|
+
#onDisconnect = null;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.#socketPath = options.socketPath;
|
|
29
|
+
this.#clientId = options.clientId ?? `client-${crypto.randomUUID().slice(0, 8)}`;
|
|
30
|
+
this.#mode = options.mode ?? "read-write";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Connect to the socket server.
|
|
34
|
+
*/
|
|
35
|
+
async connect() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
this.#socket = connect(this.#socketPath);
|
|
38
|
+
this.#socket.on("connect", () => {
|
|
39
|
+
// Send attach message
|
|
40
|
+
const attach = {
|
|
41
|
+
type: "attach",
|
|
42
|
+
clientId: this.#clientId,
|
|
43
|
+
mode: this.#mode,
|
|
44
|
+
};
|
|
45
|
+
this.#send(attach);
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
this.#socket.on("data", (data) => {
|
|
49
|
+
this.#handleData(data);
|
|
50
|
+
});
|
|
51
|
+
this.#socket.on("close", () => {
|
|
52
|
+
if (this.#onDisconnect) {
|
|
53
|
+
this.#onDisconnect();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
this.#socket.on("error", (err) => {
|
|
57
|
+
reject(err);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Disconnect from the socket server.
|
|
63
|
+
*/
|
|
64
|
+
disconnect() {
|
|
65
|
+
if (this.#socket) {
|
|
66
|
+
const detach = {
|
|
67
|
+
type: "detach",
|
|
68
|
+
clientId: this.#clientId,
|
|
69
|
+
};
|
|
70
|
+
this.#send(detach);
|
|
71
|
+
this.#socket.end();
|
|
72
|
+
this.#socket = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Send input to the session.
|
|
77
|
+
*/
|
|
78
|
+
sendInput(data) {
|
|
79
|
+
if (!this.#socket) {
|
|
80
|
+
throw new Error("Not connected");
|
|
81
|
+
}
|
|
82
|
+
const input = {
|
|
83
|
+
type: "input",
|
|
84
|
+
data,
|
|
85
|
+
clientId: this.#clientId,
|
|
86
|
+
};
|
|
87
|
+
this.#send(input);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Set callback for output received from session.
|
|
91
|
+
*/
|
|
92
|
+
onOutput(callback) {
|
|
93
|
+
this.#onOutput = callback;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Set callback for session metadata (received on attach).
|
|
97
|
+
*/
|
|
98
|
+
onMetadata(callback) {
|
|
99
|
+
this.#onMetadata = callback;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Set callback for when another client joins.
|
|
103
|
+
*/
|
|
104
|
+
onClientJoined(callback) {
|
|
105
|
+
this.#onClientJoined = callback;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Set callback for when another client leaves.
|
|
109
|
+
*/
|
|
110
|
+
onClientLeft(callback) {
|
|
111
|
+
this.#onClientLeft = callback;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Set callback for input echo from other clients.
|
|
115
|
+
*/
|
|
116
|
+
onInputEcho(callback) {
|
|
117
|
+
this.#onInputEcho = callback;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Set callback for errors.
|
|
121
|
+
*/
|
|
122
|
+
onError(callback) {
|
|
123
|
+
this.#onError = callback;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Set callback for disconnect.
|
|
127
|
+
*/
|
|
128
|
+
onDisconnect(callback) {
|
|
129
|
+
this.#onDisconnect = callback;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Handle incoming data from server.
|
|
133
|
+
*/
|
|
134
|
+
#handleData(data) {
|
|
135
|
+
this.#buffer += data.toString();
|
|
136
|
+
const lines = this.#buffer.split("\n");
|
|
137
|
+
this.#buffer = lines.pop() || "";
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
if (!line.trim())
|
|
140
|
+
continue;
|
|
141
|
+
try {
|
|
142
|
+
const message = JSON.parse(line);
|
|
143
|
+
this.#handleServerMessage(message);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Ignore malformed messages
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Handle a server message.
|
|
152
|
+
*/
|
|
153
|
+
#handleServerMessage(message) {
|
|
154
|
+
switch (message.type) {
|
|
155
|
+
case "output":
|
|
156
|
+
if (this.#onOutput) {
|
|
157
|
+
this.#onOutput(message.data, message.stream);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case "input_echo":
|
|
161
|
+
if (this.#onInputEcho) {
|
|
162
|
+
this.#onInputEcho(message.data, message.clientId);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case "client_joined":
|
|
166
|
+
if (this.#onClientJoined) {
|
|
167
|
+
this.#onClientJoined(message.clientId, message.mode);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case "client_left":
|
|
171
|
+
if (this.#onClientLeft) {
|
|
172
|
+
this.#onClientLeft(message.clientId);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case "session_metadata":
|
|
176
|
+
if (this.#onMetadata) {
|
|
177
|
+
this.#onMetadata(message.sessionId, message.cwd, new Date(message.createdAt));
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
case "error":
|
|
181
|
+
if (this.#onError) {
|
|
182
|
+
this.#onError(message.message);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Send a message to the server.
|
|
189
|
+
*/
|
|
190
|
+
#send(message) {
|
|
191
|
+
if (!this.#socket)
|
|
192
|
+
return;
|
|
193
|
+
const json = `${JSON.stringify(message)}\n`;
|
|
194
|
+
this.#socket.write(json);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=socket-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-client.js","sourceRoot":"","sources":["../../../src/core/socket-server/socket-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SocketClient - Client for attaching to a draht socket session\n *\n * Connects to a Unix domain socket, sends input, and receives output.\n * Used when running `draht --attach <session-id>`.\n */\n\nimport { connect, type Socket } from \"node:net\";\nimport type { ClientMessage, ClientMode, ServerMessage } from \"./types.js\";\n\nexport interface SocketClientOptions {\n\t/** Path to the Unix socket */\n\tsocketPath: string;\n\t/** Client identifier (defaults to random ID) */\n\tclientId?: string;\n\t/** Connection mode */\n\tmode?: ClientMode;\n}\n\n/**\n * SocketClient connects to a socket-based draht session.\n *\n * Provides callbacks for output, metadata, and connection events.\n */\nexport class SocketClient {\n\treadonly #socketPath: string;\n\treadonly #clientId: string;\n\treadonly #mode: ClientMode;\n\n\t#socket: Socket | null = null;\n\t#buffer = \"\";\n\n\t/** Callbacks */\n\t#onOutput: ((data: string, stream: \"stdout\" | \"stderr\") => void) | null = null;\n\t#onMetadata: ((sessionId: string, cwd: string, createdAt: Date) => void) | null = null;\n\t#onClientJoined: ((clientId: string, mode: ClientMode) => void) | null = null;\n\t#onClientLeft: ((clientId: string) => void) | null = null;\n\t#onInputEcho: ((data: string, clientId: string) => void) | null = null;\n\t#onError: ((message: string) => void) | null = null;\n\t#onDisconnect: (() => void) | null = null;\n\n\tconstructor(options: SocketClientOptions) {\n\t\tthis.#socketPath = options.socketPath;\n\t\tthis.#clientId = options.clientId ?? `client-${crypto.randomUUID().slice(0, 8)}`;\n\t\tthis.#mode = options.mode ?? \"read-write\";\n\t}\n\n\t/**\n\t * Connect to the socket server.\n\t */\n\tasync connect(): Promise<void> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.#socket = connect(this.#socketPath);\n\n\t\t\tthis.#socket.on(\"connect\", () => {\n\t\t\t\t// Send attach message\n\t\t\t\tconst attach: ClientMessage = {\n\t\t\t\t\ttype: \"attach\",\n\t\t\t\t\tclientId: this.#clientId,\n\t\t\t\t\tmode: this.#mode,\n\t\t\t\t};\n\t\t\t\tthis.#send(attach);\n\t\t\t\tresolve();\n\t\t\t});\n\n\t\t\tthis.#socket.on(\"data\", (data) => {\n\t\t\t\tthis.#handleData(data);\n\t\t\t});\n\n\t\t\tthis.#socket.on(\"close\", () => {\n\t\t\t\tif (this.#onDisconnect) {\n\t\t\t\t\tthis.#onDisconnect();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tthis.#socket.on(\"error\", (err) => {\n\t\t\t\treject(err);\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Disconnect from the socket server.\n\t */\n\tdisconnect(): void {\n\t\tif (this.#socket) {\n\t\t\tconst detach: ClientMessage = {\n\t\t\t\ttype: \"detach\",\n\t\t\t\tclientId: this.#clientId,\n\t\t\t};\n\t\t\tthis.#send(detach);\n\t\t\tthis.#socket.end();\n\t\t\tthis.#socket = null;\n\t\t}\n\t}\n\n\t/**\n\t * Send input to the session.\n\t */\n\tsendInput(data: string): void {\n\t\tif (!this.#socket) {\n\t\t\tthrow new Error(\"Not connected\");\n\t\t}\n\n\t\tconst input: ClientMessage = {\n\t\t\ttype: \"input\",\n\t\t\tdata,\n\t\t\tclientId: this.#clientId,\n\t\t};\n\t\tthis.#send(input);\n\t}\n\n\t/**\n\t * Set callback for output received from session.\n\t */\n\tonOutput(callback: (data: string, stream: \"stdout\" | \"stderr\") => void): void {\n\t\tthis.#onOutput = callback;\n\t}\n\n\t/**\n\t * Set callback for session metadata (received on attach).\n\t */\n\tonMetadata(callback: (sessionId: string, cwd: string, createdAt: Date) => void): void {\n\t\tthis.#onMetadata = callback;\n\t}\n\n\t/**\n\t * Set callback for when another client joins.\n\t */\n\tonClientJoined(callback: (clientId: string, mode: ClientMode) => void): void {\n\t\tthis.#onClientJoined = callback;\n\t}\n\n\t/**\n\t * Set callback for when another client leaves.\n\t */\n\tonClientLeft(callback: (clientId: string) => void): void {\n\t\tthis.#onClientLeft = callback;\n\t}\n\n\t/**\n\t * Set callback for input echo from other clients.\n\t */\n\tonInputEcho(callback: (data: string, clientId: string) => void): void {\n\t\tthis.#onInputEcho = callback;\n\t}\n\n\t/**\n\t * Set callback for errors.\n\t */\n\tonError(callback: (message: string) => void): void {\n\t\tthis.#onError = callback;\n\t}\n\n\t/**\n\t * Set callback for disconnect.\n\t */\n\tonDisconnect(callback: () => void): void {\n\t\tthis.#onDisconnect = callback;\n\t}\n\n\t/**\n\t * Handle incoming data from server.\n\t */\n\t#handleData(data: Buffer): void {\n\t\tthis.#buffer += data.toString();\n\t\tconst lines = this.#buffer.split(\"\\n\");\n\t\tthis.#buffer = lines.pop() || \"\";\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\n\t\t\ttry {\n\t\t\t\tconst message = JSON.parse(line) as ServerMessage;\n\t\t\t\tthis.#handleServerMessage(message);\n\t\t\t} catch {\n\t\t\t\t// Ignore malformed messages\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Handle a server message.\n\t */\n\t#handleServerMessage(message: ServerMessage): void {\n\t\tswitch (message.type) {\n\t\t\tcase \"output\":\n\t\t\t\tif (this.#onOutput) {\n\t\t\t\t\tthis.#onOutput(message.data, message.stream);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"input_echo\":\n\t\t\t\tif (this.#onInputEcho) {\n\t\t\t\t\tthis.#onInputEcho(message.data, message.clientId);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"client_joined\":\n\t\t\t\tif (this.#onClientJoined) {\n\t\t\t\t\tthis.#onClientJoined(message.clientId, message.mode);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"client_left\":\n\t\t\t\tif (this.#onClientLeft) {\n\t\t\t\t\tthis.#onClientLeft(message.clientId);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"session_metadata\":\n\t\t\t\tif (this.#onMetadata) {\n\t\t\t\t\tthis.#onMetadata(message.sessionId, message.cwd, new Date(message.createdAt));\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\":\n\t\t\t\tif (this.#onError) {\n\t\t\t\t\tthis.#onError(message.message);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * Send a message to the server.\n\t */\n\t#send(message: ClientMessage): void {\n\t\tif (!this.#socket) return;\n\t\tconst json = `${JSON.stringify(message)}\\n`;\n\t\tthis.#socket.write(json);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocketServer - Unix domain socket server for attachable draht sessions
|
|
3
|
+
*
|
|
4
|
+
* Enables tmux-style multi-client attachment:
|
|
5
|
+
* - Multiple readers/writers can connect simultaneously
|
|
6
|
+
* - All clients see the same output
|
|
7
|
+
* - Input from any client is echoed to all others
|
|
8
|
+
* - Clients can join/leave without disrupting the session
|
|
9
|
+
*/
|
|
10
|
+
export interface SocketServerOptions {
|
|
11
|
+
/** Session ID (used for socket filename) */
|
|
12
|
+
sessionId: string;
|
|
13
|
+
/** Directory for socket files (default: ~/.draht/agent/sockets) */
|
|
14
|
+
socketDir: string;
|
|
15
|
+
/** Current working directory (included in metadata) */
|
|
16
|
+
cwd: string;
|
|
17
|
+
/** Maximum number of concurrent clients */
|
|
18
|
+
maxClients?: number;
|
|
19
|
+
/** Whether to echo input to all clients (tmux-style) */
|
|
20
|
+
broadcastInputEcho?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* SocketServer manages a Unix domain socket for a single draht session.
|
|
24
|
+
*
|
|
25
|
+
* Clients connect, send input, and receive output in real-time.
|
|
26
|
+
* All communication uses JSON-over-socket with newline framing.
|
|
27
|
+
*/
|
|
28
|
+
export declare class SocketServer {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(options: SocketServerOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Start the socket server.
|
|
33
|
+
* Creates socket directory, binds Unix socket, and starts listening.
|
|
34
|
+
*/
|
|
35
|
+
start(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Stop the socket server and clean up.
|
|
38
|
+
*/
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Broadcast output to all connected clients.
|
|
42
|
+
*
|
|
43
|
+
* @param data - Output text
|
|
44
|
+
* @param stream - Output stream (stdout or stderr)
|
|
45
|
+
*/
|
|
46
|
+
broadcastOutput(data: string, stream?: "stdout" | "stderr"): void;
|
|
47
|
+
/**
|
|
48
|
+
* Set callback for input received from clients.
|
|
49
|
+
*/
|
|
50
|
+
onInput(callback: (data: string, clientId: string) => void): void;
|
|
51
|
+
/**
|
|
52
|
+
* Get socket path for this session.
|
|
53
|
+
*/
|
|
54
|
+
get socketPath(): string;
|
|
55
|
+
/**
|
|
56
|
+
* Get number of connected clients.
|
|
57
|
+
*/
|
|
58
|
+
get clientCount(): number;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=socket-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../../../src/core/socket-server/socket-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,mBAAmB;IACnC,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;GAKG;AACH,qBAAa,YAAY;;IAexB,YAAY,OAAO,EAAE,mBAAmB,EAQvC;IAED;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAwB3B;IAED;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB1B;IAED;;;;;OAKG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,QAAQ,GAAG,QAAmB,GAAG,IAAI,CAO1E;IAED;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAEhE;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;CAmLD","sourcesContent":["/**\n * SocketServer - Unix domain socket server for attachable draht sessions\n *\n * Enables tmux-style multi-client attachment:\n * - Multiple readers/writers can connect simultaneously\n * - All clients see the same output\n * - Input from any client is echoed to all others\n * - Clients can join/leave without disrupting the session\n */\n\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rm, writeFile } from \"node:fs/promises\";\nimport { createServer, type Server, type Socket } from \"node:net\";\nimport path from \"node:path\";\nimport type { ClientMessage, ClientMode, ConnectedClient, ServerMessage } from \"./types.js\";\n\nexport interface SocketServerOptions {\n\t/** Session ID (used for socket filename) */\n\tsessionId: string;\n\t/** Directory for socket files (default: ~/.draht/agent/sockets) */\n\tsocketDir: string;\n\t/** Current working directory (included in metadata) */\n\tcwd: string;\n\t/** Maximum number of concurrent clients */\n\tmaxClients?: number;\n\t/** Whether to echo input to all clients (tmux-style) */\n\tbroadcastInputEcho?: boolean;\n}\n\n/**\n * SocketServer manages a Unix domain socket for a single draht session.\n *\n * Clients connect, send input, and receive output in real-time.\n * All communication uses JSON-over-socket with newline framing.\n */\nexport class SocketServer {\n\treadonly #sessionId: string;\n\treadonly #socketPath: string;\n\treadonly #lockPath: string;\n\treadonly #cwd: string;\n\treadonly #maxClients: number;\n\treadonly #broadcastInputEcho: boolean;\n\n\t#server: Server | null = null;\n\t#clients = new Map<string, ConnectedClient>();\n\t#createdAt = new Date();\n\n\t/** Callback for input received from any client */\n\t#onInput: ((data: string, clientId: string) => void) | null = null;\n\n\tconstructor(options: SocketServerOptions) {\n\t\tthis.#sessionId = options.sessionId;\n\t\tthis.#cwd = options.cwd;\n\t\tthis.#maxClients = options.maxClients ?? 10;\n\t\tthis.#broadcastInputEcho = options.broadcastInputEcho ?? true;\n\n\t\tthis.#socketPath = path.join(options.socketDir, `${options.sessionId}.sock`);\n\t\tthis.#lockPath = path.join(options.socketDir, `${options.sessionId}.lock`);\n\t}\n\n\t/**\n\t * Start the socket server.\n\t * Creates socket directory, binds Unix socket, and starts listening.\n\t */\n\tasync start(): Promise<void> {\n\t\t// Ensure socket directory exists\n\t\tconst socketDir = path.dirname(this.#socketPath);\n\t\tawait mkdir(socketDir, { recursive: true, mode: 0o700 });\n\n\t\t// Clean up stale socket if it exists\n\t\tif (existsSync(this.#socketPath)) {\n\t\t\tawait rm(this.#socketPath, { force: true });\n\t\t}\n\n\t\t// Write PID lock file for cleanup on crash\n\t\tawait writeFile(this.#lockPath, `${process.pid}\\n${this.#cwd}\\n${this.#createdAt.toISOString()}`);\n\n\t\t// Create Unix domain socket server\n\t\tthis.#server = createServer((socket) => this.#handleConnection(socket));\n\n\t\t// Bind to socket path\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.#server!.listen(this.#socketPath, () => resolve());\n\t\t\tthis.#server!.on(\"error\", reject);\n\t\t});\n\n\t\t// Set socket permissions (owner-only)\n\t\tawait import(\"node:fs/promises\").then((fs) => fs.chmod(this.#socketPath, 0o600));\n\t}\n\n\t/**\n\t * Stop the socket server and clean up.\n\t */\n\tasync stop(): Promise<void> {\n\t\t// Disconnect all clients\n\t\tfor (const client of this.#clients.values()) {\n\t\t\tclient.socket.end();\n\t\t}\n\t\tthis.#clients.clear();\n\n\t\t// Close server\n\t\tif (this.#server) {\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tthis.#server!.close(() => resolve());\n\t\t\t});\n\t\t\tthis.#server = null;\n\t\t}\n\n\t\t// Clean up socket and lock files\n\t\tawait rm(this.#socketPath, { force: true });\n\t\tawait rm(this.#lockPath, { force: true });\n\t}\n\n\t/**\n\t * Broadcast output to all connected clients.\n\t *\n\t * @param data - Output text\n\t * @param stream - Output stream (stdout or stderr)\n\t */\n\tbroadcastOutput(data: string, stream: \"stdout\" | \"stderr\" = \"stdout\"): void {\n\t\tconst message: ServerMessage = {\n\t\t\ttype: \"output\",\n\t\t\tdata,\n\t\t\tstream,\n\t\t};\n\t\tthis.#broadcast(message);\n\t}\n\n\t/**\n\t * Set callback for input received from clients.\n\t */\n\tonInput(callback: (data: string, clientId: string) => void): void {\n\t\tthis.#onInput = callback;\n\t}\n\n\t/**\n\t * Get socket path for this session.\n\t */\n\tget socketPath(): string {\n\t\treturn this.#socketPath;\n\t}\n\n\t/**\n\t * Get number of connected clients.\n\t */\n\tget clientCount(): number {\n\t\treturn this.#clients.size;\n\t}\n\n\t/**\n\t * Handle new client connection.\n\t */\n\t#handleConnection(socket: Socket): void {\n\t\tlet clientId: string | null = null;\n\t\tlet _mode: ClientMode = \"read-write\";\n\t\tlet buffer = \"\";\n\n\t\t// Handle incoming data (JSON messages)\n\t\tsocket.on(\"data\", (data) => {\n\t\t\tbuffer += data.toString();\n\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (!line.trim()) continue;\n\n\t\t\t\ttry {\n\t\t\t\t\tconst message = JSON.parse(line) as ClientMessage;\n\t\t\t\t\tthis.#handleClientMessage(message, socket, (id, m) => {\n\t\t\t\t\t\tclientId = id;\n\t\t\t\t\t\t_mode = m;\n\t\t\t\t\t});\n\t\t\t\t} catch (_err) {\n\t\t\t\t\tthis.#sendError(socket, \"Invalid JSON message\");\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t// Handle client disconnect\n\t\tsocket.on(\"close\", () => {\n\t\t\tif (clientId) {\n\t\t\t\tthis.#handleClientDisconnect(clientId);\n\t\t\t}\n\t\t});\n\n\t\tsocket.on(\"error\", () => {\n\t\t\tif (clientId) {\n\t\t\t\tthis.#handleClientDisconnect(clientId);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Handle a client message.\n\t */\n\t#handleClientMessage(\n\t\tmessage: ClientMessage,\n\t\tsocket: Socket,\n\t\tonAttach: (id: string, mode: ClientMode) => void,\n\t): void {\n\t\tswitch (message.type) {\n\t\t\tcase \"attach\": {\n\t\t\t\t// Check max clients\n\t\t\t\tif (this.#clients.size >= this.#maxClients) {\n\t\t\t\t\tthis.#sendError(socket, \"Maximum clients reached\");\n\t\t\t\t\tsocket.end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Check for duplicate client ID\n\t\t\t\tif (this.#clients.has(message.clientId)) {\n\t\t\t\t\tthis.#sendError(socket, \"Client ID already connected\");\n\t\t\t\t\tsocket.end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Register client\n\t\t\t\tconst client: ConnectedClient = {\n\t\t\t\t\tid: message.clientId,\n\t\t\t\t\tmode: message.mode,\n\t\t\t\t\tsocket,\n\t\t\t\t\tconnectedAt: new Date(),\n\t\t\t\t};\n\t\t\t\tthis.#clients.set(message.clientId, client);\n\t\t\t\tonAttach(message.clientId, message.mode);\n\n\t\t\t\t// Send session metadata\n\t\t\t\tconst metadata: ServerMessage = {\n\t\t\t\t\ttype: \"session_metadata\",\n\t\t\t\t\tsessionId: this.#sessionId,\n\t\t\t\t\tcwd: this.#cwd,\n\t\t\t\t\tcreatedAt: this.#createdAt.toISOString(),\n\t\t\t\t};\n\t\t\t\tthis.#send(socket, metadata);\n\n\t\t\t\t// Notify other clients\n\t\t\t\tconst joined: ServerMessage = {\n\t\t\t\t\ttype: \"client_joined\",\n\t\t\t\t\tclientId: message.clientId,\n\t\t\t\t\tmode: message.mode,\n\t\t\t\t};\n\t\t\t\tthis.#broadcast(joined, message.clientId);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"input\": {\n\t\t\t\t// Check if client is in read-write mode\n\t\t\t\tconst client = this.#clients.get(message.clientId);\n\t\t\t\tif (!client || client.mode === \"read-only\") {\n\t\t\t\t\tthis.#sendError(socket, \"Read-only clients cannot send input\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Forward input to session\n\t\t\t\tif (this.#onInput) {\n\t\t\t\t\tthis.#onInput(message.data, message.clientId);\n\t\t\t\t}\n\n\t\t\t\t// Echo input to all other clients (tmux-style)\n\t\t\t\tif (this.#broadcastInputEcho) {\n\t\t\t\t\tconst echo: ServerMessage = {\n\t\t\t\t\t\ttype: \"input_echo\",\n\t\t\t\t\t\tdata: message.data,\n\t\t\t\t\t\tclientId: message.clientId,\n\t\t\t\t\t};\n\t\t\t\t\tthis.#broadcast(echo, message.clientId);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"detach\": {\n\t\t\t\tthis.#handleClientDisconnect(message.clientId);\n\t\t\t\tsocket.end();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Handle client disconnect.\n\t */\n\t#handleClientDisconnect(clientId: string): void {\n\t\tconst client = this.#clients.get(clientId);\n\t\tif (!client) return;\n\n\t\tthis.#clients.delete(clientId);\n\n\t\t// Notify other clients\n\t\tconst left: ServerMessage = {\n\t\t\ttype: \"client_left\",\n\t\t\tclientId,\n\t\t};\n\t\tthis.#broadcast(left);\n\t}\n\n\t/**\n\t * Broadcast a message to all clients (or all except one).\n\t */\n\t#broadcast(message: ServerMessage, excludeClientId?: string): void {\n\t\tconst json = `${JSON.stringify(message)}\\n`;\n\t\tfor (const client of this.#clients.values()) {\n\t\t\tif (client.id !== excludeClientId) {\n\t\t\t\tclient.socket.write(json);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Send a message to a specific socket.\n\t */\n\t#send(socket: Socket, message: ServerMessage): void {\n\t\tconst json = `${JSON.stringify(message)}\\n`;\n\t\tsocket.write(json);\n\t}\n\n\t/**\n\t * Send an error message to a specific socket.\n\t */\n\t#sendError(socket: Socket, message: string, code?: string): void {\n\t\tconst error: ServerMessage = {\n\t\t\ttype: \"error\",\n\t\t\tmessage,\n\t\t\tcode,\n\t\t};\n\t\tthis.#send(socket, error);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocketServer - Unix domain socket server for attachable draht sessions
|
|
3
|
+
*
|
|
4
|
+
* Enables tmux-style multi-client attachment:
|
|
5
|
+
* - Multiple readers/writers can connect simultaneously
|
|
6
|
+
* - All clients see the same output
|
|
7
|
+
* - Input from any client is echoed to all others
|
|
8
|
+
* - Clients can join/leave without disrupting the session
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { createServer } from "node:net";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
/**
|
|
15
|
+
* SocketServer manages a Unix domain socket for a single draht session.
|
|
16
|
+
*
|
|
17
|
+
* Clients connect, send input, and receive output in real-time.
|
|
18
|
+
* All communication uses JSON-over-socket with newline framing.
|
|
19
|
+
*/
|
|
20
|
+
export class SocketServer {
|
|
21
|
+
#sessionId;
|
|
22
|
+
#socketPath;
|
|
23
|
+
#lockPath;
|
|
24
|
+
#cwd;
|
|
25
|
+
#maxClients;
|
|
26
|
+
#broadcastInputEcho;
|
|
27
|
+
#server = null;
|
|
28
|
+
#clients = new Map();
|
|
29
|
+
#createdAt = new Date();
|
|
30
|
+
/** Callback for input received from any client */
|
|
31
|
+
#onInput = null;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.#sessionId = options.sessionId;
|
|
34
|
+
this.#cwd = options.cwd;
|
|
35
|
+
this.#maxClients = options.maxClients ?? 10;
|
|
36
|
+
this.#broadcastInputEcho = options.broadcastInputEcho ?? true;
|
|
37
|
+
this.#socketPath = path.join(options.socketDir, `${options.sessionId}.sock`);
|
|
38
|
+
this.#lockPath = path.join(options.socketDir, `${options.sessionId}.lock`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Start the socket server.
|
|
42
|
+
* Creates socket directory, binds Unix socket, and starts listening.
|
|
43
|
+
*/
|
|
44
|
+
async start() {
|
|
45
|
+
// Ensure socket directory exists
|
|
46
|
+
const socketDir = path.dirname(this.#socketPath);
|
|
47
|
+
await mkdir(socketDir, { recursive: true, mode: 0o700 });
|
|
48
|
+
// Clean up stale socket if it exists
|
|
49
|
+
if (existsSync(this.#socketPath)) {
|
|
50
|
+
await rm(this.#socketPath, { force: true });
|
|
51
|
+
}
|
|
52
|
+
// Write PID lock file for cleanup on crash
|
|
53
|
+
await writeFile(this.#lockPath, `${process.pid}\n${this.#cwd}\n${this.#createdAt.toISOString()}`);
|
|
54
|
+
// Create Unix domain socket server
|
|
55
|
+
this.#server = createServer((socket) => this.#handleConnection(socket));
|
|
56
|
+
// Bind to socket path
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
this.#server.listen(this.#socketPath, () => resolve());
|
|
59
|
+
this.#server.on("error", reject);
|
|
60
|
+
});
|
|
61
|
+
// Set socket permissions (owner-only)
|
|
62
|
+
await import("node:fs/promises").then((fs) => fs.chmod(this.#socketPath, 0o600));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Stop the socket server and clean up.
|
|
66
|
+
*/
|
|
67
|
+
async stop() {
|
|
68
|
+
// Disconnect all clients
|
|
69
|
+
for (const client of this.#clients.values()) {
|
|
70
|
+
client.socket.end();
|
|
71
|
+
}
|
|
72
|
+
this.#clients.clear();
|
|
73
|
+
// Close server
|
|
74
|
+
if (this.#server) {
|
|
75
|
+
await new Promise((resolve) => {
|
|
76
|
+
this.#server.close(() => resolve());
|
|
77
|
+
});
|
|
78
|
+
this.#server = null;
|
|
79
|
+
}
|
|
80
|
+
// Clean up socket and lock files
|
|
81
|
+
await rm(this.#socketPath, { force: true });
|
|
82
|
+
await rm(this.#lockPath, { force: true });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Broadcast output to all connected clients.
|
|
86
|
+
*
|
|
87
|
+
* @param data - Output text
|
|
88
|
+
* @param stream - Output stream (stdout or stderr)
|
|
89
|
+
*/
|
|
90
|
+
broadcastOutput(data, stream = "stdout") {
|
|
91
|
+
const message = {
|
|
92
|
+
type: "output",
|
|
93
|
+
data,
|
|
94
|
+
stream,
|
|
95
|
+
};
|
|
96
|
+
this.#broadcast(message);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Set callback for input received from clients.
|
|
100
|
+
*/
|
|
101
|
+
onInput(callback) {
|
|
102
|
+
this.#onInput = callback;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get socket path for this session.
|
|
106
|
+
*/
|
|
107
|
+
get socketPath() {
|
|
108
|
+
return this.#socketPath;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get number of connected clients.
|
|
112
|
+
*/
|
|
113
|
+
get clientCount() {
|
|
114
|
+
return this.#clients.size;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Handle new client connection.
|
|
118
|
+
*/
|
|
119
|
+
#handleConnection(socket) {
|
|
120
|
+
let clientId = null;
|
|
121
|
+
let _mode = "read-write";
|
|
122
|
+
let buffer = "";
|
|
123
|
+
// Handle incoming data (JSON messages)
|
|
124
|
+
socket.on("data", (data) => {
|
|
125
|
+
buffer += data.toString();
|
|
126
|
+
const lines = buffer.split("\n");
|
|
127
|
+
buffer = lines.pop() || "";
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
if (!line.trim())
|
|
130
|
+
continue;
|
|
131
|
+
try {
|
|
132
|
+
const message = JSON.parse(line);
|
|
133
|
+
this.#handleClientMessage(message, socket, (id, m) => {
|
|
134
|
+
clientId = id;
|
|
135
|
+
_mode = m;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (_err) {
|
|
139
|
+
this.#sendError(socket, "Invalid JSON message");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Handle client disconnect
|
|
144
|
+
socket.on("close", () => {
|
|
145
|
+
if (clientId) {
|
|
146
|
+
this.#handleClientDisconnect(clientId);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
socket.on("error", () => {
|
|
150
|
+
if (clientId) {
|
|
151
|
+
this.#handleClientDisconnect(clientId);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Handle a client message.
|
|
157
|
+
*/
|
|
158
|
+
#handleClientMessage(message, socket, onAttach) {
|
|
159
|
+
switch (message.type) {
|
|
160
|
+
case "attach": {
|
|
161
|
+
// Check max clients
|
|
162
|
+
if (this.#clients.size >= this.#maxClients) {
|
|
163
|
+
this.#sendError(socket, "Maximum clients reached");
|
|
164
|
+
socket.end();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Check for duplicate client ID
|
|
168
|
+
if (this.#clients.has(message.clientId)) {
|
|
169
|
+
this.#sendError(socket, "Client ID already connected");
|
|
170
|
+
socket.end();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Register client
|
|
174
|
+
const client = {
|
|
175
|
+
id: message.clientId,
|
|
176
|
+
mode: message.mode,
|
|
177
|
+
socket,
|
|
178
|
+
connectedAt: new Date(),
|
|
179
|
+
};
|
|
180
|
+
this.#clients.set(message.clientId, client);
|
|
181
|
+
onAttach(message.clientId, message.mode);
|
|
182
|
+
// Send session metadata
|
|
183
|
+
const metadata = {
|
|
184
|
+
type: "session_metadata",
|
|
185
|
+
sessionId: this.#sessionId,
|
|
186
|
+
cwd: this.#cwd,
|
|
187
|
+
createdAt: this.#createdAt.toISOString(),
|
|
188
|
+
};
|
|
189
|
+
this.#send(socket, metadata);
|
|
190
|
+
// Notify other clients
|
|
191
|
+
const joined = {
|
|
192
|
+
type: "client_joined",
|
|
193
|
+
clientId: message.clientId,
|
|
194
|
+
mode: message.mode,
|
|
195
|
+
};
|
|
196
|
+
this.#broadcast(joined, message.clientId);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case "input": {
|
|
200
|
+
// Check if client is in read-write mode
|
|
201
|
+
const client = this.#clients.get(message.clientId);
|
|
202
|
+
if (!client || client.mode === "read-only") {
|
|
203
|
+
this.#sendError(socket, "Read-only clients cannot send input");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Forward input to session
|
|
207
|
+
if (this.#onInput) {
|
|
208
|
+
this.#onInput(message.data, message.clientId);
|
|
209
|
+
}
|
|
210
|
+
// Echo input to all other clients (tmux-style)
|
|
211
|
+
if (this.#broadcastInputEcho) {
|
|
212
|
+
const echo = {
|
|
213
|
+
type: "input_echo",
|
|
214
|
+
data: message.data,
|
|
215
|
+
clientId: message.clientId,
|
|
216
|
+
};
|
|
217
|
+
this.#broadcast(echo, message.clientId);
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "detach": {
|
|
222
|
+
this.#handleClientDisconnect(message.clientId);
|
|
223
|
+
socket.end();
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Handle client disconnect.
|
|
230
|
+
*/
|
|
231
|
+
#handleClientDisconnect(clientId) {
|
|
232
|
+
const client = this.#clients.get(clientId);
|
|
233
|
+
if (!client)
|
|
234
|
+
return;
|
|
235
|
+
this.#clients.delete(clientId);
|
|
236
|
+
// Notify other clients
|
|
237
|
+
const left = {
|
|
238
|
+
type: "client_left",
|
|
239
|
+
clientId,
|
|
240
|
+
};
|
|
241
|
+
this.#broadcast(left);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Broadcast a message to all clients (or all except one).
|
|
245
|
+
*/
|
|
246
|
+
#broadcast(message, excludeClientId) {
|
|
247
|
+
const json = `${JSON.stringify(message)}\n`;
|
|
248
|
+
for (const client of this.#clients.values()) {
|
|
249
|
+
if (client.id !== excludeClientId) {
|
|
250
|
+
client.socket.write(json);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Send a message to a specific socket.
|
|
256
|
+
*/
|
|
257
|
+
#send(socket, message) {
|
|
258
|
+
const json = `${JSON.stringify(message)}\n`;
|
|
259
|
+
socket.write(json);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Send an error message to a specific socket.
|
|
263
|
+
*/
|
|
264
|
+
#sendError(socket, message, code) {
|
|
265
|
+
const error = {
|
|
266
|
+
type: "error",
|
|
267
|
+
message,
|
|
268
|
+
code,
|
|
269
|
+
};
|
|
270
|
+
this.#send(socket, error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=socket-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-server.js","sourceRoot":"","sources":["../../../src/core/socket-server/socket-server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SocketServer - Unix domain socket server for attachable draht sessions\n *\n * Enables tmux-style multi-client attachment:\n * - Multiple readers/writers can connect simultaneously\n * - All clients see the same output\n * - Input from any client is echoed to all others\n * - Clients can join/leave without disrupting the session\n */\n\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rm, writeFile } from \"node:fs/promises\";\nimport { createServer, type Server, type Socket } from \"node:net\";\nimport path from \"node:path\";\nimport type { ClientMessage, ClientMode, ConnectedClient, ServerMessage } from \"./types.js\";\n\nexport interface SocketServerOptions {\n\t/** Session ID (used for socket filename) */\n\tsessionId: string;\n\t/** Directory for socket files (default: ~/.draht/agent/sockets) */\n\tsocketDir: string;\n\t/** Current working directory (included in metadata) */\n\tcwd: string;\n\t/** Maximum number of concurrent clients */\n\tmaxClients?: number;\n\t/** Whether to echo input to all clients (tmux-style) */\n\tbroadcastInputEcho?: boolean;\n}\n\n/**\n * SocketServer manages a Unix domain socket for a single draht session.\n *\n * Clients connect, send input, and receive output in real-time.\n * All communication uses JSON-over-socket with newline framing.\n */\nexport class SocketServer {\n\treadonly #sessionId: string;\n\treadonly #socketPath: string;\n\treadonly #lockPath: string;\n\treadonly #cwd: string;\n\treadonly #maxClients: number;\n\treadonly #broadcastInputEcho: boolean;\n\n\t#server: Server | null = null;\n\t#clients = new Map<string, ConnectedClient>();\n\t#createdAt = new Date();\n\n\t/** Callback for input received from any client */\n\t#onInput: ((data: string, clientId: string) => void) | null = null;\n\n\tconstructor(options: SocketServerOptions) {\n\t\tthis.#sessionId = options.sessionId;\n\t\tthis.#cwd = options.cwd;\n\t\tthis.#maxClients = options.maxClients ?? 10;\n\t\tthis.#broadcastInputEcho = options.broadcastInputEcho ?? true;\n\n\t\tthis.#socketPath = path.join(options.socketDir, `${options.sessionId}.sock`);\n\t\tthis.#lockPath = path.join(options.socketDir, `${options.sessionId}.lock`);\n\t}\n\n\t/**\n\t * Start the socket server.\n\t * Creates socket directory, binds Unix socket, and starts listening.\n\t */\n\tasync start(): Promise<void> {\n\t\t// Ensure socket directory exists\n\t\tconst socketDir = path.dirname(this.#socketPath);\n\t\tawait mkdir(socketDir, { recursive: true, mode: 0o700 });\n\n\t\t// Clean up stale socket if it exists\n\t\tif (existsSync(this.#socketPath)) {\n\t\t\tawait rm(this.#socketPath, { force: true });\n\t\t}\n\n\t\t// Write PID lock file for cleanup on crash\n\t\tawait writeFile(this.#lockPath, `${process.pid}\\n${this.#cwd}\\n${this.#createdAt.toISOString()}`);\n\n\t\t// Create Unix domain socket server\n\t\tthis.#server = createServer((socket) => this.#handleConnection(socket));\n\n\t\t// Bind to socket path\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.#server!.listen(this.#socketPath, () => resolve());\n\t\t\tthis.#server!.on(\"error\", reject);\n\t\t});\n\n\t\t// Set socket permissions (owner-only)\n\t\tawait import(\"node:fs/promises\").then((fs) => fs.chmod(this.#socketPath, 0o600));\n\t}\n\n\t/**\n\t * Stop the socket server and clean up.\n\t */\n\tasync stop(): Promise<void> {\n\t\t// Disconnect all clients\n\t\tfor (const client of this.#clients.values()) {\n\t\t\tclient.socket.end();\n\t\t}\n\t\tthis.#clients.clear();\n\n\t\t// Close server\n\t\tif (this.#server) {\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tthis.#server!.close(() => resolve());\n\t\t\t});\n\t\t\tthis.#server = null;\n\t\t}\n\n\t\t// Clean up socket and lock files\n\t\tawait rm(this.#socketPath, { force: true });\n\t\tawait rm(this.#lockPath, { force: true });\n\t}\n\n\t/**\n\t * Broadcast output to all connected clients.\n\t *\n\t * @param data - Output text\n\t * @param stream - Output stream (stdout or stderr)\n\t */\n\tbroadcastOutput(data: string, stream: \"stdout\" | \"stderr\" = \"stdout\"): void {\n\t\tconst message: ServerMessage = {\n\t\t\ttype: \"output\",\n\t\t\tdata,\n\t\t\tstream,\n\t\t};\n\t\tthis.#broadcast(message);\n\t}\n\n\t/**\n\t * Set callback for input received from clients.\n\t */\n\tonInput(callback: (data: string, clientId: string) => void): void {\n\t\tthis.#onInput = callback;\n\t}\n\n\t/**\n\t * Get socket path for this session.\n\t */\n\tget socketPath(): string {\n\t\treturn this.#socketPath;\n\t}\n\n\t/**\n\t * Get number of connected clients.\n\t */\n\tget clientCount(): number {\n\t\treturn this.#clients.size;\n\t}\n\n\t/**\n\t * Handle new client connection.\n\t */\n\t#handleConnection(socket: Socket): void {\n\t\tlet clientId: string | null = null;\n\t\tlet _mode: ClientMode = \"read-write\";\n\t\tlet buffer = \"\";\n\n\t\t// Handle incoming data (JSON messages)\n\t\tsocket.on(\"data\", (data) => {\n\t\t\tbuffer += data.toString();\n\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (!line.trim()) continue;\n\n\t\t\t\ttry {\n\t\t\t\t\tconst message = JSON.parse(line) as ClientMessage;\n\t\t\t\t\tthis.#handleClientMessage(message, socket, (id, m) => {\n\t\t\t\t\t\tclientId = id;\n\t\t\t\t\t\t_mode = m;\n\t\t\t\t\t});\n\t\t\t\t} catch (_err) {\n\t\t\t\t\tthis.#sendError(socket, \"Invalid JSON message\");\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t// Handle client disconnect\n\t\tsocket.on(\"close\", () => {\n\t\t\tif (clientId) {\n\t\t\t\tthis.#handleClientDisconnect(clientId);\n\t\t\t}\n\t\t});\n\n\t\tsocket.on(\"error\", () => {\n\t\t\tif (clientId) {\n\t\t\t\tthis.#handleClientDisconnect(clientId);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Handle a client message.\n\t */\n\t#handleClientMessage(\n\t\tmessage: ClientMessage,\n\t\tsocket: Socket,\n\t\tonAttach: (id: string, mode: ClientMode) => void,\n\t): void {\n\t\tswitch (message.type) {\n\t\t\tcase \"attach\": {\n\t\t\t\t// Check max clients\n\t\t\t\tif (this.#clients.size >= this.#maxClients) {\n\t\t\t\t\tthis.#sendError(socket, \"Maximum clients reached\");\n\t\t\t\t\tsocket.end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Check for duplicate client ID\n\t\t\t\tif (this.#clients.has(message.clientId)) {\n\t\t\t\t\tthis.#sendError(socket, \"Client ID already connected\");\n\t\t\t\t\tsocket.end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Register client\n\t\t\t\tconst client: ConnectedClient = {\n\t\t\t\t\tid: message.clientId,\n\t\t\t\t\tmode: message.mode,\n\t\t\t\t\tsocket,\n\t\t\t\t\tconnectedAt: new Date(),\n\t\t\t\t};\n\t\t\t\tthis.#clients.set(message.clientId, client);\n\t\t\t\tonAttach(message.clientId, message.mode);\n\n\t\t\t\t// Send session metadata\n\t\t\t\tconst metadata: ServerMessage = {\n\t\t\t\t\ttype: \"session_metadata\",\n\t\t\t\t\tsessionId: this.#sessionId,\n\t\t\t\t\tcwd: this.#cwd,\n\t\t\t\t\tcreatedAt: this.#createdAt.toISOString(),\n\t\t\t\t};\n\t\t\t\tthis.#send(socket, metadata);\n\n\t\t\t\t// Notify other clients\n\t\t\t\tconst joined: ServerMessage = {\n\t\t\t\t\ttype: \"client_joined\",\n\t\t\t\t\tclientId: message.clientId,\n\t\t\t\t\tmode: message.mode,\n\t\t\t\t};\n\t\t\t\tthis.#broadcast(joined, message.clientId);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"input\": {\n\t\t\t\t// Check if client is in read-write mode\n\t\t\t\tconst client = this.#clients.get(message.clientId);\n\t\t\t\tif (!client || client.mode === \"read-only\") {\n\t\t\t\t\tthis.#sendError(socket, \"Read-only clients cannot send input\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Forward input to session\n\t\t\t\tif (this.#onInput) {\n\t\t\t\t\tthis.#onInput(message.data, message.clientId);\n\t\t\t\t}\n\n\t\t\t\t// Echo input to all other clients (tmux-style)\n\t\t\t\tif (this.#broadcastInputEcho) {\n\t\t\t\t\tconst echo: ServerMessage = {\n\t\t\t\t\t\ttype: \"input_echo\",\n\t\t\t\t\t\tdata: message.data,\n\t\t\t\t\t\tclientId: message.clientId,\n\t\t\t\t\t};\n\t\t\t\t\tthis.#broadcast(echo, message.clientId);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"detach\": {\n\t\t\t\tthis.#handleClientDisconnect(message.clientId);\n\t\t\t\tsocket.end();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Handle client disconnect.\n\t */\n\t#handleClientDisconnect(clientId: string): void {\n\t\tconst client = this.#clients.get(clientId);\n\t\tif (!client) return;\n\n\t\tthis.#clients.delete(clientId);\n\n\t\t// Notify other clients\n\t\tconst left: ServerMessage = {\n\t\t\ttype: \"client_left\",\n\t\t\tclientId,\n\t\t};\n\t\tthis.#broadcast(left);\n\t}\n\n\t/**\n\t * Broadcast a message to all clients (or all except one).\n\t */\n\t#broadcast(message: ServerMessage, excludeClientId?: string): void {\n\t\tconst json = `${JSON.stringify(message)}\\n`;\n\t\tfor (const client of this.#clients.values()) {\n\t\t\tif (client.id !== excludeClientId) {\n\t\t\t\tclient.socket.write(json);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Send a message to a specific socket.\n\t */\n\t#send(socket: Socket, message: ServerMessage): void {\n\t\tconst json = `${JSON.stringify(message)}\\n`;\n\t\tsocket.write(json);\n\t}\n\n\t/**\n\t * Send an error message to a specific socket.\n\t */\n\t#sendError(socket: Socket, message: string, code?: string): void {\n\t\tconst error: ServerMessage = {\n\t\t\ttype: \"error\",\n\t\t\tmessage,\n\t\t\tcode,\n\t\t};\n\t\tthis.#send(socket, error);\n\t}\n}\n"]}
|