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/README.md +10 -2
- package/dist/nara-cli-bundle.cjs +349 -134
- package/package.json +2 -2
- package/src/cli/commands/agent.ts +1 -1
- package/src/cli/commands/config.ts +118 -0
- package/src/cli/commands/quest.ts +3 -3
- package/src/cli/commands/zkid.ts +2 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/utils/agent-config.ts +17 -15
- package/src/cli/utils/wallet.ts +20 -19
- package/src/tests/helpers.ts +3 -1
- package/src/tests/quest.test.ts +22 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naracli",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 ?? "";
|
package/src/cli/commands/zkid.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
18
|
-
const fs = await import("node:fs/promises");
|
|
20
|
+
export function loadAgentConfig(): AgentConfig {
|
|
19
21
|
try {
|
|
20
|
-
const raw =
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
39
|
-
const config =
|
|
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
|
-
|
|
43
|
+
saveAgentConfig(config);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export
|
|
45
|
-
const config =
|
|
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
|
-
|
|
49
|
+
saveAgentConfig(config);
|
|
48
50
|
}
|
package/src/cli/utils/wallet.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
16
|
-
? join(homedir(),
|
|
17
|
-
|
|
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.
|
|
25
|
-
* 3.
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
63
|
+
* Get RPC URL
|
|
63
64
|
*
|
|
64
65
|
* Priority:
|
|
65
66
|
* 1. CLI flag (rpcUrl parameter)
|
|
66
|
-
* 2.
|
|
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
|
-
|
|
71
|
+
if (rpcUrl) return rpcUrl;
|
|
72
|
+
const config = loadAgentConfig();
|
|
73
|
+
return config.rpc_url || DEFAULT_RPC_URL;
|
|
73
74
|
}
|
package/src/tests/helpers.ts
CHANGED
|
@@ -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 =
|
|
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 {
|
package/src/tests/quest.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
});
|