claude-setup 1.1.4 → 1.1.6

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/dist/builder.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface FileDiff {
15
15
  deleted: string[];
16
16
  }
17
17
  export declare function buildSyncCommand(diff: FileDiff, collected: CollectedFiles, state: ExistingState): string;
18
+ /**
19
+ * Bootstrap stack-sync.md — installed by init so /stack-sync works immediately.
20
+ * When run, it tells Claude to invoke npx claude-setup sync first (which
21
+ * regenerates this file with the real diff), then re-read and process it.
22
+ */
23
+ export declare function buildBootstrapSync(): string;
18
24
  export declare function buildRemoveCommand(input: string, state: ExistingState): string;
19
25
  export interface AtomicStep {
20
26
  filename: string;
package/dist/builder.js CHANGED
@@ -171,6 +171,35 @@ export function buildSyncCommand(diff, collected, state) {
171
171
  DELETED_FILES: deletedStr,
172
172
  }, "sync");
173
173
  }
174
+ /**
175
+ * Bootstrap stack-sync.md — installed by init so /stack-sync works immediately.
176
+ * When run, it tells Claude to invoke npx claude-setup sync first (which
177
+ * regenerates this file with the real diff), then re-read and process it.
178
+ */
179
+ export function buildBootstrapSync() {
180
+ return `<!-- stack-sync: self-refreshing sync command. Last generated: ${new Date().toISOString().split("T")[0]} -->
181
+
182
+ > **Step 1 — always do this first:** Run \`npx claude-setup sync\` to refresh this file with the latest diff.
183
+ > - If it outputs "No changes since…" → respond "Stack already in sync." and STOP.
184
+ > - Otherwise → re-read \`.claude/commands/stack-sync.md\` (Step 2), then process the diff below (Step 3).
185
+
186
+ ## Changes since last setup
187
+
188
+ ### Added files
189
+ (none — run \`npx claude-setup sync\` to populate)
190
+
191
+ ### Modified files
192
+ (none)
193
+
194
+ ### Deleted files
195
+ (none)
196
+
197
+ ## Your job
198
+
199
+ For EACH changed file: does this change have any implication for the Claude Code setup?
200
+ Update ONLY what the change demands. Do NOT rewrite files — surgical edits only.
201
+ `;
202
+ }
174
203
  export function buildRemoveCommand(input, state) {
175
204
  const emptyCollected = { configs: {}, source: [], skipped: [] };
176
205
  return applyTemplate("remove.md", emptyCollected, state, { USER_INPUT: input }, "remove");
@@ -240,6 +269,11 @@ export function buildAtomicSteps(collected, state) {
240
269
  `\`⚠️ UNKNOWN PACKAGE — [service] MCP server not added: package name unverified. Find it at https://github.com/modelcontextprotocol/servers\`\n` +
241
270
  `Do not add a placeholder. Do not guess.\n\n` +
242
271
  `### OS-correct format (detected: ${os})\n` +
272
+ `**Preferred: use CLI to add (writes to .mcp.json automatically):**\n` +
273
+ (os === "Windows"
274
+ ? `\`\`\`\nclaude mcp add --scope project --transport stdio <name> -- cmd /c npx -y <package>\n\`\`\`\n`
275
+ : `\`\`\`\nclaude mcp add --scope project --transport stdio <name> -- npx -y <package>\n\`\`\`\n`) +
276
+ `**Or write .mcp.json directly:**\n` +
243
277
  (os === "Windows"
244
278
  ? `Use: \`{ "command": "cmd", "args": ["/c", "npx", "-y", "<package>"] }\`\n`
245
279
  : `Use: \`{ "command": "npx", "args": ["-y", "<package>"] }\`\n`) +
@@ -270,6 +304,13 @@ export function buildAtomicSteps(collected, state) {
270
304
  ? `\`{ "command": "cmd", "args": ["/c", "bun", "run", "\${CLAUDE_PLUGIN_ROOT}/servers/telegram"], "env": { "TELEGRAM_BOT_TOKEN": "\${TELEGRAM_BOT_TOKEN}" } }\`\n`
271
305
  : `\`{ "command": "bun", "args": ["run", "\${CLAUDE_PLUGIN_ROOT}/servers/telegram"], "env": { "TELEGRAM_BOT_TOKEN": "\${TELEGRAM_BOT_TOKEN}" } }\`\n`) +
272
306
  `After adding, flag: \`⚠️ CHANNEL ACTIVATION REQUIRED — launch with: claude --channels plugin:telegram@claude-plugins-official\`\n\n` +
307
+ `### Self-correction fallback\n` +
308
+ `If MCP configuration fails or produces warnings:\n` +
309
+ `1. Read the official MCP documentation: https://modelcontextprotocol.io/introduction\n` +
310
+ `2. Verify the package name against the MCP servers registry: https://github.com/modelcontextprotocol/servers\n` +
311
+ `3. Check the server's README for required env vars and correct args format\n` +
312
+ `4. Re-run \`npx claude-setup doctor\` to validate the fix\n` +
313
+ `Do NOT leave broken MCP configuration in place — either fix it or remove the entry.\n\n` +
273
314
  `### Output\n` +
274
315
  `Created/Updated: ✅ .mcp.json — [what server and evidence source]\n` +
275
316
  `Skipped: ⏭ .mcp.json — checked [files], found [nothing], no action\n`,
@@ -4,7 +4,7 @@ import { collectProjectFiles } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildAddCommand } from "../builder.js";
7
- import { estimateTokens, estimateCost } from "../tokens.js";
7
+ import { estimateTokens, estimateCost, formatCost } from "../tokens.js";
8
8
  import { c, section } from "../output.js";
9
9
  function ensureDir(dir) {
10
10
  if (!existsSync(dir))
@@ -59,6 +59,6 @@ capabilities that need documentation, MCP servers, skills, and hooks together.
59
59
  });
