@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/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, 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;
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 shortPubkey(key) {
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 โ€” earn ๐Ÿ“Ž Clips by completing tasks")
88
- .version("0.1.6")
89
- .option("-n, --network <network>", "Network to use (devnet|localnet)")
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 normalized = value.toLowerCase().trim();
95
- if (normalized === "devnet" || normalized === "localnet") {
96
- return normalized;
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
- // Explicit flags override saved config
102
- if (cli.opts().json === true)
46
+ const opts = cli.opts();
47
+ if (opts.json)
103
48
  return true;
104
- if (cli.opts().human === true)
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 requested = cli.opts().network;
116
- if (!requested)
117
- return;
118
- if (normalizeNetwork(requested) !== null)
59
+ const raw = cli.opts().network;
60
+ if (!raw)
119
61
  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"');
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 provisionPrivyWallet();
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 programClient = await getProgram();
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:", pubkey.toBase58());
179
+ info("๐Ÿ‘ค Wallet:", walletAddr);
180
+ info("๐Ÿ”— Server:", resolveServerName());
166
181
  blank();
167
182
  }
168
183
  // Check if already registered
169
- const existing = await getAgentAccount(programClient, pubkey);
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
- agent_pubkey: pubkey.toBase58(),
176
- clips_balance: existing.clipsBalance.toNumber(),
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.toNumber());
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
- const protocolPda = getProtocolPda(programClient.programId);
192
- const agentPda = getAgentPda(programClient.programId, pubkey);
206
+ let result;
193
207
  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();
208
+ result = await adapter.registerAgentWithInvite(opts.invite);
218
209
  }
219
210
  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);
211
+ result = await adapter.registerAgent();
212
+ }
231
213
  spinner?.succeed("Agent registered!");
232
214
  if (isJsonMode()) {
233
215
  jsonOutput({
234
216
  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,
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:", agent.clipsBalance.toNumber());
244
- if (agent.invitedBy && !isZeroPubkey(agent.invitedBy)) {
245
- info("๐Ÿค Invited by:", asPubkey(agent.invitedBy).toBase58());
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 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);
251
+ const adapter = createAdapter();
252
+ const walletAddr = await adapter.getWalletAddress();
277
253
  const spinner = isJsonMode() ? null : spin("Preparing invite code...");
278
254
  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");
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
- agent_pubkey: pubkey.toBase58(),
303
- invite_code: inviteCode,
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:", invite.invitesRedeemed);
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 programClient = await getProgram();
336
- const provider = programClient.provider;
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 getAgentAccount(programClient, pubkey);
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: "Not registered. Run: pc init",
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 get started");
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(programClient, pubkey, agent.efficiencyTier);
362
- spinner?.stop();
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
- pubkey: pubkey.toBase58(),
370
- clips: agent.clipsBalance.toNumber(),
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:", pubkey.toBase58());
381
- info("๐Ÿ“Ž Clips:", agent.clipsBalance.toNumber());
363
+ info("๐Ÿ‘ค Wallet:", walletAddr);
364
+ info("๐Ÿ“Ž Clips:", agent.clipsBalance);
382
365
  info("โญ Tier:", agent.efficiencyTier);
383
- info("โœ… Completed:", `${agent.tasksCompleted} tasks`);
384
- heading("Tasks");
366
+ info("โœ… Tasks completed:", agent.tasksCompleted);
367
+ blank();
368
+ heading("Recommendations");
385
369
  if (doable.length > 0) {
386
- info("๐Ÿ“‹ Available:", `${doable.length} tasks`);
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 programClient = await getProgram();
417
- const provider = programClient.provider;
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 getAgentAccount(programClient, pubkey);
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(programClient, pubkey, agent.efficiencyTier);
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
- // Human mode does not display task content payloads, so skip CID fetches.
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.account.taskId,
466
- title: fromFixedBytes(task.account.title),
467
- rewardClips: task.account.rewardClips.toNumber(),
468
- maxClaims: task.account.maxClaims,
469
- currentClaims: task.account.currentClaims,
470
- minTier: task.account.minTier,
471
- requiredTaskId: task.account.requiredTaskId === NO_PREREQ_TASK_ID
472
- ? null
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 programClient = await getProgram();
530
- const provider = programClient.provider;
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 getAgentAccount(programClient, pubkey);
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 taskPda = getTaskPda(programClient.programId, taskId);
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: reward,
571
+ clips_awarded: result.clipsAwarded,
607
572
  });
608
573
  }
609
574
  else {
610
- info("๐Ÿ”— Proof CID:", shortPubkey(proofCid));
611
- info("๐Ÿ“Ž Earned:", `${reward} Clips`);
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 savedNetwork = getNetwork();
634
+ const server = resolveServerName();
635
+ const serverConfig = getServerConfig(server);
670
636
  if (isJsonMode()) {
671
637
  jsonOutput({
672
638
  mode,
673
- network: NETWORK,
674
- saved_network: savedNetwork,
675
- rpc_url: RPC_URL,
676
- rpc_fallback_url: RPC_FALLBACK_URL,
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("๐ŸŒ Network:", NETWORK);
686
- info("๐Ÿ“ Saved network:", savedNetwork);
687
- info("๐Ÿ”— RPC:", RPC_URL);
688
- info("๐Ÿ›Ÿ RPC fallback:", RPC_FALLBACK_URL);
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
- network: getNetwork(),
701
- effective_network: NETWORK,
702
- rpc_url: RPC_URL,
703
- rpc_fallback_url: RPC_FALLBACK_URL,
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("๐Ÿ“ Saved network:", values.network);
716
- info("๐ŸŒ Effective network:", values.effective_network);
680
+ info("๐Ÿ–ฅ๏ธ Server:", values.server);
681
+ info("โ›“๏ธ Chain:", values.chain);
717
682
  info("๐Ÿ”— RPC:", values.rpc_url);
718
- info("๐Ÿ›Ÿ RPC fallback:", values.rpc_fallback_url);
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: 'Unknown key. Valid keys: mode, network, effective_network, rpc_url, rpc_fallback_url, program_id, config_path',
694
+ error: `Unknown key. Valid keys: ${Object.keys(values).join(", ")}`,
731
695
  });
732
696
  }
733
697
  else {
734
- fail('Unknown key. Valid keys: mode, network, effective_network, rpc_url, rpc_fallback_url, program_id, config_path');
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 "network"' });
785
+ jsonOutput({ ok: false, error: 'Unsupported key. Use "mode", "network", or "server"' });
799
786
  }
800
787
  else {
801
- fail('Unsupported key. Use "mode" or "network"');
788
+ fail('Unsupported key. Use "mode", "network", or "server"');
802
789
  }
803
790
  process.exit(1);
804
791
  });