hovclaw 0.1.4 → 0.1.5

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.
@@ -36,7 +36,7 @@
36
36
  <input
37
37
  id="gateway-url"
38
38
  type="text"
39
- placeholder="ws://127.0.0.1:18789"
39
+ placeholder="ws://127.0.0.1:18889"
40
40
  spellcheck="false"
41
41
  />
42
42
  </label>
package/dist/hovclaw.js CHANGED
@@ -181,7 +181,7 @@ const DEFAULT_FILE_CONFIG = {
181
181
  gateway: {
182
182
  enabled: true,
183
183
  host: "127.0.0.1",
184
- port: 18789,
184
+ port: 18889,
185
185
  mode: "local",
186
186
  tickIntervalMs: 3e4,
187
187
  webUi: {
@@ -1696,8 +1696,13 @@ async function writeFileIfMissing(filePath, content) {
1696
1696
  throw error;
1697
1697
  }
1698
1698
  }
1699
+ function isMeaningfulWorkspaceEntry(entry) {
1700
+ if (IGNORED_WORKSPACE_ENTRIES.has(entry)) return false;
1701
+ if (entry.startsWith(".")) return false;
1702
+ return true;
1703
+ }
1699
1704
  async function listMeaningfulWorkspaceEntries(workspaceDir) {
1700
- return (await fs$1.readdir(workspaceDir)).filter((entry) => !IGNORED_WORKSPACE_ENTRIES.has(entry));
1705
+ return (await fs$1.readdir(workspaceDir)).filter((entry) => isMeaningfulWorkspaceEntry(entry));
1701
1706
  }
1702
1707
  function resolveAgentWorkspaceDir(config, agentId) {
1703
1708
  return (config.agents.list.find((entry) => entry.id === agentId)?.workspace)?.trim() || config.agents.defaults.workspace;
@@ -1769,6 +1774,7 @@ function buildDefaultSystemPrompt(workspaceDir) {
1769
1774
  "",
1770
1775
  "## Messaging",
1771
1776
  "Respond with clear, direct language tailored to the user's request.",
1777
+ "Default to plain text and avoid markdown formatting unless the user asks for markdown or structured output.",
1772
1778
  "If you cannot complete a request, explain the blocker and the minimum next step."
1773
1779
  ].join("\n");
1774
1780
  }
@@ -2257,7 +2263,8 @@ var PiAgentManager = class {
2257
2263
  model: model.resolvedRef,
2258
2264
  modelResolutionReason: selectedModel.reason,
2259
2265
  source: input.meta.source,
2260
- channel: input.meta.channel
2266
+ channel: input.meta.channel,
2267
+ ...input.meta.sender ? { sender: input.meta.sender } : {}
2261
2268
  }
2262
2269
  });
