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
|
|
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
|
-
|
|
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
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
684
|
-
|
|
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 =
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
714
|
-
if (
|
|
715
|
-
session.
|
|
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 (
|
|
719
|
-
|
|
720
|
-
session.totalCost += message.total_cost_usd;
|
|
721
|
-
}
|
|
980
|
+
if (event.type === "result") {
|
|
981
|
+
session.totalCost += event.costUsd;
|
|
722
982
|
}
|
|
723
|
-
yield
|
|
983
|
+
yield event;
|
|
724
984
|
}
|
|
725
985
|
session.messageCount++;
|
|
726
986
|
} catch (err) {
|
|
727
|
-
if (err
|
|
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
|
|
1006
|
+
const provider = await ensureProvider(session.provider);
|
|
1007
|
+
const systemPromptParts = buildSystemPromptParts(session);
|
|
747
1008
|
try {
|
|
748
|
-
const stream =
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
|
764
|
-
if (
|
|
765
|
-
session.
|
|
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 (
|
|
769
|
-
session.totalCost +=
|
|
1021
|
+
if (event.type === "result") {
|
|
1022
|
+
session.totalCost += event.costUsd;
|
|
770
1023
|
}
|
|
771
|
-
yield
|
|
1024
|
+
yield event;
|
|
772
1025
|
}
|
|
773
1026
|
session.messageCount++;
|
|
774
1027
|
} catch (err) {
|
|
775
|
-
if (err
|
|
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
|
|
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.
|
|
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
|
|
1483
|
+
function renderTaskToolEmbed(action, dataJson) {
|
|
1191
1484
|
try {
|
|
1192
|
-
const data = JSON.parse(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const
|
|
1310
|
-
if (
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
pendingImagePath = null;
|
|
1604
|
+
lastToolName = event.action;
|
|
1605
|
+
break;
|
|
1359
1606
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1379
|
-
const isTaskResult =
|
|
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(
|
|
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 =
|
|
1390
|
-
const embed = new
|
|
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 (
|
|
1395
|
-
const contentId = storeExpandable(
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
|
1442
|
-
const
|
|
1443
|
-
|
|
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
|
|
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
|
|
1895
|
+
return handleSessionNew(interaction);
|
|
1585
1896
|
case "resume":
|
|
1586
|
-
return
|
|
1897
|
+
return handleSessionResume(interaction);
|
|
1587
1898
|
case "list":
|
|
1588
|
-
return
|
|
1899
|
+
return handleSessionList(interaction);
|
|
1589
1900
|
case "end":
|
|
1590
|
-
return
|
|
1901
|
+
return handleSessionEnd(interaction);
|
|
1591
1902
|
case "continue":
|
|
1592
|
-
return
|
|
1903
|
+
return handleSessionContinue(interaction);
|
|
1593
1904
|
case "stop":
|
|
1594
|
-
return
|
|
1905
|
+
return handleSessionStop(interaction);
|
|
1595
1906
|
case "output":
|
|
1596
|
-
return
|
|
1907
|
+
return handleSessionOutput(interaction);
|
|
1597
1908
|
case "attach":
|
|
1598
|
-
return
|
|
1909
|
+
return handleSessionAttach(interaction);
|
|
1599
1910
|
case "sync":
|
|
1600
|
-
return
|
|
1911
|
+
return handleSessionSync(interaction);
|
|
1912
|
+
case "id":
|
|
1913
|
+
return handleSessionId(interaction);
|
|
1601
1914
|
case "model":
|
|
1602
|
-
return
|
|
1915
|
+
return handleSessionModel(interaction);
|
|
1603
1916
|
case "verbose":
|
|
1604
|
-
return
|
|
1917
|
+
return handleSessionVerbose(interaction);
|
|
1605
1918
|
case "mode":
|
|
1606
|
-
return
|
|
1919
|
+
return handleSessionMode(interaction);
|
|
1607
1920
|
default:
|
|
1608
1921
|
await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
|
|
1609
1922
|
}
|
|
1610
1923
|
}
|
|
1611
|
-
|
|
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:
|
|
1952
|
+
name: `${provider}-${session.id}`,
|
|
1631
1953
|
type: ChannelType.GuildText,
|
|
1632
1954
|
parent: category.id,
|
|
1633
|
-
topic:
|
|
1955
|
+
topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
|
|
1634
1956
|
});
|
|
1635
1957
|
linkChannel(session.id, channel.id);
|
|
1636
|
-
const
|
|
1637
|
-
{ name: "Channel", value:
|
|
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
|
-
|
|
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
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
|
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
|
|
1767
|
-
const
|
|
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
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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,
|
|
2114
|
+
const session = await createSession(name, directory, "pending", projectName, provider, providerSessionId);
|
|
1785
2115
|
channel = await guild.channels.create({
|
|
1786
|
-
name:
|
|
2116
|
+
name: `${provider}-${session.id}`,
|
|
1787
2117
|
type: ChannelType.GuildText,
|
|
1788
2118
|
parent: category.id,
|
|
1789
|
-
topic:
|
|
2119
|
+
topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
|
|
1790
2120
|
});
|
|
1791
2121
|
linkChannel(session.id, channel.id);
|
|
1792
|
-
const
|
|
1793
|
-
{ name: "Channel", value:
|
|
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: "
|
|
1797
|
-
|
|
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 ${
|
|
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
|
|
1804
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2570
|
-
|
|
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" })
|
|
2581
|
-
if (resized.length <=
|
|
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
|
-
|
|
2589
|
-
if (buf.length >
|
|
2590
|
-
|
|
2591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 "
|
|
3050
|
-
return await
|
|
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 === "
|
|
3063
|
-
return await
|
|
3507
|
+
if (interaction.commandName === "session") {
|
|
3508
|
+
return await handleSessionAutocomplete(interaction);
|
|
3064
3509
|
}
|
|
3065
3510
|
if (interaction.commandName === "plugin") {
|
|
3066
3511
|
return await handlePluginAutocomplete(interaction);
|