peely 0.9.4 → 0.9.5
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/README.md +5 -1
- package/cli.js +50 -163
- package/package.json +4 -1
- package/src/ai/index.js +8 -8
- package/src/ai/providers/copilot.js +21 -29
- package/src/ai/providers/ollama.js +2 -3
- package/src/ai/providers/openai.js +15 -6
- package/src/daemon/client.js +7 -0
- package/src/daemon/server.js +4 -4
- package/src/interfaces/create_interface.js +7 -3
- package/src/interfaces/discord/index.js +17 -4
- package/src/interfaces/terminal/commands.js +401 -0
- package/src/interfaces/terminal/index.js +25 -155
- package/src/plugins/index.js +12 -1
- package/src/plugins/plugins/create_plugin.js +44 -0
- package/src/plugins/plugins/math.js +4 -1
- package/src/utils/autoupdateInfo.js +12 -1
- package/src/utils/config.js +9 -3
- package/src/utils/events.js +2 -2
- package/src/utils/settings.js +1 -0
|
@@ -21,11 +21,17 @@ const getDb = async () => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
// ── Per-user conversation memory (persistent) ──
|
|
24
|
-
// In-memory cache backed by disk
|
|
24
|
+
// In-memory cache backed by disk, with LRU eviction
|
|
25
|
+
const MAX_CACHED_CONVERSATIONS = 100;
|
|
25
26
|
const conversations = new Map();
|
|
26
27
|
|
|
27
28
|
const getConversation = (userId) => {
|
|
28
29
|
if (!conversations.has(userId)) {
|
|
30
|
+
// Evict oldest entry if cache is full
|
|
31
|
+
if (conversations.size >= MAX_CACHED_CONVERSATIONS) {
|
|
32
|
+
const oldest = conversations.keys().next().value;
|
|
33
|
+
conversations.delete(oldest);
|
|
34
|
+
}
|
|
29
35
|
// Load from disk on first access
|
|
30
36
|
conversations.set(userId, memory.load(`discord-${userId}`));
|
|
31
37
|
}
|
|
@@ -141,9 +147,10 @@ client.on(Events.InteractionCreate, async (interaction) => {
|
|
|
141
147
|
if (commandName === "pair") {
|
|
142
148
|
const database = await getDb();
|
|
143
149
|
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
144
|
-
|
|
150
|
+
// Store with timestamp so expiry survives process restarts
|
|
151
|
+
await database.set(`pairCode_${code}`, { userId: user.id, createdAt: Date.now() });
|
|
145
152
|
|
|
146
|
-
//
|
|
153
|
+
// Best-effort in-process cleanup after 5 minutes
|
|
147
154
|
setTimeout(async () => {
|
|
148
155
|
await database.delete(`pairCode_${code}`).catch(() => {});
|
|
149
156
|
}, 5 * 60 * 1000);
|
|
@@ -229,9 +236,15 @@ client.on(Events.InteractionCreate, async (interaction) => {
|
|
|
229
236
|
}
|
|
230
237
|
});
|
|
231
238
|
|
|
232
|
-
// ── Message handler (
|
|
239
|
+
// ── Message handler (DMs and mentions only) ──
|
|
233
240
|
client.on(Events.MessageCreate, async (message) => {
|
|
234
241
|
if (message.author.bot) return;
|
|
242
|
+
|
|
243
|
+
// Only respond in DMs or when the bot is mentioned
|
|
244
|
+
const isDM = !message.guild;
|
|
245
|
+
const isMentioned = message.mentions.has(client.user);
|
|
246
|
+
if (!isDM && !isMentioned) return;
|
|
247
|
+
|
|
235
248
|
console.log(chalk.dim(` [msg] ${message.author.tag}: ${message.content.slice(0, 50)}`));
|
|
236
249
|
|
|
237
250
|
let content = message.content.replace(/<@!?\d+>/g, "").trim();
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command & action helpers used by both cli.js (via terminal re-export)
|
|
3
|
+
* and the interactive REPL. Everything lives here so there is a single
|
|
4
|
+
* source of truth — no separate "shared" directory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require("chalk");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const config = require("../../utils/config");
|
|
11
|
+
const PATHS = require("../../utils/paths");
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
14
|
+
// Branding
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
const logo = `
|
|
18
|
+
${chalk.magenta("╔══════════════════════════════╗")}
|
|
19
|
+
${chalk.magenta("║")} ${chalk.bold.white("🍌 peely")} ${chalk.dim("— your AI assistant")} ${chalk.magenta("║")}
|
|
20
|
+
${chalk.magenta("╚══════════════════════════════╝")}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const interactiveLogo = (() => {
|
|
24
|
+
try {
|
|
25
|
+
const p = path.join(__dirname, "..", "..", "assets", "ascii-logo.txt");
|
|
26
|
+
const art = fs.readFileSync(p, "utf8");
|
|
27
|
+
return (
|
|
28
|
+
art
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((line) => (line.trim() === "" ? "" : chalk.magenta(" " + line)))
|
|
31
|
+
.join("\n") +
|
|
32
|
+
"\n\n" +
|
|
33
|
+
chalk.bold.white(" 🍌 peely") +
|
|
34
|
+
" " +
|
|
35
|
+
chalk.dim("— interactive mode") +
|
|
36
|
+
"\n"
|
|
37
|
+
);
|
|
38
|
+
} catch (_) {
|
|
39
|
+
return logo;
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
44
|
+
// Status
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
const getStatusInfo = () => ({
|
|
48
|
+
model: config.get("ai.model") || null,
|
|
49
|
+
discordConfigured: !!config.get("interfaces.discord.token"),
|
|
50
|
+
githubConfigured: !!config.get("github.token"),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Print a formatted status block to stdout.
|
|
55
|
+
* @param {{ messageCount?: number, background?: string }} extra
|
|
56
|
+
*/
|
|
57
|
+
const printStatus = (extra = {}) => {
|
|
58
|
+
const info = getStatusInfo();
|
|
59
|
+
const model = info.model || chalk.dim("not set");
|
|
60
|
+
const discord = info.discordConfigured
|
|
61
|
+
? chalk.green("configured")
|
|
62
|
+
: chalk.red("not set");
|
|
63
|
+
const github = info.githubConfigured
|
|
64
|
+
? chalk.green("authorized")
|
|
65
|
+
: chalk.red("not set");
|
|
66
|
+
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.bold(" Status:"));
|
|
69
|
+
console.log(` AI Model: ${model}`);
|
|
70
|
+
console.log(` GitHub: ${github}`);
|
|
71
|
+
console.log(` Discord: ${discord}`);
|
|
72
|
+
if (typeof extra.messageCount === "number") {
|
|
73
|
+
console.log(` Messages: ${extra.messageCount}`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof extra.background === "string") {
|
|
76
|
+
console.log(` Background: ${extra.background}`);
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
82
|
+
// Discord pairing
|
|
83
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} code — 6-char pair code
|
|
87
|
+
* @returns {{ ok: boolean, userId?: string, error?: string }}
|
|
88
|
+
*/
|
|
89
|
+
const pairDiscord = async (code) => {
|
|
90
|
+
if (!code) return { ok: false, error: "Usage: peely pair discord <code>" };
|
|
91
|
+
|
|
92
|
+
const { SqliteDriver, QuickDB } = require("quick.db");
|
|
93
|
+
const db = new QuickDB({ driver: new SqliteDriver(PATHS.quickDb) });
|
|
94
|
+
const record = await db.get(`pairCode_${code.toUpperCase()}`);
|
|
95
|
+
|
|
96
|
+
let userId;
|
|
97
|
+
if (record && typeof record === "object" && record.userId) {
|
|
98
|
+
if (record.createdAt && Date.now() - record.createdAt > 5 * 60 * 1000) {
|
|
99
|
+
await db.delete(`pairCode_${code.toUpperCase()}`);
|
|
100
|
+
return { ok: false, error: `Invalid or expired pair code: ${code}` };
|
|
101
|
+
}
|
|
102
|
+
userId = record.userId;
|
|
103
|
+
} else if (typeof record === "string") {
|
|
104
|
+
userId = record;
|
|
105
|
+
} else {
|
|
106
|
+
return { ok: false, error: `Invalid or expired pair code: ${code}` };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await db.set(`paired_${userId}`, true);
|
|
110
|
+
await db.delete(`pairCode_${code.toUpperCase()}`);
|
|
111
|
+
config.set("interfaces.discord.pairedUsers", [
|
|
112
|
+
...(config.get("interfaces.discord.pairedUsers") || []),
|
|
113
|
+
userId,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
return { ok: true, userId };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
120
|
+
// PID helpers
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} pidFile — absolute path
|
|
125
|
+
* @returns {{ alive: boolean, pid: string|null }}
|
|
126
|
+
*/
|
|
127
|
+
const checkPidFile = (pidFile) => {
|
|
128
|
+
if (!fs.existsSync(pidFile)) return { alive: false, pid: null };
|
|
129
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
130
|
+
try {
|
|
131
|
+
process.kill(Number(pid), 0);
|
|
132
|
+
return { alive: true, pid };
|
|
133
|
+
} catch (_) {
|
|
134
|
+
return { alive: false, pid };
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
139
|
+
// Command handlers (AI, settings, interfaces …)
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
141
|
+
|
|
142
|
+
const chooseModel = async () => {
|
|
143
|
+
const ai = require("../../ai");
|
|
144
|
+
await ai.chooseModel();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const openSettings = async () => {
|
|
148
|
+
const { settingsMenu } = require("../../utils/settings");
|
|
149
|
+
await settingsMenu();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Prompt for a Discord Bot Token and save it.
|
|
154
|
+
* @returns {{ ok: boolean }}
|
|
155
|
+
*/
|
|
156
|
+
const setupDiscordToken = async () => {
|
|
157
|
+
const { text, isCancel } = require("@clack/prompts");
|
|
158
|
+
const token = await text({ message: "Enter your Discord Bot Token:" });
|
|
159
|
+
if (!isCancel(token) && token && token.trim()) {
|
|
160
|
+
config.set("interfaces.discord.token", token.trim());
|
|
161
|
+
return { ok: true };
|
|
162
|
+
}
|
|
163
|
+
return { ok: false };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ── Timers ────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
const getTimers = () => {
|
|
169
|
+
const { events } = require("../../utils/events");
|
|
170
|
+
return events.listScheduled();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const printTimers = () => {
|
|
174
|
+
const scheduled = getTimers();
|
|
175
|
+
console.log();
|
|
176
|
+
if (scheduled.length === 0) {
|
|
177
|
+
console.log(chalk.dim(" No active timers."));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(chalk.bold(" Active timers:"));
|
|
180
|
+
for (const t of scheduled) {
|
|
181
|
+
const secs = Math.ceil(t.remainingMs / 1000);
|
|
182
|
+
const desc = t.meta?.task || t.meta?.message || t.id;
|
|
183
|
+
console.log(` ⏱️ ${t.id} — ${secs}s left — ${desc}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// ── Plugins ───────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const getPlugins = () => {
|
|
192
|
+
const pluginModule = require("../../plugins");
|
|
193
|
+
return pluginModule.plugins;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const printPlugins = () => {
|
|
197
|
+
const plugins = getPlugins();
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(chalk.bold(" Loaded plugins:"));
|
|
200
|
+
for (const p of plugins) {
|
|
201
|
+
const toolCount = p.tools ? Object.keys(p.tools).length : 0;
|
|
202
|
+
console.log(` • ${chalk.cyan(p.name)} — ${toolCount} tool(s) — ${p.description || ""}`);
|
|
203
|
+
}
|
|
204
|
+
console.log();
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// ── Interfaces ────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
const getInterfaces = () => {
|
|
210
|
+
const { listInterfaces } = require("../create_interface");
|
|
211
|
+
return listInterfaces();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const printInterfaces = () => {
|
|
215
|
+
const all = getInterfaces();
|
|
216
|
+
console.log();
|
|
217
|
+
console.log(chalk.bold(" Interfaces:"));
|
|
218
|
+
for (const iface of all) {
|
|
219
|
+
const tag =
|
|
220
|
+
iface.type === "built-in"
|
|
221
|
+
? chalk.dim("[built-in]")
|
|
222
|
+
: chalk.cyan("[custom]");
|
|
223
|
+
console.log(` ${tag} ${chalk.bold(iface.name)} — ${iface.description}`);
|
|
224
|
+
}
|
|
225
|
+
console.log();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const createInterface = async () => {
|
|
229
|
+
const creator = require("../create_interface");
|
|
230
|
+
await creator.createInterface();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const deleteInterface = (name) => {
|
|
234
|
+
const { deleteInterface: del } = require("../create_interface");
|
|
235
|
+
return del(name);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const startInterface = async (name) => {
|
|
239
|
+
const interfaces = require("../");
|
|
240
|
+
const { loadCustomInterface } = require("../create_interface");
|
|
241
|
+
const mod = interfaces[name] || loadCustomInterface(name);
|
|
242
|
+
if (!mod || typeof mod.start !== "function") return false;
|
|
243
|
+
await mod.start();
|
|
244
|
+
return true;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Interactive interface management menu (@clack).
|
|
249
|
+
*/
|
|
250
|
+
const interfaceMenu = async () => {
|
|
251
|
+
const { select, isCancel, log: cLog } = require("@clack/prompts");
|
|
252
|
+
|
|
253
|
+
printInterfaces();
|
|
254
|
+
|
|
255
|
+
const all = getInterfaces();
|
|
256
|
+
const action = await select({
|
|
257
|
+
message: "What would you like to do?",
|
|
258
|
+
options: [
|
|
259
|
+
{ value: "create", label: "🔧 Create new interface" },
|
|
260
|
+
{
|
|
261
|
+
value: "delete",
|
|
262
|
+
label: "🗑️ Delete a custom interface",
|
|
263
|
+
hint:
|
|
264
|
+
all.filter((i) => i.type === "custom").length === 0
|
|
265
|
+
? "none yet"
|
|
266
|
+
: undefined,
|
|
267
|
+
},
|
|
268
|
+
{ value: "back", label: "← Back" },
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (isCancel(action) || action === "back") return;
|
|
273
|
+
|
|
274
|
+
if (action === "create") {
|
|
275
|
+
await createInterface();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (action === "delete") {
|
|
280
|
+
const customs = all.filter((i) => i.type === "custom");
|
|
281
|
+
if (customs.length === 0) {
|
|
282
|
+
cLog.warn("No custom interfaces to delete.");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const which = await select({
|
|
286
|
+
message: "Which interface to delete?",
|
|
287
|
+
options: [
|
|
288
|
+
...customs.map((i) => ({
|
|
289
|
+
value: i.name,
|
|
290
|
+
label: i.name,
|
|
291
|
+
hint: i.description,
|
|
292
|
+
})),
|
|
293
|
+
{ value: "back", label: "← Back" },
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
if (isCancel(which) || which === "back") return;
|
|
297
|
+
if (deleteInterface(which)) {
|
|
298
|
+
cLog.success(`Deleted "${which}".`);
|
|
299
|
+
} else {
|
|
300
|
+
cLog.error(`Interface "${which}" not found.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
306
|
+
// Help text
|
|
307
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
308
|
+
|
|
309
|
+
const CLI_COMMANDS = [
|
|
310
|
+
{ usage: "peely", desc: "Start interactive TUI" },
|
|
311
|
+
{ usage: "peely setup", desc: "First-time onboarding wizard" },
|
|
312
|
+
{ usage: "peely chat <message>", desc: "One-shot chat (connects to daemon)" },
|
|
313
|
+
{ usage: "peely daemon start", desc: "Start daemon in background" },
|
|
314
|
+
{ usage: "peely daemon stop", desc: "Stop daemon" },
|
|
315
|
+
{ usage: "peely daemon restart", desc: "Restart daemon (for updates)" },
|
|
316
|
+
{ usage: "peely daemon status", desc: "Show daemon status" },
|
|
317
|
+
{ usage: "peely start", desc: "Legacy: Run Discord bot in background" },
|
|
318
|
+
{ usage: "peely stop", desc: "Legacy: Stop background process" },
|
|
319
|
+
{ usage: "peely discord", desc: "Start Discord bot only" },
|
|
320
|
+
{ usage: "peely pair discord <code>", desc: "Pair Discord account" },
|
|
321
|
+
{ usage: "peely pair discord setup", desc: "Set Discord bot token" },
|
|
322
|
+
{ usage: "peely model", desc: "Choose AI model" },
|
|
323
|
+
{ usage: "peely settings", desc: "Edit config, tokens & API keys" },
|
|
324
|
+
{ usage: "peely interface create", desc: "Create a new custom interface" },
|
|
325
|
+
{ usage: "peely interface list", desc: "List all interfaces" },
|
|
326
|
+
{ usage: "peely interface start <name>", desc: "Start a custom interface" },
|
|
327
|
+
{ usage: "peely interface delete <name>", desc: "Delete a custom interface" },
|
|
328
|
+
{ usage: "peely status", desc: "Show config status" },
|
|
329
|
+
{ usage: "peely help", desc: "Show this help" },
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const SLASH_COMMANDS = [
|
|
333
|
+
{ usage: "/help", desc: "Show this help" },
|
|
334
|
+
{ usage: "/clear", desc: "Clear conversation history" },
|
|
335
|
+
{ usage: "/model", desc: "Switch AI model" },
|
|
336
|
+
{ usage: "/settings", desc: "Edit config, tokens & API keys" },
|
|
337
|
+
{ usage: "/timers", desc: "Show active timers" },
|
|
338
|
+
{ usage: "/plugins", desc: "List loaded plugins" },
|
|
339
|
+
{ usage: "/interfaces", desc: "List & create interfaces" },
|
|
340
|
+
{ usage: "/pair discord <code>", desc: "Pair a Discord account" },
|
|
341
|
+
{ usage: "/status", desc: "Show system status" },
|
|
342
|
+
{ usage: "/exit", desc: "Exit peely" },
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
const printCliHelp = (headerLogo) => {
|
|
346
|
+
console.log(headerLogo);
|
|
347
|
+
console.log(chalk.bold(" Usage:"));
|
|
348
|
+
for (const { usage, desc } of CLI_COMMANDS) {
|
|
349
|
+
console.log(` ${chalk.cyan(usage.padEnd(30))} ${desc}`);
|
|
350
|
+
}
|
|
351
|
+
console.log();
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const buildSlashHelp = () => {
|
|
355
|
+
const lines = [chalk.bold(" Commands:")];
|
|
356
|
+
for (const { usage, desc } of SLASH_COMMANDS) {
|
|
357
|
+
lines.push(` ${chalk.cyan(usage.padEnd(24))} ${desc}`);
|
|
358
|
+
}
|
|
359
|
+
return "\n" + lines.join("\n") + "\n";
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
363
|
+
// Exports
|
|
364
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
365
|
+
|
|
366
|
+
module.exports = {
|
|
367
|
+
// Branding
|
|
368
|
+
logo,
|
|
369
|
+
interactiveLogo,
|
|
370
|
+
|
|
371
|
+
// Status
|
|
372
|
+
getStatusInfo,
|
|
373
|
+
printStatus,
|
|
374
|
+
|
|
375
|
+
// Discord
|
|
376
|
+
pairDiscord,
|
|
377
|
+
|
|
378
|
+
// PID
|
|
379
|
+
checkPidFile,
|
|
380
|
+
|
|
381
|
+
// Commands
|
|
382
|
+
chooseModel,
|
|
383
|
+
openSettings,
|
|
384
|
+
setupDiscordToken,
|
|
385
|
+
getTimers,
|
|
386
|
+
printTimers,
|
|
387
|
+
getPlugins,
|
|
388
|
+
printPlugins,
|
|
389
|
+
getInterfaces,
|
|
390
|
+
printInterfaces,
|
|
391
|
+
createInterface,
|
|
392
|
+
deleteInterface,
|
|
393
|
+
startInterface,
|
|
394
|
+
interfaceMenu,
|
|
395
|
+
|
|
396
|
+
// Help
|
|
397
|
+
CLI_COMMANDS,
|
|
398
|
+
SLASH_COMMANDS,
|
|
399
|
+
printCliHelp,
|
|
400
|
+
buildSlashHelp,
|
|
401
|
+
};
|
|
@@ -3,44 +3,18 @@ const chalk = require("chalk");
|
|
|
3
3
|
const config = require("../../utils/config");
|
|
4
4
|
const ai = require("../../ai");
|
|
5
5
|
const memory = require("../../utils/memory");
|
|
6
|
-
const PATHS = require("../../utils/paths");
|
|
7
6
|
const ora = require("ora");
|
|
7
|
+
const cmds = require("./commands");
|
|
8
8
|
|
|
9
9
|
// ── State ── (loaded from disk)
|
|
10
10
|
const conversationHistory = memory.load("terminal");
|
|
11
11
|
let running = true;
|
|
12
12
|
|
|
13
13
|
// ── UI helpers ──
|
|
14
|
-
const LOGO =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const p = path.join(__dirname, "..", "..", "assets", "ascii-logo.txt");
|
|
19
|
-
const art = fs.readFileSync(p, "utf8");
|
|
20
|
-
return art
|
|
21
|
-
.split("\n")
|
|
22
|
-
.map((line) => (line.trim() === "" ? "" : chalk.magenta(" " + line)))
|
|
23
|
-
.join("\n")
|
|
24
|
-
+ "\n\n" + chalk.bold.white(" 🍌 peely") + " " + chalk.dim("— interactive mode") + "\n";
|
|
25
|
-
} catch (err) {
|
|
26
|
-
// Fallback banner
|
|
27
|
-
return `\n${chalk.magenta(" ____ _ _ _ ")}\n${chalk.magenta(" | _ \\ ___ __ _(_) (_) ___ ")}\n${chalk.magenta(" | |_) / _ \\/ _\\` | | | |/ _ \\ ")}\n${chalk.magenta(" | __/ __/ (_| | | | | (_) |")}\n${chalk.magenta(" |_| \\___|\\__, |_|_|_|\\___/ ")}\n${chalk.magenta(" |___/ ")}\n\n${chalk.bold.white(" 🍌 peely")} ${chalk.dim("— interactive mode")}\n`;
|
|
28
|
-
}
|
|
29
|
-
})();
|
|
30
|
-
|
|
31
|
-
const HELP_TEXT = `
|
|
32
|
-
${chalk.bold(" Commands:")}
|
|
33
|
-
${chalk.cyan("/help")} Show this help
|
|
34
|
-
${chalk.cyan("/clear")} Clear conversation history
|
|
35
|
-
${chalk.cyan("/model")} Switch AI model
|
|
36
|
-
${chalk.cyan("/settings")} Edit config, tokens & API keys
|
|
37
|
-
${chalk.cyan("/timers")} Show active timers
|
|
38
|
-
${chalk.cyan("/plugins")} List loaded plugins
|
|
39
|
-
${chalk.cyan("/interfaces")} List & create interfaces
|
|
40
|
-
${chalk.cyan("/pair discord <code>")} Pair a Discord account
|
|
41
|
-
${chalk.cyan("/status")} Show system status
|
|
42
|
-
${chalk.cyan("/exit")} Exit peely
|
|
43
|
-
`;
|
|
14
|
+
const LOGO = cmds.interactiveLogo;
|
|
15
|
+
const { printStatus, pairDiscord } = cmds;
|
|
16
|
+
|
|
17
|
+
const HELP_TEXT = cmds.buildSlashHelp();
|
|
44
18
|
|
|
45
19
|
const printSeparator = () => {
|
|
46
20
|
console.log(chalk.dim(" " + "─".repeat(50)));
|
|
@@ -84,56 +58,23 @@ const handleSlashCommand = async (input, rl) => {
|
|
|
84
58
|
return true;
|
|
85
59
|
|
|
86
60
|
case "model":
|
|
87
|
-
return { clack: true, run:
|
|
88
|
-
await ai.chooseModel();
|
|
89
|
-
}};
|
|
90
|
-
|
|
91
|
-
case "status": {
|
|
92
|
-
const model = config.get("ai.model") || chalk.dim("not set");
|
|
93
|
-
const discord = config.get("interfaces.discord.token")
|
|
94
|
-
? chalk.green("configured")
|
|
95
|
-
: chalk.red("not set");
|
|
96
|
-
const github = config.get("github.token")
|
|
97
|
-
? chalk.green("authorized")
|
|
98
|
-
: chalk.red("not set");
|
|
99
|
-
const msgs = conversationHistory.length;
|
|
61
|
+
return { clack: true, run: () => cmds.chooseModel() };
|
|
100
62
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(` AI Model: ${model}`);
|
|
104
|
-
console.log(` GitHub: ${github}`);
|
|
105
|
-
console.log(` Discord: ${discord}`);
|
|
106
|
-
console.log(` Messages: ${msgs}`);
|
|
107
|
-
console.log();
|
|
63
|
+
case "status":
|
|
64
|
+
printStatus({ messageCount: conversationHistory.length });
|
|
108
65
|
return true;
|
|
109
|
-
}
|
|
110
66
|
|
|
111
67
|
case "pair": {
|
|
112
68
|
if (parts[1] === "discord" && parts[2]) {
|
|
113
69
|
const code = parts[2].toUpperCase();
|
|
114
70
|
try {
|
|
115
|
-
const
|
|
116
|
-
const db = new QuickDB({
|
|
117
|
-
driver: new SqliteDriver(PATHS.quickDb),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const userId = await db.get(`pairCode_${code}`);
|
|
121
|
-
if (!userId) {
|
|
122
|
-
console.log();
|
|
123
|
-
printError(`Invalid or expired pair code: ${code}`);
|
|
124
|
-
console.log();
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
await db.set(`paired_${userId}`, true);
|
|
129
|
-
await db.delete(`pairCode_${code}`);
|
|
130
|
-
config.set("interfaces.discord.pairedUsers", [
|
|
131
|
-
...(config.get("interfaces.discord.pairedUsers") || []),
|
|
132
|
-
userId,
|
|
133
|
-
]);
|
|
134
|
-
|
|
71
|
+
const result = await pairDiscord(code);
|
|
135
72
|
console.log();
|
|
136
|
-
|
|
73
|
+
if (result.ok) {
|
|
74
|
+
printSuccess(`Paired Discord user ${result.userId}!`);
|
|
75
|
+
} else {
|
|
76
|
+
printError(result.error);
|
|
77
|
+
}
|
|
137
78
|
console.log();
|
|
138
79
|
} catch (err) {
|
|
139
80
|
console.log();
|
|
@@ -157,99 +98,22 @@ const handleSlashCommand = async (input, rl) => {
|
|
|
157
98
|
return true;
|
|
158
99
|
|
|
159
100
|
case "timers": {
|
|
160
|
-
|
|
161
|
-
const scheduled = events.listScheduled();
|
|
162
|
-
console.log();
|
|
163
|
-
if (scheduled.length === 0) {
|
|
164
|
-
console.log(chalk.dim(" No active timers."));
|
|
165
|
-
} else {
|
|
166
|
-
console.log(chalk.bold(" Active timers:"));
|
|
167
|
-
for (const t of scheduled) {
|
|
168
|
-
const secs = Math.ceil(t.remainingMs / 1000);
|
|
169
|
-
const desc = t.meta?.task || t.meta?.message || t.id;
|
|
170
|
-
console.log(` ⏱️ ${t.id} — ${secs}s left — ${desc}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
console.log();
|
|
101
|
+
cmds.printTimers();
|
|
174
102
|
return true;
|
|
175
103
|
}
|
|
176
104
|
|
|
177
105
|
case "plugins": {
|
|
178
|
-
|
|
179
|
-
console.log();
|
|
180
|
-
console.log(chalk.bold(" Loaded plugins:"));
|
|
181
|
-
for (const p of pluginModule.plugins) {
|
|
182
|
-
const toolCount = p.tools ? Object.keys(p.tools).length : 0;
|
|
183
|
-
console.log(` • ${chalk.cyan(p.name)} — ${toolCount} tool(s) — ${p.description || ""}`);
|
|
184
|
-
}
|
|
185
|
-
console.log();
|
|
106
|
+
cmds.printPlugins();
|
|
186
107
|
return true;
|
|
187
108
|
}
|
|
188
109
|
|
|
189
110
|
case "interfaces":
|
|
190
111
|
case "interface": {
|
|
191
|
-
|
|
192
|
-
return { clack: true, run: async () => {
|
|
193
|
-
const { listInterfaces, createInterface, deleteInterface } = require("../create_interface");
|
|
194
|
-
const { select, isCancel, log: cLog } = require("@clack/prompts");
|
|
195
|
-
|
|
196
|
-
const all = listInterfaces();
|
|
197
|
-
console.log();
|
|
198
|
-
console.log(chalk.bold(" Interfaces:"));
|
|
199
|
-
for (const iface of all) {
|
|
200
|
-
const tag = iface.type === "built-in"
|
|
201
|
-
? chalk.dim("[built-in]")
|
|
202
|
-
: chalk.cyan("[custom]");
|
|
203
|
-
console.log(` ${tag} ${chalk.bold(iface.name)} \u2014 ${iface.description}`);
|
|
204
|
-
}
|
|
205
|
-
console.log();
|
|
206
|
-
|
|
207
|
-
const action = await select({
|
|
208
|
-
message: "What would you like to do?",
|
|
209
|
-
options: [
|
|
210
|
-
{ value: "create", label: "\uD83D\uDD27 Create new interface" },
|
|
211
|
-
{ value: "delete", label: "\uD83D\uDDD1\uFE0F Delete a custom interface",
|
|
212
|
-
hint: all.filter(i => i.type === "custom").length === 0 ? "none yet" : undefined },
|
|
213
|
-
{ value: "back", label: "\u2190 Back" },
|
|
214
|
-
],
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (isCancel(action) || action === "back") return;
|
|
218
|
-
|
|
219
|
-
if (action === "create") {
|
|
220
|
-
await createInterface();
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (action === "delete") {
|
|
225
|
-
const customs = all.filter(i => i.type === "custom");
|
|
226
|
-
if (customs.length === 0) {
|
|
227
|
-
cLog.warn("No custom interfaces to delete.");
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const which = await select({
|
|
231
|
-
message: "Which interface to delete?",
|
|
232
|
-
options: [
|
|
233
|
-
...customs.map(i => ({ value: i.name, label: i.name, hint: i.description })),
|
|
234
|
-
{ value: "back", label: "\u2190 Back" },
|
|
235
|
-
],
|
|
236
|
-
});
|
|
237
|
-
if (isCancel(which) || which === "back") return;
|
|
238
|
-
if (deleteInterface(which)) {
|
|
239
|
-
cLog.success(`Deleted "${which}".`);
|
|
240
|
-
} else {
|
|
241
|
-
cLog.error(`Interface "${which}" not found.`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}};
|
|
112
|
+
return { clack: true, run: () => cmds.interfaceMenu() };
|
|
245
113
|
}
|
|
246
114
|
|
|
247
115
|
case "settings": {
|
|
248
|
-
|
|
249
|
-
return { clack: true, run: async () => {
|
|
250
|
-
const { settingsMenu } = require("../../utils/settings");
|
|
251
|
-
await settingsMenu();
|
|
252
|
-
}};
|
|
116
|
+
return { clack: true, run: () => cmds.openSettings() };
|
|
253
117
|
}
|
|
254
118
|
|
|
255
119
|
default:
|
|
@@ -276,6 +140,12 @@ const handleChat = async (input) => {
|
|
|
276
140
|
|
|
277
141
|
const reply = response.content || "...";
|
|
278
142
|
conversationHistory.push({ role: "assistant", content: reply });
|
|
143
|
+
|
|
144
|
+
// Keep history manageable (same limits as daemon/discord)
|
|
145
|
+
if (conversationHistory.length > 80) {
|
|
146
|
+
conversationHistory.splice(0, conversationHistory.length - 60);
|
|
147
|
+
}
|
|
148
|
+
|
|
279
149
|
memory.save("terminal", conversationHistory);
|
|
280
150
|
|
|
281
151
|
console.log();
|
|
@@ -419,4 +289,4 @@ const start = async () => {
|
|
|
419
289
|
});
|
|
420
290
|
};
|
|
421
291
|
|
|
422
|
-
module.exports = { start };
|
|
292
|
+
module.exports = { start, ...cmds };
|