@fdcn/sourcefed 0.2.8 → 0.3.0
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-DK3LZ5RU.js → chunk-QPDTUNFA.js} +45 -19
- package/dist/cli.js +72 -38
- package/dist/daemon.js +45 -19
- package/dist/data/core/SKILL.md +4 -2
- package/dist/data/core/references/commands.md +2 -2
- package/dist/data/sourcefed/SKILL.md +4 -4
- package/dist/mcp.js +2 -2
- package/dist/opencode.js +139 -12545
- package/dist/pi.js +2 -2
- package/dist/tui-compiled.mjs +144 -126
- package/dist.dts/adapters/opencode/bridge.d.ts +4 -5
- package/dist.dts/adapters/opencode/index.d.ts +4 -3
- package/dist.dts/adapters/opencode/tools/monitor_create.d.ts +52 -2
- package/dist.dts/adapters/opencode/tools/monitor_list.d.ts +13 -2
- package/dist.dts/adapters/opencode/tools/monitor_start.d.ts +19 -2
- package/dist.dts/adapters/opencode/tools/monitor_status.d.ts +19 -2
- package/dist.dts/adapters/opencode/tools/monitor_stop.d.ts +19 -2
- package/dist.dts/adapters/opencode/tui/plugin.d.ts +2 -2
- package/dist.dts/adapters/opencode/tui/sidebar.d.ts +8 -3
- package/dist.dts/adapters/opencode/tui.d.ts +3 -2
- package/dist.dts/index.d.ts +1 -1
- package/dist.dts/packages/daemon/src/daemon.d.ts +2 -0
- package/dist.dts/packages/sourcefed/src/server.d.ts +2 -6
- package/dist.dts/providers/slack/src/monitor.d.ts +1 -1
- package/dist.dts/providers/slack/src/poll/utils/parse-slack-read-result.d.ts +1 -1
- package/dist.dts/providers/slack/src/poll/utils/slack-api.d.ts +4 -0
- package/dist.dts/providers/slack/src/schema.d.ts +1 -1
- package/dist.dts/providers/slack/src/types/source-record.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,12 +33,12 @@ OpenCode loads this package as both a server and a TUI plugin:
|
|
|
33
33
|
|
|
34
34
|
```json
|
|
35
35
|
// opencode.json
|
|
36
|
-
{ "plugin": ["@fdcn/sourcefed@0.
|
|
36
|
+
{ "plugin": ["@fdcn/sourcefed@0.3.0"] }
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
```json
|
|
40
40
|
// tui.json
|
|
41
|
-
{ "plugin": ["@fdcn/sourcefed@0.
|
|
41
|
+
{ "plugin": ["@fdcn/sourcefed@0.3.0"] }
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The package exposes `./server`, `./tui`, `./opencode`, `./opencode/tui`, and `./pi` entrypoints for hosts that load them directly.
|
|
@@ -11008,7 +11008,7 @@ function safeParse24(schema, input, config$1) {
|
|
|
11008
11008
|
var SlackSourceSchema = /* @__PURE__ */ object23({
|
|
11009
11009
|
type: /* @__PURE__ */ literal3("slack"),
|
|
11010
11010
|
channelId: /* @__PURE__ */ string24(),
|
|
11011
|
-
threadTs: /* @__PURE__ */ string24()
|
|
11011
|
+
threadTs: /* @__PURE__ */ optional22(/* @__PURE__ */ string24())
|
|
11012
11012
|
});
|
|
11013
11013
|
var SlackCursorSchema = /* @__PURE__ */ object23({
|
|
11014
11014
|
primed: /* @__PURE__ */ boolean23(),
|
|
@@ -11025,7 +11025,7 @@ function messageAt(ts) {
|
|
|
11025
11025
|
if (value > 0) return new Date(value * 1e3).toISOString();
|
|
11026
11026
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
11027
11027
|
}
|
|
11028
|
-
function parseSlackReadResult(result, cursorRaw) {
|
|
11028
|
+
function parseSlackReadResult(result, cursorRaw, location = "thread") {
|
|
11029
11029
|
const parsedCursor = /* @__PURE__ */ safeParse24(SlackCursorSchema, cursorRaw);
|
|
11030
11030
|
let previous = {};
|
|
11031
11031
|
if (parsedCursor.success) previous = parsedCursor.output;
|
|
@@ -11053,7 +11053,7 @@ function parseSlackReadResult(result, cursorRaw) {
|
|
|
11053
11053
|
kind: "message",
|
|
11054
11054
|
id: `message:${message.ts}`,
|
|
11055
11055
|
at: messageAt(message.ts),
|
|
11056
|
-
summary: `Slack
|
|
11056
|
+
summary: `Slack ${location} message by ${author}`,
|
|
11057
11057
|
body: text,
|
|
11058
11058
|
actionable: true
|
|
11059
11059
|
};
|
|
@@ -11074,15 +11074,19 @@ function slackToken() {
|
|
|
11074
11074
|
return process.env.SOURCEFED_SLACK_TOKEN;
|
|
11075
11075
|
}
|
|
11076
11076
|
async function fetchSlackThread(channelId, threadTs) {
|
|
11077
|
+
return fetchSlackMessages("conversations.replies", { channel: channelId, ts: threadTs, inclusive: "false" });
|
|
11078
|
+
}
|
|
11079
|
+
async function fetchSlackDm(channelId) {
|
|
11080
|
+
return fetchSlackMessages("conversations.history", { channel: channelId });
|
|
11081
|
+
}
|
|
11082
|
+
async function fetchSlackMessages(method, params) {
|
|
11077
11083
|
const messages = [];
|
|
11078
11084
|
let cursor = "";
|
|
11079
11085
|
let received = false;
|
|
11080
11086
|
do {
|
|
11081
|
-
const page = await slackApi(
|
|
11082
|
-
|
|
11083
|
-
ts: threadTs,
|
|
11087
|
+
const page = await slackApi(method, {
|
|
11088
|
+
...params,
|
|
11084
11089
|
limit: "1000",
|
|
11085
|
-
inclusive: "false",
|
|
11086
11090
|
...cursor ? { cursor } : {}
|
|
11087
11091
|
});
|
|
11088
11092
|
if (!page) break;
|
|
@@ -11106,8 +11110,7 @@ async function fetchSlackThread(channelId, threadTs) {
|
|
|
11106
11110
|
}
|
|
11107
11111
|
usersCursor = page.response_metadata?.next_cursor ?? "";
|
|
11108
11112
|
} while (usersCursor);
|
|
11109
|
-
|
|
11110
|
-
return result;
|
|
11113
|
+
return { messages, users };
|
|
11111
11114
|
}
|
|
11112
11115
|
async function slackApi(method, params) {
|
|
11113
11116
|
const token = slackToken();
|
|
@@ -11132,9 +11135,9 @@ async function slackApi(method, params) {
|
|
|
11132
11135
|
}
|
|
11133
11136
|
}
|
|
11134
11137
|
async function pollSlack(source, cursorRaw) {
|
|
11135
|
-
const result = await fetchSlackThread(source.channelId, source.threadTs);
|
|
11138
|
+
const result = source.threadTs ? await fetchSlackThread(source.channelId, source.threadTs) : await fetchSlackDm(source.channelId);
|
|
11136
11139
|
if (!result) throw new Error("slack API request failed; check SOURCEFED_SLACK_TOKEN");
|
|
11137
|
-
const parsed = parseSlackReadResult(result, cursorRaw);
|
|
11140
|
+
const parsed = parseSlackReadResult(result, cursorRaw, source.threadTs ? "thread" : "DM");
|
|
11138
11141
|
return { ...parsed, terminal: false };
|
|
11139
11142
|
}
|
|
11140
11143
|
var slackPollMonitor = new PollMonitor3({
|
|
@@ -11151,7 +11154,8 @@ function parseSlackWebhook(payload, _eventName, deliveryId) {
|
|
|
11151
11154
|
const event = payload.event;
|
|
11152
11155
|
const channelId = event?.channel;
|
|
11153
11156
|
const threadTs = event?.thread_ts;
|
|
11154
|
-
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId
|
|
11157
|
+
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId) return void 0;
|
|
11158
|
+
if (!threadTs && !channelId.startsWith("D")) return void 0;
|
|
11155
11159
|
let text = "[message without text]";
|
|
11156
11160
|
if (event.text?.trim()) text = event.text.trim();
|
|
11157
11161
|
let author = event.username;
|
|
@@ -11160,12 +11164,14 @@ function parseSlackWebhook(payload, _eventName, deliveryId) {
|
|
|
11160
11164
|
if (!author) author = "someone";
|
|
11161
11165
|
let at = messageAt(event.event_ts);
|
|
11162
11166
|
if (!event.event_ts) at = messageAt(event.ts);
|
|
11167
|
+
const source = threadTs ? { type: "slack", channelId, threadTs } : { type: "slack", channelId };
|
|
11168
|
+
const location = threadTs ? "thread" : "DM";
|
|
11163
11169
|
return {
|
|
11164
|
-
source
|
|
11170
|
+
source,
|
|
11165
11171
|
kind: "message",
|
|
11166
11172
|
id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
|
|
11167
11173
|
at,
|
|
11168
|
-
summary: `Slack
|
|
11174
|
+
summary: `Slack ${location} message by ${author}`,
|
|
11169
11175
|
body: `${text}
|
|
11170
11176
|
|
|
11171
11177
|
Slack event: ${deliveryId}`,
|
|
@@ -11218,14 +11224,16 @@ var SlackMonitor = class extends Monitor3 {
|
|
|
11218
11224
|
constructor() {
|
|
11219
11225
|
super({ type: "slack", schema: SlackSourceSchema });
|
|
11220
11226
|
}
|
|
11221
|
-
key = (source) => `slack:${source.channelId}#${source.threadTs}`;
|
|
11227
|
+
key = (source) => source.threadTs ? `slack:${source.channelId}#${source.threadTs}` : `slack:${source.channelId}`;
|
|
11222
11228
|
icon = "\u{F04B1}";
|
|
11223
|
-
label = () => "thread";
|
|
11229
|
+
label = (source) => source.threadTs ? "thread" : "DM";
|
|
11224
11230
|
detail = (source) => ` ${source.channelId}`;
|
|
11225
|
-
describe = (source) => `Slack ${source.channelId} thread ${source.threadTs}`;
|
|
11231
|
+
describe = (source) => source.threadTs ? `Slack ${source.channelId} thread ${source.threadTs}` : `Slack DM ${source.channelId}`;
|
|
11226
11232
|
build(input) {
|
|
11227
11233
|
if (input.channelId && input.threadTs) return { type: "slack", channelId: input.channelId, threadTs: input.threadTs };
|
|
11228
|
-
if (
|
|
11234
|
+
if (input.channelId?.startsWith("D")) return { type: "slack", channelId: input.channelId };
|
|
11235
|
+
if (input.channelId) return { error: "threadTs is required unless channelId is a Slack DM ID" };
|
|
11236
|
+
if (!input.threadUrl) return { error: "channelId or threadUrl is required for a slack monitor" };
|
|
11229
11237
|
let url;
|
|
11230
11238
|
try {
|
|
11231
11239
|
url = new URL(input.threadUrl);
|
|
@@ -11597,6 +11605,7 @@ var SourcefedDaemon = class {
|
|
|
11597
11605
|
if (!monitor) return { ok: false, error: `monitor ${id} was not found for this target` };
|
|
11598
11606
|
const stopped = await this.service.stop(id);
|
|
11599
11607
|
if ("error" in stopped) return { ok: false, error: stopped.error };
|
|
11608
|
+
await this.purgeMonitorEvents(monitor.target, id);
|
|
11600
11609
|
return { ok: true, monitor: monitorView(stopped) };
|
|
11601
11610
|
}
|
|
11602
11611
|
async startMonitor(target, id) {
|
|
@@ -11610,6 +11619,7 @@ var SourcefedDaemon = class {
|
|
|
11610
11619
|
const monitor = await ownedMonitor(this.service, id, target);
|
|
11611
11620
|
if (!monitor) return { ok: false, error: `monitor ${id} was not found for this target` };
|
|
11612
11621
|
await this.service.remove(id);
|
|
11622
|
+
await this.purgeMonitorEvents(monitor.target, id);
|
|
11613
11623
|
return { ok: true, removed: true };
|
|
11614
11624
|
}
|
|
11615
11625
|
async readEvents(target) {
|
|
@@ -11683,6 +11693,8 @@ var SourcefedDaemon = class {
|
|
|
11683
11693
|
async deliver(key, target, listeners) {
|
|
11684
11694
|
const queued = await this.queue.read(target);
|
|
11685
11695
|
if (queued.length === 0) return;
|
|
11696
|
+
const enabled = new Set((await this.service.list()).filter((monitor) => monitor.enabled).map((monitor) => monitor.id));
|
|
11697
|
+
await this.dropQueued(target, queued.filter((event) => !enabled.has(event.monitorID)).map((event) => event.id));
|
|
11686
11698
|
let receivedByListener = this.received.get(key);
|
|
11687
11699
|
if (!receivedByListener) {
|
|
11688
11700
|
receivedByListener = /* @__PURE__ */ new Map();
|
|
@@ -11692,7 +11704,7 @@ var SourcefedDaemon = class {
|
|
|
11692
11704
|
for (const received of receivedByListener.values()) {
|
|
11693
11705
|
for (const id of received) claimed.add(id);
|
|
11694
11706
|
}
|
|
11695
|
-
const fresh = queued.filter((event) => !claimed.has(event.id));
|
|
11707
|
+
const fresh = queued.filter((event) => enabled.has(event.monitorID) && !claimed.has(event.id));
|
|
11696
11708
|
if (fresh.length === 0) return;
|
|
11697
11709
|
for (const listener of [...listeners]) {
|
|
11698
11710
|
let received = receivedByListener.get(listener);
|
|
@@ -11711,6 +11723,20 @@ var SourcefedDaemon = class {
|
|
|
11711
11723
|
}
|
|
11712
11724
|
}
|
|
11713
11725
|
}
|
|
11726
|
+
async dropQueued(target, eventIDs) {
|
|
11727
|
+
if (eventIDs.length === 0) return;
|
|
11728
|
+
await this.queue.acknowledge(target, eventIDs);
|
|
11729
|
+
const receivedByListener = this.received.get(targetKey(target));
|
|
11730
|
+
if (!receivedByListener) return;
|
|
11731
|
+
const ids = new Set(eventIDs);
|
|
11732
|
+
for (const received of receivedByListener.values()) {
|
|
11733
|
+
for (const id of ids) received.delete(id);
|
|
11734
|
+
}
|
|
11735
|
+
}
|
|
11736
|
+
async purgeMonitorEvents(target, monitorID) {
|
|
11737
|
+
const queued = await this.queue.read(target);
|
|
11738
|
+
await this.dropQueued(target, queued.filter((event) => event.monitorID === monitorID).map((event) => event.id));
|
|
11739
|
+
}
|
|
11714
11740
|
};
|
|
11715
11741
|
function monitorView(monitor) {
|
|
11716
11742
|
const definition = sourceDefinition(monitor.source);
|
package/dist/cli.js
CHANGED
|
@@ -11014,7 +11014,7 @@ function safeParse24(schema, input, config$1) {
|
|
|
11014
11014
|
var SlackSourceSchema = /* @__PURE__ */ object23({
|
|
11015
11015
|
type: /* @__PURE__ */ literal3("slack"),
|
|
11016
11016
|
channelId: /* @__PURE__ */ string24(),
|
|
11017
|
-
threadTs: /* @__PURE__ */ string24()
|
|
11017
|
+
threadTs: /* @__PURE__ */ optional22(/* @__PURE__ */ string24())
|
|
11018
11018
|
});
|
|
11019
11019
|
var SlackCursorSchema = /* @__PURE__ */ object23({
|
|
11020
11020
|
primed: /* @__PURE__ */ boolean23(),
|
|
@@ -11031,7 +11031,7 @@ function messageAt(ts) {
|
|
|
11031
11031
|
if (value > 0) return new Date(value * 1e3).toISOString();
|
|
11032
11032
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
11033
11033
|
}
|
|
11034
|
-
function parseSlackReadResult(result, cursorRaw) {
|
|
11034
|
+
function parseSlackReadResult(result, cursorRaw, location = "thread") {
|
|
11035
11035
|
const parsedCursor = /* @__PURE__ */ safeParse24(SlackCursorSchema, cursorRaw);
|
|
11036
11036
|
let previous = {};
|
|
11037
11037
|
if (parsedCursor.success) previous = parsedCursor.output;
|
|
@@ -11059,7 +11059,7 @@ function parseSlackReadResult(result, cursorRaw) {
|
|
|
11059
11059
|
kind: "message",
|
|
11060
11060
|
id: `message:${message.ts}`,
|
|
11061
11061
|
at: messageAt(message.ts),
|
|
11062
|
-
summary: `Slack
|
|
11062
|
+
summary: `Slack ${location} message by ${author}`,
|
|
11063
11063
|
body: text,
|
|
11064
11064
|
actionable: true
|
|
11065
11065
|
};
|
|
@@ -11080,15 +11080,19 @@ function slackToken() {
|
|
|
11080
11080
|
return process.env.SOURCEFED_SLACK_TOKEN;
|
|
11081
11081
|
}
|
|
11082
11082
|
async function fetchSlackThread(channelId, threadTs) {
|
|
11083
|
+
return fetchSlackMessages("conversations.replies", { channel: channelId, ts: threadTs, inclusive: "false" });
|
|
11084
|
+
}
|
|
11085
|
+
async function fetchSlackDm(channelId) {
|
|
11086
|
+
return fetchSlackMessages("conversations.history", { channel: channelId });
|
|
11087
|
+
}
|
|
11088
|
+
async function fetchSlackMessages(method, params) {
|
|
11083
11089
|
const messages = [];
|
|
11084
11090
|
let cursor = "";
|
|
11085
11091
|
let received = false;
|
|
11086
11092
|
do {
|
|
11087
|
-
const page = await slackApi(
|
|
11088
|
-
|
|
11089
|
-
ts: threadTs,
|
|
11093
|
+
const page = await slackApi(method, {
|
|
11094
|
+
...params,
|
|
11090
11095
|
limit: "1000",
|
|
11091
|
-
inclusive: "false",
|
|
11092
11096
|
...cursor ? { cursor } : {}
|
|
11093
11097
|
});
|
|
11094
11098
|
if (!page) break;
|
|
@@ -11112,8 +11116,7 @@ async function fetchSlackThread(channelId, threadTs) {
|
|
|
11112
11116
|
}
|
|
11113
11117
|
usersCursor = page.response_metadata?.next_cursor ?? "";
|
|
11114
11118
|
} while (usersCursor);
|
|
11115
|
-
|
|
11116
|
-
return result;
|
|
11119
|
+
return { messages, users };
|
|
11117
11120
|
}
|
|
11118
11121
|
async function slackApi(method, params) {
|
|
11119
11122
|
const token = slackToken();
|
|
@@ -11138,9 +11141,9 @@ async function slackApi(method, params) {
|
|
|
11138
11141
|
}
|
|
11139
11142
|
}
|
|
11140
11143
|
async function pollSlack(source, cursorRaw) {
|
|
11141
|
-
const result = await fetchSlackThread(source.channelId, source.threadTs);
|
|
11144
|
+
const result = source.threadTs ? await fetchSlackThread(source.channelId, source.threadTs) : await fetchSlackDm(source.channelId);
|
|
11142
11145
|
if (!result) throw new Error("slack API request failed; check SOURCEFED_SLACK_TOKEN");
|
|
11143
|
-
const parsed = parseSlackReadResult(result, cursorRaw);
|
|
11146
|
+
const parsed = parseSlackReadResult(result, cursorRaw, source.threadTs ? "thread" : "DM");
|
|
11144
11147
|
return { ...parsed, terminal: false };
|
|
11145
11148
|
}
|
|
11146
11149
|
var slackPollMonitor = new PollMonitor3({
|
|
@@ -11157,7 +11160,8 @@ function parseSlackWebhook(payload, _eventName, deliveryId) {
|
|
|
11157
11160
|
const event = payload.event;
|
|
11158
11161
|
const channelId = event?.channel;
|
|
11159
11162
|
const threadTs = event?.thread_ts;
|
|
11160
|
-
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId
|
|
11163
|
+
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId) return void 0;
|
|
11164
|
+
if (!threadTs && !channelId.startsWith("D")) return void 0;
|
|
11161
11165
|
let text = "[message without text]";
|
|
11162
11166
|
if (event.text?.trim()) text = event.text.trim();
|
|
11163
11167
|
let author = event.username;
|
|
@@ -11166,12 +11170,14 @@ function parseSlackWebhook(payload, _eventName, deliveryId) {
|
|
|
11166
11170
|
if (!author) author = "someone";
|
|
11167
11171
|
let at = messageAt(event.event_ts);
|
|
11168
11172
|
if (!event.event_ts) at = messageAt(event.ts);
|
|
11173
|
+
const source = threadTs ? { type: "slack", channelId, threadTs } : { type: "slack", channelId };
|
|
11174
|
+
const location = threadTs ? "thread" : "DM";
|
|
11169
11175
|
return {
|
|
11170
|
-
source
|
|
11176
|
+
source,
|
|
11171
11177
|
kind: "message",
|
|
11172
11178
|
id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
|
|
11173
11179
|
at,
|
|
11174
|
-
summary: `Slack
|
|
11180
|
+
summary: `Slack ${location} message by ${author}`,
|
|
11175
11181
|
body: `${text}
|
|
11176
11182
|
|
|
11177
11183
|
Slack event: ${deliveryId}`,
|
|
@@ -11224,14 +11230,16 @@ var SlackMonitor = class extends Monitor3 {
|
|
|
11224
11230
|
constructor() {
|
|
11225
11231
|
super({ type: "slack", schema: SlackSourceSchema });
|
|
11226
11232
|
}
|
|
11227
|
-
key = (source) => `slack:${source.channelId}#${source.threadTs}`;
|
|
11233
|
+
key = (source) => source.threadTs ? `slack:${source.channelId}#${source.threadTs}` : `slack:${source.channelId}`;
|
|
11228
11234
|
icon = "\u{F04B1}";
|
|
11229
|
-
label = () => "thread";
|
|
11235
|
+
label = (source) => source.threadTs ? "thread" : "DM";
|
|
11230
11236
|
detail = (source) => ` ${source.channelId}`;
|
|
11231
|
-
describe = (source) => `Slack ${source.channelId} thread ${source.threadTs}`;
|
|
11237
|
+
describe = (source) => source.threadTs ? `Slack ${source.channelId} thread ${source.threadTs}` : `Slack DM ${source.channelId}`;
|
|
11232
11238
|
build(input) {
|
|
11233
11239
|
if (input.channelId && input.threadTs) return { type: "slack", channelId: input.channelId, threadTs: input.threadTs };
|
|
11234
|
-
if (
|
|
11240
|
+
if (input.channelId?.startsWith("D")) return { type: "slack", channelId: input.channelId };
|
|
11241
|
+
if (input.channelId) return { error: "threadTs is required unless channelId is a Slack DM ID" };
|
|
11242
|
+
if (!input.threadUrl) return { error: "channelId or threadUrl is required for a slack monitor" };
|
|
11235
11243
|
let url2;
|
|
11236
11244
|
try {
|
|
11237
11245
|
url2 = new URL(input.threadUrl);
|
|
@@ -11520,6 +11528,7 @@ var SourcefedDaemon = class {
|
|
|
11520
11528
|
if (!monitor) return { ok: false, error: `monitor ${id} was not found for this target` };
|
|
11521
11529
|
const stopped = await this.service.stop(id);
|
|
11522
11530
|
if ("error" in stopped) return { ok: false, error: stopped.error };
|
|
11531
|
+
await this.purgeMonitorEvents(monitor.target, id);
|
|
11523
11532
|
return { ok: true, monitor: monitorView(stopped) };
|
|
11524
11533
|
}
|
|
11525
11534
|
async startMonitor(target, id) {
|
|
@@ -11533,6 +11542,7 @@ var SourcefedDaemon = class {
|
|
|
11533
11542
|
const monitor = await ownedMonitor(this.service, id, target);
|
|
11534
11543
|
if (!monitor) return { ok: false, error: `monitor ${id} was not found for this target` };
|
|
11535
11544
|
await this.service.remove(id);
|
|
11545
|
+
await this.purgeMonitorEvents(monitor.target, id);
|
|
11536
11546
|
return { ok: true, removed: true };
|
|
11537
11547
|
}
|
|
11538
11548
|
async readEvents(target) {
|
|
@@ -11606,6 +11616,8 @@ var SourcefedDaemon = class {
|
|
|
11606
11616
|
async deliver(key, target, listeners) {
|
|
11607
11617
|
const queued = await this.queue.read(target);
|
|
11608
11618
|
if (queued.length === 0) return;
|
|
11619
|
+
const enabled = new Set((await this.service.list()).filter((monitor) => monitor.enabled).map((monitor) => monitor.id));
|
|
11620
|
+
await this.dropQueued(target, queued.filter((event) => !enabled.has(event.monitorID)).map((event) => event.id));
|
|
11609
11621
|
let receivedByListener = this.received.get(key);
|
|
11610
11622
|
if (!receivedByListener) {
|
|
11611
11623
|
receivedByListener = /* @__PURE__ */ new Map();
|
|
@@ -11615,7 +11627,7 @@ var SourcefedDaemon = class {
|
|
|
11615
11627
|
for (const received of receivedByListener.values()) {
|
|
11616
11628
|
for (const id of received) claimed.add(id);
|
|
11617
11629
|
}
|
|
11618
|
-
const fresh = queued.filter((event) => !claimed.has(event.id));
|
|
11630
|
+
const fresh = queued.filter((event) => enabled.has(event.monitorID) && !claimed.has(event.id));
|
|
11619
11631
|
if (fresh.length === 0) return;
|
|
11620
11632
|
for (const listener of [...listeners]) {
|
|
11621
11633
|
let received = receivedByListener.get(listener);
|
|
@@ -11634,6 +11646,20 @@ var SourcefedDaemon = class {
|
|
|
11634
11646
|
}
|
|
11635
11647
|
}
|
|
11636
11648
|
}
|
|
11649
|
+
async dropQueued(target, eventIDs) {
|
|
11650
|
+
if (eventIDs.length === 0) return;
|
|
11651
|
+
await this.queue.acknowledge(target, eventIDs);
|
|
11652
|
+
const receivedByListener = this.received.get(targetKey(target));
|
|
11653
|
+
if (!receivedByListener) return;
|
|
11654
|
+
const ids = new Set(eventIDs);
|
|
11655
|
+
for (const received of receivedByListener.values()) {
|
|
11656
|
+
for (const id of ids) received.delete(id);
|
|
11657
|
+
}
|
|
11658
|
+
}
|
|
11659
|
+
async purgeMonitorEvents(target, monitorID) {
|
|
11660
|
+
const queued = await this.queue.read(target);
|
|
11661
|
+
await this.dropQueued(target, queued.filter((event) => event.monitorID === monitorID).map((event) => event.id));
|
|
11662
|
+
}
|
|
11637
11663
|
};
|
|
11638
11664
|
function monitorView(monitor) {
|
|
11639
11665
|
const definition = sourceDefinition(monitor.source);
|
|
@@ -43501,7 +43527,7 @@ function safeParse242(schema, input, config$1) {
|
|
|
43501
43527
|
var SlackSourceSchema2 = /* @__PURE__ */ object232({
|
|
43502
43528
|
type: /* @__PURE__ */ literal32("slack"),
|
|
43503
43529
|
channelId: /* @__PURE__ */ string242(),
|
|
43504
|
-
threadTs: /* @__PURE__ */ string242()
|
|
43530
|
+
threadTs: /* @__PURE__ */ optional2222(/* @__PURE__ */ string242())
|
|
43505
43531
|
});
|
|
43506
43532
|
var SlackCursorSchema2 = /* @__PURE__ */ object232({
|
|
43507
43533
|
primed: /* @__PURE__ */ boolean232(),
|
|
@@ -43518,7 +43544,7 @@ function messageAt2(ts) {
|
|
|
43518
43544
|
if (value > 0) return new Date(value * 1e3).toISOString();
|
|
43519
43545
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
43520
43546
|
}
|
|
43521
|
-
function parseSlackReadResult2(result, cursorRaw) {
|
|
43547
|
+
function parseSlackReadResult2(result, cursorRaw, location = "thread") {
|
|
43522
43548
|
const parsedCursor = /* @__PURE__ */ safeParse242(SlackCursorSchema2, cursorRaw);
|
|
43523
43549
|
let previous = {};
|
|
43524
43550
|
if (parsedCursor.success) previous = parsedCursor.output;
|
|
@@ -43546,7 +43572,7 @@ function parseSlackReadResult2(result, cursorRaw) {
|
|
|
43546
43572
|
kind: "message",
|
|
43547
43573
|
id: `message:${message.ts}`,
|
|
43548
43574
|
at: messageAt2(message.ts),
|
|
43549
|
-
summary: `Slack
|
|
43575
|
+
summary: `Slack ${location} message by ${author}`,
|
|
43550
43576
|
body: text,
|
|
43551
43577
|
actionable: true
|
|
43552
43578
|
};
|
|
@@ -43567,15 +43593,19 @@ function slackToken2() {
|
|
|
43567
43593
|
return process.env.SOURCEFED_SLACK_TOKEN;
|
|
43568
43594
|
}
|
|
43569
43595
|
async function fetchSlackThread2(channelId, threadTs) {
|
|
43596
|
+
return fetchSlackMessages2("conversations.replies", { channel: channelId, ts: threadTs, inclusive: "false" });
|
|
43597
|
+
}
|
|
43598
|
+
async function fetchSlackDm2(channelId) {
|
|
43599
|
+
return fetchSlackMessages2("conversations.history", { channel: channelId });
|
|
43600
|
+
}
|
|
43601
|
+
async function fetchSlackMessages2(method, params) {
|
|
43570
43602
|
const messages = [];
|
|
43571
43603
|
let cursor = "";
|
|
43572
43604
|
let received = false;
|
|
43573
43605
|
do {
|
|
43574
|
-
const page = await slackApi2(
|
|
43575
|
-
|
|
43576
|
-
ts: threadTs,
|
|
43606
|
+
const page = await slackApi2(method, {
|
|
43607
|
+
...params,
|
|
43577
43608
|
limit: "1000",
|
|
43578
|
-
inclusive: "false",
|
|
43579
43609
|
...cursor ? { cursor } : {}
|
|
43580
43610
|
});
|
|
43581
43611
|
if (!page) break;
|
|
@@ -43599,8 +43629,7 @@ async function fetchSlackThread2(channelId, threadTs) {
|
|
|
43599
43629
|
}
|
|
43600
43630
|
usersCursor = page.response_metadata?.next_cursor ?? "";
|
|
43601
43631
|
} while (usersCursor);
|
|
43602
|
-
|
|
43603
|
-
return result;
|
|
43632
|
+
return { messages, users };
|
|
43604
43633
|
}
|
|
43605
43634
|
async function slackApi2(method, params) {
|
|
43606
43635
|
const token = slackToken2();
|
|
@@ -43625,9 +43654,9 @@ async function slackApi2(method, params) {
|
|
|
43625
43654
|
}
|
|
43626
43655
|
}
|
|
43627
43656
|
async function pollSlack2(source, cursorRaw) {
|
|
43628
|
-
const result = await fetchSlackThread2(source.channelId, source.threadTs);
|
|
43657
|
+
const result = source.threadTs ? await fetchSlackThread2(source.channelId, source.threadTs) : await fetchSlackDm2(source.channelId);
|
|
43629
43658
|
if (!result) throw new Error("slack API request failed; check SOURCEFED_SLACK_TOKEN");
|
|
43630
|
-
const parsed = parseSlackReadResult2(result, cursorRaw);
|
|
43659
|
+
const parsed = parseSlackReadResult2(result, cursorRaw, source.threadTs ? "thread" : "DM");
|
|
43631
43660
|
return { ...parsed, terminal: false };
|
|
43632
43661
|
}
|
|
43633
43662
|
var slackPollMonitor2 = new PollMonitor32({
|
|
@@ -43644,7 +43673,8 @@ function parseSlackWebhook2(payload, _eventName, deliveryId) {
|
|
|
43644
43673
|
const event = payload.event;
|
|
43645
43674
|
const channelId = event?.channel;
|
|
43646
43675
|
const threadTs = event?.thread_ts;
|
|
43647
|
-
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId
|
|
43676
|
+
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId) return void 0;
|
|
43677
|
+
if (!threadTs && !channelId.startsWith("D")) return void 0;
|
|
43648
43678
|
let text = "[message without text]";
|
|
43649
43679
|
if (event.text?.trim()) text = event.text.trim();
|
|
43650
43680
|
let author = event.username;
|
|
@@ -43653,12 +43683,14 @@ function parseSlackWebhook2(payload, _eventName, deliveryId) {
|
|
|
43653
43683
|
if (!author) author = "someone";
|
|
43654
43684
|
let at = messageAt2(event.event_ts);
|
|
43655
43685
|
if (!event.event_ts) at = messageAt2(event.ts);
|
|
43686
|
+
const source = threadTs ? { type: "slack", channelId, threadTs } : { type: "slack", channelId };
|
|
43687
|
+
const location = threadTs ? "thread" : "DM";
|
|
43656
43688
|
return {
|
|
43657
|
-
source
|
|
43689
|
+
source,
|
|
43658
43690
|
kind: "message",
|
|
43659
43691
|
id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
|
|
43660
43692
|
at,
|
|
43661
|
-
summary: `Slack
|
|
43693
|
+
summary: `Slack ${location} message by ${author}`,
|
|
43662
43694
|
body: `${text}
|
|
43663
43695
|
|
|
43664
43696
|
Slack event: ${deliveryId}`,
|
|
@@ -43711,14 +43743,16 @@ var SlackMonitor2 = class extends Monitor32 {
|
|
|
43711
43743
|
constructor() {
|
|
43712
43744
|
super({ type: "slack", schema: SlackSourceSchema2 });
|
|
43713
43745
|
}
|
|
43714
|
-
key = (source) => `slack:${source.channelId}#${source.threadTs}`;
|
|
43746
|
+
key = (source) => source.threadTs ? `slack:${source.channelId}#${source.threadTs}` : `slack:${source.channelId}`;
|
|
43715
43747
|
icon = "\u{F04B1}";
|
|
43716
|
-
label = () => "thread";
|
|
43748
|
+
label = (source) => source.threadTs ? "thread" : "DM";
|
|
43717
43749
|
detail = (source) => ` ${source.channelId}`;
|
|
43718
|
-
describe = (source) => `Slack ${source.channelId} thread ${source.threadTs}`;
|
|
43750
|
+
describe = (source) => source.threadTs ? `Slack ${source.channelId} thread ${source.threadTs}` : `Slack DM ${source.channelId}`;
|
|
43719
43751
|
build(input) {
|
|
43720
43752
|
if (input.channelId && input.threadTs) return { type: "slack", channelId: input.channelId, threadTs: input.threadTs };
|
|
43721
|
-
if (
|
|
43753
|
+
if (input.channelId?.startsWith("D")) return { type: "slack", channelId: input.channelId };
|
|
43754
|
+
if (input.channelId) return { error: "threadTs is required unless channelId is a Slack DM ID" };
|
|
43755
|
+
if (!input.threadUrl) return { error: "channelId or threadUrl is required for a slack monitor" };
|
|
43722
43756
|
let url2;
|
|
43723
43757
|
try {
|
|
43724
43758
|
url2 = new URL(input.threadUrl);
|
|
@@ -43817,7 +43851,7 @@ function buildServer(daemon) {
|
|
|
43817
43851
|
server.registerTool(
|
|
43818
43852
|
"monitor_create",
|
|
43819
43853
|
{
|
|
43820
|
-
description: "Create or reuse a monitor for a Jira issue, GitHub pull request, or Slack
|
|
43854
|
+
description: "Create or reuse a monitor for a Jira issue, GitHub pull request, Slack thread, or Slack DM.",
|
|
43821
43855
|
inputSchema: createSchema,
|
|
43822
43856
|
annotations: { idempotentHint: true, readOnlyHint: false }
|
|
43823
43857
|
},
|
package/dist/daemon.js
CHANGED
|
@@ -11010,7 +11010,7 @@ function safeParse24(schema, input, config$1) {
|
|
|
11010
11010
|
var SlackSourceSchema = /* @__PURE__ */ object23({
|
|
11011
11011
|
type: /* @__PURE__ */ literal3("slack"),
|
|
11012
11012
|
channelId: /* @__PURE__ */ string24(),
|
|
11013
|
-
threadTs: /* @__PURE__ */ string24()
|
|
11013
|
+
threadTs: /* @__PURE__ */ optional22(/* @__PURE__ */ string24())
|
|
11014
11014
|
});
|
|
11015
11015
|
var SlackCursorSchema = /* @__PURE__ */ object23({
|
|
11016
11016
|
primed: /* @__PURE__ */ boolean23(),
|
|
@@ -11027,7 +11027,7 @@ function messageAt(ts) {
|
|
|
11027
11027
|
if (value > 0) return new Date(value * 1e3).toISOString();
|
|
11028
11028
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
11029
11029
|
}
|
|
11030
|
-
function parseSlackReadResult(result, cursorRaw) {
|
|
11030
|
+
function parseSlackReadResult(result, cursorRaw, location = "thread") {
|
|
11031
11031
|
const parsedCursor = /* @__PURE__ */ safeParse24(SlackCursorSchema, cursorRaw);
|
|
11032
11032
|
let previous = {};
|
|
11033
11033
|
if (parsedCursor.success) previous = parsedCursor.output;
|
|
@@ -11055,7 +11055,7 @@ function parseSlackReadResult(result, cursorRaw) {
|
|
|
11055
11055
|
kind: "message",
|
|
11056
11056
|
id: `message:${message.ts}`,
|
|
11057
11057
|
at: messageAt(message.ts),
|
|
11058
|
-
summary: `Slack
|
|
11058
|
+
summary: `Slack ${location} message by ${author}`,
|
|
11059
11059
|
body: text,
|
|
11060
11060
|
actionable: true
|
|
11061
11061
|
};
|
|
@@ -11076,15 +11076,19 @@ function slackToken() {
|
|
|
11076
11076
|
return process.env.SOURCEFED_SLACK_TOKEN;
|
|
11077
11077
|
}
|
|
11078
11078
|
async function fetchSlackThread(channelId, threadTs) {
|
|
11079
|
+
return fetchSlackMessages("conversations.replies", { channel: channelId, ts: threadTs, inclusive: "false" });
|
|
11080
|
+
}
|
|
11081
|
+
async function fetchSlackDm(channelId) {
|
|
11082
|
+
return fetchSlackMessages("conversations.history", { channel: channelId });
|
|
11083
|
+
}
|
|
11084
|
+
async function fetchSlackMessages(method, params) {
|
|
11079
11085
|
const messages = [];
|
|
11080
11086
|
let cursor = "";
|
|
11081
11087
|
let received = false;
|
|
11082
11088
|
do {
|
|
11083
|
-
const page = await slackApi(
|
|
11084
|
-
|
|
11085
|
-
ts: threadTs,
|
|
11089
|
+
const page = await slackApi(method, {
|
|
11090
|
+
...params,
|
|
11086
11091
|
limit: "1000",
|
|
11087
|
-
inclusive: "false",
|
|
11088
11092
|
...cursor ? { cursor } : {}
|
|
11089
11093
|
});
|
|
11090
11094
|
if (!page) break;
|
|
@@ -11108,8 +11112,7 @@ async function fetchSlackThread(channelId, threadTs) {
|
|
|
11108
11112
|
}
|
|
11109
11113
|
usersCursor = page.response_metadata?.next_cursor ?? "";
|
|
11110
11114
|
} while (usersCursor);
|
|
11111
|
-
|
|
11112
|
-
return result;
|
|
11115
|
+
return { messages, users };
|
|
11113
11116
|
}
|
|
11114
11117
|
async function slackApi(method, params) {
|
|
11115
11118
|
const token = slackToken();
|
|
@@ -11134,9 +11137,9 @@ async function slackApi(method, params) {
|
|
|
11134
11137
|
}
|
|
11135
11138
|
}
|
|
11136
11139
|
async function pollSlack(source, cursorRaw) {
|
|
11137
|
-
const result = await fetchSlackThread(source.channelId, source.threadTs);
|
|
11140
|
+
const result = source.threadTs ? await fetchSlackThread(source.channelId, source.threadTs) : await fetchSlackDm(source.channelId);
|
|
11138
11141
|
if (!result) throw new Error("slack API request failed; check SOURCEFED_SLACK_TOKEN");
|
|
11139
|
-
const parsed = parseSlackReadResult(result, cursorRaw);
|
|
11142
|
+
const parsed = parseSlackReadResult(result, cursorRaw, source.threadTs ? "thread" : "DM");
|
|
11140
11143
|
return { ...parsed, terminal: false };
|
|
11141
11144
|
}
|
|
11142
11145
|
var slackPollMonitor = new PollMonitor3({
|
|
@@ -11153,7 +11156,8 @@ function parseSlackWebhook(payload, _eventName, deliveryId) {
|
|
|
11153
11156
|
const event = payload.event;
|
|
11154
11157
|
const channelId = event?.channel;
|
|
11155
11158
|
const threadTs = event?.thread_ts;
|
|
11156
|
-
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId
|
|
11159
|
+
if (payload.type !== "event_callback" || event?.type !== "message" || !channelId) return void 0;
|
|
11160
|
+
if (!threadTs && !channelId.startsWith("D")) return void 0;
|
|
11157
11161
|
let text = "[message without text]";
|
|
11158
11162
|
if (event.text?.trim()) text = event.text.trim();
|
|
11159
11163
|
let author = event.username;
|
|
@@ -11162,12 +11166,14 @@ function parseSlackWebhook(payload, _eventName, deliveryId) {
|
|
|
11162
11166
|
if (!author) author = "someone";
|
|
11163
11167
|
let at = messageAt(event.event_ts);
|
|
11164
11168
|
if (!event.event_ts) at = messageAt(event.ts);
|
|
11169
|
+
const source = threadTs ? { type: "slack", channelId, threadTs } : { type: "slack", channelId };
|
|
11170
|
+
const location = threadTs ? "thread" : "DM";
|
|
11165
11171
|
return {
|
|
11166
|
-
source
|
|
11172
|
+
source,
|
|
11167
11173
|
kind: "message",
|
|
11168
11174
|
id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
|
|
11169
11175
|
at,
|
|
11170
|
-
summary: `Slack
|
|
11176
|
+
summary: `Slack ${location} message by ${author}`,
|
|
11171
11177
|
body: `${text}
|
|
11172
11178
|
|
|
11173
11179
|
Slack event: ${deliveryId}`,
|
|
@@ -11220,14 +11226,16 @@ var SlackMonitor = class extends Monitor3 {
|
|
|
11220
11226
|
constructor() {
|
|
11221
11227
|
super({ type: "slack", schema: SlackSourceSchema });
|
|
11222
11228
|
}
|
|
11223
|
-
key = (source) => `slack:${source.channelId}#${source.threadTs}`;
|
|
11229
|
+
key = (source) => source.threadTs ? `slack:${source.channelId}#${source.threadTs}` : `slack:${source.channelId}`;
|
|
11224
11230
|
icon = "\u{F04B1}";
|
|
11225
|
-
label = () => "thread";
|
|
11231
|
+
label = (source) => source.threadTs ? "thread" : "DM";
|
|
11226
11232
|
detail = (source) => ` ${source.channelId}`;
|
|
11227
|
-
describe = (source) => `Slack ${source.channelId} thread ${source.threadTs}`;
|
|
11233
|
+
describe = (source) => source.threadTs ? `Slack ${source.channelId} thread ${source.threadTs}` : `Slack DM ${source.channelId}`;
|
|
11228
11234
|
build(input) {
|
|
11229
11235
|
if (input.channelId && input.threadTs) return { type: "slack", channelId: input.channelId, threadTs: input.threadTs };
|
|
11230
|
-
if (
|
|
11236
|
+
if (input.channelId?.startsWith("D")) return { type: "slack", channelId: input.channelId };
|
|
11237
|
+
if (input.channelId) return { error: "threadTs is required unless channelId is a Slack DM ID" };
|
|
11238
|
+
if (!input.threadUrl) return { error: "channelId or threadUrl is required for a slack monitor" };
|
|
11231
11239
|
let url;
|
|
11232
11240
|
try {
|
|
11233
11241
|
url = new URL(input.threadUrl);
|
|
@@ -11622,6 +11630,7 @@ var SourcefedDaemon = class {
|
|
|
11622
11630
|
if (!monitor) return { ok: false, error: `monitor ${id} was not found for this target` };
|
|
11623
11631
|
const stopped = await this.service.stop(id);
|
|
11624
11632
|
if ("error" in stopped) return { ok: false, error: stopped.error };
|
|
11633
|
+
await this.purgeMonitorEvents(monitor.target, id);
|
|
11625
11634
|
return { ok: true, monitor: monitorView(stopped) };
|
|
11626
11635
|
}
|
|
11627
11636
|
async startMonitor(target, id) {
|
|
@@ -11635,6 +11644,7 @@ var SourcefedDaemon = class {
|
|
|
11635
11644
|
const monitor = await ownedMonitor(this.service, id, target);
|
|
11636
11645
|
if (!monitor) return { ok: false, error: `monitor ${id} was not found for this target` };
|
|
11637
11646
|
await this.service.remove(id);
|
|
11647
|
+
await this.purgeMonitorEvents(monitor.target, id);
|
|
11638
11648
|
return { ok: true, removed: true };
|
|
11639
11649
|
}
|
|
11640
11650
|
async readEvents(target) {
|
|
@@ -11708,6 +11718,8 @@ var SourcefedDaemon = class {
|
|
|
11708
11718
|
async deliver(key, target, listeners) {
|
|
11709
11719
|
const queued = await this.queue.read(target);
|
|
11710
11720
|
if (queued.length === 0) return;
|
|
11721
|
+
const enabled = new Set((await this.service.list()).filter((monitor) => monitor.enabled).map((monitor) => monitor.id));
|
|
11722
|
+
await this.dropQueued(target, queued.filter((event) => !enabled.has(event.monitorID)).map((event) => event.id));
|
|
11711
11723
|
let receivedByListener = this.received.get(key);
|
|
11712
11724
|
if (!receivedByListener) {
|
|
11713
11725
|
receivedByListener = /* @__PURE__ */ new Map();
|
|
@@ -11717,7 +11729,7 @@ var SourcefedDaemon = class {
|
|
|
11717
11729
|
for (const received of receivedByListener.values()) {
|
|
11718
11730
|
for (const id of received) claimed.add(id);
|
|
11719
11731
|
}
|
|
11720
|
-
const fresh = queued.filter((event) => !claimed.has(event.id));
|
|
11732
|
+
const fresh = queued.filter((event) => enabled.has(event.monitorID) && !claimed.has(event.id));
|
|
11721
11733
|
if (fresh.length === 0) return;
|
|
11722
11734
|
for (const listener of [...listeners]) {
|
|
11723
11735
|
let received = receivedByListener.get(listener);
|
|
@@ -11736,6 +11748,20 @@ var SourcefedDaemon = class {
|
|
|
11736
11748
|
}
|
|
11737
11749
|
}
|
|
11738
11750
|
}
|
|
11751
|
+
async dropQueued(target, eventIDs) {
|
|
11752
|
+
if (eventIDs.length === 0) return;
|
|
11753
|
+
await this.queue.acknowledge(target, eventIDs);
|
|
11754
|
+
const receivedByListener = this.received.get(targetKey(target));
|
|
11755
|
+
if (!receivedByListener) return;
|
|
11756
|
+
const ids = new Set(eventIDs);
|
|
11757
|
+
for (const received of receivedByListener.values()) {
|
|
11758
|
+
for (const id of ids) received.delete(id);
|
|
11759
|
+
}
|
|
11760
|
+
}
|
|
11761
|
+
async purgeMonitorEvents(target, monitorID) {
|
|
11762
|
+
const queued = await this.queue.read(target);
|
|
11763
|
+
await this.dropQueued(target, queued.filter((event) => event.monitorID === monitorID).map((event) => event.id));
|
|
11764
|
+
}
|
|
11739
11765
|
};
|
|
11740
11766
|
function monitorView(monitor) {
|
|
11741
11767
|
const definition = sourceDefinition(monitor.source);
|