@hengyuliu/agenthub-daemon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1112 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/daemon-core.ts
|
|
4
|
+
import { hostname, platform } from "os";
|
|
5
|
+
import { homedir as homedir3 } from "os";
|
|
6
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
7
|
+
import { join as joinPath3 } from "path";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
|
|
10
|
+
// src/ws-client.ts
|
|
11
|
+
import WebSocket from "ws";
|
|
12
|
+
|
|
13
|
+
// ../shared/src/index.ts
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var RuntimeKind = z.enum(["claude", "codex", "cursor", "gemini", "manual"]);
|
|
16
|
+
var AgentStatus = z.enum(["hot", "cold", "offline"]);
|
|
17
|
+
var TeamKind = z.enum(["personal", "workspace"]);
|
|
18
|
+
var TeamMemberRole = z.enum(["owner", "admin", "member", "viewer"]);
|
|
19
|
+
var SubjectKind = z.enum(["human", "agent", "session"]);
|
|
20
|
+
var Team = z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
slug: z.string(),
|
|
23
|
+
name: z.string(),
|
|
24
|
+
kind: TeamKind,
|
|
25
|
+
createdBy: z.string(),
|
|
26
|
+
createdAt: z.string()
|
|
27
|
+
});
|
|
28
|
+
var TeamMember = z.object({
|
|
29
|
+
teamId: z.string(),
|
|
30
|
+
subjectKind: SubjectKind,
|
|
31
|
+
subjectId: z.string(),
|
|
32
|
+
role: TeamMemberRole,
|
|
33
|
+
joinedAt: z.string()
|
|
34
|
+
});
|
|
35
|
+
var ClaudeSessionInfo = z.object({
|
|
36
|
+
sessionId: z.string(),
|
|
37
|
+
// the .jsonl uuid
|
|
38
|
+
projectDir: z.string(),
|
|
39
|
+
// decoded best-effort; raw if not decodable
|
|
40
|
+
rawProjectDirName: z.string(),
|
|
41
|
+
// exact dirname under ~/.claude/projects/
|
|
42
|
+
lastModified: z.string(),
|
|
43
|
+
messageCount: z.number().int().nonnegative(),
|
|
44
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
45
|
+
customTitle: z.string().nullable().default(null),
|
|
46
|
+
firstSnippet: z.string().nullable().default(null)
|
|
47
|
+
});
|
|
48
|
+
var Computer = z.object({
|
|
49
|
+
id: z.string(),
|
|
50
|
+
// server-generated UUID (V0.2.1)
|
|
51
|
+
ownerUserId: z.string(),
|
|
52
|
+
// who registered this computer
|
|
53
|
+
hostname: z.string(),
|
|
54
|
+
customName: z.string().nullable().default(null),
|
|
55
|
+
platform: z.string(),
|
|
56
|
+
daemonVersion: z.string(),
|
|
57
|
+
detectedRuntimes: z.array(RuntimeKind),
|
|
58
|
+
online: z.boolean(),
|
|
59
|
+
lastSeen: z.string(),
|
|
60
|
+
/** Plaintext token — only returned to the owner; hidden for non-owners. */
|
|
61
|
+
connectTokenPlaintext: z.string().nullable().default(null),
|
|
62
|
+
/** Live inventory from the daemon — NOT persisted; in-memory only. */
|
|
63
|
+
liveSessionInventory: z.array(ClaudeSessionInfo).default([])
|
|
64
|
+
});
|
|
65
|
+
var Session = z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
computerId: z.string(),
|
|
68
|
+
sessionUuid: z.string(),
|
|
69
|
+
// the .jsonl filename uuid
|
|
70
|
+
projectDir: z.string(),
|
|
71
|
+
displayName: z.string(),
|
|
72
|
+
// user-given on import
|
|
73
|
+
customTitle: z.string().nullable().default(null),
|
|
74
|
+
firstSnippet: z.string().nullable().default(null),
|
|
75
|
+
messageCount: z.number().int().nonnegative(),
|
|
76
|
+
lastModified: z.string().nullable().default(null),
|
|
77
|
+
importedBy: z.string(),
|
|
78
|
+
importedAt: z.string(),
|
|
79
|
+
parentSessionId: z.string().nullable().default(null),
|
|
80
|
+
forkPointMessageUuid: z.string().nullable().default(null),
|
|
81
|
+
forkedAt: z.string().nullable().default(null),
|
|
82
|
+
createdAt: z.string()
|
|
83
|
+
});
|
|
84
|
+
var Agent = z.object({
|
|
85
|
+
id: z.string(),
|
|
86
|
+
teamId: z.string(),
|
|
87
|
+
name: z.string(),
|
|
88
|
+
description: z.string().nullable().default(null),
|
|
89
|
+
rolePrompt: z.string().nullable().default(null),
|
|
90
|
+
runtime: RuntimeKind,
|
|
91
|
+
model: z.string().nullable().default(null),
|
|
92
|
+
homeComputerId: z.string(),
|
|
93
|
+
envVars: z.record(z.string()).default({}),
|
|
94
|
+
agentSessionUuid: z.string().nullable().default(null),
|
|
95
|
+
status: AgentStatus,
|
|
96
|
+
lastActive: z.string().nullable().default(null),
|
|
97
|
+
createdBy: z.string().nullable().default(null),
|
|
98
|
+
createdAt: z.string(),
|
|
99
|
+
/** Joined from agent_sessions. Always ≥ 1 (enforced by DB trigger). */
|
|
100
|
+
sourceSessionIds: z.array(z.string())
|
|
101
|
+
});
|
|
102
|
+
var ChannelKind = z.enum(["channel", "dm"]);
|
|
103
|
+
var ChannelMemberKind = z.enum(["human", "agent", "session"]);
|
|
104
|
+
var ChannelMember = z.object({
|
|
105
|
+
memberId: z.string(),
|
|
106
|
+
memberKind: ChannelMemberKind
|
|
107
|
+
});
|
|
108
|
+
var Channel = z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
// slug or "dm-<id>"
|
|
111
|
+
teamId: z.string(),
|
|
112
|
+
name: z.string(),
|
|
113
|
+
kind: ChannelKind,
|
|
114
|
+
purpose: z.string().nullable().default(null),
|
|
115
|
+
// V0.2 workspace purpose
|
|
116
|
+
members: z.array(ChannelMember).default([]),
|
|
117
|
+
/** Compatibility: agent ids extracted from members where kind='agent'. */
|
|
118
|
+
memberAgentIds: z.array(z.string()).default([]),
|
|
119
|
+
memberHumanIds: z.array(z.string()).default([]),
|
|
120
|
+
memberSessionIds: z.array(z.string()).default([]),
|
|
121
|
+
createdAt: z.string()
|
|
122
|
+
});
|
|
123
|
+
var MessageRole = z.enum(["human", "agent", "system"]);
|
|
124
|
+
var MessageSource = z.enum([
|
|
125
|
+
"web",
|
|
126
|
+
"telegram",
|
|
127
|
+
"inter-agent",
|
|
128
|
+
"peer-query",
|
|
129
|
+
"peer-response",
|
|
130
|
+
"system",
|
|
131
|
+
"imported-history"
|
|
132
|
+
]);
|
|
133
|
+
var Message = z.object({
|
|
134
|
+
id: z.string(),
|
|
135
|
+
channelId: z.string(),
|
|
136
|
+
teamId: z.string(),
|
|
137
|
+
authorId: z.string(),
|
|
138
|
+
role: MessageRole,
|
|
139
|
+
source: MessageSource,
|
|
140
|
+
content: z.string(),
|
|
141
|
+
mentions: z.array(z.string()).default([]),
|
|
142
|
+
threadParentId: z.string().nullable().default(null),
|
|
143
|
+
createdAt: z.string()
|
|
144
|
+
});
|
|
145
|
+
var DaemonHelloFrame = z.object({
|
|
146
|
+
type: z.literal("daemon.hello"),
|
|
147
|
+
payload: z.object({
|
|
148
|
+
daemonId: z.string(),
|
|
149
|
+
hostname: z.string(),
|
|
150
|
+
platform: z.string(),
|
|
151
|
+
daemonVersion: z.string(),
|
|
152
|
+
detectedRuntimes: z.array(RuntimeKind)
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
var AgentStatusFrame = z.object({
|
|
156
|
+
type: z.literal("agent.status"),
|
|
157
|
+
payload: z.object({
|
|
158
|
+
agentId: z.string(),
|
|
159
|
+
status: AgentStatus
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
var AgentMessageFrame = z.object({
|
|
163
|
+
type: z.literal("agent.message"),
|
|
164
|
+
payload: z.object({
|
|
165
|
+
agentId: z.string(),
|
|
166
|
+
channelId: z.string(),
|
|
167
|
+
content: z.string(),
|
|
168
|
+
mentions: z.array(z.string()).default([]),
|
|
169
|
+
threadParentId: z.string().nullable().default(null)
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
var AgentSessionUpdateFrame = z.object({
|
|
173
|
+
type: z.literal("agent.session_update"),
|
|
174
|
+
payload: z.object({
|
|
175
|
+
agentId: z.string(),
|
|
176
|
+
resumeSessionId: z.string()
|
|
177
|
+
})
|
|
178
|
+
});
|
|
179
|
+
var DaemonSessionsAdvertisedFrame = z.object({
|
|
180
|
+
type: z.literal("daemon.sessions_advertised"),
|
|
181
|
+
payload: z.object({
|
|
182
|
+
daemonId: z.string(),
|
|
183
|
+
sessions: z.array(ClaudeSessionInfo)
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
var AgentReadHistoryRequestFrame = z.object({
|
|
187
|
+
type: z.literal("agent.read_history.request"),
|
|
188
|
+
id: z.string(),
|
|
189
|
+
payload: z.object({
|
|
190
|
+
agentId: z.string(),
|
|
191
|
+
channelId: z.string(),
|
|
192
|
+
limit: z.number().int().positive().max(200).default(50),
|
|
193
|
+
before: z.string().nullable().default(null)
|
|
194
|
+
})
|
|
195
|
+
});
|
|
196
|
+
var BackendHelloAckFrame = z.object({
|
|
197
|
+
type: z.literal("backend.hello_ack"),
|
|
198
|
+
payload: z.object({
|
|
199
|
+
accepted: z.boolean(),
|
|
200
|
+
reason: z.string().optional()
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
var RouteToAgentFrame = z.object({
|
|
204
|
+
type: z.literal("backend.route_to_agent"),
|
|
205
|
+
payload: z.object({
|
|
206
|
+
agentId: z.string(),
|
|
207
|
+
channelId: z.string(),
|
|
208
|
+
fromId: z.string(),
|
|
209
|
+
fromRole: MessageRole,
|
|
210
|
+
content: z.string(),
|
|
211
|
+
mentions: z.array(z.string()).default([])
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
var HistoryResponseFrame = z.object({
|
|
215
|
+
type: z.literal("backend.history.response"),
|
|
216
|
+
id: z.string(),
|
|
217
|
+
payload: z.object({
|
|
218
|
+
messages: z.array(Message)
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
var AgentSpawnFrame = z.object({
|
|
222
|
+
type: z.literal("backend.agent.spawn"),
|
|
223
|
+
payload: z.object({
|
|
224
|
+
agentId: z.string(),
|
|
225
|
+
runtime: RuntimeKind,
|
|
226
|
+
workspacePath: z.string().nullable(),
|
|
227
|
+
resumeSessionId: z.string().nullable(),
|
|
228
|
+
historyTargetChannelId: z.string().nullable().default(null)
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
var ImportedMessage = z.object({
|
|
232
|
+
role: z.enum(["human", "agent"]),
|
|
233
|
+
content: z.string(),
|
|
234
|
+
timestamp: z.string()
|
|
235
|
+
});
|
|
236
|
+
var DaemonSessionHistoryFrame = z.object({
|
|
237
|
+
type: z.literal("daemon.session_history"),
|
|
238
|
+
payload: z.object({
|
|
239
|
+
daemonId: z.string(),
|
|
240
|
+
agentId: z.string(),
|
|
241
|
+
channelId: z.string(),
|
|
242
|
+
messages: z.array(ImportedMessage)
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
var AgentStopFrame = z.object({
|
|
246
|
+
type: z.literal("backend.agent.stop"),
|
|
247
|
+
payload: z.object({
|
|
248
|
+
agentId: z.string()
|
|
249
|
+
})
|
|
250
|
+
});
|
|
251
|
+
var BusFrame = z.discriminatedUnion("type", [
|
|
252
|
+
DaemonHelloFrame,
|
|
253
|
+
DaemonSessionsAdvertisedFrame,
|
|
254
|
+
DaemonSessionHistoryFrame,
|
|
255
|
+
AgentSessionUpdateFrame,
|
|
256
|
+
AgentStatusFrame,
|
|
257
|
+
AgentMessageFrame,
|
|
258
|
+
AgentReadHistoryRequestFrame,
|
|
259
|
+
BackendHelloAckFrame,
|
|
260
|
+
RouteToAgentFrame,
|
|
261
|
+
HistoryResponseFrame,
|
|
262
|
+
AgentSpawnFrame,
|
|
263
|
+
AgentStopFrame
|
|
264
|
+
]);
|
|
265
|
+
var HumanSendMessageBody = z.object({
|
|
266
|
+
channelId: z.string(),
|
|
267
|
+
content: z.string(),
|
|
268
|
+
mentions: z.array(z.string()).default([]),
|
|
269
|
+
threadParentId: z.string().nullable().default(null)
|
|
270
|
+
});
|
|
271
|
+
var WebMessageEvent = z.object({
|
|
272
|
+
type: z.literal("message"),
|
|
273
|
+
payload: Message
|
|
274
|
+
});
|
|
275
|
+
var WebAgentStatusEvent = z.object({
|
|
276
|
+
type: z.literal("agent.status"),
|
|
277
|
+
payload: z.object({
|
|
278
|
+
agentId: z.string(),
|
|
279
|
+
status: AgentStatus
|
|
280
|
+
})
|
|
281
|
+
});
|
|
282
|
+
var WebComputerEvent = z.object({
|
|
283
|
+
type: z.literal("computer"),
|
|
284
|
+
payload: Computer
|
|
285
|
+
});
|
|
286
|
+
var WebAgentEvent = z.object({
|
|
287
|
+
type: z.literal("agent"),
|
|
288
|
+
payload: Agent
|
|
289
|
+
});
|
|
290
|
+
var WebChannelEvent = z.object({
|
|
291
|
+
type: z.literal("channel"),
|
|
292
|
+
payload: Channel
|
|
293
|
+
});
|
|
294
|
+
var WebAgentRemovedEvent = z.object({
|
|
295
|
+
type: z.literal("agent.removed"),
|
|
296
|
+
payload: z.object({ agentId: z.string() })
|
|
297
|
+
});
|
|
298
|
+
var WebChannelRemovedEvent = z.object({
|
|
299
|
+
type: z.literal("channel.removed"),
|
|
300
|
+
payload: z.object({ channelId: z.string() })
|
|
301
|
+
});
|
|
302
|
+
var WebSessionEvent = z.object({
|
|
303
|
+
type: z.literal("session"),
|
|
304
|
+
payload: Session
|
|
305
|
+
});
|
|
306
|
+
var WebSessionRemovedEvent = z.object({
|
|
307
|
+
type: z.literal("session.removed"),
|
|
308
|
+
payload: z.object({ sessionId: z.string() })
|
|
309
|
+
});
|
|
310
|
+
var WebEvent = z.discriminatedUnion("type", [
|
|
311
|
+
WebMessageEvent,
|
|
312
|
+
WebAgentStatusEvent,
|
|
313
|
+
WebComputerEvent,
|
|
314
|
+
WebAgentEvent,
|
|
315
|
+
WebChannelEvent,
|
|
316
|
+
WebAgentRemovedEvent,
|
|
317
|
+
WebChannelRemovedEvent,
|
|
318
|
+
WebSessionEvent,
|
|
319
|
+
WebSessionRemovedEvent
|
|
320
|
+
]);
|
|
321
|
+
var RenameComputerBody = z.object({
|
|
322
|
+
customName: z.string().min(1).max(64).nullable()
|
|
323
|
+
});
|
|
324
|
+
var CreateDmBody = z.object({
|
|
325
|
+
targetKind: z.enum(["agent", "session"]),
|
|
326
|
+
targetId: z.string()
|
|
327
|
+
});
|
|
328
|
+
var ImportSessionBody = z.object({
|
|
329
|
+
computerId: z.string(),
|
|
330
|
+
sessionUuid: z.string(),
|
|
331
|
+
projectDir: z.string(),
|
|
332
|
+
displayName: z.string(),
|
|
333
|
+
customTitle: z.string().nullable().default(null),
|
|
334
|
+
firstSnippet: z.string().nullable().default(null),
|
|
335
|
+
messageCount: z.number().int().nonnegative().default(0),
|
|
336
|
+
lastModified: z.string().nullable().default(null)
|
|
337
|
+
});
|
|
338
|
+
var CreateAgentBody = z.object({
|
|
339
|
+
name: z.string().min(1).max(64),
|
|
340
|
+
description: z.string().nullable().default(null),
|
|
341
|
+
rolePrompt: z.string().nullable().default(null),
|
|
342
|
+
runtime: RuntimeKind,
|
|
343
|
+
model: z.string().nullable().default(null),
|
|
344
|
+
homeComputerId: z.string(),
|
|
345
|
+
envVars: z.record(z.string()).default({}),
|
|
346
|
+
sourceSessionIds: z.array(z.string()).min(1).max(20)
|
|
347
|
+
});
|
|
348
|
+
var CreateChannelBody = z.object({
|
|
349
|
+
name: z.string().min(1).max(64),
|
|
350
|
+
purpose: z.string().nullable().default(null),
|
|
351
|
+
memberAgentIds: z.array(z.string()).default([]),
|
|
352
|
+
memberSessionIds: z.array(z.string()).default([])
|
|
353
|
+
});
|
|
354
|
+
var CreateTeamBody = z.object({
|
|
355
|
+
name: z.string().min(1).max(64)
|
|
356
|
+
});
|
|
357
|
+
var CreateComputerBody = z.object({
|
|
358
|
+
name: z.string().min(1).max(64)
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// src/ws-client.ts
|
|
362
|
+
var RECONNECT_DELAYS_MS = [500, 1e3, 2e3, 5e3, 1e4, 3e4];
|
|
363
|
+
var WsClient = class {
|
|
364
|
+
constructor(opts) {
|
|
365
|
+
this.opts = opts;
|
|
366
|
+
}
|
|
367
|
+
opts;
|
|
368
|
+
ws = null;
|
|
369
|
+
closed = false;
|
|
370
|
+
reconnectAttempt = 0;
|
|
371
|
+
start() {
|
|
372
|
+
this.connect();
|
|
373
|
+
}
|
|
374
|
+
send(frame) {
|
|
375
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
376
|
+
this.ws.send(JSON.stringify(frame));
|
|
377
|
+
}
|
|
378
|
+
stop() {
|
|
379
|
+
this.closed = true;
|
|
380
|
+
this.ws?.close();
|
|
381
|
+
}
|
|
382
|
+
connect() {
|
|
383
|
+
if (this.closed) return;
|
|
384
|
+
const url = new URL(this.opts.serverUrl);
|
|
385
|
+
url.searchParams.set("api_key", this.opts.apiKey);
|
|
386
|
+
const ws = new WebSocket(url);
|
|
387
|
+
this.ws = ws;
|
|
388
|
+
ws.on("open", () => {
|
|
389
|
+
this.reconnectAttempt = 0;
|
|
390
|
+
this.opts.onOpen?.();
|
|
391
|
+
});
|
|
392
|
+
ws.on("message", (data) => {
|
|
393
|
+
let raw;
|
|
394
|
+
try {
|
|
395
|
+
raw = JSON.parse(data.toString());
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const parsed2 = BusFrame.safeParse(raw);
|
|
400
|
+
if (!parsed2.success) return;
|
|
401
|
+
this.opts.onFrame?.(parsed2.data);
|
|
402
|
+
});
|
|
403
|
+
ws.on("close", (code, reasonBuf) => {
|
|
404
|
+
const reason = reasonBuf.toString();
|
|
405
|
+
this.opts.onClose?.(code, reason);
|
|
406
|
+
this.ws = null;
|
|
407
|
+
this.scheduleReconnect();
|
|
408
|
+
});
|
|
409
|
+
ws.on("error", () => {
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
scheduleReconnect() {
|
|
413
|
+
if (this.closed) return;
|
|
414
|
+
const idx = Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1);
|
|
415
|
+
const delay = RECONNECT_DELAYS_MS[idx] ?? 3e4;
|
|
416
|
+
this.reconnectAttempt += 1;
|
|
417
|
+
setTimeout(() => this.connect(), delay);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// src/runtime-detect.ts
|
|
422
|
+
import { spawn } from "child_process";
|
|
423
|
+
var CANDIDATES = ["claude", "codex", "cursor", "gemini"];
|
|
424
|
+
async function detectRuntimes() {
|
|
425
|
+
const results = await Promise.all(
|
|
426
|
+
CANDIDATES.map(async (name) => {
|
|
427
|
+
const ok = await new Promise((resolve) => {
|
|
428
|
+
const child = spawn("which", [name], { stdio: "ignore" });
|
|
429
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
430
|
+
child.on("error", () => resolve(false));
|
|
431
|
+
});
|
|
432
|
+
return ok ? name : null;
|
|
433
|
+
})
|
|
434
|
+
);
|
|
435
|
+
return results.filter((r) => r !== null);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/agent-process.ts
|
|
439
|
+
import { spawn as spawn2 } from "child_process";
|
|
440
|
+
import { fileURLToPath } from "url";
|
|
441
|
+
import { resolve as resolvePath, dirname } from "path";
|
|
442
|
+
import { existsSync } from "fs";
|
|
443
|
+
function resolveChatBridge() {
|
|
444
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
445
|
+
const distPath = resolvePath(here, "chat-bridge.js");
|
|
446
|
+
if (existsSync(distPath)) {
|
|
447
|
+
return { command: "node", pathArg: distPath };
|
|
448
|
+
}
|
|
449
|
+
return { command: "tsx", pathArg: resolvePath(here, "chat-bridge.ts") };
|
|
450
|
+
}
|
|
451
|
+
function runTurn(opts) {
|
|
452
|
+
const bridge = resolveChatBridge();
|
|
453
|
+
const mcpConfig = {
|
|
454
|
+
mcpServers: {
|
|
455
|
+
chat: {
|
|
456
|
+
command: bridge.command,
|
|
457
|
+
args: [
|
|
458
|
+
bridge.pathArg,
|
|
459
|
+
"--agent-id",
|
|
460
|
+
opts.agentId,
|
|
461
|
+
"--server-url",
|
|
462
|
+
opts.serverUrl,
|
|
463
|
+
"--api-key",
|
|
464
|
+
opts.apiKey
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const args = [
|
|
470
|
+
"-p",
|
|
471
|
+
opts.prompt,
|
|
472
|
+
"--output-format",
|
|
473
|
+
"stream-json",
|
|
474
|
+
"--verbose",
|
|
475
|
+
"--allow-dangerously-skip-permissions",
|
|
476
|
+
"--dangerously-skip-permissions",
|
|
477
|
+
"--permission-mode",
|
|
478
|
+
"bypassPermissions",
|
|
479
|
+
"--disallowed-tools",
|
|
480
|
+
"EnterPlanMode,ExitPlanMode",
|
|
481
|
+
"--mcp-config",
|
|
482
|
+
JSON.stringify(mcpConfig)
|
|
483
|
+
];
|
|
484
|
+
if (opts.resumeSessionId) {
|
|
485
|
+
args.push("--resume", opts.resumeSessionId);
|
|
486
|
+
}
|
|
487
|
+
return new Promise((resolve) => {
|
|
488
|
+
const child = spawn2("claude", args, {
|
|
489
|
+
cwd: opts.workspacePath ?? process.cwd(),
|
|
490
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
491
|
+
env: { ...process.env }
|
|
492
|
+
});
|
|
493
|
+
let stdoutBuffer = "";
|
|
494
|
+
const textChunks = [];
|
|
495
|
+
let sessionId = null;
|
|
496
|
+
let errorMessage = null;
|
|
497
|
+
child.stdout.on("data", (chunk) => {
|
|
498
|
+
stdoutBuffer += chunk.toString();
|
|
499
|
+
let nl = stdoutBuffer.indexOf("\n");
|
|
500
|
+
while (nl !== -1) {
|
|
501
|
+
const line = stdoutBuffer.slice(0, nl).trim();
|
|
502
|
+
stdoutBuffer = stdoutBuffer.slice(nl + 1);
|
|
503
|
+
if (line) handleLine(line);
|
|
504
|
+
nl = stdoutBuffer.indexOf("\n");
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
child.stderr.on("data", (chunk) => {
|
|
508
|
+
process.stderr.write(`[agent ${opts.agentId.slice(0, 8)} stderr] ${chunk.toString()}`);
|
|
509
|
+
});
|
|
510
|
+
child.on("exit", (code) => {
|
|
511
|
+
if (stdoutBuffer.trim()) handleLine(stdoutBuffer.trim());
|
|
512
|
+
resolve({
|
|
513
|
+
text: textChunks.join("").trim(),
|
|
514
|
+
sessionId,
|
|
515
|
+
exitCode: code,
|
|
516
|
+
errorMessage
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
function handleLine(line) {
|
|
520
|
+
let parsed2;
|
|
521
|
+
try {
|
|
522
|
+
parsed2 = JSON.parse(line);
|
|
523
|
+
} catch {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (!parsed2 || typeof parsed2 !== "object") return;
|
|
527
|
+
const obj = parsed2;
|
|
528
|
+
const type = obj.type;
|
|
529
|
+
if (type === "assistant") {
|
|
530
|
+
const msg = obj.message;
|
|
531
|
+
if (msg && Array.isArray(msg.content)) {
|
|
532
|
+
for (const block of msg.content) {
|
|
533
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
534
|
+
textChunks.push(block.text);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (type === "result") {
|
|
541
|
+
if (typeof obj.session_id === "string") sessionId = obj.session_id;
|
|
542
|
+
if (obj.is_error && typeof obj.result === "string") errorMessage = obj.result;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/session-scanner.ts
|
|
550
|
+
import { promises as fs } from "fs";
|
|
551
|
+
import { join as joinPath } from "path";
|
|
552
|
+
import { homedir } from "os";
|
|
553
|
+
var CLAUDE_PROJECTS_DIR = joinPath(homedir(), ".claude", "projects");
|
|
554
|
+
var READ_BUDGET_BYTES = 5 * 1024 * 1024;
|
|
555
|
+
var SNIPPET_PROBE_BYTES = 64 * 1024;
|
|
556
|
+
var SNIPPET_MAX_CHARS = 200;
|
|
557
|
+
function decodeProjectDir(rawName) {
|
|
558
|
+
if (rawName.startsWith("-")) {
|
|
559
|
+
return "/" + rawName.slice(1).replaceAll("-", "/");
|
|
560
|
+
}
|
|
561
|
+
return rawName.replaceAll("-", "/");
|
|
562
|
+
}
|
|
563
|
+
function parseBuffer(buf, partialCount) {
|
|
564
|
+
let customTitle = null;
|
|
565
|
+
let snippet = null;
|
|
566
|
+
let messageCount = 0;
|
|
567
|
+
let start = 0;
|
|
568
|
+
for (let i = 0; i <= buf.length; i += 1) {
|
|
569
|
+
if (i === buf.length || buf[i] === 10) {
|
|
570
|
+
if (i > start) {
|
|
571
|
+
messageCount += 1;
|
|
572
|
+
const line = buf.toString("utf-8", start, i);
|
|
573
|
+
if (customTitle === null) {
|
|
574
|
+
const t = tryExtractCustomTitle(line);
|
|
575
|
+
if (t !== null) customTitle = t;
|
|
576
|
+
}
|
|
577
|
+
if (snippet === null) {
|
|
578
|
+
snippet = tryExtractUserText(line);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
start = i + 1;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (partialCount) messageCount = 0;
|
|
585
|
+
return { customTitle, snippet, messageCount };
|
|
586
|
+
}
|
|
587
|
+
function tryExtractCustomTitle(line) {
|
|
588
|
+
if (!line.includes('"custom-title"')) return null;
|
|
589
|
+
let obj;
|
|
590
|
+
try {
|
|
591
|
+
obj = JSON.parse(line);
|
|
592
|
+
} catch {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
if (!obj || typeof obj !== "object") return null;
|
|
596
|
+
const o = obj;
|
|
597
|
+
if (o.type !== "custom-title") return null;
|
|
598
|
+
const t = o.customTitle ?? o.custom_title ?? o.title;
|
|
599
|
+
if (typeof t !== "string" || !t.trim()) return null;
|
|
600
|
+
return t.trim();
|
|
601
|
+
}
|
|
602
|
+
function tryExtractUserText(line) {
|
|
603
|
+
let obj;
|
|
604
|
+
try {
|
|
605
|
+
obj = JSON.parse(line);
|
|
606
|
+
} catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
if (!obj || typeof obj !== "object") return null;
|
|
610
|
+
const o = obj;
|
|
611
|
+
if (o.type !== "user") return null;
|
|
612
|
+
const msg = o.message;
|
|
613
|
+
if (!msg || typeof msg !== "object") return null;
|
|
614
|
+
const m = msg;
|
|
615
|
+
if (m.role !== "user") return null;
|
|
616
|
+
const content = m.content;
|
|
617
|
+
let text = null;
|
|
618
|
+
if (typeof content === "string") {
|
|
619
|
+
text = content;
|
|
620
|
+
} else if (Array.isArray(content)) {
|
|
621
|
+
const texts = [];
|
|
622
|
+
for (const block of content) {
|
|
623
|
+
if (block && typeof block === "object") {
|
|
624
|
+
const b = block;
|
|
625
|
+
if (b.type === "text" && typeof b.text === "string") texts.push(b.text);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (texts.length > 0) text = texts.join(" ");
|
|
629
|
+
}
|
|
630
|
+
if (!text) return null;
|
|
631
|
+
const trimmed = text.trim();
|
|
632
|
+
if (!trimmed) return null;
|
|
633
|
+
if (trimmed.startsWith("<command-")) return null;
|
|
634
|
+
if (trimmed.startsWith("<system-reminder>")) return null;
|
|
635
|
+
if (trimmed.startsWith("Caveat:")) return null;
|
|
636
|
+
if (trimmed.startsWith("[Request interrupted")) return null;
|
|
637
|
+
return trimmed.replace(/\s+/g, " ").slice(0, SNIPPET_MAX_CHARS);
|
|
638
|
+
}
|
|
639
|
+
async function readSessionMeta(filePath, sizeBytes) {
|
|
640
|
+
try {
|
|
641
|
+
if (sizeBytes <= READ_BUDGET_BYTES) {
|
|
642
|
+
const buf = await fs.readFile(filePath);
|
|
643
|
+
return parseBuffer(buf, false);
|
|
644
|
+
}
|
|
645
|
+
const fd = await fs.open(filePath, "r");
|
|
646
|
+
try {
|
|
647
|
+
const buf = Buffer.alloc(Math.min(SNIPPET_PROBE_BYTES, sizeBytes));
|
|
648
|
+
await fd.read(buf, 0, buf.length, 0);
|
|
649
|
+
const partial = parseBuffer(buf, true);
|
|
650
|
+
return {
|
|
651
|
+
customTitle: partial.customTitle,
|
|
652
|
+
snippet: partial.snippet,
|
|
653
|
+
messageCount: Math.max(0, Math.round(sizeBytes / 1500))
|
|
654
|
+
};
|
|
655
|
+
} finally {
|
|
656
|
+
await fd.close();
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
return { customTitle: null, snippet: null, messageCount: 0 };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function scanClaudeSessions() {
|
|
663
|
+
let projectDirs;
|
|
664
|
+
try {
|
|
665
|
+
projectDirs = await fs.readdir(CLAUDE_PROJECTS_DIR);
|
|
666
|
+
} catch {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
const out = [];
|
|
670
|
+
for (const rawName of projectDirs) {
|
|
671
|
+
const dirPath = joinPath(CLAUDE_PROJECTS_DIR, rawName);
|
|
672
|
+
let dirStat;
|
|
673
|
+
try {
|
|
674
|
+
dirStat = await fs.stat(dirPath);
|
|
675
|
+
} catch {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (!dirStat.isDirectory()) continue;
|
|
679
|
+
let files;
|
|
680
|
+
try {
|
|
681
|
+
files = await fs.readdir(dirPath);
|
|
682
|
+
} catch {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const decoded = decodeProjectDir(rawName);
|
|
686
|
+
for (const file of files) {
|
|
687
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
688
|
+
const sessionId = file.slice(0, -".jsonl".length);
|
|
689
|
+
const filePath = joinPath(dirPath, file);
|
|
690
|
+
let stat;
|
|
691
|
+
try {
|
|
692
|
+
stat = await fs.stat(filePath);
|
|
693
|
+
} catch {
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const meta = await readSessionMeta(filePath, stat.size);
|
|
697
|
+
out.push({
|
|
698
|
+
sessionId,
|
|
699
|
+
projectDir: decoded,
|
|
700
|
+
rawProjectDirName: rawName,
|
|
701
|
+
lastModified: stat.mtime.toISOString(),
|
|
702
|
+
messageCount: meta.messageCount,
|
|
703
|
+
sizeBytes: stat.size,
|
|
704
|
+
customTitle: meta.customTitle,
|
|
705
|
+
firstSnippet: meta.snippet
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
out.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/session-reader.ts
|
|
714
|
+
import { promises as fs2 } from "fs";
|
|
715
|
+
import { join as joinPath2 } from "path";
|
|
716
|
+
import { homedir as homedir2 } from "os";
|
|
717
|
+
var CLAUDE_PROJECTS_DIR2 = joinPath2(homedir2(), ".claude", "projects");
|
|
718
|
+
async function findSessionFile(sessionId) {
|
|
719
|
+
let projectDirs;
|
|
720
|
+
try {
|
|
721
|
+
projectDirs = await fs2.readdir(CLAUDE_PROJECTS_DIR2);
|
|
722
|
+
} catch {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const target = `${sessionId}.jsonl`;
|
|
726
|
+
for (const dir of projectDirs) {
|
|
727
|
+
const candidate = joinPath2(CLAUDE_PROJECTS_DIR2, dir, target);
|
|
728
|
+
try {
|
|
729
|
+
const st = await fs2.stat(candidate);
|
|
730
|
+
if (st.isFile()) return candidate;
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
function extractText(content) {
|
|
737
|
+
if (typeof content === "string") return content;
|
|
738
|
+
if (!Array.isArray(content)) return null;
|
|
739
|
+
const pieces = [];
|
|
740
|
+
for (const block of content) {
|
|
741
|
+
if (!block || typeof block !== "object") continue;
|
|
742
|
+
const b = block;
|
|
743
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
744
|
+
pieces.push(b.text);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return pieces.length > 0 ? pieces.join("\n").trim() : null;
|
|
748
|
+
}
|
|
749
|
+
function isNoiseUserText(text) {
|
|
750
|
+
const t = text.trim();
|
|
751
|
+
if (!t) return true;
|
|
752
|
+
if (t.startsWith("<command-")) return true;
|
|
753
|
+
if (t.startsWith("<system-reminder>")) return true;
|
|
754
|
+
if (t.startsWith("Caveat:")) return true;
|
|
755
|
+
if (t.startsWith("[Request interrupted")) return true;
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
var DAEMON_PROMPT_PREFIX = /^\[#[^\]\s]+ from \w+:[a-z0-9-]{1,12}\]\s+/;
|
|
759
|
+
var DAEMON_PROMPT_SUFFIX = /\n\n+Reply directly with your response text\. Do not call any tools\.\s*$/;
|
|
760
|
+
function unwrapDaemonPrompt(text) {
|
|
761
|
+
const trimmedTail = text.replace(DAEMON_PROMPT_SUFFIX, "");
|
|
762
|
+
const trimmedHead = trimmedTail.replace(DAEMON_PROMPT_PREFIX, "");
|
|
763
|
+
return trimmedHead;
|
|
764
|
+
}
|
|
765
|
+
async function readSessionMessages(sessionId) {
|
|
766
|
+
const path = await findSessionFile(sessionId);
|
|
767
|
+
if (!path) return [];
|
|
768
|
+
let buf;
|
|
769
|
+
try {
|
|
770
|
+
buf = await fs2.readFile(path);
|
|
771
|
+
} catch {
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
const out = [];
|
|
775
|
+
let start = 0;
|
|
776
|
+
for (let i = 0; i <= buf.length; i += 1) {
|
|
777
|
+
if (i === buf.length || buf[i] === 10) {
|
|
778
|
+
if (i > start) {
|
|
779
|
+
const line = buf.toString("utf-8", start, i);
|
|
780
|
+
const m = parseLine(line);
|
|
781
|
+
if (m) out.push(m);
|
|
782
|
+
}
|
|
783
|
+
start = i + 1;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
788
|
+
function parseLine(line) {
|
|
789
|
+
let obj;
|
|
790
|
+
try {
|
|
791
|
+
obj = JSON.parse(line);
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
if (!obj || typeof obj !== "object") return null;
|
|
796
|
+
const o = obj;
|
|
797
|
+
const t = o.type;
|
|
798
|
+
if (t !== "user" && t !== "assistant") return null;
|
|
799
|
+
const msg = o.message;
|
|
800
|
+
if (!msg || typeof msg !== "object") return null;
|
|
801
|
+
const m = msg;
|
|
802
|
+
const role = m.role;
|
|
803
|
+
if (role !== "user" && role !== "assistant") return null;
|
|
804
|
+
const rawText = extractText(m.content);
|
|
805
|
+
if (!rawText) return null;
|
|
806
|
+
if (role === "user" && isNoiseUserText(rawText)) return null;
|
|
807
|
+
const cleaned = role === "user" ? unwrapDaemonPrompt(rawText).trim() : rawText.trim();
|
|
808
|
+
if (!cleaned) return null;
|
|
809
|
+
const ts = typeof o.timestamp === "string" ? o.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
810
|
+
return {
|
|
811
|
+
role: role === "user" ? "human" : "agent",
|
|
812
|
+
content: cleaned,
|
|
813
|
+
timestamp: ts
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/daemon-core.ts
|
|
818
|
+
var DAEMON_VERSION = "0.0.1";
|
|
819
|
+
var DaemonCore = class {
|
|
820
|
+
constructor(args) {
|
|
821
|
+
this.args = args;
|
|
822
|
+
this.daemonId = args.daemonId ?? loadOrCreateDaemonId();
|
|
823
|
+
}
|
|
824
|
+
args;
|
|
825
|
+
ws = null;
|
|
826
|
+
agents = /* @__PURE__ */ new Map();
|
|
827
|
+
daemonId;
|
|
828
|
+
sessionScanTimer = null;
|
|
829
|
+
async start() {
|
|
830
|
+
const detected = await detectRuntimes();
|
|
831
|
+
process.stderr.write(
|
|
832
|
+
`[daemon] starting id=${this.daemonId} runtimes=${detected.join(",") || "(none)"}
|
|
833
|
+
`
|
|
834
|
+
);
|
|
835
|
+
this.ws = new WsClient({
|
|
836
|
+
serverUrl: this.args.serverUrl,
|
|
837
|
+
apiKey: this.args.apiKey,
|
|
838
|
+
onOpen: () => this.onConnected(detected),
|
|
839
|
+
onFrame: (f) => this.onFrame(f),
|
|
840
|
+
onClose: (code, reason) => {
|
|
841
|
+
process.stderr.write(`[daemon] ws closed code=${code} reason=${reason}
|
|
842
|
+
`);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
this.ws.start();
|
|
846
|
+
}
|
|
847
|
+
async stop() {
|
|
848
|
+
if (this.sessionScanTimer) clearInterval(this.sessionScanTimer);
|
|
849
|
+
this.agents.clear();
|
|
850
|
+
this.ws?.stop();
|
|
851
|
+
}
|
|
852
|
+
onConnected(detected) {
|
|
853
|
+
process.stderr.write("[daemon] ws open, sending hello\n");
|
|
854
|
+
this.ws?.send({
|
|
855
|
+
type: "daemon.hello",
|
|
856
|
+
payload: {
|
|
857
|
+
daemonId: this.daemonId,
|
|
858
|
+
hostname: hostname(),
|
|
859
|
+
platform: platform(),
|
|
860
|
+
daemonVersion: DAEMON_VERSION,
|
|
861
|
+
detectedRuntimes: detected
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
void this.advertiseSessions();
|
|
865
|
+
if (!this.sessionScanTimer) {
|
|
866
|
+
this.sessionScanTimer = setInterval(() => {
|
|
867
|
+
void this.advertiseSessions();
|
|
868
|
+
}, 6e4);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async advertiseSessions() {
|
|
872
|
+
try {
|
|
873
|
+
const sessions = await scanClaudeSessions();
|
|
874
|
+
process.stderr.write(`[daemon] discovered ${sessions.length} claude session(s)
|
|
875
|
+
`);
|
|
876
|
+
this.ws?.send({
|
|
877
|
+
type: "daemon.sessions_advertised",
|
|
878
|
+
payload: { daemonId: this.daemonId, sessions }
|
|
879
|
+
});
|
|
880
|
+
} catch (err) {
|
|
881
|
+
process.stderr.write(`[daemon] session scan failed: ${String(err)}
|
|
882
|
+
`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
onFrame(frame) {
|
|
886
|
+
switch (frame.type) {
|
|
887
|
+
case "backend.hello_ack":
|
|
888
|
+
process.stderr.write(
|
|
889
|
+
`[daemon] hello_ack accepted=${frame.payload.accepted}` + (frame.payload.reason ? ` reason=${frame.payload.reason}` : "") + "\n"
|
|
890
|
+
);
|
|
891
|
+
return;
|
|
892
|
+
case "backend.agent.spawn":
|
|
893
|
+
this.handleSpawn(frame.payload);
|
|
894
|
+
return;
|
|
895
|
+
case "backend.agent.stop":
|
|
896
|
+
this.handleStop(frame.payload.agentId);
|
|
897
|
+
return;
|
|
898
|
+
case "backend.route_to_agent":
|
|
899
|
+
this.handleRouteToAgent(frame.payload);
|
|
900
|
+
return;
|
|
901
|
+
default:
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
handleSpawn(payload) {
|
|
906
|
+
if (payload.runtime !== "claude") {
|
|
907
|
+
process.stderr.write(
|
|
908
|
+
`[daemon] runtime ${payload.runtime} not supported in V0.1 (claude only)
|
|
909
|
+
`
|
|
910
|
+
);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (this.agents.has(payload.agentId)) {
|
|
914
|
+
process.stderr.write(`[daemon] agent ${payload.agentId} already registered
|
|
915
|
+
`);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
process.stderr.write(
|
|
919
|
+
`[daemon] registered agent ${payload.agentId.slice(0, 8)} (per-turn spawn model)
|
|
920
|
+
`
|
|
921
|
+
);
|
|
922
|
+
this.agents.set(payload.agentId, {
|
|
923
|
+
agentId: payload.agentId,
|
|
924
|
+
workspacePath: payload.workspacePath,
|
|
925
|
+
resumeSessionId: payload.resumeSessionId,
|
|
926
|
+
busy: false,
|
|
927
|
+
pendingPrompts: []
|
|
928
|
+
});
|
|
929
|
+
this.ws?.send({
|
|
930
|
+
type: "agent.status",
|
|
931
|
+
payload: { agentId: payload.agentId, status: "hot" }
|
|
932
|
+
});
|
|
933
|
+
if (payload.historyTargetChannelId && payload.resumeSessionId) {
|
|
934
|
+
void this.shipSessionHistory(
|
|
935
|
+
payload.agentId,
|
|
936
|
+
payload.resumeSessionId,
|
|
937
|
+
payload.historyTargetChannelId
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async shipSessionHistory(agentId, sessionId, channelId) {
|
|
942
|
+
try {
|
|
943
|
+
const messages = await readSessionMessages(sessionId);
|
|
944
|
+
process.stderr.write(
|
|
945
|
+
`[daemon] shipping ${messages.length} historical message(s) for agent ${agentId.slice(0, 8)}
|
|
946
|
+
`
|
|
947
|
+
);
|
|
948
|
+
this.ws?.send({
|
|
949
|
+
type: "daemon.session_history",
|
|
950
|
+
payload: {
|
|
951
|
+
daemonId: this.daemonId,
|
|
952
|
+
agentId,
|
|
953
|
+
channelId,
|
|
954
|
+
messages
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
} catch (err) {
|
|
958
|
+
process.stderr.write(`[daemon] session history read failed: ${String(err)}
|
|
959
|
+
`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
handleStop(agentId) {
|
|
963
|
+
if (this.agents.delete(agentId)) {
|
|
964
|
+
this.ws?.send({
|
|
965
|
+
type: "agent.status",
|
|
966
|
+
payload: { agentId, status: "cold" }
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
handleRouteToAgent(payload) {
|
|
971
|
+
const state = this.agents.get(payload.agentId);
|
|
972
|
+
if (!state) {
|
|
973
|
+
process.stderr.write(
|
|
974
|
+
`[daemon] route_to_agent for unregistered agent ${payload.agentId}; ignoring
|
|
975
|
+
`
|
|
976
|
+
);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const prompt = `[#${payload.channelId} from ${payload.fromRole}:${payload.fromId.slice(0, 8)}] ${payload.content}`;
|
|
980
|
+
state.pendingPrompts.push(prompt);
|
|
981
|
+
void this.drainQueue(state, payload.channelId);
|
|
982
|
+
}
|
|
983
|
+
async drainQueue(state, channelId) {
|
|
984
|
+
if (state.busy) return;
|
|
985
|
+
state.busy = true;
|
|
986
|
+
try {
|
|
987
|
+
while (state.pendingPrompts.length > 0) {
|
|
988
|
+
const prompt = state.pendingPrompts.shift();
|
|
989
|
+
process.stderr.write(
|
|
990
|
+
`[daemon] running turn for agent ${state.agentId.slice(0, 8)} (resume=${state.resumeSessionId ?? "new"})
|
|
991
|
+
`
|
|
992
|
+
);
|
|
993
|
+
const result = await runTurn({
|
|
994
|
+
agentId: state.agentId,
|
|
995
|
+
prompt,
|
|
996
|
+
resumeSessionId: state.resumeSessionId,
|
|
997
|
+
workspacePath: state.workspacePath,
|
|
998
|
+
serverUrl: this.args.serverUrl,
|
|
999
|
+
apiKey: this.args.apiKey
|
|
1000
|
+
});
|
|
1001
|
+
if (result.sessionId && result.sessionId !== state.resumeSessionId) {
|
|
1002
|
+
state.resumeSessionId = result.sessionId;
|
|
1003
|
+
this.ws?.send({
|
|
1004
|
+
type: "agent.session_update",
|
|
1005
|
+
payload: { agentId: state.agentId, resumeSessionId: result.sessionId }
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
process.stderr.write(
|
|
1009
|
+
`[daemon] turn done agent=${state.agentId.slice(0, 8)} exit=${result.exitCode} text_len=${result.text.length}
|
|
1010
|
+
`
|
|
1011
|
+
);
|
|
1012
|
+
if (result.errorMessage) {
|
|
1013
|
+
process.stderr.write(`[daemon] turn error: ${result.errorMessage}
|
|
1014
|
+
`);
|
|
1015
|
+
}
|
|
1016
|
+
if (result.text) {
|
|
1017
|
+
this.ws?.send({
|
|
1018
|
+
type: "agent.message",
|
|
1019
|
+
payload: {
|
|
1020
|
+
agentId: state.agentId,
|
|
1021
|
+
channelId,
|
|
1022
|
+
content: result.text,
|
|
1023
|
+
mentions: [],
|
|
1024
|
+
threadParentId: null
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} finally {
|
|
1030
|
+
state.busy = false;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
function loadOrCreateDaemonId() {
|
|
1035
|
+
const dir = joinPath3(homedir3(), ".agenthub");
|
|
1036
|
+
const file = joinPath3(dir, "daemon-id");
|
|
1037
|
+
try {
|
|
1038
|
+
if (existsSync2(file)) {
|
|
1039
|
+
const v = readFileSync(file, "utf-8").trim();
|
|
1040
|
+
if (v) return v;
|
|
1041
|
+
}
|
|
1042
|
+
mkdirSync(dir, { recursive: true });
|
|
1043
|
+
const id = randomUUID();
|
|
1044
|
+
writeFileSync(file, id);
|
|
1045
|
+
return id;
|
|
1046
|
+
} catch {
|
|
1047
|
+
return randomUUID();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/cli-args.ts
|
|
1052
|
+
var DAEMON_CLI_USAGE = `Usage: agenthub-daemon --server-url <url> --api-key <key> [--daemon-id <id>]
|
|
1053
|
+
|
|
1054
|
+
--server-url Backend bus URL (ws:// or wss://). Required.
|
|
1055
|
+
--api-key Auth token for this daemon. Required.
|
|
1056
|
+
--daemon-id Persistent daemon identifier. Optional \u2014 auto-generated and
|
|
1057
|
+
cached in ~/.agenthub/daemon-id if omitted.`;
|
|
1058
|
+
function parseDaemonCliArgs(argv) {
|
|
1059
|
+
let serverUrl = "";
|
|
1060
|
+
let apiKey = "";
|
|
1061
|
+
let daemonId = null;
|
|
1062
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1063
|
+
const arg = argv[i];
|
|
1064
|
+
const next = argv[i + 1];
|
|
1065
|
+
switch (arg) {
|
|
1066
|
+
case "--server-url":
|
|
1067
|
+
if (!next) return null;
|
|
1068
|
+
serverUrl = next;
|
|
1069
|
+
i += 1;
|
|
1070
|
+
break;
|
|
1071
|
+
case "--api-key":
|
|
1072
|
+
if (!next) return null;
|
|
1073
|
+
apiKey = next;
|
|
1074
|
+
i += 1;
|
|
1075
|
+
break;
|
|
1076
|
+
case "--daemon-id":
|
|
1077
|
+
if (!next) return null;
|
|
1078
|
+
daemonId = next;
|
|
1079
|
+
i += 1;
|
|
1080
|
+
break;
|
|
1081
|
+
case "--help":
|
|
1082
|
+
case "-h":
|
|
1083
|
+
return null;
|
|
1084
|
+
case "--":
|
|
1085
|
+
break;
|
|
1086
|
+
default:
|
|
1087
|
+
if (arg && arg.startsWith("--")) return null;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (!serverUrl || !apiKey) return null;
|
|
1091
|
+
return { serverUrl, apiKey, daemonId };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/index.ts
|
|
1095
|
+
var parsed = parseDaemonCliArgs(process.argv.slice(2));
|
|
1096
|
+
if (!parsed) {
|
|
1097
|
+
console.error(DAEMON_CLI_USAGE);
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
var daemon = new DaemonCore(parsed);
|
|
1101
|
+
var shutdown = async () => {
|
|
1102
|
+
process.stderr.write("[daemon] shutting down\n");
|
|
1103
|
+
await daemon.stop();
|
|
1104
|
+
process.exit(0);
|
|
1105
|
+
};
|
|
1106
|
+
process.on("SIGTERM", () => {
|
|
1107
|
+
void shutdown();
|
|
1108
|
+
});
|
|
1109
|
+
process.on("SIGINT", () => {
|
|
1110
|
+
void shutdown();
|
|
1111
|
+
});
|
|
1112
|
+
void daemon.start();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hengyuliu/agenthub-daemon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AgentHub local daemon — connects your machine to AgentHub and bridges AI agent runtimes (Claude/Codex/Cursor/Gemini) to the cloud bus",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agenthub-daemon": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "tsx watch src/index.ts",
|
|
14
|
+
"start": "tsx src/index.ts",
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"agent",
|
|
21
|
+
"daemon",
|
|
22
|
+
"claude",
|
|
23
|
+
"codex",
|
|
24
|
+
"ai",
|
|
25
|
+
"agenthub"
|
|
26
|
+
],
|
|
27
|
+
"author": "hengyuliu",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
|
+
"ws": "^8.18.0",
|
|
32
|
+
"zod": "^3.23.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@agenthub/shared": "workspace:*",
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"@types/ws": "^8.5.0",
|
|
38
|
+
"tsup": "^8.3.0",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
}
|
|
42
|
+
}
|