@fdcn/sourcefed 0.3.6 → 0.3.8

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 (52) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-X3BZSHKZ.js → chunk-5JL3UV72.js} +1328 -399
  3. package/dist/cli.js +9888 -8063
  4. package/dist/daemon.js +1194 -263
  5. package/dist/data/core/SKILL.md +9 -4
  6. package/dist/data/core/references/commands.md +6 -1
  7. package/dist/data/sourcefed/SKILL.md +3 -2
  8. package/dist/guidance.md +6 -2
  9. package/dist/mcp.js +6 -2
  10. package/dist/opencode.js +263 -122
  11. package/dist/pi.js +161 -39
  12. package/dist/tui-compiled.mjs +306 -166
  13. package/dist.dts/adapters/opencode/bridge.d.ts +66 -22
  14. package/dist.dts/adapters/opencode/subscriptions.d.ts +46 -0
  15. package/dist.dts/adapters/opencode/tool-result.d.ts +2 -1
  16. package/dist.dts/adapters/opencode/tools/monitor_create.d.ts +19 -2
  17. package/dist.dts/adapters/opencode/tools/monitor_list.d.ts +3 -2
  18. package/dist.dts/adapters/opencode/tools/monitor_start.d.ts +3 -2
  19. package/dist.dts/adapters/opencode/tools/monitor_status.d.ts +3 -2
  20. package/dist.dts/adapters/opencode/tools/monitor_stop.d.ts +3 -2
  21. package/dist.dts/adapters/pi/src/dialog.d.ts +1 -2
  22. package/dist.dts/adapters/pi/src/monitors-dialog.d.ts +32 -0
  23. package/dist.dts/packages/core/src/types/monitor-create-input.d.ts +4 -0
  24. package/dist.dts/packages/daemon/src/event-stream.d.ts +26 -0
  25. package/dist.dts/packages/daemon/src/registry.d.ts +2 -0
  26. package/dist.dts/packages/daemon/src/stream-lifecycle.test.d.ts +1 -0
  27. package/dist.dts/packages/daemon/src/types/client.d.ts +3 -1
  28. package/dist.dts/packages/daemon/src/types/daemon.d.ts +4 -0
  29. package/dist.dts/packages/daemon/src/utils/index.d.ts +0 -1
  30. package/dist.dts/providers/yab/src/index.d.ts +2 -0
  31. package/dist.dts/providers/yab/src/monitor.d.ts +14 -0
  32. package/dist.dts/providers/yab/src/poll/index.d.ts +2 -0
  33. package/dist.dts/providers/yab/src/poll/monitor.d.ts +3 -0
  34. package/dist.dts/providers/yab/src/poll/utils/feed-events.d.ts +5 -0
  35. package/dist.dts/providers/yab/src/poll/utils/index.d.ts +4 -0
  36. package/dist.dts/providers/yab/src/poll/utils/parse-feed.d.ts +3 -0
  37. package/dist.dts/providers/yab/src/poll/utils/poll-yab.d.ts +7 -0
  38. package/dist.dts/providers/yab/src/poll/utils/yab-mcp.d.ts +10 -0
  39. package/dist.dts/providers/yab/src/schema.d.ts +12 -0
  40. package/dist.dts/providers/yab/src/types/cursor.d.ts +4 -0
  41. package/dist.dts/providers/yab/src/types/event.d.ts +8 -0
  42. package/dist.dts/providers/yab/src/types/feed-entry.d.ts +8 -0
  43. package/dist.dts/providers/yab/src/types/index.d.ts +5 -0
  44. package/dist.dts/providers/yab/src/types/page.d.ts +6 -0
  45. package/dist.dts/providers/yab/src/types/source-record.d.ts +7 -0
  46. package/dist.dts/providers/yab/src/yab.test.d.ts +1 -0
  47. package/package.json +1 -1
  48. package/dist.dts/packages/daemon/src/utils/forward.d.ts +0 -32
  49. package/dist.dts/packages/daemon/src/utils/gate.d.ts +0 -33
  50. package/dist.dts/packages/daemon/src/utils/request-router.d.ts +0 -11
  51. /package/dist.dts/{packages/daemon/src/forward.test.d.ts → adapters/opencode/lifecycle.test.d.ts} +0 -0
  52. /package/dist.dts/{packages/daemon/src/gate.test.d.ts → adapters/pi/src/monitors-dialog.test.d.ts} +0 -0
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: core
3
- description: Core sourcefed usage guide. Read this before creating monitors. Covers monitor creation per source (Jira issue, GitHub PR, Slack thread), event delivery and acknowledgement semantics, daemon modes, CLI usage, and troubleshooting. Use when the user asks to watch or monitor a Jira ticket, GitHub PR, Slack thread, or when sourcefed monitor events arrive in the session.
3
+ description: Core sourcefed usage guide. Read this before creating monitors. Covers monitor creation per source (Jira issue, GitHub PR, Slack thread, Yab feed), event delivery and acknowledgement semantics, daemon modes, CLI usage, and troubleshooting. Use when the user asks to watch or monitor a Jira ticket, GitHub PR, Slack thread, a feed or inbox they are signed in to in Yab, or when sourcefed monitor events arrive in the session.
4
4
  ---
