agentcord 0.1.7 → 0.1.9

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.
@@ -53,7 +53,13 @@ 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")).addSubcommand((sub) => sub.setName("mode").setDescription("Set session mode (auto/plan/normal)").addStringOption((opt) => opt.setName("mode").setDescription("Session mode").setRequired(true).addChoices(
56
+ const session = new SlashCommandBuilder().setName("session").setDescription("Manage AI coding sessions").addSubcommand((sub) => sub.setName("new").setDescription("Create a new coding session").addStringOption((opt) => opt.setName("name").setDescription("Session name").setRequired(true)).addStringOption((opt) => opt.setName("provider").setDescription("AI provider").addChoices(
57
+ { name: "Claude Code", value: "claude" },
58
+ { name: "OpenAI Codex", value: "codex" }
59
+ )).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("resume").setDescription("Resume an existing session from terminal").addStringOption((opt) => opt.setName("session-id").setDescription("Provider session ID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("name").setDescription("Name for the Discord channel").setRequired(true)).addStringOption((opt) => opt.setName("provider").setDescription("AI provider").addChoices(
60
+ { name: "Claude Code", value: "claude" },
61
+ { name: "OpenAI Codex", value: "codex" }
62
+ )).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, gpt-5.3-codex)").setRequired(true))).addSubcommand((sub) => sub.setName("id").setDescription("Show the provider session ID for this channel")).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
63
  { name: "Auto \u2014 full autonomy", value: "auto" },
58
64
  { name: "Plan \u2014 plan before executing", value: "plan" },
59
65
  { name: "Normal \u2014 ask before destructive ops", value: "normal" }
@@ -69,11 +75,33 @@ function getCommandDefinitions() {
69
75
  { name: "General", value: "general" }
70
76
  ))).addSubcommand((sub) => sub.setName("list").setDescription("List available agent personas")).addSubcommand((sub) => sub.setName("clear").setDescription("Clear agent persona"));
71
77
  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"));
78
+ 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(
79
+ { name: "User \u2014 available everywhere", value: "user" },
80
+ { name: "Project \u2014 this project only", value: "project" },
81
+ { name: "Local \u2014 this directory only", value: "local" }
82
+ ))).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(
83
+ { name: "User", value: "user" },
84
+ { name: "Project", value: "project" },
85
+ { name: "Local", value: "local" }
86
+ ))).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(
87
+ { name: "User", value: "user" },
88
+ { name: "Project", value: "project" },
89
+ { name: "Local", value: "local" }
90
+ ))).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(
91
+ { name: "User", value: "user" },
92
+ { name: "Project", value: "project" },
93
+ { name: "Local", value: "local" }
94
+ ))).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(
95
+ { name: "User", value: "user" },
96
+ { name: "Project", value: "project" },
97
+ { name: "Local", value: "local" }
98
+ ))).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
99
  return [
73
- claude.toJSON(),
100
+ session.toJSON(),
74
101
  shell.toJSON(),
75
102
  agent.toJSON(),
76
- project.toJSON()
103
+ project.toJSON(),
104
+ plugin.toJSON()
77
105
  ];
78
106
  }
