@slock-ai/daemon 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,12 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
+ function toLocalTime(iso) {
8
+ const d = new Date(iso);
9
+ if (isNaN(d.getTime())) return iso;
10
+ const pad = (n) => String(n).padStart(2, "0");
11
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
12
+ }
7
13
  var args = process.argv.slice(2);
8
14
  var agentId = "";
9
15
  var serverUrl = "http://localhost:3001";
@@ -92,7 +98,8 @@ server.tool(
92
98
  const formatted = data.messages.map((m) => {
93
99
  const channel = m.channel_type === "dm" ? `DM:@${m.channel_name}` : `#${m.channel_name}`;
94
100
  const senderPrefix = m.sender_type === "agent" ? "(agent) " : "";
95
- return `[${channel}] ${senderPrefix}@${m.sender_name}: ${m.content}`;
101
+ const time = m.timestamp ? ` (${toLocalTime(m.timestamp)})` : "";
102
+ return `[${channel}]${time} ${senderPrefix}@${m.sender_name}: ${m.content}`;
96
103
  }).join("\n");
97
104
  return {
98
105
  content: [{ type: "text", text: formatted }]
@@ -193,7 +200,8 @@ server.tool(
193
200
  }
194
201
  const formatted = data.messages.map((m) => {
195
202
  const senderPrefix = m.senderType === "agent" ? "(agent) " : "";
196
- return `[seq:${m.seq}] ${senderPrefix}@${m.senderName}: ${m.content}`;
203
+ const time = m.createdAt ? ` (${toLocalTime(m.createdAt)})` : "";
204
+ return `[seq:${m.seq}]${time} ${senderPrefix}@${m.senderName}: ${m.content}`;
197
205
  }).join("\n");
198
206
  let footer = "";
199
207
  if (data.historyLimited) {
package/dist/index.js CHANGED
@@ -622,9 +622,17 @@ function getDriver(runtimeId) {
622
622
 
623
623
  // src/agentProcessManager.ts
624
624
  var DATA_DIR = path2.join(os.homedir(), ".slock", "agents");
625
+ function toLocalTime(iso) {
626
+ const d = new Date(iso);
627
+ if (isNaN(d.getTime())) return iso;
628
+ const pad = (n) => String(n).padStart(2, "0");
629
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
630
+ }
625
631
  var MAX_TRAJECTORY_TEXT = 2e3;
626
632
  var AgentProcessManager = class {
627
633
  agents = /* @__PURE__ */ new Map();
634
+ agentsStarting = /* @__PURE__ */ new Set();
635
+ // Prevent concurrent starts of same agent
628
636
  chatBridgePath;
629
637
  sendToServer;
630
638
  daemonApiKey;
@@ -634,16 +642,18 @@ var AgentProcessManager = class {
634
642
  this.daemonApiKey = daemonApiKey;
635
643
  }
636
644
  async startAgent(agentId, config, wakeMessage, unreadSummary) {
637
- if (this.agents.has(agentId)) return;
638
- const driver = getDriver(config.runtime || "claude");
639
- const agentDataDir = path2.join(DATA_DIR, agentId);
640
- await mkdir(agentDataDir, { recursive: true });
641
- const memoryMdPath = path2.join(agentDataDir, "MEMORY.md");
645
+ if (this.agents.has(agentId) || this.agentsStarting.has(agentId)) return;
646
+ this.agentsStarting.add(agentId);
642
647
  try {
643
- await access(memoryMdPath);
644
- } catch {
645
- const agentName = config.displayName || config.name;
646
- const initialMemoryMd = `# ${agentName}
648
+ const driver = getDriver(config.runtime || "claude");
649
+ const agentDataDir = path2.join(DATA_DIR, agentId);
650
+ await mkdir(agentDataDir, { recursive: true });
651
+ const memoryMdPath = path2.join(agentDataDir, "MEMORY.md");
652
+ try {
653
+ await access(memoryMdPath);
654
+ } catch {
655
+ const agentName = config.displayName || config.name;
656
+ const initialMemoryMd = `# ${agentName}
647
657
 
648
658
  ## Role
649
659
  ${config.description || "No role defined yet."}
@@ -654,122 +664,136 @@ ${config.description || "No role defined yet."}
654
664
  ## Active Context
655
665
  - First startup.
656
666
  `;
657
- await writeFile(memoryMdPath, initialMemoryMd);
658
- }
659
- await mkdir(path2.join(agentDataDir, "notes"), { recursive: true });
660
- const isResume = !!config.sessionId;
661
- let prompt;
662
- if (isResume && wakeMessage) {
663
- const channelLabel = wakeMessage.channel_type === "dm" ? `DM:@${wakeMessage.channel_name}` : `#${wakeMessage.channel_name}`;
664
- const senderPrefix = wakeMessage.sender_type === "agent" ? "(agent) " : "";
665
- const formatted = `[${channelLabel}] ${senderPrefix}@${wakeMessage.sender_name}: ${wakeMessage.content}`;
666
- prompt = `New message received:
667
+ await writeFile(memoryMdPath, initialMemoryMd);
668
+ }
669
+ await mkdir(path2.join(agentDataDir, "notes"), { recursive: true });
670
+ const isResume = !!config.sessionId;
671
+ let prompt;
672
+ if (isResume && wakeMessage) {
673
+ const channelLabel = wakeMessage.channel_type === "dm" ? `DM:@${wakeMessage.channel_name}` : `#${wakeMessage.channel_name}`;
674
+ const senderPrefix = wakeMessage.sender_type === "agent" ? "(agent) " : "";
675
+ const time = wakeMessage.timestamp ? ` (${toLocalTime(wakeMessage.timestamp)})` : "";
676
+ const formatted = `[${channelLabel}]${time} ${senderPrefix}@${wakeMessage.sender_name}: ${wakeMessage.content}`;
677
+ prompt = `New message received:
667
678
 
668
679
  ${formatted}`;
669
- if (unreadSummary && Object.keys(unreadSummary).length > 0) {
670
- const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
671
- if (otherUnread.length > 0) {
672
- prompt += `
680
+ if (unreadSummary && Object.keys(unreadSummary).length > 0) {
681
+ const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
682
+ if (otherUnread.length > 0) {
683
+ prompt += `
673
684
 
674
685
  You also have unread messages in other channels:`;
675
- for (const [ch, count] of otherUnread) {
676
- prompt += `
686
+ for (const [ch, count] of otherUnread) {
687
+ prompt += `
677
688
  - ${ch}: ${count} unread`;
678
- }
679
- prompt += `
689
+ }
690
+ prompt += `
680
691
 
681
692
  Use read_history to catch up, or respond to the message above first.`;
693
+ }
682
694
  }
683
- }
684
- prompt += `
695
+ prompt += `
685
696
 
686
697
  Respond as appropriate \u2014 reply using send_message, or take action as needed. Then call receive_message(block=true) to keep listening.`;
687
- if (driver.supportsStdinNotification) {
688
- prompt += `
698
+ if (driver.supportsStdinNotification) {
699
+ prompt += `
689
700
 
690
701
  Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
691
- }
692
- } else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
693
- prompt = `You have unread messages from while you were offline:`;
694
- for (const [ch, count] of Object.entries(unreadSummary)) {
695
- prompt += `
702
+ }
703
+ } else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
704
+ prompt = `You have unread messages from while you were offline:`;
705
+ for (const [ch, count] of Object.entries(unreadSummary)) {
706
+ prompt += `
696
707
  - ${ch}: ${count} unread`;
697
- }
698
- prompt += `
708
+ }
709
+ prompt += `
699
710
 
700
711
  Use read_history to catch up on important channels, then call receive_message(block=true) to listen for new messages.`;
701
- if (driver.supportsStdinNotification) {
702
- prompt += `
712
+ if (driver.supportsStdinNotification) {
713
+ prompt += `
703
714
 
704
715
  Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
705
- }
706
- } else if (isResume) {
707
- prompt = `No new messages while you were away. Call ${driver.mcpToolPrefix}receive_message(block=true) to listen for new messages.`;
708
- if (driver.supportsStdinNotification) {
709
- prompt += `
716
+ }
717
+ } else if (isResume) {
718
+ prompt = `No new messages while you were away. Call ${driver.mcpToolPrefix}receive_message(block=true) to listen for new messages.`;
719
+ if (driver.supportsStdinNotification) {
720
+ prompt += `
710
721
 
711
722
  Note: While you are busy, you may receive [System notification: ...] messages about new messages. Finish your current step, then call receive_message to check.`;
712
- }
713
- } else {
714
- prompt = driver.buildSystemPrompt(config, agentId);
715
- }
716
- const { process: proc } = driver.spawn({
717
- agentId,
718
- config,
719
- prompt,
720
- workingDirectory: agentDataDir,
721
- chatBridgePath: this.chatBridgePath,
722
- daemonApiKey: this.daemonApiKey
723
- });
724
- const agentProcess = {
725
- process: proc,
726
- driver,
727
- inbox: [],
728
- pendingReceive: null,
729
- config,
730
- sessionId: config.sessionId || null,
731
- isInReceiveMessage: false,
732
- notificationTimer: null,
733
- pendingNotificationCount: 0
734
- };
735
- this.agents.set(agentId, agentProcess);
736
- let buffer = "";
737
- proc.stdout?.on("data", (chunk) => {
738
- buffer += chunk.toString();
739
- const lines = buffer.split("\n");
740
- buffer = lines.pop() || "";
741
- for (const line of lines) {
742
- if (!line.trim()) continue;
743
- const events = driver.parseLine(line);
744
- for (const event of events) {
745
- this.handleParsedEvent(agentId, event, driver);
746
723
  }
724
+ } else {
725
+ prompt = driver.buildSystemPrompt(config, agentId);
747
726
  }
748
- });
749
- proc.stderr?.on("data", (chunk) => {
750
- const text = chunk.toString().trim();
751
- if (!text) return;
752
- if (/Reconnecting\.\.\.|Falling back from WebSockets/i.test(text)) return;
753
- console.error(`[Agent ${agentId} stderr]: ${text}`);
754
- });
755
- proc.on("exit", (code) => {
756
- console.log(`[Agent ${agentId}] Process exited with code ${code}`);
757
- if (this.agents.has(agentId)) {
758
- const ap = this.agents.get(agentId);
759
- if (ap.pendingReceive) {
760
- clearTimeout(ap.pendingReceive.timer);
761
- ap.pendingReceive.resolve([]);
727
+ const { process: proc } = driver.spawn({
728
+ agentId,
729
+ config,
730
+ prompt,
731
+ workingDirectory: agentDataDir,
732
+ chatBridgePath: this.chatBridgePath,
733
+ daemonApiKey: this.daemonApiKey
734
+ });
735
+ const agentProcess = {
736
+ process: proc,
737
+ driver,
738
+ inbox: [],
739
+ pendingReceive: null,
740
+ config,
741
+ sessionId: config.sessionId || null,
742
+ isInReceiveMessage: false,
743
+ notificationTimer: null,
744
+ pendingNotificationCount: 0
745
+ };
746
+ this.agents.set(agentId, agentProcess);
747
+ this.agentsStarting.delete(agentId);
748
+ let buffer = "";
749
+ proc.stdout?.on("data", (chunk) => {
750
+ buffer += chunk.toString();
751
+ const lines = buffer.split("\n");
752
+ buffer = lines.pop() || "";
753
+ for (const line of lines) {
754
+ if (!line.trim()) continue;
755
+ const events = driver.parseLine(line);
756
+ for (const event of events) {
757
+ this.handleParsedEvent(agentId, event, driver);
758
+ }
762
759
  }
763
- if (ap.notificationTimer) {
764
- clearTimeout(ap.notificationTimer);
760
+ });
761
+ proc.stderr?.on("data", (chunk) => {
762
+ const text = chunk.toString().trim();
763
+ if (!text) return;
764
+ if (/Reconnecting\.\.\.|Falling back from WebSockets/i.test(text)) return;
765
+ console.error(`[Agent ${agentId} stderr]: ${text}`);
766
+ });
767
+ proc.on("exit", (code) => {
768
+ console.log(`[Agent ${agentId}] Process exited with code ${code}`);
769
+ if (this.agents.has(agentId)) {
770
+ const ap = this.agents.get(agentId);
771
+ if (ap.process !== proc) return;
772
+ if (ap.pendingReceive) {
773
+ clearTimeout(ap.pendingReceive.timer);
774
+ ap.pendingReceive.resolve([]);
775
+ }
776
+ if (ap.notificationTimer) {
777
+ clearTimeout(ap.notificationTimer);
778
+ }
779
+ this.agents.delete(agentId);
780
+ if (code === 0) {
781
+ this.sendToServer({ type: "agent:status", agentId, status: "sleeping" });
782
+ this.sendToServer({ type: "agent:activity", agentId, activity: "sleeping", detail: "" });
783
+ } else {
784
+ const reason = code === null ? "killed by signal" : `exit code ${code}`;
785
+ console.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
786
+ this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
787
+ this.sendToServer({ type: "agent:activity", agentId, activity: "offline", detail: `Crashed (${reason})` });
788
+ }
765
789
  }
766
- this.agents.delete(agentId);
767
- this.sendToServer({ type: "agent:status", agentId, status: "sleeping" });
768
- this.sendToServer({ type: "agent:activity", agentId, activity: "sleeping", detail: "" });
769
- }
770
- });
771
- this.sendToServer({ type: "agent:status", agentId, status: "active" });
772
- this.sendToServer({ type: "agent:activity", agentId, activity: "working", detail: "Starting\u2026" });
790
+ });
791
+ this.sendToServer({ type: "agent:status", agentId, status: "active" });
792
+ this.sendToServer({ type: "agent:activity", agentId, activity: "working", detail: "Starting\u2026" });
793
+ } catch (err) {
794
+ this.agentsStarting.delete(agentId);
795
+ throw err;
796
+ }
773
797
  }
774
798
  async stopAgent(agentId) {
775
799
  const ap = this.agents.get(agentId);
@@ -896,15 +920,22 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
896
920
  }
897
921
  }
898
922
  // Workspace file browsing
899
- async getFileTree(agentId) {
923
+ async getFileTree(agentId, dirPath) {
900
924
  const agentDir = path2.join(DATA_DIR, agentId);
901
925
  try {
902
926
  await stat(agentDir);
903
927
  } catch {
904
928
  return [];
905
929
  }
906
- const count = { n: 0 };
907
- return this.buildFileTree(agentDir, agentDir, count);
930
+ let targetDir = agentDir;
931
+ if (dirPath) {
932
+ const resolved = path2.resolve(agentDir, dirPath);
933
+ if (!resolved.startsWith(agentDir + path2.sep) && resolved !== agentDir) {
934
+ return [];
935
+ }
936
+ targetDir = resolved;
937
+ }
938
+ return this.listDirectoryChildren(targetDir, agentDir);
908
939
  }
909
940
  async readFile(agentId, filePath) {
910
941
  const agentDir = path2.join(DATA_DIR, agentId);
@@ -1031,7 +1062,8 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1031
1062
  ap.process.stdin?.write(encoded + "\n");
1032
1063
  }
1033
1064
  }
1034
- async buildFileTree(dir, rootDir, count) {
1065
+ /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
1066
+ async listDirectoryChildren(dir, rootDir) {
1035
1067
  let entries;
1036
1068
  try {
1037
1069
  entries = await readdir(dir, { withFileTypes: true });
@@ -1045,7 +1077,6 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1045
1077
  });
1046
1078
  const nodes = [];
1047
1079
  for (const entry of entries) {
1048
- if (count.n >= 500) break;
1049
1080
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1050
1081
  const fullPath = path2.join(dir, entry.name);
1051
1082
  const relativePath = path2.relative(rootDir, fullPath);
@@ -1055,10 +1086,8 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1055
1086
  } catch {
1056
1087
  continue;
1057
1088
  }
1058
- count.n++;
1059
1089
  if (entry.isDirectory()) {
1060
- const children = await this.buildFileTree(fullPath, rootDir, count);
1061
- nodes.push({ name: entry.name, path: relativePath, isDirectory: true, size: 0, modifiedAt: info.mtime.toISOString(), children });
1090
+ nodes.push({ name: entry.name, path: relativePath, isDirectory: true, size: 0, modifiedAt: info.mtime.toISOString() });
1062
1091
  } else {
1063
1092
  nodes.push({ name: entry.name, path: relativePath, isDirectory: false, size: info.size, modifiedAt: info.mtime.toISOString() });
1064
1093
  }
@@ -1117,7 +1146,12 @@ connection = new DaemonConnection({
1117
1146
  switch (msg.type) {
1118
1147
  case "agent:start":
1119
1148
  console.log(`[Daemon] Starting agent ${msg.agentId} (model: ${msg.config.model}, session: ${msg.config.sessionId || "new"}${msg.wakeMessage ? ", with wake message" : ""})`);
1120
- agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary);
1149
+ agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary).catch((err) => {
1150
+ const reason = err instanceof Error ? err.message : String(err);
1151
+ console.error(`[Daemon] Failed to start agent ${msg.agentId}:`, reason);
1152
+ connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive" });
1153
+ connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: `Start failed: ${reason}` });
1154
+ });
1121
1155
  break;
1122
1156
  case "agent:stop":
1123
1157
  console.log(`[Daemon] Stopping agent ${msg.agentId}`);
@@ -1137,8 +1171,8 @@ connection = new DaemonConnection({
1137
1171
  connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
1138
1172
  break;
1139
1173
  case "agent:workspace:list":
1140
- agentManager.getFileTree(msg.agentId).then((files) => {
1141
- connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files });
1174
+ agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
1175
+ connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
1142
1176
  });
1143
1177
  break;
1144
1178
  case "agent:workspace:read":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"