@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.
- package/ARCHITECTURE.md +418 -378
- package/Dockerfile +1 -1
- package/README.md +16 -9
- package/package.json +1 -1
- package/src/__tests__/account-registry.test.ts +1 -0
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -0
- package/src/__tests__/asset-search-tool.test.ts +7 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +29 -0
- package/src/__tests__/channel-guardian.test.ts +2143 -1546
- package/src/__tests__/channel-retry-sweep.test.ts +169 -14
- package/src/__tests__/claude-code-tool-profiles.test.ts +1 -0
- package/src/__tests__/computer-use-tools.test.ts +1 -0
- package/src/__tests__/contacts-tools.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +1 -0
- package/src/__tests__/credential-policy-validate.test.ts +97 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +1 -0
- package/src/__tests__/credential-vault.test.ts +1 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +1 -0
- package/src/__tests__/file-edit-tool.test.ts +1 -0
- package/src/__tests__/file-read-tool.test.ts +1 -0
- package/src/__tests__/file-write-tool.test.ts +1 -0
- package/src/__tests__/followup-tools.test.ts +1 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +5 -4
- package/src/__tests__/guardian-grant-minting.test.ts +3 -0
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +4 -3
- package/src/__tests__/guardian-routing-state.test.ts +8 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/headless-browser-interactions.test.ts +1 -0
- package/src/__tests__/headless-browser-navigate.test.ts +1 -0
- package/src/__tests__/headless-browser-read-tools.test.ts +1 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +1 -0
- package/src/__tests__/host-file-edit-tool.test.ts +1 -0
- package/src/__tests__/host-file-read-tool.test.ts +1 -0
- package/src/__tests__/host-file-write-tool.test.ts +1 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -0
- package/src/__tests__/lifecycle-docs-guard.test.ts +207 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +1 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +8 -0
- package/src/__tests__/messaging-send-tool.test.ts +1 -0
- package/src/__tests__/playbook-execution.test.ts +1 -0
- package/src/__tests__/playbook-tools.test.ts +1 -0
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/relay-server.test.ts +4 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -0
- package/src/__tests__/schedule-tools.test.ts +1 -0
- package/src/__tests__/secret-onetime-send.test.ts +4 -0
- package/src/__tests__/secret-scanner-executor.test.ts +2 -0
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/send-notification-tool.test.ts +2 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/shell-credential-ref.test.ts +1 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skill-load-tool.test.ts +1 -0
- package/src/__tests__/skill-script-runner-host.test.ts +1 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -0
- package/src/__tests__/skill-script-runner.test.ts +1 -0
- package/src/__tests__/skill-tool-factory.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/__tests__/subagent-tools.test.ts +1 -1
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-session-integration.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/task-management-tools.test.ts +1 -0
- package/src/__tests__/task-tools.test.ts +1 -0
- package/src/__tests__/terminal-tools.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +2 -2
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +1 -0
- package/src/__tests__/tool-executor.test.ts +1 -0
- package/src/__tests__/trust-context-guards.test.ts +218 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +6 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +1 -0
- package/src/__tests__/trusted-contact-verification.test.ts +1 -0
- package/src/__tests__/view-image-tool.test.ts +1 -0
- package/src/agent/loop.ts +9 -2
- package/src/calls/guardian-dispatch.ts +4 -4
- package/src/cli/mcp.ts +183 -3
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -4
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +42 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +10 -3
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -1
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +91 -162
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +21 -6
- package/src/config/bundled-tool-registry.ts +281 -267
- package/src/config/system-prompt.ts +4 -2
- package/src/daemon/computer-use-session.ts +1 -0
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +42 -2
- package/src/daemon/session-agent-loop.ts +4 -1
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +2 -2
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +2 -1
- package/src/daemon/session.ts +5 -2
- package/src/mcp/client.ts +55 -6
- package/src/mcp/manager.ts +9 -0
- package/src/mcp/mcp-oauth-provider.ts +347 -0
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/delivery-status.ts +43 -0
- package/src/memory/guardian-bindings.ts +3 -3
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +108 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/schema.ts +1 -1
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/gmail/adapter.ts +9 -3
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/runtime/actor-trust-resolver.ts +13 -4
- package/src/runtime/channel-retry-sweep.ts +31 -14
- package/src/runtime/guardian-context-resolver.ts +25 -64
- package/src/runtime/guardian-outbound-actions.ts +399 -108
- package/src/runtime/guardian-vellum-migration.ts +1 -23
- package/src/runtime/guardian-verification-templates.ts +66 -30
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/local-actor-identity.ts +4 -6
- package/src/runtime/middleware/actor-token.ts +2 -8
- package/src/runtime/routes/channel-route-shared.ts +0 -1
- package/src/runtime/routes/inbound-message-handler.ts +3 -4
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/runtime/tool-grant-request-helper.ts +1 -1
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/credentials/policy-validate.ts +22 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +10 -1
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /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();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|