@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.
Files changed (57) hide show
  1. package/Dockerfile +7 -7
  2. package/openapi.yaml +151 -53
  3. package/package.json +5 -4
  4. package/scripts/bundled-plugin-packages.ts +154 -0
  5. package/scripts/generate-bundled-plugin-packages.ts +29 -0
  6. package/scripts/test.ts +15 -13
  7. package/src/__tests__/managed-store.test.ts +121 -0
  8. package/src/__tests__/scaffold-managed-skill-tool.test.ts +88 -0
  9. package/src/calls/__tests__/voice-control-protocol.test.ts +24 -2
  10. package/src/calls/__tests__/voice-session-bridge.test.ts +22 -2
  11. package/src/calls/voice-control-protocol.ts +13 -4
  12. package/src/calls/voice-session-bridge.ts +27 -6
  13. package/src/cli/commands/__tests__/plugins.test.ts +66 -0
  14. package/src/cli/commands/plugins.ts +50 -18
  15. package/src/cli/lib/__tests__/install-from-github.test.ts +67 -0
  16. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +169 -0
  17. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +57 -0
  18. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +14 -0
  19. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +27 -1
  20. package/src/cli/lib/__tests__/plugin-details.test.ts +9 -2
  21. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +20 -0
  22. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +43 -0
  23. package/src/cli/lib/__tests__/search-plugins.test.ts +31 -0
  24. package/src/cli/lib/bundled-plugin-packages.json +4 -0
  25. package/src/cli/lib/bundled-plugin-packages.ts +87 -0
  26. package/src/cli/lib/diff-plugin.ts +1 -1
  27. package/src/cli/lib/inspect-plugin.ts +80 -12
  28. package/src/cli/lib/install-from-github.ts +133 -76
  29. package/src/cli/lib/plugin-catalog-cache.ts +22 -3
  30. package/src/cli/lib/plugin-catalog-local.ts +13 -3
  31. package/src/cli/lib/plugin-catalog-platform.ts +6 -1
  32. package/src/cli/lib/plugin-catalog-resolve.ts +20 -0
  33. package/src/cli/lib/plugin-details.ts +12 -0
  34. package/src/cli/lib/plugin-marketplace.ts +121 -21
  35. package/src/cli/lib/plugin-pin-history.ts +5 -2
  36. package/src/cli/lib/search-plugins.ts +58 -16
  37. package/src/cli/lib/upgrade-plugin.ts +48 -3
  38. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  39. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  40. package/src/daemon/conversation-tool-setup.ts +4 -22
  41. package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +374 -0
  42. package/src/live-voice/__tests__/live-voice-vad.test.ts +658 -3
  43. package/src/live-voice/__tests__/protocol.test.ts +48 -0
  44. package/src/live-voice/__tests__/session-controls.test.ts +43 -0
  45. package/src/live-voice/live-voice-session.ts +784 -64
  46. package/src/live-voice/protocol.ts +20 -0
  47. package/src/live-voice/session-controls.ts +61 -3
  48. package/src/monitoring/plugin-auto-update.ts +6 -0
  49. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +15 -0
  50. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  51. package/src/runtime/routes/__tests__/plugins-routes.test.ts +102 -0
  52. package/src/runtime/routes/plugins-routes.ts +69 -49
  53. package/src/skills/managed-store.ts +153 -31
  54. package/src/tools/skills/find-similar-skills.test.ts +214 -2
  55. package/src/tools/skills/find-similar-skills.ts +62 -9
  56. package/src/tools/skills/resolve-execute-invocation.ts +24 -0
  57. 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
- const entries = await fetchMarketplaceEntries(
297
- { fetch: deps.fetch },
298
- { ref: marketplaceRef },
299
- );
300
- const match = entries.find((e) => e.name === name);
301
- if (match) {
302
- const committedAt = await fetchCommitDate(
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 = readRemote(match, marketplaceRef, committedAt);
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 the canonical GitHub source.
2
+ * Install a reviewed plugin by name from its catalog source.
3
3
  *
4
- * A name resolves to a whitelisted external ecosystem plugin — an entry in the
5
- * curated `plugins/marketplace.json` manifest. The pinned
6
- * `owner/repo[/path]@ref` (see {@link ./plugin-marketplace}) is fetched with a
7
- * shallow `git` clone at that ref — one network operation regardless of repo
8
- * size, immune to GitHub's unauthenticated API rate-limit, and recording the
9
- * exact resolved commit for provenance — and materialized into
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 these PRE-RESOLVED, TRUSTED GitHub coordinates while STILL
155
- * overlaying the curated `plugins/<name>` adapter stub. This is the offline
156
- * analogue of a marketplace install: the pin came from the reviewed bundled
157
- * catalog instead of a live marketplace fetch, so — unlike
158
- * {@link InstallPluginOptions.directSource} — the source is trusted and the
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?: PluginFetchSource;
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: PluginFetchSource): string {
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 the concrete GitHub coordinates of its pinned
331
- * marketplace entry, or `null` when no entry claims the name.
332
- *
333
- * The marketplace is external-only by construction — a same-named
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<PluginFetchSource | null> {
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: PluginFetchSource;
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 = opts.commitOverride
491
- ? { ...source, ref: opts.commitOverride }
492
- : source;
493
- stubRef = marketplaceRef;
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.ref;
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: PluginFetchSource;
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 interface InstallMetaSource {
739
- /** Source kind. Only `github` is written today. */
740
- readonly kind: string;
741
- readonly owner: string;
742
- readonly repo: string;
743
- /** Repo-relative directory holding the plugin root; absent = repo root. */
744
- readonly path?: string;
745
- /** Ref the install resolved through (the pinned commit SHA for marketplace installs). */
746
- readonly ref: string;
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: PluginFetchSource;
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 = await copyExternalViaGit(
858
- opts.source,
859
- opts.destDir,
860
- deps.runGit ?? defaultGitRunner,
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: PluginFetchSource;
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: "github",
1611
- owner: source.owner,
1612
- repo: source.repo,
1613
- path: source.rootPath || undefined,
1614
- ref,
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 { readBundledPluginCatalog } from "./plugin-catalog-local.js";
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
- cache.set(ref, { catalog, timestamp: Date.now() });
50
- return catalog;
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 pinned GitHub source from the bundled manifest,
66
- * or `null` when no bundled entry claims the name. The offline analogue of
67
- * resolving against the remote-fetched marketplace — used when platform
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 type { MarketplaceEntry } from "./plugin-marketplace.js";
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,