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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.23",
3
+ "version": "1.13.24",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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); `thinking:on` (only when thinking is enabled); `style:NAME` (when output style is not "default"); `[VIM_MODE]`
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) * 10000 + 0.5);
207
- const frac = String(n % 10000).padStart(4, "0");
208
- return `$${Math.floor(n / 10000)}.${frac}`;
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(RATE_5H_PCT, 80)) c5 = C.RED;
330
- else if (gte(RATE_5H_PCT, 60)) c5 = C.YELLOW;
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}${numStr(RATE_5H_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_5H_RESETS)})${C.RST}`;
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(RATE_7D_PCT, 80)) c7 = C.RED;
337
- else if (gte(RATE_7D_PCT, 60)) c7 = C.YELLOW;
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}${numStr(RATE_7D_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_7D_RESETS)})${C.RST}`;
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) * 10000 + 0.5)
149
- return "$%d.%04d" % (n // 10000, n % 10000)
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
- if gte(RATE_5H_PCT, 80):
277
+ r5 = fmt_pct(RATE_5H_PCT)
278
+ if gte(r5, 80):
277
279
  c5 = RED
278
- elif gte(RATE_5H_PCT, 60):
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, num_str(RATE_5H_PCT), RST, DIM, fmt_rel_time(RATE_5H_RESETS), RST,
285
+ DIM, RST, c5, r5, RST, DIM, fmt_rel_time(RATE_5H_RESETS), RST,
284
286
  )
285
287
  if has_7d:
286
- if gte(RATE_7D_PCT, 80):
288
+ r7 = fmt_pct(RATE_7D_PCT)
289
+ if gte(r7, 80):
287
290
  c7 = RED
288
- elif gte(RATE_7D_PCT, 60):
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, num_str(RATE_7D_PCT), RST, DIM, fmt_rel_time(RATE_7D_RESETS), RST,
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 suppress line 7 (.claude infra commits behind main)
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.XXXX) — integer round-half-up (cross-runtime) ---------
182
+ # ---- Cost formatter ($X.XX) — integer round-half-up (cross-runtime) -----------
168
183
  fmt_cost() {
169
- awk -v c="$1" 'BEGIN{ n=int(c*10000 + 0.5); printf "$%d.%04d", int(n/10000), n%10000 }'
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
- if awk_gte "$RATE_5H_PCT" 80; then
337
+ R5=$(fmt_pct "$RATE_5H_PCT")
338
+ if awk_gte "$R5" 80; then
328
339
  C5="$RED"
329
- elif awk_gte "$RATE_5H_PCT" 60; then
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}${RATE_5H_PCT}%${RST} ${DIM}(resets in ${REL5})${RST}"
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
- if awk_gte "$RATE_7D_PCT" 80; then
350
+ R7=$(fmt_pct "$RATE_7D_PCT")
351
+ if awk_gte "$R7" 80; then
340
352
  C7="$RED"
341
- elif awk_gte "$RATE_7D_PCT" 60; then
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}${RATE_7D_PCT}%${RST} ${DIM}(resets in ${REL7})${RST}"
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: