@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,478 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { LogFormat, LogLevel } from "./logger";
4
+ import { ensureConfigDir, ensureParentDir, getDaemonLogPath, getDefaultConfigPath, getLegacyConfigPath } from "./paths";
5
+ import { getHooksDir, IS_WINDOWS } from "./platform";
6
+
7
+ // ── Config schema ─────────────────────────────────────────────────────────────
8
+
9
+ export interface DaemonConfig {
10
+ /** Interval in ms between tmux/process collection cycles (default: 10000) */
11
+ pollIntervalMs: number;
12
+ /** Override Unix socket path for local IPC on macOS/Linux */
13
+ socketPath?: string;
14
+ /** Timeout in ms for daemon-owned external commands (default: 5000) */
15
+ commandTimeoutMs: number;
16
+ /** Timeout in ms for daemon-owned network requests (default: 10000) */
17
+ networkTimeoutMs: number;
18
+ /** Minimum log level written to stderr (default: "info") */
19
+ logLevel: LogLevel;
20
+ /** stderr log format (default: "auto") */
21
+ logFormat: LogFormat;
22
+ /** Whether scrylogd writes its own JSONL log file (default: true) */
23
+ logFileEnabled: boolean;
24
+ /** Path to the JSONL daemon log file */
25
+ logFilePath: string;
26
+ /** Max active log file size in bytes before rotation */
27
+ logFileMaxBytes: number;
28
+ /** Number of log files to retain including the active file */
29
+ logFileMaxFiles: number;
30
+ /** Legacy API TCP port retained for config compatibility. */
31
+ port?: number;
32
+ /** Legacy API TCP host retained for config compatibility. */
33
+ host?: string;
34
+ }
35
+
36
+ export interface ApiConfig {
37
+ /** Whether to expose the REST API on TCP (default: true on non-Windows) */
38
+ tcpEnabled: boolean;
39
+ /** Hostname for the TCP API listener (default: "127.0.0.1") */
40
+ host: string;
41
+ /** TCP port for the REST API listener (default: 7788) */
42
+ port: number;
43
+ /** Bearer token for API HTTP auth (required for remote access) */
44
+ bearerToken?: string;
45
+ /** SSE keepalive comment-frame interval in ms; 0 disables keepalive (default: 15000) */
46
+ sseKeepaliveMs: number;
47
+ }
48
+
49
+ export interface OpenCodeConfig {
50
+ /** Which OpenCode ingestion path to use (default: "hooks") */
51
+ mode: "hooks" | "sse";
52
+ /** Explicit list of OpenCode instance URLs to monitor (auto-discover if empty) */
53
+ instances: string[];
54
+ /** Whether to enable auto-discovery via process scanning (default: true) */
55
+ autoDiscover: boolean;
56
+ }
57
+
58
+ export interface ClaudeConfig {
59
+ /** Override path to ~/.claude directory (default: auto-detected) */
60
+ claudeDir?: string;
61
+ /** Enable hooks integration for enriched data (default: false) */
62
+ hooksEnabled: boolean;
63
+ /** Directory where hook scripts write their output (default: /tmp/scrylog-claude-hooks) */
64
+ hooksDir: string;
65
+ }
66
+
67
+ export interface CodexConfig {
68
+ /** Override path to ~/.codex directory (default: auto-detect, respects $CODEX_HOME) */
69
+ codexDir?: string;
70
+ /** Enable hooks integration for enriched data (default: false) */
71
+ hooksEnabled: boolean;
72
+ /** Directory where hook scripts write their output (default: /tmp/scrylog-codex-hooks) */
73
+ hooksDir: string;
74
+ }
75
+
76
+ export interface TmuxConfig {
77
+ /** Number of lines to capture for preview (default: 50) */
78
+ previewLines: number;
79
+ /** Number of lines to capture for detail view (default: 200) */
80
+ detailLines: number;
81
+ }
82
+
83
+ export interface TuiConfig {
84
+ /** Refresh interval in ms for polling-based updates (default: 5000) */
85
+ refreshMs: number;
86
+ /** Whether to show standalone sessions section (default: true) */
87
+ showStandalone: boolean;
88
+ /** Whether to connect to daemon via SSE for push updates (default: true) */
89
+ usePushUpdates: boolean;
90
+ /** Hide standalone sessions with no activity in the last N hours (default: 6, 0 = show all) */
91
+ standaloneMaxAgeHours: number;
92
+ /** Debounce window in ms for coalescing push-event bursts into a single refresh (default: 120, 0 = disabled) */
93
+ pushDebounceMs: number;
94
+ }
95
+
96
+ export interface McpConfig {
97
+ /** Whether the local stdio MCP binary should be allowed (default: true) */
98
+ stdioEnabled: boolean;
99
+ /** Whether to expose MCP over HTTP (default: false) */
100
+ httpEnabled: boolean;
101
+ /** Host for the MCP HTTP listener (default: 127.0.0.1) */
102
+ host: string;
103
+ /** Port for the MCP HTTP transport (default: 7789) */
104
+ port: number;
105
+ /** Bearer token for MCP HTTP auth (required for remote access) */
106
+ bearerToken?: string;
107
+ }
108
+
109
+ export interface CorsConfig {
110
+ /** Whether to send CORS headers on API responses (default: false) */
111
+ enabled: boolean;
112
+ /** Allowed origins — use ["*"] for public, or explicit URLs (default: []) */
113
+ origins: string[];
114
+ /** Allowed HTTP methods (default: ["GET", "POST", "OPTIONS"]) */
115
+ methods: string[];
116
+ /** Allowed request headers (default: ["Content-Type", "Authorization"]) */
117
+ allowedHeaders: string[];
118
+ /** Whether to expose headers to the browser (default: []) */
119
+ exposedHeaders: string[];
120
+ /** Whether to include credentials in CORS requests (default: false) */
121
+ credentials: boolean;
122
+ /** Preflight cache duration in seconds (default: 600) */
123
+ maxAge: number;
124
+ }
125
+
126
+ export interface UsageClaudeConfig {
127
+ /** Explicit data source override (default: "auto") */
128
+ dataSource: "auto" | "oauth" | "web" | "cli";
129
+ /** Whether to enrich OAuth/CLI with Web API extras (default: true) */
130
+ webExtrasEnabled: boolean;
131
+ }
132
+
133
+ export interface UsageCodexConfig {
134
+ /** Explicit data source override (default: "auto") */
135
+ dataSource: "auto" | "oauth";
136
+ /** Override for ChatGPT API base URL (reads from ~/.codex/config.toml if not set) */
137
+ chatgptBaseUrl?: string;
138
+ }
139
+
140
+ export interface UsageOpenCodeGoConfig {
141
+ /** Server function ID for workspace resolution (default: hardcoded) */
142
+ serverFnId: string;
143
+ }
144
+
145
+ export interface UsageConfig {
146
+ /** Whether usage collection is enabled (default: true) */
147
+ enabled: boolean;
148
+ /** Poll interval in ms (default: 300000 = 5 minutes) */
149
+ pollIntervalMs: number;
150
+ /** Which providers to poll (default: ["claude", "opencode-go"]) */
151
+ providers: string[];
152
+ claude: UsageClaudeConfig;
153
+ codex: UsageCodexConfig;
154
+ opencodeGo: UsageOpenCodeGoConfig;
155
+ }
156
+
157
+ export interface RetentionConfig {
158
+ /** Maximum sessions retained in memory before pruning inactive oldest sessions */
159
+ maxSessions: number;
160
+ /** Maximum inactive standalone session age in hours (0 = disabled) */
161
+ standaloneMaxAgeHours: number;
162
+ /** TTL for excluded Claude local-command IDs in hours */
163
+ localCommandTtlHours: number;
164
+ /** Maximum entries retained by bounded in-memory caches */
165
+ cacheMaxEntries: number;
166
+ /** Maximum Claude transcript bytes to parse from the tail of large files */
167
+ transcriptMaxBytes: number;
168
+ }
169
+
170
+ export interface ScrylogConfig {
171
+ daemon: DaemonConfig;
172
+ api: ApiConfig;
173
+ cors: CorsConfig;
174
+ opencode: OpenCodeConfig;
175
+ claude: ClaudeConfig;
176
+ codex: CodexConfig;
177
+ tmux: TmuxConfig;
178
+ tui: TuiConfig;
179
+ mcp: McpConfig;
180
+ usage: UsageConfig;
181
+ retention: RetentionConfig;
182
+ }
183
+
184
+ // ── Defaults ──────────────────────────────────────────────────────────────────
185
+
186
+ export const CONFIG_DEFAULTS: ScrylogConfig = {
187
+ daemon: {
188
+ pollIntervalMs: 10_000,
189
+ commandTimeoutMs: 5_000,
190
+ networkTimeoutMs: 10_000,
191
+ logLevel: "info",
192
+ logFormat: "auto",
193
+ logFileEnabled: true,
194
+ logFilePath: getDaemonLogPath(),
195
+ logFileMaxBytes: 5 * 1024 * 1024,
196
+ logFileMaxFiles: 3,
197
+ },
198
+ api: {
199
+ tcpEnabled: IS_WINDOWS,
200
+ host: "127.0.0.1",
201
+ port: 7788,
202
+ sseKeepaliveMs: 15_000,
203
+ },
204
+ cors: {
205
+ enabled: false,
206
+ origins: [],
207
+ methods: ["GET", "POST", "OPTIONS"],
208
+ allowedHeaders: ["Content-Type", "Authorization"],
209
+ exposedHeaders: [],
210
+ credentials: false,
211
+ maxAge: 600,
212
+ },
213
+ opencode: {
214
+ mode: "hooks",
215
+ instances: [],
216
+ autoDiscover: true,
217
+ },
218
+ claude: {
219
+ hooksEnabled: false,
220
+ hooksDir: getHooksDir("claude-hooks"),
221
+ },
222
+ codex: {
223
+ hooksEnabled: false,
224
+ hooksDir: getHooksDir("codex-hooks"),
225
+ },
226
+ tmux: {
227
+ previewLines: 50,
228
+ detailLines: 200,
229
+ },
230
+ tui: {
231
+ refreshMs: 5_000,
232
+ showStandalone: true,
233
+ usePushUpdates: true,
234
+ standaloneMaxAgeHours: 6,
235
+ pushDebounceMs: 120,
236
+ },
237
+ mcp: {
238
+ stdioEnabled: true,
239
+ httpEnabled: false,
240
+ host: "127.0.0.1",
241
+ port: 7789,
242
+ },
243
+ usage: {
244
+ enabled: true,
245
+ pollIntervalMs: 300_000,
246
+ providers: ["claude", "opencode-go", "codex"],
247
+ claude: {
248
+ dataSource: "auto",
249
+ webExtrasEnabled: true,
250
+ },
251
+ codex: {
252
+ dataSource: "auto",
253
+ },
254
+ opencodeGo: {
255
+ serverFnId: "def39973159c7f0483d8793a822b8dbb10d067e12c65455fcb4608459ba0234f",
256
+ },
257
+ },
258
+ retention: {
259
+ maxSessions: 500,
260
+ standaloneMaxAgeHours: 168,
261
+ localCommandTtlHours: 72,
262
+ cacheMaxEntries: 1_000,
263
+ transcriptMaxBytes: 10 * 1024 * 1024,
264
+ },
265
+ };
266
+
267
+ // ── Resolution ────────────────────────────────────────────────────────────────
268
+
269
+ /** Ordered list of default config file locations to probe */
270
+ export function getDefaultConfigPaths(): string[] {
271
+ return [getDefaultConfigPath(), getLegacyConfigPath()];
272
+ }
273
+
274
+ export function getResolvedConfigPath(explicitPath?: string): string {
275
+ if (explicitPath) {
276
+ return resolve(explicitPath);
277
+ }
278
+ if (process.env.SCRYLOG_CONFIG) {
279
+ return resolve(process.env.SCRYLOG_CONFIG);
280
+ }
281
+ for (const path of getDefaultConfigPaths()) {
282
+ if (existsSync(path)) {
283
+ return path;
284
+ }
285
+ }
286
+ return getDefaultConfigPath();
287
+ }
288
+
289
+ /**
290
+ * Load and merge config from:
291
+ * 1. --config flag value
292
+ * 2. SCRYLOG_CONFIG env var
293
+ * 3. ~/.config/scrylog/config.json
294
+ * 4. ~/.scrylog.json
295
+ * 5. Built-in defaults
296
+ */
297
+ export function loadConfig(explicitPath?: string): ScrylogConfig {
298
+ const paths: string[] = [];
299
+
300
+ if (explicitPath) {
301
+ paths.push(resolve(explicitPath));
302
+ } else if (process.env.SCRYLOG_CONFIG) {
303
+ paths.push(resolve(process.env.SCRYLOG_CONFIG));
304
+ } else {
305
+ paths.push(...getDefaultConfigPaths());
306
+ }
307
+
308
+ for (const path of paths) {
309
+ if (existsSync(path)) {
310
+ return loadConfigFile(path);
311
+ }
312
+ }
313
+
314
+ return { ...CONFIG_DEFAULTS };
315
+ }
316
+
317
+ function loadConfigFile(path: string): ScrylogConfig {
318
+ const raw = readConfigOverride(path);
319
+ if (!raw) {
320
+ return { ...CONFIG_DEFAULTS };
321
+ }
322
+
323
+ return normalizeConfig(raw);
324
+ }
325
+
326
+ export function readConfigOverride(path: string): PlainObject | null {
327
+ let raw: unknown;
328
+
329
+ try {
330
+ const content = readFileSync(path, "utf8");
331
+ raw = JSON.parse(content);
332
+ } catch (error) {
333
+ process.stderr.write(
334
+ `[scrylog] Warning: could not parse config file ${path}: ${error instanceof Error ? error.message : String(error)}\n`,
335
+ );
336
+ return null;
337
+ }
338
+
339
+ if (!raw || typeof raw !== "object") {
340
+ return null;
341
+ }
342
+
343
+ return raw as PlainObject;
344
+ }
345
+
346
+ export function normalizeConfig(raw: PlainObject): ScrylogConfig {
347
+ const merged = deepMerge(CONFIG_DEFAULTS as unknown as PlainObject, raw as Partial<PlainObject>) as unknown as ScrylogConfig;
348
+ return normalizeOperationalConfig(applyLegacyCompatibility(merged, raw));
349
+ }
350
+
351
+ export function writeConfig(config: ScrylogConfig, explicitPath?: string): string {
352
+ const path = getResolvedConfigPath(explicitPath);
353
+ ensureConfigDir();
354
+ ensureParentDir(path);
355
+ const tempPath = `${path}.tmp`;
356
+ writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
357
+ renameSync(tempPath, path);
358
+ return path;
359
+ }
360
+
361
+ export function updateConfig(
362
+ updater: (current: ScrylogConfig) => ScrylogConfig,
363
+ explicitPath?: string,
364
+ ): { path: string; config: ScrylogConfig } {
365
+ const path = getResolvedConfigPath(explicitPath);
366
+ const current = existsSync(path) ? loadConfig(path) : { ...CONFIG_DEFAULTS };
367
+ const updated = updater(current);
368
+ writeConfig(updated, path);
369
+ return { path, config: updated };
370
+ }
371
+
372
+ // ── Deep merge ────────────────────────────────────────────────────────────────
373
+
374
+ type PlainObject = Record<string, unknown>;
375
+
376
+ function isPlainObject(value: unknown): value is PlainObject {
377
+ return typeof value === "object" && value !== null && !Array.isArray(value);
378
+ }
379
+
380
+ export function deepMerge<T extends PlainObject>(base: T, override: Partial<T>): T {
381
+ const result: PlainObject = { ...base };
382
+
383
+ for (const key of Object.keys(override)) {
384
+ const baseValue = result[key];
385
+ const overrideValue = (override as PlainObject)[key];
386
+
387
+ if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
388
+ result[key] = deepMerge(baseValue, overrideValue);
389
+ } else if (overrideValue !== undefined) {
390
+ result[key] = overrideValue;
391
+ }
392
+ }
393
+
394
+ return result as T;
395
+ }
396
+
397
+ function applyLegacyCompatibility(config: ScrylogConfig, raw: PlainObject): ScrylogConfig {
398
+ const rawDaemon = isPlainObject(raw["daemon"]) ? raw["daemon"] : undefined;
399
+ const rawApi = isPlainObject(raw["api"]) ? raw["api"] : undefined;
400
+ const rawMcp = isPlainObject(raw["mcp"]) ? raw["mcp"] : undefined;
401
+
402
+ const next: ScrylogConfig = {
403
+ ...config,
404
+ api: { ...config.api },
405
+ daemon: { ...config.daemon },
406
+ mcp: { ...config.mcp },
407
+ };
408
+
409
+ if (!rawApi && rawDaemon) {
410
+ const legacyHost = typeof rawDaemon["host"] === "string" ? rawDaemon["host"] : undefined;
411
+ const legacyPort = typeof rawDaemon["port"] === "number" ? rawDaemon["port"] : undefined;
412
+ if (legacyHost) next.api.host = legacyHost;
413
+ if (legacyPort !== undefined) next.api.port = legacyPort;
414
+ if (legacyHost || legacyPort !== undefined) {
415
+ next.api.tcpEnabled = legacyPort === 0 ? false : true;
416
+ }
417
+ }
418
+
419
+ if (rawMcp && rawMcp["httpEnabled"] === undefined) {
420
+ const legacyPort = typeof rawMcp["port"] === "number" ? rawMcp["port"] : undefined;
421
+ const legacyHost = typeof rawMcp["host"] === "string" ? rawMcp["host"] : undefined;
422
+ const legacyToken = typeof rawMcp["bearerToken"] === "string" ? rawMcp["bearerToken"] : undefined;
423
+ if (legacyHost) next.mcp.host = legacyHost;
424
+ if (legacyPort !== undefined) next.mcp.port = legacyPort;
425
+ if (legacyToken) next.mcp.bearerToken = legacyToken;
426
+ if (legacyHost || legacyPort !== undefined || legacyToken) {
427
+ next.mcp.httpEnabled = true;
428
+ }
429
+ }
430
+
431
+ return next;
432
+ }
433
+
434
+ function normalizeOperationalConfig(config: ScrylogConfig): ScrylogConfig {
435
+ return {
436
+ ...config,
437
+ daemon: {
438
+ ...config.daemon,
439
+ pollIntervalMs: clampNumber(config.daemon.pollIntervalMs, 1_000, 300_000, CONFIG_DEFAULTS.daemon.pollIntervalMs),
440
+ commandTimeoutMs: clampNumber(config.daemon.commandTimeoutMs, 1_000, 120_000, CONFIG_DEFAULTS.daemon.commandTimeoutMs),
441
+ networkTimeoutMs: clampNumber(config.daemon.networkTimeoutMs, 1_000, 120_000, CONFIG_DEFAULTS.daemon.networkTimeoutMs),
442
+ logFileMaxBytes: clampNumber(config.daemon.logFileMaxBytes, 0, 100 * 1024 * 1024, CONFIG_DEFAULTS.daemon.logFileMaxBytes),
443
+ logFileMaxFiles: clampNumber(config.daemon.logFileMaxFiles, 1, 20, CONFIG_DEFAULTS.daemon.logFileMaxFiles),
444
+ },
445
+ api: {
446
+ ...config.api,
447
+ sseKeepaliveMs: clampNumber(config.api.sseKeepaliveMs, 0, 120_000, CONFIG_DEFAULTS.api.sseKeepaliveMs),
448
+ },
449
+ tmux: {
450
+ ...config.tmux,
451
+ previewLines: clampNumber(config.tmux.previewLines, 0, 500, CONFIG_DEFAULTS.tmux.previewLines),
452
+ detailLines: clampNumber(config.tmux.detailLines, 0, 5_000, CONFIG_DEFAULTS.tmux.detailLines),
453
+ },
454
+ tui: {
455
+ ...config.tui,
456
+ refreshMs: clampNumber(config.tui.refreshMs, 500, 300_000, CONFIG_DEFAULTS.tui.refreshMs),
457
+ standaloneMaxAgeHours: clampNumber(config.tui.standaloneMaxAgeHours, 0, 8_760, CONFIG_DEFAULTS.tui.standaloneMaxAgeHours),
458
+ pushDebounceMs: clampNumber(config.tui.pushDebounceMs, 0, 2_000, CONFIG_DEFAULTS.tui.pushDebounceMs),
459
+ },
460
+ usage: {
461
+ ...config.usage,
462
+ pollIntervalMs: clampNumber(config.usage.pollIntervalMs, 60_000, 3_600_000, CONFIG_DEFAULTS.usage.pollIntervalMs),
463
+ },
464
+ retention: {
465
+ ...config.retention,
466
+ maxSessions: clampNumber(config.retention.maxSessions, 0, 100_000, CONFIG_DEFAULTS.retention.maxSessions),
467
+ standaloneMaxAgeHours: clampNumber(config.retention.standaloneMaxAgeHours, 0, 8_760, CONFIG_DEFAULTS.retention.standaloneMaxAgeHours),
468
+ localCommandTtlHours: clampNumber(config.retention.localCommandTtlHours, 1, 8_760, CONFIG_DEFAULTS.retention.localCommandTtlHours),
469
+ cacheMaxEntries: clampNumber(config.retention.cacheMaxEntries, 10, 100_000, CONFIG_DEFAULTS.retention.cacheMaxEntries),
470
+ transcriptMaxBytes: clampNumber(config.retention.transcriptMaxBytes, 64 * 1024, 100 * 1024 * 1024, CONFIG_DEFAULTS.retention.transcriptMaxBytes),
471
+ },
472
+ };
473
+ }
474
+
475
+ function clampNumber(value: number, min: number, max: number, fallback: number): number {
476
+ if (!Number.isFinite(value)) return fallback;
477
+ return Math.min(max, Math.max(min, Math.trunc(value)));
478
+ }
@@ -0,0 +1,80 @@
1
+ import { join } from "node:path";
2
+ import { IS_WINDOWS, getUserId } from "./platform";
3
+
4
+ /**
5
+ * @deprecated Import IS_WINDOWS and getUserId from `shared/platform.ts` instead.
6
+ * Re-exported here for backward compatibility during the migration.
7
+ */
8
+ export { IS_WINDOWS, getUserId };
9
+
10
+ /**
11
+ * Return the Unix socket path for the daemon.
12
+ * Includes the current user's uid so multiple users on the same host don't conflict.
13
+ */
14
+ export function getSocketPath(override?: string): string {
15
+ if (override) {
16
+ return override;
17
+ }
18
+
19
+ const uid = getUserId();
20
+ return join("/tmp", `scrylog-${uid}.sock`);
21
+ }
22
+
23
+ /**
24
+ * Build the base URL used for daemon HTTP connections.
25
+ * - On macOS/Linux: HTTP over Unix socket via the `unix://` path convention
26
+ * - On Windows (or when TCP is forced): regular TCP URL
27
+ */
28
+ export function getDaemonUrl(options: {
29
+ host: string;
30
+ port: number;
31
+ socketPath?: string;
32
+ forceTcp?: boolean;
33
+ }): string {
34
+ if (!IS_WINDOWS && !options.forceTcp) {
35
+ return `http://localhost`;
36
+ }
37
+
38
+ return `http://${options.host}:${options.port}`;
39
+ }
40
+
41
+ /**
42
+ * Create a `fetch` function that routes requests over the Unix socket.
43
+ * Falls back to a plain `fetch` pointing at the TCP address on Windows.
44
+ * Injects an Authorization header when bearerToken is provided.
45
+ */
46
+ export function createDaemonFetch(options: {
47
+ host: string;
48
+ port: number;
49
+ socketPath?: string;
50
+ forceTcp?: boolean;
51
+ bearerToken?: string;
52
+ }): (input: string | URL | Request, init?: RequestInit) => Promise<Response> {
53
+ const headers: Record<string, string> = options.bearerToken
54
+ ? { Authorization: `Bearer ${options.bearerToken}` }
55
+ : {};
56
+
57
+ if (IS_WINDOWS || options.forceTcp) {
58
+ const base = `http://${options.host}:${options.port}`;
59
+ return (input, init) => {
60
+ const url = typeof input === "string" && input.startsWith("/") ? `${base}${input}` : input;
61
+ return fetch(url, {
62
+ ...init,
63
+ headers: { ...headers, ...(init?.headers ?? {}) },
64
+ });
65
+ };
66
+ }
67
+
68
+ const socketPath = options.socketPath ?? getSocketPath();
69
+
70
+ // Bun supports Unix socket via `unix` field in fetch options
71
+ return (input, init) => {
72
+ const url = typeof input === "string" && input.startsWith("/") ? `http://localhost${input}` : input;
73
+ // @ts-ignore -- Bun-specific `unix` option not in standard types
74
+ return fetch(url, {
75
+ ...init,
76
+ unix: socketPath,
77
+ headers: { ...headers, ...(init?.headers ?? {}) },
78
+ });
79
+ };
80
+ }