happenin 0.2.0 → 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.
- package/README.md +36 -3
- package/assets/help.md +9 -2
- package/dist/bin.js +181 -90
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -9,6 +9,16 @@ A macOS CLI that records Cursor and Claude Code agent events to a local SQLite d
|
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
12
|
+
## Try it
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx -y happenin install # add record hooks to Cursor and Claude Code
|
|
16
|
+
npx -y happenin import # import existing transcripts
|
|
17
|
+
npx -y happenin dashboard # open the live dashboard
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No global install needed: hooks installed this way run through `npx`. For a permanent setup, use the [global install](#install).
|
|
21
|
+
|
|
12
22
|
## Why
|
|
13
23
|
|
|
14
24
|
Cursor and Claude Code can emit local hooks for each session, tool use, prompt, file edit, and lifecycle event. `happenin` adds `record` hooks to those agents, writes the payloads to a local SQLite database, and serves a live browser dashboard. Data never leaves your machine.
|
|
@@ -58,7 +68,7 @@ The dashboard opens at `http://localhost:8765`. New events appear automatically.
|
|
|
58
68
|
|
|
59
69
|
### `happenin install [--cursor] [--claude]`
|
|
60
70
|
|
|
61
|
-
Backs up and appends `happenin record` hooks to `~/.cursor/hooks.json` and `~/.claude/settings.json`. Backups are written to `~/.happenin/backups/`.
|
|
71
|
+
Backs up and appends `happenin record` hooks to `~/.cursor/hooks.json` and `~/.claude/settings.json`. Backups are written to `~/.happenin/backups/`. Re-running `install` replaces the previous `happenin` hooks instead of duplicating them, and hooks installed through `npx -y happenin install` are written as `npx -y happenin` commands so they keep working without a global install.
|
|
62
72
|
|
|
63
73
|
```bash
|
|
64
74
|
happenin install --cursor
|
|
@@ -79,15 +89,21 @@ echo '{"hook_event_name":"sessionStart","sessionId":"abc123"}' | happenin record
|
|
|
79
89
|
echo '{"sessionId":"abc123"}' | happenin record claude SessionStart
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
### `happenin import`
|
|
92
|
+
### `happenin import [--force]`
|
|
83
93
|
|
|
84
94
|
Imports existing transcripts:
|
|
85
95
|
|
|
86
|
-
- Claude Code JSONL from `~/.claude/projects/<project>/<session>.jsonl`.
|
|
96
|
+
- Claude Code JSONL from `~/.claude/projects/<project>/<session>.jsonl`. Each `tool_use` block in an
|
|
97
|
+
assistant message becomes its own event with `tool_name` set (and `file_path` for file-editing
|
|
98
|
+
tools, `skill_name` for `Skill` calls), so tool usage, skills used, and files touched are
|
|
99
|
+
queryable per session and show up on the dashboard.
|
|
87
100
|
- Cursor `prompt_history.json` and `meta.json` from `~/.cursor/chats/<hash>/<session>/`.
|
|
88
101
|
|
|
89
102
|
`store.db` is skipped because it is encrypted.
|
|
90
103
|
|
|
104
|
+
Files are only re-parsed when their modification time changes; pass `--force` to clear that
|
|
105
|
+
tracking and re-import everything from scratch (useful after upgrading `happenin`).
|
|
106
|
+
|
|
91
107
|
### `happenin query [options]`
|
|
92
108
|
|
|
93
109
|
Query events from the local database and print them as JSON, JSONL, or a summary.
|
|
@@ -120,6 +136,23 @@ Starts a local HTTP server and opens the dashboard.
|
|
|
120
136
|
- `--no-open` — do not open the browser.
|
|
121
137
|
- `--silent` — alias for `--no-open`; used automatically by `npm start`.
|
|
122
138
|
|
|
139
|
+
The main screen shows total sessions/events, average duration, and success rate for the selected
|
|
140
|
+
date range, plus charts for the top 10 tools, top 10 skills, and top 10 markdown files touched.
|
|
141
|
+
Clicking a tool, skill, or file jumps to a list of every session that used it. The markdown files
|
|
142
|
+
chart can be scoped to one directory with the "md files directory" filter (the common path prefix
|
|
143
|
+
is stripped from the dropdown labels, and paths under `/private` are excluded).
|
|
144
|
+
|
|
145
|
+
A context breakdown widget splits recorded payload size (and, for imported Claude sessions, token
|
|
146
|
+
usage) into four buckets: MCP server calls, markdown file reads, bloatware (skill loads and
|
|
147
|
+
lifecycle/hook noise), and actual value (real tool calls and conversation turns). It appears on the
|
|
148
|
+
main dashboard for the current filter/date range, and again inside a session's detail view scoped
|
|
149
|
+
to that session. This is a heuristic, not an exact accounting — Claude Code does not log its system
|
|
150
|
+
prompt or tool schemas to the transcript, so there is no ground truth to bucket against.
|
|
151
|
+
|
|
152
|
+
The "Recent Sessions" bar above the charts is collapsed by default — clicking it replaces the
|
|
153
|
+
metrics/charts with the full session list; clicking it again (or picking a session) returns to the
|
|
154
|
+
previous view.
|
|
155
|
+
|
|
123
156
|
```bash
|
|
124
157
|
happenin dashboard --port 9000 --silent
|
|
125
158
|
```
|
package/assets/help.md
CHANGED
|
@@ -7,6 +7,9 @@ A macOS CLI that records Cursor and Claude Code agent events to a local SQLite d
|
|
|
7
7
|
### `happenin install [--cursor] [--claude]`
|
|
8
8
|
|
|
9
9
|
Backs up and appends `happenin record` hooks to your Cursor and Claude Code configuration files.
|
|
10
|
+
Re-running install replaces previous happenin hooks instead of duplicating them. When happenin
|
|
11
|
+
runs through npx, hooks are written as `npx -y happenin` commands so they keep working without a
|
|
12
|
+
global install.
|
|
10
13
|
|
|
11
14
|
- `--cursor` — install Cursor hooks only.
|
|
12
15
|
- `--claude` — install Claude Code hooks only.
|
|
@@ -22,15 +25,19 @@ The hook target. Reads a JSON payload from stdin, writes it to `~/.happenin/happ
|
|
|
22
25
|
|
|
23
26
|
This command is normally called by the agent hooks, not directly.
|
|
24
27
|
|
|
25
|
-
### `happenin import`
|
|
28
|
+
### `happenin import [--force]`
|
|
26
29
|
|
|
27
30
|
Imports existing transcripts:
|
|
28
31
|
|
|
29
|
-
- Claude Code JSONL from `~/.claude/projects/<project>/<session>.jsonl`.
|
|
32
|
+
- Claude Code JSONL from `~/.claude/projects/<project>/<session>.jsonl`. Each `tool_use` block in an
|
|
33
|
+
assistant message becomes its own event with `tool_name` set (and `file_path` for file-editing
|
|
34
|
+
tools, `skill_name` for `Skill` calls).
|
|
30
35
|
- Cursor `prompt_history.json` and `meta.json` from `~/.cursor/chats/<hash>/<session>/`.
|
|
31
36
|
|
|
32
37
|
`store.db` is skipped because it is encrypted.
|
|
33
38
|
|
|
39
|
+
- `--force` — clear import tracking and re-import every discovered file from scratch.
|
|
40
|
+
|
|
34
41
|
### `happenin query [options]`
|
|
35
42
|
|
|
36
43
|
Query events from the local database and print them as JSON, JSONL, or a summary.
|
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameSync as i,statSync as a,writeFileSync as o}from"node:fs";import s from"node:path";import c from"node:process";import{fileURLToPath as l}from"node:url";import{homedir as u}from"node:os";import{DatabaseSync as d}from"node:sqlite";import f from"node:http";import{execFile as p}from"node:child_process";const m=[`sessionStart`,`sessionEnd`,`beforeSubmitPrompt`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`afterAgentResponse`,`afterAgentThought`,`preCompact`,`stop`],h=`SessionStart.SessionEnd.Setup.UserPromptSubmit.UserPromptExpansion.PreToolUse.PermissionRequest.PermissionDenied.PostToolUse.PostToolUseFailure.PostToolBatch.Notification.MessageDisplay.SubagentStart.SubagentStop.TaskCreated.TaskCompleted.Stop.StopFailure.TeammateIdle.InstructionsLoaded.ConfigChange.CwdChanged.DirectoryAdded.FileChanged.WorktreeCreate.WorktreeRemove.PreCompact.PostCompact.Elicitation.ElicitationResult`.split(`.`),g=new Set([`beforeSubmitPrompt`]),_=new Set([`preToolUse`,`beforeShellExecution`,`beforeMCPExecution`,`beforeReadFile`,`subagentStart`]),
|
|
3
|
-
`,`utf8`),i(n,e)},se=(e,t)=>{if(
|
|
4
|
-
`),t&&c.stdout.write(` backup: ${t}\n`)}async function ge(e=[]){try{let{cursor:t,claude:n}=me(e);await he({cursor:t,claude:n})}catch(e){c.stderr.write(`install failed: ${
|
|
2
|
+
import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameSync as i,statSync as a,writeFileSync as o}from"node:fs";import s from"node:path";import c from"node:process";import{fileURLToPath as l}from"node:url";import{homedir as u}from"node:os";import{DatabaseSync as d}from"node:sqlite";import f from"node:http";import{execFile as p}from"node:child_process";const m=[`sessionStart`,`sessionEnd`,`beforeSubmitPrompt`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`afterAgentResponse`,`afterAgentThought`,`preCompact`,`stop`],h=`SessionStart.SessionEnd.Setup.UserPromptSubmit.UserPromptExpansion.PreToolUse.PermissionRequest.PermissionDenied.PostToolUse.PostToolUseFailure.PostToolBatch.Notification.MessageDisplay.SubagentStart.SubagentStop.TaskCreated.TaskCompleted.Stop.StopFailure.TeammateIdle.InstructionsLoaded.ConfigChange.CwdChanged.DirectoryAdded.FileChanged.WorktreeCreate.WorktreeRemove.PreCompact.PostCompact.Elicitation.ElicitationResult`.split(`.`),g=new Set([`beforeSubmitPrompt`]),_=new Set([`preToolUse`,`beforeShellExecution`,`beforeMCPExecution`,`beforeReadFile`,`subagentStart`]),v=new Set([`UserPromptSubmit`,`UserPromptExpansion`]),y=new Set([`PreToolUse`,`PermissionRequest`,`SubagentStart`,`SubagentStop`,`TaskCreated`,`TaskCompleted`,`Stop`,`WorktreeCreate`,`WorktreeRemove`]),ee=(e,t)=>{if(e===`cursor`)return g.has(t)?JSON.stringify({continue:!0}):_.has(t)?JSON.stringify({permission:`allow`}):void 0;if(e===`claude`)return v.has(t)?JSON.stringify({continue:!0}):y.has(t)?JSON.stringify({decision:`approve`}):void 0},b=e=>typeof e==`object`&&!!e&&!Array.isArray(e),te=e=>e instanceof Error?e.message:String(e),x=()=>c.env.HOME||u(),ne=()=>{let e=c.argv[1];return e&&s.isAbsolute(e)?e.split(s.sep).includes(`_npx`)?`npx -y happenin`:e:`happenin`},re=(e,t)=>e.includes(t),ie=e=>{t(e,{recursive:!0})},S=t=>e(t),ae=e=>{let t=n(e,`utf8`);return JSON.parse(t)},oe=(e,t)=>{let n=`${e}.${Date.now()}.tmp`;ie(s.dirname(e)),o(n,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,`utf8`),i(n,e)},se=(e,t)=>{if(ie(t),!S(e))return;let r=s.join(t,`${s.basename(e)}.${Date.now()}.bak`);return o(r,n(e,`utf8`),`utf8`),r},ce=e=>{if(!S(e))return{version:1,hooks:{}};let t=ae(e);if(!b(t))throw Error(`Invalid JSON object in ${e}`);let n=b(t.hooks)?t.hooks:{};return{...t,version:typeof t.version==`number`?t.version:1,hooks:n}},le=e=>{if(!S(e))return{hooks:{}};let t=ae(e);if(!b(t))throw Error(`Invalid JSON object in ${e}`);let n=b(t.hooks)?t.hooks:{};return{...t,hooks:n}},ue=(e,t,n)=>{e[t]=[...(Array.isArray(e[t])?e[t]:[]).filter(e=>!b(e)||typeof e.command!=`string`||!re(e.command,`happenin record cursor`)),{command:`${n} record cursor`}]},de=(e,t,n)=>{let r=`happenin record claude ${t}`;e[t]=[...(Array.isArray(e[t])?e[t]:[]).map(e=>b(e)&&Array.isArray(e.hooks)?{...e,hooks:e.hooks.filter(e=>!b(e)||typeof e.command!=`string`||!re(e.command,r))}:e).filter(e=>!(b(e)&&Array.isArray(e.hooks)&&e.hooks.length===0)),{matcher:``,hooks:[{type:`command`,command:`${n} record claude ${t}`}]}]},fe=e=>{let t=x(),n=s.join(t,`.cursor`,`hooks.json`),r=s.join(t,`.happenin`,`backups`,`cursor`),i=se(n,r),a=ce(n);for(let t of m)ue(a.hooks,t,e);return oe(n,a),{target:n,backup:i}},pe=e=>{let t=x(),n=s.join(t,`.claude`,`settings.json`),r=s.join(t,`.happenin`,`backups`,`claude`),i=se(n,r),a=le(n);for(let t of h)de(a.hooks,t,e);return oe(n,a),{target:n,backup:i}},me=e=>{let t=!1,n=!1;for(let r of e)r===`--cursor`&&(t=!0),r===`--claude`&&(n=!0);return!t&&!n&&(t=!0,n=!0),{cursor:t,claude:n}};async function he(e={}){let{cursor:t=!0,claude:n=!0}=e,r=ne(),i=[];t&&i.push(fe(r)),n&&i.push(pe(r));for(let{target:e,backup:t}of i)c.stdout.write(`${e} written
|
|
4
|
+
`),t&&c.stdout.write(` backup: ${t}\n`)}async function ge(e=[]){try{let{cursor:t,claude:n}=me(e);await he({cursor:t,claude:n})}catch(e){c.stderr.write(`install failed: ${te(e)}\n`),c.exitCode=1}}const _e=[`CREATE INDEX IF NOT EXISTS idx_events_source ON events(source);`,`CREATE INDEX IF NOT EXISTS idx_events_event ON events(event);`,`CREATE INDEX IF NOT EXISTS idx_events_session_id ON events(session_id);`,`CREATE INDEX IF NOT EXISTS idx_events_happened_at ON events(happened_at);`,`CREATE INDEX IF NOT EXISTS idx_events_received_at ON events(received_at);`,`CREATE INDEX IF NOT EXISTS idx_events_source_path ON events(source_path);`,`CREATE INDEX IF NOT EXISTS idx_events_subagent_id ON events(subagent_id);`];function C(e){let t=e.prepare(`PRAGMA user_version`).get();return Number(t?.user_version??0)}function ve(e){let t=C(e);if(!(t>=1)){if(e.exec(`BEGIN IMMEDIATE;`),t=C(e),t>=1){e.exec(`ROLLBACK;`);return}try{let t=e.prepare(`PRAGMA table_info(events)`).all(),n=new Set(t.map(e=>e.name)),r=t=>{n.has(t)||e.exec(`ALTER TABLE events ADD COLUMN ${t} TEXT;`)};r(`subagent_id`),r(`subagent_type`),r(`transcript_path`),e.exec(`PRAGMA user_version = 1;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}}function ye(e){let t=C(e);if(!(t>=3)){if(e.exec(`BEGIN IMMEDIATE;`),t=C(e),t>=3){e.exec(`ROLLBACK;`);return}try{e.prepare(`PRAGMA table_info(events)`).all().some(e=>e.name===`skill_name`)||e.exec(`ALTER TABLE events ADD COLUMN skill_name TEXT;`),e.exec(`PRAGMA user_version = 3;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}}const be=e=>{let t=C(e);if(!(t>=2)){if(e.exec(`BEGIN IMMEDIATE;`),t=C(e),t>=2){e.exec(`ROLLBACK;`);return}try{let t=e.prepare(`SELECT id, payload FROM events WHERE happened_at IS NULL AND json_valid(payload) = 1`).all(),n=e.prepare(`UPDATE events SET happened_at = ? WHERE id = ?`);for(let e of t){let t=we(JSON.parse(e.payload));t!==void 0&&n.run(t,e.id)}e.exec(`
|
|
5
5
|
UPDATE events
|
|
6
6
|
SET happened_at = strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch')
|
|
7
7
|
WHERE happened_at IS NULL;
|
|
@@ -24,7 +24,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
24
24
|
WHERE
|
|
25
25
|
project_path IS NULL
|
|
26
26
|
AND json_valid(payload) = 1;
|
|
27
|
-
`),e.exec(`PRAGMA user_version = 2;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}},
|
|
27
|
+
`),e.exec(`PRAGMA user_version = 2;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}},xe=e=>{let t=C(e);if(t<1)throw Error(`Database schema must be migrated before backfilling subagent metadata`);if(e.exec(`BEGIN IMMEDIATE;`),t=C(e),t<1){e.exec(`ROLLBACK;`);return}try{e.exec(`
|
|
28
28
|
UPDATE events
|
|
29
29
|
SET
|
|
30
30
|
subagent_id = NULLIF(trim(json_extract(payload, '$.subagent_id')), ''),
|
|
@@ -64,7 +64,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
64
64
|
WHERE s.event = 'subagentStart'
|
|
65
65
|
AND s.subagent_id = json_extract(events.payload, '$.tool_use_id')
|
|
66
66
|
);
|
|
67
|
-
`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}},
|
|
67
|
+
`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}},w=()=>c.env.HAPPENIN_DB?c.env.HAPPENIN_DB:s.join(u(),`.happenin`,`happenin.db`),T=(e,n=5e3)=>{let r=e??w();t(s.dirname(r),{recursive:!0});let i=new d(r);try{i.exec(`PRAGMA busy_timeout = ${n};`),i.exec(`PRAGMA journal_mode = WAL;`),i.exec(`CREATE TABLE IF NOT EXISTS events (
|
|
68
68
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
69
|
source TEXT NOT NULL,
|
|
70
70
|
client TEXT,
|
|
@@ -75,6 +75,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
75
75
|
project_path TEXT,
|
|
76
76
|
file_path TEXT,
|
|
77
77
|
tool_name TEXT,
|
|
78
|
+
skill_name TEXT,
|
|
78
79
|
payload TEXT NOT NULL,
|
|
79
80
|
source_path TEXT,
|
|
80
81
|
subagent_id TEXT,
|
|
@@ -86,7 +87,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
86
87
|
mtime INTEGER NOT NULL,
|
|
87
88
|
imported_at INTEGER NOT NULL
|
|
88
89
|
);
|
|
89
|
-
`),ve(i),ye(i);for(let e of _e)i.exec(e);return i}catch(e){try{i.close()}catch{}throw e}},
|
|
90
|
+
`),ve(i),be(i),ye(i);for(let e of _e)i.exec(e);return i}catch(e){try{i.close()}catch{}throw e}},Se=[`happenedAt`,`timestamp`,`happened_at`,`time`,`ts`,`createdAt`,`created_at`];function Ce(e){if(e!=null){if(typeof e==`number`){if(!Number.isFinite(e))return;try{return new Date(e).toISOString()}catch{return}}if(typeof e==`string`){let t=e.trim();if(t.length===0)return;if(/^\d+$/.test(t)){let e=Number(t);if(!Number.isFinite(e))return;try{return new Date(e).toISOString()}catch{return}}return Number.isNaN(Date.parse(t))?void 0:t}}}function we(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;for(let e of Se){let n=Ce(Reflect.get(t,e));if(n!==void 0)return n}}const E=(e,t)=>{let n=e.prepare(`
|
|
90
91
|
INSERT INTO events (
|
|
91
92
|
source,
|
|
92
93
|
client,
|
|
@@ -97,13 +98,14 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
97
98
|
project_path,
|
|
98
99
|
file_path,
|
|
99
100
|
tool_name,
|
|
101
|
+
skill_name,
|
|
100
102
|
payload,
|
|
101
103
|
source_path,
|
|
102
104
|
subagent_id,
|
|
103
105
|
subagent_type,
|
|
104
106
|
transcript_path
|
|
105
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
106
|
-
`),r=Date.now(),i=
|
|
107
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
108
|
+
`),r=Date.now(),i=Ce(t.happenedAt)??new Date(r).toISOString();n.run(t.source,t.client??null,t.event??null,t.sessionId??null,i,r,t.projectPath??null,t.filePath??null,t.toolName??null,t.skillName??null,t.payload,t.sourcePath??null,t.subagentId??null,t.subagentType??null,t.transcriptPath??null)},D=`COALESCE(strftime('%Y-%m-%dT%H:%M:%fZ', happened_at), strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch'))`;function Te(e){return(e===`7d`?168:e===`30d`?720:24)*60*60*1e3}function O(e,t=Date.now()){let n=[],r=[];if(e.since!==void 0&&(n.push(`id > ?`),r.push(e.since)),e.source!==void 0&&(n.push(`source = ?`),r.push(e.source)),e.event!==void 0&&(n.push(`event = ?`),r.push(e.event)),e.tool!==void 0&&e.tool!==``&&(n.push(`tool_name = ?`),r.push(e.tool)),e.skill!==void 0&&e.skill!==``&&(n.push(`skill_name = ?`),r.push(e.skill)),e.file!==void 0&&e.file!==``&&(n.push(`file_path = ?`),r.push(e.file)),e.sessionId!==void 0&&e.sessionId!==``&&(e.sessionIdExact?(n.push(`session_id = ?`),r.push(e.sessionId)):(n.push(`session_id LIKE ?`),r.push(`%${e.sessionId}%`))),e.sessionIds!==void 0&&e.sessionIds.length>0){let t=e.sessionIds.filter(e=>e!==null),i=e.sessionIds.some(e=>e===null),a=[];if(t.length>0){let e=t.map(()=>`?`).join(`,`);a.push(`session_id IN (${e})`);for(let e of t)r.push(e)}i&&a.push(`session_id IS NULL`),n.push(a.length===1?a[0]:`(${a.join(` OR `)})`)}return e.subagentId!==void 0&&e.subagentId!==``&&(n.push(`subagent_id = ?`),r.push(e.subagentId)),e.q!==void 0&&e.q!==``&&(n.push(`(payload LIKE ? OR session_id LIKE ?)`),r.push(`%${e.q}%`),r.push(`%${e.q}%`)),e.range!==void 0&&e.range!==`all`&&(n.push(`${D} >= ?`),r.push(new Date(t-Te(e.range)).toISOString())),{clause:n.length>0?`WHERE ${n.join(` AND `)}`:``,params:r}}const k=(e,t)=>{let{clause:n,params:r}=O(t),i=t.limit??100,a=t.offset??0,o=`
|
|
107
109
|
SELECT
|
|
108
110
|
id,
|
|
109
111
|
source,
|
|
@@ -115,13 +117,14 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
115
117
|
project_path AS projectPath,
|
|
116
118
|
file_path AS filePath,
|
|
117
119
|
tool_name AS toolName,
|
|
120
|
+
skill_name AS skillName,
|
|
118
121
|
payload,
|
|
119
122
|
source_path AS sourcePath,
|
|
120
123
|
subagent_id AS subagentId,
|
|
121
124
|
subagent_type AS subagentType,
|
|
122
125
|
transcript_path AS transcriptPath
|
|
123
126
|
FROM events
|
|
124
|
-
${n} ORDER BY id DESC LIMIT ?${a>0?` OFFSET ?`:``}`;return r.push(i),a>0&&r.push(a),e.prepare(o).all(...r)},A=(e,t)=>{let{clause:n,params:r}=O(t),i=`SELECT COUNT(*) AS count FROM events ${n}`,a=e.prepare(i).get(...r);return a?Number(a.count):0},
|
|
127
|
+
${n} ORDER BY id DESC LIMIT ?${a>0?` OFFSET ?`:``}`;return r.push(i),a>0&&r.push(a),e.prepare(o).all(...r)},A=(e,t)=>{let{clause:n,params:r}=O(t),i=`SELECT COUNT(*) AS count FROM events ${n}`,a=e.prepare(i).get(...r);return a?Number(a.count):0},Ee=(e,t)=>{let{clause:n,params:r}=O(t);return{total:A(e,t),bySource:e.prepare(`SELECT source, COUNT(*) AS count FROM events ${n} GROUP BY source ORDER BY count DESC`).all(...r),byEvent:e.prepare(`SELECT event, COUNT(*) AS count FROM events ${n} GROUP BY event ORDER BY count DESC`).all(...r),bySession:e.prepare(`SELECT session_id AS sessionId, COUNT(*) AS count, MIN(happened_at) AS firstAt, MAX(happened_at) AS lastAt FROM events ${n} GROUP BY session_id ORDER BY count DESC`).all(...r)}};function De(e){let t=Number(e.firstReceivedAt),n=Number(e.lastReceivedAt),r=JSON.parse(e.projectPaths),i=JSON.parse(e.toolNames),a=JSON.parse(e.skillNames),o=JSON.parse(e.filePaths),s=e.firstAt?Date.parse(e.firstAt):NaN,c=e.lastAt?Date.parse(e.lastAt):NaN,l=!Number.isNaN(s)&&!Number.isNaN(c)&&c>=s?c-s:n-t;return{sessionId:e.sessionId,subagentId:e.subagentId,subagentType:e.subagentType,firstAt:e.firstAt,lastAt:e.lastAt,firstReceivedAt:t,lastReceivedAt:n,durationMs:l,eventCount:Number(e.eventCount),projectPath:r[0]??null,projectPaths:r,tools:i,skills:a,files:o,failureCount:Number(e.failureCount)}}const Oe=(e,t,n=Date.now())=>{let{clause:r,params:i}=O(t,n),a=t.limit??100,o=t.offset??0,s=`
|
|
125
128
|
SELECT
|
|
126
129
|
session_id AS sessionId,
|
|
127
130
|
COUNT(*) AS eventCount,
|
|
@@ -131,13 +134,38 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
131
134
|
MAX(happened_at) AS lastAt,
|
|
132
135
|
COALESCE(json_group_array(DISTINCT project_path ORDER BY project_path) FILTER (WHERE project_path IS NOT NULL AND project_path <> ''), '[]') AS projectPaths,
|
|
133
136
|
COALESCE(json_group_array(DISTINCT tool_name ORDER BY tool_name) FILTER (WHERE tool_name IS NOT NULL AND tool_name <> ''), '[]') AS toolNames,
|
|
137
|
+
COALESCE(json_group_array(DISTINCT skill_name ORDER BY skill_name) FILTER (WHERE skill_name IS NOT NULL AND skill_name <> ''), '[]') AS skillNames,
|
|
138
|
+
COALESCE(json_group_array(DISTINCT file_path ORDER BY file_path) FILTER (WHERE file_path IS NOT NULL AND file_path <> ''), '[]') AS filePaths,
|
|
134
139
|
SUM(CASE WHEN event LIKE '%Failure%' THEN 1 ELSE 0 END) AS failureCount
|
|
135
140
|
FROM events
|
|
136
141
|
${r}
|
|
137
142
|
GROUP BY session_id
|
|
138
143
|
ORDER BY lastReceivedAt DESC, sessionId
|
|
139
144
|
LIMIT ?${o>0?` OFFSET ?`:``}
|
|
140
|
-
`;return i.push(a),o>0&&i.push(o),e.prepare(s).all(...i).map(
|
|
145
|
+
`;return i.push(a),o>0&&i.push(o),e.prepare(s).all(...i).map(De)},ke=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=O(t,r),o=`tool_name IS NOT NULL AND tool_name <> ''`,s=`SELECT tool_name AS tool, COUNT(*) AS count FROM events ${i?`${i} AND ${o}`:`WHERE ${o}`} GROUP BY tool_name ORDER BY count DESC, tool_name ASC LIMIT ?`;return e.prepare(s).all(...a,n)},Ae=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=O(t,r),o=`skill_name IS NOT NULL AND skill_name <> ''`,s=`SELECT skill_name AS skill, COUNT(*) AS count FROM events ${i?`${i} AND ${o}`:`WHERE ${o}`} GROUP BY skill_name ORDER BY count DESC, skill_name ASC LIMIT ?`;return e.prepare(s).all(...a,n)},je=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=O(t,r),o=[`file_path IS NOT NULL`,`file_path <> ''`,`LOWER(file_path) LIKE '%.md'`];t.mdDir&&o.push(`project_path = ?`);let s=o.join(` AND `),c=`SELECT file_path AS file, COUNT(*) AS count FROM events ${i?`${i} AND ${s}`:`WHERE ${s}`} GROUP BY file_path ORDER BY count DESC, file_path ASC LIMIT ?`,l=e.prepare(c),u=t.mdDir?[...a,t.mdDir,n]:[...a,n];return l.all(...u)},Me=`
|
|
146
|
+
CASE
|
|
147
|
+
WHEN tool_name LIKE 'mcp\\_\\_%' ESCAPE '\\' THEN 'mcpServers'
|
|
148
|
+
WHEN file_path IS NOT NULL AND file_path <> '' AND LOWER(file_path) LIKE '%.md' THEN 'mdFiles'
|
|
149
|
+
WHEN skill_name IS NOT NULL AND skill_name <> '' THEN 'bloatware'
|
|
150
|
+
WHEN tool_name IS NOT NULL AND tool_name <> '' THEN 'actualValue'
|
|
151
|
+
WHEN event IN ('user', 'assistant', 'prompt') THEN 'actualValue'
|
|
152
|
+
ELSE 'bloatware'
|
|
153
|
+
END
|
|
154
|
+
`;function Ne(){return{bytes:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0},tokens:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0}}}const Pe=(e,t={},n=Date.now())=>{let{clause:r,params:i}=O(t,n),a=`
|
|
155
|
+
SELECT
|
|
156
|
+
${Me} AS bucket,
|
|
157
|
+
SUM(LENGTH(payload)) AS bytes,
|
|
158
|
+
SUM(
|
|
159
|
+
CASE WHEN json_valid(payload) THEN
|
|
160
|
+
COALESCE(json_extract(payload, '\$.message.usage.input_tokens'), 0) +
|
|
161
|
+
COALESCE(json_extract(payload, '\$.message.usage.cache_creation_input_tokens'), 0) +
|
|
162
|
+
COALESCE(json_extract(payload, '\$.message.usage.cache_read_input_tokens'), 0)
|
|
163
|
+
ELSE 0 END
|
|
164
|
+
) AS tokens
|
|
165
|
+
FROM events
|
|
166
|
+
${r}
|
|
167
|
+
GROUP BY ${Me}
|
|
168
|
+
`,o=e.prepare(a).all(...i),s=Ne();for(let e of o)s.bytes[e.bucket]=Number(e.bytes),s.tokens[e.bucket]=Number(e.tokens);return s};function Fe(e){return e===`day`?`strftime('%Y-%m-%dT00:00:00.000Z', ${D})`:`strftime('%Y-%m-%dT%H:00:00.000Z', ${D})`}function Ie(e,t=Date.now()){let n=new Date(t);return e===`day`?n.setUTCHours(0,0,0,0):(n.setUTCMinutes(0,0,0),n.setUTCMilliseconds(0)),n}function Le(e){return e===`day`?1440*60*1e3:3600*1e3}const Re=(e,t={},n=24,r=`hour`,i=Date.now())=>{if(n<=0){let n=Fe(r),{clause:a,params:o}=O({...t,range:void 0},i),s=`SELECT ${n} AS bucket, COUNT(*) AS count FROM events ${a} GROUP BY bucket ORDER BY bucket`;return e.prepare(s).all(...o)}let a=Le(r),o=Ie(r,i),s=Ie(r,i-n*60*60*1e3),c=Math.max(1,Math.ceil((o.getTime()-s.getTime())/a)+1),l=s.toISOString(),u=Fe(r),{clause:d,params:f}=O({...t,range:void 0},i),p=`${u} >= ?`,m=`SELECT ${u} AS bucket, COUNT(*) AS count FROM events ${d?`${d} AND ${p}`:`WHERE ${p}`} GROUP BY bucket ORDER BY bucket`,h=e.prepare(m).all(...f,l),g=[],_=new Map;for(let e of h)_.set(e.bucket,e.count);for(let e=0;e<c;e++){let e=r===`day`?`${s.toISOString().slice(0,10)}T00:00:00.000Z`:s.toISOString();g.push({bucket:e,count:_.get(e)??0}),s.setTime(s.getTime()+a)}return g},ze=e=>{let t=e.prepare(`SELECT id FROM events ORDER BY id DESC LIMIT 1`).get();return t?Number(t.id):0},Be=(e,t)=>{if(e.failureCount>0)return`failed`;let n=e.lastAt?Date.parse(e.lastAt):NaN;return t-(Number.isNaN(n)?e.lastReceivedAt:Math.min(n,e.lastReceivedAt))<=300*1e3?`active`:`completed`};function Ve(e,t,n){return!(t.status!==void 0&&Be(e,n)!==t.status||t.minDuration!==void 0&&e.durationMs<t.minDuration*60*1e3||t.maxDuration!==void 0&&e.durationMs>t.maxDuration*60*1e3)}const He=(e,t,n={})=>{let r=[...new Set(t.filter(e=>e!==null))];if(r.length===0)return[];let{clause:i,params:a}=O({...n,sessionId:void 0,sessionIds:void 0,subagentId:void 0}),o=i?` ${i.replace(/^WHERE/,`AND`)}`:``,s=[];for(let t=0;t<r.length;t+=500){let n=r.slice(t,t+500),i=`
|
|
141
169
|
SELECT
|
|
142
170
|
session_id AS sessionId,
|
|
143
171
|
subagent_id AS subagentId,
|
|
@@ -149,18 +177,20 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
149
177
|
MAX(happened_at) AS lastAt,
|
|
150
178
|
COALESCE(json_group_array(DISTINCT project_path ORDER BY project_path) FILTER (WHERE project_path IS NOT NULL AND project_path <> ''), '[]') AS projectPaths,
|
|
151
179
|
COALESCE(json_group_array(DISTINCT tool_name ORDER BY tool_name) FILTER (WHERE tool_name IS NOT NULL AND tool_name <> ''), '[]') AS toolNames,
|
|
180
|
+
COALESCE(json_group_array(DISTINCT skill_name ORDER BY skill_name) FILTER (WHERE skill_name IS NOT NULL AND skill_name <> ''), '[]') AS skillNames,
|
|
181
|
+
COALESCE(json_group_array(DISTINCT file_path ORDER BY file_path) FILTER (WHERE file_path IS NOT NULL AND file_path <> ''), '[]') AS filePaths,
|
|
152
182
|
SUM(CASE WHEN event LIKE '%Failure%' THEN 1 ELSE 0 END) AS failureCount
|
|
153
183
|
FROM events
|
|
154
184
|
WHERE subagent_id IS NOT NULL AND subagent_id <> '' AND session_id IN (${n.map(()=>`?`).join(`,`)})${o}
|
|
155
185
|
GROUP BY session_id, subagent_id
|
|
156
186
|
ORDER BY lastReceivedAt DESC, subagent_id
|
|
157
|
-
`,c=e.prepare(i);s.push(...c.all(...n,...a))}return s.map(
|
|
187
|
+
`,c=e.prepare(i);s.push(...c.all(...n,...a))}return s.map(De)},j=(e,t={},n=Date.now())=>{let r=Oe(e,{...t,limit:2**53-1,offset:0},n).filter(e=>Ve(e,t,n)),i=He(e,r.map(e=>e.sessionId),t),a=new Map;for(let e of i){let t=a.get(e.sessionId)??[];t.push(e),a.set(e.sessionId,t)}let o=r.map(e=>({...e,children:a.get(e.sessionId)})),s=t.offset??0,c=t.limit;return s===0&&c===void 0?o:c===void 0?o.slice(s):o.slice(s,s+c)},Ue=e=>{let t=e.prepare(`SELECT DISTINCT source FROM events WHERE source IS NOT NULL AND source <> '' ORDER BY source`).all(),n=e.prepare(`SELECT DISTINCT event FROM events WHERE event IS NOT NULL AND event <> '' ORDER BY event`).all(),r=e.prepare(`SELECT DISTINCT tool_name AS tool FROM events WHERE tool_name IS NOT NULL AND tool_name <> '' ORDER BY tool_name`).all(),i=e.prepare(`SELECT DISTINCT project_path AS dir FROM events WHERE project_path IS NOT NULL AND project_path <> '' AND project_path NOT LIKE '/private/%' ORDER BY project_path`).all();return{sources:t.map(e=>e.source),events:n.map(e=>e.event),tools:r.map(e=>e.tool),directories:i.map(e=>e.dir)}},M=(e,t,n)=>{e.prepare(`
|
|
158
188
|
INSERT INTO imports (path, mtime, imported_at)
|
|
159
189
|
VALUES (?, ?, ?)
|
|
160
190
|
ON CONFLICT(path) DO UPDATE SET
|
|
161
191
|
mtime = excluded.mtime,
|
|
162
192
|
imported_at = excluded.imported_at
|
|
163
|
-
`).run(t,n,Date.now())},
|
|
193
|
+
`).run(t,n,Date.now())},We=(e,t)=>{let n=e.prepare(`SELECT mtime FROM imports WHERE path = ?`).get(t);return n?Number(n.mtime):void 0},Ge=e=>{e.exec(`DELETE FROM imports`)},Ke=new Set([`Read`,`Edit`,`Write`,`MultiEdit`,`NotebookEdit`]);function N(e){return typeof e==`string`&&e.length>0?e:void 0}function qe(e,t){if(!e||!Ke.has(e)||typeof t!=`object`||!t)return;let n=t;return N(n.file_path)??N(n.notebook_path)??N(n.path)}function Je(e,t){if(!(e!==`Skill`||typeof t!=`object`||!t))return N(t.skill)}function P(e){if(e==null||typeof e==`object`)return;let t=String(e).trim();return t.length>0?t:void 0}function F(e){let t=P(e);if(t!==void 0)return t;if(Array.isArray(e))for(let t of e){let e=P(t);if(e!==void 0)return e}}function Ye(e){if(typeof e!=`object`||!e)return!1;let t=e,n=t.errcode??0;return t.code===`ERR_SQLITE_ERROR`&&((n&255)==5||(n&255)==6)}function Xe(e,t,n){if(t.length===0)return;let r;try{r=JSON.parse(t)}catch{return}let i=e[0];if(i!==`cursor`&&i!==`claude`)return;let a;a=i===`cursor`?P(r.hook_event_name):e[1];let o=i===`cursor`&&a===`subagentStart`?P(r.parent_conversation_id)??P(r.conversation_id)??P(r.sessionId)??P(r.session_id):P(r.sessionId)??P(r.session_id),s=we(r),c=P(r.projectPath)??P(r.cwd)??P(r.project_path)??F(r.workspaceRoot)??F(r.workspaceRoots)??F(r.workspace_roots)??F(r.workspace_root)??P(r.workspace_path),l=P(r.toolName??r.tool_name??r.tool),u=P(r.filePath??r.file_path??r.path)??qe(l,r.tool_input),d=Je(l,r.tool_input),f=P(r.client)??(i===`cursor`?`cursor`:`claude_code`),p=P(r.subagent_id)??void 0,m=P(r.subagent_type)??void 0,h=P(r.transcript_path)??void 0,g={source:i,client:f,event:a,sessionId:o,happenedAt:s,projectPath:c,filePath:u,toolName:l,skillName:d,payload:t,subagentId:p,subagentType:m,transcriptPath:h},_=ee(i,a??``),v;try{if(v=T(n,500),!g.subagentId){let e=P(r.tool_use_id);if(e){let t=v.prepare(`SELECT subagent_id FROM events WHERE event = 'subagentStart' AND subagent_id = ? LIMIT 1`).get(e);t&&(g.subagentId=t.subagent_id)}}E(v,g)}catch(e){if(Ye(e))console.warn(`happenin: dropping event due to database lock`,e);else throw e}finally{try{v?.close()}catch{}}return _}async function Ze(e){let t=[];for await(let e of c.stdin)t.push(Buffer.isBuffer(e)?e:Buffer.from(e,`utf8`));let n=Xe(e,Buffer.concat(t).toString(`utf8`).trim());n!==void 0&&c.stdout.write(`${n}\n`)}const Qe=`claude-transcript`,I=`cursor-transcript`,$e=`cursor`;function et(e){if(e.type!==`assistant`||typeof e.message!=`object`||e.message===null)return[];let t=e.message.content;if(!Array.isArray(t))return[];let n=[];for(let e of t)if(typeof e==`object`&&e&&e.type===`tool_use`&&typeof e.name==`string`){let t=e.name,r=e.input;n.push({name:t,filePath:qe(t,r),skillName:Je(t,r)})}return n}const L=e=>{try{return r(e,{withFileTypes:!0})}catch{return[]}},tt=e=>{try{return n(e,`utf8`)}catch{return}},nt=e=>{try{return Math.floor(a(e).mtimeMs)}catch{return}},R=(e,t)=>{let n=nt(t);if(n===void 0||We(e,t)===n)return;let r=tt(t);if(r!==void 0)return{content:r,mtime:n}};function rt(e){let t=[];for(let n of L(e)){if(n.isSymbolicLink())continue;let r=s.join(e,n.name);n.isDirectory()?t.push(...rt(r)):n.isFile()&&n.name.endsWith(`.jsonl`)&&n.name!==`store.db`&&t.push(r)}return t}function it(e){let t=[];for(let n of L(e)){if(!n.isDirectory()||n.isSymbolicLink())continue;let r=s.join(e,n.name);for(let e of L(r))e.isDirectory()&&!e.isSymbolicLink()&&t.push(s.join(r,e.name))}return t}async function at(e,t){let n=R(e,t);if(n===void 0)return;let{content:r,mtime:i}=n,a=s.basename(t,`.jsonl`),o=[];for(let e of r.split(/\r?\n/)){if(e.trim().length===0)continue;let t;try{t=JSON.parse(e)}catch{continue}typeof t==`object`&&t&&o.push({obj:t,line:e})}e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(Qe,t);for(let{obj:n,line:r}of o){let i={source:Qe,client:`claude_code`,event:typeof n.type==`string`?n.type:`transcript`,sessionId:typeof n.sessionId==`string`?n.sessionId:a,happenedAt:typeof n.timestamp==`string`?n.timestamp:typeof n.timestamp==`number`?String(n.timestamp):void 0,projectPath:typeof n.cwd==`string`?n.cwd:void 0,payload:r,sourcePath:t},o=et(n);if(o.length===0){E(e,i);continue}for(let t of o)E(e,{...i,toolName:t.name,filePath:t.filePath,skillName:t.skillName})}M(e,t,i)}async function ot(e){let t=s.resolve(u(),`.claude/projects`);for(let n of rt(t))await at(e,n)}async function st(e,t,n){let r=R(e,t);if(r===void 0)return;let{content:i,mtime:a}=r,o=[],s;try{s=JSON.parse(i)}catch{return}Array.isArray(s)&&(o=s),e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(I,t);for(let r of o)typeof r==`string`&&E(e,{source:I,client:$e,event:`prompt`,sessionId:n,payload:JSON.stringify({prompt:r}),sourcePath:t});M(e,t,a)}async function ct(e,t,n){let r=R(e,t);if(r===void 0)return;let{content:i,mtime:a}=r;e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(I,t),E(e,{source:I,client:$e,event:`session_meta`,sessionId:n,payload:i,sourcePath:t}),M(e,t,a)}async function lt(e,t){let n=s.basename(t);await st(e,s.join(t,`prompt_history.json`),n),await ct(e,s.join(t,`meta.json`),n)}async function ut(e){let t=s.resolve(u(),`.cursor/chats`);for(let n of it(t))await lt(e,n)}async function dt(e){let t=T(w());try{xe(t),e?.includes(`--force`)&&Ge(t),await ft(t)}finally{t.close()}}async function ft(e){await ot(e),await ut(e)}function z(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function B(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}function V(e,t=24){return e.length<=t?e:`${e.slice(0,t-1)}…`}function pt(e){if(e.length===0)return``;let t=e[0];for(let n of e.slice(1)){let e=0;for(;e<t.length&&e<n.length&&t.charAt(e)===n.charAt(e);)e++;if(t=t.slice(0,e),t===``)return``}let n=t.lastIndexOf(`/`);return n>0?t.slice(0,n+1):``}function mt(e,t){return e.split(`/`).filter(e=>e.length>0).slice(-t).join(`/`)}function ht(e){let t=Math.max(0,Math.round(e/1e3)),n=Math.floor(t/60),r=t%60;return n>0?`${n}m ${r}s`:`${r}s`}function H(e){if(e==null||e===``)return`-`;let t=new Date(e);return Number.isNaN(t.getTime())?`-`:t.toLocaleString()}function gt(e,t={}){let n={...e,...t},r=new URLSearchParams;return n.source&&r.set(`source`,n.source),n.event&&r.set(`event`,n.event),n.sessionId&&r.set(`session`,n.sessionId),n.q&&r.set(`q`,n.q),n.since!==void 0&&r.set(`since`,String(n.since)),n.range&&r.set(`range`,n.range),n.status&&r.set(`status`,n.status),n.tool&&r.set(`tool`,n.tool),n.skill&&r.set(`skill`,n.skill),n.file&&r.set(`file`,n.file),n.mdDir&&r.set(`mdDir`,n.mdDir),n.view&&r.set(`view`,n.view),n.minDuration!==void 0&&r.set(`minDuration`,String(n.minDuration)),n.maxDuration!==void 0&&r.set(`maxDuration`,String(n.maxDuration)),n.limit!==void 0&&r.set(`limit`,String(n.limit)),n.offset!==void 0&&r.set(`offset`,String(n.offset)),r.toString()}function U(e,t={}){return`/fragments/sessions?${gt(e,t)}`}function _t(e){let t=e.totalSessions===0?`-`:`${Math.round(e.successRate)}%`;return`<div class="metric-grid">
|
|
164
194
|
<div class="metric-card metric-sessions">
|
|
165
195
|
<div class="metric-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 18 9 12 13 16 21 6"/><circle cx="21" cy="6" r="2"/></svg></div>
|
|
166
196
|
<div class="metric-label">Total Sessions</div>
|
|
@@ -174,81 +204,97 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
174
204
|
<div class="metric-card metric-duration">
|
|
175
205
|
<div class="metric-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
|
176
206
|
<div class="metric-label">Average Duration</div>
|
|
177
|
-
<div class="metric-value">${
|
|
207
|
+
<div class="metric-value">${ht(e.averageDurationMs)}</div>
|
|
178
208
|
</div>
|
|
179
209
|
<div class="metric-card metric-success">
|
|
180
210
|
<div class="metric-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 12 15 16 10"/></svg></div>
|
|
181
211
|
<div class="metric-label">Success Rate</div>
|
|
182
212
|
<div class="metric-value">${t}</div>
|
|
183
213
|
</div>
|
|
184
|
-
</div>`}function
|
|
214
|
+
</div>`}function vt(e,t){return t===`day`?e.slice(5,10):e.slice(11,16)}function yt(e,t=`hour`){if(e.length===0)return`<div class="chart-panel"><h2 class="chart-title">Event Frequency Over Time</h2><div class="chart-empty">No data</div></div>`;let n=Math.max(0,...e.map(e=>e.count)),r=e.length>1?552/(e.length-1):0,i=n>0?172/n:1,a=e.map((e,n)=>({x:44+n*r,y:216-e.count*i,count:e.count,label:vt(e.bucket,t)})),o=a.map(e=>`${e.x},${e.y}`).join(` `),s=`M ${a[0].x} 216 ${a.map(e=>`L ${e.x} ${e.y}`).join(` `)} L ${a[a.length-1].x} 216 Z`,c=e.length<=12?1:Math.ceil(e.length/8),l=a.filter((e,t)=>t%c===0).map(e=>`<text x="${e.x}" y="248" text-anchor="middle">${z(e.label)}</text>`).join(``),u=[0,.5,1].map(e=>{let t=n>0?Math.round(n*e):0;return`<text x="36" y="${216-172*e+4}" text-anchor="end" class="y-label">${t}</text>`}).join(``);return`<div class="chart-panel"><h2 class="chart-title">Event Frequency Over Time</h2>${`<svg class="event-chart" viewBox="0 0 640 260" preserveAspectRatio="xMidYMid meet"><defs><linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="var(--accent)" stop-opacity="0.5"/><stop offset="100%" stop-color="var(--accent-2)" stop-opacity="0.05"/></linearGradient></defs>${[.25,.5,.75].map(e=>{let t=216-172*e;return`<line class="grid" x1="44" y1="${t}" x2="596" y2="${t}"/>`}).join(``)}<line class="axis" x1="44" y1="44" x2="44" y2="216"/><line class="axis" x1="44" y1="216" x2="596" y2="216"/>${u}<path class="area" fill="url(#areaGradient)" d="${s}"/><polyline points="${o}"/>${l}</svg>`}${`<div class="chart-max">max ${n.toLocaleString()}</div>`}</div>`}function bt(e,t,n){return e.length<=n?e:t===`start`?`…${e.slice(-(n-1))}`:`${e.slice(0,n-1)}…`}function W(e,t,n={}){let{truncate:r=`end`,labelLimit:i=20,wideLabel:a=!1}=n,o=a?`chart-panel chart-panel--wide-label`:`chart-panel`;if(t.length===0)return`<div class="${o}"><h2 class="chart-title">${z(e)}</h2><div class="chart-empty">No data</div></div>`;let s=Math.max(...t.map(e=>e.count)),c=t.map(e=>{let t=s>0?e.count/s*100:0,n=z(bt(e.label,r,i)),a=`<span class="tool-name" title="${B(e.title??e.label)}">${n}</span><div class="tool-bar-bg"><div class="tool-bar" style="width: ${t}%"></div></div><span class="tool-count">${e.count}</span>`;return e.href?`<a class="tool-row" href="${B(e.href)}" hx-get="${B(e.href)}" hx-target="#dashboard-content" hx-swap="innerHTML">${a}</a>`:`<div class="tool-row">${a}</div>`}).join(``);return`<div class="${o}"><h2 class="chart-title">${z(e)}</h2><div class="tool-chart">${c}</div></div>`}function xt(e,t){return W(`Most Used Tools`,e.map(e=>({label:e.tool,count:e.count,href:t&&U(t,{tool:e.tool,view:`list`,offset:0})})),{labelLimit:36,wideLabel:!0})}function St(e,t){return W(`Most Used Skills`,e.map(e=>({label:e.skill,count:e.count,href:t&&U(t,{skill:e.skill,view:`list`,offset:0})})),{labelLimit:36,wideLabel:!0})}function Ct(e,t){return W(`Top Markdown Files`,e.map(e=>({label:mt(e.file,2),title:e.file,count:e.count,href:t&&U(t,{file:e.file,view:`list`,offset:0})})),{truncate:`start`,labelLimit:40,wideLabel:!0})}const G=[{key:`actualValue`,label:`Actual value`,color:`var(--success)`},{key:`mdFiles`,label:`Markdown files`,color:`var(--accent-2)`},{key:`mcpServers`,label:`MCP servers`,color:`var(--accent)`},{key:`bloatware`,label:`Bloatware`,color:`var(--warning)`}];function wt(e){return e>=1e6?`${(e/1e6).toFixed(1)}MB`:e>=1e3?`${(e/1e3).toFixed(1)}KB`:`${e}B`}function Tt(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}K`:`${e}`}function Et(e,t,n,r){let i=G.reduce((e,{key:n})=>e+t[n],0);if(i===0)return`<div class="context-bar-block">
|
|
215
|
+
<h3 class="context-bar-title">${z(e)}</h3>
|
|
216
|
+
<div class="chart-empty">${z(r)}</div>
|
|
217
|
+
</div>`;let a=G.filter(({key:e})=>t[e]>0).map(({key:e,label:r,color:a})=>{let o=t[e],s=o/i*100;return`<div class="context-segment" style="width: ${s}%; background: ${a}" title="${z(`${r}: ${n(o)} (${s.toFixed(1)}%)`)}"></div>`}).join(``),o=G.map(({key:e,label:r,color:a})=>{let o=t[e],s=o/i*100;return`<div class="context-legend-row">
|
|
218
|
+
<span class="context-legend-dot" style="background: ${a}"></span>
|
|
219
|
+
<span class="context-legend-label">${z(r)}</span>
|
|
220
|
+
<span class="context-legend-value">${z(n(o))} · ${s.toFixed(1)}%</span>
|
|
221
|
+
</div>`}).join(``);return`<div class="context-bar-block">
|
|
222
|
+
<h3 class="context-bar-title">${z(e)}</h3>
|
|
223
|
+
<div class="context-bar">${a}</div>
|
|
224
|
+
<div class="context-legend">${o}</div>
|
|
225
|
+
</div>`}function Dt(e){return`<div class="chart-panel context-breakdown">
|
|
226
|
+
<h2 class="chart-title">Context Breakdown</h2>
|
|
227
|
+
<div class="context-bars">${Et(`By payload size (all sources)`,e.bytes,wt,`No data`)}${Et(`By tokens (imported Claude sessions)`,e.tokens,Tt,"Run `happenin import` to see token-based numbers")}</div>
|
|
228
|
+
</div>`}function Ot(e,t,n){let r=new URLSearchParams({session:t});return n&&r.set(`subagent`,n),e?.source&&r.set(`source`,e.source),e?.event&&r.set(`event`,e.event),e?.q&&r.set(`q`,e.q),e?.range&&r.set(`range`,e.range),e?.status&&r.set(`status`,e.status),e?.tool&&r.set(`tool`,e.tool),e?.minDuration!==void 0&&r.set(`minDuration`,String(e.minDuration)),e?.maxDuration!==void 0&&r.set(`maxDuration`,String(e.maxDuration)),e?.limit!==void 0&&r.set(`limit`,String(e.limit)),e?.offset!==void 0&&r.set(`offset`,String(e.offset)),`/fragments/detail?${r.toString()}`}function K(e,t,n,r,i,a=!1,o=``,s=``){let c=Be(e,t),l=a?e.subagentId??`no subagent`:e.sessionId??`no session`,u=z(V(l,28)),d=z(H(e.firstAt??e.firstReceivedAt)),f=z(ht(e.durationMs)),p=(a?e.subagentId===r&&e.sessionId===n:e.sessionId===n)?` active`:``,m=e.sessionId??``,h=a?e.subagentId:void 0,g=!a&&!!e.children?.length,_=m&&!g?` hx-get="${B(Ot(i,m,h))}" hx-target="#dashboard-content" hx-swap="innerHTML"`:``,v=m&&g?` hx-get="${B(Ot(i,m))}" hx-target="#dashboard-content" hx-swap="innerHTML"`:``,y=a?` session-subagent`:``,ee=!a&&e.children?.length&&e.sessionId===n?` expanded`:``,b=!a&&e.children?.length?` session-parent`:``,te=!a&&!e.sessionId?` session-item-static`:``,x=a?` data-session="${B(m)}" data-subagent="${B(e.subagentId??``)}"`:` data-session="${B(e.sessionId??``)}"`,ne=a&&e.subagentType?` <span class="subagent-type-badge">${z(e.subagentType)}</span>`:``;return`<li class="session-item${b}${ee}${y}${p}${te}"${x}${_}>
|
|
185
229
|
${o}
|
|
186
|
-
<div class="session-main"${
|
|
230
|
+
<div class="session-main"${v}>
|
|
187
231
|
<div class="session-row">
|
|
188
|
-
<span class="session-id" title="${
|
|
232
|
+
<span class="session-id" title="${B(l)}">${u}${ne}</span>
|
|
189
233
|
<span class="status-badge status-${c}">${c}</span>
|
|
190
234
|
</div>
|
|
191
235
|
<div class="session-meta"><span>${f}</span><span>${e.eventCount} events</span><span>${d}</span></div>
|
|
192
236
|
</div>
|
|
193
237
|
<span class="session-chevron" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></span>
|
|
194
238
|
${s}
|
|
195
|
-
</li>`}function
|
|
196
|
-
<ul class="session-list sessions-table">${e.map(e=>e.children?.length?G(e,t,n,r,i,!1,`<button type="button" class="session-toggle" aria-label="toggle subagents" onclick="event.stopPropagation(); const li = this.closest('.session-item'); li.classList.toggle('expanded'); this.textContent = li.classList.contains('expanded') ? '▾' : '▸'">${e.sessionId===n?`▾`:`▸`}</button>`,`<ul class="session-children">${e.children.map(e=>G(e,t,n,r,i,!0)).join(``)}</ul>`):G(e,t,n,r,i)).join(``)}</ul>`}function at(e){if(e)try{return JSON.parse(e)}catch{return}}function ot(e){return{id:e.id,source:e.source,client:e.client,event:e.event,sessionId:e.sessionId,happenedAt:e.happenedAt,receivedAt:e.receivedAt,projectPath:e.projectPath,filePath:e.filePath,toolName:e.toolName,sourcePath:e.sourcePath,subagentId:e.subagentId,subagentType:e.subagentType,transcriptPath:e.transcriptPath,payload:at(e.payload??``)??e.payload}}function st(e){let t=new Map;for(let n of e){let e=n.subagentId??null;t.has(e)||t.set(e,[]),t.get(e).push(n)}let n=[];for(let[e,r]of t){let t=r.toSorted((e,t)=>t.id-e.id),i=t.findIndex(e=>e.event===`subagentStart`),a=i>=0?t.at(i):t.at(0),o=t.filter(e=>e!==a);n.push({subagentId:e,parent:a,children:o,maxId:t.at(0).id})}return n.toSorted((e,t)=>t.maxId-e.maxId)}function ct(e){return`<span class="ts">${H(W(e.happenedAt??e.receivedAt))}</span><span class="source">${H(e.source??``)}</span><span class="event">${H(e.event??``)}</span><span class="tool">${H(e.toolName??``)}</span>`}function lt(e){let t=H(W(e.happenedAt??e.receivedAt)),n=H(e.source??``),r=H(e.client??``),i=H(e.event??``),a=H(e.toolName??``),o=H(e.subagentId??``),s=H(e.subagentType??``),c=H(e.projectPath??``),l=H(e.filePath??``),u=o?`<div><span>Subagent</span><span>${s||`-`} (${o})</span></div>`:``,d=l?`<div><span>File</span><span>${l}</span></div>`:``;return`<div class="detail-attrs">
|
|
239
|
+
</li>`}function kt(e,t=Date.now(),n,r,i){return e.length===0?`<ul class="session-list sessions-table"><li class="empty">No sessions found.</li></ul>`:`<ul class="session-list sessions-table">${e.map(e=>e.children?.length?K(e,t,n,r,i,!1,`<button type="button" class="session-toggle" aria-label="toggle subagents" onclick="event.stopPropagation(); const li = this.closest('.session-item'); li.classList.toggle('expanded'); this.textContent = li.classList.contains('expanded') ? '▾' : '▸'">${e.sessionId===n?`▾`:`▸`}</button>`,`<ul class="session-children">${e.children.map(e=>K(e,t,n,r,i,!0)).join(``)}</ul>`):K(e,t,n,r,i)).join(``)}</ul>`}function At(e){if(e)try{return JSON.parse(e)}catch{return}}function jt(e){return{id:e.id,source:e.source,client:e.client,event:e.event,sessionId:e.sessionId,happenedAt:e.happenedAt,receivedAt:e.receivedAt,projectPath:e.projectPath,filePath:e.filePath,toolName:e.toolName,skillName:e.skillName,sourcePath:e.sourcePath,subagentId:e.subagentId,subagentType:e.subagentType,transcriptPath:e.transcriptPath,payload:At(e.payload??``)??e.payload}}function Mt(e){let t=new Map;for(let n of e){let e=n.subagentId??null;t.has(e)||t.set(e,[]),t.get(e).push(n)}let n=[];for(let[e,r]of t){let t=r.toSorted((e,t)=>t.id-e.id),i=t.findIndex(e=>e.event===`subagentStart`),a=i>=0?t.at(i):t.at(0),o=t.filter(e=>e!==a);n.push({subagentId:e,parent:a,children:o,maxId:t.at(0).id})}return n.toSorted((e,t)=>t.maxId-e.maxId)}function Nt(e){return`<span class="ts">${z(H(e.happenedAt??e.receivedAt))}</span><span class="source">${z(e.source??``)}</span><span class="event">${z(e.event??``)}</span><span class="tool">${z(e.toolName??``)}</span>`}function Pt(e){let t=z(H(e.happenedAt??e.receivedAt)),n=z(e.source??``),r=z(e.client??``),i=z(e.event??``),a=z(e.toolName??``),o=z(e.subagentId??``),s=z(e.subagentType??``),c=z(e.projectPath??``),l=z(e.filePath??``),u=z(e.skillName??``),d=o?`<div><span>Subagent</span><span>${s||`-`} (${o})</span></div>`:``,f=l?`<div><span>File</span><span>${l}</span></div>`:``,p=u?`<div><span>Skill</span><span>${u}</span></div>`:``;return`<div class="detail-attrs">
|
|
197
240
|
<div><span>Time</span><span>${t}</span></div>
|
|
198
241
|
<div><span>Source</span><span>${n}</span></div>
|
|
199
242
|
<div><span>Client</span><span>${r||`-`}</span></div>
|
|
200
243
|
<div><span>Event</span><span>${i}</span></div>
|
|
201
244
|
<div><span>Tool</span><span>${a||`-`}</span></div>
|
|
202
245
|
<div><span>Project</span><span>${c||`-`}</span></div>
|
|
203
|
-
${u}
|
|
204
246
|
${d}
|
|
205
|
-
|
|
247
|
+
${f}
|
|
248
|
+
${p}
|
|
249
|
+
</div>`}function Ft(e){return`<div class="detail-json">
|
|
206
250
|
<details class="json-toggle">
|
|
207
251
|
<summary>JSON</summary>
|
|
208
|
-
<pre>${
|
|
252
|
+
<pre>${z(JSON.stringify(jt(e),null,2))}</pre>
|
|
209
253
|
</details>
|
|
210
254
|
<button type="button" class="json-copy" onclick="copyEventJson(this)" title="Copy JSON" aria-label="Copy JSON"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
|
211
|
-
</div>`}function
|
|
212
|
-
<summary>${
|
|
213
|
-
<div class="detail-body">${
|
|
214
|
-
${
|
|
255
|
+
</div>`}function q(e,t){if(t.length===0)return``;let n=t.map(e=>`<span class="chip" title="${B(e)}">${z(V(e,40))}</span>`).join(``);return`<div class="summary-group"><span class="summary-label">${e} (${t.length})</span><div class="summary-chips">${n}</div></div>`}function It(e){if(!e)return``;let t=[q(`Tools`,e.tools),q(`Skills`,e.skills),q(`Files`,e.files)].join(``);return t?`<div class="detail-summary">${t}</div>`:``}function Lt(e){return`<details class="detail-event" data-event-id="${e.id}" open>
|
|
256
|
+
<summary>${Nt(e)}</summary>
|
|
257
|
+
<div class="detail-body">${Pt(e)}
|
|
258
|
+
${Ft(e)}
|
|
215
259
|
</div>
|
|
216
|
-
</details>`}function
|
|
217
|
-
<summary>${
|
|
260
|
+
</details>`}function Rt(e,t){let n=e.parent,r=e.children.map(Lt).join(``),i=`<span class="subagent-badge">${z(n.subagentType||`subagent`)}${` · ${z(V(t,18))}`} · ${e.children.length}</span>`;return`<details class="detail-event detail-subagent" data-event-id="${n.id}" open>
|
|
261
|
+
<summary>${Nt(n)}${i}</summary>
|
|
218
262
|
<div class="detail-body">
|
|
219
|
-
${
|
|
220
|
-
${
|
|
263
|
+
${Pt(n)}
|
|
264
|
+
${Ft(n)}
|
|
221
265
|
<div class="detail-children">${r}</div>
|
|
222
266
|
</div>
|
|
223
|
-
</details>`}function
|
|
267
|
+
</details>`}function zt(e){return Mt(e).map(e=>e.subagentId?Rt(e,e.subagentId):[e.parent,...e.children].map(Lt).join(``)).join(``)}function Bt(e,t,n,r,i,a){if(!e)return`<div class="detail-header"><h2 class="detail-title">Session Details</h2></div><div class="empty">No events.</div>`;let o=r?t.filter(e=>e.subagentId===r):t,s=r?`${e} · ${r}`:e;if(o.length===0)return`<div class="detail-header"><h2 class="detail-title">Session Details - ${z(s)}</h2></div><div class="empty">No events for this subagent.</div>`;let c=zt(o),l=JSON.stringify(o,null,2),u=n!==void 0&&n>o.length?`<div class="detail-truncated">Showing the ${o.length} most recent of ${n} events — older events are hidden and the JSON copy is partial.</div>`:``;return`<section class="session-detail-view" data-session="${B(e)}" data-subagent="${B(r??``)}">
|
|
224
268
|
<div class="detail-header">
|
|
225
269
|
<button type="button" class="detail-back" onclick="backToDashboard()" aria-label="back to dashboard"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg><span>Back</span></button>
|
|
226
|
-
<h2 class="detail-title">Session Details - ${
|
|
227
|
-
<span class="detail-count">${n??
|
|
270
|
+
<h2 class="detail-title">Session Details - ${z(s)}</h2>
|
|
271
|
+
<span class="detail-count">${n??o.length} events</span>
|
|
228
272
|
</div>
|
|
229
|
-
${
|
|
273
|
+
${It(i)}
|
|
274
|
+
${a?Dt(a):``}
|
|
275
|
+
${u}
|
|
230
276
|
<div class="detail-toolbar">
|
|
231
277
|
<input type="search" class="detail-search" placeholder="Search attributes..." oninput="filterSessionDetails(this.value)">
|
|
232
278
|
<button type="button" onclick="toggleSessionDetails(false)">Collapse All</button>
|
|
233
279
|
<button type="button" onclick="toggleSessionDetails(true)">Expand All</button>
|
|
234
280
|
</div>
|
|
235
|
-
<div class="detail-events">${
|
|
281
|
+
<div class="detail-events">${c}</div>
|
|
236
282
|
<div class="detail-actions">
|
|
237
283
|
<button type="button" onclick="copySessionJson()">Copy JSON</button>
|
|
238
284
|
</div>
|
|
239
|
-
<pre id="session-json" class="session-json">${
|
|
240
|
-
</section>`}function
|
|
285
|
+
<pre id="session-json" class="session-json">${z(l)}</pre>
|
|
286
|
+
</section>`}const Vt={bytes:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0},tokens:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0}};function Ht(e){if(!e)return;let t=Number(e);return Number.isFinite(t)&&t>=0?t:void 0}function Ut(e){if(!e)return;let t=Number(e);return Number.isInteger(t)&&t>=0?t:void 0}function Wt(e){return e===`7d`?{hours:168,groupBy:`day`}:e===`30d`?{hours:720,groupBy:`day`}:e===`all`?{hours:0,groupBy:`day`}:{hours:24,groupBy:`hour`}}function J(e){let t=e.searchParams.get(`since`),n=e.searchParams.get(`source`)||void 0,r=e.searchParams.get(`event`)||void 0,i=e.searchParams.get(`session`)||void 0,a=e.searchParams.get(`subagent`)||void 0,o=e.searchParams.get(`q`)||void 0,s=e.searchParams.get(`status`)||void 0,c=e.searchParams.get(`tool`)||void 0,l=e.searchParams.get(`skill`)||void 0,u=e.searchParams.get(`file`)||void 0,d=e.searchParams.get(`mdDir`)||void 0,f=(e.searchParams.get(`view`)||void 0)===`list`?`list`:void 0,p=e.searchParams.get(`minDuration`),m=e.searchParams.get(`maxDuration`),h=e.searchParams.get(`range`)||`24h`,g=[`24h`,`7d`,`30d`,`all`].includes(h)?h:`24h`,_=e.searchParams.get(`limit`),v=e.searchParams.get(`offset`),y;if(t){let e=parseInt(t,10);Number.isNaN(e)||(y=e)}return{since:y,source:n,event:r,sessionId:i,subagentId:a,q:o,status:s===`active`||s===`completed`||s===`failed`?s:void 0,tool:c,skill:l,file:u,mdDir:d,view:f,minDuration:Ht(p),maxDuration:Ht(m),range:g,limit:Ut(_),offset:Ut(v)}}function Gt(e,t){let n=e.limit&&e.limit>0?e.limit:25,r=e.offset??0;if(t<=n)return``;let i=Math.floor(r/n)+1,a=Math.ceil(t/n),o=r-n,s=r+n,c=U(e,{offset:Math.max(o,0),limit:n}),l=U(e,{offset:s,limit:n});return`<div class="pager">${o>=0?`<a href="${c}" hx-get="${c}" hx-target="#dashboard-content" hx-swap="innerHTML">Previous</a>`:`<button type="button" disabled>Previous</button>`}<span class="pager-info">Page ${i} of ${a}</span>${s<t?`<a href="${l}" hx-get="${l}" hx-target="#dashboard-content" hx-swap="innerHTML">Next</a>`:`<button type="button" disabled>Next</button>`}</div>`}function Kt(e){if(e.skill)return{kind:`skill`,value:e.skill};if(e.file)return{kind:`file`,value:e.file};if(e.tool)return{kind:`tool`,value:e.tool}}function qt(e,t,n){let r=j(e,{...t,subagentId:void 0,limit:void 0,offset:void 0},n),i=t.limit&&t.limit>0?t.limit:25,a=t.offset??0,o=r.slice(a,a+i),s=Kt(t),c=U(t,{tool:void 0,skill:void 0,file:void 0,offset:0}),l=s?`<div class="sessions-list-filter">Sessions using ${z(s.kind)}: <strong>${z(s.value)}</strong> <a href="${c}" hx-get="${c}" hx-target="#dashboard-content" hx-swap="innerHTML">Clear</a></div>`:``;return`<div class="main-content sessions-list-view">
|
|
287
|
+
<h2 class="session-list-title">Recent Sessions <span class="session-collapse-count">${r.length}</span></h2>
|
|
288
|
+
${l}
|
|
241
289
|
<div class="session-list-wrapper">
|
|
242
|
-
${
|
|
243
|
-
${
|
|
290
|
+
${kt(o,n,void 0,void 0,t)}
|
|
291
|
+
${Gt(t,r.length)}
|
|
244
292
|
</div>
|
|
245
|
-
</
|
|
246
|
-
<div class="main-
|
|
247
|
-
<div class="
|
|
248
|
-
|
|
249
|
-
</div>`}function
|
|
250
|
-
<div class="main-content"><div class="detail-area"><div class="empty">No events.</div></div></div>`;let i=k(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId,limit:1e3,offset:0}),a=A(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId});return`${r}
|
|
251
|
-
<div class="main-content">${mt(t.sessionId,i,a,t.subagentId)}</div>`}function St(e,t,n,r){let i=xt(r,K(n));t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(i)}function Ct(e,t,n,r){let i=k(r,K(n));t.writeHead(200,{"Content-Type":`application/json`}),t.end(JSON.stringify(i))}const wt=[{value:`24h`,label:`Last 24h`},{value:`7d`,label:`Last 7 days`},{value:`30d`,label:`Last 30 days`},{value:`all`,label:`All time`}];function Tt(e){let t=H(e.q??``),n=e.range??`24h`;return`<header class="app-header">
|
|
293
|
+
</div>`}function Jt(e,t){let n=Date.now();if(t.view===`list`)return qt(e,t,n);let{hours:r,groupBy:i}=Wt(t.range),a=j(e,{...t,subagentId:void 0,limit:void 0,offset:void 0},n),o=a.map(e=>e.sessionId),s=[],c=[],l=[],u=[],d=Vt;if(o.length>0){let a={...t,subagentId:void 0,sessionIds:o};s=Re(e,a,r,i,n),c=ke(e,a,10,n),l=Ae(e,a,10,n),u=je(e,a,10,n),d=Pe(e,a,n)}let f=a.length,p=a.reduce((e,t)=>e+t.eventCount,0),m=f>0?a.reduce((e,t)=>e+t.durationMs,0)/f:0,h=a.filter(e=>e.failureCount===0).length;return`<div class="main-content">
|
|
294
|
+
<div class="main-metrics">${_t({totalSessions:f,totalEvents:p,averageDurationMs:m,successRate:f>0?h/f*100:0})}</div>
|
|
295
|
+
<div class="top-charts">${yt(s,i)}${xt(c,t)}${St(l,t)}${Ct(u,t)}</div>
|
|
296
|
+
${Dt(d)}
|
|
297
|
+
</div>`}function Yt(e,t,n,r){let i=Jt(r,J(n));t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(i)}function Xt(e,t){let n=Date.now();if(!t.sessionId)return`<div class="main-content"><div class="detail-area"><div class="empty">No events.</div></div></div>`;let r=j(e,{...t,sessionId:void 0,subagentId:void 0,limit:void 0,offset:void 0},n),i=k(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId,limit:1e3,offset:0}),a=A(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId}),o=Pe(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId}),s=r.find(e=>e.sessionId===t.sessionId),c=t.subagentId?s?.children?.find(e=>e.subagentId===t.subagentId):s;return`<div class="main-content">${Bt(t.sessionId,i,a,t.subagentId,c,o)}</div>`}function Zt(e,t,n,r){let i=Xt(r,J(n));t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(i)}function Qt(e,t,n,r){let i=k(r,J(n));t.writeHead(200,{"Content-Type":`application/json`}),t.end(JSON.stringify(i))}const $t=[{value:`24h`,label:`Last 24h`},{value:`7d`,label:`Last 7 days`},{value:`30d`,label:`Last 30 days`},{value:`all`,label:`All time`}];function en(e){let t=z(e.q??``),n=e.range??`24h`;return`<header class="app-header">
|
|
252
298
|
<div class="brand"><svg class="brand-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="4"/><path d="M8 7v10"/><path d="M16 7v10"/><path d="M8 12h8"/></svg> happenin</div>
|
|
253
299
|
<h1 class="page-title">Session Overview Analytics</h1>
|
|
254
300
|
<div class="header-controls">
|
|
@@ -256,17 +302,18 @@ ${vt(t,e.length)}
|
|
|
256
302
|
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
|
257
303
|
<input type="text" name="q" class="search" placeholder="session or payload" value="${t}" autocomplete="off">
|
|
258
304
|
</div>
|
|
259
|
-
<label>range <select name="range">${
|
|
305
|
+
<label>range <select name="range">${$t.map(e=>`<option value="${B(e.value)}"${e.value===n?` selected`:``}>${z(e.label)}</option>`).join(``)}</select></label>
|
|
260
306
|
<button type="button" class="theme" data-theme-toggle title="Toggle light/dark theme" aria-label="toggle theme">🌙</button>
|
|
261
307
|
</div>
|
|
262
|
-
</header>`}const
|
|
263
|
-
<label>status ${Y(`status`,
|
|
308
|
+
</header>`}const tn=[`active`,`completed`,`failed`];function Y(e,t,n,r=`all`){let i=n??``,a=B(i);return`<select name="${e}"><option value=""${i===``?` selected`:``}>${r}</option>${t.map(e=>{let t=B(e);return`<option value="${t}"${t===a?` selected`:``}>${z(e)}</option>`}).join(``)}</select>`}function nn(e,t,n){let r=pt(t),i=n??``,a=B(i),o=t.map(e=>{let t=B(e);return`<option value="${t}"${t===a?` selected`:``}>${z(e.slice(r.length))}</option>`}).join(``);return`<select name="${e}"><option value=""${i===``?` selected`:``}>all directories</option>${o}</select>`}function rn(e,t){let n=t.status,r=t.source,i=t.tool,a=t.event,o=t.minDuration===void 0?``:String(t.minDuration),s=t.maxDuration===void 0?``:String(t.maxDuration),c=t.mdDir;return`<div class="filter-bar">
|
|
309
|
+
<label>status ${Y(`status`,tn,n)}</label>
|
|
264
310
|
<label>source ${Y(`source`,e.sources,r)}</label>
|
|
265
311
|
<label>tool ${Y(`tool`,e.tools,i)}</label>
|
|
266
312
|
<label>event ${Y(`event`,e.events,a)}</label>
|
|
267
|
-
<label>
|
|
268
|
-
<label>
|
|
269
|
-
|
|
313
|
+
<label>md files directory ${nn(`mdDir`,e.directories,c)}</label>
|
|
314
|
+
<label>min duration (m) <input type="number" name="minDuration" min="0" step="any" placeholder="min" value="${z(o)}"></label>
|
|
315
|
+
<label>max duration (m) <input type="number" name="maxDuration" min="0" step="any" placeholder="max" value="${z(s)}"></label>
|
|
316
|
+
</div>`}function an(e,t){let n=Ue(e),r=Jt(e,t);return`<!DOCTYPE html>
|
|
270
317
|
<html lang="en" hx-ext="sse">
|
|
271
318
|
<head>
|
|
272
319
|
<meta charset="utf-8">
|
|
@@ -450,28 +497,42 @@ ${vt(t,e.length)}
|
|
|
450
497
|
if (!content) return;
|
|
451
498
|
const detail = content.querySelector('.session-detail-view');
|
|
452
499
|
document.body.classList.toggle('session-detail-open', !!detail);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function syncSessionsToggle() {
|
|
503
|
+
const content = document.getElementById('dashboard-content');
|
|
504
|
+
const btn = document.getElementById('sessions-toggle');
|
|
505
|
+
if (!content || !btn) return;
|
|
506
|
+
const isList = !!content.querySelector('.sessions-list-view');
|
|
507
|
+
btn.setAttribute('aria-expanded', isList ? 'true' : 'false');
|
|
508
|
+
document.body.classList.toggle('sessions-list-open', isList);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function toggleSessionsList() {
|
|
512
|
+
const btn = document.getElementById('sessions-toggle');
|
|
513
|
+
const form = document.querySelector('[data-filter-form]');
|
|
514
|
+
if (!btn) return;
|
|
515
|
+
const isOpen = btn.getAttribute('aria-expanded') === 'true';
|
|
516
|
+
const params = new URLSearchParams();
|
|
517
|
+
if (form) {
|
|
518
|
+
const data = new FormData(form);
|
|
519
|
+
for (const [key, value] of data.entries()) {
|
|
520
|
+
if (value) params.set(key, String(value));
|
|
521
|
+
}
|
|
460
522
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (active) active.scrollIntoView({ block: 'nearest' });
|
|
523
|
+
if (!isOpen) params.set('view', 'list');
|
|
524
|
+
const url = '/fragments/sessions' + (params.size > 0 ? '?' + params.toString() : '');
|
|
525
|
+
if (typeof htmx !== 'undefined') {
|
|
526
|
+
htmx.ajax('GET', url, { target: '#dashboard-content', swap: 'innerHTML', source: btn });
|
|
466
527
|
}
|
|
467
528
|
}
|
|
468
529
|
|
|
469
|
-
let sidebarScroll = 0;
|
|
470
530
|
document.addEventListener('htmx:beforeSwap', (e) => {
|
|
471
|
-
const list = document.querySelector('.session-list');
|
|
472
|
-
if (list) sidebarScroll = list.scrollTop;
|
|
473
531
|
const elt = e.detail.requestConfig ? e.detail.requestConfig.elt : null;
|
|
474
|
-
|
|
532
|
+
const blocked =
|
|
533
|
+
document.body.classList.contains('session-detail-open') ||
|
|
534
|
+
document.body.classList.contains('sessions-list-open');
|
|
535
|
+
if (elt && elt.id === 'dashboard-content' && blocked) {
|
|
475
536
|
e.detail.shouldSwap = false;
|
|
476
537
|
}
|
|
477
538
|
});
|
|
@@ -481,10 +542,12 @@ ${vt(t,e.length)}
|
|
|
481
542
|
window.copySessionJson = copySessionJson;
|
|
482
543
|
window.copyEventJson = copyEventJson;
|
|
483
544
|
window.backToDashboard = backToDashboard;
|
|
545
|
+
window.toggleSessionsList = toggleSessionsList;
|
|
484
546
|
|
|
485
547
|
document.addEventListener('htmx:afterSettle', () => {
|
|
486
548
|
updateContentUrl();
|
|
487
549
|
syncDetailState();
|
|
550
|
+
syncSessionsToggle();
|
|
488
551
|
});
|
|
489
552
|
|
|
490
553
|
applyTheme(getTheme());
|
|
@@ -540,9 +603,9 @@ ${vt(t,e.length)}
|
|
|
540
603
|
--danger: #dc2626;
|
|
541
604
|
}
|
|
542
605
|
* { box-sizing: border-box; }
|
|
543
|
-
html, body { height: 100%; }
|
|
606
|
+
html, body { min-height: 100%; }
|
|
544
607
|
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 0; line-height: 1.5; }
|
|
545
|
-
.app { display: flex; flex-direction: column; height: 100%; max-width: 1400px; margin: 0 auto; padding: 1.5rem; gap: 1.25rem; }
|
|
608
|
+
.app { display: flex; flex-direction: column; min-height: 100%; max-width: 1400px; margin: 0 auto; padding: 1.5rem; gap: 1.25rem; }
|
|
546
609
|
.sticky-bar { flex: 0 0 auto; display: flex; flex-direction: column; gap: 1rem; background: var(--bg); position: sticky; top: 0; z-index: 10; }
|
|
547
610
|
.app-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; padding: 0.75rem 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; }
|
|
548
611
|
.brand { font-weight: 800; font-size: 1.25rem; display: flex; align-items: center; gap: 0.5rem; color: var(--text); }
|
|
@@ -561,11 +624,17 @@ select { min-width: 7.5rem; cursor: pointer; }
|
|
|
561
624
|
button { cursor: pointer; background: var(--surface-4); font-weight: 500; }
|
|
562
625
|
button:hover { background: var(--surface-5); }
|
|
563
626
|
.theme { display: inline-flex; align-items: center; justify-content: center; font-size: 1.25rem; padding: 0.35rem 0.6rem; line-height: 1; }
|
|
564
|
-
.
|
|
565
|
-
.
|
|
566
|
-
.
|
|
567
|
-
.
|
|
568
|
-
.session-list {
|
|
627
|
+
.sessions-toggle-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 0.4rem; width: 100%; text-align: left; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.65rem 1rem; font-size: 0.85rem; font-weight: 600; color: var(--text-2); }
|
|
628
|
+
.sessions-toggle-bar:hover { background: var(--surface-3); }
|
|
629
|
+
.sessions-toggle-bar[aria-expanded="true"] { color: var(--text); }
|
|
630
|
+
.dashboard-content-area { flex: 1 1 auto; }
|
|
631
|
+
.session-list-title { display: flex; align-items: center; gap: 0.5rem; font-size: 0.95rem; font-weight: 600; color: var(--text); margin: 0 0 0.5rem; }
|
|
632
|
+
.session-collapse-count { color: var(--text-4); font-weight: 500; }
|
|
633
|
+
.sessions-list-filter { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-2); background: var(--surface-2); border: 1px solid var(--border); border-radius: 0.4rem; padding: 0.5rem 0.75rem; margin-bottom: 0.75rem; }
|
|
634
|
+
.sessions-list-filter a { color: var(--accent); text-decoration: none; }
|
|
635
|
+
.sessions-list-filter a:hover { text-decoration: underline; }
|
|
636
|
+
.session-list-wrapper { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; }
|
|
637
|
+
.session-list { list-style: none; padding: 0; margin: 0; }
|
|
569
638
|
.status-badge { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.15rem 0.45rem; border-radius: 0.25rem; }
|
|
570
639
|
.status-active { background: rgba(34,197,94,0.15); color: var(--success); }
|
|
571
640
|
.status-completed { background: rgba(96,165,250,0.15); color: var(--accent); }
|
|
@@ -599,13 +668,12 @@ button:hover { background: var(--surface-5); }
|
|
|
599
668
|
.session-children .session-subagent .session-chevron { display: none; }
|
|
600
669
|
.session-item.expanded > .session-children { display: block; }
|
|
601
670
|
.subagent-type-badge { font-size: 0.65rem; color: var(--text-3); margin-left: 0.35rem; padding: 0.08rem 0.3rem; background: var(--surface-2); border-radius: 0.2rem; }
|
|
602
|
-
.main-content { display:
|
|
603
|
-
.main-content:has(.session-detail-view) { grid-template-rows: 1fr; }
|
|
671
|
+
.main-content { display: flex; flex-direction: column; gap: 1.25rem; }
|
|
604
672
|
.main-metrics .metric-grid { grid-template-columns: repeat(4, 1fr); gap: 1rem; }
|
|
605
673
|
.main-metrics .metric-card { min-height: 5rem; }
|
|
606
674
|
.main-metrics .metric-value { font-size: 1.65rem; }
|
|
607
|
-
.top-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem;
|
|
608
|
-
.chart-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem;
|
|
675
|
+
.top-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
|
|
676
|
+
.chart-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; display: flex; flex-direction: column; min-height: 18rem; }
|
|
609
677
|
.chart-title { font-size: 0.9rem; font-weight: 600; color: var(--text-2); margin: 0 0 0.75rem; flex: 0 0 auto; }
|
|
610
678
|
.chart-empty { color: var(--text-4); text-align: center; padding: 4rem 0; }
|
|
611
679
|
.chart-max { text-align: right; color: var(--text-4); font-size: 0.7rem; margin-top: 0.25rem; flex: 0 0 auto; }
|
|
@@ -616,15 +684,19 @@ button:hover { background: var(--surface-5); }
|
|
|
616
684
|
.event-chart text { fill: var(--text-3); font-size: 10px; }
|
|
617
685
|
.event-chart .y-label { font-size: 9px; fill: var(--text-4); }
|
|
618
686
|
.event-chart .grid { stroke: var(--surface-4); stroke-width: 1; }
|
|
619
|
-
.tool-chart { display: flex; flex-direction: column; gap: 0.65rem;
|
|
687
|
+
.tool-chart { display: flex; flex-direction: column; gap: 0.65rem; }
|
|
620
688
|
.tool-row { display: grid; grid-template-columns: 8rem 1fr 2.5rem; align-items: center; gap: 0.75rem; font-size: 0.8rem; }
|
|
689
|
+
.chart-panel--wide-label .tool-row { grid-template-columns: 60% 1fr 2.5rem; }
|
|
690
|
+
a.tool-row { text-decoration: none; color: inherit; border-radius: 0.3rem; padding: 0.15rem 0.3rem; margin: -0.15rem -0.3rem; transition: background 0.12s; }
|
|
691
|
+
a.tool-row:hover { background: var(--surface-3); }
|
|
692
|
+
a.tool-row:hover .tool-name { color: var(--accent); }
|
|
621
693
|
.tool-name { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
622
694
|
.tool-bar-bg { background: var(--surface-4); border-radius: 0.4rem; height: 0.85rem; overflow: hidden; }
|
|
623
695
|
.tool-bar { background: linear-gradient(90deg, var(--accent), var(--accent-2)); height: 100%; border-radius: 0.4rem; }
|
|
624
696
|
.tool-count { color: var(--text-3); text-align: right; font-weight: 600; }
|
|
625
697
|
.detail-area { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; }
|
|
626
698
|
.detail-area > .empty { color: var(--text-4); text-align: center; padding: 4rem 0; }
|
|
627
|
-
.session-detail-view {
|
|
699
|
+
.session-detail-view { display: flex; flex-direction: column; gap: 0.75rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; }
|
|
628
700
|
.detail-header { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; flex: 0 0 auto; }
|
|
629
701
|
.detail-back { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.8rem; padding: 0.35rem 0.6rem; flex: 0 0 auto; }
|
|
630
702
|
.detail-title { font-size: 1rem; font-weight: 600; color: var(--text); word-break: break-all; flex: 1 1 auto; min-width: 0; margin: 0; }
|
|
@@ -665,6 +737,11 @@ button:hover { background: var(--surface-5); }
|
|
|
665
737
|
.detail-attrs > div { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
|
|
666
738
|
.detail-attrs > div span:first-child { color: var(--text-4); font-size: 0.7rem; text-transform: capitalize; }
|
|
667
739
|
.detail-attrs > div span:last-child { color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; word-break: break-all; }
|
|
740
|
+
.detail-summary { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.6rem; background: var(--surface-2); border-radius: 0.4rem; }
|
|
741
|
+
.summary-group { display: flex; flex-direction: column; gap: 0.3rem; }
|
|
742
|
+
.summary-label { color: var(--text-4); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.02em; }
|
|
743
|
+
.summary-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
|
744
|
+
.chip { font-size: 0.75rem; color: var(--text-2); background: var(--surface-4); border: 1px solid var(--border); padding: 0.1rem 0.45rem; border-radius: 0.25rem; max-width: 22rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
668
745
|
.session-json { display: none; }
|
|
669
746
|
.pager { display: flex; justify-content: center; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; border-top: 1px solid var(--border); background: var(--surface-2); flex: 0 0 auto; }
|
|
670
747
|
.pager a, .pager button { background: var(--surface-4); color: var(--text); border: 1px solid var(--border); border-radius: 0.35rem; padding: 0.4rem 0.75rem; font-size: 0.85rem; text-decoration: none; }
|
|
@@ -680,8 +757,19 @@ button:hover { background: var(--surface-5); }
|
|
|
680
757
|
.metric-icon { position: absolute; top: 0.75rem; right: 0.75rem; opacity: 0.8; }
|
|
681
758
|
.metric-label { font-size: 0.72rem; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
|
|
682
759
|
.metric-value { font-size: 1.75rem; font-weight: 800; color: var(--text); margin-top: 0.25rem; letter-spacing: -0.02em; }
|
|
760
|
+
.context-breakdown { min-height: 0; }
|
|
761
|
+
.context-bars { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
|
762
|
+
.context-bar-block { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
763
|
+
.context-bar-title { font-size: 0.78rem; font-weight: 600; color: var(--text-3); margin: 0; }
|
|
764
|
+
.context-bar { display: flex; height: 0.85rem; border-radius: 0.4rem; overflow: hidden; background: var(--surface-4); }
|
|
765
|
+
.context-segment { height: 100%; }
|
|
766
|
+
.context-legend { display: flex; flex-direction: column; gap: 0.3rem; }
|
|
767
|
+
.context-legend-row { display: flex; align-items: center; gap: 0.4rem; font-size: 0.78rem; color: var(--text-2); }
|
|
768
|
+
.context-legend-dot { width: 0.6rem; height: 0.6rem; border-radius: 50%; flex: 0 0 auto; }
|
|
769
|
+
.context-legend-label { flex: 1 1 auto; }
|
|
770
|
+
.context-legend-value { color: var(--text-3); font-weight: 600; }
|
|
683
771
|
.empty { color: var(--text-4); text-align: center; padding: 2rem; }
|
|
684
|
-
@media (max-width: 960px) { .
|
|
772
|
+
@media (max-width: 960px) { .top-charts { grid-template-columns: 1fr; } .metric-grid { grid-template-columns: repeat(2, 1fr); } .main-metrics .metric-grid { grid-template-columns: repeat(2, 1fr); } .detail-attrs { grid-template-columns: repeat(2, 1fr); } .context-bars { grid-template-columns: 1fr; } }
|
|
685
773
|
@media (max-width: 640px) { .app { padding: 0.75rem; } .app-header { flex-direction: column; align-items: flex-start; } .header-controls, .filter-bar { width: 100%; } input.search { min-width: 0; width: 100%; } .search-wrap { width: 100%; } .metric-grid { grid-template-columns: 1fr; } .main-metrics .metric-grid { grid-template-columns: 1fr; } .detail-attrs { grid-template-columns: 1fr; } .detail-toolbar input[type="search"] { min-width: 0; width: 100%; } }
|
|
686
774
|
</style>
|
|
687
775
|
<script src="https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js" integrity="sha256-cepnGFv6jJjDnTFxfG/OXYUjcPzf0SnbRUN3TTFFwN4=" crossorigin="anonymous"><\/script>
|
|
@@ -691,20 +779,23 @@ button:hover { background: var(--surface-5); }
|
|
|
691
779
|
<div class="app">
|
|
692
780
|
<div class="sticky-bar">
|
|
693
781
|
<form data-filter-form hx-get="/fragments/sessions" hx-target="#dashboard-content" hx-swap="innerHTML" hx-trigger="change, input changed delay:300ms from:.search">
|
|
694
|
-
${
|
|
782
|
+
${en(t)}${rn(n,t)}
|
|
695
783
|
</form>
|
|
696
784
|
</div>
|
|
697
|
-
<
|
|
785
|
+
<button type="button" id="sessions-toggle" class="sessions-toggle-bar" aria-expanded="false" onclick="toggleSessionsList()">
|
|
786
|
+
<span class="sessions-toggle-arrow" aria-hidden="true">▸</span> Recent Sessions
|
|
787
|
+
</button>
|
|
788
|
+
<div id="dashboard-content" class="dashboard-content-area" sse-connect="/events/stream" hx-get="/fragments/sessions" hx-target="#dashboard-content" hx-swap="innerHTML" hx-trigger="sse:message">
|
|
698
789
|
${r}
|
|
699
790
|
</div>
|
|
700
791
|
</div>
|
|
701
792
|
</body>
|
|
702
|
-
</html>`}function
|
|
793
|
+
</html>`}function on(e,t,n,r){let i=J(n);t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(an(r,i))}let X;const Z=new Set;function sn(e,t){t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.setTimeout(0),t.write(`retry: 500
|
|
703
794
|
|
|
704
|
-
`);let n=e.headers[`last-event-id`],r=Array.isArray(n)?n[0]:n,i=r?parseInt(r,10):NaN,a={res:t,lastId:Number.isNaN(i)?
|
|
795
|
+
`);let n=e.headers[`last-event-id`],r=Array.isArray(n)?n[0]:n,i=r?parseInt(r,10):NaN,a={res:t,lastId:Number.isNaN(i)?ze(X):i,timer:null};a.timer=setInterval(()=>{try{let e=ze(X);e>a.lastId&&(t.write(`event: message
|
|
705
796
|
data: ping
|
|
706
797
|
|
|
707
|
-
`),a.lastId=e)}catch(e){console.error(`SSE poll error:`,e),clearInterval(a.timer),Z.delete(a)}},300),Z.add(a),e.on(`close`,()=>{clearInterval(a.timer),Z.delete(a)})}function
|
|
798
|
+
`),a.lastId=e)}catch(e){console.error(`SSE poll error:`,e),clearInterval(a.timer),Z.delete(a)}},300),Z.add(a),e.on(`close`,()=>{clearInterval(a.timer),Z.delete(a)})}function cn(e,t){let n=new URL(e.url??`/`,`http://localhost`);try{if(e.method===`GET`){if(n.pathname===`/`){on(e,t,n,X);return}if(n.pathname===`/fragments/sessions`){Yt(e,t,n,X);return}if(n.pathname===`/fragments/detail`){Zt(e,t,n,X);return}if(n.pathname===`/events`){Qt(e,t,n,X);return}if(n.pathname===`/events/stream`){sn(e,t);return}}t.writeHead(404,{"Content-Type":`text/plain`}),t.end(`Not found`)}catch(e){console.error(`Dashboard request error:`,e),t.headersSent?t.writableEnded||t.destroy?.():(t.writeHead(500,{"Content-Type":`text/plain`}),t.end(`Internal server error`))}}async function ln(e,t,n=!1){let r=f.createServer((e,t)=>{cn(e,t)});await new Promise((i,a)=>{let o=e=>{function s(t){r.off(`error`,s),r.off(`listening`,l),t.code===`EADDRINUSE`&&e<65535?o(e+1):a(t)}function l(){r.off(`error`,s);let a=`http://localhost:${e}`;n||console.log(`Dashboard: ${a}`),t&&c.platform===`darwin`&&p(`open`,[a],e=>{e&&console.error(`open failed:`,e.message)}),i()}r.once(`error`,s),r.once(`listening`,l),r.listen(e,`127.0.0.1`)};o(e)})}async function un(e){let t=8765,n=!0,r=!1,i=!1;for(let a of e){if(i){let e=parseInt(a,10);(Number.isNaN(e)||e<1||e>65535)&&(console.error(`Invalid --port`),c.exit(1)),t=e,i=!1;continue}if(a===`--port`){i=!0;continue}if(a.startsWith(`--port=`)){let e=parseInt(a.slice(7),10);(Number.isNaN(e)||e<1||e>65535)&&(console.error(`Invalid --port`),c.exit(1)),t=e}else a===`--no-open`?n=!1:a===`--silent`&&(r=!0,n=!1)}i&&(console.error(`Missing value for --port`),c.exit(1)),await dt(),X=T(w()),await ln(t,n,r)}const dn=[`json`,`jsonl`,`summary`];function Q(e){let t=e.trim();if(t===``)return;let n=Number(t);if(!(!Number.isFinite(n)||!Number.isInteger(n)||n<0))return n}function $(e){let t=e.trim();if(t===``)return;let n=Number(t);if(!(!Number.isFinite(n)||n<0))return n}const fn=[`24h`,`7d`,`30d`,`all`];function pn(){c.stdout.write(`Usage: happenin query [options]
|
|
708
799
|
|
|
709
800
|
Options:
|
|
710
801
|
--source <source> filter by source (cursor, claude, ...)
|
|
@@ -719,7 +810,7 @@ Options:
|
|
|
719
810
|
--format <format> output format: json (default), jsonl, summary
|
|
720
811
|
--db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
|
|
721
812
|
-h, --help show this help
|
|
722
|
-
`)}function
|
|
813
|
+
`)}function mn(e){let t={},n=`json`,r,i=!1,a;for(let o of e){if(a){switch(a){case`source`:t.source=o;break;case`event`:t.event=o;break;case`session`:t.sessionId=o;break;case`q`:t.q=o;break;case`since`:{let e=Q(o);e!==void 0&&(t.since=e);break}case`range`:fn.includes(o)&&(t.range=o);break;case`status`:(o===`active`||o===`completed`||o===`failed`)&&(t.status=o);break;case`tool`:t.tool=o;break;case`minDuration`:{let e=$(o);e!==void 0&&(t.minDuration=e);break}case`maxDuration`:{let e=$(o);e!==void 0&&(t.maxDuration=e);break}case`limit`:{let e=Q(o);e!==void 0&&(t.limit=e);break}case`offset`:{let e=Q(o);e!==void 0&&(t.offset=e);break}case`format`:dn.includes(o)&&(n=o);break;case`db`:r=o;break}a=void 0;continue}if(o===`-h`||o===`--help`){i=!0;continue}if(o===`--source`||o===`--event`||o===`--session`||o===`--q`||o===`--since`||o===`--range`||o===`--status`||o===`--tool`||o===`--minDuration`||o===`--maxDuration`||o===`--limit`||o===`--offset`||o===`--format`||o===`--db`){a=o.slice(2);continue}if(o.startsWith(`--`)){let e=o.indexOf(`=`);if(e!==-1){let i=o.slice(2,e),a=o.slice(e+1);switch(i){case`source`:t.source=a;break;case`event`:t.event=a;break;case`session`:t.sessionId=a;break;case`q`:t.q=a;break;case`since`:{let e=Q(a);e!==void 0&&(t.since=e);break}case`range`:fn.includes(a)&&(t.range=a);break;case`status`:(a===`active`||a===`completed`||a===`failed`)&&(t.status=a);break;case`tool`:t.tool=a;break;case`minDuration`:{let e=$(a);e!==void 0&&(t.minDuration=e);break}case`maxDuration`:{let e=$(a);e!==void 0&&(t.maxDuration=e);break}case`limit`:{let e=Q(a);e!==void 0&&(t.limit=e);break}case`offset`:{let e=Q(a);e!==void 0&&(t.offset=e);break}case`format`:dn.includes(a)&&(n=a);break;case`db`:r=a;break}}}}return{options:{range:`24h`,...t},format:n,dbPath:r,help:i}}async function hn(e,t,n,r=!1){let{options:i,format:a,dbPath:o,help:s}=mn(e);if(s){t();return}r||(delete i.status,delete i.minDuration,delete i.maxDuration);let c=T(o);try{await n(c,i,a)}finally{c.close()}}async function gn(e){await hn(e,pn,(e,t,n)=>{if(n===`summary`){let n=Ee(e,t);c.stdout.write(`${JSON.stringify(n,null,2)}\n`);return}let r={total:A(e,t),rows:k(e,t).map(jt)};if(n===`jsonl`)for(let e of r.rows)c.stdout.write(`${JSON.stringify(e)}\n`);else c.stdout.write(`${JSON.stringify(r,null,2)}\n`)})}function _n(){c.stdout.write(`Usage: happenin sessions [options]
|
|
723
814
|
|
|
724
815
|
Options:
|
|
725
816
|
--source <source> filter by source (cursor, claude, ...)
|
|
@@ -737,8 +828,8 @@ Options:
|
|
|
737
828
|
--format <format> output format: json (default), jsonl, summary
|
|
738
829
|
--db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
|
|
739
830
|
-h, --help show this help
|
|
740
|
-
`)}function
|
|
741
|
-
`)}async function
|
|
831
|
+
`)}function vn(e){return e.map(e=>{let t=`${Math.floor(e.durationMs/1e3)}s`,n=e.sessionId??`no-session`,r=e.firstAt??`?`,i=e.lastAt??`?`,a=e.projectPath??`-`,o=e.tools.join(`,`)||`-`,s=e.skills.join(`,`)||`-`,c=e.files.join(`,`)||`-`;return`${n}\t${r}\t${i}\t${t}\t${e.eventCount} events\tproject:${a}\ttools:${o}\tskills:${s}\tfiles:${c}\tfailures:${e.failureCount}`}).join(`
|
|
832
|
+
`)}async function yn(e){await hn(e,_n,(e,t,n)=>{let r=j(e,t);if(n===`jsonl`){for(let e of r)c.stdout.write(`${JSON.stringify(e)}\n`);return}if(n===`summary`){c.stdout.write(`${vn(r)}\n`);return}c.stdout.write(`${JSON.stringify(r,null,2)}\n`)},!0)}function bn(){let t=s.dirname(l(import.meta.url)),n=t;for(;;){let t=s.dirname(n);if(t===n)break;if(e(s.join(n,`package.json`)))return n;n=t}return t}const xn=bn(),Sn=s.join(xn,`assets`,`help.md`),Cn=s.join(xn,`package.json`);function wn(e){let t=!!c.stdout.isTTY&&!c.env.NO_COLOR,n=t?`\x1B[1m`:``,r=t?`\x1B[2m`:``,i=t?`\x1B[36m`:``,a=t?`\x1B[0m`:``;return e.replace(/^# (.+)$/gm,`${n}$1${a}`).replace(/^## (.+)$/gm,`${n}$1${a}`).replace(/^### `(.+)`$/gm,`${n}$1${a}`).replace(/^### (.+)$/gm,`${n}$1${a}`).replace(/^#### (.+)$/gm,`${r}$1${a}`).replace(/^- /gm,` • `).replace(/`([^`]+)`/g,`${i}$1${a}`)}function Tn(){let e;try{e=n(Sn,`utf8`)}catch{e=`# happenin
|
|
742
833
|
|
|
743
834
|
Run \`happenin <command>\`.
|
|
744
|
-
`}c.stdout.write(`\n${
|
|
835
|
+
`}c.stdout.write(`\n${wn(e)}\n`)}function En(){let e=`0.0.0`;try{let t=n(Cn,`utf8`);e=JSON.parse(t).version??e}catch{}c.stdout.write(`${e}\n`)}async function Dn(){let[e,...t]=c.argv.slice(2);if(!e||e===`--help`||e===`-h`){Tn();return}if(e===`--version`||e===`-v`){En();return}if(e===`install`){await ge(t);return}if(e===`record`){await Ze(t);return}if(e===`import`){await dt(t);return}if(e===`dashboard`){await un(t);return}if(e===`query`){await gn(t);return}if(e===`sessions`){await yn(t);return}c.stderr.write(`Unknown command: ${e}\n`),Tn(),c.exitCode=1}await Dn();export{};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "happenin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A macOS CLI that records Cursor and Claude Code agent events to a local SQLite database and serves a live browser dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "24",
|
|
55
|
-
"@vitest/coverage-v8": "
|
|
55
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
56
56
|
"eslint-plugin-security": "4.0.1",
|
|
57
57
|
"husky": "9.1.7",
|
|
58
58
|
"jscpd": "4.0.9",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"oxlint": "1.76.0",
|
|
63
63
|
"tsdown": "0.22.14",
|
|
64
64
|
"typescript": "7.0.2",
|
|
65
|
-
"vitest": "
|
|
65
|
+
"vitest": "5.0.0"
|
|
66
66
|
},
|
|
67
67
|
"lint-staged": {
|
|
68
68
|
"**/*": "oxfmt --write --no-error-on-unmatched-pattern",
|