swarmdo 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -515,7 +515,7 @@ function getCostFromStdin() {
515
515
  // misses, the displayed version is meaningful (matches what the user
516
516
  // installed), not a stale hard-coded string.
517
517
  function getPkgVersion() {
518
- let ver = '1.7.0';
518
+ let ver = '1.9.0';
519
519
  try {
520
520
  const home = os.homedir();
521
521
  const pkgPaths = [
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![swarmdo](swarmdo/assets/swarmdo-banner.svg)](https://swarmdo.com)
4
4
 
5
- [![npm version (swarmdo)](https://img.shields.io/badge/npx%20swarmdo-v1.7.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
5
+ [![npm version (swarmdo)](https://img.shields.io/badge/npx%20swarmdo-v1.9.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
6
6
  [![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](https://github.com/SwarmDo/swarmdo/blob/main/LICENSE)
7
7
  [![Website](https://img.shields.io/badge/swarmdo.com-e2a33c?style=for-the-badge&logoColor=black)](https://swarmdo.com)
8
8
  [![Star on GitHub](https://img.shields.io/github/stars/SwarmDo/swarmdo?style=for-the-badge&logo=github&color=gold)](https://github.com/SwarmDo/swarmdo)
@@ -277,7 +277,8 @@ The recent release train added a full day-to-day operations layer around the swa
277
277
  | `swarmdo worktree` (alias `wt`) | **Parallel-agent isolation** on git worktrees — add / list / diff / merge / remove |
278
278
  | `swarmdo transcript` (alias `tx`) | **Export any Claude Code session** to clean markdown — system noise stripped, ready to share; `transcript search <query>` full-text-searches every session |
279
279
  | `swarmdo compact` | **Compress noisy command output** before it reaches an LLM — strip ANSI, collapse repeats, fold `node_modules` stack frames, window long logs. `npm test 2>&1 \| swarmdo compact` or `swarmdo compact -- npm test` (exit code propagates). Deterministic, zero tokens |
280
- | `swarmdo codegraph` (alias `cg`) | **Queryable symbol index** — `codegraph index` scans TS/JS for exported symbols; `query <name>` (with `--fuzzy`/`--kind`) and `file <path>` answer "where is X defined / what does this file export" from `.swarm/codegraph.json` instead of grep+read round-trips. 1,750 symbols across 289 files in <1s |
280
+ | `swarmdo codegraph` (alias `cg`) | **Queryable symbol index** — `codegraph index` scans TS/JS for exported symbols; `query <name>` (with `--fuzzy`/`--kind`) and `file <path>` answer "where is X defined / what does this file export" from `.swarm/codegraph.json` instead of grep+read round-trips. 1,750 symbols across 289 files in <1s. Also exposed as MCP tools (`codegraph_query`/`file`/`index`/`stats`) so agents query the index in-session |
281
+ | `swarmdo redact` | **Mask secrets before they reach an LLM/log/memory** — detect API keys, tokens, and private keys (gitleaks-style rule catalog + Shannon-entropy fallback) and redact them. Stdin filter (`cat deploy.log \| swarmdo redact`), command-wrap (`swarmdo redact -- npm run deploy`), or `--scan` to fail CI on any secret. Also MCP tools (`redact_text`/`redact_scan`). Deterministic, zero tokens |
281
282
  | `swarmdo integrations` (alias `integrate`) | **Use swarmdo from Codex CLI, GitHub Copilot CLI, and pi** — one command wires AGENTS.md + each CLI's MCP config (idempotent, dry-run first, never touches your Claude Code setup) |
282
283
  | OpenRouter model pool | **Let swarms pick from any models you configure** — declare tier-mapped OpenRouter models in `swarmdo.config.json`; the router Thompson-samples among them per task and the execution layer dispatches the winner |
283
284
  | `swarmdo changelog` (alias `notes`) | **Release notes from conventional commits** — `--out NOTES.md` feeds `gh release create --notes-file` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmdo",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Swarmdo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * store.ts — the filesystem layer for codegraph: walk the tree, read sources,
3
+ * build the index (via the pure engine), and persist/load .swarm/codegraph.json.
4
+ * Shared by the `codegraph` CLI command and the codegraph MCP tools so the
5
+ * scan/persist behaviour can't drift between them.
6
+ */
7
+ import { type CodeIndex } from './codegraph.js';
8
+ export declare const INDEX_REL: string;
9
+ /** Recursively collect source files under `root`, skipping vendor/build dirs and .d.ts. */
10
+ export declare function walkSourceFiles(root: string): string[];
11
+ export declare function indexPath(root: string): string;
12
+ /** Scan a repo (or a subpath of it) and build the index. `root` is the repo
13
+ * root used for relative paths; `scanRoot` (default = root) is where to walk. */
14
+ export declare function scanRepo(root: string, scanRoot?: string): CodeIndex;
15
+ /** Persist the index to .swarm/codegraph.json under `root`. */
16
+ export declare function saveIndex(root: string, index: CodeIndex): void;
17
+ /** Load the persisted index, or null if none/unreadable. */
18
+ export declare function loadIndex(root: string): CodeIndex | null;
19
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,80 @@
1
+ /**
2
+ * store.ts — the filesystem layer for codegraph: walk the tree, read sources,
3
+ * build the index (via the pure engine), and persist/load .swarm/codegraph.json.
4
+ * Shared by the `codegraph` CLI command and the codegraph MCP tools so the
5
+ * scan/persist behaviour can't drift between them.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { buildIndex } from './codegraph.js';
10
+ export const INDEX_REL = path.join('.swarm', 'codegraph.json');
11
+ const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs']);
12
+ const SKIP_DIRS = new Set([
13
+ 'node_modules', '.git', '.swarm', 'dist', 'dist-standalone', 'build',
14
+ 'coverage', '.next', '.turbo', 'out', 'vendor', '.cache',
15
+ ]);
16
+ /** Recursively collect source files under `root`, skipping vendor/build dirs and .d.ts. */
17
+ export function walkSourceFiles(root) {
18
+ const found = [];
19
+ const stack = [root];
20
+ while (stack.length) {
21
+ const dir = stack.pop();
22
+ let entries;
23
+ try {
24
+ entries = fs.readdirSync(dir, { withFileTypes: true });
25
+ }
26
+ catch {
27
+ continue;
28
+ }
29
+ for (const e of entries) {
30
+ const full = path.join(dir, e.name);
31
+ if (e.isDirectory()) {
32
+ if (SKIP_DIRS.has(e.name))
33
+ continue;
34
+ stack.push(full);
35
+ }
36
+ else if (e.isFile()) {
37
+ const ext = path.extname(e.name);
38
+ if (SOURCE_EXT.has(ext) && !e.name.endsWith('.d.ts'))
39
+ found.push(full);
40
+ }
41
+ }
42
+ }
43
+ return found;
44
+ }
45
+ function safeRead(abs) {
46
+ try {
47
+ return fs.readFileSync(abs, 'utf8');
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ export function indexPath(root) {
54
+ return path.join(root, INDEX_REL);
55
+ }
56
+ /** Scan a repo (or a subpath of it) and build the index. `root` is the repo
57
+ * root used for relative paths; `scanRoot` (default = root) is where to walk. */
58
+ export function scanRepo(root, scanRoot = root) {
59
+ const files = walkSourceFiles(scanRoot);
60
+ const read = files
61
+ .map((abs) => ({ file: path.relative(root, abs), source: safeRead(abs) }))
62
+ .filter((f) => f.source !== null);
63
+ return buildIndex(read);
64
+ }
65
+ /** Persist the index to .swarm/codegraph.json under `root`. */
66
+ export function saveIndex(root, index) {
67
+ const p = indexPath(root);
68
+ fs.mkdirSync(path.dirname(p), { recursive: true });
69
+ fs.writeFileSync(p, JSON.stringify(index));
70
+ }
71
+ /** Load the persisted index, or null if none/unreadable. */
72
+ export function loadIndex(root) {
73
+ try {
74
+ return JSON.parse(fs.readFileSync(indexPath(root), 'utf8'));
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ //# sourceMappingURL=store.js.map
@@ -11,59 +11,10 @@
11
11
  * Engine (../codegraph/codegraph.ts) is pure + tested; this layer does the fs
12
12
  * walk and JSON persistence. MVP: exported top-level symbols in TS/JS.
13
13
  */
14
- import * as fs from 'node:fs';
15
14
  import * as path from 'node:path';
16
15
  import { output } from '../output.js';
17
- import { buildIndex, queryIndex, symbolsInFile, indexStats, } from '../codegraph/codegraph.js';
18
- const INDEX_REL = path.join('.swarm', 'codegraph.json');
19
- const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs']);
20
- const SKIP_DIRS = new Set([
21
- 'node_modules', '.git', '.swarm', 'dist', 'dist-standalone', 'build',
22
- 'coverage', '.next', '.turbo', 'out', 'vendor', '.cache',
23
- ]);
24
- function walkSourceFiles(root) {
25
- const found = [];
26
- const stack = [root];
27
- while (stack.length) {
28
- const dir = stack.pop();
29
- let entries;
30
- try {
31
- entries = fs.readdirSync(dir, { withFileTypes: true });
32
- }
33
- catch {
34
- continue;
35
- }
36
- for (const e of entries) {
37
- if (e.name.startsWith('.') && e.name !== '.') {
38
- if (e.isDirectory() && SKIP_DIRS.has(e.name))
39
- continue;
40
- }
41
- const full = path.join(dir, e.name);
42
- if (e.isDirectory()) {
43
- if (SKIP_DIRS.has(e.name))
44
- continue;
45
- stack.push(full);
46
- }
47
- else if (e.isFile()) {
48
- const ext = path.extname(e.name);
49
- if (SOURCE_EXT.has(ext) && !e.name.endsWith('.d.ts'))
50
- found.push(full);
51
- }
52
- }
53
- }
54
- return found;
55
- }
56
- function indexPath(root) {
57
- return path.join(root, INDEX_REL);
58
- }
59
- function loadIndex(root) {
60
- try {
61
- return JSON.parse(fs.readFileSync(indexPath(root), 'utf8'));
62
- }
63
- catch {
64
- return null;
65
- }
66
- }
16
+ import { queryIndex, symbolsInFile, indexStats, } from '../codegraph/codegraph.js';
17
+ import { INDEX_REL, scanRepo, saveIndex, loadIndex } from '../codegraph/store.js';
67
18
  function fmtSymbol(s) {
68
19
  return `${s.file}:${s.line} ${output.dim(`[${s.kind}]`)} ${s.signature}`;
69
20
  }
@@ -76,12 +27,8 @@ const indexCommand = {
76
27
  action: async (ctx) => {
77
28
  const root = ctx.cwd || process.cwd();
78
29
  const scanRoot = ctx.args[0] ? path.resolve(root, ctx.args[0]) : root;
79
- const files = walkSourceFiles(scanRoot);
80
- const read = files.map((abs) => ({ file: path.relative(root, abs), source: safeRead(abs) }));
81
- const index = buildIndex(read.filter((f) => f.source !== null));
82
- const dir = path.dirname(indexPath(root));
83
- fs.mkdirSync(dir, { recursive: true });
84
- fs.writeFileSync(indexPath(root), JSON.stringify(index));
30
+ const index = scanRepo(root, scanRoot);
31
+ saveIndex(root, index);
85
32
  const stats = indexStats(index);
86
33
  if (ctx.flags.json === true) {
87
34
  output.printJson({ ...stats, path: INDEX_REL });
@@ -92,14 +39,6 @@ const indexCommand = {
92
39
  return { success: true, exitCode: 0 };
93
40
  },
94
41
  };
95
- function safeRead(abs) {
96
- try {
97
- return fs.readFileSync(abs, 'utf8');
98
- }
99
- catch {
100
- return null;
101
- }
102
- }
103
42
  const queryCommand = {
104
43
  name: 'query',
105
44
  aliases: ['q'],
@@ -22,6 +22,9 @@ const commandLoaders = {
22
22
  // Deterministic command-output compression (rtk/headroom demand) — a
23
23
  // zero-token stream filter, distinct from caveman `compress` for files.
24
24
  compact: () => import('./compact.js'),
25
+ // Secret detection + redaction on the agent data path (gitleaks/trufflehog
26
+ // demand) — mask API keys/tokens before they reach an LLM/log/memory.
27
+ redact: () => import('./redact.js'),
25
28
  // Queryable exported-symbol index (codegraph demand) — where things are
26
29
  // defined without grep+read round-trips.
27
30
  codegraph: () => import('./codegraph.js'),
@@ -255,7 +258,7 @@ export async function getCommandsByCategory() {
255
258
  // three slots from 'statusline' onward (completionsCmd received the
256
259
  // statusline module, analyzeCmd received completions, …), scrambling the
257
260
  // categorized help. Every load now has a named slot.
258
- const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd,] = await Promise.all([
261
+ const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd,] = await Promise.all([
259
262
  loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
260
263
  loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
261
264
  loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
@@ -263,7 +266,7 @@ export async function getCommandsByCategory() {
263
266
  loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
264
267
  loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
265
268
  loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
266
- loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'),
269
+ loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'),
267
270
  ]);
268
271
  return {
269
272
  primary: [
@@ -279,7 +282,7 @@ export async function getCommandsByCategory() {
279
282
  utility: [
280
283
  configCmd, doctorCmd, daemonCmd, completionsCmd,
281
284
  migrateCmd, workflowCmd, demoCmd,
282
- statuslineCmd, compressCmd, compactCmd, efficiencyCmd,
285
+ statuslineCmd, compressCmd, compactCmd, redactCmd, efficiencyCmd,
283
286
  ].filter(Boolean),
284
287
  analysis: [
285
288
  analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `swarmdo redact` — strip secrets from a stream before it reaches an LLM, a
3
+ * log, or memory. Sibling of `compact`: same stdin-filter / command-wrap shape,
4
+ * but instead of de-noising it detects API keys / tokens / private keys (rule
5
+ * catalog + entropy fallback) and masks them. Deterministic, zero tokens.
6
+ *
7
+ * cat deploy.log | swarmdo redact # stdin filter → redacted stdout
8
+ * swarmdo redact -- npm run deploy # wrap a command (stdout+stderr)
9
+ * swarmdo redact --scan -- terraform apply # scan only; exit 1 if secrets found (CI gate)
10
+ *
11
+ * Redacted text → stdout (pipeable); a one-line summary → stderr unless --quiet.
12
+ * In --scan mode nothing is rewritten and the process exits non-zero when any
13
+ * secret is present, so it gates CI. See ../redact/redact.ts.
14
+ */
15
+ import type { Command } from '../types.js';
16
+ export declare const redactCommand: Command;
17
+ export default redactCommand;
18
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `swarmdo redact` — strip secrets from a stream before it reaches an LLM, a
3
+ * log, or memory. Sibling of `compact`: same stdin-filter / command-wrap shape,
4
+ * but instead of de-noising it detects API keys / tokens / private keys (rule
5
+ * catalog + entropy fallback) and masks them. Deterministic, zero tokens.
6
+ *
7
+ * cat deploy.log | swarmdo redact # stdin filter → redacted stdout
8
+ * swarmdo redact -- npm run deploy # wrap a command (stdout+stderr)
9
+ * swarmdo redact --scan -- terraform apply # scan only; exit 1 if secrets found (CI gate)
10
+ *
11
+ * Redacted text → stdout (pipeable); a one-line summary → stderr unless --quiet.
12
+ * In --scan mode nothing is rewritten and the process exits non-zero when any
13
+ * secret is present, so it gates CI. See ../redact/redact.ts.
14
+ */
15
+ import { spawnSync } from 'node:child_process';
16
+ import { output } from '../output.js';
17
+ import { redactText, scanText, formatFindingsSummary } from '../redact/redact.js';
18
+ function readStdin() {
19
+ return new Promise((resolve) => {
20
+ const chunks = [];
21
+ process.stdin.on('data', (c) => chunks.push(Buffer.from(c)));
22
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
23
+ process.stdin.on('error', () => resolve(Buffer.concat(chunks).toString('utf8')));
24
+ });
25
+ }
26
+ function optsFromFlags(ctx) {
27
+ const opts = { entropy: ctx.flags['no-entropy'] !== true };
28
+ const keep = ctx.flags.keep;
29
+ if (typeof keep === 'number')
30
+ opts.keepPrefix = keep;
31
+ else if (typeof keep === 'string')
32
+ opts.keepPrefix = parseInt(keep, 10) || 0;
33
+ if (typeof ctx.flags.token === 'string')
34
+ opts.token = ctx.flags.token;
35
+ if (typeof ctx.flags.threshold === 'string')
36
+ opts.entropyThreshold = parseFloat(ctx.flags.threshold);
37
+ const allow = ctx.flags.allow;
38
+ if (typeof allow === 'string' && allow)
39
+ opts.allowlist = allow.split(',').map((s) => s.trim()).filter(Boolean);
40
+ return opts;
41
+ }
42
+ async function run(ctx) {
43
+ const opts = optsFromFlags(ctx);
44
+ const quiet = ctx.flags.quiet === true;
45
+ const scanMode = ctx.flags.scan === true;
46
+ const asJson = ctx.flags.json === true;
47
+ // Gather input: either the wrapped command's combined output, or stdin.
48
+ let input;
49
+ let wrappedCode = 0;
50
+ const wrapping = ctx.args.length > 0;
51
+ if (wrapping) {
52
+ const [cmd, ...cmdArgs] = ctx.args;
53
+ const r = spawnSync(cmd, cmdArgs, {
54
+ cwd: ctx.cwd,
55
+ encoding: 'utf8',
56
+ maxBuffer: 256 * 1024 * 1024,
57
+ stdio: ['inherit', 'pipe', 'pipe'],
58
+ });
59
+ if (r.error) {
60
+ output.printError(`failed to run ${cmd}: ${r.error.message}`);
61
+ return { success: false, exitCode: 127 };
62
+ }
63
+ input = (r.stdout || '') + (r.stderr || '');
64
+ wrappedCode = r.status ?? (r.signal ? 1 : 0);
65
+ }
66
+ else {
67
+ if (process.stdin.isTTY) {
68
+ output.writeln(output.error('Usage: swarmdo redact [--scan] -- <command> OR <command> | swarmdo redact'));
69
+ return { success: false, exitCode: 1 };
70
+ }
71
+ input = await readStdin();
72
+ }
73
+ // Scan mode: report findings, never rewrite; exit non-zero if any secret.
74
+ if (scanMode) {
75
+ const findings = scanText(input, opts);
76
+ if (asJson) {
77
+ process.stdout.write(JSON.stringify({ count: findings.length, findings }, null, 2) + '\n');
78
+ }
79
+ else if (findings.length === 0) {
80
+ if (!quiet)
81
+ process.stderr.write('redact: no secrets found\n');
82
+ }
83
+ else {
84
+ for (const f of findings) {
85
+ process.stderr.write(`${f.line}:${f.column} ${f.ruleId} ${f.description}\n`);
86
+ }
87
+ if (!quiet)
88
+ process.stderr.write(formatFindingsSummary(findings) + '\n');
89
+ }
90
+ // A wrapped command that itself failed should still surface its failure.
91
+ const code = findings.length > 0 ? 1 : wrappedCode;
92
+ return { success: code === 0, exitCode: code };
93
+ }
94
+ // Redact mode: rewrite and emit.
95
+ const { output: redacted, findings } = redactText(input, opts);
96
+ process.stdout.write(redacted);
97
+ if (asJson) {
98
+ process.stderr.write(JSON.stringify({ count: findings.length, findings }, null, 2) + '\n');
99
+ }
100
+ else if (!quiet) {
101
+ process.stderr.write(formatFindingsSummary(findings) + '\n');
102
+ }
103
+ // In wrap mode propagate the command's exit code; in filter mode success.
104
+ return { success: wrappedCode === 0, exitCode: wrapping ? wrappedCode : 0 };
105
+ }
106
+ export const redactCommand = {
107
+ name: 'redact',
108
+ description: 'Detect & mask secrets (API keys, tokens, private keys) in a stream before it reaches an LLM/log/memory — deterministic, zero tokens',
109
+ options: [
110
+ { name: 'scan', description: 'scan only: report findings and exit 1 if any secret is present (CI gate), never rewrite', type: 'boolean' },
111
+ { name: 'json', description: 'emit findings as JSON', type: 'boolean' },
112
+ { name: 'keep', description: 'keep this many leading chars of each secret (default 3; 0 = full mask)', type: 'string' },
113
+ { name: 'token', description: 'replacement token after the kept prefix (default [REDACTED])', type: 'string' },
114
+ { name: 'no-entropy', description: 'disable the high-entropy keyword=value fallback', type: 'boolean' },
115
+ { name: 'threshold', description: 'entropy threshold in bits/char for the fallback (default 3.5)', type: 'string' },
116
+ { name: 'allow', description: 'comma-separated allowlist substrings; matching secrets are left untouched', type: 'string' },
117
+ { name: 'quiet', description: 'suppress the summary on stderr', type: 'boolean' },
118
+ ],
119
+ examples: [
120
+ { command: 'cat deploy.log | swarmdo redact', description: 'Mask secrets in a log on stdin' },
121
+ { command: 'swarmdo redact -- npm run deploy', description: 'Wrap a command; redact its output, exit code propagates' },
122
+ { command: 'swarmdo redact --scan -- terraform apply', description: 'Fail CI if the output contains secrets' },
123
+ { command: 'swarmdo redact --keep 0 --json', description: 'Full-mask and emit JSON findings' },
124
+ ],
125
+ action: run,
126
+ };
127
+ export default redactCommand;
128
+ //# sourceMappingURL=redact.js.map
@@ -19,6 +19,8 @@ import { sessionTools } from './mcp-tools/session-tools.js';
19
19
  import { hiveMindTools } from './mcp-tools/hive-mind-tools.js';
20
20
  import { workflowTools } from './mcp-tools/workflow-tools.js';
21
21
  import { analyzeTools } from './mcp-tools/analyze-tools.js';
22
+ import { codegraphTools } from './mcp-tools/codegraph-tools.js';
23
+ import { redactTools } from './mcp-tools/redact-tools.js';
22
24
  import { progressTools } from './mcp-tools/progress-tools.js';
23
25
  import { embeddingsTools } from './mcp-tools/embeddings-tools.js';
24
26
  import { claimsTools } from './mcp-tools/claims-tools.js';
@@ -102,6 +104,8 @@ const TOOL_GROUPS = {
102
104
  'hive-mind': () => hiveMindTools,
103
105
  workflow: () => workflowTools,
104
106
  analyze: () => analyzeTools,
107
+ codegraph: () => codegraphTools,
108
+ redact: () => redactTools,
105
109
  progress: () => progressTools,
106
110
  embeddings: () => embeddingsTools,
107
111
  claims: () => claimsTools,
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Codegraph MCP Tools
3
+ *
4
+ * Make the exported-symbol index (see ../codegraph) agent-native: an agent can
5
+ * ask "where is X defined?" / "what does file Y export?" in-session via MCP
6
+ * instead of shelling out to `swarmdo codegraph` or grep+read. Reads the same
7
+ * .swarm/codegraph.json the CLI writes (shared fs layer in codegraph/store.ts).
8
+ */
9
+ import type { MCPTool } from './types.js';
10
+ export declare const codegraphTools: MCPTool[];
11
+ //# sourceMappingURL=codegraph-tools.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Codegraph MCP Tools
3
+ *
4
+ * Make the exported-symbol index (see ../codegraph) agent-native: an agent can
5
+ * ask "where is X defined?" / "what does file Y export?" in-session via MCP
6
+ * instead of shelling out to `swarmdo codegraph` or grep+read. Reads the same
7
+ * .swarm/codegraph.json the CLI writes (shared fs layer in codegraph/store.ts).
8
+ */
9
+ import { queryIndex, symbolsInFile, indexStats } from '../codegraph/codegraph.js';
10
+ import { INDEX_REL, scanRepo, saveIndex, loadIndex } from '../codegraph/store.js';
11
+ const KINDS = ['function', 'class', 'interface', 'type', 'const', 'enum', 'default'];
12
+ /** Resolve the repo root for scan/load. Params may override; default = cwd. */
13
+ function rootOf(params) {
14
+ const p = params.root;
15
+ return typeof p === 'string' && p ? p : process.cwd();
16
+ }
17
+ const codegraphIndexTool = {
18
+ name: 'codegraph_index',
19
+ description: 'Scan the codebase for exported symbols and persist a queryable index to .swarm/codegraph.json. Run this once before codegraph_query/codegraph_file (or when files have changed). Use instead of a manual grep sweep when you want a structured symbol map.',
20
+ category: 'codegraph',
21
+ tags: ['code', 'symbols', 'index', 'navigation'],
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ root: { type: 'string', description: 'Repo root (default: current working directory)' },
26
+ path: { type: 'string', description: 'Subpath to scan, relative to root (default: whole repo)' },
27
+ },
28
+ },
29
+ handler: async (params) => {
30
+ const root = rootOf(params);
31
+ const scanRoot = typeof params.path === 'string' && params.path ? `${root}/${params.path}` : root;
32
+ try {
33
+ const index = scanRepo(root, scanRoot);
34
+ saveIndex(root, index);
35
+ return { ok: true, indexPath: INDEX_REL, ...indexStats(index) };
36
+ }
37
+ catch (e) {
38
+ return { error: true, message: e instanceof Error ? e.message : String(e) };
39
+ }
40
+ },
41
+ };
42
+ const codegraphQueryTool = {
43
+ name: 'codegraph_query',
44
+ description: 'Find where a symbol is defined by name — returns file, line, kind, and signature. Prefer this over grep for "where is X defined": it hits a prebuilt index (no file reads) and disambiguates by kind. Requires a prior codegraph_index. Use fuzzy for substring/partial names.',
45
+ category: 'codegraph',
46
+ tags: ['code', 'symbols', 'query', 'navigation'],
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ name: { type: 'string', description: 'Symbol name to find' },
51
+ fuzzy: { type: 'boolean', description: 'Substring, case-insensitive match instead of exact', default: false },
52
+ kind: { type: 'string', description: `Restrict to one kind: ${KINDS.join(', ')}`, enum: KINDS },
53
+ root: { type: 'string', description: 'Repo root (default: current working directory)' },
54
+ },
55
+ required: ['name'],
56
+ },
57
+ handler: async (params) => {
58
+ const root = rootOf(params);
59
+ const name = params.name;
60
+ if (typeof name !== 'string' || !name)
61
+ return { error: true, message: 'name is required' };
62
+ const index = loadIndex(root);
63
+ if (!index)
64
+ return { error: true, message: 'no index — run codegraph_index first' };
65
+ const hits = queryIndex(index, name, {
66
+ fuzzy: params.fuzzy === true,
67
+ kind: params.kind,
68
+ });
69
+ return { count: hits.length, symbols: hits };
70
+ },
71
+ };
72
+ const codegraphFileTool = {
73
+ name: 'codegraph_file',
74
+ description: 'List the symbols a specific file exports (name, line, kind) from the index — a fast "what does this module expose?" without reading the file. Requires a prior codegraph_index.',
75
+ category: 'codegraph',
76
+ tags: ['code', 'symbols', 'file', 'navigation'],
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ file: { type: 'string', description: 'Repo-relative file path (as indexed, e.g. src/index.ts)' },
81
+ root: { type: 'string', description: 'Repo root (default: current working directory)' },
82
+ },
83
+ required: ['file'],
84
+ },
85
+ handler: async (params) => {
86
+ const root = rootOf(params);
87
+ const file = params.file;
88
+ if (typeof file !== 'string' || !file)
89
+ return { error: true, message: 'file is required' };
90
+ const index = loadIndex(root);
91
+ if (!index)
92
+ return { error: true, message: 'no index — run codegraph_index first' };
93
+ const hits = symbolsInFile(index, file);
94
+ return { file, count: hits.length, symbols: hits };
95
+ },
96
+ };
97
+ const codegraphStatsTool = {
98
+ name: 'codegraph_stats',
99
+ description: 'Summary of the current symbol index: file count, symbol count, and a breakdown by kind. Requires a prior codegraph_index.',
100
+ category: 'codegraph',
101
+ tags: ['code', 'symbols', 'stats'],
102
+ inputSchema: {
103
+ type: 'object',
104
+ properties: {
105
+ root: { type: 'string', description: 'Repo root (default: current working directory)' },
106
+ },
107
+ },
108
+ handler: async (params) => {
109
+ const root = rootOf(params);
110
+ const index = loadIndex(root);
111
+ if (!index)
112
+ return { error: true, message: 'no index — run codegraph_index first' };
113
+ return indexStats(index);
114
+ },
115
+ };
116
+ export const codegraphTools = [
117
+ codegraphIndexTool,
118
+ codegraphQueryTool,
119
+ codegraphFileTool,
120
+ codegraphStatsTool,
121
+ ];
122
+ //# sourceMappingURL=codegraph-tools.js.map
@@ -15,6 +15,8 @@ export { hiveMindTools } from './hive-mind-tools.js';
15
15
  export { workflowTools } from './workflow-tools.js';
16
16
  export { coverageRouterTools } from '../swarmvector/coverage-tools.js';
17
17
  export { analyzeTools } from './analyze-tools.js';
18
+ export { codegraphTools } from './codegraph-tools.js';
19
+ export { redactTools } from './redact-tools.js';
18
20
  export { progressTools } from './progress-tools.js';
19
21
  export { transferTools } from './transfer-tools.js';
20
22
  export { securityTools } from './security-tools.js';
@@ -14,6 +14,8 @@ export { hiveMindTools } from './hive-mind-tools.js';
14
14
  export { workflowTools } from './workflow-tools.js';
15
15
  export { coverageRouterTools } from '../swarmvector/coverage-tools.js';
16
16
  export { analyzeTools } from './analyze-tools.js';
17
+ export { codegraphTools } from './codegraph-tools.js';
18
+ export { redactTools } from './redact-tools.js';
17
19
  export { progressTools } from './progress-tools.js';
18
20
  export { transferTools } from './transfer-tools.js';
19
21
  export { securityTools } from './security-tools.js';
@@ -22,6 +22,8 @@ const LEAN_GROUPS = [
22
22
  'hooks', // hooks_route / pre-task / post-edit / codemod ...
23
23
  'agentdb', // agentdb pattern store/search
24
24
  'embeddings', // embeddings_generate / search / compare
25
+ 'codegraph', // codegraph_query / file / index — "where is X defined?"
26
+ 'redact', // redact_text / redact_scan — secret guard on the data path
25
27
  ];
26
28
  /** Balanced = lean + orchestration/session/system the average user reaches for. */
27
29
  const BALANCED_EXTRA = [
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Redact MCP Tools
3
+ *
4
+ * Give agents a secret guard on the data path: before writing tool output or
5
+ * context into memory / a message / a file, an agent can mask secrets
6
+ * (redact_text) or check for their presence (redact_scan). Deterministic —
7
+ * shares the pure engine in ../redact/redact.ts with the CLI command.
8
+ */
9
+ import type { MCPTool } from './types.js';
10
+ export declare const redactTools: MCPTool[];
11
+ //# sourceMappingURL=redact-tools.d.ts.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Redact MCP Tools
3
+ *
4
+ * Give agents a secret guard on the data path: before writing tool output or
5
+ * context into memory / a message / a file, an agent can mask secrets
6
+ * (redact_text) or check for their presence (redact_scan). Deterministic —
7
+ * shares the pure engine in ../redact/redact.ts with the CLI command.
8
+ */
9
+ import { redactText, scanText } from '../redact/redact.js';
10
+ function optsOf(params) {
11
+ const opts = { entropy: params.entropy !== false };
12
+ if (typeof params.keepPrefix === 'number')
13
+ opts.keepPrefix = params.keepPrefix;
14
+ if (typeof params.token === 'string')
15
+ opts.token = params.token;
16
+ if (typeof params.entropyThreshold === 'number')
17
+ opts.entropyThreshold = params.entropyThreshold;
18
+ if (Array.isArray(params.allowlist))
19
+ opts.allowlist = params.allowlist.filter((a) => typeof a === 'string');
20
+ return opts;
21
+ }
22
+ const redactTextTool = {
23
+ name: 'redact_text',
24
+ description: 'Mask secrets (API keys, tokens, private keys) in a string before you store it in memory, put it in a message, or write it to a file. Returns the redacted text plus what was found. Deterministic; run this on any untrusted or command-derived content you are about to persist or forward.',
25
+ category: 'redact',
26
+ tags: ['security', 'secrets', 'redaction', 'safety'],
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ text: { type: 'string', description: 'The content to redact' },
31
+ keepPrefix: { type: 'number', description: 'Keep this many leading chars of each secret (default 3; 0 = full mask)' },
32
+ token: { type: 'string', description: 'Replacement token after the kept prefix (default [REDACTED])' },
33
+ entropy: { type: 'boolean', description: 'Enable the high-entropy keyword=value fallback (default true)' },
34
+ entropyThreshold: { type: 'number', description: 'Entropy threshold in bits/char for the fallback (default 3.5)' },
35
+ allowlist: { type: 'array', items: { type: 'string' }, description: 'Substrings; matching secrets are left untouched' },
36
+ },
37
+ required: ['text'],
38
+ },
39
+ handler: async (params) => {
40
+ if (typeof params.text !== 'string')
41
+ return { error: true, message: 'text is required' };
42
+ const { output, findings } = redactText(params.text, optsOf(params));
43
+ return { redacted: output, count: findings.length, findings };
44
+ },
45
+ };
46
+ const redactScanTool = {
47
+ name: 'redact_scan',
48
+ description: 'Check a string for secrets WITHOUT rewriting it — returns the list of findings (rule, line, column, description). Use to decide whether content is safe to store/forward, or to gate on it. Pair with redact_text when you actually need the masked output.',
49
+ category: 'redact',
50
+ tags: ['security', 'secrets', 'scan', 'safety'],
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ text: { type: 'string', description: 'The content to scan' },
55
+ entropy: { type: 'boolean', description: 'Enable the high-entropy keyword=value fallback (default true)' },
56
+ entropyThreshold: { type: 'number', description: 'Entropy threshold in bits/char for the fallback (default 3.5)' },
57
+ allowlist: { type: 'array', items: { type: 'string' }, description: 'Substrings to ignore' },
58
+ },
59
+ required: ['text'],
60
+ },
61
+ handler: async (params) => {
62
+ if (typeof params.text !== 'string')
63
+ return { error: true, message: 'text is required' };
64
+ const findings = scanText(params.text, optsOf(params));
65
+ return { clean: findings.length === 0, count: findings.length, findings };
66
+ },
67
+ };
68
+ export const redactTools = [redactTextTool, redactScanTool];
69
+ //# sourceMappingURL=redact-tools.js.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * redact.ts — deterministic secret detection + redaction for the agent data
3
+ * path. swarmdo pipes tool output, memory, and context into LLMs constantly;
4
+ * `compact` de-noises that stream but never redacts, and `security scan` is
5
+ * CVE-oriented, not a content filter. This engine is the missing guard: it
6
+ * catches API keys / tokens / private keys via a high-confidence rule catalog
7
+ * plus a Shannon-entropy fallback for keyword-prefixed assignments, and masks
8
+ * them before content reaches a model, a log, or memory.
9
+ *
10
+ * Pure + LLM-free by design — known-secret fixtures in, deterministic masked
11
+ * bytes out. The CLI wrapper (../commands/redact.ts) and an MCP tool share it.
12
+ */
13
+ export interface RedactRule {
14
+ /** stable slug, e.g. 'aws-access-key' */
15
+ id: string;
16
+ /** human description for --scan output */
17
+ description: string;
18
+ /** global regex; the whole match (or capture group `group`) is the secret */
19
+ regex: RegExp;
20
+ /** if set, redact this capture group rather than the whole match */
21
+ group?: number;
22
+ }
23
+ export interface Finding {
24
+ ruleId: string;
25
+ description: string;
26
+ /** 1-based line number */
27
+ line: number;
28
+ /** 1-based column of the secret within the line */
29
+ column: number;
30
+ /** the raw secret text that matched */
31
+ match: string;
32
+ /** what it was replaced with */
33
+ redacted: string;
34
+ }
35
+ export interface RedactOptions {
36
+ /** keep this many leading chars of a secret, mask the rest (default 3) */
37
+ keepPrefix?: number;
38
+ /** the replacement token appended after the kept prefix (default '[REDACTED]') */
39
+ token?: string;
40
+ /** enable the entropy fallback for keyword=value assignments (default true) */
41
+ entropy?: boolean;
42
+ /** Shannon-entropy threshold (bits/char) above which a keyworded value is a secret (default 3.5) */
43
+ entropyThreshold?: number;
44
+ /** substrings/regexes; any secret whose match includes one is left untouched */
45
+ allowlist?: Array<string | RegExp>;
46
+ }
47
+ export interface RedactResult {
48
+ output: string;
49
+ findings: Finding[];
50
+ }
51
+ /**
52
+ * High-confidence catalog. Ordered — more specific rules (e.g. Anthropic
53
+ * `sk-ant-`) come before broader ones (`sk-` OpenAI) so the specific rule wins
54
+ * and we don't double-count. Patterns are conservative to keep false positives
55
+ * low; the entropy fallback covers the long tail of custom secrets.
56
+ */
57
+ export declare const RULES: RedactRule[];
58
+ /** Shannon entropy in bits/char. Pure; used by the assignment fallback. */
59
+ export declare function shannonEntropy(s: string): number;
60
+ /** Mask a secret: keep `keepPrefix` leading chars, then the token. */
61
+ export declare function maskSecret(secret: string, opts?: RedactOptions): string;
62
+ /** Scan without rewriting — returns findings only (for --scan / CI gating). */
63
+ export declare function scanText(text: string, opts?: RedactOptions): Finding[];
64
+ /** Redact secrets in `text`, returning the rewritten output + findings. */
65
+ export declare function redactText(text: string, opts?: RedactOptions): RedactResult;
66
+ /** One-line human summary for stderr. */
67
+ export declare function formatFindingsSummary(findings: Finding[]): string;
68
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1,153 @@
1
+ /**
2
+ * redact.ts — deterministic secret detection + redaction for the agent data
3
+ * path. swarmdo pipes tool output, memory, and context into LLMs constantly;
4
+ * `compact` de-noises that stream but never redacts, and `security scan` is
5
+ * CVE-oriented, not a content filter. This engine is the missing guard: it
6
+ * catches API keys / tokens / private keys via a high-confidence rule catalog
7
+ * plus a Shannon-entropy fallback for keyword-prefixed assignments, and masks
8
+ * them before content reaches a model, a log, or memory.
9
+ *
10
+ * Pure + LLM-free by design — known-secret fixtures in, deterministic masked
11
+ * bytes out. The CLI wrapper (../commands/redact.ts) and an MCP tool share it.
12
+ */
13
+ /**
14
+ * High-confidence catalog. Ordered — more specific rules (e.g. Anthropic
15
+ * `sk-ant-`) come before broader ones (`sk-` OpenAI) so the specific rule wins
16
+ * and we don't double-count. Patterns are conservative to keep false positives
17
+ * low; the entropy fallback covers the long tail of custom secrets.
18
+ */
19
+ export const RULES = [
20
+ { id: 'private-key', description: 'Private key block header', regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g },
21
+ { id: 'aws-access-key', description: 'AWS access key ID', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}\b/g },
22
+ { id: 'github-pat', description: 'GitHub personal access token', regex: /\bghp_[0-9A-Za-z]{36}\b/g },
23
+ { id: 'github-oauth', description: 'GitHub OAuth/app token', regex: /\b(?:gho|ghu|ghs|ghr)_[0-9A-Za-z]{36}\b/g },
24
+ { id: 'github-fine-pat', description: 'GitHub fine-grained PAT', regex: /\bgithub_pat_[0-9A-Za-z_]{82}\b/g },
25
+ { id: 'gitlab-pat', description: 'GitLab personal access token', regex: /\bglpat-[0-9A-Za-z_-]{20}\b/g },
26
+ { id: 'anthropic-key', description: 'Anthropic API key', regex: /\bsk-ant-[0-9A-Za-z-]{16,}\b/g },
27
+ { id: 'openai-key', description: 'OpenAI API key', regex: /\bsk-(?:proj-)?[0-9A-Za-z]{20,}\b/g },
28
+ { id: 'google-api-key', description: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
29
+ { id: 'slack-token', description: 'Slack token', regex: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/g },
30
+ { id: 'slack-webhook', description: 'Slack incoming webhook', regex: /https:\/\/hooks\.slack\.com\/services\/T[0-9A-Za-z_]+\/B[0-9A-Za-z_]+\/[0-9A-Za-z_]+/g },
31
+ { id: 'stripe-key', description: 'Stripe secret/restricted key', regex: /\b(?:sk|rk)_live_[0-9A-Za-z]{24,}\b/g },
32
+ { id: 'sendgrid-key', description: 'SendGrid API key', regex: /\bSG\.[0-9A-Za-z_-]{22}\.[0-9A-Za-z_-]{43}\b/g },
33
+ { id: 'twilio-key', description: 'Twilio API key SID', regex: /\bSK[0-9a-fA-F]{32}\b/g },
34
+ { id: 'npm-token', description: 'npm access token', regex: /\bnpm_[0-9A-Za-z]{36}\b/g },
35
+ { id: 'jwt', description: 'JSON Web Token', regex: /\beyJ[0-9A-Za-z_-]{10,}\.eyJ[0-9A-Za-z_-]{10,}\.[0-9A-Za-z_-]{10,}\b/g },
36
+ ];
37
+ /** Keyword-prefixed assignment, e.g. `api_key = "..."` — the value is group 1. */
38
+ const ASSIGNMENT_RE = /(?:pass(?:word|wd)?|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|auth[_-]?token|client[_-]?secret)["']?\s*[:=]\s*["']?([^\s"'`,;]{8,})/gi;
39
+ /** Shannon entropy in bits/char. Pure; used by the assignment fallback. */
40
+ export function shannonEntropy(s) {
41
+ if (!s)
42
+ return 0;
43
+ const freq = new Map();
44
+ for (const ch of s)
45
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
46
+ let bits = 0;
47
+ for (const n of freq.values()) {
48
+ const p = n / s.length;
49
+ bits -= p * Math.log2(p);
50
+ }
51
+ return bits;
52
+ }
53
+ /** Mask a secret: keep `keepPrefix` leading chars, then the token. */
54
+ export function maskSecret(secret, opts = {}) {
55
+ const keep = Math.max(0, Math.min(opts.keepPrefix ?? 3, secret.length));
56
+ const token = opts.token ?? '[REDACTED]';
57
+ return secret.slice(0, keep) + token;
58
+ }
59
+ function isAllowlisted(match, allowlist) {
60
+ if (!allowlist || allowlist.length === 0)
61
+ return false;
62
+ return allowlist.some((a) => (typeof a === 'string' ? match.includes(a) : a.test(match)));
63
+ }
64
+ /** Collect all rule + entropy hits across the text, deduped by offset. */
65
+ function collectHits(text, opts) {
66
+ const hits = [];
67
+ const claimed = []; // [start,end) ranges already taken
68
+ const overlaps = (start, end) => claimed.some(([s, e]) => start < e && end > s);
69
+ for (const rule of RULES) {
70
+ rule.regex.lastIndex = 0;
71
+ let m;
72
+ while ((m = rule.regex.exec(text)) !== null) {
73
+ const secret = rule.group != null ? m[rule.group] : m[0];
74
+ if (secret == null)
75
+ continue;
76
+ const start = rule.group != null ? m.index + m[0].indexOf(secret) : m.index;
77
+ const end = start + secret.length;
78
+ if (overlaps(start, end))
79
+ continue;
80
+ if (isAllowlisted(secret, opts.allowlist))
81
+ continue;
82
+ claimed.push([start, end]);
83
+ hits.push({ ruleId: rule.id, description: rule.description, index: start, match: secret });
84
+ if (m.index === rule.regex.lastIndex)
85
+ rule.regex.lastIndex++; // guard zero-width
86
+ }
87
+ }
88
+ if (opts.entropy !== false) {
89
+ const threshold = opts.entropyThreshold ?? 3.5;
90
+ ASSIGNMENT_RE.lastIndex = 0;
91
+ let m;
92
+ while ((m = ASSIGNMENT_RE.exec(text)) !== null) {
93
+ const value = m[1];
94
+ const start = m.index + m[0].indexOf(value);
95
+ const end = start + value.length;
96
+ if (overlaps(start, end))
97
+ continue;
98
+ if (isAllowlisted(value, opts.allowlist))
99
+ continue;
100
+ if (shannonEntropy(value) < threshold)
101
+ continue;
102
+ claimed.push([start, end]);
103
+ hits.push({ ruleId: 'high-entropy-assignment', description: 'High-entropy secret assignment', index: start, match: value });
104
+ }
105
+ }
106
+ return hits.sort((a, b) => a.index - b.index);
107
+ }
108
+ /** Map an absolute offset to 1-based {line, column}. */
109
+ function lineColOf(text, index) {
110
+ let line = 1;
111
+ let last = -1;
112
+ for (let i = 0; i < index; i++) {
113
+ if (text[i] === '\n') {
114
+ line++;
115
+ last = i;
116
+ }
117
+ }
118
+ return { line, column: index - last };
119
+ }
120
+ /** Scan without rewriting — returns findings only (for --scan / CI gating). */
121
+ export function scanText(text, opts = {}) {
122
+ return collectHits(text, opts).map((h) => {
123
+ const { line, column } = lineColOf(text, h.index);
124
+ return { ruleId: h.ruleId, description: h.description, line, column, match: h.match, redacted: maskSecret(h.match, opts) };
125
+ });
126
+ }
127
+ /** Redact secrets in `text`, returning the rewritten output + findings. */
128
+ export function redactText(text, opts = {}) {
129
+ const hits = collectHits(text, opts);
130
+ const findings = [];
131
+ let out = '';
132
+ let cursor = 0;
133
+ for (const h of hits) {
134
+ const { line, column } = lineColOf(text, h.index);
135
+ const redacted = maskSecret(h.match, opts);
136
+ out += text.slice(cursor, h.index) + redacted;
137
+ cursor = h.index + h.match.length;
138
+ findings.push({ ruleId: h.ruleId, description: h.description, line, column, match: h.match, redacted });
139
+ }
140
+ out += text.slice(cursor);
141
+ return { output: out, findings };
142
+ }
143
+ /** One-line human summary for stderr. */
144
+ export function formatFindingsSummary(findings) {
145
+ if (findings.length === 0)
146
+ return 'redact: no secrets found';
147
+ const byRule = new Map();
148
+ for (const f of findings)
149
+ byRule.set(f.ruleId, (byRule.get(f.ruleId) ?? 0) + 1);
150
+ const parts = [...byRule.entries()].sort((a, b) => b[1] - a[1]).map(([r, n]) => `${r}:${n}`);
151
+ return `redact: ${findings.length} secret${findings.length === 1 ? '' : 's'} redacted (${parts.join(', ')})`;
152
+ }
153
+ //# sourceMappingURL=redact.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmdo/cli",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "description": "Swarmdo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",