@vellumai/assistant 0.3.3 → 0.3.5

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 (163) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +45 -18
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +391 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +397 -135
  12. package/src/__tests__/channel-approvals.test.ts +99 -3
  13. package/src/__tests__/channel-delivery-store.test.ts +30 -4
  14. package/src/__tests__/channel-guardian.test.ts +261 -22
  15. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  16. package/src/__tests__/config-schema.test.ts +2 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-lifecycle.test.ts +636 -0
  19. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  20. package/src/__tests__/entity-search.test.ts +615 -0
  21. package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
  22. package/src/__tests__/handlers-twilio-config.test.ts +480 -0
  23. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  24. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  25. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  26. package/src/__tests__/run-orchestrator.test.ts +22 -0
  27. package/src/__tests__/secret-scanner.test.ts +223 -0
  28. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  29. package/src/__tests__/shell-parser-property.test.ts +357 -2
  30. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  31. package/src/__tests__/system-prompt.test.ts +25 -1
  32. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  33. package/src/__tests__/twilio-routes.test.ts +39 -3
  34. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  35. package/src/__tests__/user-reference.test.ts +68 -0
  36. package/src/__tests__/web-search.test.ts +1 -1
  37. package/src/__tests__/work-item-output.test.ts +110 -0
  38. package/src/calls/call-domain.ts +8 -5
  39. package/src/calls/call-orchestrator.ts +85 -22
  40. package/src/calls/twilio-config.ts +17 -11
  41. package/src/calls/twilio-rest.ts +276 -0
  42. package/src/calls/twilio-routes.ts +39 -1
  43. package/src/cli/map.ts +6 -0
  44. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  45. package/src/commands/cc-command-registry.ts +14 -1
  46. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  47. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  48. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  49. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  50. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  51. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  52. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  53. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  54. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  55. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  56. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  57. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  58. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  59. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  60. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  61. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  62. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  63. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  64. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  65. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  66. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  67. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  68. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  69. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  70. package/src/config/bundled-skills/messaging/SKILL.md +24 -5
  71. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  72. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  73. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  74. package/src/config/defaults.ts +2 -1
  75. package/src/config/schema.ts +9 -3
  76. package/src/config/skills.ts +5 -32
  77. package/src/config/system-prompt.ts +40 -0
  78. package/src/config/templates/IDENTITY.md +2 -2
  79. package/src/config/user-reference.ts +29 -0
  80. package/src/config/vellum-skills/catalog.json +58 -0
  81. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  82. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  83. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  84. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
  86. package/src/daemon/auth-manager.ts +103 -0
  87. package/src/daemon/computer-use-session.ts +8 -1
  88. package/src/daemon/config-watcher.ts +253 -0
  89. package/src/daemon/handlers/config.ts +819 -22
  90. package/src/daemon/handlers/dictation.ts +182 -0
  91. package/src/daemon/handlers/identity.ts +14 -23
  92. package/src/daemon/handlers/index.ts +2 -0
  93. package/src/daemon/handlers/sessions.ts +2 -0
  94. package/src/daemon/handlers/shared.ts +3 -0
  95. package/src/daemon/handlers/skills.ts +6 -7
  96. package/src/daemon/handlers/work-items.ts +15 -7
  97. package/src/daemon/ipc-contract-inventory.json +10 -0
  98. package/src/daemon/ipc-contract.ts +114 -4
  99. package/src/daemon/ipc-handler.ts +87 -0
  100. package/src/daemon/lifecycle.ts +18 -4
  101. package/src/daemon/ride-shotgun-handler.ts +11 -1
  102. package/src/daemon/server.ts +111 -504
  103. package/src/daemon/session-agent-loop.ts +10 -15
  104. package/src/daemon/session-runtime-assembly.ts +115 -44
  105. package/src/daemon/session-tool-setup.ts +2 -0
  106. package/src/daemon/session.ts +19 -2
  107. package/src/inbound/public-ingress-urls.ts +3 -3
  108. package/src/memory/channel-guardian-store.ts +2 -1
  109. package/src/memory/db-connection.ts +28 -0
  110. package/src/memory/db-init.ts +1163 -0
  111. package/src/memory/db.ts +2 -2007
  112. package/src/memory/embedding-backend.ts +79 -11
  113. package/src/memory/indexer.ts +2 -0
  114. package/src/memory/job-handlers/media-processing.ts +100 -0
  115. package/src/memory/job-utils.ts +64 -4
  116. package/src/memory/jobs-store.ts +2 -1
  117. package/src/memory/jobs-worker.ts +11 -1
  118. package/src/memory/media-store.ts +759 -0
  119. package/src/memory/recall-cache.ts +107 -0
  120. package/src/memory/retriever.ts +36 -2
  121. package/src/memory/schema-migration.ts +984 -0
  122. package/src/memory/schema.ts +99 -0
  123. package/src/memory/search/entity.ts +208 -25
  124. package/src/memory/search/ranking.ts +6 -1
  125. package/src/memory/search/types.ts +26 -0
  126. package/src/messaging/provider-types.ts +2 -0
  127. package/src/messaging/providers/sms/adapter.ts +204 -0
  128. package/src/messaging/providers/sms/client.ts +93 -0
  129. package/src/messaging/providers/sms/types.ts +7 -0
  130. package/src/permissions/checker.ts +16 -2
  131. package/src/permissions/prompter.ts +14 -3
  132. package/src/permissions/trust-store.ts +7 -0
  133. package/src/runtime/approval-message-composer.ts +143 -0
  134. package/src/runtime/channel-approvals.ts +29 -7
  135. package/src/runtime/channel-guardian-service.ts +44 -18
  136. package/src/runtime/channel-readiness-service.ts +292 -0
  137. package/src/runtime/channel-readiness-types.ts +29 -0
  138. package/src/runtime/gateway-client.ts +2 -1
  139. package/src/runtime/http-server.ts +65 -28
  140. package/src/runtime/http-types.ts +3 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/channel-routes.ts +237 -103
  143. package/src/runtime/routes/run-routes.ts +7 -1
  144. package/src/runtime/run-orchestrator.ts +43 -3
  145. package/src/security/secret-scanner.ts +218 -0
  146. package/src/skills/frontmatter.ts +63 -0
  147. package/src/skills/slash-commands.ts +23 -0
  148. package/src/skills/vellum-catalog-remote.ts +107 -0
  149. package/src/tools/assets/materialize.ts +2 -2
  150. package/src/tools/browser/auto-navigate.ts +132 -24
  151. package/src/tools/browser/browser-manager.ts +67 -61
  152. package/src/tools/calls/call-start.ts +1 -0
  153. package/src/tools/claude-code/claude-code.ts +55 -3
  154. package/src/tools/credentials/vault.ts +1 -1
  155. package/src/tools/execution-target.ts +11 -1
  156. package/src/tools/executor.ts +10 -2
  157. package/src/tools/network/web-search.ts +1 -1
  158. package/src/tools/skills/vellum-catalog.ts +61 -156
  159. package/src/tools/terminal/parser.ts +21 -5
  160. package/src/tools/types.ts +2 -0
  161. package/src/twitter/router.ts +1 -1
  162. package/src/util/platform.ts +43 -1
  163. package/src/util/retry.ts +4 -4
