agent-sin 0.1.11 → 0.1.15

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 +179 -0
  11. package/builtin-skills/memo-list/skill.yaml +51 -0
  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 +10 -3
  42. package/dist/core/chat-engine.js +1563 -197
  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 +568 -14
  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 +13 -0
  73. package/dist/discord/bot.js +542 -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 +122 -31
  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
@@ -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"));
@@ -761,6 +792,9 @@ async function handleMessage(state, message) {
761
792
  if (await tryRunTodoSlashCommand(state, message, cleanText)) {
762
793
  return;
763
794
  }
795
+ if (await tryRunMemoSlashCommand(state, message, cleanText)) {
796
+ return;
797
+ }
764
798
  if (await tryRunModelSlashCommand(state, message, cleanText)) {
765
799
  return;
766
800
  }
@@ -835,20 +869,20 @@ async function handleMessage(state, message) {
835
869
  const typing = startTypingKeepalive(state, replyChannelId);
836
870
  const prevMode = intentRuntime.mode;
837
871
  try {
838
- 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);
839
873
  typing.stop();
840
874
  void saveIntentRuntimes(state);
841
875
  const isBuildEntry = prevMode !== "build" && intentRuntime.mode === "build";
842
- const decorated = withModeBadge(intentRuntime, lines, { userText, isBuildEntry });
876
+ const decorated = withModeBadge(intentRuntime, routed.lines, { userText, isBuildEntry });
843
877
  scheduleUpdateCheck(state.config.workspace);
844
878
  const banner = await consumeUpdateBanner(state.config.workspace);
845
879
  const finalLines = banner ? [banner, "", ...decorated] : decorated;
846
880
  const reply = finalLines.filter((line) => line !== undefined && line !== null).join("\n").trim();
847
881
  if (reply) {
848
- await sendChannelMessage(state, replyChannelId, reply);
882
+ await sendChannelMessageWithLocalAttachments(state, replyChannelId, reply, routed.localAttachmentPaths);
849
883
  }
850
884
  else {
851
- await sendChannelMessage(state, replyChannelId, l("(no response)", "(応答なし)"));
885
+ await sendChannelMessageWithLocalAttachments(state, replyChannelId, l("(no response)", "(応答なし)"), routed.localAttachmentPaths);
852
886
  }
853
887
  }
854
888
  catch (error) {
@@ -897,6 +931,7 @@ async function refreshDiscordHistoryBeforeCurrentMessage(state, channelId, befor
897
931
  }
898
932
  async function routeDiscordMessage(state, text, history, intentRuntime, status, replyChannelId, images = []) {
899
933
  let modelFailed = false;
934
+ const localAttachmentPaths = [];
900
935
  const lines = await routeConversationMessage({
901
936
  config: state.config,
902
937
  text,
@@ -913,9 +948,12 @@ async function routeDiscordMessage(state, text, history, intentRuntime, status,
913
948
  modelFailed = true;
914
949
  }
915
950
  },
951
+ onLocalAttachments: (paths) => {
952
+ localAttachmentPaths.push(...paths);
953
+ },
916
954
  });
917
955
  await status.set(modelFailed ? "error" : "done");
918
- return lines;
956
+ return { lines, localAttachmentPaths };
919
957
  }
920
958
  async function onChatProgressForReactions(event, status) {
921
959
  switch (event.kind) {
@@ -1224,6 +1262,179 @@ async function tryRunTodoSlashCommand(state, message, cleanText) {
1224
1262
  });
1225
1263
  return true;
1226
1264
  }
1265
+ export function parseMemoSlashCommand(text) {
1266
+ const trimmed = (text || "").trim();
1267
+ if (trimmed !== "/memo" && !/^\/memo\s/.test(trimmed)) {
1268
+ return null;
1269
+ }
1270
+ const rest = trimmed === "/memo" ? "" : trimmed.replace(/^\/memo\s+/, "").trim();
1271
+ if (!rest || rest === "help" || rest === "--help" || rest === "-h") {
1272
+ return { kind: "help", lines: memoSlashHelpLines() };
1273
+ }
1274
+ const firstSpace = rest.search(/\s/);
1275
+ const sub = (firstSpace >= 0 ? rest.slice(0, firstSpace) : rest).toLowerCase();
1276
+ const remainder = firstSpace >= 0 ? rest.slice(firstSpace + 1).trim() : "";
1277
+ if (sub === "add" || sub === "save") {
1278
+ if (!remainder) {
1279
+ return { kind: "error", lines: [l("Usage: /memo add <text>", "使い方: /memo add <本文>")] };
1280
+ }
1281
+ return { kind: "run", skillId: "memo-save", args: { text: remainder } };
1282
+ }
1283
+ if (sub === "list" || sub === "ls") {
1284
+ const parsed = parseMemoListArgs(remainder);
1285
+ if (parsed.kind === "error")
1286
+ return parsed;
1287
+ return { kind: "run", skillId: "memo-list", args: parsed.args };
1288
+ }
1289
+ if (sub === "delete" || sub === "remove" || sub === "del") {
1290
+ const parsed = parseMemoDeleteArgs(remainder);
1291
+ if (parsed.kind === "error")
1292
+ return parsed;
1293
+ return { kind: "run", skillId: "memo-delete", args: parsed.args };
1294
+ }
1295
+ return {
1296
+ kind: "error",
1297
+ lines: [
1298
+ l(`Unknown subcommand: ${sub}.`, `未対応のサブコマンドです: ${sub}。`),
1299
+ ...memoSlashHelpLines(),
1300
+ ],
1301
+ };
1302
+ }
1303
+ function parseMemoListArgs(rest) {
1304
+ let body = rest.trim();
1305
+ const dateFlag = extractValueFlag(body, "date");
1306
+ body = dateFlag.text;
1307
+ const limitFlag = extractValueFlag(body, "limit");
1308
+ body = limitFlag.text;
1309
+ const args = {};
1310
+ const bodyParts = body.split(/\s+/).filter(Boolean);
1311
+ if (dateFlag.value) {
1312
+ args.date = dateFlag.value;
1313
+ }
1314
+ else if (bodyParts.length > 0) {
1315
+ args.date = bodyParts[0];
1316
+ bodyParts.shift();
1317
+ }
1318
+ if (bodyParts.length > 0) {
1319
+ return { kind: "error", lines: [l("Usage: /memo list [YYYY-MM-DD] [--limit 20]", "使い方: /memo list [YYYY-MM-DD] [--limit 20]")] };
1320
+ }
1321
+ if (limitFlag.value) {
1322
+ const limit = parseIntegerOption(limitFlag.value);
1323
+ if (!limit || limit < 1 || limit > 50) {
1324
+ return { kind: "error", lines: [l("limit must be an integer from 1 to 50.", "limit は 1〜50 の整数で指定してください。")] };
1325
+ }
1326
+ args.limit = limit;
1327
+ }
1328
+ return { kind: "ok", args };
1329
+ }
1330
+ function parseMemoDeleteArgs(rest) {
1331
+ let target = rest.trim();
1332
+ const dateFlag = extractValueFlag(target, "date");
1333
+ target = dateFlag.text.trim();
1334
+ const args = {};
1335
+ if (dateFlag.value)
1336
+ args.date = dateFlag.value;
1337
+ if (!target) {
1338
+ return { kind: "error", lines: [l("Usage: /memo delete <index|text> [--date YYYY-MM-DD]", "使い方: /memo delete <番号|本文> [--date YYYY-MM-DD]")] };
1339
+ }
1340
+ const numeric = parseIntegerOption(target);
1341
+ if (numeric && String(numeric) === target) {
1342
+ args.index = numeric;
1343
+ }
1344
+ else {
1345
+ args.match = target;
1346
+ }
1347
+ return { kind: "ok", args };
1348
+ }
1349
+ function extractValueFlag(rest, name) {
1350
+ const pattern = new RegExp(`(^|\\s)--${escapeRegExp(name)}\\s+(\\S+)(?=\\s|$)`);
1351
+ const match = rest.match(pattern);
1352
+ if (!match)
1353
+ return { text: rest.trim() };
1354
+ const start = match.index ?? 0;
1355
+ const before = rest.slice(0, start);
1356
+ const after = rest.slice(start + match[0].length);
1357
+ const text = [before, after].map((segment) => segment.trim()).filter(Boolean).join(" ").trim();
1358
+ return { text, value: match[2] };
1359
+ }
1360
+ function parseIntegerOption(value) {
1361
+ if (!/^\d+$/.test(value.trim()))
1362
+ return null;
1363
+ const parsed = Number.parseInt(value, 10);
1364
+ return Number.isSafeInteger(parsed) ? parsed : null;
1365
+ }
1366
+ function escapeRegExp(value) {
1367
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1368
+ }
1369
+ function memoSlashHelpLines() {
1370
+ return lLines([
1371
+ "Memo commands:",
1372
+ "/memo add <text> - save a memo",
1373
+ "/memo list [YYYY-MM-DD] [--limit 20] - list memos",
1374
+ "/memo delete <index|text> [--date YYYY-MM-DD] - delete a memo",
1375
+ ], [
1376
+ "メモのショートカット:",
1377
+ "/memo add <本文> - メモを追加",
1378
+ "/memo list [YYYY-MM-DD] [--limit 20] - メモを一覧表示",
1379
+ "/memo delete <番号|本文> [--date YYYY-MM-DD] - メモを削除",
1380
+ ]);
1381
+ }
1382
+ async function tryRunMemoSlashCommand(state, message, cleanText) {
1383
+ const parsed = await withLocale(inferLocaleFromText(cleanText), () => Promise.resolve(parseMemoSlashCommand(cleanText)));
1384
+ if (!parsed)
1385
+ return false;
1386
+ await withLocale(inferLocaleFromText(cleanText), async () => {
1387
+ const status = createStatusReactor(state, message.channel_id, message.id);
1388
+ await status.set("received");
1389
+ if (parsed.kind === "help") {
1390
+ await status.set("done");
1391
+ await sendChannelMessage(state, message.channel_id, parsed.lines.join("\n"));
1392
+ return;
1393
+ }
1394
+ if (parsed.kind === "error") {
1395
+ await status.set("error");
1396
+ await sendChannelMessage(state, message.channel_id, parsed.lines.join("\n"));
1397
+ return;
1398
+ }
1399
+ await status.set("tool");
1400
+ try {
1401
+ const response = await runSkill(state.config, parsed.skillId, parsed.args);
1402
+ const display = response.result.summary ||
1403
+ response.result.title ||
1404
+ l("(no response)", "(応答なし)");
1405
+ await status.set(response.result.status === "ok" ? "done" : "error");
1406
+ await sendChannelMessage(state, message.channel_id, display);
1407
+ await appendEventLog(state.config, {
1408
+ level: "info",
1409
+ source: "discord",
1410
+ event: "memo_slash_ran",
1411
+ message: response.result.title || undefined,
1412
+ details: {
1413
+ skill_id: parsed.skillId,
1414
+ status: response.result.status,
1415
+ channel_id: message.channel_id,
1416
+ },
1417
+ });
1418
+ }
1419
+ catch (error) {
1420
+ await status.set("error");
1421
+ const detail = error instanceof SkillRunError
1422
+ ? error.originalMessage
1423
+ : error instanceof Error
1424
+ ? error.message
1425
+ : String(error);
1426
+ await sendChannelMessage(state, message.channel_id, l(`Error: ${detail}`, `エラー: ${detail}`));
1427
+ await appendEventLog(state.config, {
1428
+ level: "error",
1429
+ source: "discord",
1430
+ event: "memo_slash_failed",
1431
+ message: detail.slice(0, 200),
1432
+ details: { skill_id: parsed.skillId, channel_id: message.channel_id },
1433
+ });
1434
+ }
1435
+ });
1436
+ return true;
1437
+ }
1227
1438
  export function parseModelSlashCommand(text) {
1228
1439
  const trimmed = (text || "").trim();
1229
1440
  if (trimmed !== "/model" && !/^\/model\s/.test(trimmed)) {
@@ -1428,6 +1639,84 @@ const TODO_SLASH_COMMAND_DEFINITION = {
1428
1639
  },
1429
1640
  ],
1430
1641
  };
1642
+ const MEMO_SLASH_COMMAND_DEFINITION = {
1643
+ name: "memo",
1644
+ description: "Memo shortcut commands",
1645
+ description_localizations: { ja: "メモのショートカット" },
1646
+ type: 1,
1647
+ dm_permission: true,
1648
+ options: [
1649
+ {
1650
+ type: 1,
1651
+ name: "add",
1652
+ description: "Save a memo",
1653
+ description_localizations: { ja: "メモを追加" },
1654
+ options: [
1655
+ {
1656
+ type: 3,
1657
+ name: "text",
1658
+ description: "Memo text",
1659
+ description_localizations: { ja: "メモ本文" },
1660
+ required: true,
1661
+ },
1662
+ ],
1663
+ },
1664
+ {
1665
+ type: 1,
1666
+ name: "list",
1667
+ description: "List memos",
1668
+ description_localizations: { ja: "メモを一覧表示" },
1669
+ options: [
1670
+ {
1671
+ type: 3,
1672
+ name: "date",
1673
+ description: "Target date (YYYY-MM-DD, default today)",
1674
+ description_localizations: { ja: "対象日(YYYY-MM-DD、省略時は今日)" },
1675
+ required: false,
1676
+ },
1677
+ {
1678
+ type: 4,
1679
+ name: "limit",
1680
+ description: "Maximum number of memos",
1681
+ description_localizations: { ja: "表示する最大件数" },
1682
+ required: false,
1683
+ min_value: 1,
1684
+ max_value: 50,
1685
+ },
1686
+ ],
1687
+ },
1688
+ {
1689
+ type: 1,
1690
+ name: "delete",
1691
+ description: "Delete a memo",
1692
+ description_localizations: { ja: "メモを削除" },
1693
+ options: [
1694
+ {
1695
+ type: 4,
1696
+ name: "index",
1697
+ description: "Memo number from /memo list",
1698
+ description_localizations: { ja: "/memo list の番号" },
1699
+ required: false,
1700
+ min_value: 1,
1701
+ },
1702
+ {
1703
+ type: 3,
1704
+ name: "match",
1705
+ description: "Text contained in the memo",
1706
+ description_localizations: { ja: "メモに含まれる文字" },
1707
+ required: false,
1708
+ },
1709
+ {
1710
+ type: 3,
1711
+ name: "date",
1712
+ description: "Target date (YYYY-MM-DD, default today)",
1713
+ description_localizations: { ja: "対象日(YYYY-MM-DD、省略時は今日)" },
1714
+ required: false,
1715
+ },
1716
+ ],
1717
+ },
1718
+ ],
1719
+ };
1431
1720
  const MODEL_SLASH_COMMAND_DEFINITION = {
1432
1721
  name: "model",
1433
1722
  description: "Show or switch the chat model",
@@ -1447,6 +1736,7 @@ const MODEL_SLASH_COMMAND_DEFINITION = {
1447
1736
  };
1448
1737
  const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
1449
1738
  TODO_SLASH_COMMAND_DEFINITION,
1739
+ MEMO_SLASH_COMMAND_DEFINITION,
1450
1740
  MODEL_SLASH_COMMAND_DEFINITION,
1451
1741
  ];
1452
1742
  const DISCORD_SLASH_OPTION_TYPE_CODE = {
@@ -1590,6 +1880,12 @@ async function handleInteraction(state, interaction) {
1590
1880
  }
1591
1881
  return;
1592
1882
  }
1883
+ if (name === "memo") {
1884
+ if (interaction.type === 2) {
1885
+ await handleMemoInteraction(state, interaction);
1886
+ }
1887
+ return;
1888
+ }
1593
1889
  if (name === "model") {
1594
1890
  if (interaction.type === 4) {
1595
1891
  await handleModelAutocomplete(state, interaction);
@@ -1774,6 +2070,106 @@ async function handleTodoInteraction(state, interaction) {
1774
2070
  }
1775
2071
  });
1776
2072
  }
