arc402-cli 0.9.19 → 0.10.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/README.md +41 -2
- package/dist/abis.d.ts +1 -0
- package/dist/abis.d.ts.map +1 -1
- package/dist/abis.js +45 -14
- package/dist/abis.js.map +1 -1
- package/dist/bundler.d.ts +1 -1
- package/dist/bundler.d.ts.map +1 -1
- package/dist/bundler.js +61 -27
- package/dist/bundler.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -5
- package/dist/client.js.map +1 -1
- package/dist/coinbase-smart-wallet.js +4 -1
- package/dist/coinbase-smart-wallet.js.map +1 -1
- package/dist/commands/accept.js +28 -25
- package/dist/commands/accept.js.map +1 -1
- package/dist/commands/agent-handshake.js +18 -15
- package/dist/commands/agent-handshake.js.map +1 -1
- package/dist/commands/agent.js +104 -98
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/agreements.js +98 -62
- package/dist/commands/agreements.js.map +1 -1
- package/dist/commands/arbitrator.js +81 -45
- package/dist/commands/arbitrator.js.map +1 -1
- package/dist/commands/arena-handshake.d.ts.map +1 -1
- package/dist/commands/arena-handshake.js +35 -53
- package/dist/commands/arena-handshake.js.map +1 -1
- package/dist/commands/arena.js +18 -12
- package/dist/commands/arena.js.map +1 -1
- package/dist/commands/backup.js +36 -30
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/cancel.js +18 -15
- package/dist/commands/cancel.js.map +1 -1
- package/dist/commands/channel.js +81 -45
- package/dist/commands/channel.js.map +1 -1
- package/dist/commands/coldstart.js +34 -31
- package/dist/commands/coldstart.js.map +1 -1
- package/dist/commands/compute.d.ts +14 -0
- package/dist/commands/compute.d.ts.map +1 -0
- package/dist/commands/compute.js +466 -0
- package/dist/commands/compute.js.map +1 -0
- package/dist/commands/config.js +30 -24
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contract-interaction.js +15 -12
- package/dist/commands/contract-interaction.js.map +1 -1
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +135 -98
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/deliver.js +76 -37
- package/dist/commands/deliver.js.map +1 -1
- package/dist/commands/discover.js +27 -24
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/dispute.js +110 -104
- package/dist/commands/dispute.js.map +1 -1
- package/dist/commands/doctor.js +55 -16
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/endpoint.js +95 -56
- package/dist/commands/endpoint.js.map +1 -1
- package/dist/commands/feed.js +18 -11
- package/dist/commands/feed.js.map +1 -1
- package/dist/commands/hire.js +40 -37
- package/dist/commands/hire.js.map +1 -1
- package/dist/commands/migrate.js +33 -30
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/negotiate.d.ts.map +1 -1
- package/dist/commands/negotiate.js +36 -34
- package/dist/commands/negotiate.js.map +1 -1
- package/dist/commands/openshell.js +104 -68
- package/dist/commands/openshell.js.map +1 -1
- package/dist/commands/owner.js +20 -17
- package/dist/commands/owner.js.map +1 -1
- package/dist/commands/policy.js +43 -41
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/relay.d.ts.map +1 -1
- package/dist/commands/relay.js +51 -18
- package/dist/commands/relay.js.map +1 -1
- package/dist/commands/remediate.js +23 -20
- package/dist/commands/remediate.js.map +1 -1
- package/dist/commands/reputation.js +27 -25
- package/dist/commands/reputation.js.map +1 -1
- package/dist/commands/setup.js +104 -65
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/trust.js +20 -17
- package/dist/commands/trust.js.map +1 -1
- package/dist/commands/verify.js +21 -18
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +645 -679
- package/dist/commands/wallet.js.map +1 -1
- package/dist/commands/watch.js +36 -33
- package/dist/commands/watch.js.map +1 -1
- package/dist/commands/watchtower.js +73 -37
- package/dist/commands/watchtower.js.map +1 -1
- package/dist/commands/workroom.d.ts.map +1 -1
- package/dist/commands/workroom.js +282 -143
- package/dist/commands/workroom.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +71 -22
- package/dist/config.js.map +1 -1
- package/dist/daemon/compute-metering.d.ts +61 -0
- package/dist/daemon/compute-metering.d.ts.map +1 -0
- package/dist/daemon/compute-metering.js +299 -0
- package/dist/daemon/compute-metering.js.map +1 -0
- package/dist/daemon/compute-session.d.ts +100 -0
- package/dist/daemon/compute-session.d.ts.map +1 -0
- package/dist/daemon/compute-session.js +231 -0
- package/dist/daemon/compute-session.js.map +1 -0
- package/dist/daemon/config.d.ts +19 -1
- package/dist/daemon/config.d.ts.map +1 -1
- package/dist/daemon/config.js +90 -16
- package/dist/daemon/config.js.map +1 -1
- package/dist/daemon/credentials.d.ts +24 -0
- package/dist/daemon/credentials.d.ts.map +1 -0
- package/dist/daemon/credentials.js +80 -0
- package/dist/daemon/credentials.js.map +1 -0
- package/dist/daemon/delivery-client.d.ts +35 -0
- package/dist/daemon/delivery-client.d.ts.map +1 -0
- package/dist/daemon/delivery-client.js +231 -0
- package/dist/daemon/delivery-client.js.map +1 -0
- package/dist/daemon/file-delivery.d.ts +98 -0
- package/dist/daemon/file-delivery.d.ts.map +1 -0
- package/dist/daemon/file-delivery.js +461 -0
- package/dist/daemon/file-delivery.js.map +1 -0
- package/dist/daemon/hire-listener.d.ts +3 -3
- package/dist/daemon/hire-listener.d.ts.map +1 -1
- package/dist/daemon/hire-listener.js +47 -13
- package/dist/daemon/hire-listener.js.map +1 -1
- package/dist/daemon/index.d.ts +2 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +526 -53
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/job-lifecycle.d.ts +1 -1
- package/dist/daemon/job-lifecycle.d.ts.map +1 -1
- package/dist/daemon/job-lifecycle.js +51 -11
- package/dist/daemon/job-lifecycle.js.map +1 -1
- package/dist/daemon/notify.d.ts +1 -1
- package/dist/daemon/notify.d.ts.map +1 -1
- package/dist/daemon/notify.js +53 -19
- package/dist/daemon/notify.js.map +1 -1
- package/dist/daemon/token-metering.js +47 -8
- package/dist/daemon/token-metering.js.map +1 -1
- package/dist/daemon/userops.d.ts +2 -2
- package/dist/daemon/userops.d.ts.map +1 -1
- package/dist/daemon/userops.js +27 -23
- package/dist/daemon/userops.js.map +1 -1
- package/dist/daemon/wallet-monitor.d.ts +1 -1
- package/dist/daemon/wallet-monitor.d.ts.map +1 -1
- package/dist/daemon/wallet-monitor.js +12 -8
- package/dist/daemon/wallet-monitor.js.map +1 -1
- package/dist/daemon/worker-executor.d.ts +71 -0
- package/dist/daemon/worker-executor.d.ts.map +1 -0
- package/dist/daemon/worker-executor.js +382 -0
- package/dist/daemon/worker-executor.js.map +1 -0
- package/dist/drain-v4.js +64 -26
- package/dist/drain-v4.js.map +1 -1
- package/dist/endpoint-config.js +63 -20
- package/dist/endpoint-config.js.map +1 -1
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +49 -15
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +50 -18
- package/dist/index.js.map +1 -1
- package/dist/openshell-runtime.d.ts.map +1 -1
- package/dist/openshell-runtime.js +82 -38
- package/dist/openshell-runtime.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +85 -78
- package/dist/program.js.map +1 -1
- package/dist/repl.js +31 -25
- package/dist/repl.js.map +1 -1
- package/dist/signing.js +6 -3
- package/dist/signing.js.map +1 -1
- package/dist/telegram-notify.js +40 -3
- package/dist/telegram-notify.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +56 -89
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Footer.js +7 -4
- package/dist/tui/Footer.js.map +1 -1
- package/dist/tui/Header.d.ts +1 -1
- package/dist/tui/Header.d.ts.map +1 -1
- package/dist/tui/Header.js +14 -9
- package/dist/tui/Header.js.map +1 -1
- package/dist/tui/InputLine.d.ts +2 -1
- package/dist/tui/InputLine.d.ts.map +1 -1
- package/dist/tui/InputLine.js +47 -97
- package/dist/tui/InputLine.js.map +1 -1
- package/dist/tui/Viewport.d.ts +1 -2
- package/dist/tui/Viewport.d.ts.map +1 -1
- package/dist/tui/Viewport.js +26 -6
- package/dist/tui/Viewport.js.map +1 -1
- package/dist/tui/WalletConnectPairing.js +19 -16
- package/dist/tui/WalletConnectPairing.js.map +1 -1
- package/dist/tui/components/Button.js +9 -6
- package/dist/tui/components/Button.js.map +1 -1
- package/dist/tui/components/CeremonyView.js +8 -5
- package/dist/tui/components/CeremonyView.js.map +1 -1
- package/dist/tui/components/CompletionDropdown.js +9 -6
- package/dist/tui/components/CompletionDropdown.js.map +1 -1
- package/dist/tui/components/ConfirmPrompt.js +8 -5
- package/dist/tui/components/ConfirmPrompt.js.map +1 -1
- package/dist/tui/components/CustomTextInput.js +14 -11
- package/dist/tui/components/CustomTextInput.js.map +1 -1
- package/dist/tui/components/InteractiveTable.js +12 -9
- package/dist/tui/components/InteractiveTable.js.map +1 -1
- package/dist/tui/components/StepSpinner.js +13 -10
- package/dist/tui/components/StepSpinner.js.map +1 -1
- package/dist/tui/components/Toast.js +12 -8
- package/dist/tui/components/Toast.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +21 -28
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/useChat.js +19 -13
- package/dist/tui/useChat.js.map +1 -1
- package/dist/tui/useCommand.d.ts +2 -3
- package/dist/tui/useCommand.d.ts.map +1 -1
- package/dist/tui/useCommand.js +24 -100
- package/dist/tui/useCommand.js.map +1 -1
- package/dist/tui/useNotifications.js +8 -5
- package/dist/tui/useNotifications.js.map +1 -1
- package/dist/tui/useScroll.d.ts.map +1 -1
- package/dist/tui/useScroll.js +12 -15
- package/dist/tui/useScroll.js.map +1 -1
- package/dist/ui/banner.d.ts +0 -12
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +19 -35
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/colors.js +19 -13
- package/dist/ui/colors.js.map +1 -1
- package/dist/ui/format.js +14 -6
- package/dist/ui/format.js.map +1 -1
- package/dist/ui/qr-render.js +11 -4
- package/dist/ui/qr-render.js.map +1 -1
- package/dist/ui/rpc-fallback.js +11 -6
- package/dist/ui/rpc-fallback.js.map +1 -1
- package/dist/ui/spinner.js +12 -6
- package/dist/ui/spinner.js.map +1 -1
- package/dist/ui/tree.js +6 -3
- package/dist/ui/tree.js.map +1 -1
- package/dist/utils/format.js +41 -27
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/hash.js +42 -4
- package/dist/utils/hash.js.map +1 -1
- package/dist/utils/time.js +6 -2
- package/dist/utils/time.js.map +1 -1
- package/dist/wallet-router.d.ts +1 -1
- package/dist/wallet-router.d.ts.map +1 -1
- package/dist/wallet-router.js +19 -12
- package/dist/wallet-router.js.map +1 -1
- package/dist/walletconnect-session.d.ts +1 -1
- package/dist/walletconnect-session.d.ts.map +1 -1
- package/dist/walletconnect-session.js +11 -6
- package/dist/walletconnect-session.js.map +1 -1
- package/dist/walletconnect.d.ts +5 -6
- package/dist/walletconnect.d.ts.map +1 -1
- package/dist/walletconnect.js +35 -32
- package/dist/walletconnect.js.map +1 -1
- package/package.json +11 -10
- package/INK6-UX-SPEC.md +0 -446
- package/MIGRATION-SPEC.md +0 -108
- package/TUI-SPEC.md +0 -214
- package/scripts/authorize-machine-key.ts +0 -43
- package/scripts/drain-wallet.ts +0 -149
- package/scripts/execute-spend-only.ts +0 -81
- package/scripts/register-agent-userop.ts +0 -186
- package/src/abis.ts +0 -187
- package/src/bundler.ts +0 -235
- package/src/client.ts +0 -36
- package/src/coinbase-smart-wallet.ts +0 -51
- package/src/commands/accept.ts +0 -64
- package/src/commands/agent-handshake.ts +0 -72
- package/src/commands/agent.ts +0 -691
- package/src/commands/agreements.ts +0 -350
- package/src/commands/arbitrator.ts +0 -180
- package/src/commands/arena-handshake.ts +0 -274
- package/src/commands/arena.ts +0 -122
- package/src/commands/backup.ts +0 -117
- package/src/commands/cancel.ts +0 -35
- package/src/commands/channel.ts +0 -218
- package/src/commands/coldstart.ts +0 -165
- package/src/commands/config.ts +0 -68
- package/src/commands/contract-interaction.ts +0 -166
- package/src/commands/daemon.ts +0 -1054
- package/src/commands/deliver.ts +0 -148
- package/src/commands/discover.ts +0 -350
- package/src/commands/dispute.ts +0 -375
- package/src/commands/doctor.ts +0 -172
- package/src/commands/endpoint.ts +0 -620
- package/src/commands/feed.ts +0 -229
- package/src/commands/hire.ts +0 -245
- package/src/commands/migrate.ts +0 -177
- package/src/commands/negotiate.ts +0 -272
- package/src/commands/openshell.ts +0 -1055
- package/src/commands/owner.ts +0 -35
- package/src/commands/policy.ts +0 -263
- package/src/commands/relay.ts +0 -277
- package/src/commands/remediate.ts +0 -24
- package/src/commands/reputation.ts +0 -79
- package/src/commands/setup.ts +0 -343
- package/src/commands/trust.ts +0 -27
- package/src/commands/verify.ts +0 -91
- package/src/commands/wallet.ts +0 -3548
- package/src/commands/watch.ts +0 -220
- package/src/commands/watchtower.ts +0 -248
- package/src/commands/workroom.ts +0 -963
- package/src/config.ts +0 -220
- package/src/daemon/config.ts +0 -344
- package/src/daemon/hire-listener.ts +0 -226
- package/src/daemon/index.ts +0 -1089
- package/src/daemon/job-lifecycle.ts +0 -215
- package/src/daemon/notify.ts +0 -297
- package/src/daemon/token-metering.ts +0 -183
- package/src/daemon/userops.ts +0 -119
- package/src/daemon/wallet-monitor.ts +0 -90
- package/src/drain-v4.ts +0 -159
- package/src/endpoint-config.ts +0 -83
- package/src/endpoint-notify.ts +0 -134
- package/src/index.ts +0 -74
- package/src/openshell-runtime.ts +0 -281
- package/src/program.ts +0 -88
- package/src/repl.ts +0 -178
- package/src/signing.ts +0 -28
- package/src/telegram-notify.ts +0 -88
- package/src/tui/App.tsx +0 -263
- package/src/tui/Footer.tsx +0 -18
- package/src/tui/Header.tsx +0 -45
- package/src/tui/InputLine.tsx +0 -243
- package/src/tui/Viewport.tsx +0 -51
- package/src/tui/WalletConnectPairing.tsx +0 -114
- package/src/tui/components/Button.tsx +0 -38
- package/src/tui/components/CeremonyView.tsx +0 -39
- package/src/tui/components/CompletionDropdown.tsx +0 -56
- package/src/tui/components/ConfirmPrompt.tsx +0 -36
- package/src/tui/components/CustomTextInput.tsx +0 -132
- package/src/tui/components/InteractiveTable.tsx +0 -106
- package/src/tui/components/StepSpinner.tsx +0 -84
- package/src/tui/components/Toast.tsx +0 -59
- package/src/tui/index.tsx +0 -90
- package/src/tui/useChat.ts +0 -103
- package/src/tui/useCommand.ts +0 -238
- package/src/tui/useNotifications.ts +0 -28
- package/src/tui/useScroll.ts +0 -69
- package/src/ui/banner.ts +0 -78
- package/src/ui/colors.ts +0 -30
- package/src/ui/format.ts +0 -78
- package/src/ui/qr-render.ts +0 -92
- package/src/ui/rpc-fallback.ts +0 -59
- package/src/ui/spinner.ts +0 -56
- package/src/ui/tree.ts +0 -16
- package/src/utils/format.ts +0 -48
- package/src/utils/hash.ts +0 -5
- package/src/utils/time.ts +0 -15
- package/src/wallet-router.ts +0 -178
- package/src/walletconnect-session.ts +0 -27
- package/src/walletconnect.ts +0 -309
- package/test/time.test.js +0 -11
- package/tsconfig.json +0 -33
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ARC-402 Job Lifecycle — Post-delivery processing.
|
|
3
|
-
*
|
|
4
|
-
* Handles everything that happens after a worker completes a job:
|
|
5
|
-
* 1. Execution receipt generation
|
|
6
|
-
* 2. Learning extraction from completed work
|
|
7
|
-
* 3. Worker memory update
|
|
8
|
-
* 4. Per-agreement job directory management
|
|
9
|
-
*
|
|
10
|
-
* These functions are called by the daemon after a successful delivery.
|
|
11
|
-
*/
|
|
12
|
-
import * as fs from "fs";
|
|
13
|
-
import * as path from "path";
|
|
14
|
-
import * as os from "os";
|
|
15
|
-
import * as crypto from "crypto";
|
|
16
|
-
import { readUsageReport, type AggregatedTokenUsage } from "./token-metering.js";
|
|
17
|
-
|
|
18
|
-
const ARC402_DIR = path.join(os.homedir(), ".arc402");
|
|
19
|
-
const RECEIPTS_DIR = path.join(ARC402_DIR, "receipts");
|
|
20
|
-
const WORKER_DIR = path.join(ARC402_DIR, "worker");
|
|
21
|
-
const WORKER_MEMORY_DIR = path.join(WORKER_DIR, "memory");
|
|
22
|
-
const WORKER_CONFIG = path.join(WORKER_DIR, "config.json");
|
|
23
|
-
const JOBS_DIR = path.join(ARC402_DIR, "jobs");
|
|
24
|
-
|
|
25
|
-
// ─── Execution Receipt ────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export interface ExecutionReceipt {
|
|
28
|
-
schema: "arc402.execution-receipt.v1";
|
|
29
|
-
agreement_id: string;
|
|
30
|
-
workroom_policy_hash: string;
|
|
31
|
-
started_at: string;
|
|
32
|
-
completed_at: string;
|
|
33
|
-
deliverable_hash: string;
|
|
34
|
-
worker_address: string;
|
|
35
|
-
metrics: {
|
|
36
|
-
wall_clock_seconds: number;
|
|
37
|
-
network_hosts_contacted: string[];
|
|
38
|
-
policy_violations_attempted: number;
|
|
39
|
-
};
|
|
40
|
-
token_usage: AggregatedTokenUsage | null;
|
|
41
|
-
receipt_hash: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Generate and persist an execution receipt after job delivery.
|
|
46
|
-
*/
|
|
47
|
-
export function generateReceipt(params: {
|
|
48
|
-
agreementId: string;
|
|
49
|
-
deliverableHash: string;
|
|
50
|
-
walletAddress: string;
|
|
51
|
-
startedAt: string;
|
|
52
|
-
completedAt: string;
|
|
53
|
-
policyFilePath?: string;
|
|
54
|
-
networkHosts?: string[];
|
|
55
|
-
}): ExecutionReceipt {
|
|
56
|
-
fs.mkdirSync(RECEIPTS_DIR, { recursive: true });
|
|
57
|
-
|
|
58
|
-
// Compute policy hash
|
|
59
|
-
let policyHash = "0x0";
|
|
60
|
-
const policyPath = params.policyFilePath ?? path.join(ARC402_DIR, "openshell-policy.yaml");
|
|
61
|
-
if (fs.existsSync(policyPath)) {
|
|
62
|
-
const content = fs.readFileSync(policyPath, "utf-8");
|
|
63
|
-
policyHash = "0x" + crypto.createHash("sha256").update(content).digest("hex");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Calculate wall clock time
|
|
67
|
-
const started = new Date(params.startedAt).getTime();
|
|
68
|
-
const completed = new Date(params.completedAt).getTime();
|
|
69
|
-
const wallClockSeconds = Math.max(0, Math.floor((completed - started) / 1000));
|
|
70
|
-
|
|
71
|
-
// Read token usage report (if the worker wrote one)
|
|
72
|
-
const tokenUsage = readUsageReport(params.agreementId);
|
|
73
|
-
|
|
74
|
-
const receipt: ExecutionReceipt = {
|
|
75
|
-
schema: "arc402.execution-receipt.v1",
|
|
76
|
-
agreement_id: params.agreementId,
|
|
77
|
-
workroom_policy_hash: policyHash,
|
|
78
|
-
started_at: params.startedAt,
|
|
79
|
-
completed_at: params.completedAt,
|
|
80
|
-
deliverable_hash: params.deliverableHash,
|
|
81
|
-
worker_address: params.walletAddress,
|
|
82
|
-
metrics: {
|
|
83
|
-
wall_clock_seconds: wallClockSeconds,
|
|
84
|
-
network_hosts_contacted: params.networkHosts ?? [],
|
|
85
|
-
policy_violations_attempted: 0,
|
|
86
|
-
},
|
|
87
|
-
token_usage: tokenUsage,
|
|
88
|
-
receipt_hash: "", // filled below
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Receipt hash = SHA-256 of the receipt content (without the hash field)
|
|
92
|
-
const hashInput = JSON.stringify({ ...receipt, receipt_hash: undefined });
|
|
93
|
-
receipt.receipt_hash = "0x" + crypto.createHash("sha256").update(hashInput).digest("hex");
|
|
94
|
-
|
|
95
|
-
// Persist
|
|
96
|
-
const receiptPath = path.join(RECEIPTS_DIR, `${params.agreementId}.json`);
|
|
97
|
-
fs.writeFileSync(receiptPath, JSON.stringify(receipt, null, 2));
|
|
98
|
-
|
|
99
|
-
return receipt;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── Learning Extraction ──────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Extract learnings from a completed job and update worker memory.
|
|
106
|
-
*
|
|
107
|
-
* This creates:
|
|
108
|
-
* 1. A per-job memory file: memory/job-<id>.md
|
|
109
|
-
* 2. Updates the cumulative learnings.md
|
|
110
|
-
* 3. Updates the worker config (job count, earnings)
|
|
111
|
-
*/
|
|
112
|
-
export function extractLearnings(params: {
|
|
113
|
-
agreementId: string;
|
|
114
|
-
taskDescription: string;
|
|
115
|
-
deliverableHash: string;
|
|
116
|
-
priceEth: string;
|
|
117
|
-
capability: string;
|
|
118
|
-
wallClockSeconds: number;
|
|
119
|
-
success: boolean;
|
|
120
|
-
}): void {
|
|
121
|
-
fs.mkdirSync(WORKER_MEMORY_DIR, { recursive: true });
|
|
122
|
-
|
|
123
|
-
const now = new Date().toISOString();
|
|
124
|
-
const dateStr = now.split("T")[0];
|
|
125
|
-
|
|
126
|
-
// 1. Per-job memory file
|
|
127
|
-
const jobMemory = `# Job: ${params.agreementId}
|
|
128
|
-
*Completed: ${now}*
|
|
129
|
-
*Capability: ${params.capability}*
|
|
130
|
-
*Price: ${params.priceEth} ETH*
|
|
131
|
-
*Duration: ${params.wallClockSeconds}s*
|
|
132
|
-
*Outcome: ${params.success ? "delivered" : "failed"}*
|
|
133
|
-
|
|
134
|
-
## Task
|
|
135
|
-
${params.taskDescription}
|
|
136
|
-
|
|
137
|
-
## Deliverable
|
|
138
|
-
Hash: ${params.deliverableHash}
|
|
139
|
-
|
|
140
|
-
## Learnings
|
|
141
|
-
- Completed ${params.capability} task in ${params.wallClockSeconds}s
|
|
142
|
-
- ${params.success ? "Delivered successfully" : "Delivery failed — review required"}
|
|
143
|
-
`;
|
|
144
|
-
|
|
145
|
-
const jobFile = path.join(WORKER_MEMORY_DIR, `job-${params.agreementId}.md`);
|
|
146
|
-
fs.writeFileSync(jobFile, jobMemory);
|
|
147
|
-
|
|
148
|
-
// 2. Update cumulative learnings.md
|
|
149
|
-
const learningsPath = path.join(WORKER_MEMORY_DIR, "learnings.md");
|
|
150
|
-
let learnings = "";
|
|
151
|
-
if (fs.existsSync(learningsPath)) {
|
|
152
|
-
learnings = fs.readFileSync(learningsPath, "utf-8");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const newEntry = `\n### ${dateStr} — ${params.capability} (${params.agreementId.slice(0, 8)}...)
|
|
156
|
-
- Duration: ${params.wallClockSeconds}s | Price: ${params.priceEth} ETH | Outcome: ${params.success ? "✓" : "✗"}
|
|
157
|
-
`;
|
|
158
|
-
|
|
159
|
-
// Append to learnings
|
|
160
|
-
if (learnings.includes("No learnings yet")) {
|
|
161
|
-
learnings = `# Accumulated Learnings\n\n*Auto-updated after each completed job.*\n${newEntry}`;
|
|
162
|
-
} else {
|
|
163
|
-
learnings += newEntry;
|
|
164
|
-
}
|
|
165
|
-
fs.writeFileSync(learningsPath, learnings);
|
|
166
|
-
|
|
167
|
-
// 3. Update worker config
|
|
168
|
-
if (fs.existsSync(WORKER_CONFIG)) {
|
|
169
|
-
try {
|
|
170
|
-
const config = JSON.parse(fs.readFileSync(WORKER_CONFIG, "utf-8"));
|
|
171
|
-
config.job_count = (config.job_count || 0) + 1;
|
|
172
|
-
const currentEarnings = parseFloat(config.total_earned_eth || "0");
|
|
173
|
-
const jobEarnings = parseFloat(params.priceEth || "0");
|
|
174
|
-
config.total_earned_eth = (currentEarnings + jobEarnings).toFixed(6);
|
|
175
|
-
config.last_job_at = now;
|
|
176
|
-
fs.writeFileSync(WORKER_CONFIG, JSON.stringify(config, null, 2));
|
|
177
|
-
} catch { /* non-fatal — config may be malformed */ }
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ─── Per-Agreement Job Directory ──────────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Create an isolated job directory for a specific agreement.
|
|
185
|
-
* Returns the path to the job workspace.
|
|
186
|
-
*/
|
|
187
|
-
export function createJobDirectory(agreementId: string): string {
|
|
188
|
-
const jobDir = path.join(JOBS_DIR, `agreement-${agreementId}`);
|
|
189
|
-
fs.mkdirSync(jobDir, { recursive: true });
|
|
190
|
-
return jobDir;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Clean up a job directory after settlement.
|
|
195
|
-
* Preserves the receipt and job memory — only removes working files.
|
|
196
|
-
*/
|
|
197
|
-
export function cleanJobDirectory(agreementId: string): void {
|
|
198
|
-
const jobDir = path.join(JOBS_DIR, `agreement-${agreementId}`);
|
|
199
|
-
if (fs.existsSync(jobDir)) {
|
|
200
|
-
fs.rmSync(jobDir, { recursive: true, force: true });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ─── Policy Hash ──────────────────────────────────────────────────────────────
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Compute the SHA-256 hash of the current workroom policy.
|
|
208
|
-
* This hash can be registered in AgentRegistry for verifiability.
|
|
209
|
-
*/
|
|
210
|
-
export function computePolicyHash(policyFilePath?: string): string {
|
|
211
|
-
const policyPath = policyFilePath ?? path.join(ARC402_DIR, "openshell-policy.yaml");
|
|
212
|
-
if (!fs.existsSync(policyPath)) return "0x0";
|
|
213
|
-
const content = fs.readFileSync(policyPath, "utf-8");
|
|
214
|
-
return "0x" + crypto.createHash("sha256").update(content).digest("hex");
|
|
215
|
-
}
|
package/src/daemon/notify.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Multi-channel notification module for daemon events.
|
|
3
|
-
* Supports Telegram, Discord webhooks, generic webhooks, and email (via nodemailer).
|
|
4
|
-
*/
|
|
5
|
-
import * as https from "https";
|
|
6
|
-
import * as http from "http";
|
|
7
|
-
import type { DaemonConfig } from "./config.js";
|
|
8
|
-
|
|
9
|
-
export type NotifyEvent =
|
|
10
|
-
| "hire_request"
|
|
11
|
-
| "hire_accepted"
|
|
12
|
-
| "hire_rejected"
|
|
13
|
-
| "delivery"
|
|
14
|
-
| "dispute"
|
|
15
|
-
| "channel_challenge"
|
|
16
|
-
| "low_balance"
|
|
17
|
-
| "daemon_started"
|
|
18
|
-
| "daemon_stopped";
|
|
19
|
-
|
|
20
|
-
// ─── Channel interface ────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
export interface NotificationChannel {
|
|
23
|
-
send(title: string, body: string): Promise<void>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ─── HTTP helper ──────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
function httpPost(url: string, payload: string, extraHeaders: Record<string, string>): Promise<void> {
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
const parsed = new URL(url);
|
|
31
|
-
const mod = parsed.protocol === "https:" ? https : http;
|
|
32
|
-
const options = {
|
|
33
|
-
hostname: parsed.hostname,
|
|
34
|
-
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
35
|
-
path: parsed.pathname + parsed.search,
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: {
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
"Content-Length": Buffer.byteLength(payload),
|
|
40
|
-
...extraHeaders,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
const req = mod.request(options, (res) => {
|
|
44
|
-
res.resume();
|
|
45
|
-
res.on("end", () => {
|
|
46
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
47
|
-
resolve();
|
|
48
|
-
} else {
|
|
49
|
-
reject(new Error(`HTTP ${res.statusCode}`));
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
req.on("error", reject);
|
|
54
|
-
req.write(payload);
|
|
55
|
-
req.end();
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ─── Telegram channel ─────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
export class TelegramChannel implements NotificationChannel {
|
|
62
|
-
constructor(private botToken: string, private chatId: string) {}
|
|
63
|
-
|
|
64
|
-
async send(title: string, body: string): Promise<void> {
|
|
65
|
-
const text = body ? `<b>${title}</b>\n${body}` : `<b>${title}</b>`;
|
|
66
|
-
const payload = JSON.stringify({ chat_id: this.chatId, text, parse_mode: "HTML" });
|
|
67
|
-
const options: https.RequestOptions = {
|
|
68
|
-
hostname: "api.telegram.org",
|
|
69
|
-
port: 443,
|
|
70
|
-
path: `/bot${this.botToken}/sendMessage`,
|
|
71
|
-
method: "POST",
|
|
72
|
-
headers: {
|
|
73
|
-
"Content-Type": "application/json",
|
|
74
|
-
"Content-Length": Buffer.byteLength(payload),
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
await new Promise<void>((resolve, reject) => {
|
|
78
|
-
const req = https.request(options, (res) => {
|
|
79
|
-
let data = "";
|
|
80
|
-
res.on("data", (c: Buffer) => { data += c.toString(); });
|
|
81
|
-
res.on("end", () => {
|
|
82
|
-
const parsed = JSON.parse(data) as { ok: boolean; description?: string };
|
|
83
|
-
if (!parsed.ok) {
|
|
84
|
-
reject(new Error(`Telegram API error: ${parsed.description}`));
|
|
85
|
-
} else {
|
|
86
|
-
resolve();
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
req.on("error", reject);
|
|
91
|
-
req.write(payload);
|
|
92
|
-
req.end();
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ─── Discord channel ──────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
export class DiscordChannel implements NotificationChannel {
|
|
100
|
-
constructor(private webhookUrl: string) {}
|
|
101
|
-
|
|
102
|
-
async send(title: string, body: string): Promise<void> {
|
|
103
|
-
const content = body ? `**${title}**\n${body}` : `**${title}**`;
|
|
104
|
-
await httpPost(this.webhookUrl, JSON.stringify({ content }), {});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ─── Generic webhook channel ──────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
export class WebhookChannel implements NotificationChannel {
|
|
111
|
-
constructor(private url: string, private headers: Record<string, string> = {}) {}
|
|
112
|
-
|
|
113
|
-
async send(title: string, body: string): Promise<void> {
|
|
114
|
-
await httpPost(
|
|
115
|
-
this.url,
|
|
116
|
-
JSON.stringify({ title, body, timestamp: new Date().toISOString() }),
|
|
117
|
-
this.headers
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─── Email channel (optional — requires nodemailer) ───────────────────────────
|
|
123
|
-
|
|
124
|
-
export class EmailChannel implements NotificationChannel {
|
|
125
|
-
constructor(private cfg: {
|
|
126
|
-
smtpHost: string;
|
|
127
|
-
smtpPort: number;
|
|
128
|
-
smtpUser: string;
|
|
129
|
-
smtpPass: string;
|
|
130
|
-
to: string;
|
|
131
|
-
}) {}
|
|
132
|
-
|
|
133
|
-
async send(title: string, body: string): Promise<void> {
|
|
134
|
-
// nodemailer is an optional runtime dependency — load via require to skip
|
|
135
|
-
// compile-time module resolution. Throws a clear message if not installed.
|
|
136
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
-
let nodemailer: any;
|
|
138
|
-
try {
|
|
139
|
-
// @ts-expect-error nodemailer is an optional dependency
|
|
140
|
-
nodemailer = await import("nodemailer");
|
|
141
|
-
} catch {
|
|
142
|
-
throw new Error("nodemailer is not installed. Run: npm install nodemailer");
|
|
143
|
-
}
|
|
144
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
145
|
-
const transport = nodemailer.createTransport({
|
|
146
|
-
host: this.cfg.smtpHost,
|
|
147
|
-
port: this.cfg.smtpPort,
|
|
148
|
-
auth: { user: this.cfg.smtpUser, pass: this.cfg.smtpPass },
|
|
149
|
-
});
|
|
150
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
151
|
-
await transport.sendMail({
|
|
152
|
-
from: this.cfg.smtpUser,
|
|
153
|
-
to: this.cfg.to,
|
|
154
|
-
subject: title,
|
|
155
|
-
text: body,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ─── Notifier ─────────────────────────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
export class Notifier {
|
|
163
|
-
private channels: NotificationChannel[];
|
|
164
|
-
private notifyFlags: Record<NotifyEvent, boolean>;
|
|
165
|
-
|
|
166
|
-
constructor(
|
|
167
|
-
channels: NotificationChannel[],
|
|
168
|
-
flags: Partial<Record<NotifyEvent, boolean>> = {}
|
|
169
|
-
) {
|
|
170
|
-
this.channels = channels;
|
|
171
|
-
this.notifyFlags = {
|
|
172
|
-
hire_request: flags.hire_request ?? true,
|
|
173
|
-
hire_accepted: flags.hire_accepted ?? true,
|
|
174
|
-
hire_rejected: flags.hire_rejected ?? true,
|
|
175
|
-
delivery: flags.delivery ?? true,
|
|
176
|
-
dispute: flags.dispute ?? true,
|
|
177
|
-
channel_challenge: flags.channel_challenge ?? true,
|
|
178
|
-
low_balance: flags.low_balance ?? true,
|
|
179
|
-
daemon_started: true,
|
|
180
|
-
daemon_stopped: true,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
isEnabled(): boolean {
|
|
185
|
-
return this.channels.length > 0;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async send(event: NotifyEvent, title: string, body: string): Promise<void> {
|
|
189
|
-
if (!this.notifyFlags[event]) return;
|
|
190
|
-
await Promise.all(
|
|
191
|
-
this.channels.map(async (ch) => {
|
|
192
|
-
try {
|
|
193
|
-
await ch.send(title, body);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
process.stderr.write(`[notify] Channel send failed: ${err}\n`);
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async notifyHireRequest(hireId: string, hirerAddress: string, priceEth: string, capability: string): Promise<void> {
|
|
202
|
-
const short = hirerAddress.slice(0, 10);
|
|
203
|
-
await this.send("hire_request", "Hire Request", [
|
|
204
|
-
`ID: ${hireId}`,
|
|
205
|
-
`From: ${short}...`,
|
|
206
|
-
`Capability: ${capability || "unspecified"}`,
|
|
207
|
-
`Price: ${priceEth} ETH`,
|
|
208
|
-
``,
|
|
209
|
-
`Approve: arc402 daemon approve ${hireId}`,
|
|
210
|
-
`Reject: arc402 daemon reject ${hireId}`,
|
|
211
|
-
].join("\n"));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async notifyHireAccepted(hireId: string, agreementId: string): Promise<void> {
|
|
215
|
-
await this.send("hire_accepted", "Hire Accepted",
|
|
216
|
-
`ID: ${hireId}\nAgreement: ${agreementId}`
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async notifyHireRejected(hireId: string, reason: string): Promise<void> {
|
|
221
|
-
await this.send("hire_rejected", "Hire Rejected",
|
|
222
|
-
`ID: ${hireId}\nReason: ${reason}`
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async notifyDelivery(agreementId: string, deliveryHash: string, userOpHash: string): Promise<void> {
|
|
227
|
-
await this.send("delivery", "Delivery Submitted", [
|
|
228
|
-
`Agreement: ${agreementId}`,
|
|
229
|
-
`Delivery hash: ${deliveryHash.slice(0, 16)}...`,
|
|
230
|
-
`UserOp: ${userOpHash.slice(0, 16)}...`,
|
|
231
|
-
].join("\n"));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async notifyDispute(agreementId: string, raisedBy: string): Promise<void> {
|
|
235
|
-
await this.send("dispute", "Dispute Raised",
|
|
236
|
-
`Agreement: ${agreementId}\nBy: ${raisedBy}`
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async notifyChannelChallenge(channelId: string, txHash: string): Promise<void> {
|
|
241
|
-
await this.send("channel_challenge", "Channel Challenged",
|
|
242
|
-
`Channel: ${channelId.slice(0, 16)}...\nTx: ${txHash.slice(0, 16)}...`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async notifyLowBalance(balanceEth: string, thresholdEth: string): Promise<void> {
|
|
247
|
-
await this.send("low_balance", "Low Balance Alert",
|
|
248
|
-
`Current: ${balanceEth} ETH\nThreshold: ${thresholdEth} ETH`
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async notifyStarted(walletAddress: string, subsystems: string[]): Promise<void> {
|
|
253
|
-
await this.send("daemon_started", "ARC-402 Daemon Started",
|
|
254
|
-
`Wallet: ${walletAddress}\nSubsystems: ${subsystems.join(", ")}`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async notifyStopped(): Promise<void> {
|
|
259
|
-
await this.send("daemon_stopped", "ARC-402 Daemon Stopped", "");
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
export function buildNotifier(config: DaemonConfig): Notifier {
|
|
266
|
-
const notif = config.notifications;
|
|
267
|
-
const channels: NotificationChannel[] = [];
|
|
268
|
-
|
|
269
|
-
if (notif.telegram_bot_token && notif.telegram_chat_id) {
|
|
270
|
-
channels.push(new TelegramChannel(notif.telegram_bot_token, notif.telegram_chat_id));
|
|
271
|
-
}
|
|
272
|
-
if (notif.discord?.webhook_url) {
|
|
273
|
-
channels.push(new DiscordChannel(notif.discord.webhook_url));
|
|
274
|
-
}
|
|
275
|
-
if (notif.webhook?.url) {
|
|
276
|
-
channels.push(new WebhookChannel(notif.webhook.url, notif.webhook.headers ?? {}));
|
|
277
|
-
}
|
|
278
|
-
if (notif.email?.smtp_host && notif.email?.smtp_user && notif.email?.to) {
|
|
279
|
-
channels.push(new EmailChannel({
|
|
280
|
-
smtpHost: notif.email.smtp_host,
|
|
281
|
-
smtpPort: notif.email.smtp_port,
|
|
282
|
-
smtpUser: notif.email.smtp_user,
|
|
283
|
-
smtpPass: notif.email.smtp_pass,
|
|
284
|
-
to: notif.email.to,
|
|
285
|
-
}));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return new Notifier(channels, {
|
|
289
|
-
hire_request: notif.notify_on_hire_request,
|
|
290
|
-
hire_accepted: notif.notify_on_hire_accepted,
|
|
291
|
-
hire_rejected: notif.notify_on_hire_rejected,
|
|
292
|
-
delivery: notif.notify_on_delivery,
|
|
293
|
-
dispute: notif.notify_on_dispute,
|
|
294
|
-
channel_challenge: notif.notify_on_channel_challenge,
|
|
295
|
-
low_balance: notif.notify_on_low_balance,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ARC-402 Token Usage Metering
|
|
3
|
-
*
|
|
4
|
-
* Tracks LLM token consumption per agreement. The worker writes a usage report
|
|
5
|
-
* file during execution; the daemon reads it after delivery and includes the
|
|
6
|
-
* data in the execution receipt.
|
|
7
|
-
*
|
|
8
|
-
* Architecture:
|
|
9
|
-
* - Before execution: daemon creates a usage report path for the agreement
|
|
10
|
-
* - During execution: worker appends usage entries as it calls LLM APIs
|
|
11
|
-
* - After execution: daemon reads the report, aggregates totals, includes in receipt
|
|
12
|
-
*
|
|
13
|
-
* The worker writes entries in JSON Lines format to:
|
|
14
|
-
* /workroom/jobs/agreement-<id>/token-usage.jsonl
|
|
15
|
-
*
|
|
16
|
-
* Each line:
|
|
17
|
-
* {"model":"claude-sonnet-4-6","provider":"anthropic","input":1200,"output":450,"ts":"..."}
|
|
18
|
-
*
|
|
19
|
-
* This is non-invasive — no proxy, no network interception. The worker
|
|
20
|
-
* is responsible for reporting its own usage. OpenClaw agents can do this
|
|
21
|
-
* natively via session metadata.
|
|
22
|
-
*/
|
|
23
|
-
import * as fs from "fs";
|
|
24
|
-
import * as path from "path";
|
|
25
|
-
import * as os from "os";
|
|
26
|
-
|
|
27
|
-
const ARC402_DIR = path.join(os.homedir(), ".arc402");
|
|
28
|
-
const JOBS_DIR = path.join(ARC402_DIR, "jobs");
|
|
29
|
-
|
|
30
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
export interface TokenUsageEntry {
|
|
33
|
-
model: string;
|
|
34
|
-
provider: string;
|
|
35
|
-
input: number;
|
|
36
|
-
output: number;
|
|
37
|
-
ts: string;
|
|
38
|
-
cost_usd?: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface AggregatedTokenUsage {
|
|
42
|
-
total_input: number;
|
|
43
|
-
total_output: number;
|
|
44
|
-
total_tokens: number;
|
|
45
|
-
estimated_cost_usd: number;
|
|
46
|
-
models_used: string[];
|
|
47
|
-
entries: number;
|
|
48
|
-
by_model: Record<string, {
|
|
49
|
-
input: number;
|
|
50
|
-
output: number;
|
|
51
|
-
calls: number;
|
|
52
|
-
cost_usd: number;
|
|
53
|
-
}>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ─── Known model pricing (USD per 1M tokens, as of 2026-03) ──────────────────
|
|
57
|
-
|
|
58
|
-
const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
59
|
-
// Anthropic
|
|
60
|
-
"claude-opus-4-6": { input: 15.0, output: 75.0 },
|
|
61
|
-
"claude-sonnet-4-6": { input: 3.0, output: 15.0 },
|
|
62
|
-
"claude-haiku-4-5": { input: 0.8, output: 4.0 },
|
|
63
|
-
// OpenAI
|
|
64
|
-
"gpt-5.4": { input: 2.5, output: 10.0 },
|
|
65
|
-
"gpt-5.3-codex": { input: 2.5, output: 10.0 },
|
|
66
|
-
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
67
|
-
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
68
|
-
// Google
|
|
69
|
-
"gemini-2.5-pro": { input: 1.25, output: 10.0 },
|
|
70
|
-
"gemini-2.5-flash": { input: 0.15, output: 0.6 },
|
|
71
|
-
// Defaults
|
|
72
|
-
"default": { input: 2.0, output: 8.0 },
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
function estimateCost(model: string, inputTokens: number, outputTokens: number): number {
|
|
76
|
-
const pricing = MODEL_PRICING[model] ?? MODEL_PRICING["default"];
|
|
77
|
-
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ─── Usage report path ────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get the path where the worker should write token usage for an agreement.
|
|
84
|
-
* The daemon sets this as an env var before calling exec_command.
|
|
85
|
-
*/
|
|
86
|
-
export function getUsageReportPath(agreementId: string): string {
|
|
87
|
-
return path.join(JOBS_DIR, `agreement-${agreementId}`, "token-usage.jsonl");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Create the parent directory and return the path.
|
|
92
|
-
* Called by the daemon before spawning the worker.
|
|
93
|
-
*/
|
|
94
|
-
export function prepareUsageReport(agreementId: string): string {
|
|
95
|
-
const reportPath = getUsageReportPath(agreementId);
|
|
96
|
-
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
97
|
-
return reportPath;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ─── Read and aggregate ───────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Read the token usage report for an agreement and aggregate totals.
|
|
104
|
-
* Returns null if no report exists.
|
|
105
|
-
*/
|
|
106
|
-
export function readUsageReport(agreementId: string): AggregatedTokenUsage | null {
|
|
107
|
-
const reportPath = getUsageReportPath(agreementId);
|
|
108
|
-
|
|
109
|
-
if (!fs.existsSync(reportPath)) return null;
|
|
110
|
-
|
|
111
|
-
const content = fs.readFileSync(reportPath, "utf-8").trim();
|
|
112
|
-
if (!content) return null;
|
|
113
|
-
|
|
114
|
-
const lines = content.split("\n").filter(Boolean);
|
|
115
|
-
const entries: TokenUsageEntry[] = [];
|
|
116
|
-
|
|
117
|
-
for (const line of lines) {
|
|
118
|
-
try {
|
|
119
|
-
entries.push(JSON.parse(line) as TokenUsageEntry);
|
|
120
|
-
} catch {
|
|
121
|
-
// Skip malformed lines
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (entries.length === 0) return null;
|
|
126
|
-
|
|
127
|
-
// Aggregate
|
|
128
|
-
const byModel: Record<string, { input: number; output: number; calls: number; cost_usd: number }> = {};
|
|
129
|
-
let totalInput = 0;
|
|
130
|
-
let totalOutput = 0;
|
|
131
|
-
let totalCost = 0;
|
|
132
|
-
|
|
133
|
-
for (const entry of entries) {
|
|
134
|
-
totalInput += entry.input;
|
|
135
|
-
totalOutput += entry.output;
|
|
136
|
-
|
|
137
|
-
const model = entry.model || "unknown";
|
|
138
|
-
if (!byModel[model]) {
|
|
139
|
-
byModel[model] = { input: 0, output: 0, calls: 0, cost_usd: 0 };
|
|
140
|
-
}
|
|
141
|
-
byModel[model].input += entry.input;
|
|
142
|
-
byModel[model].output += entry.output;
|
|
143
|
-
byModel[model].calls += 1;
|
|
144
|
-
|
|
145
|
-
const cost = entry.cost_usd ?? estimateCost(model, entry.input, entry.output);
|
|
146
|
-
byModel[model].cost_usd += cost;
|
|
147
|
-
totalCost += cost;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
total_input: totalInput,
|
|
152
|
-
total_output: totalOutput,
|
|
153
|
-
total_tokens: totalInput + totalOutput,
|
|
154
|
-
estimated_cost_usd: Math.round(totalCost * 10000) / 10000, // 4 decimal places
|
|
155
|
-
models_used: Object.keys(byModel),
|
|
156
|
-
entries: entries.length,
|
|
157
|
-
by_model: byModel,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ─── CLI display helper ───────────────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Format token usage for CLI display.
|
|
165
|
-
*/
|
|
166
|
-
export function formatUsageReport(usage: AggregatedTokenUsage): string {
|
|
167
|
-
const lines: string[] = [];
|
|
168
|
-
lines.push(`Token Usage`);
|
|
169
|
-
lines.push(`───────────`);
|
|
170
|
-
lines.push(`Total tokens: ${usage.total_tokens.toLocaleString()} (${usage.total_input.toLocaleString()} in / ${usage.total_output.toLocaleString()} out)`);
|
|
171
|
-
lines.push(`Est. cost: $${usage.estimated_cost_usd.toFixed(4)}`);
|
|
172
|
-
lines.push(`LLM calls: ${usage.entries}`);
|
|
173
|
-
lines.push(``);
|
|
174
|
-
|
|
175
|
-
if (Object.keys(usage.by_model).length > 1) {
|
|
176
|
-
lines.push(`By model:`);
|
|
177
|
-
for (const [model, data] of Object.entries(usage.by_model)) {
|
|
178
|
-
lines.push(` ${model.padEnd(24)} ${data.calls} calls ${(data.input + data.output).toLocaleString()} tokens $${data.cost_usd.toFixed(4)}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return lines.join("\n");
|
|
183
|
-
}
|