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.
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +163 -0
- package/dist/commands/init-graph.d.ts +7 -0
- package/dist/commands/init-graph.js +270 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +211 -0
- package/dist/commands/report.d.ts +1 -0
- package/dist/commands/report.js +93 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +481 -0
- package/dist/commands/verify.d.ts +1 -0
- package/dist/commands/verify.js +334 -0
- package/dist/commands/watch.d.ts +1 -0
- package/dist/commands/watch.js +380 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +87 -0
- package/dist/utils/colors.d.ts +6 -0
- package/dist/utils/colors.js +6 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.js +39 -0
- package/dist/utils/table.d.ts +2 -0
- package/dist/utils/table.js +24 -0
- package/package.json +28 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCheck(): Promise<void>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync, statSync } from 'fs';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { green, red, amber, dim, bold } from '../utils/colors.js';
|
|
5
|
+
function findPolicyFiles(target) {
|
|
6
|
+
const files = [];
|
|
7
|
+
// If a specific file was given, just return it
|
|
8
|
+
if (target && existsSync(target)) {
|
|
9
|
+
try {
|
|
10
|
+
const stat = statSync(target);
|
|
11
|
+
if (stat.isFile()) {
|
|
12
|
+
return [target];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// fall through to directory logic
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Determine directories to scan
|
|
20
|
+
const dirs = target ? [target] : ['.'];
|
|
21
|
+
const isTargetMode = Boolean(target);
|
|
22
|
+
for (const dir of dirs) {
|
|
23
|
+
if (!existsSync(dir))
|
|
24
|
+
continue;
|
|
25
|
+
try {
|
|
26
|
+
for (const entry of readdirSync(dir)) {
|
|
27
|
+
const full = path.join(dir, entry);
|
|
28
|
+
if (isTargetMode) {
|
|
29
|
+
// When scanning a specific directory, include all yaml/jac
|
|
30
|
+
if (entry.endsWith('.yaml') || entry.endsWith('.yml') || entry.endsWith('.jac')) {
|
|
31
|
+
files.push(full);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Default mode: only policy-looking yaml files in CWD
|
|
36
|
+
if (entry.match(/corpus.*\.yaml$/) || entry.match(/.*\.policy\.yaml$/) || entry.endsWith('.jac')) {
|
|
37
|
+
files.push(full);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
// Also scan standard policy directories when no specific target
|
|
45
|
+
if (!target) {
|
|
46
|
+
const policyDirs = ['./policies/builtin', './policies', './policies/examples'];
|
|
47
|
+
for (const pd of policyDirs) {
|
|
48
|
+
if (!existsSync(pd))
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
for (const entry of readdirSync(pd)) {
|
|
52
|
+
const full = path.join(pd, entry);
|
|
53
|
+
if (entry.endsWith('.jac') || entry.endsWith('.yaml')) {
|
|
54
|
+
if (!files.includes(full)) {
|
|
55
|
+
files.push(full);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
function checkYamlFile(filePath) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
68
|
+
if (!raw.includes('agent:') || !raw.includes('rules:')) {
|
|
69
|
+
return { file: filePath, status: 'FAIL', detail: 'Missing agent or rules field' };
|
|
70
|
+
}
|
|
71
|
+
const ruleCount = (raw.match(/- name:/g) || []).length;
|
|
72
|
+
return { file: filePath, status: 'PASS', detail: `${ruleCount} rule${ruleCount !== 1 ? 's' : ''}` };
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return { file: filePath, status: 'FAIL', detail: String(e) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function checkJacFile(filePath) {
|
|
79
|
+
const resolved = path.resolve(filePath);
|
|
80
|
+
if (!resolved.endsWith('.jac')) {
|
|
81
|
+
return { file: filePath, status: 'FAIL', detail: 'Not a .jac file' };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
execSync('jac check ' + JSON.stringify(resolved), { stdio: 'pipe' });
|
|
85
|
+
return { file: filePath, status: 'PASS', detail: 'validated via Jac' };
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
const stderr = e instanceof Error && 'stderr' in e ? String(e.stderr) : '';
|
|
89
|
+
const firstLine = stderr.split('\n')[0] || 'validation failed';
|
|
90
|
+
if (firstLine.includes('command not found') || firstLine.includes('not found') || firstLine.includes('ENOENT')) {
|
|
91
|
+
return { file: filePath, status: 'WARN', detail: 'jac not installed (pip install jaseci)' };
|
|
92
|
+
}
|
|
93
|
+
return { file: filePath, status: 'FAIL', detail: firstLine.trim() };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function runCheck() {
|
|
97
|
+
const args = process.argv.slice(3);
|
|
98
|
+
let policyPath;
|
|
99
|
+
for (let i = 0; i < args.length; i++) {
|
|
100
|
+
switch (args[i]) {
|
|
101
|
+
case '--help':
|
|
102
|
+
case '-h':
|
|
103
|
+
process.stdout.write(`
|
|
104
|
+
corpus check [options]
|
|
105
|
+
|
|
106
|
+
Validate policy files (YAML and Jac) in the current directory.
|
|
107
|
+
|
|
108
|
+
Options:
|
|
109
|
+
--policy-path <path> Path to a specific policy file or directory
|
|
110
|
+
--help Show this help
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
corpus check Scan CWD for policy files
|
|
114
|
+
corpus check --policy-path ./my-policy.yaml Check a specific file
|
|
115
|
+
corpus check --policy-path ./policies/ Check a specific directory
|
|
116
|
+
|
|
117
|
+
`);
|
|
118
|
+
return;
|
|
119
|
+
case '--policy-path':
|
|
120
|
+
policyPath = args[++i];
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
process.stdout.write('\n');
|
|
125
|
+
process.stdout.write(bold(' CORPUS POLICY CHECK\n'));
|
|
126
|
+
process.stdout.write(' ' + '\u2550'.repeat(46) + '\n\n');
|
|
127
|
+
const files = findPolicyFiles(policyPath);
|
|
128
|
+
if (files.length === 0) {
|
|
129
|
+
process.stdout.write(' No policy files found.\n');
|
|
130
|
+
process.stdout.write(' Run ' + green('corpus init') + ' to create one.\n\n');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const results = [];
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
const result = file.endsWith('.jac') ? checkJacFile(file) : checkYamlFile(file);
|
|
136
|
+
results.push(result);
|
|
137
|
+
const icon = result.status === 'PASS' ? green('PASS') : result.status === 'WARN' ? amber('WARN') : red('FAIL');
|
|
138
|
+
const name = file.padEnd(40);
|
|
139
|
+
process.stdout.write(` ${name} ${icon} ${dim(result.detail)}\n`);
|
|
140
|
+
}
|
|
141
|
+
const failures = results.filter((r) => r.status === 'FAIL');
|
|
142
|
+
const warnings = results.filter((r) => r.status === 'WARN');
|
|
143
|
+
const passes = results.filter((r) => r.status === 'PASS');
|
|
144
|
+
process.stdout.write('\n');
|
|
145
|
+
if (failures.length > 0) {
|
|
146
|
+
process.stdout.write(red(` ${failures.length} file(s) failed. Fix errors before deploying.\n`));
|
|
147
|
+
process.stdout.write('\n');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
if (warnings.length > 0 && passes.length > 0) {
|
|
151
|
+
process.stdout.write(green(` ${passes.length} file(s) passed`) + `, ${warnings.length} warning(s).\n`);
|
|
152
|
+
}
|
|
153
|
+
else if (warnings.length > 0) {
|
|
154
|
+
process.stdout.write(amber(` ${warnings.length} file(s) need attention (jac not installed).\n`));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
process.stdout.write(green(` All ${passes.length} file(s) passed.\n`));
|
|
158
|
+
}
|
|
159
|
+
process.stdout.write('\n');
|
|
160
|
+
if (failures.length > 0)
|
|
161
|
+
process.exit(1);
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* corpus init / corpus graph -- Auto-generate the codebase graph
|
|
3
|
+
*
|
|
4
|
+
* Scans your project, builds a graph of every function, module, and relationship.
|
|
5
|
+
* No configuration needed. One command. Corpus learns your entire codebase.
|
|
6
|
+
*/
|
|
7
|
+
export declare function initGraph(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* corpus init / corpus graph -- Auto-generate the codebase graph
|
|
3
|
+
*
|
|
4
|
+
* Scans your project, builds a graph of every function, module, and relationship.
|
|
5
|
+
* No configuration needed. One command. Corpus learns your entire codebase.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync, appendFileSync } from 'fs';
|
|
8
|
+
import { createServer } from 'http';
|
|
9
|
+
import { exec } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
const C = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
dim: '\x1b[2m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
cyan: '\x1b[36m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
20
|
+
bg_green: '\x1b[42m',
|
|
21
|
+
bg_red: '\x1b[41m',
|
|
22
|
+
white: '\x1b[37m',
|
|
23
|
+
};
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
async function animateStep(text, delay = 80) {
|
|
28
|
+
process.stdout.write(` ${C.green}\u2713${C.reset} ${text}`);
|
|
29
|
+
await sleep(delay);
|
|
30
|
+
process.stdout.write('\n');
|
|
31
|
+
}
|
|
32
|
+
export async function initGraph(args) {
|
|
33
|
+
const flags = args.filter(a => a.startsWith('--'));
|
|
34
|
+
const positional = args.filter(a => !a.startsWith('--'));
|
|
35
|
+
const projectRoot = positional[0] ? path.resolve(positional[0]) : process.cwd();
|
|
36
|
+
const { buildGraph, saveGraph } = await import('@corpus/core');
|
|
37
|
+
if (!existsSync(projectRoot)) {
|
|
38
|
+
console.error(`\n ${C.red}Error:${C.reset} Directory not found: ${projectRoot}\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// ── Header ──
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(` ${C.cyan}${C.bold}CORPUS${C.reset} ${C.dim}The immune system for vibe-coded software${C.reset}`);
|
|
44
|
+
console.log(` ${C.dim}${'─'.repeat(50)}${C.reset}`);
|
|
45
|
+
console.log('');
|
|
46
|
+
// ── Scan ──
|
|
47
|
+
process.stdout.write(` ${C.dim}Scanning project structure...${C.reset}`);
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const graph = buildGraph(projectRoot);
|
|
50
|
+
const elapsed = Date.now() - startTime;
|
|
51
|
+
process.stdout.write(`\r ${C.green}\u2713${C.reset} Scanning project structure... ${C.dim}${elapsed}ms${C.reset}\n`);
|
|
52
|
+
// ── Animated results ──
|
|
53
|
+
await animateStep(`Found ${C.bold}${graph.stats.totalFiles}${C.reset} files across ${C.bold}${new Set(graph.nodes.filter(n => n.type === 'module').map(n => n.file.split('/')[0])).size}${C.reset} modules`);
|
|
54
|
+
await animateStep(`Mapped ${C.bold}${graph.stats.totalFunctions}${C.reset} functions and ${C.bold}${graph.edges.filter(e => e.type === 'calls').length}${C.reset} dependencies`);
|
|
55
|
+
await animateStep(`Building structural graph...`);
|
|
56
|
+
// ── Save ──
|
|
57
|
+
const graphPath = saveGraph(graph, projectRoot);
|
|
58
|
+
await animateStep(`Graph saved: ${C.bold}${graph.nodes.length}${C.reset} nodes, ${C.bold}${graph.edges.length}${C.reset} edges`);
|
|
59
|
+
// ── Config ──
|
|
60
|
+
const corpusDir = path.join(projectRoot, '.corpus');
|
|
61
|
+
if (!existsSync(corpusDir)) {
|
|
62
|
+
mkdirSync(corpusDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
const configPath = path.join(corpusDir, 'config.json');
|
|
65
|
+
if (!existsSync(configPath)) {
|
|
66
|
+
writeFileSync(configPath, JSON.stringify({
|
|
67
|
+
version: 1,
|
|
68
|
+
mode: 'watch',
|
|
69
|
+
autoFix: true,
|
|
70
|
+
mcpEnabled: true,
|
|
71
|
+
}, null, 2));
|
|
72
|
+
}
|
|
73
|
+
// ── MCP config ──
|
|
74
|
+
const mcpPath = path.join(projectRoot, '.mcp.json');
|
|
75
|
+
if (!existsSync(mcpPath)) {
|
|
76
|
+
writeFileSync(mcpPath, JSON.stringify({
|
|
77
|
+
mcpServers: {
|
|
78
|
+
corpus: {
|
|
79
|
+
command: 'npx',
|
|
80
|
+
args: ['corpus-mcp'],
|
|
81
|
+
type: 'stdio',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}, null, 2));
|
|
85
|
+
await animateStep(`MCP watchers attached to ${C.cyan}Claude Code${C.reset}, ${C.cyan}Cursor${C.reset}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const existing = JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
89
|
+
if (!existing.mcpServers?.corpus) {
|
|
90
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
91
|
+
existing.mcpServers.corpus = {
|
|
92
|
+
command: 'npx',
|
|
93
|
+
args: ['corpus-mcp'],
|
|
94
|
+
type: 'stdio',
|
|
95
|
+
};
|
|
96
|
+
writeFileSync(mcpPath, JSON.stringify(existing, null, 2));
|
|
97
|
+
await animateStep(`MCP watchers attached to ${C.cyan}Claude Code${C.reset}, ${C.cyan}Cursor${C.reset}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
await animateStep(`MCP already configured`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Gitignore ──
|
|
104
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
105
|
+
if (existsSync(gitignorePath)) {
|
|
106
|
+
const gitignore = readFileSync(gitignorePath, 'utf-8');
|
|
107
|
+
if (!gitignore.includes('.corpus')) {
|
|
108
|
+
appendFileSync(gitignorePath, '\n.corpus/\n');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ── Health summary ──
|
|
112
|
+
const healthColor = graph.stats.healthScore >= 80 ? C.green
|
|
113
|
+
: graph.stats.healthScore >= 50 ? C.yellow
|
|
114
|
+
: C.red;
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(` ${C.dim}${'─'.repeat(50)}${C.reset}`);
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(` ${C.bold}$ corpus status${C.reset}`);
|
|
119
|
+
console.log(` ${C.green}\u25CF${C.reset} Health: ${healthColor}${C.bold}${graph.stats.healthScore}/100${C.reset} ${C.dim}- All systems nominal${C.reset}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(` ${C.dim}${'─'.repeat(50)}${C.reset}`);
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(` ${C.bold}What happens next:${C.reset}`);
|
|
124
|
+
console.log(` ${C.dim}Every time your AI writes code, Corpus checks it${C.reset}`);
|
|
125
|
+
console.log(` ${C.dim}against this graph. If something breaks, Corpus${C.reset}`);
|
|
126
|
+
console.log(` ${C.dim}tells the AI to fix it. You never see the bug.${C.reset}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(` ${C.cyan}corpus watch${C.reset} ${C.dim}Real-time monitoring${C.reset}`);
|
|
129
|
+
console.log(` ${C.cyan}corpus scan${C.reset} ${C.dim}Security scan${C.reset}`);
|
|
130
|
+
console.log(` ${C.cyan}corpus verify${C.reset} ${C.dim}Trust scores${C.reset}`);
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(` ${C.dim}Your AI can't break what Corpus protects.${C.reset}`);
|
|
133
|
+
console.log(` ${C.bold}No more AI slop.${C.reset}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
// ── --open flag: serve graph in browser ──
|
|
136
|
+
if (flags.includes('--open')) {
|
|
137
|
+
await serveGraph(projectRoot, graph);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function serveGraph(projectRoot, graph) {
|
|
141
|
+
const graphJSON = JSON.stringify({
|
|
142
|
+
nodes: graph.nodes.map((n) => ({
|
|
143
|
+
id: n.id, name: n.name, type: n.type, file: n.file, line: n.line,
|
|
144
|
+
exported: n.exported, health: n.health || 'verified', trustScore: n.trustScore || 100,
|
|
145
|
+
guards: n.guards || [], params: n.params || [],
|
|
146
|
+
})),
|
|
147
|
+
edges: graph.edges.map((e) => ({
|
|
148
|
+
source: e.source, target: e.target, type: e.type,
|
|
149
|
+
})),
|
|
150
|
+
stats: graph.stats,
|
|
151
|
+
});
|
|
152
|
+
const html = `<!DOCTYPE html>
|
|
153
|
+
<html><head><meta charset="utf-8"><title>Corpus Graph — ${path.basename(projectRoot)}</title>
|
|
154
|
+
<style>
|
|
155
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
156
|
+
body { background: #050505; color: #e5e7eb; font-family: system-ui, -apple-system, sans-serif; overflow: hidden; }
|
|
157
|
+
#info { position: fixed; top: 12px; left: 16px; z-index: 10; font-size: 12px; font-family: ui-monospace, monospace; }
|
|
158
|
+
#info span { color: #10b981; }
|
|
159
|
+
#detail { position: fixed; top: 12px; right: 16px; width: 300px; z-index: 10; background: #0a0a0aee; backdrop-filter: blur(8px); border: 1px solid #1f2937; border-radius: 12px; padding: 16px; display: none; font-size: 13px; }
|
|
160
|
+
#detail h3 { font-size: 15px; font-weight: 700; margin-bottom: 8px; }
|
|
161
|
+
#detail .file { color: #6b7280; font-family: ui-monospace, monospace; font-size: 11px; margin-bottom: 8px; }
|
|
162
|
+
#detail .tag { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 10px; font-weight: 500; margin-right: 4px; margin-bottom: 4px; }
|
|
163
|
+
#detail .guards { font-family: ui-monospace, monospace; font-size: 10px; color: #6ee7b7; border-left: 2px solid #10b98140; padding-left: 6px; margin: 2px 0; }
|
|
164
|
+
#detail .score { color: #10b981; font-size: 20px; font-weight: 700; margin-top: 8px; }
|
|
165
|
+
#legend { position: fixed; bottom: 16px; left: 16px; display: flex; gap: 12px; font-size: 10px; font-family: ui-monospace, monospace; background: #0a0a0acc; padding: 6px 12px; border-radius: 8px; z-index: 10; }
|
|
166
|
+
.legend-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin-right: 4px; }
|
|
167
|
+
</style>
|
|
168
|
+
<script src="https://unpkg.com/force-graph@1.43.5/dist/force-graph.min.js"></script>
|
|
169
|
+
</head><body>
|
|
170
|
+
<div id="info"><span>corpus</span> / ${path.basename(projectRoot)} — <span id="node-count"></span> files, <span id="edge-count"></span> edges</div>
|
|
171
|
+
<div id="detail"></div>
|
|
172
|
+
<div id="legend"></div>
|
|
173
|
+
<div id="graph"></div>
|
|
174
|
+
<script>
|
|
175
|
+
const COLORS = { core:'#3b82f6', cli:'#f97316', web:'#a855f7', 'mcp-server':'#06b6d4', 'sdk-ts':'#eab308', 'sdk-python':'#10b981', functions:'#f43f5e', policies:'#8b5cf6', schema:'#64748b', src:'#3b82f6', lib:'#10b981', app:'#a855f7', components:'#06b6d4', pages:'#eab308', api:'#f43f5e', utils:'#8b5cf6', config:'#64748b', scripts:'#f97316', public:'#10b981', styles:'#a855f7' };
|
|
176
|
+
function getCluster(file) { const p = file.split('/'); if (p[0]==='packages'&&p[1]) return p[1]; if (p[0]==='apps'&&p[1]) return p[1]; return p[0]||'root'; }
|
|
177
|
+
function getColor(file) { return COLORS[getCluster(file)] || '#64748b'; }
|
|
178
|
+
|
|
179
|
+
const raw = ${graphJSON};
|
|
180
|
+
const modules = raw.nodes.filter(n => n.type === 'module');
|
|
181
|
+
const funcs = raw.nodes.filter(n => n.type === 'function');
|
|
182
|
+
const moduleIds = new Set(modules.map(n => n.id));
|
|
183
|
+
const funcToMod = {};
|
|
184
|
+
raw.nodes.forEach(n => { if (n.type==='function') { const mid='mod:'+n.file; if(moduleIds.has(mid)) funcToMod[n.id]=mid; }});
|
|
185
|
+
|
|
186
|
+
const edgeSet = new Set();
|
|
187
|
+
const links = [];
|
|
188
|
+
raw.edges.forEach(e => {
|
|
189
|
+
let s = moduleIds.has(e.source)?e.source:funcToMod[e.source];
|
|
190
|
+
let t = moduleIds.has(e.target)?e.target:funcToMod[e.target];
|
|
191
|
+
if(s&&t&&s!==t) { const k=s+'::'+t; if(!edgeSet.has(k)){edgeSet.add(k);links.push({source:s,target:t});}}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const nodes = modules.map(n => {
|
|
195
|
+
const fc = funcs.filter(f=>f.file===n.file).length;
|
|
196
|
+
return {...n, cluster:getCluster(n.file), color:getColor(n.file), funcCount:fc, val:Math.max(2,fc+1)};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
document.getElementById('node-count').textContent = nodes.length;
|
|
200
|
+
document.getElementById('edge-count').textContent = links.length;
|
|
201
|
+
|
|
202
|
+
// Legend
|
|
203
|
+
const clusters = [...new Set(nodes.map(n=>n.cluster))];
|
|
204
|
+
const legend = document.getElementById('legend');
|
|
205
|
+
clusters.forEach(c => {
|
|
206
|
+
const s = document.createElement('span');
|
|
207
|
+
s.innerHTML = '<span class="legend-dot" style="background:'+( COLORS[c]||'#64748b')+'"></span>'+c;
|
|
208
|
+
s.style.color = COLORS[c]||'#64748b';
|
|
209
|
+
legend.appendChild(s);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const Graph = ForceGraph()(document.getElementById('graph'))
|
|
213
|
+
.graphData({nodes, links})
|
|
214
|
+
.backgroundColor('#050505')
|
|
215
|
+
.linkColor(() => 'rgba(255,255,255,0.06)')
|
|
216
|
+
.linkWidth(0.5)
|
|
217
|
+
.nodeCanvasObject((node, ctx, globalScale) => {
|
|
218
|
+
const r = Math.sqrt(node.val)*2.5;
|
|
219
|
+
ctx.beginPath(); ctx.arc(node.x, node.y, r, 0, 2*Math.PI);
|
|
220
|
+
ctx.fillStyle = node.color+'90'; ctx.fill();
|
|
221
|
+
ctx.strokeStyle = node.color; ctx.lineWidth = 0.5/globalScale; ctx.stroke();
|
|
222
|
+
if(globalScale > 0.8) {
|
|
223
|
+
ctx.font = (10/globalScale)+'px ui-monospace, monospace';
|
|
224
|
+
ctx.textAlign='center'; ctx.textBaseline='top';
|
|
225
|
+
ctx.fillStyle='#ffffffaa';
|
|
226
|
+
ctx.fillText(node.name, node.x, node.y+r+2);
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
.nodePointerAreaPaint((node, color, ctx) => {
|
|
230
|
+
ctx.beginPath(); ctx.arc(node.x, node.y, Math.sqrt(node.val)*2.5+4, 0, 2*Math.PI);
|
|
231
|
+
ctx.fillStyle=color; ctx.fill();
|
|
232
|
+
})
|
|
233
|
+
.onNodeClick(node => {
|
|
234
|
+
const d = document.getElementById('detail');
|
|
235
|
+
const fileFuncs = funcs.filter(f=>f.file===node.file);
|
|
236
|
+
d.style.display = 'block';
|
|
237
|
+
d.innerHTML = '<h3>'+node.name+'</h3>'
|
|
238
|
+
+'<div class="file">'+node.file+':'+node.line+'</div>'
|
|
239
|
+
+'<div><span class="tag" style="background:'+node.color+'20;color:'+node.color+'">'+node.cluster+'</span>'
|
|
240
|
+
+'<span class="tag" style="background:#37415120;color:#9ca3af">'+node.type+'</span>'
|
|
241
|
+
+(node.exported?'<span class="tag" style="background:#10b98120;color:#10b981">exported</span>':'')
|
|
242
|
+
+'</div>'
|
|
243
|
+
+(fileFuncs.length?'<div style="margin-top:8px;color:#6b7280;font-size:9px;text-transform:uppercase;letter-spacing:0.05em">Functions ('+fileFuncs.length+')</div>'
|
|
244
|
+
+fileFuncs.map(f=>'<div style="font-family:ui-monospace,monospace;font-size:10px;color:'+(f.exported?'#a5b4fc':'#6b7280')+';padding:2px 0">'+f.name+(f.guards?.length?' <span style="color:#10b981;font-size:8px">guarded</span>':'')+'</div>').join(''):'')
|
|
245
|
+
+'<div class="score">'+node.trustScore+'<span style="color:#6b7280;font-size:12px;font-weight:400">/100</span></div>';
|
|
246
|
+
Graph.centerAt(node.x, node.y, 400);
|
|
247
|
+
Graph.zoom(3, 400);
|
|
248
|
+
})
|
|
249
|
+
.cooldownTicks(80)
|
|
250
|
+
.d3AlphaDecay(0.03)
|
|
251
|
+
.d3VelocityDecay(0.3);
|
|
252
|
+
</script></body></html>`;
|
|
253
|
+
const server = createServer((req, res) => {
|
|
254
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
255
|
+
res.end(html);
|
|
256
|
+
});
|
|
257
|
+
await new Promise((resolve) => {
|
|
258
|
+
server.listen(0, () => {
|
|
259
|
+
const addr = server.address();
|
|
260
|
+
const port = typeof addr === 'object' && addr ? addr.port : 3456;
|
|
261
|
+
const url = `http://localhost:${port}`;
|
|
262
|
+
console.log(` ${C.cyan}Graph server${C.reset} running at ${C.bold}${url}${C.reset}`);
|
|
263
|
+
console.log(` ${C.dim}Press Ctrl+C to stop${C.reset}`);
|
|
264
|
+
console.log('');
|
|
265
|
+
// Open browser
|
|
266
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
267
|
+
exec(`${cmd} ${url}`);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runInit(): Promise<void>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
2
|
+
import { resolve as resolvePath } from 'path';
|
|
3
|
+
function configureMcp() {
|
|
4
|
+
const mcpPath = '.mcp.json';
|
|
5
|
+
let config = {};
|
|
6
|
+
if (existsSync(mcpPath)) {
|
|
7
|
+
try {
|
|
8
|
+
config = JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
9
|
+
}
|
|
10
|
+
catch { /* invalid json, overwrite */ }
|
|
11
|
+
}
|
|
12
|
+
const servers = (config.mcpServers ?? {});
|
|
13
|
+
if ('corpus' in servers)
|
|
14
|
+
return; // Already configured
|
|
15
|
+
// Find the corpus MCP server binary
|
|
16
|
+
const homeCorpus = `${process.env.HOME}/.corpus/packages/mcp-server/dist/index.js`;
|
|
17
|
+
const localCorpus = '.corpus/packages/mcp-server/dist/index.js';
|
|
18
|
+
if (existsSync(homeCorpus)) {
|
|
19
|
+
servers.corpus = {
|
|
20
|
+
command: 'node',
|
|
21
|
+
args: [homeCorpus],
|
|
22
|
+
type: 'stdio',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
else if (existsSync(localCorpus)) {
|
|
26
|
+
servers.corpus = {
|
|
27
|
+
command: 'node',
|
|
28
|
+
args: [resolvePath(localCorpus)],
|
|
29
|
+
type: 'stdio',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
servers.corpus = {
|
|
34
|
+
command: 'npx',
|
|
35
|
+
args: ['corpus-mcp'],
|
|
36
|
+
type: 'stdio',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
config.mcpServers = servers;
|
|
40
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n');
|
|
41
|
+
}
|
|
42
|
+
import { createInterface } from 'readline';
|
|
43
|
+
import path from 'path';
|
|
44
|
+
import { green, bold, dim, cyan } from '../utils/colors.js';
|
|
45
|
+
function ask(rl, question, defaultVal) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
rl.question(` ${question} ${dim(`(${defaultVal})`)}: `, (answer) => {
|
|
48
|
+
resolve(answer.trim() || defaultVal);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function kebabCase(s) {
|
|
53
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
54
|
+
}
|
|
55
|
+
function installPreCommitHook() {
|
|
56
|
+
const gitDir = '.git';
|
|
57
|
+
if (!existsSync(gitDir))
|
|
58
|
+
return false;
|
|
59
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
60
|
+
if (!existsSync(hooksDir))
|
|
61
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
62
|
+
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
63
|
+
const hookContent = `#!/bin/sh
|
|
64
|
+
# Corpus pre-commit hook - scans staged files for secrets, PII, and unsafe code
|
|
65
|
+
# Installed by: corpus init --hooks
|
|
66
|
+
|
|
67
|
+
echo "[corpus] Scanning staged changes..."
|
|
68
|
+
npx corpus scan --staged
|
|
69
|
+
|
|
70
|
+
EXIT_CODE=$?
|
|
71
|
+
if [ $EXIT_CODE -eq 2 ]; then
|
|
72
|
+
echo ""
|
|
73
|
+
echo "[corpus] BLOCKED: Critical security issues found. Fix them before committing."
|
|
74
|
+
echo "[corpus] Run 'corpus scan' for details, or 'git commit --no-verify' to bypass."
|
|
75
|
+
exit 1
|
|
76
|
+
elif [ $EXIT_CODE -eq 1 ]; then
|
|
77
|
+
echo "[corpus] Warnings found. Proceeding with commit."
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
exit 0
|
|
81
|
+
`;
|
|
82
|
+
// Check if hook already exists
|
|
83
|
+
if (existsSync(hookPath)) {
|
|
84
|
+
const existing = readFileSync(hookPath, 'utf-8');
|
|
85
|
+
if (existing.includes('corpus')) {
|
|
86
|
+
return false; // Already installed
|
|
87
|
+
}
|
|
88
|
+
// Append to existing hook
|
|
89
|
+
writeFileSync(hookPath, existing + '\n' + hookContent);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
writeFileSync(hookPath, hookContent);
|
|
93
|
+
}
|
|
94
|
+
chmodSync(hookPath, '755');
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
function installHuskyHook() {
|
|
98
|
+
const huskyDir = '.husky';
|
|
99
|
+
if (!existsSync(huskyDir))
|
|
100
|
+
return false;
|
|
101
|
+
const hookPath = path.join(huskyDir, 'pre-commit');
|
|
102
|
+
const hookContent = `npx corpus scan --staged\n`;
|
|
103
|
+
if (existsSync(hookPath)) {
|
|
104
|
+
const existing = readFileSync(hookPath, 'utf-8');
|
|
105
|
+
if (existing.includes('corpus'))
|
|
106
|
+
return false;
|
|
107
|
+
writeFileSync(hookPath, existing + '\n' + hookContent);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
writeFileSync(hookPath, hookContent);
|
|
111
|
+
chmodSync(hookPath, '755');
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
export async function runInit() {
|
|
116
|
+
const args = process.argv.slice(3);
|
|
117
|
+
const wantHooks = args.includes('--hooks');
|
|
118
|
+
const wantHusky = args.includes('--husky');
|
|
119
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
120
|
+
const policyPath = './corpus.policy.yaml';
|
|
121
|
+
if (existsSync(policyPath)) {
|
|
122
|
+
const overwrite = await ask(rl, 'corpus.policy.yaml exists. Overwrite?', 'n');
|
|
123
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
124
|
+
process.stdout.write(' Keeping existing policy file.\n');
|
|
125
|
+
// Still install hooks if requested
|
|
126
|
+
if (wantHooks || wantHusky) {
|
|
127
|
+
installHooks(wantHusky);
|
|
128
|
+
}
|
|
129
|
+
rl.close();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const dirName = path.basename(process.cwd());
|
|
134
|
+
const projectName = await ask(rl, 'Project name', dirName);
|
|
135
|
+
const projectSlug = await ask(rl, 'Project slug', kebabCase(projectName));
|
|
136
|
+
// Ask about hooks if not specified via flag
|
|
137
|
+
let doHooks = wantHooks || wantHusky;
|
|
138
|
+
if (!doHooks) {
|
|
139
|
+
const hookAnswer = await ask(rl, 'Install pre-commit hook? (scans before every commit)', 'y');
|
|
140
|
+
doHooks = hookAnswer.toLowerCase() === 'y';
|
|
141
|
+
}
|
|
142
|
+
rl.close();
|
|
143
|
+
// Write policy file
|
|
144
|
+
const policyTemplate = `# corpus.policy.yaml
|
|
145
|
+
# Generated by corpus init
|
|
146
|
+
# Scans for: secrets, PII, prompt injection, unsafe code patterns
|
|
147
|
+
|
|
148
|
+
agent: ${projectSlug}
|
|
149
|
+
version: "1.0"
|
|
150
|
+
|
|
151
|
+
rules:
|
|
152
|
+
- name: block_production_writes
|
|
153
|
+
verdict: BLOCK
|
|
154
|
+
message: "Production writes require a deployment process."
|
|
155
|
+
trigger:
|
|
156
|
+
actionContains: ["prod", "production"]
|
|
157
|
+
|
|
158
|
+
- name: confirm_external_calls
|
|
159
|
+
verdict: CONFIRM
|
|
160
|
+
message: "This action calls an external service. Proceed?"
|
|
161
|
+
trigger:
|
|
162
|
+
actionStartsWith: ["http_request", "webhook_call", "api_call"]
|
|
163
|
+
|
|
164
|
+
- name: low_confidence_block
|
|
165
|
+
verdict: BLOCK
|
|
166
|
+
blockReason: LOW_CONFIDENCE
|
|
167
|
+
message: "Not confident enough to act. Please clarify your intent."
|
|
168
|
+
trigger:
|
|
169
|
+
contextKey: "confidence"
|
|
170
|
+
contextValueBelow: 0.50
|
|
171
|
+
`;
|
|
172
|
+
writeFileSync(policyPath, policyTemplate);
|
|
173
|
+
// Install hooks
|
|
174
|
+
let hookResult = '';
|
|
175
|
+
if (doHooks) {
|
|
176
|
+
hookResult = installHooks(wantHusky);
|
|
177
|
+
}
|
|
178
|
+
// Auto-configure MCP for Claude Code if .mcp.json doesn't have corpus
|
|
179
|
+
configureMcp();
|
|
180
|
+
// Print summary
|
|
181
|
+
process.stdout.write('\n');
|
|
182
|
+
process.stdout.write(green(bold(` Corpus initialized for ${projectName}\n`)));
|
|
183
|
+
process.stdout.write('\n');
|
|
184
|
+
process.stdout.write(` Policy file: ${cyan('corpus.policy.yaml')}\n`);
|
|
185
|
+
process.stdout.write(` MCP config: ${cyan('.mcp.json')} ${dim('(Claude Code / Cursor integration)')}\n`);
|
|
186
|
+
if (hookResult) {
|
|
187
|
+
process.stdout.write(` Pre-commit: ${green(hookResult)}\n`);
|
|
188
|
+
}
|
|
189
|
+
process.stdout.write('\n');
|
|
190
|
+
process.stdout.write(` ${bold('What Corpus scans for:')}\n`);
|
|
191
|
+
process.stdout.write(` Secrets API keys, tokens, credentials, database URLs\n`);
|
|
192
|
+
process.stdout.write(` PII Email addresses, phone numbers, SSNs\n`);
|
|
193
|
+
process.stdout.write(` Injection Prompt injection patterns in content\n`);
|
|
194
|
+
process.stdout.write(` Safety eval(), innerHTML, SQL injection, disabled SSL\n`);
|
|
195
|
+
process.stdout.write(` AI patterns Inlined env vars, placeholder creds, wildcard CORS\n`);
|
|
196
|
+
process.stdout.write('\n');
|
|
197
|
+
process.stdout.write(` ${bold('Commands:')}\n`);
|
|
198
|
+
process.stdout.write(` corpus scan Scan your codebase now\n`);
|
|
199
|
+
process.stdout.write(` corpus scan --staged Scan only staged changes\n`);
|
|
200
|
+
process.stdout.write(` corpus watch Real-time file watcher\n`);
|
|
201
|
+
process.stdout.write(` corpus check Validate policy files\n`);
|
|
202
|
+
process.stdout.write('\n');
|
|
203
|
+
}
|
|
204
|
+
function installHooks(useHusky) {
|
|
205
|
+
if (useHusky) {
|
|
206
|
+
const installed = installHuskyHook();
|
|
207
|
+
return installed ? 'Husky pre-commit hook installed' : 'Husky hook already exists';
|
|
208
|
+
}
|
|
209
|
+
const installed = installPreCommitHook();
|
|
210
|
+
return installed ? 'Git pre-commit hook installed' : 'Hook already installed';
|
|
211
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runReport(): Promise<void>;
|