@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.
Files changed (199) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/bun.lock +139 -2
  5. package/docs/architecture/integrations.md +7 -11
  6. package/package.json +2 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +439 -108
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/cli.test.ts +42 -1
  15. package/src/__tests__/config-schema.test.ts +11 -127
  16. package/src/__tests__/config-watcher.test.ts +0 -8
  17. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  19. package/src/__tests__/diff.test.ts +22 -0
  20. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  21. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  22. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  23. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  24. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  25. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  26. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  27. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  29. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  30. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  31. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  32. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  33. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  34. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  35. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  36. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  39. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  40. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  41. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  42. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  43. package/src/__tests__/system-prompt.test.ts +1 -1
  44. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  45. package/src/__tests__/terminal-tools.test.ts +2 -93
  46. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  47. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  48. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  49. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  50. package/src/agent/loop.ts +36 -1
  51. package/src/approvals/approval-primitive.ts +381 -0
  52. package/src/approvals/guardian-decision-primitive.ts +191 -0
  53. package/src/calls/call-controller.ts +252 -209
  54. package/src/calls/call-domain.ts +44 -6
  55. package/src/calls/guardian-dispatch.ts +48 -0
  56. package/src/calls/types.ts +1 -1
  57. package/src/calls/voice-session-bridge.ts +46 -30
  58. package/src/cli/core-commands.ts +0 -4
  59. package/src/cli/mcp.ts +58 -0
  60. package/src/cli.ts +76 -34
  61. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  62. package/src/config/assistant-feature-flags.ts +162 -0
  63. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  64. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  65. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  66. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  67. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  68. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  69. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  70. package/src/config/core-schema.ts +1 -1
  71. package/src/config/env-registry.ts +10 -0
  72. package/src/config/feature-flag-registry.json +61 -0
  73. package/src/config/loader.ts +22 -1
  74. package/src/config/mcp-schema.ts +46 -0
  75. package/src/config/sandbox-schema.ts +0 -39
  76. package/src/config/schema.ts +18 -2
  77. package/src/config/skill-state.ts +34 -0
  78. package/src/config/skills-schema.ts +0 -1
  79. package/src/config/skills.ts +9 -0
  80. package/src/config/system-prompt.ts +110 -46
  81. package/src/config/templates/SOUL.md +1 -1
  82. package/src/config/types.ts +19 -1
  83. package/src/config/vellum-skills/catalog.json +1 -1
  84. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  85. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  86. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
  87. package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
  88. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  89. package/src/daemon/config-watcher.ts +0 -1
  90. package/src/daemon/daemon-control.ts +1 -1
  91. package/src/daemon/guardian-invite-intent.ts +124 -0
  92. package/src/daemon/handlers/avatar.ts +68 -0
  93. package/src/daemon/handlers/browser.ts +2 -2
  94. package/src/daemon/handlers/guardian-actions.ts +120 -0
  95. package/src/daemon/handlers/index.ts +4 -0
  96. package/src/daemon/handlers/sessions.ts +19 -0
  97. package/src/daemon/handlers/shared.ts +3 -1
  98. package/src/daemon/install-cli-launchers.ts +58 -13
  99. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  100. package/src/daemon/ipc-contract/sessions.ts +8 -2
  101. package/src/daemon/ipc-contract/settings.ts +25 -2
  102. package/src/daemon/ipc-contract-inventory.json +10 -0
  103. package/src/daemon/ipc-contract.ts +4 -0
  104. package/src/daemon/lifecycle.ts +14 -2
  105. package/src/daemon/main.ts +1 -0
  106. package/src/daemon/providers-setup.ts +26 -1
  107. package/src/daemon/server.ts +1 -0
  108. package/src/daemon/session-lifecycle.ts +52 -7
  109. package/src/daemon/session-memory.ts +45 -0
  110. package/src/daemon/session-process.ts +258 -432
  111. package/src/daemon/session-runtime-assembly.ts +12 -0
  112. package/src/daemon/session-skill-tools.ts +14 -1
  113. package/src/daemon/session-tool-setup.ts +5 -0
  114. package/src/daemon/session.ts +11 -0
  115. package/src/daemon/shutdown-handlers.ts +11 -0
  116. package/src/daemon/tool-side-effects.ts +35 -9
  117. package/src/index.ts +2 -2
  118. package/src/mcp/client.ts +152 -0
  119. package/src/mcp/manager.ts +139 -0
  120. package/src/memory/conversation-display-order-migration.ts +44 -0
  121. package/src/memory/conversation-queries.ts +2 -0
  122. package/src/memory/conversation-store.ts +91 -0
  123. package/src/memory/db-init.ts +5 -1
  124. package/src/memory/embedding-local.ts +13 -8
  125. package/src/memory/guardian-action-store.ts +125 -2
  126. package/src/memory/ingress-invite-store.ts +95 -1
  127. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  128. package/src/memory/migrations/index.ts +2 -1
  129. package/src/memory/schema.ts +5 -1
  130. package/src/memory/scoped-approval-grants.ts +14 -5
  131. package/src/messaging/providers/slack/client.ts +12 -0
  132. package/src/messaging/providers/slack/types.ts +5 -0
  133. package/src/notifications/decision-engine.ts +49 -12
  134. package/src/notifications/emit-signal.ts +7 -0
  135. package/src/notifications/signal.ts +7 -0
  136. package/src/notifications/thread-seed-composer.ts +2 -1
  137. package/src/runtime/channel-approval-types.ts +16 -6
  138. package/src/runtime/channel-approvals.ts +19 -15
  139. package/src/runtime/channel-invite-transport.ts +85 -0
  140. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  141. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  142. package/src/runtime/guardian-action-message-composer.ts +30 -0
  143. package/src/runtime/guardian-decision-types.ts +91 -0
  144. package/src/runtime/http-server.ts +23 -1
  145. package/src/runtime/ingress-service.ts +22 -0
  146. package/src/runtime/invite-redemption-service.ts +181 -0
  147. package/src/runtime/invite-redemption-templates.ts +39 -0
  148. package/src/runtime/routes/call-routes.ts +2 -1
  149. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  150. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  151. package/src/runtime/routes/identity-routes.ts +73 -0
  152. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  153. package/src/runtime/routes/pairing-routes.ts +4 -0
  154. package/src/security/encrypted-store.ts +31 -17
  155. package/src/security/keychain.ts +176 -2
  156. package/src/security/secure-keys.ts +97 -0
  157. package/src/security/tool-approval-digest.ts +1 -1
  158. package/src/tools/browser/browser-execution.ts +2 -2
  159. package/src/tools/browser/browser-manager.ts +46 -32
  160. package/src/tools/browser/browser-screencast.ts +2 -2
  161. package/src/tools/calls/call-start.ts +1 -1
  162. package/src/tools/executor.ts +22 -17
  163. package/src/tools/mcp/mcp-tool-factory.ts +100 -0
  164. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  165. package/src/tools/registry.ts +64 -1
  166. package/src/tools/skills/load.ts +22 -8
  167. package/src/tools/system/avatar-generator.ts +119 -0
  168. package/src/tools/system/navigate-settings.ts +65 -0
  169. package/src/tools/system/open-system-settings.ts +75 -0
  170. package/src/tools/system/voice-config.ts +121 -32
  171. package/src/tools/terminal/backends/native.ts +40 -19
  172. package/src/tools/terminal/backends/types.ts +3 -3
  173. package/src/tools/terminal/parser.ts +1 -1
  174. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  175. package/src/tools/terminal/sandbox.ts +1 -12
  176. package/src/tools/terminal/shell.ts +3 -31
  177. package/src/tools/tool-approval-handler.ts +141 -3
  178. package/src/tools/tool-manifest.ts +6 -0
  179. package/src/tools/types.ts +10 -2
  180. package/src/util/diff.ts +36 -13
  181. package/Dockerfile.sandbox +0 -5
  182. package/src/__tests__/doordash-client.test.ts +0 -187
  183. package/src/__tests__/doordash-session.test.ts +0 -154
  184. package/src/__tests__/signup-e2e.test.ts +0 -354
  185. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  186. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  187. package/src/cli/doordash.ts +0 -1057
  188. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  189. package/src/config/templates/LOOKS.md +0 -25
  190. package/src/doordash/cart-queries.ts +0 -787
  191. package/src/doordash/client.ts +0 -1016
  192. package/src/doordash/order-queries.ts +0 -85
  193. package/src/doordash/queries.ts +0 -13
  194. package/src/doordash/query-extractor.ts +0 -94
  195. package/src/doordash/search-queries.ts +0 -203
  196. package/src/doordash/session.ts +0 -84
  197. package/src/doordash/store-queries.ts +0 -246
  198. package/src/doordash/types.ts +0 -367
  199. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -0,0 +1,124 @@
