@rubytech/create-realagent-code 0.1.265 → 0.1.267

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 (45) hide show
  1. package/dist/index.js +16 -0
  2. package/package.json +1 -1
  3. package/payload/platform/lib/models/dist/index.d.ts +1 -1
  4. package/payload/platform/lib/models/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/models/dist/index.js +5 -2
  6. package/payload/platform/lib/models/dist/index.js.map +1 -1
  7. package/payload/platform/lib/models/src/index.ts +5 -2
  8. package/payload/platform/neo4j/schema.cypher +13 -0
  9. package/payload/platform/package-lock.json +16 -0
  10. package/payload/platform/package.json +3 -2
  11. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js +9 -9
  12. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js.map +1 -1
  13. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +11 -3
  14. package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -2
  15. package/payload/platform/plugins/business-assistant/PLUGIN.md +1 -5
  16. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  17. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +9 -1
  18. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/http-server.js +36 -1
  20. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +10 -0
  22. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  23. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +59 -0
  24. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  25. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts +19 -0
  26. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts.map +1 -0
  27. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js +31 -0
  28. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js.map +1 -0
  29. package/payload/platform/services/whatsapp-channel/package.json +20 -0
  30. package/payload/platform/templates/account.json +1 -1
  31. package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
  32. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  33. package/payload/platform/templates/specialists/agents/research-assistant.md +1 -1
  34. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +1 -1
  35. package/payload/server/{chunk-SOLVVUST.js → chunk-W4EM7RK4.js} +2 -0
  36. package/payload/server/maxy-edge.js +1 -1
  37. package/payload/server/server.js +345 -14
  38. package/payload/platform/plugins/business-assistant/references/quote-engine.md +0 -122
  39. package/payload/platform/plugins/business-assistant/references/quote-generation.md +0 -94
  40. package/payload/platform/plugins/business-assistant/references/quoting.md +0 -85
  41. package/payload/platform/plugins/business-assistant/skills/pricing-method/SKILL.md +0 -78
  42. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/learning-from-history.md +0 -51
  43. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/maintenance.md +0 -32
  44. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/manual-definition.md +0 -42
  45. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/verification.md +0 -37
@@ -5,7 +5,7 @@
5
5
  "publicModel": "claude-haiku-4-5",
6
6
  "outputStyle": "default",
7
7
  "thinkingView": "default",
8
- "effort": "auto",
8
+ "effort": "medium",
9
9
  "enabledPlugins": [],
10
10
  "defaultAgent": "",
11
11
  "admins": []
@@ -2,7 +2,7 @@
2
2
  name: content-producer
3
3
  description: "Author professional business documents (proposal, report, brief, memo, plan, case study, brochure), render PDFs, generate images, and publish static websites. Delegate when the operator asks for a written document, a PDF render, a generated image, or to publish a prepared website."
4
4
  summary: "Produces visual output from your graph: generates images, renders pages to PDF, and hosts static websites you upload as a zip."
5
- model: claude-opus-4-8
5
+ model: claude-opus-4-8[1m]
6
6
  tools: Bash, Read, Edit, Write, MultiEdit, Skill, mcp__plugin_memory_memory__memory-search, mcp__plugin_replicate_replicate__image-generate, mcp__plugin_browser_browser__browser-render, mcp__plugin_browser_browser__browser-navigate, mcp__plugin_browser_browser__browser-evaluate, mcp__plugin_browser_browser__browser-resize, mcp__plugin_browser_browser__browser-screenshot, mcp__plugin_browser_browser__browser-pdf-save, mcp__plugin_admin_admin__plugin-read, mcp__plugin_admin_admin__public-hostname, mcp__plugin_admin_admin__publish-site
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  name: librarian
3
3
  description: "Ingest documents (PDF, text, web pages), conversation transcripts (WhatsApp, Telegram, iMessage, Slack, meeting minutes), and LinkedIn exports into the memory graph. Delegate when the operator hands you a file or transcript that should land in the graph."
4
4
  summary: "Owns foreground ingest of documents, conversation transcripts, and external archives into the memory graph."
5
- model: claude-sonnet-4-5
5
+ model: claude-opus-4-8[1m]
6
6
  tools: mcp__plugin_memory_memory__memory-ingest-extract, mcp__plugin_memory_memory__memory-ingest, mcp__plugin_memory_memory__memory-ingest-web, mcp__plugin_memory_memory__memory-archive-write, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-edge, mcp__plugin_memory_memory__memory-search, mcp__plugin_memory_memory__conversation-archive-list-chunks, mcp__plugin_memory_memory__conversation-archive-derive-insights, mcp__plugin_memory_memory__conversation-archive-enrich-rejection, mcp__plugin_contacts_contacts__contact-create, mcp__plugin_work_work__work-create, mcp__plugin_url-get_url-get__url-get, Read, Bash, Skill, WebFetch
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  name: research-assistant
3
3
  description: "Deep research on the web, knowledge management, and writing structured summaries with citations from multiple sources. Delegate when the operator wants you to research a topic, read web pages, combine findings into a summary, or reorganise stored knowledge."
