@scrylog/cli 0.1.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +836 -0
  3. package/package.json +75 -0
  4. package/src/api/docs/sessions.keys.ts +6 -0
  5. package/src/api/docs/sessions.ts +81 -0
  6. package/src/api/docs/system.keys.ts +5 -0
  7. package/src/api/docs/system.ts +66 -0
  8. package/src/api/docs/usage.keys.ts +3 -0
  9. package/src/api/docs/usage.ts +24 -0
  10. package/src/api/docs/webhooks.keys.ts +5 -0
  11. package/src/api/docs/webhooks.ts +78 -0
  12. package/src/api/index.ts +18 -0
  13. package/src/api/lib/auth.ts +43 -0
  14. package/src/api/lib/errors.ts +28 -0
  15. package/src/api/lib/route.ts +18 -0
  16. package/src/api/models/claude.ts +4 -0
  17. package/src/api/models/codex.ts +4 -0
  18. package/src/api/models/opencode.ts +11 -0
  19. package/src/api/routes/sessions.ts +47 -0
  20. package/src/api/routes/system.ts +103 -0
  21. package/src/api/routes/usage.ts +15 -0
  22. package/src/api/routes/webhooks/claude.ts +161 -0
  23. package/src/api/routes/webhooks/codex.ts +175 -0
  24. package/src/api/routes/webhooks/opencode.ts +107 -0
  25. package/src/api/routes.ts +93 -0
  26. package/src/api/schemas/common.ts +21 -0
  27. package/src/api/schemas/sessions.ts +72 -0
  28. package/src/api/schemas/usage.ts +50 -0
  29. package/src/api/schemas/webhooks/claude.ts +32 -0
  30. package/src/api/schemas/webhooks/codex.ts +34 -0
  31. package/src/api/schemas/webhooks/opencode.ts +29 -0
  32. package/src/api/services/claude-session.ts +9 -0
  33. package/src/api/services/codex-session.ts +8 -0
  34. package/src/api/services/opencode-session.ts +73 -0
  35. package/src/classifier.ts +206 -0
  36. package/src/cli/commands.ts +498 -0
  37. package/src/cli/prompt.ts +44 -0
  38. package/src/daemon/cli.ts +27 -0
  39. package/src/daemon/collectors/claude/README.md +108 -0
  40. package/src/daemon/collectors/claude/collector.ts +413 -0
  41. package/src/daemon/collectors/claude/discovery.ts +148 -0
  42. package/src/daemon/collectors/claude/hooks.ts +181 -0
  43. package/src/daemon/collectors/claude/index.ts +8 -0
  44. package/src/daemon/collectors/claude/jsonl.ts +85 -0
  45. package/src/daemon/collectors/claude/transcript.ts +246 -0
  46. package/src/daemon/collectors/codex/README.md +147 -0
  47. package/src/daemon/collectors/codex/collector.ts +661 -0
  48. package/src/daemon/collectors/codex/discovery.ts +104 -0
  49. package/src/daemon/collectors/codex/hooks.ts +179 -0
  50. package/src/daemon/collectors/codex/index.ts +7 -0
  51. package/src/daemon/collectors/codex/jsonl.ts +108 -0
  52. package/src/daemon/collectors/codex/transcript.ts +299 -0
  53. package/src/daemon/collectors/git.ts +295 -0
  54. package/src/daemon/collectors/opencode/README.md +106 -0
  55. package/src/daemon/collectors/opencode/collector.ts +543 -0
  56. package/src/daemon/collectors/opencode/discovery.ts +257 -0
  57. package/src/daemon/collectors/opencode/index.ts +5 -0
  58. package/src/daemon/collectors/opencode/session-builder.ts +59 -0
  59. package/src/daemon/collectors/opencode-db.ts +282 -0
  60. package/src/daemon/collectors/process.ts +201 -0
  61. package/src/daemon/collectors/tmux/README.md +128 -0
  62. package/src/daemon/collectors/tmux/collector.ts +472 -0
  63. package/src/daemon/collectors/tmux/enrichment.ts +241 -0
  64. package/src/daemon/collectors/tmux/index.ts +4 -0
  65. package/src/daemon/collectors/tmux/parse.ts +128 -0
  66. package/src/daemon/collectors/usage.ts +193 -0
  67. package/src/daemon/index.ts +207 -0
  68. package/src/daemon/scheduler.ts +86 -0
  69. package/src/daemon/store.ts +535 -0
  70. package/src/detect.ts +143 -0
  71. package/src/hooks/claude.ts +596 -0
  72. package/src/hooks/codex.ts +517 -0
  73. package/src/legacy/classifier.ts +215 -0
  74. package/src/legacy/detect.ts +143 -0
  75. package/src/mcp/client-server.ts +21 -0
  76. package/src/mcp/http-handler.ts +126 -0
  77. package/src/mcp/index.ts +61 -0
  78. package/src/mcp/resources/config-current.ts +19 -0
  79. package/src/mcp/resources/index.ts +11 -0
  80. package/src/mcp/resources/sessions-active.ts +19 -0
  81. package/src/mcp/resources/sessions-all.ts +19 -0
  82. package/src/mcp/schemas/sessions.ts +3 -0
  83. package/src/mcp/server.ts +17 -0
  84. package/src/mcp/services/api-adapter.ts +48 -0
  85. package/src/mcp/services/data-source.ts +22 -0
  86. package/src/mcp/services/store-adapter.ts +40 -0
  87. package/src/mcp/tools/get-active-sessions.ts +16 -0
  88. package/src/mcp/tools/get-session-children.ts +24 -0
  89. package/src/mcp/tools/get-session-messages.ts +24 -0
  90. package/src/mcp/tools/get-session.ts +23 -0
  91. package/src/mcp/tools/index.ts +15 -0
  92. package/src/mcp/tools/list-sessions.ts +16 -0
  93. package/src/neo-blessed.d.ts +4 -0
  94. package/src/oc/index.ts +302 -0
  95. package/src/service/manager.ts +242 -0
  96. package/src/shared/config.ts +478 -0
  97. package/src/shared/ipc.ts +80 -0
  98. package/src/shared/logger.ts +248 -0
  99. package/src/shared/paths.ts +73 -0
  100. package/src/shared/platform.ts +110 -0
  101. package/src/shared/session-view.ts +241 -0
  102. package/src/shared/types.ts +299 -0
  103. package/src/shared/utils.ts +339 -0
  104. package/src/tui/app.ts +920 -0
  105. package/src/tui/client.ts +149 -0
  106. package/src/tui/index.ts +181 -0
  107. package/src/types.ts +113 -0
  108. package/src/usage/README.md +323 -0
  109. package/src/usage/api.ts +99 -0
  110. package/src/usage/client.ts +3 -0
  111. package/src/usage/clients/anthropic-oauth-usage.ts +178 -0
  112. package/src/usage/clients/anthropic-web-usage.ts +245 -0
  113. package/src/usage/clients/claude-cli-probe.ts +187 -0
  114. package/src/usage/clients/codex-oauth-usage.ts +113 -0
  115. package/src/usage/clients/opencode-go-usage.ts +441 -0
  116. package/src/usage/index.ts +19 -0
  117. package/src/usage/mappers.ts +273 -0
  118. package/src/usage/planner.ts +54 -0
  119. package/src/usage/providers/claude.ts +113 -0
  120. package/src/usage/providers/codex.ts +34 -0
  121. package/src/usage/providers/opencode-go.ts +37 -0
  122. package/src/usage/resolver.ts +4 -0
  123. package/src/usage/resolvers/browser-cookies.ts +198 -0
  124. package/src/usage/resolvers/claude-auth.ts +99 -0
  125. package/src/usage/resolvers/claude-cli.ts +24 -0
  126. package/src/usage/resolvers/claude-web.ts +16 -0
  127. package/src/usage/resolvers/codex-auth.ts +117 -0
  128. package/src/usage/resolvers/opencode-go-web.ts +14 -0
  129. package/src/usage/types.ts +190 -0
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@scrylog/cli",
3
+ "version": "0.1.0",
4
+ "description": "Terminal dashboard for monitoring tmux sessions running AI coding agents",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "author": "Joel D. Martinez",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/scrylog/scrylog.git",
11
+ "directory": "apps/cli"
12
+ },
13
+ "homepage": "https://github.com/scrylog/scrylog#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/scrylog/scrylog/issues"
16
+ },
17
+ "keywords": [
18
+ "tmux",
19
+ "tui",
20
+ "terminal",
21
+ "bun",
22
+ "ssh",
23
+ "cli",
24
+ "opencode",
25
+ "codex",
26
+ "mcp",
27
+ "claude",
28
+ "dashboard"
29
+ ],
30
+ "engines": {
31
+ "bun": ">=1.3.0"
32
+ },
33
+ "bin": {
34
+ "scrylog": "src/tui/index.ts",
35
+ "scrylogd": "src/daemon/index.ts",
36
+ "scrylog-mcp": "src/mcp/index.ts",
37
+ "scrylog-claude-hook": "src/hooks/claude.ts",
38
+ "scrylog-codex-hook": "src/hooks/codex.ts",
39
+ "oc": "src/oc/index.ts"
40
+ },
41
+ "files": [
42
+ "src",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "clean": "rm -rf .turbo && rm -rf dist",
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",
55
+ "check-types": "tsc --noEmit",
56
+ "test": "bun test",
57
+ "pack:dry-run": "npm pack --dry-run"
58
+ },
59
+ "dependencies": {
60
+ "@hono/zod-openapi": "^1.3.0",
61
+ "@modelcontextprotocol/sdk": "^1.0.0",
62
+ "@opencode-ai/sdk": "1.4.10",
63
+ "@scalar/hono-api-reference": "^0.10.11",
64
+ "@scalar/openapi-to-markdown": "^0.5.9",
65
+ "hono": "^4.12.15",
66
+ "neo-blessed": "^0.2.0",
67
+ "zod": "catalog:"
68
+ },
69
+ "devDependencies": {
70
+ "@types/blessed": "^0.1.27",
71
+ "@types/bun": "catalog:",
72
+ "@types/node": "catalog:",
73
+ "typescript": "catalog:"
74
+ }
75
+ }
@@ -0,0 +1,6 @@
1
+ export enum SessionRouteKey {
2
+ LIST_SESSIONS = "LIST_SESSIONS",
3
+ SESSION_CHILDREN = "SESSION_CHILDREN",
4
+ SESSION_MESSAGES = "SESSION_MESSAGES",
5
+ SESSION_DETAIL = "SESSION_DETAIL",
6
+ }
@@ -0,0 +1,81 @@
1
+ import type { RouteConfig } from "@hono/zod-openapi";
2
+ import { ApiErrorSchema, SessionIdParamSchema } from "../schemas/common";
3
+ import {
4
+ SessionsResponseSchema,
5
+ SessionDetailResponseSchema,
6
+ SessionChildrenResponseSchema,
7
+ SessionMessagesResponseSchema,
8
+ } from "../schemas/sessions";
9
+ import { SessionRouteKey } from "./sessions.keys";
10
+
11
+ export const SessionRoutes = {
12
+ [SessionRouteKey.LIST_SESSIONS]: {
13
+ method: "get",
14
+ path: "/sessions",
15
+ tags: ["Sessions"],
16
+ summary: "List sessions",
17
+ description: "Returns the full session tree with counts.",
18
+ security: [{ BearerAuth: [] }],
19
+ responses: {
20
+ 200: {
21
+ content: { "application/json": { schema: SessionsResponseSchema } },
22
+ description: "Session tree",
23
+ },
24
+ },
25
+ },
26
+ [SessionRouteKey.SESSION_CHILDREN]: {
27
+ method: "get",
28
+ path: "/sessions/{id}/children",
29
+ tags: ["Sessions"],
30
+ summary: "List session children",
31
+ security: [{ BearerAuth: [] }],
32
+ request: { params: SessionIdParamSchema },
33
+ responses: {
34
+ 200: {
35
+ content: { "application/json": { schema: SessionChildrenResponseSchema } },
36
+ description: "Child sessions",
37
+ },
38
+ 404: {
39
+ content: { "application/json": { schema: ApiErrorSchema } },
40
+ description: "Session not found",
41
+ },
42
+ },
43
+ },
44
+ [SessionRouteKey.SESSION_MESSAGES]: {
45
+ method: "get",
46
+ path: "/sessions/{id}/messages",
47
+ tags: ["Sessions"],
48
+ summary: "List session messages",
49
+ description: "Message history for a session (placeholder — always returns empty array).",
50
+ security: [{ BearerAuth: [] }],
51
+ request: { params: SessionIdParamSchema },
52
+ responses: {
53
+ 200: {
54
+ content: { "application/json": { schema: SessionMessagesResponseSchema } },
55
+ description: "Message list",
56
+ },
57
+ 404: {
58
+ content: { "application/json": { schema: ApiErrorSchema } },
59
+ description: "Session not found",
60
+ },
61
+ },
62
+ },
63
+ [SessionRouteKey.SESSION_DETAIL]: {
64
+ method: "get",
65
+ path: "/sessions/{id}",
66
+ tags: ["Sessions"],
67
+ summary: "Get session",
68
+ security: [{ BearerAuth: [] }],
69
+ request: { params: SessionIdParamSchema },
70
+ responses: {
71
+ 200: {
72
+ content: { "application/json": { schema: SessionDetailResponseSchema } },
73
+ description: "Session detail",
74
+ },
75
+ 404: {
76
+ content: { "application/json": { schema: ApiErrorSchema } },
77
+ description: "Session not found",
78
+ },
79
+ },
80
+ },
81
+ } satisfies Record<SessionRouteKey, RouteConfig>;
@@ -0,0 +1,5 @@
1
+ export enum SystemRouteKey {
2
+ HEALTH = "HEALTH",
3
+ STATUS = "STATUS",
4
+ CONFIG = "CONFIG",
5
+ }
@@ -0,0 +1,66 @@
1
+ import { z } from "@hono/zod-openapi";
2
+ import type { RouteConfig } from "@hono/zod-openapi";
3
+ import { SystemRouteKey } from "./system.keys";
4
+
5
+ export const SystemRoutes = {
6
+ [SystemRouteKey.HEALTH]: {
7
+ method: "get",
8
+ path: "/health",
9
+ tags: ["System"],
10
+ summary: "Health check",
11
+ responses: {
12
+ 200: {
13
+ content: {
14
+ "application/json": {
15
+ schema: z.object({
16
+ ok: z.literal(true),
17
+ startedAt: z.number(),
18
+ refreshedAt: z.number(),
19
+ collectors: z.record(z.string(), z.unknown()),
20
+ }),
21
+ },
22
+ },
23
+ description: "Daemon is running",
24
+ },
25
+ },
26
+ },
27
+ [SystemRouteKey.STATUS]: {
28
+ method: "get",
29
+ path: "/status",
30
+ tags: ["System"],
31
+ summary: "Session counts and collector status",
32
+ security: [{ BearerAuth: [] }],
33
+ responses: {
34
+ 200: {
35
+ content: {
36
+ "application/json": {
37
+ schema: z.object({
38
+ counts: z.record(z.string(), z.unknown()),
39
+ collectorStatus: z.record(z.string(), z.unknown()),
40
+ generatedAt: z.number(),
41
+ }),
42
+ },
43
+ },
44
+ description: "Current status",
45
+ },
46
+ },
47
+ },
48
+ [SystemRouteKey.CONFIG]: {
49
+ method: "get",
50
+ path: "/config",
51
+ tags: ["System"],
52
+ summary: "Current daemon configuration",
53
+ description: "Bearer token is redacted in the response.",
54
+ security: [{ BearerAuth: [] }],
55
+ responses: {
56
+ 200: {
57
+ content: {
58
+ "application/json": {
59
+ schema: z.object({ config: z.record(z.string(), z.unknown()) }),
60
+ },
61
+ },
62
+ description: "Configuration",
63
+ },
64
+ },
65
+ },
66
+ } satisfies Record<SystemRouteKey, RouteConfig>;
@@ -0,0 +1,3 @@
1
+ export enum UsageRouteKey {
2
+ GET_USAGE = "GET_USAGE",
3
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "@hono/zod-openapi";
2
+ import type { RouteConfig } from "@hono/zod-openapi";
3
+ import { UsageRouteKey } from "./usage.keys";
4
+ import { UsageResponseSchema } from "../schemas/usage";
5
+
6
+ export const UsageRoutes = {
7
+ [UsageRouteKey.GET_USAGE]: {
8
+ method: "get",
9
+ path: "/usage",
10
+ tags: ["Usage"],
11
+ summary: "Get subscription usage limits",
12
+ security: [{ BearerAuth: [] }],
13
+ responses: {
14
+ 200: {
15
+ content: {
16
+ "application/json": {
17
+ schema: UsageResponseSchema,
18
+ },
19
+ },
20
+ description: "Current usage data for all configured providers",
21
+ },
22
+ },
23
+ },
24
+ } satisfies Record<UsageRouteKey, RouteConfig>;
@@ -0,0 +1,5 @@
1
+ export enum WebhookRouteKey {
2
+ WEBHOOK_OPENCODE = "WEBHOOK_OPENCODE",
3
+ WEBHOOK_CLAUDE = "WEBHOOK_CLAUDE",
4
+ WEBHOOK_CODEX = "WEBHOOK_CODEX",
5
+ }
@@ -0,0 +1,78 @@
1
+ import type { RouteConfig } from "@hono/zod-openapi";
2
+ import { ApiErrorSchema, WebhookOkSchema } from "../schemas/common";
3
+ import { OpencodeWebhookBodySchema } from "../schemas/webhooks/opencode";
4
+ import { ClaudeWebhookBodySchema } from "../schemas/webhooks/claude";
5
+ import { CodexWebhookBodySchema } from "../schemas/webhooks/codex";
6
+ import { WebhookRouteKey } from "./webhooks.keys";
7
+
8
+ export const WebhookRoutes = {
9
+ [WebhookRouteKey.WEBHOOK_OPENCODE]: {
10
+ method: "post",
11
+ path: "/opencode",
12
+ tags: ["Webhooks"],
13
+ summary: "OpenCode session hook",
14
+ description: "Receives lifecycle events from OpenCode webhooks.",
15
+ request: {
16
+ body: {
17
+ required: true,
18
+ content: { "application/json": { schema: OpencodeWebhookBodySchema } },
19
+ },
20
+ },
21
+ responses: {
22
+ 200: {
23
+ content: { "application/json": { schema: WebhookOkSchema } },
24
+ description: "Event accepted",
25
+ },
26
+ 400: {
27
+ content: { "application/json": { schema: ApiErrorSchema } },
28
+ description: "Missing required field",
29
+ },
30
+ },
31
+ },
32
+ [WebhookRouteKey.WEBHOOK_CLAUDE]: {
33
+ method: "post",
34
+ path: "/claude",
35
+ tags: ["Webhooks"],
36
+ summary: "Claude session hook",
37
+ description: "Receives lifecycle events from the Claude CLI hooks system.",
38
+ request: {
39
+ body: {
40
+ required: true,
41
+ content: { "application/json": { schema: ClaudeWebhookBodySchema } },
42
+ },
43
+ },
44
+ responses: {
45
+ 200: {
46
+ content: { "application/json": { schema: WebhookOkSchema } },
47
+ description: "Event accepted",
48
+ },
49
+ 400: {
50
+ content: { "application/json": { schema: ApiErrorSchema } },
51
+ description: "Missing required field",
52
+ },
53
+ },
54
+ },
55
+ [WebhookRouteKey.WEBHOOK_CODEX]: {
56
+ method: "post",
57
+ path: "/codex",
58
+ tags: ["Webhooks"],
59
+ summary: "Codex session hook",
60
+ description: "Receives lifecycle events from the Codex CLI hooks system.",
61
+ request: {
62
+ body: {
63
+ required: true,
64
+ content: { "application/json": { schema: CodexWebhookBodySchema } },
65
+ },
66
+ },
67
+ responses: {
68
+ 200: {
69
+ content: { "application/json": { schema: WebhookOkSchema } },
70
+ description: "Event accepted",
71
+ },
72
+ 400: {
73
+ content: { "application/json": { schema: ApiErrorSchema } },
74
+ description: "Missing required field",
75
+ },
76
+ },
77
+ },
78
+ } satisfies Record<WebhookRouteKey, RouteConfig>;
@@ -0,0 +1,18 @@
1
+ import type { ScrylogConfig } from "../shared/config";
2
+ import type { DaemonStore } from "../daemon/store";
3
+ import type { GitCollector } from "../daemon/collectors/git";
4
+ import type { Logger } from "../shared/logger";
5
+ import { createApiApp } from "./routes";
6
+
7
+ /**
8
+ * Create the fetch handler for the daemon HTTP API.
9
+ * Used by both the Unix socket and TCP listeners (same handler, two servers).
10
+ */
11
+ export function createApiHandler(
12
+ store: DaemonStore,
13
+ config: ScrylogConfig,
14
+ logger: Logger,
15
+ gitCollector?: GitCollector,
16
+ ): (request: Request) => Response | Promise<Response> {
17
+ return createApiApp(store, config, logger, gitCollector).fetch;
18
+ }
@@ -0,0 +1,43 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import type { MiddlewareHandler } from "hono";
3
+ import { UnauthorizedError } from "./errors";
4
+
5
+ function secureCompare(a: string, b: string): boolean {
6
+ const bufA = Buffer.from(a, "utf-8");
7
+ const bufB = Buffer.from(b, "utf-8");
8
+ if (bufA.length !== bufB.length) return false;
9
+ return timingSafeEqual(bufA, bufB);
10
+ }
11
+
12
+ export function createBearerAuthMiddleware(bearerToken: string | undefined): MiddlewareHandler {
13
+ const PUBLIC_PATHS = [
14
+ "/api/health",
15
+ "/api/docs",
16
+ "/api/openapi.json",
17
+ "/api/llms.txt",
18
+ ];
19
+
20
+ return async (c, next) => {
21
+ if (!bearerToken) {
22
+ return next();
23
+ }
24
+
25
+ const path = c.req.path;
26
+
27
+ if (PUBLIC_PATHS.includes(path)) {
28
+ return next();
29
+ }
30
+
31
+ if (path.startsWith("/api/webhook/")) {
32
+ return next();
33
+ }
34
+
35
+ const auth = c.req.header("Authorization");
36
+ const expected = `Bearer ${bearerToken}`;
37
+ if (!auth || !secureCompare(auth, expected)) {
38
+ throw new UnauthorizedError();
39
+ }
40
+
41
+ return next();
42
+ };
43
+ }
@@ -0,0 +1,28 @@
1
+ export class ApiError extends Error {
2
+ constructor(
3
+ public readonly status: number,
4
+ public readonly code: string,
5
+ message: string,
6
+ ) {
7
+ super(message);
8
+ this.name = "ApiError";
9
+ }
10
+ }
11
+
12
+ export class NotFoundError extends ApiError {
13
+ constructor(resource: string, id: string) {
14
+ super(404, "NOT_FOUND", `${resource} '${id}' not found`);
15
+ }
16
+ }
17
+
18
+ export class UnauthorizedError extends ApiError {
19
+ constructor() {
20
+ super(401, "UNAUTHORIZED", "Unauthorized");
21
+ }
22
+ }
23
+
24
+ export class ValidationError extends ApiError {
25
+ constructor(message: string) {
26
+ super(400, "VALIDATION_ERROR", message);
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ import { createRoute } from "@hono/zod-openapi";
2
+ import { SystemRoutes } from "../docs/system";
3
+ import { SessionRoutes } from "../docs/sessions";
4
+ import { WebhookRoutes } from "../docs/webhooks";
5
+ import { UsageRoutes } from "../docs/usage";
6
+
7
+ const _registry = {
8
+ ...SystemRoutes,
9
+ ...SessionRoutes,
10
+ ...WebhookRoutes,
11
+ ...UsageRoutes,
12
+ };
13
+
14
+ export type RouteKey = keyof typeof _registry;
15
+
16
+ export function defineRoute<K extends RouteKey>(key: K) {
17
+ return createRoute(_registry[key] as (typeof _registry)[K]);
18
+ }
@@ -0,0 +1,4 @@
1
+ import type { z } from "@hono/zod-openapi";
2
+ import type { ClaudeWebhookBodySchema } from "../schemas/webhooks/claude";
3
+
4
+ export type ClaudeWebhookPayload = z.infer<typeof ClaudeWebhookBodySchema>;
@@ -0,0 +1,4 @@
1
+ import type { z } from "@hono/zod-openapi";
2
+ import type { CodexWebhookBodySchema } from "../schemas/webhooks/codex";
3
+
4
+ export type CodexWebhookPayload = z.infer<typeof CodexWebhookBodySchema>;
@@ -0,0 +1,11 @@
1
+ import type { z } from "@hono/zod-openapi";
2
+ import type { OpencodeWebhookBodySchema } from "../schemas/webhooks/opencode";
3
+ import type { SessionState } from "../../shared/types";
4
+
5
+ export type OpencodeWebhookPayload = z.infer<typeof OpencodeWebhookBodySchema>;
6
+
7
+ export interface HookStateResult {
8
+ state: SessionState;
9
+ hasAttention: boolean;
10
+ pendingAttentionCount?: number;
11
+ }
@@ -0,0 +1,47 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { ScrylogConfig } from "../../shared/config";
3
+ import type { DaemonStore } from "../../daemon/store";
4
+ import { buildSessionView } from "../../shared/session-view";
5
+ import { NotFoundError } from "../lib/errors";
6
+ import { SessionRouteKey } from "../docs/sessions.keys";
7
+ import { defineRoute } from "../lib/route";
8
+
9
+ export function createSessionsRouter(store: DaemonStore, config: ScrylogConfig): OpenAPIHono {
10
+ const router = new OpenAPIHono();
11
+
12
+ router.openapi(defineRoute(SessionRouteKey.LIST_SESSIONS), (c) => {
13
+ const view = buildSessionView(store.getSessions(), config);
14
+ return c.json({ sessions: view.sessions, counts: view.counts, generatedAt: store.getState().refreshedAt }, 200);
15
+ });
16
+
17
+ // Must be registered before /:id to avoid the wildcard swallowing the path
18
+ router.openapi(defineRoute(SessionRouteKey.SESSION_CHILDREN), (c) => {
19
+ const { id } = c.req.valid("param");
20
+ const parent = store.getSession(id);
21
+ if (!parent) {
22
+ throw new NotFoundError("Session", id);
23
+ }
24
+ const children = buildSessionView(store.getSessions(), config).sessions.find((s) => s.id === id)?.children ?? [];
25
+ return c.json({ sessionId: id, children }, 200);
26
+ });
27
+
28
+ router.openapi(defineRoute(SessionRouteKey.SESSION_MESSAGES), (c) => {
29
+ const { id } = c.req.valid("param");
30
+ const session = store.getSession(id);
31
+ if (!session) {
32
+ throw new NotFoundError("Session", id);
33
+ }
34
+ return c.json({ sessionId: id, messages: [] }, 200);
35
+ });
36
+
37
+ router.openapi(defineRoute(SessionRouteKey.SESSION_DETAIL), (c) => {
38
+ const { id } = c.req.valid("param");
39
+ const session = store.getSession(id);
40
+ if (!session) {
41
+ throw new NotFoundError("Session", id);
42
+ }
43
+ return c.json({ session: session as unknown as Record<string, unknown> }, 200);
44
+ });
45
+
46
+ return router;
47
+ }
@@ -0,0 +1,103 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { streamSSE } from "hono/streaming";
3
+ import type { ScrylogConfig } from "../../shared/config";
4
+ import type { DaemonStore } from "../../daemon/store";
5
+ import type { Logger } from "../../shared/logger";
6
+ import { buildSessionView } from "../../shared/session-view";
7
+ import { SystemRouteKey } from "../docs/system.keys";
8
+ import { defineRoute } from "../lib/route";
9
+
10
+ export function createSystemRouter(store: DaemonStore, config: ScrylogConfig, logger: Logger): OpenAPIHono {
11
+ const router = new OpenAPIHono();
12
+
13
+ router.openapi(defineRoute(SystemRouteKey.HEALTH), (c) => {
14
+ const state = store.getState();
15
+ return c.json({
16
+ ok: true as const,
17
+ startedAt: state.startedAt,
18
+ refreshedAt: state.refreshedAt,
19
+ collectors: state.collectorStatus,
20
+ });
21
+ });
22
+
23
+ router.openapi(defineRoute(SystemRouteKey.STATUS), (c) => {
24
+ const state = store.getState();
25
+ const view = buildSessionView(state.sessions, config);
26
+ return c.json({
27
+ counts: view.counts,
28
+ collectorStatus: state.collectorStatus,
29
+ generatedAt: state.refreshedAt,
30
+ });
31
+ });
32
+
33
+ router.openapi(defineRoute(SystemRouteKey.CONFIG), (c) => {
34
+ const redacted = {
35
+ ...config,
36
+ api: { ...config.api, bearerToken: config.api.bearerToken ? "[redacted]" : undefined },
37
+ mcp: { ...config.mcp, bearerToken: config.mcp.bearerToken ? "[redacted]" : undefined },
38
+ };
39
+ return c.json({ config: redacted });
40
+ });
41
+
42
+ router.get("/events", (c) => {
43
+ const state = store.getState();
44
+ const clientId = Math.random().toString(36).slice(2, 8);
45
+
46
+ return streamSSE(c, async (stream) => {
47
+ logger.info("api", "SSE client connected", { event: "sse.client_connected", clientId });
48
+
49
+ await stream.writeSSE({
50
+ data: JSON.stringify({ type: "state:initial", data: state }),
51
+ });
52
+
53
+ let keepaliveTimer: ReturnType<typeof setInterval> | undefined;
54
+ let closed = false;
55
+
56
+ const cleanup = () => {
57
+ if (closed) return;
58
+ closed = true;
59
+ if (keepaliveTimer) clearInterval(keepaliveTimer);
60
+ unsubscribe();
61
+ logger.info("api", "SSE client disconnected", { event: "sse.client_disconnected", clientId });
62
+ };
63
+
64
+ const unsubscribe = store.onEvent((event) => {
65
+ if (closed) return;
66
+ stream.writeSSE({
67
+ event: event.type,
68
+ data: JSON.stringify(event),
69
+ }).catch((error) => {
70
+ logger.debug("api", "SSE write failed, closing", {
71
+ event: "sse.write_failed",
72
+ clientId,
73
+ errorMessage: error instanceof Error ? error.message : String(error),
74
+ });
75
+ cleanup();
76
+ });
77
+ });
78
+
79
+ if (config.api.sseKeepaliveMs > 0) {
80
+ keepaliveTimer = setInterval(() => {
81
+ if (closed) return;
82
+ stream.write(":keepalive\n\n").catch((error) => {
83
+ logger.debug("api", "SSE keepalive failed, closing", {
84
+ event: "sse.keepalive_failed",
85
+ clientId,
86
+ errorMessage: error instanceof Error ? error.message : String(error),
87
+ });
88
+ cleanup();
89
+ });
90
+ }, config.api.sseKeepaliveMs);
91
+ }
92
+
93
+ stream.onAbort(() => {
94
+ cleanup();
95
+ });
96
+
97
+ // Keep the SSE stream alive indefinitely; cleanup runs via onAbort.
98
+ await new Promise(() => {});
99
+ });
100
+ });
101
+
102
+ return router;
103
+ }
@@ -0,0 +1,15 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { DaemonStore } from "../../daemon/store";
3
+ import { UsageRouteKey } from "../docs/usage.keys";
4
+ import { defineRoute } from "../lib/route";
5
+
6
+ export function createUsageRouter(store: DaemonStore): OpenAPIHono {
7
+ const router = new OpenAPIHono();
8
+
9
+ router.openapi(defineRoute(UsageRouteKey.GET_USAGE), (c) => {
10
+ const state = store.getState().usage;
11
+ return c.json(state);
12
+ });
13
+
14
+ return router;
15
+ }