60
60
  console.log(`\n${c.green("✅")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-add")}`);
61
61
  section("Token cost");
62
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
62
+ console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
63
63
  console.log("");
64
64
  }
@@ -1,11 +1,11 @@
1
- import { writeFileSync, mkdirSync, existsSync } from "fs";
1
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { collectProjectFiles, isEmptyProject } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
- import { buildEmptyProjectCommand, buildAtomicSteps, buildOrchestratorCommand, } from "../builder.js";
6
+ import { buildEmptyProjectCommand, buildAtomicSteps, buildOrchestratorCommand, buildBootstrapSync, } from "../builder.js";
7
7
  import { createSnapshot, collectFilesForSnapshot } from "../snapshot.js";
8
- import { estimateTokens, estimateCost } from "../tokens.js";
8
+ import { estimateTokens, estimateCost, formatCost, getTokenHookScript, formatRealCostSummary } from "../tokens.js";
9
9
  import { c, section } from "../output.js";
10
10
  import { ensureConfig } from "../config.js";
11
11
  import { applyTemplate } from "./export.js";
@@ -13,6 +13,39 @@ function ensureDir(dir) {
13
13
  if (!existsSync(dir))
14
14
  mkdirSync(dir, { recursive: true });
15
15
  }
16
+ function installTokenHook(cwd = process.cwd()) {
17
+ // Write the hook script
18
+ const hooksDir = join(cwd, ".claude", "hooks");
19
+ if (!existsSync(hooksDir))
20
+ mkdirSync(hooksDir, { recursive: true });
21
+ writeFileSync(join(hooksDir, "track-tokens.cjs"), getTokenHookScript(), "utf8");
22
+ // Merge Stop hook into settings.json
23
+ const settingsPath = join(cwd, ".claude", "settings.json");
24
+ let settings = {};
25
+ if (existsSync(settingsPath)) {
26
+ try {
27
+ settings = JSON.parse(readFileSync(settingsPath, "utf8") ?? "{}");
28
+ }
29
+ catch { }
30
+ }
31
+ const hookEntry = {
32
+ hooks: [{ type: "command", command: "node \".claude/hooks/track-tokens.cjs\"" }]
33
+ };
34
+ // Merge into settings.hooks.Stop
35
+ if (!settings.hooks)
36
+ settings.hooks = {};
37
+ const hooks = settings.hooks;
38
+ if (!Array.isArray(hooks.Stop))
39
+ hooks.Stop = [];
40
+ // Only add if not already present
41
+ const alreadyPresent = hooks.Stop.some(e => Array.isArray(e.hooks) && e.hooks.some((h) => typeof h.command === "string" && h.command.includes("track-tokens")));
42
+ if (!alreadyPresent) {
43
+ hooks.Stop.push(hookEntry);
44
+ if (!existsSync(join(cwd, ".claude")))
45
+ mkdirSync(join(cwd, ".claude"), { recursive: true });
46
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
47
+ }
48
+ }
16
49
  export async function runInit(opts = {}) {
17
50
  const dryRun = opts.dryRun ?? false;
18
51
  // Feature H: --template flag — apply a template instead of scanning
@@ -41,16 +74,17 @@ export async function runInit(opts = {}) {
41
74
  if (content.length > 500)
42
75
  console.log(c.dim(`\n... +${content.length - 500} chars`));
43
76
  section("Token cost estimate");
44
- console.log(` ~${tokens.toLocaleString()} input tokens (Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)})`);
77
+ console.log(` ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`);
45
78
  return;
46
79
  }
47
80
  ensureDir(".claude/commands");
48
81
  writeFileSync(".claude/commands/stack-init.md", content, "utf8");
82
+ writeFileSync(".claude/commands/stack-sync.md", buildBootstrapSync(), "utf8");
49
83
  await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
50
- // Feature A: Create initial snapshot node
84
+ installTokenHook();
85
+ // Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
51
86
  const cwd = process.cwd();
52
- const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
53
- const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
87
+ const snapshotFiles = collectFilesForSnapshot(cwd, Object.keys(collected.configs));
54
88
  createSnapshot(cwd, "init", snapshotFiles, { summary: "initial setup (empty project)" });
55
89
  console.log(`
56
90
  ${c.green("✅")} New project detected.
@@ -61,7 +95,14 @@ Open Claude Code and run:
61
95
  Claude Code will ask 3 questions, then set up your environment.
62
96
  `);
63
97
  section("Token cost");
64
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
98
+ const realSummary1 = formatRealCostSummary(cwd);
99
+ if (realSummary1) {
100
+ console.log(realSummary1);
101
+ }
102
+ else {
103
+ console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
104
+ console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
105
+ }
65
106
  console.log("");
66
107
  return;
67
108
  }
