agentcord 0.1.8 → 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" }
@@ -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";
@@ -479,7 +744,7 @@ function detectYesNoPrompt(text) {
479
744
  }
480
745
 
481
746
  // src/session-manager.ts
482
- var SESSION_PREFIX = "claude-";
747
+ var SESSION_PREFIX = "agentcord-";
483
748
  var MODE_PROMPTS = {
484
749
  auto: "",
485
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.",
@@ -508,21 +773,27 @@ async function loadSessions() {
508
773
  const data = await sessionStore.read();
509
774
  if (!data) return;
510
775
  for (const s of data) {
511
- 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
+ }
512
788
  sessions.set(s.id, {
513
789
  ...s,
790
+ provider,
791
+ providerSessionId,
514
792
  verbose: s.verbose ?? false,
515
793
  mode: s.mode ?? "auto",
516
794
  isGenerating: false
517
795
  });
518
796
  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
797
  }
527
798
  console.log(`Restored ${sessions.size} session(s)`);
528
799
  }
@@ -534,8 +805,9 @@ async function saveSessions() {
534
805
  channelId: s.channelId,
535
806
  directory: s.directory,
536
807
  projectName: s.projectName,
808
+ provider: s.provider,
537
809
  tmuxName: s.tmuxName,
538
- claudeSessionId: s.claudeSessionId,
810
+ providerSessionId: s.providerSessionId,
539
811
  model: s.model,
540
812
  agentPersona: s.agentPersona,
541
813
  verbose: s.verbose || void 0,
@@ -548,7 +820,7 @@ async function saveSessions() {
548
820
  }
549
821
  await sessionStore.write(data);
550
822
  }
