@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,241 +0,0 @@
1
- import type { SessionModel, TmuxPaneRecord, PaneSample } from "../../../shared/types";
2
- import type { OpenCodeConfig } from "../../../shared/config";
3
- import type { DaemonStore } from "../../store";
4
- import { classifyPane } from "../../../legacy/classifier";
5
- import { parseOpencodeUrl } from "../opencode/discovery";
6
-
7
- export function isOpenCodeManagedSession(session: SessionModel): boolean {
8
- return session.detectionSource === "opencode-sdk"
9
- || session.detectionSource === "opencode-hook"
10
- || session.detectionSource === "opencode-db";
11
- }
12
-
13
- export function isClaudeManagedSession(session: SessionModel): boolean {
14
- return session.tool === "claude" && (
15
- session.detectionSource === "claude-transcript" ||
16
- session.detectionSource === "claude-hook"
17
- );
18
- }
19
-
20
- export function isCodexManagedSession(session: SessionModel): boolean {
21
- return session.tool === "codex" && (
22
- session.detectionSource === "codex-transcript" ||
23
- session.detectionSource === "codex-hook"
24
- );
25
- }
26
-
27
- export function clearStaleClaudeTmuxBindings(store: DaemonStore, livePaneIds: ReadonlySet<string>): void {
28
- const current = store.getSessions();
29
- for (const session of current) {
30
- const paneId = session.legacyPaneId ?? session.tmux?.pane;
31
- if (!isClaudeManagedSession(session) || !paneId) continue;
32
- if (!livePaneIds.has(paneId)) {
33
- store.clearTmuxBindingForPane(paneId);
34
- }
35
- }
36
- }
37
-
38
- export function clearStaleCodexTmuxBindings(store: DaemonStore, livePaneIds: ReadonlySet<string>): void {
39
- const current = store.getSessions();
40
- for (const session of current) {
41
- const paneId = session.legacyPaneId ?? session.tmux?.pane;
42
- if (!isCodexManagedSession(session) || !paneId) continue;
43
- if (!livePaneIds.has(paneId)) {
44
- store.clearTmuxBindingForPane(paneId);
45
- }
46
- }
47
- }
48
-
49
- /**
50
- * Compute stableForMs for a managed session (Claude/OpenCode) being enriched
51
- * with tmux context. The pane classifier's stableForMs tracks how long the
52
- * *pane* has been in the same heuristic state, which is unrelated to when the
53
- * managed session's state last changed (that's driven by hooks/SDK).
54
- * Using the pane value would make the timer jump to minutes the moment tmux
55
- * polls, even if the session state just changed moments ago.
56
- */
57
- export function managedStableForMs(session: SessionModel): number {
58
- const since = session.stateChangedAt ?? session.lastActivity;
59
- if (!since) return 0;
60
- return Math.max(0, Date.now() - since);
61
- }
62
-
63
- export function detectClaudePermissionPrompt(sample: PaneSample | null): { state: SessionModel["state"]; hasAttention: boolean; summary: string } | null {
64
- if (!sample) return null;
65
- const text = sample.lines.join("\n");
66
- if (!/Do you want to proceed\?/i.test(text)) return null;
67
- return {
68
- state: "waiting-input",
69
- hasAttention: true,
70
- summary: "Do you want to proceed?",
71
- };
72
- }
73
-
74
- export function isClaudePaneCandidate(
75
- processTree: Array<{ command: string; comm: string }>,
76
- currentCommand: string,
77
- ): boolean {
78
- const combined = `${currentCommand} ${processTree.map((p) => `${p.comm} ${p.command}`).join(" ")}`;
79
- // Match 'claude' binary but not 'claude-code' or other unrelated uses
80
- return /(?:^|\s|\/)(claude)(?:\s|$)/i.test(combined);
81
- }
82
-
83
- export function isCodexPaneCandidate(
84
- processTree: Array<{ command: string; comm: string }>,
85
- currentCommand: string,
86
- ): boolean {
87
- const combined = `${currentCommand} ${processTree.map((p) => `${p.comm} ${p.command}`).join(" ")}`;
88
- return /(?:^|\s|\/)(codex)(?:\s|$)/i.test(combined);
89
- }
90
-
91
- export function detectCodexPermissionPrompt(sample: PaneSample | null): { state: SessionModel["state"]; hasAttention: boolean; summary: string } | null {
92
- if (!sample) return null;
93
- const text = sample.lines.join("\n");
94
-
95
- // Codex permission prompts for tool execution approval
96
- const permMatch = text.match(/approve(?:d)?\s+(?:codex\s+)?to\s+run\s+(\S+)/i)
97
- ?? text.match(/Allow\s+(?:this\s+)?(?:command|action|tool)\??/i)
98
- ?? text.match(/\[Y\]es\/\[N\]o/i);
99
- if (permMatch) {
100
- return {
101
- state: "waiting-input",
102
- hasAttention: true,
103
- summary: "Permission request",
104
- };
105
- }
106
-
107
- // Codex agent asking a question (bullet-point lines ending in ?)
108
- const questionLines = sample.lines.filter((line) => /^\s*•\s.*\?\s*$/.test(line));
109
- if (questionLines.length > 0) {
110
- const last = questionLines[questionLines.length - 1]!.replace(/^\s*•\s*/, "").trim();
111
- return {
112
- state: "waiting-input",
113
- hasAttention: true,
114
- summary: last.slice(0, 120) || "Waiting for input",
115
- };
116
- }
117
-
118
- const nonEmpty = sample.lines.filter((l) => l.trim().length > 0);
119
- const lastNonEmpty = nonEmpty[nonEmpty.length - 1];
120
-
121
- // Prompt cursor, optionally with a prefilled confirmation question.
122
- const promptMatch = lastNonEmpty?.match(/^\s*›\s*(.*)$/);
123
- if (promptMatch) {
124
- const promptText = (promptMatch[1] ?? "").trim();
125
- if (promptText.length > 0 && /\?\s*$/.test(promptText)) {
126
- return {
127
- state: "waiting-input",
128
- hasAttention: true,
129
- summary: promptText.slice(0, 120),
130
- };
131
- }
132
- }
133
-
134
- return null;
135
- }
136
-
137
- export function clearStaleOpenCodeTmuxBindings(store: DaemonStore, livePaneIds: ReadonlySet<string>): void {
138
- const current = store.getSessions();
139
- for (const session of current) {
140
- const paneId = session.legacyPaneId ?? session.tmux?.pane;
141
- if (!isOpenCodeManagedSession(session) || !paneId) {
142
- continue;
143
- }
144
-
145
- if (!livePaneIds.has(paneId)) {
146
- store.clearTmuxBindingForPane(paneId);
147
- }
148
- }
149
- }
150
-
151
- export function isOpenCodePaneCandidate(
152
- pane: TmuxPaneRecord,
153
- processTree: Array<{ command: string }>,
154
- classifiedTool?: string,
155
- ): boolean {
156
- return Boolean(
157
- pane.opencodeSession ||
158
- classifiedTool === "opencode" ||
159
- processTree.some((process) => parseOpencodeUrl(process.command) !== undefined) ||
160
- isAttachClientPane(processTree),
161
- );
162
- }
163
-
164
- export function isAttachClientPane(processTree: Array<{ command: string }>): boolean {
165
- return processTree.some(
166
- (process) =>
167
- /(?:^|\s)(?:opencode|node\s+.*\/opencode|bun\s+.*\/opencode)\s+attach\b/i.test(process.command) ||
168
- /\bbun\s+run\s+src\/oc\/index\.ts\b/i.test(process.command) ||
169
- /(?:^|\s)(?:oc|[^\s]*\/oc)(?:\s|$)/i.test(process.command),
170
- );
171
- }
172
-
173
- export function shouldHideOpenCodePane(
174
- opencode: OpenCodeConfig,
175
- pane: TmuxPaneRecord,
176
- processTree: Array<{ command: string }>,
177
- classifiedTool?: string,
178
- ): boolean {
179
- if (opencode.mode !== "sse") {
180
- return false;
181
- }
182
-
183
- return isOpenCodePaneCandidate(pane, processTree, classifiedTool);
184
- }
185
-
186
- export function looksLikeAgent(processTree: Array<{ command: string; comm: string }>, currentCommand: string): boolean {
187
- const combined = `${currentCommand} ${processTree.map((p) => `${p.comm} ${p.command}`).join(" ")}`;
188
- return /(opencode|codex|copilotcli|copilot|claude)/i.test(combined);
189
- }
190
-
191
- /**
192
- * Converts a classified pane record into a SessionModel.
193
- * Panes that are already covered by SDK collectors (opencode/claude) will
194
- * be marked with legacyPaneId so those collectors can take precedence.
195
- */
196
- export function paneModelToSession(pane: ReturnType<typeof classifyPane>["pane"]): SessionModel {
197
- const tool = pane.tool === "opencode" ? "opencode"
198
- : pane.tool === "claude" ? "claude"
199
- : pane.tool === "codex" ? "codex"
200
- : pane.tool === "copilotcli" ? "copilotcli"
201
- : pane.tool === "shell" ? "shell"
202
- : "unknown";
203
-
204
- const state = pane.state === "working" ? "working"
205
- : pane.state === "idle" ? "idle"
206
- : pane.state === "waiting-input" ? "waiting-input"
207
- : pane.state === "shell" ? "shell"
208
- : "unknown";
209
- const claudePrompt = tool === "claude" ? detectClaudePermissionPrompt(pane.sample) : null;
210
- const codexPrompt = tool === "codex" ? detectCodexPermissionPrompt(pane.sample) : null;
211
-
212
- return {
213
- id: `tmux:${pane.paneId}`,
214
- source: "tmux",
215
- tool,
216
- state: codexPrompt?.state ?? claudePrompt?.state ?? state,
217
- project: pane.project,
218
- tmux: {
219
- session: pane.sessionName,
220
- window: String(pane.windowIndex),
221
- pane: pane.paneId,
222
- },
223
- summary: codexPrompt?.summary ?? claudePrompt?.summary ?? pane.summary,
224
- lastActivity: pane.lastChangedAt,
225
- detectionSource: "fallback-detection",
226
- legacyPaneId: pane.paneId,
227
- // Extended fields
228
- processTree: pane.processTree,
229
- processChain: pane.processChain,
230
- sample: pane.sample,
231
- toolReasons: pane.toolReasons,
232
- stateReasons: pane.stateReasons,
233
- stableForMs: pane.stableForMs,
234
- hasAttention: codexPrompt?.hasAttention ?? claudePrompt?.hasAttention ?? pane.hasAttention,
235
- currentCommand: pane.currentCommand,
236
- currentPath: pane.currentPath,
237
- panePid: pane.panePid,
238
- tty: pane.tty,
239
- paneTitle: pane.title,
240
- };
241
- }
@@ -1,4 +0,0 @@
1
- export { TmuxCollector } from "./collector";
2
- export { clearStaleClaudeTmuxBindings, clearStaleCodexTmuxBindings, clearStaleOpenCodeTmuxBindings, isAttachClientPane, shouldHideOpenCodePane, looksLikeAgent } from "./enrichment";
3
- export { collectTmuxState, parseSessions, parsePanes, capturePane } from "./parse";
4
- export type { TmuxCollectionResult } from "./parse";
@@ -1,128 +0,0 @@
1
- import type { PaneSample, SessionInfo, TmuxPaneRecord } from "../../../shared/types";
2
- import { compactWhitespace, maybeRunCommand, parseIntSafe, pickMeaningfulTail, stripAnsiAndUi } from "../../../shared/utils";
3
-
4
- export const PANE_FORMAT = [
5
- "#{session_name}",
6
- "#{window_index}",
7
- "#{window_name}",
8
- "#{pane_index}",
9
- "#{pane_id}",
10
- "#{pane_pid}",
11
- "#{pane_current_command}",
12
- "#{pane_current_path}",
13
- "#{pane_active}",
14
- "#{pane_tty}",
15
- "#{pane_title}",
16
- "#{pane_width}",
17
- "#{pane_height}",
18
- "#{@opencode-session}",
19
- "#{@claude-session}",
20
- "#{@codex-session}",
21
- ].join("\t");
22
-
23
- export const SESSION_FORMAT = ["#{session_name}", "#{session_attached}", "#{session_windows}"].join("\t");
24
-
25
- export interface TmuxCollectionResult {
26
- sessions: SessionInfo[];
27
- panes: TmuxPaneRecord[];
28
- errors: string[];
29
- }
30
-
31
- export function collectTmuxState(timeoutMs?: number): TmuxCollectionResult {
32
- const sessionResult = maybeRunCommand(["tmux", "list-sessions", "-F", SESSION_FORMAT], timeoutMs);
33
- if (!sessionResult.ok) {
34
- if (
35
- sessionResult.error.includes("failed to connect to server") ||
36
- sessionResult.error.includes("no server running")
37
- ) {
38
- return { sessions: [], panes: [], errors: [] };
39
- }
40
- return { sessions: [], panes: [], errors: [sessionResult.error] };
41
- }
42
-
43
- const paneResult = maybeRunCommand(["tmux", "list-panes", "-a", "-F", PANE_FORMAT], timeoutMs);
44
- const sessions = parseSessions(sessionResult.output);
45
-
46
- if (!paneResult.ok) {
47
- return { sessions, panes: [], errors: [paneResult.error] };
48
- }
49
-
50
- return { sessions, panes: parsePanes(paneResult.output), errors: [] };
51
- }
52
-
53
- export function capturePane(paneId: string, lines: number, timeoutMs?: number): PaneSample | null {
54
- const result = maybeRunCommand(["tmux", "capture-pane", "-p", "-t", paneId, "-S", `-${lines}`], timeoutMs);
55
- if (!result.ok) {
56
- return null;
57
- }
58
-
59
- const allLines = result.output.split(/\r?\n/).map((line: string) => line.replace(/\t/g, " "));
60
- const cleanLines = allLines.filter(
61
- (line: string, index: number, arr: string[]) => !(index === arr.length - 1 && line === ""),
62
- );
63
-
64
- return {
65
- paneId,
66
- lines: cleanLines,
67
- preview: pickMeaningfulTail(cleanLines, compactWhitespace(stripAnsiAndUi(cleanLines.slice(-3).join(" ")))),
68
- };
69
- }
70
-
71
- export function parseSessions(output: string): SessionInfo[] {
72
- return output
73
- .split(/\r?\n/)
74
- .filter(Boolean)
75
- .map((line) => {
76
- const [name, attached, windows] = line.split("\t");
77
- return {
78
- name: name ?? "",
79
- attached: attached === "1",
80
- windows: parseIntSafe(windows ?? ""),
81
- };
82
- });
83
- }
84
-
85
- export function parsePanes(output: string): TmuxPaneRecord[] {
86
- return output
87
- .split(/\r?\n/)
88
- .filter(Boolean)
89
- .map((line) => {
90
- const [
91
- sessionName,
92
- windowIndex,
93
- windowName,
94
- paneIndex,
95
- paneId,
96
- panePid,
97
- currentCommand,
98
- currentPath,
99
- active,
100
- tty,
101
- title,
102
- width,
103
- height,
104
- opencodeSession,
105
- claudeSession,
106
- codexSession,
107
- ] = line.split("\t");
108
-
109
- return {
110
- sessionName: sessionName ?? "",
111
- windowIndex: parseIntSafe(windowIndex ?? ""),
112
- windowName: windowName ?? "",
113
- paneIndex: parseIntSafe(paneIndex ?? ""),
114
- paneId: paneId ?? "",
115
- panePid: parseIntSafe(panePid ?? ""),
116
- currentCommand: currentCommand ?? "",
117
- currentPath: currentPath ?? "",
118
- active: active === "1",
119
- tty: tty ?? "",
120
- title: title ?? "",
121
- width: parseIntSafe(width ?? ""),
122
- height: parseIntSafe(height ?? ""),
123
- opencodeSession: opencodeSession || undefined,
124
- claudeSession: claudeSession || undefined,
125
- codexSession: codexSession || undefined,
126
- } satisfies TmuxPaneRecord;
127
- });
128
- }
@@ -1,193 +0,0 @@
1
- import type { ScrylogConfig } from "../../shared/config";
2
- import type { Logger } from "../../shared/logger";
3
- import type { UsageState, UsageProviderEntry } from "../../usage/types";
4
- import { ClaudeSourcePlanner } from "../../usage/planner";
5
- import { ClaudeUsageProvider } from "../../usage/providers/claude";
6
- import { CodexUsageProvider } from "../../usage/providers/codex";
7
- import { OpenCodeGoUsageProvider } from "../../usage/providers/opencode-go";
8
- import { ClaudeAuthResolver } from "../../usage/resolvers/claude-auth";
9
- import { ClaudeWebResolver } from "../../usage/resolvers/claude-web";
10
- import { ClaudeCliResolver } from "../../usage/resolvers/claude-cli";
11
- import { CodexAuthResolver } from "../../usage/resolvers/codex-auth";
12
- import { OpenCodeGoWebResolver } from "../../usage/resolvers/opencode-go-web";
13
- import type { DaemonStore } from "../store";
14
- import type { Collector } from "../scheduler";
15
-
16
- export class UsageCollector implements Collector {
17
- name = "usage";
18
- private timer?: ReturnType<typeof setInterval>;
19
- private store?: DaemonStore;
20
- private config?: ScrylogConfig;
21
- private logger?: Logger;
22
- private polling = false;
23
- private skippedPolls = 0;
24
- private errorCount = 0;
25
-
26
- async start(store: DaemonStore, config: ScrylogConfig, logger: Logger): Promise<void> {
27
- this.store = store;
28
- this.config = config;
29
- this.logger = logger;
30
-
31
- logger.info("usage", "Collector started", { event: "collector.started", collector: this.name, intervalMs: config.usage.pollIntervalMs, providers: config.usage.providers });
32
-
33
- await this.safePoll();
34
-
35
- this.timer = setInterval(() => void this.safePoll(), config.usage.pollIntervalMs);
36
- }
37
-
38
- async stop(): Promise<void> {
39
- if (this.timer) {
40
- clearInterval(this.timer);
41
- this.timer = undefined;
42
- }
43
- this.logger?.info("usage", "Collector stopped", { event: "collector.stopped", collector: this.name });
44
- }
45
-
46
- private async safePoll(): Promise<void> {
47
- const store = this.store!;
48
- if (this.polling) {
49
- this.skippedPolls += 1;
50
- store.setCollectorStatus(this.name, { skippedPolls: this.skippedPolls });
51
- this.logger?.debug("usage", "Skipping overlapping poll", { event: "collector.poll_skipped", collector: this.name, skippedPolls: this.skippedPolls });
52
- return;
53
- }
54
-
55
- this.polling = true;
56
- const startedAt = Date.now();
57
- try {
58
- await this.poll();
59
- store.setCollectorStatus(this.name, { lastDurationMs: Date.now() - startedAt, skippedPolls: this.skippedPolls, errorCount: this.errorCount });
60
- } finally {
61
- this.polling = false;
62
- }
63
- }
64
-
65
- private async poll(): Promise<void> {
66
- const store = this.store!;
67
- const config = this.config!;
68
- const logger = this.logger!;
69
-
70
- const providers: UsageProviderEntry[] = [];
71
- const attemptedAt = Date.now();
72
-
73
- for (const providerName of config.usage.providers) {
74
- if (providerName === "claude") {
75
- const oauthResolver = new ClaudeAuthResolver();
76
- const webResolver = new ClaudeWebResolver();
77
- const cliResolver = new ClaudeCliResolver();
78
- const planner = new ClaudeSourcePlanner(
79
- {
80
- dataSource: config.usage.claude.dataSource,
81
- webExtrasEnabled: config.usage.claude.webExtrasEnabled,
82
- },
83
- oauthResolver,
84
- webResolver,
85
- cliResolver,
86
- );
87
-
88
- const decision = await planner.decide();
89
- if (decision.primary === "auto") {
90
- logger.debug("usage", "Claude not available on this machine, omitting from response", { event: "provider.unavailable", provider: "claude" });
91
- continue;
92
- }
93
-
94
- const claudeProvider = new ClaudeUsageProvider(planner, oauthResolver, webResolver, cliResolver);
95
- try {
96
- const result = await withAbort((signal) => claudeProvider.fetch({ signal }), config.daemon.networkTimeoutMs, "Claude usage fetch timed out");
97
- providers.push(result.entry);
98
- } catch (err) {
99
- this.errorCount += 1;
100
- const message = err instanceof Error ? err.message : String(err);
101
- logger.warn("usage", "Claude provider unexpected error", { event: "provider.fetch_failed", provider: "claude", errorMessage: message });
102
- providers.push({
103
- provider: "claude",
104
- accountId: null,
105
- available: true,
106
- status: "error",
107
- errors: [message],
108
- });
109
- }
110
- } else if (providerName === "codex") {
111
- const resolver = new CodexAuthResolver();
112
-
113
- if (!(await resolver.isAvailable())) {
114
- logger.debug("usage", "Codex not available on this machine, omitting from response", { event: "provider.unavailable", provider: "codex" });
115
- continue;
116
- }
117
-
118
- const provider = new CodexUsageProvider(resolver);
119
-
120
- try {
121
- const result = await withAbort((signal) => provider.fetch({ signal }), config.daemon.networkTimeoutMs, "Codex usage fetch timed out");
122
- providers.push(result.entry);
123
- } catch (err) {
124
- this.errorCount += 1;
125
- const message = err instanceof Error ? err.message : String(err);
126
- logger.warn("usage", "Codex provider unexpected error", { event: "provider.fetch_failed", provider: "codex", errorMessage: message });
127
- providers.push({
128
- provider: "codex",
129
- accountId: null,
130
- available: true,
131
- status: "error",
132
- errors: [message],
133
- });
134
- }
135
- } else if (providerName === "opencode-go") {
136
- const resolver = new OpenCodeGoWebResolver();
137
-
138
- if (!(await resolver.isAvailable())) {
139
- logger.debug("usage", "OpenCode Go not available on this machine, omitting from response", { event: "provider.unavailable", provider: "opencode-go" });
140
- continue;
141
- }
142
-
143
- const provider = new OpenCodeGoUsageProvider(resolver, config.usage.opencodeGo.serverFnId);
144
-
145
- try {
146
- const result = await withAbort((signal) => provider.fetch({ signal }), config.daemon.networkTimeoutMs, "OpenCode Go usage fetch timed out");
147
- providers.push(result.entry);
148
- } catch (err) {
149
- this.errorCount += 1;
150
- const message = err instanceof Error ? err.message : String(err);
151
- logger.warn("usage", "OpenCode Go provider unexpected error", { event: "provider.fetch_failed", provider: "opencode-go", errorMessage: message });
152
- providers.push({
153
- provider: "opencode-go",
154
- accountId: null,
155
- available: true,
156
- status: "error",
157
- errors: [message],
158
- });
159
- }
160
- }
161
- }
162
-
163
- const state: UsageState = {
164
- providers,
165
- lastRefreshedAt: attemptedAt,
166
- };
167
-
168
- store.setUsage(state);
169
- store.setCollectorStatus(this.name, { running: true, lastRunAt: attemptedAt, skippedPolls: this.skippedPolls, errorCount: this.errorCount });
170
- logger.debug("usage", "Poll complete", { event: "collector.poll_completed", collector: this.name, providerCount: providers.length });
171
- }
172
- }
173
-
174
- function withAbort<T>(run: (signal: AbortSignal) => Promise<T>, timeoutMs: number, message: string): Promise<T> {
175
- const controller = new AbortController();
176
- const promise = run(controller.signal);
177
- return new Promise((resolve, reject) => {
178
- const timer = setTimeout(() => {
179
- controller.abort();
180
- reject(new Error(message));
181
- }, timeoutMs);
182
- promise.then(
183
- (value) => {
184
- clearTimeout(timer);
185
- resolve(value);
186
- },
187
- (error) => {
188
- clearTimeout(timer);
189
- reject(error);
190
- },
191
- );
192
- });
193
- }