@slock-ai/daemon 0.38.1 → 0.39.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1181 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/output.ts
7
+ var CliExit = class extends Error {
8
+ constructor(exitCode) {
9
+ super(`CliExit(${exitCode})`);
10
+ this.exitCode = exitCode;
11
+ this.name = "CliExit";
12
+ }
13
+ };
14
+ function emit(payload) {
15
+ process.stdout.write(JSON.stringify(payload) + "\n");
16
+ }
17
+ function fail(code, message, exitCode = 1) {
18
+ process.stderr.write(JSON.stringify({ ok: false, code, message }) + "\n");
19
+ throw new CliExit(exitCode);
20
+ }
21
+
22
+ // src/auth/env.ts
23
+ import fs from "fs";
24
+ var AgentBootstrapError = class extends Error {
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.code = code;
28
+ this.name = "AgentBootstrapError";
29
+ }
30
+ };
31
+ function readTokenFromFile(filePath) {
32
+ let raw;
33
+ try {
34
+ raw = fs.readFileSync(filePath, "utf-8");
35
+ } catch (err) {
36
+ throw new AgentBootstrapError(
37
+ "TOKEN_FILE_UNREADABLE",
38
+ `SLOCK_AGENT_TOKEN_FILE=${filePath} could not be read: ${err.message}`
39
+ );
40
+ }
41
+ const token = raw.trim();
42
+ if (!token) {
43
+ throw new AgentBootstrapError(
44
+ "TOKEN_FILE_EMPTY",
45
+ `SLOCK_AGENT_TOKEN_FILE=${filePath} is empty`
46
+ );
47
+ }
48
+ return token;
49
+ }
50
+ function loadAgentContext(env = process.env) {
51
+ const agentId = env.SLOCK_AGENT_ID;
52
+ const serverUrl = env.SLOCK_SERVER_URL;
53
+ const serverId = env.SLOCK_SERVER_ID ?? null;
54
+ if (!agentId) throw new AgentBootstrapError("MISSING_AGENT_ID", "SLOCK_AGENT_ID is required");
55
+ if (!serverUrl) throw new AgentBootstrapError("MISSING_SERVER_URL", "SLOCK_SERVER_URL is required");
56
+ const tokenFile = env.SLOCK_AGENT_TOKEN_FILE;
57
+ if (tokenFile) {
58
+ return { agentId, serverUrl, serverId, token: readTokenFromFile(tokenFile), tokenSource: "token-file" };
59
+ }
60
+ const tokenLiteral = env.SLOCK_AGENT_TOKEN;
61
+ if (tokenLiteral) {
62
+ return { agentId, serverUrl, serverId, token: tokenLiteral, tokenSource: "env" };
63
+ }
64
+ throw new AgentBootstrapError(
65
+ "MISSING_TOKEN",
66
+ "Neither SLOCK_AGENT_TOKEN_FILE nor SLOCK_AGENT_TOKEN is set. The daemon should inject one of these when spawning the agent process."
67
+ );
68
+ }
69
+
70
+ // src/commands/auth/whoami.ts
71
+ function registerWhoamiCommand(parent) {
72
+ parent.command("whoami").description("Print the agent context resolved from env (token value redacted)").action(() => {
73
+ let ctx;
74
+ try {
75
+ ctx = loadAgentContext();
76
+ } catch (err) {
77
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
78
+ throw err;
79
+ }
80
+ emit({
81
+ ok: true,
82
+ data: {
83
+ agentId: ctx.agentId,
84
+ serverUrl: ctx.serverUrl,
85
+ serverId: ctx.serverId,
86
+ tokenSource: ctx.tokenSource
87
+ }
88
+ });
89
+ });
90
+ }
91
+
92
+ // src/proxy.ts
93
+ import { ProxyAgent } from "undici";
94
+ var fetchDispatcherCache = /* @__PURE__ */ new Map();
95
+ function getDefaultPort(protocol) {
96
+ switch (protocol) {
97
+ case "https:":
98
+ return "443";
99
+ case "http:":
100
+ return "80";
101
+ default:
102
+ return "";
103
+ }
104
+ }
105
+ function hostMatchesNoProxyEntry(hostname, ruleHost) {
106
+ if (!ruleHost) return false;
107
+ const normalizedRule = ruleHost.replace(/^\*\./, ".").replace(/^\./, "").toLowerCase();
108
+ const normalizedHost = hostname.toLowerCase();
109
+ return normalizedHost === normalizedRule || normalizedHost.endsWith(`.${normalizedRule}`);
110
+ }
111
+ function getProxyUrlForTarget(targetUrl, env) {
112
+ const protocol = new URL(targetUrl).protocol;
113
+ switch (protocol) {
114
+ case "https:":
115
+ return env.HTTPS_PROXY || env.https_proxy || env.ALL_PROXY || env.all_proxy;
116
+ case "http:":
117
+ return env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy;
118
+ default:
119
+ return env.ALL_PROXY || env.all_proxy;
120
+ }
121
+ }
122
+ function shouldBypassProxy(targetUrl, env) {
123
+ const rawNoProxy = env.NO_PROXY || env.no_proxy;
124
+ if (!rawNoProxy) return false;
125
+ const url = new URL(targetUrl);
126
+ const hostname = url.hostname.toLowerCase();
127
+ const port = url.port || getDefaultPort(url.protocol);
128
+ return rawNoProxy.split(",").map((entry) => entry.trim()).filter(Boolean).some((entry) => {
129
+ if (entry === "*") return true;
130
+ const [ruleHost, rulePort] = entry.split(":", 2);
131
+ if (rulePort && rulePort !== port) return false;
132
+ return hostMatchesNoProxyEntry(hostname, ruleHost);
133
+ });
134
+ }
135
+ function buildFetchDispatcher(targetUrl, env = process.env) {
136
+ const proxyUrl = getProxyUrlForTarget(targetUrl, env);
137
+ if (!proxyUrl) return void 0;
138
+ if (shouldBypassProxy(targetUrl, env)) return void 0;
139
+ const cached = fetchDispatcherCache.get(proxyUrl);
140
+ if (cached) return cached;
141
+ const dispatcher = new ProxyAgent(proxyUrl);
142
+ fetchDispatcherCache.set(proxyUrl, dispatcher);
143
+ return dispatcher;
144
+ }
145
+
146
+ // src/client.ts
147
+ var ApiClient = class {
148
+ constructor(ctx) {
149
+ this.ctx = ctx;
150
+ }
151
+ buildAuthHeaders() {
152
+ const headers = {
153
+ "Authorization": `Bearer ${this.ctx.token}`,
154
+ "X-Agent-Id": this.ctx.agentId
155
+ };
156
+ if (this.ctx.serverId) headers["X-Server-Id"] = this.ctx.serverId;
157
+ return headers;
158
+ }
159
+ async parseJsonResponse(res) {
160
+ let data = null;
161
+ let error = null;
162
+ const contentType = res.headers.get("content-type") ?? "";
163
+ if (contentType.includes("application/json")) {
164
+ const parsed = await res.json().catch(() => null);
165
+ if (res.ok) {
166
+ data = parsed;
167
+ } else {
168
+ error = parsed?.error ?? `HTTP ${res.status}`;
169
+ }
170
+ } else if (!res.ok) {
171
+ error = `HTTP ${res.status}`;
172
+ }
173
+ return { ok: res.ok, status: res.status, data, error };
174
+ }
175
+ async request(method, pathname, body) {
176
+ const url = new URL(pathname, this.ctx.serverUrl).toString();
177
+ const headers = this.buildAuthHeaders();
178
+ headers["Content-Type"] = "application/json";
179
+ const dispatcher = buildFetchDispatcher(url);
180
+ const init = {
181
+ method,
182
+ headers,
183
+ body: body === void 0 ? void 0 : JSON.stringify(body)
184
+ };
185
+ if (dispatcher) init.dispatcher = dispatcher;
186
+ const res = await fetch(url, init);
187
+ return this.parseJsonResponse(res);
188
+ }
189
+ // Multipart upload. Caller builds the FormData (file part + any text fields).
190
+ // Content-Type intentionally omitted so fetch sets the correct multipart
191
+ // boundary itself.
192
+ async requestMultipart(method, pathname, form) {
193
+ const url = new URL(pathname, this.ctx.serverUrl).toString();
194
+ const dispatcher = buildFetchDispatcher(url);
195
+ const init = {
196
+ method,
197
+ headers: this.buildAuthHeaders(),
198
+ body: form
199
+ };
200
+ if (dispatcher) init.dispatcher = dispatcher;
201
+ const res = await fetch(url, init);
202
+ return this.parseJsonResponse(res);
203
+ }
204
+ // Returns the raw Response so the caller can stream / save the body.
205
+ // For non-JSON downloads (binary attachments). Caller is responsible for
206
+ // consuming the body. On non-2xx, attempts to surface a JSON error.
207
+ async requestRaw(method, pathname) {
208
+ const url = new URL(pathname, this.ctx.serverUrl).toString();
209
+ const dispatcher = buildFetchDispatcher(url);
210
+ const init = {
211
+ method,
212
+ headers: this.buildAuthHeaders(),
213
+ redirect: "follow"
214
+ };
215
+ if (dispatcher) init.dispatcher = dispatcher;
216
+ const res = await fetch(url, init);
217
+ let error = null;
218
+ if (!res.ok) {
219
+ const contentType = res.headers.get("content-type") ?? "";
220
+ if (contentType.includes("application/json")) {
221
+ const parsed = await res.json().catch(() => null);
222
+ error = parsed?.error ?? `HTTP ${res.status}`;
223
+ } else {
224
+ error = `HTTP ${res.status}`;
225
+ }
226
+ }
227
+ return { ok: res.ok, status: res.status, response: res, error };
228
+ }
229
+ };
230
+
231
+ // src/commands/server/_format.ts
232
+ function formatServerInfo(data) {
233
+ let text = "## Server\n\n";
234
+ const channels = data.channels ?? [];
235
+ const agents = data.agents ?? [];
236
+ const humans = data.humans ?? [];
237
+ text += "### Channels\n";
238
+ text += 'Visible public channels may appear even when `joined=false`. Use `slock message read --channel "#name"` to inspect them. When a channel is not joined, you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.\n';
239
+ if (channels.length > 0) {
240
+ for (const t of channels) {
241
+ const status = t.joined ? "joined" : "not joined";
242
+ text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
243
+ ` : ` - #${t.name} [${status}]
244
+ `;
245
+ }
246
+ } else {
247
+ text += " (none)\n";
248
+ }
249
+ text += "\n### Agents\n";
250
+ text += "Other AI agents in this server.\n";
251
+ if (agents.length > 0) {
252
+ for (const a of agents) {
253
+ text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
254
+ ` : ` - @${a.name} (${a.status})
255
+ `;
256
+ }
257
+ } else {
258
+ text += " (none)\n";
259
+ }
260
+ text += "\n### Humans\n";
261
+ text += `To start a new DM: slock message send --target "dm:@name" <<'EOF' followed by the message body and EOF. To reply in an existing DM: reuse the target from received messages.
262
+ `;
263
+ if (humans.length > 0) {
264
+ for (const u of humans) {
265
+ text += u.description ? ` - @${u.name} \u2014 ${u.description}
266
+ ` : ` - @${u.name}
267
+ `;
268
+ }
269
+ } else {
270
+ text += " (none)\n";
271
+ }
272
+ return text;
273
+ }
274
+
275
+ // src/commands/server/info.ts
276
+ function registerServerInfoCommand(parent) {
277
+ parent.command("info").description("List channels, agents, and humans on the current server").action(async () => {
278
+ let ctx;
279
+ try {
280
+ ctx = loadAgentContext();
281
+ } catch (err) {
282
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
283
+ throw err;
284
+ }
285
+ const client = new ApiClient(ctx);
286
+ const res = await client.request(
287
+ "GET",
288
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/server`
289
+ );
290
+ if (!res.ok) {
291
+ const code = res.status >= 500 ? "SERVER_5XX" : "INFO_FAILED";
292
+ fail(code, res.error ?? `HTTP ${res.status}`);
293
+ }
294
+ process.stdout.write(formatServerInfo(res.data));
295
+ });
296
+ }
297
+
298
+ // src/commands/message/_format.ts
299
+ function toLocalTime(iso) {
300
+ const d = new Date(iso);
301
+ if (isNaN(d.getTime())) return iso;
302
+ const pad = (n) => String(n).padStart(2, "0");
303
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
304
+ }
305
+ function formatTarget(m) {
306
+ if (m.channel_type === "thread" && m.parent_channel_name) {
307
+ const shortId = m.channel_name?.startsWith("thread-") ? m.channel_name.slice(7) : m.channel_name;
308
+ if (m.parent_channel_type === "dm") {
309
+ return `dm:@${m.parent_channel_name}:${shortId}`;
310
+ }
311
+ return `#${m.parent_channel_name}:${shortId}`;
312
+ }
313
+ if (m.channel_type === "dm") {
314
+ return `dm:@${m.channel_name}`;
315
+ }
316
+ return `#${m.channel_name}`;
317
+ }
318
+ function formatSenderHandle(m) {
319
+ const name = m.sender_name ?? "unknown";
320
+ const desc = m.sender_description ?? null;
321
+ return desc ? `@${name} \u2014 ${desc}` : `@${name}`;
322
+ }
323
+ function formatAttachmentSuffix(attachments) {
324
+ if (!attachments?.length) return "";
325
+ return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use slock attachment view to download]`;
326
+ }
327
+ function formatMessageLine(m) {
328
+ const target = formatTarget(m);
329
+ const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
330
+ const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
331
+ const senderType = ` type=${m.sender_type}`;
332
+ const content = m.content ?? "";
333
+ const attachSuffix = formatAttachmentSuffix(m.attachments);
334
+ const taskSuffix = m.task_status ? ` [task #${m.task_number} status=${m.task_status}${m.task_assignee_id ? ` assignee=${m.task_assignee_type}:${m.task_assignee_id}` : ""}]` : "";
335
+ return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${content}${attachSuffix}${taskSuffix}`;
336
+ }
337
+ function formatMessages(messages) {
338
+ if (messages.length === 0) return "No new messages.";
339
+ return messages.map(formatMessageLine).join("\n");
340
+ }
341
+ function formatHistoryMessageLine(m) {
342
+ const senderName = m.senderName ?? m.sender_name ?? "unknown";
343
+ const senderDescription = m.senderDescription ?? m.sender_description ?? null;
344
+ const headerParts = [
345
+ `seq=${m.seq}`,
346
+ `msg=${m.id || "-"}`,
347
+ `time=${m.createdAt ? toLocalTime(m.createdAt) : "-"}`
348
+ ];
349
+ if (m.senderType) headerParts.push(`type=${m.senderType}`);
350
+ if (m.threadId) headerParts.push(`threadId=${m.threadId}`);
351
+ if ((m.replyCount ?? 0) > 0) headerParts.push(`replyCount=${m.replyCount}`);
352
+ const attachSuffix = formatAttachmentSuffix(m.attachments);
353
+ const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ""}]` : "";
354
+ const handle = senderDescription ? `@${senderName} \u2014 ${senderDescription}` : `@${senderName}`;
355
+ return `[${headerParts.join(" ")}] ${handle}: ${m.content}${attachSuffix}${taskSuffix}`;
356
+ }
357
+ function formatHistory(channel, data, opts) {
358
+ if (!data.messages || data.messages.length === 0) return "No messages in this channel.";
359
+ const formatted = data.messages.map((m) => formatHistoryMessageLine({
360
+ ...m,
361
+ senderName: m.senderName ?? m.sender_name ?? "unknown",
362
+ senderDescription: m.senderDescription ?? m.sender_description ?? null
363
+ })).join("\n");
364
+ let footer = "";
365
+ if (data.historyLimited) {
366
+ footer = `
367
+
368
+ --- ${data.historyLimitMessage || "Message history is limited on this plan."} ---`;
369
+ } else if (opts?.around && data.messages.length > 0 && (data.has_older || data.has_newer)) {
370
+ const minSeq = data.messages[0].seq;
371
+ const maxSeq = data.messages[data.messages.length - 1].seq;
372
+ footer = `
373
+
374
+ --- Context window shown. Use before=${minSeq} to load older messages or after=${maxSeq} to load newer messages. ---`;
375
+ } else if (data.has_more && data.messages.length > 0) {
376
+ if (opts?.after) {
377
+ const maxSeq = data.messages[data.messages.length - 1].seq;
378
+ footer = `
379
+
380
+ --- ${data.messages.length} messages shown. Use after=${maxSeq} to load more recent messages. ---`;
381
+ } else {
382
+ const minSeq = data.messages[0].seq;
383
+ footer = `
384
+
385
+ --- ${data.messages.length} messages shown. Use before=${minSeq} to load older messages. ---`;
386
+ }
387
+ }
388
+ let header = `## Message History for ${channel}${opts?.around ? ` around ${opts.around}` : ""} (${data.messages.length} messages)`;
389
+ if ((data.last_read_seq ?? 0) > 0 && !opts?.after && !opts?.before && !opts?.around) {
390
+ header += `
391
+ Your last read position: seq ${data.last_read_seq}. Use slock message read --channel "${channel}" --after ${data.last_read_seq} to see only unread messages.`;
392
+ }
393
+ return `${header}
394
+
395
+ ${formatted}${footer}`;
396
+ }
397
+ function formatSearchTarget(result) {
398
+ if (result.channelType === "thread") {
399
+ const shortId = typeof result.channelName === "string" && result.channelName.startsWith("thread-") ? result.channelName.slice(7) : typeof result.threadId === "string" && result.threadId ? result.threadId.slice(0, 8) : result.channelName;
400
+ if (result.parentChannelType === "dm") {
401
+ return `dm:@${result.parentChannelName}:${shortId}`;
402
+ }
403
+ return `#${result.parentChannelName}:${shortId}`;
404
+ }
405
+ if (result.channelType === "dm") {
406
+ return `dm:@${result.channelName}`;
407
+ }
408
+ return `#${result.channelName}`;
409
+ }
410
+ function formatSearchResults(query, data) {
411
+ if (!data.results || data.results.length === 0) return "No search results.";
412
+ const formatted = data.results.map((result, index) => {
413
+ const target = formatSearchTarget(result);
414
+ const threadInfo = result.channelType === "thread" ? `
415
+ thread: ${result.parentChannelName} -> ${target}` : "";
416
+ return [
417
+ `[${index + 1}] msg=${result.id} seq=${result.seq} time=${result.createdAt ? toLocalTime(result.createdAt) : "-"}`,
418
+ `target: ${target}${threadInfo}`,
419
+ `sender: @${result.senderName} (${result.senderType})`,
420
+ `content: ${result.content}`,
421
+ `match: ${result.snippet}`,
422
+ `next: slock message read --channel "${target}" --around "${result.id}" --limit 20`
423
+ ].join("\n");
424
+ }).join("\n\n");
425
+ return `## Search Results for "${query}" (${data.results.length} results)
426
+
427
+ ${formatted}`;
428
+ }
429
+
430
+ // src/commands/message/send.ts
431
+ var SendContentError = class extends Error {
432
+ constructor(code, message) {
433
+ super(message);
434
+ this.code = code;
435
+ this.name = "SendContentError";
436
+ }
437
+ };
438
+ async function readStream(stream) {
439
+ let content = "";
440
+ stream.setEncoding("utf8");
441
+ for await (const chunk of stream) {
442
+ content += String(chunk);
443
+ }
444
+ return content;
445
+ }
446
+ function missingContentMessage() {
447
+ return [
448
+ "No message content received on stdin.",
449
+ "Use a heredoc or pipe content into slock message send:",
450
+ ` slock message send --target "#channel" <<'EOF'`,
451
+ " message body",
452
+ " EOF"
453
+ ].join("\n");
454
+ }
455
+ async function resolveSendContent(input = process.stdin) {
456
+ if (input.isTTY) {
457
+ throw new SendContentError("MISSING_CONTENT", missingContentMessage());
458
+ }
459
+ const content = await readStream(input);
460
+ if (content.trim().length === 0) {
461
+ throw new SendContentError("MISSING_CONTENT", missingContentMessage());
462
+ }
463
+ return content;
464
+ }
465
+ function rejectArgContent(positionalContent, opts) {
466
+ if (positionalContent.length > 0) {
467
+ throw new SendContentError(
468
+ "POSITIONAL_CONTENT_UNSUPPORTED",
469
+ [
470
+ "Message content must be provided on stdin, not as positional arguments.",
471
+ "Use:",
472
+ ` slock message send --target "#channel" <<'EOF'`,
473
+ " message body",
474
+ " EOF"
475
+ ].join("\n")
476
+ );
477
+ }
478
+ if (opts.content !== void 0) {
479
+ throw new SendContentError(
480
+ "CONTENT_FLAG_UNSUPPORTED",
481
+ [
482
+ "--content is no longer supported. Pipe message content to stdin.",
483
+ "Use:",
484
+ ` printf 'hello' | slock message send --target "#channel"`
485
+ ].join("\n")
486
+ );
487
+ }
488
+ }
489
+ function registerSendCommand(parent) {
490
+ parent.command("send").description("Send a message to a channel, DM, or thread").argument("[content...]", "Unsupported positional message content. Pipe content to stdin instead.").requiredOption("--target <target>", "Target: '#channel', 'dm:@peer', '#channel:threadId', 'dm:@peer:threadId'").option("--content <content>", "Unsupported. Pipe message content to stdin instead.").option(
491
+ "--attachment-id <id>",
492
+ "Attachment id to link (repeatable). Get one from `slock attachment upload`.",
493
+ (value, prev = []) => prev.concat(value)
494
+ ).action(async (positionalContent, opts) => {
495
+ try {
496
+ rejectArgContent(positionalContent, opts);
497
+ } catch (err) {
498
+ if (err instanceof SendContentError) fail(err.code, err.message);
499
+ throw err;
500
+ }
501
+ let ctx;
502
+ try {
503
+ ctx = loadAgentContext();
504
+ } catch (err) {
505
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
506
+ throw err;
507
+ }
508
+ const client = new ApiClient(ctx);
509
+ let content;
510
+ try {
511
+ content = await resolveSendContent();
512
+ } catch (err) {
513
+ if (err instanceof SendContentError) fail(err.code, err.message);
514
+ throw err;
515
+ }
516
+ const body = { target: opts.target, content };
517
+ if (opts.attachmentId && opts.attachmentId.length > 0) {
518
+ body.attachmentIds = opts.attachmentId;
519
+ }
520
+ const res = await client.request(
521
+ "POST",
522
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/send`,
523
+ body
524
+ );
525
+ if (!res.ok) {
526
+ const code = res.status >= 500 ? "SERVER_5XX" : "SEND_FAILED";
527
+ fail(code, res.error ?? `HTTP ${res.status}`);
528
+ }
529
+ const data = res.data;
530
+ const shortId = data.messageId ? data.messageId.slice(0, 8) : null;
531
+ const replyHint = shortId ? ` (to reply in this message's thread, use target "${opts.target.includes(":") ? opts.target : opts.target + ":" + shortId}")` : "";
532
+ let unreadSection = "";
533
+ if (data.recentUnread && data.recentUnread.length > 0) {
534
+ unreadSection = `
535
+
536
+ --- New messages you may have missed ---
537
+ ${formatMessages(data.recentUnread)}`;
538
+ }
539
+ process.stdout.write(`Message sent to ${opts.target}. Message ID: ${data.messageId}${replyHint}${unreadSection}
540
+ `);
541
+ });
542
+ }
543
+
544
+ // src/commands/message/_inbox.ts
545
+ async function drainInbox(ctx, opts) {
546
+ const client = new ApiClient(ctx);
547
+ const agentPath = `/internal/agent/${encodeURIComponent(ctx.agentId)}`;
548
+ const failCode = opts.block ? "WAIT_FAILED" : "CHECK_FAILED";
549
+ const query = [];
550
+ if (opts.block) query.push("block=true");
551
+ if (opts.block && opts.timeoutMs !== void 0) query.push(`timeout=${opts.timeoutMs}`);
552
+ const path = query.length > 0 ? `${agentPath}/receive?${query.join("&")}` : `${agentPath}/receive`;
553
+ const res = await client.request("GET", path);
554
+ if (!res.ok) {
555
+ const code = res.status >= 500 ? "SERVER_5XX" : failCode;
556
+ fail(code, res.error ?? `HTTP ${res.status}`);
557
+ }
558
+ const messages = res.data?.messages ?? [];
559
+ const seqs = messages.map((m) => m.seq).filter((s) => Number.isInteger(s) && s > 0);
560
+ if (seqs.length === 0) return { messages };
561
+ const ack = await client.request("POST", `${agentPath}/receive-ack`, { seqs });
562
+ if (ack.ok) return { messages };
563
+ const ackCode = ack.status >= 500 ? "SERVER_5XX" : "ACK_FAILED";
564
+ const ackMessage = ack.error ?? `HTTP ${ack.status}`;
565
+ return { messages, ackFailure: { code: ackCode, message: ackMessage } };
566
+ }
567
+
568
+ // src/commands/message/check.ts
569
+ function registerCheckCommand(parent) {
570
+ parent.command("check").description("Drain the agent inbox (non-blocking). Acks delivered seqs before returning.").action(async () => {
571
+ let ctx;
572
+ try {
573
+ ctx = loadAgentContext();
574
+ } catch (err) {
575
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
576
+ throw err;
577
+ }
578
+ const result = await drainInbox(ctx, { block: false });
579
+ process.stdout.write(formatMessages(result.messages) + "\n");
580
+ });
581
+ }
582
+
583
+ // src/commands/message/read.ts
584
+ function parsePositiveInt(name, raw) {
585
+ if (raw === void 0) return void 0;
586
+ const n = Number(raw);
587
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
588
+ fail("INVALID_ARG", `--${name} must be a positive integer; got ${raw}`);
589
+ }
590
+ return n;
591
+ }
592
+ function registerReadCommand(parent) {
593
+ parent.command("read").description("Read message history for a channel, DM, or thread").requiredOption("--channel <target>", "Target: '#channel', 'dm:@peer', '#channel:threadId', 'dm:@peer:threadId'").option("--before <seq>", "Return messages strictly before this seq (paginate backwards)").option("--after <seq>", "Return messages strictly after this seq (paginate forwards)").option("--around <idOrSeq>", "Center the window on this messageId or seq").option("--limit <n>", "Max messages to return (server default applies if omitted)").action(async (opts) => {
594
+ let ctx;
595
+ try {
596
+ ctx = loadAgentContext();
597
+ } catch (err) {
598
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
599
+ throw err;
600
+ }
601
+ const before = parsePositiveInt("before", opts.before);
602
+ const after = parsePositiveInt("after", opts.after);
603
+ const limit = parsePositiveInt("limit", opts.limit);
604
+ const params = new URLSearchParams();
605
+ params.set("channel", opts.channel);
606
+ if (before !== void 0) params.set("before", String(before));
607
+ if (after !== void 0) params.set("after", String(after));
608
+ if (opts.around !== void 0) params.set("around", opts.around);
609
+ if (limit !== void 0) params.set("limit", String(limit));
610
+ const client = new ApiClient(ctx);
611
+ const res = await client.request(
612
+ "GET",
613
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/history?${params.toString()}`
614
+ );
615
+ if (!res.ok) {
616
+ const code = res.status >= 500 ? "SERVER_5XX" : "READ_FAILED";
617
+ fail(code, res.error ?? `HTTP ${res.status}`);
618
+ }
619
+ process.stdout.write(formatHistory(opts.channel, res.data, { around: opts.around, after, before }) + "\n");
620
+ });
621
+ }
622
+
623
+ // src/commands/message/search.ts
624
+ function registerSearchCommand(parent) {
625
+ parent.command("search").description("Search messages across channels the agent can see").requiredOption("--query <q>", "Search query string").option("--channel <target>", "Restrict to a single channel/DM/thread").option("--sender <id>", "Restrict to messages by this sender id").option("--before <iso>", "Only messages before this ISO datetime").option("--after <iso>", "Only messages after this ISO datetime").option("--limit <n>", "Max results (server default applies if omitted)").action(async (opts) => {
626
+ let ctx;
627
+ try {
628
+ ctx = loadAgentContext();
629
+ } catch (err) {
630
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
631
+ throw err;
632
+ }
633
+ let limit;
634
+ if (opts.limit !== void 0) {
635
+ const n = Number(opts.limit);
636
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
637
+ fail("INVALID_ARG", `--limit must be a positive integer; got ${opts.limit}`);
638
+ }
639
+ limit = n;
640
+ }
641
+ const params = new URLSearchParams();
642
+ params.set("q", opts.query);
643
+ if (opts.channel) params.set("channel", opts.channel);
644
+ if (opts.sender) params.set("senderId", opts.sender);
645
+ if (opts.before) params.set("before", opts.before);
646
+ if (opts.after) params.set("after", opts.after);
647
+ if (limit !== void 0) params.set("limit", String(limit));
648
+ const client = new ApiClient(ctx);
649
+ const res = await client.request(
650
+ "GET",
651
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/search?${params.toString()}`
652
+ );
653
+ if (!res.ok) {
654
+ const code = res.status >= 500 ? "SERVER_5XX" : "SEARCH_FAILED";
655
+ fail(code, res.error ?? `HTTP ${res.status}`);
656
+ }
657
+ process.stdout.write(formatSearchResults(opts.query, res.data) + "\n");
658
+ });
659
+ }
660
+
661
+ // src/commands/attachment/upload.ts
662
+ import { existsSync, statSync, readFileSync } from "fs";
663
+ import { basename } from "path";
664
+ var MAX_BYTES = 10 * 1024 * 1024;
665
+ function registerAttachmentUploadCommand(parent) {
666
+ parent.command("upload").description("Upload a local file as an attachment (max 10MB)").requiredOption("--path <filepath>", "Absolute path to the local file to upload").option(
667
+ "--channel <target>",
668
+ "Target where the attachment will be used: '#channel', 'dm:@peer', or thread variants. Required by the v0 server until channel-less uploads land."
669
+ ).action(async (opts) => {
670
+ let ctx;
671
+ try {
672
+ ctx = loadAgentContext();
673
+ } catch (err) {
674
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
675
+ throw err;
676
+ }
677
+ if (!existsSync(opts.path)) {
678
+ fail("INVALID_ARG", `--path does not exist: ${opts.path}`);
679
+ }
680
+ const stat = statSync(opts.path);
681
+ if (!stat.isFile()) {
682
+ fail("INVALID_ARG", `--path is not a regular file: ${opts.path}`);
683
+ }
684
+ if (stat.size > MAX_BYTES) {
685
+ fail(
686
+ "INVALID_ARG",
687
+ `--path is ${stat.size} bytes; max upload size is ${MAX_BYTES} bytes (10MB)`
688
+ );
689
+ }
690
+ if (!opts.channel) {
691
+ fail(
692
+ "MISSING_CHANNEL",
693
+ "v0 server requires a channel to attach the upload to. Pass --channel '#name', 'dm:@peer', or a thread target."
694
+ );
695
+ }
696
+ const client = new ApiClient(ctx);
697
+ const agentPath = `/internal/agent/${encodeURIComponent(ctx.agentId)}`;
698
+ const resolved = await client.request(
699
+ "POST",
700
+ `${agentPath}/resolve-channel`,
701
+ { target: opts.channel }
702
+ );
703
+ if (!resolved.ok || !resolved.data?.channelId) {
704
+ const code = resolved.status >= 500 ? "SERVER_5XX" : "RESOLVE_FAILED";
705
+ fail(code, resolved.error ?? `Could not resolve channel: ${opts.channel}`);
706
+ }
707
+ const channelId = resolved.data.channelId;
708
+ const buffer = readFileSync(opts.path);
709
+ const filename = basename(opts.path);
710
+ const blob = new Blob([buffer]);
711
+ const form = new FormData();
712
+ form.append("file", blob, filename);
713
+ form.append("channelId", channelId);
714
+ const res = await client.requestMultipart(
715
+ "POST",
716
+ `${agentPath}/upload`,
717
+ form
718
+ );
719
+ if (!res.ok) {
720
+ const code = res.status >= 500 ? "SERVER_5XX" : "UPLOAD_FAILED";
721
+ fail(code, res.error ?? `HTTP ${res.status}`);
722
+ }
723
+ const d = res.data;
724
+ process.stdout.write(`File uploaded: ${d.filename} (${(d.sizeBytes / 1024).toFixed(1)}KB)
725
+ Attachment ID: ${d.id}
726
+
727
+ Use this ID with slock message send --attachment-id ${d.id} to include it in a message.
728
+ `);
729
+ });
730
+ }
731
+
732
+ // src/commands/attachment/view.ts
733
+ import { writeFileSync } from "fs";
734
+ function registerAttachmentViewCommand(parent) {
735
+ parent.command("view").description("Download an attachment by id and save it to a local path").requiredOption("--id <attachmentId>", "Attachment UUID").requiredOption("--output <path>", "Local path to write the file to").action(async (opts) => {
736
+ let ctx;
737
+ try {
738
+ ctx = loadAgentContext();
739
+ } catch (err) {
740
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
741
+ throw err;
742
+ }
743
+ const client = new ApiClient(ctx);
744
+ const res = await client.requestRaw(
745
+ "GET",
746
+ `/api/attachments/${encodeURIComponent(opts.id)}`
747
+ );
748
+ if (!res.ok) {
749
+ const code = res.status >= 500 ? "SERVER_5XX" : "VIEW_FAILED";
750
+ fail(code, res.error ?? `HTTP ${res.status}`);
751
+ }
752
+ const buffer = Buffer.from(await res.response.arrayBuffer());
753
+ writeFileSync(opts.output, buffer);
754
+ process.stdout.write(`Downloaded to: ${opts.output}
755
+ `);
756
+ });
757
+ }
758
+
759
+ // src/commands/task/_format.ts
760
+ function formatTaskList(channel, data, statusFilter) {
761
+ if (!data.tasks || data.tasks.length === 0) {
762
+ return `No${statusFilter && statusFilter !== "all" ? ` ${statusFilter}` : ""} tasks in ${channel}.`;
763
+ }
764
+ const formatted = data.tasks.map((t) => {
765
+ const assignee = t.claimedByName ? ` \u2192 @${t.claimedByName}` : "";
766
+ const creator = t.createdByName ? ` (by @${t.createdByName})` : "";
767
+ const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
768
+ const legacy = t.isLegacy ? " [LEGACY \u2014 read-only]" : "";
769
+ return `#${t.taskNumber} [${t.status}] ${t.title}${assignee}${creator}${msgId}${legacy}`;
770
+ }).join("\n");
771
+ return `## Task Board for ${channel} (${data.tasks.length} tasks)
772
+
773
+ ${formatted}`;
774
+ }
775
+ function formatTasksCreated(channel, data) {
776
+ const created = data.tasks.map((t) => `#${t.taskNumber} msg=${t.messageId.slice(0, 8)} "${t.title}"`).join("\n");
777
+ const threadHints = data.tasks.map((t) => `#${t.taskNumber} \u2192 slock message send --target "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
778
+ return `Created ${data.tasks.length} task(s) in ${channel}:
779
+ ${created}
780
+
781
+ To follow up in each task's thread:
782
+ ${threadHints}`;
783
+ }
784
+ function formatClaimResults(channel, data) {
785
+ const lines = data.results.map((r) => {
786
+ const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
787
+ if (r.success) {
788
+ const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
789
+ return `${label} (msg:${msgShort}): claimed`;
790
+ }
791
+ return `${label}: FAILED \u2014 ${r.reason || "already claimed"}`;
792
+ });
793
+ const succeeded = data.results.filter((r) => r.success).length;
794
+ const failed = data.results.length - succeeded;
795
+ let summary = `${succeeded} claimed`;
796
+ if (failed > 0) summary += `, ${failed} failed`;
797
+ const claimedMsgs = data.results.filter((r) => r.success && r.messageId).map((r) => `#${r.taskNumber} \u2192 slock message send --target "${channel}:${r.messageId.slice(0, 8)}"`).join("\n");
798
+ const threadHint = claimedMsgs ? `
799
+
800
+ Follow up in each task's thread:
801
+ ${claimedMsgs}` : "";
802
+ return `Claim results (${summary}):
803
+ ${lines.join("\n")}${threadHint}`;
804
+ }
805
+ function formatTaskUnclaimed(taskNumber) {
806
+ return `#${taskNumber} unclaimed \u2014 now open.`;
807
+ }
808
+ function formatTaskStatusUpdated(taskNumber, status) {
809
+ return `#${taskNumber} moved to ${status}.`;
810
+ }
811
+
812
+ // src/commands/task/list.ts
813
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["all", "todo", "in_progress", "in_review", "done"]);
814
+ function registerTaskListCommand(parent) {
815
+ parent.command("list").description("List tasks in a channel").requiredOption("--channel <target>", "Channel target: '#channel'").option("--status <s>", "Filter: all|todo|in_progress|in_review|done (default: server-side)").action(async (opts) => {
816
+ let ctx;
817
+ try {
818
+ ctx = loadAgentContext();
819
+ } catch (err) {
820
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
821
+ throw err;
822
+ }
823
+ if (opts.status && !VALID_STATUSES.has(opts.status)) {
824
+ fail("INVALID_ARG", `--status must be one of ${Array.from(VALID_STATUSES).join("|")}; got ${opts.status}`);
825
+ }
826
+ const params = new URLSearchParams();
827
+ params.set("channel", opts.channel);
828
+ if (opts.status) params.set("status", opts.status);
829
+ const client = new ApiClient(ctx);
830
+ const res = await client.request(
831
+ "GET",
832
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks?${params.toString()}`
833
+ );
834
+ if (!res.ok) {
835
+ const code = res.status >= 500 ? "SERVER_5XX" : "LIST_FAILED";
836
+ fail(code, res.error ?? `HTTP ${res.status}`);
837
+ }
838
+ process.stdout.write(formatTaskList(opts.channel, res.data, opts.status) + "\n");
839
+ });
840
+ }
841
+
842
+ // src/commands/task/create.ts
843
+ function registerTaskCreateCommand(parent) {
844
+ parent.command("create").description("Create one or more tasks in a channel").requiredOption("--channel <target>", "Channel target: '#channel'").requiredOption(
845
+ "--title <title>",
846
+ "Task title (repeatable for batch create)",
847
+ (value, prev = []) => prev.concat(value)
848
+ ).action(async (opts) => {
849
+ let ctx;
850
+ try {
851
+ ctx = loadAgentContext();
852
+ } catch (err) {
853
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
854
+ throw err;
855
+ }
856
+ const titles = opts.title ?? [];
857
+ if (titles.length === 0) fail("INVALID_ARG", "--title is required (at least one)");
858
+ const client = new ApiClient(ctx);
859
+ const res = await client.request(
860
+ "POST",
861
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks`,
862
+ { channel: opts.channel, tasks: titles.map((title) => ({ title })) }
863
+ );
864
+ if (!res.ok) {
865
+ const code = res.status >= 500 ? "SERVER_5XX" : "CREATE_FAILED";
866
+ fail(code, res.error ?? `HTTP ${res.status}`);
867
+ }
868
+ process.stdout.write(formatTasksCreated(opts.channel, res.data) + "\n");
869
+ });
870
+ }
871
+
872
+ // src/commands/task/claim.ts
873
+ function registerTaskClaimCommand(parent) {
874
+ parent.command("claim").description("Claim one or more tasks (by task number or message id)").requiredOption("--channel <target>", "Channel target: '#channel'").option(
875
+ "--number <n>",
876
+ "Task number to claim (repeatable)",
877
+ (value, prev = []) => prev.concat(value)
878
+ ).option(
879
+ "--message-id <id>",
880
+ "Message id (full or short) to claim (repeatable)",
881
+ (value, prev = []) => prev.concat(value)
882
+ ).action(async (opts) => {
883
+ let ctx;
884
+ try {
885
+ ctx = loadAgentContext();
886
+ } catch (err) {
887
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
888
+ throw err;
889
+ }
890
+ const numbers = (opts.number ?? []).map((raw) => {
891
+ const n = Number(raw);
892
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
893
+ fail("INVALID_ARG", `--number must be a positive integer; got ${raw}`);
894
+ }
895
+ return n;
896
+ });
897
+ const messageIds = opts.messageId ?? [];
898
+ if (numbers.length === 0 && messageIds.length === 0) {
899
+ fail("INVALID_ARG", "Provide at least one --number or --message-id");
900
+ }
901
+ const body = { channel: opts.channel };
902
+ if (numbers.length > 0) body.task_numbers = numbers;
903
+ if (messageIds.length > 0) body.message_ids = messageIds;
904
+ const client = new ApiClient(ctx);
905
+ const res = await client.request(
906
+ "POST",
907
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks/claim`,
908
+ body
909
+ );
910
+ if (!res.ok) {
911
+ const code = res.status >= 500 ? "SERVER_5XX" : "CLAIM_FAILED";
912
+ fail(code, res.error ?? `HTTP ${res.status}`);
913
+ }
914
+ process.stdout.write(formatClaimResults(opts.channel, res.data) + "\n");
915
+ });
916
+ }
917
+
918
+ // src/commands/task/unclaim.ts
919
+ function registerTaskUnclaimCommand(parent) {
920
+ parent.command("unclaim").description("Release a previously-claimed task").requiredOption("--channel <target>", "Channel target: '#channel'").requiredOption("--number <n>", "Task number to unclaim").action(async (opts) => {
921
+ let ctx;
922
+ try {
923
+ ctx = loadAgentContext();
924
+ } catch (err) {
925
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
926
+ throw err;
927
+ }
928
+ const n = Number(opts.number);
929
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
930
+ fail("INVALID_ARG", `--number must be a positive integer; got ${opts.number}`);
931
+ }
932
+ const client = new ApiClient(ctx);
933
+ const res = await client.request(
934
+ "POST",
935
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks/unclaim`,
936
+ { channel: opts.channel, task_number: n }
937
+ );
938
+ if (!res.ok) {
939
+ const code = res.status >= 500 ? "SERVER_5XX" : "UNCLAIM_FAILED";
940
+ fail(code, res.error ?? `HTTP ${res.status}`);
941
+ }
942
+ process.stdout.write(formatTaskUnclaimed(n) + "\n");
943
+ });
944
+ }
945
+
946
+ // src/commands/task/update.ts
947
+ var STATUSES = ["todo", "in_progress", "in_review", "done"];
948
+ function registerTaskUpdateCommand(parent) {
949
+ parent.command("update").description("Update task status").requiredOption("--channel <target>", "Channel target: '#channel'").requiredOption("--number <n>", "Task number to update").requiredOption(
950
+ "--status <status>",
951
+ `New status. One of: ${STATUSES.join(", ")}`
952
+ ).action(async (opts) => {
953
+ let ctx;
954
+ try {
955
+ ctx = loadAgentContext();
956
+ } catch (err) {
957
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
958
+ throw err;
959
+ }
960
+ const n = Number(opts.number);
961
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
962
+ fail("INVALID_ARG", `--number must be a positive integer; got ${opts.number}`);
963
+ }
964
+ if (!STATUSES.includes(opts.status)) {
965
+ fail(
966
+ "INVALID_ARG",
967
+ `--status must be one of: ${STATUSES.join(", ")}; got ${opts.status}`
968
+ );
969
+ }
970
+ const client = new ApiClient(ctx);
971
+ const res = await client.request(
972
+ "POST",
973
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks/update-status`,
974
+ { channel: opts.channel, task_number: n, status: opts.status }
975
+ );
976
+ if (!res.ok) {
977
+ const code = res.status >= 500 ? "SERVER_5XX" : "UPDATE_FAILED";
978
+ fail(code, res.error ?? `HTTP ${res.status}`);
979
+ }
980
+ process.stdout.write(formatTaskStatusUpdated(n, opts.status) + "\n");
981
+ });
982
+ }
983
+
984
+ // src/commands/reminder/_format.ts
985
+ function toLocalTime2(iso) {
986
+ const d = new Date(iso);
987
+ if (isNaN(d.getTime())) return iso;
988
+ const pad = (n) => String(n).padStart(2, "0");
989
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
990
+ }
991
+ function formatReminder(r) {
992
+ const fireLocal = toLocalTime2(r.fireAt);
993
+ const ref = r.msgRef ? ` ref=${r.msgRef}` : "";
994
+ const repeat = r.recurrence ? ` repeat=${r.recurrence.description}` : "";
995
+ return `#${r.reminderId.slice(0, 8)} [${r.status}] fires=${fireLocal} "${r.title}"${ref}${repeat}`;
996
+ }
997
+ function formatReminderScheduled(r, warning) {
998
+ const lines = [`Reminder scheduled: ${formatReminder(r)}`];
999
+ if (warning) lines.push(`Warning: ${warning}`);
1000
+ return lines.join("\n");
1001
+ }
1002
+ function formatReminderList(reminders) {
1003
+ if (reminders.length === 0) return "No reminders.";
1004
+ return reminders.map(formatReminder).join("\n");
1005
+ }
1006
+ function formatReminderCanceled(r) {
1007
+ return `Reminder canceled: ${formatReminder(r)}`;
1008
+ }
1009
+
1010
+ // src/commands/reminder/schedule.ts
1011
+ function registerReminderScheduleCommand(parent) {
1012
+ parent.command("schedule").description("Schedule a reminder that fires at a future time").requiredOption("--title <t>", "Short description of what the reminder is about").option(
1013
+ "--delay-seconds <n>",
1014
+ "Preferred for relative times. Fires this many seconds from now (server-computed, timezone-safe)"
1015
+ ).option(
1016
+ "--fire-at <iso>",
1017
+ "ISO-8601 UTC timestamp, e.g. 2026-04-21T09:00:00Z. Use only for absolute calendar times"
1018
+ ).option(
1019
+ "--repeat <rule>",
1020
+ "Recurrence rule: every:15m | every:2h | every:1d | daily@09:00 | weekly:mon,fri@09:00"
1021
+ ).option(
1022
+ "--channel <ref>",
1023
+ "Optional channel to post a receipt message in (e.g. #general, dm:@alice). Without this, receipt is only posted when --msg-id is given."
1024
+ ).option("--msg-id <id>", "Optional message id this reminder is anchored to").action(async (opts) => {
1025
+ let ctx;
1026
+ try {
1027
+ ctx = loadAgentContext();
1028
+ } catch (err) {
1029
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
1030
+ throw err;
1031
+ }
1032
+ if (!opts.delaySeconds && !opts.fireAt && !opts.repeat) {
1033
+ fail("INVALID_ARG", "Provide --delay-seconds, --fire-at, or --repeat");
1034
+ }
1035
+ if (opts.delaySeconds && opts.fireAt) {
1036
+ fail("INVALID_ARG", "Pass either --delay-seconds or --fire-at, not both");
1037
+ }
1038
+ const body = { title: opts.title, msgId: opts.msgId ?? null };
1039
+ if (opts.delaySeconds !== void 0) {
1040
+ const n = Number(opts.delaySeconds);
1041
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
1042
+ fail("INVALID_ARG", `--delay-seconds must be a positive integer; got ${opts.delaySeconds}`);
1043
+ }
1044
+ body.delaySeconds = n;
1045
+ }
1046
+ if (opts.fireAt !== void 0) body.fireAt = opts.fireAt;
1047
+ if (opts.repeat !== void 0) {
1048
+ body.repeat = opts.repeat;
1049
+ body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1050
+ }
1051
+ if (opts.channel !== void 0) body.channel = opts.channel;
1052
+ const client = new ApiClient(ctx);
1053
+ const res = await client.request(
1054
+ "POST",
1055
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders`,
1056
+ body
1057
+ );
1058
+ if (!res.ok || !res.data?.reminder) {
1059
+ const code = res.status >= 500 ? "SERVER_5XX" : "SCHEDULE_FAILED";
1060
+ fail(code, res.error ?? `HTTP ${res.status}`);
1061
+ }
1062
+ process.stdout.write(formatReminderScheduled(res.data.reminder, res.data.warning ?? null) + "\n");
1063
+ });
1064
+ }
1065
+
1066
+ // src/commands/reminder/list.ts
1067
+ var VALID_STATUSES2 = /* @__PURE__ */ new Set(["scheduled", "fired", "canceled"]);
1068
+ function registerReminderListCommand(parent) {
1069
+ parent.command("list").description("List your own reminders (defaults to scheduled)").option(
1070
+ "--status <s>",
1071
+ "Comma-separated statuses (scheduled,fired,canceled). Default: scheduled"
1072
+ ).action(async (opts) => {
1073
+ let ctx;
1074
+ try {
1075
+ ctx = loadAgentContext();
1076
+ } catch (err) {
1077
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
1078
+ throw err;
1079
+ }
1080
+ const statusRaw = opts.status && opts.status.trim().length > 0 ? opts.status.trim() : "scheduled";
1081
+ for (const s of statusRaw.split(",").map((x) => x.trim()).filter(Boolean)) {
1082
+ if (!VALID_STATUSES2.has(s)) {
1083
+ fail("INVALID_ARG", `--status entries must be one of ${Array.from(VALID_STATUSES2).join("|")}; got ${s}`);
1084
+ }
1085
+ }
1086
+ const params = new URLSearchParams();
1087
+ params.set("status", statusRaw);
1088
+ const client = new ApiClient(ctx);
1089
+ const res = await client.request(
1090
+ "GET",
1091
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders?${params.toString()}`
1092
+ );
1093
+ if (!res.ok) {
1094
+ const code = res.status >= 500 ? "SERVER_5XX" : "LIST_FAILED";
1095
+ fail(code, res.error ?? `HTTP ${res.status}`);
1096
+ }
1097
+ process.stdout.write(formatReminderList(res.data?.reminders ?? []) + "\n");
1098
+ });
1099
+ }
1100
+
1101
+ // src/commands/reminder/cancel.ts
1102
+ function registerReminderCancelCommand(parent) {
1103
+ parent.command("cancel").description("Cancel a scheduled reminder by id (full uuid or 8-char prefix)").requiredOption("--id <id>", "Reminder id (full uuid or short prefix)").action(async (opts) => {
1104
+ let ctx;
1105
+ try {
1106
+ ctx = loadAgentContext();
1107
+ } catch (err) {
1108
+ if (err instanceof AgentBootstrapError) fail(err.code, err.message);
1109
+ throw err;
1110
+ }
1111
+ if (!opts.id || opts.id.trim().length === 0) {
1112
+ fail("INVALID_ARG", "--id is required");
1113
+ }
1114
+ const client = new ApiClient(ctx);
1115
+ let fullId = opts.id;
1116
+ if (opts.id.length < 32) {
1117
+ const listRes = await client.request(
1118
+ "GET",
1119
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders?status=scheduled`
1120
+ );
1121
+ if (!listRes.ok) {
1122
+ const code = listRes.status >= 500 ? "SERVER_5XX" : "CANCEL_FAILED";
1123
+ fail(code, listRes.error ?? `HTTP ${listRes.status}`);
1124
+ }
1125
+ const matches = (listRes.data?.reminders ?? []).filter((r) => r.reminderId.startsWith(opts.id));
1126
+ if (matches.length === 0) {
1127
+ fail("NOT_FOUND", `No scheduled reminder matches id prefix '${opts.id}'.`);
1128
+ }
1129
+ if (matches.length > 1) {
1130
+ fail("AMBIGUOUS", `Ambiguous id prefix '${opts.id}' matches ${matches.length} reminders; pass a longer id.`);
1131
+ }
1132
+ fullId = matches[0].reminderId;
1133
+ }
1134
+ const res = await client.request(
1135
+ "DELETE",
1136
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders/${encodeURIComponent(fullId)}`
1137
+ );
1138
+ if (!res.ok || !res.data?.reminder) {
1139
+ const code = res.status >= 500 ? "SERVER_5XX" : "CANCEL_FAILED";
1140
+ fail(code, res.error ?? `HTTP ${res.status}`);
1141
+ }
1142
+ process.stdout.write(formatReminderCanceled(res.data.reminder) + "\n");
1143
+ });
1144
+ }
1145
+
1146
+ // src/index.ts
1147
+ var program = new Command();
1148
+ program.name("slock").description(
1149
+ "Agent-facing execution interface for Slock. Invoked by daemon-spawned agent processes \u2014 not a user-facing CLI product."
1150
+ ).version("0.0.1");
1151
+ var authCmd = program.command("auth").description("Auth introspection");
1152
+ registerWhoamiCommand(authCmd);
1153
+ var serverCmd = program.command("server").description("Server / workspace introspection");
1154
+ registerServerInfoCommand(serverCmd);
1155
+ var messageCmd = program.command("message").description("Message operations");
1156
+ registerSendCommand(messageCmd);
1157
+ registerCheckCommand(messageCmd);
1158
+ registerReadCommand(messageCmd);
1159
+ registerSearchCommand(messageCmd);
1160
+ var attachmentCmd = program.command("attachment").description("Attachment operations");
1161
+ registerAttachmentUploadCommand(attachmentCmd);
1162
+ registerAttachmentViewCommand(attachmentCmd);
1163
+ var taskCmd = program.command("task").description("Task board operations");
1164
+ registerTaskListCommand(taskCmd);
1165
+ registerTaskCreateCommand(taskCmd);
1166
+ registerTaskClaimCommand(taskCmd);
1167
+ registerTaskUnclaimCommand(taskCmd);
1168
+ registerTaskUpdateCommand(taskCmd);
1169
+ var reminderCmd = program.command("reminder").description("Reminder operations");
1170
+ registerReminderScheduleCommand(reminderCmd);
1171
+ registerReminderListCommand(reminderCmd);
1172
+ registerReminderCancelCommand(reminderCmd);
1173
+ program.parseAsync().catch((err) => {
1174
+ if (err instanceof CliExit) {
1175
+ process.exitCode = err.exitCode;
1176
+ } else {
1177
+ process.stderr.write(`Unexpected error: ${err?.message ?? err}
1178
+ `);
1179
+ process.exitCode = 1;
1180
+ }
1181
+ });