lazyclaude-ai 0.1.7 → 0.1.9

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/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.7 - Unreleased
3
+ ## 0.1.9 - Unreleased
4
+
5
+ - Install a LazyClaude-branded Claude Code statusLine HUD automatically during
6
+ `lazyclaude install`, with safe previous-statusLine restoration on uninstall.
7
+
8
+ ## 0.1.8
9
+
10
+ - Teach `lazyclaude doctor` to separate LazyClaude's own LSP declaration from
11
+ external Claude Code LSP plugin binary warnings.
12
+
13
+ ## 0.1.7
4
14
 
5
15
  - Expand the LazyCodex port audit beyond skill frontmatter into upstream OMO
6
16
  component coverage.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
  <p align="center">
11
11
  <img src="https://img.shields.io/badge/npm-lazyclaude--ai-cb3837" />
12
- <img src="https://img.shields.io/badge/version-0.1.7-2ea44f" />
12
+ <img src="https://img.shields.io/badge/version-0.1.9-2ea44f" />
13
13
  <img src="https://img.shields.io/badge/Claude%20Code-plugin-blueviolet" />
14
14
  <img src="https://img.shields.io/badge/license-MIT-blue" />
15
15
  </p>
@@ -19,7 +19,7 @@
19
19
  > [!NOTE]
20
20
  > LazyClaude is a quiet personal distribution for bringing LazyCodex-style prompt engineering into Claude Code. It installs as `lazyclaude@lazyclaude-ai`, so normal `claude` launches can load the LazyClaude skills and hooks without a long `--plugin-dir` command.
21
21
 
22
- This checkout is prepared as `lazyclaude-ai@0.1.7` for personal install
22
+ This checkout is prepared as `lazyclaude-ai@0.1.9` for personal install
23
23
  convenience. The repo can remain private and quiet; publishing to npm here does
24
24
  not imply public repo promotion, marketplace publication, or advertisement.
25
25
  Future package releases still require explicit user approval.
@@ -44,6 +44,8 @@ Future package releases still require explicit user approval.
44
44
  - **Workflow agents** - planner, executor, verifier, reviewer, librarian, and QA runner
45
45
  - **Local marketplace registration** - Claude can resolve `claude plugin details lazyclaude@lazyclaude-ai`
46
46
  - **MCP and LSP helpers** - plugin-local stdio MCP plus TypeScript-family LSP doctor
47
+ - **LazyClaude HUD** - npm install configures a compact Claude Code statusLine
48
+ based on `pretty-claude-hud`, branded as `[LAZYCLAUDE vX.Y.Z]`
47
49
  - **Safe uninstall** - removes only LazyClaude-managed state
48
50
 
49
51
  ## Quick Start
@@ -55,14 +57,14 @@ npx --yes lazyclaude-ai install
55
57
  ```
56
58
 
57
59
  If you are currently inside this repository checkout, which has the same
58
- package name as the registry package, `npx --yes lazyclaude-ai@0.1.7 install`
60
+ package name as the registry package, `npx --yes lazyclaude-ai@0.1.9 install`
59
61
  can resolve the local same-name source checkout and fail with
60
62
  `sh: lazyclaude-ai: command not found`. From a fresh directory, the normal
61
63
  install command works:
62
64
 
63
65
  ```bash
64
66
  cd /tmp
65
- npx --yes lazyclaude-ai@0.1.7 install
67
+ npx --yes lazyclaude-ai@0.1.9 install
66
68
  ```
67
69
 
68
70
  Validate the installed plugin:
@@ -71,6 +73,13 @@ Validate the installed plugin:
71
73
  npx --yes lazyclaude-ai doctor
72
74
  ```
73
75
 
76
+ The installer also sets Claude Code's `statusLine` command to the packaged
77
+ LazyClaude HUD. A typical no-color render starts like:
78
+
79
+ ```text
80
+ [LAZYCLAUDE v0.1.9] | O4.8 │ ctx ▄░░░░░ 9%/1000k │ 5h [░░░░░]4% │ 1w [█░░░░]35% │ git main +3 ✓
81
+ ```
82
+
74
83
  Launch Claude Code normally:
75
84
 
