@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
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bun
2
+ import { unlinkSync, existsSync } from "node:fs";
3
+ import { loadConfig } from "../shared/config";
4
+ import { getSocketPath } from "../shared/ipc";
5
+ import { IS_WINDOWS } from "../shared/platform";
6
+ import { DaemonStore } from "./store";
7
+ import { Scheduler } from "./scheduler";
8
+ import { TmuxCollector } from "./collectors/tmux";
9
+ import { ProcessCollector } from "./collectors/process";
10
+ import { GitCollector } from "./collectors/git";
11
+ import { OpenCodeCollector } from "./collectors/opencode";
12
+ import { ClaudeCollector } from "./collectors/claude";
13
+ import { CodexCollector } from "./collectors/codex";
14
+ import { OpenCodeDbCollector } from "./collectors/opencode-db";
15
+ import { UsageCollector } from "./collectors/usage";
16
+ import { createApiHandler } from "../api/index";
17
+ import { createMcpHttpHandler } from "../mcp/http-handler";
18
+ import { createLogger, parseLogFormat, parseLogLevel } from "../shared/logger";
19
+ import { parseDaemonArgs } from "./cli";
20
+
21
+ function printHelp() {
22
+ process.stdout.write(`scrylogd
23
+
24
+ Usage:
25
+ scrylogd [options]
26
+
27
+ Options:
28
+ --config <path> Load config from an explicit path
29
+ --log-level <level> Override daemon log level
30
+ --log-format <format> Override stderr log format (pretty|json)
31
+ --no-log-file Disable daemon JSONL file logging for this process
32
+ -h, --help Show this help text
33
+
34
+ Notes:
35
+ Starts the scrylog daemon, HTTP API, and MCP HTTP listener.
36
+ `);
37
+ }
38
+
39
+ // ── Parse CLI args ────────────────────────────────────────────────────────────
40
+
41
+ const args = process.argv.slice(2);
42
+ let cliOptions: ReturnType<typeof parseDaemonArgs>;
43
+ try {
44
+ cliOptions = parseDaemonArgs(args);
45
+ } catch (error) {
46
+ process.stderr.write(`scrylogd: ${error instanceof Error ? error.message : String(error)}\n`);
47
+ process.exit(1);
48
+ }
49
+
50
+ if (cliOptions.help) {
51
+ printHelp();
52
+ process.exit(0);
53
+ }
54
+ const configPath = cliOptions.configPath;
55
+ const logLevelArg = cliOptions.logLevel;
56
+ const logFormatArg = cliOptions.logFormat;
57
+
58
+ // ── Bootstrap ─────────────────────────────────────────────────────────────────
59
+
60
+ const config = loadConfig(configPath);
61
+ const logLevel = parseLogLevel(logLevelArg, config.daemon.logLevel);
62
+ const logFormat = parseLogFormat(logFormatArg, config.daemon.logFormat);
63
+ const logger = createLogger({
64
+ level: logLevel,
65
+ format: logFormat,
66
+ file: {
67
+ enabled: config.daemon.logFileEnabled && cliOptions.logFileEnabled,
68
+ path: config.daemon.logFilePath,
69
+ maxBytes: config.daemon.logFileMaxBytes,
70
+ maxFiles: config.daemon.logFileMaxFiles,
71
+ },
72
+ });
73
+ const store = new DaemonStore(logger);
74
+ const scheduler = new Scheduler(store, config, logger);
75
+ const processCollector = new ProcessCollector();
76
+ const gitCollector = new GitCollector();
77
+
78
+ scheduler.register(processCollector);
79
+ scheduler.register(gitCollector);
80
+ scheduler.register(new TmuxCollector(processCollector, gitCollector));
81
+ if (config.opencode.mode === "sse") {
82
+ scheduler.register(new OpenCodeCollector(gitCollector));
83
+ }
84
+ scheduler.register(new ClaudeCollector(gitCollector));
85
+ scheduler.register(new CodexCollector(gitCollector));
86
+ scheduler.register(new OpenCodeDbCollector(gitCollector));
87
+ if (config.usage.enabled) {
88
+ scheduler.register(new UsageCollector());
89
+ }
90
+
91
+ // ── Duplicate-instance detection ──────────────────────────────────────────────
92
+
93
+ const socketPath = getSocketPath(config.daemon.socketPath);
94
+ const apiTcpEnabled = IS_WINDOWS || config.api.tcpEnabled;
95
+
96
+ if (IS_WINDOWS) {
97
+ if (!config.api.tcpEnabled) {
98
+ logger.warn("daemon", "Windows requires TCP for duplicate detection and API access. Using TCP regardless of config.api.tcpEnabled.", { event: "daemon.tcp_required", platform: "windows" });
99
+ }
100
+
101
+ // Try connecting to an existing daemon via TCP; if it responds, exit
102
+ try {
103
+ const probeHeaders: Record<string, string> = config.api.bearerToken
104
+ ? { Authorization: `Bearer ${config.api.bearerToken}` }
105
+ : {};
106
+ const probeUrl = `http://localhost:${config.api.port}/api/health`;
107
+ const probe = await fetch(probeUrl, { headers: probeHeaders, signal: AbortSignal.timeout(config.daemon.networkTimeoutMs) });
108
+ if (probe.ok) {
109
+ logger.info("daemon", "Another daemon is already running. Exiting.", { event: "daemon.duplicate_detected", listener: "tcp", url: probeUrl });
110
+ process.exit(0);
111
+ }
112
+ } catch {
113
+ // No running daemon — continue
114
+ }
115
+ } else {
116
+ // Try connecting to an existing daemon; if it responds, exit
117
+ try {
118
+ const probeHeaders: Record<string, string> = config.api.bearerToken
119
+ ? { Authorization: `Bearer ${config.api.bearerToken}` }
120
+ : {};
121
+ // @ts-ignore -- Bun-specific unix option
122
+ const probe = await fetch("http://localhost/api/health", { unix: socketPath, headers: probeHeaders, signal: AbortSignal.timeout(config.daemon.networkTimeoutMs) });
123
+ if (probe.ok) {
124
+ logger.info("daemon", "Another daemon is already running. Exiting.", { event: "daemon.duplicate_detected", socketPath });
125
+ process.exit(0);
126
+ }
127
+ } catch {
128
+ // No running daemon — clean up stale socket and continue
129
+ if (existsSync(socketPath)) {
130
+ try {
131
+ unlinkSync(socketPath);
132
+ } catch {
133
+ // Ignore cleanup errors
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ // ── Start collectors ──────────────────────────────────────────────────────────
140
+
141
+ await scheduler.start();
142
+
143
+ // ── HTTP API handler (shared across Unix socket and TCP) ──────────────────────
144
+
145
+ const apiHandler = createApiHandler(store, config, logger, gitCollector);
146
+
147
+ // ── Unix socket listener (macOS/Linux) ────────────────────────────────────────
148
+
149
+ if (!IS_WINDOWS) {
150
+ // idleTimeout is not typed on the unix socket variant but is supported at runtime
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ Bun.serve({
153
+ unix: socketPath,
154
+ fetch: apiHandler,
155
+ idleTimeout: 0, // SSE connections must not be dropped on idle
156
+ } as any);
157
+ logger.info("daemon", "Unix socket listener started", { event: "daemon.listener_started", listener: "unix", socketPath });
158
+ }
159
+
160
+ // ── TCP listener ──────────────────────────────────────────────────────────────
161
+
162
+ if (apiTcpEnabled) {
163
+ Bun.serve({
164
+ port: config.api.port,
165
+ hostname: config.api.host,
166
+ fetch: apiHandler,
167
+ idleTimeout: 0, // SSE connections must not be dropped on idle
168
+ });
169
+ logger.info("daemon", "HTTP API listener started", { event: "daemon.listener_started", listener: "api", url: `http://${config.api.host}:${config.api.port}` });
170
+ } else {
171
+ logger.info("daemon", "HTTP API TCP listener disabled", { event: "daemon.listener_disabled", listener: "api" });
172
+ }
173
+
174
+ // ── MCP HTTP listener (in-daemon) ─────────────────────────────────────────────
175
+
176
+ const mcpHandler = createMcpHttpHandler(store, config);
177
+ if (mcpHandler) {
178
+ Bun.serve({
179
+ port: config.mcp.port,
180
+ hostname: config.mcp.host,
181
+ fetch: mcpHandler,
182
+ idleTimeout: 0, // long-lived MCP connections
183
+ });
184
+ logger.info("daemon", "MCP HTTP listener started", { event: "daemon.listener_started", listener: "mcp", url: `http://${config.mcp.host}:${config.mcp.port}` });
185
+ } else {
186
+ logger.info("daemon", "MCP HTTP listener disabled", { event: "daemon.listener_disabled", listener: "mcp" });
187
+ }
188
+
189
+ // ── Signal handling ───────────────────────────────────────────────────────────
190
+
191
+ async function shutdown(signal: string): Promise<void> {
192
+ logger.info("daemon", "Received signal. Shutting down", { event: "daemon.shutdown", signal });
193
+ await scheduler.stop();
194
+ if (!IS_WINDOWS && existsSync(socketPath)) {
195
+ try {
196
+ unlinkSync(socketPath);
197
+ } catch {
198
+ // Ignore
199
+ }
200
+ }
201
+ process.exit(0);
202
+ }
203
+
204
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
205
+ process.on("SIGINT", () => void shutdown("SIGINT"));
206
+
207
+ logger.info("daemon", "Started", { event: "daemon.started" });
@@ -0,0 +1,86 @@
1
+ import type { ScrylogConfig } from "../shared/config";
2
+ import type { Logger } from "../shared/logger";
3
+ import type { DaemonStore } from "./store";
4
+ import type { ProcessCollector } from "./collectors/process";
5
+
6
+ export interface Collector {
7
+ name: string;
8
+ /** Called once to start periodic/event-driven collection */
9
+ start(store: DaemonStore, config: ScrylogConfig, logger: Logger): void | Promise<void>;
10
+ /** Called on daemon shutdown */
11
+ stop(): void | Promise<void>;
12
+ }
13
+
14
+ /**
15
+ * Orchestrates all collectors.
16
+ * Handles lifecycle start/stop and periodic polling intervals.
17
+ */
18
+ export class Scheduler {
19
+ private collectors: Collector[] = [];
20
+ private running = false;
21
+ private retentionTimer?: ReturnType<typeof setInterval>;
22
+
23
+ constructor(
24
+ private readonly store: DaemonStore,
25
+ private readonly config: ScrylogConfig,
26
+ private readonly logger: Logger,
27
+ ) {}
28
+
29
+ register(collector: Collector): void {
30
+ this.collectors.push(collector);
31
+ if (collector.name === "process" && "processes" in collector) {
32
+ this.store.registerProcessCollector(collector as unknown as ProcessCollector);
33
+ }
34
+ }
35
+
36
+ async start(): Promise<void> {
37
+ if (this.running) {
38
+ return;
39
+ }
40
+
41
+ this.running = true;
42
+ this.logger.info("scheduler", "Starting all collectors", { event: "scheduler.collectors_starting", count: this.collectors.length });
43
+
44
+ for (const collector of this.collectors) {
45
+ try {
46
+ this.store.setCollectorStatus(collector.name, { running: true, lastRunAt: Date.now() });
47
+ this.logger.debug("scheduler", "Starting collector", { event: "collector.starting", collector: collector.name });
48
+ await collector.start(this.store, this.config, this.logger);
49
+ } catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ this.logger.warn("scheduler", "Collector failed to start", { event: "collector.start_failed", collector: collector.name, errorMessage: message });
52
+ this.store.setCollectorStatus(collector.name, { running: false, lastError: message });
53
+ this.store.reportError(collector.name, message);
54
+ }
55
+ }
56
+
57
+ this.store.applyRetention(this.config.retention);
58
+ this.retentionTimer = setInterval(() => {
59
+ this.store.applyRetention(this.config.retention);
60
+ }, Math.max(this.config.daemon.pollIntervalMs, 60_000));
61
+ }
62
+
63
+ async stop(): Promise<void> {
64
+ if (!this.running) {
65
+ return;
66
+ }
67
+
68
+ this.running = false;
69
+ this.logger.info("scheduler", "Stopping all collectors", { event: "scheduler.collectors_stopping" });
70
+ if (this.retentionTimer) {
71
+ clearInterval(this.retentionTimer);
72
+ this.retentionTimer = undefined;
73
+ }
74
+
75
+ for (const collector of this.collectors) {
76
+ try {
77
+ await collector.stop();
78
+ this.store.setCollectorStatus(collector.name, { running: false });
79
+ this.logger.debug("scheduler", "Stopped collector", { event: "collector.stopped", collector: collector.name });
80
+ } catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ this.logger.warn("scheduler", "Collector failed to stop", { event: "collector.stop_failed", collector: collector.name, errorMessage: message });
83
+ }
84
+ }
85
+ }
86
+ }