@vellumai/assistant 0.3.2 → 0.3.4

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 (109) hide show
  1. package/README.md +82 -21
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
  4. package/src/__tests__/app-git-history.test.ts +22 -27
  5. package/src/__tests__/app-git-service.test.ts +44 -78
  6. package/src/__tests__/call-orchestrator.test.ts +321 -0
  7. package/src/__tests__/channel-approval-routes.test.ts +1267 -93
  8. package/src/__tests__/channel-approval.test.ts +2 -0
  9. package/src/__tests__/channel-approvals.test.ts +51 -2
  10. package/src/__tests__/channel-delivery-store.test.ts +130 -1
  11. package/src/__tests__/channel-guardian.test.ts +371 -1
  12. package/src/__tests__/config-schema.test.ts +1 -1
  13. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  14. package/src/__tests__/daemon-lifecycle.test.ts +635 -0
  15. package/src/__tests__/daemon-server-session-init.test.ts +5 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
  17. package/src/__tests__/handlers-telegram-config.test.ts +82 -0
  18. package/src/__tests__/handlers-twilio-config.test.ts +738 -5
  19. package/src/__tests__/ingress-url-consistency.test.ts +64 -0
  20. package/src/__tests__/ipc-snapshot.test.ts +10 -0
  21. package/src/__tests__/run-orchestrator.test.ts +1 -1
  22. package/src/__tests__/secret-scanner.test.ts +223 -0
  23. package/src/__tests__/session-process-bridge.test.ts +2 -0
  24. package/src/__tests__/shell-parser-property.test.ts +357 -2
  25. package/src/__tests__/system-prompt.test.ts +25 -1
  26. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  27. package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
  28. package/src/__tests__/user-reference.test.ts +68 -0
  29. package/src/calls/call-orchestrator.ts +63 -11
  30. package/src/calls/twilio-config.ts +10 -1
  31. package/src/calls/twilio-rest.ts +70 -0
  32. package/src/cli/map.ts +6 -0
  33. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  34. package/src/commands/cc-command-registry.ts +14 -1
  35. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  36. package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
  37. package/src/config/bundled-skills/messaging/SKILL.md +4 -0
  38. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  39. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  40. package/src/config/defaults.ts +1 -1
  41. package/src/config/schema.ts +6 -3
  42. package/src/config/skills.ts +5 -32
  43. package/src/config/system-prompt.ts +16 -0
  44. package/src/config/user-reference.ts +29 -0
  45. package/src/config/vellum-skills/catalog.json +52 -0
  46. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  47. package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
  48. package/src/daemon/auth-manager.ts +103 -0
  49. package/src/daemon/computer-use-session.ts +8 -1
  50. package/src/daemon/config-watcher.ts +253 -0
  51. package/src/daemon/handlers/config.ts +193 -17
  52. package/src/daemon/handlers/sessions.ts +5 -3
  53. package/src/daemon/handlers/skills.ts +60 -17
  54. package/src/daemon/ipc-contract-inventory.json +4 -0
  55. package/src/daemon/ipc-contract.ts +16 -0
  56. package/src/daemon/ipc-handler.ts +87 -0
  57. package/src/daemon/lifecycle.ts +16 -4
  58. package/src/daemon/ride-shotgun-handler.ts +11 -1
  59. package/src/daemon/server.ts +105 -502
  60. package/src/daemon/session-agent-loop.ts +9 -14
  61. package/src/daemon/session-process.ts +20 -3
  62. package/src/daemon/session-runtime-assembly.ts +60 -44
  63. package/src/daemon/session-slash.ts +50 -2
  64. package/src/daemon/session-surfaces.ts +17 -1
  65. package/src/daemon/session.ts +8 -1
  66. package/src/inbound/public-ingress-urls.ts +20 -3
  67. package/src/index.ts +1 -23
  68. package/src/memory/app-git-service.ts +24 -0
  69. package/src/memory/app-store.ts +0 -21
  70. package/src/memory/channel-delivery-store.ts +74 -3
  71. package/src/memory/channel-guardian-store.ts +54 -26
  72. package/src/memory/conversation-key-store.ts +20 -0
  73. package/src/memory/conversation-store.ts +14 -2
  74. package/src/memory/db-connection.ts +28 -0
  75. package/src/memory/db-init.ts +1019 -0
  76. package/src/memory/db.ts +2 -1995
  77. package/src/memory/embedding-backend.ts +79 -11
  78. package/src/memory/indexer.ts +2 -0
  79. package/src/memory/job-utils.ts +64 -4
  80. package/src/memory/jobs-worker.ts +7 -1
  81. package/src/memory/recall-cache.ts +107 -0
  82. package/src/memory/retriever.ts +30 -1
  83. package/src/memory/schema-migration.ts +984 -0
  84. package/src/memory/schema.ts +6 -0
  85. package/src/memory/search/types.ts +2 -0
  86. package/src/permissions/prompter.ts +14 -3
  87. package/src/permissions/trust-store.ts +7 -0
  88. package/src/runtime/channel-approvals.ts +17 -3
  89. package/src/runtime/gateway-client.ts +2 -1
  90. package/src/runtime/http-server.ts +28 -9
  91. package/src/runtime/routes/channel-routes.ts +279 -100
  92. package/src/runtime/routes/run-routes.ts +7 -1
  93. package/src/runtime/run-orchestrator.ts +8 -1
  94. package/src/security/secret-scanner.ts +218 -0
  95. package/src/skills/clawhub.ts +6 -2
  96. package/src/skills/frontmatter.ts +63 -0
  97. package/src/skills/slash-commands.ts +23 -0
  98. package/src/skills/vellum-catalog-remote.ts +107 -0
  99. package/src/subagent/manager.ts +4 -1
  100. package/src/subagent/types.ts +2 -0
  101. package/src/tools/browser/auto-navigate.ts +132 -24
  102. package/src/tools/browser/browser-manager.ts +67 -61
  103. package/src/tools/claude-code/claude-code.ts +55 -3
  104. package/src/tools/executor.ts +10 -2
  105. package/src/tools/skills/vellum-catalog.ts +75 -127
  106. package/src/tools/subagent/spawn.ts +2 -0
  107. package/src/tools/terminal/parser.ts +21 -5
  108. package/src/util/platform.ts +8 -1
  109. package/src/util/retry.ts +4 -4
