@fdcn/sourcefed 0.3.2 → 0.3.5

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.
@@ -14,7 +14,7 @@ sourcefed monitor start --id ID [--target-kind KIND] [--target-id ID]
14
14
  sourcefed monitor remove --id ID --yes [--target-kind KIND] [--target-id ID]
15
15
  sourcefed monitor events [--target-kind KIND] [--target-id ID]
16
16
  sourcefed monitor follow [--target-kind KIND] [--target-id ID]
17
- sourcefed monitor ack --event-id ID[,ID...] [--target-kind KIND] [--target-id ID]
17
+ sourcefed monitor ack --event-id ID [--event-id ID...] [--target-kind KIND] [--target-id ID]
18
18
  sourcefed monitor sources
19
19
  sourcefed skills [list] List bundled skills
20
20
  sourcefed skills get <name> [--full] Output skill content (--full adds references)
package/dist/guidance.md CHANGED
@@ -1,71 +1,20 @@
1
- # sourcefed — monitoring guidance
1
+ # sourcefed monitors
2
2
 
3
- sourcefed is a monitor service. It lets you create **monitors** that watch a Jira issue, GitHub
4
- PR, or Slack thread and route NEW events into the host session where the monitor was created.
5
- The OpenCode and Pi adapters drive the Sourcefed daemon client (`@sourcefed/daemon`) for
6
- monitor tools and event delivery; the same daemon can be served through MCP (`@sourcefed/mcp`)
7
- or driven directly by the CLI.
3
+ Monitors watch a Jira issue, GitHub PR, or Slack thread and bring new events into this session.
8
4
 
9
- All transports speak the same daemon JSON protocol over HTTP (`POST /rpc` +
10
- `GET /events` SSE). The MCP adapter adds the 2026 resource-subscription flow
11
- (`subscriptions/listen`, `notifications/resources/updated`, `monitor_events_ack`)
12
- on top. When `SOURCEFED_DAEMON_URL` is unset, the OpenCode and Pi adapters
13
- spawn one local HTTP daemon automatically (if none is running) and connect to it.
5
+ Create one with `monitor_create` when:
14
6
 
15
- ## When to create a monitor
7
+ - you work on a Jira ticket: `{ name: "<key>", sourceType: "jira", issueKey: "PROJ-123" }`
8
+ (new comments, edits, status, assignee, priority, labels);
9
+ - your branch has a PR: `{ name: "<pr>", sourceType: "github", repo: "owner/name", prNumber: 42 }`
10
+ (reviews, comments, CI failures, conflicts, merge or close);
11
+ - you were given a Slack thread: `{ name: "<thread>", sourceType: "slack", threadUrl: "<url>" }`
12
+ (new messages; read-only, never replies).
16
13
 
17
- - **You found or are working on a Jira ticket** → create a Jira monitor for its issue key. You
18
- will be notified of new comments, description edits, and summary/status/assignee/priority/label changes.
19
- - **You are on a branch with an active PR (or just opened one)** → create a GitHub monitor for
20
- `repo#prNumber`. You will be notified of new reviews (including bot reviews), PR and
21
- line-level comments, CI failures, merge conflicts, and the PR being merged or closed.
22
- - **You were given a Slack thread** → create a Slack monitor for the thread URL. It is
23
- detect-only: it reads new messages and routes notifications into this session, but never replies.
14
+ Keep the default `pollIntervalSec` unless the work is time-sensitive (min 15).
15
+ `monitor_list`, `monitor_status { id }` and `monitor_stop { id }` act on this session's monitors.
16
+ GitHub monitors end when the PR merges.
24
17
 
