naracli 1.0.61 → 1.0.64

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.
@@ -118,6 +118,17 @@ async function handleQuestGet(options: GlobalOptions) {
118
118
  // If config fetch fails, fall back to showing stake as-is
119
119
  }
120
120
 
121
+ // Fetch free credits (stake-free answer quota)
122
+ let freeCredits = 0;
123
+ try {
124
+ const stakeInfo = await getStakeInfo(connection, wallet.publicKey);
125
+ if (stakeInfo) {
126
+ freeCredits = stakeInfo.freeCredits;
127
+ }
128
+ } catch {
129
+ // Ignore — wallet may not have a stake record
130
+ }
131
+
121
132
  const data: Record<string, any> = {
122
133
  round: quest.round,
123
134
  question: quest.question,
@@ -134,6 +145,7 @@ async function handleQuestGet(options: GlobalOptions) {
134
145
  stakeHigh: `${quest.stakeHigh} NARA`,
135
146
  stakeLow: `${quest.stakeLow} NARA`,
136
147
  avgParticipantStake: `${quest.avgParticipantStake} NARA`,
148
+ freeCredits,
137
149
  };
138
150
 
139
151
  if (options.json) {
@@ -153,6 +165,7 @@ async function handleQuestGet(options: GlobalOptions) {
153
165
  } else {
154
166
  console.log(` Stake requirement: none`);
155
167
  }
168
+ console.log(` Stake-free credits: ${freeCredits}`);
156
169
  console.log(` Deadline: ${new Date(quest.deadline * 1000).toLocaleString()}`);
157
170
  if (quest.timeRemaining > 0) {
158
171
  console.log(` Time remaining: ${formatTimeRemaining(quest.timeRemaining)}`);
@@ -202,10 +215,10 @@ async function handleQuestConfig(options: GlobalOptions) {
202
215
  console.log("");
203
216
  console.log(` Min Reward Count: ${data.minRewardCount}`);
204
217
  console.log(` Max Reward Count: ${data.maxRewardCount}`);
205
- console.log(` Reward Per Share: ${rewardPerShare} NARA`);
206
- console.log(` Extra Reward: ${extraReward} NARA`);
207
- console.log(` Stake BPS High: ${stakeBpsHigh / 100}%`);
208
- console.log(` Stake BPS Low: ${stakeBpsLow / 100}%`);
218
+ console.log(` Reward Per Share: ${rewardPerShare} NARA (${config.rewardPerShare} lamports)`);
219
+ console.log(` Extra Reward: ${extraReward} NARA (${config.extraReward} lamports)`);
220
+ console.log(` Stake BPS High: ${stakeBpsHigh / 100}% (${stakeBpsHigh} BPS)`);
221
+ console.log(` Stake BPS Low: ${stakeBpsLow / 100}% (${stakeBpsLow} BPS)`);
209
222
  console.log(` Decay (ms): ${data.decayMs}`);
210
223
  console.log(` Min Quest Interval: ${data.minQuestInterval}s`);
211
224
  console.log("");
@@ -220,7 +233,7 @@ async function handleQuestAnswer(
220
233
  const rpcUrl = getRpcUrl(options.rpcUrl);
221
234
  const connection = new Connection(rpcUrl, "confirmed");
222
235
  const wallet = await loadWallet(options.wallet);
223
- const networkConfig = loadNetworkConfig(rpcUrl);
236
+ const networkConfig = loadNetworkConfig(rpcUrl, wallet.publicKey.toBase58());
224
237
  const configAgentId = networkConfig.agent_id;
225
238
  const agent = options.agent ?? "naracli";
226
239
  const model = options.model ?? "";
@@ -13,7 +13,8 @@
13
13
  import { join } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
16
- import { DEFAULT_RPC_URL } from "nara-sdk";
16
+ import { Connection } from "@solana/web3.js";
17
+ import { DEFAULT_RPC_URL, getAgentInfo } from "nara-sdk";
17
18
 
18
19
  const CONFIG_DIR = join(homedir(), ".config", "nara");
19
20
  const GLOBAL_CONFIG_PATH = join(CONFIG_DIR, "config.json");
@@ -76,47 +77,112 @@ export interface NetworkConfig {
76
77
 
77
78
  const DEFAULT_NETWORK_CONFIG: NetworkConfig = { agent_id: "", zk_ids: [] };
78
79
 
79
- /**
80
- * Load network-specific config.
81
- * @param rpcUrl - effective RPC URL (determines which file to load)
82
- */
83
- export function loadNetworkConfig(rpcUrl?: string): NetworkConfig {
80
+ /** Read raw JSON from network config file. */
81
+ function loadRawNetworkConfig(rpcUrl?: string): Record<string, any> {
84
82
  const url = rpcUrl || getConfiguredRpcUrl();
85
83
  const path = networkConfigPath(url);
86
84
  try {
87
- const raw = readFileSync(path, "utf-8");
88
- const parsed = JSON.parse(raw);
89
- return {
90
- agent_id: typeof parsed.agent_id === "string" ? parsed.agent_id : "",
91
- zk_ids: Array.isArray(parsed.zk_ids) ? parsed.zk_ids : [],
92
- };
85
+ return JSON.parse(readFileSync(path, "utf-8"));
93
86
  } catch {
94
- return { ...DEFAULT_NETWORK_CONFIG };
87
+ return {};
95
88
  }
96
89
  }
97
90
 
91
+ /** Write raw JSON to network config file. */
92
+ function saveRawNetworkConfig(data: Record<string, any>, rpcUrl?: string): void {
93
+ const url = rpcUrl || getConfiguredRpcUrl();
94
+ const path = networkConfigPath(url);
95
+ mkdirSync(CONFIG_DIR, { recursive: true });
96
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
97
+ }
98
+
99
+ /**
100
+ * Load network-specific config.
101
+ * @param rpcUrl - effective RPC URL (determines which file to load)
102
+ * @param walletPubkey - wallet public key to look up agent_id (optional)
103
+ */
104
+ export function loadNetworkConfig(rpcUrl?: string, walletPubkey?: string): NetworkConfig {
105
+ const raw = loadRawNetworkConfig(rpcUrl);
106
+
107
+ // Resolve agent_id: new format uses wallet pubkey as key
108
+ let agent_id = "";
109
+ if (walletPubkey && typeof raw[walletPubkey] === "string") {
110
+ agent_id = raw[walletPubkey];
111
+ } else if (typeof raw.agent_id === "string" && raw.agent_id) {
112
+ // Legacy format: { "agent_id": "xxx" } — return value but don't migrate here
113
+ // Call migrateAgentIdFormat() to migrate with on-chain authority check
114
+ agent_id = raw.agent_id;
115
+ } else if (!walletPubkey) {
116
+ // No wallet provided — find first agent_id from any key (best-effort)
117
+ for (const [k, v] of Object.entries(raw)) {
118
+ if (k !== "zk_ids" && typeof v === "string" && v) {
119
+ agent_id = v;
120
+ break;
121
+ }
122
+ }
123
+ }
124
+
125
+ return {
126
+ agent_id,
127
+ zk_ids: Array.isArray(raw.zk_ids) ? raw.zk_ids : [],
128
+ };
129
+ }
130
+
98
131
  /**
99
132
  * Save network-specific config.
100
133
  */
101
134
  export function saveNetworkConfig(config: NetworkConfig, rpcUrl?: string): void {
102
- const url = rpcUrl || getConfiguredRpcUrl();
103
- const path = networkConfigPath(url);
104
- mkdirSync(CONFIG_DIR, { recursive: true });
105
- writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
135
+ const raw = loadRawNetworkConfig(rpcUrl);
136
+ // Preserve existing wallet->agentId mappings, update zk_ids
137
+ raw.zk_ids = config.zk_ids;
138
+ saveRawNetworkConfig(raw, rpcUrl);
106
139
  }
107
140
 
108
141
  // ─── Convenience helpers ─────────────────────────────────────────
109
142
 
110
- export function setAgentId(id: string, rpcUrl?: string): void {
111
- const config = loadNetworkConfig(rpcUrl);
112
- config.agent_id = id;
113
- saveNetworkConfig(config, rpcUrl);
143
+ export function setAgentId(id: string, rpcUrl?: string, walletPubkey?: string): void {
144
+ const raw = loadRawNetworkConfig(rpcUrl);
145
+ if (walletPubkey) {
146
+ raw[walletPubkey] = id;
147
+ // Clean up legacy field if present
148
+ delete raw.agent_id;
149
+ } else {
150
+ raw.agent_id = id;
151
+ }
152
+ saveRawNetworkConfig(raw, rpcUrl);
114
153
  }
115
154
 
116
- export function clearAgentId(rpcUrl?: string): void {
117
- const config = loadNetworkConfig(rpcUrl);
118
- config.agent_id = "";
119
- saveNetworkConfig(config, rpcUrl);
155
+ export function clearAgentId(rpcUrl?: string, walletPubkey?: string): void {
156
+ const raw = loadRawNetworkConfig(rpcUrl);
157
+ if (walletPubkey) {
158
+ delete raw[walletPubkey];
159
+ }
160
+ // Also clean up legacy field
161
+ delete raw.agent_id;
162
+ saveRawNetworkConfig(raw, rpcUrl);
163
+ }
164
+
165
+ /**
166
+ * Migrate legacy { "agent_id": "xxx" } to { "<authority-pubkey>": "xxx" }.
167
+ * Queries on-chain agent info to determine the authority.
168
+ * No-op if no legacy agent_id field exists.
169
+ */
170
+ export async function migrateAgentIdFormat(rpcUrl?: string): Promise<void> {
171
+ const url = rpcUrl || getConfiguredRpcUrl();
172
+ const raw = loadRawNetworkConfig(url);
173
+ if (typeof raw.agent_id !== "string" || !raw.agent_id) return;
174
+
175
+ const agentId = raw.agent_id;
176
+ try {
177
+ const connection = new Connection(url, "confirmed");
178
+ const info = await getAgentInfo(connection, agentId);
179
+ const authority = info.record.authority.toBase58();
180
+ raw[authority] = agentId;
181
+ delete raw.agent_id;
182
+ saveRawNetworkConfig(raw, url);
183
+ } catch {
184
+ // Agent not found on-chain or RPC error — keep legacy format for now
185
+ }
120
186
  }
121
187
 
122
188
  export function addZkId(name: string, rpcUrl?: string): void {