@eliya-oss/agent-slack 0.1.0
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 +295 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/assets/terminal.svg +32 -0
- package/dist/adapters/catalog/BundledMethodCatalog.js +53 -0
- package/dist/adapters/http-file-downloader/HttpFileDownloader.js +20 -0
- package/dist/adapters/keychain/KeychainTokenStore.js +152 -0
- package/dist/adapters/live.js +13 -0
- package/dist/adapters/localhost-oauth/NodeLocalhostOAuthFlow.js +160 -0
- package/dist/adapters/profile-file/FileTokenStore.js +67 -0
- package/dist/adapters/slack-sdk/SlackSdkWebApi.js +48 -0
- package/dist/application/auth.js +35 -0
- package/dist/application/commands.js +579 -0
- package/dist/application/execute.js +22 -0
- package/dist/application/payload.js +40 -0
- package/dist/application/services.js +1 -0
- package/dist/application/slack-call.js +62 -0
- package/dist/cli/args.js +70 -0
- package/dist/cli/metadata.js +330 -0
- package/dist/cli/types.js +1 -0
- package/dist/domain/errors.js +68 -0
- package/dist/domain/ids.js +31 -0
- package/dist/domain/slack.js +1 -0
- package/dist/main.js +16 -0
- package/dist/output/envelope.js +60 -0
- package/dist/output/projection.js +55 -0
- package/dist/ports/FileDownloader.js +1 -0
- package/dist/ports/MethodCatalog.js +1 -0
- package/dist/ports/OAuthFlow.js +1 -0
- package/dist/ports/SlackWebApi.js +1 -0
- package/dist/ports/TokenStore.js +1 -0
- package/package.json +63 -0
- package/skills/agent-slack/SKILL.md +35 -0
- package/skills/agent-slack/agents/openai.yaml +4 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { UnsafeMethodBlocked } from "../domain/errors.js";
|
|
2
|
+
import { pagingFrom } from "../output/envelope.js";
|
|
3
|
+
export const callSlack = async (services, options) => {
|
|
4
|
+
const safety = services.methodCatalog.safetyFor(options.method);
|
|
5
|
+
if (isUnsafe(safety) && (!options.allowWrite || !options.yes)) {
|
|
6
|
+
throw new UnsafeMethodBlocked(`Refusing unsafe Slack method ${options.method}`, {
|
|
7
|
+
method: options.method,
|
|
8
|
+
safety
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!options.all) {
|
|
12
|
+
const result = await services.slackWebApi.call({
|
|
13
|
+
method: options.method,
|
|
14
|
+
token: options.token,
|
|
15
|
+
payload: options.payload
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
method: options.method,
|
|
19
|
+
response: result.response,
|
|
20
|
+
items: extractItems(services, options.method, result.response)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const itemKey = services.methodCatalog.itemKeyFor(options.method);
|
|
24
|
+
const collected = [];
|
|
25
|
+
let cursor;
|
|
26
|
+
let lastResponse = {};
|
|
27
|
+
for (let page = 0; page < 100; page += 1) {
|
|
28
|
+
const payload = cursor === undefined ? options.payload : { ...options.payload, cursor };
|
|
29
|
+
const result = await services.slackWebApi.call({
|
|
30
|
+
method: options.method,
|
|
31
|
+
token: options.token,
|
|
32
|
+
payload
|
|
33
|
+
});
|
|
34
|
+
lastResponse = result.response;
|
|
35
|
+
collected.push(...extractItems(services, options.method, result.response));
|
|
36
|
+
const paging = pagingFrom(result.response);
|
|
37
|
+
if (paging.next_cursor === null || itemKey === null) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
cursor = paging.next_cursor;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
method: options.method,
|
|
44
|
+
response: itemKey === null ? lastResponse : { ...lastResponse, [itemKey]: collected },
|
|
45
|
+
items: collected
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const isUnsafe = (safety) => safety === "write" || safety === "destructive" || safety === "admin";
|
|
49
|
+
const extractItems = (services, method, response) => {
|
|
50
|
+
const itemKey = services.methodCatalog.itemKeyFor(method);
|
|
51
|
+
if (itemKey === null) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const value = response[itemKey];
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (value !== undefined && typeof value === "object" && value !== null) {
|
|
59
|
+
return Object.entries(value).map(([key, entry]) => ({ key, value: entry }));
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
};
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { UsageError } from "../domain/errors.js";
|
|
2
|
+
const booleanFlags = new Set([
|
|
3
|
+
"all",
|
|
4
|
+
"allow-write",
|
|
5
|
+
"help",
|
|
6
|
+
"json",
|
|
7
|
+
"raw",
|
|
8
|
+
"trace",
|
|
9
|
+
"yes",
|
|
10
|
+
"no-cache",
|
|
11
|
+
"inclusive",
|
|
12
|
+
"oauth"
|
|
13
|
+
]);
|
|
14
|
+
export const parseArgs = (argv) => {
|
|
15
|
+
const flags = new Map();
|
|
16
|
+
const positionals = [];
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
const token = argv[index];
|
|
19
|
+
if (token === undefined) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (token === "--") {
|
|
23
|
+
positionals.push(...argv.slice(index + 1));
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
if (!token.startsWith("--") || token === "-") {
|
|
27
|
+
positionals.push(token);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const raw = token.slice(2);
|
|
31
|
+
const eq = raw.indexOf("=");
|
|
32
|
+
const name = eq >= 0 ? raw.slice(0, eq) : raw;
|
|
33
|
+
if (name === "") {
|
|
34
|
+
throw new UsageError("Invalid empty flag");
|
|
35
|
+
}
|
|
36
|
+
if (eq >= 0) {
|
|
37
|
+
flags.set(name, raw.slice(eq + 1));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (booleanFlags.has(name)) {
|
|
41
|
+
flags.set(name, true);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const value = argv[index + 1];
|
|
45
|
+
if (value === undefined || value.startsWith("--")) {
|
|
46
|
+
throw new UsageError(`Missing value for --${name}`, { flag: name });
|
|
47
|
+
}
|
|
48
|
+
flags.set(name, value);
|
|
49
|
+
index += 1;
|
|
50
|
+
}
|
|
51
|
+
return { tokens: argv, flags, positionals };
|
|
52
|
+
};
|
|
53
|
+
export const flagString = (parsed, name, fallback) => {
|
|
54
|
+
const value = parsed.flags.get(name);
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
return fallback;
|
|
59
|
+
};
|
|
60
|
+
export const flagBoolean = (parsed, name) => parsed.flags.get(name) === true;
|
|
61
|
+
export const requireFlag = (parsed, name) => {
|
|
62
|
+
const value = flagString(parsed, name);
|
|
63
|
+
if (value === undefined) {
|
|
64
|
+
throw new UsageError(`Missing required --${name}`, { flag: name });
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
export const splitCsv = (value) => value === undefined || value.trim() === ""
|
|
69
|
+
? []
|
|
70
|
+
: value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export const commandMetadata = [
|
|
2
|
+
{
|
|
3
|
+
path: ["describe"],
|
|
4
|
+
summary: "Describe the full CLI command tree as JSON.",
|
|
5
|
+
flags: ["--json"],
|
|
6
|
+
safety: "read",
|
|
7
|
+
output: "command metadata",
|
|
8
|
+
examples: ["slk describe --json"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
path: ["completion"],
|
|
12
|
+
summary: "Generate shell completion words.",
|
|
13
|
+
args: ["bash|zsh"],
|
|
14
|
+
safety: "read",
|
|
15
|
+
output: "shell completion script",
|
|
16
|
+
examples: ["slk completion zsh > ~/.zfunc/_slk"]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: ["auth", "status"],
|
|
20
|
+
summary: "Show the active Slack auth profile.",
|
|
21
|
+
flags: ["--profile", "--json"],
|
|
22
|
+
safety: "read",
|
|
23
|
+
output: "profile status",
|
|
24
|
+
examples: ["slk auth status --json"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: ["auth", "login"],
|
|
28
|
+
summary: "Create a Slack auth profile with OAuth or a seeded token.",
|
|
29
|
+
flags: ["--profile", "--oauth", "--client-id", "--client-secret", "--scopes", "--user-scopes", "--redirect-uri", "--auth-url-out", "--timeout-ms", "--token", "--json"],
|
|
30
|
+
safety: "read",
|
|
31
|
+
output: "profile status",
|
|
32
|
+
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"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: ["auth", "scopes"],
|
|
39
|
+
summary: "Show granted scopes for the active profile.",
|
|
40
|
+
flags: ["--profile", "--json"],
|
|
41
|
+
safety: "read",
|
|
42
|
+
output: "scope list",
|
|
43
|
+
examples: ["slk auth scopes --json"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: ["auth", "profiles", "list"],
|
|
47
|
+
summary: "List local auth profiles.",
|
|
48
|
+
flags: ["--json"],
|
|
49
|
+
safety: "read",
|
|
50
|
+
output: "profile list",
|
|
51
|
+
examples: ["slk auth profiles list --json"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
path: ["auth", "logout"],
|
|
55
|
+
summary: "Delete a local auth profile.",
|
|
56
|
+
flags: ["--profile", "--json"],
|
|
57
|
+
safety: "destructive",
|
|
58
|
+
output: "deleted profile status",
|
|
59
|
+
examples: ["slk auth logout --profile work --yes --json"]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: ["auth", "test"],
|
|
63
|
+
summary: "Call Slack auth.test for the active profile.",
|
|
64
|
+
methods: ["auth.test"],
|
|
65
|
+
scopes: [],
|
|
66
|
+
safety: "read",
|
|
67
|
+
output: "Slack auth.test response",
|
|
68
|
+
examples: ["slk auth test --json"]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: ["team", "get"],
|
|
72
|
+
summary: "Show workspace info.",
|
|
73
|
+
methods: ["team.info"],
|
|
74
|
+
scopes: ["team:read"],
|
|
75
|
+
safety: "read",
|
|
76
|
+
output: "team info",
|
|
77
|
+
examples: ["slk team get --json"]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
path: ["team", "profile", "get"],
|
|
81
|
+
summary: "Show workspace profile fields.",
|
|
82
|
+
methods: ["team.profile.get"],
|
|
83
|
+
scopes: ["users.profile:read"],
|
|
84
|
+
safety: "read",
|
|
85
|
+
output: "team profile fields",
|
|
86
|
+
examples: ["slk team profile get --json"]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
path: ["enterprise", "get"],
|
|
90
|
+
summary: "Show enterprise/workspace identity from team info.",
|
|
91
|
+
methods: ["team.info"],
|
|
92
|
+
scopes: ["team:read"],
|
|
93
|
+
safety: "read",
|
|
94
|
+
output: "team or enterprise info",
|
|
95
|
+
examples: ["slk enterprise get --json"]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
path: ["user", "list"],
|
|
99
|
+
summary: "List users visible to the active profile.",
|
|
100
|
+
flags: ["--limit", "--all", "--json"],
|
|
101
|
+
methods: ["users.list"],
|
|
102
|
+
scopes: ["users:read"],
|
|
103
|
+
safety: "read",
|
|
104
|
+
output: "user list",
|
|
105
|
+
examples: ["slk user list --limit 50 --json"]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
path: ["user", "get"],
|
|
109
|
+
summary: "Show one user.",
|
|
110
|
+
args: ["USER_ID"],
|
|
111
|
+
methods: ["users.info"],
|
|
112
|
+
scopes: ["users:read"],
|
|
113
|
+
safety: "read",
|
|
114
|
+
output: "user info",
|
|
115
|
+
examples: ["slk user get U123 --json"]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
path: ["user", "lookup"],
|
|
119
|
+
summary: "Find a user by email.",
|
|
120
|
+
flags: ["--email", "--json"],
|
|
121
|
+
methods: ["users.lookupByEmail"],
|
|
122
|
+
scopes: ["users:read.email"],
|
|
123
|
+
safety: "read",
|
|
124
|
+
output: "user info",
|
|
125
|
+
examples: ["slk user lookup --email dev@example.com --json"]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
path: ["user", "presence", "get"],
|
|
129
|
+
summary: "Show user presence.",
|
|
130
|
+
args: ["USER_ID"],
|
|
131
|
+
methods: ["users.getPresence"],
|
|
132
|
+
scopes: ["users:read"],
|
|
133
|
+
safety: "read",
|
|
134
|
+
output: "presence",
|
|
135
|
+
examples: ["slk user presence get U123 --json"]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
path: ["usergroups", "list"],
|
|
139
|
+
summary: "List Slack user groups.",
|
|
140
|
+
methods: ["usergroups.list"],
|
|
141
|
+
scopes: ["usergroups:read"],
|
|
142
|
+
safety: "read",
|
|
143
|
+
output: "user group list",
|
|
144
|
+
examples: ["slk usergroups list --json"]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
path: ["usergroups", "users", "list"],
|
|
148
|
+
summary: "List users in a Slack user group.",
|
|
149
|
+
args: ["USERGROUP_ID"],
|
|
150
|
+
methods: ["usergroups.users.list"],
|
|
151
|
+
scopes: ["usergroups:read"],
|
|
152
|
+
safety: "read",
|
|
153
|
+
output: "user IDs",
|
|
154
|
+
examples: ["slk usergroups users list S123 --json"]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
path: ["api", "call"],
|
|
158
|
+
summary: "Call any Slack Web API method using a raw JSON payload.",
|
|
159
|
+
args: ["METHOD", "PAYLOAD_STDIN_MARKER"],
|
|
160
|
+
flags: ["--payload", "--profile", "--token", "--all", "--raw", "--format", "--allow-write", "--yes", "--json"],
|
|
161
|
+
safety: "unknown",
|
|
162
|
+
output: "Slack Web API response",
|
|
163
|
+
examples: [
|
|
164
|
+
"slk api call conversations.history --payload '{\"channel\":\"C123\"}' --json",
|
|
165
|
+
"cat payload.json | slk api call conversations.history -"
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
path: ["api", "methods", "list"],
|
|
170
|
+
summary: "List bundled Slack method metadata.",
|
|
171
|
+
flags: ["--family", "--json"],
|
|
172
|
+
safety: "read",
|
|
173
|
+
output: "method metadata list",
|
|
174
|
+
examples: ["slk api methods list --family conversations --json"]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: ["api", "method", "describe"],
|
|
178
|
+
summary: "Describe one Slack method.",
|
|
179
|
+
args: ["METHOD"],
|
|
180
|
+
flags: ["--json"],
|
|
181
|
+
safety: "read",
|
|
182
|
+
output: "method metadata",
|
|
183
|
+
examples: ["slk api method describe conversations.replies --json"]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
path: ["conversation", "list"],
|
|
187
|
+
summary: "List conversations visible to the active profile.",
|
|
188
|
+
methods: ["conversations.list"],
|
|
189
|
+
scopes: ["channels:read", "groups:read", "im:read", "mpim:read"],
|
|
190
|
+
safety: "read",
|
|
191
|
+
output: "conversation list",
|
|
192
|
+
examples: ["slk conversation list --types public_channel,private_channel --json"]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
path: ["conversation", "history"],
|
|
196
|
+
summary: "Read conversation history.",
|
|
197
|
+
args: ["CHANNEL_ID"],
|
|
198
|
+
methods: ["conversations.history"],
|
|
199
|
+
scopes: ["channels:history", "groups:history", "im:history", "mpim:history"],
|
|
200
|
+
safety: "read",
|
|
201
|
+
output: "message list",
|
|
202
|
+
examples: ["slk conversation history C123 --limit 20 --json"]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
path: ["conversation", "context"],
|
|
206
|
+
summary: "Return agent-friendly channel context.",
|
|
207
|
+
args: ["CHANNEL_ID"],
|
|
208
|
+
methods: ["conversations.history", "conversations.replies", "users.info"],
|
|
209
|
+
scopes: ["channels:history", "users:read"],
|
|
210
|
+
safety: "read",
|
|
211
|
+
output: "hydrated conversation context",
|
|
212
|
+
examples: ["slk conversation context C123 --since 24h --include users,threads --format ndjson"]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
path: ["thread", "get"],
|
|
216
|
+
summary: "Read a complete Slack thread.",
|
|
217
|
+
flags: ["--channel", "--ts", "--include", "--json"],
|
|
218
|
+
methods: ["conversations.replies"],
|
|
219
|
+
scopes: ["channels:history", "groups:history", "im:history", "mpim:history"],
|
|
220
|
+
safety: "read",
|
|
221
|
+
output: "thread messages",
|
|
222
|
+
examples: ["slk thread get --channel C123 --ts 1710000000.000100 --include users --json"]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
path: ["message", "get"],
|
|
226
|
+
summary: "Read one Slack message by channel and timestamp.",
|
|
227
|
+
flags: ["--channel", "--ts", "--json"],
|
|
228
|
+
methods: ["conversations.history"],
|
|
229
|
+
scopes: ["channels:history"],
|
|
230
|
+
safety: "read",
|
|
231
|
+
output: "message",
|
|
232
|
+
examples: ["slk message get --channel C123 --ts 1710000000.000100 --json"]
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
path: ["message", "permalink"],
|
|
236
|
+
summary: "Return a Slack permalink for a message.",
|
|
237
|
+
flags: ["--channel", "--ts", "--json"],
|
|
238
|
+
methods: ["chat.getPermalink"],
|
|
239
|
+
safety: "read",
|
|
240
|
+
output: "permalink",
|
|
241
|
+
examples: ["slk message permalink --channel C123 --ts 1710000000.000100 --json"]
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
path: ["search", "context"],
|
|
245
|
+
summary: "Search Slack for agent context.",
|
|
246
|
+
flags: ["--query", "--content-types", "--json"],
|
|
247
|
+
methods: ["assistant.search.context"],
|
|
248
|
+
scopes: ["search:read.public", "search:read.private"],
|
|
249
|
+
safety: "read",
|
|
250
|
+
output: "search context",
|
|
251
|
+
examples: ["slk search context --query 'project atlas' --json"]
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
path: ["file", "list"],
|
|
255
|
+
summary: "List files visible to the active profile.",
|
|
256
|
+
methods: ["files.list"],
|
|
257
|
+
scopes: ["files:read"],
|
|
258
|
+
safety: "read",
|
|
259
|
+
output: "file list",
|
|
260
|
+
examples: ["slk file list --channel C123 --json"]
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
path: ["file", "download"],
|
|
264
|
+
summary: "Download a Slack file to disk.",
|
|
265
|
+
args: ["FILE_ID"],
|
|
266
|
+
flags: ["--out", "--profile", "--token", "--json"],
|
|
267
|
+
methods: ["files.info"],
|
|
268
|
+
scopes: ["files:read"],
|
|
269
|
+
safety: "read",
|
|
270
|
+
output: "download status",
|
|
271
|
+
examples: ["slk file download F123 --out ./artifact.pdf --json"]
|
|
272
|
+
}
|
|
273
|
+
];
|
|
274
|
+
export const describeAllCommands = () => ({
|
|
275
|
+
name: "slk",
|
|
276
|
+
version: "0.1.0",
|
|
277
|
+
commands: commandMetadata
|
|
278
|
+
});
|
|
279
|
+
export const findCommandMetadata = (path) => commandMetadata.find((command) => command.path.join(" ") === path.join(" ")) ?? null;
|
|
280
|
+
export const describeCommandGroup = (group) => ({
|
|
281
|
+
name: group,
|
|
282
|
+
commands: commandMetadata.filter((command) => command.path[0] === group)
|
|
283
|
+
});
|
|
284
|
+
export const renderHumanHelp = (path) => {
|
|
285
|
+
const command = findCommandMetadata(path);
|
|
286
|
+
if (command !== null) {
|
|
287
|
+
return [
|
|
288
|
+
`slk ${command.path.join(" ")}`,
|
|
289
|
+
"",
|
|
290
|
+
command.summary,
|
|
291
|
+
"",
|
|
292
|
+
command.args === undefined ? "" : `Arguments: ${command.args.join(" ")}`,
|
|
293
|
+
command.flags === undefined ? "" : `Flags: ${command.flags.join(", ")}`,
|
|
294
|
+
command.scopes === undefined ? "" : `Scopes: ${command.scopes.join(", ")}`,
|
|
295
|
+
`Safety: ${command.safety}`,
|
|
296
|
+
`Output: ${command.output}`,
|
|
297
|
+
"",
|
|
298
|
+
"Examples:",
|
|
299
|
+
...command.examples.map((example) => ` ${example}`)
|
|
300
|
+
].filter((line) => line !== "").join("\n") + "\n";
|
|
301
|
+
}
|
|
302
|
+
const commands = path.length === 0 ? commandMetadata : commandMetadata.filter((item) => item.path[0] === path[0]);
|
|
303
|
+
return [
|
|
304
|
+
"slk",
|
|
305
|
+
"",
|
|
306
|
+
"Agent-readable Slack CLI.",
|
|
307
|
+
"",
|
|
308
|
+
"Commands:",
|
|
309
|
+
...commands.map((item) => ` ${item.path.join(" ").padEnd(28)} ${item.summary}`)
|
|
310
|
+
].join("\n") + "\n";
|
|
311
|
+
};
|
|
312
|
+
export const renderCompletion = (shell) => {
|
|
313
|
+
const words = [...new Set(commandMetadata.flatMap((command) => command.path))];
|
|
314
|
+
const commandWords = commandMetadata.map((command) => command.path.join(" "));
|
|
315
|
+
if (shell === "bash") {
|
|
316
|
+
return `complete -W '${words.join(" ")}' slk\n`;
|
|
317
|
+
}
|
|
318
|
+
if (shell === "zsh") {
|
|
319
|
+
return [
|
|
320
|
+
"#compdef slk",
|
|
321
|
+
"_slk_commands=(",
|
|
322
|
+
...commandWords.map((command) => ` '${command}:${summaryFor(command)}'`),
|
|
323
|
+
")",
|
|
324
|
+
"_describe 'slk command' _slk_commands",
|
|
325
|
+
""
|
|
326
|
+
].join("\n");
|
|
327
|
+
}
|
|
328
|
+
return "";
|
|
329
|
+
};
|
|
330
|
+
const summaryFor = (path) => commandMetadata.find((command) => command.path.join(" ") === path)?.summary.replace(/'/g, "") ?? "";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export class TaggedSlkError extends Error {
|
|
2
|
+
details;
|
|
3
|
+
constructor(message, details = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
this.details = details;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class NotAuthenticated extends TaggedSlkError {
|
|
10
|
+
_tag = "NotAuthenticated";
|
|
11
|
+
exitCode = 4;
|
|
12
|
+
}
|
|
13
|
+
export class MissingScope extends TaggedSlkError {
|
|
14
|
+
_tag = "MissingScope";
|
|
15
|
+
exitCode = 4;
|
|
16
|
+
}
|
|
17
|
+
export class PermissionDenied extends TaggedSlkError {
|
|
18
|
+
_tag = "PermissionDenied";
|
|
19
|
+
exitCode = 4;
|
|
20
|
+
}
|
|
21
|
+
export class SlackRateLimited extends TaggedSlkError {
|
|
22
|
+
_tag = "SlackRateLimited";
|
|
23
|
+
exitCode = 6;
|
|
24
|
+
retryAfterSeconds;
|
|
25
|
+
constructor(message, details = {}) {
|
|
26
|
+
super(message, details);
|
|
27
|
+
this.retryAfterSeconds = details.retryAfterSeconds;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class SlackApiFailed extends TaggedSlkError {
|
|
31
|
+
_tag = "SlackApiFailed";
|
|
32
|
+
exitCode = 1;
|
|
33
|
+
slackError;
|
|
34
|
+
constructor(message, details = {}) {
|
|
35
|
+
super(message, details);
|
|
36
|
+
this.slackError = details.slackError;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class InvalidPayload extends TaggedSlkError {
|
|
40
|
+
_tag = "InvalidPayload";
|
|
41
|
+
exitCode = 2;
|
|
42
|
+
}
|
|
43
|
+
export class ResourceNotFound extends TaggedSlkError {
|
|
44
|
+
_tag = "ResourceNotFound";
|
|
45
|
+
exitCode = 3;
|
|
46
|
+
}
|
|
47
|
+
export class UnsafeMethodBlocked extends TaggedSlkError {
|
|
48
|
+
_tag = "UnsafeMethodBlocked";
|
|
49
|
+
exitCode = 5;
|
|
50
|
+
}
|
|
51
|
+
export class UsageError extends TaggedSlkError {
|
|
52
|
+
_tag = "UsageError";
|
|
53
|
+
exitCode = 2;
|
|
54
|
+
}
|
|
55
|
+
export class UnsupportedMethod extends TaggedSlkError {
|
|
56
|
+
_tag = "UnsupportedMethod";
|
|
57
|
+
exitCode = 2;
|
|
58
|
+
}
|
|
59
|
+
export const isSlkError = (error) => error instanceof TaggedSlkError;
|
|
60
|
+
export const normalizeUnknownError = (error) => {
|
|
61
|
+
if (isSlkError(error)) {
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
if (error instanceof Error) {
|
|
65
|
+
return new SlackApiFailed(error.message, { cause: error.name });
|
|
66
|
+
}
|
|
67
|
+
return new SlackApiFailed("Unexpected failure", { cause: String(error) });
|
|
68
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const nonEmpty = (value, name) => {
|
|
2
|
+
if (value.trim() === "") {
|
|
3
|
+
throw new Error(`${name} cannot be empty`);
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
export const TeamId = {
|
|
8
|
+
make: (value) => nonEmpty(value, "TeamId")
|
|
9
|
+
};
|
|
10
|
+
export const EnterpriseId = {
|
|
11
|
+
make: (value) => nonEmpty(value, "EnterpriseId")
|
|
12
|
+
};
|
|
13
|
+
export const ChannelId = {
|
|
14
|
+
make: (value) => nonEmpty(value, "ChannelId")
|
|
15
|
+
};
|
|
16
|
+
export const UserId = {
|
|
17
|
+
make: (value) => nonEmpty(value, "UserId")
|
|
18
|
+
};
|
|
19
|
+
export const MessageTs = {
|
|
20
|
+
make: (value) => nonEmpty(value, "MessageTs")
|
|
21
|
+
};
|
|
22
|
+
export const SlackMethod = {
|
|
23
|
+
make: (value) => nonEmpty(value, "SlackMethod")
|
|
24
|
+
};
|
|
25
|
+
export const ProfileName = {
|
|
26
|
+
default: "default",
|
|
27
|
+
make: (value) => nonEmpty(value, "ProfileName")
|
|
28
|
+
};
|
|
29
|
+
export const Scope = {
|
|
30
|
+
make: (value) => nonEmpty(value, "Scope")
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { NodeRuntime } from "@effect/platform-node";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { createLiveServices } from "./adapters/live.js";
|
|
5
|
+
import { executeCli } from "./application/execute.js";
|
|
6
|
+
const program = Effect.promise(async () => {
|
|
7
|
+
const result = await executeCli(process.argv.slice(2), createLiveServices());
|
|
8
|
+
if (result.stdout.length > 0) {
|
|
9
|
+
process.stdout.write(result.stdout);
|
|
10
|
+
}
|
|
11
|
+
if (result.stderr.length > 0) {
|
|
12
|
+
process.stderr.write(result.stderr);
|
|
13
|
+
}
|
|
14
|
+
process.exitCode = result.exitCode;
|
|
15
|
+
});
|
|
16
|
+
NodeRuntime.runMain(program, { disableErrorReporting: true });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isSlkError, normalizeUnknownError } from "../domain/errors.js";
|
|
3
|
+
export const pagingFrom = (response) => {
|
|
4
|
+
const metadata = response.response_metadata;
|
|
5
|
+
const nextCursor = typeof metadata === "object" && metadata !== null && "next_cursor" in metadata
|
|
6
|
+
? metadata.next_cursor
|
|
7
|
+
: undefined;
|
|
8
|
+
return {
|
|
9
|
+
next_cursor: typeof nextCursor === "string" && nextCursor.length > 0 ? nextCursor : null,
|
|
10
|
+
has_more: response.has_more === true || (typeof nextCursor === "string" && nextCursor.length > 0)
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export const successEnvelope = (input) => ({
|
|
14
|
+
ok: true,
|
|
15
|
+
method: input.method,
|
|
16
|
+
team_id: input.profile?.teamId ?? null,
|
|
17
|
+
enterprise_id: input.profile?.enterpriseId ?? null,
|
|
18
|
+
profile: input.profile?.name ?? null,
|
|
19
|
+
data: input.data,
|
|
20
|
+
paging: input.paging ?? { next_cursor: null, has_more: false },
|
|
21
|
+
warnings: input.warnings ?? []
|
|
22
|
+
});
|
|
23
|
+
export const errorEnvelope = (error) => {
|
|
24
|
+
const normalized = normalizeUnknownError(error);
|
|
25
|
+
const detailsSlackError = typeof normalized.details.slackError === "string" ? normalized.details.slackError : undefined;
|
|
26
|
+
const slackError = "slackError" in normalized ? normalized.slackError ?? detailsSlackError : detailsSlackError;
|
|
27
|
+
const retryAfterSeconds = "retryAfterSeconds" in normalized ? normalized.retryAfterSeconds : undefined;
|
|
28
|
+
const suggestion = suggestionFor(normalized._tag);
|
|
29
|
+
const envelope = {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: {
|
|
32
|
+
type: normalized._tag,
|
|
33
|
+
title: normalized.message,
|
|
34
|
+
...(slackError === undefined ? {} : { slack_error: slackError }),
|
|
35
|
+
retriable: normalized._tag === "SlackRateLimited",
|
|
36
|
+
...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
|
|
37
|
+
...(suggestion === undefined ? {} : { suggestion }),
|
|
38
|
+
trace_id: `slk_${randomUUID()}`,
|
|
39
|
+
...(Object.keys(normalized.details).length === 0 ? {} : { details: normalized.details })
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return { envelope, exitCode: normalized.exitCode };
|
|
43
|
+
};
|
|
44
|
+
const suggestionFor = (tag) => {
|
|
45
|
+
switch (tag) {
|
|
46
|
+
case "NotAuthenticated":
|
|
47
|
+
return "Run slk auth login or provide a seeded profile for tests.";
|
|
48
|
+
case "MissingScope":
|
|
49
|
+
return "Reinstall or reauthorize the Slack app with the missing scope.";
|
|
50
|
+
case "SlackRateLimited":
|
|
51
|
+
return "Retry after the provided delay or reduce page size.";
|
|
52
|
+
case "UnsafeMethodBlocked":
|
|
53
|
+
return "Pass --allow-write --yes only when the operator intentionally allows this call.";
|
|
54
|
+
default:
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
export const stringifyJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
59
|
+
export const toNdjson = (items) => items.map((item) => JSON.stringify(item)).join("\n") + (items.length > 0 ? "\n" : "");
|
|
60
|
+
export const exitCodeOf = (error) => isSlkError(error) ? error.exitCode : 1;
|