happenin 0.0.1 → 0.1.1

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
@@ -1,7 +1,14 @@
1
1
  # happenin
2
2
 
3
+ [![Known Vulnerabilities](https://snyk.io/test/npm/happenin/badge.svg)](https://snyk.io/test/npm/happenin)
4
+ [![npm package version](https://badgen.net/npm/v/happenin)](https://npm.im/happenin)
5
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v3.0%20adopted-ff69b4.svg)](docs/CODE_OF_CONDUCT.md)
6
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
7
+
3
8
  A macOS CLI that records Cursor and Claude Code agent events to a local SQLite database and serves a live browser dashboard.
4
9
 
10
+ ![happenin dashboard](docs/dashboard.gif)
11
+
5
12
  ## Why
6
13
 
7
14
  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.
@@ -13,9 +20,9 @@ Cursor and Claude Code can emit local hooks for each session, tool use, prompt,
13
20
  ## Requirements
14
21
 
15
22
  - macOS
16
- - Node.js `>= 24.0.0` (uses the built-in `node:sqlite` module)
23
+ - Node.js `>= 22.13.0` (uses the built-in `node:sqlite` module, available since Node 22.13)
17
24
  - Zero runtime dependencies
18
- - The dashboard loads htmx, AlpineJS, and htmx-ext-sse from a CDN
25
+ - The dashboard loads htmx and htmx-ext-sse from a CDN
19
26
 
20
27
  ## Install
21
28
 
@@ -93,6 +100,18 @@ happenin query --session abc123 --format summary
93
100
 
94
101
  See [docs/USAGE.md](docs/USAGE.md) for all filters.
95
102
 
103
+ ### `happenin sessions [options]`
104
+
105
+ Summarize recorded events grouped by session. Useful for reviewing activity across many sessions and event volumes.
106
+
107
+ ```bash
108
+ happenin sessions --limit 10
109
+ happenin sessions --source cursor --format jsonl
110
+ happenin sessions --session abc123 --format summary
111
+ ```
112
+
113
+ See [docs/USAGE.md](docs/USAGE.md) for all filters.
114
+
96
115
  ### `happenin dashboard [--port <port>] [--no-open] [--silent]`
97
116
 
98
117
  Starts a local HTTP server and opens the dashboard.
@@ -112,7 +131,7 @@ happenin dashboard --port 9000 --silent
112
131
 
113
132
  ## Privacy
114
133
 
115
- `happenin` stores full hook payloads and transcripts locally. No data is sent over the network except for the CDN-loaded dashboard libraries (htmx, AlpineJS, htmx-ext-sse) when the dashboard is open.
134
+ `happenin` stores full hook payloads and transcripts locally. No data is sent over the network except for the CDN-loaded dashboard libraries (htmx and htmx-ext-sse) when the dashboard is open.
116
135
 
117
136
  ## Development
118
137
 
package/assets/help.md CHANGED
@@ -38,13 +38,34 @@ Query events from the local database and print them as JSON, JSONL, or a summary
38
38
  - `--source <source>` — filter by source.
39
39
  - `--event <event>` — filter by event name.
40
40
  - `--session <id>` — filter by session id (partial match).
41
- - `--q <text>` — search event payloads.
41
+ - `--q <text>` — search event payloads and session ids.
42
42
  - `--since <id>` — events with an id greater than `<id>`.
43
+ - `--range <range>` — time range: `24h`, `7d`, `30d`, `all` (default: `24h`).
44
+ - `--tool <tool>` — filter by tool name.
43
45
  - `--limit <n>` — maximum rows to return (default: `100`).
44
46
  - `--offset <n>` — skip the first `<n>` rows.
45
47
  - `--format <json|jsonl|summary>` — output format (default: `json`).
46
48
  - `--db <path>` — database path (default: `HAPPENIN_DB` or `~/.happenin/happenin.db`).
47
49
 
50
+ ### `happenin sessions [options]`
51
+
52
+ List sessions with id, start/end time, duration, event count, project path, tools used, and failure count.
53
+
54
+ - `--source <source>` — filter sessions by source.
55
+ - `--event <event>` — filter sessions by event name.
56
+ - `--session <id>` — filter by session id (partial match).
57
+ - `--q <text>` — search event payloads and session ids.
58
+ - `--since <id>` — events with an id greater than `<id>`.
59
+ - `--range <range>` — time range: `24h`, `7d`, `30d`, `all` (default: `24h`).
60
+ - `--status <status>` — filter by session status: `active`, `completed`, `failed`.
61
+ - `--tool <tool>` — filter by tool name.
62
+ - `--minDuration <m>` — minimum session duration in minutes.
63
+ - `--maxDuration <m>` — maximum session duration in minutes.
64
+ - `--limit <n>` — maximum sessions to return (default: `100`).
65
+ - `--offset <n>` — skip the first `<n>` sessions.
66
+ - `--format <json|jsonl|summary>` — output format (default: `json`).
67
+ - `--db <path>` — database path (default: `HAPPENIN_DB` or `~/.happenin/happenin.db`).
68
+
48
69
  ### `happenin dashboard [--port <port>] [--no-open] [--silent]`
49
70
 
50
71
  Starts a local HTTP server and opens the dashboard in your browser.
package/dist/bin.js CHANGED
@@ -1,7 +1,30 @@
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:process";import c from"node:path";import{homedir as l}from"node:os";import{DatabaseSync as u}from"node:sqlite";import d from"node:http";import{execFile as f}from"node:child_process";const p=[`sessionStart`,`sessionEnd`,`beforeSubmitPrompt`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`afterAgentResponse`,`afterAgentThought`,`preCompact`,`stop`],m=`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(`.`),h=new Set([`beforeSubmitPrompt`]),g=new Set([`preToolUse`,`beforeShellExecution`,`beforeMCPExecution`,`beforeReadFile`,`subagentStart`]),_=new Set([`UserPromptSubmit`,`UserPromptExpansion`]),v=new Set([`PreToolUse`,`PermissionRequest`,`SubagentStart`,`SubagentStop`,`TaskCreated`,`TaskCompleted`,`Stop`,`WorktreeCreate`,`WorktreeRemove`]),ee=(e,t)=>{if(e===`cursor`)return h.has(t)?JSON.stringify({continue:!0}):g.has(t)?JSON.stringify({permission:`allow`}):void 0;if(e===`claude`)return _.has(t)?JSON.stringify({continue:!0}):v.has(t)?JSON.stringify({decision:`approve`}):void 0},y=e=>typeof e==`object`&&!!e&&!Array.isArray(e),te=e=>e instanceof Error?e.message:String(e),b=()=>s.env.HOME||l(),ne=()=>{let e=s.argv[1];return e&&c.isAbsolute(e)?e:`happenin`},x=e=>{t(e,{recursive:!0})},S=t=>e(t),C=e=>{let t=n(e,`utf8`);return JSON.parse(t)},w=(e,t)=>{let n=`${e}.${Date.now()}.tmp`;x(c.dirname(e)),o(n,JSON.stringify(t,null,2)+`
3
- `,`utf8`),i(n,e)},T=(e,t)=>{if(x(t),!S(e))return;let r=c.join(t,`${c.basename(e)}.${Date.now()}.bak`);return o(r,n(e,`utf8`),`utf8`),r},re=e=>{if(!S(e))return{version:1,hooks:{}};let t=C(e);if(!y(t))throw Error(`Invalid JSON object in ${e}`);let n=y(t.hooks)?t.hooks:{};return{...t,version:typeof t.version==`number`?t.version:1,hooks:n}},ie=e=>{if(!S(e))return{hooks:{}};let t=C(e);if(!y(t))throw Error(`Invalid JSON object in ${e}`);let n=y(t.hooks)?t.hooks:{};return{...t,hooks:n}},ae=(e,t,n)=>{e[t]=[...Array.isArray(e[t])?e[t]:[],{command:`${n} record cursor`}]},oe=(e,t,n)=>{e[t]=[...Array.isArray(e[t])?e[t]:[],{matcher:``,hooks:[{type:`command`,command:`${n} record claude ${t}`}]}]},se=e=>{let t=b(),n=c.join(t,`.cursor`,`hooks.json`),r=c.join(t,`.happenin`,`backups`,`cursor`),i=T(n,r),a=re(n);for(let t of p)ae(a.hooks,t,e);return w(n,a),{target:n,backup:i}},ce=e=>{let t=b(),n=c.join(t,`.claude`,`settings.json`),r=c.join(t,`.happenin`,`backups`,`claude`),i=T(n,r),a=ie(n);for(let t of m)oe(a.hooks,t,e);return w(n,a),{target:n,backup:i}},le=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 ue(e={}){let{cursor:t=!0,claude:n=!0}=e,r=ne(),i=[];t&&i.push(se(r)),n&&i.push(ce(r));for(let{target:e,backup:t}of i)s.stdout.write(`${e} written
4
- `),t&&s.stdout.write(` backup: ${t}\n`)}async function de(e=[]){try{let{cursor:t,claude:n}=le(e);await ue({cursor:t,claude:n})}catch(e){s.stderr.write(`install failed: ${te(e)}\n`),s.exitCode=1}}const fe=[`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);`];function E(e){let t=e.prepare(`PRAGMA user_version`).get();return Number(t?.user_version??0)}function pe(e){let t=E(e);if(!(t>=1)){if(e.exec(`BEGIN IMMEDIATE;`),t=E(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 me=e=>{e.exec(`PRAGMA busy_timeout = 30000;`);let t=E(e);if(t<1)throw Error(`Database schema must be migrated before backfilling subagent metadata`);if(e.exec(`BEGIN IMMEDIATE;`),t=E(e),t<1){e.exec(`ROLLBACK;`);return}try{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`]),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(`
5
+ UPDATE events
6
+ SET happened_at = strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch')
7
+ WHERE happened_at IS NULL;
8
+ `),e.exec(`
9
+ UPDATE events
10
+ SET happened_at = strftime('%Y-%m-%dT%H:%M:%fZ', happened_at / 1000.0, 'unixepoch')
11
+ WHERE happened_at NOT GLOB '*[^0-9]*' AND length(happened_at) > 0;
12
+ `),e.exec(`
13
+ UPDATE events
14
+ SET project_path = COALESCE(
15
+ NULLIF(trim(json_extract(payload, '$.projectPath')), ''),
16
+ NULLIF(trim(json_extract(payload, '$.cwd')), ''),
17
+ NULLIF(trim(json_extract(payload, '$.project_path')), ''),
18
+ (SELECT trim(value) FROM json_each(payload, '$.workspaceRoot') WHERE typeof(value) = 'text' AND trim(value) <> '' LIMIT 1),
19
+ (SELECT trim(value) FROM json_each(payload, '$.workspaceRoots') WHERE typeof(value) = 'text' AND trim(value) <> '' LIMIT 1),
20
+ (SELECT trim(value) FROM json_each(payload, '$.workspace_roots') WHERE typeof(value) = 'text' AND trim(value) <> '' LIMIT 1),
21
+ (SELECT trim(value) FROM json_each(payload, '$.workspace_root') WHERE typeof(value) = 'text' AND trim(value) <> '' LIMIT 1),
22
+ NULLIF(trim(json_extract(payload, '$.workspace_path')), '')
23
+ )
24
+ WHERE
25
+ project_path IS NULL
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(`
5
28
  UPDATE events
6
29
  SET
7
30
  subagent_id = NULLIF(trim(json_extract(payload, '$.subagent_id')), ''),
@@ -21,7 +44,27 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
21
44
  OR subagent_type IS NULL
22
45
  OR transcript_path IS NULL
23
46
  );
