taximeter 0.2.2 → 0.3.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.
package/dist/cli/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Ledger,
4
+ amountSchema,
5
+ assetMetadata,
4
6
  budgetSchema,
5
7
  closeProxy,
8
+ configPatchSchema,
6
9
  countsAsSpend,
7
10
  createProxy,
8
11
  diagnosticSchema,
@@ -10,71 +13,388 @@ import {
10
13
  formatAmount,
11
14
  httpUrlSchema,
12
15
  integerStringSchema,
16
+ knownAssets,
13
17
  labelSchema,
14
18
  loadConfig,
19
+ loadConfigDetails,
15
20
  matchesAsset,
21
+ parseConfig,
16
22
  paymentEventSchema,
17
23
  portInputSchema,
18
- spentForBudget,
19
24
  toCsv,
20
25
  toInvoice,
21
26
  toJson,
22
27
  totalSchema,
23
28
  totals,
29
+ totalsForBudget,
24
30
  version
25
- } from "../chunk-DJPLFDD7.js";
31
+ } from "../chunk-Q2M76JNT.js";
26
32
 
27
33
  // src/cli/index.ts
28
34
  import { CommanderError } from "commander";
29
35
 
30
36
  // src/cli/program.ts
31
- import { existsSync as existsSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
37
+ import { existsSync as existsSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
32
38
  import { resolve as resolve2 } from "path";
33
39
  import { Command } from "commander";
34
- import { z as z4 } from "zod";
40
+ import { z as z8 } from "zod";
41
+
42
+ // src/config/amount-input.ts
43
+ import { z } from "zod";
44
+ function metadataForSelector(selector, network) {
45
+ const registry = knownAssets();
46
+ const bySymbol = registry.find((entry) => entry.assetSymbol === selector);
47
+ if (bySymbol) return bySymbol;
48
+ if (network) return assetMetadata(network, selector);
49
+ const entries = registry.filter((entry) => entry.asset.toLowerCase() === selector.toLowerCase());
50
+ const first = entries[0];
51
+ if (first && entries.every(
52
+ (entry) => entry.decimals === first.decimals && entry.assetSymbol === first.assetSymbol
53
+ ))
54
+ return first;
55
+ return { decimals: 0, decimalsKnown: false, assetSymbol: void 0 };
56
+ }
57
+ function knownAmountSymbols() {
58
+ return [
59
+ ...new Set(knownAssets().flatMap((entry) => entry.assetSymbol ? [entry.assetSymbol] : []))
60
+ ].sort();
61
+ }
62
+ function parseAmountInput(input, selector = "USDC", network) {
63
+ const text = z.string().min(1).parse(input).trim();
64
+ if (/^[0-9]+$/.test(text)) return amountSchema.parse(text);
65
+ const match = /^(0|[1-9][0-9]*)(?:\.([0-9]+))?\s*([a-zA-Z][a-zA-Z0-9]*)$/.exec(text);
66
+ if (!match)
67
+ throw new Error("Use an exact atomic integer or an amount with a known symbol, such as 5USDC.");
68
+ const symbol = match[3]?.toUpperCase();
69
+ const metadata = knownAssets().find((entry) => entry.assetSymbol === symbol);
70
+ if (!metadata) {
71
+ throw new Error(
72
+ `Unknown asset symbol ${symbol}. Known symbols: ${knownAmountSymbols().join(", ")}. An exact atomic integer is always accepted.`
73
+ );
74
+ }
75
+ const selected = metadataForSelector(selector, network);
76
+ if (!selected.decimalsKnown || selected.assetSymbol !== symbol) {
77
+ throw new Error(
78
+ `Amount unit ${symbol} does not match configured asset ${selector}. Change the asset explicitly or use an exact atomic integer.`
79
+ );
80
+ }
81
+ const fraction = match[2] ?? "";
82
+ if (fraction.length > metadata.decimals)
83
+ throw new Error(
84
+ `${symbol} supports at most ${metadata.decimals} decimal places; no rounding is performed.`
85
+ );
86
+ const scale = 10n ** BigInt(metadata.decimals);
87
+ const units = BigInt(match[1] ?? "0") * scale + BigInt(fraction.padEnd(metadata.decimals, "0") || "0");
88
+ return amountSchema.parse(units.toString());
89
+ }
90
+ function formatAmount2(amount2, selector, network) {
91
+ const atomic = amountSchema.parse(amount2);
92
+ const metadata = metadataForSelector(selector, network);
93
+ if (!metadata.decimalsKnown || !metadata.assetSymbol) return `${atomic} atomic units`;
94
+ const digits = atomic.padStart(metadata.decimals + 1, "0");
95
+ const whole = metadata.decimals ? digits.slice(0, -metadata.decimals) : digits;
96
+ const fraction = metadata.decimals ? digits.slice(-metadata.decimals).replace(/0+$/, "") : "";
97
+ return `${whole}${fraction ? `.${fraction}` : ""} ${metadata.assetSymbol}`;
98
+ }
99
+
100
+ // src/config/display.ts
101
+ function layerStatus(layer) {
102
+ if (!layer.supplied)
103
+ return layer.path ? "[not found]" : layer.name === "explicit" ? "(not supplied)" : "(none)";
104
+ const state = layer.contributed ? "in use" : layer.keys.length ? "overridden" : "empty";
105
+ const partial = layer.contributed && layer.overriddenKeys.length ? `; overridden keys: ${layer.overriddenKeys.join(", ")}` : "";
106
+ return `[${layer.path ? layer.exists ? "exists, " : "candidate, " : ""}${state}${partial}]`;
107
+ }
108
+ function formatLayers(details) {
109
+ return [
110
+ "Layers (highest priority first):",
111
+ ...details.layers.map((layer) => {
112
+ const value = layer.path ?? (layer.name === "defaults" ? "built in" : layer.name === "environment" ? layer.environmentVariables?.join(", ") ?? "" : layer.keys.join(", "));
113
+ return ` ${layer.label.padEnd(13)}${value}${value ? " " : ""}${layerStatus(layer)}`;
114
+ })
115
+ ].join("\n");
116
+ }
117
+ function formatEffectiveConfig(config) {
118
+ const policy = config.policy;
119
+ const amountDescription = (amount2, asset, network) => {
120
+ const formatted = formatAmount2(amount2, asset, network);
121
+ return formatted === `${amount2} atomic units` ? formatted : `${amount2} (${formatted})`;
122
+ };
123
+ const list = (items, meaning = "") => items.length ? items.join(", ") : `(empty${meaning ? ` \u2014 ${meaning}` : ""})`;
124
+ return [
125
+ "Effective policy:",
126
+ ` maxSinglePayment ${policy.maxSinglePayment === null ? "disabled" : amountDescription(policy.maxSinglePayment, policy.maxSingleAsset)}`,
127
+ ` maxSingleAsset ${policy.maxSingleAsset}`,
128
+ ` unknownAsset ${policy.unknownAsset}`,
129
+ ` allowHosts ${list(policy.allowHosts, "all hosts allowed")}`,
130
+ ` denyHosts ${list(policy.denyHosts)}`,
131
+ ` allowPayTo ${list(policy.allowPayTo, "all recipients allowed")}`,
132
+ "",
133
+ "Effective budgets:",
134
+ ...["perTask", "perAgent", "global"].map((scope) => {
135
+ const budget = config.budgets[scope];
136
+ if (!budget) return ` ${scope.padEnd(11)}disabled`;
137
+ const amount2 = budget.amount === void 0 ? "(no amount limit)" : amountDescription(budget.amount, budget.asset, budget.network);
138
+ const count2 = budget.maxPayments === void 0 ? "(no payment-count limit)" : `max ${budget.maxPayments} payments`;
139
+ return ` ${scope.padEnd(11)}${amount2} ${budget.window ? `${budget.window} ` : ""}${count2} asset ${budget.asset}${budget.network ? ` network ${budget.network}` : ""}`;
140
+ })
141
+ ].join("\n");
142
+ }
143
+
144
+ // src/config/edit.ts
145
+ import { randomUUID } from "crypto";
146
+ import {
147
+ closeSync,
148
+ existsSync,
149
+ fsyncSync,
150
+ mkdirSync,
151
+ openSync,
152
+ readFileSync,
153
+ renameSync,
154
+ unlinkSync,
155
+ writeFileSync
156
+ } from "fs";
157
+ import { dirname } from "path";
158
+ import { z as z2 } from "zod";
159
+ function unwrapped(schema) {
160
+ if (schema instanceof z2.ZodOptional || schema instanceof z2.ZodNullable || schema instanceof z2.ZodDefault || schema instanceof z2.ZodPrefault) {
161
+ const inner = schema.unwrap();
162
+ if (!(inner instanceof z2.ZodType)) throw new Error("Unsupported configuration schema");
163
+ return unwrapped(inner);
164
+ }
165
+ return schema;
166
+ }
167
+ function schemaPaths(schema, prefix = "") {
168
+ const paths = /* @__PURE__ */ new Map();
169
+ const value = unwrapped(schema);
170
+ if (prefix) paths.set(prefix, schema);
171
+ if (value instanceof z2.ZodObject) {
172
+ for (const [key, child] of Object.entries(value.shape)) {
173
+ if (!(child instanceof z2.ZodType)) throw new Error("Unsupported configuration schema");
174
+ for (const entry of schemaPaths(child, prefix ? `${prefix}.${key}` : key))
175
+ paths.set(...entry);
176
+ }
177
+ }
178
+ return paths;
179
+ }
180
+ function distance(left, right) {
181
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
182
+ for (let row = 1; row <= left.length; row += 1) {
183
+ const current = [row];
184
+ for (let column = 1; column <= right.length; column += 1) {
185
+ current[column] = Math.min(
186
+ (current[column - 1] ?? 0) + 1,
187
+ (previous[column] ?? 0) + 1,
188
+ (previous[column - 1] ?? 0) + (left[row - 1] === right[column - 1] ? 0 : 1)
189
+ );
190
+ }
191
+ previous = current;
192
+ }
193
+ return previous[right.length] ?? left.length;
194
+ }
195
+ function schemaForKey(input) {
196
+ const key = z2.string().parse(input);
197
+ const paths = schemaPaths(configPatchSchema);
198
+ const schema = paths.get(key);
199
+ if (schema) return schema;
200
+ const nearest = [...paths.keys()].sort(
201
+ (left, right) => distance(key, left) - distance(key, right) || left.localeCompare(right)
202
+ )[0];
203
+ throw new Error(`Unknown configuration key "${key}". Did you mean "${nearest}"?`);
204
+ }
205
+ function configValue(config, key) {
206
+ schemaForKey(key);
207
+ let value = config;
208
+ for (const part of key.split(".")) {
209
+ if (value === null || typeof value !== "object" || !Object.hasOwn(value, part))
210
+ return void 0;
211
+ value = Reflect.get(value, part);
212
+ }
213
+ return value;
214
+ }
215
+ function amountContext(config, key) {
216
+ if (key === "policy.maxSinglePayment") return { asset: config.policy.maxSingleAsset };
217
+ const match = /^budgets\.(perTask|perAgent|global)\.amount$/.exec(key);
218
+ if (!match) return void 0;
219
+ const scope = z2.enum(["perTask", "perAgent", "global"]).parse(match[1]);
220
+ return config.budgets[scope] ?? { asset: "USDC" };
221
+ }
222
+ function parsedInput(key, input, config) {
223
+ const schema = schemaForKey(key);
224
+ const value = z2.string().parse(input);
225
+ if (value === "null") return null;
226
+ const context = amountContext(config, key);
227
+ if (context) return parseAmountInput(value, context.asset, context.network);
228
+ const type = unwrapped(schema);
229
+ if (type instanceof z2.ZodArray) {
230
+ if (value.trim().startsWith("[")) return JSON.parse(value);
231
+ return value === "" ? [] : value.split(",").map((entry) => entry.trim());
232
+ }
233
+ if (type instanceof z2.ZodObject) return JSON.parse(value);
234
+ if (type instanceof z2.ZodNumber)
235
+ return z2.string().regex(/^(0|[1-9][0-9]*)$/, "Use a plain integer without units").transform(Number).parse(value);
236
+ return value;
237
+ }
238
+ function parseConfigInput(key, input, config) {
239
+ try {
240
+ return parsedInput(key, input, config);
241
+ } catch (error) {
242
+ if (error instanceof z2.ZodError)
243
+ throw new z2.ZodError(
244
+ error.issues.map((issue) => ({ ...issue, path: [...key.split("."), ...issue.path] }))
245
+ );
246
+ if (error instanceof SyntaxError) throw new Error(`${key}: expected valid JSON`);
247
+ throw error;
248
+ }
249
+ }
250
+ function configError(error) {
251
+ if (error instanceof z2.ZodError)
252
+ return new Error(
253
+ error.issues.map((issue) => `${issue.path.join(".") || "configuration"}: ${issue.message}`).join("; ")
254
+ );
255
+ return error instanceof Error ? error : new Error("Invalid configuration");
256
+ }
257
+ function changePath(layer, key, value, unset) {
258
+ const parts = key.split(".");
259
+ const final = parts.pop();
260
+ if (!final) throw new Error("Expected a configuration key");
261
+ let parent = layer;
262
+ const ancestors = [];
263
+ for (const part of parts) {
264
+ const next = parent[part];
265
+ if (next === null || typeof next !== "object" || Array.isArray(next)) {
266
+ if (unset) return;
267
+ parent[part] = {};
268
+ }
269
+ ancestors.push({ value: parent, key: part });
270
+ parent = z2.record(z2.string(), z2.unknown()).parse(parent[part]);
271
+ const ancestor = ancestors.at(-1);
272
+ if (ancestor) ancestor.value[part] = parent;
273
+ }
274
+ if (unset) {
275
+ delete parent[final];
276
+ for (const ancestor of ancestors.reverse()) {
277
+ const child = ancestor.value[ancestor.key];
278
+ if (child && typeof child === "object" && Object.keys(child).length === 0)
279
+ delete ancestor.value[ancestor.key];
280
+ else break;
281
+ }
282
+ } else parent[final] = value;
283
+ }
284
+ function atomicWrite(path, content) {
285
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
286
+ const temporary = `${path}.${randomUUID()}.tmp`;
287
+ let created = false;
288
+ try {
289
+ const descriptor = openSync(temporary, "wx", 384);
290
+ created = true;
291
+ try {
292
+ writeFileSync(descriptor, content, "utf8");
293
+ fsyncSync(descriptor);
294
+ } finally {
295
+ closeSync(descriptor);
296
+ }
297
+ renameSync(temporary, path);
298
+ created = false;
299
+ } finally {
300
+ if (created) unlinkSync(temporary);
301
+ }
302
+ }
303
+ function editConfig(input) {
304
+ try {
305
+ schemaForKey(input.key);
306
+ const options3 = input.options ?? {};
307
+ const before = loadConfigDetails(input.flags, options3);
308
+ const path = expandPath(input.file ?? before.defaultWritePath, options3.home, options3.cwd);
309
+ if (path === ":memory:") throw new Error("Choose a configuration file path, not :memory:");
310
+ const original = existsSync(path) ? readFileSync(path, "utf8") : void 0;
311
+ const layer = configPatchSchema.parse(original === void 0 ? {} : JSON.parse(original));
312
+ const targetIndex = before.layers.map((entry) => entry.path).lastIndexOf(path);
313
+ const participates = targetIndex >= 0;
314
+ const amountConfig = input.unset || !amountContext(before.config, input.key) ? before.config : participates ? loadConfigDetails(
315
+ {},
316
+ {
317
+ ...options3,
318
+ env: {},
319
+ layerOverrides: {
320
+ ...options3.layerOverrides,
321
+ ...Object.fromEntries(
322
+ before.layers.slice(0, targetIndex).filter((entry) => entry.path && entry.path !== path).map((entry) => [entry.path, {}])
323
+ )
324
+ }
325
+ }
326
+ ).config : parseConfig(layer);
327
+ const value = input.unset ? void 0 : parseConfigInput(input.key, z2.string().parse(input.value), amountConfig);
328
+ changePath(layer, input.key, value, input.unset ?? false);
329
+ const candidate = configPatchSchema.parse(layer);
330
+ if (!participates) parseConfig(candidate);
331
+ const after = loadConfigDetails(input.flags, {
332
+ ...options3,
333
+ layerOverrides: { ...options3.layerOverrides, [path]: candidate }
334
+ });
335
+ const unchanged = input.unset && original === void 0;
336
+ if (!unchanged) atomicWrite(path, `${JSON.stringify(candidate, null, 2)}
337
+ `);
338
+ return {
339
+ path,
340
+ key: input.key,
341
+ before: before.config,
342
+ after: after.config,
343
+ writtenValue: configValue(candidate, input.key),
344
+ writtenAmountContext: amountContext(amountConfig, input.key),
345
+ participates,
346
+ changed: !unchanged
347
+ };
348
+ } catch (error) {
349
+ throw configError(error);
350
+ }
351
+ }
35
352
 
36
353
  // src/server/lifecycle.ts
37
- import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
38
- import { z as z3 } from "zod";
354
+ import { existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
355
+ import { z as z5 } from "zod";
39
356
 
40
357
  // src/server/index.ts
41
- import { existsSync, readFileSync, statSync } from "fs";
358
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
42
359
  import { createServer } from "http";
43
- import { dirname, extname, resolve, sep } from "path";
360
+ import { dirname as dirname2, extname, resolve, sep } from "path";
44
361
  import { fileURLToPath } from "url";
45
- import { z as z2 } from "zod";
362
+ import { z as z4 } from "zod";
46
363
 
47
364
  // src/server/schema.ts
48
- import { z } from "zod";
365
+ import { z as z3 } from "zod";
49
366
  var dashboardEventSchema = paymentEventSchema.omit({ raw: true });
50
- var groupSchema = totalSchema.extend({ key: z.string().nullable() });
51
- var dashboardStateSchema = z.object({
52
- version: z.literal(version),
53
- generatedAt: z.iso.datetime(),
54
- totalEvents: z.number().int(),
55
- blockedEvents: z.number().int(),
56
- unknownEvents: z.number().int(),
57
- totals: z.array(totalSchema),
58
- events: z.array(dashboardEventSchema),
59
- diagnostics: z.array(diagnosticSchema),
60
- groups: z.object({
61
- task: z.array(groupSchema),
62
- agent: z.array(groupSchema),
63
- host: z.array(groupSchema)
367
+ var groupSchema = totalSchema.extend({ key: z3.string().nullable() });
368
+ var dashboardStateSchema = z3.object({
369
+ version: z3.literal(version),
370
+ generatedAt: z3.iso.datetime(),
371
+ totalEvents: z3.number().int(),
372
+ blockedEvents: z3.number().int(),
373
+ unknownEvents: z3.number().int(),
374
+ totals: z3.array(totalSchema),
375
+ events: z3.array(dashboardEventSchema),
376
+ diagnostics: z3.array(diagnosticSchema),
377
+ groups: z3.object({
378
+ task: z3.array(groupSchema),
379
+ agent: z3.array(groupSchema),
380
+ host: z3.array(groupSchema)
64
381
  }),
65
- hours: z.array(z.object({ ts: z.iso.datetime(), totals: z.array(totalSchema) })),
382
+ hours: z3.array(z3.object({ ts: z3.iso.datetime(), totals: z3.array(totalSchema) })),
66
383
  globalBudget: budgetSchema.nullable(),
67
- budgets: z.array(
68
- z.object({
69
- network: z.string(),
70
- asset: z.string(),
71
- limit: integerStringSchema,
384
+ budgets: z3.array(
385
+ z3.object({
386
+ network: z3.string(),
387
+ asset: z3.string(),
388
+ limit: integerStringSchema.nullable(),
72
389
  spent: integerStringSchema,
73
- remaining: integerStringSchema,
74
- window: z.string().nullable()
390
+ remaining: integerStringSchema.nullable(),
391
+ count: integerStringSchema,
392
+ countLimit: integerStringSchema.nullable(),
393
+ countRemaining: integerStringSchema.nullable(),
394
+ window: z3.string().nullable()
75
395
  })
76
396
  ),
77
- proxy: z.object({ port: z.number().int(), upstream: z.string().optional() })
397
+ proxy: z3.object({ port: z3.number().int(), upstream: z3.string().optional() })
78
398
  });
79
399
 
80
400
  // src/server/state.ts
@@ -100,14 +420,18 @@ function dashboardState(ledger, config, now = Date.now()) {
100
420
  const representative = events.find(
101
421
  (event) => event.network === total2.network && event.asset.toLowerCase() === total2.asset
102
422
  );
103
- const spent = representative ? spentForBudget(representative, events, budget, "global", now) : "0";
104
- const remaining = BigInt(budget.amount) - BigInt(spent);
423
+ const { amount: spent, count: count2 } = representative ? totalsForBudget(representative, events, budget, "global", now) : { amount: "0", count: "0" };
424
+ const remaining = budget.amount === void 0 ? null : BigInt(budget.amount) - BigInt(spent);
425
+ const countRemaining = budget.maxPayments === void 0 ? null : BigInt(budget.maxPayments) - BigInt(count2);
105
426
  return {
106
427
  network: total2.network,
107
428
  asset: total2.asset,
108
- limit: budget.amount,
429
+ limit: budget.amount ?? null,
109
430
  spent,
110
- remaining: (remaining < 0n ? 0n : remaining).toString(),
431
+ remaining: remaining === null ? null : (remaining < 0n ? 0n : remaining).toString(),
432
+ count: count2,
433
+ countLimit: budget.maxPayments?.toString() ?? null,
434
+ countRemaining: countRemaining === null ? null : (countRemaining < 0n ? 0n : countRemaining).toString(),
111
435
  window: budget.window ?? null
112
436
  };
113
437
  }) : [];
@@ -145,7 +469,7 @@ function dashboardState(ledger, config, now = Date.now()) {
145
469
  }
146
470
 
147
471
  // src/server/index.ts
148
- var querySchema = z2.strictObject({ format: z2.enum(["csv", "json", "invoice"]).optional() });
472
+ var querySchema = z4.strictObject({ format: z4.enum(["csv", "json", "invoice"]).optional() });
149
473
  var types = {
150
474
  ".html": "text/html; charset=utf-8",
151
475
  ".js": "text/javascript; charset=utf-8",
@@ -154,17 +478,17 @@ var types = {
154
478
  ".woff2": "font/woff2"
155
479
  };
156
480
  function uiDirectory() {
157
- const here = dirname(fileURLToPath(import.meta.url));
481
+ const here = dirname2(fileURLToPath(import.meta.url));
158
482
  return [resolve(here, "ui"), resolve(here, "../ui"), resolve(here, "../../dist/ui")].find(
159
- (path) => existsSync(resolve(path, "index.html"))
483
+ (path) => existsSync2(resolve(path, "index.html"))
160
484
  ) ?? resolve(here, "ui");
161
485
  }
162
486
  function json(response, status, value) {
163
487
  response.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
164
488
  response.end(JSON.stringify(value));
165
489
  }
166
- function createDashboard(options2) {
167
- const root = resolve(options2.uiRoot ?? uiDirectory());
490
+ function createDashboard(options3) {
491
+ const root = resolve(options3.uiRoot ?? uiDirectory());
168
492
  return createServer((request, response) => {
169
493
  response.setHeader(
170
494
  "Content-Security-Policy",
@@ -173,13 +497,13 @@ function createDashboard(options2) {
173
497
  response.setHeader("X-Content-Type-Options", "nosniff");
174
498
  response.setHeader("Referrer-Policy", "no-referrer");
175
499
  try {
176
- const input = z2.object({
177
- method: z2.string(),
178
- url: z2.string(),
179
- headers: z2.object({
180
- host: z2.string(),
181
- origin: z2.string().optional(),
182
- "sec-fetch-site": z2.string().optional()
500
+ const input = z4.object({
501
+ method: z4.string(),
502
+ url: z4.string(),
503
+ headers: z4.object({
504
+ host: z4.string(),
505
+ origin: z4.string().optional(),
506
+ "sec-fetch-site": z4.string().optional()
183
507
  })
184
508
  }).parse(request);
185
509
  const localPort = request.socket.localPort;
@@ -205,12 +529,12 @@ function createDashboard(options2) {
205
529
  }
206
530
  const query = querySchema.parse(Object.fromEntries(entries));
207
531
  if (url.pathname === "/api/summary") {
208
- json(response, 200, dashboardState(options2.ledger, options2.config));
532
+ json(response, 200, dashboardState(options3.ledger, options3.config));
209
533
  return;
210
534
  }
211
535
  if (url.pathname === "/api/export") {
212
536
  const format = query.format ?? "json";
213
- const events = options2.ledger.view();
537
+ const events = options3.ledger.view();
214
538
  const body = format === "csv" ? toCsv(events) : format === "invoice" ? toInvoice(events) : toJson(events);
215
539
  response.writeHead(200, {
216
540
  "Content-Type": format === "csv" ? "text/csv; charset=utf-8" : format === "invoice" ? "text/html; charset=utf-8" : "application/json",
@@ -226,8 +550,8 @@ function createDashboard(options2) {
226
550
  json(response, 403, { error: "forbidden" });
227
551
  return;
228
552
  }
229
- if (!existsSync(path) || !statSync(path).isFile() || !types[extname(path)]) {
230
- if (url.pathname === "/" && !existsSync(resolve(root, "index.html"))) {
553
+ if (!existsSync2(path) || !statSync(path).isFile() || !types[extname(path)]) {
554
+ if (url.pathname === "/" && !existsSync2(resolve(root, "index.html"))) {
231
555
  response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
232
556
  response.end(
233
557
  "<!doctype html><title>Taximeter</title><h1>Taximeter</h1><p>The local ledger is ready. Contributors: run npm run build to build the dashboard.</p>"
@@ -241,7 +565,7 @@ function createDashboard(options2) {
241
565
  "Content-Type": types[extname(path)],
242
566
  "Cache-Control": path.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable"
243
567
  });
244
- response.end(readFileSync(path));
568
+ response.end(readFileSync2(path));
245
569
  } catch {
246
570
  json(response, 400, { error: "invalid_request" });
247
571
  }
@@ -257,18 +581,18 @@ async function listenLocal(server, port2) {
257
581
  resolve3();
258
582
  });
259
583
  });
260
- return z3.object({ port: z3.number() }).parse(server.address()).port;
584
+ return z5.object({ port: z5.number() }).parse(server.address()).port;
261
585
  }
262
586
  function assertStopped(db) {
263
587
  if (db === ":memory:") return;
264
588
  const path = `${db}.lock`;
265
- if (!existsSync2(path)) return;
266
- const lock = z3.object({ pid: z3.number().int().positive() }).parse(JSON.parse(readFileSync2(path, "utf8")));
589
+ if (!existsSync3(path)) return;
590
+ const lock = z5.object({ pid: z5.number().int().positive() }).parse(JSON.parse(readFileSync3(path, "utf8")));
267
591
  try {
268
592
  process.kill(lock.pid, 0);
269
593
  } catch (error) {
270
- if (z3.object({ code: z3.literal("ESRCH") }).safeParse(error).success) {
271
- unlinkSync(path);
594
+ if (z5.object({ code: z5.literal("ESRCH") }).safeParse(error).success) {
595
+ unlinkSync2(path);
272
596
  return;
273
597
  }
274
598
  throw new Error("Cannot determine whether the ledger is in use.");
@@ -280,8 +604,8 @@ function acquireLock(db) {
280
604
  };
281
605
  assertStopped(db);
282
606
  const lock = `${db}.lock`;
283
- writeFileSync(lock, JSON.stringify({ pid: process.pid }), { flag: "wx", mode: 384 });
284
- return () => unlinkSync(lock);
607
+ writeFileSync2(lock, JSON.stringify({ pid: process.pid }), { flag: "wx", mode: 384 });
608
+ return () => unlinkSync2(lock);
285
609
  }
286
610
  async function startServices(input) {
287
611
  const config = { ...input, ports: { ...input.ports }, db: expandPath(input.db) };
@@ -328,43 +652,208 @@ async function startServices(input) {
328
652
  }
329
653
  }
330
654
 
655
+ // src/cli/config-command.ts
656
+ import { z as z6 } from "zod";
657
+ var common = z6.strictObject({ db: z6.string().optional(), config: z6.string().optional() });
658
+ var showFlags = common.extend({ json: z6.boolean().optional() });
659
+ var editFlags = common.extend({ file: z6.string().min(1).optional() });
660
+ function options(command) {
661
+ return command.option("--db <path>", "SQLite ledger path").option("--config <path>", "Explicit config file");
662
+ }
663
+ function valueText(value) {
664
+ return value === void 0 ? "(not set)" : typeof value === "string" ? value : JSON.stringify(value);
665
+ }
666
+ function addConfigCommands(program, output, sources) {
667
+ const group = options(
668
+ program.command("config").description("Inspect or edit local configuration")
669
+ );
670
+ const details = (flags) => loadConfigDetails(flags.db ? { db: flags.db } : {}, {
671
+ ...sources,
672
+ configFile: flags.config ?? sources.configFile
673
+ });
674
+ options(
675
+ group.command("path").description("Show configuration layers and the default write target")
676
+ ).action((_raw, command) => {
677
+ const resolved = details(common.parse(command.optsWithGlobals()));
678
+ output(
679
+ `${formatLayers(resolved)}
680
+ Default write target: ${resolved.defaultWritePath}
681
+ Use --file to write a different layer; --config selects a read layer.
682
+ `
683
+ );
684
+ });
685
+ options(group.command("show").description("Show the merged effective configuration")).option("--json", "Print the effective configuration as JSON").action((_raw, command) => {
686
+ const flags = showFlags.parse(command.optsWithGlobals());
687
+ const resolved = details(flags);
688
+ output(
689
+ flags.json ? `${JSON.stringify(resolved.config, null, 2)}
690
+ ` : `${formatLayers(resolved)}
691
+
692
+ ${formatEffectiveConfig(resolved.config)}
693
+
694
+ Ledger: ${resolved.config.db}
695
+ Proxy port: ${resolved.config.ports.proxy}
696
+ Dashboard port: ${resolved.config.ports.dashboard}
697
+ Upstream: ${resolved.config.upstream ?? "(not set)"}
698
+ `
699
+ );
700
+ });
701
+ options(group.command("get <key>").description("Read one effective value by dot path")).action(
702
+ (key, _raw, command) => {
703
+ schemaForKey(key);
704
+ output(
705
+ `${valueText(configValue(details(common.parse(command.optsWithGlobals())).config, key))}
706
+ `
707
+ );
708
+ }
709
+ );
710
+ const edit = (key, value, command, unset) => {
711
+ const flags = editFlags.parse(command.optsWithGlobals());
712
+ const result = editConfig({
713
+ key,
714
+ value,
715
+ unset,
716
+ file: flags.file,
717
+ flags: flags.db ? { db: flags.db } : {},
718
+ options: { ...sources, configFile: flags.config ?? sources.configFile }
719
+ });
720
+ const before = configValue(result.before, key);
721
+ const after = configValue(result.after, key);
722
+ const next = unset ? after : result.writtenValue;
723
+ const beforeContext = amountContext(result.before, key);
724
+ const afterContext = unset ? amountContext(result.after, key) : result.writtenAmountContext;
725
+ const human = typeof before === "string" && typeof next === "string" && beforeContext && afterContext && beforeContext.asset === afterContext.asset && beforeContext.network === afterContext.network ? ` (${formatAmount2(before, beforeContext.asset, beforeContext.network)} \u2192 ${formatAmount2(next, afterContext.asset, afterContext.network)})` : "";
726
+ output(
727
+ `${key}: ${valueText(before)} \u2192 ${unset ? `(unset in file; effective: ${valueText(after)})` : valueText(next)}${human}
728
+ `
729
+ );
730
+ output(result.changed ? `Written to ${result.path}
731
+ ` : `No file to change: ${result.path}
732
+ `);
733
+ if (!result.participates)
734
+ output(
735
+ "This file is not an active layer. Select it with --config when starting taximeter.\n"
736
+ );
737
+ else if (!unset && JSON.stringify(after) !== JSON.stringify(next))
738
+ output(
739
+ `The written value is overridden. Effective ${key}: ${valueText(after)}. Run taximeter config path to inspect the layers.
740
+ `
741
+ );
742
+ output("Restart taximeter for this to take effect.\n");
743
+ };
744
+ options(
745
+ group.command("set <key> <value>").description("Validate and atomically write one configuration value")
746
+ ).option("--file <path>", "Layer to write (default: existing cwd config, otherwise home config)").action(
747
+ (key, value, _raw, command) => edit(key, value, command, false)
748
+ );
749
+ options(group.command("unset <key>").description("Remove a value from a configuration layer")).option("--file <path>", "Layer to write (default: existing cwd config, otherwise home config)").action((key, _raw, command) => edit(key, void 0, command, true));
750
+ }
751
+
752
+ // src/cli/start-overrides.ts
753
+ import { z as z7 } from "zod";
754
+ var amount = z7.string().min(1).optional();
755
+ var count = z7.string().regex(/^[1-9][0-9]*$/, "Use a positive payment count without units").transform(Number).pipe(z7.number().int().positive().max(Number.MAX_SAFE_INTEGER)).optional();
756
+ var overridesSchema = z7.strictObject({
757
+ budgetGlobal: amount,
758
+ budgetTask: amount,
759
+ budgetAgent: amount,
760
+ maxPaymentsGlobal: count,
761
+ maxPaymentsTask: count,
762
+ maxPaymentsAgent: count,
763
+ maxSingle: amount,
764
+ allowHost: z7.array(z7.string().min(1).max(256)).optional(),
765
+ denyHost: z7.array(z7.string().min(1).max(256)).optional()
766
+ });
767
+ function startOverrides(flags, config) {
768
+ const budgets = {};
769
+ const policy = {};
770
+ const active = [];
771
+ for (const [scope, amount2, count2] of [
772
+ ["global", flags.budgetGlobal, flags.maxPaymentsGlobal],
773
+ ["perTask", flags.budgetTask, flags.maxPaymentsTask],
774
+ ["perAgent", flags.budgetAgent, flags.maxPaymentsAgent]
775
+ ]) {
776
+ if (amount2 === void 0 && count2 === void 0) continue;
777
+ const budget = config.budgets[scope];
778
+ const patch = budget ? {} : { asset: "USDC" };
779
+ if (amount2 !== void 0) {
780
+ patch.amount = parseAmountInput(amount2, budget?.asset ?? "USDC", budget?.network);
781
+ active.push({ key: `budgets.${scope}.amount`, value: patch.amount });
782
+ }
783
+ if (count2 !== void 0) {
784
+ patch.maxPayments = count2;
785
+ active.push({ key: `budgets.${scope}.maxPayments`, value: count2 });
786
+ }
787
+ budgets[scope] = patch;
788
+ }
789
+ if (flags.maxSingle !== void 0) {
790
+ policy.maxSinglePayment = parseAmountInput(flags.maxSingle, config.policy.maxSingleAsset);
791
+ active.push({ key: "policy.maxSinglePayment", value: policy.maxSinglePayment });
792
+ }
793
+ if (flags.allowHost !== void 0) {
794
+ policy.allowHosts = flags.allowHost;
795
+ active.push({ key: "policy.allowHosts", value: flags.allowHost });
796
+ }
797
+ if (flags.denyHost !== void 0) {
798
+ policy.denyHosts = flags.denyHost;
799
+ active.push({ key: "policy.denyHosts", value: flags.denyHost });
800
+ }
801
+ return { patch: configPatchSchema.parse({ budgets, policy }), active };
802
+ }
803
+
331
804
  // src/cli/program.ts
332
- var common = z4.strictObject({ db: z4.string().optional(), config: z4.string().optional() });
805
+ var common2 = z8.strictObject({ db: z8.string().optional(), config: z8.string().optional() });
333
806
  var port = portInputSchema.optional();
334
- var startFlags = common.extend({
807
+ var startFlags = common2.extend({
808
+ ...overridesSchema.shape,
335
809
  proxyPort: port,
336
810
  dashboardPort: port,
337
811
  upstream: httpUrlSchema.optional()
338
812
  });
339
- var reportFlags = common.extend({
340
- json: z4.boolean().optional(),
813
+ var reportFlags = common2.extend({
814
+ json: z8.boolean().optional(),
341
815
  task: labelSchema.optional(),
342
816
  agent: labelSchema.optional(),
343
817
  host: labelSchema.optional()
344
818
  });
345
- var exportFlags = common.extend({
346
- json: z4.string().min(1).optional(),
347
- csv: z4.string().min(1).optional(),
348
- invoice: z4.string().min(1).optional()
819
+ var exportFlags = common2.extend({
820
+ json: z8.string().min(1).optional(),
821
+ csv: z8.string().min(1).optional(),
822
+ invoice: z8.string().min(1).optional()
349
823
  }).refine(
350
824
  (flags) => [flags.json, flags.csv, flags.invoice].filter(Boolean).length <= 1,
351
825
  "Choose one export format"
352
826
  );
353
- var resetFlags = common.extend({ yes: z4.boolean().default(false) });
354
- function options(command) {
827
+ var resetFlags = common2.extend({ yes: z8.boolean().default(false) });
828
+ function options2(command) {
355
829
  return command.option("--db <path>", "SQLite ledger path").option("--config <path>", "Explicit config file");
356
830
  }
357
- function configFrom(flags) {
358
- return loadConfig(flags.db ? { db: flags.db } : {}, { configFile: flags.config });
359
- }
360
- async function runCli(input, output = (text) => process.stdout.write(text)) {
361
- const argv = z4.array(z4.string()).parse(input);
831
+ async function runCli(input, output = (text) => process.stdout.write(text), sources = {}) {
832
+ const sourceOptions = (flags) => ({
833
+ ...sources,
834
+ configFile: flags.config ?? sources.configFile
835
+ });
836
+ const configFrom = (flags) => loadConfig(flags.db ? { db: flags.db } : {}, sourceOptions(flags));
837
+ const argv = z8.array(z8.string()).parse(input);
362
838
  const program = new Command().name("taximeter").description("A taximeter for your AI agents.").version(version).exitOverride();
363
839
  program.configureOutput({ writeOut: output, writeErr: (text) => process.stderr.write(text) });
364
- options(program.command("start").description("Start the local proxy and dashboard")).option("--proxy-port <port>", "Proxy port (0 selects an available port)").option("--dashboard-port <port>", "Dashboard port").option("--upstream <url>", "Forward origin-form requests to this HTTP(S) upstream").action(async (raw) => {
840
+ options2(program.command("start").description("Start the local proxy and dashboard")).option("--proxy-port <port>", "Proxy port (0 selects an available port)").option("--dashboard-port <port>", "Dashboard port").option("--upstream <url>", "Forward origin-form requests to this HTTP(S) upstream").option(
841
+ "--budget-global <amount>",
842
+ "Override the global amount budget (atomic units or e.g. 5USDC)"
843
+ ).option("--budget-task <amount>", "Override the per-task amount budget").option("--budget-agent <amount>", "Override the per-agent amount budget").option("--max-payments-global <n>", "Override the global payment-count limit").option("--max-payments-task <n>", "Override the per-task payment-count limit").option("--max-payments-agent <n>", "Override the per-agent payment-count limit").option("--max-single <amount>", "Override the single-payment limit").option(
844
+ "--allow-host <host>",
845
+ "Allow a host (repeatable; replaces the file's allow-list)",
846
+ (value, previous = []) => [...previous, value]
847
+ ).option(
848
+ "--deny-host <host>",
849
+ "Deny a host (repeatable; replaces the file's deny-list)",
850
+ (value, previous = []) => [...previous, value]
851
+ ).action(async (raw) => {
365
852
  const flags = startFlags.parse(raw);
853
+ const overrides = startOverrides(flags, configFrom(flags));
366
854
  const config = loadConfig(
367
855
  {
856
+ ...overrides.patch,
368
857
  ...flags.db ? { db: flags.db } : {},
369
858
  ...flags.upstream ? { upstream: flags.upstream } : {},
370
859
  ports: {
@@ -372,7 +861,7 @@ async function runCli(input, output = (text) => process.stdout.write(text)) {
372
861
  ...flags.dashboardPort !== void 0 ? { dashboard: flags.dashboardPort } : {}
373
862
  }
374
863
  },
375
- { configFile: flags.config }
864
+ sourceOptions(flags)
376
865
  );
377
866
  const running = await startServices(config);
378
867
  output(
@@ -383,6 +872,16 @@ Point an HTTP-proxy-aware agent at http://127.0.0.1:${running.proxyPort}.
383
872
  HTTPS CONNECT is unmetered; use --upstream or withMeter for HTTPS payments.
384
873
  `
385
874
  );
875
+ const activeOverrides = [
876
+ ...overrides.active,
877
+ ...flags.db !== void 0 ? [{ key: "db", value: config.db }] : [],
878
+ ...flags.upstream !== void 0 ? [{ key: "upstream", value: config.upstream }] : [],
879
+ ...flags.proxyPort !== void 0 ? [{ key: "ports.proxy", value: flags.proxyPort }] : [],
880
+ ...flags.dashboardPort !== void 0 ? [{ key: "ports.dashboard", value: flags.dashboardPort }] : []
881
+ ];
882
+ for (const active of activeOverrides)
883
+ output(`Override: ${active.key} = ${valueText(active.value)} (flags; not saved)
884
+ `);
386
885
  const shutdown = () => {
387
886
  process.off("SIGINT", shutdown);
388
887
  process.off("SIGTERM", shutdown);
@@ -393,12 +892,12 @@ HTTPS CONNECT is unmetered; use --upstream or withMeter for HTTPS payments.
393
892
  process.once("SIGINT", shutdown);
394
893
  process.once("SIGTERM", shutdown);
395
894
  });
396
- options(
895
+ options2(
397
896
  program.command("report").description("Print exact totals, separately per network and asset")
398
897
  ).option("--json", "Machine-readable integer-string totals").option("--task <id>", "Filter by task").option("--agent <id>", "Filter by agent").option("--host <host>", "Filter by host").action((raw) => {
399
898
  const flags = reportFlags.parse(raw);
400
899
  const db = configFrom(flags).db;
401
- const ledger = new Ledger(existsSync3(db) ? db : ":memory:");
900
+ const ledger = new Ledger(existsSync4(db) ? db : ":memory:");
402
901
  try {
403
902
  const events = ledger.view().filter(
404
903
  (event) => (!flags.task || event.taskId === flags.task) && (!flags.agent || event.agentId === flags.agent) && (!flags.host || event.host === flags.host)
@@ -415,16 +914,16 @@ HTTPS CONNECT is unmetered; use --upstream or withMeter for HTTPS payments.
415
914
  ledger.close();
416
915
  }
417
916
  });
418
- options(program.command("export").description("Export the local ledger")).option("--json <file>", "Write JSON to a new file (default: stdout)").option("--csv <file>", "Write CSV with exact counted and authorized amounts").option("--invoice <file>", "Write a printable HTML payment statement").action((raw) => {
917
+ options2(program.command("export").description("Export the local ledger")).option("--json <file>", "Write JSON to a new file (default: stdout)").option("--csv <file>", "Write CSV with exact counted and authorized amounts").option("--invoice <file>", "Write a printable HTML payment statement").action((raw) => {
419
918
  const flags = exportFlags.parse(raw);
420
919
  const db = configFrom(flags).db;
421
- const ledger = new Ledger(existsSync3(db) ? db : ":memory:");
920
+ const ledger = new Ledger(existsSync4(db) ? db : ":memory:");
422
921
  try {
423
922
  const events = ledger.view();
424
923
  const data = flags.csv ? toCsv(events) : flags.invoice ? toInvoice(events) : toJson(events);
425
924
  const file = flags.csv ?? flags.invoice ?? flags.json;
426
925
  if (file) {
427
- writeFileSync2(resolve2(file), data, { flag: "wx", mode: 384 });
926
+ writeFileSync3(resolve2(file), data, { flag: "wx", mode: 384 });
428
927
  output(`Exported ${resolve2(file)}
429
928
  `);
430
929
  } else output(data);
@@ -432,7 +931,7 @@ HTTPS CONNECT is unmetered; use --upstream or withMeter for HTTPS payments.
432
931
  ledger.close();
433
932
  }
434
933
  });
435
- options(
934
+ options2(
436
935
  program.command("reset").description("Archive the current ledger after all writers have stopped")
437
936
  ).option("--yes", "Confirm archiving the current ledger").action((raw) => {
438
937
  const flags = resetFlags.parse(raw);
@@ -442,7 +941,7 @@ HTTPS CONNECT is unmetered; use --upstream or withMeter for HTTPS payments.
442
941
  );
443
942
  const { db } = configFrom(flags);
444
943
  assertStopped(db);
445
- if (!existsSync3(db)) {
944
+ if (!existsSync4(db)) {
446
945
  output("No ledger to archive.\n");
447
946
  return;
448
947
  }
@@ -451,25 +950,33 @@ HTTPS CONNECT is unmetered; use --upstream or withMeter for HTTPS payments.
451
950
  const ledger = new Ledger(db);
452
951
  ledger.close();
453
952
  const archive = `${db.endsWith(".db") ? db.slice(0, -3) : db}.archive-${Date.now()}.db`;
454
- if (existsSync3(archive)) throw new Error("Archive path already exists; retry later.");
455
- renameSync(db, archive);
953
+ if (existsSync4(archive)) throw new Error("Archive path already exists; retry later.");
954
+ renameSync2(db, archive);
456
955
  for (const suffix of ["-wal", "-shm"])
457
- if (existsSync3(`${db}${suffix}`)) renameSync(`${db}${suffix}`, `${archive}${suffix}`);
956
+ if (existsSync4(`${db}${suffix}`)) renameSync2(`${db}${suffix}`, `${archive}${suffix}`);
458
957
  output(`Archived ${archive}
459
958
  `);
460
959
  } finally {
461
960
  release();
462
961
  }
463
962
  });
464
- options(
963
+ options2(
465
964
  program.command("doctor").description("Check local config and SQLite without network requests")
466
- ).action((raw) => {
467
- const config = configFrom(common.parse(raw));
468
- const ledger = new Ledger(existsSync3(config.db) ? config.db : ":memory:");
965
+ ).option("--json", "Machine-readable configuration, sources, and local checks").action((raw) => {
966
+ const flags = common2.extend({ json: z8.boolean().optional() }).parse(raw);
967
+ const details = loadConfigDetails(flags.db ? { db: flags.db } : {}, sourceOptions(flags));
968
+ const config = details.config;
969
+ const ledger = new Ledger(existsSync4(config.db) ? config.db : ":memory:");
469
970
  try {
971
+ const events = ledger.eventCount();
470
972
  output(
471
- `Configuration: valid
472
- SQLite: ready (${ledger.events().length} events)
973
+ flags.json ? `${JSON.stringify({ configuration: "valid", ...details, sqlite: { status: "ready", events }, ledger: config.db, listenerBinding: "127.0.0.1", httpsConnect: "unmetered", networkChecks: "none" }, null, 2)}
974
+ ` : `Configuration: valid
975
+ ${formatLayers(details)}
976
+
977
+ ${formatEffectiveConfig(config)}
978
+
979
+ SQLite: ready (${events} events)
473
980
  Ledger: ${config.db}
474
981
  Listener binding: 127.0.0.1
475
982
  HTTPS CONNECT: unmetered
@@ -480,7 +987,12 @@ Network checks: none
480
987
  ledger.close();
481
988
  }
482
989
  });
483
- await program.parseAsync(argv, { from: "user" });
990
+ addConfigCommands(program, output, sources);
991
+ try {
992
+ await program.parseAsync(argv, { from: "user" });
993
+ } catch (error) {
994
+ throw configError(error);
995
+ }
484
996
  }
485
997
 
486
998
  // src/cli/index.ts