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.
@@ -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,9 +163,34 @@ 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);
115
- if (!diff.added.length && !diff.changed.length && !diff.deleted.length && !oobDetected) {
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
+ }
190
+ const hasChanges = diff.added.length > 0 || diff.changed.length > 0 || diff.deleted.length > 0 || oobDetected;
191
+ if (!hasChanges) {
116
192
  console.log(`${c.green("✅")} No changes since ${c.dim(lastRun.at)}. Setup is current.`);
117
- return;
193
+ // Still regenerate the command file so /stack-sync self-refresh always gets an up-to-date "no changes" state
118
194
  }
119
195
  const state = await readState();
120
196
  const content = buildSyncCommand(diff, collected, state);
@@ -145,37 +221,51 @@ export async function runSync(opts = {}) {
145
221
  console.log(formatTokenReport(estimate));
146
222
  return;
147
223
  }
224
+ // Add .claude/ internal files to snapshot
225
+ for (const f of claudeInternalFiles) {
226
+ collected.configs[f.path] = f.content;
227
+ }
148
228
  ensureDir(".claude/commands");
149
229
  writeFileSync(".claude/commands/stack-sync.md", content, "utf8");
150
230
  await updateManifest("sync", collected, { estimatedTokens: tokens, estimatedCost: cost });
151
- // Feature A: Create snapshot node
231
+ installTokenHook();
232
+ // Create snapshot node — collectFilesForSnapshot scans all .claude/ automatically
152
233
  const allPaths = [
153
234
  ...Object.keys(collected.configs),
154
235
  ...collected.source.map(s => s.path),
155
236
  ];
156
237
  const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
157
- const changeCount = diff.added.length + diff.changed.length + diff.deleted.length;
158
238
  createSnapshot(cwd, "sync", snapshotFiles, {
159
239
  summary: `+${diff.added.length} added, ~${diff.changed.length} modified, -${diff.deleted.length} deleted`,
160
240
  });
161
- console.log(`
241
+ if (hasChanges) {
242
+ console.log(`
162
243
  Changes since ${c.dim(lastRun.at)}:
163
244
  ${c.green(`+${diff.added.length}`)} added ${c.yellow(`~${diff.changed.length}`)} modified ${c.red(`-${diff.deleted.length}`)} deleted
164
245
 
165
- ${c.green("✅")} Ready. Open Claude Code and run:
166
- ${c.cyan("/stack-sync")}
167
- `);
168
- // Token cost display
169
- 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)}`)})`);
171
- // Optimization hints
172
- const runs = manifest.runs.map(r => ({ command: r.command, estimatedTokens: r.estimatedTokens }));
173
- const hints = generateHints(runs, tokens, config.tokenBudget.sync);
174
- if (hints.length) {
175
- section("Optimization hints");
176
- for (const hint of hints) {
177
- console.log(` ${c.yellow("💡")} ${hint}`);
246
+ ${c.green("✅")} Run ${c.cyan("/stack-sync")} in Claude Code to apply.
247
+ `);
248
+ }
249
+ if (hasChanges) {
250
+ // Token cost display
251
+ section("Token cost");
252
+ const realSummary = formatRealCostSummary(cwd);
253
+ if (realSummary) {
254
+ console.log(realSummary);
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")}`);
178
259
  }
260
+ // Optimization hints
261
+ const runs = manifest.runs.map(r => ({ command: r.command, estimatedTokens: r.estimatedTokens }));
262
+ const hints = generateHints(runs, tokens, config.tokenBudget.sync);
263
+ if (hints.length) {
264
+ section("Optimization hints");
265
+ for (const hint of hints) {
266
+ console.log(` ${c.yellow("💡")} ${hint}`);
267
+ }
268
+ }
269
+ console.log("");
179
270
  }
180
- console.log("");
181
271
  }
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");
@@ -362,9 +375,14 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
362
375
  counts.warnings++;
363
376
  }
364
377
  else if (result.status === "error") {
378
+ const stderr = result.stderr ?? "";
379
+ const isFileMissing = stderr.includes("Cannot find module") || stderr.includes("MODULE_NOT_FOUND");
380
+ const hint = isFileMissing
381
+ ? `\n Hint: hook file not found — run ${c.cyan("npx claude-setup init")} to reinstall it.`
382
+ : "";
365
383
  statusLine("⚠️ ", label, c.yellow(`FAIL (exit ${result.exitCode}, ${result.timeMs}ms)\n` +
366
- ` Command: ${hook.command.slice(0, 50)}\n` +
367
- ` ${result.stderr ? `stderr: ${result.stderr.slice(0, 100)}` : ""}`));
384
+ ` Command: ${hook.command.slice(0, 60)}\n` +
385
+ ` ${stderr ? `stderr: ${stderr.slice(0, 200)}` : ""}${hint}`));
368
386
  counts.warnings++;