24
- `),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}},D=()=>s.env.HAPPENIN_DB?s.env.HAPPENIN_DB:c.join(l(),`.happenin`,`happenin.db`),O=(e,n=5e3)=>{let r=e??D();t(c.dirname(r),{recursive:!0});let i=new u(r);try{i.exec(`PRAGMA busy_timeout = ${n};`),i.exec(`PRAGMA journal_mode = WAL;`),i.exec(`CREATE TABLE IF NOT EXISTS events (
47
+ `),e.exec(`
48
+ UPDATE events
49
+ SET subagent_id = (
50
+ SELECT s.subagent_id
51
+ FROM events s
52
+ WHERE s.event = 'subagentStart'
53
+ AND s.subagent_id IS NOT NULL
54
+ AND json_extract(events.payload, '$.tool_use_id') = s.subagent_id
55
+ LIMIT 1
56
+ )
57
+ WHERE
58
+ event != 'subagentStart'
59
+ AND subagent_id IS NULL
60
+ AND json_valid(payload) = 1
61
+ AND json_extract(payload, '$.tool_use_id') IS NOT NULL
62
+ AND EXISTS (
63
+ SELECT 1 FROM events s
64
+ WHERE s.event = 'subagentStart'
65
+ AND s.subagent_id = json_extract(events.payload, '$.tool_use_id')
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 (
25
68
  id INTEGER PRIMARY KEY AUTOINCREMENT,
26
69
  source TEXT NOT NULL,
27
70
  client TEXT,
@@ -43,7 +86,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
43
86
  mtime INTEGER NOT NULL,
44
87
  imported_at INTEGER NOT NULL
45
88
  );
46
- `),pe(i);for(let e of fe)i.exec(e);return i}catch(e){try{i.close()}catch{}throw e}},k=(e,t)=>{e.prepare(`
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(`
47
90
  INSERT INTO events (
48
91
  source,
49
92
  client,
@@ -60,7 +103,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
60
103
  subagent_type,
61
104
  transcript_path
62
105
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
63
- `).run(t.source,t.client??null,t.event??null,t.sessionId??null,t.happenedAt??null,Date.now(),t.projectPath??null,t.filePath??null,t.toolName??null,t.payload,t.sourcePath??null,t.subagentId??null,t.subagentType??null,t.transcriptPath??null)};function A(e){let t=[],n=[];return e.since!==void 0&&(t.push(`id > ?`),n.push(e.since)),e.source!==void 0&&(t.push(`source = ?`),n.push(e.source)),e.event!==void 0&&(t.push(`event = ?`),n.push(e.event)),e.sessionId!==void 0&&e.sessionId!==``&&(t.push(`session_id LIKE ?`),n.push(`%${e.sessionId}%`)),e.q!==void 0&&e.q!==``&&(t.push(`payload LIKE ?`),n.push(`%${e.q}%`)),{clause:t.length>0?`WHERE ${t.join(` AND `)}`:``,params:n}}const j=(e,t)=>{let{clause:n,params:r}=A(t),i=t.limit??100,a=t.offset??0,o=`
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=`
64
107
  SELECT
65
108
  id,
66
109
  source,
@@ -78,23 +121,138 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
78
121
  subagent_type AS subagentType,
79
122
  transcript_path AS transcriptPath
80
123
  FROM events
81
- ${n} ORDER BY id DESC LIMIT ?${a>0?` OFFSET ?`:``}`;return r.push(i),a>0&&r.push(a),e.prepare(o).all(...r)},M=(e,t)=>{let{clause:n,params:r}=A(t),i=`SELECT COUNT(*) AS count FROM events ${n}`,a=e.prepare(i).get(...r);return a?Number(a.count):0},he=(e,t)=>{let{clause:n,params:r}=A(t);return{total:M(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)}},N=e=>{let t=e.prepare(`SELECT id FROM events ORDER BY id DESC LIMIT 1`).get();return t?Number(t.id):0},ge=e=>({sources:[`claude`,`cursor`],events:e.prepare(`SELECT DISTINCT event FROM events WHERE event IS NOT NULL AND event <> '' ORDER BY event`).all().map(e=>e.event)}),P=(e,t,n)=>{e.prepare(`
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=`
125
+ SELECT
126
+ session_id AS sessionId,
127
+ COUNT(*) AS eventCount,
128
+ MIN(received_at) AS firstReceivedAt,
129
+ MAX(received_at) AS lastReceivedAt,
130
+ MIN(happened_at) AS firstAt,
131
+ MAX(happened_at) AS lastAt,
132
+ COALESCE(json_group_array(DISTINCT project_path ORDER BY project_path) FILTER (WHERE project_path IS NOT NULL AND project_path <> ''), '[]') AS projectPaths,
133
+ COALESCE(json_group_array(DISTINCT tool_name ORDER BY tool_name) FILTER (WHERE tool_name IS NOT NULL AND tool_name <> ''), '[]') AS toolNames,
134
+ SUM(CASE WHEN event LIKE '%Failure%' THEN 1 ELSE 0 END) AS failureCount
135
+ FROM events
136
+ ${r}
137
+ GROUP BY session_id
138
+ ORDER BY lastReceivedAt DESC, sessionId
139
+ 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(`
82
141
  INSERT INTO imports (path, mtime, imported_at)
83
142
  VALUES (?, ?, ?)
84
143
  ON CONFLICT(path) DO UPDATE SET
85
144
  mtime = excluded.mtime,
86
145
  imported_at = excluded.imported_at
87
- `).run(t,n,Date.now())},_e=(e,t)=>{let n=e.prepare(`SELECT mtime FROM imports WHERE path = ?`).get(t);return n?Number(n.mtime):void 0};function F(e){if(e==null||typeof e==`object`)return;let t=String(e).trim();return t.length>0?t:void 0}function ve(e){if(typeof e==`number`)return new Date(e).toISOString();if(typeof e==`string`&&e.length>0)return/^\d+$/.test(e)?new Date(Number(e)).toISOString():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 be(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`?F(r.hook_event_name):e[1];let o=i===`cursor`&&a===`subagentStart`,s=o?F(r.parent_conversation_id)??F(r.conversation_id)??F(r.sessionId)??F(r.session_id):F(r.sessionId)??F(r.session_id),c=ve(r.happenedAt??r.timestamp??r.happened_at??r.time),l=F(r.projectPath??r.cwd??r.project_path??r.workspaceRoot??r.workspace_path),u=F(r.filePath??r.file_path??r.path),d=F(r.toolName??r.tool_name??r.tool),f=F(r.client)??(i===`cursor`?`cursor`:`claude_code`),p=o?F(r.subagent_id):void 0,m=o?F(r.subagent_type):void 0,h=o?F(r.transcript_path):void 0,g={source:i,client:f,event:a,sessionId:s,happenedAt:c,projectPath:l,filePath:u,toolName:d,payload:t,subagentId:p,subagentType:m,transcriptPath:h},_=ee(i,a??``),v;try{v=O(n,500),k(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 xe(e){let t=[];for await(let e of s.stdin)t.push(Buffer.isBuffer(e)?e:Buffer.from(e,`utf8`));let n=be(e,Buffer.concat(t).toString(`utf8`).trim());n!==void 0&&s.stdout.write(`${n}\n`)}const I=`claude-transcript`,L=`cursor-transcript`,R=`cursor`,z=e=>{try{return r(e,{withFileTypes:!0})}catch{return[]}},Se=e=>{try{return n(e,`utf8`)}catch{return}},Ce=e=>{try{return Math.floor(a(e).mtimeMs)}catch{return}},B=(e,t)=>{let n=Ce(t);if(n===void 0||_e(e,t)===n)return;let r=Se(t);if(r!==void 0)return{content:r,mtime:n}};function V(e){let t=[];for(let n of z(e)){if(n.isSymbolicLink())continue;let r=c.join(e,n.name);n.isDirectory()?t.push(...V(r)):n.isFile()&&n.name.endsWith(`.jsonl`)&&n.name!==`store.db`&&t.push(r)}return t}function we(e){let t=[];for(let n of z(e)){if(!n.isDirectory()||n.isSymbolicLink())continue;let r=c.join(e,n.name);for(let e of z(r))e.isDirectory()&&!e.isSymbolicLink()&&t.push(c.join(r,e.name))}return t}async function Te(e,t){let n=B(e,t);if(n===void 0)return;let{content:r,mtime:i}=n,a=c.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(I,t);for(let{obj:n,line:r}of o)k(e,{source:I,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});P(e,t,i)}async function Ee(e){let t=c.resolve(l(),`.claude/projects`);for(let n of V(t))await Te(e,n)}async function De(e,t,n){let r=B(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(L,t);for(let r of o)typeof r==`string`&&k(e,{source:L,client:R,event:`prompt`,sessionId:n,payload:JSON.stringify({prompt:r}),sourcePath:t});P(e,t,a)}async function Oe(e,t,n){let r=B(e,t);if(r===void 0)return;let{content:i,mtime:a}=r;e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(L,t),k(e,{source:L,client:R,event:`session_meta`,sessionId:n,payload:i,sourcePath:t}),P(e,t,a)}async function ke(e,t){let n=c.basename(t);await De(e,c.join(t,`prompt_history.json`),n),await Oe(e,c.join(t,`meta.json`),n)}async function Ae(e){let t=c.resolve(l(),`.cursor/chats`);for(let n of we(t))await ke(e,n)}async function H(e){let t=O(D());try{me(t),await je(t)}finally{t.close()}}async function je(e){await Ee(e),await Ae(e)}function Me(e){if(e)try{return JSON.parse(e)}catch{return}}function U(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:Me(e.payload??``)??e.payload}}let W;const G=new Set;function K(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`)}function q(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}function J(e,t=30){return e.length<=t?e:`${e.slice(0,t-1)}…`}function Ne(e,t){let n=t?.includeSession??!0,r=U(e),i=q(JSON.stringify({event:r})),a=e.happenedAt?String(e.happenedAt):new Date(Number(e.receivedAt??0)).toISOString(),o=[];return n&&e.sessionId&&o.push(`session:${K(String(e.sessionId))}`),e.toolName&&o.push(`tool:${K(String(e.toolName))}`),e.filePath&&o.push(`file:${K(String(e.filePath))}`),e.subagentType&&o.push(`subagent:${K(J(e.subagentType))}`),e.transcriptPath&&o.push(`transcript:${K(J(c.basename(e.transcriptPath)))}`),`<div class="event-row" x-data="${i}" @click="detail = event"><span class="id">#${K(String(e.id))}</span> <span class="source">${K(String(e.source??``))}</span> <span class="event">${K(String(e.event??``))}</span> <span class="meta">${o.join(` `)}</span> <span class="when">${K(a)}</span></div>`}function Pe(e){let t=[],n=new Map;for(let r of e){let e=r.sessionId??void 0,i=n.get(e);i?i.push(r):(t.push(e),n.set(e,[r]))}return t.map(e=>({sessionId:e,rows:n.get(e)}))}function Fe(e,t){let n=e?K(String(e)):`no session`,r=t.length;return`<details class="session-group" data-session="${q(String(e??``))}" open><summary class="session-header"><span class="session-title">${n}</span><span class="session-count">${r}</span></summary><div class="session-events">${t.map(e=>Ne(e,{includeSession:!1})).join(``)}</div></details>`}function Y(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(`page`),s;if(t){let e=parseInt(t,10);Number.isNaN(e)||(s=e)}let c=1;if(o){let e=parseInt(o,10);!Number.isNaN(e)&&e>0&&(c=e)}let l=(c-1)*50;return{since:s,source:n,event:r,sessionId:i,q:a,limit:50,offset:l,page:c}}function Ie(e,t){let n=new URLSearchParams;return e.since&&n.set(`since`,String(e.since)),e.source&&n.set(`source`,e.source),e.event&&n.set(`event`,e.event),e.sessionId&&n.set(`session`,e.sessionId),e.q&&n.set(`q`,e.q),t!==void 0&&t>1&&n.set(`page`,String(t)),n}function X(e,t){let n=Ie(e,t);return n.size>0?`/fragments/events?${n.toString()}`:`/fragments/events`}function Le(e,t,n){let{page:r,offset:i}=e,a=r>1,o=t>i+n.length,s=a?X(e,r-1):``,c=o?X(e,r+1):``,l=a?`hx-get="${q(s)}" `:``,u=o?`hx-get="${q(c)}" `:``;return`<div id="feed-pager" hx-swap-oob="true" hx-target="#feed-items" hx-swap="innerHTML"><div class="pager"><button type="button" ${l}${a?``:`disabled `}>Prev</button><span class="page-number">${r}</span><button type="button" ${u}${o?``:`disabled `}>Next</button></div></div>`}function Re(e){e.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),e.end(Ue(ge(W)))}function ze(e,t,n){let r=Y(n),i=j(W,r),a=M(W,r),o=Pe(i),s=o.length?o.map(e=>Fe(e.sessionId,e.rows)).join(``):`<p class="empty">No events found.</p>`,c=Le(r,a,i);t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(`${s}${c}`)}function Be(e,t,n){let r=Y(n),i=j(W,r);t.writeHead(200,{"Content-Type":`application/json`}),t.end(JSON.stringify(i))}function Ve(e,t){t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.setTimeout(0),t.write(`retry: 500
88
-
89
- `);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)?N(W)??0:i,timer:null};a.timer=setInterval(()=>{try{let e=N(W);e>a.lastId&&(t.write(`event: message
90
- data: ping
91
-
92
- `),a.lastId=e)}catch(e){console.error(`SSE poll error:`,e),clearInterval(a.timer),G.delete(a)}},300),G.add(a),e.on(`close`,()=>{clearInterval(a.timer),G.delete(a)})}function He(e,t){let n=new URL(e.url??`/`,`http://localhost`);try{if(e.method===`GET`){if(n.pathname===`/`){Re(t);return}if(n.pathname===`/fragments/events`){ze(e,t,n);return}if(n.pathname===`/events`){Be(e,t,n);return}if(n.pathname===`/events/stream`){Ve(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.writeHead(500,{"Content-Type":`text/plain`}),t.end(`Internal server error`))}}function Z(e){return`<option value="" selected>all</option>${e.map(e=>`<option value="${q(e)}">${K(e)}</option>`).join(``)}`}function Ue(e={sources:[],events:[]}){return`<!DOCTYPE html>
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">
147
+ <div class="metric-card metric-sessions">
148
+ <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
+ <div class="metric-label">Total Sessions</div>
150
+ <div class="metric-value">${e.totalSessions.toLocaleString()}</div>
151
+ </div>
152
+ <div class="metric-card metric-events">
153
+ <div class="metric-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></div>
154
+ <div class="metric-label">Total Events</div>
155
+ <div class="metric-value">${e.totalEvents.toLocaleString()}</div>
156
+ </div>
157
+ <div class="metric-card metric-duration">
158
+ <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
+ <div class="metric-label">Average Duration</div>
160
+ <div class="metric-value">${W(e.averageDurationMs)}</div>
161
+ </div>
162
+ <div class="metric-card metric-success">
163
+ <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
+ <div class="metric-label">Success Rate</div>
165
+ <div class="metric-value">${t}</div>
166
+ </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">
178
+ <div><span>Time</span><span>${t}</span></div>
179
+ <div><span>Source</span><span>${n}</span></div>
180
+ <div><span>Client</span><span>${r||`-`}</span></div>
181
+ <div><span>Event</span><span>${i}</span></div>
182
+ <div><span>Tool</span><span>${a||`-`}</span></div>
183
+ <div><span>Project</span><span>${c||`-`}</span></div>
184
+ ${u}
185
+ ${d}
186
+ </div>`}function ot(e){return`<div class="detail-json">
187
+ <details class="json-toggle">
188
+ <summary>JSON</summary>
189
+ <pre>${V(JSON.stringify(rt(e),null,2))}</pre>
190
+ </details>
191
+ <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)}
196
+ </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>
199
+ <div class="detail-body">
200
+ ${at(n)}
201
+ ${ot(n)}
202
+ <div class="detail-children">${r}</div>
203
+ </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)}">
205
+ <div class="detail-header">
206
+ <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>
209
+ </div>
210
+ ${a}
211
+ <div class="detail-toolbar">
212
+ <input type="search" class="detail-search" placeholder="Search attributes..." oninput="filterSessionDetails(this.value)">
213
+ <button type="button" onclick="toggleSessionDetails(false)">Collapse All</button>
214
+ <button type="button" onclick="toggleSessionDetails(true)">Expand All</button>
215
+ </div>
216
+ <div class="detail-events">${r}</div>
217
+ <div class="detail-actions">
218
+ <button type="button" onclick="copySessionJson()">Copy JSON</button>
219
+ </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">
222
+ <div class="session-list-wrapper">
223
+ ${tt(e.slice(a,a+i),n,r,t)}
224
+ ${mt(t,e.length)}
225
+ </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">
233
+ <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
+ <h1 class="page-title">Session Overview Analytics</h1>
235
+ <div class="header-controls">
236
+ <div class="search-wrap">
237
+ <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
+ <input type="text" name="q" class="search" placeholder="session or payload" value="${t}" autocomplete="off">
239
+ </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>
241
+ <button type="button" class="theme" data-theme-toggle title="Toggle light/dark theme" aria-label="toggle theme">🌙</button>
242
+ </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>
245
+ <label>source ${Y(`source`,e.sources,r)}</label>
246
+ <label>tool ${Y(`tool`,e.tools,i)}</label>
247
+ <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>
93
251
  <html lang="en" hx-ext="sse">
94
252
  <head>
95
253
  <meta charset="utf-8">
96
254
  <meta name="viewport" content="width=device-width, initial-scale=1">
97
- <title>happenin</title>
255
+ <title>Session Overview Analytics - happenin</title>
98
256
  <script>
99
257
  (function () {
100
258
  const THEME_KEY = 'happenin-theme';
@@ -115,6 +273,12 @@ data: ping
115
273
  } catch {}
116
274
  }
117
275
 
276
+ function removeItem(key) {
277
+ try {
278
+ localStorage.removeItem(key);
279
+ } catch {}
280
+ }
281
+
118
282
  function getSystemTheme() {
119
283
  return DARK_MQL.matches ? 'dark' : 'light';
120
284
  }
@@ -134,7 +298,13 @@ data: ping
134
298
 
135
299
  function updateThemeButton(theme) {
136
300
  const button = document.querySelector('[data-theme-toggle]');
137
- if (button) button.textContent = theme === 'dark' ? '🌙' : '☀️';
301
+ if (!button) return;
302
+ button.textContent = theme === 'dark' ? '☀️' : '🌙';
303
+ button.title = getSavedTheme()
304
+ ? theme === 'dark'
305
+ ? 'Switch to light theme'
306
+ : 'Switch to dark theme'
307
+ : 'Following system theme — click to switch';
138
308
  }
139
309
 
140
310
  function initTheme() {
@@ -155,7 +325,11 @@ data: ping
155
325
  button.addEventListener('click', () => {
156
326
  const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
157
327
  const next = current === 'dark' ? 'light' : 'dark';
158
- setItem(THEME_KEY, next);
328
+ if (next === getSystemTheme()) {
329
+ removeItem(THEME_KEY);
330
+ } else {
331
+ setItem(THEME_KEY, next);
332
+ }
159
333
  applyTheme(next);
160
334
  updateThemeButton(next);
161
335
  });
@@ -164,37 +338,32 @@ data: ping
164
338
 
165
339
  function loadFilters() {
166
340
  const raw = getItem(FILTER_KEY);
167
- if (!raw) return;
341
+ if (!raw) return false;
168
342
  try {
169
343
  const values = JSON.parse(raw);
170
344
  const form = document.querySelector('[data-filter-form]');
171
- if (!form) return;
345
+ if (!form) return false;
172
346
  for (const [name, value] of Object.entries(values)) {
173
347
  const field = form.elements[name];
174
348
  if (field && value != null && value !== '') {
175
349
  field.value = String(value);
176
350
  }
177
351
  }
352
+ return true;
178
353
  } catch {}
354
+ return false;
179
355
  }
180
356
 
181
- function getCurrentPage() {
182
- const el = document.querySelector('#feed-pager .page-number');
183
- const n = el ? Number(el.textContent) : 1;
184
- return Number.isNaN(n) || n < 1 ? 1 : n;
185
- }
186
-
187
- function updateFeedUrl() {
357
+ function updateContentUrl() {
188
358
  const form = document.querySelector('[data-filter-form]');
189
- const feed = document.getElementById('feed-items');
190
- if (!form || !feed) return;
359
+ const content = document.getElementById('dashboard-content');
360
+ if (!form || !content) return;
191
361
  const data = new FormData(form);
192
362
  const params = new URLSearchParams();
193
363
  for (const [key, value] of data.entries()) {
194
364
  if (value) params.set(key, String(value));
195
365
  }
196
- params.set('page', String(getCurrentPage()));
197
- feed.setAttribute('hx-get', '/fragments/events' + (params.size > 0 ? '?' + params.toString() : ''));
366
+ content.setAttribute('hx-get', '/fragments/sessions' + (params.size > 0 ? '?' + params.toString() : ''));
198
367
  }
199
368
 
200
369
  function saveFilters(form) {
@@ -208,52 +377,92 @@ data: ping
208
377
 
209
378
  function initFilters() {
210
379
  const form = document.querySelector('[data-filter-form]');
211
- if (!form) return;
212
- loadFilters();
380
+ const content = document.getElementById('dashboard-content');
381
+ if (!form || !content) return;
382
+ const hadSaved = loadFilters();
213
383
  form.addEventListener('change', () => saveFilters(form));
214
384
  form.addEventListener('input', (event) => {
215
385
  if (event.target.classList.contains('search')) saveFilters(form);
216
386
  });
217
- updateFeedUrl();
387
+ updateContentUrl();
388
+ if (hadSaved) {
389
+ setTimeout(() => {
390
+ if (typeof htmx !== 'undefined') htmx.trigger(form, 'change');
391
+ }, 0);
392
+ }
218
393
  }
219
394
 
220
- const openSessions = new Map();
221
-
222
- function saveSessionState() {
223
- const feed = document.getElementById('feed-items');
224
- if (!feed) return;
225
- for (const details of feed.querySelectorAll('.session-group')) {
226
- openSessions.set(details.getAttribute('data-session') ?? '', details.hasAttribute('open'));
395
+ function filterSessionDetails(term) {
396
+ const container = document.querySelector('.detail-events');
397
+ if (!container) return;
398
+ const lower = term.toLowerCase();
399
+ for (const detail of container.querySelectorAll(':scope > .detail-event')) {
400
+ detail.style.display = !lower || detail.textContent.toLowerCase().includes(lower) ? '' : 'none';
227
401
  }
228
402
  }
229
403
 
230
- function restoreSessionState() {
231
- const feed = document.getElementById('feed-items');
232
- if (!feed) return;
233
- for (const details of feed.querySelectorAll('.session-group')) {
234
- const session = details.getAttribute('data-session') ?? '';
235
- if (openSessions.has(session) && !openSessions.get(session)) {
236
- details.removeAttribute('open');
237
- }
404
+ function toggleSessionDetails(open) {
405
+ const container = document.querySelector('.detail-events');
406
+ if (!container) return;
407
+ for (const detail of container.querySelectorAll('.detail-event')) {
408
+ detail.open = open;
238
409
  }
239
410
  }
240
411
 
241
- function initFeedItems() {
242
- const el = document.getElementById('feed-items');
243
- if (el && window.Alpine) Alpine.initTree(el);
412
+ function copySessionJson() {
413
+ const pre = document.getElementById('session-json');
414
+ if (!pre) return;
415
+ navigator.clipboard.writeText(pre.textContent).catch((e) => alert('Copy failed: ' + e.message));
244
416
  }
245
417
 
246
- document.addEventListener('htmx:beforeSwap', (event) => {
247
- if (event.detail.target.id === 'feed-items') saveSessionState();
248
- });
418
+ function copyEventJson(button) {
419
+ const pre = button.parentElement.querySelector('pre');
420
+ if (!pre) return;
421
+ navigator.clipboard.writeText(pre.textContent).catch((e) => alert('Copy failed: ' + e.message));
422
+ }
423
+
424
+ function backToDashboard() {
425
+ const form = document.querySelector('[data-filter-form]');
426
+ if (form && typeof htmx !== 'undefined') htmx.trigger(form, 'change');
427
+ }
428
+
429
+ function syncDetailState() {
430
+ const content = document.getElementById('dashboard-content');
431
+ if (!content) return;
432
+ const detail = content.querySelector('.session-detail-view');
433
+ 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);
437
+ }
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' });
443
+ }
444
+ }
249
445
 
250
- document.addEventListener('htmx:afterSettle', (event) => {
251
- if (event.detail.target.id === 'feed-items') restoreSessionState();
252
- updateFeedUrl();
253
- initFeedItems();
446
+ let sidebarScroll = 0;
447
+ document.addEventListener('htmx:beforeSwap', (e) => {
448
+ const list = document.querySelector('.session-list');
449
+ if (list) sidebarScroll = list.scrollTop;
450
+ const elt = e.detail.requestConfig ? e.detail.requestConfig.elt : null;
451
+ if (elt && elt.id === 'dashboard-content' && document.body.classList.contains('session-detail-open')) {
452
+ e.detail.shouldSwap = false;
453
+ }
254
454
  });
255
455
 
256
- document.addEventListener('alpine:initialized', initFeedItems);
456
+ window.filterSessionDetails = filterSessionDetails;
457
+ window.toggleSessionDetails = toggleSessionDetails;
458
+ window.copySessionJson = copySessionJson;
459
+ window.copyEventJson = copyEventJson;
460
+ window.backToDashboard = backToDashboard;
461
+
462
+ document.addEventListener('htmx:afterSettle', () => {
463
+ updateContentUrl();
464
+ syncDetailState();
465
+ });
257
466
 
258
467
  applyTheme(getTheme());
259
468
 
@@ -283,6 +492,9 @@ data: ping
283
492
  --text-4: #6b7280;
284
493
  --accent: #60a5fa;
285
494
  --accent-2: #c084fc;
495
+ --success: #22c55e;
496
+ --warning: #eab308;
497
+ --danger: #ef4444;
286
498
  }
287
499
  [data-theme="light"] {
288
500
  color-scheme: light;
@@ -300,92 +512,195 @@ data: ping
300
512
  --text-4: #9ca3af;
301
513
  --accent: #2563eb;
302
514
  --accent-2: #7c3aed;
515
+ --success: #16a34a;
516
+ --warning: #ca8a04;
517
+ --danger: #dc2626;
303
518
  }
304
519
  * { box-sizing: border-box; }
305
- body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 1rem; line-height: 1.4; }
306
- .app-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
307
- h1 { margin: 0; font-size: 1.25rem; }
308
- .theme { display: inline-flex; align-items: center; justify-content: center; font-size: 1.1rem; padding: 0.35rem 0.5rem; }
309
- .layout { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; gap: 1rem; }
310
- form { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; align-items: end; }
311
- label { display: flex; flex-direction: column; font-size: 0.75rem; gap: 0.2rem; color: var(--text-2); }
312
- input, select, button { background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: 0.35rem; padding: 0.4rem 0.6rem; font-size: 0.85rem; }
313
- button { cursor: pointer; background: var(--surface-4); }
520
+ html, body { height: 100%; }
521
+ 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; }
523
+ .sticky-bar { flex: 0 0 auto; display: flex; flex-direction: column; gap: 1rem; background: var(--bg); position: sticky; top: 0; z-index: 10; }
524
+ .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
+ .brand { font-weight: 800; font-size: 1.25rem; display: flex; align-items: center; gap: 0.5rem; color: var(--text); }
526
+ .brand-icon { color: var(--accent); }
527
+ .page-title { font-size: 1.1rem; font-weight: 600; margin: 0; color: var(--text); }
528
+ .header-controls { display: flex; gap: 0.75rem; align-items: end; }
529
+ .search-wrap { position: relative; display: flex; align-items: center; }
530
+ .search-icon { position: absolute; left: 0.6rem; color: var(--text-4); pointer-events: none; }
531
+ input.search { padding-left: 2rem; min-width: 14rem; }
532
+ .filter-bar { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: end; padding: 0.75rem 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; }
533
+ form { margin: 0; display: flex; flex-direction: column; gap: 1rem; }
534
+ label { display: flex; flex-direction: column; font-size: 0.75rem; gap: 0.25rem; color: var(--text-2); }
535
+ input, select, button { background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: 0.4rem; padding: 0.45rem 0.7rem; font-size: 0.85rem; transition: border-color 0.15s, background 0.15s; }
536
+ input:focus, select:focus, button:focus { outline: none; border-color: var(--accent); }
537
+ select { min-width: 7.5rem; cursor: pointer; }
538
+ button { cursor: pointer; background: var(--surface-4); font-weight: 500; }
314
539
  button:hover { background: var(--surface-5); }
315
- #feed { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; min-height: 4rem; overflow: hidden; }
316
- #feed-items { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.5rem; }
317
- .event-row { display: flex; gap: 0.5rem; align-items: baseline; flex-wrap: wrap; padding: 0.5rem 0.6rem; border-radius: 0.35rem; background: var(--surface-3); border: 1px solid var(--border-2); cursor: pointer; }
318
- .event-row:hover { background: var(--surface-4); }
319
- .event-row .id { color: var(--text-4); font-size: 0.7rem; min-width: 2.5rem; }
320
- .event-row .source { color: var(--accent); font-weight: 600; }
321
- .event-row .event { color: var(--accent-2); }
322
- .event-row .meta { color: var(--text-3); font-size: 0.75rem; }
323
- .event-row .when { color: var(--text-4); font-size: 0.7rem; margin-left: auto; }
324
- .session-group { margin-bottom: 0.75rem; border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; }
325
- .session-group:last-child { margin-bottom: 0; }
326
- .session-header { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.4rem 0.6rem; background: var(--surface-4); border-bottom: 1px solid var(--border); font-size: 0.85rem; }
327
- .session-header .session-title { font-weight: 600; color: var(--accent); word-break: break-all; }
328
- .session-header .session-count { color: var(--text-4); font-size: 0.75rem; }
329
- .session-events { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.5rem; background: var(--surface); }
330
- .session-events .event-row { background: var(--surface-3); border: 1px solid var(--border-2); }
331
- details.session-group > summary { list-style: none; cursor: pointer; }
332
- details.session-group > summary::-webkit-details-marker { display: none; }
333
- details.session-group > summary::before { content: "+"; color: var(--text-4); margin-right: 0.5rem; font-size: 0.85rem; }
334
- details[open].session-group > summary::before { content: "−"; }
335
- .detail { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.75rem; position: sticky; top: 1rem; height: fit-content; }
336
- .detail pre { white-space: pre-wrap; overflow-x: auto; background: var(--bg); padding: 0.75rem; border-radius: 0.35rem; border: 1px solid var(--border); font-size: 0.8rem; line-height: 1.3; }
337
- .detail .actions { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; }
338
- .empty { color: var(--text-4); padding: 1rem; text-align: center; }
339
- #feed-pager { padding: 0.5rem; border-top: 1px solid var(--border); }
340
- .pager { display: flex; justify-content: center; align-items: center; gap: 0.75rem; }
341
- .pager button { min-width: 4rem; }
342
- .pager button:disabled { opacity: 0.4; cursor: not-allowed; }
343
- .pager .page-number { color: var(--text-3); font-size: 0.85rem; }
344
- @media (max-width: 720px) { .layout { grid-template-columns: 1fr; } .detail { position: static; } }
540
+ .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; }
546
+ .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
+ .status-active { background: rgba(34,197,94,0.15); color: var(--success); }
548
+ .status-completed { background: rgba(96,165,250,0.15); color: var(--accent); }
549
+ .status-failed { background: rgba(239,68,68,0.15); color: var(--danger); }
550
+ .session-item { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.7rem 1rem; border-bottom: 1px solid var(--border-2); cursor: pointer; transition: background 0.12s; }
551
+ .session-item:hover { background: var(--surface-3); }
552
+ .session-item.active { background: var(--surface-3); box-shadow: inset 3px 0 0 var(--accent); }
553
+ .session-item.active .session-id { color: var(--accent); }
554
+ .session-item-static { cursor: default; }
555
+ .session-item-static:hover { background: none; }
556
+ .session-item:last-child { border-bottom: none; }
557
+ .session-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; }
558
+ .session-row { display: flex; align-items: center; gap: 0.5rem; justify-content: space-between; }
559
+ .session-id { font-weight: 600; font-size: 0.85rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
560
+ .session-meta { display: flex; gap: 0.75rem; font-size: 0.75rem; color: var(--text-3); }
561
+ .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
+ .session-chevron:hover { color: var(--accent); background: none; }
563
+ .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; }
566
+ .main-metrics .metric-grid { grid-template-columns: repeat(4, 1fr); gap: 1rem; }
567
+ .main-metrics .metric-card { min-height: 5rem; }
568
+ .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; }
571
+ .chart-title { font-size: 0.9rem; font-weight: 600; color: var(--text-2); margin: 0 0 0.75rem; flex: 0 0 auto; }
572
+ .chart-empty { color: var(--text-4); text-align: center; padding: 4rem 0; }
573
+ .chart-max { text-align: right; color: var(--text-4); font-size: 0.7rem; margin-top: 0.25rem; flex: 0 0 auto; }
574
+ .event-chart { width: 100%; height: 100%; min-height: 0; flex: 1 1 auto; display: block; }
575
+ .event-chart polyline { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
576
+ .event-chart .area { fill: url(#areaGradient); stroke: none; }
577
+ .event-chart .axis { stroke: var(--border-2); stroke-width: 1; }
578
+ .event-chart text { fill: var(--text-3); font-size: 10px; }
579
+ .event-chart .y-label { font-size: 9px; fill: var(--text-4); }
580
+ .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; }
582
+ .tool-row { display: grid; grid-template-columns: 8rem 1fr 2.5rem; align-items: center; gap: 0.75rem; font-size: 0.8rem; }
583
+ .tool-name { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
584
+ .tool-bar-bg { background: var(--surface-4); border-radius: 0.4rem; height: 0.85rem; overflow: hidden; }
585
+ .tool-bar { background: linear-gradient(90deg, var(--accent), var(--accent-2)); height: 100%; border-radius: 0.4rem; }
586
+ .tool-count { color: var(--text-3); text-align: right; font-weight: 600; }
587
+ .detail-area { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; }
588
+ .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; }
590
+ .detail-header { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; flex: 0 0 auto; }
591
+ .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
+ .detail-title { font-size: 1rem; font-weight: 600; color: var(--text); word-break: break-all; flex: 1 1 auto; min-width: 0; margin: 0; }
593
+ .detail-count { font-size: 0.75rem; color: var(--text-3); white-space: nowrap; flex: 0 0 auto; }
594
+ .detail-truncated { flex: 0 0 auto; font-size: 0.75rem; color: var(--warning); background: color-mix(in srgb, var(--warning) 12%, transparent); border: 1px solid color-mix(in srgb, var(--warning) 30%, transparent); border-radius: 0.4rem; padding: 0.4rem 0.6rem; }
595
+ .detail-toolbar { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; flex: 0 0 auto; }
596
+ .detail-toolbar input[type="search"] { flex: 1; min-width: 14rem; }
597
+ .detail-toolbar button { font-size: 0.8rem; padding: 0.4rem 0.65rem; }
598
+ .detail-actions { display: flex; gap: 0.5rem; flex: 0 0 auto; }
599
+ .detail-events { --timeline-x: 0.35rem; --timeline-indent: 1.4rem; --dot-x: calc(var(--timeline-x) - var(--timeline-indent)); display: flex; flex-direction: column; gap: 0.1rem; padding-left: var(--timeline-indent); flex: 1 1 auto; min-height: 0; overflow-y: auto; }
600
+ .detail-event { position: relative; display: block; font-size: 0.82rem; min-width: 0; }
601
+ .detail-event::before { content: ""; position: absolute; left: calc(var(--dot-x) - 0.3rem); top: 0.78rem; width: 0.6rem; height: 0.6rem; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px var(--surface); z-index: 1; }
602
+ .detail-event::after { content: ""; position: absolute; left: calc(var(--dot-x) - 1px); top: 1.5rem; bottom: -0.1rem; width: 2px; background: var(--border); }
603
+ .detail-event:last-child::after { display: none; }
604
+ .detail-subagent::before { background: var(--accent-2); }
605
+ .detail-event > summary { display: flex; gap: 0.75rem; flex-wrap: wrap; padding: 0.45rem 0.6rem; cursor: pointer; list-style: none; align-items: center; border-radius: 0.4rem; transition: background 0.12s; }
606
+ .detail-event > summary:hover { background: var(--surface-2); }
607
+ .detail-event > summary::-webkit-details-marker { display: none; }
608
+ .detail-event > summary::before { content: "▸"; color: var(--text-4); transition: transform 0.15s; width: 0.8rem; flex: 0 0 auto; text-align: center; }
609
+ .detail-event[open] > summary::before { content: "▾"; }
610
+ .detail-event > summary .ts { color: var(--text-4); white-space: nowrap; flex: 0 0 auto; }
611
+ .detail-event > summary .source { color: var(--accent); font-weight: 600; }
612
+ .detail-event > summary .event { color: var(--accent-2); font-weight: 500; }
613
+ .detail-event > summary .tool { color: var(--text-3); }
614
+ .subagent-badge { font-size: 0.7rem; font-weight: 600; color: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 15%, transparent); border: 1px solid color-mix(in srgb, var(--accent-2) 35%, transparent); padding: 0.1rem 0.45rem; border-radius: 0.25rem; white-space: nowrap; }
615
+ .detail-body { padding: 0.1rem 0.6rem 0.5rem 1.4rem; min-width: 0; }
616
+ .detail-event .detail-children { position: relative; margin-top: 0.25rem; padding-left: var(--timeline-indent); display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
617
+ .detail-json { position: relative; margin-top: 0.4rem; }
618
+ .detail-json .json-toggle > summary { display: inline-flex; align-items: center; gap: 0.3rem; list-style: none; cursor: pointer; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; color: var(--text-3); padding: 0.25rem 0.5rem; border-radius: 0.3rem; transition: background 0.12s, color 0.12s; }
619
+ .detail-json .json-toggle > summary:hover { background: var(--surface-2); color: var(--text); }
620
+ .detail-json .json-toggle > summary::-webkit-details-marker { display: none; }
621
+ .detail-json .json-toggle > summary::before { content: "▸"; font-size: 0.6rem; }
622
+ .detail-json .json-toggle[open] > summary::before { content: "▾"; }
623
+ .json-copy { position: absolute; top: 0.2rem; right: 0.2rem; background: none; border: none; color: var(--text-3); padding: 0.3rem; display: inline-flex; align-items: center; justify-content: center; border-radius: 0.3rem; }
624
+ .json-copy:hover { color: var(--text); background: var(--surface-2); }
625
+ .detail-event pre { white-space: pre-wrap; word-break: break-word; overflow-x: auto; max-width: 100%; background: var(--bg); border: 1px solid var(--border-2); border-radius: 0.4rem; padding: 0.6rem; margin: 0.3rem 0 0; font-size: 0.75rem; line-height: 1.4; color: var(--text-2); }
626
+ .detail-attrs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.35rem 0.75rem; padding: 0.5rem 0.6rem; background: var(--surface-2); border-radius: 0.4rem; font-size: 0.78rem; }
627
+ .detail-attrs > div { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
628
+ .detail-attrs > div span:first-child { color: var(--text-4); font-size: 0.7rem; text-transform: capitalize; }
629
+ .detail-attrs > div span:last-child { color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; word-break: break-all; }
630
+ .session-json { display: none; }
631
+ .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
+ .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; }
633
+ .pager a:hover { background: var(--surface-5); }
634
+ .pager button:disabled { opacity: 0.45; cursor: not-allowed; }
635
+ .pager-info { color: var(--text-3); font-size: 0.85rem; }
636
+ .metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
637
+ .metric-card { position: relative; overflow: hidden; background: var(--surface-2); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.85rem 1rem; display: flex; flex-direction: column; justify-content: center; }
638
+ .metric-sessions .metric-icon { color: var(--accent); }
639
+ .metric-events .metric-icon { color: var(--accent-2); }
640
+ .metric-duration .metric-icon { color: var(--warning); }
641
+ .metric-success .metric-icon { color: var(--success); }
642
+ .metric-icon { position: absolute; top: 0.75rem; right: 0.75rem; opacity: 0.8; }
643
+ .metric-label { font-size: 0.72rem; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
644
+ .metric-value { font-size: 1.75rem; font-weight: 800; color: var(--text); margin-top: 0.25rem; letter-spacing: -0.02em; }
645
+ .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); } }
647
+ @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%; } }
345
648
  </style>