@@ -81,7 +122,7 @@ Claude Code will ask 3 questions, then set up your environment.
81
122
  console.log(` .claude/commands/stack-init.md (orchestrator)`);
82
123
  console.log(`\n${c.dim(`Total: ~${tokens.toLocaleString()} tokens across ${steps.length} files`)}`);
83
124
  section("Token cost estimate");
84
- console.log(` ~${tokens.toLocaleString()} input tokens (Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)})`);
125
+ console.log(` ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`);
85
126
  return;
86
127
  }
87
128
  ensureDir(".claude/commands");
@@ -89,11 +130,12 @@ Claude Code will ask 3 questions, then set up your environment.
89
130
  writeFileSync(join(".claude/commands", step.filename), step.content, "utf8");
90
131
  }
91
132
  writeFileSync(".claude/commands/stack-init.md", orchestrator, "utf8");
133
+ writeFileSync(".claude/commands/stack-sync.md", buildBootstrapSync(), "utf8");
92
134
  await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
93
- // Feature A: Create initial snapshot node
135
+ installTokenHook();
136
+ // Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
94
137
  const cwd = process.cwd();
95
- const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
96
- const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
138
+ const snapshotFiles = collectFilesForSnapshot(cwd, Object.keys(collected.configs));
97
139
  createSnapshot(cwd, "init", snapshotFiles, {
98
140
  summary: `${steps.length - 1} atomic steps generated`,
99
141
  });
