talon-agent 4.3.1 → 4.3.3

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 (133) hide show
  1. package/package.json +1 -1
  2. package/src/app.ts +1 -1
  3. package/src/backend/claude-sdk/doctor.ts +1 -1
  4. package/src/backend/codex/doctor.ts +1 -1
  5. package/src/backend/openai-agents/doctor.ts +1 -1
  6. package/src/bootstrap.ts +3 -3
  7. package/src/cli/doctor.ts +1 -1
  8. package/src/cli/skill.ts +2 -2
  9. package/src/core/agent-runtime/backend-registry.ts +1 -1
  10. package/src/core/auth/expiry-monitor.ts +1 -1
  11. package/src/core/background/cron-spec.ts +1 -1
  12. package/src/core/background/cron.ts +1 -1
  13. package/src/core/background/heartbeat/agent.ts +1 -1
  14. package/src/core/background/triggers/command.ts +1 -1
  15. package/src/core/background/triggers/exit.ts +1 -1
  16. package/src/core/background/triggers/output.ts +1 -1
  17. package/src/core/background/triggers/resume.ts +1 -1
  18. package/src/core/background/triggers/spawn.ts +1 -1
  19. package/src/core/{doctor.ts → doctor/index.ts} +11 -11
  20. package/src/core/engine/gateway-actions/cron.ts +2 -2
  21. package/src/core/engine/gateway-actions/goals.ts +2 -2
  22. package/src/core/engine/gateway-actions/scripts.ts +1 -1
  23. package/src/core/engine/gateway-actions/skills.ts +1 -1
  24. package/src/core/engine/gateway-actions/triggers.ts +2 -2
  25. package/src/core/engine/gateway-actions/{shared.ts → validation.ts} +2 -2
  26. package/src/core/{notify.ts → frontend-runtime/admin-notify.ts} +1 -1
  27. package/src/core/plugin/native-runtimes.ts +1 -1
  28. package/src/core/prompt/assemble.ts +2 -2
  29. package/src/core/scripts/runner.ts +1 -1
  30. package/src/core/soul/forgetting.ts +2 -2
  31. package/src/core/soul/salience.ts +8 -4
  32. package/src/frontend/discord/actions/{shared.ts → buttons.ts} +3 -56
  33. package/src/frontend/discord/actions/channels.ts +34 -0
  34. package/src/frontend/discord/actions/chat-info.ts +1 -1
  35. package/src/frontend/discord/actions/index.ts +1 -1
  36. package/src/frontend/discord/actions/media.ts +1 -1
  37. package/src/frontend/discord/actions/messaging.ts +4 -2
  38. package/src/frontend/discord/actions/try-action.ts +24 -0
  39. package/src/frontend/discord/admin.ts +2 -2
  40. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  41. package/src/frontend/discord/callbacks/modals.ts +1 -1
  42. package/src/frontend/discord/commands/admin.ts +3 -3
  43. package/src/frontend/discord/commands/info.ts +2 -2
  44. package/src/frontend/discord/commands/{shared.ts → interaction.ts} +2 -1
  45. package/src/frontend/discord/commands/router.ts +1 -1
  46. package/src/frontend/discord/commands/session.ts +2 -2
  47. package/src/frontend/discord/commands/settings.ts +2 -2
  48. package/src/frontend/discord/{helpers.ts → render.ts} +3 -2
  49. package/src/frontend/native/extensions.ts +1 -1
  50. package/src/frontend/teams/chat-discovery.ts +69 -0
  51. package/src/frontend/teams/commands.ts +141 -0
  52. package/src/frontend/teams/index.ts +24 -350
  53. package/src/frontend/teams/outbound.ts +44 -0
  54. package/src/frontend/teams/poll.ts +101 -0
  55. package/src/frontend/teams/runtime.ts +51 -0
  56. package/src/frontend/teams/turn.ts +97 -0
  57. package/src/frontend/telegram/actions/index.ts +1 -1
  58. package/src/frontend/telegram/actions/messaging.ts +1 -1
  59. package/src/frontend/telegram/actions/moderation/chat.ts +63 -0
  60. package/src/frontend/telegram/actions/moderation/index.ts +74 -0
  61. package/src/frontend/telegram/actions/moderation/members.ts +77 -0
  62. package/src/frontend/telegram/actions/moderation/permissions.ts +69 -0
  63. package/src/frontend/telegram/actions/moderation/topics.ts +48 -0
  64. package/src/frontend/telegram/actions/moderation/types.ts +16 -0
  65. package/src/frontend/telegram/actions/shared.ts +4 -4
  66. package/src/frontend/telegram/admin/background.ts +66 -0
  67. package/src/frontend/telegram/admin/chunked-reply.ts +20 -0
  68. package/src/frontend/telegram/admin/health.ts +81 -0
  69. package/src/frontend/telegram/admin/sessions.ts +105 -0
  70. package/src/frontend/telegram/admin.ts +56 -261
  71. package/src/frontend/telegram/callbacks/model/backend.ts +156 -0
  72. package/src/frontend/telegram/callbacks/model/control.ts +29 -0
  73. package/src/frontend/telegram/callbacks/model/nav.ts +54 -0
  74. package/src/frontend/telegram/callbacks/model/select.ts +100 -0
  75. package/src/frontend/telegram/callbacks/model/types.ts +39 -0
  76. package/src/frontend/telegram/callbacks/model/views.ts +116 -0
  77. package/src/frontend/telegram/callbacks/model.ts +61 -331
  78. package/src/frontend/telegram/commands/admin.ts +108 -77
  79. package/src/frontend/telegram/commands/settings.ts +65 -43
  80. package/src/frontend/telegram/commands/whatsapp-pairing.ts +1 -1
  81. package/src/frontend/telegram/helpers/diagnostics.ts +1 -1
  82. package/src/frontend/telegram/middleware.ts +166 -201
  83. package/src/frontend/telegram/sticker-library.ts +2 -2
  84. package/src/frontend/terminal/builtins/context.ts +184 -0
  85. package/src/frontend/terminal/builtins/help.ts +35 -0
  86. package/src/frontend/terminal/builtins/index.ts +23 -0
  87. package/src/frontend/terminal/builtins/model.ts +198 -0
  88. package/src/frontend/terminal/builtins/session.ts +101 -0
  89. package/src/frontend/terminal/builtins/status.ts +202 -0
  90. package/src/frontend/terminal/command-registry.ts +80 -0
  91. package/src/frontend/terminal/commands.ts +11 -674
  92. package/src/frontend/terminal/input/history.ts +64 -0
  93. package/src/frontend/terminal/input/keys.ts +143 -0
  94. package/src/frontend/terminal/input/parts.ts +33 -0
  95. package/src/frontend/terminal/input/paste.ts +53 -0
  96. package/src/frontend/terminal/input/render.ts +42 -0
  97. package/src/frontend/terminal/input/state.ts +60 -0
  98. package/src/frontend/terminal/input.ts +27 -342
  99. package/src/frontend/whatsapp/actions/account.ts +3 -6
  100. package/src/frontend/whatsapp/actions/chat-info.ts +2 -1
  101. package/src/frontend/whatsapp/actions/coerce.ts +30 -0
  102. package/src/frontend/whatsapp/actions/history.ts +1 -1
  103. package/src/frontend/whatsapp/actions/media-upload.ts +82 -0
  104. package/src/frontend/whatsapp/actions/media.ts +3 -8
  105. package/src/frontend/whatsapp/actions/messaging.ts +2 -7
  106. package/src/frontend/whatsapp/actions/moderation.ts +2 -1
  107. package/src/frontend/whatsapp/actions/{shared.ts → send.ts} +4 -124
  108. package/src/frontend/whatsapp/actions/try-action.ts +24 -0
  109. package/src/frontend/whatsapp/commands.ts +1 -1
  110. package/src/frontend/whatsapp/connection.ts +1 -1
  111. package/src/frontend/whatsapp/inbound.ts +1 -1
  112. package/src/frontend/whatsapp/index.ts +2 -2
  113. package/src/frontend/whatsapp/pairing-service.ts +1 -1
  114. package/src/plugins/github/provision.ts +1 -1
  115. package/src/plugins/mempalace/provision.ts +1 -1
  116. package/src/plugins/playwright/provision.ts +1 -1
  117. package/src/storage/repositories/cron-repo.ts +1 -1
  118. package/src/storage/repositories/goals-repo.ts +1 -1
  119. package/src/storage/repositories/scripts-repo.ts +1 -1
  120. package/src/storage/repositories/triggers-repo.ts +1 -1
  121. package/src/storage/sql/schema.sql +1 -1
  122. package/src/storage/sql/statements.generated.ts +2 -2
  123. package/src/storage/sql/triggers.sql +1 -1
  124. package/src/frontend/telegram/actions/moderation.ts +0 -255
  125. /package/src/core/{doctor-types.ts → doctor/types.ts} +0 -0
  126. /package/src/core/{pairing-broker.ts → frontend-runtime/pairing-broker.ts} +0 -0
  127. /package/src/storage/{cron-store.ts → cron.ts} +0 -0
  128. /package/src/storage/{goal-store.ts → goals.ts} +0 -0
  129. /package/src/storage/{scheduled-store.ts → scheduled.ts} +0 -0
  130. /package/src/storage/{script-store.ts → scripts.ts} +0 -0
  131. /package/src/storage/{skill-store.ts → skills.ts} +0 -0
  132. /package/src/storage/{sticker-store.ts → stickers.ts} +0 -0
  133. /package/src/storage/{trigger-store.ts → triggers.ts} +0 -0