2263
2270
  const exec = async () => {
@@ -2499,6 +2506,57 @@ function isTelegramParseModeError(error) {
2499
2506
  const message = error.message.toLowerCase();
2500
2507
  return message.includes("can't parse entities") || message.includes("parse entities") || message.includes("parse mode");
2501
2508
  }
2509
+ function escapeTelegramHtml(text) {
2510
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2511
+ }
2512
+ function escapeTelegramHtmlAttr(text) {
2513
+ return escapeTelegramHtml(text).replace(/"/g, "&quot;");
2514
+ }
2515
+ function stripMarkdownToPlainText(input) {
2516
+ let value = input.replace(/\r\n/g, "\n");
2517
+ value = value.replace(/^#{1,6}\s+/gm, "");
2518
+ value = value.replace(/^>\s?/gm, "");
2519
+ value = value.replace(/```(?:[a-zA-Z0-9_-]+)?\n?([\s\S]*?)```/g, (_match, code) => code.replace(/\n$/, ""));
2520
+ value = value.replace(/`([^`\n]+)`/g, "$1");
2521
+ value = value.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, "$1");
2522
+ value = value.replace(/\*\*([^*]+)\*\*/g, "$1");
2523
+ value = value.replace(/~~([^~]+)~~/g, "$1");
2524
+ value = value.replace(/\|\|([^|]+)\|\|/g, "$1");
2525
+ value = value.replace(/\*([^*\n]+)\*/g, "$1");
2526
+ value = value.replace(/_([^_\n]+)_/g, "$1");
2527
+ return value;
2528
+ }
2529
+ function renderMarkdownToTelegramHtml(input) {
2530
+ const placeholders = [];
2531
+ const toPlaceholder = (html) => {
2532
+ return `\u0000TG_PLACEHOLDER_${placeholders.push(html) - 1}\u0000`;
2533
+ };
2534
+ let value = input.replace(/\r\n/g, "\n");
2535
+ value = value.replace(/```(?:[a-zA-Z0-9_-]+)?\n?([\s\S]*?)```/g, (_match, code) => toPlaceholder(`<pre><code>${escapeTelegramHtml(code.replace(/\n$/, ""))}</code></pre>`));
2536
+ value = value.replace(/`([^`\n]+)`/g, (_match, code) => toPlaceholder(`<code>${escapeTelegramHtml(code)}</code>`));
2537
+ value = escapeTelegramHtml(value);
2538
+ value = value.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, text, href) => `<a href="${escapeTelegramHtmlAttr(href)}">${text}</a>`);
2539
+ value = value.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
2540
+ value = value.replace(/~~([^~]+)~~/g, "<s>$1</s>");
2541
+ value = value.replace(/\|\|([^|]+)\|\|/g, "<tg-spoiler>$1</tg-spoiler>");
2542
+ value = value.replace(/\*([^*\n]+)\*/g, "<i>$1</i>");
2543
+ value = value.replace(/_([^_\n]+)_/g, "<i>$1</i>");
2544
+ return value.replace(/\u0000TG_PLACEHOLDER_(\d+)\u0000/g, (_match, rawIndex) => {
2545
+ return placeholders[Number.parseInt(rawIndex, 10)] ?? "";
2546
+ });
2547
+ }
2548
+ function prepareTelegramTextCandidates(text, textMode) {
2549
+ const plain = stripMarkdownToPlainText(text);
2550
+ const fallbackPlain = plain.trim().length > 0 ? plain : text;
2551
+ if (textMode === "plain") return {
2552
+ html: escapeTelegramHtml(fallbackPlain),
2553
+ plain: fallbackPlain
2554
+ };
2555
+ return {
2556
+ html: renderMarkdownToTelegramHtml(text),
2557
+ plain: fallbackPlain
2558
+ };
2559
+ }
2502
2560
  function mediaMethod(media) {
2503
2561
  if (media.kind === "image") return {
2504
2562
  method: "sendPhoto",
@@ -2928,25 +2986,24 @@ var TelegramChannel = class {
2928
2986
  async sendRichMessage(target, text, options) {
2929
2987
  const parsedTarget = parseTargetChatId(target.chatId);
2930
2988
  for (const chunk of chunkText(text, TELEGRAM_CHUNK_LIMIT)) {
2989
+ const rendered = prepareTelegramTextCandidates(chunk, this.textMode);
2931
2990
  const payload = {
2932
2991
  chat_id: parsedTarget.chatId,
2933
- text: chunk,
2992
+ text: rendered.html,
2934
2993
  message_thread_id: parsedTarget.messageThreadId,
2935
2994
  disable_web_page_preview: true,
2936
- reply_markup: options?.inlineKeyboard ? { inline_keyboard: options.inlineKeyboard } : void 0
2995
+ reply_markup: options?.inlineKeyboard ? { inline_keyboard: options.inlineKeyboard } : void 0,
2996
+ parse_mode: "HTML"
2937
2997
  };
2938
- if (this.textMode !== "markdown") {
2939
- await this.callApi("sendMessage", payload);
2940
- continue;
2941
- }
2942
2998
  try {
2999
+ await this.callApi("sendMessage", payload);
3000
+ } catch (error) {
3001
+ if (!isTelegramParseModeError(error)) throw error;
2943
3002
  await this.callApi("sendMessage", {
2944
3003
  ...payload,
2945
- parse_mode: "Markdown"
3004
+ text: rendered.plain,
3005
+ parse_mode: void 0
2946
3006
  });
2947
- } catch (error) {
2948
- if (!isTelegramParseModeError(error)) throw error;
2949
- await this.callApi("sendMessage", payload);
2950
3007
  }
2951
3008
  }
2952
3009
  }
@@ -2981,18 +3038,24 @@ var TelegramChannel = class {
2981
3038
  caption: media.caption?.trim() || void 0,
2982
3039
  message_thread_id: parsedTarget.messageThreadId
2983
3040
  };
2984
- if (this.textMode !== "markdown" || !payload.caption) {
3041
+ if (!payload.caption) {
2985
3042
  await this.callApi(method, payload);
2986
3043
  return;
2987
3044
  }
3045
+ const renderedCaption = prepareTelegramTextCandidates(payload.caption, this.textMode);
2988
3046
  try {
2989
3047
  await this.callApi(method, {
2990
3048
  ...payload,
2991
- parse_mode: "Markdown"
3049
+ caption: renderedCaption.html,
3050
+ parse_mode: "HTML"
2992
3051
  });
2993
3052
  } catch (error) {
2994
3053
  if (!isTelegramParseModeError(error)) throw error;
2995
- await this.callApi(method, payload);
3054
+ await this.callApi(method, {
3055
+ ...payload,
3056
+ caption: renderedCaption.plain,
3057
+ parse_mode: void 0
3058
+ });
2996
3059
  }
2997
3060
  }
2998
3061
  async setReaction(target, reaction) {
@@ -6233,7 +6296,7 @@ function registerGatewayCommands(program, deps = defaultGatewayCommandDeps) {
6233
6296
  gateway.command("run").description("Run HOVClaw daemon with gateway in foreground").action(async () => {
6234
6297
  if (!config.gateway.enabled) throw new Error("Gateway is disabled in config. Enable gateway.enabled first.");
6235
6298
  process.stdout.write(`Starting HOVClaw gateway on ${config.gateway.host}:${config.gateway.port}...\n`);
6236
- await import("./src-GZDRRc5A.js");
6299
+ await import("./src-GoIM4Tv1.js");
6237
6300
  });
6238
6301
  gateway.command("open-ui").description("Open the minimal gateway web UI in your default browser").option("--no-open", "Print URL but do not open browser").option("--json", "Print JSON output").action(async (options) => {
6239
6302
  const url = resolveGatewayWebUiUrl();
package/dist/index.js CHANGED
@@ -162,7 +162,7 @@ const DEFAULT_FILE_CONFIG = {
162
162
  gateway: {
163
163
  enabled: true,
164
164
  host: "127.0.0.1",
165
- port: 18789,
165
+ port: 18889,
166
166
  mode: "local",
167
167
  tickIntervalMs: 3e4,
168
168
  webUi: {
@@ -1480,8 +1480,13 @@ async function writeFileIfMissing(filePath, content) {
1480
1480
  throw error;
1481
1481
  }
1482
1482
  }
1483
+ function isMeaningfulWorkspaceEntry(entry) {
1484
+ if (IGNORED_WORKSPACE_ENTRIES.has(entry)) return false;
1485
+ if (entry.startsWith(".")) return false;
1486
+ return true;
1487
+ }
1483
1488
  async function listMeaningfulWorkspaceEntries(workspaceDir) {
1484
- return (await fs$1.readdir(workspaceDir)).filter((entry) => !IGNORED_WORKSPACE_ENTRIES.has(entry));
1489
+ return (await fs$1.readdir(workspaceDir)).filter((entry) => isMeaningfulWorkspaceEntry(entry));
1485
1490
  }
1486
1491
  function resolveAgentWorkspaceDir(config, agentId) {
1487
1492
  return (config.agents.list.find((entry) => entry.id === agentId)?.workspace)?.trim() || config.agents.defaults.workspace;
@@ -1553,6 +1558,7 @@ function buildDefaultSystemPrompt(workspaceDir) {
1553
1558
  "",
1554
1559
  "## Messaging",
1555
1560
  "Respond with clear, direct language tailored to the user's request.",
1561
+ "Default to plain text and avoid markdown formatting unless the user asks for markdown or structured output.",
1556
1562
  "If you cannot complete a request, explain the blocker and the minimum next step."
1557
1563
  ].join("\n");
1558
1564
  }
@@ -2041,7 +2047,8 @@ var PiAgentManager = class {
2041
2047
  model: model.resolvedRef,
2042
2048
  modelResolutionReason: selectedModel.reason,
2043
2049
  source: input.meta.source,
2044
- channel: input.meta.channel
2050
+ channel: input.meta.channel,
2051
+ ...input.meta.sender ? { sender: input.meta.sender } : {}
2045
2052
  }
2046
2053
  });
2047
2054
  const exec = async () => {
@@ -2330,6 +2337,57 @@ function isTelegramParseModeError(error) {
2330
2337
  const message = error.message.toLowerCase();
2331
2338
  return message.includes("can't parse entities") || message.includes("parse entities") || message.includes("parse mode");
2332
2339
  }
2340
+ function escapeTelegramHtml(text) {
2341
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2342
+ }
2343
+ function escapeTelegramHtmlAttr(text) {
2344
+ return escapeTelegramHtml(text).replace(/"/g, "&quot;");
2345
+ }
2346
+ function stripMarkdownToPlainText(input) {
2347
+ let value = input.replace(/\r\n/g, "\n");
2348
+ value = value.replace(/^#{1,6}\s+/gm, "");
2349
+ value = value.replace(/^>\s?/gm, "");
2350
+ value = value.replace(/```(?:[a-zA-Z0-9_-]+)?\n?([\s\S]*?)```/g, (_match, code) => code.replace(/\n$/, ""));
2351
+ value = value.replace(/`([^`\n]+)`/g, "$1");
2352
+ value = value.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, "$1");
2353
+ value = value.replace(/\*\*([^*]+)\*\*/g, "$1");
2354
+ value = value.replace(/~~([^~]+)~~/g, "$1");
2355
+ value = value.replace(/\|\|([^|]+)\|\|/g, "$1");
2356
+ value = value.replace(/\*([^*\n]+)\*/g, "$1");
2357
+ value = value.replace(/_([^_\n]+)_/g, "$1");
2358
+ return value;
2359
+ }
2360
+ function renderMarkdownToTelegramHtml(input) {
2361
+ const placeholders = [];
2362
+ const toPlaceholder = (html) => {
2363
+ return `\u0000TG_PLACEHOLDER_${placeholders.push(html) - 1}\u0000`;
2364
+ };
2365
+ let value = input.replace(/\r\n/g, "\n");
2366
+ value = value.replace(/```(?:[a-zA-Z0-9_-]+)?\n?([\s\S]*?)```/g, (_match, code) => toPlaceholder(`<pre><code>${escapeTelegramHtml(code.replace(/\n$/, ""))}</code></pre>`));
2367
+ value = value.replace(/`([^`\n]+)`/g, (_match, code) => toPlaceholder(`<code>${escapeTelegramHtml(code)}</code>`));
2368
+ value = escapeTelegramHtml(value);
2369
+ value = value.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, text, href) => `<a href="${escapeTelegramHtmlAttr(href)}">${text}</a>`);
2370
+ value = value.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
2371
+ value = value.replace(/~~([^~]+)~~/g, "<s>$1</s>");
2372
+ value = value.replace(/\|\|([^|]+)\|\|/g, "<tg-spoiler>$1</tg-spoiler>");
2373
+ value = value.replace(/\*([^*\n]+)\*/g, "<i>$1</i>");
2374
+ value = value.replace(/_([^_\n]+)_/g, "<i>$1</i>");
2375
+ return value.replace(/\u0000TG_PLACEHOLDER_(\d+)\u0000/g, (_match, rawIndex) => {
2376
+ return placeholders[Number.parseInt(rawIndex, 10)] ?? "";
2377
+ });
2378
+ }
2379
+ function prepareTelegramTextCandidates(text, textMode) {
2380
+ const plain = stripMarkdownToPlainText(text);
2381
+ const fallbackPlain = plain.trim().length > 0 ? plain : text;
2382
+ if (textMode === "plain") return {
2383
+ html: escapeTelegramHtml(fallbackPlain),
2384
+ plain: fallbackPlain
2385
+ };
2386
+ return {
2387
+ html: renderMarkdownToTelegramHtml(text),
2388
+ plain: fallbackPlain
2389
+ };
2390
+ }
2333
2391
  function mediaMethod(media) {
2334
2392
  if (media.kind === "image") return {
2335
2393
  method: "sendPhoto",
@@ -2759,25 +2817,24 @@ var TelegramChannel = class {
2759
2817
  async sendRichMessage(target, text, options) {
2760
2818
  const parsedTarget = parseTargetChatId(target.chatId);
2761
2819
  for (const chunk of chunkText(text, TELEGRAM_CHUNK_LIMIT)) {
2820
+ const rendered = prepareTelegramTextCandidates(chunk, this.textMode);
2762
2821
  const payload = {
2763
2822
  chat_id: parsedTarget.chatId,
2764
- text: chunk,
2823
+ text: rendered.html,
2765
2824
  message_thread_id: parsedTarget.messageThreadId,
2766
2825
  disable_web_page_preview: true,
2767
- reply_markup: options?.inlineKeyboard ? { inline_keyboard: options.inlineKeyboard } : void 0
2826
+ reply_markup: options?.inlineKeyboard ? { inline_keyboard: options.inlineKeyboard } : void 0,
2827
+ parse_mode: "HTML"
2768
2828
  };
2769
- if (this.textMode !== "markdown") {
2770
- await this.callApi("sendMessage", payload);
2771
- continue;
2772
- }
2773
2829
  try {
2830
+ await this.callApi("sendMessage", payload);
2831
+ } catch (error) {
2832
+ if (!isTelegramParseModeError(error)) throw error;
2774
2833
  await this.callApi("sendMessage", {
2775
2834
  ...payload,
2776
- parse_mode: "Markdown"
2835
+ text: rendered.plain,
2836
+ parse_mode: void 0
2777
2837
  });
2778
- } catch (error) {
2779
- if (!isTelegramParseModeError(error)) throw error;
2780
- await this.callApi("sendMessage", payload);
2781
2838
  }
2782
2839
  }
2783
2840
  }
@@ -2812,18 +2869,24 @@ var TelegramChannel = class {
2812
2869
  caption: media.caption?.trim() || void 0,
2813
2870
  message_thread_id: parsedTarget.messageThreadId
2814
2871
  };
2815
- if (this.textMode !== "markdown" || !payload.caption) {
2872
+ if (!payload.caption) {
2816
2873
  await this.callApi(method, payload);
2817
2874
  return;
2818
2875
  }
2876
+ const renderedCaption = prepareTelegramTextCandidates(payload.caption, this.textMode);
2819
2877
  try {
2820
2878
  await this.callApi(method, {
2821
2879
  ...payload,
2822
- parse_mode: "Markdown"
2880
+ caption: renderedCaption.html,
2881
+ parse_mode: "HTML"
2823
2882
  });
2824
2883
  } catch (error) {
2825
2884
  if (!isTelegramParseModeError(error)) throw error;
2826
- await this.callApi(method, payload);
2885
+ await this.callApi(method, {
2886
+ ...payload,
2887
+ caption: renderedCaption.plain,
2888
+ parse_mode: void 0
2889
+ });
2827
2890
  }
2828
2891
  }
2829
2892
  async setReaction(target, reaction) {
@@ -5578,6 +5641,39 @@ const channelsLogoutMethod = async (params, context) => {
5578
5641
  return { ok: true };
5579
5642
  };
5580
5643
 
5644
+ //#endregion
5645
+ //#region src/inbound-context.ts
5646
+ const SENDER_DISPLAY_NAME_MAX_CHARS = 80;
5647
+ const SENDER_TOKEN_MAX_CHARS = 80;
5648
+ const SENDER_CONTEXT_PREFIX = "[Sender Context]";
5649
+ function normalizeInlineToken(value, maxChars, fallback) {
5650
+ const resolved = (value ?? "").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim() || fallback;
5651
+ if (resolved.length <= maxChars) return resolved;
5652
+ return `${resolved.slice(0, maxChars - 3)}...`;
5653
+ }
5654
+ function sanitizeSenderDisplayName(displayName, fallbackUserId) {
5655
+ return normalizeInlineToken(displayName, SENDER_DISPLAY_NAME_MAX_CHARS, fallbackUserId);
5656
+ }
5657
+ function buildSenderContextEnvelope(sender) {
5658
+ const userId = normalizeInlineToken(sender.userId, SENDER_TOKEN_MAX_CHARS, "unknown");
5659
+ const displayName = sanitizeSenderDisplayName(sender.displayName, userId);
5660
+ const accountId = sender.accountId ? normalizeInlineToken(sender.accountId, SENDER_TOKEN_MAX_CHARS, "") : "";
5661
+ return [
5662
+ SENDER_CONTEXT_PREFIX,
5663
+ `displayName=${displayName}`,
5664
+ `userId=${userId}`,
5665
+ `channel=${sender.channel}`,
5666
+ ...accountId ? [`accountId=${accountId}`] : [],
5667
+ ...sender.peerKind ? [`peerKind=${sender.peerKind}`] : [],
5668
+ ""
5669
+ ].join("\n");
5670
+ }
5671
+ function prependSenderContext(prompt, sender) {
5672
+ if (!sender) return prompt;
5673
+ if (prompt.startsWith(`${SENDER_CONTEXT_PREFIX}\n`)) return prompt;
5674
+ return `${buildSenderContextEnvelope(sender)}${prompt}`;
5675
+ }
5676
+
5581
5677
  //#endregion
5582
5678
  //#region src/gateway/methods/chat.ts
5583
5679
  const chatHistoryParamsSchema = z.object({ sessionKey: z.string().min(1) });
@@ -5596,15 +5692,23 @@ const chatHistoryMethod = async (params, context) => {
5596
5692
  };
5597
5693
  const chatSendMethod = async (params, context, connId) => {
5598
5694
  const parsed = chatSendParamsSchema.parse(params);
5695
+ const client = context.getConnectionClient?.(connId) ?? null;
5696
+ const sender = client ? {
5697
+ displayName: client.displayName ?? client.id,
5698
+ userId: client.id,
5699
+ channel: "cli"
5700
+ } : void 0;
5701
+ const prompt = prependSenderContext(parsed.prompt, sender);
5599
5702
  let finalText = null;
5600
5703
  let finalError = null;
5601
5704
  for await (const event of context.agentManager.run({
5602
5705
  sessionKey: parsed.sessionKey,
5603
- prompt: parsed.prompt,
5706
+ prompt,
5604
5707
  modelOverride: parsed.model,
5605
5708
  meta: {
5606
5709
  source: "interactive",
5607
- channel: "cli"
5710
+ channel: "cli",
5711
+ ...sender ? { sender } : {}
5608
5712
  }
5609
5713
  })) {
5610
5714
  const maybeText = extractAssistantText(event);
@@ -6271,6 +6375,24 @@ var HovClawGatewayServer = class {
6271
6375
  this.httpServer = createServer((req, res) => {
6272
6376
  this.handleHttpRequest(req, res);
6273
6377
  });
6378
+ this.httpServer.on("error", (error) => {
6379
+ logger.error({
6380
+ error,
6381
+ host: this.runtimeConfig.gateway.host,
6382
+ port: this.runtimeConfig.gateway.port
6383
+ }, "Gateway server error; continuing without gateway listener");
6384
+ if (this.tickTimer) {
6385
+ clearInterval(this.tickTimer);
6386
+ this.tickTimer = null;
6387
+ }
6388
+ for (const state of this.connections.values()) state.socket.close(1011, "gateway-error");
6389
+ this.connections.clear();
6390
+ if (this.wss) {
6391
+ this.wss.close();
6392
+ this.wss = null;
6393
+ }
6394
+ this.httpServer = null;
6395
+ });
6274
6396
  this.httpServer.on("upgrade", (request, socket, head) => {
6275
6397
  if (!this.wss) {
6276
6398
  socket.destroy();
@@ -6433,6 +6555,10 @@ var HovClawGatewayServer = class {
6433
6555
  return;
6434
6556
  }
6435
6557
  state.connected = true;
6558
+ state.client = {
6559
+ id: connectParams.client.id,
6560
+ ...connectParams.client.displayName ? { displayName: connectParams.client.displayName } : {}
6561
+ };
6436
6562
  sendResponse(state.socket, request.id, true, {
6437
6563
  type: "hello-ok",
6438
6564
  protocol: PROTOCOL_VERSION,
@@ -6512,6 +6638,11 @@ var HovClawGatewayServer = class {
6512
6638
  readFileConfig: this.readFileConfig,
6513
6639
  writeFileConfig: this.writeFileConfig,
6514
6640
  getChannelStatus: this.getChannelStatus,
6641
+ getConnectionClient: (connId) => {
6642
+ const connection = this.connections.get(connId);
6643
+ if (!connection?.client) return null;
6644
+ return connection.client;
6645
+ },
6515
6646
  emitEvent: (event, payload, connId) => {
6516
6647
  this.emitEvent(event, payload, connId);
6517
6648
  }
@@ -8107,16 +8238,25 @@ async function main() {
8107
8238
  }
8108
8239
  }
8109
8240
  normalizedPrompt = applyThinkingLevel(normalizedPrompt, thinkingLevel, thinkingLevelForced);
8241
+ const sender = {
8242
+ displayName: msg.displayName,
8243
+ userId: msg.userId,
8244
+ channel: msg.channel,
8245
+ ...msg.accountId ? { accountId: msg.accountId } : {},
8246
+ ...msg.peer?.kind ? { peerKind: msg.peer.kind } : {}
8247
+ };
8248
+ const promptWithSender = prependSenderContext(normalizedPrompt, sender);
8110
8249
  try {
8111
8250
  await channel.setTyping?.(target, true);
8112
8251
  let finalText = null;
8113
8252
  let finalError = null;
8114
8253
  for await (const event of agentManager.run({
8115
8254
  sessionKey,
8116
- prompt: normalizedPrompt,
8255
+ prompt: promptWithSender,
8117
8256
  meta: {
8118
8257
  source: "interactive",
8119
- channel: msg.channel
8258
+ channel: msg.channel,
8259
+ sender
8120
8260
  }
8121
8261
  })) {
8122
8262
  const maybe = extractAssistantText(event);
@@ -1553,6 +1553,39 @@ const channelsLogoutMethod = async (params, context) => {
1553
1553
  return { ok: true };
1554
1554
  };
1555
1555
 
1556
+ //#endregion
1557
+ //#region src/inbound-context.ts
1558
+ const SENDER_DISPLAY_NAME_MAX_CHARS = 80;
1559
+ const SENDER_TOKEN_MAX_CHARS = 80;
1560
+ const SENDER_CONTEXT_PREFIX = "[Sender Context]";
1561
+ function normalizeInlineToken(value, maxChars, fallback) {
1562
+ const resolved = (value ?? "").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim() || fallback;
1563
+ if (resolved.length <= maxChars) return resolved;
1564
+ return `${resolved.slice(0, maxChars - 3)}...`;
1565
+ }
1566
+ function sanitizeSenderDisplayName(displayName, fallbackUserId) {
1567
+ return normalizeInlineToken(displayName, SENDER_DISPLAY_NAME_MAX_CHARS, fallbackUserId);
1568
+ }
1569
+ function buildSenderContextEnvelope(sender) {
1570
+ const userId = normalizeInlineToken(sender.userId, SENDER_TOKEN_MAX_CHARS, "unknown");
1571
+ const displayName = sanitizeSenderDisplayName(sender.displayName, userId);
1572
+ const accountId = sender.accountId ? normalizeInlineToken(sender.accountId, SENDER_TOKEN_MAX_CHARS, "") : "";
1573
+ return [
1574
+ SENDER_CONTEXT_PREFIX,
1575
+ `displayName=${displayName}`,
1576
+ `userId=${userId}`,
1577
+ `channel=${sender.channel}`,
1578
+ ...accountId ? [`accountId=${accountId}`] : [],
1579
+ ...sender.peerKind ? [`peerKind=${sender.peerKind}`] : [],
1580
+ ""
1581
+ ].join("\n");
1582
+ }
1583
+ function prependSenderContext(prompt, sender) {
1584
+ if (!sender) return prompt;
1585
+ if (prompt.startsWith(`${SENDER_CONTEXT_PREFIX}\n`)) return prompt;
1586
+ return `${buildSenderContextEnvelope(sender)}${prompt}`;
1587
+ }
1588
+
1556
1589
  //#endregion
1557
1590
  //#region src/gateway/methods/chat.ts
1558
1591
  const chatHistoryParamsSchema = z.object({ sessionKey: z.string().min(1) });
@@ -1571,15 +1604,23 @@ const chatHistoryMethod = async (params, context) => {
1571
1604
  };
1572
1605
  const chatSendMethod = async (params, context, connId) => {
1573
1606
  const parsed = chatSendParamsSchema.parse(params);
1607
+ const client = context.getConnectionClient?.(connId) ?? null;
1608
+ const sender = client ? {
1609
+ displayName: client.displayName ?? client.id,
1610
+ userId: client.id,
1611
+ channel: "cli"
1612
+ } : void 0;
1613
+ const prompt = prependSenderContext(parsed.prompt, sender);
1574
1614
  let finalText = null;
1575
1615
  let finalError = null;
1576
1616
  for await (const event of context.agentManager.run({
1577
1617
  sessionKey: parsed.sessionKey,
1578
- prompt: parsed.prompt,
1618
+ prompt,
1579
1619
  modelOverride: parsed.model,
1580
1620
  meta: {
1581
1621
  source: "interactive",
1582
- channel: "cli"
1622
+ channel: "cli",
1623
+ ...sender ? { sender } : {}
1583
1624
  }
1584
1625
  })) {
1585
1626
  const maybeText = extractAssistantText(event);
@@ -2164,6 +2205,24 @@ var HovClawGatewayServer = class {
2164
2205
  this.httpServer = createServer((req, res) => {
2165
2206
  this.handleHttpRequest(req, res);
2166
2207
  });
2208
+ this.httpServer.on("error", (error) => {
2209
+ logger.error({
2210
+ error,
2211
+ host: this.runtimeConfig.gateway.host,
2212
+ port: this.runtimeConfig.gateway.port
2213
+ }, "Gateway server error; continuing without gateway listener");
2214
+ if (this.tickTimer) {
2215
+ clearInterval(this.tickTimer);
2216
+ this.tickTimer = null;
2217
+ }
2218
+ for (const state of this.connections.values()) state.socket.close(1011, "gateway-error");
2219
+ this.connections.clear();
2220
+ if (this.wss) {
2221
+ this.wss.close();
2222
+ this.wss = null;
2223
+ }
2224
+ this.httpServer = null;
2225
+ });
2167
2226
  this.httpServer.on("upgrade", (request, socket, head) => {
2168
2227
  if (!this.wss) {
2169
2228
  socket.destroy();
@@ -2326,6 +2385,10 @@ var HovClawGatewayServer = class {
2326
2385
  return;
2327
2386
  }
2328
2387
  state.connected = true;
2388
+ state.client = {
2389
+ id: connectParams.client.id,
2390
+ ...connectParams.client.displayName ? { displayName: connectParams.client.displayName } : {}
2391
+ };
2329
2392
  sendResponse(state.socket, request.id, true, {
2330
2393
  type: "hello-ok",
2331
2394
  protocol: PROTOCOL_VERSION,
@@ -2405,6 +2468,11 @@ var HovClawGatewayServer = class {
2405
2468
  readFileConfig: this.readFileConfig,
2406
2469
  writeFileConfig: this.writeFileConfig,
2407
2470
  getChannelStatus: this.getChannelStatus,
2471
+ getConnectionClient: (connId) => {
2472
+ const connection = this.connections.get(connId);
2473
+ if (!connection?.client) return null;
2474
+ return connection.client;
2475
+ },
2408
2476
  emitEvent: (event, payload, connId) => {
2409
2477
  this.emitEvent(event, payload, connId);
2410
2478
  }
@@ -2968,16 +3036,25 @@ async function main() {
2968
3036
  }
2969
3037
  }
2970
3038
  normalizedPrompt = applyThinkingLevel(normalizedPrompt, thinkingLevel, thinkingLevelForced);
3039
+ const sender = {
3040
+ displayName: msg.displayName,
3041
+ userId: msg.userId,
3042
+ channel: msg.channel,
3043
+ ...msg.accountId ? { accountId: msg.accountId } : {},
3044
+ ...msg.peer?.kind ? { peerKind: msg.peer.kind } : {}
3045
+ };
3046
+ const promptWithSender = prependSenderContext(normalizedPrompt, sender);
2971
3047
  try {
2972
3048
  await channel.setTyping?.(target, true);
2973
3049
  let finalText = null;
2974
3050
  let finalError = null;
2975
3051
  for await (const event of agentManager.run({
2976
3052
  sessionKey,
2977
- prompt: normalizedPrompt,
3053
+ prompt: promptWithSender,
2978
3054
  meta: {
2979
3055
  source: "interactive",
2980
- channel: msg.channel
3056
+ channel: msg.channel,
3057
+ sender
2981
3058
  }
2982
3059
  })) {
2983
3060
  const maybe = extractAssistantText(event);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hovclaw",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Multi-channel AI agent gateway",
5
5
  "license": "MIT",
6
6
  "type": "module",