@eliya-oss/agent-slack 0.1.9 → 0.1.11
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/.agents/cli-api.md +43 -9
- package/CHANGELOG.md +19 -0
- package/README.md +2 -2
- package/dist/application/commands.js +148 -46
- package/dist/application/execute.js +3 -2
- package/dist/cli/args.js +2 -0
- package/dist/main.js +0 -0
- package/dist/output/envelope.js +3 -0
- package/dist/output/normalize.js +154 -0
- package/package.json +21 -19
package/.agents/cli-api.md
CHANGED
|
@@ -12,6 +12,7 @@ Non-goal for v0: agent-side summarization. The CLI returns clean context; agents
|
|
|
12
12
|
|
|
13
13
|
- Noun-verb commands: `agent-slack conversation history`, `agent-slack thread get`, `agent-slack api call`.
|
|
14
14
|
- JSON is always available with `--json`; non-TTY stdout defaults to JSON.
|
|
15
|
+
- Output is token-efficient by default: read commands return slim normalized shapes, and JSON is serialized compactly (no indentation). `--pretty` restores indentation; `--full` returns raw Slack objects.
|
|
15
16
|
- Data goes to stdout. Progress, warnings, and errors go to stderr.
|
|
16
17
|
- Every command supports `--help --json`; every group supports `describe --json`.
|
|
17
18
|
- Large reads support `--cursor`, `--limit`, `--all`, and `--format ndjson`.
|
|
@@ -22,9 +23,10 @@ Non-goal for v0: agent-side summarization. The CLI returns clean context; agents
|
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
agent-slack [--profile NAME] [--team TEAM_ID] [--token user|bot|admin|app]
|
|
25
|
-
[--json] [--format json|ndjson|table] [--
|
|
26
|
+
[--json] [--format json|ndjson|table] [--pretty] [--full]
|
|
27
|
+
[--fields FIELD[,FIELD...]]
|
|
26
28
|
[--limit N] [--cursor CURSOR] [--all]
|
|
27
|
-
[--include users,
|
|
29
|
+
[--include users,threads,permalinks]
|
|
28
30
|
[--no-cache] [--trace]
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -55,7 +57,7 @@ Auth behavior:
|
|
|
55
57
|
- Browser login uses Agent Slack's public Slack app.
|
|
56
58
|
- Development and self-hosted OAuth can use Slack app credentials. The **Client ID** and **Client Secret** come from the Slack app's **Basic Information > App Credentials** section.
|
|
57
59
|
- OAuth opens the Slack OAuth URL in the default browser by default. Use `--no-open` to print the URL only, or `--auth-url-out PATH` for headless agents.
|
|
58
|
-
- Distributed Slack apps use
|
|
60
|
+
- Distributed Slack apps use `https://aslk.vercel.app/oauth/slack/callback` as the HTTPS relay redirect URI. The relay forwards Slack's `code` and `state` to the local callback.
|
|
59
61
|
- PKCE uses `user_scope=...`; OAuth with app credentials uses `scope=...` for bot scopes.
|
|
60
62
|
- Default local callback: `http://localhost:45454/oauth/slack/callback`.
|
|
61
63
|
- Stores profiles outside the project directory. The default store is `~/.config/agent-slack/profiles.json`; set `AGENT_SLACK_TOKEN_STORE=keychain` on macOS to keep token secrets in Keychain and only profile metadata on disk.
|
|
@@ -109,7 +111,7 @@ agent-slack conversation list --types public_channel,private_channel,mpim,im [--
|
|
|
109
111
|
agent-slack conversation get CHANNEL_ID
|
|
110
112
|
agent-slack conversation members CHANNEL_ID [--all]
|
|
111
113
|
agent-slack conversation history CHANNEL_ID [--oldest TS] [--latest TS] [--all]
|
|
112
|
-
agent-slack conversation context CHANNEL_ID [--since 24h] [--include users,
|
|
114
|
+
agent-slack conversation context CHANNEL_ID [--since 24h] [--include users,threads,permalinks]
|
|
113
115
|
```
|
|
114
116
|
|
|
115
117
|
`conversation context` returns normalized messages, hydrated users, thread refs, file refs, reactions, and permalinks in one deterministic payload for agents.
|
|
@@ -117,7 +119,7 @@ agent-slack conversation context CHANNEL_ID [--since 24h] [--include users,react
|
|
|
117
119
|
### Threads and messages
|
|
118
120
|
|
|
119
121
|
```bash
|
|
120
|
-
agent-slack thread get --channel CHANNEL_ID --ts MESSAGE_TS [--include users,
|
|
122
|
+
agent-slack thread get --channel CHANNEL_ID --ts MESSAGE_TS [--include users,permalinks]
|
|
121
123
|
agent-slack message get --channel CHANNEL_ID --ts MESSAGE_TS
|
|
122
124
|
agent-slack message permalink --channel CHANNEL_ID --ts MESSAGE_TS
|
|
123
125
|
```
|
|
@@ -182,6 +184,38 @@ Rules:
|
|
|
182
184
|
|
|
183
185
|
## Output Contract
|
|
184
186
|
|
|
187
|
+
### Serialization
|
|
188
|
+
|
|
189
|
+
JSON is serialized compactly by default (no indentation) to minimize an agent's
|
|
190
|
+
token cost. `--pretty` restores 2-space indentation for humans. Human TTY
|
|
191
|
+
rendering is unchanged.
|
|
192
|
+
|
|
193
|
+
### Normalization
|
|
194
|
+
|
|
195
|
+
Read commands return slim normalized shapes by default, carrying the fields an
|
|
196
|
+
agent needs to reason and dropping Slack boilerplate. `--full` bypasses
|
|
197
|
+
normalization and returns the raw Slack objects.
|
|
198
|
+
|
|
199
|
+
- **message**: `user` (or `bot_id`/`username`), `ts`, `text`, and, when present,
|
|
200
|
+
`thread_ts`, `reply_count`, `reactions` (`[{name, count}]`), `files`
|
|
201
|
+
(`[{id, name, mimetype, size}]`), `subtype`, `edited`. Drops `blocks`,
|
|
202
|
+
`client_msg_id`, `team`, and reaction user lists.
|
|
203
|
+
- **user**: `id`, `name`, `real_name` (display name), and, when present,
|
|
204
|
+
`is_bot`, `deleted`, `title`. Drops all `image_*` URLs, `color`, `status_*`,
|
|
205
|
+
`tz`, and team fields.
|
|
206
|
+
- **file**: `id`, `name`, `mimetype`, `size`, and `permalink` when present.
|
|
207
|
+
Drops thumbnails and private URLs.
|
|
208
|
+
- **conversation**: `id`, `name`, `is_private`, `is_archived`, `topic`,
|
|
209
|
+
`purpose`, `num_members` when present.
|
|
210
|
+
- `conversation context` dedupes thread roots already present in `messages` and
|
|
211
|
+
hydrates every unique author, including those who appear only in replies.
|
|
212
|
+
|
|
213
|
+
### Scope warnings
|
|
214
|
+
|
|
215
|
+
When a scope the active token lacks would make a result more complete or
|
|
216
|
+
efficient (for example `users:read` for author hydration), the CLI adds a
|
|
217
|
+
message to `warnings` instead of failing silently.
|
|
218
|
+
|
|
185
219
|
Default JSON envelope:
|
|
186
220
|
|
|
187
221
|
```json
|
|
@@ -212,7 +246,7 @@ Structured error on stderr:
|
|
|
212
246
|
{
|
|
213
247
|
"ok": false,
|
|
214
248
|
"error": {
|
|
215
|
-
"type": "
|
|
249
|
+
"type": "SlackRateLimited",
|
|
216
250
|
"title": "Slack rate limit reached",
|
|
217
251
|
"slack_error": "ratelimited",
|
|
218
252
|
"retriable": true,
|
|
@@ -230,7 +264,7 @@ Exit codes:
|
|
|
230
264
|
- `2`: usage or validation error
|
|
231
265
|
- `3`: not found
|
|
232
266
|
- `4`: auth or permission denied
|
|
233
|
-
- `5`:
|
|
267
|
+
- `5`: unsafe method blocked
|
|
234
268
|
- `6`: rate limited
|
|
235
269
|
|
|
236
270
|
Rate-limit behavior:
|
|
@@ -264,13 +298,13 @@ Schema output includes:
|
|
|
264
298
|
Read a thread:
|
|
265
299
|
|
|
266
300
|
```bash
|
|
267
|
-
agent-slack thread get --channel C123 --ts 1710000000.000100 --include users,
|
|
301
|
+
agent-slack thread get --channel C123 --ts 1710000000.000100 --include users,permalinks --json
|
|
268
302
|
```
|
|
269
303
|
|
|
270
304
|
Prepare channel-summary input:
|
|
271
305
|
|
|
272
306
|
```bash
|
|
273
|
-
agent-slack conversation context C123 --since 24h --include users,
|
|
307
|
+
agent-slack conversation context C123 --since 24h --include users,threads,permalinks --json
|
|
274
308
|
```
|
|
275
309
|
|
|
276
310
|
Search before reading:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @eliya-oss/agent-slack
|
|
2
2
|
|
|
3
|
+
## 0.1.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e01fe7d: Read commands now return slim, token-efficient output by default. Messages, users, files, and conversations are normalized to the fields that matter, and JSON is serialized compactly. On a real 30-day channel read this cut the payload by about 85% (6.4x smaller) with no loss of meaning.
|
|
8
|
+
|
|
9
|
+
- `--full` returns the raw Slack objects when you need them.
|
|
10
|
+
- `--pretty` restores indentation for humans.
|
|
11
|
+
- `conversation context` now hydrates authors that appear only in thread replies, and warns via `warnings` when `users:read` is missing instead of returning bare IDs.
|
|
12
|
+
- `thread get --include users,permalinks` now hydrates author names and per-message links (previously the flag was accepted but ignored).
|
|
13
|
+
|
|
14
|
+
Note: this changes the default output shape of read commands. Pass `--full` to get the previous raw Slack objects.
|
|
15
|
+
|
|
16
|
+
## 0.1.10
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Use the hosted Agent Slack OAuth relay for default browser login.
|
|
21
|
+
|
|
3
22
|
## 0.1.9
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# agent-slack
|
|
4
4
|
|
|
5
|
-
Slack
|
|
6
|
-
Short alias: `aslk`.
|
|
5
|
+
Read your Slack workspace from the command line: threads, channels, users, files, and search.
|
|
6
|
+
Built so your AI agents can read Slack too. Short alias: `aslk`.
|
|
7
7
|
|
|
8
8
|
<pre align="center">npm install -g @eliya-oss/agent-slack</pre>
|
|
9
9
|
|
|
@@ -2,8 +2,9 @@ import { flagBoolean, flagString, requireFlag, splitCsv } from "../cli/args.js";
|
|
|
2
2
|
import { PRIMARY_COMMAND_NAME, describeAllCommands, describeCommandGroup, findCommandMetadata, renderCompletion, renderHumanHelp } from "../cli/metadata.js";
|
|
3
3
|
import { ResourceNotFound, UnsupportedMethod, UsageError } from "../domain/errors.js";
|
|
4
4
|
import { ProfileName, Scope } from "../domain/ids.js";
|
|
5
|
-
import { pagingFrom, successEnvelope, toNdjson } from "../output/envelope.js";
|
|
5
|
+
import { pagingFrom, serializeJson, successEnvelope, toNdjson } from "../output/envelope.js";
|
|
6
6
|
import { renderHumanEnvelope } from "../output/human.js";
|
|
7
|
+
import { normalizeResponse, slimFile, slimMessage, slimUser } from "../output/normalize.js";
|
|
7
8
|
import { projectFields } from "../output/projection.js";
|
|
8
9
|
import { getProfile, parseTokenType, requireYes, tokenFor } from "./auth.js";
|
|
9
10
|
import { parseJsonPayload, requirePositional } from "./payload.js";
|
|
@@ -81,14 +82,7 @@ export const dispatch = async (parsed, services) => {
|
|
|
81
82
|
return conversationCommand(parsed, services);
|
|
82
83
|
}
|
|
83
84
|
if (first === "thread" && second === "get") {
|
|
84
|
-
return
|
|
85
|
-
method: "conversations.replies",
|
|
86
|
-
payload: {
|
|
87
|
-
channel: requireFlag(parsed, "channel"),
|
|
88
|
-
ts: requireFlag(parsed, "ts"),
|
|
89
|
-
limit: numberFlag(parsed, "limit")
|
|
90
|
-
}
|
|
91
|
-
});
|
|
85
|
+
return threadGet(parsed, services);
|
|
92
86
|
}
|
|
93
87
|
if (first === "message") {
|
|
94
88
|
if (second === "get") {
|
|
@@ -207,13 +201,14 @@ const authCommand = async (parsed, services) => {
|
|
|
207
201
|
const scopes = splitCsv(flagString(parsed, "scopes"));
|
|
208
202
|
const userScopes = splitCsv(flagString(parsed, "user-scopes"));
|
|
209
203
|
const pkceUserScopes = userScopes.length > 0 ? userScopes : scopes.length > 0 ? scopes : defaultPkceUserScopes;
|
|
204
|
+
const redirectUri = flagString(parsed, "redirect-uri") ?? (!byoOAuth && clientId === defaultPkceClientId ? defaultPkceRedirectUri : undefined);
|
|
210
205
|
const profile = await services.oauthFlow.login({
|
|
211
206
|
profileName,
|
|
212
207
|
clientId,
|
|
213
208
|
...(byoOAuth ? { clientSecret } : { pkce: true }),
|
|
214
209
|
scopes: byoOAuth ? (scopes.length === 0 ? defaultOAuthScopes : scopes) : [],
|
|
215
210
|
userScopes: byoOAuth ? userScopes : pkceUserScopes,
|
|
216
|
-
redirectUri
|
|
211
|
+
redirectUri,
|
|
217
212
|
authUrlOut: flagString(parsed, "auth-url-out"),
|
|
218
213
|
timeoutMs: numberFlag(parsed, "timeout-ms"),
|
|
219
214
|
openBrowser: !flagBoolean(parsed, "no-open")
|
|
@@ -241,13 +236,14 @@ const defaultOAuthScopes = [
|
|
|
241
236
|
];
|
|
242
237
|
const defaultPkceUserScopes = defaultOAuthScopes;
|
|
243
238
|
const defaultPkceClientId = "11499810382723.11506074725874";
|
|
239
|
+
const defaultPkceRedirectUri = "https://aslk.vercel.app/oauth/slack/callback";
|
|
244
240
|
const apiCall = async (parsed, services) => {
|
|
245
241
|
const method = requirePositional(parsed.positionals, 2, "METHOD");
|
|
246
242
|
const payload = await parseJsonPayload({
|
|
247
243
|
inline: flagString(parsed, "payload"),
|
|
248
244
|
positional: parsed.positionals[3]
|
|
249
245
|
});
|
|
250
|
-
return methodCall(parsed, services, { method, payload });
|
|
246
|
+
return methodCall(parsed, services, { method, payload }, { normalize: false });
|
|
251
247
|
};
|
|
252
248
|
const teamCommand = async (parsed, services) => {
|
|
253
249
|
const second = parsed.positionals[1];
|
|
@@ -330,6 +326,7 @@ const conversationCommand = async (parsed, services) => {
|
|
|
330
326
|
}
|
|
331
327
|
throw new UsageError("Unknown conversation command", { command: parsed.positionals.join(" ") });
|
|
332
328
|
};
|
|
329
|
+
const hasScope = (profile, scope) => profile.scopes.includes(scope);
|
|
333
330
|
const conversationContext = async (parsed, services) => {
|
|
334
331
|
const channel = requirePositional(parsed.positionals, 2, "CHANNEL_ID");
|
|
335
332
|
const profileName = flagString(parsed, "profile", "default") ?? "default";
|
|
@@ -352,26 +349,68 @@ const conversationContext = async (parsed, services) => {
|
|
|
352
349
|
allowWrite: false,
|
|
353
350
|
yes: false
|
|
354
351
|
});
|
|
352
|
+
const full = flagBoolean(parsed, "full");
|
|
353
|
+
const shape = (message) => (full ? message : slimMessage(message));
|
|
355
354
|
const messages = extractArray(history.response.messages);
|
|
355
|
+
const warnings = [];
|
|
356
356
|
const context = {
|
|
357
357
|
channel,
|
|
358
|
-
messages
|
|
358
|
+
messages: messages.map(shape)
|
|
359
359
|
};
|
|
360
|
+
// Fetch threads first so authors who appear only in replies still get hydrated.
|
|
361
|
+
const threadReplies = {};
|
|
362
|
+
if (include.has("threads")) {
|
|
363
|
+
const threads = {};
|
|
364
|
+
for (const message of messages) {
|
|
365
|
+
const record = extractLooseRecord(message);
|
|
366
|
+
const ts = stringField(record, "thread_ts") ?? stringField(record, "ts");
|
|
367
|
+
const hasReplies = typeof record.reply_count === "number" && record.reply_count > 0;
|
|
368
|
+
if (ts !== undefined && hasReplies) {
|
|
369
|
+
const thread = await callSlack(services, {
|
|
370
|
+
method: "conversations.replies",
|
|
371
|
+
payload: { channel, ts },
|
|
372
|
+
token,
|
|
373
|
+
profile,
|
|
374
|
+
all: false,
|
|
375
|
+
allowWrite: false,
|
|
376
|
+
yes: false
|
|
377
|
+
});
|
|
378
|
+
// Drop the root; it is already present in `messages`.
|
|
379
|
+
const replies = extractArray(thread.response.messages).filter((reply) => stringField(extractLooseRecord(reply), "ts") !== ts);
|
|
380
|
+
threadReplies[ts] = replies;
|
|
381
|
+
threads[ts] = replies.map(shape);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
context.threads = threads;
|
|
385
|
+
}
|
|
360
386
|
if (include.has("users")) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
387
|
+
if (hasScope(profile, "users:read")) {
|
|
388
|
+
const replyAuthors = Object.values(threadReplies)
|
|
389
|
+
.flat()
|
|
390
|
+
.map((reply) => stringField(extractLooseRecord(reply), "user"));
|
|
391
|
+
const authorIds = uniqueStrings([
|
|
392
|
+
...messages.map((message) => stringField(extractLooseRecord(message), "user")),
|
|
393
|
+
...replyAuthors
|
|
394
|
+
]);
|
|
395
|
+
const users = {};
|
|
396
|
+
for (const userId of authorIds) {
|
|
397
|
+
const user = await callSlack(services, {
|
|
398
|
+
method: "users.info",
|
|
399
|
+
payload: { user: userId },
|
|
400
|
+
token,
|
|
401
|
+
profile,
|
|
402
|
+
all: false,
|
|
403
|
+
allowWrite: false,
|
|
404
|
+
yes: false
|
|
405
|
+
});
|
|
406
|
+
const raw = user.response.user ?? user.response;
|
|
407
|
+
users[userId] = full ? raw : slimUser(raw);
|
|
408
|
+
}
|
|
409
|
+
context.users = users;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
warnings.push("users:read scope is missing, so message authors are returned as IDs only. Reconnect with users:read to hydrate names.");
|
|
373
413
|
}
|
|
374
|
-
context.users = users;
|
|
375
414
|
}
|
|
376
415
|
if (include.has("permalinks")) {
|
|
377
416
|
const permalinks = {};
|
|
@@ -392,33 +431,95 @@ const conversationContext = async (parsed, services) => {
|
|
|
392
431
|
}
|
|
393
432
|
context.permalinks = permalinks;
|
|
394
433
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
434
|
+
return {
|
|
435
|
+
method: "conversation.context",
|
|
436
|
+
profile,
|
|
437
|
+
response: history.response,
|
|
438
|
+
stdoutValue: context,
|
|
439
|
+
items: messages.map(shape),
|
|
440
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
const threadGet = async (parsed, services) => {
|
|
444
|
+
const profileName = flagString(parsed, "profile", "default") ?? "default";
|
|
445
|
+
const tokenType = parseTokenType(flagString(parsed, "token"));
|
|
446
|
+
const profile = await getProfile(services, profileName);
|
|
447
|
+
const token = tokenFor(profile, tokenType);
|
|
448
|
+
const channel = requireFlag(parsed, "channel");
|
|
449
|
+
const ts = requireFlag(parsed, "ts");
|
|
450
|
+
const full = flagBoolean(parsed, "full");
|
|
451
|
+
const include = new Set(splitCsv(flagString(parsed, "include")));
|
|
452
|
+
const result = await callSlack(services, {
|
|
453
|
+
method: "conversations.replies",
|
|
454
|
+
payload: cleanObject({ channel, ts, limit: numberFlag(parsed, "limit") }),
|
|
455
|
+
token,
|
|
456
|
+
profile,
|
|
457
|
+
all: flagBoolean(parsed, "all"),
|
|
458
|
+
allowWrite: false,
|
|
459
|
+
yes: false
|
|
460
|
+
});
|
|
461
|
+
if (flagBoolean(parsed, "raw")) {
|
|
462
|
+
return {
|
|
463
|
+
method: "conversations.replies",
|
|
464
|
+
profile,
|
|
465
|
+
stdoutValue: result.response,
|
|
466
|
+
rawStdout: serializeJson(result.response, flagBoolean(parsed, "pretty")),
|
|
467
|
+
response: result.response,
|
|
468
|
+
items: result.items
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const shape = (message) => (full ? message : slimMessage(message));
|
|
472
|
+
const rawMessages = extractArray(result.response.messages);
|
|
473
|
+
const warnings = [];
|
|
474
|
+
const data = { messages: rawMessages.map(shape) };
|
|
475
|
+
if (include.has("users")) {
|
|
476
|
+
if (hasScope(profile, "users:read")) {
|
|
477
|
+
const users = {};
|
|
478
|
+
for (const userId of uniqueStrings(rawMessages.map((message) => stringField(extractLooseRecord(message), "user")))) {
|
|
479
|
+
const user = await callSlack(services, {
|
|
480
|
+
method: "users.info",
|
|
481
|
+
payload: { user: userId },
|
|
405
482
|
token,
|
|
406
483
|
profile,
|
|
407
484
|
all: false,
|
|
408
485
|
allowWrite: false,
|
|
409
486
|
yes: false
|
|
410
487
|
});
|
|
411
|
-
|
|
488
|
+
const raw = user.response.user ?? user.response;
|
|
489
|
+
users[userId] = full ? raw : slimUser(raw);
|
|
412
490
|
}
|
|
491
|
+
data.users = users;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
warnings.push("users:read scope is missing, so message authors are returned as IDs only. Reconnect with users:read to hydrate names.");
|
|
413
495
|
}
|
|
414
|
-
|
|
496
|
+
}
|
|
497
|
+
if (include.has("permalinks")) {
|
|
498
|
+
const permalinks = {};
|
|
499
|
+
for (const messageTs of uniqueStrings(rawMessages.map((message) => stringField(extractLooseRecord(message), "ts")))) {
|
|
500
|
+
const permalink = await callSlack(services, {
|
|
501
|
+
method: "chat.getPermalink",
|
|
502
|
+
payload: { channel, message_ts: messageTs },
|
|
503
|
+
token,
|
|
504
|
+
profile,
|
|
505
|
+
all: false,
|
|
506
|
+
allowWrite: false,
|
|
507
|
+
yes: false
|
|
508
|
+
});
|
|
509
|
+
const value = stringField(permalink.response, "permalink");
|
|
510
|
+
if (value !== undefined) {
|
|
511
|
+
permalinks[messageTs] = value;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
data.permalinks = permalinks;
|
|
415
515
|
}
|
|
416
516
|
return {
|
|
417
|
-
method: "
|
|
517
|
+
method: "conversations.replies",
|
|
418
518
|
profile,
|
|
419
|
-
response:
|
|
420
|
-
stdoutValue:
|
|
421
|
-
items:
|
|
519
|
+
response: result.response,
|
|
520
|
+
stdoutValue: data,
|
|
521
|
+
items: rawMessages.map(shape),
|
|
522
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
422
523
|
};
|
|
423
524
|
};
|
|
424
525
|
const searchCommand = async (parsed, services) => {
|
|
@@ -484,10 +585,10 @@ const fileDownload = async (parsed, services) => {
|
|
|
484
585
|
method: "file.download",
|
|
485
586
|
profile,
|
|
486
587
|
response: result.response,
|
|
487
|
-
stdoutValue: { file, download }
|
|
588
|
+
stdoutValue: { file: flagBoolean(parsed, "full") ? file : slimFile(file), download }
|
|
488
589
|
};
|
|
489
590
|
};
|
|
490
|
-
const methodCall = async (parsed, services, input) => {
|
|
591
|
+
const methodCall = async (parsed, services, input, options = {}) => {
|
|
491
592
|
const profileName = flagString(parsed, "profile", "default") ?? "default";
|
|
492
593
|
const tokenType = parseTokenType(flagString(parsed, "token"));
|
|
493
594
|
const profile = await getProfile(services, profileName);
|
|
@@ -507,15 +608,16 @@ const methodCall = async (parsed, services, input) => {
|
|
|
507
608
|
method: input.method,
|
|
508
609
|
profile,
|
|
509
610
|
stdoutValue: result.response,
|
|
510
|
-
rawStdout:
|
|
611
|
+
rawStdout: serializeJson(result.response, flagBoolean(parsed, "pretty")),
|
|
511
612
|
response: result.response,
|
|
512
613
|
items: result.items
|
|
513
614
|
};
|
|
514
615
|
}
|
|
616
|
+
const normalize = options.normalize !== false && !flagBoolean(parsed, "full");
|
|
515
617
|
return {
|
|
516
618
|
method: input.method,
|
|
517
619
|
profile,
|
|
518
|
-
stdoutValue: result.response,
|
|
620
|
+
stdoutValue: normalize ? normalizeResponse(input.method, result.response) : result.response,
|
|
519
621
|
response: result.response,
|
|
520
622
|
items: result.items
|
|
521
623
|
};
|
|
@@ -536,7 +638,7 @@ export const renderDispatchResult = (parsed, result, options = {}) => {
|
|
|
536
638
|
...(result.warnings === undefined ? {} : { warnings: result.warnings })
|
|
537
639
|
});
|
|
538
640
|
if (shouldRenderJson(parsed, options)) {
|
|
539
|
-
return
|
|
641
|
+
return serializeJson(envelope, flagBoolean(parsed, "pretty"));
|
|
540
642
|
}
|
|
541
643
|
return renderHumanEnvelope(envelope, {
|
|
542
644
|
color: options.stdoutIsTty === true && options.env?.NO_COLOR === undefined && !flagBoolean(parsed, "no-color")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { flagBoolean, flagString, parseArgs } from "../cli/args.js";
|
|
2
|
-
import { errorEnvelope } from "../output/envelope.js";
|
|
2
|
+
import { errorEnvelope, serializeJson } from "../output/envelope.js";
|
|
3
3
|
import { renderHumanErrorEnvelope } from "../output/human.js";
|
|
4
4
|
import { dispatch, renderDispatchResult } from "./commands.js";
|
|
5
5
|
export const executeCli = async (argv, services, options = {}) => {
|
|
@@ -15,8 +15,9 @@ export const executeCli = async (argv, services, options = {}) => {
|
|
|
15
15
|
}
|
|
16
16
|
catch (error) {
|
|
17
17
|
const { envelope, exitCode } = errorEnvelope(error);
|
|
18
|
+
const pretty = parsed === null ? argv.includes("--pretty") : flagBoolean(parsed, "pretty");
|
|
18
19
|
const stderr = shouldRenderJsonError(argv, parsed, options)
|
|
19
|
-
?
|
|
20
|
+
? serializeJson(envelope, pretty)
|
|
20
21
|
: renderHumanErrorEnvelope(envelope, {
|
|
21
22
|
color: shouldColorError(argv, parsed, options)
|
|
22
23
|
});
|
package/dist/cli/args.js
CHANGED
package/dist/main.js
CHANGED
|
File without changes
|
package/dist/output/envelope.js
CHANGED
|
@@ -58,5 +58,8 @@ const suggestionFor = (tag) => {
|
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
60
|
export const stringifyJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
61
|
+
// Machine output is compact by default to minimize token cost; `--pretty`
|
|
62
|
+
// restores indentation for humans. See ADR-002.
|
|
63
|
+
export const serializeJson = (value, pretty) => pretty ? `${JSON.stringify(value, null, 2)}\n` : `${JSON.stringify(value)}\n`;
|
|
61
64
|
export const toNdjson = (items) => items.map((item) => JSON.stringify(item)).join("\n") + (items.length > 0 ? "\n" : "");
|
|
62
65
|
export const exitCodeOf = (error) => isSlkError(error) ? error.exitCode : 1;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Slim, token-efficient shapes for Slack read output.
|
|
2
|
+
//
|
|
3
|
+
// Raw Slack objects carry far more than an agent needs to reason: users.info
|
|
4
|
+
// returns ~7 avatar URLs plus color/status/tz/team; messages carry `blocks`
|
|
5
|
+
// (a structured duplicate of `text`), `client_msg_id`, `team`, and verbose
|
|
6
|
+
// reactions. These transforms keep the reasoning-relevant fields and drop the
|
|
7
|
+
// rest. `--full` bypasses them and returns the raw objects. See ADR-002.
|
|
8
|
+
const isRec = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
10
|
+
const str = (record, key) => typeof record[key] === "string" && record[key].length > 0 ? record[key] : undefined;
|
|
11
|
+
export const slimFile = (input) => {
|
|
12
|
+
if (!isRec(input))
|
|
13
|
+
return input;
|
|
14
|
+
const out = { id: input.id, name: input.name };
|
|
15
|
+
if (typeof input.mimetype === "string")
|
|
16
|
+
out.mimetype = input.mimetype;
|
|
17
|
+
if (typeof input.size === "number")
|
|
18
|
+
out.size = input.size;
|
|
19
|
+
const permalink = str(input, "permalink");
|
|
20
|
+
if (permalink)
|
|
21
|
+
out.permalink = permalink;
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
export const slimMessage = (input) => {
|
|
25
|
+
if (!isRec(input))
|
|
26
|
+
return input;
|
|
27
|
+
const m = input;
|
|
28
|
+
const out = {};
|
|
29
|
+
if (typeof m.user === "string")
|
|
30
|
+
out.user = m.user;
|
|
31
|
+
else if (typeof m.bot_id === "string")
|
|
32
|
+
out.bot_id = m.bot_id;
|
|
33
|
+
const username = str(m, "username");
|
|
34
|
+
if (username)
|
|
35
|
+
out.username = username;
|
|
36
|
+
const subtype = str(m, "subtype");
|
|
37
|
+
if (subtype)
|
|
38
|
+
out.subtype = subtype;
|
|
39
|
+
if (typeof m.ts === "string")
|
|
40
|
+
out.ts = m.ts;
|
|
41
|
+
out.text = typeof m.text === "string" ? m.text : "";
|
|
42
|
+
if (typeof m.thread_ts === "string" && m.thread_ts !== m.ts)
|
|
43
|
+
out.thread_ts = m.thread_ts;
|
|
44
|
+
if (typeof m.reply_count === "number")
|
|
45
|
+
out.reply_count = m.reply_count;
|
|
46
|
+
const reactions = asArray(m.reactions)
|
|
47
|
+
.filter(isRec)
|
|
48
|
+
.map((r) => ({ name: r.name, count: r.count }));
|
|
49
|
+
if (reactions.length > 0)
|
|
50
|
+
out.reactions = reactions;
|
|
51
|
+
const files = asArray(m.files).map(slimFile);
|
|
52
|
+
if (files.length > 0)
|
|
53
|
+
out.files = files;
|
|
54
|
+
if (m.edited !== undefined && m.edited !== null)
|
|
55
|
+
out.edited = true;
|
|
56
|
+
return out;
|
|
57
|
+
};
|
|
58
|
+
export const slimUser = (input) => {
|
|
59
|
+
if (!isRec(input))
|
|
60
|
+
return input;
|
|
61
|
+
const u = input;
|
|
62
|
+
const profile = isRec(u.profile) ? u.profile : {};
|
|
63
|
+
const out = { id: u.id, name: u.name };
|
|
64
|
+
const real = str(profile, "display_name") ?? str(u, "real_name") ?? str(profile, "real_name");
|
|
65
|
+
if (real)
|
|
66
|
+
out.real_name = real;
|
|
67
|
+
if (u.is_bot === true)
|
|
68
|
+
out.is_bot = true;
|
|
69
|
+
if (u.deleted === true)
|
|
70
|
+
out.deleted = true;
|
|
71
|
+
const title = str(profile, "title");
|
|
72
|
+
if (title)
|
|
73
|
+
out.title = title;
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
export const slimConversation = (input) => {
|
|
77
|
+
if (!isRec(input))
|
|
78
|
+
return input;
|
|
79
|
+
const c = input;
|
|
80
|
+
const out = { id: c.id, name: c.name };
|
|
81
|
+
if (c.is_private === true)
|
|
82
|
+
out.is_private = true;
|
|
83
|
+
if (c.is_archived === true)
|
|
84
|
+
out.is_archived = true;
|
|
85
|
+
if (c.is_im === true)
|
|
86
|
+
out.is_im = true;
|
|
87
|
+
if (c.is_mpim === true)
|
|
88
|
+
out.is_mpim = true;
|
|
89
|
+
const topic = isRec(c.topic) ? str(c.topic, "value") : undefined;
|
|
90
|
+
if (topic)
|
|
91
|
+
out.topic = topic;
|
|
92
|
+
const purpose = isRec(c.purpose) ? str(c.purpose, "value") : undefined;
|
|
93
|
+
if (purpose)
|
|
94
|
+
out.purpose = purpose;
|
|
95
|
+
if (typeof c.num_members === "number")
|
|
96
|
+
out.num_members = c.num_members;
|
|
97
|
+
return out;
|
|
98
|
+
};
|
|
99
|
+
export const slimTeam = (input) => {
|
|
100
|
+
if (!isRec(input))
|
|
101
|
+
return input;
|
|
102
|
+
const t = input;
|
|
103
|
+
const out = { id: t.id, name: t.name };
|
|
104
|
+
const domain = str(t, "domain");
|
|
105
|
+
if (domain)
|
|
106
|
+
out.domain = domain;
|
|
107
|
+
const emailDomain = str(t, "email_domain");
|
|
108
|
+
if (emailDomain)
|
|
109
|
+
out.email_domain = emailDomain;
|
|
110
|
+
const enterpriseId = str(t, "enterprise_id");
|
|
111
|
+
if (enterpriseId)
|
|
112
|
+
out.enterprise_id = enterpriseId;
|
|
113
|
+
const enterpriseName = str(t, "enterprise_name");
|
|
114
|
+
if (enterpriseName)
|
|
115
|
+
out.enterprise_name = enterpriseName;
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
// Drop the always-present envelope noise from an unmapped response.
|
|
119
|
+
const stripResponseEnvelope = (response) => {
|
|
120
|
+
const { ok, response_metadata, ...rest } = response;
|
|
121
|
+
void ok;
|
|
122
|
+
void response_metadata;
|
|
123
|
+
return rest;
|
|
124
|
+
};
|
|
125
|
+
// Normalize a raw Slack Web API response for a given method into slim `data`.
|
|
126
|
+
export const normalizeResponse = (method, response) => {
|
|
127
|
+
if (!isRec(response))
|
|
128
|
+
return response;
|
|
129
|
+
const r = response;
|
|
130
|
+
switch (method) {
|
|
131
|
+
case "conversations.history":
|
|
132
|
+
case "conversations.replies":
|
|
133
|
+
return { messages: asArray(r.messages).map(slimMessage) };
|
|
134
|
+
case "conversations.info":
|
|
135
|
+
return { channel: slimConversation(r.channel) };
|
|
136
|
+
case "conversations.list":
|
|
137
|
+
return { channels: asArray(r.channels).map(slimConversation) };
|
|
138
|
+
case "users.info":
|
|
139
|
+
case "users.lookupByEmail":
|
|
140
|
+
return { user: slimUser(r.user) };
|
|
141
|
+
case "users.list":
|
|
142
|
+
return { users: asArray(r.members).map(slimUser) };
|
|
143
|
+
case "files.info":
|
|
144
|
+
return { file: slimFile(r.file) };
|
|
145
|
+
case "files.list":
|
|
146
|
+
return { files: asArray(r.files).map(slimFile) };
|
|
147
|
+
case "team.info":
|
|
148
|
+
return { team: slimTeam(r.team) };
|
|
149
|
+
case "reactions.get":
|
|
150
|
+
return isRec(r.message) ? { ...stripResponseEnvelope(r), message: slimMessage(r.message) } : stripResponseEnvelope(r);
|
|
151
|
+
default:
|
|
152
|
+
return stripResponseEnvelope(r);
|
|
153
|
+
}
|
|
154
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eliya-oss/agent-slack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Slack context CLI for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"packageManager": "pnpm@11.9.0",
|
|
7
6
|
"bin": {
|
|
8
7
|
"agent-slack": "dist/main.js",
|
|
9
8
|
"aslk": "dist/main.js"
|
|
@@ -17,22 +16,6 @@
|
|
|
17
16
|
"LICENSE",
|
|
18
17
|
".agents/cli-api.md"
|
|
19
18
|
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "tsc -p tsconfig.build.json",
|
|
22
|
-
"check": "pnpm typecheck && pnpm test:coverage && pnpm build",
|
|
23
|
-
"changeset": "changeset",
|
|
24
|
-
"clean": "rm -rf dist coverage",
|
|
25
|
-
"dev": "tsx src/main.ts",
|
|
26
|
-
"prepack": "pnpm build",
|
|
27
|
-
"release:check": "pnpm check && npm pack --dry-run",
|
|
28
|
-
"agent-slack": "tsx src/main.ts",
|
|
29
|
-
"aslk": "tsx src/main.ts",
|
|
30
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
-
"test": "vitest run",
|
|
32
|
-
"test:coverage": "vitest run --coverage",
|
|
33
|
-
"test:emulate": "vitest run tests/emulate",
|
|
34
|
-
"version-packages": "changeset version"
|
|
35
|
-
},
|
|
36
19
|
"keywords": [
|
|
37
20
|
"slack",
|
|
38
21
|
"cli",
|
|
@@ -61,5 +44,24 @@
|
|
|
61
44
|
},
|
|
62
45
|
"engines": {
|
|
63
46
|
"node": ">=22"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"check": "pnpm typecheck && pnpm test:coverage && pnpm build",
|
|
51
|
+
"changeset": "changeset",
|
|
52
|
+
"clean": "rm -rf dist coverage",
|
|
53
|
+
"dev": "tsx src/main.ts",
|
|
54
|
+
"docs:build": "pnpm --filter docs build",
|
|
55
|
+
"docs:dev": "pnpm --filter docs dev",
|
|
56
|
+
"docs:types": "pnpm --filter docs types:check",
|
|
57
|
+
"release:check": "pnpm check && npm pack --dry-run",
|
|
58
|
+
"release:publish": "pnpm build && changeset publish",
|
|
59
|
+
"agent-slack": "tsx src/main.ts",
|
|
60
|
+
"aslk": "tsx src/main.ts",
|
|
61
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:coverage": "vitest run --coverage",
|
|
64
|
+
"test:emulate": "vitest run tests/emulate",
|
|
65
|
+
"version-packages": "changeset version"
|
|
64
66
|
}
|
|
65
|
-
}
|
|
67
|
+
}
|