@@ -155,6 +155,38 @@ export async function provisionPhoneNumber(
155
155
  };
156
156
  }
157
157
 
158
+ /** Fetch the current status of a Twilio message by SID. */
159
+ export async function fetchMessageStatus(
160
+ accountSid: string,
161
+ authToken: string,
162
+ messageSid: string,
163
+ ): Promise<{ status: string; errorCode?: string; errorMessage?: string }> {
164
+ const res = await fetch(
165
+ `${twilioBaseUrl(accountSid)}/Messages/${encodeURIComponent(messageSid)}.json`,
166
+ {
167
+ method: 'GET',
168
+ headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
169
+ },
170
+ );
171
+
172
+ if (!res.ok) {
173
+ const text = await res.text();
174
+ throw new Error(`Twilio API error ${res.status}: ${text}`);
175
+ }
176
+
177
+ const data = (await res.json()) as {
178
+ status?: string;
179
+ error_code?: number | null;
180
+ error_message?: string | null;
181
+ };
182
+
183
+ return {
184
+ status: data.status ?? 'unknown',
185
+ errorCode: data.error_code != null ? String(data.error_code) : undefined,
186
+ errorMessage: data.error_message ?? undefined,
187
+ };
188
+ }
189
+
158
190
  export interface WebhookUrls {
159
191
  voiceUrl: string;
160
192
  statusCallbackUrl: string;
@@ -224,3 +256,247 @@ export async function updatePhoneNumberWebhooks(
224
256
  throw new Error(`Twilio API error ${updateRes.status} updating webhooks: ${text}`);
225
257
  }
226
258
  }
