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 +6 -0
- package/cli.js +0 -39
- package/demo.gif +0 -0
- package/package.json +1 -1
- package/src/ai/index.js +42 -7
- package/src/interfaces/index.js +2 -28
- package/src/interfaces/terminal/commands.js +0 -109
- package/src/interfaces/terminal/index.js +0 -5
- package/src/plugins/plugins/create_plugin.js +8 -4
- package/src/utils/paths.js +0 -4
- package/src/interfaces/create_interface.js +0 -323
package/README.md
CHANGED
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
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.
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
package/src/interfaces/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
44
|
-
const ai = require("
|
|
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("
|
|
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 = [
|
package/src/utils/paths.js
CHANGED
|
@@ -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 };
|