claudeye 0.2.2 → 0.3.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.

Potentially problematic release.


This version of claudeye might be problematic. Click here for more details.

Files changed (191) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +5 -5
  4. package/.next/standalone/.next/routes-manifest.json +9 -0
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  21. package/.next/standalone/.next/server/app/_not-found.rsc +5 -5
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +5 -5
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route/app-paths-manifest.json +3 -0
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route/build-manifest.json +11 -0
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route/server-reference-manifest.json +4 -0
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +6 -0
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.map +5 -0
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -0
  34. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +2 -0
  35. package/.next/standalone/.next/server/app/icon.png/route.js +2 -1
  36. package/.next/standalone/.next/server/app/icon.png/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  38. package/.next/standalone/.next/server/app/page.js +1 -1
  39. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  42. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +35 -5
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -3
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  52. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f408c708._.js +21 -0
  53. package/.next/standalone/.next/server/chunks/[root-of-the-server]__fde83e67._.js +3 -0
  54. package/.next/standalone/.next/server/chunks/ce889_server_app_api_download_[project]_[session]_route_actions_bbdd823f.js +3 -0
  55. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_64175717.js +3 -0
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__070e2009._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0a745465._.js +3 -0
  58. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__bc37261c._.js → [root-of-the-server]__14f58da3._.js} +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__164d9311._.js +3 -0
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__2822fd21._.js +6 -0
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__31b4c2fd._.js +3 -0
  62. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__45656df2._.js → [root-of-the-server]__4e339665._.js} +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__55018089._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__6313e929._.js +3 -0
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__7e21395a._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ee388ee0._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/ssr/_0b4924bd._.js +3 -0
  68. package/.next/standalone/.next/server/chunks/ssr/{node_modules_9e089768._.js → _1404b353._.js} +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/_3d21dde5._.js +8 -0
  70. package/.next/standalone/.next/server/chunks/ssr/_fd9b1ff7._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_7769b563._.js → node_modules_next_dist_esm_eedfc1fd._.js} +2 -2
  72. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  73. package/.next/standalone/.next/server/pages/404.html +2 -2
  74. package/.next/standalone/.next/server/pages/500.html +2 -2
  75. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/server-reference-manifest.json +35 -5
  77. package/.next/standalone/.next/static/chunks/0e266948a26a3cdf.js +1 -0
  78. package/.next/standalone/.next/static/chunks/2774382cf796393c.js +4 -0
  79. package/.next/standalone/.next/static/chunks/6189ca16caad4352.js +3 -0
  80. package/.next/standalone/.next/static/chunks/8111dbe882e31821.js +1 -0
  81. package/.next/standalone/.next/static/chunks/{5a424275276f2bb9.js → bdeaeb8c9876394b.js} +1 -1
  82. package/.next/standalone/.next/static/chunks/cdbb6932218650fd.js +1 -0
  83. package/.next/standalone/.next/static/chunks/ea03555bb726c073.css +1 -0
  84. package/.next/standalone/.next/static/chunks/f091501564eb2ea3.js +4 -0
  85. package/.next/standalone/.next/static/chunks/{turbopack-2315171089e56fad.js → turbopack-fc1f23734a087d36.js} +1 -1
  86. package/.next/standalone/README.md +528 -41
  87. package/.next/standalone/app/actions/run-enrichments.ts +26 -5
  88. package/.next/standalone/app/actions/run-evals.ts +26 -5
  89. package/.next/standalone/app/actions/run-subagent-enrichments.ts +89 -0
  90. package/.next/standalone/app/actions/run-subagent-evals.ts +88 -0
  91. package/.next/standalone/app/api/download/[project]/[session]/route.ts +49 -0
  92. package/.next/standalone/app/components/copy-button.tsx +37 -0
  93. package/.next/standalone/app/components/enrichment-results-panel.tsx +33 -13
  94. package/.next/standalone/app/components/eval-results-panel.tsx +33 -13
  95. package/.next/standalone/app/components/log-viewer/entry-row.tsx +43 -14
  96. package/.next/standalone/app/components/log-viewer/queue-divider.tsx +50 -7
  97. package/.next/standalone/app/components/log-viewer/tool-input-output.tsx +13 -3
  98. package/.next/standalone/app/components/project-list.tsx +11 -11
  99. package/.next/standalone/app/components/raw-log-viewer.tsx +80 -11
  100. package/.next/standalone/app/components/refresh-button.tsx +79 -0
  101. package/.next/standalone/app/components/sessions-list.tsx +23 -14
  102. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +23 -12
  103. package/.next/standalone/bin/claudeye.mjs +112 -25
  104. package/.next/standalone/components/navbar.tsx +2 -0
  105. package/.next/standalone/dist/app.js +10 -4
  106. package/.next/standalone/dist/condition-registry.js +20 -0
  107. package/.next/standalone/dist/enrich-registry.js +26 -3
  108. package/.next/standalone/dist/enrich-runner.js +68 -13
  109. package/.next/standalone/dist/registry.js +26 -3
  110. package/.next/standalone/dist/runner.js +78 -20
  111. package/.next/standalone/dist/server-spawn.js +58 -34
  112. package/.next/standalone/lib/cache/hash.ts +67 -0
  113. package/.next/standalone/lib/cache/index.ts +9 -0
  114. package/.next/standalone/lib/cache/local-backend.ts +81 -0
  115. package/.next/standalone/lib/cache/manager.ts +127 -0
  116. package/.next/standalone/lib/cache/types.ts +19 -0
  117. package/.next/standalone/lib/evals/app.ts +30 -7
  118. package/.next/standalone/lib/evals/condition-registry.ts +26 -0
  119. package/.next/standalone/lib/evals/enrich-registry.ts +29 -3
  120. package/.next/standalone/lib/evals/enrich-runner.ts +68 -14
  121. package/.next/standalone/lib/evals/enrich-types.ts +6 -1
  122. package/.next/standalone/lib/evals/index.ts +3 -1
  123. package/.next/standalone/lib/evals/registry.ts +29 -4
  124. package/.next/standalone/lib/evals/runner.ts +77 -20
  125. package/.next/standalone/lib/evals/server-spawn.ts +67 -41
  126. package/.next/standalone/lib/evals/types.ts +16 -0
  127. package/.next/standalone/lib/log-format.ts +22 -1
  128. package/.next/standalone/package-lock.json +244 -308
  129. package/.next/standalone/package.json +1 -1
  130. package/.next/standalone/scripts/dev.ts +3 -1
  131. package/.next/standalone/scripts/parse-script-args.ts +30 -2
  132. package/.next/standalone/scripts/start.ts +3 -1
  133. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  134. package/README.md +528 -41
  135. package/bin/claudeye.mjs +112 -25
  136. package/dist/app.d.ts +17 -3
  137. package/dist/app.d.ts.map +1 -1
  138. package/dist/app.js +10 -4
  139. package/dist/app.js.map +1 -1
  140. package/dist/condition-registry.d.ts +9 -0
  141. package/dist/condition-registry.d.ts.map +1 -0
  142. package/dist/condition-registry.js +20 -0
  143. package/dist/condition-registry.js.map +1 -0
  144. package/dist/enrich-registry.d.ts +5 -1
  145. package/dist/enrich-registry.d.ts.map +1 -1
  146. package/dist/enrich-registry.js +26 -3
  147. package/dist/enrich-registry.js.map +1 -1
  148. package/dist/enrich-runner.d.ts +3 -3
  149. package/dist/enrich-runner.d.ts.map +1 -1
  150. package/dist/enrich-runner.js +68 -13
  151. package/dist/enrich-runner.js.map +1 -1
  152. package/dist/enrich-types.d.ts +6 -1
  153. package/dist/enrich-types.d.ts.map +1 -1
  154. package/dist/index.d.ts +2 -2
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/registry.d.ts +5 -2
  157. package/dist/registry.d.ts.map +1 -1
  158. package/dist/registry.js +26 -3
  159. package/dist/registry.js.map +1 -1
  160. package/dist/runner.d.ts +2 -2
  161. package/dist/runner.d.ts.map +1 -1
  162. package/dist/runner.js +78 -20
  163. package/dist/runner.js.map +1 -1
  164. package/dist/server-spawn.d.ts +2 -1
  165. package/dist/server-spawn.d.ts.map +1 -1
  166. package/dist/server-spawn.js +58 -34
  167. package/dist/server-spawn.js.map +1 -1
  168. package/dist/types.d.ts +14 -0
  169. package/dist/types.d.ts.map +1 -1
  170. package/package.json +1 -1
  171. package/.next/standalone/.next/server/chunks/[root-of-the-server]__24a1e50a._.js +0 -21
  172. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__32f115c9._.js +0 -6
  173. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__476a1712._.js +0 -3
  174. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__4ddcabf2._.js +0 -3
  175. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ad593585._.js +0 -3
  176. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__afd8e13b._.js +0 -3
  177. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__dd7ee810._.js +0 -3
  178. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ff3004de._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/ssr/_53472598._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/ssr/_863b6ca8._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/ssr/_f7347c74._.js +0 -5
  182. package/.next/standalone/.next/static/chunks/2243ff2814e7a781.js +0 -3
  183. package/.next/standalone/.next/static/chunks/50531467396cea91.css +0 -1
  184. package/.next/standalone/.next/static/chunks/8f288c01f8d7ef2d.js +0 -1
  185. package/.next/standalone/.next/static/chunks/abab1b00b2788443.js +0 -4
  186. package/.next/standalone/.next/static/chunks/d250d7f6f0a8c325.js +0 -1
  187. package/.next/standalone/.next/static/chunks/d7a572a8b7eb1ec8.js +0 -1
  188. package/.next/standalone/.next/static/chunks/fb1b0b9da3f03023.js +0 -4
  189. /package/.next/standalone/.next/static/{LoGIEEP4cORCqcFv-Ywg0 → 5JsV7rfAEOIwNOQPaX3UP}/_buildManifest.js +0 -0
  190. /package/.next/standalone/.next/static/{LoGIEEP4cORCqcFv-Ywg0 → 5JsV7rfAEOIwNOQPaX3UP}/_clientMiddlewareManifest.json +0 -0
  191. /package/.next/standalone/.next/static/{LoGIEEP4cORCqcFv-Ywg0 → 5JsV7rfAEOIwNOQPaX3UP}/_ssgManifest.js +0 -0
