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,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
|
+
}
|
package/lib/cli/setup.js
ADDED
|
@@ -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
|
+
}
|