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/CHANGELOG.md +6 -0
- package/README.md +116 -12
- package/dist/{chunk-DJPLFDD7.js → chunk-Q2M76JNT.js} +261 -68
- package/dist/cli/index.js +607 -95
- package/dist/index.d.ts +27 -12
- package/dist/index.js +1 -1
- package/dist/ui/assets/index-_UDg88nw.js +101 -0
- package/dist/ui/index.html +1 -1
- package/docs/SDK.md +20 -9
- package/package.json +1 -1
- package/dist/ui/assets/index-DSVhxCXF.js +0 -101
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-
|
|
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
|
|
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
|
|
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
|
|
38
|
-
import { z as
|
|
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
|
|
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:
|
|
51
|
-
var dashboardStateSchema =
|
|
52
|
-
version:
|
|
53
|
-
generatedAt:
|
|
54
|
-
totalEvents:
|
|
55
|
-
blockedEvents:
|
|
56
|
-
unknownEvents:
|
|
57
|
-
totals:
|
|
58
|
-
events:
|
|
59
|
-
diagnostics:
|
|
60
|
-
groups:
|
|
61
|
-
task:
|
|
62
|
-
agent:
|
|
63
|
-
host:
|
|
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:
|
|
382
|
+
hours: z3.array(z3.object({ ts: z3.iso.datetime(), totals: z3.array(totalSchema) })),
|
|
66
383
|
globalBudget: budgetSchema.nullable(),
|
|
67
|
-
budgets:
|
|
68
|
-
|
|
69
|
-
network:
|
|
70
|
-
asset:
|
|
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
|
-
|
|
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:
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
481
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
158
482
|
return [resolve(here, "ui"), resolve(here, "../ui"), resolve(here, "../../dist/ui")].find(
|
|
159
|
-
(path) =>
|
|
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(
|
|
167
|
-
const root = resolve(
|
|
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 =
|
|
177
|
-
method:
|
|
178
|
-
url:
|
|
179
|
-
headers:
|
|
180
|
-
host:
|
|
181
|
-
origin:
|
|
182
|
-
"sec-fetch-site":
|
|
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(
|
|
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 =
|
|
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 (!
|
|
230
|
-
if (url.pathname === "/" && !
|
|
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(
|
|
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
|
|
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 (!
|
|
266
|
-
const lock =
|
|
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 (
|
|
271
|
-
|
|
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
|
-
|
|
284
|
-
return () =>
|
|
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
|
|
805
|
+
var common2 = z8.strictObject({ db: z8.string().optional(), config: z8.string().optional() });
|
|
333
806
|
var port = portInputSchema.optional();
|
|
334
|
-
var startFlags =
|
|
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 =
|
|
340
|
-
json:
|
|
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 =
|
|
346
|
-
json:
|
|
347
|
-
csv:
|
|
348
|
-
invoice:
|
|
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 =
|
|
354
|
-
function
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
455
|
-
|
|
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 (
|
|
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
|
-
|
|
963
|
+
options2(
|
|
465
964
|
program.command("doctor").description("Check local config and SQLite without network requests")
|
|
466
|
-
).action((raw) => {
|
|
467
|
-
const
|
|
468
|
-
const
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|