agentcord 0.1.8 → 0.2.0

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" }
@@ -91,7 +97,7 @@ function getCommandDefinitions() {
91
97
  { name: "Local", value: "local" }
92
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)));
93
99
  return [
94
- claude.toJSON(),
100
+ session.toJSON(),
95
101
  shell.toJSON(),
96
102
  agent.toJSON(),
97
103
  project.toJSON(),
@@ -122,7 +128,7 @@ async function registerCommands() {
122
128
 
123
129
  // src/command-handlers.ts
124
130
  import {
125
- EmbedBuilder as EmbedBuilder2,
131
+ EmbedBuilder as EmbedBuilder3,
126
132
  ChannelType
127
133
  } from "discord.js";
128
134
  import { readdirSync, statSync, createReadStream } from "fs";
@@ -133,7 +139,266 @@ import { createInterface } from "readline";
133
139
  // src/session-manager.ts
134
140
  import { execFile } from "child_process";
135
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
136
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
+ }
137
402
 
138
403
  // src/persistence.ts
139
404
  import { readFile, writeFile, mkdir, rename } from "fs/promises";
@@ -473,13 +738,19 @@ function detectNumberedOptions(text) {
473
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);
474
739
  return hasQuestion ? options : null;
475
740
  }
741
+ var ABORT_PATTERNS = ["abort", "cancel", "interrupt", "killed", "signal"];
742
+ function isAbortError(err) {
743
+ if (err instanceof Error && err.name === "AbortError") return true;
744
+ const msg = (err.message || "").toLowerCase();
745
+ return ABORT_PATTERNS.some((p) => msg.includes(p));
746
+ }
476
747
  function detectYesNoPrompt(text) {
477
748
  const lower = text.toLowerCase();
478
749
  return /\b(y\/n|yes\/no|confirm|proceed)\b/.test(lower) || /\?\s*$/.test(text.trim()) && /\b(should|would you|do you want|shall)\b/.test(lower);
479
750
  }
480
751
 
481
752
  // src/session-manager.ts
