@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,242 +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
- windowName: pane.windowName,
222
- pane: pane.paneId,
223
- },
224
- summary: codexPrompt?.summary ?? claudePrompt?.summary ?? pane.summary,
225
- lastActivity: pane.lastChangedAt,
226
- detectionSource: "fallback-detection",
227
- legacyPaneId: pane.paneId,
228
- // Extended fields
229
- processTree: pane.processTree,
230
- processChain: pane.processChain,
231
- sample: pane.sample,
232
- toolReasons: pane.toolReasons,
233
- stateReasons: pane.stateReasons,
234
- stableForMs: pane.stableForMs,
235
- hasAttention: codexPrompt?.hasAttention ?? claudePrompt?.hasAttention ?? pane.hasAttention,
236
- currentCommand: pane.currentCommand,
237
- currentPath: pane.currentPath,
238
- panePid: pane.panePid,
239
- tty: pane.tty,
240
- paneTitle: pane.title,
241
- };
242
- }
@@ -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,259 +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 { keychainCoordinator } from "../../usage/resolvers/keychain-coordinator";
14
- import type { DaemonStore } from "../store";
15
- import type { Collector } from "../scheduler";
16
-
17
- interface UsageResolverSet {
18
- claudeProvider: ClaudeUsageProvider;
19
- codexProvider: CodexUsageProvider;
20
- opencodeGoProvider: OpenCodeGoUsageProvider;
21
- }
22
-
23
- export class UsageCollector implements Collector {
24
- name = "usage";
25
- private timer?: ReturnType<typeof setInterval>;
26
- private store?: DaemonStore;
27
- private config?: ScrylogConfig;
28
- private logger?: Logger;
29
- private polling = false;
30
- private skippedPolls = 0;
31
- private errorCount = 0;
32
- private pollController?: AbortController;
33
- private resolverSet?: UsageResolverSet;
34
-
35
- async start(store: DaemonStore, config: ScrylogConfig, logger: Logger): Promise<void> {
36
- this.store = store;
37
- this.config = config;
38
- this.logger = logger;
39
- keychainCoordinator.configure({
40
- allowPrompts: () => config.usage.auth.allowKeychainPrompts,
41
- onFailure: (failure) =>
42
- logger.warn("usage", "Keychain read failed", {
43
- event: "keychain_read_failed",
44
- code: failure.code,
45
- service: failure.service,
46
- account: failure.account ?? null,
47
- cooldownUntil: failure.cooldownUntil ? new Date(failure.cooldownUntil).toISOString() : null,
48
- }),
49
- });
50
-
51
- logger.info("usage", "Collector started", { event: "collector.started", collector: this.name, intervalMs: config.usage.pollIntervalMs, providers: config.usage.providers });
52
-
53
- await this.safePoll();
54
-
55
- this.timer = setInterval(() => void this.safePoll(), config.usage.pollIntervalMs);
56
- }
57
-
58
- async stop(): Promise<void> {
59
- if (this.timer) {
60
- clearInterval(this.timer);
61
- this.timer = undefined;
62
- }
63
- this.pollController?.abort();
64
- // Drop resolver/provider instances so a later start() rebuilds them from
65
- // the current configuration instead of reusing stale constructor args.
66
- this.resolverSet = undefined;
67
- this.logger?.info("usage", "Collector stopped", { event: "collector.stopped", collector: this.name });
68
- }
69
-
70
- private async safePoll(): Promise<void> {
71
- const store = this.store!;
72
- if (this.polling) {
73
- this.skippedPolls += 1;
74
- store.setCollectorStatus(this.name, { skippedPolls: this.skippedPolls });
75
- this.logger?.debug("usage", "Skipping overlapping poll", { event: "collector.poll_skipped", collector: this.name, skippedPolls: this.skippedPolls });
76
- return;
77
- }
78
-
79
- this.polling = true;
80
- const pollController = new AbortController();
81
- this.pollController = pollController;
82
- const startedAt = Date.now();
83
- try {
84
- await this.poll(pollController.signal);
85
- store.setCollectorStatus(this.name, { lastDurationMs: Date.now() - startedAt, skippedPolls: this.skippedPolls, errorCount: this.errorCount });
86
- } catch (error) {
87
- if (!pollController.signal.aborted) {
88
- this.errorCount += 1;
89
- this.logger?.warn("usage", "Poll failed", {
90
- event: "collector.poll_failed",
91
- collector: this.name,
92
- errorMessage: error instanceof Error ? error.message : String(error),
93
- });
94
- }
95
- } finally {
96
- if (this.pollController === pollController) this.pollController = undefined;
97
- this.polling = false;
98
- }
99
- }
100
-
101
- private async poll(signal: AbortSignal): Promise<void> {
102
- const store = this.store!;
103
- const config = this.config!;
104
- const logger = this.logger!;
105
-
106
- const resolvers = this.ensureResolvers(config);
107
- const providers: UsageProviderEntry[] = [];
108
- const attemptedAt = Date.now();
109
-
110
- for (const providerName of config.usage.providers) {
111
- if (providerName === "claude") {
112
- try {
113
- const result = await withAbort((requestSignal) => resolvers.claudeProvider.fetch({ signal: requestSignal }), config.daemon.networkTimeoutMs, "Claude usage fetch timed out", signal);
114
- providers.push(result.entry);
115
- } catch (err) {
116
- if (signal.aborted) throw err;
117
- this.errorCount += 1;
118
- const message = err instanceof Error ? err.message : String(err);
119
- logger.warn("usage", "Claude provider unexpected error", { event: "provider.fetch_failed", provider: "claude", errorMessage: message });
120
- providers.push({
121
- provider: "claude",
122
- accountId: null,
123
- available: true,
124
- status: "error",
125
- errors: [message],
126
- });
127
- }
128
- } else if (providerName === "codex") {
129
- try {
130
- const result = await withAbort((requestSignal) => resolvers.codexProvider.fetch({ signal: requestSignal }), config.daemon.networkTimeoutMs, "Codex usage fetch timed out", signal);
131
- providers.push(result.entry);
132
- } catch (err) {
133
- if (signal.aborted) throw err;
134
- this.errorCount += 1;
135
- const message = err instanceof Error ? err.message : String(err);
136
- logger.warn("usage", "Codex provider unexpected error", { event: "provider.fetch_failed", provider: "codex", errorMessage: message });
137
- providers.push({
138
- provider: "codex",
139
- accountId: null,
140
- available: true,
141
- status: "error",
142
- errors: [message],
143
- });
144
- }
145
- } else if (providerName === "opencode-go") {
146
- try {
147
- const result = await withAbort((requestSignal) => resolvers.opencodeGoProvider.fetch({ signal: requestSignal }), config.daemon.networkTimeoutMs, "OpenCode Go usage fetch timed out", signal);
148
- providers.push(result.entry);
149
- } catch (err) {
150
- if (signal.aborted) throw err;
151
- this.errorCount += 1;
152
- const message = err instanceof Error ? err.message : String(err);
153
- logger.warn("usage", "OpenCode Go provider unexpected error", { event: "provider.fetch_failed", provider: "opencode-go", errorMessage: message });
154
- providers.push({
155
- provider: "opencode-go",
156
- accountId: null,
157
- available: true,
158
- status: "error",
159
- errors: [message],
160
- });
161
- }
162
- }
163
- }
164
-
165
- const state: UsageState = {
166
- providers,
167
- lastRefreshedAt: attemptedAt,
168
- };
169
-
170
- store.setUsage(state);
171
- store.setCollectorStatus(this.name, { running: true, lastRunAt: attemptedAt, skippedPolls: this.skippedPolls, errorCount: this.errorCount });
172
- logger.debug("usage", "Poll complete", { event: "collector.poll_completed", collector: this.name, providerCount: providers.length });
173
- }
174
-
175
- /**
176
- * Resolvers and providers are created once per collector lifetime so
177
- * resolved credentials stay cached across polls instead of re-reading
178
- * credential sources on every cycle.
179
- */
180
- private ensureResolvers(config: ScrylogConfig): UsageResolverSet {
181
- if (!this.resolverSet) {
182
- const oauthResolver = new ClaudeAuthResolver();
183
- const webResolver = new ClaudeWebResolver();
184
- const cliResolver = new ClaudeCliResolver();
185
- const planner = new ClaudeSourcePlanner(
186
- {
187
- dataSource: config.usage.claude.dataSource,
188
- webExtrasEnabled: config.usage.claude.webExtrasEnabled,
189
- },
190
- oauthResolver,
191
- webResolver,
192
- cliResolver,
193
- );
194
-
195
- this.resolverSet = {
196
- claudeProvider: new ClaudeUsageProvider(planner, oauthResolver, webResolver, cliResolver),
197
- codexProvider: new CodexUsageProvider(
198
- new CodexAuthResolver(config.usage.codex.chatgptBaseUrl),
199
- config.usage.codex.dataSource,
200
- ),
201
- opencodeGoProvider: new OpenCodeGoUsageProvider(
202
- new OpenCodeGoWebResolver(),
203
- config.usage.opencodeGo.serverFnId,
204
- ),
205
- };
206
- }
207
- return this.resolverSet;
208
- }
209
- }
210
-
211
- export function withAbort<T>(
212
- run: (signal: AbortSignal) => Promise<T>,
213
- timeoutMs: number,
214
- message: string,
215
- parentSignal?: AbortSignal,
216
- ): Promise<T> {
217
- const controller = new AbortController();
218
- if (parentSignal?.aborted) {
219
- controller.abort();
220
- return Promise.reject(new Error("Usage poll cancelled"));
221
- }
222
- return new Promise<T>((resolve, reject) => {
223
- let timer: ReturnType<typeof setTimeout> | undefined;
224
- const cleanup = () => {
225
- if (timer) clearTimeout(timer);
226
- parentSignal?.removeEventListener("abort", onParentAbort);
227
- };
228
- const onParentAbort = () => {
229
- controller.abort();
230
- reject(new Error("Usage poll cancelled"));
231
- };
232
- parentSignal?.addEventListener("abort", onParentAbort, { once: true });
233
- // Register the deadline before starting the request so the first
234
- // synchronous step is already bounded.
235
- timer = setTimeout(() => {
236
- controller.abort();
237
- reject(new Error(message));
238
- }, timeoutMs);
239
-
240
- let promise: Promise<T>;
241
- try {
242
- promise = run(controller.signal);
243
- } catch (error) {
244
- cleanup();
245
- reject(error instanceof Error ? error : new Error(String(error)));
246
- return;
247
- }
248
- promise.then(
249
- (value) => {
250
- cleanup();
251
- resolve(value);
252
- },
253
- (error) => {
254
- cleanup();
255
- reject(error);
256
- },
257
- );
258
- });
259
- }