spendos 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/.dockerignore +4 -0
- package/.env.example +30 -0
- package/AGENTS.md +212 -0
- package/BOOTSTRAP.md +55 -0
- package/Dockerfile +52 -0
- package/HEARTBEAT.md +7 -0
- package/IDENTITY.md +23 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SOUL.md +202 -0
- package/SUBMISSION.md +128 -0
- package/TOOLS.md +40 -0
- package/USER.md +17 -0
- package/acp-seller/bin/acp.ts +807 -0
- package/acp-seller/config.json +34 -0
- package/acp-seller/package.json +55 -0
- package/acp-seller/src/commands/agent.ts +328 -0
- package/acp-seller/src/commands/bounty.ts +1189 -0
- package/acp-seller/src/commands/deploy.ts +414 -0
- package/acp-seller/src/commands/job.ts +217 -0
- package/acp-seller/src/commands/profile.ts +71 -0
- package/acp-seller/src/commands/resource.ts +91 -0
- package/acp-seller/src/commands/search.ts +327 -0
- package/acp-seller/src/commands/sell.ts +883 -0
- package/acp-seller/src/commands/serve.ts +258 -0
- package/acp-seller/src/commands/setup.ts +399 -0
- package/acp-seller/src/commands/token.ts +88 -0
- package/acp-seller/src/commands/wallet.ts +123 -0
- package/acp-seller/src/lib/api.ts +118 -0
- package/acp-seller/src/lib/auth.ts +291 -0
- package/acp-seller/src/lib/bounty.ts +257 -0
- package/acp-seller/src/lib/client.ts +42 -0
- package/acp-seller/src/lib/config.ts +240 -0
- package/acp-seller/src/lib/open.ts +41 -0
- package/acp-seller/src/lib/openclawCron.ts +138 -0
- package/acp-seller/src/lib/output.ts +104 -0
- package/acp-seller/src/lib/wallet.ts +81 -0
- package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
- package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
- package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
- package/acp-seller/src/seller/runtime/logger.ts +36 -0
- package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
- package/acp-seller/src/seller/runtime/offerings.ts +277 -0
- package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
- package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
- package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
- package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
- package/acp-seller/src/seller/runtime/seller.ts +1041 -0
- package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
- package/acp-seller/src/seller/runtime/startup.ts +270 -0
- package/acp-seller/src/seller/runtime/types.ts +62 -0
- package/acp-seller/tsconfig.json +20 -0
- package/bin/spendos.js +23 -0
- package/contracts/SpendOSAudit.sol +29 -0
- package/dist/mcp-server.mjs +153 -0
- package/jobs/translate.json +7 -0
- package/jobs/tweet-gen.json +7 -0
- package/openclaw.json +41 -0
- package/package.json +49 -0
- package/plugins/spendos-events/index.ts +78 -0
- package/plugins/spendos-events/package.json +14 -0
- package/policies/enforce-bounds.mjs +71 -0
- package/public/index.html +509 -0
- package/public/landing.html +241 -0
- package/railway.json +12 -0
- package/railway.toml +12 -0
- package/scripts/deploy.ts +48 -0
- package/scripts/test-x402-mainnet.ts +30 -0
- package/scripts/xmtp-listener.ts +61 -0
- package/setup.sh +278 -0
- package/skills/spendos/skill.md +26 -0
- package/src/agent.ts +152 -0
- package/src/audit.ts +166 -0
- package/src/governance.ts +367 -0
- package/src/job-registry.ts +306 -0
- package/src/mcp-public.ts +145 -0
- package/src/mcp-server.ts +171 -0
- package/src/opportunity-scanner.ts +138 -0
- package/src/server.ts +870 -0
- package/src/venice-x402.ts +234 -0
- package/src/xmtp.ts +109 -0
- package/src/zerion.ts +58 -0
- package/start.sh +168 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// acp token launch <symbol> <description> [--image <url>]
|
|
3
|
+
// acp token info
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import client from "../lib/client.js";
|
|
7
|
+
import { getMyAgentInfo } from "../lib/wallet.js";
|
|
8
|
+
import * as output from "../lib/output.js";
|
|
9
|
+
|
|
10
|
+
export async function launch(
|
|
11
|
+
symbol: string,
|
|
12
|
+
description: string,
|
|
13
|
+
imageUrl?: string,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
if (!symbol || !description) {
|
|
16
|
+
output.fatal(
|
|
17
|
+
"Usage: acp token launch <symbol> <description> [--image <url>]",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if token already exists
|
|
22
|
+
try {
|
|
23
|
+
const info = await getMyAgentInfo();
|
|
24
|
+
if (info.tokenAddress) {
|
|
25
|
+
const symbol = info.token?.symbol;
|
|
26
|
+
output.output(
|
|
27
|
+
{ alreadyLaunched: true, symbol, tokenAddress: info.tokenAddress },
|
|
28
|
+
() => {
|
|
29
|
+
output.heading("Token Already Launched");
|
|
30
|
+
if (symbol) output.field("Symbol", symbol);
|
|
31
|
+
output.field("Token Address", info.tokenAddress);
|
|
32
|
+
output.log(
|
|
33
|
+
"\n Each agent can only launch one token. Run `acp token info` for details.\n",
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Non-fatal — proceed with launch attempt
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const payload: Record<string, string> = { symbol, description };
|
|
45
|
+
if (imageUrl) payload.imageUrl = imageUrl;
|
|
46
|
+
|
|
47
|
+
const token = await client.post("/acp/me/tokens", payload);
|
|
48
|
+
|
|
49
|
+
output.output(token.data.data, (tokenData) => {
|
|
50
|
+
output.heading("Token Launched");
|
|
51
|
+
output.field("Symbol", tokenData.symbol ?? "");
|
|
52
|
+
output.field("Token Address", tokenData.tokenAddress ?? "");
|
|
53
|
+
output.log("");
|
|
54
|
+
});
|
|
55
|
+
} catch (e) {
|
|
56
|
+
output.fatal(
|
|
57
|
+
`Failed to launch token: ${e instanceof Error ? e.message : String(e)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function info(): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
const agentInfo = await getMyAgentInfo();
|
|
65
|
+
|
|
66
|
+
output.output(agentInfo, (data) => {
|
|
67
|
+
output.heading("Agent Token");
|
|
68
|
+
if (data.tokenAddress) {
|
|
69
|
+
output.field("Name", data.token.name);
|
|
70
|
+
output.field("Symbol", output.formatSymbol(data.token.symbol));
|
|
71
|
+
output.field("Address", data.tokenAddress);
|
|
72
|
+
output.field(
|
|
73
|
+
"URL",
|
|
74
|
+
`https://app.virtuals.io/prototypes/${data.tokenAddress}`,
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
output.log(
|
|
78
|
+
" No token launched yet. Use `acp token launch` to create one.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
output.log("");
|
|
82
|
+
});
|
|
83
|
+
} catch (e) {
|
|
84
|
+
output.fatal(
|
|
85
|
+
`Failed to get token info: ${e instanceof Error ? e.message : String(e)}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// acp wallet address — Get wallet address
|
|
3
|
+
// acp wallet balance — Get token balances
|
|
4
|
+
// acp wallet topup — Get topup URL
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
import { getPaymentUrl } from "../lib/api.js";
|
|
8
|
+
import { getMyAgentInfo } from "../lib/wallet.js";
|
|
9
|
+
import * as output from "../lib/output.js";
|
|
10
|
+
import client from "../lib/client.js";
|
|
11
|
+
|
|
12
|
+
interface WalletBalance {
|
|
13
|
+
network: string;
|
|
14
|
+
symbol: string;
|
|
15
|
+
tokenAddress: string | null;
|
|
16
|
+
tokenBalance: string;
|
|
17
|
+
decimals: number;
|
|
18
|
+
tokenPrices: { currency: string; value: string }[];
|
|
19
|
+
tokenMetadata: {
|
|
20
|
+
decimals: number | null;
|
|
21
|
+
logo: string | null;
|
|
22
|
+
name: string | null;
|
|
23
|
+
symbol: string | null;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatBalance(hexBalance: string, decimals: number): string {
|
|
28
|
+
const raw = BigInt(hexBalance);
|
|
29
|
+
if (raw === 0n) return "0";
|
|
30
|
+
const divisor = 10n ** BigInt(decimals);
|
|
31
|
+
const whole = raw / divisor;
|
|
32
|
+
const remainder = raw % divisor;
|
|
33
|
+
if (remainder === 0n) return whole.toString();
|
|
34
|
+
const fracStr = remainder
|
|
35
|
+
.toString()
|
|
36
|
+
.padStart(decimals, "0")
|
|
37
|
+
.replace(/0+$/, "");
|
|
38
|
+
return `${whole}.${fracStr}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function address(): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
const info = await getMyAgentInfo();
|
|
44
|
+
output.output({ walletAddress: info.walletAddress }, (data) => {
|
|
45
|
+
output.heading("Agent Wallet");
|
|
46
|
+
output.field("Address", data.walletAddress);
|
|
47
|
+
output.log("");
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
output.fatal(
|
|
51
|
+
`Failed to get wallet address: ${e instanceof Error ? e.message : String(e)}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function balance(): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const balances = await client.get<{ data: WalletBalance[] }>(
|
|
59
|
+
"/acp/wallet-balances",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const data = balances.data.data.map((token) => ({
|
|
63
|
+
network: token.network,
|
|
64
|
+
symbol: token.symbol,
|
|
65
|
+
tokenAddress: token.tokenAddress,
|
|
66
|
+
tokenBalance: token.tokenBalance,
|
|
67
|
+
tokenMetadata: token.tokenMetadata,
|
|
68
|
+
decimals: token.decimals,
|
|
69
|
+
tokenPrices: token.tokenPrices,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
output.output(data, (tokens) => {
|
|
73
|
+
output.heading("Wallet Balances");
|
|
74
|
+
if (tokens.length === 0) {
|
|
75
|
+
output.log(" No tokens found.");
|
|
76
|
+
}
|
|
77
|
+
for (const t of tokens) {
|
|
78
|
+
const sym =
|
|
79
|
+
t.tokenMetadata?.symbol ||
|
|
80
|
+
t.symbol ||
|
|
81
|
+
(t.tokenAddress === null ? "ETH" : "???");
|
|
82
|
+
const name =
|
|
83
|
+
t.tokenMetadata?.name || (t.tokenAddress === null ? "Ether" : "");
|
|
84
|
+
const decimals = t.tokenMetadata?.decimals ?? t.decimals ?? 18;
|
|
85
|
+
const bal = formatBalance(t.tokenBalance, decimals);
|
|
86
|
+
const price = t.tokenPrices?.[0]?.value ?? "-";
|
|
87
|
+
output.log(
|
|
88
|
+
` ${sym.padEnd(8)} ${name.padEnd(20)} ${bal.padStart(20)} $${price}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
output.log("");
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {
|
|
94
|
+
output.fatal(
|
|
95
|
+
`Failed to get wallet balance: ${e instanceof Error ? e.message : String(e)}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function topup(): Promise<void> {
|
|
101
|
+
try {
|
|
102
|
+
const info = await getMyAgentInfo();
|
|
103
|
+
const result = await getPaymentUrl();
|
|
104
|
+
|
|
105
|
+
if (!result.success || !result.url) {
|
|
106
|
+
output.fatal("Failed to get topup URL.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
output.output(
|
|
110
|
+
{ url: result.url, walletAddress: info.walletAddress },
|
|
111
|
+
(data) => {
|
|
112
|
+
output.heading("Wallet Topup");
|
|
113
|
+
output.field("Wallet Address", data.walletAddress);
|
|
114
|
+
output.field("Topup URL", data.url);
|
|
115
|
+
output.log("\n Visit the URL above to add funds to your wallet.\n");
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
output.fatal(
|
|
120
|
+
`Failed to get topup URL: ${e instanceof Error ? e.message : String(e)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// ACP API wrappers for job offerings and resources.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import client from "./client.js";
|
|
6
|
+
|
|
7
|
+
export interface PriceV2 {
|
|
8
|
+
type: "fixed" | "percentage";
|
|
9
|
+
value: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface JobOfferingData {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
priceV2: PriceV2;
|
|
16
|
+
slaMinutes: number;
|
|
17
|
+
requiredFunds: boolean;
|
|
18
|
+
requirement: Record<string, any>;
|
|
19
|
+
deliverable: string;
|
|
20
|
+
resources?: Resource[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Resource {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
url: string;
|
|
27
|
+
params?: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentData {
|
|
31
|
+
name: string;
|
|
32
|
+
tokenAddress: string;
|
|
33
|
+
resources: Resource[];
|
|
34
|
+
offerings: JobOfferingData[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CreateJobOfferingResponse {
|
|
38
|
+
success: boolean;
|
|
39
|
+
data?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PaymentUrlResponse {
|
|
43
|
+
url: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createJobOffering(
|
|
47
|
+
offering: JobOfferingData,
|
|
48
|
+
): Promise<{ success: boolean; data?: AgentData }> {
|
|
49
|
+
try {
|
|
50
|
+
const { data } = await client.post(`/acp/job-offerings`, {
|
|
51
|
+
data: offering,
|
|
52
|
+
});
|
|
53
|
+
return { success: true, data };
|
|
54
|
+
} catch (error: any) {
|
|
55
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
56
|
+
console.error(`ACP createJobOffering failed: ${msg}`);
|
|
57
|
+
return { success: false };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function deleteJobOffering(
|
|
62
|
+
offeringName: string,
|
|
63
|
+
): Promise<{ success: boolean }> {
|
|
64
|
+
try {
|
|
65
|
+
await client.delete(
|
|
66
|
+
`/acp/job-offerings/${encodeURIComponent(offeringName)}`,
|
|
67
|
+
);
|
|
68
|
+
return { success: true };
|
|
69
|
+
} catch (error: unknown) {
|
|
70
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
71
|
+
console.error(`ACP deleteJobOffering failed: ${msg}`);
|
|
72
|
+
return { success: false };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function upsertResourceApi(
|
|
77
|
+
resource: Resource,
|
|
78
|
+
): Promise<{ success: boolean; data?: AgentData }> {
|
|
79
|
+
try {
|
|
80
|
+
const { data } = await client.post(`/acp/resources`, {
|
|
81
|
+
data: resource,
|
|
82
|
+
});
|
|
83
|
+
return { success: true, data };
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
86
|
+
console.error(`ACP upsertResource failed: ${msg}`);
|
|
87
|
+
return { success: false };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function deleteResourceApi(
|
|
92
|
+
resourceName: string,
|
|
93
|
+
): Promise<{ success: boolean }> {
|
|
94
|
+
try {
|
|
95
|
+
await client.delete(`/acp/resources/${encodeURIComponent(resourceName)}`);
|
|
96
|
+
return { success: true };
|
|
97
|
+
} catch (error: unknown) {
|
|
98
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
99
|
+
console.error(`ACP deleteResource failed: ${msg}`);
|
|
100
|
+
return { success: false };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function getPaymentUrl(): Promise<{
|
|
105
|
+
success: boolean;
|
|
106
|
+
url?: string;
|
|
107
|
+
}> {
|
|
108
|
+
try {
|
|
109
|
+
const { data } = await client.get<{ data: PaymentUrlResponse }>(
|
|
110
|
+
"/acp/topup",
|
|
111
|
+
);
|
|
112
|
+
return { success: true, url: data.data.url };
|
|
113
|
+
} catch (error: any) {
|
|
114
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
115
|
+
console.error(`ACP getPaymentUrl failed: ${msg}`);
|
|
116
|
+
return { success: false };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Auth + Agent management API (acpx.virtuals.io)
|
|
3
|
+
// Shared by setup.ts, agent.ts, and any command needing session-based APIs.
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import axios, { type AxiosInstance } from "axios";
|
|
7
|
+
import * as output from "./output.js";
|
|
8
|
+
import { openUrl } from "./open.js";
|
|
9
|
+
import { readConfig, writeConfig, type AgentEntry } from "./config.js";
|
|
10
|
+
import client from "./client.js";
|
|
11
|
+
|
|
12
|
+
const API_URL = process.env.ACP_AUTH_URL || "https://acpx.virtuals.io";
|
|
13
|
+
|
|
14
|
+
// -- Response types --
|
|
15
|
+
|
|
16
|
+
export interface AuthUrlResponse {
|
|
17
|
+
authUrl: string;
|
|
18
|
+
requestId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthStatusResponse {
|
|
22
|
+
token: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Returned by list agents — no API key (never exposed after creation). */
|
|
26
|
+
export interface AgentInfoResponse {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
walletAddress: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Returned by create agent — API key shown once. */
|
|
33
|
+
export interface AgentKeyResponse {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
apiKey: string;
|
|
37
|
+
walletAddress: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Returned by regenerate — fresh API key for an existing agent. */
|
|
41
|
+
export interface RegenerateKeyResponse {
|
|
42
|
+
apiKey: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// -- HTTP clients --
|
|
46
|
+
|
|
47
|
+
function apiClient(): AxiosInstance {
|
|
48
|
+
return axios.create({
|
|
49
|
+
baseURL: API_URL,
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function apiClientWithSession(sessionToken: string): AxiosInstance {
|
|
55
|
+
return axios.create({
|
|
56
|
+
baseURL: API_URL,
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
Authorization: `Bearer ${sessionToken}`,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// -- Session token --
|
|
65
|
+
|
|
66
|
+
/** Decode the exp claim from a JWT without verifying the signature. */
|
|
67
|
+
function getJwtExpiry(token: string): Date | null {
|
|
68
|
+
try {
|
|
69
|
+
const parts = token.split(".");
|
|
70
|
+
if (parts.length !== 3) return null;
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
73
|
+
if (typeof payload.exp === "number") {
|
|
74
|
+
return new Date(payload.exp * 1000); // exp is seconds since epoch
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getValidSessionToken(): string | null {
|
|
83
|
+
const config = readConfig();
|
|
84
|
+
const token = config?.SESSION_TOKEN?.token;
|
|
85
|
+
if (!token) return null;
|
|
86
|
+
|
|
87
|
+
const expiry = getJwtExpiry(token);
|
|
88
|
+
if (!expiry || expiry <= new Date()) return null;
|
|
89
|
+
return token;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function storeSessionToken(token: string): void {
|
|
93
|
+
const config = readConfig();
|
|
94
|
+
writeConfig({ ...config, SESSION_TOKEN: { token } });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// -- Auth API --
|
|
98
|
+
|
|
99
|
+
export async function getAuthUrl(): Promise<AuthUrlResponse> {
|
|
100
|
+
const { data } = await apiClient().get<{ data: AuthUrlResponse }>(
|
|
101
|
+
"/api/auth/lite/auth-url",
|
|
102
|
+
);
|
|
103
|
+
return data.data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function getAuthStatus(
|
|
107
|
+
requestId: string,
|
|
108
|
+
): Promise<AuthStatusResponse | null> {
|
|
109
|
+
const { data } = await apiClient().get<{ data: AuthStatusResponse }>(
|
|
110
|
+
`/api/auth/lite/auth-status?requestId=${requestId}`,
|
|
111
|
+
);
|
|
112
|
+
return data?.data ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// -- Agent API --
|
|
116
|
+
|
|
117
|
+
/** Fetch all agents belonging to the authenticated user. No API keys returned. */
|
|
118
|
+
export async function fetchAgents(
|
|
119
|
+
sessionToken: string,
|
|
120
|
+
): Promise<AgentInfoResponse[]> {
|
|
121
|
+
const { data } = await apiClientWithSession(sessionToken).get<{
|
|
122
|
+
data: AgentInfoResponse[];
|
|
123
|
+
}>("/api/agents/lite");
|
|
124
|
+
return data.data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Create a new agent for the authenticated user. API key returned once. */
|
|
128
|
+
export async function createAgentApi(
|
|
129
|
+
sessionToken: string,
|
|
130
|
+
agentName: string,
|
|
131
|
+
): Promise<AgentKeyResponse> {
|
|
132
|
+
const { data } = await apiClientWithSession(sessionToken).post<{
|
|
133
|
+
data: AgentKeyResponse;
|
|
134
|
+
}>("/api/agents/lite/key", {
|
|
135
|
+
data: { name: agentName.trim() },
|
|
136
|
+
});
|
|
137
|
+
return data.data;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Regenerate the API key for an existing agent. Returns a fresh key. */
|
|
141
|
+
export async function regenerateApiKey(
|
|
142
|
+
sessionToken: string,
|
|
143
|
+
walletAddress: string,
|
|
144
|
+
): Promise<RegenerateKeyResponse> {
|
|
145
|
+
const { data } = await apiClientWithSession(sessionToken).post<{
|
|
146
|
+
data: RegenerateKeyResponse;
|
|
147
|
+
}>(`/api/agents/lite/${walletAddress}/regenerate-api`);
|
|
148
|
+
return data.data;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function isAgentApiKeyValid(apiKey: string): Promise<boolean> {
|
|
152
|
+
return await client
|
|
153
|
+
.get("/acp/me", {
|
|
154
|
+
headers: {
|
|
155
|
+
"x-api-key": apiKey,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
.then(() => true)
|
|
159
|
+
.catch(() => false);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// -- Login (polling-based, no stdin required) --
|
|
163
|
+
|
|
164
|
+
/** How often to poll the auth status endpoint (ms). */
|
|
165
|
+
const AUTH_POLL_INTERVAL_MS = 5_000;
|
|
166
|
+
|
|
167
|
+
/** How long to wait for the user to authenticate before timing out (ms). */
|
|
168
|
+
const AUTH_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
169
|
+
|
|
170
|
+
function sleep(ms: number): Promise<void> {
|
|
171
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Login flow. Opens browser / prints link, then polls until authenticated
|
|
176
|
+
* or timed out. No stdin interaction required — works in any runtime.
|
|
177
|
+
*/
|
|
178
|
+
export async function interactiveLogin(): Promise<void> {
|
|
179
|
+
let auth: AuthUrlResponse;
|
|
180
|
+
try {
|
|
181
|
+
auth = await getAuthUrl();
|
|
182
|
+
} catch (e) {
|
|
183
|
+
output.fatal(
|
|
184
|
+
`Could not get login link: ${e instanceof Error ? e.message : String(e)}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { authUrl, requestId } = auth;
|
|
189
|
+
openUrl(authUrl);
|
|
190
|
+
|
|
191
|
+
output.output(
|
|
192
|
+
{
|
|
193
|
+
action: "open_url",
|
|
194
|
+
url: authUrl,
|
|
195
|
+
message: "Authenticate at this URL to continue.",
|
|
196
|
+
},
|
|
197
|
+
() => {
|
|
198
|
+
output.log(` Opening browser...`);
|
|
199
|
+
output.log(` Login link: ${authUrl}\n`);
|
|
200
|
+
output.log(
|
|
201
|
+
` Waiting for authentication (timeout: ${AUTH_TIMEOUT_MS / 1_000}s)...\n`,
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const deadline = Date.now() + AUTH_TIMEOUT_MS;
|
|
207
|
+
let elapsed = 0;
|
|
208
|
+
|
|
209
|
+
while (Date.now() < deadline) {
|
|
210
|
+
await sleep(AUTH_POLL_INTERVAL_MS);
|
|
211
|
+
elapsed += AUTH_POLL_INTERVAL_MS;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const status = await getAuthStatus(requestId);
|
|
215
|
+
if (status?.token) {
|
|
216
|
+
storeSessionToken(status.token);
|
|
217
|
+
output.output(
|
|
218
|
+
{
|
|
219
|
+
status: "authenticated",
|
|
220
|
+
message: "Login success. Session stored.",
|
|
221
|
+
},
|
|
222
|
+
() => output.success("Login success. Session stored.\n"),
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// Auth not ready yet or transient error — keep polling
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Progress indicator every 15s (3 polls)
|
|
231
|
+
if (elapsed % 15_000 === 0) {
|
|
232
|
+
const remaining = Math.round((deadline - Date.now()) / 1_000);
|
|
233
|
+
output.log(` Still waiting... (${remaining}s remaining)`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
output.fatal(
|
|
238
|
+
`Authentication timed out after ${AUTH_TIMEOUT_MS / 1_000}s. Run \`acp login\` to try again.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Ensure we have a valid session token. If expired/missing, auto-prompts login.
|
|
244
|
+
* Returns the valid session token, or calls process.exit if login fails.
|
|
245
|
+
*/
|
|
246
|
+
export async function ensureSession(): Promise<string> {
|
|
247
|
+
const existing = getValidSessionToken();
|
|
248
|
+
if (existing) return existing;
|
|
249
|
+
|
|
250
|
+
output.warn("Session expired or not found. Logging in...\n");
|
|
251
|
+
await interactiveLogin();
|
|
252
|
+
|
|
253
|
+
const token = getValidSessionToken();
|
|
254
|
+
if (!token) {
|
|
255
|
+
output.fatal("Login failed. Cannot continue.");
|
|
256
|
+
}
|
|
257
|
+
return token;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// -- Agent sync --
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Merge server agents into local config. Returns the merged list.
|
|
264
|
+
* Server does NOT return API keys — only id, name, walletAddress.
|
|
265
|
+
* Local API keys (from create/regenerate) are preserved.
|
|
266
|
+
*/
|
|
267
|
+
export function syncAgentsToConfig(
|
|
268
|
+
serverAgents: AgentInfoResponse[],
|
|
269
|
+
): AgentEntry[] {
|
|
270
|
+
const config = readConfig();
|
|
271
|
+
const localAgents = config.agents ?? [];
|
|
272
|
+
|
|
273
|
+
const localMap = new Map<string, AgentEntry>();
|
|
274
|
+
for (const a of localAgents) {
|
|
275
|
+
localMap.set(a.id, a);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const merged: AgentEntry[] = serverAgents.map((s) => {
|
|
279
|
+
const local = localMap.get(s.id);
|
|
280
|
+
return {
|
|
281
|
+
id: s.id,
|
|
282
|
+
name: s.name,
|
|
283
|
+
walletAddress: s.walletAddress,
|
|
284
|
+
apiKey: local?.apiKey, // preserve local key if we have one
|
|
285
|
+
active: local?.active ?? false,
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
writeConfig({ ...config, agents: merged });
|
|
290
|
+
return merged;
|
|
291
|
+
}
|