happenin 0.1.1 → 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 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
  ![happenin dashboard](docs/dashboard.gif)
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`]),ee=new Set([`UserPromptSubmit`,`UserPromptExpansion`]),te=new Set([`PreToolUse`,`PermissionRequest`,`SubagentStart`,`SubagentStop`,`TaskCreated`,`TaskCompleted`,`Stop`,`WorktreeCreate`,`WorktreeRemove`]),ne=(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 ee.has(t)?JSON.stringify({continue:!0}):te.has(t)?JSON.stringify({decision:`approve`}):void 0},v=e=>typeof e==`object`&&!!e&&!Array.isArray(e),re=e=>e instanceof Error?e.message:String(e),ie=()=>c.env.HOME||u(),ae=()=>{let e=c.argv[1];return e&&s.isAbsolute(e)?e:`happenin`},oe=e=>{t(e,{recursive:!0})},y=t=>e(t),b=e=>{let t=n(e,`utf8`);return JSON.parse(t)},se=(e,t)=>{let n=`${e}.${Date.now()}.tmp`;oe(s.dirname(e)),o(n,JSON.stringify(t,null,2)+`
3
- `,`utf8`),i(n,e)},ce=(e,t)=>{if(oe(t),!y(e))return;let r=s.join(t,`${s.basename(e)}.${Date.now()}.bak`);return o(r,n(e,`utf8`),`utf8`),r},le=e=>{if(!y(e))return{version:1,hooks:{}};let t=b(e);if(!v(t))throw Error(`Invalid JSON object in ${e}`);let n=v(t.hooks)?t.hooks:{};return{...t,version:typeof t.version==`number`?t.version:1,hooks:n}},ue=e=>{if(!y(e))return{hooks:{}};let t=b(e);if(!v(t))throw Error(`Invalid JSON object in ${e}`);let n=v(t.hooks)?t.hooks:{};return{...t,hooks:n}},de=(e,t,n)=>{e[t]=[...Array.isArray(e[t])?e[t]:[],{command:`${n} record cursor`}]},fe=(e,t,n)=>{e[t]=[...Array.isArray(e[t])?e[t]:[],{matcher:``,hooks:[{type:`command`,command:`${n} record claude ${t}`}]}]},pe=e=>{let t=ie(),n=s.join(t,`.cursor`,`hooks.json`),r=s.join(t,`.happenin`,`backups`,`cursor`),i=ce(n,r),a=le(n);for(let t of m)de(a.hooks,t,e);return se(n,a),{target:n,backup:i}},me=e=>{let t=ie(),n=s.join(t,`.claude`,`settings.json`),r=s.join(t,`.happenin`,`backups`,`claude`),i=ce(n,r),a=ue(n);for(let t of h)fe(a.hooks,t,e);return se(n,a),{target:n,backup:i}},he=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 ge(e={}){let{cursor:t=!0,claude:n=!0}=e,r=ae(),i=[];t&&i.push(pe(r)),n&&i.push(me(r));for(let{target:e,backup:t}of i)c.stdout.write(`${e} written
4
- `),t&&c.stdout.write(` backup: ${t}\n`)}async function _e(e=[]){try{let{cursor:t,claude:n}=he(e);await ge({cursor:t,claude:n})}catch(e){c.stderr.write(`install failed: ${re(e)}\n`),c.exitCode=1}}const ve=[`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 x(e){let t=e.prepare(`PRAGMA user_version`).get();return Number(t?.user_version??0)}function ye(e){let t=x(e);if(!(t>=1)){if(e.exec(`BEGIN IMMEDIATE;`),t=x(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}}}const be=e=>{let t=x(e);if(!(t>=2)){if(e.exec(`BEGIN IMMEDIATE;`),t=x(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=Ce(JSON.parse(e.payload));t!==void 0&&n.run(t,e.id)}e.exec(`
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}}},xe=e=>{let t=x(e);if(t<1)throw Error(`Database schema must be migrated before backfilling subagent metadata`);if(e.exec(`BEGIN IMMEDIATE;`),t=x(e),t<1){e.exec(`ROLLBACK;`);return}try{e.exec(`
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}},S=()=>c.env.HAPPENIN_DB?c.env.HAPPENIN_DB:s.join(u(),`.happenin`,`happenin.db`),C=(e,n=5e3)=>{let r=e??S();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 (
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
- `),ye(i),be(i);for(let e of ve)i.exec(e);return i}catch(e){try{i.close()}catch{}throw e}},Se=[`happenedAt`,`timestamp`,`happened_at`,`time`,`ts`,`createdAt`,`created_at`];function w(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 Ce(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;for(let e of Se){let n=w(Reflect.get(t,e));if(n!==void 0)return n}}const T=(e,t)=>{let n=e.prepare(`
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=w(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.payload,t.sourcePath??null,t.subagentId??null,t.subagentType??null,t.transcriptPath??null)},E=`COALESCE(strftime('%Y-%m-%dT%H:%M:%fZ', happened_at), strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch'))`;function we(e){return(e===`7d`?168:e===`30d`?720:24)*60*60*1e3}function D(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.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.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(`${E} >= ?`),r.push(new Date(t-we(e.range)).toISOString())),{clause:n.length>0?`WHERE ${n.join(` AND `)}`:``,params:r}}const O=(e,t)=>{let{clause:n,params:r}=D(t),i=t.limit??100,a=t.offset??0,o=`
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)},k=(e,t)=>{let{clause:n,params:r}=D(t),i=`SELECT COUNT(*) AS count FROM events ${n}`,a=e.prepare(i).get(...r);return a?Number(a.count):0},Te=(e,t)=>{let{clause:n,params:r}=D(t);return{total:k(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)}},Ee=(e,t,n=Date.now())=>{let{clause:r,params:i}=D(t,n),a=t.limit??100,o=t.offset??0,s=`
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,19 +134,63 @@ 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(e=>{let t=Number(e.firstReceivedAt),n=Number(e.lastReceivedAt),r=JSON.parse(e.projectPaths),i=JSON.parse(e.toolNames),a=e.firstAt?Date.parse(e.firstAt):NaN,o=e.lastAt?Date.parse(e.lastAt):NaN,s=!Number.isNaN(a)&&!Number.isNaN(o)&&o>=a?o-a:n-t;return{sessionId:e.sessionId,firstAt:e.firstAt,lastAt:e.lastAt,firstReceivedAt:t,lastReceivedAt:n,durationMs:s,eventCount:Number(e.eventCount),projectPath:r[0]??null,projectPaths:r,tools:i,failureCount:Number(e.failureCount)}})},De=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=D(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)};function Oe(e){return e===`day`?`strftime('%Y-%m-%dT00:00:00.000Z', ${E})`:`strftime('%Y-%m-%dT%H:00:00.000Z', ${E})`}function ke(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 Ae(e){return e===`day`?1440*60*1e3:3600*1e3}const je=(e,t={},n=24,r=`hour`,i=Date.now())=>{if(n<=0){let n=Oe(r),{clause:a,params:o}=D({...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=Ae(r),o=ke(r,i),s=ke(r,i-n*60*60*1e3),c=Math.max(1,Math.ceil((o.getTime()-s.getTime())/a)+1),l=s.toISOString(),u=Oe(r),{clause:d,params:f}=D({...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},A=e=>{let t=e.prepare(`SELECT id FROM events ORDER BY id DESC LIMIT 1`).get();return t?Number(t.id):0},Me=(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 Ne(e,t,n){return!(t.status!==void 0&&Me(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 j=(e,t={},n=Date.now())=>{let r=Ee(e,{...t,limit:2**53-1,offset:0},n).filter(e=>Ne(e,t,n)),i=t.offset??0,a=t.limit;return i===0&&a===void 0?r:a===void 0?r.slice(i):r.slice(i,i+a)},Pe=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();return{sources:t.map(e=>e.source),events:n.map(e=>e.event),tools:r.map(e=>e.tool)}},M=(e,t,n)=>{e.prepare(`
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=`
169
+ SELECT
170
+ session_id AS sessionId,
171
+ subagent_id AS subagentId,
172
+ MAX(subagent_type) AS subagentType,
173
+ COUNT(*) AS eventCount,
174
+ MIN(received_at) AS firstReceivedAt,
175
+ MAX(received_at) AS lastReceivedAt,
176
+ MIN(happened_at) AS firstAt,
177
+ MAX(happened_at) AS lastAt,
178
+ COALESCE(json_group_array(DISTINCT project_path ORDER BY project_path) FILTER (WHERE project_path IS NOT NULL AND project_path <> ''), '[]') AS projectPaths,
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,
182
+ SUM(CASE WHEN event LIKE '%Failure%' THEN 1 ELSE 0 END) AS failureCount
183
+ FROM events
184
+ WHERE subagent_id IS NOT NULL AND subagent_id <> '' AND session_id IN (${n.map(()=>`?`).join(`,`)})${o}
185
+ GROUP BY session_id, subagent_id
186
+ ORDER BY lastReceivedAt DESC, subagent_id
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(`
141
188
  INSERT INTO imports (path, mtime, imported_at)
142
189
  VALUES (?, ?, ?)
143
190
  ON CONFLICT(path) DO UPDATE SET
144
191
  mtime = excluded.mtime,
145
192
  imported_at = excluded.imported_at
146
- `).run(t,n,Date.now())},Fe=(e,t)=>{let n=e.prepare(`SELECT mtime FROM imports WHERE path = ?`).get(t);return n?Number(n.mtime):void 0};function N(e){if(e==null||typeof e==`object`)return;let t=String(e).trim();return t.length>0?t:void 0}function P(e){let t=N(e);if(t!==void 0)return t;if(Array.isArray(e))for(let t of e){let e=N(t);if(e!==void 0)return e}}function Ie(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 Le(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`?N(r.hook_event_name):e[1];let o=i===`cursor`&&a===`subagentStart`?N(r.parent_conversation_id)??N(r.conversation_id)??N(r.sessionId)??N(r.session_id):N(r.sessionId)??N(r.session_id),s=Ce(r),c=N(r.projectPath)??N(r.cwd)??N(r.project_path)??P(r.workspaceRoot)??P(r.workspaceRoots)??P(r.workspace_roots)??P(r.workspace_root)??N(r.workspace_path),l=N(r.filePath??r.file_path??r.path),u=N(r.toolName??r.tool_name??r.tool),d=N(r.client)??(i===`cursor`?`cursor`:`claude_code`),f=N(r.subagent_id)??void 0,p=N(r.subagent_type)??void 0,m=N(r.transcript_path)??void 0,h={source:i,client:d,event:a,sessionId:o,happenedAt:s,projectPath:c,filePath:l,toolName:u,payload:t,subagentId:f,subagentType:p,transcriptPath:m},g=ne(i,a??``),_;try{if(_=C(n,500),!h.subagentId){let e=N(r.tool_use_id);if(e){let t=_.prepare(`SELECT subagent_id FROM events WHERE event = 'subagentStart' AND subagent_id = ? LIMIT 1`).get(e);t&&(h.subagentId=t.subagent_id)}}T(_,h)}catch(e){if(Ie(e))console.warn(`happenin: dropping event due to database lock`,e);else throw e}finally{try{_?.close()}catch{}}return g}async function Re(e){let t=[];for await(let e of c.stdin)t.push(Buffer.isBuffer(e)?e:Buffer.from(e,`utf8`));let n=Le(e,Buffer.concat(t).toString(`utf8`).trim());n!==void 0&&c.stdout.write(`${n}\n`)}const F=`claude-transcript`,I=`cursor-transcript`,ze=`cursor`,L=e=>{try{return r(e,{withFileTypes:!0})}catch{return[]}},Be=e=>{try{return n(e,`utf8`)}catch{return}},Ve=e=>{try{return Math.floor(a(e).mtimeMs)}catch{return}},R=(e,t)=>{let n=Ve(t);if(n===void 0||Fe(e,t)===n)return;let r=Be(t);if(r!==void 0)return{content:r,mtime:n}};function z(e){let t=[];for(let n of L(e)){if(n.isSymbolicLink())continue;let r=s.join(e,n.name);n.isDirectory()?t.push(...z(r)):n.isFile()&&n.name.endsWith(`.jsonl`)&&n.name!==`store.db`&&t.push(r)}return t}function He(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 Ue(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(F,t);for(let{obj:n,line:r}of o)T(e,{source:F,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});M(e,t,i)}async function We(e){let t=s.resolve(u(),`.claude/projects`);for(let n of z(t))await Ue(e,n)}async function Ge(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`&&T(e,{source:I,client:ze,event:`prompt`,sessionId:n,payload:JSON.stringify({prompt:r}),sourcePath:t});M(e,t,a)}async function Ke(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),T(e,{source:I,client:ze,event:`session_meta`,sessionId:n,payload:i,sourcePath:t}),M(e,t,a)}async function qe(e,t){let n=s.basename(t);await Ge(e,s.join(t,`prompt_history.json`),n),await Ke(e,s.join(t,`meta.json`),n)}async function Je(e){let t=s.resolve(u(),`.cursor/chats`);for(let n of He(t))await qe(e,n)}async function B(e){let t=C(S());try{xe(t),await Ye(t)}finally{t.close()}}async function Ye(e){await We(e),await Je(e)}function V(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`)}function H(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}function U(e,t=24){return e.length<=t?e:`${e.slice(0,t-1)}…`}function W(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 G(e){if(e==null||e===``)return`-`;let t=new Date(e);return Number.isNaN(t.getTime())?`-`:t.toLocaleString()}function Xe(e){let t=e.totalSessions===0?`-`:`${Math.round(e.successRate)}%`;return`<div class="metric-grid">
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,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`)}function B(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}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">
147
194
  <div class="metric-card metric-sessions">
148
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>
149
196
  <div class="metric-label">Total Sessions</div>
@@ -157,79 +204,97 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
157
204
  <div class="metric-card metric-duration">
158
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>
159
206
  <div class="metric-label">Average Duration</div>
160
- <div class="metric-value">${W(e.averageDurationMs)}</div>
207
+ <div class="metric-value">${ht(e.averageDurationMs)}</div>
161
208
  </div>
162
209
  <div class="metric-card metric-success">
163
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>
164
211
  <div class="metric-label">Success Rate</div>
165
212
  <div class="metric-value">${t}</div>
166
213
  </div>
167
- </div>`}function Ze(e,t){return t===`day`?e.slice(5,10):e.slice(11,16)}function Qe(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:Ze(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">${V(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 $e(e){if(e.length===0)return`<div class="chart-panel"><h2 class="chart-title">Most Used Tools</h2><div class="chart-empty">No data</div></div>`;let t=Math.max(...e.map(e=>e.count));return`<div class="chart-panel"><h2 class="chart-title">Most Used Tools</h2><div class="tool-chart">${e.map(e=>{let n=t>0?e.count/t*100:0,r=e.tool;return`<div class="tool-row"><span class="tool-name"${r.length>20?` title="${H(r)}"`:``}>${V(r.length>20?`${r.slice(0,19)}…`:r)}</span><div class="tool-bar-bg"><div class="tool-bar" style="width: ${n}%"></div></div><span class="tool-count">${e.count}</span></div>`}).join(``)}</div></div>`}function et(e,t){let n=new URLSearchParams({session:t});return e?.source&&n.set(`source`,e.source),e?.event&&n.set(`event`,e.event),e?.q&&n.set(`q`,e.q),e?.range&&n.set(`range`,e.range),e?.status&&n.set(`status`,e.status),e?.tool&&n.set(`tool`,e.tool),e?.minDuration!==void 0&&n.set(`minDuration`,String(e.minDuration)),e?.maxDuration!==void 0&&n.set(`maxDuration`,String(e.maxDuration)),e?.limit!==void 0&&n.set(`limit`,String(e.limit)),e?.offset!==void 0&&n.set(`offset`,String(e.offset)),`/fragments/detail?${n.toString()}`}function tt(e,t=Date.now(),n,r){return e.length===0?`<h2 class="session-list-title">Recent Sessions</h2><ul class="session-list sessions-table"><li class="empty">No sessions found.</li></ul>`:`<h2 class="session-list-title">Recent Sessions</h2>
168
- <ul class="session-list sessions-table">${e.map(e=>{let i=Me(e,t),a=e.sessionId??`no session`,o=V(U(a,28)),s=V(G(e.firstAt??e.firstReceivedAt)),c=V(W(e.durationMs)),l=n!==void 0&&a===n?` active`:``,u=e.sessionId?` hx-get="${H(et(r,e.sessionId))}" hx-target="#dashboard-content" hx-swap="innerHTML"`:``;return`<li class="${`session-item${l}${e.sessionId?``:` session-item-static`}`}" data-session="${H(a)}"${u}>
169
- <div class="session-main">
170
- <div class="session-row">
171
- <span class="session-id" title="${H(a)}">${o}</span>
172
- <span class="status-badge status-${i}">${i}</span>
173
- </div>
174
- <div class="session-meta"><span>${c}</span><span>${e.eventCount} events</span><span>${s}</span></div>
175
- </div>
176
- <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>
177
- </li>`}).join(``)}</ul>`}function nt(e){if(e)try{return JSON.parse(e)}catch{return}}function rt(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:nt(e.payload??``)??e.payload}}function it(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 K(e){return`<span class="ts">${V(G(e.happenedAt??e.receivedAt))}</span><span class="source">${V(e.source??``)}</span><span class="event">${V(e.event??``)}</span><span class="tool">${V(e.toolName??``)}</span>`}function at(e){let t=V(G(e.happenedAt??e.receivedAt)),n=V(e.source??``),r=V(e.client??``),i=V(e.event??``),a=V(e.toolName??``),o=V(e.subagentId??``),s=V(e.subagentType??``),c=V(e.projectPath??``),l=V(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">
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}${_}>
229
+ ${o}
230
+ <div class="session-main"${v}>
231
+ <div class="session-row">
232
+ <span class="session-id" title="${B(l)}">${u}${ne}</span>
233
+ <span class="status-badge status-${c}">${c}</span>
234
+ </div>
235
+ <div class="session-meta"><span>${f}</span><span>${e.eventCount} events</span><span>${d}</span></div>
236
+ </div>
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>
238
+ ${s}
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">
178
240
  <div><span>Time</span><span>${t}</span></div>
179
241
  <div><span>Source</span><span>${n}</span></div>
180
242
  <div><span>Client</span><span>${r||`-`}</span></div>
181
243
  <div><span>Event</span><span>${i}</span></div>
182
244
  <div><span>Tool</span><span>${a||`-`}</span></div>
183
245
  <div><span>Project</span><span>${c||`-`}</span></div>
184
- ${u}
185
246
  ${d}
186
- </div>`}function ot(e){return`<div class="detail-json">
247
+ ${f}
248
+ ${p}
249
+ </div>`}function Ft(e){return`<div class="detail-json">
187
250
  <details class="json-toggle">
188
251
  <summary>JSON</summary>
189
- <pre>${V(JSON.stringify(rt(e),null,2))}</pre>
252
+ <pre>${z(JSON.stringify(jt(e),null,2))}</pre>
190
253
  </details>
191
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>
192
- </div>`}function st(e){return`<details class="detail-event" data-event-id="${e.id}" open>
193
- <summary>${K(e)}</summary>
194
- <div class="detail-body">${at(e)}
195
- ${ot(e)}
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)}
196
259
  </div>
197
- </details>`}function ct(e,t){let n=e.parent,r=e.children.map(st).join(``),i=`<span class="subagent-badge">${V(n.subagentType||`subagent`)}${` · ${V(U(t,18))}`} · ${e.children.length}</span>`;return`<details class="detail-event detail-subagent" data-event-id="${n.id}" open>
198
- <summary>${K(n)}${i}</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>
199
262
  <div class="detail-body">
200
- ${at(n)}
201
- ${ot(n)}
263
+ ${Pt(n)}
264
+ ${Ft(n)}
202
265
  <div class="detail-children">${r}</div>
203
266
  </div>
204
- </details>`}function lt(e){return it(e).map(e=>e.subagentId?ct(e,e.subagentId):[e.parent,...e.children].map(st).join(``)).join(``)}function ut(e,t,n){if(!e)return`<div class="detail-header"><h2 class="detail-title">Session Details</h2></div><div class="empty">No events.</div>`;if(t.length===0)return`<div class="detail-header"><h2 class="detail-title">Session Details - ${V(e)}</h2></div><div class="empty">No events for this session.</div>`;let r=lt(t),i=JSON.stringify(t,null,2),a=n!==void 0&&n>t.length?`<div class="detail-truncated">Showing the ${t.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="${H(e)}">
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??``)}">
205
268
  <div class="detail-header">
206
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>
207
- <h2 class="detail-title">Session Details - ${V(e)}</h2>
208
- <span class="detail-count">${n??t.length} events</span>
270
+ <h2 class="detail-title">Session Details - ${z(s)}</h2>
271
+ <span class="detail-count">${n??o.length} events</span>
209
272
  </div>
210
- ${a}
273
+ ${It(i)}
274
+ ${a?Dt(a):``}
275
+ ${u}
211
276
  <div class="detail-toolbar">
212
277
  <input type="search" class="detail-search" placeholder="Search attributes..." oninput="filterSessionDetails(this.value)">
213
278
  <button type="button" onclick="toggleSessionDetails(false)">Collapse All</button>
214
279
  <button type="button" onclick="toggleSessionDetails(true)">Expand All</button>
215
280
  </div>
216
- <div class="detail-events">${r}</div>
281
+ <div class="detail-events">${c}</div>
217
282
  <div class="detail-actions">
218
283
  <button type="button" onclick="copySessionJson()">Copy JSON</button>
219
284
  </div>
220
- <pre id="session-json" class="session-json">${V(i)}</pre>
221
- </section>`}function dt(e){if(!e)return;let t=Number(e);return Number.isFinite(t)&&t>=0?t:void 0}function ft(e){if(!e)return;let t=Number(e);return Number.isInteger(t)&&t>=0?t:void 0}function pt(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 q(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(`q`)||void 0,o=e.searchParams.get(`status`)||void 0,s=e.searchParams.get(`tool`)||void 0,c=e.searchParams.get(`minDuration`),l=e.searchParams.get(`maxDuration`),u=e.searchParams.get(`range`)||`24h`,d=[`24h`,`7d`,`30d`,`all`].includes(u)?u:`24h`,f=e.searchParams.get(`limit`),p=e.searchParams.get(`offset`),m;if(t){let e=parseInt(t,10);Number.isNaN(e)||(m=e)}return{since:m,source:n,event:r,sessionId:i,q:a,status:o===`active`||o===`completed`||o===`failed`?o:void 0,tool:s,minDuration:dt(c),maxDuration:dt(l),range:d,limit:ft(f),offset:ft(p)}}function J(e,t,n){let r=new URLSearchParams;return e.source&&r.set(`source`,e.source),e.event&&r.set(`event`,e.event),e.sessionId&&r.set(`session`,e.sessionId),e.q&&r.set(`q`,e.q),e.since!==void 0&&r.set(`since`,String(e.since)),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)),r.set(`limit`,String(n)),r.set(`offset`,String(t)),`/fragments/sessions?${r.toString()}`}function mt(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;return`<div class="pager">${o>=0?`<a href="${J(e,o,n)}" hx-get="${J(e,o,n)}" 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="${J(e,s,n)}" hx-get="${J(e,s,n)}" hx-target="#dashboard-content" hx-swap="innerHTML">Next</a>`:`<button type="button" disabled>Next</button>`}</div>`}function ht(e,t,n,r){let i=t.limit&&t.limit>0?t.limit:25,a=t.offset??0;return`<aside class="session-sidebar">
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}
222
289
  <div class="session-list-wrapper">
223
- ${tt(e.slice(a,a+i),n,r,t)}
224
- ${mt(t,e.length)}
290
+ ${kt(o,n,void 0,void 0,t)}
291
+ ${Gt(t,r.length)}
225
292
  </div>
226
- </aside>`}function gt(e,t){let n=Date.now(),{hours:r,groupBy:i}=pt(t.range),a=j(e,{...t,limit:void 0,offset:void 0},n),o=a.map(e=>e.sessionId),s=[],c=[];if(o.length>0){let a={...t,sessionIds:o};s=je(e,a,r,i,n),c=De(e,a,10,n)}let l=a.length,u=a.reduce((e,t)=>e+t.eventCount,0),d=l>0?a.reduce((e,t)=>e+t.durationMs,0)/l:0,f=a.filter(e=>e.failureCount===0).length,p={totalSessions:l,totalEvents:u,averageDurationMs:d,successRate:l>0?f/l*100:0};return`${ht(a,t,n)}
227
- <div class="main-content">
228
- <div class="main-metrics">${Xe(p)}</div>
229
- <div class="top-charts">${Qe(s,i)}${$e(c)}</div>
230
- </div>`}function _t(e,t,n,r){let i=gt(r,q(n));t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(i)}function vt(e,t){let n=Date.now(),r=ht(j(e,{...t,sessionId:void 0,limit:void 0,offset:void 0},n),t,n,t.sessionId);if(!t.sessionId)return`${r}
231
- <div class="main-content"><div class="detail-area"><div class="empty">No events.</div></div></div>`;let i=O(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,limit:1e3,offset:0}),a=k(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0});return`${r}
232
- <div class="main-content">${ut(t.sessionId,i,a)}</div>`}function yt(e,t,n,r){let i=vt(r,q(n));t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(i)}function bt(e,t,n,r){let i=O(r,q(n));t.writeHead(200,{"Content-Type":`application/json`}),t.end(JSON.stringify(i))}const xt=[{value:`24h`,label:`Last 24h`},{value:`7d`,label:`Last 7 days`},{value:`30d`,label:`Last 30 days`},{value:`all`,label:`All time`}];function St(e){let t=V(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">
233
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>
234
299
  <h1 class="page-title">Session Overview Analytics</h1>
235
300
  <div class="header-controls">
@@ -237,17 +302,18 @@ ${mt(t,e.length)}
237
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>
238
303
  <input type="text" name="q" class="search" placeholder="session or payload" value="${t}" autocomplete="off">
239
304
  </div>
240
- <label>range <select name="range">${xt.map(e=>`<option value="${H(e.value)}"${e.value===n?` selected`:``}>${V(e.label)}</option>`).join(``)}</select></label>
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>
241
306
  <button type="button" class="theme" data-theme-toggle title="Toggle light/dark theme" aria-label="toggle theme">🌙</button>
242
307
  </div>
243
- </header>`}const Ct=[`active`,`completed`,`failed`];function Y(e,t,n,r=`all`){let i=n??``,a=H(i);return`<select name="${e}"><option value=""${i===``?` selected`:``}>${r}</option>${t.map(e=>{let t=H(e);return`<option value="${t}"${t===a?` selected`:``}>${V(e)}</option>`}).join(``)}</select>`}function wt(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);return`<div class="filter-bar">
244
- <label>status ${Y(`status`,Ct,n)}</label>
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>
245
310
  <label>source ${Y(`source`,e.sources,r)}</label>
246
311
  <label>tool ${Y(`tool`,e.tools,i)}</label>
247
312
  <label>event ${Y(`event`,e.events,a)}</label>
248
- <label>min duration (m) <input type="number" name="minDuration" min="0" step="any" placeholder="min" value="${V(o)}"></label>
249
- <label>max duration (m) <input type="number" name="maxDuration" min="0" step="any" placeholder="max" value="${V(s)}"></label>
250
- </div>`}function Tt(e,t){let n=Pe(e),r=gt(e,t);return`<!DOCTYPE html>
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>
251
317
  <html lang="en" hx-ext="sse">
252
318
  <head>
253
319
  <meta charset="utf-8">
@@ -431,24 +497,42 @@ ${mt(t,e.length)}
431
497
  if (!content) return;
432
498
  const detail = content.querySelector('.session-detail-view');
433
499
  document.body.classList.toggle('session-detail-open', !!detail);
434
- const sessionId = detail ? detail.getAttribute('data-session') : null;
435
- for (const item of content.querySelectorAll('.session-item')) {
436
- item.classList.toggle('active', !!sessionId && item.dataset.session === sessionId);
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
+ }
437
522
  }
438
- const list = content.querySelector('.session-list');
439
- if (list) {
440
- list.scrollTop = sidebarScroll;
441
- const active = content.querySelector('.session-item.active');
442
- 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 });
443
527
  }
444
528
  }
445
529
 
446
- let sidebarScroll = 0;
447
530
  document.addEventListener('htmx:beforeSwap', (e) => {
448
- const list = document.querySelector('.session-list');
449
- if (list) sidebarScroll = list.scrollTop;
450
531
  const elt = e.detail.requestConfig ? e.detail.requestConfig.elt : null;
451
- if (elt && elt.id === 'dashboard-content' && document.body.classList.contains('session-detail-open')) {
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) {
452
536
  e.detail.shouldSwap = false;
453
537
  }
454
538
  });
@@ -458,10 +542,12 @@ ${mt(t,e.length)}
458
542
  window.copySessionJson = copySessionJson;
459
543
  window.copyEventJson = copyEventJson;
460
544
  window.backToDashboard = backToDashboard;
545
+ window.toggleSessionsList = toggleSessionsList;
461
546
 
462
547
  document.addEventListener('htmx:afterSettle', () => {
463
548
  updateContentUrl();
464
549
  syncDetailState();
550
+ syncSessionsToggle();
465
551
  });
466
552
 
467
553
  applyTheme(getTheme());
@@ -517,9 +603,9 @@ ${mt(t,e.length)}
517
603
  --danger: #dc2626;
518
604
  }
519
605
  * { box-sizing: border-box; }
520
- html, body { height: 100%; }
606
+ html, body { min-height: 100%; }
521
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; }
522
- .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; }
523
609
  .sticky-bar { flex: 0 0 auto; display: flex; flex-direction: column; gap: 1rem; background: var(--bg); position: sticky; top: 0; z-index: 10; }
524
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; }
525
611
  .brand { font-weight: 800; font-size: 1.25rem; display: flex; align-items: center; gap: 0.5rem; color: var(--text); }
@@ -538,11 +624,17 @@ select { min-width: 7.5rem; cursor: pointer; }
538
624
  button { cursor: pointer; background: var(--surface-4); font-weight: 500; }
539
625
  button:hover { background: var(--surface-5); }
540
626
  .theme { display: inline-flex; align-items: center; justify-content: center; font-size: 1.25rem; padding: 0.35rem 0.6rem; line-height: 1; }
541
- .dashboard-layout { flex: 1 1 auto; display: grid; grid-template-columns: 22rem 1fr; gap: 1.25rem; min-height: 0; overflow: hidden; }
542
- .session-sidebar { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; min-height: 0; }
543
- .session-list-wrapper { flex: 1 1 auto; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
544
- .session-list-title { padding: 0.75rem 1rem; margin: 0; font-size: 0.9rem; font-weight: 600; color: var(--text-2); border-bottom: 1px solid var(--border); background: var(--surface-2); position: sticky; top: 0; z-index: 2; }
545
- .session-list { flex: 1 1 auto; list-style: none; padding: 0; margin: 0; overflow-y: auto; }
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; }
546
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; }
547
639
  .status-active { background: rgba(34,197,94,0.15); color: var(--success); }
548
640
  .status-completed { background: rgba(96,165,250,0.15); color: var(--accent); }
@@ -561,13 +653,27 @@ button:hover { background: var(--surface-5); }
561
653
  .session-chevron { color: var(--text-3); background: none; border: none; padding: 0.2rem; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
562
654
  .session-chevron:hover { color: var(--accent); background: none; }
563
655
  .session-list .empty { padding: 2rem 1rem; text-align: center; color: var(--text-4); }
564
- .main-content { display: grid; grid-template-rows: auto 1fr; gap: 1.25rem; min-height: 0; overflow: hidden; }
565
- .main-content:has(.session-detail-view) { grid-template-rows: 1fr; }
656
+ .session-item { flex-wrap: wrap; }
657
+ .session-item.session-parent { padding-bottom: 0; cursor: default; }
658
+ .session-item.session-parent:hover { background: none; }
659
+ .session-item.session-parent > .session-main { cursor: pointer; }
660
+ .session-item.session-parent > .session-main:hover { background: var(--surface-3); }
661
+ .session-toggle { background: none; border: none; color: var(--text-3); font-size: 0.8rem; padding: 0; margin-right: 0.5rem; cursor: pointer; width: 1rem; text-align: center; }
662
+ .session-toggle:hover { color: var(--accent); }
663
+ .session-children { display: none; width: 100%; list-style: none; padding: 0; margin: 0; align-self: flex-start; }
664
+ .session-children .session-item { padding-left: 2.75rem; background: none; border-bottom: 1px solid var(--border-2); border-left: 3px solid var(--border-3); }
665
+ .session-children .session-item:hover { background: var(--surface-3); }
666
+ .session-children .session-item.active { background: var(--surface-3); border-left-color: var(--accent-2); box-shadow: inset 3px 0 0 var(--accent-2); }
667
+ .session-children .session-subagent .session-id { color: var(--accent-2); }
668
+ .session-children .session-subagent .session-chevron { display: none; }
669
+ .session-item.expanded > .session-children { display: block; }
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; }
671
+ .main-content { display: flex; flex-direction: column; gap: 1.25rem; }
566
672
  .main-metrics .metric-grid { grid-template-columns: repeat(4, 1fr); gap: 1rem; }
567
673
  .main-metrics .metric-card { min-height: 5rem; }
568
674
  .main-metrics .metric-value { font-size: 1.65rem; }
569
- .top-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; min-height: 0; }
570
- .chart-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
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; }
571
677
  .chart-title { font-size: 0.9rem; font-weight: 600; color: var(--text-2); margin: 0 0 0.75rem; flex: 0 0 auto; }
572
678
  .chart-empty { color: var(--text-4); text-align: center; padding: 4rem 0; }
573
679
  .chart-max { text-align: right; color: var(--text-4); font-size: 0.7rem; margin-top: 0.25rem; flex: 0 0 auto; }
@@ -578,15 +684,19 @@ button:hover { background: var(--surface-5); }
578
684
  .event-chart text { fill: var(--text-3); font-size: 10px; }
579
685
  .event-chart .y-label { font-size: 9px; fill: var(--text-4); }
580
686
  .event-chart .grid { stroke: var(--surface-4); stroke-width: 1; }
581
- .tool-chart { display: flex; flex-direction: column; gap: 0.65rem; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
687
+ .tool-chart { display: flex; flex-direction: column; gap: 0.65rem; }
582
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); }
583
693
  .tool-name { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
584
694
  .tool-bar-bg { background: var(--surface-4); border-radius: 0.4rem; height: 0.85rem; overflow: hidden; }
585
695
  .tool-bar { background: linear-gradient(90deg, var(--accent), var(--accent-2)); height: 100%; border-radius: 0.4rem; }
586
696
  .tool-count { color: var(--text-3); text-align: right; font-weight: 600; }
587
697
  .detail-area { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; }
588
698
  .detail-area > .empty { color: var(--text-4); text-align: center; padding: 4rem 0; }
589
- .session-detail-view { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 0.75rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; overflow: hidden; }
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; }
590
700
  .detail-header { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; flex: 0 0 auto; }
