@fdcn/sourcefed 0.3.6 → 0.3.7

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/opencode.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  daemonCommand,
8
8
  daemonEnvironment,
9
9
  spawnLocalDaemon
10
- } from "./chunk-X3BZSHKZ.js";
10
+ } from "./chunk-E6J3BAXP.js";
11
11
  import "./chunk-MLKGABMK.js";
12
12
 
13
13
  // packages/sourcefed/src/server.ts
@@ -22,9 +22,193 @@ import { Plugin } from "@opencode/plugin";
22
22
  import { existsSync } from "node:fs";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { Rpc } from "@opencode/plugin";
25
+
26
+ // adapters/opencode/subscriptions.ts
27
+ var REFRESH_MS = 15e3;
28
+ var OpenCodeSubscriptions = class {
29
+ constructor(connect) {
30
+ this.connect = connect;
31
+ }
32
+ owners = /* @__PURE__ */ new Set();
33
+ subscriptions = /* @__PURE__ */ new Map();
34
+ lifetime = new AbortController();
35
+ daemon;
36
+ connecting;
37
+ refreshing;
38
+ dirty = false;
39
+ lastAttemptAt = -Infinity;
40
+ timer;
41
+ get closed() {
42
+ return this.lifetime.signal.aborted;
43
+ }
44
+ async add(owner) {
45
+ if (this.closed) throw new Error("sourcefed subscription hub is closed");
46
+ this.owners.add(owner);
47
+ if (!this.timer) {
48
+ this.timer = setInterval(() => void this.refresh(), REFRESH_MS);
49
+ this.timer.unref?.();
50
+ }
51
+ await this.refresh();
52
+ }
53
+ async remove(owner) {
54
+ this.owners.delete(owner);
55
+ const closing = [];
56
+ for (const [id, subscription] of this.subscriptions) {
57
+ if (this.ownerFor(subscription.info)) continue;
58
+ this.subscriptions.delete(id);
59
+ closing.push(this.closeSubscription(subscription));
60
+ }
61
+ if (this.owners.size > 0) {
62
+ await Promise.all(closing);
63
+ await this.refresh();
64
+ return;
65
+ }
66
+ this.lifetime.abort();
67
+ clearInterval(this.timer);
68
+ this.timer = void 0;
69
+ await Promise.all(closing);
70
+ await this.daemon?.close();
71
+ this.daemon = void 0;
72
+ await this.connecting;
73
+ await this.refreshing;
74
+ }
75
+ async request(method, params) {
76
+ const daemon = await this.ensureDaemon();
77
+ if (!daemon || this.closed) throw new Error("sourcefed daemon unavailable or bridge closed");
78
+ return daemon.request(method, params);
79
+ }
80
+ refresh() {
81
+ if (this.closed) return Promise.resolve();
82
+ this.dirty = true;
83
+ if (!this.refreshing) {
84
+ this.refreshing = this.refreshLoop();
85
+ }
86
+ return this.refreshing;
87
+ }
88
+ async refreshLoop() {
89
+ try {
90
+ do {
91
+ this.dirty = false;
92
+ try {
93
+ const daemon = await this.ensureDaemon();
94
+ if (daemon && !this.closed) await this.reconcile(daemon);
95
+ } catch (error) {
96
+ if (!this.closed) console.error(`[sourcefed] refresh failed: ${String(error)}`);
97
+ }
98
+ } while (this.dirty && !this.closed);
99
+ } finally {
100
+ this.refreshing = void 0;
101
+ }
102
+ }
103
+ async ensureDaemon() {
104
+ if (this.closed) return void 0;
105
+ if (this.daemon) return this.daemon;
106
+ if (this.connecting) return this.connecting;
107
+ if (Date.now() - this.lastAttemptAt < REFRESH_MS) return void 0;
108
+ this.lastAttemptAt = Date.now();
109
+ this.connecting = this.connect().then(async (daemon) => {
110
+ if (this.closed) {
111
+ await daemon.close();
112
+ return void 0;
113
+ }
114
+ this.daemon = daemon;
115
+ return daemon;
116
+ }).catch((error) => {
117
+ console.error(`[sourcefed] daemon unavailable: ${String(error)}`);
118
+ return void 0;
119
+ }).finally(() => {
120
+ this.connecting = void 0;
121
+ });
122
+ return this.connecting;
123
+ }
124
+ async reconcile(daemon) {
125
+ const result = await daemon.request("monitor.targets", { kind: "opencode-session" });
126
+ const targets = enabledSessions(result);
127
+ for (const [id, subscription] of this.subscriptions) {
128
+ if (targets.has(id)) continue;
129
+ this.subscriptions.delete(id);
130
+ await this.closeSubscription(subscription);
131
+ }
132
+ for (const id of targets) {
133
+ if (this.closed) return;
134
+ const reader = this.owners.values().next().value;
135
+ if (!reader) return;
136
+ let info;
137
+ try {
138
+ info = await reader.getSession(id, this.lifetime.signal);
139
+ } catch (error) {
140
+ if (!error || typeof error !== "object" || !("_tag" in error) || error._tag !== "SessionNotFoundError") {
141
+ if (!this.closed) console.error(`[sourcefed] session ${id} lookup failed: ${String(error)}`);
142
+ continue;
143
+ }
144
+ }
145
+ if (this.closed) return;
146
+ const owner = info && this.ownerFor(info);
147
+ const existing = this.subscriptions.get(id);
148
+ if (!owner || !info) {
149
+ if (existing) {
150
+ this.subscriptions.delete(id);
151
+ await this.closeSubscription(existing);
152
+ }
153
+ } else if (existing) {
154
+ existing.info = info;
155
+ } else {
156
+ await this.subscribe(daemon, id, info);
157
+ }
158
+ }
159
+ }
160
+ ownerFor(info) {
161
+ const candidates = [...this.owners].filter((owner) => owner.location.project.id === info.projectID);
162
+ return candidates.find((owner) => owner.location.directory === info.location.directory) ?? candidates[0];
163
+ }
164
+ async subscribe(daemon, id, info) {
165
+ const controller = new AbortController();
166
+ const subscription = {
167
+ info,
168
+ controller,
169
+ listener: daemon.subscribe({ kind: "opencode-session", id }, async (events) => {
170
+ await this.route(id, subscription, events);
171
+ }, { signal: controller.signal })
172
+ };
173
+ this.subscriptions.set(id, subscription);
174
+ try {
175
+ await subscription.listener;
176
+ } catch (error) {
177
+ if (this.subscriptions.get(id) === subscription) this.subscriptions.delete(id);
178
+ if (!controller.signal.aborted) console.error(`[sourcefed] subscribe ${id} failed: ${String(error)}`);
179
+ }
180
+ }
181
+ async route(id, subscription, events) {
182
+ const reader = this.owners.values().next().value;
183
+ if (!reader) throw new Error("sourcefed session owner unloaded");
184
+ const info = await reader.getSession(id, this.lifetime.signal);
185
+ const owner = this.ownerFor(info);
186
+ if (!owner) throw new Error("sourcefed session has no loaded project owner");
187
+ subscription.info = info;
188
+ await owner.routeEvents(events);
189
+ }
190
+ async closeSubscription(subscription) {
191
+ subscription.controller.abort();
192
+ await subscription.listener.then((listener) => listener.close(), () => void 0);
193
+ }
194
+ };
195
+ function enabledSessions(result) {
196
+ if (!result || typeof result !== "object" || !("targets" in result) || !Array.isArray(result.targets)) {
197
+ throw new Error("invalid monitor.targets response");
198
+ }
199
+ const ids = /* @__PURE__ */ new Set();
200
+ for (const target of result.targets) {
201
+ if (!target || target.kind !== "opencode-session" || typeof target.id !== "string") {
202
+ throw new Error("invalid opencode-session target");
203
+ }
204
+ ids.add(target.id);
205
+ }
206
+ return ids;
207
+ }
208
+
209
+ // adapters/opencode/bridge.ts
25
210
  var TARGET_KIND = "opencode-session";
