@whenlabs/when 0.9.1 → 0.9.3

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
@@ -4,6 +4,8 @@
4
4
 
5
5
  A single installable toolkit that brings six WhenLabs developer tools into your Claude Code / AI coding agent workflow. Once installed, all tools are available as MCP tools in every session — Claude uses them automatically when relevant.
6
6
 
7
+ Five tools (stale, envalid, berth, aware, vow) have CLI scan modes and run on a schedule. Velocity is the sixth tool — it is always-on and embedded (SQLite-backed), so it does not have a CLI scan mode and does not appear in `doctor`/`watch`/`init`/`ci` output.
8
+
7
9
  ## Install
8
10
 
9
11
  ```bash
@@ -61,6 +63,8 @@ These tools are available to Claude in every session after install:
61
63
  | `vow_check` | Validate licenses against policy |
62
64
  | `vow_hook_install` | Install pre-commit license check hook |
63
65
 
66
+ > This table shows a highlights subset. Run `when <tool> --help` for all available commands per tool.
67
+
64
68
  ## Multi-Editor Support
65
69
 
66
70
  Install MCP servers into other editors alongside Claude Code:
@@ -98,7 +102,7 @@ when ci # Run checks for CI (exits 1 on issues)
98
102
 
99
103
  ### `when init`
100
104
 
101
- One command to onboard any project. Auto-detects your stack, runs all 5 tools in parallel, generates AI context files if missing, and shows a summary with next steps.
105
+ One command to onboard any project. Auto-detects your stack, runs all 5 CLI tools in parallel, generates AI context files if missing, and shows a summary with next steps.
102
106
 
103
107
  ### `when doctor`
104
108
 
@@ -106,7 +110,7 @@ Runs all 5 CLI tools against the current project and displays a unified health r
106
110
 
107
111
  ### `when watch`
108
112
 
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.
113
+ Background daemon that runs all 5 CLI 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.
110
114
 
111
115
  ### `when ci`
112
116
 
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/find-bin.ts
4
+ import { resolve, dirname } from "path";
5
+ import { existsSync } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ var __dirname = dirname(fileURLToPath(import.meta.url));
8
+ function findBin(name) {
9
+ const pkgRoot = resolve(__dirname, "../..");
10
+ const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
11
+ if (existsSync(localBin)) return localBin;
12
+ const directCli = resolve(pkgRoot, "node_modules", "@whenlabs", name, "dist", "cli.js");
13
+ if (existsSync(directCli)) return directCli;
14
+ return name;
15
+ }
16
+
17
+ export {
18
+ findBin
19
+ };
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ findBin
4
+ } from "./chunk-2A2EZZF4.js";
2
5
  import {
3
6
  getStatusPath
4
7
  } from "./chunk-4ZVSCJCJ.js";
@@ -9,16 +12,6 @@ import { Command as Command5 } from "commander";
9
12
  // src/commands/delegate.ts
10
13
  import { Command } from "commander";
11
14
  import { spawn } from "child_process";
12
- import { resolve, dirname } from "path";
13
- import { existsSync } from "fs";
14
- import { fileURLToPath } from "url";
15
- var __dirname = dirname(fileURLToPath(import.meta.url));
16
- function findBin(name) {
17
- const pkgRoot = resolve(__dirname, "..");
18
- const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
19
- if (existsSync(localBin)) return localBin;
20
- return name;
21
- }
22
15
  function createDelegateCommand(name, description, binName) {
23
16
  const cmd = new Command(name);
24
17
  cmd.description(description);
@@ -53,14 +46,14 @@ import { Command as Command2 } from "commander";
53
46
 
54
47
  // src/utils/tool-runner.ts
55
48
  import { spawn as spawn2 } from "child_process";
56
- import { resolve as resolve2, dirname as dirname2 } from "path";
57
- import { existsSync as existsSync2 } from "fs";
58
- import { fileURLToPath as fileURLToPath2 } from "url";
59
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
49
+ import { resolve, dirname } from "path";
50
+ import { existsSync } from "fs";
51
+ import { fileURLToPath } from "url";
52
+ var __dirname = dirname(fileURLToPath(import.meta.url));
60
53
  function findBin2(name) {
61
- const pkgRoot = resolve2(__dirname2, "..", "..");
62
- const localBin = resolve2(pkgRoot, "node_modules", ".bin", name);
63
- if (existsSync2(localBin)) return localBin;
54
+ const pkgRoot = resolve(__dirname, "..", "..");
55
+ const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
56
+ if (existsSync(localBin)) return localBin;
64
57
  return name;
65
58
  }
66
59
  function runTool(bin, args) {
@@ -303,10 +296,10 @@ function createDoctorCommand() {
303
296
  // src/commands/init.ts
304
297
  import { Command as Command3 } from "commander";
305
298
  import { spawn as spawn3 } from "child_process";
306
- import { resolve as resolve3, dirname as dirname3, basename } from "path";
307
- import { existsSync as existsSync3, readFileSync } from "fs";
308
- import { fileURLToPath as fileURLToPath3 } from "url";
309
- var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
299
+ import { resolve as resolve2, dirname as dirname2, basename } from "path";
300
+ import { existsSync as existsSync2, readFileSync } from "fs";
301
+ import { fileURLToPath as fileURLToPath2 } from "url";
302
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
310
303
  var c2 = {
311
304
  reset: "\x1B[0m",
312
305
  bold: "\x1B[1m",
@@ -320,15 +313,15 @@ function colorize2(text, ...codes) {
320
313
  return codes.join("") + text + c2.reset;
321
314
  }
322
315
  function findBin3(name) {
323
- const pkgRoot = resolve3(__dirname3, "..");
324
- const localBin = resolve3(pkgRoot, "node_modules", ".bin", name);
325
- if (existsSync3(localBin)) return localBin;
316
+ const pkgRoot = resolve2(__dirname2, "..");
317
+ const localBin = resolve2(pkgRoot, "node_modules", ".bin", name);
318
+ if (existsSync2(localBin)) return localBin;
326
319
  return name;
327
320
  }
328
321
  function detectProject(cwd) {
329
322
  let name = basename(cwd);
330
- const pkgPath = resolve3(cwd, "package.json");
331
- if (existsSync3(pkgPath)) {
323
+ const pkgPath = resolve2(cwd, "package.json");
324
+ if (existsSync2(pkgPath)) {
332
325
  try {
333
326
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
334
327
  if (pkg.name) name = pkg.name;
@@ -349,7 +342,7 @@ function detectProject(cwd) {
349
342
  ];
350
343
  const stacks = [];
351
344
  for (const [file, stack] of stackFiles) {
352
- if (existsSync3(resolve3(cwd, file)) && !stacks.includes(stack)) {
345
+ if (existsSync2(resolve2(cwd, file)) && !stacks.includes(stack)) {
353
346
  stacks.push(stack);
354
347
  }
355
348
  }
@@ -423,7 +416,7 @@ async function scanVow(cwd) {
423
416
  }
424
417
  }
425
418
  async function scanAware(cwd) {
426
- const hasConfig = existsSync3(resolve3(cwd, ".aware.json"));
419
+ const hasConfig = existsSync2(resolve2(cwd, ".aware.json"));
427
420
  if (!hasConfig) {
428
421
  const { exitCode: exitCode2 } = await runTool2(findBin3("aware"), ["init"]);
429
422
  if (exitCode2 === 0) return { label: "AI context (aware)", status: "ok", detail: "Generated .aware.json and context files" };
@@ -488,7 +481,7 @@ function createInitCommand() {
488
481
  }
489
482
  console.log("");
490
483
  console.log(colorize2(" Next steps:", c2.bold));
491
- const mcpInstalled = existsSync3(resolve3(process.env.HOME ?? "~", ".claude", "settings.json"));
484
+ const mcpInstalled = existsSync2(resolve2(process.env.HOME ?? "~", ".claude", "settings.json"));
492
485
  if (!mcpInstalled) {
493
486
  console.log(` ${colorize2("\u2022", c2.cyan)} Run ${colorize2("when install", c2.bold)} to connect MCP tools to Claude Code`);
494
487
  }
@@ -549,7 +542,7 @@ function sleep(ms) {
549
542
  }
550
543
  function createWatchCommand() {
551
544
  const cmd = new Command4("watch");
552
- cmd.description("Run all 5 WhenLabs tools on a schedule and write results to ~/.whenlabs/status.json");
545
+ cmd.description("Run all 5 CLI tools on a schedule and write results to ~/.whenlabs/status.json (velocity is embedded and always-on \u2014 it does not participate in scheduled scans)");
553
546
  cmd.option("--once", "Run a single scan and exit");
554
547
  cmd.option("--interval <seconds>", "Override the default scan interval (seconds)", "60");
555
548
  cmd.action(async (options) => {
@@ -593,10 +586,15 @@ function createWatchCommand() {
593
586
  }
594
587
 
595
588
  // src/index.ts
589
+ import { readFileSync as readFileSync2 } from "fs";
590
+ import { resolve as resolve3, dirname as dirname3 } from "path";
591
+ import { fileURLToPath as fileURLToPath3 } from "url";
592
+ var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
593
+ var { version } = JSON.parse(readFileSync2(resolve3(__dirname3, "..", "package.json"), "utf8"));
596
594
  var program = new Command5();
597
- program.name("when").version("0.1.0").description("The WhenLabs developer toolkit \u2014 6 tools, one install");
595
+ program.name("when").version(version).description("The WhenLabs developer toolkit \u2014 6 tools, one install");
598
596
  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) => {
599
- const { install } = await import("./install-TFEGFWJ5.js");
597
+ const { install } = await import("./install-V24JHOA2.js");
600
598
  await install(options);
601
599
  });
602
600
  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) => {
@@ -0,0 +1,183 @@
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, resolve, dirname } from "path";
13
+ import { homedir } from "os";
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
15
+ import { execFileSync } from "child_process";
16
+ import { fileURLToPath } from "url";
17
+
18
+ // src/templates/claude-md-content.ts
19
+ var CLAUDE_MD_CONTENT = `# WhenLabs Toolkit
20
+
21
+ ## Task Timing (velocity-mcp)
22
+
23
+ Velocity tools are part of the unified \`whenlabs\` MCP server. Follow these rules for EVERY session:
24
+
25
+ 1. **Before starting any discrete coding task**, call \`velocity_start_task\` with:
26
+ - Appropriate \`category\` (scaffold, implement, refactor, debug, test, config, docs, deploy)
27
+ - Relevant \`tags\` (e.g. typescript, react, sqlite, api)
28
+ - Clear \`description\` of what you're about to do
29
+ - \`estimated_files\` if you know how many files you'll touch
30
+ - \`project\` set to the current project name (auto-detected from git remote or directory name)
31
+
32
+ 2. **After completing each task**, call \`velocity_end_task\` with:
33
+ - The \`task_id\` from the start call
34
+ - \`status\`: completed, failed, or abandoned
35
+ - \`actual_files\`: how many files were actually modified
36
+ - \`notes\`: any useful context about what happened
37
+
38
+ 3. **When creating a multi-step plan**, call \`velocity_estimate\` to provide the user with a time estimate before starting work.
39
+
40
+ 4. **If the user asks about speed or performance**, call \`velocity_stats\` to show aggregate data.
41
+
42
+ ### Guidelines
43
+ - Every discrete unit of work should be tracked \u2014 don't batch multiple unrelated changes into one task
44
+ - If a task is abandoned or fails, still call \`velocity_end_task\` with the appropriate status
45
+ - Use consistent tags across sessions so the similarity matching can find comparable historical tasks
46
+ - Keep descriptions concise but specific enough to be useful for future matching
47
+
48
+ ## WhenLabs MCP Tools (ALWAYS prefer these over shell commands)
49
+
50
+ 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:
51
+
52
+ | When to use | Call this tool | NOT this |
53
+ |-------------|---------------|----------|
54
+ | Check ports or port conflicts | \`berth_status\` or \`berth_check\` | \`lsof\`, \`netstat\`, \`ss\` |
55
+ | Scan dependency licenses | \`vow_scan\` or \`vow_check\` | manual \`npm ls\`, \`license-checker\` |
56
+ | Check if docs are stale | \`stale_scan\` | manual file comparison |
57
+ | Validate .env files | \`envalid_validate\` or \`envalid_detect\` | manual .env inspection |
58
+ | Generate AI context files | \`aware_init\` or \`aware_doctor\` | manual CLAUDE.md creation |
59
+
60
+ ### Tool Reference
61
+ - \`berth_status\` \u2014 Show all active ports, Docker ports, and configured ports
62
+ - \`berth_check\` \u2014 Scan a project directory for port conflicts
63
+ - \`stale_scan\` \u2014 Detect documentation drift in the codebase
64
+ - \`envalid_validate\` \u2014 Validate .env files against their schema
65
+ - \`envalid_detect\` \u2014 Find undocumented env vars in codebase
66
+ - \`aware_init\` \u2014 Auto-detect stack and generate AI context files
67
+ - \`aware_doctor\` \u2014 Diagnose project health and config issues
68
+ - \`vow_scan\` \u2014 Scan and summarize all dependency licenses
69
+ - \`vow_check\` \u2014 Validate licenses against a policy file
70
+
71
+ ### Proactive Background Scans
72
+ WhenLabs tools run automatically in the background on a schedule. The status line shows findings:
73
+ - \`stale:N\` \u2014 N docs have drifted from code. Run \`stale_scan\` and fix the drift.
74
+ - \`env:N\` \u2014 N .env issues found. Run \`envalid_validate\` and help the user fix them.
75
+ - \`ports:N\` \u2014 N port conflicts. Run \`berth_status\` and suggest resolution.
76
+ - \`lic:N?\` \u2014 N packages with unknown licenses. Run \`vow_scan\` for details.
77
+ - \`aware:stale\` \u2014 AI context files are outdated. Run \`aware_init\` to regenerate.
78
+
79
+ **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.`;
80
+
81
+ // src/commands/install.ts
82
+ var __filename = fileURLToPath(import.meta.url);
83
+ var __dirname = dirname(__filename);
84
+ var CLAUDE_MD_PATH = join(homedir(), ".claude", "CLAUDE.md");
85
+ var SCRIPTS_DIR = join(homedir(), ".claude", "scripts");
86
+ var STATUSLINE_PATH = join(SCRIPTS_DIR, "statusline.py");
87
+ var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
88
+ var OLD_START_MARKER = "<!-- velocity-mcp:start -->";
89
+ var OLD_END_MARKER = "<!-- velocity-mcp:end -->";
90
+ var STATUSLINE_SCRIPT = readFileSync(
91
+ resolve(__dirname, "..", "templates", "statusline.py"),
92
+ "utf-8"
93
+ );
94
+ function installStatusLine() {
95
+ try {
96
+ mkdirSync(SCRIPTS_DIR, { recursive: true });
97
+ writeFileSync(STATUSLINE_PATH, STATUSLINE_SCRIPT, "utf-8");
98
+ chmodSync(STATUSLINE_PATH, 493);
99
+ let settings = {};
100
+ if (existsSync(SETTINGS_PATH)) {
101
+ try {
102
+ settings = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
103
+ } catch {
104
+ settings = {};
105
+ }
106
+ }
107
+ const statuslineCmd = `python3 ${STATUSLINE_PATH}`;
108
+ const currentCmd = settings.statusLine?.command;
109
+ if (currentCmd !== statuslineCmd) {
110
+ settings.statusLine = { type: "command", command: statuslineCmd };
111
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
112
+ }
113
+ return { installed: true, message: "Status line installed (proactive background scans)" };
114
+ } catch (err) {
115
+ return { installed: false, message: `Status line install failed: ${err.message}` };
116
+ }
117
+ }
118
+ function escapeRegex(str) {
119
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
120
+ }
121
+ function hasOldBlock(filePath) {
122
+ if (!existsSync(filePath)) return false;
123
+ const content = readFileSync(filePath, "utf-8");
124
+ return content.includes(OLD_START_MARKER) && content.includes(OLD_END_MARKER);
125
+ }
126
+ function removeOldBlock(filePath) {
127
+ if (!existsSync(filePath)) return;
128
+ let content = readFileSync(filePath, "utf-8");
129
+ const pattern = new RegExp(
130
+ `\\n?${escapeRegex(OLD_START_MARKER)}[\\s\\S]*?${escapeRegex(OLD_END_MARKER)}\\n?`,
131
+ "g"
132
+ );
133
+ content = content.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
134
+ writeFileSync(filePath, content, "utf-8");
135
+ }
136
+ async function install(options = {}) {
137
+ console.log("\n\u{1F527} WhenLabs toolkit installer\n");
138
+ const editorFlags = options.all ? ALL_EDITORS : [
139
+ options.cursor && "cursor",
140
+ options.vscode && "vscode",
141
+ options.windsurf && "windsurf"
142
+ ].filter(Boolean);
143
+ const claudeOnly = editorFlags.length === 0;
144
+ if (claudeOnly) {
145
+ const mcpResult = registerMcpServer();
146
+ console.log(mcpResult.success ? ` \u2713 ${mcpResult.message}` : ` \u2717 ${mcpResult.message}`);
147
+ injectBlock(CLAUDE_MD_PATH, CLAUDE_MD_CONTENT);
148
+ console.log(` \u2713 CLAUDE.md instructions written to ${CLAUDE_MD_PATH}`);
149
+ const slResult = installStatusLine();
150
+ console.log(slResult.installed ? ` \u2713 ${slResult.message}` : ` \u2717 ${slResult.message}`);
151
+ try {
152
+ const cwd = process.cwd();
153
+ execFileSync("npx", ["--yes", "@whenlabs/aware", "init", "--force"], {
154
+ cwd,
155
+ stdio: "pipe",
156
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
157
+ timeout: 3e4
158
+ });
159
+ execFileSync("npx", ["--yes", "@whenlabs/aware", "sync"], {
160
+ cwd,
161
+ stdio: "pipe",
162
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
163
+ timeout: 3e4
164
+ });
165
+ console.log(" \u2713 AI context files generated and synced (aware init + sync)");
166
+ } catch {
167
+ console.log(" - Skipped aware init (run `when aware init` in a project directory)");
168
+ }
169
+ if (hasOldBlock(CLAUDE_MD_PATH)) {
170
+ removeOldBlock(CLAUDE_MD_PATH);
171
+ console.log(" \u2713 Removed legacy velocity-mcp markers (migrated to whenlabs block)");
172
+ }
173
+ } else {
174
+ for (const editor of editorFlags) {
175
+ const result = installForEditor(editor);
176
+ console.log(result.success ? ` \u2713 ${result.message}` : ` \u2717 ${result.message}`);
177
+ }
178
+ }
179
+ console.log("\nInstallation complete. Run `when status` to verify.\n");
180
+ }
181
+ export {
182
+ install
183
+ };