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.
- package/LICENSE +53 -14
- package/README.md +38 -11
- package/dist/commands/advanced.js +7 -278
- package/dist/commands/dev.js +11 -612
- package/dist/commands/doctor.js +1 -29
- package/dist/commands/menu.js +1 -80
- package/dist/commands/onboard.js +6 -230
- package/dist/commands/settings.js +15 -349
- package/dist/lib/agent-commands.js +2 -0
- package/dist/lib/agent-context.js +6 -184
- package/dist/lib/browser.js +1 -40
- package/dist/lib/chamba-yaml.js +10 -191
- package/dist/lib/constants.js +1 -125
- package/dist/lib/dockerfile-builder.js +41 -260
- package/dist/lib/env.js +2 -78
- package/dist/lib/git-env.js +1 -21
- package/dist/lib/global-config.js +4 -66
- package/dist/lib/pnpm-store.js +1 -19
- package/dist/lib/ports.js +1 -210
- package/dist/lib/safe-rm.js +1 -26
- package/dist/lib/sessions.js +2 -34
- package/dist/lib/shadows.js +1 -176
- package/dist/lib/skills.js +2 -0
- package/dist/lib/templates.js +1 -0
- package/dist/lib/webterm.js +1 -305
- package/dist/lib/workspace-identity.js +4 -260
- package/package.json +6 -6
- package/templates/Dockerfile +24 -12
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +2 -0
- package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
- package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
- package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
- package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
- package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
- package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
- package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
- package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
- package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
- package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
- package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
- package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
- package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
- package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
- package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
- package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
- package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
- package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
- package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
- package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
- package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
- package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
- package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
- package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
- package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
- package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
- package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
- package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
- package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
- package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
- package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
- package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/reviews/index.html +15 -0
- package/templates/pane-apps/client/specs/index.html +14 -0
- package/templates/pane-apps/server/reviews.mjs +20 -0
- package/templates/pane-apps/server/specs.mjs +5 -0
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +123 -0
- package/templates/skills/dx-review/references/acts.md +162 -0
- package/templates/skills/dx-spec/SKILL.md +561 -0
- package/templates/skills/dx-spec/references/imagination-guide.md +140 -0
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +127 -0
- package/templates/skills/dx-spec/references/spec-guide.md +145 -0
- package/templates/skills/dx-spec/references/stages.md +484 -0
- package/templates/skills/dx-spec-execute/SKILL.md +437 -0
- package/templates/startup.mjs +10 -4
- package/templates/tool-helper.sh +166 -0
- package/templates/webterm/README.md +79 -9
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +74 -0
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +4 -4
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +4 -1
- package/templates/webterm/public/app/connection.js +9 -0
- package/templates/webterm/public/app/dom.js +15 -5
- package/templates/webterm/public/app/frames.js +51 -10
- package/templates/webterm/public/app/main.js +26 -6
- package/templates/webterm/public/app/new-session.js +13 -41
- package/templates/webterm/public/app/palette.js +58 -0
- package/templates/webterm/public/app/pane-shape.js +167 -0
- package/templates/webterm/public/app/pane-shell.js +380 -0
- package/templates/webterm/public/app/pane.js +77 -193
- package/templates/webterm/public/app/reviews-host.js +15 -0
- package/templates/webterm/public/app/specs-host.js +16 -0
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/terminal.js +8 -0
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +302 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +68 -30
- package/templates/webterm/public/styles.css +270 -34
- package/templates/webterm/server.js +321 -0
- package/templates/webterm/tool-document.js +67 -0
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/index.js +25 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +133 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +148 -0
- package/templates/context/context-usage.md +0 -1
- 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
|