@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import type { FetchLike } from "../fetch-like.js";
|
|
7
|
+
import { installPlugin, readInstallMeta } from "../install-from-github.js";
|
|
8
|
+
import type { PluginCatalog } from "../search-plugins.js";
|
|
9
|
+
import { upgradePlugin } from "../upgrade-plugin.js";
|
|
10
|
+
|
|
11
|
+
const roots: string[] = [];
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
for (const root of roots.splice(0)) {
|
|
15
|
+
rmSync(root, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function packageMaterializer(calls: string[]) {
|
|
20
|
+
return (path: string, version: string, destination: string): number => {
|
|
21
|
+
calls.push(`${path}@${version}`);
|
|
22
|
+
mkdirSync(destination, { recursive: true });
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(destination, "plugin.json"),
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
27
|
+
name: "fathom",
|
|
28
|
+
version,
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
writeFileSync(
|
|
32
|
+
join(destination, "mcp.json"),
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
35
|
+
mcpServers: {
|
|
36
|
+
fathom: {
|
|
37
|
+
type: "streamable-http",
|
|
38
|
+
url: "https://api.fathom.ai/mcp",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
return 2;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function marketplaceFetch(version: string): FetchLike {
|
|
48
|
+
return (async () =>
|
|
49
|
+
new Response(
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
name: "vellum-assistant",
|
|
52
|
+
plugins: [
|
|
53
|
+
{
|
|
54
|
+
name: "fathom",
|
|
55
|
+
source: {
|
|
56
|
+
source: "local",
|
|
57
|
+
path: "plugins/mcp-catalog/fathom",
|
|
58
|
+
version,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
63
|
+
{ status: 200 },
|
|
64
|
+
)) as FetchLike;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function localCatalog(version: string): PluginCatalog {
|
|
68
|
+
return {
|
|
69
|
+
ref: "bundled",
|
|
70
|
+
matches: [
|
|
71
|
+
{
|
|
72
|
+
name: "fathom",
|
|
73
|
+
path: `local:plugins/mcp-catalog/fathom@${version}`,
|
|
74
|
+
category: null,
|
|
75
|
+
source: {
|
|
76
|
+
kind: "local",
|
|
77
|
+
path: "plugins/mcp-catalog/fathom",
|
|
78
|
+
version,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("bundled plugin upgrades", () => {
|
|
86
|
+
test("ignores a newer live manifest version the current binary cannot materialize", async () => {
|
|
87
|
+
const root = mkdtempSync(join(tmpdir(), "local-plugin-upgrade-"));
|
|
88
|
+
roots.push(root);
|
|
89
|
+
const pluginsDir = join(root, "plugins");
|
|
90
|
+
const calls: string[] = [];
|
|
91
|
+
const materializeLocalPackage = packageMaterializer(calls);
|
|
92
|
+
|
|
93
|
+
await installPlugin(
|
|
94
|
+
{
|
|
95
|
+
name: "fathom",
|
|
96
|
+
trustedSource: {
|
|
97
|
+
kind: "local",
|
|
98
|
+
path: "plugins/mcp-catalog/fathom",
|
|
99
|
+
version: "1.0.0",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
fetch: marketplaceFetch("1.0.0"),
|
|
104
|
+
workspacePluginsDir: pluginsDir,
|
|
105
|
+
materializeLocalPackage,
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const result = await upgradePlugin(
|
|
110
|
+
{ name: "fathom" },
|
|
111
|
+
{
|
|
112
|
+
fetch: marketplaceFetch("9.0.0"),
|
|
113
|
+
workspacePluginsDir: pluginsDir,
|
|
114
|
+
materializeLocalPackage,
|
|
115
|
+
localCatalog: localCatalog("1.0.0"),
|
|
116
|
+
beforeSwap: async () => {},
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(result.outcome).toBe("already-up-to-date");
|
|
121
|
+
expect(calls).toEqual(["plugins/mcp-catalog/fathom@1.0.0"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("materializes the exact inspected path and version", async () => {
|
|
125
|
+
const root = mkdtempSync(join(tmpdir(), "local-plugin-upgrade-"));
|
|
126
|
+
roots.push(root);
|
|
127
|
+
const pluginsDir = join(root, "plugins");
|
|
128
|
+
const calls: string[] = [];
|
|
129
|
+
const materializeLocalPackage = packageMaterializer(calls);
|
|
130
|
+
|
|
131
|
+
await installPlugin(
|
|
132
|
+
{
|
|
133
|
+
name: "fathom",
|
|
134
|
+
trustedSource: {
|
|
135
|
+
kind: "local",
|
|
136
|
+
path: "plugins/mcp-catalog/fathom",
|
|
137
|
+
version: "1.0.0",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
fetch: marketplaceFetch("1.0.0"),
|
|
142
|
+
workspacePluginsDir: pluginsDir,
|
|
143
|
+
materializeLocalPackage,
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const result = await upgradePlugin(
|
|
148
|
+
{ name: "fathom" },
|
|
149
|
+
{
|
|
150
|
+
fetch: marketplaceFetch("1.1.0"),
|
|
151
|
+
workspacePluginsDir: pluginsDir,
|
|
152
|
+
materializeLocalPackage,
|
|
153
|
+
localCatalog: localCatalog("1.1.0"),
|
|
154
|
+
beforeSwap: async () => {},
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.outcome).toBe("upgraded");
|
|
159
|
+
expect(calls).toEqual([
|
|
160
|
+
"plugins/mcp-catalog/fathom@1.0.0",
|
|
161
|
+
"plugins/mcp-catalog/fathom@1.1.0",
|
|
162
|
+
]);
|
|
163
|
+
expect(readInstallMeta(result.target)?.source).toEqual({
|
|
164
|
+
kind: "local",
|
|
165
|
+
path: "plugins/mcp-catalog/fathom",
|
|
166
|
+
version: "1.1.0",
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -20,10 +20,12 @@ import type { FetchLike } from "../fetch-like.js";
|
|
|
20
20
|
import {
|
|
21
21
|
getPluginCatalog,
|
|
22
22
|
invalidatePluginCatalogCache,
|
|
23
|
+
mergePlatformCatalogWithBundledLocals,
|
|
23
24
|
PLUGIN_CATALOG_CACHE_TTL_MS,
|
|
24
25
|
} from "../plugin-catalog-cache.js";
|
|
25
26
|
import { readBundledPluginCatalog } from "../plugin-catalog-local.js";
|
|
26
27
|
import type { SearchPluginsDeps } from "../search-plugins.js";
|
|
28
|
+
import type { PluginCatalog } from "../search-plugins.js";
|
|
27
29
|
|
|
28
30
|
// A non-zero base time. Bun treats `setSystemTime(new Date(0))` as "reset to
|
|
29
31
|
// the real clock", so the fake clock must start from a positive epoch.
|
|
@@ -166,3 +168,58 @@ describe("getPluginCatalog", () => {
|
|
|
166
168
|
expect(result.ref).toBe("main");
|
|
167
169
|
});
|
|
168
170
|
});
|
|
171
|
+
|
|
172
|
+
describe("mergePlatformCatalogWithBundledLocals", () => {
|
|
173
|
+
test("adds local packages while keeping a platform row on name collision", () => {
|
|
174
|
+
const platform: PluginCatalog = {
|
|
175
|
+
ref: "main",
|
|
176
|
+
matches: [
|
|
177
|
+
{
|
|
178
|
+
name: "fathom",
|
|
179
|
+
path: "github:provider/fathom@pin",
|
|
180
|
+
category: null,
|
|
181
|
+
source: {
|
|
182
|
+
kind: "github",
|
|
183
|
+
repo: "provider/fathom",
|
|
184
|
+
ref: "0".repeat(40),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
const bundled: PluginCatalog = {
|
|
190
|
+
ref: "bundled",
|
|
191
|
+
matches: [
|
|
192
|
+
{
|
|
193
|
+
name: "fathom",
|
|
194
|
+
path: "local:plugins/mcp-catalog/fathom@1.0.0",
|
|
195
|
+
category: null,
|
|
196
|
+
source: {
|
|
197
|
+
kind: "local",
|
|
198
|
+
path: "plugins/mcp-catalog/fathom",
|
|
199
|
+
version: "1.0.0",
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "notion",
|
|
204
|
+
path: "local:plugins/mcp-catalog/notion@1.0.0",
|
|
205
|
+
category: null,
|
|
206
|
+
source: {
|
|
207
|
+
kind: "local",
|
|
208
|
+
path: "plugins/mcp-catalog/notion",
|
|
209
|
+
version: "1.0.0",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const merged = mergePlatformCatalogWithBundledLocals(platform, bundled);
|
|
216
|
+
|
|
217
|
+
expect(merged.matches.map((match) => match.name)).toEqual([
|
|
218
|
+
"fathom",
|
|
219
|
+
"notion",
|
|
220
|
+
]);
|
|
221
|
+
expect(merged.matches[0]?.source.kind).toBe("github");
|
|
222
|
+
expect(merged.matches[1]?.source.kind).toBe("local");
|
|
223
|
+
expect(merged.ref).toBe("main");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -41,6 +41,15 @@ describe("fetchPluginCatalogFromPlatform", () => {
|
|
|
41
41
|
homepage: "https://example.com",
|
|
42
42
|
license: "MIT",
|
|
43
43
|
icon: "🧠",
|
|
44
|
+
integration: {
|
|
45
|
+
kind: "mcp",
|
|
46
|
+
displayName: "Memory Graph",
|
|
47
|
+
documentationUrl: "https://example.com/docs",
|
|
48
|
+
verifiedAt: "2026-09-10",
|
|
49
|
+
verification: "documentation-only",
|
|
50
|
+
setup: { mode: "oauth", instructions: "Sign in." },
|
|
51
|
+
logo: "memory-graph.png",
|
|
52
|
+
},
|
|
44
53
|
// dropped keys
|
|
45
54
|
id: "abc",
|
|
46
55
|
display_name: "Memory Graph",
|
|
@@ -69,6 +78,11 @@ describe("fetchPluginCatalogFromPlatform", () => {
|
|
|
69
78
|
category: "productivity",
|
|
70
79
|
homepage: "https://example.com",
|
|
71
80
|
license: "MIT",
|
|
81
|
+
integration: {
|
|
82
|
+
kind: "mcp",
|
|
83
|
+
displayName: "Memory Graph",
|
|
84
|
+
logo: "memory-graph.png",
|
|
85
|
+
},
|
|
72
86
|
source: {
|
|
73
87
|
kind: "github",
|
|
74
88
|
repo: "acme/memory-graph",
|
|
@@ -26,7 +26,9 @@ const FULL_SHA = "63a91ecadbf4c4719a4602a5abb00883f9966034";
|
|
|
26
26
|
|
|
27
27
|
/** A synthetic catalog match with an overridable source. */
|
|
28
28
|
function match(
|
|
29
|
-
source: Partial<
|
|
29
|
+
source: Partial<
|
|
30
|
+
Extract<PluginSearchMatch["source"], { kind: "github" }>
|
|
31
|
+
> = {},
|
|
30
32
|
): PluginSearchMatch {
|
|
31
33
|
return {
|
|
32
34
|
name: "example",
|
|
@@ -37,12 +39,32 @@ function match(
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
describe("resolveSourceFromMatch", () => {
|
|
42
|
+
test("returns an exact bundled package path and version", () => {
|
|
43
|
+
expect(
|
|
44
|
+
resolveSourceFromMatch({
|
|
45
|
+
name: "fathom",
|
|
46
|
+
path: "local:plugins/mcp-catalog/fathom@1.0.0",
|
|
47
|
+
category: null,
|
|
48
|
+
source: {
|
|
49
|
+
kind: "local",
|
|
50
|
+
path: "plugins/mcp-catalog/fathom",
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
).toEqual({
|
|
55
|
+
kind: "local",
|
|
56
|
+
path: "plugins/mcp-catalog/fathom",
|
|
57
|
+
version: "1.0.0",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
40
61
|
test("returns owner/repo/path/ref for a full-SHA match", () => {
|
|
41
62
|
expect(
|
|
42
63
|
resolveSourceFromMatch(
|
|
43
64
|
match({ repo: "acme/widget", path: "pkg/plugin" }),
|
|
44
65
|
),
|
|
45
66
|
).toEqual({
|
|
67
|
+
kind: "github",
|
|
46
68
|
owner: "acme",
|
|
47
69
|
repo: "widget",
|
|
48
70
|
path: "pkg/plugin",
|
|
@@ -52,6 +74,7 @@ describe("resolveSourceFromMatch", () => {
|
|
|
52
74
|
|
|
53
75
|
test('defaults path to "" when the source declares none', () => {
|
|
54
76
|
expect(resolveSourceFromMatch(match())).toEqual({
|
|
77
|
+
kind: "github",
|
|
55
78
|
owner: "acme",
|
|
56
79
|
repo: "example",
|
|
57
80
|
path: "",
|
|
@@ -66,6 +89,7 @@ describe("resolveSourceFromMatch", () => {
|
|
|
66
89
|
// A repo-root entry (`path: ""` or absent) is valid — the clean-path gate
|
|
67
90
|
// only applies to a non-empty path, so it must not be rejected.
|
|
68
91
|
expect(resolveSourceFromMatch(match({ path }))).toEqual({
|
|
92
|
+
kind: "github",
|
|
69
93
|
owner: "acme",
|
|
70
94
|
repo: "example",
|
|
71
95
|
path: "",
|
|
@@ -94,6 +118,7 @@ describe("resolveSourceFromMatch", () => {
|
|
|
94
118
|
|
|
95
119
|
test("resolves a clean nested path", () => {
|
|
96
120
|
expect(resolveSourceFromMatch(match({ path: "packages/plugin" }))).toEqual({
|
|
121
|
+
kind: "github",
|
|
97
122
|
owner: "acme",
|
|
98
123
|
repo: "example",
|
|
99
124
|
path: "packages/plugin",
|
|
@@ -157,6 +182,7 @@ describe("catalog-backed resolvers (bundled, offline)", () => {
|
|
|
157
182
|
expect(
|
|
158
183
|
await resolvePluginSourceFromCatalog("caveman", rejectingDeps),
|
|
159
184
|
).toEqual({
|
|
185
|
+
kind: "github",
|
|
160
186
|
owner: "JuliusBrussee",
|
|
161
187
|
repo: "caveman",
|
|
162
188
|
path: "",
|
|
@@ -108,12 +108,19 @@ function splitOnce(s: string, sep: string): [string, string] {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/** The bundled catalog entry for {@link name} — the source under test. */
|
|
111
|
-
function bundledMatch(name: string): PluginSearchMatch {
|
|
111
|
+
function bundledMatch(name: string): PluginSearchMatch & {
|
|
112
|
+
source: Extract<PluginSearchMatch["source"], { kind: "github" }>;
|
|
113
|
+
} {
|
|
112
114
|
const match = readBundledPluginCatalog().matches.find((m) => m.name === name);
|
|
113
115
|
if (!match) {
|
|
114
116
|
throw new Error(`bundled catalog has no entry for "${name}"`);
|
|
115
117
|
}
|
|
116
|
-
|
|
118
|
+
if (match.source.kind !== "github") {
|
|
119
|
+
throw new Error(`bundled catalog entry "${name}" is not GitHub-backed`);
|
|
120
|
+
}
|
|
121
|
+
return match as PluginSearchMatch & {
|
|
122
|
+
source: Extract<PluginSearchMatch["source"], { kind: "github" }>;
|
|
123
|
+
};
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
const PNG_SIGNATURE = Buffer.from([
|
|
@@ -81,6 +81,24 @@ describe("fetchMarketplaceEntries", () => {
|
|
|
81
81
|
expect(entries[1]!.source.path).toBe("packages/nested");
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
test("skips an entry with an unknown future source kind", async () => {
|
|
85
|
+
const fetch = manifestFetch({
|
|
86
|
+
...VALID_MANIFEST,
|
|
87
|
+
plugins: [
|
|
88
|
+
VALID_MANIFEST.plugins[0],
|
|
89
|
+
{
|
|
90
|
+
name: "future-plugin",
|
|
91
|
+
source: { source: "registry", package: "future-plugin" },
|
|
92
|
+
},
|
|
93
|
+
VALID_MANIFEST.plugins[1],
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const entries = await fetchMarketplaceEntries({ fetch }, { ref: "main" });
|
|
98
|
+
|
|
99
|
+
expect(entries.map((entry) => entry.name)).toEqual(["caveman", "nested"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
84
102
|
test("forwards the ref to the GitHub Contents API", async () => {
|
|
85
103
|
// GIVEN a fixture that records the ref query parameter
|
|
86
104
|
let seenRef: string | undefined;
|
|
@@ -334,6 +352,7 @@ describe("resolveMarketplaceSource", () => {
|
|
|
334
352
|
|
|
335
353
|
// THEN the owner/repo split out and path defaults to repo root
|
|
336
354
|
expect(resolved).toEqual({
|
|
355
|
+
kind: "github",
|
|
337
356
|
owner: "JuliusBrussee",
|
|
338
357
|
repo: "caveman",
|
|
339
358
|
path: "",
|
|
@@ -348,6 +367,7 @@ describe("resolveMarketplaceSource", () => {
|
|
|
348
367
|
|
|
349
368
|
// THEN the sub-path is preserved
|
|
350
369
|
expect(resolved).toEqual({
|
|
370
|
+
kind: "github",
|
|
351
371
|
owner: "acme",
|
|
352
372
|
repo: "monorepo",
|
|
353
373
|
path: "packages/nested",
|
|
@@ -22,12 +22,19 @@ import {
|
|
|
22
22
|
import { Command } from "commander";
|
|
23
23
|
|
|
24
24
|
import type { InstallPluginOptions } from "../install-from-github.js";
|
|
25
|
+
import type { PluginMatchSource } from "../search-plugins.js";
|
|
25
26
|
|
|
26
27
|
const installPluginCalls: InstallPluginOptions[] = [];
|
|
27
28
|
const platformInstallCalls: Array<{ name: string; force?: boolean }> = [];
|
|
28
29
|
|
|
29
30
|
const realGithub = await import("../install-from-github.js");
|
|
30
31
|
const realPlatform = await import("../install-from-platform.js");
|
|
32
|
+
const realCatalogCache = await import("../plugin-catalog-cache.js");
|
|
33
|
+
let platformCatalogSource: PluginMatchSource = {
|
|
34
|
+
kind: "github",
|
|
35
|
+
repo: "JuliusBrussee/caveman",
|
|
36
|
+
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
37
|
+
};
|
|
31
38
|
|
|
32
39
|
mock.module("../install-from-github.js", () => ({
|
|
33
40
|
...realGithub,
|
|
@@ -59,6 +66,21 @@ mock.module("../install-from-platform.js", () => ({
|
|
|
59
66
|
},
|
|
60
67
|
}));
|
|
61
68
|
|
|
69
|
+
mock.module("../plugin-catalog-cache.js", () => ({
|
|
70
|
+
...realCatalogCache,
|
|
71
|
+
getPluginCatalog: async () => ({
|
|
72
|
+
ref: "main",
|
|
73
|
+
matches: [
|
|
74
|
+
{
|
|
75
|
+
name: BUNDLED_PLUGIN,
|
|
76
|
+
path: "fixture",
|
|
77
|
+
category: null,
|
|
78
|
+
source: platformCatalogSource,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
}));
|
|
83
|
+
|
|
62
84
|
const { registerPluginsCommand } = await import("../../commands/plugins.js");
|
|
63
85
|
|
|
64
86
|
/** A plugin known to live in the bundled marketplace manifest. */
|
|
@@ -90,6 +112,11 @@ describe("plugins install by name — disable-platform mode", () => {
|
|
|
90
112
|
installPluginCalls.length = 0;
|
|
91
113
|
platformInstallCalls.length = 0;
|
|
92
114
|
process.exitCode = undefined;
|
|
115
|
+
platformCatalogSource = {
|
|
116
|
+
kind: "github",
|
|
117
|
+
repo: "JuliusBrussee/caveman",
|
|
118
|
+
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
119
|
+
};
|
|
93
120
|
spyOn(console, "log").mockImplementation(() => {});
|
|
94
121
|
spyOn(console, "error").mockImplementation(() => {});
|
|
95
122
|
});
|
|
@@ -156,4 +183,20 @@ describe("plugins install by name — disable-platform mode", () => {
|
|
|
156
183
|
expect(platformInstallCalls[0]!.name).toBe(BUNDLED_PLUGIN);
|
|
157
184
|
expect(process.exitCode).not.toBe(1);
|
|
158
185
|
});
|
|
186
|
+
|
|
187
|
+
test("installs a platform-visible local package without the platform tar endpoint", async () => {
|
|
188
|
+
delete process.env.VELLUM_DISABLE_PLATFORM;
|
|
189
|
+
delete process.env.IS_PLATFORM;
|
|
190
|
+
platformCatalogSource = {
|
|
191
|
+
kind: "local",
|
|
192
|
+
path: "plugins/mcp-catalog/fathom",
|
|
193
|
+
version: "1.0.0",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
await runInstall(BUNDLED_PLUGIN);
|
|
197
|
+
|
|
198
|
+
expect(platformInstallCalls).toHaveLength(0);
|
|
199
|
+
expect(installPluginCalls).toHaveLength(1);
|
|
200
|
+
expect(installPluginCalls[0]?.trustedSource).toEqual(platformCatalogSource);
|
|
201
|
+
});
|
|
159
202
|
});
|
|
@@ -151,6 +151,37 @@ describe("assertValidSearchPattern", () => {
|
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
describe("marketplaceMatch", () => {
|
|
154
|
+
test("projects a bundled MCP package and its integration metadata", () => {
|
|
155
|
+
const match = marketplaceMatch({
|
|
156
|
+
name: "fathom",
|
|
157
|
+
source: {
|
|
158
|
+
source: "local",
|
|
159
|
+
path: "plugins/mcp-catalog/fathom",
|
|
160
|
+
version: "1.0.0",
|
|
161
|
+
},
|
|
162
|
+
integration: {
|
|
163
|
+
kind: "mcp",
|
|
164
|
+
displayName: "Fathom",
|
|
165
|
+
documentationUrl: "https://developers.fathom.ai/mcp-docs",
|
|
166
|
+
verifiedAt: "2026-09-10",
|
|
167
|
+
verification: "documentation-only",
|
|
168
|
+
setup: { mode: "oauth", instructions: "Sign in to Fathom." },
|
|
169
|
+
logo: "fathom.png",
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(match).toMatchObject({
|
|
174
|
+
name: "fathom",
|
|
175
|
+
path: "local:plugins/mcp-catalog/fathom@1.0.0",
|
|
176
|
+
integration: { kind: "mcp", logo: "fathom.png" },
|
|
177
|
+
source: {
|
|
178
|
+
kind: "local",
|
|
179
|
+
path: "plugins/mcp-catalog/fathom",
|
|
180
|
+
version: "1.0.0",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
154
185
|
test("projects a root entry onto a github source with a display locator", () => {
|
|
155
186
|
expect(
|
|
156
187
|
marketplaceMatch(
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize reviewed local marketplace packages from the generated bundle.
|
|
3
|
+
* Package keys come from plugins/marketplace.json and never resolve against a
|
|
4
|
+
* caller-controlled filesystem root.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import bundledPackages from "./bundled-plugin-packages.json" with { type: "json" };
|
|
11
|
+
|
|
12
|
+
interface BundledPackageFile {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly contentBase64: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BundledPackage {
|
|
18
|
+
readonly version: string;
|
|
19
|
+
readonly files: readonly BundledPackageFile[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const packages = bundledPackages.packages as Record<string, BundledPackage>;
|
|
23
|
+
|
|
24
|
+
export class BundledPluginPackageError extends Error {
|
|
25
|
+
constructor(message: string) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "BundledPluginPackageError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Return the embedded package when its path and version match the catalog. */
|
|
32
|
+
export function getBundledPluginPackage(
|
|
33
|
+
path: string,
|
|
34
|
+
version: string,
|
|
35
|
+
): BundledPackage | null {
|
|
36
|
+
const pluginPackage = packages[path];
|
|
37
|
+
return pluginPackage?.version === version ? pluginPackage : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read one UTF-8 file from an exact embedded package. */
|
|
41
|
+
export function readBundledPluginFile(
|
|
42
|
+
path: string,
|
|
43
|
+
version: string,
|
|
44
|
+
filePath: string,
|
|
45
|
+
): string | null {
|
|
46
|
+
const file = getBundledPluginPackage(path, version)?.files.find(
|
|
47
|
+
(candidate) => candidate.path === filePath,
|
|
48
|
+
);
|
|
49
|
+
return file
|
|
50
|
+
? Buffer.from(file.contentBase64, "base64").toString("utf8")
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Copy one exact embedded package into an existing staging directory. */
|
|
55
|
+
export function materializeBundledPluginPackage(
|
|
56
|
+
path: string,
|
|
57
|
+
version: string,
|
|
58
|
+
destination: string,
|
|
59
|
+
): number {
|
|
60
|
+
const pluginPackage = getBundledPluginPackage(path, version);
|
|
61
|
+
if (!pluginPackage) {
|
|
62
|
+
throw new BundledPluginPackageError(
|
|
63
|
+
`Bundled plugin package ${path} @ ${version} is unavailable`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const file of pluginPackage.files) {
|
|
68
|
+
if (
|
|
69
|
+
file.path.length === 0 ||
|
|
70
|
+
file.path.startsWith("/") ||
|
|
71
|
+
file.path.includes("\\") ||
|
|
72
|
+
file.path
|
|
73
|
+
.split("/")
|
|
74
|
+
.some(
|
|
75
|
+
(segment) => segment === "." || segment === ".." || segment === "",
|
|
76
|
+
)
|
|
77
|
+
) {
|
|
78
|
+
throw new BundledPluginPackageError(
|
|
79
|
+
`Bundled plugin package ${path} contains an unsafe file path`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const target = join(destination, ...file.path.split("/"));
|
|
83
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
84
|
+
writeFileSync(target, Buffer.from(file.contentBase64, "base64"));
|
|
85
|
+
}
|
|
86
|
+
return pluginPackage.files.length;
|
|
87
|
+
}
|
|
@@ -290,7 +290,7 @@ export async function diffPlugin(
|
|
|
290
290
|
|
|
291
291
|
const meta = readInstallMeta(target);
|
|
292
292
|
const commit = meta?.commit ?? null;
|
|
293
|
-
if (!meta || !commit) {
|
|
293
|
+
if (!meta || meta.source.kind !== "github" || !commit) {
|
|
294
294
|
throw new PluginDiffUnavailableError(
|
|
295
295
|
name,
|
|
296
296
|
`no install commit was recorded (an older or manually-copied install); reinstall with 'assistant plugins install ${name} --force' to record provenance`,
|