@scrylog/cli 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +46 -7
  2. package/package.json +13 -10
  3. package/src/api/docs/sessions.ts +3 -2
  4. package/src/api/docs/system.ts +3 -0
  5. package/src/api/docs/usage.ts +2 -2
  6. package/src/api/routes/sessions.ts +6 -2
  7. package/src/api/routes/system.ts +4 -1
  8. package/src/api/routes/webhooks/claude.ts +9 -21
  9. package/src/api/routes/webhooks/opencode.ts +6 -1
  10. package/src/api/routes.ts +1 -1
  11. package/src/api/schemas/common.ts +8 -0
  12. package/src/api/schemas/sessions.ts +15 -1
  13. package/src/api/schemas/usage.ts +1 -0
  14. package/src/cli/commands.ts +170 -26
  15. package/src/cli/index.ts +18 -0
  16. package/src/cli/program.ts +175 -0
  17. package/src/daemon/collectors/claude/collector.ts +11 -1
  18. package/src/daemon/collectors/claude/discovery.ts +2 -23
  19. package/src/daemon/collectors/codex/collector.ts +3 -3
  20. package/src/daemon/collectors/git.ts +60 -2
  21. package/src/daemon/collectors/herdr/collector.ts +128 -0
  22. package/src/daemon/collectors/herdr/index.ts +2 -0
  23. package/src/daemon/collectors/herdr/normalization.ts +111 -0
  24. package/src/daemon/collectors/opencode/collector.ts +4 -2
  25. package/src/daemon/collectors/opencode-db.ts +17 -5
  26. package/src/daemon/collectors/tmux/collector.ts +21 -1
  27. package/src/daemon/collectors/tmux/enrichment.ts +1 -0
  28. package/src/daemon/collectors/usage.ts +115 -49
  29. package/src/daemon/index.ts +2 -0
  30. package/src/daemon/store.ts +109 -14
  31. package/src/postinstall.cjs +11 -0
  32. package/src/shared/config.ts +33 -0
  33. package/src/shared/session-exclusions.ts +5 -0
  34. package/src/shared/session-view.ts +67 -6
  35. package/src/shared/terminal-capabilities.ts +130 -0
  36. package/src/shared/types.ts +33 -2
  37. package/src/shared/version.ts +44 -0
  38. package/src/tui/App.tsx +240 -0
  39. package/src/tui/assets/providers/SOURCES.md +21 -0
  40. package/src/tui/assets/providers/claude-terminal.png +0 -0
  41. package/src/tui/assets/providers/claude-terminal.svg +62 -0
  42. package/src/tui/assets/providers/claude.png +0 -0
  43. package/src/tui/assets/providers/copilot.png +0 -0
  44. package/src/tui/assets/providers/copilot.svg +44 -0
  45. package/src/tui/assets/providers/openai-terminal.png +0 -0
  46. package/src/tui/assets/providers/openai-terminal.svg +62 -0
  47. package/src/tui/assets/providers/openai.png +0 -0
  48. package/src/tui/assets/providers/opencode-terminal.png +0 -0
  49. package/src/tui/assets/providers/opencode-terminal.svg +62 -0
  50. package/src/tui/assets/providers/opencode.png +0 -0
  51. package/src/tui/assets/providers/shell.png +0 -0
  52. package/src/tui/assets/providers/shell.svg +23 -0
  53. package/src/tui/assets/providers/unknown.png +0 -0
  54. package/src/tui/assets/providers/unknown.svg +23 -0
  55. package/src/tui/client.ts +22 -8
  56. package/src/tui/components/BindingsModal.tsx +82 -0
  57. package/src/tui/components/DashboardHeader.tsx +256 -0
  58. package/src/tui/components/KeyBar.tsx +67 -0
  59. package/src/tui/components/SessionDetailModal.tsx +169 -0
  60. package/src/tui/components/SessionsView.tsx +184 -0
  61. package/src/tui/components/TabBar.tsx +40 -0
  62. package/src/tui/components/UsageSummaryPane.tsx +272 -0
  63. package/src/tui/components/UsageView.tsx +242 -0
  64. package/src/tui/components/tool-icons.ts +24 -0
  65. package/src/tui/index.tsx +106 -0
  66. package/src/tui/keybindings.ts +49 -0
  67. package/src/tui/model.ts +295 -0
  68. package/src/tui/state.ts +77 -0
  69. package/src/tui/theme.ts +110 -0
  70. package/src/usage/README.md +3 -3
  71. package/src/usage/clients/anthropic-oauth-usage.ts +100 -20
  72. package/src/usage/clients/anthropic-web-usage.ts +133 -49
  73. package/src/usage/clients/claude-cli-probe.ts +458 -132
  74. package/src/usage/clients/codex-app-server.ts +354 -0
  75. package/src/usage/clients/codex-oauth-usage.ts +143 -16
  76. package/src/usage/clients/opencode-go-api.ts +100 -0
  77. package/src/usage/clients/opencode-go-usage.ts +163 -27
  78. package/src/usage/errors.ts +33 -0
  79. package/src/usage/index.ts +2 -1
  80. package/src/usage/local/opencode-go-local.ts +194 -0
  81. package/src/usage/mappers.ts +24 -15
  82. package/src/usage/planner.ts +89 -25
  83. package/src/usage/providers/claude.ts +203 -83
  84. package/src/usage/providers/codex.ts +92 -11
  85. package/src/usage/providers/opencode-go.ts +94 -16
  86. package/src/usage/resolvers/browser-cookies.ts +679 -69
  87. package/src/usage/resolvers/claude-auth.ts +150 -41
  88. package/src/usage/resolvers/claude-desktop-auth.ts +170 -0
  89. package/src/usage/resolvers/claude-web.ts +72 -6
  90. package/src/usage/resolvers/codex-auth.ts +74 -17
  91. package/src/usage/resolvers/keychain-coordinator.ts +397 -0
  92. package/src/usage/resolvers/opencode-go-web.ts +47 -6
  93. package/src/usage/types.ts +37 -2
  94. package/src/usage/web-session-cache.ts +37 -0
  95. package/src/neo-blessed.d.ts +0 -4
  96. package/src/tui/app.ts +0 -920
  97. package/src/tui/index.ts +0 -193
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # scrylog
2
2
 
