sparkecoder 0.1.118 → 0.1.120

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 (110) hide show
  1. package/dist/agent/index.d.ts +3 -3
  2. package/dist/agent/index.js +3 -1
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +163 -18
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +2 -2
  7. package/dist/{index-Bcz0aCAR.d.ts → index-DczYH89U.d.ts} +104 -104
  8. package/dist/index.d.ts +5 -5
  9. package/dist/index.js +162 -17
  10. package/dist/index.js.map +1 -1
  11. package/dist/{schema-BWbWmfDQ.d.ts → schema-DxrKyetI.d.ts} +3 -3
  12. package/dist/{search-DOzC4ojH.d.ts → search-CVVfuBPZ.d.ts} +4 -4
  13. package/dist/server/index.js +162 -17
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/tools/index.d.ts +3 -3
  16. package/dist/tools/index.js.map +1 -1
  17. package/package.json +1 -1
  18. package/web/.next/BUILD_ID +1 -1
  19. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  20. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  21. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  22. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  23. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  65. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  74. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  82. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  85. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  86. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  89. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  90. package/web/.next/standalone/web/.next/server/app/settings.rsc +1 -1
  91. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +1 -1
  92. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  93. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  94. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  96. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  97. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  98. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  99. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  100. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  101. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  102. /package/web/.next/standalone/web/.next/static/{T8x1J_CS0n9FaWBr5GhLe → static/uy1OnyxIm3QeGGgKEmxAj}/_buildManifest.js +0 -0
  103. /package/web/.next/standalone/web/.next/static/{T8x1J_CS0n9FaWBr5GhLe → static/uy1OnyxIm3QeGGgKEmxAj}/_clientMiddlewareManifest.json +0 -0
  104. /package/web/.next/standalone/web/.next/static/{T8x1J_CS0n9FaWBr5GhLe → static/uy1OnyxIm3QeGGgKEmxAj}/_ssgManifest.js +0 -0
  105. /package/web/.next/standalone/web/.next/static/{static/T8x1J_CS0n9FaWBr5GhLe → uy1OnyxIm3QeGGgKEmxAj}/_buildManifest.js +0 -0
  106. /package/web/.next/standalone/web/.next/static/{static/T8x1J_CS0n9FaWBr5GhLe → uy1OnyxIm3QeGGgKEmxAj}/_clientMiddlewareManifest.json +0 -0
  107. /package/web/.next/standalone/web/.next/static/{static/T8x1J_CS0n9FaWBr5GhLe → uy1OnyxIm3QeGGgKEmxAj}/_ssgManifest.js +0 -0
  108. /package/web/.next/static/{T8x1J_CS0n9FaWBr5GhLe → uy1OnyxIm3QeGGgKEmxAj}/_buildManifest.js +0 -0
  109. /package/web/.next/static/{T8x1J_CS0n9FaWBr5GhLe → uy1OnyxIm3QeGGgKEmxAj}/_clientMiddlewareManifest.json +0 -0
  110. /package/web/.next/static/{T8x1J_CS0n9FaWBr5GhLe → uy1OnyxIm3QeGGgKEmxAj}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -1160,7 +1160,7 @@ function loadConfig(configPath, workingDirectory) {
1160
1160
  ...config,
1161
1161
  server: {
1162
1162
  port: config.server.port,
1163
- host: config.server.host ?? "127.0.0.1",
1163
+ host: config.server.host ?? "0.0.0.0",
1164
1164
  publicUrl: config.server.publicUrl
1165
1165
  },
1166
1166
  resolvedWorkingDirectory,
@@ -1327,7 +1327,7 @@ function createDefaultConfig() {
1327
1327
  },
1328
1328
  server: {
1329
1329
  port: 3141,
1330
- host: "127.0.0.1"
1330
+ host: "0.0.0.0"
1331
1331
  },
1332
1332
  databasePath: "./sparkecoder.db"
1333
1333
  };
