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 +2 -2
- package/dist/commands/dev.js +3 -1
- package/dist/lib/constants.js +1 -0
- package/dist/lib/migrate-to-latest.js +5 -0
- package/dist/lib/pnpm-store.js +20 -0
- package/package.json +1 -1
- package/templates/Dockerfile +2 -1
- package/templates/claude-statusline.sh +97 -20
- package/templates/skills/totopo-statusline/SKILL.md +16 -5
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 ·
|
|
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
|
-
|
|
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
|
|
package/dist/commands/dev.js
CHANGED
|
@@ -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",
|
package/dist/lib/constants.js
CHANGED
|
@@ -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
package/templates/Dockerfile
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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%
|
|
103
|
-
|
|
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="$
|
|
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
|
-
#
|
|
147
|
-
|
|
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
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
27
|
+
Four segments, left to right, separated by a mid-dot:
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
Opus 4.7 high ·
|
|
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
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|