76
85
  ```bash
package/README_ko-KR.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
  <p align="center">
11
11
  <img src="https://img.shields.io/badge/npm-lazyclaude--ai-cb3837" />
12
- <img src="https://img.shields.io/badge/version-0.1.7-2ea44f" />
12
+ <img src="https://img.shields.io/badge/version-0.1.9-2ea44f" />
13
13
  <img src="https://img.shields.io/badge/Claude%20Code-plugin-blueviolet" />
14
14
  <img src="https://img.shields.io/badge/license-MIT-blue" />
15
15
  </p>
@@ -23,7 +23,7 @@
23
23
  > [!NOTE]
24
24
  > LazyClaude는 LazyCodex 스타일의 prompt engineering을 Claude Code로 가져오기 위한 조용한 개인용 배포판입니다. `lazyclaude@lazyclaude-ai`로 설치되므로, 매번 긴 `--plugin-dir` 없이 일반 `claude` 실행에서 LazyClaude skill과 hook을 불러올 수 있습니다.
25
25
 
26
- 현재 checkout은 `lazyclaude-ai@0.1.7` 배포용으로 준비되어 있습니다. 목적은 다른 PC에서도
26
+ 현재 checkout은 `lazyclaude-ai@0.1.9` 배포용으로 준비되어 있습니다. 목적은 다른 PC에서도
27
27
  빠르게 설치하기 위한 개인용 배포물입니다. 저장소는 비공개 저장소로
28
28
  유지할 수 있고, npm 배포가 곧 홍보, 공개 저장소 운영, Claude marketplace
29
29
  등록을 의미하지는 않습니다. 새 버전 배포는 항상 별도의 명시적 승인 후에
@@ -48,6 +48,8 @@
48
48
  - **워크플로우 agents** - planner, executor, verifier, reviewer, librarian, QA runner
49
49
  - **Local marketplace 등록** - `claude plugin details lazyclaude@lazyclaude-ai`에서 inventory 확인
50
50
  - **MCP와 LSP helper** - plugin-local stdio MCP와 TypeScript 계열 LSP doctor
51
+ - **LazyClaude HUD** - npm install 시 `pretty-claude-hud` 기반의 compact
52
+ Claude Code statusLine을 `[LAZYCLAUDE vX.Y.Z]` 브랜드로 자동 설정
51
53
  - **안전한 uninstall** - LazyClaude가 관리한 상태만 제거
52
54
 
53
55
  ## 빠른 시작
@@ -60,13 +62,13 @@ npx --yes lazyclaude-ai install
60
62
 
61
63
  현재 위치가 이 저장소 checkout이면, registry package와 같은 package name을
62
64
  가진 same-name source checkout 안에 있는 상태입니다. 이 경우
63
- `npx --yes lazyclaude-ai@0.1.7 install`이 local checkout을 먼저 해석해서
65
+ `npx --yes lazyclaude-ai@0.1.9 install`이 local checkout을 먼저 해석해서
64
66
  `sh: lazyclaude-ai: command not found`로 실패할 수 있습니다. 새 폴더에서는
65
67
  일반 설치 명령이 정상 동작합니다.
66
68
 
67
69
  ```bash
68
70
  cd /tmp
69
- npx --yes lazyclaude-ai@0.1.7 install
71
+ npx --yes lazyclaude-ai@0.1.9 install
70
72
  ```
71
73
 
72
74
  설치 상태를 확인합니다.
@@ -75,6 +77,13 @@ npx --yes lazyclaude-ai@0.1.7 install
75
77
  npx --yes lazyclaude-ai doctor
76
78
  ```
77
79
 
80
+ installer는 Claude Code의 `statusLine` command도 packaged LazyClaude HUD로
81
+ 설정합니다. 색상을 제거한 예시는 다음처럼 시작합니다.
82
+
83
+ ```text
84
+ [LAZYCLAUDE v0.1.9] | O4.8 │ ctx ▄░░░░░ 9%/1000k │ 5h [░░░░░]4% │ 1w [█░░░░]35% │ git main +3 ✓
85
+ ```
86
+
78
87
  설치 후에는 일반적인 `claude` 명령으로 Claude Code를 실행합니다.
79
88
 