551
- async function createSession(name, directory, channelId, projectName, claudeSessionId) {
823
+ async function createSession(name, directory, channelId, projectName, provider = "claude", providerSessionId) {
552
824
  const resolvedDir = resolvePath(directory);
553
825
  if (!isPathAllowed(resolvedDir, config.allowedPaths)) {
554
826
  throw new Error(`Directory not in allowed paths: ${resolvedDir}`);
@@ -556,22 +828,27 @@ async function createSession(name, directory, channelId, projectName, claudeSess
556
828
  if (!existsSync3(resolvedDir)) {
557
829
  throw new Error(`Directory does not exist: ${resolvedDir}`);
558
830
  }
831
+ const providerInstance = await ensureProvider(provider);
832
+ const usesTmux = providerInstance.supports("tmux");
559
833
  let id = sanitizeSessionName(name);
560
- let tmuxName = `${SESSION_PREFIX}${id}`;
834
+ let tmuxName = usesTmux ? `${SESSION_PREFIX}${id}` : "";
561
835
  let suffix = 1;
562
- while (sessions.has(id) || await tmuxSessionExists(tmuxName)) {
836
+ while (sessions.has(id) || usesTmux && await tmuxSessionExists(tmuxName)) {
563
837
  suffix++;
564
838
  id = sanitizeSessionName(`${name}-${suffix}`);
565
- 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);
566
843
  }
567
- await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
568
844
  const session = {
569
845
  id,
570
846
  channelId,
571
847
  directory: resolvedDir,
572
848
  projectName,
849
+ provider,
573
850
  tmuxName,
574
- claudeSessionId,
851
+ providerSessionId,
575
852
  verbose: false,
576
853
  mode: "auto",
577
854
  isGenerating: false,
@@ -601,9 +878,11 @@ async function endSession(id) {
601
878
  if (session.isGenerating && session._controller) {
602
879
  session._controller.abort();
603
880
  }
604
- try {
605
- await tmux("kill-session", "-t", session.tmuxName);
606
- } catch {
881
+ if (session.tmuxName) {
882
+ try {
883
+ await tmux("kill-session", "-t", session.tmuxName);
884
+ } catch {
885
+ }
607
886
  }
608
887
  channelToSession.delete(session.channelId);
609
888
  sessions.delete(id);
@@ -657,7 +936,7 @@ function setAgentPersona(sessionId, persona) {
657
936
  saveSessions();
658
937
  }
659
938
  }
660
- function buildSystemPrompt(session) {
939
+ function buildSystemPromptParts(session) {
661
940
  const parts = [];
662
941
  const personality = getPersonality(session.projectName);
663
942
  if (personality) parts.push(personality);
@@ -667,10 +946,14 @@ function buildSystemPrompt(session) {
667
946
  }
668
947
  const modePrompt = MODE_PROMPTS[session.mode];
669
948
  if (modePrompt) parts.push(modePrompt);
670
- if (parts.length > 0) {
671
- 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();
672
956
  }
673
- return { type: "preset", preset: "claude_code" };
674
957
  }
675
958
  async function* sendPrompt(sessionId, prompt) {
676
959
  const session = sessions.get(sessionId);
@@ -680,47 +963,25 @@ async function* sendPrompt(sessionId, prompt) {
680
963
  session._controller = controller;
681
964
  session.isGenerating = true;
682
965
  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
- }
966
+ const provider = await ensureProvider(session.provider);
967
+ const systemPromptParts = buildSystemPromptParts(session);
698
968
  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
- }
969
+ const stream = provider.sendPrompt(prompt, {
970
+ directory: session.directory,
971
+ providerSessionId: session.providerSessionId,
972
+ model: session.model,
973
+ systemPromptParts,
974
+ abortController: controller
712
975
  });
713
- for await (const message of stream) {
714
- if (message.type === "system" && "subtype" in message && message.subtype === "init") {
715
- 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;
716
979
  await saveSessions();
717
980
  }
718
- if (message.type === "result") {
719
- if ("total_cost_usd" in message) {
720
- session.totalCost += message.total_cost_usd;
721
- }
981
+ if (event.type === "result") {
982
+ session.totalCost += event.costUsd;
722
983
  }
723
- yield message;
984
+ yield event;
724
985
  }
725
986
  session.messageCount++;
726
987
  } catch (err) {
@@ -743,32 +1004,25 @@ async function* continueSession(sessionId) {
743
1004
  session._controller = controller;
744
1005
  session.isGenerating = true;
745
1006
  session.lastActivity = Date.now();
746
- const systemPrompt = buildSystemPrompt(session);
1007
+ const provider = await ensureProvider(session.provider);
1008
+ const systemPromptParts = buildSystemPromptParts(session);
747
1009
  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
- }
1010
+ const stream = provider.continueSession({
1011
+ directory: session.directory,
1012
+ providerSessionId: session.providerSessionId,
1013
+ model: session.model,
1014
+ systemPromptParts,
1015
+ abortController: controller
762
1016
  });
763
- for await (const message of stream) {
764
- if (message.type === "system" && "subtype" in message && message.subtype === "init") {
765
- 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;
766
1020
  await saveSessions();
767
1021
  }
768
- if (message.type === "result" && "total_cost_usd" in message) {
769
- session.totalCost += message.total_cost_usd;
1022
+ if (event.type === "result") {
1023
+ session.totalCost += event.costUsd;
770
1024
  }
771
- yield message;
1025
+ yield event;
772
1026
  }
773
1027
  session.messageCount++;
774
1028
  } catch (err) {
@@ -789,16 +1043,21 @@ function abortSession(sessionId) {
789
1043
  const controller = session._controller;
790
1044
  if (controller) {
791
1045
  controller.abort();
1046
+ }
1047
+ if (session.isGenerating) {
1048
+ session.isGenerating = false;
1049
+ delete session._controller;
1050
+ saveSessions();
792
1051
  return true;
793
1052
  }
794
- return false;
1053
+ return !!controller;
795
1054
  }
796
1055
  function getAttachInfo(sessionId) {
797
1056
  const session = sessions.get(sessionId);
798
- if (!session) return null;
1057
+ if (!session || !session.tmuxName) return null;
799
1058
  return {
800
1059
  command: `tmux attach -t ${session.tmuxName}`,
801
- sessionId: session.claudeSessionId
1060
+ sessionId: session.providerSessionId
802
1061
  };
803
1062
  }
804
1063
  async function listTmuxSessions() {
@@ -946,13 +1205,64 @@ async function getPluginDetail(pluginName, marketplaceName) {
946
1205
  // src/output-handler.ts
947
1206
  import {
948
1207
  AttachmentBuilder,
949
- EmbedBuilder,
1208
+ EmbedBuilder as EmbedBuilder2,
950
1209
  ActionRowBuilder,
951
1210
  ButtonBuilder,
952
1211
  ButtonStyle,
953
1212
  StringSelectMenuBuilder
954
1213
  } from "discord.js";
955
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
+ }
956
1266
  var expandableStore = /* @__PURE__ */ new Map();
957
1267
  var expandCounter = 0;
958
1268
  var pendingAnswersStore = /* @__PURE__ */ new Map();
@@ -993,11 +1303,6 @@ function makeStopButton(sessionId) {
993
1303
  new ButtonBuilder().setCustomId(`stop:${sessionId}`).setLabel("Stop").setStyle(ButtonStyle.Danger)
994
1304
  );
995
1305
  }
996
- function makeCompletionButtons(sessionId) {
997
- return new ActionRowBuilder().addComponents(
998
- new ButtonBuilder().setCustomId(`continue:${sessionId}`).setLabel("Continue").setStyle(ButtonStyle.Primary)
999
- );
1000
- }
1001
1306
  function makeOptionButtons(sessionId, options) {
1002
1307
  const rows = [];
1003
1308
  const maxOptions = Math.min(options.length, 10);
@@ -1033,6 +1338,10 @@ function makeYesNoButtons(sessionId) {
1033
1338
  new ButtonBuilder().setCustomId(`confirm:${sessionId}:no`).setLabel("No").setStyle(ButtonStyle.Danger)
1034
1339
  );
1035
1340
  }
1341
+ function shouldSuppressCommandExecution(command) {
1342
+ const normalized = command.toLowerCase();
1343
+ return normalized.includes("total-recall");
1344
+ }
1036
1345
  var MessageStreamer = class {
1037
1346
  channel;
1038
1347
  sessionId;
@@ -1142,6 +1451,25 @@ var MessageStreamer = class {
1142
1451
  this.currentMessage = null;
1143
1452
  this.currentText = "";
1144
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
+ }
1145
1473
  getText() {
1146
1474
  return this.currentText;
1147
1475
  }
@@ -1152,31 +1480,6 @@ var MessageStreamer = class {
1152
1480
  }
1153
1481
  }
1154
1482
  };
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
1483
  var STATUS_EMOJI = {
1181
1484
  pending: "\u2B1C",
1182
1485
  // white square
@@ -1187,9 +1490,39 @@ var STATUS_EMOJI = {
1187
1490
  deleted: "\u{1F5D1}\uFE0F"
1188
1491
  // wastebasket
1189
1492
  };
1190
- function renderAskUserQuestion(toolInput, sessionId) {
1493
+ function renderTaskToolEmbed(action, dataJson) {
1191
1494
  try {
1192
- 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);
1193
1526
  const questions = data.questions;
1194
1527
  if (!questions?.length) return null;
1195
1528
  const isMulti = questions.length > 1;
@@ -1203,7 +1536,7 @@ function renderAskUserQuestion(toolInput, sessionId) {
1203
1536
  const selectPrefix = isMulti ? "pick-select" : "answer-select";
1204
1537
  for (let qi = 0; qi < questions.length; qi++) {
1205
1538
  const q = questions[qi];
1206
- const embed = new EmbedBuilder().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
1539
+ const embed = new EmbedBuilder2().setColor(15965202).setTitle(q.header || "Question").setDescription(q.question);
1207
1540
  if (q.options?.length) {
1208
1541
  if (q.options.length <= 4) {
1209
1542
  const row = new ActionRowBuilder();
@@ -1241,42 +1574,9 @@ function renderAskUserQuestion(toolInput, sessionId) {
1241
1574
  return null;
1242
1575
  }
1243
1576
  }
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") {
1577
+ async function handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto", _provider = "claude") {
1275
1578
  const streamer = new MessageStreamer(channel, sessionId);
1276
- let currentToolName = null;
1277
- let currentToolInput = "";
1278
- let lastFinishedToolName = null;
1279
- let pendingImagePath = null;
1579
+ let lastToolName = null;
1280
1580
  channel.sendTyping().catch(() => {
1281
1581
  });
1282
1582
  const typingInterval = setInterval(() => {
@@ -1284,115 +1584,78 @@ async function handleOutputStream(stream, channel, sessionId, verbose = false, m
1284
1584
  });
1285
1585
  }, 8e3);
1286
1586
  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
- }
1587
+ for await (const event of stream) {
1588
+ switch (event.type) {
1589
+ case "text_delta": {
1590
+ streamer.append(event.text);
1591
+ break;
1296
1592
  }
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;
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 });
1303
1599
  }
1600
+ break;
1304
1601
  }
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
- }
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
+ });
1340
1612
  }
1341
- pendingImagePath = extractImagePath(currentToolName, currentToolInput);
1342
- lastFinishedToolName = currentToolName;
1343
- currentToolName = null;
1344
- currentToolInput = "";
1345
1613
  }
1614
+ lastToolName = event.action;
1615
+ break;
1346
1616
  }
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
- }
1356
- pendingImagePath = null;
1357
- } else {
1358
- pendingImagePath = null;
1359
- }
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
- }
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
+ );
1375
1632
  }
