@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.
- package/{src/chat-bridge.ts → dist/chat-bridge.js} +81 -126
- package/{src/agentProcessManager.ts → dist/index.js} +403 -225
- package/package.json +8 -8
- package/src/connection.ts +0 -100
- package/src/index.ts +0 -154
|
@@ -1,138 +1,214 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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}
|
|
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
|
|
125
|
+
let prompt;
|
|
59
126
|
if (isResume && wakeMessage) {
|
|
60
|
-
|
|
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
|
|
130
|
+
prompt = `New message received:
|
|
66
131
|
|
|
67
|
-
|
|
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 +=
|
|
136
|
+
prompt += `
|
|
137
|
+
|
|
138
|
+
You also have unread messages in other channels:`;
|
|
72
139
|
for (const [ch, count] of otherUnread) {
|
|
73
|
-
prompt +=
|
|
140
|
+
prompt += `
|
|
141
|
+
- ${ch}: ${count} unread`;
|
|
74
142
|
}
|
|
75
|
-
prompt +=
|
|
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
|
-
|
|
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 +=
|
|
156
|
+
prompt += `
|
|
157
|
+
- ${ch}: ${count} unread`;
|
|
85
158
|
}
|
|
86
|
-
prompt +=
|
|
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",
|
|
99
|
-
|
|
100
|
-
"--
|
|
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",
|
|
117
|
-
"
|
|
118
|
-
"--
|
|
119
|
-
"
|
|
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
|
-
|
|
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
|
|
129
|
-
const proc = spawn(runtime,
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
360
|
-
".
|
|
361
|
-
".
|
|
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 >
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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;
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
604
|
+
} catch {
|
|
605
|
+
return "";
|
|
606
|
+
}
|
|
566
607
|
}
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
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
|
|
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**
|
|
580
|
-
2. **mcp__chat__send_message**
|
|
581
|
-
3. **mcp__chat__list_server**
|
|
582
|
-
4. **mcp__chat__read_history**
|
|
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
|
|
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="...")\`
|
|
610
|
-
- **Start a NEW DM**: \`send_message(dm_to="peer-name", content="...")\`
|
|
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\`
|
|
626
|
-
- @mentions do not notify people outside the channel
|
|
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
|
|
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
|
|
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)
|
|
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**
|
|
666
|
-
2. **World/project context**
|
|
667
|
-
3. **Domain knowledge**
|
|
668
|
-
4. **Work history**
|
|
669
|
-
5. **Channel context**
|
|
670
|
-
6. **Other agents**
|
|
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\`
|
|
677
|
-
- \`notes/channels.md\`
|
|
678
|
-
- \`notes/work-log.md\`
|
|
679
|
-
- \`notes/<domain>.md\`
|
|
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**
|
|
682
|
-
- **Keep MEMORY.md current**
|
|
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
|
|
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
|
|
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 +=
|
|
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);
|