@eliya-oss/agent-slack 0.1.0 → 0.1.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Technical contract derived from `.agents/prd/001-agent-readable-slack-interface.md`. Product behavior changes belong in PRD first.
4
4
 
5
- Working binary name: `slk`.
5
+ Working binary name: `agent-slack`. Short alias: `aslk`.
6
6
 
7
7
  Goal: OAuth into Slack, expose everything Slack Web API permissions allow, and give agents stable commands for reading channels, threads, files, users, and search context.
8
8
 
@@ -10,18 +10,18 @@ Non-goal for v0: agent-side summarization. The CLI returns clean context; agents
10
10
 
11
11
  ## Design Rules
12
12
 
13
- - Noun-verb commands: `slk conversation history`, `slk thread get`, `slk api call`.
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
15
  - Data goes to stdout. Progress, warnings, and errors go to stderr.
16
16
  - Every command supports `--help --json`; every group supports `describe --json`.
17
17
  - Large reads support `--cursor`, `--limit`, `--all`, and `--format ndjson`.
18
- - Raw Slack Web API access is first-class through `slk api call`.
18
+ - Raw Slack Web API access is first-class through `agent-slack api call`.
19
19
  - Read commands are safe by default. Any future write/destructive command needs `--yes`; `api call` blocks known write methods unless `--allow-write`.
20
20
 
21
21
  ## Global Flags
22
22
 
23
23
  ```bash
24
- slk [--profile NAME] [--team TEAM_ID] [--token user|bot|admin|app]
24
+ agent-slack [--profile NAME] [--team TEAM_ID] [--token user|bot|admin|app]
25
25
  [--json] [--format json|ndjson|table] [--fields FIELD[,FIELD...]]
26
26
  [--limit N] [--cursor CURSOR] [--all]
27
27
  [--include users,reactions,files,permalinks,threads]
@@ -37,30 +37,30 @@ Defaults:
37
37
  ## Auth
38
38
 
39
39
  ```bash
40
- slk auth login
41
- slk auth login --profile work --scopes channels:read,channels:history --user-scopes search:read.public,search:read.private
42
- slk auth login --oauth --client-id CLIENT_ID --client-secret CLIENT_SECRET --auth-url-out /tmp/slk-auth-url
43
- slk auth status
44
- slk auth scopes
45
- slk auth profiles list
46
- slk auth logout --profile work
40
+ agent-slack auth login
41
+ agent-slack auth login --profile work --scopes channels:read,channels:history --user-scopes search:read.public,search:read.private
42
+ agent-slack auth login --oauth --client-id CLIENT_ID --client-secret CLIENT_SECRET --auth-url-out /tmp/agent-slack-auth-url
43
+ agent-slack auth status
44
+ agent-slack auth scopes
45
+ agent-slack auth profiles list
46
+ agent-slack auth logout --profile work
47
47
  ```
48
48
 
49
49
  Auth behavior:
50
50
 
51
51
  - Uses Slack OAuth v2 with a localhost callback.
52
52
  - Bot scopes go in `scope=...`; user scopes go in `user_scope=...`.
53
- - Stores profiles outside the project directory. The default store is `~/.config/slk/profiles.json`; set `SLK_TOKEN_STORE=keychain` on macOS to keep token secrets in Keychain and only profile metadata on disk.
53
+ - 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.
54
54
  - Supports multiple profiles and workspaces.
55
55
  - `auth status --json` returns token type, team, enterprise, user/bot identity, granted scopes, and missing recommended scopes.
56
56
 
57
57
  Scope presets:
58
58
 
59
59
  ```bash
60
- slk auth login --preset read-user
61
- slk auth login --preset read-bot
62
- slk auth login --preset assistant-search
63
- slk auth login --preset admin-read
60
+ agent-slack auth login --preset read-user
61
+ agent-slack auth login --preset read-bot
62
+ agent-slack auth login --preset assistant-search
63
+ agent-slack auth login --preset admin-read
64
64
  ```
65
65
 
66
66
  Preset intent:
@@ -77,31 +77,31 @@ Important Slack boundary: there is no token that means "read all Slack." Slack o
77
77
  ### Workspaces and identity
78
78
 
79
79
  ```bash
80
- slk auth test
81
- slk team get
82
- slk team profile get
83
- slk enterprise get
80
+ agent-slack auth test
81
+ agent-slack team get
82
+ agent-slack team profile get
83
+ agent-slack enterprise get
84
84
  ```
85
85
 
86
86
  ### Users
87
87
 
88
88
  ```bash
89
- slk user list [--all]
90
- slk user get USER_ID
91
- slk user lookup --email EMAIL
92
- slk user presence get USER_ID
93
- slk usergroups list
94
- slk usergroups users list USERGROUP_ID
89
+ agent-slack user list [--all]
90
+ agent-slack user get USER_ID
91
+ agent-slack user lookup --email EMAIL
92
+ agent-slack user presence get USER_ID
93
+ agent-slack usergroups list
94
+ agent-slack usergroups users list USERGROUP_ID
95
95
  ```
96
96
 
97
97
  ### Conversations
98
98
 
99
99
  ```bash
100
- slk conversation list --types public_channel,private_channel,mpim,im [--all]
101
- slk conversation get CHANNEL_ID
102
- slk conversation members CHANNEL_ID [--all]
103
- slk conversation history CHANNEL_ID [--oldest TS] [--latest TS] [--all]
104
- slk conversation context CHANNEL_ID [--since 24h] [--include users,reactions,files,threads]
100
+ agent-slack conversation list --types public_channel,private_channel,mpim,im [--all]
101
+ agent-slack conversation get CHANNEL_ID
102
+ agent-slack conversation members CHANNEL_ID [--all]
103
+ agent-slack conversation history CHANNEL_ID [--oldest TS] [--latest TS] [--all]
104
+ agent-slack conversation context CHANNEL_ID [--since 24h] [--include users,reactions,files,threads]
105
105
  ```
106
106
 
107
107
  `conversation context` is the agent-friendly form: normalized messages, hydrated users, thread refs, file refs, reactions, and permalinks in one deterministic payload.
@@ -109,9 +109,9 @@ slk conversation context CHANNEL_ID [--since 24h] [--include users,reactions,fil
109
109
  ### Threads and messages
110
110
 
111
111
  ```bash
112
- slk thread get --channel CHANNEL_ID --ts MESSAGE_TS [--include users,reactions,files,permalinks]
113
- slk message get --channel CHANNEL_ID --ts MESSAGE_TS
114
- slk message permalink --channel CHANNEL_ID --ts MESSAGE_TS
112
+ agent-slack thread get --channel CHANNEL_ID --ts MESSAGE_TS [--include users,reactions,files,permalinks]
113
+ agent-slack message get --channel CHANNEL_ID --ts MESSAGE_TS
114
+ agent-slack message permalink --channel CHANNEL_ID --ts MESSAGE_TS
115
115
  ```
116
116
 
117
117
  `message get` wraps the Slack history/replies calls needed to fetch one timestamp.
@@ -119,9 +119,9 @@ slk message permalink --channel CHANNEL_ID --ts MESSAGE_TS
119
119
  ### Search
120
120
 
121
121
  ```bash
122
- slk search context --query QUERY [--channel-types public_channel,private_channel,mpim,im] [--content-types messages,files,channels]
123
- slk search messages --query QUERY
124
- slk search files --query QUERY
122
+ agent-slack search context --query QUERY [--channel-types public_channel,private_channel,mpim,im] [--content-types messages,files,channels]
123
+ agent-slack search messages --query QUERY
124
+ agent-slack search files --query QUERY
125
125
  ```
126
126
 
127
127
  Prefer `search context` for agents. It maps to Slack's Real-time Search API where available. `search messages` and `search files` are legacy-compatible convenience commands.
@@ -129,26 +129,26 @@ Prefer `search context` for agents. It maps to Slack's Real-time Search API wher
129
129
  ### Files and attached context
130
130
 
131
131
  ```bash
