@vellumai/assistant 0.4.10 → 0.4.12

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 (203) hide show
  1. package/ARCHITECTURE.md +418 -378
  2. package/Dockerfile +1 -1
  3. package/README.md +16 -9
  4. package/package.json +1 -1
  5. package/src/__tests__/account-registry.test.ts +1 -0
  6. package/src/__tests__/actor-token-service.test.ts +1 -0
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  8. package/src/__tests__/asset-materialize-tool.test.ts +7 -0
  9. package/src/__tests__/asset-search-tool.test.ts +7 -0
  10. package/src/__tests__/browser-fill-credential.test.ts +1 -0
  11. package/src/__tests__/call-start-guardian-guard.test.ts +1 -0
  12. package/src/__tests__/channel-approval-routes.test.ts +29 -0
  13. package/src/__tests__/channel-guardian.test.ts +2143 -1546
  14. package/src/__tests__/channel-retry-sweep.test.ts +169 -14
  15. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -0
  16. package/src/__tests__/computer-use-tools.test.ts +1 -0
  17. package/src/__tests__/contacts-tools.test.ts +1 -0
  18. package/src/__tests__/conversation-attention-telegram.test.ts +1 -0
  19. package/src/__tests__/credential-policy-validate.test.ts +97 -0
  20. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  21. package/src/__tests__/credential-vault-unit.test.ts +1 -0
  22. package/src/__tests__/credential-vault.test.ts +1 -0
  23. package/src/__tests__/delete-managed-skill-tool.test.ts +1 -0
  24. package/src/__tests__/file-edit-tool.test.ts +1 -0
  25. package/src/__tests__/file-read-tool.test.ts +1 -0
  26. package/src/__tests__/file-write-tool.test.ts +1 -0
  27. package/src/__tests__/followup-tools.test.ts +1 -0
  28. package/src/__tests__/gateway-only-guard.test.ts +1 -1
  29. package/src/__tests__/guardian-control-plane-policy.test.ts +5 -4
  30. package/src/__tests__/guardian-grant-minting.test.ts +3 -0
  31. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +4 -3
  32. package/src/__tests__/guardian-routing-state.test.ts +8 -0
  33. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  34. package/src/__tests__/headless-browser-interactions.test.ts +1 -0
  35. package/src/__tests__/headless-browser-navigate.test.ts +1 -0
  36. package/src/__tests__/headless-browser-read-tools.test.ts +1 -0
  37. package/src/__tests__/headless-browser-snapshot.test.ts +1 -0
  38. package/src/__tests__/host-file-edit-tool.test.ts +1 -0
  39. package/src/__tests__/host-file-read-tool.test.ts +1 -0
  40. package/src/__tests__/host-file-write-tool.test.ts +1 -0
  41. package/src/__tests__/host-shell-tool.test.ts +1 -0
  42. package/src/__tests__/lifecycle-docs-guard.test.ts +207 -0
  43. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -0
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +8 -0
  45. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  46. package/src/__tests__/playbook-execution.test.ts +1 -0
  47. package/src/__tests__/playbook-tools.test.ts +1 -0
  48. package/src/__tests__/registry.test.ts +235 -187
  49. package/src/__tests__/relay-server.test.ts +4 -0
  50. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -0
  51. package/src/__tests__/schedule-tools.test.ts +1 -0
  52. package/src/__tests__/secret-onetime-send.test.ts +4 -0
  53. package/src/__tests__/secret-scanner-executor.test.ts +2 -0
  54. package/src/__tests__/secure-keys.test.ts +27 -0
  55. package/src/__tests__/send-notification-tool.test.ts +2 -0
  56. package/src/__tests__/session-agent-loop.test.ts +521 -256
  57. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  58. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  59. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  60. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  61. package/src/__tests__/shell-credential-ref.test.ts +1 -0
  62. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -0
  63. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  64. package/src/__tests__/skill-load-tool.test.ts +1 -0
  65. package/src/__tests__/skill-script-runner-host.test.ts +1 -0
  66. package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -0
  67. package/src/__tests__/skill-script-runner.test.ts +1 -0
  68. package/src/__tests__/skill-tool-factory.test.ts +1 -0
  69. package/src/__tests__/skills.test.ts +334 -276
  70. package/src/__tests__/starter-task-flow.test.ts +7 -17
  71. package/src/__tests__/subagent-tools.test.ts +1 -1
  72. package/src/__tests__/swarm-recursion.test.ts +1 -0
  73. package/src/__tests__/swarm-session-integration.test.ts +1 -0
  74. package/src/__tests__/swarm-tool.test.ts +1 -0
  75. package/src/__tests__/task-management-tools.test.ts +1 -0
  76. package/src/__tests__/task-tools.test.ts +1 -0
  77. package/src/__tests__/terminal-tools.test.ts +1 -0
  78. package/src/__tests__/tool-approval-handler.test.ts +2 -2
  79. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  81. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  82. package/src/__tests__/tool-executor-shell-integration.test.ts +1 -0
  83. package/src/__tests__/tool-executor.test.ts +1 -0
  84. package/src/__tests__/trust-context-guards.test.ts +218 -0
  85. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
  86. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +6 -0
  87. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -0
  88. package/src/__tests__/trusted-contact-verification.test.ts +1 -0
  89. package/src/__tests__/view-image-tool.test.ts +1 -0
  90. package/src/agent/loop.ts +9 -2
  91. package/src/calls/guardian-dispatch.ts +4 -4
  92. package/src/cli/mcp.ts +183 -3
  93. package/src/config/bundled-skills/agentmail/SKILL.md +4 -4
  94. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  95. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  96. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  97. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  98. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  99. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  100. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  101. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  102. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  103. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  104. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  105. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  106. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  107. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  108. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  109. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  110. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  111. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  112. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  113. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  114. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  115. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  116. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  117. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +42 -41
  118. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  119. package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  121. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  122. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +10 -3
  123. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  124. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  125. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -1
  126. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  127. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  128. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  129. package/src/config/bundled-skills/phone-calls/SKILL.md +91 -162
  130. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  131. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  132. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  133. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +21 -6
  134. package/src/config/bundled-tool-registry.ts +281 -267
  135. package/src/config/system-prompt.ts +4 -2
  136. package/src/daemon/computer-use-session.ts +1 -0
  137. package/src/daemon/handlers/skills.ts +334 -234
  138. package/src/daemon/ipc-contract/messages.ts +2 -0
  139. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  140. package/src/daemon/lifecycle.ts +358 -221
  141. package/src/daemon/response-tier.ts +2 -0
  142. package/src/daemon/server.ts +453 -193
  143. package/src/daemon/session-agent-loop-handlers.ts +42 -2
  144. package/src/daemon/session-agent-loop.ts +4 -1
  145. package/src/daemon/session-lifecycle.ts +3 -0
  146. package/src/daemon/session-memory.ts +2 -2
  147. package/src/daemon/session-process.ts +1 -0
  148. package/src/daemon/session-runtime-assembly.ts +2 -2
  149. package/src/daemon/session-surfaces.ts +22 -20
  150. package/src/daemon/session-tool-setup.ts +2 -1
  151. package/src/daemon/session.ts +5 -2
  152. package/src/mcp/client.ts +55 -6
  153. package/src/mcp/manager.ts +9 -0
  154. package/src/mcp/mcp-oauth-provider.ts +347 -0
  155. package/src/memory/channel-delivery-store.ts +1 -0
  156. package/src/memory/db-init.ts +4 -0
  157. package/src/memory/delivery-status.ts +43 -0
  158. package/src/memory/guardian-bindings.ts +3 -3
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +108 -0
  160. package/src/memory/migrations/index.ts +1 -0
  161. package/src/memory/migrations/registry.ts +6 -0
  162. package/src/memory/schema.ts +1 -1
  163. package/src/messaging/outreach-classifier.ts +12 -5
  164. package/src/messaging/provider-types.ts +2 -0
  165. package/src/messaging/providers/gmail/adapter.ts +9 -3
  166. package/src/messaging/providers/gmail/client.ts +2 -0
  167. package/src/runtime/actor-trust-resolver.ts +13 -4
  168. package/src/runtime/channel-retry-sweep.ts +31 -14
  169. package/src/runtime/guardian-context-resolver.ts +25 -64
  170. package/src/runtime/guardian-outbound-actions.ts +399 -108
  171. package/src/runtime/guardian-vellum-migration.ts +1 -23
  172. package/src/runtime/guardian-verification-templates.ts +66 -30
  173. package/src/runtime/http-errors.ts +33 -20
  174. package/src/runtime/http-server.ts +706 -291
  175. package/src/runtime/http-types.ts +26 -16
  176. package/src/runtime/local-actor-identity.ts +4 -6
  177. package/src/runtime/middleware/actor-token.ts +2 -8
  178. package/src/runtime/routes/channel-route-shared.ts +0 -1
  179. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  180. package/src/runtime/routes/secret-routes.ts +57 -2
  181. package/src/runtime/routes/surface-action-routes.ts +66 -0
  182. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  183. package/src/runtime/tool-grant-request-helper.ts +1 -1
  184. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  185. package/src/security/secure-keys.ts +17 -0
  186. package/src/skills/frontmatter.ts +9 -7
  187. package/src/tools/apps/executors.ts +2 -1
  188. package/src/tools/credentials/policy-validate.ts +22 -0
  189. package/src/tools/guardian-control-plane-policy.ts +2 -2
  190. package/src/tools/tool-manifest.ts +44 -42
  191. package/src/tools/types.ts +10 -1
  192. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  193. package/src/config/vellum-skills/catalog.json +0 -63
  194. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  195. package/src/skills/vellum-catalog-remote.ts +0 -166
  196. package/src/tools/skills/vellum-catalog.ts +0 -168
  197. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  198. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  199. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  200. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  201. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  202. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  203. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -1,295 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
