twinclaw 1.2.0 → 1.2.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.
@@ -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/chat/completions',
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 the TwinClaw gateway (default)',
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
- 'setup': "Alias of 'onboard'",
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,31 @@ export const handleVersionCli = async (argv) => {
163
167
  return 0;
164
168
  };
165
169
  /**
166
- * Handle the `start` command.
167
- * Starts the gateway in a new terminal window.
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, isChatMode = false) => {
170
- if (isChatMode) {
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
- console.log('[TwinClaw] Starting gateway in new window...');
176
184
  const entryScript = process.execPath;
177
- const appPath = process.argv[1] || process.argv[0];
185
+ console.log('[TwinClaw] Starting gateway in new window...');
186
+ const targetCmd = isChat ? 'chat' : '';
187
+ const title = isChat ? 'TwinClaw Chat' : 'TwinClaw Gateway';
178
188
  const args = [
179
189
  '/c',
180
190
  'start',
181
- '""',
191
+ `""`,
182
192
  'cmd.exe',
183
193
  '/k',
184
- `"${entryScript}" chat`
194
+ targetCmd ? `"${entryScript}" ${targetCmd}` : `"${entryScript}"`
185
195
  ];
186
196
  spawn('cmd.exe', args, {
187
197
  detached: true,
@@ -192,6 +202,10 @@ export const handleStartCli = async (argv, isChatMode = false) => {
192
202
  console.log('[TwinClaw] You can now use this terminal for other commands.');
193
203
  return 0;
194
204
  }
205
+ // For --current flag or non-Windows, return 0 to let main() continue with gateway startup
206
+ if (runInCurrentTerminal) {
207
+ console.log('[TwinClaw] Running gateway in current terminal...');
208
+ }
195
209
  return 0;
196
210
  };
197
211
  /**
@@ -249,7 +263,7 @@ export function handleUnknownCommand(argv) {
249
263
  'secret',
250
264
  'doctor',
251
265
  'onboard',
252
- 'setup',
266
+ 'persona',
253
267
  'channels',
254
268
  'gateway',
255
269
  'logs',
@@ -269,8 +283,8 @@ export function handleUnknownCommand(argv) {
269
283
  }
270
284
  // Suggest closest matching command
271
285
  const suggestions = {
272
- 'start': 'gateway start',
273
- 'stop': 'gateway stop',
286
+ 'start': 'start (new terminal) or start --current',
287
+ 'stop': 'stop',
274
288
  'staus': 'status',
275
289
  'statys': 'status',
276
290
  'hel': 'help',
@@ -283,7 +297,10 @@ export function handleUnknownCommand(argv) {
283
297
  'telegram': 'channels status',
284
298
  'log': 'logs',
285
299
  'pair': 'pairing list',
286
- 'update': 'update'
300
+ 'onboard': 'onboard',
301
+ 'setup': 'onboard',
302
+ 'update': 'update',
303
+ 'config': 'config (manage all settings)'
287
304
  };
288
305
  const lowerCommand = command.toLowerCase();
289
306
  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
+ };
@@ -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
  }
@@ -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(', TwinClaw, Persona, State, ');, 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(', -- - SOUL-- - ');, console.log(state.soul || '(empty)'));
16
- console.log(', -- - IDENTITY-- - ');, console.log(state.identity || '(empty)'));
17
- console.log(', -- - USER, CONTEXT-- - ');, console.log(state.user || '(empty)'));
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: handleConfigCommandCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Validate runtime config' },
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: ['onboard', 'setup'], handler: handleOnboardCliV2, priority: CLI_HANDLER_PRIORITY.ONBOARDING, description: 'Run onboarding' },
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
- console.error('[Dispatcher] Unhandled error processing message:', err);
135
- await logThought(`[Dispatcher] Unhandled error: ${err instanceof Error ? err.message : String(err)}`);
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
- await this.onMessage?.({ ...base, text: msg.text || base.text });
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
- throw new Error('Missing EMBEDDING_API_KEY or OPENAI_API_KEY.');
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
- const response = await fetch(endpoint, {
143
- method: 'POST',
144
- headers: {
145
- 'Content-Type': 'application/json',
146
- },
147
- body: JSON.stringify({
148
- model,
149
- prompt: input,
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
- throw new Error(`Ollama embeddings request failed (${response.status}).`);
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
- throw new Error('Ollama embeddings response did not contain an embedding array.');
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(input.payload),
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(input.payload),
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/chat/completions',
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/chat/completions',
1116
+ baseURL: modalInfo?.baseURL || 'https://api.us-west-2.modal.direct/v1',
1089
1117
  apiKeyEnvName: 'MODAL_API_KEY',
1090
1118
  });
1091
1119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twinclaw",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Eagle-eyed agentic AI gateway with multi-modal hooks and proactive memory.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {