@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import type { FetchLike } from "../fetch-like.js";
|
|
20
20
|
import {
|
|
21
21
|
getPluginCatalog,
|
|
22
|
+
getPluginCatalogForInstalledMetadata,
|
|
22
23
|
invalidatePluginCatalogCache,
|
|
23
24
|
mergePlatformCatalogWithBundledLocals,
|
|
24
25
|
PLUGIN_CATALOG_CACHE_TTL_MS,
|
|
@@ -102,7 +103,7 @@ describe("getPluginCatalog", () => {
|
|
|
102
103
|
|
|
103
104
|
// THEN the platform is fetched exactly once — the second call is cached
|
|
104
105
|
expect(calls()).toBe(1);
|
|
105
|
-
expect(first).
|
|
106
|
+
expect(first).toEqual(second);
|
|
106
107
|
expect(githubNames(first)).toEqual(["a"]);
|
|
107
108
|
expect(first.ref).toBe("main");
|
|
108
109
|
});
|
|
@@ -161,7 +162,10 @@ describe("getPluginCatalog", () => {
|
|
|
161
162
|
delete process.env.IS_PLATFORM;
|
|
162
163
|
|
|
163
164
|
const { fetch, calls } = platformFetch(["ignored"]);
|
|
164
|
-
const deps: SearchPluginsDeps = {
|
|
165
|
+
const deps: SearchPluginsDeps = {
|
|
166
|
+
fetch,
|
|
167
|
+
featureFlagEnabled: () => true,
|
|
168
|
+
};
|
|
165
169
|
|
|
166
170
|
// WHEN we request the catalog
|
|
167
171
|
const result = await getPluginCatalog("main", deps);
|
|
@@ -173,6 +177,32 @@ describe("getPluginCatalog", () => {
|
|
|
173
177
|
// The requested ref is echoed onto the wire contract.
|
|
174
178
|
expect(result.ref).toBe("main");
|
|
175
179
|
});
|
|
180
|
+
|
|
181
|
+
test("applies feature-flag visibility to bundled catalog entries", async () => {
|
|
182
|
+
process.env.VELLUM_DISABLE_PLATFORM = "true";
|
|
183
|
+
delete process.env.IS_PLATFORM;
|
|
184
|
+
|
|
185
|
+
const { fetch } = platformFetch(["ignored"]);
|
|
186
|
+
const result = await getPluginCatalog("main", {
|
|
187
|
+
fetch,
|
|
188
|
+
featureFlagEnabled: () => false,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(result.matches.map((match) => match.name)).not.toContain("gamma");
|
|
192
|
+
expect(result.matches.map((match) => match.name)).toContain("fathom");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("preserves hidden entries for installed-plugin metadata", async () => {
|
|
196
|
+
process.env.VELLUM_DISABLE_PLATFORM = "true";
|
|
197
|
+
delete process.env.IS_PLATFORM;
|
|
198
|
+
|
|
199
|
+
const { fetch } = platformFetch(["ignored"]);
|
|
200
|
+
const result = await getPluginCatalogForInstalledMetadata("main", {
|
|
201
|
+
fetch,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result.matches.map((match) => match.name)).toContain("gamma");
|
|
205
|
+
});
|
|
176
206
|
});
|
|
177
207
|
|
|
178
208
|
describe("mergePlatformCatalogWithBundledLocals", () => {
|
|
@@ -228,4 +258,64 @@ describe("mergePlatformCatalogWithBundledLocals", () => {
|
|
|
228
258
|
expect(merged.matches[1]?.source.kind).toBe("local");
|
|
229
259
|
expect(merged.ref).toBe("main");
|
|
230
260
|
});
|
|
261
|
+
test("keeps the bundled integration when a platform row without one shadows it", () => {
|
|
262
|
+
const integration = {
|
|
263
|
+
kind: "mcp",
|
|
264
|
+
displayName: "Fathom",
|
|
265
|
+
documentationUrl: "https://example.com/docs",
|
|
266
|
+
verifiedAt: "2026-09-10",
|
|
267
|
+
verification: "documentation-only",
|
|
268
|
+
setup: { mode: "oauth", instructions: "Sign in." },
|
|
269
|
+
logo: "fathom.png",
|
|
270
|
+
} as const;
|
|
271
|
+
const platformIntegration = { ...integration, displayName: "Platform" };
|
|
272
|
+
const platform: PluginCatalog = {
|
|
273
|
+
ref: "main",
|
|
274
|
+
matches: [
|
|
275
|
+
{
|
|
276
|
+
name: "fathom",
|
|
277
|
+
path: "github:provider/fathom@pin",
|
|
278
|
+
category: null,
|
|
279
|
+
source: {
|
|
280
|
+
kind: "github",
|
|
281
|
+
repo: "provider/fathom",
|
|
282
|
+
ref: "0".repeat(40),
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "linear",
|
|
287
|
+
path: "github:provider/linear@pin",
|
|
288
|
+
category: null,
|
|
289
|
+
integration: platformIntegration,
|
|
290
|
+
source: {
|
|
291
|
+
kind: "github",
|
|
292
|
+
repo: "provider/linear",
|
|
293
|
+
ref: "0".repeat(40),
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
const bundled: PluginCatalog = {
|
|
299
|
+
ref: "bundled",
|
|
300
|
+
matches: ["fathom", "linear"].map((name) => ({
|
|
301
|
+
name,
|
|
302
|
+
path: `local:plugins/mcp-catalog/${name}@1.0.0`,
|
|
303
|
+
category: null,
|
|
304
|
+
integration,
|
|
305
|
+
source: {
|
|
306
|
+
kind: "local",
|
|
307
|
+
path: `plugins/mcp-catalog/${name}`,
|
|
308
|
+
version: "1.0.0",
|
|
309
|
+
},
|
|
310
|
+
})),
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const merged = mergePlatformCatalogWithBundledLocals(platform, bundled);
|
|
314
|
+
|
|
315
|
+
const [fathom, linear] = merged.matches;
|
|
316
|
+
expect(fathom?.source.kind).toBe("github");
|
|
317
|
+
expect(fathom?.integration).toEqual(integration);
|
|
318
|
+
expect(linear?.source.kind).toBe("github");
|
|
319
|
+
expect(linear?.integration).toEqual(platformIntegration);
|
|
320
|
+
});
|
|
231
321
|
});
|
|
@@ -13,6 +13,7 @@ import bundledManifest from "../bundled-marketplace.json" with { type: "json" };
|
|
|
13
13
|
import {
|
|
14
14
|
buildBundledPluginCatalog,
|
|
15
15
|
readBundledPluginCatalog,
|
|
16
|
+
resolveBundledPluginSource,
|
|
16
17
|
} from "../plugin-catalog-local.js";
|
|
17
18
|
import { marketplaceManifestSchema } from "../plugin-marketplace.js";
|
|
18
19
|
import { marketplaceMatch } from "../search-plugins.js";
|
|
@@ -96,3 +97,17 @@ describe("readBundledPluginCatalog", () => {
|
|
|
96
97
|
expect(readBundledPluginCatalog()).toBe(readBundledPluginCatalog());
|
|
97
98
|
});
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
describe("resolveBundledPluginSource", () => {
|
|
102
|
+
test("hides a QA integration while its feature flag is off", () => {
|
|
103
|
+
expect(resolveBundledPluginSource("gamma", () => false)).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("resolves a QA integration while its feature flag is on", () => {
|
|
107
|
+
expect(resolveBundledPluginSource("gamma", () => true)).toEqual({
|
|
108
|
+
kind: "local",
|
|
109
|
+
path: "plugins/mcp-catalog/gamma",
|
|
110
|
+
version: "1.0.2",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -161,14 +161,78 @@ describe("fetchPluginCatalogFromPlatform", () => {
|
|
|
161
161
|
await expect(promise).rejects.toMatchObject({ status: 502 });
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
test(
|
|
164
|
+
test.each([
|
|
165
|
+
["a missing plugins array", {}],
|
|
166
|
+
["a non-array plugins field", { plugins: "nope" }],
|
|
167
|
+
["a non-object body", []],
|
|
168
|
+
])("throws on an invalid envelope (%s)", async (_label, body) => {
|
|
165
169
|
const promise = fetchPluginCatalogFromPlatform({
|
|
166
|
-
fetch: platformFetch(
|
|
170
|
+
fetch: platformFetch(body),
|
|
167
171
|
});
|
|
168
172
|
await expect(promise).rejects.toBeInstanceOf(PluginCatalogUnavailableError);
|
|
169
173
|
await expect(promise).rejects.toMatchObject({ status: 502 });
|
|
170
174
|
});
|
|
171
175
|
|
|
176
|
+
test("skips rows that fail the base row schema and keeps the rest", async () => {
|
|
177
|
+
const catalog = await fetchPluginCatalogFromPlatform({
|
|
178
|
+
fetch: platformFetch({
|
|
179
|
+
plugins: [
|
|
180
|
+
{ repo: "acme/nameless", ref: SHA_A },
|
|
181
|
+
{ name: 42, repo: "acme/numeric", ref: SHA_A },
|
|
182
|
+
"not-a-row",
|
|
183
|
+
{ name: "good", repo: "acme/good", ref: SHA_B },
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
expect(catalog.matches.map((m) => m.name)).toEqual(["good"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("keeps a row whose integration is invalid, without the integration", async () => {
|
|
191
|
+
const catalog = await fetchPluginCatalogFromPlatform({
|
|
192
|
+
fetch: platformFetch({
|
|
193
|
+
plugins: [
|
|
194
|
+
{
|
|
195
|
+
name: "snake-case",
|
|
196
|
+
repo: "acme/snake-case",
|
|
197
|
+
ref: SHA_A,
|
|
198
|
+
description: "kept",
|
|
199
|
+
integration: {
|
|
200
|
+
kind: "mcp",
|
|
201
|
+
display_name: "Snake Case",
|
|
202
|
+
documentation_url: "https://example.com/docs",
|
|
203
|
+
verified_at: "2026-09-10",
|
|
204
|
+
verification: "documentation-only",
|
|
205
|
+
setup: { mode: "oauth", instructions: "Sign in." },
|
|
206
|
+
logo: "snake-case.png",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "valid",
|
|
211
|
+
repo: "acme/valid",
|
|
212
|
+
ref: SHA_B,
|
|
213
|
+
integration: {
|
|
214
|
+
kind: "mcp",
|
|
215
|
+
displayName: "Valid",
|
|
216
|
+
documentationUrl: "https://example.com/docs",
|
|
217
|
+
verifiedAt: "2026-09-10",
|
|
218
|
+
verification: "documentation-only",
|
|
219
|
+
setup: { mode: "manual", instructions: "Paste a key." },
|
|
220
|
+
logo: "valid.png",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(catalog.matches.map((m) => m.name)).toEqual(["snake-case", "valid"]);
|
|
228
|
+
expect(catalog.matches[0]).toMatchObject({
|
|
229
|
+
name: "snake-case",
|
|
230
|
+
description: "kept",
|
|
231
|
+
});
|
|
232
|
+
expect(catalog.matches[0]?.integration).toBeUndefined();
|
|
233
|
+
expect(catalog.matches[1]?.integration?.displayName).toBe("Valid");
|
|
234
|
+
});
|
|
235
|
+
|
|
172
236
|
test("throws when fetch rejects (network error / abort)", async () => {
|
|
173
237
|
const promise = fetchPluginCatalogFromPlatform({
|
|
174
238
|
fetch: (async () => {
|
|
@@ -195,6 +195,27 @@ describe("catalog-backed resolvers (bundled, offline)", () => {
|
|
|
195
195
|
await resolvePluginSourceFromCatalog("no-such-plugin", rejectingDeps),
|
|
196
196
|
).toBeNull();
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
test("does not resolve a QA integration while its feature flag is off", async () => {
|
|
200
|
+
expect(
|
|
201
|
+
await resolvePluginSourceFromCatalog("gamma", {
|
|
202
|
+
...rejectingDeps,
|
|
203
|
+
featureFlagEnabled: () => false,
|
|
204
|
+
}),
|
|
205
|
+
).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("resolves a QA integration while its feature flag is on", async () => {
|
|
209
|
+
expect(
|
|
210
|
+
await resolvePluginSourceFromCatalog("gamma", {
|
|
211
|
+
...rejectingDeps,
|
|
212
|
+
featureFlagEnabled: () => true,
|
|
213
|
+
}),
|
|
214
|
+
).toMatchObject({
|
|
215
|
+
kind: "local",
|
|
216
|
+
path: "plugins/mcp-catalog/gamma",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
198
219
|
});
|
|
199
220
|
|
|
200
221
|
describe("catalog-backed resolvers (platform enabled)", () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
filterPluginCatalogByFeatureFlags,
|
|
5
|
+
MCP_CATALOG_QA_INTEGRATIONS_FLAG,
|
|
6
|
+
} from "../plugin-catalog-visibility.js";
|
|
7
|
+
import type { PluginCatalog, PluginSearchMatch } from "../search-plugins.js";
|
|
8
|
+
|
|
9
|
+
function match(name: string): PluginSearchMatch {
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
path: `local:plugins/mcp-catalog/${name}@1.0.0`,
|
|
13
|
+
category: null,
|
|
14
|
+
source: {
|
|
15
|
+
kind: "local",
|
|
16
|
+
path: `plugins/mcp-catalog/${name}`,
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function catalog(names: string[]): PluginCatalog {
|
|
23
|
+
return { ref: "main", matches: names.map(match) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("filterPluginCatalogByFeatureFlags", () => {
|
|
27
|
+
test("hides QA integrations while the flag is off", () => {
|
|
28
|
+
const input = catalog([
|
|
29
|
+
"fathom",
|
|
30
|
+
"gamma",
|
|
31
|
+
"guru",
|
|
32
|
+
"intercom",
|
|
33
|
+
"ramp",
|
|
34
|
+
"semrush",
|
|
35
|
+
"typeform",
|
|
36
|
+
"notion",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const result = filterPluginCatalogByFeatureFlags(input, (key) => {
|
|
40
|
+
expect(key).toBe(MCP_CATALOG_QA_INTEGRATIONS_FLAG);
|
|
41
|
+
return false;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result.matches.map((entry) => entry.name)).toEqual([
|
|
45
|
+
"fathom",
|
|
46
|
+
"notion",
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns the full catalog while the flag is on", () => {
|
|
51
|
+
const input = catalog([
|
|
52
|
+
"fathom",
|
|
53
|
+
"gamma",
|
|
54
|
+
"guru",
|
|
55
|
+
"intercom",
|
|
56
|
+
"ramp",
|
|
57
|
+
"semrush",
|
|
58
|
+
"typeform",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const result = filterPluginCatalogByFeatureFlags(input, () => true);
|
|
62
|
+
|
|
63
|
+
expect(result).toBe(input);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -513,6 +513,49 @@ describe("getPluginDetails (bundled catalog, offline)", () => {
|
|
|
513
513
|
expect(details.ref).toBe(historicalRef);
|
|
514
514
|
});
|
|
515
515
|
|
|
516
|
+
test("hides a gated integration at an explicit historical ref", async () => {
|
|
517
|
+
const historicalRef = "a".repeat(40);
|
|
518
|
+
let fetchCalls = 0;
|
|
519
|
+
const fetch = (async () => {
|
|
520
|
+
fetchCalls += 1;
|
|
521
|
+
return new Response("unexpected request", { status: 500 });
|
|
522
|
+
}) as FetchLike;
|
|
523
|
+
|
|
524
|
+
await expect(
|
|
525
|
+
getPluginDetails(
|
|
526
|
+
{ name: "gamma", ref: historicalRef },
|
|
527
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
528
|
+
),
|
|
529
|
+
).rejects.toBeInstanceOf(PluginDetailsNotFoundError);
|
|
530
|
+
expect(fetchCalls).toBe(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("keeps an installed gated integration visible at an explicit historical ref", async () => {
|
|
534
|
+
const target = join(workspace, "gamma");
|
|
535
|
+
mkdirSync(target, { recursive: true });
|
|
536
|
+
writeFileSync(join(target, "README.md"), "# Installed Gamma");
|
|
537
|
+
writeFileSync(
|
|
538
|
+
join(target, "package.json"),
|
|
539
|
+
JSON.stringify({ version: "1.0.0", description: "local gamma" }),
|
|
540
|
+
);
|
|
541
|
+
let fetchCalls = 0;
|
|
542
|
+
const fetch = (async () => {
|
|
543
|
+
fetchCalls += 1;
|
|
544
|
+
return new Response("unexpected request", { status: 500 });
|
|
545
|
+
}) as FetchLike;
|
|
546
|
+
|
|
547
|
+
const details = await getPluginDetails(
|
|
548
|
+
{ name: "gamma", ref: "a".repeat(40) },
|
|
549
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
expect(details.installed).toBe(true);
|
|
553
|
+
expect(details.readme).toBe("# Installed Gamma");
|
|
554
|
+
expect(details.description).toBe("local gamma");
|
|
555
|
+
expect(details.source).toBeNull();
|
|
556
|
+
expect(fetchCalls).toBe(0);
|
|
557
|
+
});
|
|
558
|
+
|
|
516
559
|
test("degrades gracefully when the historical marketplace fetch fails", async () => {
|
|
517
560
|
// GIVEN an installed copy and a historical ref whose marketplace fetch errors
|
|
518
561
|
const historicalRef = "a".repeat(40);
|
|
@@ -172,6 +172,17 @@ describe("plugins install by name — disable-platform mode", () => {
|
|
|
172
172
|
expect(message).toContain("not in the bundled marketplace catalog");
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test("does not install a feature-gated bundled integration", async () => {
|
|
176
|
+
process.env.VELLUM_DISABLE_PLATFORM = "true";
|
|
177
|
+
delete process.env.IS_PLATFORM;
|
|
178
|
+
|
|
179
|
+
await runInstall("gamma");
|
|
180
|
+
|
|
181
|
+
expect(installPluginCalls).toHaveLength(0);
|
|
182
|
+
expect(platformInstallCalls).toHaveLength(0);
|
|
183
|
+
expect(process.exitCode).toBe(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
175
186
|
test("uses the platform install endpoint when platform features are enabled", async () => {
|
|
176
187
|
delete process.env.VELLUM_DISABLE_PLATFORM;
|
|
177
188
|
delete process.env.IS_PLATFORM;
|
|
@@ -184,6 +195,21 @@ describe("plugins install by name — disable-platform mode", () => {
|
|
|
184
195
|
expect(process.exitCode).not.toBe(1);
|
|
185
196
|
});
|
|
186
197
|
|
|
198
|
+
test("does not install a name absent from a successful platform catalog", async () => {
|
|
199
|
+
delete process.env.VELLUM_DISABLE_PLATFORM;
|
|
200
|
+
delete process.env.IS_PLATFORM;
|
|
201
|
+
const errSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
202
|
+
|
|
203
|
+
await runInstall("gamma");
|
|
204
|
+
|
|
205
|
+
expect(installPluginCalls).toHaveLength(0);
|
|
206
|
+
expect(platformInstallCalls).toHaveLength(0);
|
|
207
|
+
expect(process.exitCode).toBe(1);
|
|
208
|
+
expect(String(errSpy.mock.calls[0]?.[0])).toContain(
|
|
209
|
+
"not in the marketplace catalog",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
187
213
|
test("installs a platform-visible local package without the platform tar endpoint", async () => {
|
|
188
214
|
delete process.env.VELLUM_DISABLE_PLATFORM;
|
|
189
215
|
delete process.env.IS_PLATFORM;
|
|
@@ -176,16 +176,16 @@
|
|
|
176
176
|
"icon": "\ud83d\udcbb"
|
|
177
177
|
},
|
|
178
178
|
{
|
|
179
|
-
"name": "ecommerce-
|
|
179
|
+
"name": "ecommerce-order-supporter",
|
|
180
180
|
"description": "Resolves ecommerce Where-Is-My-Order (WISMO) support inquiries, cross-checks real-time carrier tracking milestones, flags delivery exceptions, and stages customer update drafts without helpdesk seat taxes.",
|
|
181
181
|
"category": "commerce",
|
|
182
182
|
"icon": "📦",
|
|
183
183
|
"license": "MIT",
|
|
184
|
-
"homepage": "https://github.com/ZeebBoyBlue/ecommerce-
|
|
184
|
+
"homepage": "https://github.com/ZeebBoyBlue/ecommerce-order-supporter",
|
|
185
185
|
"source": {
|
|
186
186
|
"source": "github",
|
|
187
|
-
"repo": "ZeebBoyBlue/ecommerce-
|
|
188
|
-
"ref": "
|
|
187
|
+
"repo": "ZeebBoyBlue/ecommerce-order-supporter",
|
|
188
|
+
"ref": "35e2165a14447a897aee750a198c3f166458226b"
|
|
189
189
|
}
|
|
190
190
|
},
|
|
191
191
|
{
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
arePlatformFeaturesEnabled,
|
|
32
32
|
readBundledLocalPluginCatalog,
|
|
33
33
|
} from "./plugin-catalog-local.js";
|
|
34
|
+
import { isPluginCatalogEntryVisible } from "./plugin-catalog-visibility.js";
|
|
34
35
|
import { DEFAULT_PLUGIN_REF } from "./plugin-constants.js";
|
|
35
36
|
import {
|
|
36
37
|
compareFingerprint,
|
|
@@ -338,6 +339,10 @@ export async function inspectPlugin(
|
|
|
338
339
|
const local = entry ? readLocal(entry, readInstallMeta(entry.target)) : null;
|
|
339
340
|
const surfaces = entry ? detectPluginSurfaces(entry.target) : null;
|
|
340
341
|
|
|
342
|
+
if (!installed && !isPluginCatalogEntryVisible(name)) {
|
|
343
|
+
throw new PluginInspectNotFoundError(name);
|
|
344
|
+
}
|
|
345
|
+
|
|
341
346
|
let remote: PluginRemoteInfo | null = null;
|
|
342
347
|
let remoteError: string | null = null;
|
|
343
348
|
const installedLocalSource = local?.source?.kind === "local";
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readBundledPluginCatalog,
|
|
15
15
|
} from "./plugin-catalog-local.js";
|
|
16
16
|
import { fetchPluginCatalogFromPlatform } from "./plugin-catalog-platform.js";
|
|
17
|
+
import { filterPluginCatalogByFeatureFlags } from "./plugin-catalog-visibility.js";
|
|
17
18
|
import type { PluginCatalog, SearchPluginsDeps } from "./search-plugins.js";
|
|
18
19
|
|
|
19
20
|
/** How long a fetched catalog is served before a refresh is attempted. */
|
|
@@ -24,18 +25,35 @@ interface CacheEntry {
|
|
|
24
25
|
timestamp: number;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
type PluginCatalogLoadDeps = Pick<SearchPluginsDeps, "fetch">;
|
|
29
|
+
|
|
27
30
|
const cache = new Map<string, CacheEntry>();
|
|
28
31
|
|
|
29
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Add bundled local packages without overriding platform-authoritative rows.
|
|
34
|
+
* A platform row without an integration borrows the bundled row's
|
|
35
|
+
* integration of the same name, so an integration the platform omits or
|
|
36
|
+
* serves malformed never disappears from the catalog.
|
|
37
|
+
*/
|
|
30
38
|
export function mergePlatformCatalogWithBundledLocals(
|
|
31
39
|
platform: PluginCatalog,
|
|
32
40
|
bundledLocal: PluginCatalog = readBundledLocalPluginCatalog(),
|
|
33
41
|
): PluginCatalog {
|
|
42
|
+
const bundledByName = new Map(
|
|
43
|
+
bundledLocal.matches.map((match) => [match.name, match]),
|
|
44
|
+
);
|
|
45
|
+
const platformMatches = platform.matches.map((match) => {
|
|
46
|
+
const bundledIntegration = bundledByName.get(match.name)?.integration;
|
|
47
|
+
if (match.integration || !bundledIntegration) {
|
|
48
|
+
return match;
|
|
49
|
+
}
|
|
50
|
+
return { ...match, integration: bundledIntegration };
|
|
51
|
+
});
|
|
34
52
|
const seen = new Set(platform.matches.map((match) => match.name));
|
|
35
53
|
return {
|
|
36
54
|
ref: platform.ref,
|
|
37
55
|
matches: [
|
|
38
|
-
...
|
|
56
|
+
...platformMatches,
|
|
39
57
|
...bundledLocal.matches.filter((match) => !seen.has(match.name)),
|
|
40
58
|
].sort((a, b) => a.name.localeCompare(b.name)),
|
|
41
59
|
};
|
|
@@ -50,9 +68,9 @@ export function mergePlatformCatalogWithBundledLocals(
|
|
|
50
68
|
* fetch failure propagates so the caller can surface it (e.g. map a rate-limit
|
|
51
69
|
* to 503) — no stale catalog is ever served.
|
|
52
70
|
*/
|
|
53
|
-
|
|
71
|
+
async function loadPluginCatalog(
|
|
54
72
|
ref: string,
|
|
55
|
-
deps:
|
|
73
|
+
deps: PluginCatalogLoadDeps,
|
|
56
74
|
): Promise<PluginCatalog> {
|
|
57
75
|
if (!arePlatformFeaturesEnabled()) {
|
|
58
76
|
return { ...readBundledPluginCatalog(), ref };
|
|
@@ -69,6 +87,25 @@ export async function getPluginCatalog(
|
|
|
69
87
|
return merged;
|
|
70
88
|
}
|
|
71
89
|
|
|
90
|
+
/** Installable catalog with feature-flag visibility applied. */
|
|
91
|
+
export async function getPluginCatalog(
|
|
92
|
+
ref: string,
|
|
93
|
+
deps: SearchPluginsDeps,
|
|
94
|
+
): Promise<PluginCatalog> {
|
|
95
|
+
return filterPluginCatalogByFeatureFlags(
|
|
96
|
+
await loadPluginCatalog(ref, deps),
|
|
97
|
+
deps.featureFlagEnabled,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Category metadata for plugins already installed, including hidden entries. */
|
|
102
|
+
export async function getPluginCatalogForInstalledMetadata(
|
|
103
|
+
ref: string,
|
|
104
|
+
deps: PluginCatalogLoadDeps,
|
|
105
|
+
): Promise<PluginCatalog> {
|
|
106
|
+
return loadPluginCatalog(ref, deps);
|
|
107
|
+
}
|
|
108
|
+
|
|
72
109
|
/** Invalidate the cache (for testing or forced refresh). */
|
|
73
110
|
export function invalidatePluginCatalogCache(): void {
|
|
74
111
|
cache.clear();
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import bundledManifest from "./bundled-marketplace.json" with { type: "json" };
|
|
16
|
+
import {
|
|
17
|
+
isPluginCatalogEntryVisible,
|
|
18
|
+
type PluginCatalogFeatureFlagResolver,
|
|
19
|
+
} from "./plugin-catalog-visibility.js";
|
|
16
20
|
import {
|
|
17
21
|
type MarketplaceEntry,
|
|
18
22
|
marketplaceManifestSchema,
|
|
@@ -27,6 +31,7 @@ import {
|
|
|
27
31
|
// Re-exported so `local`-tagged CLI commands can gate on platform features
|
|
28
32
|
// without importing `platform/` directly (cli/no-daemon-internals allows lib).
|
|
29
33
|
export { arePlatformFeaturesEnabled } from "../../platform/feature-gate.js";
|
|
34
|
+
export { isPluginCatalogEntryVisible };
|
|
30
35
|
|
|
31
36
|
let memoizedEntries: readonly MarketplaceEntry[] | undefined;
|
|
32
37
|
|
|
@@ -80,6 +85,10 @@ export function readBundledLocalPluginCatalog(): PluginCatalog {
|
|
|
80
85
|
*/
|
|
81
86
|
export function resolveBundledPluginSource(
|
|
82
87
|
name: string,
|
|
88
|
+
isFeatureFlagEnabled?: PluginCatalogFeatureFlagResolver,
|
|
83
89
|
): ResolvedPluginSource | null {
|
|
90
|
+
if (!isPluginCatalogEntryVisible(name, isFeatureFlagEnabled)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
84
93
|
return resolveMarketplaceSource(name, bundledEntries());
|
|
85
94
|
}
|
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
* catalog sourced from the platform is indistinguishable from one read off
|
|
8
8
|
* GitHub.
|
|
9
9
|
*
|
|
10
|
-
* Every failure mode (non-2xx, unreachable/aborted, malformed
|
|
11
|
-
* {@link PluginCatalogUnavailableError}
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Every failure mode (non-2xx, unreachable/aborted, malformed envelope)
|
|
11
|
+
* throws {@link PluginCatalogUnavailableError}, so a caller never mistakes an
|
|
12
|
+
* outage for "no plugins". Rows are validated one at a time: a row with an
|
|
13
|
+
* invalid `integration` is kept without it, and a row that fails the base
|
|
14
|
+
* row schema is skipped, so one bad row never blanks the whole catalog.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
|
|
18
19
|
import { getPlatformBaseUrl } from "../../config/env.js";
|
|
20
|
+
import { getLogger } from "../../util/logger.js";
|
|
19
21
|
import {
|
|
20
22
|
type MarketplaceEntry,
|
|
21
23
|
mcpIntegrationSchema,
|
|
@@ -27,9 +29,12 @@ import {
|
|
|
27
29
|
type SearchPluginsDeps,
|
|
28
30
|
} from "./search-plugins.js";
|
|
29
31
|
|
|
32
|
+
const log = getLogger("plugin-catalog");
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* One flattened row from the platform catalog. Unknown keys (`id`,
|
|
32
|
-
* `display_name`) are accepted and dropped
|
|
36
|
+
* `display_name`) are accepted and dropped (zod strips them by default).
|
|
37
|
+
* `integration` is validated separately by {@link integrationSchema}.
|
|
33
38
|
*/
|
|
34
39
|
const platformPluginRowSchema = z.object({
|
|
35
40
|
name: z.string(),
|
|
@@ -41,11 +46,13 @@ const platformPluginRowSchema = z.object({
|
|
|
41
46
|
category: z.string().nullable().optional(),
|
|
42
47
|
homepage: z.string().nullable().optional(),
|
|
43
48
|
license: z.string().nullable().optional(),
|
|
44
|
-
integration:
|
|
49
|
+
integration: z.unknown(),
|
|
45
50
|
});
|
|
46
51
|
|
|
52
|
+
const integrationSchema = mcpIntegrationSchema.nullable().optional();
|
|
53
|
+
|
|
47
54
|
const platformCatalogSchema = z.object({
|
|
48
|
-
plugins: z.array(
|
|
55
|
+
plugins: z.array(z.unknown()),
|
|
49
56
|
});
|
|
50
57
|
|
|
51
58
|
/**
|
|
@@ -99,10 +106,22 @@ export async function fetchPluginCatalogFromPlatform(
|
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
const entries: MarketplaceEntry[] = [];
|
|
102
|
-
|
|
109
|
+
const skippedRows: string[] = [];
|
|
110
|
+
const droppedIntegrations: string[] = [];
|
|
111
|
+
for (const [index, rawRow] of parsed.data.plugins.entries()) {
|
|
112
|
+
const rowResult = platformPluginRowSchema.safeParse(rawRow);
|
|
113
|
+
if (!rowResult.success) {
|
|
114
|
+
skippedRows.push(describeInvalidRow(rawRow, index));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const row = rowResult.data;
|
|
103
118
|
if (!row.repo || !row.ref) {
|
|
104
119
|
continue;
|
|
105
120
|
}
|
|
121
|
+
const integrationResult = integrationSchema.safeParse(row.integration);
|
|
122
|
+
if (!integrationResult.success) {
|
|
123
|
+
droppedIntegrations.push(row.name);
|
|
124
|
+
}
|
|
106
125
|
entries.push({
|
|
107
126
|
name: row.name,
|
|
108
127
|
source: {
|
|
@@ -116,12 +135,32 @@ export async function fetchPluginCatalogFromPlatform(
|
|
|
116
135
|
category: row.category ?? undefined,
|
|
117
136
|
homepage: row.homepage ?? undefined,
|
|
118
137
|
license: row.license ?? undefined,
|
|
119
|
-
integration:
|
|
138
|
+
integration: integrationResult.success
|
|
139
|
+
? (integrationResult.data ?? undefined)
|
|
140
|
+
: undefined,
|
|
120
141
|
});
|
|
121
142
|
}
|
|
122
143
|
|
|
144
|
+
if (skippedRows.length > 0 || droppedIntegrations.length > 0) {
|
|
145
|
+
log.warn(
|
|
146
|
+
{ skippedRows, droppedIntegrations },
|
|
147
|
+
"platform plugin catalog contained invalid rows; skipped rows and dropped integrations",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
123
151
|
return {
|
|
124
152
|
ref: opts?.ref ?? "platform",
|
|
125
153
|
matches: projectMarketplaceEntries(entries),
|
|
126
154
|
};
|
|
127
155
|
}
|
|
156
|
+
|
|
157
|
+
/** Name of an invalid row for logging, or its position when it has none. */
|
|
158
|
+
function describeInvalidRow(row: unknown, index: number): string {
|
|
159
|
+
if (row && typeof row === "object" && "name" in row) {
|
|
160
|
+
const name = (row as { name: unknown }).name;
|
|
161
|
+
if (typeof name === "string" && name.length > 0) {
|
|
162
|
+
return name;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return `#${index}`;
|
|
166
|
+
}
|