@@ -23,11 +23,16 @@ This starts a local server on port 8020 and opens the dashboard in your browser.
23
23
  ### Options
24
24
 
25
25
  ```
26
- --projects-path <path> Path to Claude projects directory (default: ~/.claude/projects)
27
- --port <number> Preferred port (default: 8020)
28
- --evals <path> Path to an evals file (see Custom Evals below)
29
- --no-open Don't auto-open the browser
30
- -h, --help Show help
26
+ --projects-path, -p <path> Path to Claude projects directory (default: ~/.claude/projects)
27
+ --port <number> Preferred port (default: 8020)
28
+ --host <address> Host to bind to (default: localhost)
29
+ Use 0.0.0.0 for LAN access
30
+ --evals <path> Path to an evals/enrichments file (see Custom Evals below)
31
+ --cache <on|off> Enable/disable result caching (default: on)
32
+ --cache-path <path> Custom cache directory (default: ~/.claudeye/cache)
33
+ --cache-clear Clear all cached results and exit
34
+ --no-open Don't auto-open the browser
35
+ -h, --help Show help
31
36
  ```
32
37
 
33
38
  ### Examples
@@ -38,6 +43,21 @@ claudeye --projects-path /path/to/projects
38
43
 
39
44
  # Run on a different port without opening the browser