346
- <script src="https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js"><\/script>
347
- <script src="https://unpkg.com/htmx-ext-sse@2.2.4/dist/sse.js"><\/script>
348
- <script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"><\/script>
649
+ <script src="https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js" integrity="sha256-cepnGFv6jJjDnTFxfG/OXYUjcPzf0SnbRUN3TTFFwN4=" crossorigin="anonymous"><\/script>
650
+ <script src="https://unpkg.com/htmx-ext-sse@2.2.4/dist/sse.js" integrity="sha256-O1mSpUFhm6vvxMFpUFr0dN9cMDnaUeWblsz5JB7NYdI=" crossorigin="anonymous"><\/script>
349
651
  </head>
350
652
  <body>
351
- <div class="app-header">
352
- <h1>happenin</h1>
353
- <button type="button" class="theme" data-theme-toggle aria-label="toggle theme">🌙</button>
354
- </div>
355
- <form data-filter-form hx-get="/fragments/events" hx-target="#feed-items" hx-swap="innerHTML" hx-trigger="change, input changed delay:300ms from:.search">
356
- <label>source <select name="source">${Z(e.sources)}</select></label>
357
- <label>event <select name="event">${Z(e.events)}</select></label>
358
- <label>session <input type="text" name="session" class="search" placeholder="session"></label>
359
- <label>q <input type="text" name="q" class="search" placeholder="search"></label>
653
+ <div class="app">
654
+ <div class="sticky-bar">
655
+ <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)}
360
657
  </form>