4
4
  summary: "Researches topics online, manages your knowledge graph, and produces supporting visuals."
5
- model: claude-opus-4-7
5
+ model: claude-opus-4-8[1m]
6
6
  tools: WebSearch, WebFetch, Skill, mcp__plugin_memory_memory__memory-search, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-reindex, mcp__plugin_replicate_replicate__image-generate, mcp__plugin_browser_browser__browser-render, mcp__plugin_url-get_url-get__url-get
7
7
  ---
8
8
 
@@ -95,7 +95,7 @@ If the operator did **not** request socials, do not run the step; adding 1 to 3
95
95
 
96
96
  ## Step 4 — populate, render, deliver
97
97
 
98
- **Before writing any copy, call `voice-retrieve-conditioning`** with `format: "article"`, `length: "long"`, and `topic` set to the property's `addressLocality` or the marketing angle (e.g. "period townhouse in Hackney"). When the tool returns a non-null `styleCard`, condition the long-form narrative copy on it — the style card + exemplars feed the editorial copy register; brand-required structure rules still override. When the tool returns `{styleCard: null, exemplars: []}` (no operator voice profile yet, or `writer-craft` not enabled), fall back to the unconditioned register documented in `references/copy.md`. Emit narration `[property-brochure] voice-mirror styleCardBytes=<n> exemplars=<k>` when conditioning was applied.
98
+ **Before writing any copy, call `voice-retrieve-conditioning`** with `format: "article"`, `length: "long"`, `scope: "org"`, and `topic` set to the property's `addressLocality` or the marketing angle (e.g. "period townhouse in Hackney"). A brochure goes out under the agency's name, so it uses the **house voice** (`scope: "org"`), not any one operator's personal voice. When the tool returns a non-null `styleCard` (`status` of `ok` or `fallback-org`), condition the long-form narrative copy on it — the style card + exemplars feed the editorial copy register; brand-required structure rules still override. When `status` is `no-data` or `error`, fall back to the unconditioned register documented in `references/copy.md`. Emit narration `[property-brochure] voice-mirror styleCardBytes=<n> exemplars=<k>` when conditioning was applied.
99
99
 
100
100
  Read the seller brief once before writing any copy. Then walk the `{{ placeholder }}` tokens in both files top-to-bottom, substituting from the brief and `property.json`. The complete substitution contract — every token, its source field, its expected format and voice — lives in [references/placeholders.md](references/placeholders.md). Each token also has an inline `<!-- REPLACE: ... -->` comment in the template giving the short version.
101
101
 
