@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.
- package/dist/assets/highlights-eq9cgrbb.scm +604 -0
- package/dist/assets/highlights-ghv9g403.scm +205 -0
- package/dist/assets/highlights-hk7bwhj4.scm +284 -0
- package/dist/assets/highlights-r812a2qc.scm +150 -0
- package/dist/assets/highlights-x6tmsnaa.scm +115 -0
- package/dist/assets/injections-73j83es3.scm +27 -0
- package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
- package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
- package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/cli/index.js +499 -0
- package/dist/cli/index.js.map +61 -0
- package/dist/daemon/index.js +670 -0
- package/dist/daemon/index.js.map +1475 -0
- package/dist/hooks/claude.js +6 -0
- package/dist/hooks/claude.js.map +11 -0
- package/dist/hooks/codex.js +10 -0
- package/dist/hooks/codex.js.map +11 -0
- package/dist/mcp/index.js +100 -0
- package/dist/mcp/index.js.map +231 -0
- package/dist/oc/index.js +21 -0
- package/dist/oc/index.js.map +10 -0
- package/package.json +16 -13
- package/src/api/docs/sessions.keys.ts +0 -6
- package/src/api/docs/sessions.ts +0 -82
- package/src/api/docs/system.keys.ts +0 -5
- package/src/api/docs/system.ts +0 -69
- package/src/api/docs/usage.keys.ts +0 -3
- package/src/api/docs/usage.ts +0 -24
- package/src/api/docs/webhooks.keys.ts +0 -5
- package/src/api/docs/webhooks.ts +0 -78
- package/src/api/index.ts +0 -18
- package/src/api/lib/auth.ts +0 -43
- package/src/api/lib/errors.ts +0 -28
- package/src/api/lib/route.ts +0 -18
- package/src/api/models/claude.ts +0 -4
- package/src/api/models/codex.ts +0 -4
- package/src/api/models/opencode.ts +0 -11
- package/src/api/routes/sessions.ts +0 -51
- package/src/api/routes/system.ts +0 -106
- package/src/api/routes/usage.ts +0 -15
- package/src/api/routes/webhooks/claude.ts +0 -149
- package/src/api/routes/webhooks/codex.ts +0 -175
- package/src/api/routes/webhooks/opencode.ts +0 -112
- package/src/api/routes.ts +0 -93
- package/src/api/schemas/common.ts +0 -29
- package/src/api/schemas/sessions.ts +0 -86
- package/src/api/schemas/usage.ts +0 -51
- package/src/api/schemas/webhooks/claude.ts +0 -32
- package/src/api/schemas/webhooks/codex.ts +0 -34
- package/src/api/schemas/webhooks/opencode.ts +0 -29
- package/src/api/services/claude-session.ts +0 -9
- package/src/api/services/codex-session.ts +0 -8
- package/src/api/services/opencode-session.ts +0 -73
- package/src/classifier.ts +0 -206
- package/src/cli/commands.ts +0 -714
- package/src/cli/index.ts +0 -18
- package/src/cli/program.ts +0 -175
- package/src/cli/prompt.ts +0 -44
- package/src/daemon/cli.ts +0 -27
- package/src/daemon/collectors/claude/README.md +0 -108
- package/src/daemon/collectors/claude/collector.ts +0 -423
- package/src/daemon/collectors/claude/discovery.ts +0 -127
- package/src/daemon/collectors/claude/hooks.ts +0 -181
- package/src/daemon/collectors/claude/index.ts +0 -8
- package/src/daemon/collectors/claude/jsonl.ts +0 -85
- package/src/daemon/collectors/claude/transcript.ts +0 -246
- package/src/daemon/collectors/codex/README.md +0 -147
- package/src/daemon/collectors/codex/collector.ts +0 -661
- package/src/daemon/collectors/codex/discovery.ts +0 -104
- package/src/daemon/collectors/codex/hooks.ts +0 -179
- package/src/daemon/collectors/codex/index.ts +0 -7
- package/src/daemon/collectors/codex/jsonl.ts +0 -108
- package/src/daemon/collectors/codex/transcript.ts +0 -299
- package/src/daemon/collectors/git.ts +0 -353
- package/src/daemon/collectors/herdr/collector.ts +0 -128
- package/src/daemon/collectors/herdr/index.ts +0 -2
- package/src/daemon/collectors/herdr/normalization.ts +0 -111
- package/src/daemon/collectors/opencode/README.md +0 -106
- package/src/daemon/collectors/opencode/collector.ts +0 -545
- package/src/daemon/collectors/opencode/discovery.ts +0 -257
- package/src/daemon/collectors/opencode/index.ts +0 -5
- package/src/daemon/collectors/opencode/session-builder.ts +0 -59
- package/src/daemon/collectors/opencode-db.ts +0 -294
- package/src/daemon/collectors/process.ts +0 -201
- package/src/daemon/collectors/tmux/README.md +0 -128
- package/src/daemon/collectors/tmux/collector.ts +0 -492
- package/src/daemon/collectors/tmux/enrichment.ts +0 -242
- package/src/daemon/collectors/tmux/index.ts +0 -4
- package/src/daemon/collectors/tmux/parse.ts +0 -128
- package/src/daemon/collectors/usage.ts +0 -259
- package/src/daemon/index.ts +0 -209
- package/src/daemon/scheduler.ts +0 -86
- package/src/daemon/store.ts +0 -630
- package/src/detect.ts +0 -143
- package/src/hooks/claude.ts +0 -596
- package/src/hooks/codex.ts +0 -517
- package/src/legacy/classifier.ts +0 -215
- package/src/legacy/detect.ts +0 -143
- package/src/mcp/client-server.ts +0 -21
- package/src/mcp/http-handler.ts +0 -126
- package/src/mcp/index.ts +0 -61
- package/src/mcp/resources/config-current.ts +0 -19
- package/src/mcp/resources/index.ts +0 -11
- package/src/mcp/resources/sessions-active.ts +0 -19
- package/src/mcp/resources/sessions-all.ts +0 -19
- package/src/mcp/schemas/sessions.ts +0 -3
- package/src/mcp/server.ts +0 -17
- package/src/mcp/services/api-adapter.ts +0 -48
- package/src/mcp/services/data-source.ts +0 -22
- package/src/mcp/services/store-adapter.ts +0 -40
- package/src/mcp/tools/get-active-sessions.ts +0 -16
- package/src/mcp/tools/get-session-children.ts +0 -24
- package/src/mcp/tools/get-session-messages.ts +0 -24
- package/src/mcp/tools/get-session.ts +0 -23
- package/src/mcp/tools/index.ts +0 -15
- package/src/mcp/tools/list-sessions.ts +0 -16
- package/src/oc/index.ts +0 -302
- package/src/service/manager.ts +0 -284
- package/src/shared/config.ts +0 -511
- package/src/shared/ipc.ts +0 -80
- package/src/shared/logger.ts +0 -248
- package/src/shared/paths.ts +0 -73
- package/src/shared/platform.ts +0 -110
- package/src/shared/session-exclusions.ts +0 -5
- package/src/shared/session-view.ts +0 -302
- package/src/shared/terminal-capabilities.ts +0 -130
- package/src/shared/types.ts +0 -330
- package/src/shared/utils.ts +0 -339
- package/src/shared/version.ts +0 -44
- package/src/tui/App.tsx +0 -240
- package/src/tui/client.ts +0 -163
- package/src/tui/components/BindingsModal.tsx +0 -82
- package/src/tui/components/DashboardHeader.tsx +0 -256
- package/src/tui/components/KeyBar.tsx +0 -67
- package/src/tui/components/SessionDetailModal.tsx +0 -169
- package/src/tui/components/SessionsView.tsx +0 -184
- package/src/tui/components/TabBar.tsx +0 -40
- package/src/tui/components/UsageSummaryPane.tsx +0 -272
- package/src/tui/components/UsageView.tsx +0 -242
- package/src/tui/components/tool-icons.ts +0 -24
- package/src/tui/index.tsx +0 -106
- package/src/tui/keybindings.ts +0 -49
- package/src/tui/model.ts +0 -295
- package/src/tui/state.ts +0 -77
- package/src/tui/theme.ts +0 -110
- package/src/types.ts +0 -113
- package/src/usage/README.md +0 -323
- package/src/usage/api.ts +0 -99
- package/src/usage/client.ts +0 -3
- package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
- package/src/usage/clients/anthropic-web-usage.ts +0 -329
- package/src/usage/clients/claude-cli-probe.ts +0 -513
- package/src/usage/clients/codex-app-server.ts +0 -354
- package/src/usage/clients/codex-oauth-usage.ts +0 -240
- package/src/usage/clients/opencode-go-api.ts +0 -100
- package/src/usage/clients/opencode-go-usage.ts +0 -577
- package/src/usage/errors.ts +0 -33
- package/src/usage/index.ts +0 -20
- package/src/usage/local/opencode-go-local.ts +0 -194
- package/src/usage/mappers.ts +0 -282
- package/src/usage/planner.ts +0 -118
- package/src/usage/providers/claude.ts +0 -233
- package/src/usage/providers/codex.ts +0 -115
- package/src/usage/providers/opencode-go.ts +0 -115
- package/src/usage/resolver.ts +0 -4
- package/src/usage/resolvers/browser-cookies.ts +0 -808
- package/src/usage/resolvers/claude-auth.ts +0 -208
- package/src/usage/resolvers/claude-cli.ts +0 -24
- package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
- package/src/usage/resolvers/claude-web.ts +0 -82
- package/src/usage/resolvers/codex-auth.ts +0 -174
- package/src/usage/resolvers/keychain-coordinator.ts +0 -397
- package/src/usage/resolvers/opencode-go-web.ts +0 -55
- package/src/usage/types.ts +0 -225
- package/src/usage/web-session-cache.ts +0 -37
- /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
- /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
|
-
}
|