@vellumai/assistant 0.3.19 → 0.3.21
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 +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +10 -2
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { McpServerConfig } from '../../config/mcp-schema.js';
|
|
2
|
+
import type { McpServerManager } from '../../mcp/manager.js';
|
|
3
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
4
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
5
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
6
|
+
|
|
7
|
+
const riskMap: Record<string, RiskLevel> = {
|
|
8
|
+
low: RiskLevel.Low,
|
|
9
|
+
medium: RiskLevel.Medium,
|
|
10
|
+
high: RiskLevel.High,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a namespaced tool name to prevent collisions across MCP servers
|
|
15
|
+
* and with core/skill tools.
|
|
16
|
+
*/
|
|
17
|
+
export function mcpToolName(serverId: string, toolName: string): string {
|
|
18
|
+
return `mcp__${serverId}__${toolName}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a namespaced MCP tool name back into serverId and original tool name.
|
|
23
|
+
* Returns null if the name doesn't match the MCP naming convention.
|
|
24
|
+
*/
|
|
25
|
+
export function parseMcpToolName(name: string): { serverId: string; toolName: string } | null {
|
|
26
|
+
if (!name.startsWith('mcp__')) return null;
|
|
27
|
+
const prefixRemoved = name.slice(5); // remove 'mcp__'
|
|
28
|
+
const firstSep = prefixRemoved.indexOf('__');
|
|
29
|
+
if (firstSep === -1) return null;
|
|
30
|
+
return {
|
|
31
|
+
serverId: prefixRemoved.slice(0, firstSep),
|
|
32
|
+
toolName: prefixRemoved.slice(firstSep + 2),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface McpToolMetadata {
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
inputSchema: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a Tool object from MCP tool metadata.
|
|
44
|
+
* The tool delegates execution to the McpServerManager.
|
|
45
|
+
*/
|
|
46
|
+
export function createMcpTool(
|
|
47
|
+
metadata: McpToolMetadata,
|
|
48
|
+
serverId: string,
|
|
49
|
+
serverConfig: McpServerConfig,
|
|
50
|
+
manager: McpServerManager,
|
|
51
|
+
): Tool {
|
|
52
|
+
const namespacedName = mcpToolName(serverId, metadata.name);
|
|
53
|
+
const riskLevel = riskMap[serverConfig.defaultRiskLevel] ?? RiskLevel.High;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: namespacedName,
|
|
57
|
+
description: metadata.description,
|
|
58
|
+
category: 'mcp',
|
|
59
|
+
defaultRiskLevel: riskLevel,
|
|
60
|
+
origin: 'mcp',
|
|
61
|
+
ownerMcpServerId: serverId,
|
|
62
|
+
executionTarget: 'host',
|
|
63
|
+
|
|
64
|
+
getDefinition(): ToolDefinition {
|
|
65
|
+
return {
|
|
66
|
+
name: namespacedName,
|
|
67
|
+
description: metadata.description,
|
|
68
|
+
input_schema: metadata.inputSchema as ToolDefinition['input_schema'],
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
|
|
73
|
+
try {
|
|
74
|
+
const result = await manager.callTool(serverId, metadata.name, input);
|
|
75
|
+
return {
|
|
76
|
+
content: result.content,
|
|
77
|
+
isError: result.isError,
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
return {
|
|
82
|
+
content: `MCP tool execution failed: ${message}`,
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create Tool objects from all tools provided by an MCP server.
|
|
92
|
+
*/
|
|
93
|
+
export function createMcpToolsFromServer(
|
|
94
|
+
tools: McpToolMetadata[],
|
|
95
|
+
serverId: string,
|
|
96
|
+
serverConfig: McpServerConfig,
|
|
97
|
+
manager: McpServerManager,
|
|
98
|
+
): Tool[] {
|
|
99
|
+
return tools.map((tool) => createMcpTool(tool, serverId, serverConfig, manager));
|
|
100
|
+
}
|
|
@@ -373,7 +373,6 @@ export async function stopSession(sessionId: ProxySessionId): Promise<void> {
|
|
|
373
373
|
*/
|
|
374
374
|
export function getSessionEnv(
|
|
375
375
|
sessionId: ProxySessionId,
|
|
376
|
-
options?: { dockerMode?: boolean },
|
|
377
376
|
): ProxyEnvVars {
|
|
378
377
|
const managed = sessions.get(sessionId);
|
|
379
378
|
if (!managed) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -384,10 +383,7 @@ export function getSessionEnv(
|
|
|
384
383
|
// Touch the idle timer on access
|
|
385
384
|
resetIdleTimer(managed);
|
|
386
385
|
|
|
387
|
-
|
|
388
|
-
// host.docker.internal so traffic reaches the host-side proxy.
|
|
389
|
-
const host = options?.dockerMode ? 'host.docker.internal' : '127.0.0.1';
|
|
390
|
-
const proxyUrl = `http://${host}:${managed.session.port}`;
|
|
386
|
+
const proxyUrl = `http://127.0.0.1:${managed.session.port}`;
|
|
391
387
|
const env: ProxyEnvVars = {
|
|
392
388
|
HTTP_PROXY: proxyUrl,
|
|
393
389
|
HTTPS_PROXY: proxyUrl,
|
package/src/tools/registry.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function registerSkillTools(newTools: Tool[]): Tool[] {
|
|
|
121
121
|
for (const tool of newTools) {
|
|
122
122
|
const existing = tools.get(tool.name);
|
|
123
123
|
if (existing) {
|
|
124
|
-
const existingIsCore = existing.origin
|
|
124
|
+
const existingIsCore = existing.origin === 'core' || !existing.origin;
|
|
125
125
|
if (existingIsCore) {
|
|
126
126
|
log.warn(
|
|
127
127
|
{ toolName: tool.name, skillId: tool.ownerSkillId },
|
|
@@ -176,6 +176,69 @@ export function unregisterSkillTools(skillId: string): void {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Register multiple MCP-origin tools at once.
|
|
181
|
+
* Skips any tool whose name collides with a core tool (logs a warning).
|
|
182
|
+
* Throws if a tool name collides with a tool owned by a different MCP server.
|
|
183
|
+
*/
|
|
184
|
+
export function registerMcpTools(newTools: Tool[]): Tool[] {
|
|
185
|
+
const accepted: Tool[] = [];
|
|
186
|
+
for (const tool of newTools) {
|
|
187
|
+
const existing = tools.get(tool.name);
|
|
188
|
+
if (existing) {
|
|
189
|
+
const existingIsCore = existing.origin === 'core' || !existing.origin;
|
|
190
|
+
if (existingIsCore) {
|
|
191
|
+
log.warn(
|
|
192
|
+
{ toolName: tool.name, serverId: tool.ownerMcpServerId },
|
|
193
|
+
`MCP server "${tool.ownerMcpServerId}" tried to register tool "${tool.name}" which conflicts with a core tool. Skipping.`,
|
|
194
|
+
);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (existing.origin === 'skill') {
|
|
198
|
+
log.warn(
|
|
199
|
+
{ toolName: tool.name, serverId: tool.ownerMcpServerId, skillId: existing.ownerSkillId },
|
|
200
|
+
`MCP server "${tool.ownerMcpServerId}" tried to register tool "${tool.name}" which conflicts with skill tool from "${existing.ownerSkillId}". Skipping.`,
|
|
201
|
+
);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (existing.origin === 'mcp' && existing.ownerMcpServerId !== tool.ownerMcpServerId) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`MCP tool "${tool.name}" is already registered by MCP server "${existing.ownerMcpServerId}"`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
accepted.push(tool);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const tool of accepted) {
|
|
214
|
+
tools.set(tool.name, tool);
|
|
215
|
+
log.info({ name: tool.name, ownerMcpServerId: tool.ownerMcpServerId }, 'MCP tool registered');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return accepted;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Unregister all tools belonging to a specific MCP server.
|
|
223
|
+
*/
|
|
224
|
+
export function unregisterMcpTools(serverId: string): void {
|
|
225
|
+
for (const [name, tool] of tools) {
|
|
226
|
+
if (tool.origin === 'mcp' && tool.ownerMcpServerId === serverId) {
|
|
227
|
+
tools.delete(name);
|
|
228
|
+
log.info({ name, serverId }, 'MCP tool unregistered');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Return the names of all currently registered MCP-origin tools.
|
|
235
|
+
*/
|
|
236
|
+
export function getMcpToolNames(): string[] {
|
|
237
|
+
return Array.from(tools.values())
|
|
238
|
+
.filter((t) => t.origin === 'mcp')
|
|
239
|
+
.map((t) => t.name);
|
|
240
|
+
}
|
|
241
|
+
|
|
179
242
|
/**
|
|
180
243
|
* Return the names of all currently registered skill-origin tools.
|
|
181
244
|
*/
|
package/src/tools/skills/load.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isAssistantFeatureFlagEnabled } from '../../config/assistant-feature-flags.js';
|
|
2
|
+
import { getConfig } from '../../config/loader.js';
|
|
3
|
+
import { skillFlagKey } from '../../config/skill-state.js';
|
|
1
4
|
import type { SkillSummary } from '../../config/skills.js';
|
|
2
5
|
import { loadSkillBySelector, loadSkillCatalog } from '../../config/skills.js';
|
|
3
6
|
import { RiskLevel } from '../../permissions/types.js';
|
|
@@ -46,6 +49,15 @@ export class SkillLoadTool implements Tool {
|
|
|
46
49
|
|
|
47
50
|
const skill = loaded.skill;
|
|
48
51
|
|
|
52
|
+
// Assistant feature flag gate: reject loading if the skill's flag is OFF
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
if (!isAssistantFeatureFlagEnabled(skillFlagKey(skill.id), config)) {
|
|
55
|
+
return {
|
|
56
|
+
content: `Error: skill "${skill.id}" is currently unavailable (disabled by feature flag)`,
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
// Load catalog for include validation and child metadata output
|
|
50
62
|
let catalogIndex: Map<string, SkillSummary> | undefined;
|
|
51
63
|
if (skill.includes && skill.includes.length > 0) {
|
|
@@ -83,14 +95,15 @@ export class SkillLoadTool implements Tool {
|
|
|
83
95
|
const childLines: string[] = [];
|
|
84
96
|
for (const childId of skill.includes) {
|
|
85
97
|
const child = catalogIndex.get(childId);
|
|
86
|
-
if (child)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
if (!child) continue;
|
|
99
|
+
if (!isAssistantFeatureFlagEnabled(skillFlagKey(childId), config)) continue;
|
|
100
|
+
|
|
101
|
+
childLines.push(` - ${child.id}: ${child.name} — ${child.description} (${child.skillFilePath})`);
|
|
102
|
+
|
|
103
|
+
// Load the included skill's body content
|
|
104
|
+
const childLoaded = loadSkillBySelector(childId);
|
|
105
|
+
if (childLoaded.skill && childLoaded.skill.body.length > 0) {
|
|
106
|
+
includedBodies.push(`--- Included Skill: ${childLoaded.skill.name} (${childId}) ---\n${childLoaded.skill.body}`);
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
immediateChildrenSection = `Included Skills (immediate):\n${childLines.join('\n')}`;
|
|
@@ -113,6 +126,7 @@ export class SkillLoadTool implements Tool {
|
|
|
113
126
|
for (const childId of skill.includes) {
|
|
114
127
|
const child = catalogIndex.get(childId);
|
|
115
128
|
if (!child) continue;
|
|
129
|
+
if (!isAssistantFeatureFlagEnabled(skillFlagKey(childId), config)) continue;
|
|
116
130
|
let childHash: string | undefined;
|
|
117
131
|
try {
|
|
118
132
|
childHash = computeSkillVersionHash(child.directoryPath);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getConfig } from '../../config/loader.js';
|
|
5
|
+
import { generateImage, mapGeminiError } from '../../media/gemini-image-service.js';
|
|
6
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
7
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
8
|
+
import { getLogger } from '../../util/logger.js';
|
|
9
|
+
import { getWorkspaceDir } from '../../util/platform.js';
|
|
10
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
11
|
+
|
|
12
|
+
const log = getLogger('avatar-generator');
|
|
13
|
+
|
|
14
|
+
const TOOL_NAME = 'set_avatar';
|
|
15
|
+
|
|
16
|
+
/** Canonical path where the custom avatar PNG is stored. */
|
|
17
|
+
function getAvatarPath(): string {
|
|
18
|
+
return join(getWorkspaceDir(), 'data', 'avatar', 'custom-avatar.png');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const setAvatarTool: Tool = {
|
|
22
|
+
name: TOOL_NAME,
|
|
23
|
+
description:
|
|
24
|
+
'Generate a custom avatar image from a text description. ' +
|
|
25
|
+
'Saves the result as the assistant\'s avatar.',
|
|
26
|
+
category: 'system',
|
|
27
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
28
|
+
|
|
29
|
+
getDefinition(): ToolDefinition {
|
|
30
|
+
return {
|
|
31
|
+
name: TOOL_NAME,
|
|
32
|
+
description: this.description,
|
|
33
|
+
input_schema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
description: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description:
|
|
39
|
+
'A text description of the desired avatar appearance, ' +
|
|
40
|
+
'e.g. "a friendly purple cat with green eyes wearing a tiny hat".',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ['description'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async execute(
|
|
49
|
+
input: Record<string, unknown>,
|
|
50
|
+
_context: ToolContext,
|
|
51
|
+
): Promise<ToolExecutionResult> {
|
|
52
|
+
const description = input.description;
|
|
53
|
+
if (typeof description !== 'string' || description.trim() === '') {
|
|
54
|
+
return {
|
|
55
|
+
content: 'Error: description is required and must be a non-empty string.',
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const config = getConfig();
|
|
61
|
+
const apiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
|
|
62
|
+
if (!apiKey) {
|
|
63
|
+
return {
|
|
64
|
+
content: 'No Gemini API key configured. Please add your Gemini API key in Settings → Models & Services, or set the GEMINI_API_KEY environment variable.',
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
log.info({ description: description.trim() }, 'Generating avatar via Gemini');
|
|
71
|
+
|
|
72
|
+
const prompt =
|
|
73
|
+
`Create an avatar image based on this description: ${description.trim()}\n\n` +
|
|
74
|
+
'Style: cute, friendly, work-safe illustration. ' +
|
|
75
|
+
'Vibrant but soft colors. Simple and recognizable at small sizes (28px). ' +
|
|
76
|
+
'Circular or rounded composition filling the canvas. ' +
|
|
77
|
+
'Subtle background color (not white or transparent).';
|
|
78
|
+
|
|
79
|
+
const result = await generateImage(apiKey, {
|
|
80
|
+
prompt,
|
|
81
|
+
mode: 'generate',
|
|
82
|
+
model: config.imageGenModel,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (result.images.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
content: 'Error: Gemini returned no image data. Please try again.',
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const image = result.images[0];
|
|
93
|
+
const pngBuffer = Buffer.from(image.dataBase64, 'base64');
|
|
94
|
+
|
|
95
|
+
const avatarPath = getAvatarPath();
|
|
96
|
+
const avatarDir = dirname(avatarPath);
|
|
97
|
+
|
|
98
|
+
mkdirSync(avatarDir, { recursive: true });
|
|
99
|
+
writeFileSync(avatarPath, pngBuffer);
|
|
100
|
+
|
|
101
|
+
log.info({ avatarPath }, 'Avatar saved successfully');
|
|
102
|
+
|
|
103
|
+
// Side-effect hook in tool-side-effects.ts broadcasts avatar_updated to all clients.
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
content: 'Avatar updated! Your new avatar will appear shortly.',
|
|
107
|
+
isError: false,
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const message = mapGeminiError(error);
|
|
111
|
+
log.error({ error: message }, 'Avatar generation failed');
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: `Avatar generation failed: ${message}`,
|
|
115
|
+
isError: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
2
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
3
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const SETTINGS_TABS = [
|
|
6
|
+
'Voice',
|
|
7
|
+
'Connect',
|
|
8
|
+
'Trust',
|
|
9
|
+
'Model',
|
|
10
|
+
'Scheduling',
|
|
11
|
+
'Parental',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type SettingsTab = (typeof SETTINGS_TABS)[number];
|
|
15
|
+
|
|
16
|
+
export class NavigateSettingsTabTool implements Tool {
|
|
17
|
+
name = 'navigate_settings_tab';
|
|
18
|
+
description =
|
|
19
|
+
'Open the Vellum settings panel to a specific tab (e.g. Voice, Connect, Trust). ' +
|
|
20
|
+
'Use this when the user needs to review or adjust settings visually.';
|
|
21
|
+
category = 'system';
|
|
22
|
+
defaultRiskLevel = RiskLevel.Low;
|
|
23
|
+
|
|
24
|
+
getDefinition(): ToolDefinition {
|
|
25
|
+
return {
|
|
26
|
+
name: this.name,
|
|
27
|
+
description: this.description,
|
|
28
|
+
input_schema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
tab: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
enum: [...SETTINGS_TABS],
|
|
34
|
+
description: 'The settings tab to navigate to',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['tab'],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
43
|
+
const tab = input.tab as string;
|
|
44
|
+
if (!SETTINGS_TABS.includes(tab as SettingsTab)) {
|
|
45
|
+
return {
|
|
46
|
+
content: `Error: unknown tab "${tab}". Valid tabs: ${SETTINGS_TABS.join(', ')}`,
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (context.sendToClient) {
|
|
52
|
+
context.sendToClient({
|
|
53
|
+
type: 'navigate_settings',
|
|
54
|
+
tab,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content: `Opened settings to the ${tab} tab.`,
|
|
60
|
+
isError: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const navigateSettingsTabTool = new NavigateSettingsTabTool();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
2
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
3
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const PANES = {
|
|
6
|
+
microphone: {
|
|
7
|
+
url: 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone',
|
|
8
|
+
label: 'Microphone privacy',
|
|
9
|
+
instruction: 'Please toggle Vellum Assistant on.',
|
|
10
|
+
},
|
|
11
|
+
speech_recognition: {
|
|
12
|
+
url: 'x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition',
|
|
13
|
+
label: 'Speech Recognition privacy',
|
|
14
|
+
instruction: 'Please toggle Vellum Assistant on.',
|
|
15
|
+
},
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
type PaneName = keyof typeof PANES;
|
|
19
|
+
|
|
20
|
+
const VALID_PANES = Object.keys(PANES) as PaneName[];
|
|
21
|
+
|
|
22
|
+
export class OpenSystemSettingsTool implements Tool {
|
|
23
|
+
name = 'open_system_settings';
|
|
24
|
+
description =
|
|
25
|
+
'Open a specific macOS System Settings pane (e.g. Microphone or Speech Recognition privacy). ' +
|
|
26
|
+
'Use this to guide the user through granting permissions that can only be toggled in System Settings.';
|
|
27
|
+
category = 'system';
|
|
28
|
+
defaultRiskLevel = RiskLevel.Low;
|
|
29
|
+
|
|
30
|
+
getDefinition(): ToolDefinition {
|
|
31
|
+
return {
|
|
32
|
+
name: this.name,
|
|
33
|
+
description: this.description,
|
|
34
|
+
input_schema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
pane: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: [...VALID_PANES],
|
|
40
|
+
description: 'The System Settings pane to open',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ['pane'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
49
|
+
const pane = input.pane as string;
|
|
50
|
+
if (!VALID_PANES.includes(pane as PaneName)) {
|
|
51
|
+
return {
|
|
52
|
+
content: `Error: unknown pane "${pane}". Valid panes: ${VALID_PANES.join(', ')}`,
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const meta = PANES[pane as PaneName];
|
|
58
|
+
|
|
59
|
+
// Send open_url IPC to the client — the x-apple.systempreferences: scheme
|
|
60
|
+
// opens System Settings directly without a browser confirmation dialog.
|
|
61
|
+
if (context.sendToClient) {
|
|
62
|
+
context.sendToClient({
|
|
63
|
+
type: 'open_url',
|
|
64
|
+
url: meta.url,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: `Opened System Settings to ${meta.label}. ${meta.instruction}`,
|
|
70
|
+
isError: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const openSystemSettingsTool = new OpenSystemSettingsTool();
|