totopo 3.5.0 → 3.5.1-rc-2

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/README.md CHANGED
@@ -194,10 +194,10 @@ totopo keeps all three CLIs on their latest published versions, checking for upd
194
194
  For convenience, every Claude session opens with a status line at the bottom of the terminal:
195
195
 
196
196
  ```
197
- Opus 4.7 high · 174k / 1M (17%) · ▓░░░░░░░░░ (13% used, resets in 3 hr 35 min)
197
+ 174k / 1M (17%) · Opus 4.7 high · Claude Code v2.1.132 · ▓░░░░░░░░░ (12% used, resets in 3 hr 35 min)
198
198
  ```
199
199
 
200
- Model and reasoning effort, current context usage with token count colored by absolute size (green below 100k, yellow up to 500k, red beyond), and a 10-block gauge of the 5-hour rate-limit window with current usage and time-to-reset (subscriber accounts only). Ask Claude `/totopo-statusline` to customize or restore the default.
200
+ Four segments: current context usage (count / window / percentage), model and reasoning effort, the installed Claude Code CLI version with a freshness hint that escalates as the install ages, and a 10-block gauge of the 5-hour rate-limit window with current usage and time-to-reset (subscriber accounts only). The line stays on a calm grey baseline and only escalates to yellow or red when something genuinely warrants attention. Ask Claude `/totopo-statusline` to customize or restore the default.
201
201
 
202
202
  ### Persistent Agent Memory
203
203
 
@@ -11,6 +11,7 @@ import { buildAgentContextDocs, buildAgentMountArgs, injectAgentContext } from "
11
11
  import { CONTAINER_STARTUP, CONTAINER_WORKSPACE, GIT_MODE, LABEL_GIT_MODE, LABEL_MANAGED, LABEL_PROFILE, LABEL_RUNTIME_ENV, LABEL_SHADOWS, PROFILE, RUNTIME_ENV, } from "../lib/constants.js";
12
12
  import { buildDockerfile, buildImageWithTempfile } from "../lib/dockerfile-builder.js";
13
13
  import { isImageStale } from "../lib/migrate-to-latest.js";
14
+ import { buildPnpmStoreMountArgs } from "../lib/pnpm-store.js";
14
15
  import { buildShadowMountArgs, ensureShadowsInSync, expandShadowPatterns } from "../lib/shadows.js";
15
16
  import { readTotopoYaml } from "../lib/totopo-yaml.js";
16
17
  import { readActiveProfile, readGitMode, writeActiveProfile } from "../lib/workspace-identity.js";
@@ -122,8 +123,9 @@ export function startContainer(opts) {
122
123
  }
123
124
  // --- Build mount args ----------------------------------------------------------------------------------------------------------------
124
125
  const agentMounts = buildAgentMountArgs(cacheDir);
126
+ const pnpmStoreMounts = buildPnpmStoreMountArgs(cacheDir);
125
127
  // Shadow mounts must come AFTER the workspace mount to overlay correctly
126
- const mountArgs = ["-v", `${workspaceRoot}:${CONTAINER_WORKSPACE}`, ...shadowMountArgs, ...agentMounts];
128
+ const mountArgs = ["-v", `${workspaceRoot}:${CONTAINER_WORKSPACE}`, ...shadowMountArgs, ...agentMounts, ...pnpmStoreMounts];
127
129
  // --- Container labels ----------------------------------------------------------------------------------------------------------------
128
130
  const labelArgs = [
129
131
  "--label",
@@ -13,6 +13,7 @@ export const PROJECTS_DIR = "projects"; // legacy v3-rc-1/rc-2; only referenced
13
13
  // Workspace cache subdirectories (under ~/.totopo/workspaces/<id>/)
14
14
  export const AGENTS_DIR = "agents";
15
15
  export const SHADOWS_DIR = "shadows";
16
+ export const PNPM_STORE_DIR = "pnpm-store";
16
17
  // Filenames
17
18
  export const TOTOPO_YAML = "totopo.yaml";
18
19
  export const LOCK_FILE = ".lock";
@@ -533,6 +533,11 @@ export function isImageStale(containerName) {
533
533
  });
