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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/bin/squidclaw.js +512 -0
  4. package/lib/ai/gateway.js +283 -0
  5. package/lib/ai/prompt-builder.js +149 -0
  6. package/lib/api/server.js +235 -0
  7. package/lib/behavior/engine.js +187 -0
  8. package/lib/channels/hub-media.js +128 -0
  9. package/lib/channels/hub.js +89 -0
  10. package/lib/channels/whatsapp/manager.js +319 -0
  11. package/lib/channels/whatsapp/media.js +228 -0
  12. package/lib/cli/agent-cmd.js +182 -0
  13. package/lib/cli/brain-cmd.js +49 -0
  14. package/lib/cli/broadcast-cmd.js +28 -0
  15. package/lib/cli/channels-cmd.js +157 -0
  16. package/lib/cli/config-cmd.js +26 -0
  17. package/lib/cli/conversations-cmd.js +27 -0
  18. package/lib/cli/engine-cmd.js +115 -0
  19. package/lib/cli/handoff-cmd.js +26 -0
  20. package/lib/cli/hours-cmd.js +38 -0
  21. package/lib/cli/key-cmd.js +62 -0
  22. package/lib/cli/knowledge-cmd.js +59 -0
  23. package/lib/cli/memory-cmd.js +50 -0
  24. package/lib/cli/platform-cmd.js +51 -0
  25. package/lib/cli/setup.js +226 -0
  26. package/lib/cli/stats-cmd.js +66 -0
  27. package/lib/cli/tui.js +308 -0
  28. package/lib/cli/update-cmd.js +25 -0
  29. package/lib/cli/webhook-cmd.js +40 -0
  30. package/lib/core/agent-manager.js +83 -0
  31. package/lib/core/agent.js +162 -0
  32. package/lib/core/config.js +172 -0
  33. package/lib/core/logger.js +43 -0
  34. package/lib/engine.js +117 -0
  35. package/lib/features/heartbeat.js +71 -0
  36. package/lib/storage/interface.js +56 -0
  37. package/lib/storage/sqlite.js +409 -0
  38. package/package.json +48 -0
  39. package/templates/BEHAVIOR.md +42 -0
  40. package/templates/IDENTITY.md +7 -0
  41. package/templates/RULES.md +9 -0
  42. 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
+ }