@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
|
@@ -171,6 +171,94 @@ describe("scaffold_managed_skill tool", () => {
|
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
test("an overwrite through the tool keeps frontmatter it does not restate", async () => {
|
|
175
|
+
await executeScaffoldManagedSkill(
|
|
176
|
+
{
|
|
177
|
+
skill_id: "edited",
|
|
178
|
+
name: "Edited",
|
|
179
|
+
description: "V1",
|
|
180
|
+
body_markdown: "Old body.",
|
|
181
|
+
activation_hints: HINTS,
|
|
182
|
+
emoji: "📊",
|
|
183
|
+
category: "productivity",
|
|
184
|
+
includes: ["csv-basics"],
|
|
185
|
+
avoid_when: ["the report is monthly"],
|
|
186
|
+
},
|
|
187
|
+
makeContext(),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// The usual "change step 3" edit: body and hints, nothing else restated.
|
|
191
|
+
const result = await executeScaffoldManagedSkill(
|
|
192
|
+
{
|
|
193
|
+
skill_id: "edited",
|
|
194
|
+
name: "Edited",
|
|
195
|
+
description: "V2",
|
|
196
|
+
body_markdown: "New body.",
|
|
197
|
+
activation_hints: HINTS,
|
|
198
|
+
overwrite: true,
|
|
199
|
+
},
|
|
200
|
+
makeContext(),
|
|
201
|
+
);
|
|
202
|
+
expect(result.isError).toBe(false);
|
|
203
|
+
|
|
204
|
+
const skill = loadSkillCatalog().find((s) => s.id === "edited")!;
|
|
205
|
+
expect(skill.emoji).toBe("📊");
|
|
206
|
+
expect(skill.category).toBe("productivity");
|
|
207
|
+
expect(skill.includes).toEqual(["csv-basics"]);
|
|
208
|
+
expect(skill.avoidWhen).toEqual(["the report is monthly"]);
|
|
209
|
+
expect(skill.description).toBe("V2");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("an overwrite through the tool clears a field passed empty, and hints stay required", async () => {
|
|
213
|
+
await executeScaffoldManagedSkill(
|
|
214
|
+
{
|
|
215
|
+
skill_id: "cleared",
|
|
216
|
+
name: "Cleared",
|
|
217
|
+
description: "V1",
|
|
218
|
+
body_markdown: "Body.",
|
|
219
|
+
activation_hints: HINTS,
|
|
220
|
+
category: "productivity",
|
|
221
|
+
avoid_when: ["the report is monthly"],
|
|
222
|
+
},
|
|
223
|
+
makeContext(),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const noHints = await executeScaffoldManagedSkill(
|
|
227
|
+
{
|
|
228
|
+
skill_id: "cleared",
|
|
229
|
+
name: "Cleared",
|
|
230
|
+
description: "V2",
|
|
231
|
+
body_markdown: "Body.",
|
|
232
|
+
activation_hints: [],
|
|
233
|
+
overwrite: true,
|
|
234
|
+
},
|
|
235
|
+
makeContext(),
|
|
236
|
+
);
|
|
237
|
+
// An explicit empty list clears other fields, but hints must track the
|
|
238
|
+
// body, so an overwrite still has to state them.
|
|
239
|
+
expect(noHints.isError).toBe(true);
|
|
240
|
+
expect(noHints.content).toContain("activation_hints is required");
|
|
241
|
+
|
|
242
|
+
const result = await executeScaffoldManagedSkill(
|
|
243
|
+
{
|
|
244
|
+
skill_id: "cleared",
|
|
245
|
+
name: "Cleared",
|
|
246
|
+
description: "V2",
|
|
247
|
+
body_markdown: "Body.",
|
|
248
|
+
activation_hints: HINTS,
|
|
249
|
+
overwrite: true,
|
|
250
|
+
category: " ",
|
|
251
|
+
avoid_when: [],
|
|
252
|
+
},
|
|
253
|
+
makeContext(),
|
|
254
|
+
);
|
|
255
|
+
expect(result.isError).toBe(false);
|
|
256
|
+
|
|
257
|
+
const skill = loadSkillCatalog().find((s) => s.id === "cleared")!;
|
|
258
|
+
expect(skill.category).toBeUndefined();
|
|
259
|
+
expect(skill.avoidWhen).toBeUndefined();
|
|
260
|
+
});
|
|
261
|
+
|
|
174
262
|
test("the registered tool's schema rejects an omitted activation_hints before the executor runs", () => {
|
|
175
263
|
// A call through the registered tool is validated against TOOLS.json
|
|
176
264
|
// first (skill-tool-factory), so the requirement has to live in the
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
parseTerminalSessionControl,
|
|
10
10
|
type SessionControlRequest,
|
|
11
11
|
stripInternalSpeechMarkers,
|
|
12
|
+
TASK_STOP_MARKER,
|
|
12
13
|
terminalControlMarkerLength,
|
|
13
14
|
} from "../voice-control-protocol.js";
|
|
14
15
|
|
|
@@ -52,13 +53,26 @@ describe("minimize-room marker", () => {
|
|
|
52
53
|
|
|
53
54
|
describe("isIncompleteControlMarkerTail", () => {
|
|
54
55
|
test("strict prefixes of any marker are incomplete", () => {
|
|
55
|
-
for (const tail of [
|
|
56
|
+
for (const tail of [
|
|
57
|
+
"[",
|
|
58
|
+
"[-",
|
|
59
|
+
"[-1",
|
|
60
|
+
"[END_CAL",
|
|
61
|
+
"[TASK:ST",
|
|
62
|
+
"[ASK_GUARDIAN_APPRO",
|
|
63
|
+
]) {
|
|
56
64
|
expect(isIncompleteControlMarkerTail(tail)).toBe(true);
|
|
57
65
|
}
|
|
58
66
|
});
|
|
59
67
|
|
|
60
68
|
test("complete literal markers are not held", () => {
|
|
61
|
-
for (const tail of [
|
|
69
|
+
for (const tail of [
|
|
70
|
+
"[-1]",
|
|
71
|
+
"[END_CALL]",
|
|
72
|
+
TASK_STOP_MARKER,
|
|
73
|
+
"[0] answer",
|
|
74
|
+
"[-1] look here",
|
|
75
|
+
]) {
|
|
62
76
|
expect(isIncompleteControlMarkerTail(tail)).toBe(false);
|
|
63
77
|
}
|
|
64
78
|
});
|
|
@@ -148,6 +162,12 @@ describe("session control markers", () => {
|
|
|
148
162
|
);
|
|
149
163
|
});
|
|
150
164
|
|
|
165
|
+
test("strips the task-stop marker so it is never spoken", () => {
|
|
166
|
+
expect(stripInternalSpeechMarkers(`Okay. ${TASK_STOP_MARKER}`).trim()).toBe(
|
|
167
|
+
"Okay.",
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
151
171
|
test("holds a streaming timed mute until its bracket arrives", () => {
|
|
152
172
|
expect(isIncompleteControlMarkerTail("[MU")).toBe(true);
|
|
153
173
|
expect(isIncompleteControlMarkerTail("[MUTE:3")).toBe(true);
|
|
@@ -162,6 +182,7 @@ describe("session control markers", () => {
|
|
|
162
182
|
|
|
163
183
|
test.each([
|
|
164
184
|
["Okay, talk soon. [END_CALL]", { action: "end" }],
|
|
185
|
+
["Okay, stopping. [TASK:STOP]", { action: "task_stop" }],
|
|
165
186
|
["Muted. [MUTE]", { action: "mute" }],
|
|
166
187
|
["Taking a look. [LOOK:SCREEN]", { action: "look_screen" }],
|
|
167
188
|
["Show me. [LOOK:CAMERA]", { action: "look_camera" }],
|
|
@@ -200,6 +221,7 @@ describe("session control markers", () => {
|
|
|
200
221
|
test("measures the terminal marker the transcript pass strips", () => {
|
|
201
222
|
expect(terminalControlMarkerLength("Done [-1]")).toBe(4);
|
|
202
223
|
expect(terminalControlMarkerLength("Bye [END_CALL] ")).toBe(10);
|
|
224
|
+
expect(terminalControlMarkerLength("Stopping [TASK:STOP]")).toBe(11);
|
|
203
225
|
expect(terminalControlMarkerLength("Muted [MUTE:30]")).toBe(9);
|
|
204
226
|
expect(terminalControlMarkerLength("Okay [UPDATES:FEWER]")).toBe(15);
|
|
205
227
|
expect(terminalControlMarkerLength("The array [-1] sorts")).toBe(0);
|
|
@@ -842,6 +842,17 @@ describe("startVoiceTurn triage-and-escalate control prompt", () => {
|
|
|
842
842
|
expect(installed()).toContain(escalatedContinuationRule());
|
|
843
843
|
});
|
|
844
844
|
|
|
845
|
+
test("a direct escalated turn keeps the caller-supplied resume prompt verbatim", async () => {
|
|
846
|
+
const installed = captureInstalledPrompt();
|
|
847
|
+
await startVoiceTurn({
|
|
848
|
+
...makeTurnOptions(),
|
|
849
|
+
voiceControlPrompt: LIVE_VOICE_PROMPT,
|
|
850
|
+
routingLeg: "escalated",
|
|
851
|
+
directEscalated: true,
|
|
852
|
+
});
|
|
853
|
+
expect(installed()).toBe(LIVE_VOICE_PROMPT);
|
|
854
|
+
});
|
|
855
|
+
|
|
845
856
|
test("the auto-built phone prompt carries the front-door rule anchored to the caller's words", async () => {
|
|
846
857
|
const installed = captureInstalledPrompt();
|
|
847
858
|
await startVoiceTurn({
|
|
@@ -1966,7 +1977,7 @@ describe("startVoiceTurn tool-event forwarding", () => {
|
|
|
1966
1977
|
fakeConversation = fake.conversation;
|
|
1967
1978
|
}
|
|
1968
1979
|
|
|
1969
|
-
test("tool_use_start delivers the tool name,
|
|
1980
|
+
test("tool_use_start delivers the tool name, input, and active allowlist", async () => {
|
|
1970
1981
|
makeEventEmittingConversation([
|
|
1971
1982
|
{
|
|
1972
1983
|
type: "tool_use_start",
|
|
@@ -1975,6 +1986,11 @@ describe("startVoiceTurn tool-event forwarding", () => {
|
|
|
1975
1986
|
toolUseId: "toolu-1",
|
|
1976
1987
|
},
|
|
1977
1988
|
]);
|
|
1989
|
+
(
|
|
1990
|
+
fakeConversation as typeof fakeConversation & {
|
|
1991
|
+
allowedToolNames?: Set<string>;
|
|
1992
|
+
}
|
|
1993
|
+
).allowedToolNames = new Set(["web_search"]);
|
|
1978
1994
|
|
|
1979
1995
|
const starts: Array<{ toolName: string; detail?: unknown }> = [];
|
|
1980
1996
|
await startVoiceTurn({
|
|
@@ -1988,7 +2004,11 @@ describe("startVoiceTurn tool-event forwarding", () => {
|
|
|
1988
2004
|
expect(starts).toEqual([
|
|
1989
2005
|
{
|
|
1990
2006
|
toolName: "web_search",
|
|
1991
|
-
detail: {
|
|
2007
|
+
detail: {
|
|
2008
|
+
toolUseId: "toolu-1",
|
|
2009
|
+
input: { query: "weather" },
|
|
2010
|
+
allowedToolNames: new Set(["web_search"]),
|
|
2011
|
+
},
|
|
1992
2012
|
},
|
|
1993
2013
|
]);
|
|
1994
2014
|
});
|
|
@@ -13,6 +13,7 @@ export const CALL_OPENING_MARKER = "[CALL_OPENING]";
|
|
|
13
13
|
export const CALL_OPENING_ACK_MARKER = "[CALL_OPENING_ACK]";
|
|
14
14
|
export const CALL_VERIFICATION_COMPLETE_MARKER = "[CALL_VERIFICATION_COMPLETE]";
|
|
15
15
|
export const END_CALL_MARKER = "[END_CALL]";
|
|
16
|
+
export const TASK_STOP_MARKER = "[TASK:STOP]";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Verdict tokens for the fast "front-door" model (triage-and-escalate voice
|
|
@@ -89,6 +90,7 @@ const USER_INSTRUCTION_MARKER_REGEX = /\[USER_INSTRUCTION:\s*.+?\]/g;
|
|
|
89
90
|
const CALL_OPENING_MARKER_REGEX = /\[CALL_OPENING\]/g;
|
|
90
91
|
const CALL_OPENING_ACK_MARKER_REGEX = /\[CALL_OPENING_ACK\]/g;
|
|
91
92
|
const END_CALL_MARKER_REGEX = /\[END_CALL\]/g;
|
|
93
|
+
const TASK_STOP_MARKER_REGEX = /\[TASK:STOP\]/g;
|
|
92
94
|
const HOLD_VERDICT_TOKEN_REGEX = /\[0\]/g;
|
|
93
95
|
const ESCALATE_VERDICT_TOKEN_REGEX = /\[1\]/g;
|
|
94
96
|
const MINIMIZE_ROOM_MARKER_REGEX = /\[-1\]/g;
|
|
@@ -217,6 +219,7 @@ export function stripInternalSpeechMarkers(text: string): string {
|
|
|
217
219
|
.replace(CALL_OPENING_MARKER_REGEX, "")
|
|
218
220
|
.replace(CALL_OPENING_ACK_MARKER_REGEX, "")
|
|
219
221
|
.replace(END_CALL_MARKER_REGEX, "")
|
|
222
|
+
.replace(TASK_STOP_MARKER_REGEX, "")
|
|
220
223
|
.replace(HOLD_VERDICT_TOKEN_REGEX, "")
|
|
221
224
|
.replace(ESCALATE_VERDICT_TOKEN_REGEX, "")
|
|
222
225
|
.replace(MINIMIZE_ROOM_MARKER_REGEX, "")
|
|
@@ -245,6 +248,7 @@ const CONTROL_MARKER_STRINGS = [
|
|
|
245
248
|
"[CALL_OPENING]",
|
|
246
249
|
"[CALL_OPENING_ACK]",
|
|
247
250
|
"[END_CALL]",
|
|
251
|
+
TASK_STOP_MARKER,
|
|
248
252
|
"[0]",
|
|
249
253
|
"[1]",
|
|
250
254
|
"[-1]",
|
|
@@ -354,12 +358,14 @@ export function createControlMarkerHoldback(
|
|
|
354
358
|
|
|
355
359
|
/**
|
|
356
360
|
* A session control a live-voice reply asked for with a terminal marker:
|
|
357
|
-
* `end` from {@link END_CALL_MARKER}, `
|
|
358
|
-
*
|
|
359
|
-
* `
|
|
361
|
+
* `end` from {@link END_CALL_MARKER}, `task_stop` from
|
|
362
|
+
* {@link TASK_STOP_MARKER}, `mute` from {@link MUTE_MARKER} or its timed form,
|
|
363
|
+
* `updates` from the progress-cadence markers, and the look actions from the
|
|
364
|
+
* look markers.
|
|
360
365
|
*/
|
|
361
366
|
export type SessionControlRequest =
|
|
362
367
|
| { readonly action: "end" }
|
|
368
|
+
| { readonly action: "task_stop" }
|
|
363
369
|
| { readonly action: "mute"; readonly durationMs?: number }
|
|
364
370
|
| { readonly action: "updates"; readonly cadence: "fewer" | "normal" }
|
|
365
371
|
| { readonly action: "look_screen" }
|
|
@@ -367,7 +373,7 @@ export type SessionControlRequest =
|
|
|
367
373
|
| { readonly action: "look_stop" };
|
|
368
374
|
|
|
369
375
|
const TERMINAL_SESSION_CONTROL_REGEX =
|
|
370
|
-
/(\[END_CALL\]|\[UPDATES:(FEWER|NORMAL)\]|\[LOOK:(SCREEN|CAMERA|STOP)\]|\[MUTE\]|\[MUTE:\s*([^\]]*)\])\s*$/;
|
|
376
|
+
/(\[END_CALL\]|\[TASK:STOP\]|\[UPDATES:(FEWER|NORMAL)\]|\[LOOK:(SCREEN|CAMERA|STOP)\]|\[MUTE\]|\[MUTE:\s*([^\]]*)\])\s*$/;
|
|
371
377
|
|
|
372
378
|
/**
|
|
373
379
|
* The session control a reply ends with, or null.
|
|
@@ -392,6 +398,9 @@ export function parseTerminalSessionControl(
|
|
|
392
398
|
if (match[1] === END_CALL_MARKER) {
|
|
393
399
|
return { action: "end" };
|
|
394
400
|
}
|
|
401
|
+
if (match[1] === TASK_STOP_MARKER) {
|
|
402
|
+
return { action: "task_stop" };
|
|
403
|
+
}
|
|
395
404
|
if (match[2] !== undefined) {
|
|
396
405
|
return {
|
|
397
406
|
action: "updates",
|
|
@@ -200,7 +200,10 @@ function frontDoorRuleWithDigest(
|
|
|
200
200
|
function routingLegRuleFor(
|
|
201
201
|
opts: Pick<
|
|
202
202
|
VoiceTurnOptions,
|
|
203
|
-
|
|
203
|
+
| "routingLeg"
|
|
204
|
+
| "unifiedVerdict"
|
|
205
|
+
| "spokenEscalationBridge"
|
|
206
|
+
| "directEscalated"
|
|
204
207
|
>,
|
|
205
208
|
callerUtterance: string,
|
|
206
209
|
): string | null {
|
|
@@ -211,7 +214,9 @@ function routingLegRuleFor(
|
|
|
211
214
|
callerUtterance,
|
|
212
215
|
);
|
|
213
216
|
case "escalated":
|
|
214
|
-
return
|
|
217
|
+
return opts.directEscalated === true
|
|
218
|
+
? null
|
|
219
|
+
: escalatedContinuationRule(opts.spokenEscalationBridge);
|
|
215
220
|
default:
|
|
216
221
|
return null;
|
|
217
222
|
}
|
|
@@ -358,6 +363,7 @@ export interface VoiceRunEventSink {
|
|
|
358
363
|
toolName: string,
|
|
359
364
|
input: Record<string, unknown>,
|
|
360
365
|
toolUseId?: string,
|
|
366
|
+
allowedToolNames?: ReadonlySet<string>,
|
|
361
367
|
): void;
|
|
362
368
|
onToolResult(event: VoiceToolResultEvent): void;
|
|
363
369
|
}
|
|
@@ -372,7 +378,11 @@ export interface VoiceTurnCallbacks {
|
|
|
372
378
|
/** Fired when the agent run starts a definitive tool use this turn. */
|
|
373
379
|
tool_use_start?: (
|
|
374
380
|
toolName: string,
|
|
375
|
-
detail?: {
|
|
381
|
+
detail?: {
|
|
382
|
+
toolUseId?: string;
|
|
383
|
+
input?: Record<string, unknown>;
|
|
384
|
+
allowedToolNames?: ReadonlySet<string>;
|
|
385
|
+
},
|
|
376
386
|
) => void;
|
|
377
387
|
/** Fired when a tool invocation finishes. */
|
|
378
388
|
tool_result?: (event: VoiceToolResultEvent) => void;
|
|
@@ -533,6 +543,8 @@ export interface VoiceTurnOptions {
|
|
|
533
543
|
* Only meaningful with `routingLeg: "escalated"`.
|
|
534
544
|
*/
|
|
535
545
|
spokenEscalationBridge?: string;
|
|
546
|
+
/** Run the strong leg directly, without claiming a holding phrase was spoken. */
|
|
547
|
+
directEscalated?: boolean;
|
|
536
548
|
/**
|
|
537
549
|
* Marks this turn's `content` as an internal instruction rather than user
|
|
538
550
|
* speech: it persists `hidden` so `/messages` filters it after a reload,
|
|
@@ -921,9 +933,13 @@ export async function startVoiceTurn(
|
|
|
921
933
|
onError: (message) => {
|
|
922
934
|
opts.onError?.(message);
|
|
923
935
|
},
|
|
924
|
-
onToolUse: (toolName, input, toolUseId) => {
|
|
936
|
+
onToolUse: (toolName, input, toolUseId, allowedToolNames) => {
|
|
925
937
|
log.debug({ toolName, input }, "Voice turn tool_use event");
|
|
926
|
-
opts.callbacks?.tool_use_start?.(toolName, {
|
|
938
|
+
opts.callbacks?.tool_use_start?.(toolName, {
|
|
939
|
+
toolUseId,
|
|
940
|
+
input,
|
|
941
|
+
...(allowedToolNames !== undefined ? { allowedToolNames } : {}),
|
|
942
|
+
});
|
|
927
943
|
},
|
|
928
944
|
onToolResult: (event) => {
|
|
929
945
|
opts.callbacks?.tool_result?.(event);
|
|
@@ -2081,7 +2097,12 @@ export async function startVoiceTurn(
|
|
|
2081
2097
|
} else if (msg.type === "conversation_error") {
|
|
2082
2098
|
eventSink.onError(msg.userMessage);
|
|
2083
2099
|
} else if (msg.type === "tool_use_start") {
|
|
2084
|
-
eventSink.onToolUse(
|
|
2100
|
+
eventSink.onToolUse(
|
|
2101
|
+
msg.toolName,
|
|
2102
|
+
msg.input,
|
|
2103
|
+
msg.toolUseId,
|
|
2104
|
+
conversation.allowedToolNames,
|
|
2105
|
+
);
|
|
2085
2106
|
} else if (msg.type === "tool_result") {
|
|
2086
2107
|
eventSink.onToolResult({
|
|
2087
2108
|
toolName: msg.toolName,
|
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
InstallPluginOptions,
|
|
38
38
|
InstallPluginResult,
|
|
39
39
|
} from "../../lib/install-from-github.js";
|
|
40
|
+
import type { PluginSearchMatch } from "../../lib/search-plugins.js";
|
|
40
41
|
import type {
|
|
41
42
|
PluginUpgradeResult,
|
|
42
43
|
UpgradePluginDeps,
|
|
@@ -63,6 +64,7 @@ let stageFixture: ((stagingDir: string) => void) | null = null;
|
|
|
63
64
|
let installPluginCalls: InstallPluginOptions[] = [];
|
|
64
65
|
let platformInstallCalls: Array<{ name: string; force?: boolean }> = [];
|
|
65
66
|
let upgradePluginCalls: UpgradePluginOptions[] = [];
|
|
67
|
+
let catalogMatches: PluginSearchMatch[] = [];
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* Queued daemon IPC responses. The default (empty queue) is a transport
|
|
@@ -159,6 +161,10 @@ mock.module("../../lib/install-from-platform.js", () => ({
|
|
|
159
161
|
},
|
|
160
162
|
}));
|
|
161
163
|
|
|
164
|
+
mock.module("../../lib/plugin-catalog-cache.js", () => ({
|
|
165
|
+
getPluginCatalog: async () => ({ ref: "main", matches: catalogMatches }),
|
|
166
|
+
}));
|
|
167
|
+
|
|
162
168
|
mock.module("../../lib/inspect-plugin.js", () => ({
|
|
163
169
|
...realInspect,
|
|
164
170
|
inspectPlugin: async () => {
|
|
@@ -257,6 +263,7 @@ beforeEach(() => {
|
|
|
257
263
|
installPluginCalls = [];
|
|
258
264
|
platformInstallCalls = [];
|
|
259
265
|
upgradePluginCalls = [];
|
|
266
|
+
catalogMatches = [];
|
|
260
267
|
ipcResults = [];
|
|
261
268
|
inspectResult = null;
|
|
262
269
|
installTarget = null;
|
|
@@ -433,6 +440,32 @@ describe("plugins install - declared-schedules consent", () => {
|
|
|
433
440
|
expect(r.stdout).toContain('Plugin "caveman" declares 2 schedules:');
|
|
434
441
|
expect(r.exitCode).toBe(0);
|
|
435
442
|
});
|
|
443
|
+
|
|
444
|
+
test("a local catalog source bypasses the platform installer", async () => {
|
|
445
|
+
catalogMatches = [
|
|
446
|
+
{
|
|
447
|
+
name: "fathom",
|
|
448
|
+
path: "local:plugins/mcp-catalog/fathom@1.0.0",
|
|
449
|
+
category: "productivity",
|
|
450
|
+
source: {
|
|
451
|
+
kind: "local",
|
|
452
|
+
path: "plugins/mcp-catalog/fathom",
|
|
453
|
+
version: "1.0.0",
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
const r = await runCommand(["plugins", "install", "fathom"]);
|
|
459
|
+
|
|
460
|
+
expect(platformInstallCalls).toHaveLength(0);
|
|
461
|
+
expect(installPluginCalls).toHaveLength(1);
|
|
462
|
+
expect(installPluginCalls[0]?.trustedSource).toEqual({
|
|
463
|
+
kind: "local",
|
|
464
|
+
path: "plugins/mcp-catalog/fathom",
|
|
465
|
+
version: "1.0.0",
|
|
466
|
+
});
|
|
467
|
+
expect(r.exitCode).toBe(0);
|
|
468
|
+
});
|
|
436
469
|
});
|
|
437
470
|
|
|
438
471
|
describe("plugins upgrade - declared-schedules consent (local fallback)", () => {
|
|
@@ -701,6 +734,39 @@ describe("plugins inspect - schedules surface", () => {
|
|
|
701
734
|
expect(r.stdout).toContain("weekly RRULE:FREQ=WEEKLY;BYDAY=MO (execute)");
|
|
702
735
|
expect(r.exitCode).toBe(0);
|
|
703
736
|
});
|
|
737
|
+
|
|
738
|
+
test("renders a bundled package location without a GitHub URL", async () => {
|
|
739
|
+
inspectResult = {
|
|
740
|
+
...inspectionWithSurfaces({
|
|
741
|
+
skills: [],
|
|
742
|
+
hooks: [],
|
|
743
|
+
tools: [],
|
|
744
|
+
schedules: [],
|
|
745
|
+
}),
|
|
746
|
+
installed: false,
|
|
747
|
+
status: "not-installed",
|
|
748
|
+
local: null,
|
|
749
|
+
remote: {
|
|
750
|
+
kind: "local",
|
|
751
|
+
repo: "",
|
|
752
|
+
path: "plugins/mcp-catalog/fathom",
|
|
753
|
+
commit: "1.0.0",
|
|
754
|
+
committedAt: null,
|
|
755
|
+
description: null,
|
|
756
|
+
homepage: null,
|
|
757
|
+
license: null,
|
|
758
|
+
category: "productivity",
|
|
759
|
+
marketplaceRef: "main",
|
|
760
|
+
version: "1.0.0",
|
|
761
|
+
},
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const r = await runCommand(["plugins", "inspect", "fathom"]);
|
|
765
|
+
|
|
766
|
+
expect(r.stdout).toContain("bundled:plugins/mcp-catalog/fathom");
|
|
767
|
+
expect(r.stdout).not.toContain("https://github.com//");
|
|
768
|
+
expect(r.exitCode).toBe(0);
|
|
769
|
+
});
|
|
704
770
|
});
|
|
705
771
|
|
|
706
772
|
describe("plugins install - setup skill hint", () => {
|
|
@@ -235,11 +235,29 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
235
235
|
// pinned commit. With platform features disabled (air-gapped /
|
|
236
236
|
// self-hosted) there is no platform to call, so resolve the pin
|
|
237
237
|
// from the bundled catalog and install through the GitHub path.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
const platformEnabled =
|
|
239
|
+
libs.catalogLocal.arePlatformFeaturesEnabled();
|
|
240
|
+
if (platformEnabled) {
|
|
241
|
+
const match = (
|
|
242
|
+
await libs.catalogCache.getPluginCatalog(DEFAULT_PLUGIN_REF, {
|
|
243
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
244
|
+
})
|
|
245
|
+
).matches.find((candidate) => candidate.name === nameOrUrl);
|
|
246
|
+
if (match?.source.kind === "local") {
|
|
247
|
+
result = await libs.installGitHub.installPlugin(
|
|
248
|
+
{
|
|
249
|
+
name: nameOrUrl,
|
|
250
|
+
force: opts.force,
|
|
251
|
+
trustedSource: match.source,
|
|
252
|
+
},
|
|
253
|
+
{ fetch: globalThis.fetch.bind(globalThis), confirmStaged },
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
result = await libs.installPlatform.installPluginViaPlatform(
|
|
257
|
+
{ name: nameOrUrl, force: opts.force },
|
|
258
|
+
{ fetch: globalThis.fetch.bind(globalThis), confirmStaged },
|
|
259
|
+
);
|
|
260
|
+
}
|
|
243
261
|
} else {
|
|
244
262
|
const source =
|
|
245
263
|
libs.catalogLocal.resolveBundledPluginSource(nameOrUrl);
|
|
@@ -250,19 +268,30 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
250
268
|
process.exitCode = 1;
|
|
251
269
|
return;
|
|
252
270
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
repo: source.repo,
|
|
260
|
-
rootPath: source.path,
|
|
261
|
-
ref: source.ref,
|
|
271
|
+
if (source.kind === "local") {
|
|
272
|
+
result = await libs.installGitHub.installPlugin(
|
|
273
|
+
{
|
|
274
|
+
name: nameOrUrl,
|
|
275
|
+
force: opts.force,
|
|
276
|
+
trustedSource: source,
|
|
262
277
|
},
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
278
|
+
{ fetch: globalThis.fetch.bind(globalThis), confirmStaged },
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
result = await libs.installGitHub.installPlugin(
|
|
282
|
+
{
|
|
283
|
+
name: nameOrUrl,
|
|
284
|
+
force: opts.force,
|
|
285
|
+
trustedSource: {
|
|
286
|
+
owner: source.owner,
|
|
287
|
+
repo: source.repo,
|
|
288
|
+
rootPath: source.path,
|
|
289
|
+
ref: source.ref,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{ fetch: globalThis.fetch.bind(globalThis), confirmStaged },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
266
295
|
}
|
|
267
296
|
} else {
|
|
268
297
|
const installOpts = direct
|
|
@@ -1390,8 +1419,11 @@ function driftLine(changes: FingerprintComparison | null): string {
|
|
|
1390
1419
|
return parts.join(", ");
|
|
1391
1420
|
}
|
|
1392
1421
|
|
|
1393
|
-
/** Build the
|
|
1422
|
+
/** Build the display location for a bundled package or GitHub pin. */
|
|
1394
1423
|
function remoteLocation(remote: PluginRemoteInfo): string {
|
|
1424
|
+
if (remote.kind === "local") {
|
|
1425
|
+
return `bundled:${remote.path}`;
|
|
1426
|
+
}
|
|
1395
1427
|
const base = `https://github.com/${remote.repo}`;
|
|
1396
1428
|
return remote.path ? `${base}/tree/${remote.commit}/${remote.path}` : base;
|
|
1397
1429
|
}
|
|
@@ -253,6 +253,73 @@ describe("installPlugin — install lifecycle", () => {
|
|
|
253
253
|
rmSync(ws, { recursive: true, force: true });
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
+
test("installs a bundled standard package and discovers its MCP server", async () => {
|
|
257
|
+
const result = await installPlugin(
|
|
258
|
+
{
|
|
259
|
+
name: "fathom",
|
|
260
|
+
trustedSource: {
|
|
261
|
+
kind: "local",
|
|
262
|
+
path: "plugins/mcp-catalog/fathom",
|
|
263
|
+
version: "1.0.0",
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
fetch: (async () => {
|
|
268
|
+
throw new Error("local package install must not fetch");
|
|
269
|
+
}) as FetchLike,
|
|
270
|
+
runGit: unusedGitRunner,
|
|
271
|
+
workspacePluginsDir: pluginsDir,
|
|
272
|
+
materializeLocalPackage: (path, version, destination) => {
|
|
273
|
+
expect(path).toBe("plugins/mcp-catalog/fathom");
|
|
274
|
+
expect(version).toBe("1.0.0");
|
|
275
|
+
writeFileSync(
|
|
276
|
+
join(destination, "plugin.json"),
|
|
277
|
+
JSON.stringify({
|
|
278
|
+
$schema:
|
|
279
|
+
"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
280
|
+
name: "fathom",
|
|
281
|
+
version: "1.0.0",
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
writeFileSync(
|
|
285
|
+
join(destination, "mcp.json"),
|
|
286
|
+
JSON.stringify({
|
|
287
|
+
$schema:
|
|
288
|
+
"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
289
|
+
mcpServers: {
|
|
290
|
+
fathom: {
|
|
291
|
+
type: "streamable-http",
|
|
292
|
+
url: "https://api.fathom.ai/mcp",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
return 2;
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(result).toMatchObject({
|
|
303
|
+
name: "fathom",
|
|
304
|
+
fileCount: 2,
|
|
305
|
+
ref: "1.0.0",
|
|
306
|
+
commit: null,
|
|
307
|
+
});
|
|
308
|
+
expect(readInstallMeta(result.target)?.source).toEqual({
|
|
309
|
+
kind: "local",
|
|
310
|
+
path: "plugins/mcp-catalog/fathom",
|
|
311
|
+
version: "1.0.0",
|
|
312
|
+
});
|
|
313
|
+
expect(
|
|
314
|
+
readPluginMcpServers({ workspacePluginsDir: pluginsDir }).servers,
|
|
315
|
+
).toEqual([
|
|
316
|
+
expect.objectContaining({
|
|
317
|
+
pluginName: "fathom",
|
|
318
|
+
serverKey: "fathom",
|
|
319
|
+
}),
|
|
320
|
+
]);
|
|
321
|
+
});
|
|
322
|
+
|
|
256
323
|
test("refuses to overwrite an existing install without --force", async () => {
|
|
257
324
|
// GIVEN a plugin already installed at <pluginsDir>/caveman
|
|
258
325
|
const target = join(pluginsDir, "caveman");
|