@scrylog/cli 0.2.0 → 0.3.1

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 (171) hide show
  1. package/README.md +46 -7
  2. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  3. package/dist/assets/highlights-ghv9g403.scm +205 -0
  4. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  5. package/dist/assets/highlights-r812a2qc.scm +150 -0
  6. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  7. package/dist/assets/injections-73j83es3.scm +27 -0
  8. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  9. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  10. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/dist/cli/assets/providers/SOURCES.md +21 -0
  16. package/dist/cli/assets/providers/claude-terminal.png +0 -0
  17. package/dist/cli/assets/providers/claude-terminal.svg +62 -0
  18. package/dist/cli/assets/providers/claude.png +0 -0
  19. package/dist/cli/assets/providers/copilot.png +0 -0
  20. package/dist/cli/assets/providers/copilot.svg +44 -0
  21. package/dist/cli/assets/providers/openai-terminal.png +0 -0
  22. package/dist/cli/assets/providers/openai-terminal.svg +62 -0
  23. package/dist/cli/assets/providers/openai.png +0 -0
  24. package/dist/cli/assets/providers/opencode-terminal.png +0 -0
  25. package/dist/cli/assets/providers/opencode-terminal.svg +62 -0
  26. package/dist/cli/assets/providers/opencode.png +0 -0
  27. package/dist/cli/assets/providers/shell.png +0 -0
  28. package/dist/cli/assets/providers/shell.svg +23 -0
  29. package/dist/cli/assets/providers/unknown.png +0 -0
  30. package/dist/cli/assets/providers/unknown.svg +23 -0
  31. package/dist/cli/index.js +499 -0
  32. package/dist/cli/index.js.map +61 -0
  33. package/dist/daemon/index.js +670 -0
  34. package/dist/daemon/index.js.map +1475 -0
  35. package/dist/hooks/claude.js +6 -0
  36. package/dist/hooks/claude.js.map +11 -0
  37. package/dist/hooks/codex.js +10 -0
  38. package/dist/hooks/codex.js.map +11 -0
  39. package/dist/mcp/index.js +100 -0
  40. package/dist/mcp/index.js.map +231 -0
  41. package/dist/oc/index.js +21 -0
  42. package/dist/oc/index.js.map +10 -0
  43. package/package.json +26 -20
  44. package/src/postinstall.cjs +11 -0
  45. package/src/api/docs/sessions.keys.ts +0 -6
  46. package/src/api/docs/sessions.ts +0 -81
  47. package/src/api/docs/system.keys.ts +0 -5
  48. package/src/api/docs/system.ts +0 -66
  49. package/src/api/docs/usage.keys.ts +0 -3
  50. package/src/api/docs/usage.ts +0 -24
  51. package/src/api/docs/webhooks.keys.ts +0 -5
  52. package/src/api/docs/webhooks.ts +0 -78
  53. package/src/api/index.ts +0 -18
  54. package/src/api/lib/auth.ts +0 -43
  55. package/src/api/lib/errors.ts +0 -28
  56. package/src/api/lib/route.ts +0 -18
  57. package/src/api/models/claude.ts +0 -4
  58. package/src/api/models/codex.ts +0 -4
  59. package/src/api/models/opencode.ts +0 -11
  60. package/src/api/routes/sessions.ts +0 -47
  61. package/src/api/routes/system.ts +0 -103
  62. package/src/api/routes/usage.ts +0 -15
  63. package/src/api/routes/webhooks/claude.ts +0 -141
  64. package/src/api/routes/webhooks/codex.ts +0 -175
  65. package/src/api/routes/webhooks/opencode.ts +0 -111
  66. package/src/api/routes.ts +0 -93
  67. package/src/api/schemas/common.ts +0 -21
  68. package/src/api/schemas/sessions.ts +0 -72
  69. package/src/api/schemas/usage.ts +0 -50
  70. package/src/api/schemas/webhooks/claude.ts +0 -32
  71. package/src/api/schemas/webhooks/codex.ts +0 -34
  72. package/src/api/schemas/webhooks/opencode.ts +0 -29
  73. package/src/api/services/claude-session.ts +0 -9
  74. package/src/api/services/codex-session.ts +0 -8
  75. package/src/api/services/opencode-session.ts +0 -73
  76. package/src/classifier.ts +0 -206
  77. package/src/cli/commands.ts +0 -632
  78. package/src/cli/prompt.ts +0 -44
  79. package/src/daemon/cli.ts +0 -27
  80. package/src/daemon/collectors/claude/README.md +0 -108
  81. package/src/daemon/collectors/claude/collector.ts +0 -423
  82. package/src/daemon/collectors/claude/discovery.ts +0 -148
  83. package/src/daemon/collectors/claude/hooks.ts +0 -181
  84. package/src/daemon/collectors/claude/index.ts +0 -8
  85. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  86. package/src/daemon/collectors/claude/transcript.ts +0 -246
  87. package/src/daemon/collectors/codex/README.md +0 -147
  88. package/src/daemon/collectors/codex/collector.ts +0 -661
  89. package/src/daemon/collectors/codex/discovery.ts +0 -104
  90. package/src/daemon/collectors/codex/hooks.ts +0 -179
  91. package/src/daemon/collectors/codex/index.ts +0 -7
  92. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  93. package/src/daemon/collectors/codex/transcript.ts +0 -299
  94. package/src/daemon/collectors/git.ts +0 -295
  95. package/src/daemon/collectors/opencode/README.md +0 -106
  96. package/src/daemon/collectors/opencode/collector.ts +0 -545
  97. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  98. package/src/daemon/collectors/opencode/index.ts +0 -5
  99. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  100. package/src/daemon/collectors/opencode-db.ts +0 -282
  101. package/src/daemon/collectors/process.ts +0 -201
  102. package/src/daemon/collectors/tmux/README.md +0 -128
  103. package/src/daemon/collectors/tmux/collector.ts +0 -472
  104. package/src/daemon/collectors/tmux/enrichment.ts +0 -241
  105. package/src/daemon/collectors/tmux/index.ts +0 -4
  106. package/src/daemon/collectors/tmux/parse.ts +0 -128
  107. package/src/daemon/collectors/usage.ts +0 -193
  108. package/src/daemon/index.ts +0 -207
  109. package/src/daemon/scheduler.ts +0 -86
  110. package/src/daemon/store.ts +0 -588
  111. package/src/detect.ts +0 -143
  112. package/src/hooks/claude.ts +0 -596
  113. package/src/hooks/codex.ts +0 -517
  114. package/src/legacy/classifier.ts +0 -215
  115. package/src/legacy/detect.ts +0 -143
  116. package/src/mcp/client-server.ts +0 -21
  117. package/src/mcp/http-handler.ts +0 -126
  118. package/src/mcp/index.ts +0 -61
  119. package/src/mcp/resources/config-current.ts +0 -19
  120. package/src/mcp/resources/index.ts +0 -11
  121. package/src/mcp/resources/sessions-active.ts +0 -19
  122. package/src/mcp/resources/sessions-all.ts +0 -19
  123. package/src/mcp/schemas/sessions.ts +0 -3
  124. package/src/mcp/server.ts +0 -17
  125. package/src/mcp/services/api-adapter.ts +0 -48
  126. package/src/mcp/services/data-source.ts +0 -22
  127. package/src/mcp/services/store-adapter.ts +0 -40
  128. package/src/mcp/tools/get-active-sessions.ts +0 -16
  129. package/src/mcp/tools/get-session-children.ts +0 -24
  130. package/src/mcp/tools/get-session-messages.ts +0 -24
  131. package/src/mcp/tools/get-session.ts +0 -23
  132. package/src/mcp/tools/index.ts +0 -15
  133. package/src/mcp/tools/list-sessions.ts +0 -16
  134. package/src/neo-blessed.d.ts +0 -4
  135. package/src/oc/index.ts +0 -302
  136. package/src/service/manager.ts +0 -284
  137. package/src/shared/config.ts +0 -478
  138. package/src/shared/ipc.ts +0 -80
  139. package/src/shared/logger.ts +0 -248
  140. package/src/shared/paths.ts +0 -73
  141. package/src/shared/platform.ts +0 -110
  142. package/src/shared/session-view.ts +0 -241
  143. package/src/shared/types.ts +0 -299
  144. package/src/shared/utils.ts +0 -339
  145. package/src/tui/app.ts +0 -920
  146. package/src/tui/client.ts +0 -149
  147. package/src/tui/index.ts +0 -217
  148. package/src/types.ts +0 -113
  149. package/src/usage/README.md +0 -323
  150. package/src/usage/api.ts +0 -99
  151. package/src/usage/client.ts +0 -3
  152. package/src/usage/clients/anthropic-oauth-usage.ts +0 -178
  153. package/src/usage/clients/anthropic-web-usage.ts +0 -245
  154. package/src/usage/clients/claude-cli-probe.ts +0 -187
  155. package/src/usage/clients/codex-oauth-usage.ts +0 -113
  156. package/src/usage/clients/opencode-go-usage.ts +0 -441
  157. package/src/usage/index.ts +0 -20
  158. package/src/usage/mappers.ts +0 -273
  159. package/src/usage/planner.ts +0 -54
  160. package/src/usage/providers/claude.ts +0 -113
  161. package/src/usage/providers/codex.ts +0 -34
  162. package/src/usage/providers/opencode-go.ts +0 -37
  163. package/src/usage/resolver.ts +0 -4
  164. package/src/usage/resolvers/browser-cookies.ts +0 -470
  165. package/src/usage/resolvers/claude-auth.ts +0 -108
  166. package/src/usage/resolvers/claude-cli.ts +0 -24
  167. package/src/usage/resolvers/claude-desktop-auth.ts +0 -160
  168. package/src/usage/resolvers/claude-web.ts +0 -16
  169. package/src/usage/resolvers/codex-auth.ts +0 -117
  170. package/src/usage/resolvers/opencode-go-web.ts +0 -14
  171. package/src/usage/types.ts +0 -190
@@ -1,632 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, copyFileSync } from "node:fs";
2
- import { randomBytes } from "node:crypto";
3
- import { readFile } from "node:fs/promises";
4
- import { homedir } from "node:os";
5
- import { join } from "node:path";
6
- import { loadConfig, getResolvedConfigPath, updateConfig } from "../shared/config";
7
- import type { ScrylogConfig } from "../shared/config";
8
- import { getDaemonLogPath } from "../shared/paths";
9
- import { createPromptSession } from "./prompt";
10
- import { getServiceManagerOrThrow } from "../service/manager";
11
- import { createDaemonFetch, getSocketPath } from "../shared/ipc";
12
- import { IS_WINDOWS, whichCommand } from "../shared/platform";
13
-
14
- export interface GlobalCliOptions {
15
- configPath?: string;
16
- }
17
-
18
- export async function runDaemonCommand(action: string, options: GlobalCliOptions): Promise<void> {
19
- const manager = getServiceManagerOrThrow();
20
- if (action === "install") {
21
- manager.install(options.configPath);
22
- process.stdout.write("Installed daemon user service.\n");
23
- return;
24
- }
25
- if (action === "uninstall") {
26
- manager.uninstall();
27
- process.stdout.write("Uninstalled daemon user service.\n");
28
- return;
29
- }
30
- if (action === "start") {
31
- manager.start();
32
- process.stdout.write("Started daemon user service.\n");
33
- return;
34
- }
35
- if (action === "stop") {
36
- manager.stop();
37
- process.stdout.write("Stopped daemon user service.\n");
38
- return;
39
- }
40
- if (action === "restart") {
41
- manager.restart();
42
- process.stdout.write("Restarted daemon user service.\n");
43
- return;
44
- }
45
- if (action === "status") {
46
- const status = manager.status();
47
- process.stdout.write(`Platform: ${status.platform}\nInstalled: ${status.installed}\nRunning: ${status.running}\n${status.detail}\n`);
48
- return;
49
- }
50
- throw new Error(`Unknown daemon action: ${action}`);
51
- }
52
-
53
- export function runConfigCommand(action: string, options: GlobalCliOptions): void {
54
- if (action === "path") {
55
- process.stdout.write(`${getResolvedConfigPath(options.configPath)}\n`);
56
- return;
57
- }
58
- if (action === "show") {
59
- process.stdout.write(`${JSON.stringify(loadConfig(options.configPath), null, 2)}\n`);
60
- return;
61
- }
62
- throw new Error(`Unknown config action: ${action}`);
63
- }
64
-
65
- export function runTokenCommand(target: "mcp" | "api" | "both", action: string, options: GlobalCliOptions): void {
66
- if (action === "clear") {
67
- updateConfig((current) => {
68
- const next: ScrylogConfig = { ...current, mcp: { ...current.mcp }, api: { ...current.api } };
69
- if (target === "mcp" || target === "both") next.mcp.bearerToken = undefined;
70
- if (target === "api" || target === "both") next.api.bearerToken = undefined;
71
- return next;
72
- }, options.configPath);
73
- process.stdout.write(`Cleared ${target === "both" ? "API and MCP" : target.toUpperCase()} bearer token.\n`);
74
- return;
75
- }
76
-
77
- if (action === "generate" || action === "rotate") {
78
- const token = generateToken();
79
- updateConfig((current) => {
80
- const next: ScrylogConfig = { ...current, mcp: { ...current.mcp }, api: { ...current.api } };
81
- if (target === "mcp" || target === "both") next.mcp.bearerToken = token;
82
- if (target === "api" || target === "both") next.api.bearerToken = token;
83
- return next;
84
- }, options.configPath);
85
- process.stdout.write(`${token}\n`);
86
- return;
87
- }
88
-
89
- throw new Error(`Unknown token action: ${action}`);
90
- }
91
-
92
- export async function runLogsCommand(_options: GlobalCliOptions): Promise<void> {
93
- const logPath = getDaemonLogPath();
94
- process.stdout.write(`Log path: ${logPath}\n`);
95
- if (!existsSync(logPath)) {
96
- process.stdout.write("No daemon log file found.\n");
97
- return;
98
- }
99
- const content = await readFile(logPath, "utf8");
100
- const lines = content.trim().split(/\r?\n/);
101
- const recent = lines.slice(Math.max(0, lines.length - 40));
102
- process.stdout.write(`${recent.join("\n")}\n`);
103
- }
104
-
105
- export async function runDoctor(options: GlobalCliOptions): Promise<void> {
106
- const configPath = getResolvedConfigPath(options.configPath);
107
- const config = loadConfig(options.configPath);
108
- const daemonFetch = createDaemonFetch({
109
- host: config.api.host,
110
- port: config.api.port,
111
- socketPath: IS_WINDOWS ? undefined : getSocketPath(config.daemon.socketPath),
112
- });
113
-
114
- process.stdout.write(`Config path: ${configPath}\n`);
115
- process.stdout.write(`Config exists: ${existsSync(configPath)}\n`);
116
- process.stdout.write(`Socket path: ${IS_WINDOWS ? "n/a" : getSocketPath(config.daemon.socketPath)}\n`);
117
- process.stdout.write(`Log path: ${config.daemon.logFilePath}\n`);
118
- process.stdout.write(`Log file enabled: ${config.daemon.logFileEnabled}\n`);
119
- process.stdout.write(`Command timeout ms: ${config.daemon.commandTimeoutMs}\n`);
120
- process.stdout.write(`Network timeout ms: ${config.daemon.networkTimeoutMs}\n`);
121
- process.stdout.write(`Retention max sessions: ${config.retention.maxSessions}\n`);
122
- process.stdout.write(`Retention standalone max age hours: ${config.retention.standaloneMaxAgeHours}\n`);
123
- process.stdout.write(`Usage enabled: ${config.usage.enabled}\n`);
124
-
125
- try {
126
- const health = await daemonFetch("/api/health");
127
- process.stdout.write(`Local daemon reachable: ${health.ok}\n`);
128
- } catch {
129
- process.stdout.write("Local daemon reachable: false\n");
130
- }
131
-
132
- process.stdout.write(`TCP API enabled: ${config.api.tcpEnabled}\n`);
133
- process.stdout.write(`API bearer token configured: ${Boolean(config.api.bearerToken)}\n`);
134
- if (config.api.tcpEnabled) {
135
- try {
136
- const response = await fetch(`http://${config.api.host}:${config.api.port}/api/health`);
137
- process.stdout.write(`TCP API reachable: ${response.ok}\n`);
138
- } catch {
139
- process.stdout.write("TCP API reachable: false\n");
140
- }
141
- if (config.api.bearerToken) {
142
- try {
143
- const response = await fetch(`http://${config.api.host}:${config.api.port}/api/status`, {
144
- headers: { Authorization: `Bearer ${config.api.bearerToken}` },
145
- });
146
- process.stdout.write(`TCP API auth test: ${response.ok}\n`);
147
- } catch {
148
- process.stdout.write("TCP API auth test: false\n");
149
- }
150
- }
151
- }
152
-
153
- process.stdout.write(`CORS enabled: ${config.cors.enabled}\n`);
154
- if (config.cors.enabled) {
155
- process.stdout.write(`CORS origins: ${config.cors.origins.join(", ")}\n`);
156
- }
157
-
158
- process.stdout.write(`MCP HTTP enabled: ${config.mcp.httpEnabled}\n`);
159
- if (config.mcp.httpEnabled) {
160
- try {
161
- const response = await fetch(`http://${config.mcp.host}:${config.mcp.port}`, {
162
- method: "POST",
163
- headers: {
164
- Accept: "application/json, text/event-stream",
165
- "Content-Type": "application/json",
166
- ...(config.mcp.bearerToken ? { Authorization: `Bearer ${config.mcp.bearerToken}` } : {}),
167
- },
168
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping", params: {} }),
169
- });
170
- process.stdout.write(`MCP HTTP reachable: ${response.status >= 200 && response.status < 500}\n`);
171
- process.stdout.write(`MCP bearer token configured: ${Boolean(config.mcp.bearerToken)}\n`);
172
- } catch {
173
- process.stdout.write("MCP HTTP reachable: false\n");
174
- }
175
- }
176
-
177
- try {
178
- const manager = getServiceManagerOrThrow();
179
- const status = manager.status();
180
- process.stdout.write(`Service platform: ${status.platform}\nInstalled: ${status.installed}\nRunning: ${status.running}\n`);
181
- } catch (error) {
182
- process.stdout.write(`Service manager: ${error instanceof Error ? error.message : String(error)}\n`);
183
- }
184
-
185
- const tmux = Bun.spawnSync(["tmux", "-V"], { stdout: "pipe", stderr: "pipe" });
186
- process.stdout.write(`tmux available: ${tmux.exitCode === 0}\n`);
187
-
188
- if (config.opencode.mode === "hooks") {
189
- process.stdout.write("OpenCode mode: hooks\n");
190
- const opencodeSettingsPath = join(homedir(), ".config", "opencode", "opencode.json");
191
- process.stdout.write(`OpenCode plugin config present: ${existsSync(opencodeSettingsPath)}\n`);
192
- let pluginRegistered = false;
193
- if (existsSync(opencodeSettingsPath)) {
194
- try {
195
- const opencodeSettings = JSON.parse(readFileSync(opencodeSettingsPath, "utf8")) as OpenCodeSettings;
196
- pluginRegistered = (opencodeSettings.plugin ?? []).some(
197
- (plugin) => typeof plugin === "string" && plugin.includes(OPENCODE_PLUGIN_NAME),
198
- );
199
- } catch {
200
- // ignore parse errors; pluginRegistered stays false
201
- }
202
- }
203
- process.stdout.write(`OpenCode plugin registered: ${pluginRegistered}\n`);
204
- } else {
205
- process.stdout.write(`OpenCode mode: sse\nOpenCode auto-discover: ${config.opencode.autoDiscover}\nConfigured instances: ${config.opencode.instances.length}\n`);
206
- }
207
-
208
- process.stdout.write(`Claude hooks enabled: ${config.claude.hooksEnabled}\n`);
209
- if (config.claude.hooksEnabled) {
210
- const claudeSettingsPath = join(homedir(), ".claude", "settings.json");
211
- process.stdout.write(`Claude settings config present: ${existsSync(claudeSettingsPath)}\n`);
212
- }
213
-
214
- process.stdout.write(`Codex hooks enabled: ${config.codex.hooksEnabled}\n`);
215
- if (config.codex.hooksEnabled) {
216
- const codexHooksPath = join(homedir(), ".codex", "hooks.json");
217
- process.stdout.write(`Codex hooks config present: ${existsSync(codexHooksPath)}\n`);
218
- }
219
- }
220
-
221
- export async function runSetupWizard(options: GlobalCliOptions): Promise<void> {
222
- const prompt = createPromptSession();
223
- try {
224
- const resolvedPath = getResolvedConfigPath(options.configPath);
225
- const current = loadConfig(options.configPath);
226
- process.stdout.write(`Config path: ${resolvedPath}\n`);
227
-
228
- const configureOpenCode = await prompt.yesNo("Configure OpenCode now?", false);
229
- let next: ScrylogConfig = { ...current, api: { ...current.api }, mcp: { ...current.mcp }, opencode: { ...current.opencode }, daemon: { ...current.daemon }, cors: { ...current.cors }, retention: { ...current.retention }, usage: { ...current.usage } };
230
-
231
- if (configureOpenCode) {
232
- const mode = await prompt.choose("Select OpenCode mode", ["hooks", "sse"], current.opencode.mode === "hooks" ? 0 : 1);
233
- next.opencode.mode = mode as "hooks" | "sse";
234
- if (mode === "sse") {
235
- next.opencode.autoDiscover = await prompt.yesNo("Enable OpenCode auto-discovery?", current.opencode.autoDiscover);
236
- const instances = await prompt.text("Comma-separated OpenCode instance URLs", current.opencode.instances.join(","));
237
- next.opencode.instances = instances
238
- .split(",")
239
- .map((value) => value.trim())
240
- .filter(Boolean);
241
- } else {
242
- process.stdout.write("Hooks mode requires the @scrylog/opencode-plugin plugin in your OpenCode config.\n");
243
- const installPlugin = await prompt.yesNo("Run scrylog opencode hooks install now?", false);
244
- if (installPlugin) {
245
- await runOpenCodeHooksInstall(options);
246
- }
247
- }
248
- }
249
-
250
- if (isClaudeHooksInstalled()) {
251
- process.stdout.write("Claude hooks already installed.\n");
252
- } else {
253
- const installClaude = await prompt.yesNo("Install Claude hooks now?", false);
254
- if (installClaude) {
255
- await runClaudeHooksInstall(options);
256
- }
257
- }
258
-
259
- if (isCodexHooksInstalled()) {
260
- process.stdout.write("Codex hooks already installed.\n");
261
- } else {
262
- const installCodex = await prompt.yesNo("Install Codex hooks now?", false);
263
- if (installCodex) {
264
- await runCodexHooksInstall(options);
265
- }
266
- }
267
-
268
- const pollInterval = await prompt.text("Daemon poll interval (ms)", String(current.daemon.pollIntervalMs));
269
- const logLevel = await prompt.choose("Daemon log level", ["debug", "info", "warn", "error"], ["debug", "info", "warn", "error"].indexOf(current.daemon.logLevel));
270
- next.daemon.pollIntervalMs = parseOptionalInt(pollInterval, current.daemon.pollIntervalMs);
271
- next.daemon.logLevel = logLevel as ScrylogConfig["daemon"]["logLevel"];
272
- next.daemon.commandTimeoutMs = parseOptionalInt(await prompt.text("Command timeout (ms)", String(current.daemon.commandTimeoutMs)), current.daemon.commandTimeoutMs);
273
- next.daemon.networkTimeoutMs = parseOptionalInt(await prompt.text("Network timeout (ms)", String(current.daemon.networkTimeoutMs)), current.daemon.networkTimeoutMs);
274
- const socketPath = await prompt.text("Unix socket path (blank = default)", current.daemon.socketPath ?? "");
275
- next.daemon.socketPath = socketPath.trim() || undefined;
276
-
277
- next.retention.maxSessions = parseOptionalInt(await prompt.text("Retention max sessions", String(current.retention.maxSessions)), current.retention.maxSessions);
278
- next.retention.standaloneMaxAgeHours = parseOptionalInt(await prompt.text("Retention standalone max age (hours, 0 = disabled)", String(current.retention.standaloneMaxAgeHours)), current.retention.standaloneMaxAgeHours);
279
- next.usage.enabled = await prompt.yesNo("Enable usage collection?", current.usage.enabled);
280
-
281
- next.api.tcpEnabled = await prompt.yesNo("Enable TCP API listener?", current.api.tcpEnabled);
282
- if (next.api.tcpEnabled) {
283
- next.api.host = await prompt.text("TCP API host", current.api.host);
284
- next.api.port = parseOptionalInt(await prompt.text("TCP API port", String(current.api.port)), current.api.port);
285
- const tokenAction = await prompt.choose("API bearer token", ["generate", "keep current", "clear"], current.api.bearerToken ? 1 : 0);
286
- if (tokenAction === "generate") {
287
- next.api.bearerToken = generateToken();
288
- process.stdout.write(`Generated API token: ${next.api.bearerToken}\n`);
289
- } else if (tokenAction === "clear") {
290
- next.api.bearerToken = undefined;
291
- }
292
- }
293
-
294
- next.cors.enabled = await prompt.yesNo("Enable CORS for browser access?", current.cors.enabled);
295
- if (next.cors.enabled) {
296
- const originsRaw = await prompt.text("Allowed origins (comma-separated, or * for all)", current.cors.origins.join(",") || "*");
297
- next.cors.origins = originsRaw === "*"
298
- ? ["*"]
299
- : originsRaw.split(",").map((s) => s.trim()).filter(Boolean);
300
- next.cors.credentials = await prompt.yesNo("Allow credentials in CORS requests?", current.cors.credentials);
301
- }
302
-
303
- next.mcp.httpEnabled = await prompt.yesNo("Enable MCP HTTP listener?", current.mcp.httpEnabled);
304
- if (next.mcp.httpEnabled) {
305
- next.mcp.host = await prompt.text("MCP HTTP host", current.mcp.host);
306
- next.mcp.port = parseOptionalInt(await prompt.text("MCP HTTP port", String(current.mcp.port)), current.mcp.port);
307
- const tokenAction = await prompt.choose("MCP token", ["generate", "keep current", "clear"], current.mcp.bearerToken ? 1 : 0);
308
- if (tokenAction === "generate") {
309
- next.mcp.bearerToken = generateToken();
310
- process.stdout.write(`Generated MCP token: ${next.mcp.bearerToken}\n`);
311
- } else if (tokenAction === "clear") {
312
- next.mcp.bearerToken = undefined;
313
- }
314
- }
315
-
316
- updateConfig(() => next, options.configPath);
317
- process.stdout.write(`Wrote config to ${resolvedPath}\n`);
318
-
319
- const installService = await prompt.yesNo("Install daemon as a user service?", true);
320
- if (installService) {
321
- const manager = getServiceManagerOrThrow();
322
- manager.install(options.configPath);
323
- process.stdout.write("Installed daemon service.\n");
324
- if (await prompt.yesNo("Start or restart the service now?", true)) {
325
- manager.restart();
326
- process.stdout.write("Service restarted.\n");
327
- }
328
- }
329
-
330
- await runDoctor(options);
331
- } finally {
332
- prompt.close();
333
- }
334
- }
335
-
336
- export function generateToken(): string {
337
- return randomBytes(24).toString("base64url");
338
- }
339
-
340
- function parseOptionalInt(value: string, fallback: number): number {
341
- const parsed = Number.parseInt(value, 10);
342
- return Number.isNaN(parsed) ? fallback : parsed;
343
- }
344
-
345
- const OPENCODE_PLUGIN_NAME = "@scrylog/opencode-plugin";
346
-
347
- export function isClaudeHooksInstalled(homeDir = homedir()): boolean {
348
- const settingsPath = join(homeDir, ".claude", "settings.json");
349
- if (!existsSync(settingsPath)) return false;
350
- try {
351
- const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as ClaudeSettings;
352
- const hooks = settings.hooks ?? {};
353
- return Object.values(hooks).some((rules) =>
354
- rules.some((rule) =>
355
- rule.hooks.some((h) => typeof h.command === "string" && h.command.includes("scrylog-claude-hook")),
356
- ),
357
- );
358
- } catch {
359
- return false;
360
- }
361
- }
362
-
363
- export function isCodexHooksInstalled(homeDir = homedir()): boolean {
364
- const codexDir = process.env.CODEX_HOME ? process.env.CODEX_HOME : join(homeDir, ".codex");
365
- const hooksPath = join(codexDir, "hooks.json");
366
- if (!existsSync(hooksPath)) return false;
367
- try {
368
- const config = JSON.parse(readFileSync(hooksPath, "utf8")) as CodexHooksConfig;
369
- const hooks = config.hooks ?? {};
370
- return Object.values(hooks).some((rules) =>
371
- rules.some((rule) =>
372
- rule.hooks.some((h) => typeof h.command === "string" && h.command.includes("scrylog-codex-hook")),
373
- ),
374
- );
375
- } catch {
376
- return false;
377
- }
378
- }
379
-
380
- export function isOpencodePluginRegistered(homeDir = homedir()): boolean {
381
- const settingsPath = join(homeDir, ".config", "opencode", "opencode.json");
382
- if (!existsSync(settingsPath)) return false;
383
- try {
384
- const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as OpenCodeSettings;
385
- return (settings.plugin ?? []).some(
386
- (plugin) => typeof plugin === "string" && plugin.includes(OPENCODE_PLUGIN_NAME),
387
- );
388
- } catch {
389
- return false;
390
- }
391
- }
392
-
393
- function backupBeforeWrite(path: string): void {
394
- if (!existsSync(path)) return;
395
- const bak = `${path}.bak`;
396
- copyFileSync(path, bak);
397
- }
398
-
399
- interface OpenCodeSettings {
400
- plugin?: string[];
401
- [key: string]: unknown;
402
- }
403
-
404
- // ── Claude hooks setup ────────────────────────────────────────────────────────
405
-
406
- const CLAUDE_HOOK_EVENTS = [
407
- "SessionStart",
408
- "SessionEnd",
409
- "UserPromptSubmit",
410
- "PreToolUse",
411
- "PermissionRequest",
412
- "PermissionDenied",
413
- "PostToolUse",
414
- "PostToolUseFailure",
415
- "PostToolBatch",
416
- "Stop",
417
- "StopFailure",
418
- "SubagentStart",
419
- "SubagentStop",
420
- "CwdChanged",
421
- ] as const;
422
-
423
- interface ClaudeSettingsHookEntry {
424
- type: "command";
425
- command: string;
426
- }
427
-
428
- interface ClaudeSettingsHookRule {
429
- matcher: string;
430
- hooks: ClaudeSettingsHookEntry[];
431
- }
432
-
433
- interface ClaudeSettings {
434
- hooks?: Record<string, ClaudeSettingsHookRule[]>;
435
- [key: string]: unknown;
436
- }
437
-
438
- export async function runClaudeHooksInstall(options: GlobalCliOptions): Promise<void> {
439
- const claudeDir = join(homedir(), ".claude");
440
- const settingsPath = join(claudeDir, "settings.json");
441
-
442
- // Verify scrylog-claude-hook is in PATH
443
- const hookBin = whichCommand("scrylog-claude-hook") ?? "scrylog-claude-hook";
444
- const hookBinFound = hookBin !== "scrylog-claude-hook";
445
-
446
- if (!hookBinFound) {
447
- process.stderr.write(
448
- "Warning: scrylog-claude-hook not found in PATH. Make sure it is installed before Claude Code runs hooks.\n",
449
- );
450
- }
451
-
452
- // Load or create settings.json
453
- let settings: ClaudeSettings = {};
454
- if (existsSync(settingsPath)) {
455
- try {
456
- settings = JSON.parse(readFileSync(settingsPath, "utf8")) as ClaudeSettings;
457
- } catch {
458
- process.stderr.write(`Warning: could not parse ${settingsPath} — starting with empty settings.\n`);
459
- }
460
- } else {
461
- mkdirSync(claudeDir, { recursive: true });
462
- }
463
-
464
- if (isClaudeHooksInstalled()) {
465
- process.stdout.write("Claude hooks are already installed.\n");
466
- return;
467
- }
468
-
469
- // Merge hooks — add scrylog entries without removing existing ones
470
- const existingHooks = settings.hooks ?? {};
471
- const updatedHooks: Record<string, ClaudeSettingsHookRule[]> = { ...existingHooks };
472
-
473
- for (const event of CLAUDE_HOOK_EVENTS) {
474
- const existing = updatedHooks[event] ?? [];
475
- updatedHooks[event] = [
476
- ...existing,
477
- { matcher: "", hooks: [{ type: "command", command: hookBin }] },
478
- ];
479
- }
480
-
481
- settings.hooks = updatedHooks;
482
-
483
- // Write atomically with backup
484
- backupBeforeWrite(settingsPath);
485
- const tmp = `${settingsPath}.tmp`;
486
- writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
487
- renameSync(tmp, settingsPath);
488
-
489
- process.stdout.write(`Wrote Claude hooks to ${settingsPath}\n`);
490
- process.stdout.write(`Hooks installed: ${CLAUDE_HOOK_EVENTS.join(", ")}\n`);
491
- process.stdout.write(`Hook command: ${hookBin}\n`);
492
-
493
- // Enable hooksEnabled in scrylog config
494
- const { path: configPath } = updateConfig(
495
- (current) => ({ ...current, claude: { ...current.claude, hooksEnabled: true } }),
496
- options.configPath,
497
- );
498
- process.stdout.write(`Enabled claude.hooksEnabled in scrylog config: ${configPath}\n`);
499
- process.stdout.write("Done. Restart the scrylog daemon to apply changes.\n");
500
- }
501
-
502
- // ── Codex hooks setup ─────────────────────────────────────────────────────────
503
-
504
- const CODEX_HOOK_EVENTS = [
505
- "SessionStart",
506
- "PreToolUse",
507
- "PermissionRequest",
508
- "PostToolUse",
509
- "UserPromptSubmit",
510
- "SubagentStart",
511
- "SubagentStop",
512
- "Stop",
513
- ] as const;
514
-
515
- interface CodexHookEntry {
516
- type: "command";
517
- command: string;
518
- timeout?: number;
519
- }
520
-
521
- interface CodexHookRule {
522
- matcher?: string;
523
- hooks: CodexHookEntry[];
524
- }
525
-
526
- interface CodexHooksConfig {
527
- hooks?: Record<string, CodexHookRule[]>;
528
- [key: string]: unknown;
529
- }
530
-
531
- export async function runCodexHooksInstall(options: GlobalCliOptions): Promise<void> {
532
- const codexDir = process.env.CODEX_HOME ? process.env.CODEX_HOME : join(homedir(), ".codex");
533
- const hooksPath = join(codexDir, "hooks.json");
534
-
535
- // Verify scrylog-codex-hook is in PATH
536
- const hookBin = whichCommand("scrylog-codex-hook") ?? "scrylog-codex-hook";
537
- const hookBinFound = hookBin !== "scrylog-codex-hook";
538
-
539
- if (!hookBinFound) {
540
- process.stderr.write(
541
- "Warning: scrylog-codex-hook not found in PATH. Make sure it is installed before Codex CLI runs hooks.\n",
542
- );
543
- }
544
-
545
- // Load or create hooks.json
546
- let config: CodexHooksConfig = {};
547
- if (existsSync(hooksPath)) {
548
- try {
549
- config = JSON.parse(readFileSync(hooksPath, "utf8")) as CodexHooksConfig;
550
- } catch {
551
- process.stderr.write(`Warning: could not parse ${hooksPath} — starting with empty config.\n`);
552
- }
553
- } else {
554
- mkdirSync(codexDir, { recursive: true });
555
- }
556
-
557
- if (isCodexHooksInstalled()) {
558
- process.stdout.write("Codex hooks are already installed.\n");
559
- return;
560
- }
561
-
562
- // Merge hooks — add scrylog entries without removing existing ones
563
- const existingHooks = config.hooks ?? {};
564
- const updatedHooks: Record<string, CodexHookRule[]> = { ...existingHooks };
565
-
566
- for (const event of CODEX_HOOK_EVENTS) {
567
- const existing = updatedHooks[event] ?? [];
568
- const entry: CodexHookRule = {
569
- matcher: event === "UserPromptSubmit" || event === "Stop" ? undefined : "",
570
- hooks: [{ type: "command", command: hookBin, ...(event === "Stop" ? { timeout: 30 } : {}) }],
571
- };
572
- updatedHooks[event] = [...existing, entry];
573
- }
574
-
575
- config.hooks = updatedHooks;
576
-
577
- // Write atomically with backup
578
- backupBeforeWrite(hooksPath);
579
- const tmp = `${hooksPath}.tmp`;
580
- writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, "utf8");
581
- renameSync(tmp, hooksPath);
582
-
583
- process.stdout.write(`Wrote Codex hooks to ${hooksPath}\n`);
584
- process.stdout.write(`Hooks installed: ${CODEX_HOOK_EVENTS.join(", ")}\n`);
585
- process.stdout.write(`Hook command: ${hookBin}\n`);
586
-
587
- // Enable hooksEnabled in scrylog config
588
- const { path: configPath } = updateConfig(
589
- (current) => ({ ...current, codex: { ...current.codex, hooksEnabled: true } }),
590
- options.configPath,
591
- );
592
- process.stdout.write(`Enabled codex.hooksEnabled in scrylog config: ${configPath}\n`);
593
- process.stdout.write("Done. Restart the scrylog daemon to apply changes.\n");
594
- }
595
-
596
- // ── OpenCode hooks setup ──────────────────────────────────────────────────────
597
-
598
- export async function runOpenCodeHooksInstall(options: GlobalCliOptions): Promise<void> {
599
- const opencodeDir = join(homedir(), ".config", "opencode");
600
- const settingsPath = join(opencodeDir, "opencode.json");
601
-
602
- // Load or create opencode.json
603
- let settings: OpenCodeSettings = {};
604
- if (existsSync(settingsPath)) {
605
- try {
606
- settings = JSON.parse(readFileSync(settingsPath, "utf8")) as OpenCodeSettings;
607
- } catch {
608
- process.stderr.write(`Warning: could not parse ${settingsPath} — starting with empty settings.\n`);
609
- }
610
- } else {
611
- mkdirSync(opencodeDir, { recursive: true });
612
- }
613
-
614
- // Merge plugin entry without removing existing ones
615
- const existingPlugins = settings.plugin ?? [];
616
- const alreadyInstalled = isOpencodePluginRegistered();
617
-
618
- if (!alreadyInstalled) {
619
- settings.plugin = [...existingPlugins, OPENCODE_PLUGIN_NAME];
620
-
621
- // Write atomically with backup
622
- backupBeforeWrite(settingsPath);
623
- const tmp = `${settingsPath}.tmp`;
624
- writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
625
- renameSync(tmp, settingsPath);
626
- process.stdout.write(`Registered ${OPENCODE_PLUGIN_NAME} in ${settingsPath}\n`);
627
- } else {
628
- process.stdout.write(`${OPENCODE_PLUGIN_NAME} is already registered in ${settingsPath}\n`);
629
- }
630
-
631
- process.stdout.write("Done. Make sure the plugin is installed globally and restart OpenCode.\n");
632
- }
package/src/cli/prompt.ts DELETED
@@ -1,44 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { stdin as input, stdout as output } from "node:process";
3
-
4
- export interface PromptSession {
5
- text(message: string, defaultValue?: string): Promise<string>;
6
- yesNo(message: string, defaultValue?: boolean): Promise<boolean>;
7
- choose(message: string, options: string[], defaultIndex?: number): Promise<string>;
8
- close(): void;
9
- }
10
-
11
- export function createPromptSession(): PromptSession {
12
- const rl = createInterface({ input, output });
13
-
14
- return {
15
- async text(message: string, defaultValue?: string): Promise<string> {
16
- const suffix = defaultValue !== undefined ? ` [${defaultValue}]` : "";
17
- const answer = (await rl.question(`${message}${suffix}: `)).trim();
18
- return answer || defaultValue || "";
19
- },
20
- async yesNo(message: string, defaultValue = true): Promise<boolean> {
21
- const label = defaultValue ? "Y/n" : "y/N";
22
- const answer = (await rl.question(`${message} [${label}]: `)).trim().toLowerCase();
23
- if (!answer) return defaultValue;
24
- return answer === "y" || answer === "yes";
25
- },
26
- async choose(message: string, options: string[], defaultIndex = 0): Promise<string> {
27
- output.write(`${message}\n`);
28
- for (const [index, option] of options.entries()) {
29
- output.write(` ${index + 1}. ${option}\n`);
30
- }
31
- while (true) {
32
- const answer = (await rl.question(`Select [${defaultIndex + 1}]: `)).trim();
33
- const value = answer || String(defaultIndex + 1);
34
- const index = Number.parseInt(value, 10) - 1;
35
- if (!Number.isNaN(index) && index >= 0 && index < options.length) {
36
- return options[index]!;
37
- }
38
- }
39
- },
40
- close(): void {
41
- rl.close();
42
- },
43
- };
44
- }