peely 0.9.5 → 0.9.6

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 CHANGED
@@ -14,6 +14,12 @@
14
14
 
15
15
  </div>
16
16
 
17
+ <div align="center">
18
+
19
+ ![Demo of Peely in action, showing terminal interactions and Discord bot responses](demo.gif)
20
+
21
+ </div>
22
+
17
23
  ---
18
24
 
19
25
  ## 🌟 Features
package/cli.js CHANGED
@@ -309,45 +309,6 @@ const oneShot = async (msg) => {
309
309
  break;
310
310
  }
311
311
 
312
- case "interface":
313
- case "interfaces": {
314
- if (subcommand === "create" || subcommand === "new") {
315
- await tui.createInterface();
316
- } else if (subcommand === "list" || subcommand === "ls") {
317
- console.log(logo);
318
- tui.printInterfaces();
319
- } else if (subcommand === "start" || subcommand === "run") {
320
- const ifaceName = args[2];
321
- if (!ifaceName) {
322
- console.log(chalk.red(" ✗ Usage: peely interface start <name>"));
323
- break;
324
- }
325
- const ok = await tui.startInterface(ifaceName);
326
- if (!ok) {
327
- console.log(chalk.red(` ✗ Interface "${ifaceName}" not found. Run peely interface list.`));
328
- }
329
- } else if (subcommand === "delete" || subcommand === "rm") {
330
- const ifaceName = args[2];
331
- if (!ifaceName) {
332
- console.log(chalk.red(" ✗ Usage: peely interface delete <name>"));
333
- break;
334
- }
335
- if (tui.deleteInterface(ifaceName)) {
336
- console.log(chalk.green(` ✓ Deleted interface "${ifaceName}".`));
337
- } else {
338
- console.log(chalk.red(` ✗ Interface "${ifaceName}" not found.`));
339
- }
340
- } else {
341
- console.log(chalk.bold(" Interface commands:"));
342
- console.log(` ${chalk.cyan("peely interface create")} Create a new custom interface`);
343
- console.log(` ${chalk.cyan("peely interface list")} List all interfaces`);
344
- console.log(` ${chalk.cyan("peely interface start <name>")} Start a custom interface`);
345
- console.log(` ${chalk.cyan("peely interface delete <name>")} Delete a custom interface`);
346
- console.log();
347
- }
348
- break;
349
- }
350
-
351
312
  case "pair":