80
89
  ```bash
@@ -1,7 +1,7 @@
1
1
  # LazyClaude Release Checklist
2
2
 
3
3
  Status: this checkout is prepared for quiet public npm package
4
- `lazyclaude-ai@0.1.7` after explicit user approval.
4
+ `lazyclaude-ai@0.1.9` after explicit user approval.
5
5
 
6
6
  DO NOT publish a new version of LazyClaude, run `npm publish`, push release
7
7
  tags, or add a remote Claude Code marketplace entry without explicit user
@@ -44,7 +44,7 @@ to type a generated command that shells out to `npx --yes lazyclaude-ai path` fo
44
44
  normal npm installs.
45
45
 
46
46
  Run fresh-machine QA from a fresh directory, not from this repository checkout.
47
- Inside the same-name source checkout, `npx --yes lazyclaude-ai@0.1.7 install`
47
+ Inside the same-name source checkout, `npx --yes lazyclaude-ai@0.1.9 install`
48
48
  can resolve the local package and fail with `sh: lazyclaude-ai: command not
49
49
  found`. Use `cd /tmp` for the fresh directory scenario, or use the explicit
50
50
  fresh-prefix form:
@@ -51,6 +51,13 @@ const intendedPluginPath = (home = claudeHome()) => claudePluginRoot(home);
51
51
  const marketplaceRoot = (home = lazyHome()) => join(home, "marketplaces", marketplaceName);
52
52
  const marketplacePluginPath = (home = lazyHome()) => join(marketplaceRoot(home), "plugins", "lazyclaude");
53
53
  const sourcePluginPath = () => join(root, "plugins", "lazyclaude");
54
+ const lazyClaudeSettingsKey = "lazyclaude";
55
+ const knownExternalLspPlugins = {
56
+ "typescript-lsp@claude-plugins-official": ["typescript-language-server"],
57
+ "rust-analyzer-lsp@claude-plugins-official": ["rust-analyzer"],
58
+ "pyright-lsp@claude-plugins-official": ["pyright-langserver", "pyright"],
59
+ "gopls-lsp@claude-plugins-official": ["gopls"],
60
+ };
54
61
 
55
62
  const printUsage = () => {
56
63
  process.stderr.write(usage);
@@ -92,6 +99,8 @@ const writeClaudeSettings = (settings, home = claudeHome()) => {
92
99
  writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
93
100
  };
94
101
 
102
+ const hudCommandForPlugin = (pluginPath) => `node "${join(pluginPath, "bin", "lazyclaude-hud.js")}"`;
103
+
95
104
  const readKnownMarketplaces = (home = claudeHome()) => {
96
105
  const path = knownMarketplacesPath(home);
97
106
  if (!existsSync(path)) return {};
@@ -166,6 +175,70 @@ const registerMarketplace = (marketplacePath, home = claudeHome()) => {
166
175
  writeKnownMarketplaces(knownMarketplaces, home);
167
176
  };
168
177
 
178
+ const installHudStatusLine = (pluginPath, home = claudeHome()) => {
179
+ const settings = readClaudeSettings(home);
180
+ const command = hudCommandForPlugin(pluginPath);
181
+ const existingLazyClaude = settings[lazyClaudeSettingsKey] ?? {};
182
+ const now = new Date().toISOString();
183
+ const previousStatusLine =
184
+ existingLazyClaude.statusLineManaged === true
185
+ ? existingLazyClaude.previousStatusLine
186
+ : settings.statusLine;
187
+
188
+ settings.statusLine = {
189
+ type: "command",
190
+ command,
191
+ };
192
+ settings[lazyClaudeSettingsKey] = {
193
+ ...existingLazyClaude,
194
+ statusLineManaged: true,
195
+ statusLineCommand: command,
196
+ statusLineVersion: version,
197
+ statusLineInstalledAt: existingLazyClaude.statusLineInstalledAt ?? now,
198
+ statusLineUpdatedAt: now,
199
+ previousStatusLine: previousStatusLine ?? null,
200
+ };
201
+ writeClaudeSettings(settings, home);
202
+ };
203
+
204
+ const uninstallHudStatusLine = (home = claudeHome()) => {
205
+ const settingsPath = claudeSettingsPath(home);
206
+ if (!existsSync(settingsPath)) return;
207
+
208
+ const settings = readClaudeSettings(home);
209
+ const lazyClaude = settings[lazyClaudeSettingsKey];
210
+ if (lazyClaude?.statusLineManaged !== true) return;
211
+
212
+ if (settings.statusLine?.command === lazyClaude.statusLineCommand) {
213
+ if (lazyClaude.previousStatusLine) {
214
+ settings.statusLine = lazyClaude.previousStatusLine;
215
+ } else {
216
+ delete settings.statusLine;
217
+ }
218
+ } else if (settings.statusLine?.command) {
219
+ process.stdout.write("HUD_WARNING: statusLine changed by user; leaving it untouched\n");
220
+ }
221
+
222
+ const remainingLazyClaude = { ...lazyClaude };
223
+ for (const key of [
224
+ "statusLineManaged",
225
+ "statusLineCommand",
226
+ "statusLineVersion",
227
+ "statusLineInstalledAt",
228
+ "statusLineUpdatedAt",
229
+ "previousStatusLine",
230
+ ]) {
231
+ delete remainingLazyClaude[key];
232
+ }
233
+
234
+ if (Object.keys(remainingLazyClaude).length > 0) {
235
+ settings[lazyClaudeSettingsKey] = remainingLazyClaude;
236
+ } else {
237
+ delete settings[lazyClaudeSettingsKey];
238
+ }
239
+ writeClaudeSettings(settings, home);
240
+ };
241
+
169
242
  const unregisterMarketplace = (home = claudeHome()) => {
170
243
  const settingsPath = claudeSettingsPath(home);
171
244
  if (existsSync(settingsPath)) {
@@ -200,6 +273,7 @@ const registerClaudePlugin = (installRoot, marketplacePath, home = claudeHome())
200
273
  ];
201
274
  writeInstalledPlugins(registry, home);
202
275
  registerMarketplace(marketplacePath, home);
276
+ installHudStatusLine(installRoot, home);
203
277
  };
204
278
 
205
279
  const unregisterClaudePlugin = (home = claudeHome()) => {
@@ -221,6 +295,40 @@ const requireInstalledPluginPath = (home = claudeHome()) => {
221
295
  return path;
222
296
  };
223
297
 
298
+ const commandExists = (command) => {
299
+ const result = spawnSync(command, ["--version"], {
300
+ encoding: "utf8",
301
+ stdio: "ignore",
302
+ });
303
+ return !result.error;
304
+ };
305
+
306
+ const printLspDiagnostics = (pluginPath, registry) => {
307
+ const lspPath = join(pluginPath, ".lsp.json");
308
+ const config = JSON.parse(readFileSync(lspPath, "utf8"));
309
+ const lazyClaudeServers = Object.keys(config);
310
+ process.stdout.write(`LAZYCLAUDE_LSP_SERVERS: ${lazyClaudeServers.join(", ") || "none"}\n`);
311
+
312
+ for (const server of lazyClaudeServers) {
313
+ const command = config[server]?.command?.[0];
314
+ if (command && !commandExists(command)) {
315
+ process.stdout.write(`LAZYCLAUDE_LSP_WARNING: ${server} requires ${command}; install it or remove that server from .lsp.json\n`);
316
+ }
317
+ }
318
+
319
+ const externalWarnings = [];
320
+ for (const [key, commands] of Object.entries(knownExternalLspPlugins)) {
321
+ if (!registry.plugins[key]?.length) continue;
322
+ if (commands.some(commandExists)) continue;
323
+ externalWarnings.push({ key, command: commands[0] });
324
+ process.stdout.write(`EXTERNAL_LSP_WARNING: ${key} requires ${commands[0]}\n`);
325
+ }
326
+
327
+ if (externalWarnings.length > 0) {
328
+ process.stdout.write("These warnings are not emitted by LazyClaude's .lsp.json; they come from other installed Claude Code LSP plugins.\n");
329
+ }
330
+ };
331
+
224
332
  const resetCurrentPointer = (home, target) => {
225
333
  const current = currentRoot(home);
226
334
  rmSync(current, { recursive: true, force: true });
@@ -244,6 +352,7 @@ const install = ({ dryRun }) => {
244
352
  process.stdout.write(`Would copy: ${sourcePlugin} -> ${targetPlugin}\n`);
245
353
  process.stdout.write(`Would create local marketplace: ${targetMarketplace}\n`);
246
354
  process.stdout.write(`Would register Claude plugin: ${pluginKey}\n`);
355
+ process.stdout.write(`Would set Claude statusLine HUD: ${hudCommandForPlugin(targetPlugin)}\n`);
247
356
  process.stdout.write("Launch with: claude\n");
248
357
  return;
249
358
  }
@@ -268,6 +377,7 @@ const install = ({ dryRun }) => {
268
377
  process.stdout.write(`Claude plugin: ${pluginKey}\n`);
269
378
  process.stdout.write(`Marketplace: ${localMarketplace}\n`);
270
379
  process.stdout.write(`Plugin path: ${intendedPluginPath(home)}\n`);
380
+ process.stdout.write("HUD: LazyClaude statusLine installed\n");
271
381
  process.stdout.write("Launch with: claude\n");
272
382
  };
273
383
 
@@ -278,11 +388,13 @@ const doctor = ({ dryRun }) => {
278
388
  process.stdout.write(`Would check plugin files under: ${pluginPath}\n`);
279
389
  process.stdout.write(`Would run: claude plugin validate ${pluginPath}\n`);
280
390
  process.stdout.write(`Would check Claude plugin registry: ${pluginKey}\n`);
391
+ process.stdout.write(`Would check Claude statusLine HUD: ${hudCommandForPlugin(pluginPath)}\n`);
281
392
  return;
282
393
  }
283
394
 
284
395
  const pluginPath = requireInstalledPluginPath();
285
- const registryEntry = readInstalledPlugins().plugins[pluginKey]?.[0];
396
+ const registry = readInstalledPlugins();
397
+ const registryEntry = registry.plugins[pluginKey]?.[0];
286
398
  if (!registryEntry) fail(`LazyClaude is missing from Claude plugin registry: ${pluginKey}`);
287
399
  const requiredFiles = [
288
400
  ".claude-plugin/plugin.json",
@@ -300,6 +412,8 @@ const doctor = ({ dryRun }) => {
300
412
  if (!existsSync(path)) fail(`LazyClaude install is missing ${file}. Run \`lazyclaude install\` again.`);
