@vellumai/assistant 0.3.3 → 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.
- package/README.md +8 -16
- package/package.json +1 -1
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +382 -124
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +187 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +73 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +3 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +38 -0
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +36 -13
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/ipc-contract.ts +6 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +5 -14
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session.ts +8 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +1 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +15 -4
- package/src/runtime/routes/channel-routes.ts +172 -84
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -191,74 +191,80 @@ class BrowserManager {
|
|
|
191
191
|
if (this.contextCreating) return this.contextCreating;
|
|
192
192
|
|
|
193
193
|
this.contextCreating = (async () => {
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
194
|
+
// Deterministic test mode: when launch is injected via setLaunchFn,
|
|
195
|
+
// bypass ambient CDP probing/negotiation and use the injected launcher.
|
|
196
|
+
const hasInjectedLaunchFn = launchPersistentContext !== null;
|
|
197
|
+
|
|
198
|
+
if (!hasInjectedLaunchFn) {
|
|
199
|
+
// Try to detect or negotiate CDP before falling back to headless.
|
|
200
|
+
// This auto-detects an existing Chrome with --remote-debugging-port,
|
|
201
|
+
// or asks the client to restart Chrome with CDP enabled.
|
|
202
|
+
let useCdp = this._browserMode === 'cdp';
|
|
203
|
+
const sender = invokingSessionId ? this.sessionSenders.get(invokingSessionId) : undefined;
|
|
204
|
+
if (!useCdp) {
|
|
205
|
+
const cdpAvailable = await this.detectCDP();
|
|
206
|
+
if (cdpAvailable) {
|
|
207
|
+
useCdp = true;
|
|
208
|
+
} else if (invokingSessionId && sender) {
|
|
209
|
+
log.info({ sessionId: invokingSessionId }, 'Requesting CDP from client');
|
|
210
|
+
const accepted = await this.requestCDPFromClient(invokingSessionId, sender);
|
|
211
|
+
if (accepted) {
|
|
212
|
+
const nowAvailable = await this.detectCDP();
|
|
213
|
+
if (nowAvailable) {
|
|
214
|
+
useCdp = true;
|
|
215
|
+
} else {
|
|
216
|
+
log.warn('Client accepted CDP request but CDP not detected');
|
|
217
|
+
}
|
|
210
218
|
} else {
|
|
211
|
-
log.
|
|
219
|
+
log.info('Client declined CDP request');
|
|
212
220
|
}
|
|
213
|
-
} else {
|
|
214
|
-
log.info('Client declined CDP request');
|
|
215
221
|
}
|
|
216
222
|
}
|
|
217
|
-
}
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
224
|
+
if (useCdp) {
|
|
225
|
+
try {
|
|
226
|
+
const pw = await import('playwright');
|
|
227
|
+
const browser = await pw.chromium.connectOverCDP(this.cdpUrl, { timeout: 10_000 });
|
|
228
|
+
this.cdpBrowser = browser;
|
|
229
|
+
this._browserLaunched = false;
|
|
230
|
+
const contexts = browser.contexts();
|
|
231
|
+
const ctx = contexts[0] || await browser.newContext();
|
|
232
|
+
this.setBrowserMode('cdp');
|
|
233
|
+
await this.initBrowserCdpSession();
|
|
234
|
+
log.info({ cdpUrl: this.cdpUrl }, 'Connected to Chrome via CDP');
|
|
235
|
+
return ctx as unknown as BrowserContext;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
log.warn({ err }, 'CDP connectOverCDP failed');
|
|
238
|
+
this._browserMode = 'headless';
|
|
239
|
+
}
|
|
234
240
|
}
|
|
235
|
-
}
|
|
236
241
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
242
|
+
// If a client is connected, launch headed Chromium (minimized) so the user
|
|
243
|
+
// can interact directly when handoff triggers (e.g. CAPTCHAs).
|
|
244
|
+
// The window stays offscreen until bringToFront() is called during handoff.
|
|
245
|
+
const hasSender = !!(invokingSessionId && this.sessionSenders.get(invokingSessionId));
|
|
246
|
+
if (hasSender && this._browserMode === 'headless') {
|
|
247
|
+
try {
|
|
248
|
+
const pw2 = await import('playwright');
|
|
249
|
+
const headedBrowser = await pw2.chromium.launch({
|
|
250
|
+
channel: 'chrome',
|
|
251
|
+
headless: false,
|
|
252
|
+
args: [
|
|
253
|
+
'--window-position=-32000,-32000',
|
|
254
|
+
'--window-size=1,1',
|
|
255
|
+
'--disable-blink-features=AutomationControlled',
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
const ctx = headedBrowser.contexts()[0] || await headedBrowser.newContext();
|
|
259
|
+
this.cdpBrowser = headedBrowser as unknown as typeof this.cdpBrowser;
|
|
260
|
+
this._browserLaunched = true;
|
|
261
|
+
this.setBrowserMode('cdp');
|
|
262
|
+
await this.initBrowserCdpSession();
|
|
263
|
+
log.info('Launched headed Chromium (minimized) for interactive handoff support');
|
|
264
|
+
return ctx as unknown as BrowserContext;
|
|
265
|
+
} catch (err2) {
|
|
266
|
+
log.warn({ err: err2 }, 'Headed Chromium launch failed, falling back to headless');
|
|
267
|
+
}
|
|
262
268
|
}
|
|
263
269
|
}
|
|
264
270
|
|
|
@@ -6,6 +6,7 @@ import { getLogger } from '../../util/logger.js';
|
|
|
6
6
|
import { truncate } from '../../util/truncate.js';
|
|
7
7
|
import { getProfilePolicy } from '../../swarm/worker-backend.js';
|
|
8
8
|
import type { WorkerProfile } from '../../swarm/worker-backend.js';
|
|
9
|
+
import { getCCCommand, loadCCCommandTemplate } from '../../commands/cc-command-registry.js';
|
|
9
10
|
|
|
10
11
|
const log = getLogger('claude-code-tool');
|
|
11
12
|
|
|
@@ -62,7 +63,15 @@ export const claudeCodeTool: Tool = {
|
|
|
62
63
|
properties: {
|
|
63
64
|
prompt: {
|
|
64
65
|
type: 'string',
|
|
65
|
-
description: 'The coding task or question for Claude Code to work on',
|
|
66
|
+
description: 'The coding task or question for Claude Code to work on. Use this for free-form tasks. Mutually exclusive with command.',
|
|
67
|
+
},
|
|
68
|
+
command: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Name of a .claude/commands/*.md command template to execute. The template will be loaded and $ARGUMENTS substituted before execution. Use this instead of prompt when invoking a named CC command.',
|
|
71
|
+
},
|
|
72
|
+
arguments: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Arguments to substitute into the command template ($ARGUMENTS placeholder). Only used with the command input.',
|
|
66
75
|
},
|
|
67
76
|
working_dir: {
|
|
68
77
|
type: 'string',
|
|
@@ -82,7 +91,6 @@ export const claudeCodeTool: Tool = {
|
|
|
82
91
|
description: 'Worker profile that scopes tool access. Defaults to general (backward compatible).',
|
|
83
92
|
},
|
|
84
93
|
},
|
|
85
|
-
required: ['prompt'],
|
|
86
94
|
},
|
|
87
95
|
};
|
|
88
96
|
},
|
|
@@ -92,8 +100,52 @@ export const claudeCodeTool: Tool = {
|
|
|
92
100
|
return { content: 'Cancelled', isError: true };
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
const prompt = input.prompt as string;
|
|
96
103
|
const workingDir = (input.working_dir as string) || context.workingDir;
|
|
104
|
+
|
|
105
|
+
// Resolve prompt: either from direct prompt input or by loading a CC command template
|
|
106
|
+
let prompt: string;
|
|
107
|
+
if (input.command != null && typeof input.command !== 'string') {
|
|
108
|
+
return {
|
|
109
|
+
content: `Error: "command" must be a string, got ${typeof input.command}`,
|
|
110
|
+
isError: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const commandName = input.command as string | undefined;
|
|
114
|
+
if (commandName) {
|
|
115
|
+
// Command-template execution path: load .claude/commands/<command>.md,
|
|
116
|
+
// apply $ARGUMENTS substitution, and use the result as the prompt.
|
|
117
|
+
const entry = getCCCommand(workingDir, commandName);
|
|
118
|
+
if (!entry) {
|
|
119
|
+
return {
|
|
120
|
+
content: `Error: CC command "${commandName}" not found. Looked for .claude/commands/${commandName}.md in ${workingDir} and parent directories.`,
|
|
121
|
+
isError: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let template: string;
|
|
126
|
+
try {
|
|
127
|
+
template = loadCCCommandTemplate(entry);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
return {
|
|
131
|
+
content: `Error: Failed to load CC command template "${commandName}": ${message}`,
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Substitute $ARGUMENTS placeholder with the provided arguments
|
|
137
|
+
const args = (input.arguments as string) ?? '';
|
|
138
|
+
prompt = template.replace(/\$ARGUMENTS/g, args);
|
|
139
|
+
|
|
140
|
+
log.info({ command: commandName, templatePath: entry.filePath, hasArgs: !!args }, 'Loaded CC command template');
|
|
141
|
+
} else if (typeof input.prompt === 'string') {
|
|
142
|
+
prompt = input.prompt;
|
|
143
|
+
} else {
|
|
144
|
+
return {
|
|
145
|
+
content: 'Error: Either "prompt" or "command" must be provided.',
|
|
146
|
+
isError: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
97
149
|
const resumeSessionId = input.resume as string | undefined;
|
|
98
150
|
const model = (input.model as string) || 'claude-sonnet-4-6';
|
|
99
151
|
const profileName = (input.profile as WorkerProfile | undefined) ?? 'general';
|
package/src/tools/executor.ts
CHANGED
|
@@ -268,7 +268,15 @@ export class ToolExecutor {
|
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
if (response.decision === 'deny') {
|
|
271
|
-
const
|
|
271
|
+
const contextualDenial = typeof response.decisionContext === 'string'
|
|
272
|
+
? response.decisionContext.trim()
|
|
273
|
+
: '';
|
|
274
|
+
const denialMessage = contextualDenial.length > 0
|
|
275
|
+
? contextualDenial
|
|
276
|
+
: `Permission denied by user. The user chose not to allow the "${name}" tool. Do NOT retry this tool call immediately. Instead, tell the user that the action was not performed because they denied permission, and ask if they would like you to try again or take a different approach. Wait for the user to explicitly respond before retrying.`;
|
|
277
|
+
const denialReason = contextualDenial.length > 0
|
|
278
|
+
? `Permission denied (${name}): contextual policy`
|
|
279
|
+
: 'Permission denied by user';
|
|
272
280
|
const durationMs = Date.now() - startTime;
|
|
273
281
|
emitLifecycleEvent(context, {
|
|
274
282
|
type: 'permission_denied',
|
|
@@ -281,7 +289,7 @@ export class ToolExecutor {
|
|
|
281
289
|
requestId: context.requestId,
|
|
282
290
|
riskLevel,
|
|
283
291
|
decision: 'deny',
|
|
284
|
-
reason:
|
|
292
|
+
reason: denialReason,
|
|
285
293
|
durationMs,
|
|
286
294
|
});
|
|
287
295
|
return { content: denialMessage, isError: true };
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { RiskLevel } from '../../permissions/types.js';
|
|
5
2
|
import type { ToolDefinition } from '../../providers/types.js';
|
|
3
|
+
import { parseFrontmatterFields } from '../../skills/frontmatter.js';
|
|
6
4
|
import { createManagedSkill } from '../../skills/managed-store.js';
|
|
7
|
-
import {
|
|
5
|
+
import { fetchCatalogEntries, fetchSkillContent, checkVellumSkill } from '../../skills/vellum-catalog-remote.js';
|
|
8
6
|
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
9
7
|
|
|
10
|
-
const log = getLogger('vellum-skills-catalog');
|
|
11
|
-
|
|
12
|
-
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
13
|
-
|
|
14
|
-
function getVellumSkillsDir(): string {
|
|
15
|
-
return join(import.meta.dir, '..', '..', 'config', 'vellum-skills');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
8
|
export interface CatalogEntry {
|
|
19
9
|
id: string;
|
|
20
10
|
name: string;
|
|
@@ -23,123 +13,75 @@ export interface CatalogEntry {
|
|
|
23
13
|
includes?: string[];
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
const skillFilePath = join(directory, 'SKILL.md');
|
|
28
|
-
if (!existsSync(skillFilePath)) return null;
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const stat = statSync(skillFilePath);
|
|
32
|
-
if (!stat.isFile()) return null;
|
|
33
|
-
|
|
34
|
-
const content = readFileSync(skillFilePath, 'utf-8');
|
|
35
|
-
const match = content.match(FRONTMATTER_REGEX);
|
|
36
|
-
if (!match) return null;
|
|
37
|
-
|
|
38
|
-
const fields: Record<string, string> = {};
|
|
39
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
40
|
-
const trimmed = line.trim();
|
|
41
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
42
|
-
const separatorIndex = trimmed.indexOf(':');
|
|
43
|
-
if (separatorIndex === -1) continue;
|
|
44
|
-
|
|
45
|
-
const key = trimmed.slice(0, separatorIndex).trim();
|
|
46
|
-
let value = trimmed.slice(separatorIndex + 1).trim();
|
|
47
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
48
|
-
value = value.slice(1, -1);
|
|
49
|
-
}
|
|
50
|
-
fields[key] = value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const name = fields.name?.trim();
|
|
54
|
-
const description = fields.description?.trim();
|
|
55
|
-
if (!name || !description) return null;
|
|
16
|
+
export { fetchCatalogEntries as listCatalogEntries, checkVellumSkill };
|
|
56
17
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
} catch {
|
|
66
|
-
// ignore malformed metadata
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let includes: string[] | undefined;
|
|
71
|
-
const includesRaw = fields.includes?.trim();
|
|
72
|
-
if (includesRaw) {
|
|
73
|
-
try {
|
|
74
|
-
const parsed = JSON.parse(includesRaw);
|
|
75
|
-
if (Array.isArray(parsed) && parsed.every((item: unknown) => typeof item === 'string')) {
|
|
76
|
-
const filtered = (parsed as string[]).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
77
|
-
if (filtered.length > 0) includes = filtered;
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
// ignore malformed includes
|
|
81
|
-
}
|
|
82
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* Install a skill from the vellum-skills catalog by ID.
|
|
20
|
+
* Fetches SKILL.md from GitHub (with bundled fallback) and creates a managed skill.
|
|
21
|
+
* Returns { success, skillName, error }.
|
|
22
|
+
*/
|
|
23
|
+
export async function installFromVellumCatalog(skillId: string, options?: { overwrite?: boolean }): Promise<{ success: boolean; skillName?: string; error?: string }> {
|
|
24
|
+
const trimmedId = skillId.trim();
|
|
83
25
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
26
|
+
// Verify skill exists in catalog
|
|
27
|
+
const exists = await checkVellumSkill(trimmedId);
|
|
28
|
+
if (!exists) {
|
|
29
|
+
return { success: false, error: `Skill "${trimmedId}" not found in the Vellum catalog` };
|
|
88
30
|
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function listCatalogEntries(): CatalogEntry[] {
|
|
92
|
-
const catalogDir = getVellumSkillsDir();
|
|
93
|
-
if (!existsSync(catalogDir)) return [];
|
|
94
31
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!entry.isDirectory()) continue;
|
|
100
|
-
const parsed = parseCatalogEntry(join(catalogDir, entry.name));
|
|
101
|
-
if (parsed) entries.push(parsed);
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
log.warn({ err, catalogDir }, 'Failed to list catalog entries');
|
|
32
|
+
// Fetch SKILL.md content (remote with bundled fallback)
|
|
33
|
+
const content = await fetchSkillContent(trimmedId);
|
|
34
|
+
if (!content) {
|
|
35
|
+
return { success: false, error: `Skill "${trimmedId}" SKILL.md not found` };
|
|
105
36
|
}
|
|
106
37
|
|
|
107
|
-
|
|
108
|
-
|
|
38
|
+
const parsed = parseFrontmatterFields(content);
|
|
39
|
+
if (!parsed) {
|
|
40
|
+
return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md` };
|
|
41
|
+
}
|
|
109
42
|
|
|
110
|
-
|
|
111
|
-
* Install a skill from the vellum-skills catalog by ID.
|
|
112
|
-
* Returns { success, skillName, error }.
|
|
113
|
-
*/
|
|
114
|
-
export function installFromVellumCatalog(skillId: string): { success: boolean; skillName?: string; error?: string } {
|
|
115
|
-
const catalogDir = getVellumSkillsDir();
|
|
116
|
-
const skillDir = join(catalogDir, skillId.trim());
|
|
117
|
-
const skillFilePath = join(skillDir, 'SKILL.md');
|
|
43
|
+
const { fields, body: bodyMarkdown } = parsed;
|
|
118
44
|
|
|
119
|
-
|
|
120
|
-
|
|
45
|
+
const name = fields.name?.trim();
|
|
46
|
+
const description = fields.description?.trim();
|
|
47
|
+
if (!name || !description) {
|
|
48
|
+
return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md (missing name or description)` };
|
|
121
49
|
}
|
|
122
50
|
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
|
|
51
|
+
let emoji: string | undefined;
|
|
52
|
+
const metadataRaw = fields.metadata?.trim();
|
|
53
|
+
if (metadataRaw) {
|
|
54
|
+
try {
|
|
55
|
+
const metaObj = JSON.parse(metadataRaw);
|
|
56
|
+
if (metaObj?.vellum?.emoji) {
|
|
57
|
+
emoji = metaObj.vellum.emoji as string;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore malformed metadata
|
|
61
|
+
}
|
|
127
62
|
}
|
|
128
63
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
64
|
+
let includes: string[] | undefined;
|
|
65
|
+
const includesRaw = fields.includes?.trim();
|
|
66
|
+
if (includesRaw) {
|
|
67
|
+
try {
|
|
68
|
+
const includesObj = JSON.parse(includesRaw);
|
|
69
|
+
if (Array.isArray(includesObj) && includesObj.every((item: unknown) => typeof item === 'string')) {
|
|
70
|
+
const filtered = (includesObj as string[]).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
71
|
+
if (filtered.length > 0) includes = filtered;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore malformed includes
|
|
75
|
+
}
|
|
132
76
|
}
|
|
133
|
-
|
|
134
|
-
const bodyMarkdown = content.slice(match[0].length);
|
|
135
77
|
const result = createManagedSkill({
|
|
136
|
-
id:
|
|
137
|
-
name
|
|
138
|
-
description
|
|
78
|
+
id: trimmedId,
|
|
79
|
+
name,
|
|
80
|
+
description,
|
|
139
81
|
bodyMarkdown,
|
|
140
|
-
emoji
|
|
141
|
-
includes
|
|
142
|
-
overwrite: true,
|
|
82
|
+
emoji,
|
|
83
|
+
includes,
|
|
84
|
+
overwrite: options?.overwrite ?? true,
|
|
143
85
|
addToIndex: true,
|
|
144
86
|
});
|
|
145
87
|
|
|
@@ -147,7 +89,7 @@ export function installFromVellumCatalog(skillId: string): { success: boolean; s
|
|
|
147
89
|
return { success: false, error: result.error };
|
|
148
90
|
}
|
|
149
91
|
|
|
150
|
-
return { success: true, skillName:
|
|
92
|
+
return { success: true, skillName: trimmedId };
|
|
151
93
|
}
|
|
152
94
|
|
|
153
95
|
class VellumSkillsCatalogTool implements Tool {
|
|
@@ -187,7 +129,7 @@ class VellumSkillsCatalogTool implements Tool {
|
|
|
187
129
|
|
|
188
130
|
switch (action) {
|
|
189
131
|
case 'list': {
|
|
190
|
-
const entries =
|
|
132
|
+
const entries = await fetchCatalogEntries();
|
|
191
133
|
if (entries.length === 0) {
|
|
192
134
|
return { content: 'No Vellum-provided skills available in the catalog.', isError: false };
|
|
193
135
|
}
|
|
@@ -200,52 +142,15 @@ class VellumSkillsCatalogTool implements Tool {
|
|
|
200
142
|
return { content: 'Error: skill_id is required for install action', isError: true };
|
|
201
143
|
}
|
|
202
144
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const skillFilePath = join(skillDir, 'SKILL.md');
|
|
206
|
-
|
|
207
|
-
if (!existsSync(skillFilePath)) {
|
|
208
|
-
const available = listCatalogEntries().map((e) => e.id);
|
|
209
|
-
return {
|
|
210
|
-
content: `Error: skill "${skillId}" not found in the Vellum catalog. Available: ${available.join(', ') || 'none'}`,
|
|
211
|
-
isError: true,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const content = readFileSync(skillFilePath, 'utf-8');
|
|
216
|
-
const match = content.match(FRONTMATTER_REGEX);
|
|
217
|
-
if (!match) {
|
|
218
|
-
return { content: `Error: skill "${skillId}" has invalid SKILL.md (missing frontmatter)`, isError: true };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const entry = parseCatalogEntry(skillDir);
|
|
222
|
-
if (!entry) {
|
|
223
|
-
return { content: `Error: skill "${skillId}" has invalid SKILL.md`, isError: true };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const bodyMarkdown = content.slice(match[0].length);
|
|
227
|
-
|
|
228
|
-
const result = createManagedSkill({
|
|
229
|
-
id: entry.id,
|
|
230
|
-
name: entry.name,
|
|
231
|
-
description: entry.description,
|
|
232
|
-
bodyMarkdown,
|
|
233
|
-
emoji: entry.emoji,
|
|
234
|
-
includes: entry.includes,
|
|
235
|
-
overwrite: input.overwrite === true,
|
|
236
|
-
addToIndex: true,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
if (!result.created) {
|
|
145
|
+
const result = await installFromVellumCatalog(skillId, { overwrite: input.overwrite === true });
|
|
146
|
+
if (!result.success) {
|
|
240
147
|
return { content: `Error: ${result.error}`, isError: true };
|
|
241
148
|
}
|
|
242
149
|
|
|
243
150
|
return {
|
|
244
151
|
content: JSON.stringify({
|
|
245
152
|
installed: true,
|
|
246
|
-
skill_id:
|
|
247
|
-
name: entry.name,
|
|
248
|
-
path: result.path,
|
|
153
|
+
skill_id: result.skillName,
|
|
249
154
|
}),
|
|
250
155
|
isError: false,
|
|
251
156
|
};
|
|
@@ -36,7 +36,7 @@ export interface ParsedCommand {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const SHELL_PROGRAMS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'fish']);
|
|
39
|
-
const OPAQUE_PROGRAMS = new Set(['eval', 'source']);
|
|
39
|
+
const OPAQUE_PROGRAMS = new Set(['eval', 'source', 'alias']);
|
|
40
40
|
const DANGEROUS_ENV_VARS = new Set([
|
|
41
41
|
'LD_PRELOAD', 'LD_LIBRARY_PATH',
|
|
42
42
|
'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH', 'DYLD_FRAMEWORK_PATH',
|
|
@@ -89,17 +89,33 @@ const initGuard = new PromiseGuard<void>();
|
|
|
89
89
|
*/
|
|
90
90
|
function findWasmPath(pkg: string, file: string): string {
|
|
91
91
|
const dir = import.meta.dirname ?? __dirname;
|
|
92
|
-
const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
|
|
93
92
|
|
|
94
|
-
|
|
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
|
-
//
|
|
100
|
-
|
|
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
|
|
package/src/util/platform.ts
CHANGED
|
@@ -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
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
return Math.max(baseDelayMs, computeRetryDelay(attempt,
|
|
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
|
/**
|