@agent-native/core 0.68.3 → 0.69.0
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/extensions/web-search-tool.ts +45 -4
- package/corpus/core/src/secrets/register-framework-secrets.ts +7 -0
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +69 -13
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +38 -2
- package/corpus/templates/clips/app/lib/recorder-preferences.ts +67 -0
- package/corpus/templates/clips/app/routes/_app.trash.tsx +21 -0
- package/corpus/templates/clips/app/routes/record.tsx +14 -3
- package/corpus/templates/content/vitest.config.ts +3 -0
- package/dist/extensions/web-search-tool.d.ts +5 -3
- package/dist/extensions/web-search-tool.d.ts.map +1 -1
- package/dist/extensions/web-search-tool.js +36 -4
- package/dist/extensions/web-search-tool.js.map +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +6 -0
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.69.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 530de18: Add Firecrawl as a BYOK backend for the web-search agent tool. When `FIRECRAWL_API_KEY` is configured (via app secrets or environment) the tool routes searches through Firecrawl's `/v2/search` API. It slots into the existing first-configured-wins chain after Brave, Tavily, and Exa, and before Builder-managed search, and is registered as an optional framework secret so it surfaces in every template's settings UI.
|
|
8
|
+
|
|
3
9
|
## 0.68.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* 1. Brave Search API (BRAVE_SEARCH_API_KEY)
|
|
8
8
|
* 2. Tavily (TAVILY_API_KEY)
|
|
9
9
|
* 3. Exa (EXA_API_KEY)
|
|
10
|
-
* 4.
|
|
10
|
+
* 4. Firecrawl (FIRECRAWL_API_KEY)
|
|
11
|
+
* 5. Builder.io (connected Builder credentials)
|
|
11
12
|
*
|
|
12
13
|
* The first configured backend wins. If none is configured, the tool
|
|
13
14
|
* returns a helpful message telling the user which keys to add.
|
|
14
15
|
*
|
|
15
|
-
* Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,
|
|
16
|
-
* EXA_API_KEY via app secrets settings or environment
|
|
16
|
+
* Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,
|
|
17
|
+
* EXA_API_KEY, or FIRECRAWL_API_KEY via app secrets settings or environment
|
|
18
|
+
* variables.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import type { ActionEntry } from "../agent/production-agent.js";
|
|
@@ -222,6 +224,40 @@ async function searchExa(
|
|
|
222
224
|
}));
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
async function searchFirecrawl(
|
|
228
|
+
query: string,
|
|
229
|
+
count: number,
|
|
230
|
+
apiKey: string,
|
|
231
|
+
): Promise<WebSearchResult[]> {
|
|
232
|
+
const res = await fetch("https://api.firecrawl.dev/v2/search", {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: {
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
Authorization: `Bearer ${apiKey}`,
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
query,
|
|
240
|
+
limit: count,
|
|
241
|
+
}),
|
|
242
|
+
signal: AbortSignal.timeout(15_000),
|
|
243
|
+
});
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
throw new Error(`Firecrawl error ${res.status}: ${await res.text()}`);
|
|
246
|
+
}
|
|
247
|
+
// v2 groups results by source ({ data: { web: [...] } }); older shapes
|
|
248
|
+
// return a flat array ({ data: [...] }). Accept both defensively.
|
|
249
|
+
type FirecrawlResult = { title?: string; url?: string; description?: string };
|
|
250
|
+
const data = (await res.json()) as {
|
|
251
|
+
data?: FirecrawlResult[] | { web?: FirecrawlResult[] };
|
|
252
|
+
};
|
|
253
|
+
const results = Array.isArray(data.data) ? data.data : (data.data?.web ?? []);
|
|
254
|
+
return results.map((r) => ({
|
|
255
|
+
title: r.title ?? "",
|
|
256
|
+
url: r.url ?? "",
|
|
257
|
+
snippet: r.description ?? "",
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
225
261
|
async function searchBuilderManaged(
|
|
226
262
|
query: string,
|
|
227
263
|
count: number,
|
|
@@ -287,7 +323,7 @@ export function createWebSearchToolEntry(
|
|
|
287
323
|
"web-search": {
|
|
288
324
|
tool: {
|
|
289
325
|
description:
|
|
290
|
-
"Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, or
|
|
326
|
+
"Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, EXA_API_KEY, or FIRECRAWL_API_KEY.",
|
|
291
327
|
parameters: {
|
|
292
328
|
type: "object" as const,
|
|
293
329
|
properties: {
|
|
@@ -318,6 +354,7 @@ export function createWebSearchToolEntry(
|
|
|
318
354
|
const braveKey = await resolveSearchKey("BRAVE_SEARCH_API_KEY", opts);
|
|
319
355
|
const tavilyKey = await resolveSearchKey("TAVILY_API_KEY", opts);
|
|
320
356
|
const exaKey = await resolveSearchKey("EXA_API_KEY", opts);
|
|
357
|
+
const firecrawlKey = await resolveSearchKey("FIRECRAWL_API_KEY", opts);
|
|
321
358
|
const builderCredentials = await resolveBuilderSearchCredentials(opts);
|
|
322
359
|
|
|
323
360
|
let results: WebSearchResult[];
|
|
@@ -334,6 +371,9 @@ export function createWebSearchToolEntry(
|
|
|
334
371
|
} else if (exaKey) {
|
|
335
372
|
results = await searchExa(query, count, exaKey);
|
|
336
373
|
backend = "Exa";
|
|
374
|
+
} else if (firecrawlKey) {
|
|
375
|
+
results = await searchFirecrawl(query, count, firecrawlKey);
|
|
376
|
+
backend = "Firecrawl";
|
|
337
377
|
} else if (builderCredentials) {
|
|
338
378
|
managedText = await searchBuilderManaged(
|
|
339
379
|
query,
|
|
@@ -350,6 +390,7 @@ export function createWebSearchToolEntry(
|
|
|
350
390
|
" • BRAVE_SEARCH_API_KEY — https://brave.com/search/api/",
|
|
351
391
|
" • TAVILY_API_KEY — https://tavily.com/",
|
|
352
392
|
" • EXA_API_KEY — https://exa.ai/",
|
|
393
|
+
" • FIRECRAWL_API_KEY — https://firecrawl.dev/",
|
|
353
394
|
].join("\n");
|
|
354
395
|
}
|
|
355
396
|
} catch (err: unknown) {
|
|
@@ -47,6 +47,13 @@ export function registerFrameworkSecrets(): void {
|
|
|
47
47
|
"Enables the web-search agent tool via Exa. Used as fallback when Brave and Tavily are not set and before Builder-managed search.",
|
|
48
48
|
docsUrl: "https://exa.ai/",
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
key: "FIRECRAWL_API_KEY",
|
|
52
|
+
label: "Firecrawl API Key",
|
|
53
|
+
description:
|
|
54
|
+
"Enables the web-search agent tool via Firecrawl. Used as fallback when Brave, Tavily, and Exa are not set and before Builder-managed search.",
|
|
55
|
+
docsUrl: "https://firecrawl.dev/",
|
|
56
|
+
},
|
|
50
57
|
];
|
|
51
58
|
|
|
52
59
|
for (const entry of webSearchKeys) {
|
|
@@ -48,6 +48,10 @@ import {
|
|
|
48
48
|
type RecordingMode,
|
|
49
49
|
} from "./recorder-engine";
|
|
50
50
|
import type { CameraBubbleSize } from "./camera-bubble";
|
|
51
|
+
import {
|
|
52
|
+
loadRecorderPreferences,
|
|
53
|
+
saveRecorderPreferences,
|
|
54
|
+
} from "@/lib/recorder-preferences";
|
|
51
55
|
import { CameraVisualizer, type CameraTestStatus } from "./camera-visualizer";
|
|
52
56
|
import {
|
|
53
57
|
MicrophoneVisualizer,
|
|
@@ -184,11 +188,14 @@ export function PreRecordPanel({
|
|
|
184
188
|
}: PreRecordPanelProps) {
|
|
185
189
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
186
190
|
const loomInputRef = useRef<HTMLInputElement>(null);
|
|
191
|
+
// Saved selections from the last visit. A `?mode=`/`?surface=` deep link
|
|
192
|
+
// (initialMode/initialDisplaySurface) still takes precedence over them.
|
|
193
|
+
const savedPrefs = useMemo(() => loadRecorderPreferences(), []);
|
|
187
194
|
const [mode, setMode] = useState<RecordingMode>(
|
|
188
|
-
() => initialMode ?? "screen+camera",
|
|
195
|
+
() => initialMode ?? savedPrefs.mode ?? "screen+camera",
|
|
189
196
|
);
|
|
190
197
|
const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(
|
|
191
|
-
() => initialDisplaySurface ?? "window",
|
|
198
|
+
() => initialDisplaySurface ?? savedPrefs.displaySurface ?? "window",
|
|
192
199
|
);
|
|
193
200
|
const [sourceOpen, setSourceOpen] = useState(false);
|
|
194
201
|
const [deviceSettingsOpen, setDeviceSettingsOpen] = useState(false);
|
|
@@ -197,8 +204,12 @@ export function PreRecordPanel({
|
|
|
197
204
|
const [loomError, setLoomError] = useState<string | null>(null);
|
|
198
205
|
const [mics, setMics] = useState<MediaDeviceInfo[]>([]);
|
|
199
206
|
const [cameras, setCameras] = useState<MediaDeviceInfo[]>([]);
|
|
200
|
-
const [micId, setMicId] = useState<string>(
|
|
201
|
-
|
|
207
|
+
const [micId, setMicId] = useState<string>(
|
|
208
|
+
() => savedPrefs.micId ?? "default",
|
|
209
|
+
);
|
|
210
|
+
const [cameraId, setCameraId] = useState<string>(
|
|
211
|
+
() => savedPrefs.cameraId ?? "default",
|
|
212
|
+
);
|
|
202
213
|
const [enumError, setEnumError] = useState<string | null>(null);
|
|
203
214
|
const [micAccessStatus, setMicAccessStatus] =
|
|
204
215
|
useState<DeviceAccessStatus>("idle");
|
|
@@ -274,13 +285,56 @@ export function PreRecordPanel({
|
|
|
274
285
|
[mics],
|
|
275
286
|
);
|
|
276
287
|
|
|
288
|
+
// Reset to "default" only once a populated list genuinely excludes the saved
|
|
289
|
+
// device — an empty list means "not enumerated yet", and resetting then would
|
|
290
|
+
// wipe a restored preference before devices load.
|
|
277
291
|
useEffect(() => {
|
|
278
292
|
if (micId === "default" || micId === NO_MIC_DEVICE_ID) return;
|
|
279
|
-
if (!mics.some((mic) => mic.deviceId === micId)) {
|
|
293
|
+
if (mics.length > 0 && !mics.some((mic) => mic.deviceId === micId)) {
|
|
280
294
|
setMicId("default");
|
|
281
295
|
}
|
|
282
296
|
}, [micId, mics]);
|
|
283
297
|
|
|
298
|
+
// Same guard for cameras. Not persisted, so a temporarily missing device
|
|
299
|
+
// doesn't erase the saved choice.
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (cameraId === "default" || cameraId === NO_CAMERA_DEVICE_ID) return;
|
|
302
|
+
if (
|
|
303
|
+
cameras.length > 0 &&
|
|
304
|
+
!cameras.some((camera) => camera.deviceId === cameraId)
|
|
305
|
+
) {
|
|
306
|
+
setCameraId("default");
|
|
307
|
+
}
|
|
308
|
+
}, [cameraId, cameras]);
|
|
309
|
+
|
|
310
|
+
// Camera-only mode needs a camera — coerce a restored "off" sentinel to
|
|
311
|
+
// "default" so Start doesn't forward it as an exact deviceId (covers the
|
|
312
|
+
// ?mode=camera deep-link/restore path the mode buttons already handle).
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (mode === "camera" && cameraId === NO_CAMERA_DEVICE_ID) {
|
|
315
|
+
setCameraId("default");
|
|
316
|
+
}
|
|
317
|
+
}, [mode, cameraId]);
|
|
318
|
+
|
|
319
|
+
// Persist deliberate picks only (not the resets above), so an unavailable
|
|
320
|
+
// device on load can't clobber the stored preference.
|
|
321
|
+
const chooseMode = useCallback((value: RecordingMode) => {
|
|
322
|
+
setMode(value);
|
|
323
|
+
saveRecorderPreferences({ mode: value });
|
|
324
|
+
}, []);
|
|
325
|
+
const chooseDisplaySurface = useCallback((value: DisplaySurface) => {
|
|
326
|
+
setDisplaySurface(value);
|
|
327
|
+
saveRecorderPreferences({ displaySurface: value });
|
|
328
|
+
}, []);
|
|
329
|
+
const chooseMic = useCallback((value: string) => {
|
|
330
|
+
setMicId(value);
|
|
331
|
+
saveRecorderPreferences({ micId: value });
|
|
332
|
+
}, []);
|
|
333
|
+
const chooseCamera = useCallback((value: string) => {
|
|
334
|
+
setCameraId(value);
|
|
335
|
+
saveRecorderPreferences({ cameraId: value });
|
|
336
|
+
}, []);
|
|
337
|
+
|
|
284
338
|
const requestMicrophoneChoices = useCallback(async () => {
|
|
285
339
|
if (!navigator.mediaDevices?.getUserMedia) {
|
|
286
340
|
setMicAccessStatus("error");
|
|
@@ -376,9 +430,9 @@ export function PreRecordPanel({
|
|
|
376
430
|
void requestMicrophoneChoices();
|
|
377
431
|
return;
|
|
378
432
|
}
|
|
379
|
-
|
|
433
|
+
chooseMic(value);
|
|
380
434
|
},
|
|
381
|
-
[requestMicrophoneChoices],
|
|
435
|
+
[requestMicrophoneChoices, chooseMic],
|
|
382
436
|
);
|
|
383
437
|
|
|
384
438
|
const handleMicSignalChange = useCallback((hasSignal: boolean) => {
|
|
@@ -530,12 +584,12 @@ export function PreRecordPanel({
|
|
|
530
584
|
key={opt.value}
|
|
531
585
|
type="button"
|
|
532
586
|
onClick={() => {
|
|
533
|
-
|
|
587
|
+
chooseMode(opt.value);
|
|
534
588
|
if (
|
|
535
589
|
opt.value === "camera" &&
|
|
536
590
|
cameraId === NO_CAMERA_DEVICE_ID
|
|
537
591
|
) {
|
|
538
|
-
|
|
592
|
+
chooseCamera("default");
|
|
539
593
|
}
|
|
540
594
|
}}
|
|
541
595
|
className={cn(
|
|
@@ -605,7 +659,7 @@ export function PreRecordPanel({
|
|
|
605
659
|
<button
|
|
606
660
|
key={opt.value}
|
|
607
661
|
type="button"
|
|
608
|
-
onClick={() =>
|
|
662
|
+
onClick={() => chooseDisplaySurface(opt.value)}
|
|
609
663
|
className={cn(
|
|
610
664
|
"flex min-h-[76px] flex-col rounded-lg border p-2 text-left transition-colors",
|
|
611
665
|
active
|
|
@@ -703,7 +757,7 @@ export function PreRecordPanel({
|
|
|
703
757
|
<Switch
|
|
704
758
|
checked={audioEnabled}
|
|
705
759
|
onCheckedChange={(checked) =>
|
|
706
|
-
|
|
760
|
+
chooseMic(checked ? "default" : NO_MIC_DEVICE_ID)
|
|
707
761
|
}
|
|
708
762
|
disabled={busy}
|
|
709
763
|
aria-label="Include audio in this recording"
|
|
@@ -740,7 +794,7 @@ export function PreRecordPanel({
|
|
|
740
794
|
</div>
|
|
741
795
|
<Select
|
|
742
796
|
value={cameraId}
|
|
743
|
-
onValueChange={
|
|
797
|
+
onValueChange={chooseCamera}
|
|
744
798
|
disabled={!needsCamera}
|
|
745
799
|
>
|
|
746
800
|
<SelectTrigger className="h-9 min-w-0 flex-1 border-0 bg-transparent px-2 shadow-none hover:bg-muted/45 focus:ring-0 focus:ring-offset-0 disabled:opacity-60">
|
|
@@ -762,7 +816,9 @@ export function PreRecordPanel({
|
|
|
762
816
|
<Switch
|
|
763
817
|
checked={needsCamera}
|
|
764
818
|
onCheckedChange={(checked) =>
|
|
765
|
-
|
|
819
|
+
chooseCamera(
|
|
820
|
+
checked ? "default" : NO_CAMERA_DEVICE_ID,
|
|
821
|
+
)
|
|
766
822
|
}
|
|
767
823
|
disabled={busy}
|
|
768
824
|
aria-label="Include camera in this recording"
|
|
@@ -186,6 +186,17 @@ function errorName(err: unknown): string {
|
|
|
186
186
|
return (err as { name?: string } | null)?.name ?? "";
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// getUserMedia failed because the requested device is gone (unplugged / stale
|
|
190
|
+
// saved id), not a permission error — recoverable by retrying with the default.
|
|
191
|
+
function isDeviceUnavailableError(err: unknown): boolean {
|
|
192
|
+
const name = errorName(err);
|
|
193
|
+
return (
|
|
194
|
+
name === "OverconstrainedError" ||
|
|
195
|
+
name === "NotFoundError" ||
|
|
196
|
+
name === "DevicesNotFoundError"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
function errorMessage(err: unknown): string {
|
|
190
201
|
return err instanceof Error ? err.message : String(err || "Unknown error");
|
|
191
202
|
}
|
|
@@ -592,7 +603,20 @@ export class RecorderEngine {
|
|
|
592
603
|
audio: false,
|
|
593
604
|
});
|
|
594
605
|
} catch (err) {
|
|
595
|
-
|
|
606
|
+
// Stale/unplugged cameraDeviceId — retry with the default camera
|
|
607
|
+
// instead of failing the recording.
|
|
608
|
+
if (this.opts.cameraDeviceId && isDeviceUnavailableError(err)) {
|
|
609
|
+
try {
|
|
610
|
+
this.cameraStream = await navigator.mediaDevices.getUserMedia({
|
|
611
|
+
video: true,
|
|
612
|
+
audio: false,
|
|
613
|
+
});
|
|
614
|
+
} catch (retryErr) {
|
|
615
|
+
throw this.friendlyError(retryErr, "camera");
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
throw this.friendlyError(err, "camera");
|
|
619
|
+
}
|
|
596
620
|
}
|
|
597
621
|
}
|
|
598
622
|
|
|
@@ -603,7 +627,19 @@ export class RecorderEngine {
|
|
|
603
627
|
video: false,
|
|
604
628
|
});
|
|
605
629
|
} catch (err) {
|
|
606
|
-
|
|
630
|
+
// Same stale-device fallback as the camera — retry default mic.
|
|
631
|
+
if (this.opts.micDeviceId && isDeviceUnavailableError(err)) {
|
|
632
|
+
try {
|
|
633
|
+
this.micStream = await navigator.mediaDevices.getUserMedia({
|
|
634
|
+
audio: voiceFocusedAudioConstraints(),
|
|
635
|
+
video: false,
|
|
636
|
+
});
|
|
637
|
+
} catch (retryErr) {
|
|
638
|
+
throw this.friendlyError(retryErr, "microphone");
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
throw this.friendlyError(err, "microphone");
|
|
642
|
+
}
|
|
607
643
|
}
|
|
608
644
|
}
|
|
609
645
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CameraBubbleSize } from "@/components/recorder/camera-bubble";
|
|
2
|
+
import type {
|
|
3
|
+
DisplaySurface,
|
|
4
|
+
RecordingMode,
|
|
5
|
+
} from "@/components/recorder/recorder-engine";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Last-used recorder selections, remembered across `/record` visits via
|
|
9
|
+
* localStorage so the panel doesn't reset to defaults every time. Device ids
|
|
10
|
+
* are best-effort: a remembered mic/camera that no longer exists (machine
|
|
11
|
+
* changed, permission revoked) falls back to "default" in the panel, so we
|
|
12
|
+
* never feed a stale `deviceId: { exact }` to getUserMedia.
|
|
13
|
+
*
|
|
14
|
+
* `?mode=` / `?surface=` URL params still win over a saved preference — those
|
|
15
|
+
* are explicit deep-link intents (e.g. the agent navigating to a specific
|
|
16
|
+
* capture mode).
|
|
17
|
+
*/
|
|
18
|
+
export interface RecorderPreferences {
|
|
19
|
+
mode: RecordingMode;
|
|
20
|
+
displaySurface: DisplaySurface;
|
|
21
|
+
micId: string;
|
|
22
|
+
cameraId: string;
|
|
23
|
+
cameraSize: CameraBubbleSize;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STORAGE_KEY = "clips:recorder-preferences";
|
|
27
|
+
|
|
28
|
+
const VALID_MODES: RecordingMode[] = ["screen", "camera", "screen+camera"];
|
|
29
|
+
const VALID_SURFACES: DisplaySurface[] = ["monitor", "window", "browser"];
|
|
30
|
+
const VALID_SIZES: CameraBubbleSize[] = ["sm", "md", "lg"];
|
|
31
|
+
|
|
32
|
+
export function loadRecorderPreferences(): Partial<RecorderPreferences> {
|
|
33
|
+
if (typeof window === "undefined") return {};
|
|
34
|
+
try {
|
|
35
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
36
|
+
if (!raw) return {};
|
|
37
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
38
|
+
const prefs: Partial<RecorderPreferences> = {};
|
|
39
|
+
if (VALID_MODES.includes(parsed.mode as RecordingMode)) {
|
|
40
|
+
prefs.mode = parsed.mode as RecordingMode;
|
|
41
|
+
}
|
|
42
|
+
if (VALID_SURFACES.includes(parsed.displaySurface as DisplaySurface)) {
|
|
43
|
+
prefs.displaySurface = parsed.displaySurface as DisplaySurface;
|
|
44
|
+
}
|
|
45
|
+
if (typeof parsed.micId === "string") prefs.micId = parsed.micId;
|
|
46
|
+
if (typeof parsed.cameraId === "string") prefs.cameraId = parsed.cameraId;
|
|
47
|
+
if (VALID_SIZES.includes(parsed.cameraSize as CameraBubbleSize)) {
|
|
48
|
+
prefs.cameraSize = parsed.cameraSize as CameraBubbleSize;
|
|
49
|
+
}
|
|
50
|
+
return prefs;
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function saveRecorderPreferences(
|
|
57
|
+
patch: Partial<RecorderPreferences>,
|
|
58
|
+
): void {
|
|
59
|
+
if (typeof window === "undefined") return;
|
|
60
|
+
try {
|
|
61
|
+
const next = { ...loadRecorderPreferences(), ...patch };
|
|
62
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
63
|
+
} catch {
|
|
64
|
+
// localStorage may be unavailable (private mode, quota) — preferences are
|
|
65
|
+
// a nicety, never block recording on a write failure.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -88,6 +88,16 @@ export default function TrashRoute() {
|
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
const selectedIds = Array.from(selected);
|
|
91
|
+
const allSelected =
|
|
92
|
+
recordings.length > 0 && selected.size === recordings.length;
|
|
93
|
+
|
|
94
|
+
const toggleSelectAll = () => {
|
|
95
|
+
setSelected((prev) =>
|
|
96
|
+
prev.size === recordings.length
|
|
97
|
+
? new Set()
|
|
98
|
+
: new Set(recordings.map((r) => r.id)),
|
|
99
|
+
);
|
|
100
|
+
};
|
|
91
101
|
|
|
92
102
|
return (
|
|
93
103
|
<div className="flex flex-1 flex-col min-h-0">
|
|
@@ -96,6 +106,17 @@ export default function TrashRoute() {
|
|
|
96
106
|
<div className="ml-auto flex items-center gap-2">
|
|
97
107
|
{selectedIds.length > 0 && (
|
|
98
108
|
<>
|
|
109
|
+
<span className="text-sm text-muted-foreground">
|
|
110
|
+
{selectedIds.length} selected
|
|
111
|
+
</span>
|
|
112
|
+
<Button
|
|
113
|
+
size="sm"
|
|
114
|
+
variant="ghost"
|
|
115
|
+
className="gap-1.5"
|
|
116
|
+
onClick={toggleSelectAll}
|
|
117
|
+
>
|
|
118
|
+
{allSelected ? "Deselect all" : "Select all"}
|
|
119
|
+
</Button>
|
|
99
120
|
<Button
|
|
100
121
|
size="sm"
|
|
101
122
|
variant="outline"
|
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
compressBlobIfTooLarge,
|
|
54
54
|
formatMb,
|
|
55
55
|
} from "@/lib/compress";
|
|
56
|
+
import {
|
|
57
|
+
loadRecorderPreferences,
|
|
58
|
+
saveRecorderPreferences,
|
|
59
|
+
} from "@/lib/recorder-preferences";
|
|
56
60
|
import { cn } from "@/lib/utils";
|
|
57
61
|
|
|
58
62
|
// Client-side app-state writer (the server module pulls in Node's `events`
|
|
@@ -718,7 +722,14 @@ export default function RecordRoute() {
|
|
|
718
722
|
const [isPaused, setIsPaused] = useState(false);
|
|
719
723
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
720
724
|
const [cameraStream, setCameraStream] = useState<MediaStream | null>(null);
|
|
721
|
-
const [cameraSize, setCameraSize] = useState<CameraBubbleSize>(
|
|
725
|
+
const [cameraSize, setCameraSize] = useState<CameraBubbleSize>(
|
|
726
|
+
() => loadRecorderPreferences().cameraSize ?? "md",
|
|
727
|
+
);
|
|
728
|
+
// Remember the bubble size across visits alongside the panel's selections.
|
|
729
|
+
const handleCameraSizeChange = useCallback((size: CameraBubbleSize) => {
|
|
730
|
+
setCameraSize(size);
|
|
731
|
+
saveRecorderPreferences({ cameraSize: size });
|
|
732
|
+
}, []);
|
|
722
733
|
const [previewStream, setPreviewStream] = useState<MediaStream | null>(null);
|
|
723
734
|
// The capture surface the user actually picked in the browser's native screen
|
|
724
735
|
// picker (authority over the requested `displaySurface` hint). Drives whether
|
|
@@ -2058,7 +2069,7 @@ export default function RecordRoute() {
|
|
|
2058
2069
|
onImportLoom={importLoom}
|
|
2059
2070
|
importingLoom={loomImporting}
|
|
2060
2071
|
cameraSize={cameraSize}
|
|
2061
|
-
onCameraSizeChange={
|
|
2072
|
+
onCameraSizeChange={handleCameraSizeChange}
|
|
2062
2073
|
/>
|
|
2063
2074
|
) : (
|
|
2064
2075
|
<StorageSetupCard
|
|
@@ -2137,7 +2148,7 @@ export default function RecordRoute() {
|
|
|
2137
2148
|
<CameraBubble
|
|
2138
2149
|
stream={cameraStream}
|
|
2139
2150
|
size={cameraSize}
|
|
2140
|
-
onSizeChange={
|
|
2151
|
+
onSizeChange={handleCameraSizeChange}
|
|
2141
2152
|
hidden={
|
|
2142
2153
|
(uiState !== "recording" && uiState !== "countdown") ||
|
|
2143
2154
|
hideBubbleForFullScreenCapture
|
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* 1. Brave Search API (BRAVE_SEARCH_API_KEY)
|
|
8
8
|
* 2. Tavily (TAVILY_API_KEY)
|
|
9
9
|
* 3. Exa (EXA_API_KEY)
|
|
10
|
-
* 4.
|
|
10
|
+
* 4. Firecrawl (FIRECRAWL_API_KEY)
|
|
11
|
+
* 5. Builder.io (connected Builder credentials)
|
|
11
12
|
*
|
|
12
13
|
* The first configured backend wins. If none is configured, the tool
|
|
13
14
|
* returns a helpful message telling the user which keys to add.
|
|
14
15
|
*
|
|
15
|
-
* Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,
|
|
16
|
-
* EXA_API_KEY via app secrets settings or environment
|
|
16
|
+
* Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,
|
|
17
|
+
* EXA_API_KEY, or FIRECRAWL_API_KEY via app secrets settings or environment
|
|
18
|
+
* variables.
|
|
17
19
|
*/
|
|
18
20
|
import type { ActionEntry } from "../agent/production-agent.js";
|
|
19
21
|
import type { CredentialContext } from "../credentials/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-search-tool.d.ts","sourceRoot":"","sources":["../../src/extensions/web-search-tool.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"web-search-tool.d.ts","sourceRoot":"","sources":["../../src/extensions/web-search-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD;;OAEG;IACH,iBAAiB,CAAC,EAAE,CAClB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,iBAAiB,KACnB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACjC;;OAEG;IACH,oBAAoB,CAAC,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACtD;;OAEG;IACH,yBAAyB,CAAC,EAAE,MAAM,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACvE;;OAEG;IACH,0BAA0B,CAAC,EAAE,MAAM,MAAM,CAAC;IAC1C;;OAEG;IACH,wBAAwB,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzD;AAKD,UAAU,2BAA2B;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAwPD;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,GAAE,oBAAyB,GAC9B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAoH7B"}
|
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* 1. Brave Search API (BRAVE_SEARCH_API_KEY)
|
|
8
8
|
* 2. Tavily (TAVILY_API_KEY)
|
|
9
9
|
* 3. Exa (EXA_API_KEY)
|
|
10
|
-
* 4.
|
|
10
|
+
* 4. Firecrawl (FIRECRAWL_API_KEY)
|
|
11
|
+
* 5. Builder.io (connected Builder credentials)
|
|
11
12
|
*
|
|
12
13
|
* The first configured backend wins. If none is configured, the tool
|
|
13
14
|
* returns a helpful message telling the user which keys to add.
|
|
14
15
|
*
|
|
15
|
-
* Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,
|
|
16
|
-
* EXA_API_KEY via app secrets settings or environment
|
|
16
|
+
* Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,
|
|
17
|
+
* EXA_API_KEY, or FIRECRAWL_API_KEY via app secrets settings or environment
|
|
18
|
+
* variables.
|
|
17
19
|
*/
|
|
18
20
|
const DEFAULT_COUNT = 5;
|
|
19
21
|
const MAX_COUNT = 10;
|
|
@@ -136,6 +138,30 @@ async function searchExa(query, count, apiKey) {
|
|
|
136
138
|
snippet: r.text ?? "",
|
|
137
139
|
}));
|
|
138
140
|
}
|
|
141
|
+
async function searchFirecrawl(query, count, apiKey) {
|
|
142
|
+
const res = await fetch("https://api.firecrawl.dev/v2/search", {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
Authorization: `Bearer ${apiKey}`,
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
query,
|
|
150
|
+
limit: count,
|
|
151
|
+
}),
|
|
152
|
+
signal: AbortSignal.timeout(15_000),
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
throw new Error(`Firecrawl error ${res.status}: ${await res.text()}`);
|
|
156
|
+
}
|
|
157
|
+
const data = (await res.json());
|
|
158
|
+
const results = Array.isArray(data.data) ? data.data : (data.data?.web ?? []);
|
|
159
|
+
return results.map((r) => ({
|
|
160
|
+
title: r.title ?? "",
|
|
161
|
+
url: r.url ?? "",
|
|
162
|
+
snippet: r.description ?? "",
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
139
165
|
async function searchBuilderManaged(query, count, credentials, opts) {
|
|
140
166
|
const baseUrl = opts.getBuilderWebSearchBaseUrl?.() ??
|
|
141
167
|
process.env.BUILDER_WEB_SEARCH_BASE_URL ??
|
|
@@ -183,7 +209,7 @@ export function createWebSearchToolEntry(opts = {}) {
|
|
|
183
209
|
return {
|
|
184
210
|
"web-search": {
|
|
185
211
|
tool: {
|
|
186
|
-
description: "Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, or
|
|
212
|
+
description: "Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, EXA_API_KEY, or FIRECRAWL_API_KEY.",
|
|
187
213
|
parameters: {
|
|
188
214
|
type: "object",
|
|
189
215
|
properties: {
|
|
@@ -211,6 +237,7 @@ export function createWebSearchToolEntry(opts = {}) {
|
|
|
211
237
|
const braveKey = await resolveSearchKey("BRAVE_SEARCH_API_KEY", opts);
|
|
212
238
|
const tavilyKey = await resolveSearchKey("TAVILY_API_KEY", opts);
|
|
213
239
|
const exaKey = await resolveSearchKey("EXA_API_KEY", opts);
|
|
240
|
+
const firecrawlKey = await resolveSearchKey("FIRECRAWL_API_KEY", opts);
|
|
214
241
|
const builderCredentials = await resolveBuilderSearchCredentials(opts);
|
|
215
242
|
let results;
|
|
216
243
|
let backend;
|
|
@@ -228,6 +255,10 @@ export function createWebSearchToolEntry(opts = {}) {
|
|
|
228
255
|
results = await searchExa(query, count, exaKey);
|
|
229
256
|
backend = "Exa";
|
|
230
257
|
}
|
|
258
|
+
else if (firecrawlKey) {
|
|
259
|
+
results = await searchFirecrawl(query, count, firecrawlKey);
|
|
260
|
+
backend = "Firecrawl";
|
|
261
|
+
}
|
|
231
262
|
else if (builderCredentials) {
|
|
232
263
|
managedText = await searchBuilderManaged(query, count, builderCredentials, opts);
|
|
233
264
|
results = [];
|
|
@@ -240,6 +271,7 @@ export function createWebSearchToolEntry(opts = {}) {
|
|
|
240
271
|
" • BRAVE_SEARCH_API_KEY — https://brave.com/search/api/",
|
|
241
272
|
" • TAVILY_API_KEY — https://tavily.com/",
|
|
242
273
|
" • EXA_API_KEY — https://exa.ai/",
|
|
274
|
+
" • FIRECRAWL_API_KEY — https://firecrawl.dev/",
|
|
243
275
|
].join("\n");
|
|
244
276
|
}
|
|
245
277
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-search-tool.js","sourceRoot":"","sources":["../../src/extensions/web-search-tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Web-search tool — agent tool for searching the public web.\n *\n * Pluggable backends resolved at call time based on which API key is\n * configured (env var or secrets/credentials store):\n *\n * 1. Brave Search API (BRAVE_SEARCH_API_KEY)\n * 2. Tavily (TAVILY_API_KEY)\n * 3. Exa (EXA_API_KEY)\n * 4. Builder.io (connected Builder credentials)\n *\n * The first configured backend wins. If none is configured, the tool\n * returns a helpful message telling the user which keys to add.\n *\n * Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, or\n * EXA_API_KEY via app secrets settings or environment variables.\n */\n\nimport type { ActionEntry } from \"../agent/production-agent.js\";\nimport type { CredentialContext } from \"../credentials/index.js\";\n\nexport interface WebSearchResult {\n title: string;\n url: string;\n snippet: string;\n}\n\nexport interface WebSearchToolOptions {\n /**\n * Resolve a request-scoped secret by key. When not provided the tool falls\n * back to env-var lookup only.\n */\n resolveSecret?: (key: string) => Promise<string | null>;\n /**\n * Legacy credential resolver retained for older callers.\n */\n resolveCredential?: (\n key: string,\n ctx: CredentialContext,\n ) => Promise<string | undefined>;\n /**\n * Legacy credential context callback retained for older callers.\n */\n getCredentialContext?: () => CredentialContext | null;\n /**\n * Resolve connected Builder credentials for managed web search.\n */\n resolveBuilderCredentials?: () => Promise<BuilderWebSearchCredentials>;\n /**\n * Base URL for Builder-managed web search.\n */\n getBuilderWebSearchBaseUrl?: () => string;\n /**\n * Stable attribution headers for Builder-managed API calls.\n */\n getBuilderRequestHeaders?: () => Record<string, string>;\n}\n\nconst DEFAULT_COUNT = 5;\nconst MAX_COUNT = 10;\n\ninterface BuilderWebSearchCredentials {\n privateKey: string | null;\n publicKey: string | null;\n userId?: string | null;\n}\n\nasync function resolveSearchKey(\n key: string,\n opts: WebSearchToolOptions,\n): Promise<string | undefined> {\n let usedRequestScopedResolver = false;\n // 1. Try request-scoped app secrets (user/org/workspace stored key).\n if (opts.resolveSecret) {\n usedRequestScopedResolver = true;\n try {\n const value = await opts.resolveSecret(key);\n if (value) return value;\n } catch {\n // Secret lookup failures are non-fatal; fall through to legacy/env.\n }\n }\n\n // 2. Try legacy per-request credential context.\n if (opts.resolveCredential && opts.getCredentialContext) {\n const ctx = opts.getCredentialContext();\n if (ctx) {\n try {\n const value = await opts.resolveCredential(key, ctx);\n if (value) return value;\n } catch {\n // Credential lookup failures are non-fatal; fall through to env.\n }\n }\n }\n\n if (usedRequestScopedResolver) return undefined;\n\n // 3. Fall back to env var.\n return process.env[key] || undefined;\n}\n\nasync function resolveBuilderSearchCredentials(\n opts: WebSearchToolOptions,\n): Promise<BuilderWebSearchCredentials | null> {\n if (!opts.resolveBuilderCredentials) return null;\n try {\n const creds = await opts.resolveBuilderCredentials();\n if (creds.privateKey && creds.publicKey) return creds;\n } catch {\n // Builder credential lookup failures are non-fatal; BYOK backends or the\n // setup hint below can still handle the tool call.\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Backend implementations\n// ---------------------------------------------------------------------------\n\nasync function searchBrave(\n query: string,\n count: number,\n apiKey: string,\n): Promise<WebSearchResult[]> {\n const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;\n const res = await fetch(url, {\n headers: {\n Accept: \"application/json\",\n \"Accept-Encoding\": \"gzip\",\n \"X-Subscription-Token\": apiKey,\n },\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) {\n throw new Error(`Brave Search error ${res.status}: ${await res.text()}`);\n }\n const data = (await res.json()) as {\n web?: {\n results?: Array<{\n title?: string;\n url?: string;\n description?: string;\n }>;\n };\n };\n return (data.web?.results ?? []).map((r) => ({\n title: r.title ?? \"\",\n url: r.url ?? \"\",\n snippet: r.description ?? \"\",\n }));\n}\n\nasync function searchTavily(\n query: string,\n count: number,\n apiKey: string,\n): Promise<WebSearchResult[]> {\n const res = await fetch(\"https://api.tavily.com/search\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n api_key: apiKey,\n query,\n max_results: count,\n search_depth: \"basic\",\n }),\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) {\n throw new Error(`Tavily error ${res.status}: ${await res.text()}`);\n }\n const data = (await res.json()) as {\n results?: Array<{\n title?: string;\n url?: string;\n content?: string;\n }>;\n };\n return (data.results ?? []).map((r) => ({\n title: r.title ?? \"\",\n url: r.url ?? \"\",\n snippet: r.content ?? \"\",\n }));\n}\n\nasync function searchExa(\n query: string,\n count: number,\n apiKey: string,\n): Promise<WebSearchResult[]> {\n const res = await fetch(\"https://api.exa.ai/search\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n },\n body: JSON.stringify({\n query,\n numResults: count,\n type: \"auto\",\n contents: { text: { maxCharacters: 400 } },\n }),\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) {\n throw new Error(`Exa error ${res.status}: ${await res.text()}`);\n }\n const data = (await res.json()) as {\n results?: Array<{\n title?: string;\n url?: string;\n text?: string;\n }>;\n };\n return (data.results ?? []).map((r) => ({\n title: r.title ?? \"\",\n url: r.url ?? \"\",\n snippet: r.text ?? \"\",\n }));\n}\n\nasync function searchBuilderManaged(\n query: string,\n count: number,\n credentials: BuilderWebSearchCredentials,\n opts: WebSearchToolOptions,\n): Promise<string> {\n const baseUrl =\n opts.getBuilderWebSearchBaseUrl?.() ??\n process.env.BUILDER_WEB_SEARCH_BASE_URL ??\n \"https://api.builder.io/agent-native/web-search/v1\";\n const url = new URL(\n \"search\",\n baseUrl.endsWith(\"/\") ? baseUrl : `${baseUrl}/`,\n );\n const res = await fetch(url.toString(), {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${credentials.privateKey}`,\n \"x-builder-api-key\": credentials.publicKey ?? \"\",\n ...(credentials.userId\n ? { \"x-builder-user-id\": credentials.userId }\n : {}),\n ...(opts.getBuilderRequestHeaders?.() ?? {}),\n },\n body: JSON.stringify({\n query,\n count,\n source: {\n appId: \"agent-native\",\n feature: \"web-search-tool\",\n },\n }),\n signal: AbortSignal.timeout(45_000),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\n `Builder web search error ${res.status}${text ? `: ${text.slice(0, 300)}` : \"\"}`,\n );\n }\n const data = (await res.json()) as {\n text?: string;\n };\n const text = data.text?.trim();\n if (!text) {\n throw new Error(\"Builder web search returned no text.\");\n }\n return text;\n}\n\n// ---------------------------------------------------------------------------\n// Tool entry factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create the web-search tool entry for the agent tool registry.\n */\nexport function createWebSearchToolEntry(\n opts: WebSearchToolOptions = {},\n): Record<string, ActionEntry> {\n return {\n \"web-search\": {\n tool: {\n description:\n \"Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, or EXA_API_KEY.\",\n parameters: {\n type: \"object\" as const,\n properties: {\n query: {\n type: \"string\",\n description:\n \"Search query. Be specific — include API name, version, and what you need (e.g. 'Stripe API list subscriptions endpoint').\",\n },\n count: {\n type: \"number\",\n description: `Number of results to return. Default ${DEFAULT_COUNT}, max ${MAX_COUNT}.`,\n },\n },\n required: [\"query\"],\n },\n },\n run: async (args: Record<string, string>) => {\n const query = (args.query ?? \"\").trim();\n if (!query) return \"query is required.\";\n\n const rawCount = Number(args.count);\n const count =\n Number.isFinite(rawCount) && rawCount > 0\n ? Math.min(Math.floor(rawCount), MAX_COUNT)\n : DEFAULT_COUNT;\n\n // Backend selection — first configured wins.\n const braveKey = await resolveSearchKey(\"BRAVE_SEARCH_API_KEY\", opts);\n const tavilyKey = await resolveSearchKey(\"TAVILY_API_KEY\", opts);\n const exaKey = await resolveSearchKey(\"EXA_API_KEY\", opts);\n const builderCredentials = await resolveBuilderSearchCredentials(opts);\n\n let results: WebSearchResult[];\n let backend: string;\n let managedText: string | null = null;\n\n try {\n if (braveKey) {\n results = await searchBrave(query, count, braveKey);\n backend = \"Brave Search\";\n } else if (tavilyKey) {\n results = await searchTavily(query, count, tavilyKey);\n backend = \"Tavily\";\n } else if (exaKey) {\n results = await searchExa(query, count, exaKey);\n backend = \"Exa\";\n } else if (builderCredentials) {\n managedText = await searchBuilderManaged(\n query,\n count,\n builderCredentials,\n opts,\n );\n results = [];\n backend = \"Builder.io\";\n } else {\n return [\n \"No web-search backend configured.\",\n \"Connect Builder.io in Settings, or add one of the following keys via app settings or environment variables:\",\n \" • BRAVE_SEARCH_API_KEY — https://brave.com/search/api/\",\n \" • TAVILY_API_KEY — https://tavily.com/\",\n \" • EXA_API_KEY — https://exa.ai/\",\n ].join(\"\\n\");\n }\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return `Web search failed (${msg}). Try web-request to fetch a specific URL directly.`;\n }\n\n if (managedText) {\n return [\n `Web search results for \"${query}\" (backend: ${backend}):`,\n \"\",\n managedText,\n \"\",\n \"Use web-request or provider-api-docs with responseMode:'markdown' for readable docs or responseMode:'matches' plus search for compact snippets.\",\n ].join(\"\\n\");\n }\n\n if (results.length === 0) {\n return `No results found for \"${query}\" (backend: ${backend}).`;\n }\n\n const lines: string[] = [\n `Web search results for \"${query}\" (backend: ${backend}):`,\n \"\",\n ];\n for (let i = 0; i < results.length; i++) {\n const r = results[i];\n lines.push(`${i + 1}. ${r.title}`);\n lines.push(` URL: ${r.url}`);\n if (r.snippet) {\n const snippet = r.snippet.slice(0, 300).replace(/\\n+/g, \" \");\n lines.push(` ${snippet}`);\n }\n lines.push(\"\");\n }\n lines.push(\n \"Use web-request or provider-api-docs with responseMode:'markdown' for readable docs or responseMode:'matches' plus search for compact snippets.\",\n );\n return lines.join(\"\\n\");\n },\n readOnly: true,\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"web-search-tool.js","sourceRoot":"","sources":["../../src/extensions/web-search-tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Web-search tool — agent tool for searching the public web.\n *\n * Pluggable backends resolved at call time based on which API key is\n * configured (env var or secrets/credentials store):\n *\n * 1. Brave Search API (BRAVE_SEARCH_API_KEY)\n * 2. Tavily (TAVILY_API_KEY)\n * 3. Exa (EXA_API_KEY)\n * 4. Firecrawl (FIRECRAWL_API_KEY)\n * 5. Builder.io (connected Builder credentials)\n *\n * The first configured backend wins. If none is configured, the tool\n * returns a helpful message telling the user which keys to add.\n *\n * Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,\n * EXA_API_KEY, or FIRECRAWL_API_KEY via app secrets settings or environment\n * variables.\n */\n\nimport type { ActionEntry } from \"../agent/production-agent.js\";\nimport type { CredentialContext } from \"../credentials/index.js\";\n\nexport interface WebSearchResult {\n title: string;\n url: string;\n snippet: string;\n}\n\nexport interface WebSearchToolOptions {\n /**\n * Resolve a request-scoped secret by key. When not provided the tool falls\n * back to env-var lookup only.\n */\n resolveSecret?: (key: string) => Promise<string | null>;\n /**\n * Legacy credential resolver retained for older callers.\n */\n resolveCredential?: (\n key: string,\n ctx: CredentialContext,\n ) => Promise<string | undefined>;\n /**\n * Legacy credential context callback retained for older callers.\n */\n getCredentialContext?: () => CredentialContext | null;\n /**\n * Resolve connected Builder credentials for managed web search.\n */\n resolveBuilderCredentials?: () => Promise<BuilderWebSearchCredentials>;\n /**\n * Base URL for Builder-managed web search.\n */\n getBuilderWebSearchBaseUrl?: () => string;\n /**\n * Stable attribution headers for Builder-managed API calls.\n */\n getBuilderRequestHeaders?: () => Record<string, string>;\n}\n\nconst DEFAULT_COUNT = 5;\nconst MAX_COUNT = 10;\n\ninterface BuilderWebSearchCredentials {\n privateKey: string | null;\n publicKey: string | null;\n userId?: string | null;\n}\n\nasync function resolveSearchKey(\n key: string,\n opts: WebSearchToolOptions,\n): Promise<string | undefined> {\n let usedRequestScopedResolver = false;\n // 1. Try request-scoped app secrets (user/org/workspace stored key).\n if (opts.resolveSecret) {\n usedRequestScopedResolver = true;\n try {\n const value = await opts.resolveSecret(key);\n if (value) return value;\n } catch {\n // Secret lookup failures are non-fatal; fall through to legacy/env.\n }\n }\n\n // 2. Try legacy per-request credential context.\n if (opts.resolveCredential && opts.getCredentialContext) {\n const ctx = opts.getCredentialContext();\n if (ctx) {\n try {\n const value = await opts.resolveCredential(key, ctx);\n if (value) return value;\n } catch {\n // Credential lookup failures are non-fatal; fall through to env.\n }\n }\n }\n\n if (usedRequestScopedResolver) return undefined;\n\n // 3. Fall back to env var.\n return process.env[key] || undefined;\n}\n\nasync function resolveBuilderSearchCredentials(\n opts: WebSearchToolOptions,\n): Promise<BuilderWebSearchCredentials | null> {\n if (!opts.resolveBuilderCredentials) return null;\n try {\n const creds = await opts.resolveBuilderCredentials();\n if (creds.privateKey && creds.publicKey) return creds;\n } catch {\n // Builder credential lookup failures are non-fatal; BYOK backends or the\n // setup hint below can still handle the tool call.\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Backend implementations\n// ---------------------------------------------------------------------------\n\nasync function searchBrave(\n query: string,\n count: number,\n apiKey: string,\n): Promise<WebSearchResult[]> {\n const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;\n const res = await fetch(url, {\n headers: {\n Accept: \"application/json\",\n \"Accept-Encoding\": \"gzip\",\n \"X-Subscription-Token\": apiKey,\n },\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) {\n throw new Error(`Brave Search error ${res.status}: ${await res.text()}`);\n }\n const data = (await res.json()) as {\n web?: {\n results?: Array<{\n title?: string;\n url?: string;\n description?: string;\n }>;\n };\n };\n return (data.web?.results ?? []).map((r) => ({\n title: r.title ?? \"\",\n url: r.url ?? \"\",\n snippet: r.description ?? \"\",\n }));\n}\n\nasync function searchTavily(\n query: string,\n count: number,\n apiKey: string,\n): Promise<WebSearchResult[]> {\n const res = await fetch(\"https://api.tavily.com/search\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n api_key: apiKey,\n query,\n max_results: count,\n search_depth: \"basic\",\n }),\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) {\n throw new Error(`Tavily error ${res.status}: ${await res.text()}`);\n }\n const data = (await res.json()) as {\n results?: Array<{\n title?: string;\n url?: string;\n content?: string;\n }>;\n };\n return (data.results ?? []).map((r) => ({\n title: r.title ?? \"\",\n url: r.url ?? \"\",\n snippet: r.content ?? \"\",\n }));\n}\n\nasync function searchExa(\n query: string,\n count: number,\n apiKey: string,\n): Promise<WebSearchResult[]> {\n const res = await fetch(\"https://api.exa.ai/search\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n },\n body: JSON.stringify({\n query,\n numResults: count,\n type: \"auto\",\n contents: { text: { maxCharacters: 400 } },\n }),\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) {\n throw new Error(`Exa error ${res.status}: ${await res.text()}`);\n }\n const data = (await res.json()) as {\n results?: Array<{\n title?: string;\n url?: string;\n text?: string;\n }>;\n };\n return (data.results ?? []).map((r) => ({\n title: r.title ?? \"\",\n url: r.url ?? \"\",\n snippet: r.text ?? \"\",\n }));\n}\n\nasync function searchFirecrawl(\n query: string,\n count: number,\n apiKey: string,\n): Promise<WebSearchResult[]> {\n const res = await fetch(\"https://api.firecrawl.dev/v2/search\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n query,\n limit: count,\n }),\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) {\n throw new Error(`Firecrawl error ${res.status}: ${await res.text()}`);\n }\n // v2 groups results by source ({ data: { web: [...] } }); older shapes\n // return a flat array ({ data: [...] }). Accept both defensively.\n type FirecrawlResult = { title?: string; url?: string; description?: string };\n const data = (await res.json()) as {\n data?: FirecrawlResult[] | { web?: FirecrawlResult[] };\n };\n const results = Array.isArray(data.data) ? data.data : (data.data?.web ?? []);\n return results.map((r) => ({\n title: r.title ?? \"\",\n url: r.url ?? \"\",\n snippet: r.description ?? \"\",\n }));\n}\n\nasync function searchBuilderManaged(\n query: string,\n count: number,\n credentials: BuilderWebSearchCredentials,\n opts: WebSearchToolOptions,\n): Promise<string> {\n const baseUrl =\n opts.getBuilderWebSearchBaseUrl?.() ??\n process.env.BUILDER_WEB_SEARCH_BASE_URL ??\n \"https://api.builder.io/agent-native/web-search/v1\";\n const url = new URL(\n \"search\",\n baseUrl.endsWith(\"/\") ? baseUrl : `${baseUrl}/`,\n );\n const res = await fetch(url.toString(), {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${credentials.privateKey}`,\n \"x-builder-api-key\": credentials.publicKey ?? \"\",\n ...(credentials.userId\n ? { \"x-builder-user-id\": credentials.userId }\n : {}),\n ...(opts.getBuilderRequestHeaders?.() ?? {}),\n },\n body: JSON.stringify({\n query,\n count,\n source: {\n appId: \"agent-native\",\n feature: \"web-search-tool\",\n },\n }),\n signal: AbortSignal.timeout(45_000),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\n `Builder web search error ${res.status}${text ? `: ${text.slice(0, 300)}` : \"\"}`,\n );\n }\n const data = (await res.json()) as {\n text?: string;\n };\n const text = data.text?.trim();\n if (!text) {\n throw new Error(\"Builder web search returned no text.\");\n }\n return text;\n}\n\n// ---------------------------------------------------------------------------\n// Tool entry factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create the web-search tool entry for the agent tool registry.\n */\nexport function createWebSearchToolEntry(\n opts: WebSearchToolOptions = {},\n): Record<string, ActionEntry> {\n return {\n \"web-search\": {\n tool: {\n description:\n \"Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, EXA_API_KEY, or FIRECRAWL_API_KEY.\",\n parameters: {\n type: \"object\" as const,\n properties: {\n query: {\n type: \"string\",\n description:\n \"Search query. Be specific — include API name, version, and what you need (e.g. 'Stripe API list subscriptions endpoint').\",\n },\n count: {\n type: \"number\",\n description: `Number of results to return. Default ${DEFAULT_COUNT}, max ${MAX_COUNT}.`,\n },\n },\n required: [\"query\"],\n },\n },\n run: async (args: Record<string, string>) => {\n const query = (args.query ?? \"\").trim();\n if (!query) return \"query is required.\";\n\n const rawCount = Number(args.count);\n const count =\n Number.isFinite(rawCount) && rawCount > 0\n ? Math.min(Math.floor(rawCount), MAX_COUNT)\n : DEFAULT_COUNT;\n\n // Backend selection — first configured wins.\n const braveKey = await resolveSearchKey(\"BRAVE_SEARCH_API_KEY\", opts);\n const tavilyKey = await resolveSearchKey(\"TAVILY_API_KEY\", opts);\n const exaKey = await resolveSearchKey(\"EXA_API_KEY\", opts);\n const firecrawlKey = await resolveSearchKey(\"FIRECRAWL_API_KEY\", opts);\n const builderCredentials = await resolveBuilderSearchCredentials(opts);\n\n let results: WebSearchResult[];\n let backend: string;\n let managedText: string | null = null;\n\n try {\n if (braveKey) {\n results = await searchBrave(query, count, braveKey);\n backend = \"Brave Search\";\n } else if (tavilyKey) {\n results = await searchTavily(query, count, tavilyKey);\n backend = \"Tavily\";\n } else if (exaKey) {\n results = await searchExa(query, count, exaKey);\n backend = \"Exa\";\n } else if (firecrawlKey) {\n results = await searchFirecrawl(query, count, firecrawlKey);\n backend = \"Firecrawl\";\n } else if (builderCredentials) {\n managedText = await searchBuilderManaged(\n query,\n count,\n builderCredentials,\n opts,\n );\n results = [];\n backend = \"Builder.io\";\n } else {\n return [\n \"No web-search backend configured.\",\n \"Connect Builder.io in Settings, or add one of the following keys via app settings or environment variables:\",\n \" • BRAVE_SEARCH_API_KEY — https://brave.com/search/api/\",\n \" • TAVILY_API_KEY — https://tavily.com/\",\n \" • EXA_API_KEY — https://exa.ai/\",\n \" • FIRECRAWL_API_KEY — https://firecrawl.dev/\",\n ].join(\"\\n\");\n }\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return `Web search failed (${msg}). Try web-request to fetch a specific URL directly.`;\n }\n\n if (managedText) {\n return [\n `Web search results for \"${query}\" (backend: ${backend}):`,\n \"\",\n managedText,\n \"\",\n \"Use web-request or provider-api-docs with responseMode:'markdown' for readable docs or responseMode:'matches' plus search for compact snippets.\",\n ].join(\"\\n\");\n }\n\n if (results.length === 0) {\n return `No results found for \"${query}\" (backend: ${backend}).`;\n }\n\n const lines: string[] = [\n `Web search results for \"${query}\" (backend: ${backend}):`,\n \"\",\n ];\n for (let i = 0; i < results.length; i++) {\n const r = results[i];\n lines.push(`${i + 1}. ${r.title}`);\n lines.push(` URL: ${r.url}`);\n if (r.snippet) {\n const snippet = r.snippet.slice(0, 300).replace(/\\n+/g, \" \");\n lines.push(` ${snippet}`);\n }\n lines.push(\"\");\n }\n lines.push(\n \"Use web-request or provider-api-docs with responseMode:'markdown' for readable docs or responseMode:'matches' plus search for compact snippets.\",\n );\n return lines.join(\"\\n\");\n },\n readOnly: true,\n },\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-framework-secrets.d.ts","sourceRoot":"","sources":["../../src/secrets/register-framework-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,wBAAgB,wBAAwB,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"register-framework-secrets.d.ts","sourceRoot":"","sources":["../../src/secrets/register-framework-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,wBAAgB,wBAAwB,IAAI,IAAI,CAkE/C"}
|
|
@@ -37,6 +37,12 @@ export function registerFrameworkSecrets() {
|
|
|
37
37
|
description: "Enables the web-search agent tool via Exa. Used as fallback when Brave and Tavily are not set and before Builder-managed search.",
|
|
38
38
|
docsUrl: "https://exa.ai/",
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
key: "FIRECRAWL_API_KEY",
|
|
42
|
+
label: "Firecrawl API Key",
|
|
43
|
+
description: "Enables the web-search agent tool via Firecrawl. Used as fallback when Brave, Tavily, and Exa are not set and before Builder-managed search.",
|
|
44
|
+
docsUrl: "https://firecrawl.dev/",
|
|
45
|
+
},
|
|
40
46
|
];
|
|
41
47
|
for (const entry of webSearchKeys) {
|
|
42
48
|
if (!getRequiredSecret(entry.key)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-framework-secrets.js","sourceRoot":"","sources":["../../src/secrets/register-framework-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAE1E,MAAM,UAAU,wBAAwB;IACtC,kEAAkE;IAClE,0EAA0E;IAC1E,MAAM,aAAa,GAKd;QACH;YACE,GAAG,EAAE,sBAAsB;YAC3B,KAAK,EAAE,sBAAsB;YAC7B,WAAW,EACT,mHAAmH;YACrH,OAAO,EAAE,+BAA+B;SACzC;QACD;YACE,GAAG,EAAE,gBAAgB;YACrB,KAAK,EAAE,gBAAgB;YACvB,WAAW,EACT,wIAAwI;YAC1I,OAAO,EAAE,qBAAqB;SAC/B;QACD;YACE,GAAG,EAAE,aAAa;YAClB,KAAK,EAAE,aAAa;YACpB,WAAW,EACT,kIAAkI;YACpI,OAAO,EAAE,iBAAiB;SAC3B;KACF,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAClC,sBAAsB,CAAC;gBACrB,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,KAAK,EAAE,WAAW;gBAClB,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,KAAK;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,EAAE,CAAC;QACvC,sBAAsB,CAAC;YACrB,GAAG,EAAE,cAAc;YACnB,KAAK,EAAE,cAAc;YACrB,WAAW,EACT,0FAA0F;YAC5F,OAAO,EACL,kHAAkH;YACpH,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAC;IACL,CAAC;AACH,CAAC","sourcesContent":["/**\n * Framework-level secret registrations.\n *\n * Side-effect module — imported by the core-routes plugin at boot so the\n * sidebar settings UI and the `/_agent-native/secrets` list route surface the\n * relevant keys in every template.\n *\n * Each call uses a `getRequiredSecret` guard so a template that has already\n * registered the same key (often with stricter settings like `required: true`)\n * wins — the framework registration is a fallback, not an override.\n *\n * NOTE: The framework previously registered OPENAI_API_KEY here for Whisper\n * voice transcription. Voice transcription now routes through the Builder.io\n * gateway (or Groq as a BYOK fallback), so the framework no longer registers\n * the OpenAI key. Templates that need it (e.g. Clips) register it themselves.\n */\n\nimport { getRequiredSecret, registerRequiredSecret } from \"./register.js\";\n\nexport function registerFrameworkSecrets(): void {\n // Web-search tool backends — optional; the tool selects the first\n // configured manual key at call time, then falls back to Builder Connect.\n const webSearchKeys: Array<{\n key: string;\n label: string;\n description: string;\n docsUrl: string;\n }> = [\n {\n key: \"BRAVE_SEARCH_API_KEY\",\n label: \"Brave Search API Key\",\n description:\n \"Enables the web-search agent tool via Brave Search. Optional when Builder.io is connected for managed web search.\",\n docsUrl: \"https://brave.com/search/api/\",\n },\n {\n key: \"TAVILY_API_KEY\",\n label: \"Tavily API Key\",\n description:\n \"Enables the web-search agent tool via Tavily. Used as fallback when BRAVE_SEARCH_API_KEY is not set and before Builder-managed search.\",\n docsUrl: \"https://tavily.com/\",\n },\n {\n key: \"EXA_API_KEY\",\n label: \"Exa API Key\",\n description:\n \"Enables the web-search agent tool via Exa. Used as fallback when Brave and Tavily are not set and before Builder-managed search.\",\n docsUrl: \"https://exa.ai/\",\n },\n ];\n\n for (const entry of webSearchKeys) {\n if (!getRequiredSecret(entry.key)) {\n registerRequiredSecret({\n key: entry.key,\n label: entry.label,\n description: entry.description,\n docsUrl: entry.docsUrl,\n scope: \"workspace\",\n kind: \"api-key\",\n required: false,\n });\n }\n }\n\n if (!getRequiredSecret(\"GITHUB_TOKEN\")) {\n registerRequiredSecret({\n key: \"GITHUB_TOKEN\",\n label: \"GitHub token\",\n description:\n \"Enables connector-scoped repository file reads and writes for headless/cloud agent runs.\",\n docsUrl:\n \"https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\",\n scope: \"workspace\",\n kind: \"api-key\",\n required: false,\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"register-framework-secrets.js","sourceRoot":"","sources":["../../src/secrets/register-framework-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAE1E,MAAM,UAAU,wBAAwB;IACtC,kEAAkE;IAClE,0EAA0E;IAC1E,MAAM,aAAa,GAKd;QACH;YACE,GAAG,EAAE,sBAAsB;YAC3B,KAAK,EAAE,sBAAsB;YAC7B,WAAW,EACT,mHAAmH;YACrH,OAAO,EAAE,+BAA+B;SACzC;QACD;YACE,GAAG,EAAE,gBAAgB;YACrB,KAAK,EAAE,gBAAgB;YACvB,WAAW,EACT,wIAAwI;YAC1I,OAAO,EAAE,qBAAqB;SAC/B;QACD;YACE,GAAG,EAAE,aAAa;YAClB,KAAK,EAAE,aAAa;YACpB,WAAW,EACT,kIAAkI;YACpI,OAAO,EAAE,iBAAiB;SAC3B;QACD;YACE,GAAG,EAAE,mBAAmB;YACxB,KAAK,EAAE,mBAAmB;YAC1B,WAAW,EACT,8IAA8I;YAChJ,OAAO,EAAE,wBAAwB;SAClC;KACF,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAClC,sBAAsB,CAAC;gBACrB,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,KAAK,EAAE,WAAW;gBAClB,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,KAAK;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,EAAE,CAAC;QACvC,sBAAsB,CAAC;YACrB,GAAG,EAAE,cAAc;YACnB,KAAK,EAAE,cAAc;YACrB,WAAW,EACT,0FAA0F;YAC5F,OAAO,EACL,kHAAkH;YACpH,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAC;IACL,CAAC;AACH,CAAC","sourcesContent":["/**\n * Framework-level secret registrations.\n *\n * Side-effect module — imported by the core-routes plugin at boot so the\n * sidebar settings UI and the `/_agent-native/secrets` list route surface the\n * relevant keys in every template.\n *\n * Each call uses a `getRequiredSecret` guard so a template that has already\n * registered the same key (often with stricter settings like `required: true`)\n * wins — the framework registration is a fallback, not an override.\n *\n * NOTE: The framework previously registered OPENAI_API_KEY here for Whisper\n * voice transcription. Voice transcription now routes through the Builder.io\n * gateway (or Groq as a BYOK fallback), so the framework no longer registers\n * the OpenAI key. Templates that need it (e.g. Clips) register it themselves.\n */\n\nimport { getRequiredSecret, registerRequiredSecret } from \"./register.js\";\n\nexport function registerFrameworkSecrets(): void {\n // Web-search tool backends — optional; the tool selects the first\n // configured manual key at call time, then falls back to Builder Connect.\n const webSearchKeys: Array<{\n key: string;\n label: string;\n description: string;\n docsUrl: string;\n }> = [\n {\n key: \"BRAVE_SEARCH_API_KEY\",\n label: \"Brave Search API Key\",\n description:\n \"Enables the web-search agent tool via Brave Search. Optional when Builder.io is connected for managed web search.\",\n docsUrl: \"https://brave.com/search/api/\",\n },\n {\n key: \"TAVILY_API_KEY\",\n label: \"Tavily API Key\",\n description:\n \"Enables the web-search agent tool via Tavily. Used as fallback when BRAVE_SEARCH_API_KEY is not set and before Builder-managed search.\",\n docsUrl: \"https://tavily.com/\",\n },\n {\n key: \"EXA_API_KEY\",\n label: \"Exa API Key\",\n description:\n \"Enables the web-search agent tool via Exa. Used as fallback when Brave and Tavily are not set and before Builder-managed search.\",\n docsUrl: \"https://exa.ai/\",\n },\n {\n key: \"FIRECRAWL_API_KEY\",\n label: \"Firecrawl API Key\",\n description:\n \"Enables the web-search agent tool via Firecrawl. Used as fallback when Brave, Tavily, and Exa are not set and before Builder-managed search.\",\n docsUrl: \"https://firecrawl.dev/\",\n },\n ];\n\n for (const entry of webSearchKeys) {\n if (!getRequiredSecret(entry.key)) {\n registerRequiredSecret({\n key: entry.key,\n label: entry.label,\n description: entry.description,\n docsUrl: entry.docsUrl,\n scope: \"workspace\",\n kind: \"api-key\",\n required: false,\n });\n }\n }\n\n if (!getRequiredSecret(\"GITHUB_TOKEN\")) {\n registerRequiredSecret({\n key: \"GITHUB_TOKEN\",\n label: \"GitHub token\",\n description:\n \"Enables connector-scoped repository file reads and writes for headless/cloud agent runs.\",\n docsUrl:\n \"https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\",\n scope: \"workspace\",\n kind: \"api-key\",\n required: false,\n });\n }\n}\n"]}
|