chat 4.24.0 → 4.26.0
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.
- package/dist/index.d.ts +181 -7
- package/dist/index.js +314 -33
- package/docs/adapters.mdx +3 -3
- package/docs/api/cards.mdx +5 -0
- package/docs/api/chat.mdx +10 -0
- package/docs/api/modals.mdx +1 -1
- package/docs/cards.mdx +6 -0
- package/docs/getting-started.mdx +1 -1
- package/docs/guides/durable-chat-sessions-nextjs.mdx +13 -7
- package/docs/guides/slack-nextjs.mdx +2 -0
- package/docs/modals.mdx +1 -1
- package/docs/state.mdx +1 -1
- package/docs/usage.mdx +2 -2
- package/package.json +1 -1
- package/docs/adapters/whatsapp.mdx +0 -222
package/dist/index.d.ts
CHANGED
|
@@ -95,7 +95,9 @@ declare class ChannelImpl<TState = Record<string, unknown>> implements Channel<T
|
|
|
95
95
|
*/
|
|
96
96
|
threads(): AsyncIterable<ThreadSummary>;
|
|
97
97
|
fetchMetadata(): Promise<ChannelInfo>;
|
|
98
|
-
post(message:
|
|
98
|
+
post<T extends PostableObject>(message: T): Promise<T>;
|
|
99
|
+
post(message: string | AdapterPostableMessage | AsyncIterable<string> | ChatElement): Promise<SentMessage>;
|
|
100
|
+
private handlePostableObject;
|
|
99
101
|
private postSingleMessage;
|
|
100
102
|
postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
|
|
101
103
|
schedule(message: AdapterPostableMessage | ChatElement, options: {
|
|
@@ -141,6 +143,43 @@ declare class ConsoleLogger implements Logger {
|
|
|
141
143
|
error(message: string, ...args: unknown[]): void;
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Context provided to a PostableObject after it has been posted.
|
|
148
|
+
*/
|
|
149
|
+
interface PostableObjectContext {
|
|
150
|
+
adapter: Adapter;
|
|
151
|
+
logger?: Logger;
|
|
152
|
+
messageId: string;
|
|
153
|
+
threadId: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Base interface for objects that can be posted to threads/channels.
|
|
157
|
+
* Examples: Plan, Poll, etc.
|
|
158
|
+
*
|
|
159
|
+
* @template TData - The data type returned by getPostData()
|
|
160
|
+
*/
|
|
161
|
+
interface PostableObject<TData = unknown> {
|
|
162
|
+
/** Symbol identifying this as a postable object */
|
|
163
|
+
readonly $$typeof: symbol;
|
|
164
|
+
/**
|
|
165
|
+
* Get a fallback text representation for adapters that don't support this object type.
|
|
166
|
+
* This should return a human-readable string representation.
|
|
167
|
+
*/
|
|
168
|
+
getFallbackText(): string;
|
|
169
|
+
/** Get the data to send to the adapter */
|
|
170
|
+
getPostData(): TData;
|
|
171
|
+
/** Check if the adapter supports this object type */
|
|
172
|
+
isSupported(adapter: Adapter): boolean;
|
|
173
|
+
/** The kind of object - used by adapters to dispatch */
|
|
174
|
+
readonly kind: string;
|
|
175
|
+
/** Called after successful posting to bind the object to the thread */
|
|
176
|
+
onPosted(context: PostableObjectContext): void;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Type guard to check if a value is a PostableObject.
|
|
180
|
+
*/
|
|
181
|
+
declare function isPostableObject(value: unknown): value is PostableObject;
|
|
182
|
+
|
|
144
183
|
/**
|
|
145
184
|
* Serialized thread data for passing to external systems (e.g., workflow engines).
|
|
146
185
|
*/
|
|
@@ -252,7 +291,9 @@ declare class ThreadImpl<TState = Record<string, unknown>> implements Thread<TSt
|
|
|
252
291
|
isSubscribed(): Promise<boolean>;
|
|
253
292
|
subscribe(): Promise<void>;
|
|
254
293
|
unsubscribe(): Promise<void>;
|
|
255
|
-
post(message:
|
|
294
|
+
post<T extends PostableObject>(message: T): Promise<T>;
|
|
295
|
+
post(message: string | AdapterPostableMessage | AsyncIterable<string> | ChatElement): Promise<SentMessage>;
|
|
296
|
+
private handlePostableObject;
|
|
256
297
|
postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
|
|
257
298
|
schedule(message: AdapterPostableMessage | ChatElement, options: {
|
|
258
299
|
postAt: Date;
|
|
@@ -337,6 +378,85 @@ declare class NotImplementedError extends ChatError {
|
|
|
337
378
|
constructor(message: string, feature?: string, cause?: unknown);
|
|
338
379
|
}
|
|
339
380
|
|
|
381
|
+
type PlanTaskStatus = "pending" | "in_progress" | "complete" | "error";
|
|
382
|
+
interface PlanTask {
|
|
383
|
+
id: string;
|
|
384
|
+
status: PlanTaskStatus;
|
|
385
|
+
title: string;
|
|
386
|
+
}
|
|
387
|
+
interface PlanModel {
|
|
388
|
+
tasks: PlanModelTask[];
|
|
389
|
+
title: string;
|
|
390
|
+
}
|
|
391
|
+
interface PlanModelTask {
|
|
392
|
+
details?: PlanContent;
|
|
393
|
+
id: string;
|
|
394
|
+
output?: PlanContent;
|
|
395
|
+
status: PlanTaskStatus;
|
|
396
|
+
title: string;
|
|
397
|
+
}
|
|
398
|
+
type PlanContent = string | string[] | {
|
|
399
|
+
markdown: string;
|
|
400
|
+
} | {
|
|
401
|
+
ast: Root;
|
|
402
|
+
};
|
|
403
|
+
interface StartPlanOptions {
|
|
404
|
+
/** Initial plan title and first task title */
|
|
405
|
+
initialMessage: PlanContent;
|
|
406
|
+
}
|
|
407
|
+
interface AddTaskOptions {
|
|
408
|
+
/** Task details/substeps. */
|
|
409
|
+
children?: PlanContent;
|
|
410
|
+
title: PlanContent;
|
|
411
|
+
}
|
|
412
|
+
type UpdateTaskInput = PlanContent | {
|
|
413
|
+
/** Task output/results. */
|
|
414
|
+
output?: PlanContent;
|
|
415
|
+
/** Optional status override. */
|
|
416
|
+
status?: PlanTaskStatus;
|
|
417
|
+
};
|
|
418
|
+
interface CompletePlanOptions {
|
|
419
|
+
/** Final plan title shown when completed */
|
|
420
|
+
completeMessage: PlanContent;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* A Plan represents a task list that can be posted to a thread.
|
|
424
|
+
*
|
|
425
|
+
* Create a plan with `new Plan({ initialMessage: "..." })` and post it with `thread.post(plan)`.
|
|
426
|
+
* After posting, use methods like `addTask()`, `updateTask()`, and `complete()` to update it.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* const plan = new Plan({ initialMessage: "Starting task..." });
|
|
431
|
+
* await thread.post(plan);
|
|
432
|
+
* await plan.addTask({ title: "Fetch data" });
|
|
433
|
+
* await plan.updateTask("Got 42 results");
|
|
434
|
+
* await plan.complete({ completeMessage: "Done!" });
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
declare class Plan implements PostableObject<PlanModel> {
|
|
438
|
+
readonly $$typeof: symbol;
|
|
439
|
+
readonly kind = "plan";
|
|
440
|
+
private _model;
|
|
441
|
+
private _bound;
|
|
442
|
+
constructor(options: StartPlanOptions);
|
|
443
|
+
isSupported(adapter: Adapter): boolean;
|
|
444
|
+
getPostData(): PlanModel;
|
|
445
|
+
getFallbackText(): string;
|
|
446
|
+
onPosted(context: PostableObjectContext): void;
|
|
447
|
+
get id(): string;
|
|
448
|
+
get threadId(): string;
|
|
449
|
+
get title(): string;
|
|
450
|
+
get tasks(): readonly PlanTask[];
|
|
451
|
+
get currentTask(): PlanTask | null;
|
|
452
|
+
addTask(options: AddTaskOptions): Promise<PlanTask | null>;
|
|
453
|
+
updateTask(update?: UpdateTaskInput): Promise<PlanTask | null>;
|
|
454
|
+
reset(options: StartPlanOptions): Promise<PlanTask | null>;
|
|
455
|
+
complete(options: CompletePlanOptions): Promise<void>;
|
|
456
|
+
private canMutate;
|
|
457
|
+
private enqueueEdit;
|
|
458
|
+
}
|
|
459
|
+
|
|
340
460
|
/**
|
|
341
461
|
* Represents the visibility scope of a channel.
|
|
342
462
|
*
|
|
@@ -488,6 +608,16 @@ interface Adapter<TThreadId = unknown, TRawMessage = unknown> {
|
|
|
488
608
|
disconnect?(): Promise<void>;
|
|
489
609
|
/** Edit an existing message */
|
|
490
610
|
editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>;
|
|
611
|
+
/**
|
|
612
|
+
* Edit a previously posted object (Plan, Poll, etc.).
|
|
613
|
+
* If not implemented, object updates will throw PlanNotSupportedError.
|
|
614
|
+
*
|
|
615
|
+
* @param threadId - The thread containing the message
|
|
616
|
+
* @param messageId - The message ID to edit
|
|
617
|
+
* @param kind - The object kind (e.g., "plan")
|
|
618
|
+
* @param data - The object data (type depends on kind)
|
|
619
|
+
*/
|
|
620
|
+
editObject?(threadId: string, messageId: string, kind: string, data: unknown): Promise<RawMessage<TRawMessage>>;
|
|
491
621
|
/** Encode platform-specific data into a thread ID string */
|
|
492
622
|
encodeThreadId(platformData: TThreadId): string;
|
|
493
623
|
/**
|
|
@@ -633,6 +763,15 @@ interface Adapter<TThreadId = unknown, TRawMessage = unknown> {
|
|
|
633
763
|
postEphemeral?(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage<TRawMessage>>;
|
|
634
764
|
/** Post a message to a thread */
|
|
635
765
|
postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>;
|
|
766
|
+
/**
|
|
767
|
+
* Post a special object (Plan, Poll, etc.) as a single message.
|
|
768
|
+
* If not implemented, posting such objects will throw PlanNotSupportedError.
|
|
769
|
+
*
|
|
770
|
+
* @param threadId - The thread to post to
|
|
771
|
+
* @param kind - The object kind (e.g., "plan")
|
|
772
|
+
* @param data - The object data (type depends on kind)
|
|
773
|
+
*/
|
|
774
|
+
postObject?(threadId: string, kind: string, data: unknown): Promise<RawMessage<TRawMessage>>;
|
|
636
775
|
/** Remove a reaction from a message */
|
|
637
776
|
removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
|
|
638
777
|
/** Render formatted content to platform-specific string */
|
|
@@ -920,6 +1059,7 @@ interface Postable<TState = Record<string, unknown>, TRawMessage = unknown> {
|
|
|
920
1059
|
/**
|
|
921
1060
|
* Post a message.
|
|
922
1061
|
*/
|
|
1062
|
+
post<T extends PostableObject>(message: T): Promise<T>;
|
|
923
1063
|
post(message: string | PostableMessage | ChatElement): Promise<SentMessage<TRawMessage>>;
|
|
924
1064
|
/**
|
|
925
1065
|
* Post an ephemeral message visible only to a specific user.
|
|
@@ -1097,8 +1237,15 @@ interface Thread<TState = Record<string, unknown>, TRawMessage = unknown> extend
|
|
|
1097
1237
|
* // Stream from AI SDK
|
|
1098
1238
|
* const result = await agent.stream({ prompt: message.text });
|
|
1099
1239
|
* await thread.post(result.textStream);
|
|
1240
|
+
*
|
|
1241
|
+
* // Plan with live updates
|
|
1242
|
+
* const plan = new Plan({ initialMessage: "Working..." });
|
|
1243
|
+
* await thread.post(plan);
|
|
1244
|
+
* await plan.addTask({ title: "Step 1" });
|
|
1245
|
+
* await plan.complete({ completeMessage: "Done!" });
|
|
1100
1246
|
* ```
|
|
1101
1247
|
*/
|
|
1248
|
+
post<T extends PostableObject>(message: T): Promise<T>;
|
|
1102
1249
|
post(message: string | PostableMessage | ChatElement): Promise<SentMessage<TRawMessage>>;
|
|
1103
1250
|
/**
|
|
1104
1251
|
* Post an ephemeral message visible only to a specific user.
|
|
@@ -1171,6 +1318,7 @@ interface Thread<TState = Record<string, unknown>, TRawMessage = unknown> extend
|
|
|
1171
1318
|
*/
|
|
1172
1319
|
unsubscribe(): Promise<void>;
|
|
1173
1320
|
}
|
|
1321
|
+
|
|
1174
1322
|
interface ThreadInfo {
|
|
1175
1323
|
channelId: string;
|
|
1176
1324
|
channelName?: string;
|
|
@@ -1358,7 +1506,7 @@ type AdapterPostableMessage = string | PostableRaw | PostableMarkdown | Postable
|
|
|
1358
1506
|
* - `AsyncIterable<string>` - Streaming text (e.g., from AI SDK's textStream)
|
|
1359
1507
|
* - `AsyncIterable<string | StreamEvent>` - AI SDK fullStream (auto-detected, extracts text with step separators)
|
|
1360
1508
|
*/
|
|
1361
|
-
type PostableMessage = AdapterPostableMessage | AsyncIterable<string | StreamChunk | StreamEvent
|
|
1509
|
+
type PostableMessage = AdapterPostableMessage | AsyncIterable<string | StreamChunk | StreamEvent> | PostableObject;
|
|
1362
1510
|
/**
|
|
1363
1511
|
* Duck-typed stream event compatible with AI SDK's `fullStream`.
|
|
1364
1512
|
* - `text-delta` events are extracted as text output.
|
|
@@ -2733,6 +2881,28 @@ declare class Chat<TAdapters extends Record<string, Adapter> = Record<string, Ad
|
|
|
2733
2881
|
*/
|
|
2734
2882
|
declare function fromFullStream(stream: AsyncIterable<unknown>): AsyncIterable<string | StreamChunk>;
|
|
2735
2883
|
|
|
2884
|
+
/**
|
|
2885
|
+
* Standalone JSON reviver for Chat SDK objects.
|
|
2886
|
+
*
|
|
2887
|
+
* Restores serialized Thread, Channel, and Message instances during
|
|
2888
|
+
* JSON.parse() without requiring a Chat instance. This is useful in
|
|
2889
|
+
* environments like Vercel Workflow functions where importing the full
|
|
2890
|
+
* Chat instance (with its adapter dependencies) is not possible.
|
|
2891
|
+
*
|
|
2892
|
+
* Thread instances created this way use lazy adapter resolution —
|
|
2893
|
+
* the adapter is looked up from the Chat singleton when first accessed,
|
|
2894
|
+
* so `chat.registerSingleton()` must be called before using thread
|
|
2895
|
+
* methods like `post()` (typically inside a "use step" function).
|
|
2896
|
+
*/
|
|
2897
|
+
declare function reviver(_key: string, value: unknown): unknown;
|
|
2898
|
+
|
|
2899
|
+
interface StreamingMarkdownRendererOptions {
|
|
2900
|
+
/**
|
|
2901
|
+
* Wrap confirmed table blocks in code fences for append-only consumers that
|
|
2902
|
+
* cannot render markdown tables while a stream is in flight.
|
|
2903
|
+
*/
|
|
2904
|
+
wrapTablesForAppend?: boolean;
|
|
2905
|
+
}
|
|
2736
2906
|
/**
|
|
2737
2907
|
* A streaming markdown renderer that buffers potential table headers
|
|
2738
2908
|
* until confirmed by a separator line, preventing tables from flashing
|
|
@@ -2750,6 +2920,8 @@ declare class StreamingMarkdownRenderer {
|
|
|
2750
2920
|
private fenceToggles;
|
|
2751
2921
|
/** Incomplete trailing line buffer for incremental fence tracking. */
|
|
2752
2922
|
private incompleteLine;
|
|
2923
|
+
private readonly options;
|
|
2924
|
+
constructor(options?: StreamingMarkdownRendererOptions);
|
|
2753
2925
|
/** Append a chunk from the LLM stream. */
|
|
2754
2926
|
push(chunk: string): void;
|
|
2755
2927
|
/** O(1) check if accumulated text is inside an unclosed code fence. */
|
|
@@ -2766,9 +2938,10 @@ declare class StreamingMarkdownRenderer {
|
|
|
2766
2938
|
* Get text safe for append-only streaming (e.g. Slack native streaming).
|
|
2767
2939
|
*
|
|
2768
2940
|
* - Holds back unconfirmed table headers until separator arrives.
|
|
2769
|
-
* -
|
|
2770
|
-
* text
|
|
2771
|
-
* the table is still streaming,
|
|
2941
|
+
* - Optionally wraps confirmed tables in code fences so pipes render as
|
|
2942
|
+
* literal text on append-only surfaces that lack native table support.
|
|
2943
|
+
* The code fence is left OPEN while the table is still streaming,
|
|
2944
|
+
* keeping output monotonic for deltas.
|
|
2772
2945
|
* - Holds back unclosed inline markers (**, *, ~~, `, [).
|
|
2773
2946
|
* - The final editMessage replaces everything with properly formatted text.
|
|
2774
2947
|
*/
|
|
@@ -2777,6 +2950,7 @@ declare class StreamingMarkdownRenderer {
|
|
|
2777
2950
|
getText(): string;
|
|
2778
2951
|
/** Signal stream end. Flushes held-back lines. Returns final render. */
|
|
2779
2952
|
finish(): string;
|
|
2953
|
+
private formatAppendOnlyText;
|
|
2780
2954
|
}
|
|
2781
2955
|
|
|
2782
2956
|
/**
|
|
@@ -3222,4 +3396,4 @@ declare const Select: SelectComponent;
|
|
|
3222
3396
|
declare const SelectOption: SelectOptionComponent;
|
|
3223
3397
|
declare const TextInput: TextInputComponent;
|
|
3224
3398
|
|
|
3225
|
-
export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AiAssistantMessage, type AiFilePart, type AiImagePart, type AiMessage, type AiMessagePart, type AiTextPart, type AiUserMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, type ChannelVisibility, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, type ConcurrencyConfig, type ConcurrencyStrategy, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, type DirectMessageHandler, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type LinkPreview, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LockScope, type LockScopeContext, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageContext, type MessageData, type MessageHandler, MessageHistoryCache, type MessageHistoryConfig, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableRaw, type QueueEntry, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, type ScheduledMessage, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toAiMessages, toCardElement, toModalElement, toPlainText, walkAst };
|
|
3399
|
+
export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AddTaskOptions, type AiAssistantMessage, type AiFilePart, type AiImagePart, type AiMessage, type AiMessagePart, type AiTextPart, type AiUserMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, type ChannelVisibility, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, type CompletePlanOptions, type ConcurrencyConfig, type ConcurrencyStrategy, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, type DirectMessageHandler, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type LinkPreview, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LockScope, type LockScopeContext, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageContext, type MessageData, type MessageHandler, MessageHistoryCache, type MessageHistoryConfig, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, Plan, type PlanContent, type PlanModel, type PlanModelTask, type PlanTask, type PlanTaskStatus, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableObject, type PostableObjectContext, type PostableRaw, type QueueEntry, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, type ScheduledMessage, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StartPlanOptions, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type UpdateTaskInput, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isPostableObject, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, reviver, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toAiMessages, toCardElement, toModalElement, toPlainText, walkAst };
|
package/dist/index.js
CHANGED
|
@@ -380,6 +380,27 @@ var Message = class _Message {
|
|
|
380
380
|
}
|
|
381
381
|
};
|
|
382
382
|
|
|
383
|
+
// src/postable-object.ts
|
|
384
|
+
var POSTABLE_OBJECT = /* @__PURE__ */ Symbol.for("chat.postable");
|
|
385
|
+
function isPostableObject(value) {
|
|
386
|
+
return typeof value === "object" && value !== null && value.$$typeof === POSTABLE_OBJECT;
|
|
387
|
+
}
|
|
388
|
+
async function postPostableObject(obj, adapter, threadId, postFn, logger) {
|
|
389
|
+
const context = (raw) => ({
|
|
390
|
+
adapter,
|
|
391
|
+
logger,
|
|
392
|
+
messageId: raw.id,
|
|
393
|
+
threadId: raw.threadId ?? threadId
|
|
394
|
+
});
|
|
395
|
+
if (obj.isSupported(adapter) && adapter.postObject) {
|
|
396
|
+
const raw = await adapter.postObject(threadId, obj.kind, obj.getPostData());
|
|
397
|
+
obj.onPosted(context(raw));
|
|
398
|
+
} else {
|
|
399
|
+
const raw = await postFn(threadId, obj.getFallbackText());
|
|
400
|
+
obj.onPosted(context(raw));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
383
404
|
// src/errors.ts
|
|
384
405
|
var ChatError = class extends Error {
|
|
385
406
|
code;
|
|
@@ -605,6 +626,10 @@ var ChannelImpl = class _ChannelImpl {
|
|
|
605
626
|
};
|
|
606
627
|
}
|
|
607
628
|
async post(message) {
|
|
629
|
+
if (isPostableObject(message)) {
|
|
630
|
+
await this.handlePostableObject(message);
|
|
631
|
+
return message;
|
|
632
|
+
}
|
|
608
633
|
if (isAsyncIterable(message)) {
|
|
609
634
|
let accumulated = "";
|
|
610
635
|
for await (const chunk of fromFullStream(message)) {
|
|
@@ -624,6 +649,14 @@ var ChannelImpl = class _ChannelImpl {
|
|
|
624
649
|
}
|
|
625
650
|
return this.postSingleMessage(postable);
|
|
626
651
|
}
|
|
652
|
+
async handlePostableObject(obj) {
|
|
653
|
+
await postPostableObject(
|
|
654
|
+
obj,
|
|
655
|
+
this.adapter,
|
|
656
|
+
this.id,
|
|
657
|
+
(threadId, message) => this.adapter.postChannelMessage ? this.adapter.postChannelMessage(threadId, message) : this.adapter.postMessage(threadId, message)
|
|
658
|
+
);
|
|
659
|
+
}
|
|
627
660
|
async postSingleMessage(postable) {
|
|
628
661
|
const rawMessage = this.adapter.postChannelMessage ? await this.adapter.postChannelMessage(this.id, postable) : await this.adapter.postMessage(this.id, postable);
|
|
629
662
|
const sent = this.createSentMessage(
|
|
@@ -696,7 +729,7 @@ var ChannelImpl = class _ChannelImpl {
|
|
|
696
729
|
return {
|
|
697
730
|
_type: "chat:Channel",
|
|
698
731
|
id: this.id,
|
|
699
|
-
adapterName: this.adapter.name,
|
|
732
|
+
adapterName: this._adapterName ?? this.adapter.name,
|
|
700
733
|
channelVisibility: this.channelVisibility,
|
|
701
734
|
isDM: this.isDM
|
|
702
735
|
};
|
|
@@ -877,6 +910,12 @@ var StreamingMarkdownRenderer = class {
|
|
|
877
910
|
fenceToggles = 0;
|
|
878
911
|
/** Incomplete trailing line buffer for incremental fence tracking. */
|
|
879
912
|
incompleteLine = "";
|
|
913
|
+
options;
|
|
914
|
+
constructor(options = {}) {
|
|
915
|
+
this.options = {
|
|
916
|
+
wrapTablesForAppend: options.wrapTablesForAppend ?? true
|
|
917
|
+
};
|
|
918
|
+
}
|
|
880
919
|
/** Append a chunk from the LLM stream. */
|
|
881
920
|
push(chunk) {
|
|
882
921
|
this.accumulated += chunk;
|
|
@@ -928,30 +967,31 @@ var StreamingMarkdownRenderer = class {
|
|
|
928
967
|
* Get text safe for append-only streaming (e.g. Slack native streaming).
|
|
929
968
|
*
|
|
930
969
|
* - Holds back unconfirmed table headers until separator arrives.
|
|
931
|
-
* -
|
|
932
|
-
* text
|
|
933
|
-
* the table is still streaming,
|
|
970
|
+
* - Optionally wraps confirmed tables in code fences so pipes render as
|
|
971
|
+
* literal text on append-only surfaces that lack native table support.
|
|
972
|
+
* The code fence is left OPEN while the table is still streaming,
|
|
973
|
+
* keeping output monotonic for deltas.
|
|
934
974
|
* - Holds back unclosed inline markers (**, *, ~~, `, [).
|
|
935
975
|
* - The final editMessage replaces everything with properly formatted text.
|
|
936
976
|
*/
|
|
937
977
|
getCommittableText() {
|
|
938
978
|
if (this.finished) {
|
|
939
|
-
return
|
|
979
|
+
return this.formatAppendOnlyText(this.accumulated, true);
|
|
940
980
|
}
|
|
941
981
|
let text2 = this.accumulated;
|
|
942
982
|
if (text2.length > 0 && !text2.endsWith("\n")) {
|
|
943
983
|
const lastNewline = text2.lastIndexOf("\n");
|
|
944
984
|
const withoutIncompleteLine = lastNewline >= 0 ? text2.slice(0, lastNewline + 1) : "";
|
|
945
985
|
if (isInsideCodeFence(withoutIncompleteLine)) {
|
|
946
|
-
return
|
|
986
|
+
return this.formatAppendOnlyText(text2);
|
|
947
987
|
}
|
|
948
988
|
text2 = withoutIncompleteLine;
|
|
949
989
|
}
|
|
950
990
|
if (isInsideCodeFence(text2)) {
|
|
951
|
-
return
|
|
991
|
+
return this.formatAppendOnlyText(text2);
|
|
952
992
|
}
|
|
953
993
|
const committed = getCommittablePrefix(text2);
|
|
954
|
-
const wrapped =
|
|
994
|
+
const wrapped = this.formatAppendOnlyText(committed);
|
|
955
995
|
if (isInsideCodeFence(wrapped)) {
|
|
956
996
|
return wrapped;
|
|
957
997
|
}
|
|
@@ -967,6 +1007,12 @@ var StreamingMarkdownRenderer = class {
|
|
|
967
1007
|
this.dirty = true;
|
|
968
1008
|
return this.render();
|
|
969
1009
|
}
|
|
1010
|
+
formatAppendOnlyText(text2, closeFences = false) {
|
|
1011
|
+
if (!this.options.wrapTablesForAppend) {
|
|
1012
|
+
return text2;
|
|
1013
|
+
}
|
|
1014
|
+
return wrapTablesForAppend(text2, closeFences);
|
|
1015
|
+
}
|
|
970
1016
|
};
|
|
971
1017
|
var INLINE_MARKER_CHARS = /* @__PURE__ */ new Set(["*", "~", "`", "["]);
|
|
972
1018
|
function isClean(text2) {
|
|
@@ -1308,6 +1354,10 @@ var ThreadImpl = class _ThreadImpl {
|
|
|
1308
1354
|
await this._stateAdapter.unsubscribe(this.id);
|
|
1309
1355
|
}
|
|
1310
1356
|
async post(message) {
|
|
1357
|
+
if (isPostableObject(message)) {
|
|
1358
|
+
await this.handlePostableObject(message);
|
|
1359
|
+
return message;
|
|
1360
|
+
}
|
|
1311
1361
|
if (isAsyncIterable2(message)) {
|
|
1312
1362
|
return this.handleStream(message);
|
|
1313
1363
|
}
|
|
@@ -1330,6 +1380,15 @@ var ThreadImpl = class _ThreadImpl {
|
|
|
1330
1380
|
}
|
|
1331
1381
|
return result;
|
|
1332
1382
|
}
|
|
1383
|
+
async handlePostableObject(obj) {
|
|
1384
|
+
await postPostableObject(
|
|
1385
|
+
obj,
|
|
1386
|
+
this.adapter,
|
|
1387
|
+
this.id,
|
|
1388
|
+
(threadId, message) => this.adapter.postMessage(threadId, message),
|
|
1389
|
+
this._logger
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1333
1392
|
async postEphemeral(user, message, options) {
|
|
1334
1393
|
const { fallbackToDM } = options;
|
|
1335
1394
|
const userId = typeof user === "string" ? user : user.userId;
|
|
@@ -1482,7 +1541,7 @@ var ThreadImpl = class _ThreadImpl {
|
|
|
1482
1541
|
return;
|
|
1483
1542
|
}
|
|
1484
1543
|
const content = renderer.render();
|
|
1485
|
-
if (content !== lastEditContent) {
|
|
1544
|
+
if (content.trim() && content !== lastEditContent) {
|
|
1486
1545
|
try {
|
|
1487
1546
|
await this.adapter.editMessage(threadIdForEdits, msg.id, {
|
|
1488
1547
|
markdown: content
|
|
@@ -1504,12 +1563,14 @@ var ThreadImpl = class _ThreadImpl {
|
|
|
1504
1563
|
renderer.push(chunk);
|
|
1505
1564
|
if (!msg) {
|
|
1506
1565
|
const content = renderer.render();
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1566
|
+
if (content.trim()) {
|
|
1567
|
+
msg = await this.adapter.postMessage(this.id, {
|
|
1568
|
+
markdown: content
|
|
1569
|
+
});
|
|
1570
|
+
threadIdForEdits = msg.threadId || this.id;
|
|
1571
|
+
lastEditContent = content;
|
|
1572
|
+
scheduleNextEdit();
|
|
1573
|
+
}
|
|
1513
1574
|
}
|
|
1514
1575
|
}
|
|
1515
1576
|
} finally {
|
|
@@ -1526,12 +1587,12 @@ var ThreadImpl = class _ThreadImpl {
|
|
|
1526
1587
|
const finalContent = renderer.finish();
|
|
1527
1588
|
if (!msg) {
|
|
1528
1589
|
msg = await this.adapter.postMessage(this.id, {
|
|
1529
|
-
markdown: accumulated
|
|
1590
|
+
markdown: accumulated.trim() ? accumulated : " "
|
|
1530
1591
|
});
|
|
1531
1592
|
threadIdForEdits = msg.threadId || this.id;
|
|
1532
1593
|
lastEditContent = accumulated;
|
|
1533
1594
|
}
|
|
1534
|
-
if (finalContent !== lastEditContent) {
|
|
1595
|
+
if (finalContent.trim() && finalContent !== lastEditContent) {
|
|
1535
1596
|
await this.adapter.editMessage(threadIdForEdits, msg.id, {
|
|
1536
1597
|
markdown: accumulated
|
|
1537
1598
|
});
|
|
@@ -1583,7 +1644,7 @@ var ThreadImpl = class _ThreadImpl {
|
|
|
1583
1644
|
channelVisibility: this.channelVisibility,
|
|
1584
1645
|
currentMessage: this._currentMessage?.toJSON(),
|
|
1585
1646
|
isDM: this.isDM,
|
|
1586
|
-
adapterName: this.adapter.name
|
|
1647
|
+
adapterName: this._adapterName ?? this.adapter.name
|
|
1587
1648
|
};
|
|
1588
1649
|
}
|
|
1589
1650
|
/**
|
|
@@ -1773,6 +1834,23 @@ function extractMessageContent2(message) {
|
|
|
1773
1834
|
throw new Error("Invalid PostableMessage format");
|
|
1774
1835
|
}
|
|
1775
1836
|
|
|
1837
|
+
// src/reviver.ts
|
|
1838
|
+
function reviver(_key, value) {
|
|
1839
|
+
if (value && typeof value === "object" && "_type" in value) {
|
|
1840
|
+
const typed = value;
|
|
1841
|
+
if (typed._type === "chat:Thread") {
|
|
1842
|
+
return ThreadImpl.fromJSON(value);
|
|
1843
|
+
}
|
|
1844
|
+
if (typed._type === "chat:Channel") {
|
|
1845
|
+
return ChannelImpl.fromJSON(value);
|
|
1846
|
+
}
|
|
1847
|
+
if (typed._type === "chat:Message") {
|
|
1848
|
+
return Message.fromJSON(value);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return value;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1776
1854
|
// src/chat.ts
|
|
1777
1855
|
var DEFAULT_LOCK_TTL_MS = 3e4;
|
|
1778
1856
|
function sleep(ms) {
|
|
@@ -2189,21 +2267,7 @@ var Chat = class {
|
|
|
2189
2267
|
*/
|
|
2190
2268
|
reviver() {
|
|
2191
2269
|
this.registerSingleton();
|
|
2192
|
-
return
|
|
2193
|
-
if (value && typeof value === "object" && "_type" in value) {
|
|
2194
|
-
const typed = value;
|
|
2195
|
-
if (typed._type === "chat:Thread") {
|
|
2196
|
-
return ThreadImpl.fromJSON(value);
|
|
2197
|
-
}
|
|
2198
|
-
if (typed._type === "chat:Channel") {
|
|
2199
|
-
return ChannelImpl.fromJSON(value);
|
|
2200
|
-
}
|
|
2201
|
-
if (typed._type === "chat:Message") {
|
|
2202
|
-
return Message.fromJSON(value);
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
return value;
|
|
2206
|
-
};
|
|
2270
|
+
return reviver;
|
|
2207
2271
|
}
|
|
2208
2272
|
// ChatInstance interface implementations
|
|
2209
2273
|
/**
|
|
@@ -3311,6 +3375,220 @@ var Chat = class {
|
|
|
3311
3375
|
}
|
|
3312
3376
|
};
|
|
3313
3377
|
|
|
3378
|
+
// src/plan.ts
|
|
3379
|
+
function contentToPlainText(content) {
|
|
3380
|
+
if (!content) {
|
|
3381
|
+
return "";
|
|
3382
|
+
}
|
|
3383
|
+
if (Array.isArray(content)) {
|
|
3384
|
+
return content.join(" ").trim();
|
|
3385
|
+
}
|
|
3386
|
+
if (typeof content === "string") {
|
|
3387
|
+
return content;
|
|
3388
|
+
}
|
|
3389
|
+
if ("markdown" in content) {
|
|
3390
|
+
return toPlainText(parseMarkdown(content.markdown));
|
|
3391
|
+
}
|
|
3392
|
+
if ("ast" in content) {
|
|
3393
|
+
return toPlainText(content.ast);
|
|
3394
|
+
}
|
|
3395
|
+
return "";
|
|
3396
|
+
}
|
|
3397
|
+
var Plan = class {
|
|
3398
|
+
$$typeof = POSTABLE_OBJECT;
|
|
3399
|
+
kind = "plan";
|
|
3400
|
+
_model;
|
|
3401
|
+
_bound = null;
|
|
3402
|
+
constructor(options) {
|
|
3403
|
+
const title = contentToPlainText(options.initialMessage) || "Plan";
|
|
3404
|
+
const firstTask = {
|
|
3405
|
+
id: crypto.randomUUID(),
|
|
3406
|
+
title,
|
|
3407
|
+
status: "in_progress"
|
|
3408
|
+
};
|
|
3409
|
+
this._model = { title, tasks: [firstTask] };
|
|
3410
|
+
}
|
|
3411
|
+
isSupported(adapter) {
|
|
3412
|
+
return !!adapter.postObject && !!adapter.editObject;
|
|
3413
|
+
}
|
|
3414
|
+
getPostData() {
|
|
3415
|
+
return this._model;
|
|
3416
|
+
}
|
|
3417
|
+
getFallbackText() {
|
|
3418
|
+
const lines = [];
|
|
3419
|
+
lines.push(`\u{1F4CB} ${this._model.title || "Plan"}`);
|
|
3420
|
+
for (const task of this._model.tasks) {
|
|
3421
|
+
const statusIcons = {
|
|
3422
|
+
complete: "\u2705",
|
|
3423
|
+
in_progress: "\u{1F504}",
|
|
3424
|
+
error: "\u274C"
|
|
3425
|
+
};
|
|
3426
|
+
const statusIcon = statusIcons[task.status] ?? "\u2B1C";
|
|
3427
|
+
lines.push(`${statusIcon} ${task.title}`);
|
|
3428
|
+
}
|
|
3429
|
+
return lines.join("\n");
|
|
3430
|
+
}
|
|
3431
|
+
onPosted(context) {
|
|
3432
|
+
this._bound = {
|
|
3433
|
+
adapter: context.adapter,
|
|
3434
|
+
fallback: !this.isSupported(context.adapter),
|
|
3435
|
+
logger: context.logger,
|
|
3436
|
+
messageId: context.messageId,
|
|
3437
|
+
threadId: context.threadId,
|
|
3438
|
+
updateChain: Promise.resolve()
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
get id() {
|
|
3442
|
+
return this._bound?.messageId ?? "";
|
|
3443
|
+
}
|
|
3444
|
+
get threadId() {
|
|
3445
|
+
return this._bound?.threadId ?? "";
|
|
3446
|
+
}
|
|
3447
|
+
get title() {
|
|
3448
|
+
return this._model.title;
|
|
3449
|
+
}
|
|
3450
|
+
get tasks() {
|
|
3451
|
+
return this._model.tasks.map((t) => ({
|
|
3452
|
+
id: t.id,
|
|
3453
|
+
title: t.title,
|
|
3454
|
+
status: t.status
|
|
3455
|
+
}));
|
|
3456
|
+
}
|
|
3457
|
+
get currentTask() {
|
|
3458
|
+
let current;
|
|
3459
|
+
for (let i = this._model.tasks.length - 1; i >= 0; i--) {
|
|
3460
|
+
if (this._model.tasks[i].status === "in_progress") {
|
|
3461
|
+
current = this._model.tasks[i];
|
|
3462
|
+
break;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
current ??= this._model.tasks.at(-1);
|
|
3466
|
+
if (!current) {
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
return { id: current.id, title: current.title, status: current.status };
|
|
3470
|
+
}
|
|
3471
|
+
async addTask(options) {
|
|
3472
|
+
if (!this.canMutate()) {
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
const title = contentToPlainText(options.title) || "Task";
|
|
3476
|
+
for (const task of this._model.tasks) {
|
|
3477
|
+
if (task.status === "in_progress") {
|
|
3478
|
+
task.status = "complete";
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
const nextTask = {
|
|
3482
|
+
id: crypto.randomUUID(),
|
|
3483
|
+
title,
|
|
3484
|
+
status: "in_progress",
|
|
3485
|
+
details: options.children
|
|
3486
|
+
};
|
|
3487
|
+
this._model.tasks.push(nextTask);
|
|
3488
|
+
this._model.title = title;
|
|
3489
|
+
await this.enqueueEdit();
|
|
3490
|
+
return { id: nextTask.id, title: nextTask.title, status: nextTask.status };
|
|
3491
|
+
}
|
|
3492
|
+
async updateTask(update) {
|
|
3493
|
+
if (!this.canMutate()) {
|
|
3494
|
+
return null;
|
|
3495
|
+
}
|
|
3496
|
+
let current;
|
|
3497
|
+
for (let i = this._model.tasks.length - 1; i >= 0; i--) {
|
|
3498
|
+
if (this._model.tasks[i].status === "in_progress") {
|
|
3499
|
+
current = this._model.tasks[i];
|
|
3500
|
+
break;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
current ??= this._model.tasks.at(-1);
|
|
3504
|
+
if (!current) {
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
if (update !== void 0) {
|
|
3508
|
+
if (typeof update === "object" && update !== null && "output" in update) {
|
|
3509
|
+
if (update.output !== void 0) {
|
|
3510
|
+
current.output = update.output;
|
|
3511
|
+
}
|
|
3512
|
+
if (update.status) {
|
|
3513
|
+
current.status = update.status;
|
|
3514
|
+
}
|
|
3515
|
+
} else {
|
|
3516
|
+
current.output = update;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
await this.enqueueEdit();
|
|
3520
|
+
return { id: current.id, title: current.title, status: current.status };
|
|
3521
|
+
}
|
|
3522
|
+
async reset(options) {
|
|
3523
|
+
if (!this.canMutate()) {
|
|
3524
|
+
return null;
|
|
3525
|
+
}
|
|
3526
|
+
const title = contentToPlainText(options.initialMessage) || "Plan";
|
|
3527
|
+
const firstTask = {
|
|
3528
|
+
id: crypto.randomUUID(),
|
|
3529
|
+
title,
|
|
3530
|
+
status: "in_progress"
|
|
3531
|
+
};
|
|
3532
|
+
this._model = { title, tasks: [firstTask] };
|
|
3533
|
+
await this.enqueueEdit();
|
|
3534
|
+
return {
|
|
3535
|
+
id: firstTask.id,
|
|
3536
|
+
title: firstTask.title,
|
|
3537
|
+
status: firstTask.status
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
async complete(options) {
|
|
3541
|
+
if (!this.canMutate()) {
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
for (const task of this._model.tasks) {
|
|
3545
|
+
if (task.status === "in_progress") {
|
|
3546
|
+
task.status = "complete";
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
this._model.title = contentToPlainText(options.completeMessage) || this._model.title;
|
|
3550
|
+
await this.enqueueEdit();
|
|
3551
|
+
}
|
|
3552
|
+
canMutate() {
|
|
3553
|
+
return !!this._bound;
|
|
3554
|
+
}
|
|
3555
|
+
enqueueEdit() {
|
|
3556
|
+
if (!this._bound) {
|
|
3557
|
+
return Promise.resolve();
|
|
3558
|
+
}
|
|
3559
|
+
const bound = this._bound;
|
|
3560
|
+
const doEdit = async () => {
|
|
3561
|
+
if (bound.fallback) {
|
|
3562
|
+
await bound.adapter.editMessage(
|
|
3563
|
+
bound.threadId,
|
|
3564
|
+
bound.messageId,
|
|
3565
|
+
this.getFallbackText()
|
|
3566
|
+
);
|
|
3567
|
+
} else {
|
|
3568
|
+
const editObject = bound.adapter.editObject;
|
|
3569
|
+
if (!editObject) {
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
await editObject.call(
|
|
3573
|
+
bound.adapter,
|
|
3574
|
+
bound.threadId,
|
|
3575
|
+
bound.messageId,
|
|
3576
|
+
this.kind,
|
|
3577
|
+
this._model
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
const chained = bound.updateChain.then(doEdit, doEdit);
|
|
3582
|
+
bound.updateChain = chained.then(
|
|
3583
|
+
() => void 0,
|
|
3584
|
+
(err) => {
|
|
3585
|
+
bound.logger?.warn("Failed to edit plan", err);
|
|
3586
|
+
}
|
|
3587
|
+
);
|
|
3588
|
+
return chained;
|
|
3589
|
+
}
|
|
3590
|
+
};
|
|
3591
|
+
|
|
3314
3592
|
// src/emoji.ts
|
|
3315
3593
|
var emojiRegistry = /* @__PURE__ */ new Map();
|
|
3316
3594
|
function getEmoji(name) {
|
|
@@ -3749,6 +4027,7 @@ export {
|
|
|
3749
4027
|
MessageHistoryCache,
|
|
3750
4028
|
Modal2 as Modal,
|
|
3751
4029
|
NotImplementedError,
|
|
4030
|
+
Plan,
|
|
3752
4031
|
RadioSelect2 as RadioSelect,
|
|
3753
4032
|
RateLimitError,
|
|
3754
4033
|
Section2 as Section,
|
|
@@ -3787,6 +4066,7 @@ export {
|
|
|
3787
4066
|
isListNode,
|
|
3788
4067
|
isModalElement2 as isModalElement,
|
|
3789
4068
|
isParagraphNode,
|
|
4069
|
+
isPostableObject,
|
|
3790
4070
|
isStrongNode,
|
|
3791
4071
|
isTableCellNode,
|
|
3792
4072
|
isTableNode,
|
|
@@ -3796,6 +4076,7 @@ export {
|
|
|
3796
4076
|
markdownToPlainText,
|
|
3797
4077
|
paragraph,
|
|
3798
4078
|
parseMarkdown,
|
|
4079
|
+
reviver,
|
|
3799
4080
|
root,
|
|
3800
4081
|
strikethrough,
|
|
3801
4082
|
stringifyMarkdown,
|
package/docs/adapters.mdx
CHANGED
|
@@ -34,7 +34,7 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid
|
|
|
34
34
|
| Tables | ✅ Block Kit | ✅ GFM | ⚠️ ASCII | ✅ GFM | ⚠️ ASCII | ✅ GFM | ✅ GFM | ❌ |
|
|
35
35
|
| Fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ Template variables |
|
|
36
36
|
| Images in cards | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
|
|
37
|
-
| Modals | ✅ |
|
|
37
|
+
| Modals | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
38
38
|
|
|
39
39
|
### Conversations
|
|
40
40
|
|
|
@@ -64,7 +64,7 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid
|
|
|
64
64
|
⚠️ indicates partial support — the feature works with limitations. See individual adapter pages for details.
|
|
65
65
|
</Callout>
|
|
66
66
|
|
|
67
|
-
## How
|
|
67
|
+
## How adapters work
|
|
68
68
|
|
|
69
69
|
Each adapter implements a standard interface that the `Chat` class uses to route events and send messages. When a webhook arrives:
|
|
70
70
|
|
|
@@ -73,7 +73,7 @@ Each adapter implements a standard interface that the `Chat` class uses to route
|
|
|
73
73
|
3. Routes to your handlers via the `Chat` class
|
|
74
74
|
4. Converts outgoing messages from markdown/AST/cards to the platform's native format
|
|
75
75
|
|
|
76
|
-
## Using multiple
|
|
76
|
+
## Using multiple adapters
|
|
77
77
|
|
|
78
78
|
Register multiple [adapters](/adapters) and your event handlers work across all of them:
|
|
79
79
|
|
package/docs/api/cards.mdx
CHANGED
|
@@ -95,6 +95,11 @@ Button({ id: "delete", label: "Delete", style: "danger", value: "item-123" })
|
|
|
95
95
|
description: 'Optional payload sent with the action callback.',
|
|
96
96
|
type: 'string',
|
|
97
97
|
},
|
|
98
|
+
actionType: {
|
|
99
|
+
description: 'Hints to adapters like Teams that this button will open a modal via event.openModal().',
|
|
100
|
+
type: '"action" | "modal"',
|
|
101
|
+
default: '"action"',
|
|
102
|
+
},
|
|
98
103
|
}}
|
|
99
104
|
/>
|
|
100
105
|
|
package/docs/api/chat.mdx
CHANGED
|
@@ -475,3 +475,13 @@ Get a `JSON.parse` reviver that deserializes `Thread` and `Message` objects from
|
|
|
475
475
|
const data = JSON.parse(payload, bot.reviver());
|
|
476
476
|
await data.thread.post("Hello from workflow!");
|
|
477
477
|
```
|
|
478
|
+
|
|
479
|
+
There is also a standalone `reviver` export that works without a `Chat` instance. This is useful in Vercel Workflow functions where importing the full Chat instance (with its adapter dependencies) is not possible:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import { reviver } from "chat";
|
|
483
|
+
|
|
484
|
+
const data = JSON.parse(payload, reviver) as { thread: Thread; message: Message };
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
The standalone reviver uses lazy adapter resolution - the adapter is looked up from the Chat singleton when first accessed. Call `chat.registerSingleton()` before using thread methods like `post()` (typically inside a `"use step"` function).
|
package/docs/api/modals.mdx
CHANGED
|
@@ -4,7 +4,7 @@ description: Modal form components for collecting user input.
|
|
|
4
4
|
type: reference
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Modals display form dialogs that collect structured user input. Currently supported on Slack.
|
|
7
|
+
Modals display form dialogs that collect structured user input. Currently supported on Slack and Teams.
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
10
|
import { Modal, TextInput, Select, RadioSelect, SelectOption } from "chat";
|
package/docs/cards.mdx
CHANGED
|
@@ -109,6 +109,12 @@ The `id` maps to your `onAction` handler. Optional `value` passes extra data:
|
|
|
109
109
|
<Button id="report" value="bug">Report Bug</Button>
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
Set `actionType="modal"` to indicate the button opens a [modal](/docs/modals). The button still triggers your `onAction` handler, where you call `event.openModal()` — this prop tells adapters like Teams to wire up the button for dialog opening:
|
|
113
|
+
|
|
114
|
+
```tsx title="lib/bot.tsx"
|
|
115
|
+
<Button id="open-feedback" actionType="modal">Give Feedback</Button>
|
|
116
|
+
```
|
|
117
|
+
|
|
112
118
|
### CardLink
|
|
113
119
|
|
|
114
120
|
Inline hyperlink rendered as text. Unlike `LinkButton` (which must be inside `Actions`), `CardLink` can be placed directly in a card alongside other content.
|
package/docs/getting-started.mdx
CHANGED
|
@@ -14,7 +14,7 @@ Learn the core patterns for handling incoming events and posting messages back t
|
|
|
14
14
|
<Card title="Posting Messages" description="Different ways to render and send messages with thread.post()." href="/docs/posting-messages" />
|
|
15
15
|
</Cards>
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Adapters
|
|
18
18
|
|
|
19
19
|
Connect your bot to chat platforms and persist state across restarts.
|
|
20
20
|
|
|
@@ -98,14 +98,14 @@ export type ChatTurnPayload = {
|
|
|
98
98
|
|
|
99
99
|
## Create the durable session workflow
|
|
100
100
|
|
|
101
|
-
The workflow receives the serialized thread and first message, restores them with `
|
|
101
|
+
The workflow receives the serialized thread and first message, restores them with `reviver`, and then keeps waiting for more turns through the hook.
|
|
102
102
|
|
|
103
103
|
The important detail is that the workflow only orchestrates. Chat SDK side effects such as `post()`, `unsubscribe()`, and `setState()` stay inside step helpers:
|
|
104
104
|
|
|
105
105
|
```typescript title="workflows/durable-chat-session.ts" lineNumbers
|
|
106
|
-
import { Message, type Thread } from "chat";
|
|
106
|
+
import { Message, reviver, type Thread } from "chat";
|
|
107
107
|
import { createHook, getWorkflowMetadata } from "workflow";
|
|
108
|
-
import {
|
|
108
|
+
import type { ThreadState } from "@/lib/bot";
|
|
109
109
|
import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";
|
|
110
110
|
|
|
111
111
|
async function postAssistantMessage(
|
|
@@ -114,6 +114,7 @@ async function postAssistantMessage(
|
|
|
114
114
|
) {
|
|
115
115
|
"use step";
|
|
116
116
|
|
|
117
|
+
const { bot } = await import("@/lib/bot");
|
|
117
118
|
await bot.initialize();
|
|
118
119
|
await thread.post(text);
|
|
119
120
|
}
|
|
@@ -121,6 +122,7 @@ async function postAssistantMessage(
|
|
|
121
122
|
async function closeSession(thread: Thread<ThreadState>) {
|
|
122
123
|
"use step";
|
|
123
124
|
|
|
125
|
+
const { bot } = await import("@/lib/bot");
|
|
124
126
|
await bot.initialize();
|
|
125
127
|
await thread.post("Session closed.");
|
|
126
128
|
await thread.unsubscribe();
|
|
@@ -154,7 +156,7 @@ export async function durableChatSession(payload: string) {
|
|
|
154
156
|
"use workflow";
|
|
155
157
|
|
|
156
158
|
const { workflowRunId } = getWorkflowMetadata();
|
|
157
|
-
const { thread, message } = JSON.parse(payload,
|
|
159
|
+
const { thread, message } = JSON.parse(payload, reviver) as {
|
|
158
160
|
thread: Thread<ThreadState>;
|
|
159
161
|
message: Message;
|
|
160
162
|
};
|
|
@@ -186,11 +188,15 @@ export async function durableChatSession(payload: string) {
|
|
|
186
188
|
The `using` keyword requires TypeScript 5.2+ with `"lib": ["esnext.disposable"]` in your `tsconfig.json`. If you are on an older version, call `hook.dispose()` manually when the session ends.
|
|
187
189
|
</Callout>
|
|
188
190
|
|
|
191
|
+
<Callout type="warn">
|
|
192
|
+
Do not import `bot` at the top level of a workflow file. Adapter packages depend on Node.js modules that are not available in the workflow sandbox. Use the standalone `reviver` for deserialization and import `bot` dynamically inside `"use step"` functions where Node.js modules are available.
|
|
193
|
+
</Callout>
|
|
194
|
+
|
|
189
195
|
This is the core integration:
|
|
190
196
|
|
|
191
197
|
- `thread.toJSON()` and `message.toJSON()` cross the workflow boundary safely
|
|
192
|
-
- `
|
|
193
|
-
- `
|
|
198
|
+
- `reviver` restores real Chat SDK objects inside the workflow without pulling in adapter dependencies
|
|
199
|
+
- `registerSingleton()` is called in `lib/bot.ts` and the singleton is available inside step functions when `bot` is dynamically imported
|
|
194
200
|
- `createHook<ChatTurnPayload>({ token: workflowRunId })` makes the workflow run itself the session identifier
|
|
195
201
|
- `runTurn()`, `postAssistantMessage()`, and `closeSession()` are steps, so adapter and state side effects stay outside the workflow sandbox
|
|
196
202
|
|
|
@@ -328,4 +334,4 @@ From here you can add:
|
|
|
328
334
|
- [Handling Events](/docs/handling-events) — Mentions, subscribed messages, and routing behavior
|
|
329
335
|
- [Streaming](/docs/streaming) — Stream AI SDK responses directly to chat platforms
|
|
330
336
|
- [Thread API](/docs/api/thread) — `thread.toJSON()`, `thread.setState()`, and other thread primitives
|
|
331
|
-
- [Chat API](/docs/api/chat) — `
|
|
337
|
+
- [Chat API](/docs/api/chat) — `reviver`, initialization, and webhook access
|
|
@@ -87,6 +87,8 @@ After creating the app:
|
|
|
87
87
|
1. Go to **OAuth & Permissions**, click **Install to Workspace**, and copy the **Bot User OAuth Token** (`xoxb-...`) — you'll need this as `SLACK_BOT_TOKEN`
|
|
88
88
|
2. Go to **Basic Information** → **App Credentials** and copy the **Signing Secret** — you'll need this as `SLACK_SIGNING_SECRET`
|
|
89
89
|
|
|
90
|
+
If you're distributing the app across multiple workspaces via OAuth instead of installing it to one workspace, configure `clientId` and `clientSecret` on the Slack adapter and pass the same redirect URI used during the authorize step into `handleOAuthCallback(request, { redirectUri })` in your callback route.
|
|
91
|
+
|
|
90
92
|
## Configure environment variables
|
|
91
93
|
|
|
92
94
|
Create a `.env.local` file in your project root:
|
package/docs/modals.mdx
CHANGED
|
@@ -6,7 +6,7 @@ prerequisites:
|
|
|
6
6
|
- /docs/actions
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Modals open form dialogs in response to button clicks or [slash commands](/docs/slash-commands). They support text inputs, dropdowns, radio buttons, and server-side validation. Currently supported on Slack.
|
|
9
|
+
Modals open form dialogs in response to button clicks or [slash commands](/docs/slash-commands). They support text inputs, dropdowns, radio buttons, and server-side validation. Currently supported on Slack and Teams.
|
|
10
10
|
|
|
11
11
|
## Open a modal
|
|
12
12
|
|
package/docs/state.mdx
CHANGED
|
@@ -8,7 +8,7 @@ prerequisites:
|
|
|
8
8
|
|
|
9
9
|
State adapters handle persistent storage for thread subscriptions, distributed locks (to prevent duplicate processing), and caching. You must provide a state adapter when creating a `Chat` instance. Browse all available state adapters on the [Adapters](/adapters) page.
|
|
10
10
|
|
|
11
|
-
## What state
|
|
11
|
+
## What state adapters manage
|
|
12
12
|
|
|
13
13
|
### Thread subscriptions
|
|
14
14
|
|
package/docs/usage.mdx
CHANGED
|
@@ -39,7 +39,7 @@ bot.onNewMention(async (thread) => {
|
|
|
39
39
|
|
|
40
40
|
Each adapter factory auto-detects credentials from environment variables (`SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `REDIS_URL`, etc.), so you can get started with zero config. Pass explicit values to override.
|
|
41
41
|
|
|
42
|
-
## Multiple
|
|
42
|
+
## Multiple adapters
|
|
43
43
|
|
|
44
44
|
Register multiple [adapters](/adapters) to deploy your bot across platforms simultaneously:
|
|
45
45
|
|
|
@@ -76,7 +76,7 @@ Your event handlers work identically across all registered adapters — the SDK
|
|
|
76
76
|
| `fallbackStreamingPlaceholderText` | `string \| null` | `"..."` | Placeholder text while streaming starts. Set to `null` to skip |
|
|
77
77
|
| `onLockConflict` | `'drop' \| 'force' \| (threadId, message) => 'drop' \| 'force'` | `"drop"` | Behavior when a thread lock is already held. `'force'` releases the existing lock and re-acquires it, enabling interrupt/steerability for long-running handlers |
|
|
78
78
|
|
|
79
|
-
## Accessing
|
|
79
|
+
## Accessing adapters
|
|
80
80
|
|
|
81
81
|
Use `getAdapter` to access platform-specific APIs when you need functionality beyond the unified interface:
|
|
82
82
|
|
package/package.json
CHANGED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: WhatsApp
|
|
3
|
-
description: Configure the WhatsApp adapter for the WhatsApp Business Cloud API.
|
|
4
|
-
type: integration
|
|
5
|
-
prerequisites:
|
|
6
|
-
- /docs/getting-started
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Installation
|
|
10
|
-
|
|
11
|
-
```sh title="Terminal"
|
|
12
|
-
pnpm add @chat-adapter/whatsapp
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Usage
|
|
16
|
-
|
|
17
|
-
The adapter auto-detects `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_PHONE_NUMBER_ID`, and `WHATSAPP_VERIFY_TOKEN` from environment variables:
|
|
18
|
-
|
|
19
|
-
```typescript title="lib/bot.ts" lineNumbers
|
|
20
|
-
import { Chat } from "chat";
|
|
21
|
-
import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";
|
|
22
|
-
|
|
23
|
-
const bot = new Chat({
|
|
24
|
-
userName: "mybot",
|
|
25
|
-
adapters: {
|
|
26
|
-
whatsapp: createWhatsAppAdapter(),
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
bot.onNewMention(async (thread, message) => {
|
|
31
|
-
await thread.post(`You said: ${message.text}`);
|
|
32
|
-
});
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Since all WhatsApp conversations are 1:1 DMs, every incoming message is treated as a mention.
|
|
36
|
-
|
|
37
|
-
## Webhook route
|
|
38
|
-
|
|
39
|
-
WhatsApp uses two webhook mechanisms:
|
|
40
|
-
|
|
41
|
-
1. **Verification handshake** (GET) — Meta sends a `hub.verify_token` challenge that must match your `WHATSAPP_VERIFY_TOKEN`.
|
|
42
|
-
2. **Event delivery** (POST) — incoming messages, reactions, and interactive responses, verified via `X-Hub-Signature-256`.
|
|
43
|
-
|
|
44
|
-
Both are handled by the same `handleWebhook` method:
|
|
45
|
-
|
|
46
|
-
```typescript title="app/api/webhooks/whatsapp/route.ts" lineNumbers
|
|
47
|
-
import { bot } from "@/lib/bot";
|
|
48
|
-
|
|
49
|
-
export async function GET(request: Request): Promise<Response> {
|
|
50
|
-
return bot.webhooks.whatsapp(request);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function POST(request: Request): Promise<Response> {
|
|
54
|
-
return bot.webhooks.whatsapp(request);
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Meta app setup
|
|
59
|
-
|
|
60
|
-
### 1. Create a Meta app
|
|
61
|
-
|
|
62
|
-
1. Go to [developers.facebook.com/apps](https://developers.facebook.com/apps)
|
|
63
|
-
2. Click **Create App** and select **Business** type
|
|
64
|
-
3. Give it a name and click **Create App**
|
|
65
|
-
|
|
66
|
-
### 2. Add WhatsApp product
|
|
67
|
-
|
|
68
|
-
1. In the app dashboard, find **WhatsApp** and click **Set Up**
|
|
69
|
-
2. This creates a test phone number and sandbox environment
|
|
70
|
-
|
|
71
|
-
### 3. Get credentials
|
|
72
|
-
|
|
73
|
-
From the app dashboard:
|
|
74
|
-
|
|
75
|
-
| Credential | Where to find it |
|
|
76
|
-
|---|---|
|
|
77
|
-
| **Access Token** | WhatsApp > API Setup > Temporary access token (or create a System User token for production) |
|
|
78
|
-
| **Phone Number ID** | WhatsApp > API Setup > Phone number ID |
|
|
79
|
-
| **App Secret** | Settings > Basic > App Secret |
|
|
80
|
-
| **Verify Token** | You define this — any secret string you choose |
|
|
81
|
-
|
|
82
|
-
### 4. Configure webhooks
|
|
83
|
-
|
|
84
|
-
1. Go to **WhatsApp** > **Configuration** in your app dashboard
|
|
85
|
-
2. Click **Edit** next to Webhook URL
|
|
86
|
-
3. Set **Callback URL** to `https://your-domain.com/api/webhooks/whatsapp`
|
|
87
|
-
4. Set **Verify token** to the same value as your `WHATSAPP_VERIFY_TOKEN`
|
|
88
|
-
5. Click **Verify and Save**
|
|
89
|
-
6. Subscribe to the **messages** webhook field
|
|
90
|
-
|
|
91
|
-
### 5. Production access
|
|
92
|
-
|
|
93
|
-
For production use:
|
|
94
|
-
|
|
95
|
-
1. Add a real phone number under **WhatsApp** > **API Setup**
|
|
96
|
-
2. Create a **System User** in Meta Business Suite for a permanent access token
|
|
97
|
-
3. Complete Meta's **App Review** process for the `whatsapp_business_messaging` permission
|
|
98
|
-
|
|
99
|
-
## Interactive messages
|
|
100
|
-
|
|
101
|
-
Card elements are automatically converted to WhatsApp interactive messages:
|
|
102
|
-
|
|
103
|
-
- **3 or fewer buttons** — rendered as WhatsApp reply buttons (title max 20 characters)
|
|
104
|
-
- **More than 3 buttons** — falls back to formatted text
|
|
105
|
-
|
|
106
|
-
```typescript title="lib/bot.ts" lineNumbers
|
|
107
|
-
import { Card, Actions, Button, Body, BodyText } from "chat";
|
|
108
|
-
|
|
109
|
-
bot.onNewMention(async (thread) => {
|
|
110
|
-
await thread.post(
|
|
111
|
-
<Card>
|
|
112
|
-
<Body>
|
|
113
|
-
<BodyText>How can I help?</BodyText>
|
|
114
|
-
</Body>
|
|
115
|
-
<Actions>
|
|
116
|
-
<Button id="help" value="help">Get Help</Button>
|
|
117
|
-
<Button id="status" value="status">Check Status</Button>
|
|
118
|
-
</Actions>
|
|
119
|
-
</Card>
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Media attachments
|
|
125
|
-
|
|
126
|
-
Incoming media messages (images, documents, audio, video, voice, stickers) are exposed as attachments with a lazy `fetchData()` function. Media is downloaded in two steps via the Graph API — first fetching the media URL, then downloading the binary data.
|
|
127
|
-
|
|
128
|
-
```typescript title="lib/bot.ts" lineNumbers
|
|
129
|
-
bot.onNewMention(async (thread, message) => {
|
|
130
|
-
for (const attachment of message.attachments) {
|
|
131
|
-
if (attachment.fetchData) {
|
|
132
|
-
const data = await attachment.fetchData();
|
|
133
|
-
// Process the media buffer...
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## 24-hour messaging window
|
|
140
|
-
|
|
141
|
-
WhatsApp enforces a [24-hour customer service window](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages#customer-service-windows). You can only send free-form messages to a user within 24 hours of their last message. After that, you must use approved **message templates**.
|
|
142
|
-
|
|
143
|
-
## Configuration
|
|
144
|
-
|
|
145
|
-
All options are auto-detected from environment variables when not provided.
|
|
146
|
-
|
|
147
|
-
| Option | Required | Description |
|
|
148
|
-
|--------|----------|-------------|
|
|
149
|
-
| `accessToken` | No* | Meta access token. Auto-detected from `WHATSAPP_ACCESS_TOKEN` |
|
|
150
|
-
| `appSecret` | No* | App secret for webhook signature verification. Auto-detected from `WHATSAPP_APP_SECRET` |
|
|
151
|
-
| `phoneNumberId` | No* | Bot's phone number ID. Auto-detected from `WHATSAPP_PHONE_NUMBER_ID` |
|
|
152
|
-
| `verifyToken` | No* | Secret for webhook verification handshake. Auto-detected from `WHATSAPP_VERIFY_TOKEN` |
|
|
153
|
-
| `apiVersion` | No | Graph API version (defaults to `v21.0`) |
|
|
154
|
-
| `userName` | No | Bot username. Auto-detected from `WHATSAPP_BOT_USERNAME` or defaults to `whatsapp-bot` |
|
|
155
|
-
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
|
|
156
|
-
|
|
157
|
-
*All four credentials are required — either via config or environment variables.
|
|
158
|
-
|
|
159
|
-
## Environment variables
|
|
160
|
-
|
|
161
|
-
```bash title=".env.local"
|
|
162
|
-
WHATSAPP_ACCESS_TOKEN=EAAx... # Meta access token
|
|
163
|
-
WHATSAPP_APP_SECRET=abc123... # App secret for signature verification
|
|
164
|
-
WHATSAPP_PHONE_NUMBER_ID=1234567890 # Phone number ID from Meta dashboard
|
|
165
|
-
WHATSAPP_VERIFY_TOKEN=my-secret # Your chosen webhook verify token
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## Features
|
|
169
|
-
|
|
170
|
-
| Feature | Supported |
|
|
171
|
-
|---------|-----------|
|
|
172
|
-
| Mentions | N/A (all messages are DMs) |
|
|
173
|
-
| Reactions (add/remove) | Yes |
|
|
174
|
-
| Cards | Interactive messages (max 3 buttons) / text fallback |
|
|
175
|
-
| Modals | No |
|
|
176
|
-
| Streaming | No |
|
|
177
|
-
| DMs | Yes (all conversations) |
|
|
178
|
-
| Ephemeral messages | No |
|
|
179
|
-
| File uploads | Receive only |
|
|
180
|
-
| Typing indicator | Yes |
|
|
181
|
-
| Message history | No |
|
|
182
|
-
| Edit message | No (throws) |
|
|
183
|
-
| Delete message | No (throws) |
|
|
184
|
-
|
|
185
|
-
## Thread ID format
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
whatsapp:{phoneNumberId}:{userWaId}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Example: `whatsapp:1234567890:15551234567`
|
|
192
|
-
|
|
193
|
-
## Notes
|
|
194
|
-
|
|
195
|
-
- All WhatsApp conversations are 1:1 DMs between the business phone number and the user, so every message sets `isMention: true`.
|
|
196
|
-
- `editMessage()` and `deleteMessage()` throw errors — WhatsApp does not support these operations.
|
|
197
|
-
- `fetchMessages()` returns empty results — WhatsApp does not provide a message history API.
|
|
198
|
-
- Messages exceeding 4096 characters are automatically split at paragraph or line boundaries.
|
|
199
|
-
- Webhook signatures are verified using HMAC-SHA256 with `timingSafeEqual` for timing-attack resistance.
|
|
200
|
-
|
|
201
|
-
## Troubleshooting
|
|
202
|
-
|
|
203
|
-
### Webhook verification failing
|
|
204
|
-
|
|
205
|
-
- Verify `WHATSAPP_VERIFY_TOKEN` matches what you set in the Meta dashboard
|
|
206
|
-
- Ensure both GET and POST routes are configured for the webhook URL
|
|
207
|
-
|
|
208
|
-
### "Invalid signature" error
|
|
209
|
-
|
|
210
|
-
- Check that `WHATSAPP_APP_SECRET` is correct (find it under Settings > Basic in your Meta app)
|
|
211
|
-
- Ensure the raw request body is not parsed before verification
|
|
212
|
-
|
|
213
|
-
### Bot not responding
|
|
214
|
-
|
|
215
|
-
- Confirm the **messages** webhook field is subscribed in Meta dashboard
|
|
216
|
-
- Check that you're within the 24-hour messaging window (or using template messages)
|
|
217
|
-
- Verify the phone number ID matches your configured `WHATSAPP_PHONE_NUMBER_ID`
|
|
218
|
-
|
|
219
|
-
### Media downloads failing
|
|
220
|
-
|
|
221
|
-
- Ensure your access token has the required permissions
|
|
222
|
-
- Check that the media hasn't expired (WhatsApp media URLs are temporary)
|