@builtbyecho/agent-brief 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/LICENSE +21 -0
- package/README.md +76 -0
- package/package.json +43 -0
- package/src/cli.js +56 -0
- package/src/index.js +283 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BuiltByEcho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# agent-brief
|
|
2
|
+
|
|
3
|
+
Generate a concise, safety-aware project brief for coding agents.
|
|
4
|
+
|
|
5
|
+
Agents waste a surprising amount of time rediscovering the same basics: What stack is this? Which command runs tests? Is there an AGENTS.md? Are there risky instructions or secret-looking strings in the handoff context?
|
|
6
|
+
|
|
7
|
+
`agent-brief` turns a repo into a short briefing an agent can read before it starts changing code.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @builtbyecho/agent-brief
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
- Finds high-signal files: `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, etc.
|
|
16
|
+
- Infers stack and common commands.
|
|
17
|
+
- Builds a compact repo map.
|
|
18
|
+
- Scans context files for obvious secrets and risky operational instructions.
|
|
19
|
+
- Emits Markdown for humans/agents or JSON for automation.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @builtbyecho/agent-brief
|
|
25
|
+
agent-brief /path/to/repo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or run without installing:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @builtbyecho/agent-brief /path/to/repo
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
agent-brief [path] [options]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
|
|
42
|
+
- `--format markdown|json` / `-f` — output format. Default: `markdown`.
|
|
43
|
+
- `--max-file-bytes N` — max bytes to read per context file. Default: `12000`.
|
|
44
|
+
- `--no-snippets` — omit source snippets.
|
|
45
|
+
- `--fail-on-high-risk` — exit `2` if high-severity risk patterns are found.
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
agent-brief . > AGENT_BRIEF.md
|
|
51
|
+
agent-brief ~/dev/my-app --format json
|
|
52
|
+
agent-brief . --fail-on-high-risk
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why this exists
|
|
56
|
+
|
|
57
|
+
The current agent tooling boom has plenty of orchestration, MCP servers, and observability dashboards. The missing small thing is a cheap, local preflight that gives any agent the same crisp project orientation before it spends tokens or touches files.
|
|
58
|
+
|
|
59
|
+
This is intentionally zero-dependency and boring. It should be safe to run in almost any repo.
|
|
60
|
+
|
|
61
|
+
## Library API
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { generateBrief, formatMarkdown } from '@builtbyecho/agent-brief';
|
|
65
|
+
|
|
66
|
+
const brief = generateBrief(process.cwd());
|
|
67
|
+
console.log(formatMarkdown(brief));
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Notes on risk scanning
|
|
71
|
+
|
|
72
|
+
This is not a full secret scanner. It catches common token/private-key/secret-assignment patterns in the context files most likely to be pasted into agents. Use a dedicated scanner like Gitleaks or TruffleHog for full repository security reviews.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@builtbyecho/agent-brief",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate concise, safety-aware project briefs for coding agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-brief": "./src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"smoke": "node src/cli.js . --format markdown --max-file-bytes 3000"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ai-agent",
|
|
23
|
+
"agents",
|
|
24
|
+
"context",
|
|
25
|
+
"cli",
|
|
26
|
+
"mcp",
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"agentic"
|
|
29
|
+
],
|
|
30
|
+
"author": "BuiltByEcho",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/BuiltByEcho/agent-brief.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/BuiltByEcho/agent-brief/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/BuiltByEcho/agent-brief#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { generateBrief, formatJson, formatMarkdown } from './index.js';
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const args = { path: '.', format: 'markdown', maxFileBytes: 12000, noSnippets: false, failOnHighRisk: false };
|
|
7
|
+
for (let i = 0; i < argv.length; i++) {
|
|
8
|
+
const arg = argv[i];
|
|
9
|
+
if (arg === '--format' || arg === '-f') args.format = argv[++i];
|
|
10
|
+
else if (arg === '--max-file-bytes') args.maxFileBytes = Number(argv[++i]);
|
|
11
|
+
else if (arg === '--no-snippets') args.noSnippets = true;
|
|
12
|
+
else if (arg === '--fail-on-high-risk') args.failOnHighRisk = true;
|
|
13
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
14
|
+
else if (!arg.startsWith('-')) args.path = arg;
|
|
15
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
16
|
+
}
|
|
17
|
+
return args;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function help() {
|
|
21
|
+
return `agent-brief — generate concise, safety-aware project briefs for coding agents
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
agent-brief [path] [options]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-f, --format markdown|json Output format (default: markdown)
|
|
28
|
+
--max-file-bytes N Max bytes to read per context file (default: 12000)
|
|
29
|
+
--no-snippets Omit context snippets from output
|
|
30
|
+
--fail-on-high-risk Exit 2 if high-severity risk patterns are found
|
|
31
|
+
-h, --help Show help
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
npx @builtbyecho/agent-brief
|
|
35
|
+
agent-brief ~/dev/my-app --format json
|
|
36
|
+
agent-brief . --fail-on-high-risk > AGENT_BRIEF.md
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const args = parseArgs(process.argv.slice(2));
|
|
42
|
+
if (args.help) {
|
|
43
|
+
console.log(help());
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
if (!['markdown', 'json'].includes(args.format)) throw new Error('--format must be markdown or json');
|
|
47
|
+
const brief = generateBrief(resolve(args.path), {
|
|
48
|
+
maxFileBytes: args.maxFileBytes,
|
|
49
|
+
includeSnippets: !args.noSnippets
|
|
50
|
+
});
|
|
51
|
+
console.log(args.format === 'json' ? formatJson(brief) : formatMarkdown(brief));
|
|
52
|
+
if (args.failOnHighRisk && brief.risks.some(r => r.severity === 'high')) process.exit(2);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`agent-brief: ${error.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_IGNORES = new Set([
|
|
6
|
+
'.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.turbo', '.cache',
|
|
7
|
+
'vendor', 'target', '__pycache__', '.venv', 'venv', '.idea', '.vscode'
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const CONTEXT_FILES = [
|
|
11
|
+
'AGENTS.md', 'CLAUDE.md', 'GEMINI.md', 'CURSOR.md', '.cursorrules',
|
|
12
|
+
'README.md', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod',
|
|
13
|
+
'Makefile', 'justfile', 'docker-compose.yml', 'compose.yml'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SECRET_PATTERNS = [
|
|
17
|
+
[/\b(?:sk-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,})\b/g, 'possible API token'],
|
|
18
|
+
[/\b[A-Za-z0-9_]*(?:SECRET|TOKEN|API_KEY|PRIVATE_KEY|PASSWORD)[A-Za-z0-9_]*\s*=\s*['\"][^'\"]{8,}['\"]/gi, 'secret-looking assignment'],
|
|
19
|
+
[/-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g, 'private key block']
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const RISKY_TEXT = [
|
|
23
|
+
[/\brm\s+-rf\b/i, 'destructive rm -rf instruction'],
|
|
24
|
+
[/\bsudo\b/i, 'sudo/elevated command mentioned'],
|
|
25
|
+
[/\bcurl\b[^\n|;&]+\|\s*(?:sh|bash)\b/i, 'curl pipe-to-shell pattern'],
|
|
26
|
+
[/\bchmod\s+777\b/i, 'chmod 777 pattern'],
|
|
27
|
+
[/\bdisable\s+(?:safety|guardrails?|checks?)\b/i, 'instruction to disable safety/checks']
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function generateBrief(root = process.cwd(), options = {}) {
|
|
31
|
+
const opts = {
|
|
32
|
+
maxFileBytes: 12_000,
|
|
33
|
+
maxTreeEntries: 120,
|
|
34
|
+
includeSnippets: true,
|
|
35
|
+
...options
|
|
36
|
+
};
|
|
37
|
+
const absRoot = root;
|
|
38
|
+
const project = basename(absRoot);
|
|
39
|
+
const context = collectContext(absRoot, opts);
|
|
40
|
+
const tree = collectTree(absRoot, opts.maxTreeEntries);
|
|
41
|
+
const packageInfo = readPackage(absRoot);
|
|
42
|
+
const git = readGit(absRoot);
|
|
43
|
+
const risks = scanRisks(absRoot, context.files);
|
|
44
|
+
const commands = inferCommands(absRoot, packageInfo);
|
|
45
|
+
const stack = inferStack(absRoot, packageInfo);
|
|
46
|
+
const score = scoreRepo({ context, commands, risks });
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
generatedAt: new Date().toISOString(),
|
|
50
|
+
project,
|
|
51
|
+
root: absRoot,
|
|
52
|
+
score,
|
|
53
|
+
git,
|
|
54
|
+
stack,
|
|
55
|
+
commands,
|
|
56
|
+
contextFiles: context.files,
|
|
57
|
+
tree,
|
|
58
|
+
risks,
|
|
59
|
+
recommendations: recommendations({ context, commands, risks, packageInfo })
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatMarkdown(brief) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
lines.push(`# Agent Brief: ${brief.project}`);
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push(`Generated: ${brief.generatedAt}`);
|
|
68
|
+
lines.push(`Readiness: **${brief.score.grade}** (${brief.score.points}/100)`);
|
|
69
|
+
if (brief.git.branch || brief.git.status) lines.push(`Git: ${brief.git.branch || 'unknown'}${brief.git.status ? ` — ${brief.git.status}` : ''}`);
|
|
70
|
+
lines.push('');
|
|
71
|
+
|
|
72
|
+
lines.push('## Stack');
|
|
73
|
+
lines.push(brief.stack.length ? brief.stack.map(s => `- ${s}`).join('\n') : '- Unknown / mixed');
|
|
74
|
+
lines.push('');
|
|
75
|
+
|
|
76
|
+
lines.push('## High-signal commands');
|
|
77
|
+
if (brief.commands.length) {
|
|
78
|
+
for (const cmd of brief.commands) lines.push(`- ${cmd.name}: \`${cmd.command}\`${cmd.source ? ` (${cmd.source})` : ''}`);
|
|
79
|
+
} else {
|
|
80
|
+
lines.push('- No obvious build/test/lint commands found.');
|
|
81
|
+
}
|
|
82
|
+
lines.push('');
|
|
83
|
+
|
|
84
|
+
lines.push('## Agent context files');
|
|
85
|
+
if (brief.contextFiles.length) {
|
|
86
|
+
for (const file of brief.contextFiles) {
|
|
87
|
+
lines.push(`### ${file.path}`);
|
|
88
|
+
lines.push(`- ${file.bytes} bytes${file.truncated ? ' (truncated)' : ''}`);
|
|
89
|
+
if (file.summary.length) lines.push(...file.summary.map(s => `- ${s}`));
|
|
90
|
+
if (file.snippet) {
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push('```');
|
|
93
|
+
lines.push(file.snippet.trim());
|
|
94
|
+
lines.push('```');
|
|
95
|
+
}
|
|
96
|
+
lines.push('');
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
lines.push('- No AGENTS.md/README/package metadata found. Add an AGENTS.md for better agent handoff.');
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
lines.push('## Repo map');
|
|
104
|
+
lines.push('```text');
|
|
105
|
+
lines.push(brief.tree.join('\n') || '(empty)');
|
|
106
|
+
lines.push('```');
|
|
107
|
+
lines.push('');
|
|
108
|
+
|
|
109
|
+
lines.push('## Risk scan');
|
|
110
|
+
if (brief.risks.length) {
|
|
111
|
+
for (const risk of brief.risks) lines.push(`- [${risk.severity}] ${risk.path}: ${risk.message}`);
|
|
112
|
+
} else {
|
|
113
|
+
lines.push('- No obvious secret/risky-instruction patterns found in scanned context files.');
|
|
114
|
+
}
|
|
115
|
+
lines.push('');
|
|
116
|
+
|
|
117
|
+
lines.push('## Recommendations');
|
|
118
|
+
for (const rec of brief.recommendations) lines.push(`- ${rec}`);
|
|
119
|
+
lines.push('');
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function formatJson(brief) {
|
|
124
|
+
return JSON.stringify(brief, null, 2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectContext(root, opts) {
|
|
128
|
+
const files = [];
|
|
129
|
+
for (const name of CONTEXT_FILES) {
|
|
130
|
+
const path = join(root, name);
|
|
131
|
+
if (!existsSync(path) || !statSync(path).isFile()) continue;
|
|
132
|
+
const raw = readFileSync(path, 'utf8');
|
|
133
|
+
const truncated = Buffer.byteLength(raw) > opts.maxFileBytes;
|
|
134
|
+
const text = truncated ? raw.slice(0, opts.maxFileBytes) : raw;
|
|
135
|
+
files.push({
|
|
136
|
+
path: name,
|
|
137
|
+
bytes: Buffer.byteLength(raw),
|
|
138
|
+
truncated,
|
|
139
|
+
summary: summarizeText(text),
|
|
140
|
+
snippet: opts.includeSnippets ? firstUsefulLines(text, 24) : ''
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return { files };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function collectTree(root, maxEntries) {
|
|
147
|
+
const out = [];
|
|
148
|
+
walk(root, '', 0);
|
|
149
|
+
return out;
|
|
150
|
+
|
|
151
|
+
function walk(base, prefix, depth) {
|
|
152
|
+
if (out.length >= maxEntries || depth > 3) return;
|
|
153
|
+
let entries = [];
|
|
154
|
+
try { entries = readdirSync(base, { withFileTypes: true }); } catch { return; }
|
|
155
|
+
entries = entries
|
|
156
|
+
.filter(e => !DEFAULT_IGNORES.has(e.name) && !e.name.startsWith('.DS_Store'))
|
|
157
|
+
.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (out.length >= maxEntries) break;
|
|
160
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
161
|
+
out.push(`${' '.repeat(depth)}${entry.isDirectory() ? '📁' : '📄'} ${rel}`);
|
|
162
|
+
if (entry.isDirectory()) walk(join(base, entry.name), rel, depth + 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readPackage(root) {
|
|
168
|
+
const path = join(root, 'package.json');
|
|
169
|
+
if (!existsSync(path)) return null;
|
|
170
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readGit(root) {
|
|
174
|
+
const git = { branch: '', status: '' };
|
|
175
|
+
try { git.branch = execFileSync('git', ['-C', root, 'branch', '--show-current'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch {}
|
|
176
|
+
try {
|
|
177
|
+
const status = execFileSync('git', ['-C', root, 'status', '--short'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
178
|
+
git.status = status ? `${status.split('\n').length} changed file(s)` : 'clean';
|
|
179
|
+
} catch {}
|
|
180
|
+
return git;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function inferCommands(root, pkg) {
|
|
184
|
+
const commands = [];
|
|
185
|
+
if (pkg?.scripts) {
|
|
186
|
+
for (const key of ['dev', 'test', 'lint', 'typecheck', 'build', 'start']) {
|
|
187
|
+
if (pkg.scripts[key]) commands.push({ name: key, command: `npm run ${key}`, source: 'package.json' });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (existsSync(join(root, 'Makefile'))) commands.push({ name: 'make', command: 'make', source: 'Makefile' });
|
|
191
|
+
if (existsSync(join(root, 'pyproject.toml'))) commands.push({ name: 'python tests', command: 'pytest', source: 'pyproject.toml' });
|
|
192
|
+
if (existsSync(join(root, 'Cargo.toml'))) commands.push({ name: 'rust tests', command: 'cargo test', source: 'Cargo.toml' });
|
|
193
|
+
if (existsSync(join(root, 'go.mod'))) commands.push({ name: 'go tests', command: 'go test ./...', source: 'go.mod' });
|
|
194
|
+
return dedupe(commands, c => c.command);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function inferStack(root, pkg) {
|
|
198
|
+
const stack = [];
|
|
199
|
+
if (pkg) {
|
|
200
|
+
stack.push('Node.js package');
|
|
201
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
202
|
+
if (deps.next) stack.push('Next.js');
|
|
203
|
+
if (deps.react) stack.push('React');
|
|
204
|
+
if (deps.typescript) stack.push('TypeScript');
|
|
205
|
+
if (deps.vite) stack.push('Vite');
|
|
206
|
+
if (pkg.bin) stack.push('CLI tool');
|
|
207
|
+
}
|
|
208
|
+
if (existsSync(join(root, 'pyproject.toml'))) stack.push('Python');
|
|
209
|
+
if (existsSync(join(root, 'Cargo.toml'))) stack.push('Rust');
|
|
210
|
+
if (existsSync(join(root, 'go.mod'))) stack.push('Go');
|
|
211
|
+
if (existsSync(join(root, 'docker-compose.yml')) || existsSync(join(root, 'compose.yml'))) stack.push('Docker Compose');
|
|
212
|
+
return dedupe(stack, s => s);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function scanRisks(root, files) {
|
|
216
|
+
const risks = [];
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
const path = join(root, file.path);
|
|
219
|
+
let text = '';
|
|
220
|
+
try { text = readFileSync(path, 'utf8'); } catch { continue; }
|
|
221
|
+
for (const [pattern, message] of SECRET_PATTERNS) {
|
|
222
|
+
if (pattern.test(text)) risks.push({ severity: 'high', path: file.path, message });
|
|
223
|
+
pattern.lastIndex = 0;
|
|
224
|
+
}
|
|
225
|
+
for (const [pattern, message] of RISKY_TEXT) {
|
|
226
|
+
if (pattern.test(text)) risks.push({ severity: 'medium', path: file.path, message });
|
|
227
|
+
pattern.lastIndex = 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return risks;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function scoreRepo({ context, commands, risks }) {
|
|
234
|
+
let points = 50;
|
|
235
|
+
if (context.files.some(f => f.path === 'AGENTS.md')) points += 20;
|
|
236
|
+
if (context.files.some(f => f.path === 'README.md')) points += 10;
|
|
237
|
+
if (commands.some(c => c.name.includes('test') || c.name === 'test')) points += 10;
|
|
238
|
+
if (commands.some(c => c.name === 'lint' || c.name === 'typecheck')) points += 5;
|
|
239
|
+
points -= risks.filter(r => r.severity === 'high').length * 20;
|
|
240
|
+
points -= risks.filter(r => r.severity === 'medium').length * 8;
|
|
241
|
+
points = Math.max(0, Math.min(100, points));
|
|
242
|
+
const grade = points >= 85 ? 'excellent' : points >= 70 ? 'good' : points >= 50 ? 'usable' : 'needs work';
|
|
243
|
+
return { points, grade };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function recommendations({ context, commands, risks, packageInfo }) {
|
|
247
|
+
const recs = [];
|
|
248
|
+
if (!context.files.some(f => f.path === 'AGENTS.md')) recs.push('Add AGENTS.md with project rules, safe commands, and test gates for coding agents.');
|
|
249
|
+
if (!commands.some(c => c.name === 'test' || c.name.includes('tests'))) recs.push('Expose one obvious test command so agents can verify changes before finalizing.');
|
|
250
|
+
if (!commands.some(c => c.name === 'lint' || c.name === 'typecheck')) recs.push('Expose lint/typecheck commands for fast feedback.');
|
|
251
|
+
if (risks.some(r => r.severity === 'high')) recs.push('Review high-severity risk matches before sharing this repo with external agents.');
|
|
252
|
+
if (packageInfo && !packageInfo.repository) recs.push('Add package.json repository metadata for easier agent/source navigation.');
|
|
253
|
+
if (!recs.length) recs.push('Looks agent-friendly. Keep context files concise and commands current.');
|
|
254
|
+
return recs;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function summarizeText(text) {
|
|
258
|
+
return text
|
|
259
|
+
.split('\n')
|
|
260
|
+
.map(line => line.trim())
|
|
261
|
+
.filter(line => /^(#|[-*]\s|"(?:name|description|scripts)"\s*:)/.test(line))
|
|
262
|
+
.slice(0, 8)
|
|
263
|
+
.map(line => line.replace(/^#+\s*/, '').slice(0, 160));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function firstUsefulLines(text, maxLines) {
|
|
267
|
+
return text
|
|
268
|
+
.split('\n')
|
|
269
|
+
.filter(line => line.trim().length > 0)
|
|
270
|
+
.slice(0, maxLines)
|
|
271
|
+
.join('\n')
|
|
272
|
+
.slice(0, 2000);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function dedupe(items, keyFn) {
|
|
276
|
+
const seen = new Set();
|
|
277
|
+
return items.filter(item => {
|
|
278
|
+
const key = keyFn(item);
|
|
279
|
+
if (seen.has(key)) return false;
|
|
280
|
+
seen.add(key);
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
}
|