agentcord 0.1.7 → 0.1.8

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.
@@ -69,11 +69,33 @@ function getCommandDefinitions() {
69
69
  { name: "General", value: "general" }
70
70
  ))).addSubcommand((sub) => sub.setName("list").setDescription("List available agent personas")).addSubcommand((sub) => sub.setName("clear").setDescription("Clear agent persona"));
71
71
  const project = new SlashCommandBuilder().setName("project").setDescription("Configure project settings").addSubcommand((sub) => sub.setName("personality").setDescription("Set a custom personality for this project").addStringOption((opt) => opt.setName("prompt").setDescription("System prompt for the project").setRequired(true))).addSubcommand((sub) => sub.setName("personality-show").setDescription("Show the current project personality")).addSubcommand((sub) => sub.setName("personality-clear").setDescription("Clear the project personality")).addSubcommand((sub) => sub.setName("skill-add").setDescription("Add a skill (prompt template) to this project").addStringOption((opt) => opt.setName("name").setDescription("Skill name").setRequired(true)).addStringOption((opt) => opt.setName("prompt").setDescription("Prompt template (use {input} for placeholder)").setRequired(true))).addSubcommand((sub) => sub.setName("skill-remove").setDescription("Remove a skill").addStringOption((opt) => opt.setName("name").setDescription("Skill name").setRequired(true))).addSubcommand((sub) => sub.setName("skill-list").setDescription("List all skills for this project")).addSubcommand((sub) => sub.setName("skill-run").setDescription("Execute a skill").addStringOption((opt) => opt.setName("name").setDescription("Skill name").setRequired(true)).addStringOption((opt) => opt.setName("input").setDescription("Input to pass to the skill template"))).addSubcommand((sub) => sub.setName("mcp-add").setDescription("Add an MCP server to this project").addStringOption((opt) => opt.setName("name").setDescription("Server name").setRequired(true)).addStringOption((opt) => opt.setName("command").setDescription("Command to run (e.g. npx my-mcp-server)").setRequired(true)).addStringOption((opt) => opt.setName("args").setDescription("Arguments (comma-separated)"))).addSubcommand((sub) => sub.setName("mcp-remove").setDescription("Remove an MCP server").addStringOption((opt) => opt.setName("name").setDescription("Server name").setRequired(true))).addSubcommand((sub) => sub.setName("mcp-list").setDescription("List configured MCP servers")).addSubcommand((sub) => sub.setName("info").setDescription("Show project configuration"));
72
+ const plugin = new SlashCommandBuilder().setName("plugin").setDescription("Manage Claude Code plugins").addSubcommand((sub) => sub.setName("browse").setDescription("Browse available plugins from marketplaces").addStringOption((opt) => opt.setName("search").setDescription("Filter by name or keyword"))).addSubcommand((sub) => sub.setName("install").setDescription("Install a plugin").addStringOption((opt) => opt.setName("plugin").setDescription("Plugin name (e.g. feature-dev@claude-plugins-official)").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("scope").setDescription("Installation scope (default: user)").addChoices(
73
+ { name: "User \u2014 available everywhere", value: "user" },
74
+ { name: "Project \u2014 this project only", value: "project" },
75
+ { name: "Local \u2014 this directory only", value: "local" }
76
+ ))).addSubcommand((sub) => sub.setName("remove").setDescription("Uninstall a plugin").addStringOption((opt) => opt.setName("plugin").setDescription("Plugin ID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("scope").setDescription("Scope to uninstall from (default: user)").addChoices(
77
+ { name: "User", value: "user" },
78
+ { name: "Project", value: "project" },
79
+ { name: "Local", value: "local" }
80
+ ))).addSubcommand((sub) => sub.setName("list").setDescription("List installed plugins")).addSubcommand((sub) => sub.setName("info").setDescription("Show detailed info for a plugin").addStringOption((opt) => opt.setName("plugin").setDescription("Plugin name or ID").setRequired(true).setAutocomplete(true))).addSubcommand((sub) => sub.setName("enable").setDescription("Enable a disabled plugin").addStringOption((opt) => opt.setName("plugin").setDescription("Plugin ID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("scope").setDescription("Scope (default: user)").addChoices(
81
+ { name: "User", value: "user" },
82
+ { name: "Project", value: "project" },
83
+ { name: "Local", value: "local" }
84
+ ))).addSubcommand((sub) => sub.setName("disable").setDescription("Disable a plugin").addStringOption((opt) => opt.setName("plugin").setDescription("Plugin ID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("scope").setDescription("Scope (default: user)").addChoices(
85
+ { name: "User", value: "user" },
86
+ { name: "Project", value: "project" },
87
+ { name: "Local", value: "local" }
88
+ ))).addSubcommand((sub) => sub.setName("update").setDescription("Update a plugin to latest version").addStringOption((opt) => opt.setName("plugin").setDescription("Plugin ID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("scope").setDescription("Scope (default: user)").addChoices(
89
+ { name: "User", value: "user" },
90
+ { name: "Project", value: "project" },
91
+ { name: "Local", value: "local" }
92
+ ))).addSubcommand((sub) => sub.setName("marketplace-add").setDescription("Add a plugin marketplace").addStringOption((opt) => opt.setName("source").setDescription("GitHub repo (owner/repo) or git URL").setRequired(true))).addSubcommand((sub) => sub.setName("marketplace-remove").setDescription("Remove a marketplace").addStringOption((opt) => opt.setName("name").setDescription("Marketplace name").setRequired(true).setAutocomplete(true))).addSubcommand((sub) => sub.setName("marketplace-list").setDescription("List registered marketplaces")).addSubcommand((sub) => sub.setName("marketplace-update").setDescription("Update marketplace catalogs").addStringOption((opt) => opt.setName("name").setDescription("Specific marketplace (or all if omitted)").setAutocomplete(true)));
72
93
  return [
73
94
  claude.toJSON(),
74
95
  shell.toJSON(),
75
96
  agent.toJSON(),
76
- project.toJSON()
97
+ project.toJSON(),
98
+ plugin.toJSON()
77
99
  ];
78
100
  }
