@paper-clip/pc 0.1.6 โ 0.1.7
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 +28 -9
- package/baked-config.json +2 -2
- package/dist/chain-adapter.js +44 -0
- package/dist/evm-adapter.js +298 -0
- package/dist/index.js +269 -282
- package/dist/privy-evm.js +113 -0
- package/dist/privy.js +2 -2
- package/dist/settings.js +8 -0
- package/dist/solana-adapter.js +299 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -2,108 +2,52 @@
|
|
|
2
2
|
* Paperclip Protocol CLI
|
|
3
3
|
*
|
|
4
4
|
* Human-friendly command-line interface for AI agents
|
|
5
|
-
* interacting with the Paperclip Protocol on Solana.
|
|
5
|
+
* interacting with the Paperclip Protocol on Solana and EVM chains.
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import * as anchor from "@coral-xyz/anchor";
|
|
9
|
-
import bs58 from "bs58";
|
|
10
|
-
import { fromFixedBytes, getAgentPda, getClaimPda, getInvitePda, getProgram, getProtocolPda, getTaskPda, toFixedBytes, } from "./client.js";
|
|
11
8
|
import { fetchJson, uploadJson } from "./storacha.js";
|
|
12
9
|
import { banner, blank, fail, heading, info, parseError, spin, success, table, warn } from "./ui.js";
|
|
13
|
-
import { getMode,
|
|
14
|
-
import { NETWORK,
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
import { getMode, getServer, setMode, setNetwork, setServer, configPath, } from "./settings.js";
|
|
11
|
+
import { NETWORK, WALLET_TYPE, } from "./config.js";
|
|
12
|
+
import { getServerConfig, listServers, BUILTIN_SERVERS, } from "./chain-adapter.js";
|
|
13
|
+
import { SolanaAdapter } from "./solana-adapter.js";
|
|
14
|
+
import { EVMAdapter } from "./evm-adapter.js";
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// HELPERS
|
|
17
|
+
// =============================================================================
|
|
18
18
|
function jsonOutput(data) {
|
|
19
19
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
20
20
|
}
|
|
21
|
-
function
|
|
21
|
+
function shortKey(key) {
|
|
22
22
|
if (key.length <= 12)
|
|
23
23
|
return key;
|
|
24
24
|
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
25
25
|
}
|
|
26
|
-
function asPubkey(value) {
|
|
27
|
-
return value instanceof anchor.web3.PublicKey
|
|
28
|
-
? value
|
|
29
|
-
: new anchor.web3.PublicKey(value);
|
|
30
|
-
}
|
|
31
|
-
function isZeroPubkey(value) {
|
|
32
|
-
return asPubkey(value).toBuffer().equals(Buffer.alloc(32));
|
|
33
|
-
}
|
|
34
|
-
async function getAgentAccount(program, agentPubkey) {
|
|
35
|
-
const agentPda = getAgentPda(program.programId, agentPubkey);
|
|
36
|
-
try {
|
|
37
|
-
return await program.account.agentAccount.fetch(agentPda);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function listActiveTasks(program) {
|
|
44
|
-
const activeFilter = {
|
|
45
|
-
memcmp: {
|
|
46
|
-
offset: TASK_IS_ACTIVE_OFFSET,
|
|
47
|
-
bytes: bs58.encode(Buffer.from([1])),
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
const tasks = await program.account.taskRecord.all([activeFilter]);
|
|
51
|
-
return tasks.filter((task) => task.account.currentClaims < task.account.maxClaims);
|
|
52
|
-
}
|
|
53
|
-
async function listDoableTasks(program, agentPubkey, agentTier) {
|
|
54
|
-
const tasks = await listActiveTasks(program);
|
|
55
|
-
if (tasks.length === 0) {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
const tierEligible = tasks.filter((task) => agentTier >= task.account.minTier);
|
|
59
|
-
if (tierEligible.length === 0) {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
const connection = program.provider.connection;
|
|
63
|
-
const claimPdas = tierEligible.map((task) => getClaimPda(program.programId, task.account.taskId, agentPubkey));
|
|
64
|
-
const claimInfos = await connection.getMultipleAccountsInfo(claimPdas);
|
|
65
|
-
const unclaimed = tierEligible.filter((_task, idx) => !claimInfos[idx]);
|
|
66
|
-
const gated = unclaimed.filter((task) => task.account.requiredTaskId !== NO_PREREQ_TASK_ID);
|
|
67
|
-
if (gated.length === 0) {
|
|
68
|
-
return unclaimed;
|
|
69
|
-
}
|
|
70
|
-
const prerequisitePdas = gated.map((task) => getClaimPda(program.programId, task.account.requiredTaskId, agentPubkey));
|
|
71
|
-
const prerequisiteInfos = await connection.getMultipleAccountsInfo(prerequisitePdas);
|
|
72
|
-
const isGatedTaskDoable = new Set();
|
|
73
|
-
gated.forEach((task, idx) => {
|
|
74
|
-
if (prerequisiteInfos[idx]) {
|
|
75
|
-
isGatedTaskDoable.add(task.account.taskId);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
return unclaimed.filter((task) => task.account.requiredTaskId === NO_PREREQ_TASK_ID ||
|
|
79
|
-
isGatedTaskDoable.has(task.account.taskId));
|
|
80
|
-
}
|
|
81
26
|
// =============================================================================
|
|
82
27
|
// CLI SETUP
|
|
83
28
|
// =============================================================================
|
|
84
29
|
const cli = new Command();
|
|
85
30
|
cli
|
|
86
31
|
.name("pc")
|
|
87
|
-
.description("Paperclip Protocol CLI โ
|
|
88
|
-
.version("0.1.
|
|
89
|
-
.option("
|
|
32
|
+
.description("Paperclip Protocol CLI โ For AI Agents")
|
|
33
|
+
.version("0.1.7")
|
|
34
|
+
.option("--server <name>", "Server to connect to (e.g. solana-devnet, monad-testnet)")
|
|
35
|
+
.option("--network <net>", "DEPRECATED: Use --server instead")
|
|
90
36
|
.option("--json", "Force JSON output (override mode)")
|
|
91
37
|
.option("--human", "Force human output (override mode)")
|
|
92
38
|
.option("--mock-storacha", "Use mock Storacha uploads (test only)");
|
|
93
39
|
function normalizeNetwork(value) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
return
|
|
97
|
-
}
|
|
40
|
+
const lower = value.toLowerCase().trim();
|
|
41
|
+
if (lower === "devnet" || lower === "localnet")
|
|
42
|
+
return lower;
|
|
98
43
|
return null;
|
|
99
44
|
}
|
|
100
45
|
function isJsonMode() {
|
|
101
|
-
|
|
102
|
-
if (
|
|
46
|
+
const opts = cli.opts();
|
|
47
|
+
if (opts.json)
|
|
103
48
|
return true;
|
|
104
|
-
if (
|
|
49
|
+
if (opts.human)
|
|
105
50
|
return false;
|
|
106
|
-
// Otherwise use saved mode: agent=JSON, human=pretty
|
|
107
51
|
return getMode() === "agent";
|
|
108
52
|
}
|
|
109
53
|
function applyMockFlag() {
|
|
@@ -112,36 +56,109 @@ function applyMockFlag() {
|
|
|
112
56
|
}
|
|
113
57
|
}
|
|
114
58
|
function validateNetworkFlag() {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
117
|
-
return;
|
|
118
|
-
if (normalizeNetwork(requested) !== null)
|
|
59
|
+
const raw = cli.opts().network;
|
|
60
|
+
if (!raw)
|
|
119
61
|
return;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
62
|
+
const net = normalizeNetwork(raw);
|
|
63
|
+
if (!net) {
|
|
64
|
+
if (isJsonMode()) {
|
|
65
|
+
jsonOutput({ ok: false, error: `Invalid network: "${raw}". Use "devnet" or "localnet"` });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
fail(`Invalid network: "${raw}". Use "devnet" or "localnet"`);
|
|
69
|
+
}
|
|
70
|
+
process.exit(1);
|
|
125
71
|
}
|
|
126
|
-
process.exit(1);
|
|
127
72
|
}
|
|
128
73
|
cli.hook("preAction", () => {
|
|
129
74
|
validateNetworkFlag();
|
|
130
75
|
});
|
|
131
76
|
// =============================================================================
|
|
77
|
+
// ADAPTER FACTORY
|
|
78
|
+
// =============================================================================
|
|
79
|
+
function resolveServerName() {
|
|
80
|
+
// Priority: --server flag > env var > saved config > network-based default
|
|
81
|
+
const flagServer = cli.opts().server;
|
|
82
|
+
if (flagServer)
|
|
83
|
+
return flagServer;
|
|
84
|
+
const envServer = process.env.PAPERCLIP_SERVER;
|
|
85
|
+
if (envServer)
|
|
86
|
+
return envServer;
|
|
87
|
+
const savedServer = getServer();
|
|
88
|
+
if (savedServer)
|
|
89
|
+
return savedServer;
|
|
90
|
+
// Fall back to network-based server (backward compat)
|
|
91
|
+
return NETWORK === "localnet" ? "solana-localnet" : "solana-devnet";
|
|
92
|
+
}
|
|
93
|
+
function createAdapter() {
|
|
94
|
+
const serverName = resolveServerName();
|
|
95
|
+
const config = getServerConfig(serverName);
|
|
96
|
+
if (!config) {
|
|
97
|
+
const available = listServers().map((s) => s.name).join(", ");
|
|
98
|
+
throw new Error(`Unknown server "${serverName}". Available: ${available}`);
|
|
99
|
+
}
|
|
100
|
+
// Allow env overrides for contract address
|
|
101
|
+
const overriddenConfig = { ...config };
|
|
102
|
+
if (config.chain === "evm") {
|
|
103
|
+
const envContract = process.env.PAPERCLIP_EVM_CONTRACT_ADDRESS;
|
|
104
|
+
if (envContract)
|
|
105
|
+
overriddenConfig.contractAddress = envContract;
|
|
106
|
+
}
|
|
107
|
+
if (config.chain === "solana") {
|
|
108
|
+
return new SolanaAdapter(overriddenConfig);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return new EVMAdapter(overriddenConfig);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// =============================================================================
|
|
115
|
+
// SERVERS COMMAND
|
|
116
|
+
// =============================================================================
|
|
117
|
+
cli
|
|
118
|
+
.command("servers")
|
|
119
|
+
.description("List available servers")
|
|
120
|
+
.action(() => {
|
|
121
|
+
const currentServer = resolveServerName();
|
|
122
|
+
if (isJsonMode()) {
|
|
123
|
+
jsonOutput({
|
|
124
|
+
servers: BUILTIN_SERVERS.map((s) => ({
|
|
125
|
+
name: s.name,
|
|
126
|
+
chain: s.chain,
|
|
127
|
+
label: s.label,
|
|
128
|
+
rpcUrl: s.rpcUrl,
|
|
129
|
+
active: s.name === currentServer,
|
|
130
|
+
})),
|
|
131
|
+
current: currentServer,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
banner();
|
|
136
|
+
heading("Available Servers");
|
|
137
|
+
blank();
|
|
138
|
+
for (const s of BUILTIN_SERVERS) {
|
|
139
|
+
const marker = s.name === currentServer ? " โ
" : " ";
|
|
140
|
+
info(marker, `${s.name} โ ${s.label} (${s.chain})`);
|
|
141
|
+
}
|
|
142
|
+
blank();
|
|
143
|
+
info("๐ก", "Switch with: pc config set server <name>");
|
|
144
|
+
blank();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// =============================================================================
|
|
132
148
|
// INIT COMMAND
|
|
133
149
|
// =============================================================================
|
|
134
150
|
cli
|
|
135
151
|
.command("init")
|
|
136
152
|
.description("Register as an agent on the protocol")
|
|
137
|
-
.option("--invite <code>", "Invite code (inviter wallet pubkey)")
|
|
153
|
+
.option("--invite <code>", "Invite code (inviter wallet pubkey or address)")
|
|
138
154
|
.action(async (opts) => {
|
|
139
155
|
applyMockFlag();
|
|
156
|
+
const adapter = createAdapter();
|
|
140
157
|
// If using Privy, auto-provision wallet on first init
|
|
141
|
-
if (WALLET_TYPE === "privy") {
|
|
158
|
+
if (WALLET_TYPE === "privy" && adapter.provisionWallet) {
|
|
142
159
|
const spinnerProvision = isJsonMode() ? null : spin("Provisioning server wallet...");
|
|
143
160
|
try {
|
|
144
|
-
await
|
|
161
|
+
await adapter.provisionWallet();
|
|
145
162
|
spinnerProvision?.succeed("Server wallet ready");
|
|
146
163
|
}
|
|
147
164
|
catch (err) {
|
|
@@ -156,29 +173,27 @@ cli
|
|
|
156
173
|
process.exit(1);
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
|
-
const
|
|
160
|
-
const provider = programClient.provider;
|
|
161
|
-
const wallet = provider.wallet;
|
|
162
|
-
const pubkey = wallet.publicKey;
|
|
176
|
+
const walletAddr = await adapter.getWalletAddress();
|
|
163
177
|
if (!isJsonMode()) {
|
|
164
178
|
banner();
|
|
165
|
-
info("๐ค Wallet:",
|
|
179
|
+
info("๐ค Wallet:", walletAddr);
|
|
180
|
+
info("๐ Server:", resolveServerName());
|
|
166
181
|
blank();
|
|
167
182
|
}
|
|
168
183
|
// Check if already registered
|
|
169
|
-
const existing = await
|
|
184
|
+
const existing = await adapter.getAgent(walletAddr);
|
|
170
185
|
if (existing) {
|
|
171
186
|
if (isJsonMode()) {
|
|
172
187
|
jsonOutput({
|
|
173
188
|
ok: true,
|
|
174
189
|
already_registered: true,
|
|
175
|
-
|
|
176
|
-
clips_balance: existing.clipsBalance
|
|
190
|
+
agent_wallet: walletAddr,
|
|
191
|
+
clips_balance: existing.clipsBalance,
|
|
177
192
|
});
|
|
178
193
|
}
|
|
179
194
|
else {
|
|
180
195
|
success("Already registered!");
|
|
181
|
-
info("๐ Clips:", existing.clipsBalance
|
|
196
|
+
info("๐ Clips:", existing.clipsBalance);
|
|
182
197
|
info("โญ Tier:", existing.efficiencyTier);
|
|
183
198
|
info("โ
Tasks completed:", existing.tasksCompleted);
|
|
184
199
|
blank();
|
|
@@ -188,61 +203,26 @@ cli
|
|
|
188
203
|
// Register
|
|
189
204
|
const spinner = isJsonMode() ? null : spin("Registering agent...");
|
|
190
205
|
try {
|
|
191
|
-
|
|
192
|
-
const agentPda = getAgentPda(programClient.programId, pubkey);
|
|
206
|
+
let result;
|
|
193
207
|
if (opts.invite) {
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
inviterPubkey = new anchor.web3.PublicKey(opts.invite);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
throw new Error("Invalid invite code format (expected base58 pubkey)");
|
|
200
|
-
}
|
|
201
|
-
if (inviterPubkey.equals(pubkey)) {
|
|
202
|
-
throw new Error("Self-referral is not allowed");
|
|
203
|
-
}
|
|
204
|
-
const inviterAgentPda = getAgentPda(programClient.programId, inviterPubkey);
|
|
205
|
-
const invitePda = getInvitePda(programClient.programId, inviterPubkey);
|
|
206
|
-
const inviteCode = Array.from(inviterPubkey.toBuffer());
|
|
207
|
-
await programClient.methods
|
|
208
|
-
.registerAgentWithInvite(inviteCode)
|
|
209
|
-
.accounts({
|
|
210
|
-
protocol: protocolPda,
|
|
211
|
-
agentAccount: agentPda,
|
|
212
|
-
inviterAgent: inviterAgentPda,
|
|
213
|
-
inviteRecord: invitePda,
|
|
214
|
-
agent: pubkey,
|
|
215
|
-
systemProgram: anchor.web3.SystemProgram.programId,
|
|
216
|
-
})
|
|
217
|
-
.rpc();
|
|
208
|
+
result = await adapter.registerAgentWithInvite(opts.invite);
|
|
218
209
|
}
|
|
219
210
|
else {
|
|
220
|
-
await
|
|
221
|
-
|
|
222
|
-
.accounts({
|
|
223
|
-
protocol: protocolPda,
|
|
224
|
-
agentAccount: agentPda,
|
|
225
|
-
agent: pubkey,
|
|
226
|
-
systemProgram: anchor.web3.SystemProgram.programId,
|
|
227
|
-
})
|
|
228
|
-
.rpc();
|
|
229
|
-
}
|
|
230
|
-
const agent = await programClient.account.agentAccount.fetch(agentPda);
|
|
211
|
+
result = await adapter.registerAgent();
|
|
212
|
+
}
|
|
231
213
|
spinner?.succeed("Agent registered!");
|
|
232
214
|
if (isJsonMode()) {
|
|
233
215
|
jsonOutput({
|
|
234
216
|
ok: true,
|
|
235
|
-
|
|
236
|
-
clips_balance:
|
|
237
|
-
invited_by:
|
|
238
|
-
? asPubkey(agent.invitedBy).toBase58()
|
|
239
|
-
: null,
|
|
217
|
+
agent_wallet: result.wallet,
|
|
218
|
+
clips_balance: result.clipsBalance,
|
|
219
|
+
invited_by: result.invitedBy,
|
|
240
220
|
});
|
|
241
221
|
}
|
|
242
222
|
else {
|
|
243
|
-
info("๐ Clips:",
|
|
244
|
-
if (
|
|
245
|
-
info("๐ค Invited by:",
|
|
223
|
+
info("๐ Clips:", result.clipsBalance);
|
|
224
|
+
if (result.invitedBy) {
|
|
225
|
+
info("๐ค Invited by:", shortKey(result.invitedBy));
|
|
246
226
|
}
|
|
247
227
|
info("๐ Next:", "Run `pc tasks` to see available work");
|
|
248
228
|
blank();
|
|
@@ -268,46 +248,51 @@ cli
|
|
|
268
248
|
.description("Create (or show) your invite code")
|
|
269
249
|
.action(async () => {
|
|
270
250
|
applyMockFlag();
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const wallet = provider.wallet;
|
|
274
|
-
const pubkey = wallet.publicKey;
|
|
275
|
-
const agentPda = getAgentPda(programClient.programId, pubkey);
|
|
276
|
-
const invitePda = getInvitePda(programClient.programId, pubkey);
|
|
251
|
+
const adapter = createAdapter();
|
|
252
|
+
const walletAddr = await adapter.getWalletAddress();
|
|
277
253
|
const spinner = isJsonMode() ? null : spin("Preparing invite code...");
|
|
278
254
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
255
|
+
// Check if agent is registered
|
|
256
|
+
const agent = await adapter.getAgent(walletAddr);
|
|
257
|
+
if (!agent) {
|
|
258
|
+
throw new Error("Not registered. Run `pc init` first.");
|
|
259
|
+
}
|
|
260
|
+
// Check for existing invite
|
|
261
|
+
const existingInvite = await adapter.getInvite(walletAddr);
|
|
262
|
+
if (existingInvite && existingInvite.exists) {
|
|
263
|
+
spinner?.succeed("Invite code ready");
|
|
264
|
+
const inviteCode = walletAddr; // The wallet address IS the invite code
|
|
265
|
+
if (isJsonMode()) {
|
|
266
|
+
jsonOutput({
|
|
267
|
+
ok: true,
|
|
268
|
+
invite_code: inviteCode,
|
|
269
|
+
invites_redeemed: existingInvite.invitesRedeemed,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
info("๐ Invite code:", inviteCode);
|
|
274
|
+
info("๐ฅ Redeemed:", existingInvite.invitesRedeemed);
|
|
275
|
+
info("๐ Share:", `pc init --invite ${inviteCode}`);
|
|
276
|
+
blank();
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Create new invite
|
|
281
|
+
if (spinner)
|
|
282
|
+
spinner.text = "Creating invite on-chain...";
|
|
283
|
+
const result = await adapter.createInvite();
|
|
284
|
+
spinner?.succeed("Invite created!");
|
|
299
285
|
if (isJsonMode()) {
|
|
300
286
|
jsonOutput({
|
|
301
287
|
ok: true,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
invites_redeemed: invite.invitesRedeemed,
|
|
288
|
+
invite_code: result.inviteCode,
|
|
289
|
+
invites_redeemed: result.invitesRedeemed,
|
|
305
290
|
});
|
|
306
291
|
}
|
|
307
292
|
else {
|
|
308
|
-
info("๐ Invite code:", inviteCode);
|
|
309
|
-
info("๐ฅ Redeemed:",
|
|
310
|
-
info("๐ Share:", `pc init --invite ${inviteCode}`);
|
|
293
|
+
info("๐ Invite code:", result.inviteCode);
|
|
294
|
+
info("๐ฅ Redeemed:", result.invitesRedeemed);
|
|
295
|
+
info("๐ Share:", `pc init --invite ${result.inviteCode}`);
|
|
311
296
|
blank();
|
|
312
297
|
}
|
|
313
298
|
}
|
|
@@ -332,42 +317,40 @@ cli
|
|
|
332
317
|
.description("Show your agent status and recommendations")
|
|
333
318
|
.action(async () => {
|
|
334
319
|
applyMockFlag();
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
const wallet = provider.wallet;
|
|
338
|
-
const pubkey = wallet.publicKey;
|
|
320
|
+
const adapter = createAdapter();
|
|
321
|
+
const walletAddr = await adapter.getWalletAddress();
|
|
339
322
|
if (!isJsonMode()) {
|
|
340
323
|
banner();
|
|
341
324
|
}
|
|
342
325
|
const spinner = isJsonMode() ? null : spin("Loading agent status...");
|
|
343
326
|
try {
|
|
344
|
-
const agent = await
|
|
327
|
+
const agent = await adapter.getAgent(walletAddr);
|
|
345
328
|
if (!agent) {
|
|
346
329
|
spinner?.stop();
|
|
347
330
|
if (isJsonMode()) {
|
|
348
331
|
jsonOutput({
|
|
349
332
|
agent: null,
|
|
350
333
|
available_tasks: 0,
|
|
351
|
-
recommendation: "
|
|
334
|
+
recommendation: "Register first: pc init",
|
|
352
335
|
});
|
|
353
336
|
}
|
|
354
337
|
else {
|
|
355
|
-
warn("Not registered yet");
|
|
356
|
-
info("๐ Next:", "Run `pc init` to
|
|
338
|
+
warn("Not registered yet.");
|
|
339
|
+
info("๐ Next:", "Run `pc init` to register");
|
|
357
340
|
blank();
|
|
358
341
|
}
|
|
359
342
|
return;
|
|
360
343
|
}
|
|
361
|
-
const doable = await listDoableTasks(
|
|
362
|
-
|
|
344
|
+
const doable = await adapter.listDoableTasks(walletAddr, agent.efficiencyTier);
|
|
345
|
+
const recommendation = doable.length > 0
|
|
346
|
+
? `${doable.length} task${doable.length !== 1 ? "s" : ""} available โ run: pc tasks`
|
|
347
|
+
: "No tasks available right now. Check back later.";
|
|
348
|
+
spinner?.succeed("Status loaded");
|
|
363
349
|
if (isJsonMode()) {
|
|
364
|
-
const recommendation = doable.length > 0
|
|
365
|
-
? `${doable.length} tasks available. Run: pc tasks`
|
|
366
|
-
: "No tasks available. Check back later.";
|
|
367
350
|
jsonOutput({
|
|
368
351
|
agent: {
|
|
369
|
-
|
|
370
|
-
clips: agent.clipsBalance
|
|
352
|
+
wallet: walletAddr,
|
|
353
|
+
clips: agent.clipsBalance,
|
|
371
354
|
tier: agent.efficiencyTier,
|
|
372
355
|
tasks_completed: agent.tasksCompleted,
|
|
373
356
|
},
|
|
@@ -377,13 +360,14 @@ cli
|
|
|
377
360
|
}
|
|
378
361
|
else {
|
|
379
362
|
heading("Agent");
|
|
380
|
-
info("๐ค Wallet:",
|
|
381
|
-
info("๐ Clips:", agent.clipsBalance
|
|
363
|
+
info("๐ค Wallet:", walletAddr);
|
|
364
|
+
info("๐ Clips:", agent.clipsBalance);
|
|
382
365
|
info("โญ Tier:", agent.efficiencyTier);
|
|
383
|
-
info("โ
|
|
384
|
-
|
|
366
|
+
info("โ
Tasks completed:", agent.tasksCompleted);
|
|
367
|
+
blank();
|
|
368
|
+
heading("Recommendations");
|
|
385
369
|
if (doable.length > 0) {
|
|
386
|
-
info("๐ Available:", `${doable.length}
|
|
370
|
+
info("๐ Available:", `${doable.length} task${doable.length !== 1 ? "s" : ""}`);
|
|
387
371
|
info("๐ Next:", "Run `pc tasks` to browse");
|
|
388
372
|
}
|
|
389
373
|
else {
|
|
@@ -413,15 +397,13 @@ cli
|
|
|
413
397
|
.description("List available tasks you can complete")
|
|
414
398
|
.action(async () => {
|
|
415
399
|
applyMockFlag();
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
const wallet = provider.wallet;
|
|
419
|
-
const pubkey = wallet.publicKey;
|
|
400
|
+
const adapter = createAdapter();
|
|
401
|
+
const walletAddr = await adapter.getWalletAddress();
|
|
420
402
|
if (!isJsonMode()) {
|
|
421
403
|
banner();
|
|
422
404
|
}
|
|
423
405
|
// Check registration
|
|
424
|
-
const agent = await
|
|
406
|
+
const agent = await adapter.getAgent(walletAddr);
|
|
425
407
|
if (!agent) {
|
|
426
408
|
if (isJsonMode()) {
|
|
427
409
|
jsonOutput({ ok: false, error: "Not registered. Run: pc init" });
|
|
@@ -435,7 +417,7 @@ cli
|
|
|
435
417
|
const spinner = isJsonMode() ? null : spin("Fetching tasks...");
|
|
436
418
|
try {
|
|
437
419
|
const jsonMode = isJsonMode();
|
|
438
|
-
const doable = await listDoableTasks(
|
|
420
|
+
const doable = await adapter.listDoableTasks(walletAddr, agent.efficiencyTier);
|
|
439
421
|
if (doable.length === 0) {
|
|
440
422
|
spinner?.stop();
|
|
441
423
|
if (isJsonMode()) {
|
|
@@ -448,30 +430,26 @@ cli
|
|
|
448
430
|
}
|
|
449
431
|
return;
|
|
450
432
|
}
|
|
451
|
-
// Expand tasks with content from Storacha
|
|
433
|
+
// Expand tasks with content from Storacha (JSON mode only)
|
|
452
434
|
const expanded = await Promise.all(doable.map(async (task) => {
|
|
453
|
-
const contentCid = fromFixedBytes(task.account.contentCid);
|
|
454
435
|
let content = null;
|
|
455
|
-
|
|
456
|
-
if (jsonMode) {
|
|
436
|
+
if (jsonMode && task.contentCid) {
|
|
457
437
|
try {
|
|
458
|
-
content = await fetchJson(contentCid);
|
|
438
|
+
content = await fetchJson(task.contentCid);
|
|
459
439
|
}
|
|
460
440
|
catch {
|
|
461
441
|
content = null;
|
|
462
442
|
}
|
|
463
443
|
}
|
|
464
444
|
return {
|
|
465
|
-
taskId: task.
|
|
466
|
-
title:
|
|
467
|
-
rewardClips: task.
|
|
468
|
-
maxClaims: task.
|
|
469
|
-
currentClaims: task.
|
|
470
|
-
minTier: task.
|
|
471
|
-
requiredTaskId: task.
|
|
472
|
-
|
|
473
|
-
: task.account.requiredTaskId,
|
|
474
|
-
contentCid,
|
|
445
|
+
taskId: task.taskId,
|
|
446
|
+
title: task.title,
|
|
447
|
+
rewardClips: task.rewardClips,
|
|
448
|
+
maxClaims: task.maxClaims,
|
|
449
|
+
currentClaims: task.currentClaims,
|
|
450
|
+
minTier: task.minTier,
|
|
451
|
+
requiredTaskId: task.requiredTaskId,
|
|
452
|
+
contentCid: task.contentCid,
|
|
475
453
|
content,
|
|
476
454
|
};
|
|
477
455
|
}));
|
|
@@ -526,17 +504,15 @@ cli
|
|
|
526
504
|
}
|
|
527
505
|
process.exit(1);
|
|
528
506
|
}
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const wallet = provider.wallet;
|
|
532
|
-
const pubkey = wallet.publicKey;
|
|
507
|
+
const adapter = createAdapter();
|
|
508
|
+
const walletAddr = await adapter.getWalletAddress();
|
|
533
509
|
if (!isJsonMode()) {
|
|
534
510
|
banner();
|
|
535
511
|
info("๐ Task:", String(taskId));
|
|
536
512
|
blank();
|
|
537
513
|
}
|
|
538
514
|
// Check registration
|
|
539
|
-
const agent = await
|
|
515
|
+
const agent = await adapter.getAgent(walletAddr);
|
|
540
516
|
if (!agent) {
|
|
541
517
|
if (isJsonMode()) {
|
|
542
518
|
jsonOutput({ ok: false, error: "Not registered. Run: pc init" });
|
|
@@ -560,55 +536,44 @@ cli
|
|
|
560
536
|
}
|
|
561
537
|
process.exit(1);
|
|
562
538
|
}
|
|
539
|
+
// Check task exists and eligibility
|
|
540
|
+
const task = await adapter.getTask(taskId);
|
|
541
|
+
if (!task) {
|
|
542
|
+
if (isJsonMode()) {
|
|
543
|
+
jsonOutput({ ok: false, error: `Task ${taskId} not found` });
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
fail(`Task ${taskId} not found`);
|
|
547
|
+
}
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
if (agent.efficiencyTier < task.minTier) {
|
|
551
|
+
const msg = `Task requires tier ${task.minTier}, but your tier is ${agent.efficiencyTier}`;
|
|
552
|
+
if (isJsonMode()) {
|
|
553
|
+
jsonOutput({ ok: false, error: msg });
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
fail(msg);
|
|
557
|
+
}
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
563
560
|
const spinner = isJsonMode() ? null : spin("Uploading proof to Storacha...");
|
|
564
561
|
try {
|
|
565
562
|
const proofCid = await uploadJson(proof, "data");
|
|
566
563
|
if (spinner)
|
|
567
564
|
spinner.text = "Submitting proof on-chain...";
|
|
568
|
-
const
|
|
569
|
-
const agentPda = getAgentPda(programClient.programId, pubkey);
|
|
570
|
-
const claimPda = getClaimPda(programClient.programId, taskId, pubkey);
|
|
571
|
-
const task = await programClient.account.taskRecord.fetch(taskPda);
|
|
572
|
-
if (agent.efficiencyTier < task.minTier) {
|
|
573
|
-
throw new Error(`Task requires tier ${task.minTier}, but your tier is ${agent.efficiencyTier}`);
|
|
574
|
-
}
|
|
575
|
-
const submitBuilder = programClient.methods
|
|
576
|
-
.submitProof(taskId, toFixedBytes(proofCid, 64))
|
|
577
|
-
.accounts({
|
|
578
|
-
protocol: getProtocolPda(programClient.programId),
|
|
579
|
-
task: taskPda,
|
|
580
|
-
agentAccount: agentPda,
|
|
581
|
-
claim: claimPda,
|
|
582
|
-
agent: pubkey,
|
|
583
|
-
systemProgram: anchor.web3.SystemProgram.programId,
|
|
584
|
-
});
|
|
585
|
-
if (task.requiredTaskId !== NO_PREREQ_TASK_ID) {
|
|
586
|
-
const prerequisiteClaimPda = getClaimPda(programClient.programId, task.requiredTaskId, pubkey);
|
|
587
|
-
const prerequisiteClaim = await provider.connection.getAccountInfo(prerequisiteClaimPda);
|
|
588
|
-
if (!prerequisiteClaim) {
|
|
589
|
-
throw new Error(`Task requires completing task ${task.requiredTaskId} first`);
|
|
590
|
-
}
|
|
591
|
-
submitBuilder.remainingAccounts([
|
|
592
|
-
{
|
|
593
|
-
pubkey: prerequisiteClaimPda,
|
|
594
|
-
isWritable: false,
|
|
595
|
-
isSigner: false,
|
|
596
|
-
},
|
|
597
|
-
]);
|
|
598
|
-
}
|
|
599
|
-
await submitBuilder.rpc();
|
|
600
|
-
const reward = task.rewardClips.toNumber();
|
|
565
|
+
const result = await adapter.submitProof(taskId, proofCid);
|
|
601
566
|
spinner?.succeed("Proof submitted!");
|
|
602
567
|
if (isJsonMode()) {
|
|
603
568
|
jsonOutput({
|
|
604
569
|
ok: true,
|
|
605
570
|
proof_cid: proofCid,
|
|
606
|
-
clips_awarded:
|
|
571
|
+
clips_awarded: result.clipsAwarded,
|
|
607
572
|
});
|
|
608
573
|
}
|
|
609
574
|
else {
|
|
610
|
-
info("๐ Proof CID:",
|
|
611
|
-
info("๐ Earned:", `${
|
|
575
|
+
info("๐ Proof CID:", shortKey(proofCid));
|
|
576
|
+
info("๐ Earned:", `${result.clipsAwarded} Clips`);
|
|
612
577
|
blank();
|
|
613
578
|
}
|
|
614
579
|
}
|
|
@@ -666,15 +631,15 @@ const configCmd = cli
|
|
|
666
631
|
.description("Show or manage configuration");
|
|
667
632
|
configCmd.action(() => {
|
|
668
633
|
const mode = getMode();
|
|
669
|
-
const
|
|
634
|
+
const server = resolveServerName();
|
|
635
|
+
const serverConfig = getServerConfig(server);
|
|
670
636
|
if (isJsonMode()) {
|
|
671
637
|
jsonOutput({
|
|
672
638
|
mode,
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
rpc_url:
|
|
676
|
-
|
|
677
|
-
program_id: PROGRAM_ID.toBase58(),
|
|
639
|
+
server,
|
|
640
|
+
chain: serverConfig?.chain ?? "unknown",
|
|
641
|
+
rpc_url: serverConfig?.rpcUrl ?? "unknown",
|
|
642
|
+
contract: serverConfig?.contractAddress ?? "unknown",
|
|
678
643
|
config_path: configPath(),
|
|
679
644
|
});
|
|
680
645
|
}
|
|
@@ -682,11 +647,10 @@ configCmd.action(() => {
|
|
|
682
647
|
banner();
|
|
683
648
|
heading("Configuration");
|
|
684
649
|
info("๐ง Mode:", mode);
|
|
685
|
-
info("
|
|
686
|
-
info("
|
|
687
|
-
info("๐ RPC:",
|
|
688
|
-
info("
|
|
689
|
-
info("๐งพ Program:", PROGRAM_ID.toBase58());
|
|
650
|
+
info("๐ฅ๏ธ Server:", server);
|
|
651
|
+
info("โ๏ธ Chain:", serverConfig?.chain ?? "unknown");
|
|
652
|
+
info("๐ RPC:", serverConfig?.rpcUrl ?? "unknown");
|
|
653
|
+
info("๐ Contract:", serverConfig?.contractAddress ?? "unknown");
|
|
690
654
|
info("๐ Config:", configPath());
|
|
691
655
|
blank();
|
|
692
656
|
}
|
|
@@ -695,13 +659,14 @@ configCmd
|
|
|
695
659
|
.command("get [key]")
|
|
696
660
|
.description("Get a config value or show all config")
|
|
697
661
|
.action((key) => {
|
|
662
|
+
const server = resolveServerName();
|
|
663
|
+
const serverConfig = getServerConfig(server);
|
|
698
664
|
const values = {
|
|
699
665
|
mode: getMode(),
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
rpc_url:
|
|
703
|
-
|
|
704
|
-
program_id: PROGRAM_ID.toBase58(),
|
|
666
|
+
server,
|
|
667
|
+
chain: serverConfig?.chain ?? "unknown",
|
|
668
|
+
rpc_url: serverConfig?.rpcUrl ?? "unknown",
|
|
669
|
+
contract: serverConfig?.contractAddress ?? "unknown",
|
|
705
670
|
config_path: configPath(),
|
|
706
671
|
};
|
|
707
672
|
if (!key) {
|
|
@@ -712,11 +677,10 @@ configCmd
|
|
|
712
677
|
banner();
|
|
713
678
|
heading("Configuration");
|
|
714
679
|
info("๐ง Mode:", values.mode);
|
|
715
|
-
info("
|
|
716
|
-
info("
|
|
680
|
+
info("๐ฅ๏ธ Server:", values.server);
|
|
681
|
+
info("โ๏ธ Chain:", values.chain);
|
|
717
682
|
info("๐ RPC:", values.rpc_url);
|
|
718
|
-
info("
|
|
719
|
-
info("๐งพ Program:", values.program_id);
|
|
683
|
+
info("๐ Contract:", values.contract);
|
|
720
684
|
info("๐ Config:", values.config_path);
|
|
721
685
|
blank();
|
|
722
686
|
}
|
|
@@ -727,11 +691,11 @@ configCmd
|
|
|
727
691
|
if (isJsonMode()) {
|
|
728
692
|
jsonOutput({
|
|
729
693
|
ok: false,
|
|
730
|
-
error:
|
|
694
|
+
error: `Unknown key. Valid keys: ${Object.keys(values).join(", ")}`,
|
|
731
695
|
});
|
|
732
696
|
}
|
|
733
697
|
else {
|
|
734
|
-
fail(
|
|
698
|
+
fail(`Unknown key. Valid keys: ${Object.keys(values).join(", ")}`);
|
|
735
699
|
}
|
|
736
700
|
process.exit(1);
|
|
737
701
|
}
|
|
@@ -748,7 +712,7 @@ configCmd
|
|
|
748
712
|
});
|
|
749
713
|
configCmd
|
|
750
714
|
.command("set <key> <value>")
|
|
751
|
-
.description("Set a config value (supported: mode, network)")
|
|
715
|
+
.description("Set a config value (supported: mode, network, server)")
|
|
752
716
|
.action((key, value) => {
|
|
753
717
|
const normalizedKey = key.toLowerCase().trim();
|
|
754
718
|
const normalizedValue = value.toLowerCase().trim();
|
|
@@ -794,11 +758,34 @@ configCmd
|
|
|
794
758
|
}
|
|
795
759
|
return;
|
|
796
760
|
}
|
|
761
|
+
if (normalizedKey === "server") {
|
|
762
|
+
const config = getServerConfig(normalizedValue);
|
|
763
|
+
if (!config) {
|
|
764
|
+
const available = listServers().map((s) => s.name).join(", ");
|
|
765
|
+
if (isJsonMode()) {
|
|
766
|
+
jsonOutput({ ok: false, error: `Unknown server. Available: ${available}` });
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
fail(`Unknown server. Available: ${available}`);
|
|
770
|
+
}
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
setServer(normalizedValue);
|
|
774
|
+
if (isJsonMode()) {
|
|
775
|
+
jsonOutput({ ok: true, key: "server", value: normalizedValue, chain: config.chain, label: config.label });
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
banner();
|
|
779
|
+
success(`Set server = ${normalizedValue} (${config.label})`);
|
|
780
|
+
blank();
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
797
784
|
if (isJsonMode()) {
|
|
798
|
-
jsonOutput({ ok: false, error: 'Unsupported key. Use "mode" or "
|
|
785
|
+
jsonOutput({ ok: false, error: 'Unsupported key. Use "mode", "network", or "server"' });
|
|
799
786
|
}
|
|
800
787
|
else {
|
|
801
|
-
fail('Unsupported key. Use "mode" or "
|
|
788
|
+
fail('Unsupported key. Use "mode", "network", or "server"');
|
|
802
789
|
}
|
|
803
790
|
process.exit(1);
|
|
804
791
|
});
|