@slock-ai/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1921 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1921 @@
|
|
|
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
|
+
let errorCode = null;
|
|
163
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
164
|
+
if (contentType.includes("application/json")) {
|
|
165
|
+
const parsed = await res.json().catch(() => null);
|
|
166
|
+
if (res.ok) {
|
|
167
|
+
data = parsed;
|
|
168
|
+
} else {
|
|
169
|
+
error = parsed?.error ?? `HTTP ${res.status}`;
|
|
170
|
+
errorCode = parsed?.errorCode ?? null;
|
|
171
|
+
}
|
|
172
|
+
} else if (!res.ok) {
|
|
173
|
+
error = `HTTP ${res.status}`;
|
|
174
|
+
}
|
|
175
|
+
return { ok: res.ok, status: res.status, data, error, errorCode };
|
|
176
|
+
}
|
|
177
|
+
async request(method, pathname, body) {
|
|
178
|
+
const url = new URL(pathname, this.ctx.serverUrl).toString();
|
|
179
|
+
const headers = this.buildAuthHeaders();
|
|
180
|
+
headers["Content-Type"] = "application/json";
|
|
181
|
+
const dispatcher = buildFetchDispatcher(url);
|
|
182
|
+
const init = {
|
|
183
|
+
method,
|
|
184
|
+
headers,
|
|
185
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
186
|
+
};
|
|
187
|
+
if (dispatcher) init.dispatcher = dispatcher;
|
|
188
|
+
const res = await fetch(url, init);
|
|
189
|
+
return this.parseJsonResponse(res);
|
|
190
|
+
}
|
|
191
|
+
// Multipart upload. Caller builds the FormData (file part + any text fields).
|
|
192
|
+
// Content-Type intentionally omitted so fetch sets the correct multipart
|
|
193
|
+
// boundary itself.
|
|
194
|
+
async requestMultipart(method, pathname, form) {
|
|
195
|
+
const url = new URL(pathname, this.ctx.serverUrl).toString();
|
|
196
|
+
const dispatcher = buildFetchDispatcher(url);
|
|
197
|
+
const init = {
|
|
198
|
+
method,
|
|
199
|
+
headers: this.buildAuthHeaders(),
|
|
200
|
+
body: form
|
|
201
|
+
};
|
|
202
|
+
if (dispatcher) init.dispatcher = dispatcher;
|
|
203
|
+
const res = await fetch(url, init);
|
|
204
|
+
return this.parseJsonResponse(res);
|
|
205
|
+
}
|
|
206
|
+
// Returns the raw Response so the caller can stream / save the body.
|
|
207
|
+
// For non-JSON downloads (binary attachments). Caller is responsible for
|
|
208
|
+
// consuming the body. On non-2xx, attempts to surface a JSON error.
|
|
209
|
+
async requestRaw(method, pathname) {
|
|
210
|
+
const url = new URL(pathname, this.ctx.serverUrl).toString();
|
|
211
|
+
const dispatcher = buildFetchDispatcher(url);
|
|
212
|
+
const init = {
|
|
213
|
+
method,
|
|
214
|
+
headers: this.buildAuthHeaders(),
|
|
215
|
+
redirect: "follow"
|
|
216
|
+
};
|
|
217
|
+
if (dispatcher) init.dispatcher = dispatcher;
|
|
218
|
+
const res = await fetch(url, init);
|
|
219
|
+
let error = null;
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
222
|
+
if (contentType.includes("application/json")) {
|
|
223
|
+
const parsed = await res.json().catch(() => null);
|
|
224
|
+
error = parsed?.error ?? `HTTP ${res.status}`;
|
|
225
|
+
} else {
|
|
226
|
+
error = `HTTP ${res.status}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { ok: res.ok, status: res.status, response: res, error };
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/commands/server/_format.ts
|
|
234
|
+
function formatRuntimeContext(ctx) {
|
|
235
|
+
if (!ctx) return "";
|
|
236
|
+
const lines = [
|
|
237
|
+
"### Current Runtime",
|
|
238
|
+
"Authoritative context for this agent process. Do not infer computer identity from hostname or cwd when this section is present."
|
|
239
|
+
];
|
|
240
|
+
if (ctx.agentId) lines.push(`- Agent ID: ${ctx.agentId}`);
|
|
241
|
+
if (ctx.serverId) lines.push(`- Server ID: ${ctx.serverId}`);
|
|
242
|
+
if (ctx.machineName || ctx.machineId) {
|
|
243
|
+
const label = ctx.machineName && ctx.machineId ? `${ctx.machineName} (${ctx.machineId})` : ctx.machineName || ctx.machineId;
|
|
244
|
+
lines.push(`- Computer: ${label}`);
|
|
245
|
+
}
|
|
246
|
+
if (ctx.machineHostname) lines.push(`- Hostname: ${ctx.machineHostname}`);
|
|
247
|
+
if (ctx.machineOs) lines.push(`- OS: ${ctx.machineOs}`);
|
|
248
|
+
if (ctx.daemonVersion) lines.push(`- Daemon: v${ctx.daemonVersion}`);
|
|
249
|
+
if (ctx.workspacePath) lines.push(`- Workspace: ${ctx.workspacePath}`);
|
|
250
|
+
return lines.length > 2 ? `${lines.join("\n")}
|
|
251
|
+
|
|
252
|
+
` : "";
|
|
253
|
+
}
|
|
254
|
+
function formatServerInfo(data) {
|
|
255
|
+
let text = "## Server\n\n";
|
|
256
|
+
const channels = data.channels ?? [];
|
|
257
|
+
const agents = data.agents ?? [];
|
|
258
|
+
const humans = data.humans ?? [];
|
|
259
|
+
text += formatRuntimeContext(data.runtimeContext);
|
|
260
|
+
text += "### Channels\n";
|
|
261
|
+
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. To leave a channel you have joined, use `slock channel leave --target "#name"`. To stop following a thread, use `slock thread unfollow --target "#name:shortid"`.\n';
|
|
262
|
+
if (channels.length > 0) {
|
|
263
|
+
for (const t of channels) {
|
|
264
|
+
const status = t.joined ? "joined" : "not joined";
|
|
265
|
+
text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
|
|
266
|
+
` : ` - #${t.name} [${status}]
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
text += " (none)\n";
|
|
271
|
+
}
|
|
272
|
+
text += "\n### Agents\n";
|
|
273
|
+
text += "Other AI agents in this server.\n";
|
|
274
|
+
if (agents.length > 0) {
|
|
275
|
+
for (const a of agents) {
|
|
276
|
+
text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
|
|
277
|
+
` : ` - @${a.name} (${a.status})
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
text += " (none)\n";
|
|
282
|
+
}
|
|
283
|
+
text += "\n### Humans\n";
|
|
284
|
+
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.
|
|
285
|
+
`;
|
|
286
|
+
if (humans.length > 0) {
|
|
287
|
+
for (const u of humans) {
|
|
288
|
+
text += u.description ? ` - @${u.name} \u2014 ${u.description}
|
|
289
|
+
` : ` - @${u.name}
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
text += " (none)\n";
|
|
294
|
+
}
|
|
295
|
+
return text;
|
|
296
|
+
}
|
|
297
|
+
function formatChannelMembers(data) {
|
|
298
|
+
let text = "## Channel Members\n\n";
|
|
299
|
+
const ref = data.channel?.ref ?? "(unknown)";
|
|
300
|
+
const type = data.channel?.type ? ` (${data.channel.type})` : "";
|
|
301
|
+
const agents = data.agents ?? [];
|
|
302
|
+
const humans = data.humans ?? [];
|
|
303
|
+
text += `Channel: ${ref}${type}
|
|
304
|
+
`;
|
|
305
|
+
text += "Members means join/post authority for this surface.\n\n";
|
|
306
|
+
text += "### Agents\n";
|
|
307
|
+
if (agents.length > 0) {
|
|
308
|
+
for (const a of agents) {
|
|
309
|
+
text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
|
|
310
|
+
` : ` - @${a.name} (${a.status})
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
text += " (none)\n";
|
|
315
|
+
}
|
|
316
|
+
text += "\n### Humans\n";
|
|
317
|
+
if (humans.length > 0) {
|
|
318
|
+
for (const u of humans) {
|
|
319
|
+
text += u.description ? ` - @${u.name} \u2014 ${u.description}
|
|
320
|
+
` : ` - @${u.name}
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
text += " (none)\n";
|
|
325
|
+
}
|
|
326
|
+
return text;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/commands/channel/members.ts
|
|
330
|
+
function registerChannelMembersCommand(parent) {
|
|
331
|
+
parent.command("members").description("List agents and humans who are members of a channel, DM, or thread").argument("<target>", "Channel / DM / thread target, e.g. #proj-runtime, dm:@alice, #proj-runtime:abcd1234").action(async (target) => {
|
|
332
|
+
let ctx;
|
|
333
|
+
try {
|
|
334
|
+
ctx = loadAgentContext();
|
|
335
|
+
} catch (err) {
|
|
336
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
const client = new ApiClient(ctx);
|
|
340
|
+
const channel = String(target || "").trim();
|
|
341
|
+
if (!channel) fail("MEMBERS_FAILED", "target is required");
|
|
342
|
+
const encoded = encodeURIComponent(channel);
|
|
343
|
+
const res = await client.request(
|
|
344
|
+
"GET",
|
|
345
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/channel-members?channel=${encoded}`
|
|
346
|
+
);
|
|
347
|
+
if (!res.ok) {
|
|
348
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "MEMBERS_FAILED";
|
|
349
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
350
|
+
}
|
|
351
|
+
process.stdout.write(formatChannelMembers(res.data));
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/commands/server/info.ts
|
|
356
|
+
function registerServerInfoCommand(parent) {
|
|
357
|
+
parent.command("info").description("List channels, agents, and humans on the current server").action(async () => {
|
|
358
|
+
let ctx;
|
|
359
|
+
try {
|
|
360
|
+
ctx = loadAgentContext();
|
|
361
|
+
} catch (err) {
|
|
362
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
363
|
+
throw err;
|
|
364
|
+
}
|
|
365
|
+
const client = new ApiClient(ctx);
|
|
366
|
+
const res = await client.request(
|
|
367
|
+
"GET",
|
|
368
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/server`
|
|
369
|
+
);
|
|
370
|
+
if (!res.ok) {
|
|
371
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "INFO_FAILED";
|
|
372
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
373
|
+
}
|
|
374
|
+
const data = res.data;
|
|
375
|
+
if (data?.runtimeContext) {
|
|
376
|
+
data.runtimeContext = {
|
|
377
|
+
...data.runtimeContext,
|
|
378
|
+
workspacePath: data.runtimeContext.workspacePath ?? process.env.SLOCK_CURRENT_WORKSPACE_PATH ?? null
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
process.stdout.write(formatServerInfo(data));
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/commands/channel/leave.ts
|
|
386
|
+
function parseRegularChannelTarget(target) {
|
|
387
|
+
if (!target.startsWith("#")) return null;
|
|
388
|
+
if (target.includes(":")) return null;
|
|
389
|
+
const name = target.slice(1).trim();
|
|
390
|
+
return name.length > 0 ? name : null;
|
|
391
|
+
}
|
|
392
|
+
function formatLeaveChannelResult(target) {
|
|
393
|
+
return `Left ${target}. You can still inspect visible public channel history there, but you can no longer send or receive ordinary channel delivery until a human adds you again.`;
|
|
394
|
+
}
|
|
395
|
+
function formatAlreadyNotJoined(target) {
|
|
396
|
+
return `Already not joined in ${target}.`;
|
|
397
|
+
}
|
|
398
|
+
function registerChannelLeaveCommand(parent) {
|
|
399
|
+
parent.command("leave").description("Leave a regular channel you have joined").requiredOption("--target <target>", "Regular channel to leave, e.g. '#engineering'").action(async (opts) => {
|
|
400
|
+
const channelName = parseRegularChannelTarget(opts.target);
|
|
401
|
+
if (!channelName) {
|
|
402
|
+
fail("INVALID_TARGET", "Target must be a regular channel in the form '#channel-name'. DMs and thread targets are not supported.");
|
|
403
|
+
}
|
|
404
|
+
let ctx;
|
|
405
|
+
try {
|
|
406
|
+
ctx = loadAgentContext();
|
|
407
|
+
} catch (err) {
|
|
408
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
const client = new ApiClient(ctx);
|
|
412
|
+
const infoRes = await client.request(
|
|
413
|
+
"GET",
|
|
414
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/server`
|
|
415
|
+
);
|
|
416
|
+
if (!infoRes.ok) {
|
|
417
|
+
const code = infoRes.status >= 500 ? "SERVER_5XX" : "INFO_FAILED";
|
|
418
|
+
fail(code, infoRes.error ?? `HTTP ${infoRes.status}`);
|
|
419
|
+
}
|
|
420
|
+
const channel = (infoRes.data?.channels ?? []).find((candidate) => candidate.name === channelName);
|
|
421
|
+
if (!channel) {
|
|
422
|
+
fail("NOT_FOUND", `Channel not found: ${opts.target}`);
|
|
423
|
+
}
|
|
424
|
+
if (!channel.joined) {
|
|
425
|
+
process.stdout.write(formatAlreadyNotJoined(opts.target) + "\n");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const leaveRes = await client.request(
|
|
429
|
+
"POST",
|
|
430
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/channels/${encodeURIComponent(channel.id)}/leave`
|
|
431
|
+
);
|
|
432
|
+
if (!leaveRes.ok) {
|
|
433
|
+
const code = leaveRes.status >= 500 ? "SERVER_5XX" : "LEAVE_FAILED";
|
|
434
|
+
fail(code, leaveRes.error ?? `HTTP ${leaveRes.status}`);
|
|
435
|
+
}
|
|
436
|
+
process.stdout.write(formatLeaveChannelResult(opts.target) + "\n");
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/commands/thread/unfollow.ts
|
|
441
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
442
|
+
var SHORT_ID_RE = /^[0-9a-f]{8}$/i;
|
|
443
|
+
function parseThreadTarget(target) {
|
|
444
|
+
const trimmed = target.trim();
|
|
445
|
+
if (UUID_RE.test(trimmed)) return trimmed;
|
|
446
|
+
if (trimmed.startsWith("#")) {
|
|
447
|
+
const rest = trimmed.slice(1);
|
|
448
|
+
const lastColon = rest.lastIndexOf(":");
|
|
449
|
+
if (lastColon > 0 && SHORT_ID_RE.test(rest.slice(lastColon + 1))) {
|
|
450
|
+
return trimmed;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (trimmed.startsWith("dm:@") || trimmed.startsWith("DM:@")) {
|
|
454
|
+
const rest = trimmed.slice(4);
|
|
455
|
+
const lastColon = rest.lastIndexOf(":");
|
|
456
|
+
if (lastColon > 0 && SHORT_ID_RE.test(rest.slice(lastColon + 1))) {
|
|
457
|
+
return trimmed;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
function formatUnfollowThreadResult(target) {
|
|
463
|
+
return `Unfollowed ${target}. You can still inspect the thread when its parent conversation is visible, but you will no longer receive ordinary thread delivery unless you follow it again or are mentioned.`;
|
|
464
|
+
}
|
|
465
|
+
function registerThreadUnfollowCommand(parent) {
|
|
466
|
+
parent.command("unfollow").description("Stop following a thread you no longer need ordinary delivery for").requiredOption("--target <target>", "Thread target, e.g. '#engineering:abcd1234' or 'dm:@alice:abcd1234'").action(async (opts) => {
|
|
467
|
+
const thread = parseThreadTarget(opts.target);
|
|
468
|
+
if (!thread) {
|
|
469
|
+
fail("INVALID_TARGET", "Thread must be a thread target like '#channel:abcd1234', 'dm:@peer:abcd1234', or a thread channel UUID.");
|
|
470
|
+
}
|
|
471
|
+
let ctx;
|
|
472
|
+
try {
|
|
473
|
+
ctx = loadAgentContext();
|
|
474
|
+
} catch (err) {
|
|
475
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
const client = new ApiClient(ctx);
|
|
479
|
+
const res = await client.request(
|
|
480
|
+
"POST",
|
|
481
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/threads/unfollow`,
|
|
482
|
+
{ thread }
|
|
483
|
+
);
|
|
484
|
+
if (!res.ok) {
|
|
485
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "UNFOLLOW_FAILED";
|
|
486
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
487
|
+
}
|
|
488
|
+
process.stdout.write(formatUnfollowThreadResult(thread) + "\n");
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/commands/message/_format.ts
|
|
493
|
+
function toLocalTime(iso) {
|
|
494
|
+
const d = new Date(iso);
|
|
495
|
+
if (isNaN(d.getTime())) return iso;
|
|
496
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
497
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
498
|
+
}
|
|
499
|
+
function formatTarget(m) {
|
|
500
|
+
if (m.channel_type === "thread" && m.parent_channel_name) {
|
|
501
|
+
const shortId = m.channel_name?.startsWith("thread-") ? m.channel_name.slice(7) : m.channel_name;
|
|
502
|
+
if (m.parent_channel_type === "dm") {
|
|
503
|
+
return `dm:@${m.parent_channel_name}:${shortId}`;
|
|
504
|
+
}
|
|
505
|
+
return `#${m.parent_channel_name}:${shortId}`;
|
|
506
|
+
}
|
|
507
|
+
if (m.channel_type === "dm") {
|
|
508
|
+
return `dm:@${m.channel_name}`;
|
|
509
|
+
}
|
|
510
|
+
return `#${m.channel_name}`;
|
|
511
|
+
}
|
|
512
|
+
function formatSenderHandle(m) {
|
|
513
|
+
const name = m.sender_name ?? "unknown";
|
|
514
|
+
const desc = m.sender_description ?? null;
|
|
515
|
+
return desc ? `@${name} \u2014 ${desc}` : `@${name}`;
|
|
516
|
+
}
|
|
517
|
+
function formatAttachmentSuffix(attachments) {
|
|
518
|
+
if (!attachments?.length) return "";
|
|
519
|
+
return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use slock attachment view to download]`;
|
|
520
|
+
}
|
|
521
|
+
function formatMessageLine(m) {
|
|
522
|
+
const target = formatTarget(m);
|
|
523
|
+
const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
|
|
524
|
+
const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
|
|
525
|
+
const senderType = ` type=${m.sender_type}`;
|
|
526
|
+
const content = m.content ?? "";
|
|
527
|
+
const attachSuffix = formatAttachmentSuffix(m.attachments);
|
|
528
|
+
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}` : ""}]` : "";
|
|
529
|
+
return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${content}${attachSuffix}${taskSuffix}`;
|
|
530
|
+
}
|
|
531
|
+
function formatMessages(messages) {
|
|
532
|
+
if (messages.length === 0) return "No new messages.";
|
|
533
|
+
return messages.map(formatMessageLine).join("\n");
|
|
534
|
+
}
|
|
535
|
+
function formatHistoryMessageLine(m) {
|
|
536
|
+
const senderName = m.senderName ?? m.sender_name ?? "unknown";
|
|
537
|
+
const senderDescription = m.senderDescription ?? m.sender_description ?? null;
|
|
538
|
+
const headerParts = [
|
|
539
|
+
`seq=${m.seq}`,
|
|
540
|
+
`msg=${m.id || "-"}`,
|
|
541
|
+
`time=${m.createdAt ? toLocalTime(m.createdAt) : "-"}`
|
|
542
|
+
];
|
|
543
|
+
if (m.senderType) headerParts.push(`type=${m.senderType}`);
|
|
544
|
+
if (m.threadId) headerParts.push(`threadId=${m.threadId}`);
|
|
545
|
+
if ((m.replyCount ?? 0) > 0) headerParts.push(`replyCount=${m.replyCount}`);
|
|
546
|
+
const attachSuffix = formatAttachmentSuffix(m.attachments);
|
|
547
|
+
const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ""}]` : "";
|
|
548
|
+
const handle = senderDescription ? `@${senderName} \u2014 ${senderDescription}` : `@${senderName}`;
|
|
549
|
+
return `[${headerParts.join(" ")}] ${handle}: ${m.content}${attachSuffix}${taskSuffix}`;
|
|
550
|
+
}
|
|
551
|
+
function formatHistory(channel, data, opts) {
|
|
552
|
+
if (!data.messages || data.messages.length === 0) return "No messages in this channel.";
|
|
553
|
+
const formatted = data.messages.map((m) => formatHistoryMessageLine({
|
|
554
|
+
...m,
|
|
555
|
+
senderName: m.senderName ?? m.sender_name ?? "unknown",
|
|
556
|
+
senderDescription: m.senderDescription ?? m.sender_description ?? null
|
|
557
|
+
})).join("\n");
|
|
558
|
+
let footer = "";
|
|
559
|
+
if (data.historyLimited) {
|
|
560
|
+
footer = `
|
|
561
|
+
|
|
562
|
+
--- ${data.historyLimitMessage || "Message history is limited on this plan."} ---`;
|
|
563
|
+
} else if (opts?.around && data.messages.length > 0 && (data.has_older || data.has_newer)) {
|
|
564
|
+
const minSeq = data.messages[0].seq;
|
|
565
|
+
const maxSeq = data.messages[data.messages.length - 1].seq;
|
|
566
|
+
footer = `
|
|
567
|
+
|
|
568
|
+
--- Context window shown. Use before=${minSeq} to load older messages or after=${maxSeq} to load newer messages. ---`;
|
|
569
|
+
} else if (data.has_more && data.messages.length > 0) {
|
|
570
|
+
if (opts?.after) {
|
|
571
|
+
const maxSeq = data.messages[data.messages.length - 1].seq;
|
|
572
|
+
footer = `
|
|
573
|
+
|
|
574
|
+
--- ${data.messages.length} messages shown. Use after=${maxSeq} to load more recent messages. ---`;
|
|
575
|
+
} else {
|
|
576
|
+
const minSeq = data.messages[0].seq;
|
|
577
|
+
footer = `
|
|
578
|
+
|
|
579
|
+
--- ${data.messages.length} messages shown. Use before=${minSeq} to load older messages. ---`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
let header = `## Message History for ${channel}${opts?.around ? ` around ${opts.around}` : ""} (${data.messages.length} messages)`;
|
|
583
|
+
if ((data.last_read_seq ?? 0) > 0 && !opts?.after && !opts?.before && !opts?.around) {
|
|
584
|
+
header += `
|
|
585
|
+
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.`;
|
|
586
|
+
}
|
|
587
|
+
return `${header}
|
|
588
|
+
|
|
589
|
+
${formatted}${footer}`;
|
|
590
|
+
}
|
|
591
|
+
function formatSearchTarget(result) {
|
|
592
|
+
if (result.channelType === "thread") {
|
|
593
|
+
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;
|
|
594
|
+
if (result.parentChannelType === "dm") {
|
|
595
|
+
return `dm:@${result.parentChannelName}:${shortId}`;
|
|
596
|
+
}
|
|
597
|
+
return `#${result.parentChannelName}:${shortId}`;
|
|
598
|
+
}
|
|
599
|
+
if (result.channelType === "dm") {
|
|
600
|
+
return `dm:@${result.channelName}`;
|
|
601
|
+
}
|
|
602
|
+
return `#${result.channelName}`;
|
|
603
|
+
}
|
|
604
|
+
function formatSearchResults(query, data) {
|
|
605
|
+
if (!data.results || data.results.length === 0) return "No search results.";
|
|
606
|
+
const formatted = data.results.map((result, index) => {
|
|
607
|
+
const target = formatSearchTarget(result);
|
|
608
|
+
const threadInfo = result.channelType === "thread" ? `
|
|
609
|
+
thread: ${result.parentChannelName} -> ${target}` : "";
|
|
610
|
+
return [
|
|
611
|
+
`[${index + 1}] msg=${result.id} seq=${result.seq} time=${result.createdAt ? toLocalTime(result.createdAt) : "-"}`,
|
|
612
|
+
`target: ${target}${threadInfo}`,
|
|
613
|
+
`sender: @${result.senderName} (${result.senderType})`,
|
|
614
|
+
`content: ${result.content}`,
|
|
615
|
+
`match: ${result.snippet}`,
|
|
616
|
+
`next: slock message read --channel "${target}" --around "${result.id}" --limit 20`
|
|
617
|
+
].join("\n");
|
|
618
|
+
}).join("\n\n");
|
|
619
|
+
return `## Search Results for "${query}" (${data.results.length} results)
|
|
620
|
+
|
|
621
|
+
${formatted}`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/commands/message/send.ts
|
|
625
|
+
var SendContentError = class extends Error {
|
|
626
|
+
constructor(code, message) {
|
|
627
|
+
super(message);
|
|
628
|
+
this.code = code;
|
|
629
|
+
this.name = "SendContentError";
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
async function readStream(stream) {
|
|
633
|
+
let content = "";
|
|
634
|
+
stream.setEncoding("utf8");
|
|
635
|
+
for await (const chunk of stream) {
|
|
636
|
+
content += String(chunk);
|
|
637
|
+
}
|
|
638
|
+
return content;
|
|
639
|
+
}
|
|
640
|
+
function missingContentMessage() {
|
|
641
|
+
return [
|
|
642
|
+
"No message content received on stdin.",
|
|
643
|
+
"Use a heredoc or pipe content into slock message send:",
|
|
644
|
+
` slock message send --target "#channel" <<'EOF'`,
|
|
645
|
+
" message body",
|
|
646
|
+
" EOF"
|
|
647
|
+
].join("\n");
|
|
648
|
+
}
|
|
649
|
+
async function resolveSendContent(input = process.stdin) {
|
|
650
|
+
if (input.isTTY) {
|
|
651
|
+
throw new SendContentError("MISSING_CONTENT", missingContentMessage());
|
|
652
|
+
}
|
|
653
|
+
const content = await readStream(input);
|
|
654
|
+
if (content.trim().length === 0) {
|
|
655
|
+
throw new SendContentError("MISSING_CONTENT", missingContentMessage());
|
|
656
|
+
}
|
|
657
|
+
return content;
|
|
658
|
+
}
|
|
659
|
+
function rejectArgContent(positionalContent, opts) {
|
|
660
|
+
if (positionalContent.length > 0) {
|
|
661
|
+
throw new SendContentError(
|
|
662
|
+
"POSITIONAL_CONTENT_UNSUPPORTED",
|
|
663
|
+
[
|
|
664
|
+
"Message content must be provided on stdin, not as positional arguments.",
|
|
665
|
+
"Use:",
|
|
666
|
+
` slock message send --target "#channel" <<'EOF'`,
|
|
667
|
+
" message body",
|
|
668
|
+
" EOF"
|
|
669
|
+
].join("\n")
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
if (opts.content !== void 0) {
|
|
673
|
+
throw new SendContentError(
|
|
674
|
+
"CONTENT_FLAG_UNSUPPORTED",
|
|
675
|
+
[
|
|
676
|
+
"--content is no longer supported. Pipe message content to stdin.",
|
|
677
|
+
"Use:",
|
|
678
|
+
` printf 'hello' | slock message send --target "#channel"`
|
|
679
|
+
].join("\n")
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function registerSendCommand(parent) {
|
|
684
|
+
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(
|
|
685
|
+
"--attachment-id <id>",
|
|
686
|
+
"Attachment id to link (repeatable). Get one from `slock attachment upload`.",
|
|
687
|
+
(value, prev = []) => prev.concat(value)
|
|
688
|
+
).action(async (positionalContent, opts) => {
|
|
689
|
+
try {
|
|
690
|
+
rejectArgContent(positionalContent, opts);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
if (err instanceof SendContentError) fail(err.code, err.message);
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
695
|
+
let ctx;
|
|
696
|
+
try {
|
|
697
|
+
ctx = loadAgentContext();
|
|
698
|
+
} catch (err) {
|
|
699
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
700
|
+
throw err;
|
|
701
|
+
}
|
|
702
|
+
const client = new ApiClient(ctx);
|
|
703
|
+
let content;
|
|
704
|
+
try {
|
|
705
|
+
content = await resolveSendContent();
|
|
706
|
+
} catch (err) {
|
|
707
|
+
if (err instanceof SendContentError) fail(err.code, err.message);
|
|
708
|
+
throw err;
|
|
709
|
+
}
|
|
710
|
+
const body = { target: opts.target, content };
|
|
711
|
+
if (opts.attachmentId && opts.attachmentId.length > 0) {
|
|
712
|
+
body.attachmentIds = opts.attachmentId;
|
|
713
|
+
}
|
|
714
|
+
const res = await client.request(
|
|
715
|
+
"POST",
|
|
716
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/send`,
|
|
717
|
+
body
|
|
718
|
+
);
|
|
719
|
+
if (!res.ok) {
|
|
720
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "SEND_FAILED";
|
|
721
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
722
|
+
}
|
|
723
|
+
const data = res.data;
|
|
724
|
+
const shortId = data.messageId ? data.messageId.slice(0, 8) : null;
|
|
725
|
+
const replyHint = shortId ? ` (to reply in this message's thread, use target "${opts.target.includes(":") ? opts.target : opts.target + ":" + shortId}")` : "";
|
|
726
|
+
let unreadSection = "";
|
|
727
|
+
if (data.recentUnread && data.recentUnread.length > 0) {
|
|
728
|
+
unreadSection = `
|
|
729
|
+
|
|
730
|
+
--- New messages you may have missed ---
|
|
731
|
+
${formatMessages(data.recentUnread)}`;
|
|
732
|
+
}
|
|
733
|
+
process.stdout.write(`Message sent to ${opts.target}. Message ID: ${data.messageId}${replyHint}${unreadSection}
|
|
734
|
+
`);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/commands/message/_inbox.ts
|
|
739
|
+
async function drainInbox(ctx, opts) {
|
|
740
|
+
const client = new ApiClient(ctx);
|
|
741
|
+
const agentPath = `/internal/agent/${encodeURIComponent(ctx.agentId)}`;
|
|
742
|
+
const failCode = opts.block ? "WAIT_FAILED" : "CHECK_FAILED";
|
|
743
|
+
const query = [];
|
|
744
|
+
if (opts.block) query.push("block=true");
|
|
745
|
+
if (opts.block && opts.timeoutMs !== void 0) query.push(`timeout=${opts.timeoutMs}`);
|
|
746
|
+
const path = query.length > 0 ? `${agentPath}/receive?${query.join("&")}` : `${agentPath}/receive`;
|
|
747
|
+
const res = await client.request("GET", path);
|
|
748
|
+
if (!res.ok) {
|
|
749
|
+
const code = res.status >= 500 ? "SERVER_5XX" : failCode;
|
|
750
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
751
|
+
}
|
|
752
|
+
const messages = res.data?.messages ?? [];
|
|
753
|
+
const seqs = messages.map((m) => m.seq).filter((s) => Number.isInteger(s) && s > 0);
|
|
754
|
+
if (seqs.length === 0) return { messages };
|
|
755
|
+
const ack = await client.request("POST", `${agentPath}/receive-ack`, { seqs });
|
|
756
|
+
if (ack.ok) return { messages };
|
|
757
|
+
const ackCode = ack.status >= 500 ? "SERVER_5XX" : "ACK_FAILED";
|
|
758
|
+
const ackMessage = ack.error ?? `HTTP ${ack.status}`;
|
|
759
|
+
return { messages, ackFailure: { code: ackCode, message: ackMessage } };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/commands/message/check.ts
|
|
763
|
+
function registerCheckCommand(parent) {
|
|
764
|
+
parent.command("check").description("Drain the agent inbox (non-blocking). Acks delivered seqs before returning.").action(async () => {
|
|
765
|
+
let ctx;
|
|
766
|
+
try {
|
|
767
|
+
ctx = loadAgentContext();
|
|
768
|
+
} catch (err) {
|
|
769
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
770
|
+
throw err;
|
|
771
|
+
}
|
|
772
|
+
const result = await drainInbox(ctx, { block: false });
|
|
773
|
+
process.stdout.write(formatMessages(result.messages) + "\n");
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/commands/message/read.ts
|
|
778
|
+
function parsePositiveInt(name, raw) {
|
|
779
|
+
if (raw === void 0) return void 0;
|
|
780
|
+
const n = Number(raw);
|
|
781
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
782
|
+
fail("INVALID_ARG", `--${name} must be a positive integer; got ${raw}`);
|
|
783
|
+
}
|
|
784
|
+
return n;
|
|
785
|
+
}
|
|
786
|
+
function registerReadCommand(parent) {
|
|
787
|
+
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) => {
|
|
788
|
+
let ctx;
|
|
789
|
+
try {
|
|
790
|
+
ctx = loadAgentContext();
|
|
791
|
+
} catch (err) {
|
|
792
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
793
|
+
throw err;
|
|
794
|
+
}
|
|
795
|
+
const before = parsePositiveInt("before", opts.before);
|
|
796
|
+
const after = parsePositiveInt("after", opts.after);
|
|
797
|
+
const limit = parsePositiveInt("limit", opts.limit);
|
|
798
|
+
const params = new URLSearchParams();
|
|
799
|
+
params.set("channel", opts.channel);
|
|
800
|
+
if (before !== void 0) params.set("before", String(before));
|
|
801
|
+
if (after !== void 0) params.set("after", String(after));
|
|
802
|
+
if (opts.around !== void 0) params.set("around", opts.around);
|
|
803
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
804
|
+
const client = new ApiClient(ctx);
|
|
805
|
+
const res = await client.request(
|
|
806
|
+
"GET",
|
|
807
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/history?${params.toString()}`
|
|
808
|
+
);
|
|
809
|
+
if (!res.ok) {
|
|
810
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "READ_FAILED";
|
|
811
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
812
|
+
}
|
|
813
|
+
process.stdout.write(formatHistory(opts.channel, res.data, { around: opts.around, after, before }) + "\n");
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/commands/message/search.ts
|
|
818
|
+
function registerSearchCommand(parent) {
|
|
819
|
+
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) => {
|
|
820
|
+
let ctx;
|
|
821
|
+
try {
|
|
822
|
+
ctx = loadAgentContext();
|
|
823
|
+
} catch (err) {
|
|
824
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
825
|
+
throw err;
|
|
826
|
+
}
|
|
827
|
+
let limit;
|
|
828
|
+
if (opts.limit !== void 0) {
|
|
829
|
+
const n = Number(opts.limit);
|
|
830
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
831
|
+
fail("INVALID_ARG", `--limit must be a positive integer; got ${opts.limit}`);
|
|
832
|
+
}
|
|
833
|
+
limit = n;
|
|
834
|
+
}
|
|
835
|
+
const params = new URLSearchParams();
|
|
836
|
+
params.set("q", opts.query);
|
|
837
|
+
if (opts.channel) params.set("channel", opts.channel);
|
|
838
|
+
if (opts.sender) params.set("senderId", opts.sender);
|
|
839
|
+
if (opts.before) params.set("before", opts.before);
|
|
840
|
+
if (opts.after) params.set("after", opts.after);
|
|
841
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
842
|
+
const client = new ApiClient(ctx);
|
|
843
|
+
const res = await client.request(
|
|
844
|
+
"GET",
|
|
845
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/search?${params.toString()}`
|
|
846
|
+
);
|
|
847
|
+
if (!res.ok) {
|
|
848
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "SEARCH_FAILED";
|
|
849
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
850
|
+
}
|
|
851
|
+
process.stdout.write(formatSearchResults(opts.query, res.data) + "\n");
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/commands/attachment/upload.ts
|
|
856
|
+
import { existsSync, statSync, readFileSync } from "fs";
|
|
857
|
+
import { basename } from "path";
|
|
858
|
+
var MAX_BYTES = 10 * 1024 * 1024;
|
|
859
|
+
var FILENAME_MIME_MAP = {
|
|
860
|
+
".jpg": "image/jpeg",
|
|
861
|
+
".jpeg": "image/jpeg",
|
|
862
|
+
".png": "image/png",
|
|
863
|
+
".gif": "image/gif",
|
|
864
|
+
".webp": "image/webp",
|
|
865
|
+
".pdf": "application/pdf",
|
|
866
|
+
".txt": "text/plain",
|
|
867
|
+
".md": "text/markdown",
|
|
868
|
+
".json": "application/json",
|
|
869
|
+
".csv": "text/csv"
|
|
870
|
+
};
|
|
871
|
+
var MIME_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
|
|
872
|
+
var AttachmentUploadArgError = class extends Error {
|
|
873
|
+
constructor(code, message) {
|
|
874
|
+
super(message);
|
|
875
|
+
this.code = code;
|
|
876
|
+
this.name = "AttachmentUploadArgError";
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
function inferMimeTypeFromFilename(filename) {
|
|
880
|
+
const index = filename.lastIndexOf(".");
|
|
881
|
+
const ext = index >= 0 ? filename.slice(index).toLowerCase() : "";
|
|
882
|
+
return FILENAME_MIME_MAP[ext] || null;
|
|
883
|
+
}
|
|
884
|
+
function inferMimeTypeFromBuffer(buffer) {
|
|
885
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
886
|
+
return "image/png";
|
|
887
|
+
}
|
|
888
|
+
if (buffer.length >= 3 && buffer.subarray(0, 3).equals(Buffer.from([255, 216, 255]))) {
|
|
889
|
+
return "image/jpeg";
|
|
890
|
+
}
|
|
891
|
+
if (buffer.length >= 6) {
|
|
892
|
+
const header = buffer.subarray(0, 6).toString("ascii");
|
|
893
|
+
if (header === "GIF87a" || header === "GIF89a") return "image/gif";
|
|
894
|
+
}
|
|
895
|
+
if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
896
|
+
return "image/webp";
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
function normalizeExplicitMimeType(mimeType) {
|
|
901
|
+
const normalized = mimeType?.trim().toLowerCase();
|
|
902
|
+
if (!normalized) return null;
|
|
903
|
+
if (!MIME_TYPE_RE.test(normalized)) {
|
|
904
|
+
throw new AttachmentUploadArgError(
|
|
905
|
+
"INVALID_ARG",
|
|
906
|
+
`--mime-type must look like type/subtype, got: ${mimeType}`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
return normalized;
|
|
910
|
+
}
|
|
911
|
+
function inferUploadMimeType(filename, buffer, explicitMimeType) {
|
|
912
|
+
const explicit = normalizeExplicitMimeType(explicitMimeType);
|
|
913
|
+
return explicit || inferMimeTypeFromBuffer(buffer) || inferMimeTypeFromFilename(filename) || "application/octet-stream";
|
|
914
|
+
}
|
|
915
|
+
function registerAttachmentUploadCommand(parent) {
|
|
916
|
+
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(
|
|
917
|
+
"--channel <target>",
|
|
918
|
+
"Target where the attachment will be used: '#channel', 'dm:@peer', or thread variants. Required by the v0 server until channel-less uploads land."
|
|
919
|
+
).option("--mime-type <type>", "Explicit MIME type override, e.g. image/png").action(async (opts) => {
|
|
920
|
+
let ctx;
|
|
921
|
+
try {
|
|
922
|
+
ctx = loadAgentContext();
|
|
923
|
+
} catch (err) {
|
|
924
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
925
|
+
throw err;
|
|
926
|
+
}
|
|
927
|
+
if (!existsSync(opts.path)) {
|
|
928
|
+
fail("INVALID_ARG", `--path does not exist: ${opts.path}`);
|
|
929
|
+
}
|
|
930
|
+
const stat = statSync(opts.path);
|
|
931
|
+
if (!stat.isFile()) {
|
|
932
|
+
fail("INVALID_ARG", `--path is not a regular file: ${opts.path}`);
|
|
933
|
+
}
|
|
934
|
+
if (stat.size > MAX_BYTES) {
|
|
935
|
+
fail(
|
|
936
|
+
"INVALID_ARG",
|
|
937
|
+
`--path is ${stat.size} bytes; max upload size is ${MAX_BYTES} bytes (10MB)`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
if (!opts.channel) {
|
|
941
|
+
fail(
|
|
942
|
+
"MISSING_CHANNEL",
|
|
943
|
+
"v0 server requires a channel to attach the upload to. Pass --channel '#name', 'dm:@peer', or a thread target."
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
const client = new ApiClient(ctx);
|
|
947
|
+
const agentPath = `/internal/agent/${encodeURIComponent(ctx.agentId)}`;
|
|
948
|
+
const resolved = await client.request(
|
|
949
|
+
"POST",
|
|
950
|
+
`${agentPath}/resolve-channel`,
|
|
951
|
+
{ target: opts.channel }
|
|
952
|
+
);
|
|
953
|
+
if (!resolved.ok || !resolved.data?.channelId) {
|
|
954
|
+
const code = resolved.status >= 500 ? "SERVER_5XX" : "RESOLVE_FAILED";
|
|
955
|
+
fail(code, resolved.error ?? `Could not resolve channel: ${opts.channel}`);
|
|
956
|
+
}
|
|
957
|
+
const channelId = resolved.data.channelId;
|
|
958
|
+
const buffer = readFileSync(opts.path);
|
|
959
|
+
const filename = basename(opts.path);
|
|
960
|
+
let explicitMimeType;
|
|
961
|
+
try {
|
|
962
|
+
explicitMimeType = normalizeExplicitMimeType(opts.mimeType);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
if (err instanceof AttachmentUploadArgError) fail(err.code, err.message);
|
|
965
|
+
throw err;
|
|
966
|
+
}
|
|
967
|
+
const uploadMimeType = inferUploadMimeType(filename, buffer, explicitMimeType);
|
|
968
|
+
const blob = new Blob([buffer], { type: uploadMimeType });
|
|
969
|
+
const form = new FormData();
|
|
970
|
+
form.append("file", blob, filename);
|
|
971
|
+
form.append("channelId", channelId);
|
|
972
|
+
if (explicitMimeType) {
|
|
973
|
+
form.append("mimeType", explicitMimeType);
|
|
974
|
+
}
|
|
975
|
+
const res = await client.requestMultipart(
|
|
976
|
+
"POST",
|
|
977
|
+
`${agentPath}/upload`,
|
|
978
|
+
form
|
|
979
|
+
);
|
|
980
|
+
if (!res.ok) {
|
|
981
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "UPLOAD_FAILED";
|
|
982
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
983
|
+
}
|
|
984
|
+
const d = res.data;
|
|
985
|
+
process.stdout.write(`File uploaded: ${d.filename} (${(d.sizeBytes / 1024).toFixed(1)}KB)
|
|
986
|
+
Attachment ID: ${d.id}
|
|
987
|
+
|
|
988
|
+
Use this ID with slock message send --attachment-id ${d.id} to include it in a message.
|
|
989
|
+
`);
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/commands/attachment/view.ts
|
|
994
|
+
import { writeFileSync } from "fs";
|
|
995
|
+
function registerAttachmentViewCommand(parent) {
|
|
996
|
+
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) => {
|
|
997
|
+
let ctx;
|
|
998
|
+
try {
|
|
999
|
+
ctx = loadAgentContext();
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1002
|
+
throw err;
|
|
1003
|
+
}
|
|
1004
|
+
const client = new ApiClient(ctx);
|
|
1005
|
+
const res = await client.requestRaw(
|
|
1006
|
+
"GET",
|
|
1007
|
+
`/api/attachments/${encodeURIComponent(opts.id)}`
|
|
1008
|
+
);
|
|
1009
|
+
if (!res.ok) {
|
|
1010
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "VIEW_FAILED";
|
|
1011
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1012
|
+
}
|
|
1013
|
+
const buffer = Buffer.from(await res.response.arrayBuffer());
|
|
1014
|
+
writeFileSync(opts.output, buffer);
|
|
1015
|
+
process.stdout.write(`Downloaded to: ${opts.output}
|
|
1016
|
+
`);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/commands/task/_format.ts
|
|
1021
|
+
function formatTaskList(channel, data, statusFilter) {
|
|
1022
|
+
if (!data.tasks || data.tasks.length === 0) {
|
|
1023
|
+
return `No${statusFilter && statusFilter !== "all" ? ` ${statusFilter}` : ""} tasks in ${channel}.`;
|
|
1024
|
+
}
|
|
1025
|
+
const formatted = data.tasks.map((t) => {
|
|
1026
|
+
const assignee = t.claimedByName ? ` \u2192 @${t.claimedByName}` : "";
|
|
1027
|
+
const creator = t.createdByName ? ` (by @${t.createdByName})` : "";
|
|
1028
|
+
const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
|
|
1029
|
+
const legacy = t.isLegacy ? " [LEGACY \u2014 read-only]" : "";
|
|
1030
|
+
return `#${t.taskNumber} [${t.status}] ${t.title}${assignee}${creator}${msgId}${legacy}`;
|
|
1031
|
+
}).join("\n");
|
|
1032
|
+
return `## Task Board for ${channel} (${data.tasks.length} tasks)
|
|
1033
|
+
|
|
1034
|
+
${formatted}`;
|
|
1035
|
+
}
|
|
1036
|
+
function formatTasksCreated(channel, data) {
|
|
1037
|
+
const created = data.tasks.map((t) => `#${t.taskNumber} msg=${t.messageId.slice(0, 8)} "${t.title}"`).join("\n");
|
|
1038
|
+
const threadHints = data.tasks.map((t) => `#${t.taskNumber} \u2192 slock message send --target "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
|
|
1039
|
+
return `Created ${data.tasks.length} task(s) in ${channel}:
|
|
1040
|
+
${created}
|
|
1041
|
+
|
|
1042
|
+
To follow up in each task's thread:
|
|
1043
|
+
${threadHints}`;
|
|
1044
|
+
}
|
|
1045
|
+
function formatClaimResults(channel, data) {
|
|
1046
|
+
const lines = data.results.map((r) => {
|
|
1047
|
+
const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
|
|
1048
|
+
if (r.success) {
|
|
1049
|
+
const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
|
|
1050
|
+
return `${label} (msg:${msgShort}): claimed`;
|
|
1051
|
+
}
|
|
1052
|
+
return `${label}: FAILED \u2014 ${r.reason || "already claimed"}`;
|
|
1053
|
+
});
|
|
1054
|
+
const succeeded = data.results.filter((r) => r.success).length;
|
|
1055
|
+
const failed = data.results.length - succeeded;
|
|
1056
|
+
let summary = `${succeeded} claimed`;
|
|
1057
|
+
if (failed > 0) summary += `, ${failed} failed`;
|
|
1058
|
+
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");
|
|
1059
|
+
const threadHint = claimedMsgs ? `
|
|
1060
|
+
|
|
1061
|
+
Follow up in each task's thread:
|
|
1062
|
+
${claimedMsgs}` : "";
|
|
1063
|
+
return `Claim results (${summary}):
|
|
1064
|
+
${lines.join("\n")}${threadHint}`;
|
|
1065
|
+
}
|
|
1066
|
+
function formatTaskUnclaimed(taskNumber) {
|
|
1067
|
+
return `#${taskNumber} unclaimed \u2014 now open.`;
|
|
1068
|
+
}
|
|
1069
|
+
function formatTaskStatusUpdated(taskNumber, status) {
|
|
1070
|
+
return `#${taskNumber} moved to ${status}.`;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/commands/task/list.ts
|
|
1074
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["all", "todo", "in_progress", "in_review", "done"]);
|
|
1075
|
+
function registerTaskListCommand(parent) {
|
|
1076
|
+
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) => {
|
|
1077
|
+
let ctx;
|
|
1078
|
+
try {
|
|
1079
|
+
ctx = loadAgentContext();
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1082
|
+
throw err;
|
|
1083
|
+
}
|
|
1084
|
+
if (opts.status && !VALID_STATUSES.has(opts.status)) {
|
|
1085
|
+
fail("INVALID_ARG", `--status must be one of ${Array.from(VALID_STATUSES).join("|")}; got ${opts.status}`);
|
|
1086
|
+
}
|
|
1087
|
+
const params = new URLSearchParams();
|
|
1088
|
+
params.set("channel", opts.channel);
|
|
1089
|
+
if (opts.status) params.set("status", opts.status);
|
|
1090
|
+
const client = new ApiClient(ctx);
|
|
1091
|
+
const res = await client.request(
|
|
1092
|
+
"GET",
|
|
1093
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks?${params.toString()}`
|
|
1094
|
+
);
|
|
1095
|
+
if (!res.ok) {
|
|
1096
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "LIST_FAILED";
|
|
1097
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1098
|
+
}
|
|
1099
|
+
process.stdout.write(formatTaskList(opts.channel, res.data, opts.status) + "\n");
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/commands/task/create.ts
|
|
1104
|
+
function registerTaskCreateCommand(parent) {
|
|
1105
|
+
parent.command("create").description("Create one or more tasks in a channel").requiredOption("--channel <target>", "Channel target: '#channel'").requiredOption(
|
|
1106
|
+
"--title <title>",
|
|
1107
|
+
"Task title (repeatable for batch create)",
|
|
1108
|
+
(value, prev = []) => prev.concat(value)
|
|
1109
|
+
).action(async (opts) => {
|
|
1110
|
+
let ctx;
|
|
1111
|
+
try {
|
|
1112
|
+
ctx = loadAgentContext();
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1115
|
+
throw err;
|
|
1116
|
+
}
|
|
1117
|
+
const titles = opts.title ?? [];
|
|
1118
|
+
if (titles.length === 0) fail("INVALID_ARG", "--title is required (at least one)");
|
|
1119
|
+
const client = new ApiClient(ctx);
|
|
1120
|
+
const res = await client.request(
|
|
1121
|
+
"POST",
|
|
1122
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks`,
|
|
1123
|
+
{ channel: opts.channel, tasks: titles.map((title) => ({ title })) }
|
|
1124
|
+
);
|
|
1125
|
+
if (!res.ok) {
|
|
1126
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "CREATE_FAILED";
|
|
1127
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1128
|
+
}
|
|
1129
|
+
process.stdout.write(formatTasksCreated(opts.channel, res.data) + "\n");
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/commands/task/claim.ts
|
|
1134
|
+
function registerTaskClaimCommand(parent) {
|
|
1135
|
+
parent.command("claim").description("Claim one or more tasks (by task number or message id)").requiredOption("--channel <target>", "Channel target: '#channel'").option(
|
|
1136
|
+
"--number <n>",
|
|
1137
|
+
"Task number to claim (repeatable)",
|
|
1138
|
+
(value, prev = []) => prev.concat(value)
|
|
1139
|
+
).option(
|
|
1140
|
+
"--message-id <id>",
|
|
1141
|
+
"Message id (full or short) to claim (repeatable)",
|
|
1142
|
+
(value, prev = []) => prev.concat(value)
|
|
1143
|
+
).action(async (opts) => {
|
|
1144
|
+
let ctx;
|
|
1145
|
+
try {
|
|
1146
|
+
ctx = loadAgentContext();
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1149
|
+
throw err;
|
|
1150
|
+
}
|
|
1151
|
+
const numbers = (opts.number ?? []).map((raw) => {
|
|
1152
|
+
const n = Number(raw);
|
|
1153
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
1154
|
+
fail("INVALID_ARG", `--number must be a positive integer; got ${raw}`);
|
|
1155
|
+
}
|
|
1156
|
+
return n;
|
|
1157
|
+
});
|
|
1158
|
+
const messageIds = opts.messageId ?? [];
|
|
1159
|
+
if (numbers.length === 0 && messageIds.length === 0) {
|
|
1160
|
+
fail("INVALID_ARG", "Provide at least one --number or --message-id");
|
|
1161
|
+
}
|
|
1162
|
+
const body = { channel: opts.channel };
|
|
1163
|
+
if (numbers.length > 0) body.task_numbers = numbers;
|
|
1164
|
+
if (messageIds.length > 0) body.message_ids = messageIds;
|
|
1165
|
+
const client = new ApiClient(ctx);
|
|
1166
|
+
const res = await client.request(
|
|
1167
|
+
"POST",
|
|
1168
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks/claim`,
|
|
1169
|
+
body
|
|
1170
|
+
);
|
|
1171
|
+
if (!res.ok) {
|
|
1172
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "CLAIM_FAILED";
|
|
1173
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1174
|
+
}
|
|
1175
|
+
process.stdout.write(formatClaimResults(opts.channel, res.data) + "\n");
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/commands/task/unclaim.ts
|
|
1180
|
+
function registerTaskUnclaimCommand(parent) {
|
|
1181
|
+
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) => {
|
|
1182
|
+
let ctx;
|
|
1183
|
+
try {
|
|
1184
|
+
ctx = loadAgentContext();
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1187
|
+
throw err;
|
|
1188
|
+
}
|
|
1189
|
+
const n = Number(opts.number);
|
|
1190
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
1191
|
+
fail("INVALID_ARG", `--number must be a positive integer; got ${opts.number}`);
|
|
1192
|
+
}
|
|
1193
|
+
const client = new ApiClient(ctx);
|
|
1194
|
+
const res = await client.request(
|
|
1195
|
+
"POST",
|
|
1196
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks/unclaim`,
|
|
1197
|
+
{ channel: opts.channel, task_number: n }
|
|
1198
|
+
);
|
|
1199
|
+
if (!res.ok) {
|
|
1200
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "UNCLAIM_FAILED";
|
|
1201
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1202
|
+
}
|
|
1203
|
+
process.stdout.write(formatTaskUnclaimed(n) + "\n");
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// src/commands/task/update.ts
|
|
1208
|
+
var STATUSES = ["todo", "in_progress", "in_review", "done"];
|
|
1209
|
+
function registerTaskUpdateCommand(parent) {
|
|
1210
|
+
parent.command("update").description("Update task status").requiredOption("--channel <target>", "Channel target: '#channel'").requiredOption("--number <n>", "Task number to update").requiredOption(
|
|
1211
|
+
"--status <status>",
|
|
1212
|
+
`New status. One of: ${STATUSES.join(", ")}`
|
|
1213
|
+
).action(async (opts) => {
|
|
1214
|
+
let ctx;
|
|
1215
|
+
try {
|
|
1216
|
+
ctx = loadAgentContext();
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1219
|
+
throw err;
|
|
1220
|
+
}
|
|
1221
|
+
const n = Number(opts.number);
|
|
1222
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
1223
|
+
fail("INVALID_ARG", `--number must be a positive integer; got ${opts.number}`);
|
|
1224
|
+
}
|
|
1225
|
+
if (!STATUSES.includes(opts.status)) {
|
|
1226
|
+
fail(
|
|
1227
|
+
"INVALID_ARG",
|
|
1228
|
+
`--status must be one of: ${STATUSES.join(", ")}; got ${opts.status}`
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
const client = new ApiClient(ctx);
|
|
1232
|
+
const res = await client.request(
|
|
1233
|
+
"POST",
|
|
1234
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/tasks/update-status`,
|
|
1235
|
+
{ channel: opts.channel, task_number: n, status: opts.status }
|
|
1236
|
+
);
|
|
1237
|
+
if (!res.ok) {
|
|
1238
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "UPDATE_FAILED";
|
|
1239
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1240
|
+
}
|
|
1241
|
+
process.stdout.write(formatTaskStatusUpdated(n, opts.status) + "\n");
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// ../shared/src/tracing/index.ts
|
|
1246
|
+
var DEFAULT_TRACE_FLAGS = "00";
|
|
1247
|
+
var TRACE_ID_HEX_LENGTH = 32;
|
|
1248
|
+
var SPAN_ID_HEX_LENGTH = 16;
|
|
1249
|
+
var TRACE_FLAGS_HEX_LENGTH = 2;
|
|
1250
|
+
var TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
1251
|
+
var SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;
|
|
1252
|
+
var TRACE_FLAGS_PATTERN = /^[0-9a-f]{2}$/;
|
|
1253
|
+
function isTraceId(value) {
|
|
1254
|
+
return TRACE_ID_PATTERN.test(value) && value !== "0".repeat(TRACE_ID_HEX_LENGTH);
|
|
1255
|
+
}
|
|
1256
|
+
function isSpanId(value) {
|
|
1257
|
+
return SPAN_ID_PATTERN.test(value) && value !== "0".repeat(SPAN_ID_HEX_LENGTH);
|
|
1258
|
+
}
|
|
1259
|
+
function isTraceFlags(value) {
|
|
1260
|
+
return TRACE_FLAGS_PATTERN.test(value);
|
|
1261
|
+
}
|
|
1262
|
+
function assertTraceContext(context) {
|
|
1263
|
+
if (!isTraceId(context.traceId)) {
|
|
1264
|
+
throw new Error(`Invalid traceId: expected ${TRACE_ID_HEX_LENGTH} lowercase hex chars`);
|
|
1265
|
+
}
|
|
1266
|
+
if (!isSpanId(context.spanId)) {
|
|
1267
|
+
throw new Error(`Invalid spanId: expected ${SPAN_ID_HEX_LENGTH} lowercase hex chars`);
|
|
1268
|
+
}
|
|
1269
|
+
if (context.parentSpanId !== null && !isSpanId(context.parentSpanId)) {
|
|
1270
|
+
throw new Error(`Invalid parentSpanId: expected null or ${SPAN_ID_HEX_LENGTH} lowercase hex chars`);
|
|
1271
|
+
}
|
|
1272
|
+
if (!isTraceFlags(context.traceFlags)) {
|
|
1273
|
+
throw new Error(`Invalid traceFlags: expected ${TRACE_FLAGS_HEX_LENGTH} lowercase hex chars`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function createTraceContext({
|
|
1277
|
+
parent = null,
|
|
1278
|
+
traceId,
|
|
1279
|
+
spanId,
|
|
1280
|
+
traceFlags,
|
|
1281
|
+
traceIdGenerator = generateTraceId,
|
|
1282
|
+
spanIdGenerator = generateSpanId
|
|
1283
|
+
} = {}) {
|
|
1284
|
+
const context = {
|
|
1285
|
+
traceId: traceId ?? parent?.traceId ?? traceIdGenerator(),
|
|
1286
|
+
spanId: spanId ?? spanIdGenerator(),
|
|
1287
|
+
parentSpanId: parent?.spanId ?? null,
|
|
1288
|
+
traceFlags: traceFlags ?? parent?.traceFlags ?? DEFAULT_TRACE_FLAGS
|
|
1289
|
+
};
|
|
1290
|
+
assertTraceContext(context);
|
|
1291
|
+
return context;
|
|
1292
|
+
}
|
|
1293
|
+
var NoopTracer = class {
|
|
1294
|
+
startSpan(_name, options) {
|
|
1295
|
+
return new NoopActiveSpan(createTraceContext({ parent: options.parent ?? null }));
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
var NoopActiveSpan = class {
|
|
1299
|
+
context;
|
|
1300
|
+
constructor(context) {
|
|
1301
|
+
this.context = context;
|
|
1302
|
+
}
|
|
1303
|
+
addEvent() {
|
|
1304
|
+
}
|
|
1305
|
+
end() {
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
var noopTracer = new NoopTracer();
|
|
1309
|
+
function generateTraceId() {
|
|
1310
|
+
return randomNonZeroHex(TRACE_ID_HEX_LENGTH);
|
|
1311
|
+
}
|
|
1312
|
+
function generateSpanId() {
|
|
1313
|
+
return randomNonZeroHex(SPAN_ID_HEX_LENGTH);
|
|
1314
|
+
}
|
|
1315
|
+
function randomNonZeroHex(length) {
|
|
1316
|
+
let value = randomHex(length);
|
|
1317
|
+
while (value === "0".repeat(length)) {
|
|
1318
|
+
value = randomHex(length);
|
|
1319
|
+
}
|
|
1320
|
+
return value;
|
|
1321
|
+
}
|
|
1322
|
+
function randomHex(length) {
|
|
1323
|
+
const bytes = new Uint8Array(length / 2);
|
|
1324
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
1325
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ../shared/src/testing/failpoints.ts
|
|
1329
|
+
var NoopFailpointRegistry = class {
|
|
1330
|
+
get enabled() {
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
1333
|
+
isEnabled() {
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
configure() {
|
|
1337
|
+
}
|
|
1338
|
+
clear() {
|
|
1339
|
+
}
|
|
1340
|
+
getTrace() {
|
|
1341
|
+
return [];
|
|
1342
|
+
}
|
|
1343
|
+
hit(_key, _context, fallback) {
|
|
1344
|
+
return fallback ? fallback() : void 0;
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
var noopFailpointRegistry = new NoopFailpointRegistry();
|
|
1348
|
+
|
|
1349
|
+
// ../shared/src/serverPermissions.ts
|
|
1350
|
+
var EMPTY_SERVER_CAPABILITIES = Object.freeze({
|
|
1351
|
+
manageServer: false,
|
|
1352
|
+
manageChannels: false,
|
|
1353
|
+
manageAgents: false,
|
|
1354
|
+
manageMachines: false,
|
|
1355
|
+
manageMembers: false,
|
|
1356
|
+
changeMemberRoles: false,
|
|
1357
|
+
manageBilling: false,
|
|
1358
|
+
joinPublicChannels: false
|
|
1359
|
+
});
|
|
1360
|
+
var SERVER_CAPABILITY_MATRIX = {
|
|
1361
|
+
owner: Object.freeze({
|
|
1362
|
+
manageServer: true,
|
|
1363
|
+
manageChannels: true,
|
|
1364
|
+
manageAgents: true,
|
|
1365
|
+
manageMachines: true,
|
|
1366
|
+
manageMembers: true,
|
|
1367
|
+
changeMemberRoles: true,
|
|
1368
|
+
manageBilling: true,
|
|
1369
|
+
joinPublicChannels: true
|
|
1370
|
+
}),
|
|
1371
|
+
admin: Object.freeze({
|
|
1372
|
+
manageServer: true,
|
|
1373
|
+
manageChannels: true,
|
|
1374
|
+
manageAgents: true,
|
|
1375
|
+
manageMachines: true,
|
|
1376
|
+
manageMembers: true,
|
|
1377
|
+
changeMemberRoles: true,
|
|
1378
|
+
manageBilling: false,
|
|
1379
|
+
joinPublicChannels: true
|
|
1380
|
+
}),
|
|
1381
|
+
member: Object.freeze({
|
|
1382
|
+
manageServer: false,
|
|
1383
|
+
manageChannels: false,
|
|
1384
|
+
manageAgents: false,
|
|
1385
|
+
manageMachines: false,
|
|
1386
|
+
manageMembers: false,
|
|
1387
|
+
changeMemberRoles: false,
|
|
1388
|
+
manageBilling: false,
|
|
1389
|
+
joinPublicChannels: true
|
|
1390
|
+
})
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
// ../shared/src/index.ts
|
|
1394
|
+
var RUNTIMES = [
|
|
1395
|
+
{ id: "claude", displayName: "Claude Code", binary: "claude", supported: true },
|
|
1396
|
+
{ id: "codex", displayName: "Codex CLI", binary: "codex", supported: true },
|
|
1397
|
+
{ id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
|
|
1398
|
+
{ id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
|
|
1399
|
+
{ id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
|
|
1400
|
+
{ id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true }
|
|
1401
|
+
];
|
|
1402
|
+
function getRuntimeDisplayName(id) {
|
|
1403
|
+
return RUNTIMES.find((r) => r.id === id)?.displayName ?? id;
|
|
1404
|
+
}
|
|
1405
|
+
var PLAN_CONFIG = {
|
|
1406
|
+
free: {
|
|
1407
|
+
displayName: "Hobby",
|
|
1408
|
+
limits: { maxMachines: 2, maxAgents: 5, maxChannels: 5, messageHistoryDays: 30, includedAgents: 5 },
|
|
1409
|
+
comingSoon: false,
|
|
1410
|
+
price: 0,
|
|
1411
|
+
extraAgentPrice: 0
|
|
1412
|
+
},
|
|
1413
|
+
founder: {
|
|
1414
|
+
displayName: "Founder",
|
|
1415
|
+
limits: { maxMachines: -1, maxAgents: -1, maxChannels: -1, messageHistoryDays: -1, includedAgents: -1 },
|
|
1416
|
+
comingSoon: false,
|
|
1417
|
+
price: 0,
|
|
1418
|
+
extraAgentPrice: 0
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
var DISPLAY_PLAN_CONFIG = {
|
|
1422
|
+
free: PLAN_CONFIG.free,
|
|
1423
|
+
pro: {
|
|
1424
|
+
displayName: "Team",
|
|
1425
|
+
limits: { maxMachines: 8, maxAgents: 40, maxChannels: 20, messageHistoryDays: -1, includedAgents: 40 },
|
|
1426
|
+
comingSoon: true,
|
|
1427
|
+
price: 20,
|
|
1428
|
+
extraAgentPrice: 0
|
|
1429
|
+
},
|
|
1430
|
+
max: {
|
|
1431
|
+
displayName: "Business",
|
|
1432
|
+
limits: { maxMachines: 40, maxAgents: 200, maxChannels: -1, messageHistoryDays: -1, includedAgents: 200 },
|
|
1433
|
+
comingSoon: true,
|
|
1434
|
+
price: 200,
|
|
1435
|
+
extraAgentPrice: 0
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/commands/profile/_format.ts
|
|
1440
|
+
function formatCreatedAgents(createdAgents) {
|
|
1441
|
+
if (createdAgents.length === 0) {
|
|
1442
|
+
return ["- Created Agents: none"];
|
|
1443
|
+
}
|
|
1444
|
+
return [
|
|
1445
|
+
`- Created Agents (${createdAgents.length}):`,
|
|
1446
|
+
...createdAgents.map((createdAgent) => ` - @${createdAgent.name} (${getRuntimeDisplayName(createdAgent.runtime)}, ${createdAgent.status})`)
|
|
1447
|
+
];
|
|
1448
|
+
}
|
|
1449
|
+
function formatHumanProfile(profile) {
|
|
1450
|
+
const lines = [
|
|
1451
|
+
"## Profile",
|
|
1452
|
+
"",
|
|
1453
|
+
"- Type: human",
|
|
1454
|
+
`- Handle: @${profile.name}`,
|
|
1455
|
+
`- Display Name: ${profile.displayName ?? "(none)"}`,
|
|
1456
|
+
`- Description: ${profile.description ?? "(none)"}`,
|
|
1457
|
+
`- Membership: ${profile.membershipStatus}`
|
|
1458
|
+
];
|
|
1459
|
+
if (profile.role) lines.push(`- Role: ${profile.role}`);
|
|
1460
|
+
if (profile.joinedAt) lines.push(`- Joined: ${profile.joinedAt}`);
|
|
1461
|
+
if (profile.email) lines.push(`- Email: ${profile.email}`);
|
|
1462
|
+
return [...lines, ...formatCreatedAgents(profile.createdAgents)].join("\n");
|
|
1463
|
+
}
|
|
1464
|
+
function formatCreator(profile) {
|
|
1465
|
+
if (!profile.creator) return null;
|
|
1466
|
+
return profile.creator.displayName ? `${profile.creator.displayName} (@${profile.creator.name})` : `@${profile.creator.name}`;
|
|
1467
|
+
}
|
|
1468
|
+
function formatAgentProfile(profile) {
|
|
1469
|
+
const lines = [
|
|
1470
|
+
"## Profile",
|
|
1471
|
+
"",
|
|
1472
|
+
"- Type: agent",
|
|
1473
|
+
`- Handle: @${profile.name}`,
|
|
1474
|
+
`- Display Name: ${profile.displayName ?? "(none)"}`,
|
|
1475
|
+
`- Description: ${profile.description ?? "(none)"}`,
|
|
1476
|
+
`- Status: ${profile.status}`,
|
|
1477
|
+
`- Runtime: ${getRuntimeDisplayName(profile.runtime)}`,
|
|
1478
|
+
`- Model: ${profile.model}`,
|
|
1479
|
+
`- Reasoning: ${profile.reasoningEffort ?? "medium"}`
|
|
1480
|
+
];
|
|
1481
|
+
if (profile.executionMode) lines.push(`- Execution: ${profile.executionMode}`);
|
|
1482
|
+
if (profile.computerName || profile.computerId) {
|
|
1483
|
+
const label = profile.computerName && profile.computerId ? `${profile.computerName} (${profile.computerId})` : profile.computerName ?? profile.computerId;
|
|
1484
|
+
lines.push(`- Computer: ${label}`);
|
|
1485
|
+
}
|
|
1486
|
+
if (profile.computerHostname) lines.push(`- Hostname: ${profile.computerHostname}`);
|
|
1487
|
+
if (profile.daemonVersion) lines.push(`- Daemon: v${profile.daemonVersion}`);
|
|
1488
|
+
lines.push(`- Created: ${profile.createdAt}`);
|
|
1489
|
+
if (profile.deletedAt) lines.push(`- Deleted At: ${profile.deletedAt}`);
|
|
1490
|
+
const creator = formatCreator(profile);
|
|
1491
|
+
if (creator) lines.push(`- Creator: ${creator}`);
|
|
1492
|
+
return [...lines, ...formatCreatedAgents(profile.createdAgents)].join("\n");
|
|
1493
|
+
}
|
|
1494
|
+
function formatProfile(profile) {
|
|
1495
|
+
return profile.kind === "human" ? formatHumanProfile(profile) : formatAgentProfile(profile);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// src/commands/profile/show.ts
|
|
1499
|
+
function normalizeTarget(target) {
|
|
1500
|
+
if (target === void 0) return null;
|
|
1501
|
+
const trimmed = target.trim();
|
|
1502
|
+
if (!trimmed) {
|
|
1503
|
+
fail("INVALID_ARG", "profile target must not be empty");
|
|
1504
|
+
}
|
|
1505
|
+
if (!trimmed.startsWith("@")) {
|
|
1506
|
+
fail("INVALID_ARG", "profile target must start with @");
|
|
1507
|
+
}
|
|
1508
|
+
return trimmed;
|
|
1509
|
+
}
|
|
1510
|
+
function registerProfileShowCommand(parent) {
|
|
1511
|
+
parent.command("show").description("Show a profile. Omit the target to show your own profile.").argument("[target]", "Handle like @alice; omit to show your own profile").option("--json", "Emit machine-readable JSON").action(async (target, opts) => {
|
|
1512
|
+
let ctx;
|
|
1513
|
+
try {
|
|
1514
|
+
ctx = loadAgentContext();
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1517
|
+
throw err;
|
|
1518
|
+
}
|
|
1519
|
+
const normalizedTarget = normalizeTarget(target);
|
|
1520
|
+
const params = new URLSearchParams();
|
|
1521
|
+
if (normalizedTarget) params.set("target", normalizedTarget);
|
|
1522
|
+
const client = new ApiClient(ctx);
|
|
1523
|
+
const pathname = params.size > 0 ? `/internal/agent/${encodeURIComponent(ctx.agentId)}/profile?${params.toString()}` : `/internal/agent/${encodeURIComponent(ctx.agentId)}/profile`;
|
|
1524
|
+
const res = await client.request("GET", pathname);
|
|
1525
|
+
if (!res.ok || !res.data) {
|
|
1526
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "PROFILE_SHOW_FAILED";
|
|
1527
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1528
|
+
}
|
|
1529
|
+
if (opts.json) {
|
|
1530
|
+
emit({ ok: true, data: res.data });
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
process.stdout.write(`${formatProfile(res.data)}
|
|
1534
|
+
`);
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/commands/profile/update.ts
|
|
1539
|
+
import { basename as basename2 } from "path";
|
|
1540
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
1541
|
+
var MAX_PROFILE_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
1542
|
+
var PROFILE_AVATAR_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
1543
|
+
"image/jpeg",
|
|
1544
|
+
"image/png",
|
|
1545
|
+
"image/gif",
|
|
1546
|
+
"image/webp"
|
|
1547
|
+
]);
|
|
1548
|
+
var FILENAME_MIME_MAP2 = {
|
|
1549
|
+
".jpg": "image/jpeg",
|
|
1550
|
+
".jpeg": "image/jpeg",
|
|
1551
|
+
".png": "image/png",
|
|
1552
|
+
".gif": "image/gif",
|
|
1553
|
+
".webp": "image/webp"
|
|
1554
|
+
};
|
|
1555
|
+
var MAX_PROFILE_DESCRIPTION_LENGTH = 3e3;
|
|
1556
|
+
var MAX_PROFILE_DISPLAY_NAME_LENGTH = 80;
|
|
1557
|
+
function inferImageMimeType(filename, buffer) {
|
|
1558
|
+
const lowerFilename = filename.toLowerCase();
|
|
1559
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
1560
|
+
return "image/png";
|
|
1561
|
+
}
|
|
1562
|
+
if (buffer.length >= 3 && buffer.subarray(0, 3).equals(Buffer.from([255, 216, 255]))) {
|
|
1563
|
+
return "image/jpeg";
|
|
1564
|
+
}
|
|
1565
|
+
if (buffer.length >= 6) {
|
|
1566
|
+
const header = buffer.subarray(0, 6).toString("ascii");
|
|
1567
|
+
if (header === "GIF87a" || header === "GIF89a") return "image/gif";
|
|
1568
|
+
}
|
|
1569
|
+
if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
1570
|
+
return "image/webp";
|
|
1571
|
+
}
|
|
1572
|
+
const dot = lowerFilename.lastIndexOf(".");
|
|
1573
|
+
return dot >= 0 ? FILENAME_MIME_MAP2[lowerFilename.slice(dot)] ?? null : null;
|
|
1574
|
+
}
|
|
1575
|
+
function readAvatarFile(avatarFile) {
|
|
1576
|
+
if (!existsSync2(avatarFile)) {
|
|
1577
|
+
fail("PROFILE_FILE_NOT_FOUND", `Avatar file does not exist: ${avatarFile}`);
|
|
1578
|
+
}
|
|
1579
|
+
const stat = statSync2(avatarFile);
|
|
1580
|
+
if (!stat.isFile()) {
|
|
1581
|
+
fail("PROFILE_FILE_NOT_FOUND", `Avatar file is not a regular file: ${avatarFile}`);
|
|
1582
|
+
}
|
|
1583
|
+
if (stat.size > MAX_PROFILE_AVATAR_BYTES) {
|
|
1584
|
+
fail(
|
|
1585
|
+
"PROFILE_AVATAR_TOO_LARGE",
|
|
1586
|
+
`Avatar file is ${stat.size} bytes; max size is ${MAX_PROFILE_AVATAR_BYTES} bytes`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const buffer = readFileSync2(avatarFile);
|
|
1590
|
+
const filename = basename2(avatarFile);
|
|
1591
|
+
const mimeType = inferImageMimeType(filename, buffer);
|
|
1592
|
+
if (!mimeType || !PROFILE_AVATAR_MIME_TYPES.has(mimeType)) {
|
|
1593
|
+
fail(
|
|
1594
|
+
"PROFILE_AVATAR_BAD_FORMAT",
|
|
1595
|
+
"Avatar must be a JPEG, PNG, GIF, or WebP image"
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
return { filename, buffer, mimeType };
|
|
1599
|
+
}
|
|
1600
|
+
function registerProfileUpdateCommand(parent) {
|
|
1601
|
+
parent.command("update").description("Update your own profile").option("--avatar-file <path>", "Path to a local image file to use as your avatar").option("--display-name <name>", "Set your display name (non-empty)").option("--description <text>", "Set your profile description (non-empty)").option("--json", "Emit machine-readable JSON").action(async (opts) => {
|
|
1602
|
+
let ctx;
|
|
1603
|
+
try {
|
|
1604
|
+
ctx = loadAgentContext();
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1607
|
+
throw err;
|
|
1608
|
+
}
|
|
1609
|
+
const hasAvatar = opts.avatarFile !== void 0;
|
|
1610
|
+
const hasDisplayName = opts.displayName !== void 0;
|
|
1611
|
+
const hasDescription = opts.description !== void 0;
|
|
1612
|
+
if (!hasAvatar && !hasDisplayName && !hasDescription) {
|
|
1613
|
+
fail("INVALID_ARG", "Provide at least one of --avatar-file, --display-name, or --description");
|
|
1614
|
+
}
|
|
1615
|
+
let trimmedDisplayName;
|
|
1616
|
+
if (hasDisplayName) {
|
|
1617
|
+
trimmedDisplayName = opts.displayName.trim();
|
|
1618
|
+
if (trimmedDisplayName.length === 0) {
|
|
1619
|
+
fail("INVALID_ARG", "--display-name must not be empty");
|
|
1620
|
+
}
|
|
1621
|
+
if (trimmedDisplayName.length > MAX_PROFILE_DISPLAY_NAME_LENGTH) {
|
|
1622
|
+
fail("INVALID_ARG", `--display-name must be at most ${MAX_PROFILE_DISPLAY_NAME_LENGTH} characters`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (hasDescription) {
|
|
1626
|
+
if (opts.description.length === 0) {
|
|
1627
|
+
fail("INVALID_ARG", "--description must not be empty");
|
|
1628
|
+
}
|
|
1629
|
+
if (opts.description.length > MAX_PROFILE_DESCRIPTION_LENGTH) {
|
|
1630
|
+
fail("INVALID_ARG", `--description must be at most ${MAX_PROFILE_DESCRIPTION_LENGTH} characters`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const client = new ApiClient(ctx);
|
|
1634
|
+
let latestProfile = null;
|
|
1635
|
+
if (hasDisplayName || hasDescription) {
|
|
1636
|
+
const body = {};
|
|
1637
|
+
if (hasDisplayName) {
|
|
1638
|
+
body.displayName = trimmedDisplayName;
|
|
1639
|
+
}
|
|
1640
|
+
if (hasDescription) {
|
|
1641
|
+
body.description = opts.description;
|
|
1642
|
+
}
|
|
1643
|
+
const res = await client.request(
|
|
1644
|
+
"POST",
|
|
1645
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/profile`,
|
|
1646
|
+
body
|
|
1647
|
+
);
|
|
1648
|
+
if (!res.ok || !res.data) {
|
|
1649
|
+
const code = res.errorCode ?? (res.status >= 500 ? "SERVER_5XX" : "PROFILE_UPDATE_FAILED");
|
|
1650
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1651
|
+
}
|
|
1652
|
+
latestProfile = res.data;
|
|
1653
|
+
}
|
|
1654
|
+
if (hasAvatar) {
|
|
1655
|
+
const avatar = readAvatarFile(opts.avatarFile);
|
|
1656
|
+
const form = new FormData();
|
|
1657
|
+
const avatarBytes = Uint8Array.from(avatar.buffer);
|
|
1658
|
+
form.append("avatar", new Blob([avatarBytes], { type: avatar.mimeType }), avatar.filename);
|
|
1659
|
+
const res = await client.requestMultipart(
|
|
1660
|
+
"POST",
|
|
1661
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/profile/avatar`,
|
|
1662
|
+
form
|
|
1663
|
+
);
|
|
1664
|
+
if (!res.ok || !res.data) {
|
|
1665
|
+
const code = res.errorCode ?? (res.status >= 500 ? "SERVER_5XX" : "PROFILE_UPDATE_FAILED");
|
|
1666
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1667
|
+
}
|
|
1668
|
+
latestProfile = res.data;
|
|
1669
|
+
}
|
|
1670
|
+
if (!latestProfile) {
|
|
1671
|
+
fail("PROFILE_UPDATE_FAILED", "No profile returned from server");
|
|
1672
|
+
}
|
|
1673
|
+
if (opts.json) {
|
|
1674
|
+
emit({ ok: true, data: latestProfile });
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
process.stdout.write(`${formatProfile(latestProfile)}
|
|
1678
|
+
`);
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/commands/reminder/_format.ts
|
|
1683
|
+
function toLocalTime2(iso) {
|
|
1684
|
+
const d = new Date(iso);
|
|
1685
|
+
if (isNaN(d.getTime())) return iso;
|
|
1686
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1687
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1688
|
+
}
|
|
1689
|
+
function formatReminder(r) {
|
|
1690
|
+
const fireLocal = toLocalTime2(r.fireAt);
|
|
1691
|
+
const ref = r.msgRef ? ` ref=${r.msgRef}` : "";
|
|
1692
|
+
const repeat = r.recurrence ? ` repeat=${r.recurrence.description}` : "";
|
|
1693
|
+
return `#${r.reminderId.slice(0, 8)} [${r.status}] fires=${fireLocal} "${r.title}"${ref}${repeat}`;
|
|
1694
|
+
}
|
|
1695
|
+
function formatReminderScheduled(r, warning) {
|
|
1696
|
+
const lines = [`Reminder scheduled: ${formatReminder(r)}`];
|
|
1697
|
+
if (warning) lines.push(`Warning: ${warning}`);
|
|
1698
|
+
return lines.join("\n");
|
|
1699
|
+
}
|
|
1700
|
+
function formatReminderList(reminders) {
|
|
1701
|
+
if (reminders.length === 0) return "No reminders.";
|
|
1702
|
+
return reminders.map(formatReminder).join("\n");
|
|
1703
|
+
}
|
|
1704
|
+
function formatReminderCanceled(r) {
|
|
1705
|
+
return `Reminder canceled: ${formatReminder(r)}`;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/commands/reminder/schedule.ts
|
|
1709
|
+
function buildScheduleBody(opts, now = () => Intl.DateTimeFormat().resolvedOptions().timeZone) {
|
|
1710
|
+
if (!opts.delaySeconds && !opts.fireAt && !opts.repeat) {
|
|
1711
|
+
return {
|
|
1712
|
+
body: {},
|
|
1713
|
+
error: { code: "INVALID_ARG", message: "Provide --delay-seconds, --fire-at, or --repeat" }
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
if (opts.delaySeconds && opts.fireAt) {
|
|
1717
|
+
return {
|
|
1718
|
+
body: {},
|
|
1719
|
+
error: {
|
|
1720
|
+
code: "INVALID_ARG",
|
|
1721
|
+
message: "Pass either --delay-seconds or --fire-at, not both"
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
const body = { title: opts.title, msgId: opts.msgId ?? null };
|
|
1726
|
+
if (opts.delaySeconds !== void 0) {
|
|
1727
|
+
const n = Number(opts.delaySeconds);
|
|
1728
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
1729
|
+
return {
|
|
1730
|
+
body: {},
|
|
1731
|
+
error: {
|
|
1732
|
+
code: "INVALID_ARG",
|
|
1733
|
+
message: `--delay-seconds must be a positive integer; got ${opts.delaySeconds}`
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
body.delaySeconds = n;
|
|
1738
|
+
}
|
|
1739
|
+
if (opts.fireAt !== void 0) body.fireAt = opts.fireAt;
|
|
1740
|
+
if (opts.repeat !== void 0) {
|
|
1741
|
+
body.repeat = opts.repeat;
|
|
1742
|
+
body.tz = now();
|
|
1743
|
+
}
|
|
1744
|
+
if (opts.channel !== void 0) body.channel = opts.channel;
|
|
1745
|
+
if (body.msgId == null) {
|
|
1746
|
+
return {
|
|
1747
|
+
body: {},
|
|
1748
|
+
error: {
|
|
1749
|
+
code: "INVALID_ARG",
|
|
1750
|
+
message: "Reminder create requires an anchor msgId; resolve a message first and pass --msg-id"
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
return { body };
|
|
1755
|
+
}
|
|
1756
|
+
function registerReminderScheduleCommand(parent) {
|
|
1757
|
+
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(
|
|
1758
|
+
"--delay-seconds <n>",
|
|
1759
|
+
"Preferred for relative times. Fires this many seconds from now (server-computed, timezone-safe)"
|
|
1760
|
+
).option(
|
|
1761
|
+
"--fire-at <iso>",
|
|
1762
|
+
"ISO-8601 UTC timestamp, e.g. 2026-04-21T09:00:00Z. Use only for absolute calendar times"
|
|
1763
|
+
).option(
|
|
1764
|
+
"--repeat <rule>",
|
|
1765
|
+
"Recurrence rule: every:15m | every:2h | every:1d | daily@09:00 | weekly:mon,fri@09:00"
|
|
1766
|
+
).option(
|
|
1767
|
+
"--channel <ref>",
|
|
1768
|
+
"Optional channel to post a receipt message in (e.g. #general, dm:@alice)."
|
|
1769
|
+
).requiredOption(
|
|
1770
|
+
"--msg-id <id>",
|
|
1771
|
+
"Message id this reminder is anchored to. Required for agent-created reminders."
|
|
1772
|
+
).action(async (opts) => {
|
|
1773
|
+
let ctx;
|
|
1774
|
+
try {
|
|
1775
|
+
ctx = loadAgentContext();
|
|
1776
|
+
} catch (err) {
|
|
1777
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1778
|
+
throw err;
|
|
1779
|
+
}
|
|
1780
|
+
const built = buildScheduleBody(opts);
|
|
1781
|
+
if (built.error) fail(built.error.code, built.error.message);
|
|
1782
|
+
const client = new ApiClient(ctx);
|
|
1783
|
+
const res = await client.request(
|
|
1784
|
+
"POST",
|
|
1785
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders`,
|
|
1786
|
+
built.body
|
|
1787
|
+
);
|
|
1788
|
+
if (!res.ok || !res.data?.reminder) {
|
|
1789
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "SCHEDULE_FAILED";
|
|
1790
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1791
|
+
}
|
|
1792
|
+
process.stdout.write(
|
|
1793
|
+
formatReminderScheduled(res.data.reminder, res.data.warning ?? null) + "\n"
|
|
1794
|
+
);
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// src/commands/reminder/list.ts
|
|
1799
|
+
var VALID_STATUSES2 = /* @__PURE__ */ new Set(["scheduled", "fired", "canceled"]);
|
|
1800
|
+
function registerReminderListCommand(parent) {
|
|
1801
|
+
parent.command("list").description("List your own reminders (defaults to scheduled)").option(
|
|
1802
|
+
"--status <s>",
|
|
1803
|
+
"Comma-separated statuses (scheduled,fired,canceled). Default: scheduled"
|
|
1804
|
+
).action(async (opts) => {
|
|
1805
|
+
let ctx;
|
|
1806
|
+
try {
|
|
1807
|
+
ctx = loadAgentContext();
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1810
|
+
throw err;
|
|
1811
|
+
}
|
|
1812
|
+
const statusRaw = opts.status && opts.status.trim().length > 0 ? opts.status.trim() : "scheduled";
|
|
1813
|
+
for (const s of statusRaw.split(",").map((x) => x.trim()).filter(Boolean)) {
|
|
1814
|
+
if (!VALID_STATUSES2.has(s)) {
|
|
1815
|
+
fail("INVALID_ARG", `--status entries must be one of ${Array.from(VALID_STATUSES2).join("|")}; got ${s}`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const params = new URLSearchParams();
|
|
1819
|
+
params.set("status", statusRaw);
|
|
1820
|
+
const client = new ApiClient(ctx);
|
|
1821
|
+
const res = await client.request(
|
|
1822
|
+
"GET",
|
|
1823
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders?${params.toString()}`
|
|
1824
|
+
);
|
|
1825
|
+
if (!res.ok) {
|
|
1826
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "LIST_FAILED";
|
|
1827
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1828
|
+
}
|
|
1829
|
+
process.stdout.write(formatReminderList(res.data?.reminders ?? []) + "\n");
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/commands/reminder/cancel.ts
|
|
1834
|
+
function registerReminderCancelCommand(parent) {
|
|
1835
|
+
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) => {
|
|
1836
|
+
let ctx;
|
|
1837
|
+
try {
|
|
1838
|
+
ctx = loadAgentContext();
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1841
|
+
throw err;
|
|
1842
|
+
}
|
|
1843
|
+
if (!opts.id || opts.id.trim().length === 0) {
|
|
1844
|
+
fail("INVALID_ARG", "--id is required");
|
|
1845
|
+
}
|
|
1846
|
+
const client = new ApiClient(ctx);
|
|
1847
|
+
let fullId = opts.id;
|
|
1848
|
+
if (opts.id.length < 32) {
|
|
1849
|
+
const listRes = await client.request(
|
|
1850
|
+
"GET",
|
|
1851
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders?status=scheduled`
|
|
1852
|
+
);
|
|
1853
|
+
if (!listRes.ok) {
|
|
1854
|
+
const code = listRes.status >= 500 ? "SERVER_5XX" : "CANCEL_FAILED";
|
|
1855
|
+
fail(code, listRes.error ?? `HTTP ${listRes.status}`);
|
|
1856
|
+
}
|
|
1857
|
+
const matches = (listRes.data?.reminders ?? []).filter((r) => r.reminderId.startsWith(opts.id));
|
|
1858
|
+
if (matches.length === 0) {
|
|
1859
|
+
fail("NOT_FOUND", `No scheduled reminder matches id prefix '${opts.id}'.`);
|
|
1860
|
+
}
|
|
1861
|
+
if (matches.length > 1) {
|
|
1862
|
+
fail("AMBIGUOUS", `Ambiguous id prefix '${opts.id}' matches ${matches.length} reminders; pass a longer id.`);
|
|
1863
|
+
}
|
|
1864
|
+
fullId = matches[0].reminderId;
|
|
1865
|
+
}
|
|
1866
|
+
const res = await client.request(
|
|
1867
|
+
"DELETE",
|
|
1868
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/reminders/${encodeURIComponent(fullId)}`
|
|
1869
|
+
);
|
|
1870
|
+
if (!res.ok || !res.data?.reminder) {
|
|
1871
|
+
const code = res.status >= 500 ? "SERVER_5XX" : "CANCEL_FAILED";
|
|
1872
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1873
|
+
}
|
|
1874
|
+
process.stdout.write(formatReminderCanceled(res.data.reminder) + "\n");
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/index.ts
|
|
1879
|
+
var program = new Command();
|
|
1880
|
+
program.name("slock").description(
|
|
1881
|
+
"Agent-facing execution interface for Slock. Invoked by daemon-spawned agent processes \u2014 not a user-facing CLI product."
|
|
1882
|
+
).version("0.0.1");
|
|
1883
|
+
var authCmd = program.command("auth").description("Auth introspection");
|
|
1884
|
+
registerWhoamiCommand(authCmd);
|
|
1885
|
+
var channelCmd = program.command("channel").description("Channel membership operations");
|
|
1886
|
+
registerChannelMembersCommand(channelCmd);
|
|
1887
|
+
registerChannelLeaveCommand(channelCmd);
|
|
1888
|
+
var threadCmd = program.command("thread").description("Thread attention operations");
|
|
1889
|
+
registerThreadUnfollowCommand(threadCmd);
|
|
1890
|
+
var serverCmd = program.command("server").description("Server / workspace introspection");
|
|
1891
|
+
registerServerInfoCommand(serverCmd);
|
|
1892
|
+
var messageCmd = program.command("message").description("Message operations");
|
|
1893
|
+
registerSendCommand(messageCmd);
|
|
1894
|
+
registerCheckCommand(messageCmd);
|
|
1895
|
+
registerReadCommand(messageCmd);
|
|
1896
|
+
registerSearchCommand(messageCmd);
|
|
1897
|
+
var attachmentCmd = program.command("attachment").description("Attachment operations");
|
|
1898
|
+
registerAttachmentUploadCommand(attachmentCmd);
|
|
1899
|
+
registerAttachmentViewCommand(attachmentCmd);
|
|
1900
|
+
var taskCmd = program.command("task").description("Task board operations");
|
|
1901
|
+
registerTaskListCommand(taskCmd);
|
|
1902
|
+
registerTaskCreateCommand(taskCmd);
|
|
1903
|
+
registerTaskClaimCommand(taskCmd);
|
|
1904
|
+
registerTaskUnclaimCommand(taskCmd);
|
|
1905
|
+
registerTaskUpdateCommand(taskCmd);
|
|
1906
|
+
var profileCmd = program.command("profile").description("Profile operations");
|
|
1907
|
+
registerProfileShowCommand(profileCmd);
|
|
1908
|
+
registerProfileUpdateCommand(profileCmd);
|
|
1909
|
+
var reminderCmd = program.command("reminder").description("Reminder operations");
|
|
1910
|
+
registerReminderScheduleCommand(reminderCmd);
|
|
1911
|
+
registerReminderListCommand(reminderCmd);
|
|
1912
|
+
registerReminderCancelCommand(reminderCmd);
|
|
1913
|
+
program.parseAsync().catch((err) => {
|
|
1914
|
+
if (err instanceof CliExit) {
|
|
1915
|
+
process.exitCode = err.exitCode;
|
|
1916
|
+
} else {
|
|
1917
|
+
process.stderr.write(`Unexpected error: ${err?.message ?? err}
|
|
1918
|
+
`);
|
|
1919
|
+
process.exitCode = 1;
|
|
1920
|
+
}
|
|
1921
|
+
});
|