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.
@@ -1,20 +1,21 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as fsPromises from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { getWorkspaceDir } from '../config/workspace.js';
4
5
  function currentDateIso() {
5
6
  return new Date().toISOString().slice(0, 10);
6
7
  }
7
8
  function printLogsUsage() {
8
- console.log(`Logs commands:
9
- twinclaw logs View today's logs
10
- twinclaw logs --follow Follow logs in real-time
11
- twinclaw logs --follow -f Same as above (shorthand)
12
- twinclaw logs --date <YYYY-MM-DD> View logs for specific date
13
-
14
- Examples:
15
- twinclaw logs
16
- twinclaw logs --follow
17
- twinclaw logs -f
9
+ console.log(`Logs commands:
10
+ twinclaw logs View today's logs
11
+ twinclaw logs --follow Follow logs in real-time
12
+ twinclaw logs --follow -f Same as above (shorthand)
13
+ twinclaw logs --date <YYYY-MM-DD> View logs for specific date
14
+
15
+ Examples:
16
+ twinclaw logs
17
+ twinclaw logs --follow
18
+ twinclaw logs -f
18
19
  twinclaw logs --date 2026-02-20`);
19
20
  }
20
21
  /**
@@ -36,7 +37,7 @@ export async function handleLogsCli(argv) {
36
37
  if (dateIndex !== -1 && argv[dateIndex + 1]) {
37
38
  dateIso = argv[dateIndex + 1];
38
39
  }
39
- const logPath = path.resolve('memory', `${dateIso}.md`);
40
+ const logPath = path.join(getWorkspaceDir(), 'memory', `${dateIso}.md`);
40
41
  if (!fs.existsSync(logPath)) {
41
42
  console.error(`[TwinClaw Logs] No logs found for today (${dateIso}) at ${logPath}.`);
42
43
  process.exitCode = 1;
@@ -32,246 +32,14 @@ const REPL_COMMANDS = [
32
32
  { cmd: '/clear', desc: 'Clear conversation' },
33
33
  { cmd: '/quit', desc: 'Exit chat mode' },
34
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 <provider/model>');
101
- console.log('\nValid providers: modal, groq, openrouter, google, github');
102
- console.log('\nExamples:');
103
- console.log(' /model set modal (your custom model)');
104
- console.log(' /model set groq/qwen/qwen3-32b');
105
- console.log(' /model set openrouter/meta-llama/llama-3.3-70b-instruct');
106
- console.log(' /model set google/gemini-2.0-flash');
107
- return;
108
- }
109
- // Validate the model format
110
- const validProviders = ['modal', 'groq', 'openrouter', 'google', 'github', 'anthropic'];
111
- const modelLower = modelArg.toLowerCase();
112
- // Check if it follows provider/model format
113
- if (!modelLower.includes('/')) {
114
- console.log('\n⚠️ Invalid format. Use: provider/model');
115
- console.log('Example: /model set modal');
116
- console.log('Valid providers: modal, groq, openrouter, google, github');
117
- return;
118
- }
119
- const [provider] = modelLower.split('/');
120
- if (!validProviders.includes(provider)) {
121
- console.log(`\n⚠️ Unknown provider: ${provider}`);
122
- console.log('Valid providers: modal, groq, openrouter, google, github');
123
- return;
124
- }
125
- try {
126
- const { readConfig, writeConfig } = await import('../config/json-config.js');
127
- const config = await readConfig();
128
- config.models.primaryModel = modelArg;
129
- await writeConfig(config);
130
- console.log(`\n✅ Primary model set to: ${modelArg}\n`);
131
- console.log('Note: Restart gateway for changes to take effect.');
132
- }
133
- catch (err) {
134
- console.log('Error setting model:', err);
135
- }
136
- }
137
- async function setApiKey(provider, key) {
138
- if (!provider || !key) {
139
- console.log('\nUsage: /key set <provider> <api-key>');
140
- console.log('\nProviders: modal, openrouter, gemini, groq, github');
141
- console.log('\nExample: /key set modal sk-xxxxx');
142
- return;
143
- }
144
- try {
145
- const { readConfig, writeConfig } = await import('../config/json-config.js');
146
- const config = await readConfig();
147
- const keyLower = provider.toLowerCase();
148
- if (keyLower === 'modal')
149
- config.models.modalApiKey = key;
150
- else if (keyLower === 'openrouter')
151
- config.models.openRouterApiKey = key;
152
- else if (keyLower === 'gemini')
153
- config.models.geminiApiKey = key;
154
- else if (keyLower === 'groq')
155
- config.messaging.voice.groqApiKey = key;
156
- else if (keyLower === 'github')
157
- config.models.githubToken = key;
158
- else {
159
- console.log(`Unknown provider: ${provider}`);
160
- console.log('Valid providers: modal, openrouter, gemini, groq, github');
161
- return;
162
- }
163
- await writeConfig(config);
164
- console.log(`\n✅ ${provider} API key updated\n`);
165
- }
166
- catch (err) {
167
- console.log('Error setting key:', err);
168
- }
169
- }
170
- async function showChannelStatus() {
171
- try {
172
- const { readConfig } = await import('../config/json-config.js');
173
- const config = await readConfig();
174
- console.log('\n--- Messaging Channels ---');
175
- console.log(`Telegram: ${config.messaging.telegram.enabled ? 'enabled' : 'disabled'}`);
176
- if (config.messaging.telegram.enabled) {
177
- console.log(` Bot Token: ${config.messaging.telegram.botToken ? '***set***' : 'not set'}`);
178
- console.log(` User ID: ${config.messaging.telegram.userId || 'not set'}`);
179
- console.log(` Allow From: ${config.messaging.telegram.allowFrom.join(', ') || 'none'}`);
180
- }
181
- console.log(`\nWhatsApp: ${config.messaging.whatsapp.enabled ? 'enabled' : 'disabled'}`);
182
- if (config.messaging.whatsapp.enabled) {
183
- console.log(` Phone: ${config.messaging.whatsapp.phoneNumber || 'not set'}`);
184
- console.log(` Allow From: ${config.messaging.whatsapp.allowFrom.join(', ') || 'none'}`);
185
- }
186
- console.log('\nTo configure: twinclaw channels');
187
- console.log('--------------\n');
188
- }
189
- catch (err) {
190
- console.log('Error:', err);
191
- }
192
- }
193
- function handleReplCommand(line, gateway, sessionId) {
194
- const parts = line.trim().split(/\s+/);
195
- const cmd = parts[0].toLowerCase();
196
- const args = parts.slice(1).join(' ');
197
- switch (cmd) {
198
- case '/help':
199
- showReplHelp();
200
- return true;
201
- case '/status':
202
- console.log('\n--- Gateway Status ---');
203
- console.log('Status: Running ✅');
204
- console.log('Session:', sessionId);
205
- console.log('----------------------\n');
206
- return true;
207
- case '/models':
208
- case '/model':
209
- if (args.startsWith('set ')) {
210
- setModel(args.substring(4).trim());
211
- }
212
- else if (args === 'list') {
213
- console.log('\n--- Model Catalog ---');
214
- console.log('Use twinclaw config model to view full catalog');
215
- console.log('--------------------\n');
216
- }
217
- else {
218
- showModelStatus();
219
- }
220
- return true;
221
- case '/keys':
222
- case '/key':
223
- if (args.startsWith('set ')) {
224
- const keyParts = args.substring(4).trim().split(/\s+/);
225
- if (keyParts.length >= 2) {
226
- setApiKey(keyParts[0], keyParts.slice(1).join(' '));
227
- }
228
- else {
229
- console.log('\nUsage: /key set <provider> <api-key>\n');
230
- }
231
- }
232
- else {
233
- showConfigStatus();
234
- }
235
- return true;
236
- case '/channel':
237
- case '/channels':
238
- showChannelStatus();
239
- return true;
240
- case '/persona':
241
- console.log('\nTo view/edit persona, use: twinclaw persona');
242
- console.log('To edit in chat: /persona edit <soul|identity|user> <text>');
243
- console.log('--------------\n');
244
- return true;
245
- case '/doctor':
246
- console.log('\nRunning diagnostics...\n');
247
- console.log('Use: twinclaw doctor');
248
- console.log('------\n');
249
- return true;
250
- case '/config':
251
- console.log('\n--- Configuration Menu ---');
252
- console.log('Commands:');
253
- console.log(' /config show - Show full config');
254
- console.log(' /config edit - Open interactive editor');
255
- console.log(' /model set <id> - Change primary model');
256
- console.log(' /key set <prov> <key> - Set API key');
257
- console.log('------------------------\n');
258
- return true;
259
- case '/clear':
260
- console.log('\nConversation cleared.\n');
261
- return true;
262
- case '/quit':
263
- console.log('\nExiting chat mode. Use Ctrl+C to exit.\n');
264
- return true;
265
- default:
266
- return false;
267
- }
268
- }
35
+ import { CommandRouter } from './command-router.js';
269
36
  export function startBasicREPL(gateway) {
270
37
  console.log('TwinClaw basic REPL started.');
271
38
  console.log('Type /help for available commands.\n');
272
39
  void logThought('Basic REPL started.');
273
40
  const sessionId = 'default_repl';
274
41
  createSession(sessionId);
42
+ const router = new CommandRouter(gateway);
275
43
  rl.on('line', async (line) => {
276
44
  if (!line.trim())
277
45
  return;
@@ -279,9 +47,9 @@ export function startBasicREPL(gateway) {
279
47
  try {
280
48
  // Handle / commands
281
49
  if (line.trim().startsWith('/')) {
282
- if (handleReplCommand(line, gateway, sessionId)) {
283
- return;
284
- }
50
+ const response = await router.dispatch(line.trim(), 'repl', sessionId);
51
+ console.log(response);
52
+ return;
285
53
  }
286
54
  const responseText = await gateway.processText(sessionId, line);
287
55
  console.log(`\nTwinClaw: ${responseText}\n`);
@@ -622,7 +390,7 @@ function inferPrimaryModel(config) {
622
390
  return 'google/gemini-flash-lite-latest';
623
391
  }
624
392
  if (hasValue(config.models.modalApiKey)) {
625
- return 'modal/zai-org/GLM-5-FP8';
393
+ return 'modal-zai-glm-5-fp8';
626
394
  }
627
395
  return null;
628
396
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
2
  import { readQueueStateCounts } from './queue-metrics.js';
3
+ import { getDatabasePath } from '../config/workspace.js';
4
4
  function printQueueUsage() {
5
5
  console.log(`Queue commands:
6
6
  twinclaw queue status Show message queue status
@@ -16,7 +16,7 @@ Examples:
16
16
  twinclaw queue retry 550e8400-e29b-41d4-a716-446655440000`);
17
17
  }
18
18
  function getDbPath() {
19
- return path.resolve('memory', 'twinclaw.db');
19
+ return getDatabasePath();
20
20
  }
21
21
  function queryDb(sql) {
22
22
  const dbPath = getDbPath();
@@ -1,8 +1,8 @@
1
1
  import { readConfig } from '../config/json-config.js';
2
2
  import { runDoctorChecks, BINARY_CHECKS, CONFIG_CHECKS, CHANNEL_CHECKS } from './doctor.js';
3
3
  import { execSync } from 'child_process';
4
- import * as path from 'node:path';
5
4
  import { readQueueStateCounts } from './queue-metrics.js';
5
+ import { getDatabasePath } from '../config/workspace.js';
6
6
  export function getGatewayStatus() {
7
7
  try {
8
8
  const result = execSync('sc query "TwinClaw Gateway"', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
@@ -23,7 +23,7 @@ export function getGatewayStatus() {
23
23
  }
24
24
  }
25
25
  export function getQueueStatus() {
26
- const counts = readQueueStateCounts(path.resolve('memory', 'twinclaw.db'));
26
+ const counts = readQueueStateCounts(getDatabasePath());
27
27
  return {
28
28
  pending: counts.queued,
29
29
  failed: counts.failed,
@@ -1,24 +1,9 @@
1
1
  import TelegramBot from 'node-telegram-bot-api';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { readConfig, writeConfig } from '../config/json-config.js';
5
- import { getModelCatalogService } from '../services/model-catalog-service.js';
6
4
  /** Minimum ms delay between processing successive messages (human-like pacing). */
7
5
  const RATE_LIMIT_MS = 1500;
8
- const TELEGRAM_COMMANDS = [
9
- { cmd: '/start', desc: 'Show welcome message and menu' },
10
- { cmd: '/help', desc: 'Show all available commands' },
11
- { cmd: '/status', desc: 'Show gateway status' },
12
- { cmd: '/menu', desc: 'Show this menu' },
13
- { cmd: '/models', desc: 'Show configured models' },
14
- { cmd: '/model set', desc: 'Set primary model' },
15
- { cmd: '/keys', desc: 'Show API keys status' },
16
- { cmd: '/key set', desc: 'Set API key' },
17
- { cmd: '/channel', desc: 'Show channel status' },
18
- { cmd: '/persona', desc: 'Show persona' },
19
- { cmd: '/doctor', desc: 'Run diagnostics' },
20
- { cmd: '/clear', desc: 'Clear conversation' },
21
- ];
6
+ import { CommandRouter } from '../core/command-router.js';
22
7
  /**
23
8
  * Wraps the Telegram Bot API to provide:
24
9
  * - Inbound message normalization for the dispatcher
@@ -94,179 +79,10 @@ export class TelegramHandler {
94
79
  console.error('[TelegramHandler] Polling error:', err.message);
95
80
  });
96
81
  }
97
- // ── Command Handlers ─────────────────────────────────────────────────────────
98
82
  async handleCommand(chatId, command) {
99
- const parts = command.toLowerCase().trim().split(/\s+/);
100
- const cmd = parts[0];
101
- const args = parts.slice(1).join(' ');
102
- switch (cmd) {
103
- case '/start':
104
- case '/menu':
105
- const menuText = `🎯 *TwinClaw Menu*
106
-
107
- ${TELEGRAM_COMMANDS.map(c => `${c.cmd} - ${c.desc}`).join('\n')}
108
-
109
- _How can I help you today?_
110
- `;
111
- await this.#bot.sendMessage(chatId, menuText, { parse_mode: 'Markdown' });
112
- break;
113
- case '/help':
114
- await this.#bot.sendMessage(chatId, `📋 *Available Commands:*
115
-
116
- ${TELEGRAM_COMMANDS.map(c => `${c.cmd} - ${c.desc}`).join('\n')}
117
-
118
- _Just send me a message and I'll respond!_
119
- `, { parse_mode: 'Markdown' });
120
- break;
121
- case '/status':
122
- await this.#bot.sendMessage(chatId, `✅ *Gateway Status:*
123
-
124
- • Status: Running
125
- • Platform: Telegram
126
- • Mode: AI Assistant
127
-
128
- _All systems operational_
129
- `, { parse_mode: 'Markdown' });
130
- break;
131
- case '/models':
132
- case '/model':
133
- if (args.startsWith('list')) {
134
- const catalog = getModelCatalogService().getAllModels();
135
- const config = await readConfig();
136
- const currentPrimary = config.models.primaryModel || 'not set';
137
- let modelList = `📦 *Available Models*\n\nCurrent: *${currentPrimary}*\n\n`;
138
- const providerGroups = new Map();
139
- for (const model of catalog) {
140
- const models = providerGroups.get(model.provider) || [];
141
- models.push(model);
142
- providerGroups.set(model.provider, models);
143
- }
144
- let num = 1;
145
- for (const [provider, models] of providerGroups) {
146
- modelList += `*${provider.toUpperCase()}:*\n`;
147
- for (const m of models.slice(0, 5)) {
148
- const isCurrent = m.model === currentPrimary || m.id === currentPrimary;
149
- modelList += `${num}. ${m.name}${isCurrent ? ' ✅' : ''}\n`;
150
- num++;
151
- }
152
- if (models.length > 5)
153
- modelList += ` ...and ${models.length - 5} more\n`;
154
- modelList += '\n';
155
- }
156
- modelList += '_To switch: /model set <number>_';
157
- await this.#bot.sendMessage(chatId, modelList, { parse_mode: 'Markdown' });
158
- }
159
- else if (args.startsWith('set ') && args.length > 4) {
160
- const modelIdOrNum = args.substring(4).trim();
161
- const catalog = getModelCatalogService().getAllModels();
162
- let newModelId = modelIdOrNum;
163
- if (/^\d+$/.test(modelIdOrNum)) {
164
- const idx = parseInt(modelIdOrNum, 10) - 1;
165
- if (idx >= 0 && idx < catalog.length) {
166
- newModelId = `${catalog[idx].provider}/${catalog[idx].model}`;
167
- }
168
- else {
169
- await this.#bot.sendMessage(chatId, `❌ Invalid model number. Use /model list to see available models.`);
170
- break;
171
- }
172
- }
173
- const config = await readConfig();
174
- config.models.primaryModel = newModelId;
175
- await writeConfig(config);
176
- await this.#bot.sendMessage(chatId, `✅ *Model updated!*\n\nNew primary model: *${newModelId}*\n\nRestart TwinClaw for changes to take effect.`, { parse_mode: 'Markdown' });
177
- }
178
- else {
179
- const config = await readConfig();
180
- const currentPrimary = config.models.primaryModel || 'not set';
181
- await this.#bot.sendMessage(chatId, `📦 *Models*
182
-
183
- Current: *${currentPrimary}*
184
-
185
- Commands:
186
- • /model list - List available models
187
- • /model set <number> - Set primary model
188
-
189
- Example: /model set 1
190
- `, { parse_mode: 'Markdown' });
191
- }
192
- break;
193
- case '/keys':
194
- case '/key':
195
- if (args.startsWith('set ') && args.length > 4) {
196
- const keyParts = args.substring(4).trim().split(/\s+/);
197
- if (keyParts.length >= 2) {
198
- const provider = keyParts[0];
199
- const key = keyParts.slice(1).join(' ');
200
- 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_
201
- `, { parse_mode: 'Markdown' });
202
- }
203
- else {
204
- await this.#bot.sendMessage(chatId, `Usage: /key set <provider> <api-key>\n\nExample: /key set modal sk-xxxxx`);
205
- }
206
- }
207
- else {
208
- await this.#bot.sendMessage(chatId, `🔑 *API Keys*
209
-
210
- Use *twinclaw config* to manage API keys.
211
-
212
- Commands:
213
- • /keys show - Show key status
214
- • /key set <provider> <key> - Set API key
215
-
216
- _Or run: twinclaw config_
217
- `, { parse_mode: 'Markdown' });
218
- }
219
- break;
220
- case '/channel':
221
- case '/channels':
222
- await this.#bot.sendMessage(chatId, `📱 *Channels*
223
-
224
- Telegram: ✅ Connected
225
- WhatsApp: Use /channels to configure
226
-
227
- Run: *twinclaw channels* to manage
228
- `, { parse_mode: 'Markdown' });
229
- break;
230
- case '/persona':
231
- await this.#bot.sendMessage(chatId, `👤 *Persona*
232
-
233
- Run: *twinclaw persona* to view/edit
234
-
235
- Commands:
236
- • twinclaw persona show - View persona
237
- • twinclaw persona edit soul "text" - Edit soul
238
- `, { parse_mode: 'Markdown' });
239
- break;
240
- case '/doctor':
241
- await this.#bot.sendMessage(chatId, `🩺 *Diagnostics*
242
-
243
- Run: *twinclaw doctor* to check system health
244
-
245
- Checks:
246
- • Environment variables
247
- • API keys
248
- • Configuration
249
- • Channels
250
- `, { parse_mode: 'Markdown' });
251
- break;
252
- case '/config':
253
- await this.#bot.sendMessage(chatId, `⚙️ *Configuration*
254
-
255
- Use: *twinclaw config* to:
256
- • Edit configuration
257
- • Set API keys
258
- • Change models
259
- • Manage channels
260
-
261
- Run: *twinclaw config* in terminal
262
- `, { parse_mode: 'Markdown' });
263
- break;
264
- case '/clear':
265
- await this.#bot.sendMessage(chatId, '🗑️ Conversation cleared! Starting fresh.');
266
- break;
267
- default:
268
- await this.#bot.sendMessage(chatId, `Unknown command: ${command}\nType /menu for available commands.`);
269
- }
83
+ const router = new CommandRouter();
84
+ const response = await router.dispatch(command, 'telegram', String(chatId));
85
+ await this.#bot.sendMessage(chatId, response, { parse_mode: 'Markdown' });
270
86
  }
271
87
  // ── Public Send Methods ───────────────────────────────────────────────────────
272
88
  /** Send a plain-text reply to a chat. */