@grimoire-cc/cli 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/commands/logs.d.ts.map +1 -1
  2. package/dist/commands/logs.js +2 -2
  3. package/dist/commands/logs.js.map +1 -1
  4. package/dist/static/log-viewer.html +946 -690
  5. package/dist/static/static/log-viewer.html +946 -690
  6. package/package.json +1 -1
  7. package/packs/dev-pack/agents/gr.code-reviewer.md +286 -0
  8. package/packs/dev-pack/agents/gr.tdd-specialist.md +44 -0
  9. package/packs/dev-pack/grimoire.json +55 -0
  10. package/packs/dev-pack/skills/gr.tdd-specialist/SKILL.md +247 -0
  11. package/packs/dev-pack/skills/gr.tdd-specialist/reference/anti-patterns.md +166 -0
  12. package/packs/dev-pack/skills/gr.tdd-specialist/reference/language-frameworks.md +388 -0
  13. package/packs/dev-pack/skills/gr.tdd-specialist/reference/tdd-workflow-patterns.md +135 -0
  14. package/packs/docs-pack/grimoire.json +30 -0
  15. package/packs/docs-pack/skills/gr.business-logic-docs/SKILL.md +141 -0
  16. package/packs/docs-pack/skills/gr.business-logic-docs/references/tier2-template.md +74 -0
  17. package/packs/essentials-pack/agents/gr.fact-checker.md +202 -0
  18. package/packs/essentials-pack/grimoire.json +12 -0
  19. package/packs/meta-pack/grimoire.json +72 -0
  20. package/packs/meta-pack/skills/gr.context-file-guide/SKILL.md +201 -0
  21. package/packs/meta-pack/skills/gr.context-file-guide/scripts/validate-context-file.sh +29 -0
  22. package/packs/meta-pack/skills/gr.readme-guide/SKILL.md +362 -0
  23. package/packs/meta-pack/skills/gr.skill-developer/SKILL.md +321 -0
  24. package/packs/meta-pack/skills/gr.skill-developer/examples/brand-guidelines.md +94 -0
  25. package/packs/meta-pack/skills/gr.skill-developer/examples/financial-analysis.md +85 -0
  26. package/packs/meta-pack/skills/gr.skill-developer/reference/best-practices.md +410 -0
  27. package/packs/meta-pack/skills/gr.skill-developer/reference/file-organization.md +452 -0
  28. package/packs/meta-pack/skills/gr.skill-developer/reference/patterns.md +459 -0
  29. package/packs/meta-pack/skills/gr.skill-developer/reference/yaml-spec.md +214 -0
  30. package/packs/meta-pack/skills/gr.skill-developer/scripts/create-skill.sh +210 -0
  31. package/packs/meta-pack/skills/gr.skill-developer/scripts/validate-skill.py +520 -0
  32. package/packs/meta-pack/skills/gr.skill-developer/templates/basic-skill.md +94 -0
  33. package/packs/meta-pack/skills/gr.skill-developer/templates/domain-skill.md +108 -0
