@whenlabs/when 0.8.1 → 0.9.1
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 +19 -6
- package/dist/chunk-4ZVSCJCJ.js +52 -0
- package/dist/index.js +126 -24
- package/dist/install-TFEGFWJ5.js +447 -0
- package/dist/mcp.js +98 -4
- package/dist/{status-LOZGVOA3.js → status-QWAHXHNA.js} +15 -0
- package/package.json +7 -7
- package/dist/install-HPF26YW2.js +0 -124
package/README.md
CHANGED
|
@@ -16,10 +16,9 @@ This is a one-time setup. After install, all six tools are available in every pr
|
|
|
16
16
|
|
|
17
17
|
Running `npx @whenlabs/when install` will:
|
|
18
18
|
|
|
19
|
-
1. Register **
|
|
20
|
-
- `velocity-mcp` — task timing and estimation tools
|
|
21
|
-
- `whenlabs` — stale, envalid, berth, aware, and vow tools
|
|
19
|
+
1. Register a **single MCP server** (`whenlabs`) in your Claude Code configuration — all six tools, including velocity, are served from one server
|
|
22
20
|
2. Inject **CLAUDE.md instructions** so Claude knows when to use each tool automatically — and prefers them over shell commands
|
|
21
|
+
3. Clean up any legacy `velocity-mcp` registrations (velocity is now bundled)
|
|
23
22
|
|
|
24
23
|
Once connected, Claude can call any tool directly without you asking. For example, after a refactor Claude might run `stale_scan` to check for doc drift, or before a release it might run `vow_check` to validate licenses.
|
|
25
24
|
|
|
@@ -46,17 +45,21 @@ These tools are available to Claude in every session after install:
|
|
|
46
45
|
| `velocity_start_task` | Start timing a coding task |
|
|
47
46
|
| `velocity_end_task` | End timing and record results |
|
|
48
47
|
| `velocity_estimate` | Estimate time for a planned task |
|
|
49
|
-
| `velocity_stats` | Show aggregate performance stats |
|
|
48
|
+
| `velocity_stats` | Show aggregate performance stats with insights |
|
|
50
49
|
| `velocity_history` | Show task history |
|
|
51
50
|
| `stale_scan` | Detect documentation drift |
|
|
51
|
+
| `stale_fix` | Auto-fix documentation drift (wrong paths, dead links, phantom env vars) |
|
|
52
52
|
| `envalid_validate` | Validate .env files against schemas |
|
|
53
53
|
| `envalid_detect` | Find undocumented env vars in codebase |
|
|
54
|
+
| `envalid_generate_schema` | Generate .env.schema from code analysis |
|
|
54
55
|
| `berth_status` | Show active ports and conflicts |
|
|
55
56
|
| `berth_check` | Scan project for port conflicts |
|
|
57
|
+
| `berth_resolve` | Auto-resolve port conflicts (kill or reassign) |
|
|
56
58
|
| `aware_init` | Auto-detect stack, generate AI context files |
|
|
57
59
|
| `aware_doctor` | Diagnose project health and config issues |
|
|
58
60
|
| `vow_scan` | Scan and summarize dependency licenses |
|
|
59
61
|
| `vow_check` | Validate licenses against policy |
|
|
62
|
+
| `vow_hook_install` | Install pre-commit license check hook |
|
|
60
63
|
|
|
61
64
|
## Multi-Editor Support
|
|
62
65
|
|
|
@@ -78,12 +81,18 @@ You can also run tools directly from the command line:
|
|
|
78
81
|
```bash
|
|
79
82
|
when init # Onboard a project — detect stack, run all tools
|
|
80
83
|
when stale scan
|
|
84
|
+
when stale fix # Auto-fix documentation drift
|
|
81
85
|
when envalid validate
|
|
86
|
+
when envalid detect --generate # Generate schema from code
|
|
82
87
|
when berth status
|
|
88
|
+
when berth resolve # Auto-resolve port conflicts
|
|
83
89
|
when aware init
|
|
84
90
|
when vow scan
|
|
91
|
+
when vow hook install # Install pre-commit license hook
|
|
85
92
|
when status # Show installation status
|
|
86
93
|
when doctor # Run all tools, show unified health report
|
|
94
|
+
when doctor --watch # Continuous monitoring dashboard
|
|
95
|
+
when watch # Background daemon for status line
|
|
87
96
|
when ci # Run checks for CI (exits 1 on issues)
|
|
88
97
|
```
|
|
89
98
|
|
|
@@ -93,7 +102,11 @@ One command to onboard any project. Auto-detects your stack, runs all 5 tools in
|
|
|
93
102
|
|
|
94
103
|
### `when doctor`
|
|
95
104
|
|
|
96
|
-
Runs all 5 CLI tools against the current project and displays a unified health report card. Supports `--json` for machine-readable output.
|
|
105
|
+
Runs all 5 CLI tools against the current project and displays a unified health report card. Supports `--json` for machine-readable output and `--watch` for continuous monitoring with a live dashboard.
|
|
106
|
+
|
|
107
|
+
### `when watch`
|
|
108
|
+
|
|
109
|
+
Background daemon that runs all 5 tools on intervals and writes results to `~/.whenlabs/status.json`. Powers the Claude Code status line integration. Use `--once` for a single scan or `--interval <seconds>` to customize the schedule.
|
|
97
110
|
|
|
98
111
|
### `when ci`
|
|
99
112
|
|
|
@@ -118,7 +131,7 @@ when ci --json # Machine-readable JSON output
|
|
|
118
131
|
npx @whenlabs/when uninstall
|
|
119
132
|
```
|
|
120
133
|
|
|
121
|
-
Removes
|
|
134
|
+
Removes the MCP server and cleans up CLAUDE.md instructions.
|
|
122
135
|
|
|
123
136
|
## License
|
|
124
137
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/status-provider.ts
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
7
|
+
var STATUS_PATH = join(homedir(), ".whenlabs", "status.json");
|
|
8
|
+
function getStatusPath() {
|
|
9
|
+
return STATUS_PATH;
|
|
10
|
+
}
|
|
11
|
+
function readStatus() {
|
|
12
|
+
if (!existsSync(STATUS_PATH)) return null;
|
|
13
|
+
try {
|
|
14
|
+
const raw = readFileSync(STATUS_PATH, "utf-8");
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function formatStatusLine() {
|
|
21
|
+
const data = readStatus();
|
|
22
|
+
if (!data) return null;
|
|
23
|
+
const tools = data.tools;
|
|
24
|
+
const parts = [];
|
|
25
|
+
for (const [key, info] of Object.entries(tools)) {
|
|
26
|
+
const label = key === "envalid" ? "env" : key === "vow" ? "lic" : key;
|
|
27
|
+
if (info.status === "ok") {
|
|
28
|
+
parts.push(`\u2713${label}`);
|
|
29
|
+
} else if (info.status === "issues") {
|
|
30
|
+
parts.push(`\u2717${label}:${info.count}`);
|
|
31
|
+
} else {
|
|
32
|
+
parts.push(`!${label}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return parts.join(" ");
|
|
36
|
+
}
|
|
37
|
+
function isStale(maxAgeMs = 120 * 60 * 1e3) {
|
|
38
|
+
if (!existsSync(STATUS_PATH)) return true;
|
|
39
|
+
try {
|
|
40
|
+
const stat = statSync(STATUS_PATH);
|
|
41
|
+
return Date.now() - stat.mtimeMs > maxAgeMs;
|
|
42
|
+
} catch {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
getStatusPath,
|
|
49
|
+
readStatus,
|
|
50
|
+
formatStatusLine,
|
|
51
|
+
isStale
|
|
52
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getStatusPath
|
|
4
|
+
} from "./chunk-4ZVSCJCJ.js";
|
|
2
5
|
|
|
3
6
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
7
|
+
import { Command as Command5 } from "commander";
|
|
5
8
|
|
|
6
9
|
// src/commands/delegate.ts
|
|
7
10
|
import { Command } from "commander";
|
|
@@ -47,25 +50,15 @@ function createDelegateCommand(name, description, binName) {
|
|
|
47
50
|
|
|
48
51
|
// src/commands/doctor.ts
|
|
49
52
|
import { Command as Command2 } from "commander";
|
|
53
|
+
|
|
54
|
+
// src/utils/tool-runner.ts
|
|
50
55
|
import { spawn as spawn2 } from "child_process";
|
|
51
56
|
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
52
57
|
import { existsSync as existsSync2 } from "fs";
|
|
53
58
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
54
59
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
55
|
-
var c = {
|
|
56
|
-
reset: "\x1B[0m",
|
|
57
|
-
bold: "\x1B[1m",
|
|
58
|
-
green: "\x1B[32m",
|
|
59
|
-
yellow: "\x1B[33m",
|
|
60
|
-
red: "\x1B[31m",
|
|
61
|
-
cyan: "\x1B[36m",
|
|
62
|
-
dim: "\x1B[2m"
|
|
63
|
-
};
|
|
64
|
-
function colorize(text, ...codes) {
|
|
65
|
-
return codes.join("") + text + c.reset;
|
|
66
|
-
}
|
|
67
60
|
function findBin2(name) {
|
|
68
|
-
const pkgRoot = resolve2(__dirname2, "..");
|
|
61
|
+
const pkgRoot = resolve2(__dirname2, "..", "..");
|
|
69
62
|
const localBin = resolve2(pkgRoot, "node_modules", ".bin", name);
|
|
70
63
|
if (existsSync2(localBin)) return localBin;
|
|
71
64
|
return name;
|
|
@@ -215,6 +208,29 @@ async function checkAware() {
|
|
|
215
208
|
exitCode
|
|
216
209
|
};
|
|
217
210
|
}
|
|
211
|
+
async function runAllChecks(cwd) {
|
|
212
|
+
return Promise.all([
|
|
213
|
+
checkStale(cwd),
|
|
214
|
+
checkEnvalid(cwd),
|
|
215
|
+
checkBerth(cwd),
|
|
216
|
+
checkVow(cwd),
|
|
217
|
+
checkAware()
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/commands/doctor.ts
|
|
222
|
+
var c = {
|
|
223
|
+
reset: "\x1B[0m",
|
|
224
|
+
bold: "\x1B[1m",
|
|
225
|
+
green: "\x1B[32m",
|
|
226
|
+
yellow: "\x1B[33m",
|
|
227
|
+
red: "\x1B[31m",
|
|
228
|
+
cyan: "\x1B[36m",
|
|
229
|
+
dim: "\x1B[2m"
|
|
230
|
+
};
|
|
231
|
+
function colorize(text, ...codes) {
|
|
232
|
+
return codes.join("") + text + c.reset;
|
|
233
|
+
}
|
|
218
234
|
function statusIcon(result) {
|
|
219
235
|
switch (result.status) {
|
|
220
236
|
case "ok":
|
|
@@ -257,13 +273,7 @@ function createDoctorCommand() {
|
|
|
257
273
|
if (!options.json) {
|
|
258
274
|
process.stdout.write(colorize(" Running health checks\u2026", c.dim) + "\n");
|
|
259
275
|
}
|
|
260
|
-
const results = await
|
|
261
|
-
checkStale(cwd),
|
|
262
|
-
checkEnvalid(cwd),
|
|
263
|
-
checkBerth(cwd),
|
|
264
|
-
checkVow(cwd),
|
|
265
|
-
checkAware()
|
|
266
|
-
]);
|
|
276
|
+
const results = await runAllChecks(cwd);
|
|
267
277
|
const hasIssues = results.some((r) => r.status === "issues" || r.status === "error");
|
|
268
278
|
if (options.json) {
|
|
269
279
|
const output = {
|
|
@@ -491,11 +501,102 @@ function createInitCommand() {
|
|
|
491
501
|
return cmd;
|
|
492
502
|
}
|
|
493
503
|
|
|
504
|
+
// src/commands/watch.ts
|
|
505
|
+
import { Command as Command4 } from "commander";
|
|
506
|
+
import { join } from "path";
|
|
507
|
+
import { homedir } from "os";
|
|
508
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
509
|
+
var STATUS_DIR = join(homedir(), ".whenlabs");
|
|
510
|
+
function toolResultToStatus(r) {
|
|
511
|
+
const count = r.issues + r.warnings;
|
|
512
|
+
if (r.status === "error") {
|
|
513
|
+
return { status: "error", count, detail: r.detail };
|
|
514
|
+
}
|
|
515
|
+
if (r.status === "issues") {
|
|
516
|
+
return { status: "issues", count, detail: r.detail };
|
|
517
|
+
}
|
|
518
|
+
return { status: "ok", count: 0, detail: r.detail };
|
|
519
|
+
}
|
|
520
|
+
function buildSummary(results) {
|
|
521
|
+
const map = {};
|
|
522
|
+
for (const r of results) map[r.name] = r;
|
|
523
|
+
const stalePart = `stale:${map["stale"]?.issues ?? 0}`;
|
|
524
|
+
const envPart = `env:${(map["envalid"]?.issues ?? 0) + (map["envalid"]?.warnings ?? 0)}`;
|
|
525
|
+
const portsPart = `ports:${map["berth"]?.issues ?? 0}`;
|
|
526
|
+
const licPart = `lic:${(map["vow"]?.issues ?? 0) + (map["vow"]?.warnings ?? 0)}`;
|
|
527
|
+
const awarePart = `aware:${map["aware"]?.status === "ok" || map["aware"]?.status === "skipped" ? "ok" : "stale"}`;
|
|
528
|
+
return `${stalePart} ${envPart} ${portsPart} ${licPart} ${awarePart}`;
|
|
529
|
+
}
|
|
530
|
+
function writeStatus(results) {
|
|
531
|
+
mkdirSync(STATUS_DIR, { recursive: true });
|
|
532
|
+
const toolsMap = {};
|
|
533
|
+
for (const r of results) toolsMap[r.name] = r;
|
|
534
|
+
const status = {
|
|
535
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
536
|
+
tools: {
|
|
537
|
+
stale: toolResultToStatus(toolsMap["stale"]),
|
|
538
|
+
envalid: toolResultToStatus(toolsMap["envalid"]),
|
|
539
|
+
berth: toolResultToStatus(toolsMap["berth"]),
|
|
540
|
+
vow: toolResultToStatus(toolsMap["vow"]),
|
|
541
|
+
aware: toolResultToStatus(toolsMap["aware"])
|
|
542
|
+
},
|
|
543
|
+
summary: buildSummary(results)
|
|
544
|
+
};
|
|
545
|
+
writeFileSync(getStatusPath(), JSON.stringify(status, null, 2) + "\n");
|
|
546
|
+
}
|
|
547
|
+
function sleep(ms) {
|
|
548
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
549
|
+
}
|
|
550
|
+
function createWatchCommand() {
|
|
551
|
+
const cmd = new Command4("watch");
|
|
552
|
+
cmd.description("Run all 5 WhenLabs tools on a schedule and write results to ~/.whenlabs/status.json");
|
|
553
|
+
cmd.option("--once", "Run a single scan and exit");
|
|
554
|
+
cmd.option("--interval <seconds>", "Override the default scan interval (seconds)", "60");
|
|
555
|
+
cmd.action(async (options) => {
|
|
556
|
+
const cwd = process.cwd();
|
|
557
|
+
const intervalSec = Math.max(10, parseInt(options.interval ?? "60", 10));
|
|
558
|
+
let stopped = false;
|
|
559
|
+
const shutdown = () => {
|
|
560
|
+
stopped = true;
|
|
561
|
+
process.stderr.write("\nwatch: shutting down gracefully\n");
|
|
562
|
+
process.exit(0);
|
|
563
|
+
};
|
|
564
|
+
process.on("SIGINT", shutdown);
|
|
565
|
+
process.on("SIGTERM", shutdown);
|
|
566
|
+
const runScan = async () => {
|
|
567
|
+
const start = Date.now();
|
|
568
|
+
process.stderr.write(`watch: scanning... `);
|
|
569
|
+
const results = await runAllChecks(cwd);
|
|
570
|
+
writeStatus(results);
|
|
571
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
572
|
+
const summary = buildSummary(results);
|
|
573
|
+
process.stderr.write(`done in ${elapsed}s [${summary}]
|
|
574
|
+
`);
|
|
575
|
+
const hasIssues = results.some((r) => r.status === "issues" || r.status === "error");
|
|
576
|
+
return hasIssues;
|
|
577
|
+
};
|
|
578
|
+
if (options.once) {
|
|
579
|
+
const hasIssues = await runScan();
|
|
580
|
+
process.exit(hasIssues ? 1 : 0);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
process.stderr.write(`watch: started (interval=${intervalSec}s, status=${getStatusPath()})
|
|
584
|
+
`);
|
|
585
|
+
while (!stopped) {
|
|
586
|
+
await runScan();
|
|
587
|
+
for (let i = 0; i < intervalSec && !stopped; i++) {
|
|
588
|
+
await sleep(1e3);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
return cmd;
|
|
593
|
+
}
|
|
594
|
+
|
|
494
595
|
// src/index.ts
|
|
495
|
-
var program = new
|
|
596
|
+
var program = new Command5();
|
|
496
597
|
program.name("when").version("0.1.0").description("The WhenLabs developer toolkit \u2014 6 tools, one install");
|
|
497
598
|
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) => {
|
|
498
|
-
const { install } = await import("./install-
|
|
599
|
+
const { install } = await import("./install-TFEGFWJ5.js");
|
|
499
600
|
await install(options);
|
|
500
601
|
});
|
|
501
602
|
program.command("uninstall").description("Remove all WhenLabs tools").option("--cursor", "Remove MCP servers from Cursor").option("--vscode", "Remove MCP servers from VS Code").option("--windsurf", "Remove MCP servers from Windsurf").option("--all", "Remove MCP servers from all supported editors").action(async (options) => {
|
|
@@ -503,7 +604,7 @@ program.command("uninstall").description("Remove all WhenLabs tools").option("--
|
|
|
503
604
|
await uninstall(options);
|
|
504
605
|
});
|
|
505
606
|
program.command("status").description("Show installation status and velocity stats").action(async () => {
|
|
506
|
-
const { status } = await import("./status-
|
|
607
|
+
const { status } = await import("./status-QWAHXHNA.js");
|
|
507
608
|
await status();
|
|
508
609
|
});
|
|
509
610
|
program.command("ci").description("Run stale, envalid, and vow checks \u2014 exits 1 if any tool finds issues").option("--ci", "Output GitHub Actions annotations (::error file=X::message)").option("--json", "Machine-readable JSON output").action(async (options) => {
|
|
@@ -512,6 +613,7 @@ program.command("ci").description("Run stale, envalid, and vow checks \u2014 exi
|
|
|
512
613
|
});
|
|
513
614
|
program.addCommand(createInitCommand());
|
|
514
615
|
program.addCommand(createDoctorCommand());
|
|
616
|
+
program.addCommand(createWatchCommand());
|
|
515
617
|
program.addCommand(createDelegateCommand("stale", "Detect documentation drift in your codebase"));
|
|
516
618
|
program.addCommand(createDelegateCommand("envalid", "Validate .env files against a type-safe schema"));
|
|
517
619
|
program.addCommand(createDelegateCommand("berth", "Detect and resolve port conflicts"));
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ALL_EDITORS,
|
|
4
|
+
installForEditor,
|
|
5
|
+
registerMcpServer
|
|
6
|
+
} from "./chunk-3PDLNC63.js";
|
|
7
|
+
import {
|
|
8
|
+
injectBlock
|
|
9
|
+
} from "./chunk-NYUYV3UL.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/install.ts
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
15
|
+
var CLAUDE_MD_PATH = join(homedir(), ".claude", "CLAUDE.md");
|
|
16
|
+
var SCRIPTS_DIR = join(homedir(), ".claude", "scripts");
|
|
17
|
+
var STATUSLINE_PATH = join(SCRIPTS_DIR, "statusline.py");
|
|
18
|
+
var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
19
|
+
var OLD_START_MARKER = "<!-- velocity-mcp:start -->";
|
|
20
|
+
var OLD_END_MARKER = "<!-- velocity-mcp:end -->";
|
|
21
|
+
var CLAUDE_MD_CONTENT = `# WhenLabs Toolkit
|
|
22
|
+
|
|
23
|
+
## Task Timing (velocity-mcp)
|
|
24
|
+
|
|
25
|
+
Velocity tools are part of the unified \`whenlabs\` MCP server. Follow these rules for EVERY session:
|
|
26
|
+
|
|
27
|
+
1. **Before starting any discrete coding task**, call \`velocity_start_task\` with:
|
|
28
|
+
- Appropriate \`category\` (scaffold, implement, refactor, debug, test, config, docs, deploy)
|
|
29
|
+
- Relevant \`tags\` (e.g. typescript, react, sqlite, api)
|
|
30
|
+
- Clear \`description\` of what you're about to do
|
|
31
|
+
- \`estimated_files\` if you know how many files you'll touch
|
|
32
|
+
- \`project\` set to the current project name (auto-detected from git remote or directory name)
|
|
33
|
+
|
|
34
|
+
2. **After completing each task**, call \`velocity_end_task\` with:
|
|
35
|
+
- The \`task_id\` from the start call
|
|
36
|
+
- \`status\`: completed, failed, or abandoned
|
|
37
|
+
- \`actual_files\`: how many files were actually modified
|
|
38
|
+
- \`notes\`: any useful context about what happened
|
|
39
|
+
|
|
40
|
+
3. **When creating a multi-step plan**, call \`velocity_estimate\` to provide the user with a time estimate before starting work.
|
|
41
|
+
|
|
42
|
+
4. **If the user asks about speed or performance**, call \`velocity_stats\` to show aggregate data.
|
|
43
|
+
|
|
44
|
+
### Guidelines
|
|
45
|
+
- Every discrete unit of work should be tracked \u2014 don't batch multiple unrelated changes into one task
|
|
46
|
+
- If a task is abandoned or fails, still call \`velocity_end_task\` with the appropriate status
|
|
47
|
+
- Use consistent tags across sessions so the similarity matching can find comparable historical tasks
|
|
48
|
+
- Keep descriptions concise but specific enough to be useful for future matching
|
|
49
|
+
|
|
50
|
+
## WhenLabs MCP Tools (ALWAYS prefer these over shell commands)
|
|
51
|
+
|
|
52
|
+
All six tools (including velocity) are available through the unified \`whenlabs\` MCP server. **ALWAYS use these MCP tools instead of running shell commands like lsof, grep, or manual checks.** These tools are purpose-built and give better results:
|
|
53
|
+
|
|
54
|
+
| When to use | Call this tool | NOT this |
|
|
55
|
+
|-------------|---------------|----------|
|
|
56
|
+
| Check ports or port conflicts | \`berth_status\` or \`berth_check\` | \`lsof\`, \`netstat\`, \`ss\` |
|
|
57
|
+
| Scan dependency licenses | \`vow_scan\` or \`vow_check\` | manual \`npm ls\`, \`license-checker\` |
|
|
58
|
+
| Check if docs are stale | \`stale_scan\` | manual file comparison |
|
|
59
|
+
| Validate .env files | \`envalid_validate\` or \`envalid_detect\` | manual .env inspection |
|
|
60
|
+
| Generate AI context files | \`aware_init\` or \`aware_doctor\` | manual CLAUDE.md creation |
|
|
61
|
+
|
|
62
|
+
### Tool Reference
|
|
63
|
+
- \`berth_status\` \u2014 Show all active ports, Docker ports, and configured ports
|
|
64
|
+
- \`berth_check\` \u2014 Scan a project directory for port conflicts
|
|
65
|
+
- \`stale_scan\` \u2014 Detect documentation drift in the codebase
|
|
66
|
+
- \`envalid_validate\` \u2014 Validate .env files against their schema
|
|
67
|
+
- \`envalid_detect\` \u2014 Find undocumented env vars in codebase
|
|
68
|
+
- \`aware_init\` \u2014 Auto-detect stack and generate AI context files
|
|
69
|
+
- \`aware_doctor\` \u2014 Diagnose project health and config issues
|
|
70
|
+
- \`vow_scan\` \u2014 Scan and summarize all dependency licenses
|
|
71
|
+
- \`vow_check\` \u2014 Validate licenses against a policy file
|
|
72
|
+
|
|
73
|
+
### Proactive Background Scans
|
|
74
|
+
WhenLabs tools run automatically in the background on a schedule. The status line shows findings:
|
|
75
|
+
- \`stale:N\` \u2014 N docs have drifted from code. Run \`stale_scan\` and fix the drift.
|
|
76
|
+
- \`env:N\` \u2014 N .env issues found. Run \`envalid_validate\` and help the user fix them.
|
|
77
|
+
- \`ports:N\` \u2014 N port conflicts. Run \`berth_status\` and suggest resolution.
|
|
78
|
+
- \`lic:N?\` \u2014 N packages with unknown licenses. Run \`vow_scan\` for details.
|
|
79
|
+
- \`aware:stale\` \u2014 AI context files are outdated. Run \`aware_init\` to regenerate.
|
|
80
|
+
|
|
81
|
+
**When you see any of these in the status line, proactively tell the user and offer to fix the issue.** Do not wait for the user to ask.`;
|
|
82
|
+
var STATUSLINE_SCRIPT = `#!/usr/bin/env python3
|
|
83
|
+
"""WhenLabs status line for Claude Code \u2014 with proactive background tool scans."""
|
|
84
|
+
|
|
85
|
+
import json
|
|
86
|
+
import os
|
|
87
|
+
import subprocess
|
|
88
|
+
import sys
|
|
89
|
+
import time
|
|
90
|
+
from pathlib import Path
|
|
91
|
+
|
|
92
|
+
CACHE_DIR = Path.home() / ".whenlabs" / "cache"
|
|
93
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
# Scan intervals in seconds
|
|
96
|
+
INTERVALS = {
|
|
97
|
+
"berth": 900, # 15 min
|
|
98
|
+
"stale": 1800, # 30 min
|
|
99
|
+
"envalid": 1800, # 30 min
|
|
100
|
+
"vow": 3600, # 60 min
|
|
101
|
+
"aware": 3600, # 60 min
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class C:
|
|
106
|
+
AMBER = "\\033[38;2;196;106;26m"
|
|
107
|
+
BLUE = "\\033[38;2;59;130;246m"
|
|
108
|
+
CYAN = "\\033[38;2;34;211;238m"
|
|
109
|
+
GREEN = "\\033[38;2;34;197;94m"
|
|
110
|
+
RED = "\\033[38;2;239;68;68m"
|
|
111
|
+
YELLOW = "\\033[38;2;234;179;8m"
|
|
112
|
+
GRAY = "\\033[38;2;156;163;175m"
|
|
113
|
+
DIM = "\\033[2m"
|
|
114
|
+
RESET = "\\033[0m"
|
|
115
|
+
SEP = f"\\033[38;2;107;114;128m \\u30fb \\033[0m"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def git_info(cwd):
|
|
119
|
+
try:
|
|
120
|
+
subprocess.run(["git", "rev-parse", "--git-dir"], cwd=cwd, capture_output=True, check=True, timeout=1)
|
|
121
|
+
branch = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, capture_output=True, text=True, timeout=1).stdout.strip()
|
|
122
|
+
status = subprocess.run(["git", "status", "--porcelain"], cwd=cwd, capture_output=True, text=True, timeout=1).stdout
|
|
123
|
+
clean = len([l for l in status.strip().split("\\n") if l]) == 0
|
|
124
|
+
color = C.GREEN if clean else C.YELLOW
|
|
125
|
+
icon = "\\u2713" if clean else "\\u00b1"
|
|
126
|
+
return f"{color}{branch} {icon}{C.RESET}"
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def mcp_servers(data):
|
|
132
|
+
servers = []
|
|
133
|
+
try:
|
|
134
|
+
cfg = Path.home() / ".claude.json"
|
|
135
|
+
if cfg.exists():
|
|
136
|
+
with open(cfg) as f:
|
|
137
|
+
servers.extend(json.load(f).get("mcpServers", {}).keys())
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
cwd = data.get("workspace", {}).get("current_dir", "")
|
|
141
|
+
if cwd:
|
|
142
|
+
for p in [Path(cwd) / ".mcp.json", Path(cwd) / ".claude" / ".mcp.json"]:
|
|
143
|
+
try:
|
|
144
|
+
if p.exists():
|
|
145
|
+
with open(p) as f:
|
|
146
|
+
servers.extend(json.load(f).get("mcpServers", {}).keys())
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
seen = set()
|
|
150
|
+
return [s for s in servers if not (s in seen or seen.add(s))]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def context_pct(data):
|
|
154
|
+
try:
|
|
155
|
+
cw = data["context_window"]
|
|
156
|
+
size = cw["context_window_size"]
|
|
157
|
+
usage = cw.get("current_usage", {})
|
|
158
|
+
tokens = usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + usage.get("cache_read_input_tokens", 0)
|
|
159
|
+
pct = (tokens * 100) // size
|
|
160
|
+
color = C.GREEN if pct < 40 else C.YELLOW if pct < 70 else C.RED
|
|
161
|
+
return f"{C.DIM}{color}{pct}%{C.RESET}"
|
|
162
|
+
except Exception:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cost_info(data):
|
|
167
|
+
try:
|
|
168
|
+
cfg = Path.home() / ".claude.json"
|
|
169
|
+
if cfg.exists():
|
|
170
|
+
with open(cfg) as f:
|
|
171
|
+
if json.load(f).get("oauthAccount", {}).get("accountUuid"):
|
|
172
|
+
return None
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
try:
|
|
176
|
+
cost = data.get("cost", {}).get("total_cost_usd")
|
|
177
|
+
if cost:
|
|
178
|
+
color = C.GREEN if cost < 1 else C.YELLOW if cost < 5 else C.RED
|
|
179
|
+
return f"{color}\${cost:.2f}{C.RESET}"
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# --- Background tool scanning ---
|
|
186
|
+
|
|
187
|
+
def cache_path(tool, cwd):
|
|
188
|
+
project = Path(cwd).name if cwd else "global"
|
|
189
|
+
return CACHE_DIR / f"{tool}_{project}.json"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def should_run(tool, cwd):
|
|
193
|
+
cp = cache_path(tool, cwd)
|
|
194
|
+
if not cp.exists():
|
|
195
|
+
return True
|
|
196
|
+
try:
|
|
197
|
+
cached = json.loads(cp.read_text())
|
|
198
|
+
return (time.time() - cached.get("timestamp", 0)) > INTERVALS[tool]
|
|
199
|
+
except Exception:
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def run_bg(tool, args, cwd):
|
|
204
|
+
cp = cache_path(tool, cwd)
|
|
205
|
+
snippet = (
|
|
206
|
+
"import subprocess, json, time, os; "
|
|
207
|
+
f"args = {args!r}; "
|
|
208
|
+
f"cwd = {cwd!r}; "
|
|
209
|
+
f"out_path = {str(cp)!r}; "
|
|
210
|
+
"env = {**os.environ, 'FORCE_COLOR': '0', 'NO_COLOR': '1'}; "
|
|
211
|
+
"r = subprocess.run(args, cwd=cwd, capture_output=True, text=True, env=env, timeout=60); "
|
|
212
|
+
"cache = {'timestamp': time.time(), 'output': r.stdout + r.stderr, 'code': r.returncode}; "
|
|
213
|
+
"open(out_path, 'w').write(json.dumps(cache))"
|
|
214
|
+
)
|
|
215
|
+
try:
|
|
216
|
+
subprocess.Popen(
|
|
217
|
+
[sys.executable, "-c", snippet],
|
|
218
|
+
stdout=subprocess.DEVNULL,
|
|
219
|
+
stderr=subprocess.DEVNULL,
|
|
220
|
+
start_new_session=True,
|
|
221
|
+
)
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def read_cache(tool, cwd):
|
|
227
|
+
cp = cache_path(tool, cwd)
|
|
228
|
+
if not cp.exists():
|
|
229
|
+
return None
|
|
230
|
+
try:
|
|
231
|
+
return json.loads(cp.read_text())
|
|
232
|
+
except Exception:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def parse_stale(cached):
|
|
237
|
+
if not cached or cached["code"] != 0:
|
|
238
|
+
return None
|
|
239
|
+
out = cached["output"]
|
|
240
|
+
drifted = out.count("\\u2717")
|
|
241
|
+
if drifted > 0:
|
|
242
|
+
return f"{C.RED}stale:{drifted}{C.RESET}"
|
|
243
|
+
if "\\u2713" in out or "No drift" in out.lower() or "clean" in out.lower():
|
|
244
|
+
return f"{C.GREEN}stale:ok{C.RESET}"
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_envalid(cached):
|
|
249
|
+
if not cached:
|
|
250
|
+
return None
|
|
251
|
+
out = cached["output"]
|
|
252
|
+
if cached["code"] != 0:
|
|
253
|
+
errors = sum(1 for line in out.split("\\n") if "\\u2717" in line or "error" in line.lower() or "missing" in line.lower())
|
|
254
|
+
if errors > 0:
|
|
255
|
+
return f"{C.RED}env:{errors}{C.RESET}"
|
|
256
|
+
return f"{C.YELLOW}env:?{C.RESET}"
|
|
257
|
+
return f"{C.GREEN}env:ok{C.RESET}"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def parse_berth(cached):
|
|
261
|
+
if not cached:
|
|
262
|
+
return None
|
|
263
|
+
out = cached["output"]
|
|
264
|
+
conflicts = out.lower().count("conflict")
|
|
265
|
+
if conflicts > 0:
|
|
266
|
+
return f"{C.RED}ports:{conflicts}{C.RESET}"
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def parse_vow(cached):
|
|
271
|
+
if not cached:
|
|
272
|
+
return None
|
|
273
|
+
out = cached["output"]
|
|
274
|
+
unknown = 0
|
|
275
|
+
for line in out.split("\\n"):
|
|
276
|
+
low = line.lower()
|
|
277
|
+
if "unknown" in low or "unlicensed" in low:
|
|
278
|
+
for word in line.split():
|
|
279
|
+
if word.isdigit():
|
|
280
|
+
unknown += int(word)
|
|
281
|
+
break
|
|
282
|
+
else:
|
|
283
|
+
unknown += 1
|
|
284
|
+
if unknown > 0:
|
|
285
|
+
return f"{C.YELLOW}lic:{unknown}?{C.RESET}"
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def parse_aware(cached):
|
|
290
|
+
if not cached:
|
|
291
|
+
return None
|
|
292
|
+
out = cached["output"]
|
|
293
|
+
if cached["code"] != 0 or "stale" in out.lower() or "outdated" in out.lower() or "drift" in out.lower():
|
|
294
|
+
return f"{C.YELLOW}aware:stale{C.RESET}"
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def run_scans(cwd):
|
|
299
|
+
if not cwd:
|
|
300
|
+
return []
|
|
301
|
+
|
|
302
|
+
scans = {
|
|
303
|
+
"stale": (["npx", "--yes", "@whenlabs/stale", "scan"], parse_stale),
|
|
304
|
+
"envalid": (["npx", "--yes", "@whenlabs/envalid", "validate"], parse_envalid),
|
|
305
|
+
"berth": (["npx", "--yes", "@whenlabs/berth", "check", "."], parse_berth),
|
|
306
|
+
"vow": (["npx", "--yes", "@whenlabs/vow", "scan"], parse_vow),
|
|
307
|
+
"aware": (["npx", "--yes", "@whenlabs/aware", "doctor"], parse_aware),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for tool, (args, _) in scans.items():
|
|
311
|
+
if should_run(tool, cwd):
|
|
312
|
+
run_bg(tool, args, cwd)
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
results = []
|
|
316
|
+
for tool, (_, parser) in scans.items():
|
|
317
|
+
cached = read_cache(tool, cwd)
|
|
318
|
+
if cached:
|
|
319
|
+
parsed = parser(cached)
|
|
320
|
+
if parsed:
|
|
321
|
+
results.append(parsed)
|
|
322
|
+
|
|
323
|
+
return results
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def main():
|
|
327
|
+
try:
|
|
328
|
+
data = json.loads(sys.stdin.read())
|
|
329
|
+
except Exception:
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
parts = []
|
|
333
|
+
|
|
334
|
+
cwd = data.get("workspace", {}).get("current_dir", "")
|
|
335
|
+
if cwd:
|
|
336
|
+
parts.append(f"{C.BLUE}{Path(cwd).name}{C.RESET}")
|
|
337
|
+
|
|
338
|
+
if cwd:
|
|
339
|
+
g = git_info(cwd)
|
|
340
|
+
if g:
|
|
341
|
+
parts.append(g)
|
|
342
|
+
|
|
343
|
+
servers = mcp_servers(data)
|
|
344
|
+
if servers:
|
|
345
|
+
names = " ".join(servers)
|
|
346
|
+
parts.append(f"{C.AMBER}{C.DIM}{names}{C.RESET}")
|
|
347
|
+
|
|
348
|
+
c = cost_info(data)
|
|
349
|
+
if c:
|
|
350
|
+
parts.append(c)
|
|
351
|
+
|
|
352
|
+
model = data.get("model", {}).get("display_name", "")
|
|
353
|
+
if model:
|
|
354
|
+
short = "".join(ch for ch in model if ch.isalpha()).lower()
|
|
355
|
+
parts.append(f"{C.GRAY}{short}{C.RESET}")
|
|
356
|
+
|
|
357
|
+
ctx = context_pct(data)
|
|
358
|
+
if ctx:
|
|
359
|
+
parts.append(ctx)
|
|
360
|
+
|
|
361
|
+
ver = data.get("version")
|
|
362
|
+
if ver:
|
|
363
|
+
parts.append(f"{C.DIM}{C.GRAY}v{ver}{C.RESET}")
|
|
364
|
+
|
|
365
|
+
scan_results = run_scans(cwd)
|
|
366
|
+
|
|
367
|
+
print(C.SEP.join(parts))
|
|
368
|
+
|
|
369
|
+
if scan_results:
|
|
370
|
+
print(f"{C.DIM}{C.GRAY} tools:{C.RESET} {f' {C.DIM}|{C.RESET} '.join(scan_results)}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
374
|
+
main()
|
|
375
|
+
`;
|
|
376
|
+
function installStatusLine() {
|
|
377
|
+
try {
|
|
378
|
+
mkdirSync(SCRIPTS_DIR, { recursive: true });
|
|
379
|
+
writeFileSync(STATUSLINE_PATH, STATUSLINE_SCRIPT, "utf-8");
|
|
380
|
+
chmodSync(STATUSLINE_PATH, 493);
|
|
381
|
+
let settings = {};
|
|
382
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
383
|
+
try {
|
|
384
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
385
|
+
} catch {
|
|
386
|
+
settings = {};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const statuslineCmd = `python3 ${STATUSLINE_PATH}`;
|
|
390
|
+
const currentCmd = settings.statusLine?.command;
|
|
391
|
+
if (currentCmd !== statuslineCmd) {
|
|
392
|
+
settings.statusLine = { type: "command", command: statuslineCmd };
|
|
393
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
394
|
+
}
|
|
395
|
+
return { installed: true, message: "Status line installed (proactive background scans)" };
|
|
396
|
+
} catch (err) {
|
|
397
|
+
return { installed: false, message: `Status line install failed: ${err.message}` };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function escapeRegex(str) {
|
|
401
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
402
|
+
}
|
|
403
|
+
function hasOldBlock(filePath) {
|
|
404
|
+
if (!existsSync(filePath)) return false;
|
|
405
|
+
const content = readFileSync(filePath, "utf-8");
|
|
406
|
+
return content.includes(OLD_START_MARKER) && content.includes(OLD_END_MARKER);
|
|
407
|
+
}
|
|
408
|
+
function removeOldBlock(filePath) {
|
|
409
|
+
if (!existsSync(filePath)) return;
|
|
410
|
+
let content = readFileSync(filePath, "utf-8");
|
|
411
|
+
const pattern = new RegExp(
|
|
412
|
+
`\\n?${escapeRegex(OLD_START_MARKER)}[\\s\\S]*?${escapeRegex(OLD_END_MARKER)}\\n?`,
|
|
413
|
+
"g"
|
|
414
|
+
);
|
|
415
|
+
content = content.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
416
|
+
writeFileSync(filePath, content, "utf-8");
|
|
417
|
+
}
|
|
418
|
+
async function install(options = {}) {
|
|
419
|
+
console.log("\n\u{1F527} WhenLabs toolkit installer\n");
|
|
420
|
+
const editorFlags = options.all ? ALL_EDITORS : [
|
|
421
|
+
options.cursor && "cursor",
|
|
422
|
+
options.vscode && "vscode",
|
|
423
|
+
options.windsurf && "windsurf"
|
|
424
|
+
].filter(Boolean);
|
|
425
|
+
const claudeOnly = editorFlags.length === 0;
|
|
426
|
+
if (claudeOnly) {
|
|
427
|
+
const mcpResult = registerMcpServer();
|
|
428
|
+
console.log(mcpResult.success ? ` \u2713 ${mcpResult.message}` : ` \u2717 ${mcpResult.message}`);
|
|
429
|
+
injectBlock(CLAUDE_MD_PATH, CLAUDE_MD_CONTENT);
|
|
430
|
+
console.log(` \u2713 CLAUDE.md instructions written to ${CLAUDE_MD_PATH}`);
|
|
431
|
+
const slResult = installStatusLine();
|
|
432
|
+
console.log(slResult.installed ? ` \u2713 ${slResult.message}` : ` \u2717 ${slResult.message}`);
|
|
433
|
+
if (hasOldBlock(CLAUDE_MD_PATH)) {
|
|
434
|
+
removeOldBlock(CLAUDE_MD_PATH);
|
|
435
|
+
console.log(" \u2713 Removed legacy velocity-mcp markers (migrated to whenlabs block)");
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
for (const editor of editorFlags) {
|
|
439
|
+
const result = installForEditor(editor);
|
|
440
|
+
console.log(result.success ? ` \u2713 ${result.message}` : ` \u2717 ${result.message}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
console.log("\nInstallation complete. Run `when status` to verify.\n");
|
|
444
|
+
}
|
|
445
|
+
export {
|
|
446
|
+
install
|
|
447
|
+
};
|
package/dist/mcp.js
CHANGED
|
@@ -100,7 +100,7 @@ Note: Conflicts found in project "${projectName}".`);
|
|
|
100
100
|
}
|
|
101
101
|
var server = new McpServer({
|
|
102
102
|
name: "whenlabs",
|
|
103
|
-
version: "0.
|
|
103
|
+
version: "0.3.0"
|
|
104
104
|
});
|
|
105
105
|
var velocityDb = initDb();
|
|
106
106
|
var velocityQueries = new TaskQueries(velocityDb);
|
|
@@ -137,6 +137,25 @@ server.tool(
|
|
|
137
137
|
return { content: [{ type: "text", text: output + extras.join("") }] };
|
|
138
138
|
}
|
|
139
139
|
);
|
|
140
|
+
server.tool(
|
|
141
|
+
"stale_fix",
|
|
142
|
+
"Auto-fix documentation drift \u2014 generate fixes for wrong file paths, dead links, phantom env vars, outdated scripts",
|
|
143
|
+
{
|
|
144
|
+
path: z.string().optional().describe("Project directory to scan (defaults to cwd)"),
|
|
145
|
+
format: z.enum(["terminal", "diff"]).optional().describe("Output format (default: terminal)"),
|
|
146
|
+
apply: z.coerce.boolean().optional().describe("Apply high-confidence fixes directly"),
|
|
147
|
+
dryRun: z.coerce.boolean().optional().describe("Show what --apply would do without writing")
|
|
148
|
+
},
|
|
149
|
+
async ({ path, format, apply, dryRun }) => {
|
|
150
|
+
const args = ["fix"];
|
|
151
|
+
if (format) args.push("--format", format);
|
|
152
|
+
if (apply) args.push("--apply");
|
|
153
|
+
if (dryRun) args.push("--dry-run");
|
|
154
|
+
const result = await runCli("stale", args, path);
|
|
155
|
+
const output = formatOutput(result);
|
|
156
|
+
return { content: [{ type: "text", text: output }] };
|
|
157
|
+
}
|
|
158
|
+
);
|
|
140
159
|
server.tool(
|
|
141
160
|
"stale_init",
|
|
142
161
|
"Generate a .stale.yml config file for customizing documentation drift detection",
|
|
@@ -263,6 +282,21 @@ server.tool(
|
|
|
263
282
|
return { content: [{ type: "text", text: output }] };
|
|
264
283
|
}
|
|
265
284
|
);
|
|
285
|
+
server.tool(
|
|
286
|
+
"envalid_generate_schema",
|
|
287
|
+
"Generate .env.schema from code analysis \u2014 infer types, required-ness, and sensitivity from usage patterns",
|
|
288
|
+
{
|
|
289
|
+
path: z.string().optional().describe("Project directory (defaults to cwd)"),
|
|
290
|
+
output: z.string().optional().describe("Output file path (default: .env.schema)")
|
|
291
|
+
},
|
|
292
|
+
async ({ path, output }) => {
|
|
293
|
+
const args = ["detect", "--generate"];
|
|
294
|
+
if (output) args.push("-o", output);
|
|
295
|
+
const result = await runCli("envalid", args, path);
|
|
296
|
+
const outputText = formatOutput(result);
|
|
297
|
+
return { content: [{ type: "text", text: outputText }] };
|
|
298
|
+
}
|
|
299
|
+
);
|
|
266
300
|
server.tool(
|
|
267
301
|
"envalid_hook_status",
|
|
268
302
|
"Check if the envalid pre-commit git hook is installed",
|
|
@@ -379,6 +413,25 @@ server.tool(
|
|
|
379
413
|
return { content: [{ type: "text", text: output }] };
|
|
380
414
|
}
|
|
381
415
|
);
|
|
416
|
+
server.tool(
|
|
417
|
+
"berth_resolve",
|
|
418
|
+
"Auto-resolve port conflicts \u2014 detect conflicts and fix via kill or reassign strategy",
|
|
419
|
+
{
|
|
420
|
+
path: z.string().optional().describe("Project directory (defaults to cwd)"),
|
|
421
|
+
strategy: z.enum(["kill", "reassign", "auto"]).optional().describe("Resolution strategy (default: auto)"),
|
|
422
|
+
kill: z.coerce.boolean().optional().describe("Allow killing processes (required for kill/auto strategies)"),
|
|
423
|
+
dryRun: z.coerce.boolean().optional().describe("Show what would be done without making changes")
|
|
424
|
+
},
|
|
425
|
+
async ({ path, strategy, kill, dryRun }) => {
|
|
426
|
+
const args = ["resolve"];
|
|
427
|
+
if (strategy) args.push("--strategy", strategy);
|
|
428
|
+
if (kill) args.push("--kill");
|
|
429
|
+
if (dryRun) args.push("--dry-run");
|
|
430
|
+
const result = await runCli("berth", args, path);
|
|
431
|
+
const output = formatOutput(result);
|
|
432
|
+
return { content: [{ type: "text", text: output }] };
|
|
433
|
+
}
|
|
434
|
+
);
|
|
382
435
|
server.tool(
|
|
383
436
|
"berth_predict",
|
|
384
437
|
"Predict port conflicts from project config files before starting \u2014 dry-run conflict check",
|
|
@@ -427,9 +480,14 @@ server.tool(
|
|
|
427
480
|
server.tool(
|
|
428
481
|
"aware_diff",
|
|
429
482
|
"Show project changes since last sync \u2014 see what drifted in your codebase",
|
|
430
|
-
{
|
|
431
|
-
|
|
432
|
-
|
|
483
|
+
{
|
|
484
|
+
path: z.string().optional().describe("Project directory (defaults to cwd)"),
|
|
485
|
+
exitCode: z.coerce.boolean().optional().describe("Return exit code 1 if changes detected (useful for CI)")
|
|
486
|
+
},
|
|
487
|
+
async ({ path, exitCode }) => {
|
|
488
|
+
const args = ["diff"];
|
|
489
|
+
if (exitCode) args.push("--exit-code");
|
|
490
|
+
const result = await runCli("aware", args, path);
|
|
433
491
|
const output = formatOutput(result);
|
|
434
492
|
return { content: [{ type: "text", text: output }] };
|
|
435
493
|
}
|
|
@@ -579,6 +637,42 @@ server.tool(
|
|
|
579
637
|
return { content: [{ type: "text", text: outputText }] };
|
|
580
638
|
}
|
|
581
639
|
);
|
|
640
|
+
server.tool(
|
|
641
|
+
"vow_hook_install",
|
|
642
|
+
"Install a pre-commit git hook that checks dependency licenses before each commit",
|
|
643
|
+
{
|
|
644
|
+
path: z.string().optional().describe("Project directory (defaults to cwd)")
|
|
645
|
+
},
|
|
646
|
+
async ({ path }) => {
|
|
647
|
+
const result = await runCli("vow", ["hook", "install"], path);
|
|
648
|
+
const output = formatOutput(result);
|
|
649
|
+
return { content: [{ type: "text", text: output }] };
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
server.tool(
|
|
653
|
+
"vow_hook_uninstall",
|
|
654
|
+
"Remove the vow pre-commit license check hook",
|
|
655
|
+
{
|
|
656
|
+
path: z.string().optional().describe("Project directory (defaults to cwd)")
|
|
657
|
+
},
|
|
658
|
+
async ({ path }) => {
|
|
659
|
+
const result = await runCli("vow", ["hook", "uninstall"], path);
|
|
660
|
+
const output = formatOutput(result);
|
|
661
|
+
return { content: [{ type: "text", text: output }] };
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
server.tool(
|
|
665
|
+
"vow_hook_status",
|
|
666
|
+
"Check if the vow pre-commit license check hook is installed",
|
|
667
|
+
{
|
|
668
|
+
path: z.string().optional().describe("Project directory (defaults to cwd)")
|
|
669
|
+
},
|
|
670
|
+
async ({ path }) => {
|
|
671
|
+
const result = await runCli("vow", ["hook", "status"], path);
|
|
672
|
+
const output = formatOutput(result);
|
|
673
|
+
return { content: [{ type: "text", text: output }] };
|
|
674
|
+
}
|
|
675
|
+
);
|
|
582
676
|
server.tool(
|
|
583
677
|
"vow_attribution",
|
|
584
678
|
"Generate THIRD_PARTY_LICENSES.md \u2014 list all dependencies with their licenses for compliance",
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
formatStatusLine,
|
|
4
|
+
isStale,
|
|
5
|
+
readStatus
|
|
6
|
+
} from "./chunk-4ZVSCJCJ.js";
|
|
2
7
|
import {
|
|
3
8
|
hasBlock
|
|
4
9
|
} from "./chunk-NYUYV3UL.js";
|
|
@@ -36,6 +41,16 @@ async function status() {
|
|
|
36
41
|
` CLAUDE.md instructions: ${claudeMdInstalled ? "\u2713 installed" : "\u2717 not installed"}`
|
|
37
42
|
);
|
|
38
43
|
console.log(` CLAUDE.md path: ${CLAUDE_MD_PATH}`);
|
|
44
|
+
const watchData = readStatus();
|
|
45
|
+
if (watchData) {
|
|
46
|
+
const line = formatStatusLine();
|
|
47
|
+
const stale = isStale();
|
|
48
|
+
const age = stale ? " (stale)" : "";
|
|
49
|
+
console.log(`
|
|
50
|
+
Watch results${age}: ${line}`);
|
|
51
|
+
console.log(` Last scan: ${watchData.timestamp}`);
|
|
52
|
+
console.log(` Summary: ${watchData.summary}`);
|
|
53
|
+
}
|
|
39
54
|
const allGood = mcpRegistered && claudeMdInstalled;
|
|
40
55
|
if (allGood) {
|
|
41
56
|
console.log("\n Everything is set up. Run `when --help` to see available tools.\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whenlabs/when",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "The WhenLabs developer toolkit — 6 tools, one install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
-
"@whenlabs/aware": "^0.1.
|
|
33
|
-
"@whenlabs/berth": "^0.1.
|
|
34
|
-
"@whenlabs/envalid": "^0.1.
|
|
35
|
-
"@whenlabs/stale": "^0.1.
|
|
36
|
-
"@whenlabs/velocity-mcp": "^0.1.
|
|
37
|
-
"@whenlabs/vow": "^0.1.
|
|
32
|
+
"@whenlabs/aware": "^0.1.3",
|
|
33
|
+
"@whenlabs/berth": "^0.1.3",
|
|
34
|
+
"@whenlabs/envalid": "^0.1.3",
|
|
35
|
+
"@whenlabs/stale": "^0.1.3",
|
|
36
|
+
"@whenlabs/velocity-mcp": "^0.1.3",
|
|
37
|
+
"@whenlabs/vow": "^0.1.4",
|
|
38
38
|
"commander": "^12.0.0",
|
|
39
39
|
"zod": "^4.3.6"
|
|
40
40
|
},
|
package/dist/install-HPF26YW2.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ALL_EDITORS,
|
|
4
|
-
installForEditor,
|
|
5
|
-
registerMcpServer
|
|
6
|
-
} from "./chunk-3PDLNC63.js";
|
|
7
|
-
import {
|
|
8
|
-
injectBlock
|
|
9
|
-
} from "./chunk-NYUYV3UL.js";
|
|
10
|
-
|
|
11
|
-
// src/commands/install.ts
|
|
12
|
-
import { join } from "path";
|
|
13
|
-
import { homedir } from "os";
|
|
14
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
-
var CLAUDE_MD_PATH = join(homedir(), ".claude", "CLAUDE.md");
|
|
16
|
-
var OLD_START_MARKER = "<!-- velocity-mcp:start -->";
|
|
17
|
-
var OLD_END_MARKER = "<!-- velocity-mcp:end -->";
|
|
18
|
-
var CLAUDE_MD_CONTENT = `# WhenLabs Toolkit
|
|
19
|
-
|
|
20
|
-
## Task Timing (velocity-mcp)
|
|
21
|
-
|
|
22
|
-
Velocity tools are part of the unified \`whenlabs\` MCP server. Follow these rules for EVERY session:
|
|
23
|
-
|
|
24
|
-
1. **Before starting any discrete coding task**, call \`velocity_start_task\` with:
|
|
25
|
-
- Appropriate \`category\` (scaffold, implement, refactor, debug, test, config, docs, deploy)
|
|
26
|
-
- Relevant \`tags\` (e.g. typescript, react, sqlite, api)
|
|
27
|
-
- Clear \`description\` of what you're about to do
|
|
28
|
-
- \`estimated_files\` if you know how many files you'll touch
|
|
29
|
-
- \`project\` set to the current project name (auto-detected from git remote or directory name)
|
|
30
|
-
|
|
31
|
-
2. **After completing each task**, call \`velocity_end_task\` with:
|
|
32
|
-
- The \`task_id\` from the start call
|
|
33
|
-
- \`status\`: completed, failed, or abandoned
|
|
34
|
-
- \`actual_files\`: how many files were actually modified
|
|
35
|
-
- \`notes\`: any useful context about what happened
|
|
36
|
-
|
|
37
|
-
3. **When creating a multi-step plan**, call \`velocity_estimate\` to provide the user with a time estimate before starting work.
|
|
38
|
-
|
|
39
|
-
4. **If the user asks about speed or performance**, call \`velocity_stats\` to show aggregate data.
|
|
40
|
-
|
|
41
|
-
### Guidelines
|
|
42
|
-
- Every discrete unit of work should be tracked \u2014 don't batch multiple unrelated changes into one task
|
|
43
|
-
- If a task is abandoned or fails, still call \`velocity_end_task\` with the appropriate status
|
|
44
|
-
- Use consistent tags across sessions so the similarity matching can find comparable historical tasks
|
|
45
|
-
- Keep descriptions concise but specific enough to be useful for future matching
|
|
46
|
-
|
|
47
|
-
## WhenLabs MCP Tools (ALWAYS prefer these over shell commands)
|
|
48
|
-
|
|
49
|
-
All six tools (including velocity) are available through the unified \`whenlabs\` MCP server. **ALWAYS use these MCP tools instead of running shell commands like lsof, grep, or manual checks.** These tools are purpose-built and give better results:
|
|
50
|
-
|
|
51
|
-
| When to use | Call this tool | NOT this |
|
|
52
|
-
|-------------|---------------|----------|
|
|
53
|
-
| Check ports or port conflicts | \`berth_status\` or \`berth_check\` | \`lsof\`, \`netstat\`, \`ss\` |
|
|
54
|
-
| Scan dependency licenses | \`vow_scan\` or \`vow_check\` | manual \`npm ls\`, \`license-checker\` |
|
|
55
|
-
| Check if docs are stale | \`stale_scan\` | manual file comparison |
|
|
56
|
-
| Validate .env files | \`envalid_validate\` or \`envalid_detect\` | manual .env inspection |
|
|
57
|
-
| Generate AI context files | \`aware_init\` or \`aware_doctor\` | manual CLAUDE.md creation |
|
|
58
|
-
|
|
59
|
-
### Tool Reference
|
|
60
|
-
- \`berth_status\` \u2014 Show all active ports, Docker ports, and configured ports
|
|
61
|
-
- \`berth_check\` \u2014 Scan a project directory for port conflicts
|
|
62
|
-
- \`stale_scan\` \u2014 Detect documentation drift in the codebase
|
|
63
|
-
- \`envalid_validate\` \u2014 Validate .env files against their schema
|
|
64
|
-
- \`envalid_detect\` \u2014 Find undocumented env vars in codebase
|
|
65
|
-
- \`aware_init\` \u2014 Auto-detect stack and generate AI context files
|
|
66
|
-
- \`aware_doctor\` \u2014 Diagnose project health and config issues
|
|
67
|
-
- \`vow_scan\` \u2014 Scan and summarize all dependency licenses
|
|
68
|
-
- \`vow_check\` \u2014 Validate licenses against a policy file
|
|
69
|
-
|
|
70
|
-
### Proactive Background Scans
|
|
71
|
-
WhenLabs tools run automatically in the background on a schedule. The status line shows findings:
|
|
72
|
-
- \`stale:N\` \u2014 N docs have drifted from code. Run \`stale_scan\` and fix the drift.
|
|
73
|
-
- \`env:N\` \u2014 N .env issues found. Run \`envalid_validate\` and help the user fix them.
|
|
74
|
-
- \`ports:N\` \u2014 N port conflicts. Run \`berth_status\` and suggest resolution.
|
|
75
|
-
- \`lic:N?\` \u2014 N packages with unknown licenses. Run \`vow_scan\` for details.
|
|
76
|
-
- \`aware:stale\` \u2014 AI context files are outdated. Run \`aware_init\` to regenerate.
|
|
77
|
-
|
|
78
|
-
**When you see any of these in the status line, proactively tell the user and offer to fix the issue.** Do not wait for the user to ask.`;
|
|
79
|
-
function escapeRegex(str) {
|
|
80
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
81
|
-
}
|
|
82
|
-
function hasOldBlock(filePath) {
|
|
83
|
-
if (!existsSync(filePath)) return false;
|
|
84
|
-
const content = readFileSync(filePath, "utf-8");
|
|
85
|
-
return content.includes(OLD_START_MARKER) && content.includes(OLD_END_MARKER);
|
|
86
|
-
}
|
|
87
|
-
function removeOldBlock(filePath) {
|
|
88
|
-
if (!existsSync(filePath)) return;
|
|
89
|
-
let content = readFileSync(filePath, "utf-8");
|
|
90
|
-
const pattern = new RegExp(
|
|
91
|
-
`\\n?${escapeRegex(OLD_START_MARKER)}[\\s\\S]*?${escapeRegex(OLD_END_MARKER)}\\n?`,
|
|
92
|
-
"g"
|
|
93
|
-
);
|
|
94
|
-
content = content.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
95
|
-
writeFileSync(filePath, content, "utf-8");
|
|
96
|
-
}
|
|
97
|
-
async function install(options = {}) {
|
|
98
|
-
console.log("\n\u{1F527} WhenLabs toolkit installer\n");
|
|
99
|
-
const editorFlags = options.all ? ALL_EDITORS : [
|
|
100
|
-
options.cursor && "cursor",
|
|
101
|
-
options.vscode && "vscode",
|
|
102
|
-
options.windsurf && "windsurf"
|
|
103
|
-
].filter(Boolean);
|
|
104
|
-
const claudeOnly = editorFlags.length === 0;
|
|
105
|
-
if (claudeOnly) {
|
|
106
|
-
const mcpResult = registerMcpServer();
|
|
107
|
-
console.log(mcpResult.success ? ` \u2713 ${mcpResult.message}` : ` \u2717 ${mcpResult.message}`);
|
|
108
|
-
injectBlock(CLAUDE_MD_PATH, CLAUDE_MD_CONTENT);
|
|
109
|
-
console.log(` \u2713 CLAUDE.md instructions written to ${CLAUDE_MD_PATH}`);
|
|
110
|
-
if (hasOldBlock(CLAUDE_MD_PATH)) {
|
|
111
|
-
removeOldBlock(CLAUDE_MD_PATH);
|
|
112
|
-
console.log(" \u2713 Removed legacy velocity-mcp markers (migrated to whenlabs block)");
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
for (const editor of editorFlags) {
|
|
116
|
-
const result = installForEditor(editor);
|
|
117
|
-
console.log(result.success ? ` \u2713 ${result.message}` : ` \u2717 ${result.message}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
console.log("\nInstallation complete. Run `when status` to verify.\n");
|
|
121
|
-
}
|
|
122
|
-
export {
|
|
123
|
-
install
|
|
124
|
-
};
|