@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.7-dev.202607101252.74ea83d",
3
+ "version": "0.10.7-dev.202607101343.e727d4c",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
@@ -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 supply the pre-sanitized text and a use-case discriminator;
7
- * provider selection is always global — per-use-case policy only gates
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
- /** Pre-sanitized text to speak. */
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: options.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",