@wrongstack/core 0.5.3 → 0.5.6

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 (50) hide show
  1. package/dist/{agent-bridge-EiUFe3if.d.ts → agent-bridge-B07AYFBk.d.ts} +1 -1
  2. package/dist/{compactor-BP4xhKVi.d.ts → compactor-BWhJXxsW.d.ts} +1 -1
  3. package/dist/{config-BOD_HQBw.d.ts → config-BgM0BIpz.d.ts} +1 -1
  4. package/dist/{context-PH4DvBZV.d.ts → context-CLZXPPYk.d.ts} +18 -1
  5. package/dist/coordination/index.d.ts +10 -9
  6. package/dist/coordination/index.js +269 -15
  7. package/dist/coordination/index.js.map +1 -1
  8. package/dist/defaults/index.d.ts +20 -19
  9. package/dist/defaults/index.js +459 -17
  10. package/dist/defaults/index.js.map +1 -1
  11. package/dist/director-state-BmYi3DGA.d.ts +108 -0
  12. package/dist/{events-oxn-Wkub.d.ts → events-qnDZbrtb.d.ts} +67 -2
  13. package/dist/execution/index.d.ts +12 -12
  14. package/dist/extension/index.d.ts +6 -6
  15. package/dist/{index-CcbWbcpy.d.ts → index-BDnUCRvL.d.ts} +113 -6
  16. package/dist/{index-a12jc7-r.d.ts → index-DPLJw_ZI.d.ts} +5 -5
  17. package/dist/index.d.ts +329 -27
  18. package/dist/index.js +2405 -27
  19. package/dist/index.js.map +1 -1
  20. package/dist/infrastructure/index.d.ts +6 -6
  21. package/dist/kernel/index.d.ts +9 -9
  22. package/dist/kernel/index.js +98 -1
  23. package/dist/kernel/index.js.map +1 -1
  24. package/dist/{mcp-servers-uPmBxZ1B.d.ts → mcp-servers-CSMfaBuL.d.ts} +3 -3
  25. package/dist/models/index.d.ts +2 -2
  26. package/dist/{multi-agent-D6S4Z7H8.d.ts → multi-agent-Cv8wk47i.d.ts} +2 -2
  27. package/dist/observability/index.d.ts +2 -2
  28. package/dist/{path-resolver-CprD5DhS.d.ts → path-resolver-DiCUvEg6.d.ts} +2 -2
  29. package/dist/{director-state-CVzkjKRZ.d.ts → plan-templates-DaxTCPfk.d.ts} +38 -77
  30. package/dist/{provider-runner-DGisz_lG.d.ts → provider-runner-3SHqk9zB.d.ts} +3 -3
  31. package/dist/{retry-policy-DUJ8-Qc_.d.ts → retry-policy-LLUxJmYY.d.ts} +1 -1
  32. package/dist/sdd/index.d.ts +3 -3
  33. package/dist/{secret-scrubber-CB11A2P7.d.ts → secret-scrubber-BhJTNr9v.d.ts} +4 -2
  34. package/dist/{secret-scrubber-EqFa0SyI.d.ts → secret-scrubber-Z_VXuXQT.d.ts} +1 -1
  35. package/dist/security/index.d.ts +13 -3
  36. package/dist/security/index.js +34 -1
  37. package/dist/security/index.js.map +1 -1
  38. package/dist/{selector-yqyxt-Ii.d.ts → selector-DB2-byKH.d.ts} +1 -1
  39. package/dist/{session-reader-1tOyoY1s.d.ts → session-reader-4jxsYLZ8.d.ts} +1 -1
  40. package/dist/storage/index.d.ts +7 -6
  41. package/dist/storage/index.js +222 -3
  42. package/dist/storage/index.js.map +1 -1
  43. package/dist/{system-prompt-BJlzKGO6.d.ts → system-prompt-DI4Dtc1I.d.ts} +1 -1
  44. package/dist/{tool-executor-B6kRcWeF.d.ts → tool-executor-DSvmOBe6.d.ts} +4 -4
  45. package/dist/types/index.d.ts +15 -15
  46. package/dist/types/index.js +9 -1
  47. package/dist/types/index.js.map +1 -1
  48. package/dist/utils/index.d.ts +2 -2
  49. package/dist/{wstack-paths-BGu2INTm.d.ts → wstack-paths-86YPFktR.d.ts} +1 -1
  50. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,9 @@ import * as crypto2 from 'crypto';
2
2
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash } from 'crypto';
3
3
  import * as fs from 'fs';
4
4
  import * as fsp2 from 'fs/promises';
5
+ import { readFile, readdir, writeFile, stat, mkdir } from 'fs/promises';
5
6
  import * as path6 from 'path';
7
+ import { join, extname, relative } from 'path';
6
8
  import * as os4 from 'os';
7
9
  import { EventEmitter } from 'events';
8
10
  import { createGunzip } from 'zlib';
@@ -10,6 +12,13 @@ import { Readable } from 'stream';
10
12
  import { pipeline } from 'stream/promises';
11
13
  import { spawn } from 'child_process';
12
14
 
15
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
16
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
17
+ }) : x)(function(x) {
18
+ if (typeof require !== "undefined") return require.apply(this, arguments);
19
+ throw Error('Dynamic require of "' + x + '" is not supported');
20
+ });
21
+
13
22
  // src/types/errors.ts
14
23
  var WrongStackError = class extends Error {
15
24
  code;
@@ -506,6 +515,103 @@ var EventBus = class {
506
515
  return this.wildcards.length;
507
516
  }
508
517
  };
