codebyplan 1.13.22 → 1.13.24
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 +0 -109
- package/dist/cli.js +476 -690
- package/package.json +1 -1
- package/templates/hooks/README.md +3 -53
- package/templates/hooks/cbp-statusline.mjs +106 -11
- package/templates/hooks/cbp-statusline.py +79 -13
- package/templates/hooks/cbp-statusline.sh +97 -17
- package/templates/hooks/cbp-test-hooks.sh +0 -71
- package/templates/hooks/hooks.json +0 -20
- package/templates/settings.project.base.json +0 -7
- package/templates/skills/cbp-round-execute/SKILL.md +0 -18
- package/templates/skills/cbp-session-start/SKILL.md +2 -0
- package/templates/skills/cbp-task-complete/SKILL.md +0 -14
- package/templates/skills/cbp-task-start/SKILL.md +0 -8
- package/templates/hooks/cbp-cmux-branch-watch.sh +0 -39
- package/templates/hooks/cbp-cmux-workspace-sync.sh +0 -19
- package/templates/skills/cbp-setup-cmux/SKILL.md +0 -170
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON fro
|
|
|
18
18
|
|
|
19
19
|
**Render lines:**
|
|
20
20
|
|
|
21
|
-
- **Line 1 — Identity**: **folder name** (basename of `cwd`) + **git branch** (always shown — taken from `worktree.branch`, else derived via `git -C <cwd> rev-parse --abbrev-ref HEAD`, so it appears even without a registered worktree); single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model **display name only**; `effort:LEVEL` (when set); `
|
|
21
|
+
- **Line 1 — Identity**: **folder name** (basename of `cwd`) + **git branch** (always shown — taken from `worktree.branch`, else derived via `git -C <cwd> rev-parse --abbrev-ref HEAD`, so it appears even without a registered worktree); single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model **display name only**; `effort:LEVEL` (when set); `style:NAME` (when output style is not "default"); `[VIM_MODE]`
|
|
22
22
|
- **Line 2 — Context**: 20-character progress bar (green / yellow / red at 50% / 75%); `used% / ctx_size`; per-call token breakdown — `in:N out:N cache_cr:N cache_rd:N` (from `context_window.current_usage`); `⚠ 200k+` banner when `exceeds_200k_tokens` is true
|
|
23
23
|
- **Line 3 — Cost**: session cost in USD; total wall duration (`dur:`) and API-only duration (`api:`) via human-readable format; lines added/removed
|
|
24
24
|
- **Line 4 — Rate limits**: `5h: PCT% (resets in Xh)` and `7d: PCT% (resets in Xd)` with relative-time helper; colour-coded green / yellow / red at 60% / 80%; emitted only when at least one window has a non-zero `resets_at` epoch
|
|
@@ -224,63 +224,13 @@ After a `complete_round` MCP call succeeds, reconciles the round's `files_change
|
|
|
224
224
|
|
|
225
225
|
---
|
|
226
226
|
|
|
227
|
-
### `cbp-cmux-workspace-sync.sh` — SessionStart, matcher `*`
|
|
228
|
-
|
|
229
|
-
On every session start, syncs the active [cmux](https://github.com/nicholasgasior/cmux) workspace title to the current git branch, the workspace description to the repo folder basename (the directory that contains `.git/`), and applies the workspace color from `.codebyplan/cmux.json` via `cmux workspace-action --action set-color`. All three actions are delegated to `codebyplan cmux-sync`. If no `workspace_color` is configured, a one-line nudge is printed to stdout prompting the user to run `/cbp-setup-cmux`.
|
|
230
|
-
|
|
231
|
-
**Blocks vs warns**: never blocks — exit 0 on every path. A SessionStart hook must never prevent a session from opening.
|
|
232
|
-
|
|
233
|
-
**Skips when**: `$CMUX_WORKSPACE_ID` is unset (not running inside a cmux workspace). No-ops silently — cmux is an optional integration and repos without it are completely unaffected.
|
|
234
|
-
|
|
235
|
-
**cmux binary resolution order**: `$CMUX_BUNDLED_CLI_PATH` → `$CMUX_CLAUDE_HOOK_CMUX_BIN` → `cmux` on `$PATH`. All three are resolved by the `codebyplan cmux-sync` subcommand; the hook itself does not replicate this logic.
|
|
236
|
-
|
|
237
|
-
**Opt out**: settings.json override removing this entry, or plugin disable.
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
### `cbp-cmux-branch-watch.sh` — PostToolUse, matcher `Bash`
|
|
242
|
-
|
|
243
|
-
After any Bash tool call that contains a `git checkout` or `git switch` invocation, syncs the active cmux workspace title to the current branch and the workspace description to the repo folder basename. The match is broad — it also fires on file-restore forms like `git checkout -- <file>` — but `codebyplan cmux-sync` is idempotent, so a redundant sync on a non-branch-change is harmless, and a real branch change is never missed.
|
|
244
|
-
|
|
245
|
-
**Blocks vs warns**: never blocks — exit 0 on every path. PostToolUse hooks must never abort Claude.
|
|
246
|
-
|
|
247
|
-
**Skips when**: `$CMUX_WORKSPACE_ID` is unset; or the Bash command contains no `git checkout` / `git switch`; or `jq` is not on `$PATH` (safe parse failure → exit 0). Outside cmux or on commands without checkout/switch, the hook exits immediately with no work done.
|
|
248
|
-
|
|
249
|
-
**cmux binary resolution order**: same as `cbp-cmux-workspace-sync.sh` — delegated to `codebyplan cmux-sync`.
|
|
250
|
-
|
|
251
|
-
**Opt out**: settings.json override removing the PostToolUse Bash entry for this hook, or plugin disable.
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
|
|
255
|
-
### Auto dev server (`codebyplan cmux-serve`)
|
|
256
|
-
|
|
257
|
-
At the start of each round, `cbp-round-execute` (Step 2a) calls `codebyplan cmux-serve --files "<round files>"` to auto-start the dev server for any app whose source files are touched. The subcommand probes each allocated port via `node:net`, starts a `cmux new-split` terminal pane + sends the dev command for any non-listening app, then opens a browser pane. If the port is already listening (another worktree) it only opens the browser pane. No hook registration is needed — the skill invokes the subcommand directly. Gated by `auto_dev_server` in `.codebyplan/cmux.json`; no-op outside cmux.
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
### Status surface (`codebyplan cmux-status`)
|
|
262
|
-
|
|
263
|
-
The lifecycle skills push CodeByPlan development state into the cmux workspace sidebar via `codebyplan cmux-status`. No hook registration is needed — the skills invoke the subcommand directly:
|
|
264
|
-
|
|
265
|
-
| Skill | What is pushed |
|
|
266
|
-
| --- | --- |
|
|
267
|
-
| `cbp-task-start` (Step 4.5) | `--checkpoint "CHK-NNN: title" --task "TASK-N: title"` |
|
|
268
|
-
| `cbp-task-complete` (Step 7.3) | `--task "TASK-N: title done" --progress completed/total` |
|
|
269
|
-
| `cbp-round-execute` (Step 3d) | `--qa "R{n} {status}"` where status ∈ completed / blocked / re-triggering |
|
|
270
|
-
|
|
271
|
-
**`auto_status` toggle.** Gated by the `auto_status` field in `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). When `auto_status` is `false`, every call is a no-op. Default is `true` (enabled).
|
|
272
|
-
|
|
273
|
-
**No-op outside cmux.** `codebyplan cmux-status` checks for `$CMUX_WORKSPACE_ID` before doing anything. Outside a cmux workspace it exits immediately — safe to call unconditionally from skills and hooks.
|
|
274
|
-
|
|
275
|
-
---
|
|
276
|
-
|
|
277
227
|
## Supporting (not registered)
|
|
278
228
|
|
|
279
229
|
### `test-hooks.sh` — invoked by `auto-test-hooks.sh`
|
|
280
230
|
|
|
281
|
-
Test suite for the plugin's
|
|
231
|
+
Test suite for the plugin's 9 registered hooks. Runs two passes:
|
|
282
232
|
|
|
283
|
-
1. **Header check** — every registered hook (`lint-format-on-edit`, `test-coverage-gate`, `pre-commit-quality-gate`, `maestro-yaml-validate`, `auto-test-hooks`, `mcp-migration-guard`, `validate-git-stash-deny`, `cbp-mcp-round-sync
|
|
233
|
+
1. **Header check** — every registered hook (`lint-format-on-edit`, `test-coverage-gate`, `pre-commit-quality-gate`, `maestro-yaml-validate`, `auto-test-hooks`, `mcp-migration-guard`, `validate-git-stash-deny`, `cbp-mcp-round-sync`) carries the required `# Hook:` and `# Purpose:` header comments. `statusline` uses its own `# Claude Code Status Line` marker.
|
|
284
234
|
2. **Functional smoke tests** — each hook is invoked with synthetic stdin matching its fast-path / graceful-degrade input; all must exit 0.
|
|
285
235
|
|
|
286
236
|
Not in `hooks.json` — invoked indirectly via `auto-test-hooks.sh` on hook edits, or directly via `bash ${CLAUDE_PLUGIN_ROOT}/hooks/test-hooks.sh`.
|
|
@@ -77,7 +77,6 @@ function main() {
|
|
|
77
77
|
);
|
|
78
78
|
const EXCEEDS_200K = get(data, ["exceeds_200k_tokens"], false);
|
|
79
79
|
const EFFORT = get(data, ["effort", "level"], "");
|
|
80
|
-
const THINKING = get(data, ["thinking", "enabled"], false);
|
|
81
80
|
const RATE_5H_PCT = get(
|
|
82
81
|
data,
|
|
83
82
|
["rate_limits", "five_hour", "used_percentage"],
|
|
@@ -119,6 +118,7 @@ function main() {
|
|
|
119
118
|
repo_pr: true,
|
|
120
119
|
worktree: true,
|
|
121
120
|
infra_drift: true,
|
|
121
|
+
package_freshness: true,
|
|
122
122
|
no_color: false,
|
|
123
123
|
};
|
|
124
124
|
try {
|
|
@@ -138,6 +138,7 @@ function main() {
|
|
|
138
138
|
"repo_pr",
|
|
139
139
|
"worktree",
|
|
140
140
|
"infra_drift",
|
|
141
|
+
"package_freshness",
|
|
141
142
|
]) {
|
|
142
143
|
if (typeof parsed.lines[k] === "boolean") cfg[k] = parsed.lines[k];
|
|
143
144
|
}
|
|
@@ -190,6 +191,9 @@ function main() {
|
|
|
190
191
|
return String(n);
|
|
191
192
|
};
|
|
192
193
|
|
|
194
|
+
// Percentage formatter (integer round-half-up; cross-runtime identical).
|
|
195
|
+
const fmtPct = (n) => String(Math.floor(Number(n) + 0.5));
|
|
196
|
+
|
|
193
197
|
const fmtK = (val) => {
|
|
194
198
|
const v = Number(val);
|
|
195
199
|
if (v >= 1000000) {
|
|
@@ -203,9 +207,9 @@ function main() {
|
|
|
203
207
|
};
|
|
204
208
|
|
|
205
209
|
const fmtCost = (c) => {
|
|
206
|
-
const n = Math.floor(Number(c) *
|
|
207
|
-
const frac = String(n %
|
|
208
|
-
return `$${Math.floor(n /
|
|
210
|
+
const n = Math.floor(Number(c) * 100 + 0.5);
|
|
211
|
+
const frac = String(n % 100).padStart(2, "0");
|
|
212
|
+
return `$${Math.floor(n / 100)}.${frac}`;
|
|
209
213
|
};
|
|
210
214
|
|
|
211
215
|
const fmtDur = (ms) => {
|
|
@@ -281,7 +285,6 @@ function main() {
|
|
|
281
285
|
L1 += `${C.BOLD}${C.CYAN}${MODEL_ID}${C.RST}`;
|
|
282
286
|
}
|
|
283
287
|
if (EFFORT) L1 += ` ${C.DIM}effort:${C.RST}${EFFORT}`;
|
|
284
|
-
if (THINKING === true) L1 += ` ${C.YELLOW}thinking:on${C.RST}`;
|
|
285
288
|
if (OUTPUT_STYLE && OUTPUT_STYLE !== "default")
|
|
286
289
|
L1 += ` ${C.DIM}style:${C.RST}${OUTPUT_STYLE}`;
|
|
287
290
|
if (VIM_MODE) L1 += ` ${C.DIM}[${VIM_MODE}]${C.RST}`;
|
|
@@ -325,18 +328,20 @@ function main() {
|
|
|
325
328
|
if (has5h || has7d) {
|
|
326
329
|
let L4 = "";
|
|
327
330
|
if (has5h) {
|
|
331
|
+
const r5 = fmtPct(RATE_5H_PCT);
|
|
328
332
|
let c5;
|
|
329
|
-
if (gte(
|
|
330
|
-
else if (gte(
|
|
333
|
+
if (gte(r5, 80)) c5 = C.RED;
|
|
334
|
+
else if (gte(r5, 60)) c5 = C.YELLOW;
|
|
331
335
|
else c5 = C.GREEN;
|
|
332
|
-
L4 = `${C.DIM}5h:${C.RST}${c5}${
|
|
336
|
+
L4 = `${C.DIM}5h:${C.RST}${c5}${r5}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_5H_RESETS)})${C.RST}`;
|
|
333
337
|
}
|
|
334
338
|
if (has7d) {
|
|
339
|
+
const r7 = fmtPct(RATE_7D_PCT);
|
|
335
340
|
let c7;
|
|
336
|
-
if (gte(
|
|
337
|
-
else if (gte(
|
|
341
|
+
if (gte(r7, 80)) c7 = C.RED;
|
|
342
|
+
else if (gte(r7, 60)) c7 = C.YELLOW;
|
|
338
343
|
else c7 = C.GREEN;
|
|
339
|
-
const seg7 = `${C.DIM}7d:${C.RST}${c7}${
|
|
344
|
+
const seg7 = `${C.DIM}7d:${C.RST}${c7}${r7}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_7D_RESETS)})${C.RST}`;
|
|
340
345
|
L4 = L4 ? `${L4} ${C.DIM}|${C.RST} ${seg7}` : seg7;
|
|
341
346
|
}
|
|
342
347
|
out.push(L4);
|
|
@@ -418,6 +423,96 @@ function main() {
|
|
|
418
423
|
}
|
|
419
424
|
}
|
|
420
425
|
|
|
426
|
+
// ============================================================
|
|
427
|
+
// LINE 8 — Package freshness (codebyplan version / sync state)
|
|
428
|
+
// ============================================================
|
|
429
|
+
// Source: .codebyplan/claude-status.local.json (written by background refresh).
|
|
430
|
+
// Inline fallback (cache absent): read .claude/.cbp.manifest.json vs
|
|
431
|
+
// node_modules/codebyplan/package.json. HIDE when guarded (canonical_source /
|
|
432
|
+
// no_manifest / unknown) or when manifest absent (not a managed consumer).
|
|
433
|
+
if (shouldShow("PACKAGE_FRESHNESS", cfg.package_freshness)) {
|
|
434
|
+
let guarded = false;
|
|
435
|
+
let installed = "";
|
|
436
|
+
let newer = false;
|
|
437
|
+
let latest = "";
|
|
438
|
+
let inSync = true;
|
|
439
|
+
|
|
440
|
+
const cachePath = path.join(
|
|
441
|
+
root,
|
|
442
|
+
".codebyplan",
|
|
443
|
+
"claude-status.local.json"
|
|
444
|
+
);
|
|
445
|
+
if (fs.existsSync(cachePath)) {
|
|
446
|
+
try {
|
|
447
|
+
const cacheRaw = fs.readFileSync(cachePath, "utf8");
|
|
448
|
+
const cache = JSON.parse(cacheRaw);
|
|
449
|
+
if (cache && typeof cache === "object") {
|
|
450
|
+
const gr = cache.guard_reason;
|
|
451
|
+
if (
|
|
452
|
+
gr === "canonical_source" ||
|
|
453
|
+
gr === "no_manifest" ||
|
|
454
|
+
gr === "unknown"
|
|
455
|
+
) {
|
|
456
|
+
guarded = true;
|
|
457
|
+
} else {
|
|
458
|
+
installed =
|
|
459
|
+
typeof cache.installed === "string" ? cache.installed : "";
|
|
460
|
+
newer = cache.newer === true;
|
|
461
|
+
latest = typeof cache.latest === "string" ? cache.latest : "";
|
|
462
|
+
inSync = cache.in_sync !== false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Unreadable / invalid → keep guarded=false, installed=""
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
// Inline fallback: no cache — read-only file reads only, no network.
|
|
470
|
+
const manifestPath = path.join(root, ".claude", ".cbp.manifest.json");
|
|
471
|
+
const pkgPath = path.join(
|
|
472
|
+
root,
|
|
473
|
+
"node_modules",
|
|
474
|
+
"codebyplan",
|
|
475
|
+
"package.json"
|
|
476
|
+
);
|
|
477
|
+
if (!fs.existsSync(manifestPath)) {
|
|
478
|
+
// No manifest → not a managed consumer → hide.
|
|
479
|
+
guarded = true;
|
|
480
|
+
} else {
|
|
481
|
+
try {
|
|
482
|
+
const mRaw = fs.readFileSync(manifestPath, "utf8");
|
|
483
|
+
const mParsed = JSON.parse(mRaw);
|
|
484
|
+
const mVer =
|
|
485
|
+
typeof mParsed?.version === "string" ? mParsed.version : "";
|
|
486
|
+
const pRaw = fs.readFileSync(pkgPath, "utf8");
|
|
487
|
+
const pParsed = JSON.parse(pRaw);
|
|
488
|
+
const iVer =
|
|
489
|
+
typeof pParsed?.version === "string" ? pParsed.version : "";
|
|
490
|
+
installed = iVer;
|
|
491
|
+
if (mVer && iVer && mVer !== iVer) {
|
|
492
|
+
// manifest ≠ installed → .claude is out of sync → ⟳ run claude update
|
|
493
|
+
// (mirrors the doctor's version_skip → in_sync:false). No npm info in
|
|
494
|
+
// the offline fallback, so never the ↑ newer-available marker.
|
|
495
|
+
inSync = false;
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
// Can't read files → hide segment.
|
|
499
|
+
guarded = true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!guarded && installed) {
|
|
505
|
+
let L8 = `${C.DIM}cbp${C.RST} ${installed}`;
|
|
506
|
+
if (newer && latest) {
|
|
507
|
+
L8 += ` ${C.YELLOW}↑${latest}${C.RST}`;
|
|
508
|
+
}
|
|
509
|
+
if (!inSync) {
|
|
510
|
+
L8 += ` ${C.YELLOW}⟳ run claude update${C.RST}`;
|
|
511
|
+
}
|
|
512
|
+
out.push(L8);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
421
516
|
process.stdout.write(out.length ? out.join("\n") + "\n" : "");
|
|
422
517
|
}
|
|
423
518
|
|
|
@@ -60,7 +60,6 @@ def main():
|
|
|
60
60
|
CACHE_READ = _get(data, ["context_window", "current_usage", "cache_read_input_tokens"], 0)
|
|
61
61
|
EXCEEDS_200K = _get(data, ["exceeds_200k_tokens"], False)
|
|
62
62
|
EFFORT = _get(data, ["effort", "level"], "")
|
|
63
|
-
THINKING = _get(data, ["thinking", "enabled"], False)
|
|
64
63
|
RATE_5H_PCT = _get(data, ["rate_limits", "five_hour", "used_percentage"], "")
|
|
65
64
|
RATE_5H_RESETS = _get(data, ["rate_limits", "five_hour", "resets_at"], 0)
|
|
66
65
|
RATE_7D_PCT = _get(data, ["rate_limits", "seven_day", "used_percentage"], "")
|
|
@@ -81,7 +80,7 @@ def main():
|
|
|
81
80
|
cfg = {
|
|
82
81
|
"identity": True, "context": True, "cost": True,
|
|
83
82
|
"rate_limits": True, "repo_pr": True, "worktree": True,
|
|
84
|
-
"infra_drift": True, "no_color": False,
|
|
83
|
+
"infra_drift": True, "package_freshness": True, "no_color": False,
|
|
85
84
|
}
|
|
86
85
|
try:
|
|
87
86
|
with open(os.path.join(root, ".codebyplan", "statusline.json"), "r", encoding="utf-8") as fh:
|
|
@@ -91,7 +90,7 @@ def main():
|
|
|
91
90
|
cfg["no_color"] = parsed["no_color"]
|
|
92
91
|
lines = parsed.get("lines")
|
|
93
92
|
if isinstance(lines, dict):
|
|
94
|
-
for k in ["identity", "context", "cost", "rate_limits", "repo_pr", "worktree", "infra_drift"]:
|
|
93
|
+
for k in ["identity", "context", "cost", "rate_limits", "repo_pr", "worktree", "infra_drift", "package_freshness"]:
|
|
95
94
|
if isinstance(lines.get(k), bool):
|
|
96
95
|
cfg[k] = lines[k]
|
|
97
96
|
except Exception:
|
|
@@ -134,6 +133,10 @@ def main():
|
|
|
134
133
|
return str(int(x))
|
|
135
134
|
return str(n)
|
|
136
135
|
|
|
136
|
+
# Percentage formatter (integer round-half-up; cross-runtime identical).
|
|
137
|
+
def fmt_pct(n):
|
|
138
|
+
return "%d" % int(float(n) + 0.5)
|
|
139
|
+
|
|
137
140
|
def fmt_k(val):
|
|
138
141
|
v = float(val)
|
|
139
142
|
if v >= 1000000:
|
|
@@ -145,8 +148,8 @@ def main():
|
|
|
145
148
|
return str(int(v))
|
|
146
149
|
|
|
147
150
|
def fmt_cost(c):
|
|
148
|
-
n = math.floor(float(c) *
|
|
149
|
-
return "$%d.%
|
|
151
|
+
n = math.floor(float(c) * 100 + 0.5)
|
|
152
|
+
return "$%d.%02d" % (n // 100, n % 100)
|
|
150
153
|
|
|
151
154
|
def fmt_dur(ms):
|
|
152
155
|
secs = math.trunc(float(ms) / 1000)
|
|
@@ -217,8 +220,6 @@ def main():
|
|
|
217
220
|
l1 += "%s%s%s%s" % (BOLD, CYAN, MODEL_ID, RST)
|
|
218
221
|
if EFFORT:
|
|
219
222
|
l1 += " %seffort:%s%s" % (DIM, RST, EFFORT)
|
|
220
|
-
if THINKING is True:
|
|
221
|
-
l1 += " %sthinking:on%s" % (YELLOW, RST)
|
|
222
223
|
if OUTPUT_STYLE and OUTPUT_STYLE != "default":
|
|
223
224
|
l1 += " %sstyle:%s%s" % (DIM, RST, OUTPUT_STYLE)
|
|
224
225
|
if VIM_MODE:
|
|
@@ -273,24 +274,26 @@ def main():
|
|
|
273
274
|
if has_5h or has_7d:
|
|
274
275
|
l4 = ""
|
|
275
276
|
if has_5h:
|
|
276
|
-
|
|
277
|
+
r5 = fmt_pct(RATE_5H_PCT)
|
|
278
|
+
if gte(r5, 80):
|
|
277
279
|
c5 = RED
|
|
278
|
-
elif gte(
|
|
280
|
+
elif gte(r5, 60):
|
|
279
281
|
c5 = YELLOW
|
|
280
282
|
else:
|
|
281
283
|
c5 = GREEN
|
|
282
284
|
l4 = "%s5h:%s%s%s%%%s %s(resets in %s)%s" % (
|
|
283
|
-
DIM, RST, c5,
|
|
285
|
+
DIM, RST, c5, r5, RST, DIM, fmt_rel_time(RATE_5H_RESETS), RST,
|
|
284
286
|
)
|
|
285
287
|
if has_7d:
|
|
286
|
-
|
|
288
|
+
r7 = fmt_pct(RATE_7D_PCT)
|
|
289
|
+
if gte(r7, 80):
|
|
287
290
|
c7 = RED
|
|
288
|
-
elif gte(
|
|
291
|
+
elif gte(r7, 60):
|
|
289
292
|
c7 = YELLOW
|
|
290
293
|
else:
|
|
291
294
|
c7 = GREEN
|
|
292
295
|
seg7 = "%s7d:%s%s%s%%%s %s(resets in %s)%s" % (
|
|
293
|
-
DIM, RST, c7,
|
|
296
|
+
DIM, RST, c7, r7, RST, DIM, fmt_rel_time(RATE_7D_RESETS), RST,
|
|
294
297
|
)
|
|
295
298
|
l4 = ("%s %s|%s %s" % (l4, DIM, RST, seg7)) if l4 else seg7
|
|
296
299
|
out.append(l4)
|
|
@@ -343,6 +346,69 @@ def main():
|
|
|
343
346
|
if behind > 0:
|
|
344
347
|
out.append("%s⚠ infra %d behind%s %s→ /cbp-refresh-infra%s" % (YELLOW, behind, RST, DIM, RST))
|
|
345
348
|
|
|
349
|
+
# ===== LINE 8 — Package freshness (codebyplan version / sync state) =====
|
|
350
|
+
# Source: .codebyplan/claude-status.local.json (written by background refresh).
|
|
351
|
+
# Inline fallback (cache absent): read .claude/.cbp.manifest.json vs
|
|
352
|
+
# node_modules/codebyplan/package.json. HIDE when guarded (canonical_source /
|
|
353
|
+
# no_manifest / unknown) or when manifest absent (not a managed consumer).
|
|
354
|
+
if should_show("PACKAGE_FRESHNESS", cfg["package_freshness"]):
|
|
355
|
+
_guarded = False
|
|
356
|
+
_installed = ""
|
|
357
|
+
_newer = False
|
|
358
|
+
_latest = ""
|
|
359
|
+
_in_sync = True
|
|
360
|
+
|
|
361
|
+
cache_path = os.path.join(root, ".codebyplan", "claude-status.local.json")
|
|
362
|
+
if os.path.isfile(cache_path):
|
|
363
|
+
try:
|
|
364
|
+
with open(cache_path, "r", encoding="utf-8") as fh:
|
|
365
|
+
cache = json.load(fh)
|
|
366
|
+
if isinstance(cache, dict):
|
|
367
|
+
gr = cache.get("guard_reason")
|
|
368
|
+
if gr in ("canonical_source", "no_manifest", "unknown"):
|
|
369
|
+
_guarded = True
|
|
370
|
+
else:
|
|
371
|
+
_installed = cache.get("installed") if isinstance(cache.get("installed"), str) else ""
|
|
372
|
+
_newer = cache.get("newer") is True
|
|
373
|
+
_latest = cache.get("latest") if isinstance(cache.get("latest"), str) else ""
|
|
374
|
+
_in_sync = cache.get("in_sync") is not False
|
|
375
|
+
except Exception:
|
|
376
|
+
pass # Unreadable / invalid → keep _installed=""
|
|
377
|
+
else:
|
|
378
|
+
# Inline fallback: no cache — read-only file reads only, no network.
|
|
379
|
+
manifest_path = os.path.join(root, ".claude", ".cbp.manifest.json")
|
|
380
|
+
pkg_path = os.path.join(root, "node_modules", "codebyplan", "package.json")
|
|
381
|
+
if not os.path.isfile(manifest_path):
|
|
382
|
+
# No manifest → not a managed consumer → hide.
|
|
383
|
+
_guarded = True
|
|
384
|
+
else:
|
|
385
|
+
try:
|
|
386
|
+
with open(manifest_path, "r", encoding="utf-8") as fh:
|
|
387
|
+
m_data = json.load(fh)
|
|
388
|
+
m_ver = m_data.get("version") if isinstance(m_data, dict) else ""
|
|
389
|
+
m_ver = m_ver if isinstance(m_ver, str) else ""
|
|
390
|
+
with open(pkg_path, "r", encoding="utf-8") as fh:
|
|
391
|
+
p_data = json.load(fh)
|
|
392
|
+
i_ver = p_data.get("version") if isinstance(p_data, dict) else ""
|
|
393
|
+
i_ver = i_ver if isinstance(i_ver, str) else ""
|
|
394
|
+
_installed = i_ver
|
|
395
|
+
if m_ver and i_ver and m_ver != i_ver:
|
|
396
|
+
# manifest != installed -> .claude is out of sync -> run claude update
|
|
397
|
+
# (mirrors the doctor's version_skip -> in_sync:false). No npm info
|
|
398
|
+
# in the offline fallback, so never the up-arrow newer marker.
|
|
399
|
+
_in_sync = False
|
|
400
|
+
except Exception:
|
|
401
|
+
# Can't read files → hide segment.
|
|
402
|
+
_guarded = True
|
|
403
|
+
|
|
404
|
+
if not _guarded and _installed:
|
|
405
|
+
l8 = "%scbp%s %s" % (DIM, RST, _installed)
|
|
406
|
+
if _newer and _latest:
|
|
407
|
+
l8 += " %s↑%s%s" % (YELLOW, _latest, RST)
|
|
408
|
+
if not _in_sync:
|
|
409
|
+
l8 += " %s⟳ run claude update%s" % (YELLOW, RST)
|
|
410
|
+
out.append(l8)
|
|
411
|
+
|
|
346
412
|
sys.stdout.write(("\n".join(out) + "\n") if out else "")
|
|
347
413
|
|
|
348
414
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
#
|
|
16
16
|
# DISPLAY OPTIONS (team-shared, committed)
|
|
17
17
|
# .codebyplan/statusline.json -> { "lines": {identity,context,cost,rate_limits,
|
|
18
|
-
# repo_pr,worktree,infra_drift}, "no_color": bool }
|
|
18
|
+
# repo_pr,worktree,infra_drift,package_freshness}, "no_color": bool }
|
|
19
19
|
#
|
|
20
20
|
# ENV-VAR OVERRIDES (env > config > default)
|
|
21
21
|
# CBP_STATUSLINE_HIDE_IDENTITY=1 suppress line 1 (folder, branch, model, effort, …)
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
# CBP_STATUSLINE_HIDE_RATE_LIMITS=1 suppress line 4 (5h / 7d rate limits)
|
|
25
25
|
# CBP_STATUSLINE_HIDE_REPO_PR=1 suppress line 5 (repo host/owner/name, PR)
|
|
26
26
|
# CBP_STATUSLINE_HIDE_WORKTREE=1 suppress line 6 (worktree name/branch/path)
|
|
27
|
-
# CBP_STATUSLINE_HIDE_INFRA_DRIFT=1
|
|
27
|
+
# CBP_STATUSLINE_HIDE_INFRA_DRIFT=1 suppress line 7 (.claude infra commits behind main)
|
|
28
|
+
# CBP_STATUSLINE_HIDE_PACKAGE_FRESHNESS=1 suppress line 8 (codebyplan package version)
|
|
28
29
|
# CBP_STATUSLINE_NO_COLOR=1 strip all ANSI colour codes (also honoured by $NO_COLOR)
|
|
29
30
|
#
|
|
30
31
|
# TEST SEAMS (no effect in normal use)
|
|
@@ -48,6 +49,17 @@ if [ -f "$CBP_LOCAL_CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
|
48
49
|
case "$_r" in bash|node|python) CBP_RENDERER="$_r" ;; esac
|
|
49
50
|
fi
|
|
50
51
|
|
|
52
|
+
# Background claude-status cache refresh (6h staleness gate).
|
|
53
|
+
# `find -mmin -360` PRINTS the path only when the file exists AND is younger
|
|
54
|
+
# than 6h; an EMPTY result means the cache is absent OR stale (>6h) — both
|
|
55
|
+
# need a refresh. (find's exit code is 0 for a present-but-non-matching path,
|
|
56
|
+
# so we must test its output, not its status.)
|
|
57
|
+
# Fully detached — never blocks the render, never writes to this stdout.
|
|
58
|
+
CBP_STATUS_CACHE="$CBP_ROOT/.codebyplan/claude-status.local.json"
|
|
59
|
+
if [ -z "$(find "$CBP_STATUS_CACHE" -mmin -360 2>/dev/null)" ]; then
|
|
60
|
+
(npx codebyplan claude status --write-cache --quiet >/dev/null 2>&1 &)
|
|
61
|
+
fi
|
|
62
|
+
|
|
51
63
|
if [ "$CBP_RENDERER" = "node" ] && command -v node >/dev/null 2>&1 \
|
|
52
64
|
&& [ -f "$CBP_HOOK_DIR/cbp-statusline.mjs" ]; then
|
|
53
65
|
CBP_STATUSLINE_ROOT="$CBP_ROOT" exec node "$CBP_HOOK_DIR/cbp-statusline.mjs"
|
|
@@ -83,7 +95,6 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
83
95
|
@sh "CACHE_READ=\(.context_window.current_usage.cache_read_input_tokens // 0)",
|
|
84
96
|
@sh "EXCEEDS_200K=\(.exceeds_200k_tokens // false)",
|
|
85
97
|
@sh "EFFORT=\(.effort.level // "")",
|
|
86
|
-
@sh "THINKING=\(.thinking.enabled // false)",
|
|
87
98
|
@sh "RATE_5H_PCT=\(.rate_limits.five_hour.used_percentage // "")",
|
|
88
99
|
@sh "RATE_5H_RESETS=\(.rate_limits.five_hour.resets_at // 0)",
|
|
89
100
|
@sh "RATE_7D_PCT=\(.rate_limits.seven_day.used_percentage // "")",
|
|
@@ -105,7 +116,7 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
105
116
|
|
|
106
117
|
# ---- Config: line toggles + no_color from .codebyplan/statusline.json --------
|
|
107
118
|
CFG_IDENTITY=true; CFG_CONTEXT=true; CFG_COST=true
|
|
108
|
-
CFG_RATE_LIMITS=true; CFG_REPO_PR=true; CFG_WORKTREE=true; CFG_INFRA_DRIFT=true; CFG_NO_COLOR=false
|
|
119
|
+
CFG_RATE_LIMITS=true; CFG_REPO_PR=true; CFG_WORKTREE=true; CFG_INFRA_DRIFT=true; CFG_PACKAGE_FRESHNESS=true; CFG_NO_COLOR=false
|
|
109
120
|
CBP_CFG="$CBP_ROOT/.codebyplan/statusline.json"
|
|
110
121
|
if [ -f "$CBP_CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
111
122
|
# Use `!= false` / `== true` (NOT jq `//`): the `//` operator treats an explicit
|
|
@@ -119,6 +130,7 @@ if [ -f "$CBP_CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
|
119
130
|
"CFG_REPO_PR=\(.lines.repo_pr != false)",
|
|
120
131
|
"CFG_WORKTREE=\(.lines.worktree != false)",
|
|
121
132
|
"CFG_INFRA_DRIFT=\(.lines.infra_drift != false)",
|
|
133
|
+
"CFG_PACKAGE_FRESHNESS=\(.lines.package_freshness != false)",
|
|
122
134
|
"CFG_NO_COLOR=\(.no_color == true)"
|
|
123
135
|
' "$CBP_CFG" 2>/dev/null)"
|
|
124
136
|
fi
|
|
@@ -150,6 +162,9 @@ fi
|
|
|
150
162
|
# ---- Float-safe percentage comparison ----------------------------------------
|
|
151
163
|
awk_gte() { awk -v v="$1" -v t="$2" 'BEGIN{exit !(v+0 >= t+0)}'; }
|
|
152
164
|
|
|
165
|
+
# ---- Percentage formatter (integer round-half-up; cross-runtime identical) ----
|
|
166
|
+
fmt_pct() { awk -v v="$1" 'BEGIN{ printf "%d", int(v + 0.5) }'; }
|
|
167
|
+
|
|
153
168
|
# ---- Token/size formatter (K / M) — integer round-half-up (cross-runtime) -----
|
|
154
169
|
fmt_k() {
|
|
155
170
|
local val=$1
|
|
@@ -164,9 +179,9 @@ fmt_k() {
|
|
|
164
179
|
fi
|
|
165
180
|
}
|
|
166
181
|
|
|
167
|
-
# ---- Cost formatter ($X.
|
|
182
|
+
# ---- Cost formatter ($X.XX) — integer round-half-up (cross-runtime) -----------
|
|
168
183
|
fmt_cost() {
|
|
169
|
-
awk -v c="$1" 'BEGIN{ n=int(c*
|
|
184
|
+
awk -v c="$1" 'BEGIN{ n=int(c*100 + 0.5); printf "$%d.%02d", int(n/100), n%100 }'
|
|
170
185
|
}
|
|
171
186
|
|
|
172
187
|
# ---- Duration formatter (ms → Xh Xm Xs) --------------------------------------
|
|
@@ -245,11 +260,6 @@ if should_show IDENTITY "$CFG_IDENTITY"; then
|
|
|
245
260
|
L1="${L1} ${DIM}effort:${RST}${EFFORT}"
|
|
246
261
|
fi
|
|
247
262
|
|
|
248
|
-
# Thinking (only when explicitly true)
|
|
249
|
-
if [ "$THINKING" = "true" ]; then
|
|
250
|
-
L1="${L1} ${YELLOW}thinking:on${RST}"
|
|
251
|
-
fi
|
|
252
|
-
|
|
253
263
|
# Output style (when present and not "default")
|
|
254
264
|
if [ -n "$OUTPUT_STYLE" ] && [ "$OUTPUT_STYLE" != "default" ]; then
|
|
255
265
|
L1="${L1} ${DIM}style:${RST}${OUTPUT_STYLE}"
|
|
@@ -324,27 +334,29 @@ if should_show RATE_LIMITS "$CFG_RATE_LIMITS"; then
|
|
|
324
334
|
L4=""
|
|
325
335
|
|
|
326
336
|
if [ -n "$RATE_5H_PCT" ] && [ "$RATE_5H_RESETS" != "0" ]; then
|
|
327
|
-
|
|
337
|
+
R5=$(fmt_pct "$RATE_5H_PCT")
|
|
338
|
+
if awk_gte "$R5" 80; then
|
|
328
339
|
C5="$RED"
|
|
329
|
-
elif awk_gte "$
|
|
340
|
+
elif awk_gte "$R5" 60; then
|
|
330
341
|
C5="$YELLOW"
|
|
331
342
|
else
|
|
332
343
|
C5="$GREEN"
|
|
333
344
|
fi
|
|
334
345
|
REL5=$(fmt_rel_time "$RATE_5H_RESETS")
|
|
335
|
-
L4="${DIM}5h:${RST}${C5}${
|
|
346
|
+
L4="${DIM}5h:${RST}${C5}${R5}%${RST} ${DIM}(resets in ${REL5})${RST}"
|
|
336
347
|
fi
|
|
337
348
|
|
|
338
349
|
if [ -n "$RATE_7D_PCT" ] && [ "$RATE_7D_RESETS" != "0" ]; then
|
|
339
|
-
|
|
350
|
+
R7=$(fmt_pct "$RATE_7D_PCT")
|
|
351
|
+
if awk_gte "$R7" 80; then
|
|
340
352
|
C7="$RED"
|
|
341
|
-
elif awk_gte "$
|
|
353
|
+
elif awk_gte "$R7" 60; then
|
|
342
354
|
C7="$YELLOW"
|
|
343
355
|
else
|
|
344
356
|
C7="$GREEN"
|
|
345
357
|
fi
|
|
346
358
|
REL7=$(fmt_rel_time "$RATE_7D_RESETS")
|
|
347
|
-
SEG7="${DIM}7d:${RST}${C7}${
|
|
359
|
+
SEG7="${DIM}7d:${RST}${C7}${R7}%${RST} ${DIM}(resets in ${REL7})${RST}"
|
|
348
360
|
if [ -n "$L4" ]; then
|
|
349
361
|
L4="${L4} ${DIM}|${RST} ${SEG7}"
|
|
350
362
|
else
|
|
@@ -421,3 +433,71 @@ if should_show INFRA_DRIFT "$CFG_INFRA_DRIFT"; then
|
|
|
421
433
|
;;
|
|
422
434
|
esac
|
|
423
435
|
fi
|
|
436
|
+
|
|
437
|
+
# ============================================================
|
|
438
|
+
# LINE 8 — Package freshness (codebyplan version / sync state)
|
|
439
|
+
# ============================================================
|
|
440
|
+
# Source: .codebyplan/claude-status.local.json (written by background refresh).
|
|
441
|
+
# Inline fallback (cache absent): read .claude/.cbp.manifest.json vs
|
|
442
|
+
# node_modules/codebyplan/package.json. HIDE when guarded (canonical_source /
|
|
443
|
+
# no_manifest / unknown) or when manifest absent (not a managed consumer).
|
|
444
|
+
if should_show PACKAGE_FRESHNESS "$CFG_PACKAGE_FRESHNESS"; then
|
|
445
|
+
L8=""
|
|
446
|
+
_CBP_GUARDED=false
|
|
447
|
+
_CBP_INSTALLED=""
|
|
448
|
+
_CBP_NEWER=false
|
|
449
|
+
_CBP_LATEST=""
|
|
450
|
+
_CBP_IN_SYNC=true
|
|
451
|
+
|
|
452
|
+
if [ -f "$CBP_STATUS_CACHE" ] && command -v jq >/dev/null 2>&1; then
|
|
453
|
+
# Cache present — read fields.
|
|
454
|
+
_cbp_guard_reason="$(jq -r '.guard_reason // ""' "$CBP_STATUS_CACHE" 2>/dev/null)"
|
|
455
|
+
case "$_cbp_guard_reason" in
|
|
456
|
+
canonical_source|no_manifest|unknown)
|
|
457
|
+
_CBP_GUARDED=true
|
|
458
|
+
;;
|
|
459
|
+
esac
|
|
460
|
+
if [ "$_CBP_GUARDED" = "false" ]; then
|
|
461
|
+
_CBP_INSTALLED="$(jq -r '.installed // ""' "$CBP_STATUS_CACHE" 2>/dev/null)"
|
|
462
|
+
_CBP_NEWER="$(jq -r '.newer == true' "$CBP_STATUS_CACHE" 2>/dev/null)"
|
|
463
|
+
_CBP_LATEST="$(jq -r '.latest // ""' "$CBP_STATUS_CACHE" 2>/dev/null)"
|
|
464
|
+
# NOTE: `!= false` (NOT jq `//`): the `//` operator treats an explicit
|
|
465
|
+
# `false` as absent, so `.in_sync // true` would yield "true" for an
|
|
466
|
+
# out-of-sync cache and silently drop the ⟳ indicator (CHK-175 TASK-3 R1
|
|
467
|
+
# finding #1). `!= false` mirrors node `in_sync !== false` / python.
|
|
468
|
+
_CBP_IN_SYNC="$(jq -r '.in_sync != false' "$CBP_STATUS_CACHE" 2>/dev/null)"
|
|
469
|
+
fi
|
|
470
|
+
else
|
|
471
|
+
# Inline fallback: no cache, no network — read-only file reads only.
|
|
472
|
+
_cbp_manifest="$CBP_ROOT/.claude/.cbp.manifest.json"
|
|
473
|
+
_cbp_pkg="$CBP_ROOT/node_modules/codebyplan/package.json"
|
|
474
|
+
if [ ! -f "$_cbp_manifest" ]; then
|
|
475
|
+
# No manifest → not a managed consumer → hide segment.
|
|
476
|
+
_CBP_GUARDED=true
|
|
477
|
+
elif [ -f "$_cbp_pkg" ] && command -v jq >/dev/null 2>&1; then
|
|
478
|
+
_cbp_mver="$(jq -r '.version // ""' "$_cbp_manifest" 2>/dev/null)"
|
|
479
|
+
_cbp_iver="$(jq -r '.version // ""' "$_cbp_pkg" 2>/dev/null)"
|
|
480
|
+
_CBP_INSTALLED="$_cbp_iver"
|
|
481
|
+
if [ -n "$_cbp_mver" ] && [ -n "$_cbp_iver" ] && [ "$_cbp_mver" != "$_cbp_iver" ]; then
|
|
482
|
+
# manifest ≠ installed → .claude is out of sync → ⟳ run claude update
|
|
483
|
+
# (mirrors the doctor's version_skip → in_sync:false). No npm info in the
|
|
484
|
+
# offline fallback, so never the ↑ newer-available marker.
|
|
485
|
+
_CBP_IN_SYNC=false
|
|
486
|
+
fi
|
|
487
|
+
else
|
|
488
|
+
# Can't read package.json or no jq → hide segment.
|
|
489
|
+
_CBP_GUARDED=true
|
|
490
|
+
fi
|
|
491
|
+
fi
|
|
492
|
+
|
|
493
|
+
if [ "$_CBP_GUARDED" = "false" ] && [ -n "$_CBP_INSTALLED" ]; then
|
|
494
|
+
L8="${DIM}cbp${RST} ${_CBP_INSTALLED}"
|
|
495
|
+
if [ "$_CBP_NEWER" = "true" ] && [ -n "$_CBP_LATEST" ]; then
|
|
496
|
+
L8="${L8} ${YELLOW}↑${_CBP_LATEST}${RST}"
|
|
497
|
+
fi
|
|
498
|
+
if [ "$_CBP_IN_SYNC" = "false" ]; then
|
|
499
|
+
L8="${L8} ${YELLOW}⟳ run claude update${RST}"
|
|
500
|
+
fi
|
|
501
|
+
printf "%b\n" "$L8"
|
|
502
|
+
fi
|
|
503
|
+
fi
|