@jefuriiij/synthra 0.9.0 → 0.11.0

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.
@@ -1785,6 +1785,7 @@ function resolvePaths(projectRoot) {
1785
1785
  activityLog: join5(graphDir, "activity.jsonl"),
1786
1786
  tokenLog: join5(graphDir, "token_log.jsonl"),
1787
1787
  gateLog: join5(graphDir, "gate_log.jsonl"),
1788
+ bashLog: join5(graphDir, "bash_log.jsonl"),
1788
1789
  toolLog: join5(graphDir, "tool_log.jsonl"),
1789
1790
  accessLog: join5(graphDir, "access_log.jsonl"),
1790
1791
  learnStore: join5(graphDir, "learn_store.json"),
@@ -2341,6 +2342,10 @@ function loadConfig() {
2341
2342
  // mid-session. Set SYN_NO_AUTOREINDEX to disable entirely.
2342
2343
  reindexDebounceMs: num("SYN_REINDEX_DEBOUNCE_MS", 1e3),
2343
2344
  autoReindex: !process.env.SYN_NO_AUTOREINDEX,
2345
+ // Observe-only: log codebase-exploration Bash commands (grep/cat/find …) so
2346
+ // the terminal bypass of the Moat can be measured. Never blocks. Opt out
2347
+ // with SYN_NO_BASH_OBSERVE.
2348
+ bashObserve: !process.env.SYN_NO_BASH_OBSERVE,
2344
2349
  mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
2345
2350
  dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
2346
2351
  logLevel: str("SYN_LOG_LEVEL", "info"),
@@ -3101,11 +3106,14 @@ var TOOLS = [
3101
3106
  },
3102
3107
  {
3103
3108
  name: "blast_radius",
3104
- description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports, tests, and call edges (callers). Use BEFORE editing a widely-used file to see what could break. Call edges are name-resolved (precise within a file, unique-name across files) and projected to file granularity.",
3109
+ description: "See what could break before an edit. A bare file target returns all files that depend on it transitively via imports, tests, and call edges. A 'file::symbol' target returns the exact caller SYMBOLS that transitively call it (name \u2192 file:line) plus the test files guarding the impact \u2014 the precise rename-safety view. Call edges are name-resolved (precise within a file, unique-name across files).",
3105
3110
  inputSchema: {
3106
3111
  type: "object",
3107
3112
  properties: {
3108
- target: { type: "string", description: "File path or 'file::symbol' notation." },
3113
+ target: {
3114
+ type: "string",
3115
+ description: "File path (file-level dependents) or 'file::symbol' (caller symbols)."
3116
+ },
3109
3117
  depth: { type: "number", description: "Max hops to traverse. Default 3." }
3110
3118
  },
3111
3119
  required: ["target"]
@@ -3156,7 +3164,8 @@ function blastRadius(args, ctx) {
3156
3164
  const targetRaw = typeof args?.target === "string" ? args.target.trim() : "";
3157
3165
  const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 3;
3158
3166
  if (!targetRaw) return errorContent("blast_radius: 'target' (string) is required");
3159
- const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
3167
+ if (targetRaw.includes("::")) return blastRadiusSymbol(targetRaw, maxDepth, ctx);
3168
+ const filePath = targetRaw;
3160
3169
  const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
3161
3170
  if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
3162
3171
  const fileIdBySymbol = /* @__PURE__ */ new Map();
@@ -3211,6 +3220,92 @@ _(no dependents \u2014 file is isolated)_`);
3211
3220
  }
3212
3221
  return textContent(lines.join("\n"));
3213
3222
  }
3223
+ function blastRadiusSymbol(targetRaw, maxDepth, ctx) {
3224
+ const [rawFile, rawSym] = targetRaw.split("::", 2);
3225
+ const filePath = (rawFile ?? "").trim();
3226
+ const symName = (rawSym ?? "").trim();
3227
+ if (!symName) return errorContent("blast_radius: 'file::symbol' target needs a symbol name");
3228
+ const resolved = resolveFileTarget(ctx.graph, filePath);
3229
+ if ("ambiguous" in resolved) {
3230
+ const shown = resolved.ambiguous.slice(0, 5).join(", ");
3231
+ return errorContent(
3232
+ `blast_radius: '${filePath}' matches multiple files (${shown}). Pass a longer path.`
3233
+ );
3234
+ }
3235
+ if ("none" in resolved) return errorContent(`blast_radius: file not in graph: ${filePath}`);
3236
+ const fileNode = resolved.node;
3237
+ const symbol = ctx.graph.nodes.find(
3238
+ (n) => n.kind === "symbol" && n.file === fileNode.path && n.name === symName
3239
+ );
3240
+ if (!symbol)
3241
+ return errorContent(`blast_radius: symbol '${symName}' not found in ${fileNode.path}`);
3242
+ const callersBySym = /* @__PURE__ */ new Map();
3243
+ for (const e of ctx.graph.edges) {
3244
+ if (e.kind !== "calls" || e.from === e.to) continue;
3245
+ const list = callersBySym.get(e.to) ?? [];
3246
+ list.push(e.from);
3247
+ callersBySym.set(e.to, list);
3248
+ }
3249
+ const symById = /* @__PURE__ */ new Map();
3250
+ for (const n of ctx.graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
3251
+ const visited = /* @__PURE__ */ new Set([symbol.id]);
3252
+ const hits = [];
3253
+ let frontier = [symbol.id];
3254
+ for (let d = 1; d <= maxDepth; d++) {
3255
+ const next = [];
3256
+ for (const cur of frontier) {
3257
+ for (const fromId of callersBySym.get(cur) ?? []) {
3258
+ if (visited.has(fromId)) continue;
3259
+ visited.add(fromId);
3260
+ next.push(fromId);
3261
+ const s = symById.get(fromId);
3262
+ if (s) hits.push({ name: s.name, file: s.file, line: s.start_line, depth: d });
3263
+ }
3264
+ }
3265
+ frontier = next;
3266
+ if (next.length === 0) break;
3267
+ }
3268
+ const header = `# Blast radius for ${fileNode.path}::${symbol.name} (callers, depth \u2264 ${maxDepth})`;
3269
+ if (hits.length === 0) {
3270
+ const tline2 = testsCoveringLine(ctx.graph, [fileNode.path]);
3271
+ return textContent(
3272
+ `${header}
3273
+
3274
+ _(no callers \u2014 safe to rename)_${tline2 ? `
3275
+
3276
+ ${tline2}` : ""}`
3277
+ );
3278
+ }
3279
+ hits.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file) || a.line - b.line);
3280
+ const lines = [header, "", `${hits.length} caller symbol(s):`];
3281
+ for (const h of hits) lines.push(`- **depth ${h.depth}** \`${h.name}\` \u2192 ${h.file}:${h.line}`);
3282
+ const tline = testsCoveringLine(ctx.graph, [fileNode.path, ...hits.map((h) => h.file)]);
3283
+ if (tline) {
3284
+ lines.push("");
3285
+ lines.push(tline);
3286
+ }
3287
+ return textContent(lines.join("\n"));
3288
+ }
3289
+ function testsCoveringLine(graph, filePaths) {
3290
+ const fileByPath = /* @__PURE__ */ new Map();
3291
+ for (const n of graph.nodes) if (n.kind === "file") fileByPath.set(n.path, n);
3292
+ const seen = /* @__PURE__ */ new Set();
3293
+ const tests = [];
3294
+ for (const p of new Set(filePaths)) {
3295
+ const fn = fileByPath.get(p);
3296
+ if (!fn) continue;
3297
+ for (const t of findTestsForFile(graph, fn)) {
3298
+ if (!seen.has(t.path)) {
3299
+ seen.add(t.path);
3300
+ tests.push(t.path);
3301
+ }
3302
+ }
3303
+ }
3304
+ if (tests.length === 0) return "";
3305
+ const shown = tests.slice(0, TESTS_MAX_FILES);
3306
+ const omitted = tests.length - shown.length;
3307
+ return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
3308
+ }
3214
3309
  var LIKELY_ENTRY_PATTERNS = [
3215
3310
  /(?:^|\/)main\.[a-z0-9_]+$/i,
3216
3311
  /(?:^|\/)index\.[a-z0-9_]+$/i,
@@ -3288,6 +3383,7 @@ function resolveFileTarget(graph, filePath) {
3288
3383
  var DEPS_SIG_MAX = 140;
3289
3384
  var DEPS_MAX_CALLEES = 10;
3290
3385
  var DEPS_MAX_CALLERS = 12;
3386
+ var TESTS_MAX_FILES = 6;
3291
3387
  function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars) {
3292
3388
  const symById = /* @__PURE__ */ new Map();
3293
3389
  for (const n of graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
@@ -3351,6 +3447,21 @@ function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars
3351
3447
  }
3352
3448
  return lines.join("\n");
3353
3449
  }
3450
+ function buildTestsFooter(symbol, graph) {
3451
+ const fileNode = graph.nodes.find(
3452
+ (n) => n.kind === "file" && n.path === symbol.file
3453
+ );
3454
+ if (!fileNode) return "";
3455
+ const tests = findTestsForFile(graph, fileNode);
3456
+ if (tests.length > 0) {
3457
+ const shown = tests.slice(0, TESTS_MAX_FILES).map((t) => t.path);
3458
+ const omitted = tests.length - shown.length;
3459
+ const more = omitted > 0 ? ` \u2026+${omitted} more` : "";
3460
+ return `Tests (file-level): ${shown.join(" \xB7 ")}${more} \u2014 run after editing`;
3461
+ }
3462
+ if (isLikelyEntry(symbol.file)) return "";
3463
+ return "Tests: none linked to this file.";
3464
+ }
3354
3465
  async function graphRead(args, ctx) {
3355
3466
  const target = typeof args?.target === "string" ? args.target : "";
3356
3467
  if (!target) return errorContent("graph_read: 'target' (string) is required");
@@ -3394,10 +3505,15 @@ ${fileNode.content}`);
3394
3505
 
3395
3506
  ---
3396
3507
  ${deps}` : "";
3508
+ const tests = buildTestsFooter(symbol, ctx.graph);
3509
+ const testsBlock = tests ? `
3510
+
3511
+ ---
3512
+ ${tests}` : "";
3397
3513
  return textContent(
3398
3514
  `# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
3399
3515
 
3400
- ${body}${depsBlock}${editHint}`
3516
+ ${body}${depsBlock}${testsBlock}${editHint}`
3401
3517
  );
3402
3518
  }
3403
3519
  var editedFiles = /* @__PURE__ */ new Set();
@@ -3716,22 +3832,14 @@ async function handleContextUpdate(req, ctx) {
3716
3832
  }
3717
3833
 
3718
3834
  // src/server/routes/gate.ts
3835
+ import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
3836
+ import { dirname as dirname12 } from "path";
3837
+
3838
+ // src/server/routes/bash-observe.ts
3719
3839
  import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
3720
3840
  import { dirname as dirname11 } from "path";
3721
- var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
3722
- var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
3723
- function extractQuery(toolName, input) {
3724
- if (toolName === "Grep") {
3725
- const pattern = typeof input.pattern === "string" ? input.pattern : "";
3726
- const query = typeof input.query === "string" ? input.query : "";
3727
- return (pattern || query).trim() || null;
3728
- }
3729
- if (toolName === "Glob") {
3730
- const pattern = typeof input.pattern === "string" ? input.pattern : "";
3731
- return pattern.replace(/[*?/\\.]+/g, " ").trim() || null;
3732
- }
3733
- return null;
3734
- }
3841
+
3842
+ // src/server/routes/query-heuristics.ts
3735
3843
  function looksLikeNonSymbolQuery(pattern) {
3736
3844
  if (/<\/?[a-zA-Z]/.test(pattern)) return true;
3737
3845
  if (/[a-zA-Z][\w-]*-[\w-]*\s*=/.test(pattern)) return true;
@@ -3747,6 +3855,195 @@ function looksLikeNonSymbolQuery(pattern) {
3747
3855
  if (branches.length > 0 && branches.every(isKebab)) return true;
3748
3856
  return false;
3749
3857
  }
3858
+
3859
+ // src/server/routes/bash-observe.ts
3860
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "ripgrep", "ag", "ack"]);
3861
+ var READ_TOOLS = /* @__PURE__ */ new Set(["cat", "head", "tail", "less", "more", "bat", "tac"]);
3862
+ var LIST_TOOLS = /* @__PURE__ */ new Set(["find", "tree"]);
3863
+ var SOURCE_EXT = /\.(ts|tsx|js|jsx|cts|mts|cjs|mjs|py|pyi|svelte|vue|go|rs|java|kt|kts|php|rb|c|h|cpp|cc|cxx|hpp|cs|dart|json|md|css|html|hubl|sh|yml|yaml|toml)$/i;
3864
+ function tokenizeCommand(cmd) {
3865
+ const tokens = [];
3866
+ let cur = "";
3867
+ let quote = null;
3868
+ let hasContent2 = false;
3869
+ const flush = () => {
3870
+ if (hasContent2) tokens.push(cur);
3871
+ cur = "";
3872
+ hasContent2 = false;
3873
+ };
3874
+ for (let i = 0; i < cmd.length; i++) {
3875
+ const ch = cmd[i];
3876
+ if (quote) {
3877
+ if (ch === quote) quote = null;
3878
+ else cur += ch;
3879
+ hasContent2 = true;
3880
+ continue;
3881
+ }
3882
+ if (ch === '"' || ch === "'") {
3883
+ quote = ch;
3884
+ hasContent2 = true;
3885
+ continue;
3886
+ }
3887
+ if (ch === "|" || ch === "&" || ch === ";") {
3888
+ flush();
3889
+ let op = ch;
3890
+ if ((ch === "|" || ch === "&") && cmd[i + 1] === ch) {
3891
+ op += ch;
3892
+ i++;
3893
+ }
3894
+ tokens.push(op);
3895
+ continue;
3896
+ }
3897
+ if (/\s/.test(ch)) {
3898
+ flush();
3899
+ continue;
3900
+ }
3901
+ cur += ch;
3902
+ hasContent2 = true;
3903
+ }
3904
+ flush();
3905
+ return tokens;
3906
+ }
3907
+ function isOperator(t) {
3908
+ return t === "|" || t === "||" || t === "&&" || t === ";";
3909
+ }
3910
+ function splitSegments(tokens) {
3911
+ const segs = [];
3912
+ let cur = [];
3913
+ for (const t of tokens) {
3914
+ if (isOperator(t)) {
3915
+ if (cur.length) segs.push(cur);
3916
+ cur = [];
3917
+ } else {
3918
+ cur.push(t);
3919
+ }
3920
+ }
3921
+ if (cur.length) segs.push(cur);
3922
+ return segs;
3923
+ }
3924
+ function commandTokens(seg) {
3925
+ let i = 0;
3926
+ while (i < seg.length) {
3927
+ const t = seg[i];
3928
+ if (t === "env") {
3929
+ i++;
3930
+ continue;
3931
+ }
3932
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) {
3933
+ i++;
3934
+ continue;
3935
+ }
3936
+ break;
3937
+ }
3938
+ return seg.slice(i);
3939
+ }
3940
+ function baseCmd(tok) {
3941
+ return (tok.split(/[\\/]/).pop() ?? tok).toLowerCase();
3942
+ }
3943
+ function classifySegment(seg) {
3944
+ const toks = commandTokens(seg);
3945
+ if (toks.length === 0) return null;
3946
+ const cmd = baseCmd(toks[0]);
3947
+ const rest = toks.slice(1);
3948
+ const flags = rest.filter((a) => a.startsWith("-"));
3949
+ const nonFlags = rest.filter((a) => !a.startsWith("-"));
3950
+ if (SEARCH_TOOLS.has(cmd)) {
3951
+ const pattern = nonFlags[0];
3952
+ if (!pattern) return null;
3953
+ const recursive = flags.some(
3954
+ (f) => f === "-r" || f === "-R" || f === "--recursive" || /^-[a-zA-Z]*[rR]$/.test(f)
3955
+ );
3956
+ const rgLike = cmd === "rg" || cmd === "ripgrep" || cmd === "ag" || cmd === "ack";
3957
+ const hasPathArg = nonFlags.length > 1;
3958
+ if (!rgLike && !recursive && !hasPathArg) return null;
3959
+ return { kind: "search", tool: cmd, query: pattern };
3960
+ }
3961
+ if (READ_TOOLS.has(cmd)) {
3962
+ const file = nonFlags.find((a) => SOURCE_EXT.test(a));
3963
+ if (!file) return null;
3964
+ return { kind: "read", tool: cmd, query: file };
3965
+ }
3966
+ if (LIST_TOOLS.has(cmd)) {
3967
+ const nameIdx = rest.findIndex((a) => a === "-name" || a === "-iname" || a === "-path");
3968
+ const target = nameIdx >= 0 ? rest[nameIdx + 1] ?? null : nonFlags[0] ?? ".";
3969
+ return { kind: "list", tool: cmd, query: target };
3970
+ }
3971
+ return null;
3972
+ }
3973
+ function classifyBashCommand(command) {
3974
+ if (!command || typeof command !== "string") return null;
3975
+ const tokens = tokenizeCommand(command);
3976
+ if (tokens.length === 0) return null;
3977
+ if (tokens.some((t) => !isOperator(t) && t.includes(">"))) return null;
3978
+ const found = splitSegments(tokens).map(classifySegment).filter((x) => x !== null);
3979
+ if (found.length === 0) return null;
3980
+ const prio = { search: 0, read: 1, list: 2 };
3981
+ found.sort((a, b) => prio[a.kind] - prio[b.kind]);
3982
+ return found[0];
3983
+ }
3984
+ function graphHasFile(ctx, target) {
3985
+ const base = target.split(/[\\/]/).pop() ?? target;
3986
+ for (const n of ctx.graph.nodes) {
3987
+ if (n.kind !== "file") continue;
3988
+ if (n.path === target || n.path.endsWith(`/${target}`) || n.path.split("/").pop() === base) {
3989
+ return true;
3990
+ }
3991
+ }
3992
+ return false;
3993
+ }
3994
+ var Q_MAX = 200;
3995
+ var CMD_MAX = 300;
3996
+ var trunc = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
3997
+ async function logObservation(ctx, exp, confidence, avoidable, command) {
3998
+ try {
3999
+ await mkdir10(dirname11(ctx.paths.bashLog), { recursive: true });
4000
+ const entry = {
4001
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4002
+ kind: exp.kind,
4003
+ tool: exp.tool,
4004
+ query: exp.query ? trunc(exp.query, Q_MAX) : null,
4005
+ confidence,
4006
+ avoidable,
4007
+ command: trunc(command, CMD_MAX)
4008
+ };
4009
+ await appendFile4(ctx.paths.bashLog, JSON.stringify(entry) + "\n", "utf8");
4010
+ } catch {
4011
+ }
4012
+ }
4013
+ async function observeBash(input, ctx) {
4014
+ if (!loadConfig().bashObserve) return;
4015
+ const command = typeof input.command === "string" ? input.command : "";
4016
+ const exp = classifyBashCommand(command);
4017
+ if (!exp) return;
4018
+ let confidence = null;
4019
+ let avoidable = false;
4020
+ if (exp.kind === "search" && exp.query) {
4021
+ if (!looksLikeNonSymbolQuery(exp.query)) {
4022
+ const r = await retrieve(ctx.graph, exp.query);
4023
+ confidence = r.confidence;
4024
+ avoidable = r.confidence !== "low" && r.symbolMatched;
4025
+ }
4026
+ } else if (exp.kind === "read" && exp.query) {
4027
+ avoidable = graphHasFile(ctx, exp.query);
4028
+ }
4029
+ await logObservation(ctx, exp, confidence, avoidable, command);
4030
+ }
4031
+
4032
+ // src/server/routes/gate.ts
4033
+ var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
4034
+ var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
4035
+ function extractQuery(toolName, input) {
4036
+ if (toolName === "Grep") {
4037
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
4038
+ const query = typeof input.query === "string" ? input.query : "";
4039
+ return (pattern || query).trim() || null;
4040
+ }
4041
+ if (toolName === "Glob") {
4042
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
4043
+ return pattern.replace(/[*?/\\.]+/g, " ").trim() || null;
4044
+ }
4045
+ return null;
4046
+ }
3750
4047
  function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
3751
4048
  if (recentPaths.length === 0) return [];
3752
4049
  const recent = new Set(recentPaths);
@@ -3779,7 +4076,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
3779
4076
  var LOG_REASON_MAX_CHARS = 240;
3780
4077
  async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
3781
4078
  try {
3782
- await mkdir10(dirname11(ctx.paths.gateLog), { recursive: true });
4079
+ await mkdir11(dirname12(ctx.paths.gateLog), { recursive: true });
3783
4080
  const entry = {
3784
4081
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3785
4082
  tool: toolName,
@@ -3788,7 +4085,7 @@ async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
3788
4085
  reason: reason && reason.length > LOG_REASON_MAX_CHARS ? `${reason.slice(0, LOG_REASON_MAX_CHARS)}\u2026` : reason,
3789
4086
  ...hintChars === void 0 ? {} : { hint_chars: hintChars }
3790
4087
  };
3791
- await appendFile4(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
4088
+ await appendFile5(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
3792
4089
  } catch {
3793
4090
  }
3794
4091
  }
@@ -3853,6 +4150,11 @@ async function handleGate(req, ctx) {
3853
4150
  if (!req?.tool_name || typeof req.tool_name !== "string") {
3854
4151
  return { decision: "allow", reason: "no tool_name" };
3855
4152
  }
4153
+ if (req.tool_name === "Bash") {
4154
+ const input2 = req.tool_input && typeof req.tool_input === "object" ? req.tool_input : {};
4155
+ await observeBash(input2, ctx);
4156
+ return { decision: "allow" };
4157
+ }
3856
4158
  if (!BLOCKABLE_TOOLS.has(req.tool_name)) {
3857
4159
  return { decision: "allow" };
3858
4160
  }
@@ -3906,16 +4208,16 @@ async function handleGate(req, ctx) {
3906
4208
  }
3907
4209
 
3908
4210
  // src/server/routes/log.ts
3909
- import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
3910
- import { dirname as dirname12 } from "path";
4211
+ import { appendFile as appendFile6, mkdir as mkdir12 } from "fs/promises";
4212
+ import { dirname as dirname13 } from "path";
3911
4213
  async function handleLog(entry, ctx) {
3912
4214
  if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
3913
4215
  throw new Error("log: input_tokens and output_tokens (number) are required");
3914
4216
  }
3915
4217
  const written_at = (/* @__PURE__ */ new Date()).toISOString();
3916
4218
  const record = { ...entry, written_at };
3917
- await mkdir11(dirname12(ctx.paths.tokenLog), { recursive: true });
3918
- await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
4219
+ await mkdir12(dirname13(ctx.paths.tokenLog), { recursive: true });
4220
+ await appendFile6(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
3919
4221
  return { ok: true, written_at };
3920
4222
  }
3921
4223