corpus-cli 0.1.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.
@@ -0,0 +1,380 @@
1
+ import { watch, readFileSync, writeFileSync, statSync, readdirSync, existsSync, mkdirSync } from 'fs';
2
+ import path from 'path';
3
+ import { green, amber, red, dim, bold, cyan } from '../utils/colors.js';
4
+ import { detectSecrets } from '@corpus/core';
5
+ import { checkCodeSafety } from '@corpus/core';
6
+ import { checkForCVEs } from '@corpus/core';
7
+ import { checkDependencies } from '@corpus/core';
8
+ import { checkFile } from '@corpus/core';
9
+ import { shouldSuppress } from '@corpus/core';
10
+ const IGNORE_DIRS = new Set([
11
+ 'node_modules', '.git', 'dist', '.next', '__pycache__', '.venv',
12
+ 'venv', '.cache', '.turbo', 'coverage', '.nyc_output', '.claude',
13
+ '.swarm', '.claude-flow', '.insforge', '.corpus',
14
+ ]);
15
+ const SCAN_EXTENSIONS = new Set([
16
+ '.ts', '.tsx', '.js', '.jsx', '.py', '.json', '.yaml', '.yml',
17
+ '.env', '.toml', '.sh', '.sql', '.tf', '.hcl', '.md',
18
+ ]);
19
+ const stats = {
20
+ filesScanned: 0,
21
+ issuesFound: 0,
22
+ criticalCount: 0,
23
+ warningCount: 0,
24
+ passCount: 0,
25
+ startTime: Date.now(),
26
+ lastScanTime: '-',
27
+ recentEvents: [],
28
+ cvesDetected: 0,
29
+ depsChecked: 0,
30
+ contractViolations: 0,
31
+ autoHealed: 0,
32
+ };
33
+ function deepScan(content, filepath, projectRoot) {
34
+ const results = [];
35
+ // Core secret detection
36
+ try {
37
+ const secrets = detectSecrets(content, filepath);
38
+ for (const s of secrets) {
39
+ results.push({ severity: s.severity === 'CRITICAL' ? 'CRIT' : 'WARN', message: `${s.type}: ${s.redacted}`, type: s.type });
40
+ }
41
+ }
42
+ catch { }
43
+ // Code safety
44
+ try {
45
+ const safety = checkCodeSafety(content, filepath);
46
+ for (const s of safety) {
47
+ // Check pattern intelligence for suppression
48
+ const suppression = shouldSuppress(projectRoot, s.rule, filepath);
49
+ if (suppression.suppress)
50
+ continue;
51
+ results.push({ severity: s.severity === 'CRITICAL' ? 'CRIT' : s.severity === 'WARNING' ? 'WARN' : 'INFO', message: s.message, type: s.rule });
52
+ }
53
+ }
54
+ catch { }
55
+ // CVE pattern detection
56
+ try {
57
+ const cves = checkForCVEs(content, filepath);
58
+ for (const c of cves) {
59
+ results.push({ severity: 'CRIT', message: `${c.cveId}: ${c.name}`, type: `CVE:${c.cveId}`, cveId: c.cveId });
60
+ }
61
+ }
62
+ catch { }
63
+ // Graph contract verification (if graph exists)
64
+ try {
65
+ const graphResult = checkFile(projectRoot, filepath, content);
66
+ if (graphResult.verdict === 'VIOLATES') {
67
+ for (const v of graphResult.violations) {
68
+ results.push({ severity: v.severity === 'CRITICAL' ? 'CRIT' : 'WARN', message: `Contract: ${v.message}`, type: `contract:${v.type}` });
69
+ }
70
+ }
71
+ }
72
+ catch { }
73
+ return results;
74
+ }
75
+ // Async dependency check (runs separately due to network)
76
+ async function checkDeps(content, filepath, projectRoot) {
77
+ const results = [];
78
+ try {
79
+ const findings = await checkDependencies(content, filepath, { projectRoot });
80
+ for (const f of findings) {
81
+ results.push({
82
+ severity: f.severity === 'CRITICAL' ? 'CRIT' : 'WARN',
83
+ message: `Dep: ${f.package} (${f.reason})${f.similarPackages?.length ? ' → did you mean ' + f.similarPackages[0] + '?' : ''}`,
84
+ type: `dep:${f.reason}`,
85
+ });
86
+ }
87
+ }
88
+ catch { }
89
+ return results;
90
+ }
91
+ function isScannable(filepath) {
92
+ const ext = path.extname(filepath).toLowerCase();
93
+ return SCAN_EXTENSIONS.has(ext) || filepath.includes('.env');
94
+ }
95
+ // ── Disk persistence for web dashboard ───────────────────────────────────────
96
+ const DASHBOARD_FILE = '/tmp/corpus-dashboard.json';
97
+ let eventsFilePath = '';
98
+ function ensureCorpusDir(projectRoot) {
99
+ const corpusDir = path.join(projectRoot, '.corpus');
100
+ if (!existsSync(corpusDir))
101
+ mkdirSync(corpusDir, { recursive: true });
102
+ eventsFilePath = path.join(corpusDir, 'events.json');
103
+ }
104
+ function writeDashboardState(dir) {
105
+ const passRate = stats.filesScanned > 0
106
+ ? Math.round((stats.passCount / stats.filesScanned) * 100)
107
+ : 100;
108
+ const dashboard = {
109
+ score: passRate,
110
+ filesScanned: stats.filesScanned,
111
+ issues: {
112
+ critical: stats.criticalCount,
113
+ warning: stats.warningCount,
114
+ info: 0,
115
+ },
116
+ events: stats.recentEvents.map(e => ({
117
+ time: e.time,
118
+ file: e.file,
119
+ severity: e.severity,
120
+ message: e.message,
121
+ })),
122
+ uptime: uptime(),
123
+ lastUpdate: formatTime(),
124
+ status: 'active',
125
+ cvesDetected: stats.cvesDetected,
126
+ contractViolations: stats.contractViolations,
127
+ depsChecked: stats.depsChecked,
128
+ autoHealed: stats.autoHealed,
129
+ };
130
+ try {
131
+ writeFileSync(DASHBOARD_FILE, JSON.stringify(dashboard));
132
+ }
133
+ catch { /* /tmp write failed */ }
134
+ }
135
+ function writeEventToDisk(event) {
136
+ if (!eventsFilePath)
137
+ return;
138
+ try {
139
+ let events = [];
140
+ if (existsSync(eventsFilePath)) {
141
+ events = JSON.parse(readFileSync(eventsFilePath, 'utf-8'));
142
+ }
143
+ events.push({
144
+ ...event,
145
+ timestamp: new Date().toISOString(),
146
+ });
147
+ // Keep last 200 events
148
+ if (events.length > 200)
149
+ events = events.slice(-200);
150
+ writeFileSync(eventsFilePath, JSON.stringify(events));
151
+ }
152
+ catch { /* write failed */ }
153
+ }
154
+ function formatTime() {
155
+ const now = new Date();
156
+ return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
157
+ }
158
+ function uptime() {
159
+ const ms = Date.now() - stats.startTime;
160
+ const s = Math.floor(ms / 1000);
161
+ if (s < 60)
162
+ return `${s}s`;
163
+ const m = Math.floor(s / 60);
164
+ if (m < 60)
165
+ return `${m}m ${s % 60}s`;
166
+ return `${Math.floor(m / 60)}h ${m % 60}m`;
167
+ }
168
+ // ── Dashboard rendering ─────────────────────────────────────────────────────
169
+ function renderDashboard(dir) {
170
+ const passRate = stats.filesScanned > 0
171
+ ? Math.round((stats.passCount / stats.filesScanned) * 100)
172
+ : 100;
173
+ const passColor = passRate >= 95 ? green : passRate >= 80 ? amber : red;
174
+ process.stdout.write('\x1b[2J\x1b[H'); // Clear screen
175
+ process.stdout.write('\n');
176
+ process.stdout.write(bold(` CORPUS WATCH`) + dim(` ${dir}\n`));
177
+ process.stdout.write(' ' + '\u2550'.repeat(60) + '\n\n');
178
+ // Stats row
179
+ process.stdout.write(` ${bold('Pass rate')} ${passColor(`${passRate}%`)} `);
180
+ process.stdout.write(`${bold('Scanned')} ${cyan(String(stats.filesScanned))} `);
181
+ process.stdout.write(`${bold('Issues')} ${stats.issuesFound > 0 ? red(String(stats.issuesFound)) : green('0')} `);
182
+ process.stdout.write(`${bold('Uptime')} ${dim(uptime())}\n`);
183
+ process.stdout.write('\n');
184
+ // Breakdown
185
+ if (stats.criticalCount > 0 || stats.warningCount > 0) {
186
+ process.stdout.write(` ${red(`\u2716 ${stats.criticalCount} critical`)} ${amber(`\u26A0 ${stats.warningCount} warning`)} ${green(`\u2714 ${stats.passCount} clean`)}\n\n`);
187
+ }
188
+ else {
189
+ process.stdout.write(` ${green(`\u2714 ${stats.passCount} clean`)} ${dim('No issues found')}\n\n`);
190
+ }
191
+ // Intelligence stats
192
+ if (stats.cvesDetected > 0 || stats.contractViolations > 0 || stats.depsChecked > 0) {
193
+ process.stdout.write(` ${red(`CVEs: ${stats.cvesDetected}`)} ${amber(`Contracts: ${stats.contractViolations}`)} ${cyan(`Deps checked: ${stats.depsChecked}`)} ${green(`Auto-healed: ${stats.autoHealed}`)}\n\n`);
194
+ }
195
+ // Recent events — prioritize findings over PASS
196
+ process.stdout.write(dim(' Recent activity:\n'));
197
+ if (stats.recentEvents.length === 0) {
198
+ process.stdout.write(dim(' Waiting for file changes...\n'));
199
+ }
200
+ else {
201
+ // Show findings first, then recent clean files
202
+ const findings = stats.recentEvents.filter(e => e.severity !== 'PASS');
203
+ const clean = stats.recentEvents.filter(e => e.severity === 'PASS');
204
+ const display = [...findings.slice(-10), ...clean.slice(-2)].slice(-12);
205
+ for (const event of display) {
206
+ const sev = event.severity === 'CRIT' ? red('CRIT') :
207
+ event.severity === 'WARN' ? amber('WARN') :
208
+ event.severity === 'INFO' ? dim('INFO') :
209
+ green('PASS');
210
+ process.stdout.write(` ${dim(event.time)} ${sev} ${event.file.padEnd(38)} ${dim(event.message)}\n`);
211
+ }
212
+ }
213
+ process.stdout.write('\n' + dim(' Press Ctrl+C to stop\n'));
214
+ }
215
+ // ── Initial scan ────────────────────────────────────────────────────────────
216
+ function initialScan(dir) {
217
+ function walkAndScan(d) {
218
+ try {
219
+ for (const entry of readdirSync(d)) {
220
+ if (IGNORE_DIRS.has(entry))
221
+ continue;
222
+ const full = path.join(d, entry);
223
+ try {
224
+ const s = statSync(full);
225
+ if (s.isDirectory())
226
+ walkAndScan(full);
227
+ else if (s.isFile() && isScannable(full)) {
228
+ const content = readFileSync(full, 'utf-8');
229
+ const findings = deepScan(content, full, dir);
230
+ stats.filesScanned++;
231
+ const cveCount = findings.filter(f => f.type.startsWith('CVE:')).length;
232
+ const contractCount = findings.filter(f => f.type.startsWith('contract:')).length;
233
+ stats.cvesDetected += cveCount;
234
+ stats.contractViolations += contractCount;
235
+ if (findings.length === 0) {
236
+ stats.passCount++;
237
+ }
238
+ else {
239
+ stats.issuesFound += findings.length;
240
+ const hasCrit = findings.some((f) => f.severity === 'CRIT');
241
+ if (hasCrit)
242
+ stats.criticalCount++;
243
+ else
244
+ stats.warningCount++;
245
+ stats.recentEvents.push({
246
+ time: formatTime(),
247
+ file: path.relative(dir, full),
248
+ severity: hasCrit ? 'CRIT' : 'WARN',
249
+ message: findings[0].message,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ catch { /* skip */ }
255
+ }
256
+ }
257
+ catch { /* skip */ }
258
+ }
259
+ walkAndScan(dir);
260
+ }
261
+ // ── Main ────────────────────────────────────────────────────────────────────
262
+ export async function runWatch() {
263
+ const targetDir = process.argv[3] || '.';
264
+ const resolvedDir = path.resolve(targetDir);
265
+ // Ensure .corpus dir exists for event writing
266
+ ensureCorpusDir(resolvedDir);
267
+ // Initial scan
268
+ process.stdout.write(dim('\n Running initial scan...\n'));
269
+ initialScan(resolvedDir);
270
+ // Write initial dashboard state
271
+ writeDashboardState(resolvedDir);
272
+ // Render dashboard
273
+ renderDashboard(resolvedDir);
274
+ const debounce = new Map();
275
+ function handleFileChange(filepath) {
276
+ if (!isScannable(filepath))
277
+ return;
278
+ const existing = debounce.get(filepath);
279
+ if (existing)
280
+ clearTimeout(existing);
281
+ debounce.set(filepath, setTimeout(() => {
282
+ debounce.delete(filepath);
283
+ try {
284
+ const s = statSync(filepath);
285
+ if (!s.isFile())
286
+ return;
287
+ const content = readFileSync(filepath, 'utf-8');
288
+ const findings = deepScan(content, filepath, resolvedDir);
289
+ const relPath = path.relative(resolvedDir, filepath);
290
+ const time = formatTime();
291
+ stats.filesScanned++;
292
+ stats.lastScanTime = time;
293
+ const cveCount = findings.filter(f => f.type.startsWith('CVE:')).length;
294
+ const contractCount = findings.filter(f => f.type.startsWith('contract:')).length;
295
+ stats.cvesDetected += cveCount;
296
+ stats.contractViolations += contractCount;
297
+ if (findings.length === 0) {
298
+ stats.passCount++;
299
+ stats.recentEvents.push({ time, file: relPath, severity: 'PASS', message: 'Clean' });
300
+ writeEventToDisk({ type: 'verified', file: relPath, verdict: 'VERIFIED', details: 'All contracts satisfied' });
301
+ }
302
+ else {
303
+ stats.issuesFound += findings.length;
304
+ const hasCrit = findings.some((f) => f.severity === 'CRIT');
305
+ if (hasCrit)
306
+ stats.criticalCount++;
307
+ else
308
+ stats.warningCount++;
309
+ for (const f of findings) {
310
+ stats.recentEvents.push({ time, file: relPath, severity: f.severity, message: f.message });
311
+ writeEventToDisk({
312
+ type: f.severity === 'CRIT' ? 'violation' : 'scan',
313
+ file: relPath,
314
+ verdict: f.severity === 'CRIT' ? 'VIOLATION' : 'WARNING',
315
+ details: f.message,
316
+ });
317
+ }
318
+ }
319
+ // Async: check dependencies (non-blocking)
320
+ checkDeps(content, filepath, resolvedDir).then(depResults => {
321
+ if (depResults.length > 0) {
322
+ stats.depsChecked++;
323
+ for (const r of depResults) {
324
+ stats.issuesFound++;
325
+ if (r.severity === 'CRIT')
326
+ stats.criticalCount++;
327
+ else
328
+ stats.warningCount++;
329
+ stats.recentEvents.push({ time: formatTime(), file: relPath, severity: r.severity, message: r.message });
330
+ }
331
+ renderDashboard(resolvedDir);
332
+ }
333
+ });
334
+ // Keep only last 50 events
335
+ if (stats.recentEvents.length > 50) {
336
+ stats.recentEvents = stats.recentEvents.slice(-50);
337
+ }
338
+ renderDashboard(resolvedDir);
339
+ }
340
+ catch { /* file deleted */ }
341
+ }, 300));
342
+ }
343
+ // Watch
344
+ try {
345
+ watch(resolvedDir, { recursive: true }, (_event, filename) => {
346
+ if (!filename)
347
+ return;
348
+ const fullPath = path.join(resolvedDir, filename);
349
+ const parts = filename.split(path.sep);
350
+ if (parts.some((p) => IGNORE_DIRS.has(p)))
351
+ return;
352
+ handleFileChange(fullPath);
353
+ });
354
+ }
355
+ catch {
356
+ process.stderr.write(red(` Could not watch ${resolvedDir}\n`));
357
+ process.exit(1);
358
+ }
359
+ // Refresh dashboard every 5s (uptime counter)
360
+ setInterval(() => renderDashboard(resolvedDir), 5000);
361
+ await new Promise(() => {
362
+ process.on('SIGINT', () => {
363
+ process.stdout.write('\n\n');
364
+ process.stdout.write(bold(' CORPUS WATCH SESSION SUMMARY\n'));
365
+ process.stdout.write(' ' + '\u2550'.repeat(40) + '\n');
366
+ process.stdout.write(` Files scanned: ${stats.filesScanned}\n`);
367
+ process.stdout.write(` Issues found: ${stats.issuesFound}\n`);
368
+ process.stdout.write(` Critical: ${stats.criticalCount}\n`);
369
+ process.stdout.write(` Warnings: ${stats.warningCount}\n`);
370
+ process.stdout.write(` Clean: ${stats.passCount}\n`);
371
+ process.stdout.write(` CVEs detected: ${stats.cvesDetected}\n`);
372
+ process.stdout.write(` Deps checked: ${stats.depsChecked}\n`);
373
+ process.stdout.write(` Violations: ${stats.contractViolations}\n`);
374
+ process.stdout.write(` Auto-healed: ${stats.autoHealed}\n`);
375
+ process.stdout.write(` Duration: ${uptime()}\n`);
376
+ process.stdout.write('\n');
377
+ process.exit(0);
378
+ });
379
+ });
380
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ declare const command: string;
3
+ declare function main(): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const command = process.argv[2];
4
+ async function main() {
5
+ switch (command) {
6
+ case 'init': {
7
+ const { runInit } = await import('./commands/init.js');
8
+ await runInit();
9
+ // After init completes, also build the codebase graph
10
+ const { initGraph } = await import('./commands/init-graph.js');
11
+ await initGraph(process.argv.slice(3));
12
+ break;
13
+ }
14
+ case 'graph': {
15
+ const { initGraph } = await import('./commands/init-graph.js');
16
+ await initGraph(process.argv.slice(3));
17
+ break;
18
+ }
19
+ case 'verify': {
20
+ const { runVerify } = await import('./commands/verify.js');
21
+ await runVerify();
22
+ break;
23
+ }
24
+ case 'scan': {
25
+ const { runScan } = await import('./commands/scan.js');
26
+ await runScan();
27
+ break;
28
+ }
29
+ case 'watch': {
30
+ const { runWatch } = await import('./commands/watch.js');
31
+ await runWatch();
32
+ break;
33
+ }
34
+ case 'check': {
35
+ const { runCheck } = await import('./commands/check.js');
36
+ await runCheck();
37
+ break;
38
+ }
39
+ case 'report': {
40
+ const { runReport } = await import('./commands/report.js');
41
+ await runReport();
42
+ break;
43
+ }
44
+ default:
45
+ process.stdout.write(`
46
+ corpus - Runtime safety for AI agents and AI-generated code
47
+
48
+ Trust Verification:
49
+ verify Compute trust score (0-100) per file with line-by-line findings
50
+
51
+ Security Scanning:
52
+ scan Scan files for secrets, PII, injection patterns, unsafe code
53
+ watch Real-time file watcher with live security scanning
54
+
55
+ Policy Management:
56
+ init Initialize Corpus in your project (+ pre-commit hooks + graph)
57
+ check Validate all policy files
58
+ report View your agent's behavioral report
59
+
60
+ Graph & Analysis:
61
+ graph Build / rebuild the codebase graph
62
+
63
+ Usage:
64
+ corpus verify Trust score for your codebase
65
+ corpus verify --json Machine-readable trust report
66
+ corpus scan Scan current directory
67
+ corpus scan --staged Scan staged git changes (pre-commit)
68
+ corpus scan --json Machine-readable output for CI
69
+ corpus watch Watch files and scan on every save
70
+ corpus watch src/ Watch specific directory
71
+ corpus init Set up Corpus in your project
72
+ corpus graph Build the codebase graph
73
+ corpus check Validate policy files
74
+
75
+ MCP Server (for AI coding tools):
76
+ npx corpus-mcp Start the MCP server for Claude Code / Cursor
77
+
78
+ `);
79
+ if (command && command !== '--help' && command !== '-h') {
80
+ process.exit(1);
81
+ }
82
+ }
83
+ }
84
+ main().catch((e) => {
85
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,6 @@
1
+ export declare const green: (s: string) => string;
2
+ export declare const amber: (s: string) => string;
3
+ export declare const red: (s: string) => string;
4
+ export declare const dim: (s: string) => string;
5
+ export declare const bold: (s: string) => string;
6
+ export declare const cyan: (s: string) => string;
@@ -0,0 +1,6 @@
1
+ export const green = (s) => `\x1b[32m${s}\x1b[0m`;
2
+ export const amber = (s) => `\x1b[33m${s}\x1b[0m`;
3
+ export const red = (s) => `\x1b[31m${s}\x1b[0m`;
4
+ export const dim = (s) => `\x1b[2m${s}\x1b[0m`;
5
+ export const bold = (s) => `\x1b[1m${s}\x1b[0m`;
6
+ export const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
@@ -0,0 +1,3 @@
1
+ export declare function readPolicyFile(path?: string): Record<string, unknown> | null;
2
+ export declare function readEnvFile(path?: string): Record<string, string>;
3
+ export declare function writeEnvFile(path: string, vars: Record<string, string>): void;
@@ -0,0 +1,39 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
2
+ export function readPolicyFile(path = './corpus.policy.yaml') {
3
+ if (!existsSync(path))
4
+ return null;
5
+ const raw = readFileSync(path, 'utf-8');
6
+ // Simple YAML key extraction for CLI (no dependency).
7
+ // NOTE: This parser only handles top-level scalar key:value pairs.
8
+ // Nested objects, arrays, multi-line strings, and anchors are NOT supported.
9
+ // This is sufficient for reading `agent` and `version` fields used by report.ts,
10
+ // but should be replaced with a proper YAML parser if deeper parsing is needed.
11
+ const lines = raw.split('\n');
12
+ const result = {};
13
+ for (const line of lines) {
14
+ const match = line.match(/^(\w+):\s*(.+)/);
15
+ if (match) {
16
+ result[match[1]] = match[2].replace(/^["']|["']$/g, '');
17
+ }
18
+ }
19
+ return result;
20
+ }
21
+ export function readEnvFile(path = './.env.corpus') {
22
+ if (!existsSync(path))
23
+ return {};
24
+ const raw = readFileSync(path, 'utf-8');
25
+ const result = {};
26
+ for (const line of raw.split('\n')) {
27
+ const match = line.match(/^([A-Z_]+)=(.+)/);
28
+ if (match) {
29
+ result[match[1]] = match[2];
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+ export function writeEnvFile(path, vars) {
35
+ const content = Object.entries(vars)
36
+ .map(([k, v]) => `${k}=${v}`)
37
+ .join('\n') + '\n';
38
+ writeFileSync(path, content);
39
+ }
@@ -0,0 +1,2 @@
1
+ export declare function renderTable(rows: string[][]): string;
2
+ export declare function progressBar(value: number, width?: number): string;
@@ -0,0 +1,24 @@
1
+ export function renderTable(rows) {
2
+ if (rows.length === 0)
3
+ return '';
4
+ const colWidths = [];
5
+ for (const row of rows) {
6
+ for (let i = 0; i < row.length; i++) {
7
+ const stripped = row[i].replace(/\x1b\[[0-9;]*m/g, '');
8
+ colWidths[i] = Math.max(colWidths[i] ?? 0, stripped.length);
9
+ }
10
+ }
11
+ return rows
12
+ .map((row) => row
13
+ .map((cell, i) => {
14
+ const stripped = cell.replace(/\x1b\[[0-9;]*m/g, '');
15
+ const padding = colWidths[i] - stripped.length;
16
+ return cell + ' '.repeat(Math.max(0, padding));
17
+ })
18
+ .join(' '))
19
+ .join('\n');
20
+ }
21
+ export function progressBar(value, width = 20) {
22
+ const filled = Math.round((value / 100) * width);
23
+ return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
24
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "corpus-cli",
3
+ "version": "0.1.0",
4
+ "description": "The immune system for AI-generated code — scan, watch, and auto-heal vibe-coded software",
5
+ "type": "module",
6
+ "bin": {
7
+ "corpus": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc"
14
+ },
15
+ "dependencies": {
16
+ "corpus-core": "0.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.11.0",
20
+ "typescript": "^5.3.0"
21
+ },
22
+ "keywords": ["ai", "security", "scanner", "trust-score", "secrets", "vibe-coding", "claude", "cursor", "copilot", "mcp", "jac"],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/fluentflier/corpus"
26
+ },
27
+ "license": "MIT"
28
+ }