twinclaw 1.3.1 ā 1.4.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/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/json-config.js +4 -3
- package/dist/config/model-catalog.js +5 -5
- 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 +6 -238
- 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/model-router.js +3 -0
- 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');
|
|
@@ -315,9 +315,9 @@ export const MODEL_PROVIDERS = [
|
|
|
315
315
|
{ id: 'vllm', name: 'vLLM', baseUrl: 'http://localhost:8000/v1', defaultModel: 'meta-llama/Llama-3.1-70B-Instruct', supportsStreaming: true, supportsFunctionCalling: true, contextWindow: 128000 },
|
|
316
316
|
{ id: 'minimax', name: 'MiniMax', baseUrl: 'https://api.minimax.chat/v1', defaultModel: 'abab6.5s-chat', supportsStreaming: true, contextWindow: 245760 },
|
|
317
317
|
{ id: 'moonshot', name: 'Moonshot AI (Kimi K2.5)', baseUrl: 'https://api.moonshot.cn/v1', defaultModel: 'moonshot-v1-8k', supportsStreaming: true, contextWindow: 128000 },
|
|
318
|
-
{ id: 'google', name: 'Google', baseUrl: 'https://generativelanguage.googleapis.com/
|
|
318
|
+
{ id: 'google', name: 'Google', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', defaultModel: 'gemini-2.0-flash-exp', supportsStreaming: true, contextWindow: 1000000 },
|
|
319
319
|
{ id: 'xai', name: 'xAI (Grok)', baseUrl: 'https://api.x.ai/v1', defaultModel: 'grok-2-1212', supportsStreaming: true, contextWindow: 131072 },
|
|
320
|
-
{ id: 'openrouter', name: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1', defaultModel: 'anthropic/claude-sonnet-4-20250514', supportsStreaming: true, supportsFunctionCalling: true, contextWindow: 200000 },
|
|
320
|
+
{ id: 'openrouter', name: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1/chat/completions', defaultModel: 'anthropic/claude-sonnet-4-20250514', supportsStreaming: true, supportsFunctionCalling: true, contextWindow: 200000 },
|
|
321
321
|
{ id: 'qwen', name: 'Qwen', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', defaultModel: 'qwen-plus', supportsStreaming: true, contextWindow: 131072 },
|
|
322
322
|
{ id: 'zai', name: 'Z.AI', baseUrl: 'https://api.zai.net/v1', defaultModel: 'zai/llama-3.3-70b-instruct', supportsStreaming: true, contextWindow: 128000 },
|
|
323
323
|
{ id: 'qianfan', name: 'Qianfan', baseUrl: 'https://qianfan.baidubce.com/v3', defaultModel: 'ernie-4.0-8k', supportsStreaming: true, contextWindow: 32000 },
|
|
@@ -331,7 +331,8 @@ export const MODEL_PROVIDERS = [
|
|
|
331
331
|
{ id: 'venice', name: 'Venice AI', baseUrl: 'https://api.venice.ai/api/v1', defaultModel: 'qwen2.5-72b', supportsStreaming: true, contextWindow: 131072 },
|
|
332
332
|
{ id: 'litellm', name: 'LiteLLM', baseUrl: '', defaultModel: '', supportsStreaming: true },
|
|
333
333
|
{ id: 'cloudflare', name: 'Cloudflare AI Gateway', baseUrl: '', defaultModel: '@cf/meta/llama-3.1-70b-instruct', supportsStreaming: false, contextWindow: 128000 },
|
|
334
|
-
{ id: 'modal', name: 'Modal', baseUrl: 'https://api.us-west-2.modal.direct/v1', defaultModel: 'zai-org/GLM-5-FP8', supportsStreaming: true, contextWindow: 200000 },
|
|
334
|
+
{ id: 'modal', name: 'Modal', baseUrl: 'https://api.us-west-2.modal.direct/v1/chat/completions', defaultModel: 'zai-org/GLM-5-FP8', supportsStreaming: true, contextWindow: 200000 },
|
|
335
|
+
{ id: 'groq', name: 'Groq', baseUrl: 'https://api.groq.com/openai/v1/chat/completions', defaultModel: 'llama-3.3-70b-versatile', supportsStreaming: true, contextWindow: 131072 },
|
|
335
336
|
{ id: 'custom', name: 'Custom Provider', baseUrl: '', defaultModel: '', supportsStreaming: true },
|
|
336
337
|
];
|
|
337
338
|
export const DEFAULT_CONFIG = {
|
|
@@ -372,31 +372,31 @@ export const PROVIDER_INFO = {
|
|
|
372
372
|
},
|
|
373
373
|
openrouter: {
|
|
374
374
|
name: 'OpenRouter',
|
|
375
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
375
|
+
baseURL: 'https://openrouter.ai/api/v1/chat/completions',
|
|
376
376
|
apiKeyEnvName: 'OPENROUTER_API_KEY',
|
|
377
377
|
supportsVision: true
|
|
378
378
|
},
|
|
379
379
|
modal: {
|
|
380
380
|
name: 'Modal',
|
|
381
|
-
baseURL: 'https://api.us-west-2.modal.direct/v1',
|
|
381
|
+
baseURL: 'https://api.us-west-2.modal.direct/v1/chat/completions',
|
|
382
382
|
apiKeyEnvName: 'MODAL_API_KEY',
|
|
383
383
|
supportsVision: false
|
|
384
384
|
},
|
|
385
385
|
groq: {
|
|
386
386
|
name: 'Groq',
|
|
387
|
-
baseURL: 'https://api.groq.com/openai/v1',
|
|
387
|
+
baseURL: 'https://api.groq.com/openai/v1/chat/completions',
|
|
388
388
|
apiKeyEnvName: 'GROQ_API_KEY',
|
|
389
389
|
supportsVision: false
|
|
390
390
|
},
|
|
391
391
|
github: {
|
|
392
392
|
name: 'GitHub Copilot',
|
|
393
|
-
baseURL: 'https://api.githubcopilot.com',
|
|
393
|
+
baseURL: 'https://api.githubcopilot.com/chat/completions',
|
|
394
394
|
apiKeyEnvName: 'GITHUB_TOKEN',
|
|
395
395
|
supportsVision: true
|
|
396
396
|
},
|
|
397
397
|
copilot: {
|
|
398
398
|
name: 'GitHub Copilot',
|
|
399
|
-
baseURL: 'https://api.githubcopilot.com',
|
|
399
|
+
baseURL: 'https://api.githubcopilot.com/chat/completions',
|
|
400
400
|
apiKeyEnvName: 'GITHUB_TOKEN',
|
|
401
401
|
supportsVision: true
|
|
402
402
|
}
|
|
@@ -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
|
+
}
|
package/dist/core/doctor.js
CHANGED
|
@@ -184,7 +184,7 @@ export function checkEnvVar(check) {
|
|
|
184
184
|
/** @internal exported for testing */
|
|
185
185
|
export function checkFilesystem(check) {
|
|
186
186
|
const targetMap = {
|
|
187
|
-
'memory-dir':
|
|
187
|
+
'memory-dir': getWorkspaceSubdir('memory'),
|
|
188
188
|
'env-file': path.resolve('.env'),
|
|
189
189
|
'identity-soul': path.join(getIdentityDir(), 'soul.md'),
|
|
190
190
|
'identity-identity': path.join(getIdentityDir(), 'identity.md'),
|
|
@@ -245,7 +245,7 @@ export function checkConfigSchema(check) {
|
|
|
245
245
|
/** @internal exported for testing */
|
|
246
246
|
export function checkChannelAuth(check) {
|
|
247
247
|
if (check.name === 'whatsapp') {
|
|
248
|
-
const authDir = path.
|
|
248
|
+
const authDir = path.join(getWorkspaceSubdir('memory'), 'whatsapp_auth');
|
|
249
249
|
if (!fs.existsSync(authDir)) {
|
|
250
250
|
return {
|
|
251
251
|
check,
|