claude-setup 1.1.3 → 1.1.4

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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Marketplace intelligence — provides catalog info and decision logic
3
+ * for the add command to suggest plugins, skills, and MCP servers.
4
+ *
5
+ * Zero API calls at import time. Catalog is fetched only when needed.
6
+ */
7
+ export const MARKETPLACE_REPO = "jeremylongshore/claude-code-plugins-plus-skills";
8
+ export const MARKETPLACE_CATALOG_URL = `https://raw.githubusercontent.com/${MARKETPLACE_REPO}/main/.claude-plugin/marketplace.extended.json`;
9
+ /** The 20 skill categories in the marketplace */
10
+ export const SKILL_CATEGORIES = [
11
+ "01-code-quality", "02-testing", "03-security",
12
+ "04-devops", "05-api-development", "06-database",
13
+ "07-frontend", "08-backend", "09-mobile",
14
+ "10-data-science", "11-documentation", "12-project-management",
15
+ "13-communication", "14-research", "15-content-creation",
16
+ "16-business", "17-finance", "18-visual-content",
17
+ "19-legal", "20-productivity",
18
+ ];
19
+ /** SaaS packs available in the marketplace */
20
+ export const SAAS_PACKS = [
21
+ "Supabase", "Vercel", "OpenRouter", "GitHub", "Azure", "MongoDB",
22
+ "Playwright", "Tavily", "Stripe", "Slack", "Linear", "Notion",
23
+ ];
24
+ /** Keyword-to-category mapping for classifying user requests */
25
+ export const KEYWORD_CATEGORY_MAP = {
26
+ // Code quality
27
+ "lint": "01-code-quality", "format": "01-code-quality", "prettier": "01-code-quality",
28
+ "eslint": "01-code-quality", "code quality": "01-code-quality", "style": "01-code-quality",
29
+ // Testing
30
+ "test": "02-testing", "jest": "02-testing", "vitest": "02-testing",
31
+ "playwright": "02-testing", "cypress": "02-testing", "e2e": "02-testing",
32
+ "unit test": "02-testing", "integration test": "02-testing",
33
+ // Security
34
+ "security": "03-security", "auth": "03-security", "authentication": "03-security",
35
+ "oauth": "03-security", "jwt": "03-security", "encryption": "03-security",
36
+ // DevOps
37
+ "devops": "04-devops", "ci": "04-devops", "cd": "04-devops",
38
+ "docker": "04-devops", "kubernetes": "04-devops", "k8s": "04-devops",
39
+ "deploy": "04-devops", "infrastructure": "04-devops", "terraform": "04-devops",
40
+ // API
41
+ "api": "05-api-development", "rest": "05-api-development", "graphql": "05-api-development",
42
+ "swagger": "05-api-development", "openapi": "05-api-development",
43
+ // Database
44
+ "database": "06-database", "sql": "06-database", "postgres": "06-database",
45
+ "mysql": "06-database", "mongodb": "06-database", "redis": "06-database",
46
+ "prisma": "06-database", "orm": "06-database", "migration": "06-database",
47
+ // Frontend
48
+ "frontend": "07-frontend", "react": "07-frontend", "vue": "07-frontend",
49
+ "svelte": "07-frontend", "angular": "07-frontend", "css": "07-frontend",
50
+ "tailwind": "07-frontend", "ui": "07-frontend", "component": "07-frontend",
51
+ // Backend
52
+ "backend": "08-backend", "express": "08-backend", "fastapi": "08-backend",
53
+ "django": "08-backend", "spring": "08-backend", "nest": "08-backend",
54
+ // Mobile
55
+ "mobile": "09-mobile", "react native": "09-mobile", "flutter": "09-mobile",
56
+ "ios": "09-mobile", "android": "09-mobile", "swift": "09-mobile",
57
+ // Data science
58
+ "data": "10-data-science", "ml": "10-data-science", "machine learning": "10-data-science",
59
+ "pandas": "10-data-science", "numpy": "10-data-science", "jupyter": "10-data-science",
60
+ // Documentation
61
+ "docs": "11-documentation", "documentation": "11-documentation",
62
+ "readme": "11-documentation", "jsdoc": "11-documentation",
63
+ // Project management
64
+ "project management": "12-project-management", "agile": "12-project-management",
65
+ "sprint": "12-project-management", "kanban": "12-project-management",
66
+ };
67
+ /** Classify a user request into marketplace categories */
68
+ export function classifyRequest(input) {
69
+ const lower = input.toLowerCase();
70
+ const categories = new Set();
71
+ const saasMatches = [];
72
+ // Check keyword matches
73
+ for (const [keyword, category] of Object.entries(KEYWORD_CATEGORY_MAP)) {
74
+ if (lower.includes(keyword)) {
75
+ categories.add(category);
76
+ }
77
+ }
78
+ // Check SaaS pack matches
79
+ for (const saas of SAAS_PACKS) {
80
+ if (lower.includes(saas.toLowerCase())) {
81
+ saasMatches.push(saas);
82
+ }
83
+ }
84
+ return { categories: [...categories], saasMatches };
85
+ }
86
+ /** Generate marketplace search instructions for the add template */
87
+ export function buildMarketplaceInstructions(input) {
88
+ const { categories, saasMatches } = classifyRequest(input);
89
+ const lines = [];
90
+ lines.push(`## Marketplace intelligence`);
91
+ lines.push(``);
92
+ lines.push(`A plugin marketplace is available with 340+ plugins and 1,367+ agent skills.`);
93
+ lines.push(``);
94
+ if (categories.length > 0 || saasMatches.length > 0) {
95
+ lines.push(`### Matched categories for "${input}":`);
96
+ for (const cat of categories) {
97
+ lines.push(`- ${cat}`);
98
+ }
99
+ for (const saas of saasMatches) {
100
+ lines.push(`- SaaS pack: ${saas} (~30 skills)`);
101
+ }
102
+ lines.push(``);
103
+ }
104
+ lines.push(`### How to search and install`);
105
+ lines.push(``);
106
+ lines.push(`**Step 1 — Add 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`);
157
+ lines.push(``);
158
+ return lines.join("\n");
159
+ }
package/dist/os.js CHANGED
@@ -35,6 +35,11 @@ export const VERIFIED_MCP_PACKAGES = {
35
35
  brave: "@modelcontextprotocol/server-brave-search",
36
36
  puppeteer: "@modelcontextprotocol/server-puppeteer",
37
37
  slack: "@modelcontextprotocol/server-slack",
38
+ sqlite: "@modelcontextprotocol/server-sqlite",
39
+ stripe: "@stripe/mcp@latest",
40
+ redis: "@modelcontextprotocol/server-redis",
41
+ mysql: "@benborla29/mcp-server-mysql",
42
+ mongodb: "mcp-mongo-server",
38
43
  };