132
- slk file list [--channel CHANNEL_ID] [--user USER_ID] [--all]
133
- slk file get FILE_ID
134
- slk file download FILE_ID --out PATH
135
- slk file comments list FILE_ID
132
+ agent-slack file list [--channel CHANNEL_ID] [--user USER_ID] [--all]
133
+ agent-slack file get FILE_ID
134
+ agent-slack file download FILE_ID --out PATH
135
+ agent-slack file comments list FILE_ID
136
136
  ```
137
137
 
138
138
  ### Other readable Slack surfaces
139
139
 
140
140
  ```bash
141
- slk reaction get --channel CHANNEL_ID --ts MESSAGE_TS
142
- slk pin list CHANNEL_ID
143
- slk bookmark list CHANNEL_ID
144
- slk canvas list [--channel CHANNEL_ID]
145
- slk canvas get CANVAS_ID
146
- slk list list [--channel CHANNEL_ID]
147
- slk list get LIST_ID
148
- slk emoji list
149
- slk reminder list
150
- slk star list
151
- slk dnd status [USER_ID]
141
+ agent-slack reaction get --channel CHANNEL_ID --ts MESSAGE_TS
142
+ agent-slack pin list CHANNEL_ID
143
+ agent-slack bookmark list CHANNEL_ID
144
+ agent-slack canvas list [--channel CHANNEL_ID]
145
+ agent-slack canvas get CANVAS_ID
146
+ agent-slack list list [--channel CHANNEL_ID]
147
+ agent-slack list get LIST_ID
148
+ agent-slack emoji list
149
+ agent-slack reminder list
150
+ agent-slack star list
151
+ agent-slack dnd status [USER_ID]
152
152
  ```
153
153
 
154
154
  ## Generic Web API Escape Hatch
@@ -156,11 +156,11 @@ slk dnd status [USER_ID]
156
156
  This is what gives full Web API coverage.
157
157
 
158
158
  ```bash
159
- slk api methods list [--family conversations]
160
- slk api method describe conversations.history
161
- slk api call conversations.history --payload '{"channel":"C123","limit":50}'
162
- slk api call conversations.history --payload @payload.json
163
- cat payload.json | slk api call conversations.history -
159
+ agent-slack api methods list [--family conversations]
160
+ agent-slack api method describe conversations.history
161
+ agent-slack api call conversations.history --payload '{"channel":"C123","limit":50}'
162
+ agent-slack api call conversations.history --payload @payload.json
163
+ cat payload.json | agent-slack api call conversations.history -
164
164
  ```
165
165
 
166
166
  Rules:
@@ -235,10 +235,10 @@ Rate-limit behavior:
235
235
  ## Schema Introspection
236
236
 
237
237
  ```bash
238
- slk describe --json
239
- slk conversation describe --json
240
- slk conversation history --help --json
241
- slk api method describe conversations.replies --json
238
+ agent-slack describe --json
239
+ agent-slack conversation describe --json
240
+ agent-slack conversation history --help --json
241
+ agent-slack api method describe conversations.replies --json
242
242
  ```
243
243
 
244
244
  Schema output includes:
@@ -256,25 +256,25 @@ Schema output includes:
256
256
  Read a thread:
257
257
 
258
258
  ```bash
259
- slk thread get --channel C123 --ts 1710000000.000100 --include users,reactions,files,permalinks --json
259
+ agent-slack thread get --channel C123 --ts 1710000000.000100 --include users,reactions,files,permalinks --json
260
260
  ```
261
261
 
262
262
  Prepare channel-summary input:
263
263
 
264
264
  ```bash
265
- slk conversation context C123 --since 24h --include users,reactions,files,threads --format ndjson
265
+ agent-slack conversation context C123 --since 24h --include users,reactions,files,threads --format ndjson
266
266
  ```
267
267
 
268
268
  Search before reading:
269
269
 
270
270
  ```bash
271
- slk search context --query "latest on project atlas" --content-types messages,files --json
271
+ agent-slack search context --query "latest on project atlas" --content-types messages,files --json
272
272
  ```
273
273
 
274
274
  Call a new Slack method before the CLI has a wrapper:
275
275
 
276
276
  ```bash
277
- slk api call some.newMethod --payload @request.json --json
277
+ agent-slack api call some.newMethod --payload @request.json --json
278
278
  ```
279
279
 
280
280
  ## Build Order
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @eliya-oss/agent-slack
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Expose `agent-slack` as the primary binary, add the `aslk` alias, and render human-friendly TTY output while preserving JSON for agent and piped runs.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  # Agent Slack
4
4
 
5
- Slack context for AI agents, exposed through the `slk` CLI.
5
+ Slack context for AI agents, exposed through the `agent-slack` CLI.
6
+ Short alias: `aslk`.
6
7
 
7
8
  ```bash
8
9
  npm install -g @eliya-oss/agent-slack
@@ -15,11 +16,11 @@ npm install -g @eliya-oss/agent-slack
15
16
  ## Usage
16
17
 
17
18
  ```bash
18
- slk auth login --oauth --client-id "$SLACK_CLIENT_ID" --client-secret "$SLACK_CLIENT_SECRET" --json
19
- slk conversation history C123 --limit 50 --json
20
- slk thread get --channel C123 --ts 1710000000.000100 --include users,permalinks --json
21
- slk conversation context C123 --include users,threads,permalinks --format ndjson
22
- slk api call conversations.info --payload '{"channel":"C123"}' --json
19
+ agent-slack auth login --oauth --client-id "$SLACK_CLIENT_ID" --client-secret "$SLACK_CLIENT_SECRET" --json
20
+ agent-slack conversation history C123 --limit 50 --json
21
+ agent-slack thread get --channel C123 --ts 1710000000.000100 --include users,permalinks --json
22
+ agent-slack conversation context C123 --include users,threads,permalinks --format ndjson
23
+ agent-slack api call conversations.info --payload '{"channel":"C123"}' --json
23
24
  ```
24
25
 
25
26
  ## Skill
@@ -13,20 +13,20 @@
13
13
  <text x="78" y="116" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">npm install -g @eliya-oss/agent-slack</text>
14
14
 
15
15
  <text x="54" y="162" fill="#22C55E" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">$</text>
16
- <text x="78" y="162" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">slk auth status --json</text>
17
- <text x="78" y="196" fill="#93C5FD" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">{ "ok": true, "profile": "work", "data": { "teamId": "T123" } }</text>
16
+ <text x="78" y="162" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">aslk auth status</text>
17
+ <text x="78" y="196" fill="#86EFAC" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">ok auth.status profile work | team T123</text>
18
18
 
19
19
  <text x="54" y="250" fill="#22C55E" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">$</text>
20
- <text x="78" y="250" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">slk conversation context C123 --include users,threads --format ndjson</text>
21
- <text x="78" y="284" fill="#FDE68A" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">{"type":"slack.message","channel_id":"C123","ts":"1710000000.000100","text":"Ship status?"}</text>
22
- <text x="78" y="314" fill="#FDE68A" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">{"type":"slack.message","channel_id":"C123","thread_ts":"1710000000.000100","text":"Green."}</text>
20
+ <text x="78" y="250" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">aslk conversation history C123 --limit 3</text>
21
+ <text x="78" y="284" fill="#FDE68A" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">ts user text</text>
22
+ <text x="78" y="314" fill="#FDE68A" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">1710000000.000100 U123 Ship status?</text>
23
23
 
24
24
  <text x="54" y="368" fill="#22C55E" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">$</text>
25
- <text x="78" y="368" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">slk thread get --channel C123 --ts 1710000000.000100 --include users --json</text>
25
+ <text x="78" y="368" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">agent-slack thread get --channel C123 --ts 1710000000.000100 --include users --json</text>
26
26
  <text x="78" y="402" fill="#C4B5FD" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">{ "ok": true, "method": "conversations.replies", "data": { "messages": [ ... ] } }</text>
27
27
 
28
28
  <text x="54" y="456" fill="#22C55E" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">$</text>
29
- <text x="78" y="456" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">slk api call conversations.info --payload '{"channel":"C123"}' --json</text>
29
+ <text x="78" y="456" fill="#E5E7EB" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">agent-slack api call conversations.info --payload '{"channel":"C123"}' --json</text>
30
30
  <rect x="54" y="488" width="210" height="28" rx="6" fill="#122034"/>
31
- <text x="70" y="508" fill="#67E8F9" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="14">stdout is parseable JSON</text>
31
+ <text x="70" y="508" fill="#67E8F9" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="14">--json stays parseable</text>
32
32
  </svg>
@@ -13,7 +13,7 @@ export class KeychainTokenStore {
13
13
  this.secrets = secrets;
14
14
  }
15
15
  static fromEnv(env = process.env) {
16
- const configDir = env.SLK_CONFIG_DIR ?? join(env.HOME ?? process.cwd(), ".config", "slk");
16
+ const configDir = env.AGENT_SLACK_CONFIG_DIR ?? env.SLK_CONFIG_DIR ?? join(env.HOME ?? process.cwd(), ".config", "agent-slack");
17
17
  return new KeychainTokenStore(join(configDir, "profiles.keychain.json"));
18
18
  }