@@ -4,9 +4,18 @@
4
4
  * Input is a list of parts: text segments and collapsed paste blocks.
5
5
  * You can type, paste, type more, paste again. Backspace removes from the end.
6
6
  * Enter submits everything. Ctrl+U clears all. Up/Down walk prompt history.
7
+ *
8
+ * State lives in `input/state.ts`; paste, keys and drawing are each their own
9
+ * module under `input/`. This file is the wiring.
7
10
  */
8
11
 
9
- import pc from "picocolors";
12
+ import { createInputState } from "./input/state.js";
13
+ import { consumePasteChunk } from "./input/paste.js";
14
+ import { handleKeyChunk } from "./input/keys.js";
15
+ import { redraw } from "./input/render.js";
16
+
17
+ export type { InputPart } from "./input/parts.js";
18
+ export { PromptHistory } from "./input/history.js";
10
19
 
11
20
  // ── Types ────────────────────────────────────────────────────────────────────
12
21
 
@@ -19,232 +28,10 @@ export type InputHandler = {
19
28
  resume(): void;
20
29
  };
21
30
 
22
- export type InputPart =
23
- { type: "text"; content: string } | { type: "paste"; content: string };
24
- type TextPart = Extract<InputPart, { type: "text" }>;
25
- type PastePart = Extract<InputPart, { type: "paste" }>;
26
-
27
- // ── Constants ────────────────────────────────────────────────────────────────
28
-
29
- const PASTE_COLLAPSE_LINES = 3;
30
- const PASTE_COLLAPSE_CHARS = 150;
31
- const PASTE_START = "\x1b[200~";
32
- const PASTE_END = "\x1b[201~";
33
- const MAX_HISTORY_ITEMS = 100;
34
-
35
- function cloneParts(parts: readonly InputPart[]): InputPart[] {
36
- return parts.map((part) => ({ ...part }));
37
- }
38
-
39
- function partsText(parts: readonly InputPart[]): string {
40
- return parts
41
- .map((part) => part.content)
42
- .join("\n")
43
- .trim();
44
- }
45
-
46
- /**
47
- * In-process prompt history. Keeping this out of the persistent stores avoids
48
- * creating a second plaintext transcript while still giving terminal users
49
- * the expected Up/Down workflow.
50
- */
51
- export class PromptHistory {
52
- private readonly entries: InputPart[][] = [];
53
- private cursor = 0;
54
- private draft: InputPart[] | null = null;
55
-
56
- constructor(private readonly maxItems = MAX_HISTORY_ITEMS) {}
57
-
58
- add(parts: readonly InputPart[]): void {
59
- const text = partsText(parts);
60
- if (!text) return;
61
-
62
- const latest = this.entries.at(-1);
63
- if (!latest || partsText(latest) !== text) {
64
- this.entries.push(cloneParts(parts));
65
- if (this.entries.length > this.maxItems) this.entries.shift();
66
- }
67
- this.resetNavigation();
68
- }
69
-
70
- move(
71
- direction: "previous" | "next",
72
- current: readonly InputPart[],
73
- ): InputPart[] | undefined {
74
- if (this.entries.length === 0) return undefined;
75
-
76
- if (direction === "previous") {
77
- if (this.cursor === this.entries.length) {
78
- this.draft = cloneParts(current);
79
- }
80
- if (this.cursor > 0) this.cursor--;
81
- return cloneParts(this.entries[this.cursor]!);
82
- }
83
-
84
- if (this.cursor === this.entries.length) return undefined;
85
- if (this.cursor < this.entries.length - 1) {
86
- this.cursor++;
87
- return cloneParts(this.entries[this.cursor]!);
88
- }
89
-
90
- this.cursor = this.entries.length;
91
- return cloneParts(this.draft ?? [{ type: "text", content: "" }]);
92
- }
93
-
94
- /** Preserve an edited recalled prompt as the current draft. */
95
- detach(parts: readonly InputPart[]): void {
96
- if (this.cursor === this.entries.length) return;
97
- this.cursor = this.entries.length;
98
- this.draft = cloneParts(parts);
99
- }
100
-
101
- resetNavigation(): void {
102
- this.cursor = this.entries.length;
103
- this.draft = null;
104
- }
105
- }
106
-
107
31
  // ── Factory ──────────────────────────────────────────────────────────────────
