@paper-clip/pc 0.1.4
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/baked-config.json +18 -0
- package/dist/bin.js +18 -0
- package/dist/client.js +82 -0
- package/dist/config.js +99 -0
- package/dist/index.js +812 -0
- package/dist/privy.js +243 -0
- package/dist/settings.js +75 -0
- package/dist/storacha.js +97 -0
- package/dist/types.js +4 -0
- package/dist/ui.js +127 -0
- package/idl/paperclip_protocol.json +1060 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paperclip Protocol CLI
|
|
3
|
+
*
|
|
4
|
+
* Human-friendly command-line interface for AI agents
|
|
5
|
+
* interacting with the Paperclip Protocol on Solana.
|
|
6
|
+
*/
|
|
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
|
+
import { fetchJson, uploadJson } from "./storacha.js";
|
|
12
|
+
import { banner, blank, fail, heading, info, parseError, spin, success, table, warn } from "./ui.js";
|
|
13
|
+
import { getMode, getNetwork, setMode, setNetwork, configPath, } from "./settings.js";
|
|
14
|
+
import { NETWORK, PROGRAM_ID, RPC_FALLBACK_URL, RPC_URL, WALLET_TYPE, } from "./config.js";
|
|
15
|
+
import { provisionPrivyWallet } from "./privy.js";
|
|
16
|
+
const TASK_IS_ACTIVE_OFFSET = 154;
|
|
17
|
+
const NO_PREREQ_TASK_ID = 0xffffffff;
|
|
18
|
+
function jsonOutput(data) {
|
|
19
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
20
|
+
}
|
|
21
|
+
function shortPubkey(key) {
|
|
22
|
+
if (key.length <= 12)
|
|
23
|
+
return key;
|
|
24
|
+
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
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
|
+
// =============================================================================
|
|
82
|
+
// CLI SETUP
|
|
83
|
+
// =============================================================================
|
|
84
|
+
const cli = new Command();
|
|
85
|
+
cli
|
|
86
|
+
.name("pc")
|
|
87
|
+
.description("Paperclip Protocol CLI โ earn ๐ Clips by completing tasks")
|
|
88
|
+
.version("0.1.4")
|
|
89
|
+
.option("-n, --network <network>", "Network to use (devnet|localnet)")
|
|
90
|
+
.option("--json", "Force JSON output (override mode)")
|
|
91
|
+
.option("--human", "Force human output (override mode)")
|
|
92
|
+
.option("--mock-storacha", "Use mock Storacha uploads (test only)");
|
|
93
|
+
function normalizeNetwork(value) {
|
|
94
|
+
const normalized = value.toLowerCase().trim();
|
|
95
|
+
if (normalized === "devnet" || normalized === "localnet") {
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function isJsonMode() {
|
|
101
|
+
// Explicit flags override saved config
|
|
102
|
+
if (cli.opts().json === true)
|
|
103
|
+
return true;
|
|
104
|
+
if (cli.opts().human === true)
|
|
105
|
+
return false;
|
|
106
|
+
// Otherwise use saved mode: agent=JSON, human=pretty
|
|
107
|
+
return getMode() === "agent";
|
|
108
|
+
}
|
|
109
|
+
function applyMockFlag() {
|
|
110
|
+
if (cli.opts().mockStoracha) {
|
|
111
|
+
process.env.PAPERCLIP_STORACHA_MOCK = "1";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function validateNetworkFlag() {
|
|
115
|
+
const requested = cli.opts().network;
|
|
116
|
+
if (!requested)
|
|
117
|
+
return;
|
|
118
|
+
if (normalizeNetwork(requested) !== null)
|
|
119
|
+
return;
|
|
120
|
+
if (isJsonMode()) {
|
|
121
|
+
jsonOutput({ ok: false, error: 'Network must be "devnet" or "localnet"' });
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
fail('Network must be "devnet" or "localnet"');
|
|
125
|
+
}
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
cli.hook("preAction", () => {
|
|
129
|
+
validateNetworkFlag();
|
|
130
|
+
});
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// INIT COMMAND
|
|
133
|
+
// =============================================================================
|
|
134
|
+
cli
|
|
135
|
+
.command("init")
|
|
136
|
+
.description("Register as an agent on the protocol")
|
|
137
|
+
.option("--invite <code>", "Invite code (inviter wallet pubkey)")
|
|
138
|
+
.action(async (opts) => {
|
|
139
|
+
applyMockFlag();
|
|
140
|
+
// If using Privy, auto-provision wallet on first init
|
|
141
|
+
if (WALLET_TYPE === "privy") {
|
|
142
|
+
const spinnerProvision = isJsonMode() ? null : spin("Provisioning server wallet...");
|
|
143
|
+
try {
|
|
144
|
+
await provisionPrivyWallet();
|
|
145
|
+
spinnerProvision?.succeed("Server wallet ready");
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
spinnerProvision?.fail("Failed to provision wallet");
|
|
149
|
+
if (isJsonMode()) {
|
|
150
|
+
jsonOutput({ ok: false, error: parseError(err) });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
fail(parseError(err));
|
|
154
|
+
blank();
|
|
155
|
+
}
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const programClient = await getProgram();
|
|
160
|
+
const provider = programClient.provider;
|
|
161
|
+
const wallet = provider.wallet;
|
|
162
|
+
const pubkey = wallet.publicKey;
|
|
163
|
+
if (!isJsonMode()) {
|
|
164
|
+
banner();
|
|
165
|
+
info("๐ค Wallet:", pubkey.toBase58());
|
|
166
|
+
blank();
|
|
167
|
+
}
|
|
168
|
+
// Check if already registered
|
|
169
|
+
const existing = await getAgentAccount(programClient, pubkey);
|
|
170
|
+
if (existing) {
|
|
171
|
+
if (isJsonMode()) {
|
|
172
|
+
jsonOutput({
|
|
173
|
+
ok: true,
|
|
174
|
+
already_registered: true,
|
|
175
|
+
agent_pubkey: pubkey.toBase58(),
|
|
176
|
+
clips_balance: existing.clipsBalance.toNumber(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
success("Already registered!");
|
|
181
|
+
info("๐ Clips:", existing.clipsBalance.toNumber());
|
|
182
|
+
info("โญ Tier:", existing.efficiencyTier);
|
|
183
|
+
info("โ
Tasks completed:", existing.tasksCompleted);
|
|
184
|
+
blank();
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Register
|
|
189
|
+
const spinner = isJsonMode() ? null : spin("Registering agent...");
|
|
190
|
+
try {
|
|
191
|
+
const protocolPda = getProtocolPda(programClient.programId);
|
|
192
|
+
const agentPda = getAgentPda(programClient.programId, pubkey);
|
|
193
|
+
if (opts.invite) {
|
|
194
|
+
let inviterPubkey;
|
|
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();
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
await programClient.methods
|
|
221
|
+
.registerAgent()
|
|
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);
|
|
231
|
+
spinner?.succeed("Agent registered!");
|
|
232
|
+
if (isJsonMode()) {
|
|
233
|
+
jsonOutput({
|
|
234
|
+
ok: true,
|
|
235
|
+
agent_pubkey: pubkey.toBase58(),
|
|
236
|
+
clips_balance: agent.clipsBalance.toNumber(),
|
|
237
|
+
invited_by: agent.invitedBy && !isZeroPubkey(agent.invitedBy)
|
|
238
|
+
? asPubkey(agent.invitedBy).toBase58()
|
|
239
|
+
: null,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
info("๐ Clips:", agent.clipsBalance.toNumber());
|
|
244
|
+
if (agent.invitedBy && !isZeroPubkey(agent.invitedBy)) {
|
|
245
|
+
info("๐ค Invited by:", asPubkey(agent.invitedBy).toBase58());
|
|
246
|
+
}
|
|
247
|
+
info("๐ Next:", "Run `pc tasks` to see available work");
|
|
248
|
+
blank();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
spinner?.fail("Registration failed");
|
|
253
|
+
if (isJsonMode()) {
|
|
254
|
+
jsonOutput({ ok: false, error: parseError(err) });
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
fail(parseError(err));
|
|
258
|
+
blank();
|
|
259
|
+
}
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// INVITE COMMAND
|
|
265
|
+
// =============================================================================
|
|
266
|
+
cli
|
|
267
|
+
.command("invite")
|
|
268
|
+
.description("Create (or show) your invite code")
|
|
269
|
+
.action(async () => {
|
|
270
|
+
applyMockFlag();
|
|
271
|
+
const programClient = await getProgram();
|
|
272
|
+
const provider = programClient.provider;
|
|
273
|
+
const wallet = provider.wallet;
|
|
274
|
+
const pubkey = wallet.publicKey;
|
|
275
|
+
const agentPda = getAgentPda(programClient.programId, pubkey);
|
|
276
|
+
const invitePda = getInvitePda(programClient.programId, pubkey);
|
|
277
|
+
const spinner = isJsonMode() ? null : spin("Preparing invite code...");
|
|
278
|
+
try {
|
|
279
|
+
await programClient.account.agentAccount.fetch(agentPda);
|
|
280
|
+
let invite = null;
|
|
281
|
+
try {
|
|
282
|
+
invite = await programClient.account.inviteRecord.fetch(invitePda);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
await programClient.methods
|
|
286
|
+
.createInvite()
|
|
287
|
+
.accounts({
|
|
288
|
+
protocol: getProtocolPda(programClient.programId),
|
|
289
|
+
agentAccount: agentPda,
|
|
290
|
+
inviteRecord: invitePda,
|
|
291
|
+
agent: pubkey,
|
|
292
|
+
systemProgram: anchor.web3.SystemProgram.programId,
|
|
293
|
+
})
|
|
294
|
+
.rpc();
|
|
295
|
+
invite = await programClient.account.inviteRecord.fetch(invitePda);
|
|
296
|
+
}
|
|
297
|
+
const inviteCode = new anchor.web3.PublicKey(invite.inviteCode).toBase58();
|
|
298
|
+
spinner?.succeed("Invite ready");
|
|
299
|
+
if (isJsonMode()) {
|
|
300
|
+
jsonOutput({
|
|
301
|
+
ok: true,
|
|
302
|
+
agent_pubkey: pubkey.toBase58(),
|
|
303
|
+
invite_code: inviteCode,
|
|
304
|
+
invites_redeemed: invite.invitesRedeemed,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
info("๐ Invite code:", inviteCode);
|
|
309
|
+
info("๐ฅ Redeemed:", invite.invitesRedeemed);
|
|
310
|
+
info("๐ Share:", `pc init --invite ${inviteCode}`);
|
|
311
|
+
blank();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
spinner?.fail("Failed to prepare invite");
|
|
316
|
+
if (isJsonMode()) {
|
|
317
|
+
jsonOutput({ ok: false, error: parseError(err) });
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
fail(parseError(err));
|
|
321
|
+
info("๐ Tip:", "Run `pc init` first to register your agent");
|
|
322
|
+
blank();
|
|
323
|
+
}
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
// =============================================================================
|
|
328
|
+
// STATUS COMMAND
|
|
329
|
+
// =============================================================================
|
|
330
|
+
cli
|
|
331
|
+
.command("status")
|
|
332
|
+
.description("Show your agent status and recommendations")
|
|
333
|
+
.action(async () => {
|
|
334
|
+
applyMockFlag();
|
|
335
|
+
const programClient = await getProgram();
|
|
336
|
+
const provider = programClient.provider;
|
|
337
|
+
const wallet = provider.wallet;
|
|
338
|
+
const pubkey = wallet.publicKey;
|
|
339
|
+
if (!isJsonMode()) {
|
|
340
|
+
banner();
|
|
341
|
+
}
|
|
342
|
+
const spinner = isJsonMode() ? null : spin("Loading agent status...");
|
|
343
|
+
try {
|
|
344
|
+
const agent = await getAgentAccount(programClient, pubkey);
|
|
345
|
+
if (!agent) {
|
|
346
|
+
spinner?.stop();
|
|
347
|
+
if (isJsonMode()) {
|
|
348
|
+
jsonOutput({
|
|
349
|
+
agent: null,
|
|
350
|
+
available_tasks: 0,
|
|
351
|
+
recommendation: "Not registered. Run: pc init",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
warn("Not registered yet");
|
|
356
|
+
info("๐ Next:", "Run `pc init` to get started");
|
|
357
|
+
blank();
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const doable = await listDoableTasks(programClient, pubkey, agent.efficiencyTier);
|
|
362
|
+
spinner?.stop();
|
|
363
|
+
if (isJsonMode()) {
|
|
364
|
+
const recommendation = doable.length > 0
|
|
365
|
+
? `${doable.length} tasks available. Run: pc tasks`
|
|
366
|
+
: "No tasks available. Check back later.";
|
|
367
|
+
jsonOutput({
|
|
368
|
+
agent: {
|
|
369
|
+
pubkey: pubkey.toBase58(),
|
|
370
|
+
clips: agent.clipsBalance.toNumber(),
|
|
371
|
+
tier: agent.efficiencyTier,
|
|
372
|
+
tasks_completed: agent.tasksCompleted,
|
|
373
|
+
},
|
|
374
|
+
available_tasks: doable.length,
|
|
375
|
+
recommendation,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
heading("Agent");
|
|
380
|
+
info("๐ค Wallet:", pubkey.toBase58());
|
|
381
|
+
info("๐ Clips:", agent.clipsBalance.toNumber());
|
|
382
|
+
info("โญ Tier:", agent.efficiencyTier);
|
|
383
|
+
info("โ
Completed:", `${agent.tasksCompleted} tasks`);
|
|
384
|
+
heading("Tasks");
|
|
385
|
+
if (doable.length > 0) {
|
|
386
|
+
info("๐ Available:", `${doable.length} tasks`);
|
|
387
|
+
info("๐ Next:", "Run `pc tasks` to browse");
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
info("๐ Available:", "None right now");
|
|
391
|
+
info("๐ก Tip:", "Check back later for new tasks");
|
|
392
|
+
}
|
|
393
|
+
blank();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
spinner?.fail("Failed to load status");
|
|
398
|
+
if (isJsonMode()) {
|
|
399
|
+
jsonOutput({ ok: false, error: parseError(err) });
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
fail(parseError(err));
|
|
403
|
+
blank();
|
|
404
|
+
}
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
// =============================================================================
|
|
409
|
+
// TASKS COMMAND
|
|
410
|
+
// =============================================================================
|
|
411
|
+
cli
|
|
412
|
+
.command("tasks")
|
|
413
|
+
.description("List available tasks you can complete")
|
|
414
|
+
.action(async () => {
|
|
415
|
+
applyMockFlag();
|
|
416
|
+
const programClient = await getProgram();
|
|
417
|
+
const provider = programClient.provider;
|
|
418
|
+
const wallet = provider.wallet;
|
|
419
|
+
const pubkey = wallet.publicKey;
|
|
420
|
+
if (!isJsonMode()) {
|
|
421
|
+
banner();
|
|
422
|
+
}
|
|
423
|
+
// Check registration
|
|
424
|
+
const agent = await getAgentAccount(programClient, pubkey);
|
|
425
|
+
if (!agent) {
|
|
426
|
+
if (isJsonMode()) {
|
|
427
|
+
jsonOutput({ ok: false, error: "Not registered. Run: pc init" });
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
warn("Not registered yet. Run `pc init` first.");
|
|
431
|
+
blank();
|
|
432
|
+
}
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const spinner = isJsonMode() ? null : spin("Fetching tasks...");
|
|
436
|
+
try {
|
|
437
|
+
const doable = await listDoableTasks(programClient, pubkey, agent.efficiencyTier);
|
|
438
|
+
if (doable.length === 0) {
|
|
439
|
+
spinner?.stop();
|
|
440
|
+
if (isJsonMode()) {
|
|
441
|
+
jsonOutput([]);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
info("๐", "No available tasks right now.");
|
|
445
|
+
info("๐ก Tip:", "Check back later for new tasks");
|
|
446
|
+
blank();
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
// Expand tasks with content from Storacha
|
|
451
|
+
const expanded = await Promise.all(doable.map(async (task) => {
|
|
452
|
+
const contentCid = fromFixedBytes(task.account.contentCid);
|
|
453
|
+
let content;
|
|
454
|
+
try {
|
|
455
|
+
content = await fetchJson(contentCid);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
content = null;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
taskId: task.account.taskId,
|
|
462
|
+
title: fromFixedBytes(task.account.title),
|
|
463
|
+
rewardClips: task.account.rewardClips.toNumber(),
|
|
464
|
+
maxClaims: task.account.maxClaims,
|
|
465
|
+
currentClaims: task.account.currentClaims,
|
|
466
|
+
minTier: task.account.minTier,
|
|
467
|
+
requiredTaskId: task.account.requiredTaskId === NO_PREREQ_TASK_ID
|
|
468
|
+
? null
|
|
469
|
+
: task.account.requiredTaskId,
|
|
470
|
+
contentCid,
|
|
471
|
+
content,
|
|
472
|
+
};
|
|
473
|
+
}));
|
|
474
|
+
spinner?.succeed(`Found ${expanded.length} task${expanded.length !== 1 ? "s" : ""}`);
|
|
475
|
+
if (isJsonMode()) {
|
|
476
|
+
jsonOutput(expanded);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
blank();
|
|
480
|
+
table(["ID", "Title", "Reward", "Tier", "Prereq", "Slots"], expanded.map((t) => [
|
|
481
|
+
t.taskId,
|
|
482
|
+
t.title.length > 20 ? t.title.slice(0, 17) + "..." : t.title,
|
|
483
|
+
`${t.rewardClips} ๐`,
|
|
484
|
+
t.minTier,
|
|
485
|
+
t.requiredTaskId === null ? "-" : t.requiredTaskId,
|
|
486
|
+
`${t.currentClaims}/${t.maxClaims}`,
|
|
487
|
+
]));
|
|
488
|
+
blank();
|
|
489
|
+
info("๐", "Run `pc do <task_id> --proof '{...}'` to submit");
|
|
490
|
+
blank();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
spinner?.fail("Failed to fetch tasks");
|
|
495
|
+
if (isJsonMode()) {
|
|
496
|
+
jsonOutput({ ok: false, error: parseError(err) });
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
fail(parseError(err));
|
|
500
|
+
blank();
|
|
501
|
+
}
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
// =============================================================================
|
|
506
|
+
// DO COMMAND
|
|
507
|
+
// =============================================================================
|
|
508
|
+
cli
|
|
509
|
+
.command("do")
|
|
510
|
+
.description("Submit proof of work for a task")
|
|
511
|
+
.argument("<task_id>", "Task ID to submit proof for")
|
|
512
|
+
.requiredOption("--proof <json>", "Proof JSON to submit")
|
|
513
|
+
.action(async (taskIdRaw, options) => {
|
|
514
|
+
applyMockFlag();
|
|
515
|
+
const taskId = Number(taskIdRaw);
|
|
516
|
+
if (!Number.isFinite(taskId)) {
|
|
517
|
+
if (isJsonMode()) {
|
|
518
|
+
jsonOutput({ ok: false, error: "task_id must be a number" });
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
fail("task_id must be a number");
|
|
522
|
+
}
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
const programClient = await getProgram();
|
|
526
|
+
const provider = programClient.provider;
|
|
527
|
+
const wallet = provider.wallet;
|
|
528
|
+
const pubkey = wallet.publicKey;
|
|
529
|
+
if (!isJsonMode()) {
|
|
530
|
+
banner();
|
|
531
|
+
info("๐ Task:", String(taskId));
|
|
532
|
+
blank();
|
|
533
|
+
}
|
|
534
|
+
// Check registration
|
|
535
|
+
const agent = await getAgentAccount(programClient, pubkey);
|
|
536
|
+
if (!agent) {
|
|
537
|
+
if (isJsonMode()) {
|
|
538
|
+
jsonOutput({ ok: false, error: "Not registered. Run: pc init" });
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
warn("Not registered yet. Run `pc init` first.");
|
|
542
|
+
blank();
|
|
543
|
+
}
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
let proof;
|
|
547
|
+
try {
|
|
548
|
+
proof = JSON.parse(options.proof);
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
if (isJsonMode()) {
|
|
552
|
+
jsonOutput({ ok: false, error: "Invalid proof JSON" });
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
fail("Invalid proof JSON โ must be valid JSON string");
|
|
556
|
+
}
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
const spinner = isJsonMode() ? null : spin("Uploading proof to Storacha...");
|
|
560
|
+
try {
|
|
561
|
+
const proofCid = await uploadJson(proof, "data");
|
|
562
|
+
if (spinner)
|
|
563
|
+
spinner.text = "Submitting proof on-chain...";
|
|
564
|
+
const taskPda = getTaskPda(programClient.programId, taskId);
|
|
565
|
+
const agentPda = getAgentPda(programClient.programId, pubkey);
|
|
566
|
+
const claimPda = getClaimPda(programClient.programId, taskId, pubkey);
|
|
567
|
+
const task = await programClient.account.taskRecord.fetch(taskPda);
|
|
568
|
+
if (agent.efficiencyTier < task.minTier) {
|
|
569
|
+
throw new Error(`Task requires tier ${task.minTier}, but your tier is ${agent.efficiencyTier}`);
|
|
570
|
+
}
|
|
571
|
+
const submitBuilder = programClient.methods
|
|
572
|
+
.submitProof(taskId, toFixedBytes(proofCid, 64))
|
|
573
|
+
.accounts({
|
|
574
|
+
protocol: getProtocolPda(programClient.programId),
|
|
575
|
+
task: taskPda,
|
|
576
|
+
agentAccount: agentPda,
|
|
577
|
+
claim: claimPda,
|
|
578
|
+
agent: pubkey,
|
|
579
|
+
systemProgram: anchor.web3.SystemProgram.programId,
|
|
580
|
+
});
|
|
581
|
+
if (task.requiredTaskId !== NO_PREREQ_TASK_ID) {
|
|
582
|
+
const prerequisiteClaimPda = getClaimPda(programClient.programId, task.requiredTaskId, pubkey);
|
|
583
|
+
const prerequisiteClaim = await provider.connection.getAccountInfo(prerequisiteClaimPda);
|
|
584
|
+
if (!prerequisiteClaim) {
|
|
585
|
+
throw new Error(`Task requires completing task ${task.requiredTaskId} first`);
|
|
586
|
+
}
|
|
587
|
+
submitBuilder.remainingAccounts([
|
|
588
|
+
{
|
|
589
|
+
pubkey: prerequisiteClaimPda,
|
|
590
|
+
isWritable: false,
|
|
591
|
+
isSigner: false,
|
|
592
|
+
},
|
|
593
|
+
]);
|
|
594
|
+
}
|
|
595
|
+
await submitBuilder.rpc();
|
|
596
|
+
const reward = task.rewardClips.toNumber();
|
|
597
|
+
spinner?.succeed("Proof submitted!");
|
|
598
|
+
if (isJsonMode()) {
|
|
599
|
+
jsonOutput({
|
|
600
|
+
ok: true,
|
|
601
|
+
proof_cid: proofCid,
|
|
602
|
+
clips_awarded: reward,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
info("๐ Proof CID:", shortPubkey(proofCid));
|
|
607
|
+
info("๐ Earned:", `${reward} Clips`);
|
|
608
|
+
blank();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
spinner?.fail("Submission failed");
|
|
613
|
+
if (isJsonMode()) {
|
|
614
|
+
jsonOutput({ ok: false, error: parseError(err) });
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
fail(parseError(err));
|
|
618
|
+
blank();
|
|
619
|
+
}
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
// =============================================================================
|
|
624
|
+
// SET COMMAND
|
|
625
|
+
// =============================================================================
|
|
626
|
+
cli
|
|
627
|
+
.command("set")
|
|
628
|
+
.description("Switch CLI mode")
|
|
629
|
+
.argument("<mode>", "Mode to set: agent or human")
|
|
630
|
+
.action((mode) => {
|
|
631
|
+
const normalized = mode.toLowerCase().trim();
|
|
632
|
+
if (normalized !== "agent" && normalized !== "human") {
|
|
633
|
+
if (isJsonMode()) {
|
|
634
|
+
jsonOutput({ ok: false, error: 'Mode must be "agent" or "human"' });
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
fail('Mode must be "agent" or "human"');
|
|
638
|
+
}
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
setMode(normalized);
|
|
642
|
+
if (normalized === "human") {
|
|
643
|
+
banner();
|
|
644
|
+
success("Switched to human mode");
|
|
645
|
+
info("๐จ", "Pretty output with colors and spinners");
|
|
646
|
+
info("๐ก", "Switch back with: pc set agent");
|
|
647
|
+
blank();
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
jsonOutput({
|
|
651
|
+
ok: true,
|
|
652
|
+
mode: "agent",
|
|
653
|
+
message: "Switched to agent mode โ JSON output only",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
// =============================================================================
|
|
658
|
+
// CONFIG COMMAND
|
|
659
|
+
// =============================================================================
|
|
660
|
+
const configCmd = cli
|
|
661
|
+
.command("config")
|
|
662
|
+
.description("Show or manage configuration");
|
|
663
|
+
configCmd.action(() => {
|
|
664
|
+
const mode = getMode();
|
|
665
|
+
const savedNetwork = getNetwork();
|
|
666
|
+
if (isJsonMode()) {
|
|
667
|
+
jsonOutput({
|
|
668
|
+
mode,
|
|
669
|
+
network: NETWORK,
|
|
670
|
+
saved_network: savedNetwork,
|
|
671
|
+
rpc_url: RPC_URL,
|
|
672
|
+
rpc_fallback_url: RPC_FALLBACK_URL,
|
|
673
|
+
program_id: PROGRAM_ID.toBase58(),
|
|
674
|
+
config_path: configPath(),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
banner();
|
|
679
|
+
heading("Configuration");
|
|
680
|
+
info("๐ง Mode:", mode);
|
|
681
|
+
info("๐ Network:", NETWORK);
|
|
682
|
+
info("๐ Saved network:", savedNetwork);
|
|
683
|
+
info("๐ RPC:", RPC_URL);
|
|
684
|
+
info("๐ RPC fallback:", RPC_FALLBACK_URL);
|
|
685
|
+
info("๐งพ Program:", PROGRAM_ID.toBase58());
|
|
686
|
+
info("๐ Config:", configPath());
|
|
687
|
+
blank();
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
configCmd
|
|
691
|
+
.command("get [key]")
|
|
692
|
+
.description("Get a config value or show all config")
|
|
693
|
+
.action((key) => {
|
|
694
|
+
const values = {
|
|
695
|
+
mode: getMode(),
|
|
696
|
+
network: getNetwork(),
|
|
697
|
+
effective_network: NETWORK,
|
|
698
|
+
rpc_url: RPC_URL,
|
|
699
|
+
rpc_fallback_url: RPC_FALLBACK_URL,
|
|
700
|
+
program_id: PROGRAM_ID.toBase58(),
|
|
701
|
+
config_path: configPath(),
|
|
702
|
+
};
|
|
703
|
+
if (!key) {
|
|
704
|
+
if (isJsonMode()) {
|
|
705
|
+
jsonOutput(values);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
banner();
|
|
709
|
+
heading("Configuration");
|
|
710
|
+
info("๐ง Mode:", values.mode);
|
|
711
|
+
info("๐ Saved network:", values.network);
|
|
712
|
+
info("๐ Effective network:", values.effective_network);
|
|
713
|
+
info("๐ RPC:", values.rpc_url);
|
|
714
|
+
info("๐ RPC fallback:", values.rpc_fallback_url);
|
|
715
|
+
info("๐งพ Program:", values.program_id);
|
|
716
|
+
info("๐ Config:", values.config_path);
|
|
717
|
+
blank();
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const normalized = key.toLowerCase().trim();
|
|
722
|
+
if (!(normalized in values)) {
|
|
723
|
+
if (isJsonMode()) {
|
|
724
|
+
jsonOutput({
|
|
725
|
+
ok: false,
|
|
726
|
+
error: 'Unknown key. Valid keys: mode, network, effective_network, rpc_url, rpc_fallback_url, program_id, config_path',
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
fail('Unknown key. Valid keys: mode, network, effective_network, rpc_url, rpc_fallback_url, program_id, config_path');
|
|
731
|
+
}
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
const resolvedValue = values[normalized];
|
|
735
|
+
if (isJsonMode()) {
|
|
736
|
+
jsonOutput({ key: normalized, value: resolvedValue });
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
banner();
|
|
740
|
+
heading("Configuration");
|
|
741
|
+
info(`๐ง ${normalized}:`, resolvedValue);
|
|
742
|
+
blank();
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
configCmd
|
|
746
|
+
.command("set <key> <value>")
|
|
747
|
+
.description("Set a config value (supported: mode, network)")
|
|
748
|
+
.action((key, value) => {
|
|
749
|
+
const normalizedKey = key.toLowerCase().trim();
|
|
750
|
+
const normalizedValue = value.toLowerCase().trim();
|
|
751
|
+
if (normalizedKey === "mode") {
|
|
752
|
+
if (normalizedValue !== "agent" && normalizedValue !== "human") {
|
|
753
|
+
if (isJsonMode()) {
|
|
754
|
+
jsonOutput({ ok: false, error: 'Mode must be "agent" or "human"' });
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
fail('Mode must be "agent" or "human"');
|
|
758
|
+
}
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
setMode(normalizedValue);
|
|
762
|
+
if (isJsonMode()) {
|
|
763
|
+
jsonOutput({ ok: true, key: "mode", value: normalizedValue });
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
banner();
|
|
767
|
+
success(`Set mode = ${normalizedValue}`);
|
|
768
|
+
blank();
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (normalizedKey === "network") {
|
|
773
|
+
if (normalizedValue !== "devnet" && normalizedValue !== "localnet") {
|
|
774
|
+
if (isJsonMode()) {
|
|
775
|
+
jsonOutput({ ok: false, error: 'Network must be "devnet" or "localnet"' });
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
fail('Network must be "devnet" or "localnet"');
|
|
779
|
+
}
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
setNetwork(normalizedValue);
|
|
783
|
+
if (isJsonMode()) {
|
|
784
|
+
jsonOutput({ ok: true, key: "network", value: normalizedValue });
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
banner();
|
|
788
|
+
success(`Set network = ${normalizedValue}`);
|
|
789
|
+
blank();
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (isJsonMode()) {
|
|
794
|
+
jsonOutput({ ok: false, error: 'Unsupported key. Use "mode" or "network"' });
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
fail('Unsupported key. Use "mode" or "network"');
|
|
798
|
+
}
|
|
799
|
+
process.exit(1);
|
|
800
|
+
});
|
|
801
|
+
// =============================================================================
|
|
802
|
+
// RUN
|
|
803
|
+
// =============================================================================
|
|
804
|
+
cli.parseAsync(process.argv).catch((err) => {
|
|
805
|
+
if (isJsonMode()) {
|
|
806
|
+
jsonOutput({ ok: false, error: parseError(err) });
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
fail(parseError(err));
|
|
810
|
+
}
|
|
811
|
+
process.exit(1);
|
|
812
|
+
});
|