5
5
 
6
6
  # Sourcefed core
7
7
 
8
- Sourcefed lets you create **monitors** that watch a Jira issue, GitHub PR, or
9
- Slack thread or direct-message conversation and route NEW events into the session you're currently working in,
8
+ Sourcefed lets you create **monitors** that watch a Jira issue, GitHub PR,
9
+ Slack thread or direct-message conversation, or a feed read through Yab, and route NEW events into the session you're currently working in,
10
10
  so you can react to them as they happen.
11
11
 
12
12
  Every monitor is detect-only: it routes events into the session and never
@@ -21,6 +21,7 @@ affect that session's monitors.
21
21
  - **GitHub PR**: `monitor_create({ name: "PR #42", sourceType: "github", repo: "owner/repo", prNumber: 42 })`
22
22
  - **Slack thread**: `monitor_create({ name: "thread", sourceType: "slack", threadUrl: "<thread-url>" })`
23
23
  - **Whole Slack DM**: `monitor_create({ name: "DM with Alex", sourceType: "slack", channelId: "D0123456789" })`
24
+ - **Yab feed**: `monitor_create({ name: "inbox", sourceType: "yab", url: "https://mail.google.com/mail/u/0/feed/atom", space: "Personal", match: "interview|offer", exclude: "newsletter" })`
24
25
 
25
26
  You will be notified of:
26
27
 
@@ -30,9 +31,13 @@ You will be notified of:
30
31
  - **Slack thread**: new replies in the thread.
31
32
  - **Slack DM**: new top-level messages in the DM. Slack monitors are always detect-only
32
33
  and never reply.
