tina4-nodejs 3.13.100 → 3.13.101

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.
@@ -8819,14 +8819,14 @@ async function discoverRoutes(routesDir) {
8819
8819
  const currentMtime = statSync5(filePath).mtimeMs;
8820
8820
  if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
8821
8821
  const method = name.toUpperCase();
8822
- const relativePath3 = relative(routesDir, filePath);
8823
- const pattern = filePathToPattern(relativePath3);
8822
+ const relativePath2 = relative(routesDir, filePath);
8823
+ const pattern = filePathToPattern(relativePath2);
8824
8824
  try {
8825
8825
  const moduleUrl = `file://${filePath}?t=${currentMtime}`;
8826
8826
  const mod = await import(moduleUrl);
8827
8827
  const handler = mod.default ?? mod.handler;
8828
8828
  if (typeof handler !== "function") {
8829
- console.warn(` Warning: ${relativePath3} does not export a handler function, skipping`);
8829
+ console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
8830
8830
  continue;
8831
8831
  }
8832
8832
  const meta = mod.meta;
@@ -8838,7 +8838,7 @@ async function discoverRoutes(routesDir) {
8838
8838
  _seenMtimes.set(filePath, currentMtime);
8839
8839
  registeredFromThisScan++;
8840
8840
  } catch (err) {
8841
- console.error(` Error loading route ${relativePath3}:`, err);
8841
+ console.error(` Error loading route ${relativePath2}:`, err);
8842
8842
  recordBrokenImport(filePath, err);
8843
8843
  }
8844
8844
  }
@@ -8872,8 +8872,8 @@ function recordBrokenImport(filePath, error) {
8872
8872
  } catch {
8873
8873
  }
8874
8874
  }
