vellum 0.2.13 → 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 (207) 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 +113 -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 +137 -18
  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 +62 -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 +27 -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 +4 -4
  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 +93 -7
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +8 -0
  94. package/src/calls/elevenlabs-config.ts +7 -5
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +32 -37
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +29 -7
  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 +142 -34
  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 +10 -4
  116. package/src/config/schema.ts +80 -21
  117. package/src/config/types.ts +1 -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/prompter.ts +0 -4
  160. package/src/permissions/shell-identity.ts +227 -0
  161. package/src/permissions/trust-store.ts +76 -53
  162. package/src/permissions/types.ts +0 -19
  163. package/src/permissions/workspace-policy.ts +114 -0
  164. package/src/providers/retry.ts +12 -37
  165. package/src/runtime/assistant-event-hub.ts +41 -4
  166. package/src/runtime/channel-approval-parser.ts +60 -0
  167. package/src/runtime/channel-approval-types.ts +71 -0
  168. package/src/runtime/channel-approvals.ts +145 -0
  169. package/src/runtime/gateway-client.ts +16 -0
  170. package/src/runtime/http-server.ts +29 -9
  171. package/src/runtime/routes/call-routes.ts +52 -2
  172. package/src/runtime/routes/channel-routes.ts +296 -16
  173. package/src/runtime/routes/events-routes.ts +97 -28
  174. package/src/runtime/routes/run-routes.ts +2 -7
  175. package/src/runtime/run-orchestrator.ts +0 -3
  176. package/src/schedule/recurrence-engine.ts +26 -2
  177. package/src/schedule/recurrence-types.ts +1 -1
  178. package/src/schedule/schedule-store.ts +12 -3
  179. package/src/security/secret-scanner.ts +7 -0
  180. package/src/tasks/ephemeral-permissions.ts +0 -2
  181. package/src/tasks/task-scheduler.ts +2 -1
  182. package/src/tools/calls/call-start.ts +8 -0
  183. package/src/tools/execution-target.ts +21 -0
  184. package/src/tools/execution-timeout.ts +49 -0
  185. package/src/tools/executor.ts +6 -135
  186. package/src/tools/network/web-search.ts +9 -32
  187. package/src/tools/policy-context.ts +29 -0
  188. package/src/tools/schedule/update.ts +8 -1
  189. package/src/tools/terminal/parser.ts +16 -18
  190. package/src/tools/types.ts +4 -11
  191. package/src/twitter/oauth-client.ts +102 -0
  192. package/src/twitter/router.ts +101 -0
  193. package/src/util/debounce.ts +88 -0
  194. package/src/util/network-info.ts +47 -0
  195. package/src/util/platform.ts +29 -4
  196. package/src/util/promise-guard.ts +37 -0
  197. package/src/util/retry.ts +98 -0
  198. package/src/util/truncate.ts +1 -1
  199. package/src/workspace/git-service.ts +129 -112
  200. package/src/tools/contacts/contact-merge.ts +0 -55
  201. package/src/tools/contacts/contact-search.ts +0 -58
  202. package/src/tools/contacts/contact-upsert.ts +0 -64
  203. package/src/tools/playbooks/index.ts +0 -4
  204. package/src/tools/playbooks/playbook-create.ts +0 -96
  205. package/src/tools/playbooks/playbook-delete.ts +0 -52
  206. package/src/tools/playbooks/playbook-list.ts +0 -74
  207. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -1,9 +1,54 @@
1
+ import { and, eq } from 'drizzle-orm';
1
2
  import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
2
- import { executePlaybookDelete } from '../../../../tools/playbooks/playbook-delete.js';
3
+ import { getDb } from '../../../../memory/db.js';
4
+ import { memoryItems } from '../../../../memory/schema.js';
5
+ import { parsePlaybookStatement } from '../../../../playbooks/types.js';
3
6
 
