squidclaw 1.4.0 → 1.5.0

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/lib/engine.js CHANGED
@@ -62,6 +62,8 @@ export class SquidclawEngine {
62
62
  pipeline.use('media', mediaMiddleware);
63
63
  pipeline.use('auto-links', autoLinksMiddleware);
64
64
  pipeline.use('auto-memory', autoMemoryMiddleware);
65
+ const { configChatMiddleware } = await import('./middleware/config-chat.js');
66
+ pipeline.use('config-chat', configChatMiddleware);
65
67
  pipeline.use('skill-check', skillCheckMiddleware);
66
68
  pipeline.use('typing', typingMiddleware); // wraps AI call with typing indicator
67
69
  pipeline.use('ai-process', aiProcessMiddleware);
@@ -136,7 +138,13 @@ export class SquidclawEngine {
136
138
  // 5. Features (reminders, auto-memory, usage alerts)
137
139
  await this._initFeatures();
138
140
 
139
- // 5b. Sub-agents
141
+ // 5b. Config Manager
142
+ try {
143
+ const { ConfigManager } = await import('./features/config-manager.js');
144
+ this.configManager = new ConfigManager(this);
145
+ } catch {}
146
+
147
+ // 5c. Sub-agents
140
148
  try {
141
149
  const { SubAgentManager } = await import('./features/sub-agents.js');
142
150
  this.subAgents = new SubAgentManager(this);
@@ -0,0 +1,238 @@
1
+ /**
2
+ * šŸ¦‘ Config Manager
3
+ * Hot reload, chat commands, .env support, profiles, validation
4
+ */
5
+
6
+ import { loadConfig, saveConfig, getHome } from '../core/config.js';
7
+ import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { logger } from '../core/logger.js';
10
+
11
+ export class ConfigManager {
12
+ constructor(engine) {
13
+ this.engine = engine;
14
+ this.home = getHome();
15
+ this._loadEnv();
16
+ this._watchConfig();
17
+ }
18
+
19
+ // ── .env support ──
20
+
21
+ _loadEnv() {
22
+ const envPath = join(this.home, '.env');
23
+ if (!existsSync(envPath)) return;
24
+
25
+ try {
26
+ const lines = readFileSync(envPath, 'utf8').split('\n');
27
+ for (const line of lines) {
28
+ const trimmed = line.trim();
29
+ if (!trimmed || trimmed.startsWith('#')) continue;
30
+ const eqIdx = trimmed.indexOf('=');
31
+ if (eqIdx === -1) continue;
32
+ const key = trimmed.slice(0, eqIdx).trim();
33
+ const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
34
+ process.env[key] = value;
35
+ }
36
+ logger.info('config', 'Loaded .env file');
37
+ } catch {}
38
+ }
39
+
40
+ // ── Hot reload ──
41
+
42
+ _watchConfig() {
43
+ const configPath = join(this.home, 'config.json');
44
+ try {
45
+ const { watch } = require('fs');
46
+ let debounce = null;
47
+ watch(configPath, () => {
48
+ if (debounce) clearTimeout(debounce);
49
+ debounce = setTimeout(() => {
50
+ try {
51
+ this.engine.config = loadConfig();
52
+ logger.info('config', 'Config hot-reloaded');
53
+ } catch (err) {
54
+ logger.error('config', 'Hot reload failed: ' + err.message);
55
+ }
56
+ }, 1000);
57
+ });
58
+ } catch {
59
+ // fs.watch not available — use polling
60
+ this._pollConfig(configPath);
61
+ }
62
+ }
63
+
64
+ _pollConfig(configPath) {
65
+ let lastMtime = 0;
66
+ try { lastMtime = readFileSync(configPath).length; } catch {}
67
+
68
+ setInterval(() => {
69
+ try {
70
+ const content = readFileSync(configPath, 'utf8');
71
+ if (content.length !== lastMtime) {
72
+ lastMtime = content.length;
73
+ this.engine.config = loadConfig();
74
+ logger.info('config', 'Config hot-reloaded (poll)');
75
+ }
76
+ } catch {}
77
+ }, 5000);
78
+ }
79
+
80
+ // ── Set config value ──
81
+
82
+ set(path, value) {
83
+ const config = loadConfig();
84
+ const parts = path.split('.');
85
+ let obj = config;
86
+
87
+ for (let i = 0; i < parts.length - 1; i++) {
88
+ if (!obj[parts[i]]) obj[parts[i]] = {};
89
+ obj = obj[parts[i]];
90
+ }
91
+
92
+ // Auto-convert types
93
+ if (value === 'true') value = true;
94
+ else if (value === 'false') value = false;
95
+ else if (!isNaN(value) && value !== '') value = Number(value);
96
+
97
+ obj[parts[parts.length - 1]] = value;
98
+ saveConfig(config);
99
+ this.engine.config = config;
100
+
101
+ logger.info('config', `Set ${path} = ${JSON.stringify(value)}`);
102
+ return { path, value };
103
+ }
104
+
105
+ // ── Get config value ──
106
+
107
+ get(path) {
108
+ const config = loadConfig();
109
+ const parts = path.split('.');
110
+ let obj = config;
111
+
112
+ for (const part of parts) {
113
+ if (obj === undefined) return undefined;
114
+ obj = obj[part];
115
+ }
116
+
117
+ return obj;
118
+ }
119
+
120
+ // ── Profiles ──
121
+
122
+ saveProfile(name) {
123
+ const configPath = join(this.home, 'config.json');
124
+ const profilePath = join(this.home, `config.${name}.json`);
125
+ copyFileSync(configPath, profilePath);
126
+ return profilePath;
127
+ }
128
+
129
+ loadProfile(name) {
130
+ const profilePath = join(this.home, `config.${name}.json`);
131
+ if (!existsSync(profilePath)) throw new Error('Profile not found: ' + name);
132
+
133
+ const configPath = join(this.home, 'config.json');
134
+ copyFileSync(profilePath, configPath);
135
+ this.engine.config = loadConfig();
136
+ return true;
137
+ }
138
+
139
+ listProfiles() {
140
+ const { readdirSync } = require('fs');
141
+ return readdirSync(this.home)
142
+ .filter(f => f.startsWith('config.') && f.endsWith('.json') && f !== 'config.json')
143
+ .map(f => f.replace('config.', '').replace('.json', ''));
144
+ }
145
+
146
+ // ── Validation ──
147
+
148
+ async validate() {
149
+ const config = loadConfig();
150
+ const results = [];
151
+
152
+ // Check AI providers
153
+ const providers = config.ai?.providers || {};
154
+ for (const [name, prov] of Object.entries(providers)) {
155
+ if (!prov.key || prov.key === 'local') continue;
156
+
157
+ try {
158
+ const valid = await this._testProvider(name, prov.key);
159
+ results.push({ name, status: valid ? 'āœ…' : 'āŒ', message: valid ? 'Valid' : 'Invalid key' });
160
+ } catch (err) {
161
+ results.push({ name, status: 'āŒ', message: err.message });
162
+ }
163
+ }
164
+
165
+ // Check Telegram
166
+ if (config.channels?.telegram?.token) {
167
+ try {
168
+ const res = await fetch(`https://api.telegram.org/bot${config.channels.telegram.token}/getMe`);
169
+ const data = await res.json();
170
+ results.push({ name: 'Telegram', status: data.ok ? 'āœ…' : 'āŒ', message: data.ok ? '@' + data.result.username : 'Invalid token' });
171
+ } catch (err) {
172
+ results.push({ name: 'Telegram', status: 'āŒ', message: err.message });
173
+ }
174
+ }
175
+
176
+ // Check agents
177
+ const agentsDir = join(this.home, 'agents');
178
+ if (existsSync(agentsDir)) {
179
+ const { readdirSync } = require('fs');
180
+ const count = readdirSync(agentsDir, { withFileTypes: true }).filter(d => d.isDirectory()).length;
181
+ results.push({ name: 'Agents', status: 'āœ…', message: count + ' configured' });
182
+ }
183
+
184
+ return results;
185
+ }
186
+
187
+ async _testProvider(name, key) {
188
+ const urls = {
189
+ anthropic: 'https://api.anthropic.com/v1/models',
190
+ openai: 'https://api.openai.com/v1/models',
191
+ google: `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`,
192
+ groq: 'https://api.groq.com/openai/v1/models',
193
+ };
194
+
195
+ const url = urls[name];
196
+ if (!url) return true; // Can't validate, assume OK
197
+
198
+ const headers = {};
199
+ if (name === 'anthropic') {
200
+ headers['x-api-key'] = key;
201
+ headers['anthropic-version'] = '2023-06-01';
202
+ } else if (name !== 'google') {
203
+ headers['Authorization'] = 'Bearer ' + key;
204
+ }
205
+
206
+ const res = await fetch(url, { headers });
207
+ return res.ok;
208
+ }
209
+
210
+ // ── Chat command parsing ──
211
+
212
+ static parseConfigCommand(message) {
213
+ const lower = message.toLowerCase().trim();
214
+
215
+ // "switch to gpt-4o" / "use gemini"
216
+ const modelMatch = lower.match(/(?:switch|change|use)\s+(?:to\s+)?(?:model\s+)?(.+)/);
217
+ if (modelMatch) {
218
+ const model = modelMatch[1].trim();
219
+ // Map common names to actual model IDs
220
+ const aliases = {
221
+ 'gpt4': 'gpt-4o', 'gpt-4': 'gpt-4o', 'gpt4o': 'gpt-4o',
222
+ 'claude': 'claude-sonnet-4-20250514', 'sonnet': 'claude-sonnet-4-20250514',
223
+ 'opus': 'claude-opus-4', 'haiku': 'claude-haiku-3-5',
224
+ 'gemini': 'gemini-2.5-flash', 'flash': 'gemini-2.5-flash',
225
+ 'llama': 'llama-3.3-70b-versatile', 'deepseek': 'deepseek-chat',
226
+ };
227
+ return { action: 'set_model', model: aliases[model] || model };
228
+ }
229
+
230
+ // "set temperature to 0.8"
231
+ const setMatch = lower.match(/set\s+(\w[\w.]*)\s+(?:to\s+)?(.+)/);
232
+ if (setMatch) {
233
+ return { action: 'set', path: setMatch[1], value: setMatch[2] };
234
+ }
235
+
236
+ return null;
237
+ }
238
+ }
@@ -16,7 +16,8 @@ export async function commandsMiddleware(ctx, next) {
16
16
  '/memories — what I remember about you',
17
17
  '/tasks — your todo list',
18
18
  '/usage — spending report',
19
- '/exec <cmd> — run a shell command',
19
+ '/config — manage settings',
20
+ '/exec <cmd> — run a shell command',
20
21
  '/files — list sandbox files',
21
22
  '/subagents — list background tasks',
22
23
  '/help — this message',
@@ -120,6 +121,71 @@ export async function commandsMiddleware(ctx, next) {
120
121
  return;
121
122
  }
122
123
 
124
+ if (cmd === '/config') {
125
+ const args = msg.slice(8).trim();
126
+ if (!args || args === 'help') {
127
+ await ctx.reply([
128
+ 'āš™ļø *Config Commands*', '',
129
+ '/config get <path> — show value',
130
+ '/config set <path> <value> — set value',
131
+ '/config test — validate all connections',
132
+ '/config profiles — list saved profiles',
133
+ '/config save <name> — save current config',
134
+ '/config load <name> — switch profile',
135
+ '', 'Or just say: "switch to gemini" šŸ¦‘',
136
+ ].join('\n'));
137
+ return;
138
+ }
139
+
140
+ const cm = ctx.engine.configManager;
141
+ if (!cm) { await ctx.reply('āŒ Config manager not available'); return; }
142
+
143
+ if (args.startsWith('get ')) {
144
+ const val = cm.get(args.slice(4).trim());
145
+ await ctx.reply('āš™ļø ' + args.slice(4).trim() + ' = ' + JSON.stringify(val));
146
+ return;
147
+ }
148
+
149
+ if (args.startsWith('set ')) {
150
+ const parts = args.slice(4).trim().split(/\s+/);
151
+ const path = parts[0];
152
+ const value = parts.slice(1).join(' ');
153
+ cm.set(path, value);
154
+ await ctx.reply('āœ… Set *' + path + '* = ' + value);
155
+ return;
156
+ }
157
+
158
+ if (args === 'test' || args === 'validate') {
159
+ await ctx.reply('šŸ” Validating connections...');
160
+ const results = await cm.validate();
161
+ const lines = results.map(r => r.status + ' *' + r.name + '* — ' + r.message);
162
+ await ctx.reply('āš™ļø *Config Validation*\n\n' + lines.join('\n'));
163
+ return;
164
+ }
165
+
166
+ if (args === 'profiles') {
167
+ const profiles = cm.listProfiles();
168
+ await ctx.reply(profiles.length ? 'šŸ“‹ Profiles: ' + profiles.join(', ') : 'šŸ“‹ No saved profiles');
169
+ return;
170
+ }
171
+
172
+ if (args.startsWith('save ')) {
173
+ cm.saveProfile(args.slice(5).trim());
174
+ await ctx.reply('āœ… Profile saved: ' + args.slice(5).trim());
175
+ return;
176
+ }
177
+
178
+ if (args.startsWith('load ')) {
179
+ try {
180
+ cm.loadProfile(args.slice(5).trim());
181
+ await ctx.reply('āœ… Profile loaded: ' + args.slice(5).trim() + '\nāš ļø Restart recommended');
182
+ } catch (err) {
183
+ await ctx.reply('āŒ ' + err.message);
184
+ }
185
+ return;
186
+ }
187
+ }
188
+
123
189
  if (cmd === '/exec') {
124
190
  const command = msg.slice(6).trim();
125
191
  if (!command) { await ctx.reply('Usage: /exec <command>'); return; }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Handle config changes via natural language
3
+ * "switch to gemini", "use gpt-4o", "set temperature to 0.8"
4
+ */
5
+ import { logger } from '../core/logger.js';
6
+
7
+ export async function configChatMiddleware(ctx, next) {
8
+ if (!ctx.engine.configManager) { await next(); return; }
9
+
10
+ const { ConfigManager } = await import('../features/config-manager.js');
11
+ const parsed = ConfigManager.parseConfigCommand(ctx.message);
12
+
13
+ if (!parsed) { await next(); return; }
14
+
15
+ try {
16
+ if (parsed.action === 'set_model') {
17
+ ctx.engine.configManager.set('ai.defaultModel', parsed.model);
18
+ // Update agent's model too
19
+ if (ctx.agent) ctx.agent.model = parsed.model;
20
+ await ctx.reply('āœ… Model switched to *' + parsed.model + '*\n\nI will use this model from now on šŸ¦‘');
21
+ return;
22
+ }
23
+
24
+ if (parsed.action === 'set') {
25
+ ctx.engine.configManager.set(parsed.path, parsed.value);
26
+ await ctx.reply('āœ… Set *' + parsed.path + '* = ' + parsed.value);
27
+ return;
28
+ }
29
+ } catch (err) {
30
+ await ctx.reply('āŒ Config error: ' + err.message);
31
+ return;
32
+ }
33
+
34
+ await next();
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "šŸ¦‘ AI agent platform — human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {