squidclaw 0.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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/squidclaw.js +512 -0
- package/lib/ai/gateway.js +283 -0
- package/lib/ai/prompt-builder.js +149 -0
- package/lib/api/server.js +235 -0
- package/lib/behavior/engine.js +187 -0
- package/lib/channels/hub-media.js +128 -0
- package/lib/channels/hub.js +89 -0
- package/lib/channels/whatsapp/manager.js +319 -0
- package/lib/channels/whatsapp/media.js +228 -0
- package/lib/cli/agent-cmd.js +182 -0
- package/lib/cli/brain-cmd.js +49 -0
- package/lib/cli/broadcast-cmd.js +28 -0
- package/lib/cli/channels-cmd.js +157 -0
- package/lib/cli/config-cmd.js +26 -0
- package/lib/cli/conversations-cmd.js +27 -0
- package/lib/cli/engine-cmd.js +115 -0
- package/lib/cli/handoff-cmd.js +26 -0
- package/lib/cli/hours-cmd.js +38 -0
- package/lib/cli/key-cmd.js +62 -0
- package/lib/cli/knowledge-cmd.js +59 -0
- package/lib/cli/memory-cmd.js +50 -0
- package/lib/cli/platform-cmd.js +51 -0
- package/lib/cli/setup.js +226 -0
- package/lib/cli/stats-cmd.js +66 -0
- package/lib/cli/tui.js +308 -0
- package/lib/cli/update-cmd.js +25 -0
- package/lib/cli/webhook-cmd.js +40 -0
- package/lib/core/agent-manager.js +83 -0
- package/lib/core/agent.js +162 -0
- package/lib/core/config.js +172 -0
- package/lib/core/logger.js +43 -0
- package/lib/engine.js +117 -0
- package/lib/features/heartbeat.js +71 -0
- package/lib/storage/interface.js +56 -0
- package/lib/storage/sqlite.js +409 -0
- package/package.json +48 -0
- package/templates/BEHAVIOR.md +42 -0
- package/templates/IDENTITY.md +7 -0
- package/templates/RULES.md +9 -0
- package/templates/SOUL.md +19 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
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
|
+
import crypto from 'crypto';
|
|
7
|
+
import { createInterface } from 'readline';
|
|
8
|
+
|
|
9
|
+
export async function createAgent(opts) {
|
|
10
|
+
const home = getHome();
|
|
11
|
+
|
|
12
|
+
const name = opts.name || await p.text({ message: 'Agent name:', placeholder: 'Luna' });
|
|
13
|
+
if (p.isCancel(name)) return;
|
|
14
|
+
|
|
15
|
+
const purpose = opts.soul || await p.text({ message: 'What does this agent do?', placeholder: 'Customer support for my restaurant' });
|
|
16
|
+
if (p.isCancel(purpose)) return;
|
|
17
|
+
|
|
18
|
+
const language = opts.language || await p.select({
|
|
19
|
+
message: 'Language:',
|
|
20
|
+
options: [
|
|
21
|
+
{ value: 'en', label: 'π¬π§ English' },
|
|
22
|
+
{ value: 'ar', label: 'πΈπ¦ Arabic' },
|
|
23
|
+
{ value: 'bilingual', label: 'π Bilingual' },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
if (p.isCancel(language)) return;
|
|
27
|
+
|
|
28
|
+
const tone = await p.select({
|
|
29
|
+
message: 'Tone:',
|
|
30
|
+
options: [
|
|
31
|
+
{ value: 30, label: 'π Formal' },
|
|
32
|
+
{ value: 50, label: 'π€ Professional but friendly' },
|
|
33
|
+
{ value: 80, label: 'π Casual and warm' },
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
if (p.isCancel(tone)) return;
|
|
37
|
+
|
|
38
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
39
|
+
const agentDir = join(home, 'agents', id);
|
|
40
|
+
mkdirSync(join(agentDir, 'memory'), { recursive: true });
|
|
41
|
+
|
|
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';
|
|
44
|
+
|
|
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`;
|
|
46
|
+
|
|
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));
|
|
56
|
+
|
|
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));
|
|
60
|
+
|
|
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`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function listAgents() {
|
|
67
|
+
const home = getHome();
|
|
68
|
+
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();
|
|
84
|
+
return;
|
|
85
|
+
} catch {}
|
|
86
|
+
|
|
87
|
+
// Fallback to filesystem
|
|
88
|
+
const dirs = readdirSync(agentsDir);
|
|
89
|
+
console.log(chalk.cyan(`\n π¦ Agents (${dirs.length})\n βββββββββ`));
|
|
90
|
+
for (const dir of dirs) {
|
|
91
|
+
const manifestPath = join(agentsDir, dir, 'agent.json');
|
|
92
|
+
if (existsSync(manifestPath)) {
|
|
93
|
+
const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
94
|
+
console.log(` ${m.name} (${m.id}) β ${m.model || 'default'} β ${m.status}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
|
|
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)`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function chatAgent(id) {
|
|
132
|
+
const config = loadConfig();
|
|
133
|
+
const port = config.engine?.port || 9500;
|
|
134
|
+
|
|
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'));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
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
|
+
};
|
|
179
|
+
|
|
180
|
+
rl.on('close', () => { console.log(chalk.gray('\n π Bye!\n')); process.exit(0); });
|
|
181
|
+
ask();
|
|
182
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, saveConfig, MODEL_MAP, MODEL_PRICING } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export async function listBrains() {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
console.log(chalk.cyan('\n π§ Available Models\n ββββββββββββββββββ'));
|
|
7
|
+
|
|
8
|
+
const groups = [
|
|
9
|
+
{ name: 'Anthropic', provider: 'anthropic', models: ['claude-opus-4', 'claude-sonnet-4', 'claude-haiku-3.5'] },
|
|
10
|
+
{ name: 'OpenAI', provider: 'openai', models: ['gpt-4o', 'gpt-4o-mini', 'o3', 'o4-mini'] },
|
|
11
|
+
{ name: 'Google', provider: 'google', models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'] },
|
|
12
|
+
{ name: 'Groq (free)', provider: 'groq', models: ['groq/llama-4-scout', 'groq/llama-4-maverick', 'groq/llama-3.3-70b'] },
|
|
13
|
+
{ name: 'Together (free)', provider: 'together', models: ['together/llama-4-scout', 'together/llama-4-maverick'] },
|
|
14
|
+
{ name: 'Cerebras (free)', provider: 'cerebras', models: ['cerebras/llama-3.3-70b'] },
|
|
15
|
+
{ name: 'Mistral', provider: 'mistral', models: ['mistral-large', 'mistral-medium', 'mistral-small'] },
|
|
16
|
+
{ name: 'Local', provider: 'ollama', models: ['ollama/local', 'lmstudio/local'] },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const group of groups) {
|
|
20
|
+
const hasKey = config.ai?.providers?.[group.provider]?.key;
|
|
21
|
+
const keyStatus = hasKey ? chalk.green('β
') : chalk.gray('no key');
|
|
22
|
+
console.log(`\n ${group.name} (${keyStatus})`);
|
|
23
|
+
for (const model of group.models) {
|
|
24
|
+
const pricing = MODEL_PRICING[model];
|
|
25
|
+
const priceStr = pricing ? (pricing.input === 0 ? chalk.green('free') : chalk.gray(`$${pricing.input}/$${pricing.output} per 1M`)) : '';
|
|
26
|
+
const current = model === config.ai?.defaultModel ? chalk.green(' β current') : '';
|
|
27
|
+
console.log(` βββ ${model} ${priceStr}${current}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
console.log();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function setBrain(model, opts) {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
config.ai.defaultModel = model;
|
|
36
|
+
saveConfig(config);
|
|
37
|
+
console.log(chalk.green(`β
Default model set to ${model}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function showBrain() {
|
|
41
|
+
const config = loadConfig();
|
|
42
|
+
console.log(chalk.cyan('\n π§ Current Brain'));
|
|
43
|
+
console.log(` Provider: ${config.ai?.defaultProvider || 'not set'}`);
|
|
44
|
+
console.log(` Model: ${config.ai?.defaultModel || 'not set'}`);
|
|
45
|
+
if (config.ai?.fallbackChain?.length) {
|
|
46
|
+
console.log(` Fallback: ${config.ai.fallbackChain.join(' β ')}`);
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export async function sendBroadcast(opts) {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const port = config.engine?.port || 9500;
|
|
7
|
+
|
|
8
|
+
// Get contacts for the agent
|
|
9
|
+
try {
|
|
10
|
+
const contactsRes = await fetch(`http://127.0.0.1:${port}/api/agents/${opts.agent}/contacts`);
|
|
11
|
+
const contacts = await contactsRes.json();
|
|
12
|
+
|
|
13
|
+
if (contacts.length === 0) {
|
|
14
|
+
return console.log(chalk.yellow('No contacts to broadcast to'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const contactIds = contacts.map(c => c.contact_id);
|
|
18
|
+
console.log(chalk.cyan(`Broadcasting to ${contactIds.length} contacts...`));
|
|
19
|
+
|
|
20
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/agents/${opts.agent}/broadcast`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ message: opts.message, contactIds }),
|
|
24
|
+
});
|
|
25
|
+
const result = await res.json();
|
|
26
|
+
console.log(chalk.green(`β
Sent: ${result.sent} | Failed: ${result.failed}`));
|
|
27
|
+
} catch { console.log(chalk.red('Engine not running')); }
|
|
28
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { loadConfig, getHome } from '../core/config.js';
|
|
4
|
+
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
export async function loginChannel(channel, opts) {
|
|
8
|
+
if (channel !== 'whatsapp') {
|
|
9
|
+
console.log(chalk.yellow(`${channel} support coming soon π¦`));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const port = config.engine?.port || 9500;
|
|
15
|
+
const home = getHome();
|
|
16
|
+
|
|
17
|
+
// Find agent
|
|
18
|
+
let agentId = opts.agent;
|
|
19
|
+
if (!agentId) {
|
|
20
|
+
const agentsDir = join(home, 'agents');
|
|
21
|
+
if (existsSync(agentsDir)) {
|
|
22
|
+
const dirs = readdirSync(agentsDir).filter(d => existsSync(join(agentsDir, d, 'agent.json')));
|
|
23
|
+
if (dirs.length === 1) {
|
|
24
|
+
agentId = dirs[0];
|
|
25
|
+
} else if (dirs.length > 1) {
|
|
26
|
+
const choices = dirs.map(d => {
|
|
27
|
+
const m = JSON.parse(readFileSync(join(agentsDir, d, 'agent.json'), 'utf8'));
|
|
28
|
+
return { value: d, label: `${m.name} (${d})` };
|
|
29
|
+
});
|
|
30
|
+
agentId = await p.select({ message: 'Which agent?', options: choices });
|
|
31
|
+
if (p.isCancel(agentId)) return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!agentId) return console.log(chalk.red('No agents found. Run: squidclaw agent create'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Choose method
|
|
38
|
+
const method = opts.method || await p.select({
|
|
39
|
+
message: 'Login method:',
|
|
40
|
+
options: [
|
|
41
|
+
{ value: 'pairing', label: 'π± Pairing Code (enter phone number)' },
|
|
42
|
+
{ value: 'qr', label: 'π· QR Code (scan with camera)' },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
if (p.isCancel(method)) return;
|
|
46
|
+
|
|
47
|
+
if (method === 'pairing') {
|
|
48
|
+
const phone = await p.text({
|
|
49
|
+
message: 'π± Phone number (with country code):',
|
|
50
|
+
placeholder: '+966 5XX XXX XXXX',
|
|
51
|
+
validate: v => v.replace(/[^0-9+]/g, '').length < 8 ? 'Enter a valid number' : undefined,
|
|
52
|
+
});
|
|
53
|
+
if (p.isCancel(phone)) return;
|
|
54
|
+
|
|
55
|
+
const spinner = p.spinner();
|
|
56
|
+
spinner.start('Requesting pairing code...');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/whatsapp/pair`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ phone: phone.replace(/[^0-9+]/g, '') }),
|
|
63
|
+
});
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
|
|
66
|
+
if (data.pairingCode) {
|
|
67
|
+
spinner.stop('Pairing code ready!');
|
|
68
|
+
console.log(chalk.cyan(`\n π± Pairing Code: ${chalk.bold.white(data.pairingCode)}`));
|
|
69
|
+
console.log(chalk.gray(`\n Open WhatsApp β Settings β Linked Devices β Link with phone number`));
|
|
70
|
+
console.log(chalk.gray(` Enter the code above\n`));
|
|
71
|
+
|
|
72
|
+
// Poll for connection
|
|
73
|
+
const pollSpinner = p.spinner();
|
|
74
|
+
pollSpinner.start('Waiting for you to enter the code...');
|
|
75
|
+
for (let i = 0; i < 60; i++) {
|
|
76
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
77
|
+
try {
|
|
78
|
+
const statusRes = await fetch(`http://127.0.0.1:${port}/api/whatsapp/status`);
|
|
79
|
+
const statuses = await statusRes.json();
|
|
80
|
+
if (statuses[agentId]?.connected) {
|
|
81
|
+
pollSpinner.stop(chalk.green('β
WhatsApp connected!'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
pollSpinner.stop(chalk.yellow('Timed out β check status with: squidclaw channels status'));
|
|
87
|
+
} else {
|
|
88
|
+
spinner.stop(chalk.red(`Failed: ${data.error || 'Unknown error'}`));
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
spinner.stop(chalk.red(`Error: ${err.message}`));
|
|
92
|
+
console.log(chalk.gray('Make sure the engine is running: squidclaw start'));
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// QR code method
|
|
96
|
+
const spinner = p.spinner();
|
|
97
|
+
spinner.start('Generating QR code...');
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/whatsapp/start`, { method: 'POST' });
|
|
101
|
+
spinner.stop('Check terminal for QR code β scan with WhatsApp β Linked Devices');
|
|
102
|
+
console.log(chalk.gray('Waiting for scan...'));
|
|
103
|
+
|
|
104
|
+
// Poll for connection
|
|
105
|
+
for (let i = 0; i < 60; i++) {
|
|
106
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
107
|
+
try {
|
|
108
|
+
const statusRes = await fetch(`http://127.0.0.1:${port}/api/whatsapp/status`);
|
|
109
|
+
const statuses = await statusRes.json();
|
|
110
|
+
if (statuses[agentId]?.connected) {
|
|
111
|
+
console.log(chalk.green('\nβ
WhatsApp connected!\n'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
spinner.stop(chalk.red(`Error: ${err.message}`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function channelStatus() {
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
const port = config.engine?.port || 9500;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/whatsapp/status`);
|
|
128
|
+
const statuses = await res.json();
|
|
129
|
+
|
|
130
|
+
const agentsRes = await fetch(`http://127.0.0.1:${port}/api/agents`);
|
|
131
|
+
const agents = await agentsRes.json();
|
|
132
|
+
|
|
133
|
+
console.log(chalk.cyan('\n π‘ Channel Status\n ββββββββββββββββ'));
|
|
134
|
+
for (const agent of agents) {
|
|
135
|
+
const wa = statuses[agent.id];
|
|
136
|
+
const waStatus = wa?.connected ? chalk.green('β connected') : chalk.red('β disconnected');
|
|
137
|
+
console.log(` ${agent.name}: WhatsApp ${waStatus}`);
|
|
138
|
+
}
|
|
139
|
+
console.log();
|
|
140
|
+
} catch {
|
|
141
|
+
console.log(chalk.red('Engine not running. Start with: squidclaw start'));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function logoutChannel(channel, opts) {
|
|
146
|
+
const config = loadConfig();
|
|
147
|
+
const port = config.engine?.port || 9500;
|
|
148
|
+
const agentId = opts.agent;
|
|
149
|
+
if (!agentId) return console.log(chalk.red('Specify agent: --agent <id>'));
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/whatsapp/stop`, { method: 'POST' });
|
|
153
|
+
console.log(chalk.green('β
WhatsApp disconnected'));
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.log(chalk.red(`Error: ${err.message}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, setConfigValue } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export async function getConfig(key) {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
if (key) {
|
|
7
|
+
const val = key.split('.').reduce((obj, k) => obj?.[k], config);
|
|
8
|
+
console.log(typeof val === 'object' ? JSON.stringify(val, null, 2) : val);
|
|
9
|
+
} else {
|
|
10
|
+
console.log(JSON.stringify(config, null, 2));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function setConfig(key, value) {
|
|
15
|
+
setConfigValue(key, value);
|
|
16
|
+
console.log(chalk.green(`β
${key} = ${value}`));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function editConfig() {
|
|
20
|
+
const { getHome } = await import('../core/config.js');
|
|
21
|
+
const { join } = await import('path');
|
|
22
|
+
const configPath = join(getHome(), 'config.json');
|
|
23
|
+
const editor = process.env.EDITOR || 'nano';
|
|
24
|
+
const { execSync } = await import('child_process');
|
|
25
|
+
execSync(`${editor} ${configPath}`, { stdio: 'inherit' });
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export async function listConversations(opts) {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const port = config.engine?.port || 9500;
|
|
7
|
+
const agentId = opts.agent;
|
|
8
|
+
if (!agentId) return console.log(chalk.red('Specify agent: --agent <id>'));
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/contacts`);
|
|
12
|
+
const contacts = await res.json();
|
|
13
|
+
console.log(chalk.cyan(`\n π¬ Conversations (${contacts.length})\n ββββββββββββββ`));
|
|
14
|
+
for (const c of contacts.slice(0, parseInt(opts.limit) || 20)) {
|
|
15
|
+
console.log(` ${c.name || c.contact_id} β ${c.message_count} msgs β last: ${c.last_seen}`);
|
|
16
|
+
}
|
|
17
|
+
console.log();
|
|
18
|
+
} catch { console.log(chalk.red('Engine not running')); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function searchConversations(query, opts) {
|
|
22
|
+
console.log(chalk.yellow('Search coming soon π¦'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function exportConversations(opts) {
|
|
26
|
+
console.log(chalk.yellow('Export coming soon π¦'));
|
|
27
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π¦ Engine CLI Commands
|
|
3
|
+
* start, stop, restart, status
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { loadConfig, getHome } from '../core/config.js';
|
|
8
|
+
import { SquidclawEngine } from '../engine.js';
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
export async function start(opts) {
|
|
13
|
+
const engine = new SquidclawEngine({ port: parseInt(opts.port) || 9500 });
|
|
14
|
+
|
|
15
|
+
// Load agents from filesystem into DB on first start
|
|
16
|
+
await engine.start();
|
|
17
|
+
|
|
18
|
+
// Auto-register agents from filesystem
|
|
19
|
+
const agentsDir = join(getHome(), 'agents');
|
|
20
|
+
if (existsSync(agentsDir)) {
|
|
21
|
+
for (const dir of readdirSync(agentsDir)) {
|
|
22
|
+
const manifestPath = join(agentsDir, dir, 'agent.json');
|
|
23
|
+
if (existsSync(manifestPath)) {
|
|
24
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
25
|
+
const existing = await engine.storage.getAgent(manifest.id);
|
|
26
|
+
if (!existing) {
|
|
27
|
+
// Read soul from file
|
|
28
|
+
const soulPath = join(agentsDir, dir, 'SOUL.md');
|
|
29
|
+
if (existsSync(soulPath)) {
|
|
30
|
+
manifest.soul = readFileSync(soulPath, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
const behaviorPath = join(agentsDir, dir, 'BEHAVIOR.md');
|
|
33
|
+
if (existsSync(behaviorPath)) {
|
|
34
|
+
try { manifest.behavior = JSON.parse(readFileSync(behaviorPath, 'utf8')); } catch {}
|
|
35
|
+
}
|
|
36
|
+
await engine.agentManager.create(manifest);
|
|
37
|
+
console.log(` π¦ Registered agent "${manifest.name}" from filesystem`);
|
|
38
|
+
|
|
39
|
+
// Start WhatsApp if configured
|
|
40
|
+
if (manifest.whatsappNumber) {
|
|
41
|
+
try {
|
|
42
|
+
await engine.whatsappManager.startSession(manifest.id);
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function stop() {
|
|
52
|
+
console.log(chalk.yellow('π¦ Stopping Squidclaw...'));
|
|
53
|
+
try {
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
const port = config.engine?.port || 9500;
|
|
56
|
+
await fetch(`http://127.0.0.1:${port}/health`);
|
|
57
|
+
// TODO: send shutdown signal
|
|
58
|
+
console.log(chalk.green('β
Stopped'));
|
|
59
|
+
} catch {
|
|
60
|
+
console.log(chalk.gray('Engine is not running'));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function restart() {
|
|
65
|
+
await stop();
|
|
66
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
67
|
+
await start({ port: '9500' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function status() {
|
|
71
|
+
const config = loadConfig();
|
|
72
|
+
const port = config.engine?.port || 9500;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
|
|
78
|
+
console.log(chalk.cyan(`\n π¦ Squidclaw Status\n βββββββββββββββββ`));
|
|
79
|
+
console.log(` Status: ${chalk.green('β Running')}`);
|
|
80
|
+
console.log(` Port: ${port}`);
|
|
81
|
+
console.log(` Agents: ${data.agents}`);
|
|
82
|
+
console.log(` WhatsApp: ${data.whatsapp} connected`);
|
|
83
|
+
console.log(` Uptime: ${formatUptime(data.uptime)}`);
|
|
84
|
+
console.log(` Model: ${config.ai?.defaultModel || 'not set'}`);
|
|
85
|
+
console.log(` Provider: ${config.ai?.defaultProvider || 'not set'}`);
|
|
86
|
+
console.log();
|
|
87
|
+
|
|
88
|
+
// Get agent details
|
|
89
|
+
const agentsRes = await fetch(`http://127.0.0.1:${port}/api/agents`);
|
|
90
|
+
const agents = await agentsRes.json();
|
|
91
|
+
if (agents.length > 0) {
|
|
92
|
+
console.log(` Agents:`);
|
|
93
|
+
for (const a of agents) {
|
|
94
|
+
const waIcon = a.whatsappConnected ? chalk.green('π±') : chalk.gray('π±');
|
|
95
|
+
console.log(` ${waIcon} ${a.name} (${a.id}) β ${a.model || 'default'} β ${a.status}`);
|
|
96
|
+
}
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
console.log(chalk.cyan(`\n π¦ Squidclaw Status\n βββββββββββββββββ`));
|
|
101
|
+
console.log(` Status: ${chalk.red('β Stopped')}`);
|
|
102
|
+
console.log(` Start: ${chalk.cyan('squidclaw start')}\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatUptime(seconds) {
|
|
107
|
+
const d = Math.floor(seconds / 86400);
|
|
108
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
109
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
110
|
+
const parts = [];
|
|
111
|
+
if (d > 0) parts.push(`${d}d`);
|
|
112
|
+
if (h > 0) parts.push(`${h}h`);
|
|
113
|
+
parts.push(`${m}m`);
|
|
114
|
+
return parts.join(' ');
|
|
115
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export async function listHandoffs() {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const port = config.engine?.port || 9500;
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/handoffs`);
|
|
9
|
+
const handoffs = await res.json();
|
|
10
|
+
console.log(chalk.cyan(`\n π€ Pending Handoffs (${handoffs.length})\n ββββββββββββββ`));
|
|
11
|
+
for (const h of handoffs) {
|
|
12
|
+
console.log(` #${h.id} β Agent: ${h.agent_id} β Contact: ${h.contact_id} β ${h.reason}`);
|
|
13
|
+
}
|
|
14
|
+
if (handoffs.length === 0) console.log(chalk.gray(' No pending handoffs'));
|
|
15
|
+
console.log();
|
|
16
|
+
} catch { console.log(chalk.red('Engine not running')); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function resolveHandoff(id) {
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const port = config.engine?.port || 9500;
|
|
22
|
+
try {
|
|
23
|
+
await fetch(`http://127.0.0.1:${port}/api/handoffs/${id}/resolve`, { method: 'POST' });
|
|
24
|
+
console.log(chalk.green(`β
Handoff #${id} resolved β agent is back in control`));
|
|
25
|
+
} catch { console.log(chalk.red('Engine not running')); }
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export async function setHours(schedule, opts) {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const port = config.engine?.port || 9500;
|
|
7
|
+
const agentId = opts.agent;
|
|
8
|
+
if (!agentId) return console.log(chalk.red('Specify agent: --agent <id>'));
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}`, {
|
|
12
|
+
method: 'PUT',
|
|
13
|
+
headers: { 'content-type': 'application/json' },
|
|
14
|
+
body: JSON.stringify({ businessHours: schedule, timezone: opts.timezone || 'UTC' }),
|
|
15
|
+
});
|
|
16
|
+
console.log(chalk.green(`β
Business hours set: ${schedule} (${opts.timezone || 'UTC'})`));
|
|
17
|
+
} catch { console.log(chalk.red('Engine not running')); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function showHours(opts) {
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
const port = config.engine?.port || 9500;
|
|
23
|
+
const agentId = opts.agent;
|
|
24
|
+
if (!agentId) return console.log(chalk.red('Specify agent: --agent <id>'));
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}`);
|
|
28
|
+
const agent = await res.json();
|
|
29
|
+
console.log(chalk.cyan(`\n β° Business Hours`));
|
|
30
|
+
console.log(` Schedule: ${agent.businessHours || '24/7'}`);
|
|
31
|
+
console.log(` Timezone: ${agent.timezone || 'UTC'}\n`);
|
|
32
|
+
} catch { console.log(chalk.red('Engine not running')); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function setAlwaysOn(opts) {
|
|
36
|
+
await setHours(null, opts);
|
|
37
|
+
console.log(chalk.green('β
24/7 mode β no business hours'));
|
|
38
|
+
}
|