vellum 0.2.12 → 0.2.14

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 (209) hide show
  1. package/README.md +32 -0
  2. package/bun.lock +2 -2
  3. package/docs/skills.md +4 -4
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
  6. package/src/__tests__/app-git-history.test.ts +176 -0
  7. package/src/__tests__/app-git-service.test.ts +169 -0
  8. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  9. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
  10. package/src/__tests__/browser-skill-endstate.test.ts +6 -6
  11. package/src/__tests__/call-bridge.test.ts +105 -13
  12. package/src/__tests__/call-domain.test.ts +163 -0
  13. package/src/__tests__/call-orchestrator.test.ts +171 -0
  14. package/src/__tests__/call-routes-http.test.ts +246 -6
  15. package/src/__tests__/channel-approval-routes.test.ts +438 -0
  16. package/src/__tests__/channel-approval.test.ts +266 -0
  17. package/src/__tests__/channel-approvals.test.ts +393 -0
  18. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  19. package/src/__tests__/checker.test.ts +607 -1048
  20. package/src/__tests__/cli.test.ts +1 -56
  21. package/src/__tests__/config-schema.test.ts +402 -5
  22. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  23. package/src/__tests__/conflict-policy.test.ts +121 -0
  24. package/src/__tests__/conflict-store.test.ts +2 -0
  25. package/src/__tests__/contacts-tools.test.ts +3 -3
  26. package/src/__tests__/contradiction-checker.test.ts +99 -1
  27. package/src/__tests__/credential-security-invariants.test.ts +22 -6
  28. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  29. package/src/__tests__/elevenlabs-client.test.ts +271 -0
  30. package/src/__tests__/ephemeral-permissions.test.ts +73 -23
  31. package/src/__tests__/filesystem-tools.test.ts +579 -0
  32. package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
  33. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  34. package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
  35. package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
  36. package/src/__tests__/handlers-slack-config.test.ts +2 -1
  37. package/src/__tests__/handlers-telegram-config.test.ts +855 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +141 -1
  39. package/src/__tests__/hooks-runner.test.ts +6 -2
  40. package/src/__tests__/host-file-edit-tool.test.ts +124 -0
  41. package/src/__tests__/host-file-read-tool.test.ts +62 -0
  42. package/src/__tests__/host-file-write-tool.test.ts +59 -0
  43. package/src/__tests__/host-shell-tool.test.ts +251 -0
  44. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +100 -41
  46. package/src/__tests__/ipc-validate.test.ts +50 -0
  47. package/src/__tests__/key-migration.test.ts +23 -0
  48. package/src/__tests__/memory-regressions.test.ts +99 -0
  49. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  50. package/src/__tests__/oauth-callback-registry.test.ts +11 -4
  51. package/src/__tests__/playbook-execution.test.ts +502 -0
  52. package/src/__tests__/playbook-tools.test.ts +4 -6
  53. package/src/__tests__/public-ingress-urls.test.ts +34 -0
  54. package/src/__tests__/qdrant-manager.test.ts +267 -0
  55. package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
  56. package/src/__tests__/recurrence-engine.test.ts +9 -0
  57. package/src/__tests__/recurrence-types.test.ts +8 -0
  58. package/src/__tests__/registry.test.ts +1 -1
  59. package/src/__tests__/runtime-runs.test.ts +1 -25
  60. package/src/__tests__/schedule-store.test.ts +16 -14
  61. package/src/__tests__/schedule-tools.test.ts +83 -0
  62. package/src/__tests__/scheduler-recurrence.test.ts +111 -10
  63. package/src/__tests__/secret-allowlist.test.ts +18 -17
  64. package/src/__tests__/secret-ingress-handler.test.ts +11 -0
  65. package/src/__tests__/secret-scanner.test.ts +43 -0
  66. package/src/__tests__/session-conflict-gate.test.ts +442 -6
  67. package/src/__tests__/session-init.benchmark.test.ts +3 -0
  68. package/src/__tests__/session-process-bridge.test.ts +242 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -1
  70. package/src/__tests__/shell-identity.test.ts +256 -0
  71. package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
  72. package/src/__tests__/subagent-tools.test.ts +637 -54
  73. package/src/__tests__/task-management-tools.test.ts +936 -0
  74. package/src/__tests__/task-runner.test.ts +2 -2
  75. package/src/__tests__/terminal-tools.test.ts +840 -0
  76. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  77. package/src/__tests__/tool-executor.test.ts +85 -151
  78. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  79. package/src/__tests__/trust-store.test.ts +28 -453
  80. package/src/__tests__/twilio-provider.test.ts +153 -3
  81. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  82. package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
  83. package/src/__tests__/twilio-routes.test.ts +17 -262
  84. package/src/__tests__/twitter-auth-handler.test.ts +2 -1
  85. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  86. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  87. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  88. package/src/__tests__/workspace-policy.test.ts +213 -0
  89. package/src/calls/call-bridge.ts +92 -19
  90. package/src/calls/call-domain.ts +157 -5
  91. package/src/calls/call-orchestrator.ts +96 -8
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +97 -0
  94. package/src/calls/elevenlabs-config.ts +31 -0
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +50 -6
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +114 -0
  99. package/src/cli/twitter.ts +200 -21
  100. package/src/cli.ts +1 -20
  101. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
  102. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
  103. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
  104. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  105. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
  106. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  107. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
  108. package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
  109. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
  110. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
  111. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
  112. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
  113. package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
  114. package/src/config/bundled-skills/twitter/SKILL.md +103 -17
  115. package/src/config/defaults.ts +26 -2
  116. package/src/config/schema.ts +178 -9
  117. package/src/config/types.ts +3 -0
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
  119. package/src/daemon/assistant-attachments.ts +4 -2
  120. package/src/daemon/handlers/apps.ts +69 -0
  121. package/src/daemon/handlers/config.ts +543 -24
  122. package/src/daemon/handlers/index.ts +1 -0
  123. package/src/daemon/handlers/sessions.ts +22 -6
  124. package/src/daemon/handlers/shared.ts +2 -1
  125. package/src/daemon/handlers/skills.ts +5 -20
  126. package/src/daemon/ipc-contract-inventory.json +28 -0
  127. package/src/daemon/ipc-contract.ts +168 -10
  128. package/src/daemon/ipc-validate.ts +17 -0
  129. package/src/daemon/lifecycle.ts +2 -0
  130. package/src/daemon/server.ts +78 -72
  131. package/src/daemon/session-attachments.ts +1 -1
  132. package/src/daemon/session-conflict-gate.ts +62 -6
  133. package/src/daemon/session-notifiers.ts +1 -1
  134. package/src/daemon/session-process.ts +62 -3
  135. package/src/daemon/session-tool-setup.ts +1 -2
  136. package/src/daemon/tls-certs.ts +189 -0
  137. package/src/daemon/video-thumbnail.ts +5 -3
  138. package/src/hooks/manager.ts +5 -9
  139. package/src/memory/app-git-service.ts +295 -0
  140. package/src/memory/app-store.ts +21 -0
  141. package/src/memory/conflict-intent.ts +47 -4
  142. package/src/memory/conflict-policy.ts +73 -0
  143. package/src/memory/conflict-store.ts +9 -1
  144. package/src/memory/contradiction-checker.ts +28 -0
  145. package/src/memory/conversation-key-store.ts +15 -0
  146. package/src/memory/db.ts +81 -0
  147. package/src/memory/embedding-local.ts +3 -13
  148. package/src/memory/external-conversation-store.ts +234 -0
  149. package/src/memory/job-handlers/conflict.ts +22 -2
  150. package/src/memory/jobs-worker.ts +67 -28
  151. package/src/memory/runs-store.ts +54 -7
  152. package/src/memory/schema.ts +20 -0
  153. package/src/messaging/provider.ts +9 -0
  154. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  155. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  156. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  157. package/src/messaging/registry.ts +1 -0
  158. package/src/permissions/checker.ts +48 -44
  159. package/src/permissions/defaults.ts +11 -0
  160. package/src/permissions/prompter.ts +0 -4
  161. package/src/permissions/shell-identity.ts +227 -0
  162. package/src/permissions/trust-store.ts +76 -53
  163. package/src/permissions/types.ts +0 -19
  164. package/src/permissions/workspace-policy.ts +114 -0
  165. package/src/providers/retry.ts +12 -37
  166. package/src/runtime/assistant-event-hub.ts +41 -4
  167. package/src/runtime/channel-approval-parser.ts +60 -0
  168. package/src/runtime/channel-approval-types.ts +71 -0
  169. package/src/runtime/channel-approvals.ts +145 -0
  170. package/src/runtime/gateway-client.ts +16 -0
  171. package/src/runtime/http-server.ts +29 -9
  172. package/src/runtime/routes/call-routes.ts +52 -2
  173. package/src/runtime/routes/channel-routes.ts +296 -16
  174. package/src/runtime/routes/conversation-routes.ts +12 -5
  175. package/src/runtime/routes/events-routes.ts +97 -28
  176. package/src/runtime/routes/run-routes.ts +2 -7
  177. package/src/runtime/run-orchestrator.ts +0 -3
  178. package/src/schedule/recurrence-engine.ts +26 -2
  179. package/src/schedule/recurrence-types.ts +1 -1
  180. package/src/schedule/schedule-store.ts +12 -3
  181. package/src/security/secret-scanner.ts +7 -0
  182. package/src/tasks/ephemeral-permissions.ts +0 -2
  183. package/src/tasks/task-scheduler.ts +2 -1
  184. package/src/tools/calls/call-start.ts +8 -0
  185. package/src/tools/execution-target.ts +21 -0
  186. package/src/tools/execution-timeout.ts +49 -0
  187. package/src/tools/executor.ts +6 -135
  188. package/src/tools/network/web-search.ts +9 -32
  189. package/src/tools/policy-context.ts +29 -0
  190. package/src/tools/schedule/update.ts +8 -1
  191. package/src/tools/terminal/parser.ts +16 -18
  192. package/src/tools/types.ts +4 -11
  193. package/src/twitter/oauth-client.ts +102 -0
  194. package/src/twitter/router.ts +101 -0
  195. package/src/util/debounce.ts +88 -0
  196. package/src/util/network-info.ts +47 -0
  197. package/src/util/platform.ts +29 -4
  198. package/src/util/promise-guard.ts +37 -0
  199. package/src/util/retry.ts +98 -0
  200. package/src/util/truncate.ts +1 -1
  201. package/src/workspace/git-service.ts +129 -112
  202. package/src/tools/contacts/contact-merge.ts +0 -55
  203. package/src/tools/contacts/contact-search.ts +0 -58
  204. package/src/tools/contacts/contact-upsert.ts +0 -64
  205. package/src/tools/playbooks/index.ts +0 -4
  206. package/src/tools/playbooks/playbook-create.ts +0 -96
  207. package/src/tools/playbooks/playbook-delete.ts +0 -52
  208. package/src/tools/playbooks/playbook-list.ts +0 -74
  209. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -85,8 +85,9 @@ export async function runMemoryJobsOnce(
85
85
  // Periodic stale item sweep (throttled to at most once per hour)
86
86
  sweepStaleItems(config);
87
87
 
88
+ const batchSize = Math.max(1, config.memory.jobs.batchSize);
88
89
  const concurrency = Math.max(1, config.memory.jobs.workerConcurrency);
89
- const jobs = claimMemoryJobs(concurrency);
90
+ const jobs = claimMemoryJobs(batchSize);
90
91
  if (jobs.length === 0) {
91
92
  if (enableScheduledCleanup) {
92
93
  maybeEnqueueScheduledCleanupJobs(config);
@@ -94,36 +95,47 @@ export async function runMemoryJobsOnce(
94
95
  return 0;
95
96
  }
96
97
 
97
- let processed = 0;
98
+ // Group jobs by type so same-type jobs run sequentially (preventing
99
+ // checkpoint races for backfill, etc.), while different types run concurrently.
100
+ const jobsByType = new Map<string, MemoryJob[]>();
98
101
  for (const job of jobs) {
99
- try {
100
- await processJob(job, config);
101
- completeMemoryJob(job.id);
102
- processed += 1;
103
- } catch (err) {
104
- if (err instanceof BackendUnavailableError) {
105
- // Backend not configured yet -- put the job back with exponential backoff.
106
- const result = deferMemoryJob(job.id);
107
- if (result === 'failed') {
108
- log.warn({ jobId: job.id, type: job.type }, 'Embedding backend unavailable, job exceeded max deferrals');
109
- } else {
110
- log.debug({ jobId: job.id, type: job.type }, 'Embedding backend unavailable, deferring job');
111
- }
112
- } else {
113
- const message = err instanceof Error ? err.message : String(err);
114
- const category = classifyError(err);
115
- if (category === 'retryable') {
116
- const delay = retryDelayForAttempt(job.attempts + 1);
117
- failMemoryJob(job.id, message, {
118
- retryDelayMs: delay,
119
- maxAttempts: RETRY_MAX_ATTEMPTS,
120
- });
121
- log.warn({ err, jobId: job.id, type: job.type, delay, category }, 'Memory job failed (retryable)');
122
- } else {
123
- failMemoryJob(job.id, message, { maxAttempts: 1 });
124
- log.warn({ err, jobId: job.id, type: job.type, category }, 'Memory job failed (fatal)');
102
+ let group = jobsByType.get(job.type);
103
+ if (!group) {
104
+ group = [];
105
+ jobsByType.set(job.type, group);
106
+ }
107
+ group.push(job);
108
+ }
109
+
110
+ let processed = 0;
111
+ const typeGroups = [...jobsByType.values()];
112
+
113
+ // Run type groups concurrently (up to workerConcurrency at a time).
114
+ // Within each group, jobs are processed sequentially.
115
+ for (let i = 0; i < typeGroups.length; i += concurrency) {
116
+ const groupChunk = typeGroups.slice(i, i + concurrency);
117
+ const groupResults = await Promise.allSettled(
118
+ groupChunk.map(async (group) => {
119
+ let groupProcessed = 0;
120
+ for (const job of group) {
121
+ try {
122
+ await processJob(job, config);
123
+ completeMemoryJob(job.id);
124
+ groupProcessed += 1;
125
+ } catch (err) {
126
+ handleJobError(job, err);
127
+ }
125
128
  }
129
+ return groupProcessed;
130
+ }),
131
+ );
132
+ for (const result of groupResults) {
133
+ if (result.status === 'fulfilled') {
134
+ processed += result.value;
126
135
  }
136
+ // Errors within groups are already handled per-job above;
137
+ // a rejected group promise would only come from an unexpected
138
+ // error in the loop itself, which is unlikely.
127
139
  }
128
140
  }
129
141
  if (enableScheduledCleanup) {
@@ -132,6 +144,33 @@ export async function runMemoryJobsOnce(
132
144
  return processed;
133
145
  }
134
146
 
147
+ // ── Job error handling ─────────────────────────────────────────────
148
+
149
+ function handleJobError(job: MemoryJob, err: unknown): void {
150
+ if (err instanceof BackendUnavailableError) {
151
+ const result = deferMemoryJob(job.id);
152
+ if (result === 'failed') {
153
+ log.warn({ jobId: job.id, type: job.type }, 'Embedding backend unavailable, job exceeded max deferrals');
154
+ } else {
155
+ log.debug({ jobId: job.id, type: job.type }, 'Embedding backend unavailable, deferring job');
156
+ }
157
+ } else {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ const category = classifyError(err);
160
+ if (category === 'retryable') {
161
+ const delay = retryDelayForAttempt(job.attempts + 1);
162
+ failMemoryJob(job.id, message, {
163
+ retryDelayMs: delay,
164
+ maxAttempts: RETRY_MAX_ATTEMPTS,
165
+ });
166
+ log.warn({ err, jobId: job.id, type: job.type, delay, category }, 'Memory job failed (retryable)');
167
+ } else {
168
+ failMemoryJob(job.id, message, { maxAttempts: 1 });
169
+ log.warn({ err, jobId: job.id, type: job.type, category }, 'Memory job failed (fatal)');
170
+ }
171
+ }
172
+ }
173
+
135
174
  // ── Job dispatch ───────────────────────────────────────────────────
136
175
 
137
176
  async function processJob(job: MemoryJob, config: AssistantConfig): Promise<void> {
@@ -6,7 +6,7 @@
6
6
  * running → needs_secret → running → completed | failed
7
7
  */
8
8
 
9
- import { eq, inArray } from 'drizzle-orm';
9
+ import { and, eq, inArray } from 'drizzle-orm';
10
10
  import { v4 as uuid } from 'uuid';
11
11
  import { getDb } from './db.js';
12
12
  import { messageRuns } from './schema.js';
@@ -25,12 +25,6 @@ export interface PendingConfirmation {
25
25
  executionTarget?: 'sandbox' | 'host';
26
26
  allowlistOptions?: Array<{ label: string; pattern: string }>;
27
27
  scopeOptions?: Array<{ label: string; scope: string }>;
28
- /** Principal kind that initiated this tool use (e.g. 'core' or 'skill'). */
29
- principalKind?: string;
30
- /** Skill ID when principalKind is 'skill'. */
31
- principalId?: string;
32
- /** Content-hash of the skill source for version tracking. */
33
- principalVersion?: string;
34
28
  /** When false, the client should hide "always allow" / trust-rule persistence affordances. */
35
29
  persistentDecisionsAllowed?: boolean;
36
30
  }
@@ -222,6 +216,59 @@ export function failRun(runId: string, error: string): void {
222
216
  .run();
223
217
  }
224
218
 
219
+ // ---------------------------------------------------------------------------
220
+ // Pending-confirmation lookups
221
+ // ---------------------------------------------------------------------------
222
+
223
+ /** Summary of a run awaiting confirmation, used by channel approval flows. */
224
+ export interface PendingRunInfo {
225
+ runId: string;
226
+ /** The prompter-level request ID stored inside PendingConfirmation.toolUseId. */
227
+ requestId: string;
228
+ toolName: string;
229
+ input: Record<string, unknown>;
230
+ riskLevel: string;
231
+ }
232
+
233
+ /**
234
+ * Find all runs in `needs_confirmation` state for a given conversation.
235
+ *
236
+ * Returns structured info for each pending run so channel adapters can
237
+ * render approval prompts and route decisions without reaching into
238
+ * raw DB rows.
239
+ */
240
+ export function getPendingConfirmationsByConversation(conversationId: string): PendingRunInfo[] {
241
+ const db = getDb();
242
+ const rows = db
243
+ .select()
244
+ .from(messageRuns)
245
+ .where(
246
+ and(
247
+ eq(messageRuns.conversationId, conversationId),
248
+ eq(messageRuns.status, 'needs_confirmation'),
249
+ ),
250
+ )
251
+ .all();
252
+
253
+ const results: PendingRunInfo[] = [];
254
+ for (const row of rows) {
255
+ const run = rowToRun(row);
256
+ if (!run.pendingConfirmation) continue;
257
+ results.push({
258
+ runId: run.id,
259
+ requestId: run.pendingConfirmation.toolUseId,
260
+ toolName: run.pendingConfirmation.toolName,
261
+ input: run.pendingConfirmation.input,
262
+ riskLevel: run.pendingConfirmation.riskLevel,
263
+ });
264
+ }
265
+ return results;
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Orphan recovery
270
+ // ---------------------------------------------------------------------------
271
+
225
272
  /**
226
273
  * Mark all non-terminal runs as failed.
227
274
  * Called on startup to recover from daemon restarts that left runs
@@ -548,6 +548,8 @@ export const callSessions = sqliteTable('call_sessions', {
548
548
  toNumber: text('to_number').notNull(),
549
549
  task: text('task'),
550
550
  status: text('status').notNull().default('initiated'),
551
+ callerIdentityMode: text('caller_identity_mode'),
552
+ callerIdentitySource: text('caller_identity_source'),
551
553
  startedAt: integer('started_at'),
552
554
  endedAt: integer('ended_at'),
553
555
  lastError: text('last_error'),
@@ -586,3 +588,21 @@ export const processedCallbacks = sqliteTable('processed_callbacks', {
586
588
  claimId: text('claim_id'),
587
589
  createdAt: integer('created_at').notNull(),
588
590
  });
591
+
592
+ // ── External Conversation Bindings ───────────────────────────────────
593
+ // UNIQUE (source_channel, external_chat_id) enforced via idx_ext_conv_bindings_channel_chat_unique in db.ts
594
+
595
+ export const externalConversationBindings = sqliteTable('external_conversation_bindings', {
596
+ conversationId: text('conversation_id')
597
+ .primaryKey()
598
+ .references(() => conversations.id, { onDelete: 'cascade' }),
599
+ sourceChannel: text('source_channel').notNull(),
600
+ externalChatId: text('external_chat_id').notNull(),
601
+ externalUserId: text('external_user_id'),
602
+ displayName: text('display_name'),
603
+ username: text('username'),
604
+ createdAt: integer('created_at').notNull(),
605
+ updatedAt: integer('updated_at').notNull(),
606
+ lastInboundAt: integer('last_inbound_at'),
607
+ lastOutboundAt: integer('last_outbound_at'),
608
+ });
@@ -38,6 +38,15 @@ export interface MessagingProvider {
38
38
  getThreadReplies?(token: string, conversationId: string, threadId: string, options?: HistoryOptions): Promise<Message[]>;
39
39
  markRead?(token: string, conversationId: string, messageId?: string): Promise<void>;
40
40
 
41
+ /**
42
+ * Override the default credential check used by getConnectedProviders().
43
+ * When present, the registry calls this instead of looking for
44
+ * credential:${credentialService}:access_token. Useful for providers
45
+ * that don't use OAuth (e.g. Telegram bot tokens stored under a
46
+ * non-standard key).
47
+ */
48
+ isConnected?(): boolean;
49
+
41
50
  /** Platform-specific capabilities for tool routing (e.g. 'reactions', 'threads', 'labels'). */
42
51
  capabilities: Set<string>;
43
52
  }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Telegram Bot messaging provider adapter.
3
+ *
4
+ * Enables proactive outbound messaging to Telegram chats via the gateway's
5
+ * /deliver/telegram endpoint. Unlike Slack/Gmail which use direct API calls
6
+ * with OAuth tokens, Telegram delivery is proxied through the gateway which
7
+ * owns the bot token and handles Telegram API retries.
8
+ *
9
+ * The `token` parameter in MessagingProvider methods is unused for Telegram
10
+ * because delivery is authenticated via the gateway's bearer token, not
11
+ * a per-user OAuth token.
12
+ */
13
+
14
+ import type { MessagingProvider } from '../../provider.js';
15
+ import type {
16
+ Conversation,
17
+ Message,
18
+ SearchResult,
19
+ SendResult,
20
+ ConnectionInfo,
21
+ ListOptions,
22
+ HistoryOptions,
23
+ SearchOptions,
24
+ SendOptions,
25
+ } from '../../provider-types.js';
26
+ import { getSecureKey } from '../../../security/secure-keys.js';
27
+ import { readHttpToken } from '../../../util/platform.js';
28
+ import { getOrCreateConversation } from '../../../memory/conversation-key-store.js';
29
+ import * as externalConversationStore from '../../../memory/external-conversation-store.js';
30
+ import * as telegram from './client.js';
31
+
32
+ /** Resolve the gateway base URL, preferring GATEWAY_INTERNAL_BASE_URL if set. */
33
+ function getGatewayUrl(): string {
34
+ if (process.env.GATEWAY_INTERNAL_BASE_URL) {
35
+ return process.env.GATEWAY_INTERNAL_BASE_URL.replace(/\/+$/, "");
36
+ }
37
+ const port = Number(process.env.GATEWAY_PORT) || 7830;
38
+ return `http://127.0.0.1:${port}`;
39
+ }
40
+
41
+ /** Read the runtime HTTP bearer token used to authenticate with the gateway. */
42
+ function getBearerToken(): string {
43
+ const token = readHttpToken();
44
+ if (!token) {
45
+ throw new Error('No runtime HTTP bearer token available — is the daemon running?');
46
+ }
47
+ return token;
48
+ }
49
+
50
+ /** Read the Telegram bot token from the credential vault. */
51
+ function getBotToken(): string | undefined {
52
+ return getSecureKey('credential:telegram:bot_token');
53
+ }
54
+
55
+ export const telegramBotMessagingProvider: MessagingProvider = {
56
+ id: 'telegram',
57
+ displayName: 'Telegram',
58
+ credentialService: 'telegram',
59
+ capabilities: new Set(['send']),
60
+
61
+ /**
62
+ * Custom connectivity check. The standard registry check looks for
63
+ * credential:telegram:access_token, but the Telegram bot token is
64
+ * stored as credential:telegram:bot_token. This method lets the
65
+ * registry detect that Telegram credentials exist.
66
+ *
67
+ * Both bot_token and webhook_secret are required — the gateway's
68
+ * /deliver/telegram endpoint rejects requests without the webhook
69
+ * secret, so partial credentials would cause every send to fail.
70
+ */
71
+ isConnected(): boolean {
72
+ return getBotToken() !== undefined && !!getSecureKey('credential:telegram:webhook_secret');
73
+ },
74
+
75
+ async testConnection(_token: string): Promise<ConnectionInfo> {
76
+ const botToken = getBotToken();
77
+ if (!botToken) {
78
+ return {
79
+ connected: false,
80
+ user: 'unknown',
81
+ platform: 'telegram',
82
+ metadata: { error: 'No bot token found. Run the telegram-setup skill.' },
83
+ };
84
+ }
85
+
86
+ try {
87
+ const resp = await telegram.getMe(botToken);
88
+ if (!resp.ok || !resp.result) {
89
+ return {
90
+ connected: false,
91
+ user: 'unknown',
92
+ platform: 'telegram',
93
+ metadata: { error: resp.description ?? 'getMe failed' },
94
+ };
95
+ }
96
+
97
+ return {
98
+ connected: true,
99
+ user: resp.result.username ?? resp.result.first_name,
100
+ platform: 'telegram',
101
+ metadata: {
102
+ botId: resp.result.id,
103
+ botUsername: resp.result.username,
104
+ botName: resp.result.first_name,
105
+ },
106
+ };
107
+ } catch (e) {
108
+ return {
109
+ connected: false,
110
+ user: 'unknown',
111
+ platform: 'telegram',
112
+ metadata: { error: e instanceof Error ? e.message : 'getMe failed' },
113
+ };
114
+ }
115
+ },
116
+
117
+ async sendMessage(_token: string, conversationId: string, text: string, _options?: SendOptions): Promise<SendResult> {
118
+ const gatewayUrl = getGatewayUrl();
119
+ const bearerToken = getBearerToken();
120
+
121
+ await telegram.sendMessage(gatewayUrl, bearerToken, conversationId, text);
122
+
123
+ // Upsert external conversation binding so deleted/reset syncs are
124
+ // resurrected when an outbound message is sent. This ensures the
125
+ // conversation key mapping and binding exist for the next inbound.
126
+ try {
127
+ const sourceChannel = 'telegram';
128
+ const conversationKey = `${sourceChannel}:${conversationId}`;
129
+ const { conversationId: internalId } = getOrCreateConversation(conversationKey);
130
+ externalConversationStore.upsertOutboundBinding({
131
+ conversationId: internalId,
132
+ sourceChannel,
133
+ externalChatId: conversationId,
134
+ });
135
+ } catch {
136
+ // Best-effort — don't fail the send if binding upsert fails
137
+ }
138
+
139
+ return {
140
+ id: `tg-${Date.now()}`,
141
+ timestamp: Date.now(),
142
+ conversationId,
143
+ };
144
+ },
145
+
146
+ // Telegram Bot API does not support listing conversations. Bots only
147
+ // interact with chats where users have initiated contact or the bot
148
+ // has been added to a group.
149
+ async listConversations(_token: string, _options?: ListOptions): Promise<Conversation[]> {
150
+ return [];
151
+ },
152
+
153
+ // Telegram Bot API does not provide message history retrieval.
154
+ async getHistory(_token: string, _conversationId: string, _options?: HistoryOptions): Promise<Message[]> {
155
+ return [];
156
+ },
157
+
158
+ // Telegram Bot API does not support message search.
159
+ async search(_token: string, _query: string, _options?: SearchOptions): Promise<SearchResult> {
160
+ return { total: 0, messages: [], hasMore: false };
161
+ },
162
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Low-level Telegram operations.
3
+ *
4
+ * Outbound message delivery routes through the gateway's /deliver/telegram
5
+ * endpoint, which handles bot token management and Telegram API retries.
6
+ * Connection verification calls the Telegram Bot API directly with the
7
+ * stored bot token.
8
+ */
9
+
10
+ import type { TelegramGetMeResponse } from './types.js';
11
+
12
+ const TELEGRAM_API_BASE = 'https://api.telegram.org';
13
+ const DELIVERY_TIMEOUT_MS = 30_000;
14
+
15
+ export class TelegramApiError extends Error {
16
+ constructor(
17
+ public readonly status: number,
18
+ message: string,
19
+ ) {
20
+ super(message);
21
+ this.name = 'TelegramApiError';
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Verify a bot token by calling Telegram's getMe API directly.
27
+ * Used for testConnection() — the only operation that bypasses the gateway.
28
+ */
29
+ export async function getMe(botToken: string): Promise<TelegramGetMeResponse> {
30
+ const resp = await fetch(`${TELEGRAM_API_BASE}/bot${botToken}/getMe`, {
31
+ method: 'POST',
32
+ signal: AbortSignal.timeout(DELIVERY_TIMEOUT_MS),
33
+ });
34
+
35
+ if (!resp.ok) {
36
+ throw new TelegramApiError(
37
+ resp.status,
38
+ `Telegram getMe failed with status ${resp.status}`,
39
+ );
40
+ }
41
+
42
+ return resp.json() as Promise<TelegramGetMeResponse>;
43
+ }
44
+
45
+ /**
46
+ * Send a text message to a Telegram chat via the gateway's deliver endpoint.
47
+ */
48
+ export async function sendMessage(
49
+ gatewayUrl: string,
50
+ bearerToken: string,
51
+ chatId: string,
52
+ text: string,
53
+ ): Promise<void> {
54
+ await deliverToGateway(gatewayUrl, bearerToken, { chatId, text });
55
+ }
56
+
57
+ /**
58
+ * Send a message with attachments to a Telegram chat via the gateway.
59
+ */
60
+ export async function sendMessageWithAttachments(
61
+ gatewayUrl: string,
62
+ bearerToken: string,
63
+ chatId: string,
64
+ text: string | undefined,
65
+ attachmentIds: string[],
66
+ ): Promise<void> {
67
+ await deliverToGateway(gatewayUrl, bearerToken, {
68
+ chatId,
69
+ text,
70
+ attachments: attachmentIds.map((id) => ({ id })),
71
+ });
72
+ }
73
+
74
+ /** Payload accepted by the gateway's /deliver/telegram endpoint. */
75
+ interface DeliverPayload {
76
+ chatId: string;
77
+ text?: string;
78
+ attachments?: Array<{ id: string }>;
79
+ }
80
+
81
+ async function deliverToGateway(
82
+ gatewayUrl: string,
83
+ bearerToken: string,
84
+ payload: DeliverPayload,
85
+ ): Promise<void> {
86
+ const url = `${gatewayUrl}/deliver/telegram`;
87
+ const resp = await fetch(url, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ Authorization: `Bearer ${bearerToken}`,
92
+ },
93
+ body: JSON.stringify(payload),
94
+ signal: AbortSignal.timeout(DELIVERY_TIMEOUT_MS),
95
+ });
96
+
97
+ if (!resp.ok) {
98
+ const body = await resp.text().catch(() => '<unreadable>');
99
+ throw new TelegramApiError(
100
+ resp.status,
101
+ `Gateway /deliver/telegram failed (${resp.status}): ${body}`,
102
+ );
103
+ }
104
+ }
@@ -0,0 +1,15 @@
1
+ /** Telegram Bot API types used by the messaging provider. */
2
+
3
+ export interface TelegramUser {
4
+ id: number;
5
+ is_bot: boolean;
6
+ first_name: string;
7
+ last_name?: string;
8
+ username?: string;
9
+ }
10
+
11
+ export interface TelegramGetMeResponse {
12
+ ok: boolean;
13
+ result?: TelegramUser;
14
+ description?: string;
15
+ }
@@ -23,6 +23,7 @@ export function getMessagingProvider(id: string): MessagingProvider {
23
23
  /** Return all registered providers that have stored credentials. */
24
24
  export function getConnectedProviders(): MessagingProvider[] {
25
25
  return Array.from(providers.values()).filter((p) => {
26
+ if (p.isConnected) return p.isConnected();
26
27
  const token = getSecureKey(`credential:${p.credentialService}:access_token`);
27
28
  return token !== undefined;
28
29
  });