agentcord 0.1.6 → 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-RT4CJNKG.js → bot-MUOL7CRV.js} +880 -31
- package/dist/cli.js +1 -1
- package/package.json +3 -2
|
@@ -53,7 +53,11 @@ import {
|
|
|
53
53
|
Routes
|
|
54
54
|
} from "discord.js";
|
|
55
55
|
function getCommandDefinitions() {
|
|
56
|
-
const claude = new SlashCommandBuilder().setName("claude").setDescription("Manage Claude Code sessions").addSubcommand((sub) => sub.setName("new").setDescription("Create a new Claude Code session").addStringOption((opt) => opt.setName("name").setDescription("Session name").setRequired(true)).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("resume").setDescription("Resume an existing Claude Code session from terminal").addStringOption((opt) => opt.setName("session-id").setDescription("Claude Code session UUID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("name").setDescription("Name for the Discord channel").setRequired(true)).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("list").setDescription("List active sessions")).addSubcommand((sub) => sub.setName("end").setDescription("End the session in this channel")).addSubcommand((sub) => sub.setName("continue").setDescription("Continue the last conversation")).addSubcommand((sub) => sub.setName("stop").setDescription("Stop current generation")).addSubcommand((sub) => sub.setName("output").setDescription("Show recent conversation output").addIntegerOption((opt) => opt.setName("lines").setDescription("Number of lines (default 50)").setMinValue(1).setMaxValue(500))).addSubcommand((sub) => sub.setName("attach").setDescription("Show tmux attach command for terminal access")).addSubcommand((sub) => sub.setName("sync").setDescription("Reconnect orphaned tmux sessions")).addSubcommand((sub) => sub.setName("model").setDescription("Change the model for this session").addStringOption((opt) => opt.setName("model").setDescription("Model name (e.g. claude-sonnet-4-5-20250929)").setRequired(true))).addSubcommand((sub) => sub.setName("verbose").setDescription("Toggle showing tool calls and results in this session"))
|
|
56
|
+
const claude = new SlashCommandBuilder().setName("claude").setDescription("Manage Claude Code sessions").addSubcommand((sub) => sub.setName("new").setDescription("Create a new Claude Code session").addStringOption((opt) => opt.setName("name").setDescription("Session name").setRequired(true)).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("resume").setDescription("Resume an existing Claude Code session from terminal").addStringOption((opt) => opt.setName("session-id").setDescription("Claude Code session UUID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("name").setDescription("Name for the Discord channel").setRequired(true)).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("list").setDescription("List active sessions")).addSubcommand((sub) => sub.setName("end").setDescription("End the session in this channel")).addSubcommand((sub) => sub.setName("continue").setDescription("Continue the last conversation")).addSubcommand((sub) => sub.setName("stop").setDescription("Stop current generation")).addSubcommand((sub) => sub.setName("output").setDescription("Show recent conversation output").addIntegerOption((opt) => opt.setName("lines").setDescription("Number of lines (default 50)").setMinValue(1).setMaxValue(500))).addSubcommand((sub) => sub.setName("attach").setDescription("Show tmux attach command for terminal access")).addSubcommand((sub) => sub.setName("sync").setDescription("Reconnect orphaned tmux sessions")).addSubcommand((sub) => sub.setName("model").setDescription("Change the model for this session").addStringOption((opt) => opt.setName("model").setDescription("Model name (e.g. claude-sonnet-4-5-20250929)").setRequired(true))).addSubcommand((sub) => sub.setName("verbose").setDescription("Toggle showing tool calls and results in this session")).addSubcommand((sub) => sub.setName("mode").setDescription("Set session mode (auto/plan/normal)").addStringOption((opt) => opt.setName("mode").setDescription("Session mode").setRequired(true).addChoices(
|
|
57
|
+
{ name: "Auto \u2014 full autonomy", value: "auto" },
|
|
58
|
+
{ name: "Plan \u2014 plan before executing", value: "plan" },
|
|
59
|
+
{ name: "Normal \u2014 ask before destructive ops", value: "normal" }
|
|
60
|
+
)));
|
|
57
61
|
const shell = new SlashCommandBuilder().setName("shell").setDescription("Run shell commands in the session directory").addSubcommand((sub) => sub.setName("run").setDescription("Execute a shell command").addStringOption((opt) => opt.setName("command").setDescription("Command to run").setRequired(true))).addSubcommand((sub) => sub.setName("processes").setDescription("List running processes")).addSubcommand((sub) => sub.setName("kill").setDescription("Kill a running process").addIntegerOption((opt) => opt.setName("pid").setDescription("Process ID to kill").setRequired(true)));
|
|
58
62
|
const agent = new SlashCommandBuilder().setName("agent").setDescription("Manage agent personas").addSubcommand((sub) => sub.setName("use").setDescription("Switch to an agent persona").addStringOption((opt) => opt.setName("persona").setDescription("Agent persona name").setRequired(true).addChoices(
|
|
59
63
|
{ name: "Code Reviewer", value: "code-reviewer" },
|
|
@@ -65,11 +69,33 @@ function getCommandDefinitions() {
|
|
|
65
69
|
{ name: "General", value: "general" }
|
|
66
70
|
))).addSubcommand((sub) => sub.setName("list").setDescription("List available agent personas")).addSubcommand((sub) => sub.setName("clear").setDescription("Clear agent persona"));
|
|
67
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)));
|
|
68
93
|
return [
|
|
69
94
|
claude.toJSON(),
|
|
70
95
|
shell.toJSON(),
|
|
71
96
|
agent.toJSON(),
|
|
72
|
-
project.toJSON()
|
|
97
|
+
project.toJSON(),
|
|
98
|
+
plugin.toJSON()
|
|
73
99
|
];
|
|
74
100
|
}
|
|
75
101
|
async function registerCommands() {
|
|
@@ -100,8 +126,8 @@ import {
|
|
|
100
126
|
ChannelType
|
|
101
127
|
} from "discord.js";
|
|
102
128
|
import { readdirSync, statSync, createReadStream } from "fs";
|
|
103
|
-
import { join as
|
|
104
|
-
import { homedir as
|
|
129
|
+
import { join as join4, basename } from "path";
|
|
130
|
+
import { homedir as homedir3 } from "os";
|
|
105
131
|
import { createInterface } from "readline";
|
|
106
132
|
|
|
107
133
|
// src/session-manager.ts
|
|
@@ -241,6 +267,9 @@ async function saveProjects() {
|
|
|
241
267
|
function getProject(name) {
|
|
242
268
|
return projects[name];
|
|
243
269
|
}
|
|
270
|
+
function getProjectByCategoryId(categoryId) {
|
|
271
|
+
return Object.values(projects).find((p) => p.categoryId === categoryId);
|
|
272
|
+
}
|
|
244
273
|
function getOrCreateProject(name, directory, categoryId) {
|
|
245
274
|
if (!projects[name]) {
|
|
246
275
|
projects[name] = {
|
|
@@ -427,14 +456,19 @@ function detectNumberedOptions(text) {
|
|
|
427
456
|
const options = [];
|
|
428
457
|
const optionRegex = /^\s*(\d+)[.)]\s+(.+)$/;
|
|
429
458
|
let firstOptionLine = -1;
|
|
459
|
+
let lastOptionLine = -1;
|
|
430
460
|
for (let i = 0; i < lines.length; i++) {
|
|
431
461
|
const match = lines[i].match(optionRegex);
|
|
432
462
|
if (match) {
|
|
433
463
|
if (firstOptionLine === -1) firstOptionLine = i;
|
|
464
|
+
lastOptionLine = i;
|
|
434
465
|
options.push(match[2].trim());
|
|
435
466
|
}
|
|
436
467
|
}
|
|
437
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;
|
|
438
472
|
const preamble = lines.slice(0, firstOptionLine).join(" ").toLowerCase();
|
|
439
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);
|
|
440
474
|
return hasQuestion ? options : null;
|
|
@@ -446,6 +480,11 @@ function detectYesNoPrompt(text) {
|
|
|
446
480
|
|
|
447
481
|
// src/session-manager.ts
|
|
448
482
|
var SESSION_PREFIX = "claude-";
|
|
483
|
+
var MODE_PROMPTS = {
|
|
484
|
+
auto: "",
|
|
485
|
+
plan: "You MUST use EnterPlanMode at the start of every task. Present your plan for user approval before making any code changes. Do not write or edit files until the user approves the plan.",
|
|
486
|
+
normal: "Before performing destructive or significant operations (deleting files, running dangerous commands, making large refactors, writing to many files), use AskUserQuestion to confirm with the user first. Ask for explicit approval before proceeding with changes."
|
|
487
|
+
};
|
|
449
488
|
var sessionStore = new Store("sessions.json");
|
|
450
489
|
var sessions = /* @__PURE__ */ new Map();
|
|
451
490
|
var channelToSession = /* @__PURE__ */ new Map();
|
|
@@ -473,6 +512,7 @@ async function loadSessions() {
|
|
|
473
512
|
sessions.set(s.id, {
|
|
474
513
|
...s,
|
|
475
514
|
verbose: s.verbose ?? false,
|
|
515
|
+
mode: s.mode ?? "auto",
|
|
476
516
|
isGenerating: false
|
|
477
517
|
});
|
|
478
518
|
channelToSession.set(s.channelId, s.id);
|
|
@@ -499,6 +539,7 @@ async function saveSessions() {
|
|
|
499
539
|
model: s.model,
|
|
500
540
|
agentPersona: s.agentPersona,
|
|
501
541
|
verbose: s.verbose || void 0,
|
|
542
|
+
mode: s.mode !== "auto" ? s.mode : void 0,
|
|
502
543
|
createdAt: s.createdAt,
|
|
503
544
|
lastActivity: s.lastActivity,
|
|
504
545
|
messageCount: s.messageCount,
|
|
@@ -532,6 +573,7 @@ async function createSession(name, directory, channelId, projectName, claudeSess
|
|
|
532
573
|
tmuxName,
|
|
533
574
|
claudeSessionId,
|
|
534
575
|
verbose: false,
|
|
576
|
+
mode: "auto",
|
|
535
577
|
isGenerating: false,
|
|
536
578
|
createdAt: Date.now(),
|
|
537
579
|
lastActivity: Date.now(),
|
|
@@ -601,6 +643,13 @@ function setVerbose(sessionId, verbose) {
|
|
|
601
643
|
saveSessions();
|
|
602
644
|
}
|
|
603
645
|
}
|
|
646
|
+
function setMode(sessionId, mode) {
|
|
647
|
+
const session = sessions.get(sessionId);
|
|
648
|
+
if (session) {
|
|
649
|
+
session.mode = mode;
|
|
650
|
+
saveSessions();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
604
653
|
function setAgentPersona(sessionId, persona) {
|
|
605
654
|
const session = sessions.get(sessionId);
|
|
606
655
|
if (session) {
|
|
@@ -616,6 +665,8 @@ function buildSystemPrompt(session) {
|
|
|
616
665
|
const agent = getAgent(session.agentPersona);
|
|
617
666
|
if (agent?.systemPrompt) parts.push(agent.systemPrompt);
|
|
618
667
|
}
|
|
668
|
+
const modePrompt = MODE_PROMPTS[session.mode];
|
|
669
|
+
if (modePrompt) parts.push(modePrompt);
|
|
619
670
|
if (parts.length > 0) {
|
|
620
671
|
return { type: "preset", preset: "claude_code", append: parts.join("\n\n") };
|
|
621
672
|
}
|
|
@@ -630,9 +681,23 @@ async function* sendPrompt(sessionId, prompt) {
|
|
|
630
681
|
session.isGenerating = true;
|
|
631
682
|
session.lastActivity = Date.now();
|
|
632
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
|
+
}
|
|
633
698
|
try {
|
|
634
699
|
const stream = query({
|
|
635
|
-
prompt,
|
|
700
|
+
prompt: queryPrompt,
|
|
636
701
|
options: {
|
|
637
702
|
cwd: session.directory,
|
|
638
703
|
resume: session.claudeSessionId,
|
|
@@ -756,16 +821,158 @@ async function listTmuxSessions() {
|
|
|
756
821
|
}
|
|
757
822
|
}
|
|
758
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
|
+
|
|
759
946
|
// src/output-handler.ts
|
|
760
947
|
import {
|
|
948
|
+
AttachmentBuilder,
|
|
761
949
|
EmbedBuilder,
|
|
762
950
|
ActionRowBuilder,
|
|
763
951
|
ButtonBuilder,
|
|
764
952
|
ButtonStyle,
|
|
765
953
|
StringSelectMenuBuilder
|
|
766
954
|
} from "discord.js";
|
|
955
|
+
import { existsSync as existsSync4 } from "fs";
|
|
767
956
|
var expandableStore = /* @__PURE__ */ new Map();
|
|
768
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
|
+
}
|
|
769
976
|
setInterval(() => {
|
|
770
977
|
const now = Date.now();
|
|
771
978
|
const TTL = 10 * 60 * 1e3;
|
|
@@ -806,6 +1013,20 @@ function makeOptionButtons(sessionId, options) {
|
|
|
806
1013
|
}
|
|
807
1014
|
return rows;
|
|
808
1015
|
}
|
|
1016
|
+
function makeModeButtons(sessionId, currentMode) {
|
|
1017
|
+
const modes = [
|
|
1018
|
+
{ id: "auto", label: "\u26A1 Auto" },
|
|
1019
|
+
{ id: "plan", label: "\u{1F4CB} Plan" },
|
|
1020
|
+
{ id: "normal", label: "\u{1F6E1}\uFE0F Normal" }
|
|
1021
|
+
];
|
|
1022
|
+
const row = new ActionRowBuilder();
|
|
1023
|
+
for (const m of modes) {
|
|
1024
|
+
row.addComponents(
|
|
1025
|
+
new ButtonBuilder().setCustomId(`mode:${sessionId}:${m.id}`).setLabel(m.label).setStyle(m.id === currentMode ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(m.id === currentMode)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
return row;
|
|
1029
|
+
}
|
|
809
1030
|
function makeYesNoButtons(sessionId) {
|
|
810
1031
|
return new ActionRowBuilder().addComponents(
|
|
811
1032
|
new ButtonBuilder().setCustomId(`confirm:${sessionId}:yes`).setLabel("Yes").setStyle(ButtonStyle.Success),
|
|
@@ -931,6 +1152,20 @@ var MessageStreamer = class {
|
|
|
931
1152
|
}
|
|
932
1153
|
}
|
|
933
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
|
+
}
|
|
934
1169
|
var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
|
|
935
1170
|
"AskUserQuestion",
|
|
936
1171
|
"EnterPlanMode",
|
|
@@ -957,21 +1192,29 @@ function renderAskUserQuestion(toolInput, sessionId) {
|
|
|
957
1192
|
const data = JSON.parse(toolInput);
|
|
958
1193
|
const questions = data.questions;
|
|
959
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
|
+
}
|
|
960
1200
|
const embeds = [];
|
|
961
1201
|
const components = [];
|
|
962
|
-
|
|
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];
|
|
963
1206
|
const embed = new EmbedBuilder().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
|
|
964
1207
|
if (q.options?.length) {
|
|
965
1208
|
if (q.options.length <= 4) {
|
|
966
1209
|
const row = new ActionRowBuilder();
|
|
967
1210
|
for (let i = 0; i < q.options.length; i++) {
|
|
968
1211
|
row.addComponents(
|
|
969
|
-
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)
|
|
970
1213
|
);
|
|
971
1214
|
}
|
|
972
1215
|
components.push(row);
|
|
973
1216
|
} else {
|
|
974
|
-
const menu = new StringSelectMenuBuilder().setCustomId(
|
|
1217
|
+
const menu = new StringSelectMenuBuilder().setCustomId(`${selectPrefix}:${sessionId}:${qi}`).setPlaceholder("Select an option...");
|
|
975
1218
|
for (const opt of q.options) {
|
|
976
1219
|
menu.addOptions({
|
|
977
1220
|
label: opt.label.slice(0, 100),
|
|
@@ -986,6 +1229,13 @@ function renderAskUserQuestion(toolInput, sessionId) {
|
|
|
986
1229
|
}
|
|
987
1230
|
embeds.push(embed);
|
|
988
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
|
+
}
|
|
989
1239
|
return { embeds, components };
|
|
990
1240
|
} catch {
|
|
991
1241
|
return null;
|
|
@@ -1021,11 +1271,12 @@ function renderTaskListEmbed(resultText) {
|
|
|
1021
1271
|
}
|
|
1022
1272
|
return new EmbedBuilder().setColor(10181046).setTitle("\u{1F4CB} Task Board").setDescription(truncate(formatted, 4e3));
|
|
1023
1273
|
}
|
|
1024
|
-
async function handleOutputStream(stream, channel, sessionId, verbose = false) {
|
|
1274
|
+
async function handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto") {
|
|
1025
1275
|
const streamer = new MessageStreamer(channel, sessionId);
|
|
1026
1276
|
let currentToolName = null;
|
|
1027
1277
|
let currentToolInput = "";
|
|
1028
1278
|
let lastFinishedToolName = null;
|
|
1279
|
+
let pendingImagePath = null;
|
|
1029
1280
|
channel.sendTyping().catch(() => {
|
|
1030
1281
|
});
|
|
1031
1282
|
const typingInterval = setInterval(() => {
|
|
@@ -1087,6 +1338,7 @@ ${displayInput}
|
|
|
1087
1338
|
await channel.send({ embeds: [embed], components });
|
|
1088
1339
|
}
|
|
1089
1340
|
}
|
|
1341
|
+
pendingImagePath = extractImagePath(currentToolName, currentToolInput);
|
|
1090
1342
|
lastFinishedToolName = currentToolName;
|
|
1091
1343
|
currentToolName = null;
|
|
1092
1344
|
currentToolInput = "";
|
|
@@ -1094,6 +1346,17 @@ ${displayInput}
|
|
|
1094
1346
|
}
|
|
1095
1347
|
}
|
|
1096
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
|
+
}
|
|
1097
1360
|
const showResult = verbose || lastFinishedToolName !== null && TASK_TOOLS.has(lastFinishedToolName);
|
|
1098
1361
|
if (!showResult) continue;
|
|
1099
1362
|
await streamer.finalize();
|
|
@@ -1151,7 +1414,8 @@ ${displayResult}
|
|
|
1151
1414
|
const embed = new EmbedBuilder().setColor(isSuccess ? 3066993 : 15158332).setTitle(isSuccess ? "Completed" : "Error").addFields(
|
|
1152
1415
|
{ name: "Cost", value: `$${cost}`, inline: true },
|
|
1153
1416
|
{ name: "Duration", value: duration, inline: true },
|
|
1154
|
-
{ name: "Turns", value: `${turns}`, inline: true }
|
|
1417
|
+
{ name: "Turns", value: `${turns}`, inline: true },
|
|
1418
|
+
{ name: "Mode", value: { auto: "\u26A1 Auto", plan: "\u{1F4CB} Plan", normal: "\u{1F6E1}\uFE0F Normal" }[mode] || "\u26A1 Auto", inline: true }
|
|
1155
1419
|
);
|
|
1156
1420
|
if (result.session_id) {
|
|
1157
1421
|
embed.setFooter({ text: `Session: ${result.session_id}` });
|
|
@@ -1167,6 +1431,7 @@ ${displayResult}
|
|
|
1167
1431
|
} else if (detectYesNoPrompt(checkText)) {
|
|
1168
1432
|
components.push(makeYesNoButtons(sessionId));
|
|
1169
1433
|
}
|
|
1434
|
+
components.push(makeModeButtons(sessionId, mode));
|
|
1170
1435
|
components.push(makeCompletionButtons(sessionId));
|
|
1171
1436
|
await channel.send({ embeds: [embed], components });
|
|
1172
1437
|
}
|
|
@@ -1337,13 +1602,23 @@ async function handleClaude(interaction) {
|
|
|
1337
1602
|
return handleClaudeModel(interaction);
|
|
1338
1603
|
case "verbose":
|
|
1339
1604
|
return handleClaudeVerbose(interaction);
|
|
1605
|
+
case "mode":
|
|
1606
|
+
return handleClaudeMode(interaction);
|
|
1340
1607
|
default:
|
|
1341
1608
|
await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
|
|
1342
1609
|
}
|
|
1343
1610
|
}
|
|
1344
1611
|
async function handleClaudeNew(interaction) {
|
|
1345
1612
|
const name = interaction.options.getString("name", true);
|
|
1346
|
-
|
|
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
|
+
}
|
|
1347
1622
|
await interaction.deferReply();
|
|
1348
1623
|
let channel;
|
|
1349
1624
|
try {
|
|
@@ -1385,7 +1660,7 @@ async function handleClaudeNew(interaction) {
|
|
|
1385
1660
|
}
|
|
1386
1661
|
}
|
|
1387
1662
|
function discoverLocalSessions() {
|
|
1388
|
-
const claudeDir =
|
|
1663
|
+
const claudeDir = join4(homedir3(), ".claude", "projects");
|
|
1389
1664
|
const results = [];
|
|
1390
1665
|
let projectDirs;
|
|
1391
1666
|
try {
|
|
@@ -1394,7 +1669,7 @@ function discoverLocalSessions() {
|
|
|
1394
1669
|
return [];
|
|
1395
1670
|
}
|
|
1396
1671
|
for (const projDir of projectDirs) {
|
|
1397
|
-
const projPath =
|
|
1672
|
+
const projPath = join4(claudeDir, projDir);
|
|
1398
1673
|
let files;
|
|
1399
1674
|
try {
|
|
1400
1675
|
files = readdirSync(projPath);
|
|
@@ -1408,7 +1683,7 @@ function discoverLocalSessions() {
|
|
|
1408
1683
|
const sessionId = file.replace(".jsonl", "");
|
|
1409
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;
|
|
1410
1685
|
try {
|
|
1411
|
-
const mtime = statSync(
|
|
1686
|
+
const mtime = statSync(join4(projPath, file)).mtimeMs;
|
|
1412
1687
|
results.push({ id: sessionId, project, mtime, firstMessage: "" });
|
|
1413
1688
|
} catch {
|
|
1414
1689
|
continue;
|
|
@@ -1419,7 +1694,7 @@ function discoverLocalSessions() {
|
|
|
1419
1694
|
return results;
|
|
1420
1695
|
}
|
|
1421
1696
|
async function getFirstUserMessage(sessionId) {
|
|
1422
|
-
const claudeDir =
|
|
1697
|
+
const claudeDir = join4(homedir3(), ".claude", "projects");
|
|
1423
1698
|
let projectDirs;
|
|
1424
1699
|
try {
|
|
1425
1700
|
projectDirs = readdirSync(claudeDir);
|
|
@@ -1427,7 +1702,7 @@ async function getFirstUserMessage(sessionId) {
|
|
|
1427
1702
|
return "";
|
|
1428
1703
|
}
|
|
1429
1704
|
for (const projDir of projectDirs) {
|
|
1430
|
-
const filePath =
|
|
1705
|
+
const filePath = join4(claudeDir, projDir, `${sessionId}.jsonl`);
|
|
1431
1706
|
try {
|
|
1432
1707
|
statSync(filePath);
|
|
1433
1708
|
} catch {
|
|
@@ -1560,7 +1835,8 @@ async function handleClaudeList(interaction) {
|
|
|
1560
1835
|
for (const [project, projectSessions] of grouped) {
|
|
1561
1836
|
const lines = projectSessions.map((s) => {
|
|
1562
1837
|
const status = s.isGenerating ? "\u{1F7E2} generating" : "\u26AA idle";
|
|
1563
|
-
|
|
1838
|
+
const modeEmoji = { auto: "\u26A1", plan: "\u{1F4CB}", normal: "\u{1F6E1}\uFE0F" }[s.mode] || "\u26A1";
|
|
1839
|
+
return `**${s.id}** \u2014 ${status} ${modeEmoji} ${s.mode} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}`;
|
|
1564
1840
|
});
|
|
1565
1841
|
embed.addFields({ name: `\u{1F4C1} ${project}`, value: lines.join("\n") });
|
|
1566
1842
|
}
|
|
@@ -1596,7 +1872,7 @@ async function handleClaudeContinue(interaction) {
|
|
|
1596
1872
|
const channel = interaction.channel;
|
|
1597
1873
|
const stream = continueSession(session.id);
|
|
1598
1874
|
await interaction.editReply("Continuing...");
|
|
1599
|
-
await handleOutputStream(stream, channel, session.id, session.verbose);
|
|
1875
|
+
await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
|
|
1600
1876
|
} catch (err) {
|
|
1601
1877
|
await interaction.editReply(`Error: ${err.message}`);
|
|
1602
1878
|
}
|
|
@@ -1697,6 +1973,24 @@ async function handleClaudeVerbose(interaction) {
|
|
|
1697
1973
|
ephemeral: true
|
|
1698
1974
|
});
|
|
1699
1975
|
}
|
|
1976
|
+
var MODE_LABELS = {
|
|
1977
|
+
auto: "\u26A1 Auto \u2014 full autonomy, no confirmations",
|
|
1978
|
+
plan: "\u{1F4CB} Plan \u2014 always plans before executing changes",
|
|
1979
|
+
normal: "\u{1F6E1}\uFE0F Normal \u2014 asks before destructive operations"
|
|
1980
|
+
};
|
|
1981
|
+
async function handleClaudeMode(interaction) {
|
|
1982
|
+
const session = getSessionByChannel(interaction.channelId);
|
|
1983
|
+
if (!session) {
|
|
1984
|
+
await interaction.reply({ content: "No session in this channel.", ephemeral: true });
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const mode = interaction.options.getString("mode", true);
|
|
1988
|
+
setMode(session.id, mode);
|
|
1989
|
+
await interaction.reply({
|
|
1990
|
+
content: `Mode set to **${MODE_LABELS[mode]}**`,
|
|
1991
|
+
ephemeral: true
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1700
1994
|
async function handleShell(interaction) {
|
|
1701
1995
|
if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
|
|
1702
1996
|
await interaction.reply({ content: "You are not authorized.", ephemeral: true });
|
|
@@ -1860,7 +2154,7 @@ ${list}`, ephemeral: true });
|
|
|
1860
2154
|
const channel = interaction.channel;
|
|
1861
2155
|
await interaction.editReply(`Running skill **${name}**...`);
|
|
1862
2156
|
const stream = sendPrompt(session.id, expanded);
|
|
1863
|
-
await handleOutputStream(stream, channel, session.id, session.verbose);
|
|
2157
|
+
await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
|
|
1864
2158
|
} catch (err) {
|
|
1865
2159
|
await interaction.editReply(`Error: ${err.message}`);
|
|
1866
2160
|
}
|
|
@@ -1928,9 +2222,377 @@ ${list}`, ephemeral: true });
|
|
|
1928
2222
|
}
|
|
1929
2223
|
}
|
|
1930
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
|
+
}
|
|
1931
2557
|
|
|
1932
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;
|
|
1933
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
|
+
}
|
|
1934
2596
|
async function handleMessage(message) {
|
|
1935
2597
|
if (message.author.bot) return;
|
|
1936
2598
|
const session = getSessionByChannel(message.channelId);
|
|
@@ -1959,12 +2621,42 @@ async function handleMessage(message) {
|
|
|
1959
2621
|
return;
|
|
1960
2622
|
}
|
|
1961
2623
|
}
|
|
1962
|
-
const
|
|
1963
|
-
|
|
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;
|
|
1964
2629
|
try {
|
|
1965
2630
|
const channel = message.channel;
|
|
1966
|
-
|
|
1967
|
-
|
|
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);
|
|
2659
|
+
await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
|
|
1968
2660
|
} catch (err) {
|
|
1969
2661
|
await message.reply({
|
|
1970
2662
|
content: `Error: ${err.message}`,
|
|
@@ -1974,6 +2666,12 @@ async function handleMessage(message) {
|
|
|
1974
2666
|
}
|
|
1975
2667
|
|
|
1976
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";
|
|
1977
2675
|
async function handleButton(interaction) {
|
|
1978
2676
|
if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
|
|
1979
2677
|
await interaction.reply({ content: "Not authorized.", ephemeral: true });
|
|
@@ -2005,7 +2703,7 @@ async function handleButton(interaction) {
|
|
|
2005
2703
|
const channel = interaction.channel;
|
|
2006
2704
|
const stream = continueSession(sessionId);
|
|
2007
2705
|
await interaction.editReply("Continuing...");
|
|
2008
|
-
await handleOutputStream(stream, channel, sessionId, session.verbose);
|
|
2706
|
+
await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
|
|
2009
2707
|
} catch (err) {
|
|
2010
2708
|
await interaction.editReply(`Error: ${err.message}`);
|
|
2011
2709
|
}
|
|
@@ -2039,7 +2737,79 @@ ${display}
|
|
|
2039
2737
|
const channel = interaction.channel;
|
|
2040
2738
|
const stream = sendPrompt(sessionId, optionText);
|
|
2041
2739
|
await interaction.editReply(`Selected option ${optionIndex + 1}`);
|
|
2042
|
-
await handleOutputStream(stream, channel, sessionId, session.verbose);
|
|
2740
|
+
await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
await interaction.editReply(`Error: ${err.message}`);
|
|
2743
|
+
}
|
|
2744
|
+
return;
|
|
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);
|
|
2043
2813
|
} catch (err) {
|
|
2044
2814
|
await interaction.editReply(`Error: ${err.message}`);
|
|
2045
2815
|
}
|
|
@@ -2048,7 +2818,8 @@ ${display}
|
|
|
2048
2818
|
if (customId.startsWith("answer:")) {
|
|
2049
2819
|
const parts = customId.split(":");
|
|
2050
2820
|
const sessionId = parts[1];
|
|
2051
|
-
const
|
|
2821
|
+
const hasQuestionIndex = /^\d+$/.test(parts[2]);
|
|
2822
|
+
const answer = hasQuestionIndex ? parts.slice(3).join(":") : parts.slice(2).join(":");
|
|
2052
2823
|
const session = getSession(sessionId);
|
|
2053
2824
|
if (!session) {
|
|
2054
2825
|
await interaction.reply({ content: "Session not found.", ephemeral: true });
|
|
@@ -2059,7 +2830,7 @@ ${display}
|
|
|
2059
2830
|
const channel = interaction.channel;
|
|
2060
2831
|
const stream = sendPrompt(sessionId, answer);
|
|
2061
2832
|
await interaction.editReply(`Answered: **${truncate(answer, 100)}**`);
|
|
2062
|
-
await handleOutputStream(stream, channel, sessionId, session.verbose);
|
|
2833
|
+
await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
|
|
2063
2834
|
} catch (err) {
|
|
2064
2835
|
await interaction.editReply(`Error: ${err.message}`);
|
|
2065
2836
|
}
|
|
@@ -2079,12 +2850,45 @@ ${display}
|
|
|
2079
2850
|
const channel = interaction.channel;
|
|
2080
2851
|
const stream = sendPrompt(sessionId, answer);
|
|
2081
2852
|
await interaction.editReply(`Answered: ${answer}`);
|
|
2082
|
-
await handleOutputStream(stream, channel, sessionId, session.verbose);
|
|
2853
|
+
await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
|
|
2083
2854
|
} catch (err) {
|
|
2084
2855
|
await interaction.editReply(`Error: ${err.message}`);
|
|
2085
2856
|
}
|
|
2086
2857
|
return;
|
|
2087
2858
|
}
|
|
2859
|
+
if (customId.startsWith("mode:")) {
|
|
2860
|
+
const parts = customId.split(":");
|
|
2861
|
+
const sessionId = parts[1];
|
|
2862
|
+
const newMode = parts[2];
|
|
2863
|
+
const session = getSession(sessionId);
|
|
2864
|
+
if (!session) {
|
|
2865
|
+
await interaction.reply({ content: "Session not found.", ephemeral: true });
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
setMode(sessionId, newMode);
|
|
2869
|
+
const labels = {
|
|
2870
|
+
auto: "\u26A1 Auto \u2014 full autonomy",
|
|
2871
|
+
plan: "\u{1F4CB} Plan \u2014 plans before changes",
|
|
2872
|
+
normal: "\u{1F6E1}\uFE0F Normal \u2014 asks before destructive ops"
|
|
2873
|
+
};
|
|
2874
|
+
await interaction.reply({
|
|
2875
|
+
content: `Mode switched to **${labels[newMode]}**`,
|
|
2876
|
+
ephemeral: true
|
|
2877
|
+
});
|
|
2878
|
+
try {
|
|
2879
|
+
const original = interaction.message;
|
|
2880
|
+
const updatedComponents = original.components.map((row) => {
|
|
2881
|
+
const first = row.components?.[0];
|
|
2882
|
+
if (first?.customId?.startsWith("mode:")) {
|
|
2883
|
+
return makeModeButtons(sessionId, newMode);
|
|
2884
|
+
}
|
|
2885
|
+
return row;
|
|
2886
|
+
});
|
|
2887
|
+
await original.edit({ components: updatedComponents });
|
|
2888
|
+
} catch {
|
|
2889
|
+
}
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2088
2892
|
await interaction.reply({ content: "Unknown button.", ephemeral: true });
|
|
2089
2893
|
}
|
|
2090
2894
|
async function handleSelectMenu(interaction) {
|
|
@@ -2093,8 +2897,48 @@ async function handleSelectMenu(interaction) {
|
|
|
2093
2897
|
return;
|
|
2094
2898
|
}
|
|
2095
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
|
+
}
|
|
2096
2939
|
if (customId.startsWith("answer-select:")) {
|
|
2097
|
-
const
|
|
2940
|
+
const afterPrefix = customId.slice(14);
|
|
2941
|
+
const sessionId = afterPrefix.includes(":") ? afterPrefix.split(":")[0] : afterPrefix;
|
|
2098
2942
|
const selected = interaction.values[0];
|
|
2099
2943
|
const session = getSession(sessionId);
|
|
2100
2944
|
if (!session) {
|
|
@@ -2106,7 +2950,7 @@ async function handleSelectMenu(interaction) {
|
|
|
2106
2950
|
const channel = interaction.channel;
|
|
2107
2951
|
const stream = sendPrompt(sessionId, selected);
|
|
2108
2952
|
await interaction.editReply(`Answered: **${truncate(selected, 100)}**`);
|
|
2109
|
-
await handleOutputStream(stream, channel, sessionId, session.verbose);
|
|
2953
|
+
await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
|
|
2110
2954
|
} catch (err) {
|
|
2111
2955
|
await interaction.editReply(`Error: ${err.message}`);
|
|
2112
2956
|
}
|
|
@@ -2125,7 +2969,7 @@ async function handleSelectMenu(interaction) {
|
|
|
2125
2969
|
const channel = interaction.channel;
|
|
2126
2970
|
const stream = sendPrompt(sessionId, selected);
|
|
2127
2971
|
await interaction.editReply(`Selected: ${truncate(selected, 100)}`);
|
|
2128
|
-
await handleOutputStream(stream, channel, sessionId, session.verbose);
|
|
2972
|
+
await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
|
|
2129
2973
|
} catch (err) {
|
|
2130
2974
|
await interaction.editReply(`Error: ${err.message}`);
|
|
2131
2975
|
}
|
|
@@ -2210,12 +3054,17 @@ async function startBot() {
|
|
|
2210
3054
|
return await handleAgent(interaction);
|
|
2211
3055
|
case "project":
|
|
2212
3056
|
return await handleProject(interaction);
|
|
3057
|
+
case "plugin":
|
|
3058
|
+
return await handlePlugin(interaction);
|
|
2213
3059
|
}
|
|
2214
3060
|
}
|
|
2215
3061
|
if (interaction.isAutocomplete()) {
|
|
2216
3062
|
if (interaction.commandName === "claude") {
|
|
2217
3063
|
return await handleClaudeAutocomplete(interaction);
|
|
2218
3064
|
}
|
|
3065
|
+
if (interaction.commandName === "plugin") {
|
|
3066
|
+
return await handlePluginAutocomplete(interaction);
|
|
3067
|
+
}
|
|
2219
3068
|
}
|
|
2220
3069
|
if (interaction.isButton()) {
|
|
2221
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",
|