naracli 1.0.30 → 1.0.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naracli",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "CLI for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
@@ -53,7 +53,7 @@
53
53
  "bs58": "^6.0.0",
54
54
  "commander": "^12.1.0",
55
55
  "ed25519-hd-key": "^1.3.0",
56
- "nara-sdk": "^1.0.30",
56
+ "nara-sdk": "^1.0.32",
57
57
  "picocolors": "^1.1.1"
58
58
  }
59
59
  }
@@ -41,7 +41,7 @@ async function handleAgentRegister(agentId: string, options: GlobalOptions) {
41
41
  if (!options.json) printInfo(`Registering agent "${agentId}"...`);
42
42
  const result = await registerAgent(connection, wallet, agentId);
43
43
  if (!options.json) printSuccess(`Agent "${agentId}" registered!`);
44
- await addAgentId(agentId);
44
+ addAgentId(agentId);
45
45
 
46
46
  if (options.json) {
47
47
  formatOutput({ agentId, signature: result.signature, agentPubkey: result.agentPubkey.toBase58() }, true);
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Config commands - manage CLI configuration
3
+ */
4
+
5
+ import { Command } from "commander";
6
+ import { loadAgentConfig, saveAgentConfig } from "../utils/agent-config";
7
+ import { printError, printSuccess, formatOutput } from "../utils/output";
8
+ import { DEFAULT_RPC_URL } from "nara-sdk";
9
+ import type { GlobalOptions } from "../types";
10
+
11
+ function handleConfigGet(options: GlobalOptions) {
12
+ const config = loadAgentConfig();
13
+ const data = {
14
+ rpc_url: config.rpc_url ?? DEFAULT_RPC_URL,
15
+ wallet: config.wallet ?? "~/.config/nara/id.json",
16
+ rpc_url_custom: !!config.rpc_url,
17
+ wallet_custom: !!config.wallet,
18
+ };
19
+
20
+ if (options.json) {
21
+ formatOutput(data, true);
22
+ } else {
23
+ console.log("");
24
+ console.log(` RPC URL: ${data.rpc_url}${data.rpc_url_custom ? "" : " (default)"}`);
25
+ console.log(` Wallet: ${data.wallet}${data.wallet_custom ? "" : " (default)"}`);
26
+ console.log("");
27
+ }
28
+ }
29
+
30
+ function handleConfigSet(key: string, value: string, options: GlobalOptions) {
31
+ const config = loadAgentConfig();
32
+
33
+ switch (key) {
34
+ case "rpc-url":
35
+ config.rpc_url = value;
36
+ break;
37
+ case "wallet":
38
+ config.wallet = value;
39
+ break;
40
+ default:
41
+ throw new Error(`Unknown config key: "${key}". Valid keys: rpc-url, wallet`);
42
+ }
43
+
44
+ saveAgentConfig(config);
45
+ if (!options.json) printSuccess(`Config "${key}" set to "${value}"`);
46
+ if (options.json) formatOutput({ key, value }, true);
47
+ }
48
+
49
+ function handleConfigReset(key: string | undefined, options: GlobalOptions) {
50
+ const config = loadAgentConfig();
51
+
52
+ if (!key) {
53
+ delete config.rpc_url;
54
+ delete config.wallet;
55
+ saveAgentConfig(config);
56
+ if (!options.json) printSuccess("All config reset to defaults");
57
+ } else {
58
+ switch (key) {
59
+ case "rpc-url":
60
+ delete config.rpc_url;
61
+ break;
62
+ case "wallet":
63
+ delete config.wallet;
64
+ break;
65
+ default:
66
+ throw new Error(`Unknown config key: "${key}". Valid keys: rpc-url, wallet`);
67
+ }
68
+ saveAgentConfig(config);
69
+ if (!options.json) printSuccess(`Config "${key}" reset to default`);
70
+ }
71
+
72
+ if (options.json) formatOutput({ key: key ?? "all", reset: true }, true);
73
+ }
74
+
75
+ export function registerConfigCommands(program: Command): void {
76
+ const config = program
77
+ .command("config")
78
+ .description("Manage CLI configuration (rpc-url, wallet)");
79
+
80
+ config
81
+ .command("get")
82
+ .description("Show current configuration")
83
+ .action((_opts: any, cmd: Command) => {
84
+ try {
85
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
86
+ handleConfigGet(globalOpts);
87
+ } catch (error: any) {
88
+ printError(error.message);
89
+ process.exit(1);
90
+ }
91
+ });
92
+
93
+ config
94
+ .command("set <key> <value>")
95
+ .description("Set a config value (keys: rpc-url, wallet)")
96
+ .action((key: string, value: string, _opts: any, cmd: Command) => {
97
+ try {
98
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
99
+ handleConfigSet(key, value, globalOpts);
100
+ } catch (error: any) {
101
+ printError(error.message);
102
+ process.exit(1);
103
+ }
104
+ });
105
+
106
+ config
107
+ .command("reset [key]")
108
+ .description("Reset config to default (keys: rpc-url, wallet, or omit for all)")
109
+ .action((key: string | undefined, _opts: any, cmd: Command) => {
110
+ try {
111
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
112
+ handleConfigReset(key, globalOpts);
113
+ } catch (error: any) {
114
+ printError(error.message);
115
+ process.exit(1);
116
+ }
117
+ });
118
+ }
@@ -59,7 +59,7 @@ function formatTimeRemaining(seconds: number): string {
59
59
 
60
60
  // ─── Command: quest get ──────────────────────────────────────────
61
61
  async function handleQuestGet(options: GlobalOptions) {
62
- const rpcUrl = getRpcUrl(options.rpcUrl);
62
+ const rpcUrl = await getRpcUrl(options.rpcUrl);
63
63
  const connection = new Connection(rpcUrl, "confirmed");
64
64
 
65
65
  let wallet: Keypair;
@@ -126,10 +126,10 @@ async function handleQuestAnswer(
126
126
  answer: string,
127
127
  options: GlobalOptions & { relay?: string; agent?: string; model?: string; referral?: string }
128
128
  ) {
129
- const rpcUrl = getRpcUrl(options.rpcUrl);
129
+ const rpcUrl = await getRpcUrl(options.rpcUrl);
130
130
  const connection = new Connection(rpcUrl, "confirmed");
131
131
  const wallet = await loadWallet(options.wallet);
132
- const agentConfig = await loadAgentConfig();
132
+ const agentConfig = loadAgentConfig();
133
133
  const configAgentId = agentConfig.agent_ids[0];
134
134
  const agent = options.agent ?? "naracli";
135
135
  const model = options.model ?? "";
@@ -73,7 +73,7 @@ async function handleZkIdCreate(name: string, options: GlobalOptions) {
73
73
  if (!options.json) printInfo(`Registering ZK ID "${name}"...`);
74
74
  const signature = await createZkId(connection, wallet, name, idSecret);
75
75
  if (!options.json) printSuccess(`ZK ID "${name}" registered!`);
76
- await addZkId(name);
76
+ addZkId(name);
77
77
 
78
78
  if (options.json) {
79
79
  formatOutput({ name, signature }, true);
@@ -149,7 +149,7 @@ async function handleZkIdScan(
149
149
  if (name) {
150
150
  names = [name];
151
151
  } else {
152
- const config = await loadAgentConfig();
152
+ const config = loadAgentConfig();
153
153
  if (config.zk_ids.length === 0) {
154
154
  printError("No ZK IDs in config. Provide a name or create a ZK ID first.");
155
155
  process.exit(1);
package/src/cli/index.ts CHANGED
@@ -12,6 +12,7 @@ import { registerQuestCommands } from "./commands/quest";
12
12
  import { registerSkillsCommands } from "./commands/skills";
13
13
  import { registerZkIdCommands } from "./commands/zkid";
14
14
  import { registerAgentCommands } from "./commands/agent";
15
+ import { registerConfigCommands } from "./commands/config";
15
16
  import {
16
17
  handleWalletAddress,
17
18
  handleWalletBalance,
@@ -78,6 +79,9 @@ export function registerCommands(program: Command): void {
78
79
  // agent
79
80
  registerAgentCommands(program);
80
81
 
82
+ // config
83
+ registerConfigCommands(program);
84
+
81
85
  // Top-level: address
82
86
  program
83
87
  .command("address")
@@ -2,47 +2,49 @@
2
2
  * Agent config utilities - read/write ~/.config/nara/agent.json
3
3
  */
4
4
 
5
- import { join } from "node:path";
5
+ import { join, dirname } from "node:path";
6
6
  import { homedir } from "node:os";
7
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
7
8
 
8
9
  const AGENT_CONFIG_PATH = join(homedir(), ".config", "nara", "agent.json");
9
10
 
10
11
  export interface AgentConfig {
11
12
  agent_ids: string[];
12
13
  zk_ids: string[];
14
+ rpc_url?: string;
15
+ wallet?: string;
13
16
  }
14
17
 
15
18
  const DEFAULT_CONFIG: AgentConfig = { agent_ids: [], zk_ids: [] };
16
19
 
17
- export async function loadAgentConfig(): Promise<AgentConfig> {
18
- const fs = await import("node:fs/promises");
20
+ export function loadAgentConfig(): AgentConfig {
19
21
  try {
20
- const raw = await fs.readFile(AGENT_CONFIG_PATH, "utf-8");
22
+ const raw = readFileSync(AGENT_CONFIG_PATH, "utf-8");
21
23
  const parsed = JSON.parse(raw);
22
24
  return {
23
25
  agent_ids: Array.isArray(parsed.agent_ids) ? parsed.agent_ids : [],
24
26
  zk_ids: Array.isArray(parsed.zk_ids) ? parsed.zk_ids : [],
27
+ rpc_url: parsed.rpc_url ?? undefined,
28
+ wallet: parsed.wallet ?? undefined,
25
29
  };
26
30
  } catch {
27
31
  return { ...DEFAULT_CONFIG };
28
32
  }
29
33
  }
30
34
 
31
- export async function saveAgentConfig(config: AgentConfig): Promise<void> {
32
- const fs = await import("node:fs/promises");
33
- const { dirname } = await import("node:path");
34
- await fs.mkdir(dirname(AGENT_CONFIG_PATH), { recursive: true });
35
- await fs.writeFile(AGENT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
35
+ export function saveAgentConfig(config: AgentConfig): void {
36
+ mkdirSync(dirname(AGENT_CONFIG_PATH), { recursive: true });
37
+ writeFileSync(AGENT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
36
38
  }
37
39
 
38
- export async function addAgentId(id: string): Promise<void> {
39
- const config = await loadAgentConfig();
40
+ export function addAgentId(id: string): void {
41
+ const config = loadAgentConfig();
40
42
  config.agent_ids = [id, ...config.agent_ids.filter((x) => x !== id)];
41
- await saveAgentConfig(config);
43
+ saveAgentConfig(config);
42
44
  }
43
45
 
44
- export async function addZkId(name: string): Promise<void> {
45
- const config = await loadAgentConfig();
46
+ export function addZkId(name: string): void {
47
+ const config = loadAgentConfig();
46
48
  config.zk_ids = [name, ...config.zk_ids.filter((x) => x !== name)];
47
- await saveAgentConfig(config);
49
+ saveAgentConfig(config);
48
50
  }
@@ -6,38 +6,39 @@ import { Keypair } from "@solana/web3.js";
6
6
  import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { DEFAULT_RPC_URL } from "nara-sdk";
9
+ import { loadAgentConfig } from "./agent-config";
9
10
 
10
- const _DEFAULT_WALLET_PATH = process.env.WALLET_PATH || "~/.config/nara/id.json";
11
+ const DEFAULT_WALLET_PATH = join(homedir(), ".config", "nara", "id.json");
11
12
 
12
13
  /**
13
14
  * Resolve wallet path (expand ~ to home directory)
14
15
  */
15
- const DEFAULT_WALLET_PATH = _DEFAULT_WALLET_PATH.startsWith("~")
16
- ? join(homedir(), _DEFAULT_WALLET_PATH.slice(1))
17
- : _DEFAULT_WALLET_PATH;
16
+ function resolvePath(p: string): string {
17
+ return p.startsWith("~") ? join(homedir(), p.slice(1)) : p;
18
+ }
18
19
 
19
20
  /**
20
21
  * Load wallet keypair from file
21
22
  *
22
23
  * Priority:
23
24
  * 1. CLI flag (walletPath parameter)
24
- * 2. Default path (~/.config/nara/id.json)
25
- * 3. Error if neither exists
26
- *
27
- * @param walletPath Optional path to wallet keypair JSON file
28
- * @returns Keypair
29
- * @throws Error if wallet cannot be loaded
25
+ * 2. Config file (~/.config/nara/agent.json wallet field)
26
+ * 3. Default path (~/.config/nara/id.json)
30
27
  */
31
28
  export async function loadWallet(walletPath?: string): Promise<Keypair> {
32
- // Use provided path or default path
33
- const path = walletPath || DEFAULT_WALLET_PATH;
29
+ let path = walletPath;
30
+ if (!path) {
31
+ const config = loadAgentConfig();
32
+ path = config.wallet ? resolvePath(config.wallet) : DEFAULT_WALLET_PATH;
33
+ } else {
34
+ path = resolvePath(path);
35
+ }
34
36
 
35
37
  try {
36
38
  const fs = await import("node:fs/promises");
37
39
  const file = await fs.readFile(path, "utf-8");
38
40
  const data = JSON.parse(file);
39
41
 
40
- // Handle both array format [1,2,3,...] and object format
41
42
  if (Array.isArray(data)) {
42
43
  return Keypair.fromSecretKey(new Uint8Array(data));
43
44
  } else if (data.secretKey) {
@@ -59,15 +60,15 @@ export async function loadWallet(walletPath?: string): Promise<Keypair> {
59
60
  }
60
61
 
61
62
  /**
62
- * Get RPC URL from options
63
+ * Get RPC URL
63
64
  *
64
65
  * Priority:
65
66
  * 1. CLI flag (rpcUrl parameter)
66
- * 2. Default (from constants)
67
- *
68
- * @param rpcUrl Optional RPC URL from CLI flag
69
- * @returns RPC URL
67
+ * 2. Config file (~/.config/nara/agent.json rpc_url field)
68
+ * 3. Default (from SDK constants)
70
69
  */
71
70
  export function getRpcUrl(rpcUrl?: string): string {
72
- return rpcUrl || DEFAULT_RPC_URL;
71
+ if (rpcUrl) return rpcUrl;
72
+ const config = loadAgentConfig();
73
+ return config.rpc_url || DEFAULT_RPC_URL;
73
74
  }
@@ -5,6 +5,8 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { join, dirname } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { existsSync } from "node:fs";
9
+ import { homedir } from "node:os";
8
10
 
9
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
12
  const CLI = join(__dirname, "../../bin/nara-cli.ts");
@@ -49,7 +51,7 @@ export function runCli(args: string[], extraEnv: Record<string, string> = {}): P
49
51
  }
50
52
 
51
53
  /** Whether a wallet is configured (for write-command tests) */
52
- export const hasWallet = !!process.env.PRIVATE_KEY;
54
+ export const hasWallet = existsSync(join(homedir(), ".config", "nara", "id.json"));
53
55
 
54
56
  /** Generate a unique test resource name using a timestamp */
55
57
  export function uniqueName(prefix: string): string {
@@ -106,8 +106,8 @@ describe("quest proof generation", () => {
106
106
 
107
107
  // ─── On-chain quest answer ────────────────────────────────────────
108
108
 
109
- describe("quest answer (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : undefined }, () => {
110
- it("submits answer from test-questions", async () => {
109
+ describe("quest answer (on-chain)", { skip: !hasWallet ? "no wallet" : undefined }, () => {
110
+ it("submits answer from test-questions and outputs tx", async () => {
111
111
  const rpcUrl = process.env.RPC_URL || "https://mainnet-api.nara.build/";
112
112
  const connection = new Connection(rpcUrl, "confirmed");
113
113
 
@@ -137,9 +137,10 @@ describe("quest answer (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : unde
137
137
  "--json",
138
138
  ]);
139
139
 
140
- // exitCode 0 = success, but also handle "already answered" (exit 0 with warning)
141
140
  const output = stdout + stderr;
142
- if (output.includes("already answered")) {
141
+
142
+ // Handle known non-error cases
143
+ if (output.includes("already answered") || output.includes("Already answered")) {
143
144
  console.log(" Already answered this round");
144
145
  return;
145
146
  }
@@ -148,8 +149,23 @@ describe("quest answer (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : unde
148
149
  return;
149
150
  }
150
151
 
152
+ // Handle confirmation timeout - tx was sent but ws confirmation failed
153
+ const sigMatch = output.match(/Check signature (\w{80,})/);
154
+ if (sigMatch) {
155
+ console.log(` TX (confirmation timeout): ${sigMatch[1]}`);
156
+ return;
157
+ }
158
+
151
159
  assert.equal(exitCode, 0, `CLI failed: ${stderr}`);
152
- assert.ok(output.includes("Transaction:") || output.includes("signature"), "should show transaction");
153
- console.log(" Answer submitted successfully");
160
+
161
+ // Parse JSON output to get tx signature
162
+ try {
163
+ const data = JSON.parse(stdout);
164
+ assert.ok(data.signature, "should have signature in JSON output");
165
+ console.log(` TX: ${data.signature}`);
166
+ } catch {
167
+ assert.ok(output.includes("Transaction:") || output.includes("signature"), "should show transaction");
168
+ console.log(` Output: ${stdout.trim()}`);
169
+ }
154
170
  });
155
171
  });