3
- `scrylog` is a Bun-based terminal dashboard and daemon for monitoring AI coding sessions in `tmux` and as standalone processes.
3
+ `scrylog` is a Bun-based terminal dashboard and daemon for monitoring AI coding sessions in `tmux`, herdr, and as standalone processes.
4
4
 
5
5
  It ships four commands:
6
6
 
@@ -14,12 +14,14 @@ It ships four commands:
14
14
  ## Features
15
15
 
16
16
  - `tmux` monitoring for OpenCode, Codex, Copilot, and shell panes
17
+ - herdr default-session monitoring through its structured agent and pane API
17
18
  - OpenCode integration in `hooks` or `sse` mode
18
19
  - Claude transcript collection
19
20
  - Codex transcript collection and hook-based state tracking
20
21
  - Local daemon with Unix socket IPC on macOS and Linux, TCP fallback on Windows
21
22
  - Optional TCP REST API exposure (enabled by default on Windows)
22
23
  - Optional MCP HTTP exposure with bearer token support
24
+ - Responsive OpenTUI dashboard with Sessions and provider-usage views
23
25
  - User-service installation on macOS and Linux
24
26
  - Interactive setup wizard and diagnostics
25
27
 
@@ -27,6 +29,7 @@ It ships four commands:
27
29
 