40
45
  claudeye --port 3000 --no-open
46
+
47
+ # Bind to all interfaces for LAN access
48
+ claudeye --host 0.0.0.0
49
+
50
+ # Load custom evals and enrichments
51
+ claudeye --evals ./my-evals.js
52
+
53
+ # Disable caching
54
+ claudeye --cache off
55
+
56
+ # Use a custom cache directory
57
+ claudeye --cache-path /tmp/claudeye-cache
58
+
59
+ # Clear cached results
60
+ claudeye --cache-clear
41
61
  ```
42
62
 
43
63
  ## Features
@@ -60,6 +80,7 @@ Open a session to get a full breakdown of the agent execution:
60
80
  - **Subagent logs** — nested subagent executions load on demand
61
81
  - **Thinking blocks** — collapsible reasoning traces
62
82
  - **Virtual scrolling** — handles large logs without performance issues
83
+ - **Download** — export session logs as JSONL files
63
84
 
64
85
  ### Filtering
65
86
 
@@ -67,10 +88,73 @@ Open a session to get a full breakdown of the agent execution:
67
88
  - **Keyword search**: add multiple keywords as chips, AND logic, case-insensitive
68
89
  - Filters combine — both time and keyword conditions must match
69
90
 
91
+ ### Auto-Refresh
92
+
93
+ The navbar includes a refresh button with an auto-refresh dropdown (Off, 5s, 10s, 30s). It works globally across all pages — projects, sessions, and log views all refresh when triggered.
94
+
70
95
  ### Theme Support
71
96
 
72
97
  Light and dark mode with system preference detection. Toggle from the navbar. Preference is saved to `localStorage`.
73
98
 
99
+ ---
100
+
101
+ ## Caching
102
+
103
+ Claudeye caches eval and enrichment results to avoid recomputing them on every page load. Caching is **on by default** and requires zero configuration.
104
+
105
+ ### How It Works
106
+
107
+ - Results are cached to `~/.claudeye/cache/` as JSON files, keyed by project and session
108
+ - Cache invalidation is **content-hash-based**, not TTL-based:
109
+ - The session file's modification time and size are hashed (fast, no full file read)
110
+ - The evals module file content is hashed
111
+ - The set of registered eval/enricher names is tracked
112
+ - A cache hit is only valid when **all three** match — if the session log grows, the evals file changes, or evals are added/removed, the cache is automatically invalidated
113
+ - Clicking **Re-run** in the dashboard always bypasses the cache and runs fresh computation
114
+ - Cached results show a subtle "cached" badge next to the duration in the results panel
115
+
116
+ ### Running Without Cache
117
+
118
+ ```bash
119
+ # Disable caching entirely
120
+ claudeye --cache off
121
+
122
+ # Or during development
123
+ npm run dev -- --cache off
124
+ ```
125
+
126
+ When caching is disabled, no cache files are created or read. The startup log will show `Cache: disabled`.
127
+
128
+ ### Custom Cache Location
129
+
130
+ ```bash
131
+ claudeye --cache-path /path/to/custom/cache
132
+ ```
133
+
134
+ ### Clearing the Cache
135
+
136
+ ```bash
137
+ # Clear the default cache directory and exit
138
+ claudeye --cache-clear
139
+
140
+ # Clear a custom cache directory
141
+ claudeye --cache-clear --cache-path /path/to/custom/cache
142
+ ```
143
+
144
+ ### Startup Log
145
+
146
+ When the server starts, the cache status is shown alongside other configuration:
147
+
148
+ ```
149
+ Starting Claudeye dashboard...
150
+ Projects: /home/user/.claude/projects
151
+ Evals: /home/user/my-evals.js
152
+ Cache: local (/home/user/.claudeye/cache/)
153
+ URL: http://localhost:8020
154
+ ```
155
+
156
+ ---
157
+
74
158
  ## Custom Evals & Enrichments
75
159
 
76
160
  Claudeye supports two types of custom extensions you can define in a single JS/TS file and load with the `--evals` flag:
@@ -165,107 +249,510 @@ node my-evals.js
165
249
 
166
250
  This spawns the Claudeye dashboard as a child process. When the same file is loaded via `--evals`, `listen()` automatically becomes a no-op so you won't get a duplicate server.
167
251
 
168
- ### API Reference
252
+ ---
253
+
254
+ ## Conditional Evals & Enrichments
255
+
256
+ Conditions let you control when evals and enrichments run. There are two levels:
257
+
258
+ 1. **Global condition** — gates _all_ evals and enrichments at once
259
+ 2. **Per-eval / per-enrichment conditions** — gate individual functions
169
260
 
170
- #### `createApp()`
261
+ ### Global Condition
171
262
 
172
- Returns a `ClaudeyeApp` instance.
263
+ Use `app.condition()` to set a global condition. If it returns `false` (or throws), every registered eval and enrichment is skipped.
264
+
265
+ ```js
266
+ import { createApp } from 'claudeye';
267
+
268
+ const app = createApp();
173
269
 
