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