@0xmaxma/claude-gateway 1.8.9 → 1.8.11

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 (121) hide show
  1. package/README.md +46 -6
  2. package/config.template.json +2 -1
  3. package/dist/agent/runner.d.ts +86 -1
  4. package/dist/agent/runner.d.ts.map +1 -1
  5. package/dist/agent/runner.js +396 -9
  6. package/dist/agent/runner.js.map +1 -1
  7. package/dist/api/router.d.ts.map +1 -1
  8. package/dist/api/router.js +1028 -6
  9. package/dist/api/router.js.map +1 -1
  10. package/dist/api/share-router.d.ts.map +1 -1
  11. package/dist/api/share-router.js +24 -0
  12. package/dist/api/share-router.js.map +1 -1
  13. package/dist/api/webhooks-router.d.ts +2 -0
  14. package/dist/api/webhooks-router.d.ts.map +1 -1
  15. package/dist/api/webhooks-router.js +2 -0
  16. package/dist/api/webhooks-router.js.map +1 -1
  17. package/dist/api/whatsapp-access.d.ts +123 -0
  18. package/dist/api/whatsapp-access.d.ts.map +1 -0
  19. package/dist/api/whatsapp-access.js +135 -0
  20. package/dist/api/whatsapp-access.js.map +1 -0
  21. package/dist/api/whatsapp-cloud-access.d.ts +48 -0
  22. package/dist/api/whatsapp-cloud-access.d.ts.map +1 -0
  23. package/dist/api/whatsapp-cloud-access.js +58 -0
  24. package/dist/api/whatsapp-cloud-access.js.map +1 -0
  25. package/dist/api/whatsapp-cloud-client.d.ts +143 -0
  26. package/dist/api/whatsapp-cloud-client.d.ts.map +1 -0
  27. package/dist/api/whatsapp-cloud-client.js +298 -0
  28. package/dist/api/whatsapp-cloud-client.js.map +1 -0
  29. package/dist/api/whatsapp-cloud-webhook-router.d.ts +132 -0
  30. package/dist/api/whatsapp-cloud-webhook-router.d.ts.map +1 -0
  31. package/dist/api/whatsapp-cloud-webhook-router.js +494 -0
  32. package/dist/api/whatsapp-cloud-webhook-router.js.map +1 -0
  33. package/dist/cli/args.d.ts +34 -0
  34. package/dist/cli/args.d.ts.map +1 -1
  35. package/dist/cli/args.js +55 -0
  36. package/dist/cli/args.js.map +1 -1
  37. package/dist/cli/commands/app.d.ts +28 -0
  38. package/dist/cli/commands/app.d.ts.map +1 -0
  39. package/dist/cli/commands/app.js +411 -0
  40. package/dist/cli/commands/app.js.map +1 -0
  41. package/dist/cli/commands/doctor.d.ts.map +1 -1
  42. package/dist/cli/commands/doctor.js +77 -16
  43. package/dist/cli/commands/doctor.js.map +1 -1
  44. package/dist/cli/commands/service.d.ts.map +1 -1
  45. package/dist/cli/commands/service.js +373 -56
  46. package/dist/cli/commands/service.js.map +1 -1
  47. package/dist/cli/commands/update.d.ts.map +1 -1
  48. package/dist/cli/commands/update.js +1 -17
  49. package/dist/cli/commands/update.js.map +1 -1
  50. package/dist/cli/http-client.d.ts +8 -0
  51. package/dist/cli/http-client.d.ts.map +1 -1
  52. package/dist/cli/http-client.js +5 -2
  53. package/dist/cli/http-client.js.map +1 -1
  54. package/dist/cli/index.d.ts.map +1 -1
  55. package/dist/cli/index.js +16 -4
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/prompt.d.ts +17 -2
  58. package/dist/cli/prompt.d.ts.map +1 -1
  59. package/dist/cli/prompt.js +33 -2
  60. package/dist/cli/prompt.js.map +1 -1
  61. package/dist/config/loader.d.ts +8 -0
  62. package/dist/config/loader.d.ts.map +1 -1
  63. package/dist/config/loader.js +22 -4
  64. package/dist/config/loader.js.map +1 -1
  65. package/dist/config/watcher.d.ts.map +1 -1
  66. package/dist/config/watcher.js +12 -0
  67. package/dist/config/watcher.js.map +1 -1
  68. package/dist/config/whatsapp-accounts.d.ts +77 -0
  69. package/dist/config/whatsapp-accounts.d.ts.map +1 -0
  70. package/dist/config/whatsapp-accounts.js +218 -0
  71. package/dist/config/whatsapp-accounts.js.map +1 -0
  72. package/dist/history/db.d.ts.map +1 -1
  73. package/dist/history/db.js +43 -11
  74. package/dist/history/db.js.map +1 -1
  75. package/dist/history/types.d.ts +18 -1
  76. package/dist/history/types.d.ts.map +1 -1
  77. package/dist/history/types.js +1 -1
  78. package/dist/history/types.js.map +1 -1
  79. package/dist/index.js +26 -0
  80. package/dist/index.js.map +1 -1
  81. package/dist/session/process.d.ts.map +1 -1
  82. package/dist/session/process.js +37 -0
  83. package/dist/session/process.js.map +1 -1
  84. package/dist/session/store.d.ts +3 -2
  85. package/dist/session/store.d.ts.map +1 -1
  86. package/dist/session/store.js +1 -1
  87. package/dist/session/store.js.map +1 -1
  88. package/dist/share/session-video-catalog.d.ts +19 -0
  89. package/dist/share/session-video-catalog.d.ts.map +1 -0
  90. package/dist/share/session-video-catalog.js +119 -0
  91. package/dist/share/session-video-catalog.js.map +1 -0
  92. package/dist/shared/image-optimize.d.ts +32 -0
  93. package/dist/shared/image-optimize.d.ts.map +1 -0
  94. package/dist/shared/image-optimize.js +197 -0
  95. package/dist/shared/image-optimize.js.map +1 -0
  96. package/dist/shared/image-sniff.d.ts +9 -0
  97. package/dist/shared/image-sniff.d.ts.map +1 -1
  98. package/dist/shared/image-sniff.js +13 -0
  99. package/dist/shared/image-sniff.js.map +1 -1
  100. package/dist/shared/text-chunk.d.ts +37 -0
  101. package/dist/shared/text-chunk.d.ts.map +1 -0
  102. package/dist/shared/text-chunk.js +105 -0
  103. package/dist/shared/text-chunk.js.map +1 -0
  104. package/dist/shared/whatsapp-ack.d.ts +13 -0
  105. package/dist/shared/whatsapp-ack.d.ts.map +1 -0
  106. package/dist/shared/whatsapp-ack.js +16 -0
  107. package/dist/shared/whatsapp-ack.js.map +1 -0
  108. package/dist/types.d.ts +183 -0
  109. package/dist/types.d.ts.map +1 -1
  110. package/dist/whatsapp/manager.d.ts +173 -0
  111. package/dist/whatsapp/manager.d.ts.map +1 -0
  112. package/dist/whatsapp/manager.js +838 -0
  113. package/dist/whatsapp/manager.js.map +1 -0
  114. package/mcp/server.ts +4 -0
  115. package/mcp/tools/image/module.ts +6 -1
  116. package/mcp/tools/telegram/receiver-server.ts +15 -8
  117. package/mcp/tools/telegram/reply-attachment.ts +81 -0
  118. package/mcp/tools/video/module.ts +6 -1
  119. package/mcp/tools/whatsapp/module.ts +162 -0
  120. package/mcp/tools/whatsapp-cloud/module.ts +536 -0
  121. package/package.json +7 -2
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.optimizeImage = optimizeImage;
37
+ exports.optimizeImageFile = optimizeImageFile;
38
+ /**
39
+ * Best-effort image downscaler for OUTBOUND channel attachments.
40
+ *
41
+ * Both WhatsApp channels used to hard-fail an oversized image: the Baileys
42
+ * manager threw `image exceeds N byte cap` and the Cloud MCP tool let Meta
43
+ * reject the upload. From the agent's point of view that is the worst possible
44
+ * outcome — it generated a perfectly good chart/screenshot and the reply just
45
+ * disappears. Shrinking the picture is almost always what the human wanted, so
46
+ * the send paths now run an over-cap image through `optimizeImage` first.
47
+ *
48
+ * Deliberately NOT applied on a document/`asDocument` send: sending as a
49
+ * document is how the agent asks for the EXACT bytes to arrive (WhatsApp
50
+ * re-compresses anything sent as a photo — see WhatsAppManager.sendMessage's
51
+ * forceDocument comment), so recompressing there would defeat the only reason
52
+ * to pick that mode.
53
+ *
54
+ * `sharp` is loaded lazily via dynamic import for two reasons: it is a native
55
+ * module, so a static top-level import would make EVERY test that transitively
56
+ * reaches the send paths pay its load cost (and fail outright on a platform
57
+ * without a prebuilt binary), and a lazy load keeps this module importable —
58
+ * and therefore mockable — even when sharp itself is unavailable.
59
+ */
60
+ const fs = __importStar(require("fs"));
61
+ const os = __importStar(require("os"));
62
+ const path = __importStar(require("path"));
63
+ /**
64
+ * Quality sweep, tried at the ORIGINAL dimensions first. Most over-cap images
65
+ * from an agent are large PNG screenshots whose size is encoding, not pixels —
66
+ * a JPEG re-encode alone usually gets them under any sane cap while keeping
67
+ * every pixel of detail, so dimensions are only sacrificed once quality alone
68
+ * has failed.
69
+ */
70
+ const QUALITY_STEPS = [80, 60, 40];
71
+ /**
72
+ * Dimension sweep, applied only after the quality sweep failed, each at the
73
+ * lowest quality above. Scales (not fixed widths) so the aspect ratio and the
74
+ * relationship to the source resolution are both preserved.
75
+ */
76
+ const SCALE_STEPS = [0.75, 0.5, 0.35, 0.25];
77
+ /** The most aggressive quality, reused for every scaled attempt. */
78
+ const MIN_QUALITY = QUALITY_STEPS[QUALITY_STEPS.length - 1];
79
+ let cachedSharp = null;
80
+ /**
81
+ * Resolve sharp's callable export across the CJS/ESM interop shapes it can
82
+ * arrive in (a bare function under plain `require`, or under `.default` once
83
+ * TypeScript's esModuleInterop wraps it). Throws if neither is present — every
84
+ * caller here treats a throw as "leave the image alone".
85
+ */
86
+ async function loadSharp() {
87
+ if (cachedSharp)
88
+ return cachedSharp;
89
+ const mod = await Promise.resolve().then(() => __importStar(require('sharp')));
90
+ const factory = typeof mod === 'function'
91
+ ? mod
92
+ : (mod?.default);
93
+ if (typeof factory !== 'function')
94
+ throw new Error('sharp: no callable export');
95
+ cachedSharp = factory;
96
+ return factory;
97
+ }
98
+ /**
99
+ * Shrink `buffer` until it fits `maxBytes`, or as close as this can get.
100
+ *
101
+ * Contract — never throws, always returns something sendable:
102
+ * - already at/under the cap (or an empty/`maxBytes <= 0` input) → the
103
+ * ORIGINAL buffer, byte for byte, with sharp never loaded at all;
104
+ * - a sweep step lands under the cap → that step's output;
105
+ * - every step still exceeds the cap → the SMALLEST attempt seen (which may
106
+ * still be over — the caller decides what to do with a best effort that was
107
+ * not enough), or the original if no attempt managed to beat it;
108
+ * - sharp missing, or the bytes are not a decodable image → the original.
109
+ *
110
+ * Output is JPEG. That is a deliberate narrowing rather than format
111
+ * preservation: JPEG is the only format here with a quality knob that reliably
112
+ * trades detail for bytes, and both WhatsApp channels re-encode a photo
113
+ * on their own anyway. Transparency is flattened onto white first, because
114
+ * JPEG has no alpha channel and sharp's default would composite onto black.
115
+ */
116
+ async function optimizeImage(buffer, maxBytes) {
117
+ if (!buffer || buffer.length === 0)
118
+ return buffer;
119
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0)
120
+ return buffer;
121
+ if (buffer.length <= maxBytes)
122
+ return buffer;
123
+ let best = buffer;
124
+ try {
125
+ const sharp = await loadSharp();
126
+ // `rotate()` with no argument applies the EXIF orientation and drops the
127
+ // tag — without it a re-encoded phone photo can come out sideways.
128
+ const { width } = await sharp(buffer).metadata();
129
+ for (const quality of QUALITY_STEPS) {
130
+ const out = await sharp(buffer)
131
+ .rotate()
132
+ .flatten({ background: '#ffffff' })
133
+ .jpeg({ quality, mozjpeg: true })
134
+ .toBuffer();
135
+ if (out.length < best.length)
136
+ best = out;
137
+ if (out.length <= maxBytes)
138
+ return out;
139
+ }
140
+ // Quality alone was not enough — start giving up pixels too. Skipped
141
+ // entirely when the source has no readable width (sharp could not tell us
142
+ // the dimensions), since there is nothing sane to scale from.
143
+ if (width && width > 0) {
144
+ for (const scale of SCALE_STEPS) {
145
+ const target = Math.max(1, Math.round(width * scale));
146
+ const out = await sharp(buffer)
147
+ .rotate()
148
+ .resize({ width: target, withoutEnlargement: true })
149
+ .flatten({ background: '#ffffff' })
150
+ .jpeg({ quality: MIN_QUALITY, mozjpeg: true })
151
+ .toBuffer();
152
+ if (out.length < best.length)
153
+ best = out;
154
+ if (out.length <= maxBytes)
155
+ return out;
156
+ }
157
+ }
158
+ }
159
+ catch {
160
+ // sharp unavailable, or these bytes are not a decodable image. Best-effort
161
+ // by contract: hand back whatever is smallest so the caller's existing
162
+ // over-cap handling still runs, rather than failing the whole reply here.
163
+ }
164
+ return best;
165
+ }
166
+ /**
167
+ * File-level wrapper for the two send paths: shrink `srcPath` under `maxBytes`
168
+ * and return a path to the result.
169
+ *
170
+ * Returns `srcPath` UNCHANGED whenever nothing was gained — already under the
171
+ * cap, unreadable, or `optimizeImage` handed back the original — so a caller
172
+ * can compare identity to know whether a temp file was created. The temp file
173
+ * lands in the OS temp dir with a `.jpg` extension matching what
174
+ * `optimizeImage` actually produces, alongside the inbound media temp files
175
+ * the channel routers already write there.
176
+ */
177
+ async function optimizeImageFile(srcPath, maxBytes) {
178
+ let buf;
179
+ try {
180
+ buf = fs.readFileSync(srcPath);
181
+ }
182
+ catch {
183
+ return srcPath;
184
+ }
185
+ const out = await optimizeImage(buf, maxBytes);
186
+ if (out === buf || out.length >= buf.length)
187
+ return srcPath;
188
+ try {
189
+ const dest = path.join(os.tmpdir(), `gw-optimized-${Date.now()}-${path.basename(srcPath, path.extname(srcPath)).slice(0, 64)}.jpg`);
190
+ fs.writeFileSync(dest, out);
191
+ return dest;
192
+ }
193
+ catch {
194
+ return srcPath;
195
+ }
196
+ }
197
+ //# sourceMappingURL=image-optimize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-optimize.js","sourceRoot":"","sources":["../../src/shared/image-optimize.ts"],"names":[],"mappings":"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"}
@@ -12,4 +12,13 @@
12
12
  * not send one at all.