34
+ - **Yab feed**: each new entry of an Atom, RSS or JSON feed, read through the user's Yab
35
+ browser with their sign-ins (so private feeds like Gmail's unread-inbox feed work).
36
+ `space` picks the Yab space; `match`/`exclude` are case-insensitive regexes over the
37
+ entry's title, summary and author. Yab must be running.
33
38
 
34
39
  GitHub and Slack use webhook delivery when configured and fall back to polling
35
- when unavailable. Jira always uses polling. You can lower `pollIntervalSec`
40
+ when unavailable. Jira and Yab always use polling. You can lower `pollIntervalSec`
36
41
  (min 15) for time-sensitive work, but prefer the default to avoid hammering
37
42
  APIs.
38
43
 
@@ -26,13 +26,17 @@ sourcefed skills path [name] Print skill directory path
26
26
 
27
27
  ```text
28
28
  --name NAME Human-readable monitor name (defaults to the source type)
29
- --source-type TYPE jira | github | slack
29
+ --source-type TYPE jira | github | slack | yab
30
30
  --issue-key KEY Jira issue key (jira)
31
31
  --repo OWNER/NAME GitHub repository (github)
32
32
  --pr-number N GitHub PR number (github)
33
33
  --channel-id ID Slack channel or DM ID; a D… ID alone monitors the whole DM
34
34
  --thread-ts TS Slack parent message timestamp; omit for a whole-DM monitor
35
35
  --thread-url URL Slack thread URL (slack)
36
+ --url URL Atom, RSS or JSON feed URL read through Yab (yab)
37
+ --space NAME Yab space whose sign-ins to use (yab)
38
+ --match REGEX Only deliver entries matching this case-insensitive regex (yab)
39
+ --exclude REGEX Skip entries matching this case-insensitive regex (yab)
36
40
  --poll-interval-sec N Poll interval in seconds (min 15)
37
41
  --target-kind KIND Target kind for ownership scoping (default: cli)
38
42
  --target-id ID Target id for ownership scoping (default: hostname)
@@ -54,6 +58,7 @@ SOURCEFED_JIRA_TERMINAL_STATUS Jira status that auto-removes monitors
54
58
  GH_TOKEN / GITHUB_TOKEN GitHub token for direct API polling
55
59
  SOURCEFED_GITHUB_WEBHOOK_SECRET GitHub webhook secret (enables webhooks)
56
60
  SOURCEFED_SLACK_SIGNING_SECRET Slack signing secret (enables webhooks)
61
+ SOURCEFED_YAB_COMMAND Yab binary for yab monitors (default /Applications/yab.app/Contents/MacOS/yab)
57
62
  SOURCEFED_SKILLS_DIR Override the bundled skills directory
58
63
  ```
59
64
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: sourcefed
3
- description: Live Jira, GitHub, and Slack monitoring for agent hosts. Create monitors that route new issue comments, PR reviews, CI failures, and Slack thread or DM messages into the current session as they happen.
3
+ description: Live Jira, GitHub, Slack, and Yab feed monitoring for agent hosts. Create monitors that route new issue comments, PR reviews, CI failures, Slack thread or DM messages, and new entries of feeds read through Yab into the current session as they happen.
4
4
  hidden: true
5
5
  ---
6
6
 
7
7
  # Sourcefed
8
8
 
9
9
  Sourcefed turns external collaboration threads into live agent input: create a
10
- monitor for a Jira issue, GitHub PR, Slack thread, or Slack DM and NEW events are routed
10
+ monitor for a Jira issue, GitHub PR, Slack thread, Slack DM, or a feed read through Yab and NEW events are routed
11
11
  into the session you are working in, so you can react to them as they happen.
12
12
 
13
13
  This file is a discovery stub, not the usage guide. Before running any
@@ -25,6 +25,7 @@ releases, which is why it just points at `skills get core`.
25
25
  ## When to load this skill
26
26
 
27
27
  - A user asks to monitor a Jira ticket, GitHub PR, Slack thread, or Slack DM.
28
+ - A user wants to hear about new entries in a feed or inbox they are signed in to in Yab.
28
29
  - You found or are working on a Jira ticket, PR, thread, or DM and want new
29
30
  activity routed into your session.
30
31
  - You opened a PR and want to be notified of reviews, comments, CI failures,
package/dist/guidance.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # sourcefed monitors
2
2
 
3
- Monitors watch a Jira issue, GitHub PR, or Slack thread and bring new events into this session.
3
+ Monitors watch a Jira issue, GitHub PR, Slack thread, or a feed read through Yab and bring new events
4
+ into this session.
4
5
 
5
6
  Create one with `monitor_create` when:
6
7
 
@@ -9,7 +10,10 @@ Create one with `monitor_create` when:
9
10
  - your branch has a PR: `{ name: "<pr>", sourceType: "github", repo: "owner/name", prNumber: 42 }`
10
11
  (reviews, comments, CI failures, conflicts, merge or close);
11
12
  - you were given a Slack thread: `{ name: "<thread>", sourceType: "slack", threadUrl: "<url>" }`
12
- (new messages; read-only, never replies).
13
+ (new messages; read-only, never replies);
14
+ - the user wants new entries of a feed they are signed in to in Yab (unread Gmail, a job board's RSS):
15
+ `{ name: "<feed>", sourceType: "yab", url: "https://mail.google.com/mail/u/0/feed/atom", space: "Personal", match: "interview|offer" }`
16
+ (each new entry; `match`/`exclude` are case-insensitive regexes; Yab must be running).
13
17
 
14
18
  Keep the default `pollIntervalSec` unless the work is time-sensitive (min 15).
15
19
  `monitor_list`, `monitor_status { id }` and `monitor_stop { id }` act on this session's monitors.
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  decodeTarget,
3
3
  encodeTarget
4
- } from "./chunk-X3BZSHKZ.js";
4
+ } from "./chunk-5JL3UV72.js";
5
5
  import {
6
6
  __export
7
7
  } from "./chunk-MLKGABMK.js";
