@stratusagent/channel-slack 0.11.2 → 0.11.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/README.md +160 -18
- package/dist/index.d.ts +113 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1185 -164
- package/dist/index.js.map +1 -1
- package/manifest/stratus-agent.manifest.json +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Slack adapter for Stratus agents. **One Slack app per agent** — Slack has no w
|
|
|
4
4
|
|
|
5
5
|
- **Resumable conversations**: session keys are `slack:<agent>:<team>:<channel>:<thread_ts ?? ts>` (DMs: the DM channel id) — a thread is a conversation, it survives daemon restarts, and two agents sharing a thread keep fully separate sessions. A turn parked on a human when the daemon died is re-asked after the restart, and when it finishes its reply is posted into the thread as a fresh message — the placeholder it would have edited belonged to the old process.
|
|
6
6
|
- **Streaming replies**: post a placeholder, edit as deltas arrive (throttled for `chat.update` limits), show `⚙ tool…` status lines, finalize with the full reply — split across messages when it outgrows one.
|
|
7
|
-
- **Markdown is translated to Slack's mrkdwn** on the way out — `**bold**` to `*bold*`, `*italic*` to `_italic_`, `~~struck~~` to `~struck~`, `[text](https://…)` to `<https://…|text>`, and a `#` heading to a bold line, since mrkdwn has no headings — unless the heading already carries an asterisk of its own, which is left as it is: Slack has one bold delimiter and no way to nest it, so a second pair would print rather than render. Code spans and fences are left exactly as written, an unclosed fence included, so a `**` inside a snippet stays part of the snippet — a run of N backticks opens code and only a run of exactly N closes it, which is what lets ``a span with a ` in it`` and a ````-fence holding a ```-fence work. One or two unmatched backticks are literal text, as they are in Markdown. Anything whose spelling already matches — lists, block quotes, inline code — is untouched. A marker still being streamed stays literal until its closing half arrives.
|
|
7
|
+
- **Markdown is translated to Slack's mrkdwn** on the way out — `**bold**` to `*bold*`, `*italic*` to `_italic_`, `~~struck~~` to `~struck~`, `[text](https://…)` to `<https://…|text>`, and a `#` heading to a bold line, since mrkdwn has no headings — unless the heading already carries an asterisk of its own, which is left as it is: Slack has one bold delimiter and no way to nest it, so a second pair would print rather than render. Code spans and fences are left exactly as written, an unclosed fence included, so a `**` inside a snippet stays part of the snippet — a run of N backticks opens code and only a run of exactly N closes it, which is what lets ``a span with a ` in it`` and a ````-fence holding a ```-fence work. One or two unmatched backticks are literal text, as they are in Markdown. A run that has code inside it is still one run — ``**the `fs.read` tool**`` is bold around a snippet, not four literal asterisks — because the whole reply is rewritten at once, with the code held out of the rewrite rather than cut out of the text. Anything whose spelling already matches — lists, block quotes, inline code — is untouched. A marker still being streamed stays literal until its closing half arrives.
|
|
8
8
|
- **Mention to start, then it stays in the thread.** A mention opens a conversation; every reply in that thread reaches the agent without tagging it again, and free-form DMs are unchanged. Who an untagged reply is for is decided by [Who a message is for](#who-a-message-is-for). Inbound mentions of other users are humanized to `@Display Name` for the model; redeliveries are deduped so a slow turn never runs twice.
|
|
9
9
|
- **Approval buttons** for the gateway's `remote` permission mode: a gated tool call parks the turn and asks in its thread with **Allow once** / **Always allow** / **Deny**. See [Approving tool calls](#approving-tool-calls) — clicks are authorized by *who clicked*, never by who can see the message, and **Always allow** is offered only where the daemon would actually remember it.
|
|
10
10
|
- **Tokens are gateway infrastructure secrets**, stored in the `channels` namespace of `~/.stratus/credentials.json` — never in an agent's credential allowlist:
|
|
@@ -44,20 +44,104 @@ they are the whole model:
|
|
|
44
44
|
straight after it reaches the agent you just named, and the agent that
|
|
45
45
|
*had* the thread stands down at the same instant rather than whenever its
|
|
46
46
|
own app next catches up.
|
|
47
|
+
5. **How an agent listens is the soul's to say.** Rules 3 and 4 are the
|
|
48
|
+
`thread` mode, the default. A soul with `listens: mentions` takes only
|
|
49
|
+
messages that name it, and hears the rest. One with `listens: judge`
|
|
50
|
+
hears everything and, while attentive — eight messages or fifteen
|
|
51
|
+
minutes after it last answered a message that named it, whichever ends
|
|
52
|
+
first — runs a turn nobody asked for on each untagged message, told it
|
|
53
|
+
may answer with nothing; past the window a message is heard with no
|
|
54
|
+
turn at all, and a mention re-arms it. A reply it chose to give on its
|
|
55
|
+
own does not: it takes the thread for the thread rule, and counts
|
|
56
|
+
against its own window like any other message. A turn that decides on silence posts nothing: its
|
|
57
|
+
placeholder is opened only on the first text it streams, never on a
|
|
58
|
+
tool line, and a failed one posts no error note — one the daemon died
|
|
59
|
+
inside, failed at the next start, included, a line it had already
|
|
60
|
+
streamed staying as it was. A line an attempt the
|
|
61
|
+
provider abandoned had already posted is taken back when the retry
|
|
62
|
+
decides on silence. Neither mode takes
|
|
63
|
+
part in the holder rule — that rule answers whose an untagged reply is,
|
|
64
|
+
and for these two the soul already has — so a thread-rule agent stands
|
|
65
|
+
down for a judging or mentions-only one that spoke last: the judging
|
|
66
|
+
one decides for itself, and the mentions-only one is being talked to
|
|
67
|
+
and waits to be named. A judging agent that chooses to speak takes the
|
|
68
|
+
thread the way any speaker does, recorded once its reply has landed —
|
|
69
|
+
so a message typed while it was still deciding may be answered by both
|
|
70
|
+
it and a thread-rule colleague that held the thread before. "Spoke
|
|
71
|
+
last" is read off the thread, not the clock: the holder is whichever
|
|
72
|
+
agent's reply sits lowest, since that is the one a reader answers, and
|
|
73
|
+
a placeholder posted early and filled in slowly sits above a reply
|
|
74
|
+
posted while it was being written. A colleague's reply is never
|
|
75
|
+
judged, only heard.
|
|
76
|
+
The window also counts messages this daemon has dispatched that the
|
|
77
|
+
session does not hold yet, so a burst typed inside one turn is judged
|
|
78
|
+
up to the window and heard past it.
|
|
79
|
+
6. **Standing down is not leaving.** An agent in a thread hears what is
|
|
80
|
+
said to the other agent in it — the question that named its colleague,
|
|
81
|
+
the untagged replies that were the colleague's to answer, and what the
|
|
82
|
+
colleague *replied* — into its own session, with no turn run and
|
|
83
|
+
nothing posted. The next time it is asked, it answers as someone who
|
|
84
|
+
followed the conversation rather than one who stepped out of the room.
|
|
85
|
+
What it hears is marked as said to somebody else, so a stranger's words
|
|
86
|
+
in a shared thread never read to it as an instruction; and a stranger
|
|
87
|
+
overheard lowers its session's trust label exactly as one who addressed
|
|
88
|
+
it would, since their text is in the transcript either way. A
|
|
89
|
+
colleague's reply is heard under the colleague's own label, never as a
|
|
90
|
+
person's: an agent's word is at most `agent`, and carries whatever its
|
|
91
|
+
session has been exposed to. Replies only — a turn that said nothing,
|
|
92
|
+
or failed, is nothing to hear — and only while the hearer's app is
|
|
93
|
+
still in the channel, which is asked of Slack per reply: a person's
|
|
94
|
+
message reaches an agent through its own socket, so an app removed
|
|
95
|
+
from a private channel stops hearing on its own, but a reply is
|
|
96
|
+
forwarded by the daemon and would otherwise keep arriving.
|
|
47
97
|
|
|
48
98
|
Everyone in the thread is talking to the same agent — a reply from a second
|
|
49
99
|
person is a follow-up like any other, and channel messages reach the model
|
|
50
100
|
prefixed with the speaker's display name so it knows who said what. A reply
|
|
51
101
|
carrying a file, or one the author also broadcast to the channel, is a
|
|
52
102
|
follow-up too; what Slack marks as bookkeeping — an edit, a deletion, a
|
|
53
|
-
join — is not, and nothing answers it. **
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
join — is not, and nothing answers it. **Images are shown to the model.**
|
|
104
|
+
A PNG, JPEG, GIF, or WebP attached to a message is downloaded with the
|
|
105
|
+
bot token and sent with the message — a screenshot alone, with nothing
|
|
106
|
+
typed, is a question in its own right and gets an answer. That needs the
|
|
107
|
+
`files:read` scope, which the shipped manifest asks for; an app installed
|
|
108
|
+
without it should have the scope added under **OAuth & Permissions** and be
|
|
109
|
+
reinstalled once, and until then `stratus serve` warns, naming the scope,
|
|
110
|
+
each time an image arrives. An image over 5 MB or 8000 pixels a side (the
|
|
111
|
+
model API's limits) is not kept, and one message's images stop at 20 MB
|
|
112
|
+
together — a request has a size limit too, and images past it are named as
|
|
113
|
+
unreadable; send them in a message of their own. The same 20 MB — and 20
|
|
114
|
+
images — is what a whole thread's images may take on one request, spent
|
|
115
|
+
newest first — and the request as a whole has a limit too, so when the
|
|
116
|
+
rest of the conversation needs the room the oldest images give way to it.
|
|
117
|
+
Once a thread's images pass a limit the oldest are let
|
|
118
|
+
go of — the model is told an image was there and what it was called, and
|
|
119
|
+
the session keeps that record rather than the pixels, so a busy thread's
|
|
120
|
+
row stays bounded. A message's downloads share one 30-second deadline,
|
|
121
|
+
after which whatever has not arrived is named as unreadable, so a slow
|
|
122
|
+
link cannot hold the thread. And an image the model API itself refuses —
|
|
123
|
+
the adapter checks that a file opens and closes like the image it claims
|
|
124
|
+
to be, but that is not a decode — is dropped from the session and the turn
|
|
125
|
+
retried without it, so one bad file cannot fail every later turn of a
|
|
126
|
+
thread.
|
|
127
|
+
An OpenAI-compatible model that takes only text needs `"vision": false` in
|
|
128
|
+
config, which turns every image into that note; see the
|
|
129
|
+
[Slack guide](../../docs/guides/slack.md#sending-an-image). Whether the model actually *sees* the image depends on the
|
|
130
|
+
agent's runtime: the Anthropic API and OpenAI-compatible providers send it
|
|
131
|
+
as image content; the Claude Code and Codex harnesses take a text prompt,
|
|
132
|
+
so there the agent is told the image's name and that it cannot see it.
|
|
133
|
+
**Every other attachment is unreadable** — a log, a PDF, an image that was
|
|
134
|
+
too large — so the message reaches the agent naming those files and saying
|
|
135
|
+
they cannot be opened, which is what lets it answer honestly instead of as
|
|
136
|
+
though it had read the log. Such a file dropped in with nothing said is not
|
|
137
|
+
a question, and gets no reply.
|
|
138
|
+
Sessions are still per agent: an agent hears a thread from the mention
|
|
139
|
+
that brought it in, and what was said before that — to the other agent, or
|
|
140
|
+
by it — is not backfilled ([#147](https://github.com/stratuslabs/agent/issues/147)).
|
|
141
|
+
Bring it up to speed in the message that tags it. A colleague's reply is
|
|
142
|
+
heard by the daemon that posted it, not through Slack — an agent served by
|
|
143
|
+
another daemon is a stranger's bot to this one, and its replies are not
|
|
144
|
+
heard at all.
|
|
61
145
|
|
|
62
146
|
Three edges worth knowing. An agent whose app was installed before the
|
|
63
147
|
history scopes below is told about mentions only, and behaves exactly as it
|
|
@@ -75,10 +159,10 @@ always given when an app is down.
|
|
|
75
159
|
And the rules above are mechanical, which shows in a thread where people are
|
|
76
160
|
mostly talking to *each other*: an agent invited into one answers every
|
|
77
161
|
untagged reply in it, including the ones meant for somebody else. Give the
|
|
78
|
-
side conversation its own thread.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
[roadmap step 31](../../docs/roadmap/31-reading-the-room.md).
|
|
162
|
+
side conversation its own thread. It can now *hear* a thread it is not
|
|
163
|
+
answering; teaching it to choose — to answer only when it has something to
|
|
164
|
+
add, and to let "thanks, we've got it" be a sentence it read — is the rest
|
|
165
|
+
of [roadmap step 31](../../docs/roadmap/31-reading-the-room.md).
|
|
82
166
|
|
|
83
167
|
## Installing
|
|
84
168
|
|
|
@@ -104,7 +188,7 @@ editing `credentials.json` by hand.
|
|
|
104
188
|
The manual equivalent, if you prefer:
|
|
105
189
|
|
|
106
190
|
1. https://api.slack.com/apps → **Create New App → From a manifest** → paste `manifest/stratus-agent.manifest.json` with `NAME` replaced by the agent's name.
|
|
107
|
-
The manifest asks for the `channels:history` / `groups:history` / `mpim:history` scopes and the matching `message.*` events, which is what lets an agent [stay in a thread](#who-a-message-is-for) instead of needing a mention every time. An app created before those shipped needs them added under **OAuth & Permissions** and **Event Subscriptions** and reinstalled once; leave
|
|
191
|
+
The manifest asks for the `channels:history` / `groups:history` / `mpim:history` scopes and the matching `message.*` events, which is what lets an agent [stay in a thread](#who-a-message-is-for) instead of needing a mention every time, and for `files:read`, which is what lets it [see an attached image](#who-a-message-is-for). An app created before those shipped needs them added under **OAuth & Permissions** and **Event Subscriptions** and reinstalled once; leave the history scopes off and it answers mentions and DMs, exactly as it did before; leave `files:read` off and it hears about attachments by name only.
|
|
108
192
|
2. **Basic Information → App-Level Tokens** → generate a token with `connections:write` (that's the `appToken`, `xapp-…`).
|
|
109
193
|
3. **Install App** to the workspace → copy the **Bot User OAuth Token** (that's the `botToken`, `xoxb-…`).
|
|
110
194
|
4. Upload the agent's avatar under **Display Information**.
|
|
@@ -128,12 +212,20 @@ the parser cannot reduce to a scope. The prompt says so instead, because a
|
|
|
128
212
|
button that does exactly what **Allow once** does, under a label promising a
|
|
129
213
|
standing grant, is worse than no button.
|
|
130
214
|
|
|
215
|
+
Where it is offered, a line beside the buttons says what it would do,
|
|
216
|
+
because that differs by tool and is the half of the question the button
|
|
217
|
+
widens: for most gated tools it is a **standing grant** to the agent that
|
|
218
|
+
lasts until an operator revokes it (`stratus grants <agent>` lists and
|
|
219
|
+
revokes; see [Approvals](../../docs/guides/approvals.md)); for a shell
|
|
220
|
+
command it is that command's scope; for a browser action it is the site;
|
|
221
|
+
and for a send outside a schedule it is the rest of this session.
|
|
222
|
+
|
|
131
223
|
The resolved message describes what the daemon did rather than which button
|
|
132
224
|
was pressed — `POST /approvals` accepts `always` whatever this channel
|
|
133
|
-
rendered, so the answer can arrive from somewhere else
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
225
|
+
rendered, so the answer can arrive from somewhere else — and names the grant
|
|
226
|
+
that was made, since the request said in advance which kind it would be.
|
|
227
|
+
The one thing it cannot promise is the disk write: a daemon that could not
|
|
228
|
+
read the whitelist does not write over it, and by then the message is sent.
|
|
137
229
|
`stratus logs` has the exact line.
|
|
138
230
|
|
|
139
231
|
Who may answer is configured per agent, in `~/.stratus/config.json`:
|
|
@@ -225,12 +317,62 @@ With no list at all, every Slack sender is `unknown`, and `stratus serve`
|
|
|
225
317
|
says so at startup. An agent's own entry replaces the shared list;
|
|
226
318
|
`"slackUsers": []` excludes an agent from it.
|
|
227
319
|
|
|
320
|
+
The list also decides how a speaker is *named* in a channel turn, which
|
|
321
|
+
the model reads as `Name: text`. A principal is their display name — one
|
|
322
|
+
line, at most 80 characters, control characters and Unicode bidi controls
|
|
323
|
+
spelled out — because
|
|
324
|
+
their profile is trusted the way their messages are. With a list in force,
|
|
325
|
+
everyone else is their user id, exactly as an `@mention` of them already
|
|
326
|
+
is: a display name is text its owner typed, and the speaker position of a
|
|
327
|
+
user turn is not a place a stranger gets to put a sentence. With no list
|
|
328
|
+
at all there is nobody to prefer, so every author keeps their name — and
|
|
329
|
+
"no list" is the key being absent; `"slackUsers": []` is a list that
|
|
330
|
+
prefers nobody, so under it every author is an id.
|
|
331
|
+
|
|
228
332
|
The sender is judged on **every message**, not once per thread. A thread
|
|
229
333
|
keys one session for everyone in it, so an authorized member can open one
|
|
230
334
|
and a stranger can mention the agent inside it afterwards — the stranger's
|
|
231
335
|
turn lowers the session, and the authorized member's next turn does not
|
|
232
336
|
raise it back.
|
|
233
337
|
|
|
338
|
+
The list is a **label** by default: an unlisted sender still gets a turn,
|
|
339
|
+
marked `unknown`. For an agent that holds tools that is not enough — a
|
|
340
|
+
stranger's message is not merely uncertain, it is a prompt they chose, and
|
|
341
|
+
`unknown` does not stop `shell.run` from running on it once a listed
|
|
342
|
+
approver clicks. `admit` makes the list a **door**:
|
|
343
|
+
|
|
344
|
+
```jsonc
|
|
345
|
+
{
|
|
346
|
+
"principals": {
|
|
347
|
+
"slackUsers": ["U01DYLAN"],
|
|
348
|
+
"admit": "principals",
|
|
349
|
+
"agents": {
|
|
350
|
+
"bea": { "admit": "anyone" }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Under `"admit": "principals"` a sender not in `slackUsers` gets no turn:
|
|
357
|
+
the message is refused before it takes a place in the queue, the agent
|
|
358
|
+
does not overhear it into a thread's transcript either, and nothing is
|
|
359
|
+
posted back — a reply is a conversation the operator chose not to have.
|
|
360
|
+
The refusal is one line in the daemon log naming the agent and the user
|
|
361
|
+
id — one per message, however many times Slack delivers it, and only for a
|
|
362
|
+
message the agent would have taken up: a mention, or a reply in a thread
|
|
363
|
+
the agent holds, whether the daemon remembers that or its sessions do. A
|
|
364
|
+
stranger's reply in a thread the agent was never part of is dropped
|
|
365
|
+
without a line. `"anyone"` is
|
|
366
|
+
the default and today's behavior; the key inherits per agent like
|
|
367
|
+
`slackUsers`, so Bea above stays open while everyone else is closed. A value
|
|
368
|
+
that is neither word is a config error, never `anyone`: this is the one
|
|
369
|
+
setting here whose misspelling would open the door. So is a `slackUsers`
|
|
370
|
+
that is not a list of user ids, and so is a block or a per-agent entry that
|
|
371
|
+
is not an object: dropped for its shape, a per-agent list would fall back to
|
|
372
|
+
the shared one, the broader list it existed to narrow, and the block itself
|
|
373
|
+
would fall back to `anyone`.
|
|
374
|
+
`stratus serve` says at startup which agents refuse unlisted senders.
|
|
375
|
+
|
|
234
376
|
Like `approvals`, this block is read only from a config you chose —
|
|
235
377
|
`--config`, `STRATUS_CONFIG`, or the global `~/.stratus/config.json`. A
|
|
236
378
|
project-local `stratus.config.json` cannot appoint itself the principal.
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export interface SlackAgentConfig {
|
|
|
31
31
|
* nobody is, and every sender is `unknown`.
|
|
32
32
|
*/
|
|
33
33
|
principals?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Whether an unlisted sender gets a turn at all. `anyone` (the default)
|
|
36
|
+
* admits them as `unknown`; `principals` refuses them before a turn
|
|
37
|
+
* starts and does not let the agent overhear them either — the text
|
|
38
|
+
* would be in the transcript, which is what this mode exists to keep
|
|
39
|
+
* out. The label above is provenance; this is authorization.
|
|
40
|
+
*/
|
|
41
|
+
admit?: 'anyone' | 'principals';
|
|
34
42
|
}
|
|
35
43
|
export interface SlackSocketEventArgs {
|
|
36
44
|
ack: (response?: unknown) => Promise<void>;
|
|
@@ -81,19 +89,77 @@ export interface SlackInboundEvent {
|
|
|
81
89
|
/**
|
|
82
90
|
* Attachments on a `file_share` message. Slack puts the file metadata in
|
|
83
91
|
* the event and the bytes behind an authenticated URL that needs
|
|
84
|
-
* `files:read
|
|
85
|
-
*
|
|
92
|
+
* `files:read`. An image the model can take is downloaded and sent with
|
|
93
|
+
* the message (see `readImageAttachments`); anything else — a log, a
|
|
94
|
+
* PDF, an image too large or in a format the API refuses — reaches the
|
|
95
|
+
* turn by name only, told that it cannot be opened.
|
|
86
96
|
*/
|
|
87
|
-
files?:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
files?: SlackInboundFile[];
|
|
98
|
+
}
|
|
99
|
+
/** The slice of Slack's file object the adapter reads. */
|
|
100
|
+
export interface SlackInboundFile {
|
|
101
|
+
id?: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
title?: string;
|
|
104
|
+
/** Slack's own `image/png`-style type, decided by the uploader's client. */
|
|
105
|
+
mimetype?: string;
|
|
106
|
+
/** Bytes, as Slack reports them; checked before anything is downloaded. */
|
|
107
|
+
size?: number;
|
|
108
|
+
url_private?: string;
|
|
109
|
+
url_private_download?: string;
|
|
91
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* What came back from fetching a file's private URL. Slack answers a
|
|
113
|
+
* request the app is not allowed to make — a token without `files:read` —
|
|
114
|
+
* with a **200 and a sign-in page**, never an error status, which is why
|
|
115
|
+
* the content type travels: it is the only thing that tells an image from
|
|
116
|
+
* an HTML apology for one.
|
|
117
|
+
*/
|
|
118
|
+
export interface SlackFileDownload {
|
|
119
|
+
status: number;
|
|
120
|
+
contentType?: string;
|
|
121
|
+
body: Buffer;
|
|
122
|
+
/**
|
|
123
|
+
* Set when the body was cut off at `maxBytes`: what is in `body` is not
|
|
124
|
+
* the file, only proof that the file was bigger than the caller would
|
|
125
|
+
* take.
|
|
126
|
+
*/
|
|
127
|
+
truncated?: boolean;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Fetches a file's private URL with the bot token. `signal` is the
|
|
131
|
+
* download's deadline, and a fetcher MUST stop on it — headers and body
|
|
132
|
+
* both: the download runs inside the session's intake chain, so one that
|
|
133
|
+
* stalls holds every later message in the thread, and `stop()` waits on
|
|
134
|
+
* the handler too. `maxBytes` is the most body a fetcher may buffer; one
|
|
135
|
+
* that reads past it MUST stop there and report `truncated`, because
|
|
136
|
+
* Slack's reported size is advisory and a response is not.
|
|
137
|
+
*/
|
|
138
|
+
export type SlackFileFetcher = (url: string, botToken: string, signal: AbortSignal, maxBytes: number) => Promise<SlackFileDownload>;
|
|
92
139
|
export interface SlackSocketLike {
|
|
93
140
|
on(eventName: string, listener: (args: SlackSocketEventArgs) => void): void;
|
|
94
141
|
start(): Promise<unknown>;
|
|
95
142
|
disconnect(): Promise<void>;
|
|
96
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* What `files.uploadV2` answers with, as much of it as is read here: the
|
|
146
|
+
* file's shares, each carrying the ts of the message the file became in a
|
|
147
|
+
* channel — where it sits in the thread. Whether a just-completed upload's
|
|
148
|
+
* shares are filled in yet is Slack's business, so every part is optional
|
|
149
|
+
* and a reader without one falls back (see `ReplyRenderer.queueUpload`).
|
|
150
|
+
*/
|
|
151
|
+
export interface SlackUploadResult {
|
|
152
|
+
files?: Array<{
|
|
153
|
+
shares?: {
|
|
154
|
+
public?: Record<string, Array<{
|
|
155
|
+
ts?: string;
|
|
156
|
+
}>>;
|
|
157
|
+
private?: Record<string, Array<{
|
|
158
|
+
ts?: string;
|
|
159
|
+
}>>;
|
|
160
|
+
};
|
|
161
|
+
}>;
|
|
162
|
+
}
|
|
97
163
|
export interface SlackWebLike {
|
|
98
164
|
auth: {
|
|
99
165
|
test(): Promise<{
|
|
@@ -117,6 +183,15 @@ export interface SlackWebLike {
|
|
|
117
183
|
text: string;
|
|
118
184
|
blocks?: SlackBlock[];
|
|
119
185
|
}): Promise<unknown>;
|
|
186
|
+
/**
|
|
187
|
+
* Take a message back. The one caller is a turn nobody asked for that
|
|
188
|
+
* opened its placeholder for an attempt the provider then abandoned and
|
|
189
|
+
* decided on silence after all — see `ReplyRenderer.retract`.
|
|
190
|
+
*/
|
|
191
|
+
delete(args: {
|
|
192
|
+
channel: string;
|
|
193
|
+
ts: string;
|
|
194
|
+
}): Promise<unknown>;
|
|
120
195
|
/**
|
|
121
196
|
* Visible only to `user`. Refusing a click needs to reach the person
|
|
122
197
|
* who clicked without announcing to the channel that they tried.
|
|
@@ -135,7 +210,7 @@ export interface SlackWebLike {
|
|
|
135
210
|
filename?: string;
|
|
136
211
|
title?: string;
|
|
137
212
|
thread_ts?: string;
|
|
138
|
-
}): Promise<
|
|
213
|
+
}): Promise<SlackUploadResult>;
|
|
139
214
|
};
|
|
140
215
|
conversations: {
|
|
141
216
|
/**
|
|
@@ -178,7 +253,38 @@ export interface SlackAdapterOptions {
|
|
|
178
253
|
createSocketClient?: (appToken: string) => SlackSocketLike;
|
|
179
254
|
/** Test injection: build a Web API client for a bot token. */
|
|
180
255
|
createWebClient?: (botToken: string) => SlackWebLike;
|
|
256
|
+
/**
|
|
257
|
+
* Test injection: fetch a file's private URL with the bot token. The
|
|
258
|
+
* Web API client has no call for this — file bytes are served from a
|
|
259
|
+
* plain authenticated URL — so it is its own seam.
|
|
260
|
+
*/
|
|
261
|
+
fetchFile?: SlackFileFetcher;
|
|
262
|
+
/**
|
|
263
|
+
* How long one message's attachment downloads may take, all of them
|
|
264
|
+
* together, before the turn goes on without whatever has not arrived.
|
|
265
|
+
* Defaults to `DEFAULT_FILE_DOWNLOAD_TIMEOUT_MS`.
|
|
266
|
+
*/
|
|
267
|
+
fileDownloadTimeoutMs?: number;
|
|
268
|
+
/**
|
|
269
|
+
* Test injection: the clock, in milliseconds, that an upload's place in
|
|
270
|
+
* the thread is read from (`ReplyOutcome.spokeAt`) when the upload's
|
|
271
|
+
* answer carries no share ts. Defaults to `Date.now`.
|
|
272
|
+
*/
|
|
273
|
+
now?: () => number;
|
|
181
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* The default fetcher over a `fetch`. Exported as a factory so the bounded
|
|
277
|
+
* read can be tested against a fake `fetch` with a body that never ends;
|
|
278
|
+
* the adapter uses it over the global one.
|
|
279
|
+
*
|
|
280
|
+
* The body is read a chunk at a time and abandoned the moment it passes
|
|
281
|
+
* `maxBytes`: `arrayBuffer()` would buffer the whole response first, and
|
|
282
|
+
* the size the adapter checked before asking came from Slack's metadata,
|
|
283
|
+
* which an oversized response is under no obligation to match. A
|
|
284
|
+
* `content-length` that already says too much is refused before a byte is
|
|
285
|
+
* read.
|
|
286
|
+
*/
|
|
287
|
+
export declare const createSlackFileFetcher: (fetchImpl: typeof fetch) => SlackFileFetcher;
|
|
182
288
|
/**
|
|
183
289
|
* The Slack channel adapter. One Slack app per agent — Slack has no way to
|
|
184
290
|
* give a single bot several identities with real avatars, presence, and
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|