@scrylog/cli 0.3.0 → 0.3.2

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,257 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync, readlinkSync } from "node:fs";
2
- import type { ProcessInfo } from "../../../shared/types";
3
- import { maybeRunCommand } from "../../../shared/utils";
4
- import { IS_LINUX, IS_WINDOWS } from "../../../shared/platform";
5
- import type { Logger } from "../../../shared/logger";
6
- import { parseWindowsProcesses } from "../process";
7
-
8
- const OPENCODE_DEFAULT_PORT = 4096;
9
-
10
- /**
11
- * Subcommands of opencode that are NOT local servers.
12
- * Processes running these should not be matched as opencode server instances.
13
- */
14
- const OPENCODE_NON_SERVER_SUBCOMMANDS = /^(attach|check)\b/i;
15
-
16
- /**
17
- * Returns true if the command string represents an opencode executable invocation
18
- * (not opencode as an argument to another process, or opencode as a directory component).
19
- *
20
- * Valid executable forms:
21
- * - `opencode [args]` — bare name, first token
22
- * - `/path/to/opencode [args]` — absolute path, first token
23
- * - `node /path/to/opencode [args]` — Node.js runner
24
- * - `bun /path/to/opencode [args]` — Bun runner
25
- *
26
- * Rejected forms:
27
- * - `tmux new-session ... opencode` — opencode is an argument to tmux
28
- * - `/Users/x/.opencode/node_modules/.../binary` — opencode is a directory in the path
29
- */
30
- function isOpencodeExecutable(command: string): boolean {
31
- const tokens = command.trim().split(/\s+/);
32
- const first = tokens[0] ?? "";
33
- const second = tokens[1] ?? "";
34
-
35
- // First token ends with /opencode or IS opencode (bare name)
36
- if (/(?:^|[\\/])opencode(?:\.exe)?$/i.test(first)) return true;
37
-
38
- // Interpreter (node/bun) with second token being the opencode script
39
- if (/^(node|bun)(?:\.exe)?$/i.test(first) && /(?:^|[\\/])opencode(?:\.exe)?$/i.test(second)) return true;
40
-
41
- return false;
42
- }
43
-
44
- /**
45
- * Given a process command string, attempt to extract the OpenCode HTTP URL.
46
- * Matches:
47
- * - `opencode serve [--port N] [--hostname H]`
48
- * - `opencode [--port N]` (TUI mode with explicit port)
49
- * - `opencode` (TUI mode, no args — assumes default port 4096 on localhost)
50
- * Returns undefined if the command is not an OpenCode server process.
51
- */
52
- export function parseOpencodeUrl(command: string): string | undefined {
53
- if (!isOpencodeExecutable(command)) return undefined;
54
-
55
- // Extract the subcommand/args portion (everything after the executable token)
56
- const tokens = command.trim().split(/\s+/);
57
- const isInterpreted = /^(node|bun)(?:\.exe)?$/i.test(tokens[0] ?? "");
58
- const args = tokens.slice(isInterpreted ? 2 : 1).join(" ");
59
-
60
- // Client-only subcommands — not a local server
61
- if (OPENCODE_NON_SERVER_SUBCOMMANDS.test(args.trim())) return undefined;
62
-
63
- const portMatch = command.match(/--port[=\s]+(\d+)/);
64
- const hostMatch = command.match(/--hostname[=\s]+(\S+)/);
65
- const port = portMatch ? Number.parseInt(portMatch[1] ?? "", 10) : OPENCODE_DEFAULT_PORT;
66
- const rawHost = hostMatch ? (hostMatch[1] ?? "127.0.0.1") : "127.0.0.1";
67
- const host = rawHost === "0.0.0.0" ? "127.0.0.1" : rawHost;
68
- return `http://${host}:${port}`;
69
- }
70
-
71
- /**
72
- * Returns true if the command explicitly specifies a --port argument.
73
- * Used to decide whether lsof-based port discovery is needed.
74
- */
75
- export function hasExplicitPort(command: string): boolean {
76
- return /--port[=\s]+\d+/.test(command);
77
- }
78
-
79
- /**
80
- * Use `lsof` to find the TCP port an opencode process is listening on.
81
- * Runs `lsof -nP -p <pid>` and looks for a LISTEN TCP entry.
82
- * Returns the port number, or undefined if none found.
83
- *
84
- * This is needed when opencode is started without `--port` (TUI mode),
85
- * in which case it binds to a random ephemeral port.
86
- */
87
- export function discoverListeningPort(pid: number, timeoutMs?: number): number | undefined {
88
- const lsofResult = maybeRunCommand(["lsof", "-nP", "-p", String(pid)], timeoutMs);
89
- if (lsofResult.ok) {
90
- for (const line of lsofResult.output.split(/\r?\n/)) {
91
- // Match lines like: opencode 12345 user 10u IPv4 ... TCP 127.0.0.1:59871 (LISTEN)
92
- const m = line.match(/TCP\s+[\w.*]+:(\d+)\s+\(LISTEN\)/i);
93
- if (m) {
94
- const port = Number.parseInt(m[1] ?? "", 10);
95
- if (Number.isFinite(port) && port > 0) return port;
96
- }
97
- }
98
- }
99
-
100
- // Fallback for minimal Linux containers/WSL without lsof: read /proc/<pid>/net/tcp
101
- if (IS_LINUX) {
102
- return discoverListeningPortFromProc(pid);
103
- }
104
-
105
- return undefined;
106
- }
107
-
108
- export function parseProcNetTcpListeningPorts(content: string, socketInodes: ReadonlySet<string>): number[] {
109
- const ports: number[] = [];
110
-
111
- for (const line of content.split(/\r?\n/).filter(Boolean).slice(1)) {
112
- // Columns: sl local_address rem_address st tx_queue:rx_queue tr:tm->when retrnsmt uid timeout inode
113
- const cols = line.trim().split(/\s+/);
114
- const local = cols[1];
115
- const st = cols[3];
116
- const inode = cols[9];
117
- if (st !== "0A" || !local || !inode || !socketInodes.has(inode)) continue;
118
-
119
- const portHex = local.split(":")[1];
120
- if (!portHex) continue;
121
- const port = Number.parseInt(portHex, 16);
122
- if (Number.isFinite(port) && port > 0) ports.push(port);
123
- }
124
-
125
- return ports;
126
- }
127
-
128
- function discoverListeningPortFromProc(pid: number): number | undefined {
129
- const fdDir = `/proc/${pid}/fd`;
130
- if (!existsSync(fdDir)) return undefined;
131
-
132
- try {
133
- const socketInodes = new Set<string>();
134
- for (const fd of readdirSync(fdDir)) {
135
- try {
136
- const target = readlinkSync(`${fdDir}/${fd}`);
137
- const match = target.match(/^socket:\[(\d+)\]$/);
138
- if (match?.[1]) socketInodes.add(match[1]);
139
- } catch {
140
- // Ignore fds that disappear while scanning.
141
- }
142
- }
143
- if (socketInodes.size === 0) return undefined;
144
-
145
- for (const procPath of [`/proc/${pid}/net/tcp`, `/proc/${pid}/net/tcp6`]) {
146
- if (!existsSync(procPath)) continue;
147
- const ports = parseProcNetTcpListeningPorts(readFileSync(procPath, "utf8"), socketInodes);
148
- if (ports.length > 0) return ports[0];
149
- }
150
- } catch {
151
- // Ignore permission/read errors.
152
- }
153
-
154
- return undefined;
155
- }
156
-
157
- /**
158
- * Scan a process tree (list of descendants of a pane's root PID) looking for
159
- * an OpenCode server process. Returns the pid and derived HTTP URL if found.
160
- *
161
- * NOTE: opencode runs as a child of the shell (panePid), so we scan
162
- * descendants — not ancestors — of the pane root.
163
- */
164
- export function findOpencodeInTree(
165
- processTree: ProcessInfo[],
166
- ): { pid: number; url: string } | undefined {
167
- for (const p of processTree) {
168
- const url = parseOpencodeUrl(p.command);
169
- if (url !== undefined) return { pid: p.pid, url };
170
- }
171
- return undefined;
172
- }
173
-
174
- // ── Auto-discovery ────────────────────────────────────────────────────────────
175
-
176
- export interface DiscoveredInstance {
177
- url: string;
178
- pid?: number;
179
- }
180
-
181
- // Cache pid → discovered port to avoid repeated lsof calls each discovery cycle.
182
- // Entries are kept until the process disappears (instance teardown clears them).
183
- const pidPortCache = new Map<number, number>();
184
-
185
- export function discoverInstances(logger: Logger, timeoutMs?: number): DiscoveredInstance[] {
186
- const instances: DiscoveredInstance[] = [];
187
-
188
- // Scan for opencode processes and try to extract port from args
189
- const result = IS_WINDOWS
190
- ? maybeRunCommand(["wmic", "process", "get", "ProcessId,ParentProcessId,CommandLine", "/format:csv"], timeoutMs)
191
- : maybeRunCommand(["ps", "-ax", "-o", "pid=,command="], timeoutMs);
192
- if (result.ok) {
193
- const processes = IS_WINDOWS
194
- ? parseWindowsProcesses(result.output)
195
- : result.output.split(/\r?\n/).filter(Boolean).map((line) => {
196
- const match = line.match(/^\s*(\d+)\s+(.*)$/);
197
- if (!match) return null;
198
- return { pid: Number.parseInt(match[1] ?? "", 10), command: match[2] ?? "" };
199
- }).filter((p): p is { pid: number; command: string } => Boolean(p));
200
- logger.debug("opencode", "Process scan for autodiscover", { totalLines: processes.length });
201
-
202
- for (const { pid, command } of processes) {
203
- if (!Number.isFinite(pid) || pid <= 0) continue;
204
-
205
- const url = parseOpencodeUrl(command);
206
- if (!url) continue;
207
-
208
- // If no explicit --port in the command, the opencode TUI binds to a random
209
- // port. Use lsof to find the actual listening port and override the default.
210
- let resolvedUrl = url;
211
- if (!hasExplicitPort(command)) {
212
- const cached = pidPortCache.get(pid);
213
- if (cached !== undefined) {
214
- resolvedUrl = `http://127.0.0.1:${cached}`;
215
- logger.debug("opencode", "Using cached port for opencode TUI", { pid, port: cached });
216
- } else {
217
- const discovered = discoverListeningPort(pid, timeoutMs);
218
- if (discovered !== undefined) {
219
- pidPortCache.set(pid, discovered);
220
- resolvedUrl = `http://127.0.0.1:${discovered}`;
221
- logger.debug("opencode", "Discovered port via lsof for opencode TUI", { pid, port: discovered });
222
- } else {
223
- logger.debug("opencode", "No listening port found via lsof for opencode TUI — skipping (not yet bound or hooks mode)", { pid });
224
- continue;
225
- }
226
- }
227
- }
228
-
229
- logger.debug("opencode", "Found opencode process", { pid, url: resolvedUrl, command: command.slice(0, 80) });
230
- instances.push({ url: resolvedUrl, pid });
231
- }
232
- } else {
233
- logger.warn("opencode", "Process scan failed", { error: result.error });
234
- }
235
-
236
- // Always probe default port if no instances found via process scan
237
- if (instances.length === 0) {
238
- const fallbackUrl = `http://localhost:4096`;
239
- logger.debug("opencode", "No opencode processes found via scan — probing default port", { url: fallbackUrl });
240
- instances.push({ url: fallbackUrl });
241
- }
242
-
243
- // De-duplicate by URL
244
- const seen = new Set<string>();
245
- const deduped = instances.filter((inst) => {
246
- if (seen.has(inst.url)) return false;
247
- seen.add(inst.url);
248
- return true;
249
- });
250
-
251
- logger.debug("opencode", "Autodiscover result", { count: deduped.length, urls: deduped.map((i) => i.url) });
252
- return deduped;
253
- }
254
-
255
- export function clearPidPortCache(pid: number): void {
256
- pidPortCache.delete(pid);
257
- }
@@ -1,5 +0,0 @@
1
- export { OpenCodeCollector } from "./collector";
2
- export { parseOpencodeUrl, hasExplicitPort, discoverListeningPort, findOpencodeInTree, discoverInstances, clearPidPortCache } from "./discovery";
3
- export { mapSessionStatus, buildSessionModel } from "./session-builder";
4
- export type { DiscoveredInstance } from "./discovery";
5
- export type { InstanceState } from "./session-builder";
@@ -1,59 +0,0 @@
1
- import type { SessionModel, SessionState } from "../../../shared/types";
2
- import type { Session, SessionStatus } from "@opencode-ai/sdk";
3
- import type { GitCollector } from "../git";
4
-
5
- export interface InstanceState {
6
- url: string;
7
- pid?: number;
8
- sessions: Map<string, SessionModel>;
9
- status: Map<string, SessionStatus>;
10
- /** sessionId → set of pending permissionRequest IDs */
11
- pendingPermissions: Map<string, Set<string>>;
12
- /** sessionId → set of pending question IDs */
13
- pendingQuestions: Map<string, Set<string>>;
14
- sseAbort?: AbortController;
15
- pollTimer?: ReturnType<typeof setInterval>;
16
- polling?: boolean;
17
- skippedPolls?: number;
18
- }
19
-
20
- export function mapSessionStatus(status: SessionStatus): SessionState {
21
- if (status.type === "busy") return "working";
22
- if (status.type === "idle") return "idle";
23
- if (status.type === "retry") return "working";
24
- return "idle";
25
- }
26
-
27
- export function buildSessionModel(
28
- session: Session,
29
- status: SessionStatus,
30
- instanceUrl: string,
31
- pendingPermissions: Map<string, Set<string>>,
32
- pendingQuestions: Map<string, Set<string>>,
33
- gitCollector?: GitCollector,
34
- ): SessionModel {
35
- const hasPendingPermission = (pendingPermissions.get(session.id)?.size ?? 0) > 0;
36
- const hasPendingQuestion = (pendingQuestions.get(session.id)?.size ?? 0) > 0;
37
- const hasAttention = hasPendingPermission || hasPendingQuestion;
38
- const state: SessionState = hasAttention ? "waiting-input" : mapSessionStatus(status);
39
- const currentPath = session.directory || undefined;
40
- const project = currentPath ? gitCollector?.resolve(currentPath) : undefined;
41
-
42
- return {
43
- id: `opencode:${session.id}`,
44
- source: "standalone",
45
- tool: "opencode",
46
- state,
47
- summary: session.title || "OpenCode session",
48
- lastActivity: session.time.updated,
49
- hasAttention,
50
- opencode: {
51
- sessionId: session.id,
52
- instanceUrl,
53
- },
54
- detectionSource: "opencode-sdk",
55
- currentPath,
56
- project,
57
- parentSessionId: session.parentID ? `opencode:${session.parentID}` : undefined,
58
- };
59
- }
@@ -1,294 +0,0 @@
1
- /**
2
- * OpenCode SQLite Collector
3
- *
4
- * Reads session metadata from the OpenCode SQLite database and enriches
5
- * existing sessions in the store. Runs every 30 seconds.
6
- *
7
- * Priority rules:
8
- * - Preserves state from native collectors; a matching herdr placeholder is claimed as idle.
9
- * - If a session already exists (from SSE/webhook), merges title/directory/lastActivity.
10
- * - If a session does NOT exist yet, adds it with state="idle" and detectionSource="opencode-db".
11
- */
12
-
13
- import { Database } from "bun:sqlite";
14
- import { homedir } from "node:os";
15
- import { join } from "node:path";
16
- import { existsSync } from "node:fs";
17
- import type { ScrylogConfig } from "../../shared/config";
18
- import type { SessionModel } from "../../shared/types";
19
- import type { DaemonStore } from "../store";
20
- import type { Collector } from "../scheduler";
21
- import type { Logger } from "../../shared/logger";
22
- import { NOOP_LOGGER } from "../../shared/logger";
23
- import type { GitCollector } from "./git";
24
-
25
- const POLL_INTERVAL_MS = 30_000;
26
- const CHILD_ACTIVITY_WINDOW_MS = 90_000;
27
-
28
- export function inferChildSessionState(
29
- session: Pick<SessionModel, "detectionSource" | "parentSessionId" | "lastActivity">,
30
- parent: Pick<SessionModel, "state"> | undefined,
31
- config: Pick<ScrylogConfig, "opencode">,
32
- now: number,
33
- ): SessionModel["state"] | undefined {
34
- if (config.opencode.mode !== "hooks") return undefined;
35
- if (session.detectionSource !== "opencode-db") return undefined;
36
- if (!session.parentSessionId) return undefined;
37
- if (parent?.state !== "working" && parent?.state !== "waiting-input") return "idle";
38
- if (now - session.lastActivity > CHILD_ACTIVITY_WINDOW_MS) return "idle";
39
- return "working";
40
- }
41
-
42
- export function claimHerdrSessionForOpenCodeDb(existing: SessionModel, sessionId: string): SessionModel {
43
- if (existing.detectionSource !== "herdr") return existing;
44
- return {
45
- ...existing,
46
- state: "idle",
47
- detectionSource: "opencode-db",
48
- opencode: existing.opencode ?? { sessionId, instanceUrl: "" },
49
- };
50
- }
51
-
52
- // ── DB path resolution ────────────────────────────────────────────────────────
53
-
54
- /**
55
- * Returns an ordered list of candidate DB paths for the current platform.
56
- * The first path that exists on disk should be used.
57
- *
58
- * Priority:
59
- * 1. OPENCODE_DATA_DIR env var (explicit override, always first)
60
- * 2. Platform-specific primary path
61
- * 3. Platform-specific fallback path(s)
62
- */
63
- export function resolveDbCandidates(
64
- platform: string = process.platform,
65
- env: Record<string, string | undefined> = process.env,
66
- ): string[] {
67
- const home = homedir();
68
-
69
- if (env["OPENCODE_DATA_DIR"]) {
70
- return [join(env["OPENCODE_DATA_DIR"], "opencode.db")];
71
- }
72
-
73
- if (platform === "win32") {
74
- const appData = env["APPDATA"] ?? join(home, "AppData", "Roaming");
75
- return [
76
- join(appData, "opencode", "opencode.db"),
77
- join(home, "AppData", "Roaming", "opencode", "opencode.db"),
78
- ];
79
- }
80
-
81
- if (platform === "darwin") {
82
- return [
83
- // XDG path — used by current OpenCode releases on macOS
84
- join(home, ".local", "share", "opencode", "opencode.db"),
85
- // Native macOS path — fallback for builds that follow Apple conventions
86
- join(home, "Library", "Application Support", "opencode", "opencode.db"),
87
- ];
88
- }
89
-
90
- // Linux and other Unix-like systems
91
- return [
92
- join(home, ".local", "share", "opencode", "opencode.db"),
93
- join(home, ".config", "opencode", "opencode.db"),
94
- ];
95
- }
96
-
97
- /**
98
- * Returns the first candidate path that exists on disk, or null if none found.
99
- */
100
- function findDbPath(): { path: string; candidates: string[] } | null {
101
- const candidates = resolveDbCandidates();
102
- for (const candidate of candidates) {
103
- if (existsSync(candidate)) return { path: candidate, candidates };
104
- }
105
- return null;
106
- }
107
-
108
- // ── DB row type ───────────────────────────────────────────────────────────────
109
-
110
- interface SessionRow {
111
- id: string;
112
- title: string | null;
113
- directory: string | null;
114
- time_created: number;
115
- time_updated: number;
116
- parent_id: string | null;
117
- }
118
-
119
- // ── Collector ─────────────────────────────────────────────────────────────────
120
-
121
- export class OpenCodeDbCollector implements Collector {
122
- name = "opencode-db";
123
- private store?: DaemonStore;
124
- private config?: ScrylogConfig;
125
- private logger: Logger = NOOP_LOGGER;
126
- private timer?: ReturnType<typeof setInterval>;
127
- private gitCollector?: GitCollector;
128
- private polling = false;
129
- private skippedPolls = 0;
130
- private errorCount = 0;
131
-
132
- constructor(gitCollector?: GitCollector) {
133
- this.gitCollector = gitCollector;
134
- }
135
-
136
- async start(store: DaemonStore, config: ScrylogConfig, logger: Logger): Promise<void> {
137
- this.store = store;
138
- this.config = config;
139
- this.logger = logger;
140
-
141
- const found = findDbPath();
142
- if (!found) {
143
- const candidates = resolveDbCandidates();
144
- logger.info("opencode-db", "Database not found — collector inactive", { event: "collector.unavailable", collector: this.name, candidates });
145
- store.setCollectorStatus(this.name, {
146
- running: false,
147
- lastError: `DB not found. Tried: ${candidates.join(", ")}`,
148
- });
149
- return;
150
- }
151
-
152
- const { path: dbPath } = found;
153
- logger.info("opencode-db", "Collector started", { event: "collector.started", collector: this.name, path: dbPath, intervalMs: POLL_INTERVAL_MS });
154
- store.setCollectorStatus(this.name, { running: true, lastRunAt: Date.now() });
155
-
156
- await this.safePoll(dbPath);
157
- this.timer = setInterval(() => void this.safePoll(dbPath), POLL_INTERVAL_MS);
158
- }
159
-
160
- async stop(): Promise<void> {
161
- if (this.timer) {
162
- clearInterval(this.timer);
163
- this.timer = undefined;
164
- }
165
- this.logger.info("opencode-db", "Collector stopped", { event: "collector.stopped", collector: this.name });
166
- }
167
-
168
- // ── Internal ───────────────────────────────────────────────────────────────
169
-
170
- private async safePoll(dbPath: string): Promise<void> {
171
- const store = this.store!;
172
- if (this.polling) {
173
- this.skippedPolls += 1;
174
- store.setCollectorStatus(this.name, { skippedPolls: this.skippedPolls });
175
- this.logger.debug("opencode-db", "Skipping overlapping poll", { event: "collector.poll_skipped", collector: this.name, skippedPolls: this.skippedPolls });
176
- return;
177
- }
178
-
179
- this.polling = true;
180
- const startedAt = Date.now();
181
- try {
182
- await this.poll(dbPath);
183
- store.setCollectorStatus(this.name, { running: true, lastRunAt: Date.now(), lastDurationMs: Date.now() - startedAt, skippedPolls: this.skippedPolls, errorCount: this.errorCount });
184
- } finally {
185
- this.polling = false;
186
- }
187
- }
188
-
189
- private async poll(dbPath: string): Promise<void> {
190
- const store = this.store!;
191
- const config = this.config!;
192
- const now = Date.now();
193
- let db: Database | undefined;
194
- try {
195
- // Open read-only (WAL-safe)
196
- db = new Database(dbPath, { readonly: true });
197
-
198
- const cutoff = config.retention.standaloneMaxAgeHours > 0
199
- ? now - config.retention.standaloneMaxAgeHours * 60 * 60 * 1000
200
- : 0;
201
- const limit = Math.max(1, config.retention.maxSessions || 500);
202
- const rows = db
203
- .query<SessionRow, [number, number]>(
204
- "SELECT id, title, directory, time_created, time_updated, parent_id FROM session WHERE time_archived IS NULL AND time_updated >= ? ORDER BY time_updated DESC LIMIT ?",
205
- )
206
- .all(cutoff, limit);
207
-
208
- this.logger.debug("opencode-db", "Poll result", { event: "collector.poll_completed", collector: this.name, rows: rows.length });
209
-
210
- for (const row of rows) {
211
- const storeId = `opencode:${row.id}`;
212
- const existing = store.getSession(storeId);
213
-
214
- if (existing) {
215
- const replacingHerdr = existing.detectionSource === "herdr";
216
- const owned = claimHerdrSessionForOpenCodeDb(existing, row.id);
217
- // Merge-only: preserve state, just update metadata
218
- const newParentSessionId = row.parent_id ? `opencode:${row.parent_id}` : undefined;
219
- const effectiveParentSessionId = newParentSessionId ?? existing.parentSessionId;
220
- const needsUpdate =
221
- existing.summary !== (row.title || "OpenCode session") ||
222
- existing.currentPath !== (row.directory ?? undefined) ||
223
- existing.lastActivity !== row.time_updated ||
224
- existing.parentSessionId !== newParentSessionId;
225
- const targetState = inferChildSessionState(
226
- owned,
227
- effectiveParentSessionId ? store.getSession(effectiveParentSessionId) : undefined,
228
- config,
229
- now,
230
- );
231
-
232
- if (replacingHerdr || needsUpdate || (targetState !== undefined && targetState !== existing.state)) {
233
- const project = row.directory
234
- ? (this.gitCollector?.resolve(row.directory) ?? existing.project)
235
- : existing.project;
236
- const merged: SessionModel = {
237
- ...owned,
238
- state: targetState ?? owned.state,
239
- summary: row.title || existing.summary,
240
- currentPath: row.directory ?? existing.currentPath,
241
- lastActivity: Math.max(existing.lastActivity, row.time_updated),
242
- project: project ?? existing.project,
243
- parentSessionId: effectiveParentSessionId,
244
- };
245
- store.upsertSession(merged);
246
- this.logger.debug("opencode-db", "Enriched existing session", { id: storeId });
247
- }
248
- } else {
249
- // Session not yet known — add with minimal state
250
- const currentPath = row.directory ?? undefined;
251
- const project = currentPath ? this.gitCollector?.resolve(currentPath) : undefined;
252
- const parentSessionId = row.parent_id ? `opencode:${row.parent_id}` : undefined;
253
- const inferredChildState = inferChildSessionState(
254
- { detectionSource: "opencode-db", parentSessionId, lastActivity: row.time_updated },
255
- parentSessionId ? store.getSession(parentSessionId) : undefined,
256
- config,
257
- now,
258
- );
259
- const newSession: SessionModel = {
260
- id: storeId,
261
- source: "standalone",
262
- tool: "opencode",
263
- state: inferredChildState ?? "idle",
264
- summary: row.title || "OpenCode session",
265
- lastActivity: row.time_updated,
266
- detectionSource: "opencode-db",
267
- currentPath,
268
- project,
269
- opencode: {
270
- sessionId: row.id,
271
- instanceUrl: "",
272
- },
273
- parentSessionId,
274
- };
275
- store.upsertSession(newSession);
276
- this.logger.debug("opencode-db", "Added session from DB", { id: storeId, title: row.title });
277
- }
278
- }
279
-
280
- // All status updates consolidated in safePoll()
281
- } catch (error) {
282
- const msg = error instanceof Error ? error.message : String(error);
283
- this.errorCount += 1;
284
- this.logger.warn("opencode-db", "Poll error", { event: "collector.poll_failed", collector: this.name, errorMessage: msg });
285
- store.setCollectorStatus(this.name, { lastError: msg, errorCount: this.errorCount });
286
- } finally {
287
- try {
288
- db?.close();
289
- } catch {
290
- // Ignore close errors
291
- }
292
- }
293
- }
294
- }