1
+ // Guardian invite intent resolution for deterministic first-turn routing.
2
+ // Exports `resolveGuardianInviteIntent` as the single public entry point.
3
+ // When a guardian invite management request is detected, the session pipeline
4
+ // rewrites the message to force immediate entry into the trusted-contacts
5
+ // skill flow, bypassing the normal agent loop's tendency to produce conceptual
6
+ // preambles before loading the skill.
7
+
8
+ export type GuardianInviteIntentResult =
9
+ | { kind: 'none' }
10
+ | { kind: 'invite_management'; rewrittenContent: string; action?: 'create' | 'list' | 'revoke' };
11
+
12
+ // ── Direct invite patterns ────────────────────────────────────────────────
13
+ // These capture imperative requests to manage Telegram invite links.
14
+
15
+ const CREATE_INVITE_PATTERNS: RegExp[] = [
16
+ /\bcreate\s+(?:an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
17
+ /\binvite\s+(?:someone|somebody|a\s+friend|a\s+person)\s+(?:on|to|via|through)\s+telegram\b/i,
18
+ /\b(?:make|generate|get)\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
19
+ /\btelegram\s+invite\s*(?:link)?\b/i,
20
+ /\bsend\s+(?:a\s+|an\s+)?invite\s+(?:link\s+)?(?:on|for|via|through)\s+telegram\b/i,
21
+ /\bshare\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
22
+ /\binvite\s+(?:link\s+)?for\s+telegram\b/i,
23
+ ];
24
+
25
+ const LIST_INVITE_PATTERNS: RegExp[] = [
26
+ /\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:active\s+)?invite(?:s|\s*links?)\b/i,
27
+ /\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:telegram\s+)?invite(?:s|\s*links?)\b/i,
28
+ /\bwhat\s+invite(?:s|\s*links?)\s+(?:do\s+I\s+have|are\s+active|exist)\b/i,
29
+ /\bhow\s+many\s+invite(?:s|\s*links?)\b/i,
30
+ ];
31
+
32
+ const REVOKE_INVITE_PATTERNS: RegExp[] = [
33
+ /\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?invite\s*(?:link)?\b/i,
34
+ /\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
35
+ /\binvite\s*(?:link)?\s+(?:revoke|cancel|disable|invalidate|delete|remove)\b/i,
36
+ ];
37
+
38
+ // ── Conceptual / question patterns ──────────────────────────────────────
39
+ // These indicate the user is asking *about* invites rather than requesting
40
+ // to manage them. Return passthrough for these.
41
+
42
+ const CONCEPTUAL_PATTERNS: RegExp[] = [
43
+ /^\s*(?:how|what|why|when|where|who|which)\b.*\binvite/i,
44
+ /\bwhat\s+(?:is|are)\s+(?:an?\s+)?invite\s*(?:link)?\b/i,
45
+ /\bhow\s+(?:do|does|can)\s+(?:invite|invitation)s?\s+work\b/i,
46
+ /\bexplain\s+(?:the\s+)?invite\b/i,
47
+ /\btell\s+me\s+about\s+invite\b/i,
48
+ ];
49
+
50
+ /** Common polite/filler words stripped before checking intent-only status. */
51
+ const FILLER_PATTERN =
52
+ /\b(please|pls|plz|can\s+you|could\s+you|would\s+you|now|right\s+now|thanks|thank\s+you|thx|ty|for\s+me|ok(ay)?|hey|hi|hello|just|i\s+want\s+to|i'd\s+like\s+to|i\s+need\s+to|let's|let\s+me)\b/gi;
53
+
54
+ // ── Internal helpers ─────────────────────────────────────────────────────
55
+
56
+ function isConceptualQuestion(text: string): boolean {
57
+ const cleaned = text.replace(/^\s*(hey|hi|hello|please|pls|plz)[,\s]+/i, '');
58
+ // Allow actionable requests through even though they start with
59
+ // question-like words — these are imperative invite management requests.
60
+ if (LIST_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
61
+ if (CREATE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
62
+ if (REVOKE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
63
+ return CONCEPTUAL_PATTERNS.some((p) => p.test(cleaned));
64
+ }
65
+
66
+ function detectAction(text: string): 'create' | 'list' | 'revoke' | undefined {
67
+ // Check revoke and list before create — create patterns include the broad
68
+ // `telegram invite link` matcher that would otherwise swallow revoke/list inputs.
69
+ if (REVOKE_INVITE_PATTERNS.some((p) => p.test(text))) return 'revoke';
70
+ if (LIST_INVITE_PATTERNS.some((p) => p.test(text))) return 'list';
71
+ if (CREATE_INVITE_PATTERNS.some((p) => p.test(text))) return 'create';
72
+ return undefined;
73
+ }
74
+
75
+ // ── Structured intent resolver ───────────────────────────────────────────
76
+
77
+ /**
78
+ * Resolves guardian invite management intent from user text.
79
+ *
80
+ * Pipeline:
81
+ * 1. Skip slash commands entirely
82
+ * 2. Conceptual question gate -- questions return `none`
83
+ * 3. Detect create/list/revoke invite patterns
84
+ * 4. On match, build a deterministic model instruction to load trusted-contacts
85
+ */
86
+ export function resolveGuardianInviteIntent(text: string): GuardianInviteIntentResult {
87
+ const trimmed = text.trim();
88
+
89
+ // Never intercept slash commands
90
+ if (trimmed.startsWith('/')) {
91
+ return { kind: 'none' };
92
+ }
93
+
94
+ // Conceptual questions pass through to normal agent processing
95
+ if (isConceptualQuestion(trimmed)) {
96
+ return { kind: 'none' };
97
+ }
98
+
99
+ // Strip fillers for pattern matching but keep original for context
100
+ const withoutFillers = trimmed.replace(FILLER_PATTERN, '').replace(/\s{2,}/g, ' ').trim();
101
+
102
+ const action = detectAction(withoutFillers);
103
+ if (!action) {
104
+ return { kind: 'none' };
105
+ }
106
+
107
+ // Build the rewritten content that deterministically loads the skill
108
+ const actionDescriptions: Record<string, string> = {
109
+ create: 'The user wants to create a Telegram invite link. Create the invite, look up the bot username, and present the shareable deep link with copy-paste instructions.',
110
+ list: 'The user wants to see their invite links. List all invites (especially active ones for Telegram) and present them in a readable format.',
111
+ revoke: 'The user wants to revoke an invite link. List invites to identify the target, confirm with the user, then revoke it.',
112
+ };
113
+
114
+ const rewrittenContent = [
115
+ actionDescriptions[action],
116
+ 'Please invoke the "Trusted Contacts" skill (ID: trusted-contacts) immediately using skill_load.',
117
+ ].join('\n');
118
+
119
+ return {
120
+ kind: 'invite_management',
121
+ rewrittenContent,
122
+ action,
123
+ };
124
+ }
@@ -0,0 +1,68 @@
1
+ import * as net from 'node:net';
2
+ import { join } from 'node:path';
3
+
4
+ import { setAvatarTool } from '../../tools/system/avatar-generator.js';
5
+ import { getWorkspaceDir } from '../../util/platform.js';
6
+ import type { GenerateAvatarRequest } from '../ipc-contract/settings.js';
7
+ import { defineHandlers, type HandlerContext, log } from './shared.js';
8
+
9
+ /**
10
+ * Handle a client request to generate a custom avatar via Gemini.
11
+ * Invokes the set_avatar tool directly, sends a response to the requesting
12
+ * client, and broadcasts avatar_updated to all clients on success.
13
+ */
14
+ async function handleGenerateAvatar(
15
+ msg: GenerateAvatarRequest,
16
+ socket: net.Socket,
17
+ ctx: HandlerContext,
18
+ ): Promise<void> {
19
+ const description = msg.description?.trim();
20
+ if (!description) {
21
+ ctx.send(socket, {
22
+ type: 'generate_avatar_response',
23
+ success: false,
24
+ error: 'Description is required.',
25
+ });
26
+ return;
27
+ }
28
+
29
+ log.info({ description }, 'Generating avatar via IPC request');
30
+
31
+ try {
32
+ const result = await setAvatarTool.execute(
33
+ { description },
34
+ // Minimal tool context — avatar generation needs no session context
35
+ {} as Parameters<typeof setAvatarTool.execute>[1],
36
+ );
37
+
38
+ if (result.isError) {
39
+ ctx.send(socket, {
40
+ type: 'generate_avatar_response',
41
+ success: false,
42
+ error: result.content,
43
+ });
44
+ return;
45
+ }
46
+
47
+ // Broadcast avatar change to all connected clients
48
+ const avatarPath = join(getWorkspaceDir(), 'data', 'avatar', 'custom-avatar.png');
49
+ ctx.broadcast({ type: 'avatar_updated', avatarPath });
50
+
51
+ ctx.send(socket, {
52
+ type: 'generate_avatar_response',
53
+ success: true,
54
+ });
55
+ } catch (err) {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ log.error({ error: message }, 'Avatar generation failed unexpectedly');
58
+ ctx.send(socket, {
59
+ type: 'generate_avatar_response',
60
+ success: false,
61
+ error: message,
62
+ });
63
+ }
64
+ }
65
+
66
+ export const avatarHandlers = defineHandlers({
67
+ generate_avatar: handleGenerateAvatar,
68
+ });
@@ -1,4 +1,4 @@
1
- import { browserManager } from '../../tools/browser/browser-manager.js';
1
+ import { browserManager, SCREENCAST_HEIGHT,SCREENCAST_WIDTH } from '../../tools/browser/browser-manager.js';
2
2
  import { defineHandlers,log } from './shared.js';
3
3
 
4
4
  export const browserHandlers = defineHandlers({
@@ -10,7 +10,7 @@ export const browserHandlers = defineHandlers({
10
10
  try {
11
11
  const page = await browserManager.getOrCreateSessionPage(msg.sessionId);
12
12
  const viewport = await page.evaluate('(() => ({ vw: window.innerWidth, vh: window.innerHeight }))()') as { vw: number; vh: number };
13
- const scale = Math.min(1280 / viewport.vw, 960 / viewport.vh);
13
+ const scale = Math.min(SCREENCAST_WIDTH / viewport.vw, SCREENCAST_HEIGHT / viewport.vh);
14
14
  const pageX = msg.x / scale;
15
15
  const pageY = msg.y / scale;
16
16
  const options: Record<string, unknown> = {};
@@ -0,0 +1,120 @@
1
+ import { applyGuardianDecision } from '../../approvals/guardian-decision-primitive.js';
2
+ import { getPendingApprovalForRequest } from '../../memory/channel-guardian-store.js';
3
+ import type { ApprovalAction } from '../../runtime/channel-approval-types.js';
4
+ import { handleChannelDecision } from '../../runtime/channel-approvals.js';
5
+ import * as pendingInteractions from '../../runtime/pending-interactions.js';
6
+ import { handleAccessRequestDecision } from '../../runtime/routes/access-request-decision.js';
7
+ import { listGuardianDecisionPrompts } from '../../runtime/routes/guardian-action-routes.js';
8
+ import type { GuardianActionDecision, GuardianActionsPendingRequest } from '../ipc-protocol.js';
9
+ import { defineHandlers, log } from './shared.js';
10
+
11
+ const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_always', 'reject']);
12
+
13
+ export const guardianActionsHandlers = defineHandlers({
14
+ guardian_actions_pending_request: (msg: GuardianActionsPendingRequest, socket, ctx) => {
15
+ const prompts = listGuardianDecisionPrompts({ conversationId: msg.conversationId });
16
+ ctx.send(socket, { type: 'guardian_actions_pending_response', conversationId: msg.conversationId, prompts });
17
+ },
18
+
19
+ guardian_action_decision: (msg: GuardianActionDecision, socket, ctx) => {
20
+ // Validate the action is one of the known actions
21
+ if (!VALID_ACTIONS.has(msg.action)) {
22
+ log.warn({ requestId: msg.requestId, action: msg.action }, 'Invalid guardian action');
23
+ ctx.send(socket, {
24
+ type: 'guardian_action_decision_response',
25
+ applied: false,
26
+ reason: 'invalid_action',
27
+ requestId: msg.requestId,
28
+ });
29
+ return;
30
+ }
31
+
32
+ // Try the channel guardian approval store first (tool approval prompts)
33
+ const approval = getPendingApprovalForRequest(msg.requestId);
34
+ if (approval) {
35
+ // Enforce conversationId scoping when provided.
36
+ if (msg.conversationId && msg.conversationId !== approval.conversationId) {
37
+ log.warn({ requestId: msg.requestId, expected: approval.conversationId, got: msg.conversationId }, 'conversationId mismatch');
38
+ ctx.send(socket, {
39
+ type: 'guardian_action_decision_response',
40
+ applied: false,
41
+ reason: 'conversation_mismatch',
42
+ requestId: msg.requestId,
43
+ });
44
+ return;
45
+ }
46
+
47
+ // Access request approvals need a separate decision path — they don't have
48
+ // pending interactions and use verification sessions instead.
49
+ if (approval.toolName === 'ingress_access_request') {
50
+ const mappedAction = msg.action === 'reject' ? 'deny' as const : 'approve' as const;
51
+ // Use 'desktop' as the actor identity because this endpoint is
52
+ // unauthenticated — we cannot verify the caller is the assigned
53
+ // guardian, so we record a generic desktop origin instead of
54
+ // falsely attributing the decision to guardianExternalUserId.
55
+ const decisionResult = handleAccessRequestDecision(
56
+ approval,
57
+ mappedAction,
58
+ 'desktop',
59
+ );
60
+ ctx.send(socket, {
61
+ type: 'guardian_action_decision_response',
62
+ applied: decisionResult.type !== 'stale',
63
+ requestId: msg.requestId,
64
+ reason: decisionResult.type === 'stale' ? 'stale' : undefined,
65
+ });
66
+ return;
67
+ }
68
+
69
+ const result = applyGuardianDecision({
70
+ approval,
71
+ decision: { action: msg.action as 'approve_once' | 'approve_always' | 'reject', source: 'plain_text', requestId: msg.requestId },
72
+ actorExternalUserId: undefined,
73
+ actorChannel: 'vellum',
74
+ });
75
+ ctx.send(socket, {
76
+ type: 'guardian_action_decision_response',
77
+ applied: result.applied,
78
+ reason: result.reason,
79
+ requestId: result.requestId ?? msg.requestId,
80
+ });
81
+ return;
82
+ }
83
+
84
+ // Fall back to the pending interactions tracker (direct confirmation requests).
85
+ // Route through handleChannelDecision so approve_always properly persists trust rules.
86
+ const interaction = pendingInteractions.get(msg.requestId);
87
+ if (interaction) {
88
+ // Enforce conversationId scoping when provided.
89
+ if (msg.conversationId && msg.conversationId !== interaction.conversationId) {
90
+ log.warn({ requestId: msg.requestId, expected: interaction.conversationId, got: msg.conversationId }, 'conversationId mismatch');
91
+ ctx.send(socket, {
92
+ type: 'guardian_action_decision_response',
93
+ applied: false,
94
+ reason: 'conversation_mismatch',
95
+ requestId: msg.requestId,
96
+ });
97
+ return;
98
+ }
99
+
100
+ const result = handleChannelDecision(
101
+ interaction.conversationId,
102
+ { action: msg.action as ApprovalAction, source: 'plain_text', requestId: msg.requestId },
103
+ );
104
+ ctx.send(socket, {
105
+ type: 'guardian_action_decision_response',
106
+ applied: result.applied,
107
+ requestId: result.requestId ?? msg.requestId,
108
+ });
109
+ return;
110
+ }
111
+
112
+ log.warn({ requestId: msg.requestId }, 'No pending guardian action found for requestId');
113
+ ctx.send(socket, {
114
+ type: 'guardian_action_decision_response',
115
+ applied: false,
116
+ reason: 'not_found',
117
+ requestId: msg.requestId,
118
+ });
119
+ },
120
+ });
@@ -6,6 +6,7 @@ import type { ClientMessage } from '../ipc-protocol.js';
6
6
  import { handleRideShotgunStart, handleRideShotgunStop } from '../ride-shotgun-handler.js';
7
7
  import { handleWatchObservation } from '../watch-handler.js';
8
8
  import { appHandlers } from './apps.js';
9
+ import { avatarHandlers } from './avatar.js';
9
10
  import { browserHandlers } from './browser.js';
10
11
  import { computerUseHandlers } from './computer-use.js';
11
12
  import { configHandlers } from './config.js';
@@ -13,6 +14,7 @@ import { inboxInviteHandlers } from './config-inbox.js';
13
14
  import { diagnosticsHandlers } from './diagnostics.js';
14
15
  import { dictationHandlers } from './dictation.js';
15
16
  import { documentHandlers } from './documents.js';
17
+ import { guardianActionsHandlers } from './guardian-actions.js';
16
18
  import { homeBaseHandlers } from './home-base.js';
17
19
  import { identityHandlers } from './identity.js';
18
20
  import { miscHandlers } from './misc.js';
@@ -140,6 +142,7 @@ const handlers = {
140
142
  ...sessionHandlers,
141
143
  ...skillHandlers,
142
144
  ...appHandlers,
145
+ ...avatarHandlers,
143
146
  ...configHandlers,
144
147
  ...computerUseHandlers,
145
148
  ...publishHandlers,
@@ -147,6 +150,7 @@ const handlers = {
147
150
  ...diagnosticsHandlers,
148
151
  ...miscHandlers,
149
152
  ...documentHandlers,
153
+ ...guardianActionsHandlers,
150
154
  ...workItemHandlers,
151
155
  ...subagentHandlers,
152
156
  ...browserHandlers,
@@ -24,6 +24,7 @@ import type {
24
24
  HistoryRequest,
25
25
  MessageContentRequest,
26
26
  RegenerateRequest,
27
+ ReorderThreadsRequest,
27
28
  SandboxSetRequest,
28
29
  SecretResponse,
29
30
  ServerMessage,
@@ -547,6 +548,7 @@ export function handleSessionList(socket: net.Socket, ctx: HandlerContext, offse
547
548
  const conversationIds = conversations.map((c) => c.id);
548
549
  const bindings = externalConversationStore.getBindingsForConversations(conversationIds);
549
550
  const attentionStates = getAttentionStateByConversationIds(conversationIds);
551
+ const displayMetas = conversationStore.getDisplayMetaForConversations(conversationIds);
550
552
  ctx.send(socket, {
551
553
  type: 'session_list_response',
552
554
  sessions: conversations.map((c) => {
@@ -554,6 +556,7 @@ export function handleSessionList(socket: net.Socket, ctx: HandlerContext, offse
554
556
  const originChannel = parseChannelId(c.originChannel);
555
557
  const originInterface = parseInterfaceId(c.originInterface);
556
558
  const attn = attentionStates.get(c.id);
559
+ const displayMeta = displayMetas.get(c.id);
557
560
  const assistantAttention = attn ? {
558
561
  hasUnseenLatestAssistantMessage: attn.latestAssistantMessageAt != null &&
559
562
  (attn.lastSeenAssistantMessageAt == null || attn.lastSeenAssistantMessageAt < attn.latestAssistantMessageAt),
@@ -581,6 +584,8 @@ export function handleSessionList(socket: net.Socket, ctx: HandlerContext, offse
581
584
  ...(originChannel ? { conversationOriginChannel: originChannel } : {}),
582
585
  ...(originInterface ? { conversationOriginInterface: originInterface } : {}),
583
586
  ...(assistantAttention ? { assistantAttention } : {}),
587
+ ...(displayMeta?.displayOrder != null ? { displayOrder: displayMeta.displayOrder } : {}),
588
+ ...(displayMeta?.isPinned ? { isPinned: displayMeta.isPinned } : {}),
584
589
  };
585
590
  }),
586
591
  hasMore: offset + conversations.length < totalCount,
@@ -1145,6 +1150,19 @@ export function handleMessageContentRequest(
1145
1150
  });
1146
1151
  }
1147
1152
 
1153
+ export function handleReorderThreads(
1154
+ msg: ReorderThreadsRequest,
1155
+ _socket: net.Socket,
1156
+ _ctx: HandlerContext,
1157
+ ): void {
1158
+ if (!Array.isArray(msg.updates)) {
1159
+ return;
1160
+ }
1161
+ conversationStore.batchSetDisplayOrders(
1162
+ msg.updates.map((u) => ({ id: u.sessionId, displayOrder: u.displayOrder ?? null, isPinned: u.isPinned ?? false })),
1163
+ );
1164
+ }
1165
+
1148
1166
  export const sessionHandlers = defineHandlers({
1149
1167
  user_message: handleUserMessage,
1150
1168
  confirmation_response: handleConfirmationResponse,
@@ -1163,4 +1181,5 @@ export const sessionHandlers = defineHandlers({
1163
1181
  usage_request: handleUsageRequest,
1164
1182
  sandbox_set: handleSandboxSet,
1165
1183
  conversation_search: handleConversationSearch,
1184
+ reorder_threads: handleReorderThreads,
1166
1185
  });
@@ -177,8 +177,10 @@ export function getScreenDimensions(): { width: number; height: number } {
177
177
  if (cachedScreenDims) return cachedScreenDims;
178
178
  if (process.platform !== 'darwin') return FALLBACK_SCREEN;
179
179
  try {
180
+ // Use osascript (JXA) instead of `swift` to avoid the
181
+ // "Install Command Line Developer Tools" popup on fresh macOS installs.
180
182
  const out = execSync(
181
- `swift -e 'import CoreGraphics; let b = CGDisplayBounds(CGMainDisplayID()); print("\\(Int(b.width))x\\(Int(b.height))")'`,
183
+ `osascript -l JavaScript -e 'ObjC.import("AppKit"); var f = $.NSScreen.mainScreen.frame; Math.round(f.size.width) + "x" + Math.round(f.size.height)'`,
182
184
  { timeout: 10_000, encoding: 'utf-8' },
183
185
  ).trim();
184
186
  const [w, h] = out.split('x').map(Number);
@@ -1,24 +1,30 @@
1
1
  /**
2
2
  * Installs standalone CLI launcher scripts in ~/.vellum/bin/ so that
3
- * integration commands (e.g. `doordash`, `map`) can be invoked directly
4
- * without requiring `vellum` on PATH.
3
+ * integration commands (e.g. `map`) can be invoked directly without
4
+ * requiring `vellum` on PATH.
5
5
  *
6
6
  * Each launcher is a shell script that hardcodes absolute paths to `bun`
7
7
  * and the CLI entrypoint, forwarding all arguments to the appropriate
8
8
  * subcommand.
9
+ *
10
+ * Commands are split into two categories:
11
+ * - CORE_COMMANDS: always installed, dispatched via the main CLI entrypoint
12
+ * - Skill CLI launchers: dynamically discovered from installed skills that
13
+ * declare a `cli` entry in their SKILL.md frontmatter metadata
9
14
  */
10
15
 
11
16
  import { execSync } from 'node:child_process';
12
- import { chmodSync,existsSync, mkdirSync, writeFileSync } from 'node:fs';
17
+ import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
13
18
  import { homedir } from 'node:os';
14
19
  import { join } from 'node:path';
15
20
 
21
+ import { loadSkillCatalog } from '../config/skills.js';
16
22
  import { getLogger } from '../util/logger.js';
17
23
 
18
24
  const log = getLogger('install-cli-launchers');
19
25
 
20
- /** Integration subcommands that should get standalone launchers. */
21
- const INTEGRATION_COMMANDS = ['doordash', 'map'];
26
+ /** Core subcommands dispatched via the main CLI entrypoint (index.ts). */
27
+ const CORE_COMMANDS = ['map'];
22
28
 
23
29
  /**
24
30
  * Resolve the absolute path to the bun binary.
@@ -68,9 +74,13 @@ function hasSystemConflict(name: string, binDir: string): boolean {
68
74
  * Install standalone CLI launcher scripts in ~/.vellum/bin/.
69
75
  *
70
76
  * For each integration command, generates a shell script that execs
71
- * bun with the CLI entrypoint and the subcommand name prepended.
77
+ * bun with the appropriate entrypoint.
72
78
  * Uses the short name by default (e.g. `doordash`), falling back to
73
79
  * `vellum-<name>` if the short name conflicts with an existing system binary.
80
+ *
81
+ * Skill CLI launchers are discovered dynamically: any installed skill whose
82
+ * SKILL.md frontmatter declares `metadata.vellum.cli` will get a launcher
83
+ * pointing to the declared entry file within the skill directory.
74
84
  */
75
85
  export function installCliLaunchers(): void {
76
86
  const binDir = join(homedir(), '.vellum', 'bin');
@@ -83,13 +93,13 @@ export function installCliLaunchers(): void {
83
93
  return;
84
94
  }
85
95
 
86
- const entrypoint = resolveCliEntrypoint();
87
- if (!existsSync(entrypoint)) {
96
+ const mainEntrypoint = resolveCliEntrypoint();
97
+ if (!existsSync(mainEntrypoint)) {
88
98
  // In compiled builds (e.g. macOS app via `bun build --compile`), the
89
99
  // source tree isn't available. Launcher scripts are a dev-mode
90
100
  // convenience; compiled builds use their own command dispatch, so we
91
101
  // silently skip installation.
92
- log.debug({ entrypoint }, 'CLI entrypoint not found (compiled build?) — skipping launcher installation');
102
+ log.debug({ entrypoint: mainEntrypoint }, 'CLI entrypoint not found (compiled build?) — skipping launcher installation');
93
103
  return;
94
104
  }
95
105
 
@@ -97,18 +107,53 @@ export function installCliLaunchers(): void {
97
107
  mkdirSync(binDir, { recursive: true });
98
108
  }
99
109
 
100
- for (const name of INTEGRATION_COMMANDS) {
110
+ const installed: string[] = [];
111
+
112
+ // Install core command launchers (dispatched via main CLI)
113
+ for (const name of CORE_COMMANDS) {
101
114
  const launcherName = hasSystemConflict(name, binDir) ? `vellum-${name}` : name;
102
115
  const launcherPath = join(binDir, launcherName);
103
116
 
104
117
  const script = `#!/bin/bash
105
- exec "${bunPath}" "${entrypoint}" ${name} "$@"
118
+ exec "${bunPath}" "${mainEntrypoint}" ${name} "$@"
106
119
  `;
107
120
 
108
121
  writeFileSync(launcherPath, script);
109
122
  chmodSync(launcherPath, 0o755);
110
- log.debug({ launcherName, launcherPath }, 'Installed CLI launcher');
123
+ installed.push(launcherName);
124
+ log.debug({ launcherName, launcherPath }, 'Installed core CLI launcher');
125
+ }
126
+
127
+ // Discover and install skill CLI launchers from the skill catalog
128
+ try {
129
+ const catalog = loadSkillCatalog();
130
+ for (const skill of catalog) {
131
+ const cli = skill.metadata?.cli;
132
+ if (!cli?.command || !cli?.entry) continue;
133
+
134
+ const entrypoint = join(skill.directoryPath, cli.entry);
135
+ if (!existsSync(entrypoint)) {
136
+ log.debug({ skillId: skill.id, entrypoint }, 'Skill CLI entry point not found — skipping');
137
+ continue;
138
+ }
139
+
140
+ const launcherName = hasSystemConflict(cli.command, binDir)
141
+ ? `vellum-${cli.command}`
142
+ : cli.command;
143
+ const launcherPath = join(binDir, launcherName);
144
+
145
+ const script = `#!/bin/bash
146
+ exec "${bunPath}" "${entrypoint}" "$@"
147
+ `;
148
+
149
+ writeFileSync(launcherPath, script);
150
+ chmodSync(launcherPath, 0o755);
151
+ installed.push(launcherName);
152
+ log.debug({ launcherName, launcherPath, skillId: skill.id }, 'Installed skill CLI launcher');
153
+ }
154
+ } catch (err) {
155
+ log.warn({ err }, 'Failed to discover skill CLI launchers');
111
156
  }
112
157
 
113
- log.info({ binDir, commands: INTEGRATION_COMMANDS }, 'CLI launchers installed');
158
+ log.info({ binDir, commands: installed }, 'CLI launchers installed');
114
159
  }
@@ -0,0 +1,53 @@
1
+ // Guardian action decision IPC types.
2
+ // Enables desktop clients to fetch pending guardian prompts and submit
3
+ // button decisions deterministically (without text parsing).
4
+
5
+ // === Client -> Server ===
6
+
7
+ export interface GuardianActionsPendingRequest {
8
+ type: 'guardian_actions_pending_request';
9
+ conversationId: string;
10
+ }
11
+
12
+ export interface GuardianActionDecision {
13
+ type: 'guardian_action_decision';
14
+ requestId: string;
15
+ action: string;
16
+ conversationId?: string;
17
+ }
18
+
19
+ // === Server -> Client ===
20
+
21
+ export interface GuardianActionsPendingResponse {
22
+ type: 'guardian_actions_pending_response';
23
+ conversationId: string;
24
+ prompts: Array<{
25
+ requestId: string;
26
+ requestCode: string;
27
+ state: string;
28
+ questionText: string;
29
+ toolName: string | null;
30
+ actions: Array<{ action: string; label: string }>;
31
+ expiresAt: number;
32
+ conversationId: string;
33
+ callSessionId: string | null;
34
+ }>;
35
+ }
36
+
37
+ export interface GuardianActionDecisionResponse {
38
+ type: 'guardian_action_decision_response';
39
+ applied: boolean;
40
+ reason?: string;
41
+ requestId?: string;
42
+ userText?: string;
43
+ }
44
+
45
+ // --- Domain-level union aliases (consumed by the barrel file) ---
46
+
47
+ export type _GuardianActionsClientMessages =
48
+ | GuardianActionsPendingRequest
49
+ | GuardianActionDecision;
50
+
51
+ export type _GuardianActionsServerMessages =
52
+ | GuardianActionsPendingResponse
53
+ | GuardianActionDecisionResponse;
@@ -156,6 +156,11 @@ export interface ConversationSearchRequest {
156
156
  maxMessagesPerConversation?: number;
157
157
  }
158
158
 
159
+ export interface ReorderThreadsRequest {
160
+ type: 'reorder_threads';
161
+ updates: Array<{ sessionId: string; displayOrder: number | null; isPinned: boolean }>;
162
+ }
163
+
159
164
  // === Server → Client ===
160
165
 
161
166
  export interface ConversationSearchMatchingMessage {
@@ -213,7 +218,7 @@ export interface AssistantAttention {
213
218
 
214
219
  export interface SessionListResponse {
215
220
  type: 'session_list_response';
216
- sessions: Array<{ id: string; title: string; createdAt?: number; updatedAt: number; threadType?: ThreadType; source?: string; channelBinding?: ChannelBinding; conversationOriginChannel?: ChannelId; conversationOriginInterface?: InterfaceId; assistantAttention?: AssistantAttention }>;
221
+ sessions: Array<{ id: string; title: string; createdAt?: number; updatedAt: number; threadType?: ThreadType; source?: string; channelBinding?: ChannelBinding; conversationOriginChannel?: ChannelId; conversationOriginInterface?: InterfaceId; assistantAttention?: AssistantAttention; displayOrder?: number; isPinned?: boolean }>;
217
222
  /** Whether more sessions exist beyond the returned page. */
218
223
  hasMore?: boolean;
219
224
  }
@@ -392,7 +397,8 @@ export type _SessionsClientMessages =
392
397
  | SessionRenameRequest
393
398
  | SessionsClearRequest
394
399
  | ConversationSearchRequest
395
- | MessageContentRequest;
400
+ | MessageContentRequest
401
+ | ReorderThreadsRequest;
396
402
 
397
403
  export type _SessionsServerMessages =
398
404
  | AuthResult