@vellumai/assistant 0.5.11 → 0.5.13
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 +42 -9
- 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/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- 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__/first-greeting.test.ts +7 -0
- 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__/navigate-settings-tab.test.ts +6 -2
- 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__/platform.test.ts +3 -168
- 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__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -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__/token-estimator-accuracy.benchmark.test.ts +1 -1
- 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/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- 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-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- 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/first-greeting.ts +6 -1
- 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 +56 -5
- 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/index.ts +0 -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/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- 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/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -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 +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- 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 +45 -15
- 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/browser/browser-manager.ts +2 -2
- 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/platform.ts +1 -91
- 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
|
@@ -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
|
}
|
|
@@ -22,7 +22,12 @@ export function isWakeUpGreeting(
|
|
|
22
22
|
): boolean {
|
|
23
23
|
if (conversationMessageCount !== 0) return false;
|
|
24
24
|
if (!existsSync(getWorkspacePromptPath("BOOTSTRAP.md"))) return false;
|
|
25
|
-
return
|
|
25
|
+
return (
|
|
26
|
+
content
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[.!?]+$/, "") === "wake up, my friend"
|
|
30
|
+
);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
@@ -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",
|
|
@@ -170,6 +170,14 @@ export class HostCuProxy {
|
|
|
170
170
|
clearTimeout(timer);
|
|
171
171
|
this.pending.delete(requestId);
|
|
172
172
|
this.onInternalResolve?.(requestId);
|
|
173
|
+
try {
|
|
174
|
+
this.sendToClient({
|
|
175
|
+
type: "host_cu_cancel",
|
|
176
|
+
requestId,
|
|
177
|
+
} as ServerMessage);
|
|
178
|
+
} catch {
|
|
179
|
+
// Best-effort cancel notification — connection may already be closed.
|
|
180
|
+
}
|
|
173
181
|
resolve({ content: "Aborted", isError: true });
|
|
174
182
|
}
|
|
175
183
|
};
|
|
@@ -381,6 +389,14 @@ export class HostCuProxy {
|
|
|
381
389
|
for (const [requestId, entry] of this.pending) {
|
|
382
390
|
clearTimeout(entry.timer);
|
|
383
391
|
this.onInternalResolve?.(requestId);
|
|
392
|
+
try {
|
|
393
|
+
this.sendToClient({
|
|
394
|
+
type: "host_cu_cancel",
|
|
395
|
+
requestId,
|
|
396
|
+
} as ServerMessage);
|
|
397
|
+
} catch {
|
|
398
|
+
// Best-effort cancel notification — connection may already be closed.
|
|
399
|
+
}
|
|
384
400
|
entry.reject(
|
|
385
401
|
new AssistantError("Host CU proxy disposed", ErrorCode.INTERNAL_ERROR),
|
|
386
402
|
);
|
|
@@ -82,6 +82,14 @@ export class HostFileProxy {
|
|
|
82
82
|
clearTimeout(timer);
|
|
83
83
|
this.pending.delete(requestId);
|
|
84
84
|
this.onInternalResolve?.(requestId);
|
|
85
|
+
try {
|
|
86
|
+
this.sendToClient({
|
|
87
|
+
type: "host_file_cancel",
|
|
88
|
+
requestId,
|
|
89
|
+
} as ServerMessage);
|
|
90
|
+
} catch {
|
|
91
|
+
// Best-effort cancel notification — connection may already be closed.
|
|
92
|
+
}
|
|
85
93
|
resolve({ content: "Aborted", isError: true });
|
|
86
94
|
}
|
|
87
95
|
};
|
|
@@ -123,6 +131,14 @@ export class HostFileProxy {
|
|
|
123
131
|
for (const [requestId, entry] of this.pending) {
|
|
124
132
|
clearTimeout(entry.timer);
|
|
125
133
|
this.onInternalResolve?.(requestId);
|
|
134
|
+
try {
|
|
135
|
+
this.sendToClient({
|
|
136
|
+
type: "host_file_cancel",
|
|
137
|
+
requestId,
|
|
138
|
+
} as ServerMessage);
|
|
139
|
+
} catch {
|
|
140
|
+
// Best-effort cancel notification — connection may already be closed.
|
|
141
|
+
}
|
|
126
142
|
entry.reject(
|
|
127
143
|
new AssistantError(
|
|
128
144
|
"Host file proxy disposed",
|