@safetnsr/vet 0.2.1 → 0.4.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/README.md +38 -9
- package/dist/checks/edge.d.ts +30 -0
- package/dist/checks/edge.js +230 -0
- package/dist/checks/receipt.d.ts +48 -0
- package/dist/checks/receipt.js +306 -0
- package/dist/checks/scan.d.ts +2 -0
- package/dist/checks/scan.js +225 -0
- package/dist/checks/secrets.d.ts +2 -0
- package/dist/checks/secrets.js +280 -0
- package/dist/cli.js +31 -11
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vet
|
|
2
2
|
|
|
3
|
-
vet your AI-generated code. one command,
|
|
3
|
+
vet your AI-generated code. one command, eight checks, zero config.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @safetnsr/vet
|
|
@@ -15,9 +15,11 @@ works with Claude Code, Cursor, Copilot, Codex, Aider, Windsurf, Cline — anyth
|
|
|
15
15
|
| **ready** | is your codebase AI-friendly? | scans structure, docs, types, tests |
|
|
16
16
|
| **diff** | did the AI leave anti-patterns? | AI-specific patterns: wholesale rewrites, orphaned imports, catch-alls, over-commenting, plus secrets & stubs |
|
|
17
17
|
| **models** | using deprecated AI models? | scans code for sunset model strings across OpenAI, Anthropic, Google, Cohere |
|
|
18
|
-
| **links** | broken markdown links? | validates relative links and wikilinks |
|
|
19
18
|
| **config** | agent configs in place? | deep analysis of CLAUDE.md, .cursorrules, copilot-instructions — checks completeness, consistency, and specificity against your actual codebase |
|
|
20
19
|
| **history** | git patterns healthy? | analyzes commit churn, AI attribution, large changes |
|
|
20
|
+
| **scan** | malicious patterns in agent configs? | scans .claude/, .cursorrules, CLAUDE.md, .mcp/ for prompt injection, shell injection, exfiltration endpoints |
|
|
21
|
+
| **secrets** | leaked secrets in build output? | scans dist/, build/, .next/ + .env files for API keys, tokens, connection strings using pattern + entropy analysis |
|
|
22
|
+
| **receipt** | what did the last agent session do? | parses ~/.claude/projects/ JSONL session logs — files changed, commands run, packages installed, SHA256 integrity hash |
|
|
21
23
|
|
|
22
24
|
## usage
|
|
23
25
|
|
|
@@ -45,19 +47,25 @@ npx @safetnsr/vet --json
|
|
|
45
47
|
|
|
46
48
|
# generate configs + pre-commit hook
|
|
47
49
|
npx @safetnsr/vet init
|
|
50
|
+
|
|
51
|
+
# show last agent session receipt (ASCII or JSON)
|
|
52
|
+
npx @safetnsr/vet receipt
|
|
53
|
+
npx @safetnsr/vet receipt --json
|
|
48
54
|
```
|
|
49
55
|
|
|
50
56
|
## output
|
|
51
57
|
|
|
52
58
|
```
|
|
53
|
-
my-project
|
|
59
|
+
my-project 7.5/10
|
|
54
60
|
|
|
55
61
|
ready ████░░░░░░ 4 3 readiness issues
|
|
56
62
|
diff ████████░░ 8 3 issues (2 AI-specific) in 5 files
|
|
57
63
|
models ██████████ 10 all models current
|
|
58
|
-
links ██████░░░░ 6 3 broken links in docs/
|
|
59
64
|
config ███░░░░░░░ 3 Cursor — needs work (3/10)
|
|
60
65
|
history █████████░ 9 41 commits (~15% AI-attributed)
|
|
66
|
+
scan ██████████ 10 no malicious patterns found
|
|
67
|
+
secrets ██████████ 10 no leaked secrets
|
|
68
|
+
receipt ██████████ 10 last session: 3 files, 2 commands
|
|
61
69
|
|
|
62
70
|
✗ no README — AI agents have no project context
|
|
63
71
|
✗ no tests — AI agents produce better code when tests exist
|
|
@@ -69,7 +77,7 @@ npx @safetnsr/vet init
|
|
|
69
77
|
|
|
70
78
|
## --fix
|
|
71
79
|
|
|
72
|
-
`vet --fix`
|
|
80
|
+
`vet --fix` analyzes your codebase and generates project-specific configs:
|
|
73
81
|
|
|
74
82
|
```bash
|
|
75
83
|
$ npx @safetnsr/vet --fix
|
|
@@ -83,12 +91,10 @@ $ npx @safetnsr/vet --fix
|
|
|
83
91
|
fixed 3 issues
|
|
84
92
|
```
|
|
85
93
|
|
|
86
|
-
the generated CLAUDE.md includes your actual stack, directory structure, and framework-specific rules
|
|
94
|
+
the generated CLAUDE.md includes your actual stack, directory structure, and framework-specific rules.
|
|
87
95
|
|
|
88
96
|
## AI-specific diff patterns
|
|
89
97
|
|
|
90
|
-
vet catches things that are specific to AI-generated code:
|
|
91
|
-
|
|
92
98
|
| pattern | what it catches |
|
|
93
99
|
|---------|----------------|
|
|
94
100
|
| `[ai] wholesale rewrite` | AI rewrote an entire function when a small edit would suffice |
|
|
@@ -109,13 +115,36 @@ config score breakdown:
|
|
|
109
115
|
specificity: 3/10 — generic rules, nothing project-specific
|
|
110
116
|
```
|
|
111
117
|
|
|
118
|
+
## subcommands
|
|
119
|
+
|
|
120
|
+
### `vet receipt`
|
|
121
|
+
|
|
122
|
+
Shows a receipt for the last Claude Code agent session — what files it touched, what commands it ran, what packages it installed, plus a SHA256 integrity hash:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
╔══════════════════════════════════════════════╗
|
|
126
|
+
║ AGENT SESSION RECEIPT ║
|
|
127
|
+
╠══════════════════════════════════════════════╣
|
|
128
|
+
║ Session: abc123def456 ║
|
|
129
|
+
║ Date: 2024-01-15 14:32:11 UTC ║
|
|
130
|
+
║ Duration: 12m 34s ║
|
|
131
|
+
╠══════════════════════════════════════════════╣
|
|
132
|
+
║ FILES CREATED (3) ║
|
|
133
|
+
║ src/checks/scan.ts ║
|
|
134
|
+
║ src/checks/secrets.ts ║
|
|
135
|
+
║ test/scan.test.mjs ║
|
|
136
|
+
╠══════════════════════════════════════════════╣
|
|
137
|
+
║ SHA256: 3a7f9c2e... ║
|
|
138
|
+
╚══════════════════════════════════════════════╝
|
|
139
|
+
```
|
|
140
|
+
|
|
112
141
|
## config
|
|
113
142
|
|
|
114
143
|
create `.vetrc` in your project root (optional):
|
|
115
144
|
|
|
116
145
|
```json
|
|
117
146
|
{
|
|
118
|
-
"checks": ["ready", "diff", "models", "
|
|
147
|
+
"checks": ["ready", "diff", "models", "config", "history", "scan", "secrets", "receipt"],
|
|
119
148
|
"ignore": ["vendor/", "generated/"],
|
|
120
149
|
"thresholds": { "min": 6 }
|
|
121
150
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
type CommitCategory = 'architecture' | 'debugging' | 'integration' | 'feature' | 'boilerplate' | 'cosmetic';
|
|
3
|
+
interface DiffStats {
|
|
4
|
+
filesChanged: number;
|
|
5
|
+
insertions: number;
|
|
6
|
+
deletions: number;
|
|
7
|
+
dirsChanged: number;
|
|
8
|
+
files: string[];
|
|
9
|
+
}
|
|
10
|
+
interface ClassifiedCommit {
|
|
11
|
+
hash: string;
|
|
12
|
+
date: string;
|
|
13
|
+
message: string;
|
|
14
|
+
author: string;
|
|
15
|
+
category: CommitCategory;
|
|
16
|
+
score: number;
|
|
17
|
+
reasoning: string;
|
|
18
|
+
stats: DiffStats;
|
|
19
|
+
}
|
|
20
|
+
export interface EdgeAnalysis {
|
|
21
|
+
score: number;
|
|
22
|
+
totalCommits: number;
|
|
23
|
+
distribution: Record<CommitCategory, number>;
|
|
24
|
+
topCommits: ClassifiedCommit[];
|
|
25
|
+
recommendation: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function analyzeEdge(cwd: string, maxCommits?: number): EdgeAnalysis;
|
|
28
|
+
export declare function checkEdge(cwd: string): CheckResult;
|
|
29
|
+
export declare function runEdgeCommand(cwd: string, explain?: boolean): void;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
const CATEGORY_SCORES = {
|
|
3
|
+
architecture: 90,
|
|
4
|
+
debugging: 85,
|
|
5
|
+
integration: 80,
|
|
6
|
+
feature: 60,
|
|
7
|
+
boilerplate: 20,
|
|
8
|
+
cosmetic: 10,
|
|
9
|
+
};
|
|
10
|
+
const BOILERPLATE_RE = [/\bcrud\b/i, /\bscaffold/i, /\bgenerat(e|ed|ing)\b/i, /\binit(ial)?\b/i, /\bboilerplate\b/i, /\btemplate\b/i, /\bsetup\b/i];
|
|
11
|
+
const COSMETIC_RE = [/\brenam(e|ed|ing)\b/i, /\bformat(ting)?\b/i, /\blint(ing)?\b/i, /\bprettier\b/i, /\bstyle\b/i, /\bwhitespace\b/i, /\btypo\b/i];
|
|
12
|
+
const DEBUG_RE = [/\bfix(e[ds])?\b/i, /\bbug\b/i, /\bdebug/i, /\bpatch\b/i, /\bhotfix\b/i, /\bresolv(e|ed|ing)\b/i, /\berror\b/i, /\bissue\b/i];
|
|
13
|
+
const INTEGRATION_RE = [/\bintegrat(e|ion|ing)\b/i, /\bapi\b/i, /\bwebhook\b/i, /\bmigrat(e|ion|ing)\b/i, /\bdatabase\b/i, /\bqueue\b/i, /\bconnect/i, /\bpipeline\b/i, /\bauth(entication|orization)?\b/i];
|
|
14
|
+
function getUniqueDirs(files) {
|
|
15
|
+
const dirs = new Set();
|
|
16
|
+
for (const f of files) {
|
|
17
|
+
const parts = f.split('/');
|
|
18
|
+
dirs.add(parts.length > 1 ? parts.slice(0, -1).join('/') : '.');
|
|
19
|
+
}
|
|
20
|
+
return [...dirs];
|
|
21
|
+
}
|
|
22
|
+
function hasMultiSystemFiles(files) {
|
|
23
|
+
const layers = new Set();
|
|
24
|
+
for (const f of files) {
|
|
25
|
+
const lower = f.toLowerCase();
|
|
26
|
+
if (/route|controller|handler/.test(lower))
|
|
27
|
+
layers.add('api');
|
|
28
|
+
if (/model|schema|migration|db/.test(lower))
|
|
29
|
+
layers.add('db');
|
|
30
|
+
if (/test|spec/.test(lower))
|
|
31
|
+
layers.add('test');
|
|
32
|
+
if (/config|\.env|yaml|yml/.test(lower))
|
|
33
|
+
layers.add('config');
|
|
34
|
+
if (/middleware|auth/.test(lower))
|
|
35
|
+
layers.add('middleware');
|
|
36
|
+
if (/service|worker/.test(lower))
|
|
37
|
+
layers.add('service');
|
|
38
|
+
}
|
|
39
|
+
return layers.size >= 2;
|
|
40
|
+
}
|
|
41
|
+
function isCosmetic(stats, message) {
|
|
42
|
+
const total = stats.insertions + stats.deletions;
|
|
43
|
+
if (total < 10 && COSMETIC_RE.some(p => p.test(message)))
|
|
44
|
+
return true;
|
|
45
|
+
if (stats.insertions > 0 && stats.deletions > 0 &&
|
|
46
|
+
Math.abs(stats.insertions - stats.deletions) <= 2 &&
|
|
47
|
+
total < 20 && COSMETIC_RE.some(p => p.test(message)))
|
|
48
|
+
return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function classifyCommit(hash, date, message, author, stats) {
|
|
52
|
+
const { filesChanged, insertions, deletions, dirsChanged } = stats;
|
|
53
|
+
const total = insertions + deletions;
|
|
54
|
+
let category;
|
|
55
|
+
let reasoning;
|
|
56
|
+
if (isCosmetic(stats, message)) {
|
|
57
|
+
category = 'cosmetic';
|
|
58
|
+
reasoning = `Small change (${total} lines) matching cosmetic patterns`;
|
|
59
|
+
}
|
|
60
|
+
else if (BOILERPLATE_RE.some(p => p.test(message)) && (insertions > deletions * 3 || deletions === 0)) {
|
|
61
|
+
category = 'boilerplate';
|
|
62
|
+
reasoning = `Scaffolding pattern in commit message (${insertions} insertions, ${deletions} deletions)`;
|
|
63
|
+
}
|
|
64
|
+
else if (dirsChanged >= 3 && filesChanged >= 4) {
|
|
65
|
+
category = 'architecture';
|
|
66
|
+
reasoning = `Cross-directory changes (${dirsChanged} dirs, ${filesChanged} files) — structural refactoring`;
|
|
67
|
+
}
|
|
68
|
+
else if (filesChanged <= 2 && total <= 30 && DEBUG_RE.some(p => p.test(message))) {
|
|
69
|
+
category = 'debugging';
|
|
70
|
+
reasoning = `Targeted fix (${total} lines in ${filesChanged} file${filesChanged > 1 ? 's' : ''}) — context-heavy debugging`;
|
|
71
|
+
}
|
|
72
|
+
else if (INTEGRATION_RE.some(p => p.test(message)) || (dirsChanged >= 2 && hasMultiSystemFiles(stats.files))) {
|
|
73
|
+
category = 'integration';
|
|
74
|
+
reasoning = 'Multi-system wiring — connecting different parts of the stack';
|
|
75
|
+
}
|
|
76
|
+
else if (insertions > 100 && deletions < 10 && filesChanged >= 3 && deletions < insertions * 0.1) {
|
|
77
|
+
category = 'boilerplate';
|
|
78
|
+
reasoning = `Uniform additions (${insertions} insertions, ${deletions} deletions)`;
|
|
79
|
+
}
|
|
80
|
+
else if (total > 0) {
|
|
81
|
+
if (dirsChanged >= 2 && filesChanged >= 3 && total > 50) {
|
|
82
|
+
category = 'architecture';
|
|
83
|
+
reasoning = `Multi-directory changes (${dirsChanged} dirs, ${filesChanged} files, ${total} lines)`;
|
|
84
|
+
}
|
|
85
|
+
else if (DEBUG_RE.some(p => p.test(message)) && filesChanged <= 3) {
|
|
86
|
+
category = 'debugging';
|
|
87
|
+
reasoning = `Bug fix across ${filesChanged} file${filesChanged > 1 ? 's' : ''}`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
category = 'feature';
|
|
91
|
+
reasoning = `Feature work (${filesChanged} file${filesChanged > 1 ? 's' : ''}, ${total} lines)`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
category = 'cosmetic';
|
|
96
|
+
reasoning = 'Empty or metadata-only commit';
|
|
97
|
+
}
|
|
98
|
+
return { hash: hash.slice(0, 8), date, message: message.slice(0, 80), author, category, score: CATEGORY_SCORES[category], reasoning, stats };
|
|
99
|
+
}
|
|
100
|
+
// ── Git log parsing (no simple-git, uses execSync) ───────────────────────────
|
|
101
|
+
function gitRaw(args, cwd) {
|
|
102
|
+
try {
|
|
103
|
+
return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function getCommitStats(hash, cwd) {
|
|
110
|
+
let raw = gitRaw(['diff', '--numstat', `${hash}^`, hash, '--'], cwd);
|
|
111
|
+
if (!raw) {
|
|
112
|
+
raw = gitRaw(['diff-tree', '--numstat', '--root', hash, '--'], cwd);
|
|
113
|
+
}
|
|
114
|
+
const files = [];
|
|
115
|
+
let insertions = 0;
|
|
116
|
+
let deletions = 0;
|
|
117
|
+
for (const line of raw.split('\n').filter(Boolean)) {
|
|
118
|
+
const parts = line.split('\t');
|
|
119
|
+
if (parts.length >= 3) {
|
|
120
|
+
insertions += parts[0] === '-' ? 0 : (parseInt(parts[0] ?? '0', 10) || 0);
|
|
121
|
+
deletions += parts[1] === '-' ? 0 : (parseInt(parts[1] ?? '0', 10) || 0);
|
|
122
|
+
if (parts[2])
|
|
123
|
+
files.push(parts[2]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { filesChanged: files.length, insertions, deletions, dirsChanged: getUniqueDirs(files).length, files };
|
|
127
|
+
}
|
|
128
|
+
function getRecommendation(dist, total) {
|
|
129
|
+
if (total === 0)
|
|
130
|
+
return 'no commits to analyze';
|
|
131
|
+
const boilerPct = ((dist.boilerplate + dist.cosmetic) / total) * 100;
|
|
132
|
+
const archPct = ((dist.architecture + dist.integration) / total) * 100;
|
|
133
|
+
if (boilerPct > 40)
|
|
134
|
+
return 'Focus more on cross-system architecture and targeted debugging. Delegate scaffolding to AI.';
|
|
135
|
+
if (archPct > 50)
|
|
136
|
+
return 'Strong position. Your work is deeply contextual and hard to automate.';
|
|
137
|
+
if (dist.debugging > dist.architecture)
|
|
138
|
+
return 'Good debugging instinct. Level up by taking on more cross-system architecture.';
|
|
139
|
+
return 'Balanced mix. Push toward more integration and architecture work to increase your irreplaceability.';
|
|
140
|
+
}
|
|
141
|
+
export function analyzeEdge(cwd, maxCommits = 50) {
|
|
142
|
+
const raw = gitRaw(['log', '--format=%H|%aI|%an|%s', '--no-merges', `-n`, String(maxCommits)], cwd);
|
|
143
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
144
|
+
const commits = [];
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const pipeIdx = line.indexOf('|');
|
|
147
|
+
const rest = line.slice(pipeIdx + 1);
|
|
148
|
+
const hash = line.slice(0, pipeIdx);
|
|
149
|
+
const [date, author, ...msgParts] = rest.split('|');
|
|
150
|
+
const message = msgParts.join('|');
|
|
151
|
+
if (!hash)
|
|
152
|
+
continue;
|
|
153
|
+
const stats = getCommitStats(hash, cwd);
|
|
154
|
+
commits.push(classifyCommit(hash, date ?? '', message ?? '', author ?? '', stats));
|
|
155
|
+
}
|
|
156
|
+
const dist = { architecture: 0, debugging: 0, integration: 0, feature: 0, boilerplate: 0, cosmetic: 0 };
|
|
157
|
+
for (const c of commits)
|
|
158
|
+
dist[c.category]++;
|
|
159
|
+
const overallScore = commits.length > 0
|
|
160
|
+
? Math.round(commits.reduce((s, c) => s + c.score, 0) / commits.length)
|
|
161
|
+
: 0;
|
|
162
|
+
const topCommits = [...commits].sort((a, b) => b.score - a.score).slice(0, 3);
|
|
163
|
+
return { score: overallScore, totalCommits: commits.length, distribution: dist, topCommits, recommendation: getRecommendation(dist, commits.length) };
|
|
164
|
+
}
|
|
165
|
+
// ── CheckResult adapter ──────────────────────────────────────────────────────
|
|
166
|
+
export function checkEdge(cwd) {
|
|
167
|
+
const analysis = analyzeEdge(cwd);
|
|
168
|
+
const issues = [];
|
|
169
|
+
if (analysis.totalCommits === 0) {
|
|
170
|
+
return { name: 'edge', score: 5, maxScore: 10, issues: [{ severity: 'info', message: 'no commits to analyze', fixable: false }], summary: 'no git history' };
|
|
171
|
+
}
|
|
172
|
+
const { distribution: dist, totalCommits: total } = analysis;
|
|
173
|
+
const boilerPct = Math.round(((dist.boilerplate + dist.cosmetic) / total) * 100);
|
|
174
|
+
const archPct = Math.round(((dist.architecture + dist.integration) / total) * 100);
|
|
175
|
+
if (boilerPct > 50) {
|
|
176
|
+
issues.push({ severity: 'warning', message: `${boilerPct}% of commits are boilerplate/cosmetic — high automation risk`, fixable: false });
|
|
177
|
+
}
|
|
178
|
+
if (archPct > 40) {
|
|
179
|
+
issues.push({ severity: 'info', message: `${archPct}% architecture/integration work — strong human edge`, fixable: false });
|
|
180
|
+
}
|
|
181
|
+
if (analysis.topCommits.length > 0) {
|
|
182
|
+
const top = analysis.topCommits[0];
|
|
183
|
+
issues.push({ severity: 'info', message: `top commit: ${top.hash} (${top.category}, ${top.score}/100) — ${top.message.slice(0, 50)}`, fixable: false });
|
|
184
|
+
}
|
|
185
|
+
// Map 0-100 score to 0-10
|
|
186
|
+
const vetScore = Math.round(analysis.score / 10);
|
|
187
|
+
return {
|
|
188
|
+
name: 'edge',
|
|
189
|
+
score: vetScore,
|
|
190
|
+
maxScore: 10,
|
|
191
|
+
issues,
|
|
192
|
+
summary: `${total} commits — human edge score ${analysis.score}/100 — ${analysis.recommendation.slice(0, 60)}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// ── Standalone subcommand output ─────────────────────────────────────────────
|
|
196
|
+
export function runEdgeCommand(cwd, explain = false) {
|
|
197
|
+
const analysis = analyzeEdge(cwd, explain ? 100 : 50);
|
|
198
|
+
if (analysis.totalCommits === 0) {
|
|
199
|
+
console.log('\n no commits to analyze\n');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const { distribution: dist, totalCommits: total, score, topCommits, recommendation } = analysis;
|
|
203
|
+
const scoreColor = score >= 70 ? '\x1b[32m' : score >= 40 ? '\x1b[33m' : '\x1b[31m';
|
|
204
|
+
const RESET = '\x1b[0m';
|
|
205
|
+
const BOLD = '\x1b[1m';
|
|
206
|
+
const DIM = '\x1b[2m';
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(` ${BOLD}Human Edge Report${RESET} ${scoreColor}${score}/100${RESET}`);
|
|
209
|
+
console.log(` ${DIM}${total} commits analyzed${RESET}`);
|
|
210
|
+
console.log('');
|
|
211
|
+
const cats = ['architecture', 'debugging', 'integration', 'feature', 'boilerplate', 'cosmetic'];
|
|
212
|
+
const catEmoji = { architecture: '🏗️', debugging: '🔍', integration: '🔗', feature: '⚡', boilerplate: '📋', cosmetic: '🎨' };
|
|
213
|
+
for (const cat of cats) {
|
|
214
|
+
if (dist[cat] === 0)
|
|
215
|
+
continue;
|
|
216
|
+
const pct = Math.round((dist[cat] / total) * 100);
|
|
217
|
+
const bar = '█'.repeat(Math.max(1, Math.round(pct / 5)));
|
|
218
|
+
const catScore = CATEGORY_SCORES[cat];
|
|
219
|
+
const color = catScore >= 70 ? '\x1b[32m' : catScore >= 40 ? '\x1b[33m' : '\x1b[31m';
|
|
220
|
+
console.log(` ${catEmoji[cat]} ${cat.padEnd(14)} ${color}${String(dist[cat]).padStart(3)}${RESET} ${DIM}(${String(pct).padStart(2)}%)${RESET} ${color}${bar}${RESET}`);
|
|
221
|
+
}
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(` ${BOLD}Top commits${RESET}`);
|
|
224
|
+
for (const c of topCommits) {
|
|
225
|
+
console.log(` ${scoreColor}${c.score}${RESET} ${c.hash} ${c.message.slice(0, 55)}`);
|
|
226
|
+
}
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(` → ${recommendation}`);
|
|
229
|
+
console.log('');
|
|
230
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
interface ToolUseBlock {
|
|
3
|
+
type: 'tool_use';
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
input: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
interface SessionEntry {
|
|
9
|
+
type?: string;
|
|
10
|
+
role?: string;
|
|
11
|
+
content?: unknown;
|
|
12
|
+
timestamp?: string;
|
|
13
|
+
message?: {
|
|
14
|
+
role?: string;
|
|
15
|
+
content?: unknown;
|
|
16
|
+
};
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface GroupedActions {
|
|
20
|
+
session_id: string;
|
|
21
|
+
start_time: string | null;
|
|
22
|
+
end_time: string | null;
|
|
23
|
+
duration_seconds: number | null;
|
|
24
|
+
files_modified: string[];
|
|
25
|
+
files_deleted: string[];
|
|
26
|
+
files_created: string[];
|
|
27
|
+
commands_run: string[];
|
|
28
|
+
urls_fetched: string[];
|
|
29
|
+
packages_installed: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface SessionReceipt {
|
|
32
|
+
actions: GroupedActions;
|
|
33
|
+
sha256: string;
|
|
34
|
+
generated_at: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function findSessionFiles(baseDir?: string): string[];
|
|
37
|
+
export declare function findLatestSession(baseDir?: string): string | null;
|
|
38
|
+
export declare function parseSessionFile(filePath: string): Promise<{
|
|
39
|
+
toolUses: ToolUseBlock[];
|
|
40
|
+
entries: SessionEntry[];
|
|
41
|
+
}>;
|
|
42
|
+
export declare function groupActions(toolUses: ToolUseBlock[], entries: SessionEntry[], sessionPath: string): GroupedActions;
|
|
43
|
+
export declare function computeSha256(content: string): string;
|
|
44
|
+
export declare function renderReceiptText(actions: GroupedActions): string;
|
|
45
|
+
export declare function renderReceiptJson(actions: GroupedActions): SessionReceipt;
|
|
46
|
+
export declare function checkReceipt(cwd: string): Promise<CheckResult>;
|
|
47
|
+
export declare function runReceiptCommand(format?: 'ascii' | 'json'): Promise<void>;
|
|
48
|
+
export {};
|