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.
- package/dist/{bot-G6464LRS.js → bot-MUOL7CRV.js} +776 -18
- package/dist/cli.js +1 -1
- package/package.json +3 -2
|
@@ -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
|
|
108
|
-
import { homedir as
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
2021
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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.
|
|
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",
|