swarmdo 1.8.0 → 1.10.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.
- package/.claude/helpers/statusline.cjs +1 -1
- package/README.md +3 -1
- package/package.json +1 -1
- package/v3/@swarmdo/cli/dist/src/commands/index.js +9 -3
- package/v3/@swarmdo/cli/dist/src/commands/pack.d.ts +18 -0
- package/v3/@swarmdo/cli/dist/src/commands/pack.js +184 -0
- package/v3/@swarmdo/cli/dist/src/commands/redact.d.ts +18 -0
- package/v3/@swarmdo/cli/dist/src/commands/redact.js +128 -0
- package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/profiles.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/redact-tools.d.ts +11 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/redact-tools.js +69 -0
- package/v3/@swarmdo/cli/dist/src/pack/pack.d.ts +59 -0
- package/v3/@swarmdo/cli/dist/src/pack/pack.js +178 -0
- package/v3/@swarmdo/cli/dist/src/redact/redact.d.ts +68 -0
- package/v3/@swarmdo/cli/dist/src/redact/redact.js +153 -0
- package/v3/@swarmdo/cli/package.json +1 -1
|
@@ -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.
|
|
518
|
+
let ver = '1.10.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
|
[](https://swarmdo.com)
|
|
4
4
|
|
|
5
|
-
[](https://img.shields.io/badge/npx%20swarmdo-v1.10.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
|
|
6
6
|
[](https://github.com/SwarmDo/swarmdo/blob/main/LICENSE)
|
|
7
7
|
[](https://swarmdo.com)
|
|
8
8
|
[](https://github.com/SwarmDo/swarmdo)
|
|
@@ -278,6 +278,8 @@ The recent release train added a full day-to-day operations layer around the swa
|
|
|
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
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 |
|
|
282
|
+
| `swarmdo pack` | **Bundle a repo into one AI-context blob** — walk the tree (respects `.gitignore` + glob `--include`/`--exclude`, skips binaries/node_modules), emit markdown/xml/json/plain with a directory tree and per-file + total token estimates. `swarmdo pack --tokens` for a budget breakdown; `--redact` masks secrets first. Deterministic |
|
|
281
283
|
| `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
284
|
| 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
285
|
| `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.
|
|
3
|
+
"version": "1.10.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",
|
|
@@ -22,6 +22,12 @@ 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'),
|
|
28
|
+
// Repo→AI-context bundle (repomix demand) — one promptable blob with tree +
|
|
29
|
+
// token counts; swarmdo's context-assembly primitive.
|
|
30
|
+
pack: () => import('./pack.js'),
|
|
25
31
|
// Queryable exported-symbol index (codegraph demand) — where things are
|
|
26
32
|
// defined without grep+read round-trips.
|
|
27
33
|
codegraph: () => import('./codegraph.js'),
|
|
@@ -255,7 +261,7 @@ export async function getCommandsByCategory() {
|
|
|
255
261
|
// three slots from 'statusline' onward (completionsCmd received the
|
|
256
262
|
// statusline module, analyzeCmd received completions, …), scrambling the
|
|
257
263
|
// 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([
|
|
264
|
+
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, packCmd,] = await Promise.all([
|
|
259
265
|
loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
|
|
260
266
|
loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
|
|
261
267
|
loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
|
|
@@ -263,7 +269,7 @@ export async function getCommandsByCategory() {
|
|
|
263
269
|
loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
|
|
264
270
|
loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
|
|
265
271
|
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'),
|
|
272
|
+
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'),
|
|
267
273
|
]);
|
|
268
274
|
return {
|
|
269
275
|
primary: [
|
|
@@ -279,7 +285,7 @@ export async function getCommandsByCategory() {
|
|
|
279
285
|
utility: [
|
|
280
286
|
configCmd, doctorCmd, daemonCmd, completionsCmd,
|
|
281
287
|
migrateCmd, workflowCmd, demoCmd,
|
|
282
|
-
statuslineCmd, compressCmd, compactCmd, efficiencyCmd,
|
|
288
|
+
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, efficiencyCmd,
|
|
283
289
|
].filter(Boolean),
|
|
284
290
|
analysis: [
|
|
285
291
|
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo pack` — bundle a repo (or subset) into one AI-friendly context blob.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo pack # markdown bundle of the repo → stdout
|
|
5
|
+
* swarmdo pack src --format xml -o ctx.xml
|
|
6
|
+
* swarmdo pack --include '*.ts' --exclude '*.test.ts'
|
|
7
|
+
* swarmdo pack --tokens # just per-file + total token counts
|
|
8
|
+
* swarmdo pack --redact # mask secrets in each file first
|
|
9
|
+
*
|
|
10
|
+
* The fs walk lives here (default skips + .gitignore + glob include/exclude +
|
|
11
|
+
* binary/size guards); the pure engine (../pack/pack.ts) does formatting +
|
|
12
|
+
* token accounting. --redact composes the ../redact engine so secrets never
|
|
13
|
+
* land in the bundle.
|
|
14
|
+
*/
|
|
15
|
+
import type { Command } from '../types.js';
|
|
16
|
+
export declare const packCommand: Command;
|
|
17
|
+
export default packCommand;
|
|
18
|
+
//# sourceMappingURL=pack.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo pack` — bundle a repo (or subset) into one AI-friendly context blob.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo pack # markdown bundle of the repo → stdout
|
|
5
|
+
* swarmdo pack src --format xml -o ctx.xml
|
|
6
|
+
* swarmdo pack --include '*.ts' --exclude '*.test.ts'
|
|
7
|
+
* swarmdo pack --tokens # just per-file + total token counts
|
|
8
|
+
* swarmdo pack --redact # mask secrets in each file first
|
|
9
|
+
*
|
|
10
|
+
* The fs walk lives here (default skips + .gitignore + glob include/exclude +
|
|
11
|
+
* binary/size guards); the pure engine (../pack/pack.ts) does formatting +
|
|
12
|
+
* token accounting. --redact composes the ../redact engine so secrets never
|
|
13
|
+
* land in the bundle.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { output } from '../output.js';
|
|
18
|
+
import { packFiles, makeIgnoreMatcher } from '../pack/pack.js';
|
|
19
|
+
import { redactText } from '../redact/redact.js';
|
|
20
|
+
const SKIP_DIRS = new Set([
|
|
21
|
+
'node_modules', '.git', '.swarm', 'dist', 'dist-standalone', 'build',
|
|
22
|
+
'coverage', '.next', '.turbo', 'out', 'vendor', '.cache',
|
|
23
|
+
]);
|
|
24
|
+
const BINARY_EXT = new Set([
|
|
25
|
+
'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'pdf', 'zip', 'gz', 'tar', 'tgz',
|
|
26
|
+
'woff', 'woff2', 'ttf', 'eot', 'mp4', 'mov', 'mp3', 'wav', 'wasm', 'so', 'dylib',
|
|
27
|
+
'dll', 'exe', 'bin', 'node', 'lock', 'jpeg',
|
|
28
|
+
]);
|
|
29
|
+
/** Looks binary if it has a NUL in the first 8 KiB. */
|
|
30
|
+
function looksBinary(buf) {
|
|
31
|
+
const n = Math.min(buf.length, 8192);
|
|
32
|
+
for (let i = 0; i < n; i++)
|
|
33
|
+
if (buf[i] === 0)
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function walk(o) {
|
|
38
|
+
const files = [];
|
|
39
|
+
const stack = [o.root];
|
|
40
|
+
while (stack.length) {
|
|
41
|
+
const dir = stack.pop();
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const e of entries) {
|
|
50
|
+
const full = path.join(dir, e.name);
|
|
51
|
+
const rel = path.relative(o.root, full).split(path.sep).join('/');
|
|
52
|
+
if (e.isDirectory()) {
|
|
53
|
+
if (SKIP_DIRS.has(e.name))
|
|
54
|
+
continue;
|
|
55
|
+
if (o.gitignore?.(rel + '/'))
|
|
56
|
+
continue;
|
|
57
|
+
stack.push(full);
|
|
58
|
+
}
|
|
59
|
+
else if (e.isFile()) {
|
|
60
|
+
if (o.gitignore?.(rel))
|
|
61
|
+
continue;
|
|
62
|
+
if (o.exclude?.(rel))
|
|
63
|
+
continue;
|
|
64
|
+
if (o.include && !o.include(rel))
|
|
65
|
+
continue;
|
|
66
|
+
if (BINARY_EXT.has(e.name.slice(e.name.lastIndexOf('.') + 1).toLowerCase()))
|
|
67
|
+
continue;
|
|
68
|
+
let stat;
|
|
69
|
+
try {
|
|
70
|
+
stat = fs.statSync(full);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (stat.size > o.maxBytes)
|
|
76
|
+
continue;
|
|
77
|
+
let buf;
|
|
78
|
+
try {
|
|
79
|
+
buf = fs.readFileSync(full);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (looksBinary(buf))
|
|
85
|
+
continue;
|
|
86
|
+
files.push({ path: rel, content: buf.toString('utf8') });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
function csv(v) {
|
|
93
|
+
if (typeof v !== 'string' || !v)
|
|
94
|
+
return undefined;
|
|
95
|
+
return v.split(',').map((s) => s.trim()).filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
async function run(ctx) {
|
|
98
|
+
const repoRoot = ctx.cwd || process.cwd();
|
|
99
|
+
const scanRoot = ctx.args[0] ? path.resolve(repoRoot, ctx.args[0]) : repoRoot;
|
|
100
|
+
// NB: use --style, not --format — `format` is a global flag (text/json/table,
|
|
101
|
+
// default 'text') that would shadow ours (#1425 removed its short alias).
|
|
102
|
+
const format = (typeof ctx.flags.style === 'string' ? ctx.flags.style : 'md');
|
|
103
|
+
if (!['md', 'xml', 'json', 'plain'].includes(format)) {
|
|
104
|
+
output.printError(`unknown --style '${format}' (use md|xml|json|plain)`);
|
|
105
|
+
return { success: false, exitCode: 1 };
|
|
106
|
+
}
|
|
107
|
+
const maxKb = typeof ctx.flags['max-file-size'] === 'string' ? parseInt(ctx.flags['max-file-size'], 10) : 512;
|
|
108
|
+
const maxBytes = (Number.isFinite(maxKb) ? maxKb : 512) * 1024;
|
|
109
|
+
const includePats = csv(ctx.flags.include);
|
|
110
|
+
const excludePats = csv(ctx.flags.exclude);
|
|
111
|
+
let gitignore;
|
|
112
|
+
if (ctx.flags['no-gitignore'] !== true) {
|
|
113
|
+
try {
|
|
114
|
+
const gi = fs.readFileSync(path.join(scanRoot, '.gitignore'), 'utf8').split('\n');
|
|
115
|
+
gitignore = makeIgnoreMatcher(gi);
|
|
116
|
+
}
|
|
117
|
+
catch { /* no .gitignore */ }
|
|
118
|
+
}
|
|
119
|
+
const files = walk({
|
|
120
|
+
root: scanRoot,
|
|
121
|
+
include: includePats ? makeIgnoreMatcher(includePats) : undefined,
|
|
122
|
+
exclude: excludePats ? makeIgnoreMatcher(excludePats) : undefined,
|
|
123
|
+
gitignore,
|
|
124
|
+
maxBytes,
|
|
125
|
+
});
|
|
126
|
+
if (files.length === 0) {
|
|
127
|
+
output.printError('no files matched — check the path, --include/--exclude, or .gitignore');
|
|
128
|
+
return { success: false, exitCode: 1 };
|
|
129
|
+
}
|
|
130
|
+
const opts = { format, tree: ctx.flags['no-tree'] !== true };
|
|
131
|
+
if (ctx.flags.redact === true) {
|
|
132
|
+
opts.transform = (content) => redactText(content).output;
|
|
133
|
+
}
|
|
134
|
+
const { output: bundle, stats } = packFiles(files, opts);
|
|
135
|
+
// --tokens: report accounting only, no bundle.
|
|
136
|
+
if (ctx.flags.tokens === true) {
|
|
137
|
+
if (ctx.flags.json === true) {
|
|
138
|
+
output.printJson(stats);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
for (const f of [...stats.perFile].sort((a, b) => b.tokens - a.tokens)) {
|
|
142
|
+
output.writeln(`${String(f.tokens).padStart(8)} ${f.path}`);
|
|
143
|
+
}
|
|
144
|
+
output.writeln(output.bold(`${String(stats.tokens).padStart(8)} TOTAL (${stats.files} files, ${(stats.bytes / 1024).toFixed(1)} KiB)`));
|
|
145
|
+
}
|
|
146
|
+
return { success: true, exitCode: 0 };
|
|
147
|
+
}
|
|
148
|
+
const outFile = (typeof ctx.flags.output === 'string' && ctx.flags.output) || (typeof ctx.flags.o === 'string' && ctx.flags.o);
|
|
149
|
+
if (outFile) {
|
|
150
|
+
fs.writeFileSync(path.resolve(repoRoot, outFile), bundle);
|
|
151
|
+
output.printSuccess(`packed ${stats.files} files (~${stats.tokens} tokens) → ${outFile}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
process.stdout.write(bundle);
|
|
155
|
+
if (!process.stdout.isTTY) { /* piped: keep stdout clean */ }
|
|
156
|
+
process.stderr.write(output.dim(`packed ${stats.files} files (~${stats.tokens} tokens)\n`));
|
|
157
|
+
}
|
|
158
|
+
return { success: true, exitCode: 0 };
|
|
159
|
+
}
|
|
160
|
+
export const packCommand = {
|
|
161
|
+
name: 'pack',
|
|
162
|
+
description: 'Bundle a repo (or subset) into one AI-friendly context blob (md/xml/json/plain) with a tree + token counts — deterministic',
|
|
163
|
+
options: [
|
|
164
|
+
{ name: 'style', description: 'output format: md (default), xml, json, plain', type: 'string' },
|
|
165
|
+
{ name: 'include', description: 'comma-separated globs; keep only matching files (e.g. "*.ts,*.md")', type: 'string' },
|
|
166
|
+
{ name: 'exclude', description: 'comma-separated globs to skip (e.g. "*.test.ts")', type: 'string' },
|
|
167
|
+
{ name: 'output', short: 'o', description: 'write the bundle to a file instead of stdout', type: 'string' },
|
|
168
|
+
{ name: 'no-tree', description: 'omit the directory tree header', type: 'boolean' },
|
|
169
|
+
{ name: 'no-gitignore', description: "don't apply the repo's .gitignore", type: 'boolean' },
|
|
170
|
+
{ name: 'max-file-size', description: 'skip files larger than N KiB (default 512)', type: 'string' },
|
|
171
|
+
{ name: 'redact', description: 'mask secrets in each file before bundling (uses `swarmdo redact`)', type: 'boolean' },
|
|
172
|
+
{ name: 'tokens', description: 'print per-file + total token estimates only, no bundle', type: 'boolean' },
|
|
173
|
+
{ name: 'json', description: 'with --tokens, emit the stats as JSON', type: 'boolean' },
|
|
174
|
+
],
|
|
175
|
+
examples: [
|
|
176
|
+
{ command: 'swarmdo pack src -o context.md', description: 'Bundle src/ to a markdown file' },
|
|
177
|
+
{ command: "swarmdo pack --include '*.ts' --exclude '*.test.ts'", description: 'Only non-test TypeScript' },
|
|
178
|
+
{ command: 'swarmdo pack --tokens', description: 'Token budget breakdown, largest first' },
|
|
179
|
+
{ command: 'swarmdo pack --redact --style xml', description: 'Secret-safe XML bundle' },
|
|
180
|
+
],
|
|
181
|
+
action: run,
|
|
182
|
+
};
|
|
183
|
+
export default packCommand;
|
|
184
|
+
//# sourceMappingURL=pack.js.map
|
|
@@ -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
|
|
@@ -20,6 +20,7 @@ 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
22
|
import { codegraphTools } from './mcp-tools/codegraph-tools.js';
|
|
23
|
+
import { redactTools } from './mcp-tools/redact-tools.js';
|
|
23
24
|
import { progressTools } from './mcp-tools/progress-tools.js';
|
|
24
25
|
import { embeddingsTools } from './mcp-tools/embeddings-tools.js';
|
|
25
26
|
import { claimsTools } from './mcp-tools/claims-tools.js';
|
|
@@ -104,6 +105,7 @@ const TOOL_GROUPS = {
|
|
|
104
105
|
workflow: () => workflowTools,
|
|
105
106
|
analyze: () => analyzeTools,
|
|
106
107
|
codegraph: () => codegraphTools,
|
|
108
|
+
redact: () => redactTools,
|
|
107
109
|
progress: () => progressTools,
|
|
108
110
|
embeddings: () => embeddingsTools,
|
|
109
111
|
claims: () => claimsTools,
|
|
@@ -16,6 +16,7 @@ export { workflowTools } from './workflow-tools.js';
|
|
|
16
16
|
export { coverageRouterTools } from '../swarmvector/coverage-tools.js';
|
|
17
17
|
export { analyzeTools } from './analyze-tools.js';
|
|
18
18
|
export { codegraphTools } from './codegraph-tools.js';
|
|
19
|
+
export { redactTools } from './redact-tools.js';
|
|
19
20
|
export { progressTools } from './progress-tools.js';
|
|
20
21
|
export { transferTools } from './transfer-tools.js';
|
|
21
22
|
export { securityTools } from './security-tools.js';
|
|
@@ -15,6 +15,7 @@ export { workflowTools } from './workflow-tools.js';
|
|
|
15
15
|
export { coverageRouterTools } from '../swarmvector/coverage-tools.js';
|
|
16
16
|
export { analyzeTools } from './analyze-tools.js';
|
|
17
17
|
export { codegraphTools } from './codegraph-tools.js';
|
|
18
|
+
export { redactTools } from './redact-tools.js';
|
|
18
19
|
export { progressTools } from './progress-tools.js';
|
|
19
20
|
export { transferTools } from './transfer-tools.js';
|
|
20
21
|
export { securityTools } from './security-tools.js';
|
|
@@ -23,6 +23,7 @@ const LEAN_GROUPS = [
|
|
|
23
23
|
'agentdb', // agentdb pattern store/search
|
|
24
24
|
'embeddings', // embeddings_generate / search / compare
|
|
25
25
|
'codegraph', // codegraph_query / file / index — "where is X defined?"
|
|
26
|
+
'redact', // redact_text / redact_scan — secret guard on the data path
|
|
26
27
|
];
|
|
27
28
|
/** Balanced = lean + orchestration/session/system the average user reaches for. */
|
|
28
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,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pack.ts — bundle a repo (or a subset) into one AI-friendly context blob.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo has no context-assembly primitive: `codegraph` indexes symbols for
|
|
5
|
+
* *querying* and `compact` de-noises a *stream*, but neither emits promptable
|
|
6
|
+
* source for *feeding a model*. `pack` walks the tree (respecting default skips
|
|
7
|
+
* + a minimal .gitignore + glob include/exclude), and writes a bundle in
|
|
8
|
+
* markdown / xml / json / plain with a directory tree and per-file + total
|
|
9
|
+
* token estimates. Deterministic — same inputs, byte-identical output.
|
|
10
|
+
*
|
|
11
|
+
* Pure engine: the fs walk is injected as a file list + reader by the caller
|
|
12
|
+
* (../commands/pack.ts), so this module has no fs dependency and is trivially
|
|
13
|
+
* testable. Optional secret redaction reuses ../redact/redact.ts.
|
|
14
|
+
*/
|
|
15
|
+
export type PackFormat = 'md' | 'xml' | 'json' | 'plain';
|
|
16
|
+
export interface PackFile {
|
|
17
|
+
/** repo-relative path, POSIX separators */
|
|
18
|
+
path: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PackOptions {
|
|
22
|
+
format?: PackFormat;
|
|
23
|
+
/** include a directory tree at the top (default true) */
|
|
24
|
+
tree?: boolean;
|
|
25
|
+
/** optional per-file transform (e.g. secret redaction) applied before bundling */
|
|
26
|
+
transform?: (content: string, path: string) => string;
|
|
27
|
+
}
|
|
28
|
+
export interface PackFileStat {
|
|
29
|
+
path: string;
|
|
30
|
+
bytes: number;
|
|
31
|
+
tokens: number;
|
|
32
|
+
}
|
|
33
|
+
export interface PackResult {
|
|
34
|
+
output: string;
|
|
35
|
+
stats: {
|
|
36
|
+
files: number;
|
|
37
|
+
bytes: number;
|
|
38
|
+
tokens: number;
|
|
39
|
+
perFile: PackFileStat[];
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Cheap, deterministic token estimate (~4 chars/token, the widely-used rule of
|
|
44
|
+
* thumb for English + code). Not a real BPE tokenizer — good enough for budget
|
|
45
|
+
* warnings, and dependency-free.
|
|
46
|
+
*/
|
|
47
|
+
export declare function estimateTokens(text: string): number;
|
|
48
|
+
/** Build an ASCII directory tree from a sorted file list. */
|
|
49
|
+
export declare function buildTree(paths: string[]): string;
|
|
50
|
+
/** Bundle files into a single context blob per `format`. */
|
|
51
|
+
export declare function packFiles(input: PackFile[], opts?: PackOptions): PackResult;
|
|
52
|
+
/**
|
|
53
|
+
* Minimal .gitignore-style matcher. Handles the common cases — plain names,
|
|
54
|
+
* `dir/`, `*.ext` globs, leading `/` anchors, and `!` negation — not the full
|
|
55
|
+
* spec (no `**` depth semantics beyond substring, no nested ignore files).
|
|
56
|
+
* Good enough to keep obvious noise out; full semantics is a follow-up.
|
|
57
|
+
*/
|
|
58
|
+
export declare function makeIgnoreMatcher(patterns: string[]): (relPath: string) => boolean;
|
|
59
|
+
//# sourceMappingURL=pack.d.ts.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pack.ts — bundle a repo (or a subset) into one AI-friendly context blob.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo has no context-assembly primitive: `codegraph` indexes symbols for
|
|
5
|
+
* *querying* and `compact` de-noises a *stream*, but neither emits promptable
|
|
6
|
+
* source for *feeding a model*. `pack` walks the tree (respecting default skips
|
|
7
|
+
* + a minimal .gitignore + glob include/exclude), and writes a bundle in
|
|
8
|
+
* markdown / xml / json / plain with a directory tree and per-file + total
|
|
9
|
+
* token estimates. Deterministic — same inputs, byte-identical output.
|
|
10
|
+
*
|
|
11
|
+
* Pure engine: the fs walk is injected as a file list + reader by the caller
|
|
12
|
+
* (../commands/pack.ts), so this module has no fs dependency and is trivially
|
|
13
|
+
* testable. Optional secret redaction reuses ../redact/redact.ts.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Cheap, deterministic token estimate (~4 chars/token, the widely-used rule of
|
|
17
|
+
* thumb for English + code). Not a real BPE tokenizer — good enough for budget
|
|
18
|
+
* warnings, and dependency-free.
|
|
19
|
+
*/
|
|
20
|
+
export function estimateTokens(text) {
|
|
21
|
+
if (!text)
|
|
22
|
+
return 0;
|
|
23
|
+
return Math.ceil(text.length / 4);
|
|
24
|
+
}
|
|
25
|
+
/** Build an ASCII directory tree from a sorted file list. */
|
|
26
|
+
export function buildTree(paths) {
|
|
27
|
+
const root = {};
|
|
28
|
+
for (const p of [...paths].sort()) {
|
|
29
|
+
const parts = p.split('/');
|
|
30
|
+
let node = root;
|
|
31
|
+
for (let i = 0; i < parts.length; i++) {
|
|
32
|
+
const part = parts[i];
|
|
33
|
+
const isFile = i === parts.length - 1;
|
|
34
|
+
if (isFile) {
|
|
35
|
+
if (!node['\0files'])
|
|
36
|
+
node['\0files'] = [];
|
|
37
|
+
node['\0files'].push(part);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
node[part] = node[part] ?? {};
|
|
41
|
+
node = node[part];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const lines = [];
|
|
46
|
+
const walk = (node, prefix) => {
|
|
47
|
+
const dirs = Object.keys(node).filter((k) => k !== '\0files').sort();
|
|
48
|
+
const files = (node['\0files'] ?? []).sort();
|
|
49
|
+
const entries = [...dirs.map((d) => ['dir', d]), ...files.map((f) => ['file', f])];
|
|
50
|
+
entries.forEach(([kind, name], i) => {
|
|
51
|
+
const last = i === entries.length - 1;
|
|
52
|
+
lines.push(`${prefix}${last ? '└── ' : '├── '}${name}${kind === 'dir' ? '/' : ''}`);
|
|
53
|
+
if (kind === 'dir')
|
|
54
|
+
walk(node[name], prefix + (last ? ' ' : '│ '));
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
walk(root, '');
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
/** Pick a fenced-code language hint from a file extension. */
|
|
61
|
+
function langOf(path) {
|
|
62
|
+
const ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
|
|
63
|
+
const map = {
|
|
64
|
+
ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx', mjs: 'javascript', cjs: 'javascript',
|
|
65
|
+
json: 'json', md: 'markdown', py: 'python', rs: 'rust', go: 'go', sh: 'bash', yml: 'yaml', yaml: 'yaml',
|
|
66
|
+
html: 'html', css: 'css', sql: 'sql', toml: 'toml',
|
|
67
|
+
};
|
|
68
|
+
return map[ext] ?? '';
|
|
69
|
+
}
|
|
70
|
+
function renderMarkdown(files, opts) {
|
|
71
|
+
const parts = ['# Repository context', ''];
|
|
72
|
+
if (opts.tree !== false) {
|
|
73
|
+
parts.push('## Files', '', '```', buildTree(files.map((f) => f.path)), '```', '');
|
|
74
|
+
}
|
|
75
|
+
parts.push('## Contents', '');
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
const lang = langOf(f.path);
|
|
78
|
+
parts.push(`### ${f.path}`, '', '```' + lang, f.content.replace(/\n$/, ''), '```', '');
|
|
79
|
+
}
|
|
80
|
+
return parts.join('\n');
|
|
81
|
+
}
|
|
82
|
+
function escapeXml(s) {
|
|
83
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
84
|
+
}
|
|
85
|
+
function renderXml(files, opts) {
|
|
86
|
+
const parts = ['<repository>'];
|
|
87
|
+
if (opts.tree !== false) {
|
|
88
|
+
parts.push(' <tree>', escapeXml(buildTree(files.map((f) => f.path))), ' </tree>');
|
|
89
|
+
}
|
|
90
|
+
parts.push(' <files>');
|
|
91
|
+
for (const f of files) {
|
|
92
|
+
parts.push(` <file path="${escapeXml(f.path)}">`, escapeXml(f.content.replace(/\n$/, '')), ' </file>');
|
|
93
|
+
}
|
|
94
|
+
parts.push(' </files>', '</repository>');
|
|
95
|
+
return parts.join('\n');
|
|
96
|
+
}
|
|
97
|
+
function renderJson(files) {
|
|
98
|
+
return JSON.stringify({ files: files.map((f) => ({ path: f.path, content: f.content })) }, null, 2);
|
|
99
|
+
}
|
|
100
|
+
function renderPlain(files) {
|
|
101
|
+
const parts = [];
|
|
102
|
+
for (const f of files) {
|
|
103
|
+
parts.push('='.repeat(8) + ' ' + f.path + ' ' + '='.repeat(8), f.content.replace(/\n$/, ''), '');
|
|
104
|
+
}
|
|
105
|
+
return parts.join('\n');
|
|
106
|
+
}
|
|
107
|
+
/** Bundle files into a single context blob per `format`. */
|
|
108
|
+
export function packFiles(input, opts = {}) {
|
|
109
|
+
const files = input
|
|
110
|
+
.map((f) => ({ path: f.path, content: opts.transform ? opts.transform(f.content, f.path) : f.content }))
|
|
111
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
112
|
+
const format = opts.format ?? 'md';
|
|
113
|
+
let output;
|
|
114
|
+
switch (format) {
|
|
115
|
+
case 'xml':
|
|
116
|
+
output = renderXml(files, opts);
|
|
117
|
+
break;
|
|
118
|
+
case 'json':
|
|
119
|
+
output = renderJson(files);
|
|
120
|
+
break;
|
|
121
|
+
case 'plain':
|
|
122
|
+
output = renderPlain(files);
|
|
123
|
+
break;
|
|
124
|
+
default: output = renderMarkdown(files, opts);
|
|
125
|
+
}
|
|
126
|
+
const perFile = files.map((f) => ({
|
|
127
|
+
path: f.path,
|
|
128
|
+
bytes: Buffer.byteLength(f.content, 'utf8'),
|
|
129
|
+
tokens: estimateTokens(f.content),
|
|
130
|
+
}));
|
|
131
|
+
return {
|
|
132
|
+
output,
|
|
133
|
+
stats: {
|
|
134
|
+
files: files.length,
|
|
135
|
+
bytes: perFile.reduce((n, f) => n + f.bytes, 0),
|
|
136
|
+
tokens: estimateTokens(output),
|
|
137
|
+
perFile,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Minimal .gitignore-style matcher. Handles the common cases — plain names,
|
|
143
|
+
* `dir/`, `*.ext` globs, leading `/` anchors, and `!` negation — not the full
|
|
144
|
+
* spec (no `**` depth semantics beyond substring, no nested ignore files).
|
|
145
|
+
* Good enough to keep obvious noise out; full semantics is a follow-up.
|
|
146
|
+
*/
|
|
147
|
+
export function makeIgnoreMatcher(patterns) {
|
|
148
|
+
const rules = patterns
|
|
149
|
+
.map((p) => p.trim())
|
|
150
|
+
.filter((p) => p && !p.startsWith('#'))
|
|
151
|
+
.map((p) => {
|
|
152
|
+
const negate = p.startsWith('!');
|
|
153
|
+
let body = negate ? p.slice(1) : p;
|
|
154
|
+
const dirOnly = body.endsWith('/');
|
|
155
|
+
if (dirOnly)
|
|
156
|
+
body = body.slice(0, -1);
|
|
157
|
+
const anchored = body.startsWith('/');
|
|
158
|
+
if (anchored)
|
|
159
|
+
body = body.slice(1);
|
|
160
|
+
const re = new RegExp('^' +
|
|
161
|
+
body
|
|
162
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
163
|
+
.replace(/\*/g, '[^/]*')
|
|
164
|
+
.replace(/\?/g, '[^/]') +
|
|
165
|
+
(dirOnly ? '(/|$)' : '($|/)'));
|
|
166
|
+
return { negate, anchored, re };
|
|
167
|
+
});
|
|
168
|
+
return (relPath) => {
|
|
169
|
+
let ignored = false;
|
|
170
|
+
for (const r of rules) {
|
|
171
|
+
const candidates = r.anchored ? [relPath] : [relPath, ...relPath.split('/').map((_, i, a) => a.slice(i).join('/'))];
|
|
172
|
+
if (candidates.some((c) => r.re.test(c)))
|
|
173
|
+
ignored = !r.negate;
|
|
174
|
+
}
|
|
175
|
+
return ignored;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=pack.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.
|
|
3
|
+
"version": "1.10.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",
|