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 +11 -1
- package/README.md +13 -4
- package/README_ko-KR.md +13 -4
- package/RELEASE_CHECKLIST.md +2 -2
- package/bin/lazyclaude-ai.js +122 -1
- package/docs/lsp.md +5 -0
- package/package.json +2 -1
- package/plugins/lazyclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/lazyclaude/bin/lazyclaude-hud.js +205 -0
- package/scripts/postinstall.mjs +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/RELEASE_CHECKLIST.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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:
|
package/bin/lazyclaude-ai.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|
|
@@ -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);
|