@whenlabs/when 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
 
@@ -2,15 +2,18 @@
2
2
 
3
3
  // src/utils/mcp-config.ts
4
4
  import { execSync } from "child_process";
5
+ import { existsSync, readFileSync, writeFileSync } from "fs";
6
+ import { resolve } from "path";
5
7
  function registerServer(name, command) {
8
+ try {
9
+ execSync(`claude mcp remove -s user ${name}`, { stdio: "pipe" });
10
+ } catch {
11
+ }
6
12
  try {
7
13
  execSync(`claude mcp add -s user ${name} -- ${command}`, { stdio: "pipe" });
8
- return { success: true, message: `MCP server "${name}" registered successfully.` };
14
+ return { success: true, message: `MCP server "${name}" registered.` };
9
15
  } catch (err) {
10
16
  const output = err instanceof Error && "stderr" in err ? err.stderr?.toString() ?? "" : "";
11
- if (output.includes("already exists") || output.includes("already registered")) {
12
- return { success: true, message: `MCP server "${name}" is already registered.` };
13
- }
14
17
  return {
15
18
  success: false,
16
19
  message: `Failed to register "${name}": ${output || (err instanceof Error ? err.message : String(err))}`
@@ -32,12 +35,36 @@ function unregisterServer(name) {
32
35
  };
33
36
  }
34
37
  }
38
+ function cleanLegacyMcpJson() {
39
+ let dir = process.cwd();
40
+ const root = resolve("/");
41
+ while (dir !== root) {
42
+ const mcpJson = resolve(dir, ".mcp.json");
43
+ if (existsSync(mcpJson)) {
44
+ try {
45
+ const data = JSON.parse(readFileSync(mcpJson, "utf-8"));
46
+ if (data?.mcpServers?.["velocity-mcp"]) {
47
+ delete data.mcpServers["velocity-mcp"];
48
+ writeFileSync(mcpJson, JSON.stringify(data, null, 2) + "\n", "utf-8");
49
+ return mcpJson;
50
+ }
51
+ } catch {
52
+ }
53
+ }
54
+ dir = resolve(dir, "..");
55
+ }
56
+ return null;
57
+ }
35
58
  function registerMcpServer() {
36
59
  const whenlabs = registerServer("whenlabs", "npx @whenlabs/when when-mcp");
37
60
  const legacyCleanup = unregisterServer("velocity-mcp");
61
+ const cleanedFile = cleanLegacyMcpJson();
38
62
  const messages = [whenlabs.message];
39
63
  if (legacyCleanup.success && !legacyCleanup.message.includes("was not registered")) {
40
- messages.push("Removed legacy standalone velocity-mcp (now bundled in whenlabs)");
64
+ messages.push("Removed legacy velocity-mcp from user config (now bundled in whenlabs)");
65
+ }
66
+ if (cleanedFile) {
67
+ messages.push(`Removed legacy velocity-mcp from ${cleanedFile}`);
41
68
  }
42
69
  return {
43
70
  success: whenlabs.success,
@@ -60,7 +87,7 @@ function unregisterMcpServer() {
60
87
  // src/utils/editor-config.ts
61
88
  import { join } from "path";
62
89
  import { homedir } from "os";
63
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
90
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
64
91
  var MCP_SERVERS = {
65
92
  "velocity-mcp": { command: "npx", args: ["@whenlabs/velocity-mcp"] },
66
93
  whenlabs: { command: "when-mcp", args: [] }
@@ -77,16 +104,16 @@ function getConfigPath(editor) {
77
104
  }
78
105
  }
79
106
  function readJsonFile(filePath) {
80
- if (!existsSync(filePath)) return {};
107
+ if (!existsSync2(filePath)) return {};
81
108
  try {
82
- return JSON.parse(readFileSync(filePath, "utf-8"));
109
+ return JSON.parse(readFileSync2(filePath, "utf-8"));
83
110
  } catch {
84
111
  return {};
85
112
  }
86
113
  }
87
114
  function ensureDir(filePath) {
88
115
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
89
- if (!existsSync(dir)) {
116
+ if (!existsSync2(dir)) {
90
117
  mkdirSync(dir, { recursive: true });
91
118
  }
92
119
  }
@@ -102,7 +129,7 @@ function installCursorOrWindsurf(editor) {
102
129
  }
103
130
  };
104
131
  ensureDir(configPath);
105
- writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
132
+ writeFileSync2(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
106
133
  return { editor, success: true, message: `${capitalize(editor)}: MCP servers registered at ${configPath}` };
107
134
  }
108
135
  function installVSCode() {
@@ -121,12 +148,12 @@ function installVSCode() {
121
148
  }
122
149
  };
123
150
  ensureDir(configPath);
124
- writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
151
+ writeFileSync2(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
125
152
  return { editor: "vscode", success: true, message: `VS Code: MCP servers registered at ${configPath}` };
126
153
  }
127
154
  function uninstallCursorOrWindsurf(editor) {
128
155
  const configPath = getConfigPath(editor);
129
- if (!existsSync(configPath)) {
156
+ if (!existsSync2(configPath)) {
130
157
  return { editor, success: true, message: `${capitalize(editor)}: config not found, nothing to remove` };
131
158
  }
132
159
  const existing = readJsonFile(configPath);
@@ -135,12 +162,12 @@ function uninstallCursorOrWindsurf(editor) {
135
162
  delete mcpServers[key];
136
163
  }
137
164
  const updated = { ...existing, mcpServers };
138
- writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
165
+ writeFileSync2(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
139
166
  return { editor, success: true, message: `${capitalize(editor)}: MCP servers removed from ${configPath}` };
140
167
  }
141
168
  function uninstallVSCode() {
142
169
  const configPath = getConfigPath("vscode");
143
- if (!existsSync(configPath)) {
170
+ if (!existsSync2(configPath)) {
144
171
  return { editor: "vscode", success: true, message: "VS Code: config not found, nothing to remove" };
145
172
  }
146
173
  const existing = readJsonFile(configPath);
@@ -153,7 +180,7 @@ function uninstallVSCode() {
153
180
  ...existing,
154
181
  mcp: { ...mcpSection, servers }
155
182
  };
156
- writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
183
+ writeFileSync2(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
157
184
  return { editor: "vscode", success: true, message: `VS Code: MCP servers removed from ${configPath}` };
158
185
  }
159
186
  function capitalize(s) {
@@ -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,19 +501,110 @@ 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-JRDDTX6Y.js");
599
+ const { install } = await import("./install-HPF26YW2.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) => {
502
- const { uninstall } = await import("./uninstall-QQ2Y3AQ3.js");
603
+ const { uninstall } = await import("./uninstall-X5NO3Z6I.js");
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"));
@@ -3,7 +3,7 @@ import {
3
3
  ALL_EDITORS,
4
4
  installForEditor,
5
5
  registerMcpServer
6
- } from "./chunk-2Y7YZAIR.js";
6
+ } from "./chunk-3PDLNC63.js";
7
7
  import {
8
8
  injectBlock
9
9
  } from "./chunk-NYUYV3UL.js";
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");
@@ -3,7 +3,7 @@ import {
3
3
  ALL_EDITORS,
4
4
  uninstallForEditor,
5
5
  unregisterMcpServer
6
- } from "./chunk-2Y7YZAIR.js";
6
+ } from "./chunk-3PDLNC63.js";
7
7
  import {
8
8
  removeBlock
9
9
  } from "./chunk-NYUYV3UL.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whenlabs/when",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
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
  },