@thelapyae/geniclaw 1.1.11 → 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/bin/geniclaw.js CHANGED
@@ -1,82 +1,139 @@
1
1
  #!/usr/bin/env node
2
- const { spawn, execSync } = require('child_process');
3
- const path = require('path');
2
+ const { program } = require('commander');
4
3
  const fs = require('fs');
4
+ const path = require('path');
5
5
  const os = require('os');
6
- const { program } = require('commander');
6
+ const { spawn, execSync, fork } = require('child_process');
7
7
  const inquirer = require('inquirer');
8
- const https = require('https'); // For simple URL fetch if needed in TUI directly or via script
8
+ const { ConfigManager } = require('../dist/config-manager');
9
9
 
10
- // Config Paths
10
+ // Paths
11
11
  const LOCAL_DIR = path.join(process.cwd(), '.geniclaw');
12
12
  const GENICLAW_WORK_DIR = process.env.GENICLAW_WORK_DIR || (fs.existsSync(LOCAL_DIR) ? LOCAL_DIR : path.join(os.homedir(), '.geniclaw'));
13
13
  const CONFIG_FILE = path.join(GENICLAW_WORK_DIR, 'config.json');
14
14
  const SESSIONS_DIR = path.join(GENICLAW_WORK_DIR, 'sessions');
15
15
  const SKILLS_DIR = path.join(GENICLAW_WORK_DIR, 'skills');
16
+ const { runDoctor } = require('../dist/doctor');
16
17
 
17
18
  const SCRIPT_PATH = path.join(__dirname, '../geniclaw.sh');
18
19
 
19
20
  // Helper to run shell script commands
20
21
  function runScript(args) {
21
- const child = spawn(SCRIPT_PATH, args, {
22
- stdio: 'inherit',
23
- env: process.env
24
- });
25
-
26
- return new Promise((resolve) => {
27
- child.on('exit', (code) => {
28
- resolve(code);
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(SCRIPT_PATH, args, { stdio: 'inherit' });
24
+ child.on('close', (code) => {
25
+ if (code === 0) resolve();
26
+ else reject(new Error(`Script exited with code ${code}`));
29
27
  });
28
+ child.on('error', reject);
30
29
  });
31
30
  }
32
31
 
33
- // Load Config
32
+ // Config Helpers
34
33
  function loadConfig() {
35
- if (fs.existsSync(CONFIG_FILE)) {
36
- try {
37
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
38
- } catch (e) {
39
- return {};
40
- }
41
- }
42
- return {};
34
+ return ConfigManager.load();
43
35
  }
44
36
 
45
- // Save Config
46
37
  function saveConfig(config) {
47
- if (!fs.existsSync(GENICLAW_WORK_DIR)) {
48
- fs.mkdirSync(GENICLAW_WORK_DIR, { recursive: true });
49
- }
50
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
38
+ ConfigManager.save(config);
39
+ }
40
+
41
+ // Ensure sessions dir exists
42
+ if (!fs.existsSync(SESSIONS_DIR)) {
43
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true });
44
+ }
45
+
46
+ async function promptContinue() {
47
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
51
48
  }
52
49
 