25
- ## How to create one
26
-
27
- Use the `monitor_create` tool:
28
-
29
- - Jira: `monitor_create({ name: "<key>", sourceType: "jira", issueKey: "PROJ-12345" })`
30
- - GitHub: `monitor_create({ name: "<pr>", sourceType: "github", repo: "owner/name", prNumber: 42 })`
31
- - Slack: `monitor_create({ name: "<thread>", sourceType: "slack", threadUrl: "<slack-thread-url>" })`
32
-
33
- GitHub and Slack use webhook delivery when configured and fall back to polling when unavailable.
34
- Jira always uses polling. Slack monitors are
35
- always detect-only and never reply. You can lower
36
- `pollIntervalSec` (min 15) for time-sensitive work, but prefer the default to avoid hammering APIs.
37
- Actionable events trigger an agent turn. Stable all-passing CI is suppressed to avoid notification noise; other informational events are added as context only.
38
-
39
- The abstract `Monitor` consolidates transport fallback and recovery and composes whichever
40
- generic `PollMonitor` and `WebhookMonitor` transports a source exposes as class attributes.
41
- Each provider is its own package (`@sourcefed/provider-{jira,github,slack}`) with monitor and
42
- event modules; the built-in registry (`SOURCE_MAP`) is composed by the daemon, which imports
43
- the provider packages directly. Jira exposes only polling;
44
- GitHub and Slack expose both. Do not add source-specific transport branches to tools,
45
- scheduling, or the daemon.
46
-
47
- ## Skills
48
-
49
- The CLI bundles skills that teach agents how to use sourcefed, in the agent-browser style: a
50
- thin discovery stub points at CLI-served content that always matches the installed version.
51
-
52
- ```bash
53
- sourcefed skills # list available skills
54
- sourcefed skills get core # load the core usage guide (--full adds references)
55
- ```
56
-
57
- ## Lifecycle
58
-
59
- - Monitors belong to the target that created them; list, status, and stop only affect that target's monitors.
60
- - `monitor_list` — see monitors created by this session.
61
- - `monitor_status { id }` — detail on one of this session's monitors.
62
- - `monitor_stop { id }` — stop routing events for one of this session's monitors.
63
- - GitHub monitors remove themselves after the PR is merged. Jira monitors remove themselves when
64
- `SOURCEFED_JIRA_TERMINAL_STATUS` matches the issue status; leave it unset to disable automatic Jira removal.
65
-
66
- ## When an event arrives
67
-
68
- Events are fed into your session as a message (e.g. a new review comment, a CI failure, a Jira
69
- status change). Treat it like any new input: read it, decide whether it needs action, and act
70
- (or tell the user it needs a decision). If the event is non-actionable (e.g. CI all-passing),
71
- acknowledge it and move on — do not fabricate work from it.
18
+ An event arrives as a message. Act on it if it needs action, or tell the user it needs a
19
+ decision; acknowledge an informational one (such as CI passing) and move on. Never invent work
20
+ from it. `sourcefed skills get core` has the full guide.
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  decodeTarget,
3
3
  encodeTarget
4
- } from "./chunk-QPDTUNFA.js";
4
+ } from "./chunk-X3BZSHKZ.js";
5
5
  import {
6
6
  __export
7
7
  } from "./chunk-MLKGABMK.js";
package/dist/opencode.js CHANGED
@@ -1,10 +1,13 @@
1
+ import {
2
+ eventToText
3
+ } from "./chunk-BNLZQKEB.js";
1
4
  import {
2
5
  SOURCE_TYPES,
3
6
  connectDaemonClient,
4
7
  daemonCommand,
5
8
  daemonEnvironment,
6
9
  spawnLocalDaemon
7
- } from "./chunk-QPDTUNFA.js";
10
+ } from "./chunk-X3BZSHKZ.js";
8
11
  import "./chunk-MLKGABMK.js";
9
12
 
10
13
  // packages/sourcefed/src/server.ts
@@ -18,26 +21,74 @@ import { Plugin } from "@opencode/plugin";
18
21
  // adapters/opencode/bridge.ts
19
22
  import { existsSync } from "node:fs";
20
23
  import { fileURLToPath } from "node:url";
24
+ import { Rpc } from "@opencode/plugin";
25
+ var TARGET_KIND = "opencode-session";
26
+ var GATE_TIMEOUT_MS = 3e3;
27
+ var RESUBSCRIBE_MS = 15e3;
28
+ var ChauffeurGate = Rpc.define({
29
+ id: "chauffeur",
30
+ methods: {
31
+ gate: {
32
+ input: {
33
+ type: "object",
34
+ properties: {
35
+ sessionID: { type: "string" },
36
+ // The monitor that produced the event, so the gate knows what it watches.
37
+ monitorID: { type: "string" },
38
+ source: { type: "string" },
39
+ kind: { type: "string" },
40
+ summary: { type: "string" },
41
+ body: { type: "string" },
42
+ actionable: { type: "boolean" }
43
+ },
44
+ required: ["sessionID", "source", "kind", "summary", "actionable"]
45
+ },
46
+ output: {
47
+ type: "object",
48
+ properties: { deliver: { type: "boolean" } },
49
+ required: ["deliver"]
50
+ }
51
+ }
52
+ },
53
+ events: {}
54
+ });
21
55
  var activeBridge;
22
56
  var OpenCodeBridge = class {
23
- constructor(session) {
57
+ constructor(session, rpc) {
24
58
  this.session = session;
59
+ this.rpc = rpc;
25
60
  }
26
61
  daemon;
27
62
  listeners = /* @__PURE__ */ new Map();
28
63
  lastAttemptAt = 0;
29
64
  lastError;
65
+ timer;
66
+ /**
67
+ * Chauffeur or the CLI can create a monitor for a session here at any time,
68
+ * without this bridge's tools, so check for new targets periodically too.
69
+ */
30
70
  async start() {
31
71
  await this.ensureDaemon();
72
+ this.timer = setInterval(() => void this.refresh(), RESUBSCRIBE_MS);
73
+ this.timer.unref?.();
32
74
  }
33
75
  async ensureTarget(sessionID) {
34
76
  await this.ensureDaemon();
35
- if (this.listeners.has(sessionID)) return;
36
- const target = this.target(sessionID);
37
- const listener = await this.daemon.subscribe(target, async (events) => {
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) => {
38
83
  await this.routeEvents(events);
39
84
  });
40
- this.listeners.set(sessionID, listener);
85
+ this.listeners.set(sessionID, pending);
86
+ try {
87
+ await pending;
88
+ } catch (error) {
89
+ this.listeners.delete(sessionID);
90
+ throw error;
91
+ }
41
92
  }
42
93
  async callTool(name, arguments_, sessionID) {
43
94
  await this.ensureDaemon();
@@ -48,52 +99,109 @@ var OpenCodeBridge = class {
48
99
  return this.daemon.request(name.replace(/_/g, "."), { ...arguments_, target: this.target(sessionID) });
49
100
  }
50
101
  async close() {
51
- for (const listener of this.listeners.values()) await listener.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);
52
105
  this.listeners.clear();
53
106
  await this.daemon?.close();
54
107
  this.daemon = void 0;
55
108
  }
109
+ async refresh() {
110
+ if (!this.daemon) return this.ensureDaemon();
111
+ await this.resubscribe();
112
+ }
56
113
  async ensureDaemon() {
57
114
  if (this.daemon) return;
58
115
  if (Date.now() - this.lastAttemptAt < 15e3) return;
59
116
  this.lastAttemptAt = Date.now();
60
117
  try {
61
- const url = process.env.SOURCEFED_DAEMON_URL;
62
- if (url) {
63
- this.daemon = await connectDaemonClient({ name: "sourcefed-opencode", url });
64
- return;
65
- }
66
- const local = daemonCommand(cliEntry());
67
- const spawned = await spawnLocalDaemon({
68
- command: local.command,
69
- args: local.args,
70
- env: daemonEnvironment()
71
- });
72
- this.daemon = await connectDaemonClient({ name: "sourcefed-opencode", url: spawned.url });
118
+ this.daemon = await connectDaemon();
119
+ void this.resubscribe();
73
120
  } catch (error) {
74
121
  this.lastError = error instanceof Error ? error.message : String(error);
75
122
  console.error(`[sourcefed] daemon unavailable: ${this.lastError}`);
76
123
  }
77
124
  }
78
- // V2 `session.prompt` has no model field — the model is a session property and
79
- // prompting does not reset it, so no model-reuse dance is needed. Actionable
80
- // events prompt the agent; non-actionable ones inject a synthetic message that
81
- // lands in the transcript without triggering a reply.
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
+ });
146
+ }
147
+ // Every event arrives as a synthetic message, never as a user prompt, so it is
148
+ // not mistaken for the user's own words. Actionable events resume an idle
149
+ // agent; the rest wait in the transcript for the next turn.
82
150
  async routeEvents(events) {
83
151
  for (const queued of events) {
84
152
  const sessionID = queued.target.id;
85
- const text = eventText(queued.event);
86
- if (queued.event.actionable) {
87
- await this.session.prompt({ sessionID, text });
88
- } else {
89
- await this.session.synthetic({ sessionID, text });
90
- }
153
+ if (!await this.admitted(sessionID, queued)) continue;
154
+ await this.session.synthetic({
155
+ sessionID,
156
+ text: eventToText(queued.event),
157
+ description: `sourcefed ${queued.event.kind}`,
158
+ metadata: { sourcefed: { eventID: queued.id, monitorID: queued.monitorID, kind: queued.event.kind } },
159
+ resume: queued.event.actionable
160
+ });
161
+ }
162
+ }
163
+ /**
164
+ * Ask Chauffeur's gate, when it runs in this process. Only an explicit
165
+ * `deliver: false` withholds the event; no gate, an error, or a slow answer
166
+ * delivers.
167
+ */
168
+ async admitted(sessionID, queued) {
169
+ if (!this.rpc) return true;
170
+ const { event } = queued;
171
+ try {
172
+ const source = event.source?.type;
173
+ const reply = await this.rpc(ChauffeurGate).gate(
174
+ {
175
+ sessionID,
176
+ monitorID: queued.monitorID,
177
+ source: typeof source === "string" ? source : "unknown",
178
+ kind: event.kind,
179
+ summary: event.summary,
180
+ body: event.body ?? "",
181
+ actionable: event.actionable
182
+ },
183
+ { signal: AbortSignal.timeout(GATE_TIMEOUT_MS) }
184
+ );
185
+ return reply.deliver !== false;
186
+ } catch {
187
+ return true;
91
188
  }
92
189
  }
