@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.
- package/.agents/cli-api.md +65 -65
- package/CHANGELOG.md +6 -0
- package/README.md +7 -6
- package/assets/terminal.svg +8 -8
- package/dist/adapters/keychain/KeychainTokenStore.js +1 -1
- package/dist/adapters/live.js +11 -7
- package/dist/adapters/localhost-oauth/NodeLocalhostOAuthFlow.js +9 -6
- package/dist/adapters/profile-file/FileTokenStore.js +3 -3
- package/dist/application/commands.js +18 -8
- package/dist/application/execute.js +2 -2
- package/dist/cli/args.js +1 -0
- package/dist/cli/metadata.js +42 -38
- package/dist/main.js +4 -1
- package/dist/output/envelope.js +1 -1
- package/dist/output/human.js +289 -0
- package/package.json +5 -3
- package/skills/agent-slack/SKILL.md +13 -13
- package/skills/agent-slack/agents/openai.yaml +2 -2
package/.agents/cli-api.md
CHANGED
|
@@ -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: `
|
|
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: `
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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/
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
cat payload.json |
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
+
agent-slack api call some.newMethod --payload @request.json --json
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
## Build Order
|
package/CHANGELOG.md
CHANGED
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 `
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
package/assets/terminal.svg
CHANGED
|
@@ -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">
|
|
17
|
-
<text x="78" y="196" fill="#
|
|
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">
|
|
21
|
-
<text x="78" y="284" fill="#FDE68A" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">
|
|
22
|
-
<text x="78" y="314" fill="#FDE68A" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="16">
|
|
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">
|
|
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">
|
|
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"
|
|
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", "
|
|
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) {
|
package/dist/adapters/live.js
CHANGED
|
@@ -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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
...(
|
|
18
|
-
...(
|
|
19
|
-
...(
|
|
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", "
|
|
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] ??
|
|
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
|
|
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
|
-
|
|
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
package/dist/cli/metadata.js
CHANGED
|
@@ -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: ["
|
|
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: ["
|
|
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: ["
|
|
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
|
-
"
|
|
34
|
-
"
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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
|
-
"
|
|
165
|
-
"cat payload.json |
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
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: ["
|
|
274
|
+
examples: ["agent-slack file download F123 --out ./artifact.pdf --json"]
|
|
272
275
|
}
|
|
273
276
|
];
|
|
274
277
|
export const describeAllCommands = () => ({
|
|
275
|
-
name:
|
|
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
|
-
|
|
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
|
-
|
|
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(" ")}'
|
|
320
|
+
return `complete -W '${words.join(" ")}' ${COMMAND_NAMES.join(" ")}\n`;
|
|
317
321
|
}
|
|
318
322
|
if (shell === "zsh") {
|
|
319
323
|
return [
|
|
320
|
-
|
|
321
|
-
"
|
|
324
|
+
`#compdef ${COMMAND_NAMES.join(" ")}`,
|
|
325
|
+
"_agent_slack_commands=(",
|
|
322
326
|
...commandWords.map((command) => ` '${command}:${summaryFor(command)}'`),
|
|
323
327
|
")",
|
|
324
|
-
|
|
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
|
}
|
package/dist/output/envelope.js
CHANGED
|
@@ -44,7 +44,7 @@ export const errorEnvelope = (error) => {
|
|
|
44
44
|
const suggestionFor = (tag) => {
|
|
45
45
|
switch (tag) {
|
|
46
46
|
case "NotAuthenticated":
|
|
47
|
-
return "Run
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 `
|
|
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
|
-
#
|
|
6
|
+
# agent-slack CLI
|
|
7
7
|
|
|
8
|
-
Use `
|
|
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: `
|
|
13
|
-
2. Check auth: `
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 `
|
|
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
|
|
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
|
|
4
|
-
default_prompt: "Use $agent-slack to retrieve Slack channel or thread context with the
|
|
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."
|