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 +6 -0
- package/dist/builder.js +41 -0
- package/dist/commands/add.js +2 -2
- package/dist/commands/init.js +62 -13
- package/dist/commands/remove.js +2 -2
- package/dist/commands/restore.js +179 -12
- package/dist/commands/status.js +65 -25
- package/dist/commands/sync.js +110 -20
- package/dist/doctor.js +34 -6
- package/dist/marketplace.d.ts +14 -1
- package/dist/marketplace.js +117 -61
- package/dist/snapshot.d.ts +27 -5
- package/dist/snapshot.js +230 -40
- package/dist/tokens.d.ts +75 -6
- package/dist/tokens.js +542 -9
- package/package.json +1 -1
- package/templates/init-empty.md +49 -7
- package/templates/remove.md +6 -2
- package/templates/sync.md +4 -0
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`,
|
package/dist/commands/add.js
CHANGED
|
@@ -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(
|
|
62
|
+
console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
|
|
63
63
|
console.log("");
|
|
64
64
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
84
|
+
installTokenHook();
|
|
85
|
+
// Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
|
|
51
86
|
const cwd = process.cwd();
|
|
52
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
135
|
+
installTokenHook();
|
|
136
|
+
// Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
|
|
94
137
|
const cwd = process.cwd();
|
|
95
|
-
const
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/remove.js
CHANGED
|
@@ -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(
|
|
43
|
+
console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
|
|
44
44
|
console.log("");
|
|
45
45
|
}
|
package/dist/commands/restore.js
CHANGED
|
@@ -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
|
|
27
|
-
const
|
|
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}${
|
|
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(
|
|
140
|
+
console.log(` ${c.dim(" ──→")}`);
|
|
32
141
|
}
|
|
33
142
|
console.log("");
|
|
34
|
-
const
|
|
35
|
-
|
|
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("
|
|
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
|
-
|
|
60
|
-
|
|
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
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log(`
|
|
97
|
-
console.log(` Total cost
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
console.log(`
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
}
|