@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.
@@ -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
- return { version: 1, id: String(root.id), toolVersion: String(root.toolVersion), platforms, operations };
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 = 1;
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.2.0";
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 ${String(SAYCODE_CHANNEL_API_VERSION)}`);
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
- requestId,
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 !== 1 || !contribution || contribution.provider !== event.provider) {
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 key = JSON.stringify([input.channelId, input.connectionId]);
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 !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
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 !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof formatter !== "function" || registeredFormatters.has(channelId)) {
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 !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredIntake.has(channelId)) {
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);