@vellumai/assistant 0.10.7-dev.202607101252.74ea83d → 0.10.7-dev.202607101343.e727d4c
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/package.json
CHANGED
|
@@ -78,14 +78,14 @@ function makeContentsFetch(opts: {
|
|
|
78
78
|
const prefix = apiPath ? `${apiPath}/` : "";
|
|
79
79
|
const direct = new Map<string, boolean>();
|
|
80
80
|
for (const key of Object.keys(tree)) {
|
|
81
|
-
if (!key.startsWith(prefix)) continue;
|
|
81
|
+
if (!key.startsWith(prefix)) {continue;}
|
|
82
82
|
const remainder = key.slice(prefix.length);
|
|
83
|
-
if (!remainder) continue;
|
|
83
|
+
if (!remainder) {continue;}
|
|
84
84
|
const seg = remainder.split("/")[0]!;
|
|
85
85
|
const isDir = remainder.includes("/") || tree[`${prefix}${seg}`] === null;
|
|
86
86
|
direct.set(seg, (direct.get(seg) ?? false) || isDir);
|
|
87
87
|
}
|
|
88
|
-
if (direct.size === 0) return null;
|
|
88
|
+
if (direct.size === 0) {return null;}
|
|
89
89
|
return Array.from(direct.entries()).map(([name, isDir]) => {
|
|
90
90
|
const path = `${prefix}${name}`;
|
|
91
91
|
return isDir
|
|
@@ -157,7 +157,7 @@ function fakeGitRunner(opts: {
|
|
|
157
157
|
opts.calls?.push([...args]);
|
|
158
158
|
switch (args[0]) {
|
|
159
159
|
case "fetch": {
|
|
160
|
-
if (opts.fetchError) throw opts.fetchError;
|
|
160
|
+
if (opts.fetchError) {throw opts.fetchError;}
|
|
161
161
|
mkdirSync(join(cwd, ".git"), { recursive: true });
|
|
162
162
|
writeFileSync(join(cwd, ".git", "config"), "[core]\n");
|
|
163
163
|
for (const [rel, content] of Object.entries(opts.tree)) {
|
|
@@ -1097,6 +1097,38 @@ describe("installPlugin — direct (untrusted) install", () => {
|
|
|
1097
1097
|
).rejects.toBeInstanceOf(PluginSourceUnavailableError);
|
|
1098
1098
|
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
1099
1099
|
});
|
|
1100
|
+
|
|
1101
|
+
test("synthesizes a minimal package.json when the upstream repo ships none", async () => {
|
|
1102
|
+
// GIVEN a plugin tree with no package.json at all
|
|
1103
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1104
|
+
const runGit = fakeGitRunner({
|
|
1105
|
+
tree: { "hooks/init.ts": "//", "README.md": "# bare" },
|
|
1106
|
+
commit: "f".repeat(40),
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// WHEN we install directly
|
|
1110
|
+
await installPlugin(
|
|
1111
|
+
{
|
|
1112
|
+
name: "bare-plugin",
|
|
1113
|
+
directSource: {
|
|
1114
|
+
owner: "owner",
|
|
1115
|
+
repo: "bare",
|
|
1116
|
+
rootPath: "",
|
|
1117
|
+
ref: "HEAD",
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
// THEN a minimal package.json is synthesized with the install name and
|
|
1124
|
+
// the default plugin-api peer dependency range
|
|
1125
|
+
const pkgPath = join(pluginsDir, "bare-plugin", "package.json");
|
|
1126
|
+
expect(existsSync(pkgPath)).toBe(true);
|
|
1127
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1128
|
+
expect(pkg.name).toBe("bare-plugin");
|
|
1129
|
+
expect(pkg.version).toBe("0.0.0");
|
|
1130
|
+
expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeDefined();
|
|
1131
|
+
});
|
|
1100
1132
|
});
|
|
1101
1133
|
|
|
1102
1134
|
describe("sanitizePluginName", () => {
|
|
@@ -714,10 +714,23 @@ export async function materializePluginTree(
|
|
|
714
714
|
// An external clone is often a foreign-ecosystem plugin (e.g. a Claude Code
|
|
715
715
|
// plugin) that the Vellum loader can't run as-is. When we curate an adapter
|
|
716
716
|
// stub for it, overlay the stub and run its transform so the materialized
|
|
717
|
-
// tree is a valid Vellum plugin. Raw clones (no stub) are left untouched
|
|
717
|
+
// tree is a valid Vellum plugin. Raw clones (no stub) are left untouched,
|
|
718
|
+
// except for a minimal package.json synthesis when the upstream repo shipped
|
|
719
|
+
// none — the Vellum loader hard-requires one and would silently skip the
|
|
720
|
+
// plugin without it. The synthesis runs only for direct installs
|
|
721
|
+
// (stubRef === null); the upgrade path re-materializes baselines through
|
|
722
|
+
// this function with a non-null stubRef, and synthesizing a package.json
|
|
723
|
+
// not present at install time would corrupt the fingerprint comparison.
|
|
718
724
|
if (cloned.fileCount > 0 && opts.stubRef !== null) {
|
|
719
725
|
await applyAdapterStub(opts.name, opts.stubRef, opts.destDir, deps);
|
|
720
726
|
}
|
|
727
|
+
if (
|
|
728
|
+
cloned.fileCount > 0 &&
|
|
729
|
+
opts.stubRef === null &&
|
|
730
|
+
!existsSync(join(opts.destDir, "package.json"))
|
|
731
|
+
) {
|
|
732
|
+
synthesizeMinimalPackageJson(opts.name, opts.destDir);
|
|
733
|
+
}
|
|
721
734
|
return cloned;
|
|
722
735
|
}
|
|
723
736
|
|
|
@@ -974,6 +987,34 @@ function normalizeInstalledManifest(
|
|
|
974
987
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
975
988
|
}
|
|
976
989
|
|
|
990
|
+
/**
|
|
991
|
+
* Write a minimal Vellum-compatible `package.json` into a staged plugin that
|
|
992
|
+
* shipped no manifest of its own. The Vellum external plugin loader
|
|
993
|
+
* (`buildPluginFromDir`) hard-requires a `package.json` validated against
|
|
994
|
+
* `PluginPackageJsonSchema` and silently skips the plugin when it's missing.
|
|
995
|
+
*
|
|
996
|
+
* The synthesized manifest carries the install name and the default
|
|
997
|
+
* `@vellumai/plugin-api` peer dependency range. No foreign-ecosystem manifest
|
|
998
|
+
* data is read — the install name is the only identity we trust for an
|
|
999
|
+
* untrusted direct install.
|
|
1000
|
+
*/
|
|
1001
|
+
function synthesizeMinimalPackageJson(
|
|
1002
|
+
name: string,
|
|
1003
|
+
stagingDir: string,
|
|
1004
|
+
): void {
|
|
1005
|
+
const manifestPath = join(stagingDir, "package.json");
|
|
1006
|
+
|
|
1007
|
+
const manifest: PackageManifest = {
|
|
1008
|
+
name,
|
|
1009
|
+
version: "0.0.0",
|
|
1010
|
+
peerDependencies: {
|
|
1011
|
+
"@vellumai/plugin-api": PLUGIN_API_PEER_RANGE,
|
|
1012
|
+
},
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
977
1018
|
/**
|
|
978
1019
|
* Resolve the absolute path of the adapter script named by the (overlaid stub)
|
|
979
1020
|
* `package.json`'s `scripts.postinstall`, or `null` when there is no stub
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -235,3 +235,12 @@ export {
|
|
|
235
235
|
syncMessageToDisk,
|
|
236
236
|
updateMessageMetadata,
|
|
237
237
|
} from "../persistence/conversation-plugin-facade.js";
|
|
238
|
+
// Synthesize text to speech through the assistant's globally configured TTS
|
|
239
|
+
// provider (ElevenLabs, Fish Audio, etc.). Plugins that need voice output —
|
|
240
|
+
// e.g. a meeting bot speaking into a live call — use this instead of managing
|
|
241
|
+
// TTS credentials and provider config themselves. Returns a Buffer + MIME type.
|
|
242
|
+
// Text is sanitized internally (markdown/URLs/emoji stripped) so callers can
|
|
243
|
+
// pass raw model output directly.
|
|
244
|
+
export type { SynthesizeTextOptions } from "../tts/synthesize-text.js";
|
|
245
|
+
export { synthesizeText, TtsSynthesisError } from "../tts/synthesize-text.js";
|
|
246
|
+
export type { TtsSynthesisResult } from "../tts/types.js";
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Resolves the globally configured provider via {@link resolveTtsConfig},
|
|
5
5
|
* looks up the provider adapter in the registry, and delegates synthesis.
|
|
6
|
-
* Callers
|
|
7
|
-
*
|
|
6
|
+
* Callers can pass raw text (including markdown, URLs, emoji); it is sanitized
|
|
7
|
+
* internally via {@link sanitizeForTts} before being sent to the provider.
|
|
8
|
+
* Provider selection is always global — per-use-case policy only gates
|
|
8
9
|
* capabilities (e.g. format checks), never overrides the chosen provider.
|
|
9
10
|
*/
|
|
10
11
|
|
|
12
|
+
import { sanitizeForTts } from "../calls/tts-text-sanitizer.js";
|
|
11
13
|
import { getConfig } from "../config/loader.js";
|
|
12
14
|
import { getLogger } from "../util/logger.js";
|
|
13
15
|
import { getTtsProvider } from "./provider-catalog.js";
|
|
@@ -21,7 +23,7 @@ const log = getLogger("tts:synthesize");
|
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
22
24
|
|
|
23
25
|
export interface SynthesizeTextOptions {
|
|
24
|
-
/**
|
|
26
|
+
/** Text to speak. Sanitized internally — callers can pass raw model output. */
|
|
25
27
|
text: string;
|
|
26
28
|
|
|
27
29
|
/** Product surface requesting synthesis. */
|
|
@@ -72,6 +74,14 @@ export async function synthesizeText(
|
|
|
72
74
|
const config = getConfig();
|
|
73
75
|
const { provider: providerId } = resolveTtsConfig(config);
|
|
74
76
|
|
|
77
|
+
const sanitized = sanitizeForTts(options.text).trim();
|
|
78
|
+
if (!sanitized) {
|
|
79
|
+
throw new TtsSynthesisError(
|
|
80
|
+
"TTS_SYNTHESIS_FAILED",
|
|
81
|
+
"Text is empty after sanitization (markdown/URLs/emoji stripped).",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
let provider;
|
|
76
86
|
try {
|
|
77
87
|
provider = getTtsProvider(providerId);
|
|
@@ -93,14 +103,14 @@ export async function synthesizeText(
|
|
|
93
103
|
|
|
94
104
|
try {
|
|
95
105
|
return await provider.synthesize({
|
|
96
|
-
text:
|
|
106
|
+
text: sanitized,
|
|
97
107
|
useCase: options.useCase,
|
|
98
108
|
voiceId: options.voiceId,
|
|
99
109
|
signal: options.signal,
|
|
100
110
|
});
|
|
101
111
|
} catch (err) {
|
|
102
112
|
// Re-throw TtsSynthesisError as-is (e.g. from inner adapter errors).
|
|
103
|
-
if (err instanceof TtsSynthesisError) throw err;
|
|
113
|
+
if (err instanceof TtsSynthesisError) {throw err;}
|
|
104
114
|
|
|
105
115
|
throw new TtsSynthesisError(
|
|
106
116
|
"TTS_SYNTHESIS_FAILED",
|