@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 +103 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +788 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|