@@ -104,6 +146,13 @@ ${c.green("✅")} Ready. Open Claude Code and run:
104
146
  Runs ${steps.length - 1} atomic steps. If one fails, re-run only that step.
105
147
  `);
106
148
  section("Token cost");
107
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
149
+ const realSummary2 = formatRealCostSummary(cwd);
150
+ if (realSummary2) {
151
+ console.log(realSummary2);
152
+ }
153
+ else {
154
+ console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
155
+ console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
156
+ }
108
157
  console.log("");
109
158
  }
@@ -4,7 +4,7 @@ import { collectProjectFiles } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildRemoveCommand } from "../builder.js";
7
- import { estimateTokens, estimateCost } from "../tokens.js";
7
+ import { estimateTokens, estimateCost, formatCost } from "../tokens.js";
8
8
  import { c, section } from "../output.js";
9
9
  function ensureDir(dir) {
10
10
  if (!existsSync(dir))
@@ -40,6 +40,6 @@ export async function runRemove() {
40
40
  });
41
41
  console.log(`\n${c.green("✅")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-remove")}`);
42
42
  section("Token cost");
43
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
43
+ console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
44
44
  console.log("");
45
45
  }
@@ -1,4 +1,4 @@
1
- import { readTimeline, restoreSnapshot } from "../snapshot.js";
1
+ import { readTimeline, restoreSnapshot, updateRestoredNode } from "../snapshot.js";
2
2
  import { c, section } from "../output.js";
3
3
  import { createInterface } from "readline";
4
4
  async function promptFreeText(question) {
@@ -10,6 +10,97 @@ async function promptFreeText(question) {
10
10
  });
11
11
  });
12
12
  }
