agent-sin 0.1.12 → 0.1.16

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 (97) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +2 -1
  3. package/builtin-skills/_shared/_todo_lib.py +290 -0
  4. package/builtin-skills/even-g2-setup/main.ts +896 -0
  5. package/builtin-skills/even-g2-setup/skill.yaml +133 -0
  6. package/builtin-skills/memo-delete/main.py +28 -107
  7. package/builtin-skills/memo-delete/skill.yaml +10 -21
  8. package/builtin-skills/memo-index/main.py +96 -64
  9. package/builtin-skills/memo-index/skill.yaml +4 -10
  10. package/builtin-skills/memo-list/main.py +126 -72
  11. package/builtin-skills/memo-list/skill.yaml +8 -14
  12. package/builtin-skills/memo-save/main.py +191 -25
  13. package/builtin-skills/memo-save/skill.yaml +29 -5
  14. package/builtin-skills/memo-search/main.py +38 -18
  15. package/builtin-skills/memo-vector-search/main.py +11 -6
  16. package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
  17. package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
  18. package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
  19. package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
  20. package/builtin-skills/schedule-add/main.py +26 -0
  21. package/builtin-skills/service-restart/main.ts +249 -0
  22. package/builtin-skills/service-restart/skill.yaml +49 -0
  23. package/builtin-skills/todo-add/main.py +3 -1
  24. package/builtin-skills/todo-delete/main.py +3 -1
  25. package/builtin-skills/todo-done/main.py +3 -1
  26. package/builtin-skills/todo-list/main.py +4 -1
  27. package/builtin-skills/todo-tick/main.py +3 -1
  28. package/builtin-skills/topic-knowledge-read/main.py +118 -0
  29. package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
  30. package/dist/builder/build-action-classifier.d.ts +18 -0
  31. package/dist/builder/build-action-classifier.js +82 -1
  32. package/dist/builder/build-flow.d.ts +33 -4
  33. package/dist/builder/build-flow.js +251 -89
  34. package/dist/builder/builder-session.d.ts +1 -1
  35. package/dist/builder/builder-session.js +112 -7
  36. package/dist/builder/conversation-router.d.ts +4 -2
  37. package/dist/builder/conversation-router.js +19 -2
  38. package/dist/cli/index.js +323 -20
  39. package/dist/core/ai-provider.d.ts +1 -0
  40. package/dist/core/ai-provider.js +8 -3
  41. package/dist/core/chat-engine.d.ts +9 -3
  42. package/dist/core/chat-engine.js +1263 -146
  43. package/dist/core/config.d.ts +4 -0
  44. package/dist/core/config.js +82 -0
  45. package/dist/core/daily-memory-promotion.d.ts +7 -0
  46. package/dist/core/daily-memory-promotion.js +596 -18
  47. package/dist/core/image-attachments.d.ts +31 -0
  48. package/dist/core/image-attachments.js +237 -0
  49. package/dist/core/logger.d.ts +2 -1
  50. package/dist/core/logger.js +77 -1
  51. package/dist/core/memo-migration.d.ts +3 -0
  52. package/dist/core/memo-migration.js +422 -0
  53. package/dist/core/native-modules.d.ts +24 -0
  54. package/dist/core/native-modules.js +99 -0
  55. package/dist/core/notifier.d.ts +8 -3
  56. package/dist/core/notifier.js +191 -17
  57. package/dist/core/obsidian-vault.d.ts +19 -0
  58. package/dist/core/obsidian-vault.js +477 -0
  59. package/dist/core/operating-model.d.ts +2 -0
  60. package/dist/core/operating-model.js +15 -0
  61. package/dist/core/output-writer.d.ts +3 -2
  62. package/dist/core/output-writer.js +108 -7
  63. package/dist/core/profile-memory.js +22 -1
  64. package/dist/core/runtime.d.ts +2 -0
  65. package/dist/core/runtime.js +9 -1
  66. package/dist/core/secrets.d.ts +4 -0
  67. package/dist/core/secrets.js +34 -0
  68. package/dist/core/skill-history.d.ts +44 -0
  69. package/dist/core/skill-history.js +329 -0
  70. package/dist/core/skill-registry.d.ts +5 -0
  71. package/dist/core/skill-registry.js +11 -0
  72. package/dist/discord/bot.d.ts +1 -0
  73. package/dist/discord/bot.js +181 -10
  74. package/dist/even-g2/gateway.d.ts +15 -0
  75. package/dist/even-g2/gateway.js +868 -0
  76. package/dist/runtimes/codex-app-server.d.ts +5 -1
  77. package/dist/runtimes/codex-app-server.js +147 -8
  78. package/dist/runtimes/python-runner.js +82 -0
  79. package/dist/runtimes/typescript-runner.js +13 -1
  80. package/dist/skills-sdk/types.d.ts +19 -4
  81. package/dist/telegram/bot.d.ts +1 -0
  82. package/dist/telegram/bot.js +115 -7
  83. package/package.json +3 -1
  84. package/templates/even-g2-agent/README.md +83 -0
  85. package/templates/even-g2-agent/app.json +20 -0
  86. package/templates/even-g2-agent/index.html +31 -0
  87. package/templates/even-g2-agent/package-lock.json +1836 -0
  88. package/templates/even-g2-agent/package.json +22 -0
  89. package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
  90. package/templates/even-g2-agent/src/embedded-config.ts +4 -0
  91. package/templates/even-g2-agent/src/main.ts +539 -0
  92. package/templates/even-g2-agent/src/style.css +70 -0
  93. package/templates/even-g2-agent/tsconfig.json +11 -0
  94. package/templates/skill-python/main.py +20 -2
  95. package/templates/skill-python/skill.yaml +9 -0
  96. package/templates/skill-typescript/main.ts +40 -5
  97. package/templates/skill-typescript/skill.yaml +9 -0