1633
+ await channel.send({ embeds: [embed], components });
1376
1634
  }
1635
+ lastToolName = event.toolName;
1636
+ break;
1377
1637
  }
1378
- if (resultText) {
1379
- 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();
1380
1643
  if (isTaskResult && !verbose) {
1381
- const boardEmbed = renderTaskListEmbed(resultText);
1644
+ const boardEmbed = renderTaskListEmbed(event.result);
1382
1645
  if (boardEmbed) {
1383
1646
  await channel.send({
1384
1647
  embeds: [boardEmbed],
1385
1648
  components: [makeStopButton(sessionId)]
1386
1649
  });
1387
1650
  }
1388
- } else {
1389
- const displayResult = resultText.length > 1e3 ? truncate(resultText, 1e3) : resultText;
1390
- 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(`\`\`\`
1391
1654
  ${displayResult}
1392
1655
  \`\`\``);
1393
1656
  const components = [makeStopButton(sessionId)];
1394
- if (resultText.length > 1e3) {
1395
- const contentId = storeExpandable(resultText);
1657
+ if (event.result.length > 1e3) {
1658
+ const contentId = storeExpandable(event.result);
1396
1659
  components.unshift(
1397
1660
  new ActionRowBuilder().addComponents(
1398
1661
  new ButtonBuilder().setCustomId(`expand:${contentId}`).setLabel("Show Full Output").setStyle(ButtonStyle.Secondary)
@@ -1401,47 +1664,111 @@ ${displayResult}
1401
1664
  }
1402
1665
  await channel.send({ embeds: [embed], components });
1403
1666
  }
1667
+ break;
1404
1668
  }
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}` });
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;
1422
1707
  }
1423
- if (!isSuccess && result.errors?.length) {
1424
- embed.setDescription(result.errors.join("\n"));
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;
1425
1716
  }
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));
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;
1749
+ }
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;
1757
+ }
1758
+ case "session_init": {
1759
+ break;
1433
1760
  }
1434
- components.push(makeModeButtons(sessionId, mode));
1435
- components.push(makeCompletionButtons(sessionId));
1436
- await channel.send({ embeds: [embed], components });
1437
1761
  }
1438
1762
  }
1439
1763
  } catch (err) {
1440
1764
  await streamer.finalize();
1441
- if (err.name !== "AbortError") {
1442
- const embed = new EmbedBuilder().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1443
- ${err.message}
1444
- \`\`\``);
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.`);
1445
1772
  await channel.send({ embeds: [embed] });
1446
1773
  }
1447
1774
  } finally {
@@ -1573,7 +1900,7 @@ async function ensureProjectCategory(guild, projectName, directory) {
1573
1900
  updateProjectCategory(projectName, category.id, logChannel2.id);
1574
1901
  return { category, logChannel: logChannel2 };
1575
1902
  }
1576
- async function handleClaude(interaction) {
1903
+ async function handleSession(interaction) {
1577
1904
  if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
1578
1905
  await interaction.reply({ content: "You are not authorized to use this bot.", ephemeral: true });
1579
1906
  return;
@@ -1581,35 +1908,46 @@ async function handleClaude(interaction) {
1581
1908
  const sub = interaction.options.getSubcommand();
1582
1909
  switch (sub) {
1583
1910
  case "new":
1584
- return handleClaudeNew(interaction);
1911
+ return handleSessionNew(interaction);
1585
1912
  case "resume":
1586
- return handleClaudeResume(interaction);
1913
+ return handleSessionResume(interaction);
1587
1914
  case "list":
1588
- return handleClaudeList(interaction);
1915
+ return handleSessionList(interaction);
1589
1916
  case "end":
1590
- return handleClaudeEnd(interaction);
1917
+ return handleSessionEnd(interaction);
1591
1918
  case "continue":
1592
- return handleClaudeContinue(interaction);
1919
+ return handleSessionContinue(interaction);
1593
1920
  case "stop":
1594
- return handleClaudeStop(interaction);
1921
+ return handleSessionStop(interaction);
1595
1922
  case "output":
1596
- return handleClaudeOutput(interaction);
1923
+ return handleSessionOutput(interaction);
1597
1924
  case "attach":
1598
- return handleClaudeAttach(interaction);
1925
+ return handleSessionAttach(interaction);
1599
1926
  case "sync":
1600
- return handleClaudeSync(interaction);
1927
+ return handleSessionSync(interaction);
1928
+ case "id":
1929
+ return handleSessionId(interaction);
1601
1930
  case "model":
1602
- return handleClaudeModel(interaction);
1931
+ return handleSessionModel(interaction);
1603
1932
  case "verbose":
1604
- return handleClaudeVerbose(interaction);
1933
+ return handleSessionVerbose(interaction);
1605
1934
  case "mode":
1606
- return handleClaudeMode(interaction);
1935
+ return handleSessionMode(interaction);
1607
1936
  default:
1608
1937
  await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
1609
1938
  }
1610
1939
  }
1611
- 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) {
1612
1949
  const name = interaction.options.getString("name", true);
1950
+ const provider = interaction.options.getString("provider") || "claude";
1613
1951
  let directory = interaction.options.getString("directory");
1614
1952
  if (!directory) {
1615
1953
  const parentId = interaction.channel?.parentId;
@@ -1625,30 +1963,35 @@ async function handleClaudeNew(interaction) {
1625
1963
  const guild = interaction.guild;
1626
1964
  const projectName = projectNameFromDir(directory);
1627
1965
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1628
- const session = await createSession(name, directory, "pending", projectName);
1966
+ const session = await createSession(name, directory, "pending", projectName, provider);
1629
1967
  channel = await guild.channels.create({
1630
- name: `claude-${session.id}`,
1968
+ name: `${provider}-${session.id}`,
1631
1969
  type: ChannelType.GuildText,
1632
1970
  parent: category.id,
1633
- topic: `Claude session | Dir: ${directory}`
1971
+ topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
1634
1972
  });
1635
1973
  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 },
1974
+ const fields = [
1975
+ { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
1976
+ { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
1638
1977
  { 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
- );
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);
1642
1984
  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
- });
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] });
1652
1995
  } catch (err) {
1653
1996
  if (channel) {
1654
1997
  try {
@@ -1748,7 +2091,7 @@ function formatTimeAgo(mtime) {
1748
2091
  if (ago < 864e5) return `${Math.floor(ago / 36e5)}h ago`;
1749
2092
  return `${Math.floor(ago / 864e5)}d ago`;
1750
2093
  }
1751
- async function handleClaudeAutocomplete(interaction) {
2094
+ async function handleSessionAutocomplete(interaction) {
1752
2095
  const focused = interaction.options.getFocused();
1753
2096
  const localSessions = discoverLocalSessions();
1754
2097
  const filtered = focused ? localSessions.filter((s) => s.id.includes(focused.toLowerCase()) || s.project.toLowerCase().includes(focused.toLowerCase())) : localSessions;
@@ -1763,17 +2106,20 @@ async function handleClaudeAutocomplete(interaction) {
1763
2106
  );
1764
2107
  await interaction.respond(choices);
1765
2108
  }
1766
- async function handleClaudeResume(interaction) {
1767
- const claudeSessionId = interaction.options.getString("session-id", true);
2109
+ async function handleSessionResume(interaction) {
2110
+ const providerSessionId = interaction.options.getString("session-id", true);
1768
2111
  const name = interaction.options.getString("name", true);
2112
+ const provider = interaction.options.getString("provider") || "claude";
1769
2113
  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;
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
+ }
1777
2123
  }
1778
2124
  await interaction.deferReply();
1779
2125
  let channel;
@@ -1781,32 +2127,39 @@ async function handleClaudeResume(interaction) {
1781
2127
  const guild = interaction.guild;
1782
2128
  const projectName = projectNameFromDir(directory);
1783
2129
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1784
- const session = await createSession(name, directory, "pending", projectName, claudeSessionId);
2130
+ const session = await createSession(name, directory, "pending", projectName, provider, providerSessionId);
1785
2131
  channel = await guild.channels.create({
1786
- name: `claude-${session.id}`,
2132
+ name: `${provider}-${session.id}`,
1787
2133
  type: ChannelType.GuildText,
1788
2134
  parent: category.id,
1789
- topic: `Claude session (resumed) | Dir: ${directory}`
2135
+ topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
1790
2136
  });
1791
2137
  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 },
2138
+ const fields = [
2139
+ { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
2140
+ { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
1794
2141
  { name: "Directory", value: session.directory, inline: true },
1795
2142
  { 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
- );
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);
1799
2149
  await interaction.editReply({ embeds: [embed] });
1800
- 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
+ }
1801
2158
  await channel.send({
1802
2159
  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
- )
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)
1810
2163
  ]
1811
2164
  });
1812
2165
  } catch (err) {
@@ -1819,7 +2172,7 @@ async function handleClaudeResume(interaction) {
1819
2172
  await interaction.editReply(`Failed to resume session: ${err.message}`);
1820
2173
  }
1821
2174
  }
1822
- async function handleClaudeList(interaction) {
2175
+ async function handleSessionList(interaction) {
1823
2176
  const allSessions = getAllSessions();
1824
2177
  if (allSessions.length === 0) {
1825
2178
  await interaction.reply({ content: "No active sessions.", ephemeral: true });
@@ -1831,33 +2184,42 @@ async function handleClaudeList(interaction) {
1831
2184
  arr.push(s);
1832
2185
  grouped.set(s.projectName, arr);
1833
2186
  }
1834
- const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Active Sessions (${allSessions.length})`);
2187
+ const embed = new EmbedBuilder3().setColor(3447003).setTitle(`Active Sessions (${allSessions.length})`);
1835
2188
  for (const [project, projectSessions] of grouped) {
1836
2189
  const lines = projectSessions.map((s) => {
1837
2190
  const status = s.isGenerating ? "\u{1F7E2} generating" : "\u26AA idle";
1838
2191
  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)}`;
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)}`;
1840
2194
  });
