chamba 0.3.1 → 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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -44
  3. package/bin/chamba.js +212 -0
  4. package/dist/commands/advanced.js +278 -0
  5. package/dist/commands/dev.js +619 -0
  6. package/dist/commands/doctor.js +29 -0
  7. package/dist/commands/menu.js +80 -0
  8. package/dist/commands/onboard.js +229 -0
  9. package/dist/commands/settings.js +349 -0
  10. package/dist/lib/agent-context.js +177 -0
  11. package/dist/lib/browser.js +40 -0
  12. package/dist/lib/chamba-yaml.js +191 -0
  13. package/dist/lib/constants.js +135 -0
  14. package/dist/lib/dockerfile-builder.js +267 -0
  15. package/dist/lib/env.js +78 -0
  16. package/dist/lib/global-config.js +66 -0
  17. package/dist/lib/pnpm-store.js +19 -0
  18. package/dist/lib/ports.js +210 -0
  19. package/dist/lib/safe-rm.js +26 -0
  20. package/dist/lib/sessions.js +34 -0
  21. package/dist/lib/shadows.js +174 -0
  22. package/dist/lib/webterm.js +490 -0
  23. package/dist/lib/workspace-identity.js +260 -0
  24. package/package.json +61 -23
  25. package/schema/chamba.schema.json +65 -0
  26. package/templates/.dockerignore +3 -0
  27. package/templates/Dockerfile +173 -0
  28. package/templates/claude-statusline.sh +120 -0
  29. package/templates/context/baseline.md +13 -0
  30. package/templates/context/context-usage.md +1 -0
  31. package/templates/context/git-mode-local.md +1 -0
  32. package/templates/context/git-mode-strict.md +1 -0
  33. package/templates/context/git-mode-unrestricted.md +1 -0
  34. package/templates/context/git-unavailable.md +1 -0
  35. package/templates/context/shadow-paths.md +3 -0
  36. package/templates/context-usage.sh +249 -0
  37. package/templates/git-readonly-wrapper.mjs +309 -0
  38. package/templates/npmrc +2 -0
  39. package/templates/pnpm-config.yaml +9 -0
  40. package/templates/runtime-constants.mjs +18 -0
  41. package/templates/skills/chamba-statusline/SKILL.md +79 -0
  42. package/templates/skills/context-usage/SKILL.md +53 -0
  43. package/templates/skills/web-pane/SKILL.md +62 -0
  44. package/templates/startup-git-mode.mjs +145 -0
  45. package/templates/startup.mjs +333 -0
  46. package/templates/webpane.sh +126 -0
  47. package/templates/webterm/README.md +157 -0
  48. package/templates/webterm/artifacts.js +583 -0
  49. package/templates/webterm/config.js +269 -0
  50. package/templates/webterm/context/claude.md +14 -0
  51. package/templates/webterm/conversation.js +248 -0
  52. package/templates/webterm/package-lock.json +884 -0
  53. package/templates/webterm/package.json +17 -0
  54. package/templates/webterm/pane.js +156 -0
  55. package/templates/webterm/proc.js +89 -0
  56. package/templates/webterm/public/app/alerts.js +472 -0
  57. package/templates/webterm/public/app/cards.js +123 -0
  58. package/templates/webterm/public/app/clipboard.js +229 -0
  59. package/templates/webterm/public/app/composer.js +226 -0
  60. package/templates/webterm/public/app/connection.js +342 -0
  61. package/templates/webterm/public/app/dictation.js +98 -0
  62. package/templates/webterm/public/app/dom.js +37 -0
  63. package/templates/webterm/public/app/drafts.js +244 -0
  64. package/templates/webterm/public/app/frames.js +166 -0
  65. package/templates/webterm/public/app/main.js +82 -0
  66. package/templates/webterm/public/app/new-session.js +188 -0
  67. package/templates/webterm/public/app/note.js +24 -0
  68. package/templates/webterm/public/app/pane-frame.js +166 -0
  69. package/templates/webterm/public/app/pane.js +353 -0
  70. package/templates/webterm/public/app/state.js +51 -0
  71. package/templates/webterm/public/app/status-strip.js +170 -0
  72. package/templates/webterm/public/app/tabs.js +475 -0
  73. package/templates/webterm/public/app/terminal.js +102 -0
  74. package/templates/webterm/public/app/theme.js +46 -0
  75. package/templates/webterm/public/favicon.svg +21 -0
  76. package/templates/webterm/public/index.html +105 -0
  77. package/templates/webterm/public/styles.css +1193 -0
  78. package/templates/webterm/server.js +1142 -0
  79. package/templates/webterm/sessions.js +515 -0
  80. package/templates/webterm/snapshot.js +135 -0
  81. package/templates/webterm.sh +167 -0
  82. package/dist/cli.js +0 -1582
  83. package/dist/server.js +0 -1831
  84. package/inject/annotate.js +0 -18
  85. package/skill/README.md +0 -12
  86. package/skill/SKILL.md +0 -93
  87. package/web/assets/highlighted-body-OFNGDK62-Bn4Eu7CG.js +0 -1
  88. package/web/assets/index-B9DI4F1Z.js +0 -202
  89. package/web/assets/index-DK_n6CTo.css +0 -2
  90. package/web/assets/mermaid-GHXKKRXX-CEMduc-U.js +0 -1
  91. package/web/index.html +0 -28
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // git-readonly-wrapper.mjs -- Read-only git wrapper for strict mode
4
+ // Baked into the container image at /usr/local/share/chamba/git-readonly.
5
+ // startup.mjs symlinks /usr/local/bin/git -> this file when git_mode=strict.
6
+ // PATH puts /usr/local/bin before /usr/bin so this is invoked when an agent
7
+ // runs `git`. Allowed subcommands forward to /usr/bin/git unchanged; blocked
8
+ // ones print a clear error and exit non-zero.
9
+ //
10
+ // Threat model: guardrails for cooperative agents, not adversarial containment.
11
+ // /usr/bin/git remains accessible by absolute path; remote ops stay blocked at
12
+ // the gitconfig protocol layer regardless of which binary is invoked.
13
+ //
14
+ // The classifier is exported for unit testing. Pure Node built-ins, no deps.
15
+ // =============================================================================
16
+
17
+ import { spawnSync } from "node:child_process";
18
+ import { realpathSync } from "node:fs";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const REAL_GIT = "/usr/bin/git";
22
+
23
+ // -- Read-only global actions: print and exit, no subcommand needed -----------
24
+ const READ_ONLY_GLOBAL_ACTIONS = new Set(["--version", "--help", "-h", "--html-path", "--man-path", "--info-path"]);
25
+
26
+ // -- Global flags that consume the next argv element as their value -----------
27
+ const TWO_ARG_GLOBALS = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--super-prefix", "--attr-source"]);
28
+
29
+ // -- Subcommands that are unconditionally read-only ---------------------------
30
+ const READ_SUBCOMMANDS = new Set([
31
+ "status",
32
+ "log",
33
+ "show",
34
+ "diff",
35
+ "blame",
36
+ "reflog",
37
+ "rev-parse",
38
+ "rev-list",
39
+ "describe",
40
+ "cat-file",
41
+ "name-rev",
42
+ "fsck",
43
+ "shortlog",
44
+ "grep",
45
+ "count-objects",
46
+ "var",
47
+ "help",
48
+ "version",
49
+ "ls-files",
50
+ "ls-tree",
51
+ "merge-base",
52
+ "for-each-ref",
53
+ "show-ref",
54
+ "symbolic-ref",
55
+ "check-ignore",
56
+ "check-attr",
57
+ "check-mailmap",
58
+ "check-ref-format",
59
+ "whatchanged",
60
+ "cherry",
61
+ "range-diff",
62
+ "verify-commit",
63
+ "verify-tag",
64
+ "annotate",
65
+ "instaweb",
66
+ "diff-tree",
67
+ "diff-index",
68
+ "diff-files",
69
+ ]);
70
+
71
+ // -- branch/tag flags that indicate mutation; block on any match --------------
72
+ const BRANCH_MUTATING_FLAGS = new Set([
73
+ "-d",
74
+ "-D",
75
+ "-m",
76
+ "-M",
77
+ "-c",
78
+ "-C",
79
+ "--delete",
80
+ "--move",
81
+ "--copy",
82
+ "--set-upstream",
83
+ "--set-upstream-to",
84
+ "--unset-upstream",
85
+ "--edit-description",
86
+ "--create-reflog",
87
+ ]);
88
+
89
+ const TAG_MUTATING_FLAGS = new Set([
90
+ "-d",
91
+ "-D",
92
+ "-m",
93
+ "-a",
94
+ "-s",
95
+ "-u",
96
+ "-f",
97
+ "--delete",
98
+ "--message",
99
+ "--annotate",
100
+ "--sign",
101
+ "--local-user",
102
+ "--cleanup",
103
+ "--force",
104
+ ]);
105
+
106
+ // -- config flags: explicit read/write markers + flags that consume the next arg ----
107
+ const CONFIG_READ_FLAGS = new Set([
108
+ "--get",
109
+ "--get-all",
110
+ "--get-regexp",
111
+ "--get-urlmatch",
112
+ "--get-color",
113
+ "--get-colorbool",
114
+ "--list",
115
+ "-l",
116
+ "--show-origin",
117
+ "--show-scope",
118
+ "--name-only",
119
+ ]);
120
+ const CONFIG_WRITE_FLAGS = new Set([
121
+ "--unset",
122
+ "--unset-all",
123
+ "--add",
124
+ "--replace-all",
125
+ "--remove-section",
126
+ "--rename-section",
127
+ "-e",
128
+ "--edit",
129
+ ]);
130
+ // Flags that take their next arg as a value -- skip the value when counting tokens.
131
+ // --file/-f/--blob = scope; --type = value coercion; --default = fallback for --get.
132
+ const CONFIG_TWO_ARG_FLAGS = new Set(["--file", "-f", "--blob", "--type", "--default"]);
133
+ // Keys whose write form is allowed in strict mode. Narrow by design: each entry must be
134
+ // safe (cannot escalate to remote ops or weaken protocol.allow). core.hooksPath unblocks
135
+ // `pnpm install` for repos whose prepare script points git at a tracked hooks directory.
136
+ // Compared lowercased: git treats section/variable names case-insensitively.
137
+ const CONFIG_WRITE_ALLOWLIST = new Set(["core.hookspath"]);
138
+
139
+ // -- remote: block these subactions, allow the rest (default = list) ----------
140
+ const REMOTE_MUTATING_ACTIONS = new Set(["add", "remove", "rm", "rename", "set-url", "prune", "update", "set-head", "set-branches"]);
141
+
142
+ // -- stash: only these subactions are read-only; bare `git stash` mutates -----
143
+ const STASH_READ_ACTIONS = new Set(["list", "show"]);
144
+
145
+ // -- worktree: only `list` is read-only ---------------------------------------
146
+ const WORKTREE_READ_ACTIONS = new Set(["list"]);
147
+
148
+ // -- notes: list/show are read; bare `git notes` defaults to list -------------
149
+ const NOTES_READ_ACTIONS = new Set(["list", "show", "get-ref"]);
150
+
151
+ // -- bisect: log/view are read; everything else mutates the bisect state ------
152
+ const BISECT_READ_ACTIONS = new Set(["log", "view"]);
153
+
154
+ /**
155
+ * Walk argv left-to-right and find the first non-flag token (the subcommand).
156
+ * Skips git's global option flags, including ones whose value is in the next argv slot.
157
+ * Returns { subcmd, rest } where rest is the args after the subcommand.
158
+ */
159
+ export function findSubcommand(argv) {
160
+ let i = 0;
161
+ while (i < argv.length) {
162
+ const arg = argv[i];
163
+ if (!arg.startsWith("-")) {
164
+ return { subcmd: arg, rest: argv.slice(i + 1) };
165
+ }
166
+ if (TWO_ARG_GLOBALS.has(arg)) {
167
+ // -c key=value, -C path, --git-dir path, etc.
168
+ i += 2;
169
+ continue;
170
+ }
171
+ // --foo=bar, --bare, --no-pager, --paginate, etc. - one-arg
172
+ i += 1;
173
+ }
174
+ return { subcmd: null, rest: [] };
175
+ }
176
+
177
+ /**
178
+ * Classify a git invocation under strict mode.
179
+ * Returns { allow: true } or { allow: false, reason: string }.
180
+ * Pure function - exported for unit testing without forking.
181
+ */
182
+ export function classify(argv) {
183
+ // Read-only global actions (--version, --help, etc.) short-circuit.
184
+ for (const a of argv) {
185
+ if (READ_ONLY_GLOBAL_ACTIONS.has(a)) return { allow: true };
186
+ if (a.startsWith("--list-cmds=")) return { allow: true };
187
+ }
188
+
189
+ const { subcmd, rest } = findSubcommand(argv);
190
+
191
+ // Bare `git` (no subcommand) prints usage - read-only.
192
+ if (subcmd === null) return { allow: true };
193
+
194
+ if (READ_SUBCOMMANDS.has(subcmd)) return { allow: true };
195
+
196
+ if (subcmd === "branch") {
197
+ for (const a of rest) {
198
+ if (BRANCH_MUTATING_FLAGS.has(a)) return blocked(`branch ${a}`);
199
+ }
200
+ return { allow: true };
201
+ }
202
+
203
+ if (subcmd === "tag") {
204
+ for (const a of rest) {
205
+ if (TAG_MUTATING_FLAGS.has(a)) return blocked(`tag ${a}`);
206
+ }
207
+ return { allow: true };
208
+ }
209
+
210
+ if (subcmd === "config") {
211
+ // Explicit write flags take precedence over everything else.
212
+ for (const a of rest) {
213
+ if (CONFIG_WRITE_FLAGS.has(a)) return blocked(`config ${a}`);
214
+ }
215
+ // Explicit read flags are an unconditional allow.
216
+ for (const a of rest) {
217
+ if (CONFIG_READ_FLAGS.has(a)) return { allow: true };
218
+ }
219
+ // Otherwise count non-flag tokens after the subcommand:
220
+ // `config <key>` -> 1 token -> read
221
+ // `config <key> <value>` -> 2 tokens -> write (allowed only if key is in allowlist)
222
+ // Scope flags (--system, --global, ...) are flags and don't count.
223
+ let nonFlagCount = 0;
224
+ let firstKey = null;
225
+ for (let i = 0; i < rest.length; i++) {
226
+ const a = rest[i];
227
+ if (a.startsWith("-")) {
228
+ if (CONFIG_TWO_ARG_FLAGS.has(a)) i++; // also consume its value
229
+ continue;
230
+ }
231
+ nonFlagCount++;
232
+ if (nonFlagCount === 1) firstKey = a;
233
+ if (nonFlagCount >= 2) {
234
+ if (firstKey !== null && CONFIG_WRITE_ALLOWLIST.has(firstKey.toLowerCase())) return { allow: true };
235
+ return blocked("config (write)");
236
+ }
237
+ }
238
+ return { allow: true };
239
+ }
240
+
241
+ if (subcmd === "stash") {
242
+ const action = firstNonFlag(rest);
243
+ if (action !== null && STASH_READ_ACTIONS.has(action)) return { allow: true };
244
+ return blocked(action ? `stash ${action}` : "stash");
245
+ }
246
+
247
+ if (subcmd === "remote") {
248
+ const action = firstNonFlag(rest);
249
+ if (action !== null && REMOTE_MUTATING_ACTIONS.has(action)) return blocked(`remote ${action}`);
250
+ return { allow: true };
251
+ }
252
+
253
+ if (subcmd === "worktree") {
254
+ const action = firstNonFlag(rest);
255
+ if (action !== null && WORKTREE_READ_ACTIONS.has(action)) return { allow: true };
256
+ return blocked(action ? `worktree ${action}` : "worktree");
257
+ }
258
+
259
+ if (subcmd === "notes") {
260
+ const action = firstNonFlag(rest);
261
+ if (action === null || NOTES_READ_ACTIONS.has(action)) return { allow: true };
262
+ return blocked(`notes ${action}`);
263
+ }
264
+
265
+ if (subcmd === "bisect") {
266
+ const action = firstNonFlag(rest);
267
+ if (action !== null && BISECT_READ_ACTIONS.has(action)) return { allow: true };
268
+ return blocked(action ? `bisect ${action}` : "bisect");
269
+ }
270
+
271
+ return blocked(subcmd);
272
+ }
273
+
274
+ function firstNonFlag(args) {
275
+ for (const a of args) {
276
+ if (!a.startsWith("-")) return a;
277
+ }
278
+ return null;
279
+ }
280
+
281
+ function blocked(label) {
282
+ return {
283
+ allow: false,
284
+ reason: `git: '${label}' blocked in strict mode (read-only). Switch git mode via 'chamba' menu > Settings > Git mode.`,
285
+ };
286
+ }
287
+
288
+ // Skip the runtime invocation when imported (e.g. by the test suite). The wrapper is normally
289
+ // invoked through the symlink at /usr/local/bin/git, so a literal argv[1] vs import.meta.url
290
+ // comparison wouldn't match -- realpathSync resolves the symlink before comparing.
291
+ function detectIsMain() {
292
+ if (!process.argv[1]) return false;
293
+ try {
294
+ return fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+ const isMain = detectIsMain();
300
+
301
+ if (isMain) {
302
+ const result = classify(process.argv.slice(2));
303
+ if (!result.allow) {
304
+ process.stderr.write(`${result.reason}\n`);
305
+ process.exit(1);
306
+ }
307
+ const child = spawnSync(REAL_GIT, process.argv.slice(2), { stdio: "inherit" });
308
+ process.exit(child.status ?? 1);
309
+ }
@@ -0,0 +1,2 @@
1
+ store-dir=/home/devuser/.local/share/pnpm/store
2
+ package-import-method=copy
@@ -0,0 +1,9 @@
1
+ # pnpm global config. pnpm 11+ reads its settings from here (~/.config/pnpm/config.yaml),
2
+ # no longer from ~/.npmrc. Keys are camelCase, unlike the kebab-case equivalents in .npmrc.
3
+ #
4
+ # storeDir pins the global store onto a bind-mounted, same-device cache so pnpm never falls back
5
+ # to creating a .pnpm-store inside the mounted /workspace. packageImportMethod=copy avoids
6
+ # cross-filesystem hardlink failures. The matching ~/.npmrc keeps this working for anyone who
7
+ # pins an older pnpm via packageManager (pnpm <= 10 still reads .npmrc).
8
+ storeDir: /home/devuser/.local/share/pnpm/store
9
+ packageImportMethod: copy
@@ -0,0 +1,18 @@
1
+ // =============================================================================
2
+ // runtime-constants.mjs -- Constants shared between container-side runtime
3
+ // scripts and the chamba CLI build.
4
+ //
5
+ // Container-side scripts (startup.mjs, startup-git-mode.mjs) cannot import
6
+ // from src/lib/constants.ts since the TS source isn't shipped to the image.
7
+ // Keep this file plain ESM and import it from both sides; src/lib/constants.ts
8
+ // re-exports the values so TS callers stay typed.
9
+ // =============================================================================
10
+
11
+ export const GIT_MODE = Object.freeze({
12
+ strict: "strict",
13
+ local: "local",
14
+ unrestricted: "unrestricted",
15
+ });
16
+
17
+ export const GIT_WRAPPER_PATH = "/usr/local/bin/git";
18
+ export const GIT_WRAPPER_SOURCE = "/usr/local/share/chamba/git-readonly";
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: chamba-statusline
3
+ description: Explain where a session's status lives in chamba - the strip in the web interface, the snapshot behind it, and the context-usage helper. Use when the user mentions the status line, asks why the terminal has none, asks what the strip above the composer means, or wants a status line of their own.
4
+ ---
5
+
6
+ # chamba-statusline: Where a session's status is
7
+
8
+ In chamba there is no status line in the terminal. The script Claude Code runs for one is installed and does run, but it prints nothing: it writes a snapshot file instead, and chamba draws the status from that file in the web interface, as a strip above the composer.
9
+
10
+ That is the answer to most questions this skill gets. The rest of it is the detail behind it, and what to do when the user wants something different.
11
+
12
+ **Important constants:**
13
+ - The chamba script is baked into the image at `{{statusline_path}}` (read-only, root-owned).
14
+ - Claude reads its status line config from `~/.claude/settings.json` (`statusLine.command`).
15
+ - Snapshots are written to `~/.claude/context-usage/<session_id>.json`, one per conversation, on every prompt render.
16
+
17
+ ## Step 1 - The strip, which is where the numbers are
18
+
19
+ The web interface shows the attached session's status in one line between the terminal and the composer. Four segments, left to right, separated by a mid-dot:
20
+
21
+ 1. **Model** - the display name Claude Code reports, with any trailing parenthetical (such as "(1M context)") trimmed, and the reasoning effort beside it.
22
+ 2. **Context** - tokens held, then the size of the window they sit in, a bar, and the percentage. The size half is dropped when Claude Code does not report a window size. The bar turns yellow as the window fills and red near the top.
23
+ 3. **Quota** - what is LEFT of the five-hour rate-limit window, as a bar and a percentage, with the time until it recharges beside it. It is an energy meter: full and green when fresh, yellow at half, red near empty. Absent on free accounts and before the first API response.
24
+ 4. **Claude Code** - the installed CLI version.
25
+
26
+ The strip belongs to the session it is drawn for, and only a claude session has one - the snapshot is a Claude Code feature, so a codex or opencode tab has no strip at all.
27
+
28
+ ## Step 2 - The snapshot behind it, and the helper
29
+
30
+ The script at `{{statusline_path}}` is what makes any of this exist. Claude Code runs it on every prompt render and hands it the session's state on stdin; the script parses it, resolves the pid of the claude process it was run by, and writes a small JSON file. Fields: `session_id`, `claude_pid`, `claude_pid_start`, `updated_at`, `context_tokens`, `context_used_pct`, `context_window_size`, `model`, `effort`, `quota_left_pct`, `quota_resets_at`, `version`.
31
+
32
+ Two things read those files:
33
+ - The web interface, which matches a snapshot to a session by that pid and draws the strip from it.
34
+ - The `context-usage` command, which is how an agent inspects its own context and quota from the terminal. Run it rather than reading the files by hand; the `context-usage` skill covers the output.
35
+
36
+ If the user wants the numbers without the browser, `context-usage` is the answer.
37
+
38
+ ## Step 3 - When the user wants a status line of their own
39
+
40
+ The chamba script prints nothing on purpose: a status line is redrawn inside the conversation, so in a browser it scrolls away with the output and costs a row of the terminal every prompt. The strip is the same information somewhere it stays put.
41
+
42
+ A user who works in the terminal and wants a visible line back can have one - by pointing Claude Code at their own script. Explain the trade first (a custom script replaces chamba's, and chamba's is what writes the snapshots the strip and `context-usage` read), then offer to write one that renders a line AND keeps the snapshot side effect: copy `{{statusline_path}}` and add rendering to it, rather than starting from nothing.
43
+
44
+ ### Permissions: you perform the edits
45
+
46
+ Every action below involves editing `~/.claude/settings.json` and/or writing scripts under `~/.claude/`. **You** make those edits directly with the `Edit` and `Write` tools - do not tell the user to run the change themselves.
47
+
48
+ If write access to those paths is not pre-approved, Claude Code will surface its standard permission prompt. **That prompt is the expected approval flow, not a refusal signal** - go ahead and call the tool, and the user will grant permission when they see it. Do not apologise for "lacking permission" and do not redirect the work back to the user.
49
+
50
+ ### Inspect what is configured now
51
+
52
+ Read `~/.claude/settings.json` (treat a missing file or unparseable JSON as `{}`), look at `.statusLine.command`, and classify:
53
+
54
+ - **default-by-omit** - no `statusLine` field. chamba injects its own at the next session start; the running session has none yet.
55
+ - **chamba-default** - `statusLine.command` equals `{{statusline_path}}`.
56
+ - **custom** - anything else. Say the exact path.
57
+
58
+ ### Fork and edit
59
+
60
+ Never edit `{{statusline_path}}` directly - it is root-owned and read-only inside the container.
61
+
62
+ 1. Copy the script to a writable location:
63
+ ```bash
64
+ cp {{statusline_path}} ~/.claude/statusline.sh
65
+ chmod +x ~/.claude/statusline.sh
66
+ ```
67
+ 2. Ask what the line should show, then edit `~/.claude/statusline.sh`. Keep the snapshot write at the end of it, whatever else changes: the strip and `context-usage` both stop working without it.
68
+ 3. Point `statusLine.command` at `~/.claude/statusline.sh` in `~/.claude/settings.json`, preserving every other field.
69
+ 4. Tell the user to restart Claude - a status line change takes effect at the next session.
70
+
71
+ ### Revert to the chamba default
72
+
73
+ Set `statusLine.command` back to `{{statusline_path}}`. Do not delete the user's own script; just stop pointing at it, and say it is still on disk. Restart Claude.
74
+
75
+ ## Notes
76
+
77
+ - Always preserve other fields in `settings.json` when editing. The file may hold unrelated user settings.
78
+ - If `settings.json` does not exist or is unparseable, create it fresh as `{ "statusLine": { ... } }`.
79
+ - A custom script must use only the fields documented in the official Claude Code statusline reference: https://code.claude.com/docs/en/statusline#available-data - consult that page for the full list of available JSON fields, their types, nullability, and conditional presence.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: context-usage
3
+ description: Report the current session's context-window and rate-limit (quota) usage. Use whenever the user asks how much context, tokens, or quota is used or left, how full the context window is, or when the rate limit resets.
4
+ ---
5
+
6
+ # context-usage: Report context and quota usage
7
+
8
+ In a chamba container you CAN see your own context-window and rate-limit usage.
9
+ The status line script chamba installs writes a per-session snapshot on every prompt render (it renders nothing in the terminal - the web interface draws a strip from the same file); a bundled helper reads it back.
10
+
11
+ ## Procedure
12
+
13
+ 1. Run:
14
+
15
+ ```bash
16
+ context-usage
17
+ ```
18
+
19
+ Example output:
20
+
21
+ ```
22
+ session: 78b4025b-... (this session, updated 3s ago)
23
+ context: 70.8k tokens (7% of 1M window)
24
+ quota: 84% remaining, resets in 21m
25
+ model: Fable 5 (effort high)
26
+ ```
27
+
28
+ 2. Answer in ONE short line, in exactly this shape:
29
+
30
+ > Context: 70.8k tokens (7%) - quota: 84% left, resets in 21m.
31
+
32
+ If the user asked only about context or only about quota, answer with just that half.
33
+ Expand beyond one line only when the user explicitly asks for more detail.
34
+ Do not add commentary, interpretation, or advice.
35
+
36
+ 3. Caveats are the exception, not the norm.
37
+ The `(this session, ...)` marker on the session line means the snapshot is guaranteed to be this session's own - say nothing about it.
38
+ Only when the marker is absent AND the output contains a `warning:` line, append one short sentence relaying that warning.
39
+
40
+ ## Interpretation notes
41
+
42
+ - The snapshot reflects usage as of the moment the current prompt was submitted; tokens consumed during the in-flight turn are not included yet.
43
+ No need to mention this unless asked.
44
+ - The `quota` line refers to the 5-hour rate-limit window and is absent when Claude Code did not report rate-limit data.
45
+
46
+ ## Fallback
47
+
48
+ If the `context-usage` command is not found, read the snapshot directly: list `~/.claude/context-usage/*.json`, pick the most recently modified file, and interpret its fields - `context_tokens`, `context_used_pct`, `context_window_size`, `model`, `effort`, `quota_left_pct`, `quota_resets_at` (epoch seconds), `updated_at` (epoch seconds), `version`, `session_id`.
49
+
50
+ If the command does not exist AND there are no snapshot files either, the container image predates this feature.
51
+ In that case tell the user to rebuild the image: start a new chamba session on the host and accept the rebuild prompt.
52
+
53
+ If the command exists but no snapshot files do, no Claude session in this container has rendered a prompt yet; say so rather than guessing values.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: web-pane
3
+ description: Publish an HTML page into the web pane beside the terminal - and ask questions with a form in it. Use when the user says "show me", "as a page", "in the pane", "publish this", "write it up", or asks for a plan, a table, a comparison, a diagram, a report, or a long explanation; and whenever you need to ask something with more options or structure than your question tooling holds.
4
+ ---
5
+
6
+ # web-pane: publish a page, and ask with it
7
+
8
+ The web pane is the panel beside the terminal in the chamba web interface. You publish a standalone HTML file into it and it appears at once, as a chip in the bar above the pane; the user clicks the chip to read it.
9
+
10
+ Publish when the answer is bigger than the terminal carries well, and when the question is.
11
+
12
+ ```bash
13
+ webpane report.html --title "Publish-ports rework"
14
+ ```
15
+
16
+ The file's own `<title>` is what the pane shows. `--title` names the file it is saved as, and defaults to the filename. The helper prints what it saved, or a plain message and a non-zero exit when it could not - the usual reason being a plain terminal session, which has no pane.
17
+
18
+ ## When to reach for it
19
+
20
+ - **Long or structured output.** A plan, a comparison table, a diagram, a report, an explanation with sections. Anything a user would want to scroll back to, keep, or read twice.
21
+ - **A question with shape.** More options than a plain prompt holds, options that need a sentence each, several questions at once, or a free-text answer alongside a choice. Your own question tooling is right for a quick fork in the road; a page is right when the choice deserves a table.
22
+ - **Anything the user asked to see.** "Show me", "write it up", "as a page" - take those literally.
23
+
24
+ Do not publish a one-paragraph answer, or a wall of code that belongs in a file. A page nobody needed is a chip in the way.
25
+
26
+ After publishing, say in one line what you put there. The page is the answer; the terminal line is a pointer to it.
27
+
28
+ ## Writing the page
29
+
30
+ One file, standalone, dark.
31
+
32
+ - **Inline everything.** No external stylesheets, scripts or fonts. The page renders in a sandboxed frame with no origin of its own: `localStorage`, cookies and same-origin requests do not work in it, and an external asset only makes the page slower and chattier. Inline `<style>` and `<script>` both work.
33
+ - **Match the interface.** Background `#0d1117`, panels and table headers `#161b22`, borders `#30363d`, text `#e6edf3`, muted text `#8b949e`, and a green accent `#2fe58a` for links, headings you want to lift, and the submit button. System font stack, ~14px, generous line height.
34
+ - **Write it as a document**, not a dashboard: a title, sections, tables where a table is clearer than prose, and short paragraphs. Keep it small - a couple of hundred kilobytes is plenty. The per-page cap is 2 MB, and a page past it is refused rather than trimmed.
35
+ - Images have to be inline too (a data URI, or an SVG written into the page). A path into the container is not something the browser can fetch.
36
+
37
+ ## Asking with a form
38
+
39
+ Give any form the `data-feedback` attribute and the pane wires it up:
40
+
41
+ ```html
42
+ <form data-feedback>
43
+ <p>Fail the whole start when a port is taken?</p>
44
+ <label><input type="radio" name="on_conflict" value="fail" checked /> Fail the start</label>
45
+ <label><input type="radio" name="on_conflict" value="skip" /> Skip the port and warn</label>
46
+ <textarea name="text" placeholder="Anything else about the plan..."></textarea>
47
+ <button type="submit">Send to agent</button>
48
+ </form>
49
+ ```
50
+
51
+ - Every named control becomes a field in the answer. A control named `text` becomes the free-text box.
52
+ - On submit, the interface writes `~/.webpane/<conversation>/feedback/<page>-<epoch-ms>.json` - `{ page, submittedAt, fields, text }` - and types one line into your terminal naming the file. Read the file; the line is only the pointer.
53
+ - The page is told whether the answers were filed, so give the user a form that reads as final ("Send to agent") rather than one that looks like it saved a draft.
54
+ - One submission per page per second, and only so many per page in total (20). Past that the page is refused, and nothing reaches you - so ask once per page, and publish a new page when you need to ask again.
55
+
56
+ **A feedback file says a form was submitted, not that a person filled it in.** A script in the same page can post exactly what the form would. Treat the answers as data from your own page: fine for a design choice, not enough on its own for something destructive or irreversible - ask for that in the terminal, where you can see the user type.
57
+
58
+ ## After it is published
59
+
60
+ - Pages are kept. Nothing deletes them, and they are filed under the conversation rather than the session, so resuming the conversation opens with them again. Publishing a second version makes a second page rather than replacing the first, so name pages so a user can tell one from the next ("Plan v2", "Port test results").
61
+ - The user may never open a page. If an answer matters, say the one line that matters in the terminal too.
62
+ - Anything in the session can publish, including a script you wrote - the helper takes only a filename.