@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.
- package/Dockerfile +2 -0
- package/README.md +45 -18
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +85 -22
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +24 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- 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 =
|
|
14
|
-
const PAGE_WAIT_MS =
|
|
15
|
-
const SCROLL_WAIT_MS =
|
|
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(
|
|
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
|
-
|
|
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
|
|
156
|
-
await
|
|
157
|
-
await sleep(
|
|
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({
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
/**
|
|
232
|
-
|
|
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
|
-
'
|
|
235
|
-
'[role="
|
|
236
|
-
'[role="tablist"] button',
|
|
310
|
+
'[role="tab"]:not(:first-child)',
|
|
311
|
+
'[role="tablist"] button:not(:first-child)',
|
|
237
312
|
'button[data-tab]',
|
|
238
|
-
'
|
|
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(
|
|
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
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
`
|
|
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
|
|
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';
|
package/src/tools/executor.ts
CHANGED
|
@@ -268,7 +268,15 @@ export class ToolExecutor {
|
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
if (response.decision === 'deny') {
|
|
271
|
-
const
|
|
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:
|
|
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
|
|
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
|
}
|