@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 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 **two MCP servers** in your Claude Code configuration:
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 both MCP servers and cleans up CLAUDE.md instructions.
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 Command4 } from "commander";
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 Promise.all([
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 Command4();
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-HPF26YW2.js");
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-LOZGVOA3.js");
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.2.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
- { path: z.string().optional().describe("Project directory (defaults to cwd)") },
431
- async ({ path }) => {
432
- const result = await runCli("aware", ["diff"], path);
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.8.1",
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.2",
33
- "@whenlabs/berth": "^0.1.0",
34
- "@whenlabs/envalid": "^0.1.2",
35
- "@whenlabs/stale": "^0.1.0",
36
- "@whenlabs/velocity-mcp": "^0.1.2",
37
- "@whenlabs/vow": "^0.1.3",
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
  },
@@ -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
- };