79
101
  async function registerCommands() {
@@ -104,8 +126,8 @@ import {
104
126
  ChannelType
105
127
  } from "discord.js";
106
128
  import { readdirSync, statSync, createReadStream } from "fs";
107
- import { join as join3, basename } from "path";
108
- import { homedir as homedir2 } from "os";
129
+ import { join as join4, basename } from "path";
130
+ import { homedir as homedir3 } from "os";
109
131
  import { createInterface } from "readline";
110
132
 
111
133
  // src/session-manager.ts
@@ -245,6 +267,9 @@ async function saveProjects() {
245
267
  function getProject(name) {
246
268
  return projects[name];
247
269
  }
270
+ function getProjectByCategoryId(categoryId) {
271
+ return Object.values(projects).find((p) => p.categoryId === categoryId);
272
+ }
248
273
  function getOrCreateProject(name, directory, categoryId) {
249
274
  if (!projects[name]) {
250
275
  projects[name] = {
@@ -431,14 +456,19 @@ function detectNumberedOptions(text) {
431
456
  const options = [];
432
457
  const optionRegex = /^\s*(\d+)[.)]\s+(.+)$/;
433
458
  let firstOptionLine = -1;
459
+ let lastOptionLine = -1;
434
460
  for (let i = 0; i < lines.length; i++) {
435
461
  const match = lines[i].match(optionRegex);
436
462
  if (match) {
437
463
  if (firstOptionLine === -1) firstOptionLine = i;
464
+ lastOptionLine = i;
438
465
  options.push(match[2].trim());
439
466
  }
440
467
  }
441
468
  if (options.length < 2 || options.length > 6) return null;
469
+ if (options.some((o) => o.length > 80)) return null;
470
+ const linesAfter = lines.slice(lastOptionLine + 1).filter((l) => l.trim()).length;
471
+ if (linesAfter > 3) return null;
442
472
  const preamble = lines.slice(0, firstOptionLine).join(" ").toLowerCase();
443
473
  const hasQuestion = /\?\s*$/.test(preamble.trim()) || /\b(which|choose|select|pick|prefer|would you like|how would you|what approach|option)\b/.test(preamble);
444
474
  return hasQuestion ? options : null;
@@ -651,9 +681,23 @@ async function* sendPrompt(sessionId, prompt) {
651
681
  session.isGenerating = true;
652
682
  session.lastActivity = Date.now();
653
683
  const systemPrompt = buildSystemPrompt(session);
684
+ let queryPrompt;
685
+ if (typeof prompt === "string") {
686
+ queryPrompt = prompt;
687
+ } else {
688
+ const userMessage = {
689
+ type: "user",
690
+ message: { role: "user", content: prompt },
691
+ parent_tool_use_id: null,
692
+ session_id: ""
693
+ };
694
+ queryPrompt = (async function* () {
695
+ yield userMessage;
696
+ })();
697
+ }
654
698
  try {
655
699
  const stream = query({
656
- prompt,
700
+ prompt: queryPrompt,
657
701
  options: {
658
702
  cwd: session.directory,
659
703
  resume: session.claudeSessionId,
@@ -777,16 +821,158 @@ async function listTmuxSessions() {
777
821
  }
778
822
  }
779
823
 
824
+ // src/plugin-manager.ts
825
+ import { execFile as execFile2 } from "child_process";
826
+ import { readFile as readFile3 } from "fs/promises";
827
+ import { join as join3 } from "path";
828
+ import { homedir as homedir2 } from "os";
829
+ var PLUGINS_DIR = join3(homedir2(), ".claude", "plugins");
830
+ var MARKETPLACES_DIR = join3(PLUGINS_DIR, "marketplaces");
831
+ function runClaude(args, cwd) {
832
+ return new Promise((resolve2) => {
833
+ execFile2("claude", args, {
834
+ cwd: cwd || process.cwd(),
835
+ timeout: 12e4,
836
+ env: { ...process.env }
837
+ }, (error, stdout, stderr) => {
838
+ resolve2({
839
+ stdout: stdout || "",
840
+ stderr: stderr || "",
841
+ code: error ? error.code ?? 1 : 0
842
+ });
843
+ });
844
+ });
845
+ }
846
+ var CACHE_TTL_MS = 3e4;
847
+ var installedCache = null;
848
+ var availableCache = null;
849
+ var marketplaceCache = null;
850
+ function invalidateCache() {
851
+ installedCache = null;
852
+ availableCache = null;
853
+ marketplaceCache = null;
854
+ }
855
+ async function listInstalled() {
856
+ if (installedCache && Date.now() - installedCache.ts < CACHE_TTL_MS) {
857
+ return installedCache.data;
858
+ }
859
+ const result = await runClaude(["plugin", "list", "--json"]);
860
+ if (result.code !== 0) throw new Error(`Failed to list plugins: ${result.stderr}`);
861
+ const data = JSON.parse(result.stdout);
862
+ installedCache = { data, ts: Date.now() };
863
+ return data;
864
+ }
865
+ async function listAvailable() {
866
+ if (availableCache && Date.now() - availableCache.ts < CACHE_TTL_MS) {
867
+ return availableCache.data;
868
+ }
869
+ const result = await runClaude(["plugin", "list", "--available", "--json"]);
870
+ if (result.code !== 0) throw new Error(`Failed to list available plugins: ${result.stderr}`);
871
+ const data = JSON.parse(result.stdout);
872
+ availableCache = { data, ts: Date.now() };
873
+ return data;
874
+ }
875
+ async function installPlugin(pluginId, scope, cwd) {
876
+ const result = await runClaude(["plugin", "install", pluginId, "-s", scope], cwd);
877
+ invalidateCache();
878
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Install failed");
879
+ return result.stdout.trim() || "Plugin installed successfully.";
880
+ }
881
+ async function uninstallPlugin(pluginId, scope, cwd) {
882
+ const result = await runClaude(["plugin", "uninstall", pluginId, "-s", scope], cwd);
883
+ invalidateCache();
884
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Uninstall failed");
885
+ return result.stdout.trim() || "Plugin uninstalled successfully.";
886
+ }
887
+ async function enablePlugin(pluginId, scope, cwd) {
888
+ const result = await runClaude(["plugin", "enable", pluginId, "-s", scope], cwd);
889
+ invalidateCache();
890
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Enable failed");
891
+ return result.stdout.trim() || "Plugin enabled.";
892
+ }
893
+ async function disablePlugin(pluginId, scope, cwd) {
894
+ const result = await runClaude(["plugin", "disable", pluginId, "-s", scope], cwd);
895
+ invalidateCache();
896
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Disable failed");
897
+ return result.stdout.trim() || "Plugin disabled.";
898
+ }
899
+ async function updatePlugin(pluginId, scope, cwd) {
900
+ const result = await runClaude(["plugin", "update", pluginId, "-s", scope], cwd);
901
+ invalidateCache();
902
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Update failed");
903
+ return result.stdout.trim() || "Plugin updated.";
904
+ }
905
+ async function listMarketplaces() {
906
+ if (marketplaceCache && Date.now() - marketplaceCache.ts < CACHE_TTL_MS) {
907
+ return marketplaceCache.data;
908
+ }
909
+ const result = await runClaude(["plugin", "marketplace", "list", "--json"]);
910
+ if (result.code !== 0) throw new Error(`Failed to list marketplaces: ${result.stderr}`);
911
+ const data = JSON.parse(result.stdout);
912
+ marketplaceCache = { data, ts: Date.now() };
913
+ return data;
914
+ }
915
+ async function addMarketplace(source) {
916
+ const result = await runClaude(["plugin", "marketplace", "add", source]);
917
+ invalidateCache();
918
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Add marketplace failed");
919
+ return result.stdout.trim() || "Marketplace added.";
920
+ }
921
+ async function removeMarketplace(name) {
922
+ const result = await runClaude(["plugin", "marketplace", "remove", name]);
923
+ invalidateCache();
924
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Remove marketplace failed");
925
+ return result.stdout.trim() || "Marketplace removed.";
926
+ }
927
+ async function updateMarketplaces(name) {
928
+ const args = ["plugin", "marketplace", "update"];
929
+ if (name) args.push(name);
930
+ const result = await runClaude(args);
931
+ invalidateCache();
932
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Update marketplace failed");
933
+ return result.stdout.trim() || "Marketplace(s) updated.";
934
+ }
935
+ async function getPluginDetail(pluginName, marketplaceName) {
936
+ try {
937
+ const marketplacePath = join3(MARKETPLACES_DIR, marketplaceName, ".claude-plugin", "marketplace.json");
938
+ const raw = await readFile3(marketplacePath, "utf-8");
939
+ const catalog = JSON.parse(raw);
940
+ return catalog.plugins.find((p) => p.name === pluginName) || null;
941
+ } catch {
942
+ return null;
943
+ }
944
+ }
945
+
780
946
  // src/output-handler.ts
781
947
  import {
948
+ AttachmentBuilder,
782
949
  EmbedBuilder,
783
950
  ActionRowBuilder,
784
951
  ButtonBuilder,
785
952
  ButtonStyle,
786
953
  StringSelectMenuBuilder
787
954
  } from "discord.js";
955
+ import { existsSync as existsSync4 } from "fs";
788
956
  var expandableStore = /* @__PURE__ */ new Map();
789
957
  var expandCounter = 0;
958
+ var pendingAnswersStore = /* @__PURE__ */ new Map();
959
+ var questionCountStore = /* @__PURE__ */ new Map();
960
+ function setPendingAnswer(sessionId, questionIndex, answer) {
961
+ if (!pendingAnswersStore.has(sessionId)) {
962
+ pendingAnswersStore.set(sessionId, /* @__PURE__ */ new Map());
963
+ }
964
+ pendingAnswersStore.get(sessionId).set(questionIndex, answer);
965
+ }
966
+ function getPendingAnswers(sessionId) {
967
+ return pendingAnswersStore.get(sessionId);
968
+ }
969
+ function clearPendingAnswers(sessionId) {
970
+ pendingAnswersStore.delete(sessionId);
971
+ questionCountStore.delete(sessionId);
972
+ }
973
+ function getQuestionCount(sessionId) {
974
+ return questionCountStore.get(sessionId) || 0;
975
+ }
790
976
  setInterval(() => {
791
977
  const now = Date.now();
792
978
  const TTL = 10 * 60 * 1e3;
@@ -966,6 +1152,20 @@ var MessageStreamer = class {
966
1152
  }
967
1153
  }
968
1154
  };
1155
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"]);
1156
+ function extractImagePath(toolName, toolInput) {
1157
+ try {
1158
+ const data = JSON.parse(toolInput);
1159
+ if (toolName === "Write" || toolName === "Read") {
1160
+ const filePath = data.file_path;
1161
+ if (filePath && IMAGE_EXTENSIONS.has(filePath.slice(filePath.lastIndexOf(".")).toLowerCase())) {
1162
+ return filePath;
1163
+ }
1164
+ }
1165
+ } catch {
1166
+ }
1167
+ return null;
1168
+ }
969
1169
  var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
970
1170
  "AskUserQuestion",
971
1171
  "EnterPlanMode",
@@ -992,21 +1192,29 @@ function renderAskUserQuestion(toolInput, sessionId) {
992
1192
  const data = JSON.parse(toolInput);
993
1193
  const questions = data.questions;
994
1194
  if (!questions?.length) return null;
1195
+ const isMulti = questions.length > 1;
1196
+ if (isMulti) {
1197
+ clearPendingAnswers(sessionId);
1198
+ questionCountStore.set(sessionId, questions.length);
1199
+ }
995
1200
  const embeds = [];
996
1201
  const components = [];
997
- for (const q of questions) {
1202
+ const btnPrefix = isMulti ? "pick" : "answer";
1203
+ const selectPrefix = isMulti ? "pick-select" : "answer-select";
1204
+ for (let qi = 0; qi < questions.length; qi++) {
1205
+ const q = questions[qi];
998
1206
  const embed = new EmbedBuilder().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
999
1207
  if (q.options?.length) {
1000
1208
  if (q.options.length <= 4) {
1001
1209
  const row = new ActionRowBuilder();
1002
1210
  for (let i = 0; i < q.options.length; i++) {
1003
1211
  row.addComponents(
1004
- new ButtonBuilder().setCustomId(`answer:${sessionId}:${q.options[i].label}`).setLabel(q.options[i].label.slice(0, 80)).setStyle(i === 0 ? ButtonStyle.Primary : ButtonStyle.Secondary)
1212
+ new ButtonBuilder().setCustomId(`${btnPrefix}:${sessionId}:${qi}:${q.options[i].label}`).setLabel(q.options[i].label.slice(0, 80)).setStyle(i === 0 ? ButtonStyle.Primary : ButtonStyle.Secondary)
1005
1213
  );
1006
1214
  }
1007
1215
  components.push(row);
1008
1216
  } else {
1009
- const menu = new StringSelectMenuBuilder().setCustomId(`answer-select:${sessionId}`).setPlaceholder("Select an option...");
1217
+ const menu = new StringSelectMenuBuilder().setCustomId(`${selectPrefix}:${sessionId}:${qi}`).setPlaceholder("Select an option...");
1010
1218
  for (const opt of q.options) {
1011
1219
  menu.addOptions({
1012
1220
  label: opt.label.slice(0, 100),
@@ -1021,6 +1229,13 @@ function renderAskUserQuestion(toolInput, sessionId) {
1021
1229
  }
1022
1230
  embeds.push(embed);
1023
1231
  }
1232
+ if (isMulti) {
1233
+ components.push(
1234
+ new ActionRowBuilder().addComponents(
1235
+ new ButtonBuilder().setCustomId(`submit-answers:${sessionId}`).setLabel("Submit Answers").setStyle(ButtonStyle.Success)
1236
+ )
1237
+ );
1238
+ }
1024
1239
  return { embeds, components };
1025
1240
  } catch {
1026
1241
  return null;
@@ -1061,6 +1276,7 @@ async function handleOutputStream(stream, channel, sessionId, verbose = false, m
1061
1276
  let currentToolName = null;
1062
1277
  let currentToolInput = "";
1063
1278
  let lastFinishedToolName = null;
1279
+ let pendingImagePath = null;
1064
1280
  channel.sendTyping().catch(() => {
1065
1281
  });
1066
1282
  const typingInterval = setInterval(() => {
@@ -1122,6 +1338,7 @@ ${displayInput}
1122
1338
  await channel.send({ embeds: [embed], components });
1123
1339
  }
1124
1340
  }
1341
+ pendingImagePath = extractImagePath(currentToolName, currentToolInput);
1125
1342
  lastFinishedToolName = currentToolName;
1126
1343
  currentToolName = null;
1127
1344
  currentToolInput = "";
@@ -1129,6 +1346,17 @@ ${displayInput}
1129
1346
  }
1130
1347
  }
1131
1348
  if (message.type === "user") {
1349
+ if (pendingImagePath && existsSync4(pendingImagePath)) {
1350
+ try {
1351
+ await streamer.finalize();
1352
+ const attachment = new AttachmentBuilder(pendingImagePath);
1353
+ await channel.send({ files: [attachment] });
1354
+ } catch {
1355
+ }
1356
+ pendingImagePath = null;
1357
+ } else {
1358
+ pendingImagePath = null;
1359
+ }
1132
1360
  const showResult = verbose || lastFinishedToolName !== null && TASK_TOOLS.has(lastFinishedToolName);
1133
1361
  if (!showResult) continue;
1134
1362
  await streamer.finalize();
@@ -1382,7 +1610,15 @@ async function handleClaude(interaction) {
1382
1610
  }
1383
1611
  async function handleClaudeNew(interaction) {
1384
1612
  const name = interaction.options.getString("name", true);
1385
- const directory = interaction.options.getString("directory") || config.defaultDirectory;
1613
+ let directory = interaction.options.getString("directory");
1614
+ if (!directory) {
1615
+ const parentId = interaction.channel?.parentId;
1616
+ if (parentId) {
1617
+ const project = getProjectByCategoryId(parentId);
1618
+ if (project) directory = project.directory;
1619
+ }
1620
+ directory = directory || config.defaultDirectory;
1621
+ }
1386
1622
  await interaction.deferReply();
1387
1623
  let channel;
1388
1624
  try {
@@ -1424,7 +1660,7 @@ async function handleClaudeNew(interaction) {
1424
1660
  }
1425
1661
  }
1426
1662
  function discoverLocalSessions() {
1427
- const claudeDir = join3(homedir2(), ".claude", "projects");
1663
+ const claudeDir = join4(homedir3(), ".claude", "projects");
1428
1664
  const results = [];
1429
1665
  let projectDirs;
1430
1666
  try {
@@ -1433,7 +1669,7 @@ function discoverLocalSessions() {
1433
1669
  return [];
1434
1670
  }
1435
1671
  for (const projDir of projectDirs) {
1436
- const projPath = join3(claudeDir, projDir);
1672
+ const projPath = join4(claudeDir, projDir);
1437
1673
  let files;
1438
1674
  try {
1439
1675
  files = readdirSync(projPath);
@@ -1447,7 +1683,7 @@ function discoverLocalSessions() {
1447
1683
  const sessionId = file.replace(".jsonl", "");
1448
1684
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) continue;
1449
1685
  try {
1450
- const mtime = statSync(join3(projPath, file)).mtimeMs;
1686
+ const mtime = statSync(join4(projPath, file)).mtimeMs;
1451
1687
  results.push({ id: sessionId, project, mtime, firstMessage: "" });
1452
1688
  } catch {
1453
1689
  continue;
@@ -1458,7 +1694,7 @@ function discoverLocalSessions() {
1458
1694
  return results;
1459
1695
  }
1460
1696
  async function getFirstUserMessage(sessionId) {
1461
- const claudeDir = join3(homedir2(), ".claude", "projects");
1697
+ const claudeDir = join4(homedir3(), ".claude", "projects");
1462
1698
  let projectDirs;
1463
1699
  try {
1464
1700
  projectDirs = readdirSync(claudeDir);
@@ -1466,7 +1702,7 @@ async function getFirstUserMessage(sessionId) {
1466
1702
  return "";
1467
1703
  }
1468
1704
  for (const projDir of projectDirs) {
1469
- const filePath = join3(claudeDir, projDir, `${sessionId}.jsonl`);
1705
+ const filePath = join4(claudeDir, projDir, `${sessionId}.jsonl`);
1470
1706
  try {
1471
1707
  statSync(filePath);
1472
1708
  } catch {
@@ -1986,9 +2222,377 @@ ${list}`, ephemeral: true });
1986
2222
  }
1987
2223
  }
1988
2224
  }
2225
+ async function handlePlugin(interaction) {
2226
+ if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
2227
+ await interaction.reply({ content: "You are not authorized.", ephemeral: true });
2228
+ return;
2229
+ }
2230
+ const sub = interaction.options.getSubcommand();
2231
+ switch (sub) {
2232
+ case "browse":
2233
+ return handlePluginBrowse(interaction);
2234
+ case "install":
2235
+ return handlePluginInstall(interaction);
2236
+ case "remove":
2237
+ return handlePluginRemove(interaction);
2238
+ case "list":
2239
+ return handlePluginList(interaction);
2240
+ case "info":
2241
+ return handlePluginInfo(interaction);
2242
+ case "enable":
2243
+ return handlePluginEnable(interaction);
2244
+ case "disable":
2245
+ return handlePluginDisable(interaction);
2246
+ case "update":
2247
+ return handlePluginUpdate(interaction);
2248
+ case "marketplace-add":
2249
+ return handleMarketplaceAdd(interaction);
2250
+ case "marketplace-remove":
2251
+ return handleMarketplaceRemove(interaction);
2252
+ case "marketplace-list":
2253
+ return handleMarketplaceList(interaction);
2254
+ case "marketplace-update":
2255
+ return handleMarketplaceUpdate(interaction);
2256
+ default:
2257
+ await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
2258
+ }
2259
+ }
2260
+ function resolveScopeAndCwd(interaction) {
2261
+ const scope = interaction.options.getString("scope") || "user";
2262
+ if (scope === "user") return { scope };
2263
+ const session = getSessionByChannel(interaction.channelId);
2264
+ if (!session) {
2265
+ return { scope, error: `Scope \`${scope}\` requires an active session. Run this from a session channel, or use \`user\` scope.` };
2266
+ }
2267
+ return { scope, cwd: session.directory };
2268
+ }
2269
+ async function handlePluginBrowse(interaction) {
2270
+ await interaction.deferReply({ ephemeral: true });
2271
+ try {
2272
+ const search = interaction.options.getString("search")?.toLowerCase();
2273
+ const { installed, available } = await listAvailable();
2274
+ const installedIds = new Set(installed.map((p) => p.id));
2275
+ let filtered = available;
2276
+ if (search) {
2277
+ filtered = available.filter((p) => p.name.toLowerCase().includes(search) || p.description.toLowerCase().includes(search) || p.marketplaceName.toLowerCase().includes(search));
2278
+ }
2279
+ filtered.sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
2280
+ if (filtered.length === 0) {
2281
+ await interaction.editReply("No plugins found matching your search.");
2282
+ return;
2283
+ }
2284
+ const shown = filtered.slice(0, 15);
2285
+ const embed = new EmbedBuilder2().setColor(8141549).setTitle("Available Plugins").setDescription(`Showing ${shown.length} of ${filtered.length} plugins. Use \`/plugin install\` to install.`);
2286
+ for (const p of shown) {
2287
+ const status = installedIds.has(p.pluginId) ? " \u2705" : "";
2288
+ const count = p.installCount ? ` | ${p.installCount.toLocaleString()} installs` : "";
2289
+ embed.addFields({
2290
+ name: `${p.name}${status}`,
2291
+ value: `${truncate(p.description, 150)}
2292
+ *${p.marketplaceName}*${count}`
2293
+ });
2294
+ }
2295
+ await interaction.editReply({ embeds: [embed] });
2296
+ } catch (err) {
2297
+ await interaction.editReply(`Error: ${err.message}`);
2298
+ }
2299
+ }
2300
+ async function handlePluginInstall(interaction) {
2301
+ const pluginId = interaction.options.getString("plugin", true);
2302
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2303
+ if (error) {
2304
+ await interaction.reply({ content: error, ephemeral: true });
2305
+ return;
2306
+ }
2307
+ await interaction.deferReply({ ephemeral: true });
2308
+ try {
2309
+ const result = await installPlugin(pluginId, scope, cwd);
2310
+ const embed = new EmbedBuilder2().setColor(3066993).setTitle("Plugin Installed").setDescription(`**${pluginId}** installed with \`${scope}\` scope.`).addFields({ name: "Output", value: truncate(result, 1e3) || "Done." });
2311
+ await interaction.editReply({ embeds: [embed] });
2312
+ log(`Plugin "${pluginId}" installed (scope=${scope}) by ${interaction.user.tag}`);
2313
+ } catch (err) {
2314
+ await interaction.editReply(`Failed to install: ${err.message}`);
2315
+ }
2316
+ }
2317
+ async function handlePluginRemove(interaction) {
2318
+ const pluginId = interaction.options.getString("plugin", true);
2319
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2320
+ if (error) {
2321
+ await interaction.reply({ content: error, ephemeral: true });
2322
+ return;
2323
+ }
2324
+ await interaction.deferReply({ ephemeral: true });
2325
+ try {
2326
+ const result = await uninstallPlugin(pluginId, scope, cwd);
2327
+ await interaction.editReply(`Plugin **${pluginId}** removed.
2328
+ ${truncate(result, 500)}`);
2329
+ log(`Plugin "${pluginId}" removed (scope=${scope}) by ${interaction.user.tag}`);
2330
+ } catch (err) {
2331
+ await interaction.editReply(`Failed to remove: ${err.message}`);
2332
+ }
2333
+ }
2334
+ async function handlePluginList(interaction) {
2335
+ await interaction.deferReply({ ephemeral: true });
2336
+ try {
2337
+ const plugins = await listInstalled();
2338
+ if (plugins.length === 0) {
2339
+ await interaction.editReply("No plugins installed.");
2340
+ return;
2341
+ }
2342
+ const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Installed Plugins (${plugins.length})`);
2343
+ for (const p of plugins) {
2344
+ const icon = p.enabled ? "\u2705" : "\u274C";
2345
+ const scopeLabel = p.scope.charAt(0).toUpperCase() + p.scope.slice(1);
2346
+ const project = p.projectPath ? `
2347
+ Project: \`${p.projectPath}\`` : "";
2348
+ embed.addFields({
2349
+ name: `${icon} ${p.id}`,
2350
+ value: `v${p.version} | ${scopeLabel} scope${project}`
2351
+ });
2352
+ }
2353
+ await interaction.editReply({ embeds: [embed] });
2354
+ } catch (err) {
2355
+ await interaction.editReply(`Error: ${err.message}`);
2356
+ }
2357
+ }
2358
+ async function handlePluginInfo(interaction) {
2359
+ const pluginId = interaction.options.getString("plugin", true);
2360
+ await interaction.deferReply({ ephemeral: true });
2361
+ try {
2362
+ const parts = pluginId.split("@");
2363
+ const pluginName = parts[0];
2364
+ const marketplaceName = parts[1];
2365
+ const installed = await listInstalled();
2366
+ const installedEntry = installed.find((p) => p.id === pluginId);
2367
+ let detail = null;
2368
+ if (marketplaceName) {
2369
+ detail = await getPluginDetail(pluginName, marketplaceName);
2370
+ }
2371
+ const embed = new EmbedBuilder2().setColor(15965202).setTitle(`Plugin: ${pluginName}`);
2372
+ if (detail) {
2373
+ embed.setDescription(detail.description);
2374
+ if (detail.author) {
2375
+ embed.addFields({ name: "Author", value: detail.author.name, inline: true });
2376
+ }
2377
+ if (detail.category) {
2378
+ embed.addFields({ name: "Category", value: detail.category, inline: true });
2379
+ }
2380
+ if (detail.version) {
2381
+ embed.addFields({ name: "Version", value: detail.version, inline: true });
2382
+ }
2383
+ if (detail.tags?.length) {
2384
+ embed.addFields({ name: "Tags", value: detail.tags.join(", "), inline: false });
2385
+ }
2386
+ if (detail.homepage) {
2387
+ embed.addFields({ name: "Homepage", value: detail.homepage, inline: false });
2388
+ }
2389
+ if (detail.lspServers) {
2390
+ embed.addFields({ name: "LSP Servers", value: Object.keys(detail.lspServers).join(", "), inline: true });
2391
+ }
2392
+ if (detail.mcpServers) {
2393
+ embed.addFields({ name: "MCP Servers", value: Object.keys(detail.mcpServers).join(", "), inline: true });
2394
+ }
2395
+ }
2396
+ if (installedEntry) {
2397
+ const icon = installedEntry.enabled ? "\u2705 Enabled" : "\u274C Disabled";
2398
+ embed.addFields(
2399
+ { name: "Status", value: `${icon} | v${installedEntry.version}`, inline: true },
2400
+ { name: "Scope", value: installedEntry.scope, inline: true },
2401
+ { name: "Installed", value: new Date(installedEntry.installedAt).toLocaleDateString(), inline: true }
2402
+ );
2403
+ } else {
2404
+ embed.addFields({ name: "Status", value: "Not installed", inline: true });
2405
+ }
2406
+ if (marketplaceName) {
2407
+ embed.setFooter({ text: `Marketplace: ${marketplaceName}` });
2408
+ }
2409
+ await interaction.editReply({ embeds: [embed] });
2410
+ } catch (err) {
2411
+ await interaction.editReply(`Error: ${err.message}`);
2412
+ }
2413
+ }
2414
+ async function handlePluginEnable(interaction) {
2415
+ const pluginId = interaction.options.getString("plugin", true);
2416
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2417
+ if (error) {
2418
+ await interaction.reply({ content: error, ephemeral: true });
2419
+ return;
2420
+ }
2421
+ await interaction.deferReply({ ephemeral: true });
2422
+ try {
2423
+ await enablePlugin(pluginId, scope, cwd);
2424
+ await interaction.editReply(`Plugin **${pluginId}** enabled (\`${scope}\` scope).`);
2425
+ log(`Plugin "${pluginId}" enabled (scope=${scope}) by ${interaction.user.tag}`);
2426
+ } catch (err) {
2427
+ await interaction.editReply(`Failed to enable: ${err.message}`);
2428
+ }
2429
+ }
2430
+ async function handlePluginDisable(interaction) {
2431
+ const pluginId = interaction.options.getString("plugin", true);
2432
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2433
+ if (error) {
2434
+ await interaction.reply({ content: error, ephemeral: true });
2435
+ return;
2436
+ }
2437
+ await interaction.deferReply({ ephemeral: true });
2438
+ try {
2439
+ await disablePlugin(pluginId, scope, cwd);
2440
+ await interaction.editReply(`Plugin **${pluginId}** disabled (\`${scope}\` scope).`);
2441
+ log(`Plugin "${pluginId}" disabled (scope=${scope}) by ${interaction.user.tag}`);
2442
+ } catch (err) {
2443
+ await interaction.editReply(`Failed to disable: ${err.message}`);
2444
+ }
2445
+ }
2446
+ async function handlePluginUpdate(interaction) {
2447
+ const pluginId = interaction.options.getString("plugin", true);
2448
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2449
+ if (error) {
2450
+ await interaction.reply({ content: error, ephemeral: true });
2451
+ return;
2452
+ }
2453
+ await interaction.deferReply({ ephemeral: true });
2454
+ try {
2455
+ const result = await updatePlugin(pluginId, scope, cwd);
2456
+ await interaction.editReply(`Plugin **${pluginId}** updated.
2457
+ ${truncate(result, 500)}`);
2458
+ log(`Plugin "${pluginId}" updated (scope=${scope}) by ${interaction.user.tag}`);
2459
+ } catch (err) {
2460
+ await interaction.editReply(`Failed to update: ${err.message}`);
2461
+ }
2462
+ }
2463
+ async function handleMarketplaceAdd(interaction) {
2464
+ const source = interaction.options.getString("source", true);
2465
+ await interaction.deferReply({ ephemeral: true });
2466
+ try {
2467
+ const result = await addMarketplace(source);
2468
+ await interaction.editReply(`Marketplace added from \`${source}\`.
2469
+ ${truncate(result, 500)}`);
2470
+ log(`Marketplace "${source}" added by ${interaction.user.tag}`);
2471
+ } catch (err) {
2472
+ await interaction.editReply(`Failed to add marketplace: ${err.message}`);
2473
+ }
2474
+ }
2475
+ async function handleMarketplaceRemove(interaction) {
2476
+ const name = interaction.options.getString("name", true);
2477
+ await interaction.deferReply({ ephemeral: true });
2478
+ try {
2479
+ const result = await removeMarketplace(name);
2480
+ await interaction.editReply(`Marketplace **${name}** removed.
2481
+ ${truncate(result, 500)}`);
2482
+ log(`Marketplace "${name}" removed by ${interaction.user.tag}`);
2483
+ } catch (err) {
2484
+ await interaction.editReply(`Failed to remove marketplace: ${err.message}`);
2485
+ }
2486
+ }
2487
+ async function handleMarketplaceList(interaction) {
2488
+ await interaction.deferReply({ ephemeral: true });
2489
+ try {
2490
+ const marketplaces = await listMarketplaces();
2491
+ if (marketplaces.length === 0) {
2492
+ await interaction.editReply("No marketplaces registered.");
2493
+ return;
2494
+ }
2495
+ const embed = new EmbedBuilder2().setColor(10181046).setTitle(`Marketplaces (${marketplaces.length})`);
2496
+ for (const m of marketplaces) {
2497
+ const source = m.repo || m.url || m.source;
2498
+ embed.addFields({
2499
+ name: m.name,
2500
+ value: `Source: \`${source}\`
2501
+ Path: \`${m.installLocation}\``
2502
+ });
2503
+ }
2504
+ await interaction.editReply({ embeds: [embed] });
2505
+ } catch (err) {
2506
+ await interaction.editReply(`Error: ${err.message}`);
2507
+ }
2508
+ }
2509
+ async function handleMarketplaceUpdate(interaction) {
2510
+ const name = interaction.options.getString("name") || void 0;
2511
+ await interaction.deferReply({ ephemeral: true });
2512
+ try {
2513
+ const result = await updateMarketplaces(name);
2514
+ await interaction.editReply(`Marketplace${name ? ` **${name}**` : "s"} updated.
2515
+ ${truncate(result, 500)}`);
2516
+ log(`Marketplace${name ? ` "${name}"` : "s"} updated by ${interaction.user.tag}`);
2517
+ } catch (err) {
2518
+ await interaction.editReply(`Failed to update: ${err.message}`);
2519
+ }
2520
+ }
2521
+ async function handlePluginAutocomplete(interaction) {
2522
+ const sub = interaction.options.getSubcommand();
2523
+ const focused = interaction.options.getFocused().toLowerCase();
2524
+ try {
2525
+ if (sub === "install" || sub === "info" && !focused.includes("@")) {
2526
+ const { available } = await listAvailable();
2527
+ const filtered = focused ? available.filter((p) => p.name.toLowerCase().includes(focused) || p.pluginId.toLowerCase().includes(focused) || p.description.toLowerCase().includes(focused)) : available;
2528
+ filtered.sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
2529
+ const choices = filtered.slice(0, 25).map((p) => ({
2530
+ name: `${p.name} (${p.marketplaceName})`.slice(0, 100),
2531
+ value: p.pluginId
2532
+ }));
2533
+ await interaction.respond(choices);
2534
+ } else if (["remove", "enable", "disable", "update", "info"].includes(sub)) {
2535
+ const installed = await listInstalled();
2536
+ const filtered = focused ? installed.filter((p) => p.id.toLowerCase().includes(focused)) : installed;
2537
+ const choices = filtered.slice(0, 25).map((p) => ({
2538
+ name: `${p.id} (v${p.version}, ${p.scope})`.slice(0, 100),
2539
+ value: p.id
2540
+ }));
2541
+ await interaction.respond(choices);
2542
+ } else if (sub === "marketplace-remove" || sub === "marketplace-update") {
2543
+ const marketplaces = await listMarketplaces();
2544
+ const filtered = focused ? marketplaces.filter((m) => m.name.toLowerCase().includes(focused)) : marketplaces;
2545
+ const choices = filtered.slice(0, 25).map((m) => ({
2546
+ name: m.name,
2547
+ value: m.name
2548
+ }));
2549
+ await interaction.respond(choices);
2550
+ } else {
2551
+ await interaction.respond([]);
2552
+ }
2553
+ } catch {
2554
+ await interaction.respond([]);
2555
+ }
2556
+ }
1989
2557
 
1990
2558
  // src/message-handler.ts
2559
+ import sharp from "sharp";
2560
+ var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
2561
+ "image/jpeg",
2562
+ "image/png",
2563
+ "image/gif",
2564
+ "image/webp"
2565
+ ]);
2566
+ var MAX_IMAGE_SIZE = 20 * 1024 * 1024;
2567
+ var MAX_BASE64_BYTES = 5 * 1024 * 1024;
1991
2568
  var userLastMessage = /* @__PURE__ */ new Map();
2569
+ async function resizeImageToFit(buf, mediaType) {
2570
+ if (buf.length <= MAX_BASE64_BYTES) return buf;
2571
+ const isJpeg = mediaType === "image/jpeg";
2572
+ const format = isJpeg ? "jpeg" : "webp";
2573
+ let img = sharp(buf);
2574
+ const meta = await img.metadata();
2575
+ const width = meta.width || 1;
2576
+ const height = meta.height || 1;
2577
+ let scale = 1;
2578
+ for (let i = 0; i < 5; i++) {
2579
+ scale *= 0.7;
2580
+ const resized = await sharp(buf).resize(Math.round(width * scale), Math.round(height * scale), { fit: "inside" })[format]({ quality: 80 }).toBuffer();
2581
+ if (resized.length <= MAX_BASE64_BYTES) return resized;
2582
+ }
2583
+ return sharp(buf).resize(Math.round(width * scale * 0.5), Math.round(height * scale * 0.5), { fit: "inside" }).jpeg({ quality: 60 }).toBuffer();
2584
+ }
2585
+ async function fetchImageAsBase64(url, mediaType) {
2586
+ const res = await fetch(url);
2587
+ if (!res.ok) throw new Error(`Failed to download image: ${res.status}`);
2588
+ let buf = Buffer.from(await res.arrayBuffer());
2589
+ if (buf.length > MAX_BASE64_BYTES) {
2590
+ buf = await resizeImageToFit(buf, mediaType);
2591
+ const newType = mediaType === "image/jpeg" ? "image/jpeg" : "image/webp";
2592
+ return { data: buf.toString("base64"), mediaType: newType };
2593
+ }
2594
+ return { data: buf.toString("base64"), mediaType };
2595
+ }
1992
2596
  async function handleMessage(message) {
1993
2597
  if (message.author.bot) return;
1994
2598
  const session = getSessionByChannel(message.channelId);
@@ -2017,11 +2621,41 @@ async function handleMessage(message) {
2017
2621
  return;
2018
2622
  }
2019
2623
  }
2020
- const content = message.content.trim();
2021
- if (!content) return;
2624
+ const text = message.content.trim();
2625
+ const imageAttachments = message.attachments.filter(
2626
+ (a) => a.contentType && SUPPORTED_IMAGE_TYPES.has(a.contentType) && a.size <= MAX_IMAGE_SIZE
2627
+ );
2628
+ if (!text && imageAttachments.size === 0) return;
2022
2629
  try {
2023
2630
  const channel = message.channel;
2024
- const stream = sendPrompt(session.id, content);
2631
+ let prompt;
2632
+ if (imageAttachments.size === 0) {
2633
+ prompt = text;
2634
+ } else {
2635
+ const blocks = [];
2636
+ const imageResults = await Promise.allSettled(
2637
+ imageAttachments.map((a) => fetchImageAsBase64(a.url, a.contentType))
2638
+ );
2639
+ for (const result of imageResults) {
2640
+ if (result.status === "fulfilled") {
2641
+ blocks.push({
2642
+ type: "image",
2643
+ source: {
2644
+ type: "base64",
2645
+ media_type: result.value.mediaType,
2646
+ data: result.value.data
2647
+ }
2648
+ });
2649
+ }
2650
+ }
2651
+ if (text) {
2652
+ blocks.push({ type: "text", text });
2653
+ } else if (blocks.length > 0) {
2654
+ blocks.push({ type: "text", text: "What is in this image?" });
2655
+ }
2656
+ prompt = blocks;
2657
+ }
2658
+ const stream = sendPrompt(session.id, prompt);
2025
2659
  await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
2026
2660
  } catch (err) {
2027
2661
  await message.reply({
@@ -2032,6 +2666,12 @@ async function handleMessage(message) {
2032
2666
  }
2033
2667
 
2034
2668
  // src/button-handler.ts
2669
+ import {
2670
+ ActionRowBuilder as ActionRowBuilder2,
2671
+ ButtonBuilder as ButtonBuilder2,
2672
+ ButtonStyle as ButtonStyle2,
2673
+ StringSelectMenuBuilder as StringSelectMenuBuilder2
2674
+ } from "discord.js";
2035
2675
  async function handleButton(interaction) {
2036
2676
  if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
2037
2677
  await interaction.reply({ content: "Not authorized.", ephemeral: true });
@@ -2103,10 +2743,83 @@ ${display}
2103
2743
  }
2104
2744
  return;
2105
2745
  }
2746
+ if (customId.startsWith("pick:")) {
2747
+ const parts = customId.split(":");
2748
+ const sessionId = parts[1];
2749
+ const questionIndex = parseInt(parts[2], 10);
2750
+ const answer = parts.slice(3).join(":");
2751
+ const session = getSession(sessionId);
2752
+ if (!session) {
2753
+ await interaction.reply({ content: "Session not found.", ephemeral: true });
2754
+ return;
2755
+ }
2756
+ setPendingAnswer(sessionId, questionIndex, answer);
2757
+ const totalQuestions = getQuestionCount(sessionId);
2758
+ const pending = getPendingAnswers(sessionId);
2759
+ const answeredCount = pending?.size || 0;
2760
+ try {
2761
+ const original = interaction.message;
2762
+ const updatedComponents = original.components.map((row) => {
2763
+ const firstComponent = row.components?.[0];
2764
+ if (!firstComponent?.customId?.startsWith("pick:")) return row;
2765
+ const rowQi = parseInt(firstComponent.customId.split(":")[2], 10);
2766
+ if (rowQi !== questionIndex) return row;
2767
+ const newRow = new ActionRowBuilder2();
2768
+ for (const btn of row.components) {
2769
+ const btnAnswer = btn.customId.split(":").slice(3).join(":");
2770
+ const isSelected = btnAnswer === answer;
2771
+ newRow.addComponents(
2772
+ new ButtonBuilder2().setCustomId(btn.customId).setLabel(btn.label).setStyle(isSelected ? ButtonStyle2.Success : ButtonStyle2.Secondary)
2773
+ );
2774
+ }
2775
+ return newRow;
2776
+ });
2777
+ await original.edit({ components: updatedComponents });
2778
+ } catch {
2779
+ }
2780
+ await interaction.reply({
2781
+ content: `Selected for Q${questionIndex + 1}: **${truncate(answer, 100)}** (${answeredCount}/${totalQuestions} answered)`,
2782
+ ephemeral: true
2783
+ });
2784
+ return;
2785
+ }
2786
+ if (customId.startsWith("submit-answers:")) {
2787
+ const sessionId = customId.slice(15);
2788
+ const session = getSession(sessionId);
2789
+ if (!session) {
2790
+ await interaction.reply({ content: "Session not found.", ephemeral: true });
2791
+ return;
2792
+ }
2793
+ const totalQuestions = getQuestionCount(sessionId);
2794
+ const pending = getPendingAnswers(sessionId);
2795
+ if (!pending || pending.size === 0) {
2796
+ await interaction.reply({ content: "No answers selected yet. Pick an answer for each question first.", ephemeral: true });
2797
+ return;
2798
+ }
2799
+ const answerLines = [];
2800
+ for (let i = 0; i < totalQuestions; i++) {
2801
+ const ans = pending.get(i);
2802
+ answerLines.push(`Q${i + 1}: ${ans || "(no answer)"}`);
2803
+ }
2804
+ const combined = answerLines.join("\n");
2805
+ clearPendingAnswers(sessionId);
2806
+ await interaction.deferReply();
2807
+ try {
2808
+ const channel = interaction.channel;
2809
+ const stream = sendPrompt(sessionId, combined);
2810
+ await interaction.editReply(`Submitted answers:
2811
+ ${combined}`);
2812
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
2813
+ } catch (err) {
2814
+ await interaction.editReply(`Error: ${err.message}`);
2815
+ }
2816
+ return;
2817
+ }
2106
2818
  if (customId.startsWith("answer:")) {
2107
2819
  const parts = customId.split(":");
2108
2820
  const sessionId = parts[1];
2109
- const answer = parts.slice(2).join(":");
2821
+ const hasQuestionIndex = /^\d+$/.test(parts[2]);
2822
+ const answer = hasQuestionIndex ? parts.slice(3).join(":") : parts.slice(2).join(":");
2110
2823
  const session = getSession(sessionId);
2111
2824
  if (!session) {
2112
2825
  await interaction.reply({ content: "Session not found.", ephemeral: true });
@@ -2184,8 +2897,48 @@ async function handleSelectMenu(interaction) {
2184
2897
  return;
2185
2898
  }
2186
2899
  const customId = interaction.customId;
2900
+ if (customId.startsWith("pick-select:")) {
2901
+ const parts = customId.split(":");
2902
+ const sessionId = parts[1];
2903
+ const questionIndex = parseInt(parts[2], 10);
2904
+ const selected = interaction.values[0];
2905
+ const session = getSession(sessionId);
2906
+ if (!session) {
2907
+ await interaction.reply({ content: "Session not found.", ephemeral: true });
2908
+ return;
2909
+ }
2910
+ setPendingAnswer(sessionId, questionIndex, selected);
2911
+ const totalQuestions = getQuestionCount(sessionId);
2912
+ const pending = getPendingAnswers(sessionId);
2913
+ const answeredCount = pending?.size || 0;
2914
+ try {
2915
+ const original = interaction.message;
2916
+ const updatedComponents = original.components.map((row) => {
2917
+ const comp = row.components?.[0];
2918
+ if (comp?.customId !== customId) return row;
2919
+ const menu = new StringSelectMenuBuilder2().setCustomId(customId).setPlaceholder(`Selected: ${selected.slice(0, 80)}`);
2920
+ for (const opt of comp.options) {
2921
+ menu.addOptions({
2922
+ label: opt.label,
2923
+ description: opt.description || void 0,
2924
+ value: opt.value,
2925
+ default: opt.value === selected
2926
+ });
2927
+ }
2928
+ return new ActionRowBuilder2().addComponents(menu);
2929
+ });
2930
+ await original.edit({ components: updatedComponents });
2931
+ } catch {
2932
+ }
2933
+ await interaction.reply({
2934
+ content: `Selected for Q${questionIndex + 1}: **${truncate(selected, 100)}** (${answeredCount}/${totalQuestions} answered)`,
2935
+ ephemeral: true
2936
+ });
2937
+ return;
2938
+ }
2187
2939
  if (customId.startsWith("answer-select:")) {
2188
- const sessionId = customId.slice(14);
2940
+ const afterPrefix = customId.slice(14);
2941
+ const sessionId = afterPrefix.includes(":") ? afterPrefix.split(":")[0] : afterPrefix;
2189
2942
  const selected = interaction.values[0];
2190
2943
  const session = getSession(sessionId);
2191
2944
  if (!session) {
@@ -2301,12 +3054,17 @@ async function startBot() {
2301
3054
  return await handleAgent(interaction);
2302
3055
  case "project":
2303
3056
  return await handleProject(interaction);
3057
+ case "plugin":
3058
+ return await handlePlugin(interaction);
2304
3059
  }
2305
3060
  }
2306
3061
  if (interaction.isAutocomplete()) {
2307
3062
  if (interaction.commandName === "claude") {
2308
3063
  return await handleClaudeAutocomplete(interaction);
2309
3064
  }
3065
+ if (interaction.commandName === "plugin") {
3066
+ return await handlePluginAutocomplete(interaction);
3067
+ }
2310
3068
  }
2311
3069
  if (interaction.isButton()) {
2312
3070
  return await handleButton(interaction);
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ switch (command) {
18
18
  console.log("Run \x1B[36magentcord setup\x1B[0m to configure.\n");
19
19
  process.exit(1);
20
20
  }
21
- const { startBot } = await import("./bot-G6464LRS.js");
21
+ const { startBot } = await import("./bot-MUOL7CRV.js");
22
22
  console.log("agentcord starting...");
23
23
  await startBot();
24
24
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcord",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Discord bot for managing AI coding agent sessions (Claude Code, Codex, and more)",
6
6
  "bin": {
@@ -50,7 +50,8 @@
50
50
  "@anthropic-ai/claude-agent-sdk": "^0.2.36",
51
51
  "@clack/prompts": "^1.0.0",
52
52
  "discord.js": "^14.16.3",
53
- "dotenv": "^17.2.4"
53
+ "dotenv": "^17.2.4",
54
+ "sharp": "^0.34.5"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/node": "^22.10.0",