53
- // Helper: Get Skills
54
- function getSkills() {
50
+ // Skill Management
51
+ function listSkills() {
55
52
  if (!fs.existsSync(SKILLS_DIR)) return [];
56
53
  return fs.readdirSync(SKILLS_DIR)
57
54
  .filter(f => f.endsWith('.json'))
58
55
  .map(f => {
59
- try { return JSON.parse(fs.readFileSync(path.join(SKILLS_DIR, f), 'utf8')); }
60
- catch(e) { return null; }
56
+ try {
57
+ const data = JSON.parse(fs.readFileSync(path.join(SKILLS_DIR, f), 'utf8'));
58
+ return { name: data.name, filename: f, description: data.description };
59
+ } catch (e) { return null; }
61
60
  })
62
61
  .filter(s => s !== null);
63
62
  }
64
63
 
65
- // Helper: Install Skill
64
+ async function manageSkills() {
65
+ const config = loadConfig();
66
+ const skills = listSkills();
67
+ const activeSkill = config.activeSkill || 'None';
68
+
69
+ console.clear();
70
+ console.log('🧠 Skill Manager');
71
+ console.log(`Active Skill: ${activeSkill}`);
72
+ console.log('----------------');
73
+
74
+ const { action } = await inquirer.prompt([
75
+ {
76
+ type: 'list',
77
+ name: 'action',
78
+ message: 'Choose an action:',
79
+ choices: [
80
+ { name: 'Activate/Deactivate Skill', value: 'activate' },
81
+ { name: 'Install New Skill (URL)', value: 'install' },
82
+ { name: 'List Installed Skills', value: 'list' },
83
+ { name: 'Back', value: 'back' }
84
+ ]
85
+ }
86
+ ]);
87
+
88
+ if (action === 'back') return;
89
+
90
+ if (action === 'activate') {
91
+ const choices = [{ name: 'None (Disable Skills)', value: null }, ...skills.map(s => ({ name: `${s.name} - ${s.description}`, value: s.name }))];
92
+ const { skill } = await inquirer.prompt([
93
+ { type: 'list', name: 'skill', message: 'Select Skill to Activate:', choices }
94
+ ]);
95
+ config.activeSkill = skill; // null if None
96
+ saveConfig(config);
97
+ console.log(`✅ Active Skill set to: ${skill || 'None'}`);
98
+ }
99
+
100
+ if (action === 'install') {
101
+ const { url } = await inquirer.prompt([
102
+ { type: 'input', name: 'url', message: 'Enter Skill JSON URL:' }
103
+ ]);
104
+ try {
105
+ await installSkillFromUrl(url);
106
+ console.log('✅ Skill installed successfully.');
107
+ } catch (e) {
108
+ console.log(`❌ Error installing skill: ${e.message}`);
109
+ }
110
+ }
111
+
112
+ if (action === 'list') {
113
+ skills.forEach(s => console.log(`- ${s.name}: ${s.description}`));
114
+ }
115
+
116
+ await promptContinue();
117
+ return manageSkills();
118
+ }
119
+
66
120
  async function installSkillFromUrl(url) {
121
+ // Simple fetch and save
122
+ // In a real app, use the SkillManager class from src/skill-manager.ts if possible,
123
+ // but here we are in JS land (bin).
124
+ // actually we can require the built JS from dist if needed, but let's keep it simple dependency-free if we can or use the one we have.
125
+ // The previous implementation used a fetch.
67
126
  return new Promise((resolve, reject) => {
127
+ const https = require('https');
68
128
  https.get(url, (res) => {
69
129
  let data = '';
70
130
  res.on('data', chunk => data += chunk);
71
131
  res.on('end', () => {
72
132
  try {
73
133
  const skill = JSON.parse(data);
74
- if (!skill.name || !skill.systemInstruction) {
75
- reject(new Error("Invalid skill JSON"));
76
- return;
77
- }
134
+ if (!skill.name || !skill.prompts) throw new Error('Invalid Skill JSON');
78
135
  if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
79
- const safeName = skill.name.replace(/[^a-zA-Z0-9_-]/g, '_');
136
+ const safeName = skill.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
80
137
  fs.writeFileSync(path.join(SKILLS_DIR, `${safeName}.json`), JSON.stringify(skill, null, 2));
81
138
  resolve(skill);
82
139
  } catch(e) { reject(e); }
@@ -95,27 +152,35 @@ async function showMenu() {
95
152
  const config = loadConfig();
96
153
  const activeSession = config.activeSession || 'default';
97
154
  const activeSkill = config.activeSkill || 'None';
155
+ const activeProvider = config.activeProvider || 'gemini';
156
+
157
+ // Check if the ACTIVE provider has a key
158
+ const activeKeyField = `${activeProvider}ApiKey`;
159
+ const hasKey = config[activeKeyField] || process.env[activeKeyField.toUpperCase().replace(/([A-Z])/g, '_$1').toUpperCase()] || (activeProvider === 'gemini' && process.env.GEMINI_API_KEY);
98
160
 
99
- if (!config.geminiApiKey) {
100
- console.log('⚠️ Configuration Missing: No Gemini API Key found.');
161
+ if (!hasKey) {
162
+ console.log(`⚠️ Configuration Missing: No API Key found for active provider (${activeProvider}).`);
101
163
  const { runSetup } = await inquirer.prompt([
102
- { type: 'confirm', name: 'runSetup', message: 'Would you like to run the setup wizard now?', default: true }
164
+ { type: 'confirm', name: 'runSetup', message: 'Would you like to configure keys now?', default: true }
103
165
  ]);
104
166
 
105
167
  if (runSetup) {
106
- await runScript(['setup']);
107
- await promptContinue();
168
+ // If they say yes, go to keys menu directly? Or setup?
169
+ // Local setup script is gemini-focused. Let's send them to manageApiKey menu.
170
+ await manageApiKeys(activeProvider);
108
171
  return showMenu();
109
172
  }
110
173
  }
111
174
 
112
175
  // Show current status summary
113
176
  try {
114
- console.log(`Current Model: ${config.geminiModel || 'Default'}`);
115
- console.log(`Active Session: 📂 ${activeSession}`);
116
- console.log(`Active Skill: 🧠 ${activeSkill}`);
117
- if (config.geminiApiUrl) console.log(`API URL: 🔌 ${config.geminiApiUrl}`);
118
- if (config.geminiGatewayKey) console.log(`Gateway Key: 🔑 Set`);
177
+ console.log(`Active Provider: ${config.activeProvider || 'gemini'} (${config.activeProvider === 'gemini' ? config.geminiModel : (config[config.activeProvider + 'Model'] || 'Default')})`);
178
+ console.log(`Active Session: 📂 ${activeSession}`);
179
+ console.log(`Active Skill: 🧠 ${activeSkill}`);
180
+ if (config.providerRouting && (config.providerRouting.chat !== config.activeProvider)) {
181
+ console.log(`Routing: 🔀 Custom`);
182
+ }
183
+ if (config.geminiApiUrl) console.log(`API URL: 🔌 ${config.geminiApiUrl}`);
119
184
  } catch (e) {}
120
185
  console.log('');
121
186
 
@@ -128,16 +193,22 @@ async function showMenu() {
128
193
  { name: 'Start Daemon', value: 'start' },
129
194
  { name: 'Stop Daemon', value: 'stop' },
130
195
  { name: 'Restart Daemon', value: 'restart' },
131
- { name: 'Check Status', value: 'status' },
132
196
  { name: 'View Logs', value: 'logs' },
133
197
  new inquirer.Separator(),
134
- { name: '📂 Manage Sessions', value: 'sessions' },
198
+ { name: '🤖 Select AI Provider & Model', value: 'provider' },
199
+ { name: '🔑 Manage API Keys', value: 'keys' },
200
+ { name: '🔀 Task Routing (Chat/Heartbeat/Cron)', value: 'routing' },
201
+ { name: '🔎 Web Search Config', value: 'search' },
135
202
  { name: '🧠 Manage Skills', value: 'skills' },
136
- { name: '🤖 Change Gemini Model', value: 'model' },
137
- { name: '🔌 Change API Base URL', value: 'apiUrl' },
203
+ { name: '📂 Switch Session', value: 'session' },
204
+ { name: ' Create New Session', value: 'new_session' },
205
+ { name: '🗑️ Delete Session', value: 'delete_session' },
206
+ new inquirer.Separator(),
207
+ { name: '🔌 Set Custom API URL', value: 'proxy' },
138
208
  { name: '🔑 Set Gateway/Proxy Key', value: 'gatewayKey' },
139
209
  new inquirer.Separator(),
140
210
  { name: '✨ Update GeniClaw', value: 'update' },
211
+ { name: '🚑 Run Doctor (Fix Issues)', value: 'doctor' },
141
212
  { name: 'Run Setup Wizard', value: 'setup' },
142
213
  { name: 'Exit', value: 'exit' }
143
214
  ]
@@ -145,13 +216,20 @@ async function showMenu() {
145
216
  ]);
146
217
 
147
218
  if (action === 'exit') {
219
+ console.log('Bye! 👋');
148
220
  process.exit(0);
149
221
  }
150
-
222
+
151
223
  if (action === 'update') {
152
224
  await performUpdate();
153
225
  return; // Exit after update to allow restart
154
226
  }
227
+
228
+ if (action === 'doctor') {
229
+ await runDoctor();
230
+ await promptContinue();
231
+ return showMenu();
232
+ }
155
233
 
156
234
  if (action === 'setup') {
157
235
  await runScript(['setup']);
@@ -159,125 +237,111 @@ async function showMenu() {
159
237
  return showMenu();
160
238
  }
161
239
 
162
- if (action === 'model') {
163
- await changeModel();
240
+ if (action === 'start' || action === 'stop' || action === 'restart') {
241
+ await runScript([action]);
164
242
  await promptContinue();
165
243
  return showMenu();
166
244
  }
167
245
 
168
- if (action === 'apiUrl') {
169
- await changeApiUrl();
246
+ if (action === 'logs') {
247
+ await runScript(['logs']);
170
248
  await promptContinue();
171
249
  return showMenu();
172
250
  }
173
251
 
174
- if (action === 'gatewayKey') {
175
- await changeGatewayKey();
176
- await promptContinue();
252
+ if (action === 'session') {
253
+ await switchSession();
177
254
  return showMenu();
178
255
  }
179
256
 
180
- if (action === 'sessions') {
181
- await manageSessions();
257
+ if (action === 'new_session') {
258
+ await createSession();
182
259
  return showMenu();
183
260
  }
184
261
 
262
+ if (action === 'delete_session') {
263
+ await deleteSession();
264
+ return showMenu();
265
+ }
266
+
185
267
  if (action === 'skills') {
186
268
  await manageSkills();
187
269
  return showMenu();
188
270
  }
189
271
 
190
- if (action === 'logs') {
191
- await showLogsMenu();
272
+ if (action === 'provider') {
273
+ await selectProviderAndModel();
192
274
  return showMenu();
193
275
  }
194
276
 
195
- // Process other actions via script
196
- await runScript([action]);
197
- await promptContinue();
198
- return showMenu();
199
- }
277
+ if (action === 'keys') {
278
+ await manageApiKeys();
279
+ return showMenu();
280
+ }
200
281
 
201
- async function promptContinue() {
202
- await inquirer.prompt([
203
- {
204
- type: 'input',
205
- name: 'continue',
206
- message: 'Press Enter to continue...',
207
- }
208
- ]);
209
- }
282
+ if (action === 'routing') {
283
+ await manageTaskRouting();
284
+ return showMenu();
285
+ }
210
286
 
211
- async function changeModel() {
212
- const config = loadConfig();
213
- const currentModel = config.geminiModel || 'gemini-2.5-flash';
287
+ if (action === 'search') {
288
+ await configureWebSearch();
289
+ return showMenu();
290
+ }
214
291
 
215
- const { model } = await inquirer.prompt([
216
- {
217
- type: 'list',
218
- name: 'model',
219
- message: 'Select Gemini Model:',
220
- default: currentModel,
221
- choices: [
222
- 'gemini-2.5-flash',
223
- 'gemini-flash-lite-latest',
224
- 'gemini-2.5-pro',
225
- 'gemini-3-flash-preview',
226
- 'gemini-3-pro-preview',
227
- new inquirer.Separator(),
228
- { name: 'Custom (Enter manually)', value: 'custom' }
229
- ]
230
- }
231
- ]);
292
+ if (action === 'proxy') {
293
+ await setCustomUrl();
294
+ return showMenu();
295
+ }
232
296
 
233
- let selectedModel = model;
234
- if (model === 'custom') {
235
- const { customModel } = await inquirer.prompt([
236
- {
237
- type: 'input',
238
- name: 'customModel',
239
- message: 'Enter model name:',
240
- validate: input => input.length > 0
241
- }
242
- ]);
243
- selectedModel = customModel;
297
+ if (action === 'gatewayKey') {
298
+ await setGatewayKey();
299
+ return showMenu();
244
300
  }
301
+ }
245
302
 
246
- config.geminiModel = selectedModel;
247
- saveConfig(config);
248
- console.log(`✅ Model updated to: ${selectedModel}`);
249
- console.log('Note: You may need to restart the daemon for changes to take effect.');
303
+ async function performUpdate() {
304
+ console.log('Checking for updates...');
305
+ try {
306
+ const updateCmd = spawn('npm', ['install', '-g', '@thelapyae/geniclaw@latest'], { stdio: 'inherit' });
307
+ updateCmd.on('close', (code) => {
308
+ if (code === 0) {
309
+ console.log('✅ Update successful! Please restart GeniClaw.');
310
+ process.exit(0);
311
+ } else {
312
+ console.log('❌ Update failed.');
313
+ }
314
+ });
315
+ } catch (e) {
316
+ console.log('❌ Error updating: ' + e.message);
317
+ }
250
318
  }
251
319
 
252
- async function changeApiUrl() {
320
+ async function setCustomUrl() {
253
321
  const config = loadConfig();
254
322
  const currentUrl = config.geminiApiUrl || 'Default (Google)';
255
-
323
+
256
324
  const { url } = await inquirer.prompt([
257
325
  {
258
326
  type: 'input',
259
327
  name: 'url',
260
328
  message: `Enter Custom API Base URL (Current: ${currentUrl}):`,
261
- suffix: '\n(Leave empty to reset to default Google API)',
329
+ suffix: '\n(Leave empty to reset to default)',
262
330
  }
263
331
  ]);
264
332
 
265
- if (!url || url.trim() === '') {
266
- delete config.geminiApiUrl;
267
- console.log('✅ Reset to default Google API URL.');
268
- } else {
269
- // Basic validation
270
- if (!url.startsWith('http')) {
271
- console.log('⚠️ URL should start with http:// or https://');
272
- }
333
+ if (url && url.trim() !== '') {
273
334
  config.geminiApiUrl = url.trim();
274
- console.log(`✅ API URL updated to: ${config.geminiApiUrl}`);
335
+ console.log(`✅ API URL set to: ${url}`);
336
+ } else {
337
+ delete config.geminiApiUrl;
338
+ console.log('✅ API URL reset to default.');
275
339
  }
276
340
  saveConfig(config);
277
- console.log('Note: Restart daemon to apply changes.');
341
+ await promptContinue();
278
342
  }
279
343
 
280
- async function changeGatewayKey() {
344
+ async function setGatewayKey() {
281
345
  const config = loadConfig();
282
346
  const currentKey = config.geminiGatewayKey ? '********' : 'Not Set';
283
347
 
@@ -285,308 +349,398 @@ async function changeGatewayKey() {
285
349
  {
286
350
  type: 'password',
287
351
  name: 'key',
288
- message: `Enter Gateway API Key (Current: ${currentKey}):`,
289
- suffix: '\n(Leave empty to remove key)',
352
+ message: `Enter Gateway/Proxy Auth Key (Current: ${currentKey}):`,
353
+ suffix: '\n(Leave empty to remove/keep)',
290
354
  mask: '*'
291
355
  }
292
356
  ]);
293
357
 
294
- if (!key || key.trim() === '') {
295
- delete config.geminiGatewayKey;
296
- console.log('✅ Gateway Key removed.');
297
- } else {
358
+ if (key && key.trim() !== '') {
298
359
  config.geminiGatewayKey = key.trim();
360
+ saveConfig(config);
299
361
  console.log('✅ Gateway Key updated.');
362
+ } else if (key === '') {
363
+ // Ask if remove? Na, empty usually means keep or skip.
364
+ // Let's add specific logic: if empty, do nothing.
365
+ // If they want to clear, they might need a clear option.
366
+ // For CLI simplicity, let's keep it as is.
367
+ console.log('No change.');
300
368
  }
301
- saveConfig(config);
302
- console.log('Note: Restart daemon to apply changes.');
369
+ await promptContinue();
303
370
  }
304
371
 
305
- async function manageSessions() {
372
+ async function selectProviderAndModel() {
306
373
  const config = loadConfig();
307
- const activeSession = config.activeSession || 'default';
308
- const sessions = fs.existsSync(SESSIONS_DIR)
309
- ? fs.readdirSync(SESSIONS_DIR).filter(file => fs.statSync(path.join(SESSIONS_DIR, file)).isDirectory())
310
- : ['default'];
374
+ let provider = config.activeProvider || 'gemini';
311
375
 
312
- const { action } = await inquirer.prompt([
376
+ // 1. Select Provider
377
+ const { newProvider } = await inquirer.prompt([
313
378
  {
314
379
  type: 'list',
315
- name: 'action',
316
- message: `Manage Sessions (Active: ${activeSession})`,
380
+ name: 'newProvider',
381
+ message: 'Select Active AI Provider:',
382
+ default: provider,
317
383
  choices: [
318
- { name: 'Switch Session', value: 'switch' },
319
- { name: 'Create New Session', value: 'create' },
320
- { name: 'Delete Session', value: 'delete' },
384
+ { name: 'Google Gemini', value: 'gemini' },
385
+ { name: 'OpenAI', value: 'openai' },
386
+ { name: 'Anthropic', value: 'anthropic' },
387
+ { name: 'Groq', value: 'groq' },
388
+ { name: 'Deepseek', value: 'deepseek' },
389
+ { name: 'xAI (Grok)', value: 'grok' },
321
390
  { name: 'Back', value: 'back' }
322
391
  ]
323
392
  }
324
393
  ]);
325
394
 
326
- if (action === 'back') return;
327
-
328
- if (action === 'switch') {
329
- const { session } = await inquirer.prompt([
395
+ if (newProvider === 'back') return;
396
+ config.activeProvider = newProvider;
397
+ saveConfig(config);
398
+ console.log(`✅ Active Provider set to: ${newProvider}`);
399
+
400
+ // 2. Select Model for that Provider
401
+ if (newProvider === 'gemini') {
402
+ const { model } = await inquirer.prompt([
330
403
  {
331
404
  type: 'list',
332
- name: 'session',
333
- message: 'Select session to switch to:',
334
- choices: sessions.map(s => s === activeSession ? { name: `${s} (active)`, value: s } : s)
405
+ name: 'model',
406
+ message: 'Select Gemini Model:',
407
+ default: config.geminiModel,
408
+ choices: [
409
+ { name: 'Gemini 3 Pro Preview', value: 'gemini-3-pro-preview' },
410
+ { name: 'Gemini 3 Flash Preview', value: 'gemini-3-flash-preview' },
411
+ { name: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro' },
412
+ { name: 'Gemini 2.5 Flash', value: 'gemini-2.5-flash' },
413
+ { name: 'Gemini 2.5 Flash Lite', value: 'gemini-2.5-flash-lite' }
414
+ ]
335
415
  }
336
416
  ]);
337
-
338
- if (session !== activeSession) {
339
- config.activeSession = session;
340
- saveConfig(config);
341
- console.log(`✅ Switched to session: ${session}`);
342
- } else {
343
- console.log('Already on this session.');
344
- }
345
- }
346
-
347
- if (action === 'create') {
348
- const { name } = await inquirer.prompt([
417
+ config.geminiModel = model;
418
+ } else if (newProvider === 'openai') {
419
+ const { model } = await inquirer.prompt([
349
420
  {
350
- type: 'input',
351
- name: 'name',
352
- message: 'Enter new session name (alphanumeric):',
353
- validate: input => /^[a-zA-Z0-9_-]+$/.test(input) ? true : 'Invalid name.'
421
+ type: 'list',
422
+ name: 'model',
423
+ message: 'Select OpenAI Model:',
424
+ default: config.openaiModel,
425
+ choices: [
426
+ { name: 'GPT-5.3 Codex (Best for Code)', value: 'gpt-5.3-codex' },
427
+ { name: 'GPT-5.2 Pro', value: 'gpt-5.2-pro' },
428
+ { name: 'GPT-5.2', value: 'gpt-5.2' },
429
+ { name: 'GPT-4.1 Nano', value: 'gpt-4.1-nano' },
430
+ { name: 'OpenAI o3-mini', value: 'o3-mini' }
431
+ ]
354
432
  }
355
433
  ]);
356
-
357
- const sessionPath = path.join(SESSIONS_DIR, name);
358
- if (fs.existsSync(sessionPath)) {
359
- console.log('❌ Session already exists.');
360
- } else {
361
- fs.mkdirSync(sessionPath, { recursive: true });
362
- console.log(`✅ Session '${name}' created.`);
363
- const { switchTo } = await inquirer.prompt([
364
- { type: 'confirm', name: 'switchTo', message: 'Switch to this session now?', default: true }
365
- ]);
366
- if (switchTo) {
367
- config.activeSession = name;
368
- saveConfig(config);
434
+ config.openaiModel = model;
435
+ } else if (newProvider === 'anthropic') {
436
+ const { model } = await inquirer.prompt([
437
+ {
438
+ type: 'list',
439
+ name: 'model',
440
+ message: 'Select Claude Model:',
441
+ default: config.anthropicModel,
442
+ choices: [
443
+ { name: 'Claude Opus 4.6', value: 'claude-opus-4.6' },
444
+ { name: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },
445
+ { name: 'Claude Haiku 4.5', value: 'claude-haiku-4.5' }
446
+ ]
369
447
  }
370
- }
448
+ ]);
449
+ config.anthropicModel = model;
450
+ } else if (newProvider === 'groq') {
451
+ const { model } = await inquirer.prompt([
452
+ {
453
+ type: 'list',
454
+ name: 'model',
455
+ message: 'Select Groq Model:',
456
+ default: config.groqModel,
457
+ choices: [
458
+ { name: 'Llama 3.3 70B Versatile', value: 'llama-3.3-70b-versatile' },
459
+ { name: 'Llama 3.1 8B Instant', value: 'llama-3.1-8b-instant' },
460
+ { name: 'OpenAI GPT-OSS 120B', value: 'openai/gpt-oss-120b' },
461
+ { name: 'Mixtral 8x7B', value: 'mixtral-8x7b-32768' }
462
+ ]
463
+ }
464
+ ]);
465
+ config.groqModel = model;
466
+ } else if (newProvider === 'deepseek') {
467
+ const { model } = await inquirer.prompt([
468
+ {
469
+ type: 'list',
470
+ name: 'model',
471
+ message: 'Select DeepSeek Model:',
472
+ default: config.deepseekModel,
473
+ choices: [
474
+ { name: 'DeepSeek V3 (General)', value: 'deepseek-chat' },
475
+ { name: 'DeepSeek R1 (Reasoning)', value: 'deepseek-reasoner' },
476
+ { name: 'DeepSeek Coder V3', value: 'deepseek-coder' }
477
+ ]
478
+ }
479
+ ]);
480
+ config.deepseekModel = model;
481
+ } else if (newProvider === 'grok') {
482
+ const { model } = await inquirer.prompt([
483
+ {
484
+ type: 'list',
485
+ name: 'model',
486
+ message: 'Select Grok Model:',
487
+ default: config.grokModel,
488
+ choices: [
489
+ { name: 'Grok 4', value: 'grok-4' },
490
+ { name: 'Grok 4.1 Fast', value: 'grok-4-1-fast' },
491
+ { name: 'Grok 3', value: 'grok-3' },
492
+ { name: 'Grok 2', value: 'grok-2-1212' }
493
+ ]
494
+ }
495
+ ]);
496
+ config.grokModel = model;
371
497
  }
372
498
 
373
- if (action === 'delete') {
374
- const { session } = await inquirer.prompt([
499
+ saveConfig(config);
500
+ console.log(`✅ Model set to: ${config[newProvider + 'Model'] || config.geminiModel}`);
501
+ await promptContinue();
502
+ }
503
+
504
+
505
+ async function manageApiKeys(targetProvider = null) {
506
+ const config = loadConfig();
507
+ let provider = targetProvider;
508
+
509
+ if (!provider) {
510
+ const result = await inquirer.prompt([
375
511
  {
376
512
  type: 'list',
377
- name: 'session',
378
- message: 'Select session to delete:',
379
- choices: sessions.filter(s => s !== 'default' && s !== activeSession)
513
+ name: 'provider',
514
+ message: 'Select Provider to set API Key:',
515
+ choices: [
516
+ { name: 'Google Gemini', value: 'gemini' },
517
+ { name: 'OpenAI', value: 'openai' },
518
+ { name: 'Anthropic', value: 'anthropic' },
519
+ { name: 'Groq', value: 'groq' },
520
+ { name: 'Deepseek', value: 'deepseek' },
521
+ { name: 'xAI (Grok)', value: 'grok' },
522
+ { name: 'Brave Search', value: 'brave' },
523
+ { name: 'Back', value: 'back' }
524
+ ]
380
525
  }
381
526
  ]);
382
- if (session) {
383
- const { confirm } = await inquirer.prompt([
384
- { type: 'confirm', name: 'confirm', message: `Delete '${session}' permanently?`, default: false }
385
- ]);
386
- if (confirm) {
387
- fs.rmSync(path.join(SESSIONS_DIR, session), { recursive: true, force: true });
388
- console.log(`🗑️ Session deleted.`);
389
- }
527
+ if (result.provider === 'back') return;
528
+ provider = result.provider;
529
+ }
530
+
531
+ const keyField = `${provider}ApiKey`;
532
+ const currentKey = config[keyField] ? '********' : 'Not Set';
533
+
534
+ const { key } = await inquirer.prompt([
535
+ {
536
+ type: 'password',
537
+ name: 'key',
538
+ message: `Enter API Key for ${provider} (Current: ${currentKey}):`,
539
+ suffix: '\n(Leave empty to keep current)',
540
+ mask: '*'
390
541
  }
542
+ ]);
543
+
544
+ if (key && key.trim() !== '') {
545
+ config[keyField] = key.trim();
546
+ saveConfig(config);
547
+ console.log(`✅ API Key for ${provider} updated.`);
548
+ } else {
549
+ console.log('No change.');
391
550
  }
392
- return manageSessions();
551
+
552
+ if (!targetProvider) await promptContinue();
393
553
  }
394
554
 
395
- async function manageSkills() {
555
+ async function manageTaskRouting() {
396
556
  const config = loadConfig();
397
- const activeSkill = config.activeSkill || 'None';
398
- const skills = getSkills();
399
-
400
- const { action } = await inquirer.prompt([
557
+ const routing = config.providerRouting || { chat: 'gemini', heartbeat: 'gemini', cron: 'gemini' };
558
+
559
+ const { task } = await inquirer.prompt([
401
560
  {
402
561
  type: 'list',
403
- name: 'action',
404
- message: `Manage Skills (Active: ${activeSkill})`,
562
+ name: 'task',
563
+ message: 'Select Task to Route:',
405
564
  choices: [
406
- { name: 'Acivate/Deactivate Skill', value: 'activate' },
407
- { name: 'Install Skill (from URL)', value: 'install' },
408
- { name: 'Create New Skill', value: 'create' },
409
- { name: 'List Installed Skills', value: 'list' },
410
- { name: 'Delete Skill', value: 'delete' },
565
+ { name: `Chat (Current: ${routing.chat || 'default'})`, value: 'chat' },
566
+ { name: `Heartbeat (Current: ${routing.heartbeat || 'default'})`, value: 'heartbeat' },
567
+ { name: `Cron Jobs (Current: ${routing.cron || 'default'})`, value: 'cron' },
411
568
  { name: 'Back', value: 'back' }
412
569
  ]
413
570
  }
414
571
  ]);
415
572
 
416
- if (action === 'back') return;
573
+ if (task === 'back') return;
417
574
 
418
- if (action === 'activate') {
419
- const choices = [
420
- { name: 'None (Deactivate)', value: null },
421
- new inquirer.Separator(),
422
- ...skills.map(s => ({ name: s.name, value: s.name }))
423
- ];
424
- const { skill } = await inquirer.prompt([
425
- {
426
- type: 'list',
427
- name: 'skill',
428
- message: 'Select skill to activate:',
429
- choices: choices,
430
- default: activeSkill === 'None' ? null : activeSkill
431
- }
432
- ]);
575
+ const { provider } = await inquirer.prompt([
576
+ {
577
+ type: 'list',
578
+ name: 'provider',
579
+ message: `Select Provider for ${task}:`,
580
+ choices: [
581
+ { name: 'Use Active Provider (Default)', value: null },
582
+ new inquirer.Separator(),
583
+ { name: 'Google Gemini', value: 'gemini' },
584
+ { name: 'OpenAI', value: 'openai' },
585
+ { name: 'Anthropic', value: 'anthropic' },
586
+ { name: 'Groq', value: 'groq' },
587
+ { name: 'Deepseek', value: 'deepseek' },
588
+ { name: 'xAI (Grok)', value: 'grok' }
589
+ ]
590
+ }
591
+ ]);
433
592
 
434
- config.activeSkill = skill;
435
- saveConfig(config);
436
- console.log(skill ? `✅ Activated skill: ${skill}` : `✅ Skill deactivated.`);
593
+ if (provider) {
594
+ routing[task] = provider;
595
+ } else {
596
+ delete routing[task];
437
597
  }
598
+
599
+ config.providerRouting = routing;
600
+ saveConfig(config);
601
+ console.log(`✅ Routing updated.`);
602
+
603
+ return manageTaskRouting();
604
+ }
438
605
 
439
- if (action === 'install') {
440
- const { url } = await inquirer.prompt([
441
- { type: 'input', name: 'url', message: 'Enter Skill JSON URL:' }
442
- ]);
443
- try {
444
- console.log('Downloading...');
445
- const skill = await installSkillFromUrl(url);
446
- console.log(`✅ Installed skill: ${skill.name}`);
447
- const { activate } = await inquirer.prompt([
448
- { type: 'confirm', name: 'activate', message: 'Activate this skill now?', default: true }
449
- ]);
450
- if (activate) {
451
- config.activeSkill = skill.name;
452
- saveConfig(config);
453
- }
454
- } catch(e) {
455
- console.log(`❌ Failed: ${e.message}`);
606
+ async function configureWebSearch() {
607
+ const config = loadConfig();
608
+ const currentKey = config.braveApiKey ? '********' : 'Not Set';
609
+
610
+ console.log('configured Web Search uses Brave Search API.');
611
+ console.log('Get a free key at: https://brave.com/search/api/');
612
+
613
+ const { key } = await inquirer.prompt([
614
+ {
615
+ type: 'password',
616
+ name: 'key',
617
+ message: `Enter Brave Search API Key (Current: ${currentKey}):`,
618
+ suffix: '\n(Leave empty to remove/keep)',
619
+ mask: '*'
456
620
  }
621
+ ]);
622
+
623
+ if (key && key.trim() !== '') {
624
+ config.braveApiKey = key.trim();
625
+ saveConfig(config);
626
+ console.log('✅ Brave API Key set.');
627
+ } else if (key === '') {
628
+ // do nothing
629
+ console.log('No change.');
630
+ } else {
631
+ // e.g. space to clear? For now TUI doesn't support clear easily.
457
632
  }
633
+ await promptContinue();
634
+ }
458
635
 
459
- if (action === 'create') {
460
- // Simple creation wizard
461
- const answers = await inquirer.prompt([
462
- { type: 'input', name: 'name', message: 'Skill Name:', validate: i => /^[a-z0-9_-]+$/.test(i) },
463
- { type: 'input', name: 'description', message: 'Description:' },
464
- { type: 'editor', name: 'instruction', message: 'System Instruction (Persona):' }
465
- ]);
466
-
467
- const skill = {
468
- name: answers.name,
469
- version: "1.0.0",
470
- description: answers.description,
471
- author: "local",
472
- systemInstruction: answers.instruction,
473
- examples: []
474
- };
475
-
476
- if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
477
- fs.writeFileSync(path.join(SKILLS_DIR, `${skill.name}.json`), JSON.stringify(skill, null, 2));
478
- console.log(`✅ Skill '${skill.name}' created.`);
479
- }
480
636
 
481
- if (action === 'list') {
482
- console.log('\nInstalled Skills:');
483
- skills.forEach(s => {
484
- console.log(`- ${s.name}: ${s.description} (${s.active ? 'ACTIVE' : ''})`);
485
- });
486
- await promptContinue();
487
- }
488
-
489
- if (action === 'delete') {
490
- const { skill } = await inquirer.prompt([
491
- {
492
- type: 'list',
493
- name: 'skill',
494
- message: 'Select skill to delete:',
495
- choices: skills.map(s => ({ name: s.name, value: s.name}))
496
- }
637
+ // --- Main Logic ---
638
+
639
+ function switchSession() {
640
+ const sessions = fs.readdirSync(SESSIONS_DIR).filter(f => fs.statSync(path.join(SESSIONS_DIR, f)).isDirectory());
641
+ const config = loadConfig();
642
+ const activeSession = config.activeSession || 'default';
643
+
644
+ return inquirer.prompt([
645
+ {
646
+ type: 'list',
647
+ name: 'session',
648
+ message: 'Select Session:',
649
+ choices: sessions.map(s => ({ name: s === activeSession ? `${s} (Active)` : s, value: s })).concat([{ name: 'Back', value: 'back' }])
650
+ }
651
+ ]).then(answer => {
652
+ if (answer.session === 'back') return;
653
+ config.activeSession = answer.session;
654
+ saveConfig(config);
655
+ console.log(`Switched to session: ${answer.session}`);
656
+ });
657
+ }
658
+
659
+ async function createSession() {
660
+ const config = loadConfig();
661
+ const { name } = await inquirer.prompt([
662
+ {
663
+ type: 'input',
664
+ name: 'name',
665
+ message: 'Enter new session name (alphanumeric):',
666
+ validate: input => /^[a-zA-Z0-9_-]+$/.test(input) ? true : 'Invalid name.'
667
+ }
668
+ ]);
669
+
670
+ const sessionPath = path.join(SESSIONS_DIR, name);
671
+ if (fs.existsSync(sessionPath)) {
672
+ console.log('❌ Session already exists.');
673
+ } else {
674
+ fs.mkdirSync(sessionPath, { recursive: true });
675
+ console.log(`✅ Session '${name}' created.`);
676
+ const { switchTo } = await inquirer.prompt([
677
+ { type: 'confirm', name: 'switchTo', message: 'Switch to this session now?', default: true }
497
678
  ]);
498
- if (skill) {
499
- const { confirm } = await inquirer.prompt([
500
- { type: 'confirm', name: 'confirm', message: `Delete '${skill}'?`, default: false }
501
- ]);
502
- if (confirm) {
503
- fs.unlinkSync(path.join(SKILLS_DIR, `${skill}.json`));
504
- if (config.activeSkill === skill) {
505
- delete config.activeSkill;
506
- saveConfig(config);
507
- }
508
- console.log(`🗑️ Skill deleted.`);
509
- }
679
+ if (switchTo) {
680
+ config.activeSession = name;
681
+ saveConfig(config);
510
682
  }
511
683
  }
512
-
513
- return manageSkills();
514
684
  }
515
685
 
516
- async function showLogsMenu() {
517
- const { logType } = await inquirer.prompt([
686
+ async function deleteSession() {
687
+ const config = loadConfig();
688
+ const activeSession = config.activeSession || 'default';
689
+ const sessions = fs.readdirSync(SESSIONS_DIR).filter(f => fs.statSync(path.join(SESSIONS_DIR, f)).isDirectory());
690
+
691
+ const { session } = await inquirer.prompt([
518
692
  {
519
693
  type: 'list',
520
- name: 'logType',
521
- message: 'Which logs to view?',
522
- choices: [
523
- { name: 'Telegram', value: 'telegram' },
524
- { name: 'Queue Processor', value: 'queue' },
525
- { name: 'Daemon', value: 'daemon' },
526
- { name: 'Back', value: 'back' }
527
- ]
694
+ name: 'session',
695
+ message: 'Select session to delete:',
696
+ choices: sessions.filter(s => s !== 'default' && s !== activeSession).concat([{ name: 'Cancel', value: 'cancel' }])
528
697
  }
529
698
  ]);
530
699
 
531
- if (logType === 'back') return;
532
-
533
- console.log('Press Ctrl+C to exit logs.');
534
- await runScript(['logs', logType]);
535
- }
536
-
537
- async function performUpdate() {
538
- console.log('🔄 Checking for updates and installing latest version...');
539
- console.log('📦 Running: npm install -g @thelapyae/geniclaw@latest');
700
+ if (session === 'cancel') return;
540
701
 
541
- try {
542
- execSync('npm install -g @thelapyae/geniclaw@latest', { stdio: 'inherit' });
543
- console.log('\n✅ Update completed! Please restart geniclaw.');
544
- process.exit(0);
545
- } catch (error) {
546
- console.error('\n❌ Update failed.');
547
- console.error('Error details:', error.message);
548
- console.log('Try running with sudo: sudo geniclaw update');
549
-
550
- const { trySudo } = await inquirer.prompt([
551
- { type: 'confirm', name: 'trySudo', message: 'Try again with sudo?', default: false }
552
- ]);
553
-
554
- if (trySudo) {
555
- try {
556
- execSync('sudo npm install -g @thelapyae/geniclaw@latest', { stdio: 'inherit' });
557
- console.log('\n✅ Update completed! Please restart geniclaw.');
558
- process.exit(0);
559
- } catch (e) {
560
- console.error('\n❌ Sudo update also failed.');
561
- }
562
- }
702
+ // confirm
703
+ const { confirm } = await inquirer.prompt([
704
+ { type: 'confirm', name: 'confirm', message: `Are you sure you want to delete '${session}' and all its history?`, default: false }
705
+ ]);
706
+
707
+ if (confirm) {
708
+ fs.rmSync(path.join(SESSIONS_DIR, session), { recursive: true, force: true });
709
+ console.log(`🗑️ Session '${session}' deleted.`);
563
710
  }
564
711
  }
565
712
 
566
713
 
567
- // CLI Configuration
568
- const packageJson = require('../package.json');
714
+ // CLI Args Handling
569
715
  program
570
- .version(packageJson.version)
571
- .description('GeniClaw Management CLI')
572
- .arguments('[command]')
573
- .action(async (command) => {
716
+ .version(require('../package.json').version)
717
+ .argument('[command]', 'Command to run (start, stop, logs, etc)')
718
+ .argument('[args...]', 'Arguments for the command')
719
+ .action(async (command, args) => {
574
720
  if (!command) {
575
- // No args -> TUI Mode
576
721
  await showMenu();
577
722
  } else {
578
- if (command === 'model') {
579
- await changeModel();
580
- } else if (command === 'sessions') {
581
- await manageSessions();
582
- } else if (command === 'skills') {
583
- await manageSkills();
584
- } else if (command === 'update') {
585
- await performUpdate();
586
- } else {
587
- const args = process.argv.slice(2);
588
- await runScript(args);
589
- }
723
+ if (command === 'provider') {
724
+ await selectProviderAndModel();
725
+ } else if (command === 'keys') {
726
+ await manageApiKeys();
727
+ } else if (command === 'routing') {
728
+ await manageTaskRouting();
729
+ } else if (command === 'search') {
730
+ await configureWebSearch();
731
+ } else if (command === 'services') {
732
+ // Not implemented directly yet, maybe mapping to start/stop?
733
+ // Just fall through to script for now
734
+ await runScript(args);
735
+ } else if (command === 'doctor') {
736
+ await runDoctor();
737
+ } else if (command === 'update') {
738
+ await performUpdate();
739
+ } else {
740
+ // Pass through to shell script
741
+ const allArgs = [command, ...args];
742
+ await runScript(allArgs);
743
+ }
590
744
  }
591
745
  });
592
746