tonik 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/dist/cli.js +420 -0
  4. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Takahiro Inaba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # tonik
2
+
3
+ **Score and tune how you use AI — right in your terminal.**
4
+
5
+ `tonik` reads your local Claude Code session logs and shows a beautiful dashboard
6
+ of how efficiently you're using AI: token spend, cost, cache efficiency, and a
7
+ single score that tells you whether things are getting better or worse. Then it
8
+ points at the concrete waste and tells you how to fix it.
9
+
10
+ No account. No API key. No data leaves your machine — it just reads the logs
11
+ Claude Code already keeps locally.
12
+
13
+ ```
14
+ $ npx tonik
15
+
16
+ ╭─ Usage · tinaba96 · 14d ──────────────╮ ╭─ Score ────╮
17
+ │ ▁▂▃▅▇▆▄▃▂▄▆█ ↓ 18% │ │ │
18
+ │ in 4.2M out 1.1M cost $38.20 │ │ ⬢ 72/100 │
19
+ ╰───────────────────────────────────────╯ │ ▲ +6 │
20
+ ╭─ Health ──────────────────────────────╮ ╰────────────╯
21
+ │ sessions 63 turns/session 7.4 ⚠ │
22
+ │ cache hit 41% ⚠ │
23
+ ╰────────────────────────────────────────╯
24
+ ╭─ Findings ────────────────────────────────────────────╮
25
+ │ ✂ low cache reuse — $9.10 spent re-creating context │
26
+ │ ✂ 7.4 turns/session — try splitting prompts │
27
+ ╰────────────────────────────────────────────────────────╯
28
+ ```
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ npx tonik # run without installing
34
+ npm i -g tonik # or install globally
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```sh
40
+ tonik # dashboard for the current project (last 14 days)
41
+ tonik --global # all projects combined
42
+ tonik --since 7d # change the time window
43
+ tonik --theme card # switch visual theme (dashboard | minimal | card)
44
+ tonik --json # machine-readable output (for CI / scripts)
45
+ ```
46
+
47
+ ## How the score works
48
+
49
+ The score is intentionally transparent — it's the sum of three explainable
50
+ sub-scores, each grounded in numbers you can verify:
51
+
52
+ | Sub-score | Max | Based on |
53
+ | --- | --- | --- |
54
+ | Efficiency | 40 | Cache hit rate (reused context ÷ created context) |
55
+ | Conversation | 30 | Avg human turns per session (fewer, well-scoped is better) |
56
+ | Cost discipline | 30 | Token spend trend over the window (improving earns more) |
57
+
58
+ No magic, no black box. If you don't like the weighting, it's open source.
59
+
60
+ ## Roadmap
61
+
62
+ - `tonik fix` — apply the suggested improvements, not just list them
63
+ - More themes and custom color palettes
64
+ - A `tonik-core` library so other tools can reuse the analysis
65
+
66
+ ## License
67
+
68
+ [MIT](./LICENSE) © Takahiro Inaba
package/dist/cli.js ADDED
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+
6
+ // src/logs.ts
7
+ import { createReadStream } from "fs";
8
+ import { readdir, stat } from "fs/promises";
9
+ import { createInterface } from "readline";
10
+ import { homedir } from "os";
11
+ import { join } from "path";
12
+ var PROJECTS_DIR = join(homedir(), ".claude", "projects");
13
+ function cwdToProjectDir(cwd) {
14
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
15
+ }
16
+ function projectLabel(projectDir) {
17
+ const parts = projectDir.split("-").filter(Boolean);
18
+ return parts[parts.length - 1] ?? projectDir;
19
+ }
20
+ function num(v) {
21
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
22
+ }
23
+ function isHumanPrompt(record) {
24
+ if (record?.type !== "user") return false;
25
+ if (record.toolUseResult || record.isMeta) return false;
26
+ const content = record?.message?.content;
27
+ if (typeof content === "string") return content.length > 0;
28
+ if (Array.isArray(content)) {
29
+ return content.some((b) => b?.type === "text" || typeof b === "string");
30
+ }
31
+ return false;
32
+ }
33
+ function parseLine(line, project, since) {
34
+ let record;
35
+ try {
36
+ record = JSON.parse(line);
37
+ } catch {
38
+ return null;
39
+ }
40
+ const ts = record?.timestamp ? Date.parse(record.timestamp) : NaN;
41
+ if (!Number.isFinite(ts) || ts < since) return null;
42
+ const usage = record?.message?.usage;
43
+ return {
44
+ ts,
45
+ type: record?.type ?? "unknown",
46
+ sessionId: record?.sessionId ?? "unknown",
47
+ project,
48
+ model: record?.message?.model ?? null,
49
+ input: num(usage?.input_tokens),
50
+ output: num(usage?.output_tokens),
51
+ cacheRead: num(usage?.cache_read_input_tokens),
52
+ cacheCreation: num(usage?.cache_creation_input_tokens),
53
+ isHumanPrompt: isHumanPrompt(record)
54
+ };
55
+ }
56
+ async function readJsonl(file, project, since) {
57
+ const entries = [];
58
+ const rl = createInterface({
59
+ input: createReadStream(file, { encoding: "utf8" }),
60
+ crlfDelay: Infinity
61
+ });
62
+ for await (const line of rl) {
63
+ if (!line.trim()) continue;
64
+ const entry = parseLine(line, project, since);
65
+ if (entry) entries.push(entry);
66
+ }
67
+ return entries;
68
+ }
69
+ async function loadEntries(opts) {
70
+ let projectDirs;
71
+ try {
72
+ projectDirs = await readdir(PROJECTS_DIR);
73
+ } catch {
74
+ return [];
75
+ }
76
+ if (opts.projectDir) {
77
+ projectDirs = projectDirs.filter((d) => d === opts.projectDir);
78
+ }
79
+ const all = [];
80
+ for (const dir of projectDirs) {
81
+ const dirPath = join(PROJECTS_DIR, dir);
82
+ let files;
83
+ try {
84
+ const s = await stat(dirPath);
85
+ if (!s.isDirectory()) continue;
86
+ files = (await readdir(dirPath)).filter((f) => f.endsWith(".jsonl"));
87
+ } catch {
88
+ continue;
89
+ }
90
+ for (const f of files) {
91
+ try {
92
+ const entries = await readJsonl(join(dirPath, f), dir, opts.since);
93
+ all.push(...entries);
94
+ } catch {
95
+ }
96
+ }
97
+ }
98
+ return all;
99
+ }
100
+
101
+ // src/pricing.ts
102
+ var PRICES = {
103
+ opus: { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
104
+ sonnet: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
105
+ haiku: { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 }
106
+ };
107
+ var DEFAULT_PRICE = PRICES.sonnet;
108
+ function priceFor(model) {
109
+ if (!model) return DEFAULT_PRICE;
110
+ const m = model.toLowerCase();
111
+ if (m.includes("opus")) return PRICES.opus;
112
+ if (m.includes("sonnet")) return PRICES.sonnet;
113
+ if (m.includes("haiku")) return PRICES.haiku;
114
+ return DEFAULT_PRICE;
115
+ }
116
+ function entryCost(e) {
117
+ const p = priceFor(e.model);
118
+ return (e.input * p.input + e.output * p.output + e.cacheCreation * p.cacheWrite + e.cacheRead * p.cacheRead) / 1e6;
119
+ }
120
+
121
+ // src/metrics.ts
122
+ var DAY_MS = 864e5;
123
+ function clamp01(n) {
124
+ return Math.max(0, Math.min(1, n));
125
+ }
126
+ function computeMetrics(entries, opts) {
127
+ const totals = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
128
+ let cost = 0;
129
+ let wastedCost = 0;
130
+ const sessions = /* @__PURE__ */ new Set();
131
+ let humanPrompts = 0;
132
+ const startDay = Math.floor(opts.since / DAY_MS);
133
+ const daily = new Array(opts.windowDays).fill(0);
134
+ for (const e of entries) {
135
+ totals.input += e.input;
136
+ totals.output += e.output;
137
+ totals.cacheRead += e.cacheRead;
138
+ totals.cacheCreation += e.cacheCreation;
139
+ cost += entryCost(e);
140
+ wastedCost += e.cacheCreation * priceFor(e.model).cacheWrite / 1e6;
141
+ if (e.sessionId !== "unknown") sessions.add(e.sessionId);
142
+ if (e.isHumanPrompt) humanPrompts += 1;
143
+ const idx = Math.floor(e.ts / DAY_MS) - startDay;
144
+ if (idx >= 0 && idx < opts.windowDays) {
145
+ daily[idx] += e.input + e.output + e.cacheRead + e.cacheCreation;
146
+ }
147
+ }
148
+ const cacheDenom = totals.cacheRead + totals.cacheCreation;
149
+ const cacheHitRate = cacheDenom > 0 ? totals.cacheRead / cacheDenom : 0;
150
+ const sessionCount = sessions.size;
151
+ const avgTurns = sessionCount > 0 ? humanPrompts / sessionCount : 0;
152
+ const half = Math.floor(opts.windowDays / 2);
153
+ const firstHalf = daily.slice(0, half).reduce((a, b) => a + b, 0);
154
+ const secondHalf = daily.slice(half).reduce((a, b) => a + b, 0);
155
+ const span = firstHalf + secondHalf;
156
+ const trendReliable = firstHalf > 0 && span > 0 && firstHalf / span >= 0.1;
157
+ const trendPct = trendReliable ? (secondHalf - firstHalf) / firstHalf * 100 : 0;
158
+ const hasData = entries.length > 0 && (cacheDenom > 0 || sessionCount > 0);
159
+ const efficiency = clamp01(cacheHitRate / 0.8) * 40;
160
+ const conversation = sessionCount > 0 ? clamp01((15 - avgTurns) / 10) * 30 : 0;
161
+ const costScore = !hasData ? 0 : trendReliable ? clamp01((20 - trendPct) / 30) * 30 : 20;
162
+ const subscores = {
163
+ efficiency: Math.round(efficiency),
164
+ conversation: Math.round(conversation),
165
+ cost: Math.round(costScore)
166
+ };
167
+ const score = subscores.efficiency + subscores.conversation + subscores.cost;
168
+ const findings = [];
169
+ if (cacheHitRate < 0.6 && wastedCost > 0.5) {
170
+ findings.push(
171
+ `low cache reuse (${Math.round(cacheHitRate * 100)}%) \u2014 $${wastedCost.toFixed(2)} spent re-creating context`
172
+ );
173
+ }
174
+ if (avgTurns > 7) {
175
+ findings.push(
176
+ `${avgTurns.toFixed(1)} turns/session \u2014 try splitting work into clearer, scoped prompts`
177
+ );
178
+ }
179
+ if (trendReliable && trendPct > 15) {
180
+ findings.push(
181
+ `token spend up ${Math.round(trendPct)}% this period \u2014 watch growing context size`
182
+ );
183
+ }
184
+ if (hasData && findings.length === 0) {
185
+ findings.push("looking efficient \u2014 no major waste detected. Keep it up!");
186
+ }
187
+ return {
188
+ scope: opts.scope,
189
+ windowDays: opts.windowDays,
190
+ totals,
191
+ cost,
192
+ wastedCost,
193
+ cacheHitRate,
194
+ sessions: sessionCount,
195
+ avgTurns,
196
+ trendPct,
197
+ daily,
198
+ score,
199
+ subscores,
200
+ findings,
201
+ hasData
202
+ };
203
+ }
204
+
205
+ // src/ui.tsx
206
+ import { Box, Text } from "ink";
207
+
208
+ // src/format.ts
209
+ var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
210
+ function formatTokens(n) {
211
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
212
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
213
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
214
+ return String(Math.round(n));
215
+ }
216
+ function formatCost(n) {
217
+ return `$${n.toFixed(2)}`;
218
+ }
219
+ function formatPct(n) {
220
+ const rounded = Math.round(n);
221
+ if (rounded > 0) return `\u2191 ${rounded}%`;
222
+ if (rounded < 0) return `\u2193 ${Math.abs(rounded)}%`;
223
+ return `\u2192 0%`;
224
+ }
225
+ function sparkline(values, width = 24) {
226
+ if (values.length === 0) return "";
227
+ const series = values.length > width ? values.slice(values.length - width) : values;
228
+ const max = Math.max(...series);
229
+ if (max <= 0) return BARS[0].repeat(series.length);
230
+ return series.map((v) => {
231
+ const i = Math.round(v / max * (BARS.length - 1));
232
+ return BARS[Math.max(0, Math.min(BARS.length - 1, i))];
233
+ }).join("");
234
+ }
235
+
236
+ // src/ui.tsx
237
+ import { jsx, jsxs } from "react/jsx-runtime";
238
+ function scoreColor(score) {
239
+ if (score >= 70) return "green";
240
+ if (score >= 40) return "yellow";
241
+ return "red";
242
+ }
243
+ function scoreLabel(score) {
244
+ if (score >= 80) return "excellent";
245
+ if (score >= 60) return "good";
246
+ if (score >= 40) return "fair";
247
+ return "needs work";
248
+ }
249
+ function Panel(props) {
250
+ return /* @__PURE__ */ jsxs(
251
+ Box,
252
+ {
253
+ borderStyle: "round",
254
+ borderColor: "gray",
255
+ flexDirection: "column",
256
+ paddingX: 1,
257
+ flexGrow: props.flexGrow,
258
+ minWidth: props.minWidth,
259
+ width: props.width,
260
+ children: [
261
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: props.title }),
262
+ props.children
263
+ ]
264
+ }
265
+ );
266
+ }
267
+ function warn(active) {
268
+ return active ? " \u26A0" : "";
269
+ }
270
+ function Dashboard({ m }) {
271
+ if (!m.hasData) {
272
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsxs(Panel, { title: `tonik \xB7 ${m.scope}`, children: [
273
+ /* @__PURE__ */ jsxs(Text, { children: [
274
+ "No Claude Code usage found for this scope in the last ",
275
+ m.windowDays,
276
+ " days."
277
+ ] }),
278
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Try `tonik --global` or a wider window with `--since 30d`." })
279
+ ] }) });
280
+ }
281
+ const inTokens = m.totals.input + m.totals.cacheRead + m.totals.cacheCreation;
282
+ const trendColor = m.trendPct <= 0 ? "green" : m.trendPct <= 15 ? "yellow" : "red";
283
+ const cacheLow = m.cacheHitRate < 0.6;
284
+ const turnsHigh = m.avgTurns > 7;
285
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
286
+ /* @__PURE__ */ jsxs(Box, { children: [
287
+ /* @__PURE__ */ jsxs(Panel, { title: `Usage \xB7 ${m.scope} \xB7 ${m.windowDays}d`, flexGrow: 1, minWidth: 42, children: [
288
+ /* @__PURE__ */ jsxs(Box, { children: [
289
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: sparkline(m.daily) }),
290
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { color: trendColor, children: formatPct(m.trendPct) }) })
291
+ ] }),
292
+ /* @__PURE__ */ jsxs(Text, { children: [
293
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "in " }),
294
+ formatTokens(inTokens),
295
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " out " }),
296
+ formatTokens(m.totals.output),
297
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " cost " }),
298
+ /* @__PURE__ */ jsx(Text, { bold: true, children: formatCost(m.cost) })
299
+ ] })
300
+ ] }),
301
+ /* @__PURE__ */ jsx(Panel, { title: "Score", width: 16, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
302
+ /* @__PURE__ */ jsxs(Text, { color: scoreColor(m.score), bold: true, children: [
303
+ "\u2B22 ",
304
+ m.score,
305
+ "/100"
306
+ ] }),
307
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: scoreLabel(m.score) })
308
+ ] }) })
309
+ ] }),
310
+ /* @__PURE__ */ jsxs(Panel, { title: "Health", children: [
311
+ /* @__PURE__ */ jsxs(Text, { children: [
312
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "sessions " }),
313
+ m.sessions,
314
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " turns/session " }),
315
+ /* @__PURE__ */ jsxs(Text, { color: turnsHigh ? "yellow" : void 0, children: [
316
+ m.avgTurns.toFixed(1),
317
+ warn(turnsHigh)
318
+ ] }),
319
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " cache hit " }),
320
+ /* @__PURE__ */ jsxs(Text, { color: cacheLow ? "yellow" : "green", children: [
321
+ Math.round(m.cacheHitRate * 100),
322
+ "%",
323
+ warn(cacheLow)
324
+ ] })
325
+ ] }),
326
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
327
+ "score = efficiency ",
328
+ m.subscores.efficiency,
329
+ "/40 \xB7 conversation",
330
+ " ",
331
+ m.subscores.conversation,
332
+ "/30 \xB7 cost ",
333
+ m.subscores.cost,
334
+ "/30"
335
+ ] })
336
+ ] }),
337
+ /* @__PURE__ */ jsx(Panel, { title: "Findings", children: m.findings.map((f, i) => /* @__PURE__ */ jsxs(Text, { children: [
338
+ /* @__PURE__ */ jsx(Text, { color: f.startsWith("looking") ? "green" : "magenta", children: f.startsWith("looking") ? "\u2713 " : "\u2702 " }),
339
+ f
340
+ ] }, i)) })
341
+ ] });
342
+ }
343
+
344
+ // src/cli.tsx
345
+ import { jsx as jsx2 } from "react/jsx-runtime";
346
+ var DAY_MS2 = 864e5;
347
+ function parseSince(v) {
348
+ if (!v) return 14;
349
+ const m = /^(\d+)\s*d?$/.exec(v.trim());
350
+ return m ? Math.max(1, parseInt(m[1], 10)) : 14;
351
+ }
352
+ function parseArgs(argv) {
353
+ const args = {
354
+ global: false,
355
+ sinceDays: 14,
356
+ theme: "dashboard",
357
+ json: false,
358
+ help: false,
359
+ command: null
360
+ };
361
+ for (let i = 0; i < argv.length; i++) {
362
+ const a = argv[i];
363
+ if (a === "--global" || a === "-g") args.global = true;
364
+ else if (a === "--json") args.json = true;
365
+ else if (a === "--help" || a === "-h") args.help = true;
366
+ else if (a === "--since") args.sinceDays = parseSince(argv[++i]);
367
+ else if (a.startsWith("--since=")) args.sinceDays = parseSince(a.slice("--since=".length));
368
+ else if (a === "--theme") args.theme = argv[++i] ?? "dashboard";
369
+ else if (a.startsWith("--theme=")) args.theme = a.slice("--theme=".length);
370
+ else if (!a.startsWith("-") && !args.command) args.command = a;
371
+ }
372
+ return args;
373
+ }
374
+ var HELP = `
375
+ tonik \u2014 score and tune how you use AI
376
+
377
+ Usage
378
+ $ tonik [command] [options]
379
+
380
+ Commands
381
+ (default) Show the usage dashboard for the current project
382
+ fix Apply suggested improvements (coming soon)
383
+ tips Show detailed improvement tips (coming soon)
384
+
385
+ Options
386
+ -g, --global Combine all projects instead of the current one
387
+ --since <Nd> Time window in days (default: 14d)
388
+ --theme <name> Visual theme: dashboard (default)
389
+ --json Output raw metrics as JSON
390
+ -h, --help Show this help
391
+ `;
392
+ async function main() {
393
+ const args = parseArgs(process.argv.slice(2));
394
+ if (args.help) {
395
+ console.log(HELP);
396
+ return;
397
+ }
398
+ if (args.command === "fix" || args.command === "tips") {
399
+ console.log(`
400
+ tonik ${args.command} is coming soon \u2014 for now, see the Findings panel.
401
+ `);
402
+ return;
403
+ }
404
+ const projectDir = args.global ? null : cwdToProjectDir(process.cwd());
405
+ const since = Date.now() - args.sinceDays * DAY_MS2;
406
+ const entries = await loadEntries({ projectDir, since });
407
+ const scope = args.global ? "all projects" : projectLabel(projectDir);
408
+ const metrics = computeMetrics(entries, { scope, windowDays: args.sinceDays, since });
409
+ if (args.json) {
410
+ console.log(JSON.stringify(metrics, null, 2));
411
+ return;
412
+ }
413
+ const app = render(/* @__PURE__ */ jsx2(Dashboard, { m: metrics }));
414
+ app.unmount();
415
+ await app.waitUntilExit();
416
+ }
417
+ main().catch((err) => {
418
+ console.error(err);
419
+ process.exitCode = 1;
420
+ });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "tonik",
3
+ "version": "0.1.0",
4
+ "description": "Score and tune how you use AI — right in your terminal.",
5
+ "type": "module",
6
+ "bin": {
7
+ "tonik": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "dev": "tsx src/cli.tsx",
17
+ "build": "tsup src/cli.tsx --format esm --clean --target node18",
18
+ "start": "node dist/cli.js",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "keywords": [
22
+ "cli",
23
+ "ai",
24
+ "claude",
25
+ "tokens",
26
+ "analytics",
27
+ "developer-tools",
28
+ "typescript",
29
+ "ink"
30
+ ],
31
+ "author": "Takahiro Inaba (@tinaba96)",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/tinaba96/tonik.git"
36
+ },
37
+ "homepage": "https://github.com/tinaba96/tonik#readme",
38
+ "dependencies": {
39
+ "ink": "^5.0.1",
40
+ "react": "^18.3.1"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.14.0",
44
+ "@types/react": "^18.3.3",
45
+ "tsup": "^8.1.0",
46
+ "tsx": "^4.16.0",
47
+ "typescript": "^5.5.0"
48
+ }
49
+ }