happenin 0.0.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/LICENSE.md +19 -0
- package/README.md +180 -0
- package/assets/help.md +59 -0
- package/dist/bin.js +391 -0
- package/package.json +74 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 YogliB
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# happenin
|
|
2
|
+
|
|
3
|
+
A macOS CLI that records Cursor and Claude Code agent events to a local SQLite database and serves a live browser dashboard.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Stability
|
|
10
|
+
|
|
11
|
+
`happenin` is pre-1.0. The CLI, database schema, and dashboard output may change in small ways between releases until `v1.0.0`. Any breaking changes will be minor and listed in the [changelog](docs/CHANGELOG.md).
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- macOS
|
|
16
|
+
- Node.js `>= 24.0.0` (uses the built-in `node:sqlite` module)
|
|
17
|
+
- Zero runtime dependencies
|
|
18
|
+
- The dashboard loads htmx, AlpineJS, and htmx-ext-sse from a CDN
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g happenin
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or run from source:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone git@github.com:YogliB/happenin.git
|
|
30
|
+
cd happenin
|
|
31
|
+
npm install
|
|
32
|
+
npm run build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Install hooks into Cursor and Claude Code (backs up existing configs)
|
|
39
|
+
happenin install
|
|
40
|
+
|
|
41
|
+
# Import existing transcripts
|
|
42
|
+
happenin import
|
|
43
|
+
|
|
44
|
+
# Start the live dashboard
|
|
45
|
+
happenin dashboard
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The dashboard opens at `http://localhost:8765`. New events appear automatically.
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
### `happenin install [--cursor] [--claude]`
|
|
53
|
+
|
|
54
|
+
Backs up and appends `happenin record` hooks to `~/.cursor/hooks.json` and `~/.claude/settings.json`. Backups are written to `~/.happenin/backups/`.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
happenin install --cursor
|
|
58
|
+
happenin install --claude
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `happenin record <source> [event]`
|
|
62
|
+
|
|
63
|
+
The hook target. Reads a JSON payload from stdin, writes it to `~/.happenin/happenin.db`, and prints the required non-blocking response for the agent.
|
|
64
|
+
|
|
65
|
+
- `source` — `cursor` or `claude`.
|
|
66
|
+
- `event` — only required for Claude; Cursor payloads include `hook_event_name`.
|
|
67
|
+
|
|
68
|
+
This command is normally called by the agent hooks, not directly:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
echo '{"hook_event_name":"sessionStart","sessionId":"abc123"}' | happenin record cursor
|
|
72
|
+
echo '{"sessionId":"abc123"}' | happenin record claude SessionStart
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `happenin import`
|
|
76
|
+
|
|
77
|
+
Imports existing transcripts:
|
|
78
|
+
|
|
79
|
+
- Claude Code JSONL from `~/.claude/projects/<project>/<session>.jsonl`.
|
|
80
|
+
- Cursor `prompt_history.json` and `meta.json` from `~/.cursor/chats/<hash>/<session>/`.
|
|
81
|
+
|
|
82
|
+
`store.db` is skipped because it is encrypted.
|
|
83
|
+
|
|
84
|
+
### `happenin query [options]`
|
|
85
|
+
|
|
86
|
+
Query events from the local database and print them as JSON, JSONL, or a summary.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
happenin query --limit 10
|
|
90
|
+
happenin query --source cursor --event subagentStart --format jsonl
|
|
91
|
+
happenin query --session abc123 --format summary
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [docs/USAGE.md](docs/USAGE.md) for all filters.
|
|
95
|
+
|
|
96
|
+
### `happenin dashboard [--port <port>] [--no-open] [--silent]`
|
|
97
|
+
|
|
98
|
+
Starts a local HTTP server and opens the dashboard.
|
|
99
|
+
|
|
100
|
+
- `--port` — port to listen on (default: `8765`).
|
|
101
|
+
- `--no-open` — do not open the browser.
|
|
102
|
+
- `--silent` — alias for `--no-open`; used automatically by `npm start`.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
happenin dashboard --port 9000 --silent
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
- `HAPPENIN_DB` — override the SQLite database path. Default: `~/.happenin/happenin.db`.
|
|
111
|
+
- `NO_COLOR` — disable colored help output.
|
|
112
|
+
|
|
113
|
+
## Privacy
|
|
114
|
+
|
|
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.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
nub install
|
|
121
|
+
nub run build
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The same scripts also work with npm:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install
|
|
128
|
+
npm run build
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Before opening a pull request, run the full check suite:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm run build
|
|
135
|
+
npm run typecheck
|
|
136
|
+
npm run format
|
|
137
|
+
npm run lint
|
|
138
|
+
npm run duplicates:ci
|
|
139
|
+
npm run knip:ci
|
|
140
|
+
npm run test:ci
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for details.
|
|
144
|
+
|
|
145
|
+
## Documentation
|
|
146
|
+
|
|
147
|
+
### User docs
|
|
148
|
+
|
|
149
|
+
| Doc | Purpose |
|
|
150
|
+
| -------------------------------------------------- | -------------------------------- |
|
|
151
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | How the pieces fit together. |
|
|
152
|
+
| [docs/USAGE.md](docs/USAGE.md) | Full usage guide. |
|
|
153
|
+
| [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common problems. |
|
|
154
|
+
| [docs/CHANGELOG.md](docs/CHANGELOG.md) | Release notes. |
|
|
155
|
+
| [assets/help.md](assets/help.md) | CLI help text shown by `--help`. |
|
|
156
|
+
|
|
157
|
+
### Agent and LLM docs
|
|
158
|
+
|
|
159
|
+
| Doc | Purpose |
|
|
160
|
+
| ---------------------------------------------------- | --------------------------------- |
|
|
161
|
+
| [AGENTS.md](AGENTS.md) | Agent-facing entry point. |
|
|
162
|
+
| [CLAUDE.md](CLAUDE.md) | Symlink to `AGENTS.md`. |
|
|
163
|
+
| [llms.txt](llms.txt) | LLM/AI index of docs and sources. |
|
|
164
|
+
| [skills/happenin/SKILL.md](skills/happenin/SKILL.md) | Cross-agent skill instructions. |
|
|
165
|
+
|
|
166
|
+
## Agent skill
|
|
167
|
+
|
|
168
|
+
Install the reusable `happenin` skill for agents that support `SKILL.md` files:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npx skills add YogliB/happenin --skill happenin
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Then ask the agent to record or inspect agent events. The skill covers `install`, `record`, `import`, `query`, and `dashboard`, plus field extraction and required hook responses.
|
|
175
|
+
|
|
176
|
+
See [skills/happenin/SKILL.md](skills/happenin/SKILL.md) for the full instructions.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
[MIT](LICENSE.md)
|
package/assets/help.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# happenin
|
|
2
|
+
|
|
3
|
+
A macOS CLI that records Cursor and Claude Code agent events to a local SQLite database and serves a live browser dashboard.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### `happenin install [--cursor] [--claude]`
|
|
8
|
+
|
|
9
|
+
Backs up and appends `happenin record` hooks to your Cursor and Claude Code configuration files.
|
|
10
|
+
|
|
11
|
+
- `--cursor` — install Cursor hooks only.
|
|
12
|
+
- `--claude` — install Claude Code hooks only.
|
|
13
|
+
|
|
14
|
+
By default both are installed. Backups are written to `~/.happenin/backups/`.
|
|
15
|
+
|
|
16
|
+
### `happenin record <source> [event]`
|
|
17
|
+
|
|
18
|
+
The hook target. Reads a JSON payload from stdin, writes it to `~/.happenin/happenin.db`, and prints the required non-blocking response for the agent.
|
|
19
|
+
|
|
20
|
+
- `source` — `cursor` or `claude`.
|
|
21
|
+
- `event` — event name, only required for Claude (Cursor payloads include `hook_event_name`).
|
|
22
|
+
|
|
23
|
+
This command is normally called by the agent hooks, not directly.
|
|
24
|
+
|
|
25
|
+
### `happenin import`
|
|
26
|
+
|
|
27
|
+
Imports existing transcripts:
|
|
28
|
+
|
|
29
|
+
- Claude Code JSONL from `~/.claude/projects/<project>/<session>.jsonl`.
|
|
30
|
+
- Cursor `prompt_history.json` and `meta.json` from `~/.cursor/chats/<hash>/<session>/`.
|
|
31
|
+
|
|
32
|
+
`store.db` is skipped because it is encrypted.
|
|
33
|
+
|
|
34
|
+
### `happenin query [options]`
|
|
35
|
+
|
|
36
|
+
Query events from the local database and print them as JSON, JSONL, or a summary.
|
|
37
|
+
|
|
38
|
+
- `--source <source>` — filter by source.
|
|
39
|
+
- `--event <event>` — filter by event name.
|
|
40
|
+
- `--session <id>` — filter by session id (partial match).
|
|
41
|
+
- `--q <text>` — search event payloads.
|
|
42
|
+
- `--since <id>` — events with an id greater than `<id>`.
|
|
43
|
+
- `--limit <n>` — maximum rows to return (default: `100`).
|
|
44
|
+
- `--offset <n>` — skip the first `<n>` rows.
|
|
45
|
+
- `--format <json|jsonl|summary>` — output format (default: `json`).
|
|
46
|
+
- `--db <path>` — database path (default: `HAPPENIN_DB` or `~/.happenin/happenin.db`).
|
|
47
|
+
|
|
48
|
+
### `happenin dashboard [--port <port>] [--no-open] [--silent]`
|
|
49
|
+
|
|
50
|
+
Starts a local HTTP server and opens the dashboard in your browser.
|
|
51
|
+
|
|
52
|
+
- `--port` — port to listen on (default: `8765`).
|
|
53
|
+
- `--no-open` — do not open the browser.
|
|
54
|
+
- `--silent` — alias for `--no-open`; used automatically by `npm start`.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
- `HAPPENIN_DB` — override the SQLite database path (default: `~/.happenin/happenin.db`).
|
|
59
|
+
- `NO_COLOR` — disable colored help output.
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
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(`
|
|
5
|
+
UPDATE events
|
|
6
|
+
SET
|
|
7
|
+
subagent_id = NULLIF(trim(json_extract(payload, '$.subagent_id')), ''),
|
|
8
|
+
subagent_type = NULLIF(trim(json_extract(payload, '$.subagent_type')), ''),
|
|
9
|
+
transcript_path = NULLIF(trim(json_extract(payload, '$.transcript_path')), ''),
|
|
10
|
+
session_id = COALESCE(
|
|
11
|
+
NULLIF(trim(json_extract(payload, '$.parent_conversation_id')), ''),
|
|
12
|
+
NULLIF(trim(json_extract(payload, '$.conversation_id')), ''),
|
|
13
|
+
NULLIF(trim(session_id), '')
|
|
14
|
+
)
|
|
15
|
+
WHERE
|
|
16
|
+
source = 'cursor'
|
|
17
|
+
AND event = 'subagentStart'
|
|
18
|
+
AND json_valid(payload) = 1
|
|
19
|
+
AND (
|
|
20
|
+
subagent_id IS NULL
|
|
21
|
+
OR subagent_type IS NULL
|
|
22
|
+
OR transcript_path IS NULL
|
|
23
|
+
);
|
|
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 (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
source TEXT NOT NULL,
|
|
27
|
+
client TEXT,
|
|
28
|
+
event TEXT,
|
|
29
|
+
session_id TEXT,
|
|
30
|
+
happened_at TEXT,
|
|
31
|
+
received_at INTEGER NOT NULL,
|
|
32
|
+
project_path TEXT,
|
|
33
|
+
file_path TEXT,
|
|
34
|
+
tool_name TEXT,
|
|
35
|
+
payload TEXT NOT NULL,
|
|
36
|
+
source_path TEXT,
|
|
37
|
+
subagent_id TEXT,
|
|
38
|
+
subagent_type TEXT,
|
|
39
|
+
transcript_path TEXT
|
|
40
|
+
);`),i.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
42
|
+
path TEXT PRIMARY KEY,
|
|
43
|
+
mtime INTEGER NOT NULL,
|
|
44
|
+
imported_at INTEGER NOT NULL
|
|
45
|
+
);
|
|
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(`
|
|
47
|
+
INSERT INTO events (
|
|
48
|
+
source,
|
|
49
|
+
client,
|
|
50
|
+
event,
|
|
51
|
+
session_id,
|
|
52
|
+
happened_at,
|
|
53
|
+
received_at,
|
|
54
|
+
project_path,
|
|
55
|
+
file_path,
|
|
56
|
+
tool_name,
|
|
57
|
+
payload,
|
|
58
|
+
source_path,
|
|
59
|
+
subagent_id,
|
|
60
|
+
subagent_type,
|
|
61
|
+
transcript_path
|
|
62
|
+
) 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=`
|
|
64
|
+
SELECT
|
|
65
|
+
id,
|
|
66
|
+
source,
|
|
67
|
+
client,
|
|
68
|
+
event,
|
|
69
|
+
session_id AS sessionId,
|
|
70
|
+
happened_at AS happenedAt,
|
|
71
|
+
received_at AS receivedAt,
|
|
72
|
+
project_path AS projectPath,
|
|
73
|
+
file_path AS filePath,
|
|
74
|
+
tool_name AS toolName,
|
|
75
|
+
payload,
|
|
76
|
+
source_path AS sourcePath,
|
|
77
|
+
subagent_id AS subagentId,
|
|
78
|
+
subagent_type AS subagentType,
|
|
79
|
+
transcript_path AS transcriptPath
|
|
80
|
+
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(`
|
|
82
|
+
INSERT INTO imports (path, mtime, imported_at)
|
|
83
|
+
VALUES (?, ?, ?)
|
|
84
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
85
|
+
mtime = excluded.mtime,
|
|
86
|
+
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,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function q(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}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>
|
|
93
|
+
<html lang="en" hx-ext="sse">
|
|
94
|
+
<head>
|
|
95
|
+
<meta charset="utf-8">
|
|
96
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
97
|
+
<title>happenin</title>
|
|
98
|
+
<script>
|
|
99
|
+
(function () {
|
|
100
|
+
const THEME_KEY = 'happenin-theme';
|
|
101
|
+
const FILTER_KEY = 'happenin-filters';
|
|
102
|
+
const DARK_MQL = window.matchMedia('(prefers-color-scheme: dark)');
|
|
103
|
+
|
|
104
|
+
function getItem(key) {
|
|
105
|
+
try {
|
|
106
|
+
return localStorage.getItem(key);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function setItem(key, value) {
|
|
113
|
+
try {
|
|
114
|
+
localStorage.setItem(key, value);
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getSystemTheme() {
|
|
119
|
+
return DARK_MQL.matches ? 'dark' : 'light';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getSavedTheme() {
|
|
123
|
+
const stored = getItem(THEME_KEY);
|
|
124
|
+
return stored === 'light' || stored === 'dark' ? stored : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getTheme() {
|
|
128
|
+
return getSavedTheme() || getSystemTheme();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyTheme(theme) {
|
|
132
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function updateThemeButton(theme) {
|
|
136
|
+
const button = document.querySelector('[data-theme-toggle]');
|
|
137
|
+
if (button) button.textContent = theme === 'dark' ? '🌙' : '☀️';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function initTheme() {
|
|
141
|
+
const theme = getTheme();
|
|
142
|
+
applyTheme(theme);
|
|
143
|
+
updateThemeButton(theme);
|
|
144
|
+
|
|
145
|
+
DARK_MQL.addEventListener('change', () => {
|
|
146
|
+
if (!getSavedTheme()) {
|
|
147
|
+
const next = getSystemTheme();
|
|
148
|
+
applyTheme(next);
|
|
149
|
+
updateThemeButton(next);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const button = document.querySelector('[data-theme-toggle]');
|
|
154
|
+
if (button) {
|
|
155
|
+
button.addEventListener('click', () => {
|
|
156
|
+
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
|
157
|
+
const next = current === 'dark' ? 'light' : 'dark';
|
|
158
|
+
setItem(THEME_KEY, next);
|
|
159
|
+
applyTheme(next);
|
|
160
|
+
updateThemeButton(next);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function loadFilters() {
|
|
166
|
+
const raw = getItem(FILTER_KEY);
|
|
167
|
+
if (!raw) return;
|
|
168
|
+
try {
|
|
169
|
+
const values = JSON.parse(raw);
|
|
170
|
+
const form = document.querySelector('[data-filter-form]');
|
|
171
|
+
if (!form) return;
|
|
172
|
+
for (const [name, value] of Object.entries(values)) {
|
|
173
|
+
const field = form.elements[name];
|
|
174
|
+
if (field && value != null && value !== '') {
|
|
175
|
+
field.value = String(value);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
|
|
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() {
|
|
188
|
+
const form = document.querySelector('[data-filter-form]');
|
|
189
|
+
const feed = document.getElementById('feed-items');
|
|
190
|
+
if (!form || !feed) return;
|
|
191
|
+
const data = new FormData(form);
|
|
192
|
+
const params = new URLSearchParams();
|
|
193
|
+
for (const [key, value] of data.entries()) {
|
|
194
|
+
if (value) params.set(key, String(value));
|
|
195
|
+
}
|
|
196
|
+
params.set('page', String(getCurrentPage()));
|
|
197
|
+
feed.setAttribute('hx-get', '/fragments/events' + (params.size > 0 ? '?' + params.toString() : ''));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function saveFilters(form) {
|
|
201
|
+
const data = new FormData(form);
|
|
202
|
+
const filters = {};
|
|
203
|
+
for (const [key, value] of data.entries()) {
|
|
204
|
+
if (value) filters[key] = String(value);
|
|
205
|
+
}
|
|
206
|
+
setItem(FILTER_KEY, JSON.stringify(filters));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function initFilters() {
|
|
210
|
+
const form = document.querySelector('[data-filter-form]');
|
|
211
|
+
if (!form) return;
|
|
212
|
+
loadFilters();
|
|
213
|
+
form.addEventListener('change', () => saveFilters(form));
|
|
214
|
+
form.addEventListener('input', (event) => {
|
|
215
|
+
if (event.target.classList.contains('search')) saveFilters(form);
|
|
216
|
+
});
|
|
217
|
+
updateFeedUrl();
|
|
218
|
+
}
|
|
219
|
+
|
|
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'));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
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
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function initFeedItems() {
|
|
242
|
+
const el = document.getElementById('feed-items');
|
|
243
|
+
if (el && window.Alpine) Alpine.initTree(el);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
document.addEventListener('htmx:beforeSwap', (event) => {
|
|
247
|
+
if (event.detail.target.id === 'feed-items') saveSessionState();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
document.addEventListener('htmx:afterSettle', (event) => {
|
|
251
|
+
if (event.detail.target.id === 'feed-items') restoreSessionState();
|
|
252
|
+
updateFeedUrl();
|
|
253
|
+
initFeedItems();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
document.addEventListener('alpine:initialized', initFeedItems);
|
|
257
|
+
|
|
258
|
+
applyTheme(getTheme());
|
|
259
|
+
|
|
260
|
+
if (document.readyState === 'loading') {
|
|
261
|
+
document.addEventListener('DOMContentLoaded', initTheme);
|
|
262
|
+
document.addEventListener('DOMContentLoaded', initFilters);
|
|
263
|
+
} else {
|
|
264
|
+
initTheme();
|
|
265
|
+
initFilters();
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
<\/script>
|
|
269
|
+
<style>
|
|
270
|
+
:root {
|
|
271
|
+
color-scheme: dark;
|
|
272
|
+
--bg: #0d0d0f;
|
|
273
|
+
--surface: #111216;
|
|
274
|
+
--surface-2: #14161b;
|
|
275
|
+
--surface-3: #16181d;
|
|
276
|
+
--surface-4: #1c1f26;
|
|
277
|
+
--surface-5: #252830;
|
|
278
|
+
--border: #23262d;
|
|
279
|
+
--border-2: #24282f;
|
|
280
|
+
--text: #e6e6e6;
|
|
281
|
+
--text-2: #9aa3b2;
|
|
282
|
+
--text-3: #9ca3af;
|
|
283
|
+
--text-4: #6b7280;
|
|
284
|
+
--accent: #60a5fa;
|
|
285
|
+
--accent-2: #c084fc;
|
|
286
|
+
}
|
|
287
|
+
[data-theme="light"] {
|
|
288
|
+
color-scheme: light;
|
|
289
|
+
--bg: #ffffff;
|
|
290
|
+
--surface: #f9fafb;
|
|
291
|
+
--surface-2: #f3f4f6;
|
|
292
|
+
--surface-3: #ffffff;
|
|
293
|
+
--surface-4: #f3f4f6;
|
|
294
|
+
--surface-5: #e5e7eb;
|
|
295
|
+
--border: #e5e7eb;
|
|
296
|
+
--border-2: #d1d5db;
|
|
297
|
+
--text: #111827;
|
|
298
|
+
--text-2: #6b7280;
|
|
299
|
+
--text-3: #6b7280;
|
|
300
|
+
--text-4: #9ca3af;
|
|
301
|
+
--accent: #2563eb;
|
|
302
|
+
--accent-2: #7c3aed;
|
|
303
|
+
}
|
|
304
|
+
* { 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); }
|
|
314
|
+
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; } }
|
|
345
|
+
</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>
|
|
349
|
+
</head>
|
|
350
|
+
<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>
|
|
360
|
+
</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>
|
|
373
|
+
</div>
|
|
374
|
+
</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]
|
|
376
|
+
|
|
377
|
+
Options:
|
|
378
|
+
--source <source> filter by source (cursor, claude, ...)
|
|
379
|
+
--event <event> filter by event name
|
|
380
|
+
--session <id> filter by session id (partial match)
|
|
381
|
+
--q <text> search event payloads
|
|
382
|
+
--since <id> events with id greater than <id>
|
|
383
|
+
--limit <n> maximum rows to return (default: 100)
|
|
384
|
+
--offset <n> skip first <n> rows
|
|
385
|
+
--format <format> output format: json (default), jsonl, summary
|
|
386
|
+
--db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
|
|
387
|
+
-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
|
|
389
|
+
|
|
390
|
+
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{};
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "happenin",
|
|
3
|
+
"version": "0.0.1",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"ai-agents",
|
|
8
|
+
"claude",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"cli",
|
|
11
|
+
"cursor",
|
|
12
|
+
"dashboard",
|
|
13
|
+
"logging",
|
|
14
|
+
"macos",
|
|
15
|
+
"sqlite"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/YogliB/happenin#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/YogliB/happenin/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "YogliB",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/YogliB/happenin.git"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"happenin": "dist/bin.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"assets",
|
|
32
|
+
"dist",
|
|
33
|
+
"LICENSE.md",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"type": "module",
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown --config tsdown.config.ts",
|
|
39
|
+
"dev": "tsdown --config tsdown.config.ts --watch",
|
|
40
|
+
"start": "node scripts/dev-server.js dashboard",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint": "oxlint --fix src",
|
|
43
|
+
"lint:ci": "oxlint src",
|
|
44
|
+
"format": "oxfmt --write .",
|
|
45
|
+
"format:ci": "oxfmt --check .",
|
|
46
|
+
"duplicates:ci": "jscpd --threshold 5 src",
|
|
47
|
+
"knip:ci": "knip",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:ci": "vitest run --coverage",
|
|
50
|
+
"prepare": "husky",
|
|
51
|
+
"prepublishOnly": "npm run build"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "24",
|
|
55
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
56
|
+
"eslint-plugin-security": "4.0.1",
|
|
57
|
+
"husky": "9.1.7",
|
|
58
|
+
"jscpd": "4.0.9",
|
|
59
|
+
"knip": "6.11.0",
|
|
60
|
+
"lint-staged": "17.1.1",
|
|
61
|
+
"oxfmt": "0.61.0",
|
|
62
|
+
"oxlint": "1.76.0",
|
|
63
|
+
"tsdown": "0.22.14",
|
|
64
|
+
"typescript": "7.0.2",
|
|
65
|
+
"vitest": "^4.1.10"
|
|
66
|
+
},
|
|
67
|
+
"lint-staged": {
|
|
68
|
+
"**/*": "oxfmt --write --no-error-on-unmatched-pattern",
|
|
69
|
+
"**/*.ts": "oxlint --fix"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=24.0.0"
|
|
73
|
+
}
|
|
74
|
+
}
|