@buzzni/saycode-cli 0.9.8 → 0.9.10

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;
@@ -48,6 +48,15 @@ function contributionList(value, path2, allowedKeys, parse) {
48
48
  return parse(entry, entryPath);
49
49
  });
50
50
  }
51
+ const ENTRYPOINT_PATTERN = /\.(?:cjs|mjs|js)$/;
52
+ function relativePath(value, path2, requireScript = false) {
53
+ const parsed = string(value, path2);
54
+ const segments = parsed.split("/");
55
+ if (parsed.startsWith("/") || parsed.includes("\\") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || requireScript && !ENTRYPOINT_PATTERN.test(parsed)) {
56
+ fail$5(path2, "expected safe relative path");
57
+ }
58
+ return parsed;
59
+ }
51
60
  function keys$1(value, allowed) {
52
61
  const result = record$1(value, "widget");
53
62
  assertOnlyKeys(result, new Set(allowed), "widget");
@@ -138,6 +147,23 @@ function assertExtensionFileViewers(extensionId2, viewers, panels, permissions)
138
147
  if (!permissions.includes("filePreviews.read") || !viewer.id.startsWith(`${extensionId2}.`) || !viewer.panelId.startsWith(`${extensionId2}.`) || !panels.some((panel) => panel.id === viewer.panelId)) fail$5("manifest.contributes.fileViewers", "requires filePreviews.read and an owned panel");
139
148
  }
140
149
  }
150
+ const ELEMENT_PICK_INSPECT_PERMISSION = "browserPicks.inspect";
151
+ function parseExtensionElementPickInspectors(value, apiVersion) {
152
+ if (apiVersion < 4) fail$5("manifest.contributes.elementPickInspectors", "requires Extension API version 4");
153
+ return contributionList(value, "manifest.contributes.elementPickInspectors", ["id", "title", "script"], (entry, path2) => ({
154
+ id: id(entry.id, `${path2}.id`),
155
+ title: string(entry.title, `${path2}.title`),
156
+ script: relativePath(entry.script, `${path2}.script`, true)
157
+ }));
158
+ }
159
+ function assertExtensionElementPickInspectors(extensionId2, inspectors, permissions) {
160
+ if (inspectors.length > 0 !== permissions.includes(ELEMENT_PICK_INSPECT_PERMISSION)) {
161
+ fail$5("manifest.contributes.elementPickInspectors", `requires ${ELEMENT_PICK_INSPECT_PERMISSION} permission and an inspector that uses it`);
162
+ }
163
+ for (const inspector of inspectors) {
164
+ if (!inspector.id.startsWith(`${extensionId2}.`)) fail$5("manifest.contributes.elementPickInspectors", "expected an id declared by the same extension");
165
+ }
166
+ }
141
167
  function parseMachineCommands(value, extensionId2, apiVersion) {
142
168
  if (apiVersion < 3) fail$5("manifest.machineCommands", "requires Extension API version 3");
143
169
  const root = record$1(value, "manifest.machineCommands");
@@ -443,8 +469,11 @@ function parseExtensionArtifactActions(value, path2 = "manifest.contributes.arti
443
469
  };
444
470
  });
445
471
  }
446
- const CHANNEL_API_VERSION = 1;
472
+ const CHANNEL_API_VERSION = 2;
447
473
  const CHANNEL_MINIMUM_API_VERSION = 1;
474
+ function isSupportedChannelApiVersion(value) {
475
+ return typeof value === "number" && Number.isInteger(value) && value >= CHANNEL_MINIMUM_API_VERSION && value <= CHANNEL_API_VERSION;
476
+ }
448
477
  const CHANNEL_PROVIDERS = ["telegram", "slack", "discord"];
