@fdcn/sourcefed 0.3.2 → 0.3.4
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/mcp.js +1 -1
- package/dist/opencode.js +124 -37
- package/dist/pi.js +5 -4
- package/dist.dts/adapters/opencode/bridge.d.ts +18 -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/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,10 +21,41 @@ 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 ChauffeurGate = Rpc.define({
|
|
28
|
+
id: "chauffeur",
|
|
29
|
+
methods: {
|
|
30
|
+
gate: {
|
|
31
|
+
input: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
sessionID: { type: "string" },
|
|
35
|
+
// The monitor that produced the event, so the gate knows what it watches.
|
|
36
|
+
monitorID: { type: "string" },
|
|
37
|
+
source: { type: "string" },
|
|
38
|
+
kind: { type: "string" },
|
|
39
|
+
summary: { type: "string" },
|
|
40
|
+
body: { type: "string" },
|
|
41
|
+
actionable: { type: "boolean" }
|
|
42
|
+
},
|
|
43
|
+
required: ["sessionID", "source", "kind", "summary", "actionable"]
|
|
44
|
+
},
|
|
45
|
+
output: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: { deliver: { type: "boolean" } },
|
|
48
|
+
required: ["deliver"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
events: {}
|
|
53
|
+
});
|
|
21
54
|
var activeBridge;
|
|
22
55
|
var OpenCodeBridge = class {
|
|
23
|
-
constructor(session) {
|
|
56
|
+
constructor(session, rpc) {
|
|
24
57
|
this.session = session;
|
|
58
|
+
this.rpc = rpc;
|
|
25
59
|
}
|
|
26
60
|
daemon;
|
|
27
61
|
listeners = /* @__PURE__ */ new Map();
|
|
@@ -32,12 +66,21 @@ var OpenCodeBridge = class {
|
|
|
32
66
|
}
|
|
33
67
|
async ensureTarget(sessionID) {
|
|
34
68
|
await this.ensureDaemon();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
69
|
+
const existing = this.listeners.get(sessionID);
|
|
70
|
+
if (existing) {
|
|
71
|
+
await existing;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const pending = this.daemon.subscribe(this.target(sessionID), async (events) => {
|
|
38
75
|
await this.routeEvents(events);
|
|
39
76
|
});
|
|
40
|
-
this.listeners.set(sessionID,
|
|
77
|
+
this.listeners.set(sessionID, pending);
|
|
78
|
+
try {
|
|
79
|
+
await pending;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.listeners.delete(sessionID);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
41
84
|
}
|
|
42
85
|
async callTool(name, arguments_, sessionID) {
|
|
43
86
|
await this.ensureDaemon();
|
|
@@ -48,7 +91,7 @@ var OpenCodeBridge = class {
|
|
|
48
91
|
return this.daemon.request(name.replace(/_/g, "."), { ...arguments_, target: this.target(sessionID) });
|
|
49
92
|
}
|
|
50
93
|
async close() {
|
|
51
|
-
for (const
|
|
94
|
+
for (const pending of this.listeners.values()) await pending.then((listener) => listener.close(), () => void 0);
|
|
52
95
|
this.listeners.clear();
|
|
53
96
|
await this.daemon?.close();
|
|
54
97
|
this.daemon = void 0;
|
|
@@ -58,42 +101,92 @@ var OpenCodeBridge = class {
|
|
|
58
101
|
if (Date.now() - this.lastAttemptAt < 15e3) return;
|
|
59
102
|
this.lastAttemptAt = Date.now();
|
|
60
103
|
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 });
|
|
104
|
+
this.daemon = await connectDaemon();
|
|
105
|
+
void this.resubscribe();
|
|
73
106
|
} catch (error) {
|
|
74
107
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
75
108
|
console.error(`[sourcefed] daemon unavailable: ${this.lastError}`);
|
|
76
109
|
}
|
|
77
110
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Monitors outlive this process, so after a restart nothing listens for their
|
|
113
|
+
* sessions until something subscribes. Subscribe every session that has an
|
|
114
|
+
* enabled monitor and still exists here; a deleted session is skipped, since
|
|
115
|
+
* delivering to it would fail and retry forever.
|
|
116
|
+
*/
|
|
117
|
+
async resubscribe() {
|
|
118
|
+
try {
|
|
119
|
+
const { targets } = await this.daemon.request("monitor.targets", { kind: TARGET_KIND });
|
|
120
|
+
await Promise.all(targets.map((target) => this.resubscribeTarget(target.id)));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`[sourcefed] resubscribe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async resubscribeTarget(sessionID) {
|
|
126
|
+
const exists = await this.session.get({ sessionID }).then(() => true, () => false);
|
|
127
|
+
if (!exists) return;
|
|
128
|
+
await this.ensureTarget(sessionID).catch((error) => {
|
|
129
|
+
console.error(`[sourcefed] subscribe ${sessionID} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Every event arrives as a synthetic message, never as a user prompt, so it is
|
|
133
|
+
// not mistaken for the user's own words. Actionable events resume an idle
|
|
134
|
+
// agent; the rest wait in the transcript for the next turn.
|
|
82
135
|
async routeEvents(events) {
|
|
83
136
|
for (const queued of events) {
|
|
84
137
|
const sessionID = queued.target.id;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
138
|
+
if (!await this.admitted(sessionID, queued)) continue;
|
|
139
|
+
await this.session.synthetic({
|
|
140
|
+
sessionID,
|
|
141
|
+
text: eventToText(queued.event),
|
|
142
|
+
description: `sourcefed ${queued.event.kind}`,
|
|
143
|
+
metadata: { sourcefed: { eventID: queued.id, monitorID: queued.monitorID, kind: queued.event.kind } },
|
|
144
|
+
resume: queued.event.actionable
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Ask Chauffeur's gate, when it runs in this process. Only an explicit
|
|
150
|
+
* `deliver: false` withholds the event; no gate, an error, or a slow answer
|
|
151
|
+
* delivers.
|
|
152
|
+
*/
|
|
153
|
+
async admitted(sessionID, queued) {
|
|
154
|
+
if (!this.rpc) return true;
|
|
155
|
+
const { event } = queued;
|
|
156
|
+
try {
|
|
157
|
+
const source = event.source?.type;
|
|
158
|
+
const reply = await this.rpc(ChauffeurGate).gate(
|
|
159
|
+
{
|
|
160
|
+
sessionID,
|
|
161
|
+
monitorID: queued.monitorID,
|
|
162
|
+
source: typeof source === "string" ? source : "unknown",
|
|
163
|
+
kind: event.kind,
|
|
164
|
+
summary: event.summary,
|
|
165
|
+
body: event.body ?? "",
|
|
166
|
+
actionable: event.actionable
|
|
167
|
+
},
|
|
168
|
+
{ signal: AbortSignal.timeout(GATE_TIMEOUT_MS) }
|
|
169
|
+
);
|
|
170
|
+
return reply.deliver !== false;
|
|
171
|
+
} catch {
|
|
172
|
+
return true;
|
|
91
173
|
}
|
|
92
174
|
}
|
|
93
175
|
target(sessionID) {
|
|
94
|
-
return { kind:
|
|
176
|
+
return { kind: TARGET_KIND, id: sessionID };
|
|
95
177
|
}
|
|
96
178
|
};
|
|
179
|
+
async function connectDaemon() {
|
|
180
|
+
const url = process.env.SOURCEFED_DAEMON_URL;
|
|
181
|
+
if (url) return connectDaemonClient({ name: "sourcefed-opencode", url });
|
|
182
|
+
const local = daemonCommand(cliEntry());
|
|
183
|
+
const spawned = await spawnLocalDaemon({
|
|
184
|
+
command: local.command,
|
|
185
|
+
args: local.args,
|
|
186
|
+
env: daemonEnvironment()
|
|
187
|
+
});
|
|
188
|
+
return connectDaemonClient({ name: "sourcefed-opencode", url: spawned.url });
|
|
189
|
+
}
|
|
97
190
|
function cliEntry() {
|
|
98
191
|
const bundled = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
99
192
|
if (existsSync(bundled)) return bundled;
|
|
@@ -106,12 +199,6 @@ function getOpenCodeBridge() {
|
|
|
106
199
|
if (!activeBridge) throw new Error("Sourcefed OpenCode bridge is not initialized");
|
|
107
200
|
return activeBridge;
|
|
108
201
|
}
|
|
109
|
-
function eventText(event) {
|
|
110
|
-
const header = `[sourcefed monitor] ${event.summary}`;
|
|
111
|
-
return event.body ? `${header}
|
|
112
|
-
|
|
113
|
-
${event.body}` : header;
|
|
114
|
-
}
|
|
115
202
|
|
|
116
203
|
// adapters/opencode/tool-result.ts
|
|
117
204
|
async function callMonitorTool(name, arguments_, sessionID) {
|
|
@@ -219,7 +306,7 @@ var monitor_stop_default = definition5;
|
|
|
219
306
|
|
|
220
307
|
// adapters/opencode/index.ts
|
|
221
308
|
async function setupSourcefed(ctx) {
|
|
222
|
-
const bridge = new OpenCodeBridge(ctx.session);
|
|
309
|
+
const bridge = new OpenCodeBridge(ctx.session, ctx.rpc);
|
|
223
310
|
setOpenCodeBridge(bridge);
|
|
224
311
|
await bridge.start();
|
|
225
312
|
await ctx.tool.transform((editor) => {
|
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,34 @@
|
|
|
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
|
-
constructor(session: SessionDomain);
|
|
11
|
+
constructor(session: SessionDomain, rpc?: RpcDomain | undefined);
|
|
10
12
|
start(): Promise<void>;
|
|
11
13
|
ensureTarget(sessionID: string): Promise<void>;
|
|
12
14
|
callTool(name: string, arguments_: Record<string, unknown>, sessionID: string): Promise<unknown>;
|
|
13
15
|
close(): Promise<void>;
|
|
14
16
|
private ensureDaemon;
|
|
17
|
+
/**
|
|
18
|
+
* Monitors outlive this process, so after a restart nothing listens for their
|
|
19
|
+
* sessions until something subscribes. Subscribe every session that has an
|
|
20
|
+
* enabled monitor and still exists here; a deleted session is skipped, since
|
|
21
|
+
* delivering to it would fail and retry forever.
|
|
22
|
+
*/
|
|
23
|
+
private resubscribe;
|
|
24
|
+
private resubscribeTarget;
|
|
15
25
|
private routeEvents;
|
|
26
|
+
/**
|
|
27
|
+
* Ask Chauffeur's gate, when it runs in this process. Only an explicit
|
|
28
|
+
* `deliver: false` withholds the event; no gate, an error, or a slow answer
|
|
29
|
+
* delivers.
|
|
30
|
+
*/
|
|
31
|
+
private admitted;
|
|
16
32
|
private target;
|
|
17
33
|
}
|
|
18
34
|
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 {};
|