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 @@
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>;