peely 0.9.4 → 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.
@@ -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,17 @@ 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
- case "interfaces":
190
- 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
- }};
245
- }
246
-
247
110
  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
- }};
111
+ return { clack: true, run: () => cmds.openSettings() };
253
112
  }
254
113
 
255
114
  default:
@@ -276,6 +135,12 @@ const handleChat = async (input) => {
276
135
 
277
136
  const reply = response.content || "...";
278
137
  conversationHistory.push({ role: "assistant", content: reply });
138
+
139
+ // Keep history manageable (same limits as daemon/discord)
140
+ if (conversationHistory.length > 80) {
141
+ conversationHistory.splice(0, conversationHistory.length - 60);
142
+ }
143
+
279
144
  memory.save("terminal", conversationHistory);
280
145
 
281
146
  console.log();
@@ -419,4 +284,4 @@ const start = async () => {
419
284
  });
420
285
  };
421
286
 
422
- module.exports = { start };
287
+ module.exports = { start, ...cmds };
@@ -12,7 +12,18 @@ if (!fs.existsSync(CUSTOM_DIR)) fs.mkdirSync(CUSTOM_DIR, { recursive: true });
12
12
  const builtinList = fs.readdirSync(BUILTIN_DIR).filter((f) => f.endsWith(".js"));
13
13
  const plugins = builtinList.map((f) => {
14
14
  const plugin = require(`./plugins/${f}`);
15
- if (plugin.initialize) plugin.initialize();
15
+ if (plugin.initialize) {
16
+ try {
17
+ const result = plugin.initialize();
18
+ if (result && typeof result.catch === "function") {
19
+ result.catch((err) =>
20
+ console.error(`⚠️ Plugin ${f} init error:`, err.message)
21
+ );
22
+ }
23
+ } catch (err) {
24
+ console.error(`⚠️ Plugin ${f} init error:`, err.message);
25
+ }
26
+ }
16
27
  return plugin;
17
28
  });
18
29
 