174
- #### `app.eval(name, fn)`
270
+ // Only run evals/enrichments for sessions with actual content
271
+ app.condition(({ entries }) => entries.length > 0);
175
272
 
176
- Register an eval function. Chainable.
273
+ // Only run for non-test projects
274
+ app.condition(({ projectName }) => !projectName.includes('test'));
275
+ ```
276
+
277
+ The global condition receives the same `EvalContext` as evals and enrichments, so you can inspect entries, stats, project name, and session ID.
278
+
279
+ > **Note:** Calling `app.condition()` multiple times replaces the previous condition — only the last one is active.
280
+
281
+ ### Per-Eval Conditions
282
+
283
+ Pass a `condition` function in the options object (third argument) to `app.eval()`:
284
+
285
+ ```js
286
+ app.eval('budget-check',
287
+ ({ stats }) => ({
288
+ pass: stats.toolCallCount <= 50,
289
+ score: 1 - Math.min(stats.toolCallCount / 100, 1),
290
+ message: `${stats.toolCallCount} tool call(s)`,
291
+ }),
292
+ {
293
+ condition: ({ stats }) => stats.toolCallCount > 0,
294
+ }
295
+ );
296
+ ```
297
+
298
+ If the condition returns `false`, the eval is marked as **skipped** in the results panel. If the condition throws, the eval is marked as **errored** with the message `Condition error: <message>`.
299
+
300
+ ### Per-Enrichment Conditions
301
+
302
+ Same pattern — pass a `condition` in the options to `app.enrich()`:
303
+
304
+ ```js
305
+ app.enrich('error-analysis',
306
+ ({ entries }) => {
307
+ const errors = entries.filter(e => e.raw?.is_error === true);
308
+ return {
309
+ 'Total Errors': errors.length,
310
+ 'Error Rate': `${((errors.length / entries.length) * 100).toFixed(1)}%`,
311
+ };
312
+ },
313
+ {
314
+ condition: ({ entries }) => entries.some(e => e.raw?.is_error === true),
315
+ }
316
+ );
317
+ ```
318
+
319
+ ### Evaluation Order
320
+
321
+ When a session is loaded, conditions are evaluated in this order:
322
+
323
+ ```
324
+ 1. Global condition checked
325
+ ├─ Returns false or throws → ALL evals/enrichments marked "skipped"
326
+ └─ Returns true → proceed to step 2
327
+
328
+ 2. For each eval/enrichment:
329
+ ├─ Has per-item condition?
330
+ │ ├─ Returns false → that item marked "skipped"
331
+ │ ├─ Throws → that item marked "errored" (not skipped)
332
+ │ └─ Returns true → run the function
333
+ └─ No condition → run the function
334
+
335
+ 3. Function executes
336
+ ├─ Returns result → recorded normally
337
+ └─ Throws → marked "errored", other items still run
338
+ ```
339
+
340
+ ### Condition Function Signature
341
+
342
+ Both global and per-item conditions use the same type:
343
+
344
+ ```ts
345
+ type ConditionFunction = (context: EvalContext) => boolean | Promise<boolean>;
346
+ ```
347
+
348
+ Conditions can be async — the runner awaits them.
349
+
350
+ ### Combining Global and Per-Item Conditions
351
+
352
+ Global and per-item conditions stack. The global condition runs first; if it passes, per-item conditions are checked individually:
353
+
354
+ ```js
355
+ const app = createApp();
356
+
357
+ // Global: skip everything for empty sessions
358
+ app.condition(({ entries }) => entries.length > 0);
359
+
360
+ // Per-eval: only check turn count for sessions with tool calls
361
+ app.eval('efficient-tools',
362
+ ({ stats }) => ({
363
+ pass: stats.toolCallCount <= stats.turnCount * 2,
364
+ score: Math.max(0, 1 - (stats.toolCallCount / (stats.turnCount * 4))),
365
+ }),
366
+ { condition: ({ stats }) => stats.toolCallCount > 0 }
367
+ );
368
+
369
+ // Per-enrichment: only compute cost for sessions that used a model
370
+ app.enrich('cost',
371
+ ({ entries }) => {
372
+ const tokens = entries.reduce((s, e) => s + (e.raw?.usage?.total_tokens || 0), 0);
373
+ return { 'Total Tokens': tokens, 'Est. Cost': `$${(tokens * 0.00001).toFixed(4)}` };
374
+ },
375
+ { condition: ({ stats }) => stats.models.length > 0 }
376
+ );
377
+ ```
378
+
379
+ ### UI Behavior
380
+
381
+ In the dashboard, conditional results appear as follows:
382
+
383
+ | Status | Evals Panel | Enrichments Panel |
384
+ |--------|-------------|-------------------|
385
+ | **Skipped** | Grayed-out row with "skipped" label | Grayed-out row with "skipped" label |
386
+ | **Condition error** | Row with warning icon and error message | Row with warning icon and error message |
387
+ | **Passed / Data** | Green check with score bar | Key-value pairs grouped by enricher |
388
+ | **Failed** | Red X with score bar | N/A |
389
+
390
+ Skipped items are counted separately in the summary bar (e.g. "2 passed, 1 skipped").
391
+
392
+ ### Full Example with Conditions
393
+
394
+ ```js
395
+ import { createApp } from 'claudeye';
396
+
397
+ const app = createApp();
398
+
399
+ // ── Global condition: require at least one user message ──
400
+ app.condition(({ entries }) =>
401
+ entries.some(e => e.type === 'user')
402
+ );
403
+
404
+ // ── Evals ──
405
+
406
+ // Always runs (when global condition passes)
407
+ app.eval('has-completion', ({ entries }) => {
408
+ const lastAssistant = [...entries].reverse().find(e => e.type === 'assistant');
409
+ const hasText = lastAssistant?.message?.content?.some?.(b => b.type === 'text');
410
+ return {
411
+ pass: !!hasText,
412
+ score: hasText ? 1.0 : 0,
413
+ message: hasText ? 'Session completed with text response' : 'No final text response',
414
+ };
415
+ });
416
+
417
+ // Only runs for sessions with tool calls
418
+ app.eval('tool-success-rate',
419
+ ({ entries }) => {
420
+ const toolResults = entries.filter(e => e.type === 'tool');
421
+ const errors = toolResults.filter(e => e.raw?.is_error === true);
422
+ const rate = toolResults.length > 0 ? 1 - (errors.length / toolResults.length) : 1;
423
+ return {
424
+ pass: rate >= 0.9,
425
+ score: rate,
426
+ message: `${errors.length}/${toolResults.length} tool errors`,
427
+ };
428
+ },
429
+ { condition: ({ stats }) => stats.toolCallCount > 0 }
430
+ );
431
+
432
+ // Only runs for longer sessions
433
+ app.eval('under-budget',
434
+ ({ stats }) => ({
435
+ pass: stats.turnCount <= 30,
436
+ score: Math.max(0, 1 - stats.turnCount / 60),
437
+ message: `${stats.turnCount} turns`,
438
+ }),
439
+ { condition: ({ stats }) => stats.turnCount >= 5 }
440
+ );
441
+
442
+ // ── Enrichments ──
443
+
444
+ // Always runs
445
+ app.enrich('overview', ({ stats }) => ({
446
+ 'Turns': stats.turnCount,
447
+ 'Tool Calls': stats.toolCallCount,
448
+ 'Duration': stats.duration,
449
+ 'Models': stats.models.join(', ') || 'none',
450
+ }));
451
+
452
+ // Only when subagents were spawned
453
+ app.enrich('subagent-info',
454
+ ({ entries, stats }) => {
455
+ const subagentEntries = entries.filter(e => e.type === 'assistant' && e.parentUuid);
456
+ return {
457
+ 'Subagent Count': stats.subagentCount,
458
+ 'Subagent Messages': subagentEntries.length,
459
+ };
460
+ },
461
+ { condition: ({ stats }) => stats.subagentCount > 0 }
462
+ );
463
+
464
+ // Async condition example
465
+ app.enrich('advanced-metrics',
466
+ ({ entries }) => ({
467
+ 'Entry Count': entries.length,
468
+ 'Avg Entry Size': Math.round(
469
+ entries.reduce((s, e) => s + JSON.stringify(e).length, 0) / entries.length
470
+ ),
471
+ }),
472
+ {
473
+ condition: async ({ entries }) => {
474
+ // Conditions can be async
475
+ return entries.length > 10;
476
+ },
477
+ }
478
+ );
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Subagent-Scoped Evals & Enrichments
484
+
485
+ By default, evals and enrichments run at the session level. You can also register evals and enrichments that run against individual subagent logs using the `scope` option.
486
+
487
+ ### Scope Options
488
+
489
+ The `scope` option controls when an eval or enrichment runs:
490
+
491
+ | Scope | Runs at session level | Runs at subagent level |
492
+ |-------|----------------------|----------------------|
493
+ | `'session'` (default) | Yes | No |
494
+ | `'subagent'` | No | Yes |
495
+ | `'both'` | Yes | Yes |
496
+
497
+ ### Basic Usage
498
+
499
+ ```js
500
+ import { createApp } from 'claudeye';
501
+
502
+ const app = createApp();
503
+
504
+ // Session-only (default, backward-compatible)
505
+ app.eval('no-errors', fn);
506
+
507
+ // Subagent-only, runs for all subagent types
508
+ app.eval('agent-quality', fn, { scope: 'subagent' });
509
+
510
+ // Subagent-only, only runs for Explore subagents
511
+ app.eval('explore-depth', fn, { scope: 'subagent', subagentType: 'Explore' });
512
+
513
+ // Runs at both session and subagent level
514
+ app.eval('general-check', fn, { scope: 'both' });
515
+
516
+ // Enrichments work the same way
517
+ app.enrich('agent-cost', fn, { scope: 'subagent' });
518
+ app.enrich('agent-tokens', fn, { scope: 'subagent', subagentType: 'Explore' });
519
+ ```
520
+
521
+ ### Subagent Context
522
+
523
+ When running at subagent level, the `EvalContext` includes additional metadata:
524
+
525
+ ```js
526
+ app.eval('adaptive-eval', (ctx) => {
527
+ if (ctx.scope === 'subagent') {
528
+ // Running against a subagent's log
529
+ console.log(ctx.subagentId); // e.g. 'a1b2c3'
530
+ console.log(ctx.subagentType); // e.g. 'Explore'
531
+ console.log(ctx.subagentDescription); // e.g. 'Search for auth code'
532
+ console.log(ctx.parentSessionId); // parent session ID
533
+ }
534
+ return { pass: true };
535
+ }, { scope: 'both' });
536
+ ```
537
+
538
+ ### SubagentType Filtering
539
+
540
+ When you specify `subagentType`, the eval/enrichment only runs for subagents of that type. Subagents of other types will not see the eval panel at all.
541
+
542
+ ```js
543
+ // Only runs for Explore subagents
544
+ app.eval('explore-thoroughness', ({ entries }) => ({
545
+ pass: entries.length > 5,
546
+ score: Math.min(entries.length / 20, 1),
547
+ }), { scope: 'subagent', subagentType: 'Explore' });
548
+
549
+ // Runs for all subagent types
550
+ app.eval('agent-efficiency', ({ stats }) => ({
551
+ pass: stats.turnCount <= 10,
552
+ score: Math.max(0, 1 - stats.turnCount / 20),
553
+ }), { scope: 'subagent' });
554
+ ```
555
+
556
+ ### UI Behavior
557
+
558
+ When a subagent is expanded in the log viewer, eval and enrichment panels appear below the stats bar (only if matching subagent-scoped evals/enrichments are registered). Panels use a compact layout to fit within the nested subagent view. If no subagent-scoped evals match the subagent's type, the panels are not rendered.
559
+
560
+ ### Caching
561
+
562
+ Subagent eval/enrichment results are cached separately from session results:
563
+
564
+ ```
565
+ ~/.claudeye/cache/evals/{project}/{sessionId}.json # session-level
566
+ ~/.claudeye/cache/evals/{project}/{sessionId}/agent-{id}.json # subagent-level
567
+ ```
568
+
569
+ Cache invalidation works the same way — based on the subagent log file's mtime+size and the evals module content hash.
570
+
571
+ ### Edge Cases
572
+
573
+ - **No subagents in session**: Subagent-scoped evals never run — panels only mount inside expanded subagent cards
574
+ - **`scope: 'both'` with `subagentType`**: At session level, the `subagentType` filter is ignored. At subagent level, it applies.
575
+ - **Conditions + scope**: Scope filtering happens first (registry level), then conditions run with the full `EvalContext` including scope metadata
576
+ - **Backward compatibility**: Existing evals with no `scope` option default to `'session'` — behavior is unchanged
577
+
578
+ ---
579
+
580
+ ## API Reference
581
+
582
+ ### `createApp()`
583
+
584
+ Returns a `ClaudeyeApp` instance. All methods are chainable.
585
+
586
+ ```ts
587
+ import { createApp } from 'claudeye';
588
+ const app = createApp();
589
+ ```
590
+
591
+ ### `app.condition(fn)`
592
+
593
+ Set a global condition that gates all evals and enrichments. Calling this multiple times replaces the previous condition.
594
+
595
+ ```ts
596
+ app.condition(({ entries, stats, projectName, sessionId }) => boolean | Promise<boolean>);
597
+ ```
598
+
599
+ ### `app.eval(name, fn, options?)`
600
+
601
+ Register an eval function.
177
602
 
