@whenlabs/when 0.10.0 → 0.11.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/action.yml CHANGED
@@ -1,17 +1,79 @@
1
1
  name: 'WhenLabs CI Check'
2
2
  description: 'Run documentation drift, env validation, and license checks'
3
+ branding:
4
+ icon: 'check-circle'
5
+ color: 'orange'
3
6
  inputs:
4
7
  tools:
5
- description: 'Comma-separated tools to run'
8
+ description: 'Comma-separated tools to run (stale, envalid, vow)'
6
9
  default: 'stale,envalid,vow'
10
+ checks:
11
+ description: 'Alias for tools — select which checks to run (stale, envalid, vow)'
12
+ default: ''
7
13
  fail_on:
8
14
  description: 'Fail on: error, warning, or none'
9
15
  default: 'error'
16
+ comment:
17
+ description: 'Post results as a PR comment when running on a pull request'
18
+ default: 'false'
10
19
  runs:
11
20
  using: 'composite'
12
21
  steps:
13
22
  - uses: actions/setup-node@v4
14
23
  with:
15
24
  node-version: '20'
16
- - run: npx @whenlabs/when ci --ci
25
+
26
+ - name: Cache npm
27
+ uses: actions/cache@v4
28
+ with:
29
+ path: ~/.npm
30
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
31
+ restore-keys: |
32
+ ${{ runner.os }}-node-
33
+
34
+ - name: Run WhenLabs checks
35
+ id: whenlabs_check
36
+ run: |
37
+ SELECTED="${{ inputs.checks != '' && inputs.checks || inputs.tools }}"
38
+ WHENLABS_TOOLS="$SELECTED" npx @whenlabs/when ci --ci 2>&1 | tee /tmp/whenlabs-output.txt
39
+ echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
40
+ shell: bash
41
+
42
+ - name: Post PR comment
43
+ if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' }}
44
+ uses: actions/github-script@v7
45
+ with:
46
+ script: |
47
+ const fs = require('fs');
48
+ const output = fs.readFileSync('/tmp/whenlabs-output.txt', 'utf8');
49
+ const exitCode = '${{ steps.whenlabs_check.outputs.exit_code }}';
50
+ const status = exitCode === '0' ? '✅ All checks passed' : '❌ Issues found';
51
+ const body = [
52
+ '## WhenLabs CI Check',
53
+ '',
54
+ status,
55
+ '',
56
+ '<details><summary>Full output</summary>',
57
+ '',
58
+ '```',
59
+ output.trim(),
60
+ '```',
61
+ '',
62
+ '</details>',
63
+ ].join('\n');
64
+ await github.rest.issues.createComment({
65
+ owner: context.repo.owner,
66
+ repo: context.repo.repo,
67
+ issue_number: context.issue.number,
68
+ body,
69
+ });
70
+
71
+ - name: Fail if issues found
72
+ if: ${{ inputs.fail_on != 'none' }}
73
+ run: |
74
+ EXIT="${{ steps.whenlabs_check.outputs.exit_code }}"
75
+ if [ "$EXIT" != "0" ]; then
76
+ echo "WhenLabs checks found issues. Set fail_on: none to suppress this failure."
77
+ exit 1
78
+ fi
17
79
  shell: bash
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/velocity-dashboard.ts
4
+ import { writeFileSync, mkdirSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { execSync } from "child_process";
8
+ import { TaskQueries, initDb } from "@whenlabs/velocity-mcp/lib";
9
+ var DASH_DIR = join(homedir(), ".whenlabs");
10
+ var DASH_PATH = join(DASH_DIR, "dashboard.html");
11
+ function parseTags(raw) {
12
+ try {
13
+ return JSON.parse(raw);
14
+ } catch {
15
+ return raw ? raw.split(",").map((t) => t.trim()).filter(Boolean) : [];
16
+ }
17
+ }
18
+ function fmtDuration(seconds) {
19
+ if (seconds < 60) return `${Math.round(seconds)}s`;
20
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
21
+ const h = Math.floor(seconds / 3600);
22
+ const m = Math.round(seconds % 3600 / 60);
23
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
24
+ }
25
+ function buildCategoryStats(tasks) {
26
+ const map = /* @__PURE__ */ new Map();
27
+ for (const t of tasks) {
28
+ if (!t.duration_seconds) continue;
29
+ const existing = map.get(t.category) ?? { count: 0, total: 0 };
30
+ existing.count++;
31
+ existing.total += t.duration_seconds;
32
+ map.set(t.category, existing);
33
+ }
34
+ return Array.from(map.entries()).map(([category, { count, total }]) => ({
35
+ category,
36
+ count,
37
+ avgDuration: total / count,
38
+ totalDuration: total
39
+ })).sort((a, b) => b.count - a.count);
40
+ }
41
+ function buildTagCounts(tasks) {
42
+ const map = /* @__PURE__ */ new Map();
43
+ for (const t of tasks) {
44
+ for (const tag of parseTags(t.tags)) {
45
+ map.set(tag, (map.get(tag) ?? 0) + 1);
46
+ }
47
+ }
48
+ return Array.from(map.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count).slice(0, 15);
49
+ }
50
+ function svgBarChart(data, opts) {
51
+ const { width = 520, barHeight = 32, color = "#6366f1", valueFormatter = String } = opts;
52
+ if (data.length === 0) return '<p style="color:#888;font-size:13px">No data yet.</p>';
53
+ const maxVal = Math.max(...data.map((d) => d.value), 1);
54
+ const labelWidth = 120;
55
+ const valueWidth = 70;
56
+ const chartWidth = width - labelWidth - valueWidth - 16;
57
+ const height = data.length * (barHeight + 8) + 16;
58
+ const bars = data.map((d, i) => {
59
+ const barW = Math.max(2, d.value / maxVal * chartWidth);
60
+ const y = i * (barHeight + 8) + 8;
61
+ const sub = d.subtitle ? `<text x="${labelWidth + barW + 6}" y="${y + barHeight / 2 + 4}" font-size="10" fill="#999">${d.subtitle}</text>` : "";
62
+ return `
63
+ <text x="${labelWidth - 6}" y="${y + barHeight / 2 + 4}" text-anchor="end" font-size="12" fill="#ddd" font-family="system-ui,sans-serif">${d.label}</text>
64
+ <rect x="${labelWidth}" y="${y}" width="${barW}" height="${barHeight}" rx="4" fill="${color}" opacity="0.85"/>
65
+ <text x="${labelWidth + barW + 6}" y="${y + barHeight / 2 + 4}" font-size="12" fill="#e2e8f0" font-family="system-ui,sans-serif">${valueFormatter(d.value)}${sub ? "" : ""}</text>
66
+ ${sub}`;
67
+ }).join("");
68
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${bars}</svg>`;
69
+ }
70
+ function generateHtml(tasks) {
71
+ const now = /* @__PURE__ */ new Date();
72
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
73
+ const recentTasks = tasks.filter((t) => t.ended_at && new Date(t.ended_at) >= sevenDaysAgo).sort((a, b) => new Date(b.ended_at).getTime() - new Date(a.ended_at).getTime()).slice(0, 20);
74
+ const catStats = buildCategoryStats(tasks);
75
+ const tagCounts = buildTagCounts(tasks);
76
+ const totalTasks = tasks.length;
77
+ const totalSeconds = tasks.reduce((sum, t) => sum + (t.duration_seconds ?? 0), 0);
78
+ const avgSeconds = totalTasks > 0 ? totalSeconds / totalTasks : 0;
79
+ const completedCount = tasks.filter((t) => t.status === "completed").length;
80
+ const catChartData = catStats.map((c) => ({
81
+ label: c.category,
82
+ value: c.count,
83
+ subtitle: fmtDuration(c.avgDuration) + " avg"
84
+ }));
85
+ const durationChartData = catStats.filter((c) => c.avgDuration > 0).map((c) => ({
86
+ label: c.category,
87
+ value: Math.round(c.avgDuration),
88
+ subtitle: ""
89
+ }));
90
+ const tagChartData = tagCounts.map((t) => ({
91
+ label: t.tag,
92
+ value: t.count
93
+ }));
94
+ const catChart = svgBarChart(catChartData, {
95
+ color: "#6366f1",
96
+ valueFormatter: (v) => `${v} tasks`
97
+ });
98
+ const durationChart = svgBarChart(durationChartData, {
99
+ color: "#10b981",
100
+ valueFormatter: fmtDuration
101
+ });
102
+ const tagChart = svgBarChart(tagChartData, {
103
+ color: "#f59e0b",
104
+ valueFormatter: (v) => `${v}`
105
+ });
106
+ const recentRows = recentTasks.map((t) => {
107
+ const tags = parseTags(t.tags);
108
+ const dur = t.duration_seconds ? fmtDuration(t.duration_seconds) : "\u2014";
109
+ const statusColor = t.status === "completed" ? "#10b981" : t.status === "failed" ? "#ef4444" : "#94a3b8";
110
+ const tagBadges = tags.slice(0, 4).map((tag) => `<span class="tag">${tag}</span>`).join("");
111
+ return `
112
+ <tr>
113
+ <td><span class="cat-badge cat-${t.category}">${t.category}</span></td>
114
+ <td class="desc">${t.description}</td>
115
+ <td>${tagBadges}</td>
116
+ <td style="color:${statusColor};text-align:center">${t.status ?? "\u2014"}</td>
117
+ <td style="text-align:right;color:#94a3b8">${dur}</td>
118
+ </tr>`;
119
+ }).join("");
120
+ return `<!DOCTYPE html>
121
+ <html lang="en">
122
+ <head>
123
+ <meta charset="UTF-8">
124
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
125
+ <title>Velocity Dashboard</title>
126
+ <style>
127
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
128
+ body {
129
+ font-family: system-ui, -apple-system, sans-serif;
130
+ background: #0f172a;
131
+ color: #e2e8f0;
132
+ min-height: 100vh;
133
+ padding: 24px;
134
+ }
135
+ .header {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ margin-bottom: 28px;
140
+ border-bottom: 1px solid #1e293b;
141
+ padding-bottom: 16px;
142
+ }
143
+ .header h1 { font-size: 22px; font-weight: 700; color: #f1f5f9; }
144
+ .header .subtitle { font-size: 12px; color: #64748b; margin-top: 2px; }
145
+ .badge-when {
146
+ background: #6366f1;
147
+ color: #fff;
148
+ font-size: 11px;
149
+ font-weight: 600;
150
+ padding: 3px 10px;
151
+ border-radius: 999px;
152
+ }
153
+ .stats-grid {
154
+ display: grid;
155
+ grid-template-columns: repeat(4, 1fr);
156
+ gap: 14px;
157
+ margin-bottom: 28px;
158
+ }
159
+ @media (max-width: 700px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
160
+ .stat-card {
161
+ background: #1e293b;
162
+ border: 1px solid #334155;
163
+ border-radius: 10px;
164
+ padding: 16px 20px;
165
+ }
166
+ .stat-card .label { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: .05em; }
167
+ .stat-card .value { font-size: 28px; font-weight: 700; color: #f1f5f9; margin: 4px 0 2px; }
168
+ .stat-card .sub { font-size: 11px; color: #64748b; }
169
+ .charts-grid {
170
+ display: grid;
171
+ grid-template-columns: 1fr 1fr;
172
+ gap: 18px;
173
+ margin-bottom: 28px;
174
+ }
175
+ @media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } }
176
+ .chart-card {
177
+ background: #1e293b;
178
+ border: 1px solid #334155;
179
+ border-radius: 10px;
180
+ padding: 18px 20px;
181
+ }
182
+ .chart-card.full { grid-column: 1 / -1; }
183
+ .chart-card h2 { font-size: 13px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 14px; }
184
+ .table-card {
185
+ background: #1e293b;
186
+ border: 1px solid #334155;
187
+ border-radius: 10px;
188
+ padding: 18px 20px;
189
+ overflow-x: auto;
190
+ }
191
+ .table-card h2 { font-size: 13px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 14px; }
192
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
193
+ th { text-align: left; color: #64748b; font-weight: 500; font-size: 11px; text-transform: uppercase; padding: 6px 10px; border-bottom: 1px solid #334155; }
194
+ td { padding: 8px 10px; border-bottom: 1px solid #1e293b; vertical-align: middle; }
195
+ tr:last-child td { border-bottom: none; }
196
+ .desc { max-width: 340px; color: #cbd5e1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
197
+ .tag {
198
+ display: inline-block;
199
+ background: #334155;
200
+ color: #94a3b8;
201
+ font-size: 10px;
202
+ padding: 1px 7px;
203
+ border-radius: 999px;
204
+ margin-right: 3px;
205
+ }
206
+ .cat-badge {
207
+ display: inline-block;
208
+ font-size: 10px;
209
+ font-weight: 600;
210
+ padding: 2px 8px;
211
+ border-radius: 999px;
212
+ text-transform: uppercase;
213
+ letter-spacing: .03em;
214
+ }
215
+ .cat-scaffold { background:#1e3a5f; color:#60a5fa; }
216
+ .cat-implement { background:#1a2e3b; color:#38bdf8; }
217
+ .cat-refactor { background:#1e3b2e; color:#4ade80; }
218
+ .cat-debug { background:#3b2020; color:#f87171; }
219
+ .cat-test { background:#2d2b3b; color:#a78bfa; }
220
+ .cat-config { background:#2d2a1a; color:#fbbf24; }
221
+ .cat-docs { background:#1e2e3b; color:#7dd3fc; }
222
+ .cat-deploy { background:#1e2e1e; color:#86efac; }
223
+ .generated-at { font-size: 11px; color: #475569; margin-top: 20px; text-align: right; }
224
+ </style>
225
+ </head>
226
+ <body>
227
+ <div class="header">
228
+ <div>
229
+ <h1>Velocity Dashboard</h1>
230
+ <div class="subtitle">Generated ${now.toLocaleString()}</div>
231
+ </div>
232
+ <span class="badge-when">@whenlabs/when</span>
233
+ </div>
234
+
235
+ <div class="stats-grid">
236
+ <div class="stat-card">
237
+ <div class="label">Total Tasks</div>
238
+ <div class="value">${totalTasks}</div>
239
+ <div class="sub">${completedCount} completed</div>
240
+ </div>
241
+ <div class="stat-card">
242
+ <div class="label">Total Time</div>
243
+ <div class="value">${fmtDuration(totalSeconds)}</div>
244
+ <div class="sub">across all tasks</div>
245
+ </div>
246
+ <div class="stat-card">
247
+ <div class="label">Avg Duration</div>
248
+ <div class="value">${fmtDuration(avgSeconds)}</div>
249
+ <div class="sub">per task</div>
250
+ </div>
251
+ <div class="stat-card">
252
+ <div class="label">Categories</div>
253
+ <div class="value">${catStats.length}</div>
254
+ <div class="sub">active categories</div>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="charts-grid">
259
+ <div class="chart-card">
260
+ <h2>Tasks by Category</h2>
261
+ ${catChart}
262
+ </div>
263
+ <div class="chart-card">
264
+ <h2>Avg Duration by Category</h2>
265
+ ${durationChart}
266
+ </div>
267
+ <div class="chart-card full">
268
+ <h2>Top Tags</h2>
269
+ ${tagChart}
270
+ </div>
271
+ </div>
272
+
273
+ <div class="table-card">
274
+ <h2>Recent Tasks (last 7 days)</h2>
275
+ ${recentRows ? `<table>
276
+ <thead><tr>
277
+ <th>Category</th><th>Description</th><th>Tags</th><th style="text-align:center">Status</th><th style="text-align:right">Duration</th>
278
+ </tr></thead>
279
+ <tbody>${recentRows}</tbody>
280
+ </table>` : '<p style="color:#888;font-size:13px">No tasks in the last 7 days.</p>'}
281
+ </div>
282
+
283
+ <div class="generated-at">Generated by <code>when velocity dashboard</code></div>
284
+ </body>
285
+ </html>`;
286
+ }
287
+ async function generateDashboard() {
288
+ const db = initDb();
289
+ const queries = new TaskQueries(db);
290
+ const now = (/* @__PURE__ */ new Date()).toISOString();
291
+ const yearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3).toISOString();
292
+ const tasks = queries.getCompletedInRange(yearAgo, now);
293
+ db.close();
294
+ const html = generateHtml(tasks);
295
+ mkdirSync(DASH_DIR, { recursive: true });
296
+ writeFileSync(DASH_PATH, html, "utf8");
297
+ const catStats = buildCategoryStats(tasks);
298
+ const totalSeconds = tasks.reduce((sum, t) => sum + (t.duration_seconds ?? 0), 0);
299
+ const topCats = catStats.slice(0, 3).map((c) => `${c.category}(${c.count})`).join(", ");
300
+ const summary = [
301
+ `Total tasks: ${tasks.length}`,
302
+ `Total time: ${fmtDuration(totalSeconds)}`,
303
+ `Top categories: ${topCats || "none yet"}`,
304
+ `Dashboard written to: ${DASH_PATH}`
305
+ ].join("\n");
306
+ return { path: DASH_PATH, summary };
307
+ }
308
+ function openFile(filePath) {
309
+ const platform = process.platform;
310
+ try {
311
+ if (platform === "darwin") {
312
+ execSync(`open "${filePath}"`, { stdio: "ignore" });
313
+ } else if (platform === "linux") {
314
+ execSync(`xdg-open "${filePath}"`, { stdio: "ignore" });
315
+ }
316
+ } catch {
317
+ }
318
+ }
319
+ function registerVelocityDashboard(server) {
320
+ server.tool(
321
+ "velocity_dashboard",
322
+ "Generate an HTML dashboard with charts showing task timing stats \u2014 opens in browser",
323
+ {},
324
+ async () => {
325
+ const { path, summary } = await generateDashboard();
326
+ openFile(path);
327
+ const text = `Dashboard generated and opened in browser.
328
+
329
+ ${summary}`;
330
+ return { content: [{ type: "text", text }] };
331
+ }
332
+ );
333
+ }
334
+
335
+ // src/utils/find-bin.ts
336
+ import { resolve, dirname } from "path";
337
+ import { existsSync } from "fs";
338
+ import { fileURLToPath } from "url";
339
+ var __dirname = dirname(fileURLToPath(import.meta.url));
340
+ function findBin(name) {
341
+ const pkgRoot = resolve(__dirname, "../..");
342
+ const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
343
+ if (existsSync(localBin)) return localBin;
344
+ const directCli = resolve(pkgRoot, "node_modules", "@whenlabs", name, "dist", "cli.js");
345
+ if (existsSync(directCli)) return directCli;
346
+ return name;
347
+ }
348
+
349
+ // src/config/whenlabs-config.ts
350
+ import { existsSync as existsSync2, readFileSync } from "fs";
351
+ import { resolve as resolve2 } from "path";
352
+ import { parse } from "yaml";
353
+ var CONFIG_FILENAME = ".whenlabs.yml";
354
+ function loadConfig(projectPath) {
355
+ const dir = projectPath ?? process.cwd();
356
+ const configPath = resolve2(dir, CONFIG_FILENAME);
357
+ if (!existsSync2(configPath)) return null;
358
+ try {
359
+ const raw = readFileSync(configPath, "utf-8");
360
+ const parsed = parse(raw);
361
+ if (!parsed || typeof parsed !== "object") return null;
362
+ return parsed;
363
+ } catch {
364
+ return null;
365
+ }
366
+ }
367
+
368
+ // src/mcp/run-cli.ts
369
+ import { spawn } from "child_process";
370
+ import { join as join2 } from "path";
371
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
372
+ import { homedir as homedir2 } from "os";
373
+ function runCli(bin, args, cwd) {
374
+ return new Promise((res) => {
375
+ const child = spawn(findBin(bin), args, {
376
+ cwd: cwd || process.cwd(),
377
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
378
+ });
379
+ let stdout = "";
380
+ let stderr = "";
381
+ child.stdout.on("data", (d) => stdout += d);
382
+ child.stderr.on("data", (d) => stderr += d);
383
+ child.on("error", (err) => res({ stdout, stderr: err.message, code: 1 }));
384
+ child.on("close", (code) => res({ stdout, stderr, code: code ?? 0 }));
385
+ });
386
+ }
387
+ var CACHE_DIR = join2(homedir2(), ".whenlabs", "cache");
388
+ function writeCache(tool, project, output, code) {
389
+ try {
390
+ mkdirSync2(CACHE_DIR, { recursive: true });
391
+ const file = join2(CACHE_DIR, `${tool}_${project}.json`);
392
+ writeFileSync2(file, JSON.stringify({ timestamp: Date.now(), output, code }));
393
+ } catch {
394
+ }
395
+ }
396
+ function deriveProject(path) {
397
+ const dir = path || process.cwd();
398
+ return dir.replace(/\\/g, "/").split("/").filter(Boolean).pop() || "unknown";
399
+ }
400
+ function readAwareProjectName(path) {
401
+ try {
402
+ const awareFile = join2(path || process.cwd(), ".aware.json");
403
+ if (!existsSync3(awareFile)) return null;
404
+ const data = JSON.parse(readFileSync2(awareFile, "utf8"));
405
+ return data.name || data.project || null;
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ function formatOutput(result) {
411
+ const parts = [];
412
+ if (result.stdout.trim()) parts.push(result.stdout.trim());
413
+ if (result.stderr.trim()) parts.push(result.stderr.trim());
414
+ return parts.join("\n") || "No output";
415
+ }
416
+ async function checkTriggers(toolName, result, path) {
417
+ const output = result.stdout || result.stderr || "";
418
+ const extras = [];
419
+ if (toolName === "aware_init") {
420
+ const madeChanges = /wrote|created|updated|generated/i.test(output);
421
+ if (madeChanges) {
422
+ const staleResult = await runCli("stale", ["scan"], path);
423
+ const staleOutput = staleResult.stdout || staleResult.stderr || "";
424
+ writeCache("stale", deriveProject(path), staleOutput, staleResult.code);
425
+ if (staleOutput.trim()) {
426
+ extras.push(`
427
+ --- Auto-triggered stale_scan (stack change detected) ---
428
+ ${staleOutput}`);
429
+ }
430
+ }
431
+ }
432
+ if (toolName === "vow_scan") {
433
+ const hasUnknown = /unknown|UNKNOWN|unlicensed/i.test(output);
434
+ if (hasUnknown) {
435
+ extras.push("\nNote: Unknown licenses detected \u2014 check README for license accuracy claims.");
436
+ }
437
+ }
438
+ if (toolName === "berth_check") {
439
+ const hasConflicts = /conflict|in use|occupied|taken/i.test(output);
440
+ if (hasConflicts) {
441
+ const projectName = readAwareProjectName(path);
442
+ if (projectName) {
443
+ extras.push(`
444
+ Note: Conflicts found in project "${projectName}".`);
445
+ }
446
+ try {
447
+ const cacheFiles = readdirSync(CACHE_DIR).filter((f) => f.startsWith("stale_"));
448
+ for (const cacheFile of cacheFiles) {
449
+ const cached = JSON.parse(readFileSync2(join2(CACHE_DIR, cacheFile), "utf8"));
450
+ if (/\b\d{4,5}\b/.test(cached.output || "")) {
451
+ extras.push("\nTip: Port references found in documentation \u2014 stale_scan may need re-run after resolving conflicts.");
452
+ break;
453
+ }
454
+ }
455
+ } catch {
456
+ }
457
+ }
458
+ }
459
+ if (toolName === "envalid_detect") {
460
+ const serviceUrlMatches = output.match(/\b[A-Z_]*(?:HOST|PORT|URL|URI)[A-Z_]*\b/g);
461
+ if (serviceUrlMatches && serviceUrlMatches.length > 0) {
462
+ const examples = [...new Set(serviceUrlMatches)].slice(0, 3).join(", ");
463
+ extras.push(`
464
+ Tip: Service URLs detected (${examples}, etc.) \u2014 run berth_register to track their ports for conflict detection.`);
465
+ }
466
+ }
467
+ if (toolName === "velocity_end_task") {
468
+ const largeChange = /actual_files["\s:]+([1-9]\d)/i.test(output) || /\b([6-9]|\d{2,})\s+files?\b/i.test(output);
469
+ if (largeChange) {
470
+ extras.push("\nTip: Large change detected \u2014 consider running stale_scan to check for documentation drift.");
471
+ }
472
+ }
473
+ if (toolName === "vow_scan") {
474
+ const cacheFile = join2(CACHE_DIR, `vow_scan_${deriveProject(path)}.json`);
475
+ const isFirstScan = !existsSync3(cacheFile);
476
+ const hasNewPackages = /new package|added|installed/i.test(output);
477
+ if (isFirstScan || hasNewPackages) {
478
+ extras.push("\nTip: Dependency changes detected \u2014 run aware_sync to update AI context files with new library info.");
479
+ }
480
+ }
481
+ return extras;
482
+ }
483
+
484
+ export {
485
+ findBin,
486
+ CONFIG_FILENAME,
487
+ loadConfig,
488
+ runCli,
489
+ CACHE_DIR,
490
+ writeCache,
491
+ deriveProject,
492
+ formatOutput,
493
+ checkTriggers,
494
+ generateDashboard,
495
+ registerVelocityDashboard
496
+ };
package/dist/index.js CHANGED
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CACHE_DIR,
3
4
  CONFIG_FILENAME,
5
+ deriveProject,
4
6
  findBin,
5
- loadConfig
6
- } from "./chunk-JOMP6AU5.js";
7
+ generateDashboard,
8
+ loadConfig,
9
+ runCli,
10
+ writeCache
11
+ } from "./chunk-JI5NCJQ2.js";
7
12
  import {
8
13
  getStatusPath
9
14
  } from "./chunk-4ZVSCJCJ.js";
10
15
 
11
16
  // src/index.ts
12
- import { Command as Command6 } from "commander";
17
+ import { Command as Command10 } from "commander";
13
18
 
14
19
  // src/commands/delegate.ts
15
20
  import { Command } from "commander";
@@ -684,7 +689,7 @@ function writeStatus(results) {
684
689
  writeFileSync2(getStatusPath(), JSON.stringify(status, null, 2) + "\n");
685
690
  }
686
691
  function sleep(ms) {
687
- return new Promise((resolve5) => setTimeout(resolve5, ms));
692
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
688
693
  }
689
694
  function createWatchCommand() {
690
695
  const cmd = new Command4("watch");
@@ -928,13 +933,341 @@ function createConfigCommand() {
928
933
  return cmd;
929
934
  }
930
935
 
931
- // src/index.ts
936
+ // src/commands/upgrade.ts
937
+ import { Command as Command6 } from "commander";
938
+ import { execSync } from "child_process";
932
939
  import { readFileSync as readFileSync3 } from "fs";
933
940
  import { resolve as resolve4, dirname as dirname3 } from "path";
934
941
  import { fileURLToPath as fileURLToPath3 } from "url";
935
942
  var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
936
- var { version } = JSON.parse(readFileSync3(resolve4(__dirname3, "..", "package.json"), "utf8"));
937
- var program = new Command6();
943
+ var c4 = {
944
+ reset: "\x1B[0m",
945
+ bold: "\x1B[1m",
946
+ green: "\x1B[32m",
947
+ yellow: "\x1B[33m",
948
+ red: "\x1B[31m",
949
+ cyan: "\x1B[36m",
950
+ dim: "\x1B[2m"
951
+ };
952
+ function colorize4(text, ...codes) {
953
+ return codes.join("") + text + c4.reset;
954
+ }
955
+ function parseVersion(v) {
956
+ return v.trim().split(".").map(Number);
957
+ }
958
+ function versionGte(a, b) {
959
+ const pa = parseVersion(a);
960
+ const pb = parseVersion(b);
961
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
962
+ const na = pa[i] ?? 0;
963
+ const nb = pb[i] ?? 0;
964
+ if (na > nb) return true;
965
+ if (na < nb) return false;
966
+ }
967
+ return true;
968
+ }
969
+ function createUpgradeCommand() {
970
+ const cmd = new Command6("upgrade");
971
+ cmd.description("Upgrade @whenlabs/when to the latest version");
972
+ cmd.action(async () => {
973
+ console.log("");
974
+ console.log(colorize4(" when upgrade", c4.bold, c4.cyan));
975
+ console.log(colorize4(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c4.dim));
976
+ const pkgPath = resolve4(__dirname3, "..", "..", "package.json");
977
+ let current;
978
+ try {
979
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
980
+ current = pkg.version;
981
+ } catch {
982
+ console.log(` ${colorize4("!", c4.red)} Could not read current version`);
983
+ console.log("");
984
+ process.exitCode = 1;
985
+ return;
986
+ }
987
+ console.log(` ${colorize4("current", c4.dim)} ${colorize4(current, c4.bold)}`);
988
+ let latest;
989
+ try {
990
+ latest = execSync("npm view @whenlabs/when version", { encoding: "utf-8" }).trim();
991
+ } catch {
992
+ console.log(` ${colorize4("!", c4.yellow)} Could not reach npm registry \u2014 check your network connection`);
993
+ console.log("");
994
+ process.exitCode = 1;
995
+ return;
996
+ }
997
+ console.log(` ${colorize4("latest", c4.dim)} ${colorize4(latest, c4.bold)}`);
998
+ console.log("");
999
+ if (versionGte(current, latest)) {
1000
+ console.log(` ${colorize4("\u2713", c4.green)} Already up to date`);
1001
+ console.log("");
1002
+ return;
1003
+ }
1004
+ console.log(` ${colorize4("\u2191", c4.yellow)} Upgrade available: ${colorize4(current, c4.dim)} \u2192 ${colorize4(latest, c4.green + c4.bold)}`);
1005
+ console.log(` ${colorize4("\u2022", c4.dim)} Running: ${colorize4("npm install -g @whenlabs/when@latest", c4.bold)}`);
1006
+ console.log("");
1007
+ try {
1008
+ execSync("npm install -g @whenlabs/when@latest", { stdio: "inherit" });
1009
+ console.log("");
1010
+ console.log(` ${colorize4("\u2713", c4.green)} Upgraded to ${colorize4(latest, c4.bold)}`);
1011
+ } catch {
1012
+ console.log(` ${colorize4("\u2717", c4.red)} Install failed \u2014 try running with sudo or check npm permissions`);
1013
+ process.exitCode = 1;
1014
+ }
1015
+ console.log("");
1016
+ });
1017
+ return cmd;
1018
+ }
1019
+
1020
+ // src/commands/eject.ts
1021
+ import { Command as Command7 } from "commander";
1022
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4, copyFileSync } from "fs";
1023
+ import { resolve as resolve5 } from "path";
1024
+ import { stringify as stringify3 } from "yaml";
1025
+ var c5 = {
1026
+ reset: "\x1B[0m",
1027
+ bold: "\x1B[1m",
1028
+ green: "\x1B[32m",
1029
+ yellow: "\x1B[33m",
1030
+ red: "\x1B[31m",
1031
+ cyan: "\x1B[36m",
1032
+ dim: "\x1B[2m"
1033
+ };
1034
+ function colorize5(text, ...codes) {
1035
+ return codes.join("") + text + c5.reset;
1036
+ }
1037
+ function createEjectCommand() {
1038
+ const cmd = new Command7("eject");
1039
+ cmd.description("Write each tool section of .whenlabs.yml back to its native config format");
1040
+ cmd.option("--force", "Overwrite existing files without prompting");
1041
+ cmd.action((options) => {
1042
+ const cwd = process.cwd();
1043
+ console.log("");
1044
+ console.log(colorize5(" when eject", c5.bold, c5.cyan));
1045
+ console.log(colorize5(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c5.dim));
1046
+ const config = loadConfig(cwd);
1047
+ if (!config) {
1048
+ console.log(` ${colorize5("!", c5.yellow)} No ${colorize5(".whenlabs.yml", c5.bold)} found \u2014 nothing to eject`);
1049
+ console.log(` ${colorize5("\u2022", c5.dim)} Run ${colorize5("when config init", c5.bold)} to generate one first`);
1050
+ console.log("");
1051
+ return;
1052
+ }
1053
+ let ejected = 0;
1054
+ let skipped = 0;
1055
+ if (config.stale && Object.keys(config.stale).length > 0) {
1056
+ const dest = resolve5(cwd, ".stale.yml");
1057
+ if (existsSync4(dest) && !options.force) {
1058
+ console.log(` ${colorize5("!", c5.yellow)} ${colorize5(".stale.yml", c5.bold)} already exists \u2014 use ${colorize5("--force", c5.bold)} to overwrite`);
1059
+ skipped++;
1060
+ } else {
1061
+ const yaml = stringify3(config.stale, { lineWidth: 0 });
1062
+ writeFileSync4(dest, yaml, "utf-8");
1063
+ console.log(` ${colorize5("+", c5.green)} Wrote ${colorize5(".stale.yml", c5.bold)}`);
1064
+ ejected++;
1065
+ }
1066
+ } else if (config.stale !== void 0) {
1067
+ console.log(` ${colorize5("-", c5.dim)} stale: empty config \u2014 skipping .stale.yml`);
1068
+ }
1069
+ if (config.vow && Object.keys(config.vow).length > 0) {
1070
+ const dest = resolve5(cwd, ".vow.json");
1071
+ if (existsSync4(dest) && !options.force) {
1072
+ console.log(` ${colorize5("!", c5.yellow)} ${colorize5(".vow.json", c5.bold)} already exists \u2014 use ${colorize5("--force", c5.bold)} to overwrite`);
1073
+ skipped++;
1074
+ } else {
1075
+ writeFileSync4(dest, JSON.stringify(config.vow, null, 2) + "\n", "utf-8");
1076
+ console.log(` ${colorize5("+", c5.green)} Wrote ${colorize5(".vow.json", c5.bold)}`);
1077
+ ejected++;
1078
+ }
1079
+ } else if (config.vow !== void 0) {
1080
+ console.log(` ${colorize5("-", c5.dim)} vow: empty config \u2014 skipping .vow.json`);
1081
+ }
1082
+ if (config.envalid?.schema) {
1083
+ const src = resolve5(cwd, config.envalid.schema);
1084
+ const dest = resolve5(cwd, ".env.schema");
1085
+ const isSamePath = resolve5(src) === resolve5(dest);
1086
+ if (isSamePath) {
1087
+ console.log(` ${colorize5("-", c5.dim)} envalid.schema already points to ${colorize5(".env.schema", c5.bold)}`);
1088
+ } else if (!existsSync4(src)) {
1089
+ console.log(` ${colorize5("!", c5.yellow)} envalid.schema source ${colorize5(config.envalid.schema, c5.bold)} not found \u2014 skipping`);
1090
+ skipped++;
1091
+ } else if (existsSync4(dest) && !options.force) {
1092
+ console.log(` ${colorize5("!", c5.yellow)} ${colorize5(".env.schema", c5.bold)} already exists \u2014 use ${colorize5("--force", c5.bold)} to overwrite`);
1093
+ skipped++;
1094
+ } else {
1095
+ copyFileSync(src, dest);
1096
+ console.log(` ${colorize5("+", c5.green)} Copied ${colorize5(config.envalid.schema, c5.bold)} \u2192 ${colorize5(".env.schema", c5.bold)}`);
1097
+ ejected++;
1098
+ }
1099
+ }
1100
+ if (config.berth !== void 0) {
1101
+ const portCount = config.berth.ports ? Object.keys(config.berth.ports).length : 0;
1102
+ if (portCount > 0) {
1103
+ console.log(` ${colorize5("\u2022", c5.dim)} berth: ${portCount} port(s) configured \u2014 berth has no standalone config file, managed via ${colorize5(".whenlabs.yml", c5.bold)}`);
1104
+ } else {
1105
+ console.log(` ${colorize5("-", c5.dim)} berth: no standalone config file`);
1106
+ }
1107
+ }
1108
+ console.log("");
1109
+ if (ejected > 0) {
1110
+ console.log(` ${colorize5("\u2713", c5.green)} Ejected ${colorize5(String(ejected), c5.bold)} file(s)`);
1111
+ }
1112
+ if (skipped > 0) {
1113
+ console.log(` ${colorize5("!", c5.yellow)} Skipped ${colorize5(String(skipped), c5.bold)} file(s) \u2014 run with ${colorize5("--force", c5.bold)} to overwrite`);
1114
+ }
1115
+ if (ejected === 0 && skipped === 0) {
1116
+ console.log(` ${colorize5("-", c5.dim)} Nothing to eject`);
1117
+ }
1118
+ console.log("");
1119
+ });
1120
+ return cmd;
1121
+ }
1122
+
1123
+ // src/commands/diff.ts
1124
+ import { Command as Command8 } from "commander";
1125
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1126
+ import { join as join2 } from "path";
1127
+ var c6 = {
1128
+ reset: "\x1B[0m",
1129
+ bold: "\x1B[1m",
1130
+ green: "\x1B[32m",
1131
+ yellow: "\x1B[33m",
1132
+ red: "\x1B[31m",
1133
+ cyan: "\x1B[36m",
1134
+ dim: "\x1B[2m"
1135
+ };
1136
+ function colorize6(text, ...codes) {
1137
+ return codes.join("") + text + c6.reset;
1138
+ }
1139
+ function readCache(tool, project) {
1140
+ const file = join2(CACHE_DIR, `${tool}_${project}.json`);
1141
+ if (!existsSync5(file)) return null;
1142
+ try {
1143
+ return JSON.parse(readFileSync5(file, "utf-8"));
1144
+ } catch {
1145
+ return null;
1146
+ }
1147
+ }
1148
+ function diffLines(oldOutput, newOutput) {
1149
+ const oldLines = new Set(oldOutput.split("\n").map((l) => l.trim()).filter(Boolean));
1150
+ const newLines = new Set(newOutput.split("\n").map((l) => l.trim()).filter(Boolean));
1151
+ const added = [];
1152
+ const removed = [];
1153
+ const unchanged = [];
1154
+ for (const line of newLines) {
1155
+ if (oldLines.has(line)) {
1156
+ unchanged.push(line);
1157
+ } else {
1158
+ added.push(line);
1159
+ }
1160
+ }
1161
+ for (const line of oldLines) {
1162
+ if (!newLines.has(line)) {
1163
+ removed.push(line);
1164
+ }
1165
+ }
1166
+ return { added, removed, unchanged };
1167
+ }
1168
+ var TOOLS = [
1169
+ { bin: "stale", args: ["scan"], label: "stale" },
1170
+ { bin: "envalid", args: ["validate"], label: "envalid" },
1171
+ { bin: "berth", args: ["status"], label: "berth" },
1172
+ { bin: "vow", args: ["scan"], label: "vow" },
1173
+ { bin: "aware", args: ["doctor"], label: "aware" }
1174
+ ];
1175
+ function createDiffCommand() {
1176
+ const cmd = new Command8("diff");
1177
+ cmd.description("Compare cached tool results to fresh runs and show what changed");
1178
+ cmd.action(async () => {
1179
+ const cwd = process.cwd();
1180
+ const project = deriveProject(cwd);
1181
+ console.log("");
1182
+ console.log(colorize6(" when diff", c6.bold, c6.cyan));
1183
+ console.log(colorize6(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c6.dim));
1184
+ console.log(` ${colorize6("project", c6.dim)} ${colorize6(project, c6.bold)}`);
1185
+ console.log("");
1186
+ let anyChanges = false;
1187
+ for (const tool of TOOLS) {
1188
+ const cached = readCache(tool.label, project);
1189
+ const fresh = await runCli(tool.bin, tool.args, cwd);
1190
+ const freshOutput = fresh.stdout.trim() || fresh.stderr.trim() || "";
1191
+ if (!cached) {
1192
+ console.log(` ${colorize6(tool.label, c6.bold, c6.cyan)}`);
1193
+ if (freshOutput) {
1194
+ for (const line of freshOutput.split("\n").slice(0, 5)) {
1195
+ if (line.trim()) console.log(` ${colorize6(line, c6.dim)}`);
1196
+ }
1197
+ const total = freshOutput.split("\n").filter(Boolean).length;
1198
+ if (total > 5) console.log(` ${colorize6(`\u2026 ${total - 5} more lines`, c6.dim)}`);
1199
+ } else {
1200
+ console.log(` ${colorize6("no output", c6.dim)}`);
1201
+ }
1202
+ console.log(` ${colorize6("(no prior cache \u2014 this is now the baseline)", c6.dim)}`);
1203
+ } else {
1204
+ const oldOutput = cached.output.trim();
1205
+ const { added, removed, unchanged } = diffLines(oldOutput, freshOutput);
1206
+ const hasChanges = added.length > 0 || removed.length > 0;
1207
+ if (hasChanges) anyChanges = true;
1208
+ console.log(` ${colorize6(tool.label, c6.bold, c6.cyan)}`);
1209
+ if (!hasChanges) {
1210
+ console.log(` ${colorize6("\u2713", c6.dim)} ${colorize6("no changes", c6.dim)} ${colorize6(`(${unchanged.length} line(s))`, c6.dim)}`);
1211
+ } else {
1212
+ for (const line of added) {
1213
+ console.log(` ${colorize6("+", c6.green)} ${colorize6(line, c6.green)}`);
1214
+ }
1215
+ for (const line of removed) {
1216
+ console.log(` ${colorize6("-", c6.red)} ${colorize6(line, c6.red)}`);
1217
+ }
1218
+ if (unchanged.length > 0) {
1219
+ console.log(` ${colorize6("\xB7", c6.dim)} ${colorize6(`${unchanged.length} line(s) unchanged`, c6.dim)}`);
1220
+ }
1221
+ }
1222
+ }
1223
+ writeCache(tool.label, project, freshOutput, fresh.code);
1224
+ console.log("");
1225
+ }
1226
+ if (!anyChanges) {
1227
+ console.log(` ${colorize6("\u2713", c6.green)} All tools unchanged since last run`);
1228
+ } else {
1229
+ console.log(` ${colorize6("\u2022", c6.dim)} Cache updated with latest results`);
1230
+ }
1231
+ console.log("");
1232
+ });
1233
+ return cmd;
1234
+ }
1235
+
1236
+ // src/commands/dashboard.ts
1237
+ import { Command as Command9 } from "commander";
1238
+ import { execSync as execSync2 } from "child_process";
1239
+ function createDashboardCommand() {
1240
+ const cmd = new Command9("dashboard");
1241
+ cmd.description("Generate an HTML velocity dashboard and open it in the browser");
1242
+ cmd.option("--no-open", "Write the HTML file without opening the browser");
1243
+ cmd.action(async (options) => {
1244
+ const { path, summary } = await generateDashboard();
1245
+ console.log(summary);
1246
+ if (options.open !== false) {
1247
+ const platform = process.platform;
1248
+ try {
1249
+ if (platform === "darwin") {
1250
+ execSync2(`open "${path}"`, { stdio: "ignore" });
1251
+ } else if (platform === "linux") {
1252
+ execSync2(`xdg-open "${path}"`, { stdio: "ignore" });
1253
+ } else {
1254
+ console.log(`Dashboard written to: ${path}`);
1255
+ }
1256
+ } catch {
1257
+ console.log(`Dashboard written to: ${path}`);
1258
+ }
1259
+ }
1260
+ });
1261
+ return cmd;
1262
+ }
1263
+
1264
+ // src/index.ts
1265
+ import { readFileSync as readFileSync6 } from "fs";
1266
+ import { resolve as resolve6, dirname as dirname4 } from "path";
1267
+ import { fileURLToPath as fileURLToPath4 } from "url";
1268
+ var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
1269
+ var { version } = JSON.parse(readFileSync6(resolve6(__dirname4, "..", "package.json"), "utf8"));
1270
+ var program = new Command10();
938
1271
  program.name("when").version(version).description("The WhenLabs developer toolkit \u2014 6 tools, one install");
939
1272
  program.command("install").description("Install all WhenLabs tools globally (MCP server + CLAUDE.md instructions)").option("--cursor", "Install MCP servers into Cursor (~/.cursor/mcp.json)").option("--vscode", "Install MCP servers into VS Code (settings.json)").option("--windsurf", "Install MCP servers into Windsurf (~/.codeium/windsurf/mcp_config.json)").option("--all", "Install MCP servers into all supported editors").action(async (options) => {
940
1273
  const { install } = await import("./install-33GE3HKA.js");
@@ -956,10 +1289,15 @@ program.addCommand(createInitCommand());
956
1289
  program.addCommand(createDoctorCommand());
957
1290
  program.addCommand(createWatchCommand());
958
1291
  program.addCommand(createConfigCommand());
1292
+ program.addCommand(createUpgradeCommand());
1293
+ program.addCommand(createEjectCommand());
1294
+ program.addCommand(createDiffCommand());
959
1295
  program.addCommand(createDelegateCommand("stale", "Detect documentation drift in your codebase"));
960
1296
  program.addCommand(createDelegateCommand("envalid", "Validate .env files against a type-safe schema"));
961
1297
  program.addCommand(createDelegateCommand("berth", "Detect and resolve port conflicts"));
962
1298
  program.addCommand(createDelegateCommand("aware", "Auto-detect your stack and generate AI context files"));
963
1299
  program.addCommand(createDelegateCommand("vow", "Scan dependency licenses and validate against policies"));
964
- program.addCommand(createDelegateCommand("velocity", "velocity-mcp task timing server", "velocity-mcp"));
1300
+ var velocityCmd = createDelegateCommand("velocity", "velocity-mcp task timing server", "velocity-mcp");
1301
+ velocityCmd.addCommand(createDashboardCommand());
1302
+ program.addCommand(velocityCmd);
965
1303
  program.parse();
package/dist/mcp.js CHANGED
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- findBin
4
- } from "./chunk-JOMP6AU5.js";
3
+ checkTriggers,
4
+ deriveProject,
5
+ formatOutput,
6
+ registerVelocityDashboard,
7
+ runCli,
8
+ writeCache
9
+ } from "./chunk-JI5NCJQ2.js";
5
10
 
6
11
  // src/mcp/index.ts
7
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
- import { readFileSync as readFileSync2 } from "fs";
14
+ import { readFileSync } from "fs";
10
15
  import { resolve, dirname } from "path";
11
16
  import { fileURLToPath } from "url";
12
17
  import {
@@ -21,124 +26,6 @@ import {
21
26
 
22
27
  // src/mcp/stale.ts
23
28
  import { z } from "zod";
24
-
25
- // src/mcp/run-cli.ts
26
- import { spawn } from "child_process";
27
- import { join } from "path";
28
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
29
- import { homedir } from "os";
30
- function runCli(bin, args, cwd) {
31
- return new Promise((res) => {
32
- const child = spawn(findBin(bin), args, {
33
- cwd: cwd || process.cwd(),
34
- env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
35
- });
36
- let stdout = "";
37
- let stderr = "";
38
- child.stdout.on("data", (d) => stdout += d);
39
- child.stderr.on("data", (d) => stderr += d);
40
- child.on("error", (err) => res({ stdout, stderr: err.message, code: 1 }));
41
- child.on("close", (code) => res({ stdout, stderr, code: code ?? 0 }));
42
- });
43
- }
44
- var CACHE_DIR = join(homedir(), ".whenlabs", "cache");
45
- function writeCache(tool, project, output, code) {
46
- try {
47
- mkdirSync(CACHE_DIR, { recursive: true });
48
- const file = join(CACHE_DIR, `${tool}_${project}.json`);
49
- writeFileSync(file, JSON.stringify({ timestamp: Date.now(), output, code }));
50
- } catch {
51
- }
52
- }
53
- function deriveProject(path) {
54
- const dir = path || process.cwd();
55
- return dir.replace(/\\/g, "/").split("/").filter(Boolean).pop() || "unknown";
56
- }
57
- function readAwareProjectName(path) {
58
- try {
59
- const awareFile = join(path || process.cwd(), ".aware.json");
60
- if (!existsSync(awareFile)) return null;
61
- const data = JSON.parse(readFileSync(awareFile, "utf8"));
62
- return data.name || data.project || null;
63
- } catch {
64
- return null;
65
- }
66
- }
67
- function formatOutput(result) {
68
- const parts = [];
69
- if (result.stdout.trim()) parts.push(result.stdout.trim());
70
- if (result.stderr.trim()) parts.push(result.stderr.trim());
71
- return parts.join("\n") || "No output";
72
- }
73
- async function checkTriggers(toolName, result, path) {
74
- const output = result.stdout || result.stderr || "";
75
- const extras = [];
76
- if (toolName === "aware_init") {
77
- const madeChanges = /wrote|created|updated|generated/i.test(output);
78
- if (madeChanges) {
79
- const staleResult = await runCli("stale", ["scan"], path);
80
- const staleOutput = staleResult.stdout || staleResult.stderr || "";
81
- writeCache("stale", deriveProject(path), staleOutput, staleResult.code);
82
- if (staleOutput.trim()) {
83
- extras.push(`
84
- --- Auto-triggered stale_scan (stack change detected) ---
85
- ${staleOutput}`);
86
- }
87
- }
88
- }
89
- if (toolName === "vow_scan") {
90
- const hasUnknown = /unknown|UNKNOWN|unlicensed/i.test(output);
91
- if (hasUnknown) {
92
- extras.push("\nNote: Unknown licenses detected \u2014 check README for license accuracy claims.");
93
- }
94
- }
95
- if (toolName === "berth_check") {
96
- const hasConflicts = /conflict|in use|occupied|taken/i.test(output);
97
- if (hasConflicts) {
98
- const projectName = readAwareProjectName(path);
99
- if (projectName) {
100
- extras.push(`
101
- Note: Conflicts found in project "${projectName}".`);
102
- }
103
- try {
104
- const cacheFiles = readdirSync(CACHE_DIR).filter((f) => f.startsWith("stale_"));
105
- for (const cacheFile of cacheFiles) {
106
- const cached = JSON.parse(readFileSync(join(CACHE_DIR, cacheFile), "utf8"));
107
- if (/\b\d{4,5}\b/.test(cached.output || "")) {
108
- extras.push("\nTip: Port references found in documentation \u2014 stale_scan may need re-run after resolving conflicts.");
109
- break;
110
- }
111
- }
112
- } catch {
113
- }
114
- }
115
- }
116
- if (toolName === "envalid_detect") {
117
- const serviceUrlMatches = output.match(/\b[A-Z_]*(?:HOST|PORT|URL|URI)[A-Z_]*\b/g);
118
- if (serviceUrlMatches && serviceUrlMatches.length > 0) {
119
- const examples = [...new Set(serviceUrlMatches)].slice(0, 3).join(", ");
120
- extras.push(`
121
- Tip: Service URLs detected (${examples}, etc.) \u2014 run berth_register to track their ports for conflict detection.`);
122
- }
123
- }
124
- if (toolName === "velocity_end_task") {
125
- const largeChange = /actual_files["\s:]+([1-9]\d)/i.test(output) || /\b([6-9]|\d{2,})\s+files?\b/i.test(output);
126
- if (largeChange) {
127
- extras.push("\nTip: Large change detected \u2014 consider running stale_scan to check for documentation drift.");
128
- }
129
- }
130
- if (toolName === "vow_scan") {
131
- const cacheFile = join(CACHE_DIR, `vow_scan_${deriveProject(path)}.json`);
132
- const isFirstScan = !existsSync(cacheFile);
133
- const hasNewPackages = /new package|added|installed/i.test(output);
134
- if (isFirstScan || hasNewPackages) {
135
- extras.push("\nTip: Dependency changes detected \u2014 run aware_sync to update AI context files with new library info.");
136
- }
137
- }
138
- return extras;
139
- }
140
-
141
- // src/mcp/stale.ts
142
29
  function registerStaleTools(server2) {
143
30
  server2.tool(
144
31
  "stale_scan",
@@ -833,7 +720,7 @@ function registerVowTools(server2) {
833
720
 
834
721
  // src/mcp/index.ts
835
722
  var __dirname = dirname(fileURLToPath(import.meta.url));
836
- var { version } = JSON.parse(readFileSync2(resolve(__dirname, "..", "package.json"), "utf8"));
723
+ var { version } = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8"));
837
724
  var server = new McpServer({
838
725
  name: "whenlabs",
839
726
  version
@@ -851,6 +738,7 @@ registerEnvalidTools(server);
851
738
  registerBerthTools(server);
852
739
  registerAwareTools(server);
853
740
  registerVowTools(server);
741
+ registerVelocityDashboard(server);
854
742
  process.on("SIGINT", () => {
855
743
  velocityDb.close();
856
744
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whenlabs/when",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The WhenLabs developer toolkit — 6 tools, one install",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,40 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/utils/find-bin.ts
4
- import { resolve, dirname } from "path";
5
- import { existsSync } from "fs";
6
- import { fileURLToPath } from "url";
7
- var __dirname = dirname(fileURLToPath(import.meta.url));
8
- function findBin(name) {
9
- const pkgRoot = resolve(__dirname, "../..");
10
- const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
11
- if (existsSync(localBin)) return localBin;
12
- const directCli = resolve(pkgRoot, "node_modules", "@whenlabs", name, "dist", "cli.js");
13
- if (existsSync(directCli)) return directCli;
14
- return name;
15
- }
16
-
17
- // src/config/whenlabs-config.ts
18
- import { existsSync as existsSync2, readFileSync } from "fs";
19
- import { resolve as resolve2 } from "path";
20
- import { parse } from "yaml";
21
- var CONFIG_FILENAME = ".whenlabs.yml";
22
- function loadConfig(projectPath) {
23
- const dir = projectPath ?? process.cwd();
24
- const configPath = resolve2(dir, CONFIG_FILENAME);
25
- if (!existsSync2(configPath)) return null;
26
- try {
27
- const raw = readFileSync(configPath, "utf-8");
28
- const parsed = parse(raw);
29
- if (!parsed || typeof parsed !== "object") return null;
30
- return parsed;
31
- } catch {
32
- return null;
33
- }
34
- }
35
-
36
- export {
37
- findBin,
38
- CONFIG_FILENAME,
39
- loadConfig
40
- };