301
413
  }
302
414
 
415
+ printLspDiagnostics(pluginPath, registry);
416
+
303
417
  const claude = spawnSync("claude", ["--version"], { encoding: "utf8" });
304
418
  if (claude.error) {
305
419
  process.stdout.write("CLAUDE_WARNING: claude executable not found in PATH\n");
@@ -322,6 +436,12 @@ const doctor = ({ dryRun }) => {
322
436
  }
323
437
 
324
438
  process.stdout.write(`Plugin path: ${pluginPath}\n`);
439
+ const settings = readClaudeSettings();
440
+ if (settings.statusLine?.command === hudCommandForPlugin(pluginPath)) {
441
+ process.stdout.write("HUD_STATUSLINE_PASS\n");
442
+ } else {
443
+ process.stdout.write("HUD_STATUSLINE_WARNING: LazyClaude HUD is not the active Claude statusLine\n");
444
+ }
325
445
  process.stdout.write("Launch with: claude\n");
326
446
  process.stdout.write("DOCTOR_PASS\n");
327
447
  };
@@ -359,6 +479,7 @@ const uninstall = ({ dryRun }) => {
359
479
  rmSync(marketplaceRoot(home), { recursive: true, force: true });
360
480
  unregisterClaudePlugin();
361
481
  unregisterMarketplace();
482
+ uninstallHudStatusLine();
362
483
  rmSync(pluginCacheRoot, { recursive: true, force: true });
363
484
  process.stdout.write("UNINSTALL_PASS\n");
364
485
  };
