@vellumai/assistant 0.12.2-staging.4 → 0.12.2-staging.6
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 -7
- package/openapi.yaml +151 -53
- package/package.json +5 -4
- package/scripts/bundled-plugin-packages.ts +154 -0
- package/scripts/generate-bundled-plugin-packages.ts +29 -0
- package/scripts/test.ts +15 -13
- package/src/__tests__/managed-store.test.ts +121 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +88 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +24 -2
- package/src/calls/__tests__/voice-session-bridge.test.ts +22 -2
- package/src/calls/voice-control-protocol.ts +13 -4
- package/src/calls/voice-session-bridge.ts +27 -6
- package/src/cli/commands/__tests__/plugins.test.ts +66 -0
- package/src/cli/commands/plugins.ts +50 -18
- package/src/cli/lib/__tests__/install-from-github.test.ts +67 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +169 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +57 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +14 -0
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +27 -1
- package/src/cli/lib/__tests__/plugin-details.test.ts +9 -2
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +20 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +43 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +31 -0
- package/src/cli/lib/bundled-plugin-packages.json +4 -0
- package/src/cli/lib/bundled-plugin-packages.ts +87 -0
- package/src/cli/lib/diff-plugin.ts +1 -1
- package/src/cli/lib/inspect-plugin.ts +80 -12
- package/src/cli/lib/install-from-github.ts +133 -76
- package/src/cli/lib/plugin-catalog-cache.ts +22 -3
- package/src/cli/lib/plugin-catalog-local.ts +13 -3
- package/src/cli/lib/plugin-catalog-platform.ts +6 -1
- package/src/cli/lib/plugin-catalog-resolve.ts +20 -0
- package/src/cli/lib/plugin-details.ts +12 -0
- package/src/cli/lib/plugin-marketplace.ts +121 -21
- package/src/cli/lib/plugin-pin-history.ts +5 -2
- package/src/cli/lib/search-plugins.ts +58 -16
- package/src/cli/lib/upgrade-plugin.ts +48 -3
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/daemon/conversation-tool-setup.ts +4 -22
- package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +374 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +658 -3
- package/src/live-voice/__tests__/protocol.test.ts +48 -0
- package/src/live-voice/__tests__/session-controls.test.ts +43 -0
- package/src/live-voice/live-voice-session.ts +784 -64
- package/src/live-voice/protocol.ts +20 -0
- package/src/live-voice/session-controls.ts +61 -3
- package/src/monitoring/plugin-auto-update.ts +6 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +15 -0
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +102 -0
- package/src/runtime/routes/plugins-routes.ts +69 -49
- package/src/skills/managed-store.ts +153 -31
- package/src/tools/skills/find-similar-skills.test.ts +214 -2
- package/src/tools/skills/find-similar-skills.ts +62 -9
- package/src/tools/skills/resolve-execute-invocation.ts +24 -0
- package/src/tools/skills/scaffold-managed.ts +16 -13
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type InstalledPluginInfo,
|
|
28
28
|
readInstalledPlugin,
|
|
29
29
|
} from "./list-installed-plugins.js";
|
|
30
|
+
import { readBundledLocalPluginCatalog } from "./plugin-catalog-local.js";
|
|
30
31
|
import { DEFAULT_PLUGIN_REF } from "./plugin-constants.js";
|
|
31
32
|
import {
|
|
32
33
|
compareFingerprint,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
detectPluginSurfaces,
|
|
41
42
|
type PluginSurfaces,
|
|
42
43
|
} from "./plugin-surfaces.js";
|
|
44
|
+
import type { PluginCatalog, PluginSearchMatch } from "./search-plugins.js";
|
|
43
45
|
|
|
44
46
|
/** Full commit SHA (40 hex SHA-1 or 64 hex SHA-256). */
|
|
45
47
|
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
@@ -101,6 +103,8 @@ export interface PluginLocalInfo {
|
|
|
101
103
|
|
|
102
104
|
/** The marketplace's current pin and advertised metadata for a plugin. */
|
|
103
105
|
export interface PluginRemoteInfo {
|
|
106
|
+
/** Present for a package embedded in the assistant distribution. */
|
|
107
|
+
readonly kind?: "local";
|
|
104
108
|
/** `owner/repo` of the external plugin repository. */
|
|
105
109
|
readonly repo: string;
|
|
106
110
|
/** Repo-relative directory holding the plugin root; `""` = repo root. */
|
|
@@ -119,6 +123,8 @@ export interface PluginRemoteInfo {
|
|
|
119
123
|
readonly category: string | null;
|
|
120
124
|
/** Ref of the canonical repo the marketplace manifest was read from. */
|
|
121
125
|
readonly marketplaceRef: string;
|
|
126
|
+
/** Bundled package version. Present when {@link kind} is `local`. */
|
|
127
|
+
readonly version?: string;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
/** Resolved inspection of a single plugin. */
|
|
@@ -165,6 +171,8 @@ export interface InspectPluginDeps {
|
|
|
165
171
|
readonly fetch: FetchLike;
|
|
166
172
|
/** Override the workspace plugins directory. Falls back to the live workspace. */
|
|
167
173
|
readonly workspacePluginsDir?: string;
|
|
174
|
+
/** Override the embedded local-package catalog for focused tests. */
|
|
175
|
+
readonly localCatalog?: PluginCatalog;
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
function readLocal(
|
|
@@ -177,7 +185,7 @@ function readLocal(
|
|
|
177
185
|
// revision instead of dropping to "unknown".
|
|
178
186
|
const commit =
|
|
179
187
|
manifest?.commit ??
|
|
180
|
-
(manifest && FULL_SHA_RE.test(manifest.source.ref)
|
|
188
|
+
(manifest?.source.kind === "github" && FULL_SHA_RE.test(manifest.source.ref)
|
|
181
189
|
? manifest.source.ref
|
|
182
190
|
: null);
|
|
183
191
|
// Compare the on-disk tree against the install-time baseline, applying the
|
|
@@ -203,6 +211,21 @@ function readRemote(
|
|
|
203
211
|
marketplaceRef: string,
|
|
204
212
|
committedAt: string | null,
|
|
205
213
|
): PluginRemoteInfo {
|
|
214
|
+
if (entry.source.source === "local") {
|
|
215
|
+
return {
|
|
216
|
+
kind: "local",
|
|
217
|
+
repo: "",
|
|
218
|
+
path: entry.source.path,
|
|
219
|
+
commit: entry.source.version,
|
|
220
|
+
committedAt: null,
|
|
221
|
+
description: entry.description ?? null,
|
|
222
|
+
homepage: entry.homepage ?? null,
|
|
223
|
+
license: entry.license ?? null,
|
|
224
|
+
category: entry.category ?? null,
|
|
225
|
+
marketplaceRef,
|
|
226
|
+
version: entry.source.version,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
206
229
|
return {
|
|
207
230
|
repo: entry.source.repo,
|
|
208
231
|
path: entry.source.path ?? "",
|
|
@@ -216,6 +239,28 @@ function readRemote(
|
|
|
216
239
|
};
|
|
217
240
|
}
|
|
218
241
|
|
|
242
|
+
function readLocalPackageRemote(
|
|
243
|
+
match: PluginSearchMatch,
|
|
244
|
+
marketplaceRef: string,
|
|
245
|
+
): PluginRemoteInfo | null {
|
|
246
|
+
if (match.source.kind !== "local") {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
kind: "local",
|
|
251
|
+
repo: "",
|
|
252
|
+
path: match.source.path,
|
|
253
|
+
commit: match.source.version,
|
|
254
|
+
committedAt: null,
|
|
255
|
+
description: match.description ?? null,
|
|
256
|
+
homepage: match.homepage ?? null,
|
|
257
|
+
license: match.license ?? null,
|
|
258
|
+
category: match.category,
|
|
259
|
+
marketplaceRef,
|
|
260
|
+
version: match.source.version,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
219
264
|
/**
|
|
220
265
|
* Resolve the committer date of a pinned commit from GitHub, normalized to a
|
|
221
266
|
* UTC ISO-8601 string so it is directly comparable with the install-time
|
|
@@ -292,19 +337,34 @@ export async function inspectPlugin(
|
|
|
292
337
|
|
|
293
338
|
let remote: PluginRemoteInfo | null = null;
|
|
294
339
|
let remoteError: string | null = null;
|
|
340
|
+
const installedLocalSource = local?.source?.kind === "local";
|
|
295
341
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
match.source.repo,
|
|
304
|
-
match.source.ref,
|
|
305
|
-
deps.fetch,
|
|
342
|
+
if (installedLocalSource) {
|
|
343
|
+
const catalog = deps.localCatalog ?? readBundledLocalPluginCatalog();
|
|
344
|
+
const match = catalog.matches.find(
|
|
345
|
+
(candidate) =>
|
|
346
|
+
candidate.name === name &&
|
|
347
|
+
candidate.source.kind === "local" &&
|
|
348
|
+
candidate.source.path === local.source?.path,
|
|
306
349
|
);
|
|
307
|
-
remote =
|
|
350
|
+
remote = match ? readLocalPackageRemote(match, catalog.ref) : null;
|
|
351
|
+
} else {
|
|
352
|
+
const entries = await fetchMarketplaceEntries(
|
|
353
|
+
{ fetch: deps.fetch },
|
|
354
|
+
{ ref: marketplaceRef },
|
|
355
|
+
);
|
|
356
|
+
const match = entries.find((entry) => entry.name === name);
|
|
357
|
+
if (match) {
|
|
358
|
+
const committedAt =
|
|
359
|
+
match.source.source === "github"
|
|
360
|
+
? await fetchCommitDate(
|
|
361
|
+
match.source.repo,
|
|
362
|
+
match.source.ref,
|
|
363
|
+
deps.fetch,
|
|
364
|
+
)
|
|
365
|
+
: null;
|
|
366
|
+
remote = readRemote(match, marketplaceRef, committedAt);
|
|
367
|
+
}
|
|
308
368
|
}
|
|
309
369
|
} catch (err) {
|
|
310
370
|
remoteError = err instanceof Error ? err.message : String(err);
|
|
@@ -335,6 +395,14 @@ function classify(
|
|
|
335
395
|
if (!remote) {
|
|
336
396
|
return "not-in-marketplace";
|
|
337
397
|
}
|
|
398
|
+
if (remote.kind === "local") {
|
|
399
|
+
if (local?.source?.kind !== "local") {
|
|
400
|
+
return "unknown-provenance";
|
|
401
|
+
}
|
|
402
|
+
return local.source.version === remote.version
|
|
403
|
+
? "up-to-date"
|
|
404
|
+
: "update-available";
|
|
405
|
+
}
|
|
338
406
|
if (!local?.commit) {
|
|
339
407
|
return "unknown-provenance";
|
|
340
408
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Install a plugin by name from
|
|
2
|
+
* Install a reviewed plugin by name from its catalog source.
|
|
3
3
|
*
|
|
4
|
-
* A name resolves to
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `<workspacePluginsDir>/<name>/` so the daemon discovers it on next start.
|
|
4
|
+
* A name resolves to an entry in the curated `plugins/marketplace.json`
|
|
5
|
+
* manifest. A GitHub source is fetched with a shallow clone at its pinned ref.
|
|
6
|
+
* A local source is read from the exact package version embedded in the
|
|
7
|
+
* assistant distribution. Both are materialized into
|
|
8
|
+
* `<workspacePluginsDir>/<name>/` and record their source in the provenance
|
|
9
|
+
* sidecar.
|
|
11
10
|
*
|
|
12
11
|
* When we curate an adapter stub for the plugin (a `plugins/<name>/` directory
|
|
13
12
|
* in this repo with a `scripts.postinstall` command), the stub is overlaid
|
|
@@ -56,6 +55,7 @@ import {
|
|
|
56
55
|
getPluginManifestInstallAction,
|
|
57
56
|
readPluginManifest,
|
|
58
57
|
} from "../../util/plugin-manifest.js";
|
|
58
|
+
import { materializeBundledPluginPackage } from "./bundled-plugin-packages.js";
|
|
59
59
|
import type { FetchLike } from "./fetch-like.js";
|
|
60
60
|
import {
|
|
61
61
|
type DependencyInstaller,
|
|
@@ -151,20 +151,13 @@ export interface InstallPluginOptions {
|
|
|
151
151
|
*/
|
|
152
152
|
readonly directSource?: PluginFetchSource;
|
|
153
153
|
/**
|
|
154
|
-
* Install from
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* adapter stub is kept. `trustedSource` names the EXTERNAL plugin repo, so
|
|
160
|
-
* `trustedSource.ref` (its immutable content commit) cannot address the stub,
|
|
161
|
-
* which lives in this repo; the stub is fetched from the canonical repo at
|
|
162
|
-
* {@link InstallPluginOptions.ref} (default {@link DEFAULT_PLUGIN_REF}) — the
|
|
163
|
-
* bundled catalog records no canonical-repo pin. When set, marketplace
|
|
164
|
-
* resolution and {@link InstallPluginOptions.commitOverride} are skipped;
|
|
165
|
-
* `trustedSource.ref` selects the commit to clone (the reviewed pin).
|
|
154
|
+
* Install from an exact source resolved by the reviewed catalog. GitHub
|
|
155
|
+
* sources retain the curated adapter overlay. Local sources are complete
|
|
156
|
+
* packages embedded in the assistant distribution and do not use an adapter.
|
|
157
|
+
* Marketplace resolution and {@link InstallPluginOptions.commitOverride} are
|
|
158
|
+
* skipped when this is set.
|
|
166
159
|
*/
|
|
167
|
-
readonly trustedSource?:
|
|
160
|
+
readonly trustedSource?: PluginMaterializeSource;
|
|
168
161
|
}
|
|
169
162
|
|
|
170
163
|
/**
|
|
@@ -197,6 +190,8 @@ export interface InstallPluginDeps {
|
|
|
197
190
|
readonly beforeSwap?: () => Promise<void>;
|
|
198
191
|
/** Consent gate between staging and finalize; see {@link ConfirmStagedInstall}. */
|
|
199
192
|
readonly confirmStaged?: ConfirmStagedInstall;
|
|
193
|
+
/** Override embedded-package materialization for focused tests. */
|
|
194
|
+
readonly materializeLocalPackage?: typeof materializeBundledPluginPackage;
|
|
200
195
|
}
|
|
201
196
|
|
|
202
197
|
/** Successful install result. */
|
|
@@ -319,21 +314,39 @@ export interface PluginFetchSource {
|
|
|
319
314
|
readonly ref: string;
|
|
320
315
|
}
|
|
321
316
|
|
|
317
|
+
/** Reviewed package embedded in the assistant distribution. */
|
|
318
|
+
export interface LocalPluginFetchSource {
|
|
319
|
+
readonly kind: "local";
|
|
320
|
+
readonly path: string;
|
|
321
|
+
readonly version: string;
|
|
322
|
+
readonly ref?: undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export type PluginMaterializeSource =
|
|
326
|
+
| PluginFetchSource
|
|
327
|
+
| LocalPluginFetchSource;
|
|
328
|
+
|
|
329
|
+
export function isLocalPluginSource(
|
|
330
|
+
source: PluginMaterializeSource,
|
|
331
|
+
): source is LocalPluginFetchSource {
|
|
332
|
+
return "kind" in source && source.kind === "local";
|
|
333
|
+
}
|
|
334
|
+
|
|
322
335
|
/** Build the `owner/repo/path` label used in not-found errors. */
|
|
323
|
-
function sourceLabel(source:
|
|
336
|
+
function sourceLabel(source: PluginMaterializeSource): string {
|
|
337
|
+
if (isLocalPluginSource(source)) {
|
|
338
|
+
return source.path;
|
|
339
|
+
}
|
|
324
340
|
return source.rootPath
|
|
325
341
|
? `${source.owner}/${source.repo}/${source.rootPath}`
|
|
326
342
|
: `${source.owner}/${source.repo}`;
|
|
327
343
|
}
|
|
328
344
|
|
|
329
345
|
/**
|
|
330
|
-
* Resolve a plugin name to
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* `plugins/<name>` directory is the plugin's optional *adapter stub* (a curated
|
|
335
|
-
* `package.json` + postinstall script overlaid onto the clone to translate it
|
|
336
|
-
* into Vellum's shape; see {@link applyAdapterStub}), not a standalone plugin.
|
|
346
|
+
* Resolve a plugin name to its exact marketplace source, or `null` when no
|
|
347
|
+
* entry claims the name. A GitHub entry may have a same-named adapter stub
|
|
348
|
+
* under `plugins/<name>` that is overlaid onto the clone. A local entry refers
|
|
349
|
+
* to a fully self-contained package embedded in the assistant distribution.
|
|
337
350
|
*
|
|
338
351
|
* A transient marketplace failure (rate-limit / 5xx) surfaces as a retryable
|
|
339
352
|
* {@link PluginSourceUnavailableError}; a malformed manifest propagates as a
|
|
@@ -344,7 +357,7 @@ async function resolvePluginSource(
|
|
|
344
357
|
name: string,
|
|
345
358
|
marketplaceRef: string,
|
|
346
359
|
fetchFn: FetchLike,
|
|
347
|
-
): Promise<
|
|
360
|
+
): Promise<PluginMaterializeSource | null> {
|
|
348
361
|
let resolved: ResolvedPluginSource | null;
|
|
349
362
|
try {
|
|
350
363
|
const entries = await fetchMarketplaceEntries(
|
|
@@ -363,6 +376,14 @@ async function resolvePluginSource(
|
|
|
363
376
|
return null;
|
|
364
377
|
}
|
|
365
378
|
|
|
379
|
+
if (resolved.kind === "local") {
|
|
380
|
+
return {
|
|
381
|
+
kind: "local",
|
|
382
|
+
path: resolved.path,
|
|
383
|
+
version: resolved.version,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
366
387
|
return {
|
|
367
388
|
owner: resolved.owner,
|
|
368
389
|
repo: resolved.repo,
|
|
@@ -460,7 +481,7 @@ export async function installPlugin(
|
|
|
460
481
|
// adapter stub). A trusted pre-resolved source (offline bundled-catalog
|
|
461
482
|
// install) supplies its coordinates too but keeps the curated overlay.
|
|
462
483
|
// Otherwise the name is resolved against the reviewed manifest.
|
|
463
|
-
let effectiveSource:
|
|
484
|
+
let effectiveSource: PluginMaterializeSource;
|
|
464
485
|
// Ref the curated adapter stub is fetched at, or `null` to skip the overlay.
|
|
465
486
|
let stubRef: string | null;
|
|
466
487
|
if (opts.directSource) {
|
|
@@ -474,7 +495,7 @@ export async function installPlugin(
|
|
|
474
495
|
// recorded offline, the stub is fetched at `marketplaceRef` (its default,
|
|
475
496
|
// DEFAULT_PLUGIN_REF).
|
|
476
497
|
effectiveSource = opts.trustedSource;
|
|
477
|
-
stubRef = marketplaceRef;
|
|
498
|
+
stubRef = isLocalPluginSource(effectiveSource) ? null : marketplaceRef;
|
|
478
499
|
} else {
|
|
479
500
|
const source = await resolvePluginSource(name, marketplaceRef, deps.fetch);
|
|
480
501
|
if (!source) {
|
|
@@ -487,12 +508,15 @@ export async function installPlugin(
|
|
|
487
508
|
// A commit override installs a specific plugin revision while still taking
|
|
488
509
|
// owner/repo/path (and the adapter stub, via `marketplaceRef`) from the
|
|
489
510
|
// manifest; otherwise the reviewed pin from the manifest is materialized.
|
|
490
|
-
effectiveSource =
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
511
|
+
effectiveSource =
|
|
512
|
+
opts.commitOverride && !isLocalPluginSource(source)
|
|
513
|
+
? { ...source, ref: opts.commitOverride }
|
|
514
|
+
: source;
|
|
515
|
+
stubRef = isLocalPluginSource(effectiveSource) ? null : marketplaceRef;
|
|
494
516
|
}
|
|
495
|
-
const ref = effectiveSource
|
|
517
|
+
const ref = isLocalPluginSource(effectiveSource)
|
|
518
|
+
? effectiveSource.version
|
|
519
|
+
: effectiveSource.ref;
|
|
496
520
|
|
|
497
521
|
const pluginsDir = deps.workspacePluginsDir ?? getWorkspacePluginsDir();
|
|
498
522
|
const target = join(pluginsDir, name);
|
|
@@ -595,7 +619,7 @@ export async function confirmStagedOrAbort(
|
|
|
595
619
|
export interface FinalizeStagedInstallParams {
|
|
596
620
|
readonly name: string;
|
|
597
621
|
/** Source coordinates recorded in the provenance sidecar. */
|
|
598
|
-
readonly source:
|
|
622
|
+
readonly source: PluginMaterializeSource;
|
|
599
623
|
/** Ref recorded in the sidecar (the resolved commit SHA for marketplace installs). */
|
|
600
624
|
readonly ref: string;
|
|
601
625
|
readonly commit: string | null;
|
|
@@ -735,16 +759,22 @@ export const INSTALL_META_FILENAME = "install-meta.json";
|
|
|
735
759
|
export type InstallOrigin = "vellum";
|
|
736
760
|
|
|
737
761
|
/** Resolved source coordinates recorded in the provenance sidecar. */
|
|
738
|
-
export
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
762
|
+
export type InstallMetaSource =
|
|
763
|
+
| {
|
|
764
|
+
readonly kind: "github";
|
|
765
|
+
readonly owner: string;
|
|
766
|
+
readonly repo: string;
|
|
767
|
+
readonly path?: string;
|
|
768
|
+
readonly ref: string;
|
|
769
|
+
}
|
|
770
|
+
| {
|
|
771
|
+
readonly kind: "local";
|
|
772
|
+
readonly path: string;
|
|
773
|
+
readonly version: string;
|
|
774
|
+
readonly owner?: undefined;
|
|
775
|
+
readonly repo?: undefined;
|
|
776
|
+
readonly ref?: undefined;
|
|
777
|
+
};
|
|
748
778
|
|
|
749
779
|
/**
|
|
750
780
|
* Parsed contents of the `install-meta.json` provenance sidecar — what was
|
|
@@ -839,7 +869,7 @@ export interface MaterializedTree {
|
|
|
839
869
|
export async function materializePluginTree(
|
|
840
870
|
opts: {
|
|
841
871
|
/** Source coordinates; `source.ref` selects the commit to clone. */
|
|
842
|
-
readonly source:
|
|
872
|
+
readonly source: PluginMaterializeSource;
|
|
843
873
|
/** Install name, used to locate the curated adapter stub. */
|
|
844
874
|
readonly name: string;
|
|
845
875
|
/**
|
|
@@ -854,11 +884,19 @@ export async function materializePluginTree(
|
|
|
854
884
|
},
|
|
855
885
|
deps: InstallPluginDeps,
|
|
856
886
|
): Promise<MaterializedTree> {
|
|
857
|
-
const cloned =
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
887
|
+
const cloned = isLocalPluginSource(opts.source)
|
|
888
|
+
? {
|
|
889
|
+
fileCount: (
|
|
890
|
+
deps.materializeLocalPackage ?? materializeBundledPluginPackage
|
|
891
|
+
)(opts.source.path, opts.source.version, opts.destDir),
|
|
892
|
+
commit: null,
|
|
893
|
+
committedAt: null,
|
|
894
|
+
}
|
|
895
|
+
: await copyExternalViaGit(
|
|
896
|
+
opts.source,
|
|
897
|
+
opts.destDir,
|
|
898
|
+
deps.runGit ?? defaultGitRunner,
|
|
899
|
+
);
|
|
862
900
|
// An external clone is often a foreign-ecosystem plugin (e.g. a Claude Code
|
|
863
901
|
// plugin) that the Vellum loader can't run as-is. When we curate an adapter
|
|
864
902
|
// stub for it, overlay the stub and run its transform so the materialized
|
|
@@ -1551,7 +1589,7 @@ export async function resolveTreeRefPath(
|
|
|
1551
1589
|
/** Inputs for {@link writeInstallMeta}, resolved during a fresh install. */
|
|
1552
1590
|
interface WriteInstallMetaParams {
|
|
1553
1591
|
readonly name: string;
|
|
1554
|
-
readonly source:
|
|
1592
|
+
readonly source: PluginMaterializeSource;
|
|
1555
1593
|
readonly ref: string;
|
|
1556
1594
|
readonly commit: string | null;
|
|
1557
1595
|
/** ISO-8601 committer timestamp of {@link WriteInstallMetaParams.commit} (UTC); null when unknown. */
|
|
@@ -1598,21 +1636,24 @@ function writeInstallMeta(
|
|
|
1598
1636
|
contentHash,
|
|
1599
1637
|
}: WriteInstallMetaParams,
|
|
1600
1638
|
): void {
|
|
1639
|
+
const local = isLocalPluginSource(source);
|
|
1601
1640
|
const meta: InstallMeta = {
|
|
1602
1641
|
origin: "vellum",
|
|
1603
1642
|
installedAt: new Date().toISOString(),
|
|
1604
1643
|
version: readStagedManifestVersion(stagingDir),
|
|
1605
|
-
sourceRepo: `${source.owner}/${source.repo}`,
|
|
1644
|
+
sourceRepo: local ? undefined : `${source.owner}/${source.repo}`,
|
|
1606
1645
|
contentHash,
|
|
1607
1646
|
author: "user",
|
|
1608
1647
|
name,
|
|
1609
|
-
source:
|
|
1610
|
-
kind: "
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1648
|
+
source: local
|
|
1649
|
+
? { kind: "local", path: source.path, version: source.version }
|
|
1650
|
+
: {
|
|
1651
|
+
kind: "github",
|
|
1652
|
+
owner: source.owner,
|
|
1653
|
+
repo: source.repo,
|
|
1654
|
+
path: source.rootPath || undefined,
|
|
1655
|
+
ref,
|
|
1656
|
+
},
|
|
1616
1657
|
commit,
|
|
1617
1658
|
committedAt,
|
|
1618
1659
|
...(etag ? { etag } : {}),
|
|
@@ -1656,15 +1697,37 @@ export function readInstallMeta(pluginDir: string): InstallMeta | null {
|
|
|
1656
1697
|
return null;
|
|
1657
1698
|
}
|
|
1658
1699
|
const source = src as Record<string, unknown>;
|
|
1659
|
-
if (
|
|
1660
|
-
typeof obj.name !== "string" ||
|
|
1661
|
-
typeof source.owner !== "string" ||
|
|
1662
|
-
typeof source.repo !== "string" ||
|
|
1663
|
-
typeof source.ref !== "string"
|
|
1664
|
-
) {
|
|
1700
|
+
if (typeof obj.name !== "string") {
|
|
1665
1701
|
return null;
|
|
1666
1702
|
}
|
|
1667
1703
|
|
|
1704
|
+
let parsedSource: InstallMetaSource;
|
|
1705
|
+
if (source.kind === "local") {
|
|
1706
|
+
if (typeof source.path !== "string" || typeof source.version !== "string") {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
parsedSource = {
|
|
1710
|
+
kind: "local",
|
|
1711
|
+
path: source.path,
|
|
1712
|
+
version: source.version,
|
|
1713
|
+
};
|
|
1714
|
+
} else {
|
|
1715
|
+
if (
|
|
1716
|
+
typeof source.owner !== "string" ||
|
|
1717
|
+
typeof source.repo !== "string" ||
|
|
1718
|
+
typeof source.ref !== "string"
|
|
1719
|
+
) {
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
parsedSource = {
|
|
1723
|
+
kind: "github",
|
|
1724
|
+
owner: source.owner,
|
|
1725
|
+
repo: source.repo,
|
|
1726
|
+
path: typeof source.path === "string" ? source.path : undefined,
|
|
1727
|
+
ref: source.ref,
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1668
1731
|
const optionalString = (value: unknown): string | undefined =>
|
|
1669
1732
|
typeof value === "string" ? value : undefined;
|
|
1670
1733
|
|
|
@@ -1682,13 +1745,7 @@ export function readInstallMeta(pluginDir: string): InstallMeta | null {
|
|
|
1682
1745
|
? obj.author
|
|
1683
1746
|
: undefined,
|
|
1684
1747
|
name: obj.name,
|
|
1685
|
-
source:
|
|
1686
|
-
kind: typeof source.kind === "string" ? source.kind : "github",
|
|
1687
|
-
owner: source.owner,
|
|
1688
|
-
repo: source.repo,
|
|
1689
|
-
path: typeof source.path === "string" ? source.path : undefined,
|
|
1690
|
-
ref: source.ref,
|
|
1691
|
-
},
|
|
1748
|
+
source: parsedSource,
|
|
1692
1749
|
commit: typeof obj.commit === "string" ? obj.commit : null,
|
|
1693
1750
|
...(typeof obj.etag === "string" ? { etag: obj.etag } : {}),
|
|
1694
1751
|
committedAt: typeof obj.committedAt === "string" ? obj.committedAt : null,
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { arePlatformFeaturesEnabled } from "../../platform/feature-gate.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
readBundledLocalPluginCatalog,
|
|
14
|
+
readBundledPluginCatalog,
|
|
15
|
+
} from "./plugin-catalog-local.js";
|
|
13
16
|
import { fetchPluginCatalogFromPlatform } from "./plugin-catalog-platform.js";
|
|
14
17
|
import type { PluginCatalog, SearchPluginsDeps } from "./search-plugins.js";
|
|
15
18
|
|
|
@@ -23,6 +26,21 @@ interface CacheEntry {
|
|
|
23
26
|
|
|
24
27
|
const cache = new Map<string, CacheEntry>();
|
|
25
28
|
|
|
29
|
+
/** Add bundled local packages without overriding platform-authoritative rows. */
|
|
30
|
+
export function mergePlatformCatalogWithBundledLocals(
|
|
31
|
+
platform: PluginCatalog,
|
|
32
|
+
bundledLocal: PluginCatalog = readBundledLocalPluginCatalog(),
|
|
33
|
+
): PluginCatalog {
|
|
34
|
+
const seen = new Set(platform.matches.map((match) => match.name));
|
|
35
|
+
return {
|
|
36
|
+
ref: platform.ref,
|
|
37
|
+
matches: [
|
|
38
|
+
...platform.matches,
|
|
39
|
+
...bundledLocal.matches.filter((match) => !seen.has(match.name)),
|
|
40
|
+
].sort((a, b) => a.name.localeCompare(b.name)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
/**
|
|
27
45
|
* Resolve the full plugin catalog at {@link ref}.
|
|
28
46
|
*
|
|
@@ -46,8 +64,9 @@ export async function getPluginCatalog(
|
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
const catalog = await fetchPluginCatalogFromPlatform(deps, { ref });
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
const merged = mergePlatformCatalogWithBundledLocals(catalog);
|
|
68
|
+
cache.set(ref, { catalog: merged, timestamp: Date.now() });
|
|
69
|
+
return merged;
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
/** Invalidate the cache (for testing or forced refresh). */
|
|
@@ -61,10 +61,20 @@ export function readBundledPluginCatalog(): PluginCatalog {
|
|
|
61
61
|
return memoized;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Local packages the current assistant release can materialize itself. */
|
|
65
|
+
export function readBundledLocalPluginCatalog(): PluginCatalog {
|
|
66
|
+
return {
|
|
67
|
+
ref: "bundled",
|
|
68
|
+
matches: projectMarketplaceEntries(
|
|
69
|
+
bundledEntries().filter((entry) => entry.source.source === "local"),
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
/**
|
|
65
|
-
* Resolve an install name to its
|
|
66
|
-
*
|
|
67
|
-
* resolving against the remote-fetched marketplace
|
|
75
|
+
* Resolve an install name to its exact source from the bundled manifest, or
|
|
76
|
+
* `null` when no bundled entry claims the name. The offline analogue of
|
|
77
|
+
* resolving against the remote-fetched marketplace, used when platform
|
|
68
78
|
* features are disabled and `assistant plugins install <name>` must resolve the
|
|
69
79
|
* pin without any network call.
|
|
70
80
|
*/
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
|
|
18
18
|
import { getPlatformBaseUrl } from "../../config/env.js";
|
|
19
|
-
import
|
|
19
|
+
import {
|
|
20
|
+
type MarketplaceEntry,
|
|
21
|
+
mcpIntegrationSchema,
|
|
22
|
+
} from "./plugin-marketplace.js";
|
|
20
23
|
import {
|
|
21
24
|
type PluginCatalog,
|
|
22
25
|
PluginCatalogUnavailableError,
|
|
@@ -38,6 +41,7 @@ const platformPluginRowSchema = z.object({
|
|
|
38
41
|
category: z.string().nullable().optional(),
|
|
39
42
|
homepage: z.string().nullable().optional(),
|
|
40
43
|
license: z.string().nullable().optional(),
|
|
44
|
+
integration: mcpIntegrationSchema.nullable().optional(),
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
const platformCatalogSchema = z.object({
|
|
@@ -112,6 +116,7 @@ export async function fetchPluginCatalogFromPlatform(
|
|
|
112
116
|
category: row.category ?? undefined,
|
|
113
117
|
homepage: row.homepage ?? undefined,
|
|
114
118
|
license: row.license ?? undefined,
|
|
119
|
+
integration: row.integration ?? undefined,
|
|
115
120
|
});
|
|
116
121
|
}
|
|
117
122
|
|
|
@@ -12,6 +12,7 @@ import { getPluginCatalog } from "./plugin-catalog-cache.js";
|
|
|
12
12
|
import { DEFAULT_PLUGIN_REF } from "./plugin-constants.js";
|
|
13
13
|
import {
|
|
14
14
|
githubSourceSchema,
|
|
15
|
+
localSourceSchema,
|
|
15
16
|
type ResolvedPluginSource,
|
|
16
17
|
} from "./plugin-marketplace.js";
|
|
17
18
|
import type { PluginSearchMatch, SearchPluginsDeps } from "./search-plugins.js";
|
|
@@ -40,6 +41,24 @@ export async function findCatalogEntry(
|
|
|
40
41
|
export function resolveSourceFromMatch(
|
|
41
42
|
match: PluginSearchMatch,
|
|
42
43
|
): ResolvedPluginSource {
|
|
44
|
+
if (match.source.kind === "local") {
|
|
45
|
+
const parsed = localSourceSchema.safeParse({
|
|
46
|
+
source: "local",
|
|
47
|
+
path: match.source.path,
|
|
48
|
+
version: match.source.version,
|
|
49
|
+
});
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Catalog entry "${match.name}" (${match.source.path}) has an invalid source: ` +
|
|
53
|
+
parsed.error.issues.map((issue) => issue.message).join("; "),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
kind: "local",
|
|
58
|
+
path: parsed.data.path,
|
|
59
|
+
version: parsed.data.version,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
43
62
|
// Repo-root `""` maps to `undefined` (omitted = root) so the schema's
|
|
44
63
|
// non-empty path refine does not reject a valid repo-root entry.
|
|
45
64
|
const path = match.source.path || undefined;
|
|
@@ -57,6 +76,7 @@ export function resolveSourceFromMatch(
|
|
|
57
76
|
}
|
|
58
77
|
const [owner, repoName] = parsed.data.repo.split("/", 2) as [string, string];
|
|
59
78
|
return {
|
|
79
|
+
kind: "github",
|
|
60
80
|
owner,
|
|
61
81
|
repo: repoName,
|
|
62
82
|
path: match.source.path ?? "",
|
|
@@ -34,6 +34,7 @@ import { join } from "node:path";
|
|
|
34
34
|
|
|
35
35
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
36
36
|
import { readPluginManifest } from "../../util/plugin-manifest.js";
|
|
37
|
+
import { readBundledPluginFile } from "./bundled-plugin-packages.js";
|
|
37
38
|
import type { FetchLike } from "./fetch-like.js";
|
|
38
39
|
import { sanitizePluginName } from "./install-from-github.js";
|
|
39
40
|
import {
|
|
@@ -300,6 +301,17 @@ async function readRemotePlugin(
|
|
|
300
301
|
source: PluginMatchSource,
|
|
301
302
|
fetchFn: FetchLike,
|
|
302
303
|
): Promise<RemotePlugin> {
|
|
304
|
+
if (source.kind === "local") {
|
|
305
|
+
const manifest = readBundledPluginFile(
|
|
306
|
+
source.path,
|
|
307
|
+
source.version,
|
|
308
|
+
"plugin.json",
|
|
309
|
+
);
|
|
310
|
+
return {
|
|
311
|
+
manifest: manifest ? safeParseManifest(manifest) : emptyManifest(),
|
|
312
|
+
readme: null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
303
315
|
const [owner, repo] = source.repo.split("/", 2) as [string, string];
|
|
304
316
|
const entries = await listDirSafe(
|
|
305
317
|
owner,
|