352
313
  if (subcommand === "discord") {
353
314
  const codeOrSetup = args[2];
package/demo.gif ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peely",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "main": "cli.js",
5
5
  "bin": {
6
6
  "peely": "cli.js"
package/src/ai/index.js CHANGED
@@ -35,6 +35,14 @@ const buildSystemPrompt = (registry) => {
35
35
  })
36
36
  .join("\n");
37
37
 
38
+ // Build a dynamic "prefer specific tools" hint from actual plugin names
39
+ const pluginNames = [...new Set(Object.values(registry).map((t) => t.pluginName))];
40
+ const toolHint = pluginNames.length > 0
41
+ ? `You have these tool categories: ${pluginNames.join(", ")}. ` +
42
+ "Pick the most specific tool for the job. " +
43
+ "Only fall back to search (if available) when no specific tool exists."
44
+ : "You have no tools available. Answer from your own knowledge.";
45
+
38
46
  return `You are peely 🍌 - a personal AI assistant with actual personality.
39
47
 
40
48
  -- WHO YOU ARE --
@@ -52,14 +60,15 @@ const buildSystemPrompt = (registry) => {
52
60
  - You're allowed to be a little weird, a little opinionated, a little charming. You're peely 🍌, not a customer service bot.
53
61
 
54
62
  -- YOUR TOOLS --
63
+ These are the ONLY tools you have. The tool ID format is pluginName.toolName.
55
64
  ${toolDescriptions}
56
65
 
57
66
  -- RULES --
58
67
  1. When a tool can help, CALL IT IMMEDIATELY. Don't narrate what you're about to do - just do it.
59
68
  Output the tool call JSON. NEVER announce a tool call without actually making it.
60
- 2. ALWAYS prefer the most specific tool for the job:
61
- Weather -> weather tools. Facts -> search.search. Math -> math tools. Discord -> discord tools.
62
- Only fall back to search.search if no specific tool exists.
69
+ 2. ${toolHint}
70
+ NEVER invent tool IDs that are not listed above. If a tool doesn't exist in the list, it doesn't exist. Period.
71
+ The tool ID MUST match exactly use the full "pluginName.toolName" format shown above.
63
72
  3. To call a tool, reply with ONLY this JSON (no other text before or after):
64
73
  {"tool_calls":[{"id":"search.search","args":["query"]}]}
65
74
  You may call multiple tools at once.
@@ -73,6 +82,8 @@ const buildSystemPrompt = (registry) => {
73
82
  9. BEFORE calling an ACTION tool (sending messages, etc.), make sure you have ALL required info.
74
83
  If something's missing, ask. Don't guess. Don't send empty messages.
75
84
  Read-only tools (search, list, math) can be called freely.
85
+ 10. Use tools when possible. Don't just say "I would use X tool here" - actually use it. The tools are your superpower - use them to help the user in ways you couldn't on your own.
86
+ 11. You're peely 🍌 - be friendly, helpful, and a little fun. Don't be a boring robot.
76
87
 
77
88
  thank you for being you, peely 🍌. I hope you'll have fun :) - developer of peely 🍌
78
89
  `;
@@ -155,15 +166,39 @@ const executeTool = async (call, registry) => {
155
166
  // ── Try to parse tool_calls JSON from assistant text ──
156
167
  const parseToolCalls = (text) => {
157
168
  if (!text) return null;
169
+
170
+ // 1. Entire response is JSON (most common path)
158
171
  try {
159
- // Find JSON that contains "tool_calls" — avoid matching unrelated JSON
160
- const match = text.match(/\{[\s\S]*?"tool_calls"\s*:\s*\[[\s\S]*?\]\s*\}/);
161
- if (!match) return null;
162
- const obj = JSON.parse(match[0]);
172
+ const obj = JSON.parse(text.trim());
163
173
  if (Array.isArray(obj.tool_calls) && obj.tool_calls.length > 0) {
164
174
  return obj.tool_calls;
165
175
  }
166
176
  } catch (_) {}
177
+
178
+ // 2. JSON embedded in surrounding text — find balanced braces
179
+ const idx = text.indexOf('"tool_calls"');
180
+ if (idx === -1) return null;
181
+
182
+ const start = text.lastIndexOf("{", idx);
183
+ if (start === -1) return null;
184
+
185
+ let depth = 0;
186
+ for (let i = start; i < text.length; i++) {
187
+ if (text[i] === "{") depth++;
188
+ else if (text[i] === "}") {
189
+ depth--;
190
+ if (depth === 0) {
191
+ try {
192
+ const obj = JSON.parse(text.slice(start, i + 1));
193
+ if (Array.isArray(obj.tool_calls) && obj.tool_calls.length > 0) {
194
+ return obj.tool_calls;
195
+ }
196
+ } catch (_) {}
197
+ break;
198
+ }
199
+ }
200
+ }
201
+
167
202
  return null;
168
203
  };
169
204
 
@@ -1,35 +1,9 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const PATHS = require("../utils/paths");
4
-
5
1
  // Built-in interfaces (lazy-loaded to avoid side effects on require)
6
- const builtIn = {
2
+ module.exports = {
7
3
  get terminal() {
8
4
  return require("./terminal");
9
5
  },
10
6
  get discord() {
11
7
  return require("./discord");
12
8
  },
13
- };
14
-
15
- // Auto-discover custom interfaces from ~/.peely/interfaces/custom/
16
- const customDir = PATHS.customInterfaces;
17
- const custom = {};
18
-
19
- if (fs.existsSync(customDir)) {
20
- for (const entry of fs.readdirSync(customDir, { withFileTypes: true })) {
21
- if (!entry.isDirectory()) continue;
22
- const indexPath = path.join(customDir, entry.name, "index.js");
23
- if (!fs.existsSync(indexPath)) continue;
24
- // Lazy-load via getter so broken interfaces don't crash everything
25
- Object.defineProperty(custom, entry.name, {
26
- get() {
27
- return require(indexPath);
28
- },
29
- enumerable: true,
30
- configurable: true,
31
- });
32
- }
33
- }
34
-
35
- module.exports = { ...builtIn, ...custom };
9
+ };
@@ -204,104 +204,6 @@ const printPlugins = () => {
204
204
  console.log();
205
205
  };
206
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
207
  // ═══════════════════════════════════════════════════════════════════
306
208
  // Help text
307
209
  // ═══════════════════════════════════════════════════════════════════
@@ -321,10 +223,6 @@ const CLI_COMMANDS = [
321
223
  { usage: "peely pair discord setup", desc: "Set Discord bot token" },
322
224
  { usage: "peely model", desc: "Choose AI model" },
323
225
  { 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
226
  { usage: "peely status", desc: "Show config status" },
329
227
  { usage: "peely help", desc: "Show this help" },
330
228
  ];
@@ -336,7 +234,6 @@ const SLASH_COMMANDS = [
336
234
  { usage: "/settings", desc: "Edit config, tokens & API keys" },
337
235
  { usage: "/timers", desc: "Show active timers" },
338
236
  { usage: "/plugins", desc: "List loaded plugins" },
339
- { usage: "/interfaces", desc: "List & create interfaces" },
340
237
  { usage: "/pair discord <code>", desc: "Pair a Discord account" },
341
238
  { usage: "/status", desc: "Show system status" },
342
239
  { usage: "/exit", desc: "Exit peely" },
@@ -386,12 +283,6 @@ module.exports = {
386
283
  printTimers,
387
284
  getPlugins,
388
285
  printPlugins,
389
- getInterfaces,
390
- printInterfaces,
391
- createInterface,
392
- deleteInterface,
393
- startInterface,
394
- interfaceMenu,
395
286
 
396
287
  // Help
397
288
  CLI_COMMANDS,
@@ -107,11 +107,6 @@ const handleSlashCommand = async (input, rl) => {
107
107
  return true;
108
108
  }
109
109
 
110
- case "interfaces":
111
- case "interface": {
112
- return { clack: true, run: () => cmds.interfaceMenu() };
113
- }
114
-
115
110
  case "settings": {
116
111
  return { clack: true, run: () => cmds.openSettings() };
117
112
  }
@@ -9,6 +9,9 @@ const CUSTOM_DIR = PATHS.customPlugins;
9
9
  if (!fs.existsSync(CUSTOM_DIR)) fs.mkdirSync(CUSTOM_DIR, { recursive: true });
10
10
 
11
11
  // ── Agent system prompt ──
12
+ // __PEELY_ROOT__ is replaced at runtime with the actual package root path
13
+ const PEELY_ROOT = path.resolve(__dirname, "..", "..", "..");
14
+
12
15
  const AGENT_PROMPT = `You are a plugin code generator for peely (a Node.js AI assistant).
13
16
 
14
17
  Your job: generate a complete, working plugin file based on the user's description.
@@ -40,8 +43,8 @@ Plugins can trigger the AI to perform any task using ai.invoke(task).
40
43
  This is the preferred way to make things happen — describe WHAT, let the AI figure out HOW.
41
44
 
42
45
  \`\`\`js
43
- // Lazy-load to avoid circular dependency
44
- const ai = require("../../ai");
46
+ // ALWAYS use this exact require path — it works from any plugin location:
47
+ const ai = require("${PEELY_ROOT.replace(/\\/g, "/")}/src/ai");
45
48
  await ai.invoke("send a Discord DM to username saying hello");
46
49
  await ai.invoke("search for weather in Warsaw and tell me");
47
50
  \`\`\`
@@ -52,7 +55,7 @@ Use ai.invoke() when:
52
55
  - You want the AI to decide which tools to use based on context
53
56
 
54
57
  AVAILABLE UTILITIES:
55
- - const { events } = require("../../utils/events") — event bus for async events
58
+ - const { events } = require("${PEELY_ROOT.replace(/\\/g, "/")}/src/utils/events") — event bus
56
59
  events.on("eventName", callback) / events.emit("eventName", data)
57
60
  events.scheduleTimeout(id, ms, callback, meta) / events.cancelTimeout(id)
58
61
 
@@ -67,7 +70,8 @@ RULES:
67
70
  8. Plugin name must be lowercase with underscores, no spaces.
68
71
  9. Keep it simple and focused. One plugin = one domain of functionality.
69
72
  10. Do NOT include any text outside the JavaScript code.
70
- 11. When the plugin needs to trigger actions, USE ai.invoke() instead of hardcoding logic.`;
73
+ 11. When the plugin needs to trigger actions, USE ai.invoke() instead of hardcoding logic.
74
+ 12. For AI/utility requires, ALWAYS use the absolute paths shown above. NEVER use relative paths like "../../ai".`;
71
75
 
72
76
  // ── Dangerous pattern scanner for generated code ──
73
77
  const DANGEROUS_PATTERNS = [
@@ -17,7 +17,6 @@ const dirs = [
17
17
  path.join(DATA_HOME, "data"),
18
18
  path.join(DATA_HOME, "data", "conversations"),
19
19
  path.join(DATA_HOME, "plugins", "custom"),
20
- path.join(DATA_HOME, "interfaces", "custom"),
21
20
  ];
22
21
 
23
22
  for (const d of dirs) {
@@ -55,9 +54,6 @@ const PATHS = {
55
54
 
56
55
  /** plugins/custom/ directory for user-created plugins */
57
56
  customPlugins: path.join(DATA_HOME, "plugins", "custom"),
58
-
59
- /** interfaces/custom/ directory for user-created interfaces */
60
- customInterfaces: path.join(DATA_HOME, "interfaces", "custom"),
61
57
  };
62
58
 
63
59
  // ── One-time migration from old (in-project) layout ──
@@ -1,323 +0,0 @@
1
- /**
2
- * Interface scaffolding — create new custom interfaces.
3
- *
4
- * Custom interfaces live in ~/.peely/interfaces/custom/<name>/index.js
5
- * so they survive npm updates. Each interface exports { name, description, start }.
6
- */
7
-
8
- const fs = require("fs");
9
- const path = require("path");
10
- const chalk = require("chalk");
11
- const { intro, text, select, isCancel, log, outro } = require("@clack/prompts");
12
- const PATHS = require("../utils/paths");
13
-
14
- const CUSTOM_DIR = PATHS.customInterfaces;
15
-
16
- // ── Template ──
17
- const template = (name, description, type) => {
18
- const base = `/**
19
- * ${name} — custom peely interface
20
- * ${description}
21
- */
22
-
23
- const path = require("path");
24
-
25
- // Resolve paths relative to the peely package root
26
- const peelyRoot = path.resolve(__dirname, "..", "..", "..", "..", "..");
27
- const config = require(path.join(peelyRoot, "src/utils/config"));
28
- const ai = require(path.join(peelyRoot, "src/ai"));
29
- const memory = require(path.join(peelyRoot, "src/utils/memory"));
30
- const chalk = require("chalk");
31
-
32
- // Load / create conversation history for this interface
33
- const conversationHistory = memory.load("${name}");
34
-
35
- `;
36
-
37
- if (type === "http") {
38
- return (
39
- base +
40
- `const http = require("http");
41
-
42
- const PORT = 3000;
43
-
44
- const start = async () => {
45
- const server = http.createServer(async (req, res) => {
46
- if (req.method === "POST" && req.url === "/chat") {
47
- let body = "";
48
- for await (const chunk of req) body += chunk;
49
-
50
- try {
51
- const { message } = JSON.parse(body);
52
- conversationHistory.push({ role: "user", content: message });
53
-
54
- const response = await ai.chat(conversationHistory);
55
- const reply = response.content || "...";
56
- conversationHistory.push({ role: "assistant", content: reply });
57
- memory.save("${name}", conversationHistory);
58
-
59
- res.writeHead(200, { "Content-Type": "application/json" });
60
- res.end(JSON.stringify({ reply }));
61
- } catch (err) {
62
- res.writeHead(500, { "Content-Type": "application/json" });
63
- res.end(JSON.stringify({ error: err.message }));
64
- }
65
- } else {
66
- res.writeHead(200, { "Content-Type": "text/plain" });
67
- res.end("peely ${name} interface — POST /chat to talk");
68
- }
69
- });
70
-
71
- server.listen(PORT, () => {
72
- console.log(chalk.green(\` ✓ ${name} interface listening on http://localhost:\${PORT}\`));
73
- });
74
-
75
- return new Promise(() => {}); // keep alive
76
- };
77
-
78
- module.exports = {
79
- name: "${name}",
80
- description: "${description}",
81
- start,
82
- };
83
- `
84
- );
85
- }
86
-
87
- if (type === "websocket") {
88
- return (
89
- base +
90
- `// NOTE: install ws first — npm i ws
91
- const { WebSocketServer } = require("ws");
92
-
93
- const PORT = 8080;
94
-
95
- const start = async () => {
96
- const wss = new WebSocketServer({ port: PORT });
97
- console.log(chalk.green(\` ✓ ${name} interface listening on ws://localhost:\${PORT}\`));
98
-
99
- wss.on("connection", (ws) => {
100
- const history = [...conversationHistory]; // per-connection copy
101
-
102
- ws.on("message", async (data) => {
103
- try {
104
- const message = data.toString();
105
- history.push({ role: "user", content: message });
106
-
107
- const response = await ai.chat(history);
108
- const reply = response.content || "...";
109
- history.push({ role: "assistant", content: reply });
110
- memory.save("${name}", history);
111
-
112
- ws.send(JSON.stringify({ reply }));
113
- } catch (err) {
114
- ws.send(JSON.stringify({ error: err.message }));
115
- }
116
- });
117
-
118
- ws.send(JSON.stringify({ reply: "Connected to peely ${name} interface!" }));
119
- });
120
-
121
- return new Promise(() => {}); // keep alive
122
- };
123
-
124
- module.exports = {
125
- name: "${name}",
126
- description: "${description}",
127
- start,
128
- };
129
- `
130
- );
131
- }
132
-
133
- if (type === "stdin") {
134
- return (
135
- base +
136
- `const readline = require("readline");
137
-
138
- const start = async () => {
139
- const rl = readline.createInterface({
140
- input: process.stdin,
141
- output: process.stdout,
142
- prompt: chalk.bold.cyan(" ${name} › "),
143
- terminal: true,
144
- });
145
-
146
- console.log(chalk.magenta(" ${name}") + chalk.dim(" — type a message or Ctrl+C to exit"));
147
- console.log();
148
- rl.prompt();
149
-
150
- rl.on("line", async (line) => {
151
- const input = line.trim();
152
- if (!input) { rl.prompt(); return; }
153
-
154
- if (input === "/exit" || input === "/quit") {
155
- console.log(chalk.dim(" 👋 Bye!"));
156
- rl.close();
157
- return;
158
- }
159
-
160
- conversationHistory.push({ role: "user", content: input });
161
-
162
- try {
163
- const response = await ai.chat(conversationHistory);
164
- const reply = response.content || "...";
165
- conversationHistory.push({ role: "assistant", content: reply });
166
- memory.save("${name}", conversationHistory);
167
- console.log(chalk.bold.magenta(" 🍌 ") + reply);
168
- } catch (err) {
169
- console.log(chalk.red(" ✗ ") + err.message);
170
- conversationHistory.pop();
171
- }
172
-
173
- console.log();
174
- rl.prompt();
175
- });
176
-
177
- rl.on("close", () => process.exit(0));
178
- return new Promise(() => {});
179
- };
180
-
181
- module.exports = {
182
- name: "${name}",
183
- description: "${description}",
184
- start,
185
- };
186
- `
187
- );
188
- }
189
-
190
- // Blank / custom
191
- return (
192
- base +
193
- `const start = async () => {
194
- console.log(chalk.green(" ✓ ${name} interface started"));
195
-
196
- // ── Your interface logic goes here ──
197
- // Use ai.chat(conversationHistory) to talk to the AI
198
- // Use memory.save("${name}", conversationHistory) to persist
199
- // Use config.get() / config.set() for settings
200
-
201
- // Example: handle a single message
202
- // conversationHistory.push({ role: "user", content: "hello" });
203
- // const response = await ai.chat(conversationHistory);
204
- // console.log(response.content);
205
-
206
- return new Promise(() => {}); // keep alive
207
- };
208
-
209
- module.exports = {
210
- name: "${name}",
211
- description: "${description}",
212
- start,
213
- };
214
- `
215
- );
216
- };
217
-
218
- // ── Wizard ──
219
- const createInterface = async () => {
220
- intro(chalk.magenta("🔌 Create a new interface"));
221
-
222
- const name = await text({
223
- message: "Interface name:",
224
- placeholder: "e.g. slack, telegram, web",
225
- validate: (val) => {
226
- if (!val || !val.trim()) return "Name is required";
227
- if (!/^[a-z][a-z0-9_-]*$/.test(val.trim()))
228
- return "Lowercase alphanumeric, hyphens, underscores only";
229
- const dir = path.join(CUSTOM_DIR, val.trim());
230
- if (fs.existsSync(dir)) return `Interface "${val.trim()}" already exists`;
231
- },
232
- });
233
-
234
- if (isCancel(name)) { outro(chalk.dim("Cancelled.")); return; }
235
-
236
- const description = await text({
237
- message: "Short description:",
238
- placeholder: "e.g. Slack bot interface for peely",
239
- });
240
-
241
- if (isCancel(description)) { outro(chalk.dim("Cancelled.")); return; }
242
-
243
- const type = await select({
244
- message: "Template:",
245
- options: [
246
- { value: "http", label: "🌐 HTTP server", hint: "REST API on localhost" },
247
- { value: "websocket", label: "🔌 WebSocket", hint: "real-time bidirectional" },
248
- { value: "stdin", label: "⌨️ Stdin / readline", hint: "simple terminal loop" },
249
- { value: "blank", label: "📄 Blank", hint: "empty start() scaffold" },
250
- ],
251
- });
252
-
253
- if (isCancel(type)) { outro(chalk.dim("Cancelled.")); return; }
254
-
255
- const safeName = name.trim();
256
- const safeDesc = (description || "").trim() || `Custom ${safeName} interface`;
257
-
258
- const dir = path.join(CUSTOM_DIR, safeName);
259
- fs.mkdirSync(dir, { recursive: true });
260
-
261
- const code = template(safeName, safeDesc, type);
262
- const filePath = path.join(dir, "index.js");
263
- fs.writeFileSync(filePath, code, "utf-8");
264
-
265
- log.success(`Created ${chalk.cyan(safeName)} interface`);
266
- log.info(`File: ${chalk.dim(filePath)}`);
267
- log.info(`Start: ${chalk.cyan(`peely interface start ${safeName}`)}`);
268
- outro(chalk.dim("Done!"));
269
-
270
- return safeName;
271
- };
272
-
273
- // ── List all interfaces (built-in + custom) ──
274
- const listInterfaces = () => {
275
- const builtIn = [
276
- { name: "terminal", description: "Interactive TUI", type: "built-in" },
277
- { name: "discord", description: "Discord bot", type: "built-in" },
278
- ];
279
-
280
- const custom = [];
281
- if (fs.existsSync(CUSTOM_DIR)) {
282
- for (const entry of fs.readdirSync(CUSTOM_DIR, { withFileTypes: true })) {
283
- if (!entry.isDirectory()) continue;
284
- const indexPath = path.join(CUSTOM_DIR, entry.name, "index.js");
285
- if (!fs.existsSync(indexPath)) continue;
286
- try {
287
- const mod = require(indexPath);
288
- custom.push({
289
- name: mod.name || entry.name,
290
- description: mod.description || "",
291
- type: "custom",
292
- path: indexPath,
293
- });
294
- } catch (err) {
295
- custom.push({
296
- name: entry.name,
297
- description: chalk.red(`load error: ${err.message}`),
298
- type: "custom",
299
- path: indexPath,
300
- });
301
- }
302
- }
303
- }
304
-
305
- return [...builtIn, ...custom];
306
- };
307
-
308
- // ── Load a custom interface by name ──
309
- const loadCustomInterface = (name) => {
310
- const indexPath = path.join(CUSTOM_DIR, name, "index.js");
311
- if (!fs.existsSync(indexPath)) return null;
312
- return require(indexPath);
313
- };
314
-
315
- // ── Delete a custom interface ──
316
- const deleteInterface = (name) => {
317
- const dir = path.join(CUSTOM_DIR, name);
318
- if (!fs.existsSync(dir)) return false;
319
- fs.rmSync(dir, { recursive: true, force: true });
320
- return true;
321
- };
322
-
323
- module.exports = { createInterface, listInterfaces, loadCustomInterface, deleteInterface };