361
- <div class="layout" x-data="{ detail: null }">
362
- <div id="feed" sse-connect="/events/stream">
363
- <div id="feed-items" hx-get="/fragments/events?page=1" hx-trigger="load, sse:message" hx-target="#feed-items" hx-swap="innerHTML"></div>
364
- <div id="feed-pager" hx-target="#feed-items" hx-swap="innerHTML"></div>
365
- </div>
366
- <aside class="detail" x-show="detail">
367
- <div class="actions">
368
- <button @click="navigator.clipboard.writeText(JSON.stringify(detail, null, 2))">Copy JSON</button>
369
- <button @click="detail = null">Close</button>
370
- </div>
371
- <pre x-text="JSON.stringify(detail, null, 2)"></pre>
372
- </aside>
658
+ </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">
660
+ ${r}
661
+ </div>
373
662
  </div>
374
663
  </body>
375
- </html>`}async function We(e,t){let n=d.createServer((e,t)=>{He(e,t)});await new Promise((r,i)=>{let a=e=>{function o(t){n.off(`error`,o),n.off(`listening`,c),t.code===`EADDRINUSE`&&e<65535?a(e+1):i(t)}function c(){n.off(`error`,o);let i=`http://localhost:${e}`;console.log(`Dashboard: ${i}`),t&&s.platform===`darwin`&&f(`open`,[i],e=>{e&&console.error(`open failed:`,e.message)}),r()}n.once(`error`,o),n.once(`listening`,c),n.listen(e)};a(e)})}async function Ge(e){let t=8765,n=!0,r=!1;for(let i of e){if(r){let e=parseInt(i,10);(Number.isNaN(e)||e<1||e>65535)&&(console.error(`Invalid --port`),s.exit(1)),t=e,r=!1;continue}if(i===`--port`){r=!0;continue}if(i.startsWith(`--port=`)){let e=parseInt(i.slice(7),10);(Number.isNaN(e)||e<1||e>65535)&&(console.error(`Invalid --port`),s.exit(1)),t=e}else(i===`--no-open`||i===`--silent`)&&(n=!1)}r&&(console.error(`Missing value for --port`),s.exit(1)),await H(),W=O(D()),await We(t,n)}const Q=[`json`,`jsonl`,`summary`];function $(e){let t=Number(e);return Number.isNaN(t)?void 0:t}function Ke(){s.stdout.write(`Usage: happenin query [options]
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
665
+
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
667
+ data: ping
668
+
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]
376
670
 
