@eliya-oss/agent-slack 0.1.3 → 0.1.4
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 +8 -3
- package/CHANGELOG.md +8 -0
- package/README.md +26 -3
- package/assets/terminal.webp +0 -0
- package/dist/adapters/localhost-oauth/NodeLocalhostOAuthFlow.js +43 -18
- package/dist/application/commands.js +14 -7
- package/dist/application/execute.js +16 -3
- package/dist/cli/metadata.js +43 -6
- package/dist/output/envelope.js +5 -3
- package/dist/output/human.js +15 -0
- package/package.json +1 -1
- package/skills/agent-slack/SKILL.md +31 -1
- package/assets/terminal.svg +0 -32
package/.agents/cli-api.md
CHANGED
|
@@ -38,6 +38,7 @@ Defaults:
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
agent-slack auth login
|
|
41
|
+
agent-slack auth login --token xoxb-... --scopes channels:read,channels:history,users:read
|
|
41
42
|
agent-slack auth login --profile work --scopes channels:read,channels:history --user-scopes search:read.public,search:read.private
|
|
42
43
|
agent-slack auth login --oauth --client-id CLIENT_ID --client-secret CLIENT_SECRET --auth-url-out /tmp/agent-slack-auth-url
|
|
43
44
|
agent-slack auth login --oauth --client-id CLIENT_ID --client-secret CLIENT_SECRET --no-open
|
|
@@ -49,9 +50,13 @@ agent-slack auth logout --profile work
|
|
|
49
50
|
|
|
50
51
|
Auth behavior:
|
|
51
52
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
53
|
+
- Primary setup: `agent-slack auth login` uses the bundled Agent Slack public Client ID, opens Slack in the browser with PKCE, and stores a local user-token profile.
|
|
54
|
+
- Users can also provide an existing Bot User OAuth Token with `--token`.
|
|
55
|
+
- A normal user should not create a Slack app or provide client credentials.
|
|
56
|
+
- Developer fallback: OAuth mode can use BYO Slack app credentials. The **Client ID** and **Client Secret** come from the Slack app's **Basic Information -> App Credentials** section.
|
|
57
|
+
- OAuth opens the Slack OAuth URL in the default browser by default. Use `--no-open` to print the URL only, or `--auth-url-out PATH` for headless agents and tests.
|
|
58
|
+
- PKCE uses `user_scope=...`; BYO OAuth bot scopes go in `scope=...`.
|
|
59
|
+
- Default local callback: `http://localhost:45454/oauth/slack/callback`.
|
|
55
60
|
- 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.
|
|
56
61
|
- Supports multiple profiles and workspaces.
|
|
57
62
|
- `auth status --json` returns token type, team, enterprise, user/bot identity, granted scopes, and missing recommended scopes.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @eliya-oss/agent-slack
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add Slack PKCE browser login.
|
|
8
|
+
|
|
9
|
+
No-flag `auth login` now uses the bundled Agent Slack public Client ID with Slack OAuth PKCE for browser-based user-token login without a client secret. Token setup and BYO OAuth remain available for bot-token, development, and self-hosted workflows.
|
|
10
|
+
|
|
3
11
|
## 0.1.3
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -9,21 +9,44 @@ Short alias: `aslk`.
|
|
|
9
9
|
npm install -g @eliya-oss/agent-slack
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
<img src="./assets/terminal.
|
|
12
|
+
<img src="./assets/terminal.webp" alt="Agent Slack terminal screenshot" width="820">
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
agent-slack auth login --
|
|
19
|
+
agent-slack auth login --json
|
|
20
20
|
agent-slack conversation history C123 --limit 50 --json
|
|
21
21
|
agent-slack thread get --channel C123 --ts 1710000000.000100 --include users,permalinks --json
|
|
22
22
|
agent-slack conversation context C123 --include users,threads,permalinks --format ndjson
|
|
23
23
|
agent-slack api call conversations.info --payload '{"channel":"C123"}' --json
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## Auth
|
|
27
|
+
|
|
28
|
+
Browser login:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
agent-slack auth login
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Agent Slack opens Slack in the browser with PKCE and stores a local user-token profile. Users do not create Slack apps or handle Slack client secrets.
|
|
35
|
+
|
|
36
|
+
Token setup:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
agent-slack auth login --token "$SLACK_BOT_TOKEN" --scopes channels:read,channels:history,users:read
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Developer/self-hosted fallback:
|
|
43
|
+
|
|
44
|
+
1. Create a Slack app at <https://api.slack.com/apps>.
|
|
45
|
+
2. Add the needed scopes under **OAuth & Permissions**.
|
|
46
|
+
3. Install it to your workspace and copy the **Bot User OAuth Token**.
|
|
47
|
+
4. Run `agent-slack auth login --token "$SLACK_BOT_TOKEN" --scopes ...`.
|
|
48
|
+
|
|
49
|
+
OAuth with `--client-id` and `--client-secret` is only for bring-your-own-app development and self-hosted setups.
|
|
27
50
|
|
|
28
51
|
## Skill
|
|
29
52
|
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { writeFile } from "node:fs/promises";
|
|
4
4
|
import { createServer } from "node:http";
|
|
@@ -13,22 +13,26 @@ export class NodeLocalhostOAuthFlow {
|
|
|
13
13
|
const apiBaseUrl = env.AGENT_SLACK_API_BASE_URL ?? env.SLK_SLACK_API_BASE_URL;
|
|
14
14
|
const emulatorBaseUrl = apiBaseUrl?.replace(/\/api\/?$/, "");
|
|
15
15
|
const redirectHost = env.AGENT_SLACK_OAUTH_REDIRECT_HOST ?? env.SLK_OAUTH_REDIRECT_HOST;
|
|
16
|
+
const redirectPort = env.AGENT_SLACK_OAUTH_REDIRECT_PORT ?? env.SLK_OAUTH_REDIRECT_PORT;
|
|
16
17
|
const redirectPath = env.AGENT_SLACK_OAUTH_REDIRECT_PATH ?? env.SLK_OAUTH_REDIRECT_PATH;
|
|
17
18
|
const timeoutMs = env.AGENT_SLACK_OAUTH_TIMEOUT_MS ?? env.SLK_OAUTH_TIMEOUT_MS;
|
|
18
19
|
return new NodeLocalhostOAuthFlow({
|
|
19
20
|
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`),
|
|
20
21
|
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`),
|
|
21
22
|
...(redirectHost === undefined ? {} : { defaultRedirectHost: redirectHost }),
|
|
23
|
+
...(redirectPort === undefined ? {} : { defaultRedirectPort: Number(redirectPort) }),
|
|
22
24
|
...(redirectPath === undefined ? {} : { defaultRedirectPath: redirectPath }),
|
|
23
25
|
...(timeoutMs === undefined ? {} : { defaultTimeoutMs: Number(timeoutMs) })
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
async login(input) {
|
|
27
29
|
const state = randomBytes(24).toString("base64url");
|
|
30
|
+
const codeVerifier = input.pkce === true ? randomBytes(32).toString("base64url") : undefined;
|
|
28
31
|
const timeoutMs = input.timeoutMs ?? this.options.defaultTimeoutMs ?? 120_000;
|
|
29
32
|
const server = createServer();
|
|
30
33
|
const started = await listen(server, input.redirectUri, {
|
|
31
|
-
host: this.options.defaultRedirectHost ??
|
|
34
|
+
host: this.options.defaultRedirectHost ?? defaultRedirectHost,
|
|
35
|
+
port: this.options.defaultRedirectPort ?? defaultRedirectPort,
|
|
32
36
|
path: this.options.defaultRedirectPath ?? "/oauth/slack/callback"
|
|
33
37
|
});
|
|
34
38
|
const authorizationUrl = buildAuthorizationUrl({
|
|
@@ -37,7 +41,8 @@ export class NodeLocalhostOAuthFlow {
|
|
|
37
41
|
redirectUri: started.redirectUri,
|
|
38
42
|
scopes: input.scopes,
|
|
39
43
|
userScopes: input.userScopes,
|
|
40
|
-
state
|
|
44
|
+
state,
|
|
45
|
+
...(codeVerifier === undefined ? {} : { codeChallenge: codeChallengeFor(codeVerifier) })
|
|
41
46
|
});
|
|
42
47
|
if (input.authUrlOut !== undefined) {
|
|
43
48
|
await writeFile(input.authUrlOut, authorizationUrl, "utf8");
|
|
@@ -79,9 +84,10 @@ export class NodeLocalhostOAuthFlow {
|
|
|
79
84
|
code,
|
|
80
85
|
clientId: input.clientId,
|
|
81
86
|
clientSecret: input.clientSecret,
|
|
87
|
+
codeVerifier,
|
|
82
88
|
redirectUri: started.redirectUri
|
|
83
89
|
});
|
|
84
|
-
const profile = toAuthProfile(input.profileName, access);
|
|
90
|
+
const profile = toAuthProfile(input.profileName, access, input.pkce === true ? "user" : "bot");
|
|
85
91
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end("<html><body>Slack auth complete. You can close this tab.</body></html>");
|
|
86
92
|
clearTimeout(timer);
|
|
87
93
|
closeServer(server);
|
|
@@ -101,7 +107,7 @@ const listen = (server, redirectUri, fallback) => new Promise((resolve, reject)
|
|
|
101
107
|
const parsed = redirectUri === undefined ? null : new URL(redirectUri);
|
|
102
108
|
const host = parsed?.hostname ?? fallback.host;
|
|
103
109
|
const path = parsed?.pathname ?? fallback.path;
|
|
104
|
-
const port = parsed === null || parsed.port === "" ?
|
|
110
|
+
const port = parsed === null || parsed.port === "" ? fallback.port : Number(parsed.port);
|
|
105
111
|
if (!Number.isInteger(port) || port < 0) {
|
|
106
112
|
reject(new UsageError("OAuth redirect URI must include a valid port", { redirectUri }));
|
|
107
113
|
return;
|
|
@@ -109,34 +115,48 @@ const listen = (server, redirectUri, fallback) => new Promise((resolve, reject)
|
|
|
109
115
|
server.once("error", reject);
|
|
110
116
|
server.listen(port, host, () => {
|
|
111
117
|
const address = server.address();
|
|
112
|
-
const actualPort = parsed === null || parsed.port === "" ?
|
|
118
|
+
const actualPort = parsed === null || parsed.port === "" ? port : address.port;
|
|
113
119
|
resolve({
|
|
114
120
|
redirectUri: redirectUri ?? `http://${host}:${actualPort}${path}`,
|
|
115
121
|
path
|
|
116
122
|
});
|
|
117
123
|
});
|
|
118
124
|
});
|
|
125
|
+
const defaultRedirectHost = "localhost";
|
|
126
|
+
const defaultRedirectPort = 45454;
|
|
119
127
|
const buildAuthorizationUrl = (input) => {
|
|
120
128
|
const url = new URL(input.authorizeUrl);
|
|
121
129
|
url.searchParams.set("client_id", input.clientId);
|
|
122
|
-
|
|
130
|
+
if (input.scopes.length > 0) {
|
|
131
|
+
url.searchParams.set("scope", input.scopes.join(","));
|
|
132
|
+
}
|
|
123
133
|
if (input.userScopes.length > 0) {
|
|
124
134
|
url.searchParams.set("user_scope", input.userScopes.join(","));
|
|
125
135
|
}
|
|
126
136
|
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
127
137
|
url.searchParams.set("state", input.state);
|
|
138
|
+
if (input.codeChallenge !== undefined) {
|
|
139
|
+
url.searchParams.set("code_challenge", input.codeChallenge);
|
|
140
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
141
|
+
}
|
|
128
142
|
return url.toString();
|
|
129
143
|
};
|
|
130
144
|
const exchangeCode = async (input) => {
|
|
145
|
+
const requestBody = new URLSearchParams({
|
|
146
|
+
code: input.code,
|
|
147
|
+
client_id: input.clientId,
|
|
148
|
+
redirect_uri: input.redirectUri
|
|
149
|
+
});
|
|
150
|
+
if (input.clientSecret !== undefined) {
|
|
151
|
+
requestBody.set("client_secret", input.clientSecret);
|
|
152
|
+
}
|
|
153
|
+
if (input.codeVerifier !== undefined) {
|
|
154
|
+
requestBody.set("code_verifier", input.codeVerifier);
|
|
155
|
+
}
|
|
131
156
|
const response = await fetch(input.tokenUrl, {
|
|
132
157
|
method: "POST",
|
|
133
158
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
134
|
-
body:
|
|
135
|
-
code: input.code,
|
|
136
|
-
client_id: input.clientId,
|
|
137
|
-
client_secret: input.clientSecret,
|
|
138
|
-
redirect_uri: input.redirectUri
|
|
139
|
-
})
|
|
159
|
+
body: requestBody
|
|
140
160
|
});
|
|
141
161
|
const body = await response.json();
|
|
142
162
|
if (body.ok === false) {
|
|
@@ -149,21 +169,26 @@ const exchangeCode = async (input) => {
|
|
|
149
169
|
}
|
|
150
170
|
return body;
|
|
151
171
|
};
|
|
152
|
-
const toAuthProfile = (profileName, response) => {
|
|
172
|
+
const toAuthProfile = (profileName, response, preferredTokenType) => {
|
|
153
173
|
const botScopes = splitScopes(response.scope);
|
|
154
174
|
const userScopes = splitScopes(response.authed_user?.scope);
|
|
175
|
+
const rootTokenIsUser = preferredTokenType === "user" && response.access_token !== undefined && response.token_type !== "bot";
|
|
176
|
+
const userToken = response.authed_user?.access_token ?? (rootTokenIsUser ? response.access_token : undefined);
|
|
177
|
+
const useUserToken = preferredTokenType === "user" && userToken !== undefined;
|
|
178
|
+
const scopes = useUserToken && userScopes.length > 0 ? userScopes : [...botScopes, ...userScopes];
|
|
155
179
|
return {
|
|
156
180
|
name: ProfileName.make(profileName),
|
|
157
|
-
tokenType: response.access_token === undefined ? "user" : "bot",
|
|
181
|
+
tokenType: useUserToken || response.access_token === undefined ? "user" : "bot",
|
|
158
182
|
enterpriseId: response.enterprise?.id ?? null,
|
|
159
183
|
...(response.team?.id === undefined ? {} : { teamId: response.team.id }),
|
|
160
184
|
...(response.authed_user?.id === undefined ? {} : { userId: response.authed_user.id }),
|
|
161
185
|
...(response.bot_user_id === undefined ? {} : { botId: response.bot_user_id }),
|
|
162
|
-
...(response.access_token === undefined ? {} : { botToken: response.access_token }),
|
|
163
|
-
...(
|
|
164
|
-
scopes: [...new Set(
|
|
186
|
+
...(response.access_token === undefined || useUserToken ? {} : { botToken: response.access_token }),
|
|
187
|
+
...(userToken === undefined ? {} : { userToken }),
|
|
188
|
+
scopes: [...new Set(scopes)].map(Scope.make)
|
|
165
189
|
};
|
|
166
190
|
};
|
|
191
|
+
const codeChallengeFor = (codeVerifier) => createHash("sha256").update(codeVerifier).digest("base64url");
|
|
167
192
|
const splitScopes = (value) => value === undefined || value.trim() === "" ? [] : value.split(",").map((scope) => scope.trim()).filter(Boolean);
|
|
168
193
|
const closeServer = (server) => {
|
|
169
194
|
server.close();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { flagBoolean, flagString, requireFlag, splitCsv } from "../cli/args.js";
|
|
2
2
|
import { PRIMARY_COMMAND_NAME, describeAllCommands, describeCommandGroup, findCommandMetadata, renderCompletion, renderHumanHelp } from "../cli/metadata.js";
|
|
3
|
-
import {
|
|
3
|
+
import { 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
6
|
import { renderHumanEnvelope } from "../output/human.js";
|
|
@@ -196,18 +196,23 @@ const authCommand = async (parsed, services) => {
|
|
|
196
196
|
await services.tokenStore.setProfile(profile);
|
|
197
197
|
return { method: "auth.login", profile, stdoutValue: sanitizeProfile(profile) };
|
|
198
198
|
}
|
|
199
|
-
const clientId = flagString(parsed, "client-id") ?? process.env.AGENT_SLACK_CLIENT_ID ?? process.env.SLK_CLIENT_ID ?? process.env.SLACK_CLIENT_ID;
|
|
199
|
+
const clientId = flagString(parsed, "client-id") ?? process.env.AGENT_SLACK_CLIENT_ID ?? process.env.AGENT_SLACK_PUBLIC_CLIENT_ID ?? process.env.SLK_CLIENT_ID ?? process.env.SLACK_CLIENT_ID ?? defaultPkceClientId;
|
|
200
200
|
const clientSecret = flagString(parsed, "client-secret") ?? process.env.AGENT_SLACK_CLIENT_SECRET ?? process.env.SLK_CLIENT_SECRET ?? process.env.SLACK_CLIENT_SECRET;
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
const byoOAuth = flagBoolean(parsed, "oauth");
|
|
202
|
+
if (byoOAuth && (clientId === undefined || clientSecret === undefined)) {
|
|
203
|
+
throw new UsageError("BYO Slack OAuth needs both --client-id and --client-secret.", {
|
|
204
|
+
suggestion: "Use --token with an existing Bot User OAuth Token, or pass both BYO Slack app credentials from Basic Information -> App Credentials."
|
|
205
|
+
});
|
|
203
206
|
}
|
|
204
207
|
const scopes = splitCsv(flagString(parsed, "scopes"));
|
|
208
|
+
const userScopes = splitCsv(flagString(parsed, "user-scopes"));
|
|
209
|
+
const pkceUserScopes = userScopes.length > 0 ? userScopes : scopes.length > 0 ? scopes : defaultPkceUserScopes;
|
|
205
210
|
const profile = await services.oauthFlow.login({
|
|
206
211
|
profileName,
|
|
207
212
|
clientId,
|
|
208
|
-
clientSecret,
|
|
209
|
-
scopes: scopes.length === 0 ? defaultOAuthScopes : scopes,
|
|
210
|
-
userScopes:
|
|
213
|
+
...(byoOAuth ? { clientSecret } : { pkce: true }),
|
|
214
|
+
scopes: byoOAuth ? (scopes.length === 0 ? defaultOAuthScopes : scopes) : [],
|
|
215
|
+
userScopes: byoOAuth ? userScopes : pkceUserScopes,
|
|
211
216
|
redirectUri: flagString(parsed, "redirect-uri"),
|
|
212
217
|
authUrlOut: flagString(parsed, "auth-url-out"),
|
|
213
218
|
timeoutMs: numberFlag(parsed, "timeout-ms"),
|
|
@@ -234,6 +239,8 @@ const defaultOAuthScopes = [
|
|
|
234
239
|
"bookmarks:read",
|
|
235
240
|
"team:read"
|
|
236
241
|
];
|
|
242
|
+
const defaultPkceUserScopes = defaultOAuthScopes;
|
|
243
|
+
const defaultPkceClientId = "8263309029475.11499738391475";
|
|
237
244
|
const apiCall = async (parsed, services) => {
|
|
238
245
|
const method = requirePositional(parsed.positionals, 2, "METHOD");
|
|
239
246
|
const payload = await parseJsonPayload({
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { parseArgs } from "../cli/args.js";
|
|
1
|
+
import { flagBoolean, flagString, parseArgs } from "../cli/args.js";
|
|
2
2
|
import { errorEnvelope } from "../output/envelope.js";
|
|
3
|
+
import { renderHumanErrorEnvelope } from "../output/human.js";
|
|
3
4
|
import { dispatch, renderDispatchResult } from "./commands.js";
|
|
4
5
|
export const executeCli = async (argv, services, options = {}) => {
|
|
6
|
+
let parsed = null;
|
|
5
7
|
try {
|
|
6
|
-
|
|
8
|
+
parsed = parseArgs(argv);
|
|
7
9
|
const result = await dispatch(parsed, services);
|
|
8
10
|
return {
|
|
9
11
|
exitCode: 0,
|
|
@@ -13,10 +15,21 @@ export const executeCli = async (argv, services, options = {}) => {
|
|
|
13
15
|
}
|
|
14
16
|
catch (error) {
|
|
15
17
|
const { envelope, exitCode } = errorEnvelope(error);
|
|
18
|
+
const stderr = shouldRenderJsonError(argv, parsed, options)
|
|
19
|
+
? `${JSON.stringify(envelope, null, 2)}\n`
|
|
20
|
+
: renderHumanErrorEnvelope(envelope, {
|
|
21
|
+
color: shouldColorError(argv, parsed, options)
|
|
22
|
+
});
|
|
16
23
|
return {
|
|
17
24
|
exitCode,
|
|
18
25
|
stdout: "",
|
|
19
|
-
stderr
|
|
26
|
+
stderr
|
|
20
27
|
};
|
|
21
28
|
}
|
|
22
29
|
};
|
|
30
|
+
const shouldRenderJsonError = (argv, parsed, options) => parsed === null
|
|
31
|
+
? argv.includes("--json") || options.stdoutIsTty !== true
|
|
32
|
+
: flagBoolean(parsed, "json") || flagString(parsed, "format") === "json" || options.stdoutIsTty !== true;
|
|
33
|
+
const shouldColorError = (argv, parsed, options) => options.stdoutIsTty === true &&
|
|
34
|
+
options.env?.NO_COLOR === undefined &&
|
|
35
|
+
(parsed === null ? !argv.includes("--no-color") : !flagBoolean(parsed, "no-color"));
|
package/dist/cli/metadata.js
CHANGED
|
@@ -32,13 +32,14 @@ export const commandMetadata = [
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
path: ["auth", "login"],
|
|
35
|
-
summary: "
|
|
35
|
+
summary: "Connect a Slack workspace profile.",
|
|
36
36
|
flags: ["--profile", "--oauth", "--client-id", "--client-secret", "--scopes", "--user-scopes", "--redirect-uri", "--auth-url-out", "--timeout-ms", "--no-open", "--token", "--json"],
|
|
37
37
|
safety: "read",
|
|
38
38
|
output: "profile status",
|
|
39
39
|
examples: [
|
|
40
|
+
"agent-slack auth login --json",
|
|
41
|
+
"AGENT_SLACK_TOKEN=xoxb-... agent-slack auth login --profile work --scopes channels:read --json",
|
|
40
42
|
"agent-slack auth login --oauth --client-id 123.456 --client-secret secret --scopes channels:read,channels:history --json",
|
|
41
|
-
"AGENT_SLACK_TOKEN=xoxb-... agent-slack auth login --profile work --scopes channels:read --json"
|
|
42
43
|
]
|
|
43
44
|
},
|
|
44
45
|
{
|
|
@@ -290,6 +291,9 @@ export const describeCommandGroup = (group) => ({
|
|
|
290
291
|
commands: commandMetadata.filter((command) => command.path[0] === group)
|
|
291
292
|
});
|
|
292
293
|
export const renderHumanHelp = (path) => {
|
|
294
|
+
if (path.join(" ") === "auth login") {
|
|
295
|
+
return renderAuthLoginHelp();
|
|
296
|
+
}
|
|
293
297
|
const command = findCommandMetadata(path);
|
|
294
298
|
if (command !== null) {
|
|
295
299
|
return [
|
|
@@ -297,11 +301,8 @@ export const renderHumanHelp = (path) => {
|
|
|
297
301
|
"",
|
|
298
302
|
command.summary,
|
|
299
303
|
"",
|
|
300
|
-
command.args === undefined ? "" : `
|
|
301
|
-
command.flags === undefined ? "" : `Flags: ${command.flags.join(", ")}`,
|
|
304
|
+
command.args === undefined ? "" : `Usage: ${PRIMARY_COMMAND_NAME} ${command.path.join(" ")} ${command.args.join(" ")}`,
|
|
302
305
|
command.scopes === undefined ? "" : `Scopes: ${command.scopes.join(", ")}`,
|
|
303
|
-
`Safety: ${command.safety}`,
|
|
304
|
-
`Output: ${command.output}`,
|
|
305
306
|
"",
|
|
306
307
|
"Examples:",
|
|
307
308
|
...command.examples.map((example) => ` ${example}`)
|
|
@@ -317,6 +318,42 @@ export const renderHumanHelp = (path) => {
|
|
|
317
318
|
...commands.map((item) => ` ${item.path.join(" ").padEnd(28)} ${item.summary}`)
|
|
318
319
|
].join("\n") + "\n";
|
|
319
320
|
};
|
|
321
|
+
const renderAuthLoginHelp = () => [
|
|
322
|
+
"agent-slack auth login",
|
|
323
|
+
"",
|
|
324
|
+
"Connect a Slack workspace profile.",
|
|
325
|
+
"",
|
|
326
|
+
"Browser login",
|
|
327
|
+
" agent-slack auth login",
|
|
328
|
+
"",
|
|
329
|
+
" Opens Slack in the browser with PKCE and stores a local user-token profile.",
|
|
330
|
+
" No Slack app secret is stored in the CLI.",
|
|
331
|
+
"",
|
|
332
|
+
"Token setup",
|
|
333
|
+
" agent-slack auth login --token \"$SLACK_BOT_TOKEN\" --scopes channels:read,channels:history,users:read",
|
|
334
|
+
"",
|
|
335
|
+
" Stores an existing Slack Bot User OAuth Token as a local profile.",
|
|
336
|
+
"",
|
|
337
|
+
"Developer fallback: bring your own Slack app OAuth credentials",
|
|
338
|
+
" agent-slack auth login --oauth --client-id \"$SLACK_CLIENT_ID\" --client-secret \"$SLACK_CLIENT_SECRET\"",
|
|
339
|
+
"",
|
|
340
|
+
"Options",
|
|
341
|
+
" --profile NAME Save as a named profile.",
|
|
342
|
+
" --token TOKEN Store an existing Bot User OAuth Token.",
|
|
343
|
+
" --scopes LIST Token scopes, or PKCE user scopes when --oauth is not used.",
|
|
344
|
+
" --user-scopes LIST Explicit Slack user scopes for OAuth.",
|
|
345
|
+
" --oauth Use Slack OAuth with BYO app credentials.",
|
|
346
|
+
" --client-id VALUE Override the Slack app Client ID.",
|
|
347
|
+
" --client-secret VALUE Slack app Client Secret.",
|
|
348
|
+
" --no-open Print OAuth URL instead of opening the browser.",
|
|
349
|
+
" --auth-url-out PATH Write OAuth URL for headless flows.",
|
|
350
|
+
" --json Emit a parseable JSON envelope.",
|
|
351
|
+
"",
|
|
352
|
+
"Notes",
|
|
353
|
+
" Normal users should not create a Slack app or handle client credentials.",
|
|
354
|
+
" BYO OAuth credentials are only for development and self-hosted setups.",
|
|
355
|
+
""
|
|
356
|
+
].join("\n");
|
|
320
357
|
export const renderCompletion = (shell) => {
|
|
321
358
|
const words = [...new Set(commandMetadata.flatMap((command) => command.path))];
|
|
322
359
|
const commandWords = commandMetadata.map((command) => command.path.join(" "));
|
package/dist/output/envelope.js
CHANGED
|
@@ -22,10 +22,12 @@ export const successEnvelope = (input) => ({
|
|
|
22
22
|
});
|
|
23
23
|
export const errorEnvelope = (error) => {
|
|
24
24
|
const normalized = normalizeUnknownError(error);
|
|
25
|
-
const
|
|
25
|
+
const details = Object.fromEntries(Object.entries(normalized.details).filter(([key]) => key !== "suggestion"));
|
|
26
|
+
const detailsSlackError = typeof details.slackError === "string" ? details.slackError : undefined;
|
|
26
27
|
const slackError = "slackError" in normalized ? normalized.slackError ?? detailsSlackError : detailsSlackError;
|
|
27
28
|
const retryAfterSeconds = "retryAfterSeconds" in normalized ? normalized.retryAfterSeconds : undefined;
|
|
28
|
-
const
|
|
29
|
+
const detailSuggestion = typeof normalized.details.suggestion === "string" ? normalized.details.suggestion : undefined;
|
|
30
|
+
const suggestion = detailSuggestion ?? suggestionFor(normalized._tag);
|
|
29
31
|
const envelope = {
|
|
30
32
|
ok: false,
|
|
31
33
|
error: {
|
|
@@ -36,7 +38,7 @@ export const errorEnvelope = (error) => {
|
|
|
36
38
|
...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
|
|
37
39
|
...(suggestion === undefined ? {} : { suggestion }),
|
|
38
40
|
trace_id: `slk_${randomUUID()}`,
|
|
39
|
-
...(Object.keys(
|
|
41
|
+
...(Object.keys(details).length === 0 ? {} : { details })
|
|
40
42
|
}
|
|
41
43
|
};
|
|
42
44
|
return { envelope, exitCode: normalized.exitCode };
|
package/dist/output/human.js
CHANGED
|
@@ -3,6 +3,7 @@ const codes = {
|
|
|
3
3
|
cyan: [36, 39],
|
|
4
4
|
dim: [2, 22],
|
|
5
5
|
green: [32, 39],
|
|
6
|
+
red: [31, 39],
|
|
6
7
|
yellow: [33, 39]
|
|
7
8
|
};
|
|
8
9
|
export const renderHumanEnvelope = (envelope, options) => {
|
|
@@ -21,6 +22,20 @@ export const renderHumanEnvelope = (envelope, options) => {
|
|
|
21
22
|
];
|
|
22
23
|
return `${lines.join("\n")}\n`;
|
|
23
24
|
};
|
|
25
|
+
export const renderHumanErrorEnvelope = (envelope, options) => {
|
|
26
|
+
const paint = (name, value) => options.color ? `\u001b[${codes[name][0]}m${value}\u001b[${codes[name][1]}m` : value;
|
|
27
|
+
const error = envelope.error;
|
|
28
|
+
const lines = [
|
|
29
|
+
`${paint("red", "Error")} ${paint("bold", error.type)}`,
|
|
30
|
+
error.title,
|
|
31
|
+
...(error.slack_error === undefined ? [] : ["", `${paint("dim", "Slack")} ${error.slack_error}`]),
|
|
32
|
+
...(error.retry_after_seconds === undefined ? [] : [`${paint("dim", "Retry after")} ${error.retry_after_seconds}s`]),
|
|
33
|
+
...(error.suggestion === undefined ? [] : ["", paint("yellow", "Next"), ` ${error.suggestion}`]),
|
|
34
|
+
"",
|
|
35
|
+
`${paint("dim", "Trace")} ${error.trace_id}`
|
|
36
|
+
];
|
|
37
|
+
return `${lines.join("\n")}\n`;
|
|
38
|
+
};
|
|
24
39
|
const isCommandCatalog = (value) => {
|
|
25
40
|
if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.commands)) {
|
|
26
41
|
return false;
|
package/package.json
CHANGED
|
@@ -11,11 +11,41 @@ Use `agent-slack` as the Slack context boundary. `aslk` is the short alias. Pref
|
|
|
11
11
|
|
|
12
12
|
1. Check availability: `agent-slack describe --json`.
|
|
13
13
|
2. Check auth: `agent-slack auth status --json`.
|
|
14
|
-
3. If auth is missing, ask the user to run `agent-slack auth login
|
|
14
|
+
3. If auth is missing, ask the user to run `agent-slack auth login`. It opens Slack in the browser with PKCE and stores a local user-token profile.
|
|
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.
|
|
17
17
|
6. Read structured errors from stderr; do not parse progress text from stdout.
|
|
18
18
|
|
|
19
|
+
## Onboarding
|
|
20
|
+
|
|
21
|
+
Browser login:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
agent-slack auth login
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Agent Slack opens Slack in the browser with PKCE and stores a local user-token profile. Users should not create a Slack app or handle `client-id`/`client-secret`.
|
|
28
|
+
|
|
29
|
+
Token setup:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
agent-slack auth login --token "$SLACK_BOT_TOKEN" --scopes channels:read,channels:history,users:read --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This stores an existing Slack Bot User OAuth Token as a local profile.
|
|
36
|
+
|
|
37
|
+
Developer/self-hosted fallback:
|
|
38
|
+
|
|
39
|
+
1. Create or open a Slack app at https://api.slack.com/apps.
|
|
40
|
+
2. Go to **OAuth & Permissions**, add the needed bot scopes, install the app to the workspace, and copy the **Bot User OAuth Token**.
|
|
41
|
+
3. For OAuth auth, get the **Client ID** and **Client Secret** from **Basic Information -> App Credentials** in that Slack app, then run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
agent-slack auth login --oauth --client-id "$SLACK_CLIENT_ID" --client-secret "$SLACK_CLIENT_SECRET" --json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Treat Slack app creation and client credentials as an advanced developer fallback only.
|
|
48
|
+
|
|
19
49
|
## Common Commands
|
|
20
50
|
|
|
21
51
|
```bash
|
package/assets/terminal.svg
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
<svg width="980" height="560" viewBox="0 0 980 560" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
|
|
2
|
-
<title id="title">Agent Slack terminal screenshot</title>
|
|
3
|
-
<desc id="desc">A terminal showing Agent Slack commands that read Slack context as JSON and NDJSON.</desc>
|
|
4
|
-
<rect width="980" height="560" rx="18" fill="#111827"/>
|
|
5
|
-
<rect x="22" y="22" width="936" height="516" rx="14" fill="#0B1020" stroke="#263244"/>
|
|
6
|
-
<rect x="22" y="22" width="936" height="48" rx="14" fill="#151D2E"/>
|
|
7
|
-
<circle cx="52" cy="46" r="7" fill="#FF5F57"/>
|
|
8
|
-
<circle cx="76" cy="46" r="7" fill="#FFBD2E"/>
|
|
9
|
-
<circle cx="100" cy="46" r="7" fill="#28C840"/>
|
|
10
|
-
<text x="490" y="51" text-anchor="middle" fill="#94A3B8" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="14">agent-slack</text>
|
|
11
|
-
|
|
12
|
-
<text x="54" y="116" fill="#22C55E" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18">$</text>
|
|
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
|
-
|
|
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">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
|
-
|
|
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">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
|
-
|
|
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">agent-slack thread get --channel C123 --ts 1710000000.000100 --include users --json</text>
|
|
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
|
-
|
|
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">agent-slack api call conversations.info --payload '{"channel":"C123"}' --json</text>
|
|
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">--json stays parseable</text>
|
|
32
|
-
</svg>
|