178
603
  - **`name`** — unique string identifier for the eval
179
604
  - **`fn`** — function receiving an `EvalContext` and returning an `EvalResult`
605
+ - **`options.condition`** — optional condition function to gate this eval
606
+ - **`options.scope`** — `'session'` (default), `'subagent'`, or `'both'`
607
+ - **`options.subagentType`** — only run for subagents of this type (e.g. `'Explore'`)
180
608
 
181
609
  ```ts
182
610
  app.eval('my-eval', (ctx) => ({ pass: true, score: 1.0 }));
611
+
612
+ app.eval('conditional-eval', evalFn, {
613
+ condition: (ctx) => ctx.stats.turnCount > 0,
614
+ });
615
+
616
+ app.eval('subagent-eval', evalFn, {
617
+ scope: 'subagent',
618
+ subagentType: 'Explore',
619
+ });
183
620
  ```
184
621
 
185
- #### `app.enrich(name, fn)`
622
+ ### `app.enrich(name, fn, options?)`
186
623
 
187
- Register an enricher function. Chainable.
624
+ Register an enricher function.
188
625
 
189
626
  - **`name`** — unique string identifier for the enricher
190
627
  - **`fn`** — function receiving an `EvalContext` and returning a `Record<string, string | number | boolean>`
628
+ - **`options.condition`** — optional condition function to gate this enricher
629
+ - **`options.scope`** — `'session'` (default), `'subagent'`, or `'both'`
630
+ - **`options.subagentType`** — only run for subagents of this type (e.g. `'Explore'`)
191
631
 
