@kernel.chat/kbot 3.3.1 → 3.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 +9 -10
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +30 -3
- package/dist/agent.js.map +1 -1
- package/dist/auth.d.ts +8 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +37 -0
- package/dist/auth.js.map +1 -1
- package/dist/bootstrap.d.ts +24 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +646 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli.js +38 -0
- package/dist/cli.js.map +1 -1
- package/dist/planner.d.ts +2 -0
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +55 -2
- package/dist/planner.js.map +1 -1
- package/dist/tool-pipeline.d.ts +22 -1
- package/dist/tool-pipeline.d.ts.map +1 -1
- package/dist/tool-pipeline.js +62 -1
- package/dist/tool-pipeline.js.map +1 -1
- package/dist/tools/forge.d.ts +8 -0
- package/dist/tools/forge.d.ts.map +1 -0
- package/dist/tools/forge.js +207 -0
- package/dist/tools/forge.js.map +1 -0
- package/dist/tools/forge.test.d.ts +2 -0
- package/dist/tools/forge.test.d.ts.map +1 -0
- package/dist/tools/forge.test.js +250 -0
- package/dist/tools/forge.test.js.map +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -2
- package/dist/tools/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
// kbot Bootstrap — Outer-loop optimizer for any project
|
|
2
|
+
//
|
|
3
|
+
// Most tools help you build. Bootstrap helps you be seen.
|
|
4
|
+
// It measures the gap between what your project IS and what the world PERCEIVES,
|
|
5
|
+
// then tells you exactly what to fix — highest impact first.
|
|
6
|
+
//
|
|
7
|
+
// The bootstrap pattern:
|
|
8
|
+
// 1. Sense — measure surfaces (README, npm, GitHub, docs)
|
|
9
|
+
// 2. Score — grade each dimension of visibility
|
|
10
|
+
// 3. Gap — identify the biggest delta between capability and perception
|
|
11
|
+
// 4. Act — recommend (or execute) the single highest-impact fix
|
|
12
|
+
// 5. Record — log the run so the next one starts from a higher floor
|
|
13
|
+
//
|
|
14
|
+
// This is not a feature tool. It's the meta-tool that makes features matter.
|
|
15
|
+
//
|
|
16
|
+
// Reference: Hernandez, I. (2026). "The Bootstrap Pattern: Outer-Loop
|
|
17
|
+
// Optimization for Open Source Projects." kernel.chat/bootstrap
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { join, basename } from 'node:path';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
21
|
+
import chalk from 'chalk';
|
|
22
|
+
// ── Helpers ──
|
|
23
|
+
function execQuiet(cmd, timeoutMs = 5000) {
|
|
24
|
+
try {
|
|
25
|
+
return execSync(cmd, { encoding: 'utf-8', timeout: timeoutMs, stdio: 'pipe', cwd: process.cwd() }).trim();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function fileExists(path) {
|
|
32
|
+
return existsSync(join(process.cwd(), path));
|
|
33
|
+
}
|
|
34
|
+
function readFile(path) {
|
|
35
|
+
const full = join(process.cwd(), path);
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(full, 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function gradeFromPercent(pct) {
|
|
44
|
+
if (pct >= 90)
|
|
45
|
+
return 'A';
|
|
46
|
+
if (pct >= 80)
|
|
47
|
+
return 'B';
|
|
48
|
+
if (pct >= 70)
|
|
49
|
+
return 'C';
|
|
50
|
+
if (pct >= 60)
|
|
51
|
+
return 'D';
|
|
52
|
+
return 'F';
|
|
53
|
+
}
|
|
54
|
+
// ── GitHub API ──
|
|
55
|
+
async function githubRepoData(repo) {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
58
|
+
headers: { 'User-Agent': 'kbot-bootstrap/1.0', Accept: 'application/vnd.github.v3+json' },
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
return null;
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── npm API ──
|
|
69
|
+
async function npmData(pkg) {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`);
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
return null;
|
|
74
|
+
return res.json();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function npmDownloads(pkg) {
|
|
81
|
+
try {
|
|
82
|
+
const [weekRes, dayRes] = await Promise.all([
|
|
83
|
+
fetch(`https://api.npmjs.org/downloads/point/last-week/${pkg}`),
|
|
84
|
+
fetch(`https://api.npmjs.org/downloads/point/last-day/${pkg}`),
|
|
85
|
+
]);
|
|
86
|
+
if (!weekRes.ok || !dayRes.ok)
|
|
87
|
+
return null;
|
|
88
|
+
const [week, day] = await Promise.all([weekRes.json(), dayRes.json()]);
|
|
89
|
+
return { weekly: week.downloads, daily: day.downloads };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ── Sections ──
|
|
96
|
+
async function checkFirstImpression() {
|
|
97
|
+
const section = {
|
|
98
|
+
name: 'First Impression',
|
|
99
|
+
score: 0,
|
|
100
|
+
maxScore: 25,
|
|
101
|
+
findings: [],
|
|
102
|
+
status: 'pass',
|
|
103
|
+
};
|
|
104
|
+
// README exists and has substance
|
|
105
|
+
const readme = readFile('README.md');
|
|
106
|
+
if (!readme) {
|
|
107
|
+
section.findings.push('No README.md');
|
|
108
|
+
section.fix = 'Create a README.md — this is the first thing anyone sees';
|
|
109
|
+
section.status = 'fail';
|
|
110
|
+
return section;
|
|
111
|
+
}
|
|
112
|
+
section.score += 2;
|
|
113
|
+
section.findings.push('README.md exists');
|
|
114
|
+
// Length check
|
|
115
|
+
if (readme.length > 2000) {
|
|
116
|
+
section.score += 3;
|
|
117
|
+
section.findings.push(`README is substantial (${(readme.length / 1024).toFixed(1)}KB)`);
|
|
118
|
+
}
|
|
119
|
+
else if (readme.length > 500) {
|
|
120
|
+
section.score += 1;
|
|
121
|
+
section.findings.push('README is short — consider expanding');
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
section.findings.push('README is too short (under 500 chars)');
|
|
125
|
+
section.fix = 'Expand your README — explain what this does and why someone should care';
|
|
126
|
+
}
|
|
127
|
+
// Has a GIF/image/screenshot
|
|
128
|
+
const hasImage = /!\[.*\]\(.*\.(gif|png|jpg|jpeg|svg|webp)/i.test(readme) ||
|
|
129
|
+
/<img\s+src=/i.test(readme);
|
|
130
|
+
if (hasImage) {
|
|
131
|
+
section.score += 5;
|
|
132
|
+
section.findings.push('Has visual demo (GIF/image)');
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
section.findings.push('No visual demo — GIFs increase star conversion 5-10x');
|
|
136
|
+
if (!section.fix)
|
|
137
|
+
section.fix = 'Add a GIF or screenshot to your README — visual demos dramatically increase engagement';
|
|
138
|
+
}
|
|
139
|
+
// Install command
|
|
140
|
+
const hasInstall = /npm (install|i)\s|pip install|cargo install|brew install|go install|curl.*install/i.test(readme);
|
|
141
|
+
if (hasInstall) {
|
|
142
|
+
section.score += 3;
|
|
143
|
+
section.findings.push('Has install command');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
section.findings.push('No install command in README');
|
|
147
|
+
if (!section.fix)
|
|
148
|
+
section.fix = 'Add a one-line install command near the top of your README';
|
|
149
|
+
}
|
|
150
|
+
// Quick start / usage examples
|
|
151
|
+
const hasUsage = /quick\s*start|usage|example|getting\s*started/i.test(readme);
|
|
152
|
+
if (hasUsage) {
|
|
153
|
+
section.score += 3;
|
|
154
|
+
section.findings.push('Has usage/quickstart section');
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
section.findings.push('No usage examples');
|
|
158
|
+
}
|
|
159
|
+
// Badges
|
|
160
|
+
const badgeCount = (readme.match(/\[!\[.*\]\(.*\)\]\(.*\)/g) || []).length +
|
|
161
|
+
(readme.match(/<img src="https:\/\/img\.shields\.io/g) || []).length;
|
|
162
|
+
if (badgeCount >= 3) {
|
|
163
|
+
section.score += 3;
|
|
164
|
+
section.findings.push(`${badgeCount} badges`);
|
|
165
|
+
}
|
|
166
|
+
else if (badgeCount > 0) {
|
|
167
|
+
section.score += 1;
|
|
168
|
+
section.findings.push(`Only ${badgeCount} badge(s) — consider adding version, license, downloads`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
section.findings.push('No badges');
|
|
172
|
+
}
|
|
173
|
+
// Comparison table
|
|
174
|
+
const hasComparison = /\|.*\|.*\|.*\n\|.*---.*\|/m.test(readme) &&
|
|
175
|
+
/compar|vs\b|alternative/i.test(readme);
|
|
176
|
+
if (hasComparison) {
|
|
177
|
+
section.score += 3;
|
|
178
|
+
section.findings.push('Has comparison table');
|
|
179
|
+
}
|
|
180
|
+
// Architecture/how it works
|
|
181
|
+
const hasArchitecture = /architect|how it works|under the hood|design/i.test(readme);
|
|
182
|
+
if (hasArchitecture) {
|
|
183
|
+
section.score += 3;
|
|
184
|
+
section.findings.push('Has architecture/design section');
|
|
185
|
+
}
|
|
186
|
+
return section;
|
|
187
|
+
}
|
|
188
|
+
async function checkDistribution(packageJson) {
|
|
189
|
+
const section = {
|
|
190
|
+
name: 'Distribution',
|
|
191
|
+
score: 0,
|
|
192
|
+
maxScore: 25,
|
|
193
|
+
findings: [],
|
|
194
|
+
status: 'pass',
|
|
195
|
+
};
|
|
196
|
+
// package.json exists
|
|
197
|
+
if (!packageJson) {
|
|
198
|
+
const hasPkg = fileExists('package.json') || fileExists('Cargo.toml') ||
|
|
199
|
+
fileExists('pyproject.toml') || fileExists('go.mod');
|
|
200
|
+
if (hasPkg) {
|
|
201
|
+
section.score += 2;
|
|
202
|
+
section.findings.push('Has package manifest');
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
section.findings.push('No package manifest found');
|
|
206
|
+
section.fix = 'Add a package manifest (package.json, Cargo.toml, etc.) to make your project installable';
|
|
207
|
+
section.status = 'warn';
|
|
208
|
+
}
|
|
209
|
+
return section;
|
|
210
|
+
}
|
|
211
|
+
section.score += 2;
|
|
212
|
+
section.findings.push('package.json exists');
|
|
213
|
+
const name = packageJson.name;
|
|
214
|
+
// Published to npm
|
|
215
|
+
if (name) {
|
|
216
|
+
const npm = await npmData(name);
|
|
217
|
+
if (npm) {
|
|
218
|
+
section.score += 5;
|
|
219
|
+
section.findings.push(`Published on npm: ${name}`);
|
|
220
|
+
// Downloads
|
|
221
|
+
const dl = await npmDownloads(name);
|
|
222
|
+
if (dl) {
|
|
223
|
+
if (dl.weekly > 1000) {
|
|
224
|
+
section.score += 5;
|
|
225
|
+
section.findings.push(`${dl.weekly.toLocaleString()}/week downloads`);
|
|
226
|
+
}
|
|
227
|
+
else if (dl.weekly > 100) {
|
|
228
|
+
section.score += 3;
|
|
229
|
+
section.findings.push(`${dl.weekly.toLocaleString()}/week downloads — growing`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
section.score += 1;
|
|
233
|
+
section.findings.push(`${dl.weekly.toLocaleString()}/week downloads — low`);
|
|
234
|
+
if (!section.fix)
|
|
235
|
+
section.fix = 'Downloads are low — focus on launch posts (HN, Reddit, Twitter) to drive awareness';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
section.findings.push('Not published on npm');
|
|
241
|
+
if (!section.fix)
|
|
242
|
+
section.fix = 'Publish to npm — `npm publish --access public`';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// npm description
|
|
246
|
+
const desc = packageJson.description;
|
|
247
|
+
if (desc && desc.length > 50) {
|
|
248
|
+
section.score += 3;
|
|
249
|
+
section.findings.push('Has detailed npm description');
|
|
250
|
+
}
|
|
251
|
+
else if (desc) {
|
|
252
|
+
section.score += 1;
|
|
253
|
+
section.findings.push('npm description is short — expand it for better search ranking');
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
section.findings.push('No npm description');
|
|
257
|
+
}
|
|
258
|
+
// Keywords
|
|
259
|
+
const keywords = packageJson.keywords;
|
|
260
|
+
if (keywords && keywords.length >= 10) {
|
|
261
|
+
section.score += 3;
|
|
262
|
+
section.findings.push(`${keywords.length} npm keywords`);
|
|
263
|
+
}
|
|
264
|
+
else if (keywords && keywords.length > 0) {
|
|
265
|
+
section.score += 1;
|
|
266
|
+
section.findings.push(`Only ${keywords.length} keywords — more = better npm search ranking`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
section.findings.push('No npm keywords');
|
|
270
|
+
}
|
|
271
|
+
// Docker
|
|
272
|
+
if (fileExists('Dockerfile')) {
|
|
273
|
+
section.score += 3;
|
|
274
|
+
section.findings.push('Has Dockerfile');
|
|
275
|
+
}
|
|
276
|
+
// Install script
|
|
277
|
+
if (fileExists('install.sh') || fileExists('install.ps1')) {
|
|
278
|
+
section.score += 2;
|
|
279
|
+
section.findings.push('Has install script');
|
|
280
|
+
}
|
|
281
|
+
// Homebrew
|
|
282
|
+
const readme = readFile('README.md') || '';
|
|
283
|
+
if (/brew install/i.test(readme)) {
|
|
284
|
+
section.score += 2;
|
|
285
|
+
section.findings.push('Has Homebrew formula');
|
|
286
|
+
}
|
|
287
|
+
return section;
|
|
288
|
+
}
|
|
289
|
+
async function checkGitHubPresence() {
|
|
290
|
+
const section = {
|
|
291
|
+
name: 'GitHub Presence',
|
|
292
|
+
score: 0,
|
|
293
|
+
maxScore: 25,
|
|
294
|
+
findings: [],
|
|
295
|
+
status: 'pass',
|
|
296
|
+
};
|
|
297
|
+
// Detect GitHub repo
|
|
298
|
+
const remoteUrl = execQuiet('git remote get-url origin');
|
|
299
|
+
if (!remoteUrl) {
|
|
300
|
+
section.findings.push('No git remote — cannot check GitHub presence');
|
|
301
|
+
section.status = 'warn';
|
|
302
|
+
section.fix = 'Push your project to GitHub';
|
|
303
|
+
return section;
|
|
304
|
+
}
|
|
305
|
+
// Extract owner/repo
|
|
306
|
+
const match = remoteUrl.match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
307
|
+
if (!match) {
|
|
308
|
+
section.findings.push('Remote is not GitHub');
|
|
309
|
+
section.score += 2;
|
|
310
|
+
return section;
|
|
311
|
+
}
|
|
312
|
+
const repo = `${match[1]}/${match[2]}`;
|
|
313
|
+
section.findings.push(`GitHub: ${repo}`);
|
|
314
|
+
const data = await githubRepoData(repo);
|
|
315
|
+
if (!data) {
|
|
316
|
+
section.findings.push('Could not fetch GitHub data (rate limited or private)');
|
|
317
|
+
section.score += 2;
|
|
318
|
+
return section;
|
|
319
|
+
}
|
|
320
|
+
// Stars
|
|
321
|
+
const stars = data.stargazers_count || 0;
|
|
322
|
+
if (stars >= 100) {
|
|
323
|
+
section.score += 8;
|
|
324
|
+
section.findings.push(`${stars} stars — strong social proof`);
|
|
325
|
+
}
|
|
326
|
+
else if (stars >= 10) {
|
|
327
|
+
section.score += 4;
|
|
328
|
+
section.findings.push(`${stars} stars — building momentum`);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
section.score += 1;
|
|
332
|
+
section.findings.push(`${stars} star(s) — the first impression isn't converting to stars`);
|
|
333
|
+
if (!section.fix)
|
|
334
|
+
section.fix = 'Stars are low — improve README visual (add GIF), then post to HN/Reddit/Twitter';
|
|
335
|
+
}
|
|
336
|
+
// Description
|
|
337
|
+
if (data.description) {
|
|
338
|
+
section.score += 3;
|
|
339
|
+
section.findings.push('Has GitHub description');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
section.findings.push('No GitHub description');
|
|
343
|
+
if (!section.fix)
|
|
344
|
+
section.fix = 'Add a GitHub repo description — it appears in search results';
|
|
345
|
+
}
|
|
346
|
+
// Topics
|
|
347
|
+
if (data.topics?.length >= 5) {
|
|
348
|
+
section.score += 3;
|
|
349
|
+
section.findings.push(`${data.topics.length} topics`);
|
|
350
|
+
}
|
|
351
|
+
else if (data.topics?.length > 0) {
|
|
352
|
+
section.score += 1;
|
|
353
|
+
section.findings.push(`Only ${data.topics.length} topic(s) — add more for discoverability`);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
section.findings.push('No topics — these help people find your project');
|
|
357
|
+
}
|
|
358
|
+
// License
|
|
359
|
+
if (data.license) {
|
|
360
|
+
section.score += 2;
|
|
361
|
+
section.findings.push(`License: ${data.license.spdx_id}`);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
section.findings.push('No license — many developers won\'t use unlicensed projects');
|
|
365
|
+
if (!section.fix)
|
|
366
|
+
section.fix = 'Add a LICENSE file (MIT is most common for open source)';
|
|
367
|
+
}
|
|
368
|
+
// Activity
|
|
369
|
+
const daysAgo = (Date.now() - new Date(data.pushed_at).getTime()) / 86400000;
|
|
370
|
+
if (daysAgo < 7) {
|
|
371
|
+
section.score += 3;
|
|
372
|
+
section.findings.push(`Active — last push ${Math.floor(daysAgo)}d ago`);
|
|
373
|
+
}
|
|
374
|
+
else if (daysAgo < 30) {
|
|
375
|
+
section.score += 2;
|
|
376
|
+
section.findings.push(`Last push ${Math.floor(daysAgo)}d ago`);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
section.findings.push(`Stale — last push ${Math.floor(daysAgo)}d ago`);
|
|
380
|
+
section.status = 'warn';
|
|
381
|
+
}
|
|
382
|
+
// Community files
|
|
383
|
+
const communityFiles = ['CONTRIBUTING.md', 'CODE_OF_CONDUCT.md', '.github/ISSUE_TEMPLATE'];
|
|
384
|
+
let communityCount = 0;
|
|
385
|
+
for (const f of communityFiles) {
|
|
386
|
+
if (fileExists(f))
|
|
387
|
+
communityCount++;
|
|
388
|
+
}
|
|
389
|
+
if (communityCount >= 2) {
|
|
390
|
+
section.score += 3;
|
|
391
|
+
section.findings.push(`${communityCount}/3 community files`);
|
|
392
|
+
}
|
|
393
|
+
else if (communityCount > 0) {
|
|
394
|
+
section.score += 1;
|
|
395
|
+
section.findings.push(`${communityCount}/3 community files — add CONTRIBUTING.md and CODE_OF_CONDUCT.md`);
|
|
396
|
+
}
|
|
397
|
+
// Forks and watchers
|
|
398
|
+
if (data.forks_count > 0) {
|
|
399
|
+
section.score += 2;
|
|
400
|
+
section.findings.push(`${data.forks_count} fork(s)`);
|
|
401
|
+
}
|
|
402
|
+
// Clone-to-star ratio
|
|
403
|
+
if (stars > 0 && data.forks_count > 0) {
|
|
404
|
+
const ratio = stars / (data.forks_count + stars);
|
|
405
|
+
section.findings.push(`Clone-to-star ratio: ${(ratio * 100).toFixed(1)}%`);
|
|
406
|
+
}
|
|
407
|
+
return section;
|
|
408
|
+
}
|
|
409
|
+
function checkSurfaceCoherence(packageJson) {
|
|
410
|
+
const section = {
|
|
411
|
+
name: 'Surface Coherence',
|
|
412
|
+
score: 0,
|
|
413
|
+
maxScore: 25,
|
|
414
|
+
findings: [],
|
|
415
|
+
status: 'pass',
|
|
416
|
+
};
|
|
417
|
+
const readme = readFile('README.md') || '';
|
|
418
|
+
const pkg = packageJson || {};
|
|
419
|
+
// Version consistency
|
|
420
|
+
const pkgVersion = pkg.version;
|
|
421
|
+
if (pkgVersion) {
|
|
422
|
+
const versionInReadme = readme.includes(pkgVersion);
|
|
423
|
+
if (versionInReadme) {
|
|
424
|
+
section.score += 3;
|
|
425
|
+
section.findings.push(`README mentions current version (${pkgVersion})`);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
section.findings.push(`README doesn't mention version ${pkgVersion}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Description consistency
|
|
432
|
+
const pkgDesc = pkg.description;
|
|
433
|
+
if (pkgDesc && readme.length > 100) {
|
|
434
|
+
// Check if key phrases from npm description appear in README
|
|
435
|
+
const keyWords = pkgDesc.split(/\s+/).filter(w => w.length > 5).slice(0, 5);
|
|
436
|
+
const matchCount = keyWords.filter(w => readme.toLowerCase().includes(w.toLowerCase())).length;
|
|
437
|
+
if (matchCount >= 3) {
|
|
438
|
+
section.score += 3;
|
|
439
|
+
section.findings.push('npm description aligns with README');
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
section.findings.push('npm description and README tell different stories');
|
|
443
|
+
if (!section.fix)
|
|
444
|
+
section.fix = 'Align your npm description with your README — they should tell the same story';
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Changelog / What's New
|
|
448
|
+
const hasChangelog = fileExists('CHANGELOG.md') || /what.s new|changelog|release/i.test(readme);
|
|
449
|
+
if (hasChangelog) {
|
|
450
|
+
section.score += 3;
|
|
451
|
+
section.findings.push('Has changelog or "What\'s New" section');
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
section.findings.push('No changelog — users want to know what changed');
|
|
455
|
+
}
|
|
456
|
+
// ROADMAP
|
|
457
|
+
if (fileExists('ROADMAP.md') || /roadmap/i.test(readme)) {
|
|
458
|
+
section.score += 3;
|
|
459
|
+
section.findings.push('Has roadmap');
|
|
460
|
+
}
|
|
461
|
+
// Links consistency
|
|
462
|
+
const links = [];
|
|
463
|
+
if (/npmjs\.com/i.test(readme))
|
|
464
|
+
links.push('npm');
|
|
465
|
+
if (/github\.com/i.test(readme))
|
|
466
|
+
links.push('GitHub');
|
|
467
|
+
if (/discord/i.test(readme))
|
|
468
|
+
links.push('Discord');
|
|
469
|
+
if (/twitter\.com|x\.com/i.test(readme))
|
|
470
|
+
links.push('Twitter/X');
|
|
471
|
+
if (links.length >= 3) {
|
|
472
|
+
section.score += 3;
|
|
473
|
+
section.findings.push(`${links.length} links: ${links.join(', ')}`);
|
|
474
|
+
}
|
|
475
|
+
else if (links.length > 0) {
|
|
476
|
+
section.score += 1;
|
|
477
|
+
section.findings.push(`Only ${links.length} link(s) — add npm, GitHub, Discord, Twitter`);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
section.findings.push('No community links in README');
|
|
481
|
+
}
|
|
482
|
+
// SEO files
|
|
483
|
+
if (fileExists('robots.txt')) {
|
|
484
|
+
section.score += 1;
|
|
485
|
+
section.findings.push('Has robots.txt');
|
|
486
|
+
}
|
|
487
|
+
if (fileExists('sitemap.xml')) {
|
|
488
|
+
section.score += 1;
|
|
489
|
+
section.findings.push('Has sitemap.xml');
|
|
490
|
+
}
|
|
491
|
+
// Count surface files that mention the project name
|
|
492
|
+
const name = pkg.name;
|
|
493
|
+
if (name) {
|
|
494
|
+
const surfaceFiles = ['README.md', 'CONTRIBUTING.md', 'ROADMAP.md', 'package.json'];
|
|
495
|
+
const existing = surfaceFiles.filter(f => fileExists(f));
|
|
496
|
+
section.score += Math.min(4, existing.length);
|
|
497
|
+
section.findings.push(`${existing.length}/${surfaceFiles.length} surface files present`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
section.score += 2;
|
|
501
|
+
}
|
|
502
|
+
// Staleness detection
|
|
503
|
+
const gitLog = execQuiet('git log -1 --format=%H -- README.md');
|
|
504
|
+
const lastCommit = execQuiet('git log -1 --format=%H');
|
|
505
|
+
if (gitLog && lastCommit) {
|
|
506
|
+
const readmeAge = execQuiet('git log -1 --format=%cr -- README.md');
|
|
507
|
+
if (readmeAge) {
|
|
508
|
+
section.findings.push(`README last updated: ${readmeAge}`);
|
|
509
|
+
if (/month|year/i.test(readmeAge)) {
|
|
510
|
+
section.findings.push('README may be stale — review for accuracy');
|
|
511
|
+
if (!section.fix)
|
|
512
|
+
section.fix = 'Your README hasn\'t been updated recently — review it for stale numbers, versions, and feature lists';
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
section.score += 4;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return section;
|
|
520
|
+
}
|
|
521
|
+
// ── Main ──
|
|
522
|
+
export async function runBootstrap() {
|
|
523
|
+
const projectName = basename(process.cwd());
|
|
524
|
+
// Load package.json if it exists
|
|
525
|
+
let packageJson = null;
|
|
526
|
+
try {
|
|
527
|
+
packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'));
|
|
528
|
+
}
|
|
529
|
+
catch { /* not a Node project */ }
|
|
530
|
+
// Run all checks in parallel where possible
|
|
531
|
+
const [firstImpression, distribution, githubPresence] = await Promise.all([
|
|
532
|
+
checkFirstImpression(),
|
|
533
|
+
checkDistribution(packageJson),
|
|
534
|
+
checkGitHubPresence(),
|
|
535
|
+
]);
|
|
536
|
+
// Surface coherence is sync (reads local files only)
|
|
537
|
+
const surfaceCoherence = checkSurfaceCoherence(packageJson);
|
|
538
|
+
const sections = [firstImpression, distribution, githubPresence, surfaceCoherence];
|
|
539
|
+
// Calculate totals
|
|
540
|
+
const totalScore = sections.reduce((sum, s) => sum + s.score, 0);
|
|
541
|
+
const totalMax = sections.reduce((sum, s) => sum + s.maxScore, 0);
|
|
542
|
+
const pct = Math.round((totalScore / totalMax) * 100);
|
|
543
|
+
const grade = gradeFromPercent(pct);
|
|
544
|
+
// Set section statuses
|
|
545
|
+
for (const s of sections) {
|
|
546
|
+
const sPct = s.maxScore > 0 ? (s.score / s.maxScore) * 100 : 0;
|
|
547
|
+
if (s.status !== 'fail') {
|
|
548
|
+
s.status = sPct >= 60 ? 'pass' : 'warn';
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Find the top fix — lowest-scoring section with a fix
|
|
552
|
+
const withFixes = sections.filter(s => s.fix).sort((a, b) => {
|
|
553
|
+
const aPct = a.score / a.maxScore;
|
|
554
|
+
const bPct = b.score / b.maxScore;
|
|
555
|
+
return aPct - bPct; // lowest percentage first = highest impact
|
|
556
|
+
});
|
|
557
|
+
const topFix = withFixes[0]?.fix || 'All sections look good — focus on sharing your project';
|
|
558
|
+
// Summary
|
|
559
|
+
const fails = sections.filter(s => s.status === 'fail');
|
|
560
|
+
const warns = sections.filter(s => s.status === 'warn');
|
|
561
|
+
const summary = fails.length > 0
|
|
562
|
+
? `${fails.length} critical gap(s). Your project is invisible in ${fails.map(s => s.name.toLowerCase()).join(', ')}.`
|
|
563
|
+
: warns.length > 0
|
|
564
|
+
? `${warns.length} area(s) need work. Fix the top recommendation to compound.`
|
|
565
|
+
: 'Strong visibility. Focus on distribution — post, share, submit to lists.';
|
|
566
|
+
return {
|
|
567
|
+
project: packageJson?.name || projectName,
|
|
568
|
+
score: totalScore,
|
|
569
|
+
maxScore: totalMax,
|
|
570
|
+
grade,
|
|
571
|
+
sections,
|
|
572
|
+
topFix,
|
|
573
|
+
summary,
|
|
574
|
+
timestamp: new Date().toISOString(),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// ── Formatting ──
|
|
578
|
+
export function formatBootstrapReport(report) {
|
|
579
|
+
const statusIcon = (s) => s === 'pass' ? '✅' : s === 'warn' ? '⚠️' : '❌';
|
|
580
|
+
const pct = Math.round((report.score / report.maxScore) * 100);
|
|
581
|
+
const lines = [];
|
|
582
|
+
// Header
|
|
583
|
+
lines.push('');
|
|
584
|
+
lines.push(chalk.bold(` kbot Bootstrap — ${report.project}`));
|
|
585
|
+
lines.push(chalk.dim(` ──────────────────────────────────────────────────`));
|
|
586
|
+
lines.push('');
|
|
587
|
+
lines.push(` ${chalk.bold('Score:')} ${report.score}/${report.maxScore} (${pct}%) — Grade ${chalk.bold(report.grade)}`);
|
|
588
|
+
lines.push(` ${chalk.dim(report.summary)}`);
|
|
589
|
+
lines.push('');
|
|
590
|
+
// Sections
|
|
591
|
+
for (const section of report.sections) {
|
|
592
|
+
const sPct = section.maxScore > 0 ? Math.round((section.score / section.maxScore) * 100) : 0;
|
|
593
|
+
const icon = section.status === 'pass' ? chalk.green('✓') : section.status === 'warn' ? chalk.yellow('!') : chalk.red('✗');
|
|
594
|
+
lines.push(` ${icon} ${chalk.bold(section.name)}${chalk.dim(` — ${section.score}/${section.maxScore} (${sPct}%)`)}`);
|
|
595
|
+
for (const f of section.findings) {
|
|
596
|
+
lines.push(` ${chalk.dim('·')} ${f}`);
|
|
597
|
+
}
|
|
598
|
+
if (section.fix && section.status !== 'pass') {
|
|
599
|
+
lines.push(` ${chalk.yellow('→')} ${chalk.yellow(section.fix)}`);
|
|
600
|
+
}
|
|
601
|
+
lines.push('');
|
|
602
|
+
}
|
|
603
|
+
// Top fix
|
|
604
|
+
lines.push(chalk.dim(` ──────────────────────────────────────────────────`));
|
|
605
|
+
lines.push(` ${chalk.bold('Top fix:')} ${report.topFix}`);
|
|
606
|
+
lines.push('');
|
|
607
|
+
lines.push(chalk.dim(` The bootstrap pattern: close the gap between what your project IS`));
|
|
608
|
+
lines.push(chalk.dim(` and what the world PERCEIVES. Fix one thing per run. Compound.`));
|
|
609
|
+
lines.push('');
|
|
610
|
+
return lines.join('\n');
|
|
611
|
+
}
|
|
612
|
+
export function formatBootstrapMarkdown(report) {
|
|
613
|
+
const statusIcon = (s) => s === 'pass' ? '✅' : s === 'warn' ? '⚠️' : '❌';
|
|
614
|
+
const pct = Math.round((report.score / report.maxScore) * 100);
|
|
615
|
+
const lines = [
|
|
616
|
+
`# Bootstrap Report: ${report.project}`,
|
|
617
|
+
'',
|
|
618
|
+
`> Generated by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — the outer-loop optimizer`,
|
|
619
|
+
'',
|
|
620
|
+
`## Score: ${report.score}/${report.maxScore} (${pct}%) — Grade ${report.grade}`,
|
|
621
|
+
'',
|
|
622
|
+
`**${report.summary}**`,
|
|
623
|
+
'',
|
|
624
|
+
];
|
|
625
|
+
for (const section of report.sections) {
|
|
626
|
+
const sPct = section.maxScore > 0 ? Math.round((section.score / section.maxScore) * 100) : 0;
|
|
627
|
+
lines.push(`### ${statusIcon(section.status)} ${section.name} — ${section.score}/${section.maxScore} (${sPct}%)`);
|
|
628
|
+
for (const f of section.findings) {
|
|
629
|
+
lines.push(`- ${f}`);
|
|
630
|
+
}
|
|
631
|
+
if (section.fix && section.status !== 'pass') {
|
|
632
|
+
lines.push(`- **Fix:** ${section.fix}`);
|
|
633
|
+
}
|
|
634
|
+
lines.push('');
|
|
635
|
+
}
|
|
636
|
+
lines.push('---');
|
|
637
|
+
lines.push('');
|
|
638
|
+
lines.push(`**Top fix:** ${report.topFix}`);
|
|
639
|
+
lines.push('');
|
|
640
|
+
lines.push(`*The bootstrap pattern: close the gap between what your project IS and what the world PERCEIVES.*`);
|
|
641
|
+
lines.push(`*Fix one thing per run. Compound.*`);
|
|
642
|
+
lines.push('');
|
|
643
|
+
lines.push(`*[kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 22 agents, 284 tools, 20 providers*`);
|
|
644
|
+
return lines.join('\n');
|
|
645
|
+
}
|
|
646
|
+
//# sourceMappingURL=bootstrap.js.map
|