93
190
  target(sessionID) {
94
- return { kind: "opencode-session", id: sessionID };
191
+ return { kind: TARGET_KIND, id: sessionID };
95
192
  }
96
193
  };
194
+ async function connectDaemon() {
195
+ const url = process.env.SOURCEFED_DAEMON_URL;
196
+ if (url) return connectDaemonClient({ name: "sourcefed-opencode", url });
197
+ const local = daemonCommand(cliEntry());
198
+ const spawned = await spawnLocalDaemon({
199
+ command: local.command,
200
+ args: local.args,
201
+ env: daemonEnvironment()
202
+ });
203
+ return connectDaemonClient({ name: "sourcefed-opencode", url: spawned.url });
204
+ }
97
205
  function cliEntry() {
98
206
  const bundled = fileURLToPath(new URL("./cli.js", import.meta.url));
99
207
  if (existsSync(bundled)) return bundled;
@@ -106,12 +214,6 @@ function getOpenCodeBridge() {
106
214
  if (!activeBridge) throw new Error("Sourcefed OpenCode bridge is not initialized");
107
215
  return activeBridge;
108
216
  }
109
- function eventText(event) {
110
- const header = `[sourcefed monitor] ${event.summary}`;
111
- return event.body ? `${header}
112
-
113
- ${event.body}` : header;
114
- }
115
217
 
116
218
  // adapters/opencode/tool-result.ts
117
219
  async function callMonitorTool(name, arguments_, sessionID) {
@@ -219,7 +321,7 @@ var monitor_stop_default = definition5;
219
321
 
220
322
  // adapters/opencode/index.ts
221
323
  async function setupSourcefed(ctx) {
222
- const bridge = new OpenCodeBridge(ctx.session);
324
+ const bridge = new OpenCodeBridge(ctx.session, ctx.rpc);
223
325
  setOpenCodeBridge(bridge);
224
326
  await bridge.start();
225
327
  await ctx.tool.transform((editor) => {
@@ -238,12 +340,13 @@ var opencode_default = Plugin.define({
238
340
 
239
341
  // packages/sourcefed/src/server.ts
240
342
  var guidance = readFileSync(fileURLToPath2(new URL("./guidance.md", import.meta.url)), "utf8");
343
+ var heading = guidance.split("\n", 1)[0] ?? guidance;
241
344
  var server_default = Plugin2.define({
242
345
  id: "sourcefed",
243
346
  async setup(ctx) {
244
347
  const cleanup = await setupSourcefed(ctx);
245
348
  await ctx.session.hook("context", (event) => {
246
- if (!event.system.some((part) => part.type === "text" && part.text.includes("sourcefed"))) {
349
+ if (!event.system.some((part) => part.type === "text" && part.text.includes(heading))) {
247
350
  event.system.push({ type: "text", text: guidance });
248
351
  }
249
352
  });
package/dist/pi.js CHANGED
@@ -1,9 +1,12 @@
1
+ import {
2
+ eventToText
3
+ } from "./chunk-BNLZQKEB.js";
1
4
  import {
2
5
  connectDaemonClient,
3
6
  daemonCommand,
4
7
  daemonEnvironment,
5
8
  spawnLocalDaemon
6
- } from "./chunk-QPDTUNFA.js";
9
+ } from "./chunk-X3BZSHKZ.js";
7
10
  import "./chunk-MLKGABMK.js";
8
11
 
9
12
  // adapters/pi/src/index.ts
@@ -248,9 +251,7 @@ async function routeEvents(pi, events) {
248
251
  for (const queued of events) {
249
252
  await pi.sendMessage({
250
253
  customType: "sourcefed-monitor",
251
- content: `[sourcefed monitor] ${queued.event.summary}${queued.event.body ? `
252
-
253
- ${queued.event.body}` : ""}`,
254
+ content: eventToText(queued.event),
254
255
  display: true,
255
256
  details: { actionable: queued.event.actionable, eventID: queued.id }
256
257
  }, { triggerTurn: queued.event.actionable, deliverAs: "steer" });
@@ -1,18 +1,40 @@
1
- import type { Plugin } from "@opencode/plugin";
1
+ import { type Plugin } from "@opencode/plugin";
2
2
  type SessionDomain = Plugin.Context["session"];
3
+ type RpcDomain = Plugin.Context["rpc"];
3
4
  export declare class OpenCodeBridge {
4
5
  private readonly session;
6
+ private readonly rpc?;
5
7
  private daemon?;
6
8
  private readonly listeners;
7
9
  private lastAttemptAt;
8
10
  private lastError;
9
- constructor(session: SessionDomain);
11
+ private timer;
12
+ constructor(session: SessionDomain, rpc?: RpcDomain | undefined);
13
+ /**
14
+ * Chauffeur or the CLI can create a monitor for a session here at any time,
15
+ * without this bridge's tools, so check for new targets periodically too.
16
+ */
10
17
  start(): Promise<void>;
11
18
  ensureTarget(sessionID: string): Promise<void>;
12
19
  callTool(name: string, arguments_: Record<string, unknown>, sessionID: string): Promise<unknown>;
13
20
  close(): Promise<void>;
21
+ private refresh;
14
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;
15
31
  private routeEvents;
32
+ /**
33
+ * Ask Chauffeur's gate, when it runs in this process. Only an explicit
34
+ * `deliver: false` withholds the event; no gate, an error, or a slow answer
35
+ * delivers.
36
+ */
37
+ private admitted;
16
38
  private target;
17
39
  }
18
40
  export declare function setOpenCodeBridge(bridge: OpenCodeBridge): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,3 @@
1
1
  export declare function flag(args: string[], name: string): string | undefined;
2
+ /** Every value of a repeatable flag, in order: `--name a --name b` gives `["a", "b"]`. */
3
+ export declare function flags(args: string[], name: string): string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -19,6 +19,11 @@ export declare class SourcefedDaemon {
19
19
  webhook(request: Request): Promise<Response>;
20
20
  createMonitor(target: MonitorTarget, input: DaemonCreateInput): Promise<DaemonResult>;
21
21
  listMonitors(target: MonitorTarget): Promise<DaemonResult>;
22
+ /**
23
+ * Every target of `kind` with an enabled monitor, so a host that restarts can
24
+ * resubscribe the sessions whose monitors outlived it.
25
+ */
26
+ listTargets(kind: string): Promise<MonitorTarget[]>;
22
27
  getMonitor(target: MonitorTarget, id: string): Promise<DaemonResult>;
23
28
  stopMonitor(target: MonitorTarget, id: string): Promise<DaemonResult>;
24
29
  startMonitor(target: MonitorTarget, id: string): Promise<DaemonResult>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { MonitorEvent, MonitorEventSink, MonitorRecord } from "@fdcn/sourcefed/core";
2
+ export type ForwardOptions = {
3
+ url: string;
4
+ token?: string;
5
+ fetch?: typeof fetch;
6
+ };
7
+ /** The JSON body POSTed for each delivered event. */
8
+ export type ForwardedEvent = {
9
+ target: MonitorRecord["target"];
10
+ monitorID: string;
11
+ source: MonitorRecord["source"];
12
+ event: MonitorEvent;
13
+ };
14
+ /** `SOURCEFED_FORWARD_URL` (and optional `SOURCEFED_FORWARD_TOKEN`), when set. */
15
+ export declare function forwardFromEnv(): ForwardOptions | undefined;
16
+ /**
17
+ * Delivers to `inner`, then POSTs a copy of each delivered event to another
18
+ * local consumer. Forwarding never blocks or fails delivery.
19
+ */
20
+ export declare class ForwardingEventSink implements MonitorEventSink {
21
+ private readonly inner;
22
+ private readonly options;
23
+ constructor(inner: MonitorEventSink, options: ForwardOptions);
24
+ deliver(input: {
25
+ monitor: MonitorRecord;
26
+ event: MonitorEvent;
27
+ }): Promise<{
28
+ ok: boolean;
29
+ error?: string;
30
+ }>;
31
+ private forward;
32
+ }
@@ -0,0 +1,33 @@
1
+ import type { MonitorEvent, MonitorEventSink, MonitorRecord } from "@fdcn/sourcefed/core";
2
+ export type GateOptions = {
3
+ url: string;
4
+ token?: string;
5
+ fetch?: typeof fetch;
6
+ };
7
+ /** The JSON body POSTed for each event; the gate replies `{ "deliver": boolean }`. */
8
+ export type GatedEvent = {
9
+ target: MonitorRecord["target"];
10
+ monitorID: string;
11
+ source: MonitorRecord["source"];
12
+ event: MonitorEvent;
13
+ };
14
+ /** `SOURCEFED_GATE_URL` (and optional `SOURCEFED_GATE_TOKEN`), when set. */
15
+ export declare function gateFromEnv(): GateOptions | undefined;
16
+ /**
17
+ * Asks another local consumer whether each event should reach the session.
18
+ * Only an explicit `{ "deliver": false }` withholds it; a failed, slow, or
19
+ * unclear answer delivers, so the gate can never lose an event.
20
+ */
21
+ export declare class GatedEventSink implements MonitorEventSink {
22
+ private readonly inner;
23
+ private readonly options;
24
+ constructor(inner: MonitorEventSink, options: GateOptions);
25
+ deliver(input: {
26
+ monitor: MonitorRecord;
27
+ event: MonitorEvent;
28
+ }): Promise<{
29
+ ok: boolean;
30
+ error?: string;
31
+ }>;
32
+ private withheld;
33
+ }
@@ -1,8 +1,16 @@
1
+ type SlackUser = {
2
+ id: string;
3
+ name: string;
4
+ real_name: string;
5
+ };
1
6
  export declare function fetchSlackThread(channelId: string, threadTs: string): Promise<{
2
7
  messages: any[];
3
- users: any[];
4
- } | undefined>;
8
+ users: SlackUser[];
9
+ }>;
5
10
  export declare function fetchSlackDm(channelId: string): Promise<{
6
11
  messages: any[];
7
- users: any[];
8
- } | undefined>;
12
+ users: SlackUser[];
13
+ }>;
14
+ /** Forget the cached user list, so a test starts from an empty cache. */
15
+ export declare function resetSlackUsers(): void;
16
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fdcn/sourcefed",
3
- "version": "0.3.2",
3
+ "version": "0.3.5",
4
4
  "description": "Live Jira, GitHub, and Slack monitoring for agent hosts: CLI, daemon, MCP server, and host plugins.",
5
5
  "license": "MIT",
6
6
  "type": "module",