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 +31 -1
- package/bin/bootstrap.mjs +178 -32
- package/bin/cli.js +36 -1
- package/bin/doctor.mjs +162 -0
- package/bin/scaffold.mjs +1 -1
- package/config/instrumentation.js +28 -14
- package/lib/ai/model.js +13 -0
- package/lib/api/health.js +120 -0
- package/package.json +2 -2
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/
|
|
35
|
-
DIR: '[ 2/
|
|
36
|
-
SCAF: '[ 3/
|
|
37
|
-
DEPS: '[ 4/
|
|
38
|
-
SETUP: '[ 5/
|
|
39
|
-
ENV_W: '[ 6/
|
|
40
|
-
START: '[ 7/
|
|
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:
|
|
232
|
+
// Mode — auto-detected based on available infrastructure
|
|
233
|
+
GIGACLAW_MODE: mode,
|
|
196
234
|
|
|
197
|
-
//
|
|
198
|
-
LLM_PROVIDER:
|
|
199
|
-
LLM_MODEL:
|
|
200
|
-
//
|
|
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:
|
|
205
|
-
LOCAL_LLM_MODEL:
|
|
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
|
-
│
|
|
349
|
+
│ ${modeStr}${' '.repeat(Math.max(0, 52 - modeStr.length))}│
|
|
298
350
|
│ │
|
|
299
|
-
│
|
|
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
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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 (
|
|
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(
|
|
470
|
-
logOk(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
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.
|
|
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",
|