squadrant 0.9.0 → 0.9.1
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/package.json
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
#
|
|
3
3
|
# migrate-to-squadrant.sh — one-time live cutover from claude-cockpit → squadrant.
|
|
4
4
|
#
|
|
5
|
-
# Renames the runtime config dir, hub vault,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# Renames the runtime config dir, hub vault, this project's spoke vault, the
|
|
6
|
+
# repo folder, the local project key (projects.cockpit -> projects.squadrant),
|
|
7
|
+
# the daemon launchd label, Claude Code's per-project session/memory dirs, and
|
|
8
|
+
# rewrites config.json to the new brand.
|
|
9
|
+
# Idempotent (safe to re-run) and supports --dry-run (prints every action and a
|
|
10
|
+
# concrete config-rewrite preview WITHOUT mutating anything).
|
|
11
|
+
#
|
|
12
|
+
# The repo folder cannot mv itself while it is the running checkout, so Step 0
|
|
13
|
+
# guards that: if invoked from the old repo it prints the exact `mv` to run and
|
|
14
|
+
# exits. Re-run from the new path ($HOME/me/squadrant) to do the full cutover.
|
|
9
15
|
#
|
|
10
16
|
# This is run MANUALLY by the user at cutover — it terminates the live captain
|
|
11
17
|
# session and bounces the daemon. The old daemon keeps running old `dist` until
|
|
@@ -24,14 +30,32 @@ case "${1:-}" in
|
|
|
24
30
|
* ) echo "usage: $0 [--dry-run]" >&2; exit 2 ;;
|
|
25
31
|
esac
|
|
26
32
|
|
|
33
|
+
# Claude Code munges a project's absolute path into its state-dir name by
|
|
34
|
+
# replacing every '/' and '.' with '-' (e.g. /Users/me/claude-cockpit ->
|
|
35
|
+
# -Users-me-claude-cockpit, and the inner /.worktrees/ -> --worktrees-).
|
|
36
|
+
munge_path() { local p="${1//\//-}"; printf '%s' "${p//./-}"; }
|
|
37
|
+
|
|
27
38
|
OLD_CONFIG="$HOME/.config/cockpit"
|
|
28
39
|
NEW_CONFIG="$HOME/.config/squadrant"
|
|
29
40
|
OLD_HUB="$HOME/cockpit-hub"
|
|
30
41
|
NEW_HUB="$HOME/squadrant-hub"
|
|
42
|
+
OLD_REPO="$HOME/me/claude-cockpit"
|
|
43
|
+
NEW_REPO="$HOME/me/squadrant"
|
|
44
|
+
# Spoke vault for THIS project, addressed after the hub move (step 3).
|
|
45
|
+
OLD_SPOKE="$NEW_HUB/spokes/cockpit"
|
|
46
|
+
NEW_SPOKE="$NEW_HUB/spokes/squadrant"
|
|
31
47
|
OLD_LABEL="com.cockpit.daemon"
|
|
32
48
|
NEW_LABEL="com.squadrant.daemon"
|
|
33
49
|
OLD_PLIST="$HOME/Library/LaunchAgents/${OLD_LABEL}.plist"
|
|
34
|
-
|
|
50
|
+
# Claude Code's per-project state (session .jsonl transcripts + memory/ auto-memory)
|
|
51
|
+
# is keyed by the munged repo path, so the folder rename orphans it (the new path
|
|
52
|
+
# starts empty). Renaming the state dir prefix re-links it to the renamed repo.
|
|
53
|
+
CLAUDE_PROJECTS="$HOME/.claude/projects"
|
|
54
|
+
OLD_MUNGED="$(munge_path "$OLD_REPO")"
|
|
55
|
+
NEW_MUNGED="$(munge_path "$NEW_REPO")"
|
|
56
|
+
# MIGRATE_REPO_ROOT lets --dry-run simulate running from the renamed repo
|
|
57
|
+
# (to preview the full plan past the Step-0 guard); unset in real runs.
|
|
58
|
+
REPO_ROOT="${MIGRATE_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
35
59
|
UID_NUM="$(id -u)"
|
|
36
60
|
TS="$(date +%Y%m%d-%H%M%S)"
|
|
37
61
|
BACKUP="$HOME/squadrant-migration-backup-${TS}.tgz"
|
|
@@ -40,8 +64,9 @@ step() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
|
|
|
40
64
|
run() { printf ' $ %s\n' "$*"; [ "$DRY_RUN" -eq 1 ] || eval "$@"; }
|
|
41
65
|
note() { printf ' · %s\n' "$*"; }
|
|
42
66
|
|
|
43
|
-
# ---- config.json rewrite (pure
|
|
44
|
-
#
|
|
67
|
+
# ---- config.json rewrite (pure). Rules are scoped so only THIS project's
|
|
68
|
+
# brand strings change: "me/claude-cockpit" + "spokes/cockpit" are unique
|
|
69
|
+
# to the cockpit project, so the other 20+ projects' paths are untouched.
|
|
45
70
|
rewrite_config() { # $1=src json $2=mode (apply <dest> | preview)
|
|
46
71
|
local src="$1" mode="$2" dest="${3:-}"
|
|
47
72
|
SRC="$src" MODE="$mode" DEST="$dest" python3 - <<'PY'
|
|
@@ -51,6 +76,8 @@ with open(src) as f:
|
|
|
51
76
|
d = json.load(f)
|
|
52
77
|
RULES = [("cockpit-hub", "squadrant-hub"),
|
|
53
78
|
(".config/cockpit", ".config/squadrant"),
|
|
79
|
+
("me/claude-cockpit", "me/squadrant"),
|
|
80
|
+
("spokes/cockpit", "spokes/squadrant"),
|
|
54
81
|
("⚓ cockpit-captain", "⚓ squadrant-captain")]
|
|
55
82
|
changes = []
|
|
56
83
|
def fix(s):
|
|
@@ -69,6 +96,11 @@ def walk(o):
|
|
|
69
96
|
return fix(o)
|
|
70
97
|
return o
|
|
71
98
|
d = walk(d)
|
|
99
|
+
# Rename the project key projects.cockpit -> projects.squadrant (preserve order).
|
|
100
|
+
if isinstance(d.get("projects"), dict) and "cockpit" in d["projects"]:
|
|
101
|
+
d["projects"] = {("squadrant" if k == "cockpit" else k): v
|
|
102
|
+
for k, v in d["projects"].items()}
|
|
103
|
+
changes.append(("key:projects.cockpit", "key:projects.squadrant"))
|
|
72
104
|
if "_cockpitVersion" in d:
|
|
73
105
|
d["_squadrantVersion"] = d.pop("_cockpitVersion")
|
|
74
106
|
changes.append(("key:_cockpitVersion", "key:_squadrantVersion"))
|
|
@@ -85,10 +117,14 @@ PY
|
|
|
85
117
|
}
|
|
86
118
|
|
|
87
119
|
printf '\033[1mSquadrant migration%s\033[0m\n' "$([ "$DRY_RUN" -eq 1 ] && echo ' (DRY RUN — nothing will change)')"
|
|
88
|
-
note "repo: $
|
|
120
|
+
note "repo: $OLD_REPO -> $NEW_REPO"
|
|
89
121
|
note "config: $OLD_CONFIG -> $NEW_CONFIG"
|
|
90
122
|
note "hub: $OLD_HUB -> $NEW_HUB"
|
|
123
|
+
note "spoke: $OLD_SPOKE -> $NEW_SPOKE"
|
|
124
|
+
note "project key: projects.cockpit -> projects.squadrant"
|
|
125
|
+
note "sessions: $CLAUDE_PROJECTS/$OLD_MUNGED* -> $NEW_MUNGED*"
|
|
91
126
|
note "daemon: $OLD_LABEL -> $NEW_LABEL"
|
|
127
|
+
note "running from: $REPO_ROOT"
|
|
92
128
|
|
|
93
129
|
# Already migrated? (new dir present, old gone) — nothing to do.
|
|
94
130
|
if [ -d "$NEW_CONFIG" ] && [ ! -d "$OLD_CONFIG" ]; then
|
|
@@ -96,6 +132,31 @@ if [ -d "$NEW_CONFIG" ] && [ ! -d "$OLD_CONFIG" ]; then
|
|
|
96
132
|
exit 0
|
|
97
133
|
fi
|
|
98
134
|
|
|
135
|
+
# ---- Step 0: the repo folder must already live at its new name. The script
|
|
136
|
+
# cannot mv its own running checkout, so if we are not in $NEW_REPO we
|
|
137
|
+
# print the exact move + re-run command and stop here.
|
|
138
|
+
step "0. Repo folder rename (must run from the NEW path)"
|
|
139
|
+
if [ "$REPO_ROOT" != "$NEW_REPO" ]; then
|
|
140
|
+
note "running from: $REPO_ROOT"
|
|
141
|
+
note "expected: $NEW_REPO"
|
|
142
|
+
cat <<EOF
|
|
143
|
+
|
|
144
|
+
The repo folder still needs to be renamed, and this script cannot move its own
|
|
145
|
+
running checkout. Do it manually, then re-run from the new location:
|
|
146
|
+
|
|
147
|
+
# 1. close captains (cmux) so nothing holds the old path open
|
|
148
|
+
# 2. move the repo folder:
|
|
149
|
+
mv '$OLD_REPO' '$NEW_REPO'
|
|
150
|
+
# 3. re-run this script from the renamed repo:
|
|
151
|
+
bash '$NEW_REPO/scripts/migrate-to-squadrant.sh'$([ "$DRY_RUN" -eq 1 ] && echo ' --dry-run')
|
|
152
|
+
|
|
153
|
+
(The daemon plist and the global npm link both point at the repo dir; moving it
|
|
154
|
+
first lets steps 6-7 relink and bootstrap from the new location.)
|
|
155
|
+
EOF
|
|
156
|
+
exit 0
|
|
157
|
+
fi
|
|
158
|
+
note "running from $NEW_REPO ✓"
|
|
159
|
+
|
|
99
160
|
step "1. Backup ~/.config/cockpit and ~/cockpit-hub"
|
|
100
161
|
# Archive with paths RELATIVE to $HOME so rollback is `tar xzf <backup> -C $HOME`.
|
|
101
162
|
BACKUP_RELS=()
|
|
@@ -112,11 +173,13 @@ step "2. Stop captains + bootout the old daemon"
|
|
|
112
173
|
note "Captains are cmux workspaces — close them in cmux, or let the relaunch below recreate them."
|
|
113
174
|
run "launchctl bootout gui/${UID_NUM}/${OLD_LABEL} 2>/dev/null || true"
|
|
114
175
|
|
|
115
|
-
step "3. Move config dir and
|
|
176
|
+
step "3. Move config dir, hub vault, and this project's spoke vault"
|
|
116
177
|
if [ -d "$OLD_CONFIG" ] && [ ! -d "$NEW_CONFIG" ]; then run "mv '$OLD_CONFIG' '$NEW_CONFIG'"; else note "config move skipped (old absent or new exists)"; fi
|
|
117
178
|
if [ -d "$OLD_HUB" ] && [ ! -d "$NEW_HUB" ]; then run "mv '$OLD_HUB' '$NEW_HUB'"; else note "hub move skipped (old absent or new exists)"; fi
|
|
179
|
+
# Spoke lives under the hub; source check covers both pre- and post-hub-move location.
|
|
180
|
+
if { [ -d "$OLD_SPOKE" ] || [ -d "$OLD_HUB/spokes/cockpit" ]; } && [ ! -d "$NEW_SPOKE" ]; then run "mv '$OLD_SPOKE' '$NEW_SPOKE'"; else note "spoke move skipped (source absent or target exists)"; fi
|
|
118
181
|
|
|
119
|
-
step "4. Rewrite config.json (cockpit-hub→squadrant-hub, .config/cockpit→.config/squadrant, ⚓ cockpit-captain→⚓ squadrant-captain, _cockpitVersion key)"
|
|
182
|
+
step "4. Rewrite config.json (cockpit-hub→squadrant-hub, .config/cockpit→.config/squadrant, me/claude-cockpit→me/squadrant, spokes/cockpit→spokes/squadrant, ⚓ cockpit-captain→⚓ squadrant-captain, projects.cockpit→projects.squadrant key, _cockpitVersion key)"
|
|
120
183
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
121
184
|
CFG_SRC="$OLD_CONFIG/config.json"; [ -f "$CFG_SRC" ] || CFG_SRC="$NEW_CONFIG/config.json"
|
|
122
185
|
if [ -f "$CFG_SRC" ]; then rewrite_config "$CFG_SRC" preview; else note "no config.json found to preview"; fi
|
|
@@ -131,10 +194,69 @@ else
|
|
|
131
194
|
fi
|
|
132
195
|
fi
|
|
133
196
|
|
|
197
|
+
step "4.5 Preserve Claude Code session history + auto-memory (rename per-project state dirs)"
|
|
198
|
+
# The main project dir ($OLD_MUNGED) holds the session .jsonl transcripts and the
|
|
199
|
+
# memory/ subdir; its worktree dirs ($OLD_MUNGED--worktrees-*) share the prefix and
|
|
200
|
+
# are orphaned by the SAME folder rename. Swap the prefix on each so the renamed
|
|
201
|
+
# repo finds them. Pure rename — no data is copied or deleted (idempotent: skips
|
|
202
|
+
# any dir whose new-name target already exists).
|
|
203
|
+
if [ -d "$CLAUDE_PROJECTS" ] && [ -n "$OLD_MUNGED" ]; then
|
|
204
|
+
main_dir="$CLAUDE_PROJECTS/$OLD_MUNGED"
|
|
205
|
+
if [ -d "$main_dir" ]; then
|
|
206
|
+
sessions="$(find "$main_dir" -maxdepth 1 -name '*.jsonl' 2>/dev/null | wc -l | tr -d ' ')"
|
|
207
|
+
mem="$([ -d "$main_dir/memory" ] && find "$main_dir/memory" -type f 2>/dev/null | wc -l | tr -d ' ' || echo 0)"
|
|
208
|
+
note "main project dir: $sessions session transcript(s), $mem auto-memory file(s) to preserve"
|
|
209
|
+
fi
|
|
210
|
+
migrated=0; skipped=0
|
|
211
|
+
shopt -s nullglob
|
|
212
|
+
for src in "$CLAUDE_PROJECTS/$OLD_MUNGED" "$CLAUDE_PROJECTS/$OLD_MUNGED-"*; do
|
|
213
|
+
[ -d "$src" ] || continue
|
|
214
|
+
base="$(basename "$src")"
|
|
215
|
+
dest="$CLAUDE_PROJECTS/${NEW_MUNGED}${base#"$OLD_MUNGED"}"
|
|
216
|
+
if [ -e "$dest" ]; then note "skip (target exists): $base"; skipped=$((skipped + 1)); continue; fi
|
|
217
|
+
run "mv '$src' '$dest'"
|
|
218
|
+
migrated=$((migrated + 1))
|
|
219
|
+
done
|
|
220
|
+
shopt -u nullglob
|
|
221
|
+
note "$([ "$DRY_RUN" -eq 1 ] && echo 'would migrate' || echo 'migrated') $migrated state dir(s) (main + worktrees); skipped $skipped"
|
|
222
|
+
else
|
|
223
|
+
note "no $CLAUDE_PROJECTS dir — skipping session/memory preservation"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
step "4.6 Rewrite stale 'cockpit crew _hook' commands in Claude Code settings files"
|
|
227
|
+
# The hook-generation source already emits `squadrant crew _hook`, but settings
|
|
228
|
+
# files written on disk BEFORE the rebrand still invoke the removed `cockpit`
|
|
229
|
+
# binary, so every Stop/PostToolUse hook in the captain (and any pre-rebrand crew)
|
|
230
|
+
# fails with "cockpit: command not found". Rewrite the command token in place.
|
|
231
|
+
# Scoped to the leading "cockpit crew _hook" so permission patterns like
|
|
232
|
+
# Bash(cockpit:*) are left untouched. Idempotent (a second run matches nothing).
|
|
233
|
+
HOOK_SETTINGS=(
|
|
234
|
+
"$NEW_REPO/.claude/settings.json"
|
|
235
|
+
"$NEW_REPO/.claude/settings.local.json"
|
|
236
|
+
"$HOME/.claude/settings.json"
|
|
237
|
+
"$HOME/.claude/settings.local.json"
|
|
238
|
+
)
|
|
239
|
+
hooks_fixed=0
|
|
240
|
+
for sf in "${HOOK_SETTINGS[@]}"; do
|
|
241
|
+
[ -f "$sf" ] || continue
|
|
242
|
+
if grep -q 'cockpit crew _hook' "$sf" 2>/dev/null; then
|
|
243
|
+
run "sed -i '' 's/cockpit crew _hook/squadrant crew _hook/g' '$sf'"
|
|
244
|
+
hooks_fixed=$((hooks_fixed + 1))
|
|
245
|
+
else
|
|
246
|
+
note "ok (no stale hook): $sf"
|
|
247
|
+
fi
|
|
248
|
+
done
|
|
249
|
+
note "$([ "$DRY_RUN" -eq 1 ] && echo 'would rewrite' || echo 'rewrote') hook command in $hooks_fixed settings file(s)"
|
|
250
|
+
|
|
134
251
|
step "5. Remove old launchd plist (the rebuilt daemon installs com.squadrant.daemon.plist on first run)"
|
|
135
252
|
[ -f "$OLD_PLIST" ] && run "rm -f '$OLD_PLIST'" || note "old plist absent"
|
|
136
253
|
|
|
137
254
|
step "6. Build the rebranded binary + relink the global 'squadrant'/'squad' bin"
|
|
255
|
+
# Reinstall FIRST: the repo-folder rename (Step 0) leaves pnpm's workspace symlinks
|
|
256
|
+
# pointing at the old @cockpit/* package dirs, so a build before `install` aborts
|
|
257
|
+
# with hundreds of unresolved-import errors. `install` regenerates the links for
|
|
258
|
+
# the @squadrant/* packages at their new path; only then can the build resolve them.
|
|
259
|
+
run "pnpm -C '$REPO_ROOT' install"
|
|
138
260
|
run "pnpm -C '$REPO_ROOT' build"
|
|
139
261
|
run "pnpm -C '$REPO_ROOT' link --global"
|
|
140
262
|
note "removes the old global 'cockpit' bin; 'squadrant' and 'squad' now resolve to $REPO_ROOT/dist/index.js"
|
|
@@ -150,9 +272,15 @@ cat <<EOF
|
|
|
150
272
|
2. Verify CLI: squadrant --version (expect 0.9.0) and squad --help
|
|
151
273
|
3. Relaunch captains: squadrant launch <project> (recreates the ⚓ squadrant-captain workspaces)
|
|
152
274
|
4. Tail the log: tail -f ${NEW_CONFIG}/squadrantd.log
|
|
275
|
+
5. Verify sessions: ls ${CLAUDE_PROJECTS}/${NEW_MUNGED}/ (your history + memory/ should be here)
|
|
153
276
|
Rollback (if needed):
|
|
154
277
|
launchctl bootout gui/${UID_NUM}/${NEW_LABEL} 2>/dev/null || true
|
|
155
278
|
rm -rf ${NEW_CONFIG} ${NEW_HUB}
|
|
156
279
|
tar xzf ${BACKUP} -C ${HOME}
|
|
280
|
+
mv ${NEW_REPO} ${OLD_REPO} # move the repo folder back
|
|
281
|
+
# session/memory dirs are pure renames — move them back by prefix:
|
|
282
|
+
for d in ${CLAUDE_PROJECTS}/${NEW_MUNGED} ${CLAUDE_PROJECTS}/${NEW_MUNGED}-*; do
|
|
283
|
+
[ -d "\$d" ] && mv "\$d" "${CLAUDE_PROJECTS}/${OLD_MUNGED}\${d#${CLAUDE_PROJECTS}/${NEW_MUNGED}}"
|
|
284
|
+
done
|
|
157
285
|
# then reinstall the old plist + relink the old 'cockpit' bin
|
|
158
286
|
EOF
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# remap-claude-mem.sh — unify a claude-mem project slug after a repo rebrand.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# scripts/remap-claude-mem.sh [--dry-run] [OLD_SLUG] [NEW_SLUG]
|
|
7
|
+
# scripts/remap-claude-mem.sh --dry-run # preview, mutate nothing
|
|
8
|
+
# scripts/remap-claude-mem.sh # claude-cockpit -> squadrant
|
|
9
|
+
# scripts/remap-claude-mem.sh --dry-run cockpit-old squadrant
|
|
10
|
+
#
|
|
11
|
+
# WHAT IT DOES
|
|
12
|
+
# Makes a claude-mem project's history follow a rebrand so that, when you work
|
|
13
|
+
# in the renamed repo, the old project's memory surfaces under the new slug.
|
|
14
|
+
#
|
|
15
|
+
# Default mapping (after the claude-cockpit -> squadrant rebrand):
|
|
16
|
+
# claude-cockpit -> squadrant (the main project)
|
|
17
|
+
# claude-cockpit/<worktree> -> squadrant/<worktree> (per-worktree subslugs)
|
|
18
|
+
# cwd .../me/claude-cockpit -> .../me/squadrant (pending_messages queue)
|
|
19
|
+
#
|
|
20
|
+
# APPROACH — native `merged_into_project`, NOT a destructive project rewrite
|
|
21
|
+
# claude-mem already ships a "worktree adoption" mechanism that unifies slugs by
|
|
22
|
+
# setting observations.merged_into_project / session_summaries.merged_into_project
|
|
23
|
+
# to the target, and its read path resolves it everywhere:
|
|
24
|
+
# - session-start injection (context-generator) queries SQLite directly with
|
|
25
|
+
# `WHERE (o.project = ? OR o.merged_into_project = ?)`
|
|
26
|
+
# - semantic search (chroma) filters `{$or:[{project},{merged_into_project}]}`
|
|
27
|
+
# So pointing merged_into_project at the new slug surfaces the old rows under it.
|
|
28
|
+
# This is the approach claude-mem itself supports — chosen over rewriting
|
|
29
|
+
# `project` because it is additive, non-destructive, and reversible (the column
|
|
30
|
+
# was NULL before; the .db backup restores the exact prior state).
|
|
31
|
+
#
|
|
32
|
+
# Tables WITHOUT a merge column (sdk_sessions.project, pending_messages.cwd) are
|
|
33
|
+
# session/queue metadata, not searchable memory, and are not vector-stored — for
|
|
34
|
+
# those we rewrite the literal value (the only mechanism available, FTS-safe).
|
|
35
|
+
#
|
|
36
|
+
# FTS5 — no rebuild needed
|
|
37
|
+
# observations_fts / session_summaries_fts / user_prompts_fts index only CONTENT
|
|
38
|
+
# columns (title, narrative, text, ...), NOT project/merged_into_project. We
|
|
39
|
+
# change no FTS-indexed column, and the AFTER UPDATE triggers re-sync FTS from
|
|
40
|
+
# the (unchanged) content automatically. So FTS stays consistent with zero work.
|
|
41
|
+
#
|
|
42
|
+
# CHROMA (vector store) — intentionally NOT touched (see below)
|
|
43
|
+
# claude-mem's chroma runs as an EMBEDDED persistent store
|
|
44
|
+
# (`chroma-mcp --client-type persistent --data-dir ~/.claude-mem/chroma`), held
|
|
45
|
+
# EXCLUSIVELY by the running claude-mem worker. There is no HTTP endpoint. The
|
|
46
|
+
# only supported write path is the worker's own `chroma_update_documents`, which
|
|
47
|
+
# is invoked exclusively during worktree adoption — there is NO claude-mem CLI
|
|
48
|
+
# command to re-attribute existing docs, and a second process writing the
|
|
49
|
+
# persistent store (or raw-editing chroma.sqlite3) risks corruption. We therefore
|
|
50
|
+
# do NOT mutate chroma here.
|
|
51
|
+
#
|
|
52
|
+
# Impact: the SQLite change above fully fixes the PRIMARY surface — the memory
|
|
53
|
+
# injected at session start (which reads SQLite directly). The only gap is that
|
|
54
|
+
# EXPLICIT semantic search (mem-search / smart_search MCP tools) will keep
|
|
55
|
+
# attributing pre-rebrand observations to the OLD slug until their chroma
|
|
56
|
+
# metadata is patched. Safest fallback for that, when it matters:
|
|
57
|
+
# - new post-rebrand observations are searchable under the new slug already;
|
|
58
|
+
# - to re-attribute the historical vectors, ask claude-mem upstream for a
|
|
59
|
+
# re-sync/merge command, or stop the worker and run a one-off
|
|
60
|
+
# chroma_update_documents pass — do NOT edit chroma.sqlite3 by hand.
|
|
61
|
+
#
|
|
62
|
+
# TESTING — never touches the live DB unless you point it there
|
|
63
|
+
# Set CLAUDE_MEM_HOME to a temp dir containing a COPY of claude-mem.db to dry-run
|
|
64
|
+
# and apply against the copy. Make a consistent snapshot copy with:
|
|
65
|
+
# sqlite3 "file:$HOME/.claude-mem/claude-mem.db?immutable=1" \
|
|
66
|
+
# "VACUUM INTO '/tmp/cm-test/claude-mem.db'"
|
|
67
|
+
# CLAUDE_MEM_HOME=/tmp/cm-test scripts/remap-claude-mem.sh --dry-run
|
|
68
|
+
#
|
|
69
|
+
# WHEN TO RUN (live)
|
|
70
|
+
# Run this ONCE, AFTER the repo cutover, while claude-mem is quiet. First confirm
|
|
71
|
+
# the real new slug claude-mem assigns to the renamed repo (run any session in
|
|
72
|
+
# .../me/squadrant and check `project` in a fresh observation), then run this with
|
|
73
|
+
# that slug as NEW_SLUG. Idempotent: safe to re-run; a second run changes 0 rows.
|
|
74
|
+
#
|
|
75
|
+
set -euo pipefail
|
|
76
|
+
|
|
77
|
+
DRY_RUN=0
|
|
78
|
+
POS=()
|
|
79
|
+
for arg in "$@"; do
|
|
80
|
+
case "$arg" in
|
|
81
|
+
--dry-run) DRY_RUN=1 ;;
|
|
82
|
+
-h|--help) sed -n '2,80p' "$0" | sed 's/^#//'; exit 0 ;;
|
|
83
|
+
--*) echo "unknown flag: $arg" >&2; echo "usage: $0 [--dry-run] [OLD_SLUG] [NEW_SLUG]" >&2; exit 2 ;;
|
|
84
|
+
*) POS+=("$arg") ;;
|
|
85
|
+
esac
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
OLD_SLUG="${POS[0]:-claude-cockpit}"
|
|
89
|
+
NEW_SLUG="${POS[1]:-squadrant}"
|
|
90
|
+
|
|
91
|
+
# Slugs are interpolated into SQL; restrict to a safe charset (no quotes/slashes).
|
|
92
|
+
slug_ok() { [[ "$1" =~ ^[A-Za-z0-9._-]+$ ]]; }
|
|
93
|
+
slug_ok "$OLD_SLUG" || { echo "invalid OLD_SLUG: '$OLD_SLUG' (allowed: A-Za-z0-9._-)" >&2; exit 2; }
|
|
94
|
+
slug_ok "$NEW_SLUG" || { echo "invalid NEW_SLUG: '$NEW_SLUG' (allowed: A-Za-z0-9._-)" >&2; exit 2; }
|
|
95
|
+
[ "$OLD_SLUG" = "$NEW_SLUG" ] && { echo "OLD_SLUG and NEW_SLUG are identical — nothing to do." >&2; exit 2; }
|
|
96
|
+
|
|
97
|
+
CLAUDE_MEM_HOME="${CLAUDE_MEM_HOME:-$HOME/.claude-mem}"
|
|
98
|
+
DB="$CLAUDE_MEM_HOME/claude-mem.db"
|
|
99
|
+
# Repo working-copy paths recorded in pending_messages.cwd (overridable for tests).
|
|
100
|
+
OLD_CWD="${OLD_CWD:-$HOME/me/$OLD_SLUG}"
|
|
101
|
+
NEW_CWD="${NEW_CWD:-$HOME/me/$NEW_SLUG}"
|
|
102
|
+
TS="$(date +%Y%m%d-%H%M%S)"
|
|
103
|
+
BACKUP="$DB.bak-rebrand-$TS"
|
|
104
|
+
|
|
105
|
+
step() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
|
|
106
|
+
note() { printf ' · %s\n' "$*"; }
|
|
107
|
+
|
|
108
|
+
command -v sqlite3 >/dev/null || { echo "sqlite3 not found on PATH" >&2; exit 1; }
|
|
109
|
+
[ -f "$DB" ] || { echo "claude-mem DB not found: $DB" >&2; exit 1; }
|
|
110
|
+
|
|
111
|
+
# Read-only count helper (immutable open: never creates/locks WAL on the source).
|
|
112
|
+
roq() { sqlite3 "file:$DB?immutable=1" "$1"; }
|
|
113
|
+
|
|
114
|
+
printf '\033[1mclaude-mem slug remap%s\033[0m\n' "$([ "$DRY_RUN" -eq 1 ] && echo ' (DRY RUN — nothing will change)')"
|
|
115
|
+
note "db: $DB"
|
|
116
|
+
note "old slug: $OLD_SLUG (+ subslugs $OLD_SLUG/<worktree>)"
|
|
117
|
+
note "new slug: $NEW_SLUG"
|
|
118
|
+
note "cwd: $OLD_CWD -> $NEW_CWD"
|
|
119
|
+
|
|
120
|
+
# ---- Per-table counts of rows that WOULD change (same predicates as the UPDATEs).
|
|
121
|
+
OBS_N=$(roq "SELECT COUNT(*) FROM observations WHERE (project='$OLD_SLUG' OR project LIKE '$OLD_SLUG/%' OR merged_into_project='$OLD_SLUG') AND (merged_into_project IS NULL OR merged_into_project<>'$NEW_SLUG');")
|
|
122
|
+
SUM_N=$(roq "SELECT COUNT(*) FROM session_summaries WHERE (project='$OLD_SLUG' OR project LIKE '$OLD_SLUG/%' OR merged_into_project='$OLD_SLUG') AND (merged_into_project IS NULL OR merged_into_project<>'$NEW_SLUG');")
|
|
123
|
+
SDK_N=$(roq "SELECT COUNT(*) FROM sdk_sessions WHERE project='$OLD_SLUG' OR project LIKE '$OLD_SLUG/%';")
|
|
124
|
+
PEND_N=$(roq "SELECT COUNT(*) FROM pending_messages WHERE cwd='$OLD_CWD' OR cwd LIKE '$OLD_CWD/%';")
|
|
125
|
+
|
|
126
|
+
OBS_TOTAL_BEFORE=$(roq "SELECT COUNT(*) FROM observations;")
|
|
127
|
+
RESOLVE_BEFORE=$(roq "SELECT COUNT(*) FROM observations WHERE project='$NEW_SLUG' OR merged_into_project='$NEW_SLUG';")
|
|
128
|
+
|
|
129
|
+
step "Rows that would change"
|
|
130
|
+
note "observations.merged_into_project -> $NEW_SLUG : $OBS_N"
|
|
131
|
+
note "session_summaries.merged_into_project -> $NEW_SLUG : $SUM_N"
|
|
132
|
+
note "sdk_sessions.project rewrite : $SDK_N"
|
|
133
|
+
note "pending_messages.cwd rewrite : $PEND_N"
|
|
134
|
+
note "(observations resolvable under '$NEW_SLUG' now: $RESOLVE_BEFORE of $OBS_TOTAL_BEFORE total)"
|
|
135
|
+
|
|
136
|
+
if [ "$((OBS_N + SUM_N + SDK_N + PEND_N))" -eq 0 ]; then
|
|
137
|
+
step "Nothing to remap — already unified under '$NEW_SLUG' (idempotent no-op)."
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
142
|
+
step "Dry run complete — no changes written."
|
|
143
|
+
exit 0
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# ---- Backup is mandatory. Refuse to proceed if it fails. (Chroma is not touched,
|
|
147
|
+
# so only the .db is backed up — see header.)
|
|
148
|
+
step "Backup claude-mem.db (required)"
|
|
149
|
+
cp "$DB" "$BACKUP" || { echo "backup failed — aborting, nothing changed" >&2; exit 1; }
|
|
150
|
+
note "backup -> $BACKUP (rollback: cp '$BACKUP' '$DB')"
|
|
151
|
+
|
|
152
|
+
# ---- Apply, single transaction. busy_timeout tolerates a briefly-active worker;
|
|
153
|
+
# still, run this while claude-mem is quiet (see header).
|
|
154
|
+
step "Apply remap (single transaction)"
|
|
155
|
+
sqlite3 "$DB" <<SQL
|
|
156
|
+
.timeout 10000
|
|
157
|
+
BEGIN IMMEDIATE;
|
|
158
|
+
|
|
159
|
+
UPDATE observations
|
|
160
|
+
SET merged_into_project='$NEW_SLUG'
|
|
161
|
+
WHERE (project='$OLD_SLUG' OR project LIKE '$OLD_SLUG/%' OR merged_into_project='$OLD_SLUG')
|
|
162
|
+
AND (merged_into_project IS NULL OR merged_into_project<>'$NEW_SLUG');
|
|
163
|
+
|
|
164
|
+
UPDATE session_summaries
|
|
165
|
+
SET merged_into_project='$NEW_SLUG'
|
|
166
|
+
WHERE (project='$OLD_SLUG' OR project LIKE '$OLD_SLUG/%' OR merged_into_project='$OLD_SLUG')
|
|
167
|
+
AND (merged_into_project IS NULL OR merged_into_project<>'$NEW_SLUG');
|
|
168
|
+
|
|
169
|
+
-- sdk_sessions has no merge column: rewrite the literal project (main + subslugs).
|
|
170
|
+
UPDATE sdk_sessions SET project='$NEW_SLUG' WHERE project='$OLD_SLUG';
|
|
171
|
+
UPDATE sdk_sessions
|
|
172
|
+
SET project='$NEW_SLUG' || substr(project, length('$OLD_SLUG') + 1)
|
|
173
|
+
WHERE project LIKE '$OLD_SLUG/%';
|
|
174
|
+
|
|
175
|
+
-- pending_messages queue: re-point the repo working-copy path.
|
|
176
|
+
UPDATE pending_messages
|
|
177
|
+
SET cwd='$NEW_CWD' || substr(cwd, length('$OLD_CWD') + 1)
|
|
178
|
+
WHERE cwd='$OLD_CWD' OR cwd LIKE '$OLD_CWD/%';
|
|
179
|
+
|
|
180
|
+
COMMIT;
|
|
181
|
+
SQL
|
|
182
|
+
note "transaction committed"
|
|
183
|
+
|
|
184
|
+
# ---- Verify: history now resolves under the new slug and nothing was lost.
|
|
185
|
+
step "Verify"
|
|
186
|
+
OBS_TOTAL_AFTER=$(roq "SELECT COUNT(*) FROM observations;")
|
|
187
|
+
RESOLVE_AFTER=$(roq "SELECT COUNT(*) FROM observations WHERE project='$NEW_SLUG' OR merged_into_project='$NEW_SLUG';")
|
|
188
|
+
OLD_LEFT=$(roq "SELECT COUNT(*) FROM sdk_sessions WHERE project='$OLD_SLUG' OR project LIKE '$OLD_SLUG/%';")
|
|
189
|
+
|
|
190
|
+
note "observations total: $OBS_TOTAL_BEFORE -> $OBS_TOTAL_AFTER"
|
|
191
|
+
note "observations resolvable under '$NEW_SLUG': $RESOLVE_BEFORE -> $RESOLVE_AFTER"
|
|
192
|
+
note "sdk_sessions still on old slug: $OLD_LEFT (expect 0)"
|
|
193
|
+
|
|
194
|
+
# Data loss = the total DECREASED. A live observer can legitimately INSERT new
|
|
195
|
+
# rows mid-remap (we never DELETE), so AFTER > BEFORE is expected and fine; only
|
|
196
|
+
# AFTER < BEFORE means rows were lost. (Strict `-ne` here false-alarmed on that.)
|
|
197
|
+
if [ "$OBS_TOTAL_AFTER" -lt "$OBS_TOTAL_BEFORE" ]; then
|
|
198
|
+
echo "FATAL: observation count dropped ($OBS_TOTAL_BEFORE -> $OBS_TOTAL_AFTER) — DATA LOSS. Restore: cp '$BACKUP' '$DB'" >&2
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
if [ "$OLD_LEFT" -ne 0 ]; then
|
|
202
|
+
echo "WARN: $OLD_LEFT sdk_sessions still on old slug (unexpected)." >&2
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
step "Done — '$OLD_SLUG' history now resolves under '$NEW_SLUG'."
|
|
206
|
+
note "Re-run is a safe no-op. Chroma semantic search re-attribution: see header (intentionally not modified)."
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Live E2E for #117: spawnInjector("hidden") must NOT create a split-pane and
|
|
3
|
-
// must NOT steal focus. Drives the real built cmux driver against a throwaway
|
|
4
|
-
// cmux workspace, then inspects `cmux tree` to assert:
|
|
5
|
-
// 1. the workspace has exactly ONE pane (a split would create a second)
|
|
6
|
-
// 2. the relay surface exists as a background tab in that pane
|
|
7
|
-
// 3. the surface selected before spawn is still [selected] afterward
|
|
8
|
-
import { execFileSync } from "node:child_process";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
|
|
12
|
-
const CMUX = "/Applications/cmux.app/Contents/Resources/bin/cmux";
|
|
13
|
-
const cmux = (args) => execFileSync(CMUX, args, { encoding: "utf-8" }).trim();
|
|
14
|
-
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
15
|
-
const { createCmuxDriver } = await import(
|
|
16
|
-
"file://" + join(__dirname, "..", "dist", "runtimes", "cmux.js")
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
let failures = 0;
|
|
20
|
-
const assert = (cond, label) => {
|
|
21
|
-
console.log(` ${cond ? "✓" : "✗"} ${label}`);
|
|
22
|
-
if (!cond) failures++;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const out = cmux(["new-workspace", "--cwd", "/tmp", "--command", "bash"]);
|
|
26
|
-
const ws = out.match(/workspace:\d+/)?.[0];
|
|
27
|
-
cmux(["rename-workspace", "--workspace", ws, "zz-117-smoke"]);
|
|
28
|
-
const driver = createCmuxDriver();
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const treeBefore = cmux(["tree", "--workspace", ws]);
|
|
32
|
-
const capSurface = treeBefore.match(/(surface:\d+)\s+\[terminal\][^\n]*\[selected\]/)?.[1];
|
|
33
|
-
console.log(`workspace=${ws} captain surface=${capSurface}`);
|
|
34
|
-
|
|
35
|
-
const pane = await driver.spawnInjector({
|
|
36
|
-
captainWorkspace: { id: ws, name: "zz-117-smoke", status: "running" },
|
|
37
|
-
command: "echo notify-relay-stub; sleep 30",
|
|
38
|
-
title: "✉ notify-relay",
|
|
39
|
-
placement: "hidden",
|
|
40
|
-
});
|
|
41
|
-
console.log(`spawnInjector returned ${pane.surfaceId}`);
|
|
42
|
-
|
|
43
|
-
const tree = cmux(["tree", "--workspace", ws]);
|
|
44
|
-
console.log("--- tree after spawnInjector(hidden) ---\n" + tree);
|
|
45
|
-
|
|
46
|
-
const paneCount = (tree.match(/^\s*[├└]?[─ ]*pane\s+pane:\d+/gm) || []).length;
|
|
47
|
-
assert(paneCount === 1, `exactly ONE pane — no split (got ${paneCount})`);
|
|
48
|
-
|
|
49
|
-
const surfaces = (tree.match(/surface:\d+/g) || []);
|
|
50
|
-
assert(surfaces.includes(pane.surfaceId), `relay surface ${pane.surfaceId} present as a tab`);
|
|
51
|
-
|
|
52
|
-
const selected = tree.match(/(surface:\d+)\s+\[terminal\][^\n]*\[selected\]/)?.[1];
|
|
53
|
-
assert(selected === capSurface, `captain surface ${capSurface} still [selected] (relay did NOT steal focus; got ${selected})`);
|
|
54
|
-
|
|
55
|
-
console.log(`\n${failures === 0 ? "✔ PLACEMENT SMOKE PASSED" : `✗ ${failures} FAILURES`}`);
|
|
56
|
-
} finally {
|
|
57
|
-
cmux(["close-workspace", "--workspace", ws]);
|
|
58
|
-
process.exit(failures > 0 ? 1 : 0);
|
|
59
|
-
}
|