happenin 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +100 -62
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameSync as i,statSync as a,writeFileSync as o}from"node:fs";import s from"node:path";import c from"node:process";import{fileURLToPath as l}from"node:url";import{homedir as u}from"node:os";import{DatabaseSync as d}from"node:sqlite";import f from"node:http";import{execFile as p}from"node:child_process";const m=[`sessionStart`,`sessionEnd`,`beforeSubmitPrompt`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`afterAgentResponse`,`afterAgentThought`,`preCompact`,`stop`],h=`SessionStart.SessionEnd.Setup.UserPromptSubmit.UserPromptExpansion.PreToolUse.PermissionRequest.PermissionDenied.PostToolUse.PostToolUseFailure.PostToolBatch.Notification.MessageDisplay.SubagentStart.SubagentStop.TaskCreated.TaskCompleted.Stop.StopFailure.TeammateIdle.InstructionsLoaded.ConfigChange.CwdChanged.DirectoryAdded.FileChanged.WorktreeCreate.WorktreeRemove.PreCompact.PostCompact.Elicitation.ElicitationResult`.split(`.`),g=new Set([`beforeSubmitPrompt`]),_=new Set([`preToolUse`,`beforeShellExecution`,`beforeMCPExecution`,`beforeReadFile`,`subagentStart`]),ee=new Set([`UserPromptSubmit`,`UserPromptExpansion`]),te=new Set([`PreToolUse`,`PermissionRequest`,`SubagentStart`,`SubagentStop`,`TaskCreated`,`TaskCompleted`,`Stop`,`WorktreeCreate`,`WorktreeRemove`]),ne=(e,t)=>{if(e===`cursor`)return g.has(t)?JSON.stringify({continue:!0}):_.has(t)?JSON.stringify({permission:`allow`}):void 0;if(e===`claude`)return ee.has(t)?JSON.stringify({continue:!0}):te.has(t)?JSON.stringify({decision:`approve`}):void 0},v=e=>typeof e==`object`&&!!e&&!Array.isArray(e),re=e=>e instanceof Error?e.message:String(e),
|
|
3
|
-
`,`utf8`),i(n,e)},
|
|
4
|
-
`),t&&c.stdout.write(` backup: ${t}\n`)}async function
|
|
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),y=()=>c.env.HOME||u(),ie=()=>{let e=c.argv[1];return e&&s.isAbsolute(e)?e:`happenin`},ae=e=>{t(e,{recursive:!0})},b=t=>e(t),oe=e=>{let t=n(e,`utf8`);return JSON.parse(t)},x=(e,t)=>{let n=`${e}.${Date.now()}.tmp`;ae(s.dirname(e)),o(n,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,`utf8`),i(n,e)},se=(e,t)=>{if(ae(t),!b(e))return;let r=s.join(t,`${s.basename(e)}.${Date.now()}.bak`);return o(r,n(e,`utf8`),`utf8`),r},ce=e=>{if(!b(e))return{version:1,hooks:{}};let t=oe(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}},le=e=>{if(!b(e))return{hooks:{}};let t=oe(e);if(!v(t))throw Error(`Invalid JSON object in ${e}`);let n=v(t.hooks)?t.hooks:{};return{...t,hooks:n}},ue=(e,t,n)=>{e[t]=[...Array.isArray(e[t])?e[t]:[],{command:`${n} record cursor`}]},de=(e,t,n)=>{e[t]=[...Array.isArray(e[t])?e[t]:[],{matcher:``,hooks:[{type:`command`,command:`${n} record claude ${t}`}]}]},fe=e=>{let t=y(),n=s.join(t,`.cursor`,`hooks.json`),r=s.join(t,`.happenin`,`backups`,`cursor`),i=se(n,r),a=ce(n);for(let t of m)ue(a.hooks,t,e);return x(n,a),{target:n,backup:i}},pe=e=>{let t=y(),n=s.join(t,`.claude`,`settings.json`),r=s.join(t,`.happenin`,`backups`,`claude`),i=se(n,r),a=le(n);for(let t of h)de(a.hooks,t,e);return x(n,a),{target:n,backup:i}},me=e=>{let t=!1,n=!1;for(let r of e)r===`--cursor`&&(t=!0),r===`--claude`&&(n=!0);return!t&&!n&&(t=!0,n=!0),{cursor:t,claude:n}};async function he(e={}){let{cursor:t=!0,claude:n=!0}=e,r=ie(),i=[];t&&i.push(fe(r)),n&&i.push(pe(r));for(let{target:e,backup:t}of i)c.stdout.write(`${e} written
|
|
4
|
+
`),t&&c.stdout.write(` backup: ${t}\n`)}async function ge(e=[]){try{let{cursor:t,claude:n}=me(e);await he({cursor:t,claude:n})}catch(e){c.stderr.write(`install failed: ${re(e)}\n`),c.exitCode=1}}const _e=[`CREATE INDEX IF NOT EXISTS idx_events_source ON events(source);`,`CREATE INDEX IF NOT EXISTS idx_events_event ON events(event);`,`CREATE INDEX IF NOT EXISTS idx_events_session_id ON events(session_id);`,`CREATE INDEX IF NOT EXISTS idx_events_happened_at ON events(happened_at);`,`CREATE INDEX IF NOT EXISTS idx_events_received_at ON events(received_at);`,`CREATE INDEX IF NOT EXISTS idx_events_source_path ON events(source_path);`,`CREATE INDEX IF NOT EXISTS idx_events_subagent_id ON events(subagent_id);`];function S(e){let t=e.prepare(`PRAGMA user_version`).get();return Number(t?.user_version??0)}function ve(e){let t=S(e);if(!(t>=1)){if(e.exec(`BEGIN IMMEDIATE;`),t=S(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 ye=e=>{let t=S(e);if(!(t>=2)){if(e.exec(`BEGIN IMMEDIATE;`),t=S(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=Se(JSON.parse(e.payload));t!==void 0&&n.run(t,e.id)}e.exec(`
|
|
5
5
|
UPDATE events
|
|
6
6
|
SET happened_at = strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch')
|
|
7
7
|
WHERE happened_at IS NULL;
|
|
@@ -24,7 +24,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
24
24
|
WHERE
|
|
25
25
|
project_path IS NULL
|
|
26
26
|
AND json_valid(payload) = 1;
|
|
27
|
-
`),e.exec(`PRAGMA user_version = 2;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}},
|
|
27
|
+
`),e.exec(`PRAGMA user_version = 2;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}},be=e=>{let t=S(e);if(t<1)throw Error(`Database schema must be migrated before backfilling subagent metadata`);if(e.exec(`BEGIN IMMEDIATE;`),t=S(e),t<1){e.exec(`ROLLBACK;`);return}try{e.exec(`
|
|
28
28
|
UPDATE events
|
|
29
29
|
SET
|
|
30
30
|
subagent_id = NULLIF(trim(json_extract(payload, '$.subagent_id')), ''),
|
|
@@ -64,7 +64,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
64
64
|
WHERE s.event = 'subagentStart'
|
|
65
65
|
AND s.subagent_id = json_extract(events.payload, '$.tool_use_id')
|
|
66
66
|
);
|
|
67
|
-
`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}},
|
|
67
|
+
`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}},C=()=>c.env.HAPPENIN_DB?c.env.HAPPENIN_DB:s.join(u(),`.happenin`,`happenin.db`),w=(e,n=5e3)=>{let r=e??C();t(s.dirname(r),{recursive:!0});let i=new d(r);try{i.exec(`PRAGMA busy_timeout = ${n};`),i.exec(`PRAGMA journal_mode = WAL;`),i.exec(`CREATE TABLE IF NOT EXISTS events (
|
|
68
68
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
69
|
source TEXT NOT NULL,
|
|
70
70
|
client TEXT,
|
|
@@ -86,7 +86,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
86
86
|
mtime INTEGER NOT NULL,
|
|
87
87
|
imported_at INTEGER NOT NULL
|
|
88
88
|
);
|
|
89
|
-
`),
|
|
89
|
+
`),ve(i),ye(i);for(let e of _e)i.exec(e);return i}catch(e){try{i.close()}catch{}throw e}},xe=[`happenedAt`,`timestamp`,`happened_at`,`time`,`ts`,`createdAt`,`created_at`];function T(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 Se(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;for(let e of xe){let n=T(Reflect.get(t,e));if(n!==void 0)return n}}const E=(e,t)=>{let n=e.prepare(`
|
|
90
90
|
INSERT INTO events (
|
|
91
91
|
source,
|
|
92
92
|
client,
|
|
@@ -103,7 +103,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
103
103
|
subagent_type,
|
|
104
104
|
transcript_path
|
|
105
105
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
106
|
-
`),r=Date.now(),i=
|
|
106
|
+
`),r=Date.now(),i=T(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)},D=`COALESCE(strftime('%Y-%m-%dT%H:%M:%fZ', happened_at), strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch'))`;function Ce(e){return(e===`7d`?168:e===`30d`?720:24)*60*60*1e3}function O(e,t=Date.now()){let n=[],r=[];if(e.since!==void 0&&(n.push(`id > ?`),r.push(e.since)),e.source!==void 0&&(n.push(`source = ?`),r.push(e.source)),e.event!==void 0&&(n.push(`event = ?`),r.push(e.event)),e.tool!==void 0&&e.tool!==``&&(n.push(`tool_name = ?`),r.push(e.tool)),e.sessionId!==void 0&&e.sessionId!==``&&(e.sessionIdExact?(n.push(`session_id = ?`),r.push(e.sessionId)):(n.push(`session_id LIKE ?`),r.push(`%${e.sessionId}%`))),e.sessionIds!==void 0&&e.sessionIds.length>0){let t=e.sessionIds.filter(e=>e!==null),i=e.sessionIds.some(e=>e===null),a=[];if(t.length>0){let e=t.map(()=>`?`).join(`,`);a.push(`session_id IN (${e})`);for(let e of t)r.push(e)}i&&a.push(`session_id IS NULL`),n.push(a.length===1?a[0]:`(${a.join(` OR `)})`)}return e.subagentId!==void 0&&e.subagentId!==``&&(n.push(`subagent_id = ?`),r.push(e.subagentId)),e.q!==void 0&&e.q!==``&&(n.push(`(payload LIKE ? OR session_id LIKE ?)`),r.push(`%${e.q}%`),r.push(`%${e.q}%`)),e.range!==void 0&&e.range!==`all`&&(n.push(`${D} >= ?`),r.push(new Date(t-Ce(e.range)).toISOString())),{clause:n.length>0?`WHERE ${n.join(` AND `)}`:``,params:r}}const k=(e,t)=>{let{clause:n,params:r}=O(t),i=t.limit??100,a=t.offset??0,o=`
|
|
107
107
|
SELECT
|
|
108
108
|
id,
|
|
109
109
|
source,
|
|
@@ -121,7 +121,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
121
121
|
subagent_type AS subagentType,
|
|
122
122
|
transcript_path AS transcriptPath
|
|
123
123
|
FROM events
|
|
124
|
-
${n} ORDER BY id DESC LIMIT ?${a>0?` OFFSET ?`:``}`;return r.push(i),a>0&&r.push(a),e.prepare(o).all(...r)},
|
|
124
|
+
${n} ORDER BY id DESC LIMIT ?${a>0?` OFFSET ?`:``}`;return r.push(i),a>0&&r.push(a),e.prepare(o).all(...r)},A=(e,t)=>{let{clause:n,params:r}=O(t),i=`SELECT COUNT(*) AS count FROM events ${n}`,a=e.prepare(i).get(...r);return a?Number(a.count):0},we=(e,t)=>{let{clause:n,params:r}=O(t);return{total:A(e,t),bySource:e.prepare(`SELECT source, COUNT(*) AS count FROM events ${n} GROUP BY source ORDER BY count DESC`).all(...r),byEvent:e.prepare(`SELECT event, COUNT(*) AS count FROM events ${n} GROUP BY event ORDER BY count DESC`).all(...r),bySession:e.prepare(`SELECT session_id AS sessionId, COUNT(*) AS count, MIN(happened_at) AS firstAt, MAX(happened_at) AS lastAt FROM events ${n} GROUP BY session_id ORDER BY count DESC`).all(...r)}};function j(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,subagentId:e.subagentId,subagentType:e.subagentType,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)}}const Te=(e,t,n=Date.now())=>{let{clause:r,params:i}=O(t,n),a=t.limit??100,o=t.offset??0,s=`
|
|
125
125
|
SELECT
|
|
126
126
|
session_id AS sessionId,
|
|
127
127
|
COUNT(*) AS eventCount,
|
|
@@ -137,13 +137,30 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
137
137
|
GROUP BY session_id
|
|
138
138
|
ORDER BY lastReceivedAt DESC, sessionId
|
|
139
139
|
LIMIT ?${o>0?` OFFSET ?`:``}
|
|
140
|
-
`;return i.push(a),o>0&&i.push(o),e.prepare(s).all(...i).map(
|
|
140
|
+
`;return i.push(a),o>0&&i.push(o),e.prepare(s).all(...i).map(j)},Ee=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=O(t,r),o=`tool_name IS NOT NULL AND tool_name <> ''`,s=`SELECT tool_name AS tool, COUNT(*) AS count FROM events ${i?`${i} AND ${o}`:`WHERE ${o}`} GROUP BY tool_name ORDER BY count DESC, tool_name ASC LIMIT ?`;return e.prepare(s).all(...a,n)};function De(e){return e===`day`?`strftime('%Y-%m-%dT00:00:00.000Z', ${D})`:`strftime('%Y-%m-%dT%H:00:00.000Z', ${D})`}function Oe(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 ke(e){return e===`day`?1440*60*1e3:3600*1e3}const Ae=(e,t={},n=24,r=`hour`,i=Date.now())=>{if(n<=0){let n=De(r),{clause:a,params:o}=O({...t,range:void 0},i),s=`SELECT ${n} AS bucket, COUNT(*) AS count FROM events ${a} GROUP BY bucket ORDER BY bucket`;return e.prepare(s).all(...o)}let a=ke(r),o=Oe(r,i),s=Oe(r,i-n*60*60*1e3),c=Math.max(1,Math.ceil((o.getTime()-s.getTime())/a)+1),l=s.toISOString(),u=De(r),{clause:d,params:f}=O({...t,range:void 0},i),p=`${u} >= ?`,m=`SELECT ${u} AS bucket, COUNT(*) AS count FROM events ${d?`${d} AND ${p}`:`WHERE ${p}`} GROUP BY bucket ORDER BY bucket`,h=e.prepare(m).all(...f,l),g=[],_=new Map;for(let e of h)_.set(e.bucket,e.count);for(let e=0;e<c;e++){let e=r===`day`?`${s.toISOString().slice(0,10)}T00:00:00.000Z`:s.toISOString();g.push({bucket:e,count:_.get(e)??0}),s.setTime(s.getTime()+a)}return g},je=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 Pe=(e,t,n={})=>{let r=[...new Set(t.filter(e=>e!==null))];if(r.length===0)return[];let{clause:i,params:a}=O({...n,sessionId:void 0,sessionIds:void 0,subagentId:void 0}),o=i?` ${i.replace(/^WHERE/,`AND`)}`:``,s=[];for(let t=0;t<r.length;t+=500){let n=r.slice(t,t+500),i=`
|
|
141
|
+
SELECT
|
|
142
|
+
session_id AS sessionId,
|
|
143
|
+
subagent_id AS subagentId,
|
|
144
|
+
MAX(subagent_type) AS subagentType,
|
|
145
|
+
COUNT(*) AS eventCount,
|
|
146
|
+
MIN(received_at) AS firstReceivedAt,
|
|
147
|
+
MAX(received_at) AS lastReceivedAt,
|
|
148
|
+
MIN(happened_at) AS firstAt,
|
|
149
|
+
MAX(happened_at) AS lastAt,
|
|
150
|
+
COALESCE(json_group_array(DISTINCT project_path ORDER BY project_path) FILTER (WHERE project_path IS NOT NULL AND project_path <> ''), '[]') AS projectPaths,
|
|
151
|
+
COALESCE(json_group_array(DISTINCT tool_name ORDER BY tool_name) FILTER (WHERE tool_name IS NOT NULL AND tool_name <> ''), '[]') AS toolNames,
|
|
152
|
+
SUM(CASE WHEN event LIKE '%Failure%' THEN 1 ELSE 0 END) AS failureCount
|
|
153
|
+
FROM events
|
|
154
|
+
WHERE subagent_id IS NOT NULL AND subagent_id <> '' AND session_id IN (${n.map(()=>`?`).join(`,`)})${o}
|
|
155
|
+
GROUP BY session_id, subagent_id
|
|
156
|
+
ORDER BY lastReceivedAt DESC, subagent_id
|
|
157
|
+
`,c=e.prepare(i);s.push(...c.all(...n,...a))}return s.map(j)},M=(e,t={},n=Date.now())=>{let r=Te(e,{...t,limit:2**53-1,offset:0},n).filter(e=>Ne(e,t,n)),i=Pe(e,r.map(e=>e.sessionId),t),a=new Map;for(let e of i){let t=a.get(e.sessionId)??[];t.push(e),a.set(e.sessionId,t)}let o=r.map(e=>({...e,children:a.get(e.sessionId)})),s=t.offset??0,c=t.limit;return s===0&&c===void 0?o:c===void 0?o.slice(s):o.slice(s,s+c)},Fe=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)}},N=(e,t,n)=>{e.prepare(`
|
|
141
158
|
INSERT INTO imports (path, mtime, imported_at)
|
|
142
159
|
VALUES (?, ?, ?)
|
|
143
160
|
ON CONFLICT(path) DO UPDATE SET
|
|
144
161
|
mtime = excluded.mtime,
|
|
145
162
|
imported_at = excluded.imported_at
|
|
146
|
-
`).run(t,n,Date.now())},
|
|
163
|
+
`).run(t,n,Date.now())},Ie=(e,t)=>{let n=e.prepare(`SELECT mtime FROM imports WHERE path = ?`).get(t);return n?Number(n.mtime):void 0};function P(e){if(e==null||typeof e==`object`)return;let t=String(e).trim();return t.length>0?t:void 0}function F(e){let t=P(e);if(t!==void 0)return t;if(Array.isArray(e))for(let t of e){let e=P(t);if(e!==void 0)return e}}function Le(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 Re(e,t,n){if(t.length===0)return;let r;try{r=JSON.parse(t)}catch{return}let i=e[0];if(i!==`cursor`&&i!==`claude`)return;let a;a=i===`cursor`?P(r.hook_event_name):e[1];let o=i===`cursor`&&a===`subagentStart`?P(r.parent_conversation_id)??P(r.conversation_id)??P(r.sessionId)??P(r.session_id):P(r.sessionId)??P(r.session_id),s=Se(r),c=P(r.projectPath)??P(r.cwd)??P(r.project_path)??F(r.workspaceRoot)??F(r.workspaceRoots)??F(r.workspace_roots)??F(r.workspace_root)??P(r.workspace_path),l=P(r.filePath??r.file_path??r.path),u=P(r.toolName??r.tool_name??r.tool),d=P(r.client)??(i===`cursor`?`cursor`:`claude_code`),f=P(r.subagent_id)??void 0,p=P(r.subagent_type)??void 0,m=P(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(_=w(n,500),!h.subagentId){let e=P(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)}}E(_,h)}catch(e){if(Le(e))console.warn(`happenin: dropping event due to database lock`,e);else throw e}finally{try{_?.close()}catch{}}return g}async function ze(e){let t=[];for await(let e of c.stdin)t.push(Buffer.isBuffer(e)?e:Buffer.from(e,`utf8`));let n=Re(e,Buffer.concat(t).toString(`utf8`).trim());n!==void 0&&c.stdout.write(`${n}\n`)}const Be=`claude-transcript`,I=`cursor-transcript`,L=`cursor`,R=e=>{try{return r(e,{withFileTypes:!0})}catch{return[]}},Ve=e=>{try{return n(e,`utf8`)}catch{return}},He=e=>{try{return Math.floor(a(e).mtimeMs)}catch{return}},z=(e,t)=>{let n=He(t);if(n===void 0||Ie(e,t)===n)return;let r=Ve(t);if(r!==void 0)return{content:r,mtime:n}};function B(e){let t=[];for(let n of R(e)){if(n.isSymbolicLink())continue;let r=s.join(e,n.name);n.isDirectory()?t.push(...B(r)):n.isFile()&&n.name.endsWith(`.jsonl`)&&n.name!==`store.db`&&t.push(r)}return t}function Ue(e){let t=[];for(let n of R(e)){if(!n.isDirectory()||n.isSymbolicLink())continue;let r=s.join(e,n.name);for(let e of R(r))e.isDirectory()&&!e.isSymbolicLink()&&t.push(s.join(r,e.name))}return t}async function We(e,t){let n=z(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(Be,t);for(let{obj:n,line:r}of o)E(e,{source:Be,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});N(e,t,i)}async function Ge(e){let t=s.resolve(u(),`.claude/projects`);for(let n of B(t))await We(e,n)}async function Ke(e,t,n){let r=z(e,t);if(r===void 0)return;let{content:i,mtime:a}=r,o=[],s;try{s=JSON.parse(i)}catch{return}Array.isArray(s)&&(o=s),e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(I,t);for(let r of o)typeof r==`string`&&E(e,{source:I,client:L,event:`prompt`,sessionId:n,payload:JSON.stringify({prompt:r}),sourcePath:t});N(e,t,a)}async function qe(e,t,n){let r=z(e,t);if(r===void 0)return;let{content:i,mtime:a}=r;e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(I,t),E(e,{source:I,client:L,event:`session_meta`,sessionId:n,payload:i,sourcePath:t}),N(e,t,a)}async function Je(e,t){let n=s.basename(t);await Ke(e,s.join(t,`prompt_history.json`),n),await qe(e,s.join(t,`meta.json`),n)}async function Ye(e){let t=s.resolve(u(),`.cursor/chats`);for(let n of Ue(t))await Je(e,n)}async function V(e){let t=w(C());try{be(t),await Xe(t)}finally{t.close()}}async function Xe(e){await Ge(e),await Ye(e)}function H(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function U(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}function Ze(e,t=24){return e.length<=t?e:`${e.slice(0,t-1)}…`}function Qe(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 W(e){if(e==null||e===``)return`-`;let t=new Date(e);return Number.isNaN(t.getTime())?`-`:t.toLocaleString()}function $e(e){let t=e.totalSessions===0?`-`:`${Math.round(e.successRate)}%`;return`<div class="metric-grid">
|
|
147
164
|
<div class="metric-card metric-sessions">
|
|
148
165
|
<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
166
|
<div class="metric-label">Total Sessions</div>
|
|
@@ -157,24 +174,26 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
157
174
|
<div class="metric-card metric-duration">
|
|
158
175
|
<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
176
|
<div class="metric-label">Average Duration</div>
|
|
160
|
-
<div class="metric-value">${
|
|
177
|
+
<div class="metric-value">${Qe(e.averageDurationMs)}</div>
|
|
161
178
|
</div>
|
|
162
179
|
<div class="metric-card metric-success">
|
|
163
180
|
<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
181
|
<div class="metric-label">Success Rate</div>
|
|
165
182
|
<div class="metric-value">${t}</div>
|
|
166
183
|
</div>
|
|
167
|
-
</div>`}function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
</div>`}function et(e,t){return t===`day`?e.slice(5,10):e.slice(11,16)}function tt(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:et(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">${H(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 nt(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="${U(r)}"`:``}>${H(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 rt(e,t,n){let r=new URLSearchParams({session:t});return n&&r.set(`subagent`,n),e?.source&&r.set(`source`,e.source),e?.event&&r.set(`event`,e.event),e?.q&&r.set(`q`,e.q),e?.range&&r.set(`range`,e.range),e?.status&&r.set(`status`,e.status),e?.tool&&r.set(`tool`,e.tool),e?.minDuration!==void 0&&r.set(`minDuration`,String(e.minDuration)),e?.maxDuration!==void 0&&r.set(`maxDuration`,String(e.maxDuration)),e?.limit!==void 0&&r.set(`limit`,String(e.limit)),e?.offset!==void 0&&r.set(`offset`,String(e.offset)),`/fragments/detail?${r.toString()}`}function G(e,t,n,r,i,a=!1,o=``,s=``){let c=Me(e,t),l=a?e.subagentId??`no subagent`:e.sessionId??`no session`,u=H(Ze(l,28)),d=H(W(e.firstAt??e.firstReceivedAt)),f=H(Qe(e.durationMs)),p=(a?e.subagentId===r&&e.sessionId===n:e.sessionId===n)?` active`:``,m=e.sessionId??``,h=a?e.subagentId:void 0,g=!a&&!!e.children?.length,_=m&&!g?` hx-get="${U(rt(i,m,h))}" hx-target="#dashboard-content" hx-swap="innerHTML"`:``,ee=m&&g?` hx-get="${U(rt(i,m))}" hx-target="#dashboard-content" hx-swap="innerHTML"`:``,te=a?` session-subagent`:``,ne=!a&&e.children?.length&&e.sessionId===n?` expanded`:``,v=!a&&e.children?.length?` session-parent`:``,re=!a&&!e.sessionId?` session-item-static`:``,y=a?` data-session="${U(m)}" data-subagent="${U(e.subagentId??``)}"`:` data-session="${U(e.sessionId??``)}"`,ie=a&&e.subagentType?` <span class="subagent-type-badge">${H(e.subagentType)}</span>`:``;return`<li class="session-item${v}${ne}${te}${p}${re}"${y}${_}>
|
|
185
|
+
${o}
|
|
186
|
+
<div class="session-main"${ee}>
|
|
187
|
+
<div class="session-row">
|
|
188
|
+
<span class="session-id" title="${U(l)}">${u}${ie}</span>
|
|
189
|
+
<span class="status-badge status-${c}">${c}</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="session-meta"><span>${f}</span><span>${e.eventCount} events</span><span>${d}</span></div>
|
|
192
|
+
</div>
|
|
193
|
+
<span class="session-chevron" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></span>
|
|
194
|
+
${s}
|
|
195
|
+
</li>`}function it(e,t=Date.now(),n,r,i){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>
|
|
196
|
+
<ul class="session-list sessions-table">${e.map(e=>e.children?.length?G(e,t,n,r,i,!1,`<button type="button" class="session-toggle" aria-label="toggle subagents" onclick="event.stopPropagation(); const li = this.closest('.session-item'); li.classList.toggle('expanded'); this.textContent = li.classList.contains('expanded') ? '▾' : '▸'">${e.sessionId===n?`▾`:`▸`}</button>`,`<ul class="session-children">${e.children.map(e=>G(e,t,n,r,i,!0)).join(``)}</ul>`):G(e,t,n,r,i)).join(``)}</ul>`}function at(e){if(e)try{return JSON.parse(e)}catch{return}}function ot(e){return{id:e.id,source:e.source,client:e.client,event:e.event,sessionId:e.sessionId,happenedAt:e.happenedAt,receivedAt:e.receivedAt,projectPath:e.projectPath,filePath:e.filePath,toolName:e.toolName,sourcePath:e.sourcePath,subagentId:e.subagentId,subagentType:e.subagentType,transcriptPath:e.transcriptPath,payload:at(e.payload??``)??e.payload}}function st(e){let t=new Map;for(let n of e){let e=n.subagentId??null;t.has(e)||t.set(e,[]),t.get(e).push(n)}let n=[];for(let[e,r]of t){let t=r.toSorted((e,t)=>t.id-e.id),i=t.findIndex(e=>e.event===`subagentStart`),a=i>=0?t.at(i):t.at(0),o=t.filter(e=>e!==a);n.push({subagentId:e,parent:a,children:o,maxId:t.at(0).id})}return n.toSorted((e,t)=>t.maxId-e.maxId)}function ct(e){return`<span class="ts">${H(W(e.happenedAt??e.receivedAt))}</span><span class="source">${H(e.source??``)}</span><span class="event">${H(e.event??``)}</span><span class="tool">${H(e.toolName??``)}</span>`}function lt(e){let t=H(W(e.happenedAt??e.receivedAt)),n=H(e.source??``),r=H(e.client??``),i=H(e.event??``),a=H(e.toolName??``),o=H(e.subagentId??``),s=H(e.subagentType??``),c=H(e.projectPath??``),l=H(e.filePath??``),u=o?`<div><span>Subagent</span><span>${s||`-`} (${o})</span></div>`:``,d=l?`<div><span>File</span><span>${l}</span></div>`:``;return`<div class="detail-attrs">
|
|
178
197
|
<div><span>Time</span><span>${t}</span></div>
|
|
179
198
|
<div><span>Source</span><span>${n}</span></div>
|
|
180
199
|
<div><span>Client</span><span>${r||`-`}</span></div>
|
|
@@ -183,53 +202,53 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
183
202
|
<div><span>Project</span><span>${c||`-`}</span></div>
|
|
184
203
|
${u}
|
|
185
204
|
${d}
|
|
186
|
-
</div>`}function
|
|
205
|
+
</div>`}function ut(e){return`<div class="detail-json">
|
|
187
206
|
<details class="json-toggle">
|
|
188
207
|
<summary>JSON</summary>
|
|
189
|
-
<pre>${
|
|
208
|
+
<pre>${H(JSON.stringify(ot(e),null,2))}</pre>
|
|
190
209
|
</details>
|
|
191
210
|
<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
|
|
193
|
-
<summary>${
|
|
194
|
-
<div class="detail-body">${
|
|
195
|
-
${
|
|
211
|
+
</div>`}function dt(e){return`<details class="detail-event" data-event-id="${e.id}" open>
|
|
212
|
+
<summary>${ct(e)}</summary>
|
|
213
|
+
<div class="detail-body">${lt(e)}
|
|
214
|
+
${ut(e)}
|
|
196
215
|
</div>
|
|
197
|
-
</details>`}function
|
|
198
|
-
<summary>${
|
|
216
|
+
</details>`}function ft(e,t){let n=e.parent,r=e.children.map(dt).join(``),i=`<span class="subagent-badge">${H(n.subagentType||`subagent`)}${` · ${H(Ze(t,18))}`} · ${e.children.length}</span>`;return`<details class="detail-event detail-subagent" data-event-id="${n.id}" open>
|
|
217
|
+
<summary>${ct(n)}${i}</summary>
|
|
199
218
|
<div class="detail-body">
|
|
200
|
-
${
|
|
201
|
-
${
|
|
219
|
+
${lt(n)}
|
|
220
|
+
${ut(n)}
|
|
202
221
|
<div class="detail-children">${r}</div>
|
|
203
222
|
</div>
|
|
204
|
-
</details>`}function
|
|
223
|
+
</details>`}function pt(e){return st(e).map(e=>e.subagentId?ft(e,e.subagentId):[e.parent,...e.children].map(dt).join(``)).join(``)}function mt(e,t,n,r){if(!e)return`<div class="detail-header"><h2 class="detail-title">Session Details</h2></div><div class="empty">No events.</div>`;let i=r?t.filter(e=>e.subagentId===r):t,a=r?`${e} · ${r}`:e;if(i.length===0)return`<div class="detail-header"><h2 class="detail-title">Session Details - ${H(a)}</h2></div><div class="empty">No events for this subagent.</div>`;let o=pt(i),s=JSON.stringify(i,null,2),c=n!==void 0&&n>i.length?`<div class="detail-truncated">Showing the ${i.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="${U(e)}" data-subagent="${U(r??``)}">
|
|
205
224
|
<div class="detail-header">
|
|
206
225
|
<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 - ${
|
|
208
|
-
<span class="detail-count">${n??
|
|
226
|
+
<h2 class="detail-title">Session Details - ${H(a)}</h2>
|
|
227
|
+
<span class="detail-count">${n??i.length} events</span>
|
|
209
228
|
</div>
|
|
210
|
-
${
|
|
229
|
+
${c}
|
|
211
230
|
<div class="detail-toolbar">
|
|
212
231
|
<input type="search" class="detail-search" placeholder="Search attributes..." oninput="filterSessionDetails(this.value)">
|
|
213
232
|
<button type="button" onclick="toggleSessionDetails(false)">Collapse All</button>
|
|
214
233
|
<button type="button" onclick="toggleSessionDetails(true)">Expand All</button>
|
|
215
234
|
</div>
|
|
216
|
-
<div class="detail-events">${
|
|
235
|
+
<div class="detail-events">${o}</div>
|
|
217
236
|
<div class="detail-actions">
|
|
218
237
|
<button type="button" onclick="copySessionJson()">Copy JSON</button>
|
|
219
238
|
</div>
|
|
220
|
-
<pre id="session-json" class="session-json">${
|
|
221
|
-
</section>`}function
|
|
239
|
+
<pre id="session-json" class="session-json">${H(s)}</pre>
|
|
240
|
+
</section>`}function ht(e){if(!e)return;let t=Number(e);return Number.isFinite(t)&&t>=0?t:void 0}function gt(e){if(!e)return;let t=Number(e);return Number.isInteger(t)&&t>=0?t:void 0}function _t(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 K(e){let t=e.searchParams.get(`since`),n=e.searchParams.get(`source`)||void 0,r=e.searchParams.get(`event`)||void 0,i=e.searchParams.get(`session`)||void 0,a=e.searchParams.get(`subagent`)||void 0,o=e.searchParams.get(`q`)||void 0,s=e.searchParams.get(`status`)||void 0,c=e.searchParams.get(`tool`)||void 0,l=e.searchParams.get(`minDuration`),u=e.searchParams.get(`maxDuration`),d=e.searchParams.get(`range`)||`24h`,f=[`24h`,`7d`,`30d`,`all`].includes(d)?d:`24h`,p=e.searchParams.get(`limit`),m=e.searchParams.get(`offset`),h;if(t){let e=parseInt(t,10);Number.isNaN(e)||(h=e)}return{since:h,source:n,event:r,sessionId:i,subagentId:a,q:o,status:s===`active`||s===`completed`||s===`failed`?s:void 0,tool:c,minDuration:ht(l),maxDuration:ht(u),range:f,limit:gt(p),offset:gt(m)}}function q(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 vt(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="${q(e,o,n)}" hx-get="${q(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="${q(e,s,n)}" hx-get="${q(e,s,n)}" hx-target="#dashboard-content" hx-swap="innerHTML">Next</a>`:`<button type="button" disabled>Next</button>`}</div>`}function J(e,t,n,r,i){let a=t.limit&&t.limit>0?t.limit:25,o=t.offset??0;return`<aside class="session-sidebar">
|
|
222
241
|
<div class="session-list-wrapper">
|
|
223
|
-
${
|
|
224
|
-
${
|
|
242
|
+
${it(e.slice(o,o+a),n,r,i,t)}
|
|
243
|
+
${vt(t,e.length)}
|
|
225
244
|
</div>
|
|
226
|
-
</aside>`}function
|
|
245
|
+
</aside>`}function yt(e,t){let n=Date.now(),{hours:r,groupBy:i}=_t(t.range),a=M(e,{...t,subagentId:void 0,limit:void 0,offset:void 0},n),o=a.map(e=>e.sessionId),s=[],c=[];if(o.length>0){let a={...t,subagentId:void 0,sessionIds:o};s=Ae(e,a,r,i,n),c=Ee(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`${J(a,t,n)}
|
|
227
246
|
<div class="main-content">
|
|
228
|
-
<div class="main-metrics">${
|
|
229
|
-
<div class="top-charts">${
|
|
230
|
-
</div>`}function
|
|
231
|
-
<div class="main-content"><div class="detail-area"><div class="empty">No events.</div></div></div>`;let i=
|
|
232
|
-
<div class="main-content">${
|
|
247
|
+
<div class="main-metrics">${$e(p)}</div>
|
|
248
|
+
<div class="top-charts">${tt(s,i)}${nt(c)}</div>
|
|
249
|
+
</div>`}function bt(e,t,n,r){let i=yt(r,K(n));t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(i)}function xt(e,t){let n=Date.now(),r=J(M(e,{...t,sessionId:void 0,subagentId:void 0,limit:void 0,offset:void 0},n),t,n,t.sessionId,t.subagentId);if(!t.sessionId)return`${r}
|
|
250
|
+
<div class="main-content"><div class="detail-area"><div class="empty">No events.</div></div></div>`;let i=k(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId,limit:1e3,offset:0}),a=A(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId});return`${r}
|
|
251
|
+
<div class="main-content">${mt(t.sessionId,i,a,t.subagentId)}</div>`}function St(e,t,n,r){let i=xt(r,K(n));t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(i)}function Ct(e,t,n,r){let i=k(r,K(n));t.writeHead(200,{"Content-Type":`application/json`}),t.end(JSON.stringify(i))}const wt=[{value:`24h`,label:`Last 24h`},{value:`7d`,label:`Last 7 days`},{value:`30d`,label:`Last 30 days`},{value:`all`,label:`All time`}];function Tt(e){let t=H(e.q??``),n=e.range??`24h`;return`<header class="app-header">
|
|
233
252
|
<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
253
|
<h1 class="page-title">Session Overview Analytics</h1>
|
|
235
254
|
<div class="header-controls">
|
|
@@ -237,17 +256,17 @@ ${mt(t,e.length)}
|
|
|
237
256
|
<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
257
|
<input type="text" name="q" class="search" placeholder="session or payload" value="${t}" autocomplete="off">
|
|
239
258
|
</div>
|
|
240
|
-
<label>range <select name="range">${
|
|
259
|
+
<label>range <select name="range">${wt.map(e=>`<option value="${U(e.value)}"${e.value===n?` selected`:``}>${H(e.label)}</option>`).join(``)}</select></label>
|
|
241
260
|
<button type="button" class="theme" data-theme-toggle title="Toggle light/dark theme" aria-label="toggle theme">🌙</button>
|
|
242
261
|
</div>
|
|
243
|
-
</header>`}const
|
|
244
|
-
<label>status ${Y(`status`,
|
|
262
|
+
</header>`}const Et=[`active`,`completed`,`failed`];function Y(e,t,n,r=`all`){let i=n??``,a=U(i);return`<select name="${e}"><option value=""${i===``?` selected`:``}>${r}</option>${t.map(e=>{let t=U(e);return`<option value="${t}"${t===a?` selected`:``}>${H(e)}</option>`}).join(``)}</select>`}function Dt(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">
|
|
263
|
+
<label>status ${Y(`status`,Et,n)}</label>
|
|
245
264
|
<label>source ${Y(`source`,e.sources,r)}</label>
|
|
246
265
|
<label>tool ${Y(`tool`,e.tools,i)}</label>
|
|
247
266
|
<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="${
|
|
249
|
-
<label>max duration (m) <input type="number" name="maxDuration" min="0" step="any" placeholder="max" value="${
|
|
250
|
-
</div>`}function
|
|
267
|
+
<label>min duration (m) <input type="number" name="minDuration" min="0" step="any" placeholder="min" value="${H(o)}"></label>
|
|
268
|
+
<label>max duration (m) <input type="number" name="maxDuration" min="0" step="any" placeholder="max" value="${H(s)}"></label>
|
|
269
|
+
</div>`}function Ot(e,t){let n=Fe(e),r=yt(e,t);return`<!DOCTYPE html>
|
|
251
270
|
<html lang="en" hx-ext="sse">
|
|
252
271
|
<head>
|
|
253
272
|
<meta charset="utf-8">
|
|
@@ -432,8 +451,12 @@ ${mt(t,e.length)}
|
|
|
432
451
|
const detail = content.querySelector('.session-detail-view');
|
|
433
452
|
document.body.classList.toggle('session-detail-open', !!detail);
|
|
434
453
|
const sessionId = detail ? detail.getAttribute('data-session') : null;
|
|
454
|
+
const subagentId = detail ? detail.getAttribute('data-subagent') || null : null;
|
|
435
455
|
for (const item of content.querySelectorAll('.session-item')) {
|
|
436
|
-
|
|
456
|
+
const sessionMatch = !!sessionId && item.dataset.session === sessionId;
|
|
457
|
+
const itemSubagent = item.dataset.subagent || null;
|
|
458
|
+
const subagentMatch = itemSubagent === null || itemSubagent === subagentId;
|
|
459
|
+
item.classList.toggle('active', sessionMatch && subagentMatch);
|
|
437
460
|
}
|
|
438
461
|
const list = content.querySelector('.session-list');
|
|
439
462
|
if (list) {
|
|
@@ -561,6 +584,21 @@ button:hover { background: var(--surface-5); }
|
|
|
561
584
|
.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
585
|
.session-chevron:hover { color: var(--accent); background: none; }
|
|
563
586
|
.session-list .empty { padding: 2rem 1rem; text-align: center; color: var(--text-4); }
|
|
587
|
+
.session-item { flex-wrap: wrap; }
|
|
588
|
+
.session-item.session-parent { padding-bottom: 0; cursor: default; }
|
|
589
|
+
.session-item.session-parent:hover { background: none; }
|
|
590
|
+
.session-item.session-parent > .session-main { cursor: pointer; }
|
|
591
|
+
.session-item.session-parent > .session-main:hover { background: var(--surface-3); }
|
|
592
|
+
.session-toggle { background: none; border: none; color: var(--text-3); font-size: 0.8rem; padding: 0; margin-right: 0.5rem; cursor: pointer; width: 1rem; text-align: center; }
|
|
593
|
+
.session-toggle:hover { color: var(--accent); }
|
|
594
|
+
.session-children { display: none; width: 100%; list-style: none; padding: 0; margin: 0; align-self: flex-start; }
|
|
595
|
+
.session-children .session-item { padding-left: 2.75rem; background: none; border-bottom: 1px solid var(--border-2); border-left: 3px solid var(--border-3); }
|
|
596
|
+
.session-children .session-item:hover { background: var(--surface-3); }
|
|
597
|
+
.session-children .session-item.active { background: var(--surface-3); border-left-color: var(--accent-2); box-shadow: inset 3px 0 0 var(--accent-2); }
|
|
598
|
+
.session-children .session-subagent .session-id { color: var(--accent-2); }
|
|
599
|
+
.session-children .session-subagent .session-chevron { display: none; }
|
|
600
|
+
.session-item.expanded > .session-children { display: block; }
|
|
601
|
+
.subagent-type-badge { font-size: 0.65rem; color: var(--text-3); margin-left: 0.35rem; padding: 0.08rem 0.3rem; background: var(--surface-2); border-radius: 0.2rem; }
|
|
564
602
|
.main-content { display: grid; grid-template-rows: auto 1fr; gap: 1.25rem; min-height: 0; overflow: hidden; }
|
|
565
603
|
.main-content:has(.session-detail-view) { grid-template-rows: 1fr; }
|
|
566
604
|
.main-metrics .metric-grid { grid-template-columns: repeat(4, 1fr); gap: 1rem; }
|
|
@@ -653,7 +691,7 @@ button:hover { background: var(--surface-5); }
|
|
|
653
691
|
<div class="app">
|
|
654
692
|
<div class="sticky-bar">
|
|
655
693
|
<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
|
-
${
|
|
694
|
+
${Tt(t)}${Dt(n,t)}
|
|
657
695
|
</form>
|
|
658
696
|
</div>
|
|
659
697
|
<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">
|
|
@@ -661,12 +699,12 @@ ${r}
|
|
|
661
699
|
</div>
|
|
662
700
|
</div>
|
|
663
701
|
</body>
|
|
664
|
-
</html>`}function
|
|
702
|
+
</html>`}function kt(e,t,n,r){let i=K(n);t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(Ot(r,i))}let X;const Z=new Set;function At(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
703
|
|
|
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)?
|
|
704
|
+
`);let n=e.headers[`last-event-id`],r=Array.isArray(n)?n[0]:n,i=r?parseInt(r,10):NaN,a={res:t,lastId:Number.isNaN(i)?je(X):i,timer:null};a.timer=setInterval(()=>{try{let e=je(X);e>a.lastId&&(t.write(`event: message
|
|
667
705
|
data: ping
|
|
668
706
|
|
|
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
|
|
707
|
+
`),a.lastId=e)}catch(e){console.error(`SSE poll error:`,e),clearInterval(a.timer),Z.delete(a)}},300),Z.add(a),e.on(`close`,()=>{clearInterval(a.timer),Z.delete(a)})}function jt(e,t){let n=new URL(e.url??`/`,`http://localhost`);try{if(e.method===`GET`){if(n.pathname===`/`){kt(e,t,n,X);return}if(n.pathname===`/fragments/sessions`){bt(e,t,n,X);return}if(n.pathname===`/fragments/detail`){St(e,t,n,X);return}if(n.pathname===`/events`){Ct(e,t,n,X);return}if(n.pathname===`/events/stream`){At(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 Mt(e,t,n=!1){let r=f.createServer((e,t)=>{jt(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 Nt(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 V(),X=w(C()),await Mt(t,n,r)}const Pt=[`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 Ft=[`24h`,`7d`,`30d`,`all`];function It(){c.stdout.write(`Usage: happenin query [options]
|
|
670
708
|
|
|
671
709
|
Options:
|
|
672
710
|
--source <source> filter by source (cursor, claude, ...)
|
|
@@ -681,7 +719,7 @@ Options:
|
|
|
681
719
|
--format <format> output format: json (default), jsonl, summary
|
|
682
720
|
--db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
|
|
683
721
|
-h, --help show this help
|
|
684
|
-
`)}function
|
|
722
|
+
`)}function Lt(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`:Ft.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`:Pt.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`:Ft.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`:Pt.includes(a)&&(n=a);break;case`db`:r=a;break}}}}return{options:{range:`24h`,...t},format:n,dbPath:r,help:i}}async function Rt(e,t,n,r=!1){let{options:i,format:a,dbPath:o,help:s}=Lt(e);if(s){t();return}r||(delete i.status,delete i.minDuration,delete i.maxDuration);let c=w(o);try{await n(c,i,a)}finally{c.close()}}async function zt(e){await Rt(e,It,(e,t,n)=>{if(n===`summary`){let n=we(e,t);c.stdout.write(`${JSON.stringify(n,null,2)}\n`);return}let r={total:A(e,t),rows:k(e,t).map(ot)};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 Bt(){c.stdout.write(`Usage: happenin sessions [options]
|
|
685
723
|
|
|
686
724
|
Options:
|
|
687
725
|
--source <source> filter by source (cursor, claude, ...)
|
|
@@ -699,8 +737,8 @@ Options:
|
|
|
699
737
|
--format <format> output format: json (default), jsonl, summary
|
|
700
738
|
--db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
|
|
701
739
|
-h, --help show this help
|
|
702
|
-
`)}function
|
|
703
|
-
`)}async function
|
|
740
|
+
`)}function Vt(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(`
|
|
741
|
+
`)}async function Ht(e){await Rt(e,Bt,(e,t,n)=>{let r=M(e,t);if(n===`jsonl`){for(let e of r)c.stdout.write(`${JSON.stringify(e)}\n`);return}if(n===`summary`){c.stdout.write(`${Vt(r)}\n`);return}c.stdout.write(`${JSON.stringify(r,null,2)}\n`)},!0)}function Ut(){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 Wt=Ut(),Gt=s.join(Wt,`assets`,`help.md`),Kt=s.join(Wt,`package.json`);function qt(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 Jt(){let e;try{e=n(Gt,`utf8`)}catch{e=`# happenin
|
|
704
742
|
|
|
705
743
|
Run \`happenin <command>\`.
|
|
706
|
-
`}c.stdout.write(`\n${
|
|
744
|
+
`}c.stdout.write(`\n${qt(e)}\n`)}function Yt(){let e=`0.0.0`;try{let t=n(Kt,`utf8`);e=JSON.parse(t).version??e}catch{}c.stdout.write(`${e}\n`)}async function Xt(){let[e,...t]=c.argv.slice(2);if(!e||e===`--help`||e===`-h`){Jt();return}if(e===`--version`||e===`-v`){Yt();return}if(e===`install`){await ge(t);return}if(e===`record`){await ze(t);return}if(e===`import`){await V(t);return}if(e===`dashboard`){await Nt(t);return}if(e===`query`){await zt(t);return}if(e===`sessions`){await Ht(t);return}c.stderr.write(`Unknown command: ${e}\n`),Jt(),c.exitCode=1}await Xt();export{};
|
package/package.json
CHANGED