108
32
 
109
33
  export function createInput(promptStr: string): InputHandler {
110
- let lineCallback: ((text: string) => void) | null = null;
111
- let pendingResolve: ((value: string) => void) | null = null;
112
- let paused = false;
113
-
114
- // Input is an ordered list of parts
115
- let parts: InputPart[] = [{ type: "text", content: "" }];
116
- const history = new PromptHistory();
117
-
118
- // Bracketed paste accumulation
119
- let inPaste = false;
120
- let pasteAccum = "";
121
-
122
- // ── Helpers ──
123
-
124
- function lastPart(): InputPart {
125
- return parts[parts.length - 1]!;
126
- }
127
-
128
- /** Ensure the last part is a text part (for typing into). */
129
- function ensureTrailingText(): TextPart {
130
- const last = lastPart();
131
- if (last.type === "text") return last;
132
- const t: TextPart = { type: "text", content: "" };
133
- parts.push(t);
134
- return t;
135
- }
136
-
137
- function pasteTag(p: PastePart): string {
138
- const lines = p.content.split("\n").length;
139
- return lines > 1
140
- ? `[Pasted ~${lines} lines]`
141
- : `[Pasted ${p.content.length} chars]`;
142
- }
143
-
144
- // ── Drawing ──
145
- // Track how many visual rows the last render occupied.
146
- // Move up to the first row, clear to end of screen, rewrite.
147
-
148
- let prevRows = 1;
149
-
150
- function redraw(): void {
151
- const cols = process.stdout.columns || 80;
152
-
153
- // Build display string and measure visible length (strip ANSI)
154
- let display = promptStr;
155
- let visLen = 4; // " ❯ " = 4 visible chars
156
- for (const p of parts) {
157
- if (p.type === "text") {
158
- display += p.content;
159
- visLen += p.content.length;
160
- } else {
161
- const tag = pasteTag(p);
162
- display += pc.dim(tag);
163
- visLen += tag.length;
164
- }
165
- }
166
-
167
- // Move cursor to start of the previous render, clear to end of screen
168
- if (prevRows > 1) {
169
- process.stdout.write(`\x1b[${prevRows - 1}A`); // move up
170
- }
171
- process.stdout.write(`\r\x1b[J${display}\x1b[?25h`); // col 0, clear to EOS, write, show cursor
172
-
173
- prevRows = Math.max(1, Math.ceil(visLen / cols));
174
- }
175
-
176
- function getFullText(): string {
177
- return partsText(parts);
178
- }
179
-
180
- function clear(): void {
181
- parts = [{ type: "text", content: "" }];
182
- history.resetNavigation();
183
- }
184
-
185
- function submit(): void {
186
- const text = getFullText();
187
- if (!pendingResolve) history.add(parts);
188
- clear();
189
- process.stdout.write("\n");
190
-
191
- if (pendingResolve) {
192
- const resolve = pendingResolve;
193
- pendingResolve = null;
194
- resolve(text);
195
- return;
196
- }
197
- if (lineCallback) lineCallback(text);
198
- }
199
-
200
- function handlePasteComplete(text: string): void {
201
- const lineCount = text.split("\n").length;
202
- if (
203
- lineCount >= PASTE_COLLAPSE_LINES ||
204
- text.length > PASTE_COLLAPSE_CHARS
205
- ) {
206
- // Collapse into a paste part
207
- parts.push({ type: "paste", content: text });
208
- } else {
209
- // Short paste — inline into current text part
210
- ensureTrailingText().content += text.replace(/\n/g, " ");
211
- }
212
- history.detach(parts);
213
- redraw();
214
- }
215
-
216
- function handleBackspace(): void {
217
- const last = lastPart();
218
- if (last.type === "text" && last.content.length > 0) {
219
- // Delete last char from text
220
- last.content = last.content.slice(0, -1);
221
- } else if (
222
- last.type === "text" &&
223
- last.content === "" &&
224
- parts.length > 1
225
- ) {
226
- // Empty trailing text — remove it, then remove the paste before it
227
- parts.pop();
228
- parts.pop();
229
- // Ensure we always have at least one text part
230
- if (parts.length === 0) parts.push({ type: "text", content: "" });
231
- ensureTrailingText();
232
- } else if (last.type === "paste") {
233
- // Remove the paste block
234
- parts.pop();
235
- if (parts.length === 0) parts.push({ type: "text", content: "" });
236
- ensureTrailingText();
237
- }
238
- history.detach(parts);
239
- redraw();
240
- }
241
-
242
- function navigateHistory(direction: "previous" | "next"): void {
243
- const recalled = history.move(direction, parts);
244
- if (!recalled) return;
245
- parts = recalled;
246
- redraw();
247
- }
34
+ const state = createInputState(promptStr);
248
35
 
249
36
  // ── Raw mode ──
250
37
 
@@ -256,140 +43,38 @@ export function createInput(promptStr: string): InputHandler {
256
43
  process.stdout.write("\x1b[?2004h");
257
44
 
258
45
  process.stdin.on("data", (chunk: string) => {
259
- if (paused) return;
260
-
261
- // ── Bracketed paste ──
262
- if (chunk.includes(PASTE_START)) {
263
- inPaste = true;
264
- pasteAccum = chunk.split(PASTE_START).slice(1).join(PASTE_START);
265
- if (pasteAccum.includes(PASTE_END)) {
266
- inPaste = false;
267
- handlePasteComplete(pasteAccum.split(PASTE_END)[0]!);
268
- pasteAccum = "";
269
- }
270
- return;
271
- }
272
- if (inPaste) {
273
- if (chunk.includes(PASTE_END)) {
274
- pasteAccum += chunk.split(PASTE_END)[0]!;
275
- inPaste = false;
276
- handlePasteComplete(pasteAccum);
277
- pasteAccum = "";
278
- } else {
279
- pasteAccum += chunk;
280
- }
281
- return;
282
- }
283
-
284
- // ── Normal input ──
285
- for (let i = 0; i < chunk.length; i++) {
286
- const ch = chunk[i]!;
287
- const code = chunk.charCodeAt(i);
288
-
289
- if (code === 0x03) {
290
- // Ctrl+C
291
- process.stdout.write("\n\x1b[?2004l");
292
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
293
- process.exit(0);
294
- }
295
-
296
- if (code === 0x15) {
297
- // Ctrl+U
298
- clear();
299
- redraw();
300
- continue;
301
- }
302
-
303
- if (code === 0x0d || code === 0x0a) {
304
- // Enter
305
- if (getFullText()) {
306
- submit();
307
- } else {
308
- process.stdout.write("\n");
309
- redraw();
310
- }
311
- continue;
312
- }
313
-
314
- if (code === 0x7f || code === 0x08) {
315
- // Backspace
316
- handleBackspace();
317
- continue;
318
- }
319
-
320
- if (code === 0x1b) {
321
- if (i + 1 < chunk.length && chunk[i + 1] === "[") {
322
- // CSI escape sequence. Up/Down navigate history; the rest are
323
- // ignored until cursor editing is implemented.
324
- let end = i + 2;
325
- while (end < chunk.length && chunk.charCodeAt(end) < 0x40) end++;
326
- const final = chunk[end];
327
- if (final === "A") navigateHistory("previous");
328
- if (final === "B") navigateHistory("next");
329
- i = end;
330
- } else if (
331
- i + 2 < chunk.length &&
332
- chunk[i + 1] === "O" &&
333
- (chunk[i + 2] === "A" || chunk[i + 2] === "B")
334
- ) {
335
- // Some terminals use application-cursor sequences (ESC O A/B).
336
- navigateHistory(chunk[i + 2] === "A" ? "previous" : "next");
337
- i += 2;
338
- } else if (pendingResolve) {
339
- // Bare Escape during waitForInput — cancel
340
- const resolve = pendingResolve;
341
- pendingResolve = null;
342
- clear();
343
- process.stdout.write("\n");
344
- resolve("");
345
- }
346
- continue;
347
- }
348
-
349
- if (code === 0x09) {
350
- // Tab
351
- ensureTrailingText().content += " ";
352
- history.detach(parts);
353
- redraw();
354
- continue;
355
- }
356
-
357
- if (code < 0x20) continue;
358
-
359
- // Printable char
360
- ensureTrailingText().content += ch;
361
- history.detach(parts);
362
- redraw();
363
- }
46
+ if (state.paused) return;
47
+ if (consumePasteChunk(state, chunk)) return;
48
+ handleKeyChunk(state, chunk);
364
49
  });
365
50
 
366
51
  return {
367
52
  onLine(callback) {
368
- lineCallback = callback;
53
+ state.lineCallback = callback;
369
54
  },
370
55
  prompt() {
371
- paused = false;
372
- prevRows = 1; // fresh prompt = 1 row
373
- redraw();
56
+ state.paused = false;
57
+ state.prevRows = 1; // fresh prompt = 1 row
58
+ redraw(state);
374
59
  },
375
60
  waitForInput(): Promise<string> {
376
61
  return new Promise((resolve) => {
377
- pendingResolve = resolve;
378
- paused = false;
379
- prevRows = 1;
380
- redraw();
62
+ state.pendingResolve = resolve;
63
+ state.paused = false;
64
+ state.prevRows = 1;
65
+ redraw(state);
381
66
  });
382
67
  },
383
68
  pause() {
384
- paused = true;
69
+ state.paused = true;
385
70
  },
386
71
  resume() {
387
- paused = false;
72
+ state.paused = false;
388
73
  },
389
74
  close() {
390
- if (pendingResolve) {
391
- const resolve = pendingResolve;
392
- pendingResolve = null;
75
+ if (state.pendingResolve) {
76
+ const resolve = state.pendingResolve;
77
+ state.pendingResolve = null;
393
78
  resolve("");
394
79
  }
395
80
  process.stdout.write("\x1b[?2004l");
@@ -16,12 +16,9 @@
16
16
 
17
17
  import { jidNormalizedUser, type WASocket } from "baileys";
18
18
  import type { ActionResult } from "../../../core/types.js";
19
- import {
20
- readStatusText,
21
- resolveMediaUpload,
22
- toUserJid,
23
- tryAction,
24
- } from "./shared.js";
19
+ import { tryAction } from "./try-action.js";
20
+ import { resolveMediaUpload } from "./media-upload.js";
21
+ import { readStatusText, toUserJid } from "./coerce.js";
25
22
  import type { WhatsAppActionContext, WhatsAppActionHandlers } from "./types.js";
26
23
 
27
24
  /** The account's own JID, device suffix stripped. */
@@ -7,7 +7,8 @@
7
7
  * retrieval (get_message_by_id, download_media) lives in history.ts.
8
8
  */
9
9
 
10
- import { readStatusText, toUserJid, tryAction } from "./shared.js";
10
+ import { tryAction } from "./try-action.js";
11
+ import { readStatusText, toUserJid } from "./coerce.js";
11
12
  import type { WhatsAppActionHandlers } from "./types.js";
12
13
 
13
14
  /** Render one participant the way the other frontends render members. */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shape coercion for the member-facing WhatsApp actions: model-supplied user
3
+ * references → JIDs, and Baileys' several `fetchStatus` result shapes → text.
4
+ */
5
+
6
+ /**
7
+ * Coerce a user reference the model supplied — bare number, JID, or the
8
+ * numeric id from a member listing — into a WhatsApp user JID.
9
+ */
10
+ export function toUserJid(value: unknown): string | null {
11
+ const raw = String(value ?? "").trim();
12
+ if (!raw) return null;
13
+ if (raw.includes("@")) return raw;
14
+ const digits = raw.replace(/[^0-9]/g, "");
15
+ return digits ? `${digits}@s.whatsapp.net` : null;
16
+ }
17
+
18
+ /**
19
+ * Pull the about text out of a `fetchStatus` result. Baileys has
20
+ * returned both a bare object and a one-element array across versions,
21
+ * and the payload itself nests `status` either as a string or as
22
+ * `{ status }` — accept all four shapes rather than guess one.
23
+ */
24
+ export function readStatusText(result: unknown): string | undefined {
25
+ const entry = (Array.isArray(result) ? result[0] : result) as
26
+ { status?: string | { status?: string } } | undefined;
27
+ return typeof entry?.status === "string"
28
+ ? entry.status
29
+ : entry?.status?.status;
30
+ }
@@ -19,7 +19,7 @@ import {
19
19
  } from "../../../storage/history.js";
20
20
  import { saveInboundMedia } from "../media-store.js";
21
21
  import { lookupMessage, resolveKey } from "../message-store.js";
22
- import { tryAction } from "./shared.js";
22
+ import { tryAction } from "./try-action.js";
23
23
  import type { WhatsAppActionHandlers } from "./types.js";
24
24
 
25
25
  function toMsgId(raw: unknown): number {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Media-source resolution for the WhatsApp send actions: workspace path or
3
+ * public URL → something Baileys can upload, plus the mimetype guess the
4
+ * document/audio paths need.
5
+ */
6
+
7
+ import { existsSync, statSync } from "node:fs";
8
+ import { basename, extname } from "node:path";
9
+ import { expandFsPath } from "../../../util/fs-path.js";
10
+
11
+ /** WhatsApp's own ceiling for a media upload. */
12
+ const MAX_MEDIA_BYTES = 64 * 1024 * 1024;
13
+
14
+ /** Baileys accepts a Buffer, a stream, or `{ url }` for local paths and HTTP. */
15
+ export type MediaUpload = { url: string };
16
+
17
+ /**
18
+ * Resolve one media input to something Baileys can upload. Two sources:
19
+ * a public URL (WhatsApp's uploader fetches it) or a workspace file path
20
+ * (streamed from disk). `file_id` is a Telegram concept with no WhatsApp
21
+ * equivalent — say so instead of failing obscurely.
22
+ */
23
+ export function resolveMediaUpload(
24
+ src: { file_path?: unknown; url?: unknown; file_id?: unknown },
25
+ label: string,
26
+ ): { media: MediaUpload; fileName: string } | { error: string } {
27
+ if (src.url) {
28
+ const url = String(src.url);
29
+ return {
30
+ media: { url },
31
+ fileName: basename(new URL(url).pathname) || "file",
32
+ };
33
+ }
34
+ if (src.file_id) {
35
+ return {
36
+ error:
37
+ `${label}: WhatsApp has no file_id — re-send by url (public) or ` +
38
+ `file_path (workspace file)`,
39
+ };
40
+ }
41
+ if (!src.file_path) {
42
+ return {
43
+ error: `${label}: provide file_path (workspace file) or url (public)`,
44
+ };
45
+ }
46
+ const filePath = expandFsPath(String(src.file_path));
47
+ if (!existsSync(filePath)) {
48
+ return {
49
+ error: `File not found: ${filePath} — check the workspace path, or send by url instead`,
50
+ };
51
+ }
52
+ if (statSync(filePath).size > MAX_MEDIA_BYTES) {
53
+ return { error: `${label}: file exceeds WhatsApp's 64MB limit` };
54
+ }
55
+ return { media: { url: filePath }, fileName: basename(filePath) };
56
+ }
57
+
58
+ /** Extension → mimetype for the document/audio paths that require one. */
59
+ const MIME_BY_EXT: Record<string, string> = {
60
+ ".pdf": "application/pdf",
61
+ ".txt": "text/plain",
62
+ ".md": "text/markdown",
63
+ ".json": "application/json",
64
+ ".csv": "text/csv",
65
+ ".zip": "application/zip",
66
+ ".png": "image/png",
67
+ ".jpg": "image/jpeg",
68
+ ".jpeg": "image/jpeg",
69
+ ".gif": "image/gif",
70
+ ".webp": "image/webp",
71
+ ".mp4": "video/mp4",
72
+ ".mov": "video/quicktime",
73
+ ".mp3": "audio/mpeg",
74
+ ".m4a": "audio/mp4",
75
+ ".ogg": "audio/ogg",
76
+ ".opus": "audio/ogg; codecs=opus",
77
+ ".wav": "audio/wav",
78
+ };
79
+
80
+ export function guessMimetype(fileName: string, fallback: string): string {
81
+ return MIME_BY_EXT[extname(fileName).toLowerCase()] ?? fallback;
82
+ }
@@ -10,14 +10,9 @@
10
10
 
11
11
  import type { AnyMessageContent } from "baileys";
12
12
  import { toWhatsAppText } from "../formatting.js";
13
- import {
14
- guessMimetype,
15
- resolveMediaUpload,
16
- resolveQuoted,
17
- sendContent,
18
- sendText,
19
- tryAction,
20
- } from "./shared.js";
13
+ import { tryAction } from "./try-action.js";
14
+ import { guessMimetype, resolveMediaUpload } from "./media-upload.js";
15
+ import { resolveQuoted, sendContent, sendText } from "./send.js";
21
16
  import type { WhatsAppActionHandlers } from "./types.js";
22
17
 
23
18
  /** Optional caption, translated to WhatsApp's dialect. */
@@ -13,13 +13,8 @@ import { log } from "../../../util/log.js";
13
13
  import { toWhatsAppText } from "../formatting.js";
14
14
  import { lookupMessage, resolveKey } from "../message-store.js";
15
15
  import { recordPin, listPins, forgetPin } from "../pins.js";
16
- import {
17
- resolveQuoted,
18
- sendContent,
19
- sendText,
20
- tryAction,
21
- boundedSend,
22
- } from "./shared.js";
16
+ import { tryAction } from "./try-action.js";
17
+ import { resolveQuoted, sendContent, sendText, boundedSend } from "./send.js";
23
18
  import type { WhatsAppActionHandlers } from "./types.js";
24
19
 
25
20
  /** WhatsApp keeps a pin for 24h, 7d, or 30d — no indefinite option. */
@@ -9,7 +9,8 @@
9
9
  * rather than retrying something the platform cannot do.
10
10
  */
11
11
 
12
- import { toUserJid, tryAction } from "./shared.js";
12
+ import { tryAction } from "./try-action.js";
13
+ import { toUserJid } from "./coerce.js";
13
14
  import type { WhatsAppActionHandlers } from "./types.js";
14
15
 
15
16
  /** Ops that act on one member. */