@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,545 +0,0 @@
1
- import type { ScrylogConfig } from "../../../shared/config";
2
- import type { SessionModel, SessionState } from "../../../shared/types";
3
- import type { DaemonStore } from "../../store";
4
- import type { Collector } from "../../scheduler";
5
- import type { Logger } from "../../../shared/logger";
6
- import { NOOP_LOGGER } from "../../../shared/logger";
7
- import { createOpencodeClient } from "@opencode-ai/sdk";
8
- import type { Session, SessionStatus } from "@opencode-ai/sdk";
9
- import type { GitCollector } from "../git";
10
- import { discoverInstances, type DiscoveredInstance, clearPidPortCache } from "./discovery";
11
- import { buildSessionModel, type InstanceState } from "./session-builder";
12
-
13
- // ── Collector ─────────────────────────────────────────────────────────────────
14
-
15
- export class OpenCodeCollector implements Collector {
16
- name = "opencode";
17
- private instances: InstanceState[] = [];
18
- private store?: DaemonStore;
19
- private config?: ScrylogConfig;
20
- private logger: Logger = NOOP_LOGGER;
21
- private discoveryTimer?: ReturnType<typeof setInterval>;
22
- private gitCollector?: GitCollector;
23
- private refreshing = false;
24
- private skippedRefreshes = 0;
25
- private errorCount = 0;
26
-
27
- constructor(gitCollector?: GitCollector) {
28
- this.gitCollector = gitCollector;
29
- }
30
-
31
- async start(store: DaemonStore, config: ScrylogConfig, logger: Logger): Promise<void> {
32
- this.store = store;
33
- this.config = config;
34
- this.logger = logger;
35
-
36
- if (config.opencode.instances.length > 0) {
37
- logger.info("opencode", "Collector started — using configured instances", {
38
- event: "collector.started",
39
- collector: this.name,
40
- instances: config.opencode.instances,
41
- });
42
- } else if (config.opencode.autoDiscover) {
43
- logger.info("opencode", "Collector started — auto-discover enabled", { event: "collector.started", collector: this.name, autoDiscover: true });
44
- } else {
45
- logger.info("opencode", "Collector started — discovery disabled (no instances configured, autoDiscover=false)", { event: "collector.started", collector: this.name, autoDiscover: false });
46
- }
47
-
48
- await this.safeRefresh();
49
-
50
- // Re-discover instances periodically (in case opencode starts later)
51
- this.discoveryTimer = setInterval(() => void this.safeRefresh(), 60_000);
52
- }
53
-
54
- async stop(): Promise<void> {
55
- if (this.discoveryTimer) {
56
- clearInterval(this.discoveryTimer);
57
- }
58
-
59
- for (const inst of this.instances) {
60
- this.teardownInstance(inst);
61
- }
62
- this.instances = [];
63
-
64
- this.logger.info("opencode", "Collector stopped", { event: "collector.stopped", collector: this.name });
65
- }
66
-
67
- // ── Internal ───────────────────────────────────────────────────────────────
68
-
69
- private async safeRefresh(): Promise<void> {
70
- const store = this.store!;
71
- if (this.refreshing) {
72
- this.skippedRefreshes += 1;
73
- store.setCollectorStatus(this.name, { skippedPolls: this.skippedRefreshes });
74
- return;
75
- }
76
- this.refreshing = true;
77
- const startedAt = Date.now();
78
- try {
79
- await this.refresh();
80
- store.setCollectorStatus(this.name, { lastDurationMs: Date.now() - startedAt, skippedPolls: this.skippedRefreshes, errorCount: this.errorCount });
81
- } finally {
82
- this.refreshing = false;
83
- }
84
- }
85
-
86
- private async refresh(): Promise<void> {
87
- const config = this.config!;
88
- const discovered: DiscoveredInstance[] = [];
89
-
90
- if (config.opencode.instances.length > 0) {
91
- for (const url of config.opencode.instances) {
92
- discovered.push({ url });
93
- }
94
- this.logger.debug("opencode", "Using configured instances", { urls: config.opencode.instances });
95
- } else if (config.opencode.autoDiscover) {
96
- discovered.push(...discoverInstances(this.logger, config.daemon.commandTimeoutMs));
97
- }
98
-
99
- // Connect to new instances
100
- const existingUrls = new Set(this.instances.map((i) => i.url));
101
- for (const disc of discovered) {
102
- if (!existingUrls.has(disc.url)) {
103
- this.logger.debug("opencode", "Connecting to new instance", { url: disc.url, pid: disc.pid });
104
- const inst: InstanceState = {
105
- url: disc.url,
106
- pid: disc.pid,
107
- sessions: new Map(),
108
- status: new Map(),
109
- pendingPermissions: new Map(),
110
- pendingQuestions: new Map(),
111
- };
112
- this.instances.push(inst);
113
- void this.connectInstance(inst);
114
- }
115
- }
116
-
117
- // Tear down instances no longer discovered
118
- const discoveredUrls = new Set(discovered.map((d) => d.url));
119
- for (const inst of this.instances) {
120
- if (!discoveredUrls.has(inst.url)) {
121
- this.logger.debug("opencode", "Removing instance no longer discovered", { url: inst.url });
122
- this.teardownInstance(inst);
123
- this.instances = this.instances.filter((i) => i.url !== inst.url);
124
- }
125
- }
126
- }
127
-
128
- private async connectInstance(inst: InstanceState): Promise<void> {
129
- const store = this.store!;
130
- this.logger.info("opencode", "Connecting to instance", { event: "integration.connecting", instanceUrl: inst.url });
131
-
132
- try {
133
- const client = createOpencodeClient({ baseUrl: inst.url });
134
-
135
- // Load initial sessions
136
- const listResult = await client.session.list({ throwOnError: false });
137
- if (!listResult.data) {
138
- this.logger.warn("opencode", "Instance returned no session data", { event: "integration.invalid_response", instanceUrl: inst.url });
139
- return;
140
- }
141
-
142
- const sessions = (listResult.data as Session[]) ?? [];
143
-
144
- const statusMapResult = await client.session.status({ throwOnError: false });
145
- const statusMap = (statusMapResult.data as { [key: string]: SessionStatus } | undefined) ?? {};
146
-
147
- // Seed pending permissions on connect
148
- let pendingSeedCount = 0;
149
- try {
150
- // Use dynamic access since permission API may not exist on all SDK versions
151
- const permClient = (client as unknown as Record<string, { list: (opts: unknown) => Promise<{ data?: unknown }> }>)["permission"];
152
- if (permClient?.list) {
153
- const permResult = await permClient.list({ throwOnError: false });
154
- const pending = (permResult.data as Array<{ id: string; sessionID: string }> | undefined) ?? [];
155
- for (const req of pending) {
156
- if (!inst.pendingPermissions.has(req.sessionID)) {
157
- inst.pendingPermissions.set(req.sessionID, new Set());
158
- }
159
- inst.pendingPermissions.get(req.sessionID)!.add(req.id);
160
- pendingSeedCount++;
161
- }
162
- if (pendingSeedCount > 0) {
163
- this.logger.debug("opencode", "Seeded pending permissions on connect", { url: inst.url, count: pendingSeedCount });
164
- }
165
- }
166
- } catch {
167
- // permission.list may not exist on older opencode versions; ignore
168
- }
169
-
170
- for (const session of sessions) {
171
- const status: SessionStatus = statusMap[session.id] ?? { type: "idle" };
172
- const model = buildSessionModel(session, status, inst.url, inst.pendingPermissions, inst.pendingQuestions, this.gitCollector);
173
- inst.sessions.set(session.id, model);
174
- inst.status.set(session.id, status);
175
- store.upsertSession(model);
176
- }
177
-
178
- // Rebuild parent→child links after initial load
179
- store.syncChildSessionIds();
180
-
181
- // Register PID in store for PID-ancestry dedup
182
- if (inst.pid) {
183
- store.registerSdkPid(inst.pid, inst.url);
184
- }
185
-
186
- this.logger.info("opencode", "Connected to instance", {
187
- event: "integration.connected",
188
- instanceUrl: inst.url,
189
- sessions: sessions.length,
190
- pendingPermissions: pendingSeedCount,
191
- });
192
-
193
- store.setCollectorStatus(this.name, { running: true, lastRunAt: Date.now() });
194
-
195
- // Subscribe to SSE events
196
- this.subscribeSSE(inst, client);
197
- } catch (error) {
198
- const msg = error instanceof Error ? error.message : String(error);
199
- this.logger.warn("opencode", "Failed to connect to instance", { event: "integration.connect_failed", instanceUrl: inst.url, errorMessage: msg });
200
- store.setCollectorStatus(this.name, { lastError: msg });
201
- // Start polling fallback
202
- this.startPollFallback(inst);
203
- }
204
- }
205
-
206
- private subscribeSSE(
207
- inst: InstanceState,
208
- client: ReturnType<typeof createOpencodeClient>,
209
- ): void {
210
- const store = this.store!;
211
- const abort = new AbortController();
212
- inst.sseAbort = abort;
213
-
214
- const connect = async () => {
215
- try {
216
- // Use client.event.subscribe() (GET /event) instead of client.global.event()
217
- // global.event() only emits server.* events (heartbeat, connected).
218
- // event.subscribe() delivers all session/permission events for this instance.
219
- const sseResult = await client.event.subscribe({ throwOnError: false });
220
- if (!sseResult.stream) {
221
- this.logger.warn("opencode", "SSE stream unavailable", { event: "integration.unavailable", instanceUrl: inst.url, transport: "sse" });
222
- return;
223
- }
224
-
225
- this.logger.info("opencode", "SSE stream subscribed", { event: "integration.subscribed", instanceUrl: inst.url, transport: "sse" });
226
-
227
- // event.subscribe() yields Event directly: { type: string, properties: unknown }
228
- for await (const event of sseResult.stream as AsyncIterable<{
229
- type?: string;
230
- properties?: unknown;
231
- }>) {
232
- if (abort.signal.aborted) break;
233
-
234
- const payload = event;
235
- if (!payload?.type) continue;
236
-
237
- // Log ALL events including heartbeats so we can confirm stream is alive
238
- this.logger.debug("opencode", "SSE event received", {
239
- event: "integration.event_received",
240
- instanceUrl: inst.url,
241
- type: payload.type,
242
- });
243
-
244
- // Skip internal server heartbeat/connected events — not actionable
245
- if (payload.type === "server.heartbeat" || payload.type === "server.connected") continue;
246
-
247
- if (payload.type === "session.status") {
248
- const props = payload.properties as { sessionID: string; status: SessionStatus } | undefined;
249
- if (!props) continue;
250
- const existing = inst.sessions.get(props.sessionID);
251
- if (existing) {
252
- const prevStatus = inst.status.get(props.sessionID);
253
- const prevState = existing.state;
254
- inst.status.set(props.sessionID, props.status);
255
- const updated = buildSessionModel(
256
- { id: props.sessionID, title: existing.summary, time: { created: 0, updated: existing.lastActivity }, directory: existing.currentPath ?? "" } as Session,
257
- props.status,
258
- inst.url,
259
- inst.pendingPermissions,
260
- inst.pendingQuestions,
261
- this.gitCollector,
262
- );
263
- const merged: SessionModel = { ...existing, state: updated.state, hasAttention: updated.hasAttention, project: updated.project ?? existing.project };
264
- inst.sessions.set(props.sessionID, merged);
265
- store.upsertSession(merged);
266
- if (prevState !== merged.state) {
267
- this.logger.debug("opencode", "Session state changed via SSE", {
268
- sessionId: props.sessionID,
269
- from: prevState,
270
- to: merged.state,
271
- sdkStatus: props.status.type,
272
- prevSdkStatus: prevStatus?.type,
273
- });
274
- }
275
- }
276
- } else if (payload.type === "session.idle") {
277
- // Explicit idle signal — treat as status update to idle
278
- const props = payload.properties as { sessionID?: string } | undefined;
279
- if (props?.sessionID) {
280
- const existing = inst.sessions.get(props.sessionID);
281
- if (existing && existing.state !== "idle") {
282
- const idleStatus: SessionStatus = { type: "idle" };
283
- inst.status.set(props.sessionID, idleStatus);
284
- const merged: SessionModel = { ...existing, state: "idle", hasAttention: false };
285
- inst.sessions.set(props.sessionID, merged);
286
- store.upsertSession(merged);
287
- this.logger.debug("opencode", "Session state changed via SSE", {
288
- sessionId: props.sessionID,
289
- from: existing.state,
290
- to: "idle",
291
- sdkStatus: "idle",
292
- });
293
- }
294
- }
295
- } else if (payload.type === "session.updated" || payload.type === "session.created") {
296
- // properties.info carries the full Session object — no extra API call needed
297
- const props = payload.properties as { info?: Session } | undefined;
298
- if (props?.info) {
299
- const session = props.info;
300
- const status = inst.status.get(session.id) ?? { type: "idle" as const };
301
- const model = buildSessionModel(session, status, inst.url, inst.pendingPermissions, inst.pendingQuestions, this.gitCollector);
302
- inst.sessions.set(session.id, model);
303
- inst.status.set(session.id, status);
304
- store.upsertSession(model);
305
- // Rebuild parent→child links whenever a session is created or updated
306
- store.syncChildSessionIds();
307
- this.logger.debug("opencode", `Session ${payload.type.replace("session.", "")} via SSE`, {
308
- sessionId: session.id,
309
- title: session.title,
310
- });
311
- }
312
- } else if (payload.type === "session.deleted") {
313
- const props = payload.properties as { info?: { id?: string } } | undefined;
314
- const sessionId = props?.info?.id;
315
- if (sessionId) {
316
- this.logger.debug("opencode", "Session deleted via SSE", { sessionId });
317
- inst.sessions.delete(sessionId);
318
- inst.pendingPermissions.delete(sessionId);
319
- store.removeSession(`opencode:${sessionId}`);
320
- // Rebuild parent→child links after deletion
321
- store.syncChildSessionIds();
322
- }
323
- } else if (payload.type === "session.error") {
324
- const props = payload.properties as { sessionID?: string; error?: { name?: string; data?: { message?: string; statusCode?: number } } } | undefined;
325
- if (props?.sessionID) {
326
- this.logger.warn("opencode", "Session error via SSE", {
327
- sessionId: props.sessionID,
328
- errorName: props.error?.name,
329
- error: props.error?.data?.message,
330
- statusCode: props.error?.data?.statusCode,
331
- });
332
- }
333
- } else if (payload.type === "permission.updated") {
334
- // permission.updated fires when a tool permission prompt is pending
335
- // properties IS the Permission object directly
336
- const perm = payload.properties as { id?: string; sessionID?: string; type?: string; title?: string } | undefined;
337
- if (perm?.id && perm.sessionID) {
338
- if (!inst.pendingPermissions.has(perm.sessionID)) {
339
- inst.pendingPermissions.set(perm.sessionID, new Set());
340
- }
341
- inst.pendingPermissions.get(perm.sessionID)!.add(perm.id);
342
- this.logger.debug("opencode", "Permission requested — session needs attention", {
343
- sessionId: perm.sessionID,
344
- permId: perm.id,
345
- permType: perm.type,
346
- title: perm.title,
347
- });
348
- this.refreshSessionState(inst, perm.sessionID, store);
349
- }
350
- } else if (payload.type === "permission.replied") {
351
- // properties: { sessionID, permissionID, response }
352
- const props = payload.properties as { sessionID?: string; permissionID?: string; response?: string } | undefined;
353
- if (props?.permissionID && props.sessionID) {
354
- inst.pendingPermissions.get(props.sessionID)?.delete(props.permissionID);
355
- this.logger.debug("opencode", "Permission replied — unblocking session", {
356
- sessionId: props.sessionID,
357
- permId: props.permissionID,
358
- response: props.response,
359
- });
360
- this.refreshSessionState(inst, props.sessionID, store);
361
- }
362
- }
363
- }
364
- } catch (error) {
365
- if (abort.signal.aborted) return;
366
- const msg = error instanceof Error ? error.message : String(error);
367
- this.errorCount += 1;
368
- this.logger.warn("opencode", "SSE disconnected — falling back to polling", { event: "integration.disconnected", instanceUrl: inst.url, transport: "sse", errorMessage: msg });
369
- store.setCollectorStatus(this.name, { lastError: msg, errorCount: this.errorCount });
370
- this.startPollFallback(inst);
371
- }
372
- };
373
-
374
- void connect();
375
- }
376
-
377
- /** Re-compute and push updated state for a single session after permission/question change */
378
- private refreshSessionState(inst: InstanceState, sessionId: string, store: DaemonStore): void {
379
- const existing = inst.sessions.get(sessionId);
380
- if (!existing) return;
381
- const status = inst.status.get(sessionId) ?? { type: "idle" as const };
382
- const prevState = existing.state;
383
- const updated = buildSessionModel(
384
- { id: sessionId, title: existing.summary, time: { created: 0, updated: existing.lastActivity }, directory: existing.currentPath ?? "" } as Session,
385
- status,
386
- inst.url,
387
- inst.pendingPermissions,
388
- inst.pendingQuestions,
389
- this.gitCollector,
390
- );
391
- const merged: SessionModel = { ...existing, state: updated.state, hasAttention: updated.hasAttention, project: updated.project ?? existing.project };
392
- inst.sessions.set(sessionId, merged);
393
- store.upsertSession(merged);
394
- if (prevState !== merged.state) {
395
- this.logger.debug("opencode", "Session state changed via permission/question event", {
396
- sessionId,
397
- from: prevState,
398
- to: merged.state,
399
- hasAttention: merged.hasAttention,
400
- });
401
- }
402
- }
403
-
404
- private async loadSingleSession(
405
- inst: InstanceState,
406
- client: ReturnType<typeof createOpencodeClient>,
407
- sessionId: string,
408
- ): Promise<void> {
409
- const store = this.store!;
410
- try {
411
- const [sessionResult, statusMapResult] = await Promise.all([
412
- client.session.get({ path: { id: sessionId }, throwOnError: false }),
413
- client.session.status({ throwOnError: false }),
414
- ]);
415
- const session = sessionResult.data as Session | undefined;
416
- const statusMap = (statusMapResult.data as { [key: string]: SessionStatus } | undefined) ?? {};
417
- const status = statusMap[sessionId] ?? { type: "idle" as const };
418
- if (!session) return;
419
-
420
- const model = buildSessionModel(session, status, inst.url, inst.pendingPermissions, inst.pendingQuestions, this.gitCollector);
421
- inst.sessions.set(sessionId, model);
422
- inst.status.set(sessionId, status);
423
- store.upsertSession(model);
424
- } catch {
425
- // Ignore per-session load errors
426
- }
427
- }
428
-
429
- private startPollFallback(inst: InstanceState): void {
430
- if (inst.pollTimer) return;
431
- this.logger.warn("opencode", "Starting poll fallback", { event: "collector.fallback_started", instanceUrl: inst.url, intervalMs: 10_000 });
432
- inst.pollTimer = setInterval(() => void this.safePollInstance(inst), 10_000);
433
- void this.safePollInstance(inst);
434
- }
435
-
436
- /**
437
- * skippedPolls in collector status aggregates two counters:
438
- * - `skippedRefreshes`: global discovery cycles skipped due to concurrent safeRefresh
439
- * - `inst.skippedPolls`: per-instance poll cycles skipped due to concurrent safePollInstance
440
- * This reports the sum for the current instance only.
441
- */
442
- private async safePollInstance(inst: InstanceState): Promise<void> {
443
- const store = this.store!;
444
- if (inst.polling) {
445
- inst.skippedPolls = (inst.skippedPolls ?? 0) + 1;
446
- store.setCollectorStatus(this.name, { skippedPolls: this.skippedRefreshes + (inst.skippedPolls ?? 0) });
447
- return;
448
- }
449
- inst.polling = true;
450
- const startedAt = Date.now();
451
- try {
452
- await this.pollInstance(inst);
453
- store.setCollectorStatus(this.name, { lastDurationMs: Date.now() - startedAt, skippedPolls: this.skippedRefreshes + (inst.skippedPolls ?? 0), errorCount: this.errorCount });
454
- } finally {
455
- inst.polling = false;
456
- }
457
- }
458
-
459
- private async pollInstance(inst: InstanceState): Promise<void> {
460
- const store = this.store!;
461
- try {
462
- const client = createOpencodeClient({ baseUrl: inst.url });
463
- const listResult = await client.session.list({ throwOnError: false });
464
- if (!listResult.data) return;
465
-
466
- // Ensure PID is registered even if connectInstance previously failed.
467
- // Without this, pollInstance keeps sessions alive but sdkPidSet stays
468
- // empty, causing the tmux collector to create a duplicate fallback entry.
469
- if (inst.pid && !store.getSdkPids().has(inst.pid)) {
470
- store.registerSdkPid(inst.pid, inst.url);
471
- }
472
-
473
- const sessions = (listResult.data as Session[]) ?? [];
474
- const seenIds = new Set<string>();
475
-
476
- const statusMapResult = await client.session.status({ throwOnError: false });
477
- const statusMap = (statusMapResult.data as { [key: string]: SessionStatus } | undefined) ?? {};
478
-
479
- for (const session of sessions) {
480
- seenIds.add(session.id);
481
- const status: SessionStatus = statusMap[session.id] ?? { type: "idle" };
482
- const prevState = inst.sessions.get(session.id)?.state;
483
- const model = buildSessionModel(session, status, inst.url, inst.pendingPermissions, inst.pendingQuestions, this.gitCollector);
484
- inst.sessions.set(session.id, model);
485
- inst.status.set(session.id, status);
486
- store.upsertSession(model);
487
- if (prevState !== undefined && prevState !== model.state) {
488
- this.logger.debug("opencode", "Session state changed via poll", {
489
- sessionId: session.id,
490
- from: prevState,
491
- to: model.state,
492
- sdkStatus: status.type,
493
- });
494
- }
495
- }
496
-
497
- // Remove sessions no longer in list
498
- for (const [id] of inst.sessions) {
499
- if (!seenIds.has(id)) {
500
- inst.sessions.delete(id);
501
- inst.pendingPermissions.delete(id);
502
- inst.pendingQuestions.delete(id);
503
- store.removeSession(`opencode:${id}`);
504
- }
505
- }
506
-
507
- this.logger.debug("opencode", "Poll cycle complete", { event: "collector.poll_completed", collector: this.name, instanceUrl: inst.url, sessions: sessions.length });
508
- store.setCollectorStatus(this.name, { running: true, lastRunAt: Date.now() });
509
- // Rebuild parent→child links after each poll cycle
510
- store.syncChildSessionIds();
511
- } catch (error) {
512
- const msg = error instanceof Error ? error.message : String(error);
513
- this.errorCount += 1;
514
- this.logger.warn("opencode", "Poll error", { event: "collector.poll_failed", collector: this.name, instanceUrl: inst.url, errorMessage: msg });
515
- store.setCollectorStatus(this.name, {
516
- lastError: msg,
517
- errorCount: this.errorCount,
518
- });
519
- }
520
- }
521
-
522
- private teardownInstance(inst: InstanceState): void {
523
- this.logger.info("opencode", "Tearing down instance", { event: "integration.teardown", instanceUrl: inst.url, sessions: inst.sessions.size });
524
- if (inst.sseAbort) {
525
- inst.sseAbort.abort();
526
- }
527
- if (inst.pollTimer) {
528
- clearInterval(inst.pollTimer);
529
- }
530
- if (inst.pid) {
531
- this.store?.unregisterSdkPid(inst.pid);
532
- clearPidPortCache(inst.pid);
533
- }
534
- const store = this.store;
535
- if (store) {
536
- for (const [id] of inst.sessions) {
537
- store.removeSession(`opencode:${id}`);
538
- }
539
- }
540
- inst.sessions.clear();
541
- inst.status.clear();
542
- inst.pendingPermissions.clear();
543
- inst.pendingQuestions.clear();
544
- }
545
- }