591
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; }
592
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; }
@@ -627,6 +737,11 @@ button:hover { background: var(--surface-5); }
627
737
  .detail-attrs > div { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
628
738
  .detail-attrs > div span:first-child { color: var(--text-4); font-size: 0.7rem; text-transform: capitalize; }
629
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; }
630
745
  .session-json { display: none; }
631
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; }
632
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; }
@@ -642,8 +757,19 @@ button:hover { background: var(--surface-5); }
642
757
  .metric-icon { position: absolute; top: 0.75rem; right: 0.75rem; opacity: 0.8; }
643
758
  .metric-label { font-size: 0.72rem; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
644
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; }
645
771
  .empty { color: var(--text-4); text-align: center; padding: 2rem; }
646
- @media (max-width: 960px) { .dashboard-layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; } .session-sidebar { max-height: 24rem; } .session-list { max-height: 16rem; } .main-content { overflow-y: auto; grid-template-rows: auto auto; } .top-charts { grid-template-columns: 1fr; } .chart-panel { min-height: 16rem; } .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); } }
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; } }
647
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%; } }
648
774
  </style>
649
775
  <script src="https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js" integrity="sha256-cepnGFv6jJjDnTFxfG/OXYUjcPzf0SnbRUN3TTFFwN4=" crossorigin="anonymous"><\/script>