@@ -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,32 @@ 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".`;
75
+
76
+ // ── Dangerous pattern scanner for generated code ──
77
+ const DANGEROUS_PATTERNS = [
78
+ /\bchild_process\b/,
79
+ /\bexec\s*\(/,
80
+ /\bexecSync\s*\(/,
81
+ /\bspawn\s*\(/,
82
+ /\beval\s*\(/,
83
+ /\bFunction\s*\(/,
84
+ /\bprocess\.env\b/,
85
+ /\bprocess\.exit\b/,
86
+ /\bglobal\./,
87
+ /\brequire\s*\(\s*['"]child_process['"]/,
88
+ ];
89
+
90
+ const scanForDangerousPatterns = (code) => {
91
+ const matches = [];
92
+ for (const pat of DANGEROUS_PATTERNS) {
93
+ if (pat.test(code)) {
94
+ matches.push(pat.source);
95
+ }
96
+ }
97
+ return matches;
98
+ };
71
99
 
72
100
  // ── The actual tool functions ──
73
101
 
@@ -116,6 +144,16 @@ const createPlugin = async (name, description) => {
116
144
  return `ERROR: Generated code has a syntax error: ${err.message}. Try again with a simpler description.`;
117
145
  }
118
146
 
147
+ // Scan for dangerous patterns (security check)
148
+ const dangers = scanForDangerousPatterns(code);
149
+ if (dangers.length > 0) {
150
+ return (
151
+ `\u26a0\ufe0f BLOCKED: Generated plugin code contains potentially dangerous patterns:\n` +
152
+ dangers.map((d) => ` \u2022 ${d}`).join("\n") +
153
+ `\n\nThe code was NOT saved. Try a safer description or review the generated code manually.`
154
+ );
155
+ }
156
+
119
157
  fs.writeFileSync(filePath, code, "utf-8");
120
158
 
121
159
  // Hot-reload: register the new plugin into the live tool registry
@@ -177,6 +215,16 @@ const editPlugin = async (name, changes) => {
177
215
  return `ERROR: Updated code has a syntax error: ${err.message}. Try again.`;
178
216
  }
179
217
 
218
+ // Scan for dangerous patterns (security check)
219
+ const dangers = scanForDangerousPatterns(code);
220
+ if (dangers.length > 0) {
221
+ return (
222
+ `\u26a0\ufe0f BLOCKED: Updated plugin code contains potentially dangerous patterns:\n` +
223
+ dangers.map((d) => ` \u2022 ${d}`).join("\n") +
224
+ `\n\nThe code was NOT saved. Try a safer modification or review the generated code manually.`
225
+ );
226
+ }
227
+
180
228
  // Backup old version
181
229
  const backupPath = filePath + ".bak";
182
230
  fs.copyFileSync(filePath, backupPath);
@@ -1,7 +1,10 @@
1
1
  const add = (a, b) => a + b;
2
2
  const subtract = (a, b) => a - b;
3
3
  const multiply = (a, b) => a * b;
4
- const divide = (a, b) => a / b;
4
+ const divide = (a, b) => {
5
+ if (b === 0) return "Error: division by zero";
6
+ return a / b;
7
+ };
5
8
 
6
9
  module.exports = {
7
10
  name: "math",
@@ -27,6 +27,17 @@ const center = (text, width) => {
27
27
  return " ".repeat(left) + text + " ".repeat(right);
28
28
  };
29
29
 
30
+ /** Compare two semver strings. Returns true if a > b. */
31
+ const semverGt = (a, b) => {
32
+ const pa = a.split(".").map(Number);
33
+ const pb = b.split(".").map(Number);
34
+ for (let i = 0; i < 3; i++) {
35
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
36
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
37
+ }
38
+ return false;
39
+ };
40
+
30
41
  const checkUpdate = async () => {
31
42
  const newest = await axios
32
43
  .get(NPM_URL)
@@ -36,7 +47,7 @@ const checkUpdate = async () => {
36
47
  if (!newest) return;
37
48
 
38
49
  const pkgVersion = require("../../package.json").version;
39
- if (newest <= pkgVersion) return;
50
+ if (!semverGt(newest, pkgVersion)) return;
40
51
 
41
52
  console.log("");
42
53
  const title = chalk.bold("🚀 Peely Update Available");
@@ -4,20 +4,26 @@ const PATHS = require("./paths");
4
4
  const CONFIG_PATH = PATHS.config;
5
5
 
6
6
  if (!fs.existsSync(CONFIG_PATH)) {
7
- fs.writeFileSync(CONFIG_PATH, JSON.stringify({}, null, 4));
7
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify({}, null, 4), { mode: 0o600 });
8
8
  }
9
9
 
10
+ // In-memory cache to avoid redundant synchronous disk I/O
11
+ let _configCache = null;
12
+
10
13
  const readConfig = () => {
14
+ if (_configCache) return _configCache;
11
15
  try {
12
16
  const raw = fs.readFileSync(CONFIG_PATH, "utf8");
13
- return JSON.parse(raw || "{}");
17
+ _configCache = JSON.parse(raw || "{}");
18
+ return _configCache;
14
19
  } catch (err) {
15
20
  return {};
16
21
  }
17
22
  };
18
23
 
19
24
  const writeConfig = (cfg) => {
20
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 4));
25
+ _configCache = cfg;
26
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 4), { mode: 0o600 });
21
27
  };
22
28
 
23
29
  const get = (key) => {
@@ -16,7 +16,7 @@ const EventEmitter = require("events");
16
16
  * and when the event fires it triggers the callback (e.g. send a Discord message).
17
17
  */
18
18
 
19
- class peelyEvents extends EventEmitter {
19
+ class PeelyEvents extends EventEmitter {
20
20
  constructor() {
21
21
  super();
22
22
  this.setMaxListeners(100);
@@ -66,6 +66,6 @@ class peelyEvents extends EventEmitter {
66
66
  }
67
67
  }
68
68
 
69
- const events = new peelyEvents();
69
+ const events = new PeelyEvents();
70
70
 
71
71
  module.exports = { events };
@@ -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 ──
@@ -22,6 +22,7 @@ const ENTRIES = [
22
22
  const mask = (val) => {
23
23
  if (!val) return "not set";
24
24
  const s = String(val);
25
+ if (s.length <= 6) return "•".repeat(s.length);
25
26
  return s.slice(0, 6) + "•".repeat(Math.min(s.length - 6, 20));
26
27
  };
27
28