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,62 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, saveConfig } from '../core/config.js';
3
+
4
+ export async function setKey(provider, apiKey) {
5
+ const config = loadConfig();
6
+ if (!config.ai.providers[provider]) {
7
+ console.log(chalk.red(`Unknown provider: ${provider}`));
8
+ console.log(`Available: anthropic, openai, google, groq, together, cerebras, openrouter, mistral, ollama, lmstudio`);
9
+ return;
10
+ }
11
+ config.ai.providers[provider].key = apiKey;
12
+ if (!config.ai.defaultProvider) config.ai.defaultProvider = provider;
13
+ saveConfig(config);
14
+ console.log(chalk.green(`✅ ${provider} key saved`));
15
+ }
16
+
17
+ export async function testKey(opts) {
18
+ const config = loadConfig();
19
+ const provider = opts.provider || config.ai?.defaultProvider;
20
+ if (!provider) return console.log(chalk.red('No provider configured'));
21
+
22
+ const key = config.ai.providers[provider]?.key;
23
+ if (!key) return console.log(chalk.red(`No key set for ${provider}`));
24
+
25
+ console.log(`Testing ${provider}...`);
26
+ try {
27
+ // Quick test with a simple request
28
+ const { AIGateway } = await import('../ai/gateway.js');
29
+ const gw = new AIGateway(config);
30
+ const result = await gw.chat([
31
+ { role: 'system', content: 'Reply with just: ok' },
32
+ { role: 'user', content: 'test' },
33
+ ], { model: config.ai.defaultModel });
34
+ console.log(chalk.green(`✅ ${provider} is working! (${result.model})`));
35
+ } catch (err) {
36
+ console.log(chalk.red(`❌ Failed: ${err.message}`));
37
+ }
38
+ }
39
+
40
+ export async function showKeys() {
41
+ const config = loadConfig();
42
+ console.log(chalk.cyan('\n 🔑 API Keys\n ───────────'));
43
+ for (const [name, conf] of Object.entries(config.ai.providers)) {
44
+ if (conf.key && conf.key !== 'local') {
45
+ const masked = conf.key.slice(0, 10) + '...' + conf.key.slice(-4);
46
+ const isDefault = name === config.ai.defaultProvider ? chalk.green(' ← default') : '';
47
+ console.log(` ${name}: ${masked}${isDefault}`);
48
+ } else if (conf.key === 'local') {
49
+ console.log(` ${chalk.gray(name)}: local`);
50
+ }
51
+ }
52
+ console.log();
53
+ }
54
+
55
+ export async function removeKey(provider) {
56
+ const config = loadConfig();
57
+ if (config.ai.providers[provider]) {
58
+ config.ai.providers[provider].key = null;
59
+ saveConfig(config);
60
+ console.log(chalk.green(`✅ ${provider} key removed`));
61
+ }
62
+ }
@@ -0,0 +1,59 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig } from '../core/config.js';
3
+ import { readFileSync, statSync } from 'fs';
4
+ import { basename, extname } from 'path';
5
+
6
+ export async function uploadKnowledge(file, opts) {
7
+ const config = loadConfig();
8
+ const port = config.engine?.port || 9500;
9
+ const agentId = opts.agent;
10
+ if (!agentId) return console.log(chalk.red('Specify agent: --agent <id>'));
11
+
12
+ try {
13
+ const content = readFileSync(file, 'utf8');
14
+ const stat = statSync(file);
15
+ const filename = basename(file);
16
+ const fileType = extname(file).slice(1);
17
+
18
+ const res = await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/knowledge`, {
19
+ method: 'POST',
20
+ headers: { 'content-type': 'application/json' },
21
+ body: JSON.stringify({ filename, fileType, content, fileSize: stat.size }),
22
+ });
23
+
24
+ if (res.ok) {
25
+ console.log(chalk.green(`✅ "${filename}" uploaded to ${agentId}'s knowledge base`));
26
+ } else {
27
+ const err = await res.json();
28
+ console.log(chalk.red(`Failed: ${err.error}`));
29
+ }
30
+ } catch (err) {
31
+ console.log(chalk.red(`Error: ${err.message}`));
32
+ }
33
+ }
34
+
35
+ export async function listKnowledge(opts) {
36
+ const config = loadConfig();
37
+ const port = config.engine?.port || 9500;
38
+ const agentId = opts.agent;
39
+ if (!agentId) return console.log(chalk.red('Specify agent: --agent <id>'));
40
+
41
+ try {
42
+ const res = await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/knowledge`);
43
+ const docs = await res.json();
44
+ console.log(chalk.cyan(`\n 📚 Knowledge Base (${docs.length} docs)\n ───────────────`));
45
+ for (const doc of docs) {
46
+ console.log(` ${doc.filename} (${doc.file_type}) — ${doc.chunk_count} chunks — ${doc.status}`);
47
+ }
48
+ console.log();
49
+ } catch { console.log(chalk.red('Engine not running')); }
50
+ }
51
+
52
+ export async function deleteKnowledge(docId) {
53
+ const config = loadConfig();
54
+ const port = config.engine?.port || 9500;
55
+ try {
56
+ await fetch(`http://127.0.0.1:${port}/api/knowledge/${docId}`, { method: 'DELETE' });
57
+ console.log(chalk.green('✅ Document deleted'));
58
+ } catch { console.log(chalk.red('Engine not running')); }
59
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig } from '../core/config.js';
3
+
4
+ export async function showMemory(agentId) {
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/agents/${agentId}/memory`);
9
+ const memories = await res.json();
10
+ console.log(chalk.cyan(`\n 🧠 Memory for ${agentId}\n ───────────────`));
11
+ if (memories.length === 0) {
12
+ console.log(chalk.gray(' No memories yet'));
13
+ }
14
+ for (const m of memories) {
15
+ console.log(` ${chalk.yellow(m.key)}: ${m.value}`);
16
+ }
17
+ console.log();
18
+ } catch { console.log(chalk.red('Engine not running')); }
19
+ }
20
+
21
+ export async function clearMemory(agentId, opts) {
22
+ if (!opts.confirm) {
23
+ const { default: p } = await import('@clack/prompts');
24
+ // Simple confirmation
25
+ console.log(chalk.yellow(`This will clear all memories for agent ${agentId}`));
26
+ }
27
+ const config = loadConfig();
28
+ const port = config.engine?.port || 9500;
29
+ try {
30
+ await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/memory`, { method: 'DELETE' });
31
+ console.log(chalk.green('✅ Memory cleared'));
32
+ } catch { console.log(chalk.red('Engine not running')); }
33
+ }
34
+
35
+ export async function exportMemory(agentId, opts) {
36
+ const config = loadConfig();
37
+ const port = config.engine?.port || 9500;
38
+ try {
39
+ const res = await fetch(`http://127.0.0.1:${port}/api/agents/${agentId}/memory`);
40
+ const memories = await res.json();
41
+ const output = JSON.stringify(memories, null, 2);
42
+ if (opts.output) {
43
+ const { writeFileSync } = await import('fs');
44
+ writeFileSync(opts.output, output);
45
+ console.log(chalk.green(`✅ Exported to ${opts.output}`));
46
+ } else {
47
+ console.log(output);
48
+ }
49
+ } catch { console.log(chalk.red('Engine not running')); }
50
+ }
@@ -0,0 +1,51 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, saveConfig } from '../core/config.js';
3
+
4
+ export async function initPlatform(opts) {
5
+ if (!opts.supabaseUrl || !opts.supabaseKey) {
6
+ console.log(chalk.red('Required: --supabase-url <url> --supabase-key <key>'));
7
+ return;
8
+ }
9
+ const config = loadConfig();
10
+ config.engine.mode = 'platform';
11
+ config.platform.enabled = true;
12
+ config.storage.type = 'supabase';
13
+ config.storage.supabase.url = opts.supabaseUrl;
14
+ config.storage.supabase.key = opts.supabaseKey;
15
+ saveConfig(config);
16
+ console.log(chalk.green('✅ Platform mode enabled with Supabase'));
17
+ }
18
+
19
+ export async function platformAgents() {
20
+ const config = loadConfig();
21
+ const port = config.engine?.port || 9500;
22
+ try {
23
+ const res = await fetch(`http://127.0.0.1:${port}/api/agents`);
24
+ const agents = await res.json();
25
+ console.log(chalk.cyan(`\n 🏢 Platform Agents (${agents.length})\n ──────────────`));
26
+ for (const a of agents) {
27
+ console.log(` ${a.name} (${a.id}) — ${a.model} — WA: ${a.whatsappConnected ? '✅' : '❌'}`);
28
+ }
29
+ console.log();
30
+ } catch { console.log(chalk.red('Engine not running')); }
31
+ }
32
+
33
+ export async function platformUsage() {
34
+ const config = loadConfig();
35
+ const port = config.engine?.port || 9500;
36
+ try {
37
+ const res = await fetch(`http://127.0.0.1:${port}/api/usage`);
38
+ const data = await res.json();
39
+ console.log(chalk.cyan('\n 📊 Platform Usage\n ─────────────'));
40
+ let total = { tokens: 0, cost: 0, msgs: 0 };
41
+ for (const row of (Array.isArray(data) ? data : [])) {
42
+ const tokens = (row.input_tokens || 0) + (row.output_tokens || 0);
43
+ total.tokens += tokens;
44
+ total.cost += row.cost_usd || 0;
45
+ total.msgs += row.messages || 0;
46
+ console.log(` ${row.agent_id}: ${row.messages} msgs, ${tokens} tokens, $${(row.cost_usd||0).toFixed(4)}`);
47
+ }
48
+ console.log(chalk.gray(' ─────────────'));
49
+ console.log(` Total: ${total.msgs} msgs, ${total.tokens} tokens, $${total.cost.toFixed(4)}\n`);
50
+ } catch { console.log(chalk.red('Engine not running')); }
51
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * 🦑 Setup Wizard
3
+ * Interactive first-time setup
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import chalk from 'chalk';
8
+ import { loadConfig, saveConfig, ensureHome, getHome } from '../core/config.js';
9
+ import { writeFileSync, mkdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ import crypto from 'crypto';
12
+
13
+ export async function setup() {
14
+ console.clear();
15
+ p.intro(chalk.cyan('🦑 Welcome to Squidclaw!'));
16
+
17
+ const config = loadConfig();
18
+ ensureHome();
19
+
20
+ // Step 1: AI Provider
21
+ const provider = await p.select({
22
+ message: 'Choose your AI provider:',
23
+ options: [
24
+ { value: 'anthropic', label: '🟣 Anthropic (Claude)', hint: 'recommended' },
25
+ { value: 'openai', label: '🟢 OpenAI (GPT-4o)' },
26
+ { value: 'google', label: '🔵 Google (Gemini)', hint: 'free tier available' },
27
+ { value: 'groq', label: '⚡ Groq (Llama)', hint: 'free, very fast' },
28
+ { value: 'cerebras', label: '🧠 Cerebras (Llama)', hint: 'free, fastest' },
29
+ { value: 'ollama', label: '🏠 Ollama (Local)', hint: 'runs on your machine, no key needed' },
30
+ ],
31
+ });
32
+ if (p.isCancel(provider)) return p.cancel('Setup cancelled');
33
+
34
+ // Step 2: API Key
35
+ let apiKey = 'local';
36
+ if (provider !== 'ollama' && provider !== 'lmstudio') {
37
+ const keyHint = {
38
+ anthropic: 'Get key at console.anthropic.com',
39
+ openai: 'Get key at platform.openai.com',
40
+ google: 'Get key at aistudio.google.dev',
41
+ groq: 'Get key at console.groq.com (free)',
42
+ cerebras: 'Get key at cloud.cerebras.ai (free)',
43
+ };
44
+
45
+ apiKey = await p.text({
46
+ message: `API Key: ${chalk.gray(keyHint[provider] || '')}`,
47
+ placeholder: 'sk-...',
48
+ validate: (v) => v.length < 5 ? 'Key is too short' : undefined,
49
+ });
50
+ if (p.isCancel(apiKey)) return p.cancel('Setup cancelled');
51
+ }
52
+
53
+ // Step 3: Model
54
+ const modelOptions = {
55
+ anthropic: [
56
+ { value: 'claude-sonnet-4', label: 'Claude Sonnet 4', hint: 'balanced — recommended' },
57
+ { value: 'claude-opus-4', label: 'Claude Opus 4', hint: 'most powerful' },
58
+ { value: 'claude-haiku-3.5', label: 'Claude Haiku 3.5', hint: 'fastest, cheapest' },
59
+ ],
60
+ openai: [
61
+ { value: 'gpt-4o', label: 'GPT-4o', hint: 'recommended' },
62
+ { value: 'gpt-4o-mini', label: 'GPT-4o Mini', hint: 'cheaper' },
63
+ ],
64
+ google: [
65
+ { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', hint: 'recommended' },
66
+ { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', hint: 'most powerful' },
67
+ ],
68
+ groq: [
69
+ { value: 'groq/llama-4-scout', label: 'Llama 4 Scout', hint: 'free, fast' },
70
+ { value: 'groq/llama-4-maverick', label: 'Llama 4 Maverick', hint: 'free, powerful' },
71
+ ],
72
+ cerebras: [
73
+ { value: 'cerebras/llama-3.3-70b', label: 'Llama 3.3 70B', hint: 'free, fastest' },
74
+ ],
75
+ ollama: [
76
+ { value: 'ollama/llama3.3', label: 'Llama 3.3 (local)' },
77
+ ],
78
+ };
79
+
80
+ const model = await p.select({
81
+ message: 'Choose your model:',
82
+ options: modelOptions[provider] || modelOptions.openai,
83
+ });
84
+ if (p.isCancel(model)) return p.cancel('Setup cancelled');
85
+
86
+ // Step 4: Create first agent
87
+ p.note('Let\'s create your first AI agent');
88
+
89
+ const agentName = await p.text({
90
+ message: 'What should your agent be called?',
91
+ placeholder: 'Luna',
92
+ validate: (v) => v.length < 1 ? 'Name is required' : undefined,
93
+ });
94
+ if (p.isCancel(agentName)) return p.cancel('Setup cancelled');
95
+
96
+ const agentPurpose = await p.text({
97
+ message: 'What does this agent do?',
98
+ placeholder: 'Customer support for my coffee shop',
99
+ });
100
+ if (p.isCancel(agentPurpose)) return p.cancel('Setup cancelled');
101
+
102
+ const language = await p.select({
103
+ message: 'Language:',
104
+ options: [
105
+ { value: 'en', label: '🇬🇧 English' },
106
+ { value: 'ar', label: '🇸🇦 Arabic' },
107
+ { value: 'bilingual', label: '🌍 Bilingual (Arabic + English)' },
108
+ ],
109
+ });
110
+ if (p.isCancel(language)) return p.cancel('Setup cancelled');
111
+
112
+ const tone = await p.select({
113
+ message: 'Tone:',
114
+ options: [
115
+ { value: 30, label: '👔 Formal' },
116
+ { value: 50, label: '🤝 Professional but friendly' },
117
+ { value: 80, label: '😊 Casual and warm' },
118
+ ],
119
+ });
120
+ if (p.isCancel(tone)) return p.cancel('Setup cancelled');
121
+
122
+ // Step 5: WhatsApp
123
+ const connectWA = await p.confirm({
124
+ message: 'Connect WhatsApp now?',
125
+ initialValue: true,
126
+ });
127
+ if (p.isCancel(connectWA)) return p.cancel('Setup cancelled');
128
+
129
+ // Save config
130
+ config.ai.defaultProvider = provider;
131
+ config.ai.defaultModel = model;
132
+ config.ai.providers[provider].key = apiKey;
133
+
134
+ if (connectWA) {
135
+ config.channels.whatsapp.enabled = true;
136
+ }
137
+
138
+ saveConfig(config);
139
+
140
+ // Create agent files
141
+ const agentId = crypto.randomUUID().slice(0, 8);
142
+ const agentDir = join(getHome(), 'agents', agentId);
143
+ mkdirSync(join(agentDir, 'memory'), { recursive: true });
144
+
145
+ const soul = `# ${agentName}
146
+
147
+ ## Who I Am
148
+ I'm ${agentName}. ${agentPurpose || 'I\'m a helpful AI assistant.'}
149
+
150
+ ## How I Speak
151
+ - Language: ${language === 'ar' ? 'Arabic' : language === 'bilingual' ? 'Bilingual — I match whatever language the person uses' : 'English'}
152
+ - Tone: ${tone > 60 ? 'Casual and friendly. I text like a real person.' : tone > 30 ? 'Professional but warm. Friendly without being too casual.' : 'Formal and polished. I maintain professionalism.'}
153
+ - Platform: WhatsApp — short messages, natural, human-like
154
+ - I use emojis naturally but don't overdo it
155
+
156
+ ## What I Do
157
+ ${agentPurpose || 'Help people with their questions and tasks.'}
158
+
159
+ ## What I Never Do
160
+ - Say "As an AI" or "I'd be happy to help" or "Great question!"
161
+ - Send walls of text — I keep it short and conversational
162
+ - Make things up when I don't know — I say "let me check" instead
163
+ `;
164
+
165
+ writeFileSync(join(agentDir, 'SOUL.md'), soul);
166
+ writeFileSync(join(agentDir, 'IDENTITY.md'), `# ${agentName}\n- Name: ${agentName}\n- Language: ${language}\n- Emoji: 🦑\n`);
167
+ writeFileSync(join(agentDir, 'MEMORY.md'), `# ${agentName}'s Memory\n\n_Learning about people and building relationships._\n`);
168
+ writeFileSync(join(agentDir, 'RULES.md'), `# Rules\n\n1. Be helpful, be human, be honest\n2. Keep messages short — this is WhatsApp, not email\n3. If unsure, say so instead of making things up\n`);
169
+ writeFileSync(join(agentDir, 'BEHAVIOR.md'), JSON.stringify({
170
+ splitMessages: true,
171
+ maxChunkLength: 200,
172
+ reactBeforeReply: true,
173
+ reactOnlyEndings: true,
174
+ autoDetectLanguage: true,
175
+ avoidPhrases: [
176
+ "Is there anything else I can help with?",
177
+ "I'd be happy to help!",
178
+ "Great question!",
179
+ "As an AI",
180
+ ],
181
+ handoff: { enabled: false },
182
+ heartbeat: '30m',
183
+ }, null, 2));
184
+
185
+ // Save agent to a manifest file for the engine to pick up
186
+ const manifest = {
187
+ id: agentId,
188
+ name: agentName,
189
+ soul,
190
+ language,
191
+ tone,
192
+ model,
193
+ status: 'active',
194
+ createdAt: new Date().toISOString(),
195
+ };
196
+ writeFileSync(join(agentDir, 'agent.json'), JSON.stringify(manifest, null, 2));
197
+
198
+ p.note(`Agent "${agentName}" created in ${agentDir}`);
199
+
200
+ // WhatsApp pairing
201
+ if (connectWA) {
202
+ const phone = await p.text({
203
+ message: '📱 Your WhatsApp phone number:',
204
+ placeholder: '+966 5XX XXX XXXX',
205
+ validate: (v) => v.replace(/[^0-9+]/g, '').length < 8 ? 'Enter a valid phone number' : undefined,
206
+ });
207
+
208
+ if (!p.isCancel(phone)) {
209
+ p.note(`WhatsApp will connect when you run ${chalk.cyan('squidclaw start')}\nThen run ${chalk.cyan('squidclaw channels login whatsapp')} to link your account.`);
210
+
211
+ // Save phone to agent manifest
212
+ manifest.whatsappNumber = phone.replace(/[^0-9+]/g, '');
213
+ writeFileSync(join(agentDir, 'agent.json'), JSON.stringify(manifest, null, 2));
214
+ }
215
+ }
216
+
217
+ // Done!
218
+ p.outro(chalk.green(`
219
+ 🦑 Setup complete!
220
+
221
+ Start: ${chalk.cyan('squidclaw start')}
222
+ Status: ${chalk.cyan('squidclaw status')}
223
+ Chat: ${chalk.cyan(`squidclaw agent chat ${agentId}`)}
224
+ Docs: ${chalk.cyan('https://squidclaw.dev')}
225
+ `));
226
+ }
@@ -0,0 +1,66 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig } from '../core/config.js';
3
+
4
+ export async function showUsage(opts) {
5
+ const config = loadConfig();
6
+ const port = config.engine?.port || 9500;
7
+ const period = opts.period || '30d';
8
+
9
+ try {
10
+ const url = opts.agent
11
+ ? `http://127.0.0.1:${port}/api/usage?agentId=${opts.agent}&period=${period}`
12
+ : `http://127.0.0.1:${port}/api/usage?period=${period}`;
13
+ const res = await fetch(url);
14
+ const data = await res.json();
15
+
16
+ console.log(chalk.cyan(`\n 📊 Usage (${period})\n ──────────`));
17
+ if (Array.isArray(data)) {
18
+ let totalTokens = 0, totalCost = 0, totalMsgs = 0;
19
+ for (const row of data) {
20
+ const tokens = (row.input_tokens || 0) + (row.output_tokens || 0);
21
+ totalTokens += tokens;
22
+ totalCost += row.cost_usd || 0;
23
+ totalMsgs += row.messages || 0;
24
+ console.log(` ${row.agent_id}: ${row.messages} msgs, ${fmtNum(tokens)} tokens, $${(row.cost_usd || 0).toFixed(4)}`);
25
+ }
26
+ console.log(chalk.gray(` ──────────`));
27
+ console.log(` Total: ${totalMsgs} msgs, ${fmtNum(totalTokens)} tokens, $${totalCost.toFixed(4)}`);
28
+ } else {
29
+ const tokens = (data.input_tokens || 0) + (data.output_tokens || 0);
30
+ console.log(` Messages: ${data.messages || 0}`);
31
+ console.log(` Tokens: ${fmtNum(tokens)}`);
32
+ console.log(` Cost: $${(data.cost_usd || 0).toFixed(4)}`);
33
+ }
34
+ console.log();
35
+ } catch {
36
+ console.log(chalk.red('Engine not running'));
37
+ }
38
+ }
39
+
40
+ export async function showStats(opts) {
41
+ await showUsage(opts);
42
+ }
43
+
44
+ export async function showLogs(opts) {
45
+ const { createReadStream, existsSync } = await import('fs');
46
+ const { join } = await import('path');
47
+ const { getHome } = await import('../core/config.js');
48
+ const logFile = join(getHome(), 'logs', 'squidclaw.log');
49
+ if (!existsSync(logFile)) return console.log(chalk.gray('No logs yet'));
50
+
51
+ const { execSync } = await import('child_process');
52
+ const lines = opts.lines || '50';
53
+ if (opts.follow) {
54
+ const { spawn } = await import('child_process');
55
+ const tail = spawn('tail', ['-f', '-n', lines, logFile], { stdio: 'inherit' });
56
+ tail.on('exit', () => process.exit(0));
57
+ } else {
58
+ console.log(execSync(`tail -n ${lines} ${logFile}`, { encoding: 'utf8' }));
59
+ }
60
+ }
61
+
62
+ function fmtNum(n) {
63
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
64
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
65
+ return String(n);
66
+ }