@statforge/claudestat 1.3.0 → 1.5.0

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,300 @@
1
+ "use strict";
2
+ /**
3
+ * claude-code.ts — WatcherAdapter para Claude Code
4
+ *
5
+ * Claude Code escribe trazas JSONL en ~/.claude/projects/{hash}/{session-id}.jsonl
6
+ * Cada línea "assistant" contiene usage tokens y modelo.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.claudeCodeAdapter = void 0;
13
+ exports.getContextWindow = getContextWindow;
14
+ exports.getAllBlockCostsForSession = getAllBlockCostsForSession;
15
+ exports.getSessionPrompts = getSessionPrompts;
16
+ const path_1 = __importDefault(require("path"));
17
+ const promises_1 = __importDefault(require("fs/promises"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const adapter_1 = require("./adapter");
20
+ const paths_1 = require("../paths");
21
+ const pricing_1 = require("../pricing");
22
+ const PROJECTS_DIR = path_1.default.join((0, paths_1.getClaudeDir)(), 'projects');
23
+ const KNOWN_CONTEXT_WINDOWS = {
24
+ 'claude-opus-4-6': 200000,
25
+ 'claude-sonnet-4-6': 200000,
26
+ 'claude-haiku-4-5': 200000,
27
+ };
28
+ function getContextWindow(model) {
29
+ return KNOWN_CONTEXT_WINDOWS[model] ?? 200000;
30
+ }
31
+ const fileOffsets = new Map();
32
+ const FILE_OFFSET_TTL = 30 * 60000;
33
+ function cleanupStaleOffsets() {
34
+ const now = Date.now();
35
+ for (const [key, entry] of fileOffsets) {
36
+ if (now - entry.lastAccess > FILE_OFFSET_TTL)
37
+ fileOffsets.delete(key);
38
+ }
39
+ }
40
+ async function processJSONL(filePath) {
41
+ let fileContent;
42
+ try {
43
+ fileContent = await promises_1.default.readFile(filePath, 'utf8');
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ const currentSize = Buffer.byteLength(fileContent, 'utf8');
49
+ const knownEntry = fileOffsets.get(filePath);
50
+ const knownOffset = knownEntry?.offset ?? 0;
51
+ if (currentSize < knownOffset)
52
+ fileOffsets.set(filePath, { offset: 0, lastAccess: Date.now() });
53
+ const totals = {
54
+ input_tokens: 0, output_tokens: 0,
55
+ cache_read: 0, cache_creation: 0, cost_usd: 0,
56
+ context_used: 0, context_window: 200000
57
+ };
58
+ let lastInputUsd = 0;
59
+ let lastOutputUsd = 0;
60
+ let lastInputTokens = 0;
61
+ let lastOutputTokens = 0;
62
+ let lastModel;
63
+ let firstTs;
64
+ for (const raw of fileContent.split('\n')) {
65
+ const line = raw.trim();
66
+ if (!line)
67
+ continue;
68
+ try {
69
+ const obj = JSON.parse(line);
70
+ if (obj.type !== 'assistant')
71
+ continue;
72
+ const msg = obj.message;
73
+ if (!msg?.usage)
74
+ continue;
75
+ const usage = msg.usage;
76
+ const model = msg.model ?? 'claude-sonnet-4-6';
77
+ if (firstTs === undefined && obj.timestamp) {
78
+ try {
79
+ firstTs = new Date(obj.timestamp).getTime();
80
+ }
81
+ catch { /* ignore */ }
82
+ }
83
+ totals.input_tokens += usage.input_tokens ?? 0;
84
+ totals.output_tokens += usage.output_tokens ?? 0;
85
+ totals.cache_read += usage.cache_read_input_tokens ?? 0;
86
+ totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
87
+ totals.cost_usd += (0, pricing_1.calcCost)(model, usage);
88
+ totals.context_used = (usage.input_tokens ?? 0)
89
+ + (usage.cache_read_input_tokens ?? 0)
90
+ + (usage.cache_creation_input_tokens ?? 0);
91
+ totals.context_window = getContextWindow(model);
92
+ const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
93
+ const M = 1000000;
94
+ lastInputUsd = ((usage.input_tokens ?? 0) * price.input +
95
+ (usage.cache_read_input_tokens ?? 0) * price.cacheRead +
96
+ (usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
97
+ lastOutputUsd = ((usage.output_tokens ?? 0) * price.output) / M;
98
+ lastInputTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
99
+ lastOutputTokens = usage.output_tokens ?? 0;
100
+ lastModel = model ?? lastModel;
101
+ }
102
+ catch { /* skip malformed lines */ }
103
+ }
104
+ if (lastInputUsd + lastOutputUsd > 0) {
105
+ totals.lastEntry = {
106
+ inputUsd: lastInputUsd, outputUsd: lastOutputUsd,
107
+ totalUsd: lastInputUsd + lastOutputUsd,
108
+ inputTokens: lastInputTokens, outputTokens: lastOutputTokens,
109
+ };
110
+ }
111
+ totals.lastModel = lastModel;
112
+ totals.firstTs = firstTs;
113
+ fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now() });
114
+ return totals;
115
+ }
116
+ exports.claudeCodeAdapter = {
117
+ name: 'claude-code',
118
+ label: 'Claude Code',
119
+ get shortName() { return 'CC'; },
120
+ detect() {
121
+ try {
122
+ return fs_1.default.existsSync(PROJECTS_DIR);
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ },
128
+ getWatchPaths() {
129
+ return [`${PROJECTS_DIR}/**/*.jsonl`];
130
+ },
131
+ parseEvent(raw, _filePath) {
132
+ try {
133
+ const obj = JSON.parse(raw);
134
+ if (!obj.type || !obj.session_id)
135
+ return null;
136
+ return {
137
+ sessionId: obj.session_id,
138
+ type: obj.type,
139
+ toolName: obj.tool_name,
140
+ toolInput: obj.tool_input ? JSON.stringify(obj.tool_input) : undefined,
141
+ ts: obj.ts ?? obj.timestamp ?? Date.now(),
142
+ cwd: obj.cwd,
143
+ };
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ },
149
+ async getSessionCost(filePath) {
150
+ return processJSONL(filePath);
151
+ },
152
+ };
153
+ setInterval(cleanupStaleOffsets, 5 * 60000).unref();
154
+ (0, adapter_1.registerAdapter)(exports.claudeCodeAdapter);
155
+ // ─── Session-level utilities (used by routes/stream and routes/misc) ───────────
156
+ const blockCostCache = new Map();
157
+ const costCacheLocks = new Map();
158
+ const BLOCK_COST_TTL = 5 * 60000;
159
+ async function getAllBlockCostsForSession(sessionId) {
160
+ const cached = blockCostCache.get(sessionId);
161
+ if (cached && Date.now() - cached.ts < BLOCK_COST_TTL)
162
+ return cached.data;
163
+ if (costCacheLocks.get(sessionId))
164
+ return cached?.data ?? [];
165
+ costCacheLocks.set(sessionId, true);
166
+ try {
167
+ if (!fs_1.default.existsSync(PROJECTS_DIR))
168
+ return [];
169
+ const dirs = await promises_1.default.readdir(PROJECTS_DIR);
170
+ for (const dir of dirs) {
171
+ const dirPath = path_1.default.join(PROJECTS_DIR, dir);
172
+ try {
173
+ const stat = await promises_1.default.stat(dirPath);
174
+ if (!stat.isDirectory())
175
+ continue;
176
+ }
177
+ catch {
178
+ continue;
179
+ }
180
+ const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
181
+ try {
182
+ await promises_1.default.access(filePath);
183
+ }
184
+ catch {
185
+ continue;
186
+ }
187
+ const result = [];
188
+ let current = null;
189
+ const content = await promises_1.default.readFile(filePath, 'utf8');
190
+ for (const raw of content.split('\n')) {
191
+ const line = raw.trim();
192
+ if (!line)
193
+ continue;
194
+ try {
195
+ const obj = JSON.parse(line);
196
+ if (obj.type === 'human' || obj.type === 'user') {
197
+ const msgContent = obj.message?.content;
198
+ if (Array.isArray(msgContent) && msgContent[0]?.type === 'tool_result')
199
+ continue;
200
+ const text = typeof msgContent === 'string' ? msgContent
201
+ : Array.isArray(msgContent)
202
+ ? (msgContent.find((c) => c?.type === 'text')?.text ?? '')
203
+ : '';
204
+ if (text.includes('<system-reminder>') || text.includes('<command-name>'))
205
+ continue;
206
+ current = { inputUsd: 0, outputUsd: 0, totalUsd: 0, inputTokens: 0, outputTokens: 0 };
207
+ result.push(current);
208
+ }
209
+ if (obj.type === 'assistant' && current) {
210
+ const usage = obj.message?.usage;
211
+ const model = obj.message?.model ?? 'claude-sonnet-4-6';
212
+ if (!usage)
213
+ continue;
214
+ const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
215
+ const M = 1000000;
216
+ const inUsd = ((usage.input_tokens ?? 0) * price.input + (usage.cache_read_input_tokens ?? 0) * price.cacheRead + (usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
217
+ const outUsd = ((usage.output_tokens ?? 0) * price.output) / M;
218
+ const inTok = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
219
+ const outTok = usage.output_tokens ?? 0;
220
+ current.inputUsd += inUsd;
221
+ current.outputUsd += outUsd;
222
+ current.totalUsd += inUsd + outUsd;
223
+ current.inputTokens += inTok;
224
+ current.outputTokens += outTok;
225
+ }
226
+ }
227
+ catch { /* skip malformed */ }
228
+ }
229
+ const filtered = result.filter(b => b.totalUsd > 0);
230
+ blockCostCache.set(sessionId, { data: filtered, ts: Date.now() });
231
+ return filtered;
232
+ }
233
+ }
234
+ catch (e) {
235
+ console.warn('[enricher] Error calculating block costs:', e);
236
+ }
237
+ finally {
238
+ costCacheLocks.delete(sessionId);
239
+ }
240
+ return cached?.data ?? [];
241
+ }
242
+ async function getSessionPrompts(sessionId) {
243
+ try {
244
+ if (!fs_1.default.existsSync(PROJECTS_DIR))
245
+ return [];
246
+ const dirs = await promises_1.default.readdir(PROJECTS_DIR);
247
+ for (const dir of dirs) {
248
+ const dirPath = path_1.default.join(PROJECTS_DIR, dir);
249
+ try {
250
+ const stat = await promises_1.default.stat(dirPath);
251
+ if (!stat.isDirectory())
252
+ continue;
253
+ }
254
+ catch {
255
+ continue;
256
+ }
257
+ const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
258
+ try {
259
+ await promises_1.default.access(filePath);
260
+ }
261
+ catch {
262
+ continue;
263
+ }
264
+ const results = [];
265
+ const content = await promises_1.default.readFile(filePath, 'utf8');
266
+ let index = 0;
267
+ for (const raw of content.split('\n')) {
268
+ const line = raw.trim();
269
+ if (!line)
270
+ continue;
271
+ try {
272
+ const obj = JSON.parse(line);
273
+ if (obj.type !== 'human' && obj.type !== 'user')
274
+ continue;
275
+ const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : 0;
276
+ if (!ts || isNaN(ts))
277
+ continue;
278
+ const msgContent = obj.message?.content;
279
+ let text = '';
280
+ if (typeof msgContent === 'string')
281
+ text = msgContent;
282
+ else if (Array.isArray(msgContent)) {
283
+ const textBlocks = msgContent.filter(c => c?.type === 'text');
284
+ if (textBlocks.length === 0)
285
+ continue;
286
+ text = textBlocks.map((c) => c.text ?? '').join('\n').trim();
287
+ }
288
+ if (text.includes('<command-name>') || text.includes('<local-command-stdout>') || text.includes('<system-reminder>') || text.length === 0)
289
+ continue;
290
+ index++;
291
+ results.push({ index, ts, text });
292
+ }
293
+ catch { /* skip */ }
294
+ }
295
+ return results;
296
+ }
297
+ }
298
+ catch { /* skip */ }
299
+ return [];
300
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * codebuff.ts — WatcherAdapter para Codebuff (coding CLI agent)
3
+ *
4
+ * Codebuff stores traces in ~/.codebuff/logs/ as JSONL files.
5
+ * This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
6
+ */
7
+ import { type WatcherAdapter } from './adapter';
8
+ export declare const codebuffAdapter: WatcherAdapter;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * codebuff.ts — WatcherAdapter para Codebuff (coding CLI agent)
4
+ *
5
+ * Codebuff stores traces in ~/.codebuff/logs/ as JSONL files.
6
+ * This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.codebuffAdapter = void 0;
13
+ const path_1 = __importDefault(require("path"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const adapter_1 = require("./adapter");
17
+ const CODEBUFF_DIR = path_1.default.join(os_1.default.homedir(), '.codebuff', 'logs');
18
+ exports.codebuffAdapter = {
19
+ name: 'codebuff',
20
+ label: 'Codebuff',
21
+ get shortName() { return 'CB'; },
22
+ detect() {
23
+ try {
24
+ return fs_1.default.existsSync(CODEBUFF_DIR);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ },
30
+ getWatchPaths() {
31
+ return [`${CODEBUFF_DIR}/**/*.jsonl`];
32
+ },
33
+ parseEvent(_raw, _filePath) {
34
+ // TODO: implement when Codebuff trace format is known
35
+ return null;
36
+ },
37
+ async getSessionCost(_filePath) {
38
+ // TODO: implement when Codebuff trace format is known
39
+ return null;
40
+ },
41
+ };
42
+ (0, adapter_1.registerAdapter)(exports.codebuffAdapter);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * codex.ts — WatcherAdapter para Codex (OpenAI CLI coding agent)
3
+ *
4
+ * Codex stores traces in ~/.codex/logs/ as JSONL files.
5
+ * Format differs from Claude Code — this is a scaffold.
6
+ * Fill in parseEvent/getSessionCost once you have sample traces.
7
+ */
8
+ import { type WatcherAdapter } from './adapter';
9
+ export declare const codexAdapter: WatcherAdapter;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * codex.ts — WatcherAdapter para Codex (OpenAI CLI coding agent)
4
+ *
5
+ * Codex stores traces in ~/.codex/logs/ as JSONL files.
6
+ * Format differs from Claude Code — this is a scaffold.
7
+ * Fill in parseEvent/getSessionCost once you have sample traces.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.codexAdapter = void 0;
14
+ const path_1 = __importDefault(require("path"));
15
+ const os_1 = __importDefault(require("os"));
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const adapter_1 = require("./adapter");
18
+ const CODEX_DIR = path_1.default.join(os_1.default.homedir(), '.codex', 'logs');
19
+ exports.codexAdapter = {
20
+ name: 'codex',
21
+ label: 'Codex',
22
+ get shortName() { return 'Codex'; },
23
+ detect() {
24
+ try {
25
+ return fs_1.default.existsSync(CODEX_DIR);
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ },
31
+ getWatchPaths() {
32
+ return [`${CODEX_DIR}/**/*.jsonl`];
33
+ },
34
+ parseEvent(_raw, _filePath) {
35
+ // TODO: implement when Codex trace format is known
36
+ return null;
37
+ },
38
+ async getSessionCost(_filePath) {
39
+ // TODO: implement when Codex trace format is known
40
+ return null;
41
+ },
42
+ };
43
+ (0, adapter_1.registerAdapter)(exports.codexAdapter);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * droid.ts — WatcherAdapter para Droid (coding CLI agent)
3
+ *
4
+ * Droid stores traces in ~/.droid/logs/ as JSONL files.
5
+ * This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
6
+ */
7
+ import { type WatcherAdapter } from './adapter';
8
+ export declare const droidAdapter: WatcherAdapter;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * droid.ts — WatcherAdapter para Droid (coding CLI agent)
4
+ *
5
+ * Droid stores traces in ~/.droid/logs/ as JSONL files.
6
+ * This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.droidAdapter = void 0;
13
+ const path_1 = __importDefault(require("path"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const adapter_1 = require("./adapter");
17
+ const DROID_DIR = path_1.default.join(os_1.default.homedir(), '.droid', 'logs');
18
+ exports.droidAdapter = {
19
+ name: 'droid',
20
+ label: 'Droid',
21
+ get shortName() { return 'Droid'; },
22
+ detect() {
23
+ try {
24
+ return fs_1.default.existsSync(DROID_DIR);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ },
30
+ getWatchPaths() {
31
+ return [`${DROID_DIR}/**/*.jsonl`];
32
+ },
33
+ parseEvent(_raw, _filePath) {
34
+ // TODO: implement when Droid trace format is known
35
+ return null;
36
+ },
37
+ async getSessionCost(_filePath) {
38
+ // TODO: implement when Droid trace format is known
39
+ return null;
40
+ },
41
+ };
42
+ (0, adapter_1.registerAdapter)(exports.droidAdapter);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * opencode.ts — WatcherAdapter para OpenCode
3
+ *
4
+ * OpenCode stores traces in ~/.opencode/logs/ as JSONL files.
5
+ * Format is similar to Claude Code but with different field names.
6
+ * This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
7
+ */
8
+ import { type WatcherAdapter } from './adapter';
9
+ export declare const opencodeAdapter: WatcherAdapter;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * opencode.ts — WatcherAdapter para OpenCode
4
+ *
5
+ * OpenCode stores traces in ~/.opencode/logs/ as JSONL files.
6
+ * Format is similar to Claude Code but with different field names.
7
+ * This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.opencodeAdapter = void 0;
14
+ const path_1 = __importDefault(require("path"));
15
+ const os_1 = __importDefault(require("os"));
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const adapter_1 = require("./adapter");
18
+ const OPENCODE_DIR = path_1.default.join(os_1.default.homedir(), '.opencode', 'logs');
19
+ exports.opencodeAdapter = {
20
+ name: 'opencode',
21
+ label: 'OpenCode',
22
+ get shortName() { return 'OC'; },
23
+ detect() {
24
+ try {
25
+ return fs_1.default.existsSync(OPENCODE_DIR);
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ },
31
+ getWatchPaths() {
32
+ return [`${OPENCODE_DIR}/**/*.jsonl`];
33
+ },
34
+ parseEvent(_raw, _filePath) {
35
+ // TODO: implement when OpenCode trace format is known
36
+ return null;
37
+ },
38
+ async getSessionCost(_filePath) {
39
+ // TODO: implement when OpenCode trace format is known
40
+ return null;
41
+ },
42
+ };
43
+ (0, adapter_1.registerAdapter)(exports.opencodeAdapter);
package/hooks/event.js CHANGED
@@ -46,18 +46,13 @@ process.stdin.on('end', () => {
46
46
  signal: AbortSignal.timeout(1500),
47
47
  })
48
48
  .then(r => r.json())
49
- .catch(() => {
50
- // Si el daemon no responde, loggeamos en stderr (visible en logs de Claude)
51
- // pero NO bloqueamos — un fallo del daemon no debe interrumpir el trabajo
52
- process.stderr.write(`[claudetrace] daemon no disponible — kill-switch desactivado\n`)
53
- return { blocked: false }
54
- }),
49
+ .catch(() => ({ blocked: false })), // daemon no disponible → fail-open
55
50
  ])
56
51
  .then(([_, ks]) => {
57
52
  if (ks && ks.blocked) {
58
- // Claude Code muestra este stderr al usuario antes de cancelar la acción
59
- process.stderr.write(`\n🚫 claudetrace kill switch activado\n`)
60
- process.stderr.write(` ${ks.reason ?? 'Cuota de uso superada.'}\n\n`)
53
+ process.stderr.write(`\n🚫 claudestat kill switch active\n`)
54
+ process.stderr.write(` ${ks.reason ?? 'Usage quota exceeded.'}\n`)
55
+ process.stderr.write(` To disable: claudestat config --kill-switch false\n\n`)
61
56
  process.exit(2)
62
57
  } else {
63
58
  process.exit(0)
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@statforge/claudestat",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
4
8
  "description": "Observability layer for Claude Code — live token tracking, cost analytics, quota guard, loop detection, and usage dashboard. The htop for Claude Code.",
5
9
  "keywords": [
6
10
  "claude-code",
@@ -54,12 +58,16 @@
54
58
  "scripts": {
55
59
  "build": "tsc && npm run build:dashboard",
56
60
  "build:dashboard": "cd dashboard && npm install && npm run build",
61
+ "build:bun": "bun run scripts/build-binary.ts",
62
+ "build:bun:all": "bun run scripts/build-binary.ts --all",
57
63
  "prepublishOnly": "npm run build",
58
64
  "prepack": "npm run build",
59
65
  "dev": "tsx src/index.ts",
60
66
  "dev:full": "tsx src/index.ts start & sleep 1 && cd dashboard && npm run dev",
61
67
  "start": "node dist/index.js",
62
- "test": "node --require tsx/cjs tests/index.ts"
68
+ "test": "node --require tsx/cjs tests/index.ts",
69
+ "typecheck": "tsc --noEmit",
70
+ "lint": "tsc --noEmit --strict"
63
71
  },
64
72
  "dependencies": {
65
73
  "@anthropic-ai/sdk": "^0.88.0",