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.
@@ -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
- await database.set(`pairCode_${code}`, user.id);
150
+ // Store with timestamp so expiry survives process restarts
151
+ await database.set(`pairCode_${code}`, { userId: user.id, createdAt: Date.now() });
145
152
 
146
- // Expire after 5 minutes
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 (all messages) ──
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
- try {
16
- const fs = require("fs");
17
- const path = require("path");
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: async () => {
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
- console.log();
102
- console.log(chalk.bold(" Status:"));
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 { SqliteDriver, QuickDB } = require("quick.db");
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
- printSuccess(`Paired Discord user ${userId}!`);
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
- const { events } = require("../../utils/events");
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
- const pluginModule = require("../../plugins");
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
- // @clack/prompts wizard needs exclusive stdin
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
- // @clack/prompts needs exclusive stdin signal processLine to handle it
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 };