@@ -8545,9 +8545,50 @@ function isSlackConfigured() {
8545
8545
  function getSlackSigningSecret() {
8546
8546
  return readSlackConfig()?.signingSecret ?? null;
8547
8547
  }
8548
+ function getSlackBotToken() {
8549
+ return readSlackConfig()?.botToken ?? null;
8550
+ }
8548
8551
  function getDefaultOrchestratorName() {
8549
8552
  return readSlackConfig()?.defaultOrchestratorName ?? null;
8550
8553
  }
8554
+ function getCachedSlackSelfIdentity() {
8555
+ const cfg = readSlackConfig();
8556
+ if (!cfg) return null;
8557
+ if (cachedSelf && cachedSelf.token === cfg.botToken) return cachedSelf.identity;
8558
+ return null;
8559
+ }
8560
+ async function ensureSlackSelfIdentity() {
8561
+ const cfg = readSlackConfig();
8562
+ if (!cfg) return null;
8563
+ if (cachedSelf && cachedSelf.token === cfg.botToken) return cachedSelf.identity;
8564
+ if (selfInflight) return selfInflight;
8565
+ selfInflight = (async () => {
8566
+ try {
8567
+ const res = await fetch("https://slack.com/api/auth.test", {
8568
+ method: "POST",
8569
+ headers: { Authorization: `Bearer ${cfg.botToken}` }
8570
+ });
8571
+ const data = await res.json().catch(() => ({}));
8572
+ if (!data?.ok) {
8573
+ console.warn(`[slack] auth.test failed: ${data?.error || `HTTP ${res.status}`}`);
8574
+ return null;
8575
+ }
8576
+ const identity = {
8577
+ botUserId: String(data.user_id || ""),
8578
+ botId: String(data.bot_id || ""),
8579
+ teamId: data.team_id ? String(data.team_id) : void 0
8580
+ };
8581
+ cachedSelf = { token: cfg.botToken, identity };
8582
+ return identity;
8583
+ } catch (err) {
8584
+ console.warn("[slack] auth.test error:", err?.message || err);
8585
+ return null;
8586
+ } finally {
8587
+ selfInflight = null;
8588
+ }
8589
+ })();
8590
+ return selfInflight;
8591
+ }
8551
8592
  function getSlackAllowlistPolicy() {
8552
8593
  try {
8553
8594
  const cfg = getConfig();
@@ -8561,6 +8602,62 @@ function getSlackAllowlistPolicy() {
8561
8602
  return { allowedUsers: [], allowedChannels: [], allowDmsFromAnyone: true };
8562
8603
  }
8563
8604
  }
8605
+ async function fetchSlackUserName(userId) {
8606
+ const token = getSlackBotToken();
8607
+ if (!token) return null;
8608
+ try {
8609
+ const res = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, {
8610
+ headers: { Authorization: `Bearer ${token}` }
8611
+ });
8612
+ const data = await res.json().catch(() => ({}));
8613
+ if (!data?.ok) {
8614
+ console.warn(`[slack] users.info(${userId}) failed: ${data?.error || `HTTP ${res.status}`}`);
8615
+ return null;
8616
+ }
8617
+ const profile = data.user?.profile || {};
8618
+ const name = profile.display_name_normalized || profile.display_name || profile.real_name_normalized || profile.real_name || data.user?.real_name || data.user?.name || null;
8619
+ return name ? String(name) : null;
8620
+ } catch (err) {
8621
+ console.warn(`[slack] users.info(${userId}) error:`, err?.message || err);
8622
+ return null;
8623
+ }
8624
+ }
8625
+ async function resolveSlackUserName(userId) {
8626
+ if (!userId) return null;
8627
+ const now = Date.now();
8628
+ const hit = userNameCache.get(userId);
8629
+ if (hit && hit.expiresAt > now) return hit.name;
8630
+ const inflight = userInflight.get(userId);
8631
+ if (inflight) return inflight;
8632
+ const p = (async () => {
8633
+ const name = await fetchSlackUserName(userId);
8634
+ userNameCache.set(userId, {
8635
+ name,
8636
+ expiresAt: now + (name ? USER_TTL_MS : USER_FAIL_TTL_MS)
8637
+ });
8638
+ userInflight.delete(userId);
8639
+ return name;
8640
+ })();
8641
+ userInflight.set(userId, p);
8642
+ return p;
8643
+ }
8644
+ async function normalizeSlackMentions(text) {
8645
+ if (!text) return text;
8646
+ const userMentionRe = /<@([UW][A-Z0-9]+)(?:\|([^>]+))?>/g;
8647
+ const userIds = /* @__PURE__ */ new Set();
8648
+ for (const m of text.matchAll(userMentionRe)) {
8649
+ if (!m[2]) userIds.add(m[1]);
8650
+ }
8651
+ if (userIds.size > 0) {
8652
+ await Promise.all([...userIds].map((id) => resolveSlackUserName(id)));
8653
+ }
8654
+ return text.replace(userMentionRe, (_full, id, label) => {
8655
+ if (label) return `${label} <@${id}>`;
8656
+ const cached = userNameCache.get(id);
8657
+ const name = cached?.name;
8658
+ return name ? `${name} <@${id}>` : `<@${id}>`;
8659
+ });
8660
+ }
8564
8661
  function getSlackDeniedReplyPolicy() {
8565
8662
  try {
8566
8663
  const cfg = getConfig();
@@ -8573,11 +8670,17 @@ function getSlackDeniedReplyPolicy() {
8573
8670
  return { enabled: true, template: DEFAULT_DENIED_TEMPLATE };
8574
8671
  }
8575
8672
  }
8576
- var DEFAULT_DENIED_TEMPLATE;
8673
+ var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userNameCache, userInflight, DEFAULT_DENIED_TEMPLATE;
8577
8674
  var init_client3 = __esm({
8578
8675
  "src/integrations/slack/client.ts"() {
8579
8676
  "use strict";
8580
8677
  init_config();
8678
+ cachedSelf = null;
8679
+ selfInflight = null;
8680
+ USER_TTL_MS = 60 * 60 * 1e3;
8681
+ USER_FAIL_TTL_MS = 5 * 60 * 1e3;
8682
+ userNameCache = /* @__PURE__ */ new Map();
8683
+ userInflight = /* @__PURE__ */ new Map();
8581
8684
  DEFAULT_DENIED_TEMPLATE = "Sorry, you don't have permission to use this bot. (Contact the bot owner if you think this is a mistake.)";
8582
8685
  }
8583
8686
  });
@@ -8592,21 +8695,36 @@ function markThreadOwned(channel, threadTs) {
8592
8695
  function isThreadOwned(channel, threadTs) {
8593
8696
  return ownedThreads.has(threadKey(channel, threadTs));
8594
8697
  }
8595
- function stripMention(text) {
8596
- return String(text || "").replace(/<@[^>]+>/g, "").trim();
8698
+ function isSelfAuthored(event, self) {
8699
+ if (!self) return true;
8700
+ if (self.botId && event.bot_id && event.bot_id === self.botId) return true;
8701
+ if (self.botUserId && event.user && event.user === self.botUserId) return true;
8702
+ return false;
8597
8703
  }
8598
- function slackEventToInboundResult(event) {
8704
+ function slackEventToInboundResult(event, opts = {}) {
8599
8705
  if (!event) return { event: null, dropReason: "empty_text" };
8600
- if (event.bot_id) return { event: null, dropReason: "bot_message" };
8601
- if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
8706
+ const self = opts.self ?? getCachedSlackSelfIdentity();
8707
+ const isBotAuthored = !!event.bot_id || event.type === "message" && event.subtype === "bot_message";
8708
+ if (isBotAuthored && isSelfAuthored(event, self)) {
8602
8709
  return { event: null, dropReason: "bot_message" };
8603
8710
  }
8711
+ if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
8712
+ return { event: null, dropReason: "ignored_subtype" };
8713
+ }
8604
8714
  const isDm = event.type === "message" && event.channel_type === "im";
8605
8715
  const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
8716
+ const isNonThreadChannelMsg = event.type === "message" && !isDm && !isThreadReply && (event.channel_type === "channel" || event.channel_type === "group" || event.channel_type === "mpim" || // Some payload shapes omit channel_type for channel messages.
8717
+ typeof event.channel === "string");
8606
8718
  if (event.type !== "app_mention" && !isDm && !isThreadReply) {
8719
+ if (isNonThreadChannelMsg) {
8720
+ return { event: null, dropReason: "non_thread_channel_msg" };
8721
+ }
8722
+ if (event.type !== "message") {
8723
+ return { event: null, dropReason: "non_message_event" };
8724
+ }
8607
8725
  return { event: null, dropReason: "unsupported_type" };
8608
8726
  }
8609
- const text = event.type === "app_mention" ? stripMention(event.text) : (event.text ?? "").trim();
8727
+ const text = (event.text ?? "").trim();
8610
8728
  if (!text) return { event: null, dropReason: "empty_text" };
8611
8729
  const policy = getSlackAllowlistPolicy();
8612
8730
  const userAllowlistActive = policy.allowedUsers.length > 0;
@@ -8674,7 +8792,6 @@ var init_slack = __esm({
8674
8792
  }
8675
8793
  };
8676
8794
  IGNORED_MESSAGE_SUBTYPES = /* @__PURE__ */ new Set([
8677
- "bot_message",
8678
8795
  "message_changed",
8679
8796
  "message_deleted",
8680
8797
  "channel_join",
@@ -14576,16 +14693,18 @@ init_webhook_events();
14576
14693
  init_inbox();
14577
14694
  var recentlyHandled = /* @__PURE__ */ new Map();
14578
14695
  var MAX_RECENT = 1e3;
14579
- function alreadyHandled(channel, ts) {
14696
+ function wasHandled(channel, ts) {
14580
14697
  if (!channel || !ts) return false;
14698
+ return recentlyHandled.has(`${channel}\u241F${ts}`);
14699
+ }
14700
+ function markHandled(channel, ts) {
14701
+ if (!channel || !ts) return;
14581
14702
  const key2 = `${channel}\u241F${ts}`;
14582
- if (recentlyHandled.has(key2)) return true;
14583
14703
  recentlyHandled.set(key2, Date.now());
14584
14704
  if (recentlyHandled.size > MAX_RECENT) {
14585
14705
  const oldest = recentlyHandled.keys().next().value;
14586
14706
  if (oldest) recentlyHandled.delete(oldest);
14587
14707
  }
14588
- return false;
14589
14708
  }
14590
14709
  var slack = new Hono6();
14591
14710
  slack.post("/events", async (c) => {
@@ -14622,11 +14741,12 @@ slack.post("/events", async (c) => {
14622
14741
  textSnippet: typeof ev.text === "string" ? ev.text : void 0,
14623
14742
  meta: { ts: ev.ts, thread_ts: ev.thread_ts, team: ev.team, event_subtype: ev.subtype }
14624
14743
  });
14625
- if (alreadyHandled(ev.channel, ev.ts)) {
14744
+ if (wasHandled(ev.channel, ev.ts)) {
14626
14745
  updateEvent(auditId, { status: "dropped", dropReason: "duplicate_delivery" });
14627
14746
  return c.json({ ok: true });
14628
14747
  }
14629
- const { event: inbound, dropReason } = slackEventToInboundResult(ev);
14748
+ const self = await ensureSlackSelfIdentity();
14749
+ const { event: inbound, dropReason } = slackEventToInboundResult(ev, { self });
14630
14750
  if (inbound) {
14631
14751
  const isThreadReply = ev.type === "message" && ev.channel_type !== "im" && typeof ev.thread_ts === "string" && ev.thread_ts !== ev.ts;
14632
14752
  if (isThreadReply) {
@@ -14645,7 +14765,18 @@ slack.post("/events", async (c) => {
14645
14765
  }
14646
14766
  const orchestratorId = await findOrCreateOrchestratorId();
14647
14767
  if (orchestratorId) {
14768
+ inbound.content = await normalizeSlackMentions(inbound.content);
14769
+ if (ev.user) {
14770
+ const speakerName = await resolveSlackUserName(ev.user);
14771
+ if (speakerName) {
14772
+ inbound.content = inbound.content.replace(
14773
+ `user=${ev.user}`,
14774
+ `user=${speakerName} <@${ev.user}>`
14775
+ );
14776
+ }
14777
+ }
14648
14778
  pushToInbox(orchestratorId, inbound);
14779
+ markHandled(ev.channel, ev.ts);
14649
14780
  updateEvent(auditId, { status: "routed", sessionId: orchestratorId });
14650
14781
  } else {
14651
14782
  updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
@@ -15696,13 +15827,15 @@ async function startServer(options = {}) {
15696
15827
  if (!options.quiet) console.warn(`[scheduler] start skipped: ${err.message}`);
15697
15828
  }
15698
15829
  const port = options.port || config.server.port;
15699
- const host = options.host || config.server.host || "0.0.0.0";
15830
+ const envHost = process.env.SPARKECODER_API_HOST || process.env.SPARKECODER_HOST;
15831
+ const host = envHost || options.host || config.server.host || "0.0.0.0";
15832
+ const hostSource = envHost ? process.env.SPARKECODER_API_HOST ? "env SPARKECODER_API_HOST" : "env SPARKECODER_HOST" : options.host ? "--host flag" : config.server.host ? "config.server.host" : "default";
15700
15833
  const publicUrl = options.publicUrl || config.server.publicUrl;
15701
15834
  const app = await createApp({ quiet: options.quiet });
15702
15835
  if (!options.quiet) {
15703
15836
  console.log(`
15704
15837
  \u{1F680} SparkECoder API Server`);
15705
- console.log(` \u2192 Running at http://${host}:${port}`);
15838
+ console.log(` \u2192 Binding to ${host}:${port} (source: ${hostSource})`);
15706
15839
  if (publicUrl) {
15707
15840
  console.log(` \u2192 Public URL: ${publicUrl}`);
15708
15841
  }
@@ -15711,10 +15844,22 @@ async function startServer(options = {}) {
15711
15844
  console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
15712
15845
  `);
15713
15846
  }
15847
+ if (host === "127.0.0.1" || host === "localhost") {
15848
+ console.log(`[sparkecoder] \u26A0 API bound to ${host} only \u2014 not reachable from outside the machine.`);
15849
+ console.log(`[sparkecoder] For tunnels/reverse proxies (Modal/Fly/Docker), set --host 0.0.0.0 or SPARKECODER_API_HOST=0.0.0.0.`);
15850
+ }
15714
15851
  serverInstance = serve({
15715
15852
  fetch: app.fetch,
15716
15853
  port,
15717
15854
  hostname: host
15855
+ }, (info) => {
15856
+ const actual = `${info.address}:${info.port}`;
15857
+ const requested = `${host}:${port}`;
15858
+ if (info.address === "127.0.0.1" && host !== "127.0.0.1" && host !== "localhost") {
15859
+ console.warn(`[sparkecoder] \u2717 API listener bound to ${actual} but ${requested} was requested. External requests will be refused.`);
15860
+ } else {
15861
+ console.log(`[sparkecoder] \u2713 API listening on ${actual}`);
15862
+ }
15718
15863
  });
15719
15864
  let webPort;
15720
15865
  let webStarted;
@@ -16782,7 +16927,7 @@ var program = new Command();
16782
16927
  program.name("sparkecoder").description("AI coding agent - just type sparkecoder to start chatting").version(getCliVersion()).option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI when auto-starting server").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--dangerously-skip-approvals", "Auto-approve all tool calls (no confirmation prompts)").action(async (options) => {
16783
16928
  await runChat(options);
16784
16929
  });
16785
- program.command("server").description("Start the SparkECoder server (API + Web UI)").option("-p, --port <port>", "API server port", "3141").option("-h, --host <host>", "Server host", "127.0.0.1").option("-c, --config <path>", "Path to config file").option("-w, --working-dir <path>", "Working directory").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--setup-secret <secret>", "Setup secret (or short-lived JWT) used for /auth/register and /tunnels when the remote server has SETUP_SECRET configured. Equivalent to setting SPARKECODER_SETUP_SECRET env.").action(async (options) => {
16930
+ program.command("server").description("Start the SparkECoder server (API + Web UI)").option("-p, --port <port>", "API server port", "3141").option("-h, --host <host>", "Server bind address (use 127.0.0.1 to restrict to loopback). Overridable via SPARKECODER_API_HOST / SPARKECODER_HOST env.", "0.0.0.0").option("-c, --config <path>", "Path to config file").option("-w, --working-dir <path>", "Working directory").option("--web-port <port>", "Web UI port", "6969").option("--no-web", "Do not start web UI").option("--public-url <url>", "Public URL for web UI to connect to API (for Docker/remote access)").option("-v, --verbose", "Enable verbose logging for web server").option("--setup-secret <secret>", "Setup secret (or short-lived JWT) used for /auth/register and /tunnels when the remote server has SETUP_SECRET configured. Equivalent to setting SPARKECODER_SETUP_SECRET env.").action(async (options) => {
16786
16931
  if (options.setupSecret) {
16787
16932
  process.env.SPARKECODER_SETUP_SECRET = options.setupSecret;
16788
16933
  if (!process.env.SPARKECODER_TUNNEL_SECRET) {