twinclaw 1.2.0 → 1.2.2
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/config/model-catalog.js +1 -1
- package/dist/core/cli.js +34 -16
- package/dist/core/config-cli.js +171 -0
- package/dist/core/onboarding.js +242 -0
- package/dist/core/persona-cli.js +8 -4
- package/dist/index.js +10 -14
- package/dist/interfaces/dispatcher.js +24 -2
- package/dist/interfaces/telegram_handler.js +155 -1
- package/dist/services/embedding-service.js +24 -13
- package/dist/services/model-router.js +32 -4
- package/package.json +1 -1
|
@@ -295,7 +295,7 @@ export const PROVIDER_INFO = {
|
|
|
295
295
|
},
|
|
296
296
|
modal: {
|
|
297
297
|
name: 'Modal',
|
|
298
|
-
baseURL: 'https://api.us-west-2.modal.direct/v1
|
|
298
|
+
baseURL: 'https://api.us-west-2.modal.direct/v1',
|
|
299
299
|
apiKeyEnvName: 'MODAL_API_KEY',
|
|
300
300
|
supportsVision: false
|
|
301
301
|
},
|
package/dist/core/cli.js
CHANGED
|
@@ -37,17 +37,21 @@ export const handleHelpCli = async (argv) => {
|
|
|
37
37
|
{
|
|
38
38
|
title: 'Commands',
|
|
39
39
|
content: {
|
|
40
|
-
'twinclaw': 'Start
|
|
40
|
+
'twinclaw': 'Start TwinClaw (default - runs onboarding first time)',
|
|
41
|
+
'start': 'Start gateway in new terminal window',
|
|
42
|
+
'start --current': 'Start gateway in current terminal',
|
|
43
|
+
'chat': 'Start gateway with interactive chat REPL',
|
|
44
|
+
'stop': 'Stop the gateway',
|
|
41
45
|
'status': 'Show system status and health overview',
|
|
42
46
|
'doctor': 'Run diagnostics and validate prerequisites',
|
|
43
47
|
'onboard': 'Run the interactive onboarding wizard',
|
|
44
|
-
'
|
|
48
|
+
'persona': 'Manage the agent core persona (soul, identity, user)',
|
|
45
49
|
'pairing': 'Manage DM pairing approvals',
|
|
46
50
|
'secret': 'Manage secrets in the secure vault',
|
|
51
|
+
'config': 'Manage all configuration (API keys, channels, models)',
|
|
47
52
|
'channels': 'Manage messaging channels (Telegram/WhatsApp)',
|
|
48
53
|
'gateway': 'Manage the TwinClaw background service',
|
|
49
54
|
'logs': 'View gateway logs',
|
|
50
|
-
'config': 'Configuration management',
|
|
51
55
|
'queue': 'Message queue status',
|
|
52
56
|
'update': 'Check for and install latest version',
|
|
53
57
|
'self': 'Self-improving agent commands',
|
|
@@ -163,25 +167,32 @@ export const handleVersionCli = async (argv) => {
|
|
|
163
167
|
return 0;
|
|
164
168
|
};
|
|
165
169
|
/**
|
|
166
|
-
* Handle the `start`
|
|
167
|
-
*
|
|
170
|
+
* Handle the `start` and `chat` commands.
|
|
171
|
+
* - `start` - starts gateway in a new terminal window (default)
|
|
172
|
+
* - `start --current` - starts gateway in current terminal
|
|
173
|
+
* - `chat` - starts gateway with chat REPL immediately
|
|
168
174
|
*/
|
|
169
|
-
export const handleStartCli = async (argv
|
|
170
|
-
|
|
175
|
+
export const handleStartCli = async (argv) => {
|
|
176
|
+
const command = argv[0];
|
|
177
|
+
const isChat = command === 'chat';
|
|
178
|
+
const runInCurrentTerminal = argv.includes('--current');
|
|
179
|
+
if (isChat) {
|
|
171
180
|
global.TWINCLAW_CHAT_MODE = true;
|
|
172
181
|
}
|
|
173
|
-
if (process.platform === 'win32') {
|
|
182
|
+
if (process.platform === 'win32' && !runInCurrentTerminal) {
|
|
174
183
|
const { spawn } = await import('node:child_process');
|
|
175
184
|
console.log('[TwinClaw] Starting gateway in new window...');
|
|
176
|
-
const
|
|
177
|
-
const
|
|
185
|
+
const targetCmd = isChat ? 'chat' : '';
|
|
186
|
+
const title = isChat ? 'TwinClaw Chat' : 'TwinClaw Gateway';
|
|
187
|
+
// Use twinclaw command directly
|
|
188
|
+
const cmdToRun = targetCmd ? `twinclaw ${targetCmd}` : 'twinclaw';
|
|
178
189
|
const args = [
|
|
179
190
|
'/c',
|
|
180
191
|
'start',
|
|
181
|
-
|
|
192
|
+
`""`,
|
|
182
193
|
'cmd.exe',
|
|
183
194
|
'/k',
|
|
184
|
-
|
|
195
|
+
cmdToRun
|
|
185
196
|
];
|
|
186
197
|
spawn('cmd.exe', args, {
|
|
187
198
|
detached: true,
|
|
@@ -192,6 +203,10 @@ export const handleStartCli = async (argv, isChatMode = false) => {
|
|
|
192
203
|
console.log('[TwinClaw] You can now use this terminal for other commands.');
|
|
193
204
|
return 0;
|
|
194
205
|
}
|
|
206
|
+
// For --current flag or non-Windows, return 0 to let main() continue with gateway startup
|
|
207
|
+
if (runInCurrentTerminal) {
|
|
208
|
+
console.log('[TwinClaw] Running gateway in current terminal...');
|
|
209
|
+
}
|
|
195
210
|
return 0;
|
|
196
211
|
};
|
|
197
212
|
/**
|
|
@@ -249,7 +264,7 @@ export function handleUnknownCommand(argv) {
|
|
|
249
264
|
'secret',
|
|
250
265
|
'doctor',
|
|
251
266
|
'onboard',
|
|
252
|
-
'
|
|
267
|
+
'persona',
|
|
253
268
|
'channels',
|
|
254
269
|
'gateway',
|
|
255
270
|
'logs',
|
|
@@ -269,8 +284,8 @@ export function handleUnknownCommand(argv) {
|
|
|
269
284
|
}
|
|
270
285
|
// Suggest closest matching command
|
|
271
286
|
const suggestions = {
|
|
272
|
-
'start': '
|
|
273
|
-
'stop': '
|
|
287
|
+
'start': 'start (new terminal) or start --current',
|
|
288
|
+
'stop': 'stop',
|
|
274
289
|
'staus': 'status',
|
|
275
290
|
'statys': 'status',
|
|
276
291
|
'hel': 'help',
|
|
@@ -283,7 +298,10 @@ export function handleUnknownCommand(argv) {
|
|
|
283
298
|
'telegram': 'channels status',
|
|
284
299
|
'log': 'logs',
|
|
285
300
|
'pair': 'pairing list',
|
|
286
|
-
'
|
|
301
|
+
'onboard': 'onboard',
|
|
302
|
+
'setup': 'onboard',
|
|
303
|
+
'update': 'update',
|
|
304
|
+
'config': 'config (manage all settings)'
|
|
287
305
|
};
|
|
288
306
|
const lowerCommand = command.toLowerCase();
|
|
289
307
|
let suggestion = '';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { handleOnboardCliV2 } from './onboarding.js';
|
|
2
|
+
import { handleDoctorCli } from './cli.js';
|
|
3
|
+
import { readConfig } from '../config/json-config.js';
|
|
4
|
+
function printConfigHelp() {
|
|
5
|
+
console.log(`
|
|
6
|
+
TwinClaw Configuration Manager
|
|
7
|
+
================================
|
|
8
|
+
|
|
9
|
+
Usage: twinclaw config [command]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
edit Open interactive configuration editor
|
|
13
|
+
show Show current configuration
|
|
14
|
+
api Manage API keys (openrouter, gemini, groq, modal, github)
|
|
15
|
+
channel Manage messaging channels (telegram, whatsapp)
|
|
16
|
+
model Manage AI models
|
|
17
|
+
doctor Run diagnostics
|
|
18
|
+
reset Reset configuration and run onboarding again
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
twinclaw config edit # Interactive editor
|
|
22
|
+
twinclaw config show # View current config
|
|
23
|
+
twinclaw config api # Manage API keys
|
|
24
|
+
twinclaw config channel # Manage channels
|
|
25
|
+
twinclaw config model # Manage models
|
|
26
|
+
twinclaw config doctor # Run diagnostics
|
|
27
|
+
twinclaw config reset # Reset and re-onboard
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
async function showConfig() {
|
|
31
|
+
try {
|
|
32
|
+
const config = await readConfig();
|
|
33
|
+
console.log('\n=== TwinClaw Current Configuration ===\n');
|
|
34
|
+
console.log('Runtime:');
|
|
35
|
+
console.log(` API Port: ${config.runtime.apiPort}`);
|
|
36
|
+
console.log(` API Secret: ${config.runtime.apiSecret ? '***set***' : '***not set***'}`);
|
|
37
|
+
console.log('\nModels:');
|
|
38
|
+
console.log(` Primary: ${config.models.primaryModel || '***not set***'}`);
|
|
39
|
+
console.log(` Definitions: ${config.models.definitions?.length || 0} model(s) configured`);
|
|
40
|
+
if (config.models.openRouterApiKey)
|
|
41
|
+
console.log(` OpenRouter: ***set***`);
|
|
42
|
+
if (config.models.modalApiKey)
|
|
43
|
+
console.log(` Modal: ***set***`);
|
|
44
|
+
if (config.models.geminiApiKey)
|
|
45
|
+
console.log(` Gemini: ***set***`);
|
|
46
|
+
if (config.models.githubToken)
|
|
47
|
+
console.log(` GitHub: ***set***`);
|
|
48
|
+
console.log('\nMessaging:');
|
|
49
|
+
console.log(` Telegram: ${config.messaging.telegram.enabled ? 'enabled' : 'disabled'}`);
|
|
50
|
+
if (config.messaging.telegram.botToken)
|
|
51
|
+
console.log(` Bot Token: ***set***`);
|
|
52
|
+
if (config.messaging.telegram.userId)
|
|
53
|
+
console.log(` User ID: ${config.messaging.telegram.userId}`);
|
|
54
|
+
console.log(` WhatsApp: ${config.messaging.whatsapp.enabled ? 'enabled' : 'disabled'}`);
|
|
55
|
+
if (config.messaging.whatsapp.phoneNumber)
|
|
56
|
+
console.log(` Phone: ${config.messaging.whatsapp.phoneNumber}`);
|
|
57
|
+
console.log('\nChannels (allowFrom):');
|
|
58
|
+
console.log(` Telegram: ${config.messaging.telegram.allowFrom.join(', ') || 'none'}`);
|
|
59
|
+
console.log(` WhatsApp: ${config.messaging.whatsapp.allowFrom.join(', ') || 'none'}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error('Failed to read config:', err);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function manageApiKeys() {
|
|
69
|
+
console.log(`
|
|
70
|
+
API Key Management
|
|
71
|
+
==================
|
|
72
|
+
|
|
73
|
+
Available providers:
|
|
74
|
+
openrouter - OpenRouter API key
|
|
75
|
+
modal - Modal API key
|
|
76
|
+
gemini - Google Gemini API key
|
|
77
|
+
groq - Groq API key
|
|
78
|
+
github - GitHub Token (for Copilot/Models)
|
|
79
|
+
|
|
80
|
+
Usage: twinclaw config api [provider] [value]
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
twinclaw config api openrouter sk-xxxxx
|
|
84
|
+
twinclaw config api gemini xxxxx
|
|
85
|
+
twinclaw config api # View current keys
|
|
86
|
+
`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
async function manageChannel() {
|
|
90
|
+
console.log(`
|
|
91
|
+
Channel Management
|
|
92
|
+
==================
|
|
93
|
+
|
|
94
|
+
Usage: twinclaw channels [command]
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
twinclaw channels status # Show channel status
|
|
98
|
+
twinclaw channels login telegram # Login to Telegram
|
|
99
|
+
twinclaw channels login whatsapp # Login to WhatsApp
|
|
100
|
+
|
|
101
|
+
Or run: twinclaw channels
|
|
102
|
+
`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
async function manageModels() {
|
|
106
|
+
console.log(`
|
|
107
|
+
Model Management
|
|
108
|
+
================
|
|
109
|
+
|
|
110
|
+
Current models:
|
|
111
|
+
`);
|
|
112
|
+
try {
|
|
113
|
+
const config = await readConfig();
|
|
114
|
+
if (config.models.definitions && config.models.definitions.length > 0) {
|
|
115
|
+
for (const def of config.models.definitions) {
|
|
116
|
+
console.log(` - ${def.provider}/${def.model}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(' No models configured');
|
|
121
|
+
}
|
|
122
|
+
console.log(`\nPrimary: ${config.models.primaryModel || 'not set'}`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.log(' (Could not read models)');
|
|
126
|
+
}
|
|
127
|
+
console.log(`
|
|
128
|
+
To change models, run: twinclaw onboard
|
|
129
|
+
`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
async function resetConfig() {
|
|
133
|
+
console.log('[TwinClaw] Resetting configuration...');
|
|
134
|
+
console.log('[TwinClaw] This will delete your config and run onboarding again.');
|
|
135
|
+
console.log('[TwinClaw] Note: This feature requires manual config deletion.');
|
|
136
|
+
console.log('\nTo reset, delete your config file and run:');
|
|
137
|
+
console.log(' twinclaw onboard');
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
export const handleConfigCli = async (argv) => {
|
|
141
|
+
const subcommand = argv[1];
|
|
142
|
+
if (!subcommand || subcommand === 'help' || argv.includes('--help')) {
|
|
143
|
+
printConfigHelp();
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
switch (subcommand) {
|
|
147
|
+
case 'show':
|
|
148
|
+
case 'view':
|
|
149
|
+
return await showConfig();
|
|
150
|
+
case 'edit':
|
|
151
|
+
case 'wizard':
|
|
152
|
+
return await handleOnboardCliV2(['onboard']);
|
|
153
|
+
case 'api':
|
|
154
|
+
case 'keys':
|
|
155
|
+
return await manageApiKeys();
|
|
156
|
+
case 'channel':
|
|
157
|
+
case 'channels':
|
|
158
|
+
return await manageChannel();
|
|
159
|
+
case 'model':
|
|
160
|
+
case 'models':
|
|
161
|
+
return await manageModels();
|
|
162
|
+
case 'doctor':
|
|
163
|
+
return await handleDoctorCli(['doctor']);
|
|
164
|
+
case 'reset':
|
|
165
|
+
return await resetConfig();
|
|
166
|
+
default:
|
|
167
|
+
console.error(`Unknown config command: ${subcommand}`);
|
|
168
|
+
printConfigHelp();
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
};
|
package/dist/core/onboarding.js
CHANGED
|
@@ -14,14 +14,256 @@ const rl = readline.createInterface({
|
|
|
14
14
|
input: process.stdin,
|
|
15
15
|
output: process.stdout,
|
|
16
16
|
});
|
|
17
|
+
const REPL_COMMANDS = [
|
|
18
|
+
{ cmd: '/help', desc: 'Show all commands' },
|
|
19
|
+
{ cmd: '/status', desc: 'Show gateway status' },
|
|
20
|
+
{ cmd: '/models', desc: 'Show configured models' },
|
|
21
|
+
{ cmd: '/model set <name>', desc: 'Set primary model (e.g., /model set modal)' },
|
|
22
|
+
{ cmd: '/model list', desc: 'List available models in catalog' },
|
|
23
|
+
{ cmd: '/keys', desc: 'Show API keys status' },
|
|
24
|
+
{ cmd: '/key set <provider> <key>', desc: 'Set API key (e.g., /key set modal sk-xxx)' },
|
|
25
|
+
{ cmd: '/channel', desc: 'Show messaging channels status' },
|
|
26
|
+
{ cmd: '/channel telegram', desc: 'Configure Telegram settings' },
|
|
27
|
+
{ cmd: '/channel whatsapp', desc: 'Configure WhatsApp settings' },
|
|
28
|
+
{ cmd: '/persona', desc: 'Show current persona' },
|
|
29
|
+
{ cmd: '/persona edit <soul|identity|user> <text>', desc: 'Edit persona' },
|
|
30
|
+
{ cmd: '/doctor', desc: 'Run diagnostics' },
|
|
31
|
+
{ cmd: '/config', desc: 'Open configuration menu' },
|
|
32
|
+
{ cmd: '/clear', desc: 'Clear conversation' },
|
|
33
|
+
{ cmd: '/quit', desc: 'Exit chat mode' },
|
|
34
|
+
];
|
|
35
|
+
function showReplHelp() {
|
|
36
|
+
console.log('\n╔═══════════════════════════════════════════╗');
|
|
37
|
+
console.log('║ TwinClaw Chat Commands ║');
|
|
38
|
+
console.log('╠═══════════════════════════════════════════╣');
|
|
39
|
+
for (const c of REPL_COMMANDS) {
|
|
40
|
+
console.log(`║ ${c.cmd.padEnd(35)} ║`);
|
|
41
|
+
console.log(`║ ${c.desc.padEnd(33)} ║`);
|
|
42
|
+
console.log('║ ║');
|
|
43
|
+
}
|
|
44
|
+
console.log('╚═══════════════════════════════════════════╝\n');
|
|
45
|
+
}
|
|
46
|
+
async function showConfigStatus() {
|
|
47
|
+
try {
|
|
48
|
+
const { readConfig } = await import('../config/json-config.js');
|
|
49
|
+
const config = await readConfig();
|
|
50
|
+
console.log('\n╔═══════════════════════════════════════════╗');
|
|
51
|
+
console.log('║ Current Configuration ║');
|
|
52
|
+
console.log('╠═══════════════════════════════════════════╣');
|
|
53
|
+
console.log(`║ Runtime ║`);
|
|
54
|
+
console.log(`║ API Port: ${config.runtime.apiPort} ║`);
|
|
55
|
+
console.log(`║ ║`);
|
|
56
|
+
console.log(`║ Models ║`);
|
|
57
|
+
console.log(`║ Primary: ${(config.models.primaryModel || 'not set').padEnd(25)} ║`);
|
|
58
|
+
if (config.models.definitions?.length) {
|
|
59
|
+
console.log(`║ Defined: ${config.models.definitions.length} models ║`);
|
|
60
|
+
}
|
|
61
|
+
console.log(`║ ║`);
|
|
62
|
+
console.log(`║ API Keys ║`);
|
|
63
|
+
console.log(`║ Modal: ${config.models.modalApiKey ? '***set***' : 'not set'.padEnd(22)} ║`);
|
|
64
|
+
console.log(`║ OpenRouter: ${config.models.openRouterApiKey ? '***set***' : 'not set'.padEnd(17)} ║`);
|
|
65
|
+
console.log(`║ Gemini: ${config.models.geminiApiKey ? '***set***' : 'not set'.padEnd(21)} ║`);
|
|
66
|
+
console.log(`║ Groq: ${config.messaging.voice.groqApiKey ? '***set***' : 'not set'.padEnd(24)} ║`);
|
|
67
|
+
console.log(`║ GitHub: ${config.models.githubToken ? '***set***' : 'not set'.padEnd(21)} ║`);
|
|
68
|
+
console.log(`║ ║`);
|
|
69
|
+
console.log(`║ Channels ║`);
|
|
70
|
+
console.log(`║ Telegram: ${config.messaging.telegram.enabled ? 'enabled' : 'disabled'.padEnd(19)} ║`);
|
|
71
|
+
console.log(`║ WhatsApp: ${config.messaging.whatsapp.enabled ? 'enabled' : 'disabled'.padEnd(18)} ║`);
|
|
72
|
+
console.log('╚═══════════════════════════════════════════╝\n');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.log('Error loading config:', err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function showModelStatus() {
|
|
79
|
+
try {
|
|
80
|
+
const { readConfig } = await import('../config/json-config.js');
|
|
81
|
+
const config = await readConfig();
|
|
82
|
+
console.log('\n--- Models ---');
|
|
83
|
+
console.log(`Primary: ${config.models.primaryModel || 'not set'}`);
|
|
84
|
+
if (config.models.definitions?.length) {
|
|
85
|
+
console.log('\nConfigured Models:');
|
|
86
|
+
for (const def of config.models.definitions) {
|
|
87
|
+
const isPrimary = def.model === config.models.primaryModel ? ' *' : '';
|
|
88
|
+
console.log(` - ${def.provider}/${def.model}${isPrimary}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
console.log('\nTo change: /model set <provider/model>');
|
|
92
|
+
console.log('-----------\n');
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.log('Error:', err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function setModel(modelArg) {
|
|
99
|
+
if (!modelArg) {
|
|
100
|
+
console.log('\nUsage: /model set <model-id>\n');
|
|
101
|
+
console.log('Examples:');
|
|
102
|
+
console.log(' /model set modal');
|
|
103
|
+
console.log(' /model set openai/gpt-4o');
|
|
104
|
+
console.log(' /model set anthropic/claude-3-sonnet');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const { readConfig, writeConfig } = await import('../config/json-config.js');
|
|
109
|
+
const config = await readConfig();
|
|
110
|
+
config.models.primaryModel = modelArg;
|
|
111
|
+
await writeConfig(config);
|
|
112
|
+
console.log(`\n✅ Primary model set to: ${modelArg}\n`);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
console.log('Error setting model:', err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function setApiKey(provider, key) {
|
|
119
|
+
if (!provider || !key) {
|
|
120
|
+
console.log('\nUsage: /key set <provider> <api-key>');
|
|
121
|
+
console.log('\nProviders: modal, openrouter, gemini, groq, github');
|
|
122
|
+
console.log('\nExample: /key set modal sk-xxxxx');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const { readConfig, writeConfig } = await import('../config/json-config.js');
|
|
127
|
+
const config = await readConfig();
|
|
128
|
+
const keyLower = provider.toLowerCase();
|
|
129
|
+
if (keyLower === 'modal')
|
|
130
|
+
config.models.modalApiKey = key;
|
|
131
|
+
else if (keyLower === 'openrouter')
|
|
132
|
+
config.models.openRouterApiKey = key;
|
|
133
|
+
else if (keyLower === 'gemini')
|
|
134
|
+
config.models.geminiApiKey = key;
|
|
135
|
+
else if (keyLower === 'groq')
|
|
136
|
+
config.messaging.voice.groqApiKey = key;
|
|
137
|
+
else if (keyLower === 'github')
|
|
138
|
+
config.models.githubToken = key;
|
|
139
|
+
else {
|
|
140
|
+
console.log(`Unknown provider: ${provider}`);
|
|
141
|
+
console.log('Valid providers: modal, openrouter, gemini, groq, github');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await writeConfig(config);
|
|
145
|
+
console.log(`\n✅ ${provider} API key updated\n`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.log('Error setting key:', err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function showChannelStatus() {
|
|
152
|
+
try {
|
|
153
|
+
const { readConfig } = await import('../config/json-config.js');
|
|
154
|
+
const config = await readConfig();
|
|
155
|
+
console.log('\n--- Messaging Channels ---');
|
|
156
|
+
console.log(`Telegram: ${config.messaging.telegram.enabled ? 'enabled' : 'disabled'}`);
|
|
157
|
+
if (config.messaging.telegram.enabled) {
|
|
158
|
+
console.log(` Bot Token: ${config.messaging.telegram.botToken ? '***set***' : 'not set'}`);
|
|
159
|
+
console.log(` User ID: ${config.messaging.telegram.userId || 'not set'}`);
|
|
160
|
+
console.log(` Allow From: ${config.messaging.telegram.allowFrom.join(', ') || 'none'}`);
|
|
161
|
+
}
|
|
162
|
+
console.log(`\nWhatsApp: ${config.messaging.whatsapp.enabled ? 'enabled' : 'disabled'}`);
|
|
163
|
+
if (config.messaging.whatsapp.enabled) {
|
|
164
|
+
console.log(` Phone: ${config.messaging.whatsapp.phoneNumber || 'not set'}`);
|
|
165
|
+
console.log(` Allow From: ${config.messaging.whatsapp.allowFrom.join(', ') || 'none'}`);
|
|
166
|
+
}
|
|
167
|
+
console.log('\nTo configure: twinclaw channels');
|
|
168
|
+
console.log('--------------\n');
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
console.log('Error:', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function handleReplCommand(line, gateway, sessionId) {
|
|
175
|
+
const parts = line.trim().split(/\s+/);
|
|
176
|
+
const cmd = parts[0].toLowerCase();
|
|
177
|
+
const args = parts.slice(1).join(' ');
|
|
178
|
+
switch (cmd) {
|
|
179
|
+
case '/help':
|
|
180
|
+
showReplHelp();
|
|
181
|
+
return true;
|
|
182
|
+
case '/status':
|
|
183
|
+
console.log('\n--- Gateway Status ---');
|
|
184
|
+
console.log('Status: Running ✅');
|
|
185
|
+
console.log('Session:', sessionId);
|
|
186
|
+
console.log('----------------------\n');
|
|
187
|
+
return true;
|
|
188
|
+
case '/models':
|
|
189
|
+
case '/model':
|
|
190
|
+
if (args.startsWith('set ')) {
|
|
191
|
+
setModel(args.substring(4).trim());
|
|
192
|
+
}
|
|
193
|
+
else if (args === 'list') {
|
|
194
|
+
console.log('\n--- Model Catalog ---');
|
|
195
|
+
console.log('Use twinclaw config model to view full catalog');
|
|
196
|
+
console.log('--------------------\n');
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
showModelStatus();
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
case '/keys':
|
|
203
|
+
case '/key':
|
|
204
|
+
if (args.startsWith('set ')) {
|
|
205
|
+
const keyParts = args.substring(4).trim().split(/\s+/);
|
|
206
|
+
if (keyParts.length >= 2) {
|
|
207
|
+
setApiKey(keyParts[0], keyParts.slice(1).join(' '));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.log('\nUsage: /key set <provider> <api-key>\n');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
showConfigStatus();
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
case '/channel':
|
|
218
|
+
case '/channels':
|
|
219
|
+
showChannelStatus();
|
|
220
|
+
return true;
|
|
221
|
+
case '/persona':
|
|
222
|
+
console.log('\nTo view/edit persona, use: twinclaw persona');
|
|
223
|
+
console.log('To edit in chat: /persona edit <soul|identity|user> <text>');
|
|
224
|
+
console.log('--------------\n');
|
|
225
|
+
return true;
|
|
226
|
+
case '/doctor':
|
|
227
|
+
console.log('\nRunning diagnostics...\n');
|
|
228
|
+
console.log('Use: twinclaw doctor');
|
|
229
|
+
console.log('------\n');
|
|
230
|
+
return true;
|
|
231
|
+
case '/config':
|
|
232
|
+
console.log('\n--- Configuration Menu ---');
|
|
233
|
+
console.log('Commands:');
|
|
234
|
+
console.log(' /config show - Show full config');
|
|
235
|
+
console.log(' /config edit - Open interactive editor');
|
|
236
|
+
console.log(' /model set <id> - Change primary model');
|
|
237
|
+
console.log(' /key set <prov> <key> - Set API key');
|
|
238
|
+
console.log('------------------------\n');
|
|
239
|
+
return true;
|
|
240
|
+
case '/clear':
|
|
241
|
+
console.log('\nConversation cleared.\n');
|
|
242
|
+
return true;
|
|
243
|
+
case '/quit':
|
|
244
|
+
console.log('\nExiting chat mode. Use Ctrl+C to exit.\n');
|
|
245
|
+
return true;
|
|
246
|
+
default:
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
17
250
|
export function startBasicREPL(gateway) {
|
|
18
251
|
console.log('TwinClaw basic REPL started.');
|
|
252
|
+
console.log('Type /help for available commands.\n');
|
|
19
253
|
void logThought('Basic REPL started.');
|
|
20
254
|
const sessionId = 'default_repl';
|
|
21
255
|
createSession(sessionId);
|
|
22
256
|
rl.on('line', async (line) => {
|
|
257
|
+
if (!line.trim())
|
|
258
|
+
return;
|
|
23
259
|
await logThought(`REPL input received (${line.length} chars).`);
|
|
24
260
|
try {
|
|
261
|
+
// Handle / commands
|
|
262
|
+
if (line.trim().startsWith('/')) {
|
|
263
|
+
if (handleReplCommand(line, gateway, sessionId)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
25
267
|
const responseText = await gateway.processText(sessionId, line);
|
|
26
268
|
console.log(`\nTwinClaw: ${responseText}\n`);
|
|
27
269
|
}
|
package/dist/core/persona-cli.js
CHANGED
|
@@ -9,12 +9,16 @@ export const handlePersonaCli = async (argv) => {
|
|
|
9
9
|
const subcommand = argv[1];
|
|
10
10
|
if (!subcommand || subcommand === 'show' || subcommand === 'view') {
|
|
11
11
|
const state = await service.getState();
|
|
12
|
-
console.log('
|
|
12
|
+
console.log('TwinClaw Persona State');
|
|
13
|
+
console.log('──────────────────────────────────────────────────');
|
|
13
14
|
console.log(`Revision: ${state.revision}`);
|
|
14
15
|
console.log(`Updated: ${state.updatedAt}`);
|
|
15
|
-
console.log('
|
|
16
|
-
console.log(
|
|
17
|
-
console.log('
|
|
16
|
+
console.log('--- SOUL ---');
|
|
17
|
+
console.log(state.soul || '(empty)');
|
|
18
|
+
console.log('--- IDENTITY ---');
|
|
19
|
+
console.log(state.identity || '(empty)');
|
|
20
|
+
console.log('--- USER CONTEXT ---');
|
|
21
|
+
console.log(state.user || '(empty)');
|
|
18
22
|
return 0;
|
|
19
23
|
}
|
|
20
24
|
if (subcommand === 'edit' || subcommand === 'update') {
|
package/dist/index.js
CHANGED
|
@@ -31,10 +31,12 @@ import { validatePreStartConfig } from './config/prestart-validation.js';
|
|
|
31
31
|
import { getIdentityDir, getWorkspaceSubdir } from './config/workspace.js';
|
|
32
32
|
import { handleSecretVaultCli } from './core/secret-vault-cli.js';
|
|
33
33
|
import { handlePairingCli } from './core/pairing-cli.js';
|
|
34
|
-
import { handleChannelsCli } from './core/channels-cli.js';
|
|
35
34
|
import { handleGatewayCli } from './core/gateway-cli.js';
|
|
36
35
|
import { handleLogsCli } from './core/logs-cli.js';
|
|
37
36
|
import { handleQueueCli } from './core/queue-cli.js';
|
|
37
|
+
import { handleConfigCli } from './core/config-cli.js';
|
|
38
|
+
import { handlePersonaCli } from './core/persona-cli.js';
|
|
39
|
+
import { handleChannelsCli } from './core/channels-cli.js';
|
|
38
40
|
import { getDmPairingService } from './services/dm-pairing.js';
|
|
39
41
|
import { randomUUID } from 'node:crypto';
|
|
40
42
|
import { initializeSelfHealer, getSelfHealingService } from './services/self-healing.js';
|
|
@@ -58,22 +60,22 @@ const CLI_HANDLER_PRIORITY = {
|
|
|
58
60
|
STATUS: 10,
|
|
59
61
|
};
|
|
60
62
|
const handleSecretCommandCli = async (args) => handleSecretVaultCli(['secret', ...args.slice(1)], secretVault) ? 0 : 1;
|
|
61
|
-
const handleConfigCommandCli = async (args) => handleSecretVaultCli(['config', ...args.slice(1)], secretVault) ? 0 : 1;
|
|
62
63
|
const CLI_HANDLERS = [
|
|
63
64
|
{ command: 'help', handler: handleHelpCli, priority: CLI_HANDLER_PRIORITY.HELP, description: 'Show help' },
|
|
64
65
|
{ command: 'self', handler: handleSelfImproveCli, priority: CLI_HANDLER_PRIORITY.SELF, description: 'Self-improvement commands' },
|
|
65
66
|
{ command: 'doctor', handler: handleDoctorCli, priority: CLI_HANDLER_PRIORITY.DOCTOR, description: 'Run diagnostics' },
|
|
66
67
|
{ command: 'update', handler: handleUpdateCli, priority: CLI_HANDLER_PRIORITY.UPDATE, description: 'Check for updates' },
|
|
67
68
|
{ command: ['version', '--version', '-v'], handler: handleVersionCli, priority: CLI_HANDLER_PRIORITY.VERSION, description: 'Show version' },
|
|
68
|
-
{ command: 'start', handler: handleStartCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Start gateway' },
|
|
69
|
+
{ command: 'start', handler: handleStartCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Start gateway in new terminal' },
|
|
70
|
+
{ command: 'chat', handler: handleStartCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Start gateway with chat REPL' },
|
|
69
71
|
{ command: 'stop', handler: handleStopCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Stop gateway' },
|
|
70
|
-
{ command: 'chat', handler: async (args) => { return await handleStartCli(['start', ...args.slice(1)], true); }, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Open terminal chat' },
|
|
71
72
|
{ command: 'secret', handler: handleSecretCommandCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage secrets' },
|
|
72
|
-
{ command: 'config', handler:
|
|
73
|
+
{ command: 'config', handler: handleConfigCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage configuration' },
|
|
73
74
|
{ command: 'pairing', handler: async (args) => { return await handlePairingCli(args, pairingService) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage pairings' },
|
|
74
|
-
{ command:
|
|
75
|
+
{ command: 'persona', handler: handlePersonaCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage agent persona' },
|
|
76
|
+
{ command: 'onboard', handler: handleOnboardCliV2, priority: CLI_HANDLER_PRIORITY.ONBOARDING, description: 'Run onboarding wizard' },
|
|
75
77
|
{ command: 'logs', handler: async (args) => { return await handleLogsCli(args) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.OPERATIONS, description: 'View logs' },
|
|
76
|
-
{ command: 'gateway', handler: async (args) => { return await handleGatewayCli(args) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.OPERATIONS, description: 'Manage gateway' },
|
|
78
|
+
{ command: 'gateway', handler: async (args) => { return await handleGatewayCli(args) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.OPERATIONS, description: 'Manage gateway service' },
|
|
77
79
|
{ command: 'channels', handler: async (args) => { return await handleChannelsCli(args) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.OPERATIONS, description: 'Manage channels' },
|
|
78
80
|
{ command: 'status', handler: handleStatusCli, priority: CLI_HANDLER_PRIORITY.STATUS, description: 'Show status' },
|
|
79
81
|
{ command: 'queue', handler: async (args) => { return await handleQueueCli(args) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.STATUS, description: 'Manage queue' },
|
|
@@ -84,7 +86,7 @@ function printPreStartValidationFailure(issues) {
|
|
|
84
86
|
console.error(` [${issue.class}] [${issue.key}] ${issue.message}`);
|
|
85
87
|
console.error(` Remediation: ${issue.remediation}`);
|
|
86
88
|
}
|
|
87
|
-
console.error("Run 'twinclaw onboard' to fix configuration, then retry.");
|
|
89
|
+
console.error("Run 'twinclaw onboard' (or 'twinclaw setup') to fix configuration, then retry.");
|
|
88
90
|
}
|
|
89
91
|
function assertWindowsOnlyRuntime() {
|
|
90
92
|
if (process.platform === 'win32') {
|
|
@@ -129,12 +131,6 @@ async function main() {
|
|
|
129
131
|
assertWindowsOnlyRuntime();
|
|
130
132
|
const argv = process.argv.slice(2);
|
|
131
133
|
const command = argv[0];
|
|
132
|
-
// Handle --onboard flag as a shortcut
|
|
133
|
-
if (argv.includes('--onboard')) {
|
|
134
|
-
const onboardingArgs = ['onboard', ...argv.filter(a => a !== '--onboard')];
|
|
135
|
-
const exitCode = await handleOnboardCliV2(onboardingArgs);
|
|
136
|
-
process.exit(exitCode);
|
|
137
|
-
}
|
|
138
134
|
// Sort handlers by priority
|
|
139
135
|
const sortedHandlers = [...CLI_HANDLERS].sort((a, b) => b.priority - a.priority);
|
|
140
136
|
if (command) {
|
|
@@ -131,8 +131,30 @@ export class Dispatcher {
|
|
|
131
131
|
await this.#dispatch(normalized, responseText);
|
|
132
132
|
}
|
|
133
133
|
catch (err) {
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
135
|
+
console.error('[Dispatcher] Error processing message:', err);
|
|
136
|
+
let friendlyMessage = "Sorry, I encountered an error. Please try again.";
|
|
137
|
+
// Handle vision/image errors gracefully
|
|
138
|
+
if (errorMessage.includes('does not support image') ||
|
|
139
|
+
errorMessage.includes('image input') ||
|
|
140
|
+
errorMessage.includes('vision') ||
|
|
141
|
+
errorMessage.includes('Cannot read') ||
|
|
142
|
+
errorMessage.includes('does not support vision')) {
|
|
143
|
+
friendlyMessage = "⚠️ Sorry, I can't process images with my current model. Please try sending a text message instead.";
|
|
144
|
+
}
|
|
145
|
+
// Handle model exhaustion errors
|
|
146
|
+
else if (errorMessage.includes('all configured models exhausted') ||
|
|
147
|
+
errorMessage.includes('models exhausted') ||
|
|
148
|
+
errorMessage.includes('tool_call_id')) {
|
|
149
|
+
friendlyMessage = "⚠️ I'm having trouble with my AI models right now. Please try again in a moment, or check that your API keys are configured correctly.";
|
|
150
|
+
}
|
|
151
|
+
// Always try to send a response
|
|
152
|
+
try {
|
|
153
|
+
await this.#dispatch(message, friendlyMessage);
|
|
154
|
+
}
|
|
155
|
+
catch (dispatchErr) {
|
|
156
|
+
console.error('[Dispatcher] Failed to send error message:', dispatchErr);
|
|
157
|
+
}
|
|
136
158
|
}
|
|
137
159
|
}
|
|
138
160
|
#resolveAccessConfig(channel, config) {
|
|
@@ -3,6 +3,20 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
/** Minimum ms delay between processing successive messages (human-like pacing). */
|
|
5
5
|
const RATE_LIMIT_MS = 1500;
|
|
6
|
+
const TELEGRAM_COMMANDS = [
|
|
7
|
+
{ cmd: '/start', desc: 'Show welcome message and menu' },
|
|
8
|
+
{ cmd: '/help', desc: 'Show all available commands' },
|
|
9
|
+
{ cmd: '/status', desc: 'Show gateway status' },
|
|
10
|
+
{ cmd: '/menu', desc: 'Show this menu' },
|
|
11
|
+
{ cmd: '/models', desc: 'Show configured models' },
|
|
12
|
+
{ cmd: '/model set', desc: 'Set primary model' },
|
|
13
|
+
{ cmd: '/keys', desc: 'Show API keys status' },
|
|
14
|
+
{ cmd: '/key set', desc: 'Set API key' },
|
|
15
|
+
{ cmd: '/channel', desc: 'Show channel status' },
|
|
16
|
+
{ cmd: '/persona', desc: 'Show persona' },
|
|
17
|
+
{ cmd: '/doctor', desc: 'Run diagnostics' },
|
|
18
|
+
{ cmd: '/clear', desc: 'Clear conversation' },
|
|
19
|
+
];
|
|
6
20
|
/**
|
|
7
21
|
* Wraps the Telegram Bot API to provide:
|
|
8
22
|
* - Inbound message normalization for the dispatcher
|
|
@@ -62,13 +76,153 @@ export class TelegramHandler {
|
|
|
62
76
|
}
|
|
63
77
|
// Process text - this runs whether voice succeeded or failed
|
|
64
78
|
if (msg.text || base.text) {
|
|
65
|
-
|
|
79
|
+
const text = msg.text ?? base.text;
|
|
80
|
+
if (!text)
|
|
81
|
+
return;
|
|
82
|
+
// Handle commands
|
|
83
|
+
if (text.trim().startsWith('/')) {
|
|
84
|
+
const command = text.trim().split(' ')[0];
|
|
85
|
+
await this.handleCommand(msg.chat.id, command);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await this.onMessage?.({ ...base, text });
|
|
66
89
|
}
|
|
67
90
|
});
|
|
68
91
|
this.#bot.on('polling_error', (err) => {
|
|
69
92
|
console.error('[TelegramHandler] Polling error:', err.message);
|
|
70
93
|
});
|
|
71
94
|
}
|
|
95
|
+
// ── Command Handlers ─────────────────────────────────────────────────────────
|
|
96
|
+
async handleCommand(chatId, command) {
|
|
97
|
+
const parts = command.toLowerCase().trim().split(/\s+/);
|
|
98
|
+
const cmd = parts[0];
|
|
99
|
+
const args = parts.slice(1).join(' ');
|
|
100
|
+
switch (cmd) {
|
|
101
|
+
case '/start':
|
|
102
|
+
case '/menu':
|
|
103
|
+
const menuText = `🎯 *TwinClaw Menu*
|
|
104
|
+
|
|
105
|
+
${TELEGRAM_COMMANDS.map(c => `${c.cmd} - ${c.desc}`).join('\n')}
|
|
106
|
+
|
|
107
|
+
_How can I help you today?_
|
|
108
|
+
`;
|
|
109
|
+
await this.#bot.sendMessage(chatId, menuText, { parse_mode: 'Markdown' });
|
|
110
|
+
break;
|
|
111
|
+
case '/help':
|
|
112
|
+
await this.#bot.sendMessage(chatId, `📋 *Available Commands:*
|
|
113
|
+
|
|
114
|
+
${TELEGRAM_COMMANDS.map(c => `${c.cmd} - ${c.desc}`).join('\n')}
|
|
115
|
+
|
|
116
|
+
_Just send me a message and I'll respond!_
|
|
117
|
+
`, { parse_mode: 'Markdown' });
|
|
118
|
+
break;
|
|
119
|
+
case '/status':
|
|
120
|
+
await this.#bot.sendMessage(chatId, `✅ *Gateway Status:*
|
|
121
|
+
|
|
122
|
+
• Status: Running
|
|
123
|
+
• Platform: Telegram
|
|
124
|
+
• Mode: AI Assistant
|
|
125
|
+
|
|
126
|
+
_All systems operational_
|
|
127
|
+
`, { parse_mode: 'Markdown' });
|
|
128
|
+
break;
|
|
129
|
+
case '/models':
|
|
130
|
+
case '/model':
|
|
131
|
+
if (args.startsWith('set ') && args.length > 4) {
|
|
132
|
+
const modelId = args.substring(4).trim();
|
|
133
|
+
await this.#bot.sendMessage(chatId, `To change model to *${modelId}*, run:\n\n\`/key set modal ${modelId}\`\n\n_Or use: twinclaw config edit_`, { parse_mode: 'Markdown' });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await this.#bot.sendMessage(chatId, `📦 *Models*
|
|
137
|
+
|
|
138
|
+
Use *twinclaw config* to manage models.
|
|
139
|
+
|
|
140
|
+
Commands:
|
|
141
|
+
• /model list - List available models
|
|
142
|
+
• /model set <id> - Set primary model
|
|
143
|
+
|
|
144
|
+
_Or run: twinclaw config model_
|
|
145
|
+
`, { parse_mode: 'Markdown' });
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case '/keys':
|
|
149
|
+
case '/key':
|
|
150
|
+
if (args.startsWith('set ') && args.length > 4) {
|
|
151
|
+
const keyParts = args.substring(4).trim().split(/\s+/);
|
|
152
|
+
if (keyParts.length >= 2) {
|
|
153
|
+
const provider = keyParts[0];
|
|
154
|
+
const key = keyParts.slice(1).join(' ');
|
|
155
|
+
await this.#bot.sendMessage(chatId, `🔐 To set *${provider}* API key, run:\n\n\`twinclaw secret set ${provider.toUpperCase()}_API_KEY ${key}\`\n\n_Or use: twinclaw config edit_
|
|
156
|
+
`, { parse_mode: 'Markdown' });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
await this.#bot.sendMessage(chatId, `Usage: /key set <provider> <api-key>\n\nExample: /key set modal sk-xxxxx`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await this.#bot.sendMessage(chatId, `🔑 *API Keys*
|
|
164
|
+
|
|
165
|
+
Use *twinclaw config* to manage API keys.
|
|
166
|
+
|
|
167
|
+
Commands:
|
|
168
|
+
• /keys show - Show key status
|
|
169
|
+
• /key set <provider> <key> - Set API key
|
|
170
|
+
|
|
171
|
+
_Or run: twinclaw config_
|
|
172
|
+
`, { parse_mode: 'Markdown' });
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case '/channel':
|
|
176
|
+
case '/channels':
|
|
177
|
+
await this.#bot.sendMessage(chatId, `📱 *Channels*
|
|
178
|
+
|
|
179
|
+
Telegram: ✅ Connected
|
|
180
|
+
WhatsApp: Use /channels to configure
|
|
181
|
+
|
|
182
|
+
Run: *twinclaw channels* to manage
|
|
183
|
+
`, { parse_mode: 'Markdown' });
|
|
184
|
+
break;
|
|
185
|
+
case '/persona':
|
|
186
|
+
await this.#bot.sendMessage(chatId, `👤 *Persona*
|
|
187
|
+
|
|
188
|
+
Run: *twinclaw persona* to view/edit
|
|
189
|
+
|
|
190
|
+
Commands:
|
|
191
|
+
• twinclaw persona show - View persona
|
|
192
|
+
• twinclaw persona edit soul "text" - Edit soul
|
|
193
|
+
`, { parse_mode: 'Markdown' });
|
|
194
|
+
break;
|
|
195
|
+
case '/doctor':
|
|
196
|
+
await this.#bot.sendMessage(chatId, `🩺 *Diagnostics*
|
|
197
|
+
|
|
198
|
+
Run: *twinclaw doctor* to check system health
|
|
199
|
+
|
|
200
|
+
Checks:
|
|
201
|
+
• Environment variables
|
|
202
|
+
• API keys
|
|
203
|
+
• Configuration
|
|
204
|
+
• Channels
|
|
205
|
+
`, { parse_mode: 'Markdown' });
|
|
206
|
+
break;
|
|
207
|
+
case '/config':
|
|
208
|
+
await this.#bot.sendMessage(chatId, `⚙️ *Configuration*
|
|
209
|
+
|
|
210
|
+
Use: *twinclaw config* to:
|
|
211
|
+
• Edit configuration
|
|
212
|
+
• Set API keys
|
|
213
|
+
• Change models
|
|
214
|
+
• Manage channels
|
|
215
|
+
|
|
216
|
+
Run: *twinclaw config* in terminal
|
|
217
|
+
`, { parse_mode: 'Markdown' });
|
|
218
|
+
break;
|
|
219
|
+
case '/clear':
|
|
220
|
+
await this.#bot.sendMessage(chatId, '🗑️ Conversation cleared! Starting fresh.');
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
await this.#bot.sendMessage(chatId, `Unknown command: ${command}\nType /menu for available commands.`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
72
226
|
// ── Public Send Methods ───────────────────────────────────────────────────────
|
|
73
227
|
/** Send a plain-text reply to a chat. */
|
|
74
228
|
async sendText(chatId, text) {
|
|
@@ -105,7 +105,8 @@ export class EmbeddingService {
|
|
|
105
105
|
const secretVault = getSecretVaultService();
|
|
106
106
|
const apiKey = secretVault.readSecret('EMBEDDING_API_KEY') ?? secretVault.readSecret('OPENAI_API_KEY') ?? '';
|
|
107
107
|
if (!apiKey) {
|
|
108
|
-
|
|
108
|
+
this.debugLog('[EmbeddingService] OpenAI: No API key configured, skipping.');
|
|
109
|
+
return [];
|
|
109
110
|
}
|
|
110
111
|
const endpoint = getConfigValue('EMBEDDING_API_URL') ?? DEFAULT_OPENAI_URL;
|
|
111
112
|
const model = getConfigValue('EMBEDDING_MODEL') ?? DEFAULT_OPENAI_MODEL;
|
|
@@ -139,25 +140,35 @@ export class EmbeddingService {
|
|
|
139
140
|
const baseUrl = getConfigValue('OLLAMA_BASE_URL') ?? DEFAULT_OLLAMA_URL;
|
|
140
141
|
const model = getConfigValue('OLLAMA_EMBEDDING_MODEL') ?? DEFAULT_OLLAMA_MODEL;
|
|
141
142
|
const endpoint = `${baseUrl.replace(/\/$/, '')}/api/embeddings`;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
let response;
|
|
144
|
+
try {
|
|
145
|
+
response = await fetch(endpoint, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
model,
|
|
152
|
+
prompt: input,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
this.debugLog(`[EmbeddingService] Ollama: Connection failed (${message}). Is Ollama running?`);
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
152
161
|
if (!response.ok) {
|
|
153
|
-
|
|
162
|
+
this.debugLog(`[EmbeddingService] Ollama: Request failed (${response.status}).`);
|
|
163
|
+
return [];
|
|
154
164
|
}
|
|
155
165
|
const data = await response.json();
|
|
156
166
|
const embedding = typeof data === 'object' && data !== null
|
|
157
167
|
? data.embedding
|
|
158
168
|
: undefined;
|
|
159
169
|
if (!isFiniteNumberArray(embedding)) {
|
|
160
|
-
|
|
170
|
+
this.debugLog('[EmbeddingService] Ollama: Invalid response format.');
|
|
171
|
+
return [];
|
|
161
172
|
}
|
|
162
173
|
return embedding;
|
|
163
174
|
}
|
|
@@ -525,9 +525,22 @@ export class ModelRouter {
|
|
|
525
525
|
this.trackUsageAttempt(input.config.id);
|
|
526
526
|
this.recordEvent('attempt', input.config, `Attempting ${input.config.model} (profile=${input.directive.profile}, severity=${input.directive.severity}).`);
|
|
527
527
|
const startedAt = this.nowFn();
|
|
528
|
+
const timeoutMs = 60000; // 60 second timeout
|
|
529
|
+
// For providers that don't support tools well, strip them from the request
|
|
530
|
+
const providerId = this.resolveProviderId(input.config);
|
|
531
|
+
const noToolProviders = ['stepfun', 'openrouter'];
|
|
532
|
+
let payload = input.payload;
|
|
533
|
+
if (noToolProviders.includes(providerId) && input.payload.tools) {
|
|
534
|
+
payload = { ...input.payload };
|
|
535
|
+
delete payload.tools;
|
|
536
|
+
delete payload.tool_choice;
|
|
537
|
+
}
|
|
528
538
|
try {
|
|
539
|
+
const controller = new AbortController();
|
|
540
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
529
541
|
const response = await fetch(input.config.baseURL, {
|
|
530
542
|
method: 'POST',
|
|
543
|
+
signal: controller.signal,
|
|
531
544
|
headers: {
|
|
532
545
|
'Content-Type': 'application/json',
|
|
533
546
|
Authorization: `Bearer ${input.apiKey}`,
|
|
@@ -535,8 +548,9 @@ export class ModelRouter {
|
|
|
535
548
|
? { 'HTTP-Referer': 'https://twinclaw.ai', 'X-Title': 'TwinClaw' }
|
|
536
549
|
: {}),
|
|
537
550
|
},
|
|
538
|
-
body: JSON.stringify(
|
|
551
|
+
body: JSON.stringify(payload),
|
|
539
552
|
});
|
|
553
|
+
clearTimeout(timeoutId);
|
|
540
554
|
const latencyMs = this.nowFn() - startedAt;
|
|
541
555
|
if (response.status === 429) {
|
|
542
556
|
const cooldownMs = parseRetryAfterMs(response.headers.get('retry-after')) ?? this.defaultRateLimitCooldownMs;
|
|
@@ -650,11 +664,24 @@ export class ModelRouter {
|
|
|
650
664
|
this.trackUsageAttempt(input.config.id);
|
|
651
665
|
this.recordEvent('attempt', input.config, `Attempting streaming ${input.config.model} (profile=${input.directive.profile}).`);
|
|
652
666
|
const startedAt = this.nowFn();
|
|
667
|
+
const timeoutMs = 60000;
|
|
653
668
|
const responseContentParts = [];
|
|
654
669
|
const toolCalls = [];
|
|
670
|
+
// For providers that don't support tools well, strip them from the request
|
|
671
|
+
const providerId = this.resolveProviderId(input.config);
|
|
672
|
+
const noToolProviders = ['stepfun', 'openrouter'];
|
|
673
|
+
let payload = input.payload;
|
|
674
|
+
if (noToolProviders.includes(providerId) && input.payload.tools) {
|
|
675
|
+
payload = { ...input.payload };
|
|
676
|
+
delete payload.tools;
|
|
677
|
+
delete payload.tool_choice;
|
|
678
|
+
}
|
|
655
679
|
try {
|
|
680
|
+
const controller = new AbortController();
|
|
681
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
656
682
|
const response = await fetch(input.config.baseURL, {
|
|
657
683
|
method: 'POST',
|
|
684
|
+
signal: controller.signal,
|
|
658
685
|
headers: {
|
|
659
686
|
'Content-Type': 'application/json',
|
|
660
687
|
Authorization: `Bearer ${input.apiKey}`,
|
|
@@ -662,8 +689,9 @@ export class ModelRouter {
|
|
|
662
689
|
? { 'HTTP-Referer': 'https://twinclaw.ai', 'X-Title': 'TwinClaw' }
|
|
663
690
|
: {}),
|
|
664
691
|
},
|
|
665
|
-
body: JSON.stringify(
|
|
692
|
+
body: JSON.stringify(payload),
|
|
666
693
|
});
|
|
694
|
+
clearTimeout(timeoutId);
|
|
667
695
|
if (response.status === 429) {
|
|
668
696
|
const cooldownMs = parseRetryAfterMs(response.headers.get('retry-after')) ?? this.defaultRateLimitCooldownMs;
|
|
669
697
|
this.#recordFailure(`429 Too Many Requests: ${input.config.model}`);
|
|
@@ -1058,7 +1086,7 @@ export class ModelRouter {
|
|
|
1058
1086
|
configModels.push({
|
|
1059
1087
|
id: MODEL_SLOT_IDS.PRIMARY,
|
|
1060
1088
|
model: 'zai-org/GLM-5-FP8',
|
|
1061
|
-
baseURL: modalInfo?.baseURL || 'https://api.us-west-2.modal.direct/v1
|
|
1089
|
+
baseURL: modalInfo?.baseURL || 'https://api.us-west-2.modal.direct/v1',
|
|
1062
1090
|
apiKeyEnvName: 'MODAL_API_KEY',
|
|
1063
1091
|
});
|
|
1064
1092
|
}
|
|
@@ -1085,7 +1113,7 @@ export class ModelRouter {
|
|
|
1085
1113
|
configModels.push({
|
|
1086
1114
|
id: MODEL_SLOT_IDS.PRIMARY,
|
|
1087
1115
|
model: 'zai-org/GLM-5-FP8',
|
|
1088
|
-
baseURL: modalInfo?.baseURL || 'https://api.us-west-2.modal.direct/v1
|
|
1116
|
+
baseURL: modalInfo?.baseURL || 'https://api.us-west-2.modal.direct/v1',
|
|
1089
1117
|
apiKeyEnvName: 'MODAL_API_KEY',
|
|
1090
1118
|
});
|
|
1091
1119
|
}
|