@slock-ai/daemon 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +183 -28
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -98,15 +98,17 @@ function toolRef(prefix, name) {
98
98
  }
99
99
  function buildBaseSystemPrompt(config, opts) {
100
100
  const t = (name) => toolRef(opts.toolPrefix, name);
101
+ const messageDeliveryText = opts.includeStdinNotificationSection ? "New messages will be delivered to you automatically via stdin." : "The daemon will automatically restart you when new messages arrive.";
101
102
  const criticalRules = [
102
103
  `- Do NOT output text directly. ALL communication goes through ${t("send_message")}.`,
103
104
  ...opts.extraCriticalRules,
104
- `- Do NOT explore the filesystem looking for messaging scripts. The MCP tools are already available.`
105
+ `- Do NOT explore the filesystem looking for messaging scripts. The MCP tools are already available.`,
106
+ `- NEVER start working on a task without claiming it first via ${t("claim_tasks")}. If the claim fails, do NOT work on it.`
105
107
  ];
106
108
  const startupSteps = [
107
- `1. **Read MEMORY.md** (in your cwd). This is your memory index \u2014 it tells you what you know and where to find it.`,
108
- `2. Follow the instructions in MEMORY.md to read any other memory files you need (e.g. channel summaries, role definitions, user preferences).`,
109
- `3. Stop and wait. New messages will be delivered to you automatically via stdin.`,
109
+ `1. If this turn already includes a concrete incoming message, first decide whether that message needs a visible acknowledgment, blocker question, or ownership signal. If it does, send it early with ${t("send_message")} before deep context gathering.`,
110
+ `2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.`,
111
+ `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
110
112
  `4. When you receive a message, process it and reply with ${t("send_message")}.`,
111
113
  `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.`
112
114
  ];
@@ -217,7 +219,7 @@ Each channel has a task board with two independent dimensions: **status** (progr
217
219
  - **Unclaim**: \`unclaim_task(channel="#channel-name", task_number=3)\` \u2014 remove your assignment. Does not change progress status.
218
220
  - **Update status**: \`update_task_status(channel="#channel-name", task_number=3, status="in_review")\` \u2014 move a task to a new status. Valid transitions: todo\u2192in_progress, in_progress\u2192in_review, in_progress\u2192done, in_review\u2192done, in_review\u2192in_progress.
219
221
 
220
- **CRITICAL: You MUST claim a task before starting work on it.** Never begin working on a task without claiming it first. The claim mechanism prevents multiple agents from doing the same work. If your claim fails (someone else claimed it), move on to another task.
222
+ **CRITICAL: You MUST claim a task before starting ANY work on it.** Call \`${t("claim_tasks")}\` first. If the claim fails (someone else already claimed it), you MUST NOT work on that task \u2014 move on to another one. This is the only way to prevent duplicate work across agents. No exceptions.
221
223
 
222
224
  **IMPORTANT: When you finish a task, use \`update_task_status(..., status="in_review")\`.** This gives humans a chance to validate your work before it's marked as done. Only set status to \`done\` directly for trivial tasks that don't need review.
223
225
 
@@ -250,7 +252,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
250
252
 
251
253
  - **Don't interrupt 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 not at you. Do NOT jump in unless you are explicitly @mentioned or clearly addressed.
252
254
  - **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.
253
- - **Claim before you start.** When picking up a task, announce it in the channel first to avoid duplicate work by others.
255
+ - **Claim before you start.** Always call \`${t("claim_tasks")}\` before doing any work on a task. If the claim fails, stop immediately and pick a different task.
254
256
 
255
257
  ### Formatting \u2014 No HTML
256
258
 
@@ -801,28 +803,48 @@ function toLocalTime(iso) {
801
803
  const pad = (n) => String(n).padStart(2, "0");
802
804
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
803
805
  }
806
+ function formatChannelLabel(message) {
807
+ return message.channel_type === "dm" ? `DM:@${message.channel_name}` : `#${message.channel_name}`;
808
+ }
809
+ function buildUnreadSummary(messages, excludeChannel) {
810
+ const summary = /* @__PURE__ */ new Map();
811
+ for (const message of messages) {
812
+ const label = formatChannelLabel(message);
813
+ if (excludeChannel && label === excludeChannel) continue;
814
+ summary.set(label, (summary.get(label) || 0) + 1);
815
+ }
816
+ return summary.size > 0 ? Object.fromEntries(summary) : void 0;
817
+ }
804
818
  var MAX_TRAJECTORY_TEXT = 2e3;
805
819
  var ACTIVITY_HEARTBEAT_MS = 6e4;
806
- var AgentProcessManager = class {
820
+ function getMessageDeliveryText(supportsStdinNotification) {
821
+ return supportsStdinNotification ? "New messages will be delivered to you automatically via stdin." : "The daemon will automatically restart you when new messages arrive.";
822
+ }
823
+ var AgentProcessManager = class _AgentProcessManager {
807
824
  agents = /* @__PURE__ */ new Map();
808
825
  agentsStarting = /* @__PURE__ */ new Set();
809
826
  // Prevent concurrent starts of same agent
827
+ startingInboxes = /* @__PURE__ */ new Map();
810
828
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
811
829
  idleAgentConfigs = /* @__PURE__ */ new Map();
812
830
  chatBridgePath;
813
831
  sendToServer;
814
832
  daemonApiKey;
815
- constructor(chatBridgePath2, sendToServer, daemonApiKey) {
833
+ dataDir;
834
+ driverResolver;
835
+ constructor(chatBridgePath2, sendToServer, daemonApiKey, opts = {}) {
816
836
  this.chatBridgePath = chatBridgePath2;
817
837
  this.sendToServer = sendToServer;
818
838
  this.daemonApiKey = daemonApiKey;
839
+ this.dataDir = opts.dataDir || DATA_DIR;
840
+ this.driverResolver = opts.driverResolver || getDriver;
819
841
  }
820
- async startAgent(agentId, config, wakeMessage, unreadSummary) {
842
+ async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt) {
821
843
  if (this.agents.has(agentId) || this.agentsStarting.has(agentId)) return;
822
844
  this.agentsStarting.add(agentId);
823
845
  try {
824
- const driver = getDriver(config.runtime || "claude");
825
- const agentDataDir = path3.join(DATA_DIR, agentId);
846
+ const driver = this.driverResolver(config.runtime || "claude");
847
+ const agentDataDir = path3.join(this.dataDir, agentId);
826
848
  await mkdir(agentDataDir, { recursive: true });
827
849
  const memoryMdPath = path3.join(agentDataDir, "MEMORY.md");
828
850
  try {
@@ -845,7 +867,14 @@ ${config.description || "No role defined yet."}
845
867
  await mkdir(path3.join(agentDataDir, "notes"), { recursive: true });
846
868
  const isResume = !!config.sessionId;
847
869
  let prompt;
848
- if (isResume && wakeMessage) {
870
+ if (isResume && resumePrompt) {
871
+ prompt = resumePrompt;
872
+ if (driver.supportsStdinNotification) {
873
+ prompt += `
874
+
875
+ Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.`;
876
+ }
877
+ } else if (isResume && wakeMessage) {
849
878
  const channelLabel = wakeMessage.channel_type === "dm" ? `DM:@${wakeMessage.channel_name}` : `#${wakeMessage.channel_name}`;
850
879
  const senderPrefix = wakeMessage.sender_type === "agent" ? "(agent) " : "";
851
880
  const time = wakeMessage.timestamp ? ` (${toLocalTime(wakeMessage.timestamp)})` : "";
@@ -872,7 +901,7 @@ Use read_history to catch up, or respond to the message above first.`;
872
901
 
873
902
  Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
874
903
 
875
- IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. New messages will be delivered to you automatically via stdin.`;
904
+ IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
876
905
  if (driver.supportsStdinNotification) {
877
906
  prompt += `
878
907
 
@@ -886,14 +915,9 @@ Note: While you are busy, you may receive [System notification: ...] messages. F
886
915
  }
887
916
  prompt += `
888
917
 
889
- Use read_history to catch up on important channels, then stop. New messages will be delivered to you automatically.`;
890
- if (driver.supportsStdinNotification) {
891
- prompt += `
892
-
893
- Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.`;
894
- }
918
+ Use read_history to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call check_messages in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: reply with send_message and claim the relevant task before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
895
919
  } else if (isResume) {
896
- prompt = `No new messages while you were away. Nothing to do \u2014 just stop. New messages will be delivered to you automatically via stdin.`;
920
+ prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
897
921
  if (driver.supportsStdinNotification) {
898
922
  prompt += `
899
923
 
@@ -913,7 +937,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
913
937
  const agentProcess = {
914
938
  process: proc,
915
939
  driver,
916
- inbox: [],
940
+ inbox: this.startingInboxes.get(agentId) || [],
917
941
  config,
918
942
  sessionId: config.sessionId || null,
919
943
  isIdle: false,
@@ -923,6 +947,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
923
947
  lastActivity: "",
924
948
  lastActivityDetail: ""
925
949
  };
950
+ this.startingInboxes.delete(agentId);
926
951
  this.agents.set(agentId, agentProcess);
927
952
  this.agentsStarting.delete(agentId);
928
953
  let buffer = "";
@@ -957,6 +982,26 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
957
982
  }
958
983
  this.agents.delete(agentId);
959
984
  if (code === 0) {
985
+ const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
986
+ const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
987
+ if (queuedWakeMessage) {
988
+ const nextConfig = { ...ap.config, sessionId: ap.sessionId };
989
+ this.idleAgentConfigs.set(agentId, {
990
+ config: nextConfig,
991
+ sessionId: ap.sessionId
992
+ });
993
+ this.broadcastActivity(agentId, "working", "Message received");
994
+ this.idleAgentConfigs.delete(agentId);
995
+ this.startAgent(agentId, nextConfig, queuedWakeMessage, unreadSummary2).catch((err) => {
996
+ console.error(`[Agent ${agentId}] Failed to continue with queued message:`, err);
997
+ this.idleAgentConfigs.set(agentId, {
998
+ config: nextConfig,
999
+ sessionId: ap.sessionId
1000
+ });
1001
+ this.broadcastActivity(agentId, "online", "Process idle");
1002
+ });
1003
+ return;
1004
+ }
960
1005
  this.idleAgentConfigs.set(agentId, {
961
1006
  config: { ...ap.config, sessionId: ap.sessionId },
962
1007
  sessionId: ap.sessionId
@@ -1017,6 +1062,12 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1017
1062
  deliverMessage(agentId, message) {
1018
1063
  const ap = this.agents.get(agentId);
1019
1064
  if (!ap) {
1065
+ if (this.agentsStarting.has(agentId)) {
1066
+ const pending = this.startingInboxes.get(agentId) || [];
1067
+ pending.push(message);
1068
+ this.startingInboxes.set(agentId, pending);
1069
+ return;
1070
+ }
1020
1071
  const cached = this.idleAgentConfigs.get(agentId);
1021
1072
  if (cached) {
1022
1073
  console.log(`[Agent ${agentId}] Auto-restarting idle process for incoming message`);
@@ -1044,7 +1095,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1044
1095
  }
1045
1096
  }
1046
1097
  async resetWorkspace(agentId) {
1047
- const agentDataDir = path3.join(DATA_DIR, agentId);
1098
+ const agentDataDir = path3.join(this.dataDir, agentId);
1048
1099
  try {
1049
1100
  await rm(agentDataDir, { recursive: true, force: true });
1050
1101
  console.log(`[Agent ${agentId}] Workspace deleted: ${agentDataDir}`);
@@ -1065,13 +1116,13 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1065
1116
  const results = [];
1066
1117
  let entries;
1067
1118
  try {
1068
- entries = await readdir(DATA_DIR, { withFileTypes: true });
1119
+ entries = await readdir(this.dataDir, { withFileTypes: true });
1069
1120
  } catch {
1070
1121
  return [];
1071
1122
  }
1072
1123
  for (const entry of entries) {
1073
1124
  if (!entry.isDirectory()) continue;
1074
- const dirPath = path3.join(DATA_DIR, entry.name);
1125
+ const dirPath = path3.join(this.dataDir, entry.name);
1075
1126
  try {
1076
1127
  const dirContents = await readdir(dirPath, { withFileTypes: true });
1077
1128
  let totalSize = 0;
@@ -1108,7 +1159,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1108
1159
  if (directoryName.includes("/") || directoryName.includes("..") || directoryName.includes("\\")) {
1109
1160
  return false;
1110
1161
  }
1111
- const targetDir = path3.join(DATA_DIR, directoryName);
1162
+ const targetDir = path3.join(this.dataDir, directoryName);
1112
1163
  try {
1113
1164
  await rm(targetDir, { recursive: true, force: true });
1114
1165
  console.log(`[Workspace] Deleted directory: ${targetDir}`);
@@ -1120,7 +1171,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1120
1171
  }
1121
1172
  // Workspace file browsing
1122
1173
  async getFileTree(agentId, dirPath) {
1123
- const agentDir = path3.join(DATA_DIR, agentId);
1174
+ const agentDir = path3.join(this.dataDir, agentId);
1124
1175
  try {
1125
1176
  await stat(agentDir);
1126
1177
  } catch {
@@ -1137,7 +1188,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1137
1188
  return this.listDirectoryChildren(targetDir, agentDir);
1138
1189
  }
1139
1190
  async readFile(agentId, filePath) {
1140
- const agentDir = path3.join(DATA_DIR, agentId);
1191
+ const agentDir = path3.join(this.dataDir, agentId);
1141
1192
  const resolved = path3.resolve(agentDir, filePath);
1142
1193
  if (!resolved.startsWith(agentDir + path3.sep) && resolved !== agentDir) {
1143
1194
  throw new Error("Access denied");
@@ -1171,6 +1222,102 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1171
1222
  const content = await readFile(resolved, "utf-8");
1172
1223
  return { content, binary: false };
1173
1224
  }
1225
+ // Skill scanning
1226
+ // Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
1227
+ // To add a new runtime, add an entry here.
1228
+ static SKILL_PATHS = {
1229
+ claude: {
1230
+ // Claude reads shared skills via symlinks in ~/.claude/skills/, not from ~/.agents/skills/
1231
+ global: [".claude/skills", ".claude/commands"],
1232
+ workspace: [".claude/skills", ".claude/commands"]
1233
+ },
1234
+ codex: {
1235
+ // Codex natively scans ~/.agents/skills/ and has built-in .system skills
1236
+ global: [".codex/skills", ".codex/skills/.system", ".agents/skills"],
1237
+ workspace: [".codex/skills", ".agents/skills"]
1238
+ }
1239
+ };
1240
+ async listSkills(agentId, runtimeHint) {
1241
+ const agent = this.agents.get(agentId);
1242
+ const runtime = runtimeHint || agent?.config.runtime || "claude";
1243
+ const home = os.homedir();
1244
+ const workspaceDir = path3.join(this.dataDir, agentId);
1245
+ const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
1246
+ const globalResults = await Promise.all(
1247
+ paths.global.map((p) => this.scanSkillsDir(path3.join(home, p)))
1248
+ );
1249
+ const workspaceResults = await Promise.all(
1250
+ paths.workspace.map((p) => this.scanSkillsDir(path3.join(workspaceDir, p)))
1251
+ );
1252
+ const dedup = (skills) => {
1253
+ const seen = /* @__PURE__ */ new Set();
1254
+ return skills.filter((s) => {
1255
+ if (seen.has(s.name)) return false;
1256
+ seen.add(s.name);
1257
+ return true;
1258
+ });
1259
+ };
1260
+ const shorten = (skills) => skills.map((s) => ({
1261
+ ...s,
1262
+ sourcePath: s.sourcePath?.startsWith(home) ? "~" + s.sourcePath.slice(home.length) : s.sourcePath
1263
+ }));
1264
+ return {
1265
+ global: shorten(dedup(globalResults.flat())),
1266
+ workspace: shorten(dedup(workspaceResults.flat()))
1267
+ };
1268
+ }
1269
+ async scanSkillsDir(dir) {
1270
+ let entries;
1271
+ try {
1272
+ entries = await readdir(dir, { withFileTypes: true });
1273
+ } catch {
1274
+ return [];
1275
+ }
1276
+ const skills = [];
1277
+ for (const entry of entries) {
1278
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
1279
+ const skillMd = path3.join(dir, entry.name, "SKILL.md");
1280
+ try {
1281
+ const content = await readFile(skillMd, "utf-8");
1282
+ const skill = this.parseSkillMd(entry.name, content);
1283
+ skill.sourcePath = dir;
1284
+ skills.push(skill);
1285
+ } catch {
1286
+ }
1287
+ } else if (entry.name.endsWith(".md")) {
1288
+ const cmdName = entry.name.replace(/\.md$/, "");
1289
+ try {
1290
+ const content = await readFile(path3.join(dir, entry.name), "utf-8");
1291
+ const skill = this.parseSkillMd(cmdName, content);
1292
+ skill.sourcePath = dir;
1293
+ skills.push(skill);
1294
+ } catch {
1295
+ }
1296
+ }
1297
+ }
1298
+ return skills;
1299
+ }
1300
+ parseSkillMd(dirName, content) {
1301
+ const info = {
1302
+ name: dirName,
1303
+ displayName: dirName,
1304
+ description: "",
1305
+ userInvocable: false
1306
+ };
1307
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1308
+ if (!match) return info;
1309
+ const frontmatter = match[1];
1310
+ for (const line of frontmatter.split("\n")) {
1311
+ const colonIdx = line.indexOf(":");
1312
+ if (colonIdx === -1) continue;
1313
+ const key = line.slice(0, colonIdx).trim();
1314
+ const value = line.slice(colonIdx + 1).trim();
1315
+ if (key === "name") info.displayName = value;
1316
+ if (key === "description") info.description = value;
1317
+ if (key === "user-invocable") info.userInvocable = value === "true";
1318
+ }
1319
+ return info;
1320
+ }
1174
1321
  // Private methods
1175
1322
  /**
1176
1323
  * Broadcast an activity change — emits a single agent:activity event that carries
@@ -1419,7 +1566,7 @@ connection = new DaemonConnection({
1419
1566
  switch (msg.type) {
1420
1567
  case "agent:start":
1421
1568
  console.log(`[Daemon] Starting agent ${msg.agentId} (model: ${msg.config.model}, session: ${msg.config.sessionId || "new"}${msg.wakeMessage ? ", with wake message" : ""})`);
1422
- agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary).catch((err) => {
1569
+ agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary, msg.resumePrompt).catch((err) => {
1423
1570
  const reason = err instanceof Error ? err.message : String(err);
1424
1571
  console.error(`[Daemon] Failed to start agent ${msg.agentId}:`, reason);
1425
1572
  connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive" });
@@ -1463,6 +1610,14 @@ connection = new DaemonConnection({
1463
1610
  });
1464
1611
  });
1465
1612
  break;
1613
+ case "agent:skills:list":
1614
+ agentManager.listSkills(msg.agentId, msg.runtime).then(({ global, workspace }) => {
1615
+ connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global, workspace });
1616
+ }).catch((err) => {
1617
+ console.error(`[Daemon] Failed to list skills for ${msg.agentId}:`, err);
1618
+ connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
1619
+ });
1620
+ break;
1466
1621
  case "machine:workspace:scan":
1467
1622
  console.log("[Daemon] Scanning all workspace directories");
1468
1623
  agentManager.scanAllWorkspaces().then((directories) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"
@@ -32,6 +32,7 @@
32
32
  "dev": "tsx watch src/index.ts",
33
33
  "start": "tsx src/index.ts",
34
34
  "build": "tsup",
35
+ "test": "node --import tsx --test src/**/*.test.ts",
35
36
  "typecheck": "tsc --noEmit",
36
37
  "release:patch": "npm version patch --no-git-tag-version && cd ../.. && pnpm install --lockfile-only && git add packages/daemon/package.json pnpm-lock.yaml && git commit -m \"chore: bump @slock-ai/daemon to v$(node -p \"require('./packages/daemon/package.json').version\")\" && git tag daemon-v$(node -p \"require('./packages/daemon/package.json').version\") && git push && git push --tags",
37
38
  "release:minor": "npm version minor --no-git-tag-version && cd ../.. && pnpm install --lockfile-only && git add packages/daemon/package.json pnpm-lock.yaml && git commit -m \"chore: bump @slock-ai/daemon to v$(node -p \"require('./packages/daemon/package.json').version\")\" && git tag daemon-v$(node -p \"require('./packages/daemon/package.json').version\") && git push && git push --tags",