chamba 0.3.0 → 0.4.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 +21 -0
- package/README.md +47 -44
- package/bin/chamba.js +212 -0
- package/dist/commands/advanced.js +278 -0
- package/dist/commands/dev.js +619 -0
- package/dist/commands/doctor.js +29 -0
- package/dist/commands/menu.js +80 -0
- package/dist/commands/onboard.js +229 -0
- package/dist/commands/settings.js +349 -0
- package/dist/lib/agent-context.js +177 -0
- package/dist/lib/browser.js +40 -0
- package/dist/lib/chamba-yaml.js +191 -0
- package/dist/lib/constants.js +135 -0
- package/dist/lib/dockerfile-builder.js +267 -0
- package/dist/lib/env.js +78 -0
- package/dist/lib/global-config.js +66 -0
- package/dist/lib/pnpm-store.js +19 -0
- package/dist/lib/ports.js +210 -0
- package/dist/lib/safe-rm.js +26 -0
- package/dist/lib/sessions.js +34 -0
- package/dist/lib/shadows.js +174 -0
- package/dist/lib/webterm.js +490 -0
- package/dist/lib/workspace-identity.js +260 -0
- package/package.json +61 -24
- package/schema/chamba.schema.json +65 -0
- package/templates/.dockerignore +3 -0
- package/templates/Dockerfile +173 -0
- package/templates/claude-statusline.sh +120 -0
- package/templates/context/baseline.md +13 -0
- package/templates/context/context-usage.md +1 -0
- package/templates/context/git-mode-local.md +1 -0
- package/templates/context/git-mode-strict.md +1 -0
- package/templates/context/git-mode-unrestricted.md +1 -0
- package/templates/context/git-unavailable.md +1 -0
- package/templates/context/shadow-paths.md +3 -0
- package/templates/context-usage.sh +249 -0
- package/templates/git-readonly-wrapper.mjs +309 -0
- package/templates/npmrc +2 -0
- package/templates/pnpm-config.yaml +9 -0
- package/templates/runtime-constants.mjs +18 -0
- package/templates/skills/chamba-statusline/SKILL.md +79 -0
- package/templates/skills/context-usage/SKILL.md +53 -0
- package/templates/skills/web-pane/SKILL.md +62 -0
- package/templates/startup-git-mode.mjs +145 -0
- package/templates/startup.mjs +333 -0
- package/templates/webpane.sh +126 -0
- package/templates/webterm/README.md +157 -0
- package/templates/webterm/artifacts.js +583 -0
- package/templates/webterm/config.js +269 -0
- package/templates/webterm/context/claude.md +14 -0
- package/templates/webterm/conversation.js +248 -0
- package/templates/webterm/package-lock.json +884 -0
- package/templates/webterm/package.json +17 -0
- package/templates/webterm/pane.js +156 -0
- package/templates/webterm/proc.js +89 -0
- package/templates/webterm/public/app/alerts.js +472 -0
- package/templates/webterm/public/app/cards.js +123 -0
- package/templates/webterm/public/app/clipboard.js +229 -0
- package/templates/webterm/public/app/composer.js +226 -0
- package/templates/webterm/public/app/connection.js +342 -0
- package/templates/webterm/public/app/dictation.js +98 -0
- package/templates/webterm/public/app/dom.js +37 -0
- package/templates/webterm/public/app/drafts.js +244 -0
- package/templates/webterm/public/app/frames.js +166 -0
- package/templates/webterm/public/app/main.js +82 -0
- package/templates/webterm/public/app/new-session.js +188 -0
- package/templates/webterm/public/app/note.js +24 -0
- package/templates/webterm/public/app/pane-frame.js +166 -0
- package/templates/webterm/public/app/pane.js +353 -0
- package/templates/webterm/public/app/state.js +51 -0
- package/templates/webterm/public/app/status-strip.js +170 -0
- package/templates/webterm/public/app/tabs.js +475 -0
- package/templates/webterm/public/app/terminal.js +102 -0
- package/templates/webterm/public/app/theme.js +46 -0
- package/templates/webterm/public/favicon.svg +21 -0
- package/templates/webterm/public/index.html +105 -0
- package/templates/webterm/public/styles.css +1193 -0
- package/templates/webterm/server.js +1142 -0
- package/templates/webterm/sessions.js +515 -0
- package/templates/webterm/snapshot.js +135 -0
- package/templates/webterm.sh +167 -0
- package/dist/cli.js +0 -1691
- package/dist/server.js +0 -1919
- package/inject/annotate.js +0 -18
- package/skill/README.md +0 -12
- package/skill/SKILL.md +0 -93
- package/web/assets/highlighted-body-OFNGDK62-Bn4Eu7CG.js +0 -1
- package/web/assets/index-B9DI4F1Z.js +0 -202
- package/web/assets/index-DK_n6CTo.css +0 -2
- package/web/assets/mermaid-GHXKKRXX-CEMduc-U.js +0 -1
- package/web/index.html +0 -28
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# The Claude Code status line chamba installs - which renders nothing.
|
|
4
|
+
# Baked into the container image at /usr/local/share/chamba/claude-statusline.sh
|
|
5
|
+
# Referenced from ~/.claude/settings.json -> statusLine.command
|
|
6
|
+
#
|
|
7
|
+
# Chamba shows a session's status in the web interface, as a strip above the composer, so there
|
|
8
|
+
# is no status line in the terminal: this script prints nothing at all. What it is still here for
|
|
9
|
+
# is the side effect - Claude Code runs it on every prompt render and hands it the session's
|
|
10
|
+
# state on stdin, which makes it the one place that state can be captured from.
|
|
11
|
+
#
|
|
12
|
+
# It writes a per-session snapshot of the parsed data to
|
|
13
|
+
# ~/.claude/context-usage/<session_id>.json. Two things read those files: the web interface,
|
|
14
|
+
# which draws the strip from them, and the context-usage helper, which lets an agent inspect its
|
|
15
|
+
# own context and quota. Best-effort throughout: anything that fails is silent, because a status
|
|
16
|
+
# line that errors is a status line Claude Code complains about. Old snapshots are cleaned by
|
|
17
|
+
# startup.mjs at session start.
|
|
18
|
+
#
|
|
19
|
+
# Every field uses jq's // fallback, so a field that goes missing in a future Claude Code
|
|
20
|
+
# release becomes null in the snapshot rather than failing the script.
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
# Resolve the pid of this session's Claude CLI process: the nearest ancestor whose comm is
|
|
24
|
+
# exactly "claude" (npm bin) or a node process running claude-code. Recorded in the snapshot so
|
|
25
|
+
# a reader can match it to a session deterministically - the context-usage helper runs as a
|
|
26
|
+
# descendant of the SAME process, and the web interface knows the pid its session was started
|
|
27
|
+
# with. Prints nothing when resolution fails (no /proc, unexpected tree); readers then fall back
|
|
28
|
+
# to newest-by-mtime.
|
|
29
|
+
# Exact comm match matters: this script's own comm is "claude-statusli" when executed directly.
|
|
30
|
+
find_claude_pid() {
|
|
31
|
+
fcp_pid=$$
|
|
32
|
+
while [ "$fcp_pid" -gt 1 ] 2>/dev/null; do
|
|
33
|
+
fcp_comm=""
|
|
34
|
+
read -r fcp_comm 2>/dev/null < "/proc/$fcp_pid/comm" || return
|
|
35
|
+
case "$fcp_comm" in
|
|
36
|
+
claude) printf '%s' "$fcp_pid"; return ;;
|
|
37
|
+
node*) grep -aq "claude-code" "/proc/$fcp_pid/cmdline" 2>/dev/null && { printf '%s' "$fcp_pid"; return; } ;;
|
|
38
|
+
esac
|
|
39
|
+
read -r fcp_stat 2>/dev/null < "/proc/$fcp_pid/stat" || return
|
|
40
|
+
# Stat format is "pid (comm) state ppid ..." and comm may contain anything; strip
|
|
41
|
+
# through the last ")" so the remainder starts with state, then take field 2.
|
|
42
|
+
fcp_rest="${fcp_stat##*) }"
|
|
43
|
+
set -- $fcp_rest
|
|
44
|
+
fcp_pid=$2
|
|
45
|
+
done
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Start time of a process (jiffies since host boot, /proc/<pid>/stat field 22). Recorded next
|
|
49
|
+
# to the pid: snapshots persist in the bind-mounted home across container restarts while pids
|
|
50
|
+
# get recycled, so only the (pid, start time) pair identifies a process unambiguously.
|
|
51
|
+
# Prints nothing when the pid or /proc is unavailable; readers then match on the pid alone.
|
|
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
|
+
# Digits-only guards keep the jq tonumber below safe. CHAMBA_CLAUDE_PID and
|
|
60
|
+
# CHAMBA_CLAUDE_PID_START are test/debug overrides.
|
|
61
|
+
claude_pid="${CHAMBA_CLAUDE_PID:-$(find_claude_pid)}"
|
|
62
|
+
case "$claude_pid" in *[!0-9]*) claude_pid="" ;; esac
|
|
63
|
+
claude_pid_start=""
|
|
64
|
+
[ -n "$claude_pid" ] && claude_pid_start="${CHAMBA_CLAUDE_PID_START:-$(proc_start_time "$claude_pid")}"
|
|
65
|
+
case "$claude_pid_start" in *[!0-9]*) claude_pid_start="" ;; esac
|
|
66
|
+
|
|
67
|
+
# The installed Claude Code version, from the npm package metadata. It is not in the JSON on
|
|
68
|
+
# stdin, and the strip shows it, so it is read here and passed into the snapshot with everything
|
|
69
|
+
# else. Empty when the path is not readable, which is what the strip treats as "not known".
|
|
70
|
+
cc_pkg="/usr/lib/node_modules/@anthropic-ai/claude-code/package.json"
|
|
71
|
+
cc_version=""
|
|
72
|
+
[ -r "$cc_pkg" ] && cc_version=$(jq -r '.version // ""' "$cc_pkg" 2>/dev/null)
|
|
73
|
+
|
|
74
|
+
# One jq pass over the session state, straight into the snapshot object.
|
|
75
|
+
# Tokens are floor-rounded and percentages rounded to integers, so a reader uses them as they are.
|
|
76
|
+
# The rate limit is inverted here: the API reports the percentage USED, and everything downstream
|
|
77
|
+
# shows the percentage REMAINING (an energy-left metaphor), clamped to 0..100 so an over-limit
|
|
78
|
+
# report never becomes a negative number.
|
|
79
|
+
snapshot_json=$(jq -rc \
|
|
80
|
+
--arg claude_pid "$claude_pid" \
|
|
81
|
+
--arg claude_pid_start "$claude_pid_start" \
|
|
82
|
+
--arg version "$cc_version" '
|
|
83
|
+
(.session_id // "") as $sid |
|
|
84
|
+
{
|
|
85
|
+
session_id: $sid,
|
|
86
|
+
claude_pid: (if $claude_pid == "" then null else ($claude_pid | tonumber) end),
|
|
87
|
+
claude_pid_start: (if $claude_pid_start == "" then null else ($claude_pid_start | tonumber) end),
|
|
88
|
+
updated_at: (now | floor),
|
|
89
|
+
context_tokens: (.context_window.total_input_tokens // 0),
|
|
90
|
+
context_used_pct: ((.context_window.used_percentage // 0) | round),
|
|
91
|
+
context_window_size: (.context_window.context_window_size // 0),
|
|
92
|
+
model: ((.model.display_name // "") | split(" (") | .[0]),
|
|
93
|
+
effort: (.effort.level // ""),
|
|
94
|
+
quota_left_pct: (.rate_limits.five_hour.used_percentage
|
|
95
|
+
| if . == null then null else ((100 - round) | if . < 0 then 0 elif . > 100 then 100 else . end) end),
|
|
96
|
+
quota_resets_at: (.rate_limits.five_hour.resets_at // null),
|
|
97
|
+
version: (if $version == "" then null else $version end)
|
|
98
|
+
}' 2>/dev/null)
|
|
99
|
+
|
|
100
|
+
# The session id becomes a filename below; allow only safe characters (Claude Code emits a
|
|
101
|
+
# UUID). Anything unexpected (or an absent id) skips the write entirely.
|
|
102
|
+
session_id=$(printf '%s' "$snapshot_json" | jq -r '.session_id // ""' 2>/dev/null)
|
|
103
|
+
case "$session_id" in ''|*[!A-Za-z0-9-]*) session_id="" ;; esac
|
|
104
|
+
|
|
105
|
+
# Temp file + mv in the same directory = atomic rename; concurrent sessions write distinct files,
|
|
106
|
+
# so no locking is needed. Dotted temp names never match the *.json globs used by readers;
|
|
107
|
+
# orphans and old snapshots are cleaned up by startup.mjs. Each render is its own process, so $$
|
|
108
|
+
# makes the temp name unique without spawning mktemp.
|
|
109
|
+
if [ -n "$session_id" ] && [ -n "$snapshot_json" ]; then
|
|
110
|
+
snap_dir="${HOME:-/home/devuser}/.claude/context-usage"
|
|
111
|
+
snap_tmp="$snap_dir/.tmp.$$"
|
|
112
|
+
{
|
|
113
|
+
[ -d "$snap_dir" ] || mkdir -p "$snap_dir"
|
|
114
|
+
printf '%s\n' "$snapshot_json" > "$snap_tmp" &&
|
|
115
|
+
mv -f "$snap_tmp" "$snap_dir/$session_id.json"
|
|
116
|
+
} 2>/dev/null || rm -f "$snap_tmp" 2>/dev/null
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Nothing is printed: the strip in the web interface is where a session's status is shown.
|
|
120
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# chamba Workspace Context
|
|
2
|
+
|
|
3
|
+
You are running inside a chamba dev container.
|
|
4
|
+
|
|
5
|
+
- You have full access to the workspace directory at `/workspace`. Some operations may require running on the host.
|
|
6
|
+
- `/workspace/chamba.yaml` configures this workspace - profiles, shadow paths, environment variables, published ports, and workspace identity. Only modify it when the user explicitly asks. Every field is described in the JSON schema the file's header points at.
|
|
7
|
+
- Files outside mounted paths cannot be read, written, or executed. If a command fails because of missing files or permissions, tell the user: "This requires running on the host - please run `<command>` outside the container."
|
|
8
|
+
- This file (`{{tool_path}}`) is managed by chamba and overwritten on every session start. Do not edit it.
|
|
9
|
+
- At the start of every session, greet the user with one short line noting the chamba sandbox and the current git mode. When `CHAMBA_WEB_SESSION=1` is in your environment you are being reached through the web interface, so add that anything long or structured can go in the web pane beside the terminal. For example: "You're in a chamba sandbox - git mode: local, filesystem limited to /workspace. Ask for anything as a page and it opens in the web pane."
|
|
10
|
+
- If the user asks about the web interface (reaching this agent from a browser), tell them it starts with the container and opens in their browser when they open a session - there is nothing to turn on. `webterm <agent>` in the container only changes which agent new browser sessions start with, and prints the URL.
|
|
11
|
+
- **The web pane.** Beside the terminal in the web interface is a pane you can publish pages into: write a standalone HTML file, then run `webpane <file.html> --title "A title"`. Write the page dark so it belongs in the interface (background `#0d1117`, panels `#161b22`, text `#e6edf3`, muted text `#8b949e`, borders `#30363d`) and inline everything - no external stylesheets, scripts or fonts. The page's own `<title>` is what the pane shows. Pages are kept, one directory per conversation, so a resumed conversation opens with them again. In a plain terminal session there is no pane and the helper says so.
|
|
12
|
+
- **Prefer a page whenever the answer is longer or more structured than a terminal reply carries well** - a plan, a comparison table, a diagram, a long explanation - and whenever you need to ask something with more options or structure than your own question tooling holds. Then say in one line what you published. The page is the answer, not a summary of it.
|
|
13
|
+
- **A page can ask, not only show.** Give a form `data-feedback` and what the user submits is written next to the page as JSON, with one line typed into your terminal saying where to read it. Read that file before you answer. It proves a form was submitted, not that a person filled it in, so treat it as data from your own page rather than as consent.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- You can check your own context-window and rate-limit usage at any time: run `context-usage`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- The user set git mode to **local** in chamba - remote git operations (push, pull, fetch, clone) are blocked and will fail; local git operations are unrestricted.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- The user set git mode to **strict** in chamba - git commands that modify the repository and remote access (push, pull, fetch, clone) are blocked and return a clear error. Read-only inspection commands (e.g. `git status`, `git log`, `git show`, `git diff`, `git blame`, `git branch --list`, `git rev-parse`) work - use them freely.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- The user set git mode to **unrestricted** in chamba - chamba does not enforce any git-specific restrictions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- Git is not available - no `.git` directory was found in the workspace root. If git operations are needed, ask the user to run them on the host.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
- Shadow paths - the following patterns are overlaid with container-local storage and do not reflect the host filesystem (git-tracked paths are skipped):
|
|
2
|
+
{{pattern_list}}
|
|
3
|
+
- Matching paths start empty but may accumulate container-local content over time (e.g. `npm install` populates a shadowed `node_modules`). Do not assume they are empty, and do not attempt to sync or restore their host contents.
|
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
' "$newest" 2>/dev/null)
|
|
121
|
+
|
|
122
|
+
if [ -z "$parsed" ]; then
|
|
123
|
+
echo "Snapshot $newest is unreadable." >&2
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
IFS= read -r session_id
|
|
129
|
+
IFS= read -r updated_at
|
|
130
|
+
IFS= read -r tokens
|
|
131
|
+
IFS= read -r used_pct
|
|
132
|
+
IFS= read -r window_size
|
|
133
|
+
IFS= read -r model
|
|
134
|
+
IFS= read -r effort
|
|
135
|
+
IFS= read -r quota_left
|
|
136
|
+
IFS= read -r quota_resets_at
|
|
137
|
+
} <<EOF
|
|
138
|
+
$parsed
|
|
139
|
+
EOF
|
|
140
|
+
|
|
141
|
+
now=$(date +%s)
|
|
142
|
+
|
|
143
|
+
# Format a seconds delta as "Xh Ym", "Xm", or "Xs".
|
|
144
|
+
fmt_delta() {
|
|
145
|
+
d=$1
|
|
146
|
+
if [ "$d" -ge 3600 ]; then
|
|
147
|
+
echo "$(( d / 3600 ))h $(( (d % 3600) / 60 ))m"
|
|
148
|
+
elif [ "$d" -ge 60 ]; then
|
|
149
|
+
echo "$(( d / 60 ))m"
|
|
150
|
+
else
|
|
151
|
+
echo "${d}s"
|
|
152
|
+
fi
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Age of the snapshot from its own updated_at field (portable, and equal to file mtime).
|
|
156
|
+
# age stays empty when updated_at is missing or non-numeric.
|
|
157
|
+
age=""
|
|
158
|
+
age_label="unknown"
|
|
159
|
+
case "$updated_at" in
|
|
160
|
+
'' | *[!0-9]*) ;;
|
|
161
|
+
*)
|
|
162
|
+
age=$(( now - updated_at ))
|
|
163
|
+
[ "$age" -lt 0 ] && age=0
|
|
164
|
+
age_label="$(fmt_delta "$age") ago"
|
|
165
|
+
;;
|
|
166
|
+
esac
|
|
167
|
+
|
|
168
|
+
# Token counts read the way the strip in the web interface says them: 45.0k below
|
|
169
|
+
# 100k, 245k above, 1M above a million.
|
|
170
|
+
fmt_tokens() {
|
|
171
|
+
case "$1" in
|
|
172
|
+
'' | *[!0-9]*) printf '?' ;;
|
|
173
|
+
*) awk -v t="$1" 'BEGIN {
|
|
174
|
+
if (t == 0) printf "0k";
|
|
175
|
+
else if (t >= 1000000) { m = t/1000000; if (m == int(m)) printf "%dM", m; else printf "%.1fM", m; }
|
|
176
|
+
else if (t >= 100000) printf "%dk", int(t/1000 + 0.5);
|
|
177
|
+
else printf "%.1fk", t/1000;
|
|
178
|
+
}' ;;
|
|
179
|
+
esac
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
tokens_label=$(fmt_tokens "$tokens")
|
|
183
|
+
|
|
184
|
+
# The window the tokens sit in, when the snapshot knows how big it is. Unknown (an
|
|
185
|
+
# older snapshot, or a release that stopped reporting it) leaves the whole clause out,
|
|
186
|
+
# rather than naming a window with no size.
|
|
187
|
+
window_label=""
|
|
188
|
+
case "$window_size" in
|
|
189
|
+
'' | 0 | *[!0-9]*) ;;
|
|
190
|
+
*) window_label=" of $(fmt_tokens "$window_size") window" ;;
|
|
191
|
+
esac
|
|
192
|
+
|
|
193
|
+
# The "this session" marker means the snapshot was matched by pid and is guaranteed to
|
|
194
|
+
# describe the asking session; without it the newest-file heuristic picked the snapshot.
|
|
195
|
+
if [ -n "$matched" ]; then
|
|
196
|
+
printf 'session: %s (this session, updated %s)\n' "${session_id:-unknown}" "$age_label"
|
|
197
|
+
else
|
|
198
|
+
printf 'session: %s (updated %s)\n' "${session_id:-unknown}" "$age_label"
|
|
199
|
+
fi
|
|
200
|
+
printf 'context: %s tokens (%s%%%s)\n' "$tokens_label" "${used_pct:-?}" "$window_label"
|
|
201
|
+
|
|
202
|
+
# Quota line only when the snapshot carried rate-limit data.
|
|
203
|
+
if [ -n "$quota_left" ]; then
|
|
204
|
+
reset_label=""
|
|
205
|
+
case "$quota_resets_at" in
|
|
206
|
+
'' | *[!0-9]*) ;;
|
|
207
|
+
*)
|
|
208
|
+
delta=$(( quota_resets_at - now ))
|
|
209
|
+
if [ "$delta" -gt 0 ]; then
|
|
210
|
+
reset_label=", resets in $(fmt_delta "$delta")"
|
|
211
|
+
else
|
|
212
|
+
reset_label=", resets now"
|
|
213
|
+
fi
|
|
214
|
+
;;
|
|
215
|
+
esac
|
|
216
|
+
printf 'quota: %s%% remaining%s\n' "$quota_left" "$reset_label"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
if [ -n "$model" ]; then
|
|
220
|
+
effort_label=""
|
|
221
|
+
[ -n "$effort" ] && effort_label=" (effort $effort)"
|
|
222
|
+
printf 'model: %s%s\n' "$model" "$effort_label"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# Warnings apply only to the heuristic path: a pid-matched snapshot is this session's own
|
|
226
|
+
# by construction, however old it is (it reflects the last render, i.e. the turn start).
|
|
227
|
+
|
|
228
|
+
# Freshness warning: a snapshot older than 5 minutes likely belongs to an idle session.
|
|
229
|
+
if [ -z "$matched" ] && [ -n "$age" ] && [ "$age" -gt 300 ]; then
|
|
230
|
+
printf 'warning: snapshot is %s old - it may not reflect the current session\n' "$(fmt_delta "$age")"
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# Ambiguity warning: another session wrote its snapshot around the same moment, so mtime
|
|
234
|
+
# ordering may not identify the asking session. Compare updated_at of the two newest files.
|
|
235
|
+
# Guarded on age being set, which implies updated_at is numeric. The explicit matched check
|
|
236
|
+
# keeps this off the pid path even if a future edit populates $second unconditionally.
|
|
237
|
+
if [ -z "$matched" ] && [ -n "$second" ] && [ -n "$age" ]; then
|
|
238
|
+
second_updated=$(jq -r '.updated_at // ""' "$second" 2>/dev/null)
|
|
239
|
+
case "$second_updated" in
|
|
240
|
+
'' | *[!0-9]*) ;;
|
|
241
|
+
*)
|
|
242
|
+
gap=$(( updated_at - second_updated ))
|
|
243
|
+
[ "$gap" -lt 0 ] && gap=$(( -gap ))
|
|
244
|
+
if [ "$gap" -lt 60 ]; then
|
|
245
|
+
printf 'warning: another session updated its snapshot within the last minute - this may not be your session (%s)\n' "$second"
|
|
246
|
+
fi
|
|
247
|
+
;;
|
|
248
|
+
esac
|
|
249
|
+
fi
|