1841
2195
  embed.addFields({ name: `\u{1F4C1} ${project}`, value: lines.join("\n") });
1842
2196
  }
1843
2197
  await interaction.reply({ embeds: [embed] });
1844
2198
  }
1845
- async function handleClaudeEnd(interaction) {
2199
+ async function handleSessionEnd(interaction) {
1846
2200
  const session = getSessionByChannel(interaction.channelId);
1847
2201
  if (!session) {
1848
2202
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
1849
2203
  return;
1850
2204
  }
2205
+ const channel = interaction.channel;
1851
2206
  await interaction.deferReply();
1852
2207
  try {
1853
2208
  await endSession(session.id);
1854
- await interaction.editReply(`Session "${session.id}" ended. You can delete this channel.`);
1855
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);
1856
2218
  } catch (err) {
1857
2219
  await interaction.editReply(`Failed to end session: ${err.message}`);
1858
2220
  }
1859
2221
  }
1860
- async function handleClaudeContinue(interaction) {
2222
+ async function handleSessionContinue(interaction) {
1861
2223
  const session = getSessionByChannel(interaction.channelId);
1862
2224
  if (!session) {
1863
2225
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1872,12 +2234,12 @@ async function handleClaudeContinue(interaction) {
1872
2234
  const channel = interaction.channel;
1873
2235
  const stream = continueSession(session.id);
1874
2236
  await interaction.editReply("Continuing...");
1875
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
2237
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
1876
2238
  } catch (err) {
1877
2239
  await interaction.editReply(`Error: ${err.message}`);
1878
2240
  }
1879
2241
  }
1880
- async function handleClaudeStop(interaction) {
2242
+ async function handleSessionStop(interaction) {
1881
2243
  const session = getSessionByChannel(interaction.channelId);
1882
2244
  if (!session) {
1883
2245
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1889,18 +2251,18 @@ async function handleClaudeStop(interaction) {
1889
2251
  ephemeral: true
1890
2252
  });
1891
2253
  }
1892
- async function handleClaudeOutput(interaction) {
2254
+ async function handleSessionOutput(interaction) {
1893
2255
  const session = getSessionByChannel(interaction.channelId);
1894
2256
  if (!session) {
1895
2257
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
1896
2258
  return;
1897
2259
  }
1898
2260
  await interaction.reply({
1899
- 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.",
1900
2262
  ephemeral: true
1901
2263
  });
1902
2264
  }
1903
- async function handleClaudeAttach(interaction) {
2265
+ async function handleSessionAttach(interaction) {
1904
2266
  const session = getSessionByChannel(interaction.channelId);
1905
2267
  if (!session) {
1906
2268
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1908,10 +2270,13 @@ async function handleClaudeAttach(interaction) {
1908
2270
  }
1909
2271
  const info = getAttachInfo(session.id);
1910
2272
  if (!info) {
1911
- 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
+ });
1912
2277
  return;
1913
2278
  }
1914
- const embed = new EmbedBuilder2().setColor(10181046).setTitle("Terminal Access").addFields(
2279
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle("Terminal Access").addFields(
1915
2280
  { name: "Attach to tmux", value: `\`\`\`
1916
2281
  ${info.command}
1917
2282
  \`\`\`` }
@@ -1926,7 +2291,7 @@ cd ${session.directory} && claude --resume ${info.sessionId}
1926
2291
  }
1927
2292
  await interaction.reply({ embeds: [embed], ephemeral: true });
1928
2293
  }
1929
- async function handleClaudeSync(interaction) {
2294
+ async function handleSessionSync(interaction) {
1930
2295
  await interaction.deferReply();
1931
2296
  const guild = interaction.guild;
1932
2297
  const tmuxSessions = await listTmuxSessions();
@@ -1941,16 +2306,36 @@ async function handleClaudeSync(interaction) {
1941
2306
  name: `claude-${tmuxSession.id}`,
1942
2307
  type: ChannelType.GuildText,
1943
2308
  parent: category.id,
1944
- topic: `Claude session (synced) | Dir: ${tmuxSession.directory}`
2309
+ topic: `Claude Code session (synced) | Dir: ${tmuxSession.directory}`
1945
2310
  });
1946
- await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName);
2311
+ await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName, "claude");
1947
2312
  synced++;
1948
2313
  }
1949
2314
  await interaction.editReply(
1950
2315
  synced > 0 ? `Synced ${synced} orphaned session(s).` : "No orphaned sessions found."
1951
2316
  );
1952
2317
  }
1953
- 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) {
1954
2339
  const session = getSessionByChannel(interaction.channelId);
1955
2340
  if (!session) {
1956
2341
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1960,7 +2345,7 @@ async function handleClaudeModel(interaction) {
1960
2345
  setModel(session.id, model);
1961
2346
  await interaction.reply({ content: `Model set to \`${model}\` for this session.`, ephemeral: true });
1962
2347
  }
1963
- async function handleClaudeVerbose(interaction) {
2348
+ async function handleSessionVerbose(interaction) {
1964
2349
  const session = getSessionByChannel(interaction.channelId);
1965
2350
  if (!session) {
1966
2351
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -1978,7 +2363,7 @@ var MODE_LABELS = {
1978
2363
  plan: "\u{1F4CB} Plan \u2014 always plans before executing changes",
1979
2364
  normal: "\u{1F6E1}\uFE0F Normal \u2014 asks before destructive operations"
1980
2365
  };
1981
- async function handleClaudeMode(interaction) {
2366
+ async function handleSessionMode(interaction) {
1982
2367
  const session = getSessionByChannel(interaction.channelId);
1983
2368
  if (!session) {
1984
2369
  await interaction.reply({ content: "No session in this channel.", ephemeral: true });
@@ -2061,7 +2446,7 @@ async function handleAgent(interaction) {
2061
2446
  }
2062
2447
  case "list": {
2063
2448
  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"));
2449
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle("Agent Personas").setDescription(agents2.map((a) => `${a.emoji} **${a.name}** \u2014 ${a.description}`).join("\n"));
2065
2450
  await interaction.reply({ embeds: [embed], ephemeral: true });
2066
2451
  break;
2067
2452
  }
@@ -2154,7 +2539,7 @@ ${list}`, ephemeral: true });
2154
2539
  const channel = interaction.channel;
2155
2540
  await interaction.editReply(`Running skill **${name}**...`);
2156
2541
  const stream = sendPrompt(session.id, expanded);
2157
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
2542
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
2158
2543
  } catch (err) {
2159
2544
  await interaction.editReply(`Error: ${err.message}`);
2160
2545
  }
@@ -2199,7 +2584,7 @@ ${list}`, ephemeral: true });
2199
2584
  await interaction.reply({ content: "Project not found.", ephemeral: true });
2200
2585
  return;
2201
2586
  }
2202
- const embed = new EmbedBuilder2().setColor(15965202).setTitle(`Project: ${projectName}`).addFields(
2587
+ const embed = new EmbedBuilder3().setColor(15965202).setTitle(`Project: ${projectName}`).addFields(
2203
2588
  { name: "Directory", value: `\`${project.directory}\``, inline: false },
2204
2589
  {
2205
2590
  name: "Personality",
@@ -2282,7 +2667,7 @@ async function handlePluginBrowse(interaction) {
2282
2667
  return;
2283
2668
  }
2284
2669
  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.`);
2670
+ const embed = new EmbedBuilder3().setColor(8141549).setTitle("Available Plugins").setDescription(`Showing ${shown.length} of ${filtered.length} plugins. Use \`/plugin install\` to install.`);
2286
2671
  for (const p of shown) {
2287
2672
  const status = installedIds.has(p.pluginId) ? " \u2705" : "";
2288
2673
  const count = p.installCount ? ` | ${p.installCount.toLocaleString()} installs` : "";
@@ -2307,7 +2692,7 @@ async function handlePluginInstall(interaction) {
2307
2692
  await interaction.deferReply({ ephemeral: true });
2308
2693
  try {
2309
2694
  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." });
2695
+ const embed = new EmbedBuilder3().setColor(3066993).setTitle("Plugin Installed").setDescription(`**${pluginId}** installed with \`${scope}\` scope.`).addFields({ name: "Output", value: truncate(result, 1e3) || "Done." });
2311
2696
  await interaction.editReply({ embeds: [embed] });
2312
2697
  log(`Plugin "${pluginId}" installed (scope=${scope}) by ${interaction.user.tag}`);
2313
2698
  } catch (err) {
@@ -2339,7 +2724,7 @@ async function handlePluginList(interaction) {
2339
2724
  await interaction.editReply("No plugins installed.");
2340
2725
  return;
2341
2726
  }
2342
- const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Installed Plugins (${plugins.length})`);
2727
+ const embed = new EmbedBuilder3().setColor(3447003).setTitle(`Installed Plugins (${plugins.length})`);
2343
2728
  for (const p of plugins) {
2344
2729
  const icon = p.enabled ? "\u2705" : "\u274C";
2345
2730
  const scopeLabel = p.scope.charAt(0).toUpperCase() + p.scope.slice(1);
@@ -2368,7 +2753,7 @@ async function handlePluginInfo(interaction) {
2368
2753
  if (marketplaceName) {
2369
2754
  detail = await getPluginDetail(pluginName, marketplaceName);
2370
2755
  }
2371
- const embed = new EmbedBuilder2().setColor(15965202).setTitle(`Plugin: ${pluginName}`);
2756
+ const embed = new EmbedBuilder3().setColor(15965202).setTitle(`Plugin: ${pluginName}`);
2372
2757
  if (detail) {
2373
2758
  embed.setDescription(detail.description);
2374
2759
  if (detail.author) {
@@ -2492,7 +2877,7 @@ async function handleMarketplaceList(interaction) {
2492
2877
  await interaction.editReply("No marketplaces registered.");
2493
2878
  return;
2494
2879
  }
2495
- const embed = new EmbedBuilder2().setColor(10181046).setTitle(`Marketplaces (${marketplaces.length})`);
2880
+ const embed = new EmbedBuilder3().setColor(10181046).setTitle(`Marketplaces (${marketplaces.length})`);
2496
2881
  for (const m of marketplaces) {
2497
2882
  const source = m.repo || m.url || m.source;
2498
2883
  embed.addFields({
@@ -2563,33 +2948,94 @@ var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
2563
2948
  "image/gif",
2564
2949
  "image/webp"
2565
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
+ ]);
2566
3003
  var MAX_IMAGE_SIZE = 20 * 1024 * 1024;
2567
- var MAX_BASE64_BYTES = 5 * 1024 * 1024;
3004
+ var MAX_TEXT_FILE_SIZE = 512 * 1024;
3005
+ var MAX_RAW_BYTES = Math.floor(5 * 1024 * 1024 * 3 / 4);
2568
3006
  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();
3007
+ async function resizeImageToFit(buf) {
3008
+ const meta = await sharp(buf).metadata();
2575
3009
  const width = meta.width || 1;
2576
3010
  const height = meta.height || 1;
2577
3011
  let scale = 1;
2578
3012
  for (let i = 0; i < 5; i++) {
2579
3013
  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;
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;
2582
3016
  }
2583
3017
  return sharp(buf).resize(Math.round(width * scale * 0.5), Math.round(height * scale * 0.5), { fit: "inside" }).jpeg({ quality: 60 }).toBuffer();
2584
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
+ }
2585
3032
  async function fetchImageAsBase64(url, mediaType) {
2586
3033
  const res = await fetch(url);
2587
3034
  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 };
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" };
2593
3039
  }
2594
3040
  return { data: buf.toString("base64"), mediaType };
2595
3041
  }
@@ -2615,7 +3061,7 @@ async function handleMessage(message) {
2615
3061
  }
2616
3062
  if (session.isGenerating) {
2617
3063
  await message.reply({
2618
- content: "Could not interrupt the current generation. Try `/claude stop`.",
3064
+ content: "Could not interrupt the current generation. Try `/session stop`.",
2619
3065
  allowedMentions: { repliedUser: false }
2620
3066
  });
2621
3067
  return;
@@ -2625,14 +3071,34 @@ async function handleMessage(message) {
2625
3071
  const imageAttachments = message.attachments.filter(
2626
3072
  (a) => a.contentType && SUPPORTED_IMAGE_TYPES.has(a.contentType) && a.size <= MAX_IMAGE_SIZE
2627
3073
  );
2628
- if (!text && imageAttachments.size === 0) return;
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;
2629
3078
  try {
2630
3079
  const channel = message.channel;
3080
+ const hasAttachments = imageAttachments.size > 0 || textAttachments.size > 0;
2631
3081
  let prompt;
2632
- if (imageAttachments.size === 0) {
3082
+ if (!hasAttachments) {
2633
3083
  prompt = text;
2634
3084
  } else {
2635
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
+ }
2636
3102
  const imageResults = await Promise.allSettled(
2637
3103
  imageAttachments.map((a) => fetchImageAsBase64(a.url, a.contentType))
2638
3104
  );
@@ -2650,16 +3116,25 @@ async function handleMessage(message) {
2650
3116
  }
2651
3117
  if (text) {
2652
3118
  blocks.push({ type: "text", text });
2653
- } else if (blocks.length > 0) {
3119
+ } else if (imageAttachments.size > 0 && textAttachments.size === 0) {
2654
3120
  blocks.push({ type: "text", text: "What is in this image?" });
3121
+ } else {
3122
+ blocks.push({ type: "text", text: "Here are the attached files." });
2655
3123
  }
2656
3124
  prompt = blocks;
2657
3125
  }
2658
3126
  const stream = sendPrompt(session.id, prompt);
2659
- await handleOutputStream(stream, channel, session.id, session.verbose, session.mode);
3127
+ await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
2660
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);
2661
3135
  await message.reply({
2662
- content: `Error: ${err.message}`,
3136
+ content: `Error: ${errMsg}
3137
+ -# Session reset \u2014 next message will start a fresh provider session.`,
2663
3138
  allowedMentions: { repliedUser: false }
2664
3139
  });
2665
3140
  }
@@ -2703,7 +3178,7 @@ async function handleButton(interaction) {
2703
3178
  const channel = interaction.channel;
2704
3179
  const stream = continueSession(sessionId);
2705
3180
  await interaction.editReply("Continuing...");
2706
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3181
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2707
3182
  } catch (err) {
2708
3183
  await interaction.editReply(`Error: ${err.message}`);
2709
3184
  }
@@ -2737,7 +3212,7 @@ ${display}
2737
3212
  const channel = interaction.channel;
2738
3213
  const stream = sendPrompt(sessionId, optionText);
2739
3214
  await interaction.editReply(`Selected option ${optionIndex + 1}`);
2740
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3215
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2741
3216
  } catch (err) {
2742
3217
  await interaction.editReply(`Error: ${err.message}`);
2743
3218
  }
@@ -2809,7 +3284,7 @@ ${display}
2809
3284
  const stream = sendPrompt(sessionId, combined);
2810
3285
  await interaction.editReply(`Submitted answers:
2811
3286
  ${combined}`);
2812
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3287
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2813
3288
  } catch (err) {
2814
3289
  await interaction.editReply(`Error: ${err.message}`);
2815
3290
  }
@@ -2830,7 +3305,7 @@ ${combined}`);
2830
3305
  const channel = interaction.channel;
2831
3306
  const stream = sendPrompt(sessionId, answer);
2832
3307
  await interaction.editReply(`Answered: **${truncate(answer, 100)}**`);
2833
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3308
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2834
3309
  } catch (err) {
2835
3310
  await interaction.editReply(`Error: ${err.message}`);
2836
3311
  }
@@ -2850,7 +3325,7 @@ ${combined}`);
2850
3325
  const channel = interaction.channel;
2851
3326
  const stream = sendPrompt(sessionId, answer);
2852
3327
  await interaction.editReply(`Answered: ${answer}`);
2853
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3328
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2854
3329
  } catch (err) {
2855
3330
  await interaction.editReply(`Error: ${err.message}`);
2856
3331
  }
@@ -2950,7 +3425,7 @@ async function handleSelectMenu(interaction) {
2950
3425
  const channel = interaction.channel;
2951
3426
  const stream = sendPrompt(sessionId, selected);
2952
3427
  await interaction.editReply(`Answered: **${truncate(selected, 100)}**`);
2953
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3428
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2954
3429
  } catch (err) {
2955
3430
  await interaction.editReply(`Error: ${err.message}`);
2956
3431
  }
@@ -2969,7 +3444,7 @@ async function handleSelectMenu(interaction) {
2969
3444
  const channel = interaction.channel;
2970
3445
  const stream = sendPrompt(sessionId, selected);
2971
3446
  await interaction.editReply(`Selected: ${truncate(selected, 100)}`);
2972
- await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode);
3447
+ await handleOutputStream(stream, channel, sessionId, session.verbose, session.mode, session.provider);
2973
3448
  } catch (err) {
2974
3449
  await interaction.editReply(`Error: ${err.message}`);
2975
3450
  }
@@ -3046,8 +3521,8 @@ async function startBot() {
3046
3521
  try {
3047
3522
  if (interaction.type === InteractionType.ApplicationCommand && interaction.isChatInputCommand()) {
3048
3523
  switch (interaction.commandName) {
3049
- case "claude":
3050
- return await handleClaude(interaction);
3524
+ case "session":
3525
+ return await handleSession(interaction);
3051
3526
  case "shell":
3052
3527
  return await handleShell(interaction);
3053
3528
  case "agent":
@@ -3059,8 +3534,8 @@ async function startBot() {
3059
3534
  }
3060
3535
  }
3061
3536
  if (interaction.isAutocomplete()) {
3062
- if (interaction.commandName === "claude") {
3063
- return await handleClaudeAutocomplete(interaction);
3537
+ if (interaction.commandName === "session") {
3538
+ return await handleSessionAutocomplete(interaction);
3064
3539
  }
3065
3540
  if (interaction.commandName === "plugin") {
3066
3541
  return await handlePluginAutocomplete(interaction);