@rubytech/create-maxy-code 0.1.266 → 0.1.268

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 (23) hide show
  1. package/dist/index.js +16 -0
  2. package/package.json +1 -1
  3. package/payload/platform/package-lock.json +16 -0
  4. package/payload/platform/package.json +3 -2
  5. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  6. package/payload/platform/services/claude-session-manager/dist/http-server.js +36 -1
  7. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  8. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts +19 -0
  9. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts.map +1 -0
  10. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js +31 -0
  11. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js.map +1 -0
  12. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +40 -0
  13. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -0
  14. package/payload/platform/services/whatsapp-channel/dist/notification.js +36 -0
  15. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -0
  16. package/payload/platform/services/whatsapp-channel/dist/server.d.ts +3 -0
  17. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -0
  18. package/payload/platform/services/whatsapp-channel/dist/server.js +114 -0
  19. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -0
  20. package/payload/platform/services/whatsapp-channel/package.json +20 -0
  21. package/payload/server/{chunk-SOLVVUST.js → chunk-W4EM7RK4.js} +2 -0
  22. package/payload/server/maxy-edge.js +1 -1
  23. package/payload/server/server.js +345 -14
@@ -0,0 +1,36 @@
1
+ // Pure helpers for the WhatsApp channel server. No MCP SDK import here, so
2
+ // these can be unit-tested without the SDK installed and so the server's
3
+ // transport wiring (server.ts) stays separable from its message shaping.
4
+ /**
5
+ * Shape an inbound WhatsApp payload as a Claude Code channel notification.
6
+ * `content` becomes the `<channel>` body; each `meta` entry becomes a tag
7
+ * attribute, so keys must be identifier-safe ([A-Za-z0-9_]) — hyphenated
8
+ * keys are silently dropped by Claude Code.
9
+ */
10
+ export function buildInboundNotification(p) {
11
+ return {
12
+ method: 'notifications/claude/channel',
13
+ params: {
14
+ content: p.text,
15
+ meta: { sender_id: p.senderId, wa_message_id: p.waMessageId },
16
+ },
17
+ };
18
+ }
19
+ /**
20
+ * Defence in depth: this server serves exactly one sender. Even though the
21
+ * gateway only streams that sender's messages, refuse anything else before
22
+ * it reaches the session.
23
+ */
24
+ export function acceptsSender(mySenderId, p) {
25
+ return p.senderId === mySenderId;
26
+ }
27
+ /**
28
+ * Build the body for POST /wa-channel/reply. The reply tool exposes only
29
+ * `text` to the model; the senderId is the server's fixed sender (from spawn
30
+ * env), never trusted from the model, so a compromised turn cannot redirect a
31
+ * reply to a different WhatsApp recipient.
32
+ */
33
+ export function buildReplyPostBody(mySenderId, text) {
34
+ return { senderId: mySenderId, text };
35
+ }
36
+ //# sourceMappingURL=notification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification.js","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,yEAAyE;AACzE,yEAAyE;AAWzE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,CAAiB;IACxD,OAAO;QACL,MAAM,EAAE,8BAAuC;QAC/C,MAAM,EAAE;YACN,OAAO,EAAE,CAAC,CAAC,IAAI;YACf,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC,WAAW,EAA4B;SACxF;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,CAAuB;IACvE,OAAO,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAA;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAkB,EAAE,IAAY;IACjE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;AACvC,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // Per-sender WhatsApp channel MCP server. Spawned as a stdio child of one
3
+ // `claude` session (so it dies when that session dies — no bound port, no
4
+ // orphan). It:
5
+ // - subscribes to the gateway for ITS sender's inbound (SSE) and emits each
6
+ // message as a notifications/claude/channel event;
7
+ // - exposes a `reply` tool that posts the agent's reply back to the gateway,
8
+ // which sends it to WhatsApp.
9
+ //
10
+ // Transport glue only — message shaping and the reply body are the unit-tested
11
+ // pure helpers in ./notification. Verified end-to-end at deploy (Task 7).
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
+ import { acceptsSender, buildInboundNotification, buildReplyPostBody } from './notification.js';
16
+ function requireEnv(name) {
17
+ const v = process.env[name];
18
+ if (!v)
19
+ throw new Error(`whatsapp-channel: missing required env ${name}`);
20
+ return v;
21
+ }
22
+ const SENDER_ID = requireEnv('WA_CHANNEL_SENDER_ID');
23
+ const GATEWAY_URL = requireEnv('WA_CHANNEL_GATEWAY_URL').replace(/\/$/, '');
24
+ // Co-visibility: the operator watches the same session in claude.ai/code,
25
+ // where the reply tool call shows but its text does not. Instruct the agent to
26
+ // also write its answer as normal assistant text so the operator can read it
27
+ // in the web view, AND relay that same text via the reply tool to WhatsApp.
28
+ const INSTRUCTIONS = `Messages from the WhatsApp sender arrive as <channel source="whatsapp" sender_id="...">. ` +
29
+ `Always do two things with your response, in this order: (1) write your answer as a normal ` +
30
+ `assistant message so it is visible in the session, then (2) call the reply tool with that ` +
31
+ `same text to send it to WhatsApp. Do not skip either step.`;
32
+ const mcp = new Server({ name: 'whatsapp', version: '0.1.0' }, {
33
+ capabilities: { experimental: { 'claude/channel': {} }, tools: {} },
34
+ instructions: INSTRUCTIONS,
35
+ });
36
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
37
+ tools: [
38
+ {
39
+ name: 'reply',
40
+ description: 'Send your reply text to the WhatsApp sender of this conversation.',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: { text: { type: 'string', description: 'The message to send to WhatsApp' } },
44
+ required: ['text'],
45
+ },
46
+ },
47
+ ],
48
+ }));
49
+ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
50
+ if (req.params.name !== 'reply')
51
+ throw new Error(`unknown tool: ${req.params.name}`);
52
+ const text = req.params.arguments?.text;
53
+ if (typeof text !== 'string')
54
+ throw new Error('reply: text must be a string');
55
+ const res = await fetch(`${GATEWAY_URL}/wa-channel/reply`, {
56
+ method: 'POST',
57
+ headers: { 'content-type': 'application/json' },
58
+ body: JSON.stringify(buildReplyPostBody(SENDER_ID, text)),
59
+ });
60
+ if (!res.ok)
61
+ throw new Error(`reply: gateway returned ${res.status}`);
62
+ return { content: [{ type: 'text', text: 'sent' }] };
63
+ });
64
+ // Subscribe to this sender's inbound stream and emit each message as an event.
65
+ async function subscribeInbound() {
66
+ const url = `${GATEWAY_URL}/wa-channel/inbound?senderId=${encodeURIComponent(SENDER_ID)}`;
67
+ const res = await fetch(url, { headers: { accept: 'text/event-stream' } });
68
+ if (!res.ok || !res.body)
69
+ throw new Error(`inbound subscribe failed: ${res.status}`);
70
+ const reader = res.body.getReader();
71
+ const decoder = new TextDecoder();
72
+ let buffer = '';
73
+ for (;;) {
74
+ const { value, done } = await reader.read();
75
+ if (done)
76
+ break;
77
+ buffer += decoder.decode(value, { stream: true });
78
+ const frames = buffer.split('\n\n');
79
+ buffer = frames.pop() ?? '';
80
+ for (const frame of frames) {
81
+ const dataLine = frame.split('\n').find((l) => l.startsWith('data:'));
82
+ if (!dataLine)
83
+ continue;
84
+ const data = dataLine.slice('data:'.length).trim();
85
+ if (!data)
86
+ continue; // ping
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(data);
90
+ }
91
+ catch {
92
+ continue;
93
+ }
94
+ if (!acceptsSender(SENDER_ID, payload))
95
+ continue;
96
+ await mcp.notification(buildInboundNotification(payload));
97
+ }
98
+ }
99
+ }
100
+ async function main() {
101
+ await mcp.connect(new StdioServerTransport());
102
+ // Tell the gateway this sender's channel is live so any cold-start queue drains.
103
+ await fetch(`${GATEWAY_URL}/wa-channel/ready`, {
104
+ method: 'POST',
105
+ headers: { 'content-type': 'application/json' },
106
+ body: JSON.stringify({ senderId: SENDER_ID }),
107
+ }).catch(() => { });
108
+ await subscribeInbound();
109
+ }
110
+ main().catch((err) => {
111
+ console.error(`[whatsapp-channel] fatal: ${err instanceof Error ? err.message : String(err)}`);
112
+ process.exit(1);
113
+ });
114
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@maxy/whatsapp-channel",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "test": "vitest run"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.7.0",
17
+ "@types/node": "^22.0.0",
18
+ "vitest": "^4.1.2"
19
+ }
20
+ }
@@ -5590,6 +5590,8 @@ var clientIpMiddleware = async (c, next) => {
5590
5590
  };
5591
5591
 
5592
5592
  export {
5593
+ HtmlEscapedCallbackPhase,
5594
+ resolveCallback,
5593
5595
  Hono2 as Hono,
5594
5596
  getRequestListener,
5595
5597
  serve,
@@ -14,7 +14,7 @@ import {
14
14
  sanitizeClientCorrId,
15
15
  vncLog,
16
16
  websockifyLog
17
- } from "./chunk-SOLVVUST.js";
17
+ } from "./chunk-W4EM7RK4.js";
18
18
  import "./chunk-PFF6I7KP.js";
19
19
 
20
20
  // server/edge.ts
@@ -9,6 +9,7 @@ import {
9
9
  COMMERCIAL_MODE,
10
10
  GREETING_DIRECTIVE,
11
11
  Hono,
12
+ HtmlEscapedCallbackPhase,
12
13
  INSTALL_OWNER_FILE,
13
14
  LOG_DIR,
14
15
  MAXY_DIR,
@@ -71,6 +72,7 @@ import {
71
72
  requirePortEnv,
72
73
  resolveAccount,
73
74
  resolveAgentConfig,
75
+ resolveCallback,
74
76
  resolveClientIp,
75
77
  resolveDefaultAgentSlug,
76
78
  resolveUserAccounts,
@@ -93,7 +95,7 @@ import {
93
95
  vncLog,
94
96
  walkPremiumBundles,
95
97
  writeAdminUserAndPerson
96
- } from "./chunk-SOLVVUST.js";
98
+ } from "./chunk-W4EM7RK4.js";
97
99
  import {
98
100
  __commonJS,
99
101
  __toESM
@@ -1105,24 +1107,24 @@ var pr54206Applied = () => {
1105
1107
  return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18;
1106
1108
  };
1107
1109
  var useReadableToWeb = pr54206Applied();
1108
- var createStreamBody = (stream) => {
1110
+ var createStreamBody = (stream2) => {
1109
1111
  if (useReadableToWeb) {
1110
- return Readable.toWeb(stream);
1112
+ return Readable.toWeb(stream2);
1111
1113
  }
1112
1114
  const body = new ReadableStream({
1113
1115
  start(controller) {
1114
- stream.on("data", (chunk) => {
1116
+ stream2.on("data", (chunk) => {
1115
1117
  controller.enqueue(chunk);
1116
1118
  });
1117
- stream.on("error", (err) => {
1119
+ stream2.on("error", (err) => {
1118
1120
  controller.error(err);
1119
1121
  });
1120
- stream.on("end", () => {
1122
+ stream2.on("end", () => {
1121
1123
  controller.close();
1122
1124
  });
1123
1125
  },
1124
1126
  cancel() {
1125
- stream.destroy();
1127
+ stream2.destroy();
1126
1128
  }
1127
1129
  });
1128
1130
  return body;
@@ -1227,10 +1229,10 @@ var serveStatic = (options = { root: "" }) => {
1227
1229
  end = size - 1;
1228
1230
  }
1229
1231
  const chunksize = end - start + 1;
1230
- const stream = createReadStream(path2, { start, end });
1232
+ const stream2 = createReadStream(path2, { start, end });
1231
1233
  c.header("Content-Length", chunksize.toString());
1232
1234
  c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
1233
- result = c.body(createStreamBody(stream), 206);
1235
+ result = c.body(createStreamBody(stream2), 206);
1234
1236
  }
1235
1237
  await options.onFound?.(path2, c);
1236
1238
  return result;
@@ -2892,9 +2894,9 @@ function getDownloadableContent(message) {
2892
2894
  if (msg.stickerMessage) return { downloadable: msg.stickerMessage, mediaType: "sticker" };
2893
2895
  return void 0;
2894
2896
  }
2895
- async function streamToBuffer(stream) {
2897
+ async function streamToBuffer(stream2) {
2896
2898
  const chunks = [];
2897
- for await (const chunk of stream) {
2899
+ for await (const chunk of stream2) {
2898
2900
  chunks.push(Buffer.from(chunk));
2899
2901
  }
2900
2902
  return Buffer.concat(chunks);
@@ -2925,8 +2927,8 @@ async function downloadInboundMedia(msg, sock, opts) {
2925
2927
  const downloadable = getDownloadableContent(content);
2926
2928
  if (downloadable) {
2927
2929
  try {
2928
- const stream = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
2929
- buffer = await streamToBuffer(stream);
2930
+ const stream2 = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
2931
+ buffer = await streamToBuffer(stream2);
2930
2932
  } catch (fallbackErr) {
2931
2933
  console.error(`${TAG8} direct download fallback failed: ${String(fallbackErr)}`);
2932
2934
  }
@@ -4671,7 +4673,8 @@ async function managerRcSpawn(opts) {
4671
4673
  initialMessage: opts.initialMessage,
4672
4674
  closeAfterTurn: opts.closeAfterTurn,
4673
4675
  sliceToken: opts.sliceToken,
4674
- personId: opts.personId
4676
+ personId: opts.personId,
4677
+ waChannel: opts.waChannel
4675
4678
  })
4676
4679
  }).catch((err) => ({ __throw: err instanceof Error ? err.message : String(err) }));
4677
4680
  if ("__throw" in res) {
@@ -15567,6 +15570,300 @@ function startReaper2() {
15567
15570
  startReaper();
15568
15571
  }
15569
15572
 
15573
+ // app/lib/whatsapp/gateway/inbound-hub.ts
15574
+ var InboundHub = class {
15575
+ senders = /* @__PURE__ */ new Map();
15576
+ state(senderId) {
15577
+ let s = this.senders.get(senderId);
15578
+ if (!s) {
15579
+ s = { subscriber: null, queue: [] };
15580
+ this.senders.set(senderId, s);
15581
+ }
15582
+ return s;
15583
+ }
15584
+ /** Route an inbound message to its sender. Delivers immediately if that
15585
+ * sender's channel server is attached; otherwise queues it (cold start). */
15586
+ deliver(payload) {
15587
+ const s = this.state(payload.senderId);
15588
+ if (s.subscriber) {
15589
+ s.subscriber(payload);
15590
+ } else {
15591
+ s.queue.push(payload);
15592
+ }
15593
+ }
15594
+ /** A sender's channel server connected. Drain any queued messages to it in
15595
+ * arrival order, then route live. Replaces any prior subscriber. */
15596
+ attach(senderId, subscriber) {
15597
+ const s = this.state(senderId);
15598
+ s.subscriber = subscriber;
15599
+ if (s.queue.length > 0) {
15600
+ const pending = s.queue;
15601
+ s.queue = [];
15602
+ for (const payload of pending) subscriber(payload);
15603
+ }
15604
+ }
15605
+ /** A sender's channel server disconnected (session ended/restarting).
15606
+ * Subsequent messages queue again until re-attach, so nothing is lost
15607
+ * across a restart. */
15608
+ detach(senderId) {
15609
+ const s = this.senders.get(senderId);
15610
+ if (s) s.subscriber = null;
15611
+ }
15612
+ /** Whether a sender currently has a live channel server. The gateway uses
15613
+ * this to decide whether an inbound needs a cold-start spawn/resume. */
15614
+ hasSubscriber(senderId) {
15615
+ return this.senders.get(senderId)?.subscriber != null;
15616
+ }
15617
+ };
15618
+
15619
+ // node_modules/hono/dist/utils/stream.js
15620
+ var StreamingApi = class {
15621
+ writer;
15622
+ encoder;
15623
+ writable;
15624
+ abortSubscribers = [];
15625
+ responseReadable;
15626
+ /**
15627
+ * Whether the stream has been aborted.
15628
+ */
15629
+ aborted = false;
15630
+ /**
15631
+ * Whether the stream has been closed normally.
15632
+ */
15633
+ closed = false;
15634
+ constructor(writable, _readable) {
15635
+ this.writable = writable;
15636
+ this.writer = writable.getWriter();
15637
+ this.encoder = new TextEncoder();
15638
+ const reader = _readable.getReader();
15639
+ this.abortSubscribers.push(async () => {
15640
+ await reader.cancel();
15641
+ });
15642
+ this.responseReadable = new ReadableStream({
15643
+ async pull(controller) {
15644
+ const { done, value } = await reader.read();
15645
+ done ? controller.close() : controller.enqueue(value);
15646
+ },
15647
+ cancel: () => {
15648
+ this.abort();
15649
+ }
15650
+ });
15651
+ }
15652
+ async write(input) {
15653
+ try {
15654
+ if (typeof input === "string") {
15655
+ input = this.encoder.encode(input);
15656
+ }
15657
+ await this.writer.write(input);
15658
+ } catch {
15659
+ }
15660
+ return this;
15661
+ }
15662
+ async writeln(input) {
15663
+ await this.write(input + "\n");
15664
+ return this;
15665
+ }
15666
+ sleep(ms) {
15667
+ return new Promise((res) => setTimeout(res, ms));
15668
+ }
15669
+ async close() {
15670
+ try {
15671
+ await this.writer.close();
15672
+ } catch {
15673
+ }
15674
+ this.closed = true;
15675
+ }
15676
+ async pipe(body) {
15677
+ this.writer.releaseLock();
15678
+ await body.pipeTo(this.writable, { preventClose: true });
15679
+ this.writer = this.writable.getWriter();
15680
+ }
15681
+ onAbort(listener) {
15682
+ this.abortSubscribers.push(listener);
15683
+ }
15684
+ /**
15685
+ * Abort the stream.
15686
+ * You can call this method when stream is aborted by external event.
15687
+ */
15688
+ abort() {
15689
+ if (!this.aborted) {
15690
+ this.aborted = true;
15691
+ this.abortSubscribers.forEach((subscriber) => subscriber());
15692
+ }
15693
+ }
15694
+ };
15695
+
15696
+ // node_modules/hono/dist/helper/streaming/utils.js
15697
+ var isOldBunVersion = () => {
15698
+ const version = typeof Bun !== "undefined" ? Bun.version : void 0;
15699
+ if (version === void 0) {
15700
+ return false;
15701
+ }
15702
+ const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
15703
+ isOldBunVersion = () => result;
15704
+ return result;
15705
+ };
15706
+
15707
+ // node_modules/hono/dist/helper/streaming/sse.js
15708
+ var SSEStreamingApi = class extends StreamingApi {
15709
+ constructor(writable, readable) {
15710
+ super(writable, readable);
15711
+ }
15712
+ async writeSSE(message) {
15713
+ const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
15714
+ const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
15715
+ return `data: ${line}`;
15716
+ }).join("\n");
15717
+ for (const key of ["event", "id", "retry"]) {
15718
+ if (message[key] && /[\r\n]/.test(message[key])) {
15719
+ throw new Error(`${key} must not contain "\\r" or "\\n"`);
15720
+ }
15721
+ }
15722
+ const sseData = [
15723
+ message.event && `event: ${message.event}`,
15724
+ dataLines,
15725
+ message.id && `id: ${message.id}`,
15726
+ message.retry && `retry: ${message.retry}`
15727
+ ].filter(Boolean).join("\n") + "\n\n";
15728
+ await this.write(sseData);
15729
+ }
15730
+ };
15731
+ var run = async (stream2, cb, onError) => {
15732
+ try {
15733
+ await cb(stream2);
15734
+ } catch (e) {
15735
+ if (e instanceof Error && onError) {
15736
+ await onError(e, stream2);
15737
+ await stream2.writeSSE({
15738
+ event: "error",
15739
+ data: e.message
15740
+ });
15741
+ } else {
15742
+ console.error(e);
15743
+ }
15744
+ } finally {
15745
+ stream2.close();
15746
+ }
15747
+ };
15748
+ var contextStash = /* @__PURE__ */ new WeakMap();
15749
+ var streamSSE = (c, cb, onError) => {
15750
+ const { readable, writable } = new TransformStream();
15751
+ const stream2 = new SSEStreamingApi(writable, readable);
15752
+ if (isOldBunVersion()) {
15753
+ c.req.raw.signal.addEventListener("abort", () => {
15754
+ if (!stream2.closed) {
15755
+ stream2.abort();
15756
+ }
15757
+ });
15758
+ }
15759
+ contextStash.set(stream2.responseReadable, c);
15760
+ c.header("Transfer-Encoding", "chunked");
15761
+ c.header("Content-Type", "text/event-stream");
15762
+ c.header("Cache-Control", "no-cache");
15763
+ c.header("Connection", "keep-alive");
15764
+ run(stream2, cb, onError);
15765
+ return c.newResponse(stream2.responseReadable);
15766
+ };
15767
+
15768
+ // app/lib/whatsapp/gateway/routes.ts
15769
+ function createWaChannelRoutes(deps) {
15770
+ const app45 = new Hono();
15771
+ app45.get("/wa-channel/inbound", (c) => {
15772
+ const senderId = c.req.query("senderId");
15773
+ if (!senderId) return c.json({ error: "senderId required" }, 400);
15774
+ return streamSSE(c, async (stream2) => {
15775
+ deps.hub.attach(senderId, (payload) => {
15776
+ void stream2.writeSSE({ data: JSON.stringify(payload) });
15777
+ });
15778
+ console.error(`[whatsapp-native] op=channel-attached senderId=${senderId}`);
15779
+ stream2.onAbort(() => {
15780
+ deps.hub.detach(senderId);
15781
+ console.error(`[whatsapp-native] op=channel-detached senderId=${senderId}`);
15782
+ });
15783
+ while (!stream2.aborted) {
15784
+ await stream2.sleep(15e3);
15785
+ await stream2.writeSSE({ data: "", event: "ping" });
15786
+ }
15787
+ });
15788
+ });
15789
+ app45.post("/wa-channel/reply", async (c) => {
15790
+ const body = await c.req.json().catch(() => null);
15791
+ const senderId = body?.senderId;
15792
+ const text = body?.text;
15793
+ if (typeof senderId !== "string" || typeof text !== "string") {
15794
+ return c.json({ error: "senderId and text required" }, 400);
15795
+ }
15796
+ const bytes = Buffer.byteLength(text, "utf8");
15797
+ try {
15798
+ await deps.sendOutbound(senderId, text);
15799
+ } catch (err) {
15800
+ console.error(
15801
+ `[whatsapp-native] op=reply-failed senderId=${senderId} bytes=${bytes} error=${err instanceof Error ? err.message : String(err)}`
15802
+ );
15803
+ return c.json({ error: "send-failed" }, 500);
15804
+ }
15805
+ console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} bytes=${bytes}`);
15806
+ return c.json({ ok: true });
15807
+ });
15808
+ app45.post("/wa-channel/ready", async (c) => {
15809
+ const body = await c.req.json().catch(() => null);
15810
+ const senderId = body?.senderId;
15811
+ if (typeof senderId !== "string") {
15812
+ return c.json({ error: "senderId required" }, 400);
15813
+ }
15814
+ deps.onReady?.(senderId);
15815
+ return c.json({ ok: true });
15816
+ });
15817
+ return app45;
15818
+ }
15819
+
15820
+ // app/lib/whatsapp/gateway/wa-gateway.ts
15821
+ var WaGateway = class {
15822
+ constructor(deps) {
15823
+ this.deps = deps;
15824
+ }
15825
+ hub = new InboundHub();
15826
+ replies = /* @__PURE__ */ new Map();
15827
+ spawning = /* @__PURE__ */ new Set();
15828
+ seq = 0;
15829
+ /** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
15830
+ routes() {
15831
+ return createWaChannelRoutes({
15832
+ hub: this.hub,
15833
+ sendOutbound: (senderId, text) => this.sendOutbound(senderId, text)
15834
+ });
15835
+ }
15836
+ /** Handle one inbound admin WhatsApp message. */
15837
+ async handleInbound(input) {
15838
+ const bytes = Buffer.byteLength(input.text, "utf8");
15839
+ const hadSubscriber = this.hub.hasSubscriber(input.senderId);
15840
+ this.replies.set(input.senderId, input.reply);
15841
+ this.hub.deliver({ senderId: input.senderId, text: input.text, waMessageId: `wa-${++this.seq}` });
15842
+ console.error(
15843
+ `[whatsapp-native] op=inbound senderId=${input.senderId} accountId=${input.accountId} bytes=${bytes} delivery=${hadSubscriber ? "immediate" : "queued"}`
15844
+ );
15845
+ if (!hadSubscriber && !this.spawning.has(input.senderId)) {
15846
+ this.spawning.add(input.senderId);
15847
+ console.error(`[whatsapp-native] op=spawn-trigger senderId=${input.senderId}`);
15848
+ try {
15849
+ await this.deps.ensureChannelSession({
15850
+ accountId: input.accountId,
15851
+ senderId: input.senderId,
15852
+ gatewayUrl: this.deps.gatewayUrl,
15853
+ serverPath: this.deps.serverPath
15854
+ });
15855
+ } finally {
15856
+ this.spawning.delete(input.senderId);
15857
+ }
15858
+ }
15859
+ }
15860
+ async sendOutbound(senderId, text) {
15861
+ const reply = this.replies.get(senderId);
15862
+ if (!reply) throw new Error(`wa-gateway: no reply route for sender ${senderId}`);
15863
+ await reply(text);
15864
+ }
15865
+ };
15866
+
15570
15867
  // app/lib/admin-sse-registry.ts
15571
15868
  var activeAdminSSEControllers = /* @__PURE__ */ new Set();
15572
15869
  function broadcastAdminShutdown(reason) {
@@ -16524,6 +16821,23 @@ app44.all("*", (c) => {
16524
16821
  });
16525
16822
  var port = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "ui-server" });
16526
16823
  var hostname = process.env.HOSTNAME ?? "127.0.0.1";
16824
+ var waNativeGatewayUrl = `http://127.0.0.1:${port}`;
16825
+ var waChannelServerPath = process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve26(process.env.MAXY_PLATFORM_ROOT ?? join18(__dirname, ".."), "services/whatsapp-channel/dist/server.js");
16826
+ var waGateway = new WaGateway({
16827
+ gatewayUrl: waNativeGatewayUrl,
16828
+ serverPath: waChannelServerPath,
16829
+ ensureChannelSession: async ({ accountId, senderId, gatewayUrl, serverPath }) => {
16830
+ const result = await managerRcSpawn({
16831
+ sessionId: adminSessionIdFor(accountId, senderId),
16832
+ waChannel: { senderId, gatewayUrl, serverPath },
16833
+ logContext: `channel=whatsapp-native senderId=${senderId}`
16834
+ });
16835
+ if ("error" in result) {
16836
+ console.error(`[whatsapp-native] spawn-failed senderId=${senderId} error=${result.error} status=${result.status}`);
16837
+ }
16838
+ }
16839
+ });
16840
+ app44.route("/", waGateway.routes());
16527
16841
  var httpServer = serve({ fetch: app44.fetch, port, hostname });
16528
16842
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
16529
16843
  startAuthHealthHeartbeat();
@@ -16702,6 +17016,23 @@ init({
16702
17016
  platformRoot: resolve26(process.env.MAXY_PLATFORM_ROOT ?? join18(__dirname, "..")),
16703
17017
  accountConfig: bootAccountConfig,
16704
17018
  onMessage: async (msg) => {
17019
+ if (process.env.MAXY_WA_NATIVE_CHANNEL === "1" && msg.agentType === "admin" && !msg.isOwnerMirror && msg.text) {
17020
+ try {
17021
+ void msg.composing().catch(() => {
17022
+ });
17023
+ await waGateway.handleInbound({
17024
+ accountId: msg.accountId,
17025
+ senderId: msg.senderPhone,
17026
+ text: msg.text,
17027
+ reply: msg.reply
17028
+ });
17029
+ } catch (err) {
17030
+ console.error(
17031
+ `[whatsapp-native] reject senderId=${msg.senderPhone} message=${err instanceof Error ? err.message : String(err)}`
17032
+ );
17033
+ }
17034
+ return;
17035
+ }
16705
17036
  if ((msg.text || msg.mediaPath) && !msg.isOwnerMirror) {
16706
17037
  try {
16707
17038
  void msg.composing().catch(() => {