377
671
  Options:
378
672
  --source <source> filter by source (cursor, claude, ...)
379
673
  --event <event> filter by event name
380
674
  --session <id> filter by session id (partial match)
381
- --q <text> search event payloads
675
+ --q <text> search event payloads and session ids
382
676
  --since <id> events with id greater than <id>
677
+ --range <range> time range: 24h, 7d, 30d, all (default: 24h)
678
+ --tool <tool> filter by tool name
383
679
  --limit <n> maximum rows to return (default: 100)
384
680
  --offset <n> skip first <n> rows
385
681
  --format <format> output format: json (default), jsonl, summary
386
682
  --db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
387
683
  -h, --help show this help
388
- `)}function qe(e){let t={},n=`json`,r,i;for(let a of e){if(i){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=$(a);e!==void 0&&(t.since=e);break}case`limit`:{let e=$(a);e!==void 0&&(t.limit=e);break}case`offset`:{let e=$(a);e!==void 0&&(t.offset=e);break}case`format`:Q.includes(a)&&(n=a);break;case`db`:r=a;break}i=void 0;continue}if((a===`-h`||a===`--help`)&&(Ke(),s.exit(0)),a===`--source`||a===`--event`||a===`--session`||a===`--q`||a===`--since`||a===`--limit`||a===`--offset`||a===`--format`||a===`--db`){i=a.slice(2);continue}if(a.startsWith(`--`)){let e=a.indexOf(`=`);if(e!==-1){let i=a.slice(2,e),o=a.slice(e+1);switch(i){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=$(o);e!==void 0&&(t.since=e);break}case`limit`:{let e=$(o);e!==void 0&&(t.limit=e);break}case`offset`:{let e=$(o);e!==void 0&&(t.offset=e);break}case`format`:Q.includes(o)&&(n=o);break;case`db`:r=o;break}}}}return{options:t,format:n,dbPath:r}}async function Je(e){let{options:t,format:n,dbPath:r}=qe(e),i=O(r);try{if(n===`summary`){let e=he(i,t);s.stdout.write(`${JSON.stringify(e,null,2)}\n`);return}let e={total:M(i,t),rows:j(i,t).map(U)};if(n===`jsonl`)for(let t of e.rows)s.stdout.write(`${JSON.stringify(t)}\n`);else s.stdout.write(`${JSON.stringify(e,null,2)}\n`)}finally{i.close()}}const Ye=new URL(`../assets/help.md`,import.meta.url),Xe=new URL(`../package.json`,import.meta.url);function Ze(e){let t=!!s.stdout.isTTY&&!s.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 Qe(){let e;try{e=n(Ye,`utf8`)}catch{e=`# happenin
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]
685
+
686
+ Options:
687
+ --source <source> filter by source (cursor, claude, ...)
688
+ --event <event> filter by event name
689
+ --session <id> filter by session id (partial match)
690
+ --q <text> search event payloads and session ids
691
+ --since <id> events with id greater than <id>
692
+ --range <range> time range: 24h, 7d, 30d, all (default: 24h)
693
+ --status <status> filter by session status: active, completed, failed
694
+ --tool <tool> filter by tool name
695
+ --minDuration <m> minimum session duration in minutes
696
+ --maxDuration <m> maximum session duration in minutes
697
+ --limit <n> maximum sessions to return (default: 100)
698
+ --offset <n> skip first <n> sessions
699
+ --format <format> output format: json (default), jsonl, summary
700
+ --db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
701
+ -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
389
704
 
390
705
  Run \`happenin <command>\`.
391
- `}s.stdout.write(`\n${Ze(e)}\n`)}function $e(){let e=`0.0.0`;try{let t=n(Xe,`utf8`);e=JSON.parse(t).version??e}catch{}s.stdout.write(`${e}\n`)}async function et(){let[e,...t]=s.argv.slice(2);if(!e||e===`--help`||e===`-h`){Qe();return}if(e===`--version`||e===`-v`){$e();return}if(e===`install`){await de(t);return}if(e===`record`){await xe(t);return}if(e===`import`){await H(t);return}if(e===`dashboard`){await Ge(t);return}if(e===`query`){await Je(t);return}s.stderr.write(`Unknown command: ${e}\n`),Qe(),s.exitCode=1}et();export{};
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{};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happenin",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
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",
@@ -69,6 +69,6 @@
69
69
  "**/*.ts": "oxlint --fix"
70
70
  },
71
71
  "engines": {
72
- "node": ">=24.0.0"
72
+ "node": ">=22.13.0"
73
73
  }
74
74
  }