@@ -1,21 +1,11 @@
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
- interface CatalogEntry {
8
+ export interface CatalogEntry {
19
9
  id: string;
20
10
  name: string;
21
11
  description: string;
@@ -23,88 +13,83 @@ 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
- }
16
+ export { fetchCatalogEntries as listCatalogEntries, checkVellumSkill };
52
17
 
53
- const name = fields.name?.trim();
54
- const description = fields.description?.trim();
55
- if (!name || !description) return null;
56
-
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
- }
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();
69
25
 
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
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` };
30
+ }
31
+
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` };
36
+ }
37
+
38
+ const parsed = parseFrontmatterFields(content);
39
+ if (!parsed) {
40
+ return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md` };
41
+ }
42
+
43
+ const { fields, body: bodyMarkdown } = parsed;
44
+
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)` };
49
+ }
50
+
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;
81
58
  }
59
+ } catch {
60
+ // ignore malformed metadata
82
61
  }
83
-
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;
88
62
  }
89
- }
90
63
 
91
- function listCatalogEntries(): CatalogEntry[] {
92
- const catalogDir = getVellumSkillsDir();
93
- if (!existsSync(catalogDir)) return [];
94
-
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);
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
102
75
  }
103
- } catch (err) {
104
- log.warn({ err, catalogDir }, 'Failed to list catalog entries');
76
+ }
77
+ const result = createManagedSkill({
78
+ id: trimmedId,
79
+ name,
80
+ description,
81
+ bodyMarkdown,
82
+ emoji,
83
+ includes,
84
+ overwrite: options?.overwrite ?? true,
85
+ addToIndex: true,
86
+ });
87
+
88
+ if (!result.created) {
89
+ return { success: false, error: result.error };
105
90
  }
106
91
 
107
- return entries.sort((a, b) => a.id.localeCompare(b.id));
92
+ return { success: true, skillName: trimmedId };
108
93
  }
109
94
 
110
95
  class VellumSkillsCatalogTool implements Tool {
@@ -144,7 +129,7 @@ class VellumSkillsCatalogTool implements Tool {
144
129
 
145
130
  switch (action) {
146
131
  case 'list': {
147
- const entries = listCatalogEntries();
132
+ const entries = await fetchCatalogEntries();
148
133
  if (entries.length === 0) {
149
134
  return { content: 'No Vellum-provided skills available in the catalog.', isError: false };
150
135
  }
@@ -157,52 +142,15 @@ class VellumSkillsCatalogTool implements Tool {
157
142
  return { content: 'Error: skill_id is required for install action', isError: true };
158
143
  }
159
144
 
160
- const catalogDir = getVellumSkillsDir();
161
- const skillDir = join(catalogDir, skillId.trim());
162
- const skillFilePath = join(skillDir, 'SKILL.md');
163
-
164
- if (!existsSync(skillFilePath)) {
165
- const available = listCatalogEntries().map((e) => e.id);
166
- return {
167
- content: `Error: skill "${skillId}" not found in the Vellum catalog. Available: ${available.join(', ') || 'none'}`,
168
- isError: true,
169
- };
170
- }
171
-
172
- const content = readFileSync(skillFilePath, 'utf-8');
173
- const match = content.match(FRONTMATTER_REGEX);
174
- if (!match) {
175
- return { content: `Error: skill "${skillId}" has invalid SKILL.md (missing frontmatter)`, isError: true };
176
- }
177
-
178
- const entry = parseCatalogEntry(skillDir);
179
- if (!entry) {
180
- return { content: `Error: skill "${skillId}" has invalid SKILL.md`, isError: true };
181
- }
182
-
183
- const bodyMarkdown = content.slice(match[0].length);
184
-
185
- const result = createManagedSkill({
186
- id: entry.id,
187
- name: entry.name,
188
- description: entry.description,
189
- bodyMarkdown,
190
- emoji: entry.emoji,
191
- includes: entry.includes,
192
- overwrite: input.overwrite === true,
193
- addToIndex: true,
194
- });
195
-
196
- if (!result.created) {
145
+ const result = await installFromVellumCatalog(skillId, { overwrite: input.overwrite === true });
146
+ if (!result.success) {
197
147
  return { content: `Error: ${result.error}`, isError: true };
198
148
  }
199
149
 
200
150
  return {
201
151
  content: JSON.stringify({
202
152
  installed: true,
203
- skill_id: entry.id,
204
- name: entry.name,
205
- path: result.path,
153
+ skill_id: result.skillName,
206
154
  }),
207
155
  isError: false,
208
156
  };
@@ -8,6 +8,7 @@ export async function executeSubagentSpawn(
8
8
  const label = input.label as string;
9
9
  const objective = input.objective as string;
10
10
  const extraContext = input.context as string | undefined;
11
+ const sendResultToUser = input.send_result_to_user !== false;
11
12
 
12
13
  if (!label || !objective) {
13
14
  return { content: 'Both "label" and "objective" are required.', isError: true };
@@ -26,6 +27,7 @@ export async function executeSubagentSpawn(
26
27
  label,
27
28
  objective,
28
29
  context: extraContext,
30
+ sendResultToUser,
29
31
  },
30
32
  sendToClient as (msg: unknown) => void,
31
33
  );
@@ -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
 
@@ -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
  /**
@@ -565,6 +565,13 @@ export function ensureDataDir(): void {
565
565
  mkdirSync(dir, { recursive: true });
566
566
  }
567
567
  }
568
+ // Lock down the root directory so only the owner can traverse it.
569
+ // Runtime files (socket, session token, PID) live directly under root.
570
+ try {
571
+ chmodSync(root, 0o700);
572
+ } catch {
573
+ // Non-fatal: some filesystems don't support Unix permissions
574
+ }
568
575
  }
569
576
 
570
577
  /**
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
  /**