@@ -1,559 +1,814 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
+
3
4
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Skill Router — Log Viewer</title>
7
- <style>
8
- :root {
9
- --bg: #0d1117;
10
- --surface: #161b22;
11
- --border: #30363d;
12
- --text: #e6edf3;
13
- --text-muted: #8b949e;
14
- --accent: #58a6ff;
15
- --green: #3fb950;
16
- --red: #f85149;
17
- --orange: #d29922;
18
- --purple: #bc8cff;
19
- --cyan: #39d2c0;
20
- }
21
- * { box-sizing: border-box; margin: 0; padding: 0; }
22
- body {
23
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
24
- background: var(--bg);
25
- color: var(--text);
26
- line-height: 1.5;
27
- padding: 24px;
28
- }
29
- h1 { font-size: 20px; font-weight: 600; margin-bottom: 16px; }
30
- h2 { font-size: 14px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
31
-
32
- /* Drop zone */
33
- .drop-zone {
34
- border: 2px dashed var(--border);
35
- border-radius: 12px;
36
- padding: 48px;
37
- text-align: center;
38
- cursor: pointer;
39
- transition: border-color 0.2s, background 0.2s;
40
- margin-bottom: 24px;
41
- }
42
- .drop-zone:hover, .drop-zone.drag-over {
43
- border-color: var(--accent);
44
- background: rgba(88, 166, 255, 0.05);
45
- }
46
- .drop-zone p { color: var(--text-muted); font-size: 14px; }
47
- .drop-zone .big { font-size: 16px; color: var(--text); margin-bottom: 4px; }
48
- .drop-zone input { display: none; }
49
-
50
- /* Dashboard */
51
- .dashboard { display: none; }
52
- .dashboard.visible { display: block; }
53
- .stats-grid {
54
- display: grid;
55
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
56
- gap: 12px;
57
- margin-bottom: 24px;
58
- }
59
- .stat-card {
60
- background: var(--surface);
61
- border: 1px solid var(--border);
62
- border-radius: 8px;
63
- padding: 16px;
64
- }
65
- .stat-card .value { font-size: 28px; font-weight: 700; }
66
- .stat-card .label { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
67
- .stat-card.activated .value { color: var(--green); }
68
- .stat-card.no-match .value { color: var(--text-muted); }
69
-
70
- /* Filters */
71
- .filters {
72
- display: flex;
73
- gap: 8px;
74
- flex-wrap: wrap;
75
- margin-bottom: 16px;
76
- align-items: center;
77
- }
78
- .filters label { font-size: 12px; color: var(--text-muted); margin-right: 4px; }
79
- select, input[type="text"] {
80
- background: var(--surface);
81
- border: 1px solid var(--border);
82
- border-radius: 6px;
83
- color: var(--text);
84
- padding: 6px 10px;
85
- font-size: 13px;
86
- outline: none;
87
- }
88
- select:focus, input[type="text"]:focus { border-color: var(--accent); }
89
- input[type="text"] { width: 220px; }
90
- .filter-group { display: flex; align-items: center; gap: 4px; }
91
- .btn {
92
- background: var(--surface);
93
- border: 1px solid var(--border);
94
- border-radius: 6px;
95
- color: var(--text-muted);
96
- padding: 6px 12px;
97
- font-size: 12px;
98
- cursor: pointer;
99
- transition: all 0.15s;
100
- }
101
- .btn:hover { border-color: var(--accent); color: var(--text); }
102
- .btn.active { background: rgba(88, 166, 255, 0.15); border-color: var(--accent); color: var(--accent); }
103
-
104
- /* Top skills */
105
- .top-skills {
106
- background: var(--surface);
107
- border: 1px solid var(--border);
108
- border-radius: 8px;
109
- padding: 16px;
110
- margin-bottom: 24px;
111
- }
112
- .skill-card {
113
- padding: 6px 0;
114
- }
115
- .skill-card .skill-header {
116
- display: flex;
117
- align-items: center;
118
- gap: 8px;
119
- font-size: 13px;
120
- min-width: 0;
121
- }
122
- .skill-card .skill-header .name { font-weight: 600; white-space: nowrap; }
123
- .skill-card .skill-header .desc { font-size: 12px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
124
- .skill-card .count { color: var(--text-muted); font-size: 12px; white-space: nowrap; margin-left: auto; }
125
- .skill-card .bar-bg {
126
- width: 100%;
127
- height: 2px;
128
- background: var(--border);
129
- border-radius: 1px;
130
- overflow: hidden;
131
- margin-top: 4px;
132
- }
133
- .skill-card .bar-fill {
134
- height: 100%;
135
- background: var(--accent);
136
- border-radius: 1px;
137
- transition: width 0.3s;
138
- }
139
- .skill-card .triggers {
140
- display: none;
141
- flex-wrap: wrap;
142
- gap: 4px;
143
- margin-top: 4px;
144
- }
145
- .skill-card:hover .triggers { display: flex; }
146
-
147
- /* Table */
148
- .table-wrap {
149
- overflow-x: auto;
150
- border: 1px solid var(--border);
151
- border-radius: 8px;
152
- }
153
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
154
- thead th {
155
- background: var(--surface);
156
- padding: 10px 12px;
157
- text-align: left;
158
- font-weight: 600;
159
- font-size: 12px;
160
- color: var(--text-muted);
161
- text-transform: uppercase;
162
- letter-spacing: 0.3px;
163
- border-bottom: 1px solid var(--border);
164
- position: sticky;
165
- top: 0;
166
- cursor: pointer;
167
- user-select: none;
168
- white-space: nowrap;
169
- }
170
- thead th:hover { color: var(--text); }
171
- thead th .sort-arrow { margin-left: 4px; font-size: 10px; }
172
- tbody tr { border-bottom: 1px solid var(--border); transition: background 0.1s; }
173
- tbody tr:hover { background: rgba(88, 166, 255, 0.04); }
174
- tbody tr:last-child { border-bottom: none; }
175
- td { padding: 8px 12px; vertical-align: top; }
176
-
177
- .badge {
178
- display: inline-block;
179
- padding: 2px 8px;
180
- border-radius: 12px;
181
- font-size: 11px;
182
- font-weight: 600;
183
- }
184
- .badge.activated { background: rgba(63, 185, 80, 0.15); color: var(--green); }
185
- .badge.no-match { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); }
186
- .badge.hook-prompt { background: rgba(188, 140, 255, 0.15); color: var(--purple); }
187
- .badge.hook-tool { background: rgba(57, 210, 192, 0.15); color: var(--cyan); }
188
-
189
- .prompt-text {
190
- max-width: 400px;
191
- overflow: hidden;
192
- text-overflow: ellipsis;
193
- white-space: nowrap;
194
- cursor: pointer;
195
- }
196
- .prompt-text:hover { white-space: normal; word-break: break-word; }
197
-
198
- .signal-tag {
199
- display: inline-block;
200
- padding: 1px 6px;
201
- border-radius: 4px;
202
- font-size: 11px;
203
- margin: 1px 2px;
204
- background: rgba(88, 166, 255, 0.1);
205
- color: var(--accent);
206
- }
207
- .signal-tag.keyword { background: rgba(63, 185, 80, 0.1); color: var(--green); }
208
- .signal-tag.pattern { background: rgba(210, 153, 34, 0.1); color: var(--orange); }
209
- .signal-tag.extension { background: rgba(188, 140, 255, 0.1); color: var(--purple); }
210
- .signal-tag.path { background: rgba(57, 210, 192, 0.1); color: var(--cyan); }
211
-
212
- .skills-cell { min-width: 200px; }
213
- .skill-match {
214
- margin-bottom: 4px;
215
- padding: 4px 0;
216
- }
217
- .skill-match .skill-name { font-weight: 600; font-size: 12px; }
218
- .skill-match .skill-score { color: var(--orange); font-size: 11px; margin-left: 4px; }
219
-
220
- .session-id {
221
- font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;
222
- font-size: 11px;
223
- color: var(--text-muted);
224
- max-width: 100px;
225
- overflow: hidden;
226
- text-overflow: ellipsis;
227
- white-space: nowrap;
228
- }
229
-
230
- .time-ms { color: var(--text-muted); font-size: 12px; }
231
- .time-ms.slow { color: var(--orange); }
232
-
233
- .empty-state {
234
- text-align: center;
235
- padding: 40px;
236
- color: var(--text-muted);
237
- }
238
-
239
- .count-badge {
240
- font-size: 12px;
241
- color: var(--text-muted);
242
- margin-left: 8px;
243
- font-weight: 400;
244
- }
245
-
246
- /* Sections layout */
247
- .section-row {
248
- display: grid;
249
- grid-template-columns: 1fr 1fr;
250
- gap: 16px;
251
- margin-bottom: 24px;
252
- }
253
- @media (max-width: 800px) { .section-row { grid-template-columns: 1fr; } }
254
-
255
- /* Session timeline */
256
- .session-list {
257
- background: var(--surface);
258
- border: 1px solid var(--border);
259
- border-radius: 8px;
260
- padding: 16px;
261
- max-height: 240px;
262
- overflow-y: auto;
263
- }
264
- .session-item {
265
- display: flex;
266
- justify-content: space-between;
267
- align-items: center;
268
- padding: 4px 0;
269
- font-size: 13px;
270
- cursor: pointer;
271
- transition: color 0.1s;
272
- }
273
- .session-item:hover { color: var(--accent); }
274
- .session-item .id { font-family: monospace; font-size: 11px; }
275
- .session-item .meta { color: var(--text-muted); font-size: 11px; }
276
-
277
- /* Live badge */
278
- .live-badge {
279
- display: none;
280
- align-items: center;
281
- gap: 6px;
282
- font-size: 12px;
283
- font-weight: 600;
284
- color: var(--green);
285
- margin-left: 12px;
286
- vertical-align: middle;
287
- }
288
- .live-badge.visible { display: inline-flex; }
289
- .live-badge::before {
290
- content: '';
291
- width: 8px;
292
- height: 8px;
293
- background: var(--green);
294
- border-radius: 50%;
295
- animation: pulse 2s ease-in-out infinite;
296
- }
297
- @keyframes pulse {
298
- 0%, 100% { opacity: 1; }
299
- 50% { opacity: 0.4; }
300
- }
301
- </style>
302
- </head>
303
- <body>
5
+ <meta charset="UTF-8">
6
+ <meta
7
+ name="viewport"
8
+ content="width=device-width, initial-scale=1.0"
9
+ >
10
+ <title>Skill Router — Log Viewer</title>
11
+ <style>
12
+ :root {
13
+ --bg: #0d1117;
14
+ --surface: #161b22;
15
+ --border: #30363d;
16
+ --text: #e6edf3;
17
+ --text-muted: #8b949e;
18
+ --accent: #58a6ff;
19
+ --green: #3fb950;
20
+ --red: #f85149;
21
+ --orange: #d29922;
22
+ --purple: #bc8cff;
23
+ --cyan: #39d2c0;
24
+ }
304
25
 
305
- <h1>Skill Router — Log Viewer <span class="live-badge" id="liveBadge">LIVE</span></h1>
26
+ * {
27
+ box-sizing: border-box;
28
+ margin: 0;
29
+ padding: 0;
30
+ }
306
31
 
307
- <div class="drop-zone" id="dropZone">
308
- <p class="big">Drop <code>skill-router.log</code> here</p>
309
- <p>or click to browse</p>
310
- <input type="file" id="fileInput" accept=".log,.json,.ndjson">
311
- </div>
32
+ body {
33
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
34
+ background: var(--bg);
35
+ color: var(--text);
36
+ line-height: 1.5;
37
+ padding: 24px;
38
+ }
312
39
 
313
- <div class="dashboard" id="dashboard">
314
- <!-- Stats -->
315
- <div class="stats-grid" id="statsGrid"></div>
40
+ h1 {
41
+ font-size: 20px;
42
+ font-weight: 600;
43
+ margin-bottom: 16px;
44
+ }
316
45
 
317
- <!-- Top skills + Sessions -->
318
- <div class="section-row">
319
- <div>
320
- <h2>Skills</h2>
321
- <div class="top-skills" id="skillsPanel"></div>
322
- </div>
323
- <div>
324
- <h2>Sessions</h2>
325
- <div class="session-list" id="sessionList"></div>
326
- </div>
46
+ h2 {
47
+ font-size: 14px;
48
+ font-weight: 600;
49
+ color: var(--text-muted);
50
+ text-transform: uppercase;
51
+ letter-spacing: 0.5px;
52
+ margin-bottom: 8px;
53
+ }
54
+
55
+ /* Drop zone */
56
+ .drop-zone {
57
+ border: 2px dashed var(--border);
58
+ border-radius: 12px;
59
+ padding: 48px;
60
+ text-align: center;
61
+ cursor: pointer;
62
+ transition: border-color 0.2s, background 0.2s;
63
+ margin-bottom: 24px;
64
+ }
65
+
66
+ .drop-zone:hover,
67
+ .drop-zone.drag-over {
68
+ border-color: var(--accent);
69
+ background: rgba(88, 166, 255, 0.05);
70
+ }
71
+
72
+ .drop-zone p {
73
+ color: var(--text-muted);
74
+ font-size: 14px;
75
+ }
76
+
77
+ .drop-zone .big {
78
+ font-size: 16px;
79
+ color: var(--text);
80
+ margin-bottom: 4px;
81
+ }
82
+
83
+ .drop-zone input {
84
+ display: none;
85
+ }
86
+
87
+ /* Dashboard */
88
+ .dashboard {
89
+ display: none;
90
+ }
91
+
92
+ .dashboard.visible {
93
+ display: block;
94
+ }
95
+
96
+ .stats-grid {
97
+ display: grid;
98
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
99
+ gap: 12px;
100
+ margin-bottom: 24px;
101
+ }
102
+
103
+ .stat-card {
104
+ background: var(--surface);
105
+ border: 1px solid var(--border);
106
+ border-radius: 8px;
107
+ padding: 16px;
108
+ }
109
+
110
+ .stat-card .value {
111
+ font-size: 28px;
112
+ font-weight: 700;
113
+ }
114
+
115
+ .stat-card .label {
116
+ font-size: 12px;
117
+ color: var(--text-muted);
118
+ margin-top: 2px;
119
+ }
120
+
121
+ .stat-card.activated .value {
122
+ color: var(--green);
123
+ }
124
+
125
+ .stat-card.no-match .value {
126
+ color: var(--text-muted);
127
+ }
128
+
129
+ /* Filters */
130
+ .filters {
131
+ display: flex;
132
+ gap: 8px;
133
+ flex-wrap: wrap;
134
+ margin-bottom: 16px;
135
+ align-items: center;
136
+ }
137
+
138
+ .filters label {
139
+ font-size: 12px;
140
+ color: var(--text-muted);
141
+ margin-right: 4px;
142
+ }
143
+
144
+ select,
145
+ input[type="text"] {
146
+ background: var(--surface);
147
+ border: 1px solid var(--border);
148
+ border-radius: 6px;
149
+ color: var(--text);
150
+ padding: 6px 10px;
151
+ font-size: 13px;
152
+ outline: none;
153
+ }
154
+
155
+ select:focus,
156
+ input[type="text"]:focus {
157
+ border-color: var(--accent);
158
+ }
159
+
160
+ input[type="text"] {
161
+ width: 220px;
162
+ }
163
+
164
+ .filter-group {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 4px;
168
+ }
169
+
170
+ .btn {
171
+ background: var(--surface);
172
+ border: 1px solid var(--border);
173
+ border-radius: 6px;
174
+ color: var(--text-muted);
175
+ padding: 6px 12px;
176
+ font-size: 12px;
177
+ cursor: pointer;
178
+ transition: all 0.15s;
179
+ }
180
+
181
+ .btn:hover {
182
+ border-color: var(--accent);
183
+ color: var(--text);
184
+ }
185
+
186
+ .btn.active {
187
+ background: rgba(88, 166, 255, 0.15);
188
+ border-color: var(--accent);
189
+ color: var(--accent);
190
+ }
191
+
192
+ /* Top skills */
193
+ .top-skills {
194
+ background: var(--surface);
195
+ border: 1px solid var(--border);
196
+ border-radius: 8px;
197
+ padding: 16px;
198
+ margin-bottom: 24px;
199
+ }
200
+
201
+ .skill-card {
202
+ padding: 6px 0;
203
+ }
204
+
205
+ .skill-card .skill-header {
206
+ display: flex;
207
+ align-items: center;
208
+ gap: 8px;
209
+ font-size: 13px;
210
+ min-width: 0;
211
+ }
212
+
213
+ .skill-card .skill-header .name {
214
+ font-weight: 600;
215
+ white-space: nowrap;
216
+ }
217
+
218
+ .skill-card .skill-header .desc {
219
+ font-size: 12px;
220
+ color: var(--text-muted);
221
+ white-space: nowrap;
222
+ overflow: hidden;
223
+ text-overflow: ellipsis;
224
+ }
225
+
226
+ .skill-card .count {
227
+ color: var(--text-muted);
228
+ font-size: 12px;
229
+ white-space: nowrap;
230
+ margin-left: auto;
231
+ }
232
+
233
+ .skill-card .bar-bg {
234
+ width: 100%;
235
+ height: 2px;
236
+ background: var(--border);
237
+ border-radius: 1px;
238
+ overflow: hidden;
239
+ margin-top: 4px;
240
+ }
241
+
242
+ .skill-card .bar-fill {
243
+ height: 100%;
244
+ background: var(--accent);
245
+ border-radius: 1px;
246
+ transition: width 0.3s;
247
+ }
248
+
249
+ .skill-card .triggers {
250
+ display: none;
251
+ flex-wrap: wrap;
252
+ gap: 4px;
253
+ margin-top: 4px;
254
+ }
255
+
256
+ .skill-card:hover .triggers {
257
+ display: flex;
258
+ }
259
+
260
+ /* Table */
261
+ .table-wrap {
262
+ overflow-x: auto;
263
+ border: 1px solid var(--border);
264
+ border-radius: 8px;
265
+ }
266
+
267
+ table {
268
+ width: 100%;
269
+ border-collapse: collapse;
270
+ font-size: 13px;
271
+ }
272
+
273
+ thead th {
274
+ background: var(--surface);
275
+ padding: 10px 12px;
276
+ text-align: left;
277
+ font-weight: 600;
278
+ font-size: 12px;
279
+ color: var(--text-muted);
280
+ text-transform: uppercase;
281
+ letter-spacing: 0.3px;
282
+ border-bottom: 1px solid var(--border);
283
+ position: sticky;
284
+ top: 0;
285
+ cursor: pointer;
286
+ user-select: none;
287
+ white-space: nowrap;
288
+ }
289
+
290
+ thead th:hover {
291
+ color: var(--text);
292
+ }
293
+
294
+ thead th .sort-arrow {
295
+ margin-left: 4px;
296
+ font-size: 10px;
297
+ }
298
+
299
+ tbody tr {
300
+ border-bottom: 1px solid var(--border);
301
+ transition: background 0.1s;
302
+ }
303
+
304
+ tbody tr:hover {
305
+ background: rgba(88, 166, 255, 0.04);
306
+ }
307
+
308
+ tbody tr:last-child {
309
+ border-bottom: none;
310
+ }
311
+
312
+ td {
313
+ padding: 8px 12px;
314
+ vertical-align: top;
315
+ }
316
+
317
+ .badge {
318
+ display: inline-block;
319
+ padding: 2px 8px;
320
+ border-radius: 12px;
321
+ font-size: 11px;
322
+ font-weight: 600;
323
+ }
324
+
325
+ .badge.activated {
326
+ background: rgba(63, 185, 80, 0.15);
327
+ color: var(--green);
328
+ }
329
+
330
+ .badge.no-match {
331
+ background: rgba(139, 148, 158, 0.15);
332
+ color: var(--text-muted);
333
+ }
334
+
335
+ .badge.hook-prompt {
336
+ background: rgba(188, 140, 255, 0.15);
337
+ color: var(--purple);
338
+ }
339
+
340
+ .badge.hook-tool {
341
+ background: rgba(57, 210, 192, 0.15);
342
+ color: var(--cyan);
343
+ }
344
+
345
+ .prompt-text {
346
+ max-width: 400px;
347
+ overflow: hidden;
348
+ text-overflow: ellipsis;
349
+ white-space: nowrap;
350
+ cursor: pointer;
351
+ }
352
+
353
+ .prompt-text:hover {
354
+ white-space: normal;
355
+ word-break: break-word;
356
+ }
357
+
358
+ .signal-tag {
359
+ display: inline-block;
360
+ padding: 1px 6px;
361
+ border-radius: 4px;
362
+ font-size: 11px;
363
+ margin: 1px 2px;
364
+ background: rgba(88, 166, 255, 0.1);
365
+ color: var(--accent);
366
+ }
367
+
368
+ .signal-tag.keyword {
369
+ background: rgba(63, 185, 80, 0.1);
370
+ color: var(--green);
371
+ }
372
+
373
+ .signal-tag.pattern {
374
+ background: rgba(210, 153, 34, 0.1);
375
+ color: var(--orange);
376
+ }
377
+
378
+ .signal-tag.extension {
379
+ background: rgba(188, 140, 255, 0.1);
380
+ color: var(--purple);
381
+ }
382
+
383
+ .signal-tag.path {
384
+ background: rgba(57, 210, 192, 0.1);
385
+ color: var(--cyan);
386
+ }
387
+
388
+ .skills-cell {
389
+ min-width: 200px;
390
+ }
391
+
392
+ .skill-match {
393
+ margin-bottom: 4px;
394
+ padding: 4px 0;
395
+ }
396
+
397
+ .skill-match .skill-name {
398
+ font-weight: 600;
399
+ font-size: 12px;
400
+ }
401
+
402
+ .skill-match .skill-score {
403
+ color: var(--orange);
404
+ font-size: 11px;
405
+ margin-left: 4px;
406
+ }
407
+
408
+ .session-id {
409
+ font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;
410
+ font-size: 11px;
411
+ color: var(--text-muted);
412
+ max-width: 100px;
413
+ overflow: hidden;
414
+ text-overflow: ellipsis;
415
+ white-space: nowrap;
416
+ }
417
+
418
+ .time-ms {
419
+ color: var(--text-muted);
420
+ font-size: 12px;
421
+ }
422
+
423
+ .time-ms.slow {
424
+ color: var(--orange);
425
+ }
426
+
427
+ .empty-state {
428
+ text-align: center;
429
+ padding: 40px;
430
+ color: var(--text-muted);
431
+ }
432
+
433
+ .count-badge {
434
+ font-size: 12px;
435
+ color: var(--text-muted);
436
+ margin-left: 8px;
437
+ font-weight: 400;
438
+ }
439
+
440
+ /* Sections layout */
441
+ .section-row {
442
+ display: grid;
443
+ grid-template-columns: 1fr 1fr;
444
+ gap: 16px;
445
+ margin-bottom: 24px;
446
+ }
447
+
448
+ @media (max-width: 800px) {
449
+ .section-row {
450
+ grid-template-columns: 1fr;
451
+ }
452
+ }
453
+
454
+ /* Session timeline */
455
+ .session-list {
456
+ background: var(--surface);
457
+ border: 1px solid var(--border);
458
+ border-radius: 8px;
459
+ padding: 16px;
460
+ max-height: 240px;
461
+ overflow-y: auto;
462
+ }
463
+
464
+ .session-item {
465
+ display: flex;
466
+ justify-content: space-between;
467
+ align-items: center;
468
+ padding: 4px 0;
469
+ font-size: 13px;
470
+ cursor: pointer;
471
+ transition: color 0.1s;
472
+ }
473
+
474
+ .session-item:hover {
475
+ color: var(--accent);
476
+ }
477
+
478
+ .session-item .id {
479
+ font-family: monospace;
480
+ font-size: 11px;
481
+ }
482
+
483
+ .session-item .meta {
484
+ color: var(--text-muted);
485
+ font-size: 11px;
486
+ }
487
+
488
+ /* Live badge */
489
+ .live-badge {
490
+ display: none;
491
+ align-items: center;
492
+ gap: 6px;
493
+ font-size: 12px;
494
+ font-weight: 600;
495
+ color: var(--green);
496
+ margin-left: 12px;
497
+ vertical-align: middle;
498
+ }
499
+
500
+ .live-badge.visible {
501
+ display: inline-flex;
502
+ }
503
+
504
+ .live-badge::before {
505
+ content: '';
506
+ width: 8px;
507
+ height: 8px;
508
+ background: var(--green);
509
+ border-radius: 50%;
510
+ animation: pulse 2s ease-in-out infinite;
511
+ }
512
+
513
+ @keyframes pulse {
514
+
515
+ 0%,
516
+ 100% {
517
+ opacity: 1;
518
+ }
519
+
520
+ 50% {
521
+ opacity: 0.4;
522
+ }
523
+ }
524
+ </style>
525
+ </head>
526
+
527
+ <body>
528
+
529
+ <h1>Skill Router — Log Viewer <span
530
+ class="live-badge"
531
+ id="liveBadge"
532
+ >LIVE</span></h1>
533
+
534
+ <div
535
+ class="drop-zone"
536
+ id="dropZone"
537
+ >
538
+ <p class="big">Drop <code>skill-router.log</code> here</p>
539
+ <p>or click to browse</p>
540
+ <input
541
+ type="file"
542
+ id="fileInput"
543
+ accept=".log,.json,.ndjson"
544
+ >
327
545
  </div>
328
546
 
329
- <!-- Filters -->
330
- <div class="filters" id="filters">
331
- <div class="filter-group">
332
- <label>Outcome:</label>
333
- <select id="filterOutcome">
334
- <option value="">All</option>
335
- <option value="activated">Activated</option>
336
- <option value="no_match">No Match</option>
337
- </select>
338
- </div>
339
- <div class="filter-group">
340
- <label>Hook:</label>
341
- <select id="filterHook">
342
- <option value="">All</option>
343
- <option value="prompt">UserPromptSubmit</option>
344
- <option value="tool">PreToolUse</option>
345
- </select>
547
+ <div
548
+ class="dashboard"
549
+ id="dashboard"
550
+ >
551
+ <!-- Stats -->
552
+ <div
553
+ class="stats-grid"
554
+ id="statsGrid"
555
+ ></div>
556
+
557
+ <!-- Top skills + Sessions -->
558
+ <div class="section-row">
559
+ <div>
560
+ <h2>Skills</h2>
561
+ <div
562
+ class="top-skills"
563
+ id="skillsPanel"
564
+ ></div>
565
+ </div>
566
+ <div>
567
+ <h2>Sessions</h2>
568
+ <div
569
+ class="session-list"
570
+ id="sessionList"
571
+ ></div>
572
+ </div>
346
573
  </div>
347
- <div class="filter-group">
348
- <label>Session:</label>
349
- <select id="filterSession">
350
- <option value="">All</option>
351
- </select>
574
+
575
+ <!-- Filters -->
576
+ <div
577
+ class="filters"
578
+ id="filters"
579
+ >
580
+ <div class="filter-group">
581
+ <label>Outcome:</label>
582
+ <select id="filterOutcome">
583
+ <option value="">All</option>
584
+ <option value="activated">Activated</option>
585
+ <option value="no_match">No Match</option>
586
+ </select>
587
+ </div>
588
+ <div class="filter-group">
589
+ <label>Hook:</label>
590
+ <select id="filterHook">
591
+ <option value="">All</option>
592
+ <option value="prompt">UserPromptSubmit</option>
593
+ <option value="tool">PreToolUse</option>
594
+ </select>
595
+ </div>
596
+ <div class="filter-group">
597
+ <label>Session:</label>
598
+ <select id="filterSession">
599
+ <option value="">All</option>
600
+ </select>
601
+ </div>
602
+ <div class="filter-group">
603
+ <label>Search:</label>
604
+ <input
605
+ type="text"
606
+ id="filterSearch"
607
+ placeholder="Filter by prompt or skill..."
608
+ >
609
+ </div>
610
+ <button
611
+ class="btn"
612
+ id="btnReset"
613
+ >Reset</button>
352
614
  </div>
353
- <div class="filter-group">
354
- <label>Search:</label>
355
- <input type="text" id="filterSearch" placeholder="Filter by prompt or skill...">
615
+
616
+ <!-- Table -->
617
+ <div class="table-wrap">
618
+ <table>
619
+ <thead>
620
+ <tr>
621
+ <th data-sort="index"># <span class="sort-arrow"></span></th>
622
+ <th data-sort="timestamp">Time <span class="sort-arrow"></span></th>
623
+ <th data-sort="hook">Hook <span class="sort-arrow"></span></th>
624
+ <th data-sort="prompt">Prompt <span class="sort-arrow"></span></th>
625
+ <th data-sort="outcome">Outcome <span class="sort-arrow"></span></th>
626
+ <th data-sort="skills">Skills Matched <span class="sort-arrow"></span></th>
627
+ <th data-sort="signals">Signals <span class="sort-arrow"></span></th>
628
+ <th data-sort="score">Score <span class="sort-arrow"></span></th>
629
+ <th data-sort="threshold">Thr <span class="sort-arrow"></span></th>
630
+ <th data-sort="time_ms">ms <span class="sort-arrow"></span></th>
631
+ </tr>
632
+ </thead>
633
+ <tbody id="tableBody"></tbody>
634
+ </table>
356
635
  </div>
357
- <button class="btn" id="btnReset">Reset</button>
358
636
  </div>
359
637
 
360
- <!-- Table -->
361
- <div class="table-wrap">
362
- <table>
363
- <thead>
364
- <tr>
365
- <th data-sort="index"># <span class="sort-arrow"></span></th>
366
- <th data-sort="timestamp">Time <span class="sort-arrow"></span></th>
367
- <th data-sort="hook">Hook <span class="sort-arrow"></span></th>
368
- <th data-sort="prompt">Prompt <span class="sort-arrow"></span></th>
369
- <th data-sort="outcome">Outcome <span class="sort-arrow"></span></th>
370
- <th data-sort="skills">Skills Matched <span class="sort-arrow"></span></th>
371
- <th data-sort="signals">Signals <span class="sort-arrow"></span></th>
372
- <th data-sort="score">Score <span class="sort-arrow"></span></th>
373
- <th data-sort="threshold">Thr <span class="sort-arrow"></span></th>
374
- <th data-sort="time_ms">ms <span class="sort-arrow"></span></th>
375
- </tr>
376
- </thead>
377
- <tbody id="tableBody"></tbody>
378
- </table>
379
- </div>
380
- </div>
381
-
382
- <script>
383
- let entries = [];
384
- let sortCol = 'timestamp';
385
- let sortAsc = false;
386
-
387
- // --- Append new log entries without clearing existing ones ---
388
- function appendLog(text) {
389
- let added = 0;
390
-
391
- for (const line of text.split('\n')) {
392
- const trimmed = line.trim();
393
- if (!trimmed) continue;
394
- try {
395
- const obj = JSON.parse(trimmed);
396
- obj._index = entries.length;
397
- entries.push(obj);
398
- added++;
399
- } catch { /* skip malformed */ }
400
- }
401
-
402
- if (added === 0) return;
403
-
404
- dropZone.style.display = 'none';
405
- document.getElementById('dashboard').classList.add('visible');
406
- buildDashboard();
407
- populateFilters();
408
- renderTable();
409
- }
410
-
411
- // --- Load skills from manifest ---
412
- let manifestSkills = [];
413
-
414
- async function loadManifestSkills() {
415
- try {
416
- const res = await fetch('/api/manifest');
417
- if (!res.ok) return;
418
- const manifest = await res.json();
419
- manifestSkills = manifest.skills || [];
420
- renderSkillsPanel();
421
- } catch { /* manifest not available in file: mode */ }
422
- }
423
-
424
- function renderSkillsPanel(filteredEntries) {
425
- const panel = document.getElementById('skillsPanel');
426
- if (!manifestSkills.length) {
427
- panel.innerHTML = '<div class="empty-state">No skills configured</div>';
428
- return;
429
- }
430
-
431
- // Count activations per skill from filtered entries
432
- const src = filteredEntries || entries;
433
- const skillCounts = {};
434
- for (const e of src) {
435
- for (const s of (e.skills_matched || [])) {
436
- skillCounts[s.name] = (skillCounts[s.name] || 0) + 1;
437
- }
438
- }
439
-
440
- const max = Math.max(1, ...manifestSkills.map(s => skillCounts[s.name] || 0));
441
-
442
- const sorted = [...manifestSkills].sort((a, b) => (skillCounts[b.name] || 0) - (skillCounts[a.name] || 0));
443
-
444
- panel.innerHTML = sorted.map(s => {
445
- const triggers = s.triggers || {};
446
- const count = skillCounts[s.name] || 0;
447
- const pct = (count / max * 100).toFixed(0);
448
- const tags = [
449
- ...(triggers.keywords || []).map(k => `<span class="signal-tag keyword">${esc(k)}</span>`),
450
- ...(triggers.file_extensions || []).map(e => `<span class="signal-tag extension">${esc(e)}</span>`),
451
- ...(triggers.patterns || []).map(p => `<span class="signal-tag pattern">${esc(p)}</span>`),
452
- ...(triggers.file_paths || []).map(f => `<span class="signal-tag path">${esc(f)}</span>`),
453
- ];
454
- return `<div class="skill-card">
638
+ <script>
639
+ let entries = [];
640
+ let sortCol = 'timestamp';
641
+ let sortAsc = false;
642
+
643
+ // --- Append new log entries without clearing existing ones ---
644
+ function appendLog(text) {
645
+ let added = 0;
646
+
647
+ for (const line of text.split('\n')) {
648
+ const trimmed = line.trim();
649
+ if (!trimmed) continue;
650
+ try {
651
+ const obj = JSON.parse(trimmed);
652
+ obj._index = entries.length;
653
+ entries.push(obj);
654
+ added++;
655
+ } catch { /* skip malformed */ }
656
+ }
657
+
658
+ if (added === 0) return;
659
+
660
+ dropZone.style.display = 'none';
661
+ document.getElementById('dashboard').classList.add('visible');
662
+ buildDashboard();
663
+ populateFilters();
664
+ renderTable();
665
+ }
666
+
667
+ // --- Load skills from manifest ---
668
+ let manifestSkills = [];
669
+
670
+ async function loadManifestSkills() {
671
+ try {
672
+ const res = await fetch('/api/manifest');
673
+ if (!res.ok) return;
674
+ const manifest = await res.json();
675
+ manifestSkills = manifest.skills ?? [];
676
+ renderSkillsPanel();
677
+ } catch { /* manifest not available in file: mode */ }
678
+ }
679
+
680
+ function renderSkillsPanel(filteredEntries) {
681
+ const panel = document.getElementById('skillsPanel');
682
+ if (!manifestSkills.length) {
683
+ panel.innerHTML = '<div class="empty-state">No skills configured</div>';
684
+ return;
685
+ }
686
+
687
+ // Count activations per skill from filtered entries
688
+ const src = filteredEntries ?? entries;
689
+ const skillCounts = {};
690
+ for (const e of src) {
691
+ for (const s of (e.skills_matched ?? [])) {
692
+ skillCounts[s.name] = (skillCounts[s.name] ?? 0) + 1;
693
+ }
694
+ }
695
+
696
+ const max = Math.max(1, ...manifestSkills.map(s => skillCounts[s.name] ?? 0));
697
+
698
+ const sorted = [...manifestSkills].sort((a, b) => (skillCounts[b.name] ?? 0) - (skillCounts[a.name] ?? 0));
699
+
700
+ panel.innerHTML = sorted.map(s => {
701
+ const triggers = s.triggers ?? {};
702
+ const count = skillCounts[s.name] ?? 0;
703
+ const pct = (count / max * 100).toFixed(0);
704
+ const tags = [
705
+ ...(triggers.keywords ?? []).map(k => `<span class="signal-tag keyword">${esc(k)}</span>`),
706
+ ...(triggers.file_extensions ?? []).map(e => `<span class="signal-tag extension">${esc(e)}</span>`),
707
+ ...(triggers.patterns ?? []).map(p => `<span class="signal-tag pattern">${esc(p)}</span>`),
708
+ ...(triggers.file_paths ?? []).map(f => `<span class="signal-tag path">${esc(f)}</span>`),
709
+ ];
710
+ return `<div class="skill-card">
455
711
  <div class="skill-header">
456
712
  <span class="name">${esc(s.name)}</span>
457
- <span class="desc">${esc(s.description || '')}</span>
458
713
  <span class="count">${count}</span>
459
714
  </div>
460
715
  <div class="bar-bg"><div class="bar-fill" style="width:${pct}%"></div></div>
461
716
  <div class="triggers">${tags.join('')}</div>
462
717
  </div>`;
463
- }).join('');
464
- }
465
-
466
- // --- Auto-fetch from API when served via HTTP ---
467
- (function autoFetch() {
468
- if (location.protocol === 'file:') return;
469
-
470
- loadManifestSkills();
471
-
472
- const es = new EventSource('/api/logs/stream');
473
-
474
- es.onmessage = (e) => {
475
- if (e.data.trim()) appendLog(e.data);
476
- document.getElementById('liveBadge').classList.add('visible');
477
- };
478
-
479
- es.addEventListener('reset', () => {
480
- entries = [];
481
- });
482
-
483
- es.onerror = async () => {
484
- es.close();
485
- document.getElementById('liveBadge').classList.remove('visible');
486
- // Fall back to one-shot fetch
487
- try {
488
- const res = await fetch('/api/logs');
489
- if (!res.ok) return;
490
- const text = await res.text();
491
- if (text.trim()) parseLog(text);
492
- } catch { /* fall back to drag-and-drop */ }
493
- };
494
- })();
495
-
496
- // --- File loading ---
497
- const dropZone = document.getElementById('dropZone');
498
- const fileInput = document.getElementById('fileInput');
499
-
500
- dropZone.addEventListener('click', () => fileInput.click());
501
- dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
502
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
503
- dropZone.addEventListener('drop', e => {
504
- e.preventDefault();
505
- dropZone.classList.remove('drag-over');
506
- if (e.dataTransfer.files.length) loadFile(e.dataTransfer.files[0]);
507
- });
508
- fileInput.addEventListener('change', e => { if (e.target.files.length) loadFile(e.target.files[0]); });
509
-
510
- function loadFile(file) {
511
- const reader = new FileReader();
512
- reader.onload = e => {
513
- const text = e.target.result;
514
- parseLog(text);
515
- };
516
- reader.readAsText(file);
517
- }
518
-
519
- function parseLog(text) {
520
- entries = [];
521
-
522
- for (const line of text.split('\n')) {
523
- const trimmed = line.trim();
524
- if (!trimmed) continue;
525
- try {
526
- const obj = JSON.parse(trimmed);
527
- obj._index = entries.length;
528
- entries.push(obj);
529
- } catch { /* skip malformed */ }
530
- }
531
-
532
- if (entries.length === 0) {
533
- alert('No valid log entries found in this file.');
534
- return;
535
- }
536
-
537
- dropZone.style.display = 'none';
538
- document.getElementById('dashboard').classList.add('visible');
539
- buildDashboard();
540
- populateFilters();
541
- renderTable();
542
- }
543
-
544
- // --- Dashboard ---
545
- function buildDashboard() {
546
- const total = entries.length;
547
- const activated = entries.filter(e => e.outcome === 'activated').length;
548
- const noMatch = entries.filter(e => e.outcome === 'no_match').length;
549
- const promptHooks = entries.filter(e => !e.hook_event).length;
550
- const toolHooks = entries.filter(e => e.hook_event === 'PreToolUse').length;
551
- const avgMs = (entries.reduce((s, e) => s + (e.execution_time_ms || 0), 0) / total).toFixed(1);
552
- const sessions = new Set(entries.map(e => e.session_id)).size;
553
-
554
- document.getElementById('statsGrid').innerHTML = `
718
+ }).join('');
719
+ }
720
+
721
+ // --- Auto-fetch from API when served via HTTP ---
722
+ (function autoFetch() {
723
+ if (location.protocol === 'file:') return;
724
+
725
+ loadManifestSkills();
726
+
727
+ const es = new EventSource('/api/logs/stream');
728
+
729
+ es.onmessage = (e) => {
730
+ if (e.data.trim()) appendLog(e.data);
731
+ document.getElementById('liveBadge').classList.add('visible');
732
+ };
733
+
734
+ es.addEventListener('reset', () => {
735
+ entries = [];
736
+ });
737
+
738
+ es.onerror = async () => {
739
+ es.close();
740
+ document.getElementById('liveBadge').classList.remove('visible');
741
+ // Fall back to one-shot fetch
742
+ try {
743
+ const res = await fetch('/api/logs');
744
+ if (!res.ok) return;
745
+ const text = await res.text();
746
+ if (text.trim()) parseLog(text);
747
+ } catch { /* fall back to drag-and-drop */ }
748
+ };
749
+ })();
750
+
751
+ // --- File loading ---
752
+ const dropZone = document.getElementById('dropZone');
753
+ const fileInput = document.getElementById('fileInput');
754
+
755
+ dropZone.addEventListener('click', () => fileInput.click());
756
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
757
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
758
+ dropZone.addEventListener('drop', e => {
759
+ e.preventDefault();
760
+ dropZone.classList.remove('drag-over');
761
+ if (e.dataTransfer.files.length) loadFile(e.dataTransfer.files[0]);
762
+ });
763
+ fileInput.addEventListener('change', e => { if (e.target.files.length) loadFile(e.target.files[0]); });
764
+
765
+ function loadFile(file) {
766
+ const reader = new FileReader();
767
+ reader.onload = e => {
768
+ const text = e.target.result;
769
+ parseLog(text);
770
+ };
771
+ reader.readAsText(file);
772
+ }
773
+
774
+ function parseLog(text) {
775
+ entries = [];
776
+
777
+ for (const line of text.split('\n')) {
778
+ const trimmed = line.trim();
779
+ if (!trimmed) continue;
780
+ try {
781
+ const obj = JSON.parse(trimmed);
782
+ obj._index = entries.length;
783
+ entries.push(obj);
784
+ } catch { /* skip malformed */ }
785
+ }
786
+
787
+ if (entries.length === 0) {
788
+ alert('No valid log entries found in this file.');
789
+ return;
790
+ }
791
+
792
+ dropZone.style.display = 'none';
793
+ document.getElementById('dashboard').classList.add('visible');
794
+ buildDashboard();
795
+ populateFilters();
796
+ renderTable();
797
+ }
798
+
799
+ // --- Dashboard ---
800
+ function buildDashboard() {
801
+ const total = entries.length;
802
+ const activated = entries.filter(e => e.outcome === 'activated').length;
803
+ const noMatch = entries.filter(e => e.outcome === 'no_match').length;
804
+ const promptHooks = entries.filter(e => !e.hook_event).length;
805
+ const toolHooks = entries.filter(e => e.hook_event === 'PreToolUse').length;
806
+ const avgMs = (entries.reduce((s, e) => s + (e.execution_time_ms ?? 0), 0) / total).toFixed(1);
807
+ const sessions = new Set(entries.map(e => e.session_id)).size;
808
+
809
+ document.getElementById('statsGrid').innerHTML = `
555
810
  <div class="stat-card"><div class="value">${total}</div><div class="label">Total Events</div></div>
556
- <div class="stat-card activated"><div class="value">${activated}</div><div class="label">Activated (${(activated/total*100).toFixed(0)}%)</div></div>
811
+ <div class="stat-card activated"><div class="value">${activated}</div><div class="label">Activated (${(activated / total * 100).toFixed(0)}%)</div></div>
557
812
  <div class="stat-card no-match"><div class="value">${noMatch}</div><div class="label">No Match</div></div>
558
813
  <div class="stat-card"><div class="value">${promptHooks}</div><div class="label">Prompt Hooks</div></div>
559
814
  <div class="stat-card"><div class="value">${toolHooks}</div><div class="label">Tool Hooks</div></div>
@@ -561,157 +816,157 @@ function buildDashboard() {
561
816
  <div class="stat-card"><div class="value">${sessions}</div><div class="label">Sessions</div></div>
562
817
  `;
563
818
 
564
- // Sessions
565
- const sessionMap = {};
566
- for (const e of entries) {
567
- if (!sessionMap[e.session_id]) sessionMap[e.session_id] = { count: 0, activated: 0, last: e.timestamp };
568
- sessionMap[e.session_id].count++;
569
- if (e.outcome === 'activated') sessionMap[e.session_id].activated++;
570
- if (e.timestamp > sessionMap[e.session_id].last) sessionMap[e.session_id].last = e.timestamp;
571
- }
572
- const sessionEntries = Object.entries(sessionMap).sort((a, b) => b[1].last < a[1].last ? -1 : b[1].last > a[1].last ? 1 : 0);
573
- document.getElementById('sessionList').innerHTML = sessionEntries.map(([id, info]) => `
819
+ // Sessions
820
+ const sessionMap = {};
821
+ for (const e of entries) {
822
+ if (!sessionMap[e.session_id]) sessionMap[e.session_id] = { count: 0, activated: 0, last: e.timestamp };
823
+ sessionMap[e.session_id].count++;
824
+ if (e.outcome === 'activated') sessionMap[e.session_id].activated++;
825
+ if (e.timestamp > sessionMap[e.session_id].last) sessionMap[e.session_id].last = e.timestamp;
826
+ }
827
+ const sessionEntries = Object.entries(sessionMap).sort((a, b) => b[1].last < a[1].last ? -1 : b[1].last > a[1].last ? 1 : 0);
828
+ document.getElementById('sessionList').innerHTML = sessionEntries.map(([id, info]) => `
574
829
  <div class="session-item" data-session="${id}" onclick="filterBySession('${id}')">
575
830
  <span class="id" title="${id}">${id.length > 16 ? id.slice(0, 8) + '...' : id}</span>
576
831
  <span class="meta">${info.count} events, ${info.activated} activated</span>
577
832
  </div>`).join('');
578
833
 
579
- // Skills panel is updated by renderTable() with filtered entries
580
- }
581
-
582
- function filterBySession(id) {
583
- document.getElementById('filterSession').value = id;
584
- renderTable();
585
- }
586
-
587
- // --- Filters ---
588
- function populateFilters() {
589
- const sessionSelect = document.getElementById('filterSession');
590
- const sessions = [...new Set(entries.map(e => e.session_id || 'unknown'))];
591
- sessionSelect.innerHTML = '<option value="">All</option>' +
592
- sessions.map(s => `<option value="${s}">${String(s).length > 20 ? String(s).slice(0, 8) + '...' + String(s).slice(-4) : s}</option>`).join('');
593
- }
594
-
595
- document.getElementById('filterOutcome').addEventListener('change', renderTable);
596
- document.getElementById('filterHook').addEventListener('change', renderTable);
597
- document.getElementById('filterSession').addEventListener('change', renderTable);
598
- document.getElementById('filterSearch').addEventListener('input', renderTable);
599
- document.getElementById('btnReset').addEventListener('click', () => {
600
- document.getElementById('filterOutcome').value = '';
601
- document.getElementById('filterHook').value = '';
602
- document.getElementById('filterSession').value = '';
603
- document.getElementById('filterSearch').value = '';
604
- renderTable();
605
- });
606
-
607
- // --- Sorting ---
608
- document.querySelectorAll('thead th[data-sort]').forEach(th => {
609
- th.addEventListener('click', () => {
610
- const col = th.dataset.sort;
611
- if (sortCol === col) sortAsc = !sortAsc;
612
- else { sortCol = col; sortAsc = true; }
613
- renderTable();
614
- });
615
- });
616
-
617
- function getSortValue(entry, col) {
618
- switch (col) {
619
- case 'index': return entry._index;
620
- case 'timestamp': return entry.timestamp;
621
- case 'hook': return entry.hook_event || '';
622
- case 'prompt': return entry.prompt_raw || '';
623
- case 'outcome': return entry.outcome;
624
- case 'skills': return (entry.skills_matched || []).length;
625
- case 'signals': return (entry.signals_extracted?.words_count || 0);
626
- case 'score': return Math.max(0, ...(entry.skills_matched || []).map(s => s.score));
627
- case 'threshold': return entry.threshold || 0;
628
- case 'time_ms': return entry.execution_time_ms || 0;
629
- case 'session': return entry.session_id || '';
630
- default: return 0;
631
- }
632
- }
633
-
634
- // --- Render ---
635
- function renderTable() {
636
- const outcome = document.getElementById('filterOutcome').value;
637
- const hook = document.getElementById('filterHook').value;
638
- const session = document.getElementById('filterSession').value;
639
- const search = document.getElementById('filterSearch').value.toLowerCase();
640
-
641
- let filtered = entries.filter(e => {
642
- if (outcome && e.outcome !== outcome) return false;
643
- if (hook === 'prompt' && e.hook_event) return false;
644
- if (hook === 'tool' && !e.hook_event) return false;
645
- if (session && e.session_id !== session) return false;
646
- if (search) {
647
- const haystack = [
648
- e.prompt_raw,
649
- ...(e.skills_matched || []).map(s => s.name),
650
- ...(e.skills_matched || []).flatMap(s => (s.matched_signals || []).map(sig => sig.value)),
651
- e.tool_name || '',
652
- ].join(' ').toLowerCase();
653
- if (!haystack.includes(search)) return false;
654
- }
655
- return true;
656
- });
657
-
658
- // Update skills panel with filtered data
659
- renderSkillsPanel(filtered);
660
-
661
- // Sort
662
- filtered.sort((a, b) => {
663
- const va = getSortValue(a, sortCol);
664
- const vb = getSortValue(b, sortCol);
665
- const cmp = va < vb ? -1 : va > vb ? 1 : 0;
666
- return sortAsc ? cmp : -cmp;
667
- });
668
-
669
- // Update sort arrows
670
- document.querySelectorAll('thead th[data-sort]').forEach(th => {
671
- const arrow = th.querySelector('.sort-arrow');
672
- if (th.dataset.sort === sortCol) arrow.textContent = sortAsc ? '▲' : '▼';
673
- else arrow.textContent = '';
674
- });
675
-
676
- const tbody = document.getElementById('tableBody');
677
- if (!filtered.length) {
678
- tbody.innerHTML = '<tr><td colspan="10" class="empty-state">No entries match filters</td></tr>';
679
- return;
680
- }
681
-
682
- tbody.innerHTML = filtered.map(e => {
683
- const hookType = e.hook_event
684
- ? `<span class="badge hook-tool">${e.tool_name || 'PreToolUse'}</span>`
685
- : `<span class="badge hook-prompt">Prompt</span>`;
686
-
687
- const outcomeBadge = `<span class="badge ${e.outcome === 'activated' ? 'activated' : 'no-match'}">${e.outcome}</span>`;
688
-
689
- const prompt = e.hook_event
690
- ? (e.prompt_raw || '').replace(/^\[PreToolUse:\w+\]\s*/, '')
691
- : (e.prompt_raw || '');
692
-
693
- const skillsHtml = (e.skills_matched || []).length
694
- ? (e.skills_matched || []).map(s => `
834
+ // Skills panel is updated by renderTable() with filtered entries
835
+ }
836
+
837
+ function filterBySession(id) {
838
+ document.getElementById('filterSession').value = id;
839
+ renderTable();
840
+ }
841
+
842
+ // --- Filters ---
843
+ function populateFilters() {
844
+ const sessionSelect = document.getElementById('filterSession');
845
+ const sessions = [...new Set(entries.map(e => e.session_id ?? 'unknown'))];
846
+ sessionSelect.innerHTML = '<option value="">All</option>' +
847
+ sessions.map(s => `<option value="${s}">${String(s).length > 20 ? String(s).slice(0, 8) + '...' + String(s).slice(-4) : s}</option>`).join('');
848
+ }
849
+
850
+ document.getElementById('filterOutcome').addEventListener('change', renderTable);
851
+ document.getElementById('filterHook').addEventListener('change', renderTable);
852
+ document.getElementById('filterSession').addEventListener('change', renderTable);
853
+ document.getElementById('filterSearch').addEventListener('input', renderTable);
854
+ document.getElementById('btnReset').addEventListener('click', () => {
855
+ document.getElementById('filterOutcome').value = '';
856
+ document.getElementById('filterHook').value = '';
857
+ document.getElementById('filterSession').value = '';
858
+ document.getElementById('filterSearch').value = '';
859
+ renderTable();
860
+ });
861
+
862
+ // --- Sorting ---
863
+ document.querySelectorAll('thead th[data-sort]').forEach(th => {
864
+ th.addEventListener('click', () => {
865
+ const col = th.dataset.sort;
866
+ if (sortCol === col) sortAsc = !sortAsc;
867
+ else { sortCol = col; sortAsc = true; }
868
+ renderTable();
869
+ });
870
+ });
871
+
872
+ function getSortValue(entry, col) {
873
+ switch (col) {
874
+ case 'index': return entry._index;
875
+ case 'timestamp': return entry.timestamp;
876
+ case 'hook': return entry.hook_event ?? '';
877
+ case 'prompt': return entry.prompt_raw ?? '';
878
+ case 'outcome': return entry.outcome;
879
+ case 'skills': return (entry.skills_matched ?? []).length;
880
+ case 'signals': return (entry.signals_extracted?.words_count ?? 0);
881
+ case 'score': return Math.max(0, ...(entry.skills_matched ?? []).map(s => s.score));
882
+ case 'threshold': return entry.threshold ?? 0;
883
+ case 'time_ms': return entry.execution_time_ms ?? 0;
884
+ case 'session': return entry.session_id ?? '';
885
+ default: return 0;
886
+ }
887
+ }
888
+
889
+ // --- Render ---
890
+ function renderTable() {
891
+ const outcome = document.getElementById('filterOutcome').value;
892
+ const hook = document.getElementById('filterHook').value;
893
+ const session = document.getElementById('filterSession').value;
894
+ const search = document.getElementById('filterSearch').value.toLowerCase();
895
+
896
+ let filtered = entries.filter(e => {
897
+ if (outcome && e.outcome !== outcome) return false;
898
+ if (hook === 'prompt' && e.hook_event) return false;
899
+ if (hook === 'tool' && !e.hook_event) return false;
900
+ if (session && e.session_id !== session) return false;
901
+ if (search) {
902
+ const haystack = [
903
+ e.prompt_raw,
904
+ ...(e.skills_matched ?? []).map(s => s.name),
905
+ ...(e.skills_matched ?? []).flatMap(s => (s.matched_signals ?? []).map(sig => sig.value)),
906
+ e.tool_name ?? '',
907
+ ].join(' ').toLowerCase();
908
+ if (!haystack.includes(search)) return false;
909
+ }
910
+ return true;
911
+ });
912
+
913
+ // Update skills panel with filtered data
914
+ renderSkillsPanel(filtered);
915
+
916
+ // Sort
917
+ filtered.sort((a, b) => {
918
+ const va = getSortValue(a, sortCol);
919
+ const vb = getSortValue(b, sortCol);
920
+ const cmp = va < vb ? -1 : va > vb ? 1 : 0;
921
+ return sortAsc ? cmp : -cmp;
922
+ });
923
+
924
+ // Update sort arrows
925
+ document.querySelectorAll('thead th[data-sort]').forEach(th => {
926
+ const arrow = th.querySelector('.sort-arrow');
927
+ if (th.dataset.sort === sortCol) arrow.textContent = sortAsc ? '▲' : '▼';
928
+ else arrow.textContent = '';
929
+ });
930
+
931
+ const tbody = document.getElementById('tableBody');
932
+ if (!filtered.length) {
933
+ tbody.innerHTML = '<tr><td colspan="10" class="empty-state">No entries match filters</td></tr>';
934
+ return;
935
+ }
936
+
937
+ tbody.innerHTML = filtered.map(e => {
938
+ const hookType = e.hook_event
939
+ ? `<span class="badge hook-tool">${e.tool_name ?? 'PreToolUse'}</span>`
940
+ : `<span class="badge hook-prompt">Prompt</span>`;
941
+
942
+ const outcomeBadge = `<span class="badge ${e.outcome === 'activated' ? 'activated' : 'no-match'}">${e.outcome}</span>`;
943
+
944
+ const prompt = e.hook_event
945
+ ? (e.prompt_raw ?? '').replace(/^\[PreToolUse:\w+\]\s*/, '')
946
+ : (e.prompt_raw ?? '');
947
+
948
+ const skillsHtml = (e.skills_matched ?? []).length
949
+ ? (e.skills_matched ?? []).map(s => `
695
950
  <div class="skill-match">
696
951
  <span class="skill-name">${esc(s.name)}</span>
697
952
  <span class="skill-score">${s.score}</span>
698
953
  </div>`).join('')
699
- : '<span style="color:var(--text-muted);font-size:12px">—</span>';
954
+ : '<span style="color:var(--text-muted);font-size:12px">—</span>';
700
955
 
701
- const signalsHtml = (e.skills_matched || []).flatMap(s =>
702
- (s.matched_signals || []).map(sig =>
703
- `<span class="signal-tag ${sig.type}">${sig.type}:${esc(sig.value)}</span>`)
704
- ).join('') || '<span style="color:var(--text-muted);font-size:12px">—</span>';
956
+ const signalsHtml = (e.skills_matched ?? []).flatMap(s =>
957
+ (s.matched_signals ?? []).map(sig =>
958
+ `<span class="signal-tag ${sig.type}">${sig.type}:${esc(sig.value)}</span>`)
959
+ ).join('') ?? '<span style="color:var(--text-muted);font-size:12px">—</span>';
705
960
 
706
- const topScore = (e.skills_matched || []).length
707
- ? Math.max(...(e.skills_matched || []).map(s => s.score))
708
- : '';
961
+ const topScore = (e.skills_matched ?? []).length
962
+ ? Math.max(...(e.skills_matched ?? []).map(s => s.score))
963
+ : '';
709
964
 
710
- const ts = formatTime(e.timestamp);
711
- const ms = e.execution_time_ms || 0;
712
- const msClass = ms > 3 ? 'slow' : '';
965
+ const ts = formatTime(e.timestamp);
966
+ const ms = e.execution_time_ms ?? 0;
967
+ const msClass = ms > 3 ? 'slow' : '';
713
968
 
714
- return `<tr>
969
+ return `<tr>
715
970
  <td style="color:var(--text-muted);font-size:12px">${e._index + 1}</td>
716
971
  <td style="white-space:nowrap;font-size:12px" title="${e.timestamp}">${ts}</td>
717
972
  <td>${hookType}</td>
@@ -719,26 +974,27 @@ function renderTable() {
719
974
  <td>${outcomeBadge}</td>
720
975
  <td class="skills-cell">${skillsHtml}</td>
721
976
  <td>${signalsHtml}</td>
722
- <td style="font-weight:600;color:${topScore ? 'var(--orange)' : 'var(--text-muted)'}">${topScore || '—'}</td>
977
+ <td style="font-weight:600;color:${topScore ? 'var(--orange)' : 'var(--text-muted)'}">${topScore ?? '—'}</td>
723
978
  <td style="color:var(--text-muted)">${e.threshold}</td>
724
979
  <td><span class="time-ms ${msClass}">${ms}</span></td>
725
980
  </tr>`;
726
- }).join('');
727
- }
728
-
729
- function formatTime(ts) {
730
- try {
731
- const d = new Date(ts);
732
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' +
733
- d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
734
- } catch { return ts; }
735
- }
736
-
737
- function esc(str) {
738
- const d = document.createElement('div');
739
- d.textContent = str || '';
740
- return d.innerHTML;
741
- }
742
- </script>
981
+ }).join('');
982
+ }
983
+
984
+ function formatTime(ts) {
985
+ try {
986
+ const d = new Date(ts);
987
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' +
988
+ d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
989
+ } catch { return ts; }
990
+ }
991
+
992
+ function esc(str) {
993
+ const d = document.createElement('div');
994
+ d.textContent = str ?? '';
995
+ return d.innerHTML;
996
+ }
997
+ </script>
743
998
  </body>
999
+
744
1000
  </html>