@tspappsen/elamax 1.2.3

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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Failure taxonomy and structured repair suggestions for worker task failures.
3
+ * Inspired by the AutoResearchClaw ExperimentDiagnosis pattern.
4
+ */
5
+ const TAXONOMY = [
6
+ {
7
+ type: "timeout",
8
+ label: "Timeout",
9
+ pattern: /timeout|timed?\s*out/i,
10
+ suggestion: "Increase WORKER_TIMEOUT in your Max .env file, or break the task into smaller, faster steps.",
11
+ },
12
+ {
13
+ type: "permission_error",
14
+ label: "Permission Error",
15
+ pattern: /permission denied|EACCES|EPERM|not permitted|access denied|unauthorized/i,
16
+ suggestion: "Check file/directory permissions. You may need elevated privileges or corrected ownership (chmod/chown).",
17
+ },
18
+ {
19
+ type: "network_error",
20
+ label: "Network Error",
21
+ pattern: /ECONNREFUSED|ECONNRESET|ENOTFOUND|socket hang up|fetch failed|getaddrinfo|connect ETIMEDOUT|EHOSTUNREACH/i,
22
+ suggestion: "Check your network connection, firewall rules, or proxy settings. The remote service may be temporarily unavailable.",
23
+ },
24
+ {
25
+ type: "file_not_found",
26
+ label: "File Not Found",
27
+ pattern: /ENOENT|no such file or directory|file not found|cannot find module|MODULE_NOT_FOUND/i,
28
+ suggestion: "Verify the file or directory path exists. If a module is missing, run the relevant install command (npm install, pip install, etc.).",
29
+ },
30
+ {
31
+ type: "dependency_error",
32
+ label: "Dependency Error",
33
+ pattern: /npm ERR|yarn error|pip.*error|no module named|package not found|unresolved dependency/i,
34
+ suggestion: "Install missing dependencies with the relevant package manager (npm install, pip install, cargo build, etc.).",
35
+ },
36
+ {
37
+ type: "syntax_error",
38
+ label: "Syntax Error",
39
+ pattern: /SyntaxError|syntax error|unexpected token|parse error|invalid syntax/i,
40
+ suggestion: "Review the code for syntax mistakes — mismatched brackets, missing semicolons, or invalid expressions.",
41
+ },
42
+ {
43
+ type: "config_error",
44
+ label: "Configuration Error",
45
+ pattern: /missing.*key|invalid.*key|API key|token.*invalid|credentials|auth.*failed|configuration.*error|invalid.*config/i,
46
+ suggestion: "Check your configuration files and environment variables. Ensure all required keys and credentials are set correctly.",
47
+ },
48
+ {
49
+ type: "logic_error",
50
+ label: "Logic Error",
51
+ pattern: /assertion.*failed|AssertionError|TypeError|ReferenceError|undefined is not|null is not|cannot read prop|NaN|stack overflow/i,
52
+ suggestion: "Review the task logic and the full error trace. Consider simplifying the prompt or splitting the task into smaller, verifiable steps.",
53
+ },
54
+ ];
55
+ /** Classify an error message into a named failure type with a targeted repair suggestion. */
56
+ export function diagnoseFailure(errorMessage) {
57
+ for (const entry of TAXONOMY) {
58
+ if (entry.pattern.test(errorMessage)) {
59
+ return { type: entry.type, label: entry.label, suggestion: entry.suggestion };
60
+ }
61
+ }
62
+ return {
63
+ type: "unknown",
64
+ label: "Unknown Error",
65
+ suggestion: "Review the full error output. If the problem persists, try restarting Max or breaking the task into smaller steps.",
66
+ };
67
+ }
68
+ /** Format a failure diagnosis as a structured, readable report for the orchestrator. */
69
+ export function formatDiagnosedFailure(workerName, elapsed, errorMessage) {
70
+ const diagnosis = diagnoseFailure(errorMessage);
71
+ return [
72
+ `⚠️ Worker '${workerName}' failed after ${elapsed}s`,
73
+ ``,
74
+ `Failure type: ${diagnosis.label}`,
75
+ `Error: ${errorMessage}`,
76
+ `Suggestion: ${diagnosis.suggestion}`,
77
+ ].join("\n");
78
+ }
79
+ //# sourceMappingURL=diagnosis.js.map
@@ -0,0 +1,505 @@
1
+ import { Client, GatewayIntentBits, Events, REST, Routes, SlashCommandBuilder, ThreadAutoArchiveDuration, } from "discord.js";
2
+ import { createWriteStream } from "node:fs";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { basename, extname, join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { config, persistModel } from "../config.js";
8
+ import { sendToOrchestrator, cancelCurrentMessage, getWorkers, getLastRouteResult, invalidateSession, } from "../copilot/orchestrator.js";
9
+ import { chunkMessage } from "./formatter.js";
10
+ import { searchMemories } from "../store/db.js";
11
+ import { listSkills } from "../copilot/skills.js";
12
+ import { restartDaemon } from "../daemon.js";
13
+ import { getRouterConfig, updateRouterConfig } from "../copilot/router.js";
14
+ let client;
15
+ const commands = [
16
+ new SlashCommandBuilder().setName("help").setDescription("Show help"),
17
+ new SlashCommandBuilder().setName("cancel").setDescription("Cancel the current message"),
18
+ new SlashCommandBuilder()
19
+ .setName("model")
20
+ .setDescription("Show or switch the AI model")
21
+ .addStringOption((opt) => opt.setName("name").setDescription("Model name to switch to").setRequired(false)),
22
+ new SlashCommandBuilder().setName("auto").setDescription("Toggle auto model routing"),
23
+ new SlashCommandBuilder().setName("memory").setDescription("Show stored memories"),
24
+ new SlashCommandBuilder().setName("skills").setDescription("List installed skills"),
25
+ new SlashCommandBuilder().setName("workers").setDescription("List active worker sessions"),
26
+ new SlashCommandBuilder().setName("restart").setDescription("Restart Max"),
27
+ ];
28
+ function getHelpText() {
29
+ return ("I'm Max, your AI daemon.\n\n" +
30
+ "Send a message in an allowed guild channel and I'll handle it. You can also attach images for analysis.\n\n" +
31
+ "Commands:\n" +
32
+ "/cancel — Cancel the current message\n" +
33
+ "/model — Show current model\n" +
34
+ "/model name:<name> — Switch model\n" +
35
+ "/auto — Toggle auto model routing\n" +
36
+ "/memory — Show stored memories\n" +
37
+ "/skills — List installed skills\n" +
38
+ "/workers — List active worker sessions\n" +
39
+ "/restart — Restart Max\n" +
40
+ "/help — Show this help");
41
+ }
42
+ function isAllowedChannel(channelId) {
43
+ return config.discordAllowedChannelIds.includes(channelId);
44
+ }
45
+ async function sendDiscordInteractionResponse(interaction, text) {
46
+ const chunks = chunkMessage(text);
47
+ for (let i = 0; i < chunks.length; i++) {
48
+ if (i === 0) {
49
+ if (interaction.deferred || interaction.replied) {
50
+ await interaction.editReply(chunks[i]);
51
+ }
52
+ else {
53
+ await interaction.reply(chunks[i]);
54
+ }
55
+ }
56
+ else {
57
+ await interaction.followUp(chunks[i]);
58
+ }
59
+ }
60
+ }
61
+ async function sendDiscordResponse(message, text, indicator, threadId) {
62
+ const content = text + (indicator ?? "");
63
+ const chunks = chunkMessage(content);
64
+ if (threadId) {
65
+ const threadChannel = ((client.channels.cache.get(threadId) ?? await client.channels.fetch(threadId).catch(() => null)));
66
+ if (threadChannel) {
67
+ for (let i = 0; i < chunks.length; i++) {
68
+ try {
69
+ if (i === 0) {
70
+ await message.reply(chunks[i]);
71
+ }
72
+ else {
73
+ await threadChannel.send(chunks[i]);
74
+ }
75
+ }
76
+ catch {
77
+ try {
78
+ await threadChannel.send(chunks[i]);
79
+ }
80
+ catch {
81
+ // Nothing more we can do
82
+ }
83
+ }
84
+ }
85
+ return;
86
+ }
87
+ }
88
+ const channel = message.channel;
89
+ for (let i = 0; i < chunks.length; i++) {
90
+ try {
91
+ if (i === 0) {
92
+ await message.reply(chunks[i]);
93
+ }
94
+ else {
95
+ await channel.send(chunks[i]);
96
+ }
97
+ }
98
+ catch {
99
+ try {
100
+ await channel.send(chunks[i]);
101
+ }
102
+ catch {
103
+ // Nothing more we can do
104
+ }
105
+ }
106
+ }
107
+ }
108
+ function startTypingIndicator(channel) {
109
+ void channel.sendTyping().catch(() => { });
110
+ const interval = setInterval(() => {
111
+ void channel.sendTyping().catch(() => { });
112
+ }, 8000);
113
+ return () => clearInterval(interval);
114
+ }
115
+ async function handleDiscordTurn(message, prompt, options) {
116
+ const effectiveChannelId = message.channel.isThread()
117
+ ? message.channel.parentId ?? message.channelId
118
+ : message.channelId;
119
+ const threadId = message.channel.isThread() ? message.channelId : undefined;
120
+ const messageId = message.id;
121
+ const stopTyping = startTypingIndicator(message.channel);
122
+ try {
123
+ await sendToOrchestrator(prompt, { type: "discord", channelId: effectiveChannelId, messageId, threadId }, (text, done) => {
124
+ if (!done) {
125
+ return;
126
+ }
127
+ stopTyping();
128
+ const routeResult = getLastRouteResult();
129
+ const indicator = routeResult && routeResult.routerMode === "auto"
130
+ ? `\n\n*⚡ auto · ${routeResult.model}*`
131
+ : undefined;
132
+ void sendDiscordResponse(message, text, indicator, threadId).catch((err) => {
133
+ console.error("[max] Discord response failed:", err instanceof Error ? err.message : err);
134
+ });
135
+ }, options);
136
+ }
137
+ finally {
138
+ stopTyping();
139
+ }
140
+ }
141
+ async function activeModelSupportsVision() {
142
+ try {
143
+ const { getClient } = await import("../copilot/client.js");
144
+ const sdkClient = await getClient();
145
+ const models = await sdkClient.listModels();
146
+ return models.find((model) => model.id === config.copilotModel)?.capabilities.supports.vision;
147
+ }
148
+ catch {
149
+ return undefined;
150
+ }
151
+ }
152
+ function sanitizeFileName(fileName) {
153
+ return fileName.replace(/[^a-zA-Z0-9._-]/g, "-");
154
+ }
155
+ async function downloadDiscordImages(imageAttachments) {
156
+ const tempDir = await mkdtemp(join(tmpdir(), "max-discord-"));
157
+ const attachments = [];
158
+ try {
159
+ for (let i = 0; i < imageAttachments.length; i++) {
160
+ const attachment = imageAttachments[i];
161
+ const urlPath = new URL(attachment.url).pathname;
162
+ const fallbackName = basename(urlPath) || `discord-image-${i + 1}${extname(urlPath) || ".png"}`;
163
+ const sourceName = attachment.name || fallbackName;
164
+ const safeName = sanitizeFileName(sourceName) || `discord-image-${i + 1}${extname(sourceName) || ".png"}`;
165
+ const filePath = join(tempDir, safeName);
166
+ const response = await fetch(attachment.url);
167
+ if (!response.ok || !response.body) {
168
+ throw new Error(`Discord image download failed (${response.status} ${response.statusText})`);
169
+ }
170
+ await pipeline(response.body, createWriteStream(filePath));
171
+ attachments.push({
172
+ type: "file",
173
+ path: filePath,
174
+ displayName: attachment.name || safeName,
175
+ });
176
+ }
177
+ return { tempDir, attachments };
178
+ }
179
+ catch (err) {
180
+ await removeTempDir(tempDir);
181
+ throw err;
182
+ }
183
+ }
184
+ async function removeTempDir(tempDir) {
185
+ if (!tempDir) {
186
+ return;
187
+ }
188
+ try {
189
+ await rm(tempDir, { recursive: true, force: true });
190
+ }
191
+ catch {
192
+ // Best-effort cleanup
193
+ }
194
+ }
195
+ async function handleDiscordImageMessage(message, prompt, imageAttachments) {
196
+ const supportsVision = await activeModelSupportsVision();
197
+ if (supportsVision === false) {
198
+ await sendDiscordResponse(message, `The current model (${config.copilotModel}) can't analyze images. Switch to a vision-capable model with /model and try again.`);
199
+ return;
200
+ }
201
+ let tempDir;
202
+ try {
203
+ const downloaded = await downloadDiscordImages(imageAttachments);
204
+ tempDir = downloaded.tempDir;
205
+ await handleDiscordTurn(message, prompt, { attachments: downloaded.attachments });
206
+ }
207
+ catch (err) {
208
+ const msg = err instanceof Error ? err.message : String(err);
209
+ await sendDiscordResponse(message, `I couldn't process that image: ${msg}`);
210
+ }
211
+ finally {
212
+ await removeTempDir(tempDir);
213
+ }
214
+ }
215
+ async function registerSlashCommands(readyClient) {
216
+ const guildIds = new Set();
217
+ for (const channelId of config.discordAllowedChannelIds) {
218
+ const resolved = readyClient.channels.cache.get(channelId)
219
+ ?? await readyClient.channels.fetch(channelId).catch(() => null);
220
+ if (resolved && "guildId" in resolved && typeof resolved.guildId === "string") {
221
+ guildIds.add(resolved.guildId);
222
+ }
223
+ }
224
+ const rest = new REST().setToken(config.discordBotToken);
225
+ const commandData = commands.map((command) => command.toJSON());
226
+ for (const guildId of guildIds) {
227
+ try {
228
+ await rest.put(Routes.applicationGuildCommands(readyClient.user.id, guildId), {
229
+ body: commandData,
230
+ });
231
+ console.log(`[max] Discord commands registered for guild ${guildId}`);
232
+ }
233
+ catch (err) {
234
+ console.error(`[max] Failed to register Discord commands for guild ${guildId}:`, err instanceof Error ? err.message : err);
235
+ }
236
+ }
237
+ }
238
+ async function handleSlashCommand(interaction) {
239
+ if (!interaction.inGuild() || !interaction.channelId || !isAllowedChannel(interaction.channelId)) {
240
+ try {
241
+ await interaction.reply({ content: "This channel isn't allowed for Max.", ephemeral: true });
242
+ }
243
+ catch {
244
+ // Nothing more we can do
245
+ }
246
+ return;
247
+ }
248
+ await interaction.deferReply();
249
+ switch (interaction.commandName) {
250
+ case "help": {
251
+ await sendDiscordInteractionResponse(interaction, getHelpText());
252
+ return;
253
+ }
254
+ case "cancel": {
255
+ const cancelled = await cancelCurrentMessage();
256
+ await sendDiscordInteractionResponse(interaction, cancelled ? "⛔ Cancelled." : "Nothing to cancel.");
257
+ return;
258
+ }
259
+ case "model": {
260
+ const arg = interaction.options.getString("name")?.trim();
261
+ if (arg) {
262
+ try {
263
+ const { getClient } = await import("../copilot/client.js");
264
+ const sdkClient = await getClient();
265
+ const models = await sdkClient.listModels();
266
+ const match = models.find((model) => model.id === arg);
267
+ if (!match) {
268
+ const suggestions = models
269
+ .filter((model) => model.id.includes(arg) || model.id.toLowerCase().includes(arg.toLowerCase()))
270
+ .map((model) => model.id);
271
+ const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
272
+ await sendDiscordInteractionResponse(interaction, `Model '${arg}' not found.${hint}`);
273
+ return;
274
+ }
275
+ }
276
+ catch {
277
+ // If validation fails, allow the switch and let the next request surface any real issue.
278
+ }
279
+ const previous = config.copilotModel;
280
+ config.copilotModel = arg;
281
+ persistModel(arg);
282
+ invalidateSession();
283
+ await sendDiscordInteractionResponse(interaction, `Model: ${previous} → ${arg}`);
284
+ return;
285
+ }
286
+ await sendDiscordInteractionResponse(interaction, `Current model: ${config.copilotModel}`);
287
+ return;
288
+ }
289
+ case "auto": {
290
+ const current = getRouterConfig();
291
+ const newState = !current.enabled;
292
+ updateRouterConfig({ enabled: newState });
293
+ const label = newState
294
+ ? "⚡ Auto mode on"
295
+ : `Auto mode off · using ${config.copilotModel}`;
296
+ await sendDiscordInteractionResponse(interaction, label);
297
+ return;
298
+ }
299
+ case "memory": {
300
+ const memories = searchMemories(undefined, undefined, 50);
301
+ const text = memories.length === 0
302
+ ? "No memories stored."
303
+ : `${memories.map((memory) => `#${memory.id} [${memory.category}] ${memory.content}`).join("\n")}\n\n${memories.length} total`;
304
+ await sendDiscordInteractionResponse(interaction, text);
305
+ return;
306
+ }
307
+ case "skills": {
308
+ const skills = listSkills();
309
+ const text = skills.length === 0
310
+ ? "No skills installed."
311
+ : skills.map((skill) => `• ${skill.name} (${skill.source}) — ${skill.description}`).join("\n");
312
+ await sendDiscordInteractionResponse(interaction, text);
313
+ return;
314
+ }
315
+ case "workers": {
316
+ const workers = Array.from(getWorkers().values());
317
+ const text = workers.length === 0
318
+ ? "No active worker sessions."
319
+ : workers.map((worker) => `• ${worker.name} (${worker.workingDir}) — ${worker.status}`).join("\n");
320
+ await sendDiscordInteractionResponse(interaction, text);
321
+ return;
322
+ }
323
+ case "restart": {
324
+ await sendDiscordInteractionResponse(interaction, "⏳ Restarting Max...");
325
+ setTimeout(() => {
326
+ restartDaemon().catch((err) => {
327
+ console.error("[max] Restart failed:", err);
328
+ });
329
+ }, 500);
330
+ return;
331
+ }
332
+ default: {
333
+ await sendDiscordInteractionResponse(interaction, "Unknown command.");
334
+ }
335
+ }
336
+ }
337
+ export function createDiscordBot() {
338
+ if (!config.discordBotToken) {
339
+ throw new Error("Discord bot token is missing. Run 'max setup' and enter your Discord bot token.");
340
+ }
341
+ if (config.discordAllowedChannelIds.length === 0) {
342
+ throw new Error("Discord allowed channel IDs are missing. Run 'max setup' and configure at least one Discord channel ID.");
343
+ }
344
+ if (client) {
345
+ return client;
346
+ }
347
+ client = new Client({
348
+ intents: [
349
+ GatewayIntentBits.Guilds,
350
+ GatewayIntentBits.GuildMessages,
351
+ GatewayIntentBits.MessageContent,
352
+ ],
353
+ });
354
+ client.on(Events.MessageCreate, async (message) => {
355
+ if (message.author.bot) {
356
+ return;
357
+ }
358
+ const effectiveChannelId = message.channel.isThread()
359
+ ? message.channel.parentId ?? message.channelId
360
+ : message.channelId;
361
+ if (!isAllowedChannel(effectiveChannelId)) {
362
+ return;
363
+ }
364
+ try {
365
+ const imageAttachments = [...message.attachments.values()]
366
+ .filter((attachment) => attachment.contentType?.startsWith("image/"))
367
+ .map((attachment) => ({
368
+ url: attachment.url,
369
+ name: attachment.name,
370
+ contentType: attachment.contentType ?? undefined,
371
+ }));
372
+ if (imageAttachments.length > 0) {
373
+ const prompt = message.content.trim() || "Please analyze the attached image.";
374
+ await handleDiscordImageMessage(message, prompt, imageAttachments);
375
+ return;
376
+ }
377
+ const prompt = message.content.trim();
378
+ if (!prompt) {
379
+ return;
380
+ }
381
+ await handleDiscordTurn(message, prompt);
382
+ }
383
+ catch (err) {
384
+ console.error("[max] Discord message handling failed:", err instanceof Error ? err.message : err);
385
+ try {
386
+ await message.reply("Something went wrong processing that message. Please try again.");
387
+ }
388
+ catch {
389
+ // Nothing more we can do
390
+ }
391
+ }
392
+ });
393
+ client.on(Events.Error, (err) => {
394
+ console.error("[max] Discord client error (kept alive):", err.message);
395
+ });
396
+ client.on(Events.Warn, (warning) => {
397
+ console.warn("[max] Discord warning:", warning);
398
+ });
399
+ client.on(Events.InteractionCreate, async (interaction) => {
400
+ if (!interaction.isChatInputCommand()) {
401
+ return;
402
+ }
403
+ try {
404
+ await handleSlashCommand(interaction);
405
+ }
406
+ catch (err) {
407
+ const msg = err instanceof Error ? err.message : String(err);
408
+ console.error("[max] Discord command failed:", msg);
409
+ try {
410
+ if (interaction.deferred || interaction.replied) {
411
+ await interaction.editReply(`Error: ${msg}`);
412
+ }
413
+ else {
414
+ await interaction.reply({ content: `Error: ${msg}`, ephemeral: true });
415
+ }
416
+ }
417
+ catch {
418
+ // Nothing more we can do
419
+ }
420
+ }
421
+ });
422
+ return client;
423
+ }
424
+ export async function startDiscordBot() {
425
+ if (!client) {
426
+ throw new Error("Discord bot not created");
427
+ }
428
+ if (!config.discordBotToken) {
429
+ throw new Error("Discord bot token is missing. Run 'max setup' and enter your Discord bot token.");
430
+ }
431
+ console.log("[max] Discord bot starting...");
432
+ try {
433
+ // Wait for the ClientReady event before proceeding
434
+ const readyPromise = new Promise((resolve) => {
435
+ client.once(Events.ClientReady, resolve);
436
+ });
437
+ await client.login(config.discordBotToken);
438
+ const readyClient = await readyPromise;
439
+ console.log(`[max] Discord bot connected as ${readyClient.user.tag}`);
440
+ await registerSlashCommands(readyClient);
441
+ }
442
+ catch (err) {
443
+ const msg = err instanceof Error ? err.message : String(err);
444
+ console.error("[max] ❌ Discord bot failed to start:", msg);
445
+ throw err;
446
+ }
447
+ }
448
+ export async function stopDiscordBot() {
449
+ if (client) {
450
+ client.destroy();
451
+ }
452
+ }
453
+ export async function sendDiscordProactiveMessage(text, channelId, threadId) {
454
+ if (!client || !channelId) {
455
+ return;
456
+ }
457
+ if (!isAllowedChannel(channelId)) {
458
+ return;
459
+ }
460
+ const targetChannelId = threadId ?? channelId;
461
+ try {
462
+ const channel = client.channels.cache.get(targetChannelId) ?? await client.channels.fetch(targetChannelId).catch(() => null);
463
+ if (channel?.isTextBased() && "send" in channel) {
464
+ const chunks = chunkMessage(text);
465
+ for (const chunk of chunks) {
466
+ await channel.send(chunk);
467
+ }
468
+ }
469
+ }
470
+ catch (err) {
471
+ console.error("[max] Failed to send Discord proactive message:", err instanceof Error ? err.message : err);
472
+ }
473
+ }
474
+ export async function sendDiscordPhoto(photo, channelId, caption) {
475
+ if (!client || !channelId) {
476
+ return;
477
+ }
478
+ if (!isAllowedChannel(channelId)) {
479
+ return;
480
+ }
481
+ try {
482
+ const channel = await client.channels.fetch(channelId);
483
+ if (channel?.isTextBased() && "send" in channel) {
484
+ const { AttachmentBuilder } = await import("discord.js");
485
+ const attachment = new AttachmentBuilder(photo);
486
+ await channel.send({ content: caption, files: [attachment] });
487
+ }
488
+ }
489
+ catch (err) {
490
+ console.error("[max] Failed to send Discord photo:", err instanceof Error ? err.message : err);
491
+ }
492
+ }
493
+ export async function startDiscordThread(channelId, messageId, name = "Max") {
494
+ if (!client) {
495
+ throw new Error("Discord client not initialized");
496
+ }
497
+ const channel = client.channels.cache.get(channelId) ?? await client.channels.fetch(channelId);
498
+ if (!channel || !("messages" in channel)) {
499
+ throw new Error(`Channel ${channelId} not found or does not support messages`);
500
+ }
501
+ const msg = await channel.messages.fetch(messageId);
502
+ const thread = await msg.startThread({ name, autoArchiveDuration: ThreadAutoArchiveDuration.OneHour });
503
+ return thread.id;
504
+ }
505
+ //# sourceMappingURL=bot.js.map
@@ -0,0 +1,29 @@
1
+ const DISCORD_MAX_LENGTH = 2000;
2
+ /**
3
+ * Split a long message into chunks that fit within Discord's message limit.
4
+ * Tries to split at newlines, then spaces, falling back to hard cuts.
5
+ */
6
+ export function chunkMessage(text) {
7
+ if (text.length <= DISCORD_MAX_LENGTH) {
8
+ return [text];
9
+ }
10
+ const chunks = [];
11
+ let remaining = text;
12
+ while (remaining.length > 0) {
13
+ if (remaining.length <= DISCORD_MAX_LENGTH) {
14
+ chunks.push(remaining);
15
+ break;
16
+ }
17
+ let splitAt = remaining.lastIndexOf("\n", DISCORD_MAX_LENGTH);
18
+ if (splitAt < DISCORD_MAX_LENGTH * 0.3) {
19
+ splitAt = remaining.lastIndexOf(" ", DISCORD_MAX_LENGTH);
20
+ }
21
+ if (splitAt < DISCORD_MAX_LENGTH * 0.3) {
22
+ splitAt = DISCORD_MAX_LENGTH;
23
+ }
24
+ chunks.push(remaining.slice(0, splitAt));
25
+ remaining = remaining.slice(splitAt).trimStart();
26
+ }
27
+ return chunks;
28
+ }
29
+ //# sourceMappingURL=formatter.js.map
package/dist/paths.js ADDED
@@ -0,0 +1,37 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+ import { mkdirSync } from "fs";
4
+ const profile = process.env.MAX_PROFILE?.trim() || undefined;
5
+ const folderName = profile ? `.max-${profile}` : ".max";
6
+ /** The active profile name, or undefined for the default (main) instance. */
7
+ export const MAX_PROFILE = profile;
8
+ /** True when running as the watchdog profile. */
9
+ export const IS_WATCHDOG = profile === "watchdog";
10
+ /** Base directory for all Max user data: ~/.max (or ~/.max-<profile>) */
11
+ export const MAX_HOME = process.env.MAX_HOME ?? join(homedir(), folderName);
12
+ /** Main Max's home directory — used by watchdog to read main's logs/state. */
13
+ export const MAIN_MAX_HOME = process.env.MAIN_MAX_HOME ?? join(homedir(), ".max");
14
+ /** Path to the SQLite database */
15
+ export const DB_PATH = join(MAX_HOME, "max.db");
16
+ /** Path to the user .env file */
17
+ export const ENV_PATH = join(MAX_HOME, ".env");
18
+ /** Path to user-local skills */
19
+ export const SKILLS_DIR = join(MAX_HOME, "skills");
20
+ /** Path to Max's isolated session state (keeps CLI history clean) */
21
+ export const SESSIONS_DIR = join(MAX_HOME, "sessions");
22
+ /** Path to TUI readline history */
23
+ export const HISTORY_PATH = join(MAX_HOME, "tui_history");
24
+ /** Path to optional TUI debug log */
25
+ export const TUI_DEBUG_LOG_PATH = join(MAX_HOME, "tui-debug.log");
26
+ /** Path to the daemon log file */
27
+ export const DAEMON_LOG_PATH = join(MAX_HOME, "daemon.log");
28
+ /** Path to the API bearer token file */
29
+ export const API_TOKEN_PATH = join(MAX_HOME, "api-token");
30
+ /** Path to user workspace instructions (~/.max/instructions/) */
31
+ export const INSTRUCTIONS_DIR = join(MAX_HOME, "instructions");
32
+ /** Ensure ~/.max/ and its subdirectories exist */
33
+ export function ensureMaxHome() {
34
+ mkdirSync(MAX_HOME, { recursive: true });
35
+ mkdirSync(INSTRUCTIONS_DIR, { recursive: true });
36
+ }
37
+ //# sourceMappingURL=paths.js.map