@@ -6,6 +6,7 @@ export interface SkillOutputDefinition {
6
6
  path: string;
7
7
  filename: string;
8
8
  append?: boolean;
9
+ merge_mode?: "overwrite" | "append" | "update_or_append";
9
10
  show_saved?: boolean;
10
11
  }
11
12
  export type DiscordSlashOptionType = "string" | "integer" | "number" | "boolean";
@@ -53,6 +54,10 @@ export interface SkillManifest {
53
54
  read?: boolean;
54
55
  write?: boolean;
55
56
  };
57
+ history?: {
58
+ read?: boolean;
59
+ write?: boolean;
60
+ };
56
61
  retry?: {
57
62
  max_attempts?: number;
58
63
  delay_ms?: number;
@@ -67,11 +67,22 @@ export async function loadSkillManifest(skillDir) {
67
67
  enabled: parsed.enabled ?? true,
68
68
  output_mode: parsed.output_mode,
69
69
  side_effect: parsed.side_effect === true,
70
+ history: normalizeHistoryConfig(parsed.history),
70
71
  dir: skillDir,
71
72
  source,
72
73
  override,
73
74
  };
74
75
  }
76
+ function normalizeHistoryConfig(value) {
77
+ if (!value || typeof value !== "object" || Array.isArray(value))
78
+ return undefined;
79
+ const raw = value;
80
+ const read = raw.read === true;
81
+ const write = raw.write === true;
82
+ if (!read && !write)
83
+ return undefined;
84
+ return { read, write };
85
+ }
75
86
  export async function resolveSkillEntryPath(manifest) {
76
87
  assertSafeRelativeSkillPath(manifest.entry, "entry");
77
88
  const logicalDir = path.resolve(manifest.dir);
@@ -23,6 +23,7 @@ export interface DiscordMessage {
23
23
  author: DiscordUser;
24
24
  content: string;
25
25
  mentions: DiscordUser[];
26
+ mention_roles?: string[];
26
27
  attachments?: DiscordAttachment[];
27
28
  }
28
29
  export interface DiscordAttachment {
@@ -3,10 +3,11 @@ import path from "node:path";
3
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { loadConfig, loadModels } from "../core/config.js";
5
5
  import { appendEventLog } from "../core/logger.js";
6
- import { createIntentRuntime, renderBuildFooter, shouldShowBuildFooter, } from "../builder/build-flow.js";
6
+ import { createIntentRuntime, enterEditModeForSkill, parseSlashBuildDirect, renderBuildFooter, shouldShowBuildFooter, } from "../builder/build-flow.js";
7
7
  import { routeConversationMessage, } from "../builder/conversation-router.js";
8
8
  import { formatBuildProgress, progressIntervalMs } from "../builder/progress-format.js";
9
9
  import { chunkText, cleanAttachmentText, formatAttachmentLabel, formatBytes, guessImageMimeType as guessImageMimeTypeFromName, indentAttachmentContent, isImageLikeFile, isTextLikeFile, } from "../core/message-utils.js";
10
+ import { collectLocalFileAttachments, collectLocalImageAttachments, } from "../core/image-attachments.js";
10
11
  import { isEmptyIntentRuntime, loadIntentRuntimeMap, saveIntentRuntimeMap, } from "../builder/intent-runtime-store.js";
11
12
  import { inferLocaleFromText, l, lLines, withLocale } from "../core/i18n.js";
12
13
  import { modelsLines, skillsLines } from "../core/info-lines.js";
@@ -106,6 +107,7 @@ export async function runDiscordBot(config) {
106
107
  historiesLoaded: new Set(),
107
108
  intentRuntimes: persistedIntentRuntimes,
108
109
  intentRuntimesFile,
110
+ buildPickPending: new Map(),
109
111
  ws: null,
110
112
  seq: null,
111
113
  sessionId: null,
@@ -173,11 +175,12 @@ export function parseSnowflakeList(raw) {
173
175
  export const parseAllowedUserIds = parseSnowflakeList;
174
176
  export function classifyMessage(message, botUserId, allowedUserIds, listenChannelIds = new Set(), botThreadIds = new Set()) {
175
177
  const isDirect = !message.guild_id;
176
- const isMentioned = botUserId
178
+ const isListenChannel = listenChannelIds.has(message.channel_id);
179
+ const hasBotUserMention = botUserId
177
180
  ? Array.isArray(message.mentions) && message.mentions.some((user) => user.id === botUserId)
178
181
  : false;
182
+ const isMentioned = hasBotUserMention || (isListenChannel && hasLeadingRoleMention(message));
179
183
  const isAllowed = allowedUserIds.has(message.author.id);
180
- const isListenChannel = listenChannelIds.has(message.channel_id);
181
184
  const isBotThread = botThreadIds.has(message.channel_id);
182
185
  return { isDirect, isMentioned, isAllowed, isListenChannel, isBotThread };
183
186
  }
@@ -186,7 +189,13 @@ export function stripBotMention(content, botUserId) {
186
189
  return content.trim();
187
190
  }
188
191
  const pattern = new RegExp(`<@!?${botUserId}>`, "g");
189
- return content.replace(pattern, "").trim();
192
+ return stripLeadingRoleMentions(content.replace(pattern, "")).trim();
193
+ }
194
+ function hasLeadingRoleMention(message) {
195
+ return /^\s*(?:<@&\d+>\s*)+/u.test(message.content || "");
196
+ }
197
+ function stripLeadingRoleMentions(content) {
198
+ return content.replace(/^\s*(?:<@&\d+>\s*)+/u, "");
190
199
  }
191
200
  const BADGE_PREFIX_PATTERN = /^(💬|🔧|✏️|✏)/u;
192
201
  const BUILD_FOOTER_FIRST_LINE = /^(?:(?:✏️|✏|🔧)\s*現在ビルドモードです|((?:現在:「[^」]*」の)?ビルドモード(?:です。抜けるには|を抜けるには).+?と返事してください))\s*$/u;
@@ -453,7 +462,7 @@ async function persistDiscordAttachmentBuffer(state, attachment, buffer, mimeTyp
453
462
  const HH = String(now.getHours()).padStart(2, "0");
454
463
  const mm = String(now.getMinutes()).padStart(2, "0");
455
464
  const ss = String(now.getSeconds()).padStart(2, "0");
456
- const dir = path.join(state.config.notes_dir, "attachments", yyyy, MM);
465
+ const dir = path.join(state.config.memory_dir, "attachments", yyyy, MM);
457
466
  await mkdir(dir, { recursive: true });
458
467
  const ext = pickAttachmentExtension(attachment.filename || attachment.title, mimeType);
459
468
  const random = Math.random().toString(36).slice(2, 8);
@@ -726,9 +735,11 @@ async function handleMessage(state, message) {
726
735
  is_listen_channel: ctx.isListenChannel,
727
736
  is_bot_thread: ctx.isBotThread,
728
737
  mention_count: Array.isArray(message.mentions) ? message.mentions.length : 0,
738
+ role_mention_count: Array.isArray(message.mention_roles) ? message.mention_roles.length : 0,
729
739
  mention_user_ids: Array.isArray(message.mentions)
730
740
  ? message.mentions.map((user) => user.id).slice(0, 5)
731
741
  : [],
742
+ mention_role_ids: Array.isArray(message.mention_roles) ? message.mention_roles.slice(0, 5) : [],
732
743
  bot_user_id: state.botUserId,
733
744
  },
734
745
  });
@@ -745,6 +756,7 @@ async function handleMessage(state, message) {
745
756
  if (cleanText === "!reset") {
746
757
  state.histories.delete(message.channel_id);
747
758
  state.historiesLoaded.delete(message.channel_id);
759
+ state.buildPickPending.delete(message.channel_id);
748
760
  await sendChannelMessage(state, message.channel_id, l("Chat history reset.", "会話履歴をリセットしました。"));
749
761
  return;
750
762
  }
@@ -753,6 +765,25 @@ async function handleMessage(state, message) {
753
765
  await sendChannelMessage(state, message.channel_id, lines.join("\n"));
754
766
  return;
755
767
  }
768
+ if (cleanText === "/build") {
769
+ const lines = await startBuildPickerForChannel(state, message.channel_id);
770
+ await sendChannelMessage(state, message.channel_id, lines.join("\n"));
771
+ return;
772
+ }
773
+ const buildSlashSkillId = parseSlashBuildDirect(cleanText);
774
+ if (buildSlashSkillId) {
775
+ const lines = await enterBuildModeFromSlashForChannel(state, message.channel_id, buildSlashSkillId);
776
+ if (lines) {
777
+ await sendChannelMessage(state, message.channel_id, lines.join("\n"));
778
+ return;
779
+ }
780
+ }
781
+ const pendingPick = state.buildPickPending.get(message.channel_id);
782
+ if (pendingPick && /^\d+$/.test(cleanText.trim())) {
783
+ const lines = await resolveBuildPickSelection(state, message.channel_id, pendingPick, cleanText.trim());
784
+ await sendChannelMessage(state, message.channel_id, lines.join("\n"));
785
+ return;
786
+ }
756
787
  if (cleanText === "/models") {
757
788
  const lines = await modelsLines(state.config);
758
789
  await sendChannelMessage(state, message.channel_id, lines.join("\n"));
@@ -838,20 +869,20 @@ async function handleMessage(state, message) {
838
869
  const typing = startTypingKeepalive(state, replyChannelId);
839
870
  const prevMode = intentRuntime.mode;
840
871
  try {
841
- const lines = await routeDiscordMessage(state, userText, history, intentRuntime, status, replyChannelId, userMessage.images);
872
+ const routed = await routeDiscordMessage(state, userText, history, intentRuntime, status, replyChannelId, userMessage.images);
842
873
  typing.stop();
843
874
  void saveIntentRuntimes(state);
844
875
  const isBuildEntry = prevMode !== "build" && intentRuntime.mode === "build";
845
- const decorated = withModeBadge(intentRuntime, lines, { userText, isBuildEntry });
876
+ const decorated = withModeBadge(intentRuntime, routed.lines, { userText, isBuildEntry });
846
877
  scheduleUpdateCheck(state.config.workspace);
847
878
  const banner = await consumeUpdateBanner(state.config.workspace);
848
879
  const finalLines = banner ? [banner, "", ...decorated] : decorated;
849
880
  const reply = finalLines.filter((line) => line !== undefined && line !== null).join("\n").trim();
850
881
  if (reply) {
851
- await sendChannelMessage(state, replyChannelId, reply);
882
+ await sendChannelMessageWithLocalAttachments(state, replyChannelId, reply, routed.localAttachmentPaths);
852
883
  }
853
884
  else {
854
- await sendChannelMessage(state, replyChannelId, l("(no response)", "(応答なし)"));
885
+ await sendChannelMessageWithLocalAttachments(state, replyChannelId, l("(no response)", "(応答なし)"), routed.localAttachmentPaths);
855
886
  }
856
887
  }
857
888
  catch (error) {
@@ -900,6 +931,7 @@ async function refreshDiscordHistoryBeforeCurrentMessage(state, channelId, befor
900
931
  }
901
932
  async function routeDiscordMessage(state, text, history, intentRuntime, status, replyChannelId, images = []) {
902
933
  let modelFailed = false;
934
+ const localAttachmentPaths = [];
903
935
  const lines = await routeConversationMessage({
904
936
  config: state.config,
905
937
  text,
@@ -916,9 +948,12 @@ async function routeDiscordMessage(state, text, history, intentRuntime, status,
916
948
  modelFailed = true;
917
949
  }
918
950
  },
951
+ onLocalAttachments: (paths) => {
952
+ localAttachmentPaths.push(...paths);
953
+ },
919
954
  });
920
955
  await status.set(modelFailed ? "error" : "done");
921
- return lines;
956
+ return { lines, localAttachmentPaths };
922
957
  }
923
958
  async function onChatProgressForReactions(event, status) {
924
959
  switch (event.kind) {
@@ -2431,6 +2466,72 @@ async function editInteractionOriginal(state, interaction, content) {
2431
2466
  }
2432
2467
  }
2433
2468
  }
2469
+ async function enterBuildModeFromSlashForChannel(state, channelId, skillId) {
2470
+ if (!skillId)
2471
+ return null;
2472
+ const skills = await listSkillManifests(state.config.skills_dir);
2473
+ const userSkills = skills.filter((skill) => skill.source !== "builtin");
2474
+ const exists = userSkills.some((skill) => skill.id === skillId);
2475
+ if (!exists) {
2476
+ const head = l(`"${skillId}" is not a registered user skill. Pick one below:`, `「${skillId}」は登録済みのユーザースキルではありません。下から番号で選んでください:`);
2477
+ const picker = await startBuildPickerForChannel(state, channelId);
2478
+ return [head, ...picker];
2479
+ }
2480
+ state.buildPickPending.delete(channelId);
2481
+ let intentRuntime = state.intentRuntimes.get(channelId);
2482
+ if (!intentRuntime) {
2483
+ intentRuntime = createIntentRuntime(true);
2484
+ state.intentRuntimes.set(channelId, intentRuntime);
2485
+ }
2486
+ const lines = await enterEditModeForSkill(state.config, skillId, intentRuntime, "discord");
2487
+ if (isEmptyIntentRuntime(intentRuntime)) {
2488
+ state.intentRuntimes.delete(channelId);
2489
+ }
2490
+ else {
2491
+ state.intentRuntimes.set(channelId, intentRuntime);
2492
+ }
2493
+ void saveIntentRuntimes(state);
2494
+ return lines;
2495
+ }
2496
+ async function startBuildPickerForChannel(state, channelId) {
2497
+ const skills = await listSkillManifests(state.config.skills_dir);
2498
+ const userSkills = skills.filter((skill) => skill.source !== "builtin");
2499
+ if (userSkills.length === 0) {
2500
+ state.buildPickPending.delete(channelId);
2501
+ return [
2502
+ l("No user-created skills yet. Ask in chat to create one.", "編集できるスキルはまだありません。チャットで新しく作成を依頼してください。"),
2503
+ ];
2504
+ }
2505
+ state.buildPickPending.set(channelId, { skillIds: userSkills.map((skill) => skill.id) });
2506
+ const header = l("Pick a skill to edit. Reply with a number:", "編集するスキルを番号で返信してください:");
2507
+ const rows = userSkills.map((skill, index) => {
2508
+ const enabled = skill.enabled === false ? l("disabled", "無効") : l("enabled", "有効");
2509
+ return ` ${String(index + 1).padStart(2)}) ${skill.id} — ${skill.name} (${enabled})`;
2510
+ });
2511
+ return [header, ...rows];
2512
+ }
2513
+ async function resolveBuildPickSelection(state, channelId, pending, pickText) {
2514
+ const num = Number.parseInt(pickText, 10);
2515
+ if (!Number.isInteger(num) || num < 1 || num > pending.skillIds.length) {
2516
+ return [l("Invalid selection. Send `/build` again to redisplay the list.", "無効な選択です。もう一度 `/build` を送ってください。")];
2517
+ }
2518
+ state.buildPickPending.delete(channelId);
2519
+ const skillId = pending.skillIds[num - 1];
2520
+ let intentRuntime = state.intentRuntimes.get(channelId);
2521
+ if (!intentRuntime) {
2522
+ intentRuntime = createIntentRuntime(true);
2523
+ state.intentRuntimes.set(channelId, intentRuntime);
2524
+ }
2525
+ const lines = await enterEditModeForSkill(state.config, skillId, intentRuntime, "discord");
2526
+ if (isEmptyIntentRuntime(intentRuntime)) {
2527
+ state.intentRuntimes.delete(channelId);
2528
+ }
2529
+ else {
2530
+ state.intentRuntimes.set(channelId, intentRuntime);
2531
+ }
2532
+ void saveIntentRuntimes(state);
2533
+ return lines;
2534
+ }
2434
2535
  function handleProgressCommand(state, channelId, text) {
2435
2536
  if (text !== "!progress" && !text.startsWith("!progress ")) {
2436
2537
  return null;
@@ -2535,6 +2636,76 @@ async function sendChannelMessage(state, channelId, content) {
2535
2636
  }
2536
2637
  }
2537
2638
  }
2639
+ async function sendChannelMessageWithLocalAttachments(state, channelId, content, localAttachmentPaths = []) {
2640
+ const explicitAttachments = await collectLocalFileAttachments({
2641
+ paths: localAttachmentPaths,
2642
+ cwd: state.config.workspace,
2643
+ allowedRoots: [state.config.workspace, state.config.notes_dir],
2644
+ });
2645
+ const inlineImages = await collectLocalImageAttachments({
2646
+ text: content,
2647
+ cwd: state.config.workspace,
2648
+ allowedRoots: [state.config.workspace, state.config.notes_dir],
2649
+ });
2650
+ const attachments = dedupeLocalAttachments([...explicitAttachments, ...inlineImages]);
2651
+ if (content.trim()) {
2652
+ await sendChannelMessage(state, channelId, content);
2653
+ }
2654
+ for (const attachment of attachments) {
2655
+ await sendDiscordFileAttachment(state, channelId, attachment);
2656
+ }
2657
+ }
2658
+ async function sendDiscordFileAttachment(state, channelId, attachment) {
2659
+ try {
2660
+ const buffer = await readFile(attachment.path);
2661
+ const form = new FormData();
2662
+ form.append("payload_json", JSON.stringify({ content: "" }));
2663
+ form.append("files[0]", blobFromBuffer(buffer, attachment.mimeType), attachment.filename);
2664
+ const response = await fetch(`${REST_BASE}/channels/${channelId}/messages`, {
2665
+ method: "POST",
2666
+ headers: { authorization: `Bot ${state.token}` },
2667
+ body: form,
2668
+ });
2669
+ if (!response.ok) {
2670
+ const detail = await response.text().catch(() => "");
2671
+ console.error(`agent-sin discord: file send failed: HTTP ${response.status}: ${detail.slice(0, 200)}`);
2672
+ await appendEventLog(state.config, {
2673
+ level: "error",
2674
+ source: "discord",
2675
+ event: "file_send_failed",
2676
+ message: `HTTP ${response.status}`,
2677
+ details: { channel_id: channelId, path: attachment.path },
2678
+ });
2679
+ }
2680
+ }
2681
+ catch (error) {
2682
+ const message = error instanceof Error ? error.message : String(error);
2683
+ console.error(`agent-sin discord: file send error: ${message}`);
2684
+ await appendEventLog(state.config, {
2685
+ level: "error",
2686
+ source: "discord",
2687
+ event: "file_send_error",
2688
+ message,
2689
+ details: { channel_id: channelId, path: attachment.path },
2690
+ });
2691
+ }
2692
+ }
2693
+ function dedupeLocalAttachments(attachments) {
2694
+ const seen = new Set();
2695
+ const out = [];
2696
+ for (const attachment of attachments) {
2697
+ if (seen.has(attachment.path))
2698
+ continue;
2699
+ seen.add(attachment.path);
2700
+ out.push(attachment);
2701
+ }
2702
+ return out;
2703
+ }
2704
+ function blobFromBuffer(buffer, type) {
2705
+ const bytes = new Uint8Array(buffer.length);
2706
+ bytes.set(buffer);
2707
+ return new Blob([bytes.buffer], { type });
2708
+ }
2538
2709
  async function sendTypingIndicator(state, channelId) {
2539
2710
  try {
2540
2711
  await fetch(`${REST_BASE}/channels/${channelId}/typing`, {
@@ -0,0 +1,15 @@
1
+ import type { AppConfig } from "../core/config.js";
2
+ export interface EvenG2GatewayOptions {
3
+ host?: string;
4
+ port?: number;
5
+ token?: string;
6
+ historyChannel?: string;
7
+ }
8
+ export interface EvenG2GatewayHandle {
9
+ host: string;
10
+ port: number;
11
+ url: string;
12
+ close(): Promise<void>;
13
+ }
14
+ export declare function startEvenG2Gateway(config: AppConfig, options?: EvenG2GatewayOptions): Promise<EvenG2GatewayHandle>;
15
+ export declare function runEvenG2Gateway(config: AppConfig, options?: EvenG2GatewayOptions): Promise<number>;