chamba 0.6.1 → 0.8.0

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.
Files changed (118) hide show
  1. package/LICENSE +53 -14
  2. package/README.md +38 -11
  3. package/dist/commands/advanced.js +7 -278
  4. package/dist/commands/dev.js +11 -612
  5. package/dist/commands/doctor.js +1 -29
  6. package/dist/commands/menu.js +1 -80
  7. package/dist/commands/onboard.js +6 -230
  8. package/dist/commands/settings.js +15 -349
  9. package/dist/lib/agent-commands.js +2 -0
  10. package/dist/lib/agent-context.js +6 -184
  11. package/dist/lib/browser.js +1 -40
  12. package/dist/lib/chamba-yaml.js +10 -191
  13. package/dist/lib/constants.js +1 -125
  14. package/dist/lib/dockerfile-builder.js +41 -260
  15. package/dist/lib/env.js +2 -78
  16. package/dist/lib/git-env.js +1 -21
  17. package/dist/lib/global-config.js +4 -66
  18. package/dist/lib/pnpm-store.js +1 -19
  19. package/dist/lib/ports.js +1 -210
  20. package/dist/lib/safe-rm.js +1 -26
  21. package/dist/lib/sessions.js +2 -34
  22. package/dist/lib/shadows.js +1 -176
  23. package/dist/lib/skills.js +2 -0
  24. package/dist/lib/templates.js +1 -0
  25. package/dist/lib/webterm.js +1 -305
  26. package/dist/lib/workspace-identity.js +4 -260
  27. package/package.json +6 -6
  28. package/templates/Dockerfile +24 -12
  29. package/templates/claude-statusline.sh +5 -7
  30. package/templates/context/baseline.md +2 -0
  31. package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
  32. package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
  33. package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
  34. package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
  35. package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
  36. package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
  37. package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
  38. package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
  39. package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
  40. package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
  41. package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
  42. package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
  43. package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
  44. package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
  45. package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
  46. package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
  47. package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
  48. package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
  49. package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
  50. package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
  51. package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
  52. package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
  53. package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
  54. package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
  55. package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
  56. package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
  57. package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
  58. package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
  59. package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
  60. package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
  61. package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
  62. package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
  63. package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
  64. package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
  65. package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
  66. package/templates/pane-apps/client/reviews/index.html +15 -0
  67. package/templates/pane-apps/client/specs/index.html +14 -0
  68. package/templates/pane-apps/server/reviews.mjs +20 -0
  69. package/templates/pane-apps/server/specs.mjs +5 -0
  70. package/templates/skills/chamba-statusline/SKILL.md +7 -19
  71. package/templates/skills/dx-review/SKILL.md +123 -0
  72. package/templates/skills/dx-review/references/acts.md +162 -0
  73. package/templates/skills/dx-spec/SKILL.md +561 -0
  74. package/templates/skills/dx-spec/references/imagination-guide.md +140 -0
  75. package/templates/skills/dx-spec/references/principles-template.md +2 -0
  76. package/templates/skills/dx-spec/references/review-guide.md +127 -0
  77. package/templates/skills/dx-spec/references/spec-guide.md +145 -0
  78. package/templates/skills/dx-spec/references/stages.md +484 -0
  79. package/templates/skills/dx-spec-execute/SKILL.md +437 -0
  80. package/templates/startup.mjs +10 -4
  81. package/templates/tool-helper.sh +166 -0
  82. package/templates/webterm/README.md +79 -9
  83. package/templates/webterm/color.js +61 -0
  84. package/templates/webterm/config.js +74 -0
  85. package/templates/webterm/context/claude.md +0 -1
  86. package/templates/webterm/public/app/alerts.js +4 -4
  87. package/templates/webterm/public/app/anchored.js +81 -0
  88. package/templates/webterm/public/app/composer.js +4 -1
  89. package/templates/webterm/public/app/connection.js +9 -0
  90. package/templates/webterm/public/app/dom.js +15 -5
  91. package/templates/webterm/public/app/frames.js +51 -10
  92. package/templates/webterm/public/app/main.js +26 -6
  93. package/templates/webterm/public/app/new-session.js +13 -41
  94. package/templates/webterm/public/app/palette.js +58 -0
  95. package/templates/webterm/public/app/pane-shape.js +167 -0
  96. package/templates/webterm/public/app/pane-shell.js +380 -0
  97. package/templates/webterm/public/app/pane.js +77 -193
  98. package/templates/webterm/public/app/reviews-host.js +15 -0
  99. package/templates/webterm/public/app/specs-host.js +16 -0
  100. package/templates/webterm/public/app/state.js +5 -0
  101. package/templates/webterm/public/app/tabs.js +6 -2
  102. package/templates/webterm/public/app/terminal.js +8 -0
  103. package/templates/webterm/public/app/theme.js +75 -27
  104. package/templates/webterm/public/app/tool-host.js +302 -0
  105. package/templates/webterm/public/app/workspace-color.js +192 -0
  106. package/templates/webterm/public/index.html +68 -30
  107. package/templates/webterm/public/styles.css +270 -34
  108. package/templates/webterm/server.js +321 -0
  109. package/templates/webterm/tool-document.js +67 -0
  110. package/templates/webterm/tools/commands.js +61 -0
  111. package/templates/webterm/tools/index.js +25 -0
  112. package/templates/webterm/tools/mount.js +337 -0
  113. package/templates/webterm/tools/paths.js +34 -0
  114. package/templates/webterm/tools/reviews.js +133 -0
  115. package/templates/webterm/tools/specs.js +154 -0
  116. package/templates/webterm/typed-line.js +148 -0
  117. package/templates/context/context-usage.md +0 -1
  118. package/templates/context-usage.sh +0 -266
