@buzzni/saycode-cli 0.9.9 → 0.9.11
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 +80 -1
- package/channel-extension-host.mjs +341 -23
- package/index.mjs +44065 -33724
- package/package.json +1 -1
- package/windows-host/saycode-windows-host.exe +0 -0
|
@@ -7,7 +7,7 @@ const ASSET_PERMISSIONS = ["assets.read", "assets.write", "files.select", "draft
|
|
|
7
7
|
function usesAssetWorkflow(permissions) {
|
|
8
8
|
return permissions.some((permission) => ASSET_PERMISSIONS.includes(permission));
|
|
9
9
|
}
|
|
10
|
-
const LONG_RUNNING_PERMISSIONS = ["localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "machine.run"];
|
|
10
|
+
const LONG_RUNNING_PERMISSIONS = ["localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "browser.runtimeSetup", "machine.run"];
|
|
11
11
|
function extensionRequestTimeoutMs(permissions, side) {
|
|
12
12
|
const long = usesAssetWorkflow(permissions) || permissions.some((permission) => LONG_RUNNING_PERMISSIONS.includes(permission));
|
|
13
13
|
if (!long) return void 0;
|
|
@@ -262,7 +262,7 @@ function path(value) {
|
|
|
262
262
|
}
|
|
263
263
|
function parseManagedLocalTool(value, extensionId2) {
|
|
264
264
|
const root = row(value);
|
|
265
|
-
keys(root, ["version", "id", "toolVersion", "platforms", "operations"]);
|
|
265
|
+
keys(root, ["version", "id", "toolVersion", "platforms", "operations", "background"]);
|
|
266
266
|
if (root.version !== 1 || !ID.test(text$2(root.id)) || !String(root.id).startsWith(extensionId2 + ".") || !/^\d+\.\d+\.\d+$/.test(text$2(root.toolVersion))) fail$4();
|
|
267
267
|
const platforms = {};
|
|
268
268
|
for (const [platform, raw] of Object.entries(row(root.platforms))) {
|
|
@@ -319,7 +319,23 @@ function parseManagedLocalTool(value, extensionId2) {
|
|
|
319
319
|
operations[name] = operation;
|
|
320
320
|
}
|
|
321
321
|
if (!Object.keys(platforms).length || !Object.keys(operations).length || Object.keys(operations).length > 32) fail$4();
|
|
322
|
-
|
|
322
|
+
const background = root.background === void 0 ? void 0 : parseBackground(root.background, platforms, operations);
|
|
323
|
+
return { version: 1, id: String(root.id), toolVersion: String(root.toolVersion), platforms, operations, ...background ? { background } : {} };
|
|
324
|
+
}
|
|
325
|
+
function parseBackground(value, platforms, operations) {
|
|
326
|
+
const source = row(value);
|
|
327
|
+
keys(source, ["argv", "stopOperation"]);
|
|
328
|
+
if (!Object.keys(platforms).some((platform) => platform.startsWith("win32-") || platform.startsWith("darwin-"))) fail$4();
|
|
329
|
+
if (!Array.isArray(source.argv) || !source.argv.length || source.argv.length > 8) fail$4();
|
|
330
|
+
const argv = source.argv.map((arg) => {
|
|
331
|
+
const value2 = text$2(arg);
|
|
332
|
+
if (value2.includes("{{")) fail$4();
|
|
333
|
+
return value2;
|
|
334
|
+
});
|
|
335
|
+
if (source.stopOperation === void 0) return { argv };
|
|
336
|
+
const stop = operations[text$2(source.stopOperation)];
|
|
337
|
+
if (!stop || Object.keys(stop.inputs ?? {}).length || stop.payload !== void 0) fail$4();
|
|
338
|
+
return { argv, stopOperation: String(source.stopOperation) };
|
|
323
339
|
}
|
|
324
340
|
function managedToolArguments(operation, parameters, session) {
|
|
325
341
|
const inputs = operation.inputs ?? {};
|
|
@@ -469,8 +485,11 @@ function parseExtensionArtifactActions(value, path2 = "manifest.contributes.arti
|
|
|
469
485
|
};
|
|
470
486
|
});
|
|
471
487
|
}
|
|
472
|
-
const CHANNEL_API_VERSION =
|
|
488
|
+
const CHANNEL_API_VERSION = 2;
|
|
473
489
|
const CHANNEL_MINIMUM_API_VERSION = 1;
|
|
490
|
+
function isSupportedChannelApiVersion(value) {
|
|
491
|
+
return typeof value === "number" && Number.isInteger(value) && value >= CHANNEL_MINIMUM_API_VERSION && value <= CHANNEL_API_VERSION;
|
|
492
|
+
}
|
|
474
493
|
const CHANNEL_PROVIDERS = ["telegram", "slack", "discord"];
|
|
475
494
|
const CHANNEL_COMMAND_SCHEMA = Object.freeze({
|
|
476
495
|
pair: Object.freeze(["code!"]),
|
|
@@ -611,6 +630,7 @@ const EXTENSION_PERMISSIONS = [
|
|
|
611
630
|
"localTools.install",
|
|
612
631
|
"localTools.control",
|
|
613
632
|
"localBrowser.setup",
|
|
633
|
+
"browser.runtimeSetup",
|
|
614
634
|
"network.fetch",
|
|
615
635
|
"notifications.show",
|
|
616
636
|
"storage.read",
|
|
@@ -633,18 +653,22 @@ const EXTENSION_PERMISSIONS = [
|
|
|
633
653
|
"channels.transport",
|
|
634
654
|
"sessions.read",
|
|
635
655
|
"sessions.control",
|
|
636
|
-
"sessions.approveOnce"
|
|
656
|
+
"sessions.approveOnce",
|
|
657
|
+
// Core → adapter, not a capability the adapter invokes: Core sends the current input and the
|
|
658
|
+
// thread's earlier messages (other people's words included) for display-text normalization
|
|
659
|
+
// (specs/slack-bot-improvements contracts §2). Channel API 2 only.
|
|
660
|
+
"channels.normalize"
|
|
637
661
|
];
|
|
638
662
|
const CONVERSATION_MODS_INTERNAL = typeof __SAYCODE_MODS_INTERNAL__ !== "undefined" && __SAYCODE_MODS_INTERNAL__ === true;
|
|
639
663
|
const SAYCODE_EXTENSION_API_VERSION = CONVERSATION_MODS_INTERNAL ? 5 : 4;
|
|
640
664
|
const SAYCODE_EXTENSION_MINIMUM_API_VERSION = CONVERSATION_MODS_INTERNAL ? 4 : 2;
|
|
641
665
|
const MAX_EXTENSION_API_WINDOW_WIDTH = 2;
|
|
642
|
-
const SAYCODE_EXTENSION_ENGINE_VERSION = "1.
|
|
666
|
+
const SAYCODE_EXTENSION_ENGINE_VERSION = "1.3.0";
|
|
643
667
|
const SAYCODE_CHANNEL_API_VERSION = CHANNEL_API_VERSION;
|
|
644
668
|
const SAYCODE_CHANNEL_MINIMUM_API_VERSION = CHANNEL_MINIMUM_API_VERSION;
|
|
645
669
|
class UnsupportedChannelApiVersionError extends Error {
|
|
646
670
|
constructor(channelApiVersion) {
|
|
647
|
-
super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${
|
|
671
|
+
super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${`${SAYCODE_CHANNEL_MINIMUM_API_VERSION}-${SAYCODE_CHANNEL_API_VERSION}`}`);
|
|
648
672
|
this.channelApiVersion = channelApiVersion;
|
|
649
673
|
}
|
|
650
674
|
name = "UnsupportedChannelApiVersionError";
|
|
@@ -667,7 +691,7 @@ const V2_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
|
667
691
|
"browserViewer.install",
|
|
668
692
|
"browserViewer.installChrome"
|
|
669
693
|
]);
|
|
670
|
-
const V3_PERMISSIONS = /* @__PURE__ */ new Set(["machine.run", "artifacts.publishPublic", "assets.read", "assets.write", "files.select", "drafts.prepare", "localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup"]);
|
|
694
|
+
const V3_PERMISSIONS = /* @__PURE__ */ new Set(["machine.run", "artifacts.publishPublic", "assets.read", "assets.write", "files.select", "drafts.prepare", "localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "browser.runtimeSetup"]);
|
|
671
695
|
const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
672
696
|
"channels.receive",
|
|
673
697
|
"channels.ack",
|
|
@@ -675,7 +699,8 @@ const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
|
675
699
|
"channels.transport",
|
|
676
700
|
"sessions.read",
|
|
677
701
|
"sessions.control",
|
|
678
|
-
"sessions.approveOnce"
|
|
702
|
+
"sessions.approveOnce",
|
|
703
|
+
"channels.normalize"
|
|
679
704
|
]);
|
|
680
705
|
const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
681
706
|
"id",
|
|
@@ -925,6 +950,9 @@ function parseExtensionManifest(value, options) {
|
|
|
925
950
|
if (channelApiVersion === void 0) {
|
|
926
951
|
fail$5(`manifest.permissions[${index}]`, "requires manifest.channelApiVersion");
|
|
927
952
|
}
|
|
953
|
+
if (typed === "channels.normalize" && channelApiVersion < 2) {
|
|
954
|
+
fail$5(`manifest.permissions[${index}]`, "channels.normalize requires channel API version 2");
|
|
955
|
+
}
|
|
928
956
|
}
|
|
929
957
|
return typed;
|
|
930
958
|
});
|
|
@@ -1073,6 +1101,246 @@ function parseChannelApprovalRequest(value) {
|
|
|
1073
1101
|
}
|
|
1074
1102
|
return { approvalHandle, approveLabel, denyLabel };
|
|
1075
1103
|
}
|
|
1104
|
+
const NORMALIZE_ADDITION_SOURCES = ["blocks", "quote", "forward", "table", "unfurl", "files"];
|
|
1105
|
+
const MAX_NORMALIZE_ITEMS = 200;
|
|
1106
|
+
const MAX_USER_LABELS = 200;
|
|
1107
|
+
const MAX_BLOCKS = 50;
|
|
1108
|
+
const MAX_ATTACHMENTS = 20;
|
|
1109
|
+
const MAX_FILES = 20;
|
|
1110
|
+
const MAX_RICH_TEXT_DEPTH = 6;
|
|
1111
|
+
const MAX_URL_LENGTH = 2048;
|
|
1112
|
+
const MAX_ADDITIONS_BYTES = 8192;
|
|
1113
|
+
const MAX_PRIMARY_BYTES = 65536;
|
|
1114
|
+
const ITEM_REF = /^i(0|[1-9]\d?|1\d\d)$/;
|
|
1115
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1116
|
+
const isString = (value) => typeof value === "string";
|
|
1117
|
+
function projectSlackMessage(raw) {
|
|
1118
|
+
const state = { truncated: false };
|
|
1119
|
+
const message = isRecord(raw) ? raw : {};
|
|
1120
|
+
const projection = {};
|
|
1121
|
+
const blocks = projectBlocks(message.blocks, state);
|
|
1122
|
+
if (blocks) projection.blocks = blocks;
|
|
1123
|
+
const attachments = capped(Array.isArray(message.attachments) ? message.attachments.filter(isRecord) : [], MAX_ATTACHMENTS, state).map((attachment) => projectAttachment(attachment, state));
|
|
1124
|
+
if (attachments.length > 0) projection.attachments = attachments;
|
|
1125
|
+
const files = capped(Array.isArray(message.files) ? message.files.filter(isRecord) : [], MAX_FILES, state).map(projectFile);
|
|
1126
|
+
if (files.length > 0) projection.files = files;
|
|
1127
|
+
if (isString(message.subtype)) projection.subtype = message.subtype;
|
|
1128
|
+
return { projection, truncated: state.truncated };
|
|
1129
|
+
}
|
|
1130
|
+
function capped(items, max, state) {
|
|
1131
|
+
if (items.length > max) state.truncated = true;
|
|
1132
|
+
return items.slice(0, max);
|
|
1133
|
+
}
|
|
1134
|
+
function projectBlocks(value, state) {
|
|
1135
|
+
if (!Array.isArray(value)) return void 0;
|
|
1136
|
+
const projected = value.map((block) => projectBlock(block, state)).filter((block) => block !== null);
|
|
1137
|
+
return projected.length > 0 ? capped(projected, MAX_BLOCKS, state) : void 0;
|
|
1138
|
+
}
|
|
1139
|
+
function projectBlock(block, state) {
|
|
1140
|
+
if (!isRecord(block)) return null;
|
|
1141
|
+
switch (block.type) {
|
|
1142
|
+
case "rich_text":
|
|
1143
|
+
return { type: "rich_text", elements: projectContainers(block.elements, 1, state) };
|
|
1144
|
+
case "section": {
|
|
1145
|
+
const section = { type: "section" };
|
|
1146
|
+
const text2 = projectTextObject(block.text);
|
|
1147
|
+
if (text2) section.text = text2;
|
|
1148
|
+
if (Array.isArray(block.fields)) section.fields = block.fields.map(projectTextObject).filter((field) => field !== null);
|
|
1149
|
+
return section;
|
|
1150
|
+
}
|
|
1151
|
+
case "header": {
|
|
1152
|
+
const text2 = projectTextObject(block.text);
|
|
1153
|
+
return text2 ? { type: "header", text: text2 } : null;
|
|
1154
|
+
}
|
|
1155
|
+
case "context":
|
|
1156
|
+
return { type: "context", elements: Array.isArray(block.elements) ? block.elements.map(projectTextObject).filter((element) => element !== null) : [] };
|
|
1157
|
+
case "table":
|
|
1158
|
+
return { type: "table", rows: Array.isArray(block.rows) ? block.rows.filter(Array.isArray).map((row2) => row2.flatMap((cell) => {
|
|
1159
|
+
if (isRecord(cell) && cell.type === "raw_text" && isString(cell.text)) return [{ type: "raw_text", text: cell.text }];
|
|
1160
|
+
if (isRecord(cell) && cell.type === "rich_text") return [{ type: "rich_text", elements: projectContainers(cell.elements, 1, state) }];
|
|
1161
|
+
return [];
|
|
1162
|
+
})) : [] };
|
|
1163
|
+
default:
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function projectTextObject(value) {
|
|
1168
|
+
if (!isRecord(value) || value.type !== "mrkdwn" && value.type !== "plain_text" || !isString(value.text)) return null;
|
|
1169
|
+
return { type: value.type, text: value.text };
|
|
1170
|
+
}
|
|
1171
|
+
const CONTAINER_TYPES = /* @__PURE__ */ new Set(["rich_text_section", "rich_text_quote", "rich_text_preformatted", "rich_text_list"]);
|
|
1172
|
+
function projectContainers(value, depth, state) {
|
|
1173
|
+
if (!Array.isArray(value)) return [];
|
|
1174
|
+
return value.flatMap((node) => {
|
|
1175
|
+
const container = isRecord(node) && CONTAINER_TYPES.has(String(node.type)) ? projectContainer(node, depth, state) : null;
|
|
1176
|
+
return container ? [container] : [];
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
function projectContainer(node, depth, state) {
|
|
1180
|
+
if (depth > MAX_RICH_TEXT_DEPTH) {
|
|
1181
|
+
state.truncated = true;
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
const elements = (Array.isArray(node.elements) ? node.elements : []).flatMap((child) => {
|
|
1185
|
+
if (isRecord(child) && CONTAINER_TYPES.has(String(child.type))) {
|
|
1186
|
+
const nested = projectContainer(child, depth + 1, state);
|
|
1187
|
+
return nested ? [nested] : [];
|
|
1188
|
+
}
|
|
1189
|
+
const leaf = projectLeaf(child);
|
|
1190
|
+
return leaf ? [leaf] : [];
|
|
1191
|
+
});
|
|
1192
|
+
const container = { type: node.type, elements };
|
|
1193
|
+
if (node.style === "bullet" || node.style === "ordered") container.style = node.style;
|
|
1194
|
+
return container;
|
|
1195
|
+
}
|
|
1196
|
+
function projectLeaf(node) {
|
|
1197
|
+
if (!isRecord(node)) return null;
|
|
1198
|
+
switch (node.type) {
|
|
1199
|
+
case "text": {
|
|
1200
|
+
if (!isString(node.text)) return null;
|
|
1201
|
+
const leaf = { type: "text", text: node.text };
|
|
1202
|
+
if (isRecord(node.style)) {
|
|
1203
|
+
const style = Object.fromEntries(["bold", "italic", "strike", "code"].filter((key) => typeof node.style[key] === "boolean").map((key) => [key, node.style[key]]));
|
|
1204
|
+
if (Object.keys(style).length > 0) leaf.style = style;
|
|
1205
|
+
}
|
|
1206
|
+
return leaf;
|
|
1207
|
+
}
|
|
1208
|
+
case "link": {
|
|
1209
|
+
const label = isString(node.text) ? node.text : void 0;
|
|
1210
|
+
if (!httpUrl(node.url)) return label === void 0 ? null : { type: "text", text: label };
|
|
1211
|
+
return label === void 0 ? { type: "link", url: node.url } : { type: "link", url: node.url, text: label };
|
|
1212
|
+
}
|
|
1213
|
+
case "user":
|
|
1214
|
+
return isString(node.user_id) ? { type: "user", user_id: node.user_id } : null;
|
|
1215
|
+
case "channel":
|
|
1216
|
+
return isString(node.channel_id) ? { type: "channel", channel_id: node.channel_id } : null;
|
|
1217
|
+
case "usergroup":
|
|
1218
|
+
return isString(node.usergroup_id) ? { type: "usergroup", usergroup_id: node.usergroup_id } : null;
|
|
1219
|
+
case "emoji":
|
|
1220
|
+
return isString(node.name) ? { type: "emoji", name: node.name } : null;
|
|
1221
|
+
case "broadcast":
|
|
1222
|
+
return node.range === "here" || node.range === "channel" || node.range === "everyone" ? { type: "broadcast", range: node.range } : null;
|
|
1223
|
+
default:
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const ATTACHMENT_TEXT_KEYS = ["text", "fallback", "pretext", "title", "author_name", "footer"];
|
|
1228
|
+
const ATTACHMENT_URL_KEYS = ["title_link", "from_url", "original_url"];
|
|
1229
|
+
function projectAttachment(attachment, state) {
|
|
1230
|
+
const projected = {};
|
|
1231
|
+
for (const key of ATTACHMENT_TEXT_KEYS) if (isString(attachment[key])) projected[key] = attachment[key];
|
|
1232
|
+
for (const key of ATTACHMENT_URL_KEYS) if (httpUrl(attachment[key])) projected[key] = attachment[key];
|
|
1233
|
+
if (typeof attachment.is_share === "boolean") projected.is_share = attachment.is_share;
|
|
1234
|
+
if (typeof attachment.is_msg_unfurl === "boolean") projected.is_msg_unfurl = attachment.is_msg_unfurl;
|
|
1235
|
+
if (Array.isArray(attachment.message_blocks)) {
|
|
1236
|
+
projected.message_blocks = attachment.message_blocks.filter(isRecord).map((forwarded) => ({
|
|
1237
|
+
message: { blocks: projectBlocks(isRecord(forwarded.message) ? forwarded.message.blocks : void 0, state) ?? [] }
|
|
1238
|
+
}));
|
|
1239
|
+
}
|
|
1240
|
+
return projected;
|
|
1241
|
+
}
|
|
1242
|
+
function projectFile(file) {
|
|
1243
|
+
const projected = {};
|
|
1244
|
+
for (const key of ["name", "title", "filetype", "mimetype"]) if (isString(file[key])) projected[key] = file[key];
|
|
1245
|
+
if (Number.isSafeInteger(file.size) && file.size >= 0) projected.size = file.size;
|
|
1246
|
+
return projected;
|
|
1247
|
+
}
|
|
1248
|
+
function httpUrl(value) {
|
|
1249
|
+
return isString(value) && value.length <= MAX_URL_LENGTH && /^https?:\/\//i.test(value);
|
|
1250
|
+
}
|
|
1251
|
+
function parseChannelNormalizeRequest(value) {
|
|
1252
|
+
const request = plainRecord(value, "normalize request");
|
|
1253
|
+
exactKeySet(request, ["apiVersion", "channelId", "connectionId", "provider", "connectionRevision", "requestId", "purpose", "userLabels", "items"], "normalize request");
|
|
1254
|
+
if (request.apiVersion !== 2 || request.provider !== "slack") fail("normalize request is channel API 2 Slack only");
|
|
1255
|
+
if (request.purpose !== "currentInput" && request.purpose !== "threadHistory") fail("invalid normalize purpose");
|
|
1256
|
+
if (!Number.isSafeInteger(request.connectionRevision) || request.connectionRevision < 1) fail("invalid normalize connection revision");
|
|
1257
|
+
const labels = plainRecord(request.userLabels, "normalize userLabels");
|
|
1258
|
+
if (Object.keys(labels).length > MAX_USER_LABELS || Object.entries(labels).some(([key, label]) => key.length === 0 || key.length > 64 || !isString(label) || label.length > 256)) {
|
|
1259
|
+
fail("invalid normalize userLabels");
|
|
1260
|
+
}
|
|
1261
|
+
if (!Array.isArray(request.items) || request.items.length < 1 || request.items.length > MAX_NORMALIZE_ITEMS) fail("invalid normalize items");
|
|
1262
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1263
|
+
const items = request.items.map((value2) => {
|
|
1264
|
+
const item = plainRecord(value2, "normalize item");
|
|
1265
|
+
exactKeySet(item, ["itemRef", "primaryText", "projection", "maxAdditionsBytes"], "normalize item");
|
|
1266
|
+
if (!isString(item.itemRef) || !ITEM_REF.test(item.itemRef) || refs.has(item.itemRef)) fail("invalid normalize itemRef");
|
|
1267
|
+
refs.add(item.itemRef);
|
|
1268
|
+
if (!isString(item.primaryText) || utf8Bytes(item.primaryText) > MAX_PRIMARY_BYTES) fail("invalid normalize primaryText");
|
|
1269
|
+
if (!Number.isSafeInteger(item.maxAdditionsBytes) || item.maxAdditionsBytes < 1 || item.maxAdditionsBytes > MAX_ADDITIONS_BYTES) fail("invalid normalize maxAdditionsBytes");
|
|
1270
|
+
const projected = projectSlackMessage(item.projection);
|
|
1271
|
+
if (!isRecord(item.projection) || projected.truncated || !sameJson(projected.projection, item.projection)) fail("normalize projection is not a Core projection");
|
|
1272
|
+
return { itemRef: item.itemRef, primaryText: item.primaryText, projection: projected.projection, maxAdditionsBytes: item.maxAdditionsBytes };
|
|
1273
|
+
});
|
|
1274
|
+
return {
|
|
1275
|
+
apiVersion: 2,
|
|
1276
|
+
provider: "slack",
|
|
1277
|
+
purpose: request.purpose,
|
|
1278
|
+
channelId: boundedText(request.channelId, "channelId"),
|
|
1279
|
+
connectionId: boundedText(request.connectionId, "connectionId"),
|
|
1280
|
+
connectionRevision: request.connectionRevision,
|
|
1281
|
+
requestId: boundedText(request.requestId, "requestId"),
|
|
1282
|
+
userLabels: { ...labels },
|
|
1283
|
+
items
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function parseChannelNormalizeResult(request, value) {
|
|
1287
|
+
const result = plainRecord(value, "normalize result");
|
|
1288
|
+
exactKeySet(result, ["items"], "normalize result");
|
|
1289
|
+
if (!Array.isArray(result.items)) fail("invalid normalize result items");
|
|
1290
|
+
const wanted = new Map(request.items.map((item) => [item.itemRef, item]));
|
|
1291
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1292
|
+
for (const value2 of result.items) {
|
|
1293
|
+
const item = plainRecord(value2, "normalize result item");
|
|
1294
|
+
exactKeySet(item, ["itemRef", "additions", "truncated"], "normalize result item");
|
|
1295
|
+
const requested = isString(item.itemRef) ? wanted.get(item.itemRef) : void 0;
|
|
1296
|
+
if (!requested || parsed.has(requested.itemRef)) fail("normalize result items differ from the request");
|
|
1297
|
+
if (typeof item.truncated !== "boolean" || !Array.isArray(item.additions)) fail("invalid normalize result item");
|
|
1298
|
+
let bytes = 0;
|
|
1299
|
+
const additions = item.additions.map((value3) => {
|
|
1300
|
+
const addition = plainRecord(value3, "normalize addition");
|
|
1301
|
+
exactKeySet(addition, ["source", "text"], "normalize addition");
|
|
1302
|
+
if (!NORMALIZE_ADDITION_SOURCES.includes(addition.source) || !isString(addition.text) || addition.text.includes("\0") || hasLoneSurrogate(addition.text)) fail("invalid normalize addition");
|
|
1303
|
+
bytes += utf8Bytes(addition.text);
|
|
1304
|
+
return { source: addition.source, text: addition.text };
|
|
1305
|
+
});
|
|
1306
|
+
if (bytes > requested.maxAdditionsBytes) fail("normalize additions exceed their budget");
|
|
1307
|
+
parsed.set(requested.itemRef, { itemRef: requested.itemRef, additions: withoutRepeatedLines(requested.primaryText, additions), truncated: item.truncated });
|
|
1308
|
+
}
|
|
1309
|
+
if (parsed.size !== wanted.size) fail("normalize result items differ from the request");
|
|
1310
|
+
return request.items.map((item) => parsed.get(item.itemRef));
|
|
1311
|
+
}
|
|
1312
|
+
function withoutRepeatedLines(primaryText, additions) {
|
|
1313
|
+
const fold = (line) => line.replace(/\s+/gu, " ").trim();
|
|
1314
|
+
const known = new Set(primaryText.split("\n").map(fold).filter(Boolean));
|
|
1315
|
+
const kept = [];
|
|
1316
|
+
for (const addition of additions) {
|
|
1317
|
+
const lines2 = addition.text.split("\n");
|
|
1318
|
+
const remaining = lines2.filter((line) => fold(line) === "" || !known.has(fold(line)));
|
|
1319
|
+
for (const line of lines2) if (fold(line)) known.add(fold(line));
|
|
1320
|
+
if (remaining.some((line) => fold(line) !== "")) kept.push({ source: addition.source, text: remaining.join("\n") });
|
|
1321
|
+
}
|
|
1322
|
+
return kept;
|
|
1323
|
+
}
|
|
1324
|
+
function exactKeySet(record2, keys2, path2) {
|
|
1325
|
+
const actual = Object.keys(record2);
|
|
1326
|
+
if (actual.length !== keys2.length || !keys2.every((key) => actual.includes(key))) fail(`${path2} has unexpected keys`);
|
|
1327
|
+
}
|
|
1328
|
+
function boundedText(value, path2) {
|
|
1329
|
+
if (!isString(value) || value.length === 0 || value.length > 200) fail(`invalid normalize ${path2}`);
|
|
1330
|
+
return value;
|
|
1331
|
+
}
|
|
1332
|
+
function sameJson(a, b) {
|
|
1333
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1334
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameJson(item, b[index]));
|
|
1335
|
+
}
|
|
1336
|
+
if (isRecord(a) && isRecord(b)) {
|
|
1337
|
+
const keys2 = Object.keys(a);
|
|
1338
|
+
return keys2.length === Object.keys(b).length && keys2.every((key) => Object.hasOwn(b, key) && sameJson(a[key], b[key]));
|
|
1339
|
+
}
|
|
1340
|
+
return Object.is(a, b);
|
|
1341
|
+
}
|
|
1342
|
+
const utf8Bytes = (text2) => new TextEncoder().encode(text2).length;
|
|
1343
|
+
const hasLoneSurrogate = (text2) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(text2);
|
|
1076
1344
|
function finiteNumber(value, path2) {
|
|
1077
1345
|
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${path2} must be a finite number`);
|
|
1078
1346
|
return value;
|
|
@@ -1162,13 +1430,15 @@ function parseRequest(message) {
|
|
|
1162
1430
|
"maxChunkUtf16Units",
|
|
1163
1431
|
"maxChunks"
|
|
1164
1432
|
];
|
|
1433
|
+
const v2 = params.apiVersion === 2;
|
|
1165
1434
|
exactKeys(
|
|
1166
1435
|
params,
|
|
1167
|
-
"approval" in params ? [...formatKeys, "approval"] : formatKeys,
|
|
1436
|
+
v2 ? [...formatKeys, "mode", "dialect"] : "approval" in params ? [...formatKeys, "approval"] : formatKeys,
|
|
1168
1437
|
"request.params"
|
|
1169
1438
|
);
|
|
1439
|
+
if (v2 && (params.mode !== "verbatim" && params.mode !== "rendered" || params.dialect !== "slack-mrkdwn" || params.kind === "approval")) fail("invalid channel API 2 formatter request");
|
|
1170
1440
|
const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: {
|
|
1171
|
-
apiVersion: params.apiVersion,
|
|
1441
|
+
apiVersion: v2 ? 1 : params.apiVersion,
|
|
1172
1442
|
channelId: params.channelId,
|
|
1173
1443
|
connectionId: params.connectionId,
|
|
1174
1444
|
provider: params.provider,
|
|
@@ -1184,16 +1454,26 @@ function parseRequest(message) {
|
|
|
1184
1454
|
}
|
|
1185
1455
|
const approval = params.approval === void 0 ? void 0 : parseChannelApprovalRequest(params.approval);
|
|
1186
1456
|
const { state: _state, ...identity } = lifecycle.params;
|
|
1457
|
+
const bounds = { requestId, text: body, maxChunks: Number(params.maxChunks), maxChunkUtf16Units: Number(params.maxChunkUtf16Units) };
|
|
1458
|
+
if (v2) {
|
|
1459
|
+
return { kind: "request", id: id2, method, params: {
|
|
1460
|
+
...identity,
|
|
1461
|
+
...bounds,
|
|
1462
|
+
apiVersion: 2,
|
|
1463
|
+
kind: params.kind,
|
|
1464
|
+
mode: params.mode,
|
|
1465
|
+
dialect: "slack-mrkdwn"
|
|
1466
|
+
} };
|
|
1467
|
+
}
|
|
1187
1468
|
return { kind: "request", id: id2, method, params: {
|
|
1188
1469
|
...identity,
|
|
1189
|
-
|
|
1190
|
-
text: body,
|
|
1470
|
+
...bounds,
|
|
1191
1471
|
kind: params.kind,
|
|
1192
|
-
maxChunks: Number(params.maxChunks),
|
|
1193
|
-
maxChunkUtf16Units: Number(params.maxChunkUtf16Units),
|
|
1194
1472
|
...approval ? { approval } : {}
|
|
1195
1473
|
} };
|
|
1196
1474
|
}
|
|
1475
|
+
case "channel.normalizeMessages":
|
|
1476
|
+
return { kind: "request", id: id2, method, params: parseChannelNormalizeRequest(params) };
|
|
1197
1477
|
case "invokeCommand": {
|
|
1198
1478
|
exactKeys(params, ["commandId", "args"], "request.params");
|
|
1199
1479
|
if (!Array.isArray(params.args)) fail("request.params.args must be an array");
|
|
@@ -1276,6 +1556,17 @@ function parseExtensionWireMessage(value) {
|
|
|
1276
1556
|
if (message.kind === "event") return parseEvent(message);
|
|
1277
1557
|
fail("message.kind is unknown");
|
|
1278
1558
|
}
|
|
1559
|
+
function parseChannelRenderedReplyResult(value) {
|
|
1560
|
+
const result = plainRecord(value, "channel rendered result");
|
|
1561
|
+
if (Object.keys(result).join() !== "rendered" || !Array.isArray(result.rendered)) fail("invalid channel rendered result");
|
|
1562
|
+
return {
|
|
1563
|
+
rendered: result.rendered.map((value2) => {
|
|
1564
|
+
const chunk = plainRecord(value2, "channel rendered chunk");
|
|
1565
|
+
if (Object.keys(chunk).sort().join() !== "sourceEnd,sourceStart,text" || typeof chunk.text !== "string" || !Number.isSafeInteger(chunk.sourceStart) || !Number.isSafeInteger(chunk.sourceEnd)) fail("invalid channel rendered chunk");
|
|
1566
|
+
return { text: chunk.text, sourceStart: chunk.sourceStart, sourceEnd: chunk.sourceEnd };
|
|
1567
|
+
})
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1279
1570
|
const TIMEOUT = Symbol("extension-request-timeout");
|
|
1280
1571
|
function requestEnvelope(value) {
|
|
1281
1572
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -1297,6 +1588,7 @@ class ExtensionHostRuntime {
|
|
|
1297
1588
|
widgets = /* @__PURE__ */ new Map();
|
|
1298
1589
|
channelHandlers = /* @__PURE__ */ new Map();
|
|
1299
1590
|
channelFormatters = /* @__PURE__ */ new Map();
|
|
1591
|
+
channelNormalizers = /* @__PURE__ */ new Map();
|
|
1300
1592
|
channelIntakeHandlers = /* @__PURE__ */ new Map();
|
|
1301
1593
|
channelStates = /* @__PURE__ */ new Map();
|
|
1302
1594
|
channelCalls = /* @__PURE__ */ new Map();
|
|
@@ -1432,6 +1724,7 @@ class ExtensionHostRuntime {
|
|
|
1432
1724
|
this.commandHandlers.clear();
|
|
1433
1725
|
this.channelHandlers.clear();
|
|
1434
1726
|
this.channelFormatters.clear();
|
|
1727
|
+
this.channelNormalizers.clear();
|
|
1435
1728
|
this.channelIntakeHandlers.clear();
|
|
1436
1729
|
this.channelStates.clear();
|
|
1437
1730
|
this.channelCalls.clear();
|
|
@@ -1475,6 +1768,7 @@ class ExtensionHostRuntime {
|
|
|
1475
1768
|
this.commandHandlers.clear();
|
|
1476
1769
|
this.channelHandlers.clear();
|
|
1477
1770
|
this.channelFormatters.clear();
|
|
1771
|
+
this.channelNormalizers.clear();
|
|
1478
1772
|
this.channelIntakeHandlers.clear();
|
|
1479
1773
|
this.channelStates.clear();
|
|
1480
1774
|
this.channelCalls.clear();
|
|
@@ -1495,6 +1789,7 @@ class ExtensionHostRuntime {
|
|
|
1495
1789
|
}
|
|
1496
1790
|
if (request.method === "channel.lifecycle") return this.deliverChannel(request.params);
|
|
1497
1791
|
if (request.method === "channel.formatReply") return this.formatChannelReply(request.params);
|
|
1792
|
+
if (request.method === "channel.normalizeMessages") return this.normalizeChannelMessages(request.params);
|
|
1498
1793
|
if (request.method === "channel.intake") return this.deliverChannelIntake(request.params);
|
|
1499
1794
|
const handler = this.commandHandlers.get(request.params.commandId);
|
|
1500
1795
|
if (!handler) {
|
|
@@ -1507,7 +1802,7 @@ class ExtensionHostRuntime {
|
|
|
1507
1802
|
}
|
|
1508
1803
|
async deliverChannel(event) {
|
|
1509
1804
|
const contribution = this.options.manifest.contributes.channels?.find((channel) => channel.id === event.channelId);
|
|
1510
|
-
if (this.options.manifest.channelApiVersion
|
|
1805
|
+
if (!isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !contribution || contribution.provider !== event.provider) {
|
|
1511
1806
|
throw { code: "CHANNEL_NOT_DECLARED", message: "channel contribution/provider is not declared" };
|
|
1512
1807
|
}
|
|
1513
1808
|
const key = JSON.stringify([event.channelId, event.connectionId]);
|
|
@@ -1553,16 +1848,29 @@ class ExtensionHostRuntime {
|
|
|
1553
1848
|
return null;
|
|
1554
1849
|
}
|
|
1555
1850
|
async formatChannelReply(input) {
|
|
1556
|
-
const
|
|
1557
|
-
const current = this.channelStates.get(key);
|
|
1558
|
-
const epoch = this.channelEpoch;
|
|
1851
|
+
const isCurrent = this.channelCallIsCurrent(input);
|
|
1559
1852
|
const formatter = this.channelFormatters.get(input.channelId);
|
|
1560
|
-
const isCurrent = () => !this.disposed && !this.deactivating && this.active && epoch === this.channelEpoch && this.channelStates.get(key) === current && current?.state === "start" && current.provider === input.provider && current.connectionRevision === input.connectionRevision;
|
|
1561
1853
|
if (!isCurrent() || !formatter) throw { code: "CHANNEL_FORMAT_UNAVAILABLE", message: "channel formatter is not active" };
|
|
1562
1854
|
const result = await formatter(Object.freeze({ ...input }));
|
|
1563
1855
|
if (!isCurrent()) throw { code: "CHANNEL_FORMAT_WITHDRAWN", message: "channel formatter stopped" };
|
|
1856
|
+
if (input.apiVersion === 2 && input.mode === "rendered") return parseChannelRenderedReplyResult(result);
|
|
1564
1857
|
return parseChannelReplyFormatResult(result, input);
|
|
1565
1858
|
}
|
|
1859
|
+
async normalizeChannelMessages(input) {
|
|
1860
|
+
const isCurrent = this.channelCallIsCurrent(input);
|
|
1861
|
+
const normalizer = this.channelNormalizers.get(input.channelId);
|
|
1862
|
+
if (!isCurrent() || !normalizer) throw { code: "CHANNEL_NORMALIZE_UNAVAILABLE", message: "channel normalizer is not active" };
|
|
1863
|
+
const result = await normalizer(Object.freeze({ ...input }));
|
|
1864
|
+
if (!isCurrent()) throw { code: "CHANNEL_NORMALIZE_WITHDRAWN", message: "channel normalizer stopped" };
|
|
1865
|
+
return { items: parseChannelNormalizeResult(input, result) };
|
|
1866
|
+
}
|
|
1867
|
+
/** A formatter or normalizer answers only for the started connection generation it was asked about. */
|
|
1868
|
+
channelCallIsCurrent(input) {
|
|
1869
|
+
const key = JSON.stringify([input.channelId, input.connectionId]);
|
|
1870
|
+
const current = this.channelStates.get(key);
|
|
1871
|
+
const epoch = this.channelEpoch;
|
|
1872
|
+
return () => !this.disposed && !this.deactivating && this.active && epoch === this.channelEpoch && this.channelStates.get(key) === current && current?.state === "start" && current.provider === input.provider && current.connectionRevision === input.connectionRevision;
|
|
1873
|
+
}
|
|
1566
1874
|
async withTimeout(operation) {
|
|
1567
1875
|
let timer;
|
|
1568
1876
|
const timeout = new Promise((resolve2) => {
|
|
@@ -1596,6 +1904,7 @@ class ExtensionHostRuntime {
|
|
|
1596
1904
|
const registered = /* @__PURE__ */ new Map();
|
|
1597
1905
|
const registeredChannels = /* @__PURE__ */ new Map();
|
|
1598
1906
|
const registeredFormatters = /* @__PURE__ */ new Map();
|
|
1907
|
+
const registeredNormalizers = /* @__PURE__ */ new Map();
|
|
1599
1908
|
const registeredIntake = /* @__PURE__ */ new Map();
|
|
1600
1909
|
await loaded.activate({
|
|
1601
1910
|
extensionId: this.options.manifest.id,
|
|
@@ -1605,19 +1914,26 @@ class ExtensionHostRuntime {
|
|
|
1605
1914
|
} },
|
|
1606
1915
|
channels: {
|
|
1607
1916
|
register: (channelId, handler) => {
|
|
1608
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1917
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
|
|
1609
1918
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "channel must be declared and registered once" };
|
|
1610
1919
|
}
|
|
1611
1920
|
registeredChannels.set(channelId, handler);
|
|
1612
1921
|
},
|
|
1613
1922
|
registerFormatter: (channelId, formatter) => {
|
|
1614
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1923
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof formatter !== "function" || registeredFormatters.has(channelId)) {
|
|
1615
1924
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "formatter must be declared and registered once" };
|
|
1616
1925
|
}
|
|
1617
1926
|
registeredFormatters.set(channelId, formatter);
|
|
1618
1927
|
},
|
|
1928
|
+
registerNormalizer: (channelId, normalizer) => {
|
|
1929
|
+
const manifest = this.options.manifest;
|
|
1930
|
+
if (!this.activating || (manifest.channelApiVersion ?? 0) < 2 || !manifest.permissions.includes("channels.normalize") || !manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof normalizer !== "function" || registeredNormalizers.has(channelId)) {
|
|
1931
|
+
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "normalizer needs channel API 2, channels.normalize and one registration" };
|
|
1932
|
+
}
|
|
1933
|
+
registeredNormalizers.set(channelId, normalizer);
|
|
1934
|
+
},
|
|
1619
1935
|
onIntake: (channelId, handler) => {
|
|
1620
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1936
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredIntake.has(channelId)) {
|
|
1621
1937
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "intake handler must be declared and registered once" };
|
|
1622
1938
|
}
|
|
1623
1939
|
registeredIntake.set(channelId, handler);
|
|
@@ -1639,7 +1955,9 @@ class ExtensionHostRuntime {
|
|
|
1639
1955
|
for (const [id2, render] of registeredWidgets) this.widgets.set(id2, render);
|
|
1640
1956
|
this.channelHandlers.clear();
|
|
1641
1957
|
this.channelFormatters.clear();
|
|
1958
|
+
this.channelNormalizers.clear();
|
|
1642
1959
|
for (const [id2, formatter] of registeredFormatters) this.channelFormatters.set(id2, formatter);
|
|
1960
|
+
for (const [id2, normalizer] of registeredNormalizers) this.channelNormalizers.set(id2, normalizer);
|
|
1643
1961
|
this.channelIntakeHandlers.clear();
|
|
1644
1962
|
for (const [id2, handler] of registeredIntake) this.channelIntakeHandlers.set(id2, handler);
|
|
1645
1963
|
for (const [id2, handler] of registeredChannels) this.channelHandlers.set(id2, handler);
|