devlabs-talent 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/README.md +24 -0
- package/bin/agent-wrapped.js +7 -0
- package/package.json +22 -0
- package/src/cli.js +32 -0
- package/src/commands/analyze.js +124 -0
- package/src/preview/previewReport.js +165 -0
- package/src/report/generateReport.js +166 -0
- package/src/sources/discover.js +101 -0
- package/src/upload/token.js +15 -0
- package/src/upload/uploadReport.js +23 -0
- package/src/utils/openBrowser.js +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# devlabs-talent
|
|
2
|
+
|
|
3
|
+
Builder-level local analyzer for DevLabs Agent Wrapped.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx devlabs-talent@latest analyze --token <verified-upload-token>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Use `--color` to force the DevLabs terminal theme when recording or screenshotting output:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx devlabs-talent@latest analyze --token <verified-upload-token> --color
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
After a successful upload, the CLI opens the shareable Wrapped URL in your default browser.
|
|
16
|
+
Use `--no-open` to disable that behavior.
|
|
17
|
+
|
|
18
|
+
The upload API defaults to `https://www.devlabs.club`, and the browser opens the public
|
|
19
|
+
Wrapped URL on `https://www.devlabs.club` by default. Use `--api` only for non-production
|
|
20
|
+
upload testing, and `--public-url` only if the public web host changes.
|
|
21
|
+
|
|
22
|
+
The CLI scans safe local AI-agent usage sources across Claude Code, Codex, Cursor, and optional exported summaries. It shows a preview before upload and never uploads raw prompts, raw conversations, source code, secrets, environment variables, full local paths, or private filenames.
|
|
23
|
+
|
|
24
|
+
Set `DEVLABS_API_URL` to test against a non-production DevLabs app.
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devlabs-talent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate safe DevLabs Agent Wrapped reports from builder-level AI agent usage.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-wrapped": "bin/agent-wrapped.js",
|
|
8
|
+
"devlabs-agent-wrapped": "bin/agent-wrapped.js",
|
|
9
|
+
"devlabs-talent": "bin/agent-wrapped.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"analyze": "node ./bin/agent-wrapped.js analyze"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { analyzeCommand } from './commands/analyze.js';
|
|
2
|
+
|
|
3
|
+
function usage() {
|
|
4
|
+
console.log(`DevLabs Agent Wrapped
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
agent-wrapped analyze --token <verified-upload-token> [--api <url>] [--public-url <url>] [--import <path>] [--yes] [--color] [--no-open]
|
|
8
|
+
agent-wrapped doctor
|
|
9
|
+
agent-wrapped login
|
|
10
|
+
agent-wrapped preview
|
|
11
|
+
agent-wrapped upload
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function main(argv) {
|
|
16
|
+
const [command, ...rest] = argv;
|
|
17
|
+
if (!command || command === '--help' || command === '-h') {
|
|
18
|
+
usage();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (command === 'analyze') return analyzeCommand(rest);
|
|
22
|
+
if (command === 'doctor') {
|
|
23
|
+
console.log('Doctor checks are scaffolded. Run `agent-wrapped analyze --token <token>` to scan available agent sources.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (command === 'login' || command === 'preview' || command === 'upload') {
|
|
27
|
+
console.log(`\`${command}\` is scaffolded for a future split-command flow. V1 uses \`analyze\` end to end.`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
usage();
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import { discoverAgentSources, readSourceSamples } from '../sources/discover.js';
|
|
5
|
+
import { generateReport } from '../report/generateReport.js';
|
|
6
|
+
import { previewReport } from '../preview/previewReport.js';
|
|
7
|
+
import { uploadReport } from '../upload/uploadReport.js';
|
|
8
|
+
import { decodeUploadToken } from '../upload/token.js';
|
|
9
|
+
import { openBrowser } from '../utils/openBrowser.js';
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = {
|
|
13
|
+
imports: [],
|
|
14
|
+
yes: false,
|
|
15
|
+
color: undefined,
|
|
16
|
+
open: true,
|
|
17
|
+
api: process.env.DEVLABS_API_URL || 'https://www.devlabs.club',
|
|
18
|
+
publicUrl: process.env.DEVLABS_PUBLIC_URL || 'https://www.devlabs.club',
|
|
19
|
+
};
|
|
20
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
21
|
+
const item = argv[index];
|
|
22
|
+
if (item === '--token') args.token = argv[++index];
|
|
23
|
+
else if (item === '--api') args.api = argv[++index];
|
|
24
|
+
else if (item === '--public-url') args.publicUrl = argv[++index];
|
|
25
|
+
else if (item === '--import') args.imports.push(argv[++index]);
|
|
26
|
+
else if (item === '--yes' || item === '-y') args.yes = true;
|
|
27
|
+
else if (item === '--color') args.color = true;
|
|
28
|
+
else if (item === '--no-color') args.color = false;
|
|
29
|
+
else if (item === '--no-open') args.open = false;
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isYes(answer) {
|
|
35
|
+
return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createPrompter(skipPrompts) {
|
|
39
|
+
if (skipPrompts) return { askYes: async () => true, close: () => {} };
|
|
40
|
+
if (!input.isTTY) {
|
|
41
|
+
const answers = fs.readFileSync(0, 'utf8').split(/\r?\n/);
|
|
42
|
+
return {
|
|
43
|
+
askYes: async (question) => {
|
|
44
|
+
output.write(question);
|
|
45
|
+
const answer = answers.shift() || '';
|
|
46
|
+
output.write('\n');
|
|
47
|
+
return isYes(answer);
|
|
48
|
+
},
|
|
49
|
+
close: () => {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const rl = createInterface({ input, output });
|
|
53
|
+
return {
|
|
54
|
+
askYes: async (question) => isYes(await rl.question(question)),
|
|
55
|
+
close: () => rl.close(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function analyzeCommand(argv) {
|
|
60
|
+
const args = parseArgs(argv);
|
|
61
|
+
if (!args.token) throw new Error('Missing --token. Copy the command from the DevLabs phone verification screen.');
|
|
62
|
+
const tokenPayload = decodeUploadToken(args.token);
|
|
63
|
+
if (!tokenPayload?.builderId) throw new Error('The upload token is malformed.');
|
|
64
|
+
const prompter = createPrompter(args.yes);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
console.log('DevLabs Agent Wrapped');
|
|
68
|
+
console.log('Scanning builder-level AI agent usage sources...');
|
|
69
|
+
|
|
70
|
+
const sources = await discoverAgentSources({ imports: args.imports });
|
|
71
|
+
if (!sources.length) {
|
|
72
|
+
console.log('No local agent sources found. Add exported summaries with `--import <path>` and run again.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`Detected ${sources.length} source${sources.length === 1 ? '' : 's'}:`);
|
|
77
|
+
for (const source of sources) {
|
|
78
|
+
console.log(`- ${source.agent}: ${source.kind} (${source.safeCountLabel})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!args.yes) {
|
|
82
|
+
const approvedSources = await prompter.askYes('Analyze these sources locally? [y/N] ');
|
|
83
|
+
if (!approvedSources) {
|
|
84
|
+
console.log('Analysis cancelled. Nothing was read or uploaded.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const samples = await readSourceSamples(sources);
|
|
90
|
+
const report = generateReport({
|
|
91
|
+
builderId: tokenPayload.builderId,
|
|
92
|
+
builderName: tokenPayload.email?.split('@')[0],
|
|
93
|
+
samples,
|
|
94
|
+
publicRoot: args.publicUrl,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
previewReport(report, sources, { color: args.color });
|
|
98
|
+
|
|
99
|
+
if (!args.yes) {
|
|
100
|
+
const approved = await prompter.askYes('Upload this wrapped report to DevLabs? [y/N] ');
|
|
101
|
+
if (!approved) {
|
|
102
|
+
console.log('Upload cancelled. Nothing was sent to DevLabs.');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await uploadReport({
|
|
108
|
+
apiRoot: args.api,
|
|
109
|
+
token: args.token,
|
|
110
|
+
builderId: tokenPayload.builderId,
|
|
111
|
+
report,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log('Uploaded Agent Wrapped report.');
|
|
115
|
+
const publicUrl = new URL(result.publicUrl, args.publicUrl).toString();
|
|
116
|
+
console.log(`Public URL: ${publicUrl}`);
|
|
117
|
+
if (args.open) {
|
|
118
|
+
const opened = await openBrowser(publicUrl);
|
|
119
|
+
console.log(opened ? 'Opened shareable Wrapped card in your browser.' : 'Could not open browser automatically. Open the public URL above.');
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
prompter.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
function padRight(value, width) {
|
|
2
|
+
return String(value).padEnd(width, ' ');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function shouldUseColor(option) {
|
|
6
|
+
if (option === true) return true;
|
|
7
|
+
if (option === false) return false;
|
|
8
|
+
if (process.env.NO_COLOR) return false;
|
|
9
|
+
if (process.env.FORCE_COLOR) return true;
|
|
10
|
+
return Boolean(process.stdout.isTTY);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeTheme(colorOption) {
|
|
14
|
+
const enabled = shouldUseColor(colorOption);
|
|
15
|
+
const wrap = (open, close = '\x1b[0m') => (value) => (enabled ? `${open}${value}${close}` : value);
|
|
16
|
+
return {
|
|
17
|
+
enabled,
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
orange: wrap('\x1b[38;2;250;125;34m'),
|
|
20
|
+
blue: wrap('\x1b[38;2;22;141;247m'),
|
|
21
|
+
cream: wrap('\x1b[38;2;251;246;243m'),
|
|
22
|
+
muted: wrap('\x1b[38;2;160;154;148m'),
|
|
23
|
+
green: wrap('\x1b[38;2;80;220;150m'),
|
|
24
|
+
yellow: wrap('\x1b[38;2;255;190;90m'),
|
|
25
|
+
bold: wrap('\x1b[1m', enabled ? '\x1b[22m' : ''),
|
|
26
|
+
dim: wrap('\x1b[2m', enabled ? '\x1b[22m' : ''),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function titleCase(value) {
|
|
31
|
+
return String(value || '')
|
|
32
|
+
.replace(/[-_]/g, ' ')
|
|
33
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function confidenceLabel(value) {
|
|
37
|
+
return titleCase(value || 'moderate');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bar(value, width = 16, theme = makeTheme(false)) {
|
|
41
|
+
const safeValue = Math.max(0, Math.min(100, Number(value) || 0));
|
|
42
|
+
const filled = Math.round((safeValue / 100) * width);
|
|
43
|
+
return `${theme.orange('█'.repeat(filled))}${theme.dim(theme.blue('░'.repeat(width - filled)))}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function wrappedLines(text, width = 68) {
|
|
47
|
+
const words = String(text || '').split(/\s+/).filter(Boolean);
|
|
48
|
+
const lines = [];
|
|
49
|
+
let current = '';
|
|
50
|
+
for (const word of words) {
|
|
51
|
+
if (`${current} ${word}`.trim().length > width) {
|
|
52
|
+
if (current) lines.push(current);
|
|
53
|
+
current = word;
|
|
54
|
+
} else {
|
|
55
|
+
current = `${current} ${word}`.trim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (current) lines.push(current);
|
|
59
|
+
return lines.length ? lines : ['No summary available yet.'];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function printMetricRows(rows, theme) {
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
console.log(
|
|
65
|
+
` ${theme.cream(padRight(row.label, 14))} ${bar(row.value, 16, theme)} ${theme.orange(
|
|
66
|
+
`${String(Math.round(row.value)).padStart(3)}%`
|
|
67
|
+
)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function riskLine(report, theme) {
|
|
73
|
+
const risk = report.agentMaturity?.blindAcceptanceRisk || 'moderate';
|
|
74
|
+
if (risk === 'low') return theme.green('✓ Low blind-acceptance risk');
|
|
75
|
+
if (risk === 'moderate') return theme.yellow('⚠ Blind-acceptance risk is moderate');
|
|
76
|
+
return theme.yellow('⚠ Blind-acceptance risk is high');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function section(title, theme) {
|
|
80
|
+
console.log(theme.orange(theme.bold(title)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function previewReport(report, sources, options = {}) {
|
|
84
|
+
const theme = makeTheme(options.color);
|
|
85
|
+
const roles = report.founderRead?.bestFitRoles?.length
|
|
86
|
+
? report.founderRead.bestFitRoles
|
|
87
|
+
: ['Early-stage AI builder'];
|
|
88
|
+
const languages = report.languages?.length
|
|
89
|
+
? report.languages.slice(0, 5)
|
|
90
|
+
: [{ name: 'Unknown', percent: 100 }];
|
|
91
|
+
const frameworks = report.frameworks?.length
|
|
92
|
+
? report.frameworks.map((framework) => framework.name).slice(0, 8)
|
|
93
|
+
: ['No framework signal yet'];
|
|
94
|
+
const buildSurface = report.buildSurface || {};
|
|
95
|
+
const signals = [
|
|
96
|
+
...(report.founderRead?.strengths || []).slice(0, 3).map((item) => `${theme.green('✓')} ${item}`),
|
|
97
|
+
riskLine(report, theme),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(theme.blue('┌──────────────────────────────────────────────────────────────┐'));
|
|
102
|
+
console.log(theme.blue('│') + theme.cream(theme.bold(' DEVLABS AGENT WRAPPED ')) + theme.blue('│'));
|
|
103
|
+
console.log(theme.blue('│') + theme.orange(' verified proof-of-work from agent usage ') + theme.blue('│'));
|
|
104
|
+
console.log(theme.blue('└──────────────────────────────────────────────────────────────┘'));
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(` ${theme.cream(theme.bold(String(report.archetype || 'AI-Native Builder').toUpperCase()))}`);
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(` ${theme.muted('Founder Fit:')} ${theme.orange(theme.bold(String(report.score).padStart(3)))} ${theme.cream('/ 100')}`);
|
|
109
|
+
console.log(` ${theme.muted('Confidence:')} ${theme.cream(confidenceLabel(report.confidence))}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
section('Best Fit', theme);
|
|
113
|
+
for (const role of roles.slice(0, 4)) console.log(` ${theme.cream(role)}`);
|
|
114
|
+
console.log('');
|
|
115
|
+
|
|
116
|
+
section('Languages', theme);
|
|
117
|
+
printMetricRows(languages.map((language) => ({ label: language.name, value: language.percent })), theme);
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
section('Frameworks', theme);
|
|
121
|
+
console.log(` ${frameworks.map((item) => theme.cream(item)).join(theme.muted(' · '))}`);
|
|
122
|
+
console.log('');
|
|
123
|
+
|
|
124
|
+
section('Build Surface', theme);
|
|
125
|
+
printMetricRows([
|
|
126
|
+
{ label: 'Frontend', value: buildSurface.frontend || 0 },
|
|
127
|
+
{ label: 'Backend', value: buildSurface.backend || 0 },
|
|
128
|
+
{ label: 'Database', value: buildSurface.database || 0 },
|
|
129
|
+
{ label: 'Infra', value: buildSurface.infra || 0 },
|
|
130
|
+
{ label: 'Tests', value: buildSurface.tests || 0 },
|
|
131
|
+
], theme);
|
|
132
|
+
console.log('');
|
|
133
|
+
|
|
134
|
+
section('Validation', theme);
|
|
135
|
+
console.log(` ${theme.cream(padRight('Build/test loops', 24))} ${theme.orange(report.validation?.buildTestLoops ?? 0)}`);
|
|
136
|
+
console.log(` ${theme.cream(padRight('Error recovery loops', 24))} ${theme.orange(report.validation?.errorRecoveryLoops ?? 0)}`);
|
|
137
|
+
console.log(` ${theme.cream(padRight('Successful reruns', 24))} ${theme.orange(report.validation?.successfulReruns ?? 0)}`);
|
|
138
|
+
console.log(` ${theme.cream(padRight('Test discipline', 24))} ${theme.orange(((report.validation?.testDisciplineScore || 0) / 10).toFixed(1))} ${theme.cream('/ 10')}`);
|
|
139
|
+
console.log('');
|
|
140
|
+
|
|
141
|
+
section('Signal', theme);
|
|
142
|
+
for (const signal of signals) {
|
|
143
|
+
for (const line of wrappedLines(signal, 64)) console.log(` ${theme.cream(line)}`);
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
|
|
147
|
+
section('Founder Read', theme);
|
|
148
|
+
for (const line of wrappedLines(report.founderRead?.summary, 64)) console.log(` ${theme.cream(line)}`);
|
|
149
|
+
console.log('');
|
|
150
|
+
|
|
151
|
+
section('Source Coverage', theme);
|
|
152
|
+
console.log(` ${theme.muted('Agents:')} ${theme.cream(report.sourceCoverage?.agents?.join(', ') || 'Limited')}`);
|
|
153
|
+
console.log(` ${theme.muted('Sources:')} ${theme.cream(`${sources.length} source group${sources.length === 1 ? '' : 's'}`)}`);
|
|
154
|
+
console.log('');
|
|
155
|
+
|
|
156
|
+
section('Will Upload', theme);
|
|
157
|
+
console.log(` ${theme.cream('Aggregated language, framework, build-surface, validation,')}`);
|
|
158
|
+
console.log(` ${theme.cream('agent-maturity, evidence, and founder-read signals.')}`);
|
|
159
|
+
console.log('');
|
|
160
|
+
|
|
161
|
+
section('Will NOT Upload', theme);
|
|
162
|
+
console.log(` ${theme.cream('Raw prompts, raw conversations, source code, secrets,')}`);
|
|
163
|
+
console.log(` ${theme.cream('environment variables, full local paths, or private filenames.')}`);
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const LANGUAGE_PATTERNS = {
|
|
2
|
+
TypeScript: /\b(typescript|tsx|ts|react|next\.?js|astro)\b/gi,
|
|
3
|
+
JavaScript: /\b(javascript|node|express|vite|npm|pnpm)\b/gi,
|
|
4
|
+
Python: /\b(python|fastapi|django|flask|pytest|uv|pip)\b/gi,
|
|
5
|
+
SQL: /\b(sql|postgres|mysql|sqlite|prisma|supabase)\b/gi,
|
|
6
|
+
Shell: /\b(shell|bash|zsh|terminal|docker|deploy)\b/gi,
|
|
7
|
+
Go: /\b(go|golang|go\.mod)\b/gi,
|
|
8
|
+
Rust: /\b(rust|cargo|tokio)\b/gi,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const FRAMEWORK_PATTERNS = {
|
|
12
|
+
React: /\breact\b/gi,
|
|
13
|
+
'Next.js': /\bnext\.?js\b/gi,
|
|
14
|
+
Astro: /\bastro\b/gi,
|
|
15
|
+
Tailwind: /\btailwind\b/gi,
|
|
16
|
+
Prisma: /\bprisma\b/gi,
|
|
17
|
+
Postgres: /\bpostgres|postgresql\b/gi,
|
|
18
|
+
MongoDB: /\bmongodb|mongoose\b/gi,
|
|
19
|
+
FastAPI: /\bfastapi\b/gi,
|
|
20
|
+
Docker: /\bdocker\b/gi,
|
|
21
|
+
Vercel: /\bvercel\b/gi,
|
|
22
|
+
Cloudflare: /\bcloudflare\b/gi,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function countMatches(text, pattern) {
|
|
26
|
+
return [...text.matchAll(pattern)].length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clamp(value) {
|
|
30
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scoreFromTerms(text, terms) {
|
|
34
|
+
return terms.reduce((sum, term) => sum + countMatches(text, term), 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeReportId(builderId) {
|
|
38
|
+
return `agent-usage-${builderId}-${Date.now()}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateReport({ builderId, builderName, samples, publicRoot }) {
|
|
42
|
+
const allText = samples.map((sample) => sample.text).join('\n').toLowerCase();
|
|
43
|
+
const agents = [...new Set(samples.map((sample) => sample.agent))];
|
|
44
|
+
const sessionCount = samples.length;
|
|
45
|
+
|
|
46
|
+
const languageHits = Object.entries(LANGUAGE_PATTERNS)
|
|
47
|
+
.map(([name, pattern]) => ({ name, hits: countMatches(allText, pattern) }))
|
|
48
|
+
.filter((item) => item.hits > 0)
|
|
49
|
+
.sort((a, b) => b.hits - a.hits)
|
|
50
|
+
.slice(0, 6);
|
|
51
|
+
const languageTotal = languageHits.reduce((sum, item) => sum + item.hits, 0) || 1;
|
|
52
|
+
|
|
53
|
+
const frameworks = Object.entries(FRAMEWORK_PATTERNS)
|
|
54
|
+
.map(([name, pattern]) => ({ name, hits: countMatches(allText, pattern) }))
|
|
55
|
+
.filter((item) => item.hits > 0)
|
|
56
|
+
.sort((a, b) => b.hits - a.hits)
|
|
57
|
+
.slice(0, 10)
|
|
58
|
+
.map((item) => ({
|
|
59
|
+
name: item.name,
|
|
60
|
+
confidence: item.hits >= 5 ? 'high' : item.hits >= 2 ? 'moderate' : 'low',
|
|
61
|
+
evidence: ['agent usage aggregate'],
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const planning = scoreFromTerms(allText, [/\b(plan|approach|architecture|spec|todo|step)\b/gi]);
|
|
65
|
+
const context = scoreFromTerms(allText, [/\b(context|agents\.md|claude\.md|rules|instructions)\b/gi]);
|
|
66
|
+
const iteration = scoreFromTerms(allText, [/\b(iterate|retry|fix|patch|refactor|debug)\b/gi]);
|
|
67
|
+
const verification = scoreFromTerms(allText, [/\b(test|build|lint|typecheck|verify|rerun|ci)\b/gi]);
|
|
68
|
+
const errors = scoreFromTerms(allText, [/\b(error|failed|exception|stack trace|regression)\b/gi]);
|
|
69
|
+
|
|
70
|
+
const frontend = scoreFromTerms(allText, [/\b(ui|react|component|frontend|css|tailwind|page|route)\b/gi]);
|
|
71
|
+
const backend = scoreFromTerms(allText, [/\b(api|server|auth|backend|database|endpoint|worker)\b/gi]);
|
|
72
|
+
const database = scoreFromTerms(allText, [/\b(database|postgres|mongo|sql|schema|migration|prisma)\b/gi]);
|
|
73
|
+
const infra = scoreFromTerms(allText, [/\b(deploy|vercel|cloudflare|docker|env|ci|github action)\b/gi]);
|
|
74
|
+
const tests = scoreFromTerms(allText, [/\b(test|spec|vitest|jest|playwright|pytest|lint|typecheck)\b/gi]);
|
|
75
|
+
const docs = scoreFromTerms(allText, [/\b(readme|docs|comment|instructions|agents\.md|claude\.md)\b/gi]);
|
|
76
|
+
|
|
77
|
+
const planningScore = clamp(35 + planning * 2.4);
|
|
78
|
+
const contextScore = clamp(35 + context * 3.8);
|
|
79
|
+
const iterationScore = clamp(35 + iteration * 2.1);
|
|
80
|
+
const verificationScore = clamp(30 + verification * 2.7);
|
|
81
|
+
const testDisciplineScore = clamp(25 + verification * 2.4 + tests * 1.2);
|
|
82
|
+
const score = clamp((planningScore + contextScore + iterationScore + verificationScore + testDisciplineScore) / 5);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
builderId,
|
|
86
|
+
reportId: makeReportId(builderId),
|
|
87
|
+
builderName,
|
|
88
|
+
archetype:
|
|
89
|
+
frontend && backend
|
|
90
|
+
? 'AI-Native Full-Stack Shipper'
|
|
91
|
+
: verificationScore >= 70
|
|
92
|
+
? 'AI-Native Reliability Builder'
|
|
93
|
+
: 'AI-Native Product Builder',
|
|
94
|
+
score,
|
|
95
|
+
percentile: score >= 90 ? 8 : score >= 80 ? 14 : score >= 70 ? 24 : 38,
|
|
96
|
+
confidence: sessionCount >= 12 ? 'high' : sessionCount >= 5 ? 'moderate' : 'low',
|
|
97
|
+
source: 'uploaded_agent_usage',
|
|
98
|
+
sourceSummary: {
|
|
99
|
+
claudeSessions: samples.filter((sample) => sample.agent === 'Claude Code').length,
|
|
100
|
+
codexSessions: samples.filter((sample) => sample.agent === 'Codex').length,
|
|
101
|
+
cursorSessions: samples.filter((sample) => sample.agent === 'Cursor').length,
|
|
102
|
+
manualImports: samples.filter((sample) => sample.agent === 'Manual import').length,
|
|
103
|
+
daysCovered: undefined,
|
|
104
|
+
},
|
|
105
|
+
sourceCoverage: {
|
|
106
|
+
agents,
|
|
107
|
+
sessionCount,
|
|
108
|
+
timeframeLabel: 'Local available agent usage',
|
|
109
|
+
confidenceNotes: [
|
|
110
|
+
'Report is generated from local agent usage summaries/configs and approved by the builder before upload.',
|
|
111
|
+
'Raw prompts, conversations, source code, full paths, private filenames, and secrets are not uploaded.',
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
languages: languageHits.length
|
|
115
|
+
? languageHits.map((item) => ({
|
|
116
|
+
name: item.name,
|
|
117
|
+
percent: Math.max(1, Math.round((item.hits / languageTotal) * 100)),
|
|
118
|
+
sessions: item.hits,
|
|
119
|
+
evidence: 'session_summary',
|
|
120
|
+
}))
|
|
121
|
+
: [{ name: 'TypeScript', percent: 100, sessions: 0, evidence: 'session_summary' }],
|
|
122
|
+
frameworks,
|
|
123
|
+
buildSurface: {
|
|
124
|
+
frontend: clamp(frontend * 4),
|
|
125
|
+
backend: clamp(backend * 4),
|
|
126
|
+
database: clamp(database * 5),
|
|
127
|
+
infra: clamp(infra * 5),
|
|
128
|
+
tests: clamp(tests * 4),
|
|
129
|
+
docs: clamp(docs * 5),
|
|
130
|
+
},
|
|
131
|
+
validation: {
|
|
132
|
+
buildTestLoops: Math.max(0, Math.round(verification / 3)),
|
|
133
|
+
errorRecoveryLoops: Math.max(0, Math.round(errors / 4)),
|
|
134
|
+
successfulReruns: Math.max(0, Math.round(verification / 5)),
|
|
135
|
+
testDisciplineScore,
|
|
136
|
+
},
|
|
137
|
+
agentMaturity: {
|
|
138
|
+
planningScore,
|
|
139
|
+
contextScore,
|
|
140
|
+
iterationScore,
|
|
141
|
+
verificationScore,
|
|
142
|
+
blindAcceptanceRisk: verificationScore >= 70 ? 'low' : verificationScore >= 45 ? 'moderate' : 'high',
|
|
143
|
+
},
|
|
144
|
+
founderRead: {
|
|
145
|
+
bestFitRoles: ['AI product engineer', 'Full-stack builder', 'Prototype engineer'],
|
|
146
|
+
summary:
|
|
147
|
+
'Uses AI agents across builder workflows with measurable planning, iteration, and verification signals. Review stack fit and verification score for role matching.',
|
|
148
|
+
strengths: [
|
|
149
|
+
agents.length ? `Agent usage observed across ${agents.join(', ')}.` : 'Agent usage sources detected.',
|
|
150
|
+
verificationScore >= 60 ? 'Shows repeated validation and rerun behavior.' : 'Has a baseline AI-agent workflow ready for stronger validation.',
|
|
151
|
+
contextScore >= 60 ? 'Uses context/instruction files to steer agents.' : 'Can improve reusable context hygiene.',
|
|
152
|
+
],
|
|
153
|
+
weaknesses: verificationScore < 50 ? ['Validation/test discipline needs stronger evidence.'] : ['No major weakness detected from aggregate usage.'],
|
|
154
|
+
riskFlags: verificationScore < 45 ? ['Blind acceptance risk is elevated without stronger rerun/test signals.'] : [],
|
|
155
|
+
},
|
|
156
|
+
evidenceHighlights: [
|
|
157
|
+
`${sessionCount} safe local agent source file${sessionCount === 1 ? '' : 's'} analyzed.`,
|
|
158
|
+
agents.length ? `Sources included ${agents.join(', ')}.` : 'Agent source coverage was limited.',
|
|
159
|
+
'Only aggregated proof-of-work metrics are uploaded after local preview approval.',
|
|
160
|
+
],
|
|
161
|
+
share: {
|
|
162
|
+
publicUrl: `${publicRoot.replace(/\/$/, '')}/builder/wrapped/${builderId}`,
|
|
163
|
+
},
|
|
164
|
+
createdAt: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const MAX_SAMPLE_BYTES = 160_000;
|
|
6
|
+
const MAX_FILES_PER_SOURCE = 24;
|
|
7
|
+
|
|
8
|
+
async function exists(target) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(target);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function listFiles(dir, max = MAX_FILES_PER_SOURCE) {
|
|
18
|
+
const results = [];
|
|
19
|
+
async function walk(current) {
|
|
20
|
+
if (results.length >= max) return;
|
|
21
|
+
let entries = [];
|
|
22
|
+
try {
|
|
23
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (results.length >= max) return;
|
|
29
|
+
const full = path.join(current, entry.name);
|
|
30
|
+
if (entry.isDirectory()) await walk(full);
|
|
31
|
+
else if (/\.(json|jsonl|md|txt|toml)$/i.test(entry.name)) results.push(full);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
await walk(dir);
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function addSource(sources, agent, kind, target) {
|
|
39
|
+
if (!(await exists(target))) return;
|
|
40
|
+
const stat = await fs.stat(target);
|
|
41
|
+
const files = stat.isDirectory() ? await listFiles(target) : [target];
|
|
42
|
+
if (!files.length) return;
|
|
43
|
+
sources.push({
|
|
44
|
+
agent,
|
|
45
|
+
kind,
|
|
46
|
+
target,
|
|
47
|
+
files,
|
|
48
|
+
safeCountLabel: stat.isDirectory() ? `${files.length}+ readable summary/config files` : '1 readable file',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function discoverAgentSources({ imports = [] } = {}) {
|
|
53
|
+
const home = os.homedir();
|
|
54
|
+
const sources = [];
|
|
55
|
+
|
|
56
|
+
await addSource(sources, 'Claude Code', 'local settings', path.join(home, '.claude', 'settings.json'));
|
|
57
|
+
await addSource(sources, 'Claude Code', 'session/export summaries', path.join(home, '.claude', 'projects'));
|
|
58
|
+
await addSource(sources, 'Codex', 'local config', path.join(home, '.codex', 'config.toml'));
|
|
59
|
+
await addSource(sources, 'Codex', 'session/export summaries', path.join(home, '.codex', 'sessions'));
|
|
60
|
+
await addSource(sources, 'Codex', 'agent instructions', path.join(home, '.codex', 'AGENTS.md'));
|
|
61
|
+
await addSource(sources, 'Cursor', 'rules and summaries', path.join(home, '.cursor'));
|
|
62
|
+
|
|
63
|
+
for (const manual of imports.filter(Boolean)) {
|
|
64
|
+
await addSource(sources, 'Manual import', 'exported session summary', path.resolve(manual));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return sources;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function redactSecrets(text) {
|
|
71
|
+
return text
|
|
72
|
+
.replace(/sk-[A-Za-z0-9_-]{12,}/g, '[REDACTED_SECRET]')
|
|
73
|
+
.replace(/(api[_-]?key|secret|token|password)\s*[:=]\s*["']?[^"'\s]+/gi, '$1=[REDACTED]')
|
|
74
|
+
.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED_PRIVATE_KEY]')
|
|
75
|
+
.replace(/\/Users\/[^/\s]+\/[^\s]+/g, '[LOCAL_PATH]')
|
|
76
|
+
.replace(/[A-Za-z]:\\Users\\[^\\\s]+\\[^\s]+/g, '[LOCAL_PATH]');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function readSourceSamples(sources) {
|
|
80
|
+
const samples = [];
|
|
81
|
+
for (const source of sources) {
|
|
82
|
+
for (const file of source.files.slice(0, MAX_FILES_PER_SOURCE)) {
|
|
83
|
+
let text = '';
|
|
84
|
+
try {
|
|
85
|
+
const handle = await fs.open(file, 'r');
|
|
86
|
+
const buffer = Buffer.alloc(MAX_SAMPLE_BYTES);
|
|
87
|
+
const { bytesRead } = await handle.read(buffer, 0, MAX_SAMPLE_BYTES, 0);
|
|
88
|
+
await handle.close();
|
|
89
|
+
text = buffer.subarray(0, bytesRead).toString('utf8');
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
samples.push({
|
|
94
|
+
agent: source.agent,
|
|
95
|
+
kind: source.kind,
|
|
96
|
+
text: redactSecrets(text),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return samples;
|
|
101
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function decodeBase64Url(value) {
|
|
2
|
+
const padded = value.padEnd(value.length + ((4 - (value.length % 4)) % 4), '=');
|
|
3
|
+
return Buffer.from(padded, 'base64url').toString('utf8');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function decodeUploadToken(token) {
|
|
7
|
+
try {
|
|
8
|
+
const [, payload] = String(token).split('.');
|
|
9
|
+
if (!payload) return null;
|
|
10
|
+
const decoded = JSON.parse(decodeBase64Url(payload));
|
|
11
|
+
return decoded.kind === 'agent_wrapped_upload' ? decoded : null;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function uploadReport({ apiRoot, token, builderId, report }) {
|
|
2
|
+
const res = await fetch(new URL('/api/builder/wrapped/upload', apiRoot), {
|
|
3
|
+
method: 'POST',
|
|
4
|
+
headers: {
|
|
5
|
+
'Content-Type': 'application/json',
|
|
6
|
+
Authorization: `Bearer ${token}`,
|
|
7
|
+
},
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
builderId,
|
|
10
|
+
report,
|
|
11
|
+
localAnalysisVersion: '0.1.0',
|
|
12
|
+
consent: {
|
|
13
|
+
approvedAt: new Date().toISOString(),
|
|
14
|
+
rawContentUploaded: false,
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
const data = await res.json().catch(() => ({}));
|
|
19
|
+
if (!res.ok || !data.ok) {
|
|
20
|
+
throw new Error(`Upload failed: ${data.error || res.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function openBrowser(url) {
|
|
4
|
+
const platform = process.platform;
|
|
5
|
+
const command =
|
|
6
|
+
platform === 'darwin'
|
|
7
|
+
? 'open'
|
|
8
|
+
: platform === 'win32'
|
|
9
|
+
? 'cmd'
|
|
10
|
+
: 'xdg-open';
|
|
11
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
try {
|
|
15
|
+
const child = spawn(command, args, {
|
|
16
|
+
detached: true,
|
|
17
|
+
stdio: 'ignore',
|
|
18
|
+
});
|
|
19
|
+
child.on('error', () => resolve(false));
|
|
20
|
+
child.unref();
|
|
21
|
+
resolve(true);
|
|
22
|
+
} catch {
|
|
23
|
+
resolve(false);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|