192
632
  ```ts
193
633
  app.enrich('metrics', (ctx) => ({
194
634
  'Turn Count': ctx.stats.turnCount,
195
635
  'Models Used': ctx.stats.models.join(', '),
196
636
  }));
637
+
638
+ app.enrich('conditional-enricher', enrichFn, {
639
+ condition: (ctx) => ctx.entries.length > 0,
640
+ });
641
+
642
+ app.enrich('agent-cost', enrichFn, {
643
+ scope: 'subagent',
644
+ });
197
645
  ```
198
646
 
199
- #### `app.listen(port?, options?)`
647
+ ### `app.listen(port?, options?)`
200
648
 
201
649
  Start the Claudeye dashboard server.
202
650
 
203
651
  - **`port`** — port number (default: 8020)
652
+ - **`options.host`** — bind address (default: "localhost", use "0.0.0.0" for LAN)
204
653
  - **`options.open`** — auto-open browser (default: true)
205
654
 
206
655
  When the file is loaded via `--evals` or `CLAUDEYE_EVALS_MODULE`, `listen()` is a no-op.
207
656
 
208
- #### `EvalContext`
657
+ ### `EvalContext`
209
658
 
210
659
  Both evals and enrichers receive the same context object:
211
660
 
212
661
  ```ts
213
662
  interface EvalContext {
214
- entries: EvalLogEntry[]; // Parsed log entries for the session
215
- stats: EvalLogStats; // Computed stats (turn count, tool calls, etc.)
216
- projectName: string; // Encoded project folder name
217
- sessionId: string; // Session UUID
663
+ entries: EvalLogEntry[]; // Parsed log entries for the session or subagent
664
+ stats: EvalLogStats; // Computed stats (turn count, tool calls, etc.)
665
+ projectName: string; // Encoded project folder name
666
+ sessionId: string; // Session UUID
667
+ scope: 'session' | 'subagent'; // Whether running at session or subagent level
668
+ subagentId?: string; // Hex ID of the subagent (subagent scope only)
669
+ subagentType?: string; // e.g. 'Explore', 'Bash' (subagent scope only)
670
+ subagentDescription?: string; // Short description (subagent scope only)
671
+ parentSessionId?: string; // Parent session ID (subagent scope only)
218
672
  }
219
673
  ```