2073
+ async function handleMemoInteraction(state, interaction) {
2074
+ const userId = await ensureInteractionUserAllowed(state, interaction, "memo");
2075
+ if (!userId)
2076
+ return;
2077
+ const sub = interaction.data?.options?.[0];
2078
+ const subName = sub?.name || "";
2079
+ const optMap = new Map();
2080
+ for (const opt of sub?.options || []) {
2081
+ optMap.set(opt.name, opt.value);
2082
+ }
2083
+ await withLocale(inferLocaleFromText(extractInteractionLocaleHint(sub)), async () => {
2084
+ let skillId;
2085
+ let args;
2086
+ if (subName === "add") {
2087
+ const text = String(optMap.get("text") || "").trim();
2088
+ if (!text) {
2089
+ await respondInteraction(state, interaction, {
2090
+ content: l("Memo text is required.", "メモ本文を指定してください。"),
2091
+ ephemeral: true,
2092
+ });
2093
+ return;
2094
+ }
2095
+ skillId = "memo-save";
2096
+ args = { text };
2097
+ }
2098
+ else if (subName === "list") {
2099
+ skillId = "memo-list";
2100
+ args = {};
2101
+ const date = String(optMap.get("date") || "").trim();
2102
+ if (date)
2103
+ args.date = date;
2104
+ const limit = optMap.get("limit");
2105
+ if (typeof limit === "number")
2106
+ args.limit = limit;
2107
+ }
2108
+ else if (subName === "delete") {
2109
+ skillId = "memo-delete";
2110
+ args = {};
2111
+ const date = String(optMap.get("date") || "").trim();
2112
+ const match = String(optMap.get("match") || "").trim();
2113
+ const index = optMap.get("index");
2114
+ if (date)
2115
+ args.date = date;
2116
+ if (typeof index === "number")
2117
+ args.index = index;
2118
+ if (match)
2119
+ args.match = match;
2120
+ if (args.index === undefined && !args.match) {
2121
+ await respondInteraction(state, interaction, {
2122
+ content: l("Specify a memo number or matching text.", "番号か一致する本文を指定してください。"),
2123
+ ephemeral: true,
2124
+ });
2125
+ return;
2126
+ }
2127
+ }
2128
+ else {
2129
+ await respondInteraction(state, interaction, {
2130
+ content: l(`Unknown subcommand: ${subName || "(none)"}.`, `未対応のサブコマンドです: ${subName || "(なし)"}。`),
2131
+ ephemeral: true,
2132
+ });
2133
+ return;
2134
+ }
2135
+ const ack = await deferInteraction(state, interaction);
2136
+ if (!ack)
2137
+ return;
2138
+ try {
2139
+ const response = await runSkill(state.config, skillId, args);
2140
+ const display = response.result.summary ||
2141
+ response.result.title ||
2142
+ l("(no response)", "(応答なし)");
2143
+ await editInteractionOriginal(state, interaction, display);
2144
+ await appendEventLog(state.config, {
2145
+ level: "info",
2146
+ source: "discord",
2147
+ event: "memo_slash_ran",
2148
+ message: response.result.title || undefined,
2149
+ details: {
2150
+ skill_id: skillId,
2151
+ status: response.result.status,
2152
+ kind: "interaction",
2153
+ },
2154
+ });
2155
+ }
2156
+ catch (error) {
2157
+ const detail = error instanceof SkillRunError
2158
+ ? error.originalMessage
2159
+ : error instanceof Error
2160
+ ? error.message
2161
+ : String(error);
2162
+ await editInteractionOriginal(state, interaction, l(`Error: ${detail}`, `エラー: ${detail}`));
2163
+ await appendEventLog(state.config, {
2164
+ level: "error",
2165
+ source: "discord",
2166
+ event: "memo_slash_failed",
2167
+ message: detail.slice(0, 200),
2168
+ details: { skill_id: skillId, kind: "interaction" },
2169
+ });
2170
+ }
2171
+ });
2172
+ }
1777
2173
  async function handleModelInteraction(state, interaction) {
1778
2174
  const userId = await ensureInteractionUserAllowed(state, interaction, "model");
1779
2175
  if (!userId)
@@ -2070,6 +2466,72 @@ async function editInteractionOriginal(state, interaction, content) {
2070
2466
  }
2071
2467
  }
2072
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
+ }
2073
2535
  function handleProgressCommand(state, channelId, text) {
2074
2536
  if (text !== "!progress" && !text.startsWith("!progress ")) {
2075
2537
  return null;
@@ -2174,6 +2636,76 @@ async function sendChannelMessage(state, channelId, content) {
2174
2636
  }
2175
2637
  }
2176
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
+ }
2177
2709
  async function sendTypingIndicator(state, channelId) {
2178
2710
  try {
2179
2711
  await fetch(`${REST_BASE}/channels/${channelId}/typing`, {