@slock-ai/daemon 0.39.0 → 0.39.1-alpha.1
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-7JPDXVD5.js} +800 -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,208 @@ 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
|
+
13. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
|
|
546
|
+
14. **\`slock reminder list\`** \u2014 List your reminders.
|
|
547
|
+
15. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
|
|
548
|
+
|
|
549
|
+
When a user asks you to remind them later, at a specific time, or on a recurring schedule, prefer the reminder commands instead of relying on MEMORY or manual follow-up.
|
|
550
|
+
|
|
551
|
+
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:
|
|
552
|
+
- failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
|
|
553
|
+
|
|
554
|
+
Error code prefixes tell you the layer:
|
|
555
|
+
- \`MISSING_*\` / \`TOKEN_*\` = local auth bootstrap
|
|
556
|
+
- \`*_FAILED\` = 4xx from server
|
|
557
|
+
- \`SERVER_5XX\` = server unreachable / crashed` : `## Communication \u2014 MCP tools ONLY
|
|
304
558
|
|
|
305
|
-
|
|
559
|
+
You have MCP tools from the "chat" server. Use ONLY these for communication:
|
|
306
560
|
|
|
307
|
-
|
|
561
|
+
1. **${checkCmd}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
|
|
562
|
+
2. **${sendCmd}** \u2014 Send a message to a channel or DM.
|
|
563
|
+
3. **${serverInfoCmd}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
|
|
564
|
+
4. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
|
|
565
|
+
5. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
|
|
566
|
+
6. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
|
|
567
|
+
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).
|
|
568
|
+
8. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
|
|
569
|
+
9. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
|
|
570
|
+
10. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
|
|
571
|
+
11. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
|
|
572
|
+
12. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
|
|
573
|
+
const sendingMessagesSection = isCli ? `### Sending messages
|
|
574
|
+
|
|
575
|
+
- **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
|
|
576
|
+
- **Reply to a DM**: \`slock message send --target "dm:@peer-name" <<'EOF'\` followed by the message body and \`EOF\`
|
|
577
|
+
- **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'EOF'\` followed by the message body and \`EOF\`
|
|
578
|
+
- **Start a NEW DM**: \`slock message send --target "dm:@person-name" <<'EOF'\` followed by the message body and \`EOF\`
|
|
579
|
+
|
|
580
|
+
Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
|
|
581
|
+
\`\`\`bash
|
|
582
|
+
slock message send --target "#channel-name" <<'EOF'
|
|
583
|
+
Long message with "quotes", $vars, \`backticks\`, and code blocks.
|
|
584
|
+
EOF
|
|
585
|
+
\`\`\`
|
|
308
586
|
|
|
309
|
-
|
|
587
|
+
**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
588
|
|
|
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.
|
|
589
|
+
- **Reply to a channel**: \`${t("send_message")}(target="#channel-name", content="...")\`
|
|
590
|
+
- **Reply to a DM**: \`${t("send_message")}(target="dm:@peer-name", content="...")\`
|
|
591
|
+
- **Reply in a thread**: \`${t("send_message")}(target="#channel:shortid", content="...")\` or \`${t("send_message")}(target="dm:@peer:shortid", content="...")\`
|
|
592
|
+
- **Start a NEW DM**: \`${t("send_message")}(target="dm:@person-name", content="...")\`
|
|
323
593
|
|
|
324
|
-
|
|
325
|
-
|
|
594
|
+
**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.`;
|
|
595
|
+
const threadsSection = isCli ? `### Threads
|
|
326
596
|
|
|
327
|
-
|
|
597
|
+
Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
|
|
328
598
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
599
|
+
- **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
|
|
600
|
+
- 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.
|
|
601
|
+
- **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.
|
|
602
|
+
- When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
|
|
603
|
+
- You can read thread history: \`slock message read --channel "#general:a1b2c3d4"\`
|
|
604
|
+
- Threads cannot be nested \u2014 you cannot start a thread inside a thread.` : `### Threads
|
|
332
605
|
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
prompt += `
|
|
606
|
+
Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
|
|
336
607
|
|
|
337
|
-
|
|
608
|
+
- **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
|
|
609
|
+
- 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.
|
|
610
|
+
- **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.
|
|
611
|
+
- When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
|
|
612
|
+
- You can read thread history via ${readCmd} with the same thread target.
|
|
613
|
+
- Threads cannot be nested \u2014 you cannot start a thread inside a thread.`;
|
|
614
|
+
const discoverySection = isCli ? `### Discovering people and channels
|
|
338
615
|
|
|
339
|
-
|
|
616
|
+
Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
617
|
+
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
618
|
|
|
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
|
-
\`\`\`
|
|
619
|
+
Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
620
|
+
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.`;
|
|
621
|
+
const channelAwarenessSection = isCli ? `### Channel awareness
|
|
348
622
|
|
|
349
|
-
|
|
350
|
-
-
|
|
351
|
-
-
|
|
352
|
-
- \`
|
|
353
|
-
- \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
|
|
623
|
+
Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). 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 \`slock server info\` to review channel descriptions.` : `### Channel awareness
|
|
354
627
|
|
|
355
|
-
|
|
628
|
+
Each channel has a **name** and optionally a **description** that define its purpose (visible via ${serverInfoCmd}). Respect them:
|
|
629
|
+
- **Reply in context** \u2014 always respond in the channel/thread the message came from.
|
|
630
|
+
- **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.
|
|
631
|
+
- If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
|
|
632
|
+
const readingHistorySection = isCli ? `### Reading history
|
|
356
633
|
|
|
357
|
-
|
|
634
|
+
\`slock message read --channel "#channel-name"\` or \`slock message read --channel "dm:@peer-name"\` or \`slock message read --channel "#channel:shortid"\`
|
|
358
635
|
|
|
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="...")\`
|
|
636
|
+
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
637
|
|
|
364
|
-
|
|
638
|
+
Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
|
|
365
639
|
|
|
366
|
-
|
|
640
|
+
To jump directly to a specific hit with nearby context, pass \`around\` set to a message ID or seq number.`;
|
|
641
|
+
const tasksSection = isCli ? `### Tasks
|
|
367
642
|
|
|
368
|
-
|
|
643
|
+
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
644
|
|
|
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.
|
|
645
|
+
**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
646
|
|
|
377
|
-
|
|
647
|
+
**What you see in messages:**
|
|
648
|
+
- A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
|
|
649
|
+
- A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
|
|
650
|
+
- A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
|
|
378
651
|
|
|
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.
|
|
652
|
+
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
653
|
|
|
382
|
-
|
|
654
|
+
\`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
655
|
|
|
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.
|
|
656
|
+
**Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
|
|
388
657
|
|
|
389
|
-
|
|
658
|
+
**Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
|
|
390
659
|
|
|
391
|
-
|
|
660
|
+
**Workflow:**
|
|
661
|
+
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)
|
|
662
|
+
2. If the claim fails, someone else is working on it \u2014 move on to another task
|
|
663
|
+
3. Post updates in the task's thread: \`slock message send --target "#channel:msgShortId" <<'EOF'\` followed by the message body and \`EOF\`
|
|
664
|
+
4. When done, set status to \`in_review\` so a human can validate via \`slock task update\`
|
|
665
|
+
5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
|
|
392
666
|
|
|
393
|
-
|
|
667
|
+
**What \`slock task create\` really means:**
|
|
668
|
+
- Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
|
|
669
|
+
- \`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.
|
|
670
|
+
- \`slock task create\` only creates the task \u2014 to own it, call \`slock task claim\` afterward.
|
|
671
|
+
- 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.
|
|
672
|
+
- If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
|
|
673
|
+
- If the work already exists as a message, reuse it via \`slock task claim --message-id ...\`.
|
|
394
674
|
|
|
395
|
-
|
|
675
|
+
**Creating new tasks:**
|
|
676
|
+
- 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.
|
|
677
|
+
- If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
|
|
678
|
+
- Before calling \`slock task create\`, first check whether the work already exists on the task board or is already being handled.
|
|
679
|
+
- Reuse existing tasks and threads instead of creating duplicates.
|
|
680
|
+
- Use \`slock task create\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.` : `### Tasks
|
|
396
681
|
|
|
397
682
|
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
683
|
|
|
@@ -405,7 +690,7 @@ When someone sends a message that asks you to do something \u2014 fix a bug, wri
|
|
|
405
690
|
|
|
406
691
|
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
692
|
|
|
408
|
-
|
|
693
|
+
${readCmd} shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
|
|
409
694
|
|
|
410
695
|
**Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
|
|
411
696
|
|
|
@@ -414,24 +699,77 @@ Only top-level channel / DM messages can become tasks. Messages inside threads a
|
|
|
414
699
|
**Workflow:**
|
|
415
700
|
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
701
|
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
|
|
702
|
+
3. Post updates in the task's thread via ${sendCmd} with \`target="#channel:msgShortId"\`
|
|
703
|
+
4. When done, set status to \`in_review\` so a human can validate via ${taskUpdateCmd}
|
|
419
704
|
5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
|
|
420
705
|
|
|
421
|
-
**What
|
|
706
|
+
**What ${taskCreateCmd} really means:**
|
|
422
707
|
- 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
|
|
708
|
+
- ${taskCreateCmd} is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
|
|
709
|
+
- ${taskCreateCmd} only creates the task \u2014 to own it, call ${taskClaimCmd} afterward.
|
|
710
|
+
- Typical uses for ${taskCreateCmd} are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
|
|
426
711
|
- 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
|
|
712
|
+
- If the work already exists as a message, reuse it via ${taskClaimCmd} with the message ID.
|
|
428
713
|
|
|
429
714
|
**Creating new tasks:**
|
|
430
715
|
- 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
716
|
- If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
|
|
432
|
-
- Before calling
|
|
717
|
+
- Before calling ${taskCreateCmd}, first check whether the work already exists on the task board or is already being handled.
|
|
433
718
|
- Reuse existing tasks and threads instead of creating duplicates.
|
|
434
|
-
- Use
|
|
719
|
+
- Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
|
|
720
|
+
const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
|
|
721
|
+
let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
|
|
722
|
+
|
|
723
|
+
## Who you are
|
|
724
|
+
|
|
725
|
+
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.
|
|
726
|
+
|
|
727
|
+
${communicationSection}
|
|
728
|
+
|
|
729
|
+
CRITICAL RULES:
|
|
730
|
+
${criticalRules.join("\n")}
|
|
731
|
+
|
|
732
|
+
## Startup sequence
|
|
733
|
+
|
|
734
|
+
${startupSteps.join("\n")}`;
|
|
735
|
+
if (opts.postStartupNotes.length > 0) {
|
|
736
|
+
prompt += `
|
|
737
|
+
|
|
738
|
+
${opts.postStartupNotes.join("\n")}`;
|
|
739
|
+
}
|
|
740
|
+
prompt += `
|
|
741
|
+
|
|
742
|
+
## Messaging
|
|
743
|
+
|
|
744
|
+
Messages you receive have a single RFC 5424-style structured data header followed by the sender and content:
|
|
745
|
+
|
|
746
|
+
\`\`\`
|
|
747
|
+
[target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00 type=human] @richard: hello everyone
|
|
748
|
+
[target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01 type=agent] @Alice: hi there
|
|
749
|
+
[target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02 type=human] @richard: hey, can you help?
|
|
750
|
+
[target=#general:a1b2c3d4 msg=f3a4b5c6 time=2026-03-15T01:00:03 type=human] @richard: thread reply
|
|
751
|
+
[target=dm:@richard:x9y8z7a0 msg=d7e8f9a0 time=2026-03-15T01:00:04 type=human] @richard: DM thread reply
|
|
752
|
+
\`\`\`
|
|
753
|
+
|
|
754
|
+
Header fields:
|
|
755
|
+
- \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
|
|
756
|
+
- \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
|
|
757
|
+
- \`time=\` \u2014 timestamp.
|
|
758
|
+
- \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
|
|
759
|
+
|
|
760
|
+
\`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.
|
|
761
|
+
|
|
762
|
+
${sendingMessagesSection}
|
|
763
|
+
|
|
764
|
+
${threadsSection}
|
|
765
|
+
|
|
766
|
+
${discoverySection}
|
|
767
|
+
|
|
768
|
+
${channelAwarenessSection}
|
|
769
|
+
|
|
770
|
+
${readingHistorySection}
|
|
771
|
+
|
|
772
|
+
${tasksSection}
|
|
435
773
|
|
|
436
774
|
### Splitting tasks for parallel execution
|
|
437
775
|
|
|
@@ -445,8 +783,8 @@ When you receive a notification about new tasks, check the task board and claim
|
|
|
445
783
|
## @Mentions
|
|
446
784
|
|
|
447
785
|
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
|
|
786
|
+
- Your stable Slock @mention handle is \`@${config.name}\`.
|
|
787
|
+
- 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
788
|
- Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
|
|
451
789
|
- Mention others, not yourself \u2014 assign reviews and follow-ups to teammates.
|
|
452
790
|
- @mentions only reach people inside the channel \u2014 channels are the isolation boundary.
|
|
@@ -463,7 +801,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
|
|
|
463
801
|
|
|
464
802
|
- **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
803
|
- **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
|
|
804
|
+
- **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
805
|
- **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
806
|
- **Skip idle narration.** Only send messages when you have actionable content \u2014 avoid broadcasting that you are waiting or idle.
|
|
469
807
|
|
|
@@ -559,20 +897,21 @@ How to handle these:
|
|
|
559
897
|
- Treat direct follow-up messages as new user input for the same live session.
|
|
560
898
|
- Adapt if the new message changes priority or direction.
|
|
561
899
|
- You do NOT need to poll just because direct follow-up delivery is available.
|
|
562
|
-
- Use
|
|
900
|
+
- Use ${checkCmd} only when you need to inspect other pending channels or recover broader context.`;
|
|
563
901
|
} else {
|
|
902
|
+
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
903
|
prompt += `
|
|
565
904
|
|
|
566
905
|
## Message Notifications
|
|
567
906
|
|
|
568
907
|
While you are busy (executing tools, thinking, etc.), new messages may arrive. When this happens, you will receive a system notification like:
|
|
569
908
|
|
|
570
|
-
|
|
909
|
+
${notifyExample}
|
|
571
910
|
|
|
572
911
|
How to handle these:
|
|
573
|
-
- Call
|
|
912
|
+
- 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
913
|
- If the new message is higher priority, you may pivot to it. If not, continue your current work.
|
|
575
|
-
-
|
|
914
|
+
- ${checkCmd} returns instantly with any pending messages (or "no new messages"). It is always safe to call.`;
|
|
576
915
|
}
|
|
577
916
|
}
|
|
578
917
|
if (config.description) {
|
|
@@ -583,11 +922,62 @@ ${config.description}. This may evolve.`;
|
|
|
583
922
|
}
|
|
584
923
|
return prompt;
|
|
585
924
|
}
|
|
925
|
+
function buildCliSystemPrompt(config, opts) {
|
|
926
|
+
return buildPrompt(config, "cli", opts);
|
|
927
|
+
}
|
|
928
|
+
function buildMcpSystemPrompt(config, opts) {
|
|
929
|
+
return buildPrompt(config, "mcp", opts);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/drivers/cliTransport.ts
|
|
933
|
+
var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
934
|
+
function buildCliTransportSystemPrompt(config, opts) {
|
|
935
|
+
return buildCliSystemPrompt(config, opts);
|
|
936
|
+
}
|
|
937
|
+
function prepareCliTransport(ctx, extraEnv = {}, platform = process.platform) {
|
|
938
|
+
if (!ctx.slockCliPath) {
|
|
939
|
+
throw new Error(`${ctx.config.runtime} driver: slockCliPath is required (daemon must inject it)`);
|
|
940
|
+
}
|
|
941
|
+
const slockDir = path.join(ctx.workingDirectory, ".slock");
|
|
942
|
+
mkdirSync(slockDir, { recursive: true });
|
|
943
|
+
const tokenFile = path.join(slockDir, "agent-token");
|
|
944
|
+
writeFileSync(tokenFile, ctx.config.authToken || ctx.daemonApiKey, { mode: 384 });
|
|
945
|
+
const posixWrapper = path.join(slockDir, "slock");
|
|
946
|
+
const posixBody = `#!/usr/bin/env bash
|
|
947
|
+
exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)} "$@"
|
|
948
|
+
`;
|
|
949
|
+
writeFileSync(posixWrapper, posixBody, { mode: 493 });
|
|
950
|
+
if (platform === "win32") {
|
|
951
|
+
const cmdWrapper = path.join(slockDir, "slock.cmd");
|
|
952
|
+
const cmdBody = `@echo off\r
|
|
953
|
+
"${process.execPath}" "${ctx.slockCliPath}" %*\r
|
|
954
|
+
`;
|
|
955
|
+
writeFileSync(cmdWrapper, cmdBody);
|
|
956
|
+
}
|
|
957
|
+
const wrapperPath = platform === "win32" ? path.join(slockDir, "slock.cmd") : posixWrapper;
|
|
958
|
+
const spawnEnv = {
|
|
959
|
+
...process.env,
|
|
960
|
+
FORCE_COLOR: "0",
|
|
961
|
+
...ctx.config.envVars || {},
|
|
962
|
+
...extraEnv,
|
|
963
|
+
SLOCK_AGENT_ID: ctx.agentId,
|
|
964
|
+
SLOCK_SERVER_URL: ctx.config.serverUrl,
|
|
965
|
+
SLOCK_AGENT_TOKEN_FILE: tokenFile,
|
|
966
|
+
PATH: `${slockDir}${path.delimiter}${process.env.PATH ?? ""}`
|
|
967
|
+
};
|
|
968
|
+
delete spawnEnv.SLOCK_AGENT_TOKEN;
|
|
969
|
+
return {
|
|
970
|
+
slockDir,
|
|
971
|
+
tokenFile,
|
|
972
|
+
wrapperPath,
|
|
973
|
+
spawnEnv
|
|
974
|
+
};
|
|
975
|
+
}
|
|
586
976
|
|
|
587
977
|
// src/drivers/probe.ts
|
|
588
978
|
import { execFileSync } from "child_process";
|
|
589
979
|
import { existsSync } from "fs";
|
|
590
|
-
import
|
|
980
|
+
import path2 from "path";
|
|
591
981
|
function normalizeExecOutput(raw) {
|
|
592
982
|
return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
|
|
593
983
|
}
|
|
@@ -653,12 +1043,19 @@ function readCommandVersion(command, args = [], deps = {}) {
|
|
|
653
1043
|
}
|
|
654
1044
|
function resolveHomePath(relativePath, deps = {}) {
|
|
655
1045
|
const homeDir = deps.homeDir ?? deps.env?.HOME ?? process.env.HOME ?? "";
|
|
656
|
-
return
|
|
1046
|
+
return path2.join(homeDir, relativePath);
|
|
657
1047
|
}
|
|
658
1048
|
|
|
659
1049
|
// src/drivers/claude.ts
|
|
660
|
-
var CLAUDE_DESKTOP_CLI_RELATIVE_PATH =
|
|
1050
|
+
var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
|
|
661
1051
|
var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
|
|
1052
|
+
var CLAUDE_DISALLOWED_TOOLS = [
|
|
1053
|
+
"EnterPlanMode",
|
|
1054
|
+
"ExitPlanMode",
|
|
1055
|
+
"CronCreate",
|
|
1056
|
+
"CronList",
|
|
1057
|
+
"CronDelete"
|
|
1058
|
+
].join(",");
|
|
662
1059
|
function resolveClaudeCommand(deps = {}) {
|
|
663
1060
|
const pathCommand = resolveCommandOnPath("claude", deps);
|
|
664
1061
|
if (pathCommand) return pathCommand;
|
|
@@ -681,36 +1078,11 @@ var ClaudeDriver = class {
|
|
|
681
1078
|
supportsStdinNotification = true;
|
|
682
1079
|
mcpToolPrefix = "mcp__chat__";
|
|
683
1080
|
busyDeliveryMode = "notification";
|
|
1081
|
+
supportsNativeStandingPrompt = true;
|
|
684
1082
|
probe() {
|
|
685
1083
|
return probeClaude();
|
|
686
1084
|
}
|
|
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
|
-
}
|
|
1085
|
+
buildClaudeArgs(config, standingPrompt) {
|
|
714
1086
|
const args = [
|
|
715
1087
|
"--allow-dangerously-skip-permissions",
|
|
716
1088
|
"--dangerously-skip-permissions",
|
|
@@ -719,18 +1091,25 @@ var ClaudeDriver = class {
|
|
|
719
1091
|
"stream-json",
|
|
720
1092
|
"--input-format",
|
|
721
1093
|
"stream-json",
|
|
722
|
-
"--
|
|
723
|
-
|
|
1094
|
+
"--append-system-prompt",
|
|
1095
|
+
standingPrompt,
|
|
724
1096
|
"--model",
|
|
725
|
-
|
|
1097
|
+
config.model || "sonnet",
|
|
726
1098
|
"--disallowed-tools",
|
|
727
|
-
|
|
1099
|
+
CLAUDE_DISALLOWED_TOOLS
|
|
728
1100
|
];
|
|
729
|
-
if (
|
|
730
|
-
args.push("--resume",
|
|
1101
|
+
if (config.sessionId) {
|
|
1102
|
+
args.push("--resume", config.sessionId);
|
|
731
1103
|
}
|
|
732
|
-
|
|
1104
|
+
return args;
|
|
1105
|
+
}
|
|
1106
|
+
spawn(ctx) {
|
|
1107
|
+
const { tokenFile, spawnEnv } = prepareCliTransport(ctx);
|
|
1108
|
+
const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt);
|
|
733
1109
|
delete spawnEnv.CLAUDECODE;
|
|
1110
|
+
logger.info(
|
|
1111
|
+
`[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
|
|
1112
|
+
);
|
|
734
1113
|
const proc = spawn(resolveClaudeCommand() ?? "claude", args, {
|
|
735
1114
|
cwd: ctx.workingDirectory,
|
|
736
1115
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -774,6 +1153,12 @@ var ClaudeDriver = class {
|
|
|
774
1153
|
if (event.subtype === "init" && event.session_id) {
|
|
775
1154
|
events.push({ kind: "session_init", sessionId: event.session_id });
|
|
776
1155
|
}
|
|
1156
|
+
if (event.subtype === "status" && event.status === "compacting") {
|
|
1157
|
+
events.push({ kind: "compaction_started" });
|
|
1158
|
+
}
|
|
1159
|
+
if (event.subtype === "compact_boundary") {
|
|
1160
|
+
events.push({ kind: "compaction_finished" });
|
|
1161
|
+
}
|
|
777
1162
|
break;
|
|
778
1163
|
case "assistant": {
|
|
779
1164
|
const content = event.message?.content;
|
|
@@ -831,11 +1216,9 @@ var ClaudeDriver = class {
|
|
|
831
1216
|
});
|
|
832
1217
|
}
|
|
833
1218
|
buildSystemPrompt(config, _agentId) {
|
|
834
|
-
return
|
|
1219
|
+
return buildCliTransportSystemPrompt(config, {
|
|
835
1220
|
toolPrefix: "mcp__chat__",
|
|
836
|
-
extraCriticalRules: [
|
|
837
|
-
"- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
|
|
838
|
-
],
|
|
1221
|
+
extraCriticalRules: [],
|
|
839
1222
|
postStartupNotes: [],
|
|
840
1223
|
includeStdinNotificationSection: true,
|
|
841
1224
|
messageNotificationStyle: "poll"
|
|
@@ -847,7 +1230,7 @@ var ClaudeDriver = class {
|
|
|
847
1230
|
import { spawn as spawn2, execSync } from "child_process";
|
|
848
1231
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
849
1232
|
import os from "os";
|
|
850
|
-
import
|
|
1233
|
+
import path4 from "path";
|
|
851
1234
|
function getCodexNotificationErrorMessage(params) {
|
|
852
1235
|
const topLevelMessage = params?.message;
|
|
853
1236
|
if (typeof topLevelMessage === "string" && topLevelMessage.trim()) {
|
|
@@ -859,21 +1242,19 @@ function getCodexNotificationErrorMessage(params) {
|
|
|
859
1242
|
}
|
|
860
1243
|
return null;
|
|
861
1244
|
}
|
|
862
|
-
function
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
});
|
|
1245
|
+
function ensureGitRepoForCodex(workingDirectory, deps = {}) {
|
|
1246
|
+
const existsSyncFn = deps.existsSyncFn ?? existsSync2;
|
|
1247
|
+
const execSyncFn = deps.execSyncFn ?? execSync;
|
|
1248
|
+
const gitDir = path4.join(workingDirectory, ".git");
|
|
1249
|
+
if (existsSyncFn(gitDir)) return;
|
|
1250
|
+
execSyncFn("git init", { cwd: workingDirectory, stdio: "pipe" });
|
|
1251
|
+
execSyncFn(
|
|
1252
|
+
"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'",
|
|
1253
|
+
{
|
|
1254
|
+
cwd: workingDirectory,
|
|
1255
|
+
stdio: "pipe"
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
877
1258
|
}
|
|
878
1259
|
var CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
|
|
879
1260
|
function resolveCodexCommand(deps = {}) {
|
|
@@ -908,14 +1289,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
|
|
|
908
1289
|
let codexEntry = null;
|
|
909
1290
|
try {
|
|
910
1291
|
const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
911
|
-
const candidate =
|
|
1292
|
+
const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
|
|
912
1293
|
if (existsSync2(candidate)) codexEntry = candidate;
|
|
913
1294
|
} catch {
|
|
914
1295
|
}
|
|
915
1296
|
if (!codexEntry) {
|
|
916
1297
|
try {
|
|
917
1298
|
const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
|
|
918
|
-
const candidate =
|
|
1299
|
+
const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
919
1300
|
if (existsSync2(candidate)) codexEntry = candidate;
|
|
920
1301
|
} catch {
|
|
921
1302
|
}
|
|
@@ -930,12 +1311,6 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
|
|
|
930
1311
|
args: [codexEntry, ...commandArgs]
|
|
931
1312
|
};
|
|
932
1313
|
}
|
|
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
1314
|
function joinReasoningText(item) {
|
|
940
1315
|
const summary = Array.isArray(item.summary) ? item.summary.filter((entry) => typeof entry === "string") : [];
|
|
941
1316
|
const content = Array.isArray(item.content) ? item.content.filter((entry) => typeof entry === "string") : [];
|
|
@@ -946,9 +1321,37 @@ var CodexDriver = class {
|
|
|
946
1321
|
supportsStdinNotification = true;
|
|
947
1322
|
mcpToolPrefix = "mcp_chat_";
|
|
948
1323
|
busyDeliveryMode = "direct";
|
|
1324
|
+
supportsNativeStandingPrompt = true;
|
|
949
1325
|
probe() {
|
|
950
1326
|
return probeCodex();
|
|
951
1327
|
}
|
|
1328
|
+
buildThreadRequest(ctx) {
|
|
1329
|
+
const threadParams = {
|
|
1330
|
+
cwd: ctx.workingDirectory,
|
|
1331
|
+
approvalPolicy: "never",
|
|
1332
|
+
sandbox: "danger-full-access",
|
|
1333
|
+
developerInstructions: ctx.standingPrompt
|
|
1334
|
+
};
|
|
1335
|
+
if (ctx.config.model) {
|
|
1336
|
+
threadParams.model = ctx.config.model;
|
|
1337
|
+
}
|
|
1338
|
+
if (ctx.config.reasoningEffort) {
|
|
1339
|
+
threadParams.config = { model_reasoning_effort: ctx.config.reasoningEffort };
|
|
1340
|
+
}
|
|
1341
|
+
if (ctx.config.sessionId) {
|
|
1342
|
+
return {
|
|
1343
|
+
method: "thread/resume",
|
|
1344
|
+
params: {
|
|
1345
|
+
threadId: ctx.config.sessionId,
|
|
1346
|
+
...threadParams
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
return {
|
|
1351
|
+
method: "thread/start",
|
|
1352
|
+
params: threadParams
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
952
1355
|
process = null;
|
|
953
1356
|
requestId = 0;
|
|
954
1357
|
threadId = null;
|
|
@@ -961,7 +1364,8 @@ var CodexDriver = class {
|
|
|
961
1364
|
streamedAgentMessageIds = /* @__PURE__ */ new Set();
|
|
962
1365
|
streamedReasoningIds = /* @__PURE__ */ new Set();
|
|
963
1366
|
spawn(ctx) {
|
|
964
|
-
|
|
1367
|
+
ensureGitRepoForCodex(ctx.workingDirectory);
|
|
1368
|
+
const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" });
|
|
965
1369
|
this.process = null;
|
|
966
1370
|
this.requestId = 0;
|
|
967
1371
|
this.threadId = ctx.config.sessionId || null;
|
|
@@ -973,26 +1377,8 @@ var CodexDriver = class {
|
|
|
973
1377
|
this.sessionAnnounced = false;
|
|
974
1378
|
this.streamedAgentMessageIds.clear();
|
|
975
1379
|
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
|
-
];
|
|
1380
|
+
const args = ["app-server", "--listen", "stdio://"];
|
|
994
1381
|
const { command, args: spawnArgs } = resolveCodexSpawn(args);
|
|
995
|
-
const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
|
|
996
1382
|
const proc = spawn2(command, spawnArgs, {
|
|
997
1383
|
cwd: ctx.workingDirectory,
|
|
998
1384
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1005,31 +1391,7 @@ var CodexDriver = class {
|
|
|
1005
1391
|
clientInfo: { name: "slock-daemon", version: "1.0.0" },
|
|
1006
1392
|
capabilities: { experimentalApi: true }
|
|
1007
1393
|
});
|
|
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
|
-
}
|
|
1394
|
+
this.pendingThreadRequest = this.buildThreadRequest(ctx);
|
|
1033
1395
|
});
|
|
1034
1396
|
return { process: proc };
|
|
1035
1397
|
}
|
|
@@ -1145,6 +1507,14 @@ var CodexDriver = class {
|
|
|
1145
1507
|
events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
|
|
1146
1508
|
}
|
|
1147
1509
|
break;
|
|
1510
|
+
case "contextCompaction":
|
|
1511
|
+
if (isStarted) {
|
|
1512
|
+
events.push({ kind: "compaction_started" });
|
|
1513
|
+
}
|
|
1514
|
+
if (isCompleted) {
|
|
1515
|
+
events.push({ kind: "compaction_finished" });
|
|
1516
|
+
}
|
|
1517
|
+
break;
|
|
1148
1518
|
case "fileChange":
|
|
1149
1519
|
if (isStarted && Array.isArray(item.changes)) {
|
|
1150
1520
|
for (const change of item.changes) {
|
|
@@ -1225,11 +1595,9 @@ var CodexDriver = class {
|
|
|
1225
1595
|
});
|
|
1226
1596
|
}
|
|
1227
1597
|
buildSystemPrompt(config, _agentId) {
|
|
1228
|
-
return
|
|
1598
|
+
return buildCliTransportSystemPrompt(config, {
|
|
1229
1599
|
toolPrefix: "",
|
|
1230
|
-
extraCriticalRules: [
|
|
1231
|
-
"- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
|
|
1232
|
-
],
|
|
1600
|
+
extraCriticalRules: [],
|
|
1233
1601
|
postStartupNotes: [
|
|
1234
1602
|
"**IMPORTANT**: Your process stays alive across turns. New messages may be delivered directly into the current thread while you are working."
|
|
1235
1603
|
],
|
|
@@ -1281,8 +1649,8 @@ var CodexDriver = class {
|
|
|
1281
1649
|
}
|
|
1282
1650
|
};
|
|
1283
1651
|
function detectCodexModels(home = os.homedir()) {
|
|
1284
|
-
const cachePath =
|
|
1285
|
-
const configPath =
|
|
1652
|
+
const cachePath = path4.join(home, ".codex", "models_cache.json");
|
|
1653
|
+
const configPath = path4.join(home, ".codex", "config.toml");
|
|
1286
1654
|
let models = [];
|
|
1287
1655
|
try {
|
|
1288
1656
|
const raw = readFileSync(cachePath, "utf8");
|
|
@@ -1312,7 +1680,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
1312
1680
|
|
|
1313
1681
|
// src/drivers/copilot.ts
|
|
1314
1682
|
import { spawn as spawn3 } from "child_process";
|
|
1315
|
-
import
|
|
1683
|
+
import path5 from "path";
|
|
1316
1684
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1317
1685
|
var CopilotDriver = class {
|
|
1318
1686
|
id = "copilot";
|
|
@@ -1327,7 +1695,7 @@ var CopilotDriver = class {
|
|
|
1327
1695
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1328
1696
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1329
1697
|
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 =
|
|
1698
|
+
const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
|
|
1331
1699
|
writeFileSync2(mcpConfigPath, JSON.stringify({
|
|
1332
1700
|
mcpServers: {
|
|
1333
1701
|
chat: {
|
|
@@ -1436,7 +1804,7 @@ var CopilotDriver = class {
|
|
|
1436
1804
|
return null;
|
|
1437
1805
|
}
|
|
1438
1806
|
buildSystemPrompt(config, _agentId) {
|
|
1439
|
-
return
|
|
1807
|
+
return buildMcpSystemPrompt(config, {
|
|
1440
1808
|
toolPrefix: "",
|
|
1441
1809
|
extraCriticalRules: [
|
|
1442
1810
|
"- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
|
|
@@ -1450,22 +1818,22 @@ var CopilotDriver = class {
|
|
|
1450
1818
|
|
|
1451
1819
|
// src/drivers/cursor.ts
|
|
1452
1820
|
import { spawn as spawn4 } from "child_process";
|
|
1453
|
-
import { writeFileSync as writeFileSync3, mkdirSync, existsSync as existsSync3 } from "fs";
|
|
1454
|
-
import
|
|
1821
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
1822
|
+
import path6 from "path";
|
|
1455
1823
|
var CursorDriver = class {
|
|
1456
1824
|
id = "cursor";
|
|
1457
1825
|
supportsStdinNotification = false;
|
|
1458
1826
|
mcpToolPrefix = "mcp__chat__";
|
|
1459
1827
|
busyDeliveryMode = "none";
|
|
1460
1828
|
spawn(ctx) {
|
|
1461
|
-
const cursorDir =
|
|
1829
|
+
const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
|
|
1462
1830
|
if (!existsSync3(cursorDir)) {
|
|
1463
|
-
|
|
1831
|
+
mkdirSync2(cursorDir, { recursive: true });
|
|
1464
1832
|
}
|
|
1465
1833
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1466
1834
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1467
1835
|
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 =
|
|
1836
|
+
const mcpConfigPath = path6.join(cursorDir, "mcp.json");
|
|
1469
1837
|
writeFileSync3(mcpConfigPath, JSON.stringify({
|
|
1470
1838
|
mcpServers: {
|
|
1471
1839
|
chat: {
|
|
@@ -1510,6 +1878,10 @@ var CursorDriver = class {
|
|
|
1510
1878
|
case "system":
|
|
1511
1879
|
if (event.subtype === "init" && event.session_id) {
|
|
1512
1880
|
events.push({ kind: "session_init", sessionId: event.session_id });
|
|
1881
|
+
} else if (event.subtype === "status" && event.status === "compacting") {
|
|
1882
|
+
events.push({ kind: "compaction_started" });
|
|
1883
|
+
} else if (event.subtype === "compact_boundary") {
|
|
1884
|
+
events.push({ kind: "compaction_finished" });
|
|
1513
1885
|
}
|
|
1514
1886
|
break;
|
|
1515
1887
|
case "assistant": {
|
|
@@ -1552,7 +1924,7 @@ var CursorDriver = class {
|
|
|
1552
1924
|
return null;
|
|
1553
1925
|
}
|
|
1554
1926
|
buildSystemPrompt(config, _agentId) {
|
|
1555
|
-
return
|
|
1927
|
+
return buildMcpSystemPrompt(config, {
|
|
1556
1928
|
toolPrefix: "mcp__chat__",
|
|
1557
1929
|
extraCriticalRules: [
|
|
1558
1930
|
"- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
|
|
@@ -1566,8 +1938,8 @@ var CursorDriver = class {
|
|
|
1566
1938
|
|
|
1567
1939
|
// src/drivers/gemini.ts
|
|
1568
1940
|
import { spawn as spawn5 } from "child_process";
|
|
1569
|
-
import { writeFileSync as writeFileSync4, mkdirSync as
|
|
1570
|
-
import
|
|
1941
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
1942
|
+
import path7 from "path";
|
|
1571
1943
|
var GeminiDriver = class {
|
|
1572
1944
|
id = "gemini";
|
|
1573
1945
|
supportsStdinNotification = false;
|
|
@@ -1578,14 +1950,14 @@ var GeminiDriver = class {
|
|
|
1578
1950
|
spawn(ctx) {
|
|
1579
1951
|
this.sessionId = ctx.config.sessionId || null;
|
|
1580
1952
|
this.sessionAnnounced = false;
|
|
1581
|
-
const geminiDir =
|
|
1953
|
+
const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
|
|
1582
1954
|
if (!existsSync4(geminiDir)) {
|
|
1583
|
-
|
|
1955
|
+
mkdirSync3(geminiDir, { recursive: true });
|
|
1584
1956
|
}
|
|
1585
1957
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1586
1958
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1587
1959
|
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 =
|
|
1960
|
+
const settingsPath = path7.join(geminiDir, "settings.json");
|
|
1589
1961
|
writeFileSync4(settingsPath, JSON.stringify({
|
|
1590
1962
|
mcpServers: {
|
|
1591
1963
|
chat: {
|
|
@@ -1667,7 +2039,7 @@ var GeminiDriver = class {
|
|
|
1667
2039
|
return null;
|
|
1668
2040
|
}
|
|
1669
2041
|
buildSystemPrompt(config, _agentId) {
|
|
1670
|
-
return
|
|
2042
|
+
return buildMcpSystemPrompt(config, {
|
|
1671
2043
|
toolPrefix: "",
|
|
1672
2044
|
extraCriticalRules: [
|
|
1673
2045
|
"- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
|
|
@@ -1684,7 +2056,7 @@ import { randomUUID } from "crypto";
|
|
|
1684
2056
|
import { spawn as spawn6 } from "child_process";
|
|
1685
2057
|
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1686
2058
|
import os2 from "os";
|
|
1687
|
-
import
|
|
2059
|
+
import path8 from "path";
|
|
1688
2060
|
var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
|
|
1689
2061
|
var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
|
|
1690
2062
|
var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
|
|
@@ -1713,9 +2085,9 @@ var KimiDriver = class {
|
|
|
1713
2085
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1714
2086
|
const command = isTsSource ? "npx" : "node";
|
|
1715
2087
|
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 =
|
|
2088
|
+
const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
|
|
2089
|
+
const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
|
|
2090
|
+
const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
|
|
1719
2091
|
if (!isResume || !existsSync5(systemPromptPath)) {
|
|
1720
2092
|
writeFileSync5(systemPromptPath, ctx.prompt, "utf8");
|
|
1721
2093
|
}
|
|
@@ -1796,6 +2168,12 @@ var KimiDriver = class {
|
|
|
1796
2168
|
case "StepBegin":
|
|
1797
2169
|
events.push({ kind: "thinking", text: "" });
|
|
1798
2170
|
break;
|
|
2171
|
+
case "CompactionBegin":
|
|
2172
|
+
events.push({ kind: "compaction_started" });
|
|
2173
|
+
break;
|
|
2174
|
+
case "CompactionEnd":
|
|
2175
|
+
events.push({ kind: "compaction_finished" });
|
|
2176
|
+
break;
|
|
1799
2177
|
case "ContentPart":
|
|
1800
2178
|
if (payload.type === "think" && payload.think) {
|
|
1801
2179
|
events.push({ kind: "thinking", text: payload.think });
|
|
@@ -1848,7 +2226,7 @@ var KimiDriver = class {
|
|
|
1848
2226
|
});
|
|
1849
2227
|
}
|
|
1850
2228
|
buildSystemPrompt(config, _agentId) {
|
|
1851
|
-
return
|
|
2229
|
+
return buildMcpSystemPrompt(config, {
|
|
1852
2230
|
toolPrefix: "",
|
|
1853
2231
|
extraCriticalRules: [
|
|
1854
2232
|
"- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
|
|
@@ -1863,7 +2241,7 @@ var KimiDriver = class {
|
|
|
1863
2241
|
}
|
|
1864
2242
|
};
|
|
1865
2243
|
function detectKimiModels(home = os2.homedir()) {
|
|
1866
|
-
const configPath =
|
|
2244
|
+
const configPath = path8.join(home, ".kimi", "config.toml");
|
|
1867
2245
|
let raw;
|
|
1868
2246
|
try {
|
|
1869
2247
|
raw = readFileSync2(configPath, "utf8");
|
|
@@ -1908,7 +2286,7 @@ function getDriver(runtimeId) {
|
|
|
1908
2286
|
|
|
1909
2287
|
// src/workspaces.ts
|
|
1910
2288
|
import { readdir, rm, stat } from "fs/promises";
|
|
1911
|
-
import
|
|
2289
|
+
import path9 from "path";
|
|
1912
2290
|
function isValidWorkspaceDirectoryName(directoryName) {
|
|
1913
2291
|
return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
|
|
1914
2292
|
}
|
|
@@ -1916,7 +2294,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
|
|
|
1916
2294
|
if (!isValidWorkspaceDirectoryName(directoryName)) {
|
|
1917
2295
|
return null;
|
|
1918
2296
|
}
|
|
1919
|
-
return
|
|
2297
|
+
return path9.join(dataDir, directoryName);
|
|
1920
2298
|
}
|
|
1921
2299
|
function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
|
|
1922
2300
|
return {
|
|
@@ -1965,7 +2343,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
|
|
|
1965
2343
|
return summary;
|
|
1966
2344
|
}
|
|
1967
2345
|
const childSummaries = await Promise.all(
|
|
1968
|
-
entries.map((entry) => summarizeWorkspaceEntry(
|
|
2346
|
+
entries.map((entry) => summarizeWorkspaceEntry(path9.join(dirPath, entry.name), entry))
|
|
1969
2347
|
);
|
|
1970
2348
|
for (const childSummary of childSummaries) {
|
|
1971
2349
|
summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
|
|
@@ -1984,7 +2362,7 @@ async function scanWorkspaceDirectories(dataDir) {
|
|
|
1984
2362
|
if (!entry.isDirectory()) {
|
|
1985
2363
|
return null;
|
|
1986
2364
|
}
|
|
1987
|
-
const dirPath =
|
|
2365
|
+
const dirPath = path9.join(dataDir, entry.name);
|
|
1988
2366
|
try {
|
|
1989
2367
|
const summary = await summarizeWorkspaceDirectory(dirPath);
|
|
1990
2368
|
return {
|
|
@@ -2016,7 +2394,7 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
|
|
|
2016
2394
|
}
|
|
2017
2395
|
|
|
2018
2396
|
// src/agentProcessManager.ts
|
|
2019
|
-
var DATA_DIR =
|
|
2397
|
+
var DATA_DIR = path10.join(os3.homedir(), ".slock", "agents");
|
|
2020
2398
|
function toLocalTime(iso) {
|
|
2021
2399
|
const d = new Date(iso);
|
|
2022
2400
|
if (isNaN(d.getTime())) return iso;
|
|
@@ -2170,6 +2548,7 @@ function getBusyDeliveryNote(driver) {
|
|
|
2170
2548
|
}
|
|
2171
2549
|
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
2550
|
}
|
|
2551
|
+
var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
|
|
2173
2552
|
var AgentProcessManager = class _AgentProcessManager {
|
|
2174
2553
|
agents = /* @__PURE__ */ new Map();
|
|
2175
2554
|
agentsStarting = /* @__PURE__ */ new Set();
|
|
@@ -2178,6 +2557,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
2178
2557
|
/** Cached configs for agents whose process exited normally — enables auto-restart on next message */
|
|
2179
2558
|
idleAgentConfigs = /* @__PURE__ */ new Map();
|
|
2180
2559
|
chatBridgePath;
|
|
2560
|
+
slockCliPath;
|
|
2181
2561
|
sendToServer;
|
|
2182
2562
|
daemonApiKey;
|
|
2183
2563
|
serverUrl;
|
|
@@ -2186,6 +2566,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
2186
2566
|
defaultAgentEnvVarsProvider;
|
|
2187
2567
|
constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
|
|
2188
2568
|
this.chatBridgePath = chatBridgePath;
|
|
2569
|
+
this.slockCliPath = opts.slockCliPath ?? "";
|
|
2189
2570
|
this.sendToServer = sendToServer;
|
|
2190
2571
|
this.daemonApiKey = daemonApiKey;
|
|
2191
2572
|
this.serverUrl = opts.serverUrl;
|
|
@@ -2205,9 +2586,9 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
2205
2586
|
this.agentsStarting.add(agentId);
|
|
2206
2587
|
try {
|
|
2207
2588
|
const driver = this.driverResolver(config.runtime || "claude");
|
|
2208
|
-
const agentDataDir =
|
|
2589
|
+
const agentDataDir = path10.join(this.dataDir, agentId);
|
|
2209
2590
|
await mkdir(agentDataDir, { recursive: true });
|
|
2210
|
-
const memoryMdPath =
|
|
2591
|
+
const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
|
|
2211
2592
|
try {
|
|
2212
2593
|
await access(memoryMdPath);
|
|
2213
2594
|
} catch {
|
|
@@ -2225,8 +2606,9 @@ ${config.description || "No role defined yet."}
|
|
|
2225
2606
|
`;
|
|
2226
2607
|
await writeFile(memoryMdPath, initialMemoryMd);
|
|
2227
2608
|
}
|
|
2228
|
-
await mkdir(
|
|
2609
|
+
await mkdir(path10.join(agentDataDir, "notes"), { recursive: true });
|
|
2229
2610
|
const isResume = !!config.sessionId;
|
|
2611
|
+
const standingPrompt = driver.buildSystemPrompt(config, agentId);
|
|
2230
2612
|
let prompt;
|
|
2231
2613
|
if (isResume && resumePrompt) {
|
|
2232
2614
|
prompt = resumePrompt;
|
|
@@ -2270,15 +2652,17 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2270
2652
|
prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
|
|
2271
2653
|
prompt += getBusyDeliveryNote(driver);
|
|
2272
2654
|
} else {
|
|
2273
|
-
prompt = driver.
|
|
2655
|
+
prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
|
|
2274
2656
|
}
|
|
2275
2657
|
const effectiveConfig = await this.buildSpawnConfig(agentId, config);
|
|
2276
2658
|
const { process: proc } = driver.spawn({
|
|
2277
2659
|
agentId,
|
|
2278
2660
|
config: effectiveConfig,
|
|
2661
|
+
standingPrompt,
|
|
2279
2662
|
prompt,
|
|
2280
2663
|
workingDirectory: agentDataDir,
|
|
2281
2664
|
chatBridgePath: this.chatBridgePath,
|
|
2665
|
+
slockCliPath: this.slockCliPath,
|
|
2282
2666
|
daemonApiKey: this.daemonApiKey
|
|
2283
2667
|
});
|
|
2284
2668
|
const agentProcess = {
|
|
@@ -2566,7 +2950,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2566
2950
|
}
|
|
2567
2951
|
}
|
|
2568
2952
|
async resetWorkspace(agentId) {
|
|
2569
|
-
const agentDataDir =
|
|
2953
|
+
const agentDataDir = path10.join(this.dataDir, agentId);
|
|
2570
2954
|
try {
|
|
2571
2955
|
await rm2(agentDataDir, { recursive: true, force: true });
|
|
2572
2956
|
logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
|
|
@@ -2604,7 +2988,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2604
2988
|
}
|
|
2605
2989
|
// Workspace file browsing
|
|
2606
2990
|
async getFileTree(agentId, dirPath) {
|
|
2607
|
-
const agentDir =
|
|
2991
|
+
const agentDir = path10.join(this.dataDir, agentId);
|
|
2608
2992
|
try {
|
|
2609
2993
|
await stat2(agentDir);
|
|
2610
2994
|
} catch {
|
|
@@ -2612,8 +2996,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2612
2996
|
}
|
|
2613
2997
|
let targetDir = agentDir;
|
|
2614
2998
|
if (dirPath) {
|
|
2615
|
-
const resolved =
|
|
2616
|
-
if (!resolved.startsWith(agentDir +
|
|
2999
|
+
const resolved = path10.resolve(agentDir, dirPath);
|
|
3000
|
+
if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
|
|
2617
3001
|
return [];
|
|
2618
3002
|
}
|
|
2619
3003
|
targetDir = resolved;
|
|
@@ -2621,9 +3005,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2621
3005
|
return this.listDirectoryChildren(targetDir, agentDir);
|
|
2622
3006
|
}
|
|
2623
3007
|
async readFile(agentId, filePath) {
|
|
2624
|
-
const agentDir =
|
|
2625
|
-
const resolved =
|
|
2626
|
-
if (!resolved.startsWith(agentDir +
|
|
3008
|
+
const agentDir = path10.join(this.dataDir, agentId);
|
|
3009
|
+
const resolved = path10.resolve(agentDir, filePath);
|
|
3010
|
+
if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
|
|
2627
3011
|
throw new Error("Access denied");
|
|
2628
3012
|
}
|
|
2629
3013
|
const info = await stat2(resolved);
|
|
@@ -2647,7 +3031,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2647
3031
|
".sh",
|
|
2648
3032
|
".py"
|
|
2649
3033
|
]);
|
|
2650
|
-
const ext =
|
|
3034
|
+
const ext = path10.extname(resolved).toLowerCase();
|
|
2651
3035
|
if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
|
|
2652
3036
|
return { content: null, binary: true };
|
|
2653
3037
|
}
|
|
@@ -2674,13 +3058,13 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2674
3058
|
const agent = this.agents.get(agentId);
|
|
2675
3059
|
const runtime = runtimeHint || agent?.config.runtime || "claude";
|
|
2676
3060
|
const home = os3.homedir();
|
|
2677
|
-
const workspaceDir =
|
|
3061
|
+
const workspaceDir = path10.join(this.dataDir, agentId);
|
|
2678
3062
|
const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
|
|
2679
3063
|
const globalResults = await Promise.all(
|
|
2680
|
-
paths.global.map((p) => this.scanSkillsDir(
|
|
3064
|
+
paths.global.map((p) => this.scanSkillsDir(path10.join(home, p)))
|
|
2681
3065
|
);
|
|
2682
3066
|
const workspaceResults = await Promise.all(
|
|
2683
|
-
paths.workspace.map((p) => this.scanSkillsDir(
|
|
3067
|
+
paths.workspace.map((p) => this.scanSkillsDir(path10.join(workspaceDir, p)))
|
|
2684
3068
|
);
|
|
2685
3069
|
const dedup = (skills) => {
|
|
2686
3070
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2709,7 +3093,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2709
3093
|
const skills = [];
|
|
2710
3094
|
for (const entry of entries) {
|
|
2711
3095
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
2712
|
-
const skillMd =
|
|
3096
|
+
const skillMd = path10.join(dir, entry.name, "SKILL.md");
|
|
2713
3097
|
try {
|
|
2714
3098
|
const content = await readFile(skillMd, "utf-8");
|
|
2715
3099
|
const skill = this.parseSkillMd(entry.name, content);
|
|
@@ -2720,7 +3104,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2720
3104
|
} else if (entry.name.endsWith(".md")) {
|
|
2721
3105
|
const cmdName = entry.name.replace(/\.md$/, "");
|
|
2722
3106
|
try {
|
|
2723
|
-
const content = await readFile(
|
|
3107
|
+
const content = await readFile(path10.join(dir, entry.name), "utf-8");
|
|
2724
3108
|
const skill = this.parseSkillMd(cmdName, content);
|
|
2725
3109
|
skill.sourcePath = dir;
|
|
2726
3110
|
skills.push(skill);
|
|
@@ -2852,13 +3236,27 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2852
3236
|
}
|
|
2853
3237
|
case "tool_call": {
|
|
2854
3238
|
this.flushPendingTrajectory(agentId);
|
|
2855
|
-
const
|
|
2856
|
-
const inputSummary = summarizeToolInput(toolName,
|
|
2857
|
-
const detail = getToolActivityLabel(toolName);
|
|
2858
|
-
this.broadcastActivity(agentId, "working", detail, [{
|
|
3239
|
+
const invocation = normalizeToolDisplayInvocation(event.name, event.input);
|
|
3240
|
+
const inputSummary = summarizeToolInput(invocation.toolName, invocation.input);
|
|
3241
|
+
const detail = getToolActivityLabel(invocation.toolName);
|
|
3242
|
+
this.broadcastActivity(agentId, "working", detail, [{
|
|
3243
|
+
kind: "tool_start",
|
|
3244
|
+
toolName: invocation.toolName,
|
|
3245
|
+
toolInput: inputSummary
|
|
3246
|
+
}]);
|
|
2859
3247
|
if (ap) ap.isIdle = false;
|
|
2860
3248
|
break;
|
|
2861
3249
|
}
|
|
3250
|
+
case "compaction_started":
|
|
3251
|
+
this.flushPendingTrajectory(agentId);
|
|
3252
|
+
this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
|
|
3253
|
+
if (ap) ap.isIdle = false;
|
|
3254
|
+
break;
|
|
3255
|
+
case "compaction_finished":
|
|
3256
|
+
this.flushPendingTrajectory(agentId);
|
|
3257
|
+
this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
|
|
3258
|
+
if (ap) ap.isIdle = false;
|
|
3259
|
+
break;
|
|
2862
3260
|
case "turn_end":
|
|
2863
3261
|
this.flushPendingTrajectory(agentId);
|
|
2864
3262
|
if (ap) {
|
|
@@ -2965,8 +3363,8 @@ Respond as appropriate. Complete all your work before stopping.`;
|
|
|
2965
3363
|
const nodes = [];
|
|
2966
3364
|
for (const entry of entries) {
|
|
2967
3365
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2968
|
-
const fullPath =
|
|
2969
|
-
const relativePath =
|
|
3366
|
+
const fullPath = path10.join(dir, entry.name);
|
|
3367
|
+
const relativePath = path10.relative(rootDir, fullPath);
|
|
2970
3368
|
let info;
|
|
2971
3369
|
try {
|
|
2972
3370
|
info = await stat2(fullPath);
|
|
@@ -3116,6 +3514,85 @@ var DaemonConnection = class {
|
|
|
3116
3514
|
}
|
|
3117
3515
|
};
|
|
3118
3516
|
|
|
3517
|
+
// src/reminderCache.ts
|
|
3518
|
+
var DEFAULT_MAX_DELAY_MS = 24 * 60 * 60 * 1e3;
|
|
3519
|
+
var ReminderCache = class {
|
|
3520
|
+
entries = /* @__PURE__ */ new Map();
|
|
3521
|
+
clock;
|
|
3522
|
+
onFire;
|
|
3523
|
+
maxDelayMs;
|
|
3524
|
+
constructor(opts) {
|
|
3525
|
+
this.clock = opts.clock ?? systemClock;
|
|
3526
|
+
this.onFire = opts.onFire;
|
|
3527
|
+
this.maxDelayMs = opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
3528
|
+
}
|
|
3529
|
+
upsert(job) {
|
|
3530
|
+
const existing = this.entries.get(job.reminderId);
|
|
3531
|
+
if (existing && existing.job.version >= job.version) {
|
|
3532
|
+
logger.info(`[ReminderCache] Stale upsert for ${job.reminderId} (incoming v${job.version} <= cached v${existing.job.version}) \u2014 ignored`);
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
if (existing?.timer) this.clock.clearTimeout(existing.timer);
|
|
3536
|
+
const timer = this.scheduleTimer(job);
|
|
3537
|
+
this.entries.set(job.reminderId, { job, timer });
|
|
3538
|
+
}
|
|
3539
|
+
cancel(reminderId, version) {
|
|
3540
|
+
const existing = this.entries.get(reminderId);
|
|
3541
|
+
if (!existing) return;
|
|
3542
|
+
if (existing.job.version > version) {
|
|
3543
|
+
logger.info(`[ReminderCache] Stale cancel for ${reminderId} (incoming v${version} < cached v${existing.job.version}) \u2014 ignored`);
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
if (existing.timer) this.clock.clearTimeout(existing.timer);
|
|
3547
|
+
this.entries.delete(reminderId);
|
|
3548
|
+
}
|
|
3549
|
+
snapshot(agentId, jobs) {
|
|
3550
|
+
for (const [reminderId, entry] of this.entries) {
|
|
3551
|
+
if (entry.job.ownerAgentId !== agentId) continue;
|
|
3552
|
+
if (entry.timer) this.clock.clearTimeout(entry.timer);
|
|
3553
|
+
this.entries.delete(reminderId);
|
|
3554
|
+
}
|
|
3555
|
+
for (const job of jobs) {
|
|
3556
|
+
if (job.ownerAgentId !== agentId) {
|
|
3557
|
+
logger.warn(
|
|
3558
|
+
`[ReminderCache] snapshot for agent ${agentId} carried job ${job.reminderId} owned by ${job.ownerAgentId} \u2014 skipping`
|
|
3559
|
+
);
|
|
3560
|
+
continue;
|
|
3561
|
+
}
|
|
3562
|
+
const timer = this.scheduleTimer(job);
|
|
3563
|
+
this.entries.set(job.reminderId, { job, timer });
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
clear() {
|
|
3567
|
+
for (const entry of this.entries.values()) {
|
|
3568
|
+
if (entry.timer) this.clock.clearTimeout(entry.timer);
|
|
3569
|
+
}
|
|
3570
|
+
this.entries.clear();
|
|
3571
|
+
}
|
|
3572
|
+
size() {
|
|
3573
|
+
return this.entries.size;
|
|
3574
|
+
}
|
|
3575
|
+
getJob(reminderId) {
|
|
3576
|
+
return this.entries.get(reminderId)?.job ?? null;
|
|
3577
|
+
}
|
|
3578
|
+
scheduleTimer(job) {
|
|
3579
|
+
const fireAt = Date.parse(job.fireAt);
|
|
3580
|
+
if (Number.isNaN(fireAt)) {
|
|
3581
|
+
logger.warn(`[ReminderCache] Invalid fireAt for ${job.reminderId}: ${job.fireAt}`);
|
|
3582
|
+
return null;
|
|
3583
|
+
}
|
|
3584
|
+
const delay = Math.max(0, Math.min(this.maxDelayMs, fireAt - this.clock.now()));
|
|
3585
|
+
return this.clock.setTimeout(() => {
|
|
3586
|
+
this.entries.delete(job.reminderId);
|
|
3587
|
+
try {
|
|
3588
|
+
this.onFire(job);
|
|
3589
|
+
} catch (err) {
|
|
3590
|
+
logger.error(`[ReminderCache] onFire threw for ${job.reminderId}`, err);
|
|
3591
|
+
}
|
|
3592
|
+
}, delay);
|
|
3593
|
+
}
|
|
3594
|
+
};
|
|
3595
|
+
|
|
3119
3596
|
// src/core.ts
|
|
3120
3597
|
var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
|
|
3121
3598
|
function parseDaemonCliArgs(args) {
|
|
@@ -3137,13 +3614,25 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
|
|
|
3137
3614
|
}
|
|
3138
3615
|
}
|
|
3139
3616
|
function resolveChatBridgePath(moduleUrl = import.meta.url) {
|
|
3140
|
-
const dirname =
|
|
3141
|
-
const jsPath =
|
|
3617
|
+
const dirname = path11.dirname(fileURLToPath(moduleUrl));
|
|
3618
|
+
const jsPath = path11.resolve(dirname, "chat-bridge.js");
|
|
3142
3619
|
try {
|
|
3143
3620
|
accessSync(jsPath);
|
|
3144
3621
|
return jsPath;
|
|
3145
3622
|
} catch {
|
|
3146
|
-
return
|
|
3623
|
+
return path11.resolve(dirname, "chat-bridge.ts");
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
function resolveSlockCliPath(moduleUrl = import.meta.url) {
|
|
3627
|
+
const thisDir = path11.dirname(fileURLToPath(moduleUrl));
|
|
3628
|
+
const bundledDistPath = path11.resolve(thisDir, "cli", "index.js");
|
|
3629
|
+
try {
|
|
3630
|
+
accessSync(bundledDistPath);
|
|
3631
|
+
return bundledDistPath;
|
|
3632
|
+
} catch {
|
|
3633
|
+
const workspaceDistPath = path11.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
|
|
3634
|
+
accessSync(workspaceDistPath);
|
|
3635
|
+
return workspaceDistPath;
|
|
3147
3636
|
}
|
|
3148
3637
|
}
|
|
3149
3638
|
function detectRuntimes() {
|
|
@@ -3196,6 +3685,12 @@ function summarizeIncomingMessage(msg) {
|
|
|
3196
3685
|
return `(directory=${msg.directoryName})`;
|
|
3197
3686
|
case "machine:runtime_models:detect":
|
|
3198
3687
|
return `(runtime=${msg.runtime}, req=${msg.requestId})`;
|
|
3688
|
+
case "reminder.upsert":
|
|
3689
|
+
return `(agent=${msg.agentId}, id=${msg.reminder.reminderId}, v${msg.reminder.version}, fireAt=${msg.reminder.fireAt})`;
|
|
3690
|
+
case "reminder.cancel":
|
|
3691
|
+
return `(agent=${msg.agentId}, id=${msg.reminderId}, v${msg.version})`;
|
|
3692
|
+
case "reminder.snapshot":
|
|
3693
|
+
return `(agent=${msg.agentId}, count=${msg.reminders.length})`;
|
|
3199
3694
|
default:
|
|
3200
3695
|
return "";
|
|
3201
3696
|
}
|
|
@@ -3204,19 +3699,27 @@ var DaemonCore = class {
|
|
|
3204
3699
|
options;
|
|
3205
3700
|
daemonVersion;
|
|
3206
3701
|
chatBridgePath;
|
|
3702
|
+
slockCliPath;
|
|
3207
3703
|
runtimeDetector;
|
|
3208
3704
|
agentManager;
|
|
3209
3705
|
connection;
|
|
3706
|
+
reminderCache;
|
|
3210
3707
|
constructor(options) {
|
|
3211
3708
|
this.options = options;
|
|
3212
3709
|
this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
|
|
3213
3710
|
this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
|
|
3711
|
+
this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
|
|
3214
3712
|
this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
|
|
3713
|
+
this.reminderCache = new ReminderCache({
|
|
3714
|
+
clock: options.reminderClock,
|
|
3715
|
+
onFire: (job) => this.onReminderFire(job)
|
|
3716
|
+
});
|
|
3215
3717
|
let connection;
|
|
3216
3718
|
const agentManagerOptions = {
|
|
3217
3719
|
dataDir: options.dataDir,
|
|
3218
3720
|
serverUrl: options.serverUrl,
|
|
3219
|
-
defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider
|
|
3721
|
+
defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
|
|
3722
|
+
slockCliPath: this.slockCliPath
|
|
3220
3723
|
};
|
|
3221
3724
|
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
3725
|
const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
|
|
@@ -3236,6 +3739,7 @@ var DaemonCore = class {
|
|
|
3236
3739
|
}
|
|
3237
3740
|
async stop() {
|
|
3238
3741
|
logger.info("[Slock Daemon] Shutting down...");
|
|
3742
|
+
this.reminderCache.clear();
|
|
3239
3743
|
await this.agentManager.stopAll();
|
|
3240
3744
|
this.connection.disconnect();
|
|
3241
3745
|
}
|
|
@@ -3330,11 +3834,31 @@ var DaemonCore = class {
|
|
|
3330
3834
|
});
|
|
3331
3835
|
break;
|
|
3332
3836
|
}
|
|
3837
|
+
case "reminder.upsert":
|
|
3838
|
+
this.reminderCache.upsert(msg.reminder);
|
|
3839
|
+
break;
|
|
3840
|
+
case "reminder.cancel":
|
|
3841
|
+
this.reminderCache.cancel(msg.reminderId, msg.version);
|
|
3842
|
+
break;
|
|
3843
|
+
case "reminder.snapshot":
|
|
3844
|
+
logger.info(`[Daemon] Reminder snapshot for agent ${msg.agentId}: ${msg.reminders.length} entries`);
|
|
3845
|
+
this.reminderCache.snapshot(msg.agentId, msg.reminders);
|
|
3846
|
+
break;
|
|
3333
3847
|
case "ping":
|
|
3334
3848
|
this.connection.send({ type: "pong" });
|
|
3335
3849
|
break;
|
|
3336
3850
|
}
|
|
3337
3851
|
}
|
|
3852
|
+
onReminderFire(job) {
|
|
3853
|
+
logger.info(`[Daemon] Reminder ${job.reminderId} fired locally (agent=${job.ownerAgentId})`);
|
|
3854
|
+
this.connection.send({
|
|
3855
|
+
type: "reminder.fire_attempt",
|
|
3856
|
+
agentId: job.ownerAgentId,
|
|
3857
|
+
reminderId: job.reminderId,
|
|
3858
|
+
version: job.version,
|
|
3859
|
+
firedAtClient: (/* @__PURE__ */ new Date()).toISOString()
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3338
3862
|
handleConnect() {
|
|
3339
3863
|
const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
|
|
3340
3864
|
const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
|
|
@@ -3358,6 +3882,13 @@ var DaemonCore = class {
|
|
|
3358
3882
|
for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
|
|
3359
3883
|
this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
|
|
3360
3884
|
}
|
|
3885
|
+
const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
|
|
3886
|
+
for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
|
|
3887
|
+
agentsForSnapshot.add(agentId);
|
|
3888
|
+
}
|
|
3889
|
+
for (const agentId of agentsForSnapshot) {
|
|
3890
|
+
this.connection.send({ type: "reminder.snapshot.request", agentId });
|
|
3891
|
+
}
|
|
3361
3892
|
this.options.lifecycleHooks?.onConnect?.();
|
|
3362
3893
|
}
|
|
3363
3894
|
handleDisconnect() {
|
|
@@ -3374,6 +3905,7 @@ export {
|
|
|
3374
3905
|
parseDaemonCliArgs,
|
|
3375
3906
|
readDaemonVersion,
|
|
3376
3907
|
resolveChatBridgePath,
|
|
3908
|
+
resolveSlockCliPath,
|
|
3377
3909
|
detectRuntimes,
|
|
3378
3910
|
DaemonCore
|
|
3379
3911
|
};
|