@intentface/latch-channels 0.9.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/dist/ask.d.ts +69 -0
  4. package/dist/ask.d.ts.map +1 -0
  5. package/dist/ask.js +94 -0
  6. package/dist/ask.js.map +1 -0
  7. package/dist/commands.d.ts +41 -0
  8. package/dist/commands.d.ts.map +1 -0
  9. package/dist/commands.js +54 -0
  10. package/dist/commands.js.map +1 -0
  11. package/dist/index.d.ts +19 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +19 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/kernel.d.ts +207 -0
  16. package/dist/kernel.d.ts.map +1 -0
  17. package/dist/kernel.js +883 -0
  18. package/dist/kernel.js.map +1 -0
  19. package/dist/render.d.ts +43 -0
  20. package/dist/render.d.ts.map +1 -0
  21. package/dist/render.js +192 -0
  22. package/dist/render.js.map +1 -0
  23. package/dist/slack/api.d.ts +207 -0
  24. package/dist/slack/api.d.ts.map +1 -0
  25. package/dist/slack/api.js +249 -0
  26. package/dist/slack/api.js.map +1 -0
  27. package/dist/slack/blocks.d.ts +36 -0
  28. package/dist/slack/blocks.d.ts.map +1 -0
  29. package/dist/slack/blocks.js +161 -0
  30. package/dist/slack/blocks.js.map +1 -0
  31. package/dist/slack/ids.d.ts +40 -0
  32. package/dist/slack/ids.d.ts.map +1 -0
  33. package/dist/slack/ids.js +47 -0
  34. package/dist/slack/ids.js.map +1 -0
  35. package/dist/slack/index.d.ts +24 -0
  36. package/dist/slack/index.d.ts.map +1 -0
  37. package/dist/slack/index.js +18 -0
  38. package/dist/slack/index.js.map +1 -0
  39. package/dist/slack/manifest.d.ts +23 -0
  40. package/dist/slack/manifest.d.ts.map +1 -0
  41. package/dist/slack/manifest.js +102 -0
  42. package/dist/slack/manifest.js.map +1 -0
  43. package/dist/slack/normalize.d.ts +35 -0
  44. package/dist/slack/normalize.d.ts.map +1 -0
  45. package/dist/slack/normalize.js +63 -0
  46. package/dist/slack/normalize.js.map +1 -0
  47. package/dist/slack/payloads.d.ts +36 -0
  48. package/dist/slack/payloads.d.ts.map +1 -0
  49. package/dist/slack/payloads.js +80 -0
  50. package/dist/slack/payloads.js.map +1 -0
  51. package/dist/slack/slackChannel.d.ts +152 -0
  52. package/dist/slack/slackChannel.d.ts.map +1 -0
  53. package/dist/slack/slackChannel.js +650 -0
  54. package/dist/slack/slackChannel.js.map +1 -0
  55. package/dist/slack/stream.d.ts +88 -0
  56. package/dist/slack/stream.d.ts.map +1 -0
  57. package/dist/slack/stream.js +418 -0
  58. package/dist/slack/stream.js.map +1 -0
  59. package/dist/slack/verify.d.ts +20 -0
  60. package/dist/slack/verify.d.ts.map +1 -0
  61. package/dist/slack/verify.js +32 -0
  62. package/dist/slack/verify.js.map +1 -0
  63. package/dist/telegram/api.d.ts +110 -0
  64. package/dist/telegram/api.d.ts.map +1 -0
  65. package/dist/telegram/api.js +170 -0
  66. package/dist/telegram/api.js.map +1 -0
  67. package/dist/telegram/format.d.ts +26 -0
  68. package/dist/telegram/format.d.ts.map +1 -0
  69. package/dist/telegram/format.js +56 -0
  70. package/dist/telegram/format.js.map +1 -0
  71. package/dist/telegram/html.d.ts +11 -0
  72. package/dist/telegram/html.d.ts.map +1 -0
  73. package/dist/telegram/html.js +35 -0
  74. package/dist/telegram/html.js.map +1 -0
  75. package/dist/telegram/index.d.ts +15 -0
  76. package/dist/telegram/index.d.ts.map +1 -0
  77. package/dist/telegram/index.js +15 -0
  78. package/dist/telegram/index.js.map +1 -0
  79. package/dist/telegram/stream.d.ts +59 -0
  80. package/dist/telegram/stream.d.ts.map +1 -0
  81. package/dist/telegram/stream.js +185 -0
  82. package/dist/telegram/stream.js.map +1 -0
  83. package/dist/telegram/telegramChannel.d.ts +136 -0
  84. package/dist/telegram/telegramChannel.d.ts.map +1 -0
  85. package/dist/telegram/telegramChannel.js +277 -0
  86. package/dist/telegram/telegramChannel.js.map +1 -0
  87. package/dist/telegram/verify.d.ts +7 -0
  88. package/dist/telegram/verify.d.ts.map +1 -0
  89. package/dist/telegram/verify.js +14 -0
  90. package/dist/telegram/verify.js.map +1 -0
  91. package/dist/types.d.ts +240 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +12 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/whatsapp/api.d.ts +59 -0
  96. package/dist/whatsapp/api.d.ts.map +1 -0
  97. package/dist/whatsapp/api.js +154 -0
  98. package/dist/whatsapp/api.js.map +1 -0
  99. package/dist/whatsapp/index.d.ts +13 -0
  100. package/dist/whatsapp/index.d.ts.map +1 -0
  101. package/dist/whatsapp/index.js +13 -0
  102. package/dist/whatsapp/index.js.map +1 -0
  103. package/dist/whatsapp/verify.d.ts +17 -0
  104. package/dist/whatsapp/verify.d.ts.map +1 -0
  105. package/dist/whatsapp/verify.js +32 -0
  106. package/dist/whatsapp/verify.js.map +1 -0
  107. package/dist/whatsapp/whatsappChannel.d.ts +104 -0
  108. package/dist/whatsapp/whatsappChannel.d.ts.map +1 -0
  109. package/dist/whatsapp/whatsappChannel.js +217 -0
  110. package/dist/whatsapp/whatsappChannel.js.map +1 -0
  111. package/package.json +68 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Intentface
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @intentface/latch-channels
2
+
3
+ Bring a Latch agent to the places people talk — Slack, Telegram, WhatsApp. Reuses the core runtime, storage and durability (no separate state store); identity (platform user → Principal) and channel → agent binding stay in user-land, per Latch's isolation model.
4
+
5
+ ## What it does
6
+
7
+ - **Generic core (`.`)** — the platform-agnostic half:
8
+ - `consumeRun(response)` → `AsyncIterable<RenderEvent>` folded from the runtime's SSE `Response`; `pendingApprovals` / `pendingQuestion` read what a paused turn is waiting on from persisted history. Adding a platform is "translate `RenderEvent`s", never "re-parse the AI stream."
9
+ - `createTurnKernel(config, surface)` — everything a direct-message surface does between "a webhook arrived" and "the platform API was called", written once: dedupe, the identity gate (with stash + post-link replay), thread epochs (`/new`), commands, questionnaires, media folding (files inline, voice transcribed), the answer posted **once at finish** (split past the cap; a typing hint meanwhile), approvals and button taps back into the run. A `Surface` is pure protocol: send text / link / approval / question, edit or retire a card, download a file.
10
+ - `AuthResult`, the `lta:` / `ltq:` button-payload codecs, `splitText`.
11
+ - **Telegram (`./telegram`)** and **WhatsApp (`./whatsapp`)** — a webhook parser plus a `Surface` each, over the kernel. `telegramChannel(config)` / `whatsappChannel(config)` verify the webhook, ACK fast, and run the turn in the background (`waitUntil`).
12
+ - **Slack (`./slack`)** — `slackChannel(config)`: one agent's dedicated app; native message streaming with tool calls as task cards, initiator-owned threads, signed actor-bound approval buttons, atomic admission (`claimOnce`). One live run per chat comes from core's storage invariant on the shipped adapters; on a custom adapter without it, wire the optional `claimTurn` / `releaseTurn` lease — every adapter (kernel and Slack) honours it. Plus the per-agent app manifest generator, a minimal Web API client and hand-rolled Block Kit (no Chat SDK runtime dependency). Slack keeps its own engine because its surface is threads, not DMs.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { telegramChannel } from "@intentface/latch-channels/telegram";
18
+
19
+ const channel = telegramChannel<Principal>({
20
+ agent, botRef, botToken, webhookSecret,
21
+ runtime: { handleChat, applyApproval, applyToolResult, compactChat, loadHistory },
22
+ authorizeUser, seen, stashPending, askState, thread, waitUntil,
23
+ });
24
+ return channel.handleRequest(request); // one webhook route per bot
25
+ ```
26
+
27
+ A new DM platform: implement `Surface<User, Ref>` and a parser from your webhook body to `Inbound<User, Ref>`, then `createTurnKernel(config, surface).handleInbound(event)`.
package/dist/ask.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { AskSession, Question } from "./types.js";
2
+ /**
3
+ * The questionnaire state machine — pure, platform-free, and the single place
4
+ * the answer semantics live, so Telegram and WhatsApp can't drift from each
5
+ * other or from the web composer.
6
+ *
7
+ * A chat surface asks one question at a time, but `applyToolResult` closes the
8
+ * whole `askUser` call in one shot: answers accumulate in an `AskSession` and
9
+ * only the last question emits `completed`.
10
+ *
11
+ * The output is a flat `Record<questionText, answer>` — the exact shape the web
12
+ * composer submits (`apps/latch-platform/src/components/chat.tsx`), including
13
+ * joining multi-select picks with `", "`.
14
+ */
15
+ export type AskEvent =
16
+ /** An option button: commits a single-select question, toggles a multi-select one. */
17
+ {
18
+ kind: "pick";
19
+ option: number;
20
+ }
21
+ /** The "Done" button on a multi-select question. */
22
+ | {
23
+ kind: "done";
24
+ }
25
+ /** A typed message, taken as the answer to the question on screen. */
26
+ | {
27
+ kind: "typed";
28
+ text: string;
29
+ };
30
+ export type AskStep =
31
+ /** A multi-select tick changed; same question stays on screen. */
32
+ {
33
+ kind: "toggled";
34
+ selected: number[];
35
+ }
36
+ /** Question committed, more remain. */
37
+ | {
38
+ kind: "advanced";
39
+ cursor: number;
40
+ answers: Record<string, string>;
41
+ answered: {
42
+ question: string;
43
+ answer: string;
44
+ };
45
+ }
46
+ /** Last question committed — submit `output` via `applyToolResult`. */
47
+ | {
48
+ kind: "completed";
49
+ output: Record<string, string>;
50
+ answered: {
51
+ question: string;
52
+ answer: string;
53
+ };
54
+ }
55
+ /** Nothing to do (e.g. Done with nothing ticked); `reason` is user-facing. */
56
+ | {
57
+ kind: "rejected";
58
+ reason: string;
59
+ };
60
+ /**
61
+ * Interpret typed text as an answer to `q`. Tries, in order: an option label
62
+ * (case-insensitive), a 1-based option number (or list of them, which is how
63
+ * WhatsApp's numbered fallback answers), then the raw text — free-form answers
64
+ * are legitimate, the web composer allows them too.
65
+ */
66
+ export declare function resolveTypedAnswer(q: Question, text: string): string;
67
+ /** Apply one interaction to a session. Never mutates; returns what to do next. */
68
+ export declare function nextAskStep(session: AskSession, event: AskEvent): AskStep;
69
+ //# sourceMappingURL=ask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../src/ask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,QAAQ;AAClB,sFAAsF;AACpF;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAClC,oDAAoD;GAClD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB,sEAAsE;GACpE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpC,MAAM,MAAM,OAAO;AACjB,kEAAkE;AAChE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE;AACzC,uCAAuC;GACrC;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AACH,uEAAuE;GACrE;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AACH,8EAA8E;GAC5E;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyBzC;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAWpE;AAWD,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CA+BzE"}
package/dist/ask.js ADDED
@@ -0,0 +1,94 @@
1
+ const MULTI_JOIN = ", ";
2
+ /** Labels of the ticked options, always in OPTION order (not tap order). */
3
+ function selectedLabels(q, selected) {
4
+ return q.options
5
+ .map((o, i) => (selected.includes(i) ? o.label : undefined))
6
+ .filter((l) => l !== undefined);
7
+ }
8
+ /** Parse "1,3" / "1 3" into option indices; null unless ALL tokens are valid. */
9
+ function parseIndexList(q, text) {
10
+ const tokens = text.split(/[,\s]+/).filter(Boolean);
11
+ if (!tokens.length)
12
+ return null;
13
+ const out = [];
14
+ for (const t of tokens) {
15
+ if (!/^\d+$/.test(t))
16
+ return null;
17
+ const i = Number(t) - 1; // 1-based for humans
18
+ if (i < 0 || i >= q.options.length)
19
+ return null;
20
+ if (!out.includes(i))
21
+ out.push(i);
22
+ }
23
+ return out.sort((a, b) => a - b);
24
+ }
25
+ /**
26
+ * Interpret typed text as an answer to `q`. Tries, in order: an option label
27
+ * (case-insensitive), a 1-based option number (or list of them, which is how
28
+ * WhatsApp's numbered fallback answers), then the raw text — free-form answers
29
+ * are legitimate, the web composer allows them too.
30
+ */
31
+ export function resolveTypedAnswer(q, text) {
32
+ const trimmed = text.trim();
33
+ const byLabel = q.options.find((o) => o.label.toLowerCase() === trimmed.toLowerCase());
34
+ if (byLabel)
35
+ return byLabel.label;
36
+ const indices = parseIndexList(q, trimmed);
37
+ if (indices) {
38
+ const labels = indices.map((i) => q.options[i].label);
39
+ // A single index answers either kind; a list only makes sense for multi-select.
40
+ if (labels.length === 1 || q.multiSelect)
41
+ return labels.join(MULTI_JOIN);
42
+ }
43
+ return trimmed;
44
+ }
45
+ /** Commit `answer` for the current question and move on (or finish). */
46
+ function commit(session, q, answer) {
47
+ const answers = { ...session.answers, [q.question]: answer };
48
+ const answered = { question: q.question, answer };
49
+ const next = session.cursor + 1;
50
+ if (next >= session.questions.length)
51
+ return { kind: "completed", output: answers, answered };
52
+ return { kind: "advanced", cursor: next, answers, answered };
53
+ }
54
+ /** Apply one interaction to a session. Never mutates; returns what to do next. */
55
+ export function nextAskStep(session, event) {
56
+ const q = session.questions[session.cursor];
57
+ if (!q)
58
+ return { kind: "rejected", reason: "This questionnaire is already complete." };
59
+ switch (event.kind) {
60
+ case "pick": {
61
+ const option = q.options[event.option];
62
+ if (!option)
63
+ return { kind: "rejected", reason: "That option is no longer available." };
64
+ if (!q.multiSelect)
65
+ return commit(session, q, option.label);
66
+ const selected = session.selected.includes(event.option)
67
+ ? session.selected.filter((i) => i !== event.option)
68
+ : [...session.selected, event.option];
69
+ return { kind: "toggled", selected };
70
+ }
71
+ case "done": {
72
+ if (!q.multiSelect)
73
+ return { kind: "rejected", reason: "Pick one of the options." };
74
+ const labels = selectedLabels(q, session.selected);
75
+ if (!labels.length)
76
+ return { kind: "rejected", reason: "Pick at least one option first." };
77
+ return commit(session, q, labels.join(MULTI_JOIN));
78
+ }
79
+ case "typed": {
80
+ const typed = event.text.trim();
81
+ if (!typed)
82
+ return { kind: "rejected", reason: "That doesn't look like an answer." };
83
+ if (!q.multiSelect)
84
+ return commit(session, q, resolveTypedAnswer(q, typed));
85
+ // Multi-select: a pure index list replaces the ticks; anything else is
86
+ // appended to them, matching the web composer's options-plus-free-text join.
87
+ const indices = parseIndexList(q, typed);
88
+ if (indices)
89
+ return commit(session, q, selectedLabels(q, indices).join(MULTI_JOIN));
90
+ return commit(session, q, [...selectedLabels(q, session.selected), typed].join(MULTI_JOIN));
91
+ }
92
+ }
93
+ }
94
+ //# sourceMappingURL=ask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ask.js","sourceRoot":"","sources":["../src/ask.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Chat-surface commands — pure parsing plus the one registry every surface reads,
3
+ * so `/help`, Telegram's native command menu and the dispatcher can never list
4
+ * different things.
5
+ *
6
+ * Only KNOWN commands parse. Unknown `/`-prefixed text is not a command and must
7
+ * keep reaching the agent as an ordinary message: people paste `/Users/me/notes`
8
+ * and `/etc/hosts` into chats, and swallowing those would lose real input.
9
+ */
10
+ export interface ChannelCommand {
11
+ /** Canonical name, without the leading slash. */
12
+ name: string;
13
+ /** Alternative spellings that resolve to `name`. */
14
+ aliases?: string[];
15
+ /** One line, shown in `/help` and in Telegram's command menu. */
16
+ description: string;
17
+ }
18
+ export declare const CHANNEL_COMMANDS: ChannelCommand[];
19
+ /** Commands that end or replace the current thread, so a live questionnaire dies with them. */
20
+ export declare const THREAD_CHANGING_COMMANDS: Set<string>;
21
+ export interface ParsedCommand {
22
+ /** Canonical command name (aliases already resolved). */
23
+ name: string;
24
+ /** Everything after the command word, trimmed (empty when there was none). */
25
+ args: string;
26
+ }
27
+ /**
28
+ * Parse a message as a command, or null if it isn't one.
29
+ *
30
+ * Telegram appends `@botusername` when a command is typed in a group and can
31
+ * carry arguments; both are handled here so channels don't each re-derive it.
32
+ */
33
+ export declare function parseCommand(text: string): ParsedCommand | null;
34
+ /** The `/help` body: one `/command — description` line per command. */
35
+ export declare function helpText(commands?: ChannelCommand[]): string;
36
+ /** Telegram `setMyCommands` payload — canonical names only (aliases stay hidden). */
37
+ export declare function telegramCommandMenu(commands?: ChannelCommand[]): {
38
+ command: string;
39
+ description: string;
40
+ }[];
41
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAW5C,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,wBAAwB,aAAwC,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAW/D;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,QAAQ,GAAE,cAAc,EAAqB,GAAG,MAAM,CAE9E;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CACjC,QAAQ,GAAE,cAAc,EAAqB,GAC5C;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,CAE5C"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Chat-surface commands — pure parsing plus the one registry every surface reads,
3
+ * so `/help`, Telegram's native command menu and the dispatcher can never list
4
+ * different things.
5
+ *
6
+ * Only KNOWN commands parse. Unknown `/`-prefixed text is not a command and must
7
+ * keep reaching the agent as an ordinary message: people paste `/Users/me/notes`
8
+ * and `/etc/hosts` into chats, and swallowing those would lose real input.
9
+ */
10
+ export const CHANNEL_COMMANDS = [
11
+ { name: "new", aliases: ["reset"], description: "Start a fresh conversation" },
12
+ { name: "compact", description: "Summarize this conversation to free up context" },
13
+ {
14
+ name: "cancel",
15
+ aliases: ["skip"],
16
+ description: "Cancel whatever the agent is waiting on",
17
+ },
18
+ { name: "status", description: "Show what this conversation is doing" },
19
+ { name: "help", aliases: ["commands"], description: "List the available commands" },
20
+ { name: "start", description: "What this bot can do" },
21
+ ];
22
+ /** Commands that end or replace the current thread, so a live questionnaire dies with them. */
23
+ export const THREAD_CHANGING_COMMANDS = new Set(["new", "compact", "cancel"]);
24
+ /** name/alias (lowercased) → canonical name. */
25
+ const BY_NAME = new Map(CHANNEL_COMMANDS.flatMap((c) => [c.name, ...(c.aliases ?? [])].map((n) => [n, c.name])));
26
+ /**
27
+ * Parse a message as a command, or null if it isn't one.
28
+ *
29
+ * Telegram appends `@botusername` when a command is typed in a group and can
30
+ * carry arguments; both are handled here so channels don't each re-derive it.
31
+ */
32
+ export function parseCommand(text) {
33
+ // The command word matches Telegram's own grammar exactly — `[A-Za-z0-9_]`
34
+ // plus an optional `@botusername` — and nothing else. Anything with another
35
+ // character in it (`/new/plan.md`, `/Users/me/notes.txt`, `/help@bot/docs`)
36
+ // fails to match and stays an ordinary message, so a pasted path is never
37
+ // mistaken for the command its first segment happens to spell.
38
+ const m = /^\/([A-Za-z0-9_]+)(?:@([A-Za-z0-9_]+))?(?:\s+([\s\S]*))?$/.exec(text.trim());
39
+ if (!m)
40
+ return null;
41
+ const name = BY_NAME.get(m[1].toLowerCase());
42
+ if (!name)
43
+ return null;
44
+ return { name, args: (m[3] ?? "").trim() };
45
+ }
46
+ /** The `/help` body: one `/command — description` line per command. */
47
+ export function helpText(commands = CHANNEL_COMMANDS) {
48
+ return commands.map((c) => `/${c.name} — ${c.description}`).join("\n");
49
+ }
50
+ /** Telegram `setMyCommands` payload — canonical names only (aliases stay hidden). */
51
+ export function telegramCommandMenu(commands = CHANNEL_COMMANDS) {
52
+ return commands.map((c) => ({ command: c.name, description: c.description }));
53
+ }
54
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,MAAM,CAAC,MAAM,gBAAgB,GAAqB;IAChD,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC9E,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAClF;QACE,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,yCAAyC;KACvD;IACD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,WAAW,EAAE,6BAA6B,EAAE;IACnF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE;CACvD,CAAC;AAEF,+FAA+F;AAC/F,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;AAS9E,gDAAgD;AAChD,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAU,CAAC,CAAC,CACjG,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,2EAA2E;IAC3E,4EAA4E;IAC5E,4EAA4E;IAC5E,0EAA0E;IAC1E,+DAA+D;IAC/D,MAAM,CAAC,GAAG,2DAA2D,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IAC9C,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;AAC7C,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,QAAQ,CAAC,WAA6B,gBAAgB;IACpE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzE,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,mBAAmB,CACjC,WAA6B,gBAAgB;IAE7C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AAChF,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @intentface/latch-channels
3
+ *
4
+ * Bring a Latch agent to the places people talk — Slack first. The generic
5
+ * core here is platform-agnostic and dependency-light: a run→`RenderEvent`
6
+ * projection (`consumeRun`) and approval detection (`pendingApprovals`) over
7
+ * the runtime's public SSE contract. Per-platform adapters live in subpath
8
+ * exports (e.g. `@intentface/latch-channels/slack`) and only ever translate
9
+ * `RenderEvent`s — they never re-parse the AI stream.
10
+ *
11
+ * Identity (which platform user maps to which Principal) and channel→agent
12
+ * binding stay in user-land, per Latch's isolation model.
13
+ */
14
+ export * from "./types.js";
15
+ export * from "./render.js";
16
+ export * from "./ask.js";
17
+ export * from "./commands.js";
18
+ export * from "./kernel.js";
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @intentface/latch-channels
3
+ *
4
+ * Bring a Latch agent to the places people talk — Slack first. The generic
5
+ * core here is platform-agnostic and dependency-light: a run→`RenderEvent`
6
+ * projection (`consumeRun`) and approval detection (`pendingApprovals`) over
7
+ * the runtime's public SSE contract. Per-platform adapters live in subpath
8
+ * exports (e.g. `@intentface/latch-channels/slack`) and only ever translate
9
+ * `RenderEvent`s — they never re-parse the AI stream.
10
+ *
11
+ * Identity (which platform user maps to which Principal) and channel→agent
12
+ * binding stay in user-land, per Latch's isolation model.
13
+ */
14
+ export * from "./types.js";
15
+ export * from "./render.js";
16
+ export * from "./ask.js";
17
+ export * from "./commands.js";
18
+ export * from "./kernel.js";
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,207 @@
1
+ import type { AskSession, AskStateStore, AuthResult, ChannelRunOps, ChannelThreadStore, PendingApproval, TranscribeAudio } from "./types.js";
2
+ /**
3
+ * The turn kernel — everything a direct-message surface (Telegram, WhatsApp,
4
+ * the next one) does between "a webhook arrived" and "the platform API was
5
+ * called", written once:
6
+ *
7
+ * dedupe → identity gate (+ stash & link notice) → thread epoch → commands
8
+ * → questionnaire answers → one-turn-per-chat lease → media fold → run
9
+ * → post the answer at finish → render what the turn parked on
10
+ * (approvals, then questions) → button taps back into the run.
11
+ *
12
+ * A {@link Surface} is pure protocol: how to send text, buttons and links, how
13
+ * to edit or retire a card, how to download an attachment. It never sees the
14
+ * runtime, the stores or the state machine, so a new platform is a Surface plus
15
+ * a webhook parser that produces {@link Inbound} events — never a re-derivation
16
+ * of the admission and questionnaire semantics.
17
+ *
18
+ * Answers post ONCE at finish (split past the platform's length cap). No
19
+ * progressive edits: the only progress signal is the surface's typing hint.
20
+ */
21
+ /** An attachment reference as the platform delivered it, plus what we know about it. */
22
+ export interface InboundMedia<Ref> {
23
+ ref: Ref;
24
+ mediaType: string;
25
+ name: string;
26
+ }
27
+ export type Inbound<User, Ref> = {
28
+ kind: "message";
29
+ user: User;
30
+ text: string;
31
+ media: InboundMedia<Ref>[];
32
+ /** Persistent-dedupe key; platforms redeliver until they see a 2xx. */
33
+ deliveryKey: string;
34
+ } | {
35
+ kind: "action";
36
+ user: User;
37
+ /** The button payload — one of the strings the kernel handed the surface. */
38
+ data: string;
39
+ deliveryKey: string;
40
+ /** The tapped card's platform ref, when the platform reports it. */
41
+ card?: string;
42
+ /** Platform ack handle (Telegram callback_query id) — acked via `Surface.ack`. */
43
+ ackId?: string;
44
+ };
45
+ /** Button payloads the surface attaches to a question card. */
46
+ export interface QuestionData {
47
+ pick(optionIndex: number): string;
48
+ done: string;
49
+ }
50
+ export interface Surface<User, Ref> {
51
+ /** Platform name — the log prefix; also conventionally the chat-id prefix. */
52
+ kind: string;
53
+ /** Longest message the platform accepts; longer answers are split at paragraph breaks. */
54
+ maxTextChars: number;
55
+ /** The Latch chat id for this person at a thread epoch (0 = the original, un-suffixed id). */
56
+ chatId(user: User, epoch: number): string;
57
+ /**
58
+ * Send a message. `rich` marks agent output (GitHub-flavoured markdown) the
59
+ * surface may format natively. Returns the card ref when the platform has one.
60
+ */
61
+ sendText(user: User, text: string, opts?: {
62
+ rich?: boolean;
63
+ }): Promise<string | undefined>;
64
+ /** A message whose point is a URL to open (identity link, OAuth connect). */
65
+ sendLink(user: User, body: string, label: string, url: string): Promise<void>;
66
+ /** Approve/Deny prompt. A tap comes back as an `action` carrying `data.approve` / `data.deny`. */
67
+ sendApproval(user: User, approval: PendingApproval, data: {
68
+ approve: string;
69
+ deny: string;
70
+ }): Promise<string | undefined>;
71
+ /**
72
+ * Post the question at `session.cursor`. Without `data` (no host ask-state)
73
+ * post the numbered text form — there is nothing to tap.
74
+ */
75
+ sendQuestion(user: User, session: AskSession, data?: QuestionData): Promise<string | undefined>;
76
+ /** Re-tick a multi-select card in place. Surfaces that cannot edit omit it. */
77
+ updateQuestion?(user: User, card: string, session: AskSession, data: QuestionData): Promise<void>;
78
+ /** Show that a question was answered — collapse the card, or post a receipt. */
79
+ answered(user: User, card: string | undefined, answered: {
80
+ question: string;
81
+ answer: string;
82
+ }): Promise<void>;
83
+ /** Replace a card's content (approval settled / no longer active). Omit where messages can't be edited. */
84
+ settleCard?(user: User, card: string, text: string, opts?: {
85
+ rich?: boolean;
86
+ }): Promise<void>;
87
+ /** Remove a card's buttons without changing its text. */
88
+ retireCard?(user: User, card: string): Promise<void>;
89
+ /** Progress hint ("typing…"), refreshed every few seconds while a turn runs. */
90
+ typing?(user: User): Promise<void>;
91
+ /** Acknowledge a button tap, optionally with a toast. */
92
+ ack?(ackId: string, text?: string): Promise<void>;
93
+ /** Download an attachment, or null when it's unavailable or larger than `maxBytes`. */
94
+ fetchMedia(media: InboundMedia<Ref>, maxBytes: number): Promise<{
95
+ bytes: Uint8Array;
96
+ mediaType: string;
97
+ } | null>;
98
+ }
99
+ export interface KernelConfig<P, User, Ref> {
100
+ /** The agent this bot serves. */
101
+ agent: string;
102
+ /** Stable host-side bot id — the chat-id namespace and the dedupe-key prefix. */
103
+ botRef: string;
104
+ runtime: ChannelRunOps<P>;
105
+ /**
106
+ * Resolve the inbound platform user to a principal. `{ allowed: false, linkUrl }`
107
+ * refuses and sends a link; the triggering message is stashed for replay.
108
+ */
109
+ authorizeUser: (user: User) => Promise<AuthResult<P>>;
110
+ /** Persistent dedupe: true if this delivery key was already handled. */
111
+ seen?: (deliveryKey: string) => Promise<boolean>;
112
+ /**
113
+ * Optional host-backed one-turn-per-chat lease: a claimant token, or null
114
+ * when a turn is already running (→ "still working"). Core's storage adapter
115
+ * already refuses a second live run per chat (`ChatBusyError`, rendered the
116
+ * same way), so this is only for hosts on an adapter without that invariant.
117
+ */
118
+ claimTurn?: (chatId: string) => Promise<string | null>;
119
+ releaseTurn?: (chatId: string, token: string) => Promise<void>;
120
+ /** Stash the message that triggered a link prompt, to replay after linking. */
121
+ stashPending?: (input: {
122
+ user: User;
123
+ text: string;
124
+ media?: InboundMedia<Ref>[];
125
+ }) => Promise<void>;
126
+ /** Build the platform authorize URL for a `connect_<name>` OAuth approval. */
127
+ authorizeUrl?: (connectionName: string, ctx: {
128
+ chatId: string;
129
+ user: User;
130
+ approvalId: string;
131
+ }) => string;
132
+ /**
133
+ * Host speech-to-text. Voice/audio reaches the agent as `[Voice message]: …`
134
+ * text — never a file part (chat models generally reject audio). Without it,
135
+ * voice-only messages get a friendly refusal.
136
+ */
137
+ transcribe?: TranscribeAudio;
138
+ /** Storage for in-flight questionnaires; without it questions post as text and stay answerable in the web app. */
139
+ askState?: AskStateStore;
140
+ /** Storage for the thread epoch — what makes `/new` possible on a one-conversation surface. */
141
+ thread?: ChannelThreadStore;
142
+ /** Attachments above this are skipped with a note (default 10 MB). */
143
+ maxInlineFileBytes?: number;
144
+ }
145
+ export declare function encodeApprovalData(approved: boolean, approvalId: string): string;
146
+ export declare function decodeApprovalData(data: string | undefined): {
147
+ approved: boolean;
148
+ approvalId: string;
149
+ } | null;
150
+ /** Length of the session nonce carried in button payloads. */
151
+ export declare const ASK_NONCE_LENGTH = 6;
152
+ export type QuestionPayload = {
153
+ verb: "pick";
154
+ nonce: string;
155
+ questionIndex: number;
156
+ optionIndex: number;
157
+ } | {
158
+ verb: "done";
159
+ nonce: string;
160
+ questionIndex: number;
161
+ };
162
+ export declare function encodeQuestionData(cb: QuestionPayload): string;
163
+ export declare function decodeQuestionData(data: string | undefined): QuestionPayload | null;
164
+ export declare function newMessageId(): string;
165
+ /** Short random token pinning button payloads to one posted questionnaire. */
166
+ export declare function newNonce(): string;
167
+ /** Split text into ≤`max`-char messages, preferring paragraph/line breaks. */
168
+ export declare function splitText(text: string, max: number): string[];
169
+ /**
170
+ * Run `fn` under the one-turn-per-chat lease. Every path that mutates a chat's
171
+ * run state — fresh turns, replays, approval resumes, questionnaire submits —
172
+ * goes through here, so two of them can never interleave on one chat. When the
173
+ * lease is held, `onBusy` runs instead (the "still working" notice).
174
+ */
175
+ export declare function withTurnLease(lease: {
176
+ claimTurn?: (chatId: string) => Promise<string | null>;
177
+ releaseTurn?: (chatId: string, token: string) => Promise<void>;
178
+ }, chatId: string, onBusy: () => Promise<unknown>, fn: () => Promise<void>): Promise<void>;
179
+ export interface TurnKernel<P, User, Ref> {
180
+ /** Route one parsed webhook event. Runs post-ACK (the caller defers it). */
181
+ handleInbound(ev: Inbound<User, Ref>): Promise<void>;
182
+ /**
183
+ * Run one user message against the agent and render it — WITHOUT the auth
184
+ * gate or turn lease (callers hold those). `chatId` skips the thread lookup
185
+ * when the caller already resolved it.
186
+ */
187
+ runMessage(ctx: {
188
+ user: User;
189
+ text: string;
190
+ media?: InboundMedia<Ref>[];
191
+ principal: P;
192
+ chatId?: string;
193
+ }): Promise<void>;
194
+ /**
195
+ * Replay one already-gated message under the turn lease — the post-link
196
+ * resume. The gate passed by linking; the lease still must be honoured or a
197
+ * racing webhook message starts a second run on the same history.
198
+ */
199
+ replayMessage(ctx: {
200
+ user: User;
201
+ text: string;
202
+ media?: InboundMedia<Ref>[];
203
+ principal: P;
204
+ }): Promise<void>;
205
+ }
206
+ export declare function createTurnKernel<P, User, Ref>(cfg: KernelConfig<P, User, Ref>, surface: Surface<User, Ref>): TurnKernel<P, User, Ref>;
207
+ //# sourceMappingURL=kernel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":"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"}