534
534
  if (constantsCheck.status !== 0)
535
535
  return true;
536
+ // v3.5.1: explicit pnpm store-dir baked into ~/.npmrc to prevent pnpm's volume-check
537
+ // relocation, which would otherwise create .pnpm-store at the repo root.
538
+ const npmrcCheck = spawnSync("docker", ["exec", containerName, "grep", "-q", "^store-dir=", "/home/devuser/.npmrc"], { stdio: "pipe" });
539
+ if (npmrcCheck.status !== 0)
540
+ return true;
536
541
  // SHA-256 of the host claude-statusline.sh vs the file inside the container - any refinement to
537
542
  // the shipped script triggers an automatic rebuild prompt on next session.
538
543
  const hostScriptPath = join(PACKAGE_ROOT, "templates", "claude-statusline.sh");
@@ -0,0 +1,20 @@
1
+ // =========================================================================================================================================
2
+ // src/lib/pnpm-store.ts - Per-workspace pnpm global store mount
3
+ // pnpm hardlinks from its global store into node_modules. Hardlinks cannot cross filesystems, so when the global store sits on the
4
+ // container overlay FS while /workspace is a host bind mount, pnpm falls back to creating a per-project .pnpm-store inside the project -
5
+ // which then ends up on the host repo. Mounting a host-side cache dir onto pnpm's default global store path puts the store on the same
6
+ // device as the workspace and node_modules shadow, eliminating the fallback.
7
+ // =========================================================================================================================================
8
+ import { mkdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { CONTAINER_HOME, PNPM_STORE_DIR } from "./constants.js";
11
+ const CONTAINER_PNPM_STORE = `${CONTAINER_HOME}/.local/share/pnpm/store`;
12
+ /**
13
+ * Lazily creates the host-side pnpm store directory under the workspace cache dir
14
+ * and returns -v args mounting it onto pnpm's default global store path in the container.
15
+ */
16
+ export function buildPnpmStoreMountArgs(workspaceCacheDir) {
17
+ const hostStore = join(workspaceCacheDir, PNPM_STORE_DIR);
18
+ mkdirSync(hostStore, { recursive: true });
19
+ return ["-v", `${hostStore}:${CONTAINER_PNPM_STORE}`];
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "3.5.0",
3
+ "version": "3.5.1-rc-2",
4
4
  "description": "Run AI coding agents safely in your local codebase",
5
5
  "type": "module",
6
6
  "bin": {
@@ -85,8 +85,9 @@ RUN npm install -g \
85
85
  # ---------------------------------------------------------------------------
86
86
  RUN groupadd --gid 1001 devuser && \
87
87
  useradd --uid 1001 --gid devuser --shell /bin/bash --create-home devuser && \
88
- mkdir -p /home/devuser/.local/state /home/devuser/.local/share /home/devuser/.local/bin && \
88
+ mkdir -p /home/devuser/.local/state /home/devuser/.local/share /home/devuser/.local/share/pnpm /home/devuser/.local/bin && \
89
89
  date -u +"%Y-%m-%dT%H:%M:%S.000Z" > /home/devuser/.ai-cli-updated && \
90
+ printf 'store-dir=/home/devuser/.local/share/pnpm/store\npackage-import-method=copy\n' > /home/devuser/.npmrc && \
90
91
  chown -R devuser:devuser /home/devuser
91
92
 
92
93
  # ---------------------------------------------------------------------------
@@ -4,11 +4,15 @@
4
4
  # Baked into the container image at /usr/local/share/totopo/claude-statusline.sh
5
5
  # Referenced from ~/.claude/settings.json -> statusLine.command
6
6
  #
7
- # Render pattern:
8
- # <model> <effort> · <tokens>k / <ctx> (<pct>%) · <bar> (<rate>% used, resets in <hr> hr <min> min)
7
+ # Render pattern (4 segments separated by mid-dot):
8
+ # 1. <tokens>k / <ctx> (<pct>%)
9
+ # 2. <model> <effort>
10
+ # 3. Claude Code v<version> (<age>[, hint])
11
+ # 4. <bar> (<rate>% used, resets in <hr> hr <min> min)
9
12
  #
10
13
  # Designed to degrade gracefully: every field uses jq's // fallback, and any field that goes
11
14
  # missing in a future Claude Code release is silently skipped rather than failing the script.
15
+ # The Claude Code segment is also skipped silently when its data sources are unavailable.
12
16
  #
13
17
  # To customize or revert, ask Claude: /totopo-statusline
14
18
  # =============================================================================
@@ -57,6 +61,7 @@ YELLOW='\033[33m'
57
61
  RED='\033[31m'
58
62
  BLUE='\033[34m'
59
63
  GREY='\033[90m'
64
+ GREY_LIGHT='\033[38;5;245m'
60
65
  RESET='\033[0m'
61
66
 
62
67
  # Normalize tokens & pct (may arrive empty when current_usage is null).
@@ -82,7 +87,17 @@ awk_out=$(awk -v t="$used_tokens" -v r="$rate_pct" 'BEGIN {
82
87
  if (r != "") {
83
88
  if (r > 100) r = 100;
84
89
  if (r < 0) r = 0;
85
- filled = int(r / 10);
90
+ # Rounding tiers protect the two endpoints from misleading reads:
91
+ # exactly 0 -> empty bar; any non-zero usage rounds up to at least 1 block
92
+ # 1-9 -> 1 block (round up so a sliver never reads as empty)
93
+ # 10-90 -> nearest 10 (round half up via int((r+5)/10))
94
+ # 91-99 -> 9 blocks (round down so the bar never reads as full until truly 100)
95
+ # exactly 100 -> full bar
96
+ if (r <= 0) filled = 0;
97
+ else if (r >= 100) filled = 10;
98
+ else if (r > 90) filled = 9;
99
+ else if (r < 10) filled = 1;
100
+ else filled = int((r + 5) / 10);
86
101
  bar = "";
87
102
  for (i = 0; i < filled; i++) bar = bar "▓";
88
103
  for (i = filled; i < 10; i++) bar = bar "░";
@@ -99,11 +114,12 @@ awk_out=$(awk -v t="$used_tokens" -v r="$rate_pct" 'BEGIN {
99
114
  $awk_out
100
115
  EOF
101
116
 
102
- # Bar color by rate-limit usage: <50% green, <80% yellow, >=80% red.
103
- bar_color="$GREEN"
117
+ # Bar color by rate-limit usage: <50% light grey, <80% yellow, >=80% red.
118
+ # Light grey at the calm baseline keeps the line quiet until usage actually warrants attention.
119
+ bar_color="$GREY_LIGHT"
104
120
  if [ -n "$rate_pct" ]; then
105
121
  if [ "$rate_pct" -lt 50 ]; then
106
- bar_color="$GREEN"
122
+ bar_color="$GREY_LIGHT"
107
123
  elif [ "$rate_pct" -lt 80 ]; then
108
124
  bar_color="$YELLOW"
109
125
  else
@@ -143,30 +159,91 @@ case "$rate_resets_at" in
143
159
  ;;
144
160
  esac
145
161
 
146
- # Mid-dot separator between segments.
147
- SEP=" ${GREY}·${RESET} "
162
+ # Claude Code installed version + freshness of last update.
163
+ # Version comes from the npm package metadata; the timestamp file is written at image build time
164
+ # (Dockerfile) and at session start by startup.mjs after a successful `npm install -g ... @latest`.
165
+ # Both paths are stable and outside the default shadow patterns.
166
+ cc_pkg="/usr/lib/node_modules/@anthropic-ai/claude-code/package.json"
167
+ cc_ts_file="/home/devuser/.ai-cli-updated"
168
+
169
+ cc_version=""
170
+ [ -r "$cc_pkg" ] && cc_version=$(jq -r '.version // ""' "$cc_pkg" 2>/dev/null)
171
+
172
+ # Days since last successful update; clamped to >= 0 to absorb clock skew.
173
+ # Empty string when the timestamp file is missing or unparseable -- segment then omits the parens.
174
+ cc_age_days=""
175
+ if [ -r "$cc_ts_file" ]; then
176
+ cc_iso=$(tr -d '[:space:]' < "$cc_ts_file" 2>/dev/null)
177
+ if [ -n "$cc_iso" ]; then
178
+ cc_secs=$(date -d "$cc_iso" +%s 2>/dev/null)
179
+ if [ -n "$cc_secs" ]; then
180
+ cc_now=$(date +%s)
181
+ cc_age_days=$(( (cc_now - cc_secs) / 86400 ))
182
+ [ "$cc_age_days" -lt 0 ] && cc_age_days=0
183
+ fi
184
+ fi
185
+ fi
148
186
 
149
- # Model + effort
150
- out=""
151
- if [ -n "$model_display" ]; then
152
- out="${BLUE}${model_display}${RESET}"
153
- [ -n "$effort" ] && out="${out} ${GREY}${effort}${RESET}"
187
+ # Build the Claude Code segment. Empty when version is unknown.
188
+ # Age tiers (whole parens content takes the staleness color):
189
+ # <1d -> no parens (fresh: just the version)
190
+ # 1-6d -> "Nd ago" grey
191
+ # 7-29d -> "Nw ago, <hint>" yellow
192
+ # >=30d -> "Nmo ago, <hint>" red
193
+ # Hint reads "open a new totopo session to update" -- the auto-update only runs at container start,
194
+ # so restarting Claude inside the same container does not refresh the CLI.
195
+ cc_seg=""
196
+ if [ -n "$cc_version" ]; then
197
+ cc_seg="${GREY_LIGHT}Claude Code v${cc_version}${RESET}"
198
+ if [ -n "$cc_age_days" ] && [ "$cc_age_days" -ge 1 ]; then
199
+ if [ "$cc_age_days" -lt 7 ]; then
200
+ cc_age_label="${cc_age_days}d ago"
201
+ cc_age_color="$GREY"
202
+ elif [ "$cc_age_days" -lt 30 ]; then
203
+ cc_weeks=$((cc_age_days / 7))
204
+ cc_age_label="${cc_weeks}w ago, open a new totopo session to update"
205
+ cc_age_color="$YELLOW"
206
+ else
207
+ cc_months=$((cc_age_days / 30))
208
+ cc_age_label="${cc_months}mo ago, open a new totopo session to update"
209
+ cc_age_color="$RED"
210
+ fi
211
+ cc_seg="${cc_seg} ${cc_age_color}(${cc_age_label})${RESET}"
212
+ fi
154
213
  fi
155
214
 
156
- # <tokens> [/ <ctx>] (<pct>%)
215
+ # Mid-dot separator between segments.
216
+ SEP=" ${GREY}·${RESET} "
217
+
218
+ # Segment 1: tokens (always rendered; used_tokens defaults to 0).
157
219
  ctx_seg="${tokens_color}${tokens_label}${RESET}"
158
220
  [ -n "$ctx_size" ] && ctx_seg="${ctx_seg} ${GREY}/ ${ctx_size}${RESET}"
159
221
  ctx_seg="${ctx_seg} ${GREY}(${used_pct}%)${RESET}"
160
- if [ -n "$out" ]; then
161
- out="${out}${SEP}${ctx_seg}"
162
- else
163
- out="${ctx_seg}"
222
+
223
+ # Segment 2: model + effort (rendered only when model name is present; effort shares model color).
224
+ model_seg=""
225
+ if [ -n "$model_display" ]; then
226
+ model_seg="${BLUE}${model_display}${RESET}"
227
+ [ -n "$effort" ] && model_seg="${model_seg} ${BLUE}${effort}${RESET}"
164
228
  fi
165
229
 
166
- # <bar> (<rate>% used, resets in <hr> hr <min> min) -- only when rate-limit data is present
230
+ # Segment 3: cc_seg (computed above; may be empty).
231
+
232
+ # Segment 4: rate-limit gauge (rendered only when rate-limit data is present).
233
+ quota_seg=""
167
234
  if [ -n "$bar" ] && [ -n "$reset_label" ]; then
168
235
  quota_seg="${bar_color}${bar}${RESET} ${GREY}(${rate_pct}% used, resets in ${reset_label})${RESET}"
169
- out="${out}${SEP}${quota_seg}"
170
236
  fi
171
237
 
238
+ # Join non-empty segments with SEP, in order: tokens -> model -> claude-code -> gauge.
239
+ out=""
240
+ for seg in "$ctx_seg" "$model_seg" "$cc_seg" "$quota_seg"; do
241
+ [ -z "$seg" ] && continue
242
+ if [ -z "$out" ]; then
243
+ out="$seg"
244
+ else
245
+ out="${out}${SEP}${seg}"
246
+ fi
247
+ done
248
+
172
249
  printf '%b\n' "$out"
@@ -24,15 +24,18 @@ Tell the user which state they are in, and the exact command path if custom.
24
24
 
25
25
  ## Step 2 - Explain the totopo default render pattern
26
26
 
27
- Three segments separated by a mid-dot. Example:
27
+ Four segments, left to right, separated by a mid-dot:
28
28
 
29
29
  ```
30
- Opus 4.7 high · 174k / 1M (17%) · ▓░░░░░░░░░ (13% used, resets in 3 hr 35 min)
30
+ 174k / 1M (17%) · Opus 4.7 high · Claude Code v2.1.132 · ▓░░░░░░░░░ (12% used, resets in 3 hr 35 min)
31
31
  ```
32
32
 
33
- - Model name (blue) + reasoning effort (grey, hidden if unsupported).
34
- - Tokens used / context window size / percentage. Token count is colored: green below 100k, yellow up to 500k, red beyond.
35
- - 10-block gauge of the 5-hour rate-limit window (each block = 10%; green below 50%, yellow below 80%, red at or above 80%) followed by current usage and time until reset. Whole segment is hidden for free accounts and before the first API response.
33
+ 1. **Tokens** - current context-window usage (count / window size / percentage).
34
+ 2. **Model** - name and reasoning effort.
35
+ 3. **Claude Code** - installed CLI version, with a freshness hint shown only once the install starts to age.
36
+ 4. **Rate-limit gauge** - share of the 5-hour window used and time until it resets. Hidden on free accounts and before the first API response.
37
+
38
+ Most of the line stays calm; individual segments turn **yellow** or **red** when something deserves attention - typically a nudge to *clear or compact the context*, *update the harness by opening a new totopo session*, or *watch the rate-limit window*.
36
39
 
37
40
  ## Step 3 - Ask the user what they want
38
41
 
@@ -49,6 +52,14 @@ Based on the current state:
49
52
 
50
53
  ## Step 4 - Apply the chosen action
51
54
 
55
+ ### Permissions: you perform the edits
56
+
57
+ Every action in this step 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.
58
+
59
+ If write access to those paths is not pre-approved, Claude Code will surface its standard permission prompt to the user. **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.
60
+
61
+ Do not apologise for "lacking permission" and do not redirect the work back to the user. The only correct response to a custom-status-line revert request (or any other action below) is to attempt the edit yourself.
62
+
52
63
  ### Install the default explicitly
53
64
 
54
65
  Edit `~/.claude/settings.json` so it contains: