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.
@@ -19566,14 +19566,14 @@ async function discoverRoutes(routesDir) {
19566
19566
  const currentMtime = statSync7(filePath).mtimeMs;
19567
19567
  if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
19568
19568
  const method = name.toUpperCase();
19569
- const relativePath3 = relative(routesDir, filePath);
19570
- const pattern = filePathToPattern(relativePath3);
19569
+ const relativePath2 = relative(routesDir, filePath);
19570
+ const pattern = filePathToPattern(relativePath2);
19571
19571
  try {
19572
19572
  const moduleUrl = `file://${filePath}?t=${currentMtime}`;
19573
19573
  const mod = await import(moduleUrl);
19574
19574
  const handler = mod.default ?? mod.handler;
19575
19575
  if (typeof handler !== "function") {
19576
- console.warn(` Warning: ${relativePath3} does not export a handler function, skipping`);
19576
+ console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
19577
19577
  continue;
19578
19578
  }
19579
19579
  const meta = mod.meta;
@@ -19585,7 +19585,7 @@ async function discoverRoutes(routesDir) {
19585
19585
  _seenMtimes.set(filePath, currentMtime);
19586
19586
  registeredFromThisScan++;
19587
19587
  } catch (err) {
19588
- console.error(` Error loading route ${relativePath3}:`, err);
19588
+ console.error(` Error loading route ${relativePath2}:`, err);
19589
19589
  recordBrokenImport(filePath, err);
19590
19590
  }
19591
19591
  }
@@ -19619,8 +19619,8 @@ function recordBrokenImport(filePath, error) {
19619
19619
  } catch {
19620
19620
  }
19621
19621
  }
19622
- function filePathToPattern(relativePath3) {
19623
- const parts = relativePath3.replace(/\\/g, "/").split("/").slice(0, -1);
19622
+ function filePathToPattern(relativePath2) {
19623
+ const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
19624
19624
  const urlParts = parts.map((part) => {
19625
19625
  if (part.startsWith("[...") && part.endsWith("]")) {
19626
19626
  const name = part.slice(4, -1);
@@ -22857,489 +22857,127 @@ import * as fs3 from "node:fs";
22857
22857
  import * as path2 from "node:path";
22858
22858
  import { spawnSync } from "node:child_process";
22859
22859
  import { fileURLToPath } from "node:url";
22860
- function walkFiles(dir, extensions, exclude = ["node_modules", ".git", "dist", "build"]) {
22861
- const results = [];
22862
- if (!fs3.existsSync(dir)) return results;
22863
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
22864
- for (const entry of entries) {
22865
- const fullPath = path2.join(dir, entry.name);
22866
- if (entry.isDirectory()) {
22867
- if (!exclude.includes(entry.name)) {
22868
- results.push(...walkFiles(fullPath, extensions, exclude));
22869
- }
22870
- } else if (entry.isFile()) {
22871
- const ext = path2.extname(entry.name);
22872
- if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
22873
- results.push(fullPath);
22874
- }
22875
- }
22876
- }
22877
- return results;
22878
- }
22879
- function readFileSafe(filePath) {
22880
- try {
22881
- return fs3.readFileSync(filePath, "utf-8");
22882
- } catch {
22883
- return null;
22884
- }
22885
- }
22886
- function relativePath(filePath, root = ".") {
22887
- return path2.relative(root, filePath);
22888
- }
22889
- function countLines(source) {
22890
- const lines = source.split("\n");
22891
- let loc = 0;
22892
- let blank = 0;
22893
- let comment = 0;
22894
- let inBlockComment = false;
22895
- for (const line of lines) {
22896
- const stripped = line.trim();
22897
- if (!stripped) {
22898
- blank++;
22899
- continue;
22900
- }
22901
- if (inBlockComment) {
22902
- comment++;
22903
- if (stripped.includes("*/")) {
22904
- inBlockComment = false;
22905
- }
22906
- continue;
22907
- }
22908
- if (stripped.startsWith("/*")) {
22909
- comment++;
22910
- if (!stripped.includes("*/") || stripped.endsWith("/*")) {
22911
- inBlockComment = true;
22912
- }
22913
- continue;
22914
- }
22915
- if (stripped.startsWith("//")) {
22916
- comment++;
22917
- continue;
22918
- }
22919
- loc++;
22920
- }
22921
- return { loc, blank, comment };
22922
- }
22923
- function stripLiterals(source) {
22924
- const out = [];
22925
- const n = source.length;
22926
- let i = 0;
22927
- let prevSignificant = "";
22928
- let prevWord = "";
22929
- const regexKeywords = /* @__PURE__ */ new Set([
22930
- "return",
22931
- "typeof",
22932
- "instanceof",
22933
- "in",
22934
- "of",
22935
- "new",
22936
- "delete",
22937
- "void",
22938
- "throw",
22939
- "case",
22940
- "do",
22941
- "else",
22942
- "yield",
22943
- "await"
22944
- ]);
22945
- function prevEndsExpression() {
22946
- if (prevSignificant === "") return false;
22947
- if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
22948
- return !regexKeywords.has(prevWord);
22949
- }
22950
- if (prevSignificant === ")" || prevSignificant === "]") return true;
22951
- if (prevSignificant === ".") return true;
22952
- return false;
22860
+ function containsTypeScript(directory) {
22861
+ if (!fs3.existsSync(directory) || !fs3.statSync(directory).isDirectory()) return false;
22862
+ for (const entry of fs3.readdirSync(directory, { withFileTypes: true })) {
22863
+ if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
22864
+ const target = path2.join(directory, entry.name);
22865
+ if (entry.isDirectory() ? containsTypeScript(target) : /\.[cm]?[jt]sx?$/.test(entry.name)) return true;
22953
22866
  }
22954
- while (i < n) {
22955
- const ch = source[i];
22956
- const next = i + 1 < n ? source[i + 1] : "";
22957
- if (ch === "/" && next === "/") {
22958
- out.push("//");
22959
- i += 2;
22960
- while (i < n && source[i] !== "\n") {
22961
- out.push(" ");
22962
- i++;
22963
- }
22964
- continue;
22965
- }
22966
- if (ch === "/" && next === "*") {
22967
- out.push("/*");
22968
- i += 2;
22969
- while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
22970
- out.push(source[i] === "\n" ? "\n" : " ");
22971
- i++;
22972
- }
22973
- if (i < n) {
22974
- out.push("*/");
22975
- i += 2;
22976
- }
22977
- continue;
22978
- }
22979
- if (ch === '"' || ch === "'") {
22980
- const quote = ch;
22981
- out.push(quote);
22982
- i++;
22983
- while (i < n && source[i] !== quote) {
22984
- if (source[i] === "\\" && i + 1 < n) {
22985
- out.push(" ");
22986
- i += 2;
22987
- continue;
22988
- }
22989
- if (source[i] === "\n") {
22990
- out.push("\n");
22991
- i++;
22992
- break;
22993
- }
22994
- out.push(" ");
22995
- i++;
22996
- }
22997
- if (i < n && source[i] === quote) {
22998
- out.push(quote);
22999
- i++;
23000
- }
23001
- prevSignificant = quote;
23002
- prevWord = "";
23003
- continue;
23004
- }
23005
- if (ch === "`") {
23006
- out.push("`");
23007
- i++;
23008
- while (i < n && source[i] !== "`") {
23009
- if (source[i] === "\\" && i + 1 < n) {
23010
- out.push(source[i + 1] === "\n" ? " \n" : " ");
23011
- i += 2;
23012
- continue;
23013
- }
23014
- if (source[i] === "$" && source[i + 1] === "{") {
23015
- out.push("${");
23016
- i += 2;
23017
- let depth = 1;
23018
- const exprStart = i;
23019
- while (i < n && depth > 0) {
23020
- if (source[i] === "{") depth++;
23021
- else if (source[i] === "}") depth--;
23022
- if (depth === 0) break;
23023
- i++;
23024
- }
23025
- out.push(stripLiterals(source.slice(exprStart, i)));
23026
- if (i < n && source[i] === "}") {
23027
- out.push("}");
23028
- i++;
23029
- }
23030
- continue;
23031
- }
23032
- out.push(source[i] === "\n" ? "\n" : " ");
23033
- i++;
23034
- }
23035
- if (i < n && source[i] === "`") {
23036
- out.push("`");
23037
- i++;
23038
- }
23039
- prevSignificant = "`";
23040
- prevWord = "";
23041
- continue;
23042
- }
23043
- if (ch === "/" && !prevEndsExpression()) {
23044
- let j = i + 1;
23045
- let ok = false;
23046
- let inClass = false;
23047
- while (j < n) {
23048
- const c = source[j];
23049
- if (c === "\\") {
23050
- j += 2;
23051
- continue;
23052
- }
23053
- if (c === "\n") break;
23054
- if (c === "[") inClass = true;
23055
- else if (c === "]") inClass = false;
23056
- else if (c === "/" && !inClass) {
23057
- ok = true;
23058
- break;
23059
- }
23060
- j++;
23061
- }
23062
- if (ok) {
23063
- out.push("/");
23064
- for (let k = i + 1; k < j; k++) out.push(" ");
23065
- out.push("/");
23066
- i = j + 1;
23067
- while (i < n && /[a-z]/i.test(source[i])) {
23068
- out.push(source[i]);
23069
- i++;
23070
- }
23071
- prevSignificant = "/";
23072
- prevWord = "";
23073
- continue;
23074
- }
23075
- }
23076
- out.push(ch);
23077
- if (!/\s/.test(ch)) {
23078
- prevSignificant = ch;
23079
- if (/[A-Za-z0-9_$]/.test(ch)) {
23080
- prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
23081
- } else {
23082
- prevWord = "";
23083
- }
23084
- }
23085
- i++;
23086
- }
23087
- return out.join("");
23088
- }
23089
- function countClassesQuick(source) {
23090
- const matches = source.match(
23091
- /(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
23092
- );
23093
- return matches ? matches.length : 0;
23094
- }
23095
- function countFunctionsQuick(source) {
23096
- const clean = stripLiterals(source);
23097
- let count = 0;
23098
- const funcDecls = clean.match(
23099
- /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
23100
- );
23101
- if (funcDecls) count += funcDecls.length;
23102
- const methods = clean.match(
23103
- /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
23104
- );
23105
- if (methods) count += methods.length;
23106
- const arrows = clean.match(
23107
- /(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
23108
- );
23109
- if (arrows) count += arrows.length;
23110
- return count;
23111
- }
23112
- function resolveRoot(root = "src") {
23113
- const rootPath = path2.resolve(root);
23114
- if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
23115
- _lastScanRoot = rootPath;
23116
- return root;
23117
- }
23118
- const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
23119
- _lastScanRoot = fwDir;
23120
- return fwDir;
23121
- }
23122
- function quickMetrics(root = "src") {
23123
- root = resolveRoot(root);
23124
- const rootPath = path2.resolve(root);
23125
- if (!fs3.existsSync(rootPath)) {
23126
- return { error: `Directory not found: ${root}` };
23127
- }
23128
- const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
23129
- const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
23130
- const migrationsDir = path2.resolve("migrations");
23131
- const migrationFiles = [
23132
- ...walkFiles(migrationsDir, [".sql"]),
23133
- ...walkFiles(migrationsDir, [".ts"])
23134
- ];
23135
- const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
23136
- let totalLoc = 0;
23137
- let totalBlank = 0;
23138
- let totalComment = 0;
23139
- let totalClasses = 0;
23140
- let totalFunctions = 0;
23141
- const fileDetails = [];
23142
- for (const f of tsFiles) {
23143
- const source = readFileSafe(f);
23144
- if (source === null) continue;
23145
- const counts = countLines(source);
23146
- const classes = countClassesQuick(source);
23147
- const functions = countFunctionsQuick(source);
23148
- totalLoc += counts.loc;
23149
- totalBlank += counts.blank;
23150
- totalComment += counts.comment;
23151
- totalClasses += classes;
23152
- totalFunctions += functions;
23153
- fileDetails.push({
23154
- path: relativePath(f, rootPath),
23155
- loc: counts.loc,
23156
- blank: counts.blank,
23157
- comment: counts.comment,
23158
- classes,
23159
- functions
23160
- });
23161
- }
23162
- fileDetails.sort((a, b) => b.loc - a.loc);
23163
- let routeCount = 0;
23164
- let ormCount = 0;
23165
- for (const f of tsFiles) {
23166
- const source = readFileSafe(f);
23167
- if (source === null) continue;
23168
- const routes = source.match(
23169
- /(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
23170
- );
23171
- if (routes) routeCount += routes.length;
23172
- const orms = source.match(
23173
- /class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
23174
- );
23175
- if (orms) ormCount += orms.length;
23176
- }
23177
- const breakdown = {
23178
- typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
23179
- javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
23180
- templates: twigFiles.length,
23181
- migrations: migrationFiles.length,
23182
- stylesheets: scssFiles.length
23183
- };
23184
- return {
23185
- file_count: tsFiles.length,
23186
- total_loc: totalLoc,
23187
- total_blank: totalBlank,
23188
- total_comment: totalComment,
23189
- lloc: totalLoc,
23190
- classes: totalClasses,
23191
- functions: totalFunctions,
23192
- route_count: routeCount,
23193
- orm_count: ormCount,
23194
- template_count: twigFiles.length,
23195
- migration_count: migrationFiles.length,
23196
- avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
23197
- largest_files: fileDetails.slice(0, 10),
23198
- breakdown
23199
- };
22867
+ return false;
23200
22868
  }
23201
- function resolveScanTarget(root = "src") {
23202
- const resolved = resolveRoot(root);
23203
- const frameworkDir = path2.dirname(fileURLToPath(import.meta.url));
23204
- const real = path2.resolve(resolved);
23205
- const scanningFramework = real === frameworkDir || real.startsWith(frameworkDir);
23206
- return [resolved, scanningFramework ? "framework" : "project"];
22869
+ function resolveTarget(root = "src") {
22870
+ const resolved = containsTypeScript(root) ? path2.resolve(root) : path2.dirname(fileURLToPath(import.meta.url));
22871
+ const mode = containsTypeScript(root) ? "project" : "framework";
22872
+ lastScanRoot = resolved;
22873
+ return [resolved, mode];
23207
22874
  }
23208
22875
  function enginePath() {
23209
- const names = process.platform === "win32" ? ["tina4.exe", "tina4.cmd", "tina4"] : ["tina4"];
23210
- for (const dir of (process.env.PATH || "").split(path2.delimiter)) {
23211
- if (!dir) continue;
22876
+ const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
22877
+ for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
23212
22878
  for (const name of names) {
23213
- const candidate = path2.join(dir, name);
22879
+ const candidate = path2.join(directory, name);
23214
22880
  try {
23215
- if (!fs3.statSync(candidate).isFile()) continue;
23216
22881
  fs3.accessSync(candidate, fs3.constants.X_OK);
22882
+ if (!fs3.statSync(candidate).isFile()) continue;
22883
+ const descriptor = fs3.openSync(candidate, "r");
22884
+ const header = Buffer.alloc(2);
22885
+ fs3.readSync(descriptor, header, 0, 2, 0);
22886
+ fs3.closeSync(descriptor);
22887
+ if (header.toString("latin1") !== "#!") return candidate;
23217
22888
  } catch {
23218
22889
  continue;
23219
22890
  }
23220
- try {
23221
- const fd = fs3.openSync(candidate, "r");
23222
- const buf = Buffer.alloc(2);
23223
- fs3.readSync(fd, buf, 0, 2, 0);
23224
- fs3.closeSync(fd);
23225
- if (buf.toString("latin1") === "#!") continue;
23226
- } catch {
23227
- }
23228
- return candidate;
23229
22891
  }
23230
22892
  }
23231
22893
  return null;
23232
22894
  }
23233
22895
  function runEngine(target) {
23234
22896
  const binary = enginePath();
23235
- if (binary === null) {
23236
- throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
23237
- }
23238
- const proc = spawnSync(binary, ["metrics", "--path", target, "--json"], {
22897
+ if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
22898
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
23239
22899
  encoding: "utf8",
23240
- timeout: TIMEOUT_MS,
22900
+ timeout: 6e4,
23241
22901
  maxBuffer: 64 * 1024 * 1024
23242
22902
  });
23243
- if (proc.error) {
23244
- const err = proc.error;
23245
- if (err.code === "ETIMEDOUT") {
23246
- throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
23247
- }
23248
- throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
23249
- }
23250
- if (proc.status !== 0) {
23251
- const detail = (proc.stderr || proc.stdout || "").trim().split("\n")[0];
23252
- throw new MetricsEngineError(
23253
- `tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
23254
- );
22903
+ if (processResult.error) {
22904
+ throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
23255
22905
  }
23256
- if (!proc.stdout || !proc.stdout.trim()) {
23257
- throw new MetricsEngineError(`tina4 metrics produced no output for ${target}`);
22906
+ if (processResult.status !== 0) {
22907
+ const detail = (processResult.stderr || processResult.stdout || "").trim().split("\n")[0];
22908
+ throw new MetricsEngineError(`tina4 metrics failed on ${target}: ${detail || processResult.status}`);
23258
22909
  }
23259
- let payload;
23260
22910
  try {
23261
- payload = JSON.parse(proc.stdout);
23262
- } catch (e) {
23263
- throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${e.message}`);
23264
- }
23265
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
23266
- throw new MetricsEngineError("tina4 metrics returned a non-object payload");
22911
+ const payload = JSON.parse(processResult.stdout);
22912
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
22913
+ throw new Error("non-object payload");
22914
+ }
22915
+ return payload;
22916
+ } catch (error) {
22917
+ throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${error.message}`);
23267
22918
  }
23268
- return payload;
23269
22919
  }
23270
- function requireKey(payload, key, isArray) {
23271
- const value = payload[key];
23272
- const ok = isArray ? Array.isArray(value) : value !== null && typeof value === "object" && !Array.isArray(value);
23273
- if (!ok) {
23274
- throw new MetricsEngineError(
23275
- `engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
23276
- );
22920
+ function requireArray(payload, key) {
22921
+ if (!Array.isArray(payload[key])) {
22922
+ throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
23277
22923
  }
23278
- return value;
22924
+ return payload[key];
23279
22925
  }
23280
22926
  function fullAnalysis(root = "src") {
23281
- const [resolved, scanMode] = resolveScanTarget(root);
22927
+ const [resolved, scanMode] = resolveTarget(root);
23282
22928
  const payload = runEngine(resolved);
23283
- const summary = requireKey(payload, "summary", false);
23284
- const fileMetrics = requireKey(payload, "file_metrics", true);
23285
- const functions = requireKey(payload, "most_complex_functions", true);
23286
- const missing = SUMMARY_KEYS.filter((k) => !(k in summary));
23287
- if (missing.length) {
23288
- throw new MetricsEngineError(
23289
- `engine summary is missing ${missing.join(", ")} - update the CLI: ${INSTALL_HINT}`
23290
- );
23291
- }
23292
- if (fileMetrics.length) {
23293
- const absent = FILE_KEYS.filter((k) => !(k in fileMetrics[0]));
23294
- if (absent.length) throw new MetricsEngineError(`engine file_metrics is missing ${absent.join(", ")}`);
22929
+ const summary = payload.summary;
22930
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
22931
+ throw new MetricsEngineError(`engine payload has no usable 'summary' - ${INSTALL_HINT}`);
22932
+ }
22933
+ const fileMetrics = requireArray(payload, "file_metrics");
22934
+ const functions = requireArray(payload, "most_complex_functions");
22935
+ const missingSummary = SUMMARY_KEYS.filter((key) => !(key in summary));
22936
+ if (missingSummary.length) throw new MetricsEngineError(`engine summary is missing ${missingSummary.join(", ")}`);
22937
+ const missingFile = fileMetrics.length ? FILE_KEYS.filter((key) => !(key in fileMetrics[0])) : [];
22938
+ if (missingFile.length) throw new MetricsEngineError(`engine file_metrics is missing ${missingFile.join(", ")}`);
22939
+ const missingFunction = functions.length ? FUNCTION_KEYS.filter((key) => !(key in functions[0])) : [];
22940
+ if (missingFunction.length) {
22941
+ throw new MetricsEngineError(`engine function metrics are missing ${missingFunction.join(", ")}`);
23295
22942
  }
23296
- if (functions.length) {
23297
- const absent = FUNCTION_KEYS.filter((k) => !(k in functions[0]));
23298
- if (absent.length) throw new MetricsEngineError(`engine function metrics are missing ${absent.join(", ")}`);
23299
- }
23300
- const result = {};
23301
- for (const key of SUMMARY_KEYS) result[key] = summary[key];
23302
- result.file_metrics = fileMetrics;
23303
- result.most_complex_functions = functions.slice(0, 15);
23304
- result.dependency_graph = payload.dependency_graph || {};
23305
- result.scan_mode = scanMode;
23306
- result.scan_root = path2.resolve(resolved);
23307
- result.engine = "tina4-cli";
23308
- return result;
22943
+ return {
22944
+ ...Object.fromEntries(SUMMARY_KEYS.map((key) => [key, summary[key]])),
22945
+ file_metrics: fileMetrics,
22946
+ most_complex_functions: functions.slice(0, 15),
22947
+ dependency_graph: payload.dependency_graph || {},
22948
+ scan_mode: scanMode,
22949
+ scan_root: resolved,
22950
+ engine: "tina4-cli"
22951
+ };
23309
22952
  }
23310
22953
  function fileDetail(filePath) {
23311
22954
  if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
23312
22955
  let target = filePath;
23313
- if (!fs3.existsSync(target) && _lastScanRoot) {
23314
- const candidate = path2.join(_lastScanRoot, filePath);
23315
- if (fs3.existsSync(candidate)) target = candidate;
23316
- }
22956
+ if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
23317
22957
  if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
23318
22958
  if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
23319
22959
  const payload = runEngine(target);
23320
- const fileMetrics = requireKey(payload, "file_metrics", true);
23321
- if (!fileMetrics.length) {
23322
- throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
23323
- }
23324
- return { ...fileMetrics[0], engine: "tina4-cli" };
22960
+ const files = requireArray(payload, "file_metrics");
22961
+ if (!files.length) throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
22962
+ return {
22963
+ ...files[0],
22964
+ function_count: files[0].functions || 0,
22965
+ functions: requireArray(payload, "most_complex_functions"),
22966
+ engine: "tina4-cli"
22967
+ };
23325
22968
  }
23326
- var _lastScanRoot, MetricsEngineError, TIMEOUT_MS, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
22969
+ var lastScanRoot, MetricsEngineError, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
23327
22970
  var init_metrics = __esm({
23328
22971
  "src/metrics.ts"() {
23329
22972
  "use strict";
23330
- _lastScanRoot = "";
22973
+ lastScanRoot = "";
23331
22974
  MetricsEngineError = class extends Error {
23332
22975
  constructor(message) {
23333
22976
  super(message);
23334
22977
  this.name = "MetricsEngineError";
23335
22978
  }
23336
22979
  };
23337
- TIMEOUT_MS = 6e4;
23338
- INSTALL_HINT = [
23339
- "the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
23340
- " curl -fsSL https://tina4.com/install.sh | sh",
23341
- "or see https://tina4.com/cli"
23342
- ].join("\n");
22980
+ INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
23343
22981
  SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
23344
22982
  FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
23345
22983
  FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
@@ -23347,7 +22985,7 @@ var init_metrics = __esm({
23347
22985
  });
23348
22986
 
23349
22987
  // src/feedback.ts
23350
- import { readFileSync as readFileSync13, existsSync as existsSync13 } from "node:fs";
22988
+ import { readFileSync as readFileSync12, existsSync as existsSync13 } from "node:fs";
23351
22989
  import { dirname as dirname7, join as join18, resolve as resolve9 } from "node:path";
23352
22990
  import { fileURLToPath as fileURLToPath2 } from "node:url";
23353
22991
  function feedbackEnabled() {
@@ -23488,7 +23126,7 @@ var init_feedback = __esm({
23488
23126
  handleFeedbackWidgetJs = (_req, res) => {
23489
23127
  let body;
23490
23128
  if (existsSync13(WIDGET_BUNDLE_PATH)) {
23491
- body = readFileSync13(WIDGET_BUNDLE_PATH);
23129
+ body = readFileSync12(WIDGET_BUNDLE_PATH);
23492
23130
  } else {
23493
23131
  body = "console.warn('tina4-feedback-widget bundle not built yet');";
23494
23132
  }
@@ -23503,7 +23141,7 @@ var init_feedback = __esm({
23503
23141
  });
23504
23142
 
23505
23143
  // src/version.ts
23506
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
23144
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
23507
23145
  import { dirname as dirname8, join as join19 } from "node:path";
23508
23146
  import { fileURLToPath as fileURLToPath3 } from "node:url";
23509
23147
  function resolveFrameworkVersion() {
@@ -23512,7 +23150,7 @@ function resolveFrameworkVersion() {
23512
23150
  const pkgPath = join19(dir, "package.json");
23513
23151
  if (existsSync14(pkgPath)) {
23514
23152
  try {
23515
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
23153
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
23516
23154
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
23517
23155
  } catch {
23518
23156
  }
@@ -26300,8 +25938,8 @@ __export(context_exports, {
26300
25938
  fts5Supported: () => fts5Supported
26301
25939
  });
26302
25940
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
26303
- import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync16, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
26304
- import { basename as basename4, dirname as dirname10, extname as extname6, isAbsolute as isAbsolute6, join as join21, relative as relative4, resolve as resolve11 } from "node:path";
25941
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync15, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
25942
+ import { basename as basename4, dirname as dirname10, extname as extname5, isAbsolute as isAbsolute6, join as join21, relative as relative3, resolve as resolve11 } from "node:path";
26305
25943
  function fts5Supported() {
26306
25944
  try {
26307
25945
  const conn = new DatabaseSync4(":memory:");
@@ -26436,7 +26074,7 @@ var init_context = __esm({
26436
26074
  }
26437
26075
  // ── indexing ───────────────────────────────────────────────
26438
26076
  static chunksFor(label, text) {
26439
- const ext = extname6(label).toLowerCase();
26077
+ const ext = extname5(label).toLowerCase();
26440
26078
  const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
26441
26079
  if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
26442
26080
  return chunkCode(text, label);
@@ -26454,7 +26092,7 @@ var init_context = __esm({
26454
26092
  const stored = label != null ? String(label) : String(file);
26455
26093
  let text;
26456
26094
  try {
26457
- text = readFileSync16(file, "utf-8");
26095
+ text = readFileSync15(file, "utf-8");
26458
26096
  } catch {
26459
26097
  return 0;
26460
26098
  }
@@ -26475,7 +26113,7 @@ var init_context = __esm({
26475
26113
  static eligible(filename) {
26476
26114
  const fn = filename.toLowerCase();
26477
26115
  if (fn.endsWith(".min.js")) return false;
26478
- const ext = extname6(fn);
26116
+ const ext = extname5(fn);
26479
26117
  return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
26480
26118
  }
26481
26119
  /**
@@ -26504,7 +26142,7 @@ var init_context = __esm({
26504
26142
  for (const fn of files) {
26505
26143
  if (!_Context.eligible(fn)) continue;
26506
26144
  const full = join21(dir, fn);
26507
- const rel = relative4(rootAbs, full);
26145
+ const rel = relative3(rootAbs, full);
26508
26146
  total += this.indexPath(full, rel);
26509
26147
  }
26510
26148
  for (const d of subdirs) walk2(join21(dir, d));
@@ -26525,7 +26163,7 @@ var init_context = __esm({
26525
26163
  const raw = String(changedPath);
26526
26164
  const abs = isAbsolute6(raw) ? raw : join21(process.cwd(), raw);
26527
26165
  const resolved = realResolve(resolve11(abs));
26528
- const rel = relative4(this.root, resolved);
26166
+ const rel = relative3(this.root, resolved);
26529
26167
  if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
26530
26168
  return -1;
26531
26169
  }
@@ -28447,7 +28085,7 @@ var init_job = __esm({
28447
28085
  });
28448
28086
 
28449
28087
  // src/queueBackends/liteBackend.ts
28450
- import { mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7, existsSync as existsSync17 } from "node:fs";
28088
+ import { mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7, existsSync as existsSync17 } from "node:fs";
28451
28089
  import { join as join22 } from "node:path";
28452
28090
  import { randomUUID as randomUUID6 } from "node:crypto";
28453
28091
  var LiteBackend;
@@ -28547,7 +28185,7 @@ var init_liteBackend = __esm({
28547
28185
  const filePath = join22(dir, filename);
28548
28186
  let job;
28549
28187
  try {
28550
- job = JSON.parse(readFileSync17(filePath, "utf-8"));
28188
+ job = JSON.parse(readFileSync16(filePath, "utf-8"));
28551
28189
  } catch {
28552
28190
  continue;
28553
28191
  }
@@ -28611,7 +28249,7 @@ var init_liteBackend = __esm({
28611
28249
  const filePath = join22(reservedDir, filename);
28612
28250
  let record;
28613
28251
  try {
28614
- record = JSON.parse(readFileSync17(filePath, "utf-8"));
28252
+ record = JSON.parse(readFileSync16(filePath, "utf-8"));
28615
28253
  } catch {
28616
28254
  continue;
28617
28255
  }
@@ -28724,7 +28362,7 @@ var init_liteBackend = __esm({
28724
28362
  let count = 0;
28725
28363
  for (const file of files) {
28726
28364
  try {
28727
- const job = JSON.parse(readFileSync17(join22(scanDir, file), "utf-8"));
28365
+ const job = JSON.parse(readFileSync16(join22(scanDir, file), "utf-8"));
28728
28366
  if (job.status === status2) count++;
28729
28367
  } catch {
28730
28368
  }
@@ -28781,7 +28419,7 @@ var init_liteBackend = __esm({
28781
28419
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
28782
28420
  for (const file of files) {
28783
28421
  try {
28784
- const job = JSON.parse(readFileSync17(join22(dir, file), "utf-8"));
28422
+ const job = JSON.parse(readFileSync16(join22(dir, file), "utf-8"));
28785
28423
  const attempts = job.attempts || 0;
28786
28424
  if (attempts > 0 && attempts < maxRetries) {
28787
28425
  results.push(job);
@@ -28807,7 +28445,7 @@ var init_liteBackend = __esm({
28807
28445
  const failedDir = join22(this.basePath, q, "failed");
28808
28446
  const filePath = join22(failedDir, `${jobId}.queue-data`);
28809
28447
  if (existsSync17(filePath)) {
28810
- const job = JSON.parse(readFileSync17(filePath, "utf-8"));
28448
+ const job = JSON.parse(readFileSync16(filePath, "utf-8"));
28811
28449
  job.status = "pending";
28812
28450
  job.attempts = (job.attempts || 0) + 1;
28813
28451
  job.error = void 0;
@@ -28831,7 +28469,7 @@ var init_liteBackend = __esm({
28831
28469
  const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
28832
28470
  for (const file of files) {
28833
28471
  try {
28834
- const job = JSON.parse(readFileSync17(join22(failedDir, file), "utf-8"));
28472
+ const job = JSON.parse(readFileSync16(join22(failedDir, file), "utf-8"));
28835
28473
  if ((job.attempts || 0) >= maxRetries) {
28836
28474
  job.status = "dead";
28837
28475
  results.push(job);
@@ -28865,7 +28503,7 @@ var init_liteBackend = __esm({
28865
28503
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
28866
28504
  for (const file of files) {
28867
28505
  try {
28868
- const job = JSON.parse(readFileSync17(join22(dir, file), "utf-8"));
28506
+ const job = JSON.parse(readFileSync16(join22(dir, file), "utf-8"));
28869
28507
  if (job.status === status2) {
28870
28508
  unlinkSync7(join22(dir, file));
28871
28509
  count++;
@@ -28892,7 +28530,7 @@ var init_liteBackend = __esm({
28892
28530
  for (const file of files) {
28893
28531
  try {
28894
28532
  const filePath = join22(failedDir, file);
28895
- const job = JSON.parse(readFileSync17(filePath, "utf-8"));
28533
+ const job = JSON.parse(readFileSync16(filePath, "utf-8"));
28896
28534
  if ((job.attempts || 0) >= maxRetries) {
28897
28535
  continue;
28898
28536
  }
@@ -28924,7 +28562,7 @@ var init_liteBackend = __esm({
28924
28562
  const filePath = join22(dir, file);
28925
28563
  let job;
28926
28564
  try {
28927
- job = JSON.parse(readFileSync17(filePath, "utf-8"));
28565
+ job = JSON.parse(readFileSync16(filePath, "utf-8"));
28928
28566
  } catch {
28929
28567
  continue;
28930
28568
  }
@@ -30387,7 +30025,7 @@ function detectVersion(projectRoot3) {
30387
30025
  }
30388
30026
  return "0.0.0";
30389
30027
  }
30390
- function relativePath2(absPath, projectRoot3, frameworkRoots) {
30028
+ function relativePath(absPath, projectRoot3, frameworkRoots) {
30391
30029
  const norm = path6.resolve(absPath);
30392
30030
  for (const fw of frameworkRoots) {
30393
30031
  const parent = path6.dirname(fw);
@@ -30852,7 +30490,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
30852
30490
  } catch {
30853
30491
  return;
30854
30492
  }
30855
- const rel = relativePath2(absPath, projectRoot3, fwRoots);
30493
+ const rel = relativePath(absPath, projectRoot3, fwRoots);
30856
30494
  for (const cls of parsed.classes) {
30857
30495
  if (!cls.exported && source === "framework") {
30858
30496
  continue;
@@ -31488,8 +31126,8 @@ ${end}
31488
31126
 
31489
31127
  // src/devAdmin.ts
31490
31128
  import { cpus as osCpus } from "node:os";
31491
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, existsSync as existsSync21, readdirSync as readdirSync15, mkdirSync as mkdirSync17, copyFileSync, statSync as statSync16 } from "node:fs";
31492
- import { join as join26, dirname as dirname12, resolve as resolve15, relative as relative8 } from "node:path";
31129
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, existsSync as existsSync21, readdirSync as readdirSync15, mkdirSync as mkdirSync17, copyFileSync, statSync as statSync16 } from "node:fs";
31130
+ import { join as join26, dirname as dirname12, resolve as resolve15, relative as relative7 } from "node:path";
31493
31131
  import { fileURLToPath as fileURLToPath5 } from "node:url";
31494
31132
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
31495
31133
  function escapeHtml(value) {
@@ -31608,7 +31246,7 @@ function readQueueDir(dir, topic, status2) {
31608
31246
  for (const filename of readdirSync15(dir).sort()) {
31609
31247
  if (!filename.endsWith(".queue-data")) continue;
31610
31248
  try {
31611
- jobs.push(mapQueueJob(JSON.parse(readFileSync21(join26(dir, filename), "utf-8")), topic, status2));
31249
+ jobs.push(mapQueueJob(JSON.parse(readFileSync20(join26(dir, filename), "utf-8")), topic, status2));
31612
31250
  } catch {
31613
31251
  }
31614
31252
  }
@@ -31727,7 +31365,7 @@ function resolveDevEnvVar(key) {
31727
31365
  if (live !== void 0 && live !== "") return live;
31728
31366
  const envPath = join26(process.cwd(), ".env");
31729
31367
  if (!existsSync21(envPath)) return "";
31730
- for (const line of readFileSync21(envPath, "utf-8").split("\n")) {
31368
+ for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
31731
31369
  const t = line.trim();
31732
31370
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
31733
31371
  const eq = t.indexOf("=");
@@ -31737,7 +31375,7 @@ function resolveDevEnvVar(key) {
31737
31375
  }
31738
31376
  function upsertDevEnvVar(key, value) {
31739
31377
  const envPath = join26(process.cwd(), ".env");
31740
- const lines = existsSync21(envPath) ? readFileSync21(envPath, "utf-8").split("\n") : [];
31378
+ const lines = existsSync21(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
31741
31379
  let found = false;
31742
31380
  const out = [];
31743
31381
  for (const line of lines) {
@@ -31770,7 +31408,7 @@ function parseEnvFile() {
31770
31408
  const envPath = join26(process.cwd(), ".env");
31771
31409
  const result = {};
31772
31410
  if (!existsSync21(envPath)) return result;
31773
- const lines = readFileSync21(envPath, "utf-8").split("\n");
31411
+ const lines = readFileSync20(envPath, "utf-8").split("\n");
31774
31412
  for (const line of lines) {
31775
31413
  const trimmed = line.trim();
31776
31414
  if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -31810,7 +31448,7 @@ function handleGalleryDeploy(router) {
31810
31448
  const copied = [];
31811
31449
  const allFiles = walkDirRecursive(gallerySrc);
31812
31450
  for (const srcFile of allFiles) {
31813
- const rel = relative8(gallerySrc, srcFile);
31451
+ const rel = relative7(gallerySrc, srcFile);
31814
31452
  const dest = join26(projectSrc, rel);
31815
31453
  mkdirSync17(dirname12(dest), { recursive: true });
31816
31454
  copyFileSync(srcFile, dest);
@@ -32472,9 +32110,6 @@ var init_devAdmin = __esm({
32472
32110
  { method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
32473
32111
  { method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
32474
32112
  // Metrics
32475
- { method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
32476
- res.json(quickMetrics());
32477
- } },
32478
32113
  // No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
32479
32114
  // install command, never zeros that read as a healthy codebase.
32480
32115
  { method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
@@ -33283,7 +32918,7 @@ var init_devAdmin = __esm({
33283
32918
  }
33284
32919
  try {
33285
32920
  const envPath = join26(process.cwd(), ".env");
33286
- const lines = existsSync21(envPath) ? readFileSync21(envPath, "utf-8").split("\n") : [];
32921
+ const lines = existsSync21(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
33287
32922
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
33288
32923
  const newLines = [];
33289
32924
  for (const line of lines) {
@@ -33329,12 +32964,12 @@ var init_devAdmin = __esm({
33329
32964
  const metaFile = join26(entryPath, "meta.json");
33330
32965
  if (statSync16(entryPath).isDirectory() && existsSync21(metaFile)) {
33331
32966
  try {
33332
- const meta = JSON.parse(readFileSync21(metaFile, "utf-8"));
32967
+ const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
33333
32968
  meta.id = entry;
33334
32969
  const srcDir = join26(entryPath, "src");
33335
32970
  if (existsSync21(srcDir)) {
33336
32971
  const allFiles = walkDirRecursive(srcDir);
33337
- meta.files = allFiles.map((f) => relative8(srcDir, f));
32972
+ meta.files = allFiles.map((f) => relative7(srcDir, f));
33338
32973
  }
33339
32974
  const projectSrc = resolve15(process.cwd(), "src");
33340
32975
  if (existsSync21(srcDir) && meta.files) {
@@ -33452,7 +33087,7 @@ var init_devAdmin = __esm({
33452
33087
  for (const name of readdirSync15(target).sort()) {
33453
33088
  if (devFilesHidden(name)) continue;
33454
33089
  const full = join26(target, name);
33455
- const entryRel = relative8(root, full).replace(/\\/g, "/");
33090
+ const entryRel = relative7(root, full).replace(/\\/g, "/");
33456
33091
  if (isSecretPath(entryRel)) continue;
33457
33092
  let isDir = false;
33458
33093
  let size = null;
@@ -33497,7 +33132,7 @@ var init_devAdmin = __esm({
33497
33132
  size
33498
33133
  });
33499
33134
  }
33500
- res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
33135
+ res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
33501
33136
  };
33502
33137
  DEV_ADMIN_LANG_MAP = {
33503
33138
  ".py": "python",
@@ -33549,8 +33184,8 @@ var init_devAdmin = __esm({
33549
33184
  return;
33550
33185
  }
33551
33186
  try {
33552
- const content = readFileSync21(target, "utf-8");
33553
- const path8 = relative8(root, target);
33187
+ const content = readFileSync20(target, "utf-8");
33188
+ const path8 = relative7(root, target);
33554
33189
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
33555
33190
  } catch (e) {
33556
33191
  res.json({ error: e.message }, 500);
@@ -33572,10 +33207,10 @@ var init_devAdmin = __esm({
33572
33207
  writeFileSync14(target, content, "utf-8");
33573
33208
  try {
33574
33209
  const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
33575
- Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
33210
+ Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
33576
33211
  } catch {
33577
33212
  }
33578
- res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
33213
+ res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
33579
33214
  } catch (e) {
33580
33215
  res.json({ error: e.message }, 500);
33581
33216
  }
@@ -33595,7 +33230,7 @@ var init_devAdmin = __esm({
33595
33230
  return;
33596
33231
  }
33597
33232
  try {
33598
- const buf = readFileSync21(target);
33233
+ const buf = readFileSync20(target);
33599
33234
  const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
33600
33235
  const mime = {
33601
33236
  js: "application/javascript",
@@ -33637,7 +33272,7 @@ var init_devAdmin = __esm({
33637
33272
  const { renameSync: renameSync3 } = await import("node:fs");
33638
33273
  mkdirSync17(dirname12(dst), { recursive: true });
33639
33274
  renameSync3(src, dst);
33640
- res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
33275
+ res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
33641
33276
  } catch (e) {
33642
33277
  res.json({ error: e.message }, 500);
33643
33278
  }
@@ -33658,7 +33293,7 @@ var init_devAdmin = __esm({
33658
33293
  try {
33659
33294
  const { rmSync } = await import("node:fs");
33660
33295
  rmSync(target, { recursive: true, force: true });
33661
- res.json({ ok: true, deleted: relative8(root, target) });
33296
+ res.json({ ok: true, deleted: relative7(root, target) });
33662
33297
  } catch (e) {
33663
33298
  res.json({ error: e.message }, 500);
33664
33299
  }
@@ -33992,7 +33627,7 @@ var init_devAdmin = __esm({
33992
33627
  });
33993
33628
  };
33994
33629
  handleDevAdminJs = async (_req, res) => {
33995
- const { readFileSync: readFileSync28, existsSync: existsSync27 } = await import("node:fs");
33630
+ const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
33996
33631
  const { dirname: dirname15, join: join32, resolve: resolve20 } = await import("node:path");
33997
33632
  const { fileURLToPath: fileURLToPath8 } = await import("node:url");
33998
33633
  const dir = dirname15(fileURLToPath8(import.meta.url));
@@ -34008,7 +33643,7 @@ var init_devAdmin = __esm({
34008
33643
  for (const jsPath of candidates) {
34009
33644
  if (existsSync27(jsPath)) {
34010
33645
  try {
34011
- const content = readFileSync28(jsPath, "utf-8");
33646
+ const content = readFileSync27(jsPath, "utf-8");
34012
33647
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
34013
33648
  res.raw.end(content);
34014
33649
  return;
@@ -34023,7 +33658,7 @@ var init_devAdmin = __esm({
34023
33658
  });
34024
33659
 
34025
33660
  // src/i18n.ts
34026
- import { readFileSync as readFileSync22, readdirSync as readdirSync16, existsSync as existsSync22 } from "node:fs";
33661
+ import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as existsSync22 } from "node:fs";
34027
33662
  import { join as join27, resolve as resolve16 } from "node:path";
34028
33663
  var I18n;
34029
33664
  var init_i18n = __esm({
@@ -34120,7 +33755,7 @@ var init_i18n = __esm({
34120
33755
  const filePath = join27(this._localeDir, `${locale}.json`);
34121
33756
  if (existsSync22(filePath)) {
34122
33757
  try {
34123
- const raw = readFileSync22(filePath, "utf-8");
33758
+ const raw = readFileSync21(filePath, "utf-8");
34124
33759
  const data = JSON.parse(raw);
34125
33760
  this._translations.set(locale, _I18n._flatten(data));
34126
33761
  return;
@@ -34133,7 +33768,7 @@ var init_i18n = __esm({
34133
33768
  const yamlPath = join27(this._localeDir, `${locale}${ext}`);
34134
33769
  if (existsSync22(yamlPath)) {
34135
33770
  try {
34136
- const raw = readFileSync22(yamlPath, "utf-8");
33771
+ const raw = readFileSync21(yamlPath, "utf-8");
34137
33772
  const data = _I18n._parseSimpleYaml(raw);
34138
33773
  this._translations.set(locale, _I18n._flatten(data));
34139
33774
  return;
@@ -34996,8 +34631,8 @@ var init_docsAutoDiscovery = __esm({
34996
34631
  // src/server.ts
34997
34632
  import { createServer as createServer2 } from "node:http";
34998
34633
  import { randomBytes as randomBytes7 } from "node:crypto";
34999
- import { resolve as resolve18, dirname as dirname13, join as join29, relative as relative9 } from "node:path";
35000
- import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync24, statSync as statSync17 } from "node:fs";
34634
+ import { resolve as resolve18, dirname as dirname13, join as join29, relative as relative8 } from "node:path";
34635
+ import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
35001
34636
  import { isatty } from "node:tty";
35002
34637
  import { fileURLToPath as fileURLToPath6 } from "node:url";
35003
34638
  import { execFileSync as execFileSync3, exec } from "node:child_process";
@@ -35221,7 +34856,7 @@ function getGalleryDeployedState() {
35221
34856
  if (existsSync24(srcDir)) {
35222
34857
  const files = walkGalleryFiles(srcDir);
35223
34858
  const projectSrc = resolve18(process.cwd(), "src");
35224
- state[entry] = files.every((f) => existsSync24(join29(projectSrc, relative9(srcDir, f))));
34859
+ state[entry] = files.every((f) => existsSync24(join29(projectSrc, relative8(srcDir, f))));
35225
34860
  } else {
35226
34861
  state[entry] = false;
35227
34862
  }
@@ -35658,7 +35293,7 @@ function serveTemplateFallback(ctx) {
35658
35293
  if ((ctx.req.method ?? "GET") !== "GET") return false;
35659
35294
  const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
35660
35295
  if (!tplFile) return false;
35661
- const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync24(resolve18(ctx.templatesDir, tplFile), "utf-8");
35296
+ const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(resolve18(ctx.templatesDir, tplFile), "utf-8");
35662
35297
  ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
35663
35298
  ctx.res.raw.end(html);
35664
35299
  return true;
@@ -36485,7 +36120,7 @@ var init_mqttMessage = __esm({
36485
36120
  import net2 from "node:net";
36486
36121
  import tls from "node:tls";
36487
36122
  import { randomBytes as randomBytes8 } from "node:crypto";
36488
- import { existsSync as existsSync25, readFileSync as readFileSync25 } from "node:fs";
36123
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
36489
36124
  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;
36490
36125
  var init_mqtt = __esm({
36491
36126
  "src/mqtt.ts"() {
@@ -36950,7 +36585,7 @@ var init_mqtt = __esm({
36950
36585
  servername: this.host,
36951
36586
  rejectUnauthorized: this.tlsVerify
36952
36587
  };
36953
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync25(this.caFile);
36588
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
36954
36589
  sock = tls.connect(opts, () => settle(() => resolve20(sock)));
36955
36590
  } else {
36956
36591
  sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
@@ -37171,7 +36806,7 @@ var init_mqtt = __esm({
37171
36806
 
37172
36807
  // src/service.ts
37173
36808
  import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
37174
- import { join as join30, extname as extname8 } from "node:path";
36809
+ import { join as join30, extname as extname7 } from "node:path";
37175
36810
  import { pathToFileURL } from "node:url";
37176
36811
  function matchCronField(field, value) {
37177
36812
  if (field === "*") return true;
@@ -37342,7 +36977,7 @@ var init_service = __esm({
37342
36977
  return discovered;
37343
36978
  }
37344
36979
  for (const entry of entries) {
37345
- const ext = extname8(entry);
36980
+ const ext = extname7(entry);
37346
36981
  if (ext !== ".ts" && ext !== ".js") continue;
37347
36982
  const fullPath = join30(dir, entry);
37348
36983
  const stat = statSync18(fullPath);
@@ -37458,7 +37093,7 @@ var init_service = __esm({
37458
37093
  return;
37459
37094
  }
37460
37095
  for (const entry of entries) {
37461
- const ext = extname8(entry);
37096
+ const ext = extname7(entry);
37462
37097
  if (ext !== ".ts" && ext !== ".js") continue;
37463
37098
  const fullPath = join30(dir, entry);
37464
37099
  if (watchedFiles.has(fullPath)) continue;
@@ -38161,7 +37796,7 @@ var init_api = __esm({
38161
37796
  // src/messenger.ts
38162
37797
  import net3 from "node:net";
38163
37798
  import tls2 from "node:tls";
38164
- import { readFileSync as readFileSync26 } from "node:fs";
37799
+ import { readFileSync as readFileSync25 } from "node:fs";
38165
37800
  import { basename as basename6 } from "node:path";
38166
37801
  import { randomUUID as randomUUID7 } from "node:crypto";
38167
37802
  function tlsRejectUnauthorized() {
@@ -38258,7 +37893,7 @@ function buildMimeMessage(options) {
38258
37893
  }
38259
37894
  for (const filePath of options.attachments) {
38260
37895
  const fileName = basename6(filePath);
38261
- const fileData = readFileSync26(filePath);
37896
+ const fileData = readFileSync25(filePath);
38262
37897
  const base64Data = fileData.toString("base64");
38263
37898
  lines.push("");
38264
37899
  lines.push(`--${boundary}`);
@@ -39721,9 +39356,9 @@ var init_htmlElement = __esm({
39721
39356
  });
39722
39357
 
39723
39358
  // src/ai.ts
39724
- import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync17, readFileSync as readFileSync27 } from "node:fs";
39359
+ import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync17, readFileSync as readFileSync26 } from "node:fs";
39725
39360
  import { homedir } from "node:os";
39726
- import { join as join31, resolve as resolve19, relative as relative10, dirname as dirname14 } from "node:path";
39361
+ import { join as join31, resolve as resolve19, relative as relative9, dirname as dirname14 } from "node:path";
39727
39362
  import { fileURLToPath as fileURLToPath7 } from "node:url";
39728
39363
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
39729
39364
  import { createInterface } from "node:readline";
@@ -39731,7 +39366,7 @@ function readVersion() {
39731
39366
  try {
39732
39367
  const thisDir = dirname14(fileURLToPath7(import.meta.url));
39733
39368
  const rootPkg = resolve19(thisDir, "..", "..", "..", "package.json");
39734
- const pkg = JSON.parse(readFileSync27(rootPkg, "utf-8"));
39369
+ const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
39735
39370
  return pkg.version ?? "0.0.0";
39736
39371
  } catch {
39737
39372
  return "0.0.0";
@@ -39954,7 +39589,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
39954
39589
  writeFileSync17(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
39955
39590
  return "Installed";
39956
39591
  }
39957
- const existing = readFileSync27(contextPath, "utf-8");
39592
+ const existing = readFileSync26(contextPath, "utf-8");
39958
39593
  if (hasMarkers(existing, start2, end)) {
39959
39594
  writeFileSync17(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
39960
39595
  return "Refreshed skill block in";
@@ -39978,7 +39613,7 @@ function installForTool(root, tool, context) {
39978
39613
  const parentDir = dirname14(contextPath);
39979
39614
  mkdirSync20(parentDir, { recursive: true });
39980
39615
  const action = writeOrMerge(contextPath, tool.contextFile, context);
39981
- const rel = relative10(root, contextPath);
39616
+ const rel = relative9(root, contextPath);
39982
39617
  created.push(rel);
39983
39618
  console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
39984
39619
  if (tool.name === "claude-code") {
@@ -40356,7 +39991,7 @@ function generateClaudeCodeContext() {
40356
39991
  const repoRoot = resolve19(thisDir, "..", "..", "..");
40357
39992
  const claudeMdPath = join31(repoRoot, "CLAUDE.md");
40358
39993
  if (existsSync26(claudeMdPath)) {
40359
- return readFileSync27(claudeMdPath, "utf-8");
39994
+ return readFileSync26(claudeMdPath, "utf-8");
40360
39995
  }
40361
39996
  } catch {
40362
39997
  }
@@ -40550,6 +40185,292 @@ export default class User {
40550
40185
  }
40551
40186
  });
40552
40187
 
40188
+ // src/aiClient.ts
40189
+ import http2 from "node:http";
40190
+ import https2 from "node:https";
40191
+ var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
40192
+ var init_aiClient = __esm({
40193
+ "src/aiClient.ts"() {
40194
+ "use strict";
40195
+ AiError = class extends Error {
40196
+ };
40197
+ AiConfigError = class extends AiError {
40198
+ };
40199
+ AiTimeoutError = class extends AiError {
40200
+ };
40201
+ AiParseError = class extends AiError {
40202
+ };
40203
+ AiHTTPError = class extends AiError {
40204
+ constructor(message, status2 = null) {
40205
+ super(message);
40206
+ this.status = status2;
40207
+ }
40208
+ };
40209
+ Ai = class {
40210
+ static chat(messages, options = {}) {
40211
+ this.validateMessages(messages);
40212
+ const config = this.config("chat", options);
40213
+ const body = this.chatBody(config, messages, options);
40214
+ const headers = this.headers(config);
40215
+ return options.stream ? this.streamRequest(config, headers, body) : this.chatResponse(config, headers, body);
40216
+ }
40217
+ static async complete(prompt, options = {}) {
40218
+ if (typeof prompt !== "string") throw new AiConfigError("AI prompt must be a string");
40219
+ return (await this.chat([{ role: "user", content: prompt }], { ...options, stream: false })).text;
40220
+ }
40221
+ static async embed(textOrTexts, options = {}) {
40222
+ const single = typeof textOrTexts === "string";
40223
+ if (!single && (!Array.isArray(textOrTexts) || textOrTexts.length === 0 || !textOrTexts.every((item) => typeof item === "string"))) {
40224
+ throw new AiConfigError("AI embedding input must be a string or a non-empty list of strings");
40225
+ }
40226
+ const config = this.config("embed", options);
40227
+ if (config.provider === "anthropic") throw new AiConfigError("Anthropic does not provide the embedding endpoint in this contract");
40228
+ const raw = await this.requestJson(config, this.headers(config), { model: config.model, input: textOrTexts });
40229
+ try {
40230
+ const data = raw.data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
40231
+ const vectors = data.map((item) => item.embedding);
40232
+ const expected = single ? 1 : textOrTexts.length;
40233
+ 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();
40234
+ return single ? vectors[0] : vectors;
40235
+ } catch {
40236
+ throw new AiParseError("AI provider returned a malformed embedding response");
40237
+ }
40238
+ }
40239
+ static validateMessages(messages) {
40240
+ if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
40241
+ throw new AiConfigError("AI messages must contain supported roles and string content");
40242
+ }
40243
+ }
40244
+ static number(name, fallback, minimum) {
40245
+ const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
40246
+ if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
40247
+ return value;
40248
+ }
40249
+ static config(capability, options) {
40250
+ const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
40251
+ if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
40252
+ const key = process.env.TINA4_AI_KEY || null;
40253
+ if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
40254
+ const defaults = {
40255
+ local: ["http://localhost:11437", "llama3.2"],
40256
+ openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
40257
+ anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
40258
+ };
40259
+ const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
40260
+ const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
40261
+ if (!model) throw new AiConfigError("AI model must be a non-empty string");
40262
+ const totalTimeout = options.timeout === void 0 ? this.number("TINA4_AI_TIMEOUT", 60, 1e-3) : Number(options.timeout);
40263
+ if (!Number.isFinite(totalTimeout) || totalTimeout <= 0) throw new AiConfigError("AI timeout must be greater than zero");
40264
+ 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)) };
40265
+ }
40266
+ static endpoint(value, capability, provider) {
40267
+ let url;
40268
+ try {
40269
+ url = new URL(value);
40270
+ } catch {
40271
+ throw new AiConfigError("AI URL must be an http or https URL");
40272
+ }
40273
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
40274
+ const path8 = url.pathname.replace(/\/+$/, "");
40275
+ if (path8 === "" || path8 === "/v1" || path8 === "/api") {
40276
+ const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
40277
+ url.pathname = (path8 || "/v1") + suffix;
40278
+ }
40279
+ return url.toString();
40280
+ }
40281
+ static headers(config) {
40282
+ const headers = { "content-type": "application/json", accept: "application/json" };
40283
+ if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
40284
+ if (config.provider === "anthropic") {
40285
+ headers["x-api-key"] = config.key;
40286
+ headers["anthropic-version"] = "2023-06-01";
40287
+ }
40288
+ return headers;
40289
+ }
40290
+ static chatBody(config, messages, options) {
40291
+ const body = { model: config.model, messages, stream: options.stream ?? false };
40292
+ if (options.temperature !== void 0) body.temperature = options.temperature;
40293
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
40294
+ if (config.provider === "anthropic") {
40295
+ const system = messages.filter((message) => message.role === "system").map((message) => message.content);
40296
+ body.messages = messages.filter((message) => message.role !== "system");
40297
+ body.max_tokens = options.maxTokens ?? 1024;
40298
+ if (system.length) body.system = system.join("\n\n");
40299
+ }
40300
+ return body;
40301
+ }
40302
+ static open(config, deadline, headers, body) {
40303
+ const remainingMs = deadline - performance.now();
40304
+ if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
40305
+ const url = new URL(config.url);
40306
+ const payload = JSON.stringify(body);
40307
+ const controller = new AbortController();
40308
+ const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
40309
+ return new Promise((resolve20, reject) => {
40310
+ const client = url.protocol === "https:" ? https2 : http2;
40311
+ const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
40312
+ clearTimeout(connectTimer);
40313
+ resolve20({ response, cleanup: () => {
40314
+ clearTimeout(totalTimer);
40315
+ clearTimeout(connectTimer);
40316
+ } });
40317
+ });
40318
+ const connectTimer = setTimeout(() => request.destroy(new AiTimeoutError("AI connection timeout expired")), Math.min(config.connectTimeout * 1e3, remainingMs));
40319
+ request.on("socket", (socket) => {
40320
+ if (!socket.connecting) clearTimeout(connectTimer);
40321
+ socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
40322
+ });
40323
+ request.once("error", (error) => {
40324
+ clearTimeout(totalTimer);
40325
+ clearTimeout(connectTimer);
40326
+ if (error instanceof AiError) reject(error);
40327
+ else if (controller.signal.aborted) reject(new AiTimeoutError("AI total request timeout expired"));
40328
+ else reject(new AiHTTPError(`AI transport failed (${error.name})`));
40329
+ });
40330
+ request.end(payload);
40331
+ });
40332
+ }
40333
+ static async readBody(response) {
40334
+ const chunks = [];
40335
+ for await (const chunk of response) chunks.push(Buffer.from(chunk));
40336
+ return Buffer.concat(chunks).toString("utf8");
40337
+ }
40338
+ static retryDelay(headers, deadline) {
40339
+ const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
40340
+ const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
40341
+ const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
40342
+ return new Promise((resolve20) => setTimeout(resolve20, delay));
40343
+ }
40344
+ static async requestJson(config, headers, body) {
40345
+ const deadline = performance.now() + config.totalTimeout * 1e3;
40346
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
40347
+ let opened = null;
40348
+ try {
40349
+ opened = await this.open(config, deadline, headers, body);
40350
+ const status2 = opened.response.statusCode ?? 0;
40351
+ const responseHeaders = opened.response.headers;
40352
+ const raw = await this.readBody(opened.response);
40353
+ opened.cleanup();
40354
+ opened = null;
40355
+ if (status2 < 200 || status2 >= 300) {
40356
+ if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
40357
+ await this.retryDelay(responseHeaders, deadline);
40358
+ continue;
40359
+ }
40360
+ throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
40361
+ }
40362
+ let parsed;
40363
+ try {
40364
+ parsed = JSON.parse(raw);
40365
+ } catch {
40366
+ throw new AiParseError("AI provider returned malformed JSON");
40367
+ }
40368
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiParseError("AI provider returned a non-object JSON response");
40369
+ return parsed;
40370
+ } catch (error) {
40371
+ opened?.cleanup();
40372
+ if (error instanceof AiParseError || error instanceof AiHTTPError && error.status !== null) throw error;
40373
+ if (attempt >= config.maxRetries) throw error;
40374
+ }
40375
+ }
40376
+ throw new AiHTTPError("AI request failed");
40377
+ }
40378
+ static normalizeChat(provider, raw) {
40379
+ try {
40380
+ if (provider === "anthropic") {
40381
+ const content = raw.content;
40382
+ const parts = content.filter((item) => (item.type ?? "text") === "text").map((item) => item.text);
40383
+ if (!parts.length || !parts.every((part) => typeof part === "string")) throw new Error();
40384
+ const usage2 = raw.usage ?? {};
40385
+ const promptTokens = Number(usage2.input_tokens ?? 0);
40386
+ const completionTokens = Number(usage2.output_tokens ?? 0);
40387
+ 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 };
40388
+ }
40389
+ const choice = raw.choices[0];
40390
+ const text = choice.message.content;
40391
+ if (typeof text !== "string") throw new Error();
40392
+ const usage = raw.usage ?? {};
40393
+ 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 };
40394
+ } catch {
40395
+ throw new AiParseError("AI provider returned a malformed chat response");
40396
+ }
40397
+ }
40398
+ static async chatResponse(config, headers, body) {
40399
+ return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
40400
+ }
40401
+ static streamDelta(provider, data) {
40402
+ if (data === "[DONE]") return { completed: true };
40403
+ let event;
40404
+ try {
40405
+ event = JSON.parse(data);
40406
+ } catch {
40407
+ throw new AiParseError("AI provider returned malformed stream data");
40408
+ }
40409
+ const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
40410
+ if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
40411
+ return { completed: false, text };
40412
+ }
40413
+ static async *streamData(response) {
40414
+ let buffer = "";
40415
+ for await (const chunk of response) {
40416
+ buffer += Buffer.from(chunk).toString("utf8");
40417
+ let newline;
40418
+ while ((newline = buffer.indexOf("\n")) >= 0) {
40419
+ const line = buffer.slice(0, newline).trim();
40420
+ buffer = buffer.slice(newline + 1);
40421
+ if (line.startsWith("data:")) yield line.slice(5).trim();
40422
+ }
40423
+ }
40424
+ }
40425
+ static streamError(error) {
40426
+ if (error instanceof AiError) return error;
40427
+ if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
40428
+ return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
40429
+ }
40430
+ static async *streamRequest(config, headers, body) {
40431
+ const deadline = performance.now() + config.totalTimeout * 1e3;
40432
+ let yielded = false;
40433
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
40434
+ let opened = null;
40435
+ try {
40436
+ opened = await this.open(config, deadline, { ...headers, accept: "text/event-stream" }, body);
40437
+ const status2 = opened.response.statusCode ?? 0;
40438
+ if (status2 < 200 || status2 >= 300) {
40439
+ await this.readBody(opened.response);
40440
+ if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
40441
+ await this.retryDelay(opened.response.headers, deadline);
40442
+ opened.cleanup();
40443
+ opened = null;
40444
+ continue;
40445
+ }
40446
+ throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
40447
+ }
40448
+ let completed = false;
40449
+ for await (const data of this.streamData(opened.response)) {
40450
+ const delta = this.streamDelta(config.provider, data);
40451
+ if (delta.completed) {
40452
+ completed = true;
40453
+ break;
40454
+ }
40455
+ if (delta.text === void 0) continue;
40456
+ yielded = true;
40457
+ yield delta.text;
40458
+ }
40459
+ opened.cleanup();
40460
+ opened = null;
40461
+ if (completed) return;
40462
+ throw new AiParseError("AI provider stream ended before [DONE]");
40463
+ } catch (error) {
40464
+ opened?.cleanup();
40465
+ const failure = this.streamError(error);
40466
+ if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
40467
+ }
40468
+ }
40469
+ }
40470
+ };
40471
+ }
40472
+ });
40473
+
40553
40474
  // src/queueBackends/rabbitmqBackend.ts
40554
40475
  import { execFileSync as execFileSync5 } from "node:child_process";
40555
40476
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -42253,6 +42174,12 @@ __export(index_exports, {
42253
42174
  APPLICATION_JSON: () => APPLICATION_JSON,
42254
42175
  APPLICATION_OCTET: () => APPLICATION_OCTET,
42255
42176
  APPLICATION_XML: () => APPLICATION_XML,
42177
+ Ai: () => Ai,
42178
+ AiConfigError: () => AiConfigError,
42179
+ AiError: () => AiError,
42180
+ AiHTTPError: () => AiHTTPError,
42181
+ AiParseError: () => AiParseError,
42182
+ AiTimeoutError: () => AiTimeoutError,
42256
42183
  Api: () => Api,
42257
42184
  Auth: () => Auth,
42258
42185
  CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
@@ -42579,6 +42506,7 @@ var init_index = __esm({
42579
42506
  init_htmlElement();
42580
42507
  init_errorOverlay();
42581
42508
  init_ai();
42509
+ init_aiClient();
42582
42510
  init_liteBackend();
42583
42511
  init_rabbitmqBackend();
42584
42512
  init_kafkaBackend();
@@ -42608,6 +42536,12 @@ export {
42608
42536
  APPLICATION_JSON,
42609
42537
  APPLICATION_OCTET,
42610
42538
  APPLICATION_XML,
42539
+ Ai,
42540
+ AiConfigError,
42541
+ AiError,
42542
+ AiHTTPError,
42543
+ AiParseError,
42544
+ AiTimeoutError,
42611
42545
  Api,
42612
42546
  Auth,
42613
42547
  CANONICAL_SESSION_BACKENDS,