449
478
  const CHANNEL_COMMAND_SCHEMA = Object.freeze({
450
479
  pair: Object.freeze(["code!"]),
@@ -585,6 +614,7 @@ const EXTENSION_PERMISSIONS = [
585
614
  "localTools.install",
586
615
  "localTools.control",
587
616
  "localBrowser.setup",
617
+ "browser.runtimeSetup",
588
618
  "network.fetch",
589
619
  "notifications.show",
590
620
  "storage.read",
@@ -594,6 +624,7 @@ const EXTENSION_PERMISSIONS = [
594
624
  "browserViewer.installChrome",
595
625
  "artifacts.publishPublic",
596
626
  "filePreviews.read",
627
+ "browserPicks.inspect",
597
628
  // One permission per channel wire method (`src/domain/channelProtocol.ts`), deliberately not a
598
629
  // single adapter grant. Receiving and acking messages is transport work; reading a session,
599
630
  // driving it, or approving a tool call once are three larger and separately-refusable
@@ -606,7 +637,11 @@ const EXTENSION_PERMISSIONS = [
606
637
  "channels.transport",
607
638
  "sessions.read",
608
639
  "sessions.control",
609
- "sessions.approveOnce"
640
+ "sessions.approveOnce",
641
+ // Core → adapter, not a capability the adapter invokes: Core sends the current input and the
642
+ // thread's earlier messages (other people's words included) for display-text normalization
643
+ // (specs/slack-bot-improvements contracts §2). Channel API 2 only.
644
+ "channels.normalize"
610
645
  ];
611
646
  const CONVERSATION_MODS_INTERNAL = typeof __SAYCODE_MODS_INTERNAL__ !== "undefined" && __SAYCODE_MODS_INTERNAL__ === true;
612
647
  const SAYCODE_EXTENSION_API_VERSION = CONVERSATION_MODS_INTERNAL ? 5 : 4;
@@ -617,7 +652,7 @@ const SAYCODE_CHANNEL_API_VERSION = CHANNEL_API_VERSION;
617
652
  const SAYCODE_CHANNEL_MINIMUM_API_VERSION = CHANNEL_MINIMUM_API_VERSION;
618
653
  class UnsupportedChannelApiVersionError extends Error {
619
654
  constructor(channelApiVersion) {
620
- super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${String(SAYCODE_CHANNEL_API_VERSION)}`);
655
+ super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${`${SAYCODE_CHANNEL_MINIMUM_API_VERSION}-${SAYCODE_CHANNEL_API_VERSION}`}`);
621
656
  this.channelApiVersion = channelApiVersion;
622
657
  }
623
658
  name = "UnsupportedChannelApiVersionError";
@@ -633,15 +668,14 @@ class UnsupportedExtensionApiVersionError extends Error {
633
668
  }
634
669
  const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
635
670
  const ENGINE_RANGE_PATTERN = /^(?:\^|~|>=|<=|>|<)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
636
- const ENTRYPOINT_PATTERN = /\.(?:cjs|mjs|js)$/;
637
671
  const LOCALE_PATTERN = /^[a-z]{2,3}(?:-[A-Z]{2})?$/;
638
- const CONTRIBUTION_KEYS = /* @__PURE__ */ new Set(["chatWidgets", "commands", "settings", "panels", "projectTemplates", "machineActions", "projectTools", "artifactActions", "channels", "fileViewers"]);
672
+ const CONTRIBUTION_KEYS = /* @__PURE__ */ new Set(["chatWidgets", "commands", "settings", "panels", "projectTemplates", "machineActions", "projectTools", "artifactActions", "channels", "fileViewers", "elementPickInspectors"]);
639
673
  const V2_PERMISSIONS = /* @__PURE__ */ new Set([
640
674
  "browserViewer.open",
641
675
  "browserViewer.install",
642
676
  "browserViewer.installChrome"
643
677
  ]);
644
- 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"]);
678
+ 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"]);
645
679
  const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
646
680
  "channels.receive",
647
681
  "channels.ack",
@@ -649,7 +683,8 @@ const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
649
683
  "channels.transport",
650
684
  "sessions.read",
651
685
  "sessions.control",
652
- "sessions.approveOnce"
686
+ "sessions.approveOnce",
687
+ "channels.normalize"
653
688
  ]);
654
689
  const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
655
690
  "id",
@@ -707,14 +742,6 @@ function parseApiVersion(value, options) {
707
742
  }
708
743
  return value;
709
744
  }
710
- function relativePath(value, path2, requireScript = false) {
711
- const parsed = string(value, path2);
712
- const segments = parsed.split("/");
713
- if (parsed.startsWith("/") || parsed.includes("\\") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || requireScript && !ENTRYPOINT_PATTERN.test(parsed)) {
714
- fail$5(path2, "expected safe relative path");
715
- }
716
- return parsed;
717
- }
718
745
  function parseChannelApiVersion(value, apiVersion) {
719
746
  if (value === void 0) return void 0;
720
747
  if (apiVersion < 3) fail$5("manifest.channelApiVersion", "requires Extension API version 3");
@@ -738,6 +765,7 @@ function parseContributions(value, apiVersion, channelApiVersion) {
738
765
  }
739
766
  const result = {};
740
767
  if (parsed.fileViewers !== void 0) result.fileViewers = parseExtensionFileViewers(parsed.fileViewers, apiVersion);
768
+ if (parsed.elementPickInspectors !== void 0) result.elementPickInspectors = parseExtensionElementPickInspectors(parsed.elementPickInspectors, apiVersion);
741
769
  if (parsed.commands !== void 0) {
742
770
  result.commands = contributionList(
743
771
  parsed.commands,
@@ -860,7 +888,8 @@ function parseContributions(value, apiVersion, channelApiVersion) {
860
888
  ...result.machineActions ?? [],
861
889
  ...result.projectTools ?? [],
862
890
  ...result.artifactActions ?? [],
863
- ...result.channels ?? []
891
+ ...result.channels ?? [],
892
+ ...result.elementPickInspectors ?? []
864
893
  ].map((entry) => entry.id);
865
894
  if (new Set(ids).size !== ids.length) fail$5("manifest.contributes", "duplicate contribution id");
866
895
  return result;
@@ -893,6 +922,7 @@ function parseExtensionManifest(value, options) {
893
922
  const typed = parsed;
894
923
  if (typed.startsWith("sessionContext.") && apiVersion !== 5) fail$5("manifest.permissions", "sessionContext requires API 5");
895
924
  if (typed === "filePreviews.read" && apiVersion < 4) fail$5("manifest.permissions", "filePreviews.read requires Extension API version 4");
925
+ if (typed === "browserPicks.inspect" && apiVersion < 4) fail$5("manifest.permissions", "browserPicks.inspect requires Extension API version 4");
896
926
  if (apiVersion < 2 && V2_PERMISSIONS.has(typed)) {
897
927
  fail$5(`manifest.permissions[${index}]`, "requires Extension API version 2");
898
928
  }
@@ -904,6 +934,9 @@ function parseExtensionManifest(value, options) {
904
934
  if (channelApiVersion === void 0) {
905
935
  fail$5(`manifest.permissions[${index}]`, "requires manifest.channelApiVersion");
906
936
  }
937
+ if (typed === "channels.normalize" && channelApiVersion < 2) {
938
+ fail$5(`manifest.permissions[${index}]`, "channels.normalize requires channel API version 2");
939
+ }
907
940
  }
908
941
  return typed;
909
942
  });
@@ -971,6 +1004,7 @@ function parseExtensionManifest(value, options) {
971
1004
  }
972
1005
  }
973
1006
  assertExtensionFileViewers(parsedId, contributes.fileViewers ?? [], contributes.panels ?? [], permissions);
1007
+ assertExtensionElementPickInspectors(parsedId, contributes.elementPickInspectors ?? [], permissions);
974
1008
  assertExtensionProjectToolTargets(parsedId, contributes.projectTools ?? [], contributes.commands ?? []);
975
1009
  return {
976
1010
  id: parsedId,
@@ -1051,6 +1085,246 @@ function parseChannelApprovalRequest(value) {
1051
1085
  }
1052
1086
  return { approvalHandle, approveLabel, denyLabel };
1053
1087
  }
1088
+ const NORMALIZE_ADDITION_SOURCES = ["blocks", "quote", "forward", "table", "unfurl", "files"];
1089
+ const MAX_NORMALIZE_ITEMS = 200;
1090
+ const MAX_USER_LABELS = 200;
1091
+ const MAX_BLOCKS = 50;
1092
+ const MAX_ATTACHMENTS = 20;
1093
+ const MAX_FILES = 20;
1094
+ const MAX_RICH_TEXT_DEPTH = 6;
1095
+ const MAX_URL_LENGTH = 2048;
1096
+ const MAX_ADDITIONS_BYTES = 8192;
1097
+ const MAX_PRIMARY_BYTES = 65536;
1098
+ const ITEM_REF = /^i(0|[1-9]\d?|1\d\d)$/;
1099
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1100
+ const isString = (value) => typeof value === "string";
1101
+ function projectSlackMessage(raw) {
1102
+ const state = { truncated: false };
1103
+ const message = isRecord(raw) ? raw : {};
1104
+ const projection = {};
1105
+ const blocks = projectBlocks(message.blocks, state);
1106
+ if (blocks) projection.blocks = blocks;
1107
+ const attachments = capped(Array.isArray(message.attachments) ? message.attachments.filter(isRecord) : [], MAX_ATTACHMENTS, state).map((attachment) => projectAttachment(attachment, state));
1108
+ if (attachments.length > 0) projection.attachments = attachments;
1109
+ const files = capped(Array.isArray(message.files) ? message.files.filter(isRecord) : [], MAX_FILES, state).map(projectFile);
1110
+ if (files.length > 0) projection.files = files;
1111
+ if (isString(message.subtype)) projection.subtype = message.subtype;
1112
+ return { projection, truncated: state.truncated };
1113
+ }
1114
+ function capped(items, max, state) {
1115
+ if (items.length > max) state.truncated = true;
1116
+ return items.slice(0, max);
1117
+ }
1118
+ function projectBlocks(value, state) {
1119
+ if (!Array.isArray(value)) return void 0;
1120
+ const projected = value.map((block) => projectBlock(block, state)).filter((block) => block !== null);
1121
+ return projected.length > 0 ? capped(projected, MAX_BLOCKS, state) : void 0;
1122
+ }
1123
+ function projectBlock(block, state) {
1124
+ if (!isRecord(block)) return null;
1125
+ switch (block.type) {
1126
+ case "rich_text":
1127
+ return { type: "rich_text", elements: projectContainers(block.elements, 1, state) };
1128
+ case "section": {
1129
+ const section = { type: "section" };
1130
+ const text2 = projectTextObject(block.text);
1131
+ if (text2) section.text = text2;
1132
+ if (Array.isArray(block.fields)) section.fields = block.fields.map(projectTextObject).filter((field) => field !== null);
1133
+ return section;
1134
+ }
1135
+ case "header": {
1136
+ const text2 = projectTextObject(block.text);
1137
+ return text2 ? { type: "header", text: text2 } : null;
1138
+ }
1139
+ case "context":
1140
+ return { type: "context", elements: Array.isArray(block.elements) ? block.elements.map(projectTextObject).filter((element) => element !== null) : [] };
1141
+ case "table":
1142
+ return { type: "table", rows: Array.isArray(block.rows) ? block.rows.filter(Array.isArray).map((row2) => row2.flatMap((cell) => {
1143
+ if (isRecord(cell) && cell.type === "raw_text" && isString(cell.text)) return [{ type: "raw_text", text: cell.text }];
1144
+ if (isRecord(cell) && cell.type === "rich_text") return [{ type: "rich_text", elements: projectContainers(cell.elements, 1, state) }];
1145
+ return [];
1146
+ })) : [] };
1147
+ default:
1148
+ return null;
1149
+ }
1150
+ }
1151
+ function projectTextObject(value) {
1152
+ if (!isRecord(value) || value.type !== "mrkdwn" && value.type !== "plain_text" || !isString(value.text)) return null;
1153
+ return { type: value.type, text: value.text };
1154
+ }
1155
+ const CONTAINER_TYPES = /* @__PURE__ */ new Set(["rich_text_section", "rich_text_quote", "rich_text_preformatted", "rich_text_list"]);
1156
+ function projectContainers(value, depth, state) {
1157
+ if (!Array.isArray(value)) return [];
1158
+ return value.flatMap((node) => {
1159
+ const container = isRecord(node) && CONTAINER_TYPES.has(String(node.type)) ? projectContainer(node, depth, state) : null;
1160
+ return container ? [container] : [];
1161
+ });
1162
+ }
1163
+ function projectContainer(node, depth, state) {
1164
+ if (depth > MAX_RICH_TEXT_DEPTH) {
1165
+ state.truncated = true;
1166
+ return null;
1167
+ }
1168
+ const elements = (Array.isArray(node.elements) ? node.elements : []).flatMap((child) => {
1169
+ if (isRecord(child) && CONTAINER_TYPES.has(String(child.type))) {
1170
+ const nested = projectContainer(child, depth + 1, state);
1171
+ return nested ? [nested] : [];
1172
+ }
1173
+ const leaf = projectLeaf(child);
1174
+ return leaf ? [leaf] : [];
1175
+ });
1176
+ const container = { type: node.type, elements };
1177
+ if (node.style === "bullet" || node.style === "ordered") container.style = node.style;
1178
+ return container;
1179
+ }
1180
+ function projectLeaf(node) {
1181
+ if (!isRecord(node)) return null;
1182
+ switch (node.type) {
1183
+ case "text": {
1184
+ if (!isString(node.text)) return null;
1185
+ const leaf = { type: "text", text: node.text };
1186
+ if (isRecord(node.style)) {
1187
+ const style = Object.fromEntries(["bold", "italic", "strike", "code"].filter((key) => typeof node.style[key] === "boolean").map((key) => [key, node.style[key]]));
1188
+ if (Object.keys(style).length > 0) leaf.style = style;
1189
+ }
1190
+ return leaf;
1191
+ }
1192
+ case "link": {
1193
+ const label = isString(node.text) ? node.text : void 0;
1194
+ if (!httpUrl(node.url)) return label === void 0 ? null : { type: "text", text: label };
1195
+ return label === void 0 ? { type: "link", url: node.url } : { type: "link", url: node.url, text: label };
1196
+ }
1197
+ case "user":
1198
+ return isString(node.user_id) ? { type: "user", user_id: node.user_id } : null;
1199
+ case "channel":
1200
+ return isString(node.channel_id) ? { type: "channel", channel_id: node.channel_id } : null;
1201
+ case "usergroup":
1202
+ return isString(node.usergroup_id) ? { type: "usergroup", usergroup_id: node.usergroup_id } : null;
1203
+ case "emoji":
1204
+ return isString(node.name) ? { type: "emoji", name: node.name } : null;
1205
+ case "broadcast":
1206
+ return node.range === "here" || node.range === "channel" || node.range === "everyone" ? { type: "broadcast", range: node.range } : null;
1207
+ default:
1208
+ return null;
1209
+ }
1210
+ }
1211
+ const ATTACHMENT_TEXT_KEYS = ["text", "fallback", "pretext", "title", "author_name", "footer"];
1212
+ const ATTACHMENT_URL_KEYS = ["title_link", "from_url", "original_url"];
1213
+ function projectAttachment(attachment, state) {
1214
+ const projected = {};
1215
+ for (const key of ATTACHMENT_TEXT_KEYS) if (isString(attachment[key])) projected[key] = attachment[key];
1216
+ for (const key of ATTACHMENT_URL_KEYS) if (httpUrl(attachment[key])) projected[key] = attachment[key];
1217
+ if (typeof attachment.is_share === "boolean") projected.is_share = attachment.is_share;
1218
+ if (typeof attachment.is_msg_unfurl === "boolean") projected.is_msg_unfurl = attachment.is_msg_unfurl;
1219
+ if (Array.isArray(attachment.message_blocks)) {
1220
+ projected.message_blocks = attachment.message_blocks.filter(isRecord).map((forwarded) => ({
1221
+ message: { blocks: projectBlocks(isRecord(forwarded.message) ? forwarded.message.blocks : void 0, state) ?? [] }
1222
+ }));
1223
+ }
1224
+ return projected;
1225
+ }
1226
+ function projectFile(file) {
1227
+ const projected = {};
1228
+ for (const key of ["name", "title", "filetype", "mimetype"]) if (isString(file[key])) projected[key] = file[key];
1229
+ if (Number.isSafeInteger(file.size) && file.size >= 0) projected.size = file.size;
1230
+ return projected;
1231
+ }
1232
+ function httpUrl(value) {
1233
+ return isString(value) && value.length <= MAX_URL_LENGTH && /^https?:\/\//i.test(value);
1234
+ }
1235
+ function parseChannelNormalizeRequest(value) {
1236
+ const request = plainRecord(value, "normalize request");
1237
+ exactKeySet(request, ["apiVersion", "channelId", "connectionId", "provider", "connectionRevision", "requestId", "purpose", "userLabels", "items"], "normalize request");
1238
+ if (request.apiVersion !== 2 || request.provider !== "slack") fail("normalize request is channel API 2 Slack only");
1239
+ if (request.purpose !== "currentInput" && request.purpose !== "threadHistory") fail("invalid normalize purpose");
1240
+ if (!Number.isSafeInteger(request.connectionRevision) || request.connectionRevision < 1) fail("invalid normalize connection revision");
1241
+ const labels = plainRecord(request.userLabels, "normalize userLabels");
1242
+ 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)) {
1243
+ fail("invalid normalize userLabels");
1244
+ }
1245
+ if (!Array.isArray(request.items) || request.items.length < 1 || request.items.length > MAX_NORMALIZE_ITEMS) fail("invalid normalize items");
1246
+ const refs = /* @__PURE__ */ new Set();
1247
+ const items = request.items.map((value2) => {
1248
+ const item = plainRecord(value2, "normalize item");
1249
+ exactKeySet(item, ["itemRef", "primaryText", "projection", "maxAdditionsBytes"], "normalize item");
1250
+ if (!isString(item.itemRef) || !ITEM_REF.test(item.itemRef) || refs.has(item.itemRef)) fail("invalid normalize itemRef");
1251
+ refs.add(item.itemRef);
1252
+ if (!isString(item.primaryText) || utf8Bytes(item.primaryText) > MAX_PRIMARY_BYTES) fail("invalid normalize primaryText");
1253
+ if (!Number.isSafeInteger(item.maxAdditionsBytes) || item.maxAdditionsBytes < 1 || item.maxAdditionsBytes > MAX_ADDITIONS_BYTES) fail("invalid normalize maxAdditionsBytes");
1254
+ const projected = projectSlackMessage(item.projection);
1255
+ if (!isRecord(item.projection) || projected.truncated || !sameJson(projected.projection, item.projection)) fail("normalize projection is not a Core projection");
1256
+ return { itemRef: item.itemRef, primaryText: item.primaryText, projection: projected.projection, maxAdditionsBytes: item.maxAdditionsBytes };
1257
+ });
1258
+ return {
1259
+ apiVersion: 2,
1260
+ provider: "slack",
1261
+ purpose: request.purpose,
1262
+ channelId: boundedText(request.channelId, "channelId"),
1263
+ connectionId: boundedText(request.connectionId, "connectionId"),
1264
+ connectionRevision: request.connectionRevision,
1265
+ requestId: boundedText(request.requestId, "requestId"),
1266
+ userLabels: { ...labels },
1267
+ items
1268
+ };
1269
+ }
1270
+ function parseChannelNormalizeResult(request, value) {
1271
+ const result = plainRecord(value, "normalize result");
1272
+ exactKeySet(result, ["items"], "normalize result");
1273
+ if (!Array.isArray(result.items)) fail("invalid normalize result items");
1274
+ const wanted = new Map(request.items.map((item) => [item.itemRef, item]));
1275
+ const parsed = /* @__PURE__ */ new Map();
1276
+ for (const value2 of result.items) {
1277
+ const item = plainRecord(value2, "normalize result item");
1278
+ exactKeySet(item, ["itemRef", "additions", "truncated"], "normalize result item");
1279
+ const requested = isString(item.itemRef) ? wanted.get(item.itemRef) : void 0;
1280
+ if (!requested || parsed.has(requested.itemRef)) fail("normalize result items differ from the request");
1281
+ if (typeof item.truncated !== "boolean" || !Array.isArray(item.additions)) fail("invalid normalize result item");
1282
+ let bytes = 0;
1283
+ const additions = item.additions.map((value3) => {
1284
+ const addition = plainRecord(value3, "normalize addition");
1285
+ exactKeySet(addition, ["source", "text"], "normalize addition");
1286
+ if (!NORMALIZE_ADDITION_SOURCES.includes(addition.source) || !isString(addition.text) || addition.text.includes("\0") || hasLoneSurrogate(addition.text)) fail("invalid normalize addition");
1287
+ bytes += utf8Bytes(addition.text);
1288
+ return { source: addition.source, text: addition.text };
1289
+ });
1290
+ if (bytes > requested.maxAdditionsBytes) fail("normalize additions exceed their budget");
1291
+ parsed.set(requested.itemRef, { itemRef: requested.itemRef, additions: withoutRepeatedLines(requested.primaryText, additions), truncated: item.truncated });
1292
+ }
1293
+ if (parsed.size !== wanted.size) fail("normalize result items differ from the request");
1294
+ return request.items.map((item) => parsed.get(item.itemRef));
1295
+ }
1296
+ function withoutRepeatedLines(primaryText, additions) {
1297
+ const fold = (line) => line.replace(/\s+/gu, " ").trim();
1298
+ const known = new Set(primaryText.split("\n").map(fold).filter(Boolean));
1299
+ const kept = [];
1300
+ for (const addition of additions) {
1301
+ const lines2 = addition.text.split("\n");
1302
+ const remaining = lines2.filter((line) => fold(line) === "" || !known.has(fold(line)));
1303
+ for (const line of lines2) if (fold(line)) known.add(fold(line));
1304
+ if (remaining.some((line) => fold(line) !== "")) kept.push({ source: addition.source, text: remaining.join("\n") });
1305
+ }
1306
+ return kept;
1307
+ }
1308
+ function exactKeySet(record2, keys2, path2) {
1309
+ const actual = Object.keys(record2);
1310
+ if (actual.length !== keys2.length || !keys2.every((key) => actual.includes(key))) fail(`${path2} has unexpected keys`);
1311
+ }
1312
+ function boundedText(value, path2) {
1313
+ if (!isString(value) || value.length === 0 || value.length > 200) fail(`invalid normalize ${path2}`);
1314
+ return value;
1315
+ }
1316
+ function sameJson(a, b) {
1317
+ if (Array.isArray(a) || Array.isArray(b)) {
1318
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameJson(item, b[index]));
1319
+ }
1320
+ if (isRecord(a) && isRecord(b)) {
1321
+ const keys2 = Object.keys(a);
1322
+ return keys2.length === Object.keys(b).length && keys2.every((key) => Object.hasOwn(b, key) && sameJson(a[key], b[key]));
1323
+ }
1324
+ return Object.is(a, b);
1325
+ }
1326
+ const utf8Bytes = (text2) => new TextEncoder().encode(text2).length;
1327
+ const hasLoneSurrogate = (text2) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(text2);
1054
1328
  function finiteNumber(value, path2) {
1055
1329
  if (typeof value !== "number" || !Number.isFinite(value)) fail(`${path2} must be a finite number`);
1056
1330
  return value;
@@ -1120,6 +1394,13 @@ function parseRequest(message) {
1120
1394
  connectionRevision: Number(params.connectionRevision)
1121
1395
  } };
