@slock-ai/daemon 0.39.0 → 0.39.1-alpha.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.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-E6OOH3IC.js";
5
5
 
6
6
  // src/core.ts
7
- import path10 from "path";
7
+ import path11 from "path";
8
8
  import os4 from "os";
9
9
  import { createRequire } from "module";
10
10
  import { accessSync } from "fs";
@@ -37,6 +37,9 @@ var TOOL_DISPLAY_METADATA = {
37
37
  web_fetch: { logLabel: "Fetching web", activityLabel: "Fetching web\u2026", summaryKind: "url" },
38
38
  web_search: { logLabel: "Searching web", activityLabel: "Searching web\u2026", summaryKind: "query" },
39
39
  todo_write: { logLabel: "Updating tasks", activityLabel: "Updating tasks\u2026", summaryKind: "none" },
40
+ schedule_reminder: { logLabel: "Scheduling reminder", activityLabel: "Scheduling reminder\u2026", summaryKind: "reminder_title" },
41
+ list_reminders: { logLabel: "Listing reminders", activityLabel: "Listing reminders\u2026", summaryKind: "none" },
42
+ cancel_reminder: { logLabel: "Canceling reminder", activityLabel: "Canceling reminder\u2026", summaryKind: "reminder_id" },
40
43
  collab_tool_call: { logLabel: "Collaborating", activityLabel: "Collaborating\u2026", summaryKind: "none" }
41
44
  };
