@vellumai/assistant 0.5.11 → 0.5.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/Dockerfile +1 -0
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +1 -1
- package/src/credential-execution/client.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +47 -1
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/db-init.ts +16 -0
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/system-prompt.ts +43 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +5 -3
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +36 -13
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- package/src/oauth/provider-behaviors.ts +0 -634
|
@@ -56,6 +56,8 @@ import * as contactMerge from "./bundled-skills/contacts/tools/contact-merge.js"
|
|
|
56
56
|
import * as contactSearch from "./bundled-skills/contacts/tools/contact-search.js";
|
|
57
57
|
import * as contactUpsert from "./bundled-skills/contacts/tools/contact-upsert.js";
|
|
58
58
|
import * as googleContacts from "./bundled-skills/contacts/tools/google-contacts.js";
|
|
59
|
+
// ── conversations ─────────────────────────────────────────────────────────────
|
|
60
|
+
import * as renameConversation from "./bundled-skills/conversations/tools/rename-conversation.js";
|
|
59
61
|
// ── document ───────────────────────────────────────────────────────────────────
|
|
60
62
|
import * as documentCreate from "./bundled-skills/document/tools/document-create.js";
|
|
61
63
|
import * as documentUpdate from "./bundled-skills/document/tools/document-update.js";
|
|
@@ -222,6 +224,9 @@ export const bundledToolRegistry = new Map<string, SkillToolScript>([
|
|
|
222
224
|
["contacts:tools/contact-merge.ts", contactMerge],
|
|
223
225
|
["contacts:tools/google-contacts.ts", googleContacts],
|
|
224
226
|
|
|
227
|
+
// conversations
|
|
228
|
+
["conversations:tools/rename-conversation.ts", renameConversation],
|
|
229
|
+
|
|
225
230
|
// document
|
|
226
231
|
["document:tools/document-create.ts", documentCreate],
|
|
227
232
|
["document:tools/document-update.ts", documentUpdate],
|
|
@@ -255,7 +255,7 @@
|
|
|
255
255
|
"key": "managed-google-oauth",
|
|
256
256
|
"label": "Managed Google OAuth",
|
|
257
257
|
"description": "Show the Google OAuth service card in Models & Services settings",
|
|
258
|
-
"defaultEnabled":
|
|
258
|
+
"defaultEnabled": true
|
|
259
259
|
},
|
|
260
260
|
{
|
|
261
261
|
"id": "settings-embedding-provider",
|
|
@@ -245,6 +245,7 @@ export interface AgentLoopConversationContext {
|
|
|
245
245
|
trustContext?: TrustContext;
|
|
246
246
|
assistantId?: string;
|
|
247
247
|
voiceCallControlPrompt?: string;
|
|
248
|
+
transportHints?: string[];
|
|
248
249
|
|
|
249
250
|
readonly coreToolNames: Set<string>;
|
|
250
251
|
allowedToolNames?: Set<string>;
|
|
@@ -748,6 +749,7 @@ export async function runAgentLoopImpl(
|
|
|
748
749
|
temporalContext,
|
|
749
750
|
nowScratchpad,
|
|
750
751
|
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
752
|
+
transportHints: ctx.transportHints ?? null,
|
|
751
753
|
isNonInteractive: !isInteractiveResolved,
|
|
752
754
|
} as const;
|
|
753
755
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getProviderRoutingSource } from "../providers/registry.js";
|
|
1
2
|
import { ProviderError, ProviderNotConfiguredError } from "../util/errors.js";
|
|
2
3
|
import type {
|
|
3
4
|
ConversationErrorCode,
|
|
@@ -200,12 +201,40 @@ function classifyCore(
|
|
|
200
201
|
errorCategory: "context_too_large",
|
|
201
202
|
};
|
|
202
203
|
}
|
|
204
|
+
if (error.statusCode === 401 || error.statusCode === 403) {
|
|
205
|
+
if (
|
|
206
|
+
/invalid.*api.?key|invalid.*x-api-key|authentication.?error/i.test(
|
|
207
|
+
message,
|
|
208
|
+
)
|
|
209
|
+
) {
|
|
210
|
+
// Check if this provider is routed through the managed proxy.
|
|
211
|
+
// If so, the assistant API key is stale — the client should reprovision.
|
|
212
|
+
const providerName = error.provider;
|
|
213
|
+
if (getProviderRoutingSource(providerName) === "managed-proxy") {
|
|
214
|
+
return {
|
|
215
|
+
code: "MANAGED_KEY_INVALID",
|
|
216
|
+
userMessage:
|
|
217
|
+
"The assistant API key is invalid. Attempting to re-provision…",
|
|
218
|
+
retryable: true,
|
|
219
|
+
errorCategory: "managed_key_invalid",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
224
|
+
userMessage:
|
|
225
|
+
"Your API key is invalid or expired. Update it in Settings or switch to managed mode.",
|
|
226
|
+
retryable: false,
|
|
227
|
+
errorCategory: "provider_not_configured",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
203
231
|
if (error.statusCode === 401) {
|
|
204
232
|
return {
|
|
205
|
-
code: "
|
|
206
|
-
userMessage:
|
|
233
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
234
|
+
userMessage:
|
|
235
|
+
"Your API key is invalid or expired. Update it in Settings or switch to managed mode.",
|
|
207
236
|
retryable: false,
|
|
208
|
-
errorCategory: "
|
|
237
|
+
errorCategory: "provider_not_configured",
|
|
209
238
|
};
|
|
210
239
|
}
|
|
211
240
|
if (error.statusCode === 402) {
|
|
@@ -275,10 +304,11 @@ function classifyCore(
|
|
|
275
304
|
)
|
|
276
305
|
) {
|
|
277
306
|
return {
|
|
278
|
-
code: "
|
|
279
|
-
userMessage:
|
|
307
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
308
|
+
userMessage:
|
|
309
|
+
"Your API key is invalid. Update it in Settings or switch to managed mode.",
|
|
280
310
|
retryable: false,
|
|
281
|
-
errorCategory: "
|
|
311
|
+
errorCategory: "provider_not_configured",
|
|
282
312
|
};
|
|
283
313
|
}
|
|
284
314
|
return {
|
|
@@ -296,6 +296,15 @@ export async function persistUserMessage(
|
|
|
296
296
|
cleanMessage,
|
|
297
297
|
attachmentInputs,
|
|
298
298
|
);
|
|
299
|
+
log.info(
|
|
300
|
+
{
|
|
301
|
+
contentBlockTypes: Array.isArray(llmMessage.content)
|
|
302
|
+
? llmMessage.content.map((b) => b.type)
|
|
303
|
+
: typeof llmMessage.content,
|
|
304
|
+
attachmentCount: attachments.length,
|
|
305
|
+
},
|
|
306
|
+
"persistUserMessage: content blocks being sent to model",
|
|
307
|
+
);
|
|
299
308
|
ctx.messages.push(llmMessage);
|
|
300
309
|
|
|
301
310
|
try {
|
|
@@ -1000,6 +1000,23 @@ export function stripInterfaceTurnContext(messages: Message[]): Message[] {
|
|
|
1000
1000
|
]);
|
|
1001
1001
|
}
|
|
1002
1002
|
|
|
1003
|
+
// ---------------------------------------------------------------------------
|
|
1004
|
+
// Transport hints injection (e.g. Slack thread context from the gateway)
|
|
1005
|
+
// ---------------------------------------------------------------------------
|
|
1006
|
+
|
|
1007
|
+
function injectTransportHints(message: Message, hints: string[]): Message {
|
|
1008
|
+
const block = `<transport_hints>\n${hints.join("\n")}\n</transport_hints>`;
|
|
1009
|
+
return {
|
|
1010
|
+
...message,
|
|
1011
|
+
content: [{ type: "text", text: block }, ...message.content],
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** Strip `<transport_hints>` blocks injected by `injectTransportHints`. */
|
|
1016
|
+
export function stripTransportHints(messages: Message[]): Message[] {
|
|
1017
|
+
return stripUserTextBlocksByPrefix(messages, ["<transport_hints>"]);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1003
1020
|
/** Prefixes stripped by the pipeline (order doesn't matter — single pass). */
|
|
1004
1021
|
const RUNTIME_INJECTION_PREFIXES = [
|
|
1005
1022
|
"<channel_capabilities>",
|
|
@@ -1018,6 +1035,7 @@ const RUNTIME_INJECTION_PREFIXES = [
|
|
|
1018
1035
|
"<active_dynamic_page>",
|
|
1019
1036
|
"<non_interactive_context>",
|
|
1020
1037
|
"<now_scratchpad>",
|
|
1038
|
+
"<transport_hints>",
|
|
1021
1039
|
];
|
|
1022
1040
|
|
|
1023
1041
|
/**
|
|
@@ -1064,6 +1082,7 @@ export function applyRuntimeInjections(
|
|
|
1064
1082
|
voiceCallControlPrompt?: string | null;
|
|
1065
1083
|
nowScratchpad?: string | null;
|
|
1066
1084
|
isNonInteractive?: boolean;
|
|
1085
|
+
transportHints?: string[] | null;
|
|
1067
1086
|
mode?: InjectionMode;
|
|
1068
1087
|
},
|
|
1069
1088
|
): Message[] {
|
|
@@ -1165,6 +1184,20 @@ export function applyRuntimeInjections(
|
|
|
1165
1184
|
}
|
|
1166
1185
|
}
|
|
1167
1186
|
|
|
1187
|
+
if (
|
|
1188
|
+
mode === "full" &&
|
|
1189
|
+
options.transportHints &&
|
|
1190
|
+
options.transportHints.length > 0
|
|
1191
|
+
) {
|
|
1192
|
+
const userTail = result[result.length - 1];
|
|
1193
|
+
if (userTail && userTail.role === "user") {
|
|
1194
|
+
result = [
|
|
1195
|
+
...result.slice(0, -1),
|
|
1196
|
+
injectTransportHints(userTail, options.transportHints),
|
|
1197
|
+
];
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1168
1201
|
// Temporal context is injected before workspace top-level so it
|
|
1169
1202
|
// appears after workspace context in the final message content
|
|
1170
1203
|
// (both are prepended, so later injections appear first).
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
UiSurfaceShow,
|
|
27
27
|
} from "./message-protocol.js";
|
|
28
28
|
import { INTERACTIVE_SURFACE_TYPES } from "./message-protocol.js";
|
|
29
|
+
import type { UserMessageAttachment } from "./message-types/shared.js";
|
|
29
30
|
|
|
30
31
|
const log = getLogger("conversation-surfaces");
|
|
31
32
|
|
|
@@ -217,7 +218,7 @@ export interface SurfaceConversationContext {
|
|
|
217
218
|
isProcessing(): boolean;
|
|
218
219
|
enqueueMessage(
|
|
219
220
|
content: string,
|
|
220
|
-
attachments:
|
|
221
|
+
attachments: UserMessageAttachment[],
|
|
221
222
|
onEvent: (msg: ServerMessage) => void,
|
|
222
223
|
requestId: string,
|
|
223
224
|
activeSurfaceId?: string,
|
|
@@ -229,7 +230,7 @@ export interface SurfaceConversationContext {
|
|
|
229
230
|
getQueueDepth(): number;
|
|
230
231
|
processMessage(
|
|
231
232
|
content: string,
|
|
232
|
-
attachments:
|
|
233
|
+
attachments: UserMessageAttachment[],
|
|
233
234
|
onEvent: (msg: ServerMessage) => void,
|
|
234
235
|
requestId?: string,
|
|
235
236
|
activeSurfaceId?: string,
|
|
@@ -625,6 +626,36 @@ export function handleSurfaceAction(
|
|
|
625
626
|
const accState = ctx.accumulatedSurfaceState.get(surfaceId);
|
|
626
627
|
const hasAccState = accState && Object.keys(accState).length > 0;
|
|
627
628
|
|
|
629
|
+
// Extract file attachments from action data so they are sent as proper
|
|
630
|
+
// image/file content blocks instead of dumping base64 into the text.
|
|
631
|
+
let attachments: UserMessageAttachment[] = [];
|
|
632
|
+
let actionDataForText = data;
|
|
633
|
+
if (data && Array.isArray(data.files)) {
|
|
634
|
+
const files = data.files as Array<Record<string, unknown>>;
|
|
635
|
+
attachments = files
|
|
636
|
+
.filter(
|
|
637
|
+
(f) =>
|
|
638
|
+
typeof f.filename === "string" &&
|
|
639
|
+
typeof f.mimeType === "string" &&
|
|
640
|
+
typeof f.data === "string",
|
|
641
|
+
)
|
|
642
|
+
.map((f) => ({
|
|
643
|
+
filename: f.filename as string,
|
|
644
|
+
mimeType: f.mimeType as string,
|
|
645
|
+
data: f.data as string,
|
|
646
|
+
...(typeof f.extractedText === "string"
|
|
647
|
+
? { extractedText: f.extractedText }
|
|
648
|
+
: {}),
|
|
649
|
+
}));
|
|
650
|
+
// Only remove files from the text payload when we successfully parsed
|
|
651
|
+
// attachments — otherwise preserve the original data so the model still
|
|
652
|
+
// sees the files field (e.g. IDs/paths from dynamic app actions).
|
|
653
|
+
if (attachments.length > 0) {
|
|
654
|
+
const { files: _files, ...rest } = data;
|
|
655
|
+
actionDataForText = Object.keys(rest).length > 0 ? rest : undefined;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
628
659
|
let content: string;
|
|
629
660
|
let displayContent: string | undefined;
|
|
630
661
|
if (prompt) {
|
|
@@ -639,8 +670,12 @@ export function handleSurfaceAction(
|
|
|
639
670
|
.replace(/_/g, " ")
|
|
640
671
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
641
672
|
content = `[User action on app: ${summary}]`;
|
|
642
|
-
if (
|
|
643
|
-
|
|
673
|
+
if (attachments.length > 0) {
|
|
674
|
+
const names = attachments.map((a) => a.filename).join(", ");
|
|
675
|
+
content += `\n\nUploaded files: ${names}`;
|
|
676
|
+
}
|
|
677
|
+
if (actionDataForText && Object.keys(actionDataForText).length > 0) {
|
|
678
|
+
content += `\n\nAction data: ${JSON.stringify(actionDataForText)}`;
|
|
644
679
|
}
|
|
645
680
|
if (hasAccState) {
|
|
646
681
|
content += `\n\nAccumulated surface state: ${JSON.stringify(accState)}`;
|
|
@@ -648,6 +683,23 @@ export function handleSurfaceAction(
|
|
|
648
683
|
displayContent = summary;
|
|
649
684
|
}
|
|
650
685
|
|
|
686
|
+
log.info(
|
|
687
|
+
{
|
|
688
|
+
surfaceId,
|
|
689
|
+
actionId,
|
|
690
|
+
contentLength: content.length,
|
|
691
|
+
contentPreview: content.slice(0, 200),
|
|
692
|
+
attachmentCount: attachments.length,
|
|
693
|
+
attachments: attachments.map((a) => ({
|
|
694
|
+
filename: a.filename,
|
|
695
|
+
mimeType: a.mimeType,
|
|
696
|
+
dataLength: a.data?.length ?? 0,
|
|
697
|
+
hasExtractedText: !!a.extractedText,
|
|
698
|
+
})),
|
|
699
|
+
},
|
|
700
|
+
"Surface action: preparing to send message to model",
|
|
701
|
+
);
|
|
702
|
+
|
|
651
703
|
const requestId = uuid();
|
|
652
704
|
ctx.surfaceActionRequestIds.add(requestId);
|
|
653
705
|
// Use broadcastToAllClients (publishes to the SSE event hub) instead of
|
|
@@ -665,7 +717,7 @@ export function handleSurfaceAction(
|
|
|
665
717
|
|
|
666
718
|
const result = ctx.enqueueMessage(
|
|
667
719
|
content,
|
|
668
|
-
|
|
720
|
+
attachments,
|
|
669
721
|
onEvent,
|
|
670
722
|
requestId,
|
|
671
723
|
surfaceId,
|
|
@@ -706,13 +758,13 @@ export function handleSurfaceAction(
|
|
|
706
758
|
|
|
707
759
|
// Conversation is idle — process the message immediately.
|
|
708
760
|
log.info(
|
|
709
|
-
{ surfaceId, actionId, requestId },
|
|
710
|
-
"Processing surface action immediately (history-restored)",
|
|
761
|
+
{ surfaceId, actionId, requestId, attachmentCount: attachments.length },
|
|
762
|
+
"Processing surface action immediately (history-restored) with attachments",
|
|
711
763
|
);
|
|
712
764
|
ctx
|
|
713
765
|
.processMessage(
|
|
714
766
|
content,
|
|
715
|
-
|
|
767
|
+
attachments,
|
|
716
768
|
onEvent,
|
|
717
769
|
requestId,
|
|
718
770
|
surfaceId,
|
|
@@ -807,11 +859,45 @@ export function handleSurfaceAction(
|
|
|
807
859
|
});
|
|
808
860
|
}
|
|
809
861
|
|
|
862
|
+
// Extract file attachments from action data so they are sent as proper
|
|
863
|
+
// image/file content blocks instead of dumping base64 into the text.
|
|
864
|
+
let pendingAttachments: UserMessageAttachment[] = [];
|
|
865
|
+
let mergedDataForText = mergedData;
|
|
866
|
+
if (mergedData && Array.isArray(mergedData.files)) {
|
|
867
|
+
const files = mergedData.files as Array<Record<string, unknown>>;
|
|
868
|
+
pendingAttachments = files
|
|
869
|
+
.filter(
|
|
870
|
+
(f) =>
|
|
871
|
+
typeof f.filename === "string" &&
|
|
872
|
+
typeof f.mimeType === "string" &&
|
|
873
|
+
typeof f.data === "string",
|
|
874
|
+
)
|
|
875
|
+
.map((f) => ({
|
|
876
|
+
filename: f.filename as string,
|
|
877
|
+
mimeType: f.mimeType as string,
|
|
878
|
+
data: f.data as string,
|
|
879
|
+
...(typeof f.extractedText === "string"
|
|
880
|
+
? { extractedText: f.extractedText }
|
|
881
|
+
: {}),
|
|
882
|
+
}));
|
|
883
|
+
// Only remove files from the text payload when we successfully parsed
|
|
884
|
+
// attachments — otherwise preserve the original data so the model still
|
|
885
|
+
// sees the files field.
|
|
886
|
+
if (pendingAttachments.length > 0) {
|
|
887
|
+
const { files: _files, ...rest } = mergedData;
|
|
888
|
+
mergedDataForText = Object.keys(rest).length > 0 ? rest : undefined;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
810
892
|
let fallbackContent = `[User action on ${pending.surfaceType} surface: ${summary}]`;
|
|
893
|
+
if (pendingAttachments.length > 0) {
|
|
894
|
+
const names = pendingAttachments.map((a) => a.filename).join(", ");
|
|
895
|
+
fallbackContent += `\n\nUploaded files: ${names}`;
|
|
896
|
+
}
|
|
811
897
|
// Append structured data so the LLM has access to IDs/values it needs
|
|
812
898
|
// to act on (e.g. selectedIds for archiving).
|
|
813
|
-
if (
|
|
814
|
-
fallbackContent += `\n\nAction data: ${JSON.stringify(
|
|
899
|
+
if (mergedDataForText && Object.keys(mergedDataForText).length > 0) {
|
|
900
|
+
fallbackContent += `\n\nAction data: ${JSON.stringify(mergedDataForText)}`;
|
|
815
901
|
}
|
|
816
902
|
// Append deselection context for table/list surfaces so the LLM knows what the user chose to keep.
|
|
817
903
|
const selectedIds = mergedData?.selectedIds as string[] | undefined;
|
|
@@ -867,9 +953,24 @@ export function handleSurfaceAction(
|
|
|
867
953
|
attributes: { source: "surface_action", surfaceId, actionId },
|
|
868
954
|
});
|
|
869
955
|
|
|
956
|
+
log.info(
|
|
957
|
+
{
|
|
958
|
+
surfaceId,
|
|
959
|
+
actionId,
|
|
960
|
+
attachmentCount: pendingAttachments.length,
|
|
961
|
+
attachments: pendingAttachments.map((a) => ({
|
|
962
|
+
filename: a.filename,
|
|
963
|
+
mimeType: a.mimeType,
|
|
964
|
+
dataLength: a.data?.length ?? 0,
|
|
965
|
+
})),
|
|
966
|
+
contentPreview: content.slice(0, 200),
|
|
967
|
+
},
|
|
968
|
+
"Surface action follow-up: preparing to send message to model",
|
|
969
|
+
);
|
|
970
|
+
|
|
870
971
|
const result = ctx.enqueueMessage(
|
|
871
972
|
content,
|
|
872
|
-
|
|
973
|
+
pendingAttachments,
|
|
873
974
|
onEvent,
|
|
874
975
|
requestId,
|
|
875
976
|
surfaceId,
|
|
@@ -929,13 +1030,18 @@ export function handleSurfaceAction(
|
|
|
929
1030
|
ctx.pendingSurfaceActions.delete(surfaceId);
|
|
930
1031
|
}
|
|
931
1032
|
log.info(
|
|
932
|
-
{
|
|
933
|
-
|
|
1033
|
+
{
|
|
1034
|
+
surfaceId,
|
|
1035
|
+
actionId,
|
|
1036
|
+
requestId,
|
|
1037
|
+
attachmentCount: pendingAttachments.length,
|
|
1038
|
+
},
|
|
1039
|
+
"Processing surface action as follow-up with attachments",
|
|
934
1040
|
);
|
|
935
1041
|
ctx
|
|
936
1042
|
.processMessage(
|
|
937
1043
|
content,
|
|
938
|
-
|
|
1044
|
+
pendingAttachments,
|
|
939
1045
|
onEvent,
|
|
940
1046
|
requestId,
|
|
941
1047
|
surfaceId,
|
|
@@ -187,6 +187,7 @@ export class Conversation {
|
|
|
187
187
|
/** @internal */ authContext?: AuthContext;
|
|
188
188
|
/** @internal */ loadedHistoryTrustClass?: TrustClass;
|
|
189
189
|
/** @internal */ voiceCallControlPrompt?: string;
|
|
190
|
+
/** @internal */ transportHints?: string[];
|
|
190
191
|
/** @internal */ assistantId?: string;
|
|
191
192
|
/** @internal */ commandIntent?: {
|
|
192
193
|
type: string;
|
|
@@ -892,6 +893,10 @@ export class Conversation {
|
|
|
892
893
|
this.voiceCallControlPrompt = prompt ?? undefined;
|
|
893
894
|
}
|
|
894
895
|
|
|
896
|
+
setTransportHints(hints: string[] | undefined): void {
|
|
897
|
+
this.transportHints = hints;
|
|
898
|
+
}
|
|
899
|
+
|
|
895
900
|
setAssistantId(assistantId: string | null): void {
|
|
896
901
|
this.assistantId = assistantId ?? undefined;
|
|
897
902
|
}
|
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
userMessage,
|
|
26
26
|
} from "../../providers/provider-send-message.js";
|
|
27
27
|
import { isTextMimeType as isTextMime } from "../../runtime/routes/workspace-utils.js";
|
|
28
|
+
import { getCatalog } from "../../skills/catalog-cache.js";
|
|
29
|
+
import { installSkillLocally } from "../../skills/catalog-install.js";
|
|
30
|
+
import { filterByQuery } from "../../skills/catalog-search.js";
|
|
28
31
|
import {
|
|
29
32
|
clawhubCheckUpdates,
|
|
30
33
|
clawhubInspect,
|
|
@@ -232,8 +235,9 @@ export interface SkillListItem {
|
|
|
232
235
|
description: string;
|
|
233
236
|
emoji?: string;
|
|
234
237
|
homepage?: string;
|
|
235
|
-
source: "bundled" | "managed" | "workspace" | "clawhub" | "extra";
|
|
238
|
+
source: "bundled" | "managed" | "workspace" | "clawhub" | "extra" | "catalog";
|
|
236
239
|
state: "enabled" | "disabled";
|
|
240
|
+
installStatus: "bundled" | "installed" | "available";
|
|
237
241
|
updateAvailable: boolean;
|
|
238
242
|
provenance: SkillProvenance;
|
|
239
243
|
}
|
|
@@ -259,6 +263,9 @@ export function listSkills(_ctx: SkillOperationContext): SkillListItem[] {
|
|
|
259
263
|
homepage: r.summary.homepage,
|
|
260
264
|
source: r.summary.source,
|
|
261
265
|
state: r.state,
|
|
266
|
+
installStatus: (r.summary.source === "bundled"
|
|
267
|
+
? "bundled"
|
|
268
|
+
: "installed") as SkillListItem["installStatus"],
|
|
262
269
|
updateAvailable: false,
|
|
263
270
|
provenance: resolveProvenance(r.summary),
|
|
264
271
|
}));
|
|
@@ -275,6 +282,54 @@ export function listSkills(_ctx: SkillOperationContext): SkillListItem[] {
|
|
|
275
282
|
return items;
|
|
276
283
|
}
|
|
277
284
|
|
|
285
|
+
/**
|
|
286
|
+
* List installed skills merged with available catalog skills.
|
|
287
|
+
* Installed skills take precedence when deduplicating by ID.
|
|
288
|
+
*/
|
|
289
|
+
export async function listSkillsWithCatalog(
|
|
290
|
+
ctx: SkillOperationContext,
|
|
291
|
+
): Promise<SkillListItem[]> {
|
|
292
|
+
const installed = listSkills(ctx);
|
|
293
|
+
const installedIds = new Set(installed.map((s) => s.id));
|
|
294
|
+
|
|
295
|
+
let catalogSkills: import("../../skills/catalog-install.js").CatalogSkill[];
|
|
296
|
+
try {
|
|
297
|
+
catalogSkills = await getCatalog();
|
|
298
|
+
} catch {
|
|
299
|
+
// If catalog fetch fails, return installed-only
|
|
300
|
+
return installed;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// All entries from the Vellum platform API are first-party.
|
|
304
|
+
// Create SkillListItems for catalog skills not already installed.
|
|
305
|
+
const available: SkillListItem[] = catalogSkills
|
|
306
|
+
.filter((cs) => !installedIds.has(cs.id))
|
|
307
|
+
.map((cs) => ({
|
|
308
|
+
id: cs.id,
|
|
309
|
+
name: cs.metadata?.vellum?.["display-name"] ?? cs.name,
|
|
310
|
+
description: cs.description,
|
|
311
|
+
emoji: cs.emoji,
|
|
312
|
+
homepage: undefined,
|
|
313
|
+
source: "catalog" as const,
|
|
314
|
+
state: "disabled" as const,
|
|
315
|
+
installStatus: "available" as const,
|
|
316
|
+
updateAvailable: false,
|
|
317
|
+
provenance: { kind: "first-party" as const, provider: "Vellum" },
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
const merged = [...installed, ...available];
|
|
321
|
+
|
|
322
|
+
// Sort using the same provenance sort + alphabetical
|
|
323
|
+
merged.sort((a, b) => {
|
|
324
|
+
const rankDiff =
|
|
325
|
+
provenanceSortRank(a.provenance) - provenanceSortRank(b.provenance);
|
|
326
|
+
if (rankDiff !== 0) return rankDiff;
|
|
327
|
+
return a.name.localeCompare(b.name);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return merged;
|
|
331
|
+
}
|
|
332
|
+
|
|
278
333
|
/** Look up a single skill by ID from the resolved catalog, returning its SkillListItem. */
|
|
279
334
|
function findSkillById(
|
|
280
335
|
skillId: string,
|
|
@@ -294,6 +349,7 @@ function findSkillById(
|
|
|
294
349
|
homepage: r.summary.homepage,
|
|
295
350
|
source: r.summary.source,
|
|
296
351
|
state: r.state,
|
|
352
|
+
installStatus: r.summary.source === "bundled" ? "bundled" : "installed",
|
|
297
353
|
updateAvailable: false,
|
|
298
354
|
provenance: resolveProvenance(r.summary),
|
|
299
355
|
};
|
|
@@ -519,6 +575,43 @@ export async function installSkill(
|
|
|
519
575
|
return { success: true };
|
|
520
576
|
}
|
|
521
577
|
|
|
578
|
+
// Check the Vellum catalog (first-party skills hosted on the platform)
|
|
579
|
+
try {
|
|
580
|
+
const vellumCatalog = await getCatalog();
|
|
581
|
+
const catalogEntry = vellumCatalog.find((s) => s.id === spec.slug);
|
|
582
|
+
if (catalogEntry) {
|
|
583
|
+
await installSkillLocally(spec.slug, catalogEntry, true);
|
|
584
|
+
|
|
585
|
+
// Reload skill catalog so the newly installed skill is picked up
|
|
586
|
+
loadSkillCatalog();
|
|
587
|
+
|
|
588
|
+
// Auto-enable the newly installed catalog skill
|
|
589
|
+
try {
|
|
590
|
+
const raw = loadRawConfig();
|
|
591
|
+
ensureSkillEntry(raw, spec.slug).enabled = true;
|
|
592
|
+
saveConfigWithSuppression(raw, ctx);
|
|
593
|
+
ctx.broadcast({
|
|
594
|
+
type: "skills_state_changed",
|
|
595
|
+
name: spec.slug,
|
|
596
|
+
state: "enabled",
|
|
597
|
+
});
|
|
598
|
+
} catch (err) {
|
|
599
|
+
log.warn(
|
|
600
|
+
{ err, skillId: spec.slug },
|
|
601
|
+
"Failed to auto-enable installed catalog skill",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return { success: true };
|
|
606
|
+
}
|
|
607
|
+
} catch (err) {
|
|
608
|
+
// If catalog lookup/install fails, fall through to clawhub
|
|
609
|
+
log.warn(
|
|
610
|
+
{ err, skillId: spec.slug },
|
|
611
|
+
"Vellum catalog install failed, falling back to community registry",
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
522
615
|
// Install from clawhub (community)
|
|
523
616
|
const result = await clawhubInstall(spec.slug, { version: spec.version });
|
|
524
617
|
if (!result.success) {
|
|
@@ -660,8 +753,60 @@ export async function searchSkills(
|
|
|
660
753
|
{ success: true; data: unknown } | { success: false; error: string }
|
|
661
754
|
> {
|
|
662
755
|
try {
|
|
663
|
-
|
|
664
|
-
|
|
756
|
+
// Search the loaded skill catalog (bundled + installed) for matches
|
|
757
|
+
const catalog = loadSkillCatalog();
|
|
758
|
+
const catalogMatches = filterByQuery(catalog, query, [
|
|
759
|
+
(s) => s.id,
|
|
760
|
+
(s) => s.displayName,
|
|
761
|
+
(s) => s.description,
|
|
762
|
+
]);
|
|
763
|
+
|
|
764
|
+
// Shape that matches ClawhubSearchResultItem so the client
|
|
765
|
+
// (Swift ClawhubSkillItem) can decode results uniformly.
|
|
766
|
+
interface SearchItem {
|
|
767
|
+
name: string;
|
|
768
|
+
slug: string;
|
|
769
|
+
description: string;
|
|
770
|
+
author: string;
|
|
771
|
+
stars: number;
|
|
772
|
+
installs: number;
|
|
773
|
+
version: string;
|
|
774
|
+
createdAt: number;
|
|
775
|
+
source: "vellum" | "clawhub";
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const catalogItems: SearchItem[] = catalogMatches.map((s) => ({
|
|
779
|
+
name: s.displayName,
|
|
780
|
+
slug: s.id,
|
|
781
|
+
description: s.description,
|
|
782
|
+
author: "Vellum",
|
|
783
|
+
stars: 0,
|
|
784
|
+
installs: 0,
|
|
785
|
+
version: "",
|
|
786
|
+
createdAt: 0,
|
|
787
|
+
source: "vellum" as const,
|
|
788
|
+
}));
|
|
789
|
+
|
|
790
|
+
// Search the community registry (non-fatal on failure)
|
|
791
|
+
let communitySkills: SearchItem[] = [];
|
|
792
|
+
try {
|
|
793
|
+
const communityResult = await clawhubSearch(query);
|
|
794
|
+
communitySkills = communityResult.skills;
|
|
795
|
+
} catch (err) {
|
|
796
|
+
log.warn(
|
|
797
|
+
{ err },
|
|
798
|
+
"clawhub search failed, returning catalog-only results",
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Deduplicate: catalog takes precedence when slugs collide
|
|
803
|
+
const catalogSlugs = new Set(catalogItems.map((s) => s.slug));
|
|
804
|
+
const deduped = communitySkills.filter((s) => !catalogSlugs.has(s.slug));
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
success: true,
|
|
808
|
+
data: { skills: [...catalogItems, ...deduped] },
|
|
809
|
+
};
|
|
665
810
|
} catch (err) {
|
|
666
811
|
const message = err instanceof Error ? err.message : String(err);
|
|
667
812
|
log.error({ err }, "Failed to search skills");
|
|
@@ -86,6 +86,14 @@ export class HostBashProxy {
|
|
|
86
86
|
clearTimeout(timer);
|
|
87
87
|
this.pending.delete(requestId);
|
|
88
88
|
this.onInternalResolve?.(requestId);
|
|
89
|
+
try {
|
|
90
|
+
this.sendToClient({
|
|
91
|
+
type: "host_bash_cancel",
|
|
92
|
+
requestId,
|
|
93
|
+
} as ServerMessage);
|
|
94
|
+
} catch {
|
|
95
|
+
// Best-effort cancel notification — connection may already be closed.
|
|
96
|
+
}
|
|
89
97
|
resolve(formatShellOutput("", "Aborted", null, false, 0));
|
|
90
98
|
}
|
|
91
99
|
};
|
|
@@ -144,6 +152,14 @@ export class HostBashProxy {
|
|
|
144
152
|
for (const [requestId, entry] of this.pending) {
|
|
145
153
|
clearTimeout(entry.timer);
|
|
146
154
|
this.onInternalResolve?.(requestId);
|
|
155
|
+
try {
|
|
156
|
+
this.sendToClient({
|
|
157
|
+
type: "host_bash_cancel",
|
|
158
|
+
requestId,
|
|
159
|
+
} as ServerMessage);
|
|
160
|
+
} catch {
|
|
161
|
+
// Best-effort cancel notification — connection may already be closed.
|
|
162
|
+
}
|
|
147
163
|
entry.reject(
|
|
148
164
|
new AssistantError(
|
|
149
165
|
"Host bash proxy disposed",
|