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