-
3
- import { eq } from 'drizzle-orm';
4
- import { v4 as uuid } from 'uuid';
5
-
6
- import { addMessage, createConversation, setConversationOriginInterfaceIfUnset } from '../../../../memory/conversation-store.js';
7
- import { getDb } from '../../../../memory/db.js';
8
- import { conversationKeys,conversations, messages as messagesTable } from '../../../../memory/schema.js';
9
- import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
10
-
11
- // -- ChatGPT export format types --
12
-
13
- interface ChatGPTContent {
14
- content_type: string;
15
- parts?: (string | null | Record<string, unknown>)[];
16
- }
17
-
18
- interface ChatGPTNode {
19
- message: {
20
- author: { role: string };
21
- content: ChatGPTContent;
22
- create_time?: number | null;
23
- } | null;
24
- parent: string | null;
25
- children: string[];
26
- }
27
-
28
- interface ChatGPTConversation {
29
- id?: string;
30
- title: string;
31
- create_time: number;
32
- update_time: number;
33
- current_node: string;
34
- mapping: Record<string, ChatGPTNode>;
35
- }
36
-
37
- interface ImportedMessage {
38
- role: string;
39
- content: Array<{ type: string; text: string }>;
40
- createdAt: number;
41
- }
42
-
43
- interface ImportedConversation {
44
- sourceId: string;
45
- title: string;
46
- createdAt: number;
47
- updatedAt: number;
48
- messages: ImportedMessage[];
49
- }
50
-
51
- // -- Tool entry point --
52
-
53
- export async function run(
54
- input: Record<string, unknown>,
55
- _context: ToolContext,
56
- ): Promise<ToolExecutionResult> {
57
- const filePath = input.file_path as string;
58
-
59
- if (!filePath) {
60
- return { content: 'Error: file_path is required', isError: true };
61
- }
62
-
63
- if (!filePath.endsWith('.zip')) {
64
- return { content: 'Error: Only ZIP files are accepted. Please provide the ChatGPT export ZIP file.', isError: true };
65
- }
66
-
67
- if (!existsSync(filePath)) {
68
- return { content: `Error: File not found: ${filePath}`, isError: true };
69
- }
70
-
71
- let imported: ImportedConversation[];
72
- try {
73
- imported = parseChatGPTExport(filePath);
74
- } catch (err) {
75
- return {
76
- content: `Error parsing export file: ${err instanceof Error ? err.message : String(err)}`,
77
- isError: true,
78
- };
79
- }
80
-
81
- if (imported.length === 0) {
82
- return { content: 'No conversations found in the export file.', isError: false };
83
- }
84
-
85
- const db = getDb();
86
- let importedCount = 0;
87
- let skippedCount = 0;
88
- let messageCount = 0;
89
-
90
- for (const conv of imported) {
91
- const convKey = `chatgpt:${conv.sourceId}`;
92
-
93
- const existing = db
94
- .select()
95
- .from(conversationKeys)
96
- .where(eq(conversationKeys.conversationKey, convKey))
97
- .get();
98
-
99
- if (existing) {
100
- skippedCount++;
101
- continue;
102
- }
103
-
104
- const conversation = createConversation(conv.title);
105
- const importChannelMetadata = {
106
- userMessageChannel: 'vellum',
107
- assistantMessageChannel: 'vellum',
108
- userMessageInterface: 'vellum',
109
- assistantMessageInterface: 'vellum',
110
- } as const;
111
-
112
- for (const msg of conv.messages) {
113
- await addMessage(conversation.id, msg.role, JSON.stringify(msg.content), importChannelMetadata);
114
- }
115
-
116
- // addMessage auto-fills originChannel but not originInterface, so set it explicitly
117
- setConversationOriginInterfaceIfUnset(conversation.id, 'vellum');
118
-
119
- // Override timestamps to match ChatGPT originals
120
- db.update(conversations)
121
- .set({ createdAt: conv.createdAt, updatedAt: conv.updatedAt })
122
- .where(eq(conversations.id, conversation.id))
123
- .run();
124
-
125
- // Update message timestamps to match ChatGPT originals
126
- const dbMessages = db
127
- .select({ id: messagesTable.id })
128
- .from(messagesTable)
129
- .where(eq(messagesTable.conversationId, conversation.id))
130
- .orderBy(messagesTable.createdAt)
131
- .all();
132
-
133
- for (let i = 0; i < dbMessages.length && i < conv.messages.length; i++) {
134
- db.update(messagesTable)
135
- .set({ createdAt: conv.messages[i].createdAt })
136
- .where(eq(messagesTable.id, dbMessages[i].id))
137
- .run();
138
- }
139
-
140
- db.insert(conversationKeys)
141
- .values({
142
- id: uuid(),
143
- conversationKey: convKey,
144
- conversationId: conversation.id,
145
- createdAt: Date.now(),
146
- })
147
- .run();
148
-
149
- importedCount++;
150
- messageCount += conv.messages.length;
151
- }
152
-
153
- const lines = [`Imported ${importedCount} conversation(s) with ${messageCount} message(s).`];
154
- if (skippedCount > 0) {
155
- lines.push(`Skipped ${skippedCount} already-imported conversation(s).`);
156
- }
157
- return { content: lines.join('\n'), isError: false };
158
- }
159
-
160
- // -- Parser --
161
-
162
- function parseChatGPTExport(zipPath: string): ImportedConversation[] {
163
- const jsonContent = extractConversationsJsonFromZip(zipPath);
164
-
165
- const raw = JSON.parse(jsonContent);
166
- if (!Array.isArray(raw)) {
167
- throw new Error('Expected conversations.json to contain a JSON array');
168
- }
169
-
170
- const results: ImportedConversation[] = [];
171
- for (const conv of raw as ChatGPTConversation[]) {
172
- const imported = parseConversation(conv);
173
- if (imported) {
174
- results.push(imported);
175
- }
176
- }
177
- return results;
178
- }
179
-
180
- function parseConversation(conv: ChatGPTConversation): ImportedConversation | null {
181
- const { mapping, current_node } = conv;
182
- if (!mapping || !current_node || !mapping[current_node]) return null;
183
-
184
- // Walk from current_node to root via parent pointers, then reverse for chronological order
185
- const nodeIds: string[] = [];
186
- let nodeId: string | null = current_node;
187
- while (nodeId) {
188
- nodeIds.push(nodeId);
189
- nodeId = mapping[nodeId]?.parent ?? null;
190
- }
191
- nodeIds.reverse();
192
-
193
- const messages: ImportedMessage[] = [];
194
- for (const id of nodeIds) {
195
- const node = mapping[id];
196
- if (!node?.message) continue;
197
-
198
- const { author, content, create_time } = node.message;
199
- const role = author?.role;
200
- if (role !== 'user' && role !== 'assistant') continue;
201
-
202
- const text = extractText(content);
203
- if (!text) continue;
204
-
205
- messages.push({
206
- role,
207
- content: [{ type: 'text', text }],
208
- createdAt: create_time ? Math.round(create_time * 1000) : Math.round(conv.create_time * 1000),
209
- });
210
- }
211
-
212
- if (messages.length === 0) return null;
213
-
214
- return {
215
- sourceId: conv.id ?? `${conv.title}-${conv.create_time}`,
216
- title: conv.title || 'Untitled',
217
- createdAt: Math.round(conv.create_time * 1000),
218
- updatedAt: Math.round(conv.update_time * 1000),
219
- messages,
220
- };
221
- }
222
-
223
- function extractText(content: ChatGPTContent): string {
224
- if (!content?.parts) return '';
225
- return content.parts.filter((p): p is string => typeof p === 'string').join('');
226
- }
227
-
228
- // -- ZIP extraction --
229
-
230
- function extractConversationsJsonFromZip(zipPath: string): string {
231
- const buffer = readFileSync(zipPath);
232
-
233
- // Find end of central directory record (EOCD signature: 0x06054b50)
234
- let eocdOffset = -1;
235
- for (let i = buffer.length - 22; i >= 0; i--) {
236
- if (buffer[i] === 0x50 && buffer[i + 1] === 0x4b && buffer[i + 2] === 0x05 && buffer[i + 3] === 0x06) {
237
- eocdOffset = i;
238
- break;
239
- }
240
- }
241
- if (eocdOffset === -1) {
242
- throw new Error('Invalid ZIP file: could not find end of central directory');
243
- }
244
-
245
- const centralDirOffset = buffer.readUInt32LE(eocdOffset + 16);
246
- const centralDirEntries = buffer.readUInt16LE(eocdOffset + 10);
247
-
248
- // Walk central directory to find conversations.json
249
- let offset = centralDirOffset;
250
- for (let i = 0; i < centralDirEntries; i++) {
251
- if (buffer[offset] !== 0x50 || buffer[offset + 1] !== 0x4b || buffer[offset + 2] !== 0x01 || buffer[offset + 3] !== 0x02) {
252
- throw new Error('Invalid ZIP central directory entry');
253
- }
254
-
255
- const cdCompressedSize = buffer.readUInt32LE(offset + 20);
256
- const fileNameLength = buffer.readUInt16LE(offset + 28);
257
- const extraLength = buffer.readUInt16LE(offset + 30);
258
- const commentLength = buffer.readUInt16LE(offset + 32);
259
- const localHeaderOffset = buffer.readUInt32LE(offset + 42);
260
- const fileName = buffer.subarray(offset + 46, offset + 46 + fileNameLength).toString('utf-8');
261
-
262
- if (fileName === 'conversations.json' || fileName.endsWith('/conversations.json')) {
263
- return extractLocalFile(buffer, localHeaderOffset, cdCompressedSize);
264
- }
265
-
266
- offset += 46 + fileNameLength + extraLength + commentLength;
267
- }
268
-
269
- throw new Error('conversations.json not found in ZIP file');
270
- }
271
-
272
- function extractLocalFile(buffer: Buffer, offset: number, cdCompressedSize: number): string {
273
- if (buffer[offset] !== 0x50 || buffer[offset + 1] !== 0x4b || buffer[offset + 2] !== 0x03 || buffer[offset + 3] !== 0x04) {
274
- throw new Error('Invalid ZIP local file header');
275
- }
276
-
277
- const compressionMethod = buffer.readUInt16LE(offset + 8);
278
- const localCompressedSize = buffer.readUInt32LE(offset + 18);
279
- const compressedSize = cdCompressedSize > 0 ? cdCompressedSize : localCompressedSize;
280
- const fileNameLength = buffer.readUInt16LE(offset + 26);
281
- const extraLength = buffer.readUInt16LE(offset + 28);
282
-
283
- const dataOffset = offset + 30 + fileNameLength + extraLength;
284
- const fileData = buffer.subarray(dataOffset, dataOffset + compressedSize);
285
-
286
- if (compressionMethod === 0) {
287
- return fileData.toString('utf-8');
288
- } else if (compressionMethod === 8) {
289
- // eslint-disable-next-line @typescript-eslint/no-require-imports
290
- const { inflateRawSync } = require('node:zlib') as typeof import('node:zlib');
291
- return inflateRawSync(fileData).toString('utf-8');
292
- } else {
293
- throw new Error(`Unsupported ZIP compression method: ${compressionMethod}`);
294
- }
295
- }
@@ -1,166 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { gunzipSync } from 'node:zlib';
4
-
5
- import type { CatalogEntry } from '../tools/skills/vellum-catalog.js';
6
- import { getLogger } from '../util/logger.js';
7
- import { readPlatformToken } from '../util/platform.js';
8
-
9
- const log = getLogger('vellum-catalog-remote');
10
-
11
- const PLATFORM_URL = process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? 'https://assistant.vellum.ai';
12
-
13
- const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
14
-
15
- interface CatalogManifest {
16
- version: number;
17
- skills: CatalogEntry[];
18
- }
19
-
20
- let cachedEntries: CatalogEntry[] | null = null;
21
- let cacheTimestamp = 0;
22
-
23
- function getBundledCatalogPath(): string {
24
- return join(import.meta.dir, '..', 'config', 'vellum-skills', 'catalog.json');
25
- }
26
-
27
- function loadBundledCatalog(): CatalogEntry[] {
28
- try {
29
- const raw = readFileSync(getBundledCatalogPath(), 'utf-8');
30
- const manifest: CatalogManifest = JSON.parse(raw);
31
- return manifest.skills ?? [];
32
- } catch (err) {
33
- log.warn({ err }, 'Failed to read bundled catalog.json');
34
- return [];
35
- }
36
- }
37
-
38
- function getBundledSkillContent(skillId: string): string | null {
39
- try {
40
- const skillPath = join(import.meta.dir, '..', 'config', 'vellum-skills', skillId, 'SKILL.md');
41
- return readFileSync(skillPath, 'utf-8');
42
- } catch {
43
- return null;
44
- }
45
- }
46
-
47
- /** Build request headers, including platform token when available. */
48
- function buildPlatformHeaders(): Record<string, string> {
49
- const headers: Record<string, string> = {};
50
- const token = readPlatformToken();
51
- if (token) {
52
- headers['X-Session-Token'] = token;
53
- }
54
- return headers;
55
- }
56
-
57
- /**
58
- * Fetch catalog entries from the platform API. Falls back to bundled copy.
59
- * Reads the platform token from ~/.vellum/platform-token automatically.
60
- */
61
- export async function fetchCatalogEntries(): Promise<CatalogEntry[]> {
62
- const now = Date.now();
63
- if (cachedEntries && now - cacheTimestamp < CACHE_TTL_MS) {
64
- return cachedEntries;
65
- }
66
-
67
- try {
68
- const url = `${PLATFORM_URL}/v1/skills/`;
69
- const response = await fetch(url, {
70
- headers: buildPlatformHeaders(),
71
- signal: AbortSignal.timeout(5000),
72
- });
73
-
74
- if (!response.ok) {
75
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
76
- }
77
-
78
- const manifest: CatalogManifest = await response.json();
79
- const skills = manifest.skills;
80
- if (!Array.isArray(skills) || skills.length === 0) {
81
- throw new Error('Platform catalog has invalid or empty skills array');
82
- }
83
- cachedEntries = skills;
84
- cacheTimestamp = now;
85
- log.info({ count: cachedEntries.length }, 'Fetched vellum-skills catalog from platform API');
86
- return cachedEntries;
87
- } catch (err) {
88
- log.warn({ err }, 'Failed to fetch catalog from platform API, falling back to bundled copy');
89
- const bundled = loadBundledCatalog();
90
- // Cache the bundled result too so we don't re-fetch on every call during outage
91
- cachedEntries = bundled;
92
- cacheTimestamp = now;
93
- return bundled;
94
- }
95
- }
96
-
97
- /**
98
- * Extract SKILL.md content from a tar archive (uncompressed).
99
- * Tar format: 512-byte header blocks followed by file data blocks.
100
- */
101
- function extractSkillMdFromTar(tarBuffer: Buffer): string | null {
102
- let offset = 0;
103
- while (offset + 512 <= tarBuffer.length) {
104
- const header = tarBuffer.subarray(offset, offset + 512);
105
-
106
- // Check for end-of-archive (two consecutive zero blocks)
107
- if (header.every((b) => b === 0)) break;
108
-
109
- // Extract filename (bytes 0-99, null-terminated)
110
- const nameEnd = header.indexOf(0, 0);
111
- const name = header.subarray(0, Math.min(nameEnd >= 0 ? nameEnd : 100, 100)).toString('utf-8');
112
-
113
- // Extract file size (bytes 124-135, octal string)
114
- const sizeStr = header.subarray(124, 136).toString('utf-8').trim();
115
- const size = parseInt(sizeStr, 8) || 0;
116
-
117
- offset += 512; // move past header
118
-
119
- if (name.endsWith('SKILL.md') || name === 'SKILL.md') {
120
- return tarBuffer.subarray(offset, offset + size).toString('utf-8');
121
- }
122
-
123
- // Skip to next header (data blocks are padded to 512 bytes)
124
- offset += Math.ceil(size / 512) * 512;
125
- }
126
- return null;
127
- }
128
-
129
- /**
130
- * Fetch a skill's SKILL.md content from the platform tar API.
131
- * GET /v1/skills/{skill_id}/ returns a tar.gz archive containing all skill files.
132
- * Falls back to bundled copy on failure.
133
- */
134
- export async function fetchSkillContent(skillId: string): Promise<string | null> {
135
- try {
136
- const url = `${PLATFORM_URL}/v1/skills/${encodeURIComponent(skillId)}/`;
137
- const response = await fetch(url, {
138
- headers: buildPlatformHeaders(),
139
- signal: AbortSignal.timeout(15000),
140
- });
141
-
142
- if (!response.ok) {
143
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
144
- }
145
-
146
- const gzipBuffer = Buffer.from(await response.arrayBuffer());
147
- const tarBuffer = gunzipSync(gzipBuffer);
148
- const skillMd = extractSkillMdFromTar(tarBuffer);
149
-
150
- if (skillMd) {
151
- return skillMd;
152
- }
153
-
154
- log.warn({ skillId }, 'SKILL.md not found in platform tar archive, falling back to bundled');
155
- } catch (err) {
156
- log.warn({ err, skillId }, 'Failed to fetch skill content from platform API, falling back to bundled');
157
- }
158
-
159
- return getBundledSkillContent(skillId);
160
- }
161
-
162
- /** Check if a skill ID exists in the catalog. */
163
- export async function checkVellumSkill(skillId: string): Promise<boolean> {
164
- const entries = await fetchCatalogEntries();
165
- return entries.some((e) => e.id === skillId);
166
- }
@@ -1,168 +0,0 @@
1
- import { RiskLevel } from '../../permissions/types.js';
2
- import type { ToolDefinition } from '../../providers/types.js';
3
- import { parseFrontmatterFields } from '../../skills/frontmatter.js';
4
- import { createManagedSkill } from '../../skills/managed-store.js';
5
- import { checkVellumSkill,fetchCatalogEntries, fetchSkillContent } from '../../skills/vellum-catalog-remote.js';
6
- import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
7
-
8
- export interface CatalogEntry {
9
- id: string;
10
- name: string;
11
- description: string;
12
- emoji?: string;
13
- includes?: string[];
14
- version?: string;
15
- }
16
-
17
- export { checkVellumSkill,fetchCatalogEntries as listCatalogEntries };
18
-
19
- /**
20
- * Install a skill from the vellum-skills catalog by ID.
21
- * Fetches SKILL.md from GitHub (with bundled fallback) and creates a managed skill.
22
- * Returns { success, skillName, error }.
23
- */
24
- export async function installFromVellumCatalog(skillId: string, options?: { overwrite?: boolean }): Promise<{ success: boolean; skillName?: string; error?: string }> {
25
- const trimmedId = skillId.trim();
26
-
27
- // Verify skill exists in catalog and get its metadata
28
- const catalogEntries = await fetchCatalogEntries();
29
- const catalogEntry = catalogEntries.find((e) => e.id === trimmedId);
30
- if (!catalogEntry) {
31
- return { success: false, error: `Skill "${trimmedId}" not found in the Vellum catalog` };
32
- }
33
-
34
- // Fetch SKILL.md content (remote with bundled fallback)
35
- const content = await fetchSkillContent(trimmedId);
36
- if (!content) {
37
- return { success: false, error: `Skill "${trimmedId}" SKILL.md not found` };
38
- }
39
-
40
- const parsed = parseFrontmatterFields(content);
41
- if (!parsed) {
42
- return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md` };
43
- }
44
-
45
- const { fields, body: bodyMarkdown } = parsed;
46
-
47
- const name = fields.name?.trim();
48
- const description = fields.description?.trim();
49
- if (!name || !description) {
50
- return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md (missing name or description)` };
51
- }
52
-
53
- let emoji: string | undefined;
54
- const metadataRaw = fields.metadata?.trim();
55
- if (metadataRaw) {
56
- try {
57
- const metaObj = JSON.parse(metadataRaw);
58
- if (metaObj?.vellum?.emoji) {
59
- emoji = metaObj.vellum.emoji as string;
60
- }
61
- } catch {
62
- // ignore malformed metadata
63
- }
64
- }
65
-
66
- let includes: string[] | undefined;
67
- const includesRaw = fields.includes?.trim();
68
- if (includesRaw) {
69
- try {
70
- const includesObj = JSON.parse(includesRaw);
71
- if (Array.isArray(includesObj) && includesObj.every((item: unknown) => typeof item === 'string')) {
72
- const filtered = (includesObj as string[]).map((s) => s.trim()).filter((s) => s.length > 0);
73
- if (filtered.length > 0) includes = filtered;
74
- }
75
- } catch {
76
- // ignore malformed includes
77
- }
78
- }
79
- const result = createManagedSkill({
80
- id: trimmedId,
81
- name,
82
- description,
83
- bodyMarkdown,
84
- emoji,
85
- includes,
86
- overwrite: options?.overwrite ?? true,
87
- addToIndex: true,
88
- version: catalogEntry.version,
89
- });
90
-
91
- if (!result.created) {
92
- return { success: false, error: result.error };
93
- }
94
-
95
- return { success: true, skillName: trimmedId };
96
- }
97
-
98
- class VellumSkillsCatalogTool implements Tool {
99
- name = 'vellum_skills_catalog';
100
- description = 'List and install Vellum-provided skills from the first-party catalog';
101
- category = 'skills';
102
- defaultRiskLevel = RiskLevel.Low;
103
-
104
- getDefinition(): ToolDefinition {
105
- return {
106
- name: this.name,
107
- description: this.description,
108
- input_schema: {
109
- type: 'object',
110
- properties: {
111
- action: {
112
- type: 'string',
113
- enum: ['list', 'install'],
114
- description: 'The operation to perform. "list" shows available skills; "install" copies a skill to managed skills.',
115
- },
116
- skill_id: {
117
- type: 'string',
118
- description: 'The skill ID to install (required for install action).',
119
- },
120
- overwrite: {
121
- type: 'boolean',
122
- description: 'Whether to overwrite if the skill is already installed (default: false).',
123
- },
124
- },
125
- required: ['action'],
126
- },
127
- };
128
- }
129
-
130
- async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
131
- const action = input.action as string;
132
-
133
- switch (action) {
134
- case 'list': {
135
- const entries = await fetchCatalogEntries();
136
- if (entries.length === 0) {
137
- return { content: 'No Vellum-provided skills available in the catalog.', isError: false };
138
- }
139
- return { content: JSON.stringify(entries, null, 2), isError: false };
140
- }
141
-
142
- case 'install': {
143
- const skillId = input.skill_id;
144
- if (typeof skillId !== 'string' || !skillId.trim()) {
145
- return { content: 'Error: skill_id is required for install action', isError: true };
146
- }
147
-
148
- const result = await installFromVellumCatalog(skillId, { overwrite: input.overwrite === true });
149
- if (!result.success) {
150
- return { content: `Error: ${result.error}`, isError: true };
151
- }
152
-
153
- return {
154
- content: JSON.stringify({
155
- installed: true,
156
- skill_id: result.skillName,
157
- }),
158
- isError: false,
159
- };
160
- }
161
-
162
- default:
163
- return { content: `Error: unknown action "${action}". Use "list" or "install".`, isError: true };
164
- }
165
- }
166
- }
167
-
168
- export const vellumSkillsCatalogTool: Tool = new VellumSkillsCatalogTool();