@agent-native/core 0.185.0 → 0.186.0-nightly-20260923163746
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/templates/clips/actions/finalize-recording.ts +35 -1
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
- package/corpus/templates/clips/app/routes/record.tsx +1 -0
- package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
- package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
- package/dist/agent/engine/provider-endpoint-validation.js +47 -10
- package/dist/agent/engine/registry.js +8 -5
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.js +45 -9
- package/dist/client/agent-engine-key.d.ts +7 -0
- package/dist/client/agent-engine-key.js +37 -0
- package/dist/client/agent-provider-catalog.js +1 -1
- package/dist/client/settings/ProviderSetupForm.js +80 -7
- package/dist/client/settings/SettingsPanel.js +149 -11
- package/dist/client/use-chat-models.js +31 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +7 -1
- package/dist/localization/default-messages.js +7 -1
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.js +8 -5
- package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
- package/dist/server/agent-engine-ollama-models-route.js +105 -0
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/core-routes-plugin.js +5 -0
- package/dist/server/credential-provider.d.ts +13 -0
- package/dist/server/credential-provider.js +19 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -323,8 +323,35 @@ async function failStoredButUnservableRecording(params: {
|
|
|
323
323
|
eq(schema.recordings.status, "processing"),
|
|
324
324
|
),
|
|
325
325
|
)
|
|
326
|
-
.returning({
|
|
326
|
+
.returning({
|
|
327
|
+
id: schema.recordings.id,
|
|
328
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
329
|
+
});
|
|
327
330
|
if (failed.length !== 1) return false;
|
|
331
|
+
try {
|
|
332
|
+
track(
|
|
333
|
+
"clips_upload_blocking_failure",
|
|
334
|
+
{
|
|
335
|
+
app: "clips",
|
|
336
|
+
template: "clips",
|
|
337
|
+
surface: "media_verification",
|
|
338
|
+
stage: "media_verification",
|
|
339
|
+
outcome: "failed",
|
|
340
|
+
failure_type: "media_verification",
|
|
341
|
+
failure_code: "media_verification_failed",
|
|
342
|
+
output_id: id,
|
|
343
|
+
output_type: "clip",
|
|
344
|
+
recording_id: id,
|
|
345
|
+
recording_attempt_id: id,
|
|
346
|
+
...(failed[0]?.uploadAttemptId
|
|
347
|
+
? { upload_attempt_id: failed[0].uploadAttemptId }
|
|
348
|
+
: {}),
|
|
349
|
+
},
|
|
350
|
+
{ userId: ownerEmail },
|
|
351
|
+
);
|
|
352
|
+
} catch {
|
|
353
|
+
// coercion-ok: analytics is best-effort and must not change media recovery behavior.
|
|
354
|
+
}
|
|
328
355
|
const uploadStateRaw = await readAppState(`recording-upload-${id}`).catch(
|
|
329
356
|
() => null,
|
|
330
357
|
);
|
|
@@ -605,6 +632,7 @@ async function markRecordingReady(params: {
|
|
|
605
632
|
finalHeight: number;
|
|
606
633
|
finalHasAudio: boolean;
|
|
607
634
|
finalHasCamera: boolean;
|
|
635
|
+
recordingAttemptId?: string | null;
|
|
608
636
|
existingTitle: string;
|
|
609
637
|
// Whether a seekable rewrite (MP4 faststart / WebM Cues remux) was already
|
|
610
638
|
// applied to the uploaded bytes. When false, a best-effort background repair
|
|
@@ -623,6 +651,7 @@ async function markRecordingReady(params: {
|
|
|
623
651
|
finalHeight,
|
|
624
652
|
finalHasAudio,
|
|
625
653
|
finalHasCamera,
|
|
654
|
+
recordingAttemptId,
|
|
626
655
|
existingTitle,
|
|
627
656
|
seekableApplied,
|
|
628
657
|
} = params;
|
|
@@ -716,6 +745,8 @@ async function markRecordingReady(params: {
|
|
|
716
745
|
template_name: "clips",
|
|
717
746
|
output_id: id,
|
|
718
747
|
output_type: "clip",
|
|
748
|
+
recording_attempt_id: id,
|
|
749
|
+
...(recordingAttemptId ? { upload_attempt_id: recordingAttemptId } : {}),
|
|
719
750
|
duration_s: Math.round(finalDurationMs / 1000),
|
|
720
751
|
video_format: videoFormat,
|
|
721
752
|
has_audio: finalHasAudio,
|
|
@@ -864,6 +895,7 @@ async function retryPendingMediaVerification(params: {
|
|
|
864
895
|
const db = getDb();
|
|
865
896
|
const [recording] = await db
|
|
866
897
|
.select({
|
|
898
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
867
899
|
status: schema.recordings.status,
|
|
868
900
|
videoUrl: schema.recordings.videoUrl,
|
|
869
901
|
editsJson: schema.recordings.editsJson,
|
|
@@ -919,6 +951,7 @@ async function retryPendingMediaVerification(params: {
|
|
|
919
951
|
finalHeight: candidate.finalHeight,
|
|
920
952
|
finalHasAudio: candidate.finalHasAudio,
|
|
921
953
|
finalHasCamera: candidate.finalHasCamera,
|
|
954
|
+
recordingAttemptId: recording.uploadAttemptId,
|
|
922
955
|
seekableApplied: candidate.seekableApplied,
|
|
923
956
|
});
|
|
924
957
|
if (result.status === "ready" && result.transitionedToReady) {
|
|
@@ -1196,6 +1229,7 @@ export default defineAction({
|
|
|
1196
1229
|
finalHeight,
|
|
1197
1230
|
finalHasAudio,
|
|
1198
1231
|
finalHasCamera,
|
|
1232
|
+
recordingAttemptId: existing.uploadAttemptId,
|
|
1199
1233
|
existingTitle: existing.title,
|
|
1200
1234
|
};
|
|
1201
1235
|
|
|
@@ -105,7 +105,11 @@ export function RecordingOptionsMenu({
|
|
|
105
105
|
</DropdownMenuTrigger>
|
|
106
106
|
<DropdownMenuContent
|
|
107
107
|
align="end"
|
|
108
|
-
className={
|
|
108
|
+
className={
|
|
109
|
+
showCustomItems
|
|
110
|
+
? "w-64 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-x-hidden overflow-y-auto"
|
|
111
|
+
: "w-44"
|
|
112
|
+
}
|
|
109
113
|
onCloseAutoFocus={(event) => {
|
|
110
114
|
// Opening the AlertDialog while this menu is still tearing down
|
|
111
115
|
// leaves `pointer-events: none` stuck on <body>: two dismissable
|
|
@@ -399,12 +399,27 @@ function isRetryableChunkUploadStatus(status: number): boolean {
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
function trackClipUploadBlockingFailure(props: Record<string, unknown>): void {
|
|
402
|
+
const recordingId =
|
|
403
|
+
typeof props.recordingId === "string" ? props.recordingId : undefined;
|
|
404
|
+
const uploadAttemptId =
|
|
405
|
+
typeof props.uploadAttemptId === "string"
|
|
406
|
+
? props.uploadAttemptId
|
|
407
|
+
: undefined;
|
|
402
408
|
try {
|
|
403
409
|
trackEvent("clips_upload_blocking_failure", {
|
|
410
|
+
...props,
|
|
404
411
|
app: "clips",
|
|
405
412
|
template: "clips",
|
|
406
413
|
surface: "web_recorder",
|
|
407
|
-
|
|
414
|
+
output_id: recordingId,
|
|
415
|
+
output_type: "clip",
|
|
416
|
+
recording_id: recordingId,
|
|
417
|
+
recording_attempt_id: recordingId,
|
|
418
|
+
...(uploadAttemptId ? { upload_attempt_id: uploadAttemptId } : {}),
|
|
419
|
+
failure_code:
|
|
420
|
+
typeof props.failureKind === "string"
|
|
421
|
+
? props.failureKind
|
|
422
|
+
: "upload_failed",
|
|
408
423
|
});
|
|
409
424
|
} catch {
|
|
410
425
|
// Analytics should never change recording behavior.
|
|
@@ -2623,6 +2638,7 @@ export class RecorderEngine {
|
|
|
2623
2638
|
stage: "chunk_upload",
|
|
2624
2639
|
failureKind: "no_response",
|
|
2625
2640
|
recordingId: this.opts.recordingId,
|
|
2641
|
+
uploadAttemptId: this.uploadAttemptId,
|
|
2626
2642
|
chunkIndex: index,
|
|
2627
2643
|
isFinal: extra.isFinal === true,
|
|
2628
2644
|
chunkBytes: blob.size,
|
|
@@ -2663,6 +2679,7 @@ export class RecorderEngine {
|
|
|
2663
2679
|
stage: "chunk_upload",
|
|
2664
2680
|
failureKind: "http_error",
|
|
2665
2681
|
recordingId: this.opts.recordingId,
|
|
2682
|
+
uploadAttemptId: this.uploadAttemptId,
|
|
2666
2683
|
chunkIndex: index,
|
|
2667
2684
|
isFinal: extra.isFinal === true,
|
|
2668
2685
|
httpStatus: res.status,
|
|
@@ -2347,7 +2347,7 @@ export default function RecordingPage() {
|
|
|
2347
2347
|
<DropdownMenuSubTrigger>
|
|
2348
2348
|
{t("recordingPage.enhanceRecording")}
|
|
2349
2349
|
</DropdownMenuSubTrigger>
|
|
2350
|
-
<DropdownMenuSubContent className="w-56">
|
|
2350
|
+
<DropdownMenuSubContent className="w-56 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-x-hidden overflow-y-auto">
|
|
2351
2351
|
<DropdownMenuItem
|
|
2352
2352
|
disabled={requestTranscript.isPending}
|
|
2353
2353
|
onSelect={() =>
|
|
@@ -2380,7 +2380,7 @@ export default function RecordingPage() {
|
|
|
2380
2380
|
<DropdownMenuSubTrigger>
|
|
2381
2381
|
{t("recordingPage.createFromClip")}
|
|
2382
2382
|
</DropdownMenuSubTrigger>
|
|
2383
|
-
<DropdownMenuSubContent className="w-64">
|
|
2383
|
+
<DropdownMenuSubContent className="w-64 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-x-hidden overflow-y-auto">
|
|
2384
2384
|
{WORKFLOW_MENU_ITEMS.map((item) => {
|
|
2385
2385
|
const menuItem = (
|
|
2386
2386
|
<DropdownMenuItem
|
|
@@ -2112,6 +2112,7 @@ export default function RecordRoute() {
|
|
|
2112
2112
|
app_name: "clips",
|
|
2113
2113
|
template_name: "clips",
|
|
2114
2114
|
output_id: pendingRef.current?.id,
|
|
2115
|
+
recording_attempt_id: pendingRef.current?.id,
|
|
2115
2116
|
capture_type:
|
|
2116
2117
|
recordingMode === "camera"
|
|
2117
2118
|
? "camera"
|
|
@@ -180,6 +180,8 @@ function expectedDataChunksForFinalPost(
|
|
|
180
180
|
|
|
181
181
|
function trackUploadBlockingFailure(
|
|
182
182
|
ownerEmail: string,
|
|
183
|
+
recordingId: string,
|
|
184
|
+
attemptId: string | null,
|
|
183
185
|
properties: Record<string, unknown>,
|
|
184
186
|
): void {
|
|
185
187
|
try {
|
|
@@ -189,6 +191,14 @@ function trackUploadBlockingFailure(
|
|
|
189
191
|
app: "clips",
|
|
190
192
|
template: "clips",
|
|
191
193
|
surface: "server_upload",
|
|
194
|
+
output_id: recordingId,
|
|
195
|
+
output_type: "clip",
|
|
196
|
+
recording_id: recordingId,
|
|
197
|
+
// Keep this stable for the recording lifecycle; upload_attempt_id
|
|
198
|
+
// disambiguates resumable retries without breaking the join to start.
|
|
199
|
+
recording_attempt_id: recordingId,
|
|
200
|
+
...(attemptId ? { upload_attempt_id: attemptId } : {}),
|
|
201
|
+
failure_code: properties.failure_code ?? properties.failure_type,
|
|
192
202
|
...properties,
|
|
193
203
|
},
|
|
194
204
|
{ userId: ownerEmail },
|
|
@@ -693,7 +703,7 @@ export async function handleRecordingChunk(
|
|
|
693
703
|
});
|
|
694
704
|
if ((result as any)?.status === "failed") {
|
|
695
705
|
const failure = finalizeResultFailure(result);
|
|
696
|
-
trackUploadBlockingFailure(ownerEmail, {
|
|
706
|
+
trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
|
|
697
707
|
stage: "finalize_recording",
|
|
698
708
|
outcome: failure.outcome,
|
|
699
709
|
failure_type: failure.failure_type,
|
|
@@ -812,7 +822,7 @@ export async function handleRecordingChunk(
|
|
|
812
822
|
return acceptedProcessingResponse(event, recordingId, pendingState);
|
|
813
823
|
}
|
|
814
824
|
}
|
|
815
|
-
trackUploadBlockingFailure(ownerEmail, {
|
|
825
|
+
trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
|
|
816
826
|
stage: "finalize_recording",
|
|
817
827
|
outcome: "failed",
|
|
818
828
|
failure_type: classifyTrackingFailure(err),
|
|
@@ -1273,7 +1283,7 @@ async function handleResumableChunk(
|
|
|
1273
1283
|
);
|
|
1274
1284
|
if ((result as any)?.status === "failed") {
|
|
1275
1285
|
const failure = finalizeResultFailure(result);
|
|
1276
|
-
trackUploadBlockingFailure(ownerEmail, {
|
|
1286
|
+
trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
|
|
1277
1287
|
stage: "finalize_recording",
|
|
1278
1288
|
outcome: failure.outcome,
|
|
1279
1289
|
failure_type: failure.failure_type,
|
|
@@ -1380,7 +1390,7 @@ async function handleResumableChunk(
|
|
|
1380
1390
|
}
|
|
1381
1391
|
}
|
|
1382
1392
|
|
|
1383
|
-
trackUploadBlockingFailure(ownerEmail, {
|
|
1393
|
+
trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
|
|
1384
1394
|
stage: "finalize_recording",
|
|
1385
1395
|
outcome: "failed",
|
|
1386
1396
|
failure_type: classifyTrackingFailure(err),
|
|
@@ -4,3 +4,10 @@ export declare const OLLAMA_BASE_URL_ENV_VAR = "OLLAMA_BASE_URL";
|
|
|
4
4
|
export declare function isCustomOpenAiBaseUrl(value: string | undefined): boolean;
|
|
5
5
|
export declare function normalizeProviderBaseUrl(value: string): string;
|
|
6
6
|
export declare function normalizeOpenAiBaseUrl(value: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Ollama's own API has no `/v1` path — that suffix only applies to its
|
|
9
|
+
* OpenAI-compatibility shim, which this integration doesn't use. Docs for
|
|
10
|
+
* other tools commonly show `/v1`, so users copy-paste it in; strip it
|
|
11
|
+
* silently rather than making them notice and remove it themselves.
|
|
12
|
+
*/
|
|
13
|
+
export declare function stripOllamaV1Suffix(value: string): string;
|
|
@@ -28,3 +28,12 @@ export function normalizeProviderBaseUrl(value) {
|
|
|
28
28
|
export function normalizeOpenAiBaseUrl(value) {
|
|
29
29
|
return normalizeProviderBaseUrl(value);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Ollama's own API has no `/v1` path — that suffix only applies to its
|
|
33
|
+
* OpenAI-compatibility shim, which this integration doesn't use. Docs for
|
|
34
|
+
* other tools commonly show `/v1`, so users copy-paste it in; strip it
|
|
35
|
+
* silently rather than making them notice and remove it themselves.
|
|
36
|
+
*/
|
|
37
|
+
export function stripOllamaV1Suffix(value) {
|
|
38
|
+
return value.replace(/\/v1$/i, "");
|
|
39
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validate a provider endpoint before a server-side model request can use it.
|
|
3
3
|
* `allowPrivate` is reserved for operator-owned deployment configuration; it
|
|
4
|
-
* must never be enabled for a user- or agent-supplied URL.
|
|
4
|
+
* must never be enabled for a user- or agent-supplied URL. `allowLocalOllama`
|
|
5
|
+
* additionally requires the caller to have already established this is a
|
|
6
|
+
* trusted, self-hosted context (see `isTrustedSelfHostedRuntime`) — it is
|
|
7
|
+
* never safe to grant purely from the endpoint value itself. `isOllama` says
|
|
8
|
+
* this endpoint is for Ollama specifically (independent of trust level), so a
|
|
9
|
+
* copy-pasted trailing `/v1` — meaningless for Ollama's own API — is silently
|
|
10
|
+
* removed instead of left to confuse the address that gets saved and used.
|
|
5
11
|
*/
|
|
6
12
|
export declare function validateProviderBaseUrl(value: string, options?: {
|
|
7
13
|
allowPrivate?: boolean;
|
|
8
14
|
allowLocalOllama?: boolean;
|
|
15
|
+
isOllama?: boolean;
|
|
9
16
|
}): Promise<string>;
|
|
@@ -1,20 +1,57 @@
|
|
|
1
1
|
import { isBlockedExtensionUrlWithDns } from "../../extensions/url-safety.js";
|
|
2
|
-
import { normalizeProviderBaseUrl } from "./openai-compatible-endpoint.js";
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { normalizeProviderBaseUrl, stripOllamaV1Suffix, } from "./openai-compatible-endpoint.js";
|
|
3
|
+
function isPrivateIpv4Lan(a, b, c, d) {
|
|
4
|
+
if (![a, b, c, d].every((part) => part >= 0 && part <= 255))
|
|
5
|
+
return false;
|
|
6
|
+
if (a === 127)
|
|
7
|
+
return true; // loopback
|
|
8
|
+
if (a === 10)
|
|
9
|
+
return true; // 10.0.0.0/8
|
|
10
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
11
|
+
return true; // 172.16.0.0/12
|
|
12
|
+
if (a === 192 && b === 168)
|
|
13
|
+
return true; // 192.168.0.0/16
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* True for hosts an Ollama server commonly runs on: loopback, or an RFC1918
|
|
18
|
+
* LAN address such as `192.168.1.123`. Deliberately narrower than the
|
|
19
|
+
* generic SSRF private-address check in `url-safety.ts` — link-local
|
|
20
|
+
* addresses (169.254.x.x, which also covers the cloud metadata endpoint) and
|
|
21
|
+
* other reserved ranges stay blocked even for Ollama. Only literal IPs and
|
|
22
|
+
* `localhost` are recognized; an arbitrary hostname that merely resolves to
|
|
23
|
+
* a LAN address is not, so this never depends on a DNS lookup.
|
|
24
|
+
*/
|
|
25
|
+
function isLocalNetworkOllamaEndpoint(value) {
|
|
26
|
+
const hostname = new URL(value).hostname
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/^\[|\]$/g, "");
|
|
29
|
+
if (hostname === "localhost" || hostname === "::1")
|
|
30
|
+
return true;
|
|
31
|
+
const parts = hostname.split(".");
|
|
32
|
+
if (parts.length === 4 && parts.every((p) => /^\d+$/.test(p))) {
|
|
33
|
+
const [a, b, c, d] = parts.map(Number);
|
|
34
|
+
return isPrivateIpv4Lan(a, b, c, d);
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
9
37
|
}
|
|
10
38
|
/**
|
|
11
39
|
* Validate a provider endpoint before a server-side model request can use it.
|
|
12
40
|
* `allowPrivate` is reserved for operator-owned deployment configuration; it
|
|
13
|
-
* must never be enabled for a user- or agent-supplied URL.
|
|
41
|
+
* must never be enabled for a user- or agent-supplied URL. `allowLocalOllama`
|
|
42
|
+
* additionally requires the caller to have already established this is a
|
|
43
|
+
* trusted, self-hosted context (see `isTrustedSelfHostedRuntime`) — it is
|
|
44
|
+
* never safe to grant purely from the endpoint value itself. `isOllama` says
|
|
45
|
+
* this endpoint is for Ollama specifically (independent of trust level), so a
|
|
46
|
+
* copy-pasted trailing `/v1` — meaningless for Ollama's own API — is silently
|
|
47
|
+
* removed instead of left to confuse the address that gets saved and used.
|
|
14
48
|
*/
|
|
15
49
|
export async function validateProviderBaseUrl(value, options = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
50
|
+
let normalized = normalizeProviderBaseUrl(value);
|
|
51
|
+
if (options.isOllama)
|
|
52
|
+
normalized = stripOllamaV1Suffix(normalized);
|
|
53
|
+
const allowLocalOllama = options.allowLocalOllama === true &&
|
|
54
|
+
isLocalNetworkOllamaEndpoint(normalized);
|
|
18
55
|
if (!options.allowPrivate &&
|
|
19
56
|
!allowLocalOllama &&
|
|
20
57
|
(await isBlockedExtensionUrlWithDns(normalized))) {
|
|
@@ -12,7 +12,7 @@ import { getAppConfig } from "../../app-config/index.js";
|
|
|
12
12
|
import { getUserLabs } from "../../labs/store.js";
|
|
13
13
|
import { BUILDER_OAUTH_SCOPE, hasBuilderOAuthSession, resolveBuilderOAuthRequestAccess, } from "../../server/builder-oauth.js";
|
|
14
14
|
import { hasChatGPTSubscriptionCredential } from "../../server/chatgpt-subscription-oauth.js";
|
|
15
|
-
import { assertCredentialStoreReadable, canUseDeployCredentialFallbackForRequest, getBuilderCredentialAuthFailure, getProviderCredentialAuthFailure, prefetchSecrets, readDeployCredentialEnv, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveSecret, } from "../../server/credential-provider.js";
|
|
15
|
+
import { assertCredentialStoreReadable, canUseDeployCredentialFallbackForRequest, getBuilderCredentialAuthFailure, getProviderCredentialAuthFailure, isTrustedSelfHostedRuntime, prefetchSecrets, readDeployCredentialEnv, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveSecret, } from "../../server/credential-provider.js";
|
|
16
16
|
import { getRequestOrgId, getRequestContext, getRequestUserEmail, } from "../../server/request-context.js";
|
|
17
17
|
import { getSetting } from "../../settings/store.js";
|
|
18
18
|
import { getAgentAppModelDefaultForCurrentRequest } from "../app-model-defaults.js";
|
|
@@ -625,11 +625,13 @@ async function resolveProviderBaseUrl(envVar) {
|
|
|
625
625
|
const deployValue = canUseDeployCredentialFallbackForRequest(envVar)
|
|
626
626
|
? readDeployCredentialEnv(envVar)
|
|
627
627
|
: undefined;
|
|
628
|
+
const isOllama = envVar === OLLAMA_BASE_URL_ENV_VAR;
|
|
628
629
|
if (!raw) {
|
|
629
630
|
if (!deployValue)
|
|
630
631
|
return undefined;
|
|
631
632
|
return validateProviderBaseUrl(deployValue, {
|
|
632
633
|
allowPrivate: true,
|
|
634
|
+
isOllama,
|
|
633
635
|
});
|
|
634
636
|
}
|
|
635
637
|
return raw
|
|
@@ -639,8 +641,8 @@ async function resolveProviderBaseUrl(envVar) {
|
|
|
639
641
|
// allowance as the explicit deploy-only branch above without extending
|
|
640
642
|
// it to user-, org-, or workspace-scoped endpoint values.
|
|
641
643
|
allowPrivate: deployValue !== undefined && raw === deployValue,
|
|
642
|
-
allowLocalOllama:
|
|
643
|
-
|
|
644
|
+
allowLocalOllama: isOllama && isTrustedSelfHostedRuntime(),
|
|
645
|
+
isOllama,
|
|
644
646
|
})
|
|
645
647
|
: undefined;
|
|
646
648
|
}
|
|
@@ -815,9 +817,10 @@ async function engineCreateConfigForEntry(entry, apiKey, extra, credentialResolu
|
|
|
815
817
|
}
|
|
816
818
|
if (entry.name === "ai-sdk:openai" || entry.name === "ai-sdk:ollama") {
|
|
817
819
|
if (typeof safeExtra.baseURL === "string" && safeExtra.baseUrl == null) {
|
|
820
|
+
const isOllama = entry.name === "ai-sdk:ollama";
|
|
818
821
|
safeExtra.baseUrl = await validateProviderBaseUrl(safeExtra.baseURL, {
|
|
819
|
-
allowLocalOllama:
|
|
820
|
-
|
|
822
|
+
allowLocalOllama: isOllama && isTrustedSelfHostedRuntime(),
|
|
823
|
+
isOllama,
|
|
821
824
|
});
|
|
822
825
|
}
|
|
823
826
|
if (safeExtra.baseUrl == null) {
|
package/dist/cli/skills.d.ts
CHANGED
|
@@ -189,6 +189,7 @@ export type SkillsCatalogMode = "agent-native" | "all";
|
|
|
189
189
|
export interface PublicSkillCatalogEntry {
|
|
190
190
|
name: string;
|
|
191
191
|
description?: string;
|
|
192
|
+
installerGroup?: string;
|
|
192
193
|
}
|
|
193
194
|
interface ConnectSpinner {
|
|
194
195
|
start(message?: string): void;
|
|
@@ -263,7 +264,9 @@ interface SkillsClientPromptContext {
|
|
|
263
264
|
installsMcp: boolean;
|
|
264
265
|
}
|
|
265
266
|
interface SkillsTargetPromptContext {
|
|
267
|
+
message?: string;
|
|
266
268
|
initialTargets: string[];
|
|
269
|
+
required?: boolean;
|
|
267
270
|
options: Array<{
|
|
268
271
|
value: string;
|
|
269
272
|
label: string;
|
package/dist/cli/skills.js
CHANGED
|
@@ -1808,6 +1808,7 @@ function publicSkillEntries(options) {
|
|
|
1808
1808
|
.map((entry) => ({
|
|
1809
1809
|
name: entry.name.trim().toLowerCase(),
|
|
1810
1810
|
description: entry.description,
|
|
1811
|
+
installerGroup: entry.installerGroup?.trim().toLowerCase(),
|
|
1811
1812
|
}))
|
|
1812
1813
|
.filter((entry) => {
|
|
1813
1814
|
if (!entry.name || isKnownSkill(entry.name) || seen.has(entry.name)) {
|
|
@@ -1822,11 +1823,16 @@ function publicSkillNames(options) {
|
|
|
1822
1823
|
return new Set(publicSkillEntries(options).map((entry) => entry.name));
|
|
1823
1824
|
}
|
|
1824
1825
|
function publicSkillPromptOptions(options) {
|
|
1825
|
-
return publicSkillEntries(options).map((entry) =>
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1826
|
+
return publicSkillEntries(options).map((entry) => {
|
|
1827
|
+
const description = entry.description ?? "Public skill from the BuilderIO skills catalog.";
|
|
1828
|
+
return {
|
|
1829
|
+
value: entry.name,
|
|
1830
|
+
label: entry.name,
|
|
1831
|
+
hint: entry.installerGroup === entry.name
|
|
1832
|
+
? `${description} Select to choose the full group or a subset.`
|
|
1833
|
+
: description,
|
|
1834
|
+
};
|
|
1835
|
+
});
|
|
1830
1836
|
}
|
|
1831
1837
|
function skillPromptOptions(options = {}) {
|
|
1832
1838
|
return [
|
|
@@ -1999,11 +2005,12 @@ async function promptForUpdateInstructions() {
|
|
|
1999
2005
|
async function promptForSkills(context) {
|
|
2000
2006
|
const clack = await import("@clack/prompts");
|
|
2001
2007
|
const result = await clack.multiselect({
|
|
2002
|
-
message:
|
|
2003
|
-
"
|
|
2008
|
+
message: context.message ??
|
|
2009
|
+
"Which Agent-Native skills do you want to install?\n" +
|
|
2010
|
+
" (space toggles, enter confirms)",
|
|
2004
2011
|
options: context.options,
|
|
2005
2012
|
initialValues: context.initialTargets,
|
|
2006
|
-
required: true,
|
|
2013
|
+
required: context.required ?? true,
|
|
2007
2014
|
});
|
|
2008
2015
|
if (clack.isCancel(result)) {
|
|
2009
2016
|
clack.cancel("Cancelled.");
|
|
@@ -2149,7 +2156,36 @@ async function resolveSkillTargets(parsed, options) {
|
|
|
2149
2156
|
});
|
|
2150
2157
|
if (!selected || selected.length === 0)
|
|
2151
2158
|
return null;
|
|
2152
|
-
|
|
2159
|
+
const publicEntries = publicSkillEntries(options);
|
|
2160
|
+
const selectedTargets = new Set(selected);
|
|
2161
|
+
for (const group of publicEntries.filter((entry) => entry.installerGroup === entry.name && selected.includes(entry.name))) {
|
|
2162
|
+
const members = publicEntries.filter((entry) => entry.installerGroup === group.name && entry.name !== group.name);
|
|
2163
|
+
if (members.length === 0)
|
|
2164
|
+
continue;
|
|
2165
|
+
const groupLabel = group.name.replace(/^./, (character) => character.toUpperCase());
|
|
2166
|
+
const groupSelection = await prompt({
|
|
2167
|
+
message: `Which ${groupLabel} skills do you want to install?\n` +
|
|
2168
|
+
" (all selected; deselect any you want to skip)",
|
|
2169
|
+
initialTargets: members.map((entry) => entry.name),
|
|
2170
|
+
required: false,
|
|
2171
|
+
options: members.map((entry) => ({
|
|
2172
|
+
value: entry.name,
|
|
2173
|
+
label: entry.name,
|
|
2174
|
+
hint: entry.description ??
|
|
2175
|
+
"Public skill from the BuilderIO skills catalog.",
|
|
2176
|
+
})),
|
|
2177
|
+
});
|
|
2178
|
+
if (!groupSelection)
|
|
2179
|
+
return null;
|
|
2180
|
+
const memberNames = new Set(members.map((entry) => entry.name));
|
|
2181
|
+
for (const memberName of memberNames)
|
|
2182
|
+
selectedTargets.delete(memberName);
|
|
2183
|
+
for (const memberName of groupSelection) {
|
|
2184
|
+
if (memberNames.has(memberName))
|
|
2185
|
+
selectedTargets.add(memberName);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return resolveSelectedSkillTargets([...selectedTargets], options);
|
|
2153
2189
|
}
|
|
2154
2190
|
export function parseSkillsArgs(argv) {
|
|
2155
2191
|
const first = argv[0];
|
|
@@ -53,6 +53,13 @@ export declare function saveAgentEngineApiKey({ provider, key, apiKey, scope, }:
|
|
|
53
53
|
* endpoint URLs. Values are stored server-side in scoped secrets.
|
|
54
54
|
*/
|
|
55
55
|
export declare function saveAgentEngineProviderSettings({ provider, key, apiKey, baseUrl, clearBaseUrl, }: SaveAgentEngineProviderSettingsOptions): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* List the models an Ollama server actually has installed, via its native
|
|
58
|
+
* `/api/tags` endpoint. Pass `baseUrl` to check a server before it's saved
|
|
59
|
+
* (e.g. while the user is still typing the endpoint); omit it to use the
|
|
60
|
+
* saved endpoint. Throws a readable Error if the server can't be reached.
|
|
61
|
+
*/
|
|
62
|
+
export declare function fetchOllamaModels(baseUrl?: string): Promise<string[]>;
|
|
56
63
|
/**
|
|
57
64
|
* Select the provider and model for the next conversation. This is separate
|
|
58
65
|
* from saving credentials so keyless local providers such as Ollama can use
|
|
@@ -201,6 +201,43 @@ export async function saveAgentEngineProviderSettings({ provider, key, apiKey, b
|
|
|
201
201
|
}
|
|
202
202
|
dispatchConfiguredChanged();
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* List the models an Ollama server actually has installed, via its native
|
|
206
|
+
* `/api/tags` endpoint. Pass `baseUrl` to check a server before it's saved
|
|
207
|
+
* (e.g. while the user is still typing the endpoint); omit it to use the
|
|
208
|
+
* saved endpoint. Throws a readable Error if the server can't be reached.
|
|
209
|
+
*/
|
|
210
|
+
export async function fetchOllamaModels(baseUrl) {
|
|
211
|
+
const trimmed = baseUrl?.trim() ?? "";
|
|
212
|
+
const path = trimmed
|
|
213
|
+
? `/_agent-native/agent-engine/ollama-models?baseUrl=${encodeURIComponent(trimmed)}`
|
|
214
|
+
: "/_agent-native/agent-engine/ollama-models";
|
|
215
|
+
const response = await fetch(agentNativePath(path), {
|
|
216
|
+
credentials: "include",
|
|
217
|
+
});
|
|
218
|
+
const text = await response.text();
|
|
219
|
+
let payload;
|
|
220
|
+
try {
|
|
221
|
+
payload = text.trim() ? JSON.parse(text) : undefined;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error(`Could not read the Ollama models response (HTTP ${response.status}).`);
|
|
225
|
+
}
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
const message = payload &&
|
|
228
|
+
typeof payload === "object" &&
|
|
229
|
+
typeof payload.error === "string"
|
|
230
|
+
? payload.error
|
|
231
|
+
: `Could not list Ollama models (HTTP ${response.status}).`;
|
|
232
|
+
throw new Error(message);
|
|
233
|
+
}
|
|
234
|
+
const models = payload && typeof payload === "object"
|
|
235
|
+
? payload.models
|
|
236
|
+
: undefined;
|
|
237
|
+
return Array.isArray(models)
|
|
238
|
+
? models.filter((model) => typeof model === "string")
|
|
239
|
+
: [];
|
|
240
|
+
}
|
|
204
241
|
/**
|
|
205
242
|
* Select the provider and model for the next conversation. This is separate
|
|
206
243
|
* from saving credentials so keyless local providers such as Ollama can use
|
|
@@ -72,7 +72,7 @@ export const AGENT_PROVIDER_CATALOG = PROVIDER_ORDER.map((id) => {
|
|
|
72
72
|
? {
|
|
73
73
|
supportsEndpoint: true,
|
|
74
74
|
endpointPlaceholder: isOllama
|
|
75
|
-
? "http://localhost:11434"
|
|
75
|
+
? "http://localhost:11434 or local network address like http://192.168.1.123:11434"
|
|
76
76
|
: "https://gateway.example/v1",
|
|
77
77
|
}
|
|
78
78
|
: {}),
|