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.
package/dist/core/bridge.mjs
CHANGED
|
@@ -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:
|
|
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.
|
|
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.
|
|
82
|
-
displayName: "StringRay AI v1.15.
|
|
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,
|