@slock-ai/daemon 0.2.0 → 0.2.3

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.
@@ -1,138 +1,214 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
2
- import { mkdir, writeFile, access, readdir, stat, readFile, rm } from "node:fs/promises";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import type { AgentMessage, AgentConfig, FileNode, MachineToServerMessage, WorkspaceDirectoryInfo, TrajectoryEntry } from "@slock-ai/shared";
6
-
7
- const DATA_DIR = path.join(os.homedir(), ".slock", "agents");
8
-
9
- /** Max chars for thinking/text content in trajectory entries (sent over WebSocket) */
10
- const MAX_TRAJECTORY_TEXT = 2000;
11
-
12
- interface AgentProcess {
13
- process: ChildProcess;
14
- inbox: AgentMessage[];
15
- pendingReceive: {
16
- resolve: (messages: AgentMessage[]) => void;
17
- timer: ReturnType<typeof setTimeout>;
18
- } | null;
19
- config: AgentConfig;
20
- sessionId: string | null;
21
- isInReceiveMessage: boolean;
22
- notificationTimer: ReturnType<typeof setTimeout> | null;
23
- pendingNotificationCount: number;
24
- }
25
-
26
- export class AgentProcessManager {
27
- private agents = new Map<string, AgentProcess>();
28
- private chatBridgePath: string;
29
- private sendToServer: (msg: MachineToServerMessage) => void;
30
- private daemonApiKey: string;
31
-
32
- constructor(chatBridgePath: string, sendToServer: (msg: MachineToServerMessage) => void, daemonApiKey: string) {
33
- this.chatBridgePath = chatBridgePath;
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import path2 from "path";
5
+ import os2 from "os";
6
+ import { execSync } from "child_process";
7
+ import { accessSync } from "fs";
8
+ import { fileURLToPath } from "url";
9
+
10
+ // src/connection.ts
11
+ import WebSocket from "ws";
12
+ var DaemonConnection = class {
13
+ ws = null;
14
+ options;
15
+ reconnectTimer = null;
16
+ reconnectDelay = 1e3;
17
+ maxReconnectDelay = 3e4;
18
+ shouldConnect = true;
19
+ constructor(options) {
20
+ this.options = options;
21
+ }
22
+ connect() {
23
+ this.shouldConnect = true;
24
+ this.doConnect();
25
+ }
26
+ disconnect() {
27
+ this.shouldConnect = false;
28
+ if (this.reconnectTimer) {
29
+ clearTimeout(this.reconnectTimer);
30
+ this.reconnectTimer = null;
31
+ }
32
+ if (this.ws) {
33
+ this.ws.close();
34
+ this.ws = null;
35
+ }
36
+ }
37
+ send(msg) {
38
+ if (this.ws?.readyState === WebSocket.OPEN) {
39
+ this.ws.send(JSON.stringify(msg));
40
+ }
41
+ }
42
+ get connected() {
43
+ return this.ws?.readyState === WebSocket.OPEN;
44
+ }
45
+ doConnect() {
46
+ if (!this.shouldConnect) return;
47
+ const wsUrl = this.options.serverUrl.replace(/^http/, "ws") + `/daemon/connect?key=${this.options.apiKey}`;
48
+ console.log(`[Daemon] Connecting to ${this.options.serverUrl}...`);
49
+ this.ws = new WebSocket(wsUrl);
50
+ this.ws.on("open", () => {
51
+ console.log("[Daemon] Connected to server");
52
+ this.reconnectDelay = 1e3;
53
+ this.options.onConnect();
54
+ });
55
+ this.ws.on("message", (data) => {
56
+ try {
57
+ const msg = JSON.parse(data.toString());
58
+ this.options.onMessage(msg);
59
+ } catch (err) {
60
+ console.error("[Daemon] Invalid message from server:", err);
61
+ }
62
+ });
63
+ this.ws.on("close", () => {
64
+ console.log("[Daemon] Disconnected from server");
65
+ this.options.onDisconnect();
66
+ this.scheduleReconnect();
67
+ });
68
+ this.ws.on("error", (err) => {
69
+ console.error("[Daemon] WebSocket error:", err.message);
70
+ });
71
+ }
72
+ scheduleReconnect() {
73
+ if (!this.shouldConnect) return;
74
+ if (this.reconnectTimer) return;
75
+ console.log(`[Daemon] Reconnecting in ${this.reconnectDelay}ms...`);
76
+ this.reconnectTimer = setTimeout(() => {
77
+ this.reconnectTimer = null;
78
+ this.doConnect();
79
+ }, this.reconnectDelay);
80
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
81
+ }
82
+ };
83
+
84
+ // src/agentProcessManager.ts
85
+ import { spawn } from "child_process";
86
+ import { mkdir, writeFile, access, readdir, stat, readFile, rm } from "fs/promises";
87
+ import path from "path";
88
+ import os from "os";
89
+ var DATA_DIR = path.join(os.homedir(), ".slock", "agents");
90
+ var MAX_TRAJECTORY_TEXT = 2e3;
91
+ var AgentProcessManager = class {
92
+ agents = /* @__PURE__ */ new Map();
93
+ chatBridgePath;
94
+ sendToServer;
95
+ daemonApiKey;
96
+ constructor(chatBridgePath2, sendToServer, daemonApiKey) {
97
+ this.chatBridgePath = chatBridgePath2;
34
98
  this.sendToServer = sendToServer;
35
99
  this.daemonApiKey = daemonApiKey;
36
100
  }
37
-
38
- async startAgent(agentId: string, config: AgentConfig, wakeMessage?: AgentMessage, unreadSummary?: Record<string, number>) {
101
+ async startAgent(agentId, config, wakeMessage, unreadSummary) {
39
102
  if (this.agents.has(agentId)) return;
40
-
41
103
  const agentDataDir = path.join(DATA_DIR, agentId);
42
104
  await mkdir(agentDataDir, { recursive: true });
43
-
44
- // Create MEMORY.md if not exists
45
105
  const memoryMdPath = path.join(agentDataDir, "MEMORY.md");
46
106
  try {
47
107
  await access(memoryMdPath);
48
108
  } catch {
49
109
  const agentName = config.displayName || config.name;
50
- const initialMemoryMd = `# ${agentName}\n\n## Role\n${config.description || "No role defined yet."}\n\n## Key Knowledge\n- No notes yet.\n\n## Active Context\n- First startup.\n`;
110
+ const initialMemoryMd = `# ${agentName}
111
+
112
+ ## Role
113
+ ${config.description || "No role defined yet."}
114
+
115
+ ## Key Knowledge
116
+ - No notes yet.
117
+
118
+ ## Active Context
119
+ - First startup.
120
+ `;
51
121
  await writeFile(memoryMdPath, initialMemoryMd);
52
122
  }
53
-
54
123
  await mkdir(path.join(agentDataDir, "notes"), { recursive: true });
55
-
56
- // Build prompt
57
124
  const isResume = !!config.sessionId;
58
- let prompt: string;
125
+ let prompt;
59
126
  if (isResume && wakeMessage) {
60
- // Format the wake message inline so the agent sees it immediately
61
- const channelLabel = wakeMessage.channel_type === "dm"
62
- ? `DM:@${wakeMessage.channel_name}` : `#${wakeMessage.channel_name}`;
127
+ const channelLabel = wakeMessage.channel_type === "dm" ? `DM:@${wakeMessage.channel_name}` : `#${wakeMessage.channel_name}`;
63
128
  const senderPrefix = wakeMessage.sender_type === "agent" ? "(agent) " : "";
64
129
  const formatted = `[${channelLabel}] ${senderPrefix}@${wakeMessage.sender_name}: ${wakeMessage.content}`;
65
- prompt = `New message received:\n\n${formatted}`;
130
+ prompt = `New message received:
66
131
 
67
- // Add unread summary for other channels (exclude the wake message's channel)
132
+ ${formatted}`;
68
133
  if (unreadSummary && Object.keys(unreadSummary).length > 0) {
69
134
  const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
70
135
  if (otherUnread.length > 0) {
71
- prompt += `\n\nYou also have unread messages in other channels:`;
136
+ prompt += `
137
+
138
+ You also have unread messages in other channels:`;
72
139
  for (const [ch, count] of otherUnread) {
73
- prompt += `\n- ${ch}: ${count} unread`;
140
+ prompt += `
141
+ - ${ch}: ${count} unread`;
74
142
  }
75
- prompt += `\n\nUse read_history to catch up, or respond to the message above first.`;
143
+ prompt += `
144
+
145
+ Use read_history to catch up, or respond to the message above first.`;
76
146
  }
77
147
  }
148
+ prompt += `
78
149
 
79
- prompt += `\n\nRespond as appropriate reply using send_message, or take action as needed. Then call receive_message(block=true) to keep listening.\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
150
+ Respond as appropriate \u2014 reply using send_message, or take action as needed. Then call receive_message(block=true) to keep listening.
151
+
152
+ Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
80
153
  } else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
81
- // Resuming with unread messages (e.g. agent was stopped, messages arrived, then restarted)
82
154
  prompt = `You have unread messages from while you were offline:`;
83
155
  for (const [ch, count] of Object.entries(unreadSummary)) {
84
- prompt += `\n- ${ch}: ${count} unread`;
156
+ prompt += `
157
+ - ${ch}: ${count} unread`;
85
158
  }
86
- prompt += `\n\nUse read_history to catch up on important channels, then call receive_message(block=true) to listen for new messages.\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
159
+ prompt += `
160
+
161
+ Use read_history to catch up on important channels, then call receive_message(block=true) to listen for new messages.
162
+
163
+ Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
87
164
  } else if (isResume) {
88
165
  prompt = "No new messages while you were away. Call mcp__chat__receive_message(block=true) to listen for new messages.\n\nNote: While you are busy, you may receive [System notification: ...] messages about new messages. Finish your current step, then call receive_message to check.";
89
166
  } else {
90
167
  prompt = this.buildSystemPrompt(config, agentId);
91
168
  }
92
-
93
- // Build MCP config — point to remote server
94
- // Use daemon's own API key for chat-bridge authentication with /internal/* routes
95
169
  const mcpArgs = [
96
- "tsx",
97
170
  this.chatBridgePath,
98
- "--agent-id", agentId,
99
- "--server-url", config.serverUrl,
100
- "--auth-token", config.authToken || this.daemonApiKey,
171
+ "--agent-id",
172
+ agentId,
173
+ "--server-url",
174
+ config.serverUrl,
175
+ "--auth-token",
176
+ config.authToken || this.daemonApiKey
101
177
  ];
102
-
178
+ const isTsSource = this.chatBridgePath.endsWith(".ts");
103
179
  const mcpConfig = JSON.stringify({
104
180
  mcpServers: {
105
181
  chat: {
106
- command: "npx",
107
- args: mcpArgs,
108
- },
109
- },
182
+ command: isTsSource ? "npx" : "node",
183
+ args: isTsSource ? ["tsx", ...mcpArgs] : mcpArgs
184
+ }
185
+ }
110
186
  });
111
-
112
- const args = [
187
+ const args2 = [
113
188
  "--allow-dangerously-skip-permissions",
114
189
  "--dangerously-skip-permissions",
115
190
  "--verbose",
116
- "--output-format", "stream-json",
117
- "--input-format", "stream-json",
118
- "--mcp-config", mcpConfig,
119
- "--model", config.model || "sonnet",
191
+ "--output-format",
192
+ "stream-json",
193
+ "--input-format",
194
+ "stream-json",
195
+ "--mcp-config",
196
+ mcpConfig,
197
+ "--model",
198
+ config.model || "sonnet"
120
199
  ];
121
-
122
200
  if (config.sessionId) {
123
- args.push("--resume", config.sessionId);
201
+ args2.push("--resume", config.sessionId);
124
202
  }
125
-
126
203
  const runtime = config.runtime || "claude";
127
204
  const spawnEnv = { ...process.env, FORCE_COLOR: "0" };
128
- delete (spawnEnv as any).CLAUDECODE;
129
- const proc = spawn(runtime, args, {
205
+ delete spawnEnv.CLAUDECODE;
206
+ const proc = spawn(runtime, args2, {
130
207
  cwd: agentDataDir,
131
208
  stdio: ["pipe", "pipe", "pipe"],
132
- env: spawnEnv,
209
+ env: spawnEnv
133
210
  });
134
-
135
- const agentProcess: AgentProcess = {
211
+ const agentProcess = {
136
212
  process: proc,
137
213
  inbox: [],
138
214
  pendingReceive: null,
@@ -140,16 +216,12 @@ export class AgentProcessManager {
140
216
  sessionId: config.sessionId || null,
141
217
  isInReceiveMessage: false,
142
218
  notificationTimer: null,
143
- pendingNotificationCount: 0,
219
+ pendingNotificationCount: 0
144
220
  };
145
221
  this.agents.set(agentId, agentProcess);
146
-
147
- // Send initial prompt via stdin (stream-json format)
148
222
  this.writeStdinMessage(agentProcess, prompt);
149
-
150
- // Parse stream-json output
151
223
  let buffer = "";
152
- proc.stdout?.on("data", (chunk: Buffer) => {
224
+ proc.stdout?.on("data", (chunk) => {
153
225
  buffer += chunk.toString();
154
226
  const lines = buffer.split("\n");
155
227
  buffer = lines.pop() || "";
@@ -158,19 +230,18 @@ export class AgentProcessManager {
158
230
  try {
159
231
  const event = JSON.parse(line);
160
232
  this.handleStreamEvent(agentId, event);
161
- } catch { /* ignore */ }
233
+ } catch {
234
+ }
162
235
  }
163
236
  });
164
-
165
- proc.stderr?.on("data", (chunk: Buffer) => {
237
+ proc.stderr?.on("data", (chunk) => {
166
238
  const text = chunk.toString().trim();
167
239
  if (text) console.error(`[Agent ${agentId} stderr]: ${text}`);
168
240
  });
169
-
170
241
  proc.on("exit", (code) => {
171
242
  console.log(`[Agent ${agentId}] Process exited with code ${code}`);
172
243
  if (this.agents.has(agentId)) {
173
- const ap = this.agents.get(agentId)!;
244
+ const ap = this.agents.get(agentId);
174
245
  if (ap.pendingReceive) {
175
246
  clearTimeout(ap.pendingReceive.timer);
176
247
  ap.pendingReceive.resolve([]);
@@ -183,15 +254,12 @@ export class AgentProcessManager {
183
254
  this.sendToServer({ type: "agent:activity", agentId, activity: "sleeping", detail: "" });
184
255
  }
185
256
  });
186
-
187
257
  this.sendToServer({ type: "agent:status", agentId, status: "active" });
188
258
  this.sendToServer({ type: "agent:activity", agentId, activity: "working", detail: "Starting\u2026" });
189
259
  }
190
-
191
- async stopAgent(agentId: string) {
260
+ async stopAgent(agentId) {
192
261
  const ap = this.agents.get(agentId);
193
262
  if (!ap) return;
194
-
195
263
  if (ap.pendingReceive) {
196
264
  clearTimeout(ap.pendingReceive.timer);
197
265
  ap.pendingReceive.resolve([]);
@@ -199,20 +267,16 @@ export class AgentProcessManager {
199
267
  if (ap.notificationTimer) {
200
268
  clearTimeout(ap.notificationTimer);
201
269
  }
202
-
203
270
  this.agents.delete(agentId);
204
271
  ap.process.kill("SIGTERM");
205
272
  this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
206
273
  this.sendToServer({ type: "agent:activity", agentId, activity: "offline", detail: "" });
207
274
  }
208
-
209
275
  /** Hibernate: kill process but keep status as "sleeping" (auto-wakes on next message via --resume) */
210
- sleepAgent(agentId: string) {
276
+ sleepAgent(agentId) {
211
277
  const ap = this.agents.get(agentId);
212
278
  if (!ap) return;
213
-
214
279
  console.log(`[Agent ${agentId}] Hibernating (sleeping)`);
215
-
216
280
  if (ap.pendingReceive) {
217
281
  clearTimeout(ap.pendingReceive.timer);
218
282
  ap.pendingReceive.resolve([]);
@@ -220,17 +284,12 @@ export class AgentProcessManager {
220
284
  if (ap.notificationTimer) {
221
285
  clearTimeout(ap.notificationTimer);
222
286
  }
223
-
224
- // Remove from map BEFORE killing so the exit handler doesn't double-report
225
287
  this.agents.delete(agentId);
226
288
  ap.process.kill("SIGTERM");
227
- // Status already set to "sleeping" by the server; don't override
228
289
  }
229
-
230
- deliverMessage(agentId: string, message: AgentMessage) {
290
+ deliverMessage(agentId, message) {
231
291
  const ap = this.agents.get(agentId);
232
292
  if (!ap) return;
233
-
234
293
  if (ap.pendingReceive) {
235
294
  clearTimeout(ap.pendingReceive.timer);
236
295
  ap.pendingReceive.resolve([message]);
@@ -238,20 +297,16 @@ export class AgentProcessManager {
238
297
  } else {
239
298
  ap.inbox.push(message);
240
299
  }
241
-
242
- // Stdin notification: if agent is busy (not in receive_message), notify via stdin
243
- if (ap.isInReceiveMessage) return; // message will be picked up via MCP bridge
244
- if (!ap.sessionId) return; // agent not initialized yet
245
-
300
+ if (ap.isInReceiveMessage) return;
301
+ if (!ap.sessionId) return;
246
302
  ap.pendingNotificationCount++;
247
303
  if (!ap.notificationTimer) {
248
304
  ap.notificationTimer = setTimeout(() => {
249
305
  this.sendStdinNotification(agentId);
250
- }, 3000);
306
+ }, 3e3);
251
307
  }
252
308
  }
253
-
254
- async resetWorkspace(agentId: string) {
309
+ async resetWorkspace(agentId) {
255
310
  const agentDataDir = path.join(DATA_DIR, agentId);
256
311
  try {
257
312
  await rm(agentDataDir, { recursive: true, force: true });
@@ -260,38 +315,30 @@ export class AgentProcessManager {
260
315
  console.error(`[Agent ${agentId}] Failed to delete workspace:`, err);
261
316
  }
262
317
  }
263
-
264
318
  async stopAll() {
265
319
  const ids = [...this.agents.keys()];
266
320
  await Promise.all(ids.map((id) => this.stopAgent(id)));
267
321
  }
268
-
269
- getRunningAgentIds(): string[] {
322
+ getRunningAgentIds() {
270
323
  return [...this.agents.keys()];
271
324
  }
272
-
273
325
  // Machine-level workspace scanning
274
-
275
- async scanAllWorkspaces(): Promise<WorkspaceDirectoryInfo[]> {
276
- const results: WorkspaceDirectoryInfo[] = [];
326
+ async scanAllWorkspaces() {
327
+ const results = [];
277
328
  let entries;
278
329
  try {
279
330
  entries = await readdir(DATA_DIR, { withFileTypes: true });
280
331
  } catch {
281
- // DATA_DIR doesn't exist yet
282
332
  return [];
283
333
  }
284
-
285
334
  for (const entry of entries) {
286
335
  if (!entry.isDirectory()) continue;
287
-
288
336
  const dirPath = path.join(DATA_DIR, entry.name);
289
337
  try {
290
338
  const dirContents = await readdir(dirPath, { withFileTypes: true });
291
339
  let totalSize = 0;
292
- let latestMtime = new Date(0);
340
+ let latestMtime = /* @__PURE__ */ new Date(0);
293
341
  let fileCount = 0;
294
-
295
342
  for (const item of dirContents) {
296
343
  const itemPath = path.join(dirPath, item.name);
297
344
  try {
@@ -303,23 +350,23 @@ export class AgentProcessManager {
303
350
  if (info.mtime > latestMtime) {
304
351
  latestMtime = info.mtime;
305
352
  }
306
- } catch { continue; }
353
+ } catch {
354
+ continue;
355
+ }
307
356
  }
308
-
309
357
  results.push({
310
358
  directoryName: entry.name,
311
359
  totalSizeBytes: totalSize,
312
360
  lastModified: latestMtime.toISOString(),
313
- fileCount,
361
+ fileCount
314
362
  });
315
- } catch { continue; }
363
+ } catch {
364
+ continue;
365
+ }
316
366
  }
317
-
318
367
  return results;
319
368
  }
320
-
321
- async deleteWorkspaceDirectory(directoryName: string): Promise<boolean> {
322
- // Validate: no path traversal
369
+ async deleteWorkspaceDirectory(directoryName) {
323
370
  if (directoryName.includes("/") || directoryName.includes("..") || directoryName.includes("\\")) {
324
371
  return false;
325
372
  }
@@ -333,10 +380,8 @@ export class AgentProcessManager {
333
380
  return false;
334
381
  }
335
382
  }
336
-
337
383
  // Workspace file browsing
338
-
339
- async getFileTree(agentId: string): Promise<FileNode[]> {
384
+ async getFileTree(agentId) {
340
385
  const agentDir = path.join(DATA_DIR, agentId);
341
386
  try {
342
387
  await stat(agentDir);
@@ -346,8 +391,7 @@ export class AgentProcessManager {
346
391
  const count = { n: 0 };
347
392
  return this.buildFileTree(agentDir, agentDir, count);
348
393
  }
349
-
350
- async readFile(agentId: string, filePath: string): Promise<{ content: string | null; binary: boolean }> {
394
+ async readFile(agentId, filePath) {
351
395
  const agentDir = path.join(DATA_DIR, agentId);
352
396
  const resolved = path.resolve(agentDir, filePath);
353
397
  if (!resolved.startsWith(agentDir + path.sep) && resolved !== agentDir) {
@@ -355,70 +399,84 @@ export class AgentProcessManager {
355
399
  }
356
400
  const info = await stat(resolved);
357
401
  if (info.isDirectory()) throw new Error("Cannot read a directory");
358
-
359
- const TEXT_EXTENSIONS = new Set([
360
- ".md", ".txt", ".json", ".js", ".ts", ".jsx", ".tsx", ".yaml", ".yml",
361
- ".toml", ".log", ".csv", ".xml", ".html", ".css", ".sh", ".py",
402
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
403
+ ".md",
404
+ ".txt",
405
+ ".json",
406
+ ".js",
407
+ ".ts",
408
+ ".jsx",
409
+ ".tsx",
410
+ ".yaml",
411
+ ".yml",
412
+ ".toml",
413
+ ".log",
414
+ ".csv",
415
+ ".xml",
416
+ ".html",
417
+ ".css",
418
+ ".sh",
419
+ ".py"
362
420
  ]);
363
421
  const ext = path.extname(resolved).toLowerCase();
364
422
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
365
423
  return { content: null, binary: true };
366
424
  }
367
- if (info.size > 1_048_576) throw new Error("File too large");
368
-
425
+ if (info.size > 1048576) throw new Error("File too large");
369
426
  const content = await readFile(resolved, "utf-8");
370
427
  return { content, binary: false };
371
428
  }
372
-
373
429
  // Private methods
374
-
375
430
  /** Write a stream-json user message to the agent's stdin */
376
- private writeStdinMessage(ap: AgentProcess, text: string) {
431
+ writeStdinMessage(ap, text) {
377
432
  const stdinMsg = JSON.stringify({
378
433
  type: "user",
379
434
  message: {
380
435
  role: "user",
381
- content: [{ type: "text", text }],
436
+ content: [{ type: "text", text }]
382
437
  },
383
- ...(ap.sessionId ? { session_id: ap.sessionId } : {}),
438
+ ...ap.sessionId ? { session_id: ap.sessionId } : {}
384
439
  });
385
440
  ap.process.stdin?.write(stdinMsg + "\n");
386
441
  }
387
-
388
442
  /** Send a batched notification to the agent via stdin about pending messages */
389
- private sendStdinNotification(agentId: string) {
443
+ sendStdinNotification(agentId) {
390
444
  const ap = this.agents.get(agentId);
391
445
  if (!ap) return;
392
-
393
446
  const count = ap.pendingNotificationCount;
394
447
  ap.pendingNotificationCount = 0;
395
448
  ap.notificationTimer = null;
396
-
397
449
  if (count === 0) return;
398
- if (ap.isInReceiveMessage) return; // agent entered receive during batch window
450
+ if (ap.isInReceiveMessage) return;
399
451
  if (!ap.sessionId) return;
400
-
401
452
  const notification = `[System notification: You have ${count} new message${count > 1 ? "s" : ""} waiting. Call receive_message to read ${count > 1 ? "them" : "it"} when you're ready.]`;
402
453
  console.log(`[Agent ${agentId}] Sending stdin notification: ${count} message(s)`);
403
454
  this.writeStdinMessage(ap, notification);
404
455
  }
405
-
406
- private async buildFileTree(dir: string, rootDir: string, count: { n: number }): Promise<FileNode[]> {
456
+ async buildFileTree(dir, rootDir, count) {
407
457
  let entries;
408
- try { entries = await readdir(dir, { withFileTypes: true }); } catch { return []; }
458
+ try {
459
+ entries = await readdir(dir, { withFileTypes: true });
460
+ } catch {
461
+ return [];
462
+ }
409
463
  entries.sort((a, b) => {
410
464
  if (a.isDirectory() && !b.isDirectory()) return -1;
411
465
  if (!a.isDirectory() && b.isDirectory()) return 1;
412
466
  return a.name.localeCompare(b.name);
413
467
  });
414
- const nodes: FileNode[] = [];
468
+ const nodes = [];
415
469
  for (const entry of entries) {
416
470
  if (count.n >= 500) break;
417
471
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
418
472
  const fullPath = path.join(dir, entry.name);
419
473
  const relativePath = path.relative(rootDir, fullPath);
420
474
  let info;
421
- try { info = await stat(fullPath); } catch { continue; }
475
+ try {
476
+ info = await stat(fullPath);
477
+ } catch {
478
+ continue;
479
+ }
422
480
  count.n++;
423
481
  if (entry.isDirectory()) {
424
482
  const children = await this.buildFileTree(fullPath, rootDir, count);
@@ -429,12 +487,10 @@ export class AgentProcessManager {
429
487
  }
430
488
  return nodes;
431
489
  }
432
-
433
- private handleStreamEvent(agentId: string, event: any) {
434
- const trajectory: TrajectoryEntry[] = [];
490
+ handleStreamEvent(agentId, event) {
491
+ const trajectory = [];
435
492
  let activity = "";
436
493
  let detail = "";
437
-
438
494
  switch (event.type) {
439
495
  case "system":
440
496
  if (event.subtype === "init" && event.session_id) {
@@ -443,22 +499,15 @@ export class AgentProcessManager {
443
499
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.session_id });
444
500
  }
445
501
  break;
446
-
447
502
  case "assistant": {
448
- // Claude CLI stream-json emits one "assistant" per turn with full message.content array.
449
- // Each content block is complete (thinking, text, or tool_use).
450
503
  const content = event.message?.content;
451
504
  if (Array.isArray(content)) {
452
505
  for (const block of content) {
453
506
  if (block.type === "thinking" && block.thinking) {
454
- const text = block.thinking.length > MAX_TRAJECTORY_TEXT
455
- ? block.thinking.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026"
456
- : block.thinking;
507
+ const text = block.thinking.length > MAX_TRAJECTORY_TEXT ? block.thinking.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : block.thinking;
457
508
  trajectory.push({ kind: "thinking", text });
458
509
  } else if (block.type === "text" && block.text) {
459
- const text = block.text.length > MAX_TRAJECTORY_TEXT
460
- ? block.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026"
461
- : block.text;
510
+ const text = block.text.length > MAX_TRAJECTORY_TEXT ? block.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : block.text;
462
511
  trajectory.push({ kind: "text", text });
463
512
  } else if (block.type === "tool_use") {
464
513
  const toolName = block.name || "unknown_tool";
@@ -467,10 +516,8 @@ export class AgentProcessManager {
467
516
  }
468
517
  }
469
518
  }
470
-
471
- // High-level activity status + track isInReceiveMessage
472
519
  const ap = this.agents.get(agentId);
473
- const toolUses = Array.isArray(content) ? content.filter((c: any) => c.type === "tool_use") : [];
520
+ const toolUses = Array.isArray(content) ? content.filter((c) => c.type === "tool_use") : [];
474
521
  if (toolUses.length > 0) {
475
522
  const lastTool = toolUses[toolUses.length - 1];
476
523
  const toolName = lastTool.name || "tool";
@@ -478,7 +525,6 @@ export class AgentProcessManager {
478
525
  activity = "online";
479
526
  if (ap) {
480
527
  ap.isInReceiveMessage = true;
481
- // Agent is now waiting for messages via MCP bridge — clear notification state
482
528
  ap.pendingNotificationCount = 0;
483
529
  if (ap.notificationTimer) {
484
530
  clearTimeout(ap.notificationTimer);
@@ -500,7 +546,6 @@ export class AgentProcessManager {
500
546
  }
501
547
  break;
502
548
  }
503
-
504
549
  case "result": {
505
550
  activity = "online";
506
551
  const apResult = this.agents.get(agentId);
@@ -514,21 +559,16 @@ export class AgentProcessManager {
514
559
  break;
515
560
  }
516
561
  }
517
-
518
- // Send simple activity status (existing flow)
519
562
  if (activity) {
520
563
  this.sendToServer({ type: "agent:activity", agentId, activity, detail });
521
- trajectory.push({ kind: "status", activity: activity as any, detail });
564
+ trajectory.push({ kind: "status", activity, detail });
522
565
  }
523
-
524
- // Send trajectory entries (new flow)
525
566
  if (trajectory.length > 0) {
526
567
  this.sendToServer({ type: "agent:trajectory", agentId, entries: trajectory });
527
568
  }
528
569
  }
529
-
530
570
  /** Map raw tool names to user-friendly labels (all end with "…") */
531
- private toolDisplayName(toolName: string): string {
571
+ toolDisplayName(toolName) {
532
572
  if (toolName.startsWith("mcp__chat__")) return "";
533
573
  if (toolName === "Read" || toolName === "read_file") return "Reading file\u2026";
534
574
  if (toolName === "Write" || toolName === "write_file") return "Writing file\u2026";
@@ -541,9 +581,8 @@ export class AgentProcessManager {
541
581
  if (toolName === "TodoWrite") return "Updating tasks\u2026";
542
582
  return `Using ${toolName.length > 20 ? toolName.slice(0, 20) + "\u2026" : toolName}\u2026`;
543
583
  }
544
-
545
584
  /** Extract a short human-readable summary from tool input */
546
- private summarizeToolInput(toolName: string, input: any): string {
585
+ summarizeToolInput(toolName, input) {
547
586
  if (!input || typeof input !== "object") return "";
548
587
  try {
549
588
  if (toolName === "Read" || toolName === "read_file") return input.file_path || input.path || "";
@@ -562,24 +601,25 @@ export class AgentProcessManager {
562
601
  }
563
602
  if (toolName === "mcp__chat__read_history") return input.channel || "";
564
603
  return "";
565
- } catch { return ""; }
604
+ } catch {
605
+ return "";
606
+ }
566
607
  }
567
-
568
- private buildSystemPrompt(config: AgentConfig, agentId: string): string {
569
- let systemPrompt = `You are "${config.displayName || config.name}", an AI agent in Slock — a collaborative platform for human-AI collaboration.
608
+ buildSystemPrompt(config, agentId) {
609
+ let systemPrompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
570
610
 
571
611
  ## Who you are
572
612
 
573
- You are a **long-running, persistent agent**. You are NOT a one-shot assistant you live across many sessions. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Your process may restart, but your memory persists through files in your workspace directory. Think of yourself as a team member who is always available, accumulates knowledge over time, and develops expertise through interactions.
613
+ You are a **long-running, persistent agent**. You are NOT a one-shot assistant \u2014 you live across many sessions. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Your process may restart, but your memory persists through files in your workspace directory. Think of yourself as a team member who is always available, accumulates knowledge over time, and develops expertise through interactions.
574
614
 
575
- ## Communication MCP tools ONLY
615
+ ## Communication \u2014 MCP tools ONLY
576
616
 
577
617
  You have MCP tools from the "chat" server. Use ONLY these for communication:
578
618
 
579
- 1. **mcp__chat__receive_message** Call with block=true to wait for messages. This is your main loop.
580
- 2. **mcp__chat__send_message** Send a message to a channel or DM.
581
- 3. **mcp__chat__list_server** List all channels, agents, and humans in this server.
582
- 4. **mcp__chat__read_history** Read past messages from a channel or DM.
619
+ 1. **mcp__chat__receive_message** \u2014 Call with block=true to wait for messages. This is your main loop.
620
+ 2. **mcp__chat__send_message** \u2014 Send a message to a channel or DM.
621
+ 3. **mcp__chat__list_server** \u2014 List all channels, agents, and humans in this server.
622
+ 4. **mcp__chat__read_history** \u2014 Read past messages from a channel or DM.
583
623
 
584
624
  CRITICAL RULES:
585
625
  - Do NOT output text directly. ALL communication goes through mcp__chat__send_message.
@@ -588,7 +628,7 @@ CRITICAL RULES:
588
628
 
589
629
  ## Startup sequence
590
630
 
591
- 1. **Read MEMORY.md** (in your cwd). This is your memory index it tells you what you know and where to find it.
631
+ 1. **Read MEMORY.md** (in your cwd). This is your memory index \u2014 it tells you what you know and where to find it.
592
632
  2. Follow the instructions in MEMORY.md to read any other memory files you need (e.g. channel summaries, role definitions, user preferences).
593
633
  3. Call mcp__chat__receive_message(block=true) to start listening.
594
634
  4. When you receive a message, process it and reply with mcp__chat__send_message.
@@ -606,8 +646,8 @@ The \`[...]\` prefix identifies where the message came from. Reuse it as the \`c
606
646
  ### Sending messages
607
647
 
608
648
  - **Reply to a channel**: \`send_message(channel="#channel-name", content="...")\`
609
- - **Reply to a DM**: \`send_message(channel="DM:@peer-name", content="...")\` reuse the channel value from the received message
610
- - **Start a NEW DM**: \`send_message(dm_to="peer-name", content="...")\` use the human's name from list_server (no @ prefix)
649
+ - **Reply to a DM**: \`send_message(channel="DM:@peer-name", content="...")\` \u2014 reuse the channel value from the received message
650
+ - **Start a NEW DM**: \`send_message(dm_to="peer-name", content="...")\` \u2014 use the human's name from list_server (no @ prefix)
611
651
 
612
652
  **IMPORTANT**: To reply to any message (channel or DM), always use \`channel\` with the exact identifier from the received message. Only use \`dm_to\` when you want to start a brand new DM that doesn't exist yet.
613
653
 
@@ -622,24 +662,24 @@ Call \`list_server\` to see all your channels, other agents, and humans in this
622
662
  ## @Mentions
623
663
 
624
664
  In channel group chats, you can @mention people by their unique name (e.g. "@alice" or "@bob").
625
- - Every human and agent has a unique \`name\` this is their stable identifier for @mentions.
626
- - @mentions do not notify people outside the channel channels are the isolation boundary.
665
+ - Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
666
+ - @mentions do not notify people outside the channel \u2014 channels are the isolation boundary.
627
667
 
628
668
  ## Communication style
629
669
 
630
670
  Keep the user informed. They cannot see your internal reasoning, so:
631
671
  - When you receive a task, acknowledge it and briefly outline your plan before starting.
632
- - For multi-step work, send short progress updates (e.g. "Working on step 2/3").
672
+ - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
633
673
  - When done, summarize the result.
634
- - Keep updates concise one or two sentences. Don't flood the chat.
674
+ - Keep updates concise \u2014 one or two sentences. Don't flood the chat.
635
675
 
636
676
  ## Workspace & Memory
637
677
 
638
678
  Your working directory (cwd) is your **persistent workspace**. Everything you write here survives across sessions.
639
679
 
640
- ### MEMORY.md Your Memory Index (CRITICAL)
680
+ ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
641
681
 
642
- \`MEMORY.md\` is the **entry point** to all your knowledge. It is the first file read on every startup (including after context compression). Structure it as an index that points to everything you know. This file is called \`MEMORY.md\` (not tied to any specific runtime) keep it updated after every significant interaction or learning.
682
+ \`MEMORY.md\` is the **entry point** to all your knowledge. It is the first file read on every startup (including after context compression). Structure it as an index that points to everything you know. This file is called \`MEMORY.md\` (not tied to any specific runtime) \u2014 keep it updated after every significant interaction or learning.
643
683
 
644
684
  \`\`\`markdown
645
685
  # <Your Name>
@@ -662,24 +702,24 @@ Your working directory (cwd) is your **persistent workspace**. Everything you wr
662
702
 
663
703
  **Actively observe and record** the following kinds of knowledge as you encounter them in conversations:
664
704
 
665
- 1. **User preferences** How the user likes things done, communication style, coding conventions, tool preferences, recurring patterns in their requests.
666
- 2. **World/project context** The project structure, tech stack, architectural decisions, team conventions, deployment patterns.
667
- 3. **Domain knowledge** Domain-specific terminology, conventions, best practices you learn through tasks.
668
- 4. **Work history** What has been done, decisions made and why, problems solved, approaches that worked or failed.
669
- 5. **Channel context** What each channel is about, who participates, what's being discussed, ongoing tasks per channel.
670
- 6. **Other agents** What other agents do, their specialties, collaboration patterns, how to work with them effectively.
705
+ 1. **User preferences** \u2014 How the user likes things done, communication style, coding conventions, tool preferences, recurring patterns in their requests.
706
+ 2. **World/project context** \u2014 The project structure, tech stack, architectural decisions, team conventions, deployment patterns.
707
+ 3. **Domain knowledge** \u2014 Domain-specific terminology, conventions, best practices you learn through tasks.
708
+ 4. **Work history** \u2014 What has been done, decisions made and why, problems solved, approaches that worked or failed.
709
+ 5. **Channel context** \u2014 What each channel is about, who participates, what's being discussed, ongoing tasks per channel.
710
+ 6. **Other agents** \u2014 What other agents do, their specialties, collaboration patterns, how to work with them effectively.
671
711
 
672
712
  ### How to organize memory
673
713
 
674
714
  - **MEMORY.md** is always the index. Keep it concise but comprehensive as a table of contents.
675
715
  - Create a \`notes/\` directory for detailed knowledge files. Use descriptive names:
676
- - \`notes/user-preferences.md\` User's preferences and conventions
677
- - \`notes/channels.md\` Summary of each channel and its purpose
678
- - \`notes/work-log.md\` Important decisions and completed work
679
- - \`notes/<domain>.md\` Domain-specific knowledge
716
+ - \`notes/user-preferences.md\` \u2014 User's preferences and conventions
717
+ - \`notes/channels.md\` \u2014 Summary of each channel and its purpose
718
+ - \`notes/work-log.md\` \u2014 Important decisions and completed work
719
+ - \`notes/<domain>.md\` \u2014 Domain-specific knowledge
680
720
  - You can also create any other files or directories for your work (scripts, notes, data, etc.)
681
- - **Update notes proactively** Don't wait to be asked. When you learn something important, write it down.
682
- - **Keep MEMORY.md current** After updating notes, update the index in MEMORY.md if new files were added.
721
+ - **Update notes proactively** \u2014 Don't wait to be asked. When you learn something important, write it down.
722
+ - **Keep MEMORY.md current** \u2014 After updating notes, update the index in MEMORY.md if new files were added.
683
723
 
684
724
  ### Compaction safety (CRITICAL)
685
725
 
@@ -692,7 +732,7 @@ Your context will be periodically compressed to stay within limits. When this ha
692
732
 
693
733
  ## Capabilities
694
734
 
695
- You can work with any files or tools on this computer you are not confined to any directory.
735
+ You can work with any files or tools on this computer \u2014 you are not confined to any directory.
696
736
  You may develop a specialized role over time through your interactions. Embrace it.
697
737
 
698
738
  ## Message Notifications
@@ -704,13 +744,151 @@ While you are busy (executing tools, thinking, etc.), new messages may arrive. W
704
744
  How to handle these:
705
745
  - **Do NOT interrupt your current work.** Finish what you're doing first.
706
746
  - After completing your current step, call \`mcp__chat__receive_message(block=false)\` to check for messages.
707
- - Do not ignore notifications for too long acknowledge new messages in a timely manner.
747
+ - Do not ignore notifications for too long \u2014 acknowledge new messages in a timely manner.
708
748
  - These notifications are batched (you won't get one per message), so the count tells you how many are waiting.`;
709
-
710
749
  if (config.description) {
711
- systemPrompt += `\n\n## Initial role\n${config.description}. This may evolve.`;
712
- }
750
+ systemPrompt += `
713
751
 
752
+ ## Initial role
753
+ ${config.description}. This may evolve.`;
754
+ }
714
755
  return systemPrompt;
715
756
  }
757
+ };
758
+
759
+ // ../shared/src/index.ts
760
+ var RUNTIMES = [
761
+ { id: "claude", displayName: "Claude Code", binary: "claude", supported: true },
762
+ { id: "codex", displayName: "Codex CLI", binary: "codex", supported: false },
763
+ { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: false },
764
+ { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: false }
765
+ ];
766
+
767
+ // src/index.ts
768
+ function detectRuntimes() {
769
+ const detected = [];
770
+ for (const rt of RUNTIMES) {
771
+ try {
772
+ execSync(`which ${rt.binary}`, { stdio: "pipe" });
773
+ detected.push(rt.id);
774
+ } catch {
775
+ }
776
+ }
777
+ return detected;
716
778
  }
779
+ var args = process.argv.slice(2);
780
+ var serverUrl = "";
781
+ var apiKey = "";
782
+ for (let i = 0; i < args.length; i++) {
783
+ if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
784
+ if (args[i] === "--api-key" && args[i + 1]) apiKey = args[++i];
785
+ }
786
+ if (!serverUrl || !apiKey) {
787
+ console.error("Usage: slock-daemon --server-url <url> --api-key <key>");
788
+ process.exit(1);
789
+ }
790
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
791
+ var chatBridgePath = path2.resolve(__dirname, "chat-bridge.js");
792
+ try {
793
+ accessSync(chatBridgePath);
794
+ } catch {
795
+ chatBridgePath = path2.resolve(__dirname, "chat-bridge.ts");
796
+ }
797
+ var connection;
798
+ var agentManager = new AgentProcessManager(chatBridgePath, (msg) => {
799
+ connection.send(msg);
800
+ }, apiKey);
801
+ connection = new DaemonConnection({
802
+ serverUrl,
803
+ apiKey,
804
+ onMessage: (msg) => {
805
+ console.log(`[Daemon] Received: ${msg.type}`, msg.type === "ping" ? "" : JSON.stringify(msg).slice(0, 200));
806
+ switch (msg.type) {
807
+ case "agent:start":
808
+ console.log(`[Daemon] Starting agent ${msg.agentId} (model: ${msg.config.model}, session: ${msg.config.sessionId || "new"}${msg.wakeMessage ? ", with wake message" : ""})`);
809
+ agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary);
810
+ break;
811
+ case "agent:stop":
812
+ console.log(`[Daemon] Stopping agent ${msg.agentId}`);
813
+ agentManager.stopAgent(msg.agentId);
814
+ break;
815
+ case "agent:sleep":
816
+ console.log(`[Daemon] Sleeping agent ${msg.agentId}`);
817
+ agentManager.sleepAgent(msg.agentId);
818
+ break;
819
+ case "agent:reset-workspace":
820
+ console.log(`[Daemon] Resetting workspace for agent ${msg.agentId}`);
821
+ agentManager.resetWorkspace(msg.agentId);
822
+ break;
823
+ case "agent:deliver":
824
+ console.log(`[Daemon] Delivering message to ${msg.agentId}: ${msg.message.content.slice(0, 80)}`);
825
+ agentManager.deliverMessage(msg.agentId, msg.message);
826
+ connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
827
+ break;
828
+ case "agent:workspace:list":
829
+ agentManager.getFileTree(msg.agentId).then((files) => {
830
+ connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files });
831
+ });
832
+ break;
833
+ case "agent:workspace:read":
834
+ agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
835
+ connection.send({
836
+ type: "agent:workspace:file_content",
837
+ agentId: msg.agentId,
838
+ requestId: msg.requestId,
839
+ content,
840
+ binary
841
+ });
842
+ }).catch(() => {
843
+ connection.send({
844
+ type: "agent:workspace:file_content",
845
+ agentId: msg.agentId,
846
+ requestId: msg.requestId,
847
+ content: null,
848
+ binary: false
849
+ });
850
+ });
851
+ break;
852
+ case "machine:workspace:scan":
853
+ console.log("[Daemon] Scanning all workspace directories");
854
+ agentManager.scanAllWorkspaces().then((directories) => {
855
+ connection.send({ type: "machine:workspace:scan_result", directories });
856
+ });
857
+ break;
858
+ case "machine:workspace:delete":
859
+ console.log(`[Daemon] Deleting workspace directory: ${msg.directoryName}`);
860
+ agentManager.deleteWorkspaceDirectory(msg.directoryName).then((success) => {
861
+ connection.send({ type: "machine:workspace:delete_result", directoryName: msg.directoryName, success });
862
+ });
863
+ break;
864
+ case "ping":
865
+ connection.send({ type: "pong" });
866
+ break;
867
+ }
868
+ },
869
+ onConnect: () => {
870
+ const runtimes = detectRuntimes();
871
+ console.log(`[Daemon] Detected runtimes: ${runtimes.join(", ") || "none"}`);
872
+ connection.send({
873
+ type: "ready",
874
+ capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
875
+ runtimes,
876
+ runningAgents: agentManager.getRunningAgentIds(),
877
+ hostname: os2.hostname(),
878
+ os: `${os2.platform()} ${os2.arch()}`
879
+ });
880
+ },
881
+ onDisconnect: () => {
882
+ console.log("[Daemon] Lost connection \u2014 agents continue running locally");
883
+ }
884
+ });
885
+ console.log("[Slock Daemon] Starting...");
886
+ connection.connect();
887
+ var shutdown = async () => {
888
+ console.log("[Slock Daemon] Shutting down...");
889
+ await agentManager.stopAll();
890
+ connection.disconnect();
891
+ process.exit(0);
892
+ };
893
+ process.on("SIGTERM", shutdown);
894
+ process.on("SIGINT", shutdown);