482
- var SESSION_PREFIX = "claude-";
753
+ var SESSION_PREFIX = "agentcord-";
483
754
  var MODE_PROMPTS = {
484
755
  auto: "",
485
756
  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.",
@@ -508,21 +779,27 @@ async function loadSessions() {
508
779
  const data = await sessionStore.read();
509
780
  if (!data) return;
510
781
  for (const s of data) {
511
- const exists = await tmuxSessionExists(s.tmuxName);
782
+ const provider = s.provider ?? "claude";
783
+ const providerSessionId = s.providerSessionId ?? s.claudeSessionId;
784
+ if (provider === "claude") {
785
+ const exists = await tmuxSessionExists(s.tmuxName);
786
+ if (!exists) {
787
+ try {
788
+ await tmux("new-session", "-d", "-s", s.tmuxName, "-c", s.directory);
789
+ } catch {
790
+ console.warn(`Could not recreate tmux session ${s.tmuxName}`);
791
+ }
792
+ }
793
+ }
512
794
  sessions.set(s.id, {
513
795
  ...s,
796
+ provider,
797
+ providerSessionId,
514
798
  verbose: s.verbose ?? false,
515
799
  mode: s.mode ?? "auto",
516
800
  isGenerating: false
517
801
  });
518
802
  channelToSession.set(s.channelId, s.id);
519
- if (!exists) {
520
- try {
521
- await tmux("new-session", "-d", "-s", s.tmuxName, "-c", s.directory);
522
- } catch {
523
- console.warn(`Could not recreate tmux session ${s.tmuxName}`);
524
- }
525
- }
526
803
  }
527
804
  console.log(`Restored ${sessions.size} session(s)`);
528
805
  }
@@ -534,8 +811,9 @@ async function saveSessions() {
534
811
  channelId: s.channelId,
535
812
  directory: s.directory,
536
813
  projectName: s.projectName,
814
+ provider: s.provider,
537
815
  tmuxName: s.tmuxName,
538
- claudeSessionId: s.claudeSessionId,
816
+ providerSessionId: s.providerSessionId,
539
817
  model: s.model,
540
818
  agentPersona: s.agentPersona,
541
819
  verbose: s.verbose || void 0,
@@ -548,7 +826,7 @@ async function saveSessions() {
548
826
  }
549
827
  await sessionStore.write(data);
550
828
  }
551
- async function createSession(name, directory, channelId, projectName, claudeSessionId) {
829
+ async function createSession(name, directory, channelId, projectName, provider = "claude", providerSessionId) {
552
830
  const resolvedDir = resolvePath(directory);
553
831
  if (!isPathAllowed(resolvedDir, config.allowedPaths)) {
554
832
  throw new Error(`Directory not in allowed paths: ${resolvedDir}`);
@@ -556,22 +834,27 @@ async function createSession(name, directory, channelId, projectName, claudeSess
556
834
  if (!existsSync3(resolvedDir)) {
557
835
  throw new Error(`Directory does not exist: ${resolvedDir}`);
558
836
  }
837
+ const providerInstance = await ensureProvider(provider);
838
+ const usesTmux = providerInstance.supports("tmux");
559
839
  let id = sanitizeSessionName(name);
560
- let tmuxName = `${SESSION_PREFIX}${id}`;
840
+ let tmuxName = usesTmux ? `${SESSION_PREFIX}${id}` : "";
561
841
  let suffix = 1;
562
- while (sessions.has(id) || await tmuxSessionExists(tmuxName)) {
842
+ while (sessions.has(id) || usesTmux && await tmuxSessionExists(tmuxName)) {
563
843
  suffix++;
564
844
  id = sanitizeSessionName(`${name}-${suffix}`);
565
- tmuxName = `${SESSION_PREFIX}${id}`;
845
+ if (usesTmux) tmuxName = `${SESSION_PREFIX}${id}`;
846
+ }
847
+ if (usesTmux) {
848
+ await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
566
849
  }
567
- await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
568
850
  const session = {
569
851
  id,
570
852
  channelId,
571
853
  directory: resolvedDir,
572
854
  projectName,
855
+ provider,
573
856
  tmuxName,
574
- claudeSessionId,
857
+ providerSessionId,
575
858
  verbose: false,
576
859
  mode: "auto",
577
860
  isGenerating: false,
@@ -601,9 +884,11 @@ async function endSession(id) {
601
884
  if (session.isGenerating && session._controller) {
602
885
  session._controller.abort();
603
886
  }
604
- try {
605
- await tmux("kill-session", "-t", session.tmuxName);
606
- } catch {
887
+ if (session.tmuxName) {
888
+ try {
889
+ await tmux("kill-session", "-t", session.tmuxName);
890
+ } catch {
891
+ }
607
892
  }
608
893
  channelToSession.delete(session.channelId);
609
894
  sessions.delete(id);
@@ -657,7 +942,7 @@ function setAgentPersona(sessionId, persona) {
657
942
  saveSessions();
658
943
  }
659
944
  }
660
- function buildSystemPrompt(session) {
945
+ function buildSystemPromptParts(session) {
661
946
  const parts = [];
662
947
  const personality = getPersonality(session.projectName);
663
948
  if (personality) parts.push(personality);
@@ -667,10 +952,7 @@ function buildSystemPrompt(session) {
667
952
  }
668
953
  const modePrompt = MODE_PROMPTS[session.mode];
669
954
  if (modePrompt) parts.push(modePrompt);
670
- if (parts.length > 0) {
671
- return { type: "preset", preset: "claude_code", append: parts.join("\n\n") };
672
- }
673
- return { type: "preset", preset: "claude_code" };
955
+ return parts;
674
956
  }
675
957
  async function* sendPrompt(sessionId, prompt) {
676
958
  const session = sessions.get(sessionId);
@@ -680,51 +962,29 @@ async function* sendPrompt(sessionId, prompt) {
680
962
  session._controller = controller;
681
963
  session.isGenerating = true;
682
964
  session.lastActivity = Date.now();
683
- const systemPrompt = buildSystemPrompt(session);
684
- let queryPrompt;
685
- if (typeof prompt === "string") {
686
- queryPrompt = prompt;
687
- } else {
688
- const userMessage = {
689
- type: "user",
690
- message: { role: "user", content: prompt },
691
- parent_tool_use_id: null,
692
- session_id: ""
693
- };
694
- queryPrompt = (async function* () {
695
- yield userMessage;
696
- })();
697
- }
965
+ const provider = await ensureProvider(session.provider);
966
+ const systemPromptParts = buildSystemPromptParts(session);
698
967
  try {
699
- const stream = query({
700
- prompt: queryPrompt,
701
- options: {
702
- cwd: session.directory,
703
- resume: session.claudeSessionId,
704
- abortController: controller,
705
- permissionMode: "bypassPermissions",
706
- allowDangerouslySkipPermissions: true,
707
- model: session.model,
708
- systemPrompt,
709
- includePartialMessages: true,
710
- settingSources: ["user", "project", "local"]
711
- }
968
+ const stream = provider.sendPrompt(prompt, {
969
+ directory: session.directory,
970
+ providerSessionId: session.providerSessionId,
971
+ model: session.model,
972
+ systemPromptParts,
973
+ abortController: controller
712
974
  });
713
- for await (const message of stream) {
714
- if (message.type === "system" && "subtype" in message && message.subtype === "init") {
715
- session.claudeSessionId = message.session_id;
975
+ for await (const event of stream) {
976
+ if (event.type === "session_init") {
977
+ session.providerSessionId = event.providerSessionId || void 0;
716
978
  await saveSessions();
717
979
  }
718
- if (message.type === "result") {
719
- if ("total_cost_usd" in message) {
720
- session.totalCost += message.total_cost_usd;
721
- }
980
+ if (event.type === "result") {
981
+ session.totalCost += event.costUsd;
722
982
  }
723
- yield message;
983
+ yield event;
724
984
  }
725
985
  session.messageCount++;
726
986
  } catch (err) {
727
- if (err.name === "AbortError") {
987
+ if (isAbortError(err)) {
728
988
  } else {
729
989
  throw err;
730
990
  }
@@ -743,36 +1003,29 @@ async function* continueSession(sessionId) {
743
1003
  session._controller = controller;
744
1004
  session.isGenerating = true;
745
1005
  session.lastActivity = Date.now();
746
- const systemPrompt = buildSystemPrompt(session);
1006
+ const provider = await ensureProvider(session.provider);
1007
+ const systemPromptParts = buildSystemPromptParts(session);
747
1008
  try {
748
- const stream = query({
749
- prompt: "",
750
- options: {
751
- cwd: session.directory,
752
- continue: true,
753
- resume: session.claudeSessionId,
754
- abortController: controller,
755
- permissionMode: "bypassPermissions",
756
- allowDangerouslySkipPermissions: true,
757
- model: session.model,
758
- systemPrompt,
759
- includePartialMessages: true,
760
- settingSources: ["user", "project", "local"]
761
- }
1009
+ const stream = provider.continueSession({
1010
+ directory: session.directory,
1011
+ providerSessionId: session.providerSessionId,
1012
+ model: session.model,
1013
+ systemPromptParts,
1014
+ abortController: controller
762
1015
  });
763
- for await (const message of stream) {
764
- if (message.type === "system" && "subtype" in message && message.subtype === "init") {
765
- session.claudeSessionId = message.session_id;
1016
+ for await (const event of stream) {
1017
+ if (event.type === "session_init") {
1018
+ session.providerSessionId = event.providerSessionId || void 0;
766
1019
  await saveSessions();
767
1020
  }
768
- if (message.type === "result" && "total_cost_usd" in message) {
769
- session.totalCost += message.total_cost_usd;
1021
+ if (event.type === "result") {
1022
+ session.totalCost += event.costUsd;
770
1023
  }
771
- yield message;
1024
+ yield event;
772
1025
  }
773
1026
  session.messageCount++;
774
1027
  } catch (err) {
775
- if (err.name === "AbortError") {
1028
+ if (isAbortError(err)) {
776
1029
  } else {
777
1030
  throw err;
778
1031
  }
@@ -789,16 +1042,21 @@ function abortSession(sessionId) {
789
1042
  const controller = session._controller;
790
1043
  if (controller) {
791
1044
  controller.abort();
1045
+ }
1046
+ if (session.isGenerating) {
1047
+ session.isGenerating = false;
1048
+ delete session._controller;
1049
+ saveSessions();
792
1050
  return true;
793
1051
  }
794
- return false;
1052
+ return !!controller;
795
1053
  }
796
1054
  function getAttachInfo(sessionId) {
797
1055
  const session = sessions.get(sessionId);
798
- if (!session) return null;
1056
+ if (!session || !session.tmuxName) return null;
799
1057
  return {
800
1058
  command: `tmux attach -t ${session.tmuxName}`,
801
- sessionId: session.claudeSessionId
1059
+ sessionId: session.providerSessionId
802
1060
  };
803
1061
  }
804
1062
  async function listTmuxSessions() {
@@ -946,13 +1204,55 @@ async function getPluginDetail(pluginName, marketplaceName) {
946
1204
  // src/output-handler.ts
947
1205
  import {
948
1206
  AttachmentBuilder,
949
- EmbedBuilder,
1207
+ EmbedBuilder as EmbedBuilder2,
950
1208
  ActionRowBuilder,
951
1209
  ButtonBuilder,
952
1210
  ButtonStyle,
953
1211
  StringSelectMenuBuilder
954
1212
  } from "discord.js";
955
1213
  import { existsSync as existsSync4 } from "fs";
1214
+
1215
+ // src/codex-renderer.ts
1216
+ import { EmbedBuilder } from "discord.js";
1217
+ function renderCommandExecutionEmbed(event) {
1218
+ const statusEmoji = event.status === "completed" ? event.exitCode === 0 ? "\u2705" : "\u274C" : event.status === "failed" ? "\u274C" : "\u{1F504}";
1219
+ const embed = new EmbedBuilder().setColor(event.exitCode === 0 ? 3066993 : event.status === "failed" ? 15158332 : 15965202).setTitle(`${statusEmoji} Command`);
1220
+ embed.setDescription(`\`\`\`bash
1221
+ $ ${truncate(event.command, 900)}
1222
+ \`\`\``);
1223
+ if (event.output) {
1224
+ embed.addFields({
1225
+ name: "Output",
1226
+ value: `\`\`\`
1227
+ ${truncate(event.output, 900)}
1228
+ \`\`\``
1229
+ });
1230
+ }
1231
+ if (event.exitCode !== null) {
1232
+ embed.setFooter({ text: `Exit code: ${event.exitCode}` });
1233
+ }
1234
+ return embed;
1235
+ }
1236
+ function renderFileChangesEmbed(event) {
1237
+ const kindEmoji = { add: "+", update: "~", delete: "-" };
1238
+ const lines = event.changes.map(
1239
+ (c) => `${kindEmoji[c.changeKind] || "?"} ${c.filePath}`
1240
+ );
1241
+ return new EmbedBuilder().setColor(3447003).setTitle("\u{1F4C1} Files Changed").setDescription(`\`\`\`diff
1242
+ ${truncate(lines.join("\n"), 3900)}
1243
+ \`\`\``);
1244
+ }
1245
+ function renderReasoningEmbed(event) {
1246
+ return new EmbedBuilder().setColor(10181046).setTitle("\u{1F9E0} Reasoning").setDescription(truncate(event.text, 4e3));
1247
+ }
1248
+ function renderCodexTodoListEmbed(event) {
1249
+ const lines = event.items.map(
1250
+ (item) => `${item.completed ? "\u2705" : "\u2B1C"} ${item.text}`
1251
+ ).join("\n");
1252
+ return new EmbedBuilder().setColor(3447003).setTitle("\u{1F4CB} Task List").setDescription(truncate(lines, 4e3));
1253
+ }
1254
+
1255
+ // src/output-handler.ts
956
1256
  var expandableStore = /* @__PURE__ */ new Map();
957
1257
  var expandCounter = 0;
958
1258
  var pendingAnswersStore = /* @__PURE__ */ new Map();
@@ -993,11 +1293,6 @@ function makeStopButton(sessionId) {
993
1293
  new ButtonBuilder().setCustomId(`stop:${sessionId}`).setLabel("Stop").setStyle(ButtonStyle.Danger)
994
1294
  );
995
1295
  }
996
- function makeCompletionButtons(sessionId) {
997
- return new ActionRowBuilder().addComponents(
998
- new ButtonBuilder().setCustomId(`continue:${sessionId}`).setLabel("Continue").setStyle(ButtonStyle.Primary)
999
- );
1000
- }
1001
1296
  function makeOptionButtons(sessionId, options) {
1002
1297
  const rows = [];
1003
1298
  const maxOptions = Math.min(options.length, 10);
@@ -1033,6 +1328,10 @@ function makeYesNoButtons(sessionId) {
1033
1328
  new ButtonBuilder().setCustomId(`confirm:${sessionId}:no`).setLabel("No").setStyle(ButtonStyle.Danger)
1034
1329
  );
1035
1330
  }
1331
+ function shouldSuppressCommandExecution(command) {
1332
+ const normalized = command.toLowerCase();
1333
+ return normalized.includes("total-recall");
1334
+ }
1036
1335
  var MessageStreamer = class {
1037
1336
  channel;
1038
1337
  sessionId;
@@ -1142,6 +1441,25 @@ var MessageStreamer = class {
1142
1441
  this.currentMessage = null;
1143
1442
  this.currentText = "";
1144
1443
  }
1444
+ /** Discard accumulated text and delete the live message if one exists */
1445
+ async discard() {
1446
+ if (this.timer) {
1447
+ clearTimeout(this.timer);
1448
+ this.timer = null;
1449
+ }
1450
+ while (this.flushing) {
1451
+ await new Promise((r) => setTimeout(r, 50));
1452
+ }
1453
+ if (this.currentMessage) {
1454
+ try {
1455
+ await this.currentMessage.delete();
1456
+ } catch {
1457
+ }
1458
+ this.currentMessage = null;
1459
+ }
1460
+ this.currentText = "";
1461
+ this.dirty = false;
1462
+ }
1145
1463
  getText() {
1146
1464
  return this.currentText;
1147
1465
  }
@@ -1152,31 +1470,6 @@ var MessageStreamer = class {
1152
1470
  }
1153
1471
  }
1154
1472
  };
1155
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"]);
1156
- function extractImagePath(toolName, toolInput) {
1157
- try {
1158
- const data = JSON.parse(toolInput);
1159
- if (toolName === "Write" || toolName === "Read") {
1160
- const filePath = data.file_path;
1161
- if (filePath && IMAGE_EXTENSIONS.has(filePath.slice(filePath.lastIndexOf(".")).toLowerCase())) {
1162
- return filePath;
1163
- }
1164
- }
1165
- } catch {
1166
- }
1167
- return null;
1168
- }
1169
- var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
1170
- "AskUserQuestion",
1171
- "EnterPlanMode",
1172
- "ExitPlanMode"
1173
- ]);
1174
- var TASK_TOOLS = /* @__PURE__ */ new Set([
1175
- "TaskCreate",
1176
- "TaskUpdate",
1177
- "TaskList",
1178
- "TaskGet"
1179
- ]);
1180
1473
  var STATUS_EMOJI = {
1181
1474
  pending: "\u2B1C",
1182
1475
  // white square
@@ -1187,9 +1480,39 @@ var STATUS_EMOJI = {
1187
1480
  deleted: "\u{1F5D1}\uFE0F"
1188
1481
  // wastebasket
1189
1482
  };
1190
- function renderAskUserQuestion(toolInput, sessionId) {
1483
+ function renderTaskToolEmbed(action, dataJson) {
1191
1484
  try {
1192
- const data = JSON.parse(toolInput);
1485
+ const data = JSON.parse(dataJson);
1486
+ if (action === "TaskCreate") {
1487
+ const embed = new EmbedBuilder2().setColor(3447003).setTitle("\u{1F4CB} New Task").setDescription(`**${data.subject || "Untitled"}**`);
1488
+ if (data.description) {
1489
+ embed.addFields({ name: "Details", value: truncate(data.description, 300) });
1490
+ }
1491
+ return embed;
1492
+ }
1493
+ if (action === "TaskUpdate") {
1494
+ const emoji = STATUS_EMOJI[data.status] || "\u{1F4CB}";
1495
+ const parts = [];
1496
+ if (data.status) parts.push(`${emoji} **${data.status}**`);
1497
+ if (data.subject) parts.push(data.subject);
1498
+ return new EmbedBuilder2().setColor(data.status === "completed" ? 3066993 : 15965202).setTitle(`Task #${data.taskId || "?"} Updated`).setDescription(parts.join(" \u2014 ") || "Updated");
1499
+ }
1500
+ return null;
1501
+ } catch {
1502
+ return null;
1503
+ }
1504
+ }
1505
+ function renderTaskListEmbed(resultText) {
1506
+ if (!resultText.trim()) return null;
1507
+ let formatted = resultText;
1508
+ for (const [status, emoji] of Object.entries(STATUS_EMOJI)) {
1509
+ formatted = formatted.replaceAll(status, `${emoji} ${status}`);
1510
+ }
1511
+ return new EmbedBuilder2().setColor(10181046).setTitle("\u{1F4CB} Task Board").setDescription(truncate(formatted, 4e3));
1512
+ }
1513
+ function renderAskUserQuestion(questionsJson, sessionId) {
1514
+ try {
1515
+ const data = JSON.parse(questionsJson);
1193
1516
  const questions = data.questions;
1194
1517
  if (!questions?.length) return null;
1195
1518
  const isMulti = questions.length > 1;
@@ -1203,7 +1526,7 @@ function renderAskUserQuestion(toolInput, sessionId) {
1203
1526
  const selectPrefix = isMulti ? "pick-select" : "answer-select";
1204
1527
  for (let qi = 0; qi < questions.length; qi++) {
1205
1528
  const q = questions[qi];
1206
- const embed = new EmbedBuilder().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
1529
+ const embed = new EmbedBuilder2().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
1207
1530
  if (q.options?.length) {
1208
1531
  if (q.options.length <= 4) {
1209
1532
  const row = new ActionRowBuilder();
@@ -1241,42 +1564,9 @@ function renderAskUserQuestion(toolInput, sessionId) {
1241
1564
  return null;
1242
1565
  }
1243
1566
  }
1244
- function renderTaskToolEmbed(toolName, toolInput) {
1245
- try {
1246
- const data = JSON.parse(toolInput);
1247
- if (toolName === "TaskCreate") {
1248
- const embed = new EmbedBuilder().setColor(3447003).setTitle("\u{1F4CB} New Task").setDescription(`**${data.subject || "Untitled"}**`);
1249
- if (data.description) {
1250
- embed.addFields({ name: "Details", value: truncate(data.description, 300) });
1251
- }
1252
- return embed;
1253
- }
1254
- if (toolName === "TaskUpdate") {
1255
- const emoji = STATUS_EMOJI[data.status] || "\u{1F4CB}";
1256
- const parts = [];
1257
- if (data.status) parts.push(`${emoji} **${data.status}**`);
1258
- if (data.subject) parts.push(data.subject);
1259
- return new EmbedBuilder().setColor(data.status === "completed" ? 3066993 : 15965202).setTitle(`Task #${data.taskId || "?"} Updated`).setDescription(parts.join(" \u2014 ") || "Updated");
1260
- }
1261
- return null;
1262
- } catch {
1263
- return null;
1264
- }
1265
- }
1266
- function renderTaskListEmbed(resultText) {
1267
- if (!resultText.trim()) return null;
1268
- let formatted = resultText;
1269
- for (const [status, emoji] of Object.entries(STATUS_EMOJI)) {
1270
- formatted = formatted.replaceAll(status, `${emoji} ${status}`);
1271
- }
1272
- return new EmbedBuilder().setColor(10181046).setTitle("\u{1F4CB} Task Board").setDescription(truncate(formatted, 4e3));
1273
- }
1274
- async function handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto") {
1567
+ async function handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto", _provider = "claude") {
1275
1568
  const streamer = new MessageStreamer(channel, sessionId);
1276
- let currentToolName = null;
1277
- let currentToolInput = "";
1278
- let lastFinishedToolName = null;
1279
- let pendingImagePath = null;
1569
+ let lastToolName = null;
1280
1570
  channel.sendTyping().catch(() => {
1281
1571
  });
1282
1572
  const typingInterval = setInterval(() => {
@@ -1284,115 +1574,78 @@ async function handleOutputStream(stream, channel, sessionId, verbose = false, m
1284
1574
  });
1285
1575
  }, 8e3);
1286
1576
  try {
1287
- for await (const message of stream) {
1288
- if (message.type === "stream_event") {
1289
- const event = message.event;
1290
- if (event?.type === "content_block_start") {
1291
- if (event.content_block?.type === "tool_use") {
1292
- await streamer.finalize();
1293
- currentToolName = event.content_block.name || "tool";
1294
- currentToolInput = "";
1295
- }
1577
+ for await (const event of stream) {
1578
+ switch (event.type) {
1579
+ case "text_delta": {
1580
+ streamer.append(event.text);
1581
+ break;
1296
1582
  }
1297
- if (event?.type === "content_block_delta") {
1298
- if (event.delta?.type === "text_delta" && event.delta.text) {
1299
- streamer.append(event.delta.text);
1300
- }
1301
- if (event.delta?.type === "input_json_delta" && event.delta.partial_json) {
1302
- currentToolInput += event.delta.partial_json;
1583
+ case "ask_user": {
1584
+ await streamer.discard();
1585
+ const rendered = renderAskUserQuestion(event.questionsJson, sessionId);
1586
+ if (rendered) {
1587
+ rendered.components.push(makeStopButton(sessionId));
1588
+ await channel.send({ embeds: rendered.embeds, components: rendered.components });
1303
1589
  }
1590
+ break;
1304
1591
  }
1305
- if (event?.type === "content_block_stop") {
1306
- if (currentToolName) {
1307
- const isUserFacing = USER_FACING_TOOLS.has(currentToolName);
1308
- const isTaskTool = TASK_TOOLS.has(currentToolName);
1309
- const showTool = verbose || isUserFacing || isTaskTool;
1310
- if (showTool) {
1311
- const taskEmbed = isTaskTool ? renderTaskToolEmbed(currentToolName, currentToolInput) : null;
1312
- if (taskEmbed) {
1313
- await channel.send({
1314
- embeds: [taskEmbed],
1315
- components: [makeStopButton(sessionId)]
1316
- });
1317
- } else if (currentToolName === "AskUserQuestion") {
1318
- const rendered = renderAskUserQuestion(currentToolInput, sessionId);
1319
- if (rendered) {
1320
- rendered.components.push(makeStopButton(sessionId));
1321
- await channel.send({ embeds: rendered.embeds, components: rendered.components });
1322
- }
1323
- } else if (!isTaskTool) {
1324
- const toolInput = currentToolInput;
1325
- const displayInput = toolInput.length > 1e3 ? truncate(toolInput, 1e3) : toolInput;
1326
- const embed = new EmbedBuilder().setColor(isUserFacing ? 15965202 : 3447003).setTitle(isUserFacing ? `Waiting for input: ${currentToolName}` : `Tool: ${currentToolName}`).setDescription(`\`\`\`json
1327
- ${displayInput}
1328
- \`\`\``);
1329
- const components = [makeStopButton(sessionId)];
1330
- if (toolInput.length > 1e3) {
1331
- const contentId = storeExpandable(toolInput);
1332
- components.unshift(
1333
- new ActionRowBuilder().addComponents(
1334
- new ButtonBuilder().setCustomId(`expand:${contentId}`).setLabel("Show Full Input").setStyle(ButtonStyle.Secondary)
1335
- )
1336
- );
1337
- }
1338
- await channel.send({ embeds: [embed], components });
1339
- }
1592
+ case "task": {
1593
+ await streamer.finalize();
1594
+ const isTaskResult = event.action === "TaskList" || event.action === "TaskGet";
1595
+ if (!isTaskResult) {
1596
+ const taskEmbed = renderTaskToolEmbed(event.action, event.dataJson);
1597
+ if (taskEmbed) {
1598
+ await channel.send({
1599
+ embeds: [taskEmbed],
1600
+ components: [makeStopButton(sessionId)]
1601
+ });
1340
1602
  }
1341
- pendingImagePath = extractImagePath(currentToolName, currentToolInput);
1342
- lastFinishedToolName = currentToolName;
1343
- currentToolName = null;
1344
- currentToolInput = "";
1345
- }
1346
- }
1347
- }
1348
- if (message.type === "user") {
1349
- if (pendingImagePath && existsSync4(pendingImagePath)) {
1350
- try {
1351
- await streamer.finalize();
1352
- const attachment = new AttachmentBuilder(pendingImagePath);
1353
- await channel.send({ files: [attachment] });
1354
- } catch {
1355
1603
  }
1356
- pendingImagePath = null;
1357
- } else {
1358
- pendingImagePath = null;
1604
+ lastToolName = event.action;
1605
+ break;
1359
1606
  }
1360
- const showResult = verbose || lastFinishedToolName !== null && TASK_TOOLS.has(lastFinishedToolName);
1361
- if (!showResult) continue;
1362
- await streamer.finalize();
1363
- const content = message.message?.content;
1364
- let resultText = "";
1365
- if (Array.isArray(content)) {
1366
- for (const block of content) {
1367
- if (block.type === "tool_result" && block.content) {
1368
- if (typeof block.content === "string") {
1369
- resultText += block.content;
1370
- } else if (Array.isArray(block.content)) {
1371
- for (const sub of block.content) {
1372
- if (sub.type === "text") resultText += sub.text;
1373
- }
1374
- }
1607
+ case "tool_start": {
1608
+ await streamer.finalize();
1609
+ if (verbose) {
1610
+ const displayInput = event.toolInput.length > 1e3 ? truncate(event.toolInput, 1e3) : event.toolInput;
1611
+ const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Tool: ${event.toolName}`).setDescription(`\`\`\`json
1612
+ ${displayInput}
1613
+ \`\`\``);
1614
+ const components = [makeStopButton(sessionId)];
1615
+ if (event.toolInput.length > 1e3) {
1616
+ const contentId = storeExpandable(event.toolInput);
1617
+ components.unshift(
1618
+ new ActionRowBuilder().addComponents(
1619
+ new ButtonBuilder().setCustomId(`expand:${contentId}`).setLabel("Show Full Input").setStyle(ButtonStyle.Secondary)
1620
+ )
1621
+ );
1375
1622
  }
1623
+ await channel.send({ embeds: [embed], components });
1376
1624
  }
1625
+ lastToolName = event.toolName;
1626
+ break;
1377
1627
  }
1378
- if (resultText) {
1379
- const isTaskResult = lastFinishedToolName !== null && TASK_TOOLS.has(lastFinishedToolName);
1628
+ case "tool_result": {
1629
+ const isTaskResult = lastToolName !== null && (lastToolName === "TaskList" || lastToolName === "TaskGet");
1630
+ const showResult = verbose || isTaskResult;
1631
+ if (!showResult) break;
1632
+ await streamer.finalize();
1380
1633
  if (isTaskResult && !verbose) {
1381
- const boardEmbed = renderTaskListEmbed(resultText);
1634
+ const boardEmbed = renderTaskListEmbed(event.result);
1382
1635
  if (boardEmbed) {
1383
1636
  await channel.send({
1384
1637
  embeds: [boardEmbed],
1385
1638
  components: [makeStopButton(sessionId)]
1386
1639
  });
1387
1640
  }
1388
- } else {
1389
- const displayResult = resultText.length > 1e3 ? truncate(resultText, 1e3) : resultText;
1390
- const embed = new EmbedBuilder().setColor(1752220).setTitle("Tool Result").setDescription(`\`\`\`
1641
+ } else if (event.result) {
1642
+ const displayResult = event.result.length > 1e3 ? truncate(event.result, 1e3) : event.result;
1643
+ const embed = new EmbedBuilder2().setColor(1752220).setTitle("Tool Result").setDescription(`\`\`\`
1391
1644
  ${displayResult}
1392
1645
  \`\`\``);
1393
1646
  const components = [makeStopButton(sessionId)];
1394
- if (resultText.length > 1e3) {
1395
- const contentId = storeExpandable(resultText);
1647
+ if (event.result.length > 1e3) {
1648
+ const contentId = storeExpandable(event.result);
1396
1649
  components.unshift(
1397
1650
  new ActionRowBuilder().addComponents(
1398
1651
  new ButtonBuilder().setCustomId(`expand:${contentId}`).setLabel("Show Full Output").setStyle(ButtonStyle.Secondary)
@@ -1401,46 +1654,104 @@ ${displayResult}
1401
1654
  }
1402
1655
  await channel.send({ embeds: [embed], components });
1403
1656
  }
1657
+ break;
1404
1658
  }
1405
- }
1406
- if (message.type === "result") {
1407
- const lastText = streamer.getText();
1408
- await streamer.finalize();
1409
- const result = message;
1410
- const isSuccess = result.subtype === "success";
1411
- const cost = result.total_cost_usd?.toFixed(4) || "0.0000";
1412
- const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "unknown";
1413
- const turns = result.num_turns || 0;
1414
- const embed = new EmbedBuilder().setColor(isSuccess ? 3066993 : 15158332).setTitle(isSuccess ? "Completed" : "Error").addFields(
1415
- { name: "Cost", value: `$${cost}`, inline: true },
1416
- { name: "Duration", value: duration, inline: true },
1417
- { name: "Turns", value: `${turns}`, inline: true },
1418
- { name: "Mode", value: { auto: "\u26A1 Auto", plan: "\u{1F4CB} Plan", normal: "\u{1F6E1}\uFE0F Normal" }[mode] || "\u26A1 Auto", inline: true }
1419
- );
1420
- if (result.session_id) {
1421
- embed.setFooter({ text: `Session: ${result.session_id}` });
1659
+ case "image_file": {
1660
+ if (existsSync4(event.filePath)) {
1661
+ await streamer.finalize();
1662
+ const attachment = new AttachmentBuilder(event.filePath);
1663
+ await channel.send({ files: [attachment] });
1664
+ }
1665
+ break;
1666
+ }
1667
+ // ── Codex-specific events ──
1668
+ case "command_execution": {
1669
+ if (shouldSuppressCommandExecution(event.command)) break;
1670
+ await streamer.finalize();
1671
+ const embed = renderCommandExecutionEmbed(event);
1672
+ await channel.send({
1673
+ embeds: [embed],
1674
+ components: [makeStopButton(sessionId)]
1675
+ });
1676
+ break;
1677
+ }
1678
+ case "file_change": {
1679
+ await streamer.finalize();
1680
+ const embed = renderFileChangesEmbed(event);
1681
+ await channel.send({
1682
+ embeds: [embed],
1683
+ components: [makeStopButton(sessionId)]
1684
+ });
1685
+ break;
1686
+ }
1687
+ case "reasoning": {
1688
+ if (verbose) {
1689
+ await streamer.finalize();
1690
+ const embed = renderReasoningEmbed(event);
1691
+ await channel.send({
1692
+ embeds: [embed],
1693
+ components: [makeStopButton(sessionId)]
1694
+ });
1695
+ }
1696
+ break;
1422
1697
  }
1423
- if (!isSuccess && result.errors?.length) {
1424
- embed.setDescription(result.errors.join("\n"));
1698
+ case "todo_list": {
1699
+ await streamer.finalize();
1700
+ const embed = renderCodexTodoListEmbed(event);
1701
+ await channel.send({
1702
+ embeds: [embed],
1703
+ components: [makeStopButton(sessionId)]
1704
+ });
1705
+ break;
1425
1706
  }
1426
- const components = [];
1427
- const checkText = lastText || result.result || "";
1428
- const options = detectNumberedOptions(checkText);
1429
- if (options) {
1430
- components.push(...makeOptionButtons(sessionId, options));
1431
- } else if (detectYesNoPrompt(checkText)) {
1432
- components.push(makeYesNoButtons(sessionId));
1707
+ // ── Shared events ──
1708
+ case "result": {
1709
+ const lastText = streamer.getText();
1710
+ const cost = event.costUsd.toFixed(4);
1711
+ const duration = event.durationMs ? `${(event.durationMs / 1e3).toFixed(1)}s` : "unknown";
1712
+ const turns = event.numTurns || 0;
1713
+ const modeLabel = { auto: "Auto", plan: "Plan", normal: "Normal" }[mode] || "Auto";
1714
+ const statusLine = event.success ? `-# $${cost} | ${duration} | ${turns} turns | ${modeLabel}` : `-# Error | $${cost} | ${duration} | ${turns} turns`;
1715
+ streamer.append(`
1716
+ ${statusLine}`);
1717
+ if (!event.success && event.errors.length) {
1718
+ streamer.append(`
1719
+ \`\`\`
1720
+ ${event.errors.join("\n")}
1721
+ \`\`\``);
1722
+ }
1723
+ await streamer.finalize();
1724
+ const components = [];
1725
+ const checkText = lastText || "";
1726
+ const options = detectNumberedOptions(checkText);
1727
+ if (options) {
1728
+ components.push(...makeOptionButtons(sessionId, options));
1729
+ } else if (detectYesNoPrompt(checkText)) {
1730
+ components.push(makeYesNoButtons(sessionId));
1731
+ }
1732
+ components.push(makeModeButtons(sessionId, mode));
1733
+ await channel.send({ components });
1734
+ break;
1735
+ }
1736
+ case "error": {
1737
+ await streamer.finalize();
1738
+ const embed = new EmbedBuilder2().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1739
+ ${event.message}
1740
+ \`\`\``);
1741
+ await channel.send({ embeds: [embed] });
1742
+ break;
1743
+ }
1744
+ case "session_init": {
1745
+ break;
1433
1746
  }
1434
- components.push(makeModeButtons(sessionId, mode));
1435
- components.push(makeCompletionButtons(sessionId));
1436
- await channel.send({ embeds: [embed], components });
1437
1747
  }
1438
1748
  }
1439
1749
  } catch (err) {
1440
1750
  await streamer.finalize();
1441
- if (err.name !== "AbortError") {
1442
- const embed = new EmbedBuilder().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1443
- ${err.message}
1751
+ if (!isAbortError(err)) {
1752
+ const errMsg = err.message || "";
1753
+ const embed = new EmbedBuilder2().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1754
+ ${errMsg}
1444
1755
  \`\`\``);
1445
1756
  await channel.send({ embeds: [embed] });
1446
1757
  }
@@ -1573,7 +1884,7 @@ async function ensureProjectCategory(guild, projectName, directory) {
1573
1884
  updateProjectCategory(projectName, category.id, logChannel2.id);
1574
1885
  return { category, logChannel: logChannel2 };
1575
1886
  }
1576
- async function handleClaude(interaction) {
1887
+ async function handleSession(interaction) {
1577
1888
  if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
1578
1889
  await interaction.reply({ content: "You are not authorized to use this bot.", ephemeral: true });
1579
1890
  return;
@@ -1581,35 +1892,46 @@ async function handleClaude(interaction) {
1581
1892
  const sub = interaction.options.getSubcommand();
1582
1893
  switch (sub) {
1583
1894
  case "new":
1584
- return handleClaudeNew(interaction);
1895
+ return handleSessionNew(interaction);
1585
1896
  case "resume":
1586
- return handleClaudeResume(interaction);
1897
+ return handleSessionResume(interaction);
1587
1898
  case "list":
1588
- return handleClaudeList(interaction);
1899
+ return handleSessionList(interaction);
1589
1900
  case "end":
1590
- return handleClaudeEnd(interaction);
1901
+ return handleSessionEnd(interaction);
1591
1902
  case "continue":
1592
- return handleClaudeContinue(interaction);
1903
+ return handleSessionContinue(interaction);
1593
1904
  case "stop":
1594
- return handleClaudeStop(interaction);
1905
+ return handleSessionStop(interaction);
1595
1906
  case "output":
1596
- return handleClaudeOutput(interaction);
1907
+ return handleSessionOutput(interaction);
1597
1908
  case "attach":
1598
- return handleClaudeAttach(interaction);
1909
+ return handleSessionAttach(interaction);
1599
1910
  case "sync":
1600
- return handleClaudeSync(interaction);
1911
+ return handleSessionSync(interaction);
1912
+ case "id":
1913
+ return handleSessionId(interaction);
1601
1914
  case "model":
1602
- return handleClaudeModel(interaction);
1915
+ return handleSessionModel(interaction);
1603
1916
  case "verbose":
1604
- return handleClaudeVerbose(interaction);
1917
+ return handleSessionVerbose(interaction);
1605
1918
  case "mode":
1606
- return handleClaudeMode(interaction);
1919
+ return handleSessionMode(interaction);
1607
1920
  default:
1608
1921
  await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
1609
1922
  }
1610
1923
  }
1611
- async function handleClaudeNew(interaction) {
1924
+ var PROVIDER_LABELS = {
1925
+ claude: "Claude Code",
1926
+ codex: "OpenAI Codex"
1927
+ };
1928
+ var PROVIDER_COLORS = {
1929
+ claude: 3447003,
1930
+ codex: 1090431
1931
+ };
1932
+ async function handleSessionNew(interaction) {
1612
1933
  const name = interaction.options.getString("name", true);
1934
+ const provider = interaction.options.getString("provider") || "claude";
1613
1935
  let directory = interaction.options.getString("directory");
1614
1936
  if (!directory) {
1615
1937
  const parentId = interaction.channel?.parentId;
@@ -1625,30 +1947,35 @@ async function handleClaudeNew(interaction) {
1625
1947
  const guild = interaction.guild;
1626
1948
  const projectName = projectNameFromDir(directory);
1627
1949
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1628
- const session = await createSession(name, directory, "pending", projectName);
1950
+ const session = await createSession(name, directory, "pending", projectName, provider);
1629
1951
  channel = await guild.channels.create({
1630
- name: `claude-${session.id}`,
1952
+ name: `${provider}-${session.id}`,
1631
1953
  type: ChannelType.GuildText,
1632
1954
  parent: category.id,
1633
- topic: `Claude session | Dir: ${directory}`
1955
+ topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
1634
1956
  });
1635
1957
  linkChannel(session.id, channel.id);
1636
- const embed = new EmbedBuilder2().setColor(3066993).setTitle(`Session Created: ${session.id}`).addFields(
1637
- { name: "Channel", value: `#claude-${session.id}`, inline: true },
1958
+ const fields = [
1959
+ { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
1960
+ { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
1638
1961
  { name: "Directory", value: session.directory, inline: true },
1639
- { name: "Project", value: projectName, inline: true },
1640
- { name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1641
- );
1962
+ { name: "Project", value: projectName, inline: true }
1963
+ ];
1964
+ if (session.tmuxName) {
1965
+ fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1966
+ }
1967
+ const embed = new EmbedBuilder3().setColor(3066993).setTitle(`Session Created: ${session.id}`).addFields(fields);
1642
1968
  await interaction.editReply({ embeds: [embed] });
1643
- log(`Session "${session.id}" created by ${interaction.user.tag} in ${directory}`);
1644
- await channel.send({
1645
- embeds: [
1646
- new EmbedBuilder2().setColor(3447003).setTitle("Claude Code Session").setDescription("Type a message to send it to Claude. Use `/claude stop` to cancel generation.").addFields(
1647
- { name: "Directory", value: `\`${session.directory}\``, inline: false },
1648
- { name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1649
- )
1650
- ]
1651
- });
1969
+ log(`Session "${session.id}" (${provider}) created by ${interaction.user.tag} in ${directory}`);
1970
+ 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.`);
1971
+ const welcomeFields = [
1972
+ { name: "Directory", value: `\`${session.directory}\``, inline: false }
1973
+ ];
1974
+ if (session.tmuxName) {
1975
+ welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1976
+ }
1977
+ welcomeEmbed.addFields(welcomeFields);
1978
+ await channel.send({ embeds: [welcomeEmbed] });
1652
1979
  } catch (err) {
1653
1980
  if (channel) {
1654
1981
  try {
@@ -1748,7 +2075,7 @@ function formatTimeAgo(mtime) {
1748
2075
  if (ago < 864e5) return `${Math.floor(ago / 36e5)}h ago`;
1749
2076
  return `${Math.floor(ago / 864e5)}d ago`;
1750
2077
  }
1751
- async function handleClaudeAutocomplete(interaction) {
2078
+ async function handleSessionAutocomplete(interaction) {
1752
2079
  const focused = interaction.options.getFocused();
1753
2080
  const localSessions = discoverLocalSessions();
1754
2081
  const filtered = focused ? localSessions.filter((s) => s.id.includes(focused.toLowerCase()) || s.project.toLowerCase().includes(focused.toLowerCase())) : localSessions;
@@ -1763,17 +2090,20 @@ async function handleClaudeAutocomplete(interaction) {
1763
2090
  );
1764
2091
  await interaction.respond(choices);
1765
2092
  }
1766
- async function handleClaudeResume(interaction) {
1767
- const claudeSessionId = interaction.options.getString("session-id", true);
2093
+ async function handleSessionResume(interaction) {
2094
+ const providerSessionId = interaction.options.getString("session-id", true);
1768
2095
  const name = interaction.options.getString("name", true);
2096
+ const provider = interaction.options.getString("provider") || "claude";
1769
2097
  const directory = interaction.options.getString("directory") || config.defaultDirectory;
1770
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1771
- if (!uuidRegex.test(claudeSessionId)) {
1772
- await interaction.reply({
1773
- content: "Invalid session ID. Expected a UUID like `9815d35d-6508-476e-8c40-40effa4ffd6b`.",
1774
- ephemeral: true
1775
- });
1776
- return;
2098
+ if (provider === "claude") {
2099
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2100
+ if (!uuidRegex.test(providerSessionId)) {
2101
+ await interaction.reply({
2102
+ content: "Invalid session ID. Expected a UUID like `9815d35d-6508-476e-8c40-40effa4ffd6b`.",
2103
+ ephemeral: true
2104
+ });
2105
+ return;
2106
+ }
1777
2107
  }
1778
2108
  await interaction.deferReply();
1779
2109
  let channel;
@@ -1781,32 +2111,39 @@ async function handleClaudeResume(interaction) {
1781
2111
  const guild = interaction.guild;
1782
2112
  const projectName = projectNameFromDir(directory);
1783
2113
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1784
- const session = await createSession(name, directory, "pending", projectName, claudeSessionId);
2114
+ const session = await createSession(name, directory, "pending", projectName, provider, providerSessionId);
1785
2115
  channel = await guild.channels.create({
1786
- name: `claude-${session.id}`,
2116
+ name: `${provider}-${session.id}`,
1787
2117
  type: ChannelType.GuildText,
1788
2118
  parent: category.id,
1789
- topic: `Claude session (resumed) | Dir: ${directory}`
2119
+ topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
1790
2120
  });
1791
2121
  linkChannel(session.id, channel.id);
1792
- const embed = new EmbedBuilder2().setColor(15105570).setTitle(`Session Resumed: ${session.id}`).addFields(
1793
- { name: "Channel", value: `#claude-${session.id}`, inline: true },
2122
+ const fields = [
2123
+ { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
2124
+ { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
1794
2125
  { name: "Directory", value: session.directory, inline: true },
1795
2126
  { name: "Project", value: projectName, inline: true },
1796
- { name: "Claude Session", value: `\`${claudeSessionId}\``, inline: false },
1797
- { name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1798
- );
2127
+ { name: "Provider Session", value: `\`${providerSessionId}\``, inline: false }
2128
+ ];
2129
+ if (session.tmuxName) {
2130
+ fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2131
+ }
2132
+ const embed = new EmbedBuilder3().setColor(15105570).setTitle(`Session Resumed: ${session.id}`).addFields(fields);
1799
2133
  await interaction.editReply({ embeds: [embed] });
1800
- log(`Session "${session.id}" (resumed ${claudeSessionId}) created by ${interaction.user.tag} in ${directory}`);
2134
+ log(`Session "${session.id}" (${provider}, resumed ${providerSessionId}) created by ${interaction.user.tag} in ${directory}`);
2135
+ const welcomeFields = [
2136
+ { name: "Directory", value: `\`${session.directory}\``, inline: false },
2137
+ { name: "Provider Session", value: `\`${providerSessionId}\``, inline: false }
2138
+ ];
2139
+ if (session.tmuxName) {
2140
+ welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2141
+ }
1801
2142
  await channel.send({
1802
2143
  embeds: [
1803
- new EmbedBuilder2().setColor(15105570).setTitle("Claude Code Session (Resumed)").setDescription(
1804
- "This session is linked to an existing Claude Code conversation. Type a message to continue the conversation from Discord."
1805
- ).addFields(
1806
- { name: "Directory", value: `\`${session.directory}\``, inline: false },
1807
- { name: "Claude Session", value: `\`${claudeSessionId}\``, inline: false },
1808
- { name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false }
1809
- )
2144
+ new EmbedBuilder3().setColor(15105570).setTitle(`${PROVIDER_LABELS[provider]} Session (Resumed)`).setDescription(
2145
+ `This session is linked to an existing ${PROVIDER_LABELS[provider]} conversation. Type a message to continue the conversation from Discord.`
2146
+ ).addFields(welcomeFields)
1810
2147
  ]
1811
2148
  });
1812
2149
  } catch (err) {
@@ -1819,7 +2156,7 @@ async function handleClaudeResume(interaction) {
1819
2156
  await interaction.editReply(`Failed to resume session: ${err.message}`);
1820
2157
  }
1821
2158
  }
1822
- async function handleClaudeList(interaction) {
2159
+ async function handleSessionList(interaction) {
1823
2160
  const allSessions = getAllSessions();
1824
2161
  if (allSessions.length === 0) {
1825
2162
  await interaction.reply({ content: "No active sessions.", ephemeral: true });
@@ -1831,33 +2168,42 @@ async function handleClaudeList(interaction) {
1831
2168
  arr.push(s);
1832
2169
  grouped.set(s.projectName, arr);
1833
2170
  }
1834
- const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Active Sessions (${allSessions.length})`);
2171
+ const embed = new EmbedBuilder3().setColor(3447003).setTitle(`Active Sessions (${allSessions.length})`);
1835
2172
  for (const [project, projectSessions] of grouped) {
1836
2173
  const lines = projectSessions.map((s) => {
1837
2174
  const status = s.isGenerating ? "\u{1F7E2} generating" : "\u26AA idle";
1838
2175
  const modeEmoji = { auto: "\u26A1", plan: "\u{1F4CB}", normal: "\u{1F6E1}\uFE0F" }[s.mode] || "\u26A1";
1839
- return `**${s.id}** \u2014 ${status} ${modeEmoji} ${s.mode} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}`;
2176
+ const providerTag = `[${s.provider}]`;
2177
+ return `**${s.id}** ${providerTag} \u2014 ${status} ${modeEmoji} ${s.mode} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}`;
1840
2178
  });
1841
2179
  embed.addFields({ name: `\u{1F4C1} ${project}`, value: lines.join("\n") });
1842
2180
  }
1843
2181
  await interaction.reply({ embeds: [embed] });
1844
2182
  }
1845
- async function handleClaudeEnd(interaction) {
2183
+ async function handleSessionEnd(interaction) {
1846
2184
  const session = getSessionByChannel(interaction.channelId);
1847
2185
  if (!session) {
1848
2186
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
1849
2187
  return;
1850
2188
  }
2189
+ const channel = interaction.channel;
1851
2190
  await interaction.deferReply();
1852
2191
  try {
1853
2192
  await endSession(session.id);
1854
- await interaction.editReply(`Session "${session.id}" ended. You can delete this channel.`);
1855
2193
  log(`Session "${session.id}" ended by ${interaction.user.tag}`);
2194
+ await interaction.editReply(`Session "${session.id}" ended. Deleting channel...`);
2195
+ setTimeout(async () => {
2196
+ try {
2197
+ await channel?.delete();
2198
+ } catch (err) {
2199
+ log(`Failed to delete channel for session "${session.id}": ${err.message}`);
2200
+ }
2201
+ }, 2e3);
1856
2202
  } catch (err) {
1857
2203
  await interaction.editReply(`Failed to end session: ${err.message}`);
1858
2204
  }
1859
2205
  }
1860
- async function handleClaudeContinue(interaction) {
2206
+ async function handleSessionContinue(interaction) {
1861
2207
  const session = getSessionByChannel(interaction.channelId);
1862
2208
  if (!session) {
1863
2209
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1872,12 +2218,12 @@ async function handleClaudeContinue(interaction) {
1872
2218
  const channel = interaction.channel;
1873
2219
  const stream = continueSession(session.id);
1874
2220
  await interaction.editReply("Continuing...");
1875
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
2221
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
1876
2222
  } catch (err) {
1877
2223
  await interaction.editReply(`Error: ${err.message}`);
1878
2224
  }
1879
2225
  }
1880
- async function handleClaudeStop(interaction) {
2226
+ async function handleSessionStop(interaction) {
1881
2227
  const session = getSessionByChannel(interaction.channelId);
1882
2228
  if (!session) {
1883
2229
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1889,18 +2235,18 @@ async function handleClaudeStop(interaction) {
1889
2235
  ephemeral: true
1890
2236
  });
1891
2237
  }
1892
- async function handleClaudeOutput(interaction) {
2238
+ async function handleSessionOutput(interaction) {
1893
2239
  const session = getSessionByChannel(interaction.channelId);
1894
2240
  if (!session) {
1895
2241
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
1896
2242
  return;
1897
2243
  }
1898
2244
  await interaction.reply({
1899
- content: "Conversation history is managed by the Claude Code SDK. Use `/claude attach` to view the full terminal history.",
2245
+ content: "Conversation history is managed by the provider SDK. Use `/session attach` to view the full terminal history.",
1900
2246
  ephemeral: true
1901
2247
  });
1902
2248
  }
1903
- async function handleClaudeAttach(interaction) {
2249
+ async function handleSessionAttach(interaction) {
1904
2250
  const session = getSessionByChannel(interaction.channelId);
1905
2251
  if (!session) {
1906
2252
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1908,10 +2254,13 @@ async function handleClaudeAttach(interaction) {
1908
2254
  }
1909
2255
  const info = getAttachInfo(session.id);
1910
2256
  if (!info) {
1911
- await interaction.reply({ content: "Session not found.", ephemeral: true });
2257
+ await interaction.reply({
2258
+ content: `Terminal attach is not available for ${PROVIDER_LABELS[session.provider]} sessions.`,
2259
+ ephemeral: true
2260
+ });
1912
2261
  return;
1913
2262
  }
1914
- const embed = new EmbedBuilder2().setColor(10181046).setTitle("Terminal Access").addFields(
2263
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle("Terminal Access").addFields(
1915
2264
  { name: "Attach to tmux", value: `\`\`\`
1916
2265
  ${info.command}
1917
2266
  \`\`\`` }
@@ -1926,7 +2275,7 @@ cd ${session.directory} && claude --resume ${info.sessionId}
1926
2275
  }
1927
2276
  await interaction.reply({ embeds: [embed], ephemeral: true });
1928
2277
  }
1929
- async function handleClaudeSync(interaction) {
2278
+ async function handleSessionSync(interaction) {
1930
2279
  await interaction.deferReply();
1931
2280
  const guild = interaction.guild;
1932
2281
  const tmuxSessions = await listTmuxSessions();
@@ -1941,16 +2290,36 @@ async function handleClaudeSync(interaction) {
1941
2290
  name: `claude-${tmuxSession.id}`,
1942
2291
  type: ChannelType.GuildText,
1943
2292
  parent: category.id,
1944
- topic: `Claude session (synced) | Dir: ${tmuxSession.directory}`
2293
+ topic: `Claude Code session (synced) | Dir: ${tmuxSession.directory}`
1945
2294
  });
1946
- await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName);
2295
+ await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName, "claude");
1947
2296
  synced++;
1948
2297
  }
1949
2298
  await interaction.editReply(
1950
2299
  synced > 0 ? `Synced ${synced} orphaned session(s).` : "No orphaned sessions found."
1951
2300
  );
1952
2301
  }
1953
- async function handleClaudeModel(interaction) {
2302
+ async function handleSessionId(interaction) {
2303
+ const session = getSessionByChannel(interaction.channelId);
2304
+ if (!session) {
2305
+ await interaction.reply({ content: "No session in this channel.", ephemeral: true });
2306
+ return;
2307
+ }
2308
+ const providerSessionId = session.providerSessionId;
2309
+ if (!providerSessionId) {
2310
+ await interaction.reply({
2311
+ content: "No provider session ID yet. Send a message first to initialize the session.",
2312
+ ephemeral: true
2313
+ });
2314
+ return;
2315
+ }
2316
+ await interaction.reply({
2317
+ content: `**Provider session ID** (${session.provider}):
2318
+ \`${providerSessionId}\``,
2319
+ ephemeral: true
2320
+ });
2321
+ }
2322
+ async function handleSessionModel(interaction) {
1954
2323
  const session = getSessionByChannel(interaction.channelId);
1955
2324
  if (!session) {
1956
2325
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1960,7 +2329,7 @@ async function handleClaudeModel(interaction) {
1960
2329
  setModel(session.id, model);
1961
2330
  await interaction.reply({ content: `Model set to \`${model}\` for this session.`, ephemeral: true });
1962
2331
  }
1963
- async function handleClaudeVerbose(interaction) {
2332
+ async function handleSessionVerbose(interaction) {
1964
2333
  const session = getSessionByChannel(interaction.channelId);
1965
2334
  if (!session) {
1966
2335
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1978,7 +2347,7 @@ var MODE_LABELS = {
1978
2347
  plan: "\u{1F4CB} Plan \u2014 always plans before executing changes",
1979
2348
  normal: "\u{1F6E1}\uFE0F Normal \u2014 asks before destructive operations"
1980
2349
  };
1981
- async function handleClaudeMode(interaction) {
2350
+ async function handleSessionMode(interaction) {
1982
2351
  const session = getSessionByChannel(interaction.channelId);
1983
2352
  if (!session) {
1984
2353
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -2061,7 +2430,7 @@ async function handleAgent(interaction) {
2061
2430
  }
2062
2431
  case "list": {
2063
2432
  const agents2 = listAgents();
2064
- const embed = new EmbedBuilder2().setColor(10181046).setTitle("Agent Personas").setDescription(agents2.map((a) => `${a.emoji} **${a.name}** \u2014 ${a.description}`).join("\n"));
2433
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle("Agent Personas").setDescription(agents2.map((a) => `${a.emoji} **${a.name}** \u2014 ${a.description}`).join("\n"));
2065
2434
  await interaction.reply({ embeds: [embed], ephemeral: true });
2066
2435
  break;
2067
2436
  }
@@ -2154,7 +2523,7 @@ ${list}`, ephemeral: true });
2154
2523
  const channel = interaction.channel;
2155
2524
  await interaction.editReply(`Running skill **${name}**...`);
2156
2525
  const stream = sendPrompt(session.id, expanded);
2157
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
2526
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
2158
2527
  } catch (err) {
2159
2528
  await interaction.editReply(`Error: ${err.message}`);
2160
2529
  }
@@ -2199,7 +2568,7 @@ ${list}`, ephemeral: true });
2199
2568
  await interaction.reply({ content: "Project not found.", ephemeral: true });
2200
2569
  return;
2201
2570
  }
2202
- const embed = new EmbedBuilder2().setColor(15965202).setTitle(`Project: ${projectName}`).addFields(
2571
+ const embed = new EmbedBuilder3().setColor(15965202).setTitle(`Project: ${projectName}`).addFields(
2203
2572
  { name: "Directory", value: `\`${project.directory}\``, inline: false },
2204
2573
  {
2205
2574
  name: "Personality",
@@ -2282,7 +2651,7 @@ async function handlePluginBrowse(interaction) {
2282
2651
  return;
2283
2652
  }
2284
2653
  const shown = filtered.slice(0, 15);
2285
- const embed = new EmbedBuilder2().setColor(8141549).setTitle("Available Plugins").setDescription(`Showing ${shown.length} of ${filtered.length} plugins. Use \`/plugin install\` to install.`);
2654
+ const embed = new EmbedBuilder3().setColor(8141549).setTitle("Available Plugins").setDescription(`Showing ${shown.length} of ${filtered.length} plugins. Use \`/plugin install\` to install.`);
2286
2655
  for (const p of shown) {
2287
2656
  const status = installedIds.has(p.pluginId) ? " \u2705" : "";
2288
2657
  const count = p.installCount ? ` | ${p.installCount.toLocaleString()} installs` : "";
@@ -2307,7 +2676,7 @@ async function handlePluginInstall(interaction) {
2307
2676
  await interaction.deferReply({ ephemeral: true });
2308
2677
  try {
2309
2678
  const result = await installPlugin(pluginId, scope, cwd);
2310
- const embed = new EmbedBuilder2().setColor(3066993).setTitle("Plugin Installed").setDescription(`**${pluginId}** installed with \`${scope}\` scope.`).addFields({ name: "Output", value: truncate(result, 1e3) || "Done." });
2679
+ const embed = new EmbedBuilder3().setColor(3066993).setTitle("Plugin Installed").setDescription(`**${pluginId}** installed with \`${scope}\` scope.`).addFields({ name: "Output", value: truncate(result, 1e3) || "Done." });
2311
2680
  await interaction.editReply({ embeds: [embed] });
2312
2681
  log(`Plugin "${pluginId}" installed (scope=${scope}) by ${interaction.user.tag}`);
2313
2682
  } catch (err) {
@@ -2339,7 +2708,7 @@ async function handlePluginList(interaction) {
2339
2708
  await interaction.editReply("No plugins installed.");
2340
2709
  return;
2341
2710
  }
2342
- const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Installed Plugins (${plugins.length})`);
2711
+ const embed = new EmbedBuilder3().setColor(3447003).setTitle(`Installed Plugins (${plugins.length})`);
2343
2712
  for (const p of plugins) {
2344
2713
  const icon = p.enabled ? "\u2705" : "\u274C";
2345
2714
  const scopeLabel = p.scope.charAt(0).toUpperCase() + p.scope.slice(1);
@@ -2368,7 +2737,7 @@ async function handlePluginInfo(interaction) {
2368
2737
  if (marketplaceName) {
2369
2738
  detail = await getPluginDetail(pluginName, marketplaceName);
2370
2739
  }
2371
- const embed = new EmbedBuilder2().setColor(15965202).setTitle(`Plugin: ${pluginName}`);
2740
+ const embed = new EmbedBuilder3().setColor(15965202).setTitle(`Plugin: ${pluginName}`);
2372
2741
  if (detail) {
2373
2742
  embed.setDescription(detail.description);
2374
2743
  if (detail.author) {
@@ -2492,7 +2861,7 @@ async function handleMarketplaceList(interaction) {
2492
2861
  await interaction.editReply("No marketplaces registered.");
2493
2862
  return;
2494
2863
  }
2495
- const embed = new EmbedBuilder2().setColor(10181046).setTitle(`Marketplaces (${marketplaces.length})`);
2864
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle(`Marketplaces (${marketplaces.length})`);
2496
2865
  for (const m of marketplaces) {
2497
2866
  const source = m.repo || m.url || m.source;
2498
2867
  embed.addFields({
@@ -2563,33 +2932,94 @@ var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
2563
2932
  "image/gif",
2564
2933
  "image/webp"
2565
2934
  ]);
2935
+ var TEXT_CONTENT_TYPES = /* @__PURE__ */ new Set([
2936
+ "text/plain",
2937
+ "text/markdown",
2938
+ "text/csv",
2939
+ "text/html",
2940
+ "text/xml",
2941
+ "application/json",
2942
+ "application/xml",
2943
+ "application/javascript",
2944
+ "application/typescript",
2945
+ "application/x-yaml"
2946
+ ]);
2947
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2948
+ ".txt",
2949
+ ".md",
2950
+ ".json",
2951
+ ".yaml",
2952
+ ".yml",
2953
+ ".xml",
2954
+ ".csv",
2955
+ ".html",
2956
+ ".css",
2957
+ ".js",
2958
+ ".ts",
2959
+ ".jsx",
2960
+ ".tsx",
2961
+ ".swift",
2962
+ ".py",
2963
+ ".rb",
2964
+ ".go",
2965
+ ".rs",
2966
+ ".java",
2967
+ ".kt",
2968
+ ".c",
2969
+ ".cpp",
2970
+ ".h",
2971
+ ".hpp",
2972
+ ".sh",
2973
+ ".bash",
2974
+ ".zsh",
2975
+ ".toml",
2976
+ ".ini",
2977
+ ".cfg",
2978
+ ".conf",
2979
+ ".env",
2980
+ ".log",
2981
+ ".sql",
2982
+ ".graphql",
2983
+ ".proto",
2984
+ ".diff",
2985
+ ".patch"
2986
+ ]);
2566
2987
  var MAX_IMAGE_SIZE = 20 * 1024 * 1024;
2567
- var MAX_BASE64_BYTES = 5 * 1024 * 1024;
2988
+ var MAX_TEXT_FILE_SIZE = 512 * 1024;
2989
+ var MAX_RAW_BYTES = Math.floor(5 * 1024 * 1024 * 3 / 4);
2568
2990
  var userLastMessage = /* @__PURE__ */ new Map();
2569
- async function resizeImageToFit(buf, mediaType) {
2570
- if (buf.length <= MAX_BASE64_BYTES) return buf;
2571
- const isJpeg = mediaType === "image/jpeg";
2572
- const format = isJpeg ? "jpeg" : "webp";
2573
- let img = sharp(buf);
2574
- const meta = await img.metadata();
2991
+ async function resizeImageToFit(buf) {
2992
+ const meta = await sharp(buf).metadata();
2575
2993
  const width = meta.width || 1;
2576
2994
  const height = meta.height || 1;
2577
2995
  let scale = 1;
2578
2996
  for (let i = 0; i < 5; i++) {
2579
2997
  scale *= 0.7;
2580
- const resized = await sharp(buf).resize(Math.round(width * scale), Math.round(height * scale), { fit: "inside" })[format]({ quality: 80 }).toBuffer();
2581
- if (resized.length <= MAX_BASE64_BYTES) return resized;
2998
+ const resized = await sharp(buf).resize(Math.round(width * scale), Math.round(height * scale), { fit: "inside" }).jpeg({ quality: 80 }).toBuffer();
2999
+ if (resized.length <= MAX_RAW_BYTES) return resized;
2582
3000
  }
2583
3001
  return sharp(buf).resize(Math.round(width * scale * 0.5), Math.round(height * scale * 0.5), { fit: "inside" }).jpeg({ quality: 60 }).toBuffer();
2584
3002
  }
3003
+ function isTextAttachment(contentType, filename) {
3004
+ if (contentType && TEXT_CONTENT_TYPES.has(contentType.split(";")[0])) return true;
3005
+ if (filename) {
3006
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
3007
+ if (TEXT_EXTENSIONS.has(ext)) return true;
3008
+ }
3009
+ return false;
3010
+ }
3011
+ async function fetchTextFile(url) {
3012
+ const res = await fetch(url);
3013
+ if (!res.ok) throw new Error(`Failed to download file: ${res.status}`);
3014
+ return res.text();
3015
+ }
2585
3016
  async function fetchImageAsBase64(url, mediaType) {
2586
3017
  const res = await fetch(url);
2587
3018
  if (!res.ok) throw new Error(`Failed to download image: ${res.status}`);
2588
- let buf = Buffer.from(await res.arrayBuffer());
2589
- if (buf.length > MAX_BASE64_BYTES) {
2590
- buf = await resizeImageToFit(buf, mediaType);
2591
- const newType = mediaType === "image/jpeg" ? "image/jpeg" : "image/webp";
2592
- return { data: buf.toString("base64"), mediaType: newType };
3019
+ const buf = Buffer.from(await res.arrayBuffer());
3020
+ if (buf.length > MAX_RAW_BYTES) {
3021
+ const resized = await resizeImageToFit(buf);
3022
+ return { data: resized.toString("base64"), mediaType: "image/jpeg" };
2593
3023
  }
2594
3024
  return { data: buf.toString("base64"), mediaType };
2595
3025
  }
@@ -2609,30 +3039,40 @@ async function handleMessage(message) {
2609
3039
  userLastMessage.set(message.author.id, now);
2610
3040
  if (session.isGenerating) {
2611
3041
  abortSession(session.id);
2612
- const deadline = Date.now() + 5e3;
2613
- while (session.isGenerating && Date.now() < deadline) {
2614
- await new Promise((r) => setTimeout(r, 100));
2615
- }
2616
- if (session.isGenerating) {
2617
- await message.reply({
2618
- content: "Could not interrupt the current generation. Try `/claude stop`.",
2619
- allowedMentions: { repliedUser: false }
2620
- });
2621
- return;
2622
- }
3042
+ await new Promise((r) => setTimeout(r, 200));
2623
3043
  }
2624
3044
  const text = message.content.trim();
2625
3045
  const imageAttachments = message.attachments.filter(
2626
3046
  (a) => a.contentType && SUPPORTED_IMAGE_TYPES.has(a.contentType) && a.size <= MAX_IMAGE_SIZE
2627
3047
  );
2628
- if (!text && imageAttachments.size === 0) return;
3048
+ const textAttachments = message.attachments.filter(
3049
+ (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
3050
+ );
3051
+ if (!text && imageAttachments.size === 0 && textAttachments.size === 0) return;
2629
3052
  try {
2630
3053
  const channel = message.channel;
3054
+ const hasAttachments = imageAttachments.size > 0 || textAttachments.size > 0;
2631
3055
  let prompt;
2632
- if (imageAttachments.size === 0) {
3056
+ if (!hasAttachments) {
2633
3057
  prompt = text;
2634
3058
  } else {
2635
3059
  const blocks = [];
3060
+ const textResults = await Promise.allSettled(
3061
+ textAttachments.map(async (a) => ({
3062
+ name: a.name ?? "file",
3063
+ content: await fetchTextFile(a.url)
3064
+ }))
3065
+ );
3066
+ for (const result of textResults) {
3067
+ if (result.status === "fulfilled") {
3068
+ blocks.push({
3069
+ type: "text",
3070
+ text: `<file name="${result.value.name}">
3071
+ ${result.value.content}
3072
+ </file>`
3073
+ });
3074
+ }
3075
+ }
2636
3076
  const imageResults = await Promise.allSettled(
2637
3077
  imageAttachments.map((a) => fetchImageAsBase64(a.url, a.contentType))
2638
3078
  );
@@ -2650,16 +3090,21 @@ async function handleMessage(message) {
2650
3090
  }
2651
3091
  if (text) {
2652
3092
  blocks.push({ type: "text", text });
2653
- } else if (blocks.length > 0) {
3093
+ } else if (imageAttachments.size > 0 && textAttachments.size === 0) {
2654
3094
  blocks.push({ type: "text", text: "What is in this image?" });
3095
+ } else {
3096
+ blocks.push({ type: "text", text: "Here are the attached files." });
2655
3097
  }
2656
3098
  prompt = blocks;
2657
3099
  }
2658
3100
  const stream = sendPrompt(session.id, prompt);
2659
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
3101
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
2660
3102
  } catch (err) {
3103
+ if (isAbortError(err)) {
3104
+ return;
3105
+ }
2661
3106
  await message.reply({
2662
- content: `Error: ${err.message}`,
3107
+ content: `Error: ${err.message || "Unknown error"}`,
2663
3108
  allowedMentions: { repliedUser: false }
2664
3109
  });
2665
3110
  }
@@ -2703,7 +3148,7 @@ async function handleButton(interaction) {
2703
3148
  const channel = interaction.channel;
2704
3149
  const stream = continueSession(sessionId);
2705
3150
  await interaction.editReply("Continuing...");
2706
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3151
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2707
3152
  } catch (err) {
2708
3153
  await interaction.editReply(`Error: ${err.message}`);
2709
3154
  }
@@ -2737,7 +3182,7 @@ ${display}
2737
3182
  const channel = interaction.channel;
2738
3183
  const stream = sendPrompt(sessionId, optionText);
2739
3184
  await interaction.editReply(`Selected option ${optionIndex + 1}`);
2740
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3185
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2741
3186
  } catch (err) {
2742
3187
  await interaction.editReply(`Error: ${err.message}`);
2743
3188
  }
@@ -2809,7 +3254,7 @@ ${display}
2809
3254
  const stream = sendPrompt(sessionId, combined);
2810
3255
  await interaction.editReply(`Submitted answers:
2811
3256
  ${combined}`);
2812
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3257
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2813
3258
  } catch (err) {
2814
3259
  await interaction.editReply(`Error: ${err.message}`);
2815
3260
  }
@@ -2830,7 +3275,7 @@ ${combined}`);
2830
3275
  const channel = interaction.channel;
2831
3276
  const stream = sendPrompt(sessionId, answer);
2832
3277
  await interaction.editReply(`Answered: **${truncate(answer, 100)}**`);
2833
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3278
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2834
3279
  } catch (err) {
2835
3280
  await interaction.editReply(`Error: ${err.message}`);
2836
3281
  }
@@ -2850,7 +3295,7 @@ ${combined}`);
2850
3295
  const channel = interaction.channel;
2851
3296
  const stream = sendPrompt(sessionId, answer);
2852
3297
  await interaction.editReply(`Answered: ${answer}`);
2853
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3298
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2854
3299
  } catch (err) {
2855
3300
  await interaction.editReply(`Error: ${err.message}`);
2856
3301
  }
@@ -2950,7 +3395,7 @@ async function handleSelectMenu(interaction) {
2950
3395
  const channel = interaction.channel;
2951
3396
  const stream = sendPrompt(sessionId, selected);
2952
3397
  await interaction.editReply(`Answered: **${truncate(selected, 100)}**`);
2953
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3398
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2954
3399
  } catch (err) {
2955
3400
  await interaction.editReply(`Error: ${err.message}`);
2956
3401
  }
@@ -2969,7 +3414,7 @@ async function handleSelectMenu(interaction) {
2969
3414
  const channel = interaction.channel;
2970
3415
  const stream = sendPrompt(sessionId, selected);
2971
3416
  await interaction.editReply(`Selected: ${truncate(selected, 100)}`);
2972
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3417
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2973
3418
  } catch (err) {
2974
3419
  await interaction.editReply(`Error: ${err.message}`);
2975
3420
  }
@@ -3046,8 +3491,8 @@ async function startBot() {
3046
3491
  try {
3047
3492
  if (interaction.type === InteractionType.ApplicationCommand && interaction.isChatInputCommand()) {
3048
3493
  switch (interaction.commandName) {
3049
- case "claude":
3050
- return await handleClaude(interaction);
3494
+ case "session":
3495
+ return await handleSession(interaction);
3051
3496
  case "shell":
3052
3497
  return await handleShell(interaction);
3053
3498
  case "agent":
@@ -3059,8 +3504,8 @@ async function startBot() {
3059
3504
  }
3060
3505
  }
3061
3506
  if (interaction.isAutocomplete()) {
3062
- if (interaction.commandName === "claude") {
3063
- return await handleClaudeAutocomplete(interaction);
3507
+ if (interaction.commandName === "session") {
3508
+ return await handleSessionAutocomplete(interaction);
3064
3509
  }
3065
3510
  if (interaction.commandName === "plugin") {
3066
3511
  return await handlePluginAutocomplete(interaction);