@vellumai/assistant 0.12.2-staging.5 → 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 +6 -6
- package/src/daemon/conversation-tool-setup.ts +4 -22
- package/src/live-voice/__tests__/live-voice-vad.test.ts +624 -3
- package/src/live-voice/__tests__/session-controls.test.ts +18 -0
- package/src/live-voice/live-voice-session.ts +575 -53
- package/src/live-voice/session-controls.ts +7 -3
- package/src/monitoring/plugin-auto-update.ts +6 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -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 +98 -32
- package/src/tools/skills/find-similar-skills.test.ts +3 -0
- package/src/tools/skills/resolve-execute-invocation.ts +24 -0
- package/src/tools/skills/scaffold-managed.ts +16 -13
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read the curated plugin marketplace manifest from the canonical repo.
|
|
3
3
|
*
|
|
4
|
-
* The manifest at `plugins/marketplace.json` whitelists
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* a `plugins` array where each entry carries a `name` and a `source`. Only
|
|
10
|
-
* `github` sources are resolved today.
|
|
4
|
+
* The manifest at `plugins/marketplace.json` whitelists reviewed plugins so
|
|
5
|
+
* they appear in `assistant plugins search` / the web catalog and become
|
|
6
|
+
* installable by name. Its shape extends the Claude Code marketplace manifest
|
|
7
|
+
* (https://code.claude.com/docs/en/plugin-marketplaces) with packages embedded
|
|
8
|
+
* in the assistant distribution.
|
|
11
9
|
*
|
|
12
|
-
* The manifest is fetched from the repo at a git ref
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* The manifest is both fetched from the repo at a git ref and bundled into the
|
|
11
|
+
* assistant build. GitHub entries may grow without a new release. Local entries
|
|
12
|
+
* can only resolve when their exact path and version are embedded in that
|
|
13
|
+
* release.
|
|
15
14
|
* Every external source pins an explicit `ref` that MUST be a full commit SHA:
|
|
16
15
|
* the fetched code is locked to an immutable revision. Tags and branches are
|
|
17
16
|
* rejected because they are mutable — an upstream owner could retag/repoint
|
|
@@ -60,7 +59,7 @@ const PLUGIN_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
|
60
59
|
const COMMIT_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
61
60
|
|
|
62
61
|
export const githubSourceSchema = z.object({
|
|
63
|
-
/** Discriminator
|
|
62
|
+
/** Discriminator for a reviewed external repository. */
|
|
64
63
|
source: z.literal("github"),
|
|
65
64
|
/** `owner/repo` of the external plugin repository. */
|
|
66
65
|
repo: z.string().regex(REPO_SLUG_RE, "expected an `owner/repo` slug"),
|
|
@@ -89,10 +88,53 @@ export const githubSourceSchema = z.object({
|
|
|
89
88
|
),
|
|
90
89
|
});
|
|
91
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A plugin package shipped inside the assistant distribution. The path is a
|
|
93
|
+
* key into the generated bundled-package map, never a path resolved from the
|
|
94
|
+
* current working directory.
|
|
95
|
+
*/
|
|
96
|
+
export const localSourceSchema = z.object({
|
|
97
|
+
source: z.literal("local"),
|
|
98
|
+
path: z
|
|
99
|
+
.string()
|
|
100
|
+
.regex(/^plugins\/[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/)
|
|
101
|
+
.refine(
|
|
102
|
+
(path) =>
|
|
103
|
+
!path
|
|
104
|
+
.split(/[/\\]/)
|
|
105
|
+
.some((segment) => segment === "." || segment === ".."),
|
|
106
|
+
"path must be a clean bundled-package path",
|
|
107
|
+
),
|
|
108
|
+
version: z.string().min(1),
|
|
109
|
+
repo: z.never().optional(),
|
|
110
|
+
ref: z.never().optional(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const marketplaceSourceSchema = z.discriminatedUnion("source", [
|
|
114
|
+
githubSourceSchema,
|
|
115
|
+
localSourceSchema,
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
export const mcpIntegrationSchema = z.object({
|
|
119
|
+
kind: z.literal("mcp"),
|
|
120
|
+
displayName: z.string().min(1),
|
|
121
|
+
documentationUrl: z.string().url(),
|
|
122
|
+
verifiedAt: z.iso.date(),
|
|
123
|
+
verification: z.literal("documentation-only"),
|
|
124
|
+
setup: z.object({
|
|
125
|
+
mode: z.enum(["oauth", "manual"]),
|
|
126
|
+
instructions: z.string().min(1),
|
|
127
|
+
}),
|
|
128
|
+
/** Filename under the web client's bundled integration-image directory. */
|
|
129
|
+
logo: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/),
|
|
130
|
+
/** Existing main OAuth provider used to group a related MCP connection. */
|
|
131
|
+
oauthProvider: z.string().regex(PLUGIN_NAME_RE).optional(),
|
|
132
|
+
});
|
|
133
|
+
|
|
92
134
|
const marketplaceEntrySchema = z.object({
|
|
93
135
|
/** Install name. `assistant plugins install <name>` resolves to this entry. */
|
|
94
136
|
name: z.string().regex(PLUGIN_NAME_RE, "expected a kebab-case install name"),
|
|
95
|
-
source:
|
|
137
|
+
source: marketplaceSourceSchema,
|
|
96
138
|
description: z.string().optional(),
|
|
97
139
|
/** Free-form grouping hint (e.g. `productivity`). Informational. */
|
|
98
140
|
category: z.string().optional(),
|
|
@@ -111,9 +153,10 @@ const marketplaceEntrySchema = z.object({
|
|
|
111
153
|
"expected a short emoji, not a URL or path",
|
|
112
154
|
)
|
|
113
155
|
.optional(),
|
|
156
|
+
integration: mcpIntegrationSchema.optional(),
|
|
114
157
|
});
|
|
115
158
|
|
|
116
|
-
|
|
159
|
+
const marketplaceManifestBaseSchema = z.object({
|
|
117
160
|
name: z.string(),
|
|
118
161
|
owner: z
|
|
119
162
|
.object({
|
|
@@ -122,14 +165,33 @@ export const marketplaceManifestSchema = z.object({
|
|
|
122
165
|
email: z.string().optional(),
|
|
123
166
|
})
|
|
124
167
|
.optional(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export const marketplaceManifestSchema = marketplaceManifestBaseSchema.extend({
|
|
125
171
|
plugins: z.array(marketplaceEntrySchema),
|
|
126
172
|
});
|
|
127
173
|
|
|
128
|
-
|
|
174
|
+
const fetchedMarketplaceManifestSchema = marketplaceManifestBaseSchema.extend({
|
|
175
|
+
plugins: z.array(z.unknown()),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
function readSourceDiscriminator(entry: unknown): unknown {
|
|
179
|
+
if (typeof entry !== "object" || entry === null) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const source = (entry as Record<string, unknown>).source;
|
|
183
|
+
if (typeof source !== "object" || source === null) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return (source as Record<string, unknown>).source;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A single reviewed plugin entry. */
|
|
129
190
|
export type MarketplaceEntry = z.infer<typeof marketplaceEntrySchema>;
|
|
130
191
|
|
|
131
192
|
/** Concrete GitHub coordinates an entry resolves to for install. */
|
|
132
|
-
export interface
|
|
193
|
+
export interface ResolvedGitHubPluginSource {
|
|
194
|
+
readonly kind: "github";
|
|
133
195
|
readonly owner: string;
|
|
134
196
|
readonly repo: string;
|
|
135
197
|
/** Directory within the repo holding the plugin root; `""` = repo root. */
|
|
@@ -138,6 +200,17 @@ export interface ResolvedPluginSource {
|
|
|
138
200
|
readonly ref: string;
|
|
139
201
|
}
|
|
140
202
|
|
|
203
|
+
/** Concrete bundled-package coordinates an entry resolves to for install. */
|
|
204
|
+
export interface ResolvedLocalPluginSource {
|
|
205
|
+
readonly kind: "local";
|
|
206
|
+
readonly path: string;
|
|
207
|
+
readonly version: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export type ResolvedPluginSource =
|
|
211
|
+
| ResolvedGitHubPluginSource
|
|
212
|
+
| ResolvedLocalPluginSource;
|
|
213
|
+
|
|
141
214
|
/** Options controlling which marketplace revision to read. */
|
|
142
215
|
export interface FetchMarketplaceOptions {
|
|
143
216
|
/** Ref of the canonical repo to read the manifest from. */
|
|
@@ -236,23 +309,42 @@ export async function fetchMarketplaceEntries(
|
|
|
236
309
|
json = JSON.parse(await res.text());
|
|
237
310
|
} catch (err) {
|
|
238
311
|
throw new MarketplaceFetchError(
|
|
239
|
-
`Marketplace manifest is not valid JSON: ${
|
|
312
|
+
`Marketplace manifest is not valid JSON: ${
|
|
313
|
+
err instanceof Error ? err.message : String(err)
|
|
314
|
+
}`,
|
|
240
315
|
);
|
|
241
316
|
}
|
|
242
317
|
|
|
243
|
-
const parsed =
|
|
318
|
+
const parsed = fetchedMarketplaceManifestSchema.safeParse(json);
|
|
244
319
|
if (!parsed.success) {
|
|
245
320
|
throw new MarketplaceFetchError(
|
|
246
321
|
`Marketplace manifest failed validation: ${parsed.error.message}`,
|
|
247
322
|
);
|
|
248
323
|
}
|
|
249
|
-
|
|
250
|
-
|
|
324
|
+
const entries: MarketplaceEntry[] = [];
|
|
325
|
+
for (const rawEntry of parsed.data.plugins) {
|
|
326
|
+
const sourceKind = readSourceDiscriminator(rawEntry);
|
|
327
|
+
if (
|
|
328
|
+
typeof sourceKind === "string" &&
|
|
329
|
+
sourceKind !== "github" &&
|
|
330
|
+
sourceKind !== "local"
|
|
331
|
+
) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const entry = marketplaceEntrySchema.safeParse(rawEntry);
|
|
335
|
+
if (!entry.success) {
|
|
336
|
+
throw new MarketplaceFetchError(
|
|
337
|
+
`Marketplace manifest failed validation: ${entry.error.message}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
entries.push(entry.data);
|
|
341
|
+
}
|
|
342
|
+
return entries;
|
|
251
343
|
}
|
|
252
344
|
|
|
253
345
|
/**
|
|
254
|
-
* Resolve a plugin name to
|
|
255
|
-
*
|
|
346
|
+
* Resolve a plugin name to its exact source using the supplied marketplace
|
|
347
|
+
* entries. Returns `null` when no entry claims the name.
|
|
256
348
|
*/
|
|
257
349
|
export function resolveMarketplaceSource(
|
|
258
350
|
name: string,
|
|
@@ -262,8 +354,16 @@ export function resolveMarketplaceSource(
|
|
|
262
354
|
if (!entry) {
|
|
263
355
|
return null;
|
|
264
356
|
}
|
|
357
|
+
if (entry.source.source === "local") {
|
|
358
|
+
return {
|
|
359
|
+
kind: "local",
|
|
360
|
+
path: entry.source.path,
|
|
361
|
+
version: entry.source.version,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
265
364
|
const [owner, repo] = entry.source.repo.split("/", 2) as [string, string];
|
|
266
365
|
return {
|
|
366
|
+
kind: "github",
|
|
267
367
|
owner,
|
|
268
368
|
repo,
|
|
269
369
|
path: entry.source.path ?? "",
|
|
@@ -130,7 +130,9 @@ async function listManifestCommits(
|
|
|
130
130
|
body = JSON.parse(await res.text());
|
|
131
131
|
} catch (err) {
|
|
132
132
|
throw new PluginPinHistoryError(
|
|
133
|
-
`Marketplace commit history is not valid JSON: ${
|
|
133
|
+
`Marketplace commit history is not valid JSON: ${
|
|
134
|
+
err instanceof Error ? err.message : String(err)
|
|
135
|
+
}`,
|
|
134
136
|
);
|
|
135
137
|
}
|
|
136
138
|
if (!Array.isArray(body)) {
|
|
@@ -166,7 +168,8 @@ async function pinAtCommit(
|
|
|
166
168
|
{ fetch: fetchFn },
|
|
167
169
|
{ ref: marketplaceCommit },
|
|
168
170
|
);
|
|
169
|
-
|
|
171
|
+
const source = entries.find((entry) => entry.name === name)?.source;
|
|
172
|
+
return source?.source === "github" ? source.ref : null;
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
/**
|
|
@@ -22,26 +22,49 @@ export interface SearchPluginsDeps {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Where a catalog match comes from. */
|
|
25
|
-
export type PluginMatchSource =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
export type PluginMatchSource =
|
|
26
|
+
| {
|
|
27
|
+
readonly kind: "github";
|
|
28
|
+
/** `owner/repo` of the external plugin repository. */
|
|
29
|
+
readonly repo: string;
|
|
30
|
+
/** Directory within the repo, when the plugin is not at the root. */
|
|
31
|
+
readonly path?: string;
|
|
32
|
+
/** Pinned git ref the plugin is fetched from. */
|
|
33
|
+
readonly ref: string;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "local";
|
|
37
|
+
/** Exact package key in the assistant's embedded plugin bundle. */
|
|
38
|
+
readonly path: string;
|
|
39
|
+
readonly version: string;
|
|
40
|
+
readonly repo?: undefined;
|
|
41
|
+
readonly ref?: undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface McpPluginIntegration {
|
|
45
|
+
readonly kind: "mcp";
|
|
46
|
+
readonly displayName: string;
|
|
47
|
+
readonly documentationUrl: string;
|
|
48
|
+
readonly verifiedAt: string;
|
|
49
|
+
readonly verification: "documentation-only";
|
|
50
|
+
readonly setup: {
|
|
51
|
+
readonly mode: "oauth" | "manual";
|
|
52
|
+
readonly instructions: string;
|
|
53
|
+
};
|
|
54
|
+
readonly logo: string;
|
|
55
|
+
readonly oauthProvider?: string;
|
|
56
|
+
}
|
|
34
57
|
|
|
35
58
|
/** One matching catalog entry. */
|
|
36
59
|
export interface PluginSearchMatch {
|
|
37
60
|
/** Install name — `assistant plugins install <name>` resolves to it. */
|
|
38
61
|
readonly name: string;
|
|
39
62
|
/**
|
|
40
|
-
* Human-readable origin
|
|
41
|
-
*
|
|
63
|
+
* Human-readable origin: either `github:owner/repo[/path]@ref` or
|
|
64
|
+
* `local:plugins/path@version`.
|
|
42
65
|
*/
|
|
43
66
|
readonly path: string;
|
|
44
|
-
/** Short description, when known
|
|
67
|
+
/** Short description, when known. */
|
|
45
68
|
readonly description?: string;
|
|
46
69
|
/**
|
|
47
70
|
* Plugin icon: a curated emoji from the marketplace entry, or an icon URL
|
|
@@ -57,6 +80,7 @@ export interface PluginSearchMatch {
|
|
|
57
80
|
readonly homepage?: string;
|
|
58
81
|
/** License identifier, from the curated marketplace entry when present. */
|
|
59
82
|
readonly license?: string;
|
|
83
|
+
readonly integration?: McpPluginIntegration;
|
|
60
84
|
/** Discriminated origin, so callers can render/install accordingly. */
|
|
61
85
|
readonly source: PluginMatchSource;
|
|
62
86
|
}
|
|
@@ -121,14 +145,31 @@ export interface PluginCatalog {
|
|
|
121
145
|
|
|
122
146
|
/**
|
|
123
147
|
* Project a marketplace entry onto the catalog match shape, building a
|
|
124
|
-
*
|
|
148
|
+
* a source-specific locator for display.
|
|
125
149
|
*
|
|
126
150
|
* Shared by the platform fetcher and bundled reader so all catalog sources
|
|
127
151
|
* project entries identically.
|
|
128
152
|
*/
|
|
129
153
|
export function marketplaceMatch(entry: MarketplaceEntry): PluginSearchMatch {
|
|
130
|
-
const
|
|
131
|
-
|
|
154
|
+
const source: PluginMatchSource =
|
|
155
|
+
entry.source.source === "github"
|
|
156
|
+
? {
|
|
157
|
+
kind: "github",
|
|
158
|
+
repo: entry.source.repo,
|
|
159
|
+
path: entry.source.path,
|
|
160
|
+
ref: entry.source.ref,
|
|
161
|
+
}
|
|
162
|
+
: {
|
|
163
|
+
kind: "local",
|
|
164
|
+
path: entry.source.path,
|
|
165
|
+
version: entry.source.version,
|
|
166
|
+
};
|
|
167
|
+
const locator =
|
|
168
|
+
source.kind === "github"
|
|
169
|
+
? `github:${source.repo}${source.path ? `/${source.path}` : ""}@${
|
|
170
|
+
source.ref
|
|
171
|
+
}`
|
|
172
|
+
: `local:${source.path}@${source.version}`;
|
|
132
173
|
return {
|
|
133
174
|
name: entry.name,
|
|
134
175
|
path: locator,
|
|
@@ -137,7 +178,8 @@ export function marketplaceMatch(entry: MarketplaceEntry): PluginSearchMatch {
|
|
|
137
178
|
category: entry.category ?? null,
|
|
138
179
|
homepage: entry.homepage,
|
|
139
180
|
license: entry.license,
|
|
140
|
-
|
|
181
|
+
integration: entry.integration,
|
|
182
|
+
source,
|
|
141
183
|
};
|
|
142
184
|
}
|
|
143
185
|
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
finalizeStagedInstall,
|
|
65
65
|
type GitRunner,
|
|
66
66
|
installPlugin,
|
|
67
|
+
type InstallPluginDeps,
|
|
67
68
|
isFullCommitSha,
|
|
68
69
|
materializePluginTree,
|
|
69
70
|
type PluginFetchSource,
|
|
@@ -94,6 +95,7 @@ import {
|
|
|
94
95
|
type PluginUpgradeStrategy,
|
|
95
96
|
} from "./plugin-constants.js";
|
|
96
97
|
import { computeFingerprint, fingerprintsEqual } from "./plugin-fingerprint.js";
|
|
98
|
+
import type { PluginCatalog } from "./search-plugins.js";
|
|
97
99
|
import { PluginNotInstalledError } from "./uninstall-plugin.js";
|
|
98
100
|
|
|
99
101
|
/**
|
|
@@ -167,6 +169,8 @@ export interface UpgradePluginDeps {
|
|
|
167
169
|
* upgrades alike; dry runs and no-ops never stage, so it is not invoked.
|
|
168
170
|
*/
|
|
169
171
|
readonly confirmStaged?: ConfirmStagedInstall;
|
|
172
|
+
readonly materializeLocalPackage?: InstallPluginDeps["materializeLocalPackage"];
|
|
173
|
+
readonly localCatalog?: PluginCatalog;
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
/** Result of an upgrade attempt. */
|
|
@@ -317,7 +321,11 @@ export async function upgradePlugin(
|
|
|
317
321
|
try {
|
|
318
322
|
inspection = await inspectPlugin(
|
|
319
323
|
{ name },
|
|
320
|
-
{
|
|
324
|
+
{
|
|
325
|
+
fetch: deps.fetch,
|
|
326
|
+
workspacePluginsDir: deps.workspacePluginsDir,
|
|
327
|
+
localCatalog: deps.localCatalog,
|
|
328
|
+
},
|
|
321
329
|
);
|
|
322
330
|
} catch (err) {
|
|
323
331
|
if (err instanceof PluginInspectNotFoundError) {
|
|
@@ -373,6 +381,19 @@ export async function upgradePlugin(
|
|
|
373
381
|
const toTimestamp = remote.committedAt;
|
|
374
382
|
const provenanceWasUnknown = inspection.status === "unknown-provenance";
|
|
375
383
|
|
|
384
|
+
const localSource = local.source;
|
|
385
|
+
const sourceChanged =
|
|
386
|
+
remote.kind === "local"
|
|
387
|
+
? localSource?.kind !== "local" || localSource.path !== remote.path
|
|
388
|
+
: localSource?.kind === "local";
|
|
389
|
+
if (sourceChanged) {
|
|
390
|
+
throw new PluginNotUpgradableError(
|
|
391
|
+
name,
|
|
392
|
+
"its marketplace source changed; reinstall explicitly with 'plugins install " +
|
|
393
|
+
`${name} --force' to accept the new source`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
376
397
|
if (inspection.status === "up-to-date") {
|
|
377
398
|
return {
|
|
378
399
|
name,
|
|
@@ -417,6 +438,12 @@ export async function upgradePlugin(
|
|
|
417
438
|
strategy === "theirs" ||
|
|
418
439
|
strategy === "assistant"
|
|
419
440
|
) {
|
|
441
|
+
if (remote.kind === "local") {
|
|
442
|
+
throw new PluginMergeBaselineError(
|
|
443
|
+
name,
|
|
444
|
+
"bundled packages do not retain the previous package version needed for a three-way merge",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
420
447
|
const [remoteOwner, remoteRepo] = remote.repo.split("/");
|
|
421
448
|
return mergeUpgrade(
|
|
422
449
|
{
|
|
@@ -445,7 +472,19 @@ export async function upgradePlugin(
|
|
|
445
472
|
}
|
|
446
473
|
|
|
447
474
|
const result = await installPlugin(
|
|
448
|
-
{
|
|
475
|
+
{
|
|
476
|
+
name,
|
|
477
|
+
force: true,
|
|
478
|
+
...(remote.kind === "local"
|
|
479
|
+
? {
|
|
480
|
+
trustedSource: {
|
|
481
|
+
kind: "local" as const,
|
|
482
|
+
path: remote.path,
|
|
483
|
+
version: remote.version ?? remote.commit,
|
|
484
|
+
},
|
|
485
|
+
}
|
|
486
|
+
: {}),
|
|
487
|
+
},
|
|
449
488
|
{
|
|
450
489
|
fetch: deps.fetch,
|
|
451
490
|
workspacePluginsDir: deps.workspacePluginsDir,
|
|
@@ -454,6 +493,7 @@ export async function upgradePlugin(
|
|
|
454
493
|
runInstallDeps: deps.runInstallDeps,
|
|
455
494
|
beforeSwap: deps.beforeSwap,
|
|
456
495
|
confirmStaged: deps.confirmStaged,
|
|
496
|
+
materializeLocalPackage: deps.materializeLocalPackage,
|
|
457
497
|
},
|
|
458
498
|
);
|
|
459
499
|
|
|
@@ -716,7 +756,12 @@ async function mergeUpgrade(
|
|
|
716
756
|
};
|
|
717
757
|
|
|
718
758
|
const meta = readInstallMeta(local.target);
|
|
719
|
-
if (
|
|
759
|
+
if (
|
|
760
|
+
!meta ||
|
|
761
|
+
meta.source.kind !== "github" ||
|
|
762
|
+
!meta.commit ||
|
|
763
|
+
!meta.fingerprint
|
|
764
|
+
) {
|
|
720
765
|
throw new PluginMergeBaselineError(
|
|
721
766
|
name,
|
|
722
767
|
"no install commit or fingerprint was recorded (an older or manually-copied install)",
|
|
@@ -28,7 +28,7 @@ Do NOT use this skill when the user just wants to run an existing skill. That is
|
|
|
28
28
|
## Capabilities
|
|
29
29
|
|
|
30
30
|
- **Scaffold** a new managed skill with YAML frontmatter and markdown body
|
|
31
|
-
- **Edit** an existing skill by scaffolding over it (
|
|
31
|
+
- **Edit** an existing skill by scaffolding over it (replaces the body and needs `activation_hints` restated; every other frontmatter field you leave out keeps its current value, and an empty value clears one)
|
|
32
32
|
- **Delete** an existing managed skill directory
|
|
33
33
|
|
|
34
34
|
Skills created via `scaffold_managed_skill` become available for `skill_load` when a valid top-level `SKILL.md` is written under the skill directory.
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
"emoji": {
|
|
29
29
|
"type": "string",
|
|
30
|
-
"description": "Optional emoji icon for the skill."
|
|
30
|
+
"description": "Optional emoji icon for the skill. On an overwrite, omit to keep the current one; pass an empty string to clear it."
|
|
31
31
|
},
|
|
32
32
|
"category": {
|
|
33
33
|
"type": "string",
|
|
34
|
-
"description": "Optional single lowercase category for grouping the skill in the Skills UI sidebar. Must be one of the published categories
|
|
34
|
+
"description": "Optional single lowercase category for grouping the skill in the Skills UI sidebar. Must be one of the published categories: a value outside this list shows under All with no sidebar bucket, so always pick the closest fit: browsing, calendar, commerce, content, development, email, health, integrations, messaging, productivity, system, voice. On an overwrite, omit to keep the current category; pass an empty string to clear it."
|
|
35
35
|
},
|
|
36
36
|
"overwrite": {
|
|
37
37
|
"type": "boolean",
|
|
38
|
-
"description": "Whether to overwrite an existing skill with the same ID (default: false)."
|
|
38
|
+
"description": "Whether to overwrite an existing skill with the same ID (default: false). An overwrite always replaces the body and must restate activation_hints; every other frontmatter field keeps its current value when omitted (emoji, category, includes, avoid_when, and anything else the file carries), and an empty value clears one."
|
|
39
39
|
},
|
|
40
40
|
"change_summary": {
|
|
41
41
|
"type": "string",
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
"includes": {
|
|
45
45
|
"type": "array",
|
|
46
46
|
"items": { "type": "string" },
|
|
47
|
-
"description": "Optional list of child skill IDs this skill composes. When this skill is loaded via skill_load, each child's body is inlined after the parent's and the child's tools are projected, so the parent can rely on the child's procedure without re-stating it. Does not affect which skills get selected for a turn."
|
|
47
|
+
"description": "Optional list of child skill IDs this skill composes. When this skill is loaded via skill_load, each child's body is inlined after the parent's and the child's tools are projected, so the parent can rely on the child's procedure without re-stating it. Does not affect which skills get selected for a turn. On an overwrite, omit to keep the current list; pass an empty list to clear it."
|
|
48
48
|
},
|
|
49
49
|
"activation_hints": {
|
|
50
50
|
"type": "array",
|
|
51
51
|
"items": { "type": "string" },
|
|
52
|
-
"description": "Required. 1-4 short trigger phrases describing the situations where this skill should activate, phrased as the observed intent (e.g. \"user asks to deploy staging\"). Surfaced in memory as a \"Use when: …\" clause so the skill is retrievable by intent, not just by name.
|
|
52
|
+
"description": "Required. 1-4 short trigger phrases describing the situations where this skill should activate, phrased as the observed intent (e.g. \"user asks to deploy staging\"). Surfaced in memory as a \"Use when: …\" clause so the skill is retrievable by intent, not just by name. Required on an overwrite too: the hints should track the body, so restate them, revised if the procedure changed."
|
|
53
53
|
},
|
|
54
54
|
"avoid_when": {
|
|
55
55
|
"type": "array",
|
|
56
56
|
"items": { "type": "string" },
|
|
57
|
-
"description": "Optional situations where this skill should NOT be used. Surfaced in memory as an \"Avoid when: …\" clause to steer retrieval away from the wrong contexts."
|
|
57
|
+
"description": "Optional situations where this skill should NOT be used. Surfaced in memory as an \"Avoid when: …\" clause to steer retrieval away from the wrong contexts. On an overwrite, omit to keep the current list; pass an empty list to clear it."
|
|
58
58
|
},
|
|
59
59
|
"files": {
|
|
60
60
|
"type": "array",
|
|
@@ -45,11 +45,8 @@ import {
|
|
|
45
45
|
injectActivityField,
|
|
46
46
|
stripActivityField,
|
|
47
47
|
} from "../tools/schema-transforms.js";
|
|
48
|
-
import {
|
|
49
|
-
|
|
50
|
-
recoverSkillExecuteEnvelope,
|
|
51
|
-
resolveSkillExecuteInput,
|
|
52
|
-
} from "../tools/skills/execute.js";
|
|
48
|
+
import { augmentSkillExecuteError } from "../tools/skills/execute.js";
|
|
49
|
+
import { resolveSkillExecuteInvocation } from "../tools/skills/resolve-execute-invocation.js";
|
|
53
50
|
import { resolveToolInvocationAlias } from "../tools/tool-name-aliases.js";
|
|
54
51
|
import type {
|
|
55
52
|
ProxyApprovalCallback,
|
|
@@ -535,23 +532,8 @@ export function createToolExecutor(
|
|
|
535
532
|
// risk level, permission checks, hooks, and lifecycle events all fire
|
|
536
533
|
// with the real tool name.
|
|
537
534
|
if (executionName === "skill_execute") {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
// recoverSkillExecuteEnvelope), then resolve the inner tool + params.
|
|
541
|
-
const envelope = recoverSkillExecuteEnvelope(executionInput);
|
|
542
|
-
const rawToolName =
|
|
543
|
-
typeof envelope.tool === "string" ? envelope.tool : "";
|
|
544
|
-
const innerSchema = rawToolName
|
|
545
|
-
? getTool(rawToolName)?.input_schema
|
|
546
|
-
: undefined;
|
|
547
|
-
const rawToolInput = resolveSkillExecuteInput(envelope, innerSchema);
|
|
548
|
-
|
|
549
|
-
// Clone to avoid mutating shared input objects
|
|
550
|
-
const { name: toolName, input: toolInput } = resolveToolInvocationAlias(
|
|
551
|
-
rawToolName,
|
|
552
|
-
{ ...rawToolInput },
|
|
553
|
-
ctx.allowedToolNames,
|
|
554
|
-
);
|
|
535
|
+
const { name: toolName, input: toolInput } =
|
|
536
|
+
resolveSkillExecuteInvocation(executionInput, ctx.allowedToolNames);
|
|
555
537
|
|
|
556
538
|
if (!toolName) {
|
|
557
539
|
return {
|