@fdcn/sourcefed 0.2.1 → 0.2.3

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.
Files changed (38) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-7KHZCWGQ.js +5174 -0
  3. package/dist/chunk-EVSHVYPF.js +5167 -0
  4. package/dist/chunk-UOTCX4ME.js +3963 -0
  5. package/dist/cli.js +494 -362
  6. package/dist/core.js +5 -0
  7. package/dist/daemon.js +20 -1
  8. package/dist/data/core/SKILL.md +10 -0
  9. package/dist/data/core/references/commands.md +1 -0
  10. package/dist/mcp.js +1 -1
  11. package/dist/opencode.js +1 -1
  12. package/dist/pi.js +22 -3
  13. package/dist/tui-compiled.mjs +146 -101
  14. package/dist.dts/packages/cli/src/commands/daemon.d.ts +1 -0
  15. package/dist.dts/packages/cli/src/commands/mcp.d.ts +1 -0
  16. package/dist.dts/packages/cli/src/commands/monitor/ack.d.ts +2 -0
  17. package/dist.dts/packages/cli/src/commands/monitor/create.d.ts +2 -0
  18. package/dist.dts/packages/cli/src/commands/monitor/events.d.ts +2 -0
  19. package/dist.dts/packages/cli/src/commands/monitor/follow.d.ts +2 -0
  20. package/dist.dts/packages/cli/src/commands/monitor/index.d.ts +10 -0
  21. package/dist.dts/packages/cli/src/commands/monitor/list.d.ts +2 -0
  22. package/dist.dts/packages/cli/src/commands/monitor/logs.d.ts +2 -0
  23. package/dist.dts/packages/cli/src/commands/monitor/remove.d.ts +2 -0
  24. package/dist.dts/packages/cli/src/commands/monitor/sources.d.ts +2 -0
  25. package/dist.dts/packages/cli/src/commands/monitor/start.d.ts +2 -0
  26. package/dist.dts/packages/cli/src/commands/monitor/status.d.ts +2 -0
  27. package/dist.dts/packages/cli/src/commands/monitor/stop.d.ts +2 -0
  28. package/dist.dts/packages/cli/src/commands/monitor.d.ts +1 -0
  29. package/dist.dts/packages/cli/src/commands/setup.d.ts +1 -0
  30. package/dist.dts/packages/cli/src/follow.test.d.ts +1 -0
  31. package/dist.dts/packages/cli/src/utils/daemon.d.ts +2 -0
  32. package/dist.dts/packages/cli/src/utils/flags.d.ts +1 -0
  33. package/dist.dts/packages/cli/src/utils/format.d.ts +8 -0
  34. package/dist.dts/packages/cli/src/utils/net.d.ts +1 -0
  35. package/dist.dts/packages/cli/src/utils/setup.d.ts +2 -0
  36. package/dist.dts/packages/cli/src/utils/webhooks.d.ts +2 -0
  37. package/dist.dts/packages/daemon/src/types/daemon.d.ts +2 -0
  38. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/cli/src/index.ts
4
- import os7 from "node:os";
5
4
  import { realpathSync } from "node:fs";
6
- import { fileURLToPath as fileURLToPath2 } from "node:url";
7
- import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8
6
 
9
7
  // packages/daemon/dist/index.js
