codebyplan 1.13.23 → 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/dist/cli.js +445 -187
- package/package.json +1 -1
- package/templates/hooks/README.md +1 -1
- 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/skills/cbp-session-start/SKILL.md +2 -0
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
|
|
@@ -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
|
|
@@ -97,6 +97,8 @@ Check whether a newer `codebyplan` is published and safe to auto-install on this
|
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
99
|
VERSION_JSON=$(npx codebyplan version-status 2>/dev/null)
|
|
100
|
+
# Populate the claude-status cache best-effort (pure cache population — never gates session-start).
|
|
101
|
+
npx codebyplan claude status --write-cache --quiet 2>/dev/null || true
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
Parse `$VERSION_JSON` as JSON and branch on the result:
|