220
674
 
221
- #### `EvalLogStats`
675
+ ### `EvalLogEntry`
676
+
677
+ ```ts
678
+ interface EvalLogEntry {
679
+ type: string; // 'user' | 'assistant' | 'tool' | 'queue-operation' | ...
680
+ uuid: string; // Unique entry ID
681
+ parentUuid: string | null; // Parent ID for nested entries (subagents)
682
+ timestamp: string; // ISO timestamp
683
+ timestampMs: number; // Epoch milliseconds
684
+ timestampFormatted: string; // Human-readable timestamp
685
+ message?: {
686
+ role: string; // 'user' | 'assistant'
687
+ content: string | EvalContentBlock[]; // Text or structured content blocks
688
+ model?: string; // Model ID (assistant messages only)
689
+ };
690
+ raw?: Record<string, unknown>; // Raw JSONL data (usage, errors, etc.)
691
+ label?: string; // Custom label
692
+ }
693
+ ```
694
+
695
+ ### `EvalLogStats`
222
696
 
223
697
  ```ts
224
698
  interface EvalLogStats {
225
- turnCount: number;
226
- userCount: number;
227
- assistantCount: number;
228
- toolCallCount: number;
229
- subagentCount: number;
230
- duration: string; // Formatted duration (e.g. "2m 15s")
231
- models: string[]; // Model IDs used in the session
699
+ turnCount: number; // Number of conversation turns
700
+ userCount: number; // Number of user messages
701
+ assistantCount: number; // Number of assistant responses
702
+ toolCallCount: number; // Total tool invocations
703
+ subagentCount: number; // Number of subagent spawns
704
+ duration: string; // Formatted duration (e.g. "2m 15s")
705
+ models: string[]; // Distinct model IDs used
232
706
  }
233
707
  ```