42
45
  var KNOWN_TOOL_ALIASES = {
@@ -89,6 +92,9 @@ var KNOWN_TOOL_ALIASES = {
89
92
  SearchWeb: "web_search",
90
93
  TodoWrite: "todo_write",
91
94
  SetTodoList: "todo_write",
95
+ schedule_reminder: "schedule_reminder",
96
+ list_reminders: "list_reminders",
97
+ cancel_reminder: "cancel_reminder",
92
98
  collab_tool_call: "collab_tool_call"
93
99
  };
94
100
  var MCP_CHAT_NAMESPACE_PREFIXES = ["mcp__chat__", "mcp_chat_"];
@@ -119,6 +125,198 @@ function resolveToolSemantic(toolName) {
119
125
  const normalized = normalizeToolLookupName(toolName);
120
126
  return KNOWN_TOOL_ALIASES[normalized] ?? null;
121
127
  }
128
+ function tokenizeShellCommand(command) {
129
+ const tokens = [];
130
+ let current = "";
131
+ let quote = null;
132
+ let escaping = false;
133
+ for (const ch of command) {
134
+ if (escaping) {
135
+ current += ch;
136
+ escaping = false;
137
+ continue;
138
+ }
139
+ if (quote === "'") {
140
+ if (ch === "'") {
141
+ quote = null;
142
+ } else {
143
+ current += ch;
144
+ }
145
+ continue;
146
+ }
147
+ if (quote === '"') {
148
+ if (ch === '"') {
149
+ quote = null;
150
+ } else if (ch === "\\") {
151
+ escaping = true;
152
+ } else {
153
+ current += ch;
154
+ }
155
+ continue;
156
+ }
157
+ if (ch === "'" || ch === '"') {
158
+ quote = ch;
159
+ continue;
160
+ }
161
+ if (ch === "\\") {
162
+ escaping = true;
163
+ continue;
164
+ }
165
+ if (/\s/.test(ch)) {
166
+ if (current) {
167
+ tokens.push(current);
168
+ current = "";
169
+ }
170
+ continue;
171
+ }
172
+ current += ch;
173
+ }
174
+ if (escaping || quote) return null;
175
+ if (current) tokens.push(current);
176
+ return tokens;
177
+ }
178
+ function isEnvAssignmentToken(token) {
179
+ return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
180
+ }
181
+ function isSlockExecutableToken(token) {
182
+ const lastSep = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
183
+ const base = (lastSep >= 0 ? token.slice(lastSep + 1) : token).toLowerCase();
184
+ return base === "slock" || base === "slock.cmd";
185
+ }
186
+ function isShellExecutableToken(token) {
187
+ const lastSep = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
188
+ const base = (lastSep >= 0 ? token.slice(lastSep + 1) : token).toLowerCase();
189
+ return base === "bash" || base === "zsh" || base === "sh";
190
+ }
191
+ function findSlockExecutableIndex(tokens) {
192
+ const commandStartIndexes = [0];
193
+ for (let i = 0; i < tokens.length; i += 1) {
194
+ if (tokens[i] === "|" || tokens[i] === "&&" || tokens[i] === "||" || tokens[i] === ";") {
195
+ commandStartIndexes.push(i + 1);
196
+ }
197
+ }
198
+ for (const start of commandStartIndexes) {
199
+ let executableIndex = start;
200
+ while (executableIndex < tokens.length && isEnvAssignmentToken(tokens[executableIndex])) {
201
+ executableIndex += 1;
202
+ }
203
+ if (executableIndex < tokens.length && isSlockExecutableToken(tokens[executableIndex])) {
204
+ return executableIndex;
205
+ }
206
+ }
207
+ return -1;
208
+ }
209
+ function unwrapShellPayload(tokens, executableIndex) {
210
+ if (!isShellExecutableToken(tokens[executableIndex])) return null;
211
+ for (let i = executableIndex + 1; i < tokens.length; i++) {
212
+ const arg = tokens[i];
213
+ if (arg.startsWith("-") && arg.endsWith("c")) {
214
+ return i + 1 < tokens.length ? tokens[i + 1] : null;
215
+ }
216
+ if (!arg.startsWith("-")) break;
217
+ }
218
+ return null;
219
+ }
220
+ function readOptionValues(args, flag) {
221
+ const values = [];
222
+ for (let i = 0; i < args.length; i += 1) {
223
+ const arg = args[i];
224
+ if (arg === flag && i + 1 < args.length) {
225
+ values.push(args[i + 1]);
226
+ i += 1;
227
+ continue;
228
+ }
229
+ if (arg.startsWith(`${flag}=`)) {
230
+ values.push(arg.slice(flag.length + 1));
231
+ }
232
+ }
233
+ return values;
234
+ }
235
+ function readOptionValue(args, flag) {
236
+ return readOptionValues(args, flag).at(-1);
237
+ }
238
+ function parsePositiveIntegers(args, flag) {
239
+ return readOptionValues(args, flag).map((value) => Number(value)).filter((value) => Number.isFinite(value) && Number.isInteger(value) && value > 0);
240
+ }
241
+ function resolveSlockCliInvocation(toolName, input) {
242
+ if (resolveToolSemantic(toolName) !== "bash") return null;
243
+ const value = asObject(input);
244
+ if (!value || typeof value.command !== "string") return null;
245
+ const tokens = tokenizeShellCommand(value.command);
246
+ if (!tokens || tokens.length === 0) return null;
247
+ const firstExecutableIndex = (() => {
248
+ let index = 0;
249
+ while (index < tokens.length && isEnvAssignmentToken(tokens[index])) index += 1;
250
+ return index;
251
+ })();
252
+ if (firstExecutableIndex >= tokens.length) return null;
253
+ if (isShellExecutableToken(tokens[firstExecutableIndex])) {
254
+ const innerCommand = unwrapShellPayload(tokens, firstExecutableIndex);
255
+ if (innerCommand) {
256
+ return resolveSlockCliInvocation(toolName, { command: innerCommand });
257
+ }
258
+ }
259
+ const executableIndex = findSlockExecutableIndex(tokens);
260
+ if (executableIndex < 0) return null;
261
+ const cliArgs = tokens.slice(executableIndex + 1);
262
+ const [resource, action, ...rest] = cliArgs;
263
+ if (!resource || !action) return null;
264
+ switch (`${resource} ${action}`) {
265
+ case "message send":
266
+ return { toolName: "send_message", input: { target: readOptionValue(rest, "--target") } };
267
+ case "message check":
268
+ return { toolName: "check_messages", input: {} };
269
+ case "message read":
270
+ return { toolName: "read_history", input: { channel: readOptionValue(rest, "--channel") } };
271
+ case "message search":
272
+ return { toolName: "search_messages", input: { query: readOptionValue(rest, "--query") } };
273
+ case "server info":
274
+ return { toolName: "list_server", input: {} };
275
+ case "task list":
276
+ return { toolName: "list_tasks", input: { channel: readOptionValue(rest, "--channel") } };
277
+ case "task create":
278
+ return { toolName: "create_tasks", input: { channel: readOptionValue(rest, "--channel") } };
279
+ case "task claim":
280
+ return {
281
+ toolName: "claim_tasks",
282
+ input: {
283
+ channel: readOptionValue(rest, "--channel"),
284
+ task_numbers: parsePositiveIntegers(rest, "--number")
285
+ }
286
+ };
287
+ case "task unclaim":
288
+ return {
289
+ toolName: "unclaim_task",
290
+ input: {
291
+ channel: readOptionValue(rest, "--channel"),
292
+ task_number: parsePositiveIntegers(rest, "--number")[0]
293
+ }
294
+ };
295
+ case "task update":
296
+ return {
297
+ toolName: "update_task_status",
298
+ input: {
299
+ channel: readOptionValue(rest, "--channel"),
300
+ task_number: parsePositiveIntegers(rest, "--number")[0]
301
+ }
302
+ };
303
+ case "attachment upload":
304
+ return { toolName: "upload_file", input: { path: readOptionValue(rest, "--path") } };
305
+ case "attachment view":
306
+ return { toolName: "view_file", input: {} };
307
+ case "reminder schedule":
308
+ return { toolName: "schedule_reminder", input: { title: readOptionValue(rest, "--title") } };
309
+ case "reminder list":
310
+ return { toolName: "list_reminders", input: {} };
311
+ case "reminder cancel":
312
+ return { toolName: "cancel_reminder", input: { reminder_id: readOptionValue(rest, "--id") } };
313
+ default:
314
+ return null;
315
+ }
316
+ }
317
+ function normalizeToolDisplayInvocation(toolName, input) {
318
+ return resolveSlockCliInvocation(toolName, input) ?? { toolName, input };
319
+ }
122
320
  function getToolActivityLabel(toolName) {
123
321
  const semantic = resolveToolSemantic(toolName);
124
322
  if (semantic) return TOOL_DISPLAY_METADATA[semantic].activityLabel;
@@ -157,6 +355,14 @@ function summarizeToolInput(toolName, input) {
157
355
  return value.channel && value.task_number != null ? `${value.channel} #t${value.task_number}` : "";
158
356
  case "target":
159
357
  return value.target || "";
358
+ case "reminder_title": {
359
+ const title = value.title;
360
+ return typeof title === "string" ? truncateLabel(title, 40) : "";
361
+ }
362
+ case "reminder_id": {
363
+ const id = value.reminder_id;
364
+ return typeof id === "string" ? `#${id.slice(0, 8)}` : "";
365
+ }
160
366
  }
161
367
  }
162
368
 
@@ -270,129 +476,203 @@ var DISPLAY_PLAN_CONFIG = {
270
476
 
271
477
  // src/agentProcessManager.ts
272
478
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
273
- import path9 from "path";
479
+ import path10 from "path";
274
480
  import os3 from "os";
275
481
 
276
482
  // src/drivers/claude.ts
277
483
  import { spawn } from "child_process";
278
- import { writeFileSync } from "fs";
279
- import path2 from "path";
484
+ import path3 from "path";
485
+
486
+ // src/drivers/cliTransport.ts
487
+ import { mkdirSync, writeFileSync } from "fs";
488
+ import path from "path";
280
489
 
281
490
  // src/drivers/systemPrompt.ts
282
491
  function toolRef(prefix, name) {
283
492
  return `${prefix}${name}`;
284
493
  }
285
- function buildBaseSystemPrompt(config, opts) {
494
+ function buildPrompt(config, variant, opts) {
495
+ const isCli = variant === "cli";
286
496
  const t = (name) => toolRef(opts.toolPrefix, name);
497
+ const sendCmd = isCli ? "`slock message send`" : `\`${t("send_message")}\``;
498
+ const readCmd = isCli ? "`slock message read`" : `\`${t("read_history")}\``;
499
+ const checkCmd = isCli ? "`slock message check`" : `\`${t("check_messages")}\``;
500
+ const taskClaimCmd = isCli ? "`slock task claim`" : `\`${t("claim_tasks")}\``;
501
+ const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
502
+ const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
503
+ const serverInfoCmd = isCli ? "`slock server info`" : `\`${t("list_server")}\``;
287
504
  const messageDeliveryText = opts.includeStdinNotificationSection ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
288
- const criticalRules = [
289
- `- Always communicate through ${t("send_message")}. This is your only output channel.`,
505
+ const criticalRules = isCli ? [
506
+ "- Always communicate through `slock` CLI commands. This is your only output channel.",
290
507
  ...opts.extraCriticalRules,
291
- `- Use only the provided MCP tools for messaging \u2014 they are already available and ready.`,
292
- `- Always claim a task via ${t("claim_tasks")} before starting work on it. If the claim fails, move on to a different task.`
508
+ "- Use only the provided `slock` CLI commands for messaging.",
509
+ "- Always claim a task via `slock task claim` before starting work on it. If the claim fails, move on to a different task."
510
+ ] : [
511
+ `- Always communicate through ${sendCmd}. This is your only output channel.`,
512
+ ...opts.extraCriticalRules,
513
+ "- Use only the provided MCP tools for messaging \u2014 they are already available and ready.",
514
+ `- Always claim a task via ${taskClaimCmd} before starting work on it. If the claim fails, move on to a different task.`
293
515
  ];
294
- const startupSteps = [
295
- `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.`,
296
- `2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.`,
516
+ const startupSteps = isCli ? [
517
+ "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 `slock message send` before deep context gathering.",
518
+ "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
519
+ `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
520
+ "4. When you receive a message, process it and reply with `slock message send`.",
521
+ "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."
522
+ ] : [
523
+ `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 ${sendCmd} before deep context gathering.`,
524
+ "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
297
525
  `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
298
- `4. When you receive a message, process it and reply with ${t("send_message")}.`,
299
- `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.`
526
+ `4. When you receive a message, process it and reply with ${sendCmd}.`,
527
+ "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."
300
528
  ];
301
- let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
302
-
303
- ## Who you are
529
+ const communicationSection = isCli ? `## Communication \u2014 slock CLI ONLY
530
+
531
+ Use the \`slock\` CLI for chat / task / attachment operations. The daemon injects a local \`slock\` wrapper into PATH for you. Use ONLY these commands for communication:
532
+
533
+ 1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
534
+ 2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
535
+ 3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
536
+ 4. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
537
+ 5. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
538
+ 6. **\`slock task list\`** \u2014 View a channel's task board.
539
+ 7. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
540
+ 8. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
541
+ 9. **\`slock task unclaim\`** \u2014 Release your claim on a task.
542
+ 10. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
543
+ 11. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to \`slock message send\`.
544
+ 12. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
545
+
546
+ The CLI prints human-readable canonical text on success (matching the format you see in received messages and history). On failure it prints JSON to stderr:
547
+ - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
548
+
549
+ Error code prefixes tell you the layer:
550
+ - \`MISSING_*\` / \`TOKEN_*\` = local auth bootstrap
551
+ - \`*_FAILED\` = 4xx from server
552
+ - \`SERVER_5XX\` = server unreachable / crashed` : `## Communication \u2014 MCP tools ONLY
304
553
 
305
- 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.
554
+ You have MCP tools from the "chat" server. Use ONLY these for communication:
306
555
 
307
- ## Communication \u2014 MCP tools ONLY
556
+ 1. **${checkCmd}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
557
+ 2. **${sendCmd}** \u2014 Send a message to a channel or DM.
558
+ 3. **${serverInfoCmd}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
559
+ 4. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
560
+ 5. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
561
+ 6. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
562
+ 7. **${taskCreateCmd}** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
563
+ 8. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
564
+ 9. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
565
+ 10. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
566
+ 11. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
567
+ 12. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
568
+ const sendingMessagesSection = isCli ? `### Sending messages
569
+
570
+ - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
571
+ - **Reply to a DM**: \`slock message send --target "dm:@peer-name" <<'EOF'\` followed by the message body and \`EOF\`
572
+ - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'EOF'\` followed by the message body and \`EOF\`
573
+ - **Start a NEW DM**: \`slock message send --target "dm:@person-name" <<'EOF'\` followed by the message body and \`EOF\`
574
+
575
+ Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
576
+ \`\`\`bash
577
+ slock message send --target "#channel-name" <<'EOF'
578
+ Long message with "quotes", $vars, \`backticks\`, and code blocks.
579
+ EOF
580
+ \`\`\`
308
581
 
309
- You have MCP tools from the "chat" server. Use ONLY these for communication:
582
+ **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.` : `### Sending messages
310
583
 
311
- 1. **${t("check_messages")}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
312
- 2. **${t("send_message")}** \u2014 Send a message to a channel or DM.
313
- 3. **${t("list_server")}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
314
- 4. **${t("read_history")}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
315
- 5. **${t("search_messages")}** \u2014 Search messages visible to you, then inspect a hit with \`${t("read_history")}\`.
316
- 6. **${t("list_tasks")}** \u2014 View a channel's task board.
317
- 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).
318
- 8. **${t("claim_tasks")}** \u2014 Claim tasks by number (supports batch, handles conflicts).
319
- 9. **${t("unclaim_task")}** \u2014 Release your claim on a task.
320
- 10. **${t("update_task_status")}** \u2014 Change a task's status (e.g. to in_review or done).
321
- 11. **${t("upload_file")}** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to send_message.
322
- 12. **${t("view_file")}** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
584
+ - **Reply to a channel**: \`${t("send_message")}(target="#channel-name", content="...")\`
585
+ - **Reply to a DM**: \`${t("send_message")}(target="dm:@peer-name", content="...")\`
586
+ - **Reply in a thread**: \`${t("send_message")}(target="#channel:shortid", content="...")\` or \`${t("send_message")}(target="dm:@peer:shortid", content="...")\`
587
+ - **Start a NEW DM**: \`${t("send_message")}(target="dm:@person-name", content="...")\`
323
588
 
324
- CRITICAL RULES:
325
- ${criticalRules.join("\n")}
589
+ **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.`;
590
+ const threadsSection = isCli ? `### Threads
326
591
 
327
- ## Startup sequence
592
+ Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
328
593
 
329
- ${startupSteps.join("\n")}`;
330
- if (opts.postStartupNotes.length > 0) {
331
- prompt += `
594
+ - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
595
+ - 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.
596
+ - **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 \`slock message send --target "#general:a1b2c3d4" <<'EOF'\` followed by the message body and \`EOF\`. The thread will be auto-created if it doesn't exist yet.
597
+ - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
598
+ - You can read thread history: \`slock message read --channel "#general:a1b2c3d4"\`
599
+ - Threads cannot be nested \u2014 you cannot start a thread inside a thread.` : `### Threads
332
600
 
333
- ${opts.postStartupNotes.join("\n")}`;
334
- }
335
- prompt += `
601
+ Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
336
602
 
337
- ## Messaging
603
+ - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
604
+ - 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.
605
+ - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, call \`${t("send_message")}(target="#general:a1b2c3d4", content="...")\`. The thread will be auto-created if it doesn't exist yet.
606
+ - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
607
+ - You can read thread history via ${readCmd} with the same thread target.
608
+ - Threads cannot be nested \u2014 you cannot start a thread inside a thread.`;
609
+ const discoverySection = isCli ? `### Discovering people and channels
338
610
 
339
- Messages you receive have a single RFC 5424-style structured data header followed by the sender and content:
611
+ Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
612
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.` : `### Discovering people and channels
340
613
 
341
- \`\`\`
342
- [target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00 type=human] @richard: hello everyone
343
- [target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01 type=agent] @Alice: hi there
344
- [target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02 type=human] @richard: hey, can you help?
345
- [target=#general:a1b2c3d4 msg=f3a4b5c6 time=2026-03-15T01:00:03 type=human] @richard: thread reply
346
- [target=dm:@richard:x9y8z7a0 msg=d7e8f9a0 time=2026-03-15T01:00:04 type=human] @richard: DM thread reply
347
- \`\`\`
614
+ Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
615
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.`;
616
+ const channelAwarenessSection = isCli ? `### Channel awareness
348
617
 
349
- Header fields:
350
- - \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
351
- - \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
352
- - \`time=\` \u2014 timestamp.
353
- - \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
618
+ Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
619
+ - **Reply in context** \u2014 always respond in the channel/thread the message came from.
620
+ - **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.
621
+ - If unsure where something belongs, call \`slock server info\` to review channel descriptions.` : `### Channel awareness
354
622
 
355
- \`type=system\` messages announce state changes in the channel (task events, channel archived/unarchived, etc.). They are informational \u2014 don't reply to them unless they clearly request action (e.g. a task was just assigned to you). In particular, archive/unarchive notifications do not need any response. If a channel is archived, further writes there will be rejected.
623
+ Each channel has a **name** and optionally a **description** that define its purpose (visible via ${serverInfoCmd}). Respect them:
624
+ - **Reply in context** \u2014 always respond in the channel/thread the message came from.
625
+ - **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.
626
+ - If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
627
+ const readingHistorySection = isCli ? `### Reading history
356
628
 
357
- ### Sending messages
629
+ \`slock message read --channel "#channel-name"\` or \`slock message read --channel "dm:@peer-name"\` or \`slock message read --channel "#channel:shortid"\`
358
630
 
359
- - **Reply to a channel**: \`send_message(target="#channel-name", content="...")\`
360
- - **Reply to a DM**: \`send_message(target="dm:@peer-name", content="...")\`
361
- - **Reply in a thread**: \`send_message(target="#channel:shortid", content="...")\` or \`send_message(target="dm:@peer:shortid", content="...")\`
362
- - **Start a NEW DM**: \`send_message(target="dm:@person-name", content="...")\`
631
+ To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.` : `### Reading history
363
632
 
364
- **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.
633
+ Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
365
634
 
366
- ### Threads
635
+ To jump directly to a specific hit with nearby context, pass \`around\` set to a message ID or seq number.`;
636
+ const tasksSection = isCli ? `### Tasks
367
637
 
368
- Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
638
+ 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.
369
639
 
370
- - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
371
- - 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.
372
- - **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.
373
- - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
374
- - You can read thread history: \`read_history(channel="#general:a1b2c3d4")\`
375
- - Threads cannot be nested \u2014 you cannot start a thread inside a thread.
640
+ **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.
376
641
 
377
- ### Discovering people and channels
642
+ **What you see in messages:**
643
+ - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
644
+ - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
645
+ - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
378
646
 
379
- Call \`list_server\` to see all channels in this server, which ones you have joined, other agents, and humans.
380
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`read_history\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.
647
+ 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.
381
648
 
382
- ### Channel awareness
649
+ \`slock message read\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
383
650
 
384
- Each channel has a **name** and optionally a **description** that define its purpose (visible via \`list_server\`). Respect them:
385
- - **Reply in context** \u2014 always respond in the channel/thread the message came from.
386
- - **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.
387
- - If unsure where something belongs, call \`list_server\` to review channel descriptions.
651
+ **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
388
652
 
389
- ### Reading history
653
+ **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
390
654
 
391
- \`read_history(channel="#channel-name")\` or \`read_history(channel="dm:@peer-name")\` or \`read_history(channel="#channel:shortid")\`
655
+ **Workflow:**
656
+ 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)
657
+ 2. If the claim fails, someone else is working on it \u2014 move on to another task
658
+ 3. Post updates in the task's thread: \`slock message send --target "#channel:msgShortId" <<'EOF'\` followed by the message body and \`EOF\`
659
+ 4. When done, set status to \`in_review\` so a human can validate via \`slock task update\`
660
+ 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
392
661
 
393
- To jump directly to a specific hit with nearby context, use \`read_history(channel="...", around="messageId")\` or \`read_history(channel="...", around=12345)\`.
662
+ **What \`slock task create\` really means:**
663
+ - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
664
+ - \`slock task create\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
665
+ - \`slock task create\` only creates the task \u2014 to own it, call \`slock task claim\` afterward.
666
+ - Typical uses for \`slock task create\` are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
667
+ - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
668
+ - If the work already exists as a message, reuse it via \`slock task claim --message-id ...\`.
394
669
 
395
- ### Tasks
670
+ **Creating new tasks:**
671
+ - 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.
672
+ - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
673
+ - Before calling \`slock task create\`, first check whether the work already exists on the task board or is already being handled.
674
+ - Reuse existing tasks and threads instead of creating duplicates.
675
+ - Use \`slock task create\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.` : `### Tasks
396
676
 
397
677
  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.
398
678
 
@@ -405,7 +685,7 @@ When someone sends a message that asks you to do something \u2014 fix a bug, wri
405
685
 
406
686
  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.
407
687
 
408
- \`read_history\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
688
+ ${readCmd} shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
409
689
 
410
690
  **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
411
691
 
@@ -414,24 +694,77 @@ Only top-level channel / DM messages can become tasks. Messages inside threads a
414
694
  **Workflow:**
415
695
  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)
416
696
  2. If the claim fails, someone else is working on it \u2014 move on to another task
417
- 3. Post updates in the task's thread: \`send_message(target="#channel:msgShortId", ...)\`
418
- 4. When done, set status to \`in_review\` so a human can validate
697
+ 3. Post updates in the task's thread via ${sendCmd} with \`target="#channel:msgShortId"\`
698
+ 4. When done, set status to \`in_review\` so a human can validate via ${taskUpdateCmd}
419
699
  5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
420
700
 
421
- **What \`${t("create_tasks")}\` really means:**
701
+ **What ${taskCreateCmd} really means:**
422
702
  - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
423
- - \`${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.
424
- - \`${t("create_tasks")}\` only creates the task \u2014 to own it, call \`${t("claim_tasks")}\` afterward.
425
- - 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.
703
+ - ${taskCreateCmd} is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
704
+ - ${taskCreateCmd} only creates the task \u2014 to own it, call ${taskClaimCmd} afterward.
705
+ - Typical uses for ${taskCreateCmd} are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
426
706
  - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
427
- - If the work already exists as a message, reuse it via \`${t("claim_tasks")}\` with \`message_ids\`.
707
+ - If the work already exists as a message, reuse it via ${taskClaimCmd} with the message ID.
428
708
 
429
709
  **Creating new tasks:**
430
710
  - 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.
431
711
  - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
432
- - Before calling \`${t("create_tasks")}\`, first check whether the work already exists on the task board or is already being handled.
712
+ - Before calling ${taskCreateCmd}, first check whether the work already exists on the task board or is already being handled.
433
713
  - Reuse existing tasks and threads instead of creating duplicates.
434
- - Use \`${t("create_tasks")}\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.
714
+ - Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
715
+ const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
716
+ let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
717
+
718
+ ## Who you are
719
+
720
+ 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.
721
+
722
+ ${communicationSection}
723
+
724
+ CRITICAL RULES:
725
+ ${criticalRules.join("\n")}
726
+
727
+ ## Startup sequence
728
+
729
+ ${startupSteps.join("\n")}`;
730
+ if (opts.postStartupNotes.length > 0) {
731
+ prompt += `
732
+
733
+ ${opts.postStartupNotes.join("\n")}`;
734
+ }
735
+ prompt += `
736
+
737
+ ## Messaging
738
+
739
+ Messages you receive have a single RFC 5424-style structured data header followed by the sender and content:
740
+
741
+ \`\`\`
742
+ [target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00 type=human] @richard: hello everyone
743
+ [target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01 type=agent] @Alice: hi there
744
+ [target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02 type=human] @richard: hey, can you help?
745
+ [target=#general:a1b2c3d4 msg=f3a4b5c6 time=2026-03-15T01:00:03 type=human] @richard: thread reply
746
+ [target=dm:@richard:x9y8z7a0 msg=d7e8f9a0 time=2026-03-15T01:00:04 type=human] @richard: DM thread reply
747
+ \`\`\`
748
+
749
+ Header fields:
750
+ - \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
751
+ - \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
752
+ - \`time=\` \u2014 timestamp.
753
+ - \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
754
+
755
+ \`type=system\` messages announce state changes in the channel (task events, channel archived/unarchived, etc.). They are informational \u2014 don't reply to them unless they clearly request action (e.g. a task was just assigned to you). In particular, archive/unarchive notifications do not need any response. If a channel is archived, further writes there will be rejected.
756
+
757
+ ${sendingMessagesSection}
758
+
759
+ ${threadsSection}
760
+
761
+ ${discoverySection}
762
+
763
+ ${channelAwarenessSection}
764
+
765
+ ${readingHistorySection}
766
+
767
+ ${tasksSection}
435
768
 
436
769
  ### Splitting tasks for parallel execution
437
770
 
@@ -445,8 +778,8 @@ When you receive a notification about new tasks, check the task board and claim
445
778
  ## @Mentions
446
779
 
447
780
  In channel group chats, you can @mention people by their unique name (e.g. @alice or @bob).
448
- - Your stable Slock @mention handle is @${config.name}.
449
- - Your display name is ${config.displayName || config.name}. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable name.
781
+ - Your stable Slock @mention handle is \`@${config.name}\`.
782
+ - Your display name is \`${config.displayName || config.name}\`. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable \`name\`.
450
783
  - Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
451
784
  - Mention others, not yourself \u2014 assign reviews and follow-ups to teammates.
452
785
  - @mentions only reach people inside the channel \u2014 channels are the isolation boundary.
@@ -463,7 +796,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
463
796
 
464
797
  - **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.
465
798
  - **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.
466
- - **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.
799
+ - **Claim before you start.** Always call ${claimForEtiquette} before doing any work on a task. If the claim fails, stop immediately and pick a different task.
467
800
  - **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.
468
801
  - **Skip idle narration.** Only send messages when you have actionable content \u2014 avoid broadcasting that you are waiting or idle.
469
802
 
@@ -559,20 +892,21 @@ How to handle these:
559
892
  - Treat direct follow-up messages as new user input for the same live session.
560
893
  - Adapt if the new message changes priority or direction.
561
894
  - You do NOT need to poll just because direct follow-up delivery is available.
562
- - Use \`${t("check_messages")}()\` only when you need to inspect other pending channels or recover broader context.`;
895
+ - Use ${checkCmd} only when you need to inspect other pending channels or recover broader context.`;
563
896
  } else {
897
+ const notifyExample = isCli ? `\`[System notification: You have N new message(s) waiting. Call slock message check to read them when you're ready.]\`` : `\`[System notification: You have N new message(s) waiting. Call ${t("check_messages")} to read them when you're ready.]\``;
564
898
  prompt += `
565
899
 
566
900
  ## Message Notifications
567
901
 
568
902
  While you are busy (executing tools, thinking, etc.), new messages may arrive. When this happens, you will receive a system notification like:
569
903
 
570
- \`[System notification: You have N new message(s) waiting. Call check_messages to read them when you're ready.]\`
904
+ ${notifyExample}
571
905
 
572
906
  How to handle these:
573
- - 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.
907
+ - Call ${checkCmd} 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.
574
908
  - If the new message is higher priority, you may pivot to it. If not, continue your current work.
575
- - \`check_messages\` returns instantly with any pending messages (or "no new messages"). It is always safe to call.`;
909
+ - ${checkCmd} returns instantly with any pending messages (or "no new messages"). It is always safe to call.`;
576
910
  }
577
911
  }
578
912
  if (config.description) {
@@ -583,11 +917,62 @@ ${config.description}. This may evolve.`;
583
917
  }
584
918
  return prompt;
585
919
  }
920
+ function buildCliSystemPrompt(config, opts) {
921
+ return buildPrompt(config, "cli", opts);
922
+ }
923
+ function buildMcpSystemPrompt(config, opts) {
924
+ return buildPrompt(config, "mcp", opts);
925
+ }
926
+
927
+ // src/drivers/cliTransport.ts
928
+ var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
929
+ function buildCliTransportSystemPrompt(config, opts) {
930
+ return buildCliSystemPrompt(config, opts);
931
+ }
932
+ function prepareCliTransport(ctx, extraEnv = {}, platform = process.platform) {
933
+ if (!ctx.slockCliPath) {
934
+ throw new Error(`${ctx.config.runtime} driver: slockCliPath is required (daemon must inject it)`);
935
+ }
936
+ const slockDir = path.join(ctx.workingDirectory, ".slock");
937
+ mkdirSync(slockDir, { recursive: true });
938
+ const tokenFile = path.join(slockDir, "agent-token");
939
+ writeFileSync(tokenFile, ctx.config.authToken || ctx.daemonApiKey, { mode: 384 });
940
+ const posixWrapper = path.join(slockDir, "slock");
941
+ const posixBody = `#!/usr/bin/env bash
942
+ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)} "$@"
943
+ `;
944
+ writeFileSync(posixWrapper, posixBody, { mode: 493 });
945
+ if (platform === "win32") {
946
+ const cmdWrapper = path.join(slockDir, "slock.cmd");
947
+ const cmdBody = `@echo off\r
948
+ "${process.execPath}" "${ctx.slockCliPath}" %*\r
949
+ `;
950
+ writeFileSync(cmdWrapper, cmdBody);
951
+ }
952
+ const wrapperPath = platform === "win32" ? path.join(slockDir, "slock.cmd") : posixWrapper;
953
+ const spawnEnv = {
954
+ ...process.env,
955
+ FORCE_COLOR: "0",
956
+ ...ctx.config.envVars || {},
957
+ ...extraEnv,
958
+ SLOCK_AGENT_ID: ctx.agentId,
959
+ SLOCK_SERVER_URL: ctx.config.serverUrl,
960
+ SLOCK_AGENT_TOKEN_FILE: tokenFile,
961
+ PATH: `${slockDir}${path.delimiter}${process.env.PATH ?? ""}`
962
+ };
963
+ delete spawnEnv.SLOCK_AGENT_TOKEN;
964
+ return {
965
+ slockDir,
966
+ tokenFile,
967
+ wrapperPath,
968
+ spawnEnv
969
+ };
970
+ }
586
971
 
587
972
  // src/drivers/probe.ts
588
973
  import { execFileSync } from "child_process";
589
974
  import { existsSync } from "fs";
590
- import path from "path";
975
+ import path2 from "path";
591
976
  function normalizeExecOutput(raw) {
592
977
  return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
593
978
  }
@@ -653,12 +1038,19 @@ function readCommandVersion(command, args = [], deps = {}) {
653
1038
  }
654
1039
  function resolveHomePath(relativePath, deps = {}) {
655
1040
  const homeDir = deps.homeDir ?? deps.env?.HOME ?? process.env.HOME ?? "";
656
- return path.join(homeDir, relativePath);
1041
+ return path2.join(homeDir, relativePath);
657
1042
  }
658
1043
 
659
1044
  // src/drivers/claude.ts
660
- var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path2.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
1045
+ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
661
1046
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1047
+ var CLAUDE_DISALLOWED_TOOLS = [
1048
+ "EnterPlanMode",
1049
+ "ExitPlanMode",
1050
+ "CronCreate",
1051
+ "CronList",
1052
+ "CronDelete"
1053
+ ].join(",");
662
1054
  function resolveClaudeCommand(deps = {}) {
663
1055
  const pathCommand = resolveCommandOnPath("claude", deps);
664
1056
  if (pathCommand) return pathCommand;
@@ -681,36 +1073,11 @@ var ClaudeDriver = class {
681
1073
  supportsStdinNotification = true;
682
1074
  mcpToolPrefix = "mcp__chat__";
683
1075
  busyDeliveryMode = "notification";
1076
+ supportsNativeStandingPrompt = true;
684
1077
  probe() {
685
1078
  return probeClaude();
686
1079
  }
687
- spawn(ctx) {
688
- const mcpArgs = [
689
- ctx.chatBridgePath,
690
- "--agent-id",
691
- ctx.agentId,
692
- "--server-url",
693
- ctx.config.serverUrl,
694
- "--auth-token",
695
- ctx.config.authToken || ctx.daemonApiKey
696
- ];
697
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
698
- const mcpConfig = JSON.stringify({
699
- mcpServers: {
700
- chat: {
701
- command: isTsSource ? "npx" : "node",
702
- args: isTsSource ? ["tsx", ...mcpArgs] : mcpArgs
703
- }
704
- }
705
- });
706
- let mcpConfigArg;
707
- if (process.platform === "win32") {
708
- const mcpConfigPath = path2.join(ctx.workingDirectory, ".slock-claude-mcp.json");
709
- writeFileSync(mcpConfigPath, mcpConfig, "utf8");
710
- mcpConfigArg = mcpConfigPath;
711
- } else {
712
- mcpConfigArg = mcpConfig;
713
- }
1080
+ buildClaudeArgs(config, standingPrompt) {
714
1081
  const args = [
715
1082
  "--allow-dangerously-skip-permissions",
716
1083
  "--dangerously-skip-permissions",
@@ -719,18 +1086,25 @@ var ClaudeDriver = class {
719
1086
  "stream-json",
720
1087
  "--input-format",
721
1088
  "stream-json",
722
- "--mcp-config",
723
- mcpConfigArg,
1089
+ "--append-system-prompt",
1090
+ standingPrompt,
724
1091
  "--model",
725
- ctx.config.model || "sonnet",
1092
+ config.model || "sonnet",
726
1093
  "--disallowed-tools",
727
- "EnterPlanMode,ExitPlanMode"
1094
+ CLAUDE_DISALLOWED_TOOLS
728
1095
  ];
729
- if (ctx.config.sessionId) {
730
- args.push("--resume", ctx.config.sessionId);
1096
+ if (config.sessionId) {
1097
+ args.push("--resume", config.sessionId);
731
1098
  }
732
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", ...ctx.config.envVars || {} };
1099
+ return args;
1100
+ }
1101
+ spawn(ctx) {
1102
+ const { tokenFile, spawnEnv } = prepareCliTransport(ctx);
1103
+ const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt);
733
1104
  delete spawnEnv.CLAUDECODE;
1105
+ logger.info(
1106
+ `[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
1107
+ );
734
1108
  const proc = spawn(resolveClaudeCommand() ?? "claude", args, {
735
1109
  cwd: ctx.workingDirectory,
736
1110
  stdio: ["pipe", "pipe", "pipe"],
@@ -774,6 +1148,12 @@ var ClaudeDriver = class {
774
1148
  if (event.subtype === "init" && event.session_id) {
775
1149
  events.push({ kind: "session_init", sessionId: event.session_id });
776
1150
  }
1151
+ if (event.subtype === "status" && event.status === "compacting") {
1152
+ events.push({ kind: "compaction_started" });
1153
+ }
1154
+ if (event.subtype === "compact_boundary") {
1155
+ events.push({ kind: "compaction_finished" });
1156
+ }
777
1157
  break;
778
1158
  case "assistant": {
779
1159
  const content = event.message?.content;
@@ -831,11 +1211,9 @@ var ClaudeDriver = class {
831
1211
  });
832
1212
  }
833
1213
  buildSystemPrompt(config, _agentId) {
834
- return buildBaseSystemPrompt(config, {
1214
+ return buildCliTransportSystemPrompt(config, {
835
1215
  toolPrefix: "mcp__chat__",
836
- extraCriticalRules: [
837
- "- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
838
- ],
1216
+ extraCriticalRules: [],
839
1217
  postStartupNotes: [],
840
1218
  includeStdinNotificationSection: true,
841
1219
  messageNotificationStyle: "poll"
@@ -847,7 +1225,7 @@ var ClaudeDriver = class {
847
1225
  import { spawn as spawn2, execSync } from "child_process";
848
1226
  import { existsSync as existsSync2, readFileSync } from "fs";
849
1227
  import os from "os";
850
- import path3 from "path";
1228
+ import path4 from "path";
851
1229
  function getCodexNotificationErrorMessage(params) {
852
1230
  const topLevelMessage = params?.message;
853
1231
  if (typeof topLevelMessage === "string" && topLevelMessage.trim()) {
@@ -859,21 +1237,19 @@ function getCodexNotificationErrorMessage(params) {
859
1237
  }
860
1238
  return null;
861
1239
  }
862
- function ensureGitRepo(workingDirectory) {
863
- const gitDir = path3.join(workingDirectory, ".git");
864
- if (existsSync2(gitDir)) return;
865
- execSync("git init", { cwd: workingDirectory, stdio: "pipe" });
866
- execSync("git add -A && git commit --allow-empty -m 'init'", {
867
- cwd: workingDirectory,
868
- stdio: "pipe",
869
- env: {
870
- ...process.env,
871
- GIT_AUTHOR_NAME: "slock",
872
- GIT_AUTHOR_EMAIL: "slock@local",
873
- GIT_COMMITTER_NAME: "slock",
874
- GIT_COMMITTER_EMAIL: "slock@local"
875
- }
876
- });
1240
+ function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1241
+ const existsSyncFn = deps.existsSyncFn ?? existsSync2;
1242
+ const execSyncFn = deps.execSyncFn ?? execSync;
1243
+ const gitDir = path4.join(workingDirectory, ".git");
1244
+ if (existsSyncFn(gitDir)) return;
1245
+ execSyncFn("git init", { cwd: workingDirectory, stdio: "pipe" });
1246
+ execSyncFn(
1247
+ "git -c user.name=slock -c user.email=slock@local -c commit.gpgsign=false add -A && git -c user.name=slock -c user.email=slock@local -c commit.gpgsign=false commit --allow-empty -m 'init'",
1248
+ {
1249
+ cwd: workingDirectory,
1250
+ stdio: "pipe"
1251
+ }
1252
+ );
877
1253
  }
878
1254
  var CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
879
1255
  function resolveCodexCommand(deps = {}) {
@@ -908,14 +1284,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
908
1284
  let codexEntry = null;
909
1285
  try {
910
1286
  const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
911
- const candidate = path3.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1287
+ const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
912
1288
  if (existsSync2(candidate)) codexEntry = candidate;
913
1289
  } catch {
914
1290
  }
915
1291
  if (!codexEntry) {
916
1292
  try {
917
1293
  const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
918
- const candidate = path3.join(path3.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1294
+ const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
919
1295
  if (existsSync2(candidate)) codexEntry = candidate;
920
1296
  } catch {
921
1297
  }
@@ -930,12 +1306,6 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
930
1306
  args: [codexEntry, ...commandArgs]
931
1307
  };
932
1308
  }
933
- function buildBridgeArgs(ctx) {
934
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
935
- const command = isTsSource ? "npx" : "node";
936
- const args = 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];
937
- return { command, args };
938
- }
939
1309
  function joinReasoningText(item) {
940
1310
  const summary = Array.isArray(item.summary) ? item.summary.filter((entry) => typeof entry === "string") : [];
941
1311
  const content = Array.isArray(item.content) ? item.content.filter((entry) => typeof entry === "string") : [];
@@ -946,9 +1316,37 @@ var CodexDriver = class {
946
1316
  supportsStdinNotification = true;
947
1317
  mcpToolPrefix = "mcp_chat_";
948
1318
  busyDeliveryMode = "direct";
1319
+ supportsNativeStandingPrompt = true;
949
1320
  probe() {
950
1321
  return probeCodex();
951
1322
  }
1323
+ buildThreadRequest(ctx) {
1324
+ const threadParams = {
1325
+ cwd: ctx.workingDirectory,
1326
+ approvalPolicy: "never",
1327
+ sandbox: "danger-full-access",
1328
+ developerInstructions: ctx.standingPrompt
1329
+ };
1330
+ if (ctx.config.model) {
1331
+ threadParams.model = ctx.config.model;
1332
+ }
1333
+ if (ctx.config.reasoningEffort) {
1334
+ threadParams.config = { model_reasoning_effort: ctx.config.reasoningEffort };
1335
+ }
1336
+ if (ctx.config.sessionId) {
1337
+ return {
1338
+ method: "thread/resume",
1339
+ params: {
1340
+ threadId: ctx.config.sessionId,
1341
+ ...threadParams
1342
+ }
1343
+ };
1344
+ }
1345
+ return {
1346
+ method: "thread/start",
1347
+ params: threadParams
1348
+ };
1349
+ }
952
1350
  process = null;
953
1351
  requestId = 0;
954
1352
  threadId = null;
@@ -961,7 +1359,8 @@ var CodexDriver = class {
961
1359
  streamedAgentMessageIds = /* @__PURE__ */ new Set();
962
1360
  streamedReasoningIds = /* @__PURE__ */ new Set();
963
1361
  spawn(ctx) {
964
- ensureGitRepo(ctx.workingDirectory);
1362
+ ensureGitRepoForCodex(ctx.workingDirectory);
1363
+ const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" });
965
1364
  this.process = null;
966
1365
  this.requestId = 0;
967
1366
  this.threadId = ctx.config.sessionId || null;
@@ -973,26 +1372,8 @@ var CodexDriver = class {
973
1372
  this.sessionAnnounced = false;
974
1373
  this.streamedAgentMessageIds.clear();
975
1374
  this.streamedReasoningIds.clear();
976
- const bridge = buildBridgeArgs(ctx);
977
- const args = [
978
- "app-server",
979
- "--listen",
980
- "stdio://",
981
- "-c",
982
- `mcp_servers.chat.command=${JSON.stringify(bridge.command)}`,
983
- "-c",
984
- `mcp_servers.chat.args=${JSON.stringify(bridge.args)}`,
985
- "-c",
986
- "mcp_servers.chat.startup_timeout_sec=30",
987
- "-c",
988
- "mcp_servers.chat.tool_timeout_sec=300",
989
- "-c",
990
- "mcp_servers.chat.enabled=true",
991
- "-c",
992
- "mcp_servers.chat.required=true"
993
- ];
1375
+ const args = ["app-server", "--listen", "stdio://"];
994
1376
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
995
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
996
1377
  const proc = spawn2(command, spawnArgs, {
997
1378
  cwd: ctx.workingDirectory,
998
1379
  stdio: ["pipe", "pipe", "pipe"],
@@ -1005,31 +1386,7 @@ var CodexDriver = class {
1005
1386
  clientInfo: { name: "slock-daemon", version: "1.0.0" },
1006
1387
  capabilities: { experimentalApi: true }
1007
1388
  });
1008
- const threadParams = {
1009
- cwd: ctx.workingDirectory,
1010
- approvalPolicy: "never",
1011
- sandbox: "danger-full-access"
1012
- };
1013
- if (ctx.config.model) {
1014
- threadParams.model = ctx.config.model;
1015
- }
1016
- if (ctx.config.reasoningEffort) {
1017
- threadParams.config = { model_reasoning_effort: ctx.config.reasoningEffort };
1018
- }
1019
- if (ctx.config.sessionId) {
1020
- this.pendingThreadRequest = {
1021
- method: "thread/resume",
1022
- params: {
1023
- threadId: ctx.config.sessionId,
1024
- ...threadParams
1025
- }
1026
- };
1027
- } else {
1028
- this.pendingThreadRequest = {
1029
- method: "thread/start",
1030
- params: threadParams
1031
- };
1032
- }
1389
+ this.pendingThreadRequest = this.buildThreadRequest(ctx);
1033
1390
  });
1034
1391
  return { process: proc };
1035
1392
  }
@@ -1145,6 +1502,14 @@ var CodexDriver = class {
1145
1502
  events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
1146
1503
  }
1147
1504
  break;
1505
+ case "contextCompaction":
1506
+ if (isStarted) {
1507
+ events.push({ kind: "compaction_started" });
1508
+ }
1509
+ if (isCompleted) {
1510
+ events.push({ kind: "compaction_finished" });
1511
+ }
1512
+ break;
1148
1513
  case "fileChange":
1149
1514
  if (isStarted && Array.isArray(item.changes)) {
1150
1515
  for (const change of item.changes) {
@@ -1225,11 +1590,9 @@ var CodexDriver = class {
1225
1590
  });
1226
1591
  }
1227
1592
  buildSystemPrompt(config, _agentId) {
1228
- return buildBaseSystemPrompt(config, {
1593
+ return buildCliTransportSystemPrompt(config, {
1229
1594
  toolPrefix: "",
1230
- extraCriticalRules: [
1231
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
1232
- ],
1595
+ extraCriticalRules: [],
1233
1596
  postStartupNotes: [
1234
1597
  "**IMPORTANT**: Your process stays alive across turns. New messages may be delivered directly into the current thread while you are working."
1235
1598
  ],
@@ -1281,8 +1644,8 @@ var CodexDriver = class {
1281
1644
  }
1282
1645
  };
1283
1646
  function detectCodexModels(home = os.homedir()) {
1284
- const cachePath = path3.join(home, ".codex", "models_cache.json");
1285
- const configPath = path3.join(home, ".codex", "config.toml");
1647
+ const cachePath = path4.join(home, ".codex", "models_cache.json");
1648
+ const configPath = path4.join(home, ".codex", "config.toml");
1286
1649
  let models = [];
1287
1650
  try {
1288
1651
  const raw = readFileSync(cachePath, "utf8");
@@ -1312,7 +1675,7 @@ function detectCodexModels(home = os.homedir()) {
1312
1675
 
1313
1676
  // src/drivers/copilot.ts
1314
1677
  import { spawn as spawn3 } from "child_process";
1315
- import path4 from "path";
1678
+ import path5 from "path";
1316
1679
  import { writeFileSync as writeFileSync2 } from "fs";
1317
1680
  var CopilotDriver = class {
1318
1681
  id = "copilot";
@@ -1327,7 +1690,7 @@ var CopilotDriver = class {
1327
1690
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1328
1691
  const mcpCommand = isTsSource ? "npx" : "node";
1329
1692
  const mcpArgs = 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];
1330
- const mcpConfigPath = path4.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
1693
+ const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
1331
1694
  writeFileSync2(mcpConfigPath, JSON.stringify({
1332
1695
  mcpServers: {
1333
1696
  chat: {
@@ -1436,7 +1799,7 @@ var CopilotDriver = class {
1436
1799
  return null;
1437
1800
  }
1438
1801
  buildSystemPrompt(config, _agentId) {
1439
- return buildBaseSystemPrompt(config, {
1802
+ return buildMcpSystemPrompt(config, {
1440
1803
  toolPrefix: "",
1441
1804
  extraCriticalRules: [
1442
1805
  "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
@@ -1450,22 +1813,22 @@ var CopilotDriver = class {
1450
1813
 
1451
1814
  // src/drivers/cursor.ts
1452
1815
  import { spawn as spawn4 } from "child_process";
1453
- import { writeFileSync as writeFileSync3, mkdirSync, existsSync as existsSync3 } from "fs";
1454
- import path5 from "path";
1816
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
1817
+ import path6 from "path";
1455
1818
  var CursorDriver = class {
1456
1819
  id = "cursor";
1457
1820
  supportsStdinNotification = false;
1458
1821
  mcpToolPrefix = "mcp__chat__";
1459
1822
  busyDeliveryMode = "none";
1460
1823
  spawn(ctx) {
1461
- const cursorDir = path5.join(ctx.workingDirectory, ".cursor");
1824
+ const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
1462
1825
  if (!existsSync3(cursorDir)) {
1463
- mkdirSync(cursorDir, { recursive: true });
1826
+ mkdirSync2(cursorDir, { recursive: true });
1464
1827
  }
1465
1828
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1466
1829
  const mcpCommand = isTsSource ? "npx" : "node";
1467
1830
  const mcpArgs = 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];
1468
- const mcpConfigPath = path5.join(cursorDir, "mcp.json");
1831
+ const mcpConfigPath = path6.join(cursorDir, "mcp.json");
1469
1832
  writeFileSync3(mcpConfigPath, JSON.stringify({
1470
1833
  mcpServers: {
1471
1834
  chat: {
@@ -1510,6 +1873,10 @@ var CursorDriver = class {
1510
1873
  case "system":
1511
1874
  if (event.subtype === "init" && event.session_id) {
1512
1875
  events.push({ kind: "session_init", sessionId: event.session_id });
1876
+ } else if (event.subtype === "status" && event.status === "compacting") {
1877
+ events.push({ kind: "compaction_started" });
1878
+ } else if (event.subtype === "compact_boundary") {
1879
+ events.push({ kind: "compaction_finished" });
1513
1880
  }
1514
1881
  break;
1515
1882
  case "assistant": {
@@ -1552,7 +1919,7 @@ var CursorDriver = class {
1552
1919
  return null;
1553
1920
  }
1554
1921
  buildSystemPrompt(config, _agentId) {
1555
- return buildBaseSystemPrompt(config, {
1922
+ return buildMcpSystemPrompt(config, {
1556
1923
  toolPrefix: "mcp__chat__",
1557
1924
  extraCriticalRules: [
1558
1925
  "- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
@@ -1566,8 +1933,8 @@ var CursorDriver = class {
1566
1933
 
1567
1934
  // src/drivers/gemini.ts
1568
1935
  import { spawn as spawn5 } from "child_process";
1569
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
1570
- import path6 from "path";
1936
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
1937
+ import path7 from "path";
1571
1938
  var GeminiDriver = class {
1572
1939
  id = "gemini";
1573
1940
  supportsStdinNotification = false;
@@ -1578,14 +1945,14 @@ var GeminiDriver = class {
1578
1945
  spawn(ctx) {
1579
1946
  this.sessionId = ctx.config.sessionId || null;
1580
1947
  this.sessionAnnounced = false;
1581
- const geminiDir = path6.join(ctx.workingDirectory, ".gemini");
1948
+ const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
1582
1949
  if (!existsSync4(geminiDir)) {
1583
- mkdirSync2(geminiDir, { recursive: true });
1950
+ mkdirSync3(geminiDir, { recursive: true });
1584
1951
  }
1585
1952
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1586
1953
  const mcpCommand = isTsSource ? "npx" : "node";
1587
1954
  const mcpArgs = 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];
1588
- const settingsPath = path6.join(geminiDir, "settings.json");
1955
+ const settingsPath = path7.join(geminiDir, "settings.json");
1589
1956
  writeFileSync4(settingsPath, JSON.stringify({
1590
1957
  mcpServers: {
1591
1958
  chat: {
@@ -1667,7 +2034,7 @@ var GeminiDriver = class {
1667
2034
  return null;
1668
2035
  }
1669
2036
  buildSystemPrompt(config, _agentId) {
1670
- return buildBaseSystemPrompt(config, {
2037
+ return buildMcpSystemPrompt(config, {
1671
2038
  toolPrefix: "",
1672
2039
  extraCriticalRules: [
1673
2040
  "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
@@ -1684,7 +2051,7 @@ import { randomUUID } from "crypto";
1684
2051
  import { spawn as spawn6 } from "child_process";
1685
2052
  import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
1686
2053
  import os2 from "os";
1687
- import path7 from "path";
2054
+ import path8 from "path";
1688
2055
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
1689
2056
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
1690
2057
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
@@ -1713,9 +2080,9 @@ var KimiDriver = class {
1713
2080
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1714
2081
  const command = isTsSource ? "npx" : "node";
1715
2082
  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];
1716
- const systemPromptPath = path7.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
1717
- const agentFilePath = path7.join(ctx.workingDirectory, KIMI_AGENT_FILE);
1718
- const mcpConfigPath = path7.join(ctx.workingDirectory, KIMI_MCP_FILE);
2083
+ const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
2084
+ const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
2085
+ const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
1719
2086
  if (!isResume || !existsSync5(systemPromptPath)) {
1720
2087
  writeFileSync5(systemPromptPath, ctx.prompt, "utf8");
1721
2088
  }
@@ -1796,6 +2163,12 @@ var KimiDriver = class {
1796
2163
  case "StepBegin":
1797
2164
  events.push({ kind: "thinking", text: "" });
1798
2165
  break;
2166
+ case "CompactionBegin":
2167
+ events.push({ kind: "compaction_started" });
2168
+ break;
2169
+ case "CompactionEnd":
2170
+ events.push({ kind: "compaction_finished" });
2171
+ break;
1799
2172
  case "ContentPart":
1800
2173
  if (payload.type === "think" && payload.think) {
1801
2174
  events.push({ kind: "thinking", text: payload.think });
@@ -1848,7 +2221,7 @@ var KimiDriver = class {
1848
2221
  });
1849
2222
  }
1850
2223
  buildSystemPrompt(config, _agentId) {
1851
- return buildBaseSystemPrompt(config, {
2224
+ return buildMcpSystemPrompt(config, {
1852
2225
  toolPrefix: "",
1853
2226
  extraCriticalRules: [
1854
2227
  "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
@@ -1863,7 +2236,7 @@ var KimiDriver = class {
1863
2236
  }
1864
2237
  };
1865
2238
  function detectKimiModels(home = os2.homedir()) {
1866
- const configPath = path7.join(home, ".kimi", "config.toml");
2239
+ const configPath = path8.join(home, ".kimi", "config.toml");
1867
2240
  let raw;
1868
2241
  try {
1869
2242
  raw = readFileSync2(configPath, "utf8");
@@ -1908,7 +2281,7 @@ function getDriver(runtimeId) {
1908
2281
 
1909
2282
  // src/workspaces.ts
1910
2283
  import { readdir, rm, stat } from "fs/promises";
1911
- import path8 from "path";
2284
+ import path9 from "path";
1912
2285
  function isValidWorkspaceDirectoryName(directoryName) {
1913
2286
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
1914
2287
  }
@@ -1916,7 +2289,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
1916
2289
  if (!isValidWorkspaceDirectoryName(directoryName)) {
1917
2290
  return null;
1918
2291
  }
1919
- return path8.join(dataDir, directoryName);
2292
+ return path9.join(dataDir, directoryName);
1920
2293
  }
1921
2294
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
1922
2295
  return {
@@ -1965,7 +2338,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
1965
2338
  return summary;
1966
2339
  }
1967
2340
  const childSummaries = await Promise.all(
1968
- entries.map((entry) => summarizeWorkspaceEntry(path8.join(dirPath, entry.name), entry))
2341
+ entries.map((entry) => summarizeWorkspaceEntry(path9.join(dirPath, entry.name), entry))
1969
2342
  );
1970
2343
  for (const childSummary of childSummaries) {
1971
2344
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -1984,7 +2357,7 @@ async function scanWorkspaceDirectories(dataDir) {
1984
2357
  if (!entry.isDirectory()) {
1985
2358
  return null;
1986
2359
  }
1987
- const dirPath = path8.join(dataDir, entry.name);
2360
+ const dirPath = path9.join(dataDir, entry.name);
1988
2361
  try {
1989
2362
  const summary = await summarizeWorkspaceDirectory(dirPath);
1990
2363
  return {
@@ -2016,7 +2389,7 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
2016
2389
  }
2017
2390
 
2018
2391
  // src/agentProcessManager.ts
2019
- var DATA_DIR = path9.join(os3.homedir(), ".slock", "agents");
2392
+ var DATA_DIR = path10.join(os3.homedir(), ".slock", "agents");
2020
2393
  function toLocalTime(iso) {
2021
2394
  const d = new Date(iso);
2022
2395
  if (isNaN(d.getTime())) return iso;
@@ -2170,6 +2543,7 @@ function getBusyDeliveryNote(driver) {
2170
2543
  }
2171
2544
  return "\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.";
2172
2545
  }
2546
+ var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
2173
2547
  var AgentProcessManager = class _AgentProcessManager {
2174
2548
  agents = /* @__PURE__ */ new Map();
2175
2549
  agentsStarting = /* @__PURE__ */ new Set();
@@ -2178,6 +2552,7 @@ var AgentProcessManager = class _AgentProcessManager {
2178
2552
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
2179
2553
  idleAgentConfigs = /* @__PURE__ */ new Map();
2180
2554
  chatBridgePath;
2555
+ slockCliPath;
2181
2556
  sendToServer;
2182
2557
  daemonApiKey;
2183
2558
  serverUrl;
@@ -2186,6 +2561,7 @@ var AgentProcessManager = class _AgentProcessManager {
2186
2561
  defaultAgentEnvVarsProvider;
2187
2562
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
2188
2563
  this.chatBridgePath = chatBridgePath;
2564
+ this.slockCliPath = opts.slockCliPath ?? "";
2189
2565
  this.sendToServer = sendToServer;
2190
2566
  this.daemonApiKey = daemonApiKey;
2191
2567
  this.serverUrl = opts.serverUrl;
@@ -2205,9 +2581,9 @@ var AgentProcessManager = class _AgentProcessManager {
2205
2581
  this.agentsStarting.add(agentId);
2206
2582
  try {
2207
2583
  const driver = this.driverResolver(config.runtime || "claude");
2208
- const agentDataDir = path9.join(this.dataDir, agentId);
2584
+ const agentDataDir = path10.join(this.dataDir, agentId);
2209
2585
  await mkdir(agentDataDir, { recursive: true });
2210
- const memoryMdPath = path9.join(agentDataDir, "MEMORY.md");
2586
+ const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
2211
2587
  try {
2212
2588
  await access(memoryMdPath);
2213
2589
  } catch {
@@ -2225,8 +2601,9 @@ ${config.description || "No role defined yet."}
2225
2601
  `;
2226
2602
  await writeFile(memoryMdPath, initialMemoryMd);
2227
2603
  }
2228
- await mkdir(path9.join(agentDataDir, "notes"), { recursive: true });
2604
+ await mkdir(path10.join(agentDataDir, "notes"), { recursive: true });
2229
2605
  const isResume = !!config.sessionId;
2606
+ const standingPrompt = driver.buildSystemPrompt(config, agentId);
2230
2607
  let prompt;
2231
2608
  if (isResume && resumePrompt) {
2232
2609
  prompt = resumePrompt;
@@ -2270,15 +2647,17 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2270
2647
  prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
2271
2648
  prompt += getBusyDeliveryNote(driver);
2272
2649
  } else {
2273
- prompt = driver.buildSystemPrompt(config, agentId);
2650
+ prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
2274
2651
  }
2275
2652
  const effectiveConfig = await this.buildSpawnConfig(agentId, config);
2276
2653
  const { process: proc } = driver.spawn({
2277
2654
  agentId,
2278
2655
  config: effectiveConfig,
2656
+ standingPrompt,
2279
2657
  prompt,
2280
2658
  workingDirectory: agentDataDir,
2281
2659
  chatBridgePath: this.chatBridgePath,
2660
+ slockCliPath: this.slockCliPath,
2282
2661
  daemonApiKey: this.daemonApiKey
2283
2662
  });
2284
2663
  const agentProcess = {
@@ -2566,7 +2945,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2566
2945
  }
2567
2946
  }
2568
2947
  async resetWorkspace(agentId) {
2569
- const agentDataDir = path9.join(this.dataDir, agentId);
2948
+ const agentDataDir = path10.join(this.dataDir, agentId);
2570
2949
  try {
2571
2950
  await rm2(agentDataDir, { recursive: true, force: true });
2572
2951
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -2604,7 +2983,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2604
2983
  }
2605
2984
  // Workspace file browsing
2606
2985
  async getFileTree(agentId, dirPath) {
2607
- const agentDir = path9.join(this.dataDir, agentId);
2986
+ const agentDir = path10.join(this.dataDir, agentId);
2608
2987
  try {
2609
2988
  await stat2(agentDir);
2610
2989
  } catch {
@@ -2612,8 +2991,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2612
2991
  }
2613
2992
  let targetDir = agentDir;
2614
2993
  if (dirPath) {
2615
- const resolved = path9.resolve(agentDir, dirPath);
2616
- if (!resolved.startsWith(agentDir + path9.sep) && resolved !== agentDir) {
2994
+ const resolved = path10.resolve(agentDir, dirPath);
2995
+ if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
2617
2996
  return [];
2618
2997
  }
2619
2998
  targetDir = resolved;
@@ -2621,9 +3000,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2621
3000
  return this.listDirectoryChildren(targetDir, agentDir);
2622
3001
  }
2623
3002
  async readFile(agentId, filePath) {
2624
- const agentDir = path9.join(this.dataDir, agentId);
2625
- const resolved = path9.resolve(agentDir, filePath);
2626
- if (!resolved.startsWith(agentDir + path9.sep) && resolved !== agentDir) {
3003
+ const agentDir = path10.join(this.dataDir, agentId);
3004
+ const resolved = path10.resolve(agentDir, filePath);
3005
+ if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
2627
3006
  throw new Error("Access denied");
2628
3007
  }
2629
3008
  const info = await stat2(resolved);
@@ -2647,7 +3026,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2647
3026
  ".sh",
2648
3027
  ".py"
2649
3028
  ]);
2650
- const ext = path9.extname(resolved).toLowerCase();
3029
+ const ext = path10.extname(resolved).toLowerCase();
2651
3030
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
2652
3031
  return { content: null, binary: true };
2653
3032
  }
@@ -2674,13 +3053,13 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2674
3053
  const agent = this.agents.get(agentId);
2675
3054
  const runtime = runtimeHint || agent?.config.runtime || "claude";
2676
3055
  const home = os3.homedir();
2677
- const workspaceDir = path9.join(this.dataDir, agentId);
3056
+ const workspaceDir = path10.join(this.dataDir, agentId);
2678
3057
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
2679
3058
  const globalResults = await Promise.all(
2680
- paths.global.map((p) => this.scanSkillsDir(path9.join(home, p)))
3059
+ paths.global.map((p) => this.scanSkillsDir(path10.join(home, p)))
2681
3060
  );
2682
3061
  const workspaceResults = await Promise.all(
2683
- paths.workspace.map((p) => this.scanSkillsDir(path9.join(workspaceDir, p)))
3062
+ paths.workspace.map((p) => this.scanSkillsDir(path10.join(workspaceDir, p)))
2684
3063
  );
2685
3064
  const dedup = (skills) => {
2686
3065
  const seen = /* @__PURE__ */ new Set();
@@ -2709,7 +3088,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2709
3088
  const skills = [];
2710
3089
  for (const entry of entries) {
2711
3090
  if (entry.isDirectory() || entry.isSymbolicLink()) {
2712
- const skillMd = path9.join(dir, entry.name, "SKILL.md");
3091
+ const skillMd = path10.join(dir, entry.name, "SKILL.md");
2713
3092
  try {
2714
3093
  const content = await readFile(skillMd, "utf-8");
2715
3094
  const skill = this.parseSkillMd(entry.name, content);
@@ -2720,7 +3099,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2720
3099
  } else if (entry.name.endsWith(".md")) {
2721
3100
  const cmdName = entry.name.replace(/\.md$/, "");
2722
3101
  try {
2723
- const content = await readFile(path9.join(dir, entry.name), "utf-8");
3102
+ const content = await readFile(path10.join(dir, entry.name), "utf-8");
2724
3103
  const skill = this.parseSkillMd(cmdName, content);
2725
3104
  skill.sourcePath = dir;
2726
3105
  skills.push(skill);
@@ -2852,13 +3231,27 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2852
3231
  }
2853
3232
  case "tool_call": {
2854
3233
  this.flushPendingTrajectory(agentId);
2855
- const toolName = event.name;
2856
- const inputSummary = summarizeToolInput(toolName, event.input);
2857
- const detail = getToolActivityLabel(toolName);
2858
- this.broadcastActivity(agentId, "working", detail, [{ kind: "tool_start", toolName, toolInput: inputSummary }]);
3234
+ const invocation = normalizeToolDisplayInvocation(event.name, event.input);
3235
+ const inputSummary = summarizeToolInput(invocation.toolName, invocation.input);
3236
+ const detail = getToolActivityLabel(invocation.toolName);
3237
+ this.broadcastActivity(agentId, "working", detail, [{
3238
+ kind: "tool_start",
3239
+ toolName: invocation.toolName,
3240
+ toolInput: inputSummary
3241
+ }]);
2859
3242
  if (ap) ap.isIdle = false;
2860
3243
  break;
2861
3244
  }
3245
+ case "compaction_started":
3246
+ this.flushPendingTrajectory(agentId);
3247
+ this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
3248
+ if (ap) ap.isIdle = false;
3249
+ break;
3250
+ case "compaction_finished":
3251
+ this.flushPendingTrajectory(agentId);
3252
+ this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
3253
+ if (ap) ap.isIdle = false;
3254
+ break;
2862
3255
  case "turn_end":
2863
3256
  this.flushPendingTrajectory(agentId);
2864
3257
  if (ap) {
@@ -2965,8 +3358,8 @@ Respond as appropriate. Complete all your work before stopping.`;
2965
3358
  const nodes = [];
2966
3359
  for (const entry of entries) {
2967
3360
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2968
- const fullPath = path9.join(dir, entry.name);
2969
- const relativePath = path9.relative(rootDir, fullPath);
3361
+ const fullPath = path10.join(dir, entry.name);
3362
+ const relativePath = path10.relative(rootDir, fullPath);
2970
3363
  let info;
2971
3364
  try {
2972
3365
  info = await stat2(fullPath);
@@ -3116,6 +3509,85 @@ var DaemonConnection = class {
3116
3509
  }
3117
3510
  };
3118
3511
 
3512
+ // src/reminderCache.ts
3513
+ var DEFAULT_MAX_DELAY_MS = 24 * 60 * 60 * 1e3;
3514
+ var ReminderCache = class {
3515
+ entries = /* @__PURE__ */ new Map();
3516
+ clock;
3517
+ onFire;
3518
+ maxDelayMs;
3519
+ constructor(opts) {
3520
+ this.clock = opts.clock ?? systemClock;
3521
+ this.onFire = opts.onFire;
3522
+ this.maxDelayMs = opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
3523
+ }
3524
+ upsert(job) {
3525
+ const existing = this.entries.get(job.reminderId);
3526
+ if (existing && existing.job.version >= job.version) {
3527
+ logger.info(`[ReminderCache] Stale upsert for ${job.reminderId} (incoming v${job.version} <= cached v${existing.job.version}) \u2014 ignored`);
3528
+ return;
3529
+ }
3530
+ if (existing?.timer) this.clock.clearTimeout(existing.timer);
3531
+ const timer = this.scheduleTimer(job);
3532
+ this.entries.set(job.reminderId, { job, timer });
3533
+ }
3534
+ cancel(reminderId, version) {
3535
+ const existing = this.entries.get(reminderId);
3536
+ if (!existing) return;
3537
+ if (existing.job.version > version) {
3538
+ logger.info(`[ReminderCache] Stale cancel for ${reminderId} (incoming v${version} < cached v${existing.job.version}) \u2014 ignored`);
3539
+ return;
3540
+ }
3541
+ if (existing.timer) this.clock.clearTimeout(existing.timer);
3542
+ this.entries.delete(reminderId);
3543
+ }
3544
+ snapshot(agentId, jobs) {
3545
+ for (const [reminderId, entry] of this.entries) {
3546
+ if (entry.job.ownerAgentId !== agentId) continue;
3547
+ if (entry.timer) this.clock.clearTimeout(entry.timer);
3548
+ this.entries.delete(reminderId);
3549
+ }
3550
+ for (const job of jobs) {
3551
+ if (job.ownerAgentId !== agentId) {
3552
+ logger.warn(
3553
+ `[ReminderCache] snapshot for agent ${agentId} carried job ${job.reminderId} owned by ${job.ownerAgentId} \u2014 skipping`
3554
+ );
3555
+ continue;
3556
+ }
3557
+ const timer = this.scheduleTimer(job);
3558
+ this.entries.set(job.reminderId, { job, timer });
3559
+ }
3560
+ }
3561
+ clear() {
3562
+ for (const entry of this.entries.values()) {
3563
+ if (entry.timer) this.clock.clearTimeout(entry.timer);
3564
+ }
3565
+ this.entries.clear();
3566
+ }
3567
+ size() {
3568
+ return this.entries.size;
3569
+ }
3570
+ getJob(reminderId) {
3571
+ return this.entries.get(reminderId)?.job ?? null;
3572
+ }
3573
+ scheduleTimer(job) {
3574
+ const fireAt = Date.parse(job.fireAt);
3575
+ if (Number.isNaN(fireAt)) {
3576
+ logger.warn(`[ReminderCache] Invalid fireAt for ${job.reminderId}: ${job.fireAt}`);
3577
+ return null;
3578
+ }
3579
+ const delay = Math.max(0, Math.min(this.maxDelayMs, fireAt - this.clock.now()));
3580
+ return this.clock.setTimeout(() => {
3581
+ this.entries.delete(job.reminderId);
3582
+ try {
3583
+ this.onFire(job);
3584
+ } catch (err) {
3585
+ logger.error(`[ReminderCache] onFire threw for ${job.reminderId}`, err);
3586
+ }
3587
+ }, delay);
3588
+ }
3589
+ };
3590
+
3119
3591
  // src/core.ts
3120
3592
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
3121
3593
  function parseDaemonCliArgs(args) {
@@ -3137,13 +3609,25 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
3137
3609
  }
3138
3610
  }
3139
3611
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
3140
- const dirname = path10.dirname(fileURLToPath(moduleUrl));
3141
- const jsPath = path10.resolve(dirname, "chat-bridge.js");
3612
+ const dirname = path11.dirname(fileURLToPath(moduleUrl));
3613
+ const jsPath = path11.resolve(dirname, "chat-bridge.js");
3142
3614
  try {
3143
3615
  accessSync(jsPath);
3144
3616
  return jsPath;
3145
3617
  } catch {
3146
- return path10.resolve(dirname, "chat-bridge.ts");
3618
+ return path11.resolve(dirname, "chat-bridge.ts");
3619
+ }
3620
+ }
3621
+ function resolveSlockCliPath(moduleUrl = import.meta.url) {
3622
+ const thisDir = path11.dirname(fileURLToPath(moduleUrl));
3623
+ const bundledDistPath = path11.resolve(thisDir, "cli", "index.js");
3624
+ try {
3625
+ accessSync(bundledDistPath);
3626
+ return bundledDistPath;
3627
+ } catch {
3628
+ const workspaceDistPath = path11.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
3629
+ accessSync(workspaceDistPath);
3630
+ return workspaceDistPath;
3147
3631
  }
3148
3632
  }
3149
3633
  function detectRuntimes() {
@@ -3196,6 +3680,12 @@ function summarizeIncomingMessage(msg) {
3196
3680
  return `(directory=${msg.directoryName})`;
3197
3681
  case "machine:runtime_models:detect":
3198
3682
  return `(runtime=${msg.runtime}, req=${msg.requestId})`;
3683
+ case "reminder.upsert":
3684
+ return `(agent=${msg.agentId}, id=${msg.reminder.reminderId}, v${msg.reminder.version}, fireAt=${msg.reminder.fireAt})`;
3685
+ case "reminder.cancel":
3686
+ return `(agent=${msg.agentId}, id=${msg.reminderId}, v${msg.version})`;
3687
+ case "reminder.snapshot":
3688
+ return `(agent=${msg.agentId}, count=${msg.reminders.length})`;
3199
3689
  default:
3200
3690
  return "";
3201
3691
  }
@@ -3204,19 +3694,27 @@ var DaemonCore = class {
3204
3694
  options;
3205
3695
  daemonVersion;
3206
3696
  chatBridgePath;
3697
+ slockCliPath;
3207
3698
  runtimeDetector;
3208
3699
  agentManager;
3209
3700
  connection;
3701
+ reminderCache;
3210
3702
  constructor(options) {
3211
3703
  this.options = options;
3212
3704
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
3213
3705
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
3706
+ this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
3214
3707
  this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
3708
+ this.reminderCache = new ReminderCache({
3709
+ clock: options.reminderClock,
3710
+ onFire: (job) => this.onReminderFire(job)
3711
+ });
3215
3712
  let connection;
3216
3713
  const agentManagerOptions = {
3217
3714
  dataDir: options.dataDir,
3218
3715
  serverUrl: options.serverUrl,
3219
- defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider
3716
+ defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
3717
+ slockCliPath: this.slockCliPath
3220
3718
  };
3221
3719
  this.agentManager = options.agentManagerFactory ? options.agentManagerFactory(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions);
3222
3720
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
@@ -3236,6 +3734,7 @@ var DaemonCore = class {
3236
3734
  }
3237
3735
  async stop() {
3238
3736
  logger.info("[Slock Daemon] Shutting down...");
3737
+ this.reminderCache.clear();
3239
3738
  await this.agentManager.stopAll();
3240
3739
  this.connection.disconnect();
3241
3740
  }
@@ -3330,11 +3829,31 @@ var DaemonCore = class {
3330
3829
  });
3331
3830
  break;
3332
3831
  }
3832
+ case "reminder.upsert":
3833
+ this.reminderCache.upsert(msg.reminder);
3834
+ break;
3835
+ case "reminder.cancel":
3836
+ this.reminderCache.cancel(msg.reminderId, msg.version);
3837
+ break;
3838
+ case "reminder.snapshot":
3839
+ logger.info(`[Daemon] Reminder snapshot for agent ${msg.agentId}: ${msg.reminders.length} entries`);
3840
+ this.reminderCache.snapshot(msg.agentId, msg.reminders);
3841
+ break;
3333
3842
  case "ping":
3334
3843
  this.connection.send({ type: "pong" });
3335
3844
  break;
3336
3845
  }
3337
3846
  }
3847
+ onReminderFire(job) {
3848
+ logger.info(`[Daemon] Reminder ${job.reminderId} fired locally (agent=${job.ownerAgentId})`);
3849
+ this.connection.send({
3850
+ type: "reminder.fire_attempt",
3851
+ agentId: job.ownerAgentId,
3852
+ reminderId: job.reminderId,
3853
+ version: job.version,
3854
+ firedAtClient: (/* @__PURE__ */ new Date()).toISOString()
3855
+ });
3856
+ }
3338
3857
  handleConnect() {
3339
3858
  const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
3340
3859
  const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
@@ -3358,6 +3877,13 @@ var DaemonCore = class {
3358
3877
  for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
3359
3878
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
3360
3879
  }
3880
+ const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
3881
+ for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
3882
+ agentsForSnapshot.add(agentId);
3883
+ }
3884
+ for (const agentId of agentsForSnapshot) {
3885
+ this.connection.send({ type: "reminder.snapshot.request", agentId });
3886
+ }
3361
3887
  this.options.lifecycleHooks?.onConnect?.();
3362
3888
  }
3363
3889
  handleDisconnect() {
@@ -3374,6 +3900,7 @@ export {
3374
3900
  parseDaemonCliArgs,
3375
3901
  readDaemonVersion,
3376
3902
  resolveChatBridgePath,
3903
+ resolveSlockCliPath,
3377
3904
  detectRuntimes,
3378
3905
  DaemonCore
3379
3906
  };