26
211
  var GATE_TIMEOUT_MS = 3e3;
27
- var RESUBSCRIBE_MS = 15e3;
28
212
  var ChauffeurGate = Rpc.define({
29
213
  id: "chauffeur",
30
214
  methods: {
@@ -52,97 +236,53 @@ var ChauffeurGate = Rpc.define({
52
236
  },
53
237
  events: {}
54
238
  });
55
- var activeBridge;
239
+ var HUB = Symbol.for("sourcefed.opencode.subscriptions.v1");
240
+ var shared = globalThis;
241
+ function subscriptions() {
242
+ if (!shared[HUB] || shared[HUB].closed) shared[HUB] = new OpenCodeSubscriptions(connectDaemon);
243
+ return shared[HUB];
244
+ }
56
245
  var OpenCodeBridge = class {
57
- constructor(session, rpc) {
246
+ constructor(session, rpc, location, hub = subscriptions()) {
58
247
  this.session = session;
59
248
  this.rpc = rpc;
249
+ this.location = location;
250
+ this.hub = hub;
60
251
  }
61
- daemon;
62
- listeners = /* @__PURE__ */ new Map();
63
- lastAttemptAt = 0;
64
- lastError;
65
- timer;
252
+ started;
253
+ closed = false;
254
+ closing;
66
255
  /**
67
256
  * Chauffeur or the CLI can create a monitor for a session here at any time,
68
257
  * without this bridge's tools, so check for new targets periodically too.
69
258
  */
70
259
  async start() {
71
- await this.ensureDaemon();
72
- this.timer = setInterval(() => void this.refresh(), RESUBSCRIBE_MS);
73
- this.timer.unref?.();
260
+ if (this.closed) throw new Error("sourcefed bridge is closed");
261
+ this.started ??= this.hub.add(this);
262
+ await this.started;
74
263
  }
75
- async ensureTarget(sessionID) {
76
- await this.ensureDaemon();
77
- const existing = this.listeners.get(sessionID);
78
- if (existing) {
79
- await existing;
80
- return;
81
- }
82
- const pending = this.daemon.subscribe(this.target(sessionID), async (events) => {
83
- await this.routeEvents(events);
84
- });
85
- this.listeners.set(sessionID, pending);
264
+ async callTool(name, arguments_, sessionID) {
86
265
  try {
87
- await pending;
266
+ await this.start();
267
+ const method = name.replace(/_/g, ".");
268
+ const result = await this.hub.request(method, { ...arguments_, target: this.target(sessionID) });
269
+ if (["monitor.create", "monitor.start", "monitor.stop", "monitor.remove"].includes(method)) await this.hub.refresh();
270
+ return result;
88
271
  } catch (error) {
89
- this.listeners.delete(sessionID);
90
- throw error;
91
- }
92
- }
93
- async callTool(name, arguments_, sessionID) {
94
- await this.ensureDaemon();
95
- if (!this.daemon) {
96
- return { ok: false, error: `sourcefed daemon unavailable: ${this.lastError ?? "not started"}` };
272
+ return { ok: false, error: String(error) };
97
273
  }
98
- await this.ensureTarget(sessionID);
99
- return this.daemon.request(name.replace(/_/g, "."), { ...arguments_, target: this.target(sessionID) });
100
274
  }
101
- async close() {
102
- clearInterval(this.timer);
103
- this.timer = void 0;
104
- for (const pending of this.listeners.values()) await pending.then((listener) => listener.close(), () => void 0);
105
- this.listeners.clear();
106
- await this.daemon?.close();
107
- this.daemon = void 0;
275
+ close() {
276
+ if (this.closing) return this.closing;
277
+ this.closed = true;
278
+ this.closing = this.started ? this.hub.remove(this) : Promise.resolve();
279
+ return this.closing;
108
280
  }
109
281
  async refresh() {
110
- if (!this.daemon) return this.ensureDaemon();
111
- await this.resubscribe();
282
+ if (!this.closed) await this.hub.refresh();
112
283
  }
113
- async ensureDaemon() {
114
- if (this.daemon) return;
115
- if (Date.now() - this.lastAttemptAt < 15e3) return;
116
- this.lastAttemptAt = Date.now();
117
- try {
118
- this.daemon = await connectDaemon();
119
- void this.resubscribe();
120
- } catch (error) {
121
- this.lastError = error instanceof Error ? error.message : String(error);
122
- console.error(`[sourcefed] daemon unavailable: ${this.lastError}`);
123
- }
124
- }
125
- /**
126
- * Monitors outlive this process, so after a restart nothing listens for their
127
- * sessions until something subscribes. Subscribe every session that has an
128
- * enabled monitor and still exists here; a deleted session is skipped, since
129
- * delivering to it would fail and retry forever.
130
- */
131
- async resubscribe() {
132
- try {
133
- const { targets } = await this.daemon.request("monitor.targets", { kind: TARGET_KIND });
134
- await Promise.all(targets.map((target) => this.resubscribeTarget(target.id)));
135
- } catch (error) {
136
- console.error(`[sourcefed] resubscribe failed: ${error instanceof Error ? error.message : String(error)}`);
137
- }
138
- }
139
- async resubscribeTarget(sessionID) {
140
- if (this.listeners.has(sessionID)) return;
141
- const exists = await this.session.get({ sessionID }).then(() => true, () => false);
142
- if (!exists) return;
143
- await this.ensureTarget(sessionID).catch((error) => {
144
- console.error(`[sourcefed] subscribe ${sessionID} failed: ${error instanceof Error ? error.message : String(error)}`);
145
- });
284
+ getSession(sessionID, signal) {
285
+ return this.session.get({ sessionID }, { signal: AbortSignal.any([signal, AbortSignal.timeout(3e3)]) });
146
286
  }
147
287
  // Every event arrives as a synthetic message, never as a user prompt, so it is
148
288
  // not mistaken for the user's own words. Actionable events resume an idle
@@ -182,7 +322,7 @@ var OpenCodeBridge = class {
182
322
  },
183
323
  { signal: AbortSignal.timeout(GATE_TIMEOUT_MS) }
184
324
  );
185
- return reply.deliver !== false;
325
+ return !(reply && typeof reply === "object" && "deliver" in reply && reply.deliver === false);
186
326
  } catch {
187
327
  return true;
188
328
  }
@@ -207,22 +347,15 @@ function cliEntry() {
207
347
  if (existsSync(bundled)) return bundled;
208
348
  return fileURLToPath(import.meta.resolve("@sourcefed/cli"));
209
349
  }
210
- function setOpenCodeBridge(bridge) {
211
- activeBridge = bridge;
212
- }
213
- function getOpenCodeBridge() {
214
- if (!activeBridge) throw new Error("Sourcefed OpenCode bridge is not initialized");
215
- return activeBridge;
216
- }
217
350
 
218
351
  // adapters/opencode/tool-result.ts
219
- async function callMonitorTool(name, arguments_, sessionID) {
220
- const result = await getOpenCodeBridge().callTool(name, arguments_, sessionID);
352
+ async function callMonitorTool(bridge, name, arguments_, sessionID) {
353
+ const result = await bridge.callTool(name, arguments_, sessionID);
221
354
  return JSON.stringify(result);
222
355
  }
223
356
 
224
357
  // adapters/opencode/tools/monitor_create.ts
225
- var definition = {
358
+ var monitor_create_default = (bridge) => ({
226
359
  name: "monitor_create",
227
360
  description: "Create a detect-only monitor that watches a Jira issue, GitHub PR, Slack thread, or Slack DM and routes NEW events into the current session. Slack monitors only read and notify; they never reply. GitHub and Slack use webhooks when configured and polling otherwise; Jira uses polling.",
228
361
  input: {
@@ -242,13 +375,12 @@ var definition = {
242
375
  additionalProperties: false
243
376
  },
244
377
  async execute(input, context) {
245
- return { content: await callMonitorTool("monitor_create", input ?? {}, context.sessionID) };
378
+ return { content: await callMonitorTool(bridge, "monitor_create", input ?? {}, context.sessionID) };
246
379
  }
247
- };
248
- var monitor_create_default = definition;
380
+ });
249
381
 
250
382
  // adapters/opencode/tools/monitor_list.ts
251
- var definition2 = {
383
+ var monitor_list_default = (bridge) => ({
252
384
  name: "monitor_list",
253
385
  description: "List monitors created by the current session (id, name, source, delivery, enabled).",
254
386
  input: {
@@ -257,13 +389,12 @@ var definition2 = {
257
389
  additionalProperties: false
258
390
  },
259
391
  async execute(_input, context) {
260
- return { content: await callMonitorTool("monitor_list", {}, context.sessionID) };
392
+ return { content: await callMonitorTool(bridge, "monitor_list", {}, context.sessionID) };
261
393
  }
262
- };
263
- var monitor_list_default = definition2;
394
+ });
264
395
 
265
396
  // adapters/opencode/tools/monitor_status.ts
266
- var definition3 = {
397
+ var monitor_status_default = (bridge) => ({
267
398
  name: "monitor_status",
268
399
  description: "Get the status of a monitor created by the current session.",
269
400
  input: {
@@ -276,13 +407,12 @@ var definition3 = {
276
407
  },
277
408
  async execute(input, context) {
278
409
  const args = input ?? {};
279
- return { content: await callMonitorTool("monitor_status", { id: args.id }, context.sessionID) };
410
+ return { content: await callMonitorTool(bridge, "monitor_status", { id: args.id }, context.sessionID) };
280
411
  }
281
- };
282
- var monitor_status_default = definition3;
412
+ });
283
413
 
284
414
  // adapters/opencode/tools/monitor_start.ts
285
- var definition4 = {
415
+ var monitor_start_default = (bridge) => ({
286
416
  name: "monitor_start",
287
417
  description: "Start (re-enable) a stopped monitor created by the current session.",
288
418
  input: {
@@ -295,13 +425,12 @@ var definition4 = {
295
425
  },
296
426
  async execute(input, context) {
297
427
  const args = input ?? {};
298
- return { content: await callMonitorTool("monitor_start", { id: args.id }, context.sessionID) };
428
+ return { content: await callMonitorTool(bridge, "monitor_start", { id: args.id }, context.sessionID) };
299
429
  }
300
- };
301
- var monitor_start_default = definition4;
430
+ });
302
431
 
303
432
  // adapters/opencode/tools/monitor_stop.ts
304
- var definition5 = {
433
+ var monitor_stop_default = (bridge) => ({
305
434
  name: "monitor_stop",
306
435
  description: "Stop a monitor created by the current session.",
307
436
  input: {
@@ -314,23 +443,26 @@ var definition5 = {
314
443
  },
315
444
  async execute(input, context) {
316
445
  const args = input ?? {};
317
- return { content: await callMonitorTool("monitor_stop", { id: args.id }, context.sessionID) };
446
+ return { content: await callMonitorTool(bridge, "monitor_stop", { id: args.id }, context.sessionID) };
318
447
  }
319
- };
320
- var monitor_stop_default = definition5;
448
+ });
321
449
 
322
450
  // adapters/opencode/index.ts
323
451
  async function setupSourcefed(ctx) {
324
- const bridge = new OpenCodeBridge(ctx.session, ctx.rpc);
325
- setOpenCodeBridge(bridge);
326
- await bridge.start();
327
- await ctx.tool.transform((editor) => {
328
- editor.add(monitor_create_default);
329
- editor.add(monitor_list_default);
330
- editor.add(monitor_status_default);
331
- editor.add(monitor_start_default);
332
- editor.add(monitor_stop_default);
333
- });
452
+ const bridge = new OpenCodeBridge(ctx.session, ctx.rpc, ctx.location);
453
+ try {
454
+ await bridge.start();
455
+ await ctx.tool.transform((editor) => {
456
+ editor.add(monitor_create_default(bridge));
457
+ editor.add(monitor_list_default(bridge));
458
+ editor.add(monitor_status_default(bridge));
459
+ editor.add(monitor_start_default(bridge));
460
+ editor.add(monitor_stop_default(bridge));
461
+ });
462
+ } catch (error) {
463
+ await bridge.close();
464
+ throw error;
465
+ }
334
466
  return () => bridge.close();
335
467
  }
336
468
  var opencode_default = Plugin.define({
@@ -345,11 +477,16 @@ var server_default = Plugin2.define({
345
477
  id: "sourcefed",
346
478
  async setup(ctx) {
347
479
  const cleanup = await setupSourcefed(ctx);
348
- await ctx.session.hook("context", (event) => {
349
- if (!event.system.some((part) => part.type === "text" && part.text.includes(heading))) {
350
- event.system.push({ type: "text", text: guidance });
351
- }
352
- });
480
+ try {
481
+ await ctx.session.hook("context", (event) => {
482
+ if (!event.system.some((part) => part.type === "text" && part.text.includes(heading))) {
483
+ event.system.push({ type: "text", text: guidance });
484
+ }
485
+ });
486
+ } catch (error) {
487
+ await cleanup?.();
488
+ throw error;
489
+ }
353
490
  return cleanup;
354
491
  }
355
492
  });
package/dist/pi.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  daemonCommand,
7
7
  daemonEnvironment,
8
8
  spawnLocalDaemon
9
- } from "./chunk-X3BZSHKZ.js";
9
+ } from "./chunk-E6J3BAXP.js";
10
10
  import "./chunk-MLKGABMK.js";
11
11
 
12
12
  // adapters/pi/src/index.ts
@@ -1,34 +1,80 @@
1
+ import { type QueuedMonitorEvent } from "@fdcn/sourcefed/core";
1
2
  import { type Plugin } from "@opencode/plugin";
3
+ import { OpenCodeSubscriptions, type SessionLocation } from "./subscriptions.js";
2
4
  type SessionDomain = Plugin.Context["session"];
3
- type RpcDomain = Plugin.Context["rpc"];
5
+ type RpcDomain = (definition: typeof ChauffeurGate) => {
6
+ gate(input: unknown, options: {
7
+ signal: AbortSignal;
8
+ }): Promise<unknown>;
9
+ };
10
+ /**
11
+ * Chauffeur's gate, declared here so sourcefed does not depend on Chauffeur.
12
+ * When the Chauffeur plugin runs in this OpenCode process, it decides which
13
+ * events reach the session; otherwise every event is delivered.
14
+ */
15
+ declare const ChauffeurGate: {
16
+ readonly id: "chauffeur";
17
+ readonly methods: {
18
+ readonly gate: {
19
+ readonly input: {
20
+ readonly type: "object";
21
+ readonly properties: {
22
+ readonly sessionID: {
23
+ readonly type: "string";
24
+ };
25
+ readonly monitorID: {
26
+ readonly type: "string";
27
+ };
28
+ readonly source: {
29
+ readonly type: "string";
30
+ };
31
+ readonly kind: {
32
+ readonly type: "string";
33
+ };
34
+ readonly summary: {
35
+ readonly type: "string";
36
+ };
37
+ readonly body: {
38
+ readonly type: "string";
39
+ };
40
+ readonly actionable: {
41
+ readonly type: "boolean";
42
+ };
43
+ };
44
+ readonly required: readonly ["sessionID", "source", "kind", "summary", "actionable"];
45
+ };
46
+ readonly output: {
47
+ readonly type: "object";
48
+ readonly properties: {
49
+ readonly deliver: {
50
+ readonly type: "boolean";
51
+ };
52
+ };
53
+ readonly required: readonly ["deliver"];
54
+ };
55
+ };
56
+ };
57
+ readonly events: {};
58
+ };
4
59
  export declare class OpenCodeBridge {
5
60
  private readonly session;
6
- private readonly rpc?;
7
- private daemon?;
8
- private readonly listeners;
9
- private lastAttemptAt;
10
- private lastError;
11
- private timer;
12
- constructor(session: SessionDomain, rpc?: RpcDomain | undefined);
61
+ private readonly rpc;
62
+ readonly location: SessionLocation;
63
+ private readonly hub;
64
+ private started;
65
+ private closed;
66
+ private closing;
67
+ constructor(session: Pick<SessionDomain, "get" | "synthetic">, rpc: RpcDomain | undefined, location: SessionLocation, hub?: OpenCodeSubscriptions);
13
68
  /**
14
69
  * Chauffeur or the CLI can create a monitor for a session here at any time,
15
70
  * without this bridge's tools, so check for new targets periodically too.
16
71
  */
17
72
  start(): Promise<void>;
18
- ensureTarget(sessionID: string): Promise<void>;
19
73
  callTool(name: string, arguments_: Record<string, unknown>, sessionID: string): Promise<unknown>;
20
74
  close(): Promise<void>;
21
- private refresh;
22
- private ensureDaemon;
23
- /**
24
- * Monitors outlive this process, so after a restart nothing listens for their
25
- * sessions until something subscribes. Subscribe every session that has an
26
- * enabled monitor and still exists here; a deleted session is skipped, since
27
- * delivering to it would fail and retry forever.
28
- */
29
- private resubscribe;
30
- private resubscribeTarget;
31
- private routeEvents;
75
+ refresh(): Promise<void>;
76
+ getSession(sessionID: string, signal: AbortSignal): Promise<import("@opencode/client").SessionInfo>;
77
+ routeEvents(events: QueuedMonitorEvent[]): Promise<void>;
32
78
  /**
33
79
  * Ask Chauffeur's gate, when it runs in this process. Only an explicit
34
80
  * `deliver: false` withholds the event; no gate, an error, or a slow answer
@@ -37,6 +83,4 @@ export declare class OpenCodeBridge {
37
83
  private admitted;
38
84
  private target;
39
85
  }
40
- export declare function setOpenCodeBridge(bridge: OpenCodeBridge): void;
41
- export declare function getOpenCodeBridge(): OpenCodeBridge;
42
86
  export {};
@@ -0,0 +1,46 @@
1
+ import type { QueuedMonitorEvent } from "@fdcn/sourcefed/core";
2
+ import type { DaemonClient } from "@fdcn/sourcefed/daemon";
3
+ export type SessionLocation = {
4
+ directory: string;
5
+ project: {
6
+ id: string;
7
+ };
8
+ };
9
+ type SessionInfo = {
10
+ projectID: string;
11
+ location: {
12
+ directory: string;
13
+ };
14
+ };
15
+ export interface SubscriptionOwner {
16
+ readonly location: SessionLocation;
17
+ getSession(sessionID: string, signal: AbortSignal): Promise<SessionInfo>;
18
+ routeEvents(events: QueuedMonitorEvent[]): Promise<void>;
19
+ }
20
+ /** One client, timer and stream per enabled session, shared by all plugin locations. */
21
+ export declare class OpenCodeSubscriptions {
22
+ private readonly connect;
23
+ private readonly owners;
24
+ private readonly subscriptions;
25
+ private readonly lifetime;
26
+ private daemon;
27
+ private connecting;
28
+ private refreshing;
29
+ private dirty;
30
+ private lastAttemptAt;
31
+ private timer;
32
+ constructor(connect: () => Promise<DaemonClient>);
33
+ get closed(): boolean;
34
+ add(owner: SubscriptionOwner): Promise<void>;
35
+ remove(owner: SubscriptionOwner): Promise<void>;
36
+ request(method: string, params: Record<string, unknown>): Promise<unknown>;
37
+ refresh(): Promise<void>;
38
+ private refreshLoop;
39
+ private ensureDaemon;
40
+ private reconcile;
41
+ private ownerFor;
42
+ private subscribe;
43
+ private route;
44
+ private closeSubscription;
45
+ }
46
+ export {};
@@ -1 +1,2 @@
1
- export declare function callMonitorTool(name: string, arguments_: Record<string, unknown>, sessionID: string): Promise<string>;
1
+ import type { OpenCodeBridge } from "./bridge.js";
2
+ export declare function callMonitorTool(bridge: OpenCodeBridge, name: string, arguments_: Record<string, unknown>, sessionID: string): Promise<string>;
@@ -1,5 +1,6 @@
1
1
  import type { ToolContext } from "@opencode/plugin/promise/tool";
2
- declare const definition: {
2
+ import type { OpenCodeBridge } from "../bridge.js";
3
+ declare const _default: (bridge: OpenCodeBridge) => {
3
4
  name: string;
4
5
  description: string;
5
6
  input: {
@@ -50,4 +51,4 @@ declare const definition: {
50
51
  content: string;
51
52
  }>;
52
53
  };
53
- export default definition;
54
+ export default _default;