234
708
 
235
- #### `EvalResult`
709
+ ### `EvalResult`
236
710
 
237
711
  ```ts
238
712
  interface EvalResult {
239
- pass: boolean; // Required: did the eval pass?
240
- score?: number; // Optional: 0-1, clamped automatically
241
- message?: string; // Optional: shown in the UI
242
- metadata?: Record<string, unknown>; // Optional: arbitrary data
713
+ pass: boolean; // Did the eval pass?
714
+ score?: number; // 0-1, clamped automatically (default: 1.0)
715
+ message?: string; // Shown in the UI
716
+ metadata?: Record<string, unknown>; // Arbitrary data
243
717
  }
244
718
  ```
245
719
 
246
- #### `EnrichmentResult`
720
+ ### `EnrichmentResult`
247
721
 
248
722
  ```ts
249
723
  // Enrichers return a flat key-value map
250
724
  type EnrichmentResult = Record<string, string | number | boolean>;
251
725
  ```
252
726
 
253
- ### How It Works
727
+ ### `ConditionFunction`
728
+
729
+ ```ts
730
+ type ConditionFunction = (context: EvalContext) => boolean | Promise<boolean>;
731
+ ```
732
+
733
+ ---
734
+
735
+ ## How It Works
254
736
 
255
- 1. `createApp()` + `app.eval()` / `app.enrich()` register functions in global registries
256
- 2. When you run `claudeye --evals ./my-file.js`, the server dynamically imports your file, populating both the eval and enricher registries
737
+ 1. `createApp()` + `app.eval()` / `app.enrich()` / `app.condition()` register functions in global registries
738
+ 2. When you run `claudeye --evals ./my-file.js`, the server dynamically imports your file, populating the eval, enricher, and condition registries
257
739
  3. When the dashboard loads a session, server actions run all registered evals and enrichers against the session's parsed log entries
258
- 4. Each function is individually error-isolated — if one throws, the others still run
259
- 5. Results are serialized and displayed in separate panels in the dashboard UI
740
+ 4. The global condition is checked first — if it fails, everything is skipped
741
+ 5. Per-item conditions are checked individually skipped items don't block others
742
+ 6. Each function is individually error-isolated — if one throws, the others still run
743
+ 7. Results are serialized and displayed in separate panels in the dashboard UI
260
744
 
261
- ### Tips
745
+ ## Tips
262
746
 
263
747
  - Each eval and enricher is wrapped in a try/catch. If one throws, the others still run and the error is shown in the UI.
264
- - Eval scores are clamped to 0-1. If you don't provide a score, passing evals default to 1.0 and failing evals default to 0.
748
+ - Eval scores are clamped to 0-1. If you don't provide a score, it defaults to 1.0.
265
749
  - Both eval and enricher functions can be async.
750
+ - Condition functions can also be async.
266
751
  - Re-registering with the same name replaces the previous function.
267
- - Click "Re-run" in either panel to re-execute against the current session.
268
- - You can mix `app.eval()` and `app.enrich()` calls freely in the same file — they're independent systems that share the same context.
752
+ - Click "Re-run" in either panel to re-execute against the current session (always bypasses cache).
753
+ - You can mix `app.eval()`, `app.enrich()`, and `app.condition()` calls freely in the same file.
754
+ - The global condition applies to both evals and enrichments — there's no way to set separate global conditions for each.
755
+ - Per-item condition errors are treated as eval/enrichment errors (not skips), so you'll see the error message in the UI.
269
756
 
270
757
  ## License
271
758