@@ -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(() => {
@@ -1,122 +0,0 @@
1
- # Priced-quote compute engine
2
-
3
- This reference is the **compute core** for a structured, priced quote: it turns a job (the items the
4
- owner selected, with their measurements or counts) into priced figures — line prices, group
5
- subtotals, and a final total — computed by **this owner's own pricing method**.
6
-
7
- It carries no items, no units, no rates, no margins, no overheads, no taxes, and no roll-up sequence.
8
- Every such value and rule is the owner's, captured as data. A trade pricing by the square metre, a
9
- caterer pricing by the head, and a printer pricing by per-unit break are all priced by the same
10
- approach — only their captured method differs.
11
-
12
- The compute engine itself is **not shipped**. You generate a small bespoke engine for this owner the
13
- first time they need a priced quote, and regenerate it whenever their business changes. The captured
14
- method is data the engine reads. Both persist in the owner's workspace and are re-run on demand.
15
-
16
- ## When this path applies
17
-
18
- Use this for a **priced quote computed from the owner's method** — measured or counted work that has
19
- to be priced the way this owner prices it, then totalled. This is distinct from the simple
20
- photo-to-quote / chat-to-quote flow in `references/quoting.md`, which formats a quote the owner has
21
- already priced. If the owner has already worked out the numbers, stay in `quoting.md`. Come here when
22
- the numbers have to be **computed**.
23
-
24
- **Precondition:** a captured method for this owner must exist. Its *content* (the owner's items,
25
- rules, dials, roll-up, and past-job fixtures) is produced and maintained by the method-capture flow —
26
- Task 659 — which populates the method/job shape **this reference defines** below. If no captured method
27
- exists, capture it first; do not invent rates or rules here.
28
-
29
- ## Where the engine and method live
30
-
31
- The agent runs in the owner's workspace (the spawn cwd, `$ACCOUNT_DIR`). Keep the engine and the
32
- captured method together under a `quoting/` subdirectory there, addressed by relative path:
33
-
34
- - the **method** — the owner's items, dials, and roll-up, as data;
35
- - the **engine** — the bespoke script that reads a method and a job and prices it;
36
- - the owner's **past jobs**, each with the figures they actually issued, used to verify the engine.
37
-
38
- These persist across sessions, are edited when the business changes, and are the engine's only inputs.
39
- Nothing about the owner's business lives in this reference or in the plugin.
40
-
41
- ## The method shape (owner data)
42
-
43
- This section is the authoritative definition of the method/job shape; the method-capture flow (Task
44
- 659) fills it with one owner's content. The method describes how this owner prices. For each
45
- **priceable item** it records:
46
-
47
- - what the item is (an identifier, a human label, a unit);
48
- - **how a quantity arises** — the item's own rule for turning a measurement into a quantity (for
49
- example, an area from supplied dimensions, a length, a volume), or that the item carries **no
50
- measurement rule** because it is counted or taken as a single unit. This is the item's *default*
51
- quantity rule; a quantity entered on the job line always overrides it (see the job shape);
52
- - **how a line price is computed** — the owner's own rule for that item, in whatever form their
53
- history shows it takes;
54
- - which **group** the line rolls into (the owner's own section headings);
55
- - whether the item is **standing-rule priced** (a rule fixes its price) or **judgement priced** (the
56
- owner sets the price per job) — so the quote flow knows when it must ask the owner for a figure
57
- rather than computing one.
58
-
59
- Alongside the items the method holds:
60
-
61
- - named **dials** — the owner's own scalars that their rules reference (a labour day-rate, a margin
62
- multiplier, a percentage — whatever their method uses, named by them);
63
- - the **roll-up** — an ordered list of steps applied above the line items to reach the final total.
64
- Each step names its rule, the prior figures it reads, and the label of the figure it produces. This
65
- is whatever sequence the owner actually applies. It is **not** a fixed margin-then-overhead-then-tax
66
- chain: an owner may apply an overhead, a contingency, a discount, a tax, or nothing, in their own
67
- order. The engine implements exactly the step kinds this owner's roll-up uses, and no others.
68
-
69
- ## The job shape (per quote)
70
-
71
- A job is the work selected for one quote: a list of lines, each naming an item and supplying **either**
72
- measurements **or** a directly-entered quantity, plus — for a judgement-priced item — the price the
73
- owner set for this line. The quantity source is decided per line by precedence: a quantity entered on
74
- the line is used as-is; otherwise the engine derives the quantity from the line's measurements via the
75
- item's measurement rule. An item with no measurement rule therefore requires an entered quantity (or
76
- its single-unit default). The job also carries the header the issued quote needs (who it is for, the
77
- address, the reference).
78
-
79
- ## What the generated engine must do
80
-
81
- The engine reads one method and one job and produces the priced result. It must:
82
-
83
- 1. **Resolve each line's quantity** — an entered quantity takes precedence; otherwise derive it from
84
- the line's measurements via the item's measurement rule — and record which source was used.
85
- 2. **Resolve each line's price** — by the item's own rule, or by the per-line figure the owner gave for
86
- a judgement item — and record which was used.
87
- 3. **Group** the priced lines under the owner's own headings.
88
- 4. **Run the roll-up** steps in their defined order to reach the final total.
89
- 5. **Emit observability** so a wrong figure is diagnosable from the log, not just the document: per
90
- line, the resolved quantity and its source (measurement-derived vs entered) and the price rule
91
- applied and its source (item rule vs per-line override); and each roll-up step with its result.
92
- 6. **Reconcile** the sum of the group subtotals against the running line-item total and report it as an
93
- explicit pass/fail — not a silent assumption.
94
- 7. **Verify** on demand: given one of the owner's own past jobs, compare every computed figure
95
- (line, group, final) against the figures the owner actually issued and report each as a match or
96
- a mismatch.
97
-
98
- ## Expressiveness — bounded by the owner's history
99
-
100
- Build the engine to support **exactly** the quantity, pricing, and roll-up forms the owner's own
101
- history exercises — discovered when their method was captured. Do not add quantity bases, price-rule
102
- forms, or roll-up step kinds the owner does not use. Unused generality is a liability: it is untested
103
- against their figures and invites a wrong quote later. If a new job needs a form the method has never
104
- expressed, that is a change to the captured method (and a re-verification), not a guess made here.
105
-
106
- ## Acceptance — reproduction of the owner's own figures
107
-
108
- The engine is correct only when it **reproduces the owner's own past jobs** — line prices, group
109
- subtotals, and final totals — to the owner's own precision. The fixtures are that owner's
110
- history, supplied with their method; there are no built-in figures to test against. Reproducing their
111
- history, not parsing cleanly, is the signal that the engine prices the way they do. Verify on every
112
- generate and every method change; a reproduction failure blocks issuing a computed quote.
113
-
114
- ## Boundaries
115
-
116
- - **Capturing and maintaining** the content of the method (items, rules, dials, roll-up, past-job
117
- fixtures) is the method-capture flow — Task 659; it populates the shape this reference defines. This
118
- reference consumes that method; it does not build it.
119
- - **Rendering** the priced result into the documents the owner issues (internal view, client
120
- breakdown, branded quote) is Task 658. Those documents are printed to PDF through the existing
121
- `browser-pdf-save` path (see `references/invoicing.md`); this compute reference produces only the
122
- figures, not the documents.