@verbatra/sdk 0.9.0 → 0.9.3

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/dist/index.d.cts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  import Anthropic from '@anthropic-ai/sdk';
3
- import { Interactions } from '@google/genai';
4
3
  import OpenAI from 'openai';
5
4
 
6
5
  /**
@@ -236,12 +235,22 @@ interface TranslationProvider {
236
235
  type AnthropicModel = Anthropic.Messages.Model;
237
236
 
238
237
  /**
239
- * The Gemini authoring model type, sourced from the installed SDK's own model union so
240
- * verbatra restates no model IDs. It is an open union, so unknown model IDs are still
241
- * accepted. Type-only: it informs editor completions and is never validated at runtime,
242
- * where the schema stays `z.string().min(1)`.
243
- */
244
- type GeminiModel = Interactions.Model;
238
+ * The Gemini authoring model type. Unlike the sibling Anthropic and OpenAI model types, this
239
+ * union is hand-maintained rather than re-exported from `@google/genai`: every entry point of
240
+ * that package's `.d.ts` carries an unconditional top-level import of
241
+ * `@modelcontextprotocol/sdk/client/index.js`, an optional peer dependency it does not install.
242
+ * Re-exporting any type from that package, including its model union, drags that import into
243
+ * this package's own published types and breaks `tsc --noEmit` with `skipLibCheck: false` for
244
+ * any consumer who has not separately installed the MCP SDK. Keeping this list literal avoids
245
+ * that transitive dependency entirely.
246
+ *
247
+ * It is an open union (`(string & {})` at the end), so unknown or newly released model IDs are
248
+ * still accepted. Type-only: it informs editor completions and is never validated at runtime,
249
+ * where the schema stays `z.string().min(1)`. The list is current as of `@google/genai` 2.17.1
250
+ * and does not update automatically; refresh it by hand when adopting a newer SDK version that
251
+ * adds models worth completing on.
252
+ */
253
+ type GeminiModel = "gemini-2.5-flash" | "gemini-2.5-pro" | "gemma-4-26b-a4b-it" | "gemma-4-31b-it" | "gemini-flash-latest" | "gemini-flash-lite-latest" | "gemini-pro-latest" | "gemini-2.5-flash-lite" | "gemini-2.5-flash-image" | "gemini-3-flash-preview" | "gemini-3.1-pro-preview" | "gemini-3.1-pro-preview-customtools" | "gemini-3.1-flash-lite" | "gemini-3-pro-image" | "nano-banana-pro-preview" | "gemini-3.1-flash-image" | "gemini-3.5-flash" | "gemini-3.6-flash" | "gemini-3.7-flash" | "lyria-3-clip-preview" | "lyria-3-pro-preview" | "gemini-robotics-er-1.6-preview" | "gemini-robotics-er-2-preview" | (string & {});
245
254
 
246
255
  /**
247
256
  * The OpenAI authoring model type, sourced from the OpenAI SDK's published chat-model union
@@ -795,8 +804,8 @@ interface LoadConfigOptions {
795
804
  */
