@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,299 +0,0 @@
1
- import type { SessionState } from "../../../shared/types";
2
- import type { CodexJsonlEntry } from "./jsonl";
3
- import { isResponseItem, getResponseItemText, getResponseItemRole, getFunctionCallName, isEventMsg } from "./jsonl";
4
-
5
- export interface CodexTranscriptMeta {
6
- sessionId: string;
7
- model?: string;
8
- forkedFromId?: string;
9
- parentSessionId?: string;
10
- agentNickname?: string;
11
- agentRole?: string;
12
- totalInputTokens?: number;
13
- totalOutputTokens?: number;
14
- totalCachedInputTokens?: number;
15
- }
16
-
17
- export interface CodexSpawnedSubagent {
18
- agentId: string;
19
- agentType?: string;
20
- nickname?: string;
21
- }
22
-
23
- export function extractMeta(entries: CodexJsonlEntry[]): CodexTranscriptMeta {
24
- const meta: CodexTranscriptMeta = { sessionId: "" };
25
- let lastTurnModel: string | undefined;
26
-
27
- for (const entry of entries) {
28
- switch (entry.type) {
29
- case "session_meta": {
30
- const payload = entry.payload ?? {};
31
- const sid = (payload.session_id as string | undefined)
32
- ?? (payload.sessionId as string | undefined)
33
- ?? (payload.id as string | undefined)
34
- ?? entry.session_id
35
- ?? entry.sessionId
36
- ?? entry.id;
37
- if (sid && typeof sid === "string") {
38
- meta.sessionId = sid;
39
- }
40
- const forkedFrom = (payload.forked_from_id as string | undefined)
41
- ?? (payload.forkedFromId as string | undefined)
42
- ?? (payload.parent_session_id as string | undefined)
43
- ?? (payload.parentSessionId as string | undefined);
44
- if (forkedFrom && typeof forkedFrom === "string") {
45
- meta.forkedFromId = forkedFrom;
46
- }
47
-
48
- const source = payload.source as Record<string, unknown> | undefined;
49
- const subagent = source?.subagent as Record<string, unknown> | undefined;
50
- const threadSpawn = subagent?.thread_spawn as Record<string, unknown> | undefined;
51
- const parentThreadId = threadSpawn?.parent_thread_id;
52
- if (typeof parentThreadId === "string" && parentThreadId.length > 0) {
53
- meta.parentSessionId = `codex:${parentThreadId}`;
54
- }
55
-
56
- const agentNickname = payload.agent_nickname ?? threadSpawn?.agent_nickname;
57
- if (typeof agentNickname === "string" && agentNickname.length > 0) {
58
- meta.agentNickname = agentNickname;
59
- }
60
- const agentRole = payload.agent_role ?? threadSpawn?.agent_role;
61
- if (typeof agentRole === "string" && agentRole.length > 0) {
62
- meta.agentRole = agentRole;
63
- }
64
- break;
65
- }
66
- case "turn_context": {
67
- const payload = entry.payload ?? {};
68
- const model = (payload.model as string | undefined)
69
- ?? ((payload.info as Record<string, unknown> | undefined)?.model as string | undefined);
70
- if (model && typeof model === "string") {
71
- lastTurnModel = model;
72
- meta.model = model;
73
- }
74
- break;
75
- }
76
- case "event_msg": {
77
- const payload = entry.payload ?? {};
78
- if (payload.type !== "token_count") break;
79
- const info = payload.info as Record<string, unknown> | undefined;
80
- if (!info) break;
81
-
82
- // Model resolution
83
- const model = (info.model as string | undefined)
84
- ?? (info.model_name as string | undefined)
85
- ?? (payload.model as string | undefined)
86
- ?? entry.model
87
- ?? lastTurnModel;
88
- if (model && typeof model === "string") {
89
- meta.model = model;
90
- }
91
-
92
- // Token usage
93
- const totalUsage = info.total_token_usage as Record<string, number> | undefined;
94
- if (totalUsage) {
95
- meta.totalInputTokens = totalUsage.input_tokens;
96
- meta.totalOutputTokens = totalUsage.output_tokens;
97
- const cached = totalUsage.cached_input_tokens ?? totalUsage.cache_read_input_tokens ?? 0;
98
- meta.totalCachedInputTokens = Math.min(cached, totalUsage.input_tokens ?? 0);
99
- }
100
- break;
101
- }
102
- }
103
- }
104
-
105
- return meta;
106
- }
107
-
108
- function normalizeText(text: string): string {
109
- return text.trim();
110
- }
111
-
112
- function isQuestionText(text: string): boolean {
113
- const trimmed = normalizeText(text);
114
- return /^(would you|do you|can you|should i|please confirm|shall i|want me to|implement this plan|implemente this plan|apply this plan|continue with this plan|proceed with this plan)\b/i.test(trimmed)
115
- || /^(quieres que|debo|puedo|confirmas|por favor confirma|contin[uú]o|procedo)\b/i.test(trimmed)
116
- || /^¿?(qu[eé]|cu[aá]l|cu[aá]les|c[oó]mo|d[oó]nde|cu[aá]ndo|por qu[eé])(?:\s|\?|$)/i.test(trimmed);
117
- }
118
-
119
- function parseJsonObject(value: unknown): Record<string, unknown> | undefined {
120
- if (typeof value !== "string") return undefined;
121
- try {
122
- const parsed = JSON.parse(value) as unknown;
123
- return typeof parsed === "object" && parsed !== null ? parsed as Record<string, unknown> : undefined;
124
- } catch {
125
- return undefined;
126
- }
127
- }
128
-
129
- function readCompletedSubagentIds(entries: CodexJsonlEntry[]): Set<string> {
130
- const completed = new Set<string>();
131
- for (const entry of entries) {
132
- if (!isResponseItem(entry)) continue;
133
- const text = getResponseItemText(entry);
134
- if (text) {
135
- const match = /<subagent_notification>\s*([\s\S]*?)\s*<\/subagent_notification>/i.exec(text);
136
- const payload = match ? parseJsonObject(match[1]) : undefined;
137
- const agentPath = payload?.agent_path;
138
- if (typeof agentPath === "string") completed.add(agentPath);
139
- }
140
-
141
- if (entry.payload.type !== "function_call_output") continue;
142
- const output = parseJsonObject(entry.payload.output);
143
- const status = output?.status;
144
- if (typeof status !== "object" || status === null) continue;
145
- for (const [agentId, value] of Object.entries(status as Record<string, unknown>)) {
146
- if (typeof value === "object" && value !== null && "completed" in value) {
147
- completed.add(agentId);
148
- }
149
- }
150
- }
151
- return completed;
152
- }
153
-
154
- export function extractActiveSpawnedSubagents(entries: CodexJsonlEntry[]): CodexSpawnedSubagent[] {
155
- const completed = readCompletedSubagentIds(entries);
156
- const callTypes = new Map<string, string | undefined>();
157
- const spawned = new Map<string, CodexSpawnedSubagent>();
158
-
159
- for (const entry of entries) {
160
- if (!isResponseItem(entry)) continue;
161
-
162
- if (entry.payload.type === "function_call" && entry.payload.name === "spawn_agent") {
163
- const callId = entry.payload.call_id;
164
- const args = parseJsonObject(entry.payload.arguments);
165
- const agentType = args?.agent_type;
166
- if (typeof callId === "string") {
167
- callTypes.set(callId, typeof agentType === "string" ? agentType : undefined);
168
- }
169
- continue;
170
- }
171
-
172
- if (entry.payload.type !== "function_call_output") continue;
173
- const callId = entry.payload.call_id;
174
- if (typeof callId !== "string" || !callTypes.has(callId)) continue;
175
- const output = parseJsonObject(entry.payload.output);
176
- const agentId = output?.agent_id;
177
- if (typeof agentId !== "string" || completed.has(agentId)) continue;
178
- const nickname = output?.nickname;
179
- const agentType = callTypes.get(callId);
180
- spawned.set(agentId, {
181
- agentId,
182
- ...(agentType ? { agentType } : {}),
183
- ...(typeof nickname === "string" ? { nickname } : {}),
184
- });
185
- }
186
-
187
- return [...spawned.values()];
188
- }
189
-
190
- export function isResponseItemAskingQuestion(entry: CodexJsonlEntry): boolean {
191
- if (!isResponseItem(entry)) return false;
192
- if (getResponseItemRole(entry) !== "assistant") return false;
193
- const text = getResponseItemText(entry);
194
- if (!text) return false;
195
- return isQuestionText(text);
196
- }
197
-
198
- export function isRequestUserInputCall(entry: CodexJsonlEntry): boolean {
199
- if (!isResponseItem(entry)) return false;
200
- return getFunctionCallName(entry) === "request_user_input";
201
- }
202
-
203
- function isAsyncProcessRunningOutput(entry: CodexJsonlEntry): boolean {
204
- if (!isResponseItem(entry)) return false;
205
- if (entry.payload.type !== "function_call_output") return false;
206
- const output = entry.payload.output;
207
- return typeof output === "string" && /Process running with session ID\s+\d+/i.test(output);
208
- }
209
-
210
- export function findLastMeaningfulResponseIndex(entries: CodexJsonlEntry[]): number {
211
- for (let i = entries.length - 1; i >= 0; i--) {
212
- const entry = entries[i]!;
213
- if (!isResponseItem(entry)) continue;
214
- // Skip turn_aborted placeholder messages — these are system-generated
215
- // and don't represent actual user responses.
216
- const role = getResponseItemRole(entry);
217
- const text = getResponseItemText(entry);
218
- if (role === "user" && text?.includes("<turn_aborted>")) continue;
219
- return i;
220
- }
221
- return -1;
222
- }
223
-
224
- export function extractState(entries: CodexJsonlEntry[]): SessionState {
225
- if (entries.length === 0) return "idle";
226
-
227
- const hasTaskStarted = entries.some((e) => isEventMsg(e) && e.payload.type === "task_started");
228
- const hasTaskComplete = entries.some((e) => isEventMsg(e) && e.payload.type === "task_complete");
229
-
230
- const lastIdx = findLastMeaningfulResponseIndex(entries);
231
- if (lastIdx < 0) {
232
- if (hasTaskStarted && !hasTaskComplete) return "working";
233
- return "idle";
234
- }
235
-
236
- const lastEntry = entries[lastIdx]! as CodexJsonlEntry & { payload: Record<string, unknown> };
237
-
238
- if (isRequestUserInputCall(lastEntry)) return "waiting-input";
239
-
240
- const role = getResponseItemRole(lastEntry as Parameters<typeof getResponseItemRole>[0]);
241
- const text = getResponseItemText(lastEntry as Parameters<typeof getResponseItemText>[0]);
242
- const payloadType = lastEntry.payload?.type;
243
-
244
- if (role === "assistant" && text && isQuestionText(text)) return "waiting-input";
245
-
246
- if (role === "assistant" && text) return "idle";
247
-
248
- if (payloadType === "function_call") return "working";
249
- if (isAsyncProcessRunningOutput(lastEntry)) return "working";
250
-
251
- if (role === "user") return "working";
252
-
253
- const afterEntries = entries.slice(lastIdx + 1);
254
- const hasTaskStartedAfter = afterEntries.some((e) => isEventMsg(e) && e.payload.type === "task_started");
255
- const hasTaskCompleteAfter = afterEntries.some((e) => isEventMsg(e) && e.payload.type === "task_complete");
256
- if (hasTaskStartedAfter && !hasTaskCompleteAfter) return "working";
257
- if (hasTaskStarted && !hasTaskComplete) return "working";
258
-
259
- return "idle";
260
- }
261
-
262
- export function extractSummary(entries: CodexJsonlEntry[]): string | undefined {
263
- for (let i = entries.length - 1; i >= 0; i--) {
264
- const entry = entries[i]!;
265
- if (!isResponseItem(entry)) continue;
266
- if (getResponseItemRole(entry) !== "assistant") continue;
267
-
268
- const funcName = getFunctionCallName(entry);
269
- if (funcName) {
270
- const args = entry.payload?.arguments;
271
- if (funcName === "request_user_input" && typeof args === "object" && args !== null) {
272
- const question = (args as Record<string, unknown>).question;
273
- if (typeof question === "string") {
274
- const first = question.split("\n")[0] ?? "";
275
- if (first.length > 0) return first.slice(0, 120);
276
- }
277
- }
278
- return `Using ${funcName}`;
279
- }
280
-
281
- const text = getResponseItemText(entry);
282
- if (text) {
283
- const first = normalizeText(text).split("\n")[0] ?? "";
284
- if (first.length > 0) return first.slice(0, 120);
285
- }
286
- }
287
-
288
- for (let i = entries.length - 1; i >= 0; i--) {
289
- const entry = entries[i]!;
290
- if (!isEventMsg(entry)) continue;
291
- if (entry.payload.type !== "agent_message") continue;
292
- const msg = entry.payload.message;
293
- if (typeof msg === "string" && msg.trim().length > 0) {
294
- return msg.trim().split("\n")[0]!.slice(0, 120);
295
- }
296
- }
297
-
298
- return undefined;
299
- }
@@ -1,353 +0,0 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { basename, dirname, join, resolve } from "node:path";
3
- import type { ProjectInfo } from "../../shared/types";
4
- import { maybeRunCommandInDirectory, safeBasename } from "../../shared/utils";
5
- import type { ScrylogConfig } from "../../shared/config";
6
- import type { DaemonStore } from "../store";
7
- import type { Collector } from "../scheduler";
8
- import { IS_WSL } from "../../shared/platform";
9
-
10
- import type { Logger } from "../../shared/logger";
11
- import { NOOP_LOGGER } from "../../shared/logger";
12
-
13
- export type { ProjectInfo };
14
-
15
- interface GitCommandContext {
16
- cwd: string;
17
- gitDir?: string;
18
- workTree?: string;
19
- }
20
-
21
- /**
22
- * Freshness tracking for cached ProjectInfo entries. The branch (and only the
23
- * branch) goes stale when the repo's HEAD file changes; everything else
24
- * (displayName, remote, worktree status) is stable per cwd. We fingerprint the
25
- * resolved HEAD file — works for regular repos, worktrees, and WSL-backed
26
- * Windows checkouts, and never spawns a git subprocess.
27
- */
28
- interface FreshnessMeta {
29
- headPath?: string;
30
- headMtimeMs?: number;
31
- }
32
-
33
- const freshnessByProject = new WeakMap<ProjectInfo, FreshnessMeta>();
34
-
35
- function freshenProject(project: ProjectInfo, gitMarkerDirectory: string | null): ProjectInfo {
36
- if (!gitMarkerDirectory) return project;
37
- const headPath = resolveHeadPath(gitMarkerDirectory);
38
- if (!headPath) return project;
39
- const mtime = readHeadMtime(headPath);
40
- if (mtime === undefined) return project;
41
- freshnessByProject.set(project, { headPath, headMtimeMs: mtime });
42
- return project;
43
- }
44
-
45
- function resolveHeadPath(gitMarkerDirectory: string): string | undefined {
46
- const context = resolveGitCommandContext(gitMarkerDirectory);
47
- const gitDir = context.gitDir
48
- ? resolve(context.cwd, context.gitDir)
49
- : join(gitMarkerDirectory, ".git");
50
- return join(gitDir, "HEAD");
51
- }
52
-
53
- function readHeadMtime(headPath: string | undefined): number | undefined {
54
- if (!headPath) return undefined;
55
- try {
56
- return statSync(headPath).mtimeMs;
57
- } catch {
58
- return undefined;
59
- }
60
- }
61
-
62
- function isFreshnessCurrent(project: ProjectInfo): boolean {
63
- if (!project.isGitRepository) return true;
64
- const meta = freshnessByProject.get(project);
65
- if (!meta?.headPath || meta.headMtimeMs === undefined) return false;
66
- return readHeadMtime(meta.headPath) === meta.headMtimeMs;
67
- }
68
-
69
- function invalidateCachedProject(cache: Map<string, ProjectInfo>, project: ProjectInfo): void {
70
- for (const [key, value] of [...cache]) {
71
- if (value === project) cache.delete(key);
72
- }
73
- freshnessByProject.delete(project);
74
- }
75
-
76
- export function resolveProjectInfo(currentPath: string, cache = new Map<string, ProjectInfo>(), timeoutMs?: number): ProjectInfo {
77
- const normalizedPath = normalizePathForLookup(currentPath);
78
- const cached = lookupCachedProject(normalizedPath, cache);
79
- if (cached && isFreshnessCurrent(cached)) {
80
- return cached;
81
- }
82
- if (cached) {
83
- invalidateCachedProject(cache, cached);
84
- }
85
-
86
- const visited: string[] = [];
87
- const searchStart = resolveSearchStart(normalizedPath);
88
- const gitMarkerDirectory = findGitMarkerDirectory(searchStart, visited, cache);
89
-
90
- let project = buildFallbackProjectInfo(normalizedPath);
91
- if (gitMarkerDirectory) {
92
- project = resolveGitProjectInfo(gitMarkerDirectory, normalizedPath, timeoutMs) ?? buildFallbackProjectInfo(normalizedPath, gitMarkerDirectory);
93
- }
94
-
95
- const cacheKeys = project.isGitRepository
96
- ? [...visited, normalizedPath, project.gitRootPath].filter((value): value is string => Boolean(value))
97
- : [normalizedPath];
98
- for (const cacheKey of cacheKeys) {
99
- cache.set(cacheKey, project);
100
- }
101
-
102
- return project.isGitRepository ? freshenProject(project, gitMarkerDirectory) : project;
103
- }
104
-
105
- export function buildCompactProjectLabel(project: Pick<ProjectInfo, "displayName" | "isGitRepository" | "branch" | "isWorktree">): string {
106
- if (!project.isGitRepository) {
107
- return project.displayName;
108
- }
109
-
110
- const branch = project.branch || "detached";
111
- const repoKind = project.isWorktree ? "wt" : "git";
112
- return `${project.displayName}@${branch} [${repoKind}]`;
113
- }
114
-
115
- export function parseRemoteProjectName(remoteUrl: string): string {
116
- const trimmed = remoteUrl.trim().replace(/[\\/]+$/, "");
117
- if (!trimmed) {
118
- return "";
119
- }
120
-
121
- const normalized = trimmed.replace(/[:](?=[^/]+\/)/, "/");
122
- const lastSegment = normalized.split("/").filter(Boolean).pop() ?? "";
123
- return lastSegment.replace(/\.git$/i, "");
124
- }
125
-
126
- export function isGitWorktree(gitDir: string, gitCommonDir: string, cwd: string): boolean {
127
- return resolve(cwd, gitDir) !== resolve(cwd, gitCommonDir);
128
- }
129
-
130
- /**
131
- * Thin collector wrapper around the existing git resolution logic.
132
- * Exposes a shared cache for other collectors.
133
- */
134
- export class GitCollector implements Collector {
135
- name = "git";
136
- readonly cache = new Map<string, ProjectInfo>();
137
- private logger: Logger = NOOP_LOGGER;
138
- private timeoutMs: number | undefined;
139
- private maxEntries = 1_000;
140
-
141
- start(_store: DaemonStore, config: ScrylogConfig, logger: Logger): void {
142
- this.logger = logger;
143
- this.timeoutMs = config.daemon.commandTimeoutMs;
144
- this.maxEntries = config.retention.cacheMaxEntries;
145
- logger.info("git", "Collector started", { event: "collector.started", collector: this.name, mode: "on-demand" });
146
- }
147
-
148
- stop(): void {
149
- // Nothing to stop
150
- this.logger.info("git", "Collector stopped", { event: "collector.stopped", collector: this.name });
151
- }
152
-
153
- resolve(path: string): ProjectInfo {
154
- const cached = this.cache.has(path);
155
- if (!cached) {
156
- this.logger.debug("git", "Cache miss — resolving git info", { event: "cache.miss", path });
157
- }
158
- const result = resolveProjectInfo(path, this.cache, this.timeoutMs);
159
- trimMap(this.cache, this.maxEntries);
160
- if (!cached && result.isGitRepository) {
161
- this.logger.debug("git", "Resolved git project", {
162
- event: "project.resolved",
163
- path,
164
- displayName: result.displayName,
165
- branch: result.branch,
166
- isWorktree: result.isWorktree,
167
- gitRootPath: result.gitRootPath,
168
- });
169
- }
170
- return result;
171
- }
172
- }
173
-
174
- function resolveGitProjectInfo(gitMarkerDirectory: string, currentPath: string, timeoutMs?: number): ProjectInfo | null {
175
- const commandContext = resolveGitCommandContext(gitMarkerDirectory);
176
- const topLevel = runGit(commandContext, ["rev-parse", "--show-toplevel"], timeoutMs);
177
- if (!topLevel.ok) {
178
- return null;
179
- }
180
-
181
- const gitDir = runGit(commandContext, ["rev-parse", "--git-dir"], timeoutMs);
182
- const gitCommonDir = runGit(commandContext, ["rev-parse", "--git-common-dir"], timeoutMs);
183
- const remoteNamesResult = runGit(commandContext, ["remote"], timeoutMs);
184
-
185
- const gitRootPath = normalizeWslWindowsPath(topLevel.output.trim());
186
- const gitDirPath = gitDir.ok ? normalizeWslWindowsPath(gitDir.output.trim()) : undefined;
187
- const gitCommonDirPath = gitCommonDir.ok ? normalizeWslWindowsPath(gitCommonDir.output.trim()) : undefined;
188
- const repositoryIdentityPath = gitCommonDir.ok
189
- ? resolveRepositoryIdentityPath(gitMarkerDirectory, gitCommonDirPath ?? "", gitRootPath)
190
- : gitRootPath;
191
- const branch = resolveBranchName(commandContext, timeoutMs);
192
- const remoteNames = remoteNamesResult.ok
193
- ? remoteNamesResult.output.split(/\r?\n/).map((name) => name.trim()).filter(Boolean)
194
- : [];
195
- const preferredRemote = remoteNames.includes("origin") ? "origin" : remoteNames[0];
196
- const remoteUrlResult = preferredRemote ? runGit(commandContext, ["remote", "get-url", preferredRemote], timeoutMs) : null;
197
- const remoteUrl = remoteUrlResult?.ok ? remoteUrlResult.output.trim() : undefined;
198
- const displayName = parseRemoteProjectName(remoteUrl ?? "") || basename(repositoryIdentityPath) || safeBasename(currentPath);
199
- const isWorktree = gitDirPath && gitCommonDirPath ? isGitWorktree(gitDirPath, gitCommonDirPath, gitMarkerDirectory) : false;
200
-
201
- const project: ProjectInfo = {
202
- displayName,
203
- compactLabel: "",
204
- sourcePath: currentPath,
205
- isGitRepository: true,
206
- gitRootPath,
207
- repositoryIdentityPath,
208
- branch,
209
- isWorktree,
210
- remoteName: preferredRemote,
211
- remoteUrl,
212
- };
213
- project.compactLabel = buildCompactProjectLabel(project);
214
- return project;
215
- }
216
-
217
- function buildFallbackProjectInfo(currentPath: string, displayPath = currentPath): ProjectInfo {
218
- const displayName = safeBasename(displayPath);
219
- const project: ProjectInfo = {
220
- displayName,
221
- compactLabel: displayName,
222
- sourcePath: currentPath,
223
- isGitRepository: false,
224
- };
225
- return project;
226
- }
227
-
228
- function resolveSearchStart(currentPath: string): string {
229
- if (!currentPath) return currentPath;
230
- try {
231
- return statSync(currentPath).isDirectory() ? currentPath : dirname(currentPath);
232
- } catch {
233
- return dirname(currentPath) === currentPath ? currentPath : dirname(currentPath);
234
- }
235
- }
236
-
237
- function findGitMarkerDirectory(startPath: string, visited: string[], cache: Map<string, ProjectInfo>): string | null {
238
- let current = startPath;
239
- while (current) {
240
- visited.push(current);
241
- const cached = cache.get(current);
242
- if (cached) {
243
- if (cached.isGitRepository) return null;
244
- }
245
- if (hasGitMarker(current)) return current;
246
- const parent = dirname(current);
247
- if (parent === current) return null;
248
- current = parent;
249
- }
250
- return null;
251
- }
252
-
253
- function hasGitMarker(directory: string): boolean {
254
- return existsSync(join(directory, ".git"));
255
- }
256
-
257
- function lookupCachedProject(currentPath: string, cache: Map<string, ProjectInfo>): ProjectInfo | undefined {
258
- if (cache.has(currentPath)) return cache.get(currentPath);
259
- let current = resolveSearchStart(currentPath);
260
- while (current) {
261
- const cached = cache.get(current);
262
- if (cached) {
263
- if (cached.isGitRepository) return cached;
264
- }
265
- const parent = dirname(current);
266
- if (parent === current) return undefined;
267
- current = parent;
268
- }
269
- return undefined;
270
- }
271
-
272
- function normalizeBranch(branch: string): string {
273
- if (!branch || branch === "HEAD") return "detached";
274
- return branch;
275
- }
276
-
277
- function resolveRepositoryIdentityPath(cwd: string, gitCommonDir: string, gitRootPath: string): string {
278
- const resolvedGitCommonDir = resolve(cwd, gitCommonDir);
279
- if (basename(resolvedGitCommonDir) !== ".git") return gitRootPath;
280
- return dirname(resolvedGitCommonDir);
281
- }
282
-
283
- function normalizePathForLookup(currentPath: string): string {
284
- return currentPath ? resolve(currentPath) : currentPath;
285
- }
286
-
287
- function runGit(context: string | GitCommandContext, args: string[], timeoutMs?: number): { ok: true; output: string } | { ok: false; error: string } {
288
- const commandContext = typeof context === "string" ? { cwd: context } : context;
289
- const gitArgs = commandContext.gitDir
290
- ? ["--git-dir", commandContext.gitDir, ...(commandContext.workTree ? ["--work-tree", commandContext.workTree] : []), ...args]
291
- : args;
292
- return maybeRunCommandInDirectory(["git", ...gitArgs], { cwd: commandContext.cwd, timeoutMs });
293
- }
294
-
295
- function resolveBranchName(context: string | GitCommandContext, timeoutMs?: number): string {
296
- const symbolicRef = runGit(context, ["symbolic-ref", "--quiet", "--short", "HEAD"], timeoutMs);
297
- if (symbolicRef.ok) return normalizeBranch(symbolicRef.output.trim());
298
- const abbreviated = runGit(context, ["rev-parse", "--abbrev-ref", "HEAD"], timeoutMs);
299
- if (abbreviated.ok) return normalizeBranch(abbreviated.output.trim());
300
- return "detached";
301
- }
302
-
303
- export function isWindowsAbsolutePath(value: string): boolean {
304
- return /^[a-zA-Z]:[\\/]/.test(value.trim());
305
- }
306
-
307
- export function windowsPathToWslPath(value: string): string {
308
- const trimmed = value.trim();
309
- const drive = trimmed[0]?.toLowerCase();
310
- if (!drive || !isWindowsAbsolutePath(trimmed)) return value;
311
- const rest = trimmed.slice(2).replace(/\\/g, "/").replace(/^\/+/, "");
312
- return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
313
- }
314
-
315
- export function normalizeWslWindowsPath(value: string, isWsl = IS_WSL): string {
316
- return isWsl && isWindowsAbsolutePath(value) ? windowsPathToWslPath(value) : value;
317
- }
318
-
319
- export function parseGitDirFromGitFile(contents: string): string | null {
320
- const line = contents.split(/\r?\n/).find((entry) => entry.trim().toLowerCase().startsWith("gitdir:"));
321
- if (!line) return null;
322
- const gitDir = line.slice(line.indexOf(":") + 1).trim();
323
- return gitDir || null;
324
- }
325
-
326
- export function resolveGitCommandContext(gitMarkerDirectory: string, isWsl = IS_WSL): GitCommandContext {
327
- const gitDir = readGitFileGitDir(gitMarkerDirectory);
328
- if (!gitDir) return { cwd: gitMarkerDirectory };
329
-
330
- const normalizedGitDir = normalizeWslWindowsPath(gitDir, isWsl);
331
- if (normalizedGitDir === gitDir) return { cwd: gitMarkerDirectory };
332
-
333
- return { cwd: gitMarkerDirectory, gitDir: normalizedGitDir, workTree: gitMarkerDirectory };
334
- }
335
-
336
- function readGitFileGitDir(gitMarkerDirectory: string): string | null {
337
- const gitPath = join(gitMarkerDirectory, ".git");
338
- try {
339
- if (!statSync(gitPath).isFile()) return null;
340
- return parseGitDirFromGitFile(readFileSync(gitPath, "utf8"));
341
- } catch {
342
- return null;
343
- }
344
- }
345
-
346
- function trimMap<K, V>(map: Map<K, V>, maxEntries: number): void {
347
- if (maxEntries <= 0) return;
348
- while (map.size > maxEntries) {
349
- const first = map.keys().next();
350
- if (first.done) return;
351
- map.delete(first.value);
352
- }
353
- }