4
- export async function run(
5
- input: Record<string, unknown>,
6
- context: ToolContext,
7
- ): Promise<ToolExecutionResult> {
8
- return executePlaybookDelete(input, context);
7
+ export async function executePlaybookDelete(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
8
+ const playbookId = input.playbook_id as string;
9
+ if (!playbookId || typeof playbookId !== 'string') {
10
+ return { content: 'Error: playbook_id is required and must be a string', isError: true };
11
+ }
12
+
13
+ const scopeId = context.memoryScopeId ?? 'default';
14
+
15
+ try {
16
+ const db = getDb();
17
+
18
+ const existing = db
19
+ .select()
20
+ .from(memoryItems)
21
+ .where(and(
22
+ eq(memoryItems.id, playbookId),
23
+ eq(memoryItems.kind, 'playbook'),
24
+ eq(memoryItems.scopeId, scopeId),
25
+ ))
26
+ .get();
27
+
28
+ if (!existing) {
29
+ return { content: `Error: Playbook with ID "${playbookId}" not found`, isError: true };
30
+ }
31
+
32
+ const playbook = parsePlaybookStatement(existing.statement);
33
+ const triggerLabel = playbook?.trigger ?? existing.subject;
34
+
35
+ // Soft-delete by marking as superseded rather than hard-deleting,
36
+ // consistent with how other memory items are retired.
37
+ // Setting invalidAt so the cleanup job can eventually hard-delete it.
38
+ const now = Date.now();
39
+ db.update(memoryItems)
40
+ .set({ status: 'superseded', invalidAt: now })
41
+ .where(eq(memoryItems.id, existing.id))
42
+ .run();
43
+
44
+ return {
45
+ content: `Playbook deleted (ID: ${existing.id}, trigger: "${triggerLabel}").`,
46
+ isError: false,
47
+ };
48
+ } catch (err) {
49
+ const msg = err instanceof Error ? err.message : String(err);
50
+ return { content: `Error deleting playbook: ${msg}`, isError: true };
51
+ }
9
52
  }
53
+
54
+ export { executePlaybookDelete as run };
@@ -1,9 +1,76 @@
1
+ import { and, desc, eq, isNull } from 'drizzle-orm';
1
2
  import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
2
- import { executePlaybookList } from '../../../../tools/playbooks/playbook-list.js';
3
+ import { getDb } from '../../../../memory/db.js';
4
+ import { memoryItems } from '../../../../memory/schema.js';
5
+ import { parsePlaybookStatement } from '../../../../playbooks/types.js';
3
6
 
4
- export async function run(
5
- input: Record<string, unknown>,
6
- context: ToolContext,
7
- ): Promise<ToolExecutionResult> {
8
- return executePlaybookList(input, context);
7
+ export async function executePlaybookList(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
8
+ const scopeId = context.memoryScopeId ?? 'default';
9
+ const channelFilter = typeof input.channel === 'string' ? input.channel : null;
10
+ const categoryFilter = typeof input.category === 'string' ? input.category : null;
11
+
12
+ try {
13
+ const db = getDb();
14
+
15
+ const rows = db
16
+ .select({
17
+ id: memoryItems.id,
18
+ subject: memoryItems.subject,
19
+ statement: memoryItems.statement,
20
+ importance: memoryItems.importance,
21
+ lastSeenAt: memoryItems.lastSeenAt,
22
+ })
23
+ .from(memoryItems)
24
+ .where(and(
25
+ eq(memoryItems.kind, 'playbook'),
26
+ eq(memoryItems.status, 'active'),
27
+ eq(memoryItems.scopeId, scopeId),
28
+ isNull(memoryItems.invalidAt),
29
+ ))
30
+ .orderBy(desc(memoryItems.importance))
31
+ .all();
32
+
33
+ if (rows.length === 0) {
34
+ return { content: 'No playbooks found.', isError: false };
35
+ }
36
+
37
+ const entries: Array<{ id: string; subject: string; statement: string; playbook: NonNullable<ReturnType<typeof parsePlaybookStatement>> }> = [];
38
+ for (const row of rows) {
39
+ const playbook = parsePlaybookStatement(row.statement);
40
+ if (!playbook) continue;
41
+
42
+ // Apply filters
43
+ if (channelFilter && playbook.channel !== channelFilter && playbook.channel !== '*') continue;
44
+ if (categoryFilter && playbook.category !== categoryFilter) continue;
45
+
46
+ entries.push({ id: row.id, subject: row.subject, statement: row.statement, playbook });
47
+ }
48
+
49
+ if (entries.length === 0) {
50
+ const filters = [
51
+ channelFilter ? `channel="${channelFilter}"` : null,
52
+ categoryFilter ? `category="${categoryFilter}"` : null,
53
+ ].filter(Boolean).join(', ');
54
+ return { content: `No playbooks found matching ${filters}.`, isError: false };
55
+ }
56
+
57
+ // Sort by priority descending
58
+ entries.sort((a, b) => b.playbook.priority - a.playbook.priority);
59
+
60
+ const lines: string[] = [`Found ${entries.length} playbook(s):\n`];
61
+ for (const { id, playbook } of entries) {
62
+ const channelLabel = playbook.channel === '*' ? 'all channels' : playbook.channel;
63
+ const autonomyLabel = playbook.autonomyLevel === 'auto' ? 'auto'
64
+ : playbook.autonomyLevel === 'draft' ? 'draft' : 'notify';
65
+ lines.push(`- **${playbook.trigger}** (${channelLabel}) → ${playbook.action}`);
66
+ lines.push(` _ID: ${id} | category: ${playbook.category} | autonomy: ${autonomyLabel} | priority: ${playbook.priority}_`);
67
+ }
68
+
69
+ return { content: lines.join('\n'), isError: false };
70
+ } catch (err) {
71
+ const msg = err instanceof Error ? err.message : String(err);
72
+ return { content: `Error listing playbooks: ${msg}`, isError: true };
73
+ }
9
74
  }
75
+
76
+ export { executePlaybookList as run };
@@ -1,9 +1,113 @@
1
+ import { and, eq } from 'drizzle-orm';
1
2
  import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
2
- import { executePlaybookUpdate } from '../../../../tools/playbooks/playbook-update.js';
3
+ import { getDb } from '../../../../memory/db.js';
4
+ import { computeMemoryFingerprint } from '../../../../memory/fingerprint.js';
5
+ import { memoryItems } from '../../../../memory/schema.js';
6
+ import { enqueueMemoryJob } from '../../../../memory/jobs-store.js';
7
+ import { parsePlaybookStatement } from '../../../../playbooks/types.js';
8
+ import type { Playbook, PlaybookAutonomyLevel } from '../../../../playbooks/types.js';
9
+ import { truncate } from '../../../../util/truncate.js';
3
10
 
4
- export async function run(
5
- input: Record<string, unknown>,
6
- context: ToolContext,
7
- ): Promise<ToolExecutionResult> {
8
- return executePlaybookUpdate(input, context);
11
+ const VALID_AUTONOMY_LEVELS = new Set<string>(['auto', 'draft', 'notify']);
12
+
13
+ export async function executePlaybookUpdate(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
14
+ const playbookId = input.playbook_id as string;
15
+ if (!playbookId || typeof playbookId !== 'string') {
16
+ return { content: 'Error: playbook_id is required and must be a string', isError: true };
17
+ }
18
+
19
+ const scopeId = context.memoryScopeId ?? 'default';
20
+
21
+ try {
22
+ const db = getDb();
23
+
24
+ const existing = db
25
+ .select()
26
+ .from(memoryItems)
27
+ .where(and(
28
+ eq(memoryItems.id, playbookId),
29
+ eq(memoryItems.kind, 'playbook'),
30
+ eq(memoryItems.scopeId, scopeId),
31
+ ))
32
+ .get();
33
+
34
+ if (!existing) {
35
+ return { content: `Error: Playbook with ID "${playbookId}" not found`, isError: true };
36
+ }
37
+
38
+ const currentPlaybook = parsePlaybookStatement(existing.statement);
39
+ if (!currentPlaybook) {
40
+ return { content: `Error: Playbook data is corrupted for ID "${playbookId}"`, isError: true };
41
+ }
42
+
43
+ // Merge updates onto existing playbook
44
+ const updated: Playbook = {
45
+ trigger: typeof input.trigger === 'string' ? input.trigger : currentPlaybook.trigger,
46
+ channel: typeof input.channel === 'string' ? input.channel : currentPlaybook.channel,
47
+ category: typeof input.category === 'string' ? input.category : currentPlaybook.category,
48
+ action: typeof input.action === 'string' ? input.action : currentPlaybook.action,
49
+ autonomyLevel:
50
+ typeof input.autonomy_level === 'string' && VALID_AUTONOMY_LEVELS.has(input.autonomy_level)
51
+ ? (input.autonomy_level as PlaybookAutonomyLevel)
52
+ : currentPlaybook.autonomyLevel,
53
+ priority: typeof input.priority === 'number' ? input.priority : currentPlaybook.priority,
54
+ };
55
+
56
+ const statement = JSON.stringify(updated);
57
+ const subject = truncate(`Playbook: ${updated.trigger}`, 80, '');
58
+ const now = Date.now();
59
+
60
+ const fingerprint = computeMemoryFingerprint(scopeId, 'playbook', subject, statement);
61
+
62
+ // Check if another playbook already has this fingerprint
63
+ const collision = db
64
+ .select({ id: memoryItems.id })
65
+ .from(memoryItems)
66
+ .where(and(
67
+ eq(memoryItems.fingerprint, fingerprint),
68
+ eq(memoryItems.scopeId, scopeId),
69
+ ))
70
+ .get();
71
+ if (collision && collision.id !== existing.id) {
72
+ return {
73
+ content: `Error: Another playbook with this exact configuration already exists (ID: ${collision.id}).`,
74
+ isError: true,
75
+ };
76
+ }
77
+
78
+ db.update(memoryItems)
79
+ .set({
80
+ subject,
81
+ statement,
82
+ fingerprint,
83
+ lastSeenAt: now,
84
+ verificationState: 'user_confirmed',
85
+ })
86
+ .where(eq(memoryItems.id, existing.id))
87
+ .run();
88
+
89
+ enqueueMemoryJob('embed_item', { itemId: existing.id });
90
+
91
+ const autonomyLabel = updated.autonomyLevel === 'auto' ? 'execute automatically'
92
+ : updated.autonomyLevel === 'draft' ? 'draft for review' : 'notify only';
93
+
94
+ return {
95
+ content: [
96
+ 'Playbook updated successfully.',
97
+ ` ID: ${existing.id}`,
98
+ ` Trigger: ${updated.trigger}`,
99
+ ` Channel: ${updated.channel}`,
100
+ ` Category: ${updated.category}`,
101
+ ` Action: ${updated.action}`,
102
+ ` Autonomy: ${autonomyLabel}`,
103
+ ` Priority: ${updated.priority}`,
104
+ ].join('\n'),
105
+ isError: false,
106
+ };
107
+ } catch (err) {
108
+ const msg = err instanceof Error ? err.message : String(err);
109
+ return { content: `Error updating playbook: ${msg}`, isError: true };
110
+ }
9
111
  }
112
+
113
+ export { executePlaybookUpdate as run };
@@ -68,12 +68,29 @@ ngrok config check
68
68
  If not authenticated:
69
69
 
70
70
  1. Tell the user: "You need an ngrok account to create tunnels. If you don't have one, sign up at https://dashboard.ngrok.com/signup — it's free."
71
- 2. Once they have an account, ask them to paste their auth token directly in chat. They can find it at https://dashboard.ngrok.com/get-started/your-authtoken.
71
+ 2. Once they have an account, use `credential_store` to securely collect their auth token. **Never ask the user to paste the token directly in chat.**
72
72
 
73
- 3. Once the user provides the token, configure ngrok with it immediately:
74
- ```bash
75
- ngrok config add-authtoken <token>
76
- ```
73
+ Use `credential_store` with:
74
+ - action: `prompt`
75
+ - service: `ngrok`
76
+ - field: `authtoken`
77
+ - label: `ngrok Auth Token`
78
+ - description: `Get your auth token from https://dashboard.ngrok.com/get-started/your-authtoken`
79
+ - usage_description: `ngrok authentication token for creating public tunnels`
80
+
81
+ 3. Once the credential is stored, configure ngrok by reading the token directly from the OS keychain and piping it to ngrok so the plaintext never enters the conversation:
82
+
83
+ **macOS:**
84
+ ```bash
85
+ ngrok config add-authtoken "$(security find-generic-password -s vellum-assistant -a credential:ngrok:authtoken -w)"
86
+ ```
87
+
88
+ **Linux:**
89
+ ```bash
90
+ ngrok config add-authtoken "$(secret-tool lookup service vellum-assistant account credential:ngrok:authtoken)"
91
+ ```
92
+
93
+ If the keychain command fails (e.g., headless environment without a keyring), fall back to asking the user to re-enter the token via `credential_store prompt` and then paste it into `ngrok config add-authtoken` manually as a last resort.
77
94
 
78
95
  Verify authentication succeeded by checking `ngrok config check` again.
79
96
 
@@ -1,19 +1,113 @@
1
1
  ---
2
2
  name: "X"
3
- description: "Read and post on X (formerly Twitter) using your authenticated session"
3
+ description: "Read and post on X (formerly Twitter) via OAuth or browser session"
4
4
  user-invocable: true
5
5
  metadata: {"vellum": {"emoji": "𝕏"}}
6
6
  ---
7
7
 
8
8
  You are an X (formerly Twitter) assistant. Use the `execute_bash` tool to run `vellum x` CLI commands.
9
9
 
10
+ ## Connection Options
11
+
12
+ There are two supported ways to connect to X. Both are fully functional; choose whichever fits the user's situation.
13
+
14
+ ### OAuth (recommended with X developer credentials)
15
+
16
+ OAuth uses the official X API v2. It is the most reliable connection method and does not depend on browser sessions.
17
+
18
+ - Supports: **post** and **reply**
19
+ - Read-only operations (timeline, search, home, bookmarks, notifications, likes, followers, following, media) always use the browser path directly, regardless of the strategy setting.
20
+ - Setup: The user connects OAuth credentials through the Settings UI or the `twitter_auth_start` IPC flow.
21
+ - Set the strategy: `vellum x strategy set oauth`
22
+
23
+ ### Browser session (no developer credentials needed)
24
+
25
+ The browser path is quick to start and useful when the user does not have X developer app credentials. It captures auth cookies from Chrome and uses them to interact with X.
26
+
27
+ - Supports: **all operations** (post, reply, timeline, search, home, bookmarks, notifications, likes, followers, following, media)
28
+ - Setup: Run `vellum x refresh` to open Chrome and capture session cookies automatically.
29
+ - Set the strategy: `vellum x strategy set browser`
30
+
31
+ ### Auto mode (default)
32
+
33
+ When the strategy is `auto` (the default), the router tries OAuth first for supported operations if credentials are available, then falls back to the browser path. This gives the best of both worlds without requiring manual switching.
34
+
35
+ - Set auto mode: `vellum x strategy set auto`
36
+
37
+ ## First-Use Decision Flow
38
+
39
+ When the user triggers a Twitter operation and no strategy has been configured yet, follow these steps:
40
+
41
+ 1. **Check current status:**
42
+ ```bash
43
+ vellum x status --json
44
+ ```
45
+ Look at `oauthConnected`, `browserSessionActive`, `preferredStrategy`, and `strategyConfigured` in the response. If `strategyConfigured` is `false`, the user has not yet chosen a strategy and should be guided through setup.
46
+
47
+ 2. **Present both options with trade-offs:**
48
+ - **OAuth**: Most reliable and official. Requires X developer app credentials (OAuth Client ID and optional Client Secret). Supports posting and replying. Set up through Settings UI.
49
+ - **Browser session**: Quick to start, no developer credentials needed. Supports all operations including reading timelines and searching. Set up with `vellum x refresh`.
50
+
51
+ 3. **Ask the user which they prefer.** Do not choose for them.
52
+
53
+ 4. **Execute setup for the chosen path:**
54
+ - If OAuth: Guide the user to the Settings UI to connect their X developer credentials, or initiate the `twitter_auth_start` IPC flow.
55
+ - If browser: Run `vellum x refresh` to capture session cookies from Chrome.
56
+
57
+ 5. **Set the preferred strategy:**
58
+ ```bash
59
+ vellum x strategy set <oauth|browser|auto>
60
+ ```
61
+
62
+ ## Failure Recovery Flow
63
+
64
+ When a Twitter operation fails, follow these steps:
65
+
66
+ 1. **Detect the failure type from the error output:**
67
+ - `session_expired` or `SessionExpiredError` — the browser session cookies have expired.
68
+ - `OAuth is not configured` — the user chose OAuth but credentials are not set up.
69
+ - `Twitter API error (401)` — OAuth token may be expired or revoked.
70
+ - `UnsupportedOAuthOperationError` — the requested write operation is not available via OAuth.
71
+ - `Cannot connect to daemon` — the Vellum daemon is not running.
72
+
73
+ 2. **Explain the likely cause clearly** to the user.
74
+
75
+ 3. **Suggest trying the other path as an alternative:**
76
+ - If the browser session expired: suggest setting up OAuth for post/reply operations, or refresh the browser session with `vellum x refresh`.
77
+ - If OAuth failed or is not configured: suggest using the browser path with `vellum x strategy set browser` and `vellum x refresh`.
78
+ - If the operation is unsupported via OAuth: explain that this write operation is not yet supported via OAuth, and suggest using the browser path with `vellum x strategy set browser`.
79
+
80
+ 4. **Offer concrete steps to switch:**
81
+ ```bash
82
+ # Switch to the other strategy
83
+ vellum x strategy set <oauth|browser|auto>
84
+
85
+ # If switching to browser, refresh the session
86
+ vellum x refresh
87
+ ```
88
+
89
+ ## Strategy Management Commands
90
+
91
+ ```bash
92
+ # Check current strategy
93
+ vellum x strategy
94
+
95
+ # Set strategy to OAuth, browser, or auto
96
+ vellum x strategy set <oauth|browser|auto>
97
+
98
+ # Check full status (session, OAuth, and strategy info)
99
+ vellum x status --json
100
+ ```
101
+
10
102
  ## Posting
11
103
 
12
104
  ```bash
13
105
  vellum x post "The post text here"
14
106
  ```
15
107
 
16
- Returns JSON with `ok`, `tweetId`, `text`, and `url` fields. Share the URL with the user so they can verify the post.
108
+ Returns JSON with `ok`, `tweetId`, `text`, `url`, and `pathUsed` fields. The `pathUsed` field indicates whether the post was sent via `oauth` or `browser`. Share the URL with the user so they can verify the post.
109
+
110
+ The `post` command routes through the strategy router: it uses OAuth if configured and available, otherwise falls back to the browser path.
17
111
 
18
112
  ## Replying
19
113
 
@@ -23,8 +117,12 @@ vellum x reply <tweetUrl> "The reply text here"
23
117
 
24
118
  The first argument is a tweet URL (e.g. `https://x.com/user/status/123456`) or a bare tweet ID.
25
119
 
120
+ Like `post`, the `reply` command routes through the strategy router and returns a `pathUsed` field.
121
+
26
122
  ## Reading
27
123
 
124
+ Read-only operations always use the browser path directly, regardless of the strategy setting. They work the same whether the strategy is `oauth`, `browser`, or `auto` — the strategy only affects `post` and `reply` commands.
125
+
28
126
  ### User timeline
29
127
  ```bash
30
128
  vellum x timeline <screenName> [--count N]
@@ -76,20 +174,6 @@ vellum x media <screenName> [--count N]
76
174
  ```
77
175
  Returns tweets that contain media from the user's profile.
78
176
 
79
- ## Session Management
80
-
81
- Check if a session exists:
82
- ```bash
83
- vellum x status --json
84
- ```
85
-
86
- If there is no session or the session has expired, refresh it:
87
- ```bash
88
- vellum x refresh
89
- ```
90
-
91
- This opens Chrome, navigates through x.com automatically, and captures auth cookies. Do NOT tell the user to run this manually — run it yourself.
92
-
93
177
  ## Workflows
94
178
 
95
179
  ### Check Mentions
@@ -131,4 +215,6 @@ When the user wants to see how their posts are performing:
131
215
  - All commands return JSON with an `ok` field
132
216
  - When drafting replies, match the tone of the conversation — casual threads get casual replies
133
217
  - Always show the user what you're about to post and get approval before sending
134
- - If a session is expired, refresh it silently with `vellum x refresh` before retrying
218
+ - If a browser session is expired, refresh it with `vellum x refresh` before retrying, or suggest switching to OAuth for post/reply operations
219
+ - If an operation fails, check `vellum x status --json` to diagnose the issue before retrying
220
+ - The `post` and `reply` commands include a `pathUsed` field in their response so you can tell the user which connection method was used
@@ -76,6 +76,7 @@ export const DEFAULT_CONFIG: AssistantConfig = {
76
76
  },
77
77
  jobs: {
78
78
  workerConcurrency: 2,
79
+ batchSize: 10,
79
80
  },
80
81
  retention: {
81
82
  keepRawForever: true,
@@ -117,6 +118,8 @@ export const DEFAULT_CONFIG: AssistantConfig = {
117
118
  reaskCooldownTurns: 3,
118
119
  resolverLlmTimeoutMs: 12000,
119
120
  relevanceThreshold: 0.3,
121
+ askOnIrrelevantTurns: false,
122
+ conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
120
123
  },
121
124
  profile: {
122
125
  enabled: true,
@@ -159,7 +162,7 @@ export const DEFAULT_CONFIG: AssistantConfig = {
159
162
  blockIngress: true,
160
163
  },
161
164
  permissions: {
162
- mode: 'strict',
165
+ mode: 'workspace',
163
166
  },
164
167
  auditLog: {
165
168
  retentionDays: 0,
@@ -233,10 +236,10 @@ export const DEFAULT_CONFIG: AssistantConfig = {
233
236
  fallbackToStandardOnError: true,
234
237
  elevenlabs: {
235
238
  voiceId: '',
236
- voiceModelId: 'turbo_v2_5',
239
+ voiceModelId: '',
240
+ speed: 1.0,
237
241
  stability: 0.5,
238
242
  similarityBoost: 0.75,
239
- style: 0.0,
240
243
  useSpeakerBoost: true,
241
244
  agentId: '',
242
245
  apiBaseUrl: 'https://api.elevenlabs.io',
@@ -244,9 +247,12 @@ export const DEFAULT_CONFIG: AssistantConfig = {
244
247
  },
245
248
  },
246
249
  model: undefined,
250
+ callerIdentity: {
251
+ allowPerCallOverride: true,
252
+ },
247
253
  },
248
254
  ingress: {
249
- enabled: false,
255
+ enabled: undefined,
250
256
  publicBaseUrl: '',
251
257
  },
252
258
  };