79
107
  async function registerCommands() {
@@ -100,18 +128,277 @@ async function registerCommands() {
100
128
 
101
129
  // src/command-handlers.ts
102
130
  import {
103
- EmbedBuilder as EmbedBuilder2,
131
+ EmbedBuilder as EmbedBuilder3,
104
132
  ChannelType
105
133
  } from "discord.js";
106
134
  import { readdirSync, statSync, createReadStream } from "fs";
107
- import { join as join3, basename } from "path";
108
- import { homedir as homedir2 } from "os";
135
+ import { join as join4, basename } from "path";
136
+ import { homedir as homedir3 } from "os";
109
137
  import { createInterface } from "readline";
110
138
 
111
139
  // src/session-manager.ts
112
140
  import { execFile } from "child_process";
113
141
  import { existsSync as existsSync3 } from "fs";
142
+
143
+ // src/providers/index.ts
144
+ import { execFileSync } from "child_process";
145
+
146
+ // src/providers/claude-provider.ts
114
147
  import { query } from "@anthropic-ai/claude-agent-sdk";
148
+ var TASK_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TaskList", "TaskGet"]);
149
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"]);
150
+ function extractImagePath(toolName, toolInput) {
151
+ try {
152
+ const data = JSON.parse(toolInput);
153
+ if (toolName === "Write" || toolName === "Read") {
154
+ const filePath = data.file_path;
155
+ if (filePath && IMAGE_EXTENSIONS.has(filePath.slice(filePath.lastIndexOf(".")).toLowerCase())) {
156
+ return filePath;
157
+ }
158
+ }
159
+ } catch {
160
+ }
161
+ return null;
162
+ }
163
+ function buildClaudeSystemPrompt(parts) {
164
+ if (parts.length > 0) {
165
+ return { type: "preset", preset: "claude_code", append: parts.join("\n\n") };
166
+ }
167
+ return { type: "preset", preset: "claude_code" };
168
+ }
169
+ var ClaudeProvider = class {
170
+ name = "claude";
171
+ supports(feature) {
172
+ return [
173
+ "tmux",
174
+ "resume_from_terminal",
175
+ "plugins",
176
+ "ask_user_question",
177
+ "mode_switching",
178
+ "continue"
179
+ ].includes(feature);
180
+ }
181
+ async *sendPrompt(prompt, options) {
182
+ const systemPrompt = buildClaudeSystemPrompt(options.systemPromptParts);
183
+ function buildQueryPrompt() {
184
+ if (typeof prompt === "string") return prompt;
185
+ const claudeBlocks = prompt.filter((b) => b.type !== "local_image");
186
+ const userMessage = {
187
+ type: "user",
188
+ message: { role: "user", content: claudeBlocks },
189
+ parent_tool_use_id: null,
190
+ session_id: ""
191
+ };
192
+ return (async function* () {
193
+ yield userMessage;
194
+ })();
195
+ }
196
+ let retried = false;
197
+ let resumeId = options.providerSessionId;
198
+ while (true) {
199
+ let failed = false;
200
+ const stream = query({
201
+ prompt: buildQueryPrompt(),
202
+ options: {
203
+ cwd: options.directory,
204
+ resume: resumeId,
205
+ abortController: options.abortController,
206
+ permissionMode: "bypassPermissions",
207
+ allowDangerouslySkipPermissions: true,
208
+ model: options.model,
209
+ systemPrompt,
210
+ includePartialMessages: true,
211
+ settingSources: ["user", "project", "local"]
212
+ }
213
+ });
214
+ yield* this.translateStream(stream, resumeId, retried, (f, r) => {
215
+ failed = f;
216
+ retried = r;
217
+ });
218
+ if (failed && !retried) {
219
+ retried = true;
220
+ resumeId = void 0;
221
+ yield { type: "session_init", providerSessionId: "" };
222
+ continue;
223
+ }
224
+ break;
225
+ }
226
+ }
227
+ async *continueSession(options) {
228
+ const systemPrompt = buildClaudeSystemPrompt(options.systemPromptParts);
229
+ let retried = false;
230
+ let resumeId = options.providerSessionId;
231
+ while (true) {
232
+ let failed = false;
233
+ const stream = query({
234
+ prompt: "",
235
+ options: {
236
+ cwd: options.directory,
237
+ ...resumeId ? { continue: true, resume: resumeId } : {},
238
+ abortController: options.abortController,
239
+ permissionMode: "bypassPermissions",
240
+ allowDangerouslySkipPermissions: true,
241
+ model: options.model,
242
+ systemPrompt,
243
+ includePartialMessages: true,
244
+ settingSources: ["user", "project", "local"]
245
+ }
246
+ });
247
+ yield* this.translateStream(stream, resumeId, retried, (f, r) => {
248
+ failed = f;
249
+ retried = r;
250
+ });
251
+ if (failed && !retried) {
252
+ retried = true;
253
+ resumeId = void 0;
254
+ yield { type: "session_init", providerSessionId: "" };
255
+ continue;
256
+ }
257
+ break;
258
+ }
259
+ }
260
+ async *translateStream(stream, resumeId, alreadyRetried, setRetry) {
261
+ let currentToolName = null;
262
+ let currentToolInput = "";
263
+ for await (const message of stream) {
264
+ if (message.type === "system" && "subtype" in message && message.subtype === "init") {
265
+ yield { type: "session_init", providerSessionId: message.session_id };
266
+ }
267
+ if (message.type === "stream_event") {
268
+ const event = message.event;
269
+ if (event?.type === "content_block_start") {
270
+ if (event.content_block?.type === "tool_use") {
271
+ currentToolName = event.content_block.name || "tool";
272
+ currentToolInput = "";
273
+ }
274
+ }
275
+ if (event?.type === "content_block_delta") {
276
+ if (event.delta?.type === "text_delta" && event.delta.text) {
277
+ yield { type: "text_delta", text: event.delta.text };
278
+ }
279
+ if (event.delta?.type === "input_json_delta" && event.delta.partial_json) {
280
+ currentToolInput += event.delta.partial_json;
281
+ }
282
+ }
283
+ if (event?.type === "content_block_stop") {
284
+ if (currentToolName) {
285
+ if (currentToolName === "AskUserQuestion") {
286
+ yield { type: "ask_user", questionsJson: currentToolInput };
287
+ } else if (TASK_TOOLS.has(currentToolName)) {
288
+ yield { type: "task", action: currentToolName, dataJson: currentToolInput };
289
+ } else {
290
+ yield { type: "tool_start", toolName: currentToolName, toolInput: currentToolInput };
291
+ }
292
+ const imagePath = extractImagePath(currentToolName, currentToolInput);
293
+ if (imagePath) {
294
+ yield { type: "image_file", filePath: imagePath };
295
+ }
296
+ currentToolName = null;
297
+ currentToolInput = "";
298
+ }
299
+ }
300
+ }
301
+ if (message.type === "user") {
302
+ const content = message.message?.content;
303
+ let resultText = "";
304
+ let toolName = "";
305
+ if (Array.isArray(content)) {
306
+ for (const block of content) {
307
+ if (block.type === "tool_result" && block.content) {
308
+ if (typeof block.content === "string") {
309
+ resultText += block.content;
310
+ } else if (Array.isArray(block.content)) {
311
+ for (const sub of block.content) {
312
+ if (sub.type === "text") resultText += sub.text;
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ if (resultText) {
319
+ yield { type: "tool_result", toolName, result: resultText };
320
+ }
321
+ }
322
+ if (message.type === "result") {
323
+ const r = message;
324
+ if (r.subtype !== "success" && !alreadyRetried && resumeId) {
325
+ setRetry(true, false);
326
+ break;
327
+ }
328
+ yield {
329
+ type: "result",
330
+ success: r.subtype === "success",
331
+ costUsd: r.total_cost_usd ?? 0,
332
+ durationMs: r.duration_ms ?? 0,
333
+ numTurns: r.num_turns ?? 0,
334
+ errors: r.errors ?? []
335
+ };
336
+ }
337
+ }
338
+ }
339
+ };
340
+
341
+ // src/providers/index.ts
342
+ var providers = /* @__PURE__ */ new Map();
343
+ providers.set("claude", new ClaudeProvider());
344
+ var codexLoaded = false;
345
+ var PROVIDER_PACKAGES = {
346
+ codex: "@openai/codex-sdk"
347
+ };
348
+ function isPackageInstalled(pkg) {
349
+ try {
350
+ execFileSync("npm", ["ls", pkg, "--global", "--depth=0"], { stdio: "pipe" });
351
+ return true;
352
+ } catch {
353
+ }
354
+ try {
355
+ execFileSync("npm", ["ls", pkg, "--depth=0"], { stdio: "pipe" });
356
+ return true;
357
+ } catch {
358
+ }
359
+ return false;
360
+ }
361
+ function installPackageGlobally(pkg) {
362
+ console.log(`[providers] Auto-installing ${pkg} globally...`);
363
+ execFileSync("npm", ["install", "-g", pkg], { stdio: "inherit" });
364
+ console.log(`[providers] ${pkg} installed successfully.`);
365
+ }
366
+ async function loadCodexProvider() {
367
+ const pkg = PROVIDER_PACKAGES.codex;
368
+ try {
369
+ const { CodexProvider } = await import("./codex-provider-672ILQC2.js");
370
+ providers.set("codex", new CodexProvider());
371
+ codexLoaded = true;
372
+ return;
373
+ } catch {
374
+ }
375
+ if (!isPackageInstalled(pkg)) {
376
+ try {
377
+ installPackageGlobally(pkg);
378
+ } catch (err) {
379
+ throw new Error(
380
+ `Failed to auto-install ${pkg}: ${err.message}. Install manually: npm install -g ${pkg}`
381
+ );
382
+ }
383
+ }
384
+ try {
385
+ const { CodexProvider } = await import("./codex-provider-672ILQC2.js");
386
+ providers.set("codex", new CodexProvider());
387
+ codexLoaded = true;
388
+ } catch (err) {
389
+ throw new Error(
390
+ `${pkg} is installed but failed to load: ${err.message}`
391
+ );
392
+ }
393
+ }
394
+ async function ensureProvider(name) {
395
+ if (providers.has(name)) return providers.get(name);
396
+ if (name === "codex" && !codexLoaded) {
397
+ await loadCodexProvider();
398
+ return providers.get("codex");
399
+ }
400
+ throw new Error(`Unknown provider: ${name}`);
401
+ }
115
402
 
116
403
  // src/persistence.ts
117
404
  import { readFile, writeFile, mkdir, rename } from "fs/promises";
@@ -245,6 +532,9 @@ async function saveProjects() {
245
532
  function getProject(name) {
246
533
  return projects[name];
247
534
  }
535
+ function getProjectByCategoryId(categoryId) {
536
+ return Object.values(projects).find((p) => p.categoryId === categoryId);
537
+ }
248
538
  function getOrCreateProject(name, directory, categoryId) {
249
539
  if (!projects[name]) {
250
540
  projects[name] = {
@@ -431,14 +721,19 @@ function detectNumberedOptions(text) {
431
721
  const options = [];
432
722
  const optionRegex = /^\s*(\d+)[.)]\s+(.+)$/;
433
723
  let firstOptionLine = -1;
724
+ let lastOptionLine = -1;
434
725
  for (let i = 0; i < lines.length; i++) {
435
726
  const match = lines[i].match(optionRegex);
436
727
  if (match) {
437
728
  if (firstOptionLine === -1) firstOptionLine = i;
729
+ lastOptionLine = i;
438
730
  options.push(match[2].trim());
439
731
  }
440
732
  }
441
733
  if (options.length < 2 || options.length > 6) return null;
734
+ if (options.some((o) => o.length > 80)) return null;
735
+ const linesAfter = lines.slice(lastOptionLine + 1).filter((l) => l.trim()).length;
736
+ if (linesAfter > 3) return null;
442
737
  const preamble = lines.slice(0, firstOptionLine).join(" ").toLowerCase();
443
738
  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
739
  return hasQuestion ? options : null;
@@ -449,7 +744,7 @@ function detectYesNoPrompt(text) {
449
744
  }
450
745
 
451
746
  // src/session-manager.ts
452
- var SESSION_PREFIX = "claude-";
747
+ var SESSION_PREFIX = "agentcord-";
453
748
  var MODE_PROMPTS = {
454
749
  auto: "",
455
750
  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.",
@@ -478,21 +773,27 @@ async function loadSessions() {
478
773
  const data = await sessionStore.read();
479
774
  if (!data) return;
480
775
  for (const s of data) {
481
- const exists = await tmuxSessionExists(s.tmuxName);
776
+ const provider = s.provider ?? "claude";
777
+ const providerSessionId = s.providerSessionId ?? s.claudeSessionId;
778
+ if (provider === "claude") {
779
+ const exists = await tmuxSessionExists(s.tmuxName);
780
+ if (!exists) {
781
+ try {
782
+ await tmux("new-session", "-d", "-s", s.tmuxName, "-c", s.directory);
783
+ } catch {
784
+ console.warn(`Could not recreate tmux session ${s.tmuxName}`);
785
+ }
786
+ }
787
+ }
482
788
  sessions.set(s.id, {
483
789
  ...s,
790
+ provider,
791
+ providerSessionId,
484
792
  verbose: s.verbose ?? false,
485
793
  mode: s.mode ?? "auto",
486
794
  isGenerating: false
487
795
  });
488
796
  channelToSession.set(s.channelId, s.id);
489
- if (!exists) {
490
- try {
491
- await tmux("new-session", "-d", "-s", s.tmuxName, "-c", s.directory);
492
- } catch {
493
- console.warn(`Could not recreate tmux session ${s.tmuxName}`);
494
- }
495
- }
496
797
  }
497
798
  console.log(`Restored ${sessions.size} session(s)`);
498
799
  }
@@ -504,8 +805,9 @@ async function saveSessions() {
504
805
  channelId: s.channelId,
505
806
  directory: s.directory,
506
807
  projectName: s.projectName,
808
+ provider: s.provider,
507
809
  tmuxName: s.tmuxName,
508
- claudeSessionId: s.claudeSessionId,
810
+ providerSessionId: s.providerSessionId,
509
811
  model: s.model,
510
812
  agentPersona: s.agentPersona,
511
813
  verbose: s.verbose || void 0,
@@ -518,7 +820,7 @@ async function saveSessions() {
518
820
  }
519
821
  await sessionStore.write(data);
520
822
  }
521
- async function createSession(name, directory, channelId, projectName, claudeSessionId) {
823
+ async function createSession(name, directory, channelId, projectName, provider = "claude", providerSessionId) {
522
824
  const resolvedDir = resolvePath(directory);
523
825
  if (!isPathAllowed(resolvedDir, config.allowedPaths)) {
524
826
  throw new Error(`Directory not in allowed paths: ${resolvedDir}`);
@@ -526,22 +828,27 @@ async function createSession(name, directory, channelId, projectName, claudeSess
526
828
  if (!existsSync3(resolvedDir)) {
527
829
  throw new Error(`Directory does not exist: ${resolvedDir}`);
528
830
  }
831
+ const providerInstance = await ensureProvider(provider);
832
+ const usesTmux = providerInstance.supports("tmux");
529
833
  let id = sanitizeSessionName(name);
530
- let tmuxName = `${SESSION_PREFIX}${id}`;
834
+ let tmuxName = usesTmux ? `${SESSION_PREFIX}${id}` : "";
531
835
  let suffix = 1;
532
- while (sessions.has(id) || await tmuxSessionExists(tmuxName)) {
836
+ while (sessions.has(id) || usesTmux && await tmuxSessionExists(tmuxName)) {
533
837
  suffix++;
534
838
  id = sanitizeSessionName(`${name}-${suffix}`);
535
- tmuxName = `${SESSION_PREFIX}${id}`;
839
+ if (usesTmux) tmuxName = `${SESSION_PREFIX}${id}`;
840
+ }
841
+ if (usesTmux) {
842
+ await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
536
843
  }
537
- await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
538
844
  const session = {
539
845
  id,
540
846
  channelId,
541
847
  directory: resolvedDir,
542
848
  projectName,
849
+ provider,
543
850
  tmuxName,
544
- claudeSessionId,
851
+ providerSessionId,
545
852
  verbose: false,
546
853
  mode: "auto",
547
854
  isGenerating: false,
@@ -571,9 +878,11 @@ async function endSession(id) {
571
878
  if (session.isGenerating && session._controller) {
572
879
  session._controller.abort();
573
880
  }
574
- try {
575
- await tmux("kill-session", "-t", session.tmuxName);
576
- } catch {
881
+ if (session.tmuxName) {
882
+ try {
883
+ await tmux("kill-session", "-t", session.tmuxName);
884
+ } catch {
885
+ }
577
886
  }
578
887
  channelToSession.delete(session.channelId);
579
888
  sessions.delete(id);
@@ -627,7 +936,7 @@ function setAgentPersona(sessionId, persona) {
627
936
  saveSessions();
628
937
  }
629
938
  }
630
- function buildSystemPrompt(session) {
939
+ function buildSystemPromptParts(session) {
631
940
  const parts = [];
632
941
  const personality = getPersonality(session.projectName);
633
942
  if (personality) parts.push(personality);
@@ -637,10 +946,14 @@ function buildSystemPrompt(session) {
637
946
  }
638
947
  const modePrompt = MODE_PROMPTS[session.mode];
639
948
  if (modePrompt) parts.push(modePrompt);
640
- if (parts.length > 0) {
641
- return { type: "preset", preset: "claude_code", append: parts.join("\n\n") };
949
+ return parts;
950
+ }
951
+ function resetProviderSession(sessionId) {
952
+ const session = sessions.get(sessionId);
953
+ if (session) {
954
+ session.providerSessionId = void 0;
955
+ saveSessions();
642
956
  }
643
- return { type: "preset", preset: "claude_code" };
644
957
  }
645
958
  async function* sendPrompt(sessionId, prompt) {
646
959
  const session = sessions.get(sessionId);
@@ -650,33 +963,25 @@ async function* sendPrompt(sessionId, prompt) {
650
963
  session._controller = controller;
651
964
  session.isGenerating = true;
652
965
  session.lastActivity = Date.now();
653
- const systemPrompt = buildSystemPrompt(session);
966
+ const provider = await ensureProvider(session.provider);
967
+ const systemPromptParts = buildSystemPromptParts(session);
654
968
  try {
655
- const stream = query({
656
- prompt,
657
- options: {
658
- cwd: session.directory,
659
- resume: session.claudeSessionId,
660
- abortController: controller,
661
- permissionMode: "bypassPermissions",
662
- allowDangerouslySkipPermissions: true,
663
- model: session.model,
664
- systemPrompt,
665
- includePartialMessages: true,
666
- settingSources: ["user", "project", "local"]
667
- }
969
+ const stream = provider.sendPrompt(prompt, {
970
+ directory: session.directory,
971
+ providerSessionId: session.providerSessionId,
972
+ model: session.model,
973
+ systemPromptParts,
974
+ abortController: controller
668
975
  });
669
- for await (const message of stream) {
670
- if (message.type === "system" && "subtype" in message && message.subtype === "init") {
671
- session.claudeSessionId = message.session_id;
976
+ for await (const event of stream) {
977
+ if (event.type === "session_init") {
978
+ session.providerSessionId = event.providerSessionId || void 0;
672
979
  await saveSessions();
673
980
  }
674
- if (message.type === "result") {
675
- if ("total_cost_usd" in message) {
676
- session.totalCost += message.total_cost_usd;
677
- }
981
+ if (event.type === "result") {
982
+ session.totalCost += event.costUsd;
678
983
  }
679
- yield message;
984
+ yield event;
680
985
  }
681
986
  session.messageCount++;
682
987
  } catch (err) {
@@ -699,32 +1004,25 @@ async function* continueSession(sessionId) {
699
1004
  session._controller = controller;
700
1005
  session.isGenerating = true;
701
1006
  session.lastActivity = Date.now();
702
- const systemPrompt = buildSystemPrompt(session);
1007
+ const provider = await ensureProvider(session.provider);
1008
+ const systemPromptParts = buildSystemPromptParts(session);
703
1009
  try {
704
- const stream = query({
705
- prompt: "",
706
- options: {
707
- cwd: session.directory,
708
- continue: true,
709
- resume: session.claudeSessionId,
710
- abortController: controller,
711
- permissionMode: "bypassPermissions",
712
- allowDangerouslySkipPermissions: true,
713
- model: session.model,
714
- systemPrompt,
715
- includePartialMessages: true,
716
- settingSources: ["user", "project", "local"]
717
- }
1010
+ const stream = provider.continueSession({
1011
+ directory: session.directory,
1012
+ providerSessionId: session.providerSessionId,
1013
+ model: session.model,
1014
+ systemPromptParts,
1015
+ abortController: controller
718
1016
  });
719
- for await (const message of stream) {
720
- if (message.type === "system" && "subtype" in message && message.subtype === "init") {
721
- session.claudeSessionId = message.session_id;
1017
+ for await (const event of stream) {
1018
+ if (event.type === "session_init") {
1019
+ session.providerSessionId = event.providerSessionId || void 0;
722
1020
  await saveSessions();
723
1021
  }
724
- if (message.type === "result" && "total_cost_usd" in message) {
725
- session.totalCost += message.total_cost_usd;
1022
+ if (event.type === "result") {
1023
+ session.totalCost += event.costUsd;
726
1024
  }
727
- yield message;
1025
+ yield event;
728
1026
  }
729
1027
  session.messageCount++;
730
1028
  } catch (err) {
@@ -745,16 +1043,21 @@ function abortSession(sessionId) {
745
1043
  const controller = session._controller;
746
1044
  if (controller) {
747
1045
  controller.abort();
1046
+ }
1047
+ if (session.isGenerating) {
1048
+ session.isGenerating = false;
1049
+ delete session._controller;
1050
+ saveSessions();
748
1051
  return true;
749
1052
  }
750
- return false;
1053
+ return !!controller;
751
1054
  }
752
1055
  function getAttachInfo(sessionId) {
753
1056
  const session = sessions.get(sessionId);
754
- if (!session) return null;
1057
+ if (!session || !session.tmuxName) return null;
755
1058
  return {
756
1059
  command: `tmux attach -t ${session.tmuxName}`,
757
- sessionId: session.claudeSessionId
1060
+ sessionId: session.providerSessionId
758
1061
  };
759
1062
  }
760
1063
  async function listTmuxSessions() {
@@ -777,16 +1080,209 @@ async function listTmuxSessions() {
777
1080
  }
778
1081
  }
779
1082
 
1083
+ // src/plugin-manager.ts
1084
+ import { execFile as execFile2 } from "child_process";
1085
+ import { readFile as readFile3 } from "fs/promises";
1086
+ import { join as join3 } from "path";
1087
+ import { homedir as homedir2 } from "os";
1088
+ var PLUGINS_DIR = join3(homedir2(), ".claude", "plugins");
1089
+ var MARKETPLACES_DIR = join3(PLUGINS_DIR, "marketplaces");
1090
+ function runClaude(args, cwd) {
1091
+ return new Promise((resolve2) => {
1092
+ execFile2("claude", args, {
1093
+ cwd: cwd || process.cwd(),
1094
+ timeout: 12e4,
1095
+ env: { ...process.env }
1096
+ }, (error, stdout, stderr) => {
1097
+ resolve2({
1098
+ stdout: stdout || "",
1099
+ stderr: stderr || "",
1100
+ code: error ? error.code ?? 1 : 0
1101
+ });
1102
+ });
1103
+ });
1104
+ }
1105
+ var CACHE_TTL_MS = 3e4;
1106
+ var installedCache = null;
1107
+ var availableCache = null;
1108
+ var marketplaceCache = null;
1109
+ function invalidateCache() {
1110
+ installedCache = null;
1111
+ availableCache = null;
1112
+ marketplaceCache = null;
1113
+ }
1114
+ async function listInstalled() {
1115
+ if (installedCache && Date.now() - installedCache.ts < CACHE_TTL_MS) {
1116
+ return installedCache.data;
1117
+ }
1118
+ const result = await runClaude(["plugin", "list", "--json"]);
1119
+ if (result.code !== 0) throw new Error(`Failed to list plugins: ${result.stderr}`);
1120
+ const data = JSON.parse(result.stdout);
1121
+ installedCache = { data, ts: Date.now() };
1122
+ return data;
1123
+ }
1124
+ async function listAvailable() {
1125
+ if (availableCache && Date.now() - availableCache.ts < CACHE_TTL_MS) {
1126
+ return availableCache.data;
1127
+ }
1128
+ const result = await runClaude(["plugin", "list", "--available", "--json"]);
1129
+ if (result.code !== 0) throw new Error(`Failed to list available plugins: ${result.stderr}`);
1130
+ const data = JSON.parse(result.stdout);
1131
+ availableCache = { data, ts: Date.now() };
1132
+ return data;
1133
+ }
1134
+ async function installPlugin(pluginId, scope, cwd) {
1135
+ const result = await runClaude(["plugin", "install", pluginId, "-s", scope], cwd);
1136
+ invalidateCache();
1137
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Install failed");
1138
+ return result.stdout.trim() || "Plugin installed successfully.";
1139
+ }
1140
+ async function uninstallPlugin(pluginId, scope, cwd) {
1141
+ const result = await runClaude(["plugin", "uninstall", pluginId, "-s", scope], cwd);
1142
+ invalidateCache();
1143
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Uninstall failed");
1144
+ return result.stdout.trim() || "Plugin uninstalled successfully.";
1145
+ }
1146
+ async function enablePlugin(pluginId, scope, cwd) {
1147
+ const result = await runClaude(["plugin", "enable", pluginId, "-s", scope], cwd);
1148
+ invalidateCache();
1149
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Enable failed");
1150
+ return result.stdout.trim() || "Plugin enabled.";
1151
+ }
1152
+ async function disablePlugin(pluginId, scope, cwd) {
1153
+ const result = await runClaude(["plugin", "disable", pluginId, "-s", scope], cwd);
1154
+ invalidateCache();
1155
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Disable failed");
1156
+ return result.stdout.trim() || "Plugin disabled.";
1157
+ }
1158
+ async function updatePlugin(pluginId, scope, cwd) {
1159
+ const result = await runClaude(["plugin", "update", pluginId, "-s", scope], cwd);
1160
+ invalidateCache();
1161
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Update failed");
1162
+ return result.stdout.trim() || "Plugin updated.";
1163
+ }
1164
+ async function listMarketplaces() {
1165
+ if (marketplaceCache && Date.now() - marketplaceCache.ts < CACHE_TTL_MS) {
1166
+ return marketplaceCache.data;
1167
+ }
1168
+ const result = await runClaude(["plugin", "marketplace", "list", "--json"]);
1169
+ if (result.code !== 0) throw new Error(`Failed to list marketplaces: ${result.stderr}`);
1170
+ const data = JSON.parse(result.stdout);
1171
+ marketplaceCache = { data, ts: Date.now() };
1172
+ return data;
1173
+ }
1174
+ async function addMarketplace(source) {
1175
+ const result = await runClaude(["plugin", "marketplace", "add", source]);
1176
+ invalidateCache();
1177
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Add marketplace failed");
1178
+ return result.stdout.trim() || "Marketplace added.";
1179
+ }
1180
+ async function removeMarketplace(name) {
1181
+ const result = await runClaude(["plugin", "marketplace", "remove", name]);
1182
+ invalidateCache();
1183
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Remove marketplace failed");
1184
+ return result.stdout.trim() || "Marketplace removed.";
1185
+ }
1186
+ async function updateMarketplaces(name) {
1187
+ const args = ["plugin", "marketplace", "update"];
1188
+ if (name) args.push(name);
1189
+ const result = await runClaude(args);
1190
+ invalidateCache();
1191
+ if (result.code !== 0) throw new Error(result.stderr || result.stdout || "Update marketplace failed");
1192
+ return result.stdout.trim() || "Marketplace(s) updated.";
1193
+ }
1194
+ async function getPluginDetail(pluginName, marketplaceName) {
1195
+ try {
1196
+ const marketplacePath = join3(MARKETPLACES_DIR, marketplaceName, ".claude-plugin", "marketplace.json");
1197
+ const raw = await readFile3(marketplacePath, "utf-8");
1198
+ const catalog = JSON.parse(raw);
1199
+ return catalog.plugins.find((p) => p.name === pluginName) || null;
1200
+ } catch {
1201
+ return null;
1202
+ }
1203
+ }
1204
+
780
1205
  // src/output-handler.ts
781
1206
  import {
782
- EmbedBuilder,
1207
+ AttachmentBuilder,
1208
+ EmbedBuilder as EmbedBuilder2,
783
1209
  ActionRowBuilder,
784
1210
  ButtonBuilder,
785
1211
  ButtonStyle,
786
1212
  StringSelectMenuBuilder
787
1213
  } from "discord.js";
1214
+ import { existsSync as existsSync4 } from "fs";
1215
+
1216
+ // src/codex-renderer.ts
1217
+ import { EmbedBuilder } from "discord.js";
1218
+ function renderCommandExecutionEmbed(event) {
1219
+ const statusEmoji = event.status === "completed" ? event.exitCode === 0 ? "\u2705" : "\u274C" : event.status === "failed" ? "\u274C" : "\u{1F504}";
1220
+ const embed = new EmbedBuilder().setColor(event.exitCode === 0 ? 3066993 : event.status === "failed" ? 15158332 : 15965202).setTitle(`${statusEmoji} Command`);
1221
+ embed.setDescription(`\`\`\`bash
1222
+ $ ${truncate(event.command, 900)}
1223
+ \`\`\``);
1224
+ if (event.output) {
1225
+ embed.addFields({
1226
+ name: "Output",
1227
+ value: `\`\`\`
1228
+ ${truncate(event.output, 900)}
1229
+ \`\`\``
1230
+ });
1231
+ }
1232
+ if (event.exitCode !== null) {
1233
+ embed.setFooter({ text: `Exit code: ${event.exitCode}` });
1234
+ }
1235
+ return embed;
1236
+ }
1237
+ function renderFileChangesEmbed(event) {
1238
+ const kindEmoji = { add: "+", update: "~", delete: "-" };
1239
+ const lines = event.changes.map(
1240
+ (c) => `${kindEmoji[c.changeKind] || "?"} ${c.filePath}`
1241
+ );
1242
+ return new EmbedBuilder().setColor(3447003).setTitle("\u{1F4C1} Files Changed").setDescription(`\`\`\`diff
1243
+ ${truncate(lines.join("\n"), 3900)}
1244
+ \`\`\``);
1245
+ }
1246
+ function renderReasoningEmbed(event) {
1247
+ return new EmbedBuilder().setColor(10181046).setTitle("\u{1F9E0} Reasoning").setDescription(truncate(event.text, 4e3));
1248
+ }
1249
+ function renderCodexTodoListEmbed(event) {
1250
+ const lines = event.items.map(
1251
+ (item) => `${item.completed ? "\u2705" : "\u2B1C"} ${item.text}`
1252
+ ).join("\n");
1253
+ return new EmbedBuilder().setColor(3447003).setTitle("\u{1F4CB} Task List").setDescription(truncate(lines, 4e3));
1254
+ }
1255
+
1256
+ // src/output-handler.ts
1257
+ var ABORT_PATTERNS = ["abort", "cancel", "interrupt", "killed", "signal"];
1258
+ function isAbortLike(err) {
1259
+ if (err.name === "AbortError") return true;
1260
+ const msg = (err.message || "").toLowerCase();
1261
+ return ABORT_PATTERNS.some((p) => msg.includes(p));
1262
+ }
1263
+ function isAbortError(errors) {
1264
+ return errors.some((e) => ABORT_PATTERNS.some((p) => e.toLowerCase().includes(p)));
1265
+ }
788
1266
  var expandableStore = /* @__PURE__ */ new Map();
789
1267
  var expandCounter = 0;
1268
+ var pendingAnswersStore = /* @__PURE__ */ new Map();
1269
+ var questionCountStore = /* @__PURE__ */ new Map();
1270
+ function setPendingAnswer(sessionId, questionIndex, answer) {
1271
+ if (!pendingAnswersStore.has(sessionId)) {
1272
+ pendingAnswersStore.set(sessionId, /* @__PURE__ */ new Map());
1273
+ }
1274
+ pendingAnswersStore.get(sessionId).set(questionIndex, answer);
1275
+ }
1276
+ function getPendingAnswers(sessionId) {
1277
+ return pendingAnswersStore.get(sessionId);
1278
+ }
1279
+ function clearPendingAnswers(sessionId) {
1280
+ pendingAnswersStore.delete(sessionId);
1281
+ questionCountStore.delete(sessionId);
1282
+ }
1283
+ function getQuestionCount(sessionId) {
1284
+ return questionCountStore.get(sessionId) || 0;
1285
+ }
790
1286
  setInterval(() => {
791
1287
  const now = Date.now();
792
1288
  const TTL = 10 * 60 * 1e3;
@@ -807,11 +1303,6 @@ function makeStopButton(sessionId) {
807
1303
  new ButtonBuilder().setCustomId(`stop:${sessionId}`).setLabel("Stop").setStyle(ButtonStyle.Danger)
808
1304
  );
809
1305
  }
810
- function makeCompletionButtons(sessionId) {
811
- return new ActionRowBuilder().addComponents(
812
- new ButtonBuilder().setCustomId(`continue:${sessionId}`).setLabel("Continue").setStyle(ButtonStyle.Primary)
813
- );
814
- }
815
1306
  function makeOptionButtons(sessionId, options) {
816
1307
  const rows = [];
817
1308
  const maxOptions = Math.min(options.length, 10);
@@ -847,6 +1338,10 @@ function makeYesNoButtons(sessionId) {
847
1338
  new ButtonBuilder().setCustomId(`confirm:${sessionId}:no`).setLabel("No").setStyle(ButtonStyle.Danger)
848
1339
  );
849
1340
  }
1341
+ function shouldSuppressCommandExecution(command) {
1342
+ const normalized = command.toLowerCase();
1343
+ return normalized.includes("total-recall");
1344
+ }
850
1345
  var MessageStreamer = class {
851
1346
  channel;
852
1347
  sessionId;
@@ -956,6 +1451,25 @@ var MessageStreamer = class {
956
1451
  this.currentMessage = null;
957
1452
  this.currentText = "";
958
1453
  }
1454
+ /** Discard accumulated text and delete the live message if one exists */
1455
+ async discard() {
1456
+ if (this.timer) {
1457
+ clearTimeout(this.timer);
1458
+ this.timer = null;
1459
+ }
1460
+ while (this.flushing) {
1461
+ await new Promise((r) => setTimeout(r, 50));
1462
+ }
1463
+ if (this.currentMessage) {
1464
+ try {
1465
+ await this.currentMessage.delete();
1466
+ } catch {
1467
+ }
1468
+ this.currentMessage = null;
1469
+ }
1470
+ this.currentText = "";
1471
+ this.dirty = false;
1472
+ }
959
1473
  getText() {
960
1474
  return this.currentText;
961
1475
  }
@@ -966,17 +1480,6 @@ var MessageStreamer = class {
966
1480
  }
967
1481
  }
968
1482
  };
969
- var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
970
- "AskUserQuestion",
971
- "EnterPlanMode",
972
- "ExitPlanMode"
973
- ]);
974
- var TASK_TOOLS = /* @__PURE__ */ new Set([
975
- "TaskCreate",
976
- "TaskUpdate",
977
- "TaskList",
978
- "TaskGet"
979
- ]);
980
1483
  var STATUS_EMOJI = {
981
1484
  pending: "\u2B1C",
982
1485
  // white square
@@ -987,26 +1490,64 @@ var STATUS_EMOJI = {
987
1490
  deleted: "\u{1F5D1}\uFE0F"
988
1491
  // wastebasket
989
1492
  };
990
- function renderAskUserQuestion(toolInput, sessionId) {
1493
+ function renderTaskToolEmbed(action, dataJson) {
991
1494
  try {
992
- const data = JSON.parse(toolInput);
1495
+ const data = JSON.parse(dataJson);
1496
+ if (action === "TaskCreate") {
1497
+ const embed = new EmbedBuilder2().setColor(3447003).setTitle("\u{1F4CB} New Task").setDescription(`**${data.subject || "Untitled"}**`);
1498
+ if (data.description) {
1499
+ embed.addFields({ name: "Details", value: truncate(data.description, 300) });
1500
+ }
1501
+ return embed;
1502
+ }
1503
+ if (action === "TaskUpdate") {
1504
+ const emoji = STATUS_EMOJI[data.status] || "\u{1F4CB}";
1505
+ const parts = [];
1506
+ if (data.status) parts.push(`${emoji} **${data.status}**`);
1507
+ if (data.subject) parts.push(data.subject);
1508
+ return new EmbedBuilder2().setColor(data.status === "completed" ? 3066993 : 15965202).setTitle(`Task #${data.taskId || "?"} Updated`).setDescription(parts.join(" \u2014 ") || "Updated");
1509
+ }
1510
+ return null;
1511
+ } catch {
1512
+ return null;
1513
+ }
1514
+ }
1515
+ function renderTaskListEmbed(resultText) {
1516
+ if (!resultText.trim()) return null;
1517
+ let formatted = resultText;
1518
+ for (const [status, emoji] of Object.entries(STATUS_EMOJI)) {
1519
+ formatted = formatted.replaceAll(status, `${emoji} ${status}`);
1520
+ }
1521
+ return new EmbedBuilder2().setColor(10181046).setTitle("\u{1F4CB} Task Board").setDescription(truncate(formatted, 4e3));
1522
+ }
1523
+ function renderAskUserQuestion(questionsJson, sessionId) {
1524
+ try {
1525
+ const data = JSON.parse(questionsJson);
993
1526
  const questions = data.questions;
994
1527
  if (!questions?.length) return null;
1528
+ const isMulti = questions.length > 1;
1529
+ if (isMulti) {
1530
+ clearPendingAnswers(sessionId);
1531
+ questionCountStore.set(sessionId, questions.length);
1532
+ }
995
1533
  const embeds = [];
996
1534
  const components = [];
997
- for (const q of questions) {
998
- const embed = new EmbedBuilder().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
1535
+ const btnPrefix = isMulti ? "pick" : "answer";
1536
+ const selectPrefix = isMulti ? "pick-select" : "answer-select";
1537
+ for (let qi = 0; qi < questions.length; qi++) {
1538
+ const q = questions[qi];
1539
+ const embed = new EmbedBuilder2().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
999
1540
  if (q.options?.length) {
1000
1541
  if (q.options.length <= 4) {
1001
1542
  const row = new ActionRowBuilder();
1002
1543
  for (let i = 0; i < q.options.length; i++) {
1003
1544
  row.addComponents(
1004
- new ButtonBuilder().setCustomId(`answer:${sessionId}:${q.options[i].label}`).setLabel(q.options[i].label.slice(0, 80)).setStyle(i === 0 ? ButtonStyle.Primary : ButtonStyle.Secondary)
1545
+ 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
1546
  );
1006
1547
  }
1007
1548
  components.push(row);
1008
1549
  } else {
1009
- const menu = new StringSelectMenuBuilder().setCustomId(`answer-select:${sessionId}`).setPlaceholder("Select an option...");
1550
+ const menu = new StringSelectMenuBuilder().setCustomId(`${selectPrefix}:${sessionId}:${qi}`).setPlaceholder("Select an option...");
1010
1551
  for (const opt of q.options) {
1011
1552
  menu.addOptions({
1012
1553
  label: opt.label.slice(0, 100),
@@ -1021,46 +1562,21 @@ function renderAskUserQuestion(toolInput, sessionId) {
1021
1562
  }
1022
1563
  embeds.push(embed);
1023
1564
  }
1024
- return { embeds, components };
1025
- } catch {
1026
- return null;
1027
- }
1028
- }
1029
- function renderTaskToolEmbed(toolName, toolInput) {
1030
- try {
1031
- const data = JSON.parse(toolInput);
1032
- if (toolName === "TaskCreate") {
1033
- const embed = new EmbedBuilder().setColor(3447003).setTitle("\u{1F4CB} New Task").setDescription(`**${data.subject || "Untitled"}**`);
1034
- if (data.description) {
1035
- embed.addFields({ name: "Details", value: truncate(data.description, 300) });
1036
- }
1037
- return embed;
1038
- }
1039
- if (toolName === "TaskUpdate") {
1040
- const emoji = STATUS_EMOJI[data.status] || "\u{1F4CB}";
1041
- const parts = [];
1042
- if (data.status) parts.push(`${emoji} **${data.status}**`);
1043
- if (data.subject) parts.push(data.subject);
1044
- return new EmbedBuilder().setColor(data.status === "completed" ? 3066993 : 15965202).setTitle(`Task #${data.taskId || "?"} Updated`).setDescription(parts.join(" \u2014 ") || "Updated");
1565
+ if (isMulti) {
1566
+ components.push(
1567
+ new ActionRowBuilder().addComponents(
1568
+ new ButtonBuilder().setCustomId(`submit-answers:${sessionId}`).setLabel("Submit Answers").setStyle(ButtonStyle.Success)
1569
+ )
1570
+ );
1045
1571
  }
1046
- return null;
1572
+ return { embeds, components };
1047
1573
  } catch {
1048
1574
  return null;
1049
1575
  }
1050
1576
  }
1051
- function renderTaskListEmbed(resultText) {
1052
- if (!resultText.trim()) return null;
1053
- let formatted = resultText;
1054
- for (const [status, emoji] of Object.entries(STATUS_EMOJI)) {
1055
- formatted = formatted.replaceAll(status, `${emoji} ${status}`);
1056
- }
1057
- return new EmbedBuilder().setColor(10181046).setTitle("\u{1F4CB} Task Board").setDescription(truncate(formatted, 4e3));
1058
- }
1059
- async function handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto") {
1577
+ async function handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto", _provider = "claude") {
1060
1578
  const streamer = new MessageStreamer(channel, sessionId);
1061
- let currentToolName = null;
1062
- let currentToolInput = "";
1063
- let lastFinishedToolName = null;
1579
+ let lastToolName = null;
1064
1580
  channel.sendTyping().catch(() => {
1065
1581
  });
1066
1582
  const typingInterval = setInterval(() => {
@@ -1068,103 +1584,78 @@ async function handleOutputStream(stream, channel, sessionId, verbose = false, m
1068
1584
  });
1069
1585
  }, 8e3);
1070
1586
  try {
1071
- for await (const message of stream) {
1072
- if (message.type === "stream_event") {
1073
- const event = message.event;
1074
- if (event?.type === "content_block_start") {
1075
- if (event.content_block?.type === "tool_use") {
1076
- await streamer.finalize();
1077
- currentToolName = event.content_block.name || "tool";
1078
- currentToolInput = "";
1079
- }
1587
+ for await (const event of stream) {
1588
+ switch (event.type) {
1589
+ case "text_delta": {
1590
+ streamer.append(event.text);
1591
+ break;
1080
1592
  }
1081
- if (event?.type === "content_block_delta") {
1082
- if (event.delta?.type === "text_delta" && event.delta.text) {
1083
- streamer.append(event.delta.text);
1084
- }
1085
- if (event.delta?.type === "input_json_delta" && event.delta.partial_json) {
1086
- currentToolInput += event.delta.partial_json;
1593
+ case "ask_user": {
1594
+ await streamer.discard();
1595
+ const rendered = renderAskUserQuestion(event.questionsJson, sessionId);
1596
+ if (rendered) {
1597
+ rendered.components.push(makeStopButton(sessionId));
1598
+ await channel.send({ embeds: rendered.embeds, components: rendered.components });
1087
1599
  }
1600
+ break;
1088
1601
  }
1089
- if (event?.type === "content_block_stop") {
1090
- if (currentToolName) {
1091
- const isUserFacing = USER_FACING_TOOLS.has(currentToolName);
1092
- const isTaskTool = TASK_TOOLS.has(currentToolName);
1093
- const showTool = verbose || isUserFacing || isTaskTool;
1094
- if (showTool) {
1095
- const taskEmbed = isTaskTool ? renderTaskToolEmbed(currentToolName, currentToolInput) : null;
1096
- if (taskEmbed) {
1097
- await channel.send({
1098
- embeds: [taskEmbed],
1099
- components: [makeStopButton(sessionId)]
1100
- });
1101
- } else if (currentToolName === "AskUserQuestion") {
1102
- const rendered = renderAskUserQuestion(currentToolInput, sessionId);
1103
- if (rendered) {
1104
- rendered.components.push(makeStopButton(sessionId));
1105
- await channel.send({ embeds: rendered.embeds, components: rendered.components });
1106
- }
1107
- } else if (!isTaskTool) {
1108
- const toolInput = currentToolInput;
1109
- const displayInput = toolInput.length > 1e3 ? truncate(toolInput, 1e3) : toolInput;
1110
- const embed = new EmbedBuilder().setColor(isUserFacing ? 15965202 : 3447003).setTitle(isUserFacing ? `Waiting for input: ${currentToolName}` : `Tool: ${currentToolName}`).setDescription(`\`\`\`json
1111
- ${displayInput}
1112
- \`\`\``);
1113
- const components = [makeStopButton(sessionId)];
1114
- if (toolInput.length > 1e3) {
1115
- const contentId = storeExpandable(toolInput);
1116
- components.unshift(
1117
- new ActionRowBuilder().addComponents(
1118
- new ButtonBuilder().setCustomId(`expand:${contentId}`).setLabel("Show Full Input").setStyle(ButtonStyle.Secondary)
1119
- )
1120
- );
1121
- }
1122
- await channel.send({ embeds: [embed], components });
1123
- }
1602
+ case "task": {
1603
+ await streamer.finalize();
1604
+ const isTaskResult = event.action === "TaskList" || event.action === "TaskGet";
1605
+ if (!isTaskResult) {
1606
+ const taskEmbed = renderTaskToolEmbed(event.action, event.dataJson);
1607
+ if (taskEmbed) {
1608
+ await channel.send({
1609
+ embeds: [taskEmbed],
1610
+ components: [makeStopButton(sessionId)]
1611
+ });
1124
1612
  }
1125
- lastFinishedToolName = currentToolName;
1126
- currentToolName = null;
1127
- currentToolInput = "";
1128
1613
  }
1614
+ lastToolName = event.action;
1615
+ break;
1129
1616
  }
1130
- }
1131
- if (message.type === "user") {
1132
- const showResult = verbose || lastFinishedToolName !== null && TASK_TOOLS.has(lastFinishedToolName);
1133
- if (!showResult) continue;
1134
- await streamer.finalize();
1135
- const content = message.message?.content;
1136
- let resultText = "";
1137
- if (Array.isArray(content)) {
1138
- for (const block of content) {
1139
- if (block.type === "tool_result" && block.content) {
1140
- if (typeof block.content === "string") {
1141
- resultText += block.content;
1142
- } else if (Array.isArray(block.content)) {
1143
- for (const sub of block.content) {
1144
- if (sub.type === "text") resultText += sub.text;
1145
- }
1146
- }
1617
+ case "tool_start": {
1618
+ await streamer.finalize();
1619
+ if (verbose) {
1620
+ const displayInput = event.toolInput.length > 1e3 ? truncate(event.toolInput, 1e3) : event.toolInput;
1621
+ const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Tool: ${event.toolName}`).setDescription(`\`\`\`json
1622
+ ${displayInput}
1623
+ \`\`\``);
1624
+ const components = [makeStopButton(sessionId)];
1625
+ if (event.toolInput.length > 1e3) {
1626
+ const contentId = storeExpandable(event.toolInput);
1627
+ components.unshift(
1628
+ new ActionRowBuilder().addComponents(
1629
+ new ButtonBuilder().setCustomId(`expand:${contentId}`).setLabel("Show Full Input").setStyle(ButtonStyle.Secondary)
1630
+ )
1631
+ );
1147
1632
  }
1633
+ await channel.send({ embeds: [embed], components });
1148
1634
  }
1635
+ lastToolName = event.toolName;
1636
+ break;
1149
1637
  }
1150
- if (resultText) {
1151
- const isTaskResult = lastFinishedToolName !== null && TASK_TOOLS.has(lastFinishedToolName);
1638
+ case "tool_result": {
1639
+ const isTaskResult = lastToolName !== null && (lastToolName === "TaskList" || lastToolName === "TaskGet");
1640
+ const showResult = verbose || isTaskResult;
1641
+ if (!showResult) break;
1642
+ await streamer.finalize();
1152
1643
  if (isTaskResult && !verbose) {
1153
- const boardEmbed = renderTaskListEmbed(resultText);
1644
+ const boardEmbed = renderTaskListEmbed(event.result);
1154
1645
  if (boardEmbed) {
1155
1646
  await channel.send({
1156
1647
  embeds: [boardEmbed],
1157
1648
  components: [makeStopButton(sessionId)]
1158
1649
  });
1159
1650
  }
1160
- } else {
1161
- const displayResult = resultText.length > 1e3 ? truncate(resultText, 1e3) : resultText;
1162
- const embed = new EmbedBuilder().setColor(1752220).setTitle("Tool Result").setDescription(`\`\`\`
1651
+ } else if (event.result) {
1652
+ const displayResult = event.result.length > 1e3 ? truncate(event.result, 1e3) : event.result;
1653
+ const embed = new EmbedBuilder2().setColor(1752220).setTitle("Tool Result").setDescription(`\`\`\`
1163
1654
  ${displayResult}
1164
1655
  \`\`\``);
1165
1656
  const components = [makeStopButton(sessionId)];
1166
- if (resultText.length > 1e3) {
1167
- const contentId = storeExpandable(resultText);
1657
+ if (event.result.length > 1e3) {
1658
+ const contentId = storeExpandable(event.result);
1168
1659
  components.unshift(
1169
1660
  new ActionRowBuilder().addComponents(
1170
1661
  new ButtonBuilder().setCustomId(`expand:${contentId}`).setLabel("Show Full Output").setStyle(ButtonStyle.Secondary)
@@ -1173,47 +1664,111 @@ ${displayResult}
1173
1664
  }
1174
1665
  await channel.send({ embeds: [embed], components });
1175
1666
  }
1667
+ break;
1668
+ }
1669
+ case "image_file": {
1670
+ if (existsSync4(event.filePath)) {
1671
+ await streamer.finalize();
1672
+ const attachment = new AttachmentBuilder(event.filePath);
1673
+ await channel.send({ files: [attachment] });
1674
+ }
1675
+ break;
1676
+ }
1677
+ // ── Codex-specific events ──
1678
+ case "command_execution": {
1679
+ if (shouldSuppressCommandExecution(event.command)) break;
1680
+ await streamer.finalize();
1681
+ const embed = renderCommandExecutionEmbed(event);
1682
+ await channel.send({
1683
+ embeds: [embed],
1684
+ components: [makeStopButton(sessionId)]
1685
+ });
1686
+ break;
1687
+ }
1688
+ case "file_change": {
1689
+ await streamer.finalize();
1690
+ const embed = renderFileChangesEmbed(event);
1691
+ await channel.send({
1692
+ embeds: [embed],
1693
+ components: [makeStopButton(sessionId)]
1694
+ });
1695
+ break;
1696
+ }
1697
+ case "reasoning": {
1698
+ if (verbose) {
1699
+ await streamer.finalize();
1700
+ const embed = renderReasoningEmbed(event);
1701
+ await channel.send({
1702
+ embeds: [embed],
1703
+ components: [makeStopButton(sessionId)]
1704
+ });
1705
+ }
1706
+ break;
1707
+ }
1708
+ case "todo_list": {
1709
+ await streamer.finalize();
1710
+ const embed = renderCodexTodoListEmbed(event);
1711
+ await channel.send({
1712
+ embeds: [embed],
1713
+ components: [makeStopButton(sessionId)]
1714
+ });
1715
+ break;
1716
+ }
1717
+ // ── Shared events ──
1718
+ case "result": {
1719
+ const lastText = streamer.getText();
1720
+ const cost = event.costUsd.toFixed(4);
1721
+ const duration = event.durationMs ? `${(event.durationMs / 1e3).toFixed(1)}s` : "unknown";
1722
+ const turns = event.numTurns || 0;
1723
+ const modeLabel = { auto: "Auto", plan: "Plan", normal: "Normal" }[mode] || "Auto";
1724
+ const statusLine = event.success ? `-# $${cost} | ${duration} | ${turns} turns | ${modeLabel}` : `-# Error | $${cost} | ${duration} | ${turns} turns`;
1725
+ streamer.append(`
1726
+ ${statusLine}`);
1727
+ if (!event.success && event.errors.length) {
1728
+ streamer.append(`
1729
+ \`\`\`
1730
+ ${event.errors.join("\n")}
1731
+ \`\`\``);
1732
+ }
1733
+ if (!event.success && !isAbortError(event.errors)) {
1734
+ resetProviderSession(sessionId);
1735
+ streamer.append("\n-# Session reset \u2014 next message will start a fresh provider session.");
1736
+ }
1737
+ await streamer.finalize();
1738
+ const components = [];
1739
+ const checkText = lastText || "";
1740
+ const options = detectNumberedOptions(checkText);
1741
+ if (options) {
1742
+ components.push(...makeOptionButtons(sessionId, options));
1743
+ } else if (detectYesNoPrompt(checkText)) {
1744
+ components.push(makeYesNoButtons(sessionId));
1745
+ }
1746
+ components.push(makeModeButtons(sessionId, mode));
1747
+ await channel.send({ components });
1748
+ break;
1176
1749
  }
1177
- }
1178
- if (message.type === "result") {
1179
- const lastText = streamer.getText();
1180
- await streamer.finalize();
1181
- const result = message;
1182
- const isSuccess = result.subtype === "success";
1183
- const cost = result.total_cost_usd?.toFixed(4) || "0.0000";
1184
- const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "unknown";
1185
- const turns = result.num_turns || 0;
1186
- const embed = new EmbedBuilder().setColor(isSuccess ? 3066993 : 15158332).setTitle(isSuccess ? "Completed" : "Error").addFields(
1187
- { name: "Cost", value: `$${cost}`, inline: true },
1188
- { name: "Duration", value: duration, inline: true },
1189
- { name: "Turns", value: `${turns}`, inline: true },
1190
- { name: "Mode", value: { auto: "\u26A1 Auto", plan: "\u{1F4CB} Plan", normal: "\u{1F6E1}\uFE0F Normal" }[mode] || "\u26A1 Auto", inline: true }
1191
- );
1192
- if (result.session_id) {
1193
- embed.setFooter({ text: `Session: ${result.session_id}` });
1194
- }
1195
- if (!isSuccess && result.errors?.length) {
1196
- embed.setDescription(result.errors.join("\n"));
1750
+ case "error": {
1751
+ await streamer.finalize();
1752
+ const embed = new EmbedBuilder2().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1753
+ ${event.message}
1754
+ \`\`\``);
1755
+ await channel.send({ embeds: [embed] });
1756
+ break;
1197
1757
  }
1198
- const components = [];
1199
- const checkText = lastText || result.result || "";
1200
- const options = detectNumberedOptions(checkText);
1201
- if (options) {
1202
- components.push(...makeOptionButtons(sessionId, options));
1203
- } else if (detectYesNoPrompt(checkText)) {
1204
- components.push(makeYesNoButtons(sessionId));
1758
+ case "session_init": {
1759
+ break;
1205
1760
  }
1206
- components.push(makeModeButtons(sessionId, mode));
1207
- components.push(makeCompletionButtons(sessionId));
1208
- await channel.send({ embeds: [embed], components });
1209
1761
  }
1210
1762
  }
1211
1763
  } catch (err) {
1212
1764
  await streamer.finalize();
1213
- if (err.name !== "AbortError") {
1214
- const embed = new EmbedBuilder().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1215
- ${err.message}
1216
- \`\`\``);
1765
+ const errMsg = err.message || "";
1766
+ if (!isAbortLike(err)) {
1767
+ resetProviderSession(sessionId);
1768
+ const embed = new EmbedBuilder2().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1769
+ ${errMsg}
1770
+ \`\`\`
1771
+ -# Session reset \u2014 next message will start a fresh provider session.`);
1217
1772
  await channel.send({ embeds: [embed] });
1218
1773
  }
1219
1774
  } finally {
@@ -1345,7 +1900,7 @@ async function ensureProjectCategory(guild, projectName, directory) {
1345
1900
  updateProjectCategory(projectName, category.id, logChannel2.id);
1346
1901
  return { category, logChannel: logChannel2 };
1347
1902
  }
1348
- async function handleClaude(interaction) {
1903
+ async function handleSession(interaction) {
1349
1904
  if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
1350
1905
  await interaction.reply({ content: "You are not authorized to use this bot.", ephemeral: true });
1351
1906
  return;
@@ -1353,66 +1908,90 @@ async function handleClaude(interaction) {
1353
1908
  const sub = interaction.options.getSubcommand();
1354
1909
  switch (sub) {
1355
1910
  case "new":
1356
- return handleClaudeNew(interaction);
1911
+ return handleSessionNew(interaction);
1357
1912
  case "resume":
1358
- return handleClaudeResume(interaction);
1913
+ return handleSessionResume(interaction);
1359
1914
  case "list":
1360
- return handleClaudeList(interaction);
1915
+ return handleSessionList(interaction);
1361
1916
  case "end":
1362
- return handleClaudeEnd(interaction);
1917
+ return handleSessionEnd(interaction);
1363
1918
  case "continue":
1364
- return handleClaudeContinue(interaction);
1919
+ return handleSessionContinue(interaction);
1365
1920
  case "stop":
1366
- return handleClaudeStop(interaction);
1921
+ return handleSessionStop(interaction);
1367
1922
  case "output":
1368
- return handleClaudeOutput(interaction);
1923
+ return handleSessionOutput(interaction);
1369
1924
  case "attach":
1370
- return handleClaudeAttach(interaction);
1925
+ return handleSessionAttach(interaction);
1371
1926
  case "sync":
1372
- return handleClaudeSync(interaction);
1927
+ return handleSessionSync(interaction);
1928
+ case "id":
1929
+ return handleSessionId(interaction);
1373
1930
  case "model":
1374
- return handleClaudeModel(interaction);
1931
+ return handleSessionModel(interaction);
1375
1932
  case "verbose":
1376
- return handleClaudeVerbose(interaction);
1933
+ return handleSessionVerbose(interaction);
1377
1934
  case "mode":
1378
- return handleClaudeMode(interaction);
1935
+ return handleSessionMode(interaction);
1379
1936
  default:
1380
1937
  await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
1381
1938
  }
1382
1939
  }
1383
- async function handleClaudeNew(interaction) {
1940
+ var PROVIDER_LABELS = {
1941
+ claude: "Claude Code",
1942
+ codex: "OpenAI Codex"
1943
+ };
1944
+ var PROVIDER_COLORS = {
1945
+ claude: 3447003,
1946
+ codex: 1090431
1947
+ };
1948
+ async function handleSessionNew(interaction) {
1384
1949
  const name = interaction.options.getString("name", true);
1385
- const directory = interaction.options.getString("directory") || config.defaultDirectory;
1950
+ const provider = interaction.options.getString("provider") || "claude";
1951
+ let directory = interaction.options.getString("directory");
1952
+ if (!directory) {
1953
+ const parentId = interaction.channel?.parentId;
1954
+ if (parentId) {
1955
+ const project = getProjectByCategoryId(parentId);
1956
+ if (project) directory = project.directory;
1957
+ }
1958
+ directory = directory || config.defaultDirectory;
1959
+ }
1386
1960
  await interaction.deferReply();
1387
1961
  let channel;
1388
1962
  try {
1389
1963
  const guild = interaction.guild;
1390
1964
  const projectName = projectNameFromDir(directory);
1391
1965
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1392
- const session = await createSession(name, directory, "pending", projectName);
1966
+ const session = await createSession(name, directory, "pending", projectName, provider);
1393
1967
  channel = await guild.channels.create({
1394
- name: `claude-${session.id}`,
1968
+ name: `${provider}-${session.id}`,
1395
1969
  type: ChannelType.GuildText,
1396
1970
  parent: category.id,
1397
- topic: `Claude session | Dir: ${directory}`
1971
+ topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
1398
1972
  });
1399
1973
  linkChannel(session.id, channel.id);
1400
- const embed = new EmbedBuilder2().setColor(3066993).setTitle(`Session Created: ${session.id}`).addFields(
1401
- { name: "Channel", value: `#claude-${session.id}`, inline: true },
1974
+ const fields = [
1975
+ { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
1976
+ { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
1402
1977
  { name: "Directory", value: session.directory, inline: true },
1403
- { name: "Project", value: projectName, inline: true },
1404
- { name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1405
- );
1978
+ { name: "Project", value: projectName, inline: true }
1979
+ ];
1980
+ if (session.tmuxName) {
1981
+ fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1982
+ }
1983
+ const embed = new EmbedBuilder3().setColor(3066993).setTitle(`Session Created: ${session.id}`).addFields(fields);
1406
1984
  await interaction.editReply({ embeds: [embed] });
1407
- log(`Session "${session.id}" created by ${interaction.user.tag} in ${directory}`);
1408
- await channel.send({
1409
- embeds: [
1410
- new EmbedBuilder2().setColor(3447003).setTitle("Claude Code Session").setDescription("Type a message to send it to Claude. Use `/claude stop` to cancel generation.").addFields(
1411
- { name: "Directory", value: `\`${session.directory}\``, inline: false },
1412
- { name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1413
- )
1414
- ]
1415
- });
1985
+ log(`Session "${session.id}" (${provider}) created by ${interaction.user.tag} in ${directory}`);
1986
+ const welcomeEmbed = new EmbedBuilder3().setColor(PROVIDER_COLORS[provider]).setTitle(`${PROVIDER_LABELS[provider]} Session`).setDescription(`Type a message to send it to the AI. Use \`/session stop\` to cancel generation.`);
1987
+ const welcomeFields = [
1988
+ { name: "Directory", value: `\`${session.directory}\``, inline: false }
1989
+ ];
1990
+ if (session.tmuxName) {
1991
+ welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1992
+ }
1993
+ welcomeEmbed.addFields(welcomeFields);
1994
+ await channel.send({ embeds: [welcomeEmbed] });
1416
1995
  } catch (err) {
1417
1996
  if (channel) {
1418
1997
  try {
@@ -1424,7 +2003,7 @@ async function handleClaudeNew(interaction) {
1424
2003
  }
1425
2004
  }
1426
2005
  function discoverLocalSessions() {
1427
- const claudeDir = join3(homedir2(), ".claude", "projects");
2006
+ const claudeDir = join4(homedir3(), ".claude", "projects");
1428
2007
  const results = [];
1429
2008
  let projectDirs;
1430
2009
  try {
@@ -1433,7 +2012,7 @@ function discoverLocalSessions() {
1433
2012
  return [];
1434
2013
  }
1435
2014
  for (const projDir of projectDirs) {
1436
- const projPath = join3(claudeDir, projDir);
2015
+ const projPath = join4(claudeDir, projDir);
1437
2016
  let files;
1438
2017
  try {
1439
2018
  files = readdirSync(projPath);
@@ -1447,7 +2026,7 @@ function discoverLocalSessions() {
1447
2026
  const sessionId = file.replace(".jsonl", "");
1448
2027
  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
2028
  try {
1450
- const mtime = statSync(join3(projPath, file)).mtimeMs;
2029
+ const mtime = statSync(join4(projPath, file)).mtimeMs;
1451
2030
  results.push({ id: sessionId, project, mtime, firstMessage: "" });
1452
2031
  } catch {
1453
2032
  continue;
@@ -1458,7 +2037,7 @@ function discoverLocalSessions() {
1458
2037
  return results;
1459
2038
  }
1460
2039
  async function getFirstUserMessage(sessionId) {
1461
- const claudeDir = join3(homedir2(), ".claude", "projects");
2040
+ const claudeDir = join4(homedir3(), ".claude", "projects");
1462
2041
  let projectDirs;
1463
2042
  try {
1464
2043
  projectDirs = readdirSync(claudeDir);
@@ -1466,7 +2045,7 @@ async function getFirstUserMessage(sessionId) {
1466
2045
  return "";
1467
2046
  }
1468
2047
  for (const projDir of projectDirs) {
1469
- const filePath = join3(claudeDir, projDir, `${sessionId}.jsonl`);
2048
+ const filePath = join4(claudeDir, projDir, `${sessionId}.jsonl`);
1470
2049
  try {
1471
2050
  statSync(filePath);
1472
2051
  } catch {
@@ -1512,7 +2091,7 @@ function formatTimeAgo(mtime) {
1512
2091
  if (ago < 864e5) return `${Math.floor(ago / 36e5)}h ago`;
1513
2092
  return `${Math.floor(ago / 864e5)}d ago`;
1514
2093
  }
1515
- async function handleClaudeAutocomplete(interaction) {
2094
+ async function handleSessionAutocomplete(interaction) {
1516
2095
  const focused = interaction.options.getFocused();
1517
2096
  const localSessions = discoverLocalSessions();
1518
2097
  const filtered = focused ? localSessions.filter((s) => s.id.includes(focused.toLowerCase()) || s.project.toLowerCase().includes(focused.toLowerCase())) : localSessions;
@@ -1527,17 +2106,20 @@ async function handleClaudeAutocomplete(interaction) {
1527
2106
  );
1528
2107
  await interaction.respond(choices);
1529
2108
  }
1530
- async function handleClaudeResume(interaction) {
1531
- const claudeSessionId = interaction.options.getString("session-id", true);
2109
+ async function handleSessionResume(interaction) {
2110
+ const providerSessionId = interaction.options.getString("session-id", true);
1532
2111
  const name = interaction.options.getString("name", true);
2112
+ const provider = interaction.options.getString("provider") || "claude";
1533
2113
  const directory = interaction.options.getString("directory") || config.defaultDirectory;
1534
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1535
- if (!uuidRegex.test(claudeSessionId)) {
1536
- await interaction.reply({
1537
- content: "Invalid session ID. Expected a UUID like `9815d35d-6508-476e-8c40-40effa4ffd6b`.",
1538
- ephemeral: true
1539
- });
1540
- return;
2114
+ if (provider === "claude") {
2115
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2116
+ if (!uuidRegex.test(providerSessionId)) {
2117
+ await interaction.reply({
2118
+ content: "Invalid session ID. Expected a UUID like `9815d35d-6508-476e-8c40-40effa4ffd6b`.",
2119
+ ephemeral: true
2120
+ });
2121
+ return;
2122
+ }
1541
2123
  }
1542
2124
  await interaction.deferReply();
1543
2125
  let channel;
@@ -1545,32 +2127,39 @@ async function handleClaudeResume(interaction) {
1545
2127
  const guild = interaction.guild;
1546
2128
  const projectName = projectNameFromDir(directory);
1547
2129
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1548
- const session = await createSession(name, directory, "pending", projectName, claudeSessionId);
2130
+ const session = await createSession(name, directory, "pending", projectName, provider, providerSessionId);
1549
2131
  channel = await guild.channels.create({
1550
- name: `claude-${session.id}`,
2132
+ name: `${provider}-${session.id}`,
1551
2133
  type: ChannelType.GuildText,
1552
2134
  parent: category.id,
1553
- topic: `Claude session (resumed) | Dir: ${directory}`
2135
+ topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
1554
2136
  });
1555
2137
  linkChannel(session.id, channel.id);
1556
- const embed = new EmbedBuilder2().setColor(15105570).setTitle(`Session Resumed: ${session.id}`).addFields(
1557
- { name: "Channel", value: `#claude-${session.id}`, inline: true },
2138
+ const fields = [
2139
+ { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
2140
+ { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
1558
2141
  { name: "Directory", value: session.directory, inline: true },
1559
2142
  { name: "Project", value: projectName, inline: true },
1560
- { name: "Claude Session", value: `\`${claudeSessionId}\``, inline: false },
1561
- { name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1562
- );
2143
+ { name: "Provider Session", value: `\`${providerSessionId}\``, inline: false }
2144
+ ];
2145
+ if (session.tmuxName) {
2146
+ fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2147
+ }
2148
+ const embed = new EmbedBuilder3().setColor(15105570).setTitle(`Session Resumed: ${session.id}`).addFields(fields);
1563
2149
  await interaction.editReply({ embeds: [embed] });
1564
- log(`Session "${session.id}" (resumed ${claudeSessionId}) created by ${interaction.user.tag} in ${directory}`);
2150
+ log(`Session "${session.id}" (${provider}, resumed ${providerSessionId}) created by ${interaction.user.tag} in ${directory}`);
2151
+ const welcomeFields = [
2152
+ { name: "Directory", value: `\`${session.directory}\``, inline: false },
2153
+ { name: "Provider Session", value: `\`${providerSessionId}\``, inline: false }
2154
+ ];
2155
+ if (session.tmuxName) {
2156
+ welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2157
+ }
1565
2158
  await channel.send({
1566
2159
  embeds: [
1567
- new EmbedBuilder2().setColor(15105570).setTitle("Claude Code Session (Resumed)").setDescription(
1568
- "This session is linked to an existing Claude Code conversation. Type a message to continue the conversation from Discord."
1569
- ).addFields(
1570
- { name: "Directory", value: `\`${session.directory}\``, inline: false },
1571
- { name: "Claude Session", value: `\`${claudeSessionId}\``, inline: false },
1572
- { name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1573
- )
2160
+ new EmbedBuilder3().setColor(15105570).setTitle(`${PROVIDER_LABELS[provider]} Session (Resumed)`).setDescription(
2161
+ `This session is linked to an existing ${PROVIDER_LABELS[provider]} conversation. Type a message to continue the conversation from Discord.`
2162
+ ).addFields(welcomeFields)
1574
2163
  ]
1575
2164
  });
1576
2165
  } catch (err) {
@@ -1583,7 +2172,7 @@ async function handleClaudeResume(interaction) {
1583
2172
  await interaction.editReply(`Failed to resume session: ${err.message}`);
1584
2173
  }
1585
2174
  }
1586
- async function handleClaudeList(interaction) {
2175
+ async function handleSessionList(interaction) {
1587
2176
  const allSessions = getAllSessions();
1588
2177
  if (allSessions.length === 0) {
1589
2178
  await interaction.reply({ content: "No active sessions.", ephemeral: true });
@@ -1595,33 +2184,42 @@ async function handleClaudeList(interaction) {
1595
2184
  arr.push(s);
1596
2185
  grouped.set(s.projectName, arr);
1597
2186
  }
1598
- const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Active Sessions (${allSessions.length})`);
2187
+ const embed = new EmbedBuilder3().setColor(3447003).setTitle(`Active Sessions (${allSessions.length})`);
1599
2188
  for (const [project, projectSessions] of grouped) {
1600
2189
  const lines = projectSessions.map((s) => {
1601
2190
  const status = s.isGenerating ? "\u{1F7E2} generating" : "\u26AA idle";
1602
2191
  const modeEmoji = { auto: "\u26A1", plan: "\u{1F4CB}", normal: "\u{1F6E1}\uFE0F" }[s.mode] || "\u26A1";
1603
- return `**${s.id}** \u2014 ${status} ${modeEmoji} ${s.mode} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}`;
2192
+ const providerTag = `[${s.provider}]`;
2193
+ return `**${s.id}** ${providerTag} \u2014 ${status} ${modeEmoji} ${s.mode} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}`;
1604
2194
  });
1605
2195
  embed.addFields({ name: `\u{1F4C1} ${project}`, value: lines.join("\n") });
1606
2196
  }
1607
2197
  await interaction.reply({ embeds: [embed] });
1608
2198
  }
1609
- async function handleClaudeEnd(interaction) {
2199
+ async function handleSessionEnd(interaction) {
1610
2200
  const session = getSessionByChannel(interaction.channelId);
1611
2201
  if (!session) {
1612
2202
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
1613
2203
  return;
1614
2204
  }
2205
+ const channel = interaction.channel;
1615
2206
  await interaction.deferReply();
1616
2207
  try {
1617
2208
  await endSession(session.id);
1618
- await interaction.editReply(`Session "${session.id}" ended. You can delete this channel.`);
1619
2209
  log(`Session "${session.id}" ended by ${interaction.user.tag}`);
2210
+ await interaction.editReply(`Session "${session.id}" ended. Deleting channel...`);
2211
+ setTimeout(async () => {
2212
+ try {
2213
+ await channel?.delete();
2214
+ } catch (err) {
2215
+ log(`Failed to delete channel for session "${session.id}": ${err.message}`);
2216
+ }
2217
+ }, 2e3);
1620
2218
  } catch (err) {
1621
2219
  await interaction.editReply(`Failed to end session: ${err.message}`);
1622
2220
  }
1623
2221
  }
1624
- async function handleClaudeContinue(interaction) {
2222
+ async function handleSessionContinue(interaction) {
1625
2223
  const session = getSessionByChannel(interaction.channelId);
1626
2224
  if (!session) {
1627
2225
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1636,12 +2234,12 @@ async function handleClaudeContinue(interaction) {
1636
2234
  const channel = interaction.channel;
1637
2235
  const stream = continueSession(session.id);
1638
2236
  await interaction.editReply("Continuing...");
1639
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
2237
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
1640
2238
  } catch (err) {
1641
2239
  await interaction.editReply(`Error: ${err.message}`);
1642
2240
  }
1643
2241
  }
1644
- async function handleClaudeStop(interaction) {
2242
+ async function handleSessionStop(interaction) {
1645
2243
  const session = getSessionByChannel(interaction.channelId);
1646
2244
  if (!session) {
1647
2245
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1653,18 +2251,18 @@ async function handleClaudeStop(interaction) {
1653
2251
  ephemeral: true
1654
2252
  });
1655
2253
  }
1656
- async function handleClaudeOutput(interaction) {
2254
+ async function handleSessionOutput(interaction) {
1657
2255
  const session = getSessionByChannel(interaction.channelId);
1658
2256
  if (!session) {
1659
2257
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
1660
2258
  return;
1661
2259
  }
1662
2260
  await interaction.reply({
1663
- content: "Conversation history is managed by the Claude Code SDK. Use `/claude attach` to view the full terminal history.",
2261
+ content: "Conversation history is managed by the provider SDK. Use `/session attach` to view the full terminal history.",
1664
2262
  ephemeral: true
1665
2263
  });
1666
2264
  }
1667
- async function handleClaudeAttach(interaction) {
2265
+ async function handleSessionAttach(interaction) {
1668
2266
  const session = getSessionByChannel(interaction.channelId);
1669
2267
  if (!session) {
1670
2268
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1672,10 +2270,13 @@ async function handleClaudeAttach(interaction) {
1672
2270
  }
1673
2271
  const info = getAttachInfo(session.id);
1674
2272
  if (!info) {
1675
- await interaction.reply({ content: "Session not found.", ephemeral: true });
2273
+ await interaction.reply({
2274
+ content: `Terminal attach is not available for ${PROVIDER_LABELS[session.provider]} sessions.`,
2275
+ ephemeral: true
2276
+ });
1676
2277
  return;
1677
2278
  }
1678
- const embed = new EmbedBuilder2().setColor(10181046).setTitle("Terminal Access").addFields(
2279
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle("Terminal Access").addFields(
1679
2280
  { name: "Attach to tmux", value: `\`\`\`
1680
2281
  ${info.command}
1681
2282
  \`\`\`` }
@@ -1690,7 +2291,7 @@ cd ${session.directory} && claude --resume ${info.sessionId}
1690
2291
  }
1691
2292
  await interaction.reply({ embeds: [embed], ephemeral: true });
1692
2293
  }
1693
- async function handleClaudeSync(interaction) {
2294
+ async function handleSessionSync(interaction) {
1694
2295
  await interaction.deferReply();
1695
2296
  const guild = interaction.guild;
1696
2297
  const tmuxSessions = await listTmuxSessions();
@@ -1705,16 +2306,36 @@ async function handleClaudeSync(interaction) {
1705
2306
  name: `claude-${tmuxSession.id}`,
1706
2307
  type: ChannelType.GuildText,
1707
2308
  parent: category.id,
1708
- topic: `Claude session (synced) | Dir: ${tmuxSession.directory}`
2309
+ topic: `Claude Code session (synced) | Dir: ${tmuxSession.directory}`
1709
2310
  });
1710
- await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName);
2311
+ await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName, "claude");
1711
2312
  synced++;
1712
2313
  }
1713
2314
  await interaction.editReply(
1714
2315
  synced > 0 ? `Synced ${synced} orphaned session(s).` : "No orphaned sessions found."
1715
2316
  );
1716
2317
  }
1717
- async function handleClaudeModel(interaction) {
2318
+ async function handleSessionId(interaction) {
2319
+ const session = getSessionByChannel(interaction.channelId);
2320
+ if (!session) {
2321
+ await interaction.reply({ content: "No session in this channel.", ephemeral: true });
2322
+ return;
2323
+ }
2324
+ const providerSessionId = session.providerSessionId;
2325
+ if (!providerSessionId) {
2326
+ await interaction.reply({
2327
+ content: "No provider session ID yet. Send a message first to initialize the session.",
2328
+ ephemeral: true
2329
+ });
2330
+ return;
2331
+ }
2332
+ await interaction.reply({
2333
+ content: `**Provider session ID** (${session.provider}):
2334
+ \`${providerSessionId}\``,
2335
+ ephemeral: true
2336
+ });
2337
+ }
2338
+ async function handleSessionModel(interaction) {
1718
2339
  const session = getSessionByChannel(interaction.channelId);
1719
2340
  if (!session) {
1720
2341
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1724,7 +2345,7 @@ async function handleClaudeModel(interaction) {
1724
2345
  setModel(session.id, model);
1725
2346
  await interaction.reply({ content: `Model set to \`${model}\` for this session.`, ephemeral: true });
1726
2347
  }
1727
- async function handleClaudeVerbose(interaction) {
2348
+ async function handleSessionVerbose(interaction) {
1728
2349
  const session = getSessionByChannel(interaction.channelId);
1729
2350
  if (!session) {
1730
2351
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1742,7 +2363,7 @@ var MODE_LABELS = {
1742
2363
  plan: "\u{1F4CB} Plan \u2014 always plans before executing changes",
1743
2364
  normal: "\u{1F6E1}\uFE0F Normal \u2014 asks before destructive operations"
1744
2365
  };
1745
- async function handleClaudeMode(interaction) {
2366
+ async function handleSessionMode(interaction) {
1746
2367
  const session = getSessionByChannel(interaction.channelId);
1747
2368
  if (!session) {
1748
2369
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1825,7 +2446,7 @@ async function handleAgent(interaction) {
1825
2446
  }
1826
2447
  case "list": {
1827
2448
  const agents2 = listAgents();
1828
- const embed = new EmbedBuilder2().setColor(10181046).setTitle("Agent Personas").setDescription(agents2.map((a) => `${a.emoji} **${a.name}** \u2014 ${a.description}`).join("\n"));
2449
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle("Agent Personas").setDescription(agents2.map((a) => `${a.emoji} **${a.name}** \u2014 ${a.description}`).join("\n"));
1829
2450
  await interaction.reply({ embeds: [embed], ephemeral: true });
1830
2451
  break;
1831
2452
  }
@@ -1918,7 +2539,7 @@ ${list}`, ephemeral: true });
1918
2539
  const channel = interaction.channel;
1919
2540
  await interaction.editReply(`Running skill **${name}**...`);
1920
2541
  const stream = sendPrompt(session.id, expanded);
1921
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
2542
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
1922
2543
  } catch (err) {
1923
2544
  await interaction.editReply(`Error: ${err.message}`);
1924
2545
  }
@@ -1963,7 +2584,7 @@ ${list}`, ephemeral: true });
1963
2584
  await interaction.reply({ content: "Project not found.", ephemeral: true });
1964
2585
  return;
1965
2586
  }
1966
- const embed = new EmbedBuilder2().setColor(15965202).setTitle(`Project: ${projectName}`).addFields(
2587
+ const embed = new EmbedBuilder3().setColor(15965202).setTitle(`Project: ${projectName}`).addFields(
1967
2588
  { name: "Directory", value: `\`${project.directory}\``, inline: false },
1968
2589
  {
1969
2590
  name: "Personality",
@@ -1986,9 +2607,438 @@ ${list}`, ephemeral: true });
1986
2607
  }
1987
2608
  }
1988
2609
  }
2610
+ async function handlePlugin(interaction) {
2611
+ if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
2612
+ await interaction.reply({ content: "You are not authorized.", ephemeral: true });
2613
+ return;
2614
+ }
2615
+ const sub = interaction.options.getSubcommand();
2616
+ switch (sub) {
2617
+ case "browse":
2618
+ return handlePluginBrowse(interaction);
2619
+ case "install":
2620
+ return handlePluginInstall(interaction);
2621
+ case "remove":
2622
+ return handlePluginRemove(interaction);
2623
+ case "list":
2624
+ return handlePluginList(interaction);
2625
+ case "info":
2626
+ return handlePluginInfo(interaction);
2627
+ case "enable":
2628
+ return handlePluginEnable(interaction);
2629
+ case "disable":
2630
+ return handlePluginDisable(interaction);
2631
+ case "update":
2632
+ return handlePluginUpdate(interaction);
2633
+ case "marketplace-add":
2634
+ return handleMarketplaceAdd(interaction);
2635
+ case "marketplace-remove":
2636
+ return handleMarketplaceRemove(interaction);
2637
+ case "marketplace-list":
2638
+ return handleMarketplaceList(interaction);
2639
+ case "marketplace-update":
2640
+ return handleMarketplaceUpdate(interaction);
2641
+ default:
2642
+ await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
2643
+ }
2644
+ }
2645
+ function resolveScopeAndCwd(interaction) {
2646
+ const scope = interaction.options.getString("scope") || "user";
2647
+ if (scope === "user") return { scope };
2648
+ const session = getSessionByChannel(interaction.channelId);
2649
+ if (!session) {
2650
+ return { scope, error: `Scope \`${scope}\` requires an active session. Run this from a session channel, or use \`user\` scope.` };
2651
+ }
2652
+ return { scope, cwd: session.directory };
2653
+ }
2654
+ async function handlePluginBrowse(interaction) {
2655
+ await interaction.deferReply({ ephemeral: true });
2656
+ try {
2657
+ const search = interaction.options.getString("search")?.toLowerCase();
2658
+ const { installed, available } = await listAvailable();
2659
+ const installedIds = new Set(installed.map((p) => p.id));
2660
+ let filtered = available;
2661
+ if (search) {
2662
+ filtered = available.filter((p) => p.name.toLowerCase().includes(search) || p.description.toLowerCase().includes(search) || p.marketplaceName.toLowerCase().includes(search));
2663
+ }
2664
+ filtered.sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
2665
+ if (filtered.length === 0) {
2666
+ await interaction.editReply("No plugins found matching your search.");
2667
+ return;
2668
+ }
2669
+ const shown = filtered.slice(0, 15);
2670
+ const embed = new EmbedBuilder3().setColor(8141549).setTitle("Available Plugins").setDescription(`Showing ${shown.length} of ${filtered.length} plugins. Use \`/plugin install\` to install.`);
2671
+ for (const p of shown) {
2672
+ const status = installedIds.has(p.pluginId) ? " \u2705" : "";
2673
+ const count = p.installCount ? ` | ${p.installCount.toLocaleString()} installs` : "";
2674
+ embed.addFields({
2675
+ name: `${p.name}${status}`,
2676
+ value: `${truncate(p.description, 150)}
2677
+ *${p.marketplaceName}*${count}`
2678
+ });
2679
+ }
2680
+ await interaction.editReply({ embeds: [embed] });
2681
+ } catch (err) {
2682
+ await interaction.editReply(`Error: ${err.message}`);
2683
+ }
2684
+ }
2685
+ async function handlePluginInstall(interaction) {
2686
+ const pluginId = interaction.options.getString("plugin", true);
2687
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2688
+ if (error) {
2689
+ await interaction.reply({ content: error, ephemeral: true });
2690
+ return;
2691
+ }
2692
+ await interaction.deferReply({ ephemeral: true });
2693
+ try {
2694
+ const result = await installPlugin(pluginId, scope, cwd);
2695
+ const embed = new EmbedBuilder3().setColor(3066993).setTitle("Plugin Installed").setDescription(`**${pluginId}** installed with \`${scope}\` scope.`).addFields({ name: "Output", value: truncate(result, 1e3) || "Done." });
2696
+ await interaction.editReply({ embeds: [embed] });
2697
+ log(`Plugin "${pluginId}" installed (scope=${scope}) by ${interaction.user.tag}`);
2698
+ } catch (err) {
2699
+ await interaction.editReply(`Failed to install: ${err.message}`);
2700
+ }
2701
+ }
2702
+ async function handlePluginRemove(interaction) {
2703
+ const pluginId = interaction.options.getString("plugin", true);
2704
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2705
+ if (error) {
2706
+ await interaction.reply({ content: error, ephemeral: true });
2707
+ return;
2708
+ }
2709
+ await interaction.deferReply({ ephemeral: true });
2710
+ try {
2711
+ const result = await uninstallPlugin(pluginId, scope, cwd);
2712
+ await interaction.editReply(`Plugin **${pluginId}** removed.
2713
+ ${truncate(result, 500)}`);
2714
+ log(`Plugin "${pluginId}" removed (scope=${scope}) by ${interaction.user.tag}`);
2715
+ } catch (err) {
2716
+ await interaction.editReply(`Failed to remove: ${err.message}`);
2717
+ }
2718
+ }
2719
+ async function handlePluginList(interaction) {
2720
+ await interaction.deferReply({ ephemeral: true });
2721
+ try {
2722
+ const plugins = await listInstalled();
2723
+ if (plugins.length === 0) {
2724
+ await interaction.editReply("No plugins installed.");
2725
+ return;
2726
+ }
2727
+ const embed = new EmbedBuilder3().setColor(3447003).setTitle(`Installed Plugins (${plugins.length})`);
2728
+ for (const p of plugins) {
2729
+ const icon = p.enabled ? "\u2705" : "\u274C";
2730
+ const scopeLabel = p.scope.charAt(0).toUpperCase() + p.scope.slice(1);
2731
+ const project = p.projectPath ? `
2732
+ Project: \`${p.projectPath}\`` : "";
2733
+ embed.addFields({
2734
+ name: `${icon} ${p.id}`,
2735
+ value: `v${p.version} | ${scopeLabel} scope${project}`
2736
+ });
2737
+ }
2738
+ await interaction.editReply({ embeds: [embed] });
2739
+ } catch (err) {
2740
+ await interaction.editReply(`Error: ${err.message}`);
2741
+ }
2742
+ }
2743
+ async function handlePluginInfo(interaction) {
2744
+ const pluginId = interaction.options.getString("plugin", true);
2745
+ await interaction.deferReply({ ephemeral: true });
2746
+ try {
2747
+ const parts = pluginId.split("@");
2748
+ const pluginName = parts[0];
2749
+ const marketplaceName = parts[1];
2750
+ const installed = await listInstalled();
2751
+ const installedEntry = installed.find((p) => p.id === pluginId);
2752
+ let detail = null;
2753
+ if (marketplaceName) {
2754
+ detail = await getPluginDetail(pluginName, marketplaceName);
2755
+ }
2756
+ const embed = new EmbedBuilder3().setColor(15965202).setTitle(`Plugin: ${pluginName}`);
2757
+ if (detail) {
2758
+ embed.setDescription(detail.description);
2759
+ if (detail.author) {
2760
+ embed.addFields({ name: "Author", value: detail.author.name, inline: true });
2761
+ }
2762
+ if (detail.category) {
2763
+ embed.addFields({ name: "Category", value: detail.category, inline: true });
2764
+ }
2765
+ if (detail.version) {
2766
+ embed.addFields({ name: "Version", value: detail.version, inline: true });
2767
+ }
2768
+ if (detail.tags?.length) {
2769
+ embed.addFields({ name: "Tags", value: detail.tags.join(", "), inline: false });
2770
+ }
2771
+ if (detail.homepage) {
2772
+ embed.addFields({ name: "Homepage", value: detail.homepage, inline: false });
2773
+ }
2774
+ if (detail.lspServers) {
2775
+ embed.addFields({ name: "LSP Servers", value: Object.keys(detail.lspServers).join(", "), inline: true });
2776
+ }
2777
+ if (detail.mcpServers) {
2778
+ embed.addFields({ name: "MCP Servers", value: Object.keys(detail.mcpServers).join(", "), inline: true });
2779
+ }
2780
+ }
2781
+ if (installedEntry) {
2782
+ const icon = installedEntry.enabled ? "\u2705 Enabled" : "\u274C Disabled";
2783
+ embed.addFields(
2784
+ { name: "Status", value: `${icon} | v${installedEntry.version}`, inline: true },
2785
+ { name: "Scope", value: installedEntry.scope, inline: true },
2786
+ { name: "Installed", value: new Date(installedEntry.installedAt).toLocaleDateString(), inline: true }
2787
+ );
2788
+ } else {
2789
+ embed.addFields({ name: "Status", value: "Not installed", inline: true });
2790
+ }
2791
+ if (marketplaceName) {
2792
+ embed.setFooter({ text: `Marketplace: ${marketplaceName}` });
2793
+ }
2794
+ await interaction.editReply({ embeds: [embed] });
2795
+ } catch (err) {
2796
+ await interaction.editReply(`Error: ${err.message}`);
2797
+ }
2798
+ }
2799
+ async function handlePluginEnable(interaction) {
2800
+ const pluginId = interaction.options.getString("plugin", true);
2801
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2802
+ if (error) {
2803
+ await interaction.reply({ content: error, ephemeral: true });
2804
+ return;
2805
+ }
2806
+ await interaction.deferReply({ ephemeral: true });
2807
+ try {
2808
+ await enablePlugin(pluginId, scope, cwd);
2809
+ await interaction.editReply(`Plugin **${pluginId}** enabled (\`${scope}\` scope).`);
2810
+ log(`Plugin "${pluginId}" enabled (scope=${scope}) by ${interaction.user.tag}`);
2811
+ } catch (err) {
2812
+ await interaction.editReply(`Failed to enable: ${err.message}`);
2813
+ }
2814
+ }
2815
+ async function handlePluginDisable(interaction) {
2816
+ const pluginId = interaction.options.getString("plugin", true);
2817
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2818
+ if (error) {
2819
+ await interaction.reply({ content: error, ephemeral: true });
2820
+ return;
2821
+ }
2822
+ await interaction.deferReply({ ephemeral: true });
2823
+ try {
2824
+ await disablePlugin(pluginId, scope, cwd);
2825
+ await interaction.editReply(`Plugin **${pluginId}** disabled (\`${scope}\` scope).`);
2826
+ log(`Plugin "${pluginId}" disabled (scope=${scope}) by ${interaction.user.tag}`);
2827
+ } catch (err) {
2828
+ await interaction.editReply(`Failed to disable: ${err.message}`);
2829
+ }
2830
+ }
2831
+ async function handlePluginUpdate(interaction) {
2832
+ const pluginId = interaction.options.getString("plugin", true);
2833
+ const { scope, cwd, error } = resolveScopeAndCwd(interaction);
2834
+ if (error) {
2835
+ await interaction.reply({ content: error, ephemeral: true });
2836
+ return;
2837
+ }
2838
+ await interaction.deferReply({ ephemeral: true });
2839
+ try {
2840
+ const result = await updatePlugin(pluginId, scope, cwd);
2841
+ await interaction.editReply(`Plugin **${pluginId}** updated.
2842
+ ${truncate(result, 500)}`);
2843
+ log(`Plugin "${pluginId}" updated (scope=${scope}) by ${interaction.user.tag}`);
2844
+ } catch (err) {
2845
+ await interaction.editReply(`Failed to update: ${err.message}`);
2846
+ }
2847
+ }
2848
+ async function handleMarketplaceAdd(interaction) {
2849
+ const source = interaction.options.getString("source", true);
2850
+ await interaction.deferReply({ ephemeral: true });
2851
+ try {
2852
+ const result = await addMarketplace(source);
2853
+ await interaction.editReply(`Marketplace added from \`${source}\`.
2854
+ ${truncate(result, 500)}`);
2855
+ log(`Marketplace "${source}" added by ${interaction.user.tag}`);
2856
+ } catch (err) {
2857
+ await interaction.editReply(`Failed to add marketplace: ${err.message}`);
2858
+ }
2859
+ }
2860
+ async function handleMarketplaceRemove(interaction) {
2861
+ const name = interaction.options.getString("name", true);
2862
+ await interaction.deferReply({ ephemeral: true });
2863
+ try {
2864
+ const result = await removeMarketplace(name);
2865
+ await interaction.editReply(`Marketplace **${name}** removed.
2866
+ ${truncate(result, 500)}`);
2867
+ log(`Marketplace "${name}" removed by ${interaction.user.tag}`);
2868
+ } catch (err) {
2869
+ await interaction.editReply(`Failed to remove marketplace: ${err.message}`);
2870
+ }
2871
+ }
2872
+ async function handleMarketplaceList(interaction) {
2873
+ await interaction.deferReply({ ephemeral: true });
2874
+ try {
2875
+ const marketplaces = await listMarketplaces();
2876
+ if (marketplaces.length === 0) {
2877
+ await interaction.editReply("No marketplaces registered.");
2878
+ return;
2879
+ }
2880
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle(`Marketplaces (${marketplaces.length})`);
2881
+ for (const m of marketplaces) {
2882
+ const source = m.repo || m.url || m.source;
2883
+ embed.addFields({
2884
+ name: m.name,
2885
+ value: `Source: \`${source}\`
2886
+ Path: \`${m.installLocation}\``
2887
+ });
2888
+ }
2889
+ await interaction.editReply({ embeds: [embed] });
2890
+ } catch (err) {
2891
+ await interaction.editReply(`Error: ${err.message}`);
2892
+ }
2893
+ }
2894
+ async function handleMarketplaceUpdate(interaction) {
2895
+ const name = interaction.options.getString("name") || void 0;
2896
+ await interaction.deferReply({ ephemeral: true });
2897
+ try {
2898
+ const result = await updateMarketplaces(name);
2899
+ await interaction.editReply(`Marketplace${name ? ` **${name}**` : "s"} updated.
2900
+ ${truncate(result, 500)}`);
2901
+ log(`Marketplace${name ? ` "${name}"` : "s"} updated by ${interaction.user.tag}`);
2902
+ } catch (err) {
2903
+ await interaction.editReply(`Failed to update: ${err.message}`);
2904
+ }
2905
+ }
2906
+ async function handlePluginAutocomplete(interaction) {
2907
+ const sub = interaction.options.getSubcommand();
2908
+ const focused = interaction.options.getFocused().toLowerCase();
2909
+ try {
2910
+ if (sub === "install" || sub === "info" && !focused.includes("@")) {
2911
+ const { available } = await listAvailable();
2912
+ const filtered = focused ? available.filter((p) => p.name.toLowerCase().includes(focused) || p.pluginId.toLowerCase().includes(focused) || p.description.toLowerCase().includes(focused)) : available;
2913
+ filtered.sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
2914
+ const choices = filtered.slice(0, 25).map((p) => ({
2915
+ name: `${p.name} (${p.marketplaceName})`.slice(0, 100),
2916
+ value: p.pluginId
2917
+ }));
2918
+ await interaction.respond(choices);
2919
+ } else if (["remove", "enable", "disable", "update", "info"].includes(sub)) {
2920
+ const installed = await listInstalled();
2921
+ const filtered = focused ? installed.filter((p) => p.id.toLowerCase().includes(focused)) : installed;
2922
+ const choices = filtered.slice(0, 25).map((p) => ({
2923
+ name: `${p.id} (v${p.version}, ${p.scope})`.slice(0, 100),
2924
+ value: p.id
2925
+ }));
2926
+ await interaction.respond(choices);
2927
+ } else if (sub === "marketplace-remove" || sub === "marketplace-update") {
2928
+ const marketplaces = await listMarketplaces();
2929
+ const filtered = focused ? marketplaces.filter((m) => m.name.toLowerCase().includes(focused)) : marketplaces;
2930
+ const choices = filtered.slice(0, 25).map((m) => ({
2931
+ name: m.name,
2932
+ value: m.name
2933
+ }));
2934
+ await interaction.respond(choices);
2935
+ } else {
2936
+ await interaction.respond([]);
2937
+ }
2938
+ } catch {
2939
+ await interaction.respond([]);
2940
+ }
2941
+ }
1989
2942
 
1990
2943
  // src/message-handler.ts
2944
+ import sharp from "sharp";
2945
+ var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
2946
+ "image/jpeg",
2947
+ "image/png",
2948
+ "image/gif",
2949
+ "image/webp"
2950
+ ]);
2951
+ var TEXT_CONTENT_TYPES = /* @__PURE__ */ new Set([
2952
+ "text/plain",
2953
+ "text/markdown",
2954
+ "text/csv",
2955
+ "text/html",
2956
+ "text/xml",
2957
+ "application/json",
2958
+ "application/xml",
2959
+ "application/javascript",
2960
+ "application/typescript",
2961
+ "application/x-yaml"
2962
+ ]);
2963
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2964
+ ".txt",
2965
+ ".md",
2966
+ ".json",
2967
+ ".yaml",
2968
+ ".yml",
2969
+ ".xml",
2970
+ ".csv",
2971
+ ".html",
2972
+ ".css",
2973
+ ".js",
2974
+ ".ts",
2975
+ ".jsx",
2976
+ ".tsx",
2977
+ ".swift",
2978
+ ".py",
2979
+ ".rb",
2980
+ ".go",
2981
+ ".rs",
2982
+ ".java",
2983
+ ".kt",
2984
+ ".c",
2985
+ ".cpp",
2986
+ ".h",
2987
+ ".hpp",
2988
+ ".sh",
2989
+ ".bash",
2990
+ ".zsh",
2991
+ ".toml",
2992
+ ".ini",
2993
+ ".cfg",
2994
+ ".conf",
2995
+ ".env",
2996
+ ".log",
2997
+ ".sql",
2998
+ ".graphql",
2999
+ ".proto",
3000
+ ".diff",
3001
+ ".patch"
3002
+ ]);
3003
+ var MAX_IMAGE_SIZE = 20 * 1024 * 1024;
3004
+ var MAX_TEXT_FILE_SIZE = 512 * 1024;
3005
+ var MAX_RAW_BYTES = Math.floor(5 * 1024 * 1024 * 3 / 4);
1991
3006
  var userLastMessage = /* @__PURE__ */ new Map();
3007
+ async function resizeImageToFit(buf) {
3008
+ const meta = await sharp(buf).metadata();
3009
+ const width = meta.width || 1;
3010
+ const height = meta.height || 1;
3011
+ let scale = 1;
3012
+ for (let i = 0; i < 5; i++) {
3013
+ scale *= 0.7;
3014
+ const resized = await sharp(buf).resize(Math.round(width * scale), Math.round(height * scale), { fit: "inside" }).jpeg({ quality: 80 }).toBuffer();
3015
+ if (resized.length <= MAX_RAW_BYTES) return resized;
3016
+ }
3017
+ return sharp(buf).resize(Math.round(width * scale * 0.5), Math.round(height * scale * 0.5), { fit: "inside" }).jpeg({ quality: 60 }).toBuffer();
3018
+ }
3019
+ function isTextAttachment(contentType, filename) {
3020
+ if (contentType && TEXT_CONTENT_TYPES.has(contentType.split(";")[0])) return true;
3021
+ if (filename) {
3022
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
3023
+ if (TEXT_EXTENSIONS.has(ext)) return true;
3024
+ }
3025
+ return false;
3026
+ }
3027
+ async function fetchTextFile(url) {
3028
+ const res = await fetch(url);
3029
+ if (!res.ok) throw new Error(`Failed to download file: ${res.status}`);
3030
+ return res.text();
3031
+ }
3032
+ async function fetchImageAsBase64(url, mediaType) {
3033
+ const res = await fetch(url);
3034
+ if (!res.ok) throw new Error(`Failed to download image: ${res.status}`);
3035
+ const buf = Buffer.from(await res.arrayBuffer());
3036
+ if (buf.length > MAX_RAW_BYTES) {
3037
+ const resized = await resizeImageToFit(buf);
3038
+ return { data: resized.toString("base64"), mediaType: "image/jpeg" };
3039
+ }
3040
+ return { data: buf.toString("base64"), mediaType };
3041
+ }
1992
3042
  async function handleMessage(message) {
1993
3043
  if (message.author.bot) return;
1994
3044
  const session = getSessionByChannel(message.channelId);
@@ -2011,27 +3061,92 @@ async function handleMessage(message) {
2011
3061
  }
2012
3062
  if (session.isGenerating) {
2013
3063
  await message.reply({
2014
- content: "Could not interrupt the current generation. Try `/claude stop`.",
3064
+ content: "Could not interrupt the current generation. Try `/session stop`.",
2015
3065
  allowedMentions: { repliedUser: false }
2016
3066
  });
2017
3067
  return;
2018
3068
  }
2019
3069
  }
2020
- const content = message.content.trim();
2021
- if (!content) return;
3070
+ const text = message.content.trim();
3071
+ const imageAttachments = message.attachments.filter(
3072
+ (a) => a.contentType && SUPPORTED_IMAGE_TYPES.has(a.contentType) && a.size <= MAX_IMAGE_SIZE
3073
+ );
3074
+ const textAttachments = message.attachments.filter(
3075
+ (a) => !SUPPORTED_IMAGE_TYPES.has(a.contentType ?? "") && !(a.contentType?.startsWith("video/") || a.contentType?.startsWith("audio/")) && (isTextAttachment(a.contentType, a.name) || !a.contentType) && a.size <= MAX_TEXT_FILE_SIZE
3076
+ );
3077
+ if (!text && imageAttachments.size === 0 && textAttachments.size === 0) return;
2022
3078
  try {
2023
3079
  const channel = message.channel;
2024
- const stream = sendPrompt(session.id, content);
2025
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
3080
+ const hasAttachments = imageAttachments.size > 0 || textAttachments.size > 0;
3081
+ let prompt;
3082
+ if (!hasAttachments) {
3083
+ prompt = text;
3084
+ } else {
3085
+ const blocks = [];
3086
+ const textResults = await Promise.allSettled(
3087
+ textAttachments.map(async (a) => ({
3088
+ name: a.name ?? "file",
3089
+ content: await fetchTextFile(a.url)
3090
+ }))
3091
+ );
3092
+ for (const result of textResults) {
3093
+ if (result.status === "fulfilled") {
3094
+ blocks.push({
3095
+ type: "text",
3096
+ text: `<file name="${result.value.name}">
3097
+ ${result.value.content}
3098
+ </file>`
3099
+ });
3100
+ }
3101
+ }
3102
+ const imageResults = await Promise.allSettled(
3103
+ imageAttachments.map((a) => fetchImageAsBase64(a.url, a.contentType))
3104
+ );
3105
+ for (const result of imageResults) {
3106
+ if (result.status === "fulfilled") {
3107
+ blocks.push({
3108
+ type: "image",
3109
+ source: {
3110
+ type: "base64",
3111
+ media_type: result.value.mediaType,
3112
+ data: result.value.data
3113
+ }
3114
+ });
3115
+ }
3116
+ }
3117
+ if (text) {
3118
+ blocks.push({ type: "text", text });
3119
+ } else if (imageAttachments.size > 0 && textAttachments.size === 0) {
3120
+ blocks.push({ type: "text", text: "What is in this image?" });
3121
+ } else {
3122
+ blocks.push({ type: "text", text: "Here are the attached files." });
3123
+ }
3124
+ prompt = blocks;
3125
+ }
3126
+ const stream = sendPrompt(session.id, prompt);
3127
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
2026
3128
  } catch (err) {
3129
+ const errMsg = err.message || "";
3130
+ const isAbort = err.name === "AbortError" || /abort|cancel|interrupt/i.test(errMsg);
3131
+ if (isAbort) {
3132
+ return;
3133
+ }
3134
+ resetProviderSession(session.id);
2027
3135
  await message.reply({
2028
- content: `Error: ${err.message}`,
3136
+ content: `Error: ${errMsg}
3137
+ -# Session reset \u2014 next message will start a fresh provider session.`,
2029
3138
  allowedMentions: { repliedUser: false }
2030
3139
  });
2031
3140
  }
2032
3141
  }
2033
3142
 
2034
3143
  // src/button-handler.ts
3144
+ import {
3145
+ ActionRowBuilder as ActionRowBuilder2,
3146
+ ButtonBuilder as ButtonBuilder2,
3147
+ ButtonStyle as ButtonStyle2,
3148
+ StringSelectMenuBuilder as StringSelectMenuBuilder2
3149
+ } from "discord.js";
2035
3150
  async function handleButton(interaction) {
2036
3151
  if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
2037
3152
  await interaction.reply({ content: "Not authorized.", ephemeral: true });
@@ -2063,7 +3178,7 @@ async function handleButton(interaction) {
2063
3178
  const channel = interaction.channel;
2064
3179
  const stream = continueSession(sessionId);
2065
3180
  await interaction.editReply("Continuing...");
2066
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3181
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2067
3182
  } catch (err) {
2068
3183
  await interaction.editReply(`Error: ${err.message}`);
2069
3184
  }
@@ -2097,7 +3212,79 @@ ${display}
2097
3212
  const channel = interaction.channel;
2098
3213
  const stream = sendPrompt(sessionId, optionText);
2099
3214
  await interaction.editReply(`Selected option ${optionIndex + 1}`);
2100
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3215
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
3216
+ } catch (err) {
3217
+ await interaction.editReply(`Error: ${err.message}`);
3218
+ }
3219
+ return;
3220
+ }
3221
+ if (customId.startsWith("pick:")) {
3222
+ const parts = customId.split(":");
3223
+ const sessionId = parts[1];
3224
+ const questionIndex = parseInt(parts[2], 10);
3225
+ const answer = parts.slice(3).join(":");
3226
+ const session = getSession(sessionId);
3227
+ if (!session) {
3228
+ await interaction.reply({ content: "Session not found.", ephemeral: true });
3229
+ return;
3230
+ }
3231
+ setPendingAnswer(sessionId, questionIndex, answer);
3232
+ const totalQuestions = getQuestionCount(sessionId);
3233
+ const pending = getPendingAnswers(sessionId);
3234
+ const answeredCount = pending?.size || 0;
3235
+ try {
3236
+ const original = interaction.message;
3237
+ const updatedComponents = original.components.map((row) => {
3238
+ const firstComponent = row.components?.[0];
3239
+ if (!firstComponent?.customId?.startsWith("pick:")) return row;
3240
+ const rowQi = parseInt(firstComponent.customId.split(":")[2], 10);
3241
+ if (rowQi !== questionIndex) return row;
3242
+ const newRow = new ActionRowBuilder2();
3243
+ for (const btn of row.components) {
3244
+ const btnAnswer = btn.customId.split(":").slice(3).join(":");
3245
+ const isSelected = btnAnswer === answer;
3246
+ newRow.addComponents(
3247
+ new ButtonBuilder2().setCustomId(btn.customId).setLabel(btn.label).setStyle(isSelected ? ButtonStyle2.Success : ButtonStyle2.Secondary)
3248
+ );
3249
+ }
3250
+ return newRow;
3251
+ });
3252
+ await original.edit({ components: updatedComponents });
3253
+ } catch {
3254
+ }
3255
+ await interaction.reply({
3256
+ content: `Selected for Q${questionIndex + 1}: **${truncate(answer, 100)}** (${answeredCount}/${totalQuestions} answered)`,
3257
+ ephemeral: true
3258
+ });
3259
+ return;
3260
+ }
3261
+ if (customId.startsWith("submit-answers:")) {
3262
+ const sessionId = customId.slice(15);
3263
+ const session = getSession(sessionId);
3264
+ if (!session) {
3265
+ await interaction.reply({ content: "Session not found.", ephemeral: true });
3266
+ return;
3267
+ }
3268
+ const totalQuestions = getQuestionCount(sessionId);
3269
+ const pending = getPendingAnswers(sessionId);
3270
+ if (!pending || pending.size === 0) {
3271
+ await interaction.reply({ content: "No answers selected yet. Pick an answer for each question first.", ephemeral: true });
3272
+ return;
3273
+ }
3274
+ const answerLines = [];
3275
+ for (let i = 0; i < totalQuestions; i++) {
3276
+ const ans = pending.get(i);
3277
+ answerLines.push(`Q${i + 1}: ${ans || "(no answer)"}`);
3278
+ }
3279
+ const combined = answerLines.join("\n");
3280
+ clearPendingAnswers(sessionId);
3281
+ await interaction.deferReply();
3282
+ try {
3283
+ const channel = interaction.channel;
3284
+ const stream = sendPrompt(sessionId, combined);
3285
+ await interaction.editReply(`Submitted answers:
3286
+ ${combined}`);
3287
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2101
3288
  } catch (err) {
2102
3289
  await interaction.editReply(`Error: ${err.message}`);
2103
3290
  }
@@ -2106,7 +3293,8 @@ ${display}
2106
3293
  if (customId.startsWith("answer:")) {
2107
3294
  const parts = customId.split(":");
2108
3295
  const sessionId = parts[1];
2109
- const answer = parts.slice(2).join(":");
3296
+ const hasQuestionIndex = /^\d+$/.test(parts[2]);
3297
+ const answer = hasQuestionIndex ? parts.slice(3).join(":") : parts.slice(2).join(":");
2110
3298
  const session = getSession(sessionId);
2111
3299
  if (!session) {
2112
3300
  await interaction.reply({ content: "Session not found.", ephemeral: true });
@@ -2117,7 +3305,7 @@ ${display}
2117
3305
  const channel = interaction.channel;
2118
3306
  const stream = sendPrompt(sessionId, answer);
2119
3307
  await interaction.editReply(`Answered: **${truncate(answer, 100)}**`);
2120
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3308
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2121
3309
  } catch (err) {
2122
3310
  await interaction.editReply(`Error: ${err.message}`);
2123
3311
  }
@@ -2137,7 +3325,7 @@ ${display}
2137
3325
  const channel = interaction.channel;
2138
3326
  const stream = sendPrompt(sessionId, answer);
2139
3327
  await interaction.editReply(`Answered: ${answer}`);
2140
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3328
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2141
3329
  } catch (err) {
2142
3330
  await interaction.editReply(`Error: ${err.message}`);
2143
3331
  }
@@ -2184,8 +3372,48 @@ async function handleSelectMenu(interaction) {
2184
3372
  return;
2185
3373
  }
2186
3374
  const customId = interaction.customId;
3375
+ if (customId.startsWith("pick-select:")) {
3376
+ const parts = customId.split(":");
3377
+ const sessionId = parts[1];
3378
+ const questionIndex = parseInt(parts[2], 10);
3379
+ const selected = interaction.values[0];
3380
+ const session = getSession(sessionId);
3381
+ if (!session) {
3382
+ await interaction.reply({ content: "Session not found.", ephemeral: true });
3383
+ return;
3384
+ }
3385
+ setPendingAnswer(sessionId, questionIndex, selected);
3386
+ const totalQuestions = getQuestionCount(sessionId);
3387
+ const pending = getPendingAnswers(sessionId);
3388
+ const answeredCount = pending?.size || 0;
3389
+ try {
3390
+ const original = interaction.message;
3391
+ const updatedComponents = original.components.map((row) => {
3392
+ const comp = row.components?.[0];
3393
+ if (comp?.customId !== customId) return row;
3394
+ const menu = new StringSelectMenuBuilder2().setCustomId(customId).setPlaceholder(`Selected: ${selected.slice(0, 80)}`);
3395
+ for (const opt of comp.options) {
3396
+ menu.addOptions({
3397
+ label: opt.label,
3398
+ description: opt.description || void 0,
3399
+ value: opt.value,
3400
+ default: opt.value === selected
3401
+ });
3402
+ }
3403
+ return new ActionRowBuilder2().addComponents(menu);
3404
+ });
3405
+ await original.edit({ components: updatedComponents });
3406
+ } catch {
3407
+ }
3408
+ await interaction.reply({
3409
+ content: `Selected for Q${questionIndex + 1}: **${truncate(selected, 100)}** (${answeredCount}/${totalQuestions} answered)`,
3410
+ ephemeral: true
3411
+ });
3412
+ return;
3413
+ }
2187
3414
  if (customId.startsWith("answer-select:")) {
2188
- const sessionId = customId.slice(14);
3415
+ const afterPrefix = customId.slice(14);
3416
+ const sessionId = afterPrefix.includes(":") ? afterPrefix.split(":")[0] : afterPrefix;
2189
3417
  const selected = interaction.values[0];
2190
3418
  const session = getSession(sessionId);
2191
3419
  if (!session) {
@@ -2197,7 +3425,7 @@ async function handleSelectMenu(interaction) {
2197
3425
  const channel = interaction.channel;
2198
3426
  const stream = sendPrompt(sessionId, selected);
2199
3427
  await interaction.editReply(`Answered: **${truncate(selected, 100)}**`);
2200
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3428
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2201
3429
  } catch (err) {
2202
3430
  await interaction.editReply(`Error: ${err.message}`);
2203
3431
  }
@@ -2216,7 +3444,7 @@ async function handleSelectMenu(interaction) {
2216
3444
  const channel = interaction.channel;
2217
3445
  const stream = sendPrompt(sessionId, selected);
2218
3446
  await interaction.editReply(`Selected: ${truncate(selected, 100)}`);
2219
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3447
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2220
3448
  } catch (err) {
2221
3449
  await interaction.editReply(`Error: ${err.message}`);
2222
3450
  }
@@ -2293,19 +3521,24 @@ async function startBot() {
2293
3521
  try {
2294
3522
  if (interaction.type === InteractionType.ApplicationCommand && interaction.isChatInputCommand()) {
2295
3523
  switch (interaction.commandName) {
2296
- case "claude":
2297
- return await handleClaude(interaction);
3524
+ case "session":
3525
+ return await handleSession(interaction);
2298
3526
  case "shell":
2299
3527
  return await handleShell(interaction);
2300
3528
  case "agent":
2301
3529
  return await handleAgent(interaction);
2302
3530
  case "project":
2303
3531
  return await handleProject(interaction);
3532
+ case "plugin":
3533
+ return await handlePlugin(interaction);
2304
3534
  }
2305
3535
  }
2306
3536
  if (interaction.isAutocomplete()) {
2307
- if (interaction.commandName === "claude") {
2308
- return await handleClaudeAutocomplete(interaction);
3537
+ if (interaction.commandName === "session") {
3538
+ return await handleSessionAutocomplete(interaction);
3539
+ }
3540
+ if (interaction.commandName === "plugin") {
3541
+ return await handlePluginAutocomplete(interaction);
2309
3542
  }
2310
3543
  }
2311
3544
  if (interaction.isButton()) {