@fdcn/sourcefed 0.2.7 → 0.2.9

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 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.2.7"] }
36
+ { "plugin": ["@fdcn/sourcefed@0.2.9"] }
37
37
  ```
38
38
 
39
39
  ```json
40
40
  // tui.json
41
- { "plugin": ["@fdcn/sourcefed@0.2.7"] }
41
+ { "plugin": ["@fdcn/sourcefed@0.2.9"] }
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 thread message by ${author}`,
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("conversations.replies", {
11082
- channel: channelId,
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
- const result = { messages, users };
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 || !threadTs) return void 0;
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: { type: "slack", channelId, threadTs },
11170
+ source,
11165
11171
  kind: "message",
11166
11172
  id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
11167
11173
  at,
11168
- summary: `Slack thread message by ${author}`,
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 (!input.threadUrl) return { error: "channelId + threadTs or threadUrl is required for a slack monitor" };
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);
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 thread message by ${author}`,
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("conversations.replies", {
11088
- channel: channelId,
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
- const result = { messages, users };
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 || !threadTs) return void 0;
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: { type: "slack", channelId, threadTs },
11176
+ source,
11171
11177
  kind: "message",
11172
11178
  id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
11173
11179
  at,
11174
- summary: `Slack thread message by ${author}`,
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 (!input.threadUrl) return { error: "channelId + threadTs or threadUrl is required for a slack monitor" };
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);
@@ -43501,7 +43509,7 @@ function safeParse242(schema, input, config$1) {
43501
43509
  var SlackSourceSchema2 = /* @__PURE__ */ object232({
43502
43510
  type: /* @__PURE__ */ literal32("slack"),
43503
43511
  channelId: /* @__PURE__ */ string242(),
43504
- threadTs: /* @__PURE__ */ string242()
43512
+ threadTs: /* @__PURE__ */ optional2222(/* @__PURE__ */ string242())
43505
43513
  });
43506
43514
  var SlackCursorSchema2 = /* @__PURE__ */ object232({
43507
43515
  primed: /* @__PURE__ */ boolean232(),
@@ -43518,7 +43526,7 @@ function messageAt2(ts) {
43518
43526
  if (value > 0) return new Date(value * 1e3).toISOString();
43519
43527
  return (/* @__PURE__ */ new Date()).toISOString();
43520
43528
  }
43521
- function parseSlackReadResult2(result, cursorRaw) {
43529
+ function parseSlackReadResult2(result, cursorRaw, location = "thread") {
43522
43530
  const parsedCursor = /* @__PURE__ */ safeParse242(SlackCursorSchema2, cursorRaw);
43523
43531
  let previous = {};
43524
43532
  if (parsedCursor.success) previous = parsedCursor.output;
@@ -43546,7 +43554,7 @@ function parseSlackReadResult2(result, cursorRaw) {
43546
43554
  kind: "message",
43547
43555
  id: `message:${message.ts}`,
43548
43556
  at: messageAt2(message.ts),
43549
- summary: `Slack thread message by ${author}`,
43557
+ summary: `Slack ${location} message by ${author}`,
43550
43558
  body: text,
43551
43559
  actionable: true
43552
43560
  };
@@ -43567,15 +43575,19 @@ function slackToken2() {
43567
43575
  return process.env.SOURCEFED_SLACK_TOKEN;
43568
43576
  }
43569
43577
  async function fetchSlackThread2(channelId, threadTs) {
43578
+ return fetchSlackMessages2("conversations.replies", { channel: channelId, ts: threadTs, inclusive: "false" });
43579
+ }
43580
+ async function fetchSlackDm2(channelId) {
43581
+ return fetchSlackMessages2("conversations.history", { channel: channelId });
43582
+ }
43583
+ async function fetchSlackMessages2(method, params) {
43570
43584
  const messages = [];
43571
43585
  let cursor = "";
43572
43586
  let received = false;
43573
43587
  do {
43574
- const page = await slackApi2("conversations.replies", {
43575
- channel: channelId,
43576
- ts: threadTs,
43588
+ const page = await slackApi2(method, {
43589
+ ...params,
43577
43590
  limit: "1000",
43578
- inclusive: "false",
43579
43591
  ...cursor ? { cursor } : {}
43580
43592
  });
43581
43593
  if (!page) break;
@@ -43599,8 +43611,7 @@ async function fetchSlackThread2(channelId, threadTs) {
43599
43611
  }
43600
43612
  usersCursor = page.response_metadata?.next_cursor ?? "";
43601
43613
  } while (usersCursor);
43602
- const result = { messages, users };
43603
- return result;
43614
+ return { messages, users };
43604
43615
  }
43605
43616
  async function slackApi2(method, params) {
43606
43617
  const token = slackToken2();
@@ -43625,9 +43636,9 @@ async function slackApi2(method, params) {
43625
43636
  }
43626
43637
  }
43627
43638
  async function pollSlack2(source, cursorRaw) {
43628
- const result = await fetchSlackThread2(source.channelId, source.threadTs);
43639
+ const result = source.threadTs ? await fetchSlackThread2(source.channelId, source.threadTs) : await fetchSlackDm2(source.channelId);
43629
43640
  if (!result) throw new Error("slack API request failed; check SOURCEFED_SLACK_TOKEN");
43630
- const parsed = parseSlackReadResult2(result, cursorRaw);
43641
+ const parsed = parseSlackReadResult2(result, cursorRaw, source.threadTs ? "thread" : "DM");
43631
43642
  return { ...parsed, terminal: false };
43632
43643
  }
43633
43644
  var slackPollMonitor2 = new PollMonitor32({
@@ -43644,7 +43655,8 @@ function parseSlackWebhook2(payload, _eventName, deliveryId) {
43644
43655
  const event = payload.event;
43645
43656
  const channelId = event?.channel;
43646
43657
  const threadTs = event?.thread_ts;
43647
- if (payload.type !== "event_callback" || event?.type !== "message" || !channelId || !threadTs) return void 0;
43658
+ if (payload.type !== "event_callback" || event?.type !== "message" || !channelId) return void 0;
43659
+ if (!threadTs && !channelId.startsWith("D")) return void 0;
43648
43660
  let text = "[message without text]";
43649
43661
  if (event.text?.trim()) text = event.text.trim();
43650
43662
  let author = event.username;
@@ -43653,12 +43665,14 @@ function parseSlackWebhook2(payload, _eventName, deliveryId) {
43653
43665
  if (!author) author = "someone";
43654
43666
  let at = messageAt2(event.event_ts);
43655
43667
  if (!event.event_ts) at = messageAt2(event.ts);
43668
+ const source = threadTs ? { type: "slack", channelId, threadTs } : { type: "slack", channelId };
43669
+ const location = threadTs ? "thread" : "DM";
43656
43670
  return {
43657
- source: { type: "slack", channelId, threadTs },
43671
+ source,
43658
43672
  kind: "message",
43659
43673
  id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
43660
43674
  at,
43661
- summary: `Slack thread message by ${author}`,
43675
+ summary: `Slack ${location} message by ${author}`,
43662
43676
  body: `${text}
43663
43677
 
43664
43678
  Slack event: ${deliveryId}`,
@@ -43711,14 +43725,16 @@ var SlackMonitor2 = class extends Monitor32 {
43711
43725
  constructor() {
43712
43726
  super({ type: "slack", schema: SlackSourceSchema2 });
43713
43727
  }
43714
- key = (source) => `slack:${source.channelId}#${source.threadTs}`;
43728
+ key = (source) => source.threadTs ? `slack:${source.channelId}#${source.threadTs}` : `slack:${source.channelId}`;
43715
43729
  icon = "\u{F04B1}";
43716
- label = () => "thread";
43730
+ label = (source) => source.threadTs ? "thread" : "DM";
43717
43731
  detail = (source) => ` ${source.channelId}`;
43718
- describe = (source) => `Slack ${source.channelId} thread ${source.threadTs}`;
43732
+ describe = (source) => source.threadTs ? `Slack ${source.channelId} thread ${source.threadTs}` : `Slack DM ${source.channelId}`;
43719
43733
  build(input) {
43720
43734
  if (input.channelId && input.threadTs) return { type: "slack", channelId: input.channelId, threadTs: input.threadTs };
43721
- if (!input.threadUrl) return { error: "channelId + threadTs or threadUrl is required for a slack monitor" };
43735
+ if (input.channelId?.startsWith("D")) return { type: "slack", channelId: input.channelId };
43736
+ if (input.channelId) return { error: "threadTs is required unless channelId is a Slack DM ID" };
43737
+ if (!input.threadUrl) return { error: "channelId or threadUrl is required for a slack monitor" };
43722
43738
  let url2;
43723
43739
  try {
43724
43740
  url2 = new URL(input.threadUrl);
@@ -43817,7 +43833,7 @@ function buildServer(daemon) {
43817
43833
  server.registerTool(
43818
43834
  "monitor_create",
43819
43835
  {
43820
- description: "Create or reuse a monitor for a Jira issue, GitHub pull request, or Slack thread.",
43836
+ description: "Create or reuse a monitor for a Jira issue, GitHub pull request, Slack thread, or Slack DM.",
43821
43837
  inputSchema: createSchema,
43822
43838
  annotations: { idempotentHint: true, readOnlyHint: false }
43823
43839
  },
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 thread message by ${author}`,
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("conversations.replies", {
11084
- channel: channelId,
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
- const result = { messages, users };
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 || !threadTs) return void 0;
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: { type: "slack", channelId, threadTs },
11172
+ source,
11167
11173
  kind: "message",
11168
11174
  id: event.ts === void 0 ? void 0 : `message:${event.ts}`,
11169
11175
  at,
11170
- summary: `Slack thread message by ${author}`,
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 (!input.threadUrl) return { error: "channelId + threadTs or threadUrl is required for a slack monitor" };
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);
@@ -6,7 +6,7 @@ description: Core sourcefed usage guide. Read this before creating monitors. Cov
6
6
  # Sourcefed core
7
7
 
8
8
  Sourcefed lets you create **monitors** that watch a Jira issue, GitHub PR, or
9
- Slack thread and route NEW events into the session you're currently working in,
9
+ Slack thread or direct-message conversation 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
@@ -20,13 +20,15 @@ affect that session's monitors.
20
20
  - **Jira issue**: `monitor_create({ name: "PROJ-12345", sourceType: "jira", issueKey: "PROJ-12345" })`
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
+ - **Whole Slack DM**: `monitor_create({ name: "DM with Alex", sourceType: "slack", channelId: "D0123456789" })`
23
24
 
24
25
  You will be notified of:
25
26
 
26
27
  - **Jira**: new comments, description edits, and summary/status/assignee/priority/label changes.
27
28
  - **GitHub**: new reviews (including bot reviews), PR and line-level comments,
28
29
  CI failures, merge conflicts, and the PR being merged or closed.
29
- - **Slack**: new replies in the thread. Slack monitors are always detect-only
30
+ - **Slack thread**: new replies in the thread.
31
+ - **Slack DM**: new top-level messages in the DM. Slack monitors are always detect-only
30
32
  and never reply.
31
33
 
32
34
  GitHub and Slack use webhook delivery when configured and fall back to polling
@@ -30,8 +30,8 @@ sourcefed skills path [name] Print skill directory path
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
- --channel-id ID Slack channel (slack)
34
- --thread-ts TS Slack parent message timestamp (slack)
33
+ --channel-id ID Slack channel or DM ID; a D… ID alone monitors the whole DM
34
+ --thread-ts TS Slack parent message timestamp; omit for a whole-DM monitor
35
35
  --thread-url URL Slack thread URL (slack)
36
36
  --poll-interval-sec N Poll interval in seconds (min 15)
37
37
  --target-kind KIND Target kind for ownership scoping (default: cli)
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: sourcefed
3
- description: Live Jira, GitHub, and Slack monitoring for agent hosts. Create monitors that route new issue comments, PR reviews, CI failures, and Slack thread messages into the current session as they happen.
3
+ description: Live Jira, GitHub, and Slack monitoring for agent hosts. Create monitors that route new issue comments, PR reviews, CI failures, and Slack thread or DM messages 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, or Slack thread and NEW events are routed
10
+ monitor for a Jira issue, GitHub PR, Slack thread, or Slack DM 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
@@ -24,8 +24,8 @@ releases, which is why it just points at `skills get core`.
24
24
 
25
25
  ## When to load this skill
26
26
 
27
- - A user asks to monitor a Jira ticket, GitHub PR, or Slack thread.
28
- - You found or are working on a Jira ticket, PR, or thread and want new
27
+ - A user asks to monitor a Jira ticket, GitHub PR, Slack thread, or Slack DM.
28
+ - You found or are working on a Jira ticket, PR, thread, or DM and want new
29
29
  activity routed into your session.
30
30
  - You opened a PR and want to be notified of reviews, comments, CI failures,
31
31
  merge conflicts, or the merge itself.
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  decodeTarget,
3
3
  encodeTarget
4
- } from "./chunk-DK3LZ5RU.js";
4
+ } from "./chunk-QHT4DCW7.js";
5
5
  import {
6
6
  __export
7
7
  } from "./chunk-MLKGABMK.js";
@@ -4693,7 +4693,7 @@ function buildServer(daemon) {
4693
4693
  server.registerTool(
4694
4694
  "monitor_create",
4695
4695
  {
4696
- description: "Create or reuse a monitor for a Jira issue, GitHub pull request, or Slack thread.",
4696
+ description: "Create or reuse a monitor for a Jira issue, GitHub pull request, Slack thread, or Slack DM.",
4697
4697
  inputSchema: createSchema,
4698
4698
  annotations: { idempotentHint: true, readOnlyHint: false }
4699
4699
  },
package/dist/opencode.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  daemonCommand,
5
5
  daemonEnvironment,
6
6
  spawnLocalDaemon
7
- } from "./chunk-DK3LZ5RU.js";
7
+ } from "./chunk-QHT4DCW7.js";
8
8
  import {
9
9
  __export
10
10
  } from "./chunk-MLKGABMK.js";
@@ -12555,15 +12555,15 @@ async function callMonitorTool(name, arguments_, sessionID) {
12555
12555
 
12556
12556
  // adapters/opencode/tools/monitor_create.ts
12557
12557
  var definition = tool({
12558
- description: "Create a detect-only monitor that watches a Jira issue, GitHub PR, or Slack thread 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 uses polling.",
12558
+ description: "Create a detect-only monitor that watches a Jira issue, GitHub PR, Slack thread, or Slack DM 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 uses polling.",
12559
12559
  args: {
12560
12560
  name: tool.schema.string().describe("Human-readable monitor name, e.g. 'PROJ-12345', 'PR #42'"),
12561
12561
  sourceType: tool.schema.enum(SOURCE_TYPES).describe("What to watch"),
12562
12562
  issueKey: tool.schema.string().describe("Jira issue key, e.g. PROJ-12345 (required if sourceType=jira)").optional(),
12563
12563
  repo: tool.schema.string().describe("GitHub owner/name, e.g. owner/repository (required if sourceType=github)").optional(),
12564
12564
  prNumber: tool.schema.number().describe("GitHub PR number (required if sourceType=github)").optional(),
12565
- channelId: tool.schema.string().describe("Slack channel or DM ID (use with threadTs for sourceType=slack)").optional(),
12566
- threadTs: tool.schema.string().describe("Slack parent message timestamp (use with channelId for sourceType=slack)").optional(),
12565
+ channelId: tool.schema.string().describe("Slack channel or DM ID. A DM ID without threadTs monitors the whole DM conversation.").optional(),
12566
+ threadTs: tool.schema.string().describe("Slack parent message timestamp. Omit it with a DM ID to monitor the whole DM conversation.").optional(),
12567
12567
  threadUrl: tool.schema.string().describe("Slack thread URL (alternative to channelId + threadTs)").optional(),
12568
12568
  pollIntervalSec: tool.schema.number().describe("Optional polling interval in seconds (min 15, default 60)").optional()
12569
12569
  },
package/dist/pi.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  daemonCommand,
4
4
  daemonEnvironment,
5
5
  spawnLocalDaemon
6
- } from "./chunk-DK3LZ5RU.js";
6
+ } from "./chunk-QHT4DCW7.js";
7
7
  import "./chunk-MLKGABMK.js";
8
8
 
9
9
  // adapters/pi/src/index.ts
@@ -92,6 +92,7 @@ async function sourcefedExtension(pi) {
92
92
  let statusTimer;
93
93
  let statusCtx;
94
94
  pi.on("session_start", async (_event, ctx) => {
95
+ if (statusTimer) clearInterval(statusTimer);
95
96
  statusCtx = ctx;
96
97
  await ensureTarget(ctx);
97
98
  void refreshStatus();
@@ -109,7 +110,7 @@ async function sourcefedExtension(pi) {
109
110
  pi.registerTool({
110
111
  name: "sourcefed_monitor_create",
111
112
  label: "Sourcefed monitor create",
112
- description: "Create or reuse a Sourcefed monitor for the current Pi session.",
113
+ description: "Create or reuse a Jira, GitHub PR, Slack thread, or Slack DM monitor for the current Pi session.",
113
114
  parameters: Type.Object({
114
115
  name: Type.String(),
115
116
  sourceType: Type.Unsafe({ type: "string", enum: ["jira", "github", "slack"] }),
@@ -174,6 +175,14 @@ async function sourcefedExtension(pi) {
174
175
  async function refreshStatus() {
175
176
  const ctx = statusCtx;
176
177
  if (!ctx) return;
178
+ try {
179
+ await updateStatus(ctx);
180
+ } catch {
181
+ if (statusTimer) clearInterval(statusTimer);
182
+ statusTimer = void 0;
183
+ }
184
+ }
185
+ async function updateStatus(ctx) {
177
186
  const result = await call(ctx, "monitor_list", {});
178
187
  const monitors = Array.isArray(result?.monitors) ? result.monitors : [];
179
188
  const active = monitors.filter((monitor) => monitor.enabled);
@@ -102,13 +102,14 @@ function MonitorRows(props) {
102
102
  },
103
103
  get children() {
104
104
  return visible().map((monitor) => (() => {
105
- var _el$11 = _$createElement("box"), _el$12 = _$createElement("text"), _el$13 = _$createElement("text");
105
+ var _el$11 = _$createElement("box"), _el$12 = _$createElement("text"), _el$13 = _$createElement("text"), _el$14 = _$createTextNode(` `);
106
106
  _$insertNode(_el$11, _el$12);
107
107
  _$insertNode(_el$11, _el$13);
108
108
  _$setProp(_el$11, "flexDirection", "row");
109
109
  _$setProp(_el$11, "width", "100%");
110
110
  _$insert(_el$12, () => monitor.icon);
111
- _$insert(_el$13, () => monitor.detail);
111
+ _$insertNode(_el$13, _el$14);
112
+ _$insert(_el$13, () => monitor.name, null);
112
113
  _$effect((_p$) => {
113
114
  var _v$3 = monitorTone(monitor, props.theme), _v$4 = props.theme.textMuted;
114
115
  _v$3 !== _p$.e && (_p$.e = _$setProp(_el$12, "fg", _v$3, _p$.e));
@@ -8,7 +8,7 @@ export declare class SlackMonitor extends Monitor<SlackSourceRecord> {
8
8
  constructor();
9
9
  readonly key: (source: SlackSourceRecord) => string;
10
10
  readonly icon = "\uDB81\uDCB1";
11
- readonly label: () => string;
11
+ readonly label: (source: SlackSourceRecord) => string;
12
12
  readonly detail: (source: SlackSourceRecord) => string;
13
13
  readonly describe: (source: SlackSourceRecord) => string;
14
14
  build(input: MonitorCreateInput): SourceInput;
@@ -1,5 +1,5 @@
1
1
  import type { SlackCursor, SlackEvent, SlackReadResult } from "../../types";
2
- export declare function parseSlackReadResult(result: SlackReadResult, cursorRaw: unknown): {
2
+ export declare function parseSlackReadResult(result: SlackReadResult, cursorRaw: unknown, location?: "thread" | "DM"): {
3
3
  events: SlackEvent[];
4
4
  cursor: SlackCursor;
5
5
  };
@@ -2,3 +2,7 @@ export declare function fetchSlackThread(channelId: string, threadTs: string): P
2
2
  messages: any[];
3
3
  users: any[];
4
4
  } | undefined>;
5
+ export declare function fetchSlackDm(channelId: string): Promise<{
6
+ messages: any[];
7
+ users: any[];
8
+ } | undefined>;
@@ -2,7 +2,7 @@ import * as v from "valibot";
2
2
  export declare const SlackSourceSchema: v.ObjectSchema<{
3
3
  readonly type: v.LiteralSchema<"slack", undefined>;
4
4
  readonly channelId: v.StringSchema<undefined>;
5
- readonly threadTs: v.StringSchema<undefined>;
5
+ readonly threadTs: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
6
6
  }, undefined>;
7
7
  export declare const SlackCursorSchema: v.ObjectSchema<{
8
8
  readonly primed: v.BooleanSchema<undefined>;
@@ -1,5 +1,5 @@
1
1
  export type SlackSourceRecord = {
2
2
  type: "slack";
3
3
  channelId: string;
4
- threadTs: string;
4
+ threadTs?: string;
5
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fdcn/sourcefed",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Live Jira, GitHub, and Slack monitoring for agent hosts: CLI, daemon, MCP server, and host plugins.",
5
5
  "license": "MIT",
6
6
  "type": "module",