tina4-nodejs 3.13.136 → 3.13.137

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/CLAUDE.md CHANGED
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
13
13
 
14
14
  The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
15
15
 
16
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.136)
16
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.137)
17
17
 
18
18
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
19
19
 
20
20
  ## What This Project Is
21
21
 
22
- Tina4 for Node.js/TypeScript v3.13.136 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
22
+ Tina4 for Node.js/TypeScript v3.13.137 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
23
23
 
24
24
  The philosophy: zero ceremony, batteries included, file system as source of truth.
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.136",
3
+ "version": "3.13.137",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - native TypeScript conventions and shared Tina4 contracts",
6
6
  "keywords": [
@@ -12388,6 +12388,10 @@ async function recordApplied(db, name, batch, passed = 1) {
12388
12388
  insertCols.push("migration_id");
12389
12389
  values.push(name);
12390
12390
  }
12391
+ if (cols.has("name")) {
12392
+ insertCols.push("name");
12393
+ values.push(name);
12394
+ }
12391
12395
  if (isFirebirdAdapter(db)) {
12392
12396
  const rows = await adapterQuery(
12393
12397
  db,
@@ -44706,13 +44710,14 @@ var init_aiClient = __esm({
44706
44710
  }
44707
44711
  static config(capability, options) {
44708
44712
  const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
44709
- if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
44713
+ if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
44710
44714
  const key = process.env.TINA4_AI_KEY || null;
44711
- if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
44715
+ if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
44712
44716
  const defaults = {
44713
44717
  local: ["http://localhost:11437", "llama3.2"],
44714
44718
  openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
44715
- anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
44719
+ anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
44720
+ gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"]
44716
44721
  };
44717
44722
  const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
44718
44723
  const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
@@ -44730,7 +44735,7 @@ var init_aiClient = __esm({
44730
44735
  }
44731
44736
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
44732
44737
  const path10 = url.pathname.replace(/\/+$/, "");
44733
- if (path10 === "" || path10 === "/v1" || path10 === "/api") {
44738
+ if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
44734
44739
  const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
44735
44740
  url.pathname = (path10 || "/v1") + suffix;
44736
44741
  }
@@ -44738,7 +44743,7 @@ var init_aiClient = __esm({
44738
44743
  }
44739
44744
  static headers(config) {
44740
44745
  const headers = { "content-type": "application/json", accept: "application/json" };
44741
- if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
44746
+ if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
44742
44747
  if (config.provider === "anthropic") {
44743
44748
  headers["x-api-key"] = config.key;
44744
44749
  headers["anthropic-version"] = "2023-06-01";
@@ -12387,6 +12387,10 @@ async function recordApplied(db, name, batch, passed = 1) {
12387
12387
  insertCols.push("migration_id");
12388
12388
  values.push(name);
12389
12389
  }
12390
+ if (cols.has("name")) {
12391
+ insertCols.push("name");
12392
+ values.push(name);
12393
+ }
12390
12394
  if (isFirebirdAdapter(db)) {
12391
12395
  const rows = await adapterQuery(
12392
12396
  db,
@@ -44667,13 +44671,14 @@ var init_aiClient = __esm({
44667
44671
  }
44668
44672
  static config(capability, options) {
44669
44673
  const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
44670
- if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
44674
+ if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
44671
44675
  const key = process.env.TINA4_AI_KEY || null;
44672
- if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
44676
+ if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
44673
44677
  const defaults = {
44674
44678
  local: ["http://localhost:11437", "llama3.2"],
44675
44679
  openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
44676
- anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
44680
+ anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
44681
+ gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"]
44677
44682
  };
44678
44683
  const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
44679
44684
  const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
@@ -44691,7 +44696,7 @@ var init_aiClient = __esm({
44691
44696
  }
44692
44697
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
44693
44698
  const path10 = url.pathname.replace(/\/+$/, "");
44694
- if (path10 === "" || path10 === "/v1" || path10 === "/api") {
44699
+ if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
44695
44700
  const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
44696
44701
  url.pathname = (path10 || "/v1") + suffix;
44697
44702
  }
@@ -44699,7 +44704,7 @@ var init_aiClient = __esm({
44699
44704
  }
44700
44705
  static headers(config) {
44701
44706
  const headers = { "content-type": "application/json", accept: "application/json" };
44702
- if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
44707
+ if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
44703
44708
  if (config.provider === "anthropic") {
44704
44709
  headers["x-api-key"] = config.key;
44705
44710
  headers["anthropic-version"] = "2023-06-01";
@@ -87,7 +87,7 @@ export interface AiChatOptions {
87
87
  maxTokens?: number;
88
88
  stream?: boolean;
89
89
  timeout?: number;
90
- provider?: "local" | "openai" | "anthropic";
90
+ provider?: "local" | "openai" | "anthropic" | "gemini";
91
91
  /** Tools the model may call. ADR-0061 — translated per provider. */
92
92
  tools?: AiToolDeclaration[];
93
93
  /**
@@ -97,10 +97,10 @@ export interface AiChatOptions {
97
97
  */
98
98
  toolChoice?: AiToolChoice;
99
99
  }
100
- export interface AiEmbedOptions { model?: string; timeout?: number; provider?: "local" | "openai" | "anthropic" }
100
+ export interface AiEmbedOptions { model?: string; timeout?: number; provider?: "local" | "openai" | "anthropic" | "gemini" }
101
101
 
102
102
  interface Config {
103
- provider: "local" | "openai" | "anthropic";
103
+ provider: "local" | "openai" | "anthropic" | "gemini";
104
104
  url: string;
105
105
  model: string;
106
106
  key: string | null;
@@ -288,13 +288,16 @@ export class Ai {
288
288
 
289
289
  private static config(capability: "chat" | "embed", options: AiChatOptions | AiEmbedOptions): Config {
290
290
  const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
291
- if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
291
+ if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
292
292
  const key = process.env.TINA4_AI_KEY || null;
293
- if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
293
+ if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
294
+ // Gemini speaks the OpenAI wire format at its OpenAI-compatible base, so it rides the openai
295
+ // body/parse/stream path - only the base URL, the endpoint suffix append, and the Bearer key differ.
294
296
  const defaults: Record<Config["provider"], [string, string]> = {
295
297
  local: ["http://localhost:11437", "llama3.2"],
296
298
  openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
297
299
  anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
300
+ gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"],
298
301
  };
299
302
  const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : (process.env.TINA4_AI_URL ?? defaults[provider][0]);
300
303
  const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
@@ -309,7 +312,11 @@ export class Ai {
309
312
  try { url = new URL(value); } catch { throw new AiConfigError("AI URL must be an http or https URL"); }
310
313
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
311
314
  const path = url.pathname.replace(/\/+$/, "");
312
- if (path === "" || path === "/v1" || path === "/api") {
315
+ // "/v1beta/openai" is Gemini's OpenAI-compatible base; append the suffix onto it exactly as
316
+ // onto a bare host / "/v1" / "/api", so the default gemini base resolves to
317
+ // .../v1beta/openai/chat/completions (or /embeddings). A full endpoint URL the caller supplies
318
+ // (any other path) still passes through verbatim.
319
+ if (path === "" || path === "/v1" || path === "/api" || path === "/v1beta/openai") {
313
320
  const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
314
321
  url.pathname = (path || "/v1") + suffix;
315
322
  }
@@ -318,7 +325,7 @@ export class Ai {
318
325
 
319
326
  private static headers(config: Config): Record<string, string> {
320
327
  const headers: Record<string, string> = { "content-type": "application/json", accept: "application/json" };
321
- if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
328
+ if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
322
329
  if (config.provider === "anthropic") { headers["x-api-key"] = config.key!; headers["anthropic-version"] = "2023-06-01"; }
323
330
  return headers;
324
331
  }
@@ -33487,13 +33487,14 @@ var init_aiClient = __esm({
33487
33487
  }
33488
33488
  static config(capability, options) {
33489
33489
  const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
33490
- if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
33490
+ if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
33491
33491
  const key = process.env.TINA4_AI_KEY || null;
33492
- if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
33492
+ if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
33493
33493
  const defaults = {
33494
33494
  local: ["http://localhost:11437", "llama3.2"],
33495
33495
  openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
33496
- anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
33496
+ anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
33497
+ gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"]
33497
33498
  };
33498
33499
  const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
33499
33500
  const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
@@ -33511,7 +33512,7 @@ var init_aiClient = __esm({
33511
33512
  }
33512
33513
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
33513
33514
  const path10 = url.pathname.replace(/\/+$/, "");
33514
- if (path10 === "" || path10 === "/v1" || path10 === "/api") {
33515
+ if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
33515
33516
  const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
33516
33517
  url.pathname = (path10 || "/v1") + suffix;
33517
33518
  }
@@ -33519,7 +33520,7 @@ var init_aiClient = __esm({
33519
33520
  }
33520
33521
  static headers(config) {
33521
33522
  const headers = { "content-type": "application/json", accept: "application/json" };
33522
- if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
33523
+ if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
33523
33524
  if (config.provider === "anthropic") {
33524
33525
  headers["x-api-key"] = config.key;
33525
33526
  headers["anthropic-version"] = "2023-06-01";
@@ -41799,6 +41800,10 @@ async function recordApplied(db, name, batch, passed = 1) {
41799
41800
  insertCols.push("migration_id");
41800
41801
  values.push(name);
41801
41802
  }
41803
+ if (cols.has("name")) {
41804
+ insertCols.push("name");
41805
+ values.push(name);
41806
+ }
41802
41807
  if (isFirebirdAdapter(db)) {
41803
41808
  const rows = await adapterQuery(
41804
41809
  db,
@@ -524,6 +524,19 @@ async function recordApplied(
524
524
  values.push(name);
525
525
  }
526
526
 
527
+ // A table created by an OLDER Node v3 (<= 3.13.54) carries `name VARCHAR(500)
528
+ // NOT NULL` (the pre-3.13.55 tracking DDL — see git history). upgradeMigrationTable()
529
+ // copies its values into migration_name and leaves the `name` column in place
530
+ // (SQLite cannot drop a NOT NULL column without a full rebuild). Exactly like the
531
+ // python#93 `migration_id` case above, an insert that omits `name` fails its
532
+ // NOT NULL constraint, wedging EVERY migration recorded after the upgrade — so
533
+ // populate it here too, mirroring migration_name. A fresh canonical table has no
534
+ // `name` column, so this only fires on an upgraded legacy table.
535
+ if (cols.has("name")) {
536
+ insertCols.push("name");
537
+ values.push(name);
538
+ }
539
+
527
540
  if (isFirebirdAdapter(db)) {
528
541
  // Firebird: generate the id from the sequence.
529
542
  const rows = await adapterQuery<{ next_id: number }>(db,
@@ -113,7 +113,7 @@ export interface AiChatOptions {
113
113
  maxTokens?: number;
114
114
  stream?: boolean;
115
115
  timeout?: number;
116
- provider?: "local" | "openai" | "anthropic";
116
+ provider?: "local" | "openai" | "anthropic" | "gemini";
117
117
  /** Tools the model may call. ADR-0061 — translated per provider. */
118
118
  tools?: AiToolDeclaration[];
119
119
  /**
@@ -126,7 +126,7 @@ export interface AiChatOptions {
126
126
  export interface AiEmbedOptions {
127
127
  model?: string;
128
128
  timeout?: number;
129
- provider?: "local" | "openai" | "anthropic";
129
+ provider?: "local" | "openai" | "anthropic" | "gemini";
130
130
  }
131
131
  export declare class Ai {
132
132
  static chat(messages: AiMessage[], options: AiChatOptions & {