@suncreation/modu-arena 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.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @suncreation/modu-arena
2
+
3
+ Track and rank your AI coding tool usage across **Claude Code**, **OpenCode**, **Gemini CLI**, **Codex CLI**, and **Crush**.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx @suncreation/modu-arena install --api-key <your-api-key>
9
+ ```
10
+
11
+ Get your API key from the [Modu-Arena dashboard](https://your-server.com).
12
+
13
+ ## Commands
14
+
15
+ ### `install`
16
+
17
+ Set up tracking hooks for all detected AI coding tools.
18
+
19
+ ```bash
20
+ npx @suncreation/modu-arena install --api-key modu_arena_xxxxxxxx_yyyyyyyy
21
+ ```
22
+
23
+ This will:
24
+ - Save your API key to `~/.modu-arena.json`
25
+ - Detect installed AI coding tools
26
+ - Install session-end hooks for each detected tool
27
+
28
+ ### `rank`
29
+
30
+ View your usage stats.
31
+
32
+ ```bash
33
+ npx @suncreation/modu-arena rank
34
+ ```
35
+
36
+ Shows total tokens, sessions, tool breakdown, and 7/30-day trends.
37
+
38
+ ### `status`
39
+
40
+ Check your current configuration and installed hooks.
41
+
42
+ ```bash
43
+ npx @suncreation/modu-arena status
44
+ ```
45
+
46
+ ### `uninstall`
47
+
48
+ Remove all hooks and configuration.
49
+
50
+ ```bash
51
+ npx @suncreation/modu-arena uninstall
52
+ ```
53
+
54
+ ## Supported Tools
55
+
56
+ | Tool | Detection | Hook Location |
57
+ |------|-----------|---------------|
58
+ | Claude Code | `~/.claude/` | `~/.claude/hooks/session-end.sh` |
59
+ | OpenCode | `~/.opencode/` | `~/.opencode/hooks/session-end.sh` |
60
+ | Gemini CLI | `~/.gemini/` | `~/.gemini/hooks/session-end.sh` |
61
+ | Codex CLI | `~/.codex/` | `~/.codex/hooks/session-end.sh` |
62
+ | Crush | `~/.crush/` | `~/.crush/hooks/session-end.sh` |
63
+
64
+ ## Configuration
65
+
66
+ Config is stored in `~/.modu-arena.json`:
67
+
68
+ ```json
69
+ {
70
+ "apiKey": "modu_arena_xxxxxxxx_yyyyyyyy",
71
+ "serverUrl": "https://your-server.com"
72
+ }
73
+ ```
74
+
75
+ ### Custom Server URL
76
+
77
+ ```bash
78
+ MODU_ARENA_API_URL=https://your-server.com npx @suncreation/modu-arena install --api-key <key>
79
+ ```
80
+
81
+ ## How It Works
82
+
83
+ 1. **Install** sets up lightweight shell hooks in each tool's config directory
84
+ 2. When a coding session ends, the hook sends token usage data to the Modu-Arena server
85
+ 3. Data includes: input/output tokens, cache tokens, model name, and timing
86
+ 4. All submissions are authenticated with HMAC-SHA256 signatures
87
+ 5. View your stats via `rank` command or the web dashboard
88
+
89
+ ## Security
90
+
91
+ - API keys are stored locally in `~/.modu-arena.json`
92
+ - All API requests use HMAC-SHA256 signature verification
93
+ - Session data is hashed server-side for integrity and deduplication
94
+ - No source code or project content is ever transmitted
95
+
96
+ ## Requirements
97
+
98
+ - Node.js 20+
99
+ - One or more supported AI coding tools installed
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,788 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands.ts
4
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync, unlinkSync } from "fs";
5
+ import { homedir as homedir3 } from "os";
6
+ import { basename, join as join3 } from "path";
7
+
8
+ // src/adapters.ts
9
+ import { existsSync, writeFileSync, mkdirSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { join } from "path";
12
+
13
+ // src/constants.ts
14
+ var API_BASE_URL = process.env.MODU_ARENA_API_URL ?? "http://localhost:8989";
15
+ var TOOL_DISPLAY_NAMES = {
16
+ "claude-code": "Claude Code",
17
+ opencode: "OpenCode",
18
+ gemini: "Gemini CLI",
19
+ codex: "Codex CLI",
20
+ crush: "Crush"
21
+ };
22
+ var CONFIG_FILE_NAME = ".modu-arena.json";
23
+
24
+ // src/adapters.ts
25
+ var ClaudeCodeAdapter = class {
26
+ slug = "claude-code";
27
+ displayName = "Claude Code";
28
+ get configDir() {
29
+ return join(homedir(), ".claude");
30
+ }
31
+ get hooksDir() {
32
+ return join(this.configDir, "hooks");
33
+ }
34
+ getHookPath() {
35
+ return join(this.hooksDir, "session-end.sh");
36
+ }
37
+ detect() {
38
+ return existsSync(this.configDir);
39
+ }
40
+ install(apiKey) {
41
+ try {
42
+ if (!existsSync(this.hooksDir)) {
43
+ mkdirSync(this.hooksDir, { recursive: true });
44
+ }
45
+ const hookScript = this.generateHookScript(apiKey);
46
+ const hookPath = this.getHookPath();
47
+ writeFileSync(hookPath, hookScript, { mode: 493 });
48
+ return {
49
+ success: true,
50
+ message: `Claude Code hook installed at ${hookPath}`,
51
+ hookPath
52
+ };
53
+ } catch (err) {
54
+ return {
55
+ success: false,
56
+ message: `Failed to install Claude Code hook: ${err}`
57
+ };
58
+ }
59
+ }
60
+ generateHookScript(apiKey) {
61
+ return `#!/bin/bash
62
+ # Modu-Arena: Claude Code session tracking hook
63
+ # Auto-generated \u2014 do not edit manually
64
+
65
+ MODU_API_KEY="${apiKey}"
66
+ MODU_SERVER="${API_BASE_URL}"
67
+
68
+ # Claude Code passes session data via environment variables
69
+ # This hook fires at session end
70
+ if [ -n "$CLAUDE_SESSION_ID" ]; then
71
+ SESSION_DATA=$(cat <<EOF
72
+ {
73
+ "toolType": "claude-code",
74
+ "sessionId": "$CLAUDE_SESSION_ID",
75
+ "startedAt": "$CLAUDE_SESSION_STARTED_AT",
76
+ "endedAt": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
77
+ "inputTokens": \${CLAUDE_INPUT_TOKENS:-0},
78
+ "outputTokens": \${CLAUDE_OUTPUT_TOKENS:-0},
79
+ "cacheCreationTokens": \${CLAUDE_CACHE_CREATION_TOKENS:-0},
80
+ "cacheReadTokens": \${CLAUDE_CACHE_READ_TOKENS:-0},
81
+ "modelName": "\${CLAUDE_MODEL:-unknown}"
82
+ }
83
+ EOF
84
+ )
85
+
86
+ TIMESTAMP=$(date +%s)
87
+ MESSAGE="\${TIMESTAMP}:\${SESSION_DATA}"
88
+ SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$MODU_API_KEY" | sed 's/.*= //')
89
+
90
+ curl -s -X POST "\${MODU_SERVER}/api/v1/sessions" \\
91
+ -H "Content-Type: application/json" \\
92
+ -H "X-API-Key: \${MODU_API_KEY}" \\
93
+ -H "X-Timestamp: \${TIMESTAMP}" \\
94
+ -H "X-Signature: \${SIGNATURE}" \\
95
+ -d "\${SESSION_DATA}" > /dev/null 2>&1 &
96
+ fi
97
+ `;
98
+ }
99
+ };
100
+ var OpenCodeAdapter = class {
101
+ slug = "opencode";
102
+ displayName = "OpenCode";
103
+ get configDir() {
104
+ return join(homedir(), ".opencode");
105
+ }
106
+ getHookPath() {
107
+ return join(this.configDir, "hooks", "session-end.sh");
108
+ }
109
+ detect() {
110
+ return existsSync(this.configDir);
111
+ }
112
+ install(apiKey) {
113
+ try {
114
+ const hooksDir = join(this.configDir, "hooks");
115
+ if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
116
+ const hookScript = this.generateHookScript(apiKey);
117
+ const hookPath = this.getHookPath();
118
+ writeFileSync(hookPath, hookScript, { mode: 493 });
119
+ return {
120
+ success: true,
121
+ message: `OpenCode hook installed at ${hookPath}`,
122
+ hookPath
123
+ };
124
+ } catch (err) {
125
+ return {
126
+ success: false,
127
+ message: `Failed to install OpenCode hook: ${err}`
128
+ };
129
+ }
130
+ }
131
+ generateHookScript(apiKey) {
132
+ return `#!/bin/bash
133
+ # Modu-Arena: OpenCode session tracking hook
134
+ # Auto-generated \u2014 do not edit manually
135
+
136
+ MODU_API_KEY="${apiKey}"
137
+ MODU_SERVER="${API_BASE_URL}"
138
+
139
+ if [ -n "$OPENCODE_SESSION_ID" ]; then
140
+ SESSION_DATA=$(cat <<EOF
141
+ {
142
+ "toolType": "opencode",
143
+ "sessionId": "$OPENCODE_SESSION_ID",
144
+ "startedAt": "$OPENCODE_SESSION_STARTED_AT",
145
+ "endedAt": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
146
+ "inputTokens": \${OPENCODE_INPUT_TOKENS:-0},
147
+ "outputTokens": \${OPENCODE_OUTPUT_TOKENS:-0},
148
+ "modelName": "\${OPENCODE_MODEL:-unknown}"
149
+ }
150
+ EOF
151
+ )
152
+
153
+ TIMESTAMP=$(date +%s)
154
+ MESSAGE="\${TIMESTAMP}:\${SESSION_DATA}"
155
+ SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$MODU_API_KEY" | sed 's/.*= //')
156
+
157
+ curl -s -X POST "\${MODU_SERVER}/api/v1/sessions" \\
158
+ -H "Content-Type: application/json" \\
159
+ -H "X-API-Key: \${MODU_API_KEY}" \\
160
+ -H "X-Timestamp: \${TIMESTAMP}" \\
161
+ -H "X-Signature: \${SIGNATURE}" \\
162
+ -d "\${SESSION_DATA}" > /dev/null 2>&1 &
163
+ fi
164
+ `;
165
+ }
166
+ };
167
+ var GeminiAdapter = class {
168
+ slug = "gemini";
169
+ displayName = "Gemini CLI";
170
+ get configDir() {
171
+ return join(homedir(), ".gemini");
172
+ }
173
+ getHookPath() {
174
+ return join(this.configDir, "hooks", "session-end.sh");
175
+ }
176
+ detect() {
177
+ return existsSync(this.configDir);
178
+ }
179
+ install(apiKey) {
180
+ try {
181
+ const hooksDir = join(this.configDir, "hooks");
182
+ if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
183
+ const hookScript = this.generateHookScript(apiKey);
184
+ const hookPath = this.getHookPath();
185
+ writeFileSync(hookPath, hookScript, { mode: 493 });
186
+ return {
187
+ success: true,
188
+ message: `Gemini CLI hook installed at ${hookPath}`,
189
+ hookPath
190
+ };
191
+ } catch (err) {
192
+ return {
193
+ success: false,
194
+ message: `Failed to install Gemini CLI hook: ${err}`
195
+ };
196
+ }
197
+ }
198
+ generateHookScript(apiKey) {
199
+ return `#!/bin/bash
200
+ # Modu-Arena: Gemini CLI session tracking hook
201
+ # Auto-generated \u2014 do not edit manually
202
+
203
+ MODU_API_KEY="${apiKey}"
204
+ MODU_SERVER="${API_BASE_URL}"
205
+
206
+ if [ -n "$GEMINI_SESSION_ID" ]; then
207
+ SESSION_DATA=$(cat <<EOF
208
+ {
209
+ "toolType": "gemini",
210
+ "sessionId": "$GEMINI_SESSION_ID",
211
+ "startedAt": "$GEMINI_SESSION_STARTED_AT",
212
+ "endedAt": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
213
+ "inputTokens": \${GEMINI_INPUT_TOKENS:-0},
214
+ "outputTokens": \${GEMINI_OUTPUT_TOKENS:-0},
215
+ "modelName": "\${GEMINI_MODEL:-unknown}"
216
+ }
217
+ EOF
218
+ )
219
+
220
+ TIMESTAMP=$(date +%s)
221
+ MESSAGE="\${TIMESTAMP}:\${SESSION_DATA}"
222
+ SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$MODU_API_KEY" | sed 's/.*= //')
223
+
224
+ curl -s -X POST "\${MODU_SERVER}/api/v1/sessions" \\
225
+ -H "Content-Type: application/json" \\
226
+ -H "X-API-Key: \${MODU_API_KEY}" \\
227
+ -H "X-Timestamp: \${TIMESTAMP}" \\
228
+ -H "X-Signature: \${SIGNATURE}" \\
229
+ -d "\${SESSION_DATA}" > /dev/null 2>&1 &
230
+ fi
231
+ `;
232
+ }
233
+ };
234
+ var CodexAdapter = class {
235
+ slug = "codex";
236
+ displayName = "Codex CLI";
237
+ get configDir() {
238
+ return join(homedir(), ".codex");
239
+ }
240
+ getHookPath() {
241
+ return join(this.configDir, "hooks", "session-end.sh");
242
+ }
243
+ detect() {
244
+ return existsSync(this.configDir);
245
+ }
246
+ install(apiKey) {
247
+ try {
248
+ const hooksDir = join(this.configDir, "hooks");
249
+ if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
250
+ const hookScript = this.generateHookScript(apiKey);
251
+ const hookPath = this.getHookPath();
252
+ writeFileSync(hookPath, hookScript, { mode: 493 });
253
+ return {
254
+ success: true,
255
+ message: `Codex CLI hook installed at ${hookPath}`,
256
+ hookPath
257
+ };
258
+ } catch (err) {
259
+ return {
260
+ success: false,
261
+ message: `Failed to install Codex CLI hook: ${err}`
262
+ };
263
+ }
264
+ }
265
+ generateHookScript(apiKey) {
266
+ return `#!/bin/bash
267
+ # Modu-Arena: Codex CLI session tracking hook
268
+ # Auto-generated \u2014 do not edit manually
269
+
270
+ MODU_API_KEY="${apiKey}"
271
+ MODU_SERVER="${API_BASE_URL}"
272
+
273
+ if [ -n "$CODEX_SESSION_ID" ]; then
274
+ SESSION_DATA=$(cat <<EOF
275
+ {
276
+ "toolType": "codex",
277
+ "sessionId": "$CODEX_SESSION_ID",
278
+ "startedAt": "$CODEX_SESSION_STARTED_AT",
279
+ "endedAt": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
280
+ "inputTokens": \${CODEX_INPUT_TOKENS:-0},
281
+ "outputTokens": \${CODEX_OUTPUT_TOKENS:-0},
282
+ "modelName": "\${CODEX_MODEL:-unknown}"
283
+ }
284
+ EOF
285
+ )
286
+
287
+ TIMESTAMP=$(date +%s)
288
+ MESSAGE="\${TIMESTAMP}:\${SESSION_DATA}"
289
+ SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$MODU_API_KEY" | sed 's/.*= //')
290
+
291
+ curl -s -X POST "\${MODU_SERVER}/api/v1/sessions" \\
292
+ -H "Content-Type: application/json" \\
293
+ -H "X-API-Key: \${MODU_API_KEY}" \\
294
+ -H "X-Timestamp: \${TIMESTAMP}" \\
295
+ -H "X-Signature: \${SIGNATURE}" \\
296
+ -d "\${SESSION_DATA}" > /dev/null 2>&1 &
297
+ fi
298
+ `;
299
+ }
300
+ };
301
+ var CrushAdapter = class {
302
+ slug = "crush";
303
+ displayName = "Crush";
304
+ get configDir() {
305
+ return join(homedir(), ".crush");
306
+ }
307
+ getHookPath() {
308
+ return join(this.configDir, "hooks", "session-end.sh");
309
+ }
310
+ detect() {
311
+ return existsSync(this.configDir);
312
+ }
313
+ install(apiKey) {
314
+ try {
315
+ const hooksDir = join(this.configDir, "hooks");
316
+ if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
317
+ const hookScript = this.generateHookScript(apiKey);
318
+ const hookPath = this.getHookPath();
319
+ writeFileSync(hookPath, hookScript, { mode: 493 });
320
+ return {
321
+ success: true,
322
+ message: `Crush hook installed at ${hookPath}`,
323
+ hookPath
324
+ };
325
+ } catch (err) {
326
+ return {
327
+ success: false,
328
+ message: `Failed to install Crush hook: ${err}`
329
+ };
330
+ }
331
+ }
332
+ generateHookScript(apiKey) {
333
+ return `#!/bin/bash
334
+ # Modu-Arena: Crush session tracking hook
335
+ # Auto-generated \u2014 do not edit manually
336
+
337
+ MODU_API_KEY="${apiKey}"
338
+ MODU_SERVER="${API_BASE_URL}"
339
+
340
+ if [ -n "$CRUSH_SESSION_ID" ]; then
341
+ SESSION_DATA=$(cat <<EOF
342
+ {
343
+ "toolType": "crush",
344
+ "sessionId": "$CRUSH_SESSION_ID",
345
+ "startedAt": "$CRUSH_SESSION_STARTED_AT",
346
+ "endedAt": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
347
+ "inputTokens": \${CRUSH_INPUT_TOKENS:-0},
348
+ "outputTokens": \${CRUSH_OUTPUT_TOKENS:-0},
349
+ "modelName": "\${CRUSH_MODEL:-unknown}"
350
+ }
351
+ EOF
352
+ )
353
+
354
+ TIMESTAMP=$(date +%s)
355
+ MESSAGE="\${TIMESTAMP}:\${SESSION_DATA}"
356
+ SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$MODU_API_KEY" | sed 's/.*= //')
357
+
358
+ curl -s -X POST "\${MODU_SERVER}/api/v1/sessions" \\
359
+ -H "Content-Type: application/json" \\
360
+ -H "X-API-Key: \${MODU_API_KEY}" \\
361
+ -H "X-Timestamp: \${TIMESTAMP}" \\
362
+ -H "X-Signature: \${SIGNATURE}" \\
363
+ -d "\${SESSION_DATA}" > /dev/null 2>&1 &
364
+ fi
365
+ `;
366
+ }
367
+ };
368
+ function getAllAdapters() {
369
+ return [
370
+ new ClaudeCodeAdapter(),
371
+ new OpenCodeAdapter(),
372
+ new GeminiAdapter(),
373
+ new CodexAdapter(),
374
+ new CrushAdapter()
375
+ ];
376
+ }
377
+
378
+ // src/crypto.ts
379
+ import { createHmac, createHash } from "crypto";
380
+ function computeHmacSignature(apiKey, timestamp, body) {
381
+ const message = `${timestamp}:${body}`;
382
+ return createHmac("sha256", apiKey).update(message).digest("hex");
383
+ }
384
+
385
+ // src/api.ts
386
+ function baseUrl(opts) {
387
+ return opts.serverUrl || API_BASE_URL;
388
+ }
389
+ function makeAuthHeaders(apiKey, body) {
390
+ const headers = {
391
+ "Content-Type": "application/json",
392
+ "X-API-Key": apiKey
393
+ };
394
+ if (body !== void 0) {
395
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
396
+ const signature = computeHmacSignature(apiKey, timestamp, body);
397
+ headers["X-Timestamp"] = timestamp;
398
+ headers["X-Signature"] = signature;
399
+ }
400
+ return headers;
401
+ }
402
+ async function getRank(opts) {
403
+ const url = `${baseUrl(opts)}/api/v1/rank`;
404
+ const res = await fetch(url, {
405
+ method: "GET",
406
+ headers: {
407
+ "X-API-Key": opts.apiKey
408
+ }
409
+ });
410
+ const data = await res.json();
411
+ if (!res.ok) {
412
+ return { success: false, error: data.error || `HTTP ${res.status}` };
413
+ }
414
+ return data;
415
+ }
416
+ async function submitEvaluation(payload, opts) {
417
+ const body = JSON.stringify(payload);
418
+ const url = `${baseUrl(opts)}/api/v1/evaluate`;
419
+ const res = await fetch(url, {
420
+ method: "POST",
421
+ headers: makeAuthHeaders(opts.apiKey, body),
422
+ body
423
+ });
424
+ const data = await res.json();
425
+ if (!res.ok) {
426
+ return { success: false, error: data.error || `HTTP ${res.status}` };
427
+ }
428
+ return data;
429
+ }
430
+
431
+ // src/config.ts
432
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
433
+ import { homedir as homedir2 } from "os";
434
+ import { join as join2, dirname } from "path";
435
+ function getConfigPath() {
436
+ return join2(homedir2(), CONFIG_FILE_NAME);
437
+ }
438
+ function loadConfig() {
439
+ const configPath = getConfigPath();
440
+ if (!existsSync2(configPath)) return null;
441
+ try {
442
+ const raw = readFileSync2(configPath, "utf-8");
443
+ return JSON.parse(raw);
444
+ } catch {
445
+ return null;
446
+ }
447
+ }
448
+ function saveConfig(config) {
449
+ const configPath = getConfigPath();
450
+ const dir = dirname(configPath);
451
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
452
+ writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
453
+ }
454
+ function requireConfig() {
455
+ const config = loadConfig();
456
+ if (!config?.apiKey) {
457
+ console.error(
458
+ "Error: Not configured. Run `npx @suncreation/modu-arena install` first."
459
+ );
460
+ process.exit(1);
461
+ }
462
+ return config;
463
+ }
464
+
465
+ // src/commands.ts
466
+ async function installCommand(apiKey) {
467
+ console.log("\n\u{1F527} Modu-Arena \u2014 AI Coding Tool Usage Tracker\n");
468
+ const existing = loadConfig();
469
+ if (existing?.apiKey && !apiKey) {
470
+ console.log("\u2713 Already configured.");
471
+ console.log(" Use --api-key <key> to update your API key.\n");
472
+ apiKey = existing.apiKey;
473
+ }
474
+ if (!apiKey) {
475
+ console.error(
476
+ "Error: API key required.\n Get your API key from the Modu-Arena dashboard.\n Usage: npx @suncreation/modu-arena install --api-key <your-api-key>\n"
477
+ );
478
+ process.exit(1);
479
+ }
480
+ if (!apiKey.startsWith("modu_arena_")) {
481
+ console.error(
482
+ 'Error: Invalid API key format. Key must start with "modu_arena_".\n'
483
+ );
484
+ process.exit(1);
485
+ }
486
+ saveConfig({ apiKey });
487
+ console.log("\u2713 API key saved to ~/.modu-arena.json\n");
488
+ const adapters = getAllAdapters();
489
+ const results = [];
490
+ console.log("Detecting AI coding tools...\n");
491
+ for (const adapter of adapters) {
492
+ const detected = adapter.detect();
493
+ if (detected) {
494
+ console.log(` \u2713 ${adapter.displayName} detected`);
495
+ const result = adapter.install(apiKey);
496
+ results.push({ tool: adapter.displayName, result });
497
+ if (result.success) {
498
+ console.log(` \u2192 Hook installed: ${result.hookPath}`);
499
+ } else {
500
+ console.log(` \u2717 ${result.message}`);
501
+ }
502
+ } else {
503
+ console.log(` - ${adapter.displayName} not found`);
504
+ }
505
+ }
506
+ const installed = results.filter((r) => r.result.success);
507
+ console.log(
508
+ `
509
+ \u2713 Setup complete. ${installed.length} tool(s) configured.
510
+ `
511
+ );
512
+ if (installed.length === 0) {
513
+ console.log(
514
+ "No AI coding tools detected. Install one of the supported tools:\n \u2022 Claude Code (https://docs.anthropic.com/s/claude-code)\n \u2022 OpenCode (https://opencode.ai)\n \u2022 Gemini CLI (https://github.com/google-gemini/gemini-cli)\n \u2022 Codex CLI (https://github.com/openai/codex)\n \u2022 Crush (https://charm.sh/crush)\n"
515
+ );
516
+ }
517
+ }
518
+ async function rankCommand() {
519
+ const config = requireConfig();
520
+ console.log("\n\u{1F4CA} Modu-Arena \u2014 Your Stats\n");
521
+ const result = await getRank({ apiKey: config.apiKey, serverUrl: config.serverUrl });
522
+ if (!result.success) {
523
+ console.error(`Error: ${"error" in result ? result.error : "Unknown error"}
524
+ `);
525
+ process.exit(1);
526
+ }
527
+ if (!("data" in result) || !result.data) {
528
+ console.error("Error: Unexpected response format.\n");
529
+ process.exit(1);
530
+ }
531
+ const { username, usage, overview } = result.data;
532
+ console.log(` User: ${username}`);
533
+ console.log(` Tokens: ${formatNumber(usage.totalTokens)}`);
534
+ console.log(` Sessions: ${usage.totalSessions}`);
535
+ console.log(` Projects: ${overview.successfulProjectsCount}`);
536
+ console.log("");
537
+ if (usage.toolBreakdown.length > 0) {
538
+ console.log(" Tool Breakdown:");
539
+ for (const entry of usage.toolBreakdown) {
540
+ const name = TOOL_DISPLAY_NAMES[entry.tool] || entry.tool;
541
+ console.log(
542
+ ` ${name}: ${formatNumber(entry.tokens)} tokens`
543
+ );
544
+ }
545
+ console.log("");
546
+ }
547
+ const sum7 = usage.last7Days.reduce(
548
+ (acc, d) => ({ tokens: acc.tokens + d.inputTokens + d.outputTokens, sessions: acc.sessions + d.sessions }),
549
+ { tokens: 0, sessions: 0 }
550
+ );
551
+ const sum30 = usage.last30Days.reduce(
552
+ (acc, d) => ({ tokens: acc.tokens + d.inputTokens + d.outputTokens, sessions: acc.sessions + d.sessions }),
553
+ { tokens: 0, sessions: 0 }
554
+ );
555
+ console.log(
556
+ ` Last 7 days: ${formatNumber(sum7.tokens)} tokens, ${sum7.sessions} sessions`
557
+ );
558
+ console.log(
559
+ ` Last 30 days: ${formatNumber(sum30.tokens)} tokens, ${sum30.sessions} sessions`
560
+ );
561
+ console.log("");
562
+ }
563
+ function statusCommand() {
564
+ const config = loadConfig();
565
+ console.log("\n\u{1F50D} Modu-Arena \u2014 Status\n");
566
+ if (!config?.apiKey) {
567
+ console.log(" Status: Not configured");
568
+ console.log(
569
+ " Run `npx @suncreation/modu-arena install --api-key <key>` to set up.\n"
570
+ );
571
+ return;
572
+ }
573
+ const maskedKey = config.apiKey.slice(0, 15) + "..." + config.apiKey.slice(-4);
574
+ console.log(` API Key: ${maskedKey}`);
575
+ console.log(` Server: ${config.serverUrl || API_BASE_URL}`);
576
+ console.log("");
577
+ const adapters = getAllAdapters();
578
+ console.log(" Installed Hooks:");
579
+ let hookCount = 0;
580
+ for (const adapter of adapters) {
581
+ const detected = adapter.detect();
582
+ if (detected) {
583
+ const hookExists = existsSync3(adapter.getHookPath());
584
+ const status = hookExists ? "\u2713 Active" : "\u2717 Not installed";
585
+ console.log(` ${adapter.displayName}: ${status}`);
586
+ if (hookExists) hookCount++;
587
+ }
588
+ }
589
+ if (hookCount === 0) {
590
+ console.log(" (none)");
591
+ }
592
+ console.log("");
593
+ }
594
+ function uninstallCommand() {
595
+ console.log("\n\u{1F5D1}\uFE0F Modu-Arena \u2014 Uninstall\n");
596
+ const adapters = getAllAdapters();
597
+ for (const adapter of adapters) {
598
+ const hookPath = adapter.getHookPath();
599
+ if (existsSync3(hookPath)) {
600
+ unlinkSync(hookPath);
601
+ console.log(` \u2713 Removed ${adapter.displayName} hook`);
602
+ }
603
+ }
604
+ const configPath = join3(homedir3(), ".modu-arena.json");
605
+ if (existsSync3(configPath)) {
606
+ unlinkSync(configPath);
607
+ console.log(" \u2713 Removed ~/.modu-arena.json");
608
+ }
609
+ console.log("\n\u2713 Modu-Arena uninstalled.\n");
610
+ }
611
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
612
+ "node_modules",
613
+ ".git",
614
+ ".next",
615
+ ".nuxt",
616
+ "dist",
617
+ "build",
618
+ "out",
619
+ ".cache",
620
+ ".turbo",
621
+ ".vercel",
622
+ "__pycache__",
623
+ ".svelte-kit",
624
+ "coverage",
625
+ ".output",
626
+ ".parcel-cache"
627
+ ]);
628
+ function collectFileStructure(dir, maxDepth, currentDepth = 0) {
629
+ const result = {};
630
+ if (currentDepth >= maxDepth) return result;
631
+ let entries;
632
+ try {
633
+ entries = readdirSync(dir);
634
+ } catch {
635
+ return result;
636
+ }
637
+ const files = [];
638
+ for (const entry of entries) {
639
+ if (entry.startsWith(".") && IGNORE_DIRS.has(entry)) continue;
640
+ if (IGNORE_DIRS.has(entry)) continue;
641
+ const fullPath = join3(dir, entry);
642
+ let stat;
643
+ try {
644
+ stat = statSync(fullPath);
645
+ } catch {
646
+ continue;
647
+ }
648
+ if (stat.isDirectory()) {
649
+ const sub = collectFileStructure(fullPath, maxDepth, currentDepth + 1);
650
+ for (const [key, val] of Object.entries(sub)) {
651
+ result[key] = val;
652
+ }
653
+ } else {
654
+ files.push(entry);
655
+ }
656
+ }
657
+ if (files.length > 0) {
658
+ const relDir = currentDepth === 0 ? "." : dir.split("/").slice(-currentDepth).join("/");
659
+ result[relDir] = files;
660
+ }
661
+ return result;
662
+ }
663
+ async function submitCommand() {
664
+ const config = requireConfig();
665
+ console.log("\n\u{1F680} Modu-Arena \u2014 Project Submit\n");
666
+ const cwd = process.cwd();
667
+ const projectName = basename(cwd);
668
+ const readmePath = join3(cwd, "README.md");
669
+ if (!existsSync3(readmePath)) {
670
+ console.error("Error: README.md not found in the current directory.");
671
+ console.error(" Please create a README.md describing your project.\n");
672
+ process.exit(1);
673
+ }
674
+ const description = readFileSync3(readmePath, "utf-8");
675
+ if (description.trim().length === 0) {
676
+ console.error("Error: README.md is empty.\n");
677
+ process.exit(1);
678
+ }
679
+ console.log(` Project: ${projectName}`);
680
+ console.log(` README: ${readmePath}`);
681
+ console.log("");
682
+ console.log(" Collecting file structure...");
683
+ const fileStructure = collectFileStructure(cwd, 3);
684
+ const fileCount = Object.values(fileStructure).reduce((sum, files) => sum + files.length, 0);
685
+ console.log(` Found ${fileCount} file(s) in ${Object.keys(fileStructure).length} director${Object.keys(fileStructure).length === 1 ? "y" : "ies"}`);
686
+ console.log("");
687
+ console.log(" Submitting for evaluation...\n");
688
+ const result = await submitEvaluation(
689
+ { projectName, description, fileStructure },
690
+ { apiKey: config.apiKey, serverUrl: config.serverUrl }
691
+ );
692
+ if (!result.success) {
693
+ console.error(`Error: ${"error" in result ? result.error : "Unknown error"}
694
+ `);
695
+ process.exit(1);
696
+ }
697
+ const { evaluation } = result;
698
+ const statusIcon = evaluation.passed ? "\u2705" : "\u274C";
699
+ const statusText = evaluation.passed ? "PASSED" : "FAILED";
700
+ console.log(` Result: ${statusIcon} ${statusText}`);
701
+ console.log(` Total Score: ${evaluation.totalScore}/100`);
702
+ console.log("");
703
+ console.log(" Rubric Scores:");
704
+ console.log(` Functionality: ${evaluation.rubricFunctionality}/50`);
705
+ console.log(` Practicality: ${evaluation.rubricPracticality}/50`);
706
+ console.log("");
707
+ if (evaluation.feedback) {
708
+ console.log(" Feedback:");
709
+ const lines = evaluation.feedback.split("\n");
710
+ for (const line of lines) {
711
+ console.log(` ${line}`);
712
+ }
713
+ console.log("");
714
+ }
715
+ }
716
+ function formatNumber(n) {
717
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
718
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
719
+ return n.toString();
720
+ }
721
+
722
+ // src/index.ts
723
+ var args = process.argv.slice(2);
724
+ var command = args[0];
725
+ function printHelp() {
726
+ console.log(`
727
+ Modu-Arena \u2014 AI Coding Tool Usage Tracker
728
+
729
+ Usage:
730
+ npx @suncreation/modu-arena <command> [options]
731
+
732
+ Commands:
733
+ install Set up hooks for detected AI coding tools
734
+ rank View your current stats and ranking
735
+ status Check configuration and installed hooks
736
+ submit Submit current project for evaluation
737
+ uninstall Remove all hooks and configuration
738
+
739
+ Options:
740
+ --api-key <key> Your Modu-Arena API key (for install)
741
+ --help, -h Show this help message
742
+ --version, -v Show version
743
+
744
+ Examples:
745
+ npx @suncreation/modu-arena install --api-key modu_arena_AbCdEfGh_xxx...
746
+ npx @suncreation/modu-arena rank
747
+ npx @suncreation/modu-arena status
748
+ `);
749
+ }
750
+ async function main() {
751
+ if (!command || command === "--help" || command === "-h") {
752
+ printHelp();
753
+ process.exit(0);
754
+ }
755
+ if (command === "--version" || command === "-v") {
756
+ console.log("0.1.0");
757
+ process.exit(0);
758
+ }
759
+ switch (command) {
760
+ case "install": {
761
+ const keyIndex = args.indexOf("--api-key");
762
+ const apiKey = keyIndex >= 0 ? args[keyIndex + 1] : void 0;
763
+ await installCommand(apiKey);
764
+ break;
765
+ }
766
+ case "rank":
767
+ await rankCommand();
768
+ break;
769
+ case "status":
770
+ statusCommand();
771
+ break;
772
+ case "submit":
773
+ await submitCommand();
774
+ break;
775
+ case "uninstall":
776
+ uninstallCommand();
777
+ break;
778
+ default:
779
+ console.error(`Unknown command: ${command}`);
780
+ printHelp();
781
+ process.exit(1);
782
+ }
783
+ }
784
+ main().catch((err) => {
785
+ console.error("Fatal error:", err);
786
+ process.exit(1);
787
+ });
788
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands.ts","../src/adapters.ts","../src/constants.ts","../src/crypto.ts","../src/api.ts","../src/config.ts","../src/index.ts"],"sourcesContent":["/**\n * CLI Commands — install, rank, status, uninstall\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { basename, join } from 'node:path';\nimport { getAllAdapters, type InstallResult } from './adapters.js';\nimport { getRank, submitEvaluation } from './api.js';\nimport { loadConfig, saveConfig, requireConfig } from './config.js';\nimport { API_BASE_URL, TOOL_DISPLAY_NAMES, type ToolType } from './constants.js';\n\n// ─── install ───────────────────────────────────────────────────────────────\n\nexport async function installCommand(apiKey?: string): Promise<void> {\n console.log('\\n🔧 Modu-Arena — AI Coding Tool Usage Tracker\\n');\n\n // Check if already configured\n const existing = loadConfig();\n if (existing?.apiKey && !apiKey) {\n console.log('✓ Already configured.');\n console.log(' Use --api-key <key> to update your API key.\\n');\n apiKey = existing.apiKey;\n }\n\n if (!apiKey) {\n console.error(\n 'Error: API key required.\\n' +\n ' Get your API key from the Modu-Arena dashboard.\\n' +\n ' Usage: npx @suncreation/modu-arena install --api-key <your-api-key>\\n',\n );\n process.exit(1);\n }\n\n // Validate API key format\n if (!apiKey.startsWith('modu_arena_')) {\n console.error(\n 'Error: Invalid API key format. Key must start with \"modu_arena_\".\\n',\n );\n process.exit(1);\n }\n\n // Save config\n saveConfig({ apiKey });\n console.log('✓ API key saved to ~/.modu-arena.json\\n');\n\n // Detect and install hooks for each tool\n const adapters = getAllAdapters();\n const results: { tool: string; result: InstallResult }[] = [];\n\n console.log('Detecting AI coding tools...\\n');\n\n for (const adapter of adapters) {\n const detected = adapter.detect();\n if (detected) {\n console.log(` ✓ ${adapter.displayName} detected`);\n const result = adapter.install(apiKey);\n results.push({ tool: adapter.displayName, result });\n if (result.success) {\n console.log(` → Hook installed: ${result.hookPath}`);\n } else {\n console.log(` ✗ ${result.message}`);\n }\n } else {\n console.log(` - ${adapter.displayName} not found`);\n }\n }\n\n const installed = results.filter((r) => r.result.success);\n console.log(\n `\\n✓ Setup complete. ${installed.length} tool(s) configured.\\n`,\n );\n\n if (installed.length === 0) {\n console.log(\n 'No AI coding tools detected. Install one of the supported tools:\\n' +\n ' • Claude Code (https://docs.anthropic.com/s/claude-code)\\n' +\n ' • OpenCode (https://opencode.ai)\\n' +\n ' • Gemini CLI (https://github.com/google-gemini/gemini-cli)\\n' +\n ' • Codex CLI (https://github.com/openai/codex)\\n' +\n ' • Crush (https://charm.sh/crush)\\n',\n );\n }\n}\n\n// ─── rank ──────────────────────────────────────────────────────────────────\n\nexport async function rankCommand(): Promise<void> {\n const config = requireConfig();\n console.log('\\n📊 Modu-Arena — Your Stats\\n');\n\n const result = await getRank({ apiKey: config.apiKey, serverUrl: config.serverUrl });\n\n if (!result.success) {\n console.error(`Error: ${'error' in result ? result.error : 'Unknown error'}\\n`);\n process.exit(1);\n }\n\n if (!('data' in result) || !result.data) {\n console.error('Error: Unexpected response format.\\n');\n process.exit(1);\n }\n\n const { username, usage, overview } = result.data;\n\n console.log(` User: ${username}`);\n console.log(` Tokens: ${formatNumber(usage.totalTokens)}`);\n console.log(` Sessions: ${usage.totalSessions}`);\n console.log(` Projects: ${overview.successfulProjectsCount}`);\n console.log('');\n\n // Tool breakdown\n if (usage.toolBreakdown.length > 0) {\n console.log(' Tool Breakdown:');\n for (const entry of usage.toolBreakdown) {\n const name = TOOL_DISPLAY_NAMES[entry.tool as ToolType] || entry.tool;\n console.log(\n ` ${name}: ${formatNumber(entry.tokens)} tokens`,\n );\n }\n console.log('');\n }\n\n // Period stats (aggregate from daily arrays)\n const sum7 = usage.last7Days.reduce(\n (acc, d) => ({ tokens: acc.tokens + d.inputTokens + d.outputTokens, sessions: acc.sessions + d.sessions }),\n { tokens: 0, sessions: 0 },\n );\n const sum30 = usage.last30Days.reduce(\n (acc, d) => ({ tokens: acc.tokens + d.inputTokens + d.outputTokens, sessions: acc.sessions + d.sessions }),\n { tokens: 0, sessions: 0 },\n );\n console.log(\n ` Last 7 days: ${formatNumber(sum7.tokens)} tokens, ${sum7.sessions} sessions`,\n );\n console.log(\n ` Last 30 days: ${formatNumber(sum30.tokens)} tokens, ${sum30.sessions} sessions`,\n );\n console.log('');\n}\n\n// ─── status ────────────────────────────────────────────────────────────────\n\nexport function statusCommand(): void {\n const config = loadConfig();\n console.log('\\n🔍 Modu-Arena — Status\\n');\n\n if (!config?.apiKey) {\n console.log(' Status: Not configured');\n console.log(\n ' Run `npx @suncreation/modu-arena install --api-key <key>` to set up.\\n',\n );\n return;\n }\n\n const maskedKey =\n config.apiKey.slice(0, 15) + '...' + config.apiKey.slice(-4);\n console.log(` API Key: ${maskedKey}`);\n console.log(` Server: ${config.serverUrl || API_BASE_URL}`);\n console.log('');\n\n // Check installed hooks\n const adapters = getAllAdapters();\n console.log(' Installed Hooks:');\n let hookCount = 0;\n for (const adapter of adapters) {\n const detected = adapter.detect();\n if (detected) {\n const hookExists = existsSync(adapter.getHookPath());\n const status = hookExists ? '✓ Active' : '✗ Not installed';\n console.log(` ${adapter.displayName}: ${status}`);\n if (hookExists) hookCount++;\n }\n }\n if (hookCount === 0) {\n console.log(' (none)');\n }\n console.log('');\n}\n\n// ─── uninstall ─────────────────────────────────────────────────────────────\n\nexport function uninstallCommand(): void {\n console.log('\\n🗑️ Modu-Arena — Uninstall\\n');\n\n // Remove hooks\n const adapters = getAllAdapters();\n for (const adapter of adapters) {\n const hookPath = adapter.getHookPath();\n if (existsSync(hookPath)) {\n unlinkSync(hookPath);\n console.log(` ✓ Removed ${adapter.displayName} hook`);\n }\n }\n\n // Remove config\n const configPath = join(homedir(), '.modu-arena.json');\n if (existsSync(configPath)) {\n unlinkSync(configPath);\n console.log(' ✓ Removed ~/.modu-arena.json');\n }\n\n console.log('\\n✓ Modu-Arena uninstalled.\\n');\n}\n\n// ─── submit ─────────────────────────────────────────────────────────────────\n\nconst IGNORE_DIRS = new Set([\n 'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',\n '.cache', '.turbo', '.vercel', '__pycache__', '.svelte-kit',\n 'coverage', '.output', '.parcel-cache',\n]);\n\nfunction collectFileStructure(\n dir: string,\n maxDepth: number,\n currentDepth = 0,\n): Record<string, string[]> {\n const result: Record<string, string[]> = {};\n if (currentDepth >= maxDepth) return result;\n\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return result;\n }\n\n const files: string[] = [];\n for (const entry of entries) {\n if (entry.startsWith('.') && IGNORE_DIRS.has(entry)) continue;\n if (IGNORE_DIRS.has(entry)) continue;\n\n const fullPath = join(dir, entry);\n let stat;\n try {\n stat = statSync(fullPath);\n } catch {\n continue;\n }\n\n if (stat.isDirectory()) {\n const sub = collectFileStructure(fullPath, maxDepth, currentDepth + 1);\n for (const [key, val] of Object.entries(sub)) {\n result[key] = val;\n }\n } else {\n files.push(entry);\n }\n }\n\n if (files.length > 0) {\n const relDir = currentDepth === 0 ? '.' : dir.split('/').slice(-(currentDepth)).join('/');\n result[relDir] = files;\n }\n\n return result;\n}\n\nexport async function submitCommand(): Promise<void> {\n const config = requireConfig();\n console.log('\\n🚀 Modu-Arena — Project Submit\\n');\n\n const cwd = process.cwd();\n const projectName = basename(cwd);\n\n const readmePath = join(cwd, 'README.md');\n if (!existsSync(readmePath)) {\n console.error('Error: README.md not found in the current directory.');\n console.error(' Please create a README.md describing your project.\\n');\n process.exit(1);\n }\n\n const description = readFileSync(readmePath, 'utf-8');\n if (description.trim().length === 0) {\n console.error('Error: README.md is empty.\\n');\n process.exit(1);\n }\n\n console.log(` Project: ${projectName}`);\n console.log(` README: ${readmePath}`);\n console.log('');\n console.log(' Collecting file structure...');\n\n const fileStructure = collectFileStructure(cwd, 3);\n const fileCount = Object.values(fileStructure).reduce((sum, files) => sum + files.length, 0);\n console.log(` Found ${fileCount} file(s) in ${Object.keys(fileStructure).length} director${Object.keys(fileStructure).length === 1 ? 'y' : 'ies'}`);\n console.log('');\n console.log(' Submitting for evaluation...\\n');\n\n const result = await submitEvaluation(\n { projectName, description, fileStructure },\n { apiKey: config.apiKey, serverUrl: config.serverUrl },\n );\n\n if (!result.success) {\n console.error(`Error: ${'error' in result ? result.error : 'Unknown error'}\\n`);\n process.exit(1);\n }\n\n const { evaluation } = result;\n const statusIcon = evaluation.passed ? '✅' : '❌';\n const statusText = evaluation.passed ? 'PASSED' : 'FAILED';\n\n console.log(` Result: ${statusIcon} ${statusText}`);\n console.log(` Total Score: ${evaluation.totalScore}/100`);\n console.log('');\n console.log(' Rubric Scores:');\n console.log(` Functionality: ${evaluation.rubricFunctionality}/50`);\n console.log(` Practicality: ${evaluation.rubricPracticality}/50`);\n console.log('');\n\n if (evaluation.feedback) {\n console.log(' Feedback:');\n const lines = evaluation.feedback.split('\\n');\n for (const line of lines) {\n console.log(` ${line}`);\n }\n console.log('');\n }\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────────\n\nfunction formatNumber(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;\n return n.toString();\n}\n","/**\n * Tool Adapters — Hook installation for each supported AI coding tool.\n *\n * Each adapter knows how to:\n * 1. Detect if the tool is installed\n * 2. Install a session-end hook to capture token usage\n * 3. Parse session data from tool-specific formats\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { API_BASE_URL, type ToolType } from './constants.js';\n\nexport interface ToolAdapter {\n slug: ToolType;\n displayName: string;\n detect(): boolean;\n install(apiKey: string): InstallResult;\n getHookPath(): string;\n}\n\nexport interface InstallResult {\n success: boolean;\n message: string;\n hookPath?: string;\n}\n\n// ─── Claude Code Adapter ───────────────────────────────────────────────────\n\nclass ClaudeCodeAdapter implements ToolAdapter {\n slug = 'claude-code' as const;\n displayName = 'Claude Code';\n\n private get configDir(): string {\n return join(homedir(), '.claude');\n }\n\n private get hooksDir(): string {\n return join(this.configDir, 'hooks');\n }\n\n getHookPath(): string {\n return join(this.hooksDir, 'session-end.sh');\n }\n\n detect(): boolean {\n // Check for ~/.claude directory or claude binary\n return existsSync(this.configDir);\n }\n\n install(apiKey: string): InstallResult {\n try {\n if (!existsSync(this.hooksDir)) {\n mkdirSync(this.hooksDir, { recursive: true });\n }\n\n const hookScript = this.generateHookScript(apiKey);\n const hookPath = this.getHookPath();\n\n writeFileSync(hookPath, hookScript, { mode: 0o755 });\n\n return {\n success: true,\n message: `Claude Code hook installed at ${hookPath}`,\n hookPath,\n };\n } catch (err) {\n return {\n success: false,\n message: `Failed to install Claude Code hook: ${err}`,\n };\n }\n }\n\n private generateHookScript(apiKey: string): string {\n return `#!/bin/bash\n# Modu-Arena: Claude Code session tracking hook\n# Auto-generated — do not edit manually\n\nMODU_API_KEY=\"${apiKey}\"\nMODU_SERVER=\"${API_BASE_URL}\"\n\n# Claude Code passes session data via environment variables\n# This hook fires at session end\nif [ -n \"$CLAUDE_SESSION_ID\" ]; then\n SESSION_DATA=$(cat <<EOF\n{\n \"toolType\": \"claude-code\",\n \"sessionId\": \"$CLAUDE_SESSION_ID\",\n \"startedAt\": \"$CLAUDE_SESSION_STARTED_AT\",\n \"endedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\",\n \"inputTokens\": \\${CLAUDE_INPUT_TOKENS:-0},\n \"outputTokens\": \\${CLAUDE_OUTPUT_TOKENS:-0},\n \"cacheCreationTokens\": \\${CLAUDE_CACHE_CREATION_TOKENS:-0},\n \"cacheReadTokens\": \\${CLAUDE_CACHE_READ_TOKENS:-0},\n \"modelName\": \"\\${CLAUDE_MODEL:-unknown}\"\n}\nEOF\n)\n\n TIMESTAMP=$(date +%s)\n MESSAGE=\"\\${TIMESTAMP}:\\${SESSION_DATA}\"\n SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$MODU_API_KEY\" | sed 's/.*= //')\n\n curl -s -X POST \"\\${MODU_SERVER}/api/v1/sessions\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"X-API-Key: \\${MODU_API_KEY}\" \\\\\n -H \"X-Timestamp: \\${TIMESTAMP}\" \\\\\n -H \"X-Signature: \\${SIGNATURE}\" \\\\\n -d \"\\${SESSION_DATA}\" > /dev/null 2>&1 &\nfi\n`;\n }\n}\n\n// ─── OpenCode Adapter ──────────────────────────────────────────────────────\n\nclass OpenCodeAdapter implements ToolAdapter {\n slug = 'opencode' as const;\n displayName = 'OpenCode';\n\n private get configDir(): string {\n return join(homedir(), '.opencode');\n }\n\n getHookPath(): string {\n return join(this.configDir, 'hooks', 'session-end.sh');\n }\n\n detect(): boolean {\n return existsSync(this.configDir);\n }\n\n install(apiKey: string): InstallResult {\n try {\n const hooksDir = join(this.configDir, 'hooks');\n if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });\n\n const hookScript = this.generateHookScript(apiKey);\n const hookPath = this.getHookPath();\n writeFileSync(hookPath, hookScript, { mode: 0o755 });\n\n return {\n success: true,\n message: `OpenCode hook installed at ${hookPath}`,\n hookPath,\n };\n } catch (err) {\n return {\n success: false,\n message: `Failed to install OpenCode hook: ${err}`,\n };\n }\n }\n\n private generateHookScript(apiKey: string): string {\n return `#!/bin/bash\n# Modu-Arena: OpenCode session tracking hook\n# Auto-generated — do not edit manually\n\nMODU_API_KEY=\"${apiKey}\"\nMODU_SERVER=\"${API_BASE_URL}\"\n\nif [ -n \"$OPENCODE_SESSION_ID\" ]; then\n SESSION_DATA=$(cat <<EOF\n{\n \"toolType\": \"opencode\",\n \"sessionId\": \"$OPENCODE_SESSION_ID\",\n \"startedAt\": \"$OPENCODE_SESSION_STARTED_AT\",\n \"endedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\",\n \"inputTokens\": \\${OPENCODE_INPUT_TOKENS:-0},\n \"outputTokens\": \\${OPENCODE_OUTPUT_TOKENS:-0},\n \"modelName\": \"\\${OPENCODE_MODEL:-unknown}\"\n}\nEOF\n)\n\n TIMESTAMP=$(date +%s)\n MESSAGE=\"\\${TIMESTAMP}:\\${SESSION_DATA}\"\n SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$MODU_API_KEY\" | sed 's/.*= //')\n\n curl -s -X POST \"\\${MODU_SERVER}/api/v1/sessions\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"X-API-Key: \\${MODU_API_KEY}\" \\\\\n -H \"X-Timestamp: \\${TIMESTAMP}\" \\\\\n -H \"X-Signature: \\${SIGNATURE}\" \\\\\n -d \"\\${SESSION_DATA}\" > /dev/null 2>&1 &\nfi\n`;\n }\n}\n\n// ─── Gemini CLI Adapter ────────────────────────────────────────────────────\n\nclass GeminiAdapter implements ToolAdapter {\n slug = 'gemini' as const;\n displayName = 'Gemini CLI';\n\n private get configDir(): string {\n return join(homedir(), '.gemini');\n }\n\n getHookPath(): string {\n return join(this.configDir, 'hooks', 'session-end.sh');\n }\n\n detect(): boolean {\n return existsSync(this.configDir);\n }\n\n install(apiKey: string): InstallResult {\n try {\n const hooksDir = join(this.configDir, 'hooks');\n if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });\n\n const hookScript = this.generateHookScript(apiKey);\n const hookPath = this.getHookPath();\n writeFileSync(hookPath, hookScript, { mode: 0o755 });\n\n return {\n success: true,\n message: `Gemini CLI hook installed at ${hookPath}`,\n hookPath,\n };\n } catch (err) {\n return {\n success: false,\n message: `Failed to install Gemini CLI hook: ${err}`,\n };\n }\n }\n\n private generateHookScript(apiKey: string): string {\n return `#!/bin/bash\n# Modu-Arena: Gemini CLI session tracking hook\n# Auto-generated — do not edit manually\n\nMODU_API_KEY=\"${apiKey}\"\nMODU_SERVER=\"${API_BASE_URL}\"\n\nif [ -n \"$GEMINI_SESSION_ID\" ]; then\n SESSION_DATA=$(cat <<EOF\n{\n \"toolType\": \"gemini\",\n \"sessionId\": \"$GEMINI_SESSION_ID\",\n \"startedAt\": \"$GEMINI_SESSION_STARTED_AT\",\n \"endedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\",\n \"inputTokens\": \\${GEMINI_INPUT_TOKENS:-0},\n \"outputTokens\": \\${GEMINI_OUTPUT_TOKENS:-0},\n \"modelName\": \"\\${GEMINI_MODEL:-unknown}\"\n}\nEOF\n)\n\n TIMESTAMP=$(date +%s)\n MESSAGE=\"\\${TIMESTAMP}:\\${SESSION_DATA}\"\n SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$MODU_API_KEY\" | sed 's/.*= //')\n\n curl -s -X POST \"\\${MODU_SERVER}/api/v1/sessions\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"X-API-Key: \\${MODU_API_KEY}\" \\\\\n -H \"X-Timestamp: \\${TIMESTAMP}\" \\\\\n -H \"X-Signature: \\${SIGNATURE}\" \\\\\n -d \"\\${SESSION_DATA}\" > /dev/null 2>&1 &\nfi\n`;\n }\n}\n\n// ─── Codex CLI Adapter ─────────────────────────────────────────────────────\n\nclass CodexAdapter implements ToolAdapter {\n slug = 'codex' as const;\n displayName = 'Codex CLI';\n\n private get configDir(): string {\n return join(homedir(), '.codex');\n }\n\n getHookPath(): string {\n return join(this.configDir, 'hooks', 'session-end.sh');\n }\n\n detect(): boolean {\n return existsSync(this.configDir);\n }\n\n install(apiKey: string): InstallResult {\n try {\n const hooksDir = join(this.configDir, 'hooks');\n if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });\n\n const hookScript = this.generateHookScript(apiKey);\n const hookPath = this.getHookPath();\n writeFileSync(hookPath, hookScript, { mode: 0o755 });\n\n return {\n success: true,\n message: `Codex CLI hook installed at ${hookPath}`,\n hookPath,\n };\n } catch (err) {\n return {\n success: false,\n message: `Failed to install Codex CLI hook: ${err}`,\n };\n }\n }\n\n private generateHookScript(apiKey: string): string {\n return `#!/bin/bash\n# Modu-Arena: Codex CLI session tracking hook\n# Auto-generated — do not edit manually\n\nMODU_API_KEY=\"${apiKey}\"\nMODU_SERVER=\"${API_BASE_URL}\"\n\nif [ -n \"$CODEX_SESSION_ID\" ]; then\n SESSION_DATA=$(cat <<EOF\n{\n \"toolType\": \"codex\",\n \"sessionId\": \"$CODEX_SESSION_ID\",\n \"startedAt\": \"$CODEX_SESSION_STARTED_AT\",\n \"endedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\",\n \"inputTokens\": \\${CODEX_INPUT_TOKENS:-0},\n \"outputTokens\": \\${CODEX_OUTPUT_TOKENS:-0},\n \"modelName\": \"\\${CODEX_MODEL:-unknown}\"\n}\nEOF\n)\n\n TIMESTAMP=$(date +%s)\n MESSAGE=\"\\${TIMESTAMP}:\\${SESSION_DATA}\"\n SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$MODU_API_KEY\" | sed 's/.*= //')\n\n curl -s -X POST \"\\${MODU_SERVER}/api/v1/sessions\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"X-API-Key: \\${MODU_API_KEY}\" \\\\\n -H \"X-Timestamp: \\${TIMESTAMP}\" \\\\\n -H \"X-Signature: \\${SIGNATURE}\" \\\\\n -d \"\\${SESSION_DATA}\" > /dev/null 2>&1 &\nfi\n`;\n }\n}\n\n// ─── Crush Adapter ─────────────────────────────────────────────────────────\n\nclass CrushAdapter implements ToolAdapter {\n slug = 'crush' as const;\n displayName = 'Crush';\n\n private get configDir(): string {\n return join(homedir(), '.crush');\n }\n\n getHookPath(): string {\n return join(this.configDir, 'hooks', 'session-end.sh');\n }\n\n detect(): boolean {\n return existsSync(this.configDir);\n }\n\n install(apiKey: string): InstallResult {\n try {\n const hooksDir = join(this.configDir, 'hooks');\n if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });\n\n const hookScript = this.generateHookScript(apiKey);\n const hookPath = this.getHookPath();\n writeFileSync(hookPath, hookScript, { mode: 0o755 });\n\n return {\n success: true,\n message: `Crush hook installed at ${hookPath}`,\n hookPath,\n };\n } catch (err) {\n return {\n success: false,\n message: `Failed to install Crush hook: ${err}`,\n };\n }\n }\n\n private generateHookScript(apiKey: string): string {\n return `#!/bin/bash\n# Modu-Arena: Crush session tracking hook\n# Auto-generated — do not edit manually\n\nMODU_API_KEY=\"${apiKey}\"\nMODU_SERVER=\"${API_BASE_URL}\"\n\nif [ -n \"$CRUSH_SESSION_ID\" ]; then\n SESSION_DATA=$(cat <<EOF\n{\n \"toolType\": \"crush\",\n \"sessionId\": \"$CRUSH_SESSION_ID\",\n \"startedAt\": \"$CRUSH_SESSION_STARTED_AT\",\n \"endedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\",\n \"inputTokens\": \\${CRUSH_INPUT_TOKENS:-0},\n \"outputTokens\": \\${CRUSH_OUTPUT_TOKENS:-0},\n \"modelName\": \"\\${CRUSH_MODEL:-unknown}\"\n}\nEOF\n)\n\n TIMESTAMP=$(date +%s)\n MESSAGE=\"\\${TIMESTAMP}:\\${SESSION_DATA}\"\n SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$MODU_API_KEY\" | sed 's/.*= //')\n\n curl -s -X POST \"\\${MODU_SERVER}/api/v1/sessions\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"X-API-Key: \\${MODU_API_KEY}\" \\\\\n -H \"X-Timestamp: \\${TIMESTAMP}\" \\\\\n -H \"X-Signature: \\${SIGNATURE}\" \\\\\n -d \"\\${SESSION_DATA}\" > /dev/null 2>&1 &\nfi\n`;\n }\n}\n\n// ─── Registry ──────────────────────────────────────────────────────────────\n\nexport function getAllAdapters(): ToolAdapter[] {\n return [\n new ClaudeCodeAdapter(),\n new OpenCodeAdapter(),\n new GeminiAdapter(),\n new CodexAdapter(),\n new CrushAdapter(),\n ];\n}\n\nexport function getAdapter(slug: string): ToolAdapter | undefined {\n return getAllAdapters().find((a) => a.slug === slug);\n}\n","/** Base URL for the Modu-Arena API server */\nexport const API_BASE_URL =\n process.env.MODU_ARENA_API_URL ?? 'http://localhost:8989';\n\n/** API key prefix used for all keys */\nexport const API_KEY_PREFIX = 'modu_arena_';\n\n/** Supported AI coding tools */\nexport const TOOL_TYPES = [\n 'claude-code',\n 'opencode',\n 'gemini',\n 'codex',\n 'crush',\n] as const;\n\nexport type ToolType = (typeof TOOL_TYPES)[number];\n\n/** Display names for each tool */\nexport const TOOL_DISPLAY_NAMES: Record<ToolType, string> = {\n 'claude-code': 'Claude Code',\n opencode: 'OpenCode',\n gemini: 'Gemini CLI',\n codex: 'Codex CLI',\n crush: 'Crush',\n};\n\n/** Config file name stored in user home directory */\nexport const CONFIG_FILE_NAME = '.modu-arena.json';\n\n/** Minimum interval between sessions (seconds) */\nexport const MIN_SESSION_INTERVAL_SEC = 60;\n\n/** HMAC timestamp tolerance (seconds) */\nexport const HMAC_TIMESTAMP_TOLERANCE_SEC = 300;\n","import { createHmac, createHash } from 'node:crypto';\n\n/**\n * Compute HMAC-SHA256 signature for API authentication.\n *\n * message = \"{timestamp}:{bodyJsonString}\"\n * signature = HMAC-SHA256(apiKey, message).hex()\n */\nexport function computeHmacSignature(\n apiKey: string,\n timestamp: string,\n body: string,\n): string {\n const message = `${timestamp}:${body}`;\n return createHmac('sha256', apiKey).update(message).digest('hex');\n}\n\n/**\n * Compute SHA-256 session hash for integrity verification.\n *\n * data = \"{userId}:{userSalt}:{inputTokens}:{outputTokens}:{cacheCreationTokens}:{cacheReadTokens}:{modelName}:{endedAt}\"\n * hash = SHA-256(data).hex()\n */\nexport function computeSessionHash(\n userId: string,\n userSalt: string,\n inputTokens: number,\n outputTokens: number,\n cacheCreationTokens: number,\n cacheReadTokens: number,\n modelName: string,\n endedAt: string,\n): string {\n const data = `${userId}:${userSalt}:${inputTokens}:${outputTokens}:${cacheCreationTokens}:${cacheReadTokens}:${modelName}:${endedAt}`;\n return createHash('sha256').update(data).digest('hex');\n}\n","import { computeHmacSignature } from './crypto.js';\nimport { API_BASE_URL } from './constants.js';\n\nexport interface SessionPayload {\n toolType: string;\n sessionId: string;\n startedAt: string;\n endedAt: string;\n inputTokens: number;\n outputTokens: number;\n cacheCreationTokens?: number;\n cacheReadTokens?: number;\n modelName?: string;\n codeMetrics?: Record<string, unknown> | null;\n}\n\nexport interface BatchPayload {\n sessions: SessionPayload[];\n}\n\nexport interface RankResponse {\n success: boolean;\n data: {\n username: string;\n usage: {\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheTokens: number;\n totalTokens: number;\n totalSessions: number;\n toolBreakdown: Array<{ tool: string; tokens: number }>;\n last7Days: Array<{ date: string; inputTokens: number; outputTokens: number; sessions: number }>;\n last30Days: Array<{ date: string; inputTokens: number; outputTokens: number; sessions: number }>;\n };\n overview: {\n successfulProjectsCount: number;\n };\n lastUpdated: string;\n };\n}\n\nexport interface ApiError {\n error: string;\n}\n\ninterface RequestOptions {\n apiKey: string;\n serverUrl?: string;\n}\n\nfunction baseUrl(opts: RequestOptions): string {\n return opts.serverUrl || API_BASE_URL;\n}\n\nfunction makeAuthHeaders(\n apiKey: string,\n body?: string,\n): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n };\n\n if (body !== undefined) {\n const timestamp = Math.floor(Date.now() / 1000).toString();\n const signature = computeHmacSignature(apiKey, timestamp, body);\n headers['X-Timestamp'] = timestamp;\n headers['X-Signature'] = signature;\n }\n\n return headers;\n}\n\nexport async function submitSession(\n session: SessionPayload,\n opts: RequestOptions,\n): Promise<{ success: boolean; session?: unknown; error?: string }> {\n const body = JSON.stringify(session);\n const url = `${baseUrl(opts)}/api/v1/sessions`;\n\n const res = await fetch(url, {\n method: 'POST',\n headers: makeAuthHeaders(opts.apiKey, body),\n body,\n });\n\n const data = await res.json();\n if (!res.ok) {\n return { success: false, error: (data as ApiError).error || `HTTP ${res.status}` };\n }\n return data as { success: boolean; session: unknown };\n}\n\nexport async function submitBatch(\n sessions: SessionPayload[],\n opts: RequestOptions,\n): Promise<{\n success: boolean;\n processed?: number;\n duplicatesSkipped?: number;\n error?: string;\n}> {\n const body = JSON.stringify({ sessions });\n const url = `${baseUrl(opts)}/api/v1/sessions/batch`;\n\n const res = await fetch(url, {\n method: 'POST',\n headers: makeAuthHeaders(opts.apiKey, body),\n body,\n });\n\n const data = await res.json();\n if (!res.ok) {\n return { success: false, error: (data as ApiError).error || `HTTP ${res.status}` };\n }\n return data as { success: boolean; processed: number; duplicatesSkipped: number };\n}\n\nexport async function getRank(\n opts: RequestOptions,\n): Promise<RankResponse | { success: false; error: string }> {\n const url = `${baseUrl(opts)}/api/v1/rank`;\n\n const res = await fetch(url, {\n method: 'GET',\n headers: {\n 'X-API-Key': opts.apiKey,\n },\n });\n\n const data = await res.json();\n if (!res.ok) {\n return { success: false, error: (data as ApiError).error || `HTTP ${res.status}` };\n }\n return data as RankResponse;\n}\n\n// ─── Evaluate ─────────────────────────────────────────────────────────────\n\nexport interface EvaluatePayload {\n projectName: string;\n description: string;\n fileStructure?: Record<string, string[]>;\n}\n\nexport interface EvaluationResult {\n passed: boolean;\n totalScore: number;\n rubricFunctionality: number;\n rubricPracticality: number;\n feedback: string;\n}\n\nexport interface EvaluateResponse {\n success: true;\n evaluation: EvaluationResult;\n}\n\nexport async function submitEvaluation(\n payload: EvaluatePayload,\n opts: RequestOptions,\n): Promise<EvaluateResponse | { success: false; error: string }> {\n const body = JSON.stringify(payload);\n const url = `${baseUrl(opts)}/api/v1/evaluate`;\n\n const res = await fetch(url, {\n method: 'POST',\n headers: makeAuthHeaders(opts.apiKey, body),\n body,\n });\n\n const data = await res.json();\n if (!res.ok) {\n return { success: false, error: (data as ApiError).error || `HTTP ${res.status}` };\n }\n return data as EvaluateResponse;\n}\n","import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport { CONFIG_FILE_NAME } from './constants.js';\n\nexport interface Config {\n apiKey: string;\n serverUrl?: string;\n tools?: string[];\n}\n\nfunction getConfigPath(): string {\n return join(homedir(), CONFIG_FILE_NAME);\n}\n\nexport function loadConfig(): Config | null {\n const configPath = getConfigPath();\n if (!existsSync(configPath)) return null;\n\n try {\n const raw = readFileSync(configPath, 'utf-8');\n return JSON.parse(raw) as Config;\n } catch {\n return null;\n }\n}\n\nexport function saveConfig(config: Config): void {\n const configPath = getConfigPath();\n const dir = dirname(configPath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n writeFileSync(configPath, JSON.stringify(config, null, 2) + '\\n', 'utf-8');\n}\n\nexport function requireConfig(): Config {\n const config = loadConfig();\n if (!config?.apiKey) {\n console.error(\n 'Error: Not configured. Run `npx @suncreation/modu-arena install` first.',\n );\n process.exit(1);\n }\n return config;\n}\n","/**\n * @suncreation/modu-arena CLI\n *\n * Track and rank your AI coding tool usage.\n *\n * Usage:\n * npx @suncreation/modu-arena install --api-key <key>\n * npx @suncreation/modu-arena rank\n * npx @suncreation/modu-arena status\n * npx @suncreation/modu-arena uninstall\n */\n\nimport {\n installCommand,\n rankCommand,\n statusCommand,\n submitCommand,\n uninstallCommand,\n} from './commands.js';\n\nconst args = process.argv.slice(2);\nconst command = args[0];\n\nfunction printHelp(): void {\n console.log(`\nModu-Arena — AI Coding Tool Usage Tracker\n\nUsage:\n npx @suncreation/modu-arena <command> [options]\n\nCommands:\n install Set up hooks for detected AI coding tools\n rank View your current stats and ranking\n status Check configuration and installed hooks\n submit Submit current project for evaluation\n uninstall Remove all hooks and configuration\n\nOptions:\n --api-key <key> Your Modu-Arena API key (for install)\n --help, -h Show this help message\n --version, -v Show version\n\nExamples:\n npx @suncreation/modu-arena install --api-key modu_arena_AbCdEfGh_xxx...\n npx @suncreation/modu-arena rank\n npx @suncreation/modu-arena status\n`);\n}\n\nasync function main(): Promise<void> {\n if (!command || command === '--help' || command === '-h') {\n printHelp();\n process.exit(0);\n }\n\n if (command === '--version' || command === '-v') {\n console.log('0.1.0');\n process.exit(0);\n }\n\n switch (command) {\n case 'install': {\n const keyIndex = args.indexOf('--api-key');\n const apiKey = keyIndex >= 0 ? args[keyIndex + 1] : undefined;\n await installCommand(apiKey);\n break;\n }\n case 'rank':\n await rankCommand();\n break;\n case 'status':\n statusCommand();\n break;\n case 'submit':\n await submitCommand();\n break;\n case 'uninstall':\n uninstallCommand();\n break;\n default:\n console.error(`Unknown command: ${command}`);\n printHelp();\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n"],"mappings":"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","names":["existsSync","readFileSync","homedir","join","readFileSync","writeFileSync","existsSync","mkdirSync","homedir","join","join","homedir","existsSync","readFileSync","mkdirSync","writeFileSync","existsSync","join","homedir","readFileSync"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@suncreation/modu-arena",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Track and rank your AI coding tool usage across Claude Code, OpenCode, Gemini CLI, Codex CLI, and Crush",
6
+ "bin": {
7
+ "modu-arena": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
+ "lint": "biome lint ./src",
17
+ "lint:fix": "biome lint --write ./src",
18
+ "format": "biome format --write ./src",
19
+ "type-check": "tsc --noEmit"
20
+ },
21
+ "dependencies": {},
22
+ "devDependencies": {
23
+ "@biomejs/biome": "2.3.10",
24
+ "@types/node": "^20",
25
+ "tsup": "^8.4.0",
26
+ "typescript": "^5.7.3"
27
+ },
28
+ "keywords": [
29
+ "ai",
30
+ "coding",
31
+ "claude-code",
32
+ "opencode",
33
+ "gemini",
34
+ "codex",
35
+ "crush",
36
+ "token-usage",
37
+ "leaderboard",
38
+ "ranking"
39
+ ],
40
+ "author": "suncreation",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/modulabs/modu-arena"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }