@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
@@ -1,20 +1,10 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
- import { basename, join } from 'node:path';
3
-
4
1
  import { RiskLevel } from '../../permissions/types.js';
5
2
  import type { ToolDefinition } from '../../providers/types.js';
3
+ import { parseFrontmatterFields } from '../../skills/frontmatter.js';
6
4
  import { createManagedSkill } from '../../skills/managed-store.js';
7
- import { getLogger } from '../../util/logger.js';
5
+ import { fetchCatalogEntries, fetchSkillContent, checkVellumSkill } from '../../skills/vellum-catalog-remote.js';
8
6
  import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
9
7
 
10
- const log = getLogger('vellum-skills-catalog');
11
-
12
- const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
13
-
14
- function getVellumSkillsDir(): string {
15
- return join(import.meta.dir, '..', '..', 'config', 'vellum-skills');
16
- }
17
-
18
8
  export interface CatalogEntry {
19
9
  id: string;
20
10
  name: string;
@@ -23,123 +13,75 @@ export interface CatalogEntry {
23
13
  includes?: string[];
24
14
  }
25
15
 
26
- function parseCatalogEntry(directory: string): CatalogEntry | null {
27
- const skillFilePath = join(directory, 'SKILL.md');
28
- if (!existsSync(skillFilePath)) return null;
29
-
30
- try {
31
- const stat = statSync(skillFilePath);
32
- if (!stat.isFile()) return null;
33
-
34
- const content = readFileSync(skillFilePath, 'utf-8');
35
- const match = content.match(FRONTMATTER_REGEX);
36
- if (!match) return null;
37
-
38
- const fields: Record<string, string> = {};
39
- for (const line of match[1].split(/\r?\n/)) {
40
- const trimmed = line.trim();
41
- if (!trimmed || trimmed.startsWith('#')) continue;
42
- const separatorIndex = trimmed.indexOf(':');
43
- if (separatorIndex === -1) continue;
44
-
45
- const key = trimmed.slice(0, separatorIndex).trim();
46
- let value = trimmed.slice(separatorIndex + 1).trim();
47
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
48
- value = value.slice(1, -1);
49
- }
50
- fields[key] = value;
51
- }
52
-
53
- const name = fields.name?.trim();
54
- const description = fields.description?.trim();
55
- if (!name || !description) return null;
16
+ export { fetchCatalogEntries as listCatalogEntries, checkVellumSkill };
56
17
 
57
- let emoji: string | undefined;
58
- const metadataRaw = fields.metadata?.trim();
59
- if (metadataRaw) {
60
- try {
61
- const parsed = JSON.parse(metadataRaw);
62
- if (parsed?.vellum?.emoji) {
63
- emoji = parsed.vellum.emoji as string;
64
- }
65
- } catch {
66
- // ignore malformed metadata
67
- }
68
- }
69
-
70
- let includes: string[] | undefined;
71
- const includesRaw = fields.includes?.trim();
72
- if (includesRaw) {
73
- try {
74
- const parsed = JSON.parse(includesRaw);
75
- if (Array.isArray(parsed) && parsed.every((item: unknown) => typeof item === 'string')) {
76
- const filtered = (parsed as string[]).map((s) => s.trim()).filter((s) => s.length > 0);
77
- if (filtered.length > 0) includes = filtered;
78
- }
79
- } catch {
80
- // ignore malformed includes
81
- }
82
- }
18
+ /**
19
+ * Install a skill from the vellum-skills catalog by ID.
20
+ * Fetches SKILL.md from GitHub (with bundled fallback) and creates a managed skill.
21
+ * Returns { success, skillName, error }.
22
+ */
23
+ export async function installFromVellumCatalog(skillId: string, options?: { overwrite?: boolean }): Promise<{ success: boolean; skillName?: string; error?: string }> {
24
+ const trimmedId = skillId.trim();
83
25
 
84
- return { id: basename(directory), name, description, emoji, includes };
85
- } catch (err) {
86
- log.warn({ err, directory }, 'Failed to read catalog entry');
87
- return null;
26
+ // Verify skill exists in catalog
27
+ const exists = await checkVellumSkill(trimmedId);
28
+ if (!exists) {
29
+ return { success: false, error: `Skill "${trimmedId}" not found in the Vellum catalog` };
88
30
  }
89
- }
90
-
91
- export function listCatalogEntries(): CatalogEntry[] {
92
- const catalogDir = getVellumSkillsDir();
93
- if (!existsSync(catalogDir)) return [];
94
31
 
95
- const entries: CatalogEntry[] = [];
96
- try {
97
- const dirEntries = readdirSync(catalogDir, { withFileTypes: true });
98
- for (const entry of dirEntries) {
99
- if (!entry.isDirectory()) continue;
100
- const parsed = parseCatalogEntry(join(catalogDir, entry.name));
101
- if (parsed) entries.push(parsed);
102
- }
103
- } catch (err) {
104
- log.warn({ err, catalogDir }, 'Failed to list catalog entries');
32
+ // Fetch SKILL.md content (remote with bundled fallback)
33
+ const content = await fetchSkillContent(trimmedId);
34
+ if (!content) {
35
+ return { success: false, error: `Skill "${trimmedId}" SKILL.md not found` };
105
36
  }
106
37
 
107
- return entries.sort((a, b) => a.id.localeCompare(b.id));
108
- }
38
+ const parsed = parseFrontmatterFields(content);
39
+ if (!parsed) {
40
+ return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md` };
41
+ }
109
42
 
110
- /**
111
- * Install a skill from the vellum-skills catalog by ID.
112
- * Returns { success, skillName, error }.
113
- */
114
- export function installFromVellumCatalog(skillId: string): { success: boolean; skillName?: string; error?: string } {
115
- const catalogDir = getVellumSkillsDir();
116
- const skillDir = join(catalogDir, skillId.trim());
117
- const skillFilePath = join(skillDir, 'SKILL.md');
43
+ const { fields, body: bodyMarkdown } = parsed;
118
44
 
119
- if (!existsSync(skillFilePath)) {
120
- return { success: false, error: `Skill "${skillId}" not found in the Vellum catalog` };
45
+ const name = fields.name?.trim();
46
+ const description = fields.description?.trim();
47
+ if (!name || !description) {
48
+ return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md (missing name or description)` };
121
49
  }
122
50
 
123
- const content = readFileSync(skillFilePath, 'utf-8');
124
- const match = content.match(FRONTMATTER_REGEX);
125
- if (!match) {
126
- return { success: false, error: `Skill "${skillId}" has invalid SKILL.md` };
51
+ let emoji: string | undefined;
52
+ const metadataRaw = fields.metadata?.trim();
53
+ if (metadataRaw) {
54
+ try {
55
+ const metaObj = JSON.parse(metadataRaw);
56
+ if (metaObj?.vellum?.emoji) {
57
+ emoji = metaObj.vellum.emoji as string;
58
+ }
59
+ } catch {
60
+ // ignore malformed metadata
61
+ }
127
62
  }
128
63
 
129
- const entry = parseCatalogEntry(skillDir);
130
- if (!entry) {
131
- return { success: false, error: `Skill "${skillId}" has invalid SKILL.md` };
64
+ let includes: string[] | undefined;
65
+ const includesRaw = fields.includes?.trim();
66
+ if (includesRaw) {
67
+ try {
68
+ const includesObj = JSON.parse(includesRaw);
69
+ if (Array.isArray(includesObj) && includesObj.every((item: unknown) => typeof item === 'string')) {
70
+ const filtered = (includesObj as string[]).map((s) => s.trim()).filter((s) => s.length > 0);
71
+ if (filtered.length > 0) includes = filtered;
72
+ }
73
+ } catch {
74
+ // ignore malformed includes
75
+ }
132
76
  }
133
-
134
- const bodyMarkdown = content.slice(match[0].length);
135
77
  const result = createManagedSkill({
136
- id: entry.id,
137
- name: entry.name,
138
- description: entry.description,
78
+ id: trimmedId,
79
+ name,
80
+ description,
139
81
  bodyMarkdown,
140
- emoji: entry.emoji,
141
- includes: entry.includes,
142
- overwrite: true,
82
+ emoji,
83
+ includes,
84
+ overwrite: options?.overwrite ?? true,
143
85
  addToIndex: true,
144
86
  });
145
87
 
@@ -147,7 +89,7 @@ export function installFromVellumCatalog(skillId: string): { success: boolean; s
147
89
  return { success: false, error: result.error };
148
90
  }
149
91
 
150
- return { success: true, skillName: entry.id };
92
+ return { success: true, skillName: trimmedId };
151
93
  }
152
94
 
153
95
  class VellumSkillsCatalogTool implements Tool {
@@ -187,7 +129,7 @@ class VellumSkillsCatalogTool implements Tool {
187
129
 
188
130
  switch (action) {
189
131
  case 'list': {
190
- const entries = listCatalogEntries();
132
+ const entries = await fetchCatalogEntries();
191
133
  if (entries.length === 0) {
192
134
  return { content: 'No Vellum-provided skills available in the catalog.', isError: false };
193
135
  }
@@ -200,52 +142,15 @@ class VellumSkillsCatalogTool implements Tool {
200
142
  return { content: 'Error: skill_id is required for install action', isError: true };
201
143
  }
202
144
 
203
- const catalogDir = getVellumSkillsDir();
204
- const skillDir = join(catalogDir, skillId.trim());
205
- const skillFilePath = join(skillDir, 'SKILL.md');
206
-
207
- if (!existsSync(skillFilePath)) {
208
- const available = listCatalogEntries().map((e) => e.id);
209
- return {
210
- content: `Error: skill "${skillId}" not found in the Vellum catalog. Available: ${available.join(', ') || 'none'}`,
211
- isError: true,
212
- };
213
- }
214
-
215
- const content = readFileSync(skillFilePath, 'utf-8');
216
- const match = content.match(FRONTMATTER_REGEX);
217
- if (!match) {
218
- return { content: `Error: skill "${skillId}" has invalid SKILL.md (missing frontmatter)`, isError: true };
219
- }
220
-
221
- const entry = parseCatalogEntry(skillDir);
222
- if (!entry) {
223
- return { content: `Error: skill "${skillId}" has invalid SKILL.md`, isError: true };
224
- }
225
-
226
- const bodyMarkdown = content.slice(match[0].length);
227
-
228
- const result = createManagedSkill({
229
- id: entry.id,
230
- name: entry.name,
231
- description: entry.description,
232
- bodyMarkdown,
233
- emoji: entry.emoji,
234
- includes: entry.includes,
235
- overwrite: input.overwrite === true,
236
- addToIndex: true,
237
- });
238
-
239
- if (!result.created) {
145
+ const result = await installFromVellumCatalog(skillId, { overwrite: input.overwrite === true });
146
+ if (!result.success) {
240
147
  return { content: `Error: ${result.error}`, isError: true };
241
148
  }
242
149
 
243
150
  return {
244
151
  content: JSON.stringify({
245
152
  installed: true,
246
- skill_id: entry.id,
247
- name: entry.name,
248
- path: result.path,
153
+ skill_id: result.skillName,
249
154
  }),
250
155
  isError: false,
251
156
  };
@@ -36,7 +36,7 @@ export interface ParsedCommand {
36
36
  }
37
37
 
38
38
  const SHELL_PROGRAMS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'fish']);
39
- const OPAQUE_PROGRAMS = new Set(['eval', 'source']);
39
+ const OPAQUE_PROGRAMS = new Set(['eval', 'source', 'alias']);
40
40
  const DANGEROUS_ENV_VARS = new Set([
41
41
  'LD_PRELOAD', 'LD_LIBRARY_PATH',
42
42
  'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH', 'DYLD_FRAMEWORK_PATH',
@@ -89,17 +89,33 @@ const initGuard = new PromiseGuard<void>();
89
89
  */
90
90
  function findWasmPath(pkg: string, file: string): string {
91
91
  const dir = import.meta.dirname ?? __dirname;
92
- const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
93
92
 
94
- if (!existsSync(sourcePath) && dir.startsWith('/$bunfs/')) {
93
+ // In compiled Bun binaries, import.meta.dirname points into the virtual
94
+ // /$bunfs/ filesystem. Prefer bundled WASM assets shipped alongside the
95
+ // executable before falling back to process.cwd(), so we never accidentally
96
+ // pick up a mismatched version from the working directory.
97
+ if (dir.startsWith('/$bunfs/')) {
95
98
  const execDir = dirname(process.execPath);
96
99
  // macOS .app bundle: binary is in Contents/MacOS/, resources in Contents/Resources/
97
100
  const resourcesPath = join(execDir, '..', 'Resources', file);
98
101
  if (existsSync(resourcesPath)) return resourcesPath;
99
- // Fallback: next to the binary itself (non-app-bundle deployments)
100
- return join(execDir, file);
102
+ // Next to the binary itself (non-app-bundle deployments)
103
+ const execDirPath = join(execDir, file);
104
+ if (existsSync(execDirPath)) return execDirPath;
105
+ // Last resort: resolve from process.cwd() (the assistant/ directory)
106
+ const cwdPath = join(process.cwd(), 'node_modules', pkg, file);
107
+ if (existsSync(cwdPath)) return cwdPath;
108
+ return execDirPath;
101
109
  }
102
110
 
111
+ const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
112
+
113
+ if (existsSync(sourcePath)) return sourcePath;
114
+
115
+ // Fallback: resolve from process.cwd() (the assistant/ directory).
116
+ const cwdPath = join(process.cwd(), 'node_modules', pkg, file);
117
+ if (existsSync(cwdPath)) return cwdPath;
118
+
103
119
  return sourcePath;
104
120
  }
105
121
 
@@ -87,6 +87,8 @@ export interface ToolContext {
87
87
  workingDir: string;
88
88
  sessionId: string;
89
89
  conversationId: string;
90
+ /** Logical assistant scope for multi-assistant routing. */
91
+ assistantId?: string;
90
92
  /** When set, the tool execution is part of a task run. Used to retrieve ephemeral permission rules. */
91
93
  taskRunId?: string;
92
94
  /** Per-message request ID for log correlation across session/connection boundaries. */
@@ -41,7 +41,7 @@ export async function routedPostTweet(
41
41
  if (strategy === 'oauth') {
42
42
  // User explicitly wants OAuth
43
43
  if (!oauthIsAvailable()) {
44
- throw Object.assign(new Error('OAuth is not configured. Set up OAuth credentials in Settings, or switch to browser strategy: `vellum x strategy set browser`.'), {
44
+ throw Object.assign(new Error('OAuth is not configured. Provide your X developer credentials here in the chat to set up OAuth, or switch to browser strategy: `vellum x strategy set browser`.'), {
45
45
  pathUsed: 'oauth' as const,
46
46
  suggestAlternative: 'browser' as const,
47
47
  });
@@ -1,4 +1,4 @@
1
- import { mkdirSync, existsSync, statSync, unlinkSync, renameSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
1
+ import { mkdirSync, existsSync, statSync, unlinkSync, renameSync, readFileSync, writeFileSync, readdirSync, chmodSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  /**
@@ -45,6 +45,41 @@ export function getClipboardCommand(): string | null {
45
45
  return null;
46
46
  }
47
47
 
48
+ /**
49
+ * Read and parse the lockfile, trying the primary path (~/.vellum.lock.json)
50
+ * first, then falling back to the legacy path (~/.vellum.lockfile.json).
51
+ * Respects BASE_DATA_DIR for non-standard home directories.
52
+ * Returns null if neither file exists or both are malformed.
53
+ */
54
+ export function readLockfile(): Record<string, unknown> | null {
55
+ const base = process.env.BASE_DATA_DIR?.trim() || homedir();
56
+ const candidates = [
57
+ join(base, '.vellum.lock.json'),
58
+ join(base, '.vellum.lockfile.json'),
59
+ ];
60
+ for (const lockPath of candidates) {
61
+ if (!existsSync(lockPath)) continue;
62
+ try {
63
+ const raw = JSON.parse(readFileSync(lockPath, 'utf-8'));
64
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
65
+ return raw as Record<string, unknown>;
66
+ }
67
+ } catch {
68
+ // malformed JSON — try next
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Write data to the primary lockfile (~/.vellum.lock.json).
76
+ * Respects BASE_DATA_DIR for non-standard home directories.
77
+ */
78
+ export function writeLockfile(data: Record<string, unknown>): void {
79
+ const base = process.env.BASE_DATA_DIR?.trim() || homedir();
80
+ writeFileSync(join(base, '.vellum.lock.json'), JSON.stringify(data, null, 2) + '\n');
81
+ }
82
+
48
83
  /**
49
84
  * Returns the root ~/.vellum directory. User-facing files (config, prompt
50
85
  * files, skills) and runtime files (socket, PID) live here.
@@ -565,6 +600,13 @@ export function ensureDataDir(): void {
565
600
  mkdirSync(dir, { recursive: true });
566
601
  }
567
602
  }
603
+ // Lock down the root directory so only the owner can traverse it.
604
+ // Runtime files (socket, session token, PID) live directly under root.
605
+ try {
606
+ chmodSync(root, 0o700);
607
+ } catch {
608
+ // Non-fatal: some filesystems don't support Unix permissions
609
+ }
568
610
  }
569
611
 
570
612
  /**
package/src/util/retry.ts CHANGED
@@ -58,10 +58,10 @@ export function getHttpRetryDelay(
58
58
  const parsed = parseRetryAfterMs(retryAfter);
59
59
  if (parsed !== undefined) return parsed;
60
60
  }
61
- // Enforce a minimum floor of baseDelayMs when Retry-After is missing.
62
- // computeRetryDelay uses equal jitter (cap/2 + random*cap/2) which can
63
- // dip to ~500ms on attempt 0 too aggressive for 429 rate limits.
64
- return Math.max(baseDelayMs, computeRetryDelay(attempt, baseDelayMs));
61
+ // For attempt 0, double the base so jitter range [baseDelayMs, 2*baseDelayMs) stays above the floor.
62
+ // For attempt >= 1, use the original base — jitter is already above baseDelayMs.
63
+ const effectiveBase = attempt === 0 ? baseDelayMs * 2 : baseDelayMs;
64
+ return Math.max(baseDelayMs, computeRetryDelay(attempt, effectiveBase));
65
65
  }
66
66
 
67
67
  /**