squidclaw 0.9.0 → 1.1.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.
@@ -30,12 +30,48 @@ export class PromptBuilder {
30
30
  // 3. Long-term memories
31
31
  const memories = await this.storage.getMemories(agent.id);
32
32
  if (memories.length > 0) {
33
- parts.push('\n## What You Remember\n');
34
- for (const mem of memories.slice(0, 30)) {
35
- parts.push(`- ${mem.key}: ${mem.value}`);
33
+ parts.push('\n## What You Remember');
34
+ parts.push('These are facts you have learned from conversations. Use them naturally — do not list them unless asked.');
35
+ parts.push('');
36
+
37
+ // Group by type
38
+ const facts = memories.filter(m => m.type === 'fact' || m.type === 'auto');
39
+ const notes = memories.filter(m => m.type === 'noted');
40
+
41
+ if (facts.length > 0) {
42
+ for (const mem of facts.slice(0, 40)) {
43
+ parts.push(`- ${mem.key}: ${mem.value}`);
44
+ }
45
+ }
46
+ if (notes.length > 0) {
47
+ parts.push('\n### User Notes (they asked you to remember):');
48
+ for (const mem of notes.slice(0, 20)) {
49
+ parts.push(`- ${mem.value}`);
50
+ }
36
51
  }
37
52
  }
38
53
 
54
+ // Platform-specific formatting
55
+ parts.push('\n## Message Format');
56
+ parts.push('- Keep messages SHORT (1-3 sentences per message)');
57
+ parts.push('- Use ---SPLIT--- to break long responses into multiple messages');
58
+ parts.push('- NO markdown tables — use bullet lists instead');
59
+ parts.push('- Use *bold* for emphasis, not headers');
60
+ parts.push('- When sharing multiple items, split into separate messages');
61
+ parts.push('- If user says "ok", "thanks", "👍" — do NOT reply with a full message. Use ---REACT:❤️--- only');
62
+ parts.push('- Act first, ask later. Do not say "would you like me to..." — just do it');
63
+ parts.push('- Never start with "Great question!" or "I would be happy to help!"');
64
+
65
+ // Memory instructions
66
+ parts.push('\n## Memory Instructions');
67
+ parts.push('When you learn something new about the user, save it using: ---MEMORY:key:value---');
68
+ parts.push('Examples:');
69
+ parts.push(' ---MEMORY:name:Tamer---');
70
+ parts.push(' ---MEMORY:favorite_food:Sushi---');
71
+ parts.push(' ---MEMORY:project:Building Squidclaw platform---');
72
+ parts.push('Save important facts automatically. Do not tell the user you are saving — just do it silently.');
73
+ parts.push('If the user says "remember X" — save it and confirm briefly.');
74
+
39
75
  // 4. Contact context
40
76
  const contact = await this.storage.getContact(agent.id, contactId);
41
77
  if (contact) {
@@ -0,0 +1,167 @@
1
+ /**
2
+ * 🦑 Squidclaw Dashboard
3
+ * Minimal web UI for agent management
4
+ */
5
+
6
+ export function addDashboardRoutes(app, engine) {
7
+
8
+ // Dashboard HTML
9
+ app.get('/dashboard', (req, res) => {
10
+ res.send(DASHBOARD_HTML);
11
+ });
12
+
13
+ // API endpoints for dashboard
14
+ app.get('/api/dashboard/stats', async (req, res) => {
15
+ try {
16
+ const agents = engine.agentManager.listAll();
17
+ const stats = [];
18
+
19
+ for (const agent of agents) {
20
+ const usage = await engine.storage.getUsage(agent.id);
21
+ const convos = engine.storage.db.prepare(
22
+ 'SELECT COUNT(DISTINCT contact_id) as contacts FROM messages WHERE agent_id = ?'
23
+ ).get(agent.id);
24
+
25
+ stats.push({
26
+ id: agent.id,
27
+ name: agent.name,
28
+ model: agent.model,
29
+ messages: usage?.messages || 0,
30
+ tokens: (usage?.input_tokens || 0) + (usage?.output_tokens || 0),
31
+ cost: (usage?.cost_usd || 0).toFixed(4),
32
+ contacts: convos?.contacts || 0,
33
+ });
34
+ }
35
+
36
+ const uptime = process.uptime();
37
+ const memUsage = process.memoryUsage();
38
+
39
+ res.json({
40
+ uptime: Math.floor(uptime),
41
+ memory: Math.round(memUsage.rss / 1024 / 1024),
42
+ agents: stats,
43
+ channels: {
44
+ whatsapp: Object.values(engine.whatsappManager?.getStatuses() || {}).some(s => s.connected),
45
+ telegram: !!engine.telegramManager?.bots?.size,
46
+ },
47
+ });
48
+ } catch (err) {
49
+ res.status(500).json({ error: err.message });
50
+ }
51
+ });
52
+
53
+ app.get('/api/dashboard/conversations/:agentId', async (req, res) => {
54
+ try {
55
+ const contacts = engine.storage.db.prepare(
56
+ 'SELECT contact_id, MAX(created_at) as last_msg, COUNT(*) as msg_count FROM messages WHERE agent_id = ? GROUP BY contact_id ORDER BY last_msg DESC LIMIT 50'
57
+ ).all(req.params.agentId);
58
+ res.json(contacts);
59
+ } catch (err) {
60
+ res.status(500).json({ error: err.message });
61
+ }
62
+ });
63
+
64
+ app.get('/api/dashboard/memories/:agentId', async (req, res) => {
65
+ try {
66
+ const memories = await engine.storage.getMemories(req.params.agentId);
67
+ res.json(memories);
68
+ } catch (err) {
69
+ res.status(500).json({ error: err.message });
70
+ }
71
+ });
72
+ }
73
+
74
+ const DASHBOARD_HTML = `<!DOCTYPE html>
75
+ <html lang="en">
76
+ <head>
77
+ <meta charset="UTF-8">
78
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
79
+ <title>🦑 Squidclaw Dashboard</title>
80
+ <style>
81
+ * { margin: 0; padding: 0; box-sizing: border-box; }
82
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0a0f; color: #e0e0e0; }
83
+ .header { background: linear-gradient(135deg, #1a1a2e, #16213e); padding: 20px 30px; border-bottom: 1px solid #333; display: flex; align-items: center; justify-content: space-between; }
84
+ .header h1 { font-size: 24px; } .header h1 span { color: #00d4ff; }
85
+ .status-bar { display: flex; gap: 20px; font-size: 14px; color: #888; }
86
+ .status-bar .online { color: #4caf50; }
87
+ .container { max-width: 1200px; margin: 30px auto; padding: 0 20px; }
88
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 30px; }
89
+ .card { background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 20px; }
90
+ .card h3 { color: #00d4ff; margin-bottom: 15px; font-size: 16px; }
91
+ .stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #222; }
92
+ .stat:last-child { border: none; }
93
+ .stat .label { color: #888; }
94
+ .stat .value { color: #fff; font-weight: 600; }
95
+ .agent-card { background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 20px; margin-bottom: 15px; }
96
+ .agent-card h3 { color: #00d4ff; margin-bottom: 10px; }
97
+ .agent-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
98
+ .agent-stat { text-align: center; }
99
+ .agent-stat .num { font-size: 24px; font-weight: 700; color: #fff; }
100
+ .agent-stat .lbl { font-size: 12px; color: #888; margin-top: 4px; }
101
+ .pill { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; }
102
+ .pill.on { background: #1b5e20; color: #4caf50; }
103
+ .pill.off { background: #4a1010; color: #e57373; }
104
+ .refresh-btn { background: #00d4ff; color: #000; border: none; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-weight: 600; }
105
+ .refresh-btn:hover { background: #00b8d4; }
106
+ @media (max-width: 600px) { .agent-stats { grid-template-columns: repeat(2, 1fr); } }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div class="header">
111
+ <h1>🦑 <span>Squidclaw</span> Dashboard</h1>
112
+ <div style="display:flex;gap:10px;align-items:center">
113
+ <div class="status-bar">
114
+ <span id="uptime">--</span>
115
+ <span id="memory">--</span>
116
+ </div>
117
+ <button class="refresh-btn" onclick="load()">Refresh</button>
118
+ </div>
119
+ </div>
120
+ <div class="container">
121
+ <div class="grid">
122
+ <div class="card">
123
+ <h3>📡 Channels</h3>
124
+ <div class="stat"><span class="label">WhatsApp</span><span id="wa" class="value">--</span></div>
125
+ <div class="stat"><span class="label">Telegram</span><span id="tg" class="value">--</span></div>
126
+ </div>
127
+ <div class="card">
128
+ <h3>📊 System</h3>
129
+ <div class="stat"><span class="label">Uptime</span><span id="uptimeVal" class="value">--</span></div>
130
+ <div class="stat"><span class="label">Memory</span><span id="memVal" class="value">--</span></div>
131
+ <div class="stat"><span class="label">Agents</span><span id="agentCount" class="value">--</span></div>
132
+ </div>
133
+ </div>
134
+ <h2 style="margin-bottom:15px;color:#00d4ff">🤖 Agents</h2>
135
+ <div id="agents"></div>
136
+ </div>
137
+ <script>
138
+ async function load() {
139
+ const res = await fetch('/api/dashboard/stats');
140
+ const data = await res.json();
141
+ const h = Math.floor(data.uptime/3600), m = Math.floor((data.uptime%3600)/60);
142
+ document.getElementById('uptimeVal').textContent = h+'h '+m+'m';
143
+ document.getElementById('memVal').textContent = data.memory+' MB';
144
+ document.getElementById('agentCount').textContent = data.agents.length;
145
+ document.getElementById('wa').innerHTML = data.channels.whatsapp ? '<span class="pill on">Connected</span>' : '<span class="pill off">Disconnected</span>';
146
+ document.getElementById('tg').innerHTML = data.channels.telegram ? '<span class="pill on">Connected</span>' : '<span class="pill off">Disconnected</span>';
147
+
148
+ const agentsDiv = document.getElementById('agents');
149
+ agentsDiv.innerHTML = data.agents.map(a => \`
150
+ <div class="agent-card">
151
+ <h3>🤖 \${a.name || a.id}</h3>
152
+ <div style="margin-bottom:10px;color:#888;font-size:13px">Model: \${a.model} · ID: \${a.id.slice(0,8)}</div>
153
+ <div class="agent-stats">
154
+ <div class="agent-stat"><div class="num">\${a.messages}</div><div class="lbl">Messages</div></div>
155
+ <div class="agent-stat"><div class="num">\${a.contacts}</div><div class="lbl">Contacts</div></div>
156
+ <div class="agent-stat"><div class="num">\${fmtTokens(a.tokens)}</div><div class="lbl">Tokens</div></div>
157
+ <div class="agent-stat"><div class="num">$\${a.cost}</div><div class="lbl">Cost</div></div>
158
+ </div>
159
+ </div>
160
+ \`).join('');
161
+ }
162
+ function fmtTokens(n) { if(n>=1e6) return (n/1e6).toFixed(1)+'M'; if(n>=1e3) return (n/1e3).toFixed(1)+'K'; return n; }
163
+ load();
164
+ setInterval(load, 30000);
165
+ </script>
166
+ </body>
167
+ </html>`;
@@ -166,6 +166,19 @@ export class TelegramManager {
166
166
  } catch {} // Reactions might not be supported in all chats
167
167
  }
168
168
 
169
+ async sendVoice(agentId, contactId, audioBuffer, metadata = {}) {
170
+ const chatId = metadata.chatId || contactId.replace('tg_', '');
171
+ const botInfo = this.bots.get(agentId);
172
+ if (!botInfo?.bot) return;
173
+
174
+ try {
175
+ const { InputFile } = await import('grammy');
176
+ await botInfo.bot.api.sendVoice(chatId, new InputFile(audioBuffer, 'voice.ogg'));
177
+ } catch (err) {
178
+ logger.error('telegram', 'Failed to send voice: ' + err.message);
179
+ }
180
+ }
181
+
169
182
  async sendPhoto(agentId, contactId, photoData, caption, metadata = {}) {
170
183
  const chatId = metadata.chatId || contactId;
171
184
  const token = metadata.token;
@@ -1,182 +1,151 @@
1
+ /**
2
+ * 🦑 squidclaw agent — manage agents
3
+ */
4
+
1
5
  import * as p from '@clack/prompts';
2
6
  import chalk from 'chalk';
3
- import { loadConfig, getHome } from '../core/config.js';
4
- import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';
5
- import { join } from 'path';
6
7
  import crypto from 'crypto';
7
- import { createInterface } from 'readline';
8
+ import { loadConfig, saveConfig, getHome } from '../core/config.js';
9
+ import { mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from 'fs';
10
+ import { join } from 'path';
8
11
 
9
- export async function createAgent(opts) {
10
- const home = getHome();
12
+ export async function agentCommand(args) {
13
+ const sub = args[0];
14
+
15
+ switch (sub) {
16
+ case 'add': return addAgent();
17
+ case 'list': return listAgents();
18
+ case 'remove': return removeAgent(args[1]);
19
+ default:
20
+ console.log(chalk.cyan('🦑 Agent Commands:'));
21
+ console.log(' squidclaw agent add — Create a new agent');
22
+ console.log(' squidclaw agent list — List all agents');
23
+ console.log(' squidclaw agent remove — Remove an agent');
24
+ }
25
+ }
11
26
 
12
- const name = opts.name || await p.text({ message: 'Agent name:', placeholder: 'Luna' });
27
+ async function addAgent() {
28
+ p.intro(chalk.cyan('🦑 New Agent'));
29
+
30
+ const name = await p.text({ message: 'Agent name:', placeholder: 'Luna' });
13
31
  if (p.isCancel(name)) return;
14
32
 
15
- const purpose = opts.soul || await p.text({ message: 'What does this agent do?', placeholder: 'Customer support for my restaurant' });
33
+ const purpose = await p.text({ message: 'What does this agent do?', placeholder: 'Customer support for my shop' });
16
34
  if (p.isCancel(purpose)) return;
17
35
 
18
- const language = opts.language || await p.select({
36
+ const language = await p.select({
19
37
  message: 'Language:',
20
38
  options: [
21
- { value: 'en', label: '🇬🇧 English' },
22
- { value: 'ar', label: '🇸🇦 Arabic' },
23
- { value: 'bilingual', label: '🌍 Bilingual' },
39
+ { value: 'bilingual', label: 'Bilingual (Arabic + English)' },
40
+ { value: 'en', label: 'English' },
41
+ { value: 'ar', label: 'Arabic' },
24
42
  ],
25
43
  });
26
- if (p.isCancel(language)) return;
27
44
 
28
- const tone = await p.select({
29
- message: 'Tone:',
45
+ const personality = await p.select({
46
+ message: 'Personality:',
30
47
  options: [
31
- { value: 30, label: '👔 Formal' },
32
- { value: 50, label: '🤝 Professional but friendly' },
33
- { value: 80, label: '😊 Casual and warm' },
48
+ { value: 'friendly', label: 'Friendly & Helpful' },
49
+ { value: 'professional', label: 'Professional & Formal' },
50
+ { value: 'casual', label: 'Casual & Fun' },
51
+ { value: 'expert', label: 'Expert & Technical' },
34
52
  ],
35
53
  });
36
- if (p.isCancel(tone)) return;
37
54
 
38
- const id = crypto.randomUUID().slice(0, 8);
55
+ const config = loadConfig();
56
+ const id = crypto.randomBytes(4).toString('hex');
57
+ const home = getHome();
39
58
  const agentDir = join(home, 'agents', id);
59
+
40
60
  mkdirSync(join(agentDir, 'memory'), { recursive: true });
41
61
 
42
- const langDesc = language === 'ar' ? 'Arabic' : language === 'bilingual' ? 'Bilingual — match whatever language the person uses' : 'English';
43
- const toneDesc = tone > 60 ? 'Casual and friendly' : tone > 30 ? 'Professional but warm' : 'Formal and polished';
62
+ // Create agent manifest
63
+ const manifest = {
64
+ id,
65
+ name,
66
+ purpose,
67
+ language,
68
+ personality,
69
+ model: config.ai?.defaultModel || 'claude-sonnet-4-20250514',
70
+ createdAt: new Date().toISOString(),
71
+ };
44
72
 
45
- const soul = `# ${name}\n\n## Who I Am\nI'm ${name}. ${purpose}\n\n## How I Speak\n- Language: ${langDesc}\n- Tone: ${toneDesc}\n- Platform: WhatsApp — short messages, natural, human-like\n- I use emojis naturally but don't overdo it\n\n## What I Do\n${purpose}\n\n## What I Never Do\n- Say "As an AI" or "I'd be happy to help" or "Great question!"\n- Send walls of text\n- Make things up when I don't know\n`;
73
+ writeFileSync(join(agentDir, 'agent.json'), JSON.stringify(manifest, null, 2));
46
74
 
47
- writeFileSync(join(agentDir, 'SOUL.md'), soul);
48
- writeFileSync(join(agentDir, 'IDENTITY.md'), `# ${name}\n- Name: ${name}\n- Language: ${language}\n- Emoji: 🦑\n`);
49
- writeFileSync(join(agentDir, 'MEMORY.md'), `# ${name}'s Memory\n\n_Learning..._\n`);
50
- writeFileSync(join(agentDir, 'RULES.md'), `# Rules\n\n1. Be helpful, be human, be honest\n2. Keep messages short\n3. If unsure, say so\n`);
51
- writeFileSync(join(agentDir, 'BEHAVIOR.md'), JSON.stringify({
52
- splitMessages: true, maxChunkLength: 200, reactBeforeReply: true,
53
- autoDetectLanguage: true, avoidPhrases: ["Is there anything else I can help with?", "I'd be happy to help!", "Great question!", "As an AI"],
54
- handoff: { enabled: false }, heartbeat: '30m',
55
- }, null, 2));
75
+ // Create SOUL.md
76
+ const langInst = language === 'bilingual'
77
+ ? 'I speak Arabic and English fluently. I auto-detect what language the person uses and respond in the same language.'
78
+ : language === 'ar' ? 'I speak Arabic.' : 'I speak English.';
56
79
 
57
- const config = loadConfig();
58
- const manifest = { id, name, soul, language, tone, model: config.ai?.defaultModel, status: 'active', createdAt: new Date().toISOString() };
59
- writeFileSync(join(agentDir, 'agent.json'), JSON.stringify(manifest, null, 2));
80
+ const soul = `# ${name}
81
+
82
+ ## Who I Am
83
+ I am ${name}. ${purpose}.
84
+
85
+ ## How I Speak
86
+ - ${langInst}
87
+ - Short messages, natural, human-like
88
+ - Personality: ${personality}
89
+ - Emojis: natural but not overdone
90
+
91
+ ## My Skills
92
+ - Search the web for current information
93
+ - Read and summarize web pages
94
+ - Remember facts about people I talk to
95
+ - Set reminders
96
+
97
+ ## Never
98
+ - Say "As an AI" or "Great question!" or "I would be happy to help!"
99
+ - Send walls of text — keep it short
100
+ - Make things up — search the web if unsure
101
+ - Say I cannot access the internet — I can
102
+ `;
103
+
104
+ writeFileSync(join(agentDir, 'SOUL.md'), soul);
105
+ writeFileSync(join(agentDir, 'MEMORY.md'), `# ${name} Memory\n`);
60
106
 
61
- console.log(chalk.green(`\n✅ Agent "${name}" created (${id})`));
62
- console.log(chalk.gray(` Files: ${agentDir}`));
63
- console.log(chalk.gray(` Chat: squidclaw agent chat ${id}\n`));
107
+ p.outro(chalk.green(`✅ Agent "${name}" created! (ID: ${id})`));
108
+ console.log(chalk.dim(` Restart engine to load: squidclaw restart`));
64
109
  }
65
110
 
66
- export async function listAgents() {
111
+ async function listAgents() {
67
112
  const home = getHome();
68
113
  const agentsDir = join(home, 'agents');
69
- if (!existsSync(agentsDir)) return console.log(chalk.gray('No agents yet. Run: squidclaw agent create'));
70
-
71
- const config = loadConfig();
72
- const port = config.engine?.port || 9500;
73
-
74
- // Try API first
75
- try {
76
- const res = await fetch(`http://127.0.0.1:${port}/api/agents`);
77
- const agents = await res.json();
78
- console.log(chalk.cyan(`\n 🦑 Agents (${agents.length})\n ─────────`));
79
- for (const a of agents) {
80
- const wa = a.whatsappConnected ? chalk.green('📱 connected') : chalk.gray('📱 not linked');
81
- console.log(` ${a.name} (${a.id}) — ${a.model || 'default'} — ${wa}`);
82
- }
83
- console.log();
114
+ if (!existsSync(agentsDir)) {
115
+ console.log(chalk.yellow('No agents found.'));
84
116
  return;
85
- } catch {}
117
+ }
118
+
119
+ const dirs = readdirSync(agentsDir, { withFileTypes: true }).filter(d => d.isDirectory());
120
+ console.log(chalk.cyan(`\n🦑 ${dirs.length} Agent(s):\n`));
86
121
 
87
- // Fallback to filesystem
88
- const dirs = readdirSync(agentsDir);
89
- console.log(chalk.cyan(`\n 🦑 Agents (${dirs.length})\n ─────────`));
90
122
  for (const dir of dirs) {
91
- const manifestPath = join(agentsDir, dir, 'agent.json');
123
+ const manifestPath = join(agentsDir, dir.name, 'agent.json');
92
124
  if (existsSync(manifestPath)) {
93
- const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
94
- console.log(` ${m.name} (${m.id}) — ${m.model || 'default'} — ${m.status}`);
125
+ const manifest = JSON.parse((await import('fs')).readFileSync(manifestPath, 'utf8'));
126
+ console.log(` 🤖 ${manifest.name || dir.name}`);
127
+ console.log(chalk.dim(` ID: ${manifest.id} · Model: ${manifest.model} · Lang: ${manifest.language || '?'}`));
95
128
  }
96
129
  }
97
- console.log();
130
+ console.log('');
98
131
  }
99
132
 
100
- export async function editAgent(id) {
101
- const home = getHome();
102
- const agentDir = join(home, 'agents', id);
103
- const soulPath = join(agentDir, 'SOUL.md');
104
- if (!existsSync(soulPath)) return console.log(chalk.red(`Agent ${id} not found`));
105
-
106
- console.log(chalk.cyan(`Edit files in: ${agentDir}`));
107
- console.log(chalk.gray(` SOUL.md — personality`));
108
- console.log(chalk.gray(` RULES.md — hard rules`));
109
- console.log(chalk.gray(` BEHAVIOR.md — behavior config`));
110
- console.log(chalk.gray(` MEMORY.md — long-term memory\n`));
111
- }
112
-
113
- export async function deleteAgent(id) {
114
- const confirm = await p.confirm({ message: `Delete agent ${id}? This cannot be undone.` });
115
- if (!confirm || p.isCancel(confirm)) return;
116
-
117
- const config = loadConfig();
118
- const port = config.engine?.port || 9500;
119
- try {
120
- await fetch(`http://127.0.0.1:${port}/api/agents/${id}`, { method: 'DELETE' });
121
- console.log(chalk.green(`✅ Agent ${id} deleted`));
122
- } catch {
123
- // Fallback: delete from filesystem
124
- const { rmSync } = await import('fs');
125
- const agentDir = join(getHome(), 'agents', id);
126
- rmSync(agentDir, { recursive: true, force: true });
127
- console.log(chalk.green(`✅ Agent ${id} deleted (filesystem)`));
133
+ async function removeAgent(id) {
134
+ if (!id) {
135
+ console.log(chalk.red('Usage: squidclaw agent remove <agent-id>'));
136
+ return;
128
137
  }
129
- }
130
-
131
- export async function chatAgent(id) {
132
- const config = loadConfig();
133
- const port = config.engine?.port || 9500;
134
138
 
135
- // Check if engine is running
136
- try {
137
- await fetch(`http://127.0.0.1:${port}/health`);
138
- } catch {
139
- console.log(chalk.red('Engine not running. Start with: squidclaw start'));
139
+ const home = getHome();
140
+ const agentDir = join(home, 'agents', id);
141
+ if (!existsSync(agentDir)) {
142
+ console.log(chalk.red('Agent not found: ' + id));
140
143
  return;
141
144
  }
142
145
 
143
- // Get agent info
144
- let agentName = id;
145
- try {
146
- const res = await fetch(`http://127.0.0.1:${port}/api/agents/${id}`);
147
- const agent = await res.json();
148
- agentName = agent.name || id;
149
- } catch {}
150
-
151
- console.log(chalk.cyan(`\n 🦑 Chat with ${agentName}`));
152
- console.log(chalk.gray(` Type your message. Ctrl+C to exit.\n`));
153
-
154
- const rl = createInterface({ input: process.stdin, output: process.stdout });
155
-
156
- const ask = () => {
157
- rl.question(chalk.green('You: '), async (input) => {
158
- if (!input.trim()) return ask();
159
-
160
- try {
161
- const res = await fetch(`http://127.0.0.1:${port}/api/agents/${id}/chat`, {
162
- method: 'POST',
163
- headers: { 'content-type': 'application/json' },
164
- body: JSON.stringify({ message: input, contactId: 'terminal' }),
165
- });
166
- const data = await res.json();
167
- for (const msg of data.messages || []) {
168
- console.log(chalk.cyan(`${agentName}: `) + msg);
169
- }
170
- if (data.reaction) console.log(chalk.gray(` (reacted: ${data.reaction})`));
171
- if (data.usage) console.log(chalk.gray(` [${data.usage.inputTokens}+${data.usage.outputTokens} tokens, $${data.usage.cost?.toFixed(4) || 0}]`));
172
- } catch (err) {
173
- console.log(chalk.red(`Error: ${err.message}`));
174
- }
175
- console.log();
176
- ask();
177
- });
178
- };
146
+ const confirm = await p.confirm({ message: `Remove agent ${id}? This is permanent.` });
147
+ if (!confirm || p.isCancel(confirm)) return;
179
148
 
180
- rl.on('close', () => { console.log(chalk.gray('\n 👋 Bye!\n')); process.exit(0); });
181
- ask();
149
+ rmSync(agentDir, { recursive: true });
150
+ console.log(chalk.green(`✅ Agent ${id} removed.`));
182
151
  }
@@ -1,25 +1,53 @@
1
- import chalk from 'chalk';
1
+ /**
2
+ * 🦑 squidclaw update — self-update to latest version
3
+ */
4
+
2
5
  import { execSync } from 'child_process';
6
+ import chalk from 'chalk';
3
7
 
4
8
  export async function update() {
5
9
  console.log(chalk.cyan('🦑 Checking for updates...'));
10
+
6
11
  try {
7
- const current = execSync('npm view squidclaw version 2>/dev/null', { encoding: 'utf8' }).trim();
8
- const { readFileSync } = await import('fs');
9
- const { join, dirname } = await import('path');
10
- const { fileURLToPath } = await import('url');
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));
12
+ // Get current version
13
+ const pkg = JSON.parse((await import('fs')).readFileSync(
14
+ new URL('../../package.json', import.meta.url), 'utf8'
15
+ ));
16
+ const current = pkg.version;
13
17
 
14
- if (current && current !== pkg.version) {
15
- console.log(` Current: ${pkg.version} → Latest: ${current}`);
16
- console.log(chalk.cyan(' Updating...'));
17
- execSync('npm i -g squidclaw@latest', { stdio: 'inherit' });
18
- console.log(chalk.green(' ✅ Updated!'));
19
- } else {
20
- console.log(chalk.green(` ✅ Already on latest (${pkg.version})`));
18
+ // Get latest from npm
19
+ let latest;
20
+ try {
21
+ latest = execSync('npm view squidclaw version', { encoding: 'utf8' }).trim();
22
+ } catch {
23
+ latest = current;
21
24
  }
22
- } catch {
23
- console.log(chalk.yellow(' Could not check for updates. Try: npm i -g squidclaw@latest'));
25
+
26
+ console.log(` Current: ${chalk.yellow(current)}`);
27
+ console.log(` Latest: ${chalk.green(latest)}`);
28
+
29
+ if (current === latest) {
30
+ console.log(chalk.green('\n✅ Already up to date!'));
31
+ return;
32
+ }
33
+
34
+ console.log(chalk.cyan('\n📦 Updating...'));
35
+ execSync('npm i -g squidclaw@latest', { stdio: 'inherit' });
36
+
37
+ // Check if engine is running
38
+ try {
39
+ const res = await fetch('http://127.0.0.1:9500/health');
40
+ if (res.ok) {
41
+ console.log(chalk.cyan('\n🔄 Restarting engine...'));
42
+ execSync('pkill -f "squidclaw start"', { stdio: 'ignore' });
43
+ await new Promise(r => setTimeout(r, 2000));
44
+ execSync('nohup squidclaw start > /tmp/squidclaw.log 2>&1 &', { stdio: 'ignore' });
45
+ console.log(chalk.green('✅ Engine restarted!'));
46
+ }
47
+ } catch {}
48
+
49
+ console.log(chalk.green(`\n✅ Updated to v${latest}!`));
50
+ } catch (err) {
51
+ console.error(chalk.red('❌ Update failed:'), err.message);
24
52
  }
25
53
  }
@@ -29,6 +29,11 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
29
29
  agent.promptBuilder.build = async function(agentObj, cId, msg) {
30
30
  let prompt = await origBuild(agentObj, cId, msg);
31
31
 
32
+ // Add link context (auto-read URLs)
33
+ if (metadata._linkContext) {
34
+ prompt += '\n\n## Content From Links Shared\nThe user shared links. Here is the content:\n\n' + metadata._linkContext;
35
+ }
36
+
32
37
  // Add knowledge context
33
38
  if (metadata._knowledgeContext) {
34
39
  prompt += '\n\n## Relevant Knowledge\nUse this information to answer:\n\n' + metadata._knowledgeContext;
@@ -51,6 +56,8 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
51
56
  // Check if agent wants to use a tool
52
57
  if (toolRouter && result.messages.length > 0) {
53
58
  const fullResponse = result.messages.join('\n');
59
+ toolRouter._currentContactId = contactId;
60
+ toolRouter.storage = agent.storage;
54
61
  const toolResult = await toolRouter.processResponse(fullResponse, agent.id);
55
62
 
56
63
  if (toolResult.toolUsed && toolResult.toolName === 'remind' && toolResult.reminderTime) {