13
13
  */
14
14
  export declare function sniffImageExt(buf: Buffer): string;
15
+ /**
16
+ * Reduce a sender-controlled id (WhatsApp message id, etc.) to a safe temp
17
+ * filename component. Callers build paths like
18
+ * `path.join(os.tmpdir(), \`prefix-${id}.ext\`)` from a value the OTHER
19
+ * party's client sets, not us — an id containing `../` segments would
20
+ * otherwise resolve outside the intended directory. Falls back to the
21
+ * current timestamp when the id is empty/missing or sanitizes to nothing.
22
+ */
23
+ export declare function sanitizeFilenameId(id: string | undefined | null): string;
15
24
  //# sourceMappingURL=image-sniff.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-sniff.d.ts","sourceRoot":"","sources":["../../src/shared/image-sniff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD"}
1
+ {"version":3,"file":"image-sniff.d.ts","sourceRoot":"","sources":["../../src/shared/image-sniff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAGxE"}
@@ -7,6 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.sniffImageExt = sniffImageExt;
10
+ exports.sanitizeFilenameId = sanitizeFilenameId;
10
11
  /**
11
12
  * Pick a file extension from an image's magic bytes; default jpg.
12
13
  *
@@ -26,4 +27,16 @@ function sniffImageExt(buf) {
26
27
  return 'webp';
27
28
  return 'jpg';
28
29
  }
30
+ /**
31
+ * Reduce a sender-controlled id (WhatsApp message id, etc.) to a safe temp
32
+ * filename component. Callers build paths like
33
+ * `path.join(os.tmpdir(), \`prefix-${id}.ext\`)` from a value the OTHER
34
+ * party's client sets, not us — an id containing `../` segments would
35
+ * otherwise resolve outside the intended directory. Falls back to the
36
+ * current timestamp when the id is empty/missing or sanitizes to nothing.
37
+ */
38
+ function sanitizeFilenameId(id) {
39
+ const cleaned = (id ?? '').replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64);
40
+ return cleaned || String(Date.now());
41
+ }
29
42
  //# sourceMappingURL=image-sniff.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-sniff.js","sourceRoot":"","sources":["../../src/shared/image-sniff.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AASH,sCASC;AAhBD;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,GAAW;IACvC,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3F,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9G,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3F,IACE,GAAG,CAAC,MAAM,IAAI,EAAE;QAChB,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI;QAC1E,OAAO,MAAM,CAAC;IAChB,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"image-sniff.js","sourceRoot":"","sources":["../../src/shared/image-sniff.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AASH,sCASC;AAUD,gDAGC;AA7BD;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,GAAW;IACvC,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3F,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9G,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3F,IACE,GAAG,CAAC,MAAM,IAAI,EAAE;QAChB,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI;QAC1E,OAAO,MAAM,CAAC;IAChB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAAC,EAA6B;IAC9D,MAAM,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACxE,OAAO,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Channel-agnostic text chunker.
