agent-operator 0.1.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 +171 -0
- package/dist/cli/add-wallet.d.ts +1 -0
- package/dist/cli/add-wallet.js +15 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +200 -0
- package/dist/cli/status.d.ts +1 -0
- package/dist/cli/status.js +25 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +57 -0
- package/dist/connectors/base.d.ts +12 -0
- package/dist/connectors/base.js +119 -0
- package/dist/connectors/solana.d.ts +12 -0
- package/dist/connectors/solana.js +91 -0
- package/dist/connectors/tempo.d.ts +12 -0
- package/dist/connectors/tempo.js +102 -0
- package/dist/connectors/types.d.ts +28 -0
- package/dist/connectors/types.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/notifications/engine.d.ts +11 -0
- package/dist/notifications/engine.js +64 -0
- package/dist/notifications/telegram.d.ts +21 -0
- package/dist/notifications/telegram.js +146 -0
- package/dist/notifications/types.d.ts +32 -0
- package/dist/notifications/types.js +1 -0
- package/dist/operator.d.ts +26 -0
- package/dist/operator.js +206 -0
- package/dist/policies/daily-limit.d.ts +1 -0
- package/dist/policies/daily-limit.js +15 -0
- package/dist/policies/engine.d.ts +16 -0
- package/dist/policies/engine.js +24 -0
- package/dist/policies/high-value-gate.d.ts +2 -0
- package/dist/policies/high-value-gate.js +28 -0
- package/dist/policies/low-balance.d.ts +3 -0
- package/dist/policies/low-balance.js +23 -0
- package/dist/policies/new-counterparty.d.ts +2 -0
- package/dist/policies/new-counterparty.js +29 -0
- package/dist/policies/session-cap.d.ts +1 -0
- package/dist/policies/session-cap.js +11 -0
- package/dist/policies/types.d.ts +18 -0
- package/dist/policies/types.js +20 -0
- package/dist/spend/tracker.d.ts +17 -0
- package/dist/spend/tracker.js +101 -0
- package/package.json +55 -0
package/dist/operator.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { NotificationEngine } from "./notifications/engine.js";
|
|
2
|
+
import { TelegramProvider } from "./notifications/telegram.js";
|
|
3
|
+
import { runPolicyChecks } from "./policies/engine.js";
|
|
4
|
+
import { PolicyDeniedError } from "./policies/types.js";
|
|
5
|
+
import { recordSpend, getSpendSummary } from "./spend/tracker.js";
|
|
6
|
+
import { TempoConnector } from "./connectors/tempo.js";
|
|
7
|
+
import { BaseConnector } from "./connectors/base.js";
|
|
8
|
+
import { SolanaConnector } from "./connectors/solana.js";
|
|
9
|
+
export class Operator {
|
|
10
|
+
config;
|
|
11
|
+
connectors = new Map();
|
|
12
|
+
notifications;
|
|
13
|
+
dailySummaryInterval = null;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.notifications = new NotificationEngine();
|
|
17
|
+
this.initConnectors();
|
|
18
|
+
this.initNotifications();
|
|
19
|
+
}
|
|
20
|
+
initConnectors() {
|
|
21
|
+
const connectorInstances = {
|
|
22
|
+
tempo: new TempoConnector(),
|
|
23
|
+
base: new BaseConnector(),
|
|
24
|
+
solana: new SolanaConnector(),
|
|
25
|
+
};
|
|
26
|
+
// register only the connectors that wallets actually use
|
|
27
|
+
const neededChains = new Set();
|
|
28
|
+
for (const wallet of Object.values(this.config.wallets)) {
|
|
29
|
+
for (const chain of wallet.chains) {
|
|
30
|
+
neededChains.add(chain);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const chain of neededChains) {
|
|
34
|
+
const connector = connectorInstances[chain];
|
|
35
|
+
if (!connector) {
|
|
36
|
+
throw new Error(`No built-in connector for chain '${chain}'`);
|
|
37
|
+
}
|
|
38
|
+
this.connectors.set(chain, connector);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
initNotifications() {
|
|
42
|
+
for (const [walletName, wallet] of Object.entries(this.config.wallets)) {
|
|
43
|
+
const provider = this.createProvider(wallet);
|
|
44
|
+
this.notifications.registerProvider(walletName, provider);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
createProvider(wallet) {
|
|
48
|
+
const { provider, ...rest } = wallet.notifications;
|
|
49
|
+
switch (provider) {
|
|
50
|
+
case "telegram":
|
|
51
|
+
return new TelegramProvider({
|
|
52
|
+
botToken: rest.botToken,
|
|
53
|
+
chatId: rest.chatId,
|
|
54
|
+
approvalTimeout: rest.approvalTimeout,
|
|
55
|
+
});
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`Unknown notification provider '${provider}'`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async start() {
|
|
61
|
+
await this.notifications.startAll();
|
|
62
|
+
this.startDailySummary();
|
|
63
|
+
}
|
|
64
|
+
async shutdown() {
|
|
65
|
+
if (this.dailySummaryInterval) {
|
|
66
|
+
clearInterval(this.dailySummaryInterval);
|
|
67
|
+
this.dailySummaryInterval = null;
|
|
68
|
+
}
|
|
69
|
+
await this.notifications.stopAll();
|
|
70
|
+
}
|
|
71
|
+
async pay(walletName, params) {
|
|
72
|
+
const wallet = this.getWalletConfig(walletName);
|
|
73
|
+
const connector = this.getConnector(walletName, params.chain);
|
|
74
|
+
const provider = this.notifications.getProvider(walletName);
|
|
75
|
+
const alerts = wallet.notifications.alerts;
|
|
76
|
+
// run policy checks
|
|
77
|
+
try {
|
|
78
|
+
await runPolicyChecks({
|
|
79
|
+
walletName,
|
|
80
|
+
walletId: wallet.walletId,
|
|
81
|
+
service: params.service,
|
|
82
|
+
chain: params.chain,
|
|
83
|
+
maxBudget: params.maxBudget,
|
|
84
|
+
policies: wallet.policies,
|
|
85
|
+
alerts,
|
|
86
|
+
connector,
|
|
87
|
+
provider,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err instanceof PolicyDeniedError && alerts.policyDenied) {
|
|
92
|
+
await this.notifications.notify(walletName, {
|
|
93
|
+
type: "policy_denied",
|
|
94
|
+
walletName,
|
|
95
|
+
chain: params.chain,
|
|
96
|
+
data: {
|
|
97
|
+
reason: err.reason,
|
|
98
|
+
details: err.details,
|
|
99
|
+
},
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
}, alerts).catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
// notify session opened
|
|
106
|
+
if (alerts.sessionOpened) {
|
|
107
|
+
await this.notifications.notify(walletName, {
|
|
108
|
+
type: "session_opened",
|
|
109
|
+
walletName,
|
|
110
|
+
chain: params.chain,
|
|
111
|
+
data: {
|
|
112
|
+
service: params.service,
|
|
113
|
+
maxBudget: params.maxBudget,
|
|
114
|
+
},
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
}, alerts).catch(() => { });
|
|
117
|
+
}
|
|
118
|
+
// open session via connector
|
|
119
|
+
const session = await connector.openSession({
|
|
120
|
+
service: params.service,
|
|
121
|
+
maxBudget: params.maxBudget,
|
|
122
|
+
walletId: wallet.walletId,
|
|
123
|
+
});
|
|
124
|
+
// close session and record spend
|
|
125
|
+
const result = await connector.closeSession(session.sessionId);
|
|
126
|
+
recordSpend(walletName, params.service, params.chain, result.totalSpent);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
async getBalance(walletName, chain) {
|
|
130
|
+
const wallet = this.getWalletConfig(walletName);
|
|
131
|
+
const connector = this.getConnector(walletName, chain);
|
|
132
|
+
return connector.getBalance(wallet.walletId);
|
|
133
|
+
}
|
|
134
|
+
async closeSession(walletName, sessionId) {
|
|
135
|
+
// find the connector that has this session
|
|
136
|
+
// try all connectors for this wallet
|
|
137
|
+
const wallet = this.getWalletConfig(walletName);
|
|
138
|
+
for (const chain of wallet.chains) {
|
|
139
|
+
const connector = this.connectors.get(chain);
|
|
140
|
+
if (!connector)
|
|
141
|
+
continue;
|
|
142
|
+
try {
|
|
143
|
+
const result = await connector.closeSession(sessionId);
|
|
144
|
+
recordSpend(walletName, "", chain, result.totalSpent);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// session not found on this connector, try next
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Session '${sessionId}' not found on any connector for wallet '${walletName}'`);
|
|
152
|
+
}
|
|
153
|
+
getWalletNames() {
|
|
154
|
+
return Object.keys(this.config.wallets);
|
|
155
|
+
}
|
|
156
|
+
getWalletConfig(walletName) {
|
|
157
|
+
const wallet = this.config.wallets[walletName];
|
|
158
|
+
if (!wallet) {
|
|
159
|
+
throw new Error(`Wallet '${walletName}' not found in config. Available: ${Object.keys(this.config.wallets).join(", ")}`);
|
|
160
|
+
}
|
|
161
|
+
return wallet;
|
|
162
|
+
}
|
|
163
|
+
getConnector(walletName, chain) {
|
|
164
|
+
const wallet = this.getWalletConfig(walletName);
|
|
165
|
+
if (!wallet.chains.includes(chain)) {
|
|
166
|
+
throw new Error(`Wallet '${walletName}' is not configured for chain '${chain}'. Available: ${wallet.chains.join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
const connector = this.connectors.get(chain);
|
|
169
|
+
if (!connector) {
|
|
170
|
+
throw new Error(`No connector found for chain '${chain}'`);
|
|
171
|
+
}
|
|
172
|
+
return connector;
|
|
173
|
+
}
|
|
174
|
+
startDailySummary() {
|
|
175
|
+
// fire daily summary every 24h
|
|
176
|
+
this.dailySummaryInterval = setInterval(async () => {
|
|
177
|
+
for (const [walletName, wallet] of Object.entries(this.config.wallets)) {
|
|
178
|
+
const alerts = wallet.notifications.alerts;
|
|
179
|
+
if (!alerts.dailySummary)
|
|
180
|
+
continue;
|
|
181
|
+
const summary = getSpendSummary(walletName);
|
|
182
|
+
const balances = {};
|
|
183
|
+
for (const chain of wallet.chains) {
|
|
184
|
+
try {
|
|
185
|
+
const balance = await this.getBalance(walletName, chain);
|
|
186
|
+
balances[chain] = balance.amount;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
balances[chain] = -1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
await this.notifications.notify(walletName, {
|
|
193
|
+
type: "daily_summary",
|
|
194
|
+
walletName,
|
|
195
|
+
data: {
|
|
196
|
+
spent: summary.spent,
|
|
197
|
+
dailyLimit: wallet.policies.dailyLimit,
|
|
198
|
+
sessionCount: summary.sessionCount,
|
|
199
|
+
balance: balances,
|
|
200
|
+
},
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
}, alerts).catch(() => { });
|
|
203
|
+
}
|
|
204
|
+
}, 24 * 60 * 60 * 1000);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function checkDailyLimit(walletName: string, maxBudget: number, dailyLimit?: number): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getTodaySpend } from "../spend/tracker.js";
|
|
2
|
+
import { PolicyDeniedError } from "./types.js";
|
|
3
|
+
export function checkDailyLimit(walletName, maxBudget, dailyLimit) {
|
|
4
|
+
if (dailyLimit === undefined)
|
|
5
|
+
return;
|
|
6
|
+
const spent = getTodaySpend(walletName);
|
|
7
|
+
if (spent + maxBudget > dailyLimit) {
|
|
8
|
+
throw new PolicyDeniedError("daily_limit", {
|
|
9
|
+
attempted: maxBudget,
|
|
10
|
+
dailyLimit,
|
|
11
|
+
spent,
|
|
12
|
+
remaining: Math.max(0, dailyLimit - spent),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PoliciesConfig } from "./types.js";
|
|
2
|
+
import type { Connector } from "../connectors/types.js";
|
|
3
|
+
import type { NotificationProvider } from "../notifications/types.js";
|
|
4
|
+
import type { AlertsConfig } from "../notifications/types.js";
|
|
5
|
+
export interface PolicyCheckParams {
|
|
6
|
+
walletName: string;
|
|
7
|
+
walletId: string;
|
|
8
|
+
service: string;
|
|
9
|
+
chain: string;
|
|
10
|
+
maxBudget: number;
|
|
11
|
+
policies: PoliciesConfig;
|
|
12
|
+
alerts: AlertsConfig;
|
|
13
|
+
connector: Connector;
|
|
14
|
+
provider: NotificationProvider;
|
|
15
|
+
}
|
|
16
|
+
export declare function runPolicyChecks(params: PolicyCheckParams): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { checkSessionCap } from "./session-cap.js";
|
|
2
|
+
import { checkDailyLimit } from "./daily-limit.js";
|
|
3
|
+
import { checkHighValue } from "./high-value-gate.js";
|
|
4
|
+
import { checkNewCounterparty } from "./new-counterparty.js";
|
|
5
|
+
import { checkLowBalance } from "./low-balance.js";
|
|
6
|
+
export async function runPolicyChecks(params) {
|
|
7
|
+
const { walletName, walletId, service, chain, maxBudget, policies, alerts, connector, provider } = params;
|
|
8
|
+
// 1. session cap — fast, synchronous
|
|
9
|
+
checkSessionCap(maxBudget, policies.sessionCap);
|
|
10
|
+
// 2. daily limit — reads local spend file
|
|
11
|
+
checkDailyLimit(walletName, maxBudget, policies.dailyLimit);
|
|
12
|
+
// 3. high value gate — may block for approval
|
|
13
|
+
if (alerts.highValueTx) {
|
|
14
|
+
await checkHighValue(walletName, chain, service, maxBudget, policies.highValueThreshold, provider);
|
|
15
|
+
}
|
|
16
|
+
// 4. new counterparty hold — may block for approval
|
|
17
|
+
if (alerts.newCounterparty) {
|
|
18
|
+
await checkNewCounterparty(walletName, chain, service, maxBudget, policies.newCounterpartyHold, provider);
|
|
19
|
+
}
|
|
20
|
+
// 5. low balance — non-blocking alert
|
|
21
|
+
if (alerts.lowBalance) {
|
|
22
|
+
await checkLowBalance(walletName, walletId, chain, policies.lowBalanceThreshold, connector, provider);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { PolicyDeniedError } from "./types.js";
|
|
3
|
+
export async function checkHighValue(walletName, chain, service, maxBudget, highValueThreshold, provider) {
|
|
4
|
+
if (highValueThreshold === undefined)
|
|
5
|
+
return;
|
|
6
|
+
if (maxBudget <= highValueThreshold)
|
|
7
|
+
return;
|
|
8
|
+
const approvalId = randomUUID();
|
|
9
|
+
const result = await provider.sendWithApproval({
|
|
10
|
+
type: "high_value_tx",
|
|
11
|
+
walletName,
|
|
12
|
+
chain,
|
|
13
|
+
approvalId,
|
|
14
|
+
data: {
|
|
15
|
+
service,
|
|
16
|
+
amount: maxBudget,
|
|
17
|
+
threshold: highValueThreshold,
|
|
18
|
+
},
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
});
|
|
21
|
+
if (!result.approved) {
|
|
22
|
+
throw new PolicyDeniedError("high_value_rejected", {
|
|
23
|
+
attempted: maxBudget,
|
|
24
|
+
highValueThreshold,
|
|
25
|
+
service,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Connector } from "../connectors/types.js";
|
|
2
|
+
import type { NotificationProvider } from "../notifications/types.js";
|
|
3
|
+
export declare function checkLowBalance(walletName: string, walletId: string, chain: string, lowBalanceThreshold: number | undefined, connector: Connector, provider: NotificationProvider): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function checkLowBalance(walletName, walletId, chain, lowBalanceThreshold, connector, provider) {
|
|
2
|
+
if (lowBalanceThreshold === undefined)
|
|
3
|
+
return;
|
|
4
|
+
try {
|
|
5
|
+
const balance = await connector.getBalance(walletId);
|
|
6
|
+
if (balance.amount < lowBalanceThreshold) {
|
|
7
|
+
await provider.send({
|
|
8
|
+
type: "low_balance",
|
|
9
|
+
walletName,
|
|
10
|
+
chain,
|
|
11
|
+
data: {
|
|
12
|
+
balance: balance.amount,
|
|
13
|
+
currency: balance.currency,
|
|
14
|
+
threshold: lowBalanceThreshold,
|
|
15
|
+
},
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
}).catch(() => { });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// non-blocking — don't fail the payment over a balance check
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { NotificationProvider } from "../notifications/types.js";
|
|
2
|
+
export declare function checkNewCounterparty(walletName: string, chain: string, service: string, maxBudget: number, newCounterpartyHold: boolean | undefined, provider: NotificationProvider): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getCounterparties, addCounterparty } from "../spend/tracker.js";
|
|
3
|
+
import { PolicyDeniedError } from "./types.js";
|
|
4
|
+
export async function checkNewCounterparty(walletName, chain, service, maxBudget, newCounterpartyHold, provider) {
|
|
5
|
+
if (!newCounterpartyHold)
|
|
6
|
+
return;
|
|
7
|
+
const known = getCounterparties(walletName);
|
|
8
|
+
if (known.includes(service))
|
|
9
|
+
return;
|
|
10
|
+
const approvalId = randomUUID();
|
|
11
|
+
const result = await provider.sendWithApproval({
|
|
12
|
+
type: "new_counterparty",
|
|
13
|
+
walletName,
|
|
14
|
+
chain,
|
|
15
|
+
approvalId,
|
|
16
|
+
data: {
|
|
17
|
+
service,
|
|
18
|
+
maxBudget,
|
|
19
|
+
},
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
});
|
|
22
|
+
if (!result.approved) {
|
|
23
|
+
throw new PolicyDeniedError("new_counterparty_rejected", {
|
|
24
|
+
service,
|
|
25
|
+
maxBudget,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
addCounterparty(walletName, service);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function checkSessionCap(maxBudget: number, sessionCap?: number): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PolicyDeniedError } from "./types.js";
|
|
2
|
+
export function checkSessionCap(maxBudget, sessionCap) {
|
|
3
|
+
if (sessionCap === undefined)
|
|
4
|
+
return;
|
|
5
|
+
if (maxBudget > sessionCap) {
|
|
6
|
+
throw new PolicyDeniedError("session_cap", {
|
|
7
|
+
attempted: maxBudget,
|
|
8
|
+
sessionCap,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PoliciesConfig {
|
|
2
|
+
dailyLimit?: number;
|
|
3
|
+
sessionCap?: number;
|
|
4
|
+
newCounterpartyHold?: boolean;
|
|
5
|
+
lowBalanceThreshold?: number;
|
|
6
|
+
highValueThreshold?: number;
|
|
7
|
+
}
|
|
8
|
+
export type PolicyDenialReason = "daily_limit" | "session_cap" | "high_value_rejected" | "new_counterparty_rejected";
|
|
9
|
+
export declare class PolicyDeniedError extends Error {
|
|
10
|
+
readonly reason: PolicyDenialReason;
|
|
11
|
+
readonly details: Record<string, unknown>;
|
|
12
|
+
constructor(reason: PolicyDenialReason, details: Record<string, unknown>);
|
|
13
|
+
}
|
|
14
|
+
export declare class ConnectorError extends Error {
|
|
15
|
+
readonly chain: string;
|
|
16
|
+
readonly cause?: Error | undefined;
|
|
17
|
+
constructor(message: string, chain: string, cause?: Error | undefined);
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class PolicyDeniedError extends Error {
|
|
2
|
+
reason;
|
|
3
|
+
details;
|
|
4
|
+
constructor(reason, details) {
|
|
5
|
+
super(`Policy denied: ${reason}`);
|
|
6
|
+
this.reason = reason;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = "PolicyDeniedError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class ConnectorError extends Error {
|
|
12
|
+
chain;
|
|
13
|
+
cause;
|
|
14
|
+
constructor(message, chain, cause) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.chain = chain;
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
this.name = "ConnectorError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface SessionEntry {
|
|
2
|
+
service: string;
|
|
3
|
+
chain: string;
|
|
4
|
+
amount: number;
|
|
5
|
+
ts: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getTodaySpend(walletName: string): number;
|
|
8
|
+
export declare function getTodaySessions(walletName: string): SessionEntry[];
|
|
9
|
+
export declare function recordSpend(walletName: string, service: string, chain: string, amount: number): void;
|
|
10
|
+
export declare function getCounterparties(walletName: string): string[];
|
|
11
|
+
export declare function addCounterparty(walletName: string, service: string): void;
|
|
12
|
+
export declare function getSpendSummary(walletName: string): {
|
|
13
|
+
spent: number;
|
|
14
|
+
sessionCount: number;
|
|
15
|
+
sessions: SessionEntry[];
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const SPEND_DIR = join(homedir(), ".agent-operator");
|
|
5
|
+
const SPEND_FILE = join(SPEND_DIR, "spend.json");
|
|
6
|
+
function today() {
|
|
7
|
+
return new Date().toISOString().split("T")[0];
|
|
8
|
+
}
|
|
9
|
+
function ensureDir() {
|
|
10
|
+
if (!existsSync(SPEND_DIR)) {
|
|
11
|
+
mkdirSync(SPEND_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function readSpendFile() {
|
|
15
|
+
ensureDir();
|
|
16
|
+
if (!existsSync(SPEND_FILE)) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(SPEND_FILE, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeSpendFile(data) {
|
|
27
|
+
ensureDir();
|
|
28
|
+
writeFileSync(SPEND_FILE, JSON.stringify(data, null, 2));
|
|
29
|
+
}
|
|
30
|
+
function ensureWallet(data, walletName) {
|
|
31
|
+
if (!data[walletName]) {
|
|
32
|
+
data[walletName] = { days: {}, counterparties: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function ensureToday(data, walletName) {
|
|
36
|
+
const date = today();
|
|
37
|
+
if (!data[walletName].days[date]) {
|
|
38
|
+
data[walletName].days[date] = { spent: 0, sessions: [] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function cleanOldDates(data, walletName) {
|
|
42
|
+
const date = today();
|
|
43
|
+
const days = data[walletName].days;
|
|
44
|
+
for (const key of Object.keys(days)) {
|
|
45
|
+
if (key !== date) {
|
|
46
|
+
delete days[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function getTodaySpend(walletName) {
|
|
51
|
+
const data = readSpendFile();
|
|
52
|
+
ensureWallet(data, walletName);
|
|
53
|
+
const date = today();
|
|
54
|
+
return data[walletName].days[date]?.spent ?? 0;
|
|
55
|
+
}
|
|
56
|
+
export function getTodaySessions(walletName) {
|
|
57
|
+
const data = readSpendFile();
|
|
58
|
+
ensureWallet(data, walletName);
|
|
59
|
+
const date = today();
|
|
60
|
+
return data[walletName].days[date]?.sessions ?? [];
|
|
61
|
+
}
|
|
62
|
+
export function recordSpend(walletName, service, chain, amount) {
|
|
63
|
+
const data = readSpendFile();
|
|
64
|
+
ensureWallet(data, walletName);
|
|
65
|
+
cleanOldDates(data, walletName);
|
|
66
|
+
ensureToday(data, walletName);
|
|
67
|
+
const date = today();
|
|
68
|
+
const dayData = data[walletName].days[date];
|
|
69
|
+
dayData.spent += amount;
|
|
70
|
+
dayData.sessions.push({
|
|
71
|
+
service,
|
|
72
|
+
chain,
|
|
73
|
+
amount,
|
|
74
|
+
ts: new Date().toISOString(),
|
|
75
|
+
});
|
|
76
|
+
writeSpendFile(data);
|
|
77
|
+
}
|
|
78
|
+
export function getCounterparties(walletName) {
|
|
79
|
+
const data = readSpendFile();
|
|
80
|
+
ensureWallet(data, walletName);
|
|
81
|
+
return data[walletName].counterparties ?? [];
|
|
82
|
+
}
|
|
83
|
+
export function addCounterparty(walletName, service) {
|
|
84
|
+
const data = readSpendFile();
|
|
85
|
+
ensureWallet(data, walletName);
|
|
86
|
+
if (!data[walletName].counterparties.includes(service)) {
|
|
87
|
+
data[walletName].counterparties.push(service);
|
|
88
|
+
}
|
|
89
|
+
writeSpendFile(data);
|
|
90
|
+
}
|
|
91
|
+
export function getSpendSummary(walletName) {
|
|
92
|
+
const data = readSpendFile();
|
|
93
|
+
ensureWallet(data, walletName);
|
|
94
|
+
const date = today();
|
|
95
|
+
const dayData = data[walletName].days[date];
|
|
96
|
+
return {
|
|
97
|
+
spent: dayData?.spent ?? 0,
|
|
98
|
+
sessionCount: dayData?.sessions.length ?? 0,
|
|
99
|
+
sessions: dayData?.sessions ?? [],
|
|
100
|
+
};
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-operator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in toolkit for governed, observable AI agent payments",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agent-operator": "dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"dev": "tsc --watch",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"ai",
|
|
28
|
+
"agent",
|
|
29
|
+
"payments",
|
|
30
|
+
"mpp",
|
|
31
|
+
"ows",
|
|
32
|
+
"wallet",
|
|
33
|
+
"solana",
|
|
34
|
+
"tempo",
|
|
35
|
+
"base"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/AgentrDev/agent-operator"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@open-wallet-standard/core": "*",
|
|
44
|
+
"@solana/mpp": "^0.2.0",
|
|
45
|
+
"@solana/spl-token": "^0.4.14",
|
|
46
|
+
"@solana/web3.js": "^1.98.4",
|
|
47
|
+
"grammy": "^1.42.0",
|
|
48
|
+
"mppx": "*",
|
|
49
|
+
"prompts": "^2.4.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/prompts": "^2.4.9",
|
|
53
|
+
"typescript": "^5.5.0"
|
|
54
|
+
}
|
|
55
|
+
}
|