13
+ async function promptSelectSnapshot(items) {
14
+ const allItems = [
15
+ ...items,
16
+ { id: "__custom__", label: c.dim("Type snapshot ID manually…") },
17
+ { id: "__cancel__", label: c.dim("Cancel") },
18
+ ];
19
+ // Non-TTY fallback: numbered list
20
+ if (!process.stdin.isTTY) {
21
+ for (let i = 0; i < allItems.length; i++) {
22
+ console.log(` [${i + 1}] ${allItems[i].label}`);
23
+ }
24
+ const raw = await promptFreeText("\nEnter number (or 'cancel'):");
25
+ if (!raw || raw === "cancel")
26
+ return null;
27
+ const num = parseInt(raw, 10);
28
+ if (!isNaN(num) && num >= 1 && num <= allItems.length) {
29
+ const val = allItems[num - 1].id;
30
+ if (val === "__cancel__")
31
+ return null;
32
+ if (val === "__custom__")
33
+ return (await promptFreeText("Enter snapshot ID:")) || null;
34
+ return val;
35
+ }
36
+ return raw;
37
+ }
38
+ return new Promise((resolve) => {
39
+ let selectedIndex = 0;
40
+ let lineCount = 0;
41
+ const clearLines = () => {
42
+ if (lineCount > 0)
43
+ process.stdout.write(`\x1b[${lineCount}A\x1b[0J`);
44
+ };
45
+ const render = () => {
46
+ clearLines();
47
+ const lines = [];
48
+ for (let i = 0; i < allItems.length; i++) {
49
+ const isSelected = i === selectedIndex;
50
+ const marker = isSelected ? c.cyan("❯") : " ";
51
+ const text = isSelected ? c.bold(allItems[i].label) : allItems[i].label;
52
+ lines.push(` ${marker} ${text}`);
53
+ }
54
+ lines.push(``);
55
+ lines.push(` ${c.dim("↑/↓ navigate · Enter select · Ctrl+C cancel")}`);
56
+ process.stdout.write(lines.join("\n") + "\n");
57
+ lineCount = lines.length;
58
+ };
59
+ let onKey;
60
+ const cleanup = () => {
61
+ process.stdin.removeListener("data", onKey);
62
+ try {
63
+ process.stdin.setRawMode(false);
64
+ }
65
+ catch { }
66
+ process.stdin.pause();
67
+ };
68
+ onKey = (key) => {
69
+ if (key === "\u0003") {
70
+ cleanup();
71
+ process.stdout.write("\n");
72
+ process.exit(0);
73
+ }
74
+ else if (key === "\u001b[A" || key === "\u001bOA") {
75
+ selectedIndex = Math.max(0, selectedIndex - 1);
76
+ render();
77
+ }
78
+ else if (key === "\u001b[B" || key === "\u001bOB") {
79
+ selectedIndex = Math.min(allItems.length - 1, selectedIndex + 1);
80
+ render();
81
+ }
82
+ else if (key === "\r" || key === "\n") {
83
+ const chosen = allItems[selectedIndex];
84
+ cleanup();
85
+ process.stdout.write("\n");
86
+ if (chosen.id === "__cancel__") {
87
+ resolve(null);
88
+ }
89
+ else if (chosen.id === "__custom__") {
90
+ promptFreeText("Enter snapshot ID:").then(id => resolve(id || null));
91
+ }
92
+ else {
93
+ resolve(chosen.id);
94
+ }
95
+ }
96
+ };
97
+ process.stdin.setRawMode(true);
98
+ process.stdin.resume();
99
+ process.stdin.setEncoding("utf8");
100
+ process.stdin.on("data", onKey);
101
+ render();
102
+ });
103
+ }
13
104
  export async function runRestore() {
14
105
  const cwd = process.cwd();
15
106
  const timeline = readTimeline(cwd);
@@ -19,20 +110,49 @@ export async function runRestore() {
19
110
  }
20
111
  // Display timeline
21
112
  section("Snapshot timeline");
22
- console.log("");
113
+ console.log(` ${c.dim("All snapshots are always preserved — you can go back or forward freely.")}\n`);
114
+ const restoredIdx = timeline.restoredTo
115
+ ? timeline.nodes.findIndex(n => n.id === timeline.restoredTo)
116
+ : timeline.nodes.length - 1;
117
+ const latestIdx = timeline.nodes.length - 1;
23
118
  for (let i = 0; i < timeline.nodes.length; i++) {
24
119
  const node = timeline.nodes[i];
25
120
  const date = new Date(node.timestamp).toLocaleString();
26
- const current = i === timeline.nodes.length - 1 ? ` ${c.green("← current")}` : "";
27
- const connector = i < timeline.nodes.length - 1 ? "──→" : " ";
121
+ const isLatest = i === latestIdx;
122
+ const isHere = i === restoredIdx && timeline.restoredTo;
123
+ let marker = "";
124
+ let prefix = " ";
125
+ if (isHere && !isLatest) {
126
+ marker = ` ${c.cyan("◀ you are here")}`;
127
+ prefix = c.cyan("▶ ");
128
+ }
129
+ else if (isLatest && !timeline.restoredTo) {
130
+ marker = ` ${c.green("◀ you are here")}`;
131
+ prefix = c.green("▶ ");
132
+ }
133
+ else if (i > restoredIdx) {
134
+ marker = ` ${c.dim("(future — reachable)")}`;
135
+ prefix = c.dim(" ");
136
+ }
28
137
  const inputStr = node.input ? ` "${node.input}"` : "";
29
- console.log(` ${c.cyan(node.id)} ${node.command}${inputStr} ${c.dim(date)} ${node.summary}${current}`);
138
+ console.log(` ${prefix}${c.cyan(node.id)} ${node.command}${inputStr} ${c.dim(date)} ${node.summary}${marker}`);
30
139
  if (i < timeline.nodes.length - 1)
31
- console.log(` ${c.dim(connector)}`);
140
+ console.log(` ${c.dim(" ──→")}`);
32
141
  }
33
142
  console.log("");
34
- const input = await promptFreeText("Enter snapshot ID to restore (or 'cancel'):");
35
- if (!input || input === "cancel") {
143
+ const items = timeline.nodes.map((node, i) => {
144
+ const date = new Date(node.timestamp).toLocaleString();
145
+ const isLatest = i === latestIdx;
146
+ const isHere = i === restoredIdx && timeline.restoredTo;
147
+ const tag = isHere && !isLatest ? ` ${c.cyan("← you are here")}`
148
+ : isLatest && !timeline.restoredTo ? ` ${c.green("← you are here")}`
149
+ : i > restoredIdx ? ` ${c.dim("(future)")}`
150
+ : "";
151
+ return { id: node.id, label: `${node.id} ${node.command} ${c.dim(date)} ${node.summary}${tag}` };
152
+ });
153
+ console.log("Select a snapshot to restore to:\n");
154
+ const input = await promptSelectSnapshot(items);
155
+ if (!input) {
36
156
  console.log("Cancelled.");
37
157
  return;
38
158
  }
@@ -42,20 +162,67 @@ export async function runRestore() {
42
162
  return;
43
163
  }
44
164
  console.log(`\nRestoring to snapshot ${c.cyan(node.id)} (${new Date(node.timestamp).toLocaleString()})...`);
45
- console.log(`${c.dim("Other snapshots are preserved you can jump forward or back at any time.")}\n`);
46
- const result = restoreSnapshot(cwd, input);
165
+ console.log(`${c.dim("Config files will be rewritten to their state at this snapshot. Other files are untouched.")}\n`);
166
+ const result = restoreSnapshot(cwd, input, timeline);
167
+ updateRestoredNode(cwd, input);
47
168
  if (result.restored.length) {
48
169
  section("Restored files");
49
170
  for (const f of result.restored) {
50
171
  console.log(` ${c.green("✅")} ${f}`);
51
172
  }
52
173
  }
174
+ if (result.deleted.length) {
175
+ section(`Removed (added after this snapshot — ${result.deleted.length} files)`);
176
+ for (const f of result.deleted) {
177
+ console.log(` ${c.red("🗑")} ${f}`);
178
+ }
179
+ }
53
180
  if (result.failed.length) {
54
181
  section("Failed to restore");
55
182
  for (const f of result.failed) {
56
183
  console.log(` ${c.red("🔴")} ${f}`);
57
184
  }
58
185
  }
59
- console.log(`\n${c.green("✅")} Restored ${result.restored.length} file(s) to snapshot ${c.cyan(node.id)}.`);
60
- console.log(`Run ${c.cyan("npx claude-setup sync")} to capture the current state as a new node.`);
186
+ if (result.stale.length) {
187
+ section("Could not delete (permission error)");
188
+ console.log(` ${c.dim("These files exist but couldn't be removed — delete manually:")}`);
189
+ for (const f of result.stale) {
190
+ console.log(` ${c.yellow("⚠️")} ${f}`);
191
+ }
192
+ }
193
+ if (result.restored.length === 0 && result.deleted.length === 0) {
194
+ console.log(`\n${c.yellow("⚠️")} This snapshot captured 0 files — nothing to restore.`);
195
+ }
196
+ else {
197
+ const parts = [];
198
+ if (result.restored.length)
199
+ parts.push(`${result.restored.length} restored`);
200
+ if (result.deleted.length)
201
+ parts.push(`${result.deleted.length} removed`);
202
+ console.log(`\n${c.green("✅")} ${parts.join(", ")} → project is now at snapshot ${c.cyan(node.id)}.`);
203
+ }
204
+ // Re-read and display updated timeline
205
+ const updatedTimeline = readTimeline(cwd);
206
+ const updatedRestoredIdx = updatedTimeline.restoredTo
207
+ ? updatedTimeline.nodes.findIndex(n => n.id === updatedTimeline.restoredTo)
208
+ : updatedTimeline.nodes.length - 1;
209
+ console.log("");
210
+ section("Timeline — you can restore to any node at any time");
211
+ console.log("");
212
+ for (let i = 0; i < updatedTimeline.nodes.length; i++) {
213
+ const n = updatedTimeline.nodes[i];
214
+ const date = new Date(n.timestamp).toLocaleString();
215
+ const isHere = i === updatedRestoredIdx;
216
+ const isFuture = i > updatedRestoredIdx;
217
+ const marker = isHere ? ` ${c.cyan("◀ you are here")}` : isFuture ? ` ${c.dim("(future — reachable)")}` : "";
218
+ const prefix = isHere ? c.cyan("▶ ") : isFuture ? c.dim(" ") : " ";
219
+ const inputStr = n.input ? ` "${n.input}"` : "";
220
+ console.log(` ${prefix}${c.cyan(n.id)} ${n.command}${inputStr} ${c.dim(date)} ${n.summary}${marker}`);
221
+ if (i < updatedTimeline.nodes.length - 1)
222
+ console.log(` ${c.dim(" ──→")}`);
223
+ }
224
+ console.log(``);
225
+ console.log(` ${c.green("▶")} Run ${c.cyan("claude")} in this directory to start working from this point.`);
226
+ console.log(` ${c.dim("Run npx claude-setup sync to save the current state as a new snapshot.")}`);
227
+ console.log(``);
61
228
  }
@@ -4,7 +4,7 @@ import { readManifest } from "../manifest.js";
4
4
  import { readState } from "../state.js";
5
5
  import { detectOS } from "../os.js";
6
6
  import { readTimeline } from "../snapshot.js";
7
- import { computeCumulativeStats, formatCost } from "../tokens.js";
7
+ import { computeCumulativeStats, readRealTokenUsage, getProjectUsageSummary, readProjectSessions } from "../tokens.js";
8
8
  import { c, statusLine, section } from "../output.js";
9
9
  function safeJsonParse(content) {
10
10
  try {
@@ -89,33 +89,73 @@ export async function runStatus() {
89
89
  console.log(` ${c.dim("Use")} ${c.cyan("npx claude-setup compare")} ${c.dim("to diff two snapshots")}`);
90
90
  }
91
91
  // --- Feature I: Token usage stats ---
92
- const runsWithTokens = manifest.runs.filter(r => r.estimatedTokens !== undefined);
93
- if (runsWithTokens.length > 0) {
94
- section("Token usage");
95
- const stats = computeCumulativeStats(manifest.runs);
96
- console.log(` Total tokens : ~${stats.totalTokens.toLocaleString()} across ${stats.runCount} run(s)`);
97
- console.log(` Total cost : ${formatCost(stats.totalCost)}`);
98
- // Average by command type
99
- const avgEntries = Object.entries(stats.avgByCommand);
100
- if (avgEntries.length > 0) {
101
- console.log(` Avg by type :`);
102
- for (const [cmd, avg] of avgEntries) {
103
- console.log(` ${cmd}: ~${avg.toLocaleString()} tokens/run`);
92
+ // Try JSONL transcripts first (ccusage-style, most accurate)
93
+ const projectSummary = getProjectUsageSummary(cwd);
94
+ if (projectSummary && projectSummary.totalTokens > 0) {
95
+ section("Token usage (real — from JSONL transcripts)");
96
+ console.log(` Sessions tracked : ${projectSummary.sessions}`);
97
+ console.log(` Total cost : $${projectSummary.totalCost.toFixed(6)}`);
98
+ console.log(` Input tokens : ${projectSummary.inputTokens.toLocaleString()}`);
99
+ console.log(` Output tokens : ${projectSummary.outputTokens.toLocaleString()}`);
100
+ if (projectSummary.cacheCreateTokens > 0 || projectSummary.cacheReadTokens > 0) {
101
+ console.log(` Cache write : ${projectSummary.cacheCreateTokens.toLocaleString()}`);
102
+ console.log(` Cache read : ${projectSummary.cacheReadTokens.toLocaleString()}`);
103
+ }
104
+ console.log(` Total tokens : ${projectSummary.totalTokens.toLocaleString()}`);
105
+ if (projectSummary.models.length > 0) {
106
+ console.log(``);
107
+ console.log(` Per model:`);
108
+ for (const m of projectSummary.models.sort((a, b) => b.cost - a.cost)) {
109
+ const shortName = m.model.replace(/^claude-/, "").replace(/-\d{8}$/, "");
110
+ console.log(` ${shortName.padEnd(14)} ${m.totalTokens.toLocaleString().padStart(12)} tokens $${m.cost.toFixed(6)}`);
111
+ }
112
+ }
113
+ // Show last 5 sessions
114
+ const sessions = readProjectSessions(cwd);
115
+ if (sessions.length > 0) {
116
+ console.log(``);
117
+ console.log(` Recent sessions:`);
118
+ for (const s of sessions.slice(0, 5)) {
119
+ const date = s.timestamp ? new Date(s.timestamp).toLocaleString() : "unknown";
120
+ const primaryModel = s.models.sort((a, b) => b.cost - a.cost)[0]?.model ?? "unknown";
121
+ const shortModel = primaryModel.replace(/^claude-/, "").replace(/-\d{8}$/, "");
122
+ console.log(` ${c.dim(date)} ${shortModel} ${s.totalTokens.toLocaleString()} tokens $${s.totalCost.toFixed(6)}`);
104
123
  }
105
124
  }
106
- // Cost trend (last 3 vs previous 3)
107
- if (runsWithTokens.length >= 6) {
108
- const recent3 = runsWithTokens.slice(-3);
109
- const prev3 = runsWithTokens.slice(-6, -3);
110
- const recentAvg = recent3.reduce((s, r) => s + (r.estimatedTokens ?? 0), 0) / 3;
111
- const prevAvg = prev3.reduce((s, r) => s + (r.estimatedTokens ?? 0), 0) / 3;
112
- const change = ((recentAvg - prevAvg) / prevAvg) * 100;
113
- if (Math.abs(change) > 10) {
114
- const trend = change > 0 ? c.yellow(`↑ +${change.toFixed(0)}%`) : c.green(`↓ ${change.toFixed(0)}%`);
115
- console.log(` Trend : ${trend} (recent vs previous)`);
125
+ }
126
+ else {
127
+ // Fallback: Stop hook data
128
+ const realUsage = readRealTokenUsage(cwd);
129
+ if (realUsage.length > 0) {
130
+ section("Token usage (real from Stop hook)");
131
+ const last5 = realUsage.slice(-5).reverse();
132
+ let totalCost = 0;
133
+ for (const r of realUsage)
134
+ totalCost += r.cost;
135
+ console.log(` Sessions tracked : ${realUsage.length}`);
136
+ console.log(` Total real cost : $${totalCost.toFixed(6)}`);
137
+ console.log(``);
138
+ console.log(` Recent sessions:`);
139
+ for (const r of last5) {
140
+ const date = new Date(r.timestamp).toLocaleString();
141
+ const tokens = r.inputTokens + r.outputTokens + r.cacheCreate + r.cacheRead;
142
+ console.log(` ${c.dim(date)} ${r.model.split('-').slice(1, 3).join('-')} ${tokens.toLocaleString()} tokens $${r.cost.toFixed(6)}`);
116
143
  }
117
- else {
118
- console.log(` Trend : ${c.green("→ stable")}`);
144
+ }
145
+ else {
146
+ // Fall back to estimates from manifest
147
+ const runsWithTokens = manifest.runs.filter(r => r.estimatedTokens !== undefined);
148
+ if (runsWithTokens.length > 0) {
149
+ section("Token usage (estimated — real data available after first Claude Code session)");
150
+ const stats = computeCumulativeStats(manifest.runs);
151
+ console.log(` Total est. tokens: ~${stats.totalTokens.toLocaleString()} across ${stats.runCount} run(s)`);
152
+ const avgEntries = Object.entries(stats.avgByCommand);
153
+ if (avgEntries.length > 0) {
154
+ console.log(` Avg by type :`);
155
+ for (const [cmd, avg] of avgEntries) {
156
+ console.log(` ${cmd}: ~${avg.toLocaleString()} tokens/run`);
157
+ }
158
+ }
119
159
  }
120
160
  }
121
161
  }