369
387
  }
370
388
  else if (result.status === "permission") {
@@ -389,13 +407,23 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
389
407
  const template = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? readIfExists(".env.template") ?? "";
390
408
  section("Env vars");
391
409
  for (const v of unique) {
392
- if (template.includes(v)) {
393
- statusLine("✅", `\${${v}}`, "found in env template");
410
+ const isActuallySet = process.env[v] !== undefined && process.env[v] !== "";
411
+ const isInTemplate = template.includes(v);
412
+ if (isActuallySet) {
413
+ statusLine("✅", `\${${v}}`, "set in environment");
394
414
  counts.healthy++;
395
415
  }
416
+ else if (isInTemplate) {
417
+ statusLine("🔴", `\${${v}}`, c.red(`NOT SET — MCP server will fail at runtime and won't appear in /mcp.\n` +
418
+ ` Documented in .env.example but not loaded into environment.\n` +
419
+ ` Fix: set ${v} in your shell or .env file, then restart Claude Code.`));
420
+ counts.critical++;
421
+ }
396
422
  else {
397
- statusLine("⚠️ ", `\${${v}}`, c.yellow("used in .mcp.json but missing from .env.example"));
398
- counts.warnings++;
423
+ statusLine("🔴", `\${${v}}`, c.red(`NOT SET MCP server will fail at runtime and won't appear in /mcp.\n` +
424
+ ` Missing from both environment and .env.example.\n` +
425
+ ` Fix: add ${v} to .env.example and set its value in your shell or .env file.`));
426
+ counts.critical++;
399
427
  }
400
428
  }
401
429
  }
@@ -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;
@@ -6,6 +6,22 @@
6
6
  */
7
7
  export const MARKETPLACE_REPO = "jeremylongshore/claude-code-plugins-plus-skills";
8
8
  export const MARKETPLACE_CATALOG_URL = `https://raw.githubusercontent.com/${MARKETPLACE_REPO}/main/.claude-plugin/marketplace.extended.json`;
9
+ /** Additional marketplace sources for broader coverage */
10
+ export const ADDITIONAL_MARKETPLACE_SOURCES = [
11
+ {
12
+ name: "claude-plugins-official",
13
+ description: "Official Anthropic plugins (GitHub, Slack, Linear, Notion, etc.)",
14
+ installPrefix: "claude-plugins-official",
15
+ note: "No marketplace add needed — available by default"
16
+ },
17
+ {
18
+ name: "awesome-claude-code",
19
+ description: "Community collection of Claude Code skills and workflows",
20
+ catalogUrl: "https://raw.githubusercontent.com/hesreallyhim/awesome-claude-code/main/catalog.json",
21
+ installPrefix: null,
22
+ note: "Browse and manually install skills"
23
+ }
24
+ ];
9
25
  /** The 20 skill categories in the marketplace */
10
26
  export const SKILL_CATEGORIES = [
11
27
  "01-code-quality", "02-testing", "03-security",
@@ -83,77 +99,117 @@ export function classifyRequest(input) {
83
99
  }
84
100
  return { categories: [...categories], saasMatches };
85
101
  }
86
- /** Generate marketplace search instructions for the add template */
102
+ /** Generate fully-automated marketplace search and install instructions */
87
103
  export function buildMarketplaceInstructions(input) {
88
104
  const { categories, saasMatches } = classifyRequest(input);
105
+ const categoryFilter = categories[0] ?? "";
89
106
  const lines = [];
90
- lines.push(`## Marketplace intelligence`);
107
+ lines.push(`## Marketplace — fully automated install`);
91
108
  lines.push(``);
92
- lines.push(`A plugin marketplace is available with 340+ plugins and 1,367+ agent skills.`);
109
+ lines.push(`A plugin marketplace has 416 community skills + 13 official Anthropic plugins.`);
110
+ lines.push(`Follow the steps below IN ORDER. Do not stop until a skill is installed.`);
93
111
  lines.push(``);
94
112
  if (categories.length > 0 || saasMatches.length > 0) {
95
- lines.push(`### Matched categories for "${input}":`);
96
- for (const cat of categories) {
97
- lines.push(`- ${cat}`);
113
+ lines.push(`### Matched for "${input}":`);
114
+ for (const cat of categories)
115
+ lines.push(`- category: ${cat}`);
116
+ for (const saas of saasMatches)
117
+ lines.push(`- SaaS: ${saas}`);
118
+ lines.push(``);
119
+ }
120
+ lines.push(`---`);
121
+ lines.push(``);
122
+ lines.push(`### STEP 1 — Try official Anthropic plugins first (no schema errors, always works)`);
123
+ lines.push(``);
124
+ lines.push(`\`\`\``);
125
+ lines.push(`/plugin marketplace add anthropics/claude-code`);
126
+ lines.push(`\`\`\``);
127
+ lines.push(`Then search installed plugins for a match. If found, install:`);
128
+ lines.push(`\`\`\``);
129
+ lines.push(`/plugin install <name>@claude-code-plugins`);
130
+ lines.push(`\`\`\``);
131
+ lines.push(`Official plugins: agent-sdk-dev, claude-opus-4-5-migration, code-review, commit-commands,`);
132
+ lines.push(`explanatory-output-style, feature-dev, frontend-design, hookify, learning-output-style,`);
133
+ lines.push(`plugin-dev, pr-review-toolkit, ralph-wiggum, security-guidance`);
134
+ lines.push(``);
135
+ lines.push(`---`);
136
+ lines.push(``);
137
+ lines.push(`### STEP 2 — Search community catalog (jeremylongshore — 416 plugins)`);
138
+ lines.push(``);
139
+ lines.push(`NOTE: /plugin marketplace add FAILS for this repo (one entry has broken schema).`);
140
+ lines.push(`Use the DIRECT FETCH approach below instead — no marketplace add needed.`);
141
+ lines.push(``);
142
+ lines.push(`**2a. Fetch catalog and find matching plugin:**`);
143
+ lines.push(`\`\`\`bash`);
144
+ lines.push(`curl -s "${MARKETPLACE_CATALOG_URL}" \\`);
145
+ lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));const q='${categoryFilter}';const r=d.plugins.filter(p=>(!q||p.category.includes(q))&&p.name&&p.source).slice(0,5).map(p=>({name:p.name,source:p.source,desc:p.description}));console.log(JSON.stringify(r,null,2));"`);
146
+ lines.push(`\`\`\``);
147
+ lines.push(``);
148
+ lines.push(`**2b. Pick the best match — get its source path (e.g. \`./plugins/productivity/my-skill\`)**`);
149
+ lines.push(``);
150
+ lines.push(`**2c. List the skills inside that plugin:**`);
151
+ lines.push(`\`\`\`bash`);
152
+ lines.push(`# Replace PLUGIN_SOURCE_PATH with value from step 2b (e.g. plugins/productivity/my-skill)`);
153
+ lines.push(`PLUGIN_SOURCE_PATH="plugins/productivity/my-skill"`);
154
+ lines.push(`curl -s "https://api.github.com/repos/${MARKETPLACE_REPO}/contents/\${PLUGIN_SOURCE_PATH}/skills" \\`);
155
+ lines.push(` | node -e "const a=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(a.map(x=>x.name).join('\\n'));"`);
156
+ lines.push(`\`\`\``);
157
+ lines.push(``);
158
+ lines.push(`**2d. For each skill listed, download and install it:**`);
159
+ lines.push(`\`\`\`bash`);
160
+ lines.push(`# Replace PLUGIN_SOURCE_PATH and SKILL_NAME with actual values`);
161
+ lines.push(`PLUGIN_SOURCE_PATH="plugins/productivity/my-skill"`);
162
+ lines.push(`SKILL_NAME="skill-directory-name"`);
163
+ lines.push(`mkdir -p ".claude/skills/\${SKILL_NAME}"`);
164
+ lines.push(`curl -s "https://raw.githubusercontent.com/${MARKETPLACE_REPO}/main/\${PLUGIN_SOURCE_PATH}/skills/\${SKILL_NAME}/SKILL.md" \\`);
165
+ lines.push(` -o ".claude/skills/\${SKILL_NAME}/SKILL.md"`);
166
+ lines.push(`echo "Installed: .claude/skills/\${SKILL_NAME}/SKILL.md"`);
167
+ lines.push(`\`\`\``);
168
+ lines.push(``);
169
+ lines.push(`**On Windows, replace curl with:**`);
170
+ lines.push(`\`\`\`powershell`);
171
+ lines.push(`$url = "https://raw.githubusercontent.com/${MARKETPLACE_REPO}/main/$PLUGIN_SOURCE_PATH/skills/$SKILL_NAME/SKILL.md"`);
172
+ lines.push(`New-Item -ItemType Directory -Force ".claude/skills/$SKILL_NAME" | Out-Null`);
173
+ lines.push(`Invoke-WebRequest $url -OutFile ".claude/skills/$SKILL_NAME/SKILL.md"`);
174
+ lines.push(`\`\`\``);
175
+ lines.push(``);
176
+ lines.push(`---`);
177
+ lines.push(``);
178
+ lines.push(`### STEP 3 — Search additional sources`);
179
+ lines.push(``);
180
+ for (const source of ADDITIONAL_MARKETPLACE_SOURCES) {
181
+ lines.push(`**${source.name}** — ${source.description}`);
182
+ if ("catalogUrl" in source && source.catalogUrl) {
183
+ lines.push(`Catalog: ${source.catalogUrl}`);
98
184
  }
99
- for (const saas of saasMatches) {
100
- lines.push(`- SaaS pack: ${saas} (~30 skills)`);
185
+ if (source.note) {
186
+ lines.push(`Note: ${source.note}`);
101
187
  }
102
188
  lines.push(``);
103
189
  }
104
- lines.push(`### How to search and install`);
105
- lines.push(``);
106
- lines.push(`**Step 1Add the marketplace** (if not already added):`);
107
- lines.push("```");
108
- lines.push(`/plugin marketplace add ${MARKETPLACE_REPO}`);
109
- lines.push("```");
110
- lines.push(``);
111
- lines.push(`**Step 2 Search for matching plugins:**`);
112
- lines.push("```");
113
- lines.push(`# Fetch the catalog:`);
114
- lines.push(`curl -s ${MARKETPLACE_CATALOG_URL} | jq '[.[] | select(.category | test("${categories[0] ?? ""}"; "i"))]'`);
115
- lines.push("```");
116
- lines.push(``);
117
- lines.push(`**Step 3 — Install matching plugins:**`);
118
- lines.push("```");
119
- lines.push(`/plugin install <name>@claude-code-plugins-plus`);
120
- lines.push("```");
121
- lines.push(``);
122
- lines.push(`### Before suggesting any plugin, validate:`);
123
- lines.push(`- \`mcp_required\` field — if true, flag the MCP dependency`);
124
- lines.push(`- \`free\` field — if false, flag that it needs a paid API`);
125
- lines.push(`- Never suggest a plugin without checking the catalog first`);
126
- lines.push(`- Never hardcode a plugin name from memory — validate against the fetched catalog`);
127
- lines.push(``);
128
- lines.push(`### Suggestion format:`);
129
- lines.push("```");
130
- lines.push(`📦 Suggested from [claude-code-plugins-plus-skills]`);
131
- lines.push(``);
132
- lines.push(` [plugin/skill name]`);
133
- lines.push(` Category : [category]`);
134
- lines.push(` What it does: [one sentence from catalog description]`);
135
- lines.push(` Requires : [nothing / MCP: name / Paid API: service name]`);
136
- lines.push(``);
137
- lines.push(` Install:`);
138
- lines.push(` /plugin marketplace add ${MARKETPLACE_REPO}`);
139
- lines.push(` /plugin install [name]@claude-code-plugins-plus`);
140
- lines.push("```");
141
- lines.push(``);
142
- // Official Anthropic marketplace plugins (always available)
143
- lines.push(`### Official Anthropic plugins (always available, no marketplace add needed):`);
144
- lines.push(`These are installed via \`/plugin install <name>@claude-plugins-official\`:`);
145
- lines.push(`- **github** — GitHub integration (PRs, issues, repos)`);
146
- lines.push(`- **gitlab** — GitLab integration`);
147
- lines.push(`- **slack** — Slack messaging`);
148
- lines.push(`- **linear** — Linear project management`);
149
- lines.push(`- **notion** — Notion workspace`);
150
- lines.push(`- **sentry** — Error monitoring`);
151
- lines.push(`- **figma** — Design files`);
152
- lines.push(`- **vercel** — Deployment`);
153
- lines.push(`- **firebase** — Firebase services`);
154
- lines.push(`- **supabase** — Supabase backend`);
155
- lines.push(`- **atlassian** — Jira/Confluence`);
156
- lines.push(`- **asana** — Project management`);
190
+ lines.push(`---`);
191
+ lines.push(``);
192
+ lines.push(`### STEP 4 If no match found in any source, create a custom skill`);
193
+ lines.push(``);
194
+ lines.push(`\`\`\`bash`);
195
+ lines.push(`mkdir -p ".claude/skills/${input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}"`);
196
+ lines.push(`\`\`\``);
197
+ lines.push(`Then create SKILL.md with:`);
198
+ lines.push(`\`\`\`yaml`);
199
+ lines.push(`---`);
200
+ lines.push(`name: ${input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`);
201
+ lines.push(`description: ${input}`);
202
+ lines.push(`---`);
203
+ lines.push(``);
204
+ lines.push(`[Skill instructions here]`);
205
+ lines.push(`\`\`\``);
206
+ lines.push(``);
207
+ lines.push(`---`);
208
+ lines.push(``);
209
+ lines.push(`### Install result format`);
210
+ lines.push(`After installing, confirm:`);
211
+ lines.push(`✅ Installed: .claude/skills/<name>/SKILL.md [one line: what it does]`);
212
+ lines.push(`⏭ No match: searched [categories], created custom skill instead`);
157
213
  lines.push(``);
158
214
  return lines.join("\n");
159
215
  }
@@ -19,9 +19,11 @@ export interface SnapshotNode {
19
19
  input?: string;
20
20
  changedFiles: string[];
21
21
  summary: string;
22
+ fullSnapshot?: boolean;
22
23
  }
23
24
  export interface SnapshotTimeline {
24
25
  nodes: SnapshotNode[];
26
+ restoredTo?: string;
25
27
  }
26
28
  export interface SnapshotData {
27
29
  files: Record<string, string>;
@@ -39,15 +41,34 @@ export declare function createSnapshot(cwd: string, command: string, changedFile
39
41
  input?: string;
40
42
  summary?: string;
41
43
  }): SnapshotNode;
44
+ /**
45
+ * Build the complete file state at a given node.
46
+ *
47
+ * Full snapshots (fullSnapshot: true) store the entire project state — used directly.
48
+ * Legacy delta snapshots accumulate from node 0 to target (last-write-wins).
49
+ *
50
+ * Why the distinction matters: with delta snapshots, if a file was deleted between A→B,
51
+ * it would wrongly appear in cumulative state at B (still present from A). Full snapshots
52
+ * avoid this because the target node's data IS the complete truth at that point.
53
+ */
54
+ export declare function buildCumulativeState(cwd: string, nodeId: string, timeline: SnapshotTimeline): Record<string, string> | null;
42
55
  /**
43
56
  * Restore files from a snapshot node.
44
- * Writes stored file contents back to disk.
57
+ * Accumulates all file states from node 0 through the target node,
58
+ * then writes them to disk. This reconstructs the full project state
59
+ * at that point in time, not just the delta.
45
60
  * Does NOT delete other nodes — all nodes are preserved (like git).
46
61
  */
47
- export declare function restoreSnapshot(cwd: string, nodeId: string): {
62
+ export declare function restoreSnapshot(cwd: string, nodeId: string, timeline?: SnapshotTimeline): {
48
63
  restored: string[];
49
64
  failed: string[];
65
+ deleted: string[];
66
+ stale: string[];
50
67
  };
68
+ /**
69
+ * Record the last restored node in the timeline (for display purposes).
70
+ */
71
+ export declare function updateRestoredNode(cwd: string, nodeId: string): void;
51
72
  /**
52
73
  * Compare two snapshot nodes. Returns files that differ between them.
53
74
  */
@@ -62,10 +83,11 @@ export declare function compareSnapshots(cwd: string, nodeIdA: string, nodeIdB:
62
83
  identical: string[];
63
84
  };
64
85
  /**
65
- * Collect current file contents for snapshot.
66
- * Reads tracked files + CLI-managed files from disk.
86
+ * Collect ALL project files for snapshot — full git-like coverage.
87
+ * Respects .gitignore + hard exclusions (node_modules, .git, binaries, .env).
88
+ * The trackedPaths param is kept for API compat but ignored.
67
89
  */
68
- export declare function collectFilesForSnapshot(cwd: string, trackedPaths: string[]): Array<{
90
+ export declare function collectFilesForSnapshot(cwd: string, _trackedPaths: string[]): Array<{
69
91
  path: string;
70
92
  content: string;
71
93
  }>;