@@ -4680,6 +4680,10 @@ function buildServer(daemon) {
4680
4680
  channelId: string2().optional(),
4681
4681
  threadTs: string2().optional(),
4682
4682
  threadUrl: string2().optional(),
4683
+ url: string2().optional(),
4684
+ space: string2().optional(),
4685
+ match: string2().optional(),
4686
+ exclude: string2().optional(),
4683
4687
  pollIntervalSec: number2().min(15).optional(),
4684
4688
  target: targetSchema
4685
4689
  });
@@ -4693,7 +4697,7 @@ function buildServer(daemon) {
4693
4697
  server.registerTool(
4694
4698
  "monitor_create",
4695
4699
  {
4696
- description: "Create or reuse a monitor for a Jira issue, GitHub pull request, Slack thread, or Slack DM.",
4700
+ description: "Create or reuse a monitor for a Jira issue, GitHub pull request, Slack thread, Slack DM, or a feed read through Yab.",
4697
4701
  inputSchema: createSchema,
4698
4702
  annotations: { idempotentHint: true, readOnlyHint: false }
4699
4703
  },
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-5JL3UV72.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,24 +347,17 @@ 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
- 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.",
360
+ description: "Create a detect-only monitor that watches a Jira issue, GitHub PR, Slack thread, Slack DM, or a feed read through Yab (the user's browser, with their sign-ins) 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 and Yab use polling.",
228
361
  input: {
229
362
  type: "object",
230
363
  properties: {
@@ -236,19 +369,22 @@ var definition = {
236
369
  channelId: { type: "string", description: "Slack channel or DM ID. A DM ID without threadTs monitors the whole DM conversation." },
237
370
  threadTs: { type: "string", description: "Slack parent message timestamp. Omit it with a DM ID to monitor the whole DM conversation." },
238
371
  threadUrl: { type: "string", description: "Slack thread URL (alternative to channelId + threadTs)" },
372
+ url: { type: "string", description: "Atom, RSS or JSON feed URL read through Yab with the user's sign-ins, e.g. https://mail.google.com/mail/u/0/feed/atom (required if sourceType=yab)" },
373
+ space: { type: "string", description: "Yab space whose sign-ins to use (yab); omit to use wherever the site is signed in" },
374
+ match: { type: "string", description: "Case-insensitive regex; only entries whose title, summary or author match are delivered (yab)" },
375
+ exclude: { type: "string", description: "Case-insensitive regex; entries that match are skipped (yab)" },
239
376
  pollIntervalSec: { type: "number", description: "Optional polling interval in seconds (min 15, default 60)" }
240
377
  },
241
378
  required: ["name", "sourceType"],
242
379
  additionalProperties: false
243
380
  },
244
381
  async execute(input, context) {
245
- return { content: await callMonitorTool("monitor_create", input ?? {}, context.sessionID) };
382
+ return { content: await callMonitorTool(bridge, "monitor_create", input ?? {}, context.sessionID) };
246
383
  }
247
- };
248
- var monitor_create_default = definition;
384
+ });
249
385
 
250
386
  // adapters/opencode/tools/monitor_list.ts
