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,257 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Bounty API + local active-bounty state management.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { ROOT, loadApiKey } from "./config.js";
|
|
9
|
+
|
|
10
|
+
export type BountyStatus =
|
|
11
|
+
| "open"
|
|
12
|
+
| "pending_match"
|
|
13
|
+
| "claimed"
|
|
14
|
+
| "fulfilled"
|
|
15
|
+
| "expired"
|
|
16
|
+
| "rejected";
|
|
17
|
+
|
|
18
|
+
export interface BountyCreateInput {
|
|
19
|
+
poster_email?: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
budget: number;
|
|
23
|
+
category: string;
|
|
24
|
+
tags: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BountyMatchCandidate {
|
|
28
|
+
id: number;
|
|
29
|
+
name?: string;
|
|
30
|
+
walletAddress?: string;
|
|
31
|
+
offeringName?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BountyMatchStatusResponse {
|
|
36
|
+
status: BountyStatus | string;
|
|
37
|
+
candidates: BountyMatchCandidate[];
|
|
38
|
+
acp_job_id?: string | number | null;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ActiveBounty {
|
|
43
|
+
bountyId: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
status: BountyStatus | string;
|
|
46
|
+
title: string;
|
|
47
|
+
description: string;
|
|
48
|
+
budget: number;
|
|
49
|
+
category: string;
|
|
50
|
+
tags: string;
|
|
51
|
+
posterName: string;
|
|
52
|
+
posterSecret: string;
|
|
53
|
+
selectedCandidateId?: number;
|
|
54
|
+
acpJobId?: string;
|
|
55
|
+
/** Set to true after the agent has been notified about pending_match candidates. */
|
|
56
|
+
notifiedPendingMatch?: boolean;
|
|
57
|
+
/** Channel where this bounty was created (e.g. "telegram", "webchat") for routing notifications. */
|
|
58
|
+
sourceChannel?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ActiveBountiesFile {
|
|
62
|
+
bounties: ActiveBounty[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const api = axios.create({
|
|
66
|
+
baseURL:
|
|
67
|
+
process.env.ACP_BOUNTY_API_URL || "https://bounty.virtuals.io/api/v1",
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
api.interceptors.request.use((config) => {
|
|
72
|
+
loadApiKey();
|
|
73
|
+
if (process.env.LITE_AGENT_API_KEY) {
|
|
74
|
+
config.headers["x-api-key"] = process.env.LITE_AGENT_API_KEY;
|
|
75
|
+
}
|
|
76
|
+
return config;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const BOUNTY_STATE_PATH = path.resolve(ROOT, "active-bounties.json");
|
|
80
|
+
|
|
81
|
+
function extractData<T>(raw: any): T {
|
|
82
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
83
|
+
return raw.data as T;
|
|
84
|
+
}
|
|
85
|
+
return raw as T;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ensureParent(filePath: string): void {
|
|
89
|
+
const parent = path.dirname(filePath);
|
|
90
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readState(): ActiveBountiesFile {
|
|
94
|
+
if (!fs.existsSync(BOUNTY_STATE_PATH)) return { bounties: [] };
|
|
95
|
+
try {
|
|
96
|
+
const raw = JSON.parse(fs.readFileSync(BOUNTY_STATE_PATH, "utf-8"));
|
|
97
|
+
if (Array.isArray(raw?.bounties)) {
|
|
98
|
+
return { bounties: raw.bounties as ActiveBounty[] };
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
return { bounties: [] };
|
|
102
|
+
}
|
|
103
|
+
return { bounties: [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeState(next: ActiveBountiesFile): void {
|
|
107
|
+
ensureParent(BOUNTY_STATE_PATH);
|
|
108
|
+
fs.writeFileSync(BOUNTY_STATE_PATH, JSON.stringify(next, null, 2) + "\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function listActiveBounties(): ActiveBounty[] {
|
|
112
|
+
return readState().bounties;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getActiveBounty(bountyId: string): ActiveBounty | undefined {
|
|
116
|
+
return readState().bounties.find((b) => b.bountyId === bountyId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getBountyByJobId(jobId: string): ActiveBounty | undefined {
|
|
120
|
+
return readState().bounties.find((b) => b.acpJobId === jobId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function saveActiveBounty(bounty: ActiveBounty): void {
|
|
124
|
+
const state = readState();
|
|
125
|
+
const idx = state.bounties.findIndex((b) => b.bountyId === bounty.bountyId);
|
|
126
|
+
if (idx >= 0) {
|
|
127
|
+
state.bounties[idx] = bounty;
|
|
128
|
+
} else {
|
|
129
|
+
state.bounties.push(bounty);
|
|
130
|
+
}
|
|
131
|
+
writeState(state);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function removeActiveBounty(bountyId: string): void {
|
|
135
|
+
const state = readState();
|
|
136
|
+
state.bounties = state.bounties.filter((b) => b.bountyId !== bountyId);
|
|
137
|
+
writeState(state);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function createBounty(
|
|
141
|
+
input: BountyCreateInput,
|
|
142
|
+
): Promise<{ bountyId: string; posterSecret: string; raw: unknown }> {
|
|
143
|
+
const res = await api.post("/bounties/", input);
|
|
144
|
+
const body = extractData<any>(res.data);
|
|
145
|
+
|
|
146
|
+
const bountyNode =
|
|
147
|
+
body?.bounty && typeof body.bounty === "object" ? body.bounty : body;
|
|
148
|
+
|
|
149
|
+
const bountyId =
|
|
150
|
+
bountyNode?.id ??
|
|
151
|
+
bountyNode?.bounty_id ??
|
|
152
|
+
bountyNode?.bountyId ??
|
|
153
|
+
body?.id ??
|
|
154
|
+
body?.bounty_id ??
|
|
155
|
+
body?.bountyId;
|
|
156
|
+
const posterSecret =
|
|
157
|
+
body?.poster_secret ??
|
|
158
|
+
body?.posterSecret ??
|
|
159
|
+
body?.data?.poster_secret ??
|
|
160
|
+
body?.data?.posterSecret;
|
|
161
|
+
|
|
162
|
+
if (!bountyId || !posterSecret) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"Invalid create bounty response: missing id or poster_secret",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
bountyId: String(bountyId),
|
|
170
|
+
posterSecret: String(posterSecret),
|
|
171
|
+
raw: body,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function getBountyDetails(
|
|
176
|
+
bountyId: string,
|
|
177
|
+
): Promise<Record<string, unknown>> {
|
|
178
|
+
const res = await api.get(`/bounties/${encodeURIComponent(bountyId)}`);
|
|
179
|
+
return extractData<Record<string, unknown>>(res.data);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function getMatchStatus(
|
|
183
|
+
bountyId: string,
|
|
184
|
+
): Promise<BountyMatchStatusResponse> {
|
|
185
|
+
const res = await api.get(
|
|
186
|
+
`/bounties/${encodeURIComponent(bountyId)}/match-status`,
|
|
187
|
+
);
|
|
188
|
+
const body = extractData<any>(res.data);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...body,
|
|
192
|
+
status: String(body?.status ?? ""),
|
|
193
|
+
candidates: Array.isArray(body?.candidates) ? body.candidates : [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function confirmMatch(params: {
|
|
198
|
+
bountyId: string;
|
|
199
|
+
posterSecret: string;
|
|
200
|
+
candidateId: number;
|
|
201
|
+
acpJobId: string;
|
|
202
|
+
}): Promise<unknown> {
|
|
203
|
+
const { bountyId, posterSecret, candidateId, acpJobId } = params;
|
|
204
|
+
const res = await api.post(
|
|
205
|
+
`/bounties/${encodeURIComponent(bountyId)}/confirm-match`,
|
|
206
|
+
{
|
|
207
|
+
poster_secret: posterSecret,
|
|
208
|
+
candidate_id: candidateId,
|
|
209
|
+
acp_job_id: acpJobId,
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
return extractData<unknown>(res.data);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface BountyUpdateInput {
|
|
216
|
+
poster_secret: string;
|
|
217
|
+
title?: string;
|
|
218
|
+
description?: string;
|
|
219
|
+
budget?: number;
|
|
220
|
+
tags?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function updateBounty(
|
|
224
|
+
bountyId: string,
|
|
225
|
+
input: BountyUpdateInput,
|
|
226
|
+
): Promise<unknown> {
|
|
227
|
+
const res = await api.put(`/bounties/${encodeURIComponent(bountyId)}`, input);
|
|
228
|
+
return extractData<unknown>(res.data);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function rejectCandidates(params: {
|
|
232
|
+
bountyId: string;
|
|
233
|
+
posterSecret: string;
|
|
234
|
+
}): Promise<unknown> {
|
|
235
|
+
const { bountyId, posterSecret } = params;
|
|
236
|
+
const res = await api.post(
|
|
237
|
+
`/bounties/${encodeURIComponent(bountyId)}/reject-candidates`,
|
|
238
|
+
{
|
|
239
|
+
poster_secret: posterSecret,
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
return extractData<unknown>(res.data);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function syncBountyJobStatus(params: {
|
|
246
|
+
bountyId: string;
|
|
247
|
+
posterSecret: string;
|
|
248
|
+
}): Promise<unknown> {
|
|
249
|
+
const { bountyId, posterSecret } = params;
|
|
250
|
+
const res = await api.post(
|
|
251
|
+
`/bounties/${encodeURIComponent(bountyId)}/job-status`,
|
|
252
|
+
{
|
|
253
|
+
poster_secret: posterSecret,
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
return extractData<unknown>(res.data);
|
|
257
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Axios HTTP client for the ACP API.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import dotenv from "dotenv";
|
|
7
|
+
import { loadApiKey } from "./config.js";
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
// Ensure API key is loaded from config into process.env
|
|
12
|
+
loadApiKey();
|
|
13
|
+
|
|
14
|
+
const client = axios.create({
|
|
15
|
+
baseURL: process.env.ACP_API_URL || "https://claw-api.virtuals.io",
|
|
16
|
+
timeout: 30_000,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Set the API key at REQUEST TIME (not module load) so env changes are
|
|
20
|
+
// picked up and the header is never baked in as undefined.
|
|
21
|
+
// Only set if the caller hasn't already provided an explicit x-api-key.
|
|
22
|
+
client.interceptors.request.use((config) => {
|
|
23
|
+
if (!config.headers["x-api-key"]) {
|
|
24
|
+
const apiKey = loadApiKey();
|
|
25
|
+
if (apiKey) {
|
|
26
|
+
config.headers["x-api-key"] = apiKey;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
client.interceptors.response.use(
|
|
33
|
+
(response) => response,
|
|
34
|
+
(error) => {
|
|
35
|
+
if (error.response) {
|
|
36
|
+
throw new Error(JSON.stringify(error.response.data));
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export default client;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Configuration file management.
|
|
3
|
+
// Reads/writes config.json at the repo root.
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
/** Repo root — two levels up from src/lib/ */
|
|
14
|
+
export const ROOT = path.resolve(__dirname, "..", "..");
|
|
15
|
+
export const CONFIG_JSON_PATH = path.resolve(ROOT, "config.json");
|
|
16
|
+
export const LOGS_DIR = path.resolve(ROOT, "logs");
|
|
17
|
+
|
|
18
|
+
export interface AgentEntry {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
walletAddress: string;
|
|
22
|
+
apiKey: string | undefined; // only present for active/previously-switched agents
|
|
23
|
+
active: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RailwayProjectConfig {
|
|
27
|
+
project: string;
|
|
28
|
+
environment: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DeployInfo {
|
|
32
|
+
provider: string;
|
|
33
|
+
agentName: string;
|
|
34
|
+
offerings: string[];
|
|
35
|
+
deployedAt: string;
|
|
36
|
+
railwayConfig: RailwayProjectConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ConfigJson {
|
|
40
|
+
SESSION_TOKEN?: {
|
|
41
|
+
token: string;
|
|
42
|
+
};
|
|
43
|
+
LITE_AGENT_API_KEY?: string;
|
|
44
|
+
SELLER_PID?: number;
|
|
45
|
+
OPENCLAW_BOUNTY_CRON_JOB_ID?: string;
|
|
46
|
+
agents?: AgentEntry[];
|
|
47
|
+
DEPLOYS?: Record<string, DeployInfo>; // keyed by agent ID
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readConfig(): ConfigJson {
|
|
51
|
+
if (!fs.existsSync(CONFIG_JSON_PATH)) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(CONFIG_JSON_PATH, "utf-8");
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeConfig(config: ConfigJson): void {
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(CONFIG_JSON_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Failed to write config.json: ${err}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Load the API key from config.json or environment. */
|
|
71
|
+
export function loadApiKey(): string | undefined {
|
|
72
|
+
if (process.env.LITE_AGENT_API_KEY?.trim()) {
|
|
73
|
+
return process.env.LITE_AGENT_API_KEY.trim();
|
|
74
|
+
}
|
|
75
|
+
const config = readConfig();
|
|
76
|
+
const key = config.LITE_AGENT_API_KEY;
|
|
77
|
+
if (typeof key === "string" && key.trim()) {
|
|
78
|
+
process.env.LITE_AGENT_API_KEY = key;
|
|
79
|
+
return key;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Ensure API key is loaded, or exit with error. */
|
|
85
|
+
export function requireApiKey(): string {
|
|
86
|
+
const key = loadApiKey();
|
|
87
|
+
if (!key) {
|
|
88
|
+
console.error(
|
|
89
|
+
"Error: LITE_AGENT_API_KEY is not set. Run `acp setup` first.",
|
|
90
|
+
);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isProcessRunning(pid: number): boolean {
|
|
97
|
+
try {
|
|
98
|
+
process.kill(pid, 0);
|
|
99
|
+
return true;
|
|
100
|
+
} catch (err: any) {
|
|
101
|
+
return err.code !== "ESRCH";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function writePidToConfig(pid: number): void {
|
|
106
|
+
try {
|
|
107
|
+
const config = readConfig();
|
|
108
|
+
config.SELLER_PID = pid;
|
|
109
|
+
writeConfig(config);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`Failed to write PID to config.json: ${err}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function removePidFromConfig(): void {
|
|
116
|
+
try {
|
|
117
|
+
const config = readConfig();
|
|
118
|
+
if (config.SELLER_PID !== undefined) {
|
|
119
|
+
delete config.SELLER_PID;
|
|
120
|
+
writeConfig(config);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Best effort cleanup
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function checkForExistingProcess(): void {
|
|
128
|
+
const config = readConfig();
|
|
129
|
+
|
|
130
|
+
if (config.SELLER_PID !== undefined) {
|
|
131
|
+
if (isProcessRunning(config.SELLER_PID)) {
|
|
132
|
+
console.error(
|
|
133
|
+
`Seller process already running with PID: ${config.SELLER_PID}`,
|
|
134
|
+
);
|
|
135
|
+
console.error(
|
|
136
|
+
"Please stop the existing process before starting a new one.",
|
|
137
|
+
);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
} else {
|
|
140
|
+
removePidFromConfig();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Find the PID of a running seller process (config check + OS fallback). */
|
|
146
|
+
export function findSellerPid(): number | undefined {
|
|
147
|
+
const config = readConfig();
|
|
148
|
+
if (config.SELLER_PID !== undefined && isProcessRunning(config.SELLER_PID)) {
|
|
149
|
+
return config.SELLER_PID;
|
|
150
|
+
}
|
|
151
|
+
if (config.SELLER_PID !== undefined) {
|
|
152
|
+
removePidFromConfig();
|
|
153
|
+
}
|
|
154
|
+
// Fallback: scan OS processes
|
|
155
|
+
try {
|
|
156
|
+
const { execSync } = require("child_process");
|
|
157
|
+
const out = execSync(
|
|
158
|
+
'ps ax -o pid,command | grep "seller/runtime/seller.ts" | grep -v grep',
|
|
159
|
+
{
|
|
160
|
+
encoding: "utf-8",
|
|
161
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
for (const line of out.trim().split("\n")) {
|
|
165
|
+
const trimmed = line.trim();
|
|
166
|
+
if (!trimmed) continue;
|
|
167
|
+
const pid = parseInt(trimmed.split(/\s+/)[0], 10);
|
|
168
|
+
if (!isNaN(pid) && pid !== process.pid) return pid;
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// grep returns exit code 1 when no matches
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Get the currently active agent from the agents array. */
|
|
177
|
+
export function getActiveAgent(): AgentEntry | undefined {
|
|
178
|
+
const config = readConfig();
|
|
179
|
+
return config.agents?.find((a) => a.active);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Find an agent by name (case-insensitive). */
|
|
183
|
+
export function findAgentByName(name: string): AgentEntry | undefined {
|
|
184
|
+
const config = readConfig();
|
|
185
|
+
return config.agents?.find(
|
|
186
|
+
(a) => a.name.toLowerCase() === name.toLowerCase(),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function findAgentByWalletAddress(
|
|
191
|
+
walletAddress: string,
|
|
192
|
+
): AgentEntry | undefined {
|
|
193
|
+
const config = readConfig();
|
|
194
|
+
return config.agents?.find(
|
|
195
|
+
(a) => a.walletAddress.toLowerCase() === walletAddress.toLowerCase(),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Activate an agent with a (possibly new) API key. Updates active flags and LITE_AGENT_API_KEY. */
|
|
200
|
+
export function activateAgent(agentId: string, apiKey: string): void {
|
|
201
|
+
const config = readConfig();
|
|
202
|
+
const agents = (config.agents ?? []).map((a) => ({
|
|
203
|
+
...a,
|
|
204
|
+
active: a.id === agentId,
|
|
205
|
+
apiKey: a.id === agentId ? apiKey : a.apiKey,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
writeConfig({
|
|
209
|
+
...config,
|
|
210
|
+
agents,
|
|
211
|
+
LITE_AGENT_API_KEY: apiKey,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Sanitize an agent name for use as a directory name. */
|
|
216
|
+
export function sanitizeAgentName(name: string): string {
|
|
217
|
+
const sanitized = name
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
220
|
+
.replace(/(^-|-$)/g, "");
|
|
221
|
+
return sanitized || "unnamed-agent";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function formatPrice(price: unknown, priceType?: unknown): string {
|
|
225
|
+
const p = price != null ? String(price) : "-";
|
|
226
|
+
const type = String(priceType).toLowerCase();
|
|
227
|
+
if (type === "fixed") {
|
|
228
|
+
return `${p} USDC`;
|
|
229
|
+
} else if (type === "percentage") {
|
|
230
|
+
// Percentage is stored as decimal
|
|
231
|
+
const numPrice = typeof price === "number" ? price : parseFloat(p);
|
|
232
|
+
if (!isNaN(numPrice)) {
|
|
233
|
+
return `${(numPrice * 100).toFixed(2)}%`;
|
|
234
|
+
}
|
|
235
|
+
return `${p}%`;
|
|
236
|
+
} else if (priceType != null) {
|
|
237
|
+
return `${p} ${priceType}`;
|
|
238
|
+
}
|
|
239
|
+
return p;
|
|
240
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Open a URL in the user's default browser. Platform-specific, no dependencies.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const CONTROL_CHAR_REGEX = /[\u0000-\u001F\u007F]/;
|
|
8
|
+
|
|
9
|
+
function isSafeHttpUrl(url: string): boolean {
|
|
10
|
+
if (!url || CONTROL_CHAR_REGEX.test(url)) return false;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function openUrl(url: string): void {
|
|
21
|
+
if (!isSafeHttpUrl(url)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
let command = "xdg-open";
|
|
27
|
+
let args = [url];
|
|
28
|
+
|
|
29
|
+
if (platform === "darwin") {
|
|
30
|
+
command = "open";
|
|
31
|
+
} else if (platform === "win32") {
|
|
32
|
+
command = "rundll32";
|
|
33
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
execFile(command, args, (err) => {
|
|
37
|
+
if (err) {
|
|
38
|
+
// Silently fail — the URL is always printed as fallback
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|