3
+ *
4
+ * Modeled on `src/agent/line-pure.ts`'s `splitForLine` (same paragraph →
5
+ * newline → space cut-point preference) but WITHOUT LINE's two
6
+ * platform-specific behaviours:
7
+ *
8
+ * - No 5-bubble cap. LINE can only carry 5 message objects per reply/push
9
+ * call, so `splitForLine` stops after 5 chunks and truncates. WhatsApp has
10
+ * no such per-request limit (each chunk is its own API call / socket send),
11
+ * so this keeps splitting until the whole text is emitted.
12
+ * - No ellipsis/truncation. Nothing is ever dropped — the concatenation of
13
+ * the returned chunks is the input, modulo whitespace collapsed exactly at
14
+ * the cut points.
15
+ *
16
+ * Used by both WhatsApp channels (Cloud API's 4096-char text limit, Baileys'
17
+ * conservative 4000) and safe for any other channel that needs plain splitting.
18
+ */
19
+ /**
20
+ * Split `text` into chunks of at most `maxChars` characters.
21
+ *
22
+ * `mode`:
23
+ * - `'newline'` (default) — prefer to break on the last paragraph break,
24
+ * then the last newline, then the last space that falls inside the budget,
25
+ * so chunks end at a natural boundary. Whitespace at a cut point is
26
+ * trimmed (it would otherwise show up as a leading blank line on the next
27
+ * message bubble). Falls back to a hard cut when no boundary sits late
28
+ * enough in the budget to be worth using.
29
+ * - `'length'` — hard cut at exactly `maxChars`, no boundary search and no
30
+ * whitespace trimming. For callers whose limit is a strict byte/char
31
+ * budget where losing a space would matter.
32
+ *
33
+ * Returns `[]` for empty input and `[text]` when it already fits. A hard cut
34
+ * (either mode) never splits a surrogate pair — see avoidSurrogateSplit.
35
+ */
36
+ export declare function chunkText(text: string, maxChars: number, mode?: 'length' | 'newline'): string[];
37
+ //# sourceMappingURL=text-chunk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-chunk.d.ts","sourceRoot":"","sources":["../../src/shared/text-chunk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAqBH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,QAAQ,GAAG,SAAqB,GACrC,MAAM,EAAE,CA0CV"}
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Channel-agnostic text chunker.
4
+ *
5
+ * Modeled on `src/agent/line-pure.ts`'s `splitForLine` (same paragraph →
6
+ * newline → space cut-point preference) but WITHOUT LINE's two
7
+ * platform-specific behaviours:
8
+ *
9
+ * - No 5-bubble cap. LINE can only carry 5 message objects per reply/push
10
+ * call, so `splitForLine` stops after 5 chunks and truncates. WhatsApp has
11
+ * no such per-request limit (each chunk is its own API call / socket send),
12
+ * so this keeps splitting until the whole text is emitted.
13
+ * - No ellipsis/truncation. Nothing is ever dropped — the concatenation of
14
+ * the returned chunks is the input, modulo whitespace collapsed exactly at
15
+ * the cut points.
16
+ *
17
+ * Used by both WhatsApp channels (Cloud API's 4096-char text limit, Baileys'
18
+ * conservative 4000) and safe for any other channel that needs plain splitting.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.chunkText = chunkText;
22
+ /**
23
+ * A hard cut at `idx` (a UTF-16 code-unit index) can land between the two
24
+ * code units of a surrogate pair (e.g. most emoji, which sit outside the
25
+ * BMP) — splitting one across two chunks produces an unpaired surrogate in
26
+ * each, which renders as U+FFFD / mojibake on the receiving side. Nudges
27
+ * `idx` back by one so the pair stays together, unless doing so would leave
28
+ * no room in this chunk at all (`idx <= minIdx`), in which case the split is
29
+ * accepted rather than looping forever on an impossibly small budget.
30
+ */
31
+ function avoidSurrogateSplit(text, idx, minIdx = 0) {
32
+ if (idx <= minIdx || idx >= text.length)
33
+ return idx;
34
+ const before = text.charCodeAt(idx - 1);
35
+ const at = text.charCodeAt(idx);
36
+ const isHighSurrogate = before >= 0xd800 && before <= 0xdbff;
37
+ const isLowSurrogate = at >= 0xdc00 && at <= 0xdfff;
38
+ if (isHighSurrogate && isLowSurrogate && idx - 1 > minIdx)
39
+ return idx - 1;
40
+ return idx;
41
+ }
42
+ /**
43
+ * Split `text` into chunks of at most `maxChars` characters.
44
+ *
45
+ * `mode`:
46
+ * - `'newline'` (default) — prefer to break on the last paragraph break,
47
+ * then the last newline, then the last space that falls inside the budget,
48
+ * so chunks end at a natural boundary. Whitespace at a cut point is
49
+ * trimmed (it would otherwise show up as a leading blank line on the next
50
+ * message bubble). Falls back to a hard cut when no boundary sits late
51
+ * enough in the budget to be worth using.
52
+ * - `'length'` — hard cut at exactly `maxChars`, no boundary search and no
53
+ * whitespace trimming. For callers whose limit is a strict byte/char
54
+ * budget where losing a space would matter.
55
+ *
56
+ * Returns `[]` for empty input and `[text]` when it already fits. A hard cut
57
+ * (either mode) never splits a surrogate pair — see avoidSurrogateSplit.
58
+ */
59
+ function chunkText(text, maxChars, mode = 'newline') {
60
+ if (!text)
61
+ return [];
62
+ // A non-positive budget can never make progress — treat it as "no split"
63
+ // rather than looping forever.
64
+ if (!Number.isFinite(maxChars) || maxChars < 1)
65
+ return [text];
66
+ if (text.length <= maxChars)
67
+ return [text];
68
+ if (mode === 'length') {
69
+ const chunks = [];
70
+ let i = 0;
71
+ while (i < text.length) {
72
+ const end = avoidSurrogateSplit(text, Math.min(i + maxChars, text.length), i);
73
+ chunks.push(text.slice(i, end));
74
+ i = end;
75
+ }
76
+ return chunks;
77
+ }
78
+ const chunks = [];
79
+ let remaining = text;
80
+ // Only accept a boundary in the LATER half of the budget: an earlier one
81
+ // wastes so much of the chunk that it produces more messages than the text
82
+ // needs (same heuristic splitForLine uses).
83
+ const half = Math.floor(maxChars * 0.5);
84
+ while (remaining) {
85
+ if (remaining.length <= maxChars) {
86
+ chunks.push(remaining);
87
+ break;
88
+ }
89
+ let cut = remaining.lastIndexOf('\n\n', maxChars);
90
+ if (cut < half)
91
+ cut = remaining.lastIndexOf('\n', maxChars);
92
+ if (cut < half)
93
+ cut = remaining.lastIndexOf(' ', maxChars);
94
+ if (cut <= 0)
95
+ cut = avoidSurrogateSplit(remaining, maxChars);
96
+ const piece = remaining.slice(0, cut).replace(/\s+$/, '');
97
+ // A cut that trims to nothing (a run of whitespace) would push an empty
98
+ // bubble; skip it, but still advance `remaining` so the loop terminates.
99
+ if (piece)
100
+ chunks.push(piece);
101
+ remaining = remaining.slice(cut).replace(/^\s+/, '');
102
+ }
103
+ return chunks;
104
+ }
105
+ //# sourceMappingURL=text-chunk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-chunk.js","sourceRoot":"","sources":["../../src/shared/text-chunk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The ack reaction both WhatsApp channels leave on an inbound message while
3
+ * the agent works on it, cleared once the reply lands (Phase 2 of the
4
+ * WhatsApp feature-parity plan, ported from Slack's ack reaction).
5
+ *
6
+ * Lives here rather than in either channel's client so the Baileys manager
7
+ * (src/whatsapp/manager.ts) and the Cloud client (src/api/whatsapp-cloud-client.ts)
8
+ * share ONE value and can never drift into reacting with two different emoji
9
+ * for the same behaviour. ⏳ is the direct equivalent of the
10
+ * `hourglass_flowing_sand` Slack reacts with.
11
+ */
12
+ export declare const WHATSAPP_ACK_EMOJI = "\u23F3";
13
+ //# sourceMappingURL=whatsapp-ack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-ack.d.ts","sourceRoot":"","sources":["../../src/shared/whatsapp-ack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,WAAM,CAAC"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WHATSAPP_ACK_EMOJI = void 0;
4
+ /**
5
+ * The ack reaction both WhatsApp channels leave on an inbound message while
6
+ * the agent works on it, cleared once the reply lands (Phase 2 of the
7
+ * WhatsApp feature-parity plan, ported from Slack's ack reaction).
8
+ *
9
+ * Lives here rather than in either channel's client so the Baileys manager
10
+ * (src/whatsapp/manager.ts) and the Cloud client (src/api/whatsapp-cloud-client.ts)
11
+ * share ONE value and can never drift into reacting with two different emoji
12
+ * for the same behaviour. ⏳ is the direct equivalent of the
13
+ * `hourglass_flowing_sand` Slack reacts with.
14
+ */
15
+ exports.WHATSAPP_ACK_EMOJI = '⏳';
16
+ //# sourceMappingURL=whatsapp-ack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-ack.js","sourceRoot":"","sources":["../../src/shared/whatsapp-ack.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;GAUG;AACU,QAAA,kBAAkB,GAAG,GAAG,CAAC"}
package/dist/types.d.ts CHANGED
@@ -8,6 +8,75 @@ export interface HistoryConfig {
8
8
  retentionDays?: number;
9
9
  maxHistoryMessages?: number;
10
10
  }
11
+ /**
12
+ * One linked WhatsApp number (Baileys device-link) belonging to an agent.
13
+ *
14
+ * Multi-account (Phase 1 of the WhatsApp feature-parity plan): an agent used
15
+ * to have exactly ONE flat `whatsapp` block; it now has an array of these.
16
+ * Every access-control field below is per-account — two numbers on the same
17
+ * agent can run completely different DM/group policies.
18
+ *
19
+ * There is still NO credential field here: the "credential" remains the
20
+ * linked device session on disk. Its location is derived from `id` —
21
+ * `'default'` keeps the historical bare `<workspace>/.whatsapp-state/`
22
+ * directory (so upgrading a live gateway never has to move a linked
23
+ * session), every other account gets `<workspace>/.whatsapp-state/<id>/`.
24
+ */
25
+ export interface WhatsAppAccountConfig {
26
+ /**
27
+ * Stable slug identifying this account. Doubles as the on-disk state
28
+ * directory suffix and as the `account_id` the `whatsapp_reply` MCP tool
29
+ * passes back, so it must be filesystem-safe and must never be reused for
30
+ * a different number. `'default'` is reserved for the pre-multi-account
31
+ * session (see the class doc above).
32
+ */
33
+ id: string;
34
+ /** Display name shown in the Settings UI (e.g. "Sales line"). Falls back to `id`. */
35
+ label?: string;
36
+ /**
37
+ * DM access policy (mirrors `slack.dmPolicy`/`line.dmPolicy` exactly).
38
+ * Gates 1:1 conversations (JIDs ending `@s.whatsapp.net`). Necessary
39
+ * even though the number itself is authenticated via QR/pairing-code
40
+ * linking: unlike a fresh bot token, a WhatsApp number is typically the
41
+ * owner's real number that other people already have — without this,
42
+ * anyone who knows the number could reach the agent once linked.
43
+ */
44
+ dmPolicy?: 'open' | 'allowlist' | 'disabled';
45
+ /** Allowed sender JIDs/numbers (E.164 or bare digits — normalized at the access-gate boundary). */
46
+ dmAllowlist?: string[];
47
+ /**
48
+ * Group access policy (mirrors `slack.groupPolicy`/`line.groupPolicy`).
49
+ * Gates group JIDs (ending `@g.us`) the linked number is a member of.
50
+ */
51
+ groupPolicy?: 'open' | 'allowlist' | 'disabled';
52
+ /** Allowed group JIDs. */
53
+ groupAllowlist?: string[];
54
+ /**
55
+ * In groups, only respond when the bot is @mentioned (mirrors
56
+ * `slack.requireMention`/`line.requireMention`). Default true. No
57
+ * effect on DMs.
58
+ */
59
+ requireMention?: boolean;
60
+ /**
61
+ * Pairing aid for the allowlist (mirrors `slack.pairing`/`line.pairing`
62
+ * exactly). Default true (absent ⇒ on). Only has an effect under
63
+ * `allowlist` (closed-default) for either tier.
64
+ */
65
+ pairing?: boolean;
66
+ /**
67
+ * Mark inbound messages as read (blue double-ticks) once they pass the
68
+ * access gate. Default true (absent ⇒ on) — the agent HAS received the
69
+ * message at that point, so the receipt is honest. Set false for a number
70
+ * whose owner would rather not advertise that. Phase 2.
71
+ */
72
+ sendReadReceipts?: boolean;
73
+ /**
74
+ * Ack-reaction level (ported from Slack's ack reaction). `'ack'` (default
75
+ * when absent) reacts ⏳ on the inbound message at receipt and clears it
76
+ * once the reply lands; `'off'` skips BOTH the add and the clear. Phase 2.
77
+ */
78
+ reactionLevel?: 'off' | 'ack';
79
+ }
11
80
  export interface AgentConfig {
12
81
  id: string;
13
82
  description: string;
@@ -125,6 +194,95 @@ export interface AgentConfig {
125
194
  */
126
195
  pairing?: boolean;
127
196
  };
197
+ /**
198
+ * WhatsApp — via WhatsApp Web device linking (the Baileys library), NOT
199
+ * the official Meta Cloud API. Unlike every other channel, there is NO
200
+ * credential field here: the "credential" is the linked device session
201
+ * itself, persisted on disk at `<workspace>/.whatsapp-state/` (Baileys'
202
+ * `creds.json`, written by `useMultiFileAuthState`), not in this config.
203
+ * Live link status (unlinked/pending-scan/linked/reconnecting) is
204
+ * ephemeral runtime state held by WhatsAppManager, never persisted here.
205
+ *
206
+ * Unofficial/reverse-engineered protocol — using it violates WhatsApp's
207
+ * ToS and carries real account-ban risk (elevated further since GetPod
208
+ * agents run on cloud/datacenter IPs, the exact traffic profile
209
+ * WhatsApp's abuse detection flags). This is a user-accepted trade-off,
210
+ * disclosed prominently in the web UI's connect flow — not enforced or
211
+ * mitigated by this config.
212
+ */
213
+ whatsapp?: {
214
+ /**
215
+ * The linked numbers for this agent — one entry per WhatsApp account
216
+ * (Phase 1 of the WhatsApp feature-parity plan replaced the single flat
217
+ * block that used to live here with this array). Order is meaningful
218
+ * only for back-compat: `accounts[0]` is what the legacy flat
219
+ * `whatsapp_*` API response fields are derived from.
220
+ *
221
+ * An absent or empty array is treated as `[{ id: 'default' }]` at
222
+ * runtime (see `resolveWhatsAppAccounts` in src/config/whatsapp-accounts.ts),
223
+ * so an agent that has never been configured still gets a linkable
224
+ * 'default' account exactly like it did before multi-account.
225
+ */
226
+ accounts: WhatsAppAccountConfig[];
227
+ };
228
+ /**
229
+ * WhatsApp — via the official Meta WhatsApp Business Cloud API (webhook +
230
+ * bearer-token REST, NOT the unofficial Baileys device-link bridge — see
231
+ * `whatsapp` above). Real, first-class credentials issued by Meta, so this
232
+ * channel follows the Slack template (webhook-based with real credentials)
233
+ * rather than the Baileys `whatsapp` block's on-disk-session pattern.
234
+ *
235
+ * DM-only: the Cloud API has no group concept (a WhatsApp Business number
236
+ * cannot be added to a group chat the way a personal/linked number can),
237
+ * so there is no groupPolicy/groupAllowlist/requireMention here — every
238
+ * inbound sender is a bare phone-number string, never a JID.
239
+ */
240
+ whatsapp_cloud?: {
241
+ /** Permanent (or long-lived) access token — Bearer auth for the Graph API. */
242
+ accessToken: string;
243
+ /** The WhatsApp Business phone number id (Meta's numeric id, not the phone number itself). */
244
+ phoneNumberId: string;
245
+ /** App Secret — verifies X-Hub-Signature-256 (HMAC-SHA256 of the raw request body). */
246
+ appSecret: string;
247
+ /** Verify token — compared against `hub.verify_token` on the GET webhook handshake. */
248
+ verifyToken: string;
249
+ /**
250
+ * DM access policy (mirrors `slack.dmPolicy`/`line.dmPolicy` exactly).
251
+ * Gates inbound senders — a bare phone-number string (e.g. "66812345678"),
252
+ * NOT a JID. `dmAllowlist` entries must be bare digits.
253
+ */
254
+ dmPolicy?: 'open' | 'allowlist' | 'disabled';
255
+ /** Allowed sender phone numbers — bare digits (E.164 without the leading "+"), never JIDs. */
256
+ dmAllowlist?: string[];
257
+ /**
258
+ * Pairing aid for the allowlist (mirrors `slack.pairing`/`line.pairing`
259
+ * exactly). Default true (absent ⇒ on). Only has an effect under
260
+ * `allowlist` (closed-default).
261
+ */
262
+ pairing?: boolean;
263
+ /**
264
+ * Mark inbound messages as read (blue double-ticks) once they pass the
265
+ * access gate. Default true (absent ⇒ on). Same field/semantics as the
266
+ * per-account Baileys `WhatsAppAccountConfig.sendReadReceipts`. Phase 2.
267
+ */
268
+ sendReadReceipts?: boolean;
269
+ /**
270
+ * Ack-reaction level (ported from Slack's ack reaction). `'ack'` (default
271
+ * when absent) reacts ⏳ on the inbound message at receipt and clears it
272
+ * once `whatsapp_cloud_reply` lands; `'off'` skips both. Phase 2.
273
+ */
274
+ reactionLevel?: 'off' | 'ack';
275
+ /**
276
+ * Allow `whatsapp_cloud_reply` to send pre-approved message TEMPLATES
277
+ * (Phase 3). Default false — opt-in on purpose, unlike the other Phase 2/3
278
+ * booleans here which default on: a template is the one send that reaches
279
+ * a user OUTSIDE WhatsApp's 24h customer-service window, so an agent must
280
+ * never gain that reach silently just by upgrading the gateway. With this
281
+ * unset, a template send is refused with an explanatory error rather than
282
+ * being dropped.
283
+ */
284
+ templatesEnabled?: boolean;
285
+ };
128
286
  claude: {
129
287
  model: string;
130
288
  /** @deprecated --dangerously-skip-permissions is always passed now; this field is ignored. */
@@ -528,6 +686,10 @@ export interface SessionMeta {
528
686
  * can restore the composer selection on reload; the agent's own context is the
529
687
  * functional source of truth. Updated whenever a send carries image_params. */
530
688
  imageConfig?: ImageParams;
689
+ /** Last composer video options sent for this session. Mirrors imageConfig —
690
+ * persisted so the web can restore the composer selection on reload. Updated
691
+ * whenever a send carries video_params. */
692
+ videoConfig?: VideoParams;
531
693
  /** Real model from Claude stream, updated per turn (e.g. "claude-opus-4-8"). */
532
694
  model?: string;
533
695
  }
@@ -561,6 +723,27 @@ export type ImageParams = {
561
723
  image_refs?: string[];
562
724
  n?: number;
563
725
  };
726
+ /**
727
+ * Video-generation options selected in the web composer (per-session), passed
728
+ * through the chat send body as `video_params` and surfaced to the agent so it
729
+ * calls the `generate_video` MCP tool with these exact values. Mirrors ImageParams.
730
+ * Without this bridge the agent receives no composer context for video and invents
731
+ * duration/aspect (e.g. a non-existent 10s cap, an 8+8 scene split, or a landscape
732
+ * clip when 9:16 was selected).
733
+ */
734
+ export type VideoParams = {
735
+ model?: string;
736
+ resolution?: string;
737
+ aspect_ratio?: string;
738
+ /** Discrete clip length in seconds (model-specific, e.g. 6/10/15). */
739
+ duration?: number;
740
+ /**
741
+ * Source frame for image-to-video, if the composer selected one. A `ref` value
742
+ * (media-relative path or `artifact:<id>`) passed as the generate_video `image`
743
+ * argument. When present the source frame's own aspect wins over aspect_ratio.
744
+ */
745
+ image_ref?: string;
746
+ };
564
747
  export type StreamEvent = {
565
748
  type: 'text_delta';
566
749
  text: string;