twinclaw 1.3.2 → 1.4.1
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/dist/api/handlers/browser.js +2 -1
- package/dist/api/handlers/debug.js +2 -1
- package/dist/api/router.js +2 -1
- package/dist/config/model-catalog.js +91 -0
- package/dist/core/channels-cli.js +3 -1
- package/dist/core/chat-commands.js +198 -0
- package/dist/core/command-router.js +290 -0
- package/dist/core/doctor.js +2 -2
- package/dist/core/logs-cli.js +12 -11
- package/dist/core/onboarding.js +5 -237
- package/dist/core/queue-cli.js +2 -2
- package/dist/core/status-cli.js +2 -2
- package/dist/interfaces/telegram_handler.js +4 -188
- package/dist/interfaces/whatsapp_handler.js +7 -132
- package/dist/services/db.js +20 -60
- package/dist/services/device-pairing.js +2 -1
- package/dist/services/dm-pairing.js +2 -1
- package/dist/services/hooks.js +12 -0
- package/dist/services/job-scheduler.js +3 -1
- package/dist/services/learning-system.js +4 -0
- package/dist/services/model-router.js +71 -2
- package/dist/services/semantic-memory.js +16 -26
- package/dist/skills/builtin.js +35 -0
- package/dist/tools/persona-editor.js +56 -0
- package/package.json +2 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BrowserReferenceError } from '../../services/browser-service.js';
|
|
2
2
|
import { sendOk, sendError, mapError } from '../shared.js';
|
|
3
3
|
import { logThought } from '../../utils/logger.js';
|
|
4
|
+
import { getWorkspaceDir } from '../../config/workspace.js';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { isIP } from 'node:net';
|
|
6
7
|
import { unlink } from 'node:fs/promises';
|
|
@@ -150,7 +151,7 @@ export function handleBrowserSnapshot(deps) {
|
|
|
150
151
|
await deps.browserService.navigate(validatedUrl.url);
|
|
151
152
|
await logThought(`[API] Browser navigated to: ${validatedUrl.url}`);
|
|
152
153
|
}
|
|
153
|
-
const screenshotPath = path.
|
|
154
|
+
const screenshotPath = path.join(getWorkspaceDir(), 'memory', `snapshot_${Date.now()}.png`);
|
|
154
155
|
const fullPage = body.fullPage !== false;
|
|
155
156
|
const result = await deps.browserService.takeScreenshotForVlm(screenshotPath, fullPage);
|
|
156
157
|
const tree = await deps.browserService.getAccessibilityTree();
|
|
@@ -2,6 +2,7 @@ import { sendOk, sendError } from '../shared.js';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
+
import { getWorkspaceDir } from '../../config/workspace.js';
|
|
5
6
|
/** GET /debug — Extended diagnostics including full context budget, recent logs, active connections, performance metrics */
|
|
6
7
|
export function handleDebug(deps) {
|
|
7
8
|
return async (req, res) => {
|
|
@@ -46,7 +47,7 @@ export function handleDebug(deps) {
|
|
|
46
47
|
}
|
|
47
48
|
async function getRecentLogs(limit) {
|
|
48
49
|
const dateIso = new Date().toISOString().slice(0, 10);
|
|
49
|
-
const logPath = path.
|
|
50
|
+
const logPath = path.join(getWorkspaceDir(), 'memory', `${dateIso}.md`);
|
|
50
51
|
let content = '';
|
|
51
52
|
try {
|
|
52
53
|
content = await readFile(logPath, 'utf8');
|
package/dist/api/router.js
CHANGED
|
@@ -16,6 +16,7 @@ import { logThought } from '../utils/logger.js';
|
|
|
16
16
|
import { getCallbackOutcomeCounts } from '../services/db-incidents.js';
|
|
17
17
|
import { readFile } from 'node:fs/promises';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
+
import { getWorkspaceDir } from '../config/workspace.js';
|
|
19
20
|
import { getConfigValue } from '../config/json-config.js';
|
|
20
21
|
import { getDevicePairingService } from '../services/device-pairing.js';
|
|
21
22
|
import { registerSelfRoutes } from './handlers/self-routes.js';
|
|
@@ -225,7 +226,7 @@ export function startApiServer(deps) {
|
|
|
225
226
|
app.get('/logs', async (_req, res) => {
|
|
226
227
|
try {
|
|
227
228
|
const dateIso = new Date().toISOString().slice(0, 10);
|
|
228
|
-
const logPath = path.
|
|
229
|
+
const logPath = path.join(getWorkspaceDir(), 'memory', `${dateIso}.md`);
|
|
229
230
|
let content = '';
|
|
230
231
|
try {
|
|
231
232
|
content = await readFile(logPath, 'utf8');
|
|
@@ -297,6 +297,97 @@ export const STATIC_MODEL_CATALOG = [
|
|
|
297
297
|
pricing: 'Free tier available',
|
|
298
298
|
description: 'Fast inference with Groq'
|
|
299
299
|
},
|
|
300
|
+
// Alias entries for twinclaw.json model IDs that differ from catalog IDs
|
|
301
|
+
{
|
|
302
|
+
id: 'groq-qwen-qwen3-32b',
|
|
303
|
+
name: 'Qwen 3 32B (Groq)',
|
|
304
|
+
provider: 'groq',
|
|
305
|
+
model: 'qwen/qwen3-32b',
|
|
306
|
+
contextLength: 32768,
|
|
307
|
+
supportsStreaming: true,
|
|
308
|
+
pricing: 'Free tier available',
|
|
309
|
+
description: 'Fast inference with Groq'
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'groq-moonshotai-kimi-k2-instruct-0905',
|
|
313
|
+
name: 'Kimi K2 Instruct (Groq)',
|
|
314
|
+
provider: 'groq',
|
|
315
|
+
model: 'moonshotai/kimi-k2-instruct-0905',
|
|
316
|
+
contextLength: 32768,
|
|
317
|
+
supportsStreaming: true,
|
|
318
|
+
pricing: 'Free tier available',
|
|
319
|
+
description: 'Fast inference with Groq'
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: 'openrouter-z-ai-glm-4-5-air-free',
|
|
323
|
+
name: 'GLM-4.5 Air (Free)',
|
|
324
|
+
provider: 'openrouter',
|
|
325
|
+
model: 'z-ai/glm-4.5-air:free',
|
|
326
|
+
contextLength: 32000,
|
|
327
|
+
supportsStreaming: true,
|
|
328
|
+
pricing: 'Free',
|
|
329
|
+
description: 'Free model via OpenRouter'
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: 'openrouter-stepfun-step-3-5-flash-free',
|
|
333
|
+
name: 'StepFun Flash (Free)',
|
|
334
|
+
provider: 'openrouter',
|
|
335
|
+
model: 'stepfun/step-3.5-flash:free',
|
|
336
|
+
contextLength: 32000,
|
|
337
|
+
supportsStreaming: true,
|
|
338
|
+
pricing: 'Free',
|
|
339
|
+
description: 'Free model via OpenRouter'
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: 'openrouter-qwen-qwen3-coder-free',
|
|
343
|
+
name: 'Qwen3 Coder (Free)',
|
|
344
|
+
provider: 'openrouter',
|
|
345
|
+
model: 'qwen/qwen3-coder:free',
|
|
346
|
+
contextLength: 32000,
|
|
347
|
+
supportsStreaming: true,
|
|
348
|
+
pricing: 'Free',
|
|
349
|
+
description: 'Free model via OpenRouter'
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 'openrouter-arcee-ai-trinity-mini-free',
|
|
353
|
+
name: 'Trinity Mini (Free)',
|
|
354
|
+
provider: 'openrouter',
|
|
355
|
+
model: 'arcee-ai/trinity-mini:free',
|
|
356
|
+
contextLength: 32000,
|
|
357
|
+
supportsStreaming: true,
|
|
358
|
+
pricing: 'Free',
|
|
359
|
+
description: 'Free model via OpenRouter'
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: 'openrouter-arcee-ai-trinity-large-preview-free',
|
|
363
|
+
name: 'Trinity Large Preview (Free)',
|
|
364
|
+
provider: 'openrouter',
|
|
365
|
+
model: 'arcee-ai/trinity-large-preview:free',
|
|
366
|
+
contextLength: 32000,
|
|
367
|
+
supportsStreaming: true,
|
|
368
|
+
pricing: 'Free',
|
|
369
|
+
description: 'Free model via OpenRouter'
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: 'copilot-gpt-4o',
|
|
373
|
+
name: 'GPT-4o (Copilot)',
|
|
374
|
+
provider: 'copilot',
|
|
375
|
+
model: 'gpt-4o',
|
|
376
|
+
contextLength: 128000,
|
|
377
|
+
supportsStreaming: true,
|
|
378
|
+
pricing: 'Included with Copilot',
|
|
379
|
+
description: 'GitHub Copilot model'
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: 'copilot-gemini-flash',
|
|
383
|
+
name: 'Gemini Flash (Copilot)',
|
|
384
|
+
provider: 'copilot',
|
|
385
|
+
model: 'gemini-2.0-flash',
|
|
386
|
+
contextLength: 1000000,
|
|
387
|
+
supportsStreaming: true,
|
|
388
|
+
pricing: 'Included with Copilot',
|
|
389
|
+
description: 'GitHub Copilot model'
|
|
390
|
+
},
|
|
300
391
|
// OpenRouter Free Models
|
|
301
392
|
{
|
|
302
393
|
id: 'openrouter-stepfun-flash',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import WAWebJS from 'whatsapp-web.js';
|
|
2
2
|
import qrcode from 'qrcode-terminal';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getWorkspaceDir } from '../config/workspace.js';
|
|
3
5
|
import { readConfig, writeConfig } from '../config/json-config.js';
|
|
4
6
|
const { Client, LocalAuth } = WAWebJS;
|
|
5
7
|
function printUsage() {
|
|
@@ -69,7 +71,7 @@ async function runWhatsappLogin() {
|
|
|
69
71
|
console.log('Initializing secure browser environment...');
|
|
70
72
|
const disableChromiumSandbox = process.env.WHATSAPP_DISABLE_CHROMIUM_SANDBOX === 'true';
|
|
71
73
|
const clientConfig = {
|
|
72
|
-
authStrategy: new LocalAuth({ dataPath: '
|
|
74
|
+
authStrategy: new LocalAuth({ dataPath: path.join(getWorkspaceDir(), 'memory', 'whatsapp_auth') }),
|
|
73
75
|
};
|
|
74
76
|
if (disableChromiumSandbox) {
|
|
75
77
|
clientConfig.puppeteer = {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from '../config/json-config.js';
|
|
2
|
+
import { getModelCatalogService } from '../services/model-catalog-service.js';
|
|
3
|
+
export const CHAT_COMMANDS = [
|
|
4
|
+
{ cmd: '/help', desc: 'Show all commands' },
|
|
5
|
+
{ cmd: '/status', desc: 'Show gateway status' },
|
|
6
|
+
{ cmd: '/models', desc: 'Show configured models' },
|
|
7
|
+
{ cmd: '/model list', desc: 'List available models in catalog' },
|
|
8
|
+
{ cmd: '/model set <name>', desc: 'Set primary model (e.g., /model set modal)' },
|
|
9
|
+
{ cmd: '/keys', desc: 'Show API keys status' },
|
|
10
|
+
{ cmd: '/key set <provider> <key>', desc: 'Set API key (e.g., /key set modal sk-xxx)' },
|
|
11
|
+
{ cmd: '/channel', desc: 'Show messaging channels status' },
|
|
12
|
+
{ cmd: '/clear', desc: 'Clear conversation' },
|
|
13
|
+
{ cmd: '/quit', desc: 'Exit chat mode (terminal only)' },
|
|
14
|
+
];
|
|
15
|
+
export async function handleChatCommand(command, platform) {
|
|
16
|
+
const parts = command.toLowerCase().trim().split(/\s+/);
|
|
17
|
+
const cmd = parts[0];
|
|
18
|
+
const args = command.trim().substring(cmd.length).trim();
|
|
19
|
+
switch (cmd) {
|
|
20
|
+
case '/start':
|
|
21
|
+
case '/menu':
|
|
22
|
+
return {
|
|
23
|
+
handled: true,
|
|
24
|
+
response: `🎯 *TwinClaw Menu*
|
|
25
|
+
|
|
26
|
+
${CHAT_COMMANDS.map((c) => `${c.cmd} - ${c.desc}`).join('\n')}
|
|
27
|
+
|
|
28
|
+
_How can I help you today?_`,
|
|
29
|
+
};
|
|
30
|
+
case '/help':
|
|
31
|
+
return {
|
|
32
|
+
handled: true,
|
|
33
|
+
response: `📋 *Available Commands:*
|
|
34
|
+
|
|
35
|
+
${CHAT_COMMANDS.map((c) => `${c.cmd} - ${c.desc}`).join('\n')}
|
|
36
|
+
|
|
37
|
+
_Just send me a message and I'll respond!_`,
|
|
38
|
+
};
|
|
39
|
+
case '/status':
|
|
40
|
+
return {
|
|
41
|
+
handled: true,
|
|
42
|
+
response: `✅ *Gateway Status:*
|
|
43
|
+
|
|
44
|
+
• Status: Running
|
|
45
|
+
• Platform: ${platform}
|
|
46
|
+
• Mode: AI Assistant
|
|
47
|
+
|
|
48
|
+
_All systems operational_`,
|
|
49
|
+
};
|
|
50
|
+
case '/models':
|
|
51
|
+
case '/model':
|
|
52
|
+
if (args.startsWith('list')) {
|
|
53
|
+
const catalog = getModelCatalogService().getAllModels();
|
|
54
|
+
const config = await readConfig();
|
|
55
|
+
const currentPrimary = config.models.primaryModel || 'not set';
|
|
56
|
+
let modelList = `📦 *Available Models*
|
|
57
|
+
|
|
58
|
+
Current: *${currentPrimary}*
|
|
59
|
+
|
|
60
|
+
`;
|
|
61
|
+
const providerGroups = new Map();
|
|
62
|
+
for (const model of catalog) {
|
|
63
|
+
const models = providerGroups.get(model.provider) || [];
|
|
64
|
+
models.push(model);
|
|
65
|
+
providerGroups.set(model.provider, models);
|
|
66
|
+
}
|
|
67
|
+
let num = 1;
|
|
68
|
+
for (const [provider, models] of providerGroups) {
|
|
69
|
+
modelList += `*${provider.toUpperCase()}:*\n`;
|
|
70
|
+
for (const m of models.slice(0, 5)) {
|
|
71
|
+
const isCurrent = m.model === currentPrimary || m.id === currentPrimary;
|
|
72
|
+
modelList += `${num}. ${m.name}${isCurrent ? ' ✅' : ''}\n`;
|
|
73
|
+
num++;
|
|
74
|
+
}
|
|
75
|
+
if (models.length > 5)
|
|
76
|
+
modelList += ` ...and ${models.length - 5} more\n`;
|
|
77
|
+
modelList += '\n';
|
|
78
|
+
}
|
|
79
|
+
modelList += '_To switch: /model set <number or name>_';
|
|
80
|
+
return { handled: true, response: modelList };
|
|
81
|
+
}
|
|
82
|
+
else if (args.startsWith('set ') && args.length > 4) {
|
|
83
|
+
const modelIdOrNum = args.substring(4).trim();
|
|
84
|
+
const catalog = getModelCatalogService().getAllModels();
|
|
85
|
+
let newModelId = modelIdOrNum;
|
|
86
|
+
if (/^\d+$/.test(modelIdOrNum)) {
|
|
87
|
+
const idx = parseInt(modelIdOrNum, 10) - 1;
|
|
88
|
+
if (idx >= 0 && idx < catalog.length) {
|
|
89
|
+
newModelId = `${catalog[idx].provider}/${catalog[idx].model}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return { handled: true, response: '❌ Invalid model number. Use /model list to see available models.' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const config = await readConfig();
|
|
96
|
+
config.models.primaryModel = newModelId;
|
|
97
|
+
await writeConfig(config);
|
|
98
|
+
return {
|
|
99
|
+
handled: true,
|
|
100
|
+
response: `✅ *Model updated!*
|
|
101
|
+
|
|
102
|
+
New primary model: *${newModelId}*
|
|
103
|
+
|
|
104
|
+
Restart TwinClaw for changes to take effect.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const config = await readConfig();
|
|
109
|
+
const currentPrimary = config.models.primaryModel || 'not set';
|
|
110
|
+
return {
|
|
111
|
+
handled: true,
|
|
112
|
+
response: `📦 *Models*
|
|
113
|
+
|
|
114
|
+
Current: *${currentPrimary}*
|
|
115
|
+
|
|
116
|
+
Commands:
|
|
117
|
+
• /model list - List available models
|
|
118
|
+
• /model set <number> - Set primary model
|
|
119
|
+
|
|
120
|
+
Example: /model set 1`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case '/keys':
|
|
124
|
+
case '/key':
|
|
125
|
+
if (args.startsWith('set ') && args.length > 4) {
|
|
126
|
+
const keyParts = args.substring(4).trim().split(/\s+/);
|
|
127
|
+
if (keyParts.length >= 2) {
|
|
128
|
+
const provider = keyParts[0];
|
|
129
|
+
const key = keyParts.slice(1).join(' ');
|
|
130
|
+
try {
|
|
131
|
+
const config = await readConfig();
|
|
132
|
+
const keyLower = provider.toLowerCase();
|
|
133
|
+
if (keyLower === 'modal')
|
|
134
|
+
config.models.modalApiKey = key;
|
|
135
|
+
else if (keyLower === 'openrouter')
|
|
136
|
+
config.models.openRouterApiKey = key;
|
|
137
|
+
else if (keyLower === 'gemini')
|
|
138
|
+
config.models.geminiApiKey = key;
|
|
139
|
+
else if (keyLower === 'groq')
|
|
140
|
+
config.messaging.voice.groqApiKey = key;
|
|
141
|
+
else if (keyLower === 'github')
|
|
142
|
+
config.models.githubToken = key;
|
|
143
|
+
else {
|
|
144
|
+
return { handled: true, response: `Unknown provider: ${provider}\nValid providers: modal, openrouter, gemini, groq, github` };
|
|
145
|
+
}
|
|
146
|
+
await writeConfig(config);
|
|
147
|
+
return { handled: true, response: `✅ ${provider} API key updated` };
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return { handled: true, response: `❌ Error setting key` };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
return { handled: true, response: `Usage: /key set <provider> <api-key>\n\nExample: /key set modal sk-xxxxx` };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
return {
|
|
159
|
+
handled: true,
|
|
160
|
+
response: `🔑 *API Keys*
|
|
161
|
+
|
|
162
|
+
Use *twinclaw config* to manage API keys.
|
|
163
|
+
|
|
164
|
+
Commands:
|
|
165
|
+
• /keys show - Show key status
|
|
166
|
+
• /key set <provider> <key> - Set API key
|
|
167
|
+
|
|
168
|
+
_Or run: twinclaw config_`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case '/channel':
|
|
172
|
+
case '/channels':
|
|
173
|
+
return {
|
|
174
|
+
handled: true,
|
|
175
|
+
response: `📱 *Channels*
|
|
176
|
+
|
|
177
|
+
WhatsApp: ✅ Connected
|
|
178
|
+
Telegram: ✅ Connected
|
|
179
|
+
|
|
180
|
+
Run: *twinclaw channels* in terminal for full details`,
|
|
181
|
+
};
|
|
182
|
+
case '/clear':
|
|
183
|
+
return { handled: true, response: '🗑️ Conversation cleared! Starting fresh.' };
|
|
184
|
+
case '/quit':
|
|
185
|
+
if (platform === 'repl') {
|
|
186
|
+
return { handled: true, response: 'Exiting chat mode. Use Ctrl+C to exit.', shouldExit: true };
|
|
187
|
+
}
|
|
188
|
+
return { handled: true, response: 'Quit is only available in terminal REPL.' };
|
|
189
|
+
case '/persona':
|
|
190
|
+
case '/config':
|
|
191
|
+
return {
|
|
192
|
+
handled: true,
|
|
193
|
+
response: `Command ${cmd} is best run from the terminal (e.g. twinclaw config).`,
|
|
194
|
+
};
|
|
195
|
+
default:
|
|
196
|
+
return { handled: false };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from '../config/json-config.js';
|
|
2
|
+
import { getModelCatalogService } from '../services/model-catalog-service.js';
|
|
3
|
+
export class CommandRouter {
|
|
4
|
+
gateway;
|
|
5
|
+
constructor(gateway) {
|
|
6
|
+
this.gateway = gateway;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Dispatches a /command from a given platform.
|
|
10
|
+
* Return string is the response to display to the user.
|
|
11
|
+
*/
|
|
12
|
+
async dispatch(commandText, platform, sessionId) {
|
|
13
|
+
const parts = commandText.trim().split(/\s+/);
|
|
14
|
+
const cmd = parts[0].toLowerCase();
|
|
15
|
+
const args = parts.slice(1).join(' ');
|
|
16
|
+
switch (cmd) {
|
|
17
|
+
case '/start':
|
|
18
|
+
case '/menu':
|
|
19
|
+
case '/help':
|
|
20
|
+
return this.handleHelp(platform);
|
|
21
|
+
case '/status':
|
|
22
|
+
return this.handleStatus(sessionId, platform);
|
|
23
|
+
case '/models':
|
|
24
|
+
case '/model':
|
|
25
|
+
return this.handleModels(args, platform);
|
|
26
|
+
case '/keys':
|
|
27
|
+
case '/key':
|
|
28
|
+
return this.handleKeys(args, platform);
|
|
29
|
+
case '/channel':
|
|
30
|
+
case '/channels':
|
|
31
|
+
return this.handleChannelStatus(platform);
|
|
32
|
+
case '/persona':
|
|
33
|
+
return this.handlePersona(args, platform);
|
|
34
|
+
case '/doctor':
|
|
35
|
+
return this.handleDoctor(platform);
|
|
36
|
+
case '/config':
|
|
37
|
+
return this.handleConfig(platform);
|
|
38
|
+
case '/clear':
|
|
39
|
+
return this.handleClear(platform);
|
|
40
|
+
case '/quit':
|
|
41
|
+
if (platform === 'repl') {
|
|
42
|
+
return '\nExiting chat mode. Use Ctrl+C to exit.\n';
|
|
43
|
+
}
|
|
44
|
+
return `Unknown command: ${cmd}\nType /menu for available commands.`;
|
|
45
|
+
default:
|
|
46
|
+
return `Unknown command: ${cmd}\nType /menu for available commands.`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
handleHelp(platform) {
|
|
50
|
+
const commands = [
|
|
51
|
+
{ cmd: '/help', desc: 'Show all commands' },
|
|
52
|
+
{ cmd: '/status', desc: 'Show gateway status' },
|
|
53
|
+
{ cmd: '/models', desc: 'Show configured models' },
|
|
54
|
+
{ cmd: '/model set <val>', desc: 'Set primary model (e.g., /model set modal)' },
|
|
55
|
+
{ cmd: '/model list', desc: 'List available models in catalog' },
|
|
56
|
+
{ cmd: '/keys', desc: 'Show API keys status' },
|
|
57
|
+
{ cmd: '/key set <prov> <key>', desc: 'Set API key (e.g., /key set modal sk-xxx)' },
|
|
58
|
+
{ cmd: '/channel', desc: 'Show messaging channels status' },
|
|
59
|
+
{ cmd: '/persona', desc: 'Show current persona' },
|
|
60
|
+
{ cmd: '/doctor', desc: 'Run diagnostics' },
|
|
61
|
+
{ cmd: '/config', desc: 'Open configuration menu (terminal)' },
|
|
62
|
+
{ cmd: '/clear', desc: 'Clear conversation' },
|
|
63
|
+
];
|
|
64
|
+
if (platform === 'repl') {
|
|
65
|
+
commands.push({ cmd: '/quit', desc: 'Exit chat mode' });
|
|
66
|
+
}
|
|
67
|
+
if (platform === 'telegram' || platform === 'whatsapp') {
|
|
68
|
+
return `🎯 *TwinClaw Menu*\n\n📋 *Available Commands:*\n\n${commands.map(c => `${c.cmd} - ${c.desc}`).join('\n')}\n\n_How can I help you today?_`;
|
|
69
|
+
}
|
|
70
|
+
// REPL formatting
|
|
71
|
+
let output = '\n╔═══════════════════════════════════════════╗\n';
|
|
72
|
+
output += '║ TwinClaw Chat Commands ║\n';
|
|
73
|
+
output += '╠═══════════════════════════════════════════╣\n';
|
|
74
|
+
for (const c of commands) {
|
|
75
|
+
output += `║ ${c.cmd.padEnd(35)} ║\n`;
|
|
76
|
+
output += `║ ${c.desc.padEnd(33)} ║\n`;
|
|
77
|
+
output += '║ ║\n';
|
|
78
|
+
}
|
|
79
|
+
output += '╚═══════════════════════════════════════════╝\n';
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
handleStatus(sessionId, platform) {
|
|
83
|
+
if (platform === 'telegram' || platform === 'whatsapp') {
|
|
84
|
+
return `✅ *Gateway Status:*\n\n• Status: Running\n• Platform: ${platform === 'telegram' ? 'Telegram' : 'WhatsApp'}\n• Mode: AI Assistant\n\n_All systems operational_`;
|
|
85
|
+
}
|
|
86
|
+
return `\n--- Gateway Status ---\nStatus: Running ✅\nSession: ${sessionId}\n----------------------\n`;
|
|
87
|
+
}
|
|
88
|
+
async handleModels(args, platform) {
|
|
89
|
+
if (args.startsWith('list')) {
|
|
90
|
+
const catalog = getModelCatalogService().getAllModels();
|
|
91
|
+
const config = await readConfig();
|
|
92
|
+
const currentPrimary = config.models.primaryModel || 'not set';
|
|
93
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
94
|
+
let modelList = isMd ? `📦 *Available Models*\n\nCurrent: *${currentPrimary}*\n\n` : `\n--- Model Catalog ---\nCurrent: ${currentPrimary}\n\n`;
|
|
95
|
+
const providerGroups = new Map();
|
|
96
|
+
for (const model of catalog) {
|
|
97
|
+
const models = providerGroups.get(model.provider) || [];
|
|
98
|
+
models.push(model);
|
|
99
|
+
providerGroups.set(model.provider, models);
|
|
100
|
+
}
|
|
101
|
+
let num = 1;
|
|
102
|
+
for (const [provider, models] of providerGroups) {
|
|
103
|
+
modelList += isMd ? `*${provider.toUpperCase()}:*\n` : `${provider.toUpperCase()}:\n`;
|
|
104
|
+
for (const m of models.slice(0, 5)) {
|
|
105
|
+
const isCurrent = m.model === currentPrimary || m.id === currentPrimary;
|
|
106
|
+
modelList += `${num}. ${m.name}${isCurrent ? ' ✅' : ''}\n`;
|
|
107
|
+
num++;
|
|
108
|
+
}
|
|
109
|
+
if (models.length > 5)
|
|
110
|
+
modelList += ` ...and ${models.length - 5} more\n`;
|
|
111
|
+
modelList += '\n';
|
|
112
|
+
}
|
|
113
|
+
modelList += isMd ? '_To switch: /model set <number or id>_' : 'To switch: /model set <number or id>';
|
|
114
|
+
if (platform === 'repl')
|
|
115
|
+
modelList += '\n--------------------\n';
|
|
116
|
+
return modelList;
|
|
117
|
+
}
|
|
118
|
+
else if (args.startsWith('set ')) {
|
|
119
|
+
const modelIdOrNum = args.substring(4).trim();
|
|
120
|
+
if (!modelIdOrNum) {
|
|
121
|
+
return `Usage: /model set <provider/model or number>\n\nExamples:\n /model set modal\n /model set 1\n /model set openrouter/meta-llama/llama-3.3-70b-instruct`;
|
|
122
|
+
}
|
|
123
|
+
const catalog = getModelCatalogService().getAllModels();
|
|
124
|
+
let newModelId = modelIdOrNum;
|
|
125
|
+
if (/^\d+$/.test(modelIdOrNum)) {
|
|
126
|
+
const idx = parseInt(modelIdOrNum, 10) - 1;
|
|
127
|
+
if (idx >= 0 && idx < catalog.length) {
|
|
128
|
+
newModelId = `${catalog[idx].provider}/${catalog[idx].model}`;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
return `❌ Invalid model number. Use /model list to see available models.`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Allow setting provider/model string dynamically
|
|
136
|
+
if (!newModelId.includes('/')) {
|
|
137
|
+
// Usually handled gracefully by ModelRouter if it maps to a well-known default provider,
|
|
138
|
+
// but we can warn.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const config = await readConfig();
|
|
143
|
+
config.models.primaryModel = newModelId;
|
|
144
|
+
await writeConfig(config);
|
|
145
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
146
|
+
return isMd
|
|
147
|
+
? `✅ *Model updated!*\n\nNew primary model: *${newModelId}*\n\nRestart TwinClaw for changes to take effect.`
|
|
148
|
+
: `\n✅ Primary model set to: ${newModelId}\n\nNote: Restart gateway for changes to take effect.`;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return `Error setting model: ${err instanceof Error ? err.message : String(err)}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
try {
|
|
156
|
+
const config = await readConfig();
|
|
157
|
+
const currentPrimary = config.models.primaryModel || 'not set';
|
|
158
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
159
|
+
if (isMd) {
|
|
160
|
+
return `📦 *Models*\n\nCurrent: *${currentPrimary}*\n\nCommands:\n• /model list - List available models\n• /model set <number or id> - Set primary model\n\nExample: /model set 1`;
|
|
161
|
+
}
|
|
162
|
+
let out = `\n--- Models ---\nPrimary: ${currentPrimary}\n`;
|
|
163
|
+
if (config.models.definitions?.length) {
|
|
164
|
+
out += '\nConfigured Models:\n';
|
|
165
|
+
for (const def of config.models.definitions) {
|
|
166
|
+
const isPrimary = def.model === currentPrimary || def.id === currentPrimary ? ' *' : '';
|
|
167
|
+
out += ` - ${def.provider}/${def.model}${isPrimary}\n`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
out += '\nTo change: /model set <provider/model or number>\n-----------\n';
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async handleKeys(args, platform) {
|
|
179
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
180
|
+
if (args.startsWith('set ')) {
|
|
181
|
+
const keyParts = args.substring(4).trim().split(/\s+/);
|
|
182
|
+
if (keyParts.length >= 2) {
|
|
183
|
+
const provider = keyParts[0];
|
|
184
|
+
const key = keyParts.slice(1).join(' ');
|
|
185
|
+
if (isMd) {
|
|
186
|
+
return `🔐 To set *${provider}* API key, run in your terminal:\n\n\`twinclaw secret set ${provider.toUpperCase()}_API_KEY ${key}\`\n\n_Or use: twinclaw config edit_`;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const config = await readConfig();
|
|
190
|
+
const keyLower = provider.toLowerCase();
|
|
191
|
+
if (keyLower === 'modal')
|
|
192
|
+
config.models.modalApiKey = key;
|
|
193
|
+
else if (keyLower === 'openrouter')
|
|
194
|
+
config.models.openRouterApiKey = key;
|
|
195
|
+
else if (keyLower === 'gemini')
|
|
196
|
+
config.models.geminiApiKey = key;
|
|
197
|
+
else if (keyLower === 'groq')
|
|
198
|
+
config.messaging.voice.groqApiKey = key;
|
|
199
|
+
else if (keyLower === 'github')
|
|
200
|
+
config.models.githubToken = key;
|
|
201
|
+
else {
|
|
202
|
+
return `Unknown provider: ${provider}\nValid providers: modal, openrouter, gemini, groq, github`;
|
|
203
|
+
}
|
|
204
|
+
await writeConfig(config);
|
|
205
|
+
return `\n✅ ${provider} API key updated\n`;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return `Error setting key: ${err instanceof Error ? err.message : String(err)}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
return `Usage: /key set <provider> <api-key>\n\nExample: /key set modal sk-xxxxx`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (isMd) {
|
|
216
|
+
return `🔑 *API Keys*\n\nUse *twinclaw config* in terminal to manage API keys.\n\nCommands:\n• /keys show - Show key status\n• /key set <provider> <key> - Set API key\n\n_Or run: twinclaw config_`;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const config = await readConfig();
|
|
220
|
+
let out = '\n╔═══════════════════════════════════════════╗\n';
|
|
221
|
+
out += '║ Current Configuration ║\n';
|
|
222
|
+
out += '╠═══════════════════════════════════════════╣\n';
|
|
223
|
+
out += `║ API Keys ║\n`;
|
|
224
|
+
out += `║ Modal: ${config.models.modalApiKey ? '***set***' : 'not set'.padEnd(22)} ║\n`;
|
|
225
|
+
out += `║ OpenRouter: ${config.models.openRouterApiKey ? '***set***' : 'not set'.padEnd(17)} ║\n`;
|
|
226
|
+
out += `║ Gemini: ${config.models.geminiApiKey ? '***set***' : 'not set'.padEnd(21)} ║\n`;
|
|
227
|
+
out += `║ Groq: ${config.messaging.voice.groqApiKey ? '***set***' : 'not set'.padEnd(24)} ║\n`;
|
|
228
|
+
out += `║ GitHub: ${config.models.githubToken ? '***set***' : 'not set'.padEnd(21)} ║\n`;
|
|
229
|
+
out += '╚═══════════════════════════════════════════╝\n';
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
return `Error loading config: ${err instanceof Error ? err.message : String(err)}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async handleChannelStatus(platform) {
|
|
237
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
238
|
+
try {
|
|
239
|
+
const config = await readConfig();
|
|
240
|
+
if (isMd) {
|
|
241
|
+
return `📱 *Channels*\n\nTelegram: ${config.messaging.telegram.enabled ? '✅ Connected' : '❌ Disabled'}\nWhatsApp: ${config.messaging.whatsapp.enabled ? '✅ Connected' : '❌ Disabled'}\n\nRun: *twinclaw channels* in terminal to manage`;
|
|
242
|
+
}
|
|
243
|
+
let out = '\n--- Messaging Channels ---\n';
|
|
244
|
+
out += `Telegram: ${config.messaging.telegram.enabled ? 'enabled' : 'disabled'}\n`;
|
|
245
|
+
if (config.messaging.telegram.enabled) {
|
|
246
|
+
out += ` Bot Token: ${config.messaging.telegram.botToken ? '***set***' : 'not set'}\n`;
|
|
247
|
+
out += ` User ID: ${config.messaging.telegram.userId || 'not set'}\n`;
|
|
248
|
+
out += ` Allow From: ${config.messaging.telegram.allowFrom.join(', ') || 'none'}\n`;
|
|
249
|
+
}
|
|
250
|
+
out += `\nWhatsApp: ${config.messaging.whatsapp.enabled ? 'enabled' : 'disabled'}\n`;
|
|
251
|
+
if (config.messaging.whatsapp.enabled) {
|
|
252
|
+
out += ` Phone: ${config.messaging.whatsapp.phoneNumber || 'not set'}\n`;
|
|
253
|
+
out += ` Allow From: ${config.messaging.whatsapp.allowFrom.join(', ') || 'none'}\n`;
|
|
254
|
+
}
|
|
255
|
+
out += '\nTo configure: twinclaw channels\n--------------\n';
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
handlePersona(args, platform) {
|
|
263
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
264
|
+
if (isMd) {
|
|
265
|
+
return `👤 *Persona*\n\nRun: *twinclaw persona* in terminal to view/edit\n\nCommands:\n• twinclaw persona show - View persona\n• twinclaw persona edit soul "text" - Edit soul`;
|
|
266
|
+
}
|
|
267
|
+
return '\nTo view/edit persona, use: twinclaw persona\nTo edit in chat: /persona edit <soul|identity|user> <text>\n--------------\n';
|
|
268
|
+
}
|
|
269
|
+
handleDoctor(platform) {
|
|
270
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
271
|
+
if (isMd) {
|
|
272
|
+
return `🩺 *Diagnostics*\n\nRun: *twinclaw doctor* in terminal to check system health\n\nChecks:\n• Environment variables\n• API keys\n• Configuration\n• Channels`;
|
|
273
|
+
}
|
|
274
|
+
return '\nRunning diagnostics...\n\nUse: twinclaw doctor\n------\n';
|
|
275
|
+
}
|
|
276
|
+
handleConfig(platform) {
|
|
277
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
278
|
+
if (isMd) {
|
|
279
|
+
return `⚙️ *Configuration*\n\nUse: *twinclaw config* in terminal to:\n• Edit configuration\n• Set API keys\n• Change models\n• Manage channels\n\nRun: *twinclaw config* in terminal`;
|
|
280
|
+
}
|
|
281
|
+
return '\n--- Configuration Menu ---\nCommands:\n /config show - Show full config\n /config edit - Open interactive editor\n /model set <id> - Change primary model\n /key set <prov> <key> - Set API key\n------------------------\n';
|
|
282
|
+
}
|
|
283
|
+
handleClear(platform) {
|
|
284
|
+
const isMd = platform === 'telegram' || platform === 'whatsapp';
|
|
285
|
+
if (isMd) {
|
|
286
|
+
return '🗑️ Conversation cleared! Starting fresh.';
|
|
287
|
+
}
|
|
288
|
+
return '\nConversation cleared.\n';
|
|
289
|
+
}
|
|
290
|
+
}
|