experimental-ash 0.12.0 → 0.12.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/CHANGELOG.md +7 -0
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/public/channels/slack/api-encoding.d.ts +5 -0
- package/dist/src/public/channels/slack/api-encoding.js +46 -0
- package/dist/src/public/channels/slack/api.d.ts +2 -1
- package/dist/src/public/channels/slack/api.js +30 -14
- package/dist/src/public/channels/slack/blocks.js +32 -9
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 51d1669: Upgrade ai dependencies to latest canary
|
|
8
|
+
- f0b2cb5: Fix Slack channel regressions for assistant typing indicators, external file uploads, and native table rendering.
|
|
9
|
+
|
|
3
10
|
## 0.12.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.12.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.12.1";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function encodeSlackApiBody(body, encoding) {
|
|
2
|
+
if (encoding === "json") {
|
|
3
|
+
return {
|
|
4
|
+
body: JSON.stringify(body),
|
|
5
|
+
contentType: "application/json; charset=utf-8",
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
const params = new URLSearchParams();
|
|
9
|
+
if (body && typeof body === "object") {
|
|
10
|
+
for (const [key, value] of Object.entries(body)) {
|
|
11
|
+
if (value === undefined || value === null)
|
|
12
|
+
continue;
|
|
13
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
14
|
+
params.set(key, String(value));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
params.set(key, JSON.stringify(value));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
body: params.toString(),
|
|
23
|
+
contentType: "application/x-www-form-urlencoded",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function decodeSlackApiBody(body, contentType) {
|
|
27
|
+
if (typeof body !== "string")
|
|
28
|
+
return body;
|
|
29
|
+
if (contentType?.includes("application/json"))
|
|
30
|
+
return parseJson(body);
|
|
31
|
+
if (!contentType?.includes("application/x-www-form-urlencoded"))
|
|
32
|
+
return body;
|
|
33
|
+
const parsed = {};
|
|
34
|
+
for (const [key, value] of new URLSearchParams(body)) {
|
|
35
|
+
parsed[key] = value.startsWith("[") || value.startsWith("{") ? parseJson(value) : value;
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
function parseJson(value) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(value);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type CardElement, type FileUpload } from "#compiled/chat/index.js";
|
|
18
18
|
/**
|
|
19
|
-
* Slack bot token, materialized either as a literal `xoxb
|
|
19
|
+
* Slack bot token, materialized either as a literal `xoxb-...` string or
|
|
20
20
|
* as a (possibly async) function that returns one. The function form
|
|
21
21
|
* supports secret-manager lookups and credential rotation.
|
|
22
22
|
*/
|
|
@@ -59,6 +59,7 @@ export declare function callSlackApi(input: {
|
|
|
59
59
|
readonly botToken: SlackBotToken | undefined;
|
|
60
60
|
readonly operation: string;
|
|
61
61
|
readonly body: unknown;
|
|
62
|
+
readonly encoding?: "form" | "json";
|
|
62
63
|
}): Promise<SlackApiResponse>;
|
|
63
64
|
/**
|
|
64
65
|
* Builds the `request(op, body)` Slack API caller installed on every
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { isCardElement } from "#compiled/chat/index.js";
|
|
18
18
|
import { createLogger } from "#internal/logging.js";
|
|
19
|
+
import { encodeSlackApiBody } from "#public/channels/slack/api-encoding.js";
|
|
19
20
|
import { cardToBlocks, cardToFallbackText } from "#public/channels/slack/blocks.js";
|
|
20
21
|
const log = createLogger("slack.api");
|
|
21
22
|
/**
|
|
@@ -56,13 +57,14 @@ export async function resolveSlackBotToken(token) {
|
|
|
56
57
|
*/
|
|
57
58
|
export async function callSlackApi(input) {
|
|
58
59
|
const token = await resolveSlackBotToken(input.botToken);
|
|
60
|
+
const encoded = encodeSlackApiBody(input.body, input.encoding ?? "json");
|
|
59
61
|
const response = await fetch(`https://slack.com/api/${input.operation}`, {
|
|
60
62
|
method: "POST",
|
|
61
63
|
headers: {
|
|
62
64
|
authorization: `Bearer ${token}`,
|
|
63
|
-
"content-type":
|
|
65
|
+
"content-type": encoded.contentType,
|
|
64
66
|
},
|
|
65
|
-
body:
|
|
67
|
+
body: encoded.body,
|
|
66
68
|
});
|
|
67
69
|
return response.json();
|
|
68
70
|
}
|
|
@@ -92,9 +94,14 @@ export function buildSlackBinding(input) {
|
|
|
92
94
|
const fileIds = [];
|
|
93
95
|
for (const file of files) {
|
|
94
96
|
const bytes = await readFileBytes(file.data);
|
|
95
|
-
const getUrl = await
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const getUrl = await callSlackApi({
|
|
98
|
+
botToken: input.botToken,
|
|
99
|
+
operation: "files.getUploadURLExternal",
|
|
100
|
+
encoding: "form",
|
|
101
|
+
body: {
|
|
102
|
+
filename: file.filename,
|
|
103
|
+
length: bytes.byteLength,
|
|
104
|
+
},
|
|
98
105
|
});
|
|
99
106
|
if (getUrl.ok !== true ||
|
|
100
107
|
typeof getUrl.upload_url !== "string" ||
|
|
@@ -105,9 +112,9 @@ export function buildSlackBinding(input) {
|
|
|
105
112
|
method: "POST",
|
|
106
113
|
headers: {
|
|
107
114
|
authorization: `Bearer ${token}`,
|
|
108
|
-
"content-type":
|
|
115
|
+
"content-type": "application/octet-stream",
|
|
109
116
|
},
|
|
110
|
-
body:
|
|
117
|
+
body: bytes,
|
|
111
118
|
});
|
|
112
119
|
if (!uploadResponse.ok) {
|
|
113
120
|
throw new Error(`Slack upload POST returned HTTP ${uploadResponse.status} for ${file.filename}.`);
|
|
@@ -123,7 +130,12 @@ export function buildSlackBinding(input) {
|
|
|
123
130
|
completeBody.thread_ts = threadTs;
|
|
124
131
|
if (options?.initialComment)
|
|
125
132
|
completeBody.initial_comment = options.initialComment;
|
|
126
|
-
const complete = await
|
|
133
|
+
const complete = await callSlackApi({
|
|
134
|
+
botToken: input.botToken,
|
|
135
|
+
operation: "files.completeUploadExternal",
|
|
136
|
+
encoding: "form",
|
|
137
|
+
body: completeBody,
|
|
138
|
+
});
|
|
127
139
|
if (complete.ok !== true) {
|
|
128
140
|
throw new Error(`Slack files.completeUploadExternal failed: ${complete.error ?? "unknown_error"}`);
|
|
129
141
|
}
|
|
@@ -183,11 +195,15 @@ export function buildSlackBinding(input) {
|
|
|
183
195
|
if (!input.channelId || !input.threadTs)
|
|
184
196
|
return;
|
|
185
197
|
try {
|
|
186
|
-
const
|
|
198
|
+
const body = {
|
|
187
199
|
channel_id: input.channelId,
|
|
188
200
|
thread_ts: input.threadTs,
|
|
189
201
|
status: status ?? "",
|
|
190
|
-
}
|
|
202
|
+
};
|
|
203
|
+
if (status !== undefined && status.length > 0) {
|
|
204
|
+
body.loading_messages = [status];
|
|
205
|
+
}
|
|
206
|
+
const response = await request("assistant.threads.setStatus", body);
|
|
191
207
|
if (response.ok !== true) {
|
|
192
208
|
log.debug("assistant.threads.setStatus returned not-ok", {
|
|
193
209
|
error: response.error,
|
|
@@ -361,18 +377,18 @@ function convertInline(input) {
|
|
|
361
377
|
}
|
|
362
378
|
/**
|
|
363
379
|
* Normalize a {@link FileUpload.data} value (`Buffer | Blob | ArrayBuffer`) to
|
|
364
|
-
* a contiguous `
|
|
380
|
+
* a contiguous `Buffer` we can both POST and length-prefix without
|
|
365
381
|
* holding two copies of the payload in memory.
|
|
366
382
|
*/
|
|
367
383
|
async function readFileBytes(data) {
|
|
368
384
|
if (data instanceof ArrayBuffer)
|
|
369
|
-
return data;
|
|
385
|
+
return Buffer.from(data);
|
|
370
386
|
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
371
|
-
return await data.arrayBuffer();
|
|
387
|
+
return Buffer.from(await data.arrayBuffer());
|
|
372
388
|
}
|
|
373
389
|
if (ArrayBuffer.isView(data)) {
|
|
374
390
|
const view = data;
|
|
375
|
-
return
|
|
391
|
+
return Buffer.from(view.buffer, view.byteOffset, view.byteLength);
|
|
376
392
|
}
|
|
377
393
|
throw new Error("FileUpload.data must be a Buffer, ArrayBuffer, or Blob.");
|
|
378
394
|
}
|
|
@@ -34,6 +34,7 @@ import { truncatePlainText } from "#public/channels/slack/limits.js";
|
|
|
34
34
|
*/
|
|
35
35
|
export function cardToBlocks(card) {
|
|
36
36
|
const blocks = [];
|
|
37
|
+
const state = { usedNativeTable: false };
|
|
37
38
|
if (card.title) {
|
|
38
39
|
blocks.push({
|
|
39
40
|
type: "header",
|
|
@@ -50,7 +51,7 @@ export function cardToBlocks(card) {
|
|
|
50
51
|
blocks.push({ type: "image", image_url: card.imageUrl, alt_text: card.title ?? "" });
|
|
51
52
|
}
|
|
52
53
|
for (const child of card.children) {
|
|
53
|
-
appendChildBlocks(child, blocks);
|
|
54
|
+
appendChildBlocks(child, blocks, state);
|
|
54
55
|
}
|
|
55
56
|
return blocks;
|
|
56
57
|
}
|
|
@@ -72,7 +73,7 @@ export function cardToFallbackText(card) {
|
|
|
72
73
|
}
|
|
73
74
|
return lines.join("\n").trim();
|
|
74
75
|
}
|
|
75
|
-
function appendChildBlocks(child, blocks) {
|
|
76
|
+
function appendChildBlocks(child, blocks, state) {
|
|
76
77
|
switch (child.type) {
|
|
77
78
|
case "text":
|
|
78
79
|
blocks.push(textToBlock(child));
|
|
@@ -88,7 +89,7 @@ function appendChildBlocks(child, blocks) {
|
|
|
88
89
|
return;
|
|
89
90
|
case "section":
|
|
90
91
|
for (const inner of child.children) {
|
|
91
|
-
appendChildBlocks(inner, blocks);
|
|
92
|
+
appendChildBlocks(inner, blocks, state);
|
|
92
93
|
}
|
|
93
94
|
return;
|
|
94
95
|
case "fields":
|
|
@@ -98,7 +99,7 @@ function appendChildBlocks(child, blocks) {
|
|
|
98
99
|
blocks.push(linkToBlock(child));
|
|
99
100
|
return;
|
|
100
101
|
case "table":
|
|
101
|
-
blocks.push(
|
|
102
|
+
blocks.push(...tableToBlocks(child, state));
|
|
102
103
|
return;
|
|
103
104
|
default: {
|
|
104
105
|
const fallback = cardChildToFallbackText(child);
|
|
@@ -220,12 +221,34 @@ function linkToBlock(link) {
|
|
|
220
221
|
};
|
|
221
222
|
}
|
|
222
223
|
/**
|
|
223
|
-
* Renders a {@link TableElement} as
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
224
|
+
* Renders a {@link TableElement} as Slack's native table block when it
|
|
225
|
+
* fits Slack's limits. Slack allows one table block per message, so
|
|
226
|
+
* additional or oversized tables fall back to a fixed-width mrkdwn
|
|
227
|
+
* approximation.
|
|
227
228
|
*/
|
|
228
|
-
function
|
|
229
|
+
function tableToBlocks(table, state) {
|
|
230
|
+
const MAX_NATIVE_TABLE_ROWS = 100;
|
|
231
|
+
const MAX_NATIVE_TABLE_COLUMNS = 20;
|
|
232
|
+
if (!state.usedNativeTable &&
|
|
233
|
+
table.rows.length <= MAX_NATIVE_TABLE_ROWS &&
|
|
234
|
+
table.headers.length <= MAX_NATIVE_TABLE_COLUMNS) {
|
|
235
|
+
state.usedNativeTable = true;
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
type: "table",
|
|
239
|
+
rows: [
|
|
240
|
+
table.headers.map(tableCellToRawText),
|
|
241
|
+
...table.rows.map((row) => row.map(tableCellToRawText)),
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
return [tableToFallbackBlock(table)];
|
|
247
|
+
}
|
|
248
|
+
function tableCellToRawText(value) {
|
|
249
|
+
return { type: "raw_text", text: value || " " };
|
|
250
|
+
}
|
|
251
|
+
function tableToFallbackBlock(table) {
|
|
229
252
|
const widths = table.headers.map((header, columnIndex) => {
|
|
230
253
|
let width = header.length;
|
|
231
254
|
for (const row of table.rows) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "experimental-ash",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"bin": {
|
|
5
5
|
"ash": "./bin/ash.js",
|
|
6
6
|
"experimental-ash": "./bin/ash.js"
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"@workflow/core": "5.0.0-beta.5",
|
|
161
161
|
"@workflow/errors": "5.0.0-beta.2",
|
|
162
162
|
"@workflow/world-local": "5.0.0-beta.4",
|
|
163
|
-
"ai": "7.0.0-canary.
|
|
163
|
+
"ai": "7.0.0-canary.138",
|
|
164
164
|
"autoevals": "0.0.132",
|
|
165
165
|
"chat": "4.28.1",
|
|
166
166
|
"chokidar": "5.0.0",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
},
|
|
177
177
|
"peerDependencies": {
|
|
178
178
|
"@opentelemetry/api": "^1.9.0",
|
|
179
|
-
"ai": "7.0.0-canary.
|
|
179
|
+
"ai": "7.0.0-canary.138",
|
|
180
180
|
"braintrust": ">=3.0.0"
|
|
181
181
|
},
|
|
182
182
|
"peerDependenciesMeta": {
|