strray-ai 1.15.33 → 1.15.34

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.
@@ -40,10 +40,15 @@
40
40
  import {
41
41
  existsSync,
42
42
  readFileSync,
43
+ writeFileSync,
43
44
  appendFileSync,
44
45
  mkdirSync,
46
+ symlinkSync,
47
+ unlinkSync,
48
+ renameSync,
49
+ lstatSync,
45
50
  } from "fs";
46
- import { join, dirname, resolve } from "path";
51
+ import { join, dirname, resolve, relative } from "path";
47
52
  import { fileURLToPath } from "url";
48
53
  import { homedir } from "os";
49
54
  import { createServer } from "http";
@@ -374,6 +379,356 @@ async function handleStats() {
374
379
  };
375
380
  }
376
381
 
382
+ // ============================================================================
383
+ // Quality Gate Helper (bridge-native)
384
+ // ============================================================================
385
+
386
+ /**
387
+ * Run quality gate check using the loaded QualityGateModule.
388
+ * Falls back to { passed: true } when module is not available.
389
+ */
390
+ async function runQualityGateCheck(context, projectRoot, logDir) {
391
+ if (!QualityGateModule || !QualityGateModule.runQualityGate) {
392
+ return { passed: true, violations: [], note: "quality-gate module not available" };
393
+ }
394
+
395
+ try {
396
+ const result = await QualityGateModule.runQualityGate(context);
397
+ return {
398
+ passed: result.passed,
399
+ violations: result.violations,
400
+ checks: result.checks,
401
+ };
402
+ } catch (e) {
403
+ logToActivity(logDir, `quality-gate error: ${e.message}`);
404
+ return { passed: true, violations: [], error: e.message };
405
+ }
406
+ }
407
+
408
+ // ============================================================================
409
+ // Additional Command Handlers
410
+ // ============================================================================
411
+
412
+ /**
413
+ * Validate one or more files using quality gate checks.
414
+ */
415
+ async function handleValidate(input, projectRoot, logDir) {
416
+ const { files, operation } = input;
417
+ logToActivity(logDir, `validate: files=${JSON.stringify(files || [])} operation=${operation}`);
418
+
419
+ const fileArray = Array.isArray(files) ? files : (files ? [files] : []);
420
+ if (fileArray.length === 0) {
421
+ return { error: "validate requires a 'files' array" };
422
+ }
423
+
424
+ const results = [];
425
+ for (const filePath of fileArray) {
426
+ const qualityResult = await runQualityGateCheck(
427
+ { tool: "write", args: { filePath } },
428
+ projectRoot,
429
+ logDir,
430
+ );
431
+ results.push({ file: filePath, ...qualityResult });
432
+ }
433
+
434
+ const allPassed = results.every((r) => r.passed);
435
+ return {
436
+ passed: allPassed,
437
+ operation: operation || "validate",
438
+ fileResults: results,
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Check code against codex rules — quality gate + optional deep enforcement.
444
+ */
445
+ async function handleCodexCheck(input, projectRoot, logDir) {
446
+ const { code, focusAreas, operation } = input;
447
+ const codeLen = code?.length || 0;
448
+ logToActivity(logDir, `codex-check: code_length=${codeLen} focus=${focusAreas} operation=${operation}`);
449
+
450
+ const allViolations = [];
451
+ const allChecks = [];
452
+ let enforcerRan = false;
453
+
454
+ // Phase 1: Quality gate (fast meta-checks + basic patterns)
455
+ const qualityResult = await runQualityGateCheck(
456
+ { tool: "write", args: { content: code } },
457
+ projectRoot,
458
+ logDir,
459
+ );
460
+
461
+ if (qualityResult.checks) {
462
+ allChecks.push(...qualityResult.checks);
463
+ }
464
+ if (qualityResult.violations?.length) {
465
+ allViolations.push(...qualityResult.violations);
466
+ }
467
+
468
+ // Phase 2: Enforcement validators (deep code analysis — security, quality, architecture)
469
+ // Only run content-analysis validators — skip project-level validators that
470
+ // require full context (docs, tests, CI, package.json) and always fail on snippets.
471
+ const SNIPPET_SAFE_RULES = new Set([
472
+ "security-by-design",
473
+ "input-validation",
474
+ "clean-debug-logs",
475
+ "console-log-usage",
476
+ "no-duplicate-code",
477
+ "loop-safety",
478
+ "no-over-engineering",
479
+ "single-responsibility",
480
+ "error-resolution",
481
+ "module-system-consistency",
482
+ ]);
483
+
484
+ // Try to load ValidatorRegistry from enforcement/index.js
485
+ let enforcerValidators = null;
486
+ if (codeLen > 0) {
487
+ const distDirs = [
488
+ join(projectRoot, "dist"),
489
+ join(projectRoot, "node_modules", "strray-ai", "dist"),
490
+ ];
491
+
492
+ for (const distDir of distDirs) {
493
+ try {
494
+ const rePath = join(distDir, "enforcement", "index.js");
495
+ if (existsSync(rePath)) {
496
+ const reModule = await import(rePath);
497
+ const ValidatorRegistry = reModule.ValidatorRegistry;
498
+ if (ValidatorRegistry) {
499
+ enforcerValidators = new ValidatorRegistry();
500
+ }
501
+ }
502
+ } catch (e) {
503
+ // Skip — enforcer not available
504
+ }
505
+ if (enforcerValidators) break;
506
+ }
507
+ }
508
+
509
+ if (enforcerValidators && codeLen > 0) {
510
+ try {
511
+ const ctx = { operation: "write", newCode: code, files: [] };
512
+ const validators = enforcerValidators.getAllValidators();
513
+ let enforcerViolations = 0;
514
+
515
+ for (const v of validators) {
516
+ if (!SNIPPET_SAFE_RULES.has(v.ruleId)) {
517
+ if (focusAreas && focusAreas !== "all" && Array.isArray(focusAreas)) {
518
+ if (focusAreas.includes(v.category)) {
519
+ // Fall through to validate
520
+ } else {
521
+ continue;
522
+ }
523
+ } else {
524
+ continue;
525
+ }
526
+ }
527
+
528
+ try {
529
+ const result = await v.validate(ctx);
530
+ if (!result.passed) {
531
+ enforcerViolations++;
532
+ allViolations.push({
533
+ id: v.ruleId,
534
+ severity: v.severity || "error",
535
+ message: result.message,
536
+ suggestions: result.suggestions,
537
+ });
538
+ allChecks.push({ id: v.ruleId, passed: false, message: result.message });
539
+ }
540
+ } catch {
541
+ // Skip broken validators gracefully
542
+ }
543
+ }
544
+
545
+ enforcerRan = true;
546
+ logToActivity(logDir, `codex-check: Validators ran against ${validators.length} rules (${SNIPPET_SAFE_RULES.size} snippet-safe), ${enforcerViolations} violations`);
547
+ } catch (e) {
548
+ logToActivity(logDir, `codex-check: Validator error: ${e.message}`);
549
+ }
550
+ } else if (!enforcerValidators) {
551
+ logToActivity(logDir, `codex-check: Validators not available, quality gate only`);
552
+ }
553
+
554
+ const passed = allViolations.length === 0;
555
+
556
+ return {
557
+ passed,
558
+ violations: allViolations,
559
+ checks: allChecks,
560
+ focusAreas: focusAreas || "all",
561
+ enforcerRan,
562
+ };
563
+ }
564
+
565
+ /**
566
+ * Run quality gate check on a tool+args before execution.
567
+ */
568
+ async function handlePreProcess(input, projectRoot, logDir) {
569
+ const { tool, args } = input;
570
+ const startTime = Date.now();
571
+
572
+ logToActivity(logDir, `pre-process: tool=${tool}`);
573
+
574
+ // Quality gate check
575
+ const qualityResult = await runQualityGateCheck(
576
+ { tool, args: args || {} },
577
+ projectRoot,
578
+ logDir,
579
+ );
580
+
581
+ const duration = Date.now() - startTime;
582
+ logToActivity(logDir, `pre-process: complete duration=${duration}ms quality=${qualityResult.passed}`);
583
+
584
+ return {
585
+ passed: qualityResult.passed,
586
+ duration,
587
+ qualityGate: qualityResult,
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Post-process stub — ProcessorManager not available in standalone bridge.
593
+ */
594
+ async function handlePostProcess(input, projectRoot, logDir) {
595
+ logToActivity(logDir, `post-process: stub (no ProcessorManager)`);
596
+ return { ran: false, reason: "post-processors not available in standalone bridge" };
597
+ }
598
+
599
+ /**
600
+ * Manage git hooks (install, uninstall, list, status).
601
+ * Pure filesystem operations — no framework deps needed.
602
+ */
603
+ function handleHooks(input, projectRoot) {
604
+ const { action, hooks } = input;
605
+ const hookTypes = hooks || ["pre-commit", "post-commit", "pre-push", "post-push"];
606
+ const gitHooksDir = join(projectRoot, ".git", "hooks");
607
+ const strrayHooksDir = join(projectRoot, "hooks");
608
+
609
+ if (!existsSync(gitHooksDir)) {
610
+ return { error: "Not a git repository — no .git/hooks directory" };
611
+ }
612
+
613
+ const result = { managed: [], missing: [], external: [], stale: [] };
614
+
615
+ // ── list / status ───────────────────────────────────────
616
+ if (action === "list" || action === "status") {
617
+ for (const hookName of hookTypes) {
618
+ const gitHook = join(gitHooksDir, hookName);
619
+ const strrayHook = join(strrayHooksDir, hookName);
620
+
621
+ if (!existsSync(gitHook)) {
622
+ result.missing.push(hookName);
623
+ } else {
624
+ try {
625
+ const content = readFileSync(gitHook, "utf-8");
626
+ if (content.includes("StringRay") || content.includes("strray") || content.includes("run-hook.js")) {
627
+ result.managed.push(hookName);
628
+ } else {
629
+ result.external.push(hookName);
630
+ }
631
+ } catch {
632
+ result.external.push(hookName);
633
+ }
634
+ }
635
+
636
+ // Check if strray source hook exists
637
+ if (!existsSync(strrayHook)) {
638
+ result.stale.push(hookName);
639
+ }
640
+ }
641
+
642
+ return {
643
+ status: "ok",
644
+ action,
645
+ hooks: result,
646
+ gitHooksDir,
647
+ strrayHooksDir,
648
+ };
649
+ }
650
+
651
+ // ── install ─────────────────────────────────────────────
652
+ if (action === "install") {
653
+ const installed = [];
654
+ const skipped = [];
655
+ const errors = [];
656
+
657
+ for (const hookName of hookTypes) {
658
+ const src = join(strrayHooksDir, hookName);
659
+ const dst = join(gitHooksDir, hookName);
660
+
661
+ if (!existsSync(src)) {
662
+ skipped.push(hookName);
663
+ continue;
664
+ }
665
+
666
+ try {
667
+ // Backup existing non-strray hooks
668
+ if (existsSync(dst)) {
669
+ const content = readFileSync(dst, "utf-8");
670
+ if (!content.includes("StringRay") && !content.includes("strray") && !content.includes("run-hook.js")) {
671
+ renameSync(dst, `${dst}.strray-backup`);
672
+ } else {
673
+ unlinkSync(dst);
674
+ }
675
+ }
676
+
677
+ // Create symlink
678
+ const rel = relative(join(gitHooksDir), src);
679
+ try {
680
+ symlinkSync(rel, dst);
681
+ } catch {
682
+ // Symlink may fail (permissions, cross-device) — copy instead
683
+ const srcContent = readFileSync(src, "utf-8");
684
+ writeFileSync(dst, srcContent, { mode: 0o755 });
685
+ }
686
+ installed.push(hookName);
687
+ } catch (err) {
688
+ errors.push({ hook: hookName, error: err.message });
689
+ }
690
+ }
691
+
692
+ return { status: "ok", action: "install", installed, skipped, errors };
693
+ }
694
+
695
+ // ── uninstall ───────────────────────────────────────────
696
+ if (action === "uninstall") {
697
+ const removed = [];
698
+ const restored = [];
699
+
700
+ for (const hookName of hookTypes) {
701
+ const dst = join(gitHooksDir, hookName);
702
+ const backup = `${dst}.strray-backup`;
703
+
704
+ if (!existsSync(dst)) continue;
705
+
706
+ try {
707
+ const content = readFileSync(dst, "utf-8");
708
+ const isStrray = content.includes("StringRay") || content.includes("strray") || content.includes("run-hook.js");
709
+
710
+ if (isStrray || lstatSync(dst).isSymbolicLink()) {
711
+ unlinkSync(dst);
712
+
713
+ // Restore backup if exists
714
+ if (existsSync(backup)) {
715
+ renameSync(backup, dst);
716
+ restored.push(hookName);
717
+ } else {
718
+ removed.push(hookName);
719
+ }
720
+ }
721
+ } catch {
722
+ // Skip unremovable hooks
723
+ }
724
+ }
725
+
726
+ return { status: "ok", action: "uninstall", removed, restored };
727
+ }
728
+
729
+ return { error: `Unknown hooks action: ${action}. Use: install, uninstall, list, status` };
730
+ }
731
+
377
732
  // ============================================================================
378
733
  // Known Commands
379
734
  // ============================================================================
@@ -490,6 +845,16 @@ async function dispatchCommand(command, projectRoot, logDir) {
490
845
  return await handleGetCodexPrompt(command, projectRoot, logDir);
491
846
  case "get-config":
492
847
  return await handleGetConfig(command, projectRoot, logDir);
848
+ case "validate":
849
+ return await handleValidate(command, projectRoot, logDir);
850
+ case "codex-check":
851
+ return await handleCodexCheck(command, projectRoot, logDir);
852
+ case "pre-process":
853
+ return await handlePreProcess(command, projectRoot, logDir);
854
+ case "post-process":
855
+ return await handlePostProcess(command, projectRoot, logDir);
856
+ case "hooks":
857
+ return handleHooks(command, projectRoot);
493
858
  case "stats":
494
859
  return handleStats();
495
860
  default:
@@ -222,7 +222,7 @@ async function runQualityGateCheck(context, projectRoot, logDir) {
222
222
  checks: result.checks,
223
223
  };
224
224
  } catch (e) {
225
- return { passed: true, violations: [], error: e.message };
225
+ return { passed: false, violations: [{ id: "quality-gate-error", severity: "error", message: `Quality gate failed: ${e.message}` }], error: e.message };
226
226
  }
227
227
  }
228
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strray-ai",
3
- "version": "1.15.33",
3
+ "version": "1.15.34",
4
4
  "description": "⚡ StringRay ⚡: Bulletproof AI orchestration with systematic error prevention. Zero dead ends. Ship clean, tested, optimized code — every time.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -78,8 +78,8 @@ const CALCULATED_COUNTS = calculateCounts();
78
78
  const OFFICIAL_VERSIONS = {
79
79
  // Framework version
80
80
  framework: {
81
- version: "1.15.33",
82
- displayName: "StringRay AI v1.15.33",
81
+ version: "1.15.34",
82
+ displayName: "StringRay AI v1.15.34",
83
83
  lastUpdated: "2026-03-30",
84
84
  // Counts (auto-calculated, but can be overridden)
85
85
  ...CALCULATED_COUNTS,