@@ -653,20 +779,23 @@ button:hover { background: var(--surface-5); }
653
779
  <div class="app">
654
780
  <div class="sticky-bar">
655
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">
656
- ${St(t)}${wt(n,t)}
782
+ ${en(t)}${rn(n,t)}
657
783
  </form>
658
784
  </div>
659
- <div id="dashboard-content" class="dashboard-layout" sse-connect="/events/stream" hx-get="/fragments/sessions" hx-target="#dashboard-content" hx-swap="innerHTML" hx-trigger="sse:message">
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">
660
789
  ${r}
661
790
  </div>
662
791
  </div>
663
792
  </body>
664
- </html>`}function Et(e,t,n,r){let i=q(n);t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(Tt(r,i))}let X;const Z=new Set;function Dt(e,t){t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.setTimeout(0),t.write(`retry: 500
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
665
794
 
666
- `);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)?A(X):i,timer:null};a.timer=setInterval(()=>{try{let e=A(X);e>a.lastId&&(t.write(`event: message
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
667
796
  data: ping
668
797
 
669
- `),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 Ot(e,t){let n=new URL(e.url??`/`,`http://localhost`);try{if(e.method===`GET`){if(n.pathname===`/`){Et(e,t,n,X);return}if(n.pathname===`/fragments/sessions`){_t(e,t,n,X);return}if(n.pathname===`/fragments/detail`){yt(e,t,n,X);return}if(n.pathname===`/events`){bt(e,t,n,X);return}if(n.pathname===`/events/stream`){Dt(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 kt(e,t,n=!1){let r=f.createServer((e,t)=>{Ot(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 At(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 B(),X=C(S()),await kt(t,n,r)}const jt=[`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 Mt=[`24h`,`7d`,`30d`,`all`];function Nt(){c.stdout.write(`Usage: happenin query [options]
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]
670
799
 