package/docs/lsp.md CHANGED
@@ -25,6 +25,11 @@ If `typescript-language-server` is unavailable, the doctor exits successfully
25
25
  with installation guidance so local smoke tests can distinguish an environment
26
26
  gap from a plugin failure.
27
27
 
28
+ `lazyclaude doctor` also prints the LazyClaude LSP server names from the
29
+ installed `.lsp.json`. If Claude Code reports load errors for official LSP
30
+ plugins such as `rust-analyzer-lsp`, `pyright-lsp`, or `gopls-lsp`, LazyClaude
31
+ reports those as external binary warnings rather than LazyClaude plugin errors.
32
+
28
33
  ## Claude Code Reload
29
34
 
30
35
  After changing `.lsp.json`, restart Claude Code with the local plugin directory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaude-ai",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Claude Code-native LazyCodex-style workflow distribution.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "scripts": {
25
+ "postinstall": "node scripts/postinstall.mjs",
25
26
  "test": "node --test test/*.test.mjs",
26
27
  "validate:plugin": "node scripts/validate-plugin.mjs",
27
28
  "doctor": "node scripts/doctor.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lazyclaude",
3
3
  "description": "Claude Code-native LazyCodex-style workflow plugin.",
4
- "version": "0.1.7",
4
+ "version": "0.1.9",
5
5
  "author": {
6
6
  "name": "LazyClaude contributors"
7
7
  },
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
9
+ const pluginManifestPath = join(pluginRoot, ".claude-plugin", "plugin.json");
10
+ const noColor = process.env.NO_COLOR || process.env.LAZYCLAUDE_HUD_NO_COLOR === "1";
11
+ const sep = process.env.LAZYCLAUDE_HUD_SEP || "│";
12
+
13
+ const colors = noColor
14
+ ? { reset: "", bold: "", dim: "", accent: "", empty: "", green: "", yellow: "", red: "" }
15
+ : {
16
+ reset: "\x1b[0m",
17
+ bold: "\x1b[1m",
18
+ dim: "\x1b[38;5;245m",
19
+ accent: "\x1b[38;5;81m",
20
+ empty: "\x1b[38;5;238m",
21
+ green: "\x1b[38;5;71m",
22
+ yellow: "\x1b[38;5;178m",
23
+ red: "\x1b[38;5;167m",
24
+ };
25
+
26
+ const readStdin = async () => {
27
+ let input = "";
28
+ for await (const chunk of process.stdin) input += chunk;
29
+ return input;
30
+ };
31
+
32
+ const readVersion = () => {
33
+ if (process.env.LAZYCLAUDE_VERSION) return process.env.LAZYCLAUDE_VERSION;
34
+ try {
35
+ return JSON.parse(readFileSync(pluginManifestPath, "utf8")).version ?? "?";
36
+ } catch {
37
+ return "?";
38
+ }
39
+ };
40
+
41
+ const shortModel = (raw) => {
42
+ const compact = String(raw || "?")
43
+ .replace(/\([^)]*\)/gu, "")
44
+ .replace(/\[[^\]]*\]/gu, "")
45
+ .replace(/\s+/gu, " ")
46
+ .trim();
47
+ const number = compact.match(/[0-9]+(?:\.[0-9]+)?/u)?.[0] ?? "";
48
+ if (/opus/iu.test(compact)) return `O${number}`;
49
+ if (/sonnet/iu.test(compact)) return `S${number}`;
50
+ if (/haiku/iu.test(compact)) return `H${number}`;
51
+ return compact.slice(0, 10) || "?";
52
+ };
53
+
54
+ const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, Number.isFinite(value) ? value : min));
55
+
56
+ const barColor = (value) => {
57
+ if (value >= 90) return colors.red;
58
+ if (value >= 70) return colors.yellow;
59
+ return colors.green;
60
+ };
61
+
62
+ const makeBlockBar = (value, width = 5) => {
63
+ const pct = clamp(Math.round(value));
64
+ const filled = Math.floor((pct * width) / 100);
65
+ return `${barColor(pct)}${"█".repeat(filled)}${colors.empty}${"░".repeat(width - filled)}${colors.reset}`;
66
+ };
67
+
68
+ const makeContextBar = (value, width = 6) => {
69
+ const pct = clamp(Math.round(value));
70
+ const step = 100 / width;
71
+ let output = "";
72
+ for (let i = 0; i < width; i += 1) {
73
+ const progress = pct - i * step;
74
+ if (progress >= step * 0.8) output += `${colors.accent}█${colors.reset}`;
75
+ else if (progress >= step * 0.3) output += `${colors.accent}▄${colors.reset}`;
76
+ else output += `${colors.empty}░${colors.reset}`;
77
+ }
78
+ return output;
79
+ };
80
+
81
+ const latestUsageTokens = (transcriptPath) => {
82
+ if (!transcriptPath || !existsSync(transcriptPath)) return 0;
83
+ const lines = readFileSync(transcriptPath, "utf8").split(/\r?\n/u).filter(Boolean);
84
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
85
+ try {
86
+ const event = JSON.parse(lines[index]);
87
+ const usage = event?.message?.usage;
88
+ if (!usage || event.isSidechain === true || event.isApiErrorMessage === true) continue;
89
+ return (
90
+ Number(usage.input_tokens ?? 0) +
91
+ Number(usage.cache_read_input_tokens ?? 0) +
92
+ Number(usage.cache_creation_input_tokens ?? 0)
93
+ );
94
+ } catch {
95
+ }
96
+ }
97
+ return 0;
98
+ };
99
+
100
+ const latestUserMessage = (transcriptPath) => {
101
+ if (!transcriptPath || !existsSync(transcriptPath)) return "";
102
+ const lines = readFileSync(transcriptPath, "utf8").split(/\r?\n/u).filter(Boolean);
103
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
104
+ try {
105
+ const event = JSON.parse(lines[index]);
106
+ if (event.type !== "user") continue;
107
+ const content = event?.message?.content;
108
+ const text = Array.isArray(content)
109
+ ? content.filter((part) => part?.type === "text").map((part) => part.text).join(" ")
110
+ : content;
111
+ const normalized = String(text || "").replace(/\s+/gu, " ").trim();
112
+ if (normalized && !normalized.startsWith("[Request interrupted") && !normalized.startsWith("[Request cancelled")) {
113
+ return normalized;
114
+ }
115
+ } catch {
116
+ }
117
+ }
118
+ return "";
119
+ };
120
+
121
+ const gitStatus = (cwd) => {
122
+ if (!cwd || !existsSync(cwd)) return "";
123
+ try {
124
+ const branch = execFileSync("git", ["-C", cwd, "branch", "--show-current"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
125
+ if (!branch) return "";
126
+ const status = execFileSync("git", ["-C", cwd, "--no-optional-locks", "status", "--porcelain", "-uall"], {
127
+ encoding: "utf8",
128
+ stdio: ["ignore", "pipe", "ignore"],
129
+ }).trim();
130
+ const count = status ? status.split(/\r?\n/u).length : 0;
131
+ let sync = "✓";
132
+ try {
133
+ execFileSync("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "@{upstream}"], { stdio: "ignore" });
134
+ const [ahead, behind] = execFileSync("git", ["-C", cwd, "rev-list", "--left-right", "--count", "HEAD...@{upstream}"], {
135
+ encoding: "utf8",
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ }).trim().split(/\s+/u).map(Number);
138
+ sync = `${ahead > 0 ? `↑${ahead}` : ""}${behind > 0 ? `↓${behind}` : ""}` || "✓";
139
+ } catch {
140
+ sync = "✓";
141
+ }
142
+ return `${branch}${count > 0 ? ` +${count}` : ""} ${sync}`;
143
+ } catch {
144
+ return "";
145
+ }
146
+ };
147
+
148
+ const parseUsage = () => {
149
+ const raw = process.env.LAZYCLAUDE_HUD_TEST_USAGE || process.env.CLAUDE_HUD_TEST_USAGE || "";
150
+ const values = {};
151
+ for (const part of raw.split(",")) {
152
+ const [key, value] = part.split("=");
153
+ if (!key || value === undefined) continue;
154
+ values[key.trim()] = value.trim();
155
+ }
156
+ return {
157
+ fiveHour: values["5h"] === undefined ? null : clamp(Number(values["5h"])),
158
+ weekly: values["1w"] === undefined && values.wk === undefined ? null : clamp(Number(values["1w"] ?? values.wk)),
159
+ weeklyReset: values.reset1 ?? values.resetw ?? "",
160
+ };
161
+ };
162
+
163
+ const formatReset = (iso) => {
164
+ if (!iso) return "";
165
+ const target = Date.parse(iso);
166
+ if (!Number.isFinite(target)) return "";
167
+ const minutes = Math.max(0, Math.floor((target - Date.now()) / 60000));
168
+ const hours = Math.floor(minutes / 60);
169
+ const days = Math.floor(hours / 24);
170
+ if (days > 0) return `(${days}d${hours % 24}h)`;
171
+ if (hours > 0) return `(${hours}h${minutes % 60}m)`;
172
+ return `(${minutes}m)`;
173
+ };
174
+
175
+ const main = async () => {
176
+ const input = await readStdin();
177
+ const status = input.trim() ? JSON.parse(input) : {};
178
+ const version = readVersion();
179
+ const model = shortModel(status.model?.display_name ?? status.model?.id);
180
+ const maxContext = Number(status.context_window?.context_window_size ?? 200000);
181
+ const maxK = Math.max(1, Math.round(maxContext / 1000));
182
+ const tokens = latestUsageTokens(status.transcript_path);
183
+ const estimatedTokens = tokens > 0 ? tokens : 20000;
184
+ const contextPct = clamp(Math.floor((estimatedTokens * 100) / maxContext));
185
+ const usage = parseUsage();
186
+ const usageText =
187
+ usage.fiveHour === null || usage.weekly === null
188
+ ? ""
189
+ : ` ${sep} 5h [${makeBlockBar(usage.fiveHour)}]${barColor(usage.fiveHour)}${usage.fiveHour}%${colors.reset} ${sep} 1w [${makeBlockBar(usage.weekly)}]${barColor(usage.weekly)}${usage.weekly}%${colors.dim}${formatReset(usage.weeklyReset)}${colors.reset}`;
190
+ const git = gitStatus(status.cwd);
191
+ const prefix = `${colors.bold}${colors.accent}[LAZYCLAUDE v${version}]${colors.reset}`;
192
+ const line = `${prefix} | ${colors.accent}${model}${colors.dim} ${sep} ctx ${makeContextBar(contextPct)} ${contextPct}%/${maxK}k${colors.reset}${usageText}${git ? ` ${sep} git ${git}` : ""}`;
193
+ process.stdout.write(`${line}\n`);
194
+
195
+ const lastMessage = latestUserMessage(status.transcript_path);
196
+ if (lastMessage) {
197
+ const trimmed = lastMessage.length > 120 ? `${lastMessage.slice(0, 117)}...` : lastMessage;
198
+ process.stdout.write(`└─ ${trimmed}\n`);
199
+ }
200
+ };
201
+
202
+ main().catch((error) => {
203
+ process.stdout.write(`[LAZYCLAUDE v${readVersion()}] | HUD unavailable: ${error.message}\n`);
204
+ process.exit(0);
205
+ });
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
+
10
+ const skip = (reason) => {
11
+ process.stdout.write(`LazyClaude postinstall skipped: ${reason}\n`);
12
+ process.stdout.write("Run `lazyclaude install` to register the Claude Code plugin and HUD.\n");
13
+ process.exit(0);
14
+ };
15
+
16
+ if (process.env.LAZYCLAUDE_AUTO_INSTALL === "0" || process.env.LAZYCLAUDE_POSTINSTALL_SKIP === "1") {
17
+ skip("disabled by environment");
18
+ }
19
+
20
+ const forced = process.env.LAZYCLAUDE_AUTO_INSTALL === "1";
21
+ if (process.env.CI && !forced) {
22
+ skip("CI environment");
23
+ }
24
+
25
+ if (existsSync(join(root, ".git")) && !forced) {
26
+ skip("source checkout");
27
+ }
28
+
29
+ if (process.env.npm_config_global !== "true" && process.env.npm_config_location !== "global" && !forced) {
30
+ skip("non-global package install");
31
+ }
32
+
33
+ const result = spawnSync(process.execPath, [join(root, "bin", "lazyclaude-ai.js"), "install"], {
34
+ cwd: root,
35
+ encoding: "utf8",
36
+ env: {
37
+ ...process.env,
38
+ LAZYCLAUDE_POSTINSTALL: "1",
39
+ },
40
+ });
41
+
42
+ if (result.status === 0) {
43
+ if (result.stdout) process.stdout.write(result.stdout);
44
+ process.exit(0);
45
+ }
46
+
47
+ process.stdout.write("LazyClaude postinstall warning: automatic Claude Code plugin/HUD setup did not complete.\n");
48
+ if (result.stderr) process.stdout.write(result.stderr);
49
+ process.stdout.write("Run `lazyclaude install` manually after npm finishes.\n");
50
+ process.exit(0);