8875
- function filePathToPattern(relativePath3) {
8876
- const parts = relativePath3.replace(/\\/g, "/").split("/").slice(0, -1);
8875
+ function filePathToPattern(relativePath2) {
8876
+ const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
8877
8877
  const urlParts = parts.map((part) => {
8878
8878
  if (part.startsWith("[...") && part.endsWith("]")) {
8879
8879
  const name = part.slice(4, -1);
@@ -12110,489 +12110,127 @@ import * as fs3 from "node:fs";
12110
12110
  import * as path2 from "node:path";
12111
12111
  import { spawnSync } from "node:child_process";
12112
12112
  import { fileURLToPath } from "node:url";
12113
- function walkFiles(dir, extensions, exclude = ["node_modules", ".git", "dist", "build"]) {
12114
- const results = [];
12115
- if (!fs3.existsSync(dir)) return results;
12116
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
12117
- for (const entry of entries) {
12118
- const fullPath = path2.join(dir, entry.name);
12119
- if (entry.isDirectory()) {
12120
- if (!exclude.includes(entry.name)) {
12121
- results.push(...walkFiles(fullPath, extensions, exclude));
12122
- }
12123
- } else if (entry.isFile()) {
12124
- const ext = path2.extname(entry.name);
12125
- if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
12126
- results.push(fullPath);
12127
- }
12128
- }
12129
- }
12130
- return results;
12131
- }
12132
- function readFileSafe(filePath) {
12133
- try {
12134
- return fs3.readFileSync(filePath, "utf-8");
12135
- } catch {
12136
- return null;
12137
- }
12138
- }
12139
- function relativePath(filePath, root = ".") {
12140
- return path2.relative(root, filePath);
12141
- }
12142
- function countLines(source) {
12143
- const lines = source.split("\n");
12144
- let loc = 0;
12145
- let blank = 0;
12146
- let comment = 0;
12147
- let inBlockComment = false;
12148
- for (const line of lines) {
12149
- const stripped = line.trim();
12150
- if (!stripped) {
12151
- blank++;
12152
- continue;
12153
- }
12154
- if (inBlockComment) {
12155
- comment++;
12156
- if (stripped.includes("*/")) {
12157
- inBlockComment = false;
12158
- }
12159
- continue;
12160
- }
12161
- if (stripped.startsWith("/*")) {
12162
- comment++;
12163
- if (!stripped.includes("*/") || stripped.endsWith("/*")) {
12164
- inBlockComment = true;
12165
- }
12166
- continue;
12167
- }
12168
- if (stripped.startsWith("//")) {
12169
- comment++;
12170
- continue;
12171
- }
12172
- loc++;
12173
- }
12174
- return { loc, blank, comment };
12175
- }
12176
- function stripLiterals(source) {
12177
- const out = [];
12178
- const n = source.length;
12179
- let i = 0;
12180
- let prevSignificant = "";
12181
- let prevWord = "";
12182
- const regexKeywords = /* @__PURE__ */ new Set([
12183
- "return",
12184
- "typeof",
12185
- "instanceof",
12186
- "in",
12187
- "of",
12188
- "new",
12189
- "delete",
12190
- "void",
12191
- "throw",
12192
- "case",
12193
- "do",
12194
- "else",
12195
- "yield",
12196
- "await"
12197
- ]);
12198
- function prevEndsExpression() {
12199
- if (prevSignificant === "") return false;
12200
- if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
12201
- return !regexKeywords.has(prevWord);
12202
- }
12203
- if (prevSignificant === ")" || prevSignificant === "]") return true;
12204
- if (prevSignificant === ".") return true;
12205
- return false;
12113
+ function containsTypeScript(directory) {
12114
+ if (!fs3.existsSync(directory) || !fs3.statSync(directory).isDirectory()) return false;
12115
+ for (const entry of fs3.readdirSync(directory, { withFileTypes: true })) {
12116
+ if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
12117
+ const target = path2.join(directory, entry.name);
12118
+ if (entry.isDirectory() ? containsTypeScript(target) : /\.[cm]?[jt]sx?$/.test(entry.name)) return true;
12206
12119
  }
12207
- while (i < n) {
12208
- const ch = source[i];
12209
- const next = i + 1 < n ? source[i + 1] : "";
12210
- if (ch === "/" && next === "/") {
12211
- out.push("//");
12212
- i += 2;
12213
- while (i < n && source[i] !== "\n") {
12214
- out.push(" ");
12215
- i++;
12216
- }
12217
- continue;
12218
- }
12219
- if (ch === "/" && next === "*") {
12220
- out.push("/*");
12221
- i += 2;
12222
- while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
12223
- out.push(source[i] === "\n" ? "\n" : " ");
12224
- i++;
12225
- }
12226
- if (i < n) {
12227
- out.push("*/");
12228
- i += 2;
12229
- }
12230
- continue;
12231
- }
12232
- if (ch === '"' || ch === "'") {
12233
- const quote = ch;
12234
- out.push(quote);
12235
- i++;
12236
- while (i < n && source[i] !== quote) {
12237
- if (source[i] === "\\" && i + 1 < n) {
12238
- out.push(" ");
12239
- i += 2;
12240
- continue;
12241
- }
12242
- if (source[i] === "\n") {
12243
- out.push("\n");
12244
- i++;
12245
- break;
12246
- }
12247
- out.push(" ");
12248
- i++;
12249
- }
12250
- if (i < n && source[i] === quote) {
12251
- out.push(quote);
12252
- i++;
12253
- }
12254
- prevSignificant = quote;
12255
- prevWord = "";
12256
- continue;
12257
- }
12258
- if (ch === "`") {
12259
- out.push("`");
12260
- i++;
12261
- while (i < n && source[i] !== "`") {
12262
- if (source[i] === "\\" && i + 1 < n) {
12263
- out.push(source[i + 1] === "\n" ? " \n" : " ");
12264
- i += 2;
12265
- continue;
12266
- }
12267
- if (source[i] === "$" && source[i + 1] === "{") {
12268
- out.push("${");
12269
- i += 2;
12270
- let depth = 1;
12271
- const exprStart = i;
12272
- while (i < n && depth > 0) {
12273
- if (source[i] === "{") depth++;
12274
- else if (source[i] === "}") depth--;
12275
- if (depth === 0) break;
12276
- i++;
12277
- }
12278
- out.push(stripLiterals(source.slice(exprStart, i)));
12279
- if (i < n && source[i] === "}") {
12280
- out.push("}");
12281
- i++;
12282
- }
12283
- continue;
12284
- }
12285
- out.push(source[i] === "\n" ? "\n" : " ");
12286
- i++;
12287
- }
12288
- if (i < n && source[i] === "`") {
12289
- out.push("`");
12290
- i++;
12291
- }
12292
- prevSignificant = "`";
12293
- prevWord = "";
12294
- continue;
12295
- }
12296
- if (ch === "/" && !prevEndsExpression()) {
12297
- let j = i + 1;
12298
- let ok = false;
12299
- let inClass = false;
12300
- while (j < n) {
12301
- const c = source[j];
12302
- if (c === "\\") {
12303
- j += 2;
12304
- continue;
12305
- }
12306
- if (c === "\n") break;
12307
- if (c === "[") inClass = true;
12308
- else if (c === "]") inClass = false;
12309
- else if (c === "/" && !inClass) {
12310
- ok = true;
12311
- break;
12312
- }
12313
- j++;
12314
- }
12315
- if (ok) {
12316
- out.push("/");
12317
- for (let k = i + 1; k < j; k++) out.push(" ");
12318
- out.push("/");
12319
- i = j + 1;
12320
- while (i < n && /[a-z]/i.test(source[i])) {
12321
- out.push(source[i]);
12322
- i++;
12323
- }
12324
- prevSignificant = "/";
12325
- prevWord = "";
12326
- continue;
12327
- }
12328
- }
12329
- out.push(ch);
12330
- if (!/\s/.test(ch)) {
12331
- prevSignificant = ch;
12332
- if (/[A-Za-z0-9_$]/.test(ch)) {
12333
- prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
12334
- } else {
12335
- prevWord = "";
12336
- }
12337
- }
12338
- i++;
12339
- }
12340
- return out.join("");
12341
- }
12342
- function countClassesQuick(source) {
12343
- const matches = source.match(
12344
- /(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
12345
- );
12346
- return matches ? matches.length : 0;
12347
- }
12348
- function countFunctionsQuick(source) {
12349
- const clean = stripLiterals(source);
12350
- let count = 0;
12351
- const funcDecls = clean.match(
12352
- /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
12353
- );
12354
- if (funcDecls) count += funcDecls.length;
12355
- const methods = clean.match(
12356
- /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
12357
- );
12358
- if (methods) count += methods.length;
12359
- const arrows = clean.match(
12360
- /(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
12361
- );
12362
- if (arrows) count += arrows.length;
12363
- return count;
12364
- }
12365
- function resolveRoot(root = "src") {
12366
- const rootPath = path2.resolve(root);
12367
- if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
12368
- _lastScanRoot = rootPath;
12369
- return root;
12370
- }
12371
- const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
12372
- _lastScanRoot = fwDir;
12373
- return fwDir;
12374
- }
12375
- function quickMetrics(root = "src") {
12376
- root = resolveRoot(root);
12377
- const rootPath = path2.resolve(root);
12378
- if (!fs3.existsSync(rootPath)) {
12379
- return { error: `Directory not found: ${root}` };
12380
- }
12381
- const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
12382
- const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
12383
- const migrationsDir = path2.resolve("migrations");
12384
- const migrationFiles = [
12385
- ...walkFiles(migrationsDir, [".sql"]),
12386
- ...walkFiles(migrationsDir, [".ts"])
12387
- ];
12388
- const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
12389
- let totalLoc = 0;
12390
- let totalBlank = 0;
12391
- let totalComment = 0;
12392
- let totalClasses = 0;
12393
- let totalFunctions = 0;
12394
- const fileDetails = [];
12395
- for (const f of tsFiles) {
12396
- const source = readFileSafe(f);
12397
- if (source === null) continue;
12398
- const counts = countLines(source);
12399
- const classes = countClassesQuick(source);
12400
- const functions = countFunctionsQuick(source);
12401
- totalLoc += counts.loc;
12402
- totalBlank += counts.blank;
12403
- totalComment += counts.comment;
12404
- totalClasses += classes;
12405
- totalFunctions += functions;
12406
- fileDetails.push({
12407
- path: relativePath(f, rootPath),
12408
- loc: counts.loc,
12409
- blank: counts.blank,
12410
- comment: counts.comment,
12411
- classes,
12412
- functions
12413
- });
12414
- }
12415
- fileDetails.sort((a, b) => b.loc - a.loc);
12416
- let routeCount = 0;
12417
- let ormCount = 0;
12418
- for (const f of tsFiles) {
12419
- const source = readFileSafe(f);
12420
- if (source === null) continue;
12421
- const routes = source.match(
12422
- /(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
12423
- );
12424
- if (routes) routeCount += routes.length;
12425
- const orms = source.match(
12426
- /class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
12427
- );
12428
- if (orms) ormCount += orms.length;
12429
- }
12430
- const breakdown = {
12431
- typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
12432
- javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
12433
- templates: twigFiles.length,
12434
- migrations: migrationFiles.length,
12435
- stylesheets: scssFiles.length
12436
- };
12437
- return {
12438
- file_count: tsFiles.length,
12439
- total_loc: totalLoc,
12440
- total_blank: totalBlank,
12441
- total_comment: totalComment,
12442
- lloc: totalLoc,
12443
- classes: totalClasses,
12444
- functions: totalFunctions,
12445
- route_count: routeCount,
12446
- orm_count: ormCount,
12447
- template_count: twigFiles.length,
12448
- migration_count: migrationFiles.length,
12449
- avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
12450
- largest_files: fileDetails.slice(0, 10),
12451
- breakdown
12452
- };
12120
+ return false;
12453
12121
  }
12454
- function resolveScanTarget(root = "src") {
12455
- const resolved = resolveRoot(root);
12456
- const frameworkDir = path2.dirname(fileURLToPath(import.meta.url));
12457
- const real = path2.resolve(resolved);
12458
- const scanningFramework = real === frameworkDir || real.startsWith(frameworkDir);
12459
- return [resolved, scanningFramework ? "framework" : "project"];
12122
+ function resolveTarget(root = "src") {
12123
+ const resolved = containsTypeScript(root) ? path2.resolve(root) : path2.dirname(fileURLToPath(import.meta.url));
12124
+ const mode = containsTypeScript(root) ? "project" : "framework";
12125
+ lastScanRoot = resolved;
12126
+ return [resolved, mode];
12460
12127
  }
12461
12128
  function enginePath() {
12462
- const names = process.platform === "win32" ? ["tina4.exe", "tina4.cmd", "tina4"] : ["tina4"];
12463
- for (const dir of (process.env.PATH || "").split(path2.delimiter)) {
12464
- if (!dir) continue;
12129
+ const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
12130
+ for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
12465
12131
  for (const name of names) {
12466
- const candidate = path2.join(dir, name);
12132
+ const candidate = path2.join(directory, name);
12467
12133
  try {
12468
- if (!fs3.statSync(candidate).isFile()) continue;
12469
12134
  fs3.accessSync(candidate, fs3.constants.X_OK);
12135
+ if (!fs3.statSync(candidate).isFile()) continue;
12136
+ const descriptor = fs3.openSync(candidate, "r");
12137
+ const header = Buffer.alloc(2);
12138
+ fs3.readSync(descriptor, header, 0, 2, 0);
12139
+ fs3.closeSync(descriptor);
12140
+ if (header.toString("latin1") !== "#!") return candidate;
12470
12141
  } catch {
12471
12142
  continue;
12472
12143
  }
12473
- try {
12474
- const fd = fs3.openSync(candidate, "r");
12475
- const buf = Buffer.alloc(2);
12476
- fs3.readSync(fd, buf, 0, 2, 0);
12477
- fs3.closeSync(fd);
12478
- if (buf.toString("latin1") === "#!") continue;
12479
- } catch {
12480
- }
12481
- return candidate;
12482
12144
  }
12483
12145
  }
12484
12146
  return null;
12485
12147
  }
12486
12148
  function runEngine(target) {
12487
12149
  const binary = enginePath();
12488
- if (binary === null) {
12489
- throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
12490
- }
12491
- const proc = spawnSync(binary, ["metrics", "--path", target, "--json"], {
12150
+ if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
12151
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
12492
12152
  encoding: "utf8",
12493
- timeout: TIMEOUT_MS,
12153
+ timeout: 6e4,
12494
12154
  maxBuffer: 64 * 1024 * 1024
12495
12155
  });
12496
- if (proc.error) {
12497
- const err = proc.error;
12498
- if (err.code === "ETIMEDOUT") {
12499
- throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
12500
- }
12501
- throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
12502
- }
12503
- if (proc.status !== 0) {
12504
- const detail = (proc.stderr || proc.stdout || "").trim().split("\n")[0];
12505
- throw new MetricsEngineError(
12506
- `tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
12507
- );
12156
+ if (processResult.error) {
12157
+ throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
12508
12158
  }
12509
- if (!proc.stdout || !proc.stdout.trim()) {
12510
- throw new MetricsEngineError(`tina4 metrics produced no output for ${target}`);
12159
+ if (processResult.status !== 0) {
12160
+ const detail = (processResult.stderr || processResult.stdout || "").trim().split("\n")[0];
12161
+ throw new MetricsEngineError(`tina4 metrics failed on ${target}: ${detail || processResult.status}`);
12511
12162
  }
12512
- let payload;
12513
12163
  try {
12514
- payload = JSON.parse(proc.stdout);
12515
- } catch (e) {
12516
- throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${e.message}`);
12517
- }
12518
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
12519
- throw new MetricsEngineError("tina4 metrics returned a non-object payload");
12164
+ const payload = JSON.parse(processResult.stdout);
12165
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
12166
+ throw new Error("non-object payload");
12167
+ }
12168
+ return payload;
12169
+ } catch (error) {
12170
+ throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${error.message}`);
12520
12171
  }
12521
- return payload;
12522
12172
  }
12523
- function requireKey(payload, key, isArray) {
12524
- const value = payload[key];
12525
- const ok = isArray ? Array.isArray(value) : value !== null && typeof value === "object" && !Array.isArray(value);
12526
- if (!ok) {
12527
- throw new MetricsEngineError(
12528
- `engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
12529
- );
12173
+ function requireArray(payload, key) {
12174
+ if (!Array.isArray(payload[key])) {
12175
+ throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
12530
12176
  }
12531
- return value;
12177
+ return payload[key];
12532
12178
  }
12533
12179
  function fullAnalysis(root = "src") {
12534
- const [resolved, scanMode] = resolveScanTarget(root);
12180
+ const [resolved, scanMode] = resolveTarget(root);
12535
12181
  const payload = runEngine(resolved);
12536
- const summary = requireKey(payload, "summary", false);
12537
- const fileMetrics = requireKey(payload, "file_metrics", true);
12538
- const functions = requireKey(payload, "most_complex_functions", true);
12539
- const missing = SUMMARY_KEYS.filter((k) => !(k in summary));
12540
- if (missing.length) {
12541
- throw new MetricsEngineError(
12542
- `engine summary is missing ${missing.join(", ")} - update the CLI: ${INSTALL_HINT}`
12543
- );
12544
- }
12545
- if (fileMetrics.length) {
12546
- const absent = FILE_KEYS.filter((k) => !(k in fileMetrics[0]));
12547
- if (absent.length) throw new MetricsEngineError(`engine file_metrics is missing ${absent.join(", ")}`);
12182
+ const summary = payload.summary;
12183
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
12184
+ throw new MetricsEngineError(`engine payload has no usable 'summary' - ${INSTALL_HINT}`);
12185
+ }
12186
+ const fileMetrics = requireArray(payload, "file_metrics");
12187
+ const functions = requireArray(payload, "most_complex_functions");
12188
+ const missingSummary = SUMMARY_KEYS.filter((key) => !(key in summary));
12189
+ if (missingSummary.length) throw new MetricsEngineError(`engine summary is missing ${missingSummary.join(", ")}`);
12190
+ const missingFile = fileMetrics.length ? FILE_KEYS.filter((key) => !(key in fileMetrics[0])) : [];
12191
+ if (missingFile.length) throw new MetricsEngineError(`engine file_metrics is missing ${missingFile.join(", ")}`);
12192
+ const missingFunction = functions.length ? FUNCTION_KEYS.filter((key) => !(key in functions[0])) : [];
12193
+ if (missingFunction.length) {
12194
+ throw new MetricsEngineError(`engine function metrics are missing ${missingFunction.join(", ")}`);
12548
12195
  }
12549
- if (functions.length) {
12550
- const absent = FUNCTION_KEYS.filter((k) => !(k in functions[0]));
12551
- if (absent.length) throw new MetricsEngineError(`engine function metrics are missing ${absent.join(", ")}`);
12552
- }
12553
- const result = {};
12554
- for (const key of SUMMARY_KEYS) result[key] = summary[key];
12555
- result.file_metrics = fileMetrics;
12556
- result.most_complex_functions = functions.slice(0, 15);
12557
- result.dependency_graph = payload.dependency_graph || {};
12558
- result.scan_mode = scanMode;
12559
- result.scan_root = path2.resolve(resolved);
12560
- result.engine = "tina4-cli";
12561
- return result;
12196
+ return {
12197
+ ...Object.fromEntries(SUMMARY_KEYS.map((key) => [key, summary[key]])),
12198
+ file_metrics: fileMetrics,
12199
+ most_complex_functions: functions.slice(0, 15),
12200
+ dependency_graph: payload.dependency_graph || {},
12201
+ scan_mode: scanMode,
12202
+ scan_root: resolved,
12203
+ engine: "tina4-cli"
12204
+ };
12562
12205
  }
12563
12206
  function fileDetail(filePath) {
12564
12207
  if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
12565
12208
  let target = filePath;
12566
- if (!fs3.existsSync(target) && _lastScanRoot) {
12567
- const candidate = path2.join(_lastScanRoot, filePath);
12568
- if (fs3.existsSync(candidate)) target = candidate;
12569
- }
12209
+ if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
12570
12210
  if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
12571
12211
  if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
12572
12212
  const payload = runEngine(target);
12573
- const fileMetrics = requireKey(payload, "file_metrics", true);
12574
- if (!fileMetrics.length) {
12575
- throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
12576
- }
12577
- return { ...fileMetrics[0], engine: "tina4-cli" };
12213
+ const files = requireArray(payload, "file_metrics");
12214
+ if (!files.length) throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
12215
+ return {
12216
+ ...files[0],
12217
+ function_count: files[0].functions || 0,
12218
+ functions: requireArray(payload, "most_complex_functions"),
12219
+ engine: "tina4-cli"
12220
+ };
12578
12221
  }
12579
- var _lastScanRoot, MetricsEngineError, TIMEOUT_MS, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
12222
+ var lastScanRoot, MetricsEngineError, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
12580
12223
  var init_metrics = __esm({
12581
12224
  "../core/src/metrics.ts"() {
12582
12225
  "use strict";
12583
- _lastScanRoot = "";
12226
+ lastScanRoot = "";
12584
12227
  MetricsEngineError = class extends Error {
12585
12228
  constructor(message) {
12586
12229
  super(message);
12587
12230
  this.name = "MetricsEngineError";
12588
12231
  }
12589
12232
  };
12590
- TIMEOUT_MS = 6e4;
12591
- INSTALL_HINT = [
12592
- "the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
12593
- " curl -fsSL https://tina4.com/install.sh | sh",
12594
- "or see https://tina4.com/cli"
12595
- ].join("\n");
12233
+ INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
12596
12234
  SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
12597
12235
  FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
12598
12236
  FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
@@ -12600,7 +12238,7 @@ var init_metrics = __esm({
12600
12238
  });
12601
12239
 
12602
12240
  // ../core/src/feedback.ts
12603
- import { readFileSync as readFileSync11, existsSync as existsSync11 } from "node:fs";
12241
+ import { readFileSync as readFileSync10, existsSync as existsSync11 } from "node:fs";
12604
12242
  import { dirname as dirname5, join as join13, resolve as resolve5 } from "node:path";
12605
12243
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12606
12244
  function feedbackEnabled() {
@@ -12741,7 +12379,7 @@ var init_feedback = __esm({
12741
12379
  handleFeedbackWidgetJs = (_req, res) => {
12742
12380
  let body;
12743
12381
  if (existsSync11(WIDGET_BUNDLE_PATH)) {
12744
- body = readFileSync11(WIDGET_BUNDLE_PATH);
12382
+ body = readFileSync10(WIDGET_BUNDLE_PATH);
12745
12383
  } else {
12746
12384
  body = "console.warn('tina4-feedback-widget bundle not built yet');";
12747
12385
  }
@@ -12756,7 +12394,7 @@ var init_feedback = __esm({
12756
12394
  });
12757
12395
 
12758
12396
  // ../core/src/version.ts
12759
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
12397
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
12760
12398
  import { dirname as dirname6, join as join14 } from "node:path";
12761
12399
  import { fileURLToPath as fileURLToPath3 } from "node:url";
12762
12400
  function resolveFrameworkVersion() {
@@ -12765,7 +12403,7 @@ function resolveFrameworkVersion() {
12765
12403
  const pkgPath = join14(dir, "package.json");
12766
12404
  if (existsSync12(pkgPath)) {
12767
12405
  try {
12768
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
12406
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
12769
12407
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
12770
12408
  } catch {
12771
12409
  }
@@ -15553,8 +15191,8 @@ __export(context_exports, {
15553
15191
  fts5Supported: () => fts5Supported
15554
15192
  });
15555
15193
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
15556
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync14, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
15557
- import { basename as basename4, dirname as dirname8, extname as extname5, isAbsolute as isAbsolute4, join as join16, relative as relative4, resolve as resolve7 } from "node:path";
15194
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
15195
+ import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join16, relative as relative3, resolve as resolve7 } from "node:path";
15558
15196
  function fts5Supported() {
15559
15197
  try {
15560
15198
  const conn = new DatabaseSync2(":memory:");
@@ -15689,7 +15327,7 @@ var init_context = __esm({
15689
15327
  }
15690
15328
  // ── indexing ───────────────────────────────────────────────
15691
15329
  static chunksFor(label, text) {
15692
- const ext = extname5(label).toLowerCase();
15330
+ const ext = extname4(label).toLowerCase();
15693
15331
  const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
15694
15332
  if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
15695
15333
  return chunkCode(text, label);
@@ -15707,7 +15345,7 @@ var init_context = __esm({
15707
15345
  const stored = label != null ? String(label) : String(file);
15708
15346
  let text;
15709
15347
  try {
15710
- text = readFileSync14(file, "utf-8");
15348
+ text = readFileSync13(file, "utf-8");
15711
15349
  } catch {
15712
15350
  return 0;
15713
15351
  }
@@ -15728,7 +15366,7 @@ var init_context = __esm({
15728
15366
  static eligible(filename) {
15729
15367
  const fn = filename.toLowerCase();
15730
15368
  if (fn.endsWith(".min.js")) return false;
15731
- const ext = extname5(fn);
15369
+ const ext = extname4(fn);
15732
15370
  return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
15733
15371
  }
15734
15372
  /**
@@ -15757,7 +15395,7 @@ var init_context = __esm({
15757
15395
  for (const fn of files) {
15758
15396
  if (!_Context.eligible(fn)) continue;
15759
15397
  const full = join16(dir, fn);
15760
- const rel = relative4(rootAbs, full);
15398
+ const rel = relative3(rootAbs, full);
15761
15399
  total += this.indexPath(full, rel);
15762
15400
  }
15763
15401
  for (const d of subdirs) walk2(join16(dir, d));
@@ -15778,7 +15416,7 @@ var init_context = __esm({
15778
15416
  const raw = String(changedPath);
15779
15417
  const abs = isAbsolute4(raw) ? raw : join16(process.cwd(), raw);
15780
15418
  const resolved = realResolve(resolve7(abs));
15781
- const rel = relative4(this.root, resolved);
15419
+ const rel = relative3(this.root, resolved);
15782
15420
  if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
15783
15421
  return -1;
15784
15422
  }
@@ -17700,7 +17338,7 @@ var init_job = __esm({
17700
17338
  });
17701
17339
 
17702
17340
  // ../core/src/queueBackends/liteBackend.ts
17703
- import { mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6, existsSync as existsSync15 } from "node:fs";
17341
+ import { mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6, existsSync as existsSync15 } from "node:fs";
17704
17342
  import { join as join17 } from "node:path";
17705
17343
  import { randomUUID as randomUUID5 } from "node:crypto";
17706
17344
  var LiteBackend;
@@ -17800,7 +17438,7 @@ var init_liteBackend = __esm({
17800
17438
  const filePath = join17(dir, filename);
17801
17439
  let job;
17802
17440
  try {
17803
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
17441
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
17804
17442
  } catch {
17805
17443
  continue;
17806
17444
  }
@@ -17864,7 +17502,7 @@ var init_liteBackend = __esm({
17864
17502
  const filePath = join17(reservedDir, filename);
17865
17503
  let record;
17866
17504
  try {
17867
- record = JSON.parse(readFileSync15(filePath, "utf-8"));
17505
+ record = JSON.parse(readFileSync14(filePath, "utf-8"));
17868
17506
  } catch {
17869
17507
  continue;
17870
17508
  }
@@ -17977,7 +17615,7 @@ var init_liteBackend = __esm({
17977
17615
  let count = 0;
17978
17616
  for (const file of files) {
17979
17617
  try {
17980
- const job = JSON.parse(readFileSync15(join17(scanDir, file), "utf-8"));
17618
+ const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
17981
17619
  if (job.status === status2) count++;
17982
17620
  } catch {
17983
17621
  }
@@ -18034,7 +17672,7 @@ var init_liteBackend = __esm({
18034
17672
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
18035
17673
  for (const file of files) {
18036
17674
  try {
18037
- const job = JSON.parse(readFileSync15(join17(dir, file), "utf-8"));
17675
+ const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
18038
17676
  const attempts = job.attempts || 0;
18039
17677
  if (attempts > 0 && attempts < maxRetries) {
18040
17678
  results.push(job);
@@ -18060,7 +17698,7 @@ var init_liteBackend = __esm({
18060
17698
  const failedDir = join17(this.basePath, q, "failed");
18061
17699
  const filePath = join17(failedDir, `${jobId}.queue-data`);
18062
17700
  if (existsSync15(filePath)) {
18063
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
17701
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
18064
17702
  job.status = "pending";
18065
17703
  job.attempts = (job.attempts || 0) + 1;
18066
17704
  job.error = void 0;
@@ -18084,7 +17722,7 @@ var init_liteBackend = __esm({
18084
17722
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
18085
17723
  for (const file of files) {
18086
17724
  try {
18087
- const job = JSON.parse(readFileSync15(join17(failedDir, file), "utf-8"));
17725
+ const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
18088
17726
  if ((job.attempts || 0) >= maxRetries) {
18089
17727
  job.status = "dead";
18090
17728
  results.push(job);
@@ -18118,7 +17756,7 @@ var init_liteBackend = __esm({
18118
17756
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
18119
17757
  for (const file of files) {
18120
17758
  try {
18121
- const job = JSON.parse(readFileSync15(join17(dir, file), "utf-8"));
17759
+ const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
18122
17760
  if (job.status === status2) {
18123
17761
  unlinkSync6(join17(dir, file));
18124
17762
  count++;
@@ -18145,7 +17783,7 @@ var init_liteBackend = __esm({
18145
17783
  for (const file of files) {
18146
17784
  try {
18147
17785
  const filePath = join17(failedDir, file);
18148
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
17786
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
18149
17787
  if ((job.attempts || 0) >= maxRetries) {
18150
17788
  continue;
18151
17789
  }
@@ -18177,7 +17815,7 @@ var init_liteBackend = __esm({
18177
17815
  const filePath = join17(dir, file);
18178
17816
  let job;
18179
17817
  try {
18180
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
17818
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
18181
17819
  } catch {
18182
17820
  continue;
18183
17821
  }
@@ -19640,7 +19278,7 @@ function detectVersion(projectRoot3) {
19640
19278
  }
19641
19279
  return "0.0.0";
19642
19280
  }
19643
- function relativePath2(absPath, projectRoot3, frameworkRoots) {
19281
+ function relativePath(absPath, projectRoot3, frameworkRoots) {
19644
19282
  const norm = path6.resolve(absPath);
19645
19283
  for (const fw of frameworkRoots) {
19646
19284
  const parent = path6.dirname(fw);
@@ -20105,7 +19743,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
20105
19743
  } catch {
20106
19744
  return;
20107
19745
  }
20108
- const rel = relativePath2(absPath, projectRoot3, fwRoots);
19746
+ const rel = relativePath(absPath, projectRoot3, fwRoots);
20109
19747
  for (const cls of parsed.classes) {
20110
19748
  if (!cls.exported && source === "framework") {
20111
19749
  continue;
@@ -20741,8 +20379,8 @@ ${end}
20741
20379
 
20742
20380
  // ../core/src/devAdmin.ts
20743
20381
  import { cpus as osCpus } from "node:os";
20744
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync12, mkdirSync as mkdirSync13, copyFileSync, statSync as statSync14 } from "node:fs";
20745
- import { join as join21, dirname as dirname10, resolve as resolve11, relative as relative8 } from "node:path";
20382
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync12, mkdirSync as mkdirSync13, copyFileSync, statSync as statSync14 } from "node:fs";
20383
+ import { join as join21, dirname as dirname10, resolve as resolve11, relative as relative7 } from "node:path";
20746
20384
  import { fileURLToPath as fileURLToPath5 } from "node:url";
20747
20385
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
20748
20386
  function escapeHtml(value) {
@@ -20861,7 +20499,7 @@ function readQueueDir(dir, topic, status2) {
20861
20499
  for (const filename of readdirSync12(dir).sort()) {
20862
20500
  if (!filename.endsWith(".queue-data")) continue;
20863
20501
  try {
20864
- jobs.push(mapQueueJob(JSON.parse(readFileSync19(join21(dir, filename), "utf-8")), topic, status2));
20502
+ jobs.push(mapQueueJob(JSON.parse(readFileSync18(join21(dir, filename), "utf-8")), topic, status2));
20865
20503
  } catch {
20866
20504
  }
20867
20505
  }
@@ -20980,7 +20618,7 @@ function resolveDevEnvVar(key) {
20980
20618
  if (live !== void 0 && live !== "") return live;
20981
20619
  const envPath = join21(process.cwd(), ".env");
20982
20620
  if (!existsSync19(envPath)) return "";
20983
- for (const line of readFileSync19(envPath, "utf-8").split("\n")) {
20621
+ for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
20984
20622
  const t = line.trim();
20985
20623
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
20986
20624
  const eq = t.indexOf("=");
@@ -20990,7 +20628,7 @@ function resolveDevEnvVar(key) {
20990
20628
  }
20991
20629
  function upsertDevEnvVar(key, value) {
20992
20630
  const envPath = join21(process.cwd(), ".env");
20993
- const lines = existsSync19(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
20631
+ const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
20994
20632
  let found = false;
20995
20633
  const out = [];
20996
20634
  for (const line of lines) {
@@ -21023,7 +20661,7 @@ function parseEnvFile() {
21023
20661
  const envPath = join21(process.cwd(), ".env");
21024
20662
  const result = {};
21025
20663
  if (!existsSync19(envPath)) return result;
21026
- const lines = readFileSync19(envPath, "utf-8").split("\n");
20664
+ const lines = readFileSync18(envPath, "utf-8").split("\n");
21027
20665
  for (const line of lines) {
21028
20666
  const trimmed = line.trim();
21029
20667
  if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -21063,7 +20701,7 @@ function handleGalleryDeploy(router) {
21063
20701
  const copied = [];
21064
20702
  const allFiles = walkDirRecursive(gallerySrc);
21065
20703
  for (const srcFile of allFiles) {
21066
- const rel = relative8(gallerySrc, srcFile);
20704
+ const rel = relative7(gallerySrc, srcFile);
21067
20705
  const dest = join21(projectSrc, rel);
21068
20706
  mkdirSync13(dirname10(dest), { recursive: true });
21069
20707
  copyFileSync(srcFile, dest);
@@ -21725,9 +21363,6 @@ var init_devAdmin = __esm({
21725
21363
  { method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
21726
21364
  { method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
21727
21365
  // Metrics
21728
- { method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
21729
- res.json(quickMetrics());
21730
- } },
21731
21366
  // No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
21732
21367
  // install command, never zeros that read as a healthy codebase.
21733
21368
  { method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
@@ -22536,7 +22171,7 @@ var init_devAdmin = __esm({
22536
22171
  }
22537
22172
  try {
22538
22173
  const envPath = join21(process.cwd(), ".env");
22539
- const lines = existsSync19(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
22174
+ const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
22540
22175
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
22541
22176
  const newLines = [];
22542
22177
  for (const line of lines) {
@@ -22582,12 +22217,12 @@ var init_devAdmin = __esm({
22582
22217
  const metaFile = join21(entryPath, "meta.json");
22583
22218
  if (statSync14(entryPath).isDirectory() && existsSync19(metaFile)) {
22584
22219
  try {
22585
- const meta = JSON.parse(readFileSync19(metaFile, "utf-8"));
22220
+ const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
22586
22221
  meta.id = entry;
22587
22222
  const srcDir = join21(entryPath, "src");
22588
22223
  if (existsSync19(srcDir)) {
22589
22224
  const allFiles = walkDirRecursive(srcDir);
22590
- meta.files = allFiles.map((f) => relative8(srcDir, f));
22225
+ meta.files = allFiles.map((f) => relative7(srcDir, f));
22591
22226
  }
22592
22227
  const projectSrc = resolve11(process.cwd(), "src");
22593
22228
  if (existsSync19(srcDir) && meta.files) {
@@ -22705,7 +22340,7 @@ var init_devAdmin = __esm({
22705
22340
  for (const name of readdirSync12(target).sort()) {
22706
22341
  if (devFilesHidden(name)) continue;
22707
22342
  const full = join21(target, name);
22708
- const entryRel = relative8(root, full).replace(/\\/g, "/");
22343
+ const entryRel = relative7(root, full).replace(/\\/g, "/");
22709
22344
  if (isSecretPath(entryRel)) continue;
22710
22345
  let isDir = false;
22711
22346
  let size = null;
@@ -22750,7 +22385,7 @@ var init_devAdmin = __esm({
22750
22385
  size
22751
22386
  });
22752
22387
  }
22753
- res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
22388
+ res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
22754
22389
  };
22755
22390
  DEV_ADMIN_LANG_MAP = {
22756
22391
  ".py": "python",
@@ -22802,8 +22437,8 @@ var init_devAdmin = __esm({
22802
22437
  return;
22803
22438
  }
22804
22439
  try {
22805
- const content = readFileSync19(target, "utf-8");
22806
- const path8 = relative8(root, target);
22440
+ const content = readFileSync18(target, "utf-8");
22441
+ const path8 = relative7(root, target);
22807
22442
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
22808
22443
  } catch (e) {
22809
22444
  res.json({ error: e.message }, 500);
@@ -22825,10 +22460,10 @@ var init_devAdmin = __esm({
22825
22460
  writeFileSync12(target, content, "utf-8");
22826
22461
  try {
22827
22462
  const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
22828
- Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
22463
+ Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
22829
22464
  } catch {
22830
22465
  }
22831
- res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
22466
+ res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
22832
22467
  } catch (e) {
22833
22468
  res.json({ error: e.message }, 500);
22834
22469
  }
@@ -22848,7 +22483,7 @@ var init_devAdmin = __esm({
22848
22483
  return;
22849
22484
  }
22850
22485
  try {
22851
- const buf = readFileSync19(target);
22486
+ const buf = readFileSync18(target);
22852
22487
  const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
22853
22488
  const mime = {
22854
22489
  js: "application/javascript",
@@ -22890,7 +22525,7 @@ var init_devAdmin = __esm({
22890
22525
  const { renameSync: renameSync3 } = await import("node:fs");
22891
22526
  mkdirSync13(dirname10(dst), { recursive: true });
22892
22527
  renameSync3(src, dst);
22893
- res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
22528
+ res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
22894
22529
  } catch (e) {
22895
22530
  res.json({ error: e.message }, 500);
22896
22531
  }
@@ -22911,7 +22546,7 @@ var init_devAdmin = __esm({
22911
22546
  try {
22912
22547
  const { rmSync } = await import("node:fs");
22913
22548
  rmSync(target, { recursive: true, force: true });
22914
- res.json({ ok: true, deleted: relative8(root, target) });
22549
+ res.json({ ok: true, deleted: relative7(root, target) });
22915
22550
  } catch (e) {
22916
22551
  res.json({ error: e.message }, 500);
22917
22552
  }
@@ -23245,7 +22880,7 @@ var init_devAdmin = __esm({
23245
22880
  });
23246
22881
  };
23247
22882
  handleDevAdminJs = async (_req, res) => {
23248
- const { readFileSync: readFileSync28, existsSync: existsSync27 } = await import("node:fs");
22883
+ const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
23249
22884
  const { dirname: dirname15, join: join32, resolve: resolve20 } = await import("node:path");
23250
22885
  const { fileURLToPath: fileURLToPath8 } = await import("node:url");
23251
22886
  const dir = dirname15(fileURLToPath8(import.meta.url));
@@ -23261,7 +22896,7 @@ var init_devAdmin = __esm({
23261
22896
  for (const jsPath of candidates) {
23262
22897
  if (existsSync27(jsPath)) {
23263
22898
  try {
23264
- const content = readFileSync28(jsPath, "utf-8");
22899
+ const content = readFileSync27(jsPath, "utf-8");
23265
22900
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
23266
22901
  res.raw.end(content);
23267
22902
  return;
@@ -23276,7 +22911,7 @@ var init_devAdmin = __esm({
23276
22911
  });
23277
22912
 
23278
22913
  // ../core/src/i18n.ts
23279
- import { readFileSync as readFileSync20, readdirSync as readdirSync13, existsSync as existsSync20 } from "node:fs";
22914
+ import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync20 } from "node:fs";
23280
22915
  import { join as join22, resolve as resolve12 } from "node:path";
23281
22916
  var I18n;
23282
22917
  var init_i18n = __esm({
@@ -23373,7 +23008,7 @@ var init_i18n = __esm({
23373
23008
  const filePath = join22(this._localeDir, `${locale}.json`);
23374
23009
  if (existsSync20(filePath)) {
23375
23010
  try {
23376
- const raw = readFileSync20(filePath, "utf-8");
23011
+ const raw = readFileSync19(filePath, "utf-8");
23377
23012
  const data = JSON.parse(raw);
23378
23013
  this._translations.set(locale, _I18n._flatten(data));
23379
23014
  return;
@@ -23386,7 +23021,7 @@ var init_i18n = __esm({
23386
23021
  const yamlPath = join22(this._localeDir, `${locale}${ext}`);
23387
23022
  if (existsSync20(yamlPath)) {
23388
23023
  try {
23389
- const raw = readFileSync20(yamlPath, "utf-8");
23024
+ const raw = readFileSync19(yamlPath, "utf-8");
23390
23025
  const data = _I18n._parseSimpleYaml(raw);
23391
23026
  this._translations.set(locale, _I18n._flatten(data));
23392
23027
  return;
@@ -24249,8 +23884,8 @@ var init_docsAutoDiscovery = __esm({
24249
23884
  // ../core/src/server.ts
24250
23885
  import { createServer as createServer2 } from "node:http";
24251
23886
  import { randomBytes as randomBytes5 } from "node:crypto";
24252
- import { resolve as resolve14, dirname as dirname11, join as join24, relative as relative9 } from "node:path";
24253
- import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as readFileSync22, statSync as statSync15 } from "node:fs";
23887
+ import { resolve as resolve14, dirname as dirname11, join as join24, relative as relative8 } from "node:path";
23888
+ import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
24254
23889
  import { isatty } from "node:tty";
24255
23890
  import { fileURLToPath as fileURLToPath6 } from "node:url";
24256
23891
  import { execFileSync as execFileSync3, exec } from "node:child_process";
@@ -24474,7 +24109,7 @@ function getGalleryDeployedState() {
24474
24109
  if (existsSync22(srcDir)) {
24475
24110
  const files = walkGalleryFiles(srcDir);
24476
24111
  const projectSrc = resolve14(process.cwd(), "src");
24477
- state[entry] = files.every((f) => existsSync22(join24(projectSrc, relative9(srcDir, f))));
24112
+ state[entry] = files.every((f) => existsSync22(join24(projectSrc, relative8(srcDir, f))));
24478
24113
  } else {
24479
24114
  state[entry] = false;
24480
24115
  }
@@ -24911,7 +24546,7 @@ function serveTemplateFallback(ctx) {
24911
24546
  if ((ctx.req.method ?? "GET") !== "GET") return false;
24912
24547
  const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
24913
24548
  if (!tplFile) return false;
24914
- const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync22(resolve14(ctx.templatesDir, tplFile), "utf-8");
24549
+ const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve14(ctx.templatesDir, tplFile), "utf-8");
24915
24550
  ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
24916
24551
  ctx.res.raw.end(html);
24917
24552
  return true;
@@ -26206,7 +25841,7 @@ var init_mqttMessage = __esm({
26206
25841
  import net2 from "node:net";
26207
25842
  import tls from "node:tls";
26208
25843
  import { randomBytes as randomBytes6 } from "node:crypto";
26209
- import { existsSync as existsSync24, readFileSync as readFileSync23 } from "node:fs";
25844
+ import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
26210
25845
  var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
26211
25846
  var init_mqtt = __esm({
26212
25847
  "../core/src/mqtt.ts"() {
@@ -26671,7 +26306,7 @@ var init_mqtt = __esm({
26671
26306
  servername: this.host,
26672
26307
  rejectUnauthorized: this.tlsVerify
26673
26308
  };
26674
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync23(this.caFile);
26309
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
26675
26310
  sock = tls.connect(opts, () => settle(() => resolve20(sock)));
26676
26311
  } else {
26677
26312
  sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
@@ -26892,7 +26527,7 @@ var init_mqtt = __esm({
26892
26527
 
26893
26528
  // ../core/src/service.ts
26894
26529
  import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
26895
- import { join as join26, extname as extname7 } from "node:path";
26530
+ import { join as join26, extname as extname6 } from "node:path";
26896
26531
  import { pathToFileURL } from "node:url";
26897
26532
  function matchCronField(field, value) {
26898
26533
  if (field === "*") return true;
@@ -27063,7 +26698,7 @@ var init_service = __esm({
27063
26698
  return discovered;
27064
26699
  }
27065
26700
  for (const entry of entries) {
27066
- const ext = extname7(entry);
26701
+ const ext = extname6(entry);
27067
26702
  if (ext !== ".ts" && ext !== ".js") continue;
27068
26703
  const fullPath = join26(dir, entry);
27069
26704
  const stat = statSync16(fullPath);
@@ -27179,7 +26814,7 @@ var init_service = __esm({
27179
26814
  return;
27180
26815
  }
27181
26816
  for (const entry of entries) {
27182
- const ext = extname7(entry);
26817
+ const ext = extname6(entry);
27183
26818
  if (ext !== ".ts" && ext !== ".js") continue;
27184
26819
  const fullPath = join26(dir, entry);
27185
26820
  if (watchedFiles.has(fullPath)) continue;
@@ -27882,7 +27517,7 @@ var init_api = __esm({
27882
27517
  // ../core/src/messenger.ts
27883
27518
  import net3 from "node:net";
27884
27519
  import tls2 from "node:tls";
27885
- import { readFileSync as readFileSync24 } from "node:fs";
27520
+ import { readFileSync as readFileSync23 } from "node:fs";
27886
27521
  import { basename as basename6 } from "node:path";
27887
27522
  import { randomUUID as randomUUID7 } from "node:crypto";
27888
27523
  function tlsRejectUnauthorized() {
@@ -27979,7 +27614,7 @@ function buildMimeMessage(options) {
27979
27614
  }
27980
27615
  for (const filePath of options.attachments) {
27981
27616
  const fileName = basename6(filePath);
27982
- const fileData = readFileSync24(filePath);
27617
+ const fileData = readFileSync23(filePath);
27983
27618
  const base64Data = fileData.toString("base64");
27984
27619
  lines.push("");
27985
27620
  lines.push(`--${boundary}`);
@@ -29442,9 +29077,9 @@ var init_htmlElement = __esm({
29442
29077
  });
29443
29078
 
29444
29079
  // ../core/src/ai.ts
29445
- import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as readFileSync25 } from "node:fs";
29080
+ import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
29446
29081
  import { homedir } from "node:os";
29447
- import { join as join27, resolve as resolve16, relative as relative10, dirname as dirname12 } from "node:path";
29082
+ import { join as join27, resolve as resolve16, relative as relative9, dirname as dirname12 } from "node:path";
29448
29083
  import { fileURLToPath as fileURLToPath7 } from "node:url";
29449
29084
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
29450
29085
  import { createInterface } from "node:readline";
@@ -29452,7 +29087,7 @@ function readVersion() {
29452
29087
  try {
29453
29088
  const thisDir = dirname12(fileURLToPath7(import.meta.url));
29454
29089
  const rootPkg = resolve16(thisDir, "..", "..", "..", "package.json");
29455
- const pkg = JSON.parse(readFileSync25(rootPkg, "utf-8"));
29090
+ const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
29456
29091
  return pkg.version ?? "0.0.0";
29457
29092
  } catch {
29458
29093
  return "0.0.0";
@@ -29675,7 +29310,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
29675
29310
  writeFileSync15(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
29676
29311
  return "Installed";
29677
29312
  }
29678
- const existing = readFileSync25(contextPath, "utf-8");
29313
+ const existing = readFileSync24(contextPath, "utf-8");
29679
29314
  if (hasMarkers(existing, start2, end)) {
29680
29315
  writeFileSync15(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
29681
29316
  return "Refreshed skill block in";
@@ -29699,7 +29334,7 @@ function installForTool(root, tool, context) {
29699
29334
  const parentDir = dirname12(contextPath);
29700
29335
  mkdirSync16(parentDir, { recursive: true });
29701
29336
  const action = writeOrMerge(contextPath, tool.contextFile, context);
29702
- const rel = relative10(root, contextPath);
29337
+ const rel = relative9(root, contextPath);
29703
29338
  created.push(rel);
29704
29339
  console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
29705
29340
  if (tool.name === "claude-code") {
@@ -30077,7 +29712,7 @@ function generateClaudeCodeContext() {
30077
29712
  const repoRoot = resolve16(thisDir, "..", "..", "..");
30078
29713
  const claudeMdPath = join27(repoRoot, "CLAUDE.md");
30079
29714
  if (existsSync25(claudeMdPath)) {
30080
- return readFileSync25(claudeMdPath, "utf-8");
29715
+ return readFileSync24(claudeMdPath, "utf-8");
30081
29716
  }
30082
29717
  } catch {
30083
29718
  }
@@ -30271,6 +29906,292 @@ export default class User {
30271
29906
  }
30272
29907
  });
30273
29908
 
29909
+ // ../core/src/aiClient.ts
29910
+ import http2 from "node:http";
29911
+ import https2 from "node:https";
29912
+ var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
29913
+ var init_aiClient = __esm({
29914
+ "../core/src/aiClient.ts"() {
29915
+ "use strict";
29916
+ AiError = class extends Error {
29917
+ };
29918
+ AiConfigError = class extends AiError {
29919
+ };
29920
+ AiTimeoutError = class extends AiError {
29921
+ };
29922
+ AiParseError = class extends AiError {
29923
+ };
29924
+ AiHTTPError = class extends AiError {
29925
+ constructor(message, status2 = null) {
29926
+ super(message);
29927
+ this.status = status2;
29928
+ }
29929
+ };
29930
+ Ai = class {
29931
+ static chat(messages, options = {}) {
29932
+ this.validateMessages(messages);
29933
+ const config = this.config("chat", options);
29934
+ const body = this.chatBody(config, messages, options);
29935
+ const headers = this.headers(config);
29936
+ return options.stream ? this.streamRequest(config, headers, body) : this.chatResponse(config, headers, body);
29937
+ }
29938
+ static async complete(prompt, options = {}) {
29939
+ if (typeof prompt !== "string") throw new AiConfigError("AI prompt must be a string");
29940
+ return (await this.chat([{ role: "user", content: prompt }], { ...options, stream: false })).text;
29941
+ }
29942
+ static async embed(textOrTexts, options = {}) {
29943
+ const single = typeof textOrTexts === "string";
29944
+ if (!single && (!Array.isArray(textOrTexts) || textOrTexts.length === 0 || !textOrTexts.every((item) => typeof item === "string"))) {
29945
+ throw new AiConfigError("AI embedding input must be a string or a non-empty list of strings");
29946
+ }
29947
+ const config = this.config("embed", options);
29948
+ if (config.provider === "anthropic") throw new AiConfigError("Anthropic does not provide the embedding endpoint in this contract");
29949
+ const raw = await this.requestJson(config, this.headers(config), { model: config.model, input: textOrTexts });
29950
+ try {
29951
+ const data = raw.data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
29952
+ const vectors = data.map((item) => item.embedding);
29953
+ const expected = single ? 1 : textOrTexts.length;
29954
+ if (vectors.length !== expected || !vectors.every((vector) => Array.isArray(vector) && vector.length > 0 && vector.every((value) => typeof value === "number" && Number.isFinite(value)))) throw new Error();
29955
+ return single ? vectors[0] : vectors;
29956
+ } catch {
29957
+ throw new AiParseError("AI provider returned a malformed embedding response");
29958
+ }
29959
+ }
29960
+ static validateMessages(messages) {
29961
+ if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
29962
+ throw new AiConfigError("AI messages must contain supported roles and string content");
29963
+ }
29964
+ }
29965
+ static number(name, fallback, minimum) {
29966
+ const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
29967
+ if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
29968
+ return value;
29969
+ }
29970
+ static config(capability, options) {
29971
+ const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
29972
+ if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
29973
+ const key = process.env.TINA4_AI_KEY || null;
29974
+ if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
29975
+ const defaults = {
29976
+ local: ["http://localhost:11437", "llama3.2"],
29977
+ openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
29978
+ anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
29979
+ };
29980
+ const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
29981
+ const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
29982
+ if (!model) throw new AiConfigError("AI model must be a non-empty string");
29983
+ const totalTimeout = options.timeout === void 0 ? this.number("TINA4_AI_TIMEOUT", 60, 1e-3) : Number(options.timeout);
29984
+ if (!Number.isFinite(totalTimeout) || totalTimeout <= 0) throw new AiConfigError("AI timeout must be greater than zero");
29985
+ return { provider, url: this.endpoint(rawUrl, capability, provider), model, key, totalTimeout, connectTimeout: this.number("TINA4_AI_CONNECT_TIMEOUT", 10, 1e-3), maxRetries: Math.trunc(this.number("TINA4_AI_MAX_RETRIES", 2, 0)) };
29986
+ }
29987
+ static endpoint(value, capability, provider) {
29988
+ let url;
29989
+ try {
29990
+ url = new URL(value);
29991
+ } catch {
29992
+ throw new AiConfigError("AI URL must be an http or https URL");
29993
+ }
29994
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
29995
+ const path8 = url.pathname.replace(/\/+$/, "");
29996
+ if (path8 === "" || path8 === "/v1" || path8 === "/api") {
29997
+ const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
29998
+ url.pathname = (path8 || "/v1") + suffix;
29999
+ }
30000
+ return url.toString();
30001
+ }
30002
+ static headers(config) {
30003
+ const headers = { "content-type": "application/json", accept: "application/json" };
30004
+ if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
30005
+ if (config.provider === "anthropic") {
30006
+ headers["x-api-key"] = config.key;
30007
+ headers["anthropic-version"] = "2023-06-01";
30008
+ }
30009
+ return headers;
30010
+ }
30011
+ static chatBody(config, messages, options) {
30012
+ const body = { model: config.model, messages, stream: options.stream ?? false };
30013
+ if (options.temperature !== void 0) body.temperature = options.temperature;
30014
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
30015
+ if (config.provider === "anthropic") {
30016
+ const system = messages.filter((message) => message.role === "system").map((message) => message.content);
30017
+ body.messages = messages.filter((message) => message.role !== "system");
30018
+ body.max_tokens = options.maxTokens ?? 1024;
30019
+ if (system.length) body.system = system.join("\n\n");
30020
+ }
30021
+ return body;
30022
+ }
30023
+ static open(config, deadline, headers, body) {
30024
+ const remainingMs = deadline - performance.now();
30025
+ if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
30026
+ const url = new URL(config.url);
30027
+ const payload = JSON.stringify(body);
30028
+ const controller = new AbortController();
30029
+ const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
30030
+ return new Promise((resolve20, reject) => {
30031
+ const client = url.protocol === "https:" ? https2 : http2;
30032
+ const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
30033
+ clearTimeout(connectTimer);
30034
+ resolve20({ response, cleanup: () => {
30035
+ clearTimeout(totalTimer);
30036
+ clearTimeout(connectTimer);
30037
+ } });
30038
+ });
30039
+ const connectTimer = setTimeout(() => request.destroy(new AiTimeoutError("AI connection timeout expired")), Math.min(config.connectTimeout * 1e3, remainingMs));
30040
+ request.on("socket", (socket) => {
30041
+ if (!socket.connecting) clearTimeout(connectTimer);
30042
+ socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
30043
+ });
30044
+ request.once("error", (error) => {
30045
+ clearTimeout(totalTimer);
30046
+ clearTimeout(connectTimer);
30047
+ if (error instanceof AiError) reject(error);
30048
+ else if (controller.signal.aborted) reject(new AiTimeoutError("AI total request timeout expired"));
30049
+ else reject(new AiHTTPError(`AI transport failed (${error.name})`));
30050
+ });
30051
+ request.end(payload);
30052
+ });
30053
+ }
30054
+ static async readBody(response) {
30055
+ const chunks = [];
30056
+ for await (const chunk of response) chunks.push(Buffer.from(chunk));
30057
+ return Buffer.concat(chunks).toString("utf8");
30058
+ }
30059
+ static retryDelay(headers, deadline) {
30060
+ const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
30061
+ const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
30062
+ const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
30063
+ return new Promise((resolve20) => setTimeout(resolve20, delay));
30064
+ }
30065
+ static async requestJson(config, headers, body) {
30066
+ const deadline = performance.now() + config.totalTimeout * 1e3;
30067
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
30068
+ let opened = null;
30069
+ try {
30070
+ opened = await this.open(config, deadline, headers, body);
30071
+ const status2 = opened.response.statusCode ?? 0;
30072
+ const responseHeaders = opened.response.headers;
30073
+ const raw = await this.readBody(opened.response);
30074
+ opened.cleanup();
30075
+ opened = null;
30076
+ if (status2 < 200 || status2 >= 300) {
30077
+ if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
30078
+ await this.retryDelay(responseHeaders, deadline);
30079
+ continue;
30080
+ }
30081
+ throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
30082
+ }
30083
+ let parsed;
30084
+ try {
30085
+ parsed = JSON.parse(raw);
30086
+ } catch {
30087
+ throw new AiParseError("AI provider returned malformed JSON");
30088
+ }
30089
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiParseError("AI provider returned a non-object JSON response");
30090
+ return parsed;
30091
+ } catch (error) {
30092
+ opened?.cleanup();
30093
+ if (error instanceof AiParseError || error instanceof AiHTTPError && error.status !== null) throw error;
30094
+ if (attempt >= config.maxRetries) throw error;
30095
+ }
30096
+ }
30097
+ throw new AiHTTPError("AI request failed");
30098
+ }
30099
+ static normalizeChat(provider, raw) {
30100
+ try {
30101
+ if (provider === "anthropic") {
30102
+ const content = raw.content;
30103
+ const parts = content.filter((item) => (item.type ?? "text") === "text").map((item) => item.text);
30104
+ if (!parts.length || !parts.every((part) => typeof part === "string")) throw new Error();
30105
+ const usage2 = raw.usage ?? {};
30106
+ const promptTokens = Number(usage2.input_tokens ?? 0);
30107
+ const completionTokens = Number(usage2.output_tokens ?? 0);
30108
+ return { text: parts.join(""), model: String(raw.model ?? ""), usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens }, finishReason: raw.stop_reason == null ? null : String(raw.stop_reason), raw };
30109
+ }
30110
+ const choice = raw.choices[0];
30111
+ const text = choice.message.content;
30112
+ if (typeof text !== "string") throw new Error();
30113
+ const usage = raw.usage ?? {};
30114
+ return { text, model: String(raw.model ?? ""), usage: { promptTokens: Number(usage.prompt_tokens ?? 0), completionTokens: Number(usage.completion_tokens ?? 0), totalTokens: Number(usage.total_tokens ?? 0) }, finishReason: choice.finish_reason == null ? null : String(choice.finish_reason), raw };
30115
+ } catch {
30116
+ throw new AiParseError("AI provider returned a malformed chat response");
30117
+ }
30118
+ }
30119
+ static async chatResponse(config, headers, body) {
30120
+ return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
30121
+ }
30122
+ static streamDelta(provider, data) {
30123
+ if (data === "[DONE]") return { completed: true };
30124
+ let event;
30125
+ try {
30126
+ event = JSON.parse(data);
30127
+ } catch {
30128
+ throw new AiParseError("AI provider returned malformed stream data");
30129
+ }
30130
+ const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
30131
+ if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
30132
+ return { completed: false, text };
30133
+ }
30134
+ static async *streamData(response) {
30135
+ let buffer = "";
30136
+ for await (const chunk of response) {
30137
+ buffer += Buffer.from(chunk).toString("utf8");
30138
+ let newline;
30139
+ while ((newline = buffer.indexOf("\n")) >= 0) {
30140
+ const line = buffer.slice(0, newline).trim();
30141
+ buffer = buffer.slice(newline + 1);
30142
+ if (line.startsWith("data:")) yield line.slice(5).trim();
30143
+ }
30144
+ }
30145
+ }
30146
+ static streamError(error) {
30147
+ if (error instanceof AiError) return error;
30148
+ if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
30149
+ return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
30150
+ }
30151
+ static async *streamRequest(config, headers, body) {
30152
+ const deadline = performance.now() + config.totalTimeout * 1e3;
30153
+ let yielded = false;
30154
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
30155
+ let opened = null;
30156
+ try {
30157
+ opened = await this.open(config, deadline, { ...headers, accept: "text/event-stream" }, body);
30158
+ const status2 = opened.response.statusCode ?? 0;
30159
+ if (status2 < 200 || status2 >= 300) {
30160
+ await this.readBody(opened.response);
30161
+ if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
30162
+ await this.retryDelay(opened.response.headers, deadline);
30163
+ opened.cleanup();
30164
+ opened = null;
30165
+ continue;
30166
+ }
30167
+ throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
30168
+ }
30169
+ let completed = false;
30170
+ for await (const data of this.streamData(opened.response)) {
30171
+ const delta = this.streamDelta(config.provider, data);
30172
+ if (delta.completed) {
30173
+ completed = true;
30174
+ break;
30175
+ }
30176
+ if (delta.text === void 0) continue;
30177
+ yielded = true;
30178
+ yield delta.text;
30179
+ }
30180
+ opened.cleanup();
30181
+ opened = null;
30182
+ if (completed) return;
30183
+ throw new AiParseError("AI provider stream ended before [DONE]");
30184
+ } catch (error) {
30185
+ opened?.cleanup();
30186
+ const failure = this.streamError(error);
30187
+ if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
30188
+ }
30189
+ }
30190
+ }
30191
+ };
30192
+ }
30193
+ });
30194
+
30274
30195
  // ../core/src/queueBackends/rabbitmqBackend.ts
30275
30196
  import { execFileSync as execFileSync5 } from "node:child_process";
30276
30197
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -31974,6 +31895,12 @@ __export(src_exports2, {
31974
31895
  APPLICATION_JSON: () => APPLICATION_JSON,
31975
31896
  APPLICATION_OCTET: () => APPLICATION_OCTET,
31976
31897
  APPLICATION_XML: () => APPLICATION_XML,
31898
+ Ai: () => Ai,
31899
+ AiConfigError: () => AiConfigError,
31900
+ AiError: () => AiError,
31901
+ AiHTTPError: () => AiHTTPError,
31902
+ AiParseError: () => AiParseError,
31903
+ AiTimeoutError: () => AiTimeoutError,
31977
31904
  Api: () => Api,
31978
31905
  Auth: () => Auth,
31979
31906
  CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
@@ -32301,6 +32228,7 @@ var init_src2 = __esm({
32301
32228
  init_htmlElement();
32302
32229
  init_errorOverlay();
32303
32230
  init_ai();
32231
+ init_aiClient();
32304
32232
  init_liteBackend();
32305
32233
  init_rabbitmqBackend();
32306
32234
  init_kafkaBackend();
@@ -37739,7 +37667,7 @@ var init_database = __esm({
37739
37667
 
37740
37668
  // src/model.ts
37741
37669
  import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
37742
- import { join as join29, extname as extname8 } from "node:path";
37670
+ import { join as join29, extname as extname7 } from "node:path";
37743
37671
  async function discoverModels(modelsDir) {
37744
37672
  const models = [];
37745
37673
  let files;
@@ -37752,7 +37680,7 @@ async function discoverModels(modelsDir) {
37752
37680
  const filePath = join29(modelsDir, file);
37753
37681
  const stat = statSync17(filePath);
37754
37682
  if (!stat.isFile()) continue;
37755
- const ext = extname8(file);
37683
+ const ext = extname7(file);
37756
37684
  if (ext !== ".ts" && ext !== ".js") continue;
37757
37685
  try {
37758
37686
  const moduleUrl = `file://${filePath}?t=${Date.now()}`;
@@ -37791,7 +37719,7 @@ var init_model = __esm({
37791
37719
  });
37792
37720
 
37793
37721
  // src/migration.ts
37794
- import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as readFileSync26, mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "node:fs";
37722
+ import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "node:fs";
37795
37723
  import { join as join30, resolve as resolve18 } from "node:path";
37796
37724
  function unwrapAdapter(db) {
37797
37725
  let cur = db;
@@ -38119,7 +38047,7 @@ async function rollback(migrationsDir, delimiter2) {
38119
38047
  `Cannot rollback ${migration.migration_name}: no .down.sql file found`
38120
38048
  );
38121
38049
  }
38122
- const sqlContent = readFileSync26(downPath, "utf-8").trim();
38050
+ const sqlContent = readFileSync25(downPath, "utf-8").trim();
38123
38051
  if (sqlContent) {
38124
38052
  const statements = splitStatements(sqlContent, delim);
38125
38053
  try {
@@ -38317,7 +38245,7 @@ async function migrate(adapter, options) {
38317
38245
  result.skipped.push(file);
38318
38246
  continue;
38319
38247
  }
38320
- const sqlContent = readFileSync26(join30(dir, file), "utf-8").trim();
38248
+ const sqlContent = readFileSync25(join30(dir, file), "utf-8").trim();
38321
38249
  if (!sqlContent) {
38322
38250
  result.skipped.push(file);
38323
38251
  continue;
@@ -42008,7 +41936,7 @@ var init_attachment = __esm({
42008
41936
 
42009
41937
  // src/realtime/storage.ts
42010
41938
  import { randomBytes as randomBytes9 } from "node:crypto";
42011
- import { mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync17, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
41939
+ import { mkdirSync as mkdirSync20, readFileSync as readFileSync26, writeFileSync as writeFileSync17, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
42012
41940
  import { resolve as resolve19, sep as sep5 } from "node:path";
42013
41941
  import { createRequire as createRequire8 } from "node:module";
42014
41942
  function storageKey(filename = "") {
@@ -42061,7 +41989,7 @@ var init_storage = __esm({
42061
41989
  }
42062
41990
  get(key) {
42063
41991
  try {
42064
- return readFileSync27(this.pathFor(key));
41992
+ return readFileSync26(this.pathFor(key));
42065
41993
  } catch {
42066
41994
  return null;
42067
41995
  }