@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.
- package/dist/chat-bridge.js +164 -0
- package/dist/{chunk-72UW4SIL.js → chunk-SDJ4NOR7.js} +795 -268
- package/dist/cli/index.js +1181 -0
- package/dist/core.js +3 -1
- package/dist/index.js +1 -1
- package/package.json +16 -16
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-E6OOH3IC.js";
|
|
5
5
|
|
|
6
6
|
// src/core.ts
|
|
7
|
-
import
|
|
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
|
|
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
|
|
279
|
-
|
|
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
|
|
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
|
-
|
|
505
|
+
const criticalRules = isCli ? [
|
|
506
|
+
"- Always communicate through `slock` CLI commands. This is your only output channel.",
|
|
290
507
|
...opts.extraCriticalRules,
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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 ${
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
554
|
+
You have MCP tools from the "chat" server. Use ONLY these for communication:
|
|
306
555
|
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
592
|
+
Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
|
|
328
593
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
350
|
-
-
|
|
351
|
-
-
|
|
352
|
-
- \`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
+
Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
|
|
365
634
|
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
653
|
+
**Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
|
|
390
654
|
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
424
|
-
-
|
|
425
|
-
- Typical uses for
|
|
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
|
|
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
|
|
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
|
|
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
|
|
449
|
-
- Your display name is
|
|
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
|
|
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
|
|
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
|
-
|
|
904
|
+
${notifyExample}
|
|
571
905
|
|
|
572
906
|
How to handle these:
|
|
573
|
-
- Call
|
|
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
|
-
-
|
|
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
|
|
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
|
|
1041
|
+
return path2.join(homeDir, relativePath);
|
|
657
1042
|
}
|
|
658
1043
|
|
|
659
1044
|
// src/drivers/claude.ts
|
|
660
|
-
var CLAUDE_DESKTOP_CLI_RELATIVE_PATH =
|
|
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
|
-
|
|
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
|
-
"--
|
|
723
|
-
|
|
1089
|
+
"--append-system-prompt",
|
|
1090
|
+
standingPrompt,
|
|
724
1091
|
"--model",
|
|
725
|
-
|
|
1092
|
+
config.model || "sonnet",
|
|
726
1093
|
"--disallowed-tools",
|
|
727
|
-
|
|
1094
|
+
CLAUDE_DISALLOWED_TOOLS
|
|
728
1095
|
];
|
|
729
|
-
if (
|
|
730
|
-
args.push("--resume",
|
|
1096
|
+
if (config.sessionId) {
|
|
1097
|
+
args.push("--resume", config.sessionId);
|
|
731
1098
|
}
|
|
732
|
-
|
|
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
|
|
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
|
|
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
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1285
|
-
const configPath =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
1824
|
+
const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
|
|
1462
1825
|
if (!existsSync3(cursorDir)) {
|
|
1463
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1570
|
-
import
|
|
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 =
|
|
1948
|
+
const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
|
|
1582
1949
|
if (!existsSync4(geminiDir)) {
|
|
1583
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
1717
|
-
const agentFilePath =
|
|
1718
|
-
const mcpConfigPath =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2584
|
+
const agentDataDir = path10.join(this.dataDir, agentId);
|
|
2209
2585
|
await mkdir(agentDataDir, { recursive: true });
|
|
2210
|
-
const memoryMdPath =
|
|
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(
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2616
|
-
if (!resolved.startsWith(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 =
|
|
2625
|
-
const resolved =
|
|
2626
|
-
if (!resolved.startsWith(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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
2856
|
-
const inputSummary = summarizeToolInput(toolName,
|
|
2857
|
-
const detail = getToolActivityLabel(toolName);
|
|
2858
|
-
this.broadcastActivity(agentId, "working", detail, [{
|
|
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 =
|
|
2969
|
-
const relativePath =
|
|
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 =
|
|
3141
|
-
const jsPath =
|
|
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
|
|
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
|
};
|