518
+ var ScopedEventBus = class extends EventBus {
519
+ // Track registrations by a unique counter key so that EventBus.once()'s
520
+ // internal listener-removal doesn't affect our tracking (once removes the
521
+ // fn from EventBus but we still need to call our unsub during teardown).
522
+ registrations = /* @__PURE__ */ new Map();
523
+ nextKey = 0;
524
+ /**
525
+ * Identical to `EventBus.on` but the listener is tracked so that
526
+ * `teardown()` will remove it automatically.
527
+ */
528
+ on(event, fn) {
529
+ const key = this.nextKey++;
530
+ const unsub = super.on(event, fn);
531
+ this.registrations.set(key, unsub);
532
+ return () => {
533
+ this.registrations.delete(key);
534
+ unsub();
535
+ };
536
+ }
537
+ /**
538
+ * Identical to `EventBus.once` but the listener is tracked so that
539
+ * `teardown()` will remove it automatically.
540
+ *
541
+ * Uses EventBus's public API directly to avoid triggering our own `on()`
542
+ * override (which would consume a key slot for the wrapper, then orphan
543
+ * our registration entry under a different key).
544
+ */
545
+ once(event, fn) {
546
+ const key = this.nextKey++;
547
+ const wrapper = (payload) => {
548
+ EventBus.prototype.off.call(this, event, wrapper);
549
+ fn(payload);
550
+ };
551
+ EventBus.prototype.on.call(this, event, wrapper);
552
+ const unsub = () => {
553
+ this.registrations.delete(key);
554
+ EventBus.prototype.off.call(this, event, wrapper);
555
+ };
556
+ this.registrations.set(key, unsub);
557
+ return unsub;
558
+ }
559
+ /**
560
+ * Identical to `EventBus.onPattern` but the listener is tracked so that
561
+ * `teardown()` will remove it automatically.
562
+ */
563
+ onPattern(pattern, fn) {
564
+ const key = this.nextKey++;
565
+ const unsub = super.onPattern(pattern, fn);
566
+ this.registrations.set(key, unsub);
567
+ return () => {
568
+ this.registrations.delete(key);
569
+ unsub();
570
+ };
571
+ }
572
+ /**
573
+ * Identical to `EventBus.onRegex` but the listener is tracked so that
574
+ * `teardown()` will remove it automatically.
575
+ */
576
+ onRegex(regex, fn) {
577
+ const key = this.nextKey++;
578
+ const unsub = super.onRegex(regex, fn);
579
+ this.registrations.set(key, unsub);
580
+ return () => {
581
+ this.registrations.delete(key);
582
+ unsub();
583
+ };
584
+ }
585
+ /**
586
+ * Remove every listener that was registered through this scoped bus.
587
+ * Idempotent — calling it multiple times is safe.
588
+ *
589
+ * Also available as `[Symbol.dispose]` for explicit resource management:
590
+ * ```ts
591
+ * using scope = new ScopedEventBus();
592
+ * scope.on('tool.executed', handler);
593
+ * // automatically teardown()'d when scope exits
594
+ * ```
595
+ */
596
+ teardown() {
597
+ for (const unsub of this.registrations.values()) {
598
+ try {
599
+ unsub();
600
+ } catch {
601
+ }
602
+ }
603
+ this.registrations.clear();
604
+ this.clear();
605
+ }
606
+ /** Alias for `teardown()` — enables `using new ScopedEventBus()` in Node ≥ 22. */
607
+ [Symbol.dispose]() {
608
+ this.teardown();
609
+ }
610
+ /** Number of tracked registrations. */
611
+ get scopedListenerCount() {
612
+ return this.registrations.size;
613
+ }
614
+ };
509
615
  function makePatternMatcher(pattern) {
510
616
  if (pattern === "*") return () => true;
511
617
  if (pattern.endsWith(".*")) {
@@ -1688,7 +1794,15 @@ var PATTERNS = [
1688
1794
  type: "stripe_key",
1689
1795
  regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
1690
1796
  },
1691
- { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
1797
+ {
1798
+ type: "twilio_sid",
1799
+ regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g
1800
+ },
1801
+ {
1802
+ type: "telegram_bot_token",
1803
+ // Telegram tokens are of the form bot<digits>:<alphanum> in URL paths
1804
+ regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
1805
+ },
1692
1806
  {
1693
1807
  type: "jwt",
1694
1808
  // Anchored: look for literal "eyJ" which is unambiguous for JWT header
@@ -1786,8 +1900,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
1786
1900
  }
1787
1901
  let mode;
1788
1902
  try {
1789
- const stat6 = await fsp2.stat(targetPath);
1790
- mode = stat6.mode & 511;
1903
+ const stat8 = await fsp2.stat(targetPath);
1904
+ mode = stat8.mode & 511;
1791
1905
  } catch {
1792
1906
  mode = opts.mode;
1793
1907
  }
@@ -3827,8 +3941,8 @@ var DefaultSessionStore = class {
3827
3941
  return JSON.parse(raw);
3828
3942
  } catch {
3829
3943
  const full = path6.join(this.dir, `${id}.jsonl`);
3830
- const stat6 = await fsp2.stat(full);
3831
- const summary = await this.summarize(id, stat6.mtime.toISOString());
3944
+ const stat8 = await fsp2.stat(full);
3945
+ const summary = await this.summarize(id, stat8.mtime.toISOString());
3832
3946
  await fsp2.writeFile(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
3833
3947
  console.warn(
3834
3948
  `[session-store] Failed to write manifest for "${id}":`,
@@ -4495,6 +4609,16 @@ function labelOf(scope) {
4495
4609
  }
4496
4610
 
4497
4611
  // src/storage/config-store.ts
4612
+ function stripEphemeralFields(cfg) {
4613
+ const env = cfg._envSource;
4614
+ if (!env?.size) return cfg;
4615
+ const out = { ...cfg };
4616
+ for (const field of env) {
4617
+ delete out[field];
4618
+ }
4619
+ delete out._envSource;
4620
+ return out;
4621
+ }
4498
4622
  var DefaultConfigStore = class {
4499
4623
  current;
4500
4624
  watchers = /* @__PURE__ */ new Set();
@@ -4512,7 +4636,8 @@ var DefaultConfigStore = class {
4512
4636
  return ext ? ext : FROZEN_EMPTY;
4513
4637
  }
4514
4638
  update(partial) {
4515
- const next = deepFreeze(structuredClone({ ...this.current, ...partial }));
4639
+ const scrubbed = stripEphemeralFields(partial);
4640
+ const next = deepFreeze(structuredClone({ ...this.current, ...scrubbed }));
4516
4641
  if (next.version !== 1) {
4517
4642
  throw new Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
4518
4643
  }
@@ -4617,15 +4742,19 @@ var BEHAVIOR_DEFAULTS = {
4617
4742
  var ENV_MAP = {
4618
4743
  WRONGSTACK_PROVIDER: (c, v) => {
4619
4744
  c.provider = v;
4745
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("provider");
4620
4746
  },
4621
4747
  WRONGSTACK_MODEL: (c, v) => {
4622
4748
  c.model = v;
4749
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("model");
4623
4750
  },
4624
4751
  WRONGSTACK_API_KEY: (c, v) => {
4625
4752
  c.apiKey = v;
4753
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("apiKey");
4626
4754
  },
4627
4755
  WRONGSTACK_BASE_URL: (c, v) => {
4628
4756
  c.baseUrl = v;
4757
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("baseUrl");
4629
4758
  },
4630
4759
  WRONGSTACK_LOG_LEVEL: (c, v) => {
4631
4760
  if (!c.log) c.log = { level: "info" };
@@ -5219,9 +5348,149 @@ function matchIndex(plan, idOrIndex) {
5219
5348
  const lower = idOrIndex.toLowerCase();
5220
5349
  return plan.items.findIndex((it) => it.title.toLowerCase().includes(lower));
5221
5350
  }
5351
+ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
5352
+ const idx = matchIndex(plan, idOrIndex);
5353
+ if (idx === -1) return null;
5354
+ const item = plan.items[idx];
5355
+ if (!item) return null;
5356
+ let updatedPlan = plan;
5357
+ if (item.status !== "done") {
5358
+ updatedPlan = setPlanItemStatus(plan, idOrIndex, "in_progress");
5359
+ }
5360
+ const todos = [];
5361
+ todos.push({
5362
+ id: `todo_${Date.now()}_plan`,
5363
+ content: item.title,
5364
+ status: "in_progress",
5365
+ activeForm: item.title
5366
+ });
5367
+ if (subtasks && subtasks.length > 0) {
5368
+ for (const st of subtasks) {
5369
+ todos.push({
5370
+ id: `todo_${Date.now()}_${randomUUID().slice(0, 6)}`,
5371
+ content: st,
5372
+ status: "pending"
5373
+ });
5374
+ }
5375
+ }
5376
+ return { plan: updatedPlan, todos };
5377
+ }
5222
5378
  function attachPlanCheckpoint(_state, _filePath, _sessionId) {
5223
5379
  return () => void 0;
5224
5380
  }
5381
+
5382
+ // src/storage/plan-templates.ts
5383
+ var templates = {
5384
+ "new-feature": {
5385
+ name: "new-feature",
5386
+ description: "Standard workflow for adding a new feature",
5387
+ category: "development",
5388
+ items: [
5389
+ { title: "Write specification / design doc", details: "Define scope, acceptance criteria, edge cases" },
5390
+ { title: "Set up feature branch", details: "git checkout -b feature/..." },
5391
+ { title: "Implement core logic", details: "TDD preferred \u2014 write tests first" },
5392
+ { title: "Add unit tests", details: ">= 80% coverage for new code" },
5393
+ { title: "Add integration tests", details: "End-to-end happy path + error paths" },
5394
+ { title: "Update documentation", details: "README, API docs, changelog" },
5395
+ { title: "Code review", details: "Self-review before requesting review" },
5396
+ { title: "Merge and deploy", details: "CI green, tag release" }
5397
+ ]
5398
+ },
5399
+ "bug-fix": {
5400
+ name: "bug-fix",
5401
+ description: "Systematic approach to fixing bugs",
5402
+ category: "maintenance",
5403
+ items: [
5404
+ { title: "Reproduce the bug", details: "Minimal reproduction case" },
5405
+ { title: "Root cause analysis", details: "Trace through logs, debugger" },
5406
+ { title: "Write failing test", details: "Test must fail before fix" },
5407
+ { title: "Implement fix", details: "Smallest possible change" },
5408
+ { title: "Verify fix", details: "Test passes, reproduction no longer fails" },
5409
+ { title: "Regression test", details: "Ensure no related tests broken" },
5410
+ { title: "Document in changelog", details: "Brief description + issue link" }
5411
+ ]
5412
+ },
5413
+ "refactor": {
5414
+ name: "refactor",
5415
+ description: "Safe refactoring workflow",
5416
+ category: "maintenance",
5417
+ items: [
5418
+ { title: "Identify refactoring target", details: "Code smell, performance bottleneck, or tech debt" },
5419
+ { title: "Ensure test coverage", details: "Existing tests must pass before and after" },
5420
+ { title: "Write characterization tests", details: "Capture current behavior if tests weak" },
5421
+ { title: "Apply refactoring", details: "Small steps, frequent commits" },
5422
+ { title: "Run full test suite", details: "All tests must pass" },
5423
+ { title: "Performance check", details: "Ensure no regression" },
5424
+ { title: "Code review", details: "Explain the why, not just the what" }
5425
+ ]
5426
+ },
5427
+ "release": {
5428
+ name: "release",
5429
+ description: "Preparing a new release",
5430
+ category: "release",
5431
+ items: [
5432
+ { title: "Version bump", details: "package.json, lockfiles, tags" },
5433
+ { title: "Update changelog", details: "All changes since last release" },
5434
+ { title: "Run full test suite", details: "Unit + integration + e2e" },
5435
+ { title: "Build artifacts", details: "Docker images, bundles, binaries" },
5436
+ { title: "Staging smoke tests", details: "Deploy to staging, verify" },
5437
+ { title: "Production deploy", details: "Blue-green or canary" },
5438
+ { title: "Post-deploy verification", details: "Health checks, error rates" },
5439
+ { title: "Announce release", details: "Slack, email, GitHub release notes" }
5440
+ ]
5441
+ },
5442
+ "security-audit": {
5443
+ name: "security-audit",
5444
+ description: "Security review and hardening",
5445
+ category: "infrastructure",
5446
+ items: [
5447
+ { title: "Dependency audit", details: "npm audit, Snyk, Dependabot alerts" },
5448
+ { title: "Secret scan", details: "git-secrets, truffleHog, manual review" },
5449
+ { title: "Access control review", details: "IAM, roles, least privilege" },
5450
+ { title: "Input validation audit", details: "SQL injection, XSS, path traversal" },
5451
+ { title: "Authentication review", details: "Session management, MFA, password policy" },
5452
+ { title: "Logging and monitoring", details: "PII in logs, audit trails" },
5453
+ { title: "Incident response plan", details: "Runbooks, contacts, escalation" }
5454
+ ]
5455
+ },
5456
+ "onboarding": {
5457
+ name: "onboarding",
5458
+ description: "New developer onboarding checklist",
5459
+ category: "infrastructure",
5460
+ items: [
5461
+ { title: "Repository access", details: "GitHub/GitLab permissions" },
5462
+ { title: "Local environment setup", details: "Docker, dependencies, env files" },
5463
+ { title: "Run tests locally", details: "Verify green suite" },
5464
+ { title: "Read architecture docs", details: "ADR, README, onboarding guide" },
5465
+ { title: "First commit", details: "Docs fix or small improvement" },
5466
+ { title: "Pair programming session", details: "Walk through codebase with buddy" },
5467
+ { title: "Deploy to staging", details: "Verify CI/CD access" }
5468
+ ]
5469
+ }
5470
+ };
5471
+ function listPlanTemplates() {
5472
+ return Object.values(templates);
5473
+ }
5474
+ function getPlanTemplate(name) {
5475
+ return templates[name];
5476
+ }
5477
+ function formatPlanTemplates() {
5478
+ const cats = /* @__PURE__ */ new Map();
5479
+ for (const t2 of Object.values(templates)) {
5480
+ const arr = cats.get(t2.category) ?? [];
5481
+ arr.push(t2);
5482
+ cats.set(t2.category, arr);
5483
+ }
5484
+ const lines = ["Available plan templates:"];
5485
+ for (const [cat, items] of cats) {
5486
+ lines.push(`
5487
+ ${cat}:`);
5488
+ for (const t2 of items) {
5489
+ lines.push(` ${t2.name.padEnd(18)} \u2014 ${t2.description}`);
5490
+ }
5491
+ }
5492
+ return lines.join("\n");
5493
+ }
5225
5494
  async function loadDirectorState(filePath) {
5226
5495
  let raw;
5227
5496
  try {
@@ -5237,15 +5506,48 @@ async function loadDirectorState(filePath) {
5237
5506
  return null;
5238
5507
  }
5239
5508
  }
5509
+ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
5510
+ let existing;
5511
+ try {
5512
+ existing = await fsp2.readFile(lockPath, "utf8");
5513
+ } catch {
5514
+ }
5515
+ if (existing) {
5516
+ try {
5517
+ const lock2 = JSON.parse(existing);
5518
+ try {
5519
+ process.kill(lock2.pid, 0);
5520
+ return false;
5521
+ } catch {
5522
+ }
5523
+ } catch {
5524
+ }
5525
+ }
5526
+ const lock = {
5527
+ pid: processId,
5528
+ hostname: __require("os").hostname(),
5529
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5530
+ };
5531
+ await atomicWrite(lockPath, JSON.stringify(lock), { mode: 384 });
5532
+ return true;
5533
+ }
5534
+ async function releaseDirectorStateLock(lockPath) {
5535
+ try {
5536
+ await fsp2.unlink(lockPath);
5537
+ } catch {
5538
+ }
5539
+ }
5240
5540
  var DirectorStateCheckpoint = class {
5241
5541
  snapshot;
5242
5542
  filePath;
5543
+ lockPath;
5243
5544
  timer = null;
5244
5545
  debounceMs;
5245
5546
  writing = false;
5246
5547
  rewriteRequested = false;
5247
5548
  constructor(filePath, init, debounceMs = 250) {
5248
5549
  this.filePath = filePath;
5550
+ this.lockPath = `${filePath}.lock`;
5249
5551
  this.debounceMs = debounceMs;
5250
5552
  this.snapshot = {
5251
5553
  version: 1,
@@ -5255,10 +5557,36 @@ var DirectorStateCheckpoint = class {
5255
5557
  maxSpawns: init.maxSpawns,
5256
5558
  spawnDepth: init.spawnDepth,
5257
5559
  maxSpawnDepth: init.maxSpawnDepth,
5560
+ directorBudget: init.directorBudget,
5258
5561
  subagents: [],
5259
5562
  tasks: []
5260
5563
  };
5261
5564
  }
5565
+ /**
5566
+ * Attempt to acquire the lock for this checkpoint. Call this before
5567
+ * resuming a crashed director run. If it returns false, another
5568
+ * director process is still running this fleet — do not resume.
5569
+ */
5570
+ async acquireLock() {
5571
+ return acquireDirectorStateLock(this.lockPath);
5572
+ }
5573
+ /**
5574
+ * Release the lock on graceful shutdown. Call `flush()` first to ensure
5575
+ * the final checkpoint state is on disk before removing the lock.
5576
+ * Without this, the next resume will see a stale-lock and refuse.
5577
+ */
5578
+ async releaseLock() {
5579
+ return releaseDirectorStateLock(this.lockPath);
5580
+ }
5581
+ /**
5582
+ * Resume from a snapshot previously loaded via `loadDirectorState()`.
5583
+ * Use this when `--resume <runId>` is triggered — the snapshot has
5584
+ * the full fleet state (subagents, tasks) from before the crash; the
5585
+ * checkpoint continues from there.
5586
+ */
5587
+ resume(snapshot) {
5588
+ this.snapshot = snapshot;
5589
+ }
5262
5590
  current() {
5263
5591
  return this.snapshot;
5264
5592
  }
@@ -5346,6 +5674,7 @@ var DefaultPermissionPolicy = class {
5346
5674
  loaded = false;
5347
5675
  trustFile;
5348
5676
  yolo;
5677
+ forceAllYolo;
5349
5678
  /**
5350
5679
  * Session-scoped "soft deny" map. When the user presses 'n' (block once),
5351
5680
  * the tool+pattern is added here. If the LLM retries in the same session,
@@ -5378,6 +5707,7 @@ var DefaultPermissionPolicy = class {
5378
5707
  constructor(opts) {
5379
5708
  this.trustFile = opts.trustFile;
5380
5709
  this.yolo = opts.yolo ?? false;
5710
+ this.forceAllYolo = opts.forceAllYolo ?? false;
5381
5711
  this.promptDelegate = opts.promptDelegate;
5382
5712
  }
5383
5713
  /**
@@ -5397,6 +5727,14 @@ var DefaultPermissionPolicy = class {
5397
5727
  getYolo() {
5398
5728
  return this.yolo;
5399
5729
  }
5730
+ /** Toggle force-all-YOLO at runtime. */
5731
+ setForceAllYolo(enabled) {
5732
+ this.forceAllYolo = enabled;
5733
+ }
5734
+ /** Check whether force-all-YOLO is active. */
5735
+ getForceAllYolo() {
5736
+ return this.forceAllYolo;
5737
+ }
5400
5738
  async reload() {
5401
5739
  try {
5402
5740
  const raw = await fsp2.readFile(this.trustFile, "utf8");
@@ -5438,6 +5776,21 @@ var DefaultPermissionPolicy = class {
5438
5776
  return { permission: "auto", source: "trust" };
5439
5777
  }
5440
5778
  if (this.yolo) {
5779
+ if (tool.riskTier === "destructive" && !this.forceAllYolo) {
5780
+ if (this.promptDelegate) {
5781
+ const decision = await this.promptDelegate(tool, input, subject ?? tool.name);
5782
+ if (decision === "always") {
5783
+ await this.trust({ tool: tool.name, pattern: subject ?? tool.name });
5784
+ return { permission: "auto", source: "user", reason: "destructive yolo always-allowed" };
5785
+ }
5786
+ if (decision === "deny") {
5787
+ await this.deny({ tool: tool.name, pattern: subject ?? tool.name });
5788
+ return { permission: "deny", source: "user", reason: "user denied destructive yolo" };
5789
+ }
5790
+ return { permission: decision === "yes" ? "auto" : "deny", source: "user" };
5791
+ }
5792
+ return { permission: "confirm", source: "yolo_destructive", riskTier: "destructive", reason: "destructive tool needs explicit approval even in yolo mode" };
5793
+ }
5441
5794
  return { permission: "auto", source: "yolo" };
5442
5795
  }
5443
5796
  if (tool.name === "write" && subject) {
@@ -7968,6 +8321,9 @@ function makeSpawnTool(director, roster) {
7968
8321
  if (err instanceof DirectorBudgetError) {
7969
8322
  return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
7970
8323
  }
8324
+ if (err instanceof DirectorCostCapError) {
8325
+ return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
8326
+ }
7971
8327
  return { error: err instanceof Error ? err.message : String(err) };
7972
8328
  }
7973
8329
  }
@@ -8105,6 +8461,65 @@ function makeFleetUsageTool(director) {
8105
8461
  }
8106
8462
  };
8107
8463
  }
8464
+ function makeFleetSessionTool(director) {
8465
+ return {
8466
+ name: "fleet_session",
8467
+ description: "Read a subagent's JSONL transcript and extract its last assistant text, stop reason, and tool-use count. Use this to see what a running or timed-out subagent actually produced.",
8468
+ permission: "auto",
8469
+ mutating: false,
8470
+ inputSchema: {
8471
+ type: "object",
8472
+ properties: {
8473
+ subagentId: { type: "string", description: "Subagent id to read the transcript of." },
8474
+ /** Number of trailing lines to return (last N JSONL lines). Default: all. */
8475
+ tail: { type: "number", description: "Number of trailing JSONL lines to return. Omit for the full transcript." }
8476
+ },
8477
+ required: ["subagentId"]
8478
+ },
8479
+ async execute(input) {
8480
+ const i = input;
8481
+ const result = await director.readSession(i.subagentId, i.tail);
8482
+ if (!result) {
8483
+ return {
8484
+ error: `fleet_session: transcript unavailable for "${i.subagentId}". Is sessionsRoot configured?`
8485
+ };
8486
+ }
8487
+ return result;
8488
+ }
8489
+ };
8490
+ }
8491
+ function makeFleetHealthTool(director) {
8492
+ return {
8493
+ name: "fleet_health",
8494
+ description: "Per-subagent health report: budget pressure (pct of limits consumed), last activity timestamp, and current status. Use to decide whether to assign more work to a subagent or spawn a fresh one.",
8495
+ permission: "auto",
8496
+ mutating: false,
8497
+ inputSchema: { type: "object", properties: {}, required: [] },
8498
+ async execute() {
8499
+ const status = director.status();
8500
+ const snapshot = director.snapshot();
8501
+ const subagents = status.subagents ?? [];
8502
+ const perSubagent = snapshot.perSubagent ?? {};
8503
+ return {
8504
+ subagents: subagents.map((s) => {
8505
+ const usage = perSubagent[s.id];
8506
+ return {
8507
+ id: s.id,
8508
+ status: s.status,
8509
+ lastEventAt: usage?.lastEventAt,
8510
+ // Budget pressure: fraction of each limit consumed if we have it.
8511
+ // BudgetWarning events carry used/limit ratios; surface them here.
8512
+ budgetPressure: {
8513
+ iterations: usage?.iterations,
8514
+ toolCalls: usage?.toolCalls,
8515
+ costUsd: usage?.cost
8516
+ }
8517
+ };
8518
+ })
8519
+ };
8520
+ }
8521
+ };
8522
+ }
8108
8523
 
8109
8524
  // src/coordination/director.ts
8110
8525
  var DirectorBudgetError = class extends Error {
@@ -8121,6 +8536,20 @@ var DirectorBudgetError = class extends Error {
8121
8536
  this.observed = observed;
8122
8537
  }
8123
8538
  };
8539
+ var DirectorCostCapError = class extends Error {
8540
+ kind;
8541
+ limit;
8542
+ observed;
8543
+ constructor(limit, observed) {
8544
+ super(
8545
+ `Director cost cap exceeded: total fleet spend ${observed.toFixed(4)} exceeds maxCostUsd ${limit.toFixed(4)}`
8546
+ );
8547
+ this.name = "DirectorCostCapError";
8548
+ this.kind = "max_cost_usd";
8549
+ this.limit = limit;
8550
+ this.observed = observed;
8551
+ }
8552
+ };
8124
8553
  var Director = class {
8125
8554
  id;
8126
8555
  fleet;
@@ -8175,6 +8604,14 @@ var Director = class {
8175
8604
  /** Debounce timer for periodic manifest writes. */
8176
8605
  manifestTimer = null;
8177
8606
  manifestDebounceMs;
8607
+ /** Fleet-wide cost cap. Infinity means no cap. */
8608
+ maxCostUsd;
8609
+ /** Max auto-extensions per subagent per budget kind before denying. */
8610
+ maxBudgetExtensions;
8611
+ /** Sessions root for direct subagent JSONL reads (fleet_session tool). */
8612
+ sessionsRoot;
8613
+ /** Director run id for JSONL path resolution. */
8614
+ directorRunId;
8178
8615
  /** Resolves task descriptions back from `assign()` so completion events
8179
8616
  * can also carry a human-readable title. */
8180
8617
  taskDescriptions = /* @__PURE__ */ new Map();
@@ -8201,12 +8638,17 @@ var Director = class {
8201
8638
  this.spawnDepth = opts.spawnDepth ?? 0;
8202
8639
  this.sessionWriter = opts.sessionWriter ?? null;
8203
8640
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
8641
+ this.maxCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
8642
+ this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 2;
8643
+ this.sessionsRoot = opts.sessionsRoot;
8644
+ this.directorRunId = opts.directorRunId ?? this.id;
8204
8645
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
8205
8646
  directorRunId: this.id,
8206
8647
  maxSpawns: opts.maxSpawns,
8207
8648
  spawnDepth: this.spawnDepth,
8208
- maxSpawnDepth: this.maxSpawnDepth
8209
- }) : null;
8649
+ maxSpawnDepth: this.maxSpawnDepth,
8650
+ directorBudget: opts.directorBudget
8651
+ }, opts.checkpointDebounceMs ?? 250) : null;
8210
8652
  if (this.sharedScratchpadPath) {
8211
8653
  void fsp2.mkdir(this.sharedScratchpadPath, { recursive: true }).catch(() => void 0);
8212
8654
  }
@@ -8267,7 +8709,7 @@ var Director = class {
8267
8709
  const payload = e.payload;
8268
8710
  const guardKey = `${e.subagentId}:${payload.kind}`;
8269
8711
  const prior = extendCounts.get(guardKey) ?? 0;
8270
- if (prior >= 2) {
8712
+ if (prior >= this.maxBudgetExtensions) {
8271
8713
  payload.deny();
8272
8714
  extendCounts.delete(guardKey);
8273
8715
  return;
@@ -8328,6 +8770,12 @@ var Director = class {
8328
8770
  if (this.spawnCount >= this.maxSpawns) {
8329
8771
  throw new DirectorBudgetError("max_spawns", this.maxSpawns, this.spawnCount + 1);
8330
8772
  }
8773
+ if (this.maxCostUsd < Number.POSITIVE_INFINITY) {
8774
+ const totalCost = this.usage.snapshot().total?.cost ?? 0;
8775
+ if (totalCost >= this.maxCostUsd) {
8776
+ throw new DirectorCostCapError(this.maxCostUsd, totalCost);
8777
+ }
8778
+ }
8331
8779
  const result = await this.coordinator.spawn(config);
8332
8780
  this.spawnCount += 1;
8333
8781
  this.subagentMeta.set(result.subagentId, {
@@ -8507,6 +8955,7 @@ var Director = class {
8507
8955
  if (this.stateCheckpoint) {
8508
8956
  this.stateCheckpoint.setUsage(this.usage.snapshot());
8509
8957
  await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
8958
+ await this.stateCheckpoint.releaseLock().catch((err) => this.logShutdownError("state_checkpoint_lock_release", err));
8510
8959
  }
8511
8960
  }
8512
8961
  /**
@@ -8610,6 +9059,58 @@ var Director = class {
8610
9059
  completedResults() {
8611
9060
  return Array.from(this.completed.values());
8612
9061
  }
9062
+ /**
9063
+ * Inject a previously-saved checkpoint snapshot. Call this right after
9064
+ * constructing a Director during a `--resume` run so the in-memory state
9065
+ * (subagents, tasks, waiters) reflects the pre-crash reality instead of
9066
+ * starting from a blank slate. The director then resumes from there —
9067
+ * completing any in-flight tasks and ignoring tasks that already reached
9068
+ * a terminal state in the prior run.
9069
+ */
9070
+ setCheckpointState(snapshot) {
9071
+ this.stateCheckpoint?.resume(snapshot);
9072
+ }
9073
+ /**
9074
+ * Read a subagent's JSONL transcript directly from disk (no bridge
9075
+ * round-trip needed). Returns the last assistant text, stop reason,
9076
+ * tool-use count, and line count — or null if the file is unavailable.
9077
+ * Requires `sessionsRoot` to be set on construction.
9078
+ */
9079
+ async readSession(subagentId, tail) {
9080
+ if (!this.sessionsRoot) return null;
9081
+ const filePath = path6.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
9082
+ let raw;
9083
+ try {
9084
+ raw = await fsp2.readFile(filePath, "utf8");
9085
+ } catch {
9086
+ return null;
9087
+ }
9088
+ const lines = raw.split("\n").filter((l) => l.trim());
9089
+ const targetLines = tail ? lines.slice(-tail) : lines;
9090
+ let lastAssistantText;
9091
+ let lastStopReason;
9092
+ let toolUses = 0;
9093
+ for (const line of targetLines) {
9094
+ try {
9095
+ const ev = JSON.parse(line);
9096
+ if (ev.type === "assistant" && typeof ev.text === "string") {
9097
+ lastAssistantText = ev.text;
9098
+ } else if (ev.type === "stop" && ev.stopReason) {
9099
+ lastStopReason = ev.stopReason;
9100
+ } else if (ev.type === "tool_use") {
9101
+ toolUses++;
9102
+ }
9103
+ } catch {
9104
+ }
9105
+ }
9106
+ return {
9107
+ lastAssistantText,
9108
+ lastStopReason,
9109
+ toolUsesObserved: toolUses,
9110
+ events: targetLines.length,
9111
+ path: filePath
9112
+ };
9113
+ }
8613
9114
  snapshot() {
8614
9115
  return this.usage.snapshot();
8615
9116
  }
@@ -8688,10 +9189,28 @@ var Director = class {
8688
9189
  makeRollUpTool(this),
8689
9190
  makeTerminateTool(this),
8690
9191
  makeFleetStatusTool(this),
8691
- makeFleetUsageTool(this)
9192
+ makeFleetUsageTool(this),
9193
+ makeFleetSessionTool(this),
9194
+ makeFleetHealthTool(this)
8692
9195
  ];
8693
9196
  return t2;
8694
9197
  }
9198
+ /**
9199
+ * Attempt to acquire the checkpoint lock. Must be called before
9200
+ * resuming — if another director process is alive, this returns
9201
+ * false and the caller should not proceed with the resume.
9202
+ */
9203
+ async acquireCheckpointLock() {
9204
+ return this.stateCheckpoint ? this.stateCheckpoint.acquireLock() : true;
9205
+ }
9206
+ /**
9207
+ * Resume from a prior checkpoint snapshot (loaded via
9208
+ * `loadDirectorState()`). Re-attach to the fleet mid-flight so
9209
+ * subsequent spawn/assign calls update the checkpoint normally.
9210
+ */
9211
+ resumeFromCheckpoint(snapshot) {
9212
+ this.stateCheckpoint?.resume(snapshot);
9213
+ }
8695
9214
  };
8696
9215
  function createDelegateTool(opts) {
8697
9216
  const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
@@ -8797,7 +9316,7 @@ function createDelegateTool(opts) {
8797
9316
  if (typeof i.maxToolCalls === "number" && i.maxToolCalls > 0) {
8798
9317
  cfg.maxToolCalls = i.maxToolCalls;
8799
9318
  }
8800
- const SUBAGENT_TIMEOUT_BUFFER_MS = 3e4;
9319
+ const SUBAGENT_TIMEOUT_BUFFER_MS = opts.subagentTimeoutBufferMs ?? 3e4;
8801
9320
  const desiredSubTimeout = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
8802
9321
  if (!cfg.timeoutMs || cfg.timeoutMs > desiredSubTimeout) {
8803
9322
  cfg.timeoutMs = desiredSubTimeout;
@@ -8849,7 +9368,7 @@ function createDelegateTool(opts) {
8849
9368
  toolCalls: result.toolCalls,
8850
9369
  durationMs: result.durationMs,
8851
9370
  ...partial ? { partial } : {},
8852
- ...hintForKind(errorKind, retryable, backoffMs) ? { hint: hintForKind(errorKind, retryable, backoffMs) } : {}
9371
+ ...hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {}
8853
9372
  };
8854
9373
  } catch (err) {
8855
9374
  return {
@@ -8869,7 +9388,7 @@ function instantiateRosterConfig2(role, base) {
8869
9388
  id: `${role}-${randomUUID().slice(0, 8)}`
8870
9389
  };
8871
9390
  }
8872
- function hintForKind(kind, retryable, backoffMs) {
9391
+ function hintForKind(kind, retryable, backoffMs, partial) {
8873
9392
  if (!kind) return void 0;
8874
9393
  switch (kind) {
8875
9394
  case "provider_rate_limit":
@@ -8885,16 +9404,40 @@ function hintForKind(kind, retryable, backoffMs) {
8885
9404
  case "budget_iterations":
8886
9405
  case "budget_tool_calls":
8887
9406
  case "budget_tokens":
8888
- case "budget_cost":
8889
- return "Subagent exhausted its budget. The coordinator may auto-extend; otherwise raise the matching `max*` field (e.g. maxToolCalls: 600) on the next delegate, or split the task.";
8890
- case "budget_timeout":
8891
- return "Subagent hit its wall-clock budget. Raise `timeoutMs` on the next delegate or split the task.";
9407
+ case "budget_cost": {
9408
+ const base = "Subagent exhausted its budget. The coordinator may auto-extend; otherwise raise the matching `max*` field (e.g. maxToolCalls: 600) on the next delegate, or split the task.";
9409
+ if (partial?.lastAssistantText) {
9410
+ return `${base}
9411
+
9412
+ Partial output produced before budget hit:
9413
+ ${partial.lastAssistantText}`;
9414
+ }
9415
+ return base;
9416
+ }
9417
+ case "budget_timeout": {
9418
+ const base = "Subagent hit its wall-clock budget. Raise `timeoutMs` on the next delegate or split the task.";
9419
+ if (partial?.lastAssistantText) {
9420
+ return `${base}
9421
+
9422
+ Partial output produced before timeout:
9423
+ ${partial.lastAssistantText}`;
9424
+ }
9425
+ return base;
9426
+ }
8892
9427
  case "aborted_by_parent":
8893
9428
  return "Subagent was aborted (user Ctrl+C, parent unwound, or sibling failure cascade). Not retryable until the abort condition is resolved.";
8894
9429
  case "empty_response":
8895
9430
  return "Subagent ended its turn with no text and no tool calls. Almost always a prompt / config issue \u2014 clarify the task or check the model.";
8896
- case "tool_failed":
8897
- return "A tool inside the subagent returned ok:false. Inspect `partial.lastAssistantText` for the agent reasoning, then retry with corrected inputs.";
9431
+ case "tool_failed": {
9432
+ const base = "A tool inside the subagent returned ok:false. Retry with corrected inputs.";
9433
+ if (partial?.lastAssistantText) {
9434
+ return `${base}
9435
+
9436
+ Agent reasoning before failure:
9437
+ ${partial.lastAssistantText}`;
9438
+ }
9439
+ return base;
9440
+ }
8898
9441
  case "bridge_failed":
8899
9442
  return "Parent-child bridge transport failed. This is rare \u2014 restart the session and retry.";
8900
9443
  default:
@@ -9179,8 +9722,8 @@ async function loadProjectModes(modesDir) {
9179
9722
  for (const entry of entries) {
9180
9723
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
9181
9724
  const filePath = path6.join(modesDir, entry);
9182
- const stat6 = await fsp2.stat(filePath);
9183
- if (!stat6.isFile()) continue;
9725
+ const stat8 = await fsp2.stat(filePath);
9726
+ if (!stat8.isFile()) continue;
9184
9727
  const content = await fsp2.readFile(filePath, "utf8");
9185
9728
  const id = path6.basename(entry, path6.extname(entry));
9186
9729
  modes.push({
@@ -12762,10 +13305,10 @@ var SkillInstaller = class {
12762
13305
  if (!resolved.startsWith(path6.resolve(destDir))) {
12763
13306
  throw new Error(`Path traversal detected in skill file: ${file}`);
12764
13307
  }
12765
- const stat6 = await fsp2.stat(srcPath);
12766
- if (stat6.size > MAX_SKILL_FILE_SIZE) {
13308
+ const stat8 = await fsp2.stat(srcPath);
13309
+ if (stat8.size > MAX_SKILL_FILE_SIZE) {
12767
13310
  throw new Error(
12768
- `Skill file "${file}" is too large (${(stat6.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
13311
+ `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
12769
13312
  );
12770
13313
  }
12771
13314
  await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
@@ -13161,6 +13704,1841 @@ async function revertSnapshots(snapshots) {
13161
13704
  }
13162
13705
  return { revertedFiles, errors };
13163
13706
  }
13707
+ var MATCHERS = {
13708
+ pnpmWorkspace: (files) => files.includes("pnpm-workspace.yaml"),
13709
+ gradlew: (_files, dirs) => dirs.includes("gradlew"),
13710
+ mavenWrapper: (_files, dirs) => dirs.includes(".mvn"),
13711
+ dotnetSdk: (files) => files.some((f) => f.endsWith(".csproj") || f.endsWith(".fsproj")),
13712
+ yarnConfig: (files) => files.includes(".yarnrc") || files.includes("yarn.config.js"),
13713
+ notPoetryLock: (files) => !files.includes("poetry.lock")
13714
+ };
13715
+ var STACK_SIGNATURES = [
13716
+ // Node.js variants - checked in order, first match wins
13717
+ {
13718
+ stack: "nodejs",
13719
+ packageManager: "pnpm",
13720
+ manifestFiles: ["package.json"],
13721
+ lockFiles: ["pnpm-lock.yaml"],
13722
+ secondarySignatures: (f) => MATCHERS.pnpmWorkspace(f)
13723
+ },
13724
+ {
13725
+ stack: "nodejs",
13726
+ packageManager: "bun",
13727
+ manifestFiles: ["package.json"],
13728
+ lockFiles: ["bun.lockb"]
13729
+ },
13730
+ {
13731
+ stack: "nodejs",
13732
+ packageManager: "yarn",
13733
+ manifestFiles: ["package.json"],
13734
+ lockFiles: ["yarn.lock"],
13735
+ secondarySignatures: (f) => MATCHERS.yarnConfig(f)
13736
+ },
13737
+ {
13738
+ stack: "nodejs",
13739
+ packageManager: "npm",
13740
+ manifestFiles: ["package.json"],
13741
+ lockFiles: ["package-lock.json"]
13742
+ },
13743
+ // Python variants
13744
+ {
13745
+ stack: "python",
13746
+ packageManager: "poetry",
13747
+ manifestFiles: ["pyproject.toml"],
13748
+ lockFiles: ["poetry.lock"]
13749
+ },
13750
+ {
13751
+ stack: "python",
13752
+ packageManager: "pip",
13753
+ manifestFiles: ["requirements.txt", "setup.py"],
13754
+ lockFiles: ["requirements.txt"]
13755
+ },
13756
+ {
13757
+ stack: "python",
13758
+ packageManager: "pip",
13759
+ manifestFiles: ["pyproject.toml"],
13760
+ lockFiles: [],
13761
+ secondarySignatures: (f) => MATCHERS.notPoetryLock(f)
13762
+ },
13763
+ // Rust
13764
+ {
13765
+ stack: "rust",
13766
+ packageManager: "cargo",
13767
+ manifestFiles: ["Cargo.toml"],
13768
+ lockFiles: ["Cargo.lock"]
13769
+ },
13770
+ // Go
13771
+ {
13772
+ stack: "go",
13773
+ packageManager: "go",
13774
+ manifestFiles: ["go.mod"],
13775
+ lockFiles: ["go.sum"]
13776
+ },
13777
+ // Java variants
13778
+ {
13779
+ stack: "java",
13780
+ packageManager: "gradle",
13781
+ manifestFiles: ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
13782
+ lockFiles: [],
13783
+ secondarySignatures: (_f, d) => MATCHERS.gradlew(_f, d)
13784
+ },
13785
+ {
13786
+ stack: "java",
13787
+ packageManager: "maven",
13788
+ manifestFiles: ["pom.xml"],
13789
+ lockFiles: [],
13790
+ secondarySignatures: (_f, d) => MATCHERS.mavenWrapper(_f, d)
13791
+ },
13792
+ // .NET
13793
+ {
13794
+ stack: "dotnet",
13795
+ packageManager: "nuget",
13796
+ manifestFiles: ["Directory.Build.props", "Directory.Packages.props"],
13797
+ lockFiles: ["packages.lock.json"],
13798
+ secondarySignatures: (f) => MATCHERS.dotnetSdk(f)
13799
+ },
13800
+ {
13801
+ stack: "dotnet",
13802
+ packageManager: "nuget",
13803
+ manifestFiles: ["*.csproj", "*.fsproj", "*.xproj"],
13804
+ lockFiles: [],
13805
+ secondarySignatures: (f) => MATCHERS.dotnetSdk(f)
13806
+ },
13807
+ // PHP
13808
+ {
13809
+ stack: "php",
13810
+ packageManager: "composer",
13811
+ manifestFiles: ["composer.json"],
13812
+ lockFiles: ["composer.lock"]
13813
+ },
13814
+ // Ruby
13815
+ {
13816
+ stack: "ruby",
13817
+ packageManager: "bundler",
13818
+ manifestFiles: ["Gemfile"],
13819
+ lockFiles: ["Gemfile.lock"]
13820
+ },
13821
+ // C++
13822
+ {
13823
+ stack: "cpp",
13824
+ packageManager: "cmake",
13825
+ manifestFiles: ["CMakeLists.txt"],
13826
+ lockFiles: [],
13827
+ secondarySignatures: (f) => f.includes("CMakeCache.txt") || f.includes("CMakePresets.json")
13828
+ },
13829
+ // Swift
13830
+ {
13831
+ stack: "swift",
13832
+ packageManager: "swiftpm",
13833
+ manifestFiles: ["Package.swift"],
13834
+ lockFiles: []
13835
+ }
13836
+ ];
13837
+ var MONOREPO_INDICATORS = {
13838
+ pnpm: ["pnpm-workspace.yaml"],
13839
+ npm: ["lerna.json", "nx.json"],
13840
+ yarn: [],
13841
+ bun: [],
13842
+ cargo: [],
13843
+ go: ["go.work"],
13844
+ maven: [],
13845
+ gradle: [],
13846
+ nuget: ["Directory.Build.props"],
13847
+ pip: [],
13848
+ poetry: [],
13849
+ bundler: [],
13850
+ cmake: [],
13851
+ swiftpm: [],
13852
+ unknown: []
13853
+ };
13854
+ var TechStackDetector = class {
13855
+ cachedResults = /* @__PURE__ */ new Map();
13856
+ async detect(projectRoot) {
13857
+ const cached = this.cachedResults.get(projectRoot);
13858
+ if (cached) return cached;
13859
+ const result = {
13860
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
13861
+ projectRoot,
13862
+ detectedStacks: [],
13863
+ isMonorepo: false,
13864
+ workspaceConfigs: []
13865
+ };
13866
+ const entries = await readdir(projectRoot, { withFileTypes: true });
13867
+ const files = entries.filter((e) => e.isFile()).map((e) => e.name);
13868
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
13869
+ const detectedStacks = /* @__PURE__ */ new Set();
13870
+ for (const signature of STACK_SIGNATURES) {
13871
+ const detected = this.matchSignature(signature, files, dirs);
13872
+ if (detected) {
13873
+ if (detectedStacks.has(signature.stack)) continue;
13874
+ detectedStacks.add(signature.stack);
13875
+ result.detectedStacks.push(detected);
13876
+ }
13877
+ }
13878
+ result.isMonorepo = this.detectMonorepo(result.detectedStacks, dirs, files);
13879
+ if (result.isMonorepo) {
13880
+ result.workspaceConfigs = this.findWorkspaceConfigs(result.detectedStacks, dirs, files);
13881
+ }
13882
+ this.cachedResults.set(projectRoot, result);
13883
+ return result;
13884
+ }
13885
+ matchSignature(signature, files, _dirs) {
13886
+ const manifestMatch = this.findMatchingManifest(signature.manifestFiles, files);
13887
+ if (!manifestMatch) return null;
13888
+ if (signature.lockFiles.length > 0 && signature.stack === "nodejs") {
13889
+ const hasLockFile = signature.lockFiles.some((lock) => {
13890
+ if (lock.includes("*")) {
13891
+ const regex = new RegExp("^" + lock.replace("*", ".*") + "$");
13892
+ return files.some((f) => regex.test(f));
13893
+ }
13894
+ return files.includes(lock);
13895
+ });
13896
+ if (!hasLockFile && signature.packageManager !== "npm") {
13897
+ return null;
13898
+ }
13899
+ }
13900
+ return {
13901
+ stack: signature.stack,
13902
+ packageManager: signature.packageManager,
13903
+ manifestFile: manifestMatch,
13904
+ dependencies: [],
13905
+ projectPath: ""
13906
+ };
13907
+ }
13908
+ findMatchingManifest(manifests, files) {
13909
+ for (const manifest of manifests) {
13910
+ if (manifest.includes("*")) {
13911
+ const regex = new RegExp("^" + manifest.replace("*", ".*") + "$");
13912
+ const match = files.find((f) => regex.test(f));
13913
+ if (match) return match;
13914
+ } else if (files.includes(manifest)) {
13915
+ return manifest;
13916
+ }
13917
+ }
13918
+ return null;
13919
+ }
13920
+ detectMonorepo(stacks, dirs, files) {
13921
+ if (stacks.length > 1) return true;
13922
+ if (stacks.some((s) => s.packageManager === "pnpm") && files.includes("pnpm-workspace.yaml")) {
13923
+ return true;
13924
+ }
13925
+ for (const stack of stacks) {
13926
+ const indicators = MONOREPO_INDICATORS[stack.packageManager];
13927
+ if (indicators && indicators.some((ind) => files.includes(ind) || dirs.includes(ind))) {
13928
+ return true;
13929
+ }
13930
+ }
13931
+ return false;
13932
+ }
13933
+ findWorkspaceConfigs(stacks, dirs, files) {
13934
+ const configs = [];
13935
+ for (const stack of stacks) {
13936
+ const indicators = MONOREPO_INDICATORS[stack.packageManager];
13937
+ if (indicators) {
13938
+ configs.push(...indicators.filter((ind) => files.includes(ind) || dirs.includes(ind)));
13939
+ }
13940
+ }
13941
+ return [...new Set(configs)];
13942
+ }
13943
+ clearCache() {
13944
+ this.cachedResults.clear();
13945
+ }
13946
+ };
13947
+ var defaultTechStackDetector = new TechStackDetector();
13948
+
13949
+ // src/security-scanner/skill-generator.ts
13950
+ var DEFAULT_OPTIONS = {
13951
+ includeSecrets: true,
13952
+ includeInjection: true,
13953
+ includeConfig: true,
13954
+ includeDependencies: true,
13955
+ severityThreshold: "medium"
13956
+ };
13957
+ var SkillGenerator = class {
13958
+ options;
13959
+ constructor(options = {}) {
13960
+ this.options = { ...DEFAULT_OPTIONS, ...options };
13961
+ }
13962
+ generate(techStack) {
13963
+ const patterns = this.getPatternsForStack(techStack.stack);
13964
+ const content = this.buildSkillContent(techStack, patterns);
13965
+ const targetFiles = this.getTargetFilesForStack(techStack);
13966
+ return {
13967
+ name: `security-scanner-${techStack.stack}`,
13968
+ description: `Security scanner for ${techStack.stack} projects`,
13969
+ version: "1.0.0",
13970
+ techStack: techStack.stack,
13971
+ content,
13972
+ patterns,
13973
+ metadata: {
13974
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13975
+ confidence: this.calculateConfidence(techStack),
13976
+ targetFiles
13977
+ }
13978
+ };
13979
+ }
13980
+ getPatternsForStack(stack) {
13981
+ const patterns = [];
13982
+ const severityOrder = ["critical", "high", "medium", "low"];
13983
+ const minIndex = severityOrder.indexOf(this.options.severityThreshold);
13984
+ if (this.options.includeSecrets) {
13985
+ const secretPatterns = this.getSecretPatterns(stack);
13986
+ patterns.push(...secretPatterns.filter((p) => severityOrder.indexOf(p.severity) <= minIndex));
13987
+ }
13988
+ if (this.options.includeInjection) {
13989
+ const injectionPatterns = this.getInjectionPatterns(stack);
13990
+ patterns.push(...injectionPatterns.filter((p) => severityOrder.indexOf(p.severity) <= minIndex));
13991
+ }
13992
+ if (this.options.includeConfig) {
13993
+ const configPatterns = this.getConfigPatterns(stack);
13994
+ patterns.push(...configPatterns.filter((p) => severityOrder.indexOf(p.severity) <= minIndex));
13995
+ }
13996
+ return patterns;
13997
+ }
13998
+ getSecretPatterns(stack) {
13999
+ const commonSecrets = {
14000
+ id: "hardcoded-secrets",
14001
+ name: "Hardcoded Secrets",
14002
+ severity: "critical",
14003
+ description: "Detects hardcoded API keys, tokens, passwords, and private keys",
14004
+ patterns: [
14005
+ /(?:api[_-]?key|apikey|api[_-]?secret)[^\w]*[=:]\s*["']([a-zA-Z0-9_-]{20,})["']/gi,
14006
+ /(?:password|passwd|pwd)[^\w]*[=:]\s*["'][^"']{6,64}["']/gi,
14007
+ /(?:secret|token|auth)[^\w]*[=:]\s*["'][a-zA-Z0-9_/-]{20,}["']/gi,
14008
+ /-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH)?\s+PRIVATE\s+KEY-----/g
14009
+ ],
14010
+ fileExtensions: [".ts", ".js", ".py", ".go", ".java", ".rb", ".php", ".env"],
14011
+ falsePositiveMarkers: ["process.env", "process.argv", "process.env.NODE_ENV"],
14012
+ remediation: "Use environment variables or a secrets manager. Never commit secrets to version control."
14013
+ };
14014
+ const stackSpecific = {
14015
+ nodejs: [
14016
+ {
14017
+ id: "npmrc-credentials",
14018
+ name: "npmrc Credentials",
14019
+ severity: "high",
14020
+ description: "Detects .npmrc authentication tokens",
14021
+ patterns: [
14022
+ // eslint-disable-next-line no-control-regex
14023
+ /_auth\s*=\s*[a-zA-Z0-9+/]{20,}/g,
14024
+ /registry\s*=\s*https:\/\/[a-zA-Z0-9.-]+\/auth/g
14025
+ ],
14026
+ fileExtensions: [".npmrc"],
14027
+ falsePositiveMarkers: [],
14028
+ remediation: "Use npm token from environment variable, not hardcoded in .npmrc"
14029
+ }
14030
+ ],
14031
+ python: [
14032
+ {
14033
+ id: "python-secret-env",
14034
+ name: "Python Environment Secrets",
14035
+ severity: "high",
14036
+ description: "Detects secrets loaded via os.environ in Python",
14037
+ patterns: [
14038
+ /os\.environ\[['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g,
14039
+ /getenv\(['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g
14040
+ ],
14041
+ fileExtensions: [".py"],
14042
+ falsePositiveMarkers: ["os.environ.get", "getenv(", "os.getenv"],
14043
+ remediation: "Use python-dotenv or environment variable injection at runtime"
14044
+ }
14045
+ ],
14046
+ rust: [
14047
+ {
14048
+ id: "rust-env-secrets",
14049
+ name: "Rust Environment Secrets",
14050
+ severity: "high",
14051
+ description: "Detects env! macro with hardcoded secrets",
14052
+ patterns: [/env!\s*\[['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g],
14053
+ fileExtensions: [".rs"],
14054
+ falsePositiveMarkers: [],
14055
+ remediation: "Use std::env::var and handle MissingKeyError properly"
14056
+ }
14057
+ ],
14058
+ go: [
14059
+ {
14060
+ id: "go-hardcoded-secret",
14061
+ name: "Go Hardcoded Secrets",
14062
+ severity: "critical",
14063
+ description: "Detects hardcoded secrets in Go source",
14064
+ patterns: [
14065
+ /os\.Getenv\(['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g,
14066
+ /"[a-zA-Z0-9/+=]{40,}"\s*(?:&&|\|\|)\s*err\s*!=\s*nil/g
14067
+ ],
14068
+ fileExtensions: [".go"],
14069
+ falsePositiveMarkers: ["os.Getenv", "os.LookupEnv"],
14070
+ remediation: "Use viper or os.Getenv with validation"
14071
+ }
14072
+ ],
14073
+ java: [
14074
+ {
14075
+ id: "java-system-getenv",
14076
+ name: "Java System.getenv",
14077
+ severity: "medium",
14078
+ description: "Detects System.getenv() usage which may expose secrets",
14079
+ patterns: [/System\.getenv\(['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g],
14080
+ fileExtensions: [".java"],
14081
+ falsePositiveMarkers: [],
14082
+ remediation: "Use a secrets manager or environment-specific config"
14083
+ }
14084
+ ],
14085
+ dotnet: [
14086
+ {
14087
+ id: "dotnet-config-secrets",
14088
+ name: ".NET Configuration Secrets",
14089
+ severity: "high",
14090
+ description: "Detects secrets in .NET Configuration managers",
14091
+ patterns: [
14092
+ /ConfigurationManager\.ConnectionStrings/g,
14093
+ /ConfigurationManager\.AppSettings/g
14094
+ ],
14095
+ fileExtensions: [".cs", ".config"],
14096
+ falsePositiveMarkers: [],
14097
+ remediation: "Use Azure Key Vault or user secrets during development"
14098
+ }
14099
+ ]
14100
+ };
14101
+ return [commonSecrets, ...stackSpecific[stack] ?? []];
14102
+ }
14103
+ getInjectionPatterns(stack) {
14104
+ const commonInjection = {
14105
+ id: "command-injection",
14106
+ name: "Command Injection",
14107
+ severity: "critical",
14108
+ description: "Detects shell command injection vulnerabilities",
14109
+ patterns: [
14110
+ /exec\s*\(\s*[`'"].*\$/g,
14111
+ /system\s*\(\s*[`'"].*\$/g,
14112
+ /shell_exec\s*\(\s*[`'"].*\$/g,
14113
+ /exec\s*\(\s*\$/g
14114
+ ],
14115
+ fileExtensions: [".ts", ".js", ".php", ".py", ".rb"],
14116
+ falsePositiveMarkers: ["escapeshellarg", "escapeshellcmd", "sanitize"],
14117
+ remediation: "Use parameterized commands with argument arrays instead of string interpolation."
14118
+ };
14119
+ const stackSpecific = {
14120
+ nodejs: [
14121
+ {
14122
+ id: "eval-injection",
14123
+ name: "Eval Injection",
14124
+ severity: "critical",
14125
+ description: "Detects dangerous eval/Function usage with user input",
14126
+ patterns: [
14127
+ /eval\s*\(\s*(?:req|body|input|params|query)/g,
14128
+ /new\s+Function\s*\(\s*(?:req|body|input|params|query)/g
14129
+ ],
14130
+ fileExtensions: [".ts", ".js"],
14131
+ falsePositiveMarkers: ["JSON.parse"],
14132
+ remediation: "Never eval user input. Use JSON.parse for data, or proper sandboxing."
14133
+ },
14134
+ {
14135
+ id: "sql-injection-template",
14136
+ name: "SQL Injection via Template",
14137
+ severity: "critical",
14138
+ description: "Detects SQL queries built with string concatenation",
14139
+ patterns: [
14140
+ /(?:query|execute|select)\s*\(\s*[`'"].*\+.*(?:req|body|params|query)/gi,
14141
+ /\.query\s*\(\s*`.*\$\{/g
14142
+ ],
14143
+ fileExtensions: [".ts", ".js"],
14144
+ falsePositiveMarkers: ["parameterized", "prepared", "bind"],
14145
+ remediation: "Use parameterized queries or an ORM with proper query building."
14146
+ },
14147
+ {
14148
+ id: "nosql-injection",
14149
+ name: "NoSQL Injection",
14150
+ severity: "high",
14151
+ description: "Detects NoSQL query injection via user input",
14152
+ patterns: [
14153
+ /find\s*\(\s*\{.*\$where/g,
14154
+ /collection\.(?:find|aggregate)\s*\([^)]*\$/g
14155
+ ],
14156
+ fileExtensions: [".ts", ".js"],
14157
+ falsePositiveMarkers: [],
14158
+ remediation: "Sanitize and validate all user input before NoSQL queries."
14159
+ }
14160
+ ],
14161
+ python: [
14162
+ {
14163
+ id: "python-sql-injection",
14164
+ name: "Python SQL Injection",
14165
+ severity: "critical",
14166
+ description: "Detects SQL queries built with string formatting",
14167
+ patterns: [
14168
+ /execute\s*\(\s*f?["'].*%.*/g,
14169
+ /cursor\.execute\s*\([^)]*\+[^)]*\)/g
14170
+ ],
14171
+ fileExtensions: [".py"],
14172
+ falsePositiveMarkers: ["%s", "%d", "?", "parameterized"],
14173
+ remediation: "Use parameterized queries with cursor.execute(query, params)."
14174
+ },
14175
+ {
14176
+ id: "pickle-deserialization",
14177
+ name: "Pickle Deserialization",
14178
+ severity: "critical",
14179
+ description: "Detects insecure pickle deserialization",
14180
+ patterns: [
14181
+ /pickle\.load\s*\(/g,
14182
+ /pickle\.loads\s*\(/g,
14183
+ /unpickle\.load\s*\(/g
14184
+ ],
14185
+ fileExtensions: [".py"],
14186
+ falsePositiveMarkers: [],
14187
+ remediation: "Never unpickle data from untrusted sources. Use JSON or custom serialization."
14188
+ }
14189
+ ],
14190
+ go: [
14191
+ {
14192
+ id: "go-sql-injection",
14193
+ name: "Go SQL Injection",
14194
+ severity: "critical",
14195
+ description: "Detects SQL queries with string concatenation",
14196
+ patterns: [
14197
+ /db\.Query\s*\([^)]*\+[^)]*\)/g,
14198
+ /QueryContext?\s*\([^)]*\+[^)]*\)/g
14199
+ ],
14200
+ fileExtensions: [".go"],
14201
+ falsePositiveMarkers: ["$1", "$2", "?", "params"],
14202
+ remediation: 'Use parameterized queries: db.QueryContext(ctx, "SELECT * FROM users WHERE id=?", userID)'
14203
+ }
14204
+ ],
14205
+ java: [
14206
+ {
14207
+ id: "java-sql-injection",
14208
+ name: "Java SQL Injection",
14209
+ severity: "critical",
14210
+ description: "Detects SQL with string concatenation in JDBC",
14211
+ patterns: [
14212
+ /createStatement\s*\(\s*\).*\.executeQuery\s*\([^)]*\+/g,
14213
+ /Statement\s*\([^)]*\+/g
14214
+ ],
14215
+ fileExtensions: [".java"],
14216
+ falsePositiveMarkers: ["PreparedStatement", "?"],
14217
+ remediation: "Use PreparedStatement with parameters."
14218
+ }
14219
+ ],
14220
+ rust: [
14221
+ {
14222
+ id: "rust-command-injection",
14223
+ name: "Rust Command Injection",
14224
+ severity: "critical",
14225
+ description: "Detects Command::new with string interpolation",
14226
+ patterns: [
14227
+ /Command::new\s*\([^)]*\)\s*\.(?:arg|args)\s*\([^)]*\+/g,
14228
+ /Command::from\s*\(/g
14229
+ ],
14230
+ fileExtensions: [".rs"],
14231
+ falsePositiveMarkers: ["Command::new", "args\\("],
14232
+ remediation: "Use Command::new(array).args(&[...]) to avoid shell injection."
14233
+ }
14234
+ ],
14235
+ dotnet: [
14236
+ {
14237
+ id: "csharp-sql-injection",
14238
+ name: "C# SQL Injection",
14239
+ severity: "critical",
14240
+ description: "Detects SQL with string concatenation in C#",
14241
+ patterns: [
14242
+ /SqlCommand\s*\([^)]*\+[^)]*\)/g,
14243
+ /\.ExecuteQuery\s*\([^)]*\+[^)]*\)/g
14244
+ ],
14245
+ fileExtensions: [".cs"],
14246
+ falsePositiveMarkers: ["parameters.Add", "@", "SqlParameter"],
14247
+ remediation: "Use parameterized queries with SqlParameter."
14248
+ }
14249
+ ]
14250
+ };
14251
+ return [commonInjection, ...stackSpecific[stack] ?? []];
14252
+ }
14253
+ getConfigPatterns(stack) {
14254
+ const commonConfig = [
14255
+ {
14256
+ id: "insecure-tls",
14257
+ name: "Insecure TLS Configuration",
14258
+ severity: "high",
14259
+ description: "Detects disabled TLS verification or weak TLS settings",
14260
+ patterns: [
14261
+ /rejectUnauthorized\s*[:=]\s*false/g,
14262
+ /secure\s*[:=]\s*false/g,
14263
+ /ssl\s*[:=]\s*false/g,
14264
+ /TLS\s*\[\s*['"]?1\.0['"]?\]/gi,
14265
+ /InsecureRequestWarning\.disable/g
14266
+ ],
14267
+ fileExtensions: [".ts", ".js", ".py", ".go", ".java"],
14268
+ falsePositiveMarkers: ["NODE_TLS_REJECT_UNAUTHORIZED"],
14269
+ remediation: "Always verify TLS certificates in production. Use proper certificate stores."
14270
+ },
14271
+ {
14272
+ id: "debug-enabled",
14273
+ name: "Debug Mode Enabled",
14274
+ severity: "medium",
14275
+ description: "Detects debug flags that may expose sensitive information",
14276
+ patterns: [
14277
+ /debug\s*[:=]\s*true/g,
14278
+ /DEBUG\s*[:=]\s*true/g,
14279
+ /development\s*mode/g
14280
+ ],
14281
+ fileExtensions: [".ts", ".js", ".py", ".env", ".json"],
14282
+ falsePositiveMarkers: ['process.env.NODE_ENV !== "production"', "if (process.env.DEBUG)"],
14283
+ remediation: "Disable debug mode in production. Use proper log levels."
14284
+ }
14285
+ ];
14286
+ return commonConfig;
14287
+ }
14288
+ getTargetFilesForStack(techStack) {
14289
+ const filesByStack = {
14290
+ nodejs: [
14291
+ "**/*.ts",
14292
+ "**/*.js",
14293
+ "**/*.json",
14294
+ "**/.env*",
14295
+ "**/package.json",
14296
+ "**/tsconfig.json"
14297
+ ],
14298
+ python: [
14299
+ "**/*.py",
14300
+ "**/requirements*.txt",
14301
+ "**/setup.py",
14302
+ "**/pyproject.toml",
14303
+ "**/.env*"
14304
+ ],
14305
+ rust: ["**/*.rs", "**/Cargo.toml", "**/Cargo.lock"],
14306
+ go: ["**/*.go", "**/go.mod", "**/go.sum"],
14307
+ java: ["**/*.java", "**/pom.xml", "**/build.gradle", "**/*.properties"],
14308
+ dotnet: ["**/*.cs", "**/*.csproj", "**/*.config", "**/appsettings.json"],
14309
+ php: ["**/*.php", "**/.env*", "**/composer.json"],
14310
+ ruby: ["**/*.rb", "**/Gemfile", "**/.env*"],
14311
+ cpp: ["**/*.cpp", "**/*.hpp", "**/CMakeLists.txt"],
14312
+ c: ["**/*.c", "**/*.h"],
14313
+ kotlin: ["**/*.kt", "**/*.kts", "**/build.gradle.kts"],
14314
+ swift: ["**/*.swift", "**/Package.swift"],
14315
+ unknown: ["**/*"]
14316
+ };
14317
+ return filesByStack[techStack.stack] || filesByStack.unknown;
14318
+ }
14319
+ buildSkillContent(techStack, patterns) {
14320
+ const lines = [
14321
+ "---",
14322
+ `name: security-scanner-${techStack.stack}`,
14323
+ `description: |`,
14324
+ ` Auto-generated security scanner for ${techStack.stack} projects.`,
14325
+ ` Scans for secrets, injection vectors, and configuration issues.`,
14326
+ `version: 1.0.0`,
14327
+ "---",
14328
+ "",
14329
+ `# Security Scanner \u2014 ${techStack.stack.toUpperCase()}`,
14330
+ "",
14331
+ `Scans ${techStack.stack} codebase for security vulnerabilities.`,
14332
+ "",
14333
+ "## Scan Targets",
14334
+ "",
14335
+ "### Code Vulnerabilities",
14336
+ patterns.filter((p) => p.fileExtensions.some((ext) => [".ts", ".js", ".py", ".go", ".java", ".cs", ".rs"].includes(ext))).map((p) => `- **${p.name}** (${p.severity}): ${p.description}`).join("\n"),
14337
+ "",
14338
+ "### Configuration Issues",
14339
+ patterns.filter((p) => p.fileExtensions.some((ext) => [".json", ".yaml", ".yml", ".env", ".config"].includes(ext))).map((p) => `- **${p.name}** (${p.severity}): ${p.description}`).join("\n"),
14340
+ "",
14341
+ "## Severity Levels",
14342
+ "",
14343
+ "- **CRITICAL**: Remote code execution, SQL injection, hardcoded secrets",
14344
+ "- **HIGH**: Command injection, XXE, authentication bypass",
14345
+ "- **MEDIUM**: Information disclosure, weak crypto, debug mode",
14346
+ "- **LOW**: Code quality issues, missing headers",
14347
+ "",
14348
+ "## Report Format",
14349
+ "",
14350
+ "```",
14351
+ "## Security Scan Report",
14352
+ "",
14353
+ "### CRITICAL",
14354
+ "1. **[CRITICAL]** `file:line` \u2014 Description",
14355
+ " ```",
14356
+ " // vulnerable code",
14357
+ " ```",
14358
+ " **Remediation**: Fix description",
14359
+ "",
14360
+ "### Summary",
14361
+ "| Severity | Count |",
14362
+ "|----------|-------|",
14363
+ "| Critical | X |",
14364
+ "| High | X |",
14365
+ "| Medium | X |",
14366
+ "| Low | X |",
14367
+ "```",
14368
+ "",
14369
+ "## Remediation",
14370
+ "",
14371
+ patterns.map((p) => `- **${p.name}**: ${p.remediation}`).join("\n")
14372
+ ];
14373
+ return {
14374
+ type: "skill",
14375
+ content: lines.join("\n")
14376
+ };
14377
+ }
14378
+ calculateConfidence(techStack) {
14379
+ let confidence = 0.7;
14380
+ if (techStack.dependencies.length > 0) confidence += 0.1;
14381
+ if (techStack.manifestFile) confidence += 0.1;
14382
+ if (techStack.packageManager !== "unknown") confidence += 0.1;
14383
+ return Math.min(confidence, 1);
14384
+ }
14385
+ };
14386
+ var defaultSkillGenerator = new SkillGenerator();
14387
+ var DEFAULT_SCAN_OPTIONS = {
14388
+ includeSecrets: true,
14389
+ includeInjection: true,
14390
+ includeConfig: true,
14391
+ includeDependencies: true,
14392
+ excludePaths: ["node_modules", "dist", ".git", "coverage", "build", "target"],
14393
+ fileExtensions: [],
14394
+ depth: "standard"
14395
+ };
14396
+ var SecurityScanner = class {
14397
+ options;
14398
+ constructor(options = {}) {
14399
+ this.options = { ...DEFAULT_SCAN_OPTIONS, ...options };
14400
+ }
14401
+ async scan(projectRoot, skill, techStack) {
14402
+ const startTime = Date.now();
14403
+ const findings = [];
14404
+ const errors = [];
14405
+ let scannedFiles = 0;
14406
+ const targetExtensions = this.getTargetExtensions(skill, techStack);
14407
+ const files = await this.gatherFiles(projectRoot, targetExtensions);
14408
+ for (const file of files) {
14409
+ try {
14410
+ const content = await readFile(file, "utf-8");
14411
+ const fileFindings = this.scanFile(content, file, skill.patterns);
14412
+ findings.push(...fileFindings);
14413
+ scannedFiles++;
14414
+ } catch (err) {
14415
+ errors.push(`Failed to read ${file}: ${err}`);
14416
+ }
14417
+ }
14418
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
14419
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
14420
+ const summary = this.calculateSummary(findings);
14421
+ const scanDurationMs = Date.now() - startTime;
14422
+ return {
14423
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14424
+ projectRoot,
14425
+ techStack,
14426
+ findings,
14427
+ summary,
14428
+ scannedFiles,
14429
+ scanDurationMs,
14430
+ errors
14431
+ };
14432
+ }
14433
+ async gatherFiles(root, extensions) {
14434
+ const files = [];
14435
+ const maxDepth = this.options.depth === "quick" ? 2 : this.options.depth === "deep" ? 20 : 5;
14436
+ await this.gatherFilesRecursive(root, files, extensions, 0, maxDepth);
14437
+ return files;
14438
+ }
14439
+ async gatherFilesRecursive(dir, files, extensions, currentDepth, maxDepth) {
14440
+ if (currentDepth > maxDepth) return;
14441
+ try {
14442
+ const entries = await readdir(dir, { withFileTypes: true });
14443
+ for (const entry of entries) {
14444
+ const fullPath = join(dir, entry.name);
14445
+ if (entry.isDirectory()) {
14446
+ if (this.shouldExclude(entry.name)) continue;
14447
+ await this.gatherFilesRecursive(fullPath, files, extensions, currentDepth + 1, maxDepth);
14448
+ } else if (entry.isFile()) {
14449
+ if (extensions.length === 0 || extensions.includes(extname(entry.name))) {
14450
+ files.push(fullPath);
14451
+ }
14452
+ }
14453
+ }
14454
+ } catch {
14455
+ }
14456
+ }
14457
+ shouldExclude(name) {
14458
+ return this.options.excludePaths.some(
14459
+ (exclude) => name === exclude || name.startsWith(exclude + "/") || name.startsWith(exclude + "\\")
14460
+ );
14461
+ }
14462
+ getTargetExtensions(skill, _techStack) {
14463
+ if (this.options.fileExtensions.length > 0) {
14464
+ return this.options.fileExtensions;
14465
+ }
14466
+ const extensions = /* @__PURE__ */ new Set();
14467
+ for (const pattern of skill.patterns) {
14468
+ for (const ext of pattern.fileExtensions) {
14469
+ if (ext.startsWith(".")) {
14470
+ extensions.add(ext.toLowerCase());
14471
+ }
14472
+ }
14473
+ }
14474
+ return [...extensions];
14475
+ }
14476
+ scanFile(content, filePath, patterns) {
14477
+ const findings = [];
14478
+ const lines = content.split("\n");
14479
+ for (const pattern of patterns) {
14480
+ if (!this.matchesCategory(pattern)) continue;
14481
+ for (const regex of pattern.patterns) {
14482
+ regex.lastIndex = 0;
14483
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
14484
+ const line = lines[lineNum];
14485
+ if (!line) continue;
14486
+ if (regex.test(line)) {
14487
+ if (this.isFalsePositive(line, pattern.falsePositiveMarkers)) {
14488
+ continue;
14489
+ }
14490
+ findings.push({
14491
+ id: `${pattern.id}-${filePath}-${lineNum}`,
14492
+ severity: pattern.severity,
14493
+ category: this.getCategoryFromPattern(pattern),
14494
+ title: pattern.name,
14495
+ description: pattern.description,
14496
+ file: relative(process.cwd(), filePath),
14497
+ line: lineNum + 1,
14498
+ snippet: line.trim(),
14499
+ remediation: pattern.remediation,
14500
+ patternId: pattern.id,
14501
+ confidence: "high"
14502
+ });
14503
+ }
14504
+ }
14505
+ }
14506
+ }
14507
+ return findings;
14508
+ }
14509
+ matchesCategory(pattern) {
14510
+ if (pattern.id.includes("secret") || pattern.id.includes("npmrc") || pattern.id.includes("env")) {
14511
+ return this.options.includeSecrets;
14512
+ }
14513
+ if (pattern.id.includes("injection") || pattern.id.includes("sql") || pattern.id.includes("command") || pattern.id.includes("eval")) {
14514
+ return this.options.includeInjection;
14515
+ }
14516
+ if (pattern.id.includes("config") || pattern.id.includes("tls") || pattern.id.includes("debug")) {
14517
+ return this.options.includeConfig;
14518
+ }
14519
+ return true;
14520
+ }
14521
+ getCategoryFromPattern(pattern) {
14522
+ if (pattern.id.includes("secret")) return "secrets";
14523
+ if (pattern.id.includes("injection") || pattern.id.includes("sql") || pattern.id.includes("command")) return "injection";
14524
+ if (pattern.id.includes("config") || pattern.id.includes("tls") || pattern.id.includes("debug")) return "config";
14525
+ if (pattern.id.includes("dependency")) return "dependency";
14526
+ return "filesystem";
14527
+ }
14528
+ isFalsePositive(line, markers) {
14529
+ for (const marker of markers) {
14530
+ if (line.includes(marker)) {
14531
+ return true;
14532
+ }
14533
+ }
14534
+ return false;
14535
+ }
14536
+ calculateSummary(findings) {
14537
+ return {
14538
+ critical: findings.filter((f) => f.severity === "critical").length,
14539
+ high: findings.filter((f) => f.severity === "high").length,
14540
+ medium: findings.filter((f) => f.severity === "medium").length,
14541
+ low: findings.filter((f) => f.severity === "low").length,
14542
+ total: findings.length
14543
+ };
14544
+ }
14545
+ };
14546
+ var defaultSecurityScanner = new SecurityScanner();
14547
+ var DEFAULT_REPORT_OPTIONS = {
14548
+ outputDir: "security-reports",
14549
+ format: "markdown",
14550
+ includeRemediation: true,
14551
+ groupBySeverity: true
14552
+ };
14553
+ var ReportGenerator = class {
14554
+ options;
14555
+ constructor(options = {}) {
14556
+ this.options = { ...DEFAULT_REPORT_OPTIONS, ...options };
14557
+ }
14558
+ async generate(scanResult) {
14559
+ await this.ensureOutputDir();
14560
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
14561
+ const filename = `security-report-${timestamp}.${this.options.format}`;
14562
+ const filepath = join(this.options.outputDir, filename);
14563
+ let content;
14564
+ switch (this.options.format) {
14565
+ case "json":
14566
+ content = this.generateJson(scanResult);
14567
+ break;
14568
+ case "html":
14569
+ content = this.generateHtml(scanResult);
14570
+ break;
14571
+ default:
14572
+ content = this.generateMarkdown(scanResult);
14573
+ }
14574
+ await writeFile(filepath, content, "utf-8");
14575
+ return filepath;
14576
+ }
14577
+ async ensureOutputDir() {
14578
+ try {
14579
+ await stat(this.options.outputDir);
14580
+ } catch {
14581
+ const { mkdir: mkdir10 } = await import('fs/promises');
14582
+ await mkdir10(this.options.outputDir, { recursive: true });
14583
+ }
14584
+ }
14585
+ generateMarkdown(result) {
14586
+ const lines = [];
14587
+ lines.push("# Security Scan Report");
14588
+ lines.push("");
14589
+ lines.push(`**Generated:** ${new Date(result.timestamp).toLocaleString()}`);
14590
+ lines.push(`**Project:** ${result.projectRoot}`);
14591
+ lines.push(`**Tech Stack:** ${result.techStack.stack} (${result.techStack.packageManager})`);
14592
+ lines.push(`**Scanned Files:** ${result.scannedFiles}`);
14593
+ lines.push(`**Scan Duration:** ${result.scanDurationMs}ms`);
14594
+ lines.push("");
14595
+ lines.push("## Summary");
14596
+ lines.push("");
14597
+ lines.push("| Severity | Count |");
14598
+ lines.push("|----------|-------|");
14599
+ lines.push(`| \u{1F534} Critical | ${result.summary.critical} |`);
14600
+ lines.push(`| \u{1F7E0} High | ${result.summary.high} |`);
14601
+ lines.push(`| \u{1F7E1} Medium | ${result.summary.medium} |`);
14602
+ lines.push(`| \u{1F7E2} Low | ${result.summary.low} |`);
14603
+ lines.push(`| **Total** | **${result.summary.total}** |`);
14604
+ lines.push("");
14605
+ if (this.options.groupBySeverity) {
14606
+ const severityGroups = this.groupBySeverity(result.findings);
14607
+ for (const severity of ["critical", "high", "medium", "low"]) {
14608
+ const findings = severityGroups[severity] ?? [];
14609
+ if (findings.length === 0) continue;
14610
+ const emoji = severity === "critical" ? "\u{1F534}" : severity === "high" ? "\u{1F7E0}" : severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
14611
+ lines.push(`## ${emoji} ${severity.toUpperCase()} (${findings.length})`);
14612
+ lines.push("");
14613
+ for (const finding of findings) {
14614
+ lines.push(this.formatFinding(finding));
14615
+ }
14616
+ lines.push("");
14617
+ }
14618
+ } else {
14619
+ const categoryGroups = this.groupByCategory(result.findings);
14620
+ for (const [category, findings] of Object.entries(categoryGroups)) {
14621
+ lines.push(`## ${category.toUpperCase()} (${findings.length})`);
14622
+ lines.push("");
14623
+ for (const finding of findings) {
14624
+ lines.push(this.formatFinding(finding));
14625
+ }
14626
+ lines.push("");
14627
+ }
14628
+ }
14629
+ if (result.errors.length > 0) {
14630
+ lines.push("## Scan Errors");
14631
+ lines.push("");
14632
+ for (const error of result.errors) {
14633
+ lines.push(`- ${error}`);
14634
+ }
14635
+ lines.push("");
14636
+ }
14637
+ lines.push("---");
14638
+ lines.push(`*Report generated by WrongStack Security Scanner*`);
14639
+ return lines.join("\n");
14640
+ }
14641
+ formatFinding(finding) {
14642
+ const lines = [];
14643
+ lines.push(`### ${finding.title}`);
14644
+ lines.push("");
14645
+ lines.push(`**File:** \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
14646
+ lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
14647
+ lines.push(`**Category:** ${finding.category}`);
14648
+ lines.push("");
14649
+ if (finding.snippet) {
14650
+ lines.push("```");
14651
+ lines.push(finding.snippet);
14652
+ lines.push("```");
14653
+ lines.push("");
14654
+ }
14655
+ if (this.options.includeRemediation) {
14656
+ lines.push(`**Remediation:** ${finding.remediation}`);
14657
+ lines.push("");
14658
+ }
14659
+ lines.push("---");
14660
+ lines.push("");
14661
+ return lines.join("\n");
14662
+ }
14663
+ groupBySeverity(findings) {
14664
+ const groups = {
14665
+ critical: [],
14666
+ high: [],
14667
+ medium: [],
14668
+ low: []
14669
+ };
14670
+ for (const finding of findings) {
14671
+ const group = groups[finding.severity];
14672
+ if (group) {
14673
+ group.push(finding);
14674
+ }
14675
+ }
14676
+ return groups;
14677
+ }
14678
+ groupByCategory(findings) {
14679
+ const groups = {};
14680
+ for (const finding of findings) {
14681
+ const group = groups[finding.category] ?? (groups[finding.category] = []);
14682
+ group.push(finding);
14683
+ }
14684
+ return groups;
14685
+ }
14686
+ generateJson(result) {
14687
+ return JSON.stringify(
14688
+ {
14689
+ meta: {
14690
+ timestamp: result.timestamp,
14691
+ projectRoot: result.projectRoot,
14692
+ techStack: result.techStack,
14693
+ scannedFiles: result.scannedFiles,
14694
+ scanDurationMs: result.scanDurationMs
14695
+ },
14696
+ summary: result.summary,
14697
+ findings: result.findings,
14698
+ errors: result.errors
14699
+ },
14700
+ null,
14701
+ 2
14702
+ );
14703
+ }
14704
+ generateHtml(result) {
14705
+ const severityColors = {
14706
+ critical: "#dc2626",
14707
+ high: "#ea580c",
14708
+ medium: "#ca8a04",
14709
+ low: "#16a34a"
14710
+ };
14711
+ const rows = result.findings.map(
14712
+ (f) => `
14713
+ <tr>
14714
+ <td style="color: ${severityColors[f.severity]}; font-weight: bold;">${f.severity.toUpperCase()}</td>
14715
+ <td>${f.category}</td>
14716
+ <td>${f.title}</td>
14717
+ <td><code>${f.file}${f.line ? `:${f.line}` : ""}</code></td>
14718
+ <td>${f.remediation}</td>
14719
+ </tr>
14720
+ `
14721
+ ).join("");
14722
+ return `
14723
+ <!DOCTYPE html>
14724
+ <html>
14725
+ <head>
14726
+ <title>Security Scan Report - ${new Date(result.timestamp).toLocaleDateString()}</title>
14727
+ <style>
14728
+ body { font-family: system-ui, sans-serif; margin: 2rem; }
14729
+ table { border-collapse: collapse; width: 100%; }
14730
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
14731
+ th { background: #f5f5f5; }
14732
+ code { background: #f0f0f0; padding: 2px 4px; }
14733
+ </style>
14734
+ </head>
14735
+ <body>
14736
+ <h1>Security Scan Report</h1>
14737
+ <p><strong>Generated:</strong> ${new Date(result.timestamp).toLocaleString()}</p>
14738
+ <p><strong>Project:</strong> ${result.projectRoot}</p>
14739
+ <p><strong>Tech Stack:</strong> ${result.techStack.stack} (${result.techStack.packageManager})</p>
14740
+
14741
+ <h2>Summary</h2>
14742
+ <ul>
14743
+ <li>\u{1F534} Critical: ${result.summary.critical}</li>
14744
+ <li>\u{1F7E0} High: ${result.summary.high}</li>
14745
+ <li>\u{1F7E1} Medium: ${result.summary.medium}</li>
14746
+ <li>\u{1F7E2} Low: ${result.summary.low}</li>
14747
+ </ul>
14748
+
14749
+ <h2>Findings</h2>
14750
+ <table>
14751
+ <tr>
14752
+ <th>Severity</th>
14753
+ <th>Category</th>
14754
+ <th>Title</th>
14755
+ <th>Location</th>
14756
+ <th>Remediation</th>
14757
+ </tr>
14758
+ ${rows}
14759
+ </table>
14760
+ </body>
14761
+ </html>
14762
+ `.trim();
14763
+ }
14764
+ };
14765
+ var defaultReportGenerator = new ReportGenerator();
14766
+ var DEFAULT_OPTIONS2 = {
14767
+ gitignorePath: ".gitignore",
14768
+ entries: ["security-reports/", "security-reports/*"]
14769
+ };
14770
+ var GitignoreUpdater = class {
14771
+ options;
14772
+ constructor(options = {}) {
14773
+ this.options = { ...DEFAULT_OPTIONS2, ...options };
14774
+ }
14775
+ async update() {
14776
+ const added = [];
14777
+ const existing = [];
14778
+ const errors = [];
14779
+ try {
14780
+ const content = await readFile(this.options.gitignorePath, "utf-8");
14781
+ const lines = new Set(content.split(/\r?\n/).map((l) => l.trim()));
14782
+ for (const entry of this.options.entries) {
14783
+ if (lines.has(entry)) {
14784
+ existing.push(entry);
14785
+ } else {
14786
+ lines.add(entry);
14787
+ added.push(entry);
14788
+ }
14789
+ }
14790
+ if (added.length > 0) {
14791
+ const newContent = [...lines].filter(Boolean).join("\n") + "\n";
14792
+ await writeFile(this.options.gitignorePath, newContent, "utf-8");
14793
+ }
14794
+ } catch (err) {
14795
+ if (err.code === "ENOENT") {
14796
+ const content = this.options.entries.join("\n") + "\n";
14797
+ await writeFile(this.options.gitignorePath, content, "utf-8");
14798
+ added.push(...this.options.entries);
14799
+ } else {
14800
+ errors.push(`Failed to update .gitignore: ${err}`);
14801
+ }
14802
+ }
14803
+ return { added, existing, errors };
14804
+ }
14805
+ async isEntryIgnored(entry) {
14806
+ try {
14807
+ const content = await readFile(this.options.gitignorePath, "utf-8");
14808
+ const lines = content.split(/\r?\n/).map((l) => l.trim());
14809
+ return lines.includes(entry);
14810
+ } catch {
14811
+ return false;
14812
+ }
14813
+ }
14814
+ };
14815
+ var defaultGitignoreUpdater = new GitignoreUpdater();
14816
+ var NETWORK_ERR_RE2 = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
14817
+ var SecurityScannerOrchestrator = class {
14818
+ constructor(retryPolicy, errorHandler) {
14819
+ this.retryPolicy = retryPolicy;
14820
+ this.errorHandler = errorHandler;
14821
+ }
14822
+ retryPolicy;
14823
+ errorHandler;
14824
+ detector = defaultTechStackDetector;
14825
+ reportGenerator = defaultReportGenerator;
14826
+ gitignoreUpdater = defaultGitignoreUpdater;
14827
+ /**
14828
+ * Wraps provider.complete with retry logic using the injected RetryPolicy.
14829
+ */
14830
+ async completeWithRetry(provider, request, abortController, attempt = 0) {
14831
+ const signal = abortController.signal;
14832
+ try {
14833
+ return await provider.complete(request, { signal });
14834
+ } catch (err) {
14835
+ if (signal.aborted) throw err;
14836
+ const isProviderErr = err instanceof ProviderError;
14837
+ const policy = this.retryPolicy;
14838
+ const errAsErr = isProviderErr ? err : err instanceof Error ? err : new Error(String(err));
14839
+ if (!policy || !isProviderErr && !NETWORK_ERR_RE2.test(errAsErr.message)) {
14840
+ throw err;
14841
+ }
14842
+ const canRetry = policy.shouldRetry(errAsErr, attempt);
14843
+ if (!canRetry) throw err;
14844
+ if (this.errorHandler) {
14845
+ const classified = this.errorHandler.classify(err);
14846
+ if (!classified.retryable) throw err;
14847
+ }
14848
+ const delay = Math.round(policy.delayMs(attempt));
14849
+ const status = isProviderErr ? err.status : 0;
14850
+ console.warn(`[SecurityScanner] retry ${attempt + 1} after ${delay}ms (status=${status}) \u2014 ${errAsErr.message}`);
14851
+ await new Promise((resolve5) => setTimeout(resolve5, delay));
14852
+ return this.completeWithRetry(provider, request, abortController, attempt + 1);
14853
+ }
14854
+ }
14855
+ /**
14856
+ * Run full security scan with LLM assistance.
14857
+ * Accepts a full Context (active agent run) or just provider+model (pre-agent session).
14858
+ */
14859
+ async run(ctx, options) {
14860
+ const { projectRoot, reportOptions, skipGitignore, model: explicitModel } = options;
14861
+ const provider = "provider" in ctx ? ctx.provider : ctx;
14862
+ const model = explicitModel ?? ("model" in ctx ? ctx.model : void 0);
14863
+ const detectionResult = await this.detector.detect(projectRoot);
14864
+ if (detectionResult.detectedStacks.length === 0) {
14865
+ throw new Error(`No supported tech stack detected in ${projectRoot}`);
14866
+ }
14867
+ const techStack = detectionResult.detectedStacks[0];
14868
+ const generatedSkill = await this.generateSkillLLM(provider, model, projectRoot, techStack);
14869
+ const scanResult = await this.scanWithLLM(provider, model, projectRoot, generatedSkill, techStack, options);
14870
+ const synthesizedReport = await this.synthesizeReportLLM(provider, model, projectRoot, techStack, scanResult);
14871
+ const reportPath = await this.writeReport(synthesizedReport, reportOptions);
14872
+ let gitignoreResult;
14873
+ if (!skipGitignore) {
14874
+ gitignoreResult = await this.gitignoreUpdater.update();
14875
+ }
14876
+ return {
14877
+ detectionResult,
14878
+ generatedSkill,
14879
+ scanResult,
14880
+ reportPath,
14881
+ synthesizedReport,
14882
+ gitignoreResult
14883
+ };
14884
+ }
14885
+ /**
14886
+ * Generate a project-specific security skill using LLM.
14887
+ * The LLM analyzes the project structure and creates tailored security patterns.
14888
+ */
14889
+ async generateSkillLLM(provider, model, projectRoot, techStack) {
14890
+ const projectInfo = await this.gatherProjectInfo(projectRoot, techStack);
14891
+ const prompt = `You are a security expert generating a customized security scanning skill for a specific project.
14892
+
14893
+ Analyze the following project and generate a detailed security skill with:
14894
+ 1. Project-specific vulnerability patterns based on the tech stack and structure
14895
+ 2. Language/framework specific security concerns
14896
+ 3. Common attack vectors for this type of application
14897
+ 4. File patterns to scan
14898
+
14899
+ ## Project Information:
14900
+ ${projectInfo}
14901
+
14902
+ ## Tech Stack:
14903
+ - Language: ${techStack.stack}
14904
+ - Package Manager: ${techStack.packageManager}
14905
+ - Manifest: ${techStack.manifestFile}
14906
+
14907
+ ## Dependencies (first 20):
14908
+ ${techStack.dependencies.slice(0, 20).map((d) => `- ${d.name}@${d.version}`).join("\n")}
14909
+
14910
+ ## Your Task:
14911
+ Generate a JSON security skill with the following structure:
14912
+ {
14913
+ "name": "security-scanner-${techStack.stack}",
14914
+ "description": "Custom security scanner for this project",
14915
+ "techStack": "${techStack.stack}",
14916
+ "patterns": [
14917
+ {
14918
+ "id": "unique-pattern-id",
14919
+ "name": "Pattern Name",
14920
+ "severity": "critical|high|medium|low",
14921
+ "description": "What this detects",
14922
+ "fileExtensions": [".ts", ".js"],
14923
+ "remediation": "How to fix"
14924
+ }
14925
+ ],
14926
+ "targetFiles": ["**/*.ts", "**/*.js"],
14927
+ "scanInstructions": "Detailed instructions for scanning this codebase"
14928
+ }
14929
+
14930
+ Focus on:
14931
+ - ${techStack.stack === "nodejs" ? "Node.js specific: eval, prototype pollution, npm script injection, express middleware issues, passport.js misconfigs" : ""}
14932
+ - ${techStack.stack === "python" ? "Python specific: pickle deserialization, SQL injection in ORMs, template injection, insecure Django/Flask settings" : ""}
14933
+ - Common: hardcoded secrets, SQL injection, command injection, XSS, path traversal, XXE
14934
+
14935
+ Return ONLY the JSON object, no markdown, no explanation.`;
14936
+ const request = {
14937
+ model: model ?? "unknown",
14938
+ system: [{ type: "text", text: "You are a security expert. Return ONLY valid JSON." }],
14939
+ messages: [{ role: "user", content: prompt }],
14940
+ maxTokens: 4096
14941
+ };
14942
+ try {
14943
+ const abortController = new AbortController();
14944
+ const response = await this.completeWithRetry(provider, request, abortController);
14945
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
14946
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
14947
+ if (jsonMatch) {
14948
+ const sanitized = sanitizeJsonString(jsonMatch[0]) || jsonMatch[0];
14949
+ const skillData = JSON.parse(sanitized);
14950
+ return {
14951
+ name: skillData.name || `security-scanner-${techStack.stack}`,
14952
+ description: skillData.description || `Security scanner for ${techStack.stack}`,
14953
+ version: "1.0.0",
14954
+ techStack: techStack.stack,
14955
+ content: { type: "skill", content: JSON.stringify(skillData, null, 2) },
14956
+ patterns: skillData.patterns || [],
14957
+ metadata: {
14958
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14959
+ confidence: 0.85,
14960
+ targetFiles: skillData.targetFiles || []
14961
+ }
14962
+ };
14963
+ }
14964
+ } catch (err) {
14965
+ console.error("LLM skill generation failed, using fallback:", err);
14966
+ }
14967
+ return this.generateFallbackSkill(techStack);
14968
+ }
14969
+ /**
14970
+ * Scan code using LLM with the generated skill as context.
14971
+ * The LLM analyzes files and reports security findings.
14972
+ */
14973
+ async scanWithLLM(provider, model, projectRoot, skill, techStack, options) {
14974
+ const startTime = Date.now();
14975
+ const findings = [];
14976
+ const errors = [];
14977
+ let scannedFiles = 0;
14978
+ const files = await this.gatherFiles(projectRoot, skill.metadata.targetFiles, options.scanOptions?.depth || "standard");
14979
+ const batchSize = 10;
14980
+ for (let i = 0; i < files.length; i += batchSize) {
14981
+ const batch = files.slice(i, i + batchSize);
14982
+ const batchFindings = await this.scanFileBatchLLM(provider, model, batch, skill, techStack);
14983
+ findings.push(...batchFindings);
14984
+ scannedFiles += batch.length;
14985
+ }
14986
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
14987
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
14988
+ const summary = {
14989
+ critical: findings.filter((f) => f.severity === "critical").length,
14990
+ high: findings.filter((f) => f.severity === "high").length,
14991
+ medium: findings.filter((f) => f.severity === "medium").length,
14992
+ low: findings.filter((f) => f.severity === "low").length,
14993
+ total: findings.length
14994
+ };
14995
+ return {
14996
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14997
+ projectRoot,
14998
+ techStack,
14999
+ findings,
15000
+ summary,
15001
+ scannedFiles,
15002
+ scanDurationMs: Date.now() - startTime,
15003
+ errors
15004
+ };
15005
+ }
15006
+ /**
15007
+ * Scan a batch of files using LLM.
15008
+ */
15009
+ async scanFileBatchLLM(provider, model, files, skill, techStack) {
15010
+ const fileContents = [];
15011
+ for (const file of files) {
15012
+ try {
15013
+ const content = await readFile(file, "utf-8");
15014
+ const relativePath = relative(process.cwd(), file);
15015
+ fileContents.push(`
15016
+ === ${relativePath} ===
15017
+ ${content.slice(0, 2e3)}`);
15018
+ } catch {
15019
+ }
15020
+ }
15021
+ if (fileContents.length === 0) return [];
15022
+ const prompt = `You are a security expert analyzing code for vulnerabilities.
15023
+
15024
+ ## Security Patterns to Detect (from generated skill):
15025
+ ${skill.patterns.map((p) => `- ${p.name} (${p.severity}): ${p.description}`).join("\n")}
15026
+
15027
+ ## Files to Analyze:
15028
+ ${fileContents.join("\n")}
15029
+
15030
+ ## Your Task:
15031
+ Analyze each file for security vulnerabilities matching the patterns above.
15032
+ For each finding, provide:
15033
+ 1. File path (relative path from === markers)
15034
+ 2. Line number if identifiable
15035
+ 3. Severity (critical/high/medium/low)
15036
+ 4. Category (secrets/injection/config/dependency)
15037
+ 5. Description of the issue
15038
+ 6. Code snippet showing the vulnerability
15039
+ 7. Remediation steps
15040
+
15041
+ Return a JSON array of findings:
15042
+ [
15043
+ {
15044
+ "file": "path/to/file.ts",
15045
+ "line": 42,
15046
+ "severity": "high",
15047
+ "category": "injection",
15048
+ "title": "SQL Injection Risk",
15049
+ "description": "...",
15050
+ "snippet": "actual code...",
15051
+ "remediation": "..."
15052
+ }
15053
+ ]
15054
+
15055
+ Return ONLY the JSON array. If no issues found, return [].`;
15056
+ try {
15057
+ const request = {
15058
+ model: model ?? "unknown",
15059
+ system: [{ type: "text", text: "You are a security expert. Return ONLY valid JSON." }],
15060
+ messages: [{ role: "user", content: prompt }],
15061
+ maxTokens: 4096
15062
+ };
15063
+ const abortController = new AbortController();
15064
+ const response = await this.completeWithRetry(provider, request, abortController);
15065
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
15066
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
15067
+ if (jsonMatch) {
15068
+ const sanitized = sanitizeJsonString(jsonMatch[0]) || jsonMatch[0];
15069
+ const parsed = JSON.parse(sanitized);
15070
+ return parsed.map((item, idx) => ({
15071
+ id: `llm-finding-${idx}-${Date.now()}`,
15072
+ severity: item.severity,
15073
+ category: item.category || "injection",
15074
+ title: item.title,
15075
+ description: item.description,
15076
+ file: item.file,
15077
+ line: item.line,
15078
+ snippet: item.snippet,
15079
+ remediation: item.remediation,
15080
+ patternId: "llm-analysis",
15081
+ confidence: "high"
15082
+ }));
15083
+ }
15084
+ } catch (err) {
15085
+ console.error("LLM scan batch failed:", err);
15086
+ }
15087
+ return [];
15088
+ }
15089
+ /**
15090
+ * Synthesize a comprehensive security report using LLM.
15091
+ */
15092
+ async synthesizeReportLLM(provider, model, projectRoot, techStack, scanResult) {
15093
+ const prompt = `You are a security expert writing a comprehensive security report.
15094
+
15095
+ ## Scan Results:
15096
+ - Scanned Files: ${scanResult.scannedFiles}
15097
+ - Total Findings: ${scanResult.summary.total}
15098
+ - Critical: ${scanResult.summary.critical}
15099
+ - High: ${scanResult.summary.high}
15100
+ - Medium: ${scanResult.summary.medium}
15101
+ - Low: ${scanResult.summary.low}
15102
+
15103
+ ## Detailed Findings:
15104
+ ${scanResult.findings.map((f, i) => `
15105
+ ${i + 1}. [${f.severity.toUpperCase()}] ${f.title}
15106
+ File: ${f.file}${f.line ? `:${f.line}` : ""}
15107
+ Category: ${f.category}
15108
+ Description: ${f.description}
15109
+ ${f.snippet ? `Code: \`\`\`
15110
+ ${f.snippet}
15111
+ \`\`\`` : ""}
15112
+ Remediation: ${f.remediation}
15113
+ `).join("\n")}
15114
+
15115
+ ## Project:
15116
+ - Root: ${projectRoot}
15117
+ - Tech Stack: ${techStack.stack} (${techStack.packageManager})
15118
+
15119
+ ## Your Task:
15120
+ Write a comprehensive markdown security report with:
15121
+ 1. Executive Summary (overall security posture)
15122
+ 2. Critical Issues (with detailed analysis and remediation)
15123
+ 3. High Priority Issues
15124
+ 4. Medium Priority Issues
15125
+ 5. Low Priority / Informational
15126
+ 6. Security Recommendations (prioritized action items)
15127
+ 7. Summary Table
15128
+
15129
+ Format with proper markdown, emojis for severity, and actionable remediation steps.
15130
+
15131
+ Be specific about the vulnerabilities found and how to fix them.`;
15132
+ try {
15133
+ const request = {
15134
+ model: model ?? "unknown",
15135
+ system: [{ type: "text", text: "You are a security expert writing detailed reports." }],
15136
+ messages: [{ role: "user", content: prompt }],
15137
+ maxTokens: 8192
15138
+ };
15139
+ const abortController = new AbortController();
15140
+ const response = await this.completeWithRetry(provider, request, abortController);
15141
+ return response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
15142
+ } catch (err) {
15143
+ console.error("LLM report synthesis failed:", err);
15144
+ return this.generateBasicReport(projectRoot, techStack, scanResult);
15145
+ }
15146
+ }
15147
+ /**
15148
+ * Generate a basic fallback report when LLM synthesis fails.
15149
+ */
15150
+ generateBasicReport(projectRoot, techStack, scanResult) {
15151
+ const lines = [];
15152
+ lines.push("# Security Scan Report");
15153
+ lines.push("");
15154
+ lines.push(`**Generated:** ${new Date(scanResult.timestamp).toLocaleString()}`);
15155
+ lines.push(`**Project:** ${projectRoot}`);
15156
+ lines.push(`**Tech Stack:** ${techStack.stack} (${techStack.packageManager})`);
15157
+ lines.push(`**Scanned Files:** ${scanResult.scannedFiles}`);
15158
+ lines.push("");
15159
+ lines.push("## Summary");
15160
+ lines.push("");
15161
+ lines.push("| Severity | Count |");
15162
+ lines.push("|----------|-------|");
15163
+ lines.push(`| \u{1F534} Critical | ${scanResult.summary.critical} |`);
15164
+ lines.push(`| \u{1F7E0} High | ${scanResult.summary.high} |`);
15165
+ lines.push(`| \u{1F7E1} Medium | ${scanResult.summary.medium} |`);
15166
+ lines.push(`| \u{1F7E2} Low | ${scanResult.summary.low} |`);
15167
+ lines.push("");
15168
+ for (const finding of scanResult.findings) {
15169
+ const emoji = finding.severity === "critical" ? "\u{1F534}" : finding.severity === "high" ? "\u{1F7E0}" : finding.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
15170
+ lines.push(`## ${emoji} ${finding.title}`);
15171
+ lines.push("");
15172
+ lines.push(`**File:** \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
15173
+ lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
15174
+ lines.push(`**Category:** ${finding.category}`);
15175
+ lines.push("");
15176
+ if (finding.snippet) {
15177
+ lines.push("```");
15178
+ lines.push(finding.snippet);
15179
+ lines.push("```");
15180
+ lines.push("");
15181
+ }
15182
+ lines.push(`**Remediation:** ${finding.remediation}`);
15183
+ lines.push("");
15184
+ }
15185
+ return lines.join("\n");
15186
+ }
15187
+ /**
15188
+ * Write synthesized report to file.
15189
+ */
15190
+ async writeReport(content, reportOptions) {
15191
+ const outputDir = reportOptions?.outputDir || "security-reports";
15192
+ const format = reportOptions?.format || "markdown";
15193
+ try {
15194
+ await mkdir(outputDir, { recursive: true });
15195
+ } catch {
15196
+ }
15197
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
15198
+ const filename = `security-report-${timestamp}.${format}`;
15199
+ const filepath = join(outputDir, filename);
15200
+ await writeFile(filepath, content, "utf-8");
15201
+ return filepath;
15202
+ }
15203
+ /**
15204
+ * Gather project info for skill generation.
15205
+ */
15206
+ async gatherProjectInfo(projectRoot, techStack) {
15207
+ const info = [];
15208
+ const keyFiles = [
15209
+ "package.json",
15210
+ "tsconfig.json",
15211
+ ".env.example",
15212
+ "README.md",
15213
+ "CONTRIBUTING.md"
15214
+ ];
15215
+ for (const file of keyFiles) {
15216
+ try {
15217
+ const content = await readFile(join(projectRoot, file), "utf-8");
15218
+ const displayName = file === "README.md" || file === "CONTRIBUTING.md" ? "README" : file;
15219
+ info.push(`
15220
+ --- ${displayName} ---
15221
+ ${content.slice(0, 1e3)}`);
15222
+ } catch {
15223
+ }
15224
+ }
15225
+ try {
15226
+ const entries = await readdir(projectRoot, { withFileTypes: true });
15227
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).slice(0, 20);
15228
+ info.push(`
15229
+ --- Project Directories ---
15230
+ ${dirs.join(", ")}`);
15231
+ } catch {
15232
+ }
15233
+ return info.join("\n");
15234
+ }
15235
+ /**
15236
+ * Gather files to scan based on patterns.
15237
+ */
15238
+ async gatherFiles(root, patterns, depth) {
15239
+ const files = [];
15240
+ const maxDepth = depth === "quick" ? 2 : depth === "deep" ? 20 : 5;
15241
+ const extensions = [".ts", ".js", ".jsx", ".tsx", ".py", ".go", ".java", ".cs", ".rs"];
15242
+ await this.gatherFilesRecursive(root, files, extensions, 0, maxDepth);
15243
+ return files;
15244
+ }
15245
+ async gatherFilesRecursive(dir, files, extensions, currentDepth, maxDepth) {
15246
+ if (currentDepth > maxDepth) return;
15247
+ try {
15248
+ const entries = await readdir(dir, { withFileTypes: true });
15249
+ for (const entry of entries) {
15250
+ if (entry.isDirectory()) {
15251
+ const name = entry.name;
15252
+ if (name === "node_modules" || name === "dist" || name === "build" || name === ".git" || name === "coverage" || name.startsWith(".")) continue;
15253
+ await this.gatherFilesRecursive(
15254
+ join(dir, entry.name),
15255
+ files,
15256
+ extensions,
15257
+ currentDepth + 1,
15258
+ maxDepth
15259
+ );
15260
+ } else if (entry.isFile()) {
15261
+ const ext = entry.name.lastIndexOf(".");
15262
+ if (ext > 0 && extensions.includes(entry.name.slice(ext))) {
15263
+ files.push(join(dir, entry.name));
15264
+ }
15265
+ }
15266
+ }
15267
+ } catch {
15268
+ }
15269
+ }
15270
+ /**
15271
+ * Generate fallback skill when LLM fails.
15272
+ */
15273
+ generateFallbackSkill(techStack) {
15274
+ return {
15275
+ name: `security-scanner-${techStack.stack}`,
15276
+ description: `Security scanner for ${techStack.stack} projects`,
15277
+ version: "1.0.0",
15278
+ techStack: techStack.stack,
15279
+ content: { type: "skill", content: "Fallback static skill" },
15280
+ patterns: [
15281
+ {
15282
+ id: "hardcoded-secrets",
15283
+ name: "Hardcoded Secrets",
15284
+ severity: "critical",
15285
+ description: "Detects hardcoded API keys, tokens, passwords",
15286
+ patterns: [],
15287
+ fileExtensions: [".ts", ".js", ".env"],
15288
+ falsePositiveMarkers: [],
15289
+ remediation: "Use environment variables"
15290
+ }
15291
+ ],
15292
+ metadata: {
15293
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
15294
+ confidence: 0.5,
15295
+ targetFiles: [`**/*.${techStack.stack === "nodejs" ? "ts" : techStack.stack === "python" ? "py" : "ts"}`]
15296
+ }
15297
+ };
15298
+ }
15299
+ /**
15300
+ * Quick scan - legacy compatibility.
15301
+ * NOTE: This won't use LLM as it doesn't have access to ctx.
15302
+ */
15303
+ async quickScan(projectRoot) {
15304
+ const detectionResult = await this.detector.detect(projectRoot);
15305
+ if (detectionResult.detectedStacks.length === 0) {
15306
+ throw new Error(`No supported tech stack detected in ${projectRoot}`);
15307
+ }
15308
+ const techStack = detectionResult.detectedStacks[0];
15309
+ return {
15310
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15311
+ projectRoot,
15312
+ techStack,
15313
+ findings: [],
15314
+ summary: { critical: 0, high: 0, medium: 0, low: 0, total: 0 },
15315
+ scannedFiles: 0,
15316
+ scanDurationMs: 0,
15317
+ errors: ["Quick scan without LLM context is not fully supported. Use run(ctx, options) for full scan."]
15318
+ };
15319
+ }
15320
+ };
15321
+ var defaultOrchestrator = new SecurityScannerOrchestrator();
15322
+ function createSecuritySlashCommand() {
15323
+ return {
15324
+ name: "security",
15325
+ description: "Security scanning commands: scan, audit, report",
15326
+ argsHint: "[scan|audit|report] [options]",
15327
+ help: `
15328
+ # /security \u2014 Security Scanner
15329
+
15330
+ Security scanning with automatic tech stack detection.
15331
+
15332
+ ## Commands
15333
+
15334
+ ### /security scan [options]
15335
+ Run a full security scan on the current project.
15336
+ Options:
15337
+ --depth quick|standard|deep Scan depth (default: standard)
15338
+ --format markdown|json|html Report format (default: markdown)
15339
+
15340
+ ### /security audit
15341
+ Run dependency audit + security scan.
15342
+ Checks for known vulnerabilities in dependencies.
15343
+
15344
+ ### /security report [id]
15345
+ Show a previous security report.
15346
+ /security report - List available reports
15347
+ /security report <id> - Show specific report
15348
+
15349
+ ## Examples
15350
+
15351
+ /security scan
15352
+ /security scan --depth deep --format html
15353
+ /security audit
15354
+ /security report
15355
+ /security report 2025-01-15
15356
+ `,
15357
+ async run(args, ctx) {
15358
+ const parts = args.trim().split(/\s+/);
15359
+ const subcommand = parts[0] || "";
15360
+ const subArgs = parts.slice(1).join(" ");
15361
+ switch (subcommand) {
15362
+ case "scan":
15363
+ return handleScan(subArgs, ctx);
15364
+ case "audit":
15365
+ return handleAudit(ctx);
15366
+ case "report":
15367
+ return handleReport(subArgs);
15368
+ default:
15369
+ return { message: getHelpMessage() };
15370
+ }
15371
+ }
15372
+ };
15373
+ }
15374
+ async function handleScan(args, ctx) {
15375
+ const options = parseArgs(args);
15376
+ const projectRoot = ctx.projectRoot || ctx.cwd || process.cwd();
15377
+ try {
15378
+ const result = await defaultOrchestrator.run(ctx, {
15379
+ projectRoot,
15380
+ scanOptions: {
15381
+ depth: options.depth || "standard",
15382
+ includeSecrets: true,
15383
+ includeInjection: true,
15384
+ includeConfig: true
15385
+ },
15386
+ reportOptions: {
15387
+ format: options.format || "markdown"
15388
+ }
15389
+ });
15390
+ const summary = result.scanResult.summary;
15391
+ const status = summary.total === 0 ? "\u2705 No issues found" : `\u26A0\uFE0F Found ${summary.total} issues`;
15392
+ const reportContent = result.synthesizedReport || `# Security Scan Complete
15393
+
15394
+ **Project:** ${projectRoot}
15395
+ **Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
15396
+ **Scanned Files:** ${result.scanResult.scannedFiles}
15397
+ **Duration:** ${result.scanResult.scanDurationMs}ms
15398
+
15399
+ ## Summary
15400
+
15401
+ | Severity | Count |
15402
+ |----------|-------|
15403
+ | \u{1F534} Critical | ${summary.critical} |
15404
+ | \u{1F7E0} High | ${summary.high} |
15405
+ | \u{1F7E1} Medium | ${summary.medium} |
15406
+ | \u{1F7E2} Low | ${summary.low} |
15407
+
15408
+ **Status:** ${status}
15409
+
15410
+ **Report:** ${result.reportPath}
15411
+ `;
15412
+ return {
15413
+ message: reportContent,
15414
+ metadata: {
15415
+ scanResult: result.scanResult,
15416
+ reportPath: result.reportPath,
15417
+ techStack: result.detectionResult.detectedStacks[0]
15418
+ }
15419
+ };
15420
+ } catch (error) {
15421
+ return { message: `\u274C Scan failed: ${error}` };
15422
+ }
15423
+ }
15424
+ async function handleAudit(ctx) {
15425
+ const projectRoot = ctx.projectRoot || ctx.cwd || process.cwd();
15426
+ try {
15427
+ const result = await defaultOrchestrator.run(ctx, {
15428
+ projectRoot,
15429
+ reportOptions: { format: "markdown" }
15430
+ });
15431
+ const depIssues = result.scanResult.summary.critical + result.scanResult.summary.high;
15432
+ const reportContent = result.synthesizedReport || `
15433
+ # Security Audit Complete
15434
+
15435
+ **Project:** ${projectRoot}
15436
+ **Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
15437
+
15438
+ ## Dependency Health
15439
+
15440
+ | Status | Count |
15441
+ |--------|-------|
15442
+ | Critical Issues | ${result.scanResult.summary.critical} |
15443
+ | High Priority | ${result.scanResult.summary.high} |
15444
+ | Medium Priority | ${result.scanResult.summary.medium} |
15445
+ | Low Priority | ${result.scanResult.summary.low} |
15446
+
15447
+ ${depIssues === 0 ? "\u2705 No known vulnerabilities detected" : `\u26A0\uFE0F ${depIssues} vulnerabilities need attention`}
15448
+
15449
+ **Full Report:** ${result.reportPath}
15450
+ `;
15451
+ return {
15452
+ message: reportContent,
15453
+ metadata: {
15454
+ scanResult: result.scanResult,
15455
+ reportPath: result.reportPath
15456
+ }
15457
+ };
15458
+ } catch (error) {
15459
+ return { message: `\u274C Audit failed: ${error}` };
15460
+ }
15461
+ }
15462
+ async function handleReport(reportId) {
15463
+ const reportsDir = "security-reports";
15464
+ try {
15465
+ const files = await readdir(reportsDir);
15466
+ const reports = files.filter((f) => f.startsWith("security-report-") && (f.endsWith(".md") || f.endsWith(".json"))).sort().reverse();
15467
+ if (!reportId) {
15468
+ if (reports.length === 0) {
15469
+ return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
15470
+ }
15471
+ const list = reports.map((r, i) => {
15472
+ const date = r.replace("security-report-", "").replace(/\.(md|json)$/, "");
15473
+ return ` ${i + 1}. ${date}`;
15474
+ }).join("\n");
15475
+ return { message: `# Available Security Reports
15476
+
15477
+ ${list}
15478
+
15479
+ Use \`/security report <number>\` to view a specific report.` };
15480
+ }
15481
+ const index = parseInt(reportId, 10) - 1;
15482
+ if (!isNaN(index) && reports[index]) {
15483
+ const { readFile: readFile28 } = await import('fs/promises');
15484
+ const content = await readFile28(join(reportsDir, reports[index]), "utf-8");
15485
+ return { message: `# Security Report
15486
+
15487
+ ${content}` };
15488
+ }
15489
+ const match = reports.find((r) => r.includes(reportId));
15490
+ if (match) {
15491
+ const { readFile: readFile28 } = await import('fs/promises');
15492
+ const content = await readFile28(join(reportsDir, match), "utf-8");
15493
+ return { message: `# Security Report
15494
+
15495
+ ${content}` };
15496
+ }
15497
+ return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
15498
+ } catch (error) {
15499
+ return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
15500
+ }
15501
+ }
15502
+ function parseArgs(args) {
15503
+ const result = {};
15504
+ const parts = args.split(/\s+/);
15505
+ for (let i = 0; i < parts.length; i++) {
15506
+ const part = parts[i];
15507
+ if (!part || !part.startsWith("--")) continue;
15508
+ const key = part.slice(2);
15509
+ const next = parts[i + 1];
15510
+ if (next && !next.startsWith("--")) {
15511
+ result[key] = next;
15512
+ i++;
15513
+ } else {
15514
+ result[key] = "true";
15515
+ }
15516
+ }
15517
+ return result;
15518
+ }
15519
+ function getHelpMessage() {
15520
+ return `
15521
+ # /security \u2014 Security Scanner
15522
+
15523
+ **Available Commands:**
15524
+
15525
+ 1. **/security scan** \u2014 Run full security scan
15526
+ Options: --depth quick|standard|deep, --format markdown|json|html
15527
+
15528
+ 2. **/security audit** \u2014 Run dependency audit + security scan
15529
+
15530
+ 3. **/security report** \u2014 List or view security reports
15531
+
15532
+ **Examples:**
15533
+ \`\`\`
15534
+ /security scan
15535
+ /security scan --depth deep --format html
15536
+ /security audit
15537
+ /security report
15538
+ \`\`\`
15539
+ `;
15540
+ }
15541
+ var securitySlashCommand = createSecuritySlashCommand();
13164
15542
 
13165
15543
  // src/extension/registry.ts
13166
15544
  var ExtensionRegistry = class {
@@ -14619,8 +16997,8 @@ ${mem}`);
14619
16997
  }
14620
16998
  async dirExists(p) {
14621
16999
  try {
14622
- const stat6 = await fsp2.stat(p);
14623
- return stat6.isDirectory();
17000
+ const stat8 = await fsp2.stat(p);
17001
+ return stat8.isDirectory();
14624
17002
  } catch {
14625
17003
  return false;
14626
17004
  }
@@ -15371,6 +17749,6 @@ function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
15371
17749
  });
15372
17750
  }
15373
17751
 
15374
- export { AISpecBuilder, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SelectiveCompactor, SessionAnalyzer, SessionError, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, asBlocks, asText, atomicWrite, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDefaultPipelines, createDelegateTool, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, downloadGitHubTarball, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatPlan, formatTodosList, getContextWindowMode, getTemplate, githubServer, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listTemplates, loadDirectorState, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseSkillRef, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
17752
+ export { AISpecBuilder, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetUsageAggregator, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolError, ToolExecutor, ToolRegistry, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, asBlocks, asText, atomicWrite, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDefaultPipelines, createDelegateTool, createMessage, createSecuritySlashCommand, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, downloadGitHubTarball, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatPlan, formatPlanTemplates, formatTodosList, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseSkillRef, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, savePlan, saveTodosCheckpoint, securitySlashCommand, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
15375
17753
  //# sourceMappingURL=index.js.map
15376
17754
  //# sourceMappingURL=index.js.map