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 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`,
@@ -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
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 (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");
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
- 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
+ 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 (Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)})`);
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
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
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
  }
@@ -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) {
@@ -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 current = i === timeline.nodes.length - 1 ? ` ${c.green("← current")}` : "";
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
- 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.`);
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
  }
@@ -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
  }
@@ -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
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
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
- if (template.includes(v)) {
393
- statusLine("✅", `\${${v}}`, "found in env template");
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("⚠️ ", `\${${v}}`, c.yellow("used in .mcp.json but missing from .env.example"));
398
- counts.warnings++;
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
  }
@@ -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 instructions for the add template */
33
+ /** Generate fully-automated marketplace search and install instructions */
21
34
  export declare function buildMarketplaceInstructions(input: string): string;