@budgetary/mcp
Advanced tools
Sorry, the diff of this file is too big to display
| import { | ||
| MAX_TRANSCRIPT_BYTES, | ||
| PendingStore, | ||
| capTrace, | ||
| entryBinding, | ||
| findProvenTranscript, | ||
| isTranscriptDir, | ||
| pendingFilePath, | ||
| persistedCounts, | ||
| readTranscriptUsage, | ||
| submitActuals | ||
| } from "./chunk-XRQX5KPH.js"; | ||
| // src/reconcile.ts | ||
| import { readdirSync, readFileSync, statSync } from "fs"; | ||
| import { BudgetaryClient } from "@budgetary/sdk"; | ||
| var RECONCILE_MAX_RETRIES = 0; | ||
| function fileContains(path, needle) { | ||
| try { | ||
| const st = statSync(path); | ||
| if (!st.isFile() || st.size > MAX_TRANSCRIPT_BYTES) return false; | ||
| return readFileSync(path, "utf8").includes(needle); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function listDir(dir) { | ||
| try { | ||
| return readdirSync(dir); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| function mtimeMs(path) { | ||
| try { | ||
| return statSync(path).mtimeMs; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function fingerprint(path) { | ||
| try { | ||
| const st = statSync(path); | ||
| return `${st.size}:${st.mtimeMs}`; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| async function reconcileEntry(args) { | ||
| const logger = args.logger ?? { warn: () => { | ||
| } }; | ||
| const now = (args.now ?? (() => /* @__PURE__ */ new Date()))(); | ||
| const binding = entryBinding(args.entry); | ||
| if (binding === null) return "no-binding"; | ||
| const store = new PendingStore({ | ||
| path: pendingFilePath(args.home), | ||
| logger | ||
| }); | ||
| const factory = args.clientFactory ?? ((opts) => new BudgetaryClient(opts)); | ||
| const client = factory({ | ||
| apiKey: args.apiKey, | ||
| baseUrl: args.baseUrl, | ||
| maxRetries: RECONCILE_MAX_RETRIES | ||
| }); | ||
| const persisted = persistedCounts(args.entry); | ||
| if (persisted !== null) { | ||
| return finish(await trySubmit(persisted)); | ||
| } | ||
| if (!isTranscriptDir(binding.transcriptDir, args.home)) return "no-transcript"; | ||
| const path = findProvenTranscript( | ||
| binding, | ||
| { listDir, contains: fileContains, mtimeMs }, | ||
| Date.parse(args.entry.created_at) | ||
| ); | ||
| if (path === null) return "no-transcript"; | ||
| const before = fingerprint(path); | ||
| if (before === null) return "no-transcript"; | ||
| const usage = (args.readUsage ?? readTranscriptUsage)(path, { target: false }); | ||
| if (fingerprint(path) !== before) return "transcript-changed"; | ||
| if (usage === null) return "no-usage"; | ||
| const trace = capTrace(usage.trace) ?? void 0; | ||
| const counts = { | ||
| tokensIn: usage.tokensIn, | ||
| tokensOut: usage.tokensOut, | ||
| // We submit ONLY for a session whose serving process is gone and whose | ||
| // transcript is complete and stable — i.e. one that ran to termination, | ||
| // which is the same condition the hook path encodes as success (it counts | ||
| // `clear` / `logout` / `prompt_input_exit`, the normal endings). The hook's | ||
| // conservative default of `false` is not available to us: it keys off a | ||
| // termination reason only the host can report, and defaulting to it would | ||
| // stamp `false` on EVERY reconciled run — a systematic label, not a | ||
| // measurement. See the PR for the divergence this leaves. | ||
| success: true, | ||
| // Measured, not inferred from this process's clock. The hook can bound the | ||
| // run with a real session-end moment; we cannot, so the transcript's own | ||
| // last-write time is used as the end bound. Using `now` here would report | ||
| // the age of the ENTRY (up to a full day) as the run's duration. | ||
| durationMs: durationFromTranscript(path, args.entry, now), | ||
| ...trace ? { trace } : {} | ||
| }; | ||
| return finish(await trySubmit(counts)); | ||
| async function trySubmit(measured) { | ||
| try { | ||
| const outcome = await submitActuals({ | ||
| store, | ||
| client, | ||
| entry: args.entry, | ||
| counts: measured, | ||
| logger | ||
| }); | ||
| return outcome.submitted; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function finish(submitted) { | ||
| return submitted ? "submitted" : "not-submitted"; | ||
| } | ||
| } | ||
| function durationFromTranscript(path, entry, now) { | ||
| const created = Date.parse(entry.created_at); | ||
| if (!Number.isFinite(created)) return 0; | ||
| let endMs; | ||
| try { | ||
| endMs = statSync(path).mtimeMs; | ||
| } catch { | ||
| return 0; | ||
| } | ||
| if (!Number.isFinite(endMs) || endMs < created || endMs > now.getTime() + 1e3) { | ||
| return 0; | ||
| } | ||
| return Math.max(0, Math.round(endMs - created)); | ||
| } | ||
| export { | ||
| reconcileEntry | ||
| }; |
+1
-1
@@ -11,3 +11,3 @@ import { | ||
| submitActuals | ||
| } from "./chunk-FR4HH2QS.js"; | ||
| } from "./chunk-XRQX5KPH.js"; | ||
| export { | ||
@@ -14,0 +14,0 @@ MAX_ATTEMPTS, |
+1
-1
| { | ||
| "name": "@budgetary/mcp", | ||
| "version": "0.9.0", | ||
| "version": "0.9.1", | ||
| "description": "Model Context Protocol server for Budgetary: a portable pre-flight token-spend estimate tool for any MCP-capable host.", | ||
@@ -5,0 +5,0 @@ "mcpName": "io.github.thriftell/budgetary", |
Sorry, the diff of this file is too big to display
| import { | ||
| MAX_TRANSCRIPT_BYTES, | ||
| PendingStore, | ||
| capTrace, | ||
| entryBinding, | ||
| findProvenTranscript, | ||
| isTranscriptDir, | ||
| pendingFilePath, | ||
| persistedCounts, | ||
| readTranscriptUsage, | ||
| submitActuals | ||
| } from "./chunk-FR4HH2QS.js"; | ||
| // src/reconcile.ts | ||
| import { readdirSync, readFileSync, statSync } from "fs"; | ||
| import { BudgetaryClient } from "@budgetary/sdk"; | ||
| var RECONCILE_MAX_RETRIES = 0; | ||
| function fileContains(path, needle) { | ||
| try { | ||
| const st = statSync(path); | ||
| if (!st.isFile() || st.size > MAX_TRANSCRIPT_BYTES) return false; | ||
| return readFileSync(path, "utf8").includes(needle); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function listDir(dir) { | ||
| try { | ||
| return readdirSync(dir); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| function mtimeMs(path) { | ||
| try { | ||
| return statSync(path).mtimeMs; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function fingerprint(path) { | ||
| try { | ||
| const st = statSync(path); | ||
| return `${st.size}:${st.mtimeMs}`; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| async function reconcileEntry(args) { | ||
| const logger = args.logger ?? { warn: () => { | ||
| } }; | ||
| const now = (args.now ?? (() => /* @__PURE__ */ new Date()))(); | ||
| const binding = entryBinding(args.entry); | ||
| if (binding === null) return "no-binding"; | ||
| const store = new PendingStore({ | ||
| path: pendingFilePath(args.home), | ||
| logger | ||
| }); | ||
| const factory = args.clientFactory ?? ((opts) => new BudgetaryClient(opts)); | ||
| const client = factory({ | ||
| apiKey: args.apiKey, | ||
| baseUrl: args.baseUrl, | ||
| maxRetries: RECONCILE_MAX_RETRIES | ||
| }); | ||
| const persisted = persistedCounts(args.entry); | ||
| if (persisted !== null) { | ||
| return finish(await trySubmit(persisted)); | ||
| } | ||
| if (!isTranscriptDir(binding.transcriptDir, args.home)) return "no-transcript"; | ||
| const path = findProvenTranscript( | ||
| binding, | ||
| { listDir, contains: fileContains, mtimeMs }, | ||
| Date.parse(args.entry.created_at) | ||
| ); | ||
| if (path === null) return "no-transcript"; | ||
| const before = fingerprint(path); | ||
| if (before === null) return "no-transcript"; | ||
| const usage = (args.readUsage ?? readTranscriptUsage)(path, { target: false }); | ||
| if (fingerprint(path) !== before) return "transcript-changed"; | ||
| if (usage === null) return "no-usage"; | ||
| const trace = capTrace(usage.trace) ?? void 0; | ||
| const counts = { | ||
| tokensIn: usage.tokensIn, | ||
| tokensOut: usage.tokensOut, | ||
| // We submit ONLY for a session whose serving process is gone and whose | ||
| // transcript is complete and stable — i.e. one that ran to termination, | ||
| // which is the same condition the hook path encodes as success (it counts | ||
| // `clear` / `logout` / `prompt_input_exit`, the normal endings). The hook's | ||
| // conservative default of `false` is not available to us: it keys off a | ||
| // termination reason only the host can report, and defaulting to it would | ||
| // stamp `false` on EVERY reconciled run — a systematic label, not a | ||
| // measurement. See the PR for the divergence this leaves. | ||
| success: true, | ||
| // Measured, not inferred from this process's clock. The hook can bound the | ||
| // run with a real session-end moment; we cannot, so the transcript's own | ||
| // last-write time is used as the end bound. Using `now` here would report | ||
| // the age of the ENTRY (up to a full day) as the run's duration. | ||
| durationMs: durationFromTranscript(path, args.entry, now), | ||
| ...trace ? { trace } : {} | ||
| }; | ||
| return finish(await trySubmit(counts)); | ||
| async function trySubmit(measured) { | ||
| try { | ||
| const outcome = await submitActuals({ | ||
| store, | ||
| client, | ||
| entry: args.entry, | ||
| counts: measured, | ||
| logger | ||
| }); | ||
| return outcome.submitted; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function finish(submitted) { | ||
| return submitted ? "submitted" : "not-submitted"; | ||
| } | ||
| } | ||
| function durationFromTranscript(path, entry, now) { | ||
| const created = Date.parse(entry.created_at); | ||
| if (!Number.isFinite(created)) return 0; | ||
| let endMs; | ||
| try { | ||
| endMs = statSync(path).mtimeMs; | ||
| } catch { | ||
| return 0; | ||
| } | ||
| if (!Number.isFinite(endMs) || endMs < created || endMs > now.getTime() + 1e3) { | ||
| return 0; | ||
| } | ||
| return Math.max(0, Math.round(endMs - created)); | ||
| } | ||
| export { | ||
| reconcileEntry | ||
| }; |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
721230
0