@spinabot/brigade 1.5.0 → 1.6.0

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.
Files changed (109) hide show
  1. package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
  2. package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
  3. package/dist/agents/channels/bundled-channel-metas.js +11 -0
  4. package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
  5. package/dist/agents/channels/manager.d.ts.map +1 -1
  6. package/dist/agents/channels/manager.js +18 -0
  7. package/dist/agents/channels/manager.js.map +1 -1
  8. package/dist/agents/channels/sdk.d.ts +2 -0
  9. package/dist/agents/channels/sdk.d.ts.map +1 -1
  10. package/dist/agents/channels/sdk.js +2 -0
  11. package/dist/agents/channels/sdk.js.map +1 -1
  12. package/dist/agents/channels/slack/account-config.d.ts +172 -0
  13. package/dist/agents/channels/slack/account-config.d.ts.map +1 -0
  14. package/dist/agents/channels/slack/account-config.js +353 -0
  15. package/dist/agents/channels/slack/account-config.js.map +1 -0
  16. package/dist/agents/channels/slack/account-registry.d.ts +45 -0
  17. package/dist/agents/channels/slack/account-registry.d.ts.map +1 -0
  18. package/dist/agents/channels/slack/account-registry.js +58 -0
  19. package/dist/agents/channels/slack/account-registry.js.map +1 -0
  20. package/dist/agents/channels/slack/adapter.d.ts +66 -0
  21. package/dist/agents/channels/slack/adapter.d.ts.map +1 -0
  22. package/dist/agents/channels/slack/adapter.js +547 -0
  23. package/dist/agents/channels/slack/adapter.js.map +1 -0
  24. package/dist/agents/channels/slack/approval-authorize.d.ts +43 -0
  25. package/dist/agents/channels/slack/approval-authorize.d.ts.map +1 -0
  26. package/dist/agents/channels/slack/approval-authorize.js +71 -0
  27. package/dist/agents/channels/slack/approval-authorize.js.map +1 -0
  28. package/dist/agents/channels/slack/approval-native.d.ts +70 -0
  29. package/dist/agents/channels/slack/approval-native.d.ts.map +1 -0
  30. package/dist/agents/channels/slack/approval-native.js +85 -0
  31. package/dist/agents/channels/slack/approval-native.js.map +1 -0
  32. package/dist/agents/channels/slack/blocks.d.ts +125 -0
  33. package/dist/agents/channels/slack/blocks.d.ts.map +1 -0
  34. package/dist/agents/channels/slack/blocks.js +145 -0
  35. package/dist/agents/channels/slack/blocks.js.map +1 -0
  36. package/dist/agents/channels/slack/command-menu.d.ts +44 -0
  37. package/dist/agents/channels/slack/command-menu.d.ts.map +1 -0
  38. package/dist/agents/channels/slack/command-menu.js +66 -0
  39. package/dist/agents/channels/slack/command-menu.js.map +1 -0
  40. package/dist/agents/channels/slack/connection.d.ts +422 -0
  41. package/dist/agents/channels/slack/connection.d.ts.map +1 -0
  42. package/dist/agents/channels/slack/connection.js +1042 -0
  43. package/dist/agents/channels/slack/connection.js.map +1 -0
  44. package/dist/agents/channels/slack/directory-live.d.ts +129 -0
  45. package/dist/agents/channels/slack/directory-live.d.ts.map +1 -0
  46. package/dist/agents/channels/slack/directory-live.js +148 -0
  47. package/dist/agents/channels/slack/directory-live.js.map +1 -0
  48. package/dist/agents/channels/slack/draft-stream.d.ts +93 -0
  49. package/dist/agents/channels/slack/draft-stream.d.ts.map +1 -0
  50. package/dist/agents/channels/slack/draft-stream.js +218 -0
  51. package/dist/agents/channels/slack/draft-stream.js.map +1 -0
  52. package/dist/agents/channels/slack/format.d.ts +41 -0
  53. package/dist/agents/channels/slack/format.d.ts.map +1 -0
  54. package/dist/agents/channels/slack/format.js +271 -0
  55. package/dist/agents/channels/slack/format.js.map +1 -0
  56. package/dist/agents/channels/slack/inbound-extras.d.ts +179 -0
  57. package/dist/agents/channels/slack/inbound-extras.d.ts.map +1 -0
  58. package/dist/agents/channels/slack/inbound-extras.js +257 -0
  59. package/dist/agents/channels/slack/inbound-extras.js.map +1 -0
  60. package/dist/agents/channels/slack/index.d.ts +15 -0
  61. package/dist/agents/channels/slack/index.d.ts.map +1 -0
  62. package/dist/agents/channels/slack/index.js +15 -0
  63. package/dist/agents/channels/slack/index.js.map +1 -0
  64. package/dist/agents/channels/slack/media.d.ts +90 -0
  65. package/dist/agents/channels/slack/media.d.ts.map +1 -0
  66. package/dist/agents/channels/slack/media.js +215 -0
  67. package/dist/agents/channels/slack/media.js.map +1 -0
  68. package/dist/agents/channels/slack/module.d.ts +26 -0
  69. package/dist/agents/channels/slack/module.d.ts.map +1 -0
  70. package/dist/agents/channels/slack/module.js +67 -0
  71. package/dist/agents/channels/slack/module.js.map +1 -0
  72. package/dist/agents/channels/slack/plugin.d.ts +69 -0
  73. package/dist/agents/channels/slack/plugin.d.ts.map +1 -0
  74. package/dist/agents/channels/slack/plugin.js +318 -0
  75. package/dist/agents/channels/slack/plugin.js.map +1 -0
  76. package/dist/agents/channels/slack/probe.d.ts +72 -0
  77. package/dist/agents/channels/slack/probe.d.ts.map +1 -0
  78. package/dist/agents/channels/slack/probe.js +103 -0
  79. package/dist/agents/channels/slack/probe.js.map +1 -0
  80. package/dist/agents/channels/slack/proxy-agent.d.ts +30 -0
  81. package/dist/agents/channels/slack/proxy-agent.d.ts.map +1 -0
  82. package/dist/agents/channels/slack/proxy-agent.js +44 -0
  83. package/dist/agents/channels/slack/proxy-agent.js.map +1 -0
  84. package/dist/agents/channels/slack/reasoning-lane.d.ts +42 -0
  85. package/dist/agents/channels/slack/reasoning-lane.d.ts.map +1 -0
  86. package/dist/agents/channels/slack/reasoning-lane.js +68 -0
  87. package/dist/agents/channels/slack/reasoning-lane.js.map +1 -0
  88. package/dist/agents/channels/slack/user-directory.d.ts +69 -0
  89. package/dist/agents/channels/slack/user-directory.d.ts.map +1 -0
  90. package/dist/agents/channels/slack/user-directory.js +94 -0
  91. package/dist/agents/channels/slack/user-directory.js.map +1 -0
  92. package/dist/agents/channels/slack/webhook.d.ts +89 -0
  93. package/dist/agents/channels/slack/webhook.d.ts.map +1 -0
  94. package/dist/agents/channels/slack/webhook.js +228 -0
  95. package/dist/agents/channels/slack/webhook.js.map +1 -0
  96. package/dist/agents/channels/telegram/format.d.ts.map +1 -1
  97. package/dist/agents/channels/telegram/format.js +17 -1
  98. package/dist/agents/channels/telegram/format.js.map +1 -1
  99. package/dist/agents/channels/telegram/webhook.d.ts.map +1 -1
  100. package/dist/agents/channels/telegram/webhook.js +7 -1
  101. package/dist/agents/channels/telegram/webhook.js.map +1 -1
  102. package/dist/agents/extensions/modules/index.d.ts.map +1 -1
  103. package/dist/agents/extensions/modules/index.js +5 -0
  104. package/dist/agents/extensions/modules/index.js.map +1 -1
  105. package/dist/buildstamp.json +1 -1
  106. package/dist/core/server.d.ts.map +1 -1
  107. package/dist/core/server.js +24 -5
  108. package/dist/core/server.js.map +1 -1
  109. package/package.json +4 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-lane.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/slack/reasoning-lane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,8BAAoB,CAAC;AAKlD,MAAM,WAAW,mBAAmB;IACnC,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAKpE"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Slack reasoning-lane split (OPTIONAL, default OFF).
