squidclaw 0.8.3 → 1.0.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/ai/prompt-builder.js +39 -3
- package/lib/api/dashboard.js +167 -0
- package/lib/channels/telegram/bot.js +13 -0
- package/lib/cli/agent-cmd.js +107 -138
- package/lib/cli/update-cmd.js +44 -16
- package/lib/core/agent-tools-mixin.js +16 -0
- package/lib/engine.js +192 -7
- package/lib/features/auto-links.js +36 -0
- package/lib/features/auto-memory.js +107 -0
- package/lib/features/doc-ingest.js +103 -0
- package/lib/features/reminders.js +144 -0
- package/lib/features/usage-alerts.js +49 -0
- package/lib/features/voice-reply.js +55 -0
- package/lib/tools/router.js +23 -0
- package/package.json +3 -3
package/lib/ai/prompt-builder.js
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
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;
|
package/lib/cli/agent-cmd.js
CHANGED
|
@@ -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 {
|
|
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
|
|
10
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
36
|
+
const language = await p.select({
|
|
19
37
|
message: 'Language:',
|
|
20
38
|
options: [
|
|
21
|
-
{ value: '
|
|
22
|
-
{ value: '
|
|
23
|
-
{ value: '
|
|
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
|
|
29
|
-
message: '
|
|
45
|
+
const personality = await p.select({
|
|
46
|
+
message: 'Personality:',
|
|
30
47
|
options: [
|
|
31
|
-
{ value:
|
|
32
|
-
{ value:
|
|
33
|
-
{ value:
|
|
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
|
|
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
|
-
|
|
43
|
-
const
|
|
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
|
-
|
|
73
|
+
writeFileSync(join(agentDir, 'agent.json'), JSON.stringify(manifest, null, 2));
|
|
46
74
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
console.log(chalk.
|
|
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
|
-
|
|
111
|
+
async function listAgents() {
|
|
67
112
|
const home = getHome();
|
|
68
113
|
const agentsDir = join(home, 'agents');
|
|
69
|
-
if (!existsSync(agentsDir))
|
|
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
|
-
}
|
|
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
|
|
94
|
-
console.log(` ${
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
181
|
-
|
|
149
|
+
rmSync(agentDir, { recursive: true });
|
|
150
|
+
console.log(chalk.green(`✅ Agent ${id} removed.`));
|
|
182
151
|
}
|
package/lib/cli/update-cmd.js
CHANGED
|
@@ -1,25 +1,53 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
execSync('npm
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
console.log(chalk.yellow(
|
|
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;
|
|
@@ -53,6 +58,17 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
|
|
|
53
58
|
const fullResponse = result.messages.join('\n');
|
|
54
59
|
const toolResult = await toolRouter.processResponse(fullResponse, agent.id);
|
|
55
60
|
|
|
61
|
+
if (toolResult.toolUsed && toolResult.toolName === 'remind' && toolResult.reminderTime) {
|
|
62
|
+
// Reminder requested — store it via engine's reminder manager
|
|
63
|
+
result._reminder = {
|
|
64
|
+
time: toolResult.reminderTime,
|
|
65
|
+
message: toolResult.reminderMessage,
|
|
66
|
+
contactId,
|
|
67
|
+
};
|
|
68
|
+
result.messages = ['Done! I will remind you at that time ⏰'];
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
if (toolResult.toolUsed && (toolResult.imageBase64 || toolResult.imageUrl)) {
|
|
57
73
|
// Image generated — pass through directly
|
|
58
74
|
result.image = { base64: toolResult.imageBase64, url: toolResult.imageUrl, mimeType: toolResult.mimeType };
|