@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/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
+ });