28
30
  - [Bun](https://bun.sh) `>= 1.3.0`
29
31
  - `tmux` if you want `tmux` monitoring
32
+ - [herdr](https://herdr.dev) if you want herdr monitoring; absence is handled automatically
30
33
  - OpenCode if you want OpenCode monitoring
31
34
  - `ps` on Unix or `wmic` on Windows for process auto-discovery
32
35
 
@@ -38,6 +41,35 @@ It ships four commands:
38
41
  | Tier 2 | Linux / WSL 2 | Compatible | Claude OAuth via `~/.claude/.credentials.json`, Chrome/Chromium cookies, systemd service when available |
39
42
  | Tier 3 | Windows (native) | Experimental / not recommended | TCP IPC, `wmic` process scan, degraded service status. Chrome cookie decryption not available (DPAPI not implemented). Primary use is ad-hoc debugging, not daily monitoring. |
40
43
 
44
+ ## Terminal Icon Rendering
45
+
46
+ The Usage view renders provider icons with one of these values for `tui.providerIcons`:
47
+
48
+ | Mode | Behavior |
49
+ | --- | --- |
50
+ | `blocks` (default) | Bundled pixel-art PNGs rendered with Unicode half-blocks on the Usage cards. Works in any terminal, including multiplexers. |
51
+ | `graphics` | Full artwork. Probes the terminal (`XTVERSION`, kitty graphics query, Sixel support) and falls back to `blocks` when nothing is confirmed. |
52
+ | `kitty` / `sixel` | Force a specific image protocol even when detection does not confirm it (useful for terminals whose detection response is ambiguous). |
53
+ | `text` | Two-letter provider monogram on Usage cards. Never uses images. |
54
+
55
+ In the sessions list the Tool column also depends on the mode: image modes render 2x1-cell mini icons, `blocks` shows the colored brand monogram (`CL`, `CX`, `OC`, ...) plus child-session badges, and `text` keeps the full tool name. Press `?` in the dashboard to see a legend mapping each icon or monogram to its tool name (legend shown only when an image mode is active).
56
+
57
+ Compatibility of `graphics` by environment:
58
+
59
+ | Environment | Works | Detail |
60
+ | --- | --- | --- |
61
+ | Ghostty | Yes | Kitty graphics protocol (`TERM=xterm-ghostty`) |
62
+ | Kitty | Yes | Kitty graphics protocol (reference implementation) |
63
+ | WezTerm | Yes | Kitty graphics and Sixel |
64
+ | iTerm2 | Yes | Inline images and Sixel |
65
+ | Konsole, Foot | Yes | Sixel, requires pixel-resolution detection to confirm |
66
+ | Direct SSH (no multiplexer) | Yes | Inherits the local terminal's protocol support |
67
+ | tmux / herdr | Forced `blocks` | The multiplexer sits between the client and the terminal; passthrough is not reliable, so rendering degrades to `blocks` |
68
+ | Alacritty, Terminal.app, GNOME Terminal | No | No image protocol; falls back to `blocks` |
69
+ | Windows native (Tier 3) | No | Sixel support is partial/experimental; falls back to `blocks` |
70
+
71
+ Run `scrylog doctor` for a "Terminal graphics" section with detected capabilities and the recommended value for the current terminal.
72
+
41
73
  ## WSL 2 Notes
42
74
 
43
75
  - The daemon runs as a Linux process and uses the same paths as native Linux.
@@ -130,8 +162,11 @@ For most users on macOS or Linux:
130
162
  ```bash
131
163
  scrylog
132
164
  scrylog --config ~/.config/scrylog/config.json
165
+ scrylog --include-unknown
133
166
  ```
134
167
 
168
+ By default, sessions whose tool could not be identified (`tool: "unknown"`) are hidden from the dashboard and API. Launch with `--include-unknown` to request them from the daemon for the current run.
169
+
135
170
  Keyboard shortcuts:
136
171
 
137
172
  | Key | Action |
@@ -139,7 +174,7 @@ Keyboard shortcuts:
139
174
  | `↑` / `k` | Move selection up |
140
175
  | `↓` / `j` | Move selection down |
141
176
  | `Enter` | Open session detail |
142
- | `t` | Attach to selected `tmux` session |
177
+ | `t` | Attach to selected `tmux` or herdr session |
143
178
  | `/` | Search |
144
179
  | `f` | Cycle filters |
145
180
  | `a` | Show actionable sessions only |
@@ -299,7 +334,8 @@ Example config:
299
334
  "refreshMs": 5000,
300
335
  "showStandalone": true,
301
336
  "usePushUpdates": true,
302
- "standaloneMaxAgeHours": 6
337
+ "standaloneMaxAgeHours": 6,
338
+ "providerIcons": "blocks"
303
339
  },
304
340
  "mcp": {
305
341
  "stdioEnabled": true,
@@ -336,7 +372,10 @@ Notes:
336
372
  - `daemon.commandTimeoutMs` bounds daemon-owned commands such as `ps`, `tmux`, `git`, `lsof`, and `pgrep`.
337
373
  - `daemon.networkTimeoutMs` bounds daemon-owned usage/integration network requests.
338
374
  - `usage.enabled: false` disables periodic Claude/OpenCode usage probes without disabling session monitoring.
375
+ - In the dashboard, use `Tab`, `u`, or `s` to switch between Sessions and Usage. Every usage window uses the complete card width. When provider cards exceed the terminal height, scroll with `↑`/`↓`, `j`/`k`, Page Up/Page Down, or the mouse wheel. `tui.providerIcons` accepts `blocks` (portable pixel-art PNGs, the default), `graphics` (official artwork with automatic kitty/Sixel negotiation), `kitty` or `sixel` (force a specific image protocol), or `text` (two-letter monogram); all assets are bundled for offline rendering. See [Terminal icon rendering](#terminal-icon-rendering) for environment compatibility.
339
376
  - `retention` limits in-memory state. Active `working` and `waiting-input` sessions are preserved even when old.
377
+ - `retention` applies only to standalone sessions: tmux and herdr panes are never pruned by age or count, and `tool: "unknown"` sessions do not consume `retention.maxSessions` capacity (unknown standalone sessions are still pruned by age).
378
+ - Native agent collectors remain authoritative for state and summaries, but matching sessions retain `source: "herdr"` and herdr attach metadata while their pane remains in a successful herdr snapshot. Tmux takes precedence for nested sessions.
340
379
  - The daemon log file is always JSONL and rotates by size: `scrylogd.log`, `scrylogd.1.log`, `scrylogd.2.log` by default.
341
380
  - The current CLI preserves an existing legacy config path when updating config, so older `~/.scrylog.json` users are not forced onto a new file immediately.
342
381
 
@@ -346,7 +385,7 @@ Notes:
346
385
 
347
386
  | Setting | Default | Effect |
348
387
  | --- | ---: | --- |
349
- | `retention.maxSessions` | `500` | Prunes oldest inactive sessions above this count |
388
+ | `retention.maxSessions` | `500` | Prunes oldest inactive identified standalone sessions above this count (tmux/herdr/unknown rows never count) |
350
389
  | `retention.standaloneMaxAgeHours` | `168` | Prunes inactive standalone sessions older than 7 days |
351
390
  | `retention.cacheMaxEntries` | `1000` | Bounds internal path/project caches |
352
391
  | `retention.transcriptMaxBytes` | `10485760` | Parses only a bounded tail for very large Claude transcripts |
@@ -567,8 +606,8 @@ Endpoints:
567
606
  | Method | Path | Returns |
568
607
  | --- | --- | --- |
569
608
  | `GET` | `/api/health` | daemon health, timestamps, collector status |
570
- | `GET` | `/api/status` | visible counts, collector status, generated timestamp |
571
- | `GET` | `/api/sessions` | visible session tree plus counts |
609
+ | `GET` | `/api/status` | visible counts, collector status, generated timestamp (`?includeUnknown=true` opts in) |
610
+ | `GET` | `/api/sessions` | visible session tree plus counts (`?includeUnknown=true` opts in; unknown-tool rows hidden by default) |
572
611
  | `GET` | `/api/sessions/:id` | full session detail |
573
612
  | `GET` | `/api/sessions/:id/children` | visible child sessions |
574
613
  | `GET` | `/api/sessions/:id/messages` | recent messages when available |
@@ -834,7 +873,7 @@ Publish-readiness checks:
834
873
  bun run release:check
835
874
  ```
836
875
 
837
- Public packages are versioned with Changesets and validated through Turborepo. The release workflow creates a version PR on `main`; merging that PR publishes `@scrylog/cli` and `@scrylog/opencode-plugin` to npm.
876
+ Public packages are versioned with Changesets and validated through Turborepo. Manually running the Release workflow in `version` mode creates a version PR on `main`; merging that PR publishes `@scrylog/cli` and `@scrylog/opencode-plugin` to npm.
838
877
 
839
878
  ## OpenSpec
840
879
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrylog/cli",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal dashboard for monitoring tmux sessions running AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "bun": ">=1.3.0"
32
32
  },
33
33
  "bin": {
34
- "scrylog": "src/tui/index.ts",
34
+ "scrylog": "src/cli/index.ts",
35
35
  "scrylogd": "src/daemon/index.ts",
36
36
  "scrylog-mcp": "src/mcp/index.ts",
37
37
  "scrylog-claude-hook": "src/hooks/claude.ts",
@@ -49,29 +49,32 @@
49
49
  "scripts": {
50
50
  "clean": "rm -rf .turbo && rm -rf dist",
51
51
  "clean:node-modules": "rm -rf node_modules",
52
- "dev": "bun run src/tui/index.ts",
53
- "dev:daemon": "bun run src/daemon/index.ts",
54
- "start": "bun run src/tui/index.ts",
52
+ "dev": "bun run src/cli/index.ts",
53
+ "dev:daemon": "bun run src/daemon/index.ts --loglevel debug",
54
+ "start": "bun run src/cli/index.ts",
55
+ "postinstall": "bun src/postinstall.cjs",
55
56
  "check-types": "tsc --noEmit",
56
57
  "test": "bun test",
57
- "prepublishOnly": "bun run ../../scripts/resolve-catalogs.ts",
58
- "postpublish": "bun run ../../scripts/restore-catalogs.ts",
59
- "pack:dry-run": "bun run ../../scripts/resolve-catalogs.ts && npm pack --dry-run && bun run ../../scripts/restore-catalogs.ts"
58
+ "pack:dry-run": "bash ../../scripts/npm-pack.sh \"$PWD\" \"\" --dry-run"
60
59
  },
61
60
  "dependencies": {
62
61
  "@hono/zod-openapi": "^1.3.0",
62
+ "@joeldmtz/prtl-core": "^0.3.0",
63
63
  "@modelcontextprotocol/sdk": "^1.0.0",
64
+ "@opentui/core": "0.5.11",
65
+ "@opentui/react": "0.5.11",
64
66
  "@opencode-ai/sdk": "1.4.10",
65
67
  "@scalar/hono-api-reference": "^0.10.11",
66
68
  "@scalar/openapi-to-markdown": "^0.5.9",
69
+ "commander": "15.0.0",
67
70
  "hono": "^4.12.15",
68
- "neo-blessed": "^0.2.0",
71
+ "react": "19.2.7",
69
72
  "zod": "^4.3.6"
70
73
  },
71
74
  "devDependencies": {
72
- "@types/blessed": "^0.1.27",
73
75
  "@types/bun": "^1.3.14",
74
76
  "@types/node": "^24.3.0",
77
+ "@types/react": "19.2.17",
75
78
  "typescript": "5.9.3"
76
79
  }
77
80
  }
@@ -1,5 +1,5 @@
1
1
  import type { RouteConfig } from "@hono/zod-openapi";
2
- import { ApiErrorSchema, SessionIdParamSchema } from "../schemas/common";
2
+ import { ApiErrorSchema, IncludeUnknownQuerySchema, SessionIdParamSchema } from "../schemas/common";
3
3
  import {
4
4
  SessionsResponseSchema,
5
5
  SessionDetailResponseSchema,
@@ -16,6 +16,7 @@ export const SessionRoutes = {
16
16
  summary: "List sessions",
17
17
  description: "Returns the full session tree with counts.",
18
18
  security: [{ BearerAuth: [] }],
19
+ request: { query: IncludeUnknownQuerySchema },
19
20
  responses: {
20
21
  200: {
21
22
  content: { "application/json": { schema: SessionsResponseSchema } },
@@ -29,7 +30,7 @@ export const SessionRoutes = {
29
30
  tags: ["Sessions"],
30
31
  summary: "List session children",
31
32
  security: [{ BearerAuth: [] }],
32
- request: { params: SessionIdParamSchema },
33
+ request: { params: SessionIdParamSchema, query: IncludeUnknownQuerySchema },
33
34
  responses: {
34
35
  200: {
35
36
  content: { "application/json": { schema: SessionChildrenResponseSchema } },
@@ -1,5 +1,6 @@
1
1
  import { z } from "@hono/zod-openapi";
2
2
  import type { RouteConfig } from "@hono/zod-openapi";
3
+ import { IncludeUnknownQuerySchema } from "../schemas/common";
3
4
  import { SystemRouteKey } from "./system.keys";
4
5
 
5
6
  export const SystemRoutes = {
@@ -14,6 +15,7 @@ export const SystemRoutes = {
14
15
  "application/json": {
15
16
  schema: z.object({
16
17
  ok: z.literal(true),
18
+ version: z.string(),
17
19
  startedAt: z.number(),
18
20
  refreshedAt: z.number(),
19
21
  collectors: z.record(z.string(), z.unknown()),
@@ -30,6 +32,7 @@ export const SystemRoutes = {
30
32
  tags: ["System"],
31
33
  summary: "Session counts and collector status",
32
34
  security: [{ BearerAuth: [] }],
35
+ request: { query: IncludeUnknownQuerySchema },
33
36
  responses: {
34
37
  200: {
35
38
  content: {
@@ -8,7 +8,7 @@ export const UsageRoutes = {
8
8
  method: "get",
9
9
  path: "/usage",
10
10
  tags: ["Usage"],
11
- summary: "Get subscription usage limits",
11
+ summary: "Get subscription usage and extra-use credits",
12
12
  security: [{ BearerAuth: [] }],
13
13
  responses: {
14
14
  200: {
@@ -17,7 +17,7 @@ export const UsageRoutes = {
17
17
  schema: UsageResponseSchema,
18
18
  },
19
19
  },
20
- description: "Current usage data for all configured providers",
20
+ description: "Current limits, extra-usage spend, and balances for configured providers",
21
21
  },
22
22
  },
23
23
  },
@@ -10,18 +10,22 @@ export function createSessionsRouter(store: DaemonStore, config: ScrylogConfig):
10
10
  const router = new OpenAPIHono();
11
11
 
12
12
  router.openapi(defineRoute(SessionRouteKey.LIST_SESSIONS), (c) => {
13
- const view = buildSessionView(store.getSessions(), config);
13
+ const { includeUnknown } = c.req.valid("query");
14
+ const view = buildSessionView(store.getSessions(), config, Date.now(), { includeUnknown: includeUnknown === "true" });
14
15
  return c.json({ sessions: view.sessions, counts: view.counts, generatedAt: store.getState().refreshedAt }, 200);
15
16
  });
16
17
 
17
18
  // Must be registered before /:id to avoid the wildcard swallowing the path
18
19
  router.openapi(defineRoute(SessionRouteKey.SESSION_CHILDREN), (c) => {
19
20
  const { id } = c.req.valid("param");
21
+ const { includeUnknown } = c.req.valid("query");
20
22
  const parent = store.getSession(id);
21
23
  if (!parent) {
22
24
  throw new NotFoundError("Session", id);
23
25
  }
24
- const children = buildSessionView(store.getSessions(), config).sessions.find((s) => s.id === id)?.children ?? [];
26
+ const children = buildSessionView(store.getSessions(), config, Date.now(), {
27
+ includeUnknown: includeUnknown === "true",
28
+ }).sessions.find((s) => s.id === id)?.children ?? [];
25
29
  return c.json({ sessionId: id, children }, 200);
26
30
  });
27
31
 
@@ -6,6 +6,7 @@ import type { Logger } from "../../shared/logger";
6
6
  import { buildSessionView } from "../../shared/session-view";
7
7
  import { SystemRouteKey } from "../docs/system.keys";
8
8
  import { defineRoute } from "../lib/route";
9
+ import { getInstalledVersion } from "../../shared/version";
9
10
 
10
11
  export function createSystemRouter(store: DaemonStore, config: ScrylogConfig, logger: Logger): OpenAPIHono {
11
12
  const router = new OpenAPIHono();
@@ -14,6 +15,7 @@ export function createSystemRouter(store: DaemonStore, config: ScrylogConfig, lo
14
15
  const state = store.getState();
15
16
  return c.json({
16
17
  ok: true as const,
18
+ version: getInstalledVersion(),
17
19
  startedAt: state.startedAt,
18
20
  refreshedAt: state.refreshedAt,
19
21
  collectors: state.collectorStatus,
@@ -22,7 +24,8 @@ export function createSystemRouter(store: DaemonStore, config: ScrylogConfig, lo
22
24
 
23
25
  router.openapi(defineRoute(SystemRouteKey.STATUS), (c) => {
24
26
  const state = store.getState();
25
- const view = buildSessionView(state.sessions, config);
27
+ const { includeUnknown } = c.req.valid("query");
28
+ const view = buildSessionView(state.sessions, config, Date.now(), { includeUnknown: includeUnknown === "true" });
26
29
  return c.json({
27
30
  counts: view.counts,
28
31
  collectorStatus: state.collectorStatus,
@@ -7,6 +7,7 @@ import { isMetadataOnlyEvent } from "../../services/claude-session";
7
7
  import { ValidationError } from "../../lib/errors";
8
8
  import { WebhookRouteKey } from "../../docs/webhooks.keys";
9
9
  import { defineRoute } from "../../lib/route";
10
+ import { isKnownClaudeProbePath } from "../../../shared/session-exclusions";
10
11
 
11
12
  export function createClaudeWebhookRouter(store: DaemonStore, logger: Logger, gitCollector?: GitCollector): OpenAPIHono {
12
13
  const router = new OpenAPIHono();
@@ -44,29 +45,16 @@ export function createClaudeWebhookRouter(store: DaemonStore, logger: Logger, gi
44
45
  }
45
46
 
46
47
  const storeId = `claude:${sessionId}`;
47
- if (eventType === "SessionEnd") {
48
+ if (isKnownClaudeProbePath(currentPath) || isKnownClaudeProbePath(transcriptPath)) {
48
49
  store.markLocalCommand(sessionId);
49
- const existing = store.getSession(storeId);
50
- if (existing) {
51
- store.upsertSession({
52
- ...existing,
53
- state: "idle",
54
- hasAttention: false,
55
- summary: summary ?? existing.summary ?? "Claude session ended",
56
- lastActivity: lastActivity ?? Date.now(),
57
- detectionSource: "claude-hook",
58
- claude: {
59
- sessionId,
60
- transcriptPath: transcriptPath ?? existing.claude?.transcriptPath ?? "",
61
- ...(eventType !== undefined ? { hookEventType: eventType } : {}),
62
- ...(eventTime !== undefined ? { hookEventTime: eventTime } : {}),
63
- ...(hookSeq !== undefined ? { hookSeq } : {}),
64
- ...(endReason !== undefined ? { endReason } : {}),
65
- ...(errorType !== undefined ? { errorType } : {}),
66
- },
67
- });
68
- }
50
+ store.removeSession(storeId);
51
+ logger.debug("webhook", "claude probe excluded", { event: "session.excluded", tool: "claude", sessionId });
52
+ return c.json({ ok: true as const, skipped: "claude-probe" }, 200);
53
+ }
69
54
 
55
+ if (eventType === "SessionEnd") {
56
+ store.markLocalCommand(sessionId);
57
+ store.removeSession(storeId);
70
58
  logger.info("webhook", "claude session ended", { event: "session.ended", tool: "claude", sessionId, reason: endReason });
71
59
  return c.json({ ok: true as const }, 200);
72
60
  }
@@ -1,6 +1,7 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { ScrylogConfig } from "../../../shared/config";
3
3
  import type { DaemonStore } from "../../../daemon/store";
4
+ import type { GitCollector } from "../../../daemon/collectors/git";
4
5
  import type { Logger } from "../../../shared/logger";
5
6
  import type { SessionModel } from "../../../shared/types";
6
7
  import type { OpencodeWebhookPayload } from "../../models/opencode";
@@ -8,7 +9,7 @@ import { mapHookState, extractSessionParentId } from "../../services/opencode-se
8
9
  import { WebhookRouteKey } from "../../docs/webhooks.keys";
9
10
  import { defineRoute } from "../../lib/route";
10
11
 
11
- export function createOpencodeWebhookRouter(store: DaemonStore, config: ScrylogConfig, logger: Logger): OpenAPIHono {
12
+ export function createOpencodeWebhookRouter(store: DaemonStore, config: ScrylogConfig, logger: Logger, gitCollector?: GitCollector): OpenAPIHono {
12
13
  const router = new OpenAPIHono();
13
14
 
14
15
  router.openapi(defineRoute(WebhookRouteKey.WEBHOOK_OPENCODE), async (c) => {
@@ -46,6 +47,7 @@ export function createOpencodeWebhookRouter(store: DaemonStore, config: ScrylogC
46
47
 
47
48
  const title = payload.info?.title ?? payload.title ?? existing?.summary ?? "OpenCode session";
48
49
  const directory = payload.info?.directory ?? payload.directory ?? existing?.currentPath;
50
+ const resolvedProject = directory && gitCollector ? gitCollector.resolve(directory) : undefined;
49
51
  const parentSessionId = extractSessionParentId(event, payload, existing);
50
52
  const timeUpdated = payload.info?.time?.updated ?? Date.now();
51
53
 
@@ -66,8 +68,10 @@ export function createOpencodeWebhookRouter(store: DaemonStore, config: ScrylogC
66
68
  hasAttention,
67
69
  summary: title,
68
70
  currentPath: directory ?? existing.currentPath,
71
+ project: resolvedProject ?? existing.project,
69
72
  parentSessionId,
70
73
  lastActivity: stateChanged ? Date.now() : Math.max(existing.lastActivity, timeUpdated),
74
+ detectionSource: "opencode-hook",
71
75
  opencode: {
72
76
  ...existing.opencode,
73
77
  sessionId: rawId,
@@ -88,6 +92,7 @@ export function createOpencodeWebhookRouter(store: DaemonStore, config: ScrylogC
88
92
  detectionSource: "opencode-hook",
89
93
  hasAttention,
90
94
  currentPath: directory,
95
+ project: resolvedProject,
91
96
  parentSessionId,
92
97
  opencode: {
93
98
  sessionId: rawId,
package/src/api/routes.ts CHANGED
@@ -51,7 +51,7 @@ export function createApiApp(
51
51
  app.route("/api", createSystemRouter(store, config, logger));
52
52
  app.route("/api", createSessionsRouter(store, config));
53
53
  app.route("/api", createUsageRouter(store));
54
- app.route("/api/webhook", createOpencodeWebhookRouter(store, config, logger));
54
+ app.route("/api/webhook", createOpencodeWebhookRouter(store, config, logger, gitCollector));
55
55
  app.route("/api/webhook", createClaudeWebhookRouter(store, logger, gitCollector));
56
56
  app.route("/api/webhook", createCodexWebhookRouter(store, logger, gitCollector));
57
57
 
@@ -19,3 +19,11 @@ export const SessionIdParamSchema = z.object({
19
19
  .regex(/^[a-zA-Z0-9:_\-.]+$/, "Invalid session ID")
20
20
  .openapi({ param: { name: "id", in: "path" }, example: "claude:abc-123" }),
21
21
  });
22
+
23
+ export const IncludeUnknownQuerySchema = z.object({
24
+ includeUnknown: z.enum(["true", "false"]).optional().openapi({
25
+ param: { name: "includeUnknown", in: "query" },
26
+ example: "true",
27
+ description: "Include sessions whose tool could not be identified",
28
+ }),
29
+ });
@@ -17,11 +17,23 @@ const TmuxInfoSchema = z.object({
17
17
  session: z.string(),
18
18
  window: z.string(),
19
19
  pane: z.string(),
20
+ windowName: z.string().optional(),
21
+ });
22
+
23
+ const HerdrInfoSchema = z.object({
24
+ paneId: z.string(),
25
+ terminalId: z.string().optional(),
26
+ workspaceId: z.string().optional(),
27
+ workspace: z.string().optional(),
28
+ tabId: z.string().optional(),
29
+ tab: z.string().optional(),
30
+ name: z.string().optional(),
31
+ agent: z.string().optional(),
20
32
  });
21
33
 
22
34
  export const SessionListItemSchema = z.object({
23
35
  id: z.string(),
24
- source: z.enum(["tmux", "standalone"]),
36
+ source: z.enum(["tmux", "herdr", "standalone"]),
25
37
  tool: SessionToolSchema,
26
38
  state: SessionStateSchema,
27
39
  summary: z.string(),
@@ -30,8 +42,10 @@ export const SessionListItemSchema = z.object({
30
42
  hasAttention: z.boolean().optional(),
31
43
  detectionSource: z.string().optional(),
32
44
  parentSessionId: z.string().optional(),
45
+ displayName: z.string().optional(),
33
46
  project: ProjectInfoSchema.optional(),
34
47
  tmux: TmuxInfoSchema.optional(),
48
+ herdr: HerdrInfoSchema.optional(),
35
49
  currentPath: z.string().optional(),
36
50
  activeTool: z.string().optional(),
37
51
  childCount: z.number().optional(),
@@ -16,6 +16,7 @@ export const ProviderCostSnapshotSchema = z.object({
16
16
  limit: z.number(),
17
17
  currency: z.string(),
18
18
  period: z.string(),
19
+ balance: z.number().optional(),
19
20
  resetsAt: z.string().datetime().nullable().optional(),
20
21
  updatedAt: z.string().datetime(),
21
22
  });