19
19
  async getProfile(name) {
@@ -4,10 +4,14 @@ import { KeychainTokenStore } from "./keychain/KeychainTokenStore.js";
4
4
  import { NodeLocalhostOAuthFlow } from "./localhost-oauth/NodeLocalhostOAuthFlow.js";
5
5
  import { FileTokenStore } from "./profile-file/FileTokenStore.js";
6
6
  import { SlackSdkWebApi } from "./slack-sdk/SlackSdkWebApi.js";
7
- export const createLiveServices = (env = process.env) => ({
8
- tokenStore: env.SLK_TOKEN_STORE === "keychain" ? KeychainTokenStore.fromEnv(env) : FileTokenStore.fromEnv(env),
9
- oauthFlow: NodeLocalhostOAuthFlow.fromEnv(env),
10
- fileDownloader: new HttpFileDownloader(),
11
- slackWebApi: new SlackSdkWebApi(env.SLK_SLACK_API_BASE_URL === undefined ? {} : { slackApiUrl: env.SLK_SLACK_API_BASE_URL }),
12
- methodCatalog: new BundledMethodCatalog()
13
- });
7
+ export const createLiveServices = (env = process.env) => {
8
+ const tokenStore = env.AGENT_SLACK_TOKEN_STORE ?? env.SLK_TOKEN_STORE;
9
+ const slackApiUrl = env.AGENT_SLACK_API_BASE_URL ?? env.SLK_SLACK_API_BASE_URL;
10
+ return {
11
+ tokenStore: tokenStore === "keychain" ? KeychainTokenStore.fromEnv(env) : FileTokenStore.fromEnv(env),
12
+ oauthFlow: NodeLocalhostOAuthFlow.fromEnv(env),
13
+ fileDownloader: new HttpFileDownloader(),
14
+ slackWebApi: new SlackSdkWebApi(slackApiUrl === undefined ? {} : { slackApiUrl }),
15
+ methodCatalog: new BundledMethodCatalog()
16
+ };
17
+ };
@@ -9,14 +9,17 @@ export class NodeLocalhostOAuthFlow {
9
9
  this.options = options;
10
10
  }
11
11
  static fromEnv(env = process.env) {
12
- const apiBaseUrl = env.SLK_SLACK_API_BASE_URL;
12
+ const apiBaseUrl = env.AGENT_SLACK_API_BASE_URL ?? env.SLK_SLACK_API_BASE_URL;
13
13
  const emulatorBaseUrl = apiBaseUrl?.replace(/\/api\/?$/, "");
14
+ const redirectHost = env.AGENT_SLACK_OAUTH_REDIRECT_HOST ?? env.SLK_OAUTH_REDIRECT_HOST;
15
+ const redirectPath = env.AGENT_SLACK_OAUTH_REDIRECT_PATH ?? env.SLK_OAUTH_REDIRECT_PATH;
16
+ const timeoutMs = env.AGENT_SLACK_OAUTH_TIMEOUT_MS ?? env.SLK_OAUTH_TIMEOUT_MS;
14
17
  return new NodeLocalhostOAuthFlow({
15
- authorizeUrl: env.SLK_SLACK_OAUTH_AUTHORIZE_URL ?? (emulatorBaseUrl === undefined ? "https://slack.com/oauth/v2/authorize" : `${emulatorBaseUrl}/oauth/v2/authorize`),
16
- tokenUrl: env.SLK_SLACK_OAUTH_ACCESS_URL ?? (apiBaseUrl === undefined ? "https://slack.com/api/oauth.v2.access" : `${apiBaseUrl.replace(/\/?$/, "/")}oauth.v2.access`),
17
- ...(env.SLK_OAUTH_REDIRECT_HOST === undefined ? {} : { defaultRedirectHost: env.SLK_OAUTH_REDIRECT_HOST }),
18
- ...(env.SLK_OAUTH_REDIRECT_PATH === undefined ? {} : { defaultRedirectPath: env.SLK_OAUTH_REDIRECT_PATH }),
19
- ...(env.SLK_OAUTH_TIMEOUT_MS === undefined ? {} : { defaultTimeoutMs: Number(env.SLK_OAUTH_TIMEOUT_MS) })
18
+ authorizeUrl: env.AGENT_SLACK_OAUTH_AUTHORIZE_URL ?? env.SLK_SLACK_OAUTH_AUTHORIZE_URL ?? (emulatorBaseUrl === undefined ? "https://slack.com/oauth/v2/authorize" : `${emulatorBaseUrl}/oauth/v2/authorize`),
19
+ tokenUrl: env.AGENT_SLACK_OAUTH_ACCESS_URL ?? env.SLK_SLACK_OAUTH_ACCESS_URL ?? (apiBaseUrl === undefined ? "https://slack.com/api/oauth.v2.access" : `${apiBaseUrl.replace(/\/?$/, "/")}oauth.v2.access`),
20
+ ...(redirectHost === undefined ? {} : { defaultRedirectHost: redirectHost }),
21
+ ...(redirectPath === undefined ? {} : { defaultRedirectPath: redirectPath }),
22
+ ...(timeoutMs === undefined ? {} : { defaultTimeoutMs: Number(timeoutMs) })
20
23
  });
21
24
  }
22
25
  async login(input) {
@@ -9,15 +9,15 @@ export class FileTokenStore {
9
9
  this.envProfile = envProfile;
10
10
  }
11
11
  static fromEnv(env = process.env) {
12
- const configDir = env.SLK_CONFIG_DIR ?? join(env.HOME ?? process.cwd(), ".config", "slk");
13
- const token = env.SLK_TOKEN ?? env.SLACK_BOT_TOKEN ?? null;
12
+ const configDir = env.AGENT_SLACK_CONFIG_DIR ?? env.SLK_CONFIG_DIR ?? join(env.HOME ?? process.cwd(), ".config", "agent-slack");
13
+ const token = env.AGENT_SLACK_TOKEN ?? env.SLK_TOKEN ?? env.SLACK_BOT_TOKEN ?? null;
14
14
  const envProfile = token === null
15
15
  ? null
16
16
  : {
17
17
  name: ProfileName.default,
18
18
  tokenType: "bot",
19
19
  botToken: token,
20
- scopes: (env.SLK_SCOPES ?? "").split(",").filter(Boolean).map(Scope.make)
20
+ scopes: (env.AGENT_SLACK_SCOPES ?? env.SLK_SCOPES ?? "").split(",").filter(Boolean).map(Scope.make)
21
21
  };
22
22
  return new FileTokenStore(join(configDir, "profiles.json"), envProfile);
23
23
  }
@@ -1,8 +1,9 @@
1
1
  import { flagBoolean, flagString, requireFlag, splitCsv } from "../cli/args.js";
2
- import { describeAllCommands, describeCommandGroup, findCommandMetadata, renderCompletion, renderHumanHelp } from "../cli/metadata.js";
2
+ import { PRIMARY_COMMAND_NAME, describeAllCommands, describeCommandGroup, findCommandMetadata, renderCompletion, renderHumanHelp } from "../cli/metadata.js";
3
3
  import { NotAuthenticated, ResourceNotFound, UnsupportedMethod, UsageError } from "../domain/errors.js";
4
4
  import { ProfileName, Scope } from "../domain/ids.js";
5
5
  import { pagingFrom, successEnvelope, toNdjson } from "../output/envelope.js";
6
+ import { renderHumanEnvelope } from "../output/human.js";
6
7
  import { projectFields } from "../output/projection.js";
7
8
  import { getProfile, parseTokenType, requireYes, tokenFor } from "./auth.js";
8
9
  import { parseJsonPayload, requirePositional } from "./payload.js";
@@ -147,7 +148,7 @@ export const dispatch = async (parsed, services) => {
147
148
  };
148
149
  const describePath = (path, error) => {
149
150
  const metadata = findCommandMetadata(path);
150
- const value = metadata ?? describeCommandGroup(path[0] ?? "slk");
151
+ const value = metadata ?? describeCommandGroup(path[0] ?? PRIMARY_COMMAND_NAME);
151
152
  if (error !== null && metadata === null) {
152
153
  throw error;
153
154
  }
@@ -184,7 +185,7 @@ const authCommand = async (parsed, services) => {
184
185
  return methodCall(parsed, services, { method: "auth.test", payload: {} });
185
186
  }
186
187
  if (second === "login") {
187
- const token = flagString(parsed, "token") ?? process.env.SLK_TOKEN ?? process.env.SLACK_BOT_TOKEN;
188
+ const token = flagString(parsed, "token") ?? process.env.AGENT_SLACK_TOKEN ?? process.env.SLK_TOKEN ?? process.env.SLACK_BOT_TOKEN;
188
189
  if (token !== undefined && !flagBoolean(parsed, "oauth")) {
189
190
  const profile = {
190
191
  name: ProfileName.make(profileName),
@@ -195,10 +196,10 @@ const authCommand = async (parsed, services) => {
195
196
  await services.tokenStore.setProfile(profile);
196
197
  return { method: "auth.login", profile, stdoutValue: sanitizeProfile(profile) };
197
198
  }
198
- const clientId = flagString(parsed, "client-id") ?? process.env.SLK_CLIENT_ID;
199
- const clientSecret = flagString(parsed, "client-secret") ?? process.env.SLK_CLIENT_SECRET;
199
+ const clientId = flagString(parsed, "client-id") ?? process.env.AGENT_SLACK_CLIENT_ID ?? process.env.SLK_CLIENT_ID ?? process.env.SLACK_CLIENT_ID;
200
+ const clientSecret = flagString(parsed, "client-secret") ?? process.env.AGENT_SLACK_CLIENT_SECRET ?? process.env.SLK_CLIENT_SECRET ?? process.env.SLACK_CLIENT_SECRET;
200
201
  if (clientId === undefined || clientSecret === undefined) {
201
- throw new NotAuthenticated("OAuth login needs --client-id/--client-secret or SLK_CLIENT_ID/SLK_CLIENT_SECRET");
202
+ throw new NotAuthenticated("OAuth login needs --client-id/--client-secret or AGENT_SLACK_CLIENT_ID/AGENT_SLACK_CLIENT_SECRET");
202
203
  }
203
204
  const scopes = splitCsv(flagString(parsed, "scopes"));
204
205
  const profile = await services.oauthFlow.login({
@@ -511,7 +512,7 @@ const methodCall = async (parsed, services, input) => {
511
512
  items: result.items
512
513
  };
513
514
  };
514
- export const renderDispatchResult = (parsed, result) => {
515
+ export const renderDispatchResult = (parsed, result, options = {}) => {
515
516
  if (result.rawStdout !== undefined) {
516
517
  return result.rawStdout;
517
518
  }
@@ -526,7 +527,16 @@ export const renderDispatchResult = (parsed, result) => {
526
527
  ...(result.response === undefined ? {} : { paging: pagingFrom(result.response) }),
527
528
  ...(result.warnings === undefined ? {} : { warnings: result.warnings })
528
529
  });
529
- return `${JSON.stringify(envelope, null, 2)}\n`;
530
+ if (shouldRenderJson(parsed, options)) {
531
+ return `${JSON.stringify(envelope, null, 2)}\n`;
532
+ }
533
+ return renderHumanEnvelope(envelope, {
534
+ color: options.stdoutIsTty === true && options.env?.NO_COLOR === undefined && !flagBoolean(parsed, "no-color")
535
+ });
536
+ };
537
+ const shouldRenderJson = (parsed, options) => {
538
+ const format = flagString(parsed, "format");
539
+ return flagBoolean(parsed, "json") || format === "json" || options.stdoutIsTty !== true;
530
540
  };
531
541
  const numberFlag = (parsed, name) => {
532
542
  const value = flagString(parsed, name);
@@ -1,13 +1,13 @@
1
1
  import { parseArgs } from "../cli/args.js";
2
2
  import { errorEnvelope } from "../output/envelope.js";
3
3
  import { dispatch, renderDispatchResult } from "./commands.js";
4
- export const executeCli = async (argv, services) => {
4
+ export const executeCli = async (argv, services, options = {}) => {
5
5
  try {
6
6
  const parsed = parseArgs(argv);
7
7
  const result = await dispatch(parsed, services);
8
8
  return {
9
9
  exitCode: 0,
10
- stdout: renderDispatchResult(parsed, result),
10
+ stdout: renderDispatchResult(parsed, result, options),
11
11
  stderr: ""
12
12
  };
13
13
  }
package/dist/cli/args.js CHANGED
@@ -4,6 +4,7 @@ const booleanFlags = new Set([
4
4
  "allow-write",
5
5
  "help",
6
6
  "json",
7
+ "no-color",
7
8
  "raw",
8
9
  "trace",
9
10
  "yes",
@@ -1,3 +1,6 @@
1
+ export const PRIMARY_COMMAND_NAME = "agent-slack";
2
+ export const SHORT_COMMAND_NAME = "aslk";
3
+ export const COMMAND_NAMES = [PRIMARY_COMMAND_NAME, SHORT_COMMAND_NAME];
1
4
  export const commandMetadata = [
2
5
  {
3
6
  path: ["describe"],
@@ -5,7 +8,7 @@ export const commandMetadata = [
5
8
  flags: ["--json"],
6
9
  safety: "read",
7
10
  output: "command metadata",
8
- examples: ["slk describe --json"]
11
+ examples: ["agent-slack describe --json"]
9
12
  },
10
13
  {
11
14
  path: ["completion"],
@@ -13,7 +16,7 @@ export const commandMetadata = [
13
16
  args: ["bash|zsh"],
14
17
  safety: "read",
15
18
  output: "shell completion script",
16
- examples: ["slk completion zsh > ~/.zfunc/_slk"]
19
+ examples: ["agent-slack completion zsh > ~/.zfunc/_agent-slack"]
17
20
  },
18
21
  {
19
22
  path: ["auth", "status"],
@@ -21,7 +24,7 @@ export const commandMetadata = [
21
24
  flags: ["--profile", "--json"],
22
25
  safety: "read",
23
26
  output: "profile status",
24
- examples: ["slk auth status --json"]
27
+ examples: ["agent-slack auth status --json"]
25
28
  },
26
29
  {
27
30
  path: ["auth", "login"],
@@ -30,8 +33,8 @@ export const commandMetadata = [
30
33
  safety: "read",
31
34
  output: "profile status",
32
35
  examples: [
33
- "slk auth login --oauth --client-id 123.456 --client-secret secret --scopes channels:read,channels:history --json",
34
- "SLK_TOKEN=xoxb-... slk auth login --profile work --scopes channels:read --json"
36
+ "agent-slack auth login --oauth --client-id 123.456 --client-secret secret --scopes channels:read,channels:history --json",
37
+ "AGENT_SLACK_TOKEN=xoxb-... agent-slack auth login --profile work --scopes channels:read --json"
35
38
  ]
36
39
  },
37
40
  {
@@ -40,7 +43,7 @@ export const commandMetadata = [
40
43
  flags: ["--profile", "--json"],
41
44
  safety: "read",
42
45
  output: "scope list",
43
- examples: ["slk auth scopes --json"]
46
+ examples: ["agent-slack auth scopes --json"]
44
47
  },
45
48
  {
46
49
  path: ["auth", "profiles", "list"],
@@ -48,7 +51,7 @@ export const commandMetadata = [
48
51
  flags: ["--json"],
49
52
  safety: "read",
50
53
  output: "profile list",
51
- examples: ["slk auth profiles list --json"]
54
+ examples: ["agent-slack auth profiles list --json"]
52
55
  },
53
56
  {
54
57
  path: ["auth", "logout"],
@@ -56,7 +59,7 @@ export const commandMetadata = [
56
59
  flags: ["--profile", "--json"],
57
60
  safety: "destructive",
58
61
  output: "deleted profile status",
59
- examples: ["slk auth logout --profile work --yes --json"]
62
+ examples: ["agent-slack auth logout --profile work --yes --json"]
60
63
  },
61
64
  {
62
65
  path: ["auth", "test"],
@@ -65,7 +68,7 @@ export const commandMetadata = [
65
68
  scopes: [],
66
69
  safety: "read",
67
70
  output: "Slack auth.test response",
68
- examples: ["slk auth test --json"]
71
+ examples: ["agent-slack auth test --json"]
69
72
  },
70
73
  {
71
74
  path: ["team", "get"],
@@ -74,7 +77,7 @@ export const commandMetadata = [
74
77
  scopes: ["team:read"],
75
78
  safety: "read",
76
79
  output: "team info",
77
- examples: ["slk team get --json"]
80
+ examples: ["agent-slack team get --json"]
78
81
  },
79
82
  {
80
83
  path: ["team", "profile", "get"],
@@ -83,7 +86,7 @@ export const commandMetadata = [
83
86
  scopes: ["users.profile:read"],
84
87
  safety: "read",
85
88
  output: "team profile fields",
86
- examples: ["slk team profile get --json"]
89
+ examples: ["agent-slack team profile get --json"]
87
90
  },
88
91
  {
89
92
  path: ["enterprise", "get"],
@@ -92,7 +95,7 @@ export const commandMetadata = [
92
95
  scopes: ["team:read"],
93
96
  safety: "read",
94
97
  output: "team or enterprise info",
95
- examples: ["slk enterprise get --json"]
98
+ examples: ["agent-slack enterprise get --json"]
96
99
  },
97
100
  {
98
101
  path: ["user", "list"],
@@ -102,7 +105,7 @@ export const commandMetadata = [
102
105
  scopes: ["users:read"],
103
106
  safety: "read",
104
107
  output: "user list",
105
- examples: ["slk user list --limit 50 --json"]
108
+ examples: ["agent-slack user list --limit 50 --json"]
106
109
  },
107
110
  {
108
111
  path: ["user", "get"],
@@ -112,7 +115,7 @@ export const commandMetadata = [
112
115
  scopes: ["users:read"],
113
116
  safety: "read",
114
117
  output: "user info",
115
- examples: ["slk user get U123 --json"]
118
+ examples: ["agent-slack user get U123 --json"]
116
119
  },
117
120
  {
118
121
  path: ["user", "lookup"],
@@ -122,7 +125,7 @@ export const commandMetadata = [
122
125
  scopes: ["users:read.email"],
123
126
  safety: "read",
124
127
  output: "user info",
125
- examples: ["slk user lookup --email dev@example.com --json"]
128
+ examples: ["agent-slack user lookup --email dev@example.com --json"]
126
129
  },
127
130
  {
128
131
  path: ["user", "presence", "get"],
@@ -132,7 +135,7 @@ export const commandMetadata = [
132
135
  scopes: ["users:read"],
133
136
  safety: "read",
134
137
  output: "presence",
135
- examples: ["slk user presence get U123 --json"]
138
+ examples: ["agent-slack user presence get U123 --json"]
136
139
  },
137
140
  {
138
141
  path: ["usergroups", "list"],
@@ -141,7 +144,7 @@ export const commandMetadata = [
141
144
  scopes: ["usergroups:read"],
142
145
  safety: "read",
143
146
  output: "user group list",
144
- examples: ["slk usergroups list --json"]
147
+ examples: ["agent-slack usergroups list --json"]
145
148
  },
146
149
  {
147
150
  path: ["usergroups", "users", "list"],
@@ -151,7 +154,7 @@ export const commandMetadata = [
151
154
  scopes: ["usergroups:read"],
152
155
  safety: "read",
153
156
  output: "user IDs",
154
- examples: ["slk usergroups users list S123 --json"]
157
+ examples: ["agent-slack usergroups users list S123 --json"]
155
158
  },
156
159
  {
157
160
  path: ["api", "call"],
@@ -161,8 +164,8 @@ export const commandMetadata = [
161
164
  safety: "unknown",
162
165
  output: "Slack Web API response",
163
166
  examples: [
164
- "slk api call conversations.history --payload '{\"channel\":\"C123\"}' --json",
165
- "cat payload.json | slk api call conversations.history -"
167
+ "agent-slack api call conversations.history --payload '{\"channel\":\"C123\"}' --json",
168
+ "cat payload.json | agent-slack api call conversations.history -"
166
169
  ]
167
170
  },
168
171
  {
@@ -171,7 +174,7 @@ export const commandMetadata = [
171
174
  flags: ["--family", "--json"],
172
175
  safety: "read",
173
176
  output: "method metadata list",
174
- examples: ["slk api methods list --family conversations --json"]
177
+ examples: ["agent-slack api methods list --family conversations --json"]
175
178
  },
176
179
  {
177
180
  path: ["api", "method", "describe"],
@@ -180,7 +183,7 @@ export const commandMetadata = [
180
183
  flags: ["--json"],
181
184
  safety: "read",
182
185
  output: "method metadata",
183
- examples: ["slk api method describe conversations.replies --json"]
186
+ examples: ["agent-slack api method describe conversations.replies --json"]
184
187
  },
185
188
  {
186
189
  path: ["conversation", "list"],
@@ -189,7 +192,7 @@ export const commandMetadata = [
189
192
  scopes: ["channels:read", "groups:read", "im:read", "mpim:read"],
190
193
  safety: "read",
191
194
  output: "conversation list",
192
- examples: ["slk conversation list --types public_channel,private_channel --json"]
195
+ examples: ["agent-slack conversation list --types public_channel,private_channel --json"]
193
196
  },
194
197
  {
195
198
  path: ["conversation", "history"],
@@ -199,7 +202,7 @@ export const commandMetadata = [
199
202
  scopes: ["channels:history", "groups:history", "im:history", "mpim:history"],
200
203
  safety: "read",
201
204
  output: "message list",
202
- examples: ["slk conversation history C123 --limit 20 --json"]
205
+ examples: ["agent-slack conversation history C123 --limit 20 --json"]
203
206
  },
204
207
  {
205
208
  path: ["conversation", "context"],
@@ -209,7 +212,7 @@ export const commandMetadata = [
209
212
  scopes: ["channels:history", "users:read"],
210
213
  safety: "read",
211
214
  output: "hydrated conversation context",
212
- examples: ["slk conversation context C123 --since 24h --include users,threads --format ndjson"]
215
+ examples: ["agent-slack conversation context C123 --since 24h --include users,threads --format ndjson"]
213
216
  },
214
217
  {
215
218
  path: ["thread", "get"],
@@ -219,7 +222,7 @@ export const commandMetadata = [
219
222
  scopes: ["channels:history", "groups:history", "im:history", "mpim:history"],
220
223
  safety: "read",
221
224
  output: "thread messages",
222
- examples: ["slk thread get --channel C123 --ts 1710000000.000100 --include users --json"]
225
+ examples: ["agent-slack thread get --channel C123 --ts 1710000000.000100 --include users --json"]
223
226
  },
224
227
  {
225
228
  path: ["message", "get"],
@@ -229,7 +232,7 @@ export const commandMetadata = [
229
232
  scopes: ["channels:history"],
230
233
  safety: "read",
231
234
  output: "message",
232
- examples: ["slk message get --channel C123 --ts 1710000000.000100 --json"]
235
+ examples: ["agent-slack message get --channel C123 --ts 1710000000.000100 --json"]
233
236
  },
234
237
  {
235
238
  path: ["message", "permalink"],
@@ -238,7 +241,7 @@ export const commandMetadata = [
238
241
  methods: ["chat.getPermalink"],
239
242
  safety: "read",
240
243
  output: "permalink",
241
- examples: ["slk message permalink --channel C123 --ts 1710000000.000100 --json"]
244
+ examples: ["agent-slack message permalink --channel C123 --ts 1710000000.000100 --json"]
242
245
  },
243
246
  {
244
247
  path: ["search", "context"],
@@ -248,7 +251,7 @@ export const commandMetadata = [
248
251
  scopes: ["search:read.public", "search:read.private"],
249
252
  safety: "read",
250
253
  output: "search context",
251
- examples: ["slk search context --query 'project atlas' --json"]
254
+ examples: ["agent-slack search context --query 'project atlas' --json"]
252
255
  },
253
256
  {
254
257
  path: ["file", "list"],
@@ -257,7 +260,7 @@ export const commandMetadata = [
257
260
  scopes: ["files:read"],
258
261
  safety: "read",
259
262
  output: "file list",
260
- examples: ["slk file list --channel C123 --json"]
263
+ examples: ["agent-slack file list --channel C123 --json"]
261
264
  },
262
265
  {
263
266
  path: ["file", "download"],
@@ -268,11 +271,12 @@ export const commandMetadata = [
268
271
  scopes: ["files:read"],
269
272
  safety: "read",
270
273
  output: "download status",
271
- examples: ["slk file download F123 --out ./artifact.pdf --json"]
274
+ examples: ["agent-slack file download F123 --out ./artifact.pdf --json"]
272
275
  }
273
276
  ];
274
277
  export const describeAllCommands = () => ({
275
- name: "slk",
278
+ name: PRIMARY_COMMAND_NAME,
279
+ aliases: [SHORT_COMMAND_NAME],
276
280
  version: "0.1.0",
277
281
  commands: commandMetadata
278
282
  });
@@ -285,7 +289,7 @@ export const renderHumanHelp = (path) => {
285
289
  const command = findCommandMetadata(path);
286
290
  if (command !== null) {
287
291
  return [
288
- `slk ${command.path.join(" ")}`,
292
+ `${PRIMARY_COMMAND_NAME} ${command.path.join(" ")}`,
289
293
  "",
290
294
  command.summary,
291
295
  "",
@@ -301,7 +305,7 @@ export const renderHumanHelp = (path) => {
301
305
  }
302
306
  const commands = path.length === 0 ? commandMetadata : commandMetadata.filter((item) => item.path[0] === path[0]);
303
307
  return [
304
- "slk",
308
+ PRIMARY_COMMAND_NAME,
305
309
  "",
306
310
  "Agent-readable Slack CLI.",
307
311
  "",
@@ -313,15 +317,15 @@ export const renderCompletion = (shell) => {
313
317
  const words = [...new Set(commandMetadata.flatMap((command) => command.path))];
314
318
  const commandWords = commandMetadata.map((command) => command.path.join(" "));
315
319
  if (shell === "bash") {
316
- return `complete -W '${words.join(" ")}' slk\n`;
320
+ return `complete -W '${words.join(" ")}' ${COMMAND_NAMES.join(" ")}\n`;
317
321
  }
318
322
  if (shell === "zsh") {
319
323
  return [
320
- "#compdef slk",
321
- "_slk_commands=(",
324
+ `#compdef ${COMMAND_NAMES.join(" ")}`,
325
+ "_agent_slack_commands=(",
322
326
  ...commandWords.map((command) => ` '${command}:${summaryFor(command)}'`),
323
327
  ")",
324
- "_describe 'slk command' _slk_commands",
328
+ `_describe '${PRIMARY_COMMAND_NAME} command' _agent_slack_commands`,
325
329
  ""
326
330
  ].join("\n");
327
331
  }
package/dist/main.js CHANGED
@@ -4,7 +4,10 @@ import { Effect } from "effect";
4
4
  import { createLiveServices } from "./adapters/live.js";
5
5
  import { executeCli } from "./application/execute.js";
6
6
  const program = Effect.promise(async () => {
7
- const result = await executeCli(process.argv.slice(2), createLiveServices());
7
+ const result = await executeCli(process.argv.slice(2), createLiveServices(), {
8
+ stdoutIsTty: process.stdout.isTTY === true,
9
+ env: process.env
10
+ });
8
11
  if (result.stdout.length > 0) {
9
12
  process.stdout.write(result.stdout);
10
13
  }
@@ -44,7 +44,7 @@ export const errorEnvelope = (error) => {
44
44
  const suggestionFor = (tag) => {
45
45
  switch (tag) {
46
46
  case "NotAuthenticated":
47
- return "Run slk auth login or provide a seeded profile for tests.";
47
+ return "Run agent-slack auth login or provide a seeded profile for tests.";
48
48
  case "MissingScope":
49
49
  return "Reinstall or reauthorize the Slack app with the missing scope.";
50
50
  case "SlackRateLimited":
@@ -0,0 +1,289 @@
1
+ const codes = {
2
+ bold: [1, 22],
3
+ cyan: [36, 39],
4
+ dim: [2, 22],
5
+ green: [32, 39],
6
+ yellow: [33, 39]
7
+ };
8
+ export const renderHumanEnvelope = (envelope, options) => {
9
+ const paint = (name, value) => options.color ? `\u001b[${codes[name][0]}m${value}\u001b[${codes[name][1]}m` : value;
10
+ if (isCommandCatalog(envelope.data)) {
11
+ return `${renderCommandCatalog(envelope.data, paint).join("\n")}\n`;
12
+ }
13
+ const metadata = renderMetadata(envelope, paint);
14
+ const warnings = envelope.warnings.map((warning) => `${paint("yellow", "Warning")}: ${warning}`);
15
+ const data = renderHumanData(envelope.data, paint);
16
+ const lines = [
17
+ `${paint("green", "OK")} ${paint("bold", envelope.method)}`,
18
+ ...(metadata === "" ? [] : [metadata]),
19
+ ...(warnings.length === 0 ? [] : warnings),
20
+ ...(data.length === 0 ? [] : ["", ...data])
21
+ ];
22
+ return `${lines.join("\n")}\n`;
23
+ };
24
+ const isCommandCatalog = (value) => {
25
+ if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.commands)) {
26
+ return false;
27
+ }
28
+ return value.commands.every((command) => isRecord(command) &&
29
+ Array.isArray(command.path) &&
30
+ command.path.every((part) => typeof part === "string") &&
31
+ typeof command.summary === "string");
32
+ };
33
+ const renderCommandCatalog = (catalog, paint) => {
34
+ const title = catalog.version === undefined ? `${titleCase(catalog.name)} Commands` : "Agent Slack";
35
+ const aliases = catalog.aliases?.join(", ");
36
+ const groups = groupCommands(catalog.commands);
37
+ return [
38
+ `${paint("bold", title)}${catalog.version === undefined ? "" : ` ${paint("dim", catalog.version)}`}`,
39
+ paint("dim", "Slack context CLI for agents"),
40
+ "",
41
+ `${paint("dim", "Use")} ${paint("cyan", `${catalog.name} <command> [--json]`)}`,
42
+ ...(aliases === undefined ? [] : [`${paint("dim", "Alias")} ${aliases}`]),
43
+ `${paint("dim", "Output")} human in terminals, JSON when piped or with --json`,
44
+ "",
45
+ ...groups.flatMap((group) => renderCommandGroup(group, paint)),
46
+ "",
47
+ paint("dim", `Run ${catalog.name} <command> --help for details.`)
48
+ ];
49
+ };
50
+ const renderCommandGroup = (group, paint) => {
51
+ const width = Math.min(34, Math.max(...group.commands.map((command) => commandSignature(command).length)));
52
+ return [
53
+ paint("yellow", group.title),
54
+ ...group.commands.map((command) => {
55
+ const signature = pad(commandSignature(command), width);
56
+ const safety = command.safety === "destructive"
57
+ ? ` ${paint("yellow", "[destructive]")}`
58
+ : command.safety === "unknown"
59
+ ? ` ${paint("yellow", "[review]")}`
60
+ : "";
61
+ return ` ${paint("cyan", signature)} ${command.summary}${safety}`;
62
+ }),
63
+ ""
64
+ ];
65
+ };
66
+ const groupCommands = (commands) => {
67
+ const groups = new Map();
68
+ for (const command of commands) {
69
+ const key = groupKey(command);
70
+ groups.set(key, [...(groups.get(key) ?? []), command]);
71
+ }
72
+ return commandGroupOrder
73
+ .map((group) => ({
74
+ ...group,
75
+ commands: groups.get(group.key) ?? []
76
+ }))
77
+ .filter((group) => group.commands.length > 0);
78
+ };
79
+ const commandGroupOrder = [
80
+ { key: "system", title: "System" },
81
+ { key: "auth", title: "Auth" },
82
+ { key: "workspace", title: "Workspace" },
83
+ { key: "people", title: "People" },
84
+ { key: "conversations", title: "Conversations" },
85
+ { key: "search", title: "Search" },
86
+ { key: "files", title: "Files" },
87
+ { key: "surfaces", title: "Slack Objects" },
88
+ { key: "api", title: "Web API" }
89
+ ];
90
+ const groupKey = (command) => {
91
+ const root = command.path[0];
92
+ if (root === "describe" || root === "completion") {
93
+ return "system";
94
+ }
95
+ if (root === "team" || root === "enterprise") {
96
+ return "workspace";
97
+ }
98
+ if (root === "user" || root === "usergroups") {
99
+ return "people";
100
+ }
101
+ if (root === "conversation" || root === "thread" || root === "message") {
102
+ return "conversations";
103
+ }
104
+ if (root === "file") {
105
+ return "files";
106
+ }
107
+ if (root === "reaction" || root === "pin" || root === "bookmark" || root === "emoji" || root === "dnd") {
108
+ return "surfaces";
109
+ }
110
+ return root ?? "system";
111
+ };
112
+ const commandSignature = (command) => {
113
+ const args = command.args?.filter((arg) => arg !== "PAYLOAD_STDIN_MARKER").join(" ");
114
+ return [command.path.join(" "), args].filter((part) => part !== undefined && part !== "").join(" ");
115
+ };
116
+ const renderMetadata = (envelope, paint) => {
117
+ const parts = [
118
+ envelope.profile === null ? undefined : `profile ${envelope.profile}`,
119
+ envelope.team_id === null ? undefined : `team ${envelope.team_id}`,
120
+ envelope.enterprise_id === null ? undefined : `enterprise ${envelope.enterprise_id}`,
121
+ envelope.paging.has_more ? `next ${envelope.paging.next_cursor ?? "cursor"}` : undefined
122
+ ].filter((part) => part !== undefined);
123
+ return parts.length === 0 ? "" : paint("dim", parts.join(" | "));
124
+ };
125
+ const renderHumanData = (value, paint) => {
126
+ if (Array.isArray(value)) {
127
+ return renderArray(value, paint);
128
+ }
129
+ if (isRecord(value)) {
130
+ const primaryArray = findPrimaryArray(value);
131
+ const excludedKeys = new Set(primaryArray === null ? [] : [primaryArray.key]);
132
+ const fieldLines = renderFields(value, excludedKeys, paint);
133
+ const hasComplexValues = hasVisibleComplexValues(value, excludedKeys);
134
+ if (primaryArray !== null) {
135
+ return [
136
+ ...fieldLines,
137
+ fieldLines.length === 0 ? "" : "",
138
+ `${paint("cyan", labelFor(primaryArray.key))} (${primaryArray.items.length})`,
139
+ ...renderArray(primaryArray.items, paint)
140
+ ].filter((line) => line !== "");
141
+ }
142
+ if (fieldLines.length > 0 && !hasComplexValues) {
143
+ return fieldLines;
144
+ }
145
+ }
146
+ return ["data:", indent(JSON.stringify(value, null, 2))];
147
+ };
148
+ const findPrimaryArray = (record) => {
149
+ const preferred = ["messages", "members", "channels", "files", "users", "results", "items"];
150
+ for (const key of preferred) {
151
+ const value = record[key];
152
+ if (Array.isArray(value) && !value.every(isScalar)) {
153
+ return { key, items: value };
154
+ }
155
+ }
156
+ for (const [key, value] of Object.entries(record)) {
157
+ if (Array.isArray(value) && !value.every(isScalar)) {
158
+ return { key, items: value };
159
+ }
160
+ }
161
+ return null;
162
+ };
163
+ const renderFields = (record, excludedKeys, paint) => {
164
+ const entries = Object.entries(record)
165
+ .filter(([key, value]) => !excludedKeys.has(key) && shouldDisplayField(key, value) && isHumanFieldValue(value))
166
+ .map(([key, value]) => ({ key, label: labelFor(key), value }))
167
+ .sort((left, right) => fieldOrder(left.key) - fieldOrder(right.key));
168
+ if (entries.length === 0) {
169
+ return [];
170
+ }
171
+ const labelWidth = Math.max(...entries.map((entry) => entry.label.length));
172
+ return entries.map((entry) => `${paint("dim", pad(entry.label, labelWidth))} ${formatHumanValue(entry.value)}`);
173
+ };
174
+ const hasVisibleComplexValues = (record, excludedKeys) => Object.entries(record).some(([key, value]) => !excludedKeys.has(key) && shouldDisplayField(key, value) && !isHumanFieldValue(value));
175
+ const renderArray = (values, paint) => {
176
+ if (values.length === 0) {
177
+ return ["(empty)"];
178
+ }
179
+ if (!values.every(isRecord)) {
180
+ return [indent(JSON.stringify(values, null, 2))];
181
+ }
182
+ const rows = values.map((value) => value);
183
+ const columns = columnsFor(rows);
184
+ if (columns.length === 0) {
185
+ return [indent(JSON.stringify(values, null, 2))];
186
+ }
187
+ const renderedRows = rows.slice(0, 20).map((row) => columns.map((column) => truncate(formatScalar(row[column]), widthFor(column))));
188
+ const widths = columns.map((column, index) => Math.max(column.length, ...renderedRows.map((row) => row[index]?.length ?? 0)));
189
+ const header = columns.map((column, index) => pad(truncate(column, widthFor(column)), widths[index] ?? column.length)).join(" ");
190
+ const divider = widths.map((width) => "-".repeat(width)).join(" ");
191
+ const body = renderedRows.map((row) => row.map((cell, index) => pad(cell, widths[index] ?? cell.length)).join(" "));
192
+ const hiddenCount = values.length - renderedRows.length;
193
+ return [
194
+ paint("dim", header),
195
+ paint("dim", divider),
196
+ ...body,
197
+ hiddenCount > 0 ? paint("dim", `... ${hiddenCount} more`) : ""
198
+ ].filter((line) => line !== "");
199
+ };
200
+ const columnsFor = (rows) => {
201
+ const preferred = ["id", "channel", "ts", "user", "name", "real_name", "type", "subtype", "text"];
202
+ const discovered = new Set(rows.flatMap((row) => Object.keys(row).filter((key) => isScalar(row[key]))));
203
+ const ordered = preferred.filter((key) => discovered.has(key));
204
+ for (const key of discovered) {
205
+ if (!ordered.includes(key)) {
206
+ ordered.push(key);
207
+ }
208
+ if (ordered.length >= 5) {
209
+ break;
210
+ }
211
+ }
212
+ return ordered.slice(0, 5);
213
+ };
214
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
215
+ const isScalar = (value) => value === null || ["boolean", "number", "string"].includes(typeof value);
216
+ const isHumanFieldValue = (value) => isScalar(value) || (Array.isArray(value) && value.every(isScalar));
217
+ const shouldDisplayField = (key, value) => {
218
+ if (key === "ok" && value === true) {
219
+ return false;
220
+ }
221
+ if (value === null || value === undefined) {
222
+ return false;
223
+ }
224
+ if (/^has[A-Z].*Token$/.test(key) && value === false) {
225
+ return false;
226
+ }
227
+ return true;
228
+ };
229
+ const labelOverrides = {
230
+ botId: "Bot",
231
+ botToken: "Bot token",
232
+ channelId: "Channel",
233
+ enterpriseId: "Enterprise",
234
+ hasAdminToken: "Admin token",
235
+ hasAppToken: "App token",
236
+ hasBotToken: "Bot token",
237
+ hasUserToken: "User token",
238
+ messageTs: "Message TS",
239
+ name: "Name",
240
+ profile: "Profile",
241
+ scopes: "Scopes",
242
+ teamId: "Team",
243
+ tokenType: "Token",
244
+ userId: "User"
245
+ };
246
+ const orderedFields = [
247
+ "name",
248
+ "teamId",
249
+ "enterpriseId",
250
+ "userId",
251
+ "botId",
252
+ "tokenType",
253
+ "hasBotToken",
254
+ "hasUserToken",
255
+ "hasAdminToken",
256
+ "hasAppToken",
257
+ "scopes"
258
+ ];
259
+ const fieldOrder = (key) => {
260
+ const index = orderedFields.indexOf(key);
261
+ return index === -1 ? orderedFields.length : index;
262
+ };
263
+ const labelFor = (key) => labelOverrides[key] ?? titleCase(key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/_/g, " "));
264
+ const titleCase = (value) => value.split(" ").map((word) => word.toLowerCase() === "id" ? "ID" : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
265
+ const formatHumanValue = (value) => {
266
+ if (Array.isArray(value)) {
267
+ return value.length === 0 ? "(none)" : value.map(formatScalar).join(", ");
268
+ }
269
+ if (typeof value === "boolean") {
270
+ return value ? "yes" : "no";
271
+ }
272
+ return formatScalar(value);
273
+ };
274
+ const formatScalar = (value) => {
275
+ if (value === undefined) {
276
+ return "";
277
+ }
278
+ if (value === null) {
279
+ return "-";
280
+ }
281
+ if (typeof value === "string") {
282
+ return value;
283
+ }
284
+ return String(value);
285
+ };
286
+ const indent = (value) => value.split("\n").map((line) => ` ${line}`).join("\n");
287
+ const pad = (value, width) => value + " ".repeat(Math.max(0, width - value.length));
288
+ const widthFor = (column) => column === "text" ? 72 : 24;
289
+ const truncate = (value, width) => value.length > width ? `${value.slice(0, Math.max(0, width - 3))}...` : value;
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@eliya-oss/agent-slack",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Slack context CLI for AI agents.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.9.0",
7
7
  "bin": {
8
- "slk": "dist/main.js"
8
+ "agent-slack": "dist/main.js",
9
+ "aslk": "dist/main.js"
9
10
  },
10
11
  "files": [
11
12
  "dist",
@@ -24,7 +25,8 @@
24
25
  "dev": "tsx src/main.ts",
25
26
  "prepack": "pnpm build",
26
27
  "release:check": "pnpm check && npm pack --dry-run",
27
- "slk": "tsx src/main.ts",
28
+ "agent-slack": "tsx src/main.ts",
29
+ "aslk": "tsx src/main.ts",
28
30
  "typecheck": "tsc -p tsconfig.json --noEmit",
29
31
  "test": "vitest run",
30
32
  "test:coverage": "vitest run --coverage",
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: agent-slack
3
- description: "Use when Codex needs Slack context through the `slk` CLI: authenticate, inspect profiles/scopes, read channels, messages, threads, files, users, search context, or call Slack Web API methods with agent-readable JSON or NDJSON output."
3
+ description: "Use when Codex needs Slack context through the `agent-slack` CLI: authenticate, inspect profiles/scopes, read channels, messages, threads, files, users, search context, or call Slack Web API methods with agent-readable JSON or NDJSON output."
4
4
  ---
5
5
 
6
- # slk CLI
6
+ # agent-slack CLI
7
7
 
8
- Use `slk` as the Slack context boundary. Prefer read-only commands and preserve Slack's permission model: the CLI can only return data allowed by the active token, scopes, channel membership, workspace policy, and Slack plan.
8
+ Use `agent-slack` as the Slack context boundary. `aslk` is the short alias. Prefer read-only commands and preserve Slack's permission model: the CLI can only return data allowed by the active token, scopes, channel membership, workspace policy, and Slack plan.
9
9
 
10
10
  ## Workflow
11
11
 
12
- 1. Check availability: `slk describe --json`.
13
- 2. Check auth: `slk auth status --json`.
12
+ 1. Check availability: `agent-slack describe --json`.
13
+ 2. Check auth: `agent-slack auth status --json`.
14
14
  3. If auth is missing, ask the user to run OAuth or provide a seeded profile; do not invent credentials.
15
15
  4. Use specific read commands before raw API calls.
16
16
  5. Use `--json` for bounded results and `--format ndjson` for large context streams.
@@ -19,17 +19,17 @@ Use `slk` as the Slack context boundary. Prefer read-only commands and preserve
19
19
  ## Common Commands
20
20
 
21
21
  ```bash
22
- slk conversation history C123 --limit 50 --json
23
- slk thread get --channel C123 --ts 1710000000.000100 --include users,permalinks --json
24
- slk conversation context C123 --include users,threads,permalinks --format ndjson
25
- slk file download F123 --out ./artifact.bin --json
26
- slk user get U123 --json
27
- slk api call conversations.info --payload '{"channel":"C123"}' --json
22
+ agent-slack conversation history C123 --limit 50 --json
23
+ agent-slack thread get --channel C123 --ts 1710000000.000100 --include users,permalinks --json
24
+ agent-slack conversation context C123 --include users,threads,permalinks --format ndjson
25
+ agent-slack file download F123 --out ./artifact.bin --json
26
+ agent-slack user get U123 --json
27
+ agent-slack api call conversations.info --payload '{"channel":"C123"}' --json
28
28
  ```
29
29
 
30
30
  ## Safety
31
31
 
32
- - Treat `slk api call` as the escape hatch for missing wrappers.
32
+ - Treat `agent-slack api call` as the escape hatch for missing wrappers.
33
33
  - Do not pass write/admin-mutating Slack methods unless the user explicitly asks for mutation and accepts `--allow-write --yes`.
34
34
  - Never print token values. `auth status` reports token presence and scopes without secrets.
35
- - For local repo development, use `pnpm slk -- ...` from the repository root instead of global `slk`.
35
+ - For local repo development, use `pnpm agent-slack -- ...` from the repository root instead of global `agent-slack`.
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Slack CLI"
3
- short_description: "Use slk to retrieve Slack context"
4
- default_prompt: "Use $agent-slack to retrieve Slack channel or thread context with the slk CLI."
3
+ short_description: "Use agent-slack to retrieve Slack context"
4
+ default_prompt: "Use $agent-slack to retrieve Slack channel or thread context with the agent-slack CLI."