251
- var definition2 = {
387
+ var monitor_list_default = (bridge) => ({
252
388
  name: "monitor_list",
253
389
  description: "List monitors created by the current session (id, name, source, delivery, enabled).",
254
390
  input: {
@@ -257,13 +393,12 @@ var definition2 = {
257
393
  additionalProperties: false
258
394
  },
259
395
  async execute(_input, context) {
260
- return { content: await callMonitorTool("monitor_list", {}, context.sessionID) };
396
+ return { content: await callMonitorTool(bridge, "monitor_list", {}, context.sessionID) };
261
397
  }
262
- };
263
- var monitor_list_default = definition2;
398
+ });
264
399
 
265
400
  // adapters/opencode/tools/monitor_status.ts
266
- var definition3 = {
401
+ var monitor_status_default = (bridge) => ({
267
402
  name: "monitor_status",
268
403
  description: "Get the status of a monitor created by the current session.",
269
404
  input: {
@@ -276,13 +411,12 @@ var definition3 = {
276
411
  },
277
412
  async execute(input, context) {
278
413
  const args = input ?? {};
279
- return { content: await callMonitorTool("monitor_status", { id: args.id }, context.sessionID) };
414
+ return { content: await callMonitorTool(bridge, "monitor_status", { id: args.id }, context.sessionID) };
280
415
  }
281
- };
282
- var monitor_status_default = definition3;
416
+ });
283
417
 
284
418
  // adapters/opencode/tools/monitor_start.ts
285
- var definition4 = {
419
+ var monitor_start_default = (bridge) => ({
286
420
  name: "monitor_start",
287
421
  description: "Start (re-enable) a stopped monitor created by the current session.",
288
422
  input: {
@@ -295,13 +429,12 @@ var definition4 = {
295
429
  },
296
430
  async execute(input, context) {
297
431
  const args = input ?? {};
298
- return { content: await callMonitorTool("monitor_start", { id: args.id }, context.sessionID) };
432
+ return { content: await callMonitorTool(bridge, "monitor_start", { id: args.id }, context.sessionID) };
299
433
  }
300
- };
301
- var monitor_start_default = definition4;
434
+ });
302
435
 
303
436
  // adapters/opencode/tools/monitor_stop.ts
304
- var definition5 = {
437
+ var monitor_stop_default = (bridge) => ({
305
438
  name: "monitor_stop",
306
439
  description: "Stop a monitor created by the current session.",
307
440
  input: {
@@ -314,23 +447,26 @@ var definition5 = {
314
447
  },
315
448
  async execute(input, context) {
316
449
  const args = input ?? {};
317
- return { content: await callMonitorTool("monitor_stop", { id: args.id }, context.sessionID) };
450
+ return { content: await callMonitorTool(bridge, "monitor_stop", { id: args.id }, context.sessionID) };
318
451
  }
319
- };
320
- var monitor_stop_default = definition5;
452
+ });
321
453
 
322
454
  // adapters/opencode/index.ts
323
455
  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
- });
456
+ const bridge = new OpenCodeBridge(ctx.session, ctx.rpc, ctx.location);
457
+ try {
458
+ await bridge.start();
459
+ await ctx.tool.transform((editor) => {
460
+ editor.add(monitor_create_default(bridge));
461
+ editor.add(monitor_list_default(bridge));
462
+ editor.add(monitor_status_default(bridge));
463
+ editor.add(monitor_start_default(bridge));
464
+ editor.add(monitor_stop_default(bridge));
465
+ });
466
+ } catch (error) {
467
+ await bridge.close();
468
+ throw error;
469
+ }
334
470
  return () => bridge.close();
335
471
  }
336
472
  var opencode_default = Plugin.define({
@@ -345,11 +481,16 @@ var server_default = Plugin2.define({
345
481
  id: "sourcefed",
346
482
  async setup(ctx) {
347
483
  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
- });
484
+ try {
485
+ await ctx.session.hook("context", (event) => {
486
+ if (!event.system.some((part) => part.type === "text" && part.text.includes(heading))) {
487
+ event.system.push({ type: "text", text: guidance });
488
+ }
489
+ });
490
+ } catch (error) {
491
+ await cleanup?.();
492
+ throw error;
493
+ }
353
494
  return cleanup;
354
495
  }
355
496
  });