671
800
  Options:
672
801
  --source <source> filter by source (cursor, claude, ...)
@@ -681,7 +810,7 @@ Options:
681
810
  --format <format> output format: json (default), jsonl, summary
682
811
  --db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
683
812
  -h, --help show this help
684
- `)}function Pt(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`:Mt.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`:jt.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`:Mt.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`:jt.includes(a)&&(n=a);break;case`db`:r=a;break}}}}return{options:{range:`24h`,...t},format:n,dbPath:r,help:i}}async function Ft(e,t,n,r=!1){let{options:i,format:a,dbPath:o,help:s}=Pt(e);if(s){t();return}r||(delete i.status,delete i.minDuration,delete i.maxDuration);let c=C(o);try{await n(c,i,a)}finally{c.close()}}async function It(e){await Ft(e,Nt,(e,t,n)=>{if(n===`summary`){let n=Te(e,t);c.stdout.write(`${JSON.stringify(n,null,2)}\n`);return}let r={total:k(e,t),rows:O(e,t).map(rt)};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 Lt(){c.stdout.write(`Usage: happenin sessions [options]
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]
685
814
 
686
815
  Options:
687
816
  --source <source> filter by source (cursor, claude, ...)
@@ -699,8 +828,8 @@ Options:
699
828
  --format <format> output format: json (default), jsonl, summary
