claude-setup 1.1.4 → 1.1.5
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.js +12 -0
- package/dist/commands/add.js +2 -2
- package/dist/commands/init.js +57 -6
- package/dist/commands/remove.js +2 -2
- package/dist/commands/restore.js +40 -5
- package/dist/commands/status.js +65 -25
- package/dist/commands/sync.js +91 -2
- package/dist/doctor.js +27 -4
- package/dist/marketplace.d.ts +14 -1
- package/dist/marketplace.js +117 -61
- package/dist/snapshot.d.ts +16 -2
- package/dist/snapshot.js +62 -7
- 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/dist/builder.js
CHANGED
|
@@ -240,6 +240,11 @@ export function buildAtomicSteps(collected, state) {
|
|
|
240
240
|
`\`⚠️ UNKNOWN PACKAGE — [service] MCP server not added: package name unverified. Find it at https://github.com/modelcontextprotocol/servers\`\n` +
|
|
241
241
|
`Do not add a placeholder. Do not guess.\n\n` +
|
|
242
242
|
`### OS-correct format (detected: ${os})\n` +
|
|
243
|
+
`**Preferred: use CLI to add (writes to .mcp.json automatically):**\n` +
|
|
244
|
+
(os === "Windows"
|
|
245
|
+
? `\`\`\`\nclaude mcp add --scope project --transport stdio <name> -- cmd /c npx -y <package>\n\`\`\`\n`
|
|
246
|
+
: `\`\`\`\nclaude mcp add --scope project --transport stdio <name> -- npx -y <package>\n\`\`\`\n`) +
|
|
247
|
+
`**Or write .mcp.json directly:**\n` +
|
|
243
248
|
(os === "Windows"
|
|
244
249
|
? `Use: \`{ "command": "cmd", "args": ["/c", "npx", "-y", "<package>"] }\`\n`
|
|
245
250
|
: `Use: \`{ "command": "npx", "args": ["-y", "<package>"] }\`\n`) +
|
|
@@ -270,6 +275,13 @@ export function buildAtomicSteps(collected, state) {
|
|
|
270
275
|
? `\`{ "command": "cmd", "args": ["/c", "bun", "run", "\${CLAUDE_PLUGIN_ROOT}/servers/telegram"], "env": { "TELEGRAM_BOT_TOKEN": "\${TELEGRAM_BOT_TOKEN}" } }\`\n`
|
|
271
276
|
: `\`{ "command": "bun", "args": ["run", "\${CLAUDE_PLUGIN_ROOT}/servers/telegram"], "env": { "TELEGRAM_BOT_TOKEN": "\${TELEGRAM_BOT_TOKEN}" } }\`\n`) +
|
|
272
277
|
`After adding, flag: \`⚠️ CHANNEL ACTIVATION REQUIRED — launch with: claude --channels plugin:telegram@claude-plugins-official\`\n\n` +
|
|
278
|
+
`### Self-correction fallback\n` +
|
|
279
|
+
`If MCP configuration fails or produces warnings:\n` +
|
|
280
|
+
`1. Read the official MCP documentation: https://modelcontextprotocol.io/introduction\n` +
|
|
281
|
+
`2. Verify the package name against the MCP servers registry: https://github.com/modelcontextprotocol/servers\n` +
|
|
282
|
+
`3. Check the server's README for required env vars and correct args format\n` +
|
|
283
|
+
`4. Re-run \`npx claude-setup doctor\` to validate the fix\n` +
|
|
284
|
+
`Do NOT leave broken MCP configuration in place — either fix it or remove the entry.\n\n` +
|
|
273
285
|
`### Output\n` +
|
|
274
286
|
`Created/Updated: ✅ .mcp.json — [what server and evidence source]\n` +
|
|
275
287
|
`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
6
|
import { buildEmptyProjectCommand, buildAtomicSteps, buildOrchestratorCommand, } 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,12 +74,13 @@ 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");
|
|
49
82
|
await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
|
|
83
|
+
installTokenHook();
|
|
50
84
|
// Feature A: Create initial snapshot node
|
|
51
85
|
const cwd = process.cwd();
|
|
52
86
|
const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
|
|
@@ -61,7 +95,15 @@ 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
|
+
console.log(` ${c.dim(`This command estimate: ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`)}`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
|
|
105
|
+
console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
|
|
106
|
+
}
|
|
65
107
|
console.log("");
|
|
66
108
|
return;
|
|
67
109
|
}
|
|
@@ -81,7 +123,7 @@ Claude Code will ask 3 questions, then set up your environment.
|
|
|
81
123
|
console.log(` .claude/commands/stack-init.md (orchestrator)`);
|
|
82
124
|
console.log(`\n${c.dim(`Total: ~${tokens.toLocaleString()} tokens across ${steps.length} files`)}`);
|
|
83
125
|
section("Token cost estimate");
|
|
84
|
-
console.log(` ~${tokens.toLocaleString()} input tokens (
|
|
126
|
+
console.log(` ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`);
|
|
85
127
|
return;
|
|
86
128
|
}
|
|
87
129
|
ensureDir(".claude/commands");
|
|
@@ -90,6 +132,7 @@ Claude Code will ask 3 questions, then set up your environment.
|
|
|
90
132
|
}
|
|
91
133
|
writeFileSync(".claude/commands/stack-init.md", orchestrator, "utf8");
|
|
92
134
|
await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
|
|
135
|
+
installTokenHook();
|
|
93
136
|
// Feature A: Create initial snapshot node
|
|
94
137
|
const cwd = process.cwd();
|
|
95
138
|
const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
|
|
@@ -104,6 +147,14 @@ ${c.green("✅")} Ready. Open Claude Code and run:
|
|
|
104
147
|
Runs ${steps.length - 1} atomic steps. If one fails, re-run only that step.
|
|
105
148
|
`);
|
|
106
149
|
section("Token cost");
|
|
107
|
-
|
|
150
|
+
const realSummary2 = formatRealCostSummary(cwd);
|
|
151
|
+
if (realSummary2) {
|
|
152
|
+
console.log(realSummary2);
|
|
153
|
+
console.log(` ${c.dim(`This command estimate: ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`)}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
|
|
157
|
+
console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
|
|
158
|
+
}
|
|
108
159
|
console.log("");
|
|
109
160
|
}
|
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) {
|
|
@@ -20,10 +20,13 @@ export async function runRestore() {
|
|
|
20
20
|
// Display timeline
|
|
21
21
|
section("Snapshot timeline");
|
|
22
22
|
console.log("");
|
|
23
|
+
const restoredTo = timeline.restoredTo;
|
|
23
24
|
for (let i = 0; i < timeline.nodes.length; i++) {
|
|
24
25
|
const node = timeline.nodes[i];
|
|
25
26
|
const date = new Date(node.timestamp).toLocaleString();
|
|
26
|
-
const
|
|
27
|
+
const isLatest = i === timeline.nodes.length - 1;
|
|
28
|
+
const isRestored = restoredTo === node.id && !isLatest;
|
|
29
|
+
const current = isLatest ? ` ${c.green("← current")}` : isRestored ? ` ${c.cyan("← restored here")}` : "";
|
|
27
30
|
const connector = i < timeline.nodes.length - 1 ? "──→" : " ";
|
|
28
31
|
const inputStr = node.input ? ` "${node.input}"` : "";
|
|
29
32
|
console.log(` ${c.cyan(node.id)} ${node.command}${inputStr} ${c.dim(date)} ${node.summary}${current}`);
|
|
@@ -43,7 +46,8 @@ export async function runRestore() {
|
|
|
43
46
|
}
|
|
44
47
|
console.log(`\nRestoring to snapshot ${c.cyan(node.id)} (${new Date(node.timestamp).toLocaleString()})...`);
|
|
45
48
|
console.log(`${c.dim("Other snapshots are preserved — you can jump forward or back at any time.")}\n`);
|
|
46
|
-
const result = restoreSnapshot(cwd, input);
|
|
49
|
+
const result = restoreSnapshot(cwd, input, timeline);
|
|
50
|
+
updateRestoredNode(cwd, input);
|
|
47
51
|
if (result.restored.length) {
|
|
48
52
|
section("Restored files");
|
|
49
53
|
for (const f of result.restored) {
|
|
@@ -56,6 +60,37 @@ export async function runRestore() {
|
|
|
56
60
|
console.log(` ${c.red("🔴")} ${f}`);
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
if (result.stale.length) {
|
|
64
|
+
section("Files not in this snapshot (may be stale)");
|
|
65
|
+
console.log(` ${c.dim("These files exist now but were not part of the restored snapshot:")}`);
|
|
66
|
+
for (const f of result.stale) {
|
|
67
|
+
console.log(` ${c.yellow("⚠️")} ${f}`);
|
|
68
|
+
}
|
|
69
|
+
console.log(` ${c.dim("To fully reset, delete these manually or run sync to update the snapshot.")}`);
|
|
70
|
+
}
|
|
71
|
+
if (result.restored.length === 0 && result.stale.length === 0) {
|
|
72
|
+
console.log(`\n${c.yellow("⚠️")} This snapshot captured 0 files — the project was empty at that point.`);
|
|
73
|
+
console.log(` Files added since this snapshot have been left in place.`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.log(`\n${c.green("✅")} Restored ${result.restored.length} file(s) to snapshot ${c.cyan(node.id)}.`);
|
|
77
|
+
}
|
|
78
|
+
// Re-read and display updated timeline showing the restored position
|
|
79
|
+
const updatedTimeline = readTimeline(cwd);
|
|
80
|
+
console.log("");
|
|
81
|
+
section("Updated timeline");
|
|
82
|
+
console.log("");
|
|
83
|
+
for (let i = 0; i < updatedTimeline.nodes.length; i++) {
|
|
84
|
+
const n = updatedTimeline.nodes[i];
|
|
85
|
+
const date = new Date(n.timestamp).toLocaleString();
|
|
86
|
+
const isRestored = updatedTimeline.restoredTo === n.id;
|
|
87
|
+
const marker = isRestored ? ` ${c.cyan("← restored here")}` : "";
|
|
88
|
+
const connector = i < updatedTimeline.nodes.length - 1 ? "──→" : " ";
|
|
89
|
+
const inputStr = n.input ? ` "${n.input}"` : "";
|
|
90
|
+
console.log(` ${c.cyan(n.id)} ${n.command}${inputStr} ${c.dim(date)} ${n.summary}${marker}`);
|
|
91
|
+
if (i < updatedTimeline.nodes.length - 1)
|
|
92
|
+
console.log(` ${c.dim(connector)}`);
|
|
93
|
+
}
|
|
94
|
+
console.log(`\nTimeline position updated → snapshot ${c.cyan(node.id)}`);
|
|
95
|
+
console.log(`Run ${c.cyan("npx claude-setup sync")} to capture the current state as a new node.\n`);
|
|
61
96
|
}
|
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
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { glob } from "glob";
|
|
3
4
|
import { collectProjectFiles } from "../collect.js";
|
|
4
5
|
import { readState } from "../state.js";
|
|
5
6
|
import { readManifest, sha256, updateManifest } from "../manifest.js";
|
|
6
7
|
import { buildSyncCommand } from "../builder.js";
|
|
7
8
|
import { createSnapshot, collectFilesForSnapshot } from "../snapshot.js";
|
|
8
|
-
import { estimateTokens, estimateCost, formatTokenReport, buildTokenEstimate, generateHints } from "../tokens.js";
|
|
9
|
+
import { estimateTokens, estimateCost, formatCost, formatTokenReport, buildTokenEstimate, generateHints, getTokenHookScript, formatRealCostSummary } from "../tokens.js";
|
|
9
10
|
import { loadConfig } from "../config.js";
|
|
10
11
|
import { c, section } from "../output.js";
|
|
11
12
|
function ensureDir(dir) {
|
|
12
13
|
if (!existsSync(dir))
|
|
13
14
|
mkdirSync(dir, { recursive: true });
|
|
14
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
|
+
}
|
|
15
49
|
function truncate(content, maxChars) {
|
|
16
50
|
if (content.length <= maxChars)
|
|
17
51
|
return content;
|
|
@@ -72,6 +106,23 @@ function computeDiff(snapshot, collected, cwd) {
|
|
|
72
106
|
}
|
|
73
107
|
return { added, changed, deleted };
|
|
74
108
|
}
|
|
109
|
+
async function collectClaudeInternalFiles(cwd) {
|
|
110
|
+
const files = [];
|
|
111
|
+
try {
|
|
112
|
+
const skillFiles = await glob(".claude/skills/**/*.md", { cwd, posix: true });
|
|
113
|
+
const allCmds = await glob(".claude/commands/*.md", { cwd, posix: true });
|
|
114
|
+
const commandFiles = allCmds.filter(f => !f.split("/").pop().startsWith("stack-"));
|
|
115
|
+
for (const f of [...skillFiles, ...commandFiles]) {
|
|
116
|
+
try {
|
|
117
|
+
const content = readFileSync(join(cwd, f), "utf8");
|
|
118
|
+
files.push({ path: f, content });
|
|
119
|
+
}
|
|
120
|
+
catch { /* skip unreadable */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch { /* skip */ }
|
|
124
|
+
return files;
|
|
125
|
+
}
|
|
75
126
|
export async function runSync(opts = {}) {
|
|
76
127
|
const dryRun = opts.dryRun ?? false;
|
|
77
128
|
const manifest = await readManifest();
|
|
@@ -112,6 +163,30 @@ export async function runSync(opts = {}) {
|
|
|
112
163
|
console.log("");
|
|
113
164
|
const collected = await collectProjectFiles(cwd, "normal");
|
|
114
165
|
const diff = computeDiff(lastRun.snapshot, collected, cwd);
|
|
166
|
+
// Bug 3 fix: Also detect changes inside .claude/ (skills, commands)
|
|
167
|
+
const claudeInternalFiles = await collectClaudeInternalFiles(cwd);
|
|
168
|
+
for (const f of claudeInternalFiles) {
|
|
169
|
+
const hash = sha256(f.content);
|
|
170
|
+
if (!lastRun.snapshot[f.path]) {
|
|
171
|
+
diff.added.push({ path: f.path, content: truncate(f.content, 2000) });
|
|
172
|
+
}
|
|
173
|
+
else if (lastRun.snapshot[f.path] !== hash) {
|
|
174
|
+
diff.changed.push({ path: f.path, current: truncate(f.content, 2000) });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Also detect deleted .claude/ files (were in snapshot but no longer exist)
|
|
178
|
+
for (const path of Object.keys(lastRun.snapshot)) {
|
|
179
|
+
if ((path.startsWith(".claude/skills/") || (path.startsWith(".claude/commands/") && !path.split("/").pop().startsWith("stack-"))) && !path.includes("__digest__")) {
|
|
180
|
+
const alreadyInDiff = diff.added.some(f => f.path === path) ||
|
|
181
|
+
diff.changed.some(f => f.path === path) ||
|
|
182
|
+
diff.deleted.includes(path);
|
|
183
|
+
if (!alreadyInDiff && !claudeInternalFiles.some(f => f.path === path)) {
|
|
184
|
+
if (!existsSync(join(cwd, path))) {
|
|
185
|
+
diff.deleted.push(path);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
115
190
|
if (!diff.added.length && !diff.changed.length && !diff.deleted.length && !oobDetected) {
|
|
116
191
|
console.log(`${c.green("✅")} No changes since ${c.dim(lastRun.at)}. Setup is current.`);
|
|
117
192
|
return;
|
|
@@ -145,13 +220,19 @@ export async function runSync(opts = {}) {
|
|
|
145
220
|
console.log(formatTokenReport(estimate));
|
|
146
221
|
return;
|
|
147
222
|
}
|
|
223
|
+
// Add .claude/ internal files to snapshot
|
|
224
|
+
for (const f of claudeInternalFiles) {
|
|
225
|
+
collected.configs[f.path] = f.content;
|
|
226
|
+
}
|
|
148
227
|
ensureDir(".claude/commands");
|
|
149
228
|
writeFileSync(".claude/commands/stack-sync.md", content, "utf8");
|
|
150
229
|
await updateManifest("sync", collected, { estimatedTokens: tokens, estimatedCost: cost });
|
|
230
|
+
installTokenHook();
|
|
151
231
|
// Feature A: Create snapshot node
|
|
152
232
|
const allPaths = [
|
|
153
233
|
...Object.keys(collected.configs),
|
|
154
234
|
...collected.source.map(s => s.path),
|
|
235
|
+
...claudeInternalFiles.map(f => f.path),
|
|
155
236
|
];
|
|
156
237
|
const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
|
|
157
238
|
const changeCount = diff.added.length + diff.changed.length + diff.deleted.length;
|
|
@@ -167,7 +248,15 @@ ${c.green("✅")} Ready. Open Claude Code and run:
|
|
|
167
248
|
`);
|
|
168
249
|
// Token cost display
|
|
169
250
|
section("Token cost");
|
|
170
|
-
|
|
251
|
+
const realSummary = formatRealCostSummary(cwd);
|
|
252
|
+
if (realSummary) {
|
|
253
|
+
console.log(realSummary);
|
|
254
|
+
console.log(` ${c.dim(`This command estimate: ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`)}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
|
|
258
|
+
console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
|
|
259
|
+
}
|
|
171
260
|
// Optimization hints
|
|
172
261
|
const runs = manifest.runs.map(r => ({ command: r.command, estimatedTokens: r.estimatedTokens }));
|
|
173
262
|
const hints = generateHints(runs, tokens, config.tokenBudget.sync);
|
package/dist/doctor.js
CHANGED
|
@@ -116,6 +116,7 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
|
|
|
116
116
|
// --- Check 3: OS/MCP format mismatch ---
|
|
117
117
|
if (state.mcpJson.content) {
|
|
118
118
|
section("MCP servers");
|
|
119
|
+
const mcpWarningsBefore = counts.warnings + counts.critical;
|
|
119
120
|
const mcp = safeJsonParse(state.mcpJson.content);
|
|
120
121
|
if (mcp && typeof mcp.mcpServers === "object" && mcp.mcpServers !== null) {
|
|
121
122
|
const servers = mcp.mcpServers;
|
|
@@ -182,6 +183,12 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
}
|
|
186
|
+
// Check if server has no env vars but uses npx (might fail on first run due to npm download)
|
|
187
|
+
if (!config.env && (cmd === "npx" || (cmd === "cmd" && config.args?.includes("npx")))) {
|
|
188
|
+
if (verbose) {
|
|
189
|
+
statusLine("💡", name, c.dim("uses npx — will download package on first run (requires internet)"));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
185
192
|
}
|
|
186
193
|
// Check for channel-type servers
|
|
187
194
|
const channelNames = ["telegram", "discord", "fakechat"];
|
|
@@ -203,6 +210,12 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
|
|
|
203
210
|
if (verbose)
|
|
204
211
|
statusLine("⚠️ ", ".mcp.json", "no mcpServers key found");
|
|
205
212
|
}
|
|
213
|
+
if (counts.warnings + counts.critical > mcpWarningsBefore) {
|
|
214
|
+
console.log(`\n ${c.dim("MCP self-correction:")}`);
|
|
215
|
+
console.log(` • ${c.cyan("npx claude-setup doctor --fix")} — auto-fix OS format and -y flag`);
|
|
216
|
+
console.log(` • Set missing env vars, then re-run ${c.cyan("npx claude-setup doctor")}`);
|
|
217
|
+
console.log(` • Verify server packages: https://github.com/modelcontextprotocol/servers`);
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
220
|
else if (verbose) {
|
|
208
221
|
section("MCP servers");
|
|
@@ -389,13 +402,23 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
|
|
|
389
402
|
const template = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? readIfExists(".env.template") ?? "";
|
|
390
403
|
section("Env vars");
|
|
391
404
|
for (const v of unique) {
|
|
392
|
-
|
|
393
|
-
|
|
405
|
+
const isActuallySet = process.env[v] !== undefined && process.env[v] !== "";
|
|
406
|
+
const isInTemplate = template.includes(v);
|
|
407
|
+
if (isActuallySet) {
|
|
408
|
+
statusLine("✅", `\${${v}}`, "set in environment");
|
|
394
409
|
counts.healthy++;
|
|
395
410
|
}
|
|
411
|
+
else if (isInTemplate) {
|
|
412
|
+
statusLine("🔴", `\${${v}}`, c.red(`NOT SET — MCP server will fail at runtime.\n` +
|
|
413
|
+
` Documented in .env.example but not loaded into environment.\n` +
|
|
414
|
+
` Fix: set ${v} in your shell or .env file, then restart Claude Code.`));
|
|
415
|
+
counts.critical++;
|
|
416
|
+
}
|
|
396
417
|
else {
|
|
397
|
-
statusLine("
|
|
398
|
-
|
|
418
|
+
statusLine("🔴", `\${${v}}`, c.red(`NOT SET — MCP server will fail at runtime.\n` +
|
|
419
|
+
` Missing from both environment and .env.example.\n` +
|
|
420
|
+
` Fix: add ${v} to .env.example and set its value in your shell or .env file.`));
|
|
421
|
+
counts.critical++;
|
|
399
422
|
}
|
|
400
423
|
}
|
|
401
424
|
}
|
package/dist/marketplace.d.ts
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const MARKETPLACE_REPO = "jeremylongshore/claude-code-plugins-plus-skills";
|
|
8
8
|
export declare const MARKETPLACE_CATALOG_URL = "https://raw.githubusercontent.com/jeremylongshore/claude-code-plugins-plus-skills/main/.claude-plugin/marketplace.extended.json";
|
|
9
|
+
/** Additional marketplace sources for broader coverage */
|
|
10
|
+
export declare const ADDITIONAL_MARKETPLACE_SOURCES: readonly [{
|
|
11
|
+
readonly name: "claude-plugins-official";
|
|
12
|
+
readonly description: "Official Anthropic plugins (GitHub, Slack, Linear, Notion, etc.)";
|
|
13
|
+
readonly installPrefix: "claude-plugins-official";
|
|
14
|
+
readonly note: "No marketplace add needed — available by default";
|
|
15
|
+
}, {
|
|
16
|
+
readonly name: "awesome-claude-code";
|
|
17
|
+
readonly description: "Community collection of Claude Code skills and workflows";
|
|
18
|
+
readonly catalogUrl: "https://raw.githubusercontent.com/hesreallyhim/awesome-claude-code/main/catalog.json";
|
|
19
|
+
readonly installPrefix: null;
|
|
20
|
+
readonly note: "Browse and manually install skills";
|
|
21
|
+
}];
|
|
9
22
|
/** The 20 skill categories in the marketplace */
|
|
10
23
|
export declare const SKILL_CATEGORIES: readonly ["01-code-quality", "02-testing", "03-security", "04-devops", "05-api-development", "06-database", "07-frontend", "08-backend", "09-mobile", "10-data-science", "11-documentation", "12-project-management", "13-communication", "14-research", "15-content-creation", "16-business", "17-finance", "18-visual-content", "19-legal", "20-productivity"];
|
|
11
24
|
/** SaaS packs available in the marketplace */
|
|
@@ -17,5 +30,5 @@ export declare function classifyRequest(input: string): {
|
|
|
17
30
|
categories: string[];
|
|
18
31
|
saasMatches: string[];
|
|
19
32
|
};
|
|
20
|
-
/** Generate marketplace search
|
|
33
|
+
/** Generate fully-automated marketplace search and install instructions */
|
|
21
34
|
export declare function buildMarketplaceInstructions(input: string): string;
|