@@ -0,0 +1,148 @@
1
+ // =========================================================================================================================================
2
+ // typed-line.js - What a message is reduced to before a terminal ever sees it.
3
+ //
4
+ // A tool in the pane tells the agent beside it that something arrived, by typing one line into that agent's
5
+ // terminal. The line is framed - `[specs] <event>: "<detail>" - read <path>` - and the frame is a visual
6
+ // convention, so what this file protects is how the line reads. An agent must be able to tell the server's
7
+ // own words from a message's, and no message may pose as a second voice.
8
+ //
9
+ // The text rides a bracketed paste, where a newline is not a second submit but a second visible line inside
10
+ // the same prompt. So a text carrying one would show the agent a line below the framed one, in no frame at
11
+ // all. Reduction to a single line is what closes that, and it starts from the shipped single-line cleaner:
12
+ // controls dropped, whitespace runs collapsed, ends trimmed.
13
+ //
14
+ // That cleaner needs two corrections here, and both live in this wrapper, so the pane's titles and its nudge
15
+ // keep the behaviour they have.
16
+ //
17
+ // - Its length argument defaults to a page title's length, which would cut a message to a fragment. The
18
+ // limit is checked before the reduction instead, and an over-limit message is refused with a reason.
19
+ // - Its character rule keeps every code point at or above U+00A0, which leaves every invisible one alive.
20
+ // The rule this path needs is "renders as nothing", and no single Unicode property means that, so it is
21
+ // built from what survives rather than from what does not - in two layers, and it needs both.
22
+ // =========================================================================================================================================
23
+
24
+ import { cleanLine } from "./artifacts.js";
25
+
26
+ // The first layer: the categories a visible character is in. Letters, the marks that combine with them,
27
+ // numbers, and the common punctuation and symbol categories, plus the one space the collapse produces.
28
+ // Nothing outside them survives, whatever it is and whenever it was assigned - which ends the format
29
+ // characters, the bidi controls, and the tag block at U+E0000, since all three are format characters and
30
+ // none of those is a category here. The tag block is worth naming: it is a complete invisible copy of
31
+ // printable ASCII, and it is the carrier a hidden second instruction actually travels in.
32
+ const VISIBLE = /[\p{L}\p{M}\p{N}\p{P}\p{S} ]/u;
33
+
34
+ // The second layer: what the first keeps by accident. The allowed categories hold blank-rendering members of
35
+ // their own - U+3164 HANGUL FILLER is a letter, the variation selectors are combining marks, U+2800 BRAILLE
36
+ // PATTERN BLANK is a symbol - so a category allowlist alone passes all three at full width. Every code point
37
+ // carrying Default_Ignorable_Code_Point goes whatever its category, which covers the fillers and the
38
+ // selectors. U+2800 is then named, because it carries no such property and nothing else separates it from
39
+ // the braille it belongs to. That one code point is the only closed part of the rule.
40
+ const BLANK = /[\p{Default_Ignorable_Code_Point}\u2800]/u;
41
+
42
+ // No surviving code point can be confusable with the frame's quote, so every one that reads as a double
43
+ // quote is mapped to a single one - the way the pane's nudge already does for the ASCII quote alone. The
44
+ // rule is what a code point looks like rather than what it is called, so the property is no help: the
45
+ // quotation-mark property leaves out the double primes, the gershayim, the modifier letters that render as
46
+ // a pair of ticks, and the double acute, and each of those closes the frame on screen just as well.
47
+ const DOUBLE_QUOTES =
48
+ /["\u00ab\u00bb\u02ba\u02dd\u02ee\u02f6\u05f4\u201c\u201d\u201e\u201f\u2033\u2036\u275d\u275e\u2e42\u3003\u301d\u301e\u301f\uff02]|[\u{1f676}-\u{1f678}]/gu;
49
+
50
+ // All three CLIs read "@path" in a submitted line as a file to pull into the context, so no detail may carry
51
+ // one. The frame names a file in the one place it is allowed to - the path, which the server built and which
52
+ // is dropped unless the reduction leaves it whole - and a detail is not that place. So the at sign goes the
53
+ // way the quote lookalikes go, for every agent.
54
+ const FILE_MENTION = /@/g;
55
+
56
+ // The rules that are one CLI's own syntax, and nobody else's. The frame promises that a framed line instructs
57
+ // nobody, and the command line promises that its argument is a name and not a second instruction. Both
58
+ // promises rest on a character meaning something only where this server put it, and each CLI breaks that in
59
+ // its own way - so each cell below is one CLI's syntax, mapped to a code point that reads the same and means
60
+ // nothing to it. Every other agent is untouched.
61
+ //
62
+ // claude nothing of its own. The two rules above cover it.
63
+ // codex "$name" is a skill mention wherever it sits in the line.
64
+ // opencode a prompt expands a backticked shell command by running it. The expansion happens after the
65
+ // argument is placed in the command file, so a name typed by a button is scanned for one.
66
+ // Neutralizing the backtick is what ends it: the command form cannot be written without one.
67
+ // The dollar goes too: the argument is placed with String.replaceAll, where "$&" and the two
68
+ // quote forms beside it are replacement patterns that rebuild text out of the template.
69
+ //
70
+ // The replacements read the same and mean nothing to the CLI they are for. Two of the three are the
71
+ // compatibility forms, so a consumer that normalized the line to NFKC would fold them back - nothing in this
72
+ // chain normalizes, and this is the property to re-check if anything downstream ever does.
73
+ //
74
+ // Every agent has a row, empty where it needs nothing, and an agent with no row gets every rule in the table.
75
+ // A caller that omits the agent, or a fourth CLI added to the session side and not to this one, then loses a
76
+ // character rather than a guarantee.
77
+ const NO_RULES = [];
78
+ const CODEX_RULES = [[/\$/g, "\uff04"]]; // Fullwidth dollar sign
79
+ const OPENCODE_RULES = [[/`/g, "\u02cb"], ...CODEX_RULES]; // Modifier letter grave accent, then the dollar
80
+ const AGENT_RULES = {
81
+ claude: NO_RULES,
82
+ codex: CODEX_RULES,
83
+ opencode: OPENCODE_RULES,
84
+ };
85
+ const EVERY_RULE = [...OPENCODE_RULES];
86
+
87
+ /**
88
+ * One line of somebody else's text, safe to put inside the frame.
89
+ *
90
+ * A code point outside the visible categories becomes a space, which the cleaner then collapses: it was a
91
+ * control, a separator or a format character, and a space is what it was standing in for. One that renders
92
+ * as nothing inside an allowed category is removed rather than spaced, because it was padding rather than a
93
+ * gap - which is what keeps a padded tail inside the quotes instead of pushed off the row.
94
+ *
95
+ * `agent` is the agent this text is being reduced for. It is what decides the per-agent rules above; the two
96
+ * rules before them hold for every agent, and an agent the table does not name gets all of them. The detail
97
+ * is not always the user's own words either: a directory name read off the workspace comes through here too.
98
+ */
99
+ export function reduceLine(raw, max, agent) {
100
+ if (typeof raw !== "string") return "";
101
+ let kept = "";
102
+ for (const ch of raw) {
103
+ if (BLANK.test(ch)) continue;
104
+ kept += VISIBLE.test(ch) ? ch : " ";
105
+ }
106
+ let text = kept.replace(DOUBLE_QUOTES, "'").replace(FILE_MENTION, "\uff20"); // Fullwidth commercial at
107
+ // Read as the table's own entries: the agent is validated at session creation, and this keeps a name
108
+ // off Object.prototype from reaching the loop as something that is not a list of rules.
109
+ const rules = Object.hasOwn(AGENT_RULES, agent) ? AGENT_RULES[agent] : EVERY_RULE;
110
+ for (const [pattern, readsAs] of rules) text = text.replace(pattern, readsAs);
111
+ return cleanLine(text, max);
112
+ }
113
+
114
+ /**
115
+ * The line itself.
116
+ *
117
+ * `tag` and `event` are the caller's own words and never a message's, so a tool's text sits inside
118
+ * delimiters it cannot close and cannot pose as the user or as another source. `detail` is the only part
119
+ * that arrives here already reduced.
120
+ *
121
+ * `path` is a path this server built, but it is not made of this server's words: every segment of it is a
122
+ * name off the filesystem, and a name may hold any byte but the separator. So it goes through the same
123
+ * reduction, for the same agent, and it is named only when the reduction changed nothing. A path that changed
124
+ * is not a path any more - naming the reduced spelling would send the agent to a file that does not exist -
125
+ * and a path holding anything the reduction removes is the one case where naming no file is the honest line.
126
+ * A directory whose name holds one of the per-agent characters above is one of those, in that agent's session
127
+ * and in no other.
128
+ */
129
+ export function framedLine({ tag, event, detail, path, agent }) {
130
+ const said = detail ? `: "${detail}"` : "";
131
+ const named = path && reduceLine(path, path.length, agent) === path ? path : "";
132
+ return `[${tag}] ${event}${said}${named ? ` - read ${named}` : ""}`;
133
+ }
134
+
135
+ /**
136
+ * The line for a button that starts a skill: the command itself, as the user would type it.
137
+ *
138
+ * A skill the user alone may start does not start from a framed sentence, because the sentence reaches an
139
+ * agent that is not allowed to act on it. The button click is the user's own act, so the line it types is
140
+ * the command, and the frame would only get in the way of it.
141
+ *
142
+ * `command` is this server's own word, like every event word above. `argument` is the one part that came
143
+ * from somewhere else, and it arrives reduced: every code point that renders as a double quote is a single
144
+ * one by then, so it stays one quoted word and can neither close the quote nor open a second line.
145
+ */
146
+ export function commandLine(command, argument) {
147
+ return argument ? `${command} "${argument}"` : command;
148
+ }
@@ -1 +0,0 @@
1
- - You can check your own context-window and rate-limit usage at any time: run `context-usage`.
@@ -1,266 +0,0 @@
1
- #!/bin/sh
2
- # =============================================================================
3
- # context-usage -- prints the freshest Claude Code context/quota snapshot.
4
- # Baked into the container image at /usr/local/share/chamba/context-usage.sh
5
- # and symlinked to /usr/local/bin/context-usage.
6
- #
7
- # Snapshots are written by claude-statusline.sh to ~/.claude/context-usage/
8
- # on every prompt render, one file per session - that script prints nothing, so
9
- # the snapshot is the whole of what it does. Each snapshot records the pid
10
- # (and start time) of the Claude CLI process that spawned the status line; this
11
- # helper runs as a descendant of that SAME process, so matching them identifies
12
- # the asking session's own snapshot deterministically. When pid resolution fails, the
13
- # newest file by mtime is used instead (it is almost always the asking
14
- # session's own: its status line re-rendered the moment the current prompt
15
- # was submitted), and warnings cover the two ways that heuristic can mislead
16
- # (stale snapshot, another session writing at the same time).
17
- # =============================================================================
18
-
19
- snap_dir="${HOME:-/home/devuser}/.claude/context-usage"
20
-
21
- # All snapshots, newest first. Filenames are sanitized to [A-Za-z0-9-].json at
22
- # write time, so ls output is line-safe and word-splitting the list below is
23
- # safe too. An unmatched glob yields no stdout lines (error suppressed).
24
- files=$(ls -t -- "$snap_dir"/*.json 2>/dev/null)
25
-
26
- if [ -z "$files" ]; then
27
- echo "No context snapshots found in $snap_dir." >&2
28
- echo "Snapshots appear once Claude Code has rendered its first prompt in this session." >&2
29
- exit 1
30
- fi
31
-
32
- # Resolve the pid of this session's Claude CLI process; mirrors find_claude_pid in
33
- # claude-statusline.sh (see the comments there). Prints nothing when resolution fails.
34
- find_claude_pid() {
35
- fcp_pid=$$
36
- while [ "$fcp_pid" -gt 1 ] 2>/dev/null; do
37
- fcp_comm=""
38
- read -r fcp_comm 2>/dev/null < "/proc/$fcp_pid/comm" || return
39
- case "$fcp_comm" in
40
- claude) printf '%s' "$fcp_pid"; return ;;
41
- node*) grep -aq "claude-code" "/proc/$fcp_pid/cmdline" 2>/dev/null && { printf '%s' "$fcp_pid"; return; } ;;
42
- esac
43
- read -r fcp_stat 2>/dev/null < "/proc/$fcp_pid/stat" || return
44
- fcp_rest="${fcp_stat##*) }"
45
- set -- $fcp_rest
46
- fcp_pid=$2
47
- done
48
- }
49
-
50
- # Start time of a process (jiffies since host boot, /proc/<pid>/stat field 22); mirrors
51
- # proc_start_time in claude-statusline.sh. Prints nothing when unavailable.
52
- proc_start_time() {
53
- read -r pst_stat 2>/dev/null < "/proc/$1/stat" || return
54
- pst_rest="${pst_stat##*) }"
55
- set -- $pst_rest
56
- printf '%s' "${20}"
57
- }
58
-
59
- # CHAMBA_CLAUDE_PID and CHAMBA_CLAUDE_PID_START are test/debug overrides.
60
- my_claude_pid="${CHAMBA_CLAUDE_PID:-$(find_claude_pid)}"
61
- case "$my_claude_pid" in *[!0-9]*) my_claude_pid="" ;; esac
62
- my_pid_start=""
63
- [ -n "$my_claude_pid" ] && my_pid_start="${CHAMBA_CLAUDE_PID_START:-$(proc_start_time "$my_claude_pid")}"
64
- case "$my_pid_start" in *[!0-9]*) my_pid_start="" ;; esac
65
-
66
- # Deterministic selection first: the snapshot recorded by this session's own Claude
67
- # process. The list is newest-first, so the first match also wins when one process
68
- # wrote several session files over time (e.g. after /clear). Pids get recycled across
69
- # container restarts while snapshots persist in the bind-mounted home, so when both
70
- # sides carry a start time it must agree too; a missing start time on either side
71
- # falls back to the pid alone. The while-read loop (heredoc, not a pipe, so $matched
72
- # survives) keeps paths intact even if the directory prefix contains spaces.
73
- newest=""
74
- second=""
75
- matched=""
76
- if [ -n "$my_claude_pid" ]; then
77
- while IFS= read -r f; do
78
- pid_fields=$(jq -r '(.claude_pid // ""), (.claude_pid_start // "")' "$f" 2>/dev/null)
79
- {
80
- IFS= read -r fpid
81
- IFS= read -r fstart
82
- } <<INNER
83
- $pid_fields
84
- INNER
85
- [ "$fpid" = "$my_claude_pid" ] || continue
86
- if [ -n "$fstart" ] && [ -n "$my_pid_start" ] && [ "$fstart" != "$my_pid_start" ]; then
87
- continue
88
- fi
89
- matched="$f"
90
- break
91
- done <<EOF
92
- $files
93
- EOF
94
- fi
95
-
96
- if [ -n "$matched" ]; then
97
- newest="$matched"
98
- else
99
- # Heuristic fallback: newest by mtime, plus the runner-up for the ambiguity warning.
100
- {
101
- IFS= read -r newest
102
- IFS= read -r second
103
- } <<EOF
104
- $files
105
- EOF
106
- fi
107
-
108
- # Single jq pass over the snapshot, one field per line. Every path has a fallback so a
109
- # missing field becomes an empty string (or 0) instead of failing the script.
110
- parsed=$(jq -r '
111
- .session_id // "",
112
- .updated_at // "",
113
- .context_tokens // 0,
114
- .context_used_pct // 0,
115
- .context_window_size // 0,
116
- .model // "",
117
- .effort // "",
118
- .quota_left_pct // "",
119
- .quota_resets_at // "",
120
- .quota_week_left_pct // "",
121
- .quota_week_resets_at // ""
122
- ' "$newest" 2>/dev/null)
123
-
124
- if [ -z "$parsed" ]; then
125
- echo "Snapshot $newest is unreadable." >&2
126
- exit 1
127
- fi
128
-
129
- {
130
- IFS= read -r session_id
131
- IFS= read -r updated_at
132
- IFS= read -r tokens
133
- IFS= read -r used_pct
134
- IFS= read -r window_size
135
- IFS= read -r model
136
- IFS= read -r effort
137
- IFS= read -r quota_left
138
- IFS= read -r quota_resets_at
139
- IFS= read -r quota_week_left
140
- IFS= read -r quota_week_resets_at
141
- } <<EOF
142
- $parsed
143
- EOF
144
-
145
- now=$(date +%s)
146
-
147
- # Format a seconds delta as "Xd Yh", "Xh Ym", "Xm", or "Xs". The day unit is here for the seven-day
148
- # rate-limit window, which is days away for most of its life; the hours are truncated rather than
149
- # rounded so a delta of 4 days and 23h50m never reads "4d 24h".
150
- fmt_delta() {
151
- d=$1
152
- if [ "$d" -ge 86400 ]; then
153
- echo "$(( d / 86400 ))d $(( (d % 86400) / 3600 ))h"
154
- elif [ "$d" -ge 3600 ]; then
155
- echo "$(( d / 3600 ))h $(( (d % 3600) / 60 ))m"
156
- elif [ "$d" -ge 60 ]; then
157
- echo "$(( d / 60 ))m"
158
- else
159
- echo "${d}s"
160
- fi
161
- }
162
-
163
- # Age of the snapshot from its own updated_at field (portable, and equal to file mtime).
164
- # age stays empty when updated_at is missing or non-numeric.
165
- age=""
166
- age_label="unknown"
167
- case "$updated_at" in
168
- '' | *[!0-9]*) ;;
169
- *)
170
- age=$(( now - updated_at ))
171
- [ "$age" -lt 0 ] && age=0
172
- age_label="$(fmt_delta "$age") ago"
173
- ;;
174
- esac
175
-
176
- # Token counts read the way the strip in the web interface says them: 45.0k below
177
- # 100k, 245k above, 1M above a million.
178
- fmt_tokens() {
179
- case "$1" in
180
- '' | *[!0-9]*) printf '?' ;;
181
- *) awk -v t="$1" 'BEGIN {
182
- if (t == 0) printf "0k";
183
- else if (t >= 1000000) { m = t/1000000; if (m == int(m)) printf "%dM", m; else printf "%.1fM", m; }
184
- else if (t >= 100000) printf "%dk", int(t/1000 + 0.5);
185
- else printf "%.1fk", t/1000;
186
- }' ;;
187
- esac
188
- }
189
-
190
- tokens_label=$(fmt_tokens "$tokens")
191
-
192
- # The window the tokens sit in, when the snapshot knows how big it is. Unknown (an
193
- # older snapshot, or a release that stopped reporting it) leaves the whole clause out,
194
- # rather than naming a window with no size.
195
- window_label=""
196
- case "$window_size" in
197
- '' | 0 | *[!0-9]*) ;;
198
- *) window_label=" of $(fmt_tokens "$window_size") window" ;;
199
- esac
200
-
201
- # The "this session" marker means the snapshot was matched by pid and is guaranteed to
202
- # describe the asking session; without it the newest-file heuristic picked the snapshot.
203
- if [ -n "$matched" ]; then
204
- printf 'session: %s (this session, updated %s)\n' "${session_id:-unknown}" "$age_label"
205
- else
206
- printf 'session: %s (updated %s)\n' "${session_id:-unknown}" "$age_label"
207
- fi
208
- printf 'context: %s tokens (%s%%%s)\n' "$tokens_label" "${used_pct:-?}" "$window_label"
209
-
210
- # One rate-limit window as a line, printed only when the snapshot carried that window. Claude Code
211
- # reports two - the five-hour one, which "quota" has always meant here, and the seven-day one - and an
212
- # account may have both, one, or neither. The label is padded to the width the other lines use.
213
- quota_line() {
214
- ql_label=$1
215
- ql_left=$2
216
- ql_resets=$3
217
- [ -n "$ql_left" ] || return 0
218
- ql_reset_label=""
219
- case "$ql_resets" in
220
- '' | *[!0-9]*) ;;
221
- *)
222
- ql_delta=$(( ql_resets - now ))
223
- if [ "$ql_delta" -gt 0 ]; then
224
- ql_reset_label=", resets in $(fmt_delta "$ql_delta")"
225
- else
226
- ql_reset_label=", resets now"
227
- fi
228
- ;;
229
- esac
230
- printf '%-8s %s%% remaining%s\n' "$ql_label" "$ql_left" "$ql_reset_label"
231
- }
232
-
233
- quota_line 'quota:' "$quota_left" "$quota_resets_at"
234
- quota_line 'weekly:' "$quota_week_left" "$quota_week_resets_at"
235
-
236
- if [ -n "$model" ]; then
237
- effort_label=""
238
- [ -n "$effort" ] && effort_label=" (effort $effort)"
239
- printf 'model: %s%s\n' "$model" "$effort_label"
240
- fi
241
-
242
- # Warnings apply only to the heuristic path: a pid-matched snapshot is this session's own
243
- # by construction, however old it is (it reflects the last render, i.e. the turn start).
244
-
245
- # Freshness warning: a snapshot older than 5 minutes likely belongs to an idle session.
246
- if [ -z "$matched" ] && [ -n "$age" ] && [ "$age" -gt 300 ]; then
247
- printf 'warning: snapshot is %s old - it may not reflect the current session\n' "$(fmt_delta "$age")"
248
- fi
249
-
250
- # Ambiguity warning: another session wrote its snapshot around the same moment, so mtime
251
- # ordering may not identify the asking session. Compare updated_at of the two newest files.
252
- # Guarded on age being set, which implies updated_at is numeric. The explicit matched check
253
- # keeps this off the pid path even if a future edit populates $second unconditionally.
254
- if [ -z "$matched" ] && [ -n "$second" ] && [ -n "$age" ]; then
255
- second_updated=$(jq -r '.updated_at // ""' "$second" 2>/dev/null)
256
- case "$second_updated" in
257
- '' | *[!0-9]*) ;;
258
- *)
259
- gap=$(( updated_at - second_updated ))
260
- [ "$gap" -lt 0 ] && gap=$(( -gap ))
261
- if [ "$gap" -lt 60 ]; then
262
- printf 'warning: another session updated its snapshot within the last minute - this may not be your session (%s)\n' "$second"
263
- fi
264
- ;;
265
- esac
266
- fi