700
829
  --db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
701
830
  -h, --help show this help
702
- `)}function Rt(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(`,`)||`-`;return`${n}\t${r}\t${i}\t${t}\t${e.eventCount} events\tproject:${a}\ttools:${o}\tfailures:${e.failureCount}`}).join(`
703
- `)}async function zt(e){await Ft(e,Lt,(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(`${Rt(r)}\n`);return}c.stdout.write(`${JSON.stringify(r,null,2)}\n`)},!0)}function Bt(){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 Vt=Bt(),Ht=s.join(Vt,`assets`,`help.md`),Ut=s.join(Vt,`package.json`);function Wt(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 Gt(){let e;try{e=n(Ht,`utf8`)}catch{e=`# happenin
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
704
833
 
705
834
  Run \`happenin <command>\`.
706
- `}c.stdout.write(`\n${Wt(e)}\n`)}function Kt(){let e=`0.0.0`;try{let t=n(Ut,`utf8`);e=JSON.parse(t).version??e}catch{}c.stdout.write(`${e}\n`)}async function qt(){let[e,...t]=c.argv.slice(2);if(!e||e===`--help`||e===`-h`){Gt();return}if(e===`--version`||e===`-v`){Kt();return}if(e===`install`){await _e(t);return}if(e===`record`){await Re(t);return}if(e===`import`){await B(t);return}if(e===`dashboard`){await At(t);return}if(e===`query`){await It(t);return}if(e===`sessions`){await zt(t);return}c.stderr.write(`Unknown command: ${e}\n`),Gt(),c.exitCode=1}await qt();export{};
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.1.1",
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": "^4.1.10",
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": "^4.1.10"
65
+ "vitest": "5.0.0"
66
66
  },
67
67
  "lint-staged": {
68
68
  "**/*": "oxfmt --write --no-error-on-unmatched-pattern",