@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.
- package/README.md +2 -2
- package/dist/chunk-BNLZQKEB.js +962 -0
- package/dist/{chunk-QPDTUNFA.js → chunk-X3BZSHKZ.js} +55 -45
- package/dist/cli.js +104 -96
- package/dist/daemon.js +341 -1271
- package/dist/data/core/references/commands.md +1 -1
- package/dist/guidance.md +15 -66
- package/dist/mcp.js +1 -1
- package/dist/opencode.js +141 -38
- package/dist/pi.js +5 -4
- package/dist.dts/adapters/opencode/bridge.d.ts +24 -2
- package/dist.dts/adapters/opencode/bridge.test.d.ts +1 -0
- package/dist.dts/packages/cli/src/utils/flags.d.ts +2 -0
- package/dist.dts/packages/cli/src/utils/flags.test.d.ts +1 -0
- package/dist.dts/packages/daemon/src/daemon.d.ts +5 -0
- package/dist.dts/packages/daemon/src/forward.test.d.ts +1 -0
- package/dist.dts/packages/daemon/src/gate.test.d.ts +1 -0
- package/dist.dts/packages/daemon/src/utils/forward.d.ts +32 -0
- package/dist.dts/packages/daemon/src/utils/gate.d.ts +33 -0
- package/dist.dts/providers/slack/src/poll/utils/slack-api.d.ts +12 -4
- package/dist.dts/providers/slack/src/poll/utils/slack-api.test.d.ts +1 -0
- package/package.json +1 -1
|
@@ -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[
|
|
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
|
|
1
|
+
# sourcefed monitors
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
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-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
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(
|
|
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-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 {};
|
|
@@ -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:
|
|
4
|
-
}
|
|
8
|
+
users: SlackUser[];
|
|
9
|
+
}>;
|
|
5
10
|
export declare function fetchSlackDm(channelId: string): Promise<{
|
|
6
11
|
messages: any[];
|
|
7
|
-
users:
|
|
8
|
-
}
|
|
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 {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|