39
44
  /** MCP command format per OS — always includes -y to prevent npx install hangs */
40
45
  export function mcpCommandFormat(os, pkg) {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Snapshot time-travel system — like git commits for your project setup.
3
+ *
4
+ * Each sync creates a NODE (checkpoint) on a timeline.
5
+ * Nodes store actual content of changed files only — lightweight.
6
+ * Users can jump to any node and restore files to that state.
7
+ * Jumping does NOT delete other nodes — all are preserved.
8
+ *
9
+ * Storage: .claude/snapshots/
10
+ * timeline.json — ordered list of all nodes
11
+ * {node-id}.json — file contents for that node
12
+ *
13
+ * Zero API calls. All local filesystem operations.
14
+ */
15
+ export interface SnapshotNode {
16
+ id: string;
17
+ timestamp: string;
18
+ command: string;
19
+ input?: string;
20
+ changedFiles: string[];
21
+ summary: string;
22
+ }
23
+ export interface SnapshotTimeline {
24
+ nodes: SnapshotNode[];
25
+ }
26
+ export interface SnapshotData {
27
+ files: Record<string, string>;
28
+ }
29
+ export declare function readTimeline(cwd?: string): SnapshotTimeline;
30
+ export declare function readNodeData(cwd: string, nodeId: string): SnapshotData | null;
31
+ /**
32
+ * Create a snapshot node. Called during sync and init.
33
+ * Stores actual content of changed files only — not full project copy.
34
+ */
35
+ export declare function createSnapshot(cwd: string, command: string, changedFiles: Array<{
36
+ path: string;
37
+ content: string;
38
+ }>, opts?: {
39
+ input?: string;
40
+ summary?: string;
41
+ }): SnapshotNode;
42
+ /**
43
+ * Restore files from a snapshot node.
44
+ * Writes stored file contents back to disk.
45
+ * Does NOT delete other nodes — all nodes are preserved (like git).
46
+ */
47
+ export declare function restoreSnapshot(cwd: string, nodeId: string): {
48
+ restored: string[];
49
+ failed: string[];
50
+ };
51
+ /**
52
+ * Compare two snapshot nodes. Returns files that differ between them.
53
+ */
54
+ export declare function compareSnapshots(cwd: string, nodeIdA: string, nodeIdB: string): {
55
+ onlyInA: string[];
56
+ onlyInB: string[];
57
+ changed: Array<{
58
+ path: string;
59
+ linesA: number;
60
+ linesB: number;
61
+ }>;
62
+ identical: string[];
63
+ };
64
+ /**
65
+ * Collect current file contents for snapshot.
66
+ * Reads tracked files + CLI-managed files from disk.
67
+ */
68
+ export declare function collectFilesForSnapshot(cwd: string, trackedPaths: string[]): Array<{
69
+ path: string;
70
+ content: string;
71
+ }>;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Snapshot time-travel system — like git commits for your project setup.
3
+ *
4
+ * Each sync creates a NODE (checkpoint) on a timeline.
5
+ * Nodes store actual content of changed files only — lightweight.
6
+ * Users can jump to any node and restore files to that state.
7
+ * Jumping does NOT delete other nodes — all are preserved.
8
+ *
9
+ * Storage: .claude/snapshots/
10
+ * timeline.json — ordered list of all nodes
11
+ * {node-id}.json — file contents for that node
12
+ *
13
+ * Zero API calls. All local filesystem operations.
14
+ */
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
16
+ import { join, dirname } from "path";
17
+ import { createHash } from "crypto";
18
+ const SNAPSHOTS_DIR = ".claude/snapshots";
19
+ const TIMELINE_FILE = "timeline.json";
20
+ function snapshotsDir(cwd) {
21
+ return join(cwd, SNAPSHOTS_DIR);
22
+ }
23
+ function timelinePath(cwd) {
24
+ return join(snapshotsDir(cwd), TIMELINE_FILE);
25
+ }
26
+ function nodeDataPath(cwd, nodeId) {
27
+ return join(snapshotsDir(cwd), `${nodeId}.json`);
28
+ }
29
+ function generateNodeId() {
30
+ const now = new Date();
31
+ return now.toISOString()
32
+ .replace(/[-:]/g, "")
33
+ .replace("T", "-")
34
+ .split(".")[0];
35
+ }
36
+ export function readTimeline(cwd = process.cwd()) {
37
+ const p = timelinePath(cwd);
38
+ if (!existsSync(p))
39
+ return { nodes: [] };
40
+ try {
41
+ return JSON.parse(readFileSync(p, "utf8"));
42
+ }
43
+ catch {
44
+ return { nodes: [] };
45
+ }
46
+ }
47
+ function writeTimeline(cwd, timeline) {
48
+ const dir = snapshotsDir(cwd);
49
+ if (!existsSync(dir))
50
+ mkdirSync(dir, { recursive: true });
51
+ writeFileSync(timelinePath(cwd), JSON.stringify(timeline, null, 2), "utf8");
52
+ }
53
+ export function readNodeData(cwd, nodeId) {
54
+ const p = nodeDataPath(cwd, nodeId);
55
+ if (!existsSync(p))
56
+ return null;
57
+ try {
58
+ return JSON.parse(readFileSync(p, "utf8"));
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function writeNodeData(cwd, nodeId, data) {
65
+ const dir = snapshotsDir(cwd);
66
+ if (!existsSync(dir))
67
+ mkdirSync(dir, { recursive: true });
68
+ writeFileSync(nodeDataPath(cwd, nodeId), JSON.stringify(data, null, 2), "utf8");
69
+ }
70
+ /**
71
+ * Create a snapshot node. Called during sync and init.
72
+ * Stores actual content of changed files only — not full project copy.
73
+ */
74
+ export function createSnapshot(cwd, command, changedFiles, opts = {}) {
75
+ const timeline = readTimeline(cwd);
76
+ const nodeId = generateNodeId();
77
+ const data = { files: {} };
78
+ for (const f of changedFiles) {
79
+ data.files[f.path] = f.content;
80
+ }
81
+ const node = {
82
+ id: nodeId,
83
+ timestamp: new Date().toISOString(),
84
+ command,
85
+ ...(opts.input ? { input: opts.input } : {}),
86
+ changedFiles: changedFiles.map(f => f.path),
87
+ summary: opts.summary ?? `${changedFiles.length} file(s) captured`,
88
+ };
89
+ timeline.nodes.push(node);
90
+ writeTimeline(cwd, timeline);
91
+ writeNodeData(cwd, nodeId, data);
92
+ return node;
93
+ }
94
+ /**
95
+ * Restore files from a snapshot node.
96
+ * Writes stored file contents back to disk.
97
+ * Does NOT delete other nodes — all nodes are preserved (like git).
98
+ */
99
+ export function restoreSnapshot(cwd, nodeId) {
100
+ const data = readNodeData(cwd, nodeId);
101
+ if (!data)
102
+ return { restored: [], failed: [nodeId] };
103
+ const restored = [];
104
+ const failed = [];
105
+ for (const [filePath, content] of Object.entries(data.files)) {
106
+ const fullPath = join(cwd, filePath);
107
+ try {
108
+ const dir = dirname(fullPath);
109
+ if (!existsSync(dir))
110
+ mkdirSync(dir, { recursive: true });
111
+ writeFileSync(fullPath, content, "utf8");
112
+ restored.push(filePath);
113
+ }
114
+ catch {
115
+ failed.push(filePath);
116
+ }
117
+ }
118
+ return { restored, failed };
119
+ }
120
+ /**
121
+ * Compare two snapshot nodes. Returns files that differ between them.
122
+ */
123
+ export function compareSnapshots(cwd, nodeIdA, nodeIdB) {
124
+ const dataA = readNodeData(cwd, nodeIdA);
125
+ const dataB = readNodeData(cwd, nodeIdB);
126
+ const filesA = dataA?.files ?? {};
127
+ const filesB = dataB?.files ?? {};
128
+ const allPaths = new Set([...Object.keys(filesA), ...Object.keys(filesB)]);
129
+ const onlyInA = [];
130
+ const onlyInB = [];
131
+ const changed = [];
132
+ const identical = [];
133
+ for (const path of allPaths) {
134
+ const inA = path in filesA;
135
+ const inB = path in filesB;
136
+ if (inA && !inB) {
137
+ onlyInA.push(path);
138
+ }
139
+ else if (!inA && inB) {
140
+ onlyInB.push(path);
141
+ }
142
+ else {
143
+ const hashA = createHash("sha256").update(filesA[path]).digest("hex");
144
+ const hashB = createHash("sha256").update(filesB[path]).digest("hex");
145
+ if (hashA !== hashB) {
146
+ changed.push({
147
+ path,
148
+ linesA: filesA[path].split("\n").length,
149
+ linesB: filesB[path].split("\n").length,
150
+ });
151
+ }
152
+ else {
153
+ identical.push(path);
154
+ }
155
+ }
156
+ }
157
+ return { onlyInA, onlyInB, changed, identical };
158
+ }
159
+ /**
160
+ * Collect current file contents for snapshot.
161
+ * Reads tracked files + CLI-managed files from disk.
162
+ */
163
+ export function collectFilesForSnapshot(cwd, trackedPaths) {
164
+ const files = [];
165
+ const seen = new Set();
166
+ for (const filePath of trackedPaths) {
167
+ if (filePath === "__digest__" || filePath === ".env")
168
+ continue;
169
+ if (seen.has(filePath))
170
+ continue;
171
+ const fullPath = join(cwd, filePath);
172
+ if (!existsSync(fullPath))
173
+ continue;
174
+ try {
175
+ files.push({ path: filePath, content: readFileSync(fullPath, "utf8") });
176
+ seen.add(filePath);
177
+ }
178
+ catch { /* skip unreadable */ }
179
+ }
180
+ // Also capture CLI-managed files if not already included
181
+ const managed = ["CLAUDE.md", ".mcp.json", ".claude/settings.json"];
182
+ for (const m of managed) {
183
+ if (seen.has(m))
184
+ continue;
185
+ const fullPath = join(cwd, m);
186
+ if (!existsSync(fullPath))
187
+ continue;
188
+ try {
189
+ files.push({ path: m, content: readFileSync(fullPath, "utf8") });
190
+ seen.add(m);
191
+ }
192
+ catch { /* skip */ }
193
+ }
194
+ return files;
195
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Token cost tracking — visibility into what every command costs.
3
+ *
4
+ * Zero extra API calls. Token count is computed from content length.
5
+ * Estimates are based on ~4 chars per token approximation.
6
+ *
7
+ * Supports all pricing models:
8
+ * - Opus: $15/M input
9
+ * - Sonnet: $3/M input
10
+ * - Haiku: $0.25/M input
11
+ */
12
+ export interface CostBreakdown {
13
+ opus: number;
14
+ sonnet: number;
15
+ haiku: number;
16
+ }
17
+ export interface TokenEstimate {
18
+ inputTokens: number;
19
+ cost: CostBreakdown;
20
+ breakdown: Array<{
21
+ label: string;
22
+ tokens: number;
23
+ }>;
24
+ }
25
+ export declare function estimateTokens(content: string): number;
26
+ export declare function estimateCost(tokens: number): CostBreakdown;
27
+ export declare function formatCost(cost: CostBreakdown): string;
28
+ /**
29
+ * Build a detailed token estimate with per-section breakdown.
30
+ */
31
+ export declare function buildTokenEstimate(sections: Array<{
32
+ label: string;
33
+ content: string;
34
+ }>): TokenEstimate;
35
+ /**
36
+ * Format token estimate for display after a command.
37
+ */
38
+ export declare function formatTokenReport(estimate: TokenEstimate): string;
39
+ /**
40
+ * Generate optimization hints based on token usage patterns.
41
+ */
42
+ export declare function generateHints(runs: Array<{
43
+ command: string;
44
+ estimatedTokens?: number;
45
+ }>, currentTokens: number, budget: number): string[];
46
+ /**
47
+ * Compute cumulative stats for status dashboard.
48
+ */
49
+ export declare function computeCumulativeStats(runs: Array<{
50
+ command: string;
51
+ estimatedTokens?: number;
52
+ estimatedCost?: CostBreakdown;
53
+ }>): {
54
+ totalTokens: number;
55
+ totalCost: CostBreakdown;
56
+ avgByCommand: Record<string, number>;
57
+ runCount: number;
58
+ };
package/dist/tokens.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Token cost tracking — visibility into what every command costs.
3
+ *
4
+ * Zero extra API calls. Token count is computed from content length.
5
+ * Estimates are based on ~4 chars per token approximation.
6
+ *
7
+ * Supports all pricing models:
8
+ * - Opus: $15/M input
9
+ * - Sonnet: $3/M input
10
+ * - Haiku: $0.25/M input
11
+ */
12
+ // Pricing per million input tokens (current as of 2025)
13
+ const PRICING_PER_M_INPUT = {
14
+ opus: 15.0,
15
+ sonnet: 3.0,
16
+ haiku: 0.25,
17
+ };
18
+ export function estimateTokens(content) {
19
+ return Math.ceil(content.length / 4);
20
+ }
21
+ export function estimateCost(tokens) {
22
+ return {
23
+ opus: (tokens / 1_000_000) * PRICING_PER_M_INPUT.opus,
24
+ sonnet: (tokens / 1_000_000) * PRICING_PER_M_INPUT.sonnet,
25
+ haiku: (tokens / 1_000_000) * PRICING_PER_M_INPUT.haiku,
26
+ };
27
+ }
28
+ export function formatCost(cost) {
29
+ return `Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`;
30
+ }
31
+ /**
32
+ * Build a detailed token estimate with per-section breakdown.
33
+ */
34
+ export function buildTokenEstimate(sections) {
35
+ const breakdown = [];
36
+ let total = 0;
37
+ for (const s of sections) {
38
+ const tokens = estimateTokens(s.content);
39
+ breakdown.push({ label: s.label, tokens });
40
+ total += tokens;
41
+ }
42
+ return {
43
+ inputTokens: total,
44
+ cost: estimateCost(total),
45
+ breakdown,
46
+ };
47
+ }
48
+ /**
49
+ * Format token estimate for display after a command.
50
+ */
51
+ export function formatTokenReport(estimate) {
52
+ const lines = [];
53
+ lines.push(` Estimated: ~${estimate.inputTokens.toLocaleString()} input tokens`);
54
+ lines.push(` Cost: ${formatCost(estimate.cost)}`);
55
+ if (estimate.breakdown.length > 1) {
56
+ lines.push(` Breakdown:`);
57
+ // Sort by token count descending
58
+ const sorted = [...estimate.breakdown].sort((a, b) => b.tokens - a.tokens);
59
+ for (const item of sorted.slice(0, 5)) {
60
+ const pct = ((item.tokens / estimate.inputTokens) * 100).toFixed(0);
61
+ lines.push(` ${item.label}: ~${item.tokens.toLocaleString()} (${pct}%)`);
62
+ }
63
+ if (sorted.length > 5) {
64
+ lines.push(` ... +${sorted.length - 5} more sections`);
65
+ }
66
+ }
67
+ return lines.join("\n");
68
+ }
69
+ /**
70
+ * Generate optimization hints based on token usage patterns.
71
+ */
72
+ export function generateHints(runs, currentTokens, budget) {
73
+ const hints = [];
74
+ // Budget usage warning
75
+ const usage = (currentTokens / budget) * 100;
76
+ if (usage > 80) {
77
+ hints.push(`This command used ${usage.toFixed(0)}% of its ${budget.toLocaleString()} token budget — ` +
78
+ `consider increasing tokenBudget in .claude-setup.json or enabling digestMode`);
79
+ }
80
+ // Check for repeated zero-change syncs
81
+ const recentSyncs = runs.filter(r => r.command === "sync").slice(-3);
82
+ if (recentSyncs.length >= 3) {
83
+ const lowTokenSyncs = recentSyncs.filter(r => (r.estimatedTokens ?? 0) < 500);
84
+ if (lowTokenSyncs.length >= 3) {
85
+ hints.push("Last 3 syncs had minimal changes — consider syncing less frequently");
86
+ }
87
+ }
88
+ // Identify high-token commands
89
+ const avgByCommand = {};
90
+ for (const r of runs) {
91
+ if (!r.estimatedTokens)
92
+ continue;
93
+ if (!avgByCommand[r.command])
94
+ avgByCommand[r.command] = { total: 0, count: 0 };
95
+ avgByCommand[r.command].total += r.estimatedTokens;
96
+ avgByCommand[r.command].count++;
97
+ }
98
+ for (const [cmd, stats] of Object.entries(avgByCommand)) {
99
+ const avg = stats.total / stats.count;
100
+ if (avg > 10000 && cmd === "init") {
101
+ hints.push(`Average init uses ~${Math.round(avg).toLocaleString()} tokens — ` +
102
+ `digestMode and truncation rules in .claude-setup.json can reduce this`);
103
+ }
104
+ }
105
+ return hints;
106
+ }
107
+ /**
108
+ * Compute cumulative stats for status dashboard.
109
+ */
110
+ export function computeCumulativeStats(runs) {
111
+ let totalTokens = 0;
112
+ const totalCost = { opus: 0, sonnet: 0, haiku: 0 };
113
+ const commandTotals = {};
114
+ for (const r of runs) {
115
+ const tokens = r.estimatedTokens ?? 0;
116
+ totalTokens += tokens;
117
+ if (r.estimatedCost) {
118
+ totalCost.opus += r.estimatedCost.opus;
119
+ totalCost.sonnet += r.estimatedCost.sonnet;
120
+ totalCost.haiku += r.estimatedCost.haiku;
121
+ }
122
+ if (!commandTotals[r.command])
123
+ commandTotals[r.command] = { tokens: 0, count: 0 };
124
+ commandTotals[r.command].tokens += tokens;
125
+ commandTotals[r.command].count++;
126
+ }
127
+ const avgByCommand = {};
128
+ for (const [cmd, stats] of Object.entries(commandTotals)) {
129
+ avgByCommand[cmd] = Math.round(stats.tokens / stats.count);
130
+ }
131
+ return { totalTokens, totalCost, avgByCommand, runCount: runs.length };
132
+ }