| import { | ||
| deduplicateMessages, | ||
| extractAssistantData, | ||
| readJsonlLines | ||
| } from "../chunk-LZZNYR5Q.js"; | ||
| import { | ||
| insertSession, | ||
| insertTokenUsage, | ||
| upsertProject | ||
| } from "../chunk-DAM47DIR.js"; | ||
| import { | ||
| initHookDb | ||
| } from "../chunk-MVMBCMM6.js"; | ||
| import { | ||
| calculateCost | ||
| } from "../chunk-O5NAJLY3.js"; | ||
| // src/hooks/on-stop.ts | ||
| import { dirname, basename } from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| async function handleStop(payload, dbPath) { | ||
| if (payload.agent_id) return; | ||
| const db = initHookDb(dbPath); | ||
| try { | ||
| const lines = await readJsonlLines(payload.transcript_path); | ||
| const firstUser = lines.find((l) => l.type === "user"); | ||
| const sessionId = firstUser?.sessionId ?? basename(payload.transcript_path, ".jsonl"); | ||
| const claudeVersion = firstUser?.version ?? null; | ||
| const gitBranch = firstUser?.gitBranch ?? null; | ||
| const actualCwd = firstUser?.cwd ?? payload.cwd; | ||
| const firstLineTimestamp = lines[0]?.timestamp ?? null; | ||
| const rawAssistant = lines.filter((l) => l.type === "assistant").map(extractAssistantData).filter((d) => d !== null); | ||
| const messages = deduplicateMessages(rawAssistant); | ||
| if (messages.length === 0) return; | ||
| const dir = dirname(payload.transcript_path); | ||
| const projectPath = basename(dir); | ||
| const displayName = actualCwd.split("/").filter(Boolean).pop() ?? projectPath; | ||
| db.transaction(() => { | ||
| const projectId = upsertProject( | ||
| db, | ||
| projectPath, | ||
| displayName, | ||
| actualCwd, | ||
| messages[0]?.timestamp ?? firstLineTimestamp ?? (/* @__PURE__ */ new Date()).toISOString() | ||
| ); | ||
| const modelTokens = /* @__PURE__ */ new Map(); | ||
| let totalInput = 0; | ||
| let totalOutput = 0; | ||
| let totalCacheCreate = 0; | ||
| let totalCacheRead = 0; | ||
| let totalCost = 0; | ||
| let toolUseCount = 0; | ||
| const messageCosts = []; | ||
| for (const msg of messages) { | ||
| const cost = calculateCost(db, msg.model, { | ||
| input_tokens: msg.inputTokens, | ||
| output_tokens: msg.outputTokens, | ||
| cache_create_tokens: msg.cacheCreateTokens, | ||
| cache_read_tokens: msg.cacheReadTokens | ||
| }); | ||
| messageCosts.push(cost); | ||
| totalInput += msg.inputTokens; | ||
| totalOutput += msg.outputTokens; | ||
| totalCacheCreate += msg.cacheCreateTokens; | ||
| totalCacheRead += msg.cacheReadTokens; | ||
| totalCost += cost; | ||
| const mt = modelTokens.get(msg.model) ?? 0; | ||
| modelTokens.set(msg.model, mt + msg.outputTokens); | ||
| toolUseCount += msg.toolUses.length; | ||
| } | ||
| let primaryModel = null; | ||
| let maxOutput = 0; | ||
| for (const [model, output] of modelTokens) { | ||
| if (output > maxOutput) { | ||
| maxOutput = output; | ||
| primaryModel = model; | ||
| } | ||
| } | ||
| const agentCount = messages.reduce( | ||
| (sum, m) => sum + m.toolUses.filter((t) => t.name === "Agent").length, | ||
| 0 | ||
| ); | ||
| const startedAt = messages[0]?.timestamp ?? firstLineTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(); | ||
| const endedAt = messages[messages.length - 1]?.timestamp ?? null; | ||
| insertSession(db, { | ||
| id: sessionId, | ||
| projectId, | ||
| startedAt, | ||
| endedAt, | ||
| endReason: null, | ||
| // Only SessionEnd sets this | ||
| primaryModel, | ||
| claudeVersion, | ||
| gitBranch, | ||
| totalInputTokens: totalInput, | ||
| totalOutputTokens: totalOutput, | ||
| totalCacheCreateTokens: totalCacheCreate, | ||
| totalCacheReadTokens: totalCacheRead, | ||
| totalCostUsd: totalCost, | ||
| messageCount: messages.length, | ||
| toolUseCount, | ||
| agentCount | ||
| }); | ||
| for (let i = 0; i < messages.length; i++) { | ||
| const msg = messages[i]; | ||
| insertTokenUsage(db, { | ||
| sessionId, | ||
| messageId: msg.messageId, | ||
| model: msg.model, | ||
| inputTokens: msg.inputTokens, | ||
| outputTokens: msg.outputTokens, | ||
| cacheCreateTokens: msg.cacheCreateTokens, | ||
| cacheReadTokens: msg.cacheReadTokens, | ||
| costUsd: messageCosts[i], | ||
| timestamp: msg.timestamp | ||
| }); | ||
| } | ||
| })(); | ||
| } finally { | ||
| db.close(); | ||
| } | ||
| } | ||
| async function main() { | ||
| const chunks = []; | ||
| for await (const chunk of process.stdin) { | ||
| chunks.push(chunk); | ||
| } | ||
| const payload = JSON.parse(Buffer.concat(chunks).toString("utf-8")); | ||
| try { | ||
| await handleStop(payload); | ||
| } catch (err) { | ||
| process.stderr.write(`[codeledger] Stop hook error: ${err} | ||
| `); | ||
| } | ||
| process.exit(0); | ||
| } | ||
| if (process.argv[1] === fileURLToPath(import.meta.url)) { | ||
| main(); | ||
| } | ||
| export { | ||
| handleStop | ||
| }; | ||
| //# sourceMappingURL=on-stop.js.map |
| {"version":3,"sources":["../../src/hooks/on-stop.ts"],"sourcesContent":["import { initHookDb } from \"./init-db.js\";\nimport { readJsonlLines } from \"../parser/jsonl-reader.js\";\nimport {\n extractAssistantData,\n deduplicateMessages,\n} from \"../parser/message-extractor.js\";\nimport { calculateCost } from \"../db/pricing.js\";\nimport { upsertProject, insertSession, insertTokenUsage } from \"../db/queries.js\";\nimport { dirname, basename } from \"path\";\n\n/**\n * Lightweight real-time session tracking.\n *\n * Called on every Claude response (Stop event) DURING the session.\n * Only writes to dedup-safe tables:\n * - token_usage: INSERT OR IGNORE (UNIQUE constraint handles dedup)\n * - sessions: INSERT OR REPLACE (latest totals overwrite)\n * - projects: ON CONFLICT upsert (idempotent)\n *\n * Does NOT touch accumulation-based tables (daily_summaries, tool_calls)\n * or run classification — SessionEnd handles those at session completion.\n */\n\ninterface StopPayload {\n session_id: string;\n transcript_path: string;\n cwd: string;\n permission_mode: string;\n hook_event_name: string;\n stop_hook_active?: boolean;\n last_assistant_message?: string;\n agent_id?: string;\n agent_type?: string;\n}\n\nexport async function handleStop(\n payload: StopPayload,\n dbPath?: string,\n): Promise<void> {\n // Skip if this is a subagent Stop — SubagentStop hook handles those\n if (payload.agent_id) return;\n\n const db = initHookDb(dbPath);\n\n try {\n const lines = await readJsonlLines(payload.transcript_path);\n\n // Extract session metadata from first user line\n const firstUser = lines.find((l) => l.type === \"user\") as any;\n const sessionId = firstUser?.sessionId ?? basename(payload.transcript_path, \".jsonl\");\n const claudeVersion = firstUser?.version ?? null;\n const gitBranch = firstUser?.gitBranch ?? null;\n const actualCwd = firstUser?.cwd ?? payload.cwd;\n\n const firstLineTimestamp = (lines[0] as any)?.timestamp ?? null;\n\n // Extract and deduplicate assistant messages\n const rawAssistant = lines\n .filter((l) => l.type === \"assistant\")\n .map(extractAssistantData)\n .filter((d): d is NonNullable<typeof d> => d !== null);\n\n const messages = deduplicateMessages(rawAssistant);\n\n if (messages.length === 0) return;\n\n // Derive project path and display name\n const dir = dirname(payload.transcript_path);\n const projectPath = basename(dir);\n const displayName = actualCwd.split(\"/\").filter(Boolean).pop() ?? projectPath;\n\n db.transaction(() => {\n const projectId = upsertProject(\n db,\n projectPath,\n displayName,\n actualCwd,\n messages[0]?.timestamp ?? firstLineTimestamp ?? new Date().toISOString(),\n );\n\n // Compute aggregates\n const modelTokens = new Map<string, number>();\n let totalInput = 0;\n let totalOutput = 0;\n let totalCacheCreate = 0;\n let totalCacheRead = 0;\n let totalCost = 0;\n let toolUseCount = 0;\n const messageCosts: number[] = [];\n\n for (const msg of messages) {\n const cost = calculateCost(db, msg.model, {\n input_tokens: msg.inputTokens,\n output_tokens: msg.outputTokens,\n cache_create_tokens: msg.cacheCreateTokens,\n cache_read_tokens: msg.cacheReadTokens,\n });\n messageCosts.push(cost);\n totalInput += msg.inputTokens;\n totalOutput += msg.outputTokens;\n totalCacheCreate += msg.cacheCreateTokens;\n totalCacheRead += msg.cacheReadTokens;\n totalCost += cost;\n\n const mt = modelTokens.get(msg.model) ?? 0;\n modelTokens.set(msg.model, mt + msg.outputTokens);\n\n toolUseCount += msg.toolUses.length;\n }\n\n // Primary model\n let primaryModel: string | null = null;\n let maxOutput = 0;\n for (const [model, output] of modelTokens) {\n if (output > maxOutput) {\n maxOutput = output;\n primaryModel = model;\n }\n }\n\n const agentCount = messages.reduce(\n (sum, m) => sum + m.toolUses.filter((t) => t.name === \"Agent\").length,\n 0,\n );\n\n const startedAt = messages[0]?.timestamp ?? firstLineTimestamp ?? new Date().toISOString();\n const endedAt = messages[messages.length - 1]?.timestamp ?? null;\n\n // Upsert session — INSERT OR REPLACE ensures latest totals win\n insertSession(db, {\n id: sessionId,\n projectId,\n startedAt,\n endedAt,\n endReason: null, // Only SessionEnd sets this\n primaryModel,\n claudeVersion,\n gitBranch,\n totalInputTokens: totalInput,\n totalOutputTokens: totalOutput,\n totalCacheCreateTokens: totalCacheCreate,\n totalCacheReadTokens: totalCacheRead,\n totalCostUsd: totalCost,\n messageCount: messages.length,\n toolUseCount,\n agentCount,\n });\n\n // Insert token_usage — INSERT OR IGNORE handles dedup via UNIQUE(session_id, message_id)\n for (let i = 0; i < messages.length; i++) {\n const msg = messages[i];\n insertTokenUsage(db, {\n sessionId,\n messageId: msg.messageId,\n model: msg.model,\n inputTokens: msg.inputTokens,\n outputTokens: msg.outputTokens,\n cacheCreateTokens: msg.cacheCreateTokens,\n cacheReadTokens: msg.cacheReadTokens,\n costUsd: messageCosts[i],\n timestamp: msg.timestamp,\n });\n }\n })();\n } finally {\n db.close();\n }\n}\n\n// When run as hook handler: read JSON from stdin\nasync function main() {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const payload: StopPayload = JSON.parse(Buffer.concat(chunks).toString(\"utf-8\"));\n\n try {\n await handleStop(payload);\n } catch (err) {\n process.stderr.write(`[codeledger] Stop hook error: ${err}\\n`);\n }\n process.exit(0);\n}\n\nimport { fileURLToPath } from \"url\";\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n main();\n}\n"],"mappings":"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","names":[]} |
| { | ||
| "name": "codeledger", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Token-level cost intelligence for Claude Code", | ||
@@ -5,0 +5,0 @@ "author": { |
+1
-1
@@ -350,3 +350,3 @@ import { | ||
| name: "codeledger", | ||
| version: "0.1.0" | ||
| version: "0.2.0" | ||
| }); | ||
@@ -353,0 +353,0 @@ registerUsageSummary(server, db); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/index.ts","../src/tools/usage-summary.ts","../src/tools/project-usage.ts","../src/tools/model-stats.ts","../src/tools/agent-usage.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createConnection, getDefaultDbPath } from \"./db/connection.js\";\nimport { runMigrations } from \"./db/migrate.js\";\nimport { seedPricing } from \"./db/pricing.js\";\nimport { scanForNewSessions } from \"./sync/scanner.js\";\nimport { classifyAllSessions } from \"./classifier/categorize-session.js\";\nimport { registerUsageSummary } from \"./tools/usage-summary.js\";\nimport { registerProjectUsage } from \"./tools/project-usage.js\";\nimport { registerModelStats } from \"./tools/model-stats.js\";\nimport { registerAgentUsage } from \"./tools/agent-usage.js\";\nimport { registerSkillUsage } from \"./tools/skill-usage.js\";\nimport { registerCostOptimize } from \"./tools/cost-optimize.js\";\n\nconst SCAN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\nasync function main() {\n const dbPath = getDefaultDbPath();\n const claudeDir = process.env.CLAUDE_DATA_DIR ||\n `${process.env.HOME || process.env.USERPROFILE}/.claude`;\n\n const db = createConnection(dbPath);\n runMigrations(db);\n seedPricing(db);\n\n // Retroactively classify existing sessions\n classifyAllSessions(db);\n\n // Initial scan\n await scanForNewSessions(db, claudeDir);\n\n // Periodic scan\n setInterval(async () => {\n try {\n await scanForNewSessions(db, claudeDir);\n } catch (err) {\n process.stderr.write(`[codeledger] Scanner error: ${err}\\n`);\n }\n }, SCAN_INTERVAL_MS);\n\n // Create MCP server\n const server = new McpServer({\n name: \"codeledger\",\n version: \"0.1.0\",\n });\n\n // Register Phase A tools\n registerUsageSummary(server, db);\n registerProjectUsage(server, db);\n registerModelStats(server, db);\n\n // Register Phase B tools\n registerAgentUsage(server, db);\n registerSkillUsage(server, db);\n\n // Register Phase C tools\n registerCostOptimize(server, db);\n\n // Connect via stdio\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((err) => {\n process.stderr.write(`[codeledger] Fatal: ${err}\\n`);\n process.exit(1);\n});\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { periodToStart } from \"../utils/period.js\";\n\ninterface UsageSummary {\n totalCostUsd: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n sessionCount: number;\n topProject: string | null;\n modelDistribution: { model: string; cost: number; pct: number }[];\n overheadCostUsd: number;\n}\n\nexport function queryUsageSummary(\n db: Database.Database,\n period: string,\n project?: string\n): UsageSummary {\n const start = periodToStart(period);\n\n let sessionFilter = \"WHERE s.started_at >= ?\";\n const params: (string | number)[] = [start];\n\n if (project) {\n sessionFilter += \" AND p.display_name = ?\";\n params.push(project);\n }\n\n const totals = db\n .prepare(\n `\n SELECT\n COALESCE(SUM(s.total_cost_usd), 0) as cost,\n COALESCE(SUM(s.total_input_tokens), 0) as input_tok,\n COALESCE(SUM(s.total_output_tokens), 0) as output_tok,\n COUNT(*) as session_count\n FROM sessions s\n LEFT JOIN projects p ON s.project_id = p.id\n ${sessionFilter}\n `\n )\n .get(...params) as {\n cost: number;\n input_tok: number;\n output_tok: number;\n session_count: number;\n } | undefined;\n\n const topProj = db\n .prepare(\n `\n SELECT p.display_name, SUM(s.total_cost_usd) as cost\n FROM sessions s\n JOIN projects p ON s.project_id = p.id\n ${sessionFilter}\n GROUP BY p.id ORDER BY cost DESC LIMIT 1\n `\n )\n .get(...params) as { display_name: string; cost: number } | undefined;\n\n const models = db\n .prepare(\n `\n SELECT s.primary_model as model, SUM(s.total_cost_usd) as cost\n FROM sessions s\n LEFT JOIN projects p ON s.project_id = p.id\n ${sessionFilter}\n GROUP BY s.primary_model ORDER BY cost DESC\n `\n )\n .all(...params) as { model: string | null; cost: number }[];\n\n let agentFilter = \"WHERE a.started_at >= ?\";\n const agentParams: (string | number)[] = [start];\n if (project) {\n agentFilter += \" AND p.display_name = ?\";\n agentParams.push(project);\n }\n\n const overheadRow = db\n .prepare(\n `\n SELECT COALESCE(SUM(a.total_cost_usd), 0) as overhead_cost\n FROM agents a\n JOIN sessions s ON a.session_id = s.id\n LEFT JOIN projects p ON s.project_id = p.id\n ${agentFilter}\n AND a.source_category = 'overhead'\n `\n )\n .get(...agentParams) as { overhead_cost: number } | undefined;\n\n const totalCost = totals?.cost ?? 0;\n const modelDistribution = models.map((m) => ({\n model: m.model ?? \"unknown\",\n cost: m.cost,\n pct: totalCost > 0 ? Math.round((m.cost / totalCost) * 100) : 0,\n }));\n\n return {\n totalCostUsd: totalCost,\n totalInputTokens: totals?.input_tok ?? 0,\n totalOutputTokens: totals?.output_tok ?? 0,\n sessionCount: totals?.session_count ?? 0,\n topProject: topProj?.display_name ?? null,\n modelDistribution,\n overheadCostUsd: overheadRow?.overhead_cost ?? 0,\n };\n}\n\nexport function registerUsageSummary(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"usage_summary\",\n \"Get a smart summary of Claude Code usage and costs\",\n {\n period: z\n .enum([\"today\", \"week\", \"month\", \"all\"])\n .default(\"today\"),\n project: z.string().optional().describe(\"Filter by project name\"),\n },\n async ({ period, project }) => {\n const s = queryUsageSummary(db, period, project);\n\n const lines = [\n `## Usage Summary (${period})`,\n `**Total Cost:** $${s.totalCostUsd.toFixed(2)}`,\n `**Sessions:** ${s.sessionCount}`,\n `**Tokens:** ${s.totalInputTokens.toLocaleString()} input, ${s.totalOutputTokens.toLocaleString()} output`,\n s.topProject ? `**Top Project:** ${s.topProject}` : \"\",\n s.overheadCostUsd > 0 ? `**Background overhead:** $${s.overheadCostUsd.toFixed(2)} (plugin observers, system agents)` : \"\",\n \"\",\n \"**Model Distribution:**\",\n ...s.modelDistribution.map(\n (m) => `- ${m.model}: $${m.cost.toFixed(2)} (${m.pct}%)`\n ),\n ];\n\n return {\n content: [{ type: \"text\" as const, text: lines.filter(Boolean).join(\"\\n\") }],\n };\n }\n );\n}\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { periodToStart } from \"../utils/period.js\";\n\nexport function queryProjectUsage(\n db: Database.Database,\n period: string,\n sortBy: string,\n limit: number\n): any[] {\n const start = periodToStart(period);\n\n const orderCol =\n sortBy === \"tokens\"\n ? \"total_tokens\"\n : sortBy === \"sessions\"\n ? \"session_count\"\n : \"total_cost\";\n\n return db\n .prepare(\n `\n SELECT\n p.display_name as project,\n SUM(s.total_input_tokens + s.total_output_tokens) as total_tokens,\n SUM(s.total_cost_usd) as total_cost,\n COUNT(*) as session_count,\n MAX(s.started_at) as last_active\n FROM sessions s\n JOIN projects p ON s.project_id = p.id\n WHERE s.started_at >= ?\n GROUP BY p.id\n ORDER BY ${orderCol} DESC\n LIMIT ?\n `\n )\n .all(start, limit) as any[];\n}\n\nexport function registerProjectUsage(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"project_usage\",\n \"Get token usage and costs broken down by project\",\n {\n period: z\n .enum([\"today\", \"week\", \"month\", \"all\"])\n .default(\"week\"),\n sort_by: z\n .enum([\"cost\", \"tokens\", \"sessions\"])\n .default(\"cost\"),\n limit: z.number().default(10),\n },\n async ({ period, sort_by, limit }) => {\n const projects = queryProjectUsage(db, period, sort_by, limit);\n const lines = [\n `## Project Usage (${period}, top ${limit} by ${sort_by})`,\n \"\",\n \"| Project | Cost | Tokens | Sessions | Last Active |\",\n \"|---------|------|--------|----------|-------------|\",\n ...projects.map(\n (p: any) =>\n `| ${p.project} | $${p.total_cost.toFixed(2)} | ${Number(p.total_tokens).toLocaleString()} | ${p.session_count} | ${p.last_active?.split(\"T\")[0] ?? \"\\u2014\"} |`\n ),\n ];\n if (projects.length === 0) lines.push(\"No project data for this period.\");\n return { content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }] };\n }\n );\n}\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { lookupPricing } from \"../db/pricing.js\";\nimport { periodToStart } from \"../utils/period.js\";\n\nexport interface ModelStatsRow {\n model: string;\n totalCost: number;\n totalInput: number;\n totalOutput: number;\n messageCount: number;\n pct: number;\n hypotheticalSonnetCost: number;\n potentialSavings: number;\n}\n\nexport function queryModelStats(\n db: Database.Database,\n period: string\n): ModelStatsRow[] {\n const start = periodToStart(period);\n\n const rows = db\n .prepare(\n `\n SELECT\n model,\n SUM(input_tokens) as total_input,\n SUM(output_tokens) as total_output,\n SUM(cost_usd) as total_cost,\n COUNT(*) as message_count\n FROM token_usage\n WHERE timestamp >= ?\n GROUP BY model\n ORDER BY total_cost DESC\n `\n )\n .all(start) as Array<{\n model: string;\n total_input: number;\n total_output: number;\n total_cost: number;\n message_count: number;\n }>;\n\n const totalCost = rows.reduce((sum, r) => sum + r.total_cost, 0);\n\n const sonnetPricing = lookupPricing(db, \"claude-sonnet-4-5\");\n\n return rows.map((r) => {\n let hypotheticalCost = r.total_cost;\n if (\n sonnetPricing &&\n !r.model.includes(\"sonnet\") &&\n !r.model.includes(\"haiku\")\n ) {\n hypotheticalCost =\n (r.total_input * sonnetPricing.input_per_mtok) / 1_000_000 +\n (r.total_output * sonnetPricing.output_per_mtok) / 1_000_000;\n }\n return {\n model: r.model,\n totalCost: r.total_cost,\n totalInput: r.total_input,\n totalOutput: r.total_output,\n messageCount: r.message_count,\n pct: totalCost > 0 ? Math.round((r.total_cost / totalCost) * 100) : 0,\n hypotheticalSonnetCost: hypotheticalCost,\n potentialSavings: Math.max(0, r.total_cost - hypotheticalCost),\n };\n });\n}\n\nexport function registerModelStats(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"model_stats\",\n \"Analyze which models are being used and potential cost savings\",\n {\n period: z.enum([\"today\", \"week\", \"month\", \"all\"]).default(\"month\"),\n },\n async ({ period }) => {\n const stats = queryModelStats(db, period);\n const totalSavings = stats.reduce((s, m) => s + m.potentialSavings, 0);\n\n const lines = [\n `## Model Usage (${period})`,\n \"\",\n \"| Model | Cost | % | Messages | If Sonnet | Savings |\",\n \"|-------|------|---|----------|-----------|---------|\",\n ...stats.map(\n (m) =>\n `| ${m.model} | $${m.totalCost.toFixed(2)} | ${m.pct}% | ${m.messageCount} | $${m.hypotheticalSonnetCost.toFixed(2)} | $${m.potentialSavings.toFixed(2)} |`\n ),\n \"\",\n totalSavings > 0\n ? `**Potential savings if non-Opus tasks used Sonnet: $${totalSavings.toFixed(2)}**`\n : \"Model usage looks optimized.\",\n ];\n return { content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }] };\n }\n );\n}\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { periodToStart } from \"../utils/period.js\";\n\ninterface AgentUsageRow {\n agent_id: string;\n session_id: string;\n agent_type: string | null;\n description: string | null;\n model: string | null;\n total_input_tokens: number;\n total_output_tokens: number;\n total_cache_create_tokens: number;\n total_cache_read_tokens: number;\n total_cost_usd: number;\n started_at: string | null;\n ended_at: string | null;\n message_count: number;\n source_category: string;\n project: string;\n}\n\nexport function queryAgentUsage(\n db: Database.Database,\n period: string,\n filters?: { sessionId?: string; project?: string; sourceCategory?: string }\n): AgentUsageRow[] {\n const start = periodToStart(period);\n\n let whereClause = \"WHERE a.started_at >= ?\";\n const params: (string | number)[] = [start];\n\n if (filters?.sessionId) {\n whereClause += \" AND a.session_id = ?\";\n params.push(filters.sessionId);\n }\n\n if (filters?.project) {\n whereClause += \" AND p.display_name = ?\";\n params.push(filters.project);\n }\n\n if (filters?.sourceCategory) {\n whereClause += \" AND a.source_category = ?\";\n params.push(filters.sourceCategory);\n }\n\n return db\n .prepare(\n `\n SELECT\n a.id as agent_id,\n a.session_id,\n a.agent_type,\n a.description,\n a.model,\n a.total_input_tokens,\n a.total_output_tokens,\n a.total_cache_create_tokens,\n a.total_cache_read_tokens,\n a.total_cost_usd,\n a.started_at,\n a.ended_at,\n a.message_count,\n a.source_category,\n p.display_name as project\n FROM agents a\n JOIN sessions s ON a.session_id = s.id\n JOIN projects p ON s.project_id = p.id\n ${whereClause}\n ORDER BY a.total_cost_usd DESC\n `\n )\n .all(...params) as AgentUsageRow[];\n}\n\nexport function registerAgentUsage(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"agent_usage\",\n \"Get token usage broken down by subagent\",\n {\n session_id: z.string().optional().describe(\"Filter by session\"),\n project: z.string().optional().describe(\"Filter by project\"),\n period: z\n .enum([\"today\", \"week\", \"month\", \"all\"])\n .default(\"week\"),\n source_category: z\n .enum([\"user\", \"overhead\", \"all\"])\n .default(\"all\")\n .describe(\"Filter: 'user' for coding work, 'overhead' for background agents\"),\n },\n async ({ session_id, project, period, source_category }) => {\n const agents = queryAgentUsage(db, period, {\n sessionId: session_id,\n project,\n sourceCategory: source_category === \"all\" ? undefined : source_category,\n });\n\n if (agents.length === 0) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `No agent usage data found for period: ${period}`,\n },\n ],\n };\n }\n\n const totalCost = agents.reduce((sum, a) => sum + a.total_cost_usd, 0);\n const userCost = agents.filter(a => a.source_category === \"user\").reduce((s, a) => s + a.total_cost_usd, 0);\n const overheadCost = agents.filter(a => a.source_category === \"overhead\").reduce((s, a) => s + a.total_cost_usd, 0);\n\n const lines = [\n `## Agent Usage (${period})`,\n `**Total agents:** ${agents.length} | **Total cost:** $${totalCost.toFixed(2)}`,\n `**Your coding agents:** $${userCost.toFixed(2)} | **Background overhead:** $${overheadCost.toFixed(2)}`,\n \"\",\n \"| Agent | Type | Model | Tokens (in/out) | Cost | Messages | Session | Project |\",\n \"|-------|------|-------|-----------------|------|----------|---------|---------|\",\n ...agents.map((a) => {\n const tokens = `${Number(a.total_input_tokens).toLocaleString()}/${Number(a.total_output_tokens).toLocaleString()}`;\n return `| ${a.agent_id} | ${a.agent_type ?? \"\\u2014\"} | ${a.model ?? \"\\u2014\"} | ${tokens} | $${a.total_cost_usd.toFixed(2)} | ${a.message_count} | ${a.session_id.slice(0, 8)}... | ${a.project} |`;\n }),\n ];\n\n return {\n content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }],\n };\n }\n );\n}\n"],"mappings":"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","names":["z","z","z","z","z","z"]} | ||
| {"version":3,"sources":["../src/index.ts","../src/tools/usage-summary.ts","../src/tools/project-usage.ts","../src/tools/model-stats.ts","../src/tools/agent-usage.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createConnection, getDefaultDbPath } from \"./db/connection.js\";\nimport { runMigrations } from \"./db/migrate.js\";\nimport { seedPricing } from \"./db/pricing.js\";\nimport { scanForNewSessions } from \"./sync/scanner.js\";\nimport { classifyAllSessions } from \"./classifier/categorize-session.js\";\nimport { registerUsageSummary } from \"./tools/usage-summary.js\";\nimport { registerProjectUsage } from \"./tools/project-usage.js\";\nimport { registerModelStats } from \"./tools/model-stats.js\";\nimport { registerAgentUsage } from \"./tools/agent-usage.js\";\nimport { registerSkillUsage } from \"./tools/skill-usage.js\";\nimport { registerCostOptimize } from \"./tools/cost-optimize.js\";\n\nconst SCAN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\nasync function main() {\n const dbPath = getDefaultDbPath();\n const claudeDir = process.env.CLAUDE_DATA_DIR ||\n `${process.env.HOME || process.env.USERPROFILE}/.claude`;\n\n const db = createConnection(dbPath);\n runMigrations(db);\n seedPricing(db);\n\n // Retroactively classify existing sessions\n classifyAllSessions(db);\n\n // Initial scan\n await scanForNewSessions(db, claudeDir);\n\n // Periodic scan\n setInterval(async () => {\n try {\n await scanForNewSessions(db, claudeDir);\n } catch (err) {\n process.stderr.write(`[codeledger] Scanner error: ${err}\\n`);\n }\n }, SCAN_INTERVAL_MS);\n\n // Create MCP server\n const server = new McpServer({\n name: \"codeledger\",\n version: \"0.2.0\",\n });\n\n // Register Phase A tools\n registerUsageSummary(server, db);\n registerProjectUsage(server, db);\n registerModelStats(server, db);\n\n // Register Phase B tools\n registerAgentUsage(server, db);\n registerSkillUsage(server, db);\n\n // Register Phase C tools\n registerCostOptimize(server, db);\n\n // Connect via stdio\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((err) => {\n process.stderr.write(`[codeledger] Fatal: ${err}\\n`);\n process.exit(1);\n});\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { periodToStart } from \"../utils/period.js\";\n\ninterface UsageSummary {\n totalCostUsd: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n sessionCount: number;\n topProject: string | null;\n modelDistribution: { model: string; cost: number; pct: number }[];\n overheadCostUsd: number;\n}\n\nexport function queryUsageSummary(\n db: Database.Database,\n period: string,\n project?: string\n): UsageSummary {\n const start = periodToStart(period);\n\n let sessionFilter = \"WHERE s.started_at >= ?\";\n const params: (string | number)[] = [start];\n\n if (project) {\n sessionFilter += \" AND p.display_name = ?\";\n params.push(project);\n }\n\n const totals = db\n .prepare(\n `\n SELECT\n COALESCE(SUM(s.total_cost_usd), 0) as cost,\n COALESCE(SUM(s.total_input_tokens), 0) as input_tok,\n COALESCE(SUM(s.total_output_tokens), 0) as output_tok,\n COUNT(*) as session_count\n FROM sessions s\n LEFT JOIN projects p ON s.project_id = p.id\n ${sessionFilter}\n `\n )\n .get(...params) as {\n cost: number;\n input_tok: number;\n output_tok: number;\n session_count: number;\n } | undefined;\n\n const topProj = db\n .prepare(\n `\n SELECT p.display_name, SUM(s.total_cost_usd) as cost\n FROM sessions s\n JOIN projects p ON s.project_id = p.id\n ${sessionFilter}\n GROUP BY p.id ORDER BY cost DESC LIMIT 1\n `\n )\n .get(...params) as { display_name: string; cost: number } | undefined;\n\n const models = db\n .prepare(\n `\n SELECT s.primary_model as model, SUM(s.total_cost_usd) as cost\n FROM sessions s\n LEFT JOIN projects p ON s.project_id = p.id\n ${sessionFilter}\n GROUP BY s.primary_model ORDER BY cost DESC\n `\n )\n .all(...params) as { model: string | null; cost: number }[];\n\n let agentFilter = \"WHERE a.started_at >= ?\";\n const agentParams: (string | number)[] = [start];\n if (project) {\n agentFilter += \" AND p.display_name = ?\";\n agentParams.push(project);\n }\n\n const overheadRow = db\n .prepare(\n `\n SELECT COALESCE(SUM(a.total_cost_usd), 0) as overhead_cost\n FROM agents a\n JOIN sessions s ON a.session_id = s.id\n LEFT JOIN projects p ON s.project_id = p.id\n ${agentFilter}\n AND a.source_category = 'overhead'\n `\n )\n .get(...agentParams) as { overhead_cost: number } | undefined;\n\n const totalCost = totals?.cost ?? 0;\n const modelDistribution = models.map((m) => ({\n model: m.model ?? \"unknown\",\n cost: m.cost,\n pct: totalCost > 0 ? Math.round((m.cost / totalCost) * 100) : 0,\n }));\n\n return {\n totalCostUsd: totalCost,\n totalInputTokens: totals?.input_tok ?? 0,\n totalOutputTokens: totals?.output_tok ?? 0,\n sessionCount: totals?.session_count ?? 0,\n topProject: topProj?.display_name ?? null,\n modelDistribution,\n overheadCostUsd: overheadRow?.overhead_cost ?? 0,\n };\n}\n\nexport function registerUsageSummary(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"usage_summary\",\n \"Get a smart summary of Claude Code usage and costs\",\n {\n period: z\n .enum([\"today\", \"week\", \"month\", \"all\"])\n .default(\"today\"),\n project: z.string().optional().describe(\"Filter by project name\"),\n },\n async ({ period, project }) => {\n const s = queryUsageSummary(db, period, project);\n\n const lines = [\n `## Usage Summary (${period})`,\n `**Total Cost:** $${s.totalCostUsd.toFixed(2)}`,\n `**Sessions:** ${s.sessionCount}`,\n `**Tokens:** ${s.totalInputTokens.toLocaleString()} input, ${s.totalOutputTokens.toLocaleString()} output`,\n s.topProject ? `**Top Project:** ${s.topProject}` : \"\",\n s.overheadCostUsd > 0 ? `**Background overhead:** $${s.overheadCostUsd.toFixed(2)} (plugin observers, system agents)` : \"\",\n \"\",\n \"**Model Distribution:**\",\n ...s.modelDistribution.map(\n (m) => `- ${m.model}: $${m.cost.toFixed(2)} (${m.pct}%)`\n ),\n ];\n\n return {\n content: [{ type: \"text\" as const, text: lines.filter(Boolean).join(\"\\n\") }],\n };\n }\n );\n}\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { periodToStart } from \"../utils/period.js\";\n\nexport function queryProjectUsage(\n db: Database.Database,\n period: string,\n sortBy: string,\n limit: number\n): any[] {\n const start = periodToStart(period);\n\n const orderCol =\n sortBy === \"tokens\"\n ? \"total_tokens\"\n : sortBy === \"sessions\"\n ? \"session_count\"\n : \"total_cost\";\n\n return db\n .prepare(\n `\n SELECT\n p.display_name as project,\n SUM(s.total_input_tokens + s.total_output_tokens) as total_tokens,\n SUM(s.total_cost_usd) as total_cost,\n COUNT(*) as session_count,\n MAX(s.started_at) as last_active\n FROM sessions s\n JOIN projects p ON s.project_id = p.id\n WHERE s.started_at >= ?\n GROUP BY p.id\n ORDER BY ${orderCol} DESC\n LIMIT ?\n `\n )\n .all(start, limit) as any[];\n}\n\nexport function registerProjectUsage(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"project_usage\",\n \"Get token usage and costs broken down by project\",\n {\n period: z\n .enum([\"today\", \"week\", \"month\", \"all\"])\n .default(\"week\"),\n sort_by: z\n .enum([\"cost\", \"tokens\", \"sessions\"])\n .default(\"cost\"),\n limit: z.number().default(10),\n },\n async ({ period, sort_by, limit }) => {\n const projects = queryProjectUsage(db, period, sort_by, limit);\n const lines = [\n `## Project Usage (${period}, top ${limit} by ${sort_by})`,\n \"\",\n \"| Project | Cost | Tokens | Sessions | Last Active |\",\n \"|---------|------|--------|----------|-------------|\",\n ...projects.map(\n (p: any) =>\n `| ${p.project} | $${p.total_cost.toFixed(2)} | ${Number(p.total_tokens).toLocaleString()} | ${p.session_count} | ${p.last_active?.split(\"T\")[0] ?? \"\\u2014\"} |`\n ),\n ];\n if (projects.length === 0) lines.push(\"No project data for this period.\");\n return { content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }] };\n }\n );\n}\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { lookupPricing } from \"../db/pricing.js\";\nimport { periodToStart } from \"../utils/period.js\";\n\nexport interface ModelStatsRow {\n model: string;\n totalCost: number;\n totalInput: number;\n totalOutput: number;\n messageCount: number;\n pct: number;\n hypotheticalSonnetCost: number;\n potentialSavings: number;\n}\n\nexport function queryModelStats(\n db: Database.Database,\n period: string\n): ModelStatsRow[] {\n const start = periodToStart(period);\n\n const rows = db\n .prepare(\n `\n SELECT\n model,\n SUM(input_tokens) as total_input,\n SUM(output_tokens) as total_output,\n SUM(cost_usd) as total_cost,\n COUNT(*) as message_count\n FROM token_usage\n WHERE timestamp >= ?\n GROUP BY model\n ORDER BY total_cost DESC\n `\n )\n .all(start) as Array<{\n model: string;\n total_input: number;\n total_output: number;\n total_cost: number;\n message_count: number;\n }>;\n\n const totalCost = rows.reduce((sum, r) => sum + r.total_cost, 0);\n\n const sonnetPricing = lookupPricing(db, \"claude-sonnet-4-5\");\n\n return rows.map((r) => {\n let hypotheticalCost = r.total_cost;\n if (\n sonnetPricing &&\n !r.model.includes(\"sonnet\") &&\n !r.model.includes(\"haiku\")\n ) {\n hypotheticalCost =\n (r.total_input * sonnetPricing.input_per_mtok) / 1_000_000 +\n (r.total_output * sonnetPricing.output_per_mtok) / 1_000_000;\n }\n return {\n model: r.model,\n totalCost: r.total_cost,\n totalInput: r.total_input,\n totalOutput: r.total_output,\n messageCount: r.message_count,\n pct: totalCost > 0 ? Math.round((r.total_cost / totalCost) * 100) : 0,\n hypotheticalSonnetCost: hypotheticalCost,\n potentialSavings: Math.max(0, r.total_cost - hypotheticalCost),\n };\n });\n}\n\nexport function registerModelStats(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"model_stats\",\n \"Analyze which models are being used and potential cost savings\",\n {\n period: z.enum([\"today\", \"week\", \"month\", \"all\"]).default(\"month\"),\n },\n async ({ period }) => {\n const stats = queryModelStats(db, period);\n const totalSavings = stats.reduce((s, m) => s + m.potentialSavings, 0);\n\n const lines = [\n `## Model Usage (${period})`,\n \"\",\n \"| Model | Cost | % | Messages | If Sonnet | Savings |\",\n \"|-------|------|---|----------|-----------|---------|\",\n ...stats.map(\n (m) =>\n `| ${m.model} | $${m.totalCost.toFixed(2)} | ${m.pct}% | ${m.messageCount} | $${m.hypotheticalSonnetCost.toFixed(2)} | $${m.potentialSavings.toFixed(2)} |`\n ),\n \"\",\n totalSavings > 0\n ? `**Potential savings if non-Opus tasks used Sonnet: $${totalSavings.toFixed(2)}**`\n : \"Model usage looks optimized.\",\n ];\n return { content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }] };\n }\n );\n}\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type Database from \"better-sqlite3\";\nimport { z } from \"zod\";\nimport { periodToStart } from \"../utils/period.js\";\n\ninterface AgentUsageRow {\n agent_id: string;\n session_id: string;\n agent_type: string | null;\n description: string | null;\n model: string | null;\n total_input_tokens: number;\n total_output_tokens: number;\n total_cache_create_tokens: number;\n total_cache_read_tokens: number;\n total_cost_usd: number;\n started_at: string | null;\n ended_at: string | null;\n message_count: number;\n source_category: string;\n project: string;\n}\n\nexport function queryAgentUsage(\n db: Database.Database,\n period: string,\n filters?: { sessionId?: string; project?: string; sourceCategory?: string }\n): AgentUsageRow[] {\n const start = periodToStart(period);\n\n let whereClause = \"WHERE a.started_at >= ?\";\n const params: (string | number)[] = [start];\n\n if (filters?.sessionId) {\n whereClause += \" AND a.session_id = ?\";\n params.push(filters.sessionId);\n }\n\n if (filters?.project) {\n whereClause += \" AND p.display_name = ?\";\n params.push(filters.project);\n }\n\n if (filters?.sourceCategory) {\n whereClause += \" AND a.source_category = ?\";\n params.push(filters.sourceCategory);\n }\n\n return db\n .prepare(\n `\n SELECT\n a.id as agent_id,\n a.session_id,\n a.agent_type,\n a.description,\n a.model,\n a.total_input_tokens,\n a.total_output_tokens,\n a.total_cache_create_tokens,\n a.total_cache_read_tokens,\n a.total_cost_usd,\n a.started_at,\n a.ended_at,\n a.message_count,\n a.source_category,\n p.display_name as project\n FROM agents a\n JOIN sessions s ON a.session_id = s.id\n JOIN projects p ON s.project_id = p.id\n ${whereClause}\n ORDER BY a.total_cost_usd DESC\n `\n )\n .all(...params) as AgentUsageRow[];\n}\n\nexport function registerAgentUsage(\n server: McpServer,\n db: Database.Database\n): void {\n server.tool(\n \"agent_usage\",\n \"Get token usage broken down by subagent\",\n {\n session_id: z.string().optional().describe(\"Filter by session\"),\n project: z.string().optional().describe(\"Filter by project\"),\n period: z\n .enum([\"today\", \"week\", \"month\", \"all\"])\n .default(\"week\"),\n source_category: z\n .enum([\"user\", \"overhead\", \"all\"])\n .default(\"all\")\n .describe(\"Filter: 'user' for coding work, 'overhead' for background agents\"),\n },\n async ({ session_id, project, period, source_category }) => {\n const agents = queryAgentUsage(db, period, {\n sessionId: session_id,\n project,\n sourceCategory: source_category === \"all\" ? undefined : source_category,\n });\n\n if (agents.length === 0) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `No agent usage data found for period: ${period}`,\n },\n ],\n };\n }\n\n const totalCost = agents.reduce((sum, a) => sum + a.total_cost_usd, 0);\n const userCost = agents.filter(a => a.source_category === \"user\").reduce((s, a) => s + a.total_cost_usd, 0);\n const overheadCost = agents.filter(a => a.source_category === \"overhead\").reduce((s, a) => s + a.total_cost_usd, 0);\n\n const lines = [\n `## Agent Usage (${period})`,\n `**Total agents:** ${agents.length} | **Total cost:** $${totalCost.toFixed(2)}`,\n `**Your coding agents:** $${userCost.toFixed(2)} | **Background overhead:** $${overheadCost.toFixed(2)}`,\n \"\",\n \"| Agent | Type | Model | Tokens (in/out) | Cost | Messages | Session | Project |\",\n \"|-------|------|-------|-----------------|------|----------|---------|---------|\",\n ...agents.map((a) => {\n const tokens = `${Number(a.total_input_tokens).toLocaleString()}/${Number(a.total_output_tokens).toLocaleString()}`;\n return `| ${a.agent_id} | ${a.agent_type ?? \"\\u2014\"} | ${a.model ?? \"\\u2014\"} | ${tokens} | $${a.total_cost_usd.toFixed(2)} | ${a.message_count} | ${a.session_id.slice(0, 8)}... | ${a.project} |`;\n }),\n ];\n\n return {\n content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }],\n };\n }\n );\n}\n"],"mappings":"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","names":["z","z","z","z","z","z"]} |
+11
-0
@@ -27,2 +27,13 @@ { | ||
| ], | ||
| "Stop": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node ${CLAUDE_PLUGIN_ROOT}/dist/hooks/on-stop.js", | ||
| "timeout": 5 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "SessionEnd": [ | ||
@@ -29,0 +40,0 @@ { |
+1
-1
| { | ||
| "name": "codeledger", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Token-level cost intelligence for Claude Code — per-project, per-agent, per-skill usage tracking with conversational MCP querying and local dashboard", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+40
-7
@@ -39,14 +39,29 @@ # CodeLedger | ||
| ### Install as Claude Code plugin | ||
| ### Install from npm | ||
| ```bash | ||
| # Add the MCP server | ||
| claude mcp add codeledger-server node -- $(npm root -g)/codeledger/dist/index.js | ||
| npm install -g codeledger | ||
| ``` | ||
| # Or install from source | ||
| git clone https://github.com/bhvbhushan/codeledger.git | ||
| cd codeledger && npm install && npm run build | ||
| claude mcp add codeledger-server node -- $(pwd)/dist/index.js | ||
| ### Run Claude Code with the plugin | ||
| **macOS / Linux:** | ||
| ```bash | ||
| claude --plugin-dir $(npm root -g)/codeledger | ||
| ``` | ||
| **Windows (PowerShell):** | ||
| ```powershell | ||
| claude --plugin-dir "$(npm root -g)\codeledger" | ||
| ``` | ||
| **Windows (CMD):** | ||
| ```cmd | ||
| for /f "delims=" %i in ('npm root -g') do claude --plugin-dir "%i\codeledger" | ||
| ``` | ||
| This loads **everything** — 6 MCP tools, 4 hooks (real-time tracking), and slash commands. | ||
| > **Plugin marketplace:** CodeLedger has been submitted to the official Claude Code plugin directory. Once approved, installation will be simply `/plugin install codeledger`. | ||
| ### Start the dashboard | ||
@@ -60,2 +75,10 @@ | ||
| ### Install from source (development) | ||
| ```bash | ||
| git clone https://github.com/bhvbhushan/codeledger.git | ||
| cd codeledger && npm install && npm run build | ||
| claude --plugin-dir . | ||
| ``` | ||
| ## Features | ||
@@ -191,4 +214,14 @@ | ||
| ## Contributing | ||
| Contributions are welcome! Please read the [Contributing Guidelines](./CONTRIBUTING.md) before submitting a PR. | ||
| - **Bug reports** and **feature requests** — use [Issue Templates](https://github.com/bhvbhushan/codeledger/issues/new/choose) | ||
| - **Questions** — use [GitHub Discussions](https://github.com/bhvbhushan/codeledger/discussions) | ||
| - **Security vulnerabilities** — see [Security Policy](./SECURITY.md) | ||
| All contributors are expected to follow the [Code of Conduct](./CODE_OF_CONDUCT.md). | ||
| ## License | ||
| [MIT](./LICENSE) |
| <claude-mem-context> | ||
| # Recent Activity | ||
| <!-- This section is auto-generated by claude-mem. Edit content outside the tags. --> | ||
| *No recent activity* | ||
| </claude-mem-context> |
| <claude-mem-context> | ||
| # Recent Activity | ||
| <!-- This section is auto-generated by claude-mem. Edit content outside the tags. --> | ||
| *No recent activity* | ||
| </claude-mem-context> |
| <claude-mem-context> | ||
| # Recent Activity | ||
| <!-- This section is auto-generated by claude-mem. Edit content outside the tags. --> | ||
| *No recent activity* | ||
| </claude-mem-context> |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
259134
5.82%2647
6.09%225
17.19%36
-2.7%