@vellumai/assistant 0.3.3 → 0.3.5

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 (163) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +45 -18
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +391 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +397 -135
  12. package/src/__tests__/channel-approvals.test.ts +99 -3
  13. package/src/__tests__/channel-delivery-store.test.ts +30 -4
  14. package/src/__tests__/channel-guardian.test.ts +261 -22
  15. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  16. package/src/__tests__/config-schema.test.ts +2 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-lifecycle.test.ts +636 -0
  19. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  20. package/src/__tests__/entity-search.test.ts +615 -0
  21. package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
  22. package/src/__tests__/handlers-twilio-config.test.ts +480 -0
  23. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  24. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  25. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  26. package/src/__tests__/run-orchestrator.test.ts +22 -0
  27. package/src/__tests__/secret-scanner.test.ts +223 -0
  28. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  29. package/src/__tests__/shell-parser-property.test.ts +357 -2
  30. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  31. package/src/__tests__/system-prompt.test.ts +25 -1
  32. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  33. package/src/__tests__/twilio-routes.test.ts +39 -3
  34. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  35. package/src/__tests__/user-reference.test.ts +68 -0
  36. package/src/__tests__/web-search.test.ts +1 -1
  37. package/src/__tests__/work-item-output.test.ts +110 -0
  38. package/src/calls/call-domain.ts +8 -5
  39. package/src/calls/call-orchestrator.ts +85 -22
  40. package/src/calls/twilio-config.ts +17 -11
  41. package/src/calls/twilio-rest.ts +276 -0
  42. package/src/calls/twilio-routes.ts +39 -1
  43. package/src/cli/map.ts +6 -0
  44. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  45. package/src/commands/cc-command-registry.ts +14 -1
  46. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  47. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  48. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  49. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  50. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  51. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  52. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  53. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  54. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  55. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  56. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  57. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  58. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  59. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  60. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  61. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  62. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  63. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  64. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  65. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  66. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  67. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  68. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  69. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  70. package/src/config/bundled-skills/messaging/SKILL.md +24 -5
  71. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  72. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  73. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  74. package/src/config/defaults.ts +2 -1
  75. package/src/config/schema.ts +9 -3
  76. package/src/config/skills.ts +5 -32
  77. package/src/config/system-prompt.ts +40 -0
  78. package/src/config/templates/IDENTITY.md +2 -2
  79. package/src/config/user-reference.ts +29 -0
  80. package/src/config/vellum-skills/catalog.json +58 -0
  81. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  82. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  83. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  84. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
  86. package/src/daemon/auth-manager.ts +103 -0
  87. package/src/daemon/computer-use-session.ts +8 -1
  88. package/src/daemon/config-watcher.ts +253 -0
  89. package/src/daemon/handlers/config.ts +819 -22
  90. package/src/daemon/handlers/dictation.ts +182 -0
  91. package/src/daemon/handlers/identity.ts +14 -23
  92. package/src/daemon/handlers/index.ts +2 -0
  93. package/src/daemon/handlers/sessions.ts +2 -0
  94. package/src/daemon/handlers/shared.ts +3 -0
  95. package/src/daemon/handlers/skills.ts +6 -7
  96. package/src/daemon/handlers/work-items.ts +15 -7
  97. package/src/daemon/ipc-contract-inventory.json +10 -0
  98. package/src/daemon/ipc-contract.ts +114 -4
  99. package/src/daemon/ipc-handler.ts +87 -0
  100. package/src/daemon/lifecycle.ts +18 -4
  101. package/src/daemon/ride-shotgun-handler.ts +11 -1
  102. package/src/daemon/server.ts +111 -504
  103. package/src/daemon/session-agent-loop.ts +10 -15
  104. package/src/daemon/session-runtime-assembly.ts +115 -44
  105. package/src/daemon/session-tool-setup.ts +2 -0
  106. package/src/daemon/session.ts +19 -2
  107. package/src/inbound/public-ingress-urls.ts +3 -3
  108. package/src/memory/channel-guardian-store.ts +2 -1
  109. package/src/memory/db-connection.ts +28 -0
  110. package/src/memory/db-init.ts +1163 -0
  111. package/src/memory/db.ts +2 -2007
  112. package/src/memory/embedding-backend.ts +79 -11
  113. package/src/memory/indexer.ts +2 -0
  114. package/src/memory/job-handlers/media-processing.ts +100 -0
  115. package/src/memory/job-utils.ts +64 -4
  116. package/src/memory/jobs-store.ts +2 -1
  117. package/src/memory/jobs-worker.ts +11 -1
  118. package/src/memory/media-store.ts +759 -0
  119. package/src/memory/recall-cache.ts +107 -0
  120. package/src/memory/retriever.ts +36 -2
  121. package/src/memory/schema-migration.ts +984 -0
  122. package/src/memory/schema.ts +99 -0
  123. package/src/memory/search/entity.ts +208 -25
  124. package/src/memory/search/ranking.ts +6 -1
  125. package/src/memory/search/types.ts +26 -0
  126. package/src/messaging/provider-types.ts +2 -0
  127. package/src/messaging/providers/sms/adapter.ts +204 -0
  128. package/src/messaging/providers/sms/client.ts +93 -0
  129. package/src/messaging/providers/sms/types.ts +7 -0
  130. package/src/permissions/checker.ts +16 -2
  131. package/src/permissions/prompter.ts +14 -3
  132. package/src/permissions/trust-store.ts +7 -0
  133. package/src/runtime/approval-message-composer.ts +143 -0
  134. package/src/runtime/channel-approvals.ts +29 -7
  135. package/src/runtime/channel-guardian-service.ts +44 -18
  136. package/src/runtime/channel-readiness-service.ts +292 -0
  137. package/src/runtime/channel-readiness-types.ts +29 -0
  138. package/src/runtime/gateway-client.ts +2 -1
  139. package/src/runtime/http-server.ts +65 -28
  140. package/src/runtime/http-types.ts +3 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/channel-routes.ts +237 -103
  143. package/src/runtime/routes/run-routes.ts +7 -1
  144. package/src/runtime/run-orchestrator.ts +43 -3
  145. package/src/security/secret-scanner.ts +218 -0
  146. package/src/skills/frontmatter.ts +63 -0
  147. package/src/skills/slash-commands.ts +23 -0
  148. package/src/skills/vellum-catalog-remote.ts +107 -0
  149. package/src/tools/assets/materialize.ts +2 -2
  150. package/src/tools/browser/auto-navigate.ts +132 -24
  151. package/src/tools/browser/browser-manager.ts +67 -61
  152. package/src/tools/calls/call-start.ts +1 -0
  153. package/src/tools/claude-code/claude-code.ts +55 -3
  154. package/src/tools/credentials/vault.ts +1 -1
  155. package/src/tools/execution-target.ts +11 -1
  156. package/src/tools/executor.ts +10 -2
  157. package/src/tools/network/web-search.ts +1 -1
  158. package/src/tools/skills/vellum-catalog.ts +61 -156
  159. package/src/tools/terminal/parser.ts +21 -5
  160. package/src/tools/types.ts +2 -0
  161. package/src/twitter/router.ts +1 -1
  162. package/src/util/platform.ts +43 -1
  163. package/src/util/retry.ts +4 -4
@@ -10,9 +10,9 @@ import { getLogger } from '../../util/logger.js';
10
10
  const log = getLogger('auto-navigate');
11
11
 
12
12
  const CDP_BASE = 'http://localhost:9222';
13
- const MAX_PAGES = 15;
14
- const PAGE_WAIT_MS = 3500;
15
- const SCROLL_WAIT_MS = 2000;
13
+ const MAX_PAGES = 10;
14
+ const PAGE_WAIT_MS = 2500;
15
+ const SCROLL_WAIT_MS = 1000;
16
16
 
17
17
  /** Minimal CDP client — connects to one page tab. */
18
18
  class MiniCDP {
@@ -57,15 +57,28 @@ class MiniCDP {
57
57
  close() { this.ws?.close(); }
58
58
  }
59
59
 
60
+ export interface AutoNavProgress {
61
+ type: 'visiting' | 'discovered' | 'done';
62
+ url?: string;
63
+ pageNumber?: number;
64
+ totalDiscovered?: number;
65
+ visitedCount?: number;
66
+ }
67
+
60
68
  /**
61
69
  * Navigate Chrome through a domain's pages to trigger API calls.
62
70
  * Discovers internal links from the DOM and visits up to ~15 unique paths.
63
71
  *
64
72
  * @param domain The domain to crawl (e.g. "example.com").
65
73
  * @param abortSignal Optional signal to stop navigation early.
74
+ * @param onProgress Optional callback for live progress updates.
66
75
  * @returns List of visited page URLs.
67
76
  */
68
- export async function autoNavigate(domain: string, abortSignal?: { aborted: boolean }): Promise<string[]> {
77
+ export async function autoNavigate(
78
+ domain: string,
79
+ abortSignal?: { aborted: boolean },
80
+ onProgress?: (p: AutoNavProgress) => void,
81
+ ): Promise<string[]> {
69
82
  let wsUrl: string | null = null;
70
83
  try {
71
84
  const res = await fetch(`${CDP_BASE}/json/list`);
@@ -108,6 +121,7 @@ export async function autoNavigate(domain: string, abortSignal?: { aborted: bool
108
121
 
109
122
  // Navigate to the domain root first
110
123
  try {
124
+ onProgress?.({ type: 'visiting', url: rootUrl, pageNumber: 1 });
111
125
  await cdp.send('Page.navigate', { url: rootUrl });
112
126
  await sleep(PAGE_WAIT_MS);
113
127
  visited.add('/');
@@ -125,12 +139,11 @@ export async function autoNavigate(domain: string, abortSignal?: { aborted: bool
125
139
  await scrollPage(cdp);
126
140
  await sleep(SCROLL_WAIT_MS);
127
141
 
128
- // Click common interactive elements on the root page
129
- await clickInteractiveElements(cdp);
130
- await sleep(SCROLL_WAIT_MS);
131
-
132
142
  // Discover internal links from the current page
133
- const discoveredLinks = await discoverInternalLinks(cdp, domain);
143
+ let discoveredLinks = await discoverInternalLinks(cdp, domain);
144
+ // Sort links: deeper paths first (more likely to be content pages), skip shallow nav links
145
+ discoveredLinks = rankLinks(discoveredLinks);
146
+ onProgress?.({ type: 'discovered', totalDiscovered: discoveredLinks.length });
134
147
  log.info({ count: discoveredLinks.length }, 'Discovered internal links from root');
135
148
 
136
149
  // Visit discovered pages
@@ -140,6 +153,7 @@ export async function autoNavigate(domain: string, abortSignal?: { aborted: bool
140
153
  if (visited.has(link.key)) continue;
141
154
 
142
155
  const url = link.url;
156
+ onProgress?.({ type: 'visiting', url, pageNumber: visited.size + 1, totalDiscovered: discoveredLinks.length });
143
157
  log.info({ url }, 'Auto-navigate visiting page');
144
158
 
145
159
  try {
@@ -152,9 +166,9 @@ export async function autoNavigate(domain: string, abortSignal?: { aborted: bool
152
166
  await scrollPage(cdp);
153
167
  await sleep(SCROLL_WAIT_MS);
154
168
 
155
- // Click interactive elements to trigger more API calls
156
- await clickInteractiveElements(cdp);
157
- await sleep(1500);
169
+ // Click tabs/buttons within the page (NOT nav links — those navigate away)
170
+ await clickPageTabs(cdp);
171
+ await sleep(800);
158
172
 
159
173
  // Discover more links from this page
160
174
  const newLinks = await discoverInternalLinks(cdp, domain);
@@ -171,6 +185,7 @@ export async function autoNavigate(domain: string, abortSignal?: { aborted: bool
171
185
  }
172
186
 
173
187
  cdp.close();
188
+ onProgress?.({ type: 'done', visitedCount: visitedUrls.length, totalDiscovered: discoveredLinks.length });
174
189
  log.info({ visited: visitedUrls.length, total: discoveredLinks.length + 1 }, 'Auto-navigation finished');
175
190
  return visitedUrls;
176
191
  }
@@ -180,6 +195,56 @@ interface DiscoveredLink {
180
195
  url: string;
181
196
  /** Deduplication key: origin + pathname. */
182
197
  key: string;
198
+ /** Path depth (number of segments). */
199
+ depth: number;
200
+ }
201
+
202
+ /** Paths that are typically navigation chrome, not content pages. */
203
+ const SKIP_PATHS = [
204
+ '/home', '/login', '/signup', '/register', '/sign-up', '/sign-in',
205
+ '/help', '/support', '/contact', '/about', '/terms', '/privacy',
206
+ '/careers', '/press', '/blog', '/faq', '/sitemap',
207
+ ];
208
+
209
+ /** Path patterns that indicate high-value purchase/content flows. */
210
+ const HIGH_VALUE_PATTERNS = [
211
+ /\/orders/i, /\/cart/i, /\/checkout/i, /\/account/i, /\/settings/i,
212
+ /\/store\//i, /\/restaurant\//i, /\/menu/i, /\/payment/i,
213
+ /\/profile/i, /\/history/i, /\/favorites/i, /\/saved/i,
214
+ /\/search/i, /\/category/i, /\/collection/i,
215
+ ];
216
+
217
+ /** Sort links to prioritize purchase/content flows, deduplicate by pattern. */
218
+ function rankLinks(links: DiscoveredLink[]): DiscoveredLink[] {
219
+ const filtered = links.filter(l => {
220
+ const path = new URL(l.url).pathname.toLowerCase();
221
+ if (SKIP_PATHS.some(skip => path === skip || path === skip + '/')) return false;
222
+ return true;
223
+ });
224
+
225
+ // Deduplicate by host+path pattern — keep only one of /store/123, /store/456
226
+ // but preserve different subdomains (shop.example.com vs admin.example.com)
227
+ const byPattern = new Map<string, DiscoveredLink>();
228
+ for (const link of filtered) {
229
+ const parsed = new URL(link.url);
230
+ // Collapse numeric/hash segments to find the pattern
231
+ const pathPattern = parsed.pathname.replace(/\/\d+/g, '/{id}').replace(/\/[a-f0-9]{8,}/gi, '/{id}');
232
+ const pattern = parsed.hostname + pathPattern;
233
+ if (!byPattern.has(pattern)) {
234
+ byPattern.set(pattern, link);
235
+ }
236
+ }
237
+
238
+ return [...byPattern.values()].sort((a, b) => {
239
+ const aPath = new URL(a.url).pathname.toLowerCase();
240
+ const bPath = new URL(b.url).pathname.toLowerCase();
241
+ // High-value paths first
242
+ const aHighValue = HIGH_VALUE_PATTERNS.some(p => p.test(aPath)) ? 1 : 0;
243
+ const bHighValue = HIGH_VALUE_PATTERNS.some(p => p.test(bPath)) ? 1 : 0;
244
+ if (aHighValue !== bHighValue) return bHighValue - aHighValue;
245
+ // Then by depth (deeper = more specific)
246
+ return Math.min(b.depth, 4) - Math.min(a.depth, 4);
247
+ });
183
248
  }
184
249
 
185
250
  /** Extract internal links from the current page DOM, preserving subdomains. */
@@ -204,7 +269,11 @@ async function discoverInternalLinks(cdp: MiniCDP, domain: string): Promise<Disc
204
269
  const key = url.origin + url.pathname;
205
270
  if (!seen.has(key)) {
206
271
  seen.add(key);
207
- links.push({ url: url.origin + url.pathname, key });
272
+ links.push({
273
+ url: url.origin + url.pathname,
274
+ key,
275
+ depth: path.split('/').filter(Boolean).length,
276
+ });
208
277
  }
209
278
  } catch { /* skip malformed URLs */ }
210
279
  }
@@ -222,25 +291,64 @@ async function discoverInternalLinks(cdp: MiniCDP, domain: string): Promise<Disc
222
291
 
223
292
  /** Scroll the page to trigger lazy-loaded content. */
224
293
  async function scrollPage(cdp: MiniCDP): Promise<void> {
225
- await cdp.send('Runtime.evaluate', {
226
- expression: 'window.scrollBy(0, 800)',
227
- awaitPromise: false,
228
- }).catch(() => {});
294
+ // Scroll in increments to trigger multiple lazy-load thresholds
295
+ for (let i = 0; i < 3; i++) {
296
+ await cdp.send('Runtime.evaluate', {
297
+ expression: 'window.scrollBy(0, 600)',
298
+ awaitPromise: false,
299
+ }).catch(() => {});
300
+ await sleep(500);
301
+ }
229
302
  }
230
303
 
231
- /** Click common interactive elements (tabs, nav buttons) to trigger API calls. */
232
- async function clickInteractiveElements(cdp: MiniCDP): Promise<void> {
304
+ /**
305
+ * Click tabs, buttons, and flow-relevant elements within the current page.
306
+ * Avoids clicking navigation links (which would navigate away).
307
+ */
308
+ async function clickPageTabs(cdp: MiniCDP): Promise<void> {
233
309
  const selectors = [
234
- 'nav a:not([href="/"])',
235
- '[role="tab"]',
236
- '[role="tablist"] button',
310
+ '[role="tab"]:not(:first-child)',
311
+ '[role="tablist"] button:not(:first-child)',
237
312
  'button[data-tab]',
238
- '.tab, .nav-tab, .nav-link',
313
+ '[data-testid*="tab"]',
314
+ 'button[aria-expanded="false"]',
239
315
  ];
240
316
 
241
317
  for (const selector of selectors) {
242
318
  await clickInPage(cdp, selector);
243
- await sleep(800);
319
+ await sleep(600);
320
+ }
321
+
322
+ // Also try clicking purchase-flow buttons to trigger API calls
323
+ // (Add to Cart, etc. — these fire API requests even if we don't complete the flow)
324
+ await clickByText(cdp, 'Add to Cart');
325
+ await clickByText(cdp, 'Add to Order');
326
+ await clickByText(cdp, 'Add Item');
327
+ }
328
+
329
+ /** Click a button by its visible text content. */
330
+ async function clickByText(cdp: MiniCDP, text: string): Promise<boolean> {
331
+ try {
332
+ const result = await cdp.send('Runtime.evaluate', {
333
+ expression: `
334
+ (function() {
335
+ const buttons = document.querySelectorAll('button, [role="button"]');
336
+ for (const btn of buttons) {
337
+ if (btn.textContent && btn.textContent.trim().toLowerCase().includes(${JSON.stringify(text.toLowerCase())})) {
338
+ btn.scrollIntoView({ block: 'center' });
339
+ btn.click();
340
+ return true;
341
+ }
342
+ }
343
+ return false;
344
+ })()
345
+ `,
346
+ awaitPromise: false,
347
+ returnByValue: true,
348
+ }) as { result?: { value?: boolean } };
349
+ return result?.result?.value === true;
350
+ } catch {
351
+ return false;
244
352
  }
245
353
  }
246
354
 
@@ -191,74 +191,80 @@ class BrowserManager {
191
191
  if (this.contextCreating) return this.contextCreating;
192
192
 
193
193
  this.contextCreating = (async () => {
194
- // Try to detect or negotiate CDP before falling back to headless.
195
- // This auto-detects an existing Chrome with --remote-debugging-port,
196
- // or asks the client to restart Chrome with CDP enabled.
197
- let useCdp = this._browserMode === 'cdp';
198
- const sender = invokingSessionId ? this.sessionSenders.get(invokingSessionId) : undefined;
199
- if (!useCdp) {
200
- const cdpAvailable = await this.detectCDP();
201
- if (cdpAvailable) {
202
- useCdp = true;
203
- } else if (invokingSessionId && sender) {
204
- log.info({ sessionId: invokingSessionId }, 'Requesting CDP from client');
205
- const accepted = await this.requestCDPFromClient(invokingSessionId, sender);
206
- if (accepted) {
207
- const nowAvailable = await this.detectCDP();
208
- if (nowAvailable) {
209
- useCdp = true;
194
+ // Deterministic test mode: when launch is injected via setLaunchFn,
195
+ // bypass ambient CDP probing/negotiation and use the injected launcher.
196
+ const hasInjectedLaunchFn = launchPersistentContext !== null;
197
+
198
+ if (!hasInjectedLaunchFn) {
199
+ // Try to detect or negotiate CDP before falling back to headless.
200
+ // This auto-detects an existing Chrome with --remote-debugging-port,
201
+ // or asks the client to restart Chrome with CDP enabled.
202
+ let useCdp = this._browserMode === 'cdp';
203
+ const sender = invokingSessionId ? this.sessionSenders.get(invokingSessionId) : undefined;
204
+ if (!useCdp) {
205
+ const cdpAvailable = await this.detectCDP();
206
+ if (cdpAvailable) {
207
+ useCdp = true;
208
+ } else if (invokingSessionId && sender) {
209
+ log.info({ sessionId: invokingSessionId }, 'Requesting CDP from client');
210
+ const accepted = await this.requestCDPFromClient(invokingSessionId, sender);
211
+ if (accepted) {
212
+ const nowAvailable = await this.detectCDP();
213
+ if (nowAvailable) {
214
+ useCdp = true;
215
+ } else {
216
+ log.warn('Client accepted CDP request but CDP not detected');
217
+ }
210
218
  } else {
211
- log.warn('Client accepted CDP request but CDP not detected');
219
+ log.info('Client declined CDP request');
212
220
  }
213
- } else {
214
- log.info('Client declined CDP request');
215
221
  }
216
222
  }
217
- }
218
223
 
219
- if (useCdp) {
220
- try {
221
- const pw = await import('playwright');
222
- const browser = await pw.chromium.connectOverCDP(this.cdpUrl, { timeout: 10_000 });
223
- this.cdpBrowser = browser;
224
- this._browserLaunched = false;
225
- const contexts = browser.contexts();
226
- const ctx = contexts[0] || await browser.newContext();
227
- this.setBrowserMode('cdp');
228
- await this.initBrowserCdpSession();
229
- log.info({ cdpUrl: this.cdpUrl }, 'Connected to Chrome via CDP');
230
- return ctx as unknown as BrowserContext;
231
- } catch (err) {
232
- log.warn({ err }, 'CDP connectOverCDP failed');
233
- this._browserMode = 'headless';
224
+ if (useCdp) {
225
+ try {
226
+ const pw = await import('playwright');
227
+ const browser = await pw.chromium.connectOverCDP(this.cdpUrl, { timeout: 10_000 });
228
+ this.cdpBrowser = browser;
229
+ this._browserLaunched = false;
230
+ const contexts = browser.contexts();
231
+ const ctx = contexts[0] || await browser.newContext();
232
+ this.setBrowserMode('cdp');
233
+ await this.initBrowserCdpSession();
234
+ log.info({ cdpUrl: this.cdpUrl }, 'Connected to Chrome via CDP');
235
+ return ctx as unknown as BrowserContext;
236
+ } catch (err) {
237
+ log.warn({ err }, 'CDP connectOverCDP failed');
238
+ this._browserMode = 'headless';
239
+ }
234
240
  }
235
- }
236
241
 
237
- // If a client is connected, launch headed Chromium (minimized) so the user
238
- // can interact directly when handoff triggers (e.g. CAPTCHAs).
239
- // The window stays offscreen until bringToFront() is called during handoff.
240
- const hasSender = !!(invokingSessionId && this.sessionSenders.get(invokingSessionId));
241
- if (hasSender && this._browserMode === 'headless') {
242
- try {
243
- const pw2 = await import('playwright');
244
- const headedBrowser = await pw2.chromium.launch({
245
- channel: 'chrome',
246
- headless: false,
247
- args: [
248
- '--window-position=-32000,-32000',
249
- '--window-size=1,1',
250
- '--disable-blink-features=AutomationControlled',
251
- ],
252
- });
253
- const ctx = headedBrowser.contexts()[0] || await headedBrowser.newContext();
254
- this.cdpBrowser = headedBrowser as unknown as typeof this.cdpBrowser;
255
- this._browserLaunched = true;
256
- this.setBrowserMode('cdp');
257
- await this.initBrowserCdpSession();
258
- log.info('Launched headed Chromium (minimized) for interactive handoff support');
259
- return ctx as unknown as BrowserContext;
260
- } catch (err2) {
261
- log.warn({ err: err2 }, 'Headed Chromium launch failed, falling back to headless');
242
+ // If a client is connected, launch headed Chromium (minimized) so the user
243
+ // can interact directly when handoff triggers (e.g. CAPTCHAs).
244
+ // The window stays offscreen until bringToFront() is called during handoff.
245
+ const hasSender = !!(invokingSessionId && this.sessionSenders.get(invokingSessionId));
246
+ if (hasSender && this._browserMode === 'headless') {
247
+ try {
248
+ const pw2 = await import('playwright');
249
+ const headedBrowser = await pw2.chromium.launch({
250
+ channel: 'chrome',
251
+ headless: false,
252
+ args: [
253
+ '--window-position=-32000,-32000',
254
+ '--window-size=1,1',
255
+ '--disable-blink-features=AutomationControlled',
256
+ ],
257
+ });
258
+ const ctx = headedBrowser.contexts()[0] || await headedBrowser.newContext();
259
+ this.cdpBrowser = headedBrowser as unknown as typeof this.cdpBrowser;
260
+ this._browserLaunched = true;
261
+ this.setBrowserMode('cdp');
262
+ await this.initBrowserCdpSession();
263
+ log.info('Launched headed Chromium (minimized) for interactive handoff support');
264
+ return ctx as unknown as BrowserContext;
265
+ } catch (err2) {
266
+ log.warn({ err: err2 }, 'Headed Chromium launch failed, falling back to headless');
267
+ }
262
268
  }
263
269
  }
264
270
 
@@ -54,6 +54,7 @@ class CallStartTool implements Tool {
54
54
  task: input.task as string,
55
55
  context: input.context as string | undefined,
56
56
  conversationId: context.conversationId,
57
+ assistantId: context.assistantId,
57
58
  callerIdentityMode: input.caller_identity_mode as 'assistant_number' | 'user_number' | undefined,
58
59
  });
59
60
 
@@ -6,6 +6,7 @@ import { getLogger } from '../../util/logger.js';
6
6
  import { truncate } from '../../util/truncate.js';
7
7
  import { getProfilePolicy } from '../../swarm/worker-backend.js';
8
8
  import type { WorkerProfile } from '../../swarm/worker-backend.js';
9
+ import { getCCCommand, loadCCCommandTemplate } from '../../commands/cc-command-registry.js';
9
10
 
10
11
  const log = getLogger('claude-code-tool');
11
12
 
@@ -62,7 +63,15 @@ export const claudeCodeTool: Tool = {
62
63
  properties: {
63
64
  prompt: {
64
65
  type: 'string',
65
- description: 'The coding task or question for Claude Code to work on',
66
+ description: 'The coding task or question for Claude Code to work on. Use this for free-form tasks. Mutually exclusive with command.',
67
+ },
68
+ command: {
69
+ type: 'string',
70
+ description: 'Name of a .claude/commands/*.md command template to execute. The template will be loaded and $ARGUMENTS substituted before execution. Use this instead of prompt when invoking a named CC command.',
71
+ },
72
+ arguments: {
73
+ type: 'string',
74
+ description: 'Arguments to substitute into the command template ($ARGUMENTS placeholder). Only used with the command input.',
66
75
  },
67
76
  working_dir: {
68
77
  type: 'string',
@@ -82,7 +91,6 @@ export const claudeCodeTool: Tool = {
82
91
  description: 'Worker profile that scopes tool access. Defaults to general (backward compatible).',
83
92
  },
84
93
  },
85
- required: ['prompt'],
86
94
  },
87
95
  };
88
96
  },
@@ -92,8 +100,52 @@ export const claudeCodeTool: Tool = {
92
100
  return { content: 'Cancelled', isError: true };
93
101
  }
94
102
 
95
- const prompt = input.prompt as string;
96
103
  const workingDir = (input.working_dir as string) || context.workingDir;
104
+
105
+ // Resolve prompt: either from direct prompt input or by loading a CC command template
106
+ let prompt: string;
107
+ if (input.command != null && typeof input.command !== 'string') {
108
+ return {
109
+ content: `Error: "command" must be a string, got ${typeof input.command}`,
110
+ isError: true,
111
+ };
112
+ }
113
+ const commandName = input.command as string | undefined;
114
+ if (commandName) {
115
+ // Command-template execution path: load .claude/commands/<command>.md,
116
+ // apply $ARGUMENTS substitution, and use the result as the prompt.
117
+ const entry = getCCCommand(workingDir, commandName);
118
+ if (!entry) {
119
+ return {
120
+ content: `Error: CC command "${commandName}" not found. Looked for .claude/commands/${commandName}.md in ${workingDir} and parent directories.`,
121
+ isError: true,
122
+ };
123
+ }
124
+
125
+ let template: string;
126
+ try {
127
+ template = loadCCCommandTemplate(entry);
128
+ } catch (err) {
129
+ const message = err instanceof Error ? err.message : String(err);
130
+ return {
131
+ content: `Error: Failed to load CC command template "${commandName}": ${message}`,
132
+ isError: true,
133
+ };
134
+ }
135
+
136
+ // Substitute $ARGUMENTS placeholder with the provided arguments
137
+ const args = (input.arguments as string) ?? '';
138
+ prompt = template.replace(/\$ARGUMENTS/g, args);
139
+
140
+ log.info({ command: commandName, templatePath: entry.filePath, hasArgs: !!args }, 'Loaded CC command template');
141
+ } else if (typeof input.prompt === 'string') {
142
+ prompt = input.prompt;
143
+ } else {
144
+ return {
145
+ content: 'Error: Either "prompt" or "command" must be provided.',
146
+ isError: true,
147
+ };
148
+ }
97
149
  const resumeSessionId = input.resume as string | undefined;
98
150
  const model = (input.model as string) || 'claude-sonnet-4-6';
99
151
  const profileName = (input.profile as WorkerProfile | undefined) ?? 'general';
@@ -630,7 +630,7 @@ class CredentialStoreTool implements Tool {
630
630
  const dmChannel = await conversationsOpen(botToken, installingUserId);
631
631
  const welcomeMsg =
632
632
  `You have installed ${identity.user}, an AI Assistant, on ${identity.team}. ` +
633
- `Manage the assistant experience for this workspace in the workspace settings page.`;
633
+ `You can manage the assistant experience for this workspace by chatting with the assistant or from the Settings page.`;
634
634
  await postMessage(botToken, dmChannel.channel.id, welcomeMsg);
635
635
  }
636
636
  } catch (err) {
@@ -1,7 +1,12 @@
1
1
  import type { ExecutionTarget } from './types.js';
2
2
  import { getTool } from './registry.js';
3
3
 
4
- export function resolveExecutionTarget(toolName: string): ExecutionTarget {
4
+ export interface ManifestOverride {
5
+ risk: 'low' | 'medium' | 'high';
6
+ execution_target: 'host' | 'sandbox';
7
+ }
8
+
9
+ export function resolveExecutionTarget(toolName: string, manifestOverride?: ManifestOverride): ExecutionTarget {
5
10
  const tool = getTool(toolName);
6
11
  // Manifest-declared execution target is authoritative — check it first so
7
12
  // skill tools with host_/computer_use_ prefixes aren't mis-classified.
@@ -13,6 +18,11 @@ export function resolveExecutionTarget(toolName: string): ExecutionTarget {
13
18
  if (tool?.executionMode === 'proxy') {
14
19
  return 'host';
15
20
  }
21
+ // Use manifest metadata for unregistered skill tools so the Permission
22
+ // Simulator shows accurate execution targets instead of defaulting to sandbox.
23
+ if (!tool && manifestOverride) {
24
+ return manifestOverride.execution_target;
25
+ }
16
26
  // Prefix heuristics for core tools that don't declare an explicit target.
17
27
  if (toolName.startsWith('host_') || toolName.startsWith('computer_use_')) {
18
28
  return 'host';
@@ -268,7 +268,15 @@ export class ToolExecutor {
268
268
  });
269
269
 
270
270
  if (response.decision === 'deny') {
271
- const denialMessage = `Permission denied by user. The user chose not to allow the "${name}" tool. Do NOT retry this tool call immediately. Instead, tell the user that the action was not performed because they denied permission, and ask if they would like you to try again or take a different approach. Wait for the user to explicitly respond before retrying.`;
271
+ const contextualDenial = typeof response.decisionContext === 'string'
272
+ ? response.decisionContext.trim()
273
+ : '';
274
+ const denialMessage = contextualDenial.length > 0
275
+ ? contextualDenial
276
+ : `Permission denied by user. The user chose not to allow the "${name}" tool. Do NOT retry this tool call immediately. Instead, tell the user that the action was not performed because they denied permission, and ask if they would like you to try again or take a different approach. Wait for the user to explicitly respond before retrying.`;
277
+ const denialReason = contextualDenial.length > 0
278
+ ? `Permission denied (${name}): contextual policy`
279
+ : 'Permission denied by user';
272
280
  const durationMs = Date.now() - startTime;
273
281
  emitLifecycleEvent(context, {
274
282
  type: 'permission_denied',
@@ -281,7 +289,7 @@ export class ToolExecutor {
281
289
  requestId: context.requestId,
282
290
  riskLevel,
283
291
  decision: 'deny',
284
- reason: 'Permission denied by user',
292
+ reason: denialReason,
285
293
  durationMs,
286
294
  });
287
295
  return { content: denialMessage, isError: true };
@@ -268,7 +268,7 @@ class WebSearchTool implements Tool {
268
268
  apiKey = fallbackKey;
269
269
  } else {
270
270
  return {
271
- content: 'Error: No web search API key configured. Set PERPLEXITY_API_KEY or BRAVE_API_KEY environment variable, or configure a key in settings.',
271
+ content: 'Error: No web search API key configured. Set a PERPLEXITY_API_KEY or BRAVE_API_KEY environment variable, or configure it from the Settings page under API Keys.',
272
272
  isError: true,
273
273
  };
274
274
  }