1122
1396
  }
1397
+ case "channel.intake": {
1398
+ exactKeys(params, ["apiVersion", "channelId", "connectionId", "provider", "connectionRevision"], "request.params");
1399
+ const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: { ...params, state: "start" } });
1400
+ if (lifecycle.method !== "channel.lifecycle") fail("invalid channel intake");
1401
+ const { state: _state, ...identity } = lifecycle.params;
1402
+ return { kind: "request", id: id2, method, params: identity };
1403
+ }
1123
1404
  case "channel.formatReply": {
1124
1405
  const formatKeys = [
1125
1406
  "apiVersion",
@@ -1133,13 +1414,15 @@ function parseRequest(message) {
1133
1414
  "maxChunkUtf16Units",
1134
1415
  "maxChunks"
1135
1416
  ];
1417
+ const v2 = params.apiVersion === 2;
1136
1418
  exactKeys(
1137
1419
  params,
1138
- "approval" in params ? [...formatKeys, "approval"] : formatKeys,
1420
+ v2 ? [...formatKeys, "mode", "dialect"] : "approval" in params ? [...formatKeys, "approval"] : formatKeys,
1139
1421
  "request.params"
1140
1422
  );
1423
+ if (v2 && (params.mode !== "verbatim" && params.mode !== "rendered" || params.dialect !== "slack-mrkdwn" || params.kind === "approval")) fail("invalid channel API 2 formatter request");
1141
1424
  const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: {
1142
- apiVersion: params.apiVersion,
1425
+ apiVersion: v2 ? 1 : params.apiVersion,
1143
1426
  channelId: params.channelId,
1144
1427
  connectionId: params.connectionId,
1145
1428
  provider: params.provider,
@@ -1155,16 +1438,26 @@ function parseRequest(message) {
1155
1438
  }
1156
1439
  const approval = params.approval === void 0 ? void 0 : parseChannelApprovalRequest(params.approval);
1157
1440
  const { state: _state, ...identity } = lifecycle.params;
1441
+ const bounds = { requestId, text: body, maxChunks: Number(params.maxChunks), maxChunkUtf16Units: Number(params.maxChunkUtf16Units) };
1442
+ if (v2) {
1443
+ return { kind: "request", id: id2, method, params: {
1444
+ ...identity,
1445
+ ...bounds,
1446
+ apiVersion: 2,
1447
+ kind: params.kind,
1448
+ mode: params.mode,
1449
+ dialect: "slack-mrkdwn"
1450
+ } };
1451
+ }
1158
1452
  return { kind: "request", id: id2, method, params: {
1159
1453
  ...identity,
1160
- requestId,
1161
- text: body,
1454
+ ...bounds,
1162
1455
  kind: params.kind,
1163
- maxChunks: Number(params.maxChunks),
1164
- maxChunkUtf16Units: Number(params.maxChunkUtf16Units),
1165
1456
  ...approval ? { approval } : {}
1166
1457
  } };
1167
1458
  }
1459
+ case "channel.normalizeMessages":
1460
+ return { kind: "request", id: id2, method, params: parseChannelNormalizeRequest(params) };
1168
1461
  case "invokeCommand": {
1169
1462
  exactKeys(params, ["commandId", "args"], "request.params");
1170
1463
  if (!Array.isArray(params.args)) fail("request.params.args must be an array");
@@ -1247,6 +1540,17 @@ function parseExtensionWireMessage(value) {
1247
1540
  if (message.kind === "event") return parseEvent(message);
1248
1541
  fail("message.kind is unknown");
1249
1542
  }
1543
+ function parseChannelRenderedReplyResult(value) {
1544
+ const result = plainRecord(value, "channel rendered result");
1545
+ if (Object.keys(result).join() !== "rendered" || !Array.isArray(result.rendered)) fail("invalid channel rendered result");
1546
+ return {
1547
+ rendered: result.rendered.map((value2) => {
1548
+ const chunk = plainRecord(value2, "channel rendered chunk");
1549
+ 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");
1550
+ return { text: chunk.text, sourceStart: chunk.sourceStart, sourceEnd: chunk.sourceEnd };
1551
+ })
1552
+ };
1553
+ }
1250
1554
  const TIMEOUT = Symbol("extension-request-timeout");
1251
1555
  function requestEnvelope(value) {
1252
1556
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
@@ -1268,6 +1572,8 @@ class ExtensionHostRuntime {
1268
1572
  widgets = /* @__PURE__ */ new Map();
1269
1573
  channelHandlers = /* @__PURE__ */ new Map();
1270
1574
  channelFormatters = /* @__PURE__ */ new Map();
1575
+ channelNormalizers = /* @__PURE__ */ new Map();
1576
+ channelIntakeHandlers = /* @__PURE__ */ new Map();
1271
1577
  channelStates = /* @__PURE__ */ new Map();
1272
1578
  channelCalls = /* @__PURE__ */ new Map();
1273
1579
  channelEpoch = 0;
@@ -1402,6 +1708,8 @@ class ExtensionHostRuntime {
1402
1708
  this.commandHandlers.clear();
1403
1709
  this.channelHandlers.clear();
1404
1710
  this.channelFormatters.clear();
1711
+ this.channelNormalizers.clear();
1712
+ this.channelIntakeHandlers.clear();
1405
1713
  this.channelStates.clear();
1406
1714
  this.channelCalls.clear();
1407
1715
  }
@@ -1444,6 +1752,8 @@ class ExtensionHostRuntime {
1444
1752
  this.commandHandlers.clear();
1445
1753
  this.channelHandlers.clear();
1446
1754
  this.channelFormatters.clear();
1755
+ this.channelNormalizers.clear();
1756
+ this.channelIntakeHandlers.clear();
1447
1757
  this.channelStates.clear();
1448
1758
  this.channelCalls.clear();
1449
1759
  this.deactivating = false;
@@ -1463,6 +1773,8 @@ class ExtensionHostRuntime {
1463
1773
  }
1464
1774
  if (request.method === "channel.lifecycle") return this.deliverChannel(request.params);
1465
1775
  if (request.method === "channel.formatReply") return this.formatChannelReply(request.params);
1776
+ if (request.method === "channel.normalizeMessages") return this.normalizeChannelMessages(request.params);
1777
+ if (request.method === "channel.intake") return this.deliverChannelIntake(request.params);
1466
1778
  const handler = this.commandHandlers.get(request.params.commandId);
1467
1779
  if (!handler) {
1468
1780
  throw { code: "COMMAND_NOT_REGISTERED", message: `extension command is not registered: ${request.params.commandId}` };
@@ -1474,7 +1786,7 @@ class ExtensionHostRuntime {
1474
1786
  }
1475
1787
  async deliverChannel(event) {
1476
1788
  const contribution = this.options.manifest.contributes.channels?.find((channel) => channel.id === event.channelId);
1477
- if (this.options.manifest.channelApiVersion !== 1 || !contribution || contribution.provider !== event.provider) {
1789
+ if (!isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !contribution || contribution.provider !== event.provider) {
1478
1790
  throw { code: "CHANNEL_NOT_DECLARED", message: "channel contribution/provider is not declared" };
1479
1791
  }
1480
1792
  const key = JSON.stringify([event.channelId, event.connectionId]);
@@ -1511,17 +1823,38 @@ class ExtensionHostRuntime {
1511
1823
  if (this.channelCalls.get(key) === next) this.channelCalls.delete(key);
1512
1824
  }
1513
1825
  }
1826
+ /** A hint for a live connection only; anything else is answered and dropped, never an error. */
1827
+ async deliverChannelIntake(input) {
1828
+ const current = this.channelStates.get(JSON.stringify([input.channelId, input.connectionId]));
1829
+ const handler = this.channelIntakeHandlers.get(input.channelId);
1830
+ if (!handler || this.deactivating || current?.state !== "start" || current.provider !== input.provider || current.connectionRevision !== input.connectionRevision) return null;
1831
+ await handler(Object.freeze({ ...input }));
1832
+ return null;
1833
+ }
1514
1834
  async formatChannelReply(input) {
1515
- const key = JSON.stringify([input.channelId, input.connectionId]);
1516
- const current = this.channelStates.get(key);
1517
- const epoch = this.channelEpoch;
1835
+ const isCurrent = this.channelCallIsCurrent(input);
1518
1836
  const formatter = this.channelFormatters.get(input.channelId);
1519
- 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;
1520
1837
  if (!isCurrent() || !formatter) throw { code: "CHANNEL_FORMAT_UNAVAILABLE", message: "channel formatter is not active" };
1521
1838
  const result = await formatter(Object.freeze({ ...input }));
1522
1839
  if (!isCurrent()) throw { code: "CHANNEL_FORMAT_WITHDRAWN", message: "channel formatter stopped" };
1840
+ if (input.apiVersion === 2 && input.mode === "rendered") return parseChannelRenderedReplyResult(result);
1523
1841
  return parseChannelReplyFormatResult(result, input);
1524
1842
  }
1843
+ async normalizeChannelMessages(input) {
1844
+ const isCurrent = this.channelCallIsCurrent(input);
1845
+ const normalizer = this.channelNormalizers.get(input.channelId);
1846
+ if (!isCurrent() || !normalizer) throw { code: "CHANNEL_NORMALIZE_UNAVAILABLE", message: "channel normalizer is not active" };
1847
+ const result = await normalizer(Object.freeze({ ...input }));
1848
+ if (!isCurrent()) throw { code: "CHANNEL_NORMALIZE_WITHDRAWN", message: "channel normalizer stopped" };
1849
+ return { items: parseChannelNormalizeResult(input, result) };
1850
+ }
1851
+ /** A formatter or normalizer answers only for the started connection generation it was asked about. */
1852
+ channelCallIsCurrent(input) {
1853
+ const key = JSON.stringify([input.channelId, input.connectionId]);
1854
+ const current = this.channelStates.get(key);
1855
+ const epoch = this.channelEpoch;
1856
+ 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;
1857
+ }
1525
1858
  async withTimeout(operation) {
1526
1859
  let timer;
1527
1860
  const timeout = new Promise((resolve2) => {
@@ -1555,6 +1888,8 @@ class ExtensionHostRuntime {
1555
1888
  const registered = /* @__PURE__ */ new Map();
1556
1889
  const registeredChannels = /* @__PURE__ */ new Map();
1557
1890
  const registeredFormatters = /* @__PURE__ */ new Map();
1891
+ const registeredNormalizers = /* @__PURE__ */ new Map();
1892
+ const registeredIntake = /* @__PURE__ */ new Map();
1558
1893
  await loaded.activate({
1559
1894
  extensionId: this.options.manifest.id,
1560
1895
  chatWidgets: { register: (id2, render) => {
@@ -1563,16 +1898,29 @@ class ExtensionHostRuntime {
1563
1898
  } },
1564
1899
  channels: {
1565
1900
  register: (channelId, handler) => {
1566
- if (!this.activating || this.options.manifest.channelApiVersion !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
1901
+ if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
1567
1902
  throw { code: "INVALID_CHANNEL_REGISTRATION", message: "channel must be declared and registered once" };
1568
1903
  }
1569
1904
  registeredChannels.set(channelId, handler);
1570
1905
  },
1571
1906
  registerFormatter: (channelId, formatter) => {
1572
- if (!this.activating || this.options.manifest.channelApiVersion !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof formatter !== "function" || registeredFormatters.has(channelId)) {
1907
+ if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof formatter !== "function" || registeredFormatters.has(channelId)) {
1573
1908
  throw { code: "INVALID_CHANNEL_REGISTRATION", message: "formatter must be declared and registered once" };
1574
1909
  }
1575
1910
  registeredFormatters.set(channelId, formatter);
1911
+ },
1912
+ registerNormalizer: (channelId, normalizer) => {
1913
+ const manifest = this.options.manifest;
1914
+ 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)) {
1915
+ throw { code: "INVALID_CHANNEL_REGISTRATION", message: "normalizer needs channel API 2, channels.normalize and one registration" };
1916
+ }
1917
+ registeredNormalizers.set(channelId, normalizer);
1918
+ },
1919
+ onIntake: (channelId, handler) => {
1920
+ if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredIntake.has(channelId)) {
1921
+ throw { code: "INVALID_CHANNEL_REGISTRATION", message: "intake handler must be declared and registered once" };
1922
+ }
1923
+ registeredIntake.set(channelId, handler);
1576
1924
  }
1577
1925
  },
1578
1926
  commands: {
@@ -1591,7 +1939,11 @@ class ExtensionHostRuntime {
1591
1939
  for (const [id2, render] of registeredWidgets) this.widgets.set(id2, render);
1592
1940
  this.channelHandlers.clear();
1593
1941
  this.channelFormatters.clear();
1942
+ this.channelNormalizers.clear();
1594
1943
  for (const [id2, formatter] of registeredFormatters) this.channelFormatters.set(id2, formatter);
1944
+ for (const [id2, normalizer] of registeredNormalizers) this.channelNormalizers.set(id2, normalizer);
1945
+ this.channelIntakeHandlers.clear();
1946
+ for (const [id2, handler] of registeredIntake) this.channelIntakeHandlers.set(id2, handler);
1595
1947
  for (const [id2, handler] of registeredChannels) this.channelHandlers.set(id2, handler);
1596
1948
  this.commandHandlers.clear();
1597
1949
  for (const [commandId, handler] of registered) this.commandHandlers.set(commandId, handler);