@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.
- package/README.md +1 -1
- package/dist/{chunk-X3BZSHKZ.js → chunk-5JL3UV72.js} +1328 -399
- package/dist/cli.js +9888 -8063
- package/dist/daemon.js +1194 -263
- package/dist/data/core/SKILL.md +9 -4
- package/dist/data/core/references/commands.md +6 -1
- package/dist/data/sourcefed/SKILL.md +3 -2
- package/dist/guidance.md +6 -2
- package/dist/mcp.js +6 -2
- package/dist/opencode.js +263 -122
- package/dist/pi.js +161 -39
- package/dist/tui-compiled.mjs +306 -166
- package/dist.dts/adapters/opencode/bridge.d.ts +66 -22
- package/dist.dts/adapters/opencode/subscriptions.d.ts +46 -0
- package/dist.dts/adapters/opencode/tool-result.d.ts +2 -1
- package/dist.dts/adapters/opencode/tools/monitor_create.d.ts +19 -2
- package/dist.dts/adapters/opencode/tools/monitor_list.d.ts +3 -2
- package/dist.dts/adapters/opencode/tools/monitor_start.d.ts +3 -2
- package/dist.dts/adapters/opencode/tools/monitor_status.d.ts +3 -2
- package/dist.dts/adapters/opencode/tools/monitor_stop.d.ts +3 -2
- package/dist.dts/adapters/pi/src/dialog.d.ts +1 -2
- package/dist.dts/adapters/pi/src/monitors-dialog.d.ts +32 -0
- package/dist.dts/packages/core/src/types/monitor-create-input.d.ts +4 -0
- package/dist.dts/packages/daemon/src/event-stream.d.ts +26 -0
- package/dist.dts/packages/daemon/src/registry.d.ts +2 -0
- package/dist.dts/packages/daemon/src/stream-lifecycle.test.d.ts +1 -0
- package/dist.dts/packages/daemon/src/types/client.d.ts +3 -1
- package/dist.dts/packages/daemon/src/types/daemon.d.ts +4 -0
- package/dist.dts/packages/daemon/src/utils/index.d.ts +0 -1
- package/dist.dts/providers/yab/src/index.d.ts +2 -0
- package/dist.dts/providers/yab/src/monitor.d.ts +14 -0
- package/dist.dts/providers/yab/src/poll/index.d.ts +2 -0
- package/dist.dts/providers/yab/src/poll/monitor.d.ts +3 -0
- package/dist.dts/providers/yab/src/poll/utils/feed-events.d.ts +5 -0
- package/dist.dts/providers/yab/src/poll/utils/index.d.ts +4 -0
- package/dist.dts/providers/yab/src/poll/utils/parse-feed.d.ts +3 -0
- package/dist.dts/providers/yab/src/poll/utils/poll-yab.d.ts +7 -0
- package/dist.dts/providers/yab/src/poll/utils/yab-mcp.d.ts +10 -0
- package/dist.dts/providers/yab/src/schema.d.ts +12 -0
- package/dist.dts/providers/yab/src/types/cursor.d.ts +4 -0
- package/dist.dts/providers/yab/src/types/event.d.ts +8 -0
- package/dist.dts/providers/yab/src/types/feed-entry.d.ts +8 -0
- package/dist.dts/providers/yab/src/types/index.d.ts +5 -0
- package/dist.dts/providers/yab/src/types/page.d.ts +6 -0
- package/dist.dts/providers/yab/src/types/source-record.d.ts +7 -0
- package/dist.dts/providers/yab/src/yab.test.d.ts +1 -0
- package/package.json +1 -1
- package/dist.dts/packages/daemon/src/utils/forward.d.ts +0 -32
- package/dist.dts/packages/daemon/src/utils/gate.d.ts +0 -33
- package/dist.dts/packages/daemon/src/utils/request-router.d.ts +0 -11
- /package/dist.dts/{packages/daemon/src/forward.test.d.ts → adapters/opencode/lifecycle.test.d.ts} +0 -0
- /package/dist.dts/{packages/daemon/src/gate.test.d.ts → adapters/pi/src/monitors-dialog.test.d.ts} +0 -0
package/dist/data/core/SKILL.md
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
this.
|
|
104
|
-
|
|
105
|
-
this.
|
|
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.
|
|
111
|
-
await this.resubscribe();
|
|
282
|
+
if (!this.closed) await this.hub.refresh();
|
|
112
283
|
}
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
event.system.
|
|
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
|
});
|