@eliya-oss/agent-slack 0.1.10 → 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.
@@ -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] [--fields FIELD[,FIELD...]]
26
+ [--json] [--format json|ndjson|table] [--pretty] [--full]
27
+ [--fields FIELD[,FIELD...]]
26
28
  [--limit N] [--cursor CURSOR] [--all]
27
- [--include users,reactions,files,permalinks,threads]
29
+ [--include users,threads,permalinks]
28
30
  [--no-cache] [--trace]
29
31
  ```
30
32
 
@@ -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,reactions,files,threads]
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,reactions,files,permalinks]
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": "rate_limited",
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`: conflict or invalid Slack state
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,reactions,files,permalinks --json
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,reactions,files,threads --format ndjson
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,18 @@
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
+
3
16
  ## 0.1.10
4
17
 
5
18
  ### Patch Changes
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  # agent-slack
4
4
 
5
- Slack context for AI agents via the `agent-slack` CLI.
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 methodCall(parsed, services, {
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") {
@@ -249,7 +243,7 @@ const apiCall = async (parsed, services) => {
249
243
  inline: flagString(parsed, "payload"),
250
244
  positional: parsed.positionals[3]
251
245
  });
252
- return methodCall(parsed, services, { method, payload });
246
+ return methodCall(parsed, services, { method, payload }, { normalize: false });
253
247
  };
254
248
  const teamCommand = async (parsed, services) => {
255
249
  const second = parsed.positionals[1];
@@ -332,6 +326,7 @@ const conversationCommand = async (parsed, services) => {
332
326
  }
333
327
  throw new UsageError("Unknown conversation command", { command: parsed.positionals.join(" ") });
334
328
  };
329
+ const hasScope = (profile, scope) => profile.scopes.includes(scope);
335
330
  const conversationContext = async (parsed, services) => {
336
331
  const channel = requirePositional(parsed.positionals, 2, "CHANNEL_ID");
337
332
  const profileName = flagString(parsed, "profile", "default") ?? "default";
@@ -354,26 +349,68 @@ const conversationContext = async (parsed, services) => {
354
349
  allowWrite: false,
355
350
  yes: false
356
351
  });
352
+ const full = flagBoolean(parsed, "full");
353
+ const shape = (message) => (full ? message : slimMessage(message));
357
354
  const messages = extractArray(history.response.messages);
355
+ const warnings = [];
358
356
  const context = {
359
357
  channel,
360
- messages
358
+ messages: messages.map(shape)
361
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
+ }
362
386
  if (include.has("users")) {
363
- const users = {};
364
- for (const userId of uniqueStrings(messages.map((message) => stringField(extractLooseRecord(message), "user")))) {
365
- const user = await callSlack(services, {
366
- method: "users.info",
367
- payload: { user: userId },
368
- token,
369
- profile,
370
- all: false,
371
- allowWrite: false,
372
- yes: false
373
- });
374
- users[userId] = user.response.user ?? user.response;
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.");
375
413
  }
376
- context.users = users;
377
414
  }
378
415
  if (include.has("permalinks")) {
379
416
  const permalinks = {};
@@ -394,33 +431,95 @@ const conversationContext = async (parsed, services) => {
394
431
  }
395
432
  context.permalinks = permalinks;
396
433
  }
397
- if (include.has("threads")) {
398
- const threads = {};
399
- for (const message of messages) {
400
- const record = extractLooseRecord(message);
401
- const ts = stringField(record, "thread_ts") ?? stringField(record, "ts");
402
- const hasReplies = typeof record.reply_count === "number" && record.reply_count > 0;
403
- if (ts !== undefined && hasReplies) {
404
- const thread = await callSlack(services, {
405
- method: "conversations.replies",
406
- payload: { channel, ts },
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 },
407
482
  token,
408
483
  profile,
409
484
  all: false,
410
485
  allowWrite: false,
411
486
  yes: false
412
487
  });
413
- threads[ts] = thread.response.messages ?? thread.response;
488
+ const raw = user.response.user ?? user.response;
489
+ users[userId] = full ? raw : slimUser(raw);
414
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.");
415
495
  }
416
- context.threads = threads;
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;
417
515
  }
418
516
  return {
419
- method: "conversation.context",
517
+ method: "conversations.replies",
420
518
  profile,
421
- response: history.response,
422
- stdoutValue: context,
423
- items: messages
519
+ response: result.response,
520
+ stdoutValue: data,
521
+ items: rawMessages.map(shape),
522
+ ...(warnings.length > 0 ? { warnings } : {})
424
523
  };
425
524
  };
426
525
  const searchCommand = async (parsed, services) => {
@@ -486,10 +585,10 @@ const fileDownload = async (parsed, services) => {
486
585
  method: "file.download",
487
586
  profile,
488
587
  response: result.response,
489
- stdoutValue: { file, download }
588
+ stdoutValue: { file: flagBoolean(parsed, "full") ? file : slimFile(file), download }
490
589
  };
491
590
  };
492
- const methodCall = async (parsed, services, input) => {
591
+ const methodCall = async (parsed, services, input, options = {}) => {
493
592
  const profileName = flagString(parsed, "profile", "default") ?? "default";
494
593
  const tokenType = parseTokenType(flagString(parsed, "token"));
495
594
  const profile = await getProfile(services, profileName);
@@ -509,15 +608,16 @@ const methodCall = async (parsed, services, input) => {
509
608
  method: input.method,
510
609
  profile,
511
610
  stdoutValue: result.response,
512
- rawStdout: `${JSON.stringify(result.response, null, 2)}\n`,
611
+ rawStdout: serializeJson(result.response, flagBoolean(parsed, "pretty")),
513
612
  response: result.response,
514
613
  items: result.items
515
614
  };
516
615
  }
616
+ const normalize = options.normalize !== false && !flagBoolean(parsed, "full");
517
617
  return {
518
618
  method: input.method,
519
619
  profile,
520
- stdoutValue: result.response,
620
+ stdoutValue: normalize ? normalizeResponse(input.method, result.response) : result.response,
521
621
  response: result.response,
522
622
  items: result.items
523
623
  };
@@ -538,7 +638,7 @@ export const renderDispatchResult = (parsed, result, options = {}) => {
538
638
  ...(result.warnings === undefined ? {} : { warnings: result.warnings })
539
639
  });
540
640
  if (shouldRenderJson(parsed, options)) {
541
- return `${JSON.stringify(envelope, null, 2)}\n`;
641
+ return serializeJson(envelope, flagBoolean(parsed, "pretty"));
542
642
  }
543
643
  return renderHumanEnvelope(envelope, {
544
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
- ? `${JSON.stringify(envelope, null, 2)}\n`
20
+ ? serializeJson(envelope, pretty)
20
21
  : renderHumanErrorEnvelope(envelope, {
21
22
  color: shouldColorError(argv, parsed, options)
22
23
  });
package/dist/cli/args.js CHANGED
@@ -6,6 +6,8 @@ const booleanFlags = new Set([
6
6
  "json",
7
7
  "no-color",
8
8
  "raw",
9
+ "pretty",
10
+ "full",
9
11
  "trace",
10
12
  "yes",
11
13
  "no-cache",
package/dist/main.js CHANGED
File without changes
@@ -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.10",
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
+ }