uv-suite 0.26.2 → 0.26.4

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/bin/cli.js CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync, spawn } = require('child_process');
4
- const path = require('path');
5
- const fs = require('fs');
3
+ const { execSync, spawn } = require("child_process");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const crypto = require("crypto");
7
+ const readline = require("readline");
6
8
 
7
- const UV_SUITE_DIR = path.resolve(__dirname, '..');
9
+ const UV_SUITE_DIR = path.resolve(__dirname, "..");
8
10
  const args = process.argv.slice(2);
9
11
  const command = args[0];
10
- const pkg = require(path.join(UV_SUITE_DIR, 'package.json'));
12
+ const pkg = require(path.join(UV_SUITE_DIR, "package.json"));
11
13
 
12
- const PERSONAS = ['spike', 'sport', 'pro', 'professional', 'auto'];
13
- const TOOLS = ['claude', 'codex'];
14
+ const PERSONAS = ["spike", "sport", "pro", "professional", "auto"];
15
+ const TOOLS = ["claude", "codex"];
14
16
 
15
17
  function usage() {
16
18
  console.log(`
@@ -57,208 +59,333 @@ function info() {
57
59
  }
58
60
 
59
61
  function install() {
60
- const installScript = path.join(UV_SUITE_DIR, 'install.sh');
62
+ const installScript = path.join(UV_SUITE_DIR, "install.sh");
61
63
  if (!fs.existsSync(installScript)) {
62
- console.error('Error: install.sh not found at', installScript);
64
+ console.error("Error: install.sh not found at", installScript);
63
65
  process.exit(1);
64
66
  }
65
- const installArgs = args.slice(1).join(' ');
67
+ const installArgs = args.slice(1).join(" ");
66
68
  try {
67
- execSync(`bash "${installScript}" ${installArgs}`, { stdio: 'inherit' });
69
+ execSync(`bash "${installScript}" ${installArgs}`, { stdio: "inherit" });
68
70
  } catch (e) {
69
71
  process.exit(e.status || 1);
70
72
  }
71
73
  }
72
74
 
73
75
  function normPersona(p) {
74
- if (p === 'pro' || p === 'professional') return 'professional';
76
+ if (p === "pro" || p === "professional") return "professional";
75
77
  if (PERSONAS.includes(p)) return p;
76
78
  return null;
77
79
  }
78
80
 
79
81
  function personaLabel(p) {
80
82
  const labels = {
81
- spike: 'Spike — research & docs (Opus, max)',
82
- sport: 'Sport — lightweight (Sonnet, high)',
83
- professional: 'Professional — full rigor (all hooks, all guardrails)',
84
- auto: 'Auto — autonomous (max, everything approved)',
83
+ spike: "Spike — research & docs (Opus, max)",
84
+ sport: "Sport — lightweight (Sonnet, high)",
85
+ professional: "Professional — full rigor (all hooks, all guardrails)",
86
+ auto: "Auto — autonomous (max, everything approved)",
85
87
  };
86
88
  return labels[p] || p;
87
89
  }
88
90
 
89
- function ensureInstalled(persona) {
90
- const hooksDir = path.resolve('.claude/hooks');
91
- const personasDir = path.resolve('.claude/personas');
92
- const needsInstall = !fs.existsSync(personasDir) || !fs.existsSync(hooksDir);
93
-
94
- if (needsInstall) {
95
- console.log('UV Suite not installed in this project. Installing core files...');
96
- console.log('');
97
-
98
- // Fast install: copy essential files directly (no pip, no brew, no slow stuff)
99
- const srcDir = UV_SUITE_DIR;
100
- const targetDir = path.resolve('.claude');
101
-
102
- // Create directories
103
- for (const dir of ['agents', 'skills', 'hooks', 'rules', 'personas']) {
104
- fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
105
- }
91
+ // Sync package-owned files (hooks, skills, personas, agents, optional guardrails)
92
+ // from the installed npm package into the project's .claude/. Idempotent — runs
93
+ // every launch so users on older versions pick up new hooks and slash commands
94
+ // after `npm install -g uv-suite@latest` without needing `uvs install` again.
95
+ // settings.json is preserved if it exists (user customizations).
96
+ function syncPackageFiles(persona) {
97
+ const srcDir = UV_SUITE_DIR;
98
+ const targetDir = path.resolve(".claude");
99
+ const hooksDir = path.join(targetDir, "hooks");
100
+ const personasDir = path.join(targetDir, "personas");
101
+ const wasFreshInstall =
102
+ !fs.existsSync(personasDir) || !fs.existsSync(hooksDir);
103
+
104
+ for (const dir of ["agents", "skills", "hooks", "rules", "personas"]) {
105
+ fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
106
+ }
106
107
 
107
- // Copy agents
108
- const agentsSrc = path.join(srcDir, 'agents', 'claude-code');
109
- if (fs.existsSync(agentsSrc)) {
110
- for (const f of fs.readdirSync(agentsSrc)) {
111
- fs.copyFileSync(path.join(agentsSrc, f), path.join(targetDir, 'agents', f));
112
- }
108
+ const agentsSrc = path.join(srcDir, "agents", "claude-code");
109
+ if (fs.existsSync(agentsSrc)) {
110
+ for (const f of fs.readdirSync(agentsSrc)) {
111
+ fs.copyFileSync(
112
+ path.join(agentsSrc, f),
113
+ path.join(targetDir, "agents", f),
114
+ );
113
115
  }
116
+ }
114
117
 
115
- // Copy hooks
116
- const hooksSrc = path.join(srcDir, 'hooks');
117
- if (fs.existsSync(hooksSrc)) {
118
- for (const f of fs.readdirSync(hooksSrc)) {
119
- const dest = path.join(targetDir, 'hooks', f);
120
- fs.copyFileSync(path.join(hooksSrc, f), dest);
121
- fs.chmodSync(dest, 0o755);
122
- }
118
+ const hooksSrc = path.join(srcDir, "hooks");
119
+ if (fs.existsSync(hooksSrc)) {
120
+ for (const f of fs.readdirSync(hooksSrc)) {
121
+ const dest = path.join(targetDir, "hooks", f);
122
+ fs.copyFileSync(path.join(hooksSrc, f), dest);
123
+ fs.chmodSync(dest, 0o755);
123
124
  }
125
+ }
124
126
 
125
- // Copy skills
126
- const skillsSrc = path.join(srcDir, 'skills');
127
- if (fs.existsSync(skillsSrc)) {
128
- for (const d of fs.readdirSync(skillsSrc)) {
129
- const skillFile = path.join(skillsSrc, d, 'SKILL.md');
130
- if (fs.existsSync(skillFile)) {
131
- const destDir = path.join(targetDir, 'skills', d);
132
- fs.mkdirSync(destDir, { recursive: true });
133
- fs.copyFileSync(skillFile, path.join(destDir, 'SKILL.md'));
134
- }
127
+ const skillsSrc = path.join(srcDir, "skills");
128
+ if (fs.existsSync(skillsSrc)) {
129
+ for (const d of fs.readdirSync(skillsSrc)) {
130
+ const skillFile = path.join(skillsSrc, d, "SKILL.md");
131
+ if (fs.existsSync(skillFile)) {
132
+ const destDir = path.join(targetDir, "skills", d);
133
+ fs.mkdirSync(destDir, { recursive: true });
134
+ fs.copyFileSync(skillFile, path.join(destDir, "SKILL.md"));
135
135
  }
136
136
  }
137
+ }
137
138
 
138
- // Copy guardrails (for professional and auto)
139
- if (persona === 'professional' || persona === 'auto') {
140
- const guardSrc = path.join(srcDir, 'guardrails');
141
- if (fs.existsSync(guardSrc)) {
142
- for (const f of fs.readdirSync(guardSrc)) {
143
- fs.copyFileSync(path.join(guardSrc, f), path.join(targetDir, 'rules', f));
144
- }
139
+ if (persona === "professional" || persona === "auto") {
140
+ const guardSrc = path.join(srcDir, "guardrails");
141
+ if (fs.existsSync(guardSrc)) {
142
+ for (const f of fs.readdirSync(guardSrc)) {
143
+ fs.copyFileSync(
144
+ path.join(guardSrc, f),
145
+ path.join(targetDir, "rules", f),
146
+ );
145
147
  }
146
148
  }
149
+ }
147
150
 
148
- // Copy personas
149
- const personasSrc = path.join(srcDir, 'personas');
150
- if (fs.existsSync(personasSrc)) {
151
- for (const f of fs.readdirSync(personasSrc)) {
152
- fs.copyFileSync(path.join(personasSrc, f), path.join(targetDir, 'personas', f));
153
- }
151
+ const personasSrc = path.join(srcDir, "personas");
152
+ if (fs.existsSync(personasSrc)) {
153
+ for (const f of fs.readdirSync(personasSrc)) {
154
+ fs.copyFileSync(
155
+ path.join(personasSrc, f),
156
+ path.join(targetDir, "personas", f),
157
+ );
154
158
  }
159
+ }
155
160
 
156
- // Set settings.json from persona
157
- const personaFile = path.join(targetDir, 'personas', `${persona}.json`);
158
- const settingsFile = path.join(targetDir, 'settings.json');
159
- if (fs.existsSync(personaFile) && !fs.existsSync(settingsFile)) {
160
- fs.copyFileSync(personaFile, settingsFile);
161
- }
161
+ // settings.json is user-owned. Only seed it on fresh install.
162
+ const personaFile = path.join(targetDir, "personas", `${persona}.json`);
163
+ const settingsFile = path.join(targetDir, "settings.json");
164
+ if (
165
+ wasFreshInstall &&
166
+ fs.existsSync(personaFile) &&
167
+ !fs.existsSync(settingsFile)
168
+ ) {
169
+ fs.copyFileSync(personaFile, settingsFile);
170
+ }
162
171
 
172
+ if (wasFreshInstall) {
173
+ console.log(
174
+ "UV Suite not installed in this project. Installing core files...",
175
+ );
163
176
  console.log(` Installed: agents, skills, hooks, guardrails, personas`);
164
- console.log('');
177
+ console.log("");
178
+ }
179
+ }
180
+
181
+ function prompt(rl, question) {
182
+ return new Promise((resolve) => rl.question(question, resolve));
183
+ }
184
+
185
+ function normalizeKind(s) {
186
+ const v = (s || "").toLowerCase().trim();
187
+ if (["l", "long", "long-running"].includes(v)) return "long-running";
188
+ if (["o", "outcome"].includes(v)) return "outcome";
189
+ return "";
190
+ }
191
+
192
+ function normalizePriority(s) {
193
+ const v = (s || "").toLowerCase().trim();
194
+ if (["l", "low"].includes(v)) return "low";
195
+ if (["m", "med", "medium"].includes(v)) return "med";
196
+ if (["h", "high"].includes(v)) return "high";
197
+ return "";
198
+ }
199
+
200
+ // Generate a UVS_SESSION_ID, prompt for metadata (name/kind/purpose/priority),
201
+ // write it to .uv-suite-state/sessions/<sid>.json, and return the id + name.
202
+ // Skipping (Enter) leaves a field empty; the session-label-nag.sh hook will
203
+ // remind the user to run /session-init mid-flight.
204
+ async function setupSession(persona) {
205
+ const projectDir = process.cwd();
206
+ const stateDir = path.join(projectDir, ".uv-suite-state");
207
+ const sessionsDir = path.join(stateDir, "sessions");
208
+ fs.mkdirSync(sessionsDir, { recursive: true });
209
+
210
+ const sid = crypto.randomUUID();
211
+ let name = "";
212
+ let kind = "";
213
+ let purpose = "";
214
+ let priority = "";
215
+
216
+ if (process.stdin.isTTY && !process.env.UVS_NO_PROMPT) {
217
+ const rl = readline.createInterface({
218
+ input: process.stdin,
219
+ output: process.stdout,
220
+ });
221
+ console.log("");
222
+ console.log("Label this session (Enter to skip — you'll be reminded):");
223
+ name = (await prompt(rl, " name: ")).trim();
224
+ const kindRaw = await prompt(rl, " kind [long/outcome]: ");
225
+ purpose = (await prompt(rl, " purpose: ")).trim();
226
+ const priorityRaw = await prompt(rl, " priority [low/med/high]: ");
227
+ rl.close();
228
+ kind = normalizeKind(kindRaw);
229
+ priority = normalizePriority(priorityRaw);
165
230
  }
231
+
232
+ const meta = {
233
+ uvs_session_id: sid,
234
+ name,
235
+ kind,
236
+ purpose,
237
+ priority,
238
+ persona,
239
+ cwd: projectDir,
240
+ started_at: Math.floor(Date.now() / 1000),
241
+ };
242
+ fs.writeFileSync(
243
+ path.join(sessionsDir, `${sid}.json`),
244
+ JSON.stringify(meta, null, 2),
245
+ );
246
+ fs.writeFileSync(path.join(stateDir, "current-session.txt"), sid);
247
+
248
+ return { sid, name };
249
+ }
250
+
251
+ // Backwards-compat shim — older code in this file still references this name.
252
+ function ensureInstalled(persona) {
253
+ syncPackageFiles(persona);
166
254
  }
167
255
 
168
- function launchClaude(persona, extra) {
169
- ensureInstalled(persona);
170
- const settings = path.resolve('.claude/personas', `${persona}.json`);
256
+ async function launchClaude(persona, extra) {
257
+ syncPackageFiles(persona);
258
+ const settings = path.resolve(".claude/personas", `${persona}.json`);
171
259
  if (!fs.existsSync(settings)) {
172
- console.error(`Error: installation failed. Run 'uvs install --persona ${persona}' manually.`);
260
+ console.error(
261
+ `Error: installation failed. Run 'uvs install --persona ${persona}' manually.`,
262
+ );
173
263
  process.exit(1);
174
264
  }
265
+ const { sid, name } = await setupSession(persona);
266
+ console.log("");
175
267
  console.log(`UV Suite | Claude Code | ${personaLabel(persona)}`);
176
- console.log('');
177
- const child = spawn('claude', ['--settings', settings, ...extra], { stdio: 'inherit' });
178
- child.on('exit', (code) => process.exit(code || 0));
268
+ console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
269
+ console.log("");
270
+ const child = spawn("claude", ["--settings", settings, ...extra], {
271
+ stdio: "inherit",
272
+ env: { ...process.env, UVS_SESSION_ID: sid },
273
+ });
274
+ child.on("exit", (code) => process.exit(code || 0));
179
275
  }
180
276
 
181
- function launchCodex(persona, extra) {
277
+ async function launchCodex(persona, extra) {
182
278
  const approvalMap = {
183
- spike: ['--model', 'o3', '--approval-mode', 'suggest'],
184
- sport: ['--approval-mode', 'auto-edit'],
185
- professional: ['--approval-mode', 'suggest'],
186
- auto: ['--approval-mode', 'full-auto'],
279
+ spike: ["--model", "o3", "--approval-mode", "suggest"],
280
+ sport: ["--approval-mode", "auto-edit"],
281
+ professional: ["--approval-mode", "suggest"],
282
+ auto: ["--approval-mode", "full-auto"],
187
283
  };
188
- const codexArgs = approvalMap[persona] || ['--approval-mode', 'suggest'];
284
+ const codexArgs = approvalMap[persona] || ["--approval-mode", "suggest"];
285
+ const { sid, name } = await setupSession(persona);
286
+ console.log("");
189
287
  console.log(`UV Suite | Codex | ${personaLabel(persona)}`);
190
- console.log('');
191
- const child = spawn('codex', [...codexArgs, ...extra], { stdio: 'inherit' });
192
- child.on('exit', (code) => process.exit(code || 0));
288
+ console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
289
+ console.log("");
290
+ const child = spawn("codex", [...codexArgs, ...extra], {
291
+ stdio: "inherit",
292
+ env: { ...process.env, UVS_SESSION_ID: sid },
293
+ });
294
+ child.on("exit", (code) => process.exit(code || 0));
193
295
  }
194
296
 
195
297
  function watch() {
196
- const serverScript = path.join(UV_SUITE_DIR, 'watchtower', 'server.js');
298
+ const serverScript = path.join(UV_SUITE_DIR, "watchtower", "server.js");
197
299
  if (!fs.existsSync(serverScript)) {
198
- console.error('Error: watchtower server not found at', serverScript);
300
+ console.error("Error: watchtower server not found at", serverScript);
199
301
  process.exit(1);
200
302
  }
201
303
 
202
- const bg = args.includes('--bg') || args.includes('--background');
203
- console.log('UV Suite Watchtower starting...');
204
- console.log('Dashboard: http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200));
205
- console.log('');
304
+ const bg = args.includes("--bg") || args.includes("--background");
305
+ console.log("UV Suite Watchtower starting...");
306
+ console.log(
307
+ "Dashboard: http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200),
308
+ );
309
+ console.log("");
206
310
 
207
311
  if (bg) {
208
- const child = spawn('node', [serverScript], {
209
- stdio: 'ignore',
312
+ const child = spawn("node", [serverScript], {
313
+ stdio: "ignore",
210
314
  detached: true,
211
315
  });
212
316
  child.unref();
213
317
  console.log(`Running in background (PID: ${child.pid})`);
214
- console.log('Stop with: kill ' + child.pid);
318
+ console.log("Stop with: kill " + child.pid);
215
319
 
216
320
  // Open browser
217
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
218
- spawn(opener, ['http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200)], { stdio: 'ignore' });
321
+ const opener =
322
+ process.platform === "darwin"
323
+ ? "open"
324
+ : process.platform === "win32"
325
+ ? "start"
326
+ : "xdg-open";
327
+ spawn(
328
+ opener,
329
+ ["http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200)],
330
+ { stdio: "ignore" },
331
+ );
219
332
  } else {
220
333
  // Foreground — open browser after a short delay
221
334
  setTimeout(() => {
222
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
223
- spawn(opener, ['http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200)], { stdio: 'ignore' });
335
+ const opener =
336
+ process.platform === "darwin"
337
+ ? "open"
338
+ : process.platform === "win32"
339
+ ? "start"
340
+ : "xdg-open";
341
+ spawn(
342
+ opener,
343
+ ["http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200)],
344
+ { stdio: "ignore" },
345
+ );
224
346
  }, 1000);
225
347
 
226
- const child = spawn('node', [serverScript], { stdio: 'inherit' });
227
- child.on('exit', (code) => process.exit(code || 0));
348
+ const child = spawn("node", [serverScript], { stdio: "inherit" });
349
+ child.on("exit", (code) => process.exit(code || 0));
228
350
  }
229
351
  }
230
352
 
231
353
  // --- Parse and route ---
232
354
 
233
- if (!command || command === '--help' || command === '-h') {
355
+ if (!command || command === "--help" || command === "-h") {
234
356
  usage();
235
357
  process.exit(0);
236
358
  }
237
359
 
238
- if (command === 'watch') {
239
- watch();
240
- } else if (command === 'install') {
241
- install();
242
- } else if (command === 'info') {
243
- info();
244
- } else if (TOOLS.includes(command)) {
245
- // uv claude pro, uv codex auto
246
- const persona = normPersona(args[1] || 'pro');
247
- if (!persona) {
248
- console.error(`Unknown persona: ${args[1]}`);
249
- console.error('Available: spike, sport, pro, auto');
360
+ (async () => {
361
+ if (command === "watch") {
362
+ watch();
363
+ } else if (command === "install") {
364
+ install();
365
+ } else if (command === "info") {
366
+ info();
367
+ } else if (TOOLS.includes(command)) {
368
+ // uvs claude pro, uvs codex auto
369
+ const persona = normPersona(args[1] || "pro");
370
+ if (!persona) {
371
+ console.error(`Unknown persona: ${args[1]}`);
372
+ console.error("Available: spike, sport, pro, auto");
373
+ process.exit(1);
374
+ }
375
+ const extra = args.slice(2);
376
+ if (command === "claude") await launchClaude(persona, extra);
377
+ else await launchCodex(persona, extra);
378
+ } else if (normPersona(command)) {
379
+ // uvs pro (shorthand for uvs claude pro)
380
+ const persona = normPersona(command);
381
+ const extra = args.slice(1);
382
+ await launchClaude(persona, extra);
383
+ } else {
384
+ console.error(`Unknown command: ${command}`);
385
+ usage();
250
386
  process.exit(1);
251
387
  }
252
- const extra = args.slice(2);
253
- if (command === 'claude') launchClaude(persona, extra);
254
- else launchCodex(persona, extra);
255
- } else if (normPersona(command)) {
256
- // uv pro (shorthand for uv claude pro)
257
- const persona = normPersona(command);
258
- const extra = args.slice(1);
259
- launchClaude(persona, extra);
260
- } else {
261
- console.error(`Unknown command: ${command}`);
262
- usage();
388
+ })().catch((err) => {
389
+ console.error(err);
263
390
  process.exit(1);
264
- }
391
+ });
@@ -0,0 +1,124 @@
1
+ #!/bin/bash
2
+ # UV Suite helper: locate per-session checkpoint paths and print metadata.
3
+ # Used by the /checkpoint and /restore slash commands.
4
+ #
5
+ # Usage:
6
+ # checkpoint-helper.sh dir # ensure + print the dir for current session
7
+ # checkpoint-helper.sh meta # print session metadata as shell-eval'able lines
8
+ # checkpoint-helper.sh frontmatter # YAML frontmatter to embed at the top of a checkpoint
9
+ # checkpoint-helper.sh latest # cat the latest checkpoint for current session (with fallback)
10
+ # checkpoint-helper.sh list # list all sessions that have checkpoints, newest first
11
+
12
+ resolve_paths() {
13
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
14
+ STATE_DIR="$PROJECT_DIR/.uv-suite-state"
15
+ SID="${UVS_SESSION_ID:-}"
16
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
17
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
18
+ fi
19
+ CHECKPOINTS_ROOT="$PROJECT_DIR/uv-out/checkpoints"
20
+ SESSION_CP_DIR=""
21
+ [ -n "$SID" ] && SESSION_CP_DIR="$CHECKPOINTS_ROOT/$SID"
22
+ META_FILE=""
23
+ [ -n "$SID" ] && META_FILE="$STATE_DIR/sessions/$SID.json"
24
+ }
25
+
26
+ print_meta_field() {
27
+ # $1 = field name; reads from $META_FILE; empty if missing
28
+ [ -z "$META_FILE" ] || [ ! -f "$META_FILE" ] && { echo ""; return; }
29
+ if command -v jq >/dev/null 2>&1; then
30
+ jq -r --arg k "$1" '.[$k] // ""' "$META_FILE" 2>/dev/null
31
+ else
32
+ grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$META_FILE" | head -1 | sed "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\(.*\)\"/\1/"
33
+ fi
34
+ }
35
+
36
+ resolve_paths
37
+
38
+ case "$1" in
39
+ dir)
40
+ if [ -n "$SESSION_CP_DIR" ]; then
41
+ mkdir -p "$SESSION_CP_DIR"
42
+ echo "$SESSION_CP_DIR"
43
+ else
44
+ mkdir -p "$CHECKPOINTS_ROOT"
45
+ echo "$CHECKPOINTS_ROOT"
46
+ fi
47
+ ;;
48
+ meta)
49
+ echo "uvs_session_id=${SID:-}"
50
+ echo "session_name=$(print_meta_field name)"
51
+ echo "session_kind=$(print_meta_field kind)"
52
+ echo "session_purpose=$(print_meta_field purpose)"
53
+ echo "session_priority=$(print_meta_field priority)"
54
+ echo "persona=$(print_meta_field persona)"
55
+ ;;
56
+ frontmatter)
57
+ NAME=$(print_meta_field name)
58
+ KIND=$(print_meta_field kind)
59
+ PURPOSE=$(print_meta_field purpose)
60
+ PRIORITY=$(print_meta_field priority)
61
+ PERSONA=$(print_meta_field persona)
62
+ NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
63
+ cat <<EOF
64
+ ---
65
+ uvs_session_id: ${SID:-}
66
+ session_name: ${NAME}
67
+ session_kind: ${KIND}
68
+ session_purpose: ${PURPOSE}
69
+ session_priority: ${PRIORITY}
70
+ persona: ${PERSONA}
71
+ checkpoint_at: ${NOW}
72
+ ---
73
+ EOF
74
+ ;;
75
+ latest)
76
+ if [ -n "$SESSION_CP_DIR" ] && [ -f "$SESSION_CP_DIR/latest.md" ]; then
77
+ cat "$SESSION_CP_DIR/latest.md"
78
+ elif [ -f "$CHECKPOINTS_ROOT/latest.md" ]; then
79
+ echo "(no per-session checkpoint for ${SID:-this session}; showing legacy global latest.md)"
80
+ echo
81
+ cat "$CHECKPOINTS_ROOT/latest.md"
82
+ else
83
+ echo "No checkpoint found at $CHECKPOINTS_ROOT. Run /checkpoint to create one."
84
+ fi
85
+ ;;
86
+ list)
87
+ [ ! -d "$CHECKPOINTS_ROOT" ] && { echo "No checkpoints directory at $CHECKPOINTS_ROOT"; exit 0; }
88
+ found=0
89
+ for d in "$CHECKPOINTS_ROOT"/*/; do
90
+ [ -d "$d" ] || continue
91
+ cp_sid=$(basename "$d")
92
+ cp_meta="$STATE_DIR/sessions/$cp_sid.json"
93
+ cp_name=""
94
+ cp_priority=""
95
+ if [ -f "$cp_meta" ]; then
96
+ if command -v jq >/dev/null 2>&1; then
97
+ cp_name=$(jq -r '.name // ""' "$cp_meta" 2>/dev/null)
98
+ cp_priority=$(jq -r '.priority // ""' "$cp_meta" 2>/dev/null)
99
+ else
100
+ cp_name=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$cp_meta" | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
101
+ fi
102
+ fi
103
+ latest=$(ls -t "$d"*.md 2>/dev/null | head -1)
104
+ [ -z "$latest" ] && continue
105
+ ts=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$latest" 2>/dev/null || stat -c '%y' "$latest" 2>/dev/null | cut -c1-16)
106
+ label="${cp_name:-(unlabeled)}"
107
+ [ -n "$cp_priority" ] && label="$label [p:$cp_priority]"
108
+ mark=" "
109
+ [ "$cp_sid" = "$SID" ] && mark="*"
110
+ echo "$mark ${cp_sid:0:8} $ts $label"
111
+ found=1
112
+ done
113
+ [ "$found" -eq 0 ] && echo "No per-session checkpoints yet (current session: ${SID:-none})"
114
+ # Note legacy global checkpoint if present
115
+ if [ -f "$CHECKPOINTS_ROOT/latest.md" ]; then
116
+ ts=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$CHECKPOINTS_ROOT/latest.md" 2>/dev/null || stat -c '%y' "$CHECKPOINTS_ROOT/latest.md" 2>/dev/null | cut -c1-16)
117
+ echo " legacy $ts (pre-metadata global latest.md)"
118
+ fi
119
+ ;;
120
+ *)
121
+ echo "Usage: checkpoint-helper.sh [dir|meta|frontmatter|latest|list]"
122
+ exit 1
123
+ ;;
124
+ esac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uv-suite",
3
- "version": "0.26.2",
3
+ "version": "0.26.4",
4
4
  "description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
5
5
  "author": "Utsav Anand",
6
6
  "license": "MIT",
@@ -3,7 +3,8 @@ name: checkpoint
3
3
  description: >
4
4
  Save a checkpoint of the current session — what was done, key decisions, current state,
5
5
  and what's next. Use before ending a session, before /compact, or at any natural breakpoint.
6
- The next session auto-loads the latest checkpoint.
6
+ Checkpoints are stored per-session under uv-out/checkpoints/<session-id>/, so concurrent
7
+ terminals don't clobber each other. /restore picks up the latest for the current session.
7
8
  argument-hint: "[optional-label]"
8
9
  user-invocable: true
9
10
  allowed-tools:
@@ -12,39 +13,51 @@ allowed-tools:
12
13
  - Bash(git status *)
13
14
  - Bash(git diff *)
14
15
  - Bash(git log *)
16
+ - Bash(git branch *)
15
17
  - Bash(git rev-parse *)
16
18
  - Bash(date *)
17
19
  - Bash(ls *)
18
20
  - Bash(mkdir *)
21
+ - Bash(cat *)
19
22
  - Bash(echo *)
23
+ - Bash("$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh *)
20
24
  ---
21
25
 
22
- ## Resolve checkpoint directory
26
+ ## Resolve session and checkpoint directory
23
27
 
24
- Anchor checkpoints to `$CLAUDE_PROJECT_DIR` (or the git repo root, or the current
25
- working dir as a last resort) so `/checkpoint` and `/restore` always agree, no matter
26
- which subdirectory the session was launched from.
27
-
28
- !`DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/uv-out/checkpoints"; mkdir -p "$DIR"; echo "$DIR"`
28
+ !`"$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh dir`
29
29
 
30
30
  Use the absolute path printed above as `<checkpoint-dir>` for every file path below.
31
+ The directory is per-session — two `uv` launches in the same repo write to
32
+ different folders, so checkpoints don't collide.
33
+
34
+ ## Session metadata
35
+
36
+ !`"$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh meta`
37
+
38
+ ## Frontmatter to embed at the top of the checkpoint
39
+
40
+ !`"$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh frontmatter`
31
41
 
32
42
  ## Write a checkpoint
33
43
 
34
- Write a file named `<checkpoint-dir>/YYYY-MM-DD-HHMM.md` using the current timestamp.
44
+ Write a file named `<checkpoint-dir>/YYYY-MM-DD-HHMM.md` using the current
45
+ timestamp. **Begin the file with the YAML frontmatter block printed above
46
+ exactly as shown** — `/restore` parses these fields when picking which
47
+ checkpoint to load.
35
48
 
36
- Also write/overwrite `<checkpoint-dir>/latest.md` with the same content, so the next
37
- session's `/restore` always finds the freshest state.
49
+ Also write/overwrite `<checkpoint-dir>/latest.md` with the same content,
50
+ so the next session's `/restore` always finds the freshest state for this
51
+ session.
38
52
 
39
53
  ## Label
40
54
 
41
55
  $ARGUMENTS
42
56
 
43
- If a label was provided, include it in the filename: `<checkpoint-dir>/YYYY-MM-DD-HHMM-[label].md`
44
-
45
- ## What to capture
57
+ If a label was provided, include it in the filename:
58
+ `<checkpoint-dir>/YYYY-MM-DD-HHMM-[label].md`
46
59
 
47
- Review the full conversation so far and write a structured checkpoint with these exact sections:
60
+ ## Body structure (after the frontmatter)
48
61
 
49
62
  ```markdown
50
63
  # Checkpoint: [date] [time] [label if provided]
@@ -88,5 +101,5 @@ Review the full conversation so far and write a structured checkpoint with these
88
101
 
89
102
  - Be specific. "Worked on auth" is useless. "Added JWT refresh token rotation with 7-day expiry" is useful.
90
103
  - Capture WHY decisions were made, not just what. The next session needs the rationale.
91
- - Keep it under 80 lines. This isn't a novel it's a handoff.
92
- - Every checkpoint overwrites `latest.md` so the next session always finds the freshest state.
104
+ - Keep the body under 80 lines. The frontmatter is required and not counted.
105
+ - Always include the YAML frontmatter — `/restore` reads it to pick the right checkpoint and to display session context.
@@ -1,25 +1,55 @@
1
1
  ---
2
2
  name: restore
3
3
  description: >
4
- Restore the latest checkpoint from a previous session. Shows what was done,
5
- key decisions, current state, and what's next. Use at the start of a new session.
4
+ Restore the latest checkpoint for the current session shows what was done,
5
+ key decisions, current state, and what's next. With no arguments, picks the
6
+ current `UVS_SESSION_ID`'s most recent checkpoint. Pass a session id prefix
7
+ or name to restore from a different session.
8
+ argument-hint: "[<session-id-prefix> | <session-name> | list]"
6
9
  user-invocable: true
7
10
  allowed-tools:
8
11
  - Read(*)
9
12
  - Bash(ls *)
10
13
  - Bash(cat *)
11
14
  - Bash(grep *)
15
+ - Bash(find *)
12
16
  - Bash(git rev-parse *)
17
+ - Bash("$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh *)
13
18
  ---
14
19
 
15
- ## Latest checkpoint
20
+ ## Available sessions with checkpoints
16
21
 
17
- !`DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/uv-out/checkpoints"; if [ -f "$DIR/latest.md" ]; then cat "$DIR/latest.md"; else echo "No checkpoint found at $DIR. Run /checkpoint to create one."; fi`
22
+ !`"$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh list`
18
23
 
19
- ## All checkpoints
24
+ (`*` marks the current session.)
20
25
 
21
- !`DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/uv-out/checkpoints"; if [ -d "$DIR" ]; then matches=$(ls -la "$DIR"/ 2>/dev/null | grep '\.md$' | tail -10); if [ -n "$matches" ]; then echo "$matches"; else echo "No checkpoints in $DIR"; fi; else echo "No checkpoints directory at $DIR"; fi`
26
+ ## Latest checkpoint for the current session
27
+
28
+ !`"$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh latest`
29
+
30
+ ## Argument
31
+
32
+ $ARGUMENTS
22
33
 
23
34
  ## Instructions
24
35
 
25
- Read the checkpoint above. Summarize it to the user in 3-4 sentences: what was done, what's the current state, and what's next. Then ask: "Ready to pick up from here, or do you want to take a different direction?"
36
+ 1. **If `$ARGUMENTS` is empty or "latest"**: read the checkpoint shown above (the
37
+ current session's `latest.md`). Summarize it in 3-4 sentences: what was
38
+ done, current state, what's next. Then ask: "Ready to pick up from here, or
39
+ do you want to take a different direction?"
40
+
41
+ 2. **If `$ARGUMENTS` is "list"**: just show the user the available-sessions
42
+ list above and ask which one they want to restore.
43
+
44
+ 3. **If `$ARGUMENTS` looks like a session id prefix** (8-char hex / UUID-ish)
45
+ **or a session name**: match it against the list above. Read the
46
+ matching session's `latest.md` from
47
+ `<project>/uv-out/checkpoints/<full-session-id>/latest.md` using the Read
48
+ tool, then summarize as in (1).
49
+
50
+ 4. If no match is found, list the available sessions and ask the user to
51
+ pick one.
52
+
53
+ When summarizing, include the session's name and purpose from the
54
+ frontmatter at the top of the checkpoint — that's the context the next
55
+ session needs to know what it's picking up.