796
805
  type ConfigSource = {
797
806
  /**
798
- * `search` means cosmiconfig found the file by walking up from the working directory;
799
- * `explicit` means the caller named it through `configPath`.
807
+ * `search` means cosmiconfig found the file by walking up from the working directory, up to
808
+ * the nearest `.git` directory; `explicit` means the caller named it through `configPath`.
800
809
  */
801
810
  readonly kind: "search" | "explicit";
802
811
  /** The absolute path of the config file that was loaded. */
@@ -821,12 +830,26 @@ interface LoadedConfig {
821
830
  *
822
831
  * Resolution order is: an explicit `configOverride`, then an explicit `configPath`, then a
823
832
  * cosmiconfig search upward from `cwd` across `verbatra.config.ts`, the `.verbatrarc` family, and a
824
- * `verbatra` property in `package.json`. A `configPath` that does not exist is an error rather than
825
- * a fallback to searching, so a typo in a path never silently loads a different project's config.
833
+ * `verbatra` property in `package.json`. The upward search stops at the nearest ancestor directory
834
+ * containing a `.git` entry; if none is found, it stops at the user's home directory when that is an
835
+ * ancestor of `cwd`, and otherwise it does not search above `cwd` at all, so a nested workspace
836
+ * package finds a config at its monorepo root without wandering above it, and a project outside the
837
+ * home directory tree (a CI checkout, for instance) never triggers an unbounded walk to the
838
+ * filesystem root. A config file outside that directory chain, including cosmiconfig's own OS-level
839
+ * global config directory, is never used, even though the underlying search strategy checks it; it is
840
+ * treated the same as no config found. A `configPath` that does not exist is an error rather than a
841
+ * fallback to searching, so a typo in a path never silently loads a different project's config.
826
842
  *
827
843
  * A glossary given as a path is read and validated here, so the returned config always carries a
828
844
  * resolved term map.
829
845
  *
846
+ * A `verbatra.config.ts` file is transpiled and loaded through jiti. When it imports `@verbatra/sdk`
847
+ * or `@verbatra/cli`, those bare specifiers are aliased to the package that is actually running this
848
+ * function, so the import resolves to the running version even when a different, conflicting version
849
+ * of either package also happens to be reachable from the config file's own location. A package is
850
+ * only aliased when its own entry point can be resolved from the running code; otherwise the import
851
+ * falls back to jiti's ordinary bare-specifier resolution, unchanged from before this behavior existed.
852
+ *
830
853
  * @param options - Where and how to look for the config.
831
854
  * @returns The validated config with its config-source and glossary provenance.
832
855
  *
@@ -841,8 +864,10 @@ declare function loadConfigWithMeta(options?: LoadConfigOptions): Promise<Loaded
841
864
  * pass the result to {@link translate}, {@link check}, {@link diff}, or any other entry point.
842
865
  *
843
866
  * Resolution order is an explicit `configOverride`, then an explicit `configPath`, then a
844
- * cosmiconfig search upward from `cwd`. A glossary declared as a file path is read and validated
845
- * here, so the returned {@link VerbatraConfig} always carries a resolved term map.
867
+ * cosmiconfig search upward from `cwd`, stopping at the nearest ancestor `.git` directory, or
868
+ * failing that, the user's home directory when it is an ancestor of `cwd`, or failing that, `cwd`
869
+ * itself. A glossary declared as a file path is read and validated here, so the returned
870
+ * {@link VerbatraConfig} always carries a resolved term map.
846
871
  *
847
872
  * Reach for {@link loadConfigWithMeta} when you also need to know which file was loaded.
848
873
  *
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  import Anthropic from '@anthropic-ai/sdk';
3
- import { Interactions } from '@google/genai';
4
3
  import OpenAI from 'openai';
5
4
 
6
5
  /**
@@ -236,12 +235,22 @@ interface TranslationProvider {
236
235
  type AnthropicModel = Anthropic.Messages.Model;
237
236
 
238
237
  /**
239
- * The Gemini authoring model type, sourced from the installed SDK's own model union so
240
- * verbatra restates no model IDs. It is an open union, so unknown model IDs are still
241
- * accepted. Type-only: it informs editor completions and is never validated at runtime,
242
- * where the schema stays `z.string().min(1)`.
243
- */
244
- type GeminiModel = Interactions.Model;
238
+ * The Gemini authoring model type. Unlike the sibling Anthropic and OpenAI model types, this
239
+ * union is hand-maintained rather than re-exported from `@google/genai`: every entry point of
240
+ * that package's `.d.ts` carries an unconditional top-level import of
241
+ * `@modelcontextprotocol/sdk/client/index.js`, an optional peer dependency it does not install.
242
+ * Re-exporting any type from that package, including its model union, drags that import into
243
+ * this package's own published types and breaks `tsc --noEmit` with `skipLibCheck: false` for
244
+ * any consumer who has not separately installed the MCP SDK. Keeping this list literal avoids
245
+ * that transitive dependency entirely.
246
+ *
247
+ * It is an open union (`(string & {})` at the end), so unknown or newly released model IDs are
248
+ * still accepted. Type-only: it informs editor completions and is never validated at runtime,
249
+ * where the schema stays `z.string().min(1)`. The list is current as of `@google/genai` 2.17.1
250
+ * and does not update automatically; refresh it by hand when adopting a newer SDK version that
251
+ * adds models worth completing on.
252
+ */
253
+ type GeminiModel = "gemini-2.5-flash" | "gemini-2.5-pro" | "gemma-4-26b-a4b-it" | "gemma-4-31b-it" | "gemini-flash-latest" | "gemini-flash-lite-latest" | "gemini-pro-latest" | "gemini-2.5-flash-lite" | "gemini-2.5-flash-image" | "gemini-3-flash-preview" | "gemini-3.1-pro-preview" | "gemini-3.1-pro-preview-customtools" | "gemini-3.1-flash-lite" | "gemini-3-pro-image" | "nano-banana-pro-preview" | "gemini-3.1-flash-image" | "gemini-3.5-flash" | "gemini-3.6-flash" | "gemini-3.7-flash" | "lyria-3-clip-preview" | "lyria-3-pro-preview" | "gemini-robotics-er-1.6-preview" | "gemini-robotics-er-2-preview" | (string & {});
245
254
 
246
255
  /**
247
256
  * The OpenAI authoring model type, sourced from the OpenAI SDK's published chat-model union
@@ -795,8 +804,8 @@ interface LoadConfigOptions {
795
804
  */
796
805
  type ConfigSource = {
797
806
  /**
798
- * `search` means cosmiconfig found the file by walking up from the working directory;
799
- * `explicit` means the caller named it through `configPath`.
807
+ * `search` means cosmiconfig found the file by walking up from the working directory, up to
808
+ * the nearest `.git` directory; `explicit` means the caller named it through `configPath`.
800
809
  */
801
810
  readonly kind: "search" | "explicit";
802
811
  /** The absolute path of the config file that was loaded. */
@@ -821,12 +830,26 @@ interface LoadedConfig {
821
830
  *
822
831
  * Resolution order is: an explicit `configOverride`, then an explicit `configPath`, then a
823
832
  * cosmiconfig search upward from `cwd` across `verbatra.config.ts`, the `.verbatrarc` family, and a
824
- * `verbatra` property in `package.json`. A `configPath` that does not exist is an error rather than
825
- * a fallback to searching, so a typo in a path never silently loads a different project's config.
833
+ * `verbatra` property in `package.json`. The upward search stops at the nearest ancestor directory
834
+ * containing a `.git` entry; if none is found, it stops at the user's home directory when that is an
835
+ * ancestor of `cwd`, and otherwise it does not search above `cwd` at all, so a nested workspace
836
+ * package finds a config at its monorepo root without wandering above it, and a project outside the
837
+ * home directory tree (a CI checkout, for instance) never triggers an unbounded walk to the
838
+ * filesystem root. A config file outside that directory chain, including cosmiconfig's own OS-level
839
+ * global config directory, is never used, even though the underlying search strategy checks it; it is
840
+ * treated the same as no config found. A `configPath` that does not exist is an error rather than a
841
+ * fallback to searching, so a typo in a path never silently loads a different project's config.
826
842
  *
827
843
  * A glossary given as a path is read and validated here, so the returned config always carries a
828
844
  * resolved term map.
829
845
  *
846
+ * A `verbatra.config.ts` file is transpiled and loaded through jiti. When it imports `@verbatra/sdk`
847
+ * or `@verbatra/cli`, those bare specifiers are aliased to the package that is actually running this
848
+ * function, so the import resolves to the running version even when a different, conflicting version
849
+ * of either package also happens to be reachable from the config file's own location. A package is
850
+ * only aliased when its own entry point can be resolved from the running code; otherwise the import
851
+ * falls back to jiti's ordinary bare-specifier resolution, unchanged from before this behavior existed.
852
+ *
830
853
  * @param options - Where and how to look for the config.
831
854
  * @returns The validated config with its config-source and glossary provenance.
832
855
  *
@@ -841,8 +864,10 @@ declare function loadConfigWithMeta(options?: LoadConfigOptions): Promise<Loaded
841
864
  * pass the result to {@link translate}, {@link check}, {@link diff}, or any other entry point.
842
865
  *
843
866
  * Resolution order is an explicit `configOverride`, then an explicit `configPath`, then a
844
- * cosmiconfig search upward from `cwd`. A glossary declared as a file path is read and validated
845
- * here, so the returned {@link VerbatraConfig} always carries a resolved term map.
867
+ * cosmiconfig search upward from `cwd`, stopping at the nearest ancestor `.git` directory, or
868
+ * failing that, the user's home directory when it is an ancestor of `cwd`, or failing that, `cwd`
869
+ * itself. A glossary declared as a file path is read and validated here, so the returned
870
+ * {@link VerbatraConfig} always carries a resolved term map.
846
871
  *
847
872
  * Reach for {@link loadConfigWithMeta} when you also need to know which file was loaded.
848
873
  *
package/dist/index.js CHANGED
@@ -2,12 +2,13 @@ import { dirname, resolve, join, basename, relative, isAbsolute, sep, extname }
2
2
  import { z } from 'zod';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { mkdir, rm, access, open, writeFile, rename } from 'fs/promises';
5
- import { existsSync } from 'fs';
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { homedir } from 'os';
6
7
  import { cosmiconfig } from 'cosmiconfig';
7
8
  import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
9
+ import { createRequire } from 'module';
8
10
  import Anthropic from '@anthropic-ai/sdk';
9
11
  import * as deepl from 'deepl-node';
10
- import { createRequire } from 'module';
11
12
  import log from 'loglevel';
12
13
  import { GoogleGenAI } from '@google/genai';
13
14
  import OpenAI from 'openai';
@@ -500,6 +501,67 @@ async function updateGlossaryTerm(input, deps = {}) {
500
501
  return entries;
501
502
  });
502
503
  }
504
+ var SELF_ALIASED_PACKAGES = ["@verbatra/sdk", "@verbatra/cli"];
505
+ var ENTRY_CONDITION_ORDER = ["import", "node", "default", "require"];
506
+ function resolvePathsFromRunningModule(packageName) {
507
+ return createRequire(import.meta.url).resolve.paths(packageName) ?? [];
508
+ }
509
+ function readPackageManifestFromDisk(packageRoot) {
510
+ try {
511
+ return JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
512
+ } catch {
513
+ return void 0;
514
+ }
515
+ }
516
+ var defaultModuleAliasDeps = {
517
+ resolvePaths: resolvePathsFromRunningModule,
518
+ readPackageManifest: readPackageManifestFromDisk
519
+ };
520
+ function resolveRootExportPath(manifest) {
521
+ const exportsField = manifest.exports;
522
+ if (typeof exportsField === "string") {
523
+ return exportsField;
524
+ }
525
+ if (typeof exportsField === "object" && exportsField !== null) {
526
+ const rootExport = exportsField["."];
527
+ if (typeof rootExport === "string") {
528
+ return rootExport;
529
+ }
530
+ if (typeof rootExport === "object" && rootExport !== null) {
531
+ for (const condition of ENTRY_CONDITION_ORDER) {
532
+ const candidate = rootExport[condition];
533
+ if (typeof candidate === "string") {
534
+ return candidate;
535
+ }
536
+ }
537
+ }
538
+ }
539
+ return manifest.main;
540
+ }
541
+ function findPackageEntry(packageName, deps) {
542
+ for (const candidateDir of deps.resolvePaths(packageName)) {
543
+ const packageRoot = join(candidateDir, packageName);
544
+ const manifest = deps.readPackageManifest(packageRoot);
545
+ if (manifest === void 0) {
546
+ continue;
547
+ }
548
+ const relativeEntry = resolveRootExportPath(manifest);
549
+ if (relativeEntry !== void 0) {
550
+ return join(packageRoot, relativeEntry);
551
+ }
552
+ }
553
+ return void 0;
554
+ }
555
+ function resolveSelfPackageAliases(deps = defaultModuleAliasDeps) {
556
+ const aliases = {};
557
+ for (const packageName of SELF_ALIASED_PACKAGES) {
558
+ const entry = findPackageEntry(packageName, deps);
559
+ if (entry !== void 0) {
560
+ aliases[packageName] = entry;
561
+ }
562
+ }
563
+ return aliases;
564
+ }
503
565
  async function resolveGlossary(glossary, baseDir, fs) {
504
566
  if (glossary === void 0) {
505
567
  return { glossary: void 0, provenance: { source: "none" } };
@@ -2164,6 +2226,50 @@ var SEARCH_PLACES = [
2164
2226
  `${MODULE_NAME}.config.cjs`,
2165
2227
  `${MODULE_NAME}.config.ts`
2166
2228
  ];
2229
+ function isAncestorOrSelf(ancestor, startDir) {
2230
+ const target = resolve(ancestor);
2231
+ let dir = resolve(startDir);
2232
+ while (true) {
2233
+ if (dir === target) {
2234
+ return true;
2235
+ }
2236
+ const parent = dirname(dir);
2237
+ if (parent === dir) {
2238
+ return false;
2239
+ }
2240
+ dir = parent;
2241
+ }
2242
+ }
2243
+ function findSearchStopDir(startDir) {
2244
+ let dir = resolve(startDir);
2245
+ while (true) {
2246
+ if (existsSync(join(dir, ".git"))) {
2247
+ return dir;
2248
+ }
2249
+ const parent = dirname(dir);
2250
+ if (parent === dir) {
2251
+ break;
2252
+ }
2253
+ dir = parent;
2254
+ }
2255
+ const home = resolve(homedir());
2256
+ return isAncestorOrSelf(home, startDir) ? home : resolve(startDir);
2257
+ }
2258
+ function collectSearchChain(startDir, stopDir) {
2259
+ const stop = resolve(stopDir);
2260
+ const chain = /* @__PURE__ */ new Set();
2261
+ let dir = resolve(startDir);
2262
+ while (dir !== stop) {
2263
+ chain.add(dir);
2264
+ const parent = dirname(dir);
2265
+ if (parent === dir) {
2266
+ break;
2267
+ }
2268
+ dir = parent;
2269
+ }
2270
+ chain.add(stop);
2271
+ return chain;
2272
+ }
2167
2273
  function formatIssues(error) {
2168
2274
  return error.issues.map((issue) => {
2169
2275
  const path = issue.path.join(".");
@@ -2213,20 +2319,30 @@ async function loadConfigWithMeta(options = {}) {
2213
2319
  const { config: config2, glossary: glossary2 } = await finalizeConfig(parsed2, options.cwd ?? process.cwd(), fs);
2214
2320
  return { config: config2, source: { kind: "override" }, glossary: glossary2 };
2215
2321
  }
2322
+ const cwd = options.cwd ?? process.cwd();
2323
+ const stopDir = findSearchStopDir(cwd);
2216
2324
  const explorer = cosmiconfig(MODULE_NAME, {
2217
2325
  searchPlaces: SEARCH_PLACES,
2218
- loaders: { ".ts": TypeScriptLoader() }
2326
+ loaders: { ".ts": TypeScriptLoader({ alias: resolveSelfPackageAliases() }) },
2327
+ searchStrategy: "global",
2328
+ stopDir
2219
2329
  });
2220
2330
  if (options.configPath !== void 0) {
2221
2331
  return loadExplicitWithMeta(explorer, options.configPath, options.cwd, fs);
2222
2332
  }
2223
2333
  let result;
2224
2334
  try {
2225
- result = await explorer.search(options.cwd);
2335
+ result = await explorer.search(cwd);
2226
2336
  } catch (error) {
2227
2337
  const detail = errorMessage(error);
2228
2338
  throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
2229
2339
  }
2340
+ if (result !== null && result.isEmpty !== true) {
2341
+ const resultDir = dirname(resolve(result.filepath));
2342
+ if (!collectSearchChain(cwd, stopDir).has(resultDir)) {
2343
+ result = null;
2344
+ }
2345
+ }
2230
2346
  if (result === null || result.isEmpty === true) {
2231
2347
  throw new SdkError(
2232
2348
  "CONFIG_NOT_FOUND",