@slock-ai/daemon 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "src/index.ts"
@@ -33,8 +33,8 @@
33
33
  "start": "tsx src/index.ts",
34
34
  "build": "tsc",
35
35
  "typecheck": "tsc --noEmit",
36
- "release:patch": "npm version patch && git push && git push --tags",
37
- "release:minor": "npm version minor && git push && git push --tags",
38
- "release:major": "npm version major && git push && git push --tags"
36
+ "release:patch": "npm version patch --no-git-tag-version && cd ../.. && git add packages/daemon/package.json && 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
+ "release:minor": "npm version minor --no-git-tag-version && cd ../.. && git add packages/daemon/package.json && 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",
38
+ "release:major": "npm version major --no-git-tag-version && cd ../.. && git add packages/daemon/package.json && 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"
39
39
  }
40
40
  }
@@ -35,7 +35,7 @@ export class AgentProcessManager {
35
35
  this.daemonApiKey = daemonApiKey;
36
36
  }
37
37
 
38
- async startAgent(agentId: string, config: AgentConfig) {
38
+ async startAgent(agentId: string, config: AgentConfig, wakeMessage?: AgentMessage, unreadSummary?: Record<string, number>) {
39
39
  if (this.agents.has(agentId)) return;
40
40
 
41
41
  const agentDataDir = path.join(DATA_DIR, agentId);
@@ -55,9 +55,40 @@ export class AgentProcessManager {
55
55
 
56
56
  // Build prompt
57
57
  const isResume = !!config.sessionId;
58
- const prompt = isResume
59
- ? "New message waiting. Call mcp__chat__receive_message(block=true) to read it, then reply with mcp__chat__send_message. After replying, call mcp__chat__receive_message(block=true) again to keep listening for more messages.\n\nNote: While you are busy, you may receive [System notification: ...] messages about new messages. Finish your current step, then call receive_message to check."
60
- : this.buildSystemPrompt(config, agentId);
58
+ let prompt: string;
59
+ if (isResume && wakeMessage) {
60
+ // Format the wake message inline so the agent sees it immediately
61
+ const channelLabel = wakeMessage.channel_type === "dm"
62
+ ? `DM:@${wakeMessage.channel_name}` : `#${wakeMessage.channel_name}`;
63
+ const senderPrefix = wakeMessage.sender_type === "agent" ? "(agent) " : "";
64
+ const formatted = `[${channelLabel}] ${senderPrefix}@${wakeMessage.sender_name}: ${wakeMessage.content}`;
65
+ prompt = `New message received:\n\n${formatted}`;
66
+
67
+ // Add unread summary for other channels (exclude the wake message's channel)
68
+ if (unreadSummary && Object.keys(unreadSummary).length > 0) {
69
+ const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
70
+ if (otherUnread.length > 0) {
71
+ prompt += `\n\nYou also have unread messages in other channels:`;
72
+ for (const [ch, count] of otherUnread) {
73
+ prompt += `\n- ${ch}: ${count} unread`;
74
+ }
75
+ prompt += `\n\nUse read_history to catch up, or respond to the message above first.`;
76
+ }
77
+ }
78
+
79
+ prompt += `\n\nRespond as appropriate — reply using send_message, or take action as needed. Then call receive_message(block=true) to keep listening.\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
80
+ } else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
81
+ // Resuming with unread messages (e.g. agent was stopped, messages arrived, then restarted)
82
+ prompt = `You have unread messages from while you were offline:`;
83
+ for (const [ch, count] of Object.entries(unreadSummary)) {
84
+ prompt += `\n- ${ch}: ${count} unread`;
85
+ }
86
+ prompt += `\n\nUse read_history to catch up on important channels, then call receive_message(block=true) to listen for new messages.\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call receive_message to check.`;
87
+ } else if (isResume) {
88
+ prompt = "No new messages while you were away. Call mcp__chat__receive_message(block=true) to listen for new messages.\n\nNote: While you are busy, you may receive [System notification: ...] messages about new messages. Finish your current step, then call receive_message to check.";
89
+ } else {
90
+ prompt = this.buildSystemPrompt(config, agentId);
91
+ }
61
92
 
62
93
  // Build MCP config — point to remote server
63
94
  // Use daemon's own API key for chat-bridge authentication with /internal/* routes
@@ -535,7 +566,7 @@ export class AgentProcessManager {
535
566
  }
536
567
 
537
568
  private buildSystemPrompt(config: AgentConfig, agentId: string): string {
538
- let systemPrompt = `You are "${config.displayName || config.name}", an AI agent in Slock — a collaborative workspace for human-AI collaboration.
569
+ let systemPrompt = `You are "${config.displayName || config.name}", an AI agent in Slock — a collaborative platform for human-AI collaboration.
539
570
 
540
571
  ## Who you are
541
572
 
@@ -547,7 +578,7 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
547
578
 
548
579
  1. **mcp__chat__receive_message** — Call with block=true to wait for messages. This is your main loop.
549
580
  2. **mcp__chat__send_message** — Send a message to a channel or DM.
550
- 3. **mcp__chat__list_workspace** — List all channels, agents, and humans in the workspace.
581
+ 3. **mcp__chat__list_server** — List all channels, agents, and humans in this server.
551
582
  4. **mcp__chat__read_history** — Read past messages from a channel or DM.
552
583
 
553
584
  CRITICAL RULES:
@@ -576,13 +607,13 @@ The \`[...]\` prefix identifies where the message came from. Reuse it as the \`c
576
607
 
577
608
  - **Reply to a channel**: \`send_message(channel="#channel-name", content="...")\`
578
609
  - **Reply to a DM**: \`send_message(channel="DM:@peer-name", content="...")\` — reuse the channel value from the received message
579
- - **Start a NEW DM**: \`send_message(dm_to="peer-name", content="...")\` — use the human's name from list_workspace (no @ prefix)
610
+ - **Start a NEW DM**: \`send_message(dm_to="peer-name", content="...")\` — use the human's name from list_server (no @ prefix)
580
611
 
581
612
  **IMPORTANT**: To reply to any message (channel or DM), always use \`channel\` with the exact identifier from the received message. Only use \`dm_to\` when you want to start a brand new DM that doesn't exist yet.
582
613
 
583
614
  ### Discovering people and channels
584
615
 
585
- Call \`list_workspace\` to see all your channels, other agents, and humans in the workspace.
616
+ Call \`list_server\` to see all your channels, other agents, and humans in this server.
586
617
 
587
618
  ### Reading history
588
619
 
@@ -135,20 +135,20 @@ server.tool(
135
135
  }
136
136
  );
137
137
 
138
- // list_workspace tool
138
+ // list_server tool
139
139
  server.tool(
140
- "list_workspace",
141
- "List all channels you are in, all agents, and all humans in the workspace. Use this to discover who and where you can message.",
140
+ "list_server",
141
+ "List all channels you are in, all agents, and all humans in this server. Use this to discover who and where you can message.",
142
142
  {},
143
143
  async () => {
144
144
  try {
145
145
  const res = await fetch(
146
- `${serverUrl}/internal/agent/${agentId}/workspace`,
146
+ `${serverUrl}/internal/agent/${agentId}/server`,
147
147
  { method: "GET", headers: commonHeaders }
148
148
  );
149
149
  const data = await res.json();
150
150
 
151
- let text = "## Workspace\n\n";
151
+ let text = "## Server\n\n";
152
152
 
153
153
  text += "### Your Channels\n";
154
154
  text += "Use `#channel-name` with send_message to post in a channel.\n";
@@ -161,7 +161,7 @@ server.tool(
161
161
  }
162
162
 
163
163
  text += "\n### Agents\n";
164
- text += "Other AI agents in this workspace.\n";
164
+ text += "Other AI agents in this server.\n";
165
165
  if (data.agents?.length > 0) {
166
166
  for (const a of data.agents) {
167
167
  text += ` - @${a.name} (${a.status})\n`;
@@ -174,7 +174,7 @@ server.tool(
174
174
  text += "To start a new DM: send_message(dm_to=\"<name>\"). To reply in an existing DM: reuse channel from the received message.\n";
175
175
  if (data.humans?.length > 0) {
176
176
  for (const u of data.humans) {
177
- text += ` - ${u.name}\n`;
177
+ text += ` - @${u.name}\n`;
178
178
  }
179
179
  } else {
180
180
  text += " (none)\n";
@@ -194,19 +194,29 @@ server.tool(
194
194
  // read_history tool
195
195
  server.tool(
196
196
  "read_history",
197
- "Read message history for a channel or DM. Use #channel-name for channels or DM:@name for DMs.",
197
+ "Read message history for a channel or DM. Use #channel-name for channels or DM:@name for DMs. Supports pagination: use 'before' to load older messages, 'after' to load messages after a seq number (e.g. to catch up on unread).",
198
198
  {
199
199
  channel: z.string().describe("The channel to read history from — e.g. '#all', '#general', 'DM:@richard'"),
200
200
  limit: z
201
201
  .number()
202
202
  .default(50)
203
203
  .describe("Max number of messages to return (default 50, max 100)"),
204
+ before: z
205
+ .number()
206
+ .optional()
207
+ .describe("Return messages before this seq number (for backward pagination). Omit for latest messages."),
208
+ after: z
209
+ .number()
210
+ .optional()
211
+ .describe("Return messages after this seq number (for catching up on unread). Returns oldest-first."),
204
212
  },
205
- async ({ channel, limit }) => {
213
+ async ({ channel, limit, before, after }) => {
206
214
  try {
207
215
  const params = new URLSearchParams();
208
216
  params.set("channel", channel);
209
- if (limit) params.set("limit", String(limit));
217
+ params.set("limit", String(Math.min(limit, 100)));
218
+ if (before) params.set("before", String(before));
219
+ if (after) params.set("after", String(after));
210
220
 
211
221
  const res = await fetch(
212
222
  `${serverUrl}/internal/agent/${agentId}/history?${params}`,
@@ -231,17 +241,33 @@ server.tool(
231
241
  }
232
242
 
233
243
  const formatted = data.messages
234
- .map(
235
- (m: any) =>
236
- `[${new Date(m.createdAt).toLocaleTimeString()}] @${m.senderName} (${m.senderType}): ${m.content}`
237
- )
244
+ .map((m: any) => {
245
+ const senderPrefix = m.senderType === "agent" ? "(agent) " : "";
246
+ return `[seq:${m.seq}] ${senderPrefix}@${m.senderName}: ${m.content}`;
247
+ })
238
248
  .join("\n");
239
249
 
250
+ let footer = "";
251
+ if (data.has_more && data.messages.length > 0) {
252
+ if (after) {
253
+ const maxSeq = data.messages[data.messages.length - 1].seq;
254
+ footer = `\n\n--- ${data.messages.length} messages shown. Use after=${maxSeq} to load more recent messages. ---`;
255
+ } else {
256
+ const minSeq = data.messages[0].seq;
257
+ footer = `\n\n--- ${data.messages.length} messages shown. Use before=${minSeq} to load older messages. ---`;
258
+ }
259
+ }
260
+
261
+ let header = `## Message History for ${channel} (${data.messages.length} messages)`;
262
+ if (data.last_read_seq > 0 && !after && !before) {
263
+ header += `\nYour last read position: seq ${data.last_read_seq}. Use read_history(channel="${channel}", after=${data.last_read_seq}) to see only unread messages.`;
264
+ }
265
+
240
266
  return {
241
267
  content: [
242
268
  {
243
269
  type: "text" as const,
244
- text: `## Message History for ${channel} (${data.messages.length} messages)\n\n${formatted}`,
270
+ text: `${header}\n\n${formatted}${footer}`,
245
271
  },
246
272
  ],
247
273
  };
package/src/index.ts CHANGED
@@ -5,15 +5,15 @@ import { execSync } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { DaemonConnection } from "./connection.js";
7
7
  import { AgentProcessManager } from "./agentProcessManager.js";
8
- import type { ServerToMachineMessage } from "@slock-ai/shared";
8
+ import { RUNTIMES, type ServerToMachineMessage } from "@slock-ai/shared";
9
9
 
10
10
  /** Detect which CLI runtimes are installed on this machine */
11
11
  function detectRuntimes(): string[] {
12
12
  const detected: string[] = [];
13
- for (const tool of ["claude", "codex", "kimi"]) {
13
+ for (const rt of RUNTIMES) {
14
14
  try {
15
- execSync(`which ${tool}`, { stdio: "pipe" });
16
- detected.push(tool);
15
+ execSync(`which ${rt.binary}`, { stdio: "pipe" });
16
+ detected.push(rt.id);
17
17
  } catch { /* not installed */ }
18
18
  }
19
19
  return detected;
@@ -52,8 +52,8 @@ connection = new DaemonConnection({
52
52
  console.log(`[Daemon] Received: ${msg.type}`, msg.type === "ping" ? "" : JSON.stringify(msg).slice(0, 200));
53
53
  switch (msg.type) {
54
54
  case "agent:start":
55
- console.log(`[Daemon] Starting agent ${msg.agentId} (model: ${msg.config.model}, session: ${msg.config.sessionId || "new"})`);
56
- agentManager.startAgent(msg.agentId, msg.config);
55
+ console.log(`[Daemon] Starting agent ${msg.agentId} (model: ${msg.config.model}, session: ${msg.config.sessionId || "new"}${msg.wakeMessage ? ", with wake message" : ""})`);
56
+ agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary);
57
57
  break;
58
58
 
59
59
  case "agent:stop":