hopeid 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli/hopeid.js +254 -19
  2. package/package.json +1 -1
package/cli/hopeid.js CHANGED
@@ -27,6 +27,7 @@ Usage:
27
27
  hopeid scan --stdin Read message from stdin
28
28
  hopeid test Run test suite (heuristic-only)
29
29
  hopeid stats Show pattern statistics
30
+ hopeid doctor Run health checks
30
31
  hopeid setup Full OpenClaw integration setup
31
32
  hopeid help Show this help
32
33
 
@@ -73,6 +74,9 @@ async function main() {
73
74
  case 'stats':
74
75
  handleStats();
75
76
  break;
77
+ case 'doctor':
78
+ await handleDoctor(args.slice(1));
79
+ break;
76
80
  case 'setup':
77
81
  await handleSetup(args.slice(1));
78
82
  break;
@@ -290,6 +294,251 @@ function readStdin() {
290
294
  });
291
295
  }
292
296
 
297
+ async function handleDoctor(args) {
298
+ const os = require('os');
299
+
300
+ console.log('\nšŸ„ hopeIDS Doctor\n');
301
+
302
+ let exitCode = 0;
303
+ const checks = [];
304
+
305
+ // Check 1: Node.js version
306
+ const nodeVersion = process.version;
307
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
308
+ const nodeOk = nodeMajor >= 18;
309
+
310
+ checks.push({
311
+ name: 'Node.js',
312
+ status: nodeOk ? 'āœ…' : 'āŒ',
313
+ details: nodeVersion,
314
+ ok: nodeOk
315
+ });
316
+
317
+ if (!nodeOk) exitCode = 1;
318
+
319
+ // Check 2: Pattern files
320
+ let patternStatus = 'āœ…';
321
+ let patternDetails = '';
322
+ let patternOk = true;
323
+
324
+ try {
325
+ const ids = new HopeIDS({ logLevel: 'error' });
326
+ const stats = ids.getStats();
327
+ patternDetails = `${stats.patternCount} loaded (${stats.categories.length} categories)`;
328
+ } catch (error) {
329
+ patternStatus = 'āŒ';
330
+ patternDetails = `Failed to load: ${error.message}`;
331
+ patternOk = false;
332
+ exitCode = 1;
333
+ }
334
+
335
+ checks.push({
336
+ name: 'Patterns',
337
+ status: patternStatus,
338
+ details: patternDetails,
339
+ ok: patternOk
340
+ });
341
+
342
+ // Check 3: LLM endpoint
343
+ let llmStatus = 'āœ…';
344
+ let llmDetails = '';
345
+ let llmOk = true;
346
+
347
+ try {
348
+ const ids = new HopeIDS({
349
+ semanticEnabled: true,
350
+ requireLLM: false,
351
+ logLevel: 'error'
352
+ });
353
+
354
+ // Try to detect provider
355
+ await ids.semantic.ensureProvider();
356
+
357
+ const provider = ids.semantic._detectedProvider;
358
+ const model = ids.semantic.options.llmModel;
359
+ const endpoint = ids.semantic.options.llmEndpoint;
360
+
361
+ if (provider === 'none' || !provider) {
362
+ llmStatus = 'āš ļø';
363
+ llmDetails = 'No endpoint configured (pattern-only mode)';
364
+ llmOk = true; // Not an error, just a warning
365
+ } else {
366
+ // Try a quick connection test
367
+ try {
368
+ if (provider === 'ollama') {
369
+ const response = await fetch('http://localhost:11434/api/tags', {
370
+ signal: AbortSignal.timeout(2000)
371
+ });
372
+ if (!response.ok) throw new Error('Ollama not responding');
373
+ } else if (provider === 'lmstudio') {
374
+ const response = await fetch('http://localhost:1234/v1/models', {
375
+ signal: AbortSignal.timeout(2000)
376
+ });
377
+ if (!response.ok) throw new Error('LM Studio not responding');
378
+ } else if (provider === 'openai' || provider === 'anthropic') {
379
+ // Just check if API key exists
380
+ if (!ids.semantic.options.apiKey) {
381
+ throw new Error('API key not set');
382
+ }
383
+ }
384
+
385
+ llmDetails = `${provider} (${model})`;
386
+ } catch (testError) {
387
+ llmStatus = 'āš ļø';
388
+ llmDetails = `${provider} configured but unreachable: ${testError.message}`;
389
+ llmOk = true; // Warning, not error
390
+ }
391
+ }
392
+ } catch (error) {
393
+ llmStatus = 'āŒ';
394
+ llmDetails = `Error: ${error.message}`;
395
+ llmOk = false;
396
+ exitCode = 1;
397
+ }
398
+
399
+ checks.push({
400
+ name: 'LLM',
401
+ status: llmStatus,
402
+ details: llmDetails,
403
+ ok: llmOk
404
+ });
405
+
406
+ // Check 4: OpenClaw plugin
407
+ let pluginStatus = 'āœ…';
408
+ let pluginDetails = 'OpenClaw plugin found';
409
+ let pluginOk = true;
410
+
411
+ const pluginPath = path.join(__dirname, '..', 'extensions', 'openclaw-plugin');
412
+ if (!fs.existsSync(pluginPath)) {
413
+ pluginStatus = 'āš ļø';
414
+ pluginDetails = 'Plugin directory not found (optional)';
415
+ pluginOk = true; // Not critical
416
+ } else {
417
+ // Check if plugin manifest exists
418
+ const manifestPath = path.join(pluginPath, 'openclaw.plugin.json');
419
+ if (!fs.existsSync(manifestPath)) {
420
+ pluginStatus = 'āš ļø';
421
+ pluginDetails = 'Plugin manifest missing';
422
+ pluginOk = true; // Not critical
423
+ }
424
+ }
425
+
426
+ checks.push({
427
+ name: 'Plugin',
428
+ status: pluginStatus,
429
+ details: pluginDetails,
430
+ ok: pluginOk
431
+ });
432
+
433
+ // Check 5: Test suite
434
+ let testStatus = 'āœ…';
435
+ let testDetails = '';
436
+ let testOk = true;
437
+
438
+ try {
439
+ const testDir = path.join(__dirname, '../test');
440
+
441
+ if (!fs.existsSync(testDir)) {
442
+ testStatus = 'āš ļø';
443
+ testDetails = 'Test directory not found';
444
+ testOk = true; // Not critical for end users
445
+ } else {
446
+ // Count test files
447
+ const attacksDir = path.join(testDir, 'attacks');
448
+ const benignDir = path.join(testDir, 'benign');
449
+
450
+ let attackCount = 0;
451
+ let benignCount = 0;
452
+
453
+ if (fs.existsSync(attacksDir)) {
454
+ attackCount = fs.readdirSync(attacksDir).filter(f => f.endsWith('.txt')).length;
455
+ }
456
+
457
+ if (fs.existsSync(benignDir)) {
458
+ benignCount = fs.readdirSync(benignDir).filter(f => f.endsWith('.txt')).length;
459
+ }
460
+
461
+ const totalTests = attackCount + benignCount;
462
+
463
+ if (totalTests === 0) {
464
+ testStatus = 'āš ļø';
465
+ testDetails = 'No test files found';
466
+ testOk = true;
467
+ } else {
468
+ testDetails = `${totalTests} tests available (run 'hopeid test' to execute)`;
469
+ }
470
+ }
471
+ } catch (error) {
472
+ testStatus = 'āš ļø';
473
+ testDetails = `Error checking tests: ${error.message}`;
474
+ testOk = true; // Not critical
475
+ }
476
+
477
+ checks.push({
478
+ name: 'Tests',
479
+ status: testStatus,
480
+ details: testDetails,
481
+ ok: testOk
482
+ });
483
+
484
+ // Check 6: Config file
485
+ let configStatus = 'āœ…';
486
+ let configDetails = '';
487
+ let configOk = true;
488
+
489
+ const homeDir = os.homedir();
490
+ const configPaths = [
491
+ path.join(homeDir, '.hopeid', 'config.json'),
492
+ path.join(homeDir, '.config', 'hopeid', 'config.json')
493
+ ];
494
+
495
+ let configFound = false;
496
+ for (const configPath of configPaths) {
497
+ if (fs.existsSync(configPath)) {
498
+ configDetails = configPath;
499
+ configFound = true;
500
+ break;
501
+ }
502
+ }
503
+
504
+ if (!configFound) {
505
+ configStatus = 'ā„¹ļø';
506
+ configDetails = 'No config file (using defaults)';
507
+ configOk = true; // Config is optional
508
+ }
509
+
510
+ checks.push({
511
+ name: 'Config',
512
+ status: configStatus,
513
+ details: configDetails,
514
+ ok: configOk
515
+ });
516
+
517
+ // Print results
518
+ for (const check of checks) {
519
+ const padding = ' '.repeat(Math.max(0, 12 - check.name.length));
520
+ console.log(` ${check.name}:${padding}${check.status} ${check.details}`);
521
+ }
522
+
523
+ console.log();
524
+
525
+ // Summary
526
+ const failed = checks.filter(c => !c.ok).length;
527
+ const warnings = checks.filter(c => c.ok && c.status !== 'āœ…').length;
528
+
529
+ if (failed > 0) {
530
+ console.log(`āŒ ${failed} check(s) failed`);
531
+ } else if (warnings > 0) {
532
+ console.log(`āš ļø ${warnings} warning(s) - hopeIDS is functional but some features may be limited`);
533
+ } else {
534
+ console.log('āœ… All checks passed - hopeIDS is healthy!');
535
+ }
536
+
537
+ console.log();
538
+
539
+ process.exit(exitCode);
540
+ }
541
+
293
542
  async function handleSetup(args) {
294
543
  const { execSync, spawnSync } = require('child_process');
295
544
  const os = require('os');
@@ -389,25 +638,11 @@ async function handleSetup(args) {
389
638
  console.log(' ā­ļø Plugin already enabled');
390
639
  }
391
640
 
392
- // Configure sandboxing for non-main agents
393
- console.log('\nšŸ”’ Configuring sandbox for public-facing agents...');
394
-
395
- if (!config.agents) config.agents = {};
396
- if (!config.agents.defaults) config.agents.defaults = {};
397
-
398
- if (!config.agents.defaults.sandbox) {
399
- config.agents.defaults.sandbox = {
400
- mode: 'non-main',
401
- scope: 'session',
402
- workspaceAccess: 'none'
403
- };
404
- console.log(' āœ… Sandbox enabled for non-main agents');
405
- console.log(' Mode: non-main (main agent runs on host, others sandboxed)');
406
- console.log(' Scope: session (each session gets isolated container)');
407
- console.log(' Workspace: none (sandboxed agents get clean workspace)');
408
- } else {
409
- console.log(' ā­ļø Sandbox already configured');
410
- }
641
+ // Note about sandboxing (don't auto-configure - it can break workers)
642
+ console.log('\nšŸ”’ Sandbox configuration...');
643
+ console.log(' ā„¹ļø Sandbox NOT auto-configured (can break worker agents)');
644
+ console.log(' šŸ“– For public-facing agents (moltbook, social), manually add:');
645
+ console.log(' agents.list[].sandbox: { mode: "all", workspaceAccess: "none" }');
411
646
 
412
647
  // Write updated config
413
648
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hopeid",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "šŸ›”ļø Inference-based intrusion detection for AI agents. Traditional IDS matches signatures. HoPE understands intent.",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",