10
8
  import { link, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
@@ -116,9 +114,9 @@ function _isValidObjectKey(object$1, key) {
116
114
  }
117
115
  // @__NO_SIDE_EFFECTS__
118
116
  function _joinExpects(values$1, separator) {
119
- const list = [...new Set(values$1)];
120
- if (list.length > 1) return `(${list.join(` ${separator} `)})`;
121
- return list[0] ?? "never";
117
+ const list2 = [...new Set(values$1)];
118
+ if (list2.length > 1) return `(${list2.join(` ${separator} `)})`;
119
+ return list2[0] ?? "never";
122
120
  }
123
121
  var EMOJI_REGEX = new RegExp("^(?:[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|(?![\\p{Emoji_Modifier_Base}\\u{1F1E6}-\\u{1F1FF}])\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|(?![\\p{Emoji_Modifier_Base}\\u{1F1E6}-\\u{1F1FF}])\\p{Emoji_Presentation}))*)+$", "u");
124
122
  // @__NO_SIDE_EFFECTS__
@@ -566,6 +564,11 @@ var MonitorService = class {
566
564
  return this.transact((registry2) => {
567
565
  const monitor = registry2.monitors.find((entry) => entry.id === id);
568
566
  if (!monitor) return { error: `monitor ${id} was not found` };
567
+ const identity = monitorIdentity(monitor.target, monitor.source);
568
+ const duplicate = registry2.monitors.find(
569
+ (entry) => entry.id !== id && entry.enabled && monitorIdentity(entry.target, entry.source) === identity
570
+ );
571
+ if (duplicate) return { error: `monitor ${id} duplicates active monitor ${duplicate.id} for the same source` };
569
572
  monitor.enabled = true;
570
573
  monitor.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
571
574
  return monitor;
@@ -912,31 +915,31 @@ var JsonMonitorEventQueue = class {
912
915
  eventsFile;
913
916
  queue = Promise.resolve();
914
917
  async enqueue(input) {
915
- return this.transact((events) => {
918
+ return this.transact((events2) => {
916
919
  const queued = createQueuedMonitorEvent(input.monitor, input.event);
917
- const existing = events.find((event) => event.id === queued.id);
920
+ const existing = events2.find((event) => event.id === queued.id);
918
921
  if (existing) return existing;
919
- events.push(queued);
922
+ events2.push(queued);
920
923
  return queued;
921
924
  });
922
925
  }
923
926
  async read(target) {
924
- const events = await this.loadEvents();
925
- return events.filter((event) => sameTarget2(event.target, target));
927
+ const events2 = await this.loadEvents();
928
+ return events2.filter((event) => sameTarget2(event.target, target));
926
929
  }
927
930
  async acknowledge(target, eventIDs) {
928
- await this.transact((events) => {
931
+ await this.transact((events2) => {
929
932
  const ids = new Set(eventIDs);
930
- for (let index = events.length - 1; index >= 0; index -= 1) {
931
- if (sameTarget2(events[index].target, target) && ids.has(events[index].id)) events.splice(index, 1);
933
+ for (let index = events2.length - 1; index >= 0; index -= 1) {
934
+ if (sameTarget2(events2[index].target, target) && ids.has(events2[index].id)) events2.splice(index, 1);
932
935
  }
933
936
  });
934
937
  }
935
938
  async transact(operation) {
936
939
  const next = this.queue.then(async () => {
937
- const events = await this.loadEvents();
938
- const result = await operation(events);
939
- await this.saveEvents(events);
940
+ const events2 = await this.loadEvents();
941
+ const result = await operation(events2);
942
+ await this.saveEvents(events2);
940
943
  return result;
941
944
  });
942
945
  this.queue = next.then(() => void 0, () => void 0);
@@ -953,10 +956,10 @@ var JsonMonitorEventQueue = class {
953
956
  throw error2;
954
957
  }
955
958
  }
956
- async saveEvents(events) {
959
+ async saveEvents(events2) {
957
960
  await mkdir(this.stateDir, { recursive: true, mode: 448 });
958
961
  const temporary = `${this.eventsFile}.${process.pid}.tmp`;
959
- await writeFile(temporary, `${JSON.stringify(events, null, 2)}
962
+ await writeFile(temporary, `${JSON.stringify(events2, null, 2)}
960
963
  `, { mode: 384 });
961
964
  await rename(temporary, this.eventsFile);
962
965
  }
@@ -2148,7 +2151,7 @@ async function confirmStillFailing(repo, prNumber, failed) {
2148
2151
  return /FAIL|ERROR|CANCEL/i.test(state);
2149
2152
  });
2150
2153
  }
2151
- async function appendGithubCiEvent(repo, prNumber, cursor, events, rollup) {
2154
+ async function appendGithubCiEvent(repo, prNumber, cursor, events2, rollup) {
2152
2155
  const ciState = rollup.map((check) => check.conclusion ?? check.state ?? "PENDING").sort().join(",");
2153
2156
  if (!ciState || ciState === cursor.ciState) return;
2154
2157
  const previousCiState = cursor.ciState;
@@ -2159,7 +2162,7 @@ async function appendGithubCiEvent(repo, prNumber, cursor, events, rollup) {
2159
2162
  const failureState = failed.map((check) => `${check.name ?? check.context ?? "check"}:${check.conclusion ?? check.state ?? "FAILURE"}`).sort().join(",");
2160
2163
  if (failed.length > 0 && failureState !== cursor.ciFailureState) {
2161
2164
  const names = failed.map((check) => check.name ?? check.context ?? "check").join(", ");
2162
- events.push({
2165
+ events2.push({
2163
2166
  kind: "ci",
2164
2167
  id: `ci:${failureState}`,
2165
2168
  repo,
@@ -2172,13 +2175,13 @@ async function appendGithubCiEvent(repo, prNumber, cursor, events, rollup) {
2172
2175
  }
2173
2176
  cursor.ciFailureState = failureState;
2174
2177
  }
2175
- function appendGithubConflictEvent(repo, prNumber, cursor, events, mergeable) {
2178
+ function appendGithubConflictEvent(repo, prNumber, cursor, events2, mergeable) {
2176
2179
  if (mergeable === cursor.mergeable) return;
2177
2180
  const previousMergeable = cursor.mergeable;
2178
2181
  cursor.mergeable = mergeable;
2179
2182
  if (previousMergeable === void 0) return;
2180
2183
  if (mergeable !== "CONFLICTING" && mergeable !== "DIRTY") return;
2181
- events.push({
2184
+ events2.push({
2182
2185
  kind: "conflict",
2183
2186
  id: `conflict:${mergeable}`,
2184
2187
  repo,
@@ -2188,7 +2191,7 @@ function appendGithubConflictEvent(repo, prNumber, cursor, events, mergeable) {
2188
2191
  actionable: true
2189
2192
  });
2190
2193
  }
2191
- function appendGithubReviews(repo, prNumber, cursor, events, reviews, historyPrimed) {
2194
+ function appendGithubReviews(repo, prNumber, cursor, events2, reviews, historyPrimed) {
2192
2195
  const ref = `#${prNumber}`;
2193
2196
  for (const review of reviews) {
2194
2197
  const id = "rev:" + String(review.id ?? `${review.author?.login}-${review.submittedAt}`);
@@ -2199,7 +2202,7 @@ function appendGithubReviews(repo, prNumber, cursor, events, reviews, historyPri
2199
2202
  let actionable = false;
2200
2203
  if (review.state === "CHANGES_REQUESTED") actionable = true;
2201
2204
  if (review.state === "COMMENTED" && review.body?.trim()) actionable = true;
2202
- events.push({
2205
+ events2.push({
2203
2206
  kind: "review",
2204
2207
  id,
2205
2208
  repo,
@@ -2211,7 +2214,7 @@ function appendGithubReviews(repo, prNumber, cursor, events, reviews, historyPri
2211
2214
  });
2212
2215
  }
2213
2216
  }
2214
- function appendGithubStateEvent(repo, prNumber, cursor, events, prState) {
2217
+ function appendGithubStateEvent(repo, prNumber, cursor, events2, prState) {
2215
2218
  const isTerminal = prState === "MERGED" || prState === "CLOSED";
2216
2219
  if (prState === cursor.prState) return isTerminal;
2217
2220
  const previousPrState = cursor.prState;
@@ -2219,7 +2222,7 @@ function appendGithubStateEvent(repo, prNumber, cursor, events, prState) {
2219
2222
  if (previousPrState !== void 0 && isTerminal) {
2220
2223
  let summary = `PR #${prNumber} was CLOSED without merging`;
2221
2224
  if (prState === "MERGED") summary = `PR #${prNumber} was MERGED`;
2222
- events.push({
2225
+ events2.push({
2223
2226
  kind: "merged",
2224
2227
  id: `state:${prState}`,
2225
2228
  repo,
@@ -2232,14 +2235,14 @@ function appendGithubStateEvent(repo, prNumber, cursor, events, prState) {
2232
2235
  }
2233
2236
  return isTerminal;
2234
2237
  }
2235
- function appendComments(repo, prNumber, cursor, events, comments, idPrefix, historyPrimed, summary, body) {
2238
+ function appendComments(repo, prNumber, cursor, events2, comments, idPrefix, historyPrimed, summary, body) {
2236
2239
  for (const comment of comments) {
2237
2240
  const id = idPrefix + String(comment.id);
2238
2241
  if (cursor.commentIds.includes(id)) continue;
2239
2242
  cursor.commentIds.push(id);
2240
2243
  if (!historyPrimed) continue;
2241
2244
  const text = body(comment);
2242
- events.push({
2245
+ events2.push({
2243
2246
  kind: "comment",
2244
2247
  id,
2245
2248
  repo,
@@ -2269,22 +2272,22 @@ async function pollGithub(repo, prNumber, cursorRaw) {
2269
2272
  const parsedCursor = /* @__PURE__ */ safeParse22(GithubCursorSchema, cursorRaw);
2270
2273
  let cursor = emptyGithubCursor();
2271
2274
  if (parsedCursor.success) cursor = parsedCursor.output;
2272
- const events = [];
2275
+ const events2 = [];
2273
2276
  const ref = `#${prNumber}`;
2274
2277
  if (!githubToken()) {
2275
2278
  if (!warnedMissingToken) {
2276
2279
  warnedMissingToken = true;
2277
2280
  console.warn("[sourcefed] GH_TOKEN or GITHUB_TOKEN is not set; GitHub monitors will not poll");
2278
2281
  }
2279
- return { events, cursor, terminal: false };
2282
+ return { events: events2, cursor, terminal: false };
2280
2283
  }
2281
2284
  const data = await fetchPrData(repo, prNumber);
2282
- if (!data) return { events, cursor, terminal: false };
2285
+ if (!data) return { events: events2, cursor, terminal: false };
2283
2286
  let historyPrimed = cursor.historyPrimed;
2284
2287
  if (!historyPrimed) historyPrimed = Boolean(cursor.reviewIds.length || cursor.commentIds.length || cursor.ciState || cursor.mergeable || cursor.prState);
2285
2288
  const lineCommentsPrimed = cursor.lineCommentsPrimed;
2286
2289
  const conversationCommentsPrimed = cursor.conversationCommentsPrimed;
2287
- appendGithubReviews(repo, prNumber, cursor, events, data.reviews ?? [], historyPrimed);
2290
+ appendGithubReviews(repo, prNumber, cursor, events2, data.reviews ?? [], historyPrimed);
2288
2291
  if (!cursor.historyPrimed) cursor.historyPrimed = true;
2289
2292
  const lineComments = await fetchGithubComments(`repos/${repo}/pulls/${prNumber}/comments`);
2290
2293
  if (lineComments) {
@@ -2292,7 +2295,7 @@ async function pollGithub(repo, prNumber, cursorRaw) {
2292
2295
  repo,
2293
2296
  prNumber,
2294
2297
  cursor,
2295
- events,
2298
+ events2,
2296
2299
  lineComments,
2297
2300
  "c:",
2298
2301
  lineCommentsPrimed,
@@ -2311,7 +2314,7 @@ async function pollGithub(repo, prNumber, cursorRaw) {
2311
2314
  repo,
2312
2315
  prNumber,
2313
2316
  cursor,
2314
- events,
2317
+ events2,
2315
2318
  conversationComments,
2316
2319
  "issue:",
2317
2320
  conversationCommentsPrimed,
@@ -2320,12 +2323,12 @@ async function pollGithub(repo, prNumber, cursorRaw) {
2320
2323
  );
2321
2324
  cursor.conversationCommentsPrimed = true;
2322
2325
  }
2323
- await appendGithubCiEvent(repo, prNumber, cursor, events, data.statusCheckRollup ?? []);
2326
+ await appendGithubCiEvent(repo, prNumber, cursor, events2, data.statusCheckRollup ?? []);
2324
2327
  const mergeable = data.mergeable ?? data.mergeStateStatus ?? "UNKNOWN";
2325
- appendGithubConflictEvent(repo, prNumber, cursor, events, mergeable);
2328
+ appendGithubConflictEvent(repo, prNumber, cursor, events2, mergeable);
2326
2329
  const prState = data.state ?? "OPEN";
2327
- const terminal = appendGithubStateEvent(repo, prNumber, cursor, events, prState);
2328
- return { events, cursor, terminal };
2330
+ const terminal = appendGithubStateEvent(repo, prNumber, cursor, events2, prState);
2331
+ return { events: events2, cursor, terminal };
2329
2332
  }
2330
2333
  var githubPollMonitor = new PollMonitor({
2331
2334
  run: async (source, cursor) => {
@@ -3103,11 +3106,11 @@ function jiraChangeValue(item, side) {
3103
3106
  function formatJiraChangelogItem(item) {
3104
3107
  return `${item.field}: ${jiraChangeValue(item, "from")} \u2192 ${jiraChangeValue(item, "to")}`;
3105
3108
  }
3106
- function isTerminalStatus(status) {
3107
- if (typeof status !== "string") return false;
3109
+ function isTerminalStatus(status2) {
3110
+ if (typeof status2 !== "string") return false;
3108
3111
  const terminalStatus = process.env.SOURCEFED_JIRA_TERMINAL_STATUS;
3109
3112
  if (!terminalStatus) return false;
3110
- return status.trim().toLowerCase() === terminalStatus.trim().toLowerCase();
3113
+ return status2.trim().toLowerCase() === terminalStatus.trim().toLowerCase();
3111
3114
  }
3112
3115
  var TRACKED_CHANGE_FIELDS = /* @__PURE__ */ new Set(["summary", "status", "assignee", "priority", "labels"]);
3113
3116
  function meaningfulChangelogItems(items) {
@@ -3133,12 +3136,12 @@ function jiraChangelogEvent(issueKey, history) {
3133
3136
  terminal
3134
3137
  };
3135
3138
  }
3136
- function appendJiraChangelog(issueKey, cursor, events, histories) {
3139
+ function appendJiraChangelog(issueKey, cursor, events2, histories) {
3137
3140
  const total = histories.length;
3138
3141
  if (cursor.changelogCount !== 0 && total > cursor.changelogCount) {
3139
3142
  for (const history of histories.slice(cursor.changelogCount)) {
3140
3143
  const event = jiraChangelogEvent(issueKey, history);
3141
- if (event) events.push(event);
3144
+ if (event) events2.push(event);
3142
3145
  }
3143
3146
  }
3144
3147
  cursor.changelogCount = total;
@@ -3149,7 +3152,7 @@ function adfText(node) {
3149
3152
  if (Array.isArray(node.content)) return node.content.map(adfText).join("");
3150
3153
  return "";
3151
3154
  }
3152
- function appendJiraComments(issueKey, cursor, events, comments) {
3155
+ function appendJiraComments(issueKey, cursor, events2, comments) {
3153
3156
  const freshComments = [];
3154
3157
  for (const comment of comments) {
3155
3158
  const id = Number(comment.id);
@@ -3162,7 +3165,7 @@ function appendJiraComments(issueKey, cursor, events, comments) {
3162
3165
  }
3163
3166
  for (const comment of freshComments) {
3164
3167
  const body = adfText(comment.body);
3165
- events.push({
3168
+ events2.push({
3166
3169
  kind: "comment",
3167
3170
  id: `comment:${comment.id}`,
3168
3171
  issueKey,
@@ -3175,11 +3178,11 @@ function appendJiraComments(issueKey, cursor, events, comments) {
3175
3178
  cursor.commentIds.push(Number(comment.id));
3176
3179
  }
3177
3180
  }
3178
- function appendJiraDescription(issueKey, cursor, events, description) {
3181
+ function appendJiraDescription(issueKey, cursor, events2, description) {
3179
3182
  let descriptionVersion = "none";
3180
3183
  if (description) descriptionVersion = JSON.stringify(description).slice(0, 40);
3181
3184
  if (cursor.descriptionVersion !== void 0 && descriptionVersion !== cursor.descriptionVersion) {
3182
- events.push({
3185
+ events2.push({
3183
3186
  kind: "description",
3184
3187
  id: `description:${descriptionVersion}`,
3185
3188
  issueKey,
@@ -3213,15 +3216,15 @@ async function pollJira(issueKey, cursorRaw) {
3213
3216
  const parsedCursor = /* @__PURE__ */ safeParse23(JiraCursorSchema, cursorRaw);
3214
3217
  let cursor = emptyJiraCursor();
3215
3218
  if (parsedCursor.success) cursor = parsedCursor.output;
3216
- const events = [];
3219
+ const events2 = [];
3217
3220
  const comments = await jiraFetch(`/issue/${issueKey}/comment?maxResults=50&orderBy=-created`);
3218
- appendJiraComments(issueKey, cursor, events, comments.comments ?? []);
3221
+ appendJiraComments(issueKey, cursor, events2, comments.comments ?? []);
3219
3222
  const issue2 = await jiraFetch(`/issue/${issueKey}?expand=changelog&fields=status,description`);
3220
3223
  const statusName = issue2.fields?.status?.name;
3221
- appendJiraDescription(issueKey, cursor, events, issue2.fields?.description);
3224
+ appendJiraDescription(issueKey, cursor, events2, issue2.fields?.description);
3222
3225
  const changelog = issue2.changelog?.histories ?? [];
3223
- appendJiraChangelog(issueKey, cursor, events, changelog);
3224
- return { events, cursor, terminal: isTerminalStatus(statusName) };
3226
+ appendJiraChangelog(issueKey, cursor, events2, changelog);
3227
+ return { events: events2, cursor, terminal: isTerminalStatus(statusName) };
3225
3228
  }
3226
3229
  var jiraPollMonitor = new PollMonitor2({
3227
3230
  run: async (source, cursor) => {
@@ -4135,9 +4138,9 @@ function parseSlackReadResult(result, cursorRaw) {
4135
4138
  if (!cursor.latestTs) return true;
4136
4139
  return messageTimestamp(message.ts) > messageTimestamp(cursor.latestTs);
4137
4140
  });
4138
- let events = [];
4141
+ let events2 = [];
4139
4142
  if (cursor.primed) {
4140
- events = freshMessages.map((message) => {
4143
+ events2 = freshMessages.map((message) => {
4141
4144
  let text = "[message without text]";
4142
4145
  if (message.text?.trim()) text = message.text.trim();
4143
4146
  let author = users.get(message.user);
@@ -4160,7 +4163,7 @@ function parseSlackReadResult(result, cursorRaw) {
4160
4163
  if (messageTimestamp(message.ts) > messageTimestamp(latestTs)) latestTs = message.ts;
4161
4164
  }
4162
4165
  cursor.latestTs = latestTs;
4163
- return { events, cursor };
4166
+ return { events: events2, cursor };
4164
4167
  }
4165
4168
  var SLACK_API = "https://slack.com/api";
4166
4169
  var warnedMissingToken2 = false;
@@ -4351,14 +4354,17 @@ function sourceForInput(type, input) {
4351
4354
  return source.build(input);
4352
4355
  }
4353
4356
  function sourceForWebhookPath(pathname) {
4354
- const sources = Object.values(SOURCE_MAP);
4355
- for (const source of sources) {
4357
+ const sources2 = Object.values(SOURCE_MAP);
4358
+ for (const source of sources2) {
4356
4359
  if (source.webhook?.path === pathname) return source;
4357
4360
  }
4358
4361
  return void 0;
4359
4362
  }
4360
4363
  async function serveHttp(options) {
4364
+ const active = /* @__PURE__ */ new Set();
4361
4365
  const server = createServer((incoming, outgoing) => {
4366
+ active.add(outgoing);
4367
+ outgoing.on("close", () => active.delete(outgoing));
4362
4368
  void handle(incoming, outgoing, options.handler);
4363
4369
  });
4364
4370
  server.requestTimeout = 0;
@@ -4377,6 +4383,10 @@ async function serveHttp(options) {
4377
4383
  return {
4378
4384
  port,
4379
4385
  async stop() {
4386
+ for (const outgoing of active) {
4387
+ outgoing.destroy();
4388
+ }
4389
+ active.clear();
4380
4390
  server.closeIdleConnections?.();
4381
4391
  await new Promise((resolve) => {
4382
4392
  server.close(() => resolve());
@@ -4659,10 +4669,10 @@ var SourcefedDaemon = class {
4659
4669
  this.observers.add(onEvent);
4660
4670
  return () => this.observers.delete(onEvent);
4661
4671
  }
4662
- appendLog(target, events) {
4672
+ appendLog(target, events2) {
4663
4673
  const key = targetKey(target);
4664
4674
  const existing = this.deliveryLog.get(key) ?? [];
4665
- const merged = [...events.reverse(), ...existing];
4675
+ const merged = [...events2.reverse(), ...existing];
4666
4676
  this.deliveryLog.set(key, merged.slice(0, DELIVERY_LOG_LIMIT));
4667
4677
  }
4668
4678
  emit(target) {
@@ -4722,9 +4732,16 @@ function monitorView(monitor) {
4722
4732
  label: definition.label(monitor.source),
4723
4733
  detail: definition.detail(monitor.source),
4724
4734
  describe: definition.describe(monitor.source),
4725
- unresponsive: definition.unresponsive(monitor)
4735
+ unresponsive: definition.unresponsive(monitor),
4736
+ lastPolledAt: cursorTime(monitor.cursors.__lastPolledAt ?? monitor.cursors.__lastAttemptAt),
4737
+ webhookHeartbeatAt: cursorTime(monitor.cursors.__webhookHeartbeatAt)
4726
4738
  };
4727
4739
  }
4740
+ function cursorTime(value) {
4741
+ const numeric = Number(value);
4742
+ if (!Number.isFinite(numeric) || numeric <= 0) return void 0;
4743
+ return new Date(numeric).toISOString();
4744
+ }
4728
4745
  function logEntryView(queued) {
4729
4746
  const definition = sourceDefinition(queued.event.source);
4730
4747
  return {
@@ -4815,16 +4832,16 @@ var HttpDaemonClient = class {
4815
4832
  });
4816
4833
  let buffered = [];
4817
4834
  let delivering = Promise.resolve();
4818
- const deliver = (events) => {
4819
- if (!callbacks?.has(onEvents) || events.length === 0) return Promise.resolve();
4820
- delivering = delivering.then(() => deliverWithRetry(events, 0)).catch((error2) => {
4835
+ const deliver = (events2) => {
4836
+ if (!callbacks?.has(onEvents) || events2.length === 0) return Promise.resolve();
4837
+ delivering = delivering.then(() => deliverWithRetry(events2, 0)).catch((error2) => {
4821
4838
  console.error(`[sourcefed] event delivery failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
4822
4839
  });
4823
4840
  return delivering;
4824
4841
  };
4825
- const deliverWithRetry = async (events, attempt) => {
4842
+ const deliverWithRetry = async (events2, attempt) => {
4826
4843
  try {
4827
- await onEvents(events);
4844
+ await onEvents(events2);
4828
4845
  } catch (error2) {
4829
4846
  if (attempt >= 2) {
4830
4847
  console.error(`[sourcefed] event delivery failed after retries; re-draining queue: ${error2 instanceof Error ? error2.message : String(error2)}`);
@@ -4833,7 +4850,7 @@ var HttpDaemonClient = class {
4833
4850
  return;
4834
4851
  }
4835
4852
  await sleep(250 * (attempt + 1));
4836
- await deliverWithRetry(events, attempt + 1);
4853
+ await deliverWithRetry(events2, attempt + 1);
4837
4854
  }
4838
4855
  };
4839
4856
  const performDrain = async () => {
@@ -5038,8 +5055,8 @@ function eventStream(daemon, target) {
5038
5055
  const stream = new ReadableStream({
5039
5056
  start(controller) {
5040
5057
  const encoder = new TextEncoder();
5041
- unsubscribe = daemon.subscribe(target, (events) => {
5042
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "event", target, events })}
5058
+ unsubscribe = daemon.subscribe(target, (events2) => {
5059
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "event", target, events: events2 })}
5043
5060
 
5044
5061
  `));
5045
5062
  });
@@ -5065,6 +5082,73 @@ function eventStream(daemon, target) {
5065
5082
  });
5066
5083
  }
5067
5084
 
5085
+ // packages/cli/src/utils/flags.ts
5086
+ function flag(args, name) {
5087
+ const index = args.indexOf(`--${name}`);
5088
+ return index >= 0 ? args[index + 1] : void 0;
5089
+ }
5090
+
5091
+ // packages/cli/src/utils/net.ts
5092
+ function isLoopback(hostname) {
5093
+ return hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
5094
+ }
5095
+
5096
+ // packages/cli/src/utils/daemon.ts
5097
+ function createDaemon() {
5098
+ return new SourcefedDaemon({
5099
+ stateDir: process.env.SOURCEFED_STATE_DIR
5100
+ });
5101
+ }
5102
+
5103
+ // packages/cli/src/utils/webhooks.ts
5104
+ async function startWebhookServer(runtime) {
5105
+ const enabled = Boolean(
5106
+ process.env.SOURCEFED_ENABLE_WEBHOOKS === "1" || process.env.SOURCEFED_WEBHOOK_PORT || process.env.SOURCEFED_GITHUB_WEBHOOK_SECRET || process.env.SOURCEFED_SLACK_SIGNING_SECRET
5107
+ );
5108
+ if (!enabled) return void 0;
5109
+ try {
5110
+ return await serveHttp({
5111
+ hostname: process.env.SOURCEFED_WEBHOOK_HOST ?? "127.0.0.1",
5112
+ port: Number(process.env.SOURCEFED_WEBHOOK_PORT ?? 8788),
5113
+ handler: (request) => runtime.webhook(request)
5114
+ });
5115
+ } catch (error2) {
5116
+ console.error(`[sourcefed] webhook listener unavailable; continuing with MCP and polling: ${error2 instanceof Error ? error2.message : String(error2)}`);
5117
+ return void 0;
5118
+ }
5119
+ }
5120
+
5121
+ // packages/cli/src/commands/daemon.ts
5122
+ async function runDaemon(args) {
5123
+ const port = Number(flag(args, "port") ?? process.env.SOURCEFED_DAEMON_PORT ?? 18787);
5124
+ const hostname = flag(args, "host") ?? process.env.SOURCEFED_DAEMON_HOST ?? "127.0.0.1";
5125
+ const token = process.env.SOURCEFED_DAEMON_TOKEN;
5126
+ if (!isLoopback(hostname) && !token) {
5127
+ throw new Error("binding the daemon to a non-loopback address requires SOURCEFED_DAEMON_TOKEN");
5128
+ }
5129
+ const daemon = createDaemon();
5130
+ await daemon.start();
5131
+ let server;
5132
+ try {
5133
+ server = await serveHttp({
5134
+ hostname,
5135
+ port,
5136
+ handler: (request) => handleDaemonHttpRequest(daemon, request, { token })
5137
+ });
5138
+ const webhookServer = await startWebhookServer(daemon.runtime);
5139
+ console.error(webhookServer ? `[sourcefed] webhook listener on http://${process.env.SOURCEFED_WEBHOOK_HOST ?? "127.0.0.1"}:${webhookServer.port}` : "[sourcefed] no separate webhook listener (webhooks served on daemon port)");
5140
+ } catch (error2) {
5141
+ await daemon.stop();
5142
+ throw error2;
5143
+ }
5144
+ console.error(`[sourcefed] daemon on http://${hostname}:${server.port} (rpc, events, webhooks)`);
5145
+ await new Promise(() => {
5146
+ });
5147
+ }
5148
+
5149
+ // packages/cli/src/commands/mcp.ts
5150
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5151
+
5068
5152
  // packages/mcp/dist/index.js
5069
5153
  import process3 from "node:process";
5070
5154
  import { link as link5, mkdir as mkdir5, open as open5, readFile as readFile5, rename as rename5, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
@@ -5331,9 +5415,9 @@ function nullish(input) {
5331
5415
  return input === null || input === void 0;
5332
5416
  }
5333
5417
  function cleanRegex(source) {
5334
- const start = source.startsWith("^") ? 1 : 0;
5418
+ const start2 = source.startsWith("^") ? 1 : 0;
5335
5419
  const end = source.endsWith("$") ? source.length - 1 : source.length;
5336
- return source.slice(start, end);
5420
+ return source.slice(start2, end);
5337
5421
  }
5338
5422
  function floatSafeRemainder(val, step) {
5339
5423
  const ratio = val / step;
@@ -14869,19 +14953,19 @@ var MCP_NAME_HEADER_SOURCE = {
14869
14953
  "resources/read": "uri"
14870
14954
  };
14871
14955
  function stripHttpOws(value) {
14872
- let start = 0;
14873
- while (start < value.length) {
14874
- const code = value.codePointAt(start);
14956
+ let start2 = 0;
14957
+ while (start2 < value.length) {
14958
+ const code = value.codePointAt(start2);
14875
14959
  if (code !== 9 && code !== 32) break;
14876
- start += 1;
14960
+ start2 += 1;
14877
14961
  }
14878
14962
  let end = value.length;
14879
- while (end > start) {
14963
+ while (end > start2) {
14880
14964
  const code = value.codePointAt(end - 1);
14881
14965
  if (code !== 9 && code !== 32) break;
14882
14966
  end -= 1;
14883
14967
  }
14884
- return start === 0 && end === value.length ? value : value.slice(start, end);
14968
+ return start2 === 0 && end === value.length ? value : value.slice(start2, end);
14885
14969
  }
14886
14970
  function validateStandardRequestHeaders(request, route) {
14887
14971
  if (route.messageKind !== "request") return;
@@ -23640,11 +23724,11 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports, module) => {
23640
23724
  if (!f) throw new Error(`Unknown format "${name}"`);
23641
23725
  return f;
23642
23726
  };
23643
- function addFormats2(ajv, list, fs, exportName) {
23727
+ function addFormats2(ajv, list2, fs, exportName) {
23644
23728
  var _a3;
23645
23729
  var _b;
23646
23730
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 || (_b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`);
23647
- for (const f of list) ajv.addFormat(f, fs[f]);
23731
+ for (const f of list2) ajv.addFormat(f, fs[f]);
23648
23732
  }
23649
23733
  module.exports = exports = formatsPlugin;
23650
23734
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -23912,11 +23996,11 @@ data: ${JSON.stringify({
23912
23996
  const readable = new ReadableStream({
23913
23997
  start(streamController) {
23914
23998
  controller = streamController;
23915
- const ack = stampSubscriptionId({
23999
+ const ack2 = stampSubscriptionId({
23916
24000
  method: "notifications/subscriptions/acknowledged",
23917
24001
  params: { notifications: honored }
23918
24002
  }, subscriptionId);
23919
- writeNotification(ack.method, ack.params);
24003
+ writeNotification(ack2.method, ack2.params);
23920
24004
  unsubscribe = bus.subscribe((event) => {
23921
24005
  if (closed || !listenFilterAccepts(honored, event)) return;
23922
24006
  const note = stampSubscriptionId(serverEventToNotification(event), subscriptionId);
@@ -25602,7 +25686,7 @@ var WebStandardStreamableHTTPServerTransport = class {
25602
25686
  /**
25603
25687
  * Helper to create a JSON error response
25604
25688
  */
25605
- createJsonErrorResponse(status, code, message, options) {
25689
+ createJsonErrorResponse(status2, code, message, options) {
25606
25690
  const error2 = {
25607
25691
  code,
25608
25692
  message
@@ -25613,7 +25697,7 @@ var WebStandardStreamableHTTPServerTransport = class {
25613
25697
  error: error2,
25614
25698
  id: null
25615
25699
  }, {
25616
- status,
25700
+ status: status2,
25617
25701
  headers: {
25618
25702
  "Content-Type": "application/json",
25619
25703
  ...options?.headers
@@ -27665,7 +27749,7 @@ async function confirmStillFailing2(repo, prNumber, failed) {
27665
27749
  return /FAIL|ERROR|CANCEL/i.test(state);
27666
27750
  });
27667
27751
  }
27668
- async function appendGithubCiEvent2(repo, prNumber, cursor, events, rollup) {
27752
+ async function appendGithubCiEvent2(repo, prNumber, cursor, events2, rollup) {
27669
27753
  const ciState = rollup.map((check) => check.conclusion ?? check.state ?? "PENDING").sort().join(",");
27670
27754
  if (!ciState || ciState === cursor.ciState) return;
27671
27755
  const previousCiState = cursor.ciState;
@@ -27676,7 +27760,7 @@ async function appendGithubCiEvent2(repo, prNumber, cursor, events, rollup) {
27676
27760
  const failureState = failed.map((check) => `${check.name ?? check.context ?? "check"}:${check.conclusion ?? check.state ?? "FAILURE"}`).sort().join(",");
27677
27761
  if (failed.length > 0 && failureState !== cursor.ciFailureState) {
27678
27762
  const names = failed.map((check) => check.name ?? check.context ?? "check").join(", ");
27679
- events.push({
27763
+ events2.push({
27680
27764
  kind: "ci",
27681
27765
  id: `ci:${failureState}`,
27682
27766
  repo,
@@ -27689,13 +27773,13 @@ async function appendGithubCiEvent2(repo, prNumber, cursor, events, rollup) {
27689
27773
  }
27690
27774
  cursor.ciFailureState = failureState;
27691
27775
  }
27692
- function appendGithubConflictEvent2(repo, prNumber, cursor, events, mergeable) {
27776
+ function appendGithubConflictEvent2(repo, prNumber, cursor, events2, mergeable) {
27693
27777
  if (mergeable === cursor.mergeable) return;
27694
27778
  const previousMergeable = cursor.mergeable;
27695
27779
  cursor.mergeable = mergeable;
27696
27780
  if (previousMergeable === void 0) return;
27697
27781
  if (mergeable !== "CONFLICTING" && mergeable !== "DIRTY") return;
27698
- events.push({
27782
+ events2.push({
27699
27783
  kind: "conflict",
27700
27784
  id: `conflict:${mergeable}`,
27701
27785
  repo,
@@ -27705,7 +27789,7 @@ function appendGithubConflictEvent2(repo, prNumber, cursor, events, mergeable) {
27705
27789
  actionable: true
27706
27790
  });
27707
27791
  }
27708
- function appendGithubReviews2(repo, prNumber, cursor, events, reviews, historyPrimed) {
27792
+ function appendGithubReviews2(repo, prNumber, cursor, events2, reviews, historyPrimed) {
27709
27793
  const ref = `#${prNumber}`;
27710
27794
  for (const review of reviews) {
27711
27795
  const id = "rev:" + String(review.id ?? `${review.author?.login}-${review.submittedAt}`);
@@ -27716,7 +27800,7 @@ function appendGithubReviews2(repo, prNumber, cursor, events, reviews, historyPr
27716
27800
  let actionable = false;
27717
27801
  if (review.state === "CHANGES_REQUESTED") actionable = true;
27718
27802
  if (review.state === "COMMENTED" && review.body?.trim()) actionable = true;
27719
- events.push({
27803
+ events2.push({
27720
27804
  kind: "review",
27721
27805
  id,
27722
27806
  repo,
@@ -27728,7 +27812,7 @@ function appendGithubReviews2(repo, prNumber, cursor, events, reviews, historyPr
27728
27812
  });
27729
27813
  }
27730
27814
  }
27731
- function appendGithubStateEvent2(repo, prNumber, cursor, events, prState) {
27815
+ function appendGithubStateEvent2(repo, prNumber, cursor, events2, prState) {
27732
27816
  const isTerminal = prState === "MERGED" || prState === "CLOSED";
27733
27817
  if (prState === cursor.prState) return isTerminal;
27734
27818
  const previousPrState = cursor.prState;
@@ -27736,7 +27820,7 @@ function appendGithubStateEvent2(repo, prNumber, cursor, events, prState) {
27736
27820
  if (previousPrState !== void 0 && isTerminal) {
27737
27821
  let summary = `PR #${prNumber} was CLOSED without merging`;
27738
27822
  if (prState === "MERGED") summary = `PR #${prNumber} was MERGED`;
27739
- events.push({
27823
+ events2.push({
27740
27824
  kind: "merged",
27741
27825
  id: `state:${prState}`,
27742
27826
  repo,
@@ -27749,14 +27833,14 @@ function appendGithubStateEvent2(repo, prNumber, cursor, events, prState) {
27749
27833
  }
27750
27834
  return isTerminal;
27751
27835
  }
27752
- function appendComments2(repo, prNumber, cursor, events, comments, idPrefix, historyPrimed, summary, body) {
27836
+ function appendComments2(repo, prNumber, cursor, events2, comments, idPrefix, historyPrimed, summary, body) {
27753
27837
  for (const comment of comments) {
27754
27838
  const id = idPrefix + String(comment.id);
27755
27839
  if (cursor.commentIds.includes(id)) continue;
27756
27840
  cursor.commentIds.push(id);
27757
27841
  if (!historyPrimed) continue;
27758
27842
  const text = body(comment);
27759
- events.push({
27843
+ events2.push({
27760
27844
  kind: "comment",
27761
27845
  id,
27762
27846
  repo,
@@ -27786,22 +27870,22 @@ async function pollGithub2(repo, prNumber, cursorRaw) {
27786
27870
  const parsedCursor = /* @__PURE__ */ safeParse2222(GithubCursorSchema2, cursorRaw);
27787
27871
  let cursor = emptyGithubCursor2();
27788
27872
  if (parsedCursor.success) cursor = parsedCursor.output;
27789
- const events = [];
27873
+ const events2 = [];
27790
27874
  const ref = `#${prNumber}`;
27791
27875
  if (!githubToken2()) {
27792
27876
  if (!warnedMissingToken3) {
27793
27877
  warnedMissingToken3 = true;
27794
27878
  console.warn("[sourcefed] GH_TOKEN or GITHUB_TOKEN is not set; GitHub monitors will not poll");
27795
27879
  }
27796
- return { events, cursor, terminal: false };
27880
+ return { events: events2, cursor, terminal: false };
27797
27881
  }
27798
27882
  const data = await fetchPrData2(repo, prNumber);
27799
- if (!data) return { events, cursor, terminal: false };
27883
+ if (!data) return { events: events2, cursor, terminal: false };
27800
27884
  let historyPrimed = cursor.historyPrimed;
27801
27885
  if (!historyPrimed) historyPrimed = Boolean(cursor.reviewIds.length || cursor.commentIds.length || cursor.ciState || cursor.mergeable || cursor.prState);
27802
27886
  const lineCommentsPrimed = cursor.lineCommentsPrimed;
27803
27887
  const conversationCommentsPrimed = cursor.conversationCommentsPrimed;
27804
- appendGithubReviews2(repo, prNumber, cursor, events, data.reviews ?? [], historyPrimed);
27888
+ appendGithubReviews2(repo, prNumber, cursor, events2, data.reviews ?? [], historyPrimed);
27805
27889
  if (!cursor.historyPrimed) cursor.historyPrimed = true;
27806
27890
  const lineComments = await fetchGithubComments2(`repos/${repo}/pulls/${prNumber}/comments`);
27807
27891
  if (lineComments) {
@@ -27809,7 +27893,7 @@ async function pollGithub2(repo, prNumber, cursorRaw) {
27809
27893
  repo,
27810
27894
  prNumber,
27811
27895
  cursor,
27812
- events,
27896
+ events2,
27813
27897
  lineComments,
27814
27898
  "c:",
27815
27899
  lineCommentsPrimed,
@@ -27828,7 +27912,7 @@ async function pollGithub2(repo, prNumber, cursorRaw) {
27828
27912
  repo,
27829
27913
  prNumber,
27830
27914
  cursor,
27831
- events,
27915
+ events2,
27832
27916
  conversationComments,
27833
27917
  "issue:",
27834
27918
  conversationCommentsPrimed,
@@ -27837,12 +27921,12 @@ async function pollGithub2(repo, prNumber, cursorRaw) {
27837
27921
  );
27838
27922
  cursor.conversationCommentsPrimed = true;
27839
27923
  }
27840
- await appendGithubCiEvent2(repo, prNumber, cursor, events, data.statusCheckRollup ?? []);
27924
+ await appendGithubCiEvent2(repo, prNumber, cursor, events2, data.statusCheckRollup ?? []);
27841
27925
  const mergeable = data.mergeable ?? data.mergeStateStatus ?? "UNKNOWN";
27842
- appendGithubConflictEvent2(repo, prNumber, cursor, events, mergeable);
27926
+ appendGithubConflictEvent2(repo, prNumber, cursor, events2, mergeable);
27843
27927
  const prState = data.state ?? "OPEN";
27844
- const terminal = appendGithubStateEvent2(repo, prNumber, cursor, events, prState);
27845
- return { events, cursor, terminal };
27928
+ const terminal = appendGithubStateEvent2(repo, prNumber, cursor, events2, prState);
27929
+ return { events: events2, cursor, terminal };
27846
27930
  }
27847
27931
  var githubPollMonitor2 = new PollMonitor4({
27848
27932
  run: async (source, cursor) => {
@@ -28620,11 +28704,11 @@ function jiraChangeValue2(item, side) {
28620
28704
  function formatJiraChangelogItem2(item) {
28621
28705
  return `${item.field}: ${jiraChangeValue2(item, "from")} \u2192 ${jiraChangeValue2(item, "to")}`;
28622
28706
  }
28623
- function isTerminalStatus2(status) {
28624
- if (typeof status !== "string") return false;
28707
+ function isTerminalStatus2(status2) {
28708
+ if (typeof status2 !== "string") return false;
28625
28709
  const terminalStatus = process.env.SOURCEFED_JIRA_TERMINAL_STATUS;
28626
28710
  if (!terminalStatus) return false;
28627
- return status.trim().toLowerCase() === terminalStatus.trim().toLowerCase();
28711
+ return status2.trim().toLowerCase() === terminalStatus.trim().toLowerCase();
28628
28712
  }
28629
28713
  var TRACKED_CHANGE_FIELDS2 = /* @__PURE__ */ new Set(["summary", "status", "assignee", "priority", "labels"]);
28630
28714
  function meaningfulChangelogItems2(items) {
@@ -28650,12 +28734,12 @@ function jiraChangelogEvent2(issueKey, history) {
28650
28734
  terminal
28651
28735
  };
28652
28736
  }
28653
- function appendJiraChangelog2(issueKey, cursor, events, histories) {
28737
+ function appendJiraChangelog2(issueKey, cursor, events2, histories) {
28654
28738
  const total = histories.length;
28655
28739
  if (cursor.changelogCount !== 0 && total > cursor.changelogCount) {
28656
28740
  for (const history of histories.slice(cursor.changelogCount)) {
28657
28741
  const event = jiraChangelogEvent2(issueKey, history);
28658
- if (event) events.push(event);
28742
+ if (event) events2.push(event);
28659
28743
  }
28660
28744
  }
28661
28745
  cursor.changelogCount = total;
@@ -28666,7 +28750,7 @@ function adfText2(node) {
28666
28750
  if (Array.isArray(node.content)) return node.content.map(adfText2).join("");
28667
28751
  return "";
28668
28752
  }
28669
- function appendJiraComments2(issueKey, cursor, events, comments) {
28753
+ function appendJiraComments2(issueKey, cursor, events2, comments) {
28670
28754
  const freshComments = [];
28671
28755
  for (const comment of comments) {
28672
28756
  const id = Number(comment.id);
@@ -28679,7 +28763,7 @@ function appendJiraComments2(issueKey, cursor, events, comments) {
28679
28763
  }
28680
28764
  for (const comment of freshComments) {
28681
28765
  const body = adfText2(comment.body);
28682
- events.push({
28766
+ events2.push({
28683
28767
  kind: "comment",
28684
28768
  id: `comment:${comment.id}`,
28685
28769
  issueKey,
@@ -28692,11 +28776,11 @@ function appendJiraComments2(issueKey, cursor, events, comments) {
28692
28776
  cursor.commentIds.push(Number(comment.id));
28693
28777
  }
28694
28778
  }
28695
- function appendJiraDescription2(issueKey, cursor, events, description) {
28779
+ function appendJiraDescription2(issueKey, cursor, events2, description) {
28696
28780
  let descriptionVersion = "none";
28697
28781
  if (description) descriptionVersion = JSON.stringify(description).slice(0, 40);
28698
28782
  if (cursor.descriptionVersion !== void 0 && descriptionVersion !== cursor.descriptionVersion) {
28699
- events.push({
28783
+ events2.push({
28700
28784
  kind: "description",
28701
28785
  id: `description:${descriptionVersion}`,
28702
28786
  issueKey,
@@ -28730,15 +28814,15 @@ async function pollJira2(issueKey, cursorRaw) {
28730
28814
  const parsedCursor = /* @__PURE__ */ safeParse232(JiraCursorSchema2, cursorRaw);
28731
28815
  let cursor = emptyJiraCursor2();
28732
28816
  if (parsedCursor.success) cursor = parsedCursor.output;
28733
- const events = [];
28817
+ const events2 = [];
28734
28818
  const comments = await jiraFetch2(`/issue/${issueKey}/comment?maxResults=50&orderBy=-created`);
28735
- appendJiraComments2(issueKey, cursor, events, comments.comments ?? []);
28819
+ appendJiraComments2(issueKey, cursor, events2, comments.comments ?? []);
28736
28820
  const issue2 = await jiraFetch2(`/issue/${issueKey}?expand=changelog&fields=status,description`);
28737
28821
  const statusName = issue2.fields?.status?.name;
28738
- appendJiraDescription2(issueKey, cursor, events, issue2.fields?.description);
28822
+ appendJiraDescription2(issueKey, cursor, events2, issue2.fields?.description);
28739
28823
  const changelog = issue2.changelog?.histories ?? [];
28740
- appendJiraChangelog2(issueKey, cursor, events, changelog);
28741
- return { events, cursor, terminal: isTerminalStatus2(statusName) };
28824
+ appendJiraChangelog2(issueKey, cursor, events2, changelog);
28825
+ return { events: events2, cursor, terminal: isTerminalStatus2(statusName) };
28742
28826
  }
28743
28827
  var jiraPollMonitor2 = new PollMonitor22({
28744
28828
  run: async (source, cursor) => {
@@ -29652,9 +29736,9 @@ function parseSlackReadResult2(result, cursorRaw) {
29652
29736
  if (!cursor.latestTs) return true;
29653
29737
  return messageTimestamp2(message.ts) > messageTimestamp2(cursor.latestTs);
29654
29738
  });
29655
- let events = [];
29739
+ let events2 = [];
29656
29740
  if (cursor.primed) {
29657
- events = freshMessages.map((message) => {
29741
+ events2 = freshMessages.map((message) => {
29658
29742
  let text = "[message without text]";
29659
29743
  if (message.text?.trim()) text = message.text.trim();
29660
29744
  let author = users.get(message.user);
@@ -29677,7 +29761,7 @@ function parseSlackReadResult2(result, cursorRaw) {
29677
29761
  if (messageTimestamp2(message.ts) > messageTimestamp2(latestTs)) latestTs = message.ts;
29678
29762
  }
29679
29763
  cursor.latestTs = latestTs;
29680
- return { events, cursor };
29764
+ return { events: events2, cursor };
29681
29765
  }
29682
29766
  var SLACK_API2 = "https://slack.com/api";
29683
29767
  var warnedMissingToken22 = false;
@@ -30019,9 +30103,9 @@ function buildServer(daemon) {
30019
30103
  async (uri, variables) => {
30020
30104
  const target = decodeTarget2(String(variables.targetId));
30021
30105
  if (!target) throw new Error("invalid Sourcefed target resource");
30022
- const events = await daemon.readEvents(target);
30106
+ const events2 = await daemon.readEvents(target);
30023
30107
  return {
30024
- contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ events }) }]
30108
+ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ events: events2 }) }]
30025
30109
  };
30026
30110
  }
30027
30111
  );
@@ -30034,6 +30118,275 @@ function errorResult(error2) {
30034
30118
  return { content: [{ type: "text", text: error2 }], isError: true };
30035
30119
  }
30036
30120
 
30121
+ // packages/cli/src/commands/mcp.ts
30122
+ async function runMcp(args) {
30123
+ const mode = args[0] ?? "--stdio";
30124
+ if (mode === "--stdio") {
30125
+ const daemon2 = createDaemon();
30126
+ const mcp2 = createSourcefedStdio(daemon2);
30127
+ await daemon2.start();
30128
+ const webhookServer = await startWebhookServer(mcp2.runtime);
30129
+ await serveStdio(mcp2.factory);
30130
+ await webhookServer?.stop();
30131
+ await mcp2.close();
30132
+ return;
30133
+ }
30134
+ if (mode !== "--http") {
30135
+ throw new Error("mcp expects --stdio or --http");
30136
+ }
30137
+ const port = Number(flag(args, "port") ?? process.env.SOURCEFED_MCP_PORT ?? 18788);
30138
+ const hostname = flag(args, "host") ?? process.env.SOURCEFED_MCP_HOST ?? "127.0.0.1";
30139
+ const token = process.env.SOURCEFED_DAEMON_TOKEN;
30140
+ if (!isLoopback(hostname) && !token) {
30141
+ throw new Error("binding the MCP server to a non-loopback address requires SOURCEFED_DAEMON_TOKEN");
30142
+ }
30143
+ const daemon = createDaemon();
30144
+ const mcp = createSourcefedMcp(daemon);
30145
+ await daemon.start();
30146
+ let server;
30147
+ try {
30148
+ server = await serveHttp({
30149
+ hostname,
30150
+ port,
30151
+ handler: (request) => {
30152
+ const url2 = new URL(request.url);
30153
+ if (url2.pathname === "/mcp" && token && request.headers.get("authorization") !== `Bearer ${token}`) {
30154
+ return new Response("unauthorized", { status: 401 });
30155
+ }
30156
+ return url2.pathname === "/mcp" ? mcp.handler.fetch(request) : daemon.webhook(request);
30157
+ }
30158
+ });
30159
+ } catch (error2) {
30160
+ await mcp.close();
30161
+ throw error2;
30162
+ }
30163
+ console.error(`[sourcefed] MCP server on http://${hostname}:${server.port}/mcp`);
30164
+ await new Promise(() => {
30165
+ });
30166
+ }
30167
+
30168
+ // packages/cli/src/commands/monitor/index.ts
30169
+ import os7 from "node:os";
30170
+
30171
+ // packages/cli/src/utils/format.ts
30172
+ function printMonitors(monitors) {
30173
+ if (monitors.length === 0) {
30174
+ console.log(dim("no monitors for this target"));
30175
+ return;
30176
+ }
30177
+ for (const monitor of monitors) {
30178
+ const status2 = monitor.enabled ? green("enabled") : red("stopped");
30179
+ const delivery = dim(monitor.delivery);
30180
+ console.log(`${monitor.icon} ${monitor.describe} ${status2} ${delivery}`);
30181
+ }
30182
+ }
30183
+ function printLogs(logs2) {
30184
+ if (logs2.length === 0) {
30185
+ console.log(dim("no notifications sent yet"));
30186
+ return;
30187
+ }
30188
+ for (const entry of logs2) {
30189
+ const time3 = dim(new Date(entry.at).toLocaleString());
30190
+ const marker = entry.actionable ? yellow("\u25B6") : dim("\xB7");
30191
+ console.log(`${marker} ${entry.icon} ${time3} ${entry.describe}`);
30192
+ console.log(` ${entry.summary}`);
30193
+ if (entry.body) console.log(dim(` ${entry.body}`));
30194
+ }
30195
+ }
30196
+ function printResult(result) {
30197
+ console.log(JSON.stringify(result, null, 2));
30198
+ }
30199
+ function green(text) {
30200
+ return `\x1B[32m${text}\x1B[0m`;
30201
+ }
30202
+ function red(text) {
30203
+ return `\x1B[31m${text}\x1B[0m`;
30204
+ }
30205
+ function yellow(text) {
30206
+ return `\x1B[33m${text}\x1B[0m`;
30207
+ }
30208
+ function dim(text) {
30209
+ return `\x1B[2m${text}\x1B[0m`;
30210
+ }
30211
+
30212
+ // packages/cli/src/commands/monitor/ack.ts
30213
+ async function ack(context) {
30214
+ const eventIDs = (flag(context.args, "event-id") ?? "").split(",").filter(Boolean);
30215
+ if (eventIDs.length === 0) throw new Error("ack requires --event-id <id>[,<id>...]");
30216
+ printResult(await context.client.request("monitor.ack", { target: context.target, eventIDs }));
30217
+ }
30218
+
30219
+ // packages/cli/src/commands/monitor/create.ts
30220
+ async function create(context) {
30221
+ const sourceType = flag(context.args, "source-type");
30222
+ const sourceTypes = await context.client.request("daemon.sourceTypes");
30223
+ if (!sourceType || !sourceTypes.sourceTypes?.includes(sourceType)) {
30224
+ throw new Error(`create requires --source-type ${sourceTypes.sourceTypes?.join("|") ?? "unknown"}`);
30225
+ }
30226
+ const input = {
30227
+ name: flag(context.args, "name") ?? sourceType,
30228
+ sourceType,
30229
+ target: context.target
30230
+ };
30231
+ const keyMap = {
30232
+ "issue-key": "issueKey",
30233
+ repo: "repo",
30234
+ "channel-id": "channelId",
30235
+ "thread-ts": "threadTs",
30236
+ "thread-url": "threadUrl"
30237
+ };
30238
+ for (const key of Object.keys(keyMap)) {
30239
+ const value = flag(context.args, key);
30240
+ if (value) input[keyMap[key]] = value;
30241
+ }
30242
+ const prNumber = flag(context.args, "pr-number");
30243
+ if (prNumber) input.prNumber = Number(prNumber);
30244
+ const pollIntervalSec = flag(context.args, "poll-interval-sec");
30245
+ if (pollIntervalSec) input.pollIntervalSec = Number(pollIntervalSec);
30246
+ printResult(await context.client.request("monitor.create", input));
30247
+ }
30248
+
30249
+ // packages/cli/src/commands/monitor/events.ts
30250
+ async function events(context) {
30251
+ printResult(await context.client.request("monitor.events", { target: context.target }));
30252
+ }
30253
+
30254
+ // packages/cli/src/commands/monitor/follow.ts
30255
+ async function follow(context) {
30256
+ console.error(`[sourcefed] following events for ${context.target.kind}:${context.target.id}; Ctrl+C to stop`);
30257
+ const listener = await context.client.subscribe(context.target, async (events2) => {
30258
+ for (const entry of events2) {
30259
+ const time3 = dim(new Date(entry.event.at).toLocaleString());
30260
+ const marker = entry.event.actionable ? yellow("\u25B6") : dim("\xB7");
30261
+ console.log(`${marker} ${time3} ${entry.event.summary}`);
30262
+ if (entry.event.body) console.log(dim(` ${entry.event.body}`));
30263
+ }
30264
+ });
30265
+ await new Promise(() => {
30266
+ });
30267
+ await listener.close();
30268
+ }
30269
+
30270
+ // packages/cli/src/commands/monitor/list.ts
30271
+ async function list(context) {
30272
+ const result = await context.client.request("monitor.list", { target: context.target });
30273
+ printMonitors(result?.monitors ?? []);
30274
+ }
30275
+
30276
+ // packages/cli/src/commands/monitor/logs.ts
30277
+ async function logs(context) {
30278
+ const result = await context.client.request("monitor.logs", { target: context.target });
30279
+ printLogs(result?.logs ?? []);
30280
+ }
30281
+
30282
+ // packages/cli/src/commands/monitor/remove.ts
30283
+ async function remove(context) {
30284
+ if (!context.args.includes("--yes")) throw new Error("remove is destructive: pass --yes to confirm");
30285
+ const id = flag(context.args, "id");
30286
+ if (!id) throw new Error("remove requires --id");
30287
+ printResult(await context.client.request("monitor.remove", { target: context.target, id }));
30288
+ }
30289
+
30290
+ // packages/cli/src/commands/monitor/sources.ts
30291
+ async function sources(context) {
30292
+ printResult(await context.client.request("daemon.sourceTypes"));
30293
+ }
30294
+
30295
+ // packages/cli/src/commands/monitor/start.ts
30296
+ async function start(context) {
30297
+ const id = flag(context.args, "id");
30298
+ if (!id) throw new Error("start requires --id");
30299
+ printResult(await context.client.request("monitor.start", { target: context.target, id }));
30300
+ }
30301
+
30302
+ // packages/cli/src/commands/monitor/status.ts
30303
+ async function status(context) {
30304
+ const id = flag(context.args, "id");
30305
+ if (!id) throw new Error("status requires --id");
30306
+ printResult(await context.client.request("monitor.status", { target: context.target, id }));
30307
+ }
30308
+
30309
+ // packages/cli/src/commands/monitor/stop.ts
30310
+ async function stop(context) {
30311
+ const id = flag(context.args, "id");
30312
+ if (!id) throw new Error("stop requires --id");
30313
+ printResult(await context.client.request("monitor.stop", { target: context.target, id }));
30314
+ }
30315
+
30316
+ // packages/cli/src/commands/monitor/index.ts
30317
+ var SUBCOMMANDS = {
30318
+ list,
30319
+ logs,
30320
+ sources,
30321
+ status,
30322
+ stop,
30323
+ start,
30324
+ remove,
30325
+ events,
30326
+ follow,
30327
+ ack,
30328
+ create
30329
+ };
30330
+ var SUBCOMMAND_NAMES = Object.keys(SUBCOMMANDS).join(", ");
30331
+ async function runMonitor(argv) {
30332
+ const [subcommand, ...args] = argv;
30333
+ const target = {
30334
+ kind: flag(args, "target-kind") ?? "cli",
30335
+ id: flag(args, "target-id") ?? process.env.SOURCEFED_TARGET_ID ?? os7.hostname()
30336
+ };
30337
+ const client = await connectDaemonClient({
30338
+ name: "sourcefed-cli",
30339
+ url: process.env.SOURCEFED_DAEMON_URL ?? defaultDaemonUrl()
30340
+ });
30341
+ const run = subcommand ? SUBCOMMANDS[subcommand] : void 0;
30342
+ if (!run) {
30343
+ await client.close();
30344
+ throw new Error(`monitor expects ${SUBCOMMAND_NAMES}`);
30345
+ }
30346
+ try {
30347
+ await run({ client, target, args });
30348
+ } finally {
30349
+ await client.close();
30350
+ }
30351
+ }
30352
+
30353
+ // packages/cli/src/utils/setup.ts
30354
+ import { execFile } from "node:child_process";
30355
+ import { readFileSync } from "node:fs";
30356
+ import { fileURLToPath } from "node:url";
30357
+ function setupSpec() {
30358
+ const manifest = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
30359
+ if (manifest.name === "@fdcn/sourcefed" && manifest.version) return `@fdcn/sourcefed@${manifest.version}`;
30360
+ return "@fdcn/sourcefed@latest";
30361
+ }
30362
+ async function setupHost(name, args, spec) {
30363
+ console.error(`[sourcefed] installing ${spec} into ${name}`);
30364
+ try {
30365
+ await execFile(name, args);
30366
+ console.error(`[sourcefed] ${name} configured \u2014 restart ${name} to load the plugin`);
30367
+ } catch (error2) {
30368
+ const code = error2 && typeof error2 === "object" && "code" in error2 ? String(error2.code) : "";
30369
+ if (code === "ENOENT") {
30370
+ console.error(`[sourcefed] ${name} is not on PATH \u2014 install it manually:`);
30371
+ console.error(name === "opencode" ? ` add "${spec}" to the plugin array in ~/.config/opencode/opencode.jsonc and tui.json` : ` run: pi install ${spec}`);
30372
+ return;
30373
+ }
30374
+ throw error2;
30375
+ }
30376
+ }
30377
+
30378
+ // packages/cli/src/commands/setup.ts
30379
+ async function runSetup(args) {
30380
+ const spec = setupSpec();
30381
+ const requested = args[0] ?? "all";
30382
+ if (requested === "opencode" || requested === "all") {
30383
+ await setupHost("opencode", ["plugin", spec, "--global"], spec);
30384
+ }
30385
+ if (requested === "pi" || requested === "all") {
30386
+ await setupHost("pi", ["install", `npm:${spec}`], `npm:${spec}`);
30387
+ }
30388
+ }
30389
+
30037
30390
  // packages/cli/src/skills/command.ts
30038
30391
  import { readFile as readFile7, readdir as readdir2, stat as stat2 } from "node:fs/promises";
30039
30392
  import path9 from "node:path";
@@ -30042,7 +30395,7 @@ import path9 from "node:path";
30042
30395
  import { statSync } from "node:fs";
30043
30396
  import { readdir, readFile as readFile6, stat } from "node:fs/promises";
30044
30397
  import path8 from "node:path";
30045
- import { fileURLToPath } from "node:url";
30398
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
30046
30399
 
30047
30400
  // packages/cli/src/skills/frontmatter.ts
30048
30401
  function parseFrontmatter(content) {
@@ -30122,7 +30475,7 @@ async function discoverSkills() {
30122
30475
  return skills;
30123
30476
  }
30124
30477
  function skillRoot() {
30125
- let dir = path8.dirname(fileURLToPath(import.meta.url));
30478
+ let dir = path8.dirname(fileURLToPath2(import.meta.url));
30126
30479
  for (let depth = 0; depth < 6; depth++) {
30127
30480
  const marker = path8.join(dir, "data");
30128
30481
  if (statSync(marker, { throwIfNoEntry: false })?.isDirectory()) return dir;
@@ -30130,7 +30483,7 @@ function skillRoot() {
30130
30483
  if (parent === dir) break;
30131
30484
  dir = parent;
30132
30485
  }
30133
- return path8.dirname(fileURLToPath(import.meta.url));
30486
+ return path8.dirname(fileURLToPath2(import.meta.url));
30134
30487
  }
30135
30488
 
30136
30489
  // packages/cli/src/skills/command.ts
@@ -30226,252 +30579,31 @@ ${content}
30226
30579
  }
30227
30580
 
30228
30581
  // packages/cli/src/index.ts
30582
+ var COMMANDS = {
30583
+ daemon: runDaemon,
30584
+ mcp: runMcp,
30585
+ monitor: runMonitor,
30586
+ skills: runSkillsCommand,
30587
+ setup: runSetup
30588
+ };
30229
30589
  async function main(argv = process.argv.slice(2)) {
30230
- const [command, subcommand, ...rest] = argv;
30231
- if (command === "daemon") {
30232
- await runDaemon([subcommand, ...rest].filter((value) => value !== void 0));
30233
- return;
30234
- }
30235
- if (command === "mcp") {
30236
- await runMcp([subcommand, ...rest].filter((value) => value !== void 0));
30237
- return;
30238
- }
30239
- if (command === "monitor") {
30240
- await runMonitor(subcommand, rest);
30241
- return;
30242
- }
30243
- if (command === "skills") {
30244
- await runSkillsCommand([subcommand, ...rest].filter((value) => value !== void 0));
30245
- return;
30246
- }
30247
- printUsage();
30248
- process.exitCode = 1;
30249
- }
30250
- function createDaemon() {
30251
- return new SourcefedDaemon({
30252
- stateDir: process.env.SOURCEFED_STATE_DIR
30253
- });
30254
- }
30255
- async function runDaemon(args) {
30256
- const port = Number(flag(args, "port") ?? process.env.SOURCEFED_DAEMON_PORT ?? 18787);
30257
- const hostname = flag(args, "host") ?? process.env.SOURCEFED_DAEMON_HOST ?? "127.0.0.1";
30258
- const token = process.env.SOURCEFED_DAEMON_TOKEN;
30259
- if (!isLoopback(hostname) && !token) {
30260
- throw new Error("binding the daemon to a non-loopback address requires SOURCEFED_DAEMON_TOKEN");
30261
- }
30262
- const daemon = createDaemon();
30263
- await daemon.start();
30264
- let server;
30265
- try {
30266
- server = await serveHttp({
30267
- hostname,
30268
- port,
30269
- handler: (request) => handleDaemonHttpRequest(daemon, request, { token })
30270
- });
30271
- const webhookServer = await startWebhookServer(daemon.runtime);
30272
- console.error(webhookServer ? `[sourcefed] webhook listener on http://${process.env.SOURCEFED_WEBHOOK_HOST ?? "127.0.0.1"}:${webhookServer.port}` : "[sourcefed] no separate webhook listener (webhooks served on daemon port)");
30273
- } catch (error2) {
30274
- await daemon.stop();
30275
- throw error2;
30276
- }
30277
- console.error(`[sourcefed] daemon on http://${hostname}:${server.port} (rpc, events, webhooks)`);
30278
- await new Promise(() => {
30279
- });
30280
- }
30281
- function isLoopback(hostname) {
30282
- return hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
30283
- }
30284
- async function runMcp(args) {
30285
- const mode = args[0] ?? "--stdio";
30286
- if (mode === "--stdio") {
30287
- const daemon2 = createDaemon();
30288
- const mcp2 = createSourcefedStdio(daemon2);
30289
- await daemon2.start();
30290
- const webhookServer = await startWebhookServer(mcp2.runtime);
30291
- await serveStdio(mcp2.factory);
30292
- await webhookServer?.stop();
30293
- await mcp2.close();
30294
- return;
30295
- }
30296
- if (mode !== "--http") {
30297
- throw new Error("mcp expects --stdio or --http");
30298
- }
30299
- const port = Number(flag(args, "port") ?? process.env.SOURCEFED_MCP_PORT ?? 18788);
30300
- const hostname = flag(args, "host") ?? process.env.SOURCEFED_MCP_HOST ?? "127.0.0.1";
30301
- const token = process.env.SOURCEFED_DAEMON_TOKEN;
30302
- if (!isLoopback(hostname) && !token) {
30303
- throw new Error("binding the MCP server to a non-loopback address requires SOURCEFED_DAEMON_TOKEN");
30304
- }
30305
- const daemon = createDaemon();
30306
- const mcp = createSourcefedMcp(daemon);
30307
- await daemon.start();
30308
- let server;
30309
- try {
30310
- server = await serveHttp({
30311
- hostname,
30312
- port,
30313
- handler: (request) => {
30314
- const url2 = new URL(request.url);
30315
- if (url2.pathname === "/mcp" && token && request.headers.get("authorization") !== `Bearer ${token}`) {
30316
- return new Response("unauthorized", { status: 401 });
30317
- }
30318
- return url2.pathname === "/mcp" ? mcp.handler.fetch(request) : daemon.webhook(request);
30319
- }
30320
- });
30321
- } catch (error2) {
30322
- await mcp.close();
30323
- throw error2;
30324
- }
30325
- console.error(`[sourcefed] MCP server on http://${hostname}:${server.port}/mcp`);
30326
- await new Promise(() => {
30327
- });
30328
- }
30329
- async function startWebhookServer(runtime) {
30330
- const enabled = Boolean(
30331
- process.env.SOURCEFED_ENABLE_WEBHOOKS === "1" || process.env.SOURCEFED_WEBHOOK_PORT || process.env.SOURCEFED_GITHUB_WEBHOOK_SECRET || process.env.SOURCEFED_SLACK_SIGNING_SECRET
30332
- );
30333
- if (!enabled) return void 0;
30334
- try {
30335
- return await serveHttp({
30336
- hostname: process.env.SOURCEFED_WEBHOOK_HOST ?? "127.0.0.1",
30337
- port: Number(process.env.SOURCEFED_WEBHOOK_PORT ?? 8788),
30338
- handler: (request) => runtime.webhook(request)
30339
- });
30340
- } catch (error2) {
30341
- console.error(`[sourcefed] webhook listener unavailable; continuing with MCP and polling: ${error2 instanceof Error ? error2.message : String(error2)}`);
30342
- return void 0;
30343
- }
30344
- }
30345
- async function runMonitor(subcommand, args) {
30346
- const target = {
30347
- kind: flag(args, "target-kind") ?? "cli",
30348
- id: flag(args, "target-id") ?? process.env.SOURCEFED_TARGET_ID ?? os7.hostname()
30349
- };
30350
- const clientOptions = {
30351
- name: "sourcefed-cli",
30352
- url: process.env.SOURCEFED_DAEMON_URL ?? defaultDaemonUrl()
30353
- };
30354
- const client = await connectDaemonClient(clientOptions);
30355
- try {
30356
- if (subcommand === "list") {
30357
- const result = await client.request("monitor.list", { target });
30358
- printMonitors(result?.monitors ?? []);
30359
- return;
30360
- }
30361
- if (subcommand === "logs") {
30362
- const result = await client.request("monitor.logs", { target });
30363
- printLogs(result?.logs ?? []);
30364
- return;
30365
- }
30366
- if (subcommand === "sources") {
30367
- printResult(await client.request("daemon.sourceTypes"));
30368
- return;
30369
- }
30370
- const id = flag(args, "id");
30371
- if ((subcommand === "status" || subcommand === "stop" || subcommand === "start" || subcommand === "remove") && !id) throw new Error(`${subcommand} requires --id`);
30372
- if (subcommand === "status" || subcommand === "stop" || subcommand === "start") {
30373
- printResult(await client.request(`monitor.${subcommand}`, { target, id }));
30374
- return;
30375
- }
30376
- if (subcommand === "remove") {
30377
- if (!args.includes("--yes")) throw new Error("remove is destructive: pass --yes to confirm");
30378
- printResult(await client.request("monitor.remove", { target, id }));
30379
- return;
30380
- }
30381
- if (subcommand === "events") {
30382
- printResult(await client.request("monitor.events", { target }));
30383
- return;
30384
- }
30385
- if (subcommand === "ack") {
30386
- const eventIDs = (flag(args, "event-id") ?? "").split(",").filter(Boolean);
30387
- if (eventIDs.length === 0) throw new Error("ack requires --event-id <id>[,<id>...]");
30388
- printResult(await client.request("monitor.ack", { target, eventIDs }));
30389
- return;
30390
- }
30391
- if (subcommand === "create") {
30392
- const sourceType = flag(args, "source-type");
30393
- const sourceTypes = await client.request("daemon.sourceTypes");
30394
- if (!sourceType || !sourceTypes.sourceTypes?.includes(sourceType)) {
30395
- throw new Error(`create requires --source-type ${sourceTypes.sourceTypes?.join("|") ?? "unknown"}`);
30396
- }
30397
- const input = {
30398
- name: flag(args, "name") ?? sourceType,
30399
- sourceType,
30400
- target
30401
- };
30402
- const keyMap = {
30403
- "issue-key": "issueKey",
30404
- repo: "repo",
30405
- "channel-id": "channelId",
30406
- "thread-ts": "threadTs",
30407
- "thread-url": "threadUrl"
30408
- };
30409
- for (const key of Object.keys(keyMap)) {
30410
- const value = flag(args, key);
30411
- if (value) input[keyMap[key]] = value;
30412
- }
30413
- const prNumber = flag(args, "pr-number");
30414
- if (prNumber) input.prNumber = Number(prNumber);
30415
- const pollIntervalSec = flag(args, "poll-interval-sec");
30416
- if (pollIntervalSec) input.pollIntervalSec = Number(pollIntervalSec);
30417
- printResult(await client.request("monitor.create", input));
30418
- return;
30419
- }
30420
- } finally {
30421
- await client.close();
30422
- }
30423
- throw new Error("monitor expects create, list, status, stop, start, remove, events, ack, logs, or sources");
30424
- }
30425
- function flag(args, name) {
30426
- const index = args.indexOf(`--${name}`);
30427
- return index >= 0 ? args[index + 1] : void 0;
30428
- }
30429
- function printMonitors(monitors) {
30430
- if (monitors.length === 0) {
30431
- console.log(dim("no monitors for this target"));
30432
- return;
30433
- }
30434
- for (const monitor of monitors) {
30435
- const status = monitor.enabled ? green("enabled") : red("stopped");
30436
- const delivery = dim(monitor.delivery);
30437
- console.log(`${monitor.icon} ${monitor.describe} ${status} ${delivery}`);
30438
- }
30439
- }
30440
- function printLogs(logs) {
30441
- if (logs.length === 0) {
30442
- console.log(dim("no notifications sent yet"));
30590
+ const [command, ...args] = argv;
30591
+ const run = command ? COMMANDS[command] : void 0;
30592
+ if (!run) {
30593
+ printUsage();
30594
+ process.exitCode = 1;
30443
30595
  return;
30444
30596
  }
30445
- for (const entry of logs) {
30446
- const time3 = dim(new Date(entry.at).toLocaleString());
30447
- const marker = entry.actionable ? yellow("\u25B6") : dim("\xB7");
30448
- console.log(`${marker} ${entry.icon} ${time3} ${entry.describe}`);
30449
- console.log(` ${entry.summary}`);
30450
- if (entry.body) console.log(dim(` ${entry.body}`));
30451
- }
30452
- }
30453
- function green(text) {
30454
- return `\x1B[32m${text}\x1B[0m`;
30455
- }
30456
- function red(text) {
30457
- return `\x1B[31m${text}\x1B[0m`;
30458
- }
30459
- function yellow(text) {
30460
- return `\x1B[33m${text}\x1B[0m`;
30461
- }
30462
- function dim(text) {
30463
- return `\x1B[2m${text}\x1B[0m`;
30464
- }
30465
- function printResult(result) {
30466
- console.log(JSON.stringify(result, null, 2));
30597
+ await run(args);
30467
30598
  }
30468
30599
  function printUsage() {
30469
30600
  console.error("sourcefed daemon [--port PORT] [--host HOST]");
30470
30601
  console.error("sourcefed mcp --stdio|--http [--port PORT]");
30471
- console.error("sourcefed monitor create|list|status|stop|start|remove|events|ack|logs|sources [options]");
30602
+ console.error("sourcefed monitor create|list|status|stop|start|remove|events|follow|ack|logs|sources [options]");
30472
30603
  console.error("sourcefed skills [list|get <name>|path [name]]");
30604
+ console.error("sourcefed setup [opencode|pi]");
30473
30605
  }
30474
- if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath2(import.meta.url)) {
30606
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath3(import.meta.url)) {
30475
30607
  main().catch((error2) => {
30476
30608
  console.error(error2 instanceof Error ? error2.message : String(error2));
30477
30609
  process.exitCode = 1;