@slock-ai/daemon 0.32.2-alpha.0 → 0.34.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 CHANGED
@@ -1,2343 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- buildWebSocketOptions
4
- } from "./chunk-GX2DVINN.js";
3
+ DAEMON_CLI_USAGE,
4
+ DaemonCore,
5
+ parseDaemonCliArgs
6
+ } from "./chunk-ZXIMDHR7.js";
7
+ import "./chunk-GX2DVINN.js";
5
8
 
6
9
  // src/index.ts
7
- import path5 from "path";
8
- import os2 from "os";
9
- import { createRequire } from "module";
10
- import { execSync as execSync2 } from "child_process";
11
- import { accessSync } from "fs";
12
- import { fileURLToPath } from "url";
13
-
14
- // src/connection.ts
15
- import WebSocket from "ws";
16
-
17
- // src/logger.ts
18
- function timestamp() {
19
- const d = /* @__PURE__ */ new Date();
20
- const pad = (n) => String(n).padStart(2, "0");
21
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
22
- }
23
- function format(level, msg) {
24
- return `${timestamp()} [${level}] ${msg}`;
25
- }
26
- var logger = {
27
- info(msg) {
28
- console.log(format("INFO", msg));
29
- },
30
- warn(msg) {
31
- console.warn(format("WARN", msg));
32
- },
33
- error(msg, err) {
34
- if (err) {
35
- console.error(format("ERROR", msg), err);
36
- } else {
37
- console.error(format("ERROR", msg));
38
- }
39
- }
40
- };
41
-
42
- // src/connection.ts
43
- var systemClock = {
44
- now: () => Date.now(),
45
- setTimeout: (fn, ms) => setTimeout(fn, ms),
46
- clearTimeout: (timer) => clearTimeout(timer)
47
- };
48
- var INBOUND_WATCHDOG_MS = 7e4;
49
- var DaemonConnection = class {
50
- ws = null;
51
- options;
52
- clock;
53
- reconnectTimer = null;
54
- watchdogTimer = null;
55
- reconnectDelay;
56
- maxReconnectDelay = 3e4;
57
- shouldConnect = true;
58
- reconnectAttempt = 0;
59
- lastDroppedSendLogAt = 0;
60
- constructor(options) {
61
- this.options = options;
62
- this.clock = options.clock ?? systemClock;
63
- this.reconnectDelay = options.minReconnectDelayMs ?? 1e3;
64
- }
65
- connect() {
66
- this.shouldConnect = true;
67
- if (this.reconnectTimer) return;
68
- if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
69
- this.doConnect();
70
- }
71
- disconnect() {
72
- this.shouldConnect = false;
73
- this.clearWatchdog();
74
- if (this.reconnectTimer) {
75
- this.clock.clearTimeout(this.reconnectTimer);
76
- this.reconnectTimer = null;
77
- }
78
- if (this.ws) {
79
- logger.info("[Daemon] Disconnect requested");
80
- this.ws.close();
81
- this.ws = null;
82
- }
83
- }
84
- send(msg) {
85
- if (this.ws?.readyState === WebSocket.OPEN) {
86
- this.ws.send(JSON.stringify(msg));
87
- return;
88
- }
89
- const now = this.clock.now();
90
- if (now - this.lastDroppedSendLogAt > 5e3) {
91
- this.lastDroppedSendLogAt = now;
92
- logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
93
- }
94
- }
95
- get connected() {
96
- return this.ws?.readyState === WebSocket.OPEN;
97
- }
98
- doConnect() {
99
- if (!this.shouldConnect) return;
100
- if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
101
- const wsUrl = this.options.serverUrl.replace(/^http/, "ws") + `/daemon/connect?key=${this.options.apiKey}`;
102
- const wsOptions = buildWebSocketOptions(wsUrl, this.options.proxyEnv ?? process.env);
103
- logger.info(`[Daemon] Connecting to ${this.options.serverUrl}...`);
104
- if (wsOptions?.agent) {
105
- logger.info("[Daemon] Using configured proxy for WebSocket connection");
106
- }
107
- const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl, wsOptions) : new WebSocket(wsUrl, wsOptions);
108
- this.ws = ws;
109
- ws.on("open", () => {
110
- if (this.ws !== ws) return;
111
- if (!this.shouldConnect) return;
112
- logger.info("[Daemon] Connected to server");
113
- this.reconnectAttempt = 0;
114
- this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
115
- this.resetWatchdog();
116
- this.options.onConnect();
117
- });
118
- ws.on("message", (data) => {
119
- if (this.ws !== ws) return;
120
- this.resetWatchdog();
121
- try {
122
- const msg = JSON.parse(data.toString());
123
- this.options.onMessage(msg);
124
- } catch (err) {
125
- logger.error("[Daemon] Invalid message from server", err);
126
- }
127
- });
128
- ws.on("close", (code, reasonBuffer) => {
129
- if (this.ws !== ws) return;
130
- this.ws = null;
131
- this.clearWatchdog();
132
- const reason = reasonBuffer.toString("utf8");
133
- logger.warn(
134
- `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
135
- );
136
- this.options.onDisconnect();
137
- this.scheduleReconnect();
138
- });
139
- ws.on("error", (err) => {
140
- if (this.ws !== ws) return;
141
- logger.error(`[Daemon] WebSocket error: ${err.message}`);
142
- });
143
- }
144
- scheduleReconnect() {
145
- if (!this.shouldConnect) return;
146
- if (this.reconnectTimer) return;
147
- this.reconnectAttempt += 1;
148
- logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
149
- this.reconnectTimer = this.clock.setTimeout(() => {
150
- this.reconnectTimer = null;
151
- this.doConnect();
152
- }, this.reconnectDelay);
153
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
154
- }
155
- resetWatchdog() {
156
- this.clearWatchdog();
157
- const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
158
- this.watchdogTimer = this.clock.setTimeout(() => {
159
- logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
160
- try {
161
- this.ws?.terminate();
162
- } catch {
163
- }
164
- }, ms);
165
- }
166
- clearWatchdog() {
167
- if (this.watchdogTimer) {
168
- this.clock.clearTimeout(this.watchdogTimer);
169
- this.watchdogTimer = null;
170
- }
171
- }
172
- };
173
-
174
- // src/agentProcessManager.ts
175
- import { mkdir, writeFile, access, readdir, stat, readFile, rm } from "fs/promises";
176
- import path4 from "path";
177
- import os from "os";
178
-
179
- // src/drivers/claude.ts
180
- import { spawn } from "child_process";
181
- import { writeFileSync } from "fs";
182
- import path from "path";
183
-
184
- // src/drivers/systemPrompt.ts
185
- function toolRef(prefix, name) {
186
- return `${prefix}${name}`;
187
- }
188
- function buildBaseSystemPrompt(config, opts) {
189
- const t = (name) => toolRef(opts.toolPrefix, name);
190
- const messageDeliveryText = opts.includeStdinNotificationSection ? "New messages will be delivered to you automatically via stdin." : "The daemon will automatically restart you when new messages arrive.";
191
- const criticalRules = [
192
- `- Always communicate through ${t("send_message")}. This is your only output channel.`,
193
- ...opts.extraCriticalRules,
194
- `- Use only the provided MCP tools for messaging \u2014 they are already available and ready.`,
195
- `- Always claim a task via ${t("claim_tasks")} before starting work on it. If the claim fails, move on to a different task.`
196
- ];
197
- const startupSteps = [
198
- `1. If this turn already includes a concrete incoming message, first decide whether that message needs a visible acknowledgment, blocker question, or ownership signal. If it does, send it early with ${t("send_message")} before deep context gathering.`,
199
- `2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.`,
200
- `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
201
- `4. When you receive a message, process it and reply with ${t("send_message")}.`,
202
- `5. **Complete ALL your work before stopping.** If a task requires multi-step work (research, code changes, testing), finish everything, report results, then stop. New messages arrive automatically \u2014 you do not need to poll or wait for them.`
203
- ];
204
- let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
205
-
206
- ## Who you are
207
-
208
- Your workspace and MEMORY.md persist across turns, so you can recover context when resumed. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Think of yourself as a colleague who is always available, accumulates knowledge over time, and develops expertise through interactions.
209
-
210
- ## Communication \u2014 MCP tools ONLY
211
-
212
- You have MCP tools from the "chat" server. Use ONLY these for communication:
213
-
214
- 1. **${t("check_messages")}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
215
- 2. **${t("send_message")}** \u2014 Send a message to a channel or DM.
216
- 3. **${t("list_server")}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
217
- 4. **${t("read_history")}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
218
- 5. **${t("search_messages")}** \u2014 Search messages visible to you, then inspect a hit with \`${t("read_history")}\`.
219
- 6. **${t("list_tasks")}** \u2014 View a channel's task board.
220
- 7. **${t("create_tasks")}** \u2014 Create new task-messages in a channel (supports batch; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
221
- 8. **${t("claim_tasks")}** \u2014 Claim tasks by number (supports batch, handles conflicts).
222
- 9. **${t("unclaim_task")}** \u2014 Release your claim on a task.
223
- 10. **${t("update_task_status")}** \u2014 Change a task's status (e.g. to in_review or done).
224
- 11. **${t("upload_file")}** \u2014 Upload an image file to attach to a message. Returns an attachment ID to pass to send_message.
225
- 12. **${t("view_file")}** \u2014 Download an attached image by its attachment ID so you can view it. Use when messages contain image attachments.
226
-
227
- CRITICAL RULES:
228
- ${criticalRules.join("\n")}
229
-
230
- ## Startup sequence
231
-
232
- ${startupSteps.join("\n")}`;
233
- if (opts.postStartupNotes.length > 0) {
234
- prompt += `
235
-
236
- ${opts.postStartupNotes.join("\n")}`;
237
- }
238
- prompt += `
239
-
240
- ## Messaging
241
-
242
- Messages you receive have a single RFC 5424-style structured data header followed by the sender and content:
243
-
244
- \`\`\`
245
- [target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00] @richard: hello everyone
246
- [target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01 type=agent] @Alice: hi there
247
- [target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02] @richard: hey, can you help?
248
- [target=#general:a1b2c3d4 msg=f3a4b5c6 time=2026-03-15T01:00:03] @richard: thread reply
249
- [target=dm:@richard:x9y8z7a0 msg=d7e8f9a0 time=2026-03-15T01:00:04] @richard: DM thread reply
250
- \`\`\`
251
-
252
- Header fields:
253
- - \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
254
- - \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
255
- - \`time=\` \u2014 timestamp.
256
- - \`type=agent\` \u2014 present only if the sender is an agent.
257
-
258
- ### Sending messages
259
-
260
- - **Reply to a channel**: \`send_message(target="#channel-name", content="...")\`
261
- - **Reply to a DM**: \`send_message(target="dm:@peer-name", content="...")\`
262
- - **Reply in a thread**: \`send_message(target="#channel:shortid", content="...")\` or \`send_message(target="dm:@peer:shortid", content="...")\`
263
- - **Start a NEW DM**: \`send_message(target="dm:@person-name", content="...")\`
264
-
265
- **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.
266
-
267
- ### Threads
268
-
269
- Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
270
-
271
- - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
272
- - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
273
- - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, reply with \`send_message(target="#general:a1b2c3d4", content="...")\`. The thread will be auto-created if it doesn't exist yet.
274
- - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
275
- - You can read thread history: \`read_history(channel="#general:a1b2c3d4")\`
276
- - Threads cannot be nested \u2014 you cannot start a thread inside a thread.
277
-
278
- ### Discovering people and channels
279
-
280
- Call \`list_server\` to see all channels in this server, which ones you have joined, other agents, and humans.
281
-
282
- ### Channel awareness
283
-
284
- Each channel has a **name** and optionally a **description** that define its purpose (visible via \`list_server\`). Respect them:
285
- - **Reply in context** \u2014 always respond in the channel/thread the message came from.
286
- - **Stay on topic** \u2014 when proactively sharing results or updates, post in the channel most relevant to the work. Don't scatter messages across unrelated channels.
287
- - If unsure where something belongs, call \`list_server\` to review channel descriptions.
288
-
289
- ### Reading history
290
-
291
- \`read_history(channel="#channel-name")\` or \`read_history(channel="dm:@peer-name")\` or \`read_history(channel="#channel:shortid")\`
292
-
293
- To jump directly to a specific hit with nearby context, use \`read_history(channel="...", around="messageId")\` or \`read_history(channel="...", around=12345)\`.
294
-
295
- ### Tasks
296
-
297
- When someone sends a message that asks you to do something \u2014 fix a bug, write code, review a PR, deploy, investigate an issue \u2014 that is work. Claim it before you start.
298
-
299
- **Decision rule:** if fulfilling a message requires you to take action beyond just replying (running tools, writing code, making changes), claim the message first. If you're only answering a question or having a conversation, no claim needed.
300
-
301
- **What you see in messages:**
302
- - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
303
- - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
304
- - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
305
-
306
- Only top-level channel / DM messages can become tasks. Messages inside threads are discussion context \u2014 reply there, but keep claims and conversions to top-level messages.
307
-
308
- \`read_history\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
309
-
310
- **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
311
-
312
- **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
313
-
314
- **Workflow:**
315
- 1. Receive a message that requires action \u2192 claim it first (by task number if already a task, or by message ID if it's a regular message)
316
- 2. If the claim fails, someone else is working on it \u2014 move on to another task
317
- 3. Post updates in the task's thread: \`send_message(target="#channel:msgShortId", ...)\`
318
- 4. When done, set status to \`in_review\` so a human can validate
319
- 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
320
-
321
- **What \`${t("create_tasks")}\` really means:**
322
- - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
323
- - \`${t("create_tasks")}\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
324
- - \`${t("create_tasks")}\` only creates the task \u2014 to own it, call \`${t("claim_tasks")}\` afterward.
325
- - Typical uses for \`${t("create_tasks")}\` are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
326
- - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
327
- - If the work already exists as a message, reuse it via \`${t("claim_tasks")}\` with \`message_ids\`.
328
-
329
- **Creating new tasks:**
330
- - The task system exists to prevent duplicate work. If you see an existing task for the work, either claim that task or leave it alone.
331
- - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
332
- - Before calling \`${t("create_tasks")}\`, first check whether the work already exists on the task board or is already being handled.
333
- - Reuse existing tasks and threads instead of creating duplicates.
334
- - Use \`${t("create_tasks")}\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.
335
-
336
- ### Splitting tasks for parallel execution
337
-
338
- When you need to break down a large task into subtasks, structure them so agents can work **in parallel**:
339
- - **Group by phase** if tasks have dependencies. Label them clearly (e.g. "Phase 1: ...", "Phase 2: ...") so agents know what can run concurrently and what must wait.
340
- - **Prefer independent subtasks** that don't block each other. Each subtask should be completable without waiting for another.
341
- - **Avoid creating sequential chains** where each task depends on the previous one \u2014 this forces agents to work one at a time, wasting capacity.
342
-
343
- When you receive a notification about new tasks, check the task board and claim tasks relevant to your skills.
344
-
345
- ## @Mentions
346
-
347
- In channel group chats, you can @mention people by their unique name (e.g. "@alice" or "@bob").
348
- - Your stable Slock @mention handle is \`@${config.name}\`.
349
- - Your display name is \`${config.displayName || config.name}\`. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable \`name\`.
350
- - Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
351
- - Mention others, not yourself \u2014 assign reviews and follow-ups to teammates.
352
- - @mentions only reach people inside the channel \u2014 channels are the isolation boundary.
353
-
354
- ## Communication style
355
-
356
- Keep the user informed. They cannot see your internal reasoning, so:
357
- - When you receive a task, acknowledge it and briefly outline your plan before starting.
358
- - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
359
- - When done, summarize the result.
360
- - Keep updates concise \u2014 one or two sentences. Don't flood the chat.
361
-
362
- ### Conversation etiquette
363
-
364
- - **Respect ongoing conversations.** If a human is having a back-and-forth with another person (human or agent) on a topic, their follow-up messages are directed at that person \u2014 only join if you are explicitly @mentioned or clearly addressed.
365
- - **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work \u2014 let them respond to questions about it.
366
- - **Claim before you start.** Always call \`${t("claim_tasks")}\` before doing any work on a task. If the claim fails, stop immediately and pick a different task.
367
- - **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or channel before stopping.
368
- - **Skip idle narration.** Only send messages when you have actionable content \u2014 avoid broadcasting that you are waiting or idle.
369
-
370
- ### Formatting \u2014 No HTML
371
-
372
- Use plain-text @mentions (e.g. \`@alice\`) and #channel references (e.g. \`#general\`, \`#1\`) \u2014 no HTML tags.
373
-
374
- When referencing a channel or mentioning someone, write them as plain text without backticks. Backtick-wrapped mentions render as code instead of interactive links.
375
-
376
- ### Formatting \u2014 URLs in non-English text
377
-
378
- When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), always wrap the URL in angle brackets or use markdown link syntax. Otherwise the punctuation may be rendered as part of the URL.
379
-
380
- - **Wrong**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1Ahttp://localhost:3000\uFF0C\u8BF7\u67E5\u770B\` (the \`\uFF0C\` gets swallowed into the link)
381
- - **Correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A<http://localhost:3000>\uFF0C\u8BF7\u67E5\u770B\`
382
- - **Also correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A[http://localhost:3000](http://localhost:3000)\uFF0C\u8BF7\u67E5\u770B\`
383
-
384
- ## Workspace & Memory
385
-
386
- Your working directory (cwd) is your **persistent workspace**. Everything you write here survives across sessions.
387
-
388
- ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
389
-
390
- \`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.
391
-
392
- \`\`\`markdown
393
- # <Your Name>
394
-
395
- ## Role
396
- <your role definition, evolved over time>
397
-
398
- ## Key Knowledge
399
- - Read notes/user-preferences.md for user preferences and conventions
400
- - Read notes/channels.md for what each channel is about and ongoing work
401
- - Read notes/domain.md for domain-specific knowledge and conventions
402
- - ...
403
-
404
- ## Active Context
405
- - Currently working on: <brief summary>
406
- - Last interaction: <brief summary>
407
- \`\`\`
408
-
409
- ### What to memorize
410
-
411
- **Actively observe and record** the following kinds of knowledge as you encounter them in conversations:
412
-
413
- 1. **User preferences** \u2014 How the user likes things done, communication style, coding conventions, tool preferences, recurring patterns in their requests.
414
- 2. **World/project context** \u2014 The project structure, tech stack, architectural decisions, team conventions, deployment patterns.
415
- 3. **Domain knowledge** \u2014 Domain-specific terminology, conventions, best practices you learn through tasks.
416
- 4. **Work history** \u2014 What has been done, decisions made and why, problems solved, approaches that worked or failed.
417
- 5. **Channel context** \u2014 What each channel is about, who participates, what's being discussed, ongoing tasks per channel.
418
- 6. **Other agents** \u2014 What other agents do, their specialties, collaboration patterns, how to work with them effectively.
419
-
420
- ### How to organize memory
421
-
422
- - **MEMORY.md** is always the index. Keep it concise but comprehensive as a table of contents.
423
- - Create a \`notes/\` directory for detailed knowledge files. Use descriptive names:
424
- - \`notes/user-preferences.md\` \u2014 User's preferences and conventions
425
- - \`notes/channels.md\` \u2014 Summary of each channel and its purpose
426
- - \`notes/work-log.md\` \u2014 Important decisions and completed work
427
- - \`notes/<domain>.md\` \u2014 Domain-specific knowledge
428
- - You can also create any other files or directories for your work (scripts, notes, data, etc.)
429
- - **Update notes proactively** \u2014 Don't wait to be asked. When you learn something important, write it down.
430
- - **Keep MEMORY.md current** \u2014 After updating notes, update the index in MEMORY.md if new files were added.
431
-
432
- ### Compaction safety (CRITICAL)
433
-
434
- Your context will be periodically compressed to stay within limits. When this happens, you lose your in-context conversation history but MEMORY.md is always re-read. Therefore:
435
-
436
- - **MEMORY.md must be self-sufficient as a recovery point.** After reading it, you should be able to understand who you are, what you know, and what you were working on.
437
- - **Before a long task**, write a brief "Active Context" note in MEMORY.md so you can resume if interrupted mid-task.
438
- - **After completing work**, update your notes and MEMORY.md index so nothing is lost.
439
- - Keep MEMORY.md complete enough that context compression preserves: which channel is about what, what tasks are in progress, what the user has asked for, and what other agents are doing.
440
-
441
- ## Capabilities
442
-
443
- You can work with any files or tools on this computer \u2014 you are not confined to any directory.
444
- You may develop a specialized role over time through your interactions. Embrace it.`;
445
- if (opts.includeStdinNotificationSection) {
446
- prompt += `
447
-
448
- ## Message Notifications
449
-
450
- While you are busy (executing tools, thinking, etc.), new messages may arrive. When this happens, you will receive a system notification like:
451
-
452
- \`[System notification: You have N new message(s) waiting. Call check_messages to read them when you're ready.]\`
453
-
454
- How to handle these:
455
- - Call \`${t("check_messages")}()\` to check for new messages. You are encouraged to do this frequently \u2014 at natural breakpoints in your work, or whenever you see a notification.
456
- - If the new message is higher priority, you may pivot to it. If not, continue your current work.
457
- - \`check_messages\` returns instantly with any pending messages (or "no new messages"). It is always safe to call.`;
458
- }
459
- if (config.description) {
460
- prompt += `
461
-
462
- ## Initial role
463
- ${config.description}. This may evolve.`;
464
- }
465
- return prompt;
466
- }
467
-
468
- // src/drivers/claude.ts
469
- var ClaudeDriver = class {
470
- id = "claude";
471
- supportsStdinNotification = true;
472
- mcpToolPrefix = "mcp__chat__";
473
- spawn(ctx) {
474
- const mcpArgs = [
475
- ctx.chatBridgePath,
476
- "--agent-id",
477
- ctx.agentId,
478
- "--server-url",
479
- ctx.config.serverUrl,
480
- "--auth-token",
481
- ctx.config.authToken || ctx.daemonApiKey
482
- ];
483
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
484
- const mcpConfig = JSON.stringify({
485
- mcpServers: {
486
- chat: {
487
- command: isTsSource ? "npx" : "node",
488
- args: isTsSource ? ["tsx", ...mcpArgs] : mcpArgs
489
- }
490
- }
491
- });
492
- let mcpConfigArg;
493
- if (process.platform === "win32") {
494
- const mcpConfigPath = path.join(ctx.workingDirectory, ".slock-claude-mcp.json");
495
- writeFileSync(mcpConfigPath, mcpConfig, "utf8");
496
- mcpConfigArg = mcpConfigPath;
497
- } else {
498
- mcpConfigArg = mcpConfig;
499
- }
500
- const args2 = [
501
- "--allow-dangerously-skip-permissions",
502
- "--dangerously-skip-permissions",
503
- "--verbose",
504
- "--output-format",
505
- "stream-json",
506
- "--input-format",
507
- "stream-json",
508
- "--mcp-config",
509
- mcpConfigArg,
510
- "--model",
511
- ctx.config.model || "sonnet",
512
- "--disallowed-tools",
513
- "EnterPlanMode,ExitPlanMode"
514
- ];
515
- if (ctx.config.sessionId) {
516
- args2.push("--resume", ctx.config.sessionId);
517
- }
518
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", ...ctx.config.envVars || {} };
519
- delete spawnEnv.CLAUDECODE;
520
- const proc = spawn("claude", args2, {
521
- cwd: ctx.workingDirectory,
522
- stdio: ["pipe", "pipe", "pipe"],
523
- env: spawnEnv,
524
- shell: process.platform === "win32"
525
- });
526
- const stdinMsg = JSON.stringify({
527
- type: "user",
528
- message: {
529
- role: "user",
530
- content: [{ type: "text", text: ctx.prompt }]
531
- },
532
- ...ctx.config.sessionId ? { session_id: ctx.config.sessionId } : {}
533
- });
534
- proc.stdin?.write(stdinMsg + "\n");
535
- return { process: proc };
536
- }
537
- parseLine(line) {
538
- let event;
539
- try {
540
- event = JSON.parse(line);
541
- } catch {
542
- return [];
543
- }
544
- const events = [];
545
- const pushResultError = (message, fallback) => {
546
- const parts = [];
547
- if (Array.isArray(message.errors)) {
548
- for (const err of message.errors) {
549
- if (typeof err === "string" && err.trim()) parts.push(err.trim());
550
- }
551
- }
552
- if (typeof message.result === "string" && message.result.trim()) {
553
- parts.push(message.result.trim());
554
- }
555
- const detail = parts.join(" | ") || fallback;
556
- events.push({ kind: "error", message: detail });
557
- };
558
- switch (event.type) {
559
- case "system":
560
- if (event.subtype === "init" && event.session_id) {
561
- events.push({ kind: "session_init", sessionId: event.session_id });
562
- }
563
- break;
564
- case "assistant": {
565
- const content = event.message?.content;
566
- if (Array.isArray(content)) {
567
- for (const block of content) {
568
- if (block.type === "thinking" && block.thinking) {
569
- events.push({ kind: "thinking", text: block.thinking });
570
- } else if (block.type === "text" && block.text) {
571
- events.push({ kind: "text", text: block.text });
572
- } else if (block.type === "tool_use") {
573
- events.push({ kind: "tool_call", name: block.name || "unknown_tool", input: block.input });
574
- }
575
- }
576
- }
577
- break;
578
- }
579
- case "result": {
580
- const subtype = typeof event.subtype === "string" ? event.subtype : "success";
581
- const stopReason = typeof event.stop_reason === "string" ? event.stop_reason : null;
582
- switch (subtype) {
583
- case "success":
584
- if (event.is_error && stopReason !== "max_tokens") {
585
- pushResultError(event, "Execution failed");
586
- }
587
- break;
588
- case "error_during_execution":
589
- if (stopReason !== "max_tokens") {
590
- pushResultError(event, "Execution failed");
591
- }
592
- break;
593
- case "error_max_budget_usd":
594
- pushResultError(event, "Budget limit exceeded");
595
- break;
596
- case "error_max_turns":
597
- pushResultError(event, "Max turns exceeded");
598
- break;
599
- case "error_max_structured_output_retries":
600
- pushResultError(event, "Structured output retries exceeded");
601
- break;
602
- }
603
- events.push({ kind: "turn_end", sessionId: event.session_id });
604
- break;
605
- }
606
- }
607
- return events;
608
- }
609
- encodeStdinMessage(text, sessionId, _opts) {
610
- return JSON.stringify({
611
- type: "user",
612
- message: {
613
- role: "user",
614
- content: [{ type: "text", text }]
615
- },
616
- ...sessionId ? { session_id: sessionId } : {}
617
- });
618
- }
619
- buildSystemPrompt(config, _agentId) {
620
- return buildBaseSystemPrompt(config, {
621
- toolPrefix: "mcp__chat__",
622
- extraCriticalRules: [
623
- "- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
624
- ],
625
- postStartupNotes: [],
626
- includeStdinNotificationSection: true
627
- });
628
- }
629
- toolDisplayName(name) {
630
- if (name === "mcp__chat__upload_file") return "Uploading file\u2026";
631
- if (name === "mcp__chat__view_file") return "Viewing file\u2026";
632
- if (name === "mcp__chat__list_tasks") return "Listing tasks\u2026";
633
- if (name === "mcp__chat__create_tasks") return "Creating tasks\u2026";
634
- if (name === "mcp__chat__claim_tasks") return "Claiming tasks\u2026";
635
- if (name === "mcp__chat__unclaim_task") return "Unclaiming task\u2026";
636
- if (name === "mcp__chat__update_task_status") return "Updating task status\u2026";
637
- if (name === "mcp__chat__list_server") return "Listing server\u2026";
638
- if (name === "mcp__chat__read_history") return "Reading history\u2026";
639
- if (name === "mcp__chat__search_messages") return "Searching messages\u2026";
640
- if (name === "mcp__chat__check_messages") return "Checking messages\u2026";
641
- if (name.startsWith("mcp__chat__")) return "";
642
- if (name === "Read" || name === "read_file") return "Reading file\u2026";
643
- if (name === "Write" || name === "write_file") return "Writing file\u2026";
644
- if (name === "Edit" || name === "edit_file") return "Editing file\u2026";
645
- if (name === "Bash" || name === "bash") return "Running command\u2026";
646
- if (name === "Glob" || name === "glob") return "Searching files\u2026";
647
- if (name === "Grep" || name === "grep") return "Searching code\u2026";
648
- if (name === "WebFetch" || name === "web_fetch") return "Fetching web\u2026";
649
- if (name === "WebSearch" || name === "web_search") return "Searching web\u2026";
650
- if (name === "TodoWrite") return "Updating tasks\u2026";
651
- return `Using ${name.length > 20 ? name.slice(0, 20) + "\u2026" : name}\u2026`;
652
- }
653
- summarizeToolInput(name, input) {
654
- if (!input || typeof input !== "object") return "";
655
- try {
656
- if (name === "Read" || name === "read_file") return input.file_path || input.path || "";
657
- if (name === "Write" || name === "write_file") return input.file_path || input.path || "";
658
- if (name === "Edit" || name === "edit_file") return input.file_path || input.path || "";
659
- if (name === "Bash" || name === "bash") {
660
- const cmd = input.command || "";
661
- return cmd.length > 100 ? cmd.slice(0, 100) + "\u2026" : cmd;
662
- }
663
- if (name === "Glob" || name === "glob") return input.pattern || "";
664
- if (name === "Grep" || name === "grep") return input.pattern || "";
665
- if (name === "WebFetch" || name === "web_fetch") return input.url || "";
666
- if (name === "WebSearch" || name === "web_search") return input.query || "";
667
- if (name === "mcp__chat__send_message") {
668
- return input.target || input.channel || (input.dm_to ? `DM:@${input.dm_to}` : "");
669
- }
670
- if (name === "mcp__chat__read_history") return input.target || input.channel || "";
671
- if (name === "mcp__chat__search_messages") return input.query || "";
672
- if (name === "mcp__chat__list_tasks") return input.channel || "";
673
- if (name === "mcp__chat__create_tasks") return input.channel || "";
674
- if (name === "mcp__chat__claim_tasks") {
675
- const nums = input.task_numbers;
676
- return input.channel ? `${input.channel} #${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
677
- }
678
- if (name === "mcp__chat__unclaim_task") {
679
- return input.channel ? `${input.channel} #${input.task_number}` : "";
680
- }
681
- if (name === "mcp__chat__update_task_status") {
682
- return input.channel ? `${input.channel} #${input.task_number}` : "";
683
- }
684
- if (name === "mcp__chat__upload_file") return input.file_path || "";
685
- return "";
686
- } catch {
687
- return "";
688
- }
689
- }
690
- };
691
-
692
- // src/drivers/codex.ts
693
- import { spawn as spawn2, execSync } from "child_process";
694
- import { existsSync } from "fs";
695
- import path2 from "path";
696
- var CodexDriver = class {
697
- id = "codex";
698
- supportsStdinNotification = false;
699
- mcpToolPrefix = "mcp_chat_";
700
- spawn(ctx) {
701
- const gitDir = path2.join(ctx.workingDirectory, ".git");
702
- if (!existsSync(gitDir)) {
703
- execSync("git init", { cwd: ctx.workingDirectory, stdio: "pipe" });
704
- execSync("git add -A && git commit --allow-empty -m 'init'", {
705
- cwd: ctx.workingDirectory,
706
- stdio: "pipe",
707
- env: { ...process.env, GIT_AUTHOR_NAME: "slock", GIT_AUTHOR_EMAIL: "slock@local", GIT_COMMITTER_NAME: "slock", GIT_COMMITTER_EMAIL: "slock@local" }
708
- });
709
- }
710
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
711
- const command = isTsSource ? "npx" : "node";
712
- const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
713
- const args2 = ["exec"];
714
- if (ctx.config.sessionId) {
715
- args2.push("resume", ctx.config.sessionId);
716
- }
717
- args2.push(
718
- "--dangerously-bypass-approvals-and-sandbox",
719
- "--json"
720
- );
721
- args2.push(
722
- "-c",
723
- `mcp_servers.chat.command=${JSON.stringify(command)}`,
724
- "-c",
725
- `mcp_servers.chat.args=${JSON.stringify(bridgeArgs)}`,
726
- "-c",
727
- "mcp_servers.chat.startup_timeout_sec=30",
728
- "-c",
729
- "mcp_servers.chat.tool_timeout_sec=300",
730
- "-c",
731
- "mcp_servers.chat.enabled=true",
732
- "-c",
733
- "mcp_servers.chat.required=true"
734
- );
735
- if (ctx.config.model) {
736
- args2.push("-m", ctx.config.model);
737
- }
738
- if (ctx.config.reasoningEffort) {
739
- args2.push("-c", `model_reasoning_effort=${ctx.config.reasoningEffort}`);
740
- }
741
- args2.push(ctx.prompt);
742
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
743
- let spawnCmd = "codex";
744
- let spawnArgs = args2;
745
- if (process.platform === "win32") {
746
- let codexEntry = null;
747
- try {
748
- const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
749
- const candidate = path2.join(globalRoot, "@openai", "codex", "bin", "codex.js");
750
- if (existsSync(candidate)) codexEntry = candidate;
751
- } catch {
752
- }
753
- if (!codexEntry) {
754
- try {
755
- const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
756
- const candidate = path2.join(path2.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
757
- if (existsSync(candidate)) codexEntry = candidate;
758
- } catch {
759
- }
760
- }
761
- if (!codexEntry) {
762
- throw new Error(
763
- "Cannot resolve Codex CLI entry point on Windows. Ensure @openai/codex is installed globally via npm (npm i -g @openai/codex)."
764
- );
765
- }
766
- spawnCmd = process.execPath;
767
- spawnArgs = [codexEntry, ...args2];
768
- }
769
- const proc = spawn2(spawnCmd, spawnArgs, {
770
- cwd: ctx.workingDirectory,
771
- stdio: ["ignore", "pipe", "pipe"],
772
- env: spawnEnv
773
- });
774
- return { process: proc };
775
- }
776
- parseLine(line) {
777
- let event;
778
- try {
779
- event = JSON.parse(line);
780
- } catch {
781
- return [];
782
- }
783
- const events = [];
784
- switch (event.type) {
785
- case "thread.started":
786
- if (event.thread_id) {
787
- events.push({ kind: "session_init", sessionId: event.thread_id });
788
- }
789
- break;
790
- case "turn.started":
791
- events.push({ kind: "thinking", text: "" });
792
- break;
793
- case "item.started":
794
- case "item.updated":
795
- case "item.completed": {
796
- const item = event.item;
797
- if (!item) break;
798
- switch (item.type) {
799
- case "reasoning":
800
- if (item.text) {
801
- events.push({ kind: "thinking", text: item.text });
802
- }
803
- break;
804
- case "agent_message":
805
- if (item.text && event.type === "item.completed") {
806
- events.push({ kind: "text", text: item.text });
807
- }
808
- break;
809
- case "command_execution":
810
- if (event.type === "item.started") {
811
- events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
812
- }
813
- break;
814
- case "file_change":
815
- if (event.type === "item.started" && Array.isArray(item.changes)) {
816
- for (const change of item.changes) {
817
- events.push({ kind: "tool_call", name: "file_change", input: { path: change.path, kind: change.kind } });
818
- }
819
- }
820
- break;
821
- case "mcp_tool_call":
822
- if (event.type === "item.started") {
823
- const toolName = item.server && item.tool ? `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}` : item.tool || "mcp_tool";
824
- const name = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : toolName;
825
- events.push({ kind: "tool_call", name, input: item.arguments });
826
- }
827
- break;
828
- case "collab_tool_call":
829
- if (event.type === "item.started") {
830
- events.push({ kind: "tool_call", name: "collab_tool_call", input: {} });
831
- }
832
- break;
833
- case "todo_list":
834
- if (event.type === "item.started" || event.type === "item.updated") {
835
- events.push({ kind: "thinking", text: item.title || "Planning\u2026" });
836
- }
837
- break;
838
- case "web_search":
839
- if (event.type === "item.started") {
840
- events.push({ kind: "tool_call", name: "web_search", input: { query: item.query } });
841
- }
842
- break;
843
- case "error":
844
- if (item.message) {
845
- events.push({ kind: "error", message: item.message });
846
- }
847
- break;
848
- }
849
- break;
850
- }
851
- case "turn.completed":
852
- events.push({ kind: "turn_end" });
853
- break;
854
- case "turn.failed":
855
- if (event.error?.message) {
856
- events.push({ kind: "error", message: event.error.message });
857
- }
858
- events.push({ kind: "turn_end" });
859
- break;
860
- case "error":
861
- events.push({ kind: "error", message: event.message || "Unknown error" });
862
- break;
863
- }
864
- return events;
865
- }
866
- encodeStdinMessage(_text, _sessionId, _opts) {
867
- return null;
868
- }
869
- buildSystemPrompt(config, _agentId) {
870
- return buildBaseSystemPrompt(config, {
871
- toolPrefix: "",
872
- extraCriticalRules: [
873
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
874
- ],
875
- postStartupNotes: [
876
- "**IMPORTANT**: Your process exits after each turn completes. You will be automatically restarted when new messages arrive. Complete all your work, then stop \u2014 new messages will wake you up."
877
- ],
878
- includeStdinNotificationSection: false
879
- });
880
- }
881
- toolDisplayName(name) {
882
- if (name === `${this.mcpToolPrefix}upload_file`) return "Uploading file\u2026";
883
- if (name === `${this.mcpToolPrefix}view_file`) return "Viewing file\u2026";
884
- if (name === `${this.mcpToolPrefix}list_tasks`) return "Listing tasks\u2026";
885
- if (name === `${this.mcpToolPrefix}create_tasks`) return "Creating tasks\u2026";
886
- if (name === `${this.mcpToolPrefix}claim_tasks`) return "Claiming tasks\u2026";
887
- if (name === `${this.mcpToolPrefix}unclaim_task`) return "Unclaiming task\u2026";
888
- if (name === `${this.mcpToolPrefix}update_task_status`) return "Updating task status\u2026";
889
- if (name === `${this.mcpToolPrefix}list_server`) return "Listing server\u2026";
890
- if (name === `${this.mcpToolPrefix}read_history`) return "Reading history\u2026";
891
- if (name === `${this.mcpToolPrefix}search_messages`) return "Searching messages\u2026";
892
- if (name === `${this.mcpToolPrefix}check_messages`) return "Checking messages\u2026";
893
- if (name.startsWith(this.mcpToolPrefix)) return "";
894
- if (name === "shell" || name === "command_execution") return "Running command\u2026";
895
- if (name === "file_change") return "Editing file\u2026";
896
- if (name === "file_read") return "Reading file\u2026";
897
- if (name === "file_write") return "Writing file\u2026";
898
- if (name === "web_search") return "Searching web\u2026";
899
- if (name === "collab_tool_call") return "Collaborating\u2026";
900
- return `Using ${name.length > 20 ? name.slice(0, 20) + "\u2026" : name}\u2026`;
901
- }
902
- summarizeToolInput(name, input) {
903
- if (!input || typeof input !== "object") return "";
904
- try {
905
- if (name === "shell" || name === "command_execution") {
906
- const cmd = input.command || "";
907
- return cmd.length > 100 ? cmd.slice(0, 100) + "\u2026" : cmd;
908
- }
909
- if (name === "file_change") return input.path || "";
910
- if (name === "file_read") return input.path || input.file_path || "";
911
- if (name === "file_write") return input.path || input.file_path || "";
912
- if (name === "web_search") return input.query || "";
913
- if (name === `${this.mcpToolPrefix}send_message`) {
914
- return input.target || input.channel || (input.dm_to ? `DM:@${input.dm_to}` : "");
915
- }
916
- if (name === `${this.mcpToolPrefix}read_history`) return input.target || input.channel || "";
917
- if (name === `${this.mcpToolPrefix}search_messages`) return input.query || "";
918
- if (name === `${this.mcpToolPrefix}list_tasks`) return input.channel || "";
919
- if (name === `${this.mcpToolPrefix}create_tasks`) return input.channel || "";
920
- if (name === `${this.mcpToolPrefix}claim_tasks`) {
921
- const nums = input.task_numbers;
922
- return input.channel ? `${input.channel} #${Array.isArray(nums) ? nums.join(",#") : nums}` : "";
923
- }
924
- if (name === `${this.mcpToolPrefix}unclaim_task`) {
925
- return input.channel ? `${input.channel} #${input.task_number}` : "";
926
- }
927
- if (name === `${this.mcpToolPrefix}update_task_status`) {
928
- return input.channel ? `${input.channel} #${input.task_number}` : "";
929
- }
930
- if (name === `${this.mcpToolPrefix}upload_file`) return input.file_path || "";
931
- return "";
932
- } catch {
933
- return "";
934
- }
935
- }
936
- };
937
-
938
- // src/drivers/kimi.ts
939
- import { randomUUID } from "crypto";
940
- import { spawn as spawn3 } from "child_process";
941
- import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
942
- import path3 from "path";
943
- var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
944
- var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
945
- var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
946
- var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
947
- function parseToolArguments(raw) {
948
- if (typeof raw !== "string") return raw;
949
- try {
950
- return JSON.parse(raw);
951
- } catch {
952
- return raw;
953
- }
954
- }
955
- var KimiDriver = class {
956
- id = "kimi";
957
- supportsStdinNotification = true;
958
- mcpToolPrefix = "";
959
- deliverMessageDirectlyWhileBusy = true;
960
- sessionId = null;
961
- sessionAnnounced = false;
962
- promptRequestId = null;
963
- spawn(ctx) {
964
- const isResume = !!ctx.config.sessionId;
965
- this.sessionId = ctx.config.sessionId || randomUUID();
966
- this.sessionAnnounced = false;
967
- this.promptRequestId = randomUUID();
968
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
969
- const command = isTsSource ? "npx" : "node";
970
- const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
971
- const systemPromptPath = path3.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
972
- const agentFilePath = path3.join(ctx.workingDirectory, KIMI_AGENT_FILE);
973
- const mcpConfigPath = path3.join(ctx.workingDirectory, KIMI_MCP_FILE);
974
- if (!isResume || !existsSync2(systemPromptPath)) {
975
- writeFileSync2(systemPromptPath, ctx.prompt, "utf8");
976
- }
977
- writeFileSync2(agentFilePath, [
978
- "version: 1",
979
- "agent:",
980
- " extend: default",
981
- ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
982
- ""
983
- ].join("\n"), "utf8");
984
- writeFileSync2(mcpConfigPath, JSON.stringify({
985
- mcpServers: {
986
- chat: {
987
- command,
988
- args: bridgeArgs
989
- }
990
- }
991
- }), "utf8");
992
- const args2 = [
993
- "--wire",
994
- "--yolo",
995
- "--agent-file",
996
- agentFilePath,
997
- "--mcp-config-file",
998
- mcpConfigPath,
999
- "--session",
1000
- this.sessionId
1001
- ];
1002
- if (ctx.config.model && ctx.config.model !== "default") {
1003
- args2.push("--model", ctx.config.model);
1004
- }
1005
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
1006
- const proc = spawn3("kimi", args2, {
1007
- cwd: ctx.workingDirectory,
1008
- stdio: ["pipe", "pipe", "pipe"],
1009
- env: spawnEnv,
1010
- shell: process.platform === "win32"
1011
- });
1012
- proc.stdin?.write(JSON.stringify({
1013
- jsonrpc: "2.0",
1014
- id: randomUUID(),
1015
- method: "initialize",
1016
- params: {
1017
- protocol_version: KIMI_WIRE_PROTOCOL_VERSION,
1018
- client: { name: "slock-daemon", version: "1.0.0" },
1019
- capabilities: {
1020
- supports_question: false,
1021
- supports_plan_mode: false
1022
- }
1023
- }
1024
- }) + "\n");
1025
- proc.stdin?.write(JSON.stringify({
1026
- jsonrpc: "2.0",
1027
- id: this.promptRequestId,
1028
- method: "prompt",
1029
- params: {
1030
- user_input: isResume ? ctx.prompt : "Your system prompt contains your standing instructions. Follow it now and begin listening for messages."
1031
- }
1032
- }) + "\n");
1033
- return { process: proc };
1034
- }
1035
- parseLine(line) {
1036
- let message;
1037
- try {
1038
- message = JSON.parse(line);
1039
- } catch {
1040
- return [];
1041
- }
1042
- const events = [];
1043
- if (!this.sessionAnnounced && this.sessionId) {
1044
- events.push({ kind: "session_init", sessionId: this.sessionId });
1045
- this.sessionAnnounced = true;
1046
- }
1047
- if ("method" in message && message.method === "event") {
1048
- const eventType = message.params?.type;
1049
- const payload = message.params?.payload || {};
1050
- switch (eventType) {
1051
- case "StepBegin":
1052
- events.push({ kind: "thinking", text: "" });
1053
- break;
1054
- case "ContentPart":
1055
- if (payload.type === "think" && payload.think) {
1056
- events.push({ kind: "thinking", text: payload.think });
1057
- } else if (payload.type === "text" && payload.text) {
1058
- events.push({ kind: "text", text: payload.text });
1059
- }
1060
- break;
1061
- case "ToolCall":
1062
- events.push({
1063
- kind: "tool_call",
1064
- name: payload.function?.name || "unknown_tool",
1065
- input: parseToolArguments(payload.function?.arguments)
1066
- });
1067
- break;
1068
- case "TurnEnd":
1069
- events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
1070
- break;
1071
- case "StepInterrupted":
1072
- events.push({ kind: "error", message: "Turn interrupted" });
1073
- events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
1074
- break;
1075
- }
1076
- return events;
1077
- }
1078
- if ("error" in message) {
1079
- events.push({ kind: "error", message: message.error?.message || "Unknown Kimi error" });
1080
- events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
1081
- }
1082
- return events;
1083
- }
1084
- encodeStdinMessage(_text, _sessionId, opts) {
1085
- const mode = opts?.mode || "busy";
1086
- if (mode === "idle") {
1087
- return JSON.stringify({
1088
- jsonrpc: "2.0",
1089
- id: randomUUID(),
1090
- method: "prompt",
1091
- params: {
1092
- user_input: _text
1093
- }
1094
- });
1095
- }
1096
- return JSON.stringify({
1097
- jsonrpc: "2.0",
1098
- id: randomUUID(),
1099
- method: "steer",
1100
- params: {
1101
- user_input: _text
1102
- }
1103
- });
1104
- }
1105
- buildSystemPrompt(config, _agentId) {
1106
- return buildBaseSystemPrompt(config, {
1107
- toolPrefix: "",
1108
- extraCriticalRules: [
1109
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
1110
- ],
1111
- postStartupNotes: [],
1112
- includeStdinNotificationSection: true
1113
- });
1114
- }
1115
- toolDisplayName(name) {
1116
- if (name === "list_tasks") return "Viewing task board\u2026";
1117
- if (name === "create_tasks") return "Creating tasks\u2026";
1118
- if (name === "claim_tasks") return "Claiming tasks\u2026";
1119
- if (name === "unclaim_task") return "Unclaiming task\u2026";
1120
- if (name === "update_task_status") return "Updating task\u2026";
1121
- if (name === "send_message" || name === "receive_message" || name === "read_history" || name === "list_server") return "";
1122
- if (name === "Shell") return "Running command\u2026";
1123
- if (name === "ReadFile") return "Reading file\u2026";
1124
- if (name === "WriteFile" || name === "StrReplaceFile") return "Editing file\u2026";
1125
- if (name === "Glob" || name === "Grep") return "Searching code\u2026";
1126
- if (name === "SearchWeb") return "Searching web\u2026";
1127
- if (name === "FetchURL") return "Fetching web\u2026";
1128
- if (name === "SetTodoList") return "Updating tasks\u2026";
1129
- return `Using ${name.length > 20 ? name.slice(0, 20) + "\u2026" : name}\u2026`;
1130
- }
1131
- summarizeToolInput(name, input) {
1132
- if (!input || typeof input !== "object") return "";
1133
- try {
1134
- if (name === "Shell") {
1135
- const cmd = input.command || "";
1136
- return cmd.length > 100 ? cmd.slice(0, 100) + "\u2026" : cmd;
1137
- }
1138
- if (name === "ReadFile" || name === "WriteFile" || name === "StrReplaceFile") {
1139
- return input.path || "";
1140
- }
1141
- if (name === "Glob" || name === "Grep") return input.pattern || input.query || "";
1142
- if (name === "SearchWeb") return input.query || "";
1143
- if (name === "FetchURL") return input.url || "";
1144
- if (name === "send_message") return input.target || input.channel || "";
1145
- if (name === "read_history") return input.target || input.channel || "";
1146
- if (name === "list_tasks") return input.channel || "";
1147
- if (name === "create_tasks") return input.channel || "";
1148
- if (name === "claim_tasks") {
1149
- const nums = input.task_numbers;
1150
- return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
1151
- }
1152
- if (name === "unclaim_task" || name === "update_task_status") {
1153
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
1154
- }
1155
- return "";
1156
- } catch {
1157
- return "";
1158
- }
1159
- }
1160
- };
1161
-
1162
- // src/drivers/index.ts
1163
- var driverFactories = {
1164
- claude: () => new ClaudeDriver(),
1165
- codex: () => new CodexDriver(),
1166
- kimi: () => new KimiDriver()
1167
- };
1168
- function getDriver(runtimeId) {
1169
- const createDriver = driverFactories[runtimeId];
1170
- const driver = createDriver?.();
1171
- if (!driver) {
1172
- throw new Error(`Unknown runtime: ${runtimeId}. Available: ${Object.keys(driverFactories).join(", ")}`);
1173
- }
1174
- return driver;
1175
- }
1176
-
1177
- // src/agentProcessManager.ts
1178
- var DATA_DIR = path4.join(os.homedir(), ".slock", "agents");
1179
- function toLocalTime(iso) {
1180
- const d = new Date(iso);
1181
- if (isNaN(d.getTime())) return iso;
1182
- const pad = (n) => String(n).padStart(2, "0");
1183
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1184
- }
1185
- function formatChannelLabel(message) {
1186
- return message.channel_type === "dm" ? `DM:@${message.channel_name}` : `#${message.channel_name}`;
1187
- }
1188
- function formatMessageTarget(message) {
1189
- if (message.channel_type === "thread" && message.parent_channel_name) {
1190
- const shortId = message.channel_name.startsWith("thread-") ? message.channel_name.slice(7) : message.channel_name;
1191
- if (message.parent_channel_type === "dm") {
1192
- return `dm:@${message.parent_channel_name}:${shortId}`;
1193
- }
1194
- return `#${message.parent_channel_name}:${shortId}`;
1195
- }
1196
- if (message.channel_type === "dm") {
1197
- return `dm:@${message.channel_name}`;
1198
- }
1199
- return `#${message.channel_name}`;
1200
- }
1201
- function formatIncomingMessage(message) {
1202
- const target = formatMessageTarget(message);
1203
- const msgId = message.message_id ? message.message_id.slice(0, 8) : "-";
1204
- const time = message.timestamp ? toLocalTime(message.timestamp) : "-";
1205
- const senderType = message.sender_type === "agent" ? " type=agent" : "";
1206
- const renderedContent = message.content;
1207
- const attachSuffix = message.attachments?.length ? ` [${message.attachments.length} image${message.attachments.length > 1 ? "s" : ""}: ${message.attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to see]` : "";
1208
- const taskSuffix = message.task_status ? ` [task #${message.task_number} status=${message.task_status}${message.task_assignee_id ? ` assignee=${message.task_assignee_type}:${message.task_assignee_id}` : ""}]` : "";
1209
- return `[target=${target} msg=${msgId} time=${time}${senderType}] @${message.sender_name}: ${renderedContent}${attachSuffix}${taskSuffix}`;
1210
- }
1211
- function buildUnreadSummary(messages, excludeChannel) {
1212
- const summary = /* @__PURE__ */ new Map();
1213
- for (const message of messages) {
1214
- const label = formatChannelLabel(message);
1215
- if (excludeChannel && label === excludeChannel) continue;
1216
- summary.set(label, (summary.get(label) || 0) + 1);
1217
- }
1218
- return summary.size > 0 ? Object.fromEntries(summary) : void 0;
1219
- }
1220
- var MAX_TRAJECTORY_TEXT = 2e3;
1221
- var TRAJECTORY_COALESCE_MS = 350;
1222
- var ACTIVITY_HEARTBEAT_MS = 6e4;
1223
- var MAX_STDOUT_LINES = 8;
1224
- var MAX_STDOUT_LINE_LENGTH = 240;
1225
- var MAX_STDERR_LINES = 8;
1226
- var MAX_STDERR_LINE_LENGTH = 240;
1227
- function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
1228
- const next = [...lines];
1229
- for (const rawLine of chunk.split(/\r?\n/)) {
1230
- const text = rawLine.trim();
1231
- if (!text) continue;
1232
- next.push(
1233
- text.length > maxLineLength ? `${text.slice(0, maxLineLength)}...` : text
1234
- );
1235
- }
1236
- return next.slice(-maxLines);
1237
- }
1238
- function pushRecentStderr(lines, chunk) {
1239
- return pushRecentLines(lines, chunk, MAX_STDERR_LINES, MAX_STDERR_LINE_LENGTH);
1240
- }
1241
- function pushRecentStdout(lines, chunk) {
1242
- return pushRecentLines(lines, chunk, MAX_STDOUT_LINES, MAX_STDOUT_LINE_LENGTH);
1243
- }
1244
- function formatCrashReason(code, signal, ap) {
1245
- const parts = [];
1246
- if (signal) {
1247
- parts.push(`signal ${signal}`);
1248
- } else if (typeof code === "number") {
1249
- parts.push(`exit code ${code}`);
1250
- } else {
1251
- parts.push("unknown exit");
1252
- }
1253
- if (ap.spawnError) {
1254
- parts.push(`spawn error: ${ap.spawnError}`);
1255
- }
1256
- if (ap.lastRuntimeError) {
1257
- parts.push(`runtime error: ${ap.lastRuntimeError}`);
1258
- }
1259
- if (ap.recentStderr.length > 0) {
1260
- parts.push(`stderr: ${ap.recentStderr.join(" | ")}`);
1261
- }
1262
- if (!ap.lastRuntimeError && ap.recentStdout.length > 0) {
1263
- parts.push(`stdout: ${ap.recentStdout.join(" | ")}`);
1264
- }
1265
- return parts.join(" | ");
1266
- }
1267
- function summarizeCrash(code, signal) {
1268
- if (signal) return `signal ${signal}`;
1269
- if (typeof code === "number") return `exit code ${code}`;
1270
- return "unknown exit";
1271
- }
1272
- function isMissingResumeSession(ap) {
1273
- if (ap.driver.id !== "claude") return false;
1274
- if (!ap.sessionId) return false;
1275
- return /No conversation found with session ID/i.test(ap.lastRuntimeError || "");
1276
- }
1277
- function getMessageDeliveryText(supportsStdinNotification) {
1278
- return supportsStdinNotification ? "New messages will be delivered to you automatically via stdin." : "The daemon will automatically restart you when new messages arrive.";
1279
- }
1280
- var AgentProcessManager = class _AgentProcessManager {
1281
- agents = /* @__PURE__ */ new Map();
1282
- agentsStarting = /* @__PURE__ */ new Set();
1283
- // Prevent concurrent starts of same agent
1284
- startingInboxes = /* @__PURE__ */ new Map();
1285
- /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
1286
- idleAgentConfigs = /* @__PURE__ */ new Map();
1287
- chatBridgePath;
1288
- sendToServer;
1289
- daemonApiKey;
1290
- dataDir;
1291
- driverResolver;
1292
- constructor(chatBridgePath2, sendToServer, daemonApiKey, opts = {}) {
1293
- this.chatBridgePath = chatBridgePath2;
1294
- this.sendToServer = sendToServer;
1295
- this.daemonApiKey = daemonApiKey;
1296
- this.dataDir = opts.dataDir || DATA_DIR;
1297
- this.driverResolver = opts.driverResolver || getDriver;
1298
- }
1299
- async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
1300
- if (this.agents.has(agentId)) {
1301
- logger.info(`[Agent ${agentId}] Start ignored (already running)`);
1302
- return;
1303
- }
1304
- if (this.agentsStarting.has(agentId)) {
1305
- logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
1306
- return;
1307
- }
1308
- this.agentsStarting.add(agentId);
1309
- try {
1310
- const driver = this.driverResolver(config.runtime || "claude");
1311
- const agentDataDir = path4.join(this.dataDir, agentId);
1312
- await mkdir(agentDataDir, { recursive: true });
1313
- const memoryMdPath = path4.join(agentDataDir, "MEMORY.md");
1314
- try {
1315
- await access(memoryMdPath);
1316
- } catch {
1317
- const agentName = config.displayName || config.name;
1318
- const initialMemoryMd = `# ${agentName}
1319
-
1320
- ## Role
1321
- ${config.description || "No role defined yet."}
1322
-
1323
- ## Key Knowledge
1324
- - No notes yet.
1325
-
1326
- ## Active Context
1327
- - First startup.
1328
- `;
1329
- await writeFile(memoryMdPath, initialMemoryMd);
1330
- }
1331
- await mkdir(path4.join(agentDataDir, "notes"), { recursive: true });
1332
- const isResume = !!config.sessionId;
1333
- let prompt;
1334
- if (isResume && resumePrompt) {
1335
- prompt = resumePrompt;
1336
- if (driver.supportsStdinNotification) {
1337
- prompt += `
1338
-
1339
- Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.`;
1340
- }
1341
- } else if (wakeMessage) {
1342
- const channelLabel = formatChannelLabel(wakeMessage);
1343
- prompt = `New message received:
1344
-
1345
- ${formatIncomingMessage(wakeMessage)}`;
1346
- if (unreadSummary && Object.keys(unreadSummary).length > 0) {
1347
- const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
1348
- if (otherUnread.length > 0) {
1349
- prompt += `
1350
-
1351
- You also have unread messages in other channels:`;
1352
- for (const [ch, count] of otherUnread) {
1353
- prompt += `
1354
- - ${ch}: ${count} unread`;
1355
- }
1356
- prompt += `
1357
-
1358
- Use read_history to catch up, or respond to the message above first.`;
1359
- }
1360
- }
1361
- prompt += `
1362
-
1363
- Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
1364
-
1365
- IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
1366
- if (driver.supportsStdinNotification) {
1367
- prompt += `
1368
-
1369
- Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.`;
1370
- }
1371
- } else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
1372
- prompt = `You have unread messages from while you were offline:`;
1373
- for (const [ch, count] of Object.entries(unreadSummary)) {
1374
- prompt += `
1375
- - ${ch}: ${count} unread`;
1376
- }
1377
- prompt += `
1378
-
1379
- Use read_history to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call check_messages in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: reply with send_message and claim the relevant task before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
1380
- } else if (isResume) {
1381
- prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
1382
- if (driver.supportsStdinNotification) {
1383
- prompt += `
1384
-
1385
- Note: While you are busy, you may receive [System notification: ...] messages about new messages. Finish your current step, then call check_messages to check for messages.`;
1386
- }
1387
- } else {
1388
- prompt = driver.buildSystemPrompt(config, agentId);
1389
- }
1390
- const { process: proc } = driver.spawn({
1391
- agentId,
1392
- config,
1393
- prompt,
1394
- workingDirectory: agentDataDir,
1395
- chatBridgePath: this.chatBridgePath,
1396
- daemonApiKey: this.daemonApiKey
1397
- });
1398
- const agentProcess = {
1399
- process: proc,
1400
- driver,
1401
- inbox: this.startingInboxes.get(agentId) || [],
1402
- config,
1403
- sessionId: config.sessionId || null,
1404
- launchId: launchId || null,
1405
- isIdle: false,
1406
- notificationTimer: null,
1407
- pendingNotificationCount: 0,
1408
- activityHeartbeat: null,
1409
- lastActivity: "",
1410
- lastActivityDetail: "",
1411
- recentStdout: [],
1412
- recentStderr: [],
1413
- lastRuntimeError: null,
1414
- spawnError: null,
1415
- exitCode: null,
1416
- exitSignal: null,
1417
- pendingTrajectory: null
1418
- };
1419
- this.startingInboxes.delete(agentId);
1420
- this.agents.set(agentId, agentProcess);
1421
- this.agentsStarting.delete(agentId);
1422
- let buffer = "";
1423
- proc.stdout?.on("data", (chunk) => {
1424
- const chunkText = chunk.toString();
1425
- const current = this.agents.get(agentId);
1426
- if (current) {
1427
- current.recentStdout = pushRecentStdout(current.recentStdout, chunkText);
1428
- }
1429
- buffer += chunkText;
1430
- const lines = buffer.split("\n");
1431
- buffer = lines.pop() || "";
1432
- for (const line of lines) {
1433
- if (!line.trim()) continue;
1434
- const events = driver.parseLine(line);
1435
- for (const event of events) {
1436
- this.handleParsedEvent(agentId, event, driver);
1437
- }
1438
- }
1439
- });
1440
- proc.stderr?.on("data", (chunk) => {
1441
- const text = chunk.toString().trim();
1442
- if (!text) return;
1443
- if (/Reconnecting\.\.\.|Falling back from WebSockets/i.test(text)) return;
1444
- const current = this.agents.get(agentId);
1445
- if (current) {
1446
- current.recentStderr = pushRecentStderr(current.recentStderr, text);
1447
- }
1448
- logger.error(`[Agent ${agentId} stderr]: ${text}`);
1449
- });
1450
- proc.on("error", (err) => {
1451
- const current = this.agents.get(agentId);
1452
- if (current) current.spawnError = err.message;
1453
- logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
1454
- });
1455
- proc.on("exit", (code, signal) => {
1456
- const current = this.agents.get(agentId);
1457
- if (current && current.process === proc) {
1458
- current.exitCode = code;
1459
- current.exitSignal = signal;
1460
- }
1461
- logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
1462
- });
1463
- proc.on("close", (code, signal) => {
1464
- if (this.agents.has(agentId)) {
1465
- const ap = this.agents.get(agentId);
1466
- if (ap.process !== proc) return;
1467
- if (ap.notificationTimer) {
1468
- clearTimeout(ap.notificationTimer);
1469
- }
1470
- if (ap.pendingTrajectory?.timer) {
1471
- clearTimeout(ap.pendingTrajectory.timer);
1472
- }
1473
- if (ap.activityHeartbeat) {
1474
- clearInterval(ap.activityHeartbeat);
1475
- }
1476
- this.agents.delete(agentId);
1477
- const finalCode = ap.exitCode ?? code;
1478
- const finalSignal = ap.exitSignal ?? signal;
1479
- if (finalCode === 0) {
1480
- const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
1481
- const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
1482
- if (queuedWakeMessage) {
1483
- logger.info(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
1484
- const nextConfig = { ...ap.config, sessionId: ap.sessionId };
1485
- this.idleAgentConfigs.set(agentId, {
1486
- config: nextConfig,
1487
- sessionId: ap.sessionId,
1488
- launchId: ap.launchId
1489
- });
1490
- this.broadcastActivity(agentId, "working", "Message received");
1491
- this.idleAgentConfigs.delete(agentId);
1492
- this.startAgent(agentId, nextConfig, queuedWakeMessage, unreadSummary2, void 0, ap.launchId || void 0).catch((err) => {
1493
- logger.error(`[Agent ${agentId}] Failed to continue with queued message`, err);
1494
- this.idleAgentConfigs.set(agentId, {
1495
- config: nextConfig,
1496
- sessionId: ap.sessionId,
1497
- launchId: ap.launchId
1498
- });
1499
- this.broadcastActivity(agentId, "online", "Process idle");
1500
- });
1501
- return;
1502
- }
1503
- this.idleAgentConfigs.set(agentId, {
1504
- config: { ...ap.config, sessionId: ap.sessionId },
1505
- sessionId: ap.sessionId,
1506
- launchId: ap.launchId
1507
- });
1508
- if (!ap.driver.supportsStdinNotification) {
1509
- logger.info(`[Agent ${agentId}] Turn completed; cached idle state for future restart`);
1510
- }
1511
- this.broadcastActivity(agentId, "online", "Process idle");
1512
- } else {
1513
- this.idleAgentConfigs.delete(agentId);
1514
- const reason = formatCrashReason(finalCode, finalSignal, ap);
1515
- const summary = summarizeCrash(finalCode, finalSignal);
1516
- if (isMissingResumeSession(ap)) {
1517
- const staleSessionId = ap.sessionId;
1518
- const restartConfig = { ...ap.config, sessionId: null };
1519
- logger.warn(
1520
- `[Agent ${agentId}] Stored Claude session ${staleSessionId} is unavailable locally; falling back to cold start`
1521
- );
1522
- this.broadcastActivity(
1523
- agentId,
1524
- "working",
1525
- "Stored Claude session missing; cold-starting a new session\u2026",
1526
- [{ kind: "text", text: `Stored Claude session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
1527
- );
1528
- this.startAgent(agentId, restartConfig, void 0, void 0, void 0, ap.launchId || void 0).catch((err) => {
1529
- logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
1530
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
1531
- this.broadcastActivity(agentId, "offline", `Crashed (${summary})`);
1532
- });
1533
- return;
1534
- }
1535
- logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
1536
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
1537
- this.broadcastActivity(agentId, "offline", `Crashed (${summary})`);
1538
- }
1539
- }
1540
- });
1541
- this.sendAgentStatus(agentId, "active", launchId || null);
1542
- this.broadcastActivity(agentId, "working", "Starting\u2026");
1543
- } catch (err) {
1544
- this.agentsStarting.delete(agentId);
1545
- throw err;
1546
- }
1547
- }
1548
- async stopAgent(agentId, { wait = false, silent = false } = {}) {
1549
- this.idleAgentConfigs.delete(agentId);
1550
- const ap = this.agents.get(agentId);
1551
- if (!ap) {
1552
- if (!silent) {
1553
- logger.info(`[Agent ${agentId}] Stop requested but no running process was found`);
1554
- }
1555
- return;
1556
- }
1557
- if (ap.notificationTimer) {
1558
- clearTimeout(ap.notificationTimer);
1559
- }
1560
- if (ap.activityHeartbeat) {
1561
- clearInterval(ap.activityHeartbeat);
1562
- }
1563
- this.agents.delete(agentId);
1564
- ap.process.kill("SIGTERM");
1565
- if (!silent) {
1566
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
1567
- this.broadcastActivity(agentId, "offline", "Stopped");
1568
- logger.info(`[Agent ${agentId}] Stopped by request`);
1569
- }
1570
- if (wait) {
1571
- await new Promise((resolve) => {
1572
- const forceKillTimer = setTimeout(() => {
1573
- if (!silent) {
1574
- logger.warn(`[Agent ${agentId}] Stop timed out; force killing`);
1575
- }
1576
- try {
1577
- ap.process.kill("SIGKILL");
1578
- } catch {
1579
- }
1580
- resolve();
1581
- }, 5e3);
1582
- ap.process.on("exit", () => {
1583
- clearTimeout(forceKillTimer);
1584
- resolve();
1585
- });
1586
- if (ap.process.exitCode !== null || ap.process.signalCode !== null) {
1587
- clearTimeout(forceKillTimer);
1588
- resolve();
1589
- }
1590
- });
1591
- }
1592
- }
1593
- deliverMessage(agentId, message) {
1594
- const ap = this.agents.get(agentId);
1595
- if (!ap) {
1596
- if (this.agentsStarting.has(agentId)) {
1597
- const pending = this.startingInboxes.get(agentId) || [];
1598
- pending.push(message);
1599
- this.startingInboxes.set(agentId, pending);
1600
- return;
1601
- }
1602
- const cached = this.idleAgentConfigs.get(agentId);
1603
- if (cached) {
1604
- logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
1605
- this.idleAgentConfigs.delete(agentId);
1606
- this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
1607
- logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
1608
- });
1609
- }
1610
- return;
1611
- }
1612
- if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
1613
- ap.isIdle = false;
1614
- this.broadcastActivity(agentId, "working", "Message received");
1615
- this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
1616
- return;
1617
- }
1618
- ap.inbox.push(message);
1619
- if (!ap.driver.supportsStdinNotification) return;
1620
- if (!ap.sessionId) return;
1621
- ap.pendingNotificationCount++;
1622
- if (!ap.notificationTimer) {
1623
- ap.notificationTimer = setTimeout(() => {
1624
- this.sendStdinNotification(agentId);
1625
- }, 3e3);
1626
- }
1627
- }
1628
- async resetWorkspace(agentId) {
1629
- const agentDataDir = path4.join(this.dataDir, agentId);
1630
- try {
1631
- await rm(agentDataDir, { recursive: true, force: true });
1632
- logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
1633
- } catch (err) {
1634
- logger.error(`[Agent ${agentId}] Workspace reset failed`, err);
1635
- }
1636
- }
1637
- async stopAll() {
1638
- this.idleAgentConfigs.clear();
1639
- const ids = [...this.agents.keys()];
1640
- await Promise.all(ids.map((id) => this.stopAgent(id, { wait: true, silent: true })));
1641
- }
1642
- getRunningAgentIds() {
1643
- return [...this.agents.keys()];
1644
- }
1645
- getAgentSessionId(agentId) {
1646
- return this.agents.get(agentId)?.sessionId ?? null;
1647
- }
1648
- getAgentLaunchId(agentId) {
1649
- return this.agents.get(agentId)?.launchId ?? null;
1650
- }
1651
- getIdleAgentSessionIds() {
1652
- const result = [];
1653
- for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
1654
- if (sessionId) result.push({ agentId, sessionId, launchId });
1655
- }
1656
- return result;
1657
- }
1658
- // Machine-level workspace scanning
1659
- async scanAllWorkspaces() {
1660
- const results = [];
1661
- let entries;
1662
- try {
1663
- entries = await readdir(this.dataDir, { withFileTypes: true });
1664
- } catch {
1665
- return [];
1666
- }
1667
- for (const entry of entries) {
1668
- if (!entry.isDirectory()) continue;
1669
- const dirPath = path4.join(this.dataDir, entry.name);
1670
- try {
1671
- const dirContents = await readdir(dirPath, { withFileTypes: true });
1672
- let totalSize = 0;
1673
- let latestMtime = /* @__PURE__ */ new Date(0);
1674
- let fileCount = 0;
1675
- for (const item of dirContents) {
1676
- const itemPath = path4.join(dirPath, item.name);
1677
- try {
1678
- const info = await stat(itemPath);
1679
- if (item.isFile()) {
1680
- totalSize += info.size;
1681
- fileCount++;
1682
- }
1683
- if (info.mtime > latestMtime) {
1684
- latestMtime = info.mtime;
1685
- }
1686
- } catch {
1687
- continue;
1688
- }
1689
- }
1690
- results.push({
1691
- directoryName: entry.name,
1692
- totalSizeBytes: totalSize,
1693
- lastModified: latestMtime.toISOString(),
1694
- fileCount
1695
- });
1696
- } catch {
1697
- continue;
1698
- }
1699
- }
1700
- return results;
1701
- }
1702
- async deleteWorkspaceDirectory(directoryName) {
1703
- if (directoryName.includes("/") || directoryName.includes("..") || directoryName.includes("\\")) {
1704
- return false;
1705
- }
1706
- const targetDir = path4.join(this.dataDir, directoryName);
1707
- try {
1708
- await rm(targetDir, { recursive: true, force: true });
1709
- logger.info(`[Workspace] Deleted directory: ${targetDir}`);
1710
- return true;
1711
- } catch (err) {
1712
- logger.error(`[Workspace] Failed to delete directory ${targetDir}`, err);
1713
- return false;
1714
- }
1715
- }
1716
- // Workspace file browsing
1717
- async getFileTree(agentId, dirPath) {
1718
- const agentDir = path4.join(this.dataDir, agentId);
1719
- try {
1720
- await stat(agentDir);
1721
- } catch {
1722
- return [];
1723
- }
1724
- let targetDir = agentDir;
1725
- if (dirPath) {
1726
- const resolved = path4.resolve(agentDir, dirPath);
1727
- if (!resolved.startsWith(agentDir + path4.sep) && resolved !== agentDir) {
1728
- return [];
1729
- }
1730
- targetDir = resolved;
1731
- }
1732
- return this.listDirectoryChildren(targetDir, agentDir);
1733
- }
1734
- async readFile(agentId, filePath) {
1735
- const agentDir = path4.join(this.dataDir, agentId);
1736
- const resolved = path4.resolve(agentDir, filePath);
1737
- if (!resolved.startsWith(agentDir + path4.sep) && resolved !== agentDir) {
1738
- throw new Error("Access denied");
1739
- }
1740
- const info = await stat(resolved);
1741
- if (info.isDirectory()) throw new Error("Cannot read a directory");
1742
- const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
1743
- ".md",
1744
- ".txt",
1745
- ".json",
1746
- ".js",
1747
- ".ts",
1748
- ".jsx",
1749
- ".tsx",
1750
- ".yaml",
1751
- ".yml",
1752
- ".toml",
1753
- ".log",
1754
- ".csv",
1755
- ".xml",
1756
- ".html",
1757
- ".css",
1758
- ".sh",
1759
- ".py"
1760
- ]);
1761
- const ext = path4.extname(resolved).toLowerCase();
1762
- if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
1763
- return { content: null, binary: true };
1764
- }
1765
- if (info.size > 1048576) throw new Error("File too large");
1766
- const content = await readFile(resolved, "utf-8");
1767
- return { content, binary: false };
1768
- }
1769
- // Skill scanning
1770
- // Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
1771
- // To add a new runtime, add an entry here.
1772
- static SKILL_PATHS = {
1773
- claude: {
1774
- // Claude reads shared skills via symlinks in ~/.claude/skills/, not from ~/.agents/skills/
1775
- global: [".claude/skills", ".claude/commands"],
1776
- workspace: [".claude/skills", ".claude/commands"]
1777
- },
1778
- codex: {
1779
- // Codex natively scans ~/.agents/skills/ and has built-in .system skills
1780
- global: [".codex/skills", ".codex/skills/.system", ".agents/skills"],
1781
- workspace: [".codex/skills", ".agents/skills"]
1782
- }
1783
- };
1784
- async listSkills(agentId, runtimeHint) {
1785
- const agent = this.agents.get(agentId);
1786
- const runtime = runtimeHint || agent?.config.runtime || "claude";
1787
- const home = os.homedir();
1788
- const workspaceDir = path4.join(this.dataDir, agentId);
1789
- const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
1790
- const globalResults = await Promise.all(
1791
- paths.global.map((p) => this.scanSkillsDir(path4.join(home, p)))
1792
- );
1793
- const workspaceResults = await Promise.all(
1794
- paths.workspace.map((p) => this.scanSkillsDir(path4.join(workspaceDir, p)))
1795
- );
1796
- const dedup = (skills) => {
1797
- const seen = /* @__PURE__ */ new Set();
1798
- return skills.filter((s) => {
1799
- if (seen.has(s.name)) return false;
1800
- seen.add(s.name);
1801
- return true;
1802
- });
1803
- };
1804
- const shorten = (skills) => skills.map((s) => ({
1805
- ...s,
1806
- sourcePath: s.sourcePath?.startsWith(home) ? "~" + s.sourcePath.slice(home.length) : s.sourcePath
1807
- }));
1808
- return {
1809
- global: shorten(dedup(globalResults.flat())),
1810
- workspace: shorten(dedup(workspaceResults.flat()))
1811
- };
1812
- }
1813
- async scanSkillsDir(dir) {
1814
- let entries;
1815
- try {
1816
- entries = await readdir(dir, { withFileTypes: true });
1817
- } catch {
1818
- return [];
1819
- }
1820
- const skills = [];
1821
- for (const entry of entries) {
1822
- if (entry.isDirectory() || entry.isSymbolicLink()) {
1823
- const skillMd = path4.join(dir, entry.name, "SKILL.md");
1824
- try {
1825
- const content = await readFile(skillMd, "utf-8");
1826
- const skill = this.parseSkillMd(entry.name, content);
1827
- skill.sourcePath = dir;
1828
- skills.push(skill);
1829
- } catch {
1830
- }
1831
- } else if (entry.name.endsWith(".md")) {
1832
- const cmdName = entry.name.replace(/\.md$/, "");
1833
- try {
1834
- const content = await readFile(path4.join(dir, entry.name), "utf-8");
1835
- const skill = this.parseSkillMd(cmdName, content);
1836
- skill.sourcePath = dir;
1837
- skills.push(skill);
1838
- } catch {
1839
- }
1840
- }
1841
- }
1842
- return skills;
1843
- }
1844
- parseSkillMd(dirName, content) {
1845
- const info = {
1846
- name: dirName,
1847
- displayName: dirName,
1848
- description: "",
1849
- userInvocable: false
1850
- };
1851
- const match = content.match(/^---\n([\s\S]*?)\n---/);
1852
- if (!match) return info;
1853
- const frontmatter = match[1];
1854
- for (const line of frontmatter.split("\n")) {
1855
- const colonIdx = line.indexOf(":");
1856
- if (colonIdx === -1) continue;
1857
- const key = line.slice(0, colonIdx).trim();
1858
- const value = line.slice(colonIdx + 1).trim();
1859
- if (key === "name") info.displayName = value;
1860
- if (key === "description") info.description = value;
1861
- if (key === "user-invocable") info.userInvocable = value === "true";
1862
- }
1863
- return info;
1864
- }
1865
- // Private methods
1866
- /**
1867
- * Broadcast an activity change — emits a single agent:activity event that carries
1868
- * both the status (for the dot indicator) and trajectory entries (for the activity log).
1869
- */
1870
- broadcastActivity(agentId, activity, detail, extraTrajectory = []) {
1871
- const ap = this.agents.get(agentId);
1872
- const entries = [...extraTrajectory];
1873
- const hasToolStart = entries.some((e) => e.kind === "tool_start");
1874
- if (!hasToolStart) {
1875
- entries.push({ kind: "status", activity, detail });
1876
- }
1877
- this.sendToServer({ type: "agent:activity", agentId, activity, detail, entries, launchId: ap?.launchId || void 0 });
1878
- if (ap) {
1879
- ap.lastActivity = activity;
1880
- ap.lastActivityDetail = detail;
1881
- if (activity === "working" || activity === "thinking") {
1882
- if (!ap.activityHeartbeat) {
1883
- ap.activityHeartbeat = setInterval(() => {
1884
- this.sendToServer({
1885
- type: "agent:activity",
1886
- agentId,
1887
- activity: ap.lastActivity,
1888
- detail: ap.lastActivityDetail,
1889
- launchId: ap.launchId || void 0
1890
- });
1891
- }, ACTIVITY_HEARTBEAT_MS);
1892
- }
1893
- } else {
1894
- if (ap.activityHeartbeat) {
1895
- clearInterval(ap.activityHeartbeat);
1896
- ap.activityHeartbeat = null;
1897
- }
1898
- }
1899
- }
1900
- }
1901
- flushPendingTrajectory(agentId) {
1902
- const ap = this.agents.get(agentId);
1903
- const pending = ap?.pendingTrajectory;
1904
- if (!ap || !pending) return;
1905
- if (pending.timer) clearTimeout(pending.timer);
1906
- ap.pendingTrajectory = null;
1907
- const text = pending.text.length > MAX_TRAJECTORY_TEXT ? pending.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : pending.text;
1908
- if (!text) return;
1909
- if (pending.kind === "thinking") {
1910
- this.broadcastActivity(agentId, "thinking", "", [{ kind: "thinking", text }]);
1911
- } else {
1912
- this.broadcastActivity(agentId, "thinking", "", [{ kind: "text", text }]);
1913
- }
1914
- }
1915
- queueTrajectoryText(agentId, kind, text) {
1916
- const ap = this.agents.get(agentId);
1917
- if (!ap) return;
1918
- if (!text) {
1919
- this.broadcastActivity(agentId, "thinking", "");
1920
- return;
1921
- }
1922
- const pending = ap.pendingTrajectory;
1923
- if (pending && pending.kind === kind) {
1924
- pending.text += text;
1925
- if (pending.timer) clearTimeout(pending.timer);
1926
- pending.timer = setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS);
1927
- return;
1928
- }
1929
- this.flushPendingTrajectory(agentId);
1930
- if (ap.lastActivity !== "thinking" || ap.lastActivityDetail !== "") {
1931
- this.broadcastActivity(agentId, "thinking", "");
1932
- }
1933
- ap.pendingTrajectory = {
1934
- kind,
1935
- text,
1936
- timer: setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS)
1937
- };
1938
- }
1939
- /** Handle a single ParsedEvent from any runtime driver */
1940
- handleParsedEvent(agentId, event, driver) {
1941
- const ap = this.agents.get(agentId);
1942
- switch (event.kind) {
1943
- case "session_init":
1944
- if (ap) ap.sessionId = event.sessionId;
1945
- this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
1946
- break;
1947
- case "thinking": {
1948
- this.queueTrajectoryText(agentId, "thinking", event.text);
1949
- if (ap) ap.isIdle = false;
1950
- break;
1951
- }
1952
- case "text": {
1953
- this.queueTrajectoryText(agentId, "text", event.text);
1954
- if (ap) ap.isIdle = false;
1955
- break;
1956
- }
1957
- case "tool_call": {
1958
- this.flushPendingTrajectory(agentId);
1959
- const toolName = event.name;
1960
- const inputSummary = driver.summarizeToolInput(toolName, event.input);
1961
- const detail = toolName === `${driver.mcpToolPrefix}check_messages` ? "Checking messages\u2026" : toolName === `${driver.mcpToolPrefix}send_message` ? "Sending message\u2026" : driver.toolDisplayName(toolName);
1962
- this.broadcastActivity(agentId, "working", detail, [{ kind: "tool_start", toolName, toolInput: inputSummary }]);
1963
- if (ap) ap.isIdle = false;
1964
- break;
1965
- }
1966
- case "turn_end":
1967
- this.flushPendingTrajectory(agentId);
1968
- if (ap) {
1969
- if (event.sessionId) ap.sessionId = event.sessionId;
1970
- if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
1971
- const nextMessages = ap.inbox.splice(0, ap.inbox.length);
1972
- this.broadcastActivity(agentId, "working", "Message received");
1973
- this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
1974
- } else {
1975
- ap.isIdle = true;
1976
- this.broadcastActivity(agentId, "online", "Idle");
1977
- }
1978
- }
1979
- if (event.sessionId) {
1980
- this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
1981
- }
1982
- break;
1983
- case "error": {
1984
- this.flushPendingTrajectory(agentId);
1985
- if (ap) ap.lastRuntimeError = event.message;
1986
- this.broadcastActivity(agentId, "error", event.message, [
1987
- { kind: "text", text: `Error: ${event.message}` }
1988
- ]);
1989
- break;
1990
- }
1991
- }
1992
- }
1993
- sendAgentStatus(agentId, status, launchId) {
1994
- this.sendToServer({ type: "agent:status", agentId, status, launchId: launchId || void 0 });
1995
- }
1996
- /** Send a batched notification to the agent via stdin about pending messages */
1997
- sendStdinNotification(agentId) {
1998
- const ap = this.agents.get(agentId);
1999
- if (!ap) return;
2000
- const count = ap.pendingNotificationCount;
2001
- ap.pendingNotificationCount = 0;
2002
- ap.notificationTimer = null;
2003
- if (count === 0) return;
2004
- if (ap.isIdle) return;
2005
- if (!ap.sessionId) return;
2006
- if (ap.driver.deliverMessageDirectlyWhileBusy && ap.inbox.length > 0) {
2007
- const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
2008
- console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
2009
- this.broadcastActivity(agentId, "working", "Message received");
2010
- this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy");
2011
- return;
2012
- }
2013
- const notification = `[System notification: You have ${count} new message${count > 1 ? "s" : ""} waiting. Call check_messages to read ${count > 1 ? "them" : "it"} when you're ready.]`;
2014
- logger.info(`[Agent ${agentId}] Sending stdin notification: ${count} message(s)`);
2015
- const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
2016
- if (encoded) {
2017
- ap.process.stdin?.write(encoded + "\n");
2018
- }
2019
- }
2020
- /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
2021
- deliverMessagesViaStdin(agentId, ap, messages, mode) {
2022
- if (messages.length === 0) return;
2023
- const prompt = messages.length === 1 ? `New message received:
2024
-
2025
- ${formatIncomingMessage(messages[0])}
2026
-
2027
- Respond as appropriate. Complete all your work before stopping.` : `New messages received:
2028
-
2029
- ${messages.map((message) => formatIncomingMessage(message)).join("\n")}
2030
-
2031
- Respond as appropriate. Complete all your work before stopping.`;
2032
- const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
2033
- if (encoded) {
2034
- const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
2035
- logger.info(
2036
- `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
2037
- );
2038
- ap.process.stdin?.write(encoded + "\n");
2039
- }
2040
- }
2041
- /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
2042
- async listDirectoryChildren(dir, rootDir) {
2043
- let entries;
2044
- try {
2045
- entries = await readdir(dir, { withFileTypes: true });
2046
- } catch {
2047
- return [];
2048
- }
2049
- entries.sort((a, b) => {
2050
- if (a.isDirectory() && !b.isDirectory()) return -1;
2051
- if (!a.isDirectory() && b.isDirectory()) return 1;
2052
- return a.name.localeCompare(b.name);
2053
- });
2054
- const nodes = [];
2055
- for (const entry of entries) {
2056
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2057
- const fullPath = path4.join(dir, entry.name);
2058
- const relativePath = path4.relative(rootDir, fullPath);
2059
- let info;
2060
- try {
2061
- info = await stat(fullPath);
2062
- } catch {
2063
- continue;
2064
- }
2065
- if (entry.isDirectory()) {
2066
- nodes.push({ name: entry.name, path: relativePath, isDirectory: true, size: 0, modifiedAt: info.mtime.toISOString() });
2067
- } else {
2068
- nodes.push({ name: entry.name, path: relativePath, isDirectory: false, size: info.size, modifiedAt: info.mtime.toISOString() });
2069
- }
2070
- }
2071
- return nodes;
2072
- }
2073
- };
2074
-
2075
- // ../shared/src/serverPermissions.ts
2076
- var EMPTY_SERVER_CAPABILITIES = Object.freeze({
2077
- manageServer: false,
2078
- manageChannels: false,
2079
- manageAgents: false,
2080
- manageMachines: false,
2081
- manageMembers: false,
2082
- changeMemberRoles: false,
2083
- manageBilling: false,
2084
- joinPublicChannels: false
2085
- });
2086
- var SERVER_CAPABILITY_MATRIX = {
2087
- owner: Object.freeze({
2088
- manageServer: true,
2089
- manageChannels: true,
2090
- manageAgents: true,
2091
- manageMachines: true,
2092
- manageMembers: true,
2093
- changeMemberRoles: true,
2094
- manageBilling: true,
2095
- joinPublicChannels: true
2096
- }),
2097
- admin: Object.freeze({
2098
- manageServer: true,
2099
- manageChannels: true,
2100
- manageAgents: true,
2101
- manageMachines: true,
2102
- manageMembers: true,
2103
- changeMemberRoles: false,
2104
- manageBilling: false,
2105
- joinPublicChannels: true
2106
- }),
2107
- member: Object.freeze({
2108
- manageServer: false,
2109
- manageChannels: false,
2110
- manageAgents: false,
2111
- manageMachines: false,
2112
- manageMembers: false,
2113
- changeMemberRoles: false,
2114
- manageBilling: false,
2115
- joinPublicChannels: true
2116
- })
2117
- };
2118
-
2119
- // ../shared/src/index.ts
2120
- var RUNTIMES = [
2121
- { id: "claude", displayName: "Claude Code", binary: "claude", supported: true },
2122
- { id: "codex", displayName: "Codex CLI", binary: "codex", supported: true },
2123
- { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: false },
2124
- { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true }
2125
- ];
2126
- var PLAN_CONFIG = {
2127
- free: {
2128
- displayName: "Hobby",
2129
- limits: { maxMachines: 2, maxAgents: 5, maxChannels: 5, messageHistoryDays: 30, includedAgents: 5 },
2130
- comingSoon: false,
2131
- price: 0,
2132
- extraAgentPrice: 0
2133
- },
2134
- founder: {
2135
- displayName: "Founder",
2136
- limits: { maxMachines: -1, maxAgents: -1, maxChannels: -1, messageHistoryDays: -1, includedAgents: -1 },
2137
- comingSoon: false,
2138
- price: 0,
2139
- extraAgentPrice: 0
2140
- }
2141
- };
2142
- var DISPLAY_PLAN_CONFIG = {
2143
- free: PLAN_CONFIG.free,
2144
- pro: {
2145
- displayName: "Team",
2146
- limits: { maxMachines: 8, maxAgents: 40, maxChannels: 20, messageHistoryDays: -1, includedAgents: 40 },
2147
- comingSoon: true,
2148
- price: 20,
2149
- extraAgentPrice: 0
2150
- },
2151
- max: {
2152
- displayName: "Business",
2153
- limits: { maxMachines: 40, maxAgents: 200, maxChannels: -1, messageHistoryDays: -1, includedAgents: 200 },
2154
- comingSoon: true,
2155
- price: 200,
2156
- extraAgentPrice: 0
2157
- }
2158
- };
2159
-
2160
- // src/index.ts
2161
- var require2 = createRequire(import.meta.url);
2162
- var DAEMON_VERSION = require2("../package.json").version;
2163
- function formatChannelTarget(msg) {
2164
- return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
2165
- }
2166
- function summarizeIncomingMessage(msg) {
2167
- switch (msg.type) {
2168
- case "agent:start":
2169
- return `(agent=${msg.agentId}, runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`;
2170
- case "agent:stop":
2171
- return `(agent=${msg.agentId})`;
2172
- case "agent:reset-workspace":
2173
- return `(agent=${msg.agentId})`;
2174
- case "agent:deliver":
2175
- return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
2176
- case "agent:workspace:list":
2177
- return `(agent=${msg.agentId}, dir=${msg.dirPath || "."})`;
2178
- case "agent:workspace:read":
2179
- return `(agent=${msg.agentId}, path=${msg.path})`;
2180
- case "agent:skills:list":
2181
- return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
2182
- case "machine:workspace:delete":
2183
- return `(directory=${msg.directoryName})`;
2184
- default:
2185
- return "";
2186
- }
2187
- }
2188
- function detectRuntimes() {
2189
- const ids = [];
2190
- const versions = {};
2191
- const cmd = process.platform === "win32" ? "where" : "which";
2192
- for (const rt of RUNTIMES) {
2193
- try {
2194
- execSync2(`${cmd} ${rt.binary}`, { stdio: "pipe" });
2195
- ids.push(rt.id);
2196
- try {
2197
- const ver = execSync2(`${rt.binary} --version`, { stdio: "pipe", timeout: 5e3 }).toString().trim().split("\n")[0];
2198
- versions[rt.id] = ver;
2199
- } catch {
2200
- }
2201
- } catch {
2202
- }
2203
- }
2204
- return { ids, versions };
2205
- }
2206
- var args = process.argv.slice(2);
2207
- var serverUrl = "";
2208
- var apiKey = "";
2209
- for (let i = 0; i < args.length; i++) {
2210
- if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
2211
- if (args[i] === "--api-key" && args[i + 1]) apiKey = args[++i];
2212
- }
2213
- if (!serverUrl || !apiKey) {
2214
- console.error("Usage: slock-daemon --server-url <url> --api-key <key>");
10
+ var parsedArgs = parseDaemonCliArgs(process.argv.slice(2));
11
+ if (!parsedArgs) {
12
+ console.error(DAEMON_CLI_USAGE);
2215
13
  process.exit(1);
2216
14
  }
2217
- var __dirname = path5.dirname(fileURLToPath(import.meta.url));
2218
- var chatBridgePath = path5.resolve(__dirname, "chat-bridge.js");
2219
- try {
2220
- accessSync(chatBridgePath);
2221
- } catch {
2222
- chatBridgePath = path5.resolve(__dirname, "chat-bridge.ts");
2223
- }
2224
- var connection;
2225
- var agentManager = new AgentProcessManager(chatBridgePath, (msg) => {
2226
- connection.send(msg);
2227
- }, apiKey);
2228
- connection = new DaemonConnection({
2229
- serverUrl,
2230
- apiKey,
2231
- onMessage: (msg) => {
2232
- const summary = summarizeIncomingMessage(msg);
2233
- logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
2234
- switch (msg.type) {
2235
- case "agent:start":
2236
- logger.info(`[Agent ${msg.agentId}] Start requested (runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`);
2237
- agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary, msg.resumePrompt, msg.launchId).catch((err) => {
2238
- const reason = err instanceof Error ? err.message : String(err);
2239
- logger.error(`[Agent ${msg.agentId}] Start failed (${reason})`);
2240
- connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive", launchId: msg.launchId });
2241
- connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: `Start failed: ${reason}`, launchId: msg.launchId });
2242
- });
2243
- break;
2244
- case "agent:stop":
2245
- logger.info(`[Agent ${msg.agentId}] Stop requested`);
2246
- agentManager.stopAgent(msg.agentId);
2247
- break;
2248
- case "agent:reset-workspace":
2249
- logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
2250
- agentManager.resetWorkspace(msg.agentId);
2251
- break;
2252
- case "agent:deliver":
2253
- logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
2254
- agentManager.deliverMessage(msg.agentId, msg.message);
2255
- connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
2256
- break;
2257
- case "agent:workspace:list":
2258
- agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
2259
- connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
2260
- });
2261
- break;
2262
- case "agent:workspace:read":
2263
- agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
2264
- connection.send({
2265
- type: "agent:workspace:file_content",
2266
- agentId: msg.agentId,
2267
- requestId: msg.requestId,
2268
- content,
2269
- binary
2270
- });
2271
- }).catch(() => {
2272
- connection.send({
2273
- type: "agent:workspace:file_content",
2274
- agentId: msg.agentId,
2275
- requestId: msg.requestId,
2276
- content: null,
2277
- binary: false
2278
- });
2279
- });
2280
- break;
2281
- case "agent:skills:list":
2282
- agentManager.listSkills(msg.agentId, msg.runtime).then(({ global, workspace }) => {
2283
- connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global, workspace });
2284
- }).catch((err) => {
2285
- logger.error(`[Daemon] Failed to list skills for ${msg.agentId}`, err);
2286
- connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
2287
- });
2288
- break;
2289
- case "machine:workspace:scan":
2290
- logger.info("[Daemon] Scanning all workspace directories");
2291
- agentManager.scanAllWorkspaces().then((directories) => {
2292
- connection.send({ type: "machine:workspace:scan_result", directories });
2293
- });
2294
- break;
2295
- case "machine:workspace:delete":
2296
- logger.info(`[Daemon] Deleting workspace directory: ${msg.directoryName}`);
2297
- agentManager.deleteWorkspaceDirectory(msg.directoryName).then((success) => {
2298
- connection.send({ type: "machine:workspace:delete_result", directoryName: msg.directoryName, success });
2299
- });
2300
- break;
2301
- case "ping":
2302
- connection.send({ type: "pong" });
2303
- break;
2304
- }
2305
- },
2306
- onConnect: () => {
2307
- const { ids: runtimes, versions: runtimeVersions } = detectRuntimes();
2308
- const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
2309
- logger.info(`[Daemon] Detected runtimes: ${runtimeInfo.join(", ") || "none"}`);
2310
- connection.send({
2311
- type: "ready",
2312
- capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
2313
- runtimes,
2314
- runningAgents: agentManager.getRunningAgentIds(),
2315
- hostname: os2.hostname(),
2316
- os: `${os2.platform()} ${os2.arch()}`,
2317
- daemonVersion: DAEMON_VERSION
2318
- });
2319
- for (const agentId of agentManager.getRunningAgentIds()) {
2320
- const sessionId = agentManager.getAgentSessionId(agentId);
2321
- const launchId = agentManager.getAgentLaunchId(agentId);
2322
- if (sessionId) {
2323
- connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
2324
- }
2325
- }
2326
- for (const { agentId, sessionId, launchId } of agentManager.getIdleAgentSessionIds()) {
2327
- connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
2328
- }
2329
- },
2330
- onDisconnect: () => {
2331
- logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
2332
- }
2333
- });
2334
- logger.info("[Slock Daemon] Starting...");
2335
- connection.connect();
15
+ var daemon = new DaemonCore(parsedArgs);
16
+ daemon.start();
2336
17
  var shutdown = async () => {
2337
- logger.info("[Slock Daemon] Shutting down...");
2338
- await agentManager.stopAll();
2339
- connection.disconnect();
18
+ await daemon.stop();
2340
19
  process.exit(0);
2341
20
  };
2342
- process.on("SIGTERM", shutdown);
2343
- process.on("SIGINT", shutdown);
21
+ process.on("SIGTERM", () => {
22
+ void shutdown();
23
+ });
24
+ process.on("SIGINT", () => {
25
+ void shutdown();
26
+ });