@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.
- package/LICENSE +21 -0
- package/README.md +836 -0
- package/package.json +75 -0
- package/src/api/docs/sessions.keys.ts +6 -0
- package/src/api/docs/sessions.ts +81 -0
- package/src/api/docs/system.keys.ts +5 -0
- package/src/api/docs/system.ts +66 -0
- package/src/api/docs/usage.keys.ts +3 -0
- package/src/api/docs/usage.ts +24 -0
- package/src/api/docs/webhooks.keys.ts +5 -0
- package/src/api/docs/webhooks.ts +78 -0
- package/src/api/index.ts +18 -0
- package/src/api/lib/auth.ts +43 -0
- package/src/api/lib/errors.ts +28 -0
- package/src/api/lib/route.ts +18 -0
- package/src/api/models/claude.ts +4 -0
- package/src/api/models/codex.ts +4 -0
- package/src/api/models/opencode.ts +11 -0
- package/src/api/routes/sessions.ts +47 -0
- package/src/api/routes/system.ts +103 -0
- package/src/api/routes/usage.ts +15 -0
- package/src/api/routes/webhooks/claude.ts +161 -0
- package/src/api/routes/webhooks/codex.ts +175 -0
- package/src/api/routes/webhooks/opencode.ts +107 -0
- package/src/api/routes.ts +93 -0
- package/src/api/schemas/common.ts +21 -0
- package/src/api/schemas/sessions.ts +72 -0
- package/src/api/schemas/usage.ts +50 -0
- package/src/api/schemas/webhooks/claude.ts +32 -0
- package/src/api/schemas/webhooks/codex.ts +34 -0
- package/src/api/schemas/webhooks/opencode.ts +29 -0
- package/src/api/services/claude-session.ts +9 -0
- package/src/api/services/codex-session.ts +8 -0
- package/src/api/services/opencode-session.ts +73 -0
- package/src/classifier.ts +206 -0
- package/src/cli/commands.ts +498 -0
- package/src/cli/prompt.ts +44 -0
- package/src/daemon/cli.ts +27 -0
- package/src/daemon/collectors/claude/README.md +108 -0
- package/src/daemon/collectors/claude/collector.ts +413 -0
- package/src/daemon/collectors/claude/discovery.ts +148 -0
- package/src/daemon/collectors/claude/hooks.ts +181 -0
- package/src/daemon/collectors/claude/index.ts +8 -0
- package/src/daemon/collectors/claude/jsonl.ts +85 -0
- package/src/daemon/collectors/claude/transcript.ts +246 -0
- package/src/daemon/collectors/codex/README.md +147 -0
- package/src/daemon/collectors/codex/collector.ts +661 -0
- package/src/daemon/collectors/codex/discovery.ts +104 -0
- package/src/daemon/collectors/codex/hooks.ts +179 -0
- package/src/daemon/collectors/codex/index.ts +7 -0
- package/src/daemon/collectors/codex/jsonl.ts +108 -0
- package/src/daemon/collectors/codex/transcript.ts +299 -0
- package/src/daemon/collectors/git.ts +295 -0
- package/src/daemon/collectors/opencode/README.md +106 -0
- package/src/daemon/collectors/opencode/collector.ts +543 -0
- package/src/daemon/collectors/opencode/discovery.ts +257 -0
- package/src/daemon/collectors/opencode/index.ts +5 -0
- package/src/daemon/collectors/opencode/session-builder.ts +59 -0
- package/src/daemon/collectors/opencode-db.ts +282 -0
- package/src/daemon/collectors/process.ts +201 -0
- package/src/daemon/collectors/tmux/README.md +128 -0
- package/src/daemon/collectors/tmux/collector.ts +472 -0
- package/src/daemon/collectors/tmux/enrichment.ts +241 -0
- package/src/daemon/collectors/tmux/index.ts +4 -0
- package/src/daemon/collectors/tmux/parse.ts +128 -0
- package/src/daemon/collectors/usage.ts +193 -0
- package/src/daemon/index.ts +207 -0
- package/src/daemon/scheduler.ts +86 -0
- package/src/daemon/store.ts +535 -0
- package/src/detect.ts +143 -0
- package/src/hooks/claude.ts +596 -0
- package/src/hooks/codex.ts +517 -0
- package/src/legacy/classifier.ts +215 -0
- package/src/legacy/detect.ts +143 -0
- package/src/mcp/client-server.ts +21 -0
- package/src/mcp/http-handler.ts +126 -0
- package/src/mcp/index.ts +61 -0
- package/src/mcp/resources/config-current.ts +19 -0
- package/src/mcp/resources/index.ts +11 -0
- package/src/mcp/resources/sessions-active.ts +19 -0
- package/src/mcp/resources/sessions-all.ts +19 -0
- package/src/mcp/schemas/sessions.ts +3 -0
- package/src/mcp/server.ts +17 -0
- package/src/mcp/services/api-adapter.ts +48 -0
- package/src/mcp/services/data-source.ts +22 -0
- package/src/mcp/services/store-adapter.ts +40 -0
- package/src/mcp/tools/get-active-sessions.ts +16 -0
- package/src/mcp/tools/get-session-children.ts +24 -0
- package/src/mcp/tools/get-session-messages.ts +24 -0
- package/src/mcp/tools/get-session.ts +23 -0
- package/src/mcp/tools/index.ts +15 -0
- package/src/mcp/tools/list-sessions.ts +16 -0
- package/src/neo-blessed.d.ts +4 -0
- package/src/oc/index.ts +302 -0
- package/src/service/manager.ts +242 -0
- package/src/shared/config.ts +478 -0
- package/src/shared/ipc.ts +80 -0
- package/src/shared/logger.ts +248 -0
- package/src/shared/paths.ts +73 -0
- package/src/shared/platform.ts +110 -0
- package/src/shared/session-view.ts +241 -0
- package/src/shared/types.ts +299 -0
- package/src/shared/utils.ts +339 -0
- package/src/tui/app.ts +920 -0
- package/src/tui/client.ts +149 -0
- package/src/tui/index.ts +181 -0
- package/src/types.ts +113 -0
- package/src/usage/README.md +323 -0
- package/src/usage/api.ts +99 -0
- package/src/usage/client.ts +3 -0
- package/src/usage/clients/anthropic-oauth-usage.ts +178 -0
- package/src/usage/clients/anthropic-web-usage.ts +245 -0
- package/src/usage/clients/claude-cli-probe.ts +187 -0
- package/src/usage/clients/codex-oauth-usage.ts +113 -0
- package/src/usage/clients/opencode-go-usage.ts +441 -0
- package/src/usage/index.ts +19 -0
- package/src/usage/mappers.ts +273 -0
- package/src/usage/planner.ts +54 -0
- package/src/usage/providers/claude.ts +113 -0
- package/src/usage/providers/codex.ts +34 -0
- package/src/usage/providers/opencode-go.ts +37 -0
- package/src/usage/resolver.ts +4 -0
- package/src/usage/resolvers/browser-cookies.ts +198 -0
- package/src/usage/resolvers/claude-auth.ts +99 -0
- package/src/usage/resolvers/claude-cli.ts +24 -0
- package/src/usage/resolvers/claude-web.ts +16 -0
- package/src/usage/resolvers/codex-auth.ts +117 -0
- package/src/usage/resolvers/opencode-go-web.ts +14 -0
- 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
|
+
}
|