3
+ *
4
+ * Brigade strips `<think>…</think>` reasoning from every channel reply via the
5
+ * shared `sanitizeReplyForChannel` — recipients see only the final answer. For
6
+ * Slack an operator can OPT IN (config `channels.slack.surfaceReasoning: true`)
7
+ * to ALSO receive the reasoning trace as a separate, prefixed message.
8
+ *
9
+ * This module is the pure splitter: given the raw agent reply, it returns
10
+ * `{ reasoningText?, answerText }`. When surfacing is OFF the pipeline never
11
+ * calls this and behavior is byte-identical to today (strip + send answer). When
12
+ * ON, the pipeline sends `reasoningText` first (a `🧠 Reasoning:` block) then the
13
+ * normal sanitized answer.
14
+ *
15
+ * The answer half is computed with the SAME sanitizer the default path uses, so
16
+ * enabling reasoning never changes what the answer message contains — it only
17
+ * ADDS the reasoning message in front.
18
+ *
19
+ * Pure / deterministic / dependency-light (re-uses `sanitizeReplyForChannel`).
20
+ * Slack mirror of `telegram/reasoning-lane.ts`.
21
+ */
22
+ import { sanitizeReplyForChannel } from "../reply-sanitizer.js";
23
+ /** Prefix on the reasoning message so the recipient knows it's the trace. */
24
+ export const REASONING_PREFIX = "🧠 Reasoning:\n";
25
+ /** Matches a `<think>`/`<thinking>`/`<thought>` open/close tag. */
26
+ const THINK_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought)\b[^<>]*>/gi;
27
+ /**
28
+ * Extract the concatenated text INSIDE `<think>…</think>` blocks. Handles
29
+ * multiple blocks and an unclosed trailing block (model truncated mid-thought).
30
+ * Returns "" when there's no reasoning content.
31
+ */
32
+ export function extractReasoning(text) {
33
+ if (!text)
34
+ return "";
35
+ const parts = [];
36
+ let inThink = false;
37
+ let lastIndex = 0;
38
+ THINK_TAG_RE.lastIndex = 0;
39
+ for (const match of text.matchAll(THINK_TAG_RE)) {
40
+ const idx = match.index ?? 0;
41
+ const isClose = match[1] === "/";
42
+ if (inThink && isClose) {
43
+ parts.push(text.slice(lastIndex, idx));
44
+ inThink = false;
45
+ }
46
+ else if (!inThink && !isClose) {
47
+ inThink = true;
48
+ lastIndex = idx + match[0].length;
49
+ }
50
+ }
51
+ // Unclosed trailing block — keep what the model emitted.
52
+ if (inThink)
53
+ parts.push(text.slice(lastIndex));
54
+ return parts.join("\n").trim();
55
+ }
56
+ /**
57
+ * Split a raw agent reply into an optional reasoning message + the sanitized
58
+ * answer. The answer is identical to `sanitizeReplyForChannel(raw)`; the
59
+ * reasoning is only populated when a `<think>` block carried content.
60
+ */
61
+ export function splitSlackReasoning(raw) {
62
+ const answerText = sanitizeReplyForChannel(raw ?? "");
63
+ const reasoning = extractReasoning(raw ?? "");
64
+ if (!reasoning)
65
+ return { answerText };
66
+ return { reasoningText: `${REASONING_PREFIX}${reasoning}`, answerText };
67
+ }
68
+ //# sourceMappingURL=reasoning-lane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-lane.js","sourceRoot":"","sources":["../../../../src/agents/channels/slack/reasoning-lane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;AAElD,mEAAmE;AACnE,MAAM,YAAY,GAAG,kDAAkD,CAAC;AASxE;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,YAAY,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACjD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;QACjC,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;YACvC,OAAO,GAAG,KAAK,CAAC;QACjB,CAAC;aAAM,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;YACjC,OAAO,GAAG,IAAI,CAAC;YACf,SAAS,GAAG,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACnC,CAAC;IACF,CAAC;IACD,yDAAyD;IACzD,IAAI,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC9C,MAAM,UAAU,GAAG,uBAAuB,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IACtC,OAAO,EAAE,aAAa,EAAE,GAAG,gBAAgB,GAAG,SAAS,EAAE,EAAE,UAAU,EAAE,CAAC;AACzE,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Slack user-name directory — a small cached `id → display-name` resolver.
3
+ *
4
+ * Slack message events carry only the sender's user id (`U…` / `W…`); the human
5
+ * name needs a `users.info` call. Doing that synchronously on the inbound hot
6
+ * path would add a network round-trip per message, so this directory resolves
7
+ * names in the BACKGROUND:
8
+ *
9
+ * - `prime(id)` fires a memoized, non-blocking `users.info` and caches the
10
+ * resolved name.
11
+ * - `resolveNameSync(id)` reads whatever is currently cached (no network).
12
+ *
13
+ * So the FIRST message from a never-seen user surfaces the raw id; every message
14
+ * after the prime settles surfaces the name. In a real workspace the bot sees
15
+ * the same people repeatedly, so names appear almost immediately. Names are
16
+ * cached with a TTL; failed / empty lookups are negative-cached briefly so a bad
17
+ * id never hammers the API, and a concurrent burst for one id fires a single
18
+ * request (in-flight de-dupe).
19
+ *
20
+ * Built over the injected `users.info` slice so it unit-tests with a fake — no
21
+ * live workspace, no globals.
22
+ */
23
+ /** The minimal `users.info` slice the directory drives (a `WebClient` satisfies it). */
24
+ export interface SlackUsersInfoApi {
25
+ users?: {
26
+ info?(args: {
27
+ user: string;
28
+ }): Promise<{
29
+ ok?: boolean;
30
+ error?: string;
31
+ user?: SlackUserInfoUser;
32
+ }>;
33
+ };
34
+ }
35
+ /** The subset of a Slack user object the directory reads for a display name. */
36
+ export interface SlackUserInfoUser {
37
+ id?: string;
38
+ name?: string;
39
+ real_name?: string;
40
+ profile?: {
41
+ display_name?: string;
42
+ real_name?: string;
43
+ };
44
+ }
45
+ export interface SlackUserDirectory {
46
+ /** Read the cached display name for a user id (no network). Undefined when not cached yet. */
47
+ resolveNameSync(id: string | undefined): string | undefined;
48
+ /**
49
+ * Fire a memoized background `users.info` so the name is cached for next time.
50
+ * No-op when the id is empty, isn't a user id, is already fresh in the cache,
51
+ * or has a request in flight. Never throws, never blocks.
52
+ */
53
+ prime(id: string | undefined): void;
54
+ }
55
+ /**
56
+ * A Slack human USER id is `U…` or `W…` (Enterprise Grid). Bot ids (`B…`),
57
+ * channel ids (`C…`/`D…`/`G…`), and empties have no `users.info`, so we never
58
+ * call for them.
59
+ */
60
+ export declare function isSlackUserId(id: string): boolean;
61
+ export declare function createSlackUserDirectory(args: {
62
+ web: SlackUsersInfoApi | null;
63
+ ttlMs?: number;
64
+ negativeTtlMs?: number;
65
+ /** Injectable clock for deterministic TTL tests (defaults to `Date.now`). */
66
+ nowImpl?: () => number;
67
+ log?: (msg: string, meta?: Record<string, unknown>) => void;
68
+ }): SlackUserDirectory;
69
+ //# sourceMappingURL=user-directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-directory.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/slack/user-directory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IACjC,KAAK,CAAC,EAAE;QACP,IAAI,CAAC,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,EAAE,CAAC,EAAE,OAAO,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,iBAAiB,CAAA;SAAE,CAAC,CAAC;KACnG,CAAC;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,kBAAkB;IAClC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IAC5D;;;;OAIG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;CACpC;AAOD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC5D,GAAG,kBAAkB,CAyDrB"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Slack user-name directory — a small cached `id → display-name` resolver.
3
+ *
4
+ * Slack message events carry only the sender's user id (`U…` / `W…`); the human
5
+ * name needs a `users.info` call. Doing that synchronously on the inbound hot
6
+ * path would add a network round-trip per message, so this directory resolves
7
+ * names in the BACKGROUND:
8
+ *
9
+ * - `prime(id)` fires a memoized, non-blocking `users.info` and caches the
10
+ * resolved name.
11
+ * - `resolveNameSync(id)` reads whatever is currently cached (no network).
12
+ *
13
+ * So the FIRST message from a never-seen user surfaces the raw id; every message
14
+ * after the prime settles surfaces the name. In a real workspace the bot sees
15
+ * the same people repeatedly, so names appear almost immediately. Names are
16
+ * cached with a TTL; failed / empty lookups are negative-cached briefly so a bad
17
+ * id never hammers the API, and a concurrent burst for one id fires a single
18
+ * request (in-flight de-dupe).
19
+ *
20
+ * Built over the injected `users.info` slice so it unit-tests with a fake — no
21
+ * live workspace, no globals.
22
+ */
23
+ /** Positive cache lifetime — a resolved name is reused for an hour. */
24
+ const DEFAULT_TTL_MS = 60 * 60 * 1_000;
25
+ /** Negative cache lifetime — a failed / empty lookup is not retried for a minute. */
26
+ const NEGATIVE_TTL_MS = 60 * 1_000;
27
+ /**
28
+ * A Slack human USER id is `U…` or `W…` (Enterprise Grid). Bot ids (`B…`),
29
+ * channel ids (`C…`/`D…`/`G…`), and empties have no `users.info`, so we never
30
+ * call for them.
31
+ */
32
+ export function isSlackUserId(id) {
33
+ return /^[UW][A-Z0-9]+$/.test(id);
34
+ }
35
+ export function createSlackUserDirectory(args) {
36
+ const ttlMs = args.ttlMs ?? DEFAULT_TTL_MS;
37
+ const negativeTtlMs = args.negativeTtlMs ?? NEGATIVE_TTL_MS;
38
+ const now = args.nowImpl ?? (() => Date.now());
39
+ const cache = new Map();
40
+ const inflight = new Set();
41
+ // Capture a `this`-bound `users.info` caller ONCE (null when the injected web
42
+ // has no such slice — e.g. a test fake), so `prime` stays a safe no-op and the
43
+ // call preserves the WebClient facet as its receiver.
44
+ const usersApi = args.web?.users;
45
+ const fetchUser = usersApi && usersApi.info ? (id) => usersApi.info.call(usersApi, { user: id }) : null;
46
+ const nameFromUser = (u) => {
47
+ if (!u)
48
+ return undefined;
49
+ const candidate = u.profile?.display_name || u.profile?.real_name || u.real_name || u.name;
50
+ const trimmed = typeof candidate === "string" ? candidate.trim() : "";
51
+ return trimmed || undefined;
52
+ };
53
+ const isFresh = (id) => {
54
+ const hit = cache.get(id);
55
+ return hit !== undefined && hit.expiresAt > now();
56
+ };
57
+ const resolveNameSync = (id) => {
58
+ if (!id)
59
+ return undefined;
60
+ const hit = cache.get(id);
61
+ if (hit && hit.expiresAt > now())
62
+ return hit.name;
63
+ return undefined;
64
+ };
65
+ const prime = (id) => {
66
+ if (!id || !fetchUser)
67
+ return;
68
+ if (!isSlackUserId(id))
69
+ return;
70
+ if (inflight.has(id) || isFresh(id))
71
+ return;
72
+ inflight.add(id);
73
+ void (async () => {
74
+ try {
75
+ const res = await fetchUser(id);
76
+ const name = res && res.ok === false ? undefined : nameFromUser(res?.user);
77
+ cache.set(id, { ...(name ? { name } : {}), expiresAt: now() + (name ? ttlMs : negativeTtlMs) });
78
+ }
79
+ catch (err) {
80
+ // Negative-cache the failure so a transient/bad id isn't retried in a loop.
81
+ cache.set(id, { expiresAt: now() + negativeTtlMs });
82
+ args.log?.("slack users.info failed", {
83
+ user: id,
84
+ error: err instanceof Error ? err.message : String(err),
85
+ });
86
+ }
87
+ finally {
88
+ inflight.delete(id);
89
+ }
90
+ })();
91
+ };
92
+ return { resolveNameSync, prime };
93
+ }
94
+ //# sourceMappingURL=user-directory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-directory.js","sourceRoot":"","sources":["../../../../src/agents/channels/slack/user-directory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Slack Events-API gateway route.
3
+ *
4
+ * In events transport mode (`channels.slack.mode: "events"`) Slack POSTs each
5
+ * event to a public URL instead of Brigade opening a Socket Mode websocket. This
6
+ * module builds the Brigade `HttpRoute` that receives those POSTs:
7
+ *
8
+ * 1. Verify the `X-Slack-Signature` header. Slack signs every request as
9
+ * `v0=` + HMAC-SHA256 of `v0:${timestamp}:${rawBody}` keyed with the app's
10
+ * signing secret. A mismatch (or a stale timestamp outside the replay
11
+ * window) → 401, BEFORE the body is routed, so a forged event can't reach
12
+ * the agent. The signature is computed over the RAW body, so the handler
13
+ * reads the raw bytes first and verifies before `JSON.parse`.
14
+ * 2. Answer the one-time `url_verification` handshake by echoing the
15
+ * `challenge` value (Slack's endpoint-ownership check).
16
+ * 3. For an `event_callback`, hand the inner event to the started Slack
17
+ * adapter's `feedWebhookEvent("event", …)`, which runs it through the SAME
18
+ * normalize + dedupe + dispatch path Socket Mode uses. Interactive
19
+ * (`block_actions`) + slash-command payloads arrive as
20
+ * `application/x-www-form-urlencoded` with a `payload=` field and route via
21
+ * `feedWebhookEvent("interactive" | "slash", …)`.
22
+ * 4. Reply `200` so Slack marks the event delivered.
23
+ *
24
+ * The route is registered with `auth: "none"` because Slack authenticates via
25
+ * the signed request, not Brigade's operator-auth (Slack can't present an
26
+ * operator credential). The signature check IS the auth.
27
+ *
28
+ * Socket mode never registers this route — it's added by the module only when
29
+ * events mode is configured, so the default local-first install exposes no
30
+ * inbound HTTP surface. Slack mirror of `telegram/webhook.ts`.
31
+ */
32
+ import type { HttpRoute } from "../sdk.js";
33
+ /** Slack's request-signature header. */
34
+ export declare const SLACK_SIGNATURE_HEADER = "x-slack-signature";
35
+ /** Slack's request-timestamp header (replay-window guard). */
36
+ export declare const SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
37
+ /** Constant-time compare of two hex signatures — avoids leaking via timing. */
38
+ export declare function safeEqualSignature(a: string, b: string): boolean;
39
+ /**
40
+ * Verify a Slack request signature over the raw body. Returns false when no
41
+ * secret is configured AND a signature was supplied (a configured Slack app
42
+ * always signs), when the timestamp is missing / stale, or when the computed
43
+ * `v0=` HMAC doesn't match. When `expectedSecret` is empty the check is SKIPPED
44
+ * (returns true) — the operator opted out of verification (not recommended).
45
+ */
46
+ export declare function verifySlackSignature(args: {
47
+ signingSecret: string;
48
+ signature: string | undefined;
49
+ timestamp: string | undefined;
50
+ rawBody: string;
51
+ nowSeconds?: number;
52
+ }): boolean;
53
+ /**
54
+ * Parse the request payload. Slack delivers EVENTS as JSON
55
+ * (`application/json`) and INTERACTIONS / SLASH-COMMANDS as
56
+ * `application/x-www-form-urlencoded` carrying a `payload=` (interactive) or
57
+ * flat form fields (slash). Returns a discriminated shape the handler routes on.
58
+ */
59
+ export declare function parseSlackBody(rawBody: string, contentType: string): {
60
+ kind: "json";
61
+ data: Record<string, unknown>;
62
+ } | {
63
+ kind: "interactive";
64
+ data: Record<string, unknown>;
65
+ } | {
66
+ kind: "slash";
67
+ data: Record<string, unknown>;
68
+ } | null;
69
+ /** The minimal adapter surface the webhook route drives. */
70
+ export interface SlackWebhookSink {
71
+ /** Feed a parsed Slack payload into the inbound path. */
72
+ feedWebhookEvent(kind: "event" | "interactive" | "slash", payload: unknown): void;
73
+ }
74
+ export interface BuildSlackWebhookRouteArgs {
75
+ /** The gateway route path (e.g. `/slack/events`). */
76
+ path: string;
77
+ /** The configured signing secret (`""` → no signature check). */
78
+ signingSecret: string;
79
+ /** Resolve the started adapter to feed events into (null when not started). */
80
+ resolveSink: () => SlackWebhookSink | null;
81
+ /** Logger (token-redacted upstream). */
82
+ log?: (msg: string, meta?: Record<string, unknown>) => void;
83
+ }
84
+ /**
85
+ * Build the Brigade `HttpRoute` for the Slack Events API. Register it via
86
+ * `b.httpRoute(...)` from the module when events mode is active.
87
+ */
88
+ export declare function buildSlackWebhookRoute(args: BuildSlackWebhookRouteArgs): HttpRoute;
89
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/slack/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,wCAAwC;AACxC,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,8BAA8B,CAAC;AAQlE,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAOhE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAYV;AA0BD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC7B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,IAAI,CA0BpK;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAChC,yDAAyD;IACzD,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,0BAA0B;IAC1C,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,WAAW,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;IAC3C,wCAAwC;IACxC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC5D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,0BAA0B,GAAG,SAAS,CAoFlF"}
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Slack Events-API gateway route.
3
+ *
4
+ * In events transport mode (`channels.slack.mode: "events"`) Slack POSTs each
5
+ * event to a public URL instead of Brigade opening a Socket Mode websocket. This
6
+ * module builds the Brigade `HttpRoute` that receives those POSTs:
7
+ *
8
+ * 1. Verify the `X-Slack-Signature` header. Slack signs every request as
9
+ * `v0=` + HMAC-SHA256 of `v0:${timestamp}:${rawBody}` keyed with the app's
10
+ * signing secret. A mismatch (or a stale timestamp outside the replay
11
+ * window) → 401, BEFORE the body is routed, so a forged event can't reach
12
+ * the agent. The signature is computed over the RAW body, so the handler
13
+ * reads the raw bytes first and verifies before `JSON.parse`.
14
+ * 2. Answer the one-time `url_verification` handshake by echoing the
15
+ * `challenge` value (Slack's endpoint-ownership check).
16
+ * 3. For an `event_callback`, hand the inner event to the started Slack
17
+ * adapter's `feedWebhookEvent("event", …)`, which runs it through the SAME
18
+ * normalize + dedupe + dispatch path Socket Mode uses. Interactive
19
+ * (`block_actions`) + slash-command payloads arrive as
20
+ * `application/x-www-form-urlencoded` with a `payload=` field and route via
21
+ * `feedWebhookEvent("interactive" | "slash", …)`.
22
+ * 4. Reply `200` so Slack marks the event delivered.
23
+ *
24
+ * The route is registered with `auth: "none"` because Slack authenticates via
25
+ * the signed request, not Brigade's operator-auth (Slack can't present an
26
+ * operator credential). The signature check IS the auth.
27
+ *
28
+ * Socket mode never registers this route — it's added by the module only when
29
+ * events mode is configured, so the default local-first install exposes no
30
+ * inbound HTTP surface. Slack mirror of `telegram/webhook.ts`.
31
+ */
32
+ import { createHmac, timingSafeEqual } from "node:crypto";
33
+ /** Slack's request-signature header. */
34
+ export const SLACK_SIGNATURE_HEADER = "x-slack-signature";
35
+ /** Slack's request-timestamp header (replay-window guard). */
36
+ export const SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
37
+ /** Signature version prefix Slack uses (`v0`). */
38
+ const SLACK_SIG_VERSION = "v0";
39
+ /** Reject a request whose timestamp is older than this (replay protection). */
40
+ const MAX_TIMESTAMP_SKEW_SECONDS = 60 * 5;
41
+ /** Cap on the webhook body (a Slack event is small; 1 MiB is generous). */
42
+ const WEBHOOK_MAX_BODY_BYTES = 1 * 1024 * 1024;
43
+ /** Constant-time compare of two hex signatures — avoids leaking via timing. */
44
+ export function safeEqualSignature(a, b) {
45
+ if (a.length !== b.length)
46
+ return false;
47
+ try {
48
+ return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /**
55
+ * Verify a Slack request signature over the raw body. Returns false when no
56
+ * secret is configured AND a signature was supplied (a configured Slack app
57
+ * always signs), when the timestamp is missing / stale, or when the computed
58
+ * `v0=` HMAC doesn't match. When `expectedSecret` is empty the check is SKIPPED
59
+ * (returns true) — the operator opted out of verification (not recommended).
60
+ */
61
+ export function verifySlackSignature(args) {
62
+ if (!args.signingSecret)
63
+ return true; // no secret configured → no check
64
+ const sig = typeof args.signature === "string" ? args.signature : "";
65
+ const ts = typeof args.timestamp === "string" ? args.timestamp : "";
66
+ if (!sig || !ts)
67
+ return false;
68
+ const tsNum = Number.parseInt(ts, 10);
69
+ if (!Number.isFinite(tsNum))
70
+ return false;
71
+ const now = args.nowSeconds ?? Math.floor(Date.now() / 1000);
72
+ if (Math.abs(now - tsNum) > MAX_TIMESTAMP_SKEW_SECONDS)
73
+ return false; // stale → replay guard
74
+ const base = `${SLACK_SIG_VERSION}:${ts}:${args.rawBody}`;
75
+ const computed = `${SLACK_SIG_VERSION}=${createHmac("sha256", args.signingSecret).update(base).digest("hex")}`;
76
+ return safeEqualSignature(computed, sig);
77
+ }
78
+ /** Read a request body up to `maxBytes`, rejecting (→ null) when it overflows. */
79
+ function readBody(req, maxBytes) {
80
+ return new Promise((resolve) => {
81
+ const chunks = [];
82
+ let size = 0;
83
+ let overflowed = false;
84
+ req.on("data", (chunk) => {
85
+ if (overflowed)
86
+ return;
87
+ size += chunk.length;
88
+ if (size > maxBytes) {
89
+ overflowed = true;
90
+ resolve(null);
91
+ return;
92
+ }
93
+ chunks.push(chunk);
94
+ });
95
+ req.on("end", () => {
96
+ if (overflowed)
97
+ return;
98
+ resolve(Buffer.concat(chunks).toString("utf8"));
99
+ });
100
+ req.on("error", () => resolve(null));
101
+ });
102
+ }
103
+ /**
104
+ * Parse the request payload. Slack delivers EVENTS as JSON
105
+ * (`application/json`) and INTERACTIONS / SLASH-COMMANDS as
106
+ * `application/x-www-form-urlencoded` carrying a `payload=` (interactive) or
107
+ * flat form fields (slash). Returns a discriminated shape the handler routes on.
108
+ */
109
+ export function parseSlackBody(rawBody, contentType) {
110
+ const ct = (contentType ?? "").toLowerCase();
111
+ if (ct.includes("application/json")) {
112
+ try {
113
+ return { kind: "json", data: JSON.parse(rawBody) };
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ // Form-encoded: an interactive payload rides as `payload=<json>`; a slash
120
+ // command is flat form fields (`command=/x&text=…`).
121
+ const params = new URLSearchParams(rawBody);
122
+ const payload = params.get("payload");
123
+ if (payload) {
124
+ try {
125
+ return { kind: "interactive", data: JSON.parse(payload) };
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ if (params.has("command")) {
132
+ const data = {};
133
+ for (const [k, v] of params.entries())
134
+ data[k] = v;
135
+ return { kind: "slash", data };
136
+ }
137
+ return null;
138
+ }
139
+ /**
140
+ * Build the Brigade `HttpRoute` for the Slack Events API. Register it via
141
+ * `b.httpRoute(...)` from the module when events mode is active.
142
+ */
143
+ export function buildSlackWebhookRoute(args) {
144
+ const handler = async (req, res) => {
145
+ const reply = (status, body, contentType = "application/json") => {
146
+ res.statusCode = status;
147
+ res.setHeader("content-type", contentType);
148
+ res.end(typeof body === "string" ? body : JSON.stringify(body));
149
+ };
150
+ // Only POST carries events.
151
+ if ((req.method ?? "").toUpperCase() !== "POST") {
152
+ reply(405, { ok: false, error: "method not allowed" });
153
+ return;
154
+ }
155
+ // The gateway dispatcher has ALREADY drained the request stream and buffered
156
+ // it onto `req.body` (see core/server.ts). Re-reading the stream here would
157
+ // hang until the 30s timeout (→ 408) because the `data`/`end` events already
158
+ // fired. Read the pre-buffered body first; only fall back to streaming when
159
+ // the route is exercised outside the gateway (e.g. a direct unit test).
160
+ const pre = req.body;
161
+ const raw = pre ? pre.toString("utf8") : await readBody(req, WEBHOOK_MAX_BODY_BYTES);
162
+ if (raw === null) {
163
+ reply(413, { ok: false, error: "payload too large" });
164
+ return;
165
+ }
166
+ // Signature check FIRST (over the RAW body) — refuse a forged event before
167
+ // parsing / routing.
168
+ const sigHeader = req.headers[SLACK_SIGNATURE_HEADER];
169
+ const tsHeader = req.headers[SLACK_TIMESTAMP_HEADER];
170
+ const signature = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
171
+ const timestamp = Array.isArray(tsHeader) ? tsHeader[0] : tsHeader;
172
+ if (!verifySlackSignature({ signingSecret: args.signingSecret, signature, timestamp, rawBody: raw })) {
173
+ args.log?.("slack webhook rejected — bad signature");
174
+ reply(401, { ok: false, error: "unauthorized" });
175
+ return;
176
+ }
177
+ const contentType = (() => {
178
+ const c = req.headers["content-type"];
179
+ return Array.isArray(c) ? (c[0] ?? "") : (c ?? "");
180
+ })();
181
+ const parsed = parseSlackBody(raw, contentType);
182
+ if (!parsed) {
183
+ reply(400, { ok: false, error: "invalid body" });
184
+ return;
185
+ }
186
+ // The one-time endpoint-ownership handshake — echo the challenge verbatim.
187
+ if (parsed.kind === "json" && parsed.data["type"] === "url_verification") {
188
+ const challenge = typeof parsed.data["challenge"] === "string" ? parsed.data["challenge"] : "";
189
+ reply(200, { challenge });
190
+ return;
191
+ }
192
+ const sink = args.resolveSink();
193
+ if (sink) {
194
+ try {
195
+ if (parsed.kind === "json" && parsed.data["type"] === "event_callback") {
196
+ sink.feedWebhookEvent("event", parsed.data);
197
+ }
198
+ else if (parsed.kind === "interactive") {
199
+ sink.feedWebhookEvent("interactive", parsed.data);
200
+ }
201
+ else if (parsed.kind === "slash") {
202
+ sink.feedWebhookEvent("slash", parsed.data);
203
+ }
204
+ }
205
+ catch (err) {
206
+ args.log?.("slack webhook dispatch threw", { error: err instanceof Error ? err.message : String(err) });
207
+ }
208
+ }
209
+ // Always 200 so Slack doesn't retry-storm — a dispatch error is ours to fix,
210
+ // not Slack's to redeliver. A slash command replies empty to clear the spinner.
211
+ if (parsed.kind === "slash") {
212
+ reply(200, "");
213
+ }
214
+ else {
215
+ reply(200, { ok: true });
216
+ }
217
+ };
218
+ return {
219
+ method: "POST",
220
+ path: args.path,
221
+ auth: "none", // Slack can't present operator-auth; the signed request IS the auth.
222
+ match: "exact",
223
+ maxBodyBytes: WEBHOOK_MAX_BODY_BYTES,
224
+ skipSessionGuard: true,
225
+ handler,
226
+ };
227
+ }
228
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../../../src/agents/channels/slack/webhook.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/telegram/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AA+HD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+E/D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,MAA+B,GACpC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAmBhC"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/telegram/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AA8ID;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+E/D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,MAA+B,GACpC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAmBhC"}
@@ -97,7 +97,23 @@ function matchMarkdownLink(text, start) {
97
97
  return null;
98
98
  if (text[labelEnd + 1] !== "(")
99
99
  return null;
100
- const urlEnd = text.indexOf(")", labelEnd + 2);
100
+ // Balanced-paren scan from just after the opening `(` so a URL that itself
101
+ // contains parentheses (e.g. `…/Mercury_(planet)`) keeps its closing `)`. A
102
+ // plain `indexOf(")")` truncated at the FIRST `)`, dropping the rest of the url.
103
+ let depth = 1;
104
+ let urlEnd = -1;
105
+ for (let j = labelEnd + 2; j < text.length; j++) {
106
+ const c = text[j];
107
+ if (c === "(")
108
+ depth += 1;
109
+ else if (c === ")") {
110
+ depth -= 1;
111
+ if (depth === 0) {
112
+ urlEnd = j;
113
+ break;
114
+ }
115
+ }
116
+ }
101
117
  if (urlEnd === -1)
102
118
  return null;
103
119
  const label = text.slice(start + 1, labelEnd);