gigaclaw 1.9.1 → 1.9.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/api/index.js CHANGED
@@ -31,7 +31,7 @@ function getFireTriggers() {
31
31
  }
32
32
 
33
33
  // Routes that have their own authentication
34
- const PUBLIC_ROUTES = ['/telegram/webhook', '/github/webhook', '/ping'];
34
+ const PUBLIC_ROUTES = ['/telegram/webhook', '/github/webhook', '/ping', '/health', '/debug'];
35
35
 
36
36
  /**
37
37
  * Timing-safe string comparison.
@@ -270,6 +270,36 @@ async function GET(request) {
270
270
 
271
271
  switch (routePath) {
272
272
  case '/ping': return Response.json({ message: 'Pong!' });
273
+ case '/health': {
274
+ const { handleHealthCheck } = await import('../lib/api/health.js');
275
+ const result = await handleHealthCheck();
276
+ const status = result.status === 'unhealthy' ? 503 : 200;
277
+ return Response.json(result, { status });
278
+ }
279
+ case '/debug': {
280
+ const { handleHealthCheck } = await import('../lib/api/health.js');
281
+ const health = await handleHealthCheck();
282
+ return Response.json({
283
+ ...health,
284
+ env: {
285
+ NODE_ENV: process.env.NODE_ENV,
286
+ GIGACLAW_MODE: process.env.GIGACLAW_MODE,
287
+ LLM_PROVIDER: process.env.LLM_PROVIDER,
288
+ LLM_MODEL: process.env.LLM_MODEL,
289
+ LOCAL_LLM_PROVIDER: process.env.LOCAL_LLM_PROVIDER,
290
+ HYBRID_ROUTING: process.env.HYBRID_ROUTING,
291
+ ENABLE_CRON: process.env.ENABLE_CRON,
292
+ AUTH_TRUST_HOST: process.env.AUTH_TRUST_HOST,
293
+ // Never expose secrets — just show if they are set
294
+ AUTH_SECRET: process.env.AUTH_SECRET ? '[set]' : '[missing]',
295
+ NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET ? '[set]' : '[missing]',
296
+ JWT_SECRET: process.env.JWT_SECRET ? '[set]' : '[missing]',
297
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ? '[set]' : '[missing]',
298
+ },
299
+ uptime: process.uptime(),
300
+ memory: process.memoryUsage(),
301
+ });
302
+ }
273
303
  case '/jobs/status': return handleJobStatus(request);
274
304
  default: return Response.json({ error: 'Not found' }, { status: 404 });
275
305
  }
package/bin/bootstrap.mjs CHANGED
@@ -31,13 +31,14 @@ const PACKAGE_DIR = path.join(__dirname, '..');
31
31
  // Replaces verbose npm/node output with clean phase-aware status lines.
32
32
 
33
33
  const PHASES = {
34
- ENV: '[ 1/7 ] Detecting environment',
35
- DIR: '[ 2/7 ] Creating project directory',
36
- SCAF: '[ 3/7 ] Scaffolding project files',
37
- DEPS: '[ 4/7 ] Installing dependencies',
38
- SETUP: '[ 5/7 ] Configuring GigaClaw',
39
- ENV_W: '[ 6/7 ] Writing .env',
40
- START: '[ 7/7 ] Starting dev server',
34
+ ENV: '[ 1/8 ] Detecting environment',
35
+ DIR: '[ 2/8 ] Creating project directory',
36
+ SCAF: '[ 3/8 ] Scaffolding project files',
37
+ DEPS: '[ 4/8 ] Installing dependencies',
38
+ SETUP: '[ 5/8 ] Configuring GigaClaw',
39
+ ENV_W: '[ 6/8 ] Writing .env',
40
+ START: '[ 7/8 ] Starting dev server',
41
+ HEALTH: '[ 8/8 ] Validating system health',
41
42
  };
42
43
 
43
44
  function log(phase, msg) {
@@ -81,6 +82,14 @@ function printBanner() {
81
82
  const SLEEP = (ms) => new Promise((r) => setTimeout(r, ms));
82
83
 
83
84
  async function installDependencies(cwd) {
85
+ // Support --clean-install: delete node_modules and lockfile before install
86
+ if (process.env.GIGACLAW_CLEAN_INSTALL === 'true') {
87
+ logStep('Clean install requested — removing node_modules and lockfile...');
88
+ const nm = path.join(cwd, 'node_modules');
89
+ const lf = path.join(cwd, 'package-lock.json');
90
+ if (fs.existsSync(nm)) fs.rmSync(nm, { recursive: true, force: true });
91
+ if (fs.existsSync(lf)) fs.unlinkSync(lf);
92
+ }
84
93
  const MAX_ATTEMPTS = 3;
85
94
  const BACKOFF = [2000, 5000, 10000]; // exponential: 2s, 5s, 10s
86
95
 
@@ -185,36 +194,69 @@ async function runSmartSetup(cwd, envInfo, port = 3000) {
185
194
 
186
195
  const authSecret = randomBytes(32).toString('base64url');
187
196
  const nextAuthSecret = randomBytes(32).toString('base64url');
197
+ const jwtSecret = randomBytes(32).toString('base64url');
188
198
 
189
199
  const localModel = envInfo.ollamaModels.length > 0
190
200
  ? envInfo.ollamaModels[0]
191
201
  : recommendOllamaModel(envInfo.ramGb);
192
202
 
203
+ // Determine operating mode based on available infrastructure
204
+ // If an existing .env has ANTHROPIC_API_KEY, preserve hybrid mode
205
+ const existingEnv = fs.existsSync(path.join(cwd, '.env'))
206
+ ? fs.readFileSync(path.join(cwd, '.env'), 'utf-8')
207
+ : '';
208
+ const hasApiKey = /^ANTHROPIC_API_KEY=.+$/m.test(existingEnv)
209
+ || /^OPENAI_API_KEY=.+$/m.test(existingEnv)
210
+ || /^GOOGLE_API_KEY=.+$/m.test(existingEnv);
211
+ const hasOllama = envInfo.ollama;
212
+
213
+ let mode, llmProvider, llmModel;
214
+ if (hasApiKey) {
215
+ // User has a cloud API key — use hybrid mode
216
+ mode = 'hybrid';
217
+ llmProvider = 'anthropic';
218
+ llmModel = 'claude-sonnet-4-6';
219
+ } else if (hasOllama) {
220
+ // No cloud key but Ollama is running — local-only mode
221
+ mode = 'local';
222
+ llmProvider = 'ollama';
223
+ llmModel = localModel;
224
+ } else {
225
+ // No cloud key, no Ollama — local mode with placeholder (UI will still load)
226
+ mode = 'local';
227
+ llmProvider = 'ollama';
228
+ llmModel = localModel;
229
+ }
230
+
193
231
  const envVars = {
194
- // Mode
195
- GIGACLAW_MODE: 'hybrid',
232
+ // Mode — auto-detected based on available infrastructure
233
+ GIGACLAW_MODE: mode,
196
234
 
197
- // Cloud: default to Claude Sonnet (user can change via npm run setup)
198
- LLM_PROVIDER: 'anthropic',
199
- LLM_MODEL: 'claude-sonnet-4-6',
200
- // Note: ANTHROPIC_API_KEY intentionally left blank user must provide it
201
- ANTHROPIC_API_KEY: '',
235
+ // Primary LLM
236
+ LLM_PROVIDER: llmProvider,
237
+ LLM_MODEL: llmModel,
238
+ // Cloud API key only set if not already present
239
+ ...(hasApiKey ? {} : { ANTHROPIC_API_KEY: '' }),
202
240
 
203
241
  // Local: Ollama if running, else blank
204
- LOCAL_LLM_PROVIDER: envInfo.ollama ? 'ollama' : '',
205
- LOCAL_LLM_MODEL: envInfo.ollama ? localModel : '',
242
+ LOCAL_LLM_PROVIDER: hasOllama ? 'ollama' : '',
243
+ LOCAL_LLM_MODEL: hasOllama ? localModel : '',
206
244
  OLLAMA_BASE_URL: 'http://localhost:11434',
207
245
 
208
246
  // Routing
209
- HYBRID_ROUTING: 'auto',
247
+ HYBRID_ROUTING: mode === 'hybrid' ? 'auto' : 'local',
210
248
 
211
- // Auth
249
+ // Auth & JWT
212
250
  NEXTAUTH_URL: `http://localhost:${port}`,
213
251
  AUTH_URL: `http://localhost:${port}`,
214
252
  NEXTAUTH_SECRET: nextAuthSecret,
215
253
  AUTH_SECRET: authSecret,
254
+ JWT_SECRET: jwtSecret,
216
255
  AUTH_TRUST_HOST: 'true',
217
256
 
257
+ // Cron control — disabled until health check passes
258
+ ENABLE_CRON: 'false',
259
+
218
260
  // Version
219
261
  GIGACLAW_VERSION: JSON.parse(
220
262
  fs.readFileSync(path.join(PACKAGE_DIR, 'package.json'), 'utf8')
@@ -288,21 +330,37 @@ async function startDevServer(cwd, port) {
288
330
  logWarn(`Port 3000 is in use — using port ${port}`);
289
331
  }
290
332
 
333
+ // Read mode from .env for the info box
334
+ let displayMode = 'Local';
335
+ try {
336
+ const envContent = fs.readFileSync(path.join(cwd, '.env'), 'utf-8');
337
+ const modeMatch = envContent.match(/^GIGACLAW_MODE=(.*)$/m);
338
+ if (modeMatch && modeMatch[1].trim() === 'hybrid') displayMode = 'Hybrid (Cloud + Local)';
339
+ else if (modeMatch && modeMatch[1].trim() === 'local') displayMode = 'Local (On-Device)';
340
+ } catch (_) {}
341
+
342
+ const modeStr = `Mode: ${displayMode}`;
291
343
  console.log(`
292
344
  ┌─────────────────────────────────────────────────────────┐
293
345
  │ │
294
346
  │ GigaClaw is starting... │
295
347
  │ │
296
348
  │ App URL: http://localhost:${port}${' '.repeat(Math.max(0, 22 - String(port).length))}│
297
- Mode: Hybrid (Cloud + Local)
349
+ ${modeStr}${' '.repeat(Math.max(0, 52 - modeStr.length))}
298
350
  │ │
299
- Next step: add your ANTHROPIC_API_KEY to .env
300
- │ or run: npm run setup for the full wizard │
351
+ Run: npm run setup for the full wizard
301
352
  │ │
302
353
  └─────────────────────────────────────────────────────────┘
303
354
  `);
304
355
 
305
- logStep(`Launching Next.js dev server (turbopack) on port ${port}...`);
356
+ // Clean stale .next build cache to avoid chunk load errors
357
+ const nextDir = path.join(cwd, '.next');
358
+ if (fs.existsSync(nextDir)) {
359
+ logStep('Removing stale .next build cache...');
360
+ fs.rmSync(nextDir, { recursive: true, force: true });
361
+ }
362
+
363
+ logStep(`Launching Next.js dev server on port ${port}...`);
306
364
 
307
365
  const child = spawn('npm', ['run', 'dev', '--', '--port', String(port)], {
308
366
  cwd,
@@ -416,8 +474,24 @@ export async function bootstrap() {
416
474
  }
417
475
 
418
476
  if (!isExistingProject) {
419
- const dirName = 'gigaclaw-app';
420
- const newDir = path.resolve(cwd, dirName);
477
+ let dirName = 'gigaclaw-app';
478
+ let newDir = path.resolve(cwd, dirName);
479
+ // If gigaclaw-app already exists and is not a gigaclaw project, use a unique suffix
480
+ if (fs.existsSync(newDir)) {
481
+ const existingPkg = path.join(newDir, 'package.json');
482
+ let isGigaclawDir = false;
483
+ if (fs.existsSync(existingPkg)) {
484
+ try {
485
+ const p = JSON.parse(fs.readFileSync(existingPkg, 'utf8'));
486
+ if (p.dependencies?.gigaclaw || p.devDependencies?.gigaclaw) isGigaclawDir = true;
487
+ } catch (_) {}
488
+ }
489
+ if (!isGigaclawDir) {
490
+ const suffix = Date.now().toString(36).slice(-4);
491
+ dirName = `gigaclaw-app-${suffix}`;
492
+ newDir = path.resolve(cwd, dirName);
493
+ }
494
+ }
421
495
  fs.mkdirSync(newDir, { recursive: true });
422
496
  process.chdir(newDir);
423
497
  cwd = newDir;
@@ -462,19 +536,91 @@ export async function bootstrap() {
462
536
  process.exit(1);
463
537
  }
464
538
  } else {
465
- log(PHASES.SETUP, 'Applying smart defaults (hybrid mode, Claude Sonnet, auto routing)...');
466
- const { localModel } = await runSmartSetup(cwd, envInfo, devPort);
539
+ log(PHASES.SETUP, 'Applying smart defaults (auto-detecting mode)...');
540
+ const { localModel, envVars: setupVars } = await runSmartSetup(cwd, envInfo, devPort);
467
541
 
468
542
  log(PHASES.ENV_W, 'Writing .env configuration...');
469
- logOk('GIGACLAW_MODE=hybrid');
470
- logOk('LLM_PROVIDER=anthropic | LLM_MODEL=claude-sonnet-4-6');
471
- logOk(`LOCAL_LLM_PROVIDER=${envInfo.ollama ? 'ollama' : '(not configured)'} | LOCAL_LLM_MODEL=${envInfo.ollama ? localModel : '(not configured)'}`);
472
- logOk('HYBRID_ROUTING=auto');
473
- logWarn('ANTHROPIC_API_KEY is empty — run: npm run setup to add your API key');
543
+ logOk(`GIGACLAW_MODE=${setupVars.GIGACLAW_MODE}`);
544
+ logOk(`LLM_PROVIDER=${setupVars.LLM_PROVIDER} | LLM_MODEL=${setupVars.LLM_MODEL}`);
545
+ if (setupVars.LOCAL_LLM_PROVIDER) {
546
+ logOk(`LOCAL_LLM_PROVIDER=${setupVars.LOCAL_LLM_PROVIDER} | LOCAL_LLM_MODEL=${setupVars.LOCAL_LLM_MODEL}`);
547
+ }
548
+ logOk(`HYBRID_ROUTING=${setupVars.HYBRID_ROUTING}`);
549
+ if (setupVars.GIGACLAW_MODE === 'local' && !envInfo.ollama) {
550
+ logWarn('No API key and no Ollama detected — UI will load but AI chat requires a provider');
551
+ logStep('To enable AI: install Ollama (https://ollama.com) or add ANTHROPIC_API_KEY to .env');
552
+ } else if (setupVars.GIGACLAW_MODE === 'local') {
553
+ logOk('Running in local-only mode — all data stays on your machine');
554
+ } else if (!setupVars.ANTHROPIC_API_KEY && setupVars.GIGACLAW_MODE === 'hybrid') {
555
+ logWarn('ANTHROPIC_API_KEY is empty — run: npm run setup to add your API key');
556
+ }
474
557
  }
475
558
 
476
559
  // ── Phase 7: Start dev server + open browser ─────────────────────────────────────────
477
560
  log(PHASES.START, 'Starting Next.js dev server...');
478
561
 
479
- await startDevServer(cwd, devPort);
562
+ const serverChild = await startDevServer(cwd, devPort);
563
+
564
+ // ── Phase 8: Health validation ─────────────────────────────────────────────
565
+ log(PHASES.HEALTH, 'Validating system health...');
566
+ const healthUrl = `http://localhost:${devPort}/api/health`;
567
+ let healthPassed = false;
568
+ for (let attempt = 1; attempt <= 5; attempt++) {
569
+ try {
570
+ await SLEEP(3000); // Give Next.js time to compile the route
571
+ const res = await fetch(healthUrl, { signal: AbortSignal.timeout(10000) });
572
+ if (res.ok) {
573
+ const data = await res.json();
574
+ healthPassed = data.status !== 'unhealthy';
575
+ if (healthPassed) {
576
+ logOk(`Health check passed — status: ${data.status}`);
577
+ // Print subsystem summary
578
+ for (const [name, check] of Object.entries(data.checks || {})) {
579
+ const icon = check.status === 'ok' ? '✓' : check.status === 'warn' ? '⚠' : '✗';
580
+ logStep(`${icon} ${name}: ${check.message}`);
581
+ }
582
+ // Enable cron now that system is healthy
583
+ try {
584
+ const envPath = path.join(cwd, '.env');
585
+ let envContent = fs.readFileSync(envPath, 'utf-8');
586
+ envContent = envContent.replace(/^ENABLE_CRON=false$/m, 'ENABLE_CRON=true');
587
+ fs.writeFileSync(envPath, envContent);
588
+ logOk('Cron scheduler enabled (ENABLE_CRON=true)');
589
+ } catch (_) {}
590
+ break;
591
+ } else {
592
+ logWarn(`Health check returned: ${data.status} (attempt ${attempt}/5)`);
593
+ }
594
+ } else {
595
+ logWarn(`Health check returned HTTP ${res.status} (attempt ${attempt}/5)`);
596
+ }
597
+ } catch (err) {
598
+ if (attempt < 5) {
599
+ logStep(`Health check attempt ${attempt}/5 — server still compiling...`);
600
+ } else {
601
+ logWarn(`Health check failed after 5 attempts: ${err.message}`);
602
+ }
603
+ }
604
+ }
605
+
606
+ if (!healthPassed) {
607
+ logWarn('Health check did not pass — system may be degraded.');
608
+ logStep('Run: curl http://localhost:' + devPort + '/api/health to diagnose');
609
+ logStep('Run: curl http://localhost:' + devPort + '/api/debug for full diagnostics');
610
+ }
611
+
612
+ // Final summary
613
+ console.log(`
614
+ ┌─────────────────────────────────────────────────────────┐
615
+ │ │
616
+ │ ✓ GigaClaw is ready! │
617
+ │ │
618
+ │ App: http://localhost:${devPort}${' '.repeat(Math.max(0, 22 - String(devPort).length))}│
619
+ │ Health: http://localhost:${devPort}/api/health${' '.repeat(Math.max(0, 11 - String(devPort).length))}│
620
+ │ Debug: http://localhost:${devPort}/api/debug${' '.repeat(Math.max(0, 12 - String(devPort).length))}│
621
+ │ │
622
+ │ Press Ctrl+C to stop the server │
623
+ │ │
624
+ └─────────────────────────────────────────────────────────┘
625
+ `);
480
626
  }
package/bin/cli.js CHANGED
@@ -99,7 +99,10 @@ Manual commands:
99
99
  set-agent-secret <KEY> [VALUE] Set a GitHub secret with AGENT_ prefix (also updates .env)
100
100
  set-agent-llm-secret <KEY> [VALUE] Set a GitHub secret with AGENT_LLM_ prefix
101
101
  set-var <KEY> [VALUE] Set a GitHub repository variable
102
- --version, -v Show gigaclaw version
102
+ doctor Validate environment (Node, Docker, Ollama, ports, .env)
103
+ reset-build Clean .next cache, node_modules, and rebuild
104
+ --clean-install Bootstrap with fresh npm install (delete node_modules first)
105
+ --version, -v Show gigaclaw version
103
106
 
104
107
  Powered by Gignaati — https://www.gignaati.com
105
108
  `);
@@ -887,6 +890,38 @@ switch (command) {
887
890
  case 'set-var':
888
891
  await setVar(args[0], args[1]);
889
892
  break;
893
+ case 'doctor': {
894
+ const { doctor } = await import('./doctor.mjs');
895
+ await doctor();
896
+ break;
897
+ }
898
+ case 'reset-build': {
899
+ console.log('Cleaning build artifacts...');
900
+ const cwd = process.cwd();
901
+ const nextDir = path.join(cwd, '.next');
902
+ if (fs.existsSync(nextDir)) {
903
+ fs.rmSync(nextDir, { recursive: true, force: true });
904
+ console.log(' Removed .next/');
905
+ }
906
+ const nmDir = path.join(cwd, 'node_modules');
907
+ if (fs.existsSync(nmDir)) {
908
+ fs.rmSync(nmDir, { recursive: true, force: true });
909
+ console.log(' Removed node_modules/');
910
+ }
911
+ const lockFile = path.join(cwd, 'package-lock.json');
912
+ if (fs.existsSync(lockFile)) {
913
+ fs.unlinkSync(lockFile);
914
+ console.log(' Removed package-lock.json');
915
+ }
916
+ console.log('Clean complete. Run: npm install && npm run dev');
917
+ break;
918
+ }
919
+ case '--clean-install': {
920
+ process.env.GIGACLAW_CLEAN_INSTALL = 'true';
921
+ const { bootstrap } = await import('./bootstrap.mjs');
922
+ await bootstrap();
923
+ break;
924
+ }
890
925
  default:
891
926
  printUsage();
892
927
  process.exit(1);
package/bin/doctor.mjs ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * gigaclaw doctor — Environment validation command.
5
+ *
6
+ * Checks:
7
+ * - Node.js version (>= 18 required, >= 20 recommended)
8
+ * - Docker availability
9
+ * - Ollama availability and models
10
+ * - Port 3000 availability
11
+ * - .env file completeness
12
+ * - npm cache health
13
+ */
14
+
15
+ import { execSync } from 'child_process';
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import net from 'net';
19
+
20
+ const OK = '\x1b[32m✓\x1b[0m';
21
+ const WARN = '\x1b[33m⚠\x1b[0m';
22
+ const FAIL = '\x1b[31m✗\x1b[0m';
23
+
24
+ function check(icon, label, detail) {
25
+ console.log(` ${icon} ${label}${detail ? ` — ${detail}` : ''}`);
26
+ }
27
+
28
+ function tryExec(cmd) {
29
+ try {
30
+ return execSync(cmd, { encoding: 'utf-8', timeout: 10000 }).trim();
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function checkPort(port) {
37
+ return new Promise((resolve) => {
38
+ const server = net.createServer();
39
+ server.once('error', () => resolve(false));
40
+ server.once('listening', () => { server.close(); resolve(true); });
41
+ server.listen(port);
42
+ });
43
+ }
44
+
45
+ export async function doctor() {
46
+ console.log('\n GigaClaw Doctor — Environment Validation\n');
47
+ console.log(' ─────────────────────────────────────────\n');
48
+
49
+ let issues = 0;
50
+
51
+ // 1. Node.js
52
+ const nodeVersion = process.version;
53
+ const major = parseInt(nodeVersion.slice(1));
54
+ if (major >= 20) {
55
+ check(OK, 'Node.js', `${nodeVersion} (recommended)`);
56
+ } else if (major >= 18) {
57
+ check(WARN, 'Node.js', `${nodeVersion} (works, but v20+ recommended)`);
58
+ } else {
59
+ check(FAIL, 'Node.js', `${nodeVersion} (v18+ required)`);
60
+ issues++;
61
+ }
62
+
63
+ // 2. npm
64
+ const npmVersion = tryExec('npm --version');
65
+ if (npmVersion) {
66
+ check(OK, 'npm', `v${npmVersion}`);
67
+ } else {
68
+ check(FAIL, 'npm', 'not found');
69
+ issues++;
70
+ }
71
+
72
+ // 3. Docker
73
+ const dockerVersion = tryExec('docker --version');
74
+ if (dockerVersion) {
75
+ const dockerRunning = tryExec('docker info');
76
+ if (dockerRunning) {
77
+ check(OK, 'Docker', dockerVersion.replace('Docker version ', 'v'));
78
+ } else {
79
+ check(WARN, 'Docker', 'installed but daemon not running');
80
+ }
81
+ } else {
82
+ check(WARN, 'Docker', 'not installed (optional — needed for code execution sandbox)');
83
+ }
84
+
85
+ // 4. Ollama
86
+ const ollamaVersion = tryExec('ollama --version');
87
+ if (ollamaVersion) {
88
+ const ollamaModels = tryExec('ollama list');
89
+ const modelCount = ollamaModels
90
+ ? ollamaModels.split('\n').filter(l => l.trim() && !l.startsWith('NAME')).length
91
+ : 0;
92
+ check(OK, 'Ollama', `${ollamaVersion} — ${modelCount} model(s) installed`);
93
+ if (modelCount === 0) {
94
+ check(WARN, ' Models', 'No models installed. Run: ollama pull llama3.2:3b');
95
+ }
96
+ } else {
97
+ // Check if Ollama API is reachable even without CLI
98
+ const ollamaApi = tryExec('curl -s http://localhost:11434/api/tags');
99
+ if (ollamaApi) {
100
+ try {
101
+ const data = JSON.parse(ollamaApi);
102
+ check(OK, 'Ollama', `API reachable — ${data.models?.length || 0} model(s)`);
103
+ } catch {
104
+ check(WARN, 'Ollama', 'API reachable but response unexpected');
105
+ }
106
+ } else {
107
+ check(WARN, 'Ollama', 'not installed (optional — needed for local AI mode)');
108
+ check(WARN, ' Install', 'https://ollama.com/download');
109
+ }
110
+ }
111
+
112
+ // 5. Port 3000
113
+ const port3000Free = await checkPort(3000);
114
+ if (port3000Free) {
115
+ check(OK, 'Port 3000', 'available');
116
+ } else {
117
+ check(WARN, 'Port 3000', 'in use — GigaClaw will auto-select another port');
118
+ }
119
+
120
+ // 6. .env file
121
+ const envPath = path.join(process.cwd(), '.env');
122
+ if (fs.existsSync(envPath)) {
123
+ const envContent = fs.readFileSync(envPath, 'utf-8');
124
+ const requiredVars = ['AUTH_SECRET', 'NEXTAUTH_SECRET', 'NEXTAUTH_URL', 'GIGACLAW_MODE'];
125
+ const missing = requiredVars.filter(v => !new RegExp(`^${v}=.+`, 'm').test(envContent));
126
+ if (missing.length === 0) {
127
+ check(OK, '.env file', `found — all required vars set`);
128
+ } else {
129
+ check(WARN, '.env file', `found — missing: ${missing.join(', ')}`);
130
+ }
131
+
132
+ // Check API key
133
+ const hasApiKey = /^ANTHROPIC_API_KEY=.+$/m.test(envContent)
134
+ || /^OPENAI_API_KEY=.+$/m.test(envContent);
135
+ const mode = envContent.match(/^GIGACLAW_MODE=(.*)$/m)?.[1]?.trim() || 'hybrid';
136
+ if (mode === 'hybrid' && !hasApiKey) {
137
+ check(WARN, ' API Key', 'hybrid mode but no cloud API key set');
138
+ } else if (hasApiKey) {
139
+ check(OK, ' API Key', 'cloud API key configured');
140
+ } else {
141
+ check(OK, ' API Key', `not needed (${mode} mode)`);
142
+ }
143
+ } else {
144
+ check(WARN, '.env file', 'not found — run npx gigaclaw@latest to create one');
145
+ }
146
+
147
+ // 7. npm cache
148
+ const cacheVerify = tryExec('npm cache verify 2>&1 | tail -1');
149
+ if (cacheVerify && !cacheVerify.includes('error')) {
150
+ check(OK, 'npm cache', 'healthy');
151
+ } else {
152
+ check(WARN, 'npm cache', 'may have issues — run: npm cache clean --force');
153
+ }
154
+
155
+ // Summary
156
+ console.log('\n ─────────────────────────────────────────\n');
157
+ if (issues === 0) {
158
+ console.log(` ${OK} Environment looks good!\n`);
159
+ } else {
160
+ console.log(` ${FAIL} ${issues} critical issue(s) found. Fix them before running gigaclaw.\n`);
161
+ }
162
+ }
package/bin/scaffold.mjs CHANGED
@@ -129,7 +129,7 @@ export async function scaffoldProject(cwd, packageDir, { noManaged = false, sile
129
129
  name: dirName,
130
130
  private: true,
131
131
  scripts: {
132
- dev: 'next dev --turbopack',
132
+ dev: 'next dev',
133
133
  build: 'next build',
134
134
  start: 'next start',
135
135
  setup: 'gigaclaw setup',
@@ -30,7 +30,7 @@ export async function register() {
30
30
  process.env.AUTH_URL = process.env.APP_URL;
31
31
  }
32
32
 
33
- // Validate AUTH_SECRET is set (required by Auth.js for session encryption)
33
+ // Validate auth secrets (required by Auth.js for session encryption and JWT signing)
34
34
  if (!process.env.AUTH_SECRET) {
35
35
  console.error('\n ERROR: AUTH_SECRET is not set in your .env file.');
36
36
  console.error(' This is required for session encryption.');
@@ -38,25 +38,39 @@ export async function register() {
38
38
  console.error(' openssl rand -base64 32\n');
39
39
  throw new Error('AUTH_SECRET environment variable is required');
40
40
  }
41
+ if (!process.env.NEXTAUTH_SECRET) {
42
+ // Fall back to AUTH_SECRET if NEXTAUTH_SECRET is not set
43
+ process.env.NEXTAUTH_SECRET = process.env.AUTH_SECRET;
44
+ console.warn(' WARN: NEXTAUTH_SECRET not set — using AUTH_SECRET as fallback.');
45
+ }
41
46
 
42
47
  // Initialize auth database
43
48
  const { initDatabase } = await import('../lib/db/index.js');
44
49
  initDatabase();
45
50
 
46
- // Start cron scheduler
47
- const { loadCrons } = await import('../lib/cron.js');
48
- loadCrons();
49
-
50
- // Start built-in crons (version check)
51
- const { startBuiltinCrons, setUpdateAvailable } = await import('../lib/cron.js');
52
- startBuiltinCrons();
51
+ // Start cron scheduler — gated on ENABLE_CRON env var (default: false)
52
+ // Bootstrap sets ENABLE_CRON=false initially, then enables after health check passes.
53
+ if (process.env.ENABLE_CRON === 'true') {
54
+ const { loadCrons } = await import('../lib/cron.js');
55
+ loadCrons();
56
+ const { startBuiltinCrons, setUpdateAvailable } = await import('../lib/cron.js');
57
+ startBuiltinCrons();
58
+ // Warm in-memory flag from DB
59
+ try {
60
+ const { getAvailableVersion } = await import('../lib/db/update-check.js');
61
+ const stored = getAvailableVersion();
62
+ if (stored) setUpdateAvailable(stored);
63
+ } catch {}
64
+ console.log(' Cron scheduler started (ENABLE_CRON=true)');
65
+ } else {
66
+ console.log(' Cron scheduler disabled (ENABLE_CRON != true)');
67
+ }
53
68
 
54
- // Warm in-memory flag from DB (covers the window before the async cron fetch completes)
55
- try {
56
- const { getAvailableVersion } = await import('../lib/db/update-check.js');
57
- const stored = getAvailableVersion();
58
- if (stored) setUpdateAvailable(stored);
59
- } catch {}
69
+ // Auto-detect mode and log it
70
+ const mode = process.env.GIGACLAW_MODE || 'hybrid';
71
+ const provider = process.env.LLM_PROVIDER || 'anthropic';
72
+ const model = process.env.LLM_MODEL || 'unknown';
73
+ console.log(` Mode: ${mode} | Provider: ${provider} | Model: ${model}`);
60
74
 
61
75
  console.log('gigaclaw initialized');
62
76
  }
package/lib/ai/model.js CHANGED
@@ -46,6 +46,19 @@ export async function createModel(options = {}) {
46
46
  case 'anthropic': {
47
47
  const apiKey = process.env.ANTHROPIC_API_KEY;
48
48
  if (!apiKey) {
49
+ // In local mode, missing cloud key is expected — fall back to Ollama
50
+ if (process.env.GIGACLAW_MODE === 'local') {
51
+ console.warn('[model] ANTHROPIC_API_KEY missing in local mode — falling back to Ollama');
52
+ const ollamaModel = process.env.LOCAL_LLM_MODEL || 'llama3.2';
53
+ const ollamaUrl = (process.env.OLLAMA_BASE_URL || 'http://localhost:11434') + '/v1';
54
+ const { ChatOpenAI } = await import('@langchain/openai');
55
+ return new ChatOpenAI({
56
+ modelName: ollamaModel,
57
+ maxTokens,
58
+ apiKey: 'ollama',
59
+ configuration: { baseURL: ollamaUrl },
60
+ });
61
+ }
49
62
  throw new Error(
50
63
  'ANTHROPIC_API_KEY is required.\n' +
51
64
  'Get your key at: https://platform.claude.com/settings/keys\n' +
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Health check endpoint — /api/health
3
+ *
4
+ * Validates:
5
+ * 1. Auth configuration (AUTH_SECRET, NEXTAUTH_SECRET present)
6
+ * 2. LLM availability (cloud key or Ollama reachable)
7
+ * 3. Database initialized
8
+ * 4. Server readiness
9
+ *
10
+ * Returns JSON with status: 'healthy' | 'degraded' | 'unhealthy'
11
+ * and details for each subsystem.
12
+ */
13
+
14
+ import { checkOllamaHealth, checkCloudProviderConfig } from '../ai/provider-health.js';
15
+
16
+ export async function handleHealthCheck() {
17
+ const checks = {};
18
+ let overallStatus = 'healthy';
19
+
20
+ // 1. Auth configuration
21
+ const authSecret = process.env.AUTH_SECRET;
22
+ const nextAuthSecret = process.env.NEXTAUTH_SECRET;
23
+ const jwtSecret = process.env.JWT_SECRET;
24
+ if (authSecret && nextAuthSecret) {
25
+ checks.auth = { status: 'ok', message: 'Auth secrets configured' };
26
+ } else {
27
+ checks.auth = {
28
+ status: 'fail',
29
+ message: `Missing: ${[
30
+ !authSecret && 'AUTH_SECRET',
31
+ !nextAuthSecret && 'NEXTAUTH_SECRET',
32
+ ].filter(Boolean).join(', ')}`,
33
+ };
34
+ overallStatus = 'unhealthy';
35
+ }
36
+
37
+ // JWT secret (used by ws-proxy)
38
+ if (jwtSecret) {
39
+ checks.jwt = { status: 'ok', message: 'JWT secret configured' };
40
+ } else {
41
+ checks.jwt = { status: 'warn', message: 'JWT_SECRET not set — code workspace proxy disabled' };
42
+ if (overallStatus === 'healthy') overallStatus = 'degraded';
43
+ }
44
+
45
+ // 2. LLM availability
46
+ const mode = process.env.GIGACLAW_MODE || 'hybrid';
47
+ const provider = process.env.LLM_PROVIDER || 'anthropic';
48
+
49
+ if (mode === 'hybrid' || mode === 'cloud') {
50
+ const cloudCheck = checkCloudProviderConfig(provider);
51
+ if (cloudCheck.available) {
52
+ checks.cloud_llm = { status: 'ok', message: `${provider} API key configured` };
53
+ } else {
54
+ checks.cloud_llm = { status: 'fail', message: cloudCheck.error || `${provider} API key missing` };
55
+ if (overallStatus === 'healthy') overallStatus = 'degraded';
56
+ }
57
+ }
58
+
59
+ if (mode === 'hybrid' || mode === 'local') {
60
+ try {
61
+ const ollamaCheck = await checkOllamaHealth();
62
+ if (ollamaCheck.available) {
63
+ checks.local_llm = {
64
+ status: 'ok',
65
+ message: `Ollama running — ${ollamaCheck.models?.length || 0} model(s)`,
66
+ };
67
+ } else {
68
+ checks.local_llm = {
69
+ status: mode === 'local' ? 'fail' : 'warn',
70
+ message: ollamaCheck.error || 'Ollama not reachable',
71
+ };
72
+ if (mode === 'local' && overallStatus === 'healthy') overallStatus = 'degraded';
73
+ }
74
+ } catch (err) {
75
+ checks.local_llm = { status: 'warn', message: err.message };
76
+ }
77
+ }
78
+
79
+ // 3. Database
80
+ try {
81
+ const { getDb } = await import('../db/index.js');
82
+ const db = getDb();
83
+ if (db) {
84
+ checks.database = { status: 'ok', message: 'SQLite database initialized' };
85
+ } else {
86
+ checks.database = { status: 'fail', message: 'Database not initialized' };
87
+ overallStatus = 'unhealthy';
88
+ }
89
+ } catch (err) {
90
+ checks.database = { status: 'fail', message: err.message };
91
+ overallStatus = 'unhealthy';
92
+ }
93
+
94
+ // 4. Server readiness
95
+ checks.server = { status: 'ok', message: 'Server running' };
96
+
97
+ // 5. Cron status
98
+ const cronEnabled = process.env.ENABLE_CRON === 'true';
99
+ checks.cron = {
100
+ status: cronEnabled ? 'ok' : 'warn',
101
+ message: cronEnabled ? 'Cron scheduler active' : 'Cron disabled (ENABLE_CRON=false)',
102
+ };
103
+
104
+ // 6. Mode summary
105
+ checks.mode = { status: 'ok', message: `GIGACLAW_MODE=${mode}` };
106
+
107
+ // Version
108
+ let version = 'unknown';
109
+ try {
110
+ version = process.env.GIGACLAW_VERSION || 'unknown';
111
+ } catch (_) {}
112
+
113
+ return {
114
+ status: overallStatus,
115
+ version,
116
+ mode,
117
+ timestamp: new Date().toISOString(),
118
+ checks,
119
+ };
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gigaclaw",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "type": "module",
5
5
  "description": "GigaClaw — Gignaati's autonomous AI agent platform. Build, deploy, and run AI agents 24/7 with a two-layer architecture: Next.js Event Handler + Docker Agent. India-first, edge-native AI.",
6
6
  "bin": {
@@ -158,7 +158,7 @@
158
158
  },
159
159
  "dependencies": {
160
160
  "@ai-sdk/react": "^2.0.0",
161
- "@clack/prompts": "^0.10.0",
161
+ "@clack/prompts": "^0.10.1",
162
162
  "@grammyjs/parse-mode": "^2.2.0",
163
163
  "@langchain/anthropic": "^1.3.17",
164
164
  "@langchain/core": "^1.1.24",