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
|
|
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";
|
|
@@ -479,7 +744,7 @@ function detectYesNoPrompt(text) {
|
|
|
479
744
|
}
|
|
480
745
|
|
|
481
746
|
// src/session-manager.ts
|
|
482
|
-
var SESSION_PREFIX = "
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
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
|
-
|
|
671
|
-
|
|
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
|
|
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
|
-
}
|
|
966
|
+
const provider = await ensureProvider(session.provider);
|
|
967
|
+
const systemPromptParts = buildSystemPromptParts(session);
|
|
698
968
|
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
|
-
}
|
|
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
|
|
714
|
-
if (
|
|
715
|
-
session.
|
|
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 (
|
|
719
|
-
|
|
720
|
-
session.totalCost += message.total_cost_usd;
|
|
721
|
-
}
|
|
981
|
+
if (event.type === "result") {
|
|
982
|
+
session.totalCost += event.costUsd;
|
|
722
983
|
}
|
|
723
|
-
yield
|
|
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
|
|
1007
|
+
const provider = await ensureProvider(session.provider);
|
|
1008
|
+
const systemPromptParts = buildSystemPromptParts(session);
|
|
747
1009
|
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
|
-
}
|
|
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
|
|
764
|
-
if (
|
|
765
|
-
session.
|
|
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 (
|
|
769
|
-
session.totalCost +=
|
|
1022
|
+
if (event.type === "result") {
|
|
1023
|
+
session.totalCost += event.costUsd;
|
|
770
1024
|
}
|
|
771
|
-
yield
|
|
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
|
|
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.
|
|
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
|
|
1493
|
+
function renderTaskToolEmbed(action, dataJson) {
|
|
1191
1494
|
try {
|
|
1192
|
-
const data = JSON.parse(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
1379
|
-
const isTaskResult =
|
|
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(
|
|
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 =
|
|
1390
|
-
const embed = new
|
|
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 (
|
|
1395
|
-
const contentId = storeExpandable(
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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
|
|
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
|
|
1911
|
+
return handleSessionNew(interaction);
|
|
1585
1912
|
case "resume":
|
|
1586
|
-
return
|
|
1913
|
+
return handleSessionResume(interaction);
|
|
1587
1914
|
case "list":
|
|
1588
|
-
return
|
|
1915
|
+
return handleSessionList(interaction);
|
|
1589
1916
|
case "end":
|
|
1590
|
-
return
|
|
1917
|
+
return handleSessionEnd(interaction);
|
|
1591
1918
|
case "continue":
|
|
1592
|
-
return
|
|
1919
|
+
return handleSessionContinue(interaction);
|
|
1593
1920
|
case "stop":
|
|
1594
|
-
return
|
|
1921
|
+
return handleSessionStop(interaction);
|
|
1595
1922
|
case "output":
|
|
1596
|
-
return
|
|
1923
|
+
return handleSessionOutput(interaction);
|
|
1597
1924
|
case "attach":
|
|
1598
|
-
return
|
|
1925
|
+
return handleSessionAttach(interaction);
|
|
1599
1926
|
case "sync":
|
|
1600
|
-
return
|
|
1927
|
+
return handleSessionSync(interaction);
|
|
1928
|
+
case "id":
|
|
1929
|
+
return handleSessionId(interaction);
|
|
1601
1930
|
case "model":
|
|
1602
|
-
return
|
|
1931
|
+
return handleSessionModel(interaction);
|
|
1603
1932
|
case "verbose":
|
|
1604
|
-
return
|
|
1933
|
+
return handleSessionVerbose(interaction);
|
|
1605
1934
|
case "mode":
|
|
1606
|
-
return
|
|
1935
|
+
return handleSessionMode(interaction);
|
|
1607
1936
|
default:
|
|
1608
1937
|
await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
|
|
1609
1938
|
}
|
|
1610
1939
|
}
|
|
1611
|
-
|
|
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:
|
|
1968
|
+
name: `${provider}-${session.id}`,
|
|
1631
1969
|
type: ChannelType.GuildText,
|
|
1632
1970
|
parent: category.id,
|
|
1633
|
-
topic:
|
|
1971
|
+
topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
|
|
1634
1972
|
});
|
|
1635
1973
|
linkChannel(session.id, channel.id);
|
|
1636
|
-
const
|
|
1637
|
-
{ name: "Channel", value:
|
|
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
|
-
|
|
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
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
|
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
|
|
1767
|
-
const
|
|
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
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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,
|
|
2130
|
+
const session = await createSession(name, directory, "pending", projectName, provider, providerSessionId);
|
|
1785
2131
|
channel = await guild.channels.create({
|
|
1786
|
-
name:
|
|
2132
|
+
name: `${provider}-${session.id}`,
|
|
1787
2133
|
type: ChannelType.GuildText,
|
|
1788
2134
|
parent: category.id,
|
|
1789
|
-
topic:
|
|
2135
|
+
topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
|
|
1790
2136
|
});
|
|
1791
2137
|
linkChannel(session.id, channel.id);
|
|
1792
|
-
const
|
|
1793
|
-
{ name: "Channel", value:
|
|
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: "
|
|
1797
|
-
|
|
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 ${
|
|
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
|
|
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
|
-
)
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2570
|
-
|
|
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" })
|
|
2581
|
-
if (resized.length <=
|
|
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
|
-
|
|
2589
|
-
if (buf.length >
|
|
2590
|
-
|
|
2591
|
-
|
|
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 `/
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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: ${
|
|
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 "
|
|
3050
|
-
return await
|
|
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 === "
|
|
3063
|
-
return await
|
|
3537
|
+
if (interaction.commandName === "session") {
|
|
3538
|
+
return await handleSessionAutocomplete(interaction);
|
|
3064
3539
|
}
|
|
3065
3540
|
if (interaction.commandName === "plugin") {
|
|
3066
3541
|
return await handlePluginAutocomplete(interaction);
|