259
+
260
+ // ── Toll-Free Verification ──────────────────────────────────────────────
261
+
262
+ /** Twilio Messaging API base URL for toll-free verification endpoints. */
263
+ const TOLLFREE_VERIFICATION_BASE = 'https://messaging.twilio.com/v1/Tollfree/Verifications';
264
+
265
+ export interface TollFreeVerification {
266
+ sid: string;
267
+ status: string;
268
+ rejectionReason?: string;
269
+ rejectionReasons?: string[];
270
+ errorCode?: string;
271
+ editAllowed?: boolean;
272
+ editExpiration?: string;
273
+ regulationType?: string;
274
+ }
275
+
276
+ function parseTollFreeVerification(raw: Record<string, unknown>): TollFreeVerification {
277
+ return {
278
+ sid: raw.sid as string,
279
+ status: raw.status as string,
280
+ rejectionReason: (raw.rejection_reason as string) ?? undefined,
281
+ rejectionReasons: (raw.rejection_reasons as string[]) ?? undefined,
282
+ errorCode: (raw.error_code != null ? String(raw.error_code) : undefined),
283
+ editAllowed: (raw.edit_allowed as boolean) ?? undefined,
284
+ editExpiration: (raw.edit_expiration as string) ?? undefined,
285
+ regulationType: (raw.regulation_type as string) ?? undefined,
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Get toll-free verification status for a phone number.
291
+ * If `phoneNumberSid` is provided, filters by that SID; otherwise returns the
292
+ * first verification found.
293
+ */
294
+ export async function getTollFreeVerificationStatus(
295
+ accountSid: string,
296
+ authToken: string,
297
+ phoneNumberSid?: string,
298
+ ): Promise<TollFreeVerification | null> {
299
+ const params = new URLSearchParams();
300
+ if (phoneNumberSid) params.set('TollfreePhoneNumberSid', phoneNumberSid);
301
+
302
+ const url = params.toString()
303
+ ? `${TOLLFREE_VERIFICATION_BASE}?${params.toString()}`
304
+ : TOLLFREE_VERIFICATION_BASE;
305
+
306
+ const res = await fetch(url, {
307
+ method: 'GET',
308
+ headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
309
+ });
310
+
311
+ if (!res.ok) {
312
+ const text = await res.text();
313
+ throw new Error(`Twilio Toll-Free Verification API error ${res.status}: ${text}`);
314
+ }
315
+
316
+ const data = (await res.json()) as { verifications?: Array<Record<string, unknown>> };
317
+ const verifications = data.verifications ?? [];
318
+ if (verifications.length === 0) return null;
319
+
320
+ return parseTollFreeVerification(verifications[0]);
321
+ }
322
+
323
+ export interface TollFreeVerificationSubmitParams {
324
+ tollfreePhoneNumberSid: string;
325
+ businessName: string;
326
+ businessWebsite: string;
327
+ notificationEmail: string;
328
+ useCaseCategories: string[];
329
+ useCaseSummary: string;
330
+ productionMessageSample: string;
331
+ optInImageUrls: string[];
332
+ optInType: string;
333
+ messageVolume: string;
334
+ businessType?: string;
335
+ customerProfileSid?: string;
336
+ }
337
+
338
+ /** Submit a new toll-free verification request. */
339
+ export async function submitTollFreeVerification(
340
+ accountSid: string,
341
+ authToken: string,
342
+ params: TollFreeVerificationSubmitParams,
343
+ ): Promise<TollFreeVerification> {
344
+ const body = new URLSearchParams();
345
+ body.set('TollfreePhoneNumberSid', params.tollfreePhoneNumberSid);
346
+ body.set('BusinessName', params.businessName);
347
+ body.set('BusinessWebsite', params.businessWebsite);
348
+ body.set('NotificationEmail', params.notificationEmail);
349
+ body.set('UseCaseSummary', params.useCaseSummary);
350
+ body.set('ProductionMessageSample', params.productionMessageSample);
351
+ body.set('OptInType', params.optInType);
352
+ body.set('MessageVolume', params.messageVolume);
353
+ body.set('BusinessType', params.businessType ?? 'SOLE_PROPRIETOR');
354
+
355
+ for (const cat of params.useCaseCategories) {
356
+ body.append('UseCaseCategories', cat);
357
+ }
358
+ for (const url of params.optInImageUrls) {
359
+ body.append('OptInImageUrls', url);
360
+ }
361
+ if (params.customerProfileSid) {
362
+ body.set('CustomerProfileSid', params.customerProfileSid);
363
+ }
364
+
365
+ const res = await fetch(TOLLFREE_VERIFICATION_BASE, {
366
+ method: 'POST',
367
+ headers: {
368
+ Authorization: twilioAuthHeader(accountSid, authToken),
369
+ 'Content-Type': 'application/x-www-form-urlencoded',
370
+ },
371
+ body: body.toString(),
372
+ });
373
+
374
+ if (!res.ok) {
375
+ const text = await res.text();
376
+ throw new Error(`Twilio Toll-Free Verification submit error ${res.status}: ${text}`);
377
+ }
378
+
379
+ const data = (await res.json()) as Record<string, unknown>;
380
+ return parseTollFreeVerification(data);
381
+ }
382
+
383
+ /** Update an existing toll-free verification. */
384
+ export async function updateTollFreeVerification(
385
+ accountSid: string,
386
+ authToken: string,
387
+ verificationSid: string,
388
+ params: Partial<TollFreeVerificationSubmitParams>,
389
+ ): Promise<TollFreeVerification> {
390
+ const body = new URLSearchParams();
391
+ if (params.businessName) body.set('BusinessName', params.businessName);
392
+ if (params.businessWebsite) body.set('BusinessWebsite', params.businessWebsite);
393
+ if (params.notificationEmail) body.set('NotificationEmail', params.notificationEmail);
394
+ if (params.useCaseSummary) body.set('UseCaseSummary', params.useCaseSummary);
395
+ if (params.productionMessageSample) body.set('ProductionMessageSample', params.productionMessageSample);
396
+ if (params.optInType) body.set('OptInType', params.optInType);
397
+ if (params.messageVolume) body.set('MessageVolume', params.messageVolume);
398
+ if (params.businessType) body.set('BusinessType', params.businessType);
399
+ if (params.useCaseCategories) {
400
+ for (const cat of params.useCaseCategories) {
401
+ body.append('UseCaseCategories', cat);
402
+ }
403
+ }
404
+ if (params.optInImageUrls) {
405
+ for (const url of params.optInImageUrls) {
406
+ body.append('OptInImageUrls', url);
407
+ }
408
+ }
409
+ if (params.customerProfileSid) body.set('CustomerProfileSid', params.customerProfileSid);
410
+
411
+ const res = await fetch(`${TOLLFREE_VERIFICATION_BASE}/${encodeURIComponent(verificationSid)}`, {
412
+ method: 'POST',
413
+ headers: {
414
+ Authorization: twilioAuthHeader(accountSid, authToken),
415
+ 'Content-Type': 'application/x-www-form-urlencoded',
416
+ },
417
+ body: body.toString(),
418
+ });
419
+
420
+ if (!res.ok) {
421
+ const text = await res.text();
422
+ throw new Error(`Twilio Toll-Free Verification update error ${res.status}: ${text}`);
423
+ }
424
+
425
+ const data = (await res.json()) as Record<string, unknown>;
426
+ return parseTollFreeVerification(data);
427
+ }
428
+
429
+ /** Delete a toll-free verification. */
430
+ export async function deleteTollFreeVerification(
431
+ accountSid: string,
432
+ authToken: string,
433
+ verificationSid: string,
434
+ ): Promise<void> {
435
+ const res = await fetch(`${TOLLFREE_VERIFICATION_BASE}/${encodeURIComponent(verificationSid)}`, {
436
+ method: 'DELETE',
437
+ headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
438
+ });
439
+
440
+ if (!res.ok) {
441
+ const text = await res.text();
442
+ throw new Error(`Twilio Toll-Free Verification delete error ${res.status}: ${text}`);
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Get the SID for an incoming phone number.
448
+ * Looks up the number via `IncomingPhoneNumbers.json?PhoneNumber=...`.
449
+ */
450
+ export async function getPhoneNumberSid(
451
+ accountSid: string,
452
+ authToken: string,
453
+ phoneNumber: string,
454
+ ): Promise<string | null> {
455
+ const res = await fetch(
456
+ `${twilioBaseUrl(accountSid)}/IncomingPhoneNumbers.json?PhoneNumber=${encodeURIComponent(phoneNumber)}`,
457
+ {
458
+ method: 'GET',
459
+ headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
460
+ },
461
+ );
462
+
463
+ if (!res.ok) {
464
+ const text = await res.text();
465
+ throw new Error(`Twilio API error ${res.status} looking up phone number SID: ${text}`);
466
+ }
467
+
468
+ const data = (await res.json()) as {
469
+ incoming_phone_numbers: Array<{ sid: string; phone_number: string }>;
470
+ };
471
+
472
+ const match = data.incoming_phone_numbers.find((n) => n.phone_number === phoneNumber);
473
+ return match?.sid ?? null;
474
+ }
475
+
476
+ /**
477
+ * Release (delete) an incoming phone number from the Twilio account.
478
+ * Looks up the SID by phone number then sends a DELETE request.
479
+ */
480
+ export async function releasePhoneNumber(
481
+ accountSid: string,
482
+ authToken: string,
483
+ phoneNumber: string,
484
+ ): Promise<void> {
485
+ const sid = await getPhoneNumberSid(accountSid, authToken, phoneNumber);
486
+ if (!sid) {
487
+ throw new Error(`Phone number ${phoneNumber} not found on Twilio account ${accountSid}`);
488
+ }
489
+
490
+ const res = await fetch(
491
+ `${twilioBaseUrl(accountSid)}/IncomingPhoneNumbers/${sid}.json`,
492
+ {
493
+ method: 'DELETE',
494
+ headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
495
+ },
496
+ );
497
+
498
+ if (!res.ok) {
499
+ const text = await res.text();
500
+ throw new Error(`Twilio API error ${res.status} releasing phone number: ${text}`);
501
+ }
502
+ }
@@ -29,6 +29,10 @@ import { resolveVoiceQualityProfile, isVoiceProfileValid } from './voice-quality
29
29
 
30
30
  const log = getLogger('twilio-routes');
31
31
 
32
+ const CONTEXT_BLOCK_SPLIT_REGEX = /\n\s*\nContext:\s*/i;
33
+ const MAX_TASK_SUMMARY_CHARS = 120;
34
+ const DEFAULT_WELCOME_GREETING = 'Hello, this is an assistant calling. Is now a good time to talk?';
35
+
32
36
  // ── Helpers ──────────────────────────────────────────────────────────
33
37
 
34
38
  function escapeXml(str: string): string {
@@ -63,6 +67,40 @@ export function generateTwiML(
63
67
  </Response>`;
64
68
  }
65
69
 
70
+ function summarizeTaskForGreeting(task: string | null): string | null {
71
+ if (!task) return null;
72
+ const primaryTask = task.split(CONTEXT_BLOCK_SPLIT_REGEX)[0]?.trim() ?? '';
73
+ if (!primaryTask) return null;
74
+
75
+ const compact = primaryTask.replace(/\s+/g, ' ').trim().replace(/[.!?]+$/, '');
76
+ if (!compact) return null;
77
+ if (compact.length <= MAX_TASK_SUMMARY_CHARS) return compact;
78
+ return `${compact.slice(0, MAX_TASK_SUMMARY_CHARS - 3).trimEnd()}...`;
79
+ }
80
+
81
+ function formatTaskAsCallPurpose(taskSummary: string): string {
82
+ const lower = taskSummary.toLowerCase();
83
+ if (
84
+ lower.startsWith('about ') ||
85
+ lower.startsWith('to ') ||
86
+ lower.startsWith('for ') ||
87
+ lower.startsWith('regarding ')
88
+ ) {
89
+ return taskSummary;
90
+ }
91
+ return `about ${taskSummary}`;
92
+ }
93
+
94
+ export function buildWelcomeGreeting(task: string | null, configuredGreeting?: string): string {
95
+ const override = configuredGreeting?.trim();
96
+ if (override) return override;
97
+
98
+ const taskSummary = summarizeTaskForGreeting(task);
99
+ if (!taskSummary) return DEFAULT_WELCOME_GREETING;
100
+
101
+ return `Hello, I am calling ${formatTaskAsCallPurpose(taskSummary)}. Is now a good time to talk?`;
102
+ }
103
+
66
104
  /**
67
105
  * Resolve the WebSocket relay URL from Twilio config.
68
106
  *
@@ -183,7 +221,7 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
183
221
  // Fallback to legacy resolution when ingress is not configured
184
222
  relayUrl = resolveRelayUrl(twilioConfig.wssBaseUrl, twilioConfig.webhookBaseUrl);
185
223
  }
186
- const welcomeGreeting = process.env.CALL_WELCOME_GREETING ?? 'Hello, how can I help you today?';
224
+ const welcomeGreeting = buildWelcomeGreeting(session.task, process.env.CALL_WELCOME_GREETING);
187
225
 
188
226
  const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, profile);
189
227
 
package/src/cli/map.ts CHANGED
@@ -166,6 +166,12 @@ async function startLearnSession(
166
166
  continue;
167
167
  }
168
168
 
169
+ if (m.type === 'ride_shotgun_progress') {
170
+ // Live progress from auto-navigator
171
+ process.stderr.write(` ${m.message}\n`);
172
+ continue;
173
+ }
174
+
169
175
  if (m.type === 'ride_shotgun_result') {
170
176
  clearTimeout(timeoutHandle);
171
177
  socket.destroy();
@@ -276,6 +276,73 @@ describe('summary extraction', () => {
276
276
  expect(entry!.summary).toBe('');
277
277
  });
278
278
 
279
+ test('returns empty summary when frontmatter is truncated by partial read', () => {
280
+ // Simulate frontmatter that exceeds SUMMARY_READ_BYTES (1024).
281
+ // The closing --- delimiter will be cut off, causing FRONTMATTER_REGEX to
282
+ // fail. extractSummary should return '' instead of '---'.
283
+ const largeFrontmatter = '---\n' + 'key: ' + 'x'.repeat(1100) + '\n---\n\nActual summary.';
284
+ createCommandsDir(tmpDir, {
285
+ 'big-frontmatter.md': largeFrontmatter,
286
+ });
287
+
288
+ const registry = discoverCCCommands(tmpDir);
289
+ const entry = registry.entries.get('big-frontmatter');
290
+ expect(entry).toBeDefined();
291
+ expect(entry!.summary).toBe('');
292
+ });
293
+
294
+ test('returns empty summary when frontmatter is truncated (CRLF)', () => {
295
+ const largeFrontmatter = '---\r\n' + 'key: ' + 'x'.repeat(1100) + '\r\n---\r\n\r\nActual summary.';
296
+ createCommandsDir(tmpDir, {
297
+ 'big-frontmatter-crlf.md': largeFrontmatter,
298
+ });
299
+
300
+ const registry = discoverCCCommands(tmpDir);
301
+ const entry = registry.entries.get('big-frontmatter-crlf');
302
+ expect(entry).toBeDefined();
303
+ expect(entry!.summary).toBe('');
304
+ });
305
+
306
+ test('returns empty summary when frontmatter is truncated with multibyte UTF-8 characters', () => {
307
+ // When frontmatter contains multibyte UTF-8 characters (e.g., CJK text),
308
+ // the JavaScript string length (UTF-16 code units) is smaller than the
309
+ // byte length. The truncation guard must compare byte length, not
310
+ // string length, against SUMMARY_READ_BYTES (1024).
311
+ //
312
+ // Each CJK character is 3 bytes in UTF-8 but 1 code unit in UTF-16.
313
+ // We need the total byte count to reach 1024 while string length stays
314
+ // well below 1024 to exercise the bug.
315
+ const cjkChars = '\u4e00'.repeat(340); // 340 chars * 3 bytes = 1020 bytes
316
+ // '---\n' is 4 bytes, so total = 4 + 1020 = 1024 bytes, but string
317
+ // length = 4 + 340 = 344 chars — well under 1024.
318
+ const truncatedContent = '---\n' + cjkChars;
319
+ createCommandsDir(tmpDir, {
320
+ 'multibyte-frontmatter.md': truncatedContent,
321
+ });
322
+
323
+ const registry = discoverCCCommands(tmpDir);
324
+ const entry = registry.entries.get('multibyte-frontmatter');
325
+ expect(entry).toBeDefined();
326
+ // Should return '' because the frontmatter opening delimiter is present
327
+ // but the closing delimiter is missing and the byte length reached the
328
+ // read limit — indicating truncation.
329
+ expect(entry!.summary).toBe('');
330
+ });
331
+
332
+ test('returns summary for small file starting with thematic break ---', () => {
333
+ // A small markdown file that starts with "---" as a thematic break (not
334
+ // frontmatter) should still have its first content line extracted as a
335
+ // summary, rather than being treated as truncated frontmatter.
336
+ createCommandsDir(tmpDir, {
337
+ 'thematic-break.md': '---\nThis is a valid summary after a thematic break.',
338
+ });
339
+
340
+ const registry = discoverCCCommands(tmpDir);
341
+ const entry = registry.entries.get('thematic-break');
342
+ expect(entry).toBeDefined();
343
+ expect(entry!.summary).toBe('This is a valid summary after a thematic break.');
344
+ });
345
+
279
346
  test('handles frontmatter with Windows-style line endings', () => {
280
347
  createCommandsDir(tmpDir, {
281
348
  'crlf.md': '---\r\ntitle: Test\r\n---\r\n\r\nSummary with CRLF.',
@@ -1,5 +1,6 @@
1
1
  import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync } from 'node:fs';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
+ import { FRONTMATTER_REGEX } from '../skills/frontmatter.js';
3
4
  import { getLogger } from '../util/logger.js';
4
5
 
5
6
  const log = getLogger('cc-commands');
@@ -27,7 +28,6 @@ export interface CCCommandRegistry {
27
28
  // ─── Constants ───────────────────────────────────────────────────────────────
28
29
 
29
30
  const COMMAND_NAME_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
30
- const FRONTMATTER_REGEX = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/;
31
31
  const DEFAULT_CACHE_TTL_MS = 30_000;
32
32
  const MAX_SUMMARY_LENGTH = 100;
33
33
 
@@ -80,6 +80,19 @@ function extractSummary(content: string): string {
80
80
  const fmMatch = content.match(FRONTMATTER_REGEX);
81
81
  if (fmMatch) {
82
82
  body = content.slice(fmMatch[0].length);
83
+ } else if (/^---\r?\n/.test(content)) {
84
+ if (Buffer.byteLength(content, 'utf-8') >= SUMMARY_READ_BYTES) {
85
+ // Content starts with a frontmatter opening delimiter but the closing
86
+ // delimiter was not found. The content length reached SUMMARY_READ_BYTES,
87
+ // so the read was likely truncated — the missing closing `---` is
88
+ // probably just beyond the read boundary. Return empty rather than
89
+ // surfacing partial frontmatter fields as a summary.
90
+ return '';
91
+ }
92
+ // Small file that starts with `---` (thematic break or unclosed
93
+ // frontmatter opener). Skip the leading `---` line and extract the
94
+ // first real content line from the remainder.
95
+ body = content.replace(/^---\r?\n/, '');
83
96
  }
84
97
 
85
98
  // Find first non-empty line
@@ -11,7 +11,15 @@
11
11
  "properties": {
12
12
  "prompt": {
13
13
  "type": "string",
14
- "description": "The coding task or question for Claude Code to work on"
14
+ "description": "The coding task or question for Claude Code to work on. Use this for free-form tasks. Mutually exclusive with command."
15
+ },
16
+ "command": {
17
+ "type": "string",
18
+ "description": "Name of a .claude/commands/*.md command template to execute. The template will be loaded and $ARGUMENTS substituted before execution. Use this instead of prompt when invoking a named CC command."
19
+ },
20
+ "arguments": {
21
+ "type": "string",
22
+ "description": "Arguments to substitute into the command template ($ARGUMENTS placeholder). Only used with the command input."
15
23
  },
16
24
  "working_dir": {
17
25
  "type": "string",
@@ -30,8 +38,7 @@
30
38
  "enum": ["general", "researcher", "coder", "reviewer"],
31
39
  "description": "Worker profile that scopes tool access. Defaults to general (backward compatible)."
32
40
  }
33
- },
34
- "required": ["prompt"]
41
+ }
35
42
  },
36
43
  "executor": "tools/claude-code.ts",
37
44
  "execution_target": "host"
@@ -0,0 +1,15 @@
1
+ # Knowledge Graph
2
+
3
+ Query the entity knowledge graph to explore relationships between people, projects, tools, and other entities tracked in memory.
4
+
5
+ ## When to use
6
+
7
+ - When the user asks about relationships between entities ("what tools does project X use?", "who works on project Y?")
8
+ - When the user wants to explore connected entities across the knowledge graph
9
+ - When automatic memory recall doesn't surface the right relationship-based information
10
+
11
+ ## Capabilities
12
+
13
+ - **Neighbors**: Find entities directly connected to seed entities (optionally filtered by relation and entity type)
14
+ - **Typed traversal**: Multi-step traversal with type constraints at each step (e.g., "me -> works_on -> projects -> uses -> tools")
15
+ - **Intersection**: Find entities reachable from ALL given seeds (e.g., "projects both Alice and Bob work on")
@@ -0,0 +1,56 @@
1
+ {
2
+ "version": 1,
3
+ "tools": [
4
+ {
5
+ "name": "knowledge_graph_query",
6
+ "description": "Query the entity knowledge graph to find related entities and their associated memory items. Supports three query types: 'neighbors' (direct connections), 'typed_traversal' (multi-step with type filters), and 'intersection' (entities reachable from ALL seeds).",
7
+ "category": "memory",
8
+ "risk": "low",
9
+ "input_schema": {
10
+ "type": "object",
11
+ "properties": {
12
+ "query_type": {
13
+ "type": "string",
14
+ "enum": ["neighbors", "typed_traversal", "intersection"],
15
+ "description": "Type of graph query to execute"
16
+ },
17
+ "seeds": {
18
+ "type": "array",
19
+ "items": { "type": "string" },
20
+ "description": "Entity names or aliases to use as starting points"
21
+ },
22
+ "steps": {
23
+ "type": "array",
24
+ "items": {
25
+ "type": "object",
26
+ "properties": {
27
+ "relation_types": {
28
+ "type": "array",
29
+ "items": { "type": "string" },
30
+ "description": "Relation types to follow (e.g., 'uses', 'works_on', 'depends_on')"
31
+ },
32
+ "entity_types": {
33
+ "type": "array",
34
+ "items": { "type": "string" },
35
+ "description": "Entity types to include (e.g., 'person', 'project', 'tool')"
36
+ }
37
+ }
38
+ },
39
+ "description": "Traversal steps for typed_traversal and intersection queries. Each step defines type filters for one hop."
40
+ },
41
+ "max_results": {
42
+ "type": "number",
43
+ "description": "Maximum number of entities to return (default: 20)"
44
+ },
45
+ "include_items": {
46
+ "type": "boolean",
47
+ "description": "Include associated memory items for each found entity (default: true)"
48
+ }
49
+ },
50
+ "required": ["query_type", "seeds"]
51
+ },
52
+ "executor": "tools/graph-query.ts",
53
+ "execution_target": "host"
54
+ }
55
+ ]
56
+ }