@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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/api/server.js +297 -0
- package/dist/cli.js +105 -0
- package/dist/config.js +96 -0
- package/dist/copilot/classifier.js +72 -0
- package/dist/copilot/client.js +30 -0
- package/dist/copilot/mcp-config.js +22 -0
- package/dist/copilot/orchestrator.js +459 -0
- package/dist/copilot/router.js +147 -0
- package/dist/copilot/skills.js +125 -0
- package/dist/copilot/system-message.js +185 -0
- package/dist/copilot/tools.js +486 -0
- package/dist/copilot/watchdog-tools.js +312 -0
- package/dist/copilot/workspace-instructions.js +100 -0
- package/dist/daemon.js +237 -0
- package/dist/diagnosis.js +79 -0
- package/dist/discord/bot.js +505 -0
- package/dist/discord/formatter.js +29 -0
- package/dist/paths.js +37 -0
- package/dist/setup.js +476 -0
- package/dist/store/db.js +173 -0
- package/dist/telegram/bot.js +344 -0
- package/dist/telegram/formatter.js +96 -0
- package/dist/tui/index.js +1026 -0
- package/dist/update.js +72 -0
- package/dist/utils/parseJSON.js +71 -0
- package/package.json +61 -0
- package/skills/.gitkeep +0 -0
- package/skills/find-skills/SKILL.md +161 -0
- package/skills/find-skills/_meta.json +4 -0
- package/templates/instructions/AGENTS.md +18 -0
- package/templates/instructions/TOOLS.md +12 -0
package/dist/setup.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import * as readline from "readline";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
4
|
+
import { ensureMaxHome, ENV_PATH, IS_WATCHDOG, MAX_HOME, MAX_PROFILE } from "./paths.js";
|
|
5
|
+
const BOLD = "\x1b[1m";
|
|
6
|
+
const DIM = "\x1b[2m";
|
|
7
|
+
const GREEN = "\x1b[32m";
|
|
8
|
+
const YELLOW = "\x1b[33m";
|
|
9
|
+
const CYAN = "\x1b[36m";
|
|
10
|
+
const RESET = "\x1b[0m";
|
|
11
|
+
const FALLBACK_MODELS = [
|
|
12
|
+
{ id: "claude-sonnet-4.6", label: "Claude Sonnet 4.6", desc: "Fast, great for most tasks" },
|
|
13
|
+
{ id: "gpt-5.1", label: "GPT-5.1", desc: "OpenAI's fast model" },
|
|
14
|
+
{ id: "gpt-4.1", label: "GPT-4.1", desc: "Free included model" },
|
|
15
|
+
];
|
|
16
|
+
async function fetchModels() {
|
|
17
|
+
let client;
|
|
18
|
+
try {
|
|
19
|
+
client = new CopilotClient({ autoStart: true });
|
|
20
|
+
await client.start();
|
|
21
|
+
const models = await client.listModels();
|
|
22
|
+
return models
|
|
23
|
+
.filter((m) => m.policy?.state === "enabled" && !m.name.includes("(Internal only)"))
|
|
24
|
+
.map((m) => {
|
|
25
|
+
const mult = m.billing?.multiplier;
|
|
26
|
+
const desc = mult === 0 || mult === undefined ? "Included with Copilot" : `Premium (${mult}x)`;
|
|
27
|
+
return { id: m.id, label: m.name, desc };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
try {
|
|
35
|
+
await client?.stop();
|
|
36
|
+
}
|
|
37
|
+
catch { /* best-effort */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function ask(rl, question) {
|
|
41
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
42
|
+
}
|
|
43
|
+
async function askRequired(rl, prompt) {
|
|
44
|
+
while (true) {
|
|
45
|
+
const answer = (await ask(rl, prompt)).trim();
|
|
46
|
+
if (answer)
|
|
47
|
+
return answer;
|
|
48
|
+
console.log(`${YELLOW} This field is required. Please enter a value.${RESET}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function askYesNo(rl, question, defaultYes = false) {
|
|
52
|
+
const hint = defaultYes ? "(Y/n)" : "(y/N)";
|
|
53
|
+
const answer = (await ask(rl, `${question} ${hint} `)).trim().toLowerCase();
|
|
54
|
+
if (answer === "")
|
|
55
|
+
return defaultYes;
|
|
56
|
+
return answer === "y" || answer === "yes";
|
|
57
|
+
}
|
|
58
|
+
async function askPicker(rl, label, options, defaultId) {
|
|
59
|
+
console.log(`${BOLD}${label}${RESET}\n`);
|
|
60
|
+
const defaultIdx = Math.max(0, options.findIndex((o) => o.id === defaultId));
|
|
61
|
+
for (let i = 0; i < options.length; i++) {
|
|
62
|
+
const marker = i === defaultIdx ? `${GREEN}▸${RESET}` : " ";
|
|
63
|
+
const tag = i === defaultIdx ? ` ${DIM}(default)${RESET}` : "";
|
|
64
|
+
console.log(` ${marker} ${CYAN}${i + 1}${RESET} ${options[i].label}${tag}`);
|
|
65
|
+
console.log(` ${DIM}${options[i].desc}${RESET}`);
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
const input = await ask(rl, ` Pick a number ${DIM}(1-${options.length}, Enter for default)${RESET}: `);
|
|
69
|
+
const num = parseInt(input.trim(), 10);
|
|
70
|
+
if (num >= 1 && num <= options.length)
|
|
71
|
+
return options[num - 1].id;
|
|
72
|
+
return options[defaultIdx].id;
|
|
73
|
+
}
|
|
74
|
+
async function runWatchdogSetup(rl) {
|
|
75
|
+
console.log(`
|
|
76
|
+
${BOLD}╔══════════════════════════════════════════╗
|
|
77
|
+
║ 🛡️ Max Watchdog Setup ║
|
|
78
|
+
╚══════════════════════════════════════════╝${RESET}
|
|
79
|
+
`);
|
|
80
|
+
console.log(`${DIM}Config directory: ${MAX_HOME}${RESET}\n`);
|
|
81
|
+
ensureMaxHome();
|
|
82
|
+
const existing = {};
|
|
83
|
+
if (existsSync(ENV_PATH)) {
|
|
84
|
+
for (const line of readFileSync(ENV_PATH, "utf-8").split("\n")) {
|
|
85
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/);
|
|
86
|
+
if (match)
|
|
87
|
+
existing[match[1]] = match[2];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
console.log(`${BOLD}Watchdog basics${RESET}`);
|
|
91
|
+
console.log(`This profile runs a separate Max instance that watches your main Max`);
|
|
92
|
+
console.log(`process, alerts you when something breaks, and can help restart it.`);
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(`${DIM}Use a separate Telegram bot token for the watchdog so its alerts stay distinct.${RESET}`);
|
|
95
|
+
console.log();
|
|
96
|
+
let telegramToken = existing.TELEGRAM_BOT_TOKEN || "";
|
|
97
|
+
while (true) {
|
|
98
|
+
const tokenInput = (await ask(rl, `Telegram bot token${telegramToken ? ` ${DIM}(Enter to keep current: ${telegramToken.slice(0, 12)}...)${RESET}` : ""}: `)).trim();
|
|
99
|
+
if (tokenInput) {
|
|
100
|
+
telegramToken = tokenInput;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (telegramToken)
|
|
104
|
+
break;
|
|
105
|
+
console.log(`${YELLOW} This field is required. Please enter a value.${RESET}`);
|
|
106
|
+
}
|
|
107
|
+
let userId = existing.AUTHORIZED_USER_ID || "";
|
|
108
|
+
while (true) {
|
|
109
|
+
const userIdInput = (await ask(rl, `Authorized user ID${userId ? ` ${DIM}(current: ${userId})${RESET}` : ""}: `)).trim();
|
|
110
|
+
const nextUserId = userIdInput || userId;
|
|
111
|
+
const parsed = parseInt(nextUserId, 10);
|
|
112
|
+
if (nextUserId && !Number.isNaN(parsed) && parsed > 0) {
|
|
113
|
+
userId = nextUserId;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
console.log(`${YELLOW} That doesn't look like a valid user ID. It should be a positive number.${RESET}`);
|
|
117
|
+
}
|
|
118
|
+
let apiPort = existing.API_PORT || "7778";
|
|
119
|
+
while (true) {
|
|
120
|
+
const apiPortInput = (await ask(rl, `API port ${DIM}(default: ${apiPort})${RESET}: `)).trim();
|
|
121
|
+
const nextPort = apiPortInput || apiPort;
|
|
122
|
+
const parsed = parseInt(nextPort, 10);
|
|
123
|
+
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
|
|
124
|
+
apiPort = nextPort;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
console.log(`${YELLOW} Please enter a valid port between 1 and 65535.${RESET}`);
|
|
128
|
+
}
|
|
129
|
+
let mainMaxPm2Name = existing.MAIN_MAX_PM2_NAME || "max";
|
|
130
|
+
while (true) {
|
|
131
|
+
const pm2NameInput = (await ask(rl, `Main Max pm2 process name ${DIM}(default: ${mainMaxPm2Name})${RESET}: `)).trim();
|
|
132
|
+
const nextPm2Name = pm2NameInput || mainMaxPm2Name;
|
|
133
|
+
if (nextPm2Name) {
|
|
134
|
+
mainMaxPm2Name = nextPm2Name;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
console.log(`${YELLOW} Please enter a process name.${RESET}`);
|
|
138
|
+
}
|
|
139
|
+
let discordToken = existing.DISCORD_BOT_TOKEN || "";
|
|
140
|
+
let discordChannelIds = existing.DISCORD_ALLOWED_CHANNEL_IDS || "";
|
|
141
|
+
const setupDiscord = await askYesNo(rl, "Would you like watchdog alerts in Discord too?", !!discordToken || !!discordChannelIds);
|
|
142
|
+
if (setupDiscord) {
|
|
143
|
+
while (true) {
|
|
144
|
+
const tokenInput = (await ask(rl, `Discord bot token${discordToken ? ` ${DIM}(Enter to keep current: ${discordToken.slice(0, 12)}...)${RESET}` : ""}: `)).trim();
|
|
145
|
+
if (tokenInput) {
|
|
146
|
+
discordToken = tokenInput;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (discordToken)
|
|
150
|
+
break;
|
|
151
|
+
console.log(`${YELLOW} This field is required when Discord is enabled.${RESET}`);
|
|
152
|
+
}
|
|
153
|
+
while (true) {
|
|
154
|
+
const channelIdsInput = (await ask(rl, `Discord allowed channel IDs ${DIM}(comma-separated)${RESET}${discordChannelIds ? ` ${DIM}(current: ${discordChannelIds})${RESET}` : ""}: `)).trim();
|
|
155
|
+
if (channelIdsInput) {
|
|
156
|
+
discordChannelIds = channelIdsInput;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (discordChannelIds)
|
|
160
|
+
break;
|
|
161
|
+
console.log(`${YELLOW} Please enter at least one channel ID when Discord is enabled.${RESET}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
console.log(`\n${BOLD}━━━ Default Model ━━━${RESET}\n`);
|
|
165
|
+
console.log(`${DIM}Fetching available models from Copilot...${RESET}`);
|
|
166
|
+
let models = await fetchModels();
|
|
167
|
+
if (models.length === 0) {
|
|
168
|
+
console.log(`${YELLOW} Could not fetch models (Copilot CLI may not be authenticated yet).${RESET}`);
|
|
169
|
+
console.log(`${DIM} Showing a curated list — you can switch anytime after setup.${RESET}\n`);
|
|
170
|
+
models = FALLBACK_MODELS;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.log(`${GREEN} ✓ Found ${models.length} models${RESET}\n`);
|
|
174
|
+
}
|
|
175
|
+
const currentModel = existing.COPILOT_MODEL || "claude-sonnet-4.6";
|
|
176
|
+
const model = await askPicker(rl, "Choose a default model:", models, currentModel);
|
|
177
|
+
const modelLabel = models.find((m) => m.id === model)?.label || model;
|
|
178
|
+
const lines = [
|
|
179
|
+
"# Max Watchdog Configuration",
|
|
180
|
+
"MAX_PROFILE=watchdog",
|
|
181
|
+
`TELEGRAM_BOT_TOKEN=${telegramToken}`,
|
|
182
|
+
`AUTHORIZED_USER_ID=${userId}`,
|
|
183
|
+
`API_PORT=${apiPort}`,
|
|
184
|
+
`MAIN_MAX_PM2_NAME=${mainMaxPm2Name}`,
|
|
185
|
+
`COPILOT_MODEL=${model}`,
|
|
186
|
+
"# Optional Discord",
|
|
187
|
+
];
|
|
188
|
+
if (discordToken) {
|
|
189
|
+
lines.push(`DISCORD_BOT_TOKEN=${discordToken}`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
lines.push("# DISCORD_BOT_TOKEN=<token>");
|
|
193
|
+
}
|
|
194
|
+
if (discordChannelIds) {
|
|
195
|
+
lines.push(`DISCORD_ALLOWED_CHANNEL_IDS=${discordChannelIds}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
lines.push("# DISCORD_ALLOWED_CHANNEL_IDS=<ids>");
|
|
199
|
+
}
|
|
200
|
+
writeFileSync(ENV_PATH, lines.join("\n") + "\n");
|
|
201
|
+
console.log(`
|
|
202
|
+
${GREEN}${BOLD}✅ Max Watchdog is ready!${RESET}
|
|
203
|
+
${DIM}Config saved to ${ENV_PATH}${RESET}
|
|
204
|
+
|
|
205
|
+
${BOLD}Next steps:${RESET}
|
|
206
|
+
|
|
207
|
+
${CYAN}•${RESET} Start the watchdog:
|
|
208
|
+
${BOLD}MAX_PROFILE=watchdog max start${RESET}
|
|
209
|
+
${BOLD}max start --profile watchdog${RESET}
|
|
210
|
+
|
|
211
|
+
${CYAN}•${RESET} Register it with pm2:
|
|
212
|
+
${BOLD}pm2 start "max start --profile watchdog" --name max-watchdog${RESET}
|
|
213
|
+
|
|
214
|
+
${CYAN}•${RESET} Main Max pm2 process name:
|
|
215
|
+
${BOLD}${mainMaxPm2Name}${RESET}
|
|
216
|
+
|
|
217
|
+
${CYAN}•${RESET} Reminder:
|
|
218
|
+
Create a separate Telegram bot for the watchdog via ${BOLD}@BotFather${RESET}
|
|
219
|
+
|
|
220
|
+
${GREEN} ✓ Using ${modelLabel}${RESET}
|
|
221
|
+
`);
|
|
222
|
+
}
|
|
223
|
+
async function main() {
|
|
224
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
225
|
+
if (MAX_PROFILE && IS_WATCHDOG) {
|
|
226
|
+
await runWatchdogSetup(rl);
|
|
227
|
+
rl.close();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
console.log(`
|
|
231
|
+
${BOLD}╔══════════════════════════════════════════╗
|
|
232
|
+
║ 🤖 Max Setup ║
|
|
233
|
+
╚══════════════════════════════════════════╝${RESET}
|
|
234
|
+
`);
|
|
235
|
+
console.log(`${DIM}Config directory: ${MAX_HOME}${RESET}\n`);
|
|
236
|
+
ensureMaxHome();
|
|
237
|
+
// Load existing values if any
|
|
238
|
+
const existing = {};
|
|
239
|
+
if (existsSync(ENV_PATH)) {
|
|
240
|
+
for (const line of readFileSync(ENV_PATH, "utf-8").split("\n")) {
|
|
241
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/);
|
|
242
|
+
if (match)
|
|
243
|
+
existing[match[1]] = match[2];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// ── What is Max ──────────────────────────────────────────
|
|
247
|
+
console.log(`${BOLD}Meet Max${RESET}`);
|
|
248
|
+
console.log(`Max is your personal AI assistant — an always-on daemon that runs on`);
|
|
249
|
+
console.log(`your machine. Talk to him in plain English and he'll handle the rest.`);
|
|
250
|
+
console.log();
|
|
251
|
+
console.log(`${CYAN}What Max can do out of the box:${RESET}`);
|
|
252
|
+
console.log(` • Have conversations and answer questions`);
|
|
253
|
+
console.log(` • Spin up Copilot CLI sessions to code, debug, and run commands`);
|
|
254
|
+
console.log(` • Manage multiple background tasks simultaneously`);
|
|
255
|
+
console.log(` • See and attach to any Copilot session on your machine`);
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(`${CYAN}Skills — teach Max anything:${RESET}`);
|
|
258
|
+
console.log(` Max has a skill system that lets him learn new capabilities. There's`);
|
|
259
|
+
console.log(` an open source library of community skills he can install, or he can`);
|
|
260
|
+
console.log(` write his own from scratch. Just ask him:`);
|
|
261
|
+
console.log();
|
|
262
|
+
console.log(` ${DIM}"Check my email"${RESET} → Max researches how, writes a skill, does it`);
|
|
263
|
+
console.log(` ${DIM}"Turn off the lights"${RESET} → Max finds the right CLI tool, learns it`);
|
|
264
|
+
console.log(` ${DIM}"Find me a skill for"${RESET} → Max searches community skills and installs one`);
|
|
265
|
+
console.log(` ${DIM}"Learn how to use X"${RESET} → Max proactively learns before you need it`);
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(` Skills are saved permanently — Max only needs to learn once.`);
|
|
268
|
+
console.log();
|
|
269
|
+
console.log(`${CYAN}How to talk to Max:${RESET}`);
|
|
270
|
+
console.log(` • ${BOLD}Terminal${RESET} — ${CYAN}max tui${RESET} — always available, no setup needed`);
|
|
271
|
+
console.log(` • ${BOLD}Telegram${RESET} — control Max from your phone (optional, set up next)`);
|
|
272
|
+
console.log(` • ${BOLD}Discord${RESET} — control Max from Discord (optional, set up next)`);
|
|
273
|
+
console.log();
|
|
274
|
+
await ask(rl, `${DIM}Press Enter to continue...${RESET}`);
|
|
275
|
+
console.log();
|
|
276
|
+
// ── Telegram Setup ───────────────────────────────────────
|
|
277
|
+
console.log(`${BOLD}━━━ Telegram Setup (optional) ━━━${RESET}\n`);
|
|
278
|
+
console.log(`Telegram lets you talk to Max from your phone — send messages,`);
|
|
279
|
+
console.log(`dispatch coding tasks, and get notified when background work finishes.`);
|
|
280
|
+
console.log();
|
|
281
|
+
let telegramToken = existing.TELEGRAM_BOT_TOKEN || "";
|
|
282
|
+
let userId = existing.AUTHORIZED_USER_ID || "";
|
|
283
|
+
const setupTelegram = await askYesNo(rl, "Would you like to set up Telegram?");
|
|
284
|
+
if (setupTelegram) {
|
|
285
|
+
// ── Step 1: Create bot ──
|
|
286
|
+
console.log(`\n${BOLD}Step 1: Create a Telegram bot${RESET}\n`);
|
|
287
|
+
console.log(` 1. Open Telegram and search for ${BOLD}@BotFather${RESET}`);
|
|
288
|
+
console.log(` 2. Send ${CYAN}/newbot${RESET} and follow the prompts`);
|
|
289
|
+
console.log(` 3. Copy the bot token (looks like ${DIM}123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11${RESET})`);
|
|
290
|
+
console.log();
|
|
291
|
+
const tokenInput = await askRequired(rl, ` Bot token${telegramToken ? ` ${DIM}(current: ${telegramToken.slice(0, 12)}...)${RESET}` : ""}: `);
|
|
292
|
+
telegramToken = tokenInput;
|
|
293
|
+
// ── Step 2: Lock it down ──
|
|
294
|
+
console.log(`\n${BOLD}Step 2: Lock down your bot${RESET}\n`);
|
|
295
|
+
console.log(`${YELLOW} ⚠ IMPORTANT: Your bot is currently open to anyone on Telegram.${RESET}`);
|
|
296
|
+
console.log(` Max uses your Telegram user ID to ensure only YOU can control it.`);
|
|
297
|
+
console.log(` Without this, anyone who finds your bot could send it commands.`);
|
|
298
|
+
console.log();
|
|
299
|
+
console.log(` To get your user ID:`);
|
|
300
|
+
console.log(` 1. Search for ${BOLD}@userinfobot${RESET} on Telegram`);
|
|
301
|
+
console.log(` 2. Send it any message`);
|
|
302
|
+
console.log(` 3. It will reply with your user ID (a number like ${DIM}123456789${RESET})`);
|
|
303
|
+
console.log();
|
|
304
|
+
// Require user ID — cannot proceed without it
|
|
305
|
+
while (true) {
|
|
306
|
+
const userIdInput = await askRequired(rl, ` Your user ID${userId ? ` ${DIM}(current: ${userId})${RESET}` : ""}: `);
|
|
307
|
+
const parsed = parseInt(userIdInput, 10);
|
|
308
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
309
|
+
userId = userIdInput;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
console.log(`${YELLOW} That doesn't look like a valid user ID. It should be a positive number.${RESET}`);
|
|
313
|
+
}
|
|
314
|
+
console.log(`\n${GREEN} ✓ Telegram locked down — only user ${userId} can control Max.${RESET}`);
|
|
315
|
+
// ── Step 3: Disable group joins ──
|
|
316
|
+
console.log(`\n${BOLD}Step 3: Disable group joins (recommended)${RESET}\n`);
|
|
317
|
+
console.log(` For extra security, prevent your bot from being added to groups:`);
|
|
318
|
+
console.log(` 1. Go back to ${BOLD}@BotFather${RESET}`);
|
|
319
|
+
console.log(` 2. Send ${CYAN}/mybots${RESET} → select your bot → ${CYAN}Bot Settings${RESET} → ${CYAN}Allow Groups?${RESET}`);
|
|
320
|
+
console.log(` 3. Set to ${BOLD}Disable${RESET}`);
|
|
321
|
+
console.log();
|
|
322
|
+
await ask(rl, ` ${DIM}Press Enter when done (or skip)...${RESET}`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.log(`\n${DIM} Skipping Telegram. You can always set it up later with: max setup${RESET}\n`);
|
|
326
|
+
}
|
|
327
|
+
// ── Discord Setup ────────────────────────────────────────
|
|
328
|
+
console.log(`${BOLD}━━━ Discord Setup (optional) ━━━${RESET}\n`);
|
|
329
|
+
console.log(`Discord lets you talk to Max from any Discord server — send messages,`);
|
|
330
|
+
console.log(`dispatch coding tasks, and get notified when background work finishes.`);
|
|
331
|
+
console.log();
|
|
332
|
+
let discordToken = existing.DISCORD_BOT_TOKEN || "";
|
|
333
|
+
let discordChannelIds = existing.DISCORD_ALLOWED_CHANNEL_IDS || "";
|
|
334
|
+
const setupDiscord = await askYesNo(rl, "Would you like to set up Discord?");
|
|
335
|
+
if (setupDiscord) {
|
|
336
|
+
// ── Step 1: Create bot ──
|
|
337
|
+
console.log(`\n${BOLD}Step 1: Create a Discord bot${RESET}\n`);
|
|
338
|
+
console.log(` 1. Go to ${CYAN}https://discord.com/developers/applications${RESET}`);
|
|
339
|
+
console.log(` 2. Click ${BOLD}New Application${RESET}, give it a name`);
|
|
340
|
+
console.log(` 3. Go to ${BOLD}Bot${RESET} → click ${BOLD}Reset Token${RESET} → copy the token`);
|
|
341
|
+
console.log(` 4. Under ${BOLD}Privileged Gateway Intents${RESET}, enable ${BOLD}Message Content Intent${RESET}`);
|
|
342
|
+
console.log();
|
|
343
|
+
const tokenInput = await askRequired(rl, ` Bot token${discordToken ? ` ${DIM}(current: ${discordToken.slice(0, 12)}...)${RESET}` : ""}: `);
|
|
344
|
+
discordToken = tokenInput;
|
|
345
|
+
// ── Step 2: Invite bot to server ──
|
|
346
|
+
console.log(`\n${BOLD}Step 2: Invite the bot to your server${RESET}\n`);
|
|
347
|
+
console.log(` 1. In the Developer Portal, go to ${BOLD}OAuth2${RESET}`);
|
|
348
|
+
console.log(` 2. Under ${BOLD}OAuth2 URL Generator${RESET}:`);
|
|
349
|
+
console.log(` - Scopes: ${CYAN}bot${RESET}, ${CYAN}applications.commands${RESET}`);
|
|
350
|
+
console.log(` - Bot Permissions: ${CYAN}Send Messages${RESET}, ${CYAN}Read Message History${RESET}, ${CYAN}Attach Files${RESET}, ${CYAN}Use Slash Commands${RESET}`);
|
|
351
|
+
console.log(` 3. Copy the generated URL and open it in your browser`);
|
|
352
|
+
console.log(` 4. Select your server and authorize`);
|
|
353
|
+
console.log();
|
|
354
|
+
await ask(rl, ` ${DIM}Press Enter when done...${RESET}`);
|
|
355
|
+
// ── Step 3: Lock down to specific channels ──
|
|
356
|
+
console.log(`\n${BOLD}Step 3: Restrict Max to specific channels${RESET}\n`);
|
|
357
|
+
console.log(`${YELLOW} ⚠ IMPORTANT: Max will only respond in channels you allow.${RESET}`);
|
|
358
|
+
console.log(` This is how you control who can talk to Max.`);
|
|
359
|
+
console.log();
|
|
360
|
+
console.log(` To get a channel ID:`);
|
|
361
|
+
console.log(` 1. Enable Developer Mode in Discord (Settings → Advanced → Developer Mode)`);
|
|
362
|
+
console.log(` 2. Right-click a text channel → ${BOLD}Copy Channel ID${RESET}`);
|
|
363
|
+
console.log();
|
|
364
|
+
const channelIdsInput = await askRequired(rl, ` Allowed channel IDs ${DIM}(comma-separated)${RESET}${discordChannelIds ? ` ${DIM}(current: ${discordChannelIds})${RESET}` : ""}: `);
|
|
365
|
+
discordChannelIds = channelIdsInput;
|
|
366
|
+
console.log(`\n${GREEN} ✓ Discord configured — Max will only respond in the specified channels.${RESET}`);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.log(`\n${DIM} Skipping Discord. You can always set it up later with: max setup${RESET}\n`);
|
|
370
|
+
}
|
|
371
|
+
// ── Google (gogcli) Setup ─────────────────────────────────
|
|
372
|
+
console.log(`${BOLD}━━━ Google / Gmail Setup (optional) ━━━${RESET}\n`);
|
|
373
|
+
console.log(`Max includes a Google skill that lets him read your email, manage`);
|
|
374
|
+
console.log(`your calendar, access Drive, and more — using the ${BOLD}gog${RESET} CLI.`);
|
|
375
|
+
console.log();
|
|
376
|
+
const setupGoogle = await askYesNo(rl, "Would you like to set up Google services?");
|
|
377
|
+
if (setupGoogle) {
|
|
378
|
+
// ── Step 1: Install gog CLI ──
|
|
379
|
+
console.log(`\n${BOLD}Step 1: Install the gog CLI${RESET}\n`);
|
|
380
|
+
console.log(` ${CYAN}brew install steipete/tap/gogcli${RESET} ${DIM}(macOS/Linux with Homebrew)${RESET}`);
|
|
381
|
+
console.log();
|
|
382
|
+
await ask(rl, ` ${DIM}Press Enter when installed (or to skip)...${RESET}`);
|
|
383
|
+
// ── Step 2: Create OAuth credentials ──
|
|
384
|
+
console.log(`\n${BOLD}Step 2: Create OAuth credentials${RESET}\n`);
|
|
385
|
+
console.log(` You need a Google Cloud OAuth client to authenticate:`);
|
|
386
|
+
console.log(` 1. Go to ${CYAN}https://console.cloud.google.com/apis/credentials${RESET}`);
|
|
387
|
+
console.log(` 2. Create a project (if you don't have one)`);
|
|
388
|
+
console.log(` 3. Enable the APIs you want (Gmail, Calendar, Drive, etc.)`);
|
|
389
|
+
console.log(` 4. Configure the OAuth consent screen`);
|
|
390
|
+
console.log(` 5. Create an OAuth client (type: ${BOLD}Desktop app${RESET})`);
|
|
391
|
+
console.log(` 6. Download the JSON credentials file`);
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(` Then store the credentials:`);
|
|
394
|
+
console.log(` ${CYAN}gog auth credentials ~/Downloads/client_secret_....json${RESET}`);
|
|
395
|
+
console.log();
|
|
396
|
+
await ask(rl, ` ${DIM}Press Enter when done (or to skip)...${RESET}`);
|
|
397
|
+
// ── Step 3: Authenticate ──
|
|
398
|
+
console.log(`\n${BOLD}Step 3: Authenticate with your Google account${RESET}\n`);
|
|
399
|
+
console.log(` Run this command to authorize:`);
|
|
400
|
+
console.log(` ${CYAN}gog auth add your-email@gmail.com${RESET}`);
|
|
401
|
+
console.log();
|
|
402
|
+
console.log(` This opens a browser for OAuth authorization. Once done, Max can`);
|
|
403
|
+
console.log(` access your Google services on your behalf.`);
|
|
404
|
+
console.log();
|
|
405
|
+
const googleEmail = await ask(rl, ` Google email ${DIM}(Enter to skip)${RESET}: `);
|
|
406
|
+
if (googleEmail.trim()) {
|
|
407
|
+
console.log(`\n ${DIM}Run this now or later:${RESET} ${CYAN}gog auth add ${googleEmail.trim()}${RESET}`);
|
|
408
|
+
console.log(` ${DIM}Check status anytime:${RESET} ${CYAN}gog auth status${RESET}`);
|
|
409
|
+
}
|
|
410
|
+
console.log(`\n${GREEN} ✓ Google skill is ready — authenticate with gog auth add when you're set.${RESET}\n`);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
console.log(`\n${DIM} Skipping Google. You can always set it up later with: max setup${RESET}\n`);
|
|
414
|
+
}
|
|
415
|
+
// ── Model picker ─────────────────────────────────────────
|
|
416
|
+
console.log(`\n${BOLD}━━━ Default Model ━━━${RESET}\n`);
|
|
417
|
+
console.log(`${DIM}Fetching available models from Copilot...${RESET}`);
|
|
418
|
+
let models = await fetchModels();
|
|
419
|
+
if (models.length === 0) {
|
|
420
|
+
console.log(`${YELLOW} Could not fetch models (Copilot CLI may not be authenticated yet).${RESET}`);
|
|
421
|
+
console.log(`${DIM} Showing a curated list — you can switch anytime after setup.${RESET}\n`);
|
|
422
|
+
models = FALLBACK_MODELS;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
console.log(`${GREEN} ✓ Found ${models.length} models${RESET}\n`);
|
|
426
|
+
}
|
|
427
|
+
console.log(`${DIM}You can switch models anytime by telling Max "switch to gpt-4.1"${RESET}\n`);
|
|
428
|
+
const currentModel = existing.COPILOT_MODEL || "claude-sonnet-4.6";
|
|
429
|
+
const model = await askPicker(rl, "Choose a default model:", models, currentModel);
|
|
430
|
+
const modelLabel = models.find((m) => m.id === model)?.label || model;
|
|
431
|
+
console.log(`\n${GREEN} ✓ Using ${modelLabel}${RESET}\n`);
|
|
432
|
+
// ── Write config ─────────────────────────────────────────
|
|
433
|
+
const apiPort = existing.API_PORT || "7777";
|
|
434
|
+
const lines = [];
|
|
435
|
+
if (telegramToken)
|
|
436
|
+
lines.push(`TELEGRAM_BOT_TOKEN=${telegramToken}`);
|
|
437
|
+
if (userId)
|
|
438
|
+
lines.push(`AUTHORIZED_USER_ID=${userId}`);
|
|
439
|
+
if (discordToken)
|
|
440
|
+
lines.push(`DISCORD_BOT_TOKEN=${discordToken}`);
|
|
441
|
+
if (discordChannelIds)
|
|
442
|
+
lines.push(`DISCORD_ALLOWED_CHANNEL_IDS=${discordChannelIds}`);
|
|
443
|
+
lines.push(`API_PORT=${apiPort}`);
|
|
444
|
+
lines.push(`COPILOT_MODEL=${model}`);
|
|
445
|
+
writeFileSync(ENV_PATH, lines.join("\n") + "\n");
|
|
446
|
+
// ── Done ─────────────────────────────────────────────────
|
|
447
|
+
console.log(`
|
|
448
|
+
${GREEN}${BOLD}✅ Max is ready!${RESET}
|
|
449
|
+
${DIM}Config saved to ${ENV_PATH}${RESET}
|
|
450
|
+
|
|
451
|
+
${BOLD}Get started:${RESET}
|
|
452
|
+
|
|
453
|
+
${CYAN}1.${RESET} Make sure Copilot CLI is authenticated:
|
|
454
|
+
${BOLD}copilot login${RESET}
|
|
455
|
+
|
|
456
|
+
${CYAN}2.${RESET} Start Max:
|
|
457
|
+
${BOLD}max start${RESET}
|
|
458
|
+
|
|
459
|
+
${CYAN}3.${RESET} ${setupTelegram ? "Open Telegram and message your bot!" : "Connect via terminal:"}
|
|
460
|
+
${BOLD}${setupTelegram ? "(message your bot on Telegram)" : "max tui"}${RESET}
|
|
461
|
+
|
|
462
|
+
${BOLD}Things to try:${RESET}
|
|
463
|
+
|
|
464
|
+
${DIM}"Start working on the auth bug in ~/dev/myapp"${RESET}
|
|
465
|
+
${DIM}"What sessions are running?"${RESET}
|
|
466
|
+
${DIM}"Find me a skill for checking Gmail"${RESET}
|
|
467
|
+
${DIM}"Learn how to control my smart lights"${RESET}
|
|
468
|
+
${DIM}"Switch to gpt-4.1"${RESET}
|
|
469
|
+
`);
|
|
470
|
+
rl.close();
|
|
471
|
+
}
|
|
472
|
+
main().catch((err) => {
|
|
473
|
+
console.error("Setup failed:", err);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
});
|
|
476
|
+
//# sourceMappingURL=setup.js.map
|
package/dist/store/db.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { DB_PATH, ensureMaxHome } from "../paths.js";
|
|
3
|
+
let db;
|
|
4
|
+
let logInsertCount = 0;
|
|
5
|
+
export function getDb() {
|
|
6
|
+
if (!db) {
|
|
7
|
+
ensureMaxHome();
|
|
8
|
+
db = new Database(DB_PATH);
|
|
9
|
+
db.pragma("journal_mode = WAL");
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS worker_sessions (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
name TEXT UNIQUE NOT NULL,
|
|
14
|
+
copilot_session_id TEXT,
|
|
15
|
+
working_dir TEXT NOT NULL,
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
17
|
+
last_output TEXT,
|
|
18
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
19
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
20
|
+
)
|
|
21
|
+
`);
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS max_state (
|
|
24
|
+
key TEXT PRIMARY KEY,
|
|
25
|
+
value TEXT NOT NULL
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS conversation_log (
|
|
30
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
|
32
|
+
content TEXT NOT NULL,
|
|
33
|
+
source TEXT NOT NULL DEFAULT 'unknown',
|
|
34
|
+
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
35
|
+
)
|
|
36
|
+
`);
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
category TEXT NOT NULL CHECK(category IN ('preference', 'fact', 'project', 'person', 'routine')),
|
|
41
|
+
content TEXT NOT NULL,
|
|
42
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
43
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
44
|
+
last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
45
|
+
)
|
|
46
|
+
`);
|
|
47
|
+
// Migrate: if the table already existed with a stricter CHECK, recreate it
|
|
48
|
+
try {
|
|
49
|
+
db.prepare(`INSERT INTO conversation_log (role, content, source) VALUES ('system', '__migration_test__', 'test')`).run();
|
|
50
|
+
db.prepare(`DELETE FROM conversation_log WHERE content = '__migration_test__'`).run();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// CHECK constraint doesn't allow 'system' — recreate table preserving data
|
|
54
|
+
db.exec(`ALTER TABLE conversation_log RENAME TO conversation_log_old`);
|
|
55
|
+
db.exec(`
|
|
56
|
+
CREATE TABLE conversation_log (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
|
59
|
+
content TEXT NOT NULL,
|
|
60
|
+
source TEXT NOT NULL DEFAULT 'unknown',
|
|
61
|
+
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
db.exec(`INSERT INTO conversation_log (role, content, source, ts) SELECT role, content, source, ts FROM conversation_log_old`);
|
|
65
|
+
db.exec(`DROP TABLE conversation_log_old`);
|
|
66
|
+
}
|
|
67
|
+
// Prune conversation log at startup
|
|
68
|
+
db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`).run();
|
|
69
|
+
}
|
|
70
|
+
return db;
|
|
71
|
+
}
|
|
72
|
+
export function getState(key) {
|
|
73
|
+
const db = getDb();
|
|
74
|
+
const row = db.prepare(`SELECT value FROM max_state WHERE key = ?`).get(key);
|
|
75
|
+
return row?.value;
|
|
76
|
+
}
|
|
77
|
+
export function setState(key, value) {
|
|
78
|
+
const db = getDb();
|
|
79
|
+
db.prepare(`INSERT OR REPLACE INTO max_state (key, value) VALUES (?, ?)`).run(key, value);
|
|
80
|
+
}
|
|
81
|
+
/** Remove a key from persistent state. */
|
|
82
|
+
export function deleteState(key) {
|
|
83
|
+
const db = getDb();
|
|
84
|
+
db.prepare(`DELETE FROM max_state WHERE key = ?`).run(key);
|
|
85
|
+
}
|
|
86
|
+
/** Log a conversation turn (user, assistant, or system). */
|
|
87
|
+
export function logConversation(role, content, source) {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
db.prepare(`INSERT INTO conversation_log (role, content, source) VALUES (?, ?, ?)`).run(role, content, source);
|
|
90
|
+
// Keep last 200 entries to support context recovery after session loss
|
|
91
|
+
logInsertCount++;
|
|
92
|
+
if (logInsertCount % 50 === 0) {
|
|
93
|
+
db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`).run();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Get recent conversation history formatted for injection into system message. */
|
|
97
|
+
export function getRecentConversation(limit = 20) {
|
|
98
|
+
const db = getDb();
|
|
99
|
+
const rows = db.prepare(`SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?`).all(limit);
|
|
100
|
+
if (rows.length === 0)
|
|
101
|
+
return "";
|
|
102
|
+
// Reverse so oldest is first (chronological order)
|
|
103
|
+
rows.reverse();
|
|
104
|
+
return rows.map((r) => {
|
|
105
|
+
const tag = r.role === "user" ? `[${r.source}] User`
|
|
106
|
+
: r.role === "system" ? `[${r.source}] System`
|
|
107
|
+
: "Max";
|
|
108
|
+
// Truncate long messages to keep context manageable
|
|
109
|
+
const content = r.content.length > 500 ? r.content.slice(0, 500) + "…" : r.content;
|
|
110
|
+
return `${tag}: ${content}`;
|
|
111
|
+
}).join("\n\n");
|
|
112
|
+
}
|
|
113
|
+
/** Add a memory to long-term storage. */
|
|
114
|
+
export function addMemory(category, content, source = "user") {
|
|
115
|
+
const db = getDb();
|
|
116
|
+
const result = db.prepare(`INSERT INTO memories (category, content, source) VALUES (?, ?, ?)`).run(category, content, source);
|
|
117
|
+
return result.lastInsertRowid;
|
|
118
|
+
}
|
|
119
|
+
/** Search memories by keyword and/or category. */
|
|
120
|
+
export function searchMemories(keyword, category, limit = 20) {
|
|
121
|
+
const db = getDb();
|
|
122
|
+
const conditions = [];
|
|
123
|
+
const params = [];
|
|
124
|
+
if (keyword) {
|
|
125
|
+
conditions.push(`content LIKE ?`);
|
|
126
|
+
params.push(`%${keyword}%`);
|
|
127
|
+
}
|
|
128
|
+
if (category) {
|
|
129
|
+
conditions.push(`category = ?`);
|
|
130
|
+
params.push(category);
|
|
131
|
+
}
|
|
132
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
133
|
+
params.push(limit);
|
|
134
|
+
const rows = db.prepare(`SELECT id, category, content, source, created_at FROM memories ${where} ORDER BY last_accessed DESC LIMIT ?`).all(...params);
|
|
135
|
+
// Update last_accessed for returned memories
|
|
136
|
+
if (rows.length > 0) {
|
|
137
|
+
const placeholders = rows.map(() => "?").join(",");
|
|
138
|
+
db.prepare(`UPDATE memories SET last_accessed = CURRENT_TIMESTAMP WHERE id IN (${placeholders})`).run(...rows.map((r) => r.id));
|
|
139
|
+
}
|
|
140
|
+
return rows;
|
|
141
|
+
}
|
|
142
|
+
/** Remove a memory by ID. */
|
|
143
|
+
export function removeMemory(id) {
|
|
144
|
+
const db = getDb();
|
|
145
|
+
const result = db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
|
146
|
+
return result.changes > 0;
|
|
147
|
+
}
|
|
148
|
+
/** Get a compact summary of all memories for injection into system message. */
|
|
149
|
+
export function getMemorySummary() {
|
|
150
|
+
const db = getDb();
|
|
151
|
+
const rows = db.prepare(`SELECT id, category, content FROM memories ORDER BY category, last_accessed DESC`).all();
|
|
152
|
+
if (rows.length === 0)
|
|
153
|
+
return "";
|
|
154
|
+
// Group by category
|
|
155
|
+
const grouped = {};
|
|
156
|
+
for (const r of rows) {
|
|
157
|
+
if (!grouped[r.category])
|
|
158
|
+
grouped[r.category] = [];
|
|
159
|
+
grouped[r.category].push({ id: r.id, content: r.content });
|
|
160
|
+
}
|
|
161
|
+
const sections = Object.entries(grouped).map(([cat, items]) => {
|
|
162
|
+
const lines = items.map((i) => ` - [#${i.id}] ${i.content}`).join("\n");
|
|
163
|
+
return `**${cat}**:\n${lines}`;
|
|
164
|
+
});
|
|
165
|
+
return sections.join("\n");
|
|
166
|
+
}
|
|
167
|
+
export function closeDb() {
|
|
168
|
+
if (db) {
|
|
169
|
+
db.close();
|
|
170
|
+
db = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=db.js.map
|