naracli 1.0.17 → 1.0.22
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 +101 -114
- package/bin/nara-cli.ts +0 -20
- package/dist/nara-cli.mjs +49930 -2222
- package/index.ts +10 -58
- package/package.json +7 -6
- package/src/cli/commands/quest.ts +8 -7
- package/src/cli/commands/skills.ts +491 -0
- package/src/cli/commands/skillsInstall.ts +793 -0
- package/src/cli/commands/wallet.ts +13 -114
- package/src/cli/commands/zkid.ts +410 -0
- package/src/cli/index.ts +215 -9
- package/src/cli/prompts/searchMultiselect.ts +297 -0
- package/src/cli/types.ts +0 -138
- package/src/cli/utils/transaction.ts +1 -1
- package/src/cli/utils/validation.ts +0 -40
- package/src/cli/utils/wallet.ts +3 -1
- package/src/tests/helpers.ts +78 -0
- package/src/tests/skills.e2e.test.ts +126 -0
- package/src/tests/skills.test.ts +192 -0
- package/src/tests/test_skill.md +18 -0
- package/src/tests/zkid.e2e.test.ts +128 -0
- package/src/tests/zkid.test.ts +153 -0
- package/src/types/snarkjs.d.ts +4 -1
- package/dist/quest/nara_quest.json +0 -534
- package/dist/zk/answer_proof.wasm +0 -0
- package/dist/zk/answer_proof_final.zkey +0 -0
- package/src/cli/commands/config.ts +0 -125
- package/src/cli/commands/migrate.ts +0 -270
- package/src/cli/commands/pool.ts +0 -364
- package/src/cli/commands/swap.ts +0 -349
- package/src/cli/quest/nara_quest.json +0 -534
- package/src/cli/quest/nara_quest_types.ts +0 -540
- package/src/cli/zk/answer_proof.wasm +0 -0
- package/src/cli/zk/answer_proof_final.zkey +0 -0
- package/src/client.ts +0 -96
- package/src/config.ts +0 -132
- package/src/constants.ts +0 -35
- package/src/migrate.ts +0 -222
- package/src/pool.ts +0 -259
- package/src/quest.ts +0 -387
- package/src/swap.ts +0 -608
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skills add / remove / list / check / update
|
|
3
|
+
*
|
|
4
|
+
* Pulls skill content from the Nara chain and installs it into local
|
|
5
|
+
* AI-agent skill directories (same layout as nara-skills / agentskills.io).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
import { searchMultiselect, cancelSymbol } from "../prompts/searchMultiselect";
|
|
11
|
+
import { mkdir, writeFile, rm, readFile } from "node:fs/promises";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { Connection } from "@solana/web3.js";
|
|
16
|
+
import { getSkillInfo, getSkillContent, getSkillRecord } from "nara-sdk";
|
|
17
|
+
import { getRpcUrl } from "../utils/wallet";
|
|
18
|
+
import { formatOutput } from "../utils/output";
|
|
19
|
+
import type { GlobalOptions } from "../types";
|
|
20
|
+
|
|
21
|
+
// ─── Agent registry ──────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const home = homedir();
|
|
24
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join(home, ".config");
|
|
25
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
26
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
27
|
+
|
|
28
|
+
function openClawGlobalDir(): string {
|
|
29
|
+
if (existsSync(join(home, ".openclaw"))) return join(home, ".openclaw/skills");
|
|
30
|
+
if (existsSync(join(home, ".clawdbot"))) return join(home, ".clawdbot/skills");
|
|
31
|
+
if (existsSync(join(home, ".moltbot"))) return join(home, ".moltbot/skills");
|
|
32
|
+
return join(home, ".openclaw/skills");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AgentConfig {
|
|
36
|
+
displayName: string;
|
|
37
|
+
/** Relative path used for project-local installs */
|
|
38
|
+
projectDir: string;
|
|
39
|
+
/** Absolute path used for global installs */
|
|
40
|
+
globalDir: string;
|
|
41
|
+
detect: () => boolean;
|
|
42
|
+
/** Set false to exclude from the locked Universal section even if projectDir is .agents/skills */
|
|
43
|
+
showInUniversalList?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const AGENT_CONFIGS: Record<string, AgentConfig> = {
|
|
47
|
+
// ── Universal agents (share .agents/skills) ───────────────────
|
|
48
|
+
amp: {
|
|
49
|
+
displayName: "Amp",
|
|
50
|
+
projectDir: ".agents/skills",
|
|
51
|
+
globalDir: join(xdgConfig, "agents/skills"),
|
|
52
|
+
detect: () => existsSync(join(xdgConfig, "amp")),
|
|
53
|
+
},
|
|
54
|
+
cline: {
|
|
55
|
+
displayName: "Cline",
|
|
56
|
+
projectDir: ".agents/skills",
|
|
57
|
+
globalDir: join(home, ".agents/skills"),
|
|
58
|
+
detect: () => existsSync(join(home, ".cline")),
|
|
59
|
+
},
|
|
60
|
+
codex: {
|
|
61
|
+
displayName: "Codex",
|
|
62
|
+
projectDir: ".agents/skills",
|
|
63
|
+
globalDir: join(codexHome, "skills"),
|
|
64
|
+
detect: () => existsSync(codexHome) || existsSync("/etc/codex"),
|
|
65
|
+
},
|
|
66
|
+
cursor: {
|
|
67
|
+
displayName: "Cursor",
|
|
68
|
+
projectDir: ".agents/skills",
|
|
69
|
+
globalDir: join(home, ".cursor/skills"),
|
|
70
|
+
detect: () => existsSync(join(home, ".cursor")),
|
|
71
|
+
},
|
|
72
|
+
"gemini-cli": {
|
|
73
|
+
displayName: "Gemini CLI",
|
|
74
|
+
projectDir: ".agents/skills",
|
|
75
|
+
globalDir: join(home, ".gemini/skills"),
|
|
76
|
+
detect: () => existsSync(join(home, ".gemini")),
|
|
77
|
+
},
|
|
78
|
+
"github-copilot": {
|
|
79
|
+
displayName: "GitHub Copilot",
|
|
80
|
+
projectDir: ".agents/skills",
|
|
81
|
+
globalDir: join(home, ".copilot/skills"),
|
|
82
|
+
detect: () => existsSync(join(home, ".copilot")),
|
|
83
|
+
},
|
|
84
|
+
"kimi-cli": {
|
|
85
|
+
displayName: "Kimi Code CLI",
|
|
86
|
+
projectDir: ".agents/skills",
|
|
87
|
+
globalDir: join(home, ".config/agents/skills"),
|
|
88
|
+
detect: () => existsSync(join(home, ".kimi")),
|
|
89
|
+
},
|
|
90
|
+
opencode: {
|
|
91
|
+
displayName: "OpenCode",
|
|
92
|
+
projectDir: ".agents/skills",
|
|
93
|
+
globalDir: join(xdgConfig, "opencode/skills"),
|
|
94
|
+
detect: () => existsSync(join(xdgConfig, "opencode")),
|
|
95
|
+
},
|
|
96
|
+
replit: {
|
|
97
|
+
displayName: "Replit",
|
|
98
|
+
projectDir: ".agents/skills",
|
|
99
|
+
globalDir: join(xdgConfig, "agents/skills"),
|
|
100
|
+
detect: () => existsSync(join(process.cwd(), ".replit")),
|
|
101
|
+
showInUniversalList: false,
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// ── Additional agents (custom skill dirs) ─────────────────────
|
|
105
|
+
adal: {
|
|
106
|
+
displayName: "AdaL",
|
|
107
|
+
projectDir: ".adal/skills",
|
|
108
|
+
globalDir: join(home, ".adal/skills"),
|
|
109
|
+
detect: () => existsSync(join(home, ".adal")),
|
|
110
|
+
},
|
|
111
|
+
antigravity: {
|
|
112
|
+
displayName: "Antigravity",
|
|
113
|
+
projectDir: ".agent/skills",
|
|
114
|
+
globalDir: join(home, ".gemini/antigravity/skills"),
|
|
115
|
+
detect: () => existsSync(join(home, ".gemini/antigravity")),
|
|
116
|
+
},
|
|
117
|
+
augment: {
|
|
118
|
+
displayName: "Augment",
|
|
119
|
+
projectDir: ".augment/skills",
|
|
120
|
+
globalDir: join(home, ".augment/skills"),
|
|
121
|
+
detect: () => existsSync(join(home, ".augment")),
|
|
122
|
+
},
|
|
123
|
+
"claude-code": {
|
|
124
|
+
displayName: "Claude Code",
|
|
125
|
+
projectDir: ".claude/skills",
|
|
126
|
+
globalDir: join(claudeHome, "skills"),
|
|
127
|
+
detect: () => existsSync(claudeHome),
|
|
128
|
+
},
|
|
129
|
+
codebuddy: {
|
|
130
|
+
displayName: "CodeBuddy",
|
|
131
|
+
projectDir: ".codebuddy/skills",
|
|
132
|
+
globalDir: join(home, ".codebuddy/skills"),
|
|
133
|
+
detect: () => existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy")),
|
|
134
|
+
},
|
|
135
|
+
"command-code": {
|
|
136
|
+
displayName: "Command Code",
|
|
137
|
+
projectDir: ".commandcode/skills",
|
|
138
|
+
globalDir: join(home, ".commandcode/skills"),
|
|
139
|
+
detect: () => existsSync(join(home, ".commandcode")),
|
|
140
|
+
},
|
|
141
|
+
continue: {
|
|
142
|
+
displayName: "Continue",
|
|
143
|
+
projectDir: ".continue/skills",
|
|
144
|
+
globalDir: join(home, ".continue/skills"),
|
|
145
|
+
detect: () => existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue")),
|
|
146
|
+
},
|
|
147
|
+
cortex: {
|
|
148
|
+
displayName: "Cortex Code",
|
|
149
|
+
projectDir: ".cortex/skills",
|
|
150
|
+
globalDir: join(home, ".snowflake/cortex/skills"),
|
|
151
|
+
detect: () => existsSync(join(home, ".snowflake/cortex")),
|
|
152
|
+
},
|
|
153
|
+
crush: {
|
|
154
|
+
displayName: "Crush",
|
|
155
|
+
projectDir: ".crush/skills",
|
|
156
|
+
globalDir: join(home, ".config/crush/skills"),
|
|
157
|
+
detect: () => existsSync(join(home, ".config/crush")),
|
|
158
|
+
},
|
|
159
|
+
droid: {
|
|
160
|
+
displayName: "Droid",
|
|
161
|
+
projectDir: ".factory/skills",
|
|
162
|
+
globalDir: join(home, ".factory/skills"),
|
|
163
|
+
detect: () => existsSync(join(home, ".factory")),
|
|
164
|
+
},
|
|
165
|
+
goose: {
|
|
166
|
+
displayName: "Goose",
|
|
167
|
+
projectDir: ".goose/skills",
|
|
168
|
+
globalDir: join(xdgConfig, "goose/skills"),
|
|
169
|
+
detect: () => existsSync(join(xdgConfig, "goose")),
|
|
170
|
+
},
|
|
171
|
+
"iflow-cli": {
|
|
172
|
+
displayName: "iFlow CLI",
|
|
173
|
+
projectDir: ".iflow/skills",
|
|
174
|
+
globalDir: join(home, ".iflow/skills"),
|
|
175
|
+
detect: () => existsSync(join(home, ".iflow")),
|
|
176
|
+
},
|
|
177
|
+
junie: {
|
|
178
|
+
displayName: "Junie",
|
|
179
|
+
projectDir: ".junie/skills",
|
|
180
|
+
globalDir: join(home, ".junie/skills"),
|
|
181
|
+
detect: () => existsSync(join(home, ".junie")),
|
|
182
|
+
},
|
|
183
|
+
kilo: {
|
|
184
|
+
displayName: "Kilo Code",
|
|
185
|
+
projectDir: ".kilocode/skills",
|
|
186
|
+
globalDir: join(home, ".kilocode/skills"),
|
|
187
|
+
detect: () => existsSync(join(home, ".kilocode")),
|
|
188
|
+
},
|
|
189
|
+
"kiro-cli": {
|
|
190
|
+
displayName: "Kiro CLI",
|
|
191
|
+
projectDir: ".kiro/skills",
|
|
192
|
+
globalDir: join(home, ".kiro/skills"),
|
|
193
|
+
detect: () => existsSync(join(home, ".kiro")),
|
|
194
|
+
},
|
|
195
|
+
kode: {
|
|
196
|
+
displayName: "Kode",
|
|
197
|
+
projectDir: ".kode/skills",
|
|
198
|
+
globalDir: join(home, ".kode/skills"),
|
|
199
|
+
detect: () => existsSync(join(home, ".kode")),
|
|
200
|
+
},
|
|
201
|
+
mcpjam: {
|
|
202
|
+
displayName: "MCPJam",
|
|
203
|
+
projectDir: ".mcpjam/skills",
|
|
204
|
+
globalDir: join(home, ".mcpjam/skills"),
|
|
205
|
+
detect: () => existsSync(join(home, ".mcpjam")),
|
|
206
|
+
},
|
|
207
|
+
"mistral-vibe": {
|
|
208
|
+
displayName: "Mistral Vibe",
|
|
209
|
+
projectDir: ".vibe/skills",
|
|
210
|
+
globalDir: join(home, ".vibe/skills"),
|
|
211
|
+
detect: () => existsSync(join(home, ".vibe")),
|
|
212
|
+
},
|
|
213
|
+
mux: {
|
|
214
|
+
displayName: "Mux",
|
|
215
|
+
projectDir: ".mux/skills",
|
|
216
|
+
globalDir: join(home, ".mux/skills"),
|
|
217
|
+
detect: () => existsSync(join(home, ".mux")),
|
|
218
|
+
},
|
|
219
|
+
neovate: {
|
|
220
|
+
displayName: "Neovate",
|
|
221
|
+
projectDir: ".neovate/skills",
|
|
222
|
+
globalDir: join(home, ".neovate/skills"),
|
|
223
|
+
detect: () => existsSync(join(home, ".neovate")),
|
|
224
|
+
},
|
|
225
|
+
openclaw: {
|
|
226
|
+
displayName: "OpenClaw",
|
|
227
|
+
projectDir: "skills",
|
|
228
|
+
globalDir: openClawGlobalDir(),
|
|
229
|
+
detect: () =>
|
|
230
|
+
existsSync(join(home, ".openclaw")) ||
|
|
231
|
+
existsSync(join(home, ".clawdbot")) ||
|
|
232
|
+
existsSync(join(home, ".moltbot")),
|
|
233
|
+
},
|
|
234
|
+
openhands: {
|
|
235
|
+
displayName: "OpenHands",
|
|
236
|
+
projectDir: ".openhands/skills",
|
|
237
|
+
globalDir: join(home, ".openhands/skills"),
|
|
238
|
+
detect: () => existsSync(join(home, ".openhands")),
|
|
239
|
+
},
|
|
240
|
+
pi: {
|
|
241
|
+
displayName: "Pi",
|
|
242
|
+
projectDir: ".pi/skills",
|
|
243
|
+
globalDir: join(home, ".pi/agent/skills"),
|
|
244
|
+
detect: () => existsSync(join(home, ".pi/agent")),
|
|
245
|
+
},
|
|
246
|
+
pochi: {
|
|
247
|
+
displayName: "Pochi",
|
|
248
|
+
projectDir: ".pochi/skills",
|
|
249
|
+
globalDir: join(home, ".pochi/skills"),
|
|
250
|
+
detect: () => existsSync(join(home, ".pochi")),
|
|
251
|
+
},
|
|
252
|
+
qoder: {
|
|
253
|
+
displayName: "Qoder",
|
|
254
|
+
projectDir: ".qoder/skills",
|
|
255
|
+
globalDir: join(home, ".qoder/skills"),
|
|
256
|
+
detect: () => existsSync(join(home, ".qoder")),
|
|
257
|
+
},
|
|
258
|
+
"qwen-code": {
|
|
259
|
+
displayName: "Qwen Code",
|
|
260
|
+
projectDir: ".qwen/skills",
|
|
261
|
+
globalDir: join(home, ".qwen/skills"),
|
|
262
|
+
detect: () => existsSync(join(home, ".qwen")),
|
|
263
|
+
},
|
|
264
|
+
roo: {
|
|
265
|
+
displayName: "Roo Code",
|
|
266
|
+
projectDir: ".roo/skills",
|
|
267
|
+
globalDir: join(home, ".roo/skills"),
|
|
268
|
+
detect: () => existsSync(join(home, ".roo")),
|
|
269
|
+
},
|
|
270
|
+
trae: {
|
|
271
|
+
displayName: "Trae",
|
|
272
|
+
projectDir: ".trae/skills",
|
|
273
|
+
globalDir: join(home, ".trae/skills"),
|
|
274
|
+
detect: () => existsSync(join(home, ".trae")),
|
|
275
|
+
},
|
|
276
|
+
"trae-cn": {
|
|
277
|
+
displayName: "Trae CN",
|
|
278
|
+
projectDir: ".trae/skills",
|
|
279
|
+
globalDir: join(home, ".trae-cn/skills"),
|
|
280
|
+
detect: () => existsSync(join(home, ".trae-cn")),
|
|
281
|
+
},
|
|
282
|
+
windsurf: {
|
|
283
|
+
displayName: "Windsurf",
|
|
284
|
+
projectDir: ".windsurf/skills",
|
|
285
|
+
globalDir: join(home, ".codeium/windsurf/skills"),
|
|
286
|
+
detect: () => existsSync(join(home, ".codeium/windsurf")),
|
|
287
|
+
},
|
|
288
|
+
zencoder: {
|
|
289
|
+
displayName: "Zencoder",
|
|
290
|
+
projectDir: ".zencoder/skills",
|
|
291
|
+
globalDir: join(home, ".zencoder/skills"),
|
|
292
|
+
detect: () => existsSync(join(home, ".zencoder")),
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// ─── Lock file ───────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
interface SkillLockEntry {
|
|
299
|
+
chainVersion: number;
|
|
300
|
+
description: string | null;
|
|
301
|
+
installedAt: string;
|
|
302
|
+
updatedAt: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
interface SkillLock {
|
|
306
|
+
version: 1;
|
|
307
|
+
skills: Record<string, SkillLockEntry>;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getLockPath(global: boolean, cwd: string): string {
|
|
311
|
+
return global
|
|
312
|
+
? join(xdgConfig, "nara/skills-lock.json")
|
|
313
|
+
: join(cwd, ".nara/skills-lock.json");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function readLock(global: boolean, cwd: string): Promise<SkillLock> {
|
|
317
|
+
try {
|
|
318
|
+
const raw = await readFile(getLockPath(global, cwd), "utf-8");
|
|
319
|
+
return JSON.parse(raw) as SkillLock;
|
|
320
|
+
} catch {
|
|
321
|
+
return { version: 1, skills: {} };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function writeLock(lock: SkillLock, global: boolean, cwd: string): Promise<void> {
|
|
326
|
+
const lockPath = getLockPath(global, cwd);
|
|
327
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
328
|
+
await writeFile(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Agent helpers ───────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
function detectAgents(): string[] {
|
|
334
|
+
return Object.entries(AGENT_CONFIGS)
|
|
335
|
+
.filter(([, cfg]) => cfg.detect())
|
|
336
|
+
.map(([id]) => id);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function resolveInstallDirs(
|
|
340
|
+
agentIds: string[],
|
|
341
|
+
global: boolean,
|
|
342
|
+
cwd: string
|
|
343
|
+
): Array<{ agentId: string; displayName: string; dir: string }> {
|
|
344
|
+
return agentIds.map((id) => {
|
|
345
|
+
const cfg = AGENT_CONFIGS[id];
|
|
346
|
+
if (!cfg) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Unknown agent: "${id}". Valid agents: ${Object.keys(AGENT_CONFIGS).join(", ")}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
agentId: id,
|
|
353
|
+
displayName: cfg.displayName,
|
|
354
|
+
dir: global ? cfg.globalDir : join(cwd, cfg.projectDir),
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── File I/O helpers ────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
async function writeSkillFiles(
|
|
362
|
+
name: string,
|
|
363
|
+
content: Buffer,
|
|
364
|
+
targets: Array<{ dir: string }>
|
|
365
|
+
): Promise<string[]> {
|
|
366
|
+
const written: string[] = [];
|
|
367
|
+
for (const { dir } of targets) {
|
|
368
|
+
const skillDir = join(dir, name);
|
|
369
|
+
await mkdir(skillDir, { recursive: true });
|
|
370
|
+
await writeFile(join(skillDir, "SKILL.md"), content);
|
|
371
|
+
written.push(join(skillDir, "SKILL.md"));
|
|
372
|
+
}
|
|
373
|
+
return written;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function removeSkillFiles(
|
|
377
|
+
name: string,
|
|
378
|
+
targets: Array<{ dir: string }>
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
for (const { dir } of targets) {
|
|
381
|
+
await rm(join(dir, name), { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Command handlers ────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
type InstallOptions = GlobalOptions & { global?: boolean; agent?: string[]; yes?: boolean };
|
|
388
|
+
|
|
389
|
+
// ─── Helpers (mirrors nara-skills/src/add.ts) ────────────────────
|
|
390
|
+
|
|
391
|
+
function shortenPath(fullPath: string, cwd: string): string {
|
|
392
|
+
const h = homedir();
|
|
393
|
+
if (fullPath === h || fullPath.startsWith(h + "/")) return "~" + fullPath.slice(h.length);
|
|
394
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + "/")) return "." + fullPath.slice(cwd.length);
|
|
395
|
+
return fullPath;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ─── handleSkillsAdd ─────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
export async function handleSkillsAdd(name: string, options: InstallOptions) {
|
|
401
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
402
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
403
|
+
const cwd = process.cwd();
|
|
404
|
+
const nonInteractive = options.json || options.yes || !process.stdin.isTTY;
|
|
405
|
+
|
|
406
|
+
// ── Intro (matches nara-skills) ──────────────────────────────
|
|
407
|
+
if (!options.json) {
|
|
408
|
+
console.log();
|
|
409
|
+
p.intro(pc.bgCyan(pc.black(" skills ")));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── 1. Fetch skill info from chain ───────────────────────────
|
|
413
|
+
const fetchSpinner = p.spinner();
|
|
414
|
+
fetchSpinner.start("Fetching skill info...");
|
|
415
|
+
|
|
416
|
+
let info: Awaited<ReturnType<typeof getSkillInfo>>;
|
|
417
|
+
let content: Buffer | null;
|
|
418
|
+
try {
|
|
419
|
+
[info, content] = await Promise.all([
|
|
420
|
+
getSkillInfo(connection, name),
|
|
421
|
+
getSkillContent(connection, name),
|
|
422
|
+
]);
|
|
423
|
+
} catch (err: any) {
|
|
424
|
+
fetchSpinner.stop(pc.red("Failed to fetch skill"));
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!content) {
|
|
429
|
+
fetchSpinner.stop(pc.red("No content on chain"));
|
|
430
|
+
throw new Error(`Skill "${name}" has no content on chain. Upload content first with: skills upload`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fetchSpinner.stop(`Skill: ${pc.cyan(name)} v${info.record.version}`);
|
|
434
|
+
if (!options.json) {
|
|
435
|
+
p.log.message(pc.dim(info.description ?? "(no description)"));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── 2. Resolve agents ────────────────────────────────────────
|
|
439
|
+
let agentIds: string[];
|
|
440
|
+
|
|
441
|
+
if (options.agent?.length) {
|
|
442
|
+
const invalid = options.agent.filter((a) => !AGENT_CONFIGS[a]);
|
|
443
|
+
if (invalid.length) {
|
|
444
|
+
throw new Error(`Unknown agent(s): ${invalid.join(", ")}. Valid: ${Object.keys(AGENT_CONFIGS).join(", ")}`);
|
|
445
|
+
}
|
|
446
|
+
agentIds = options.agent;
|
|
447
|
+
p.log.info(`Installing to: ${agentIds.map((id) => pc.cyan(AGENT_CONFIGS[id]!.displayName)).join(", ")}`);
|
|
448
|
+
} else if (nonInteractive) {
|
|
449
|
+
const detected = detectAgents();
|
|
450
|
+
if (detected.length === 0) {
|
|
451
|
+
throw new Error("No supported agents detected. Use --agent to specify one (e.g. --agent claude-code)");
|
|
452
|
+
}
|
|
453
|
+
agentIds = detected;
|
|
454
|
+
p.log.info(`Installing to: ${agentIds.map((id) => pc.cyan(AGENT_CONFIGS[id]!.displayName)).join(", ")}`);
|
|
455
|
+
} else {
|
|
456
|
+
const agentSpinner = p.spinner();
|
|
457
|
+
agentSpinner.start("Loading agents...");
|
|
458
|
+
const detected = detectAgents();
|
|
459
|
+
agentSpinner.stop(`${Object.keys(AGENT_CONFIGS).length} agents`);
|
|
460
|
+
|
|
461
|
+
// Universal agents share .agents/skills (and have showInUniversalList !== false)
|
|
462
|
+
const universalIds = Object.entries(AGENT_CONFIGS)
|
|
463
|
+
.filter(([, c]) => c.projectDir === ".agents/skills" && c.showInUniversalList !== false)
|
|
464
|
+
.map(([id]) => id);
|
|
465
|
+
const additionalIds = Object.keys(AGENT_CONFIGS).filter((id) => !universalIds.includes(id));
|
|
466
|
+
|
|
467
|
+
const lockedSection = {
|
|
468
|
+
title: "Universal (.agents/skills)",
|
|
469
|
+
items: universalIds.map((id) => ({ value: id, label: AGENT_CONFIGS[id]!.displayName })),
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const searchItems = additionalIds.map((id) => ({
|
|
473
|
+
value: id,
|
|
474
|
+
label: AGENT_CONFIGS[id]!.displayName,
|
|
475
|
+
hint: AGENT_CONFIGS[id]!.projectDir,
|
|
476
|
+
}));
|
|
477
|
+
|
|
478
|
+
const result = await searchMultiselect({
|
|
479
|
+
message: "Which agents do you want to install to?",
|
|
480
|
+
items: searchItems,
|
|
481
|
+
initialSelected: detected.filter((id) => additionalIds.includes(id)),
|
|
482
|
+
required: false,
|
|
483
|
+
lockedSection,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (result === cancelSymbol) { p.cancel("Installation cancelled"); process.exit(0); }
|
|
487
|
+
agentIds = result as string[];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── 3. Scope ─────────────────────────────────────────────────
|
|
491
|
+
let installGlobally: boolean;
|
|
492
|
+
|
|
493
|
+
if (options.global !== undefined) {
|
|
494
|
+
installGlobally = options.global;
|
|
495
|
+
} else if (!nonInteractive) {
|
|
496
|
+
const scope = await p.select({
|
|
497
|
+
message: "Installation scope",
|
|
498
|
+
options: [
|
|
499
|
+
{ value: false, label: "Project", hint: "Install in current directory (committed with your project)" },
|
|
500
|
+
{ value: true, label: "Global", hint: "Install in home directory (available across all projects)" },
|
|
501
|
+
],
|
|
502
|
+
});
|
|
503
|
+
if (p.isCancel(scope)) { p.cancel("Installation cancelled"); process.exit(0); }
|
|
504
|
+
installGlobally = scope as boolean;
|
|
505
|
+
} else {
|
|
506
|
+
installGlobally = false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ── 4. Summary + confirmation ────────────────────────────────
|
|
510
|
+
const targets = resolveInstallDirs(agentIds, installGlobally, cwd);
|
|
511
|
+
|
|
512
|
+
if (!nonInteractive) {
|
|
513
|
+
const universalProjectDir = ".agents/skills";
|
|
514
|
+
const universalIds = Object.entries(AGENT_CONFIGS)
|
|
515
|
+
.filter(([, c]) => c.projectDir === universalProjectDir && c.showInUniversalList !== false)
|
|
516
|
+
.map(([id]) => id);
|
|
517
|
+
|
|
518
|
+
const universalTargets = targets.filter((t) => universalIds.includes(t.agentId));
|
|
519
|
+
const additionalTargets = targets.filter((t) => !universalIds.includes(t.agentId));
|
|
520
|
+
|
|
521
|
+
const summaryLines: string[] = [];
|
|
522
|
+
|
|
523
|
+
if (universalTargets.length > 0) {
|
|
524
|
+
const canonicalPath = join(universalTargets[0]!.dir, name);
|
|
525
|
+
summaryLines.push(`${pc.cyan(shortenPath(canonicalPath, cwd))}`);
|
|
526
|
+
summaryLines.push(
|
|
527
|
+
` ${pc.dim("──")} ${pc.bold("Universal (.agents/skills)")} ${pc.dim("── always included ────────────")}`
|
|
528
|
+
);
|
|
529
|
+
for (const t of universalTargets) {
|
|
530
|
+
summaryLines.push(` ${pc.green("•")} ${t.displayName}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (const t of additionalTargets) {
|
|
535
|
+
if (summaryLines.length > 0) summaryLines.push("");
|
|
536
|
+
const filePath = join(t.dir, name, "SKILL.md");
|
|
537
|
+
summaryLines.push(`${pc.cyan(shortenPath(filePath, cwd))}`);
|
|
538
|
+
summaryLines.push(` ${pc.green("•")} ${t.displayName}`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
p.note(summaryLines.join("\n"), "Installation Summary");
|
|
542
|
+
|
|
543
|
+
const confirmed = await p.confirm({ message: "Proceed with installation?" });
|
|
544
|
+
if (p.isCancel(confirmed) || !confirmed) { p.cancel("Installation cancelled"); process.exit(0); }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── 5. Install ───────────────────────────────────────────────
|
|
548
|
+
const installSpinner = p.spinner();
|
|
549
|
+
if (!options.json) installSpinner.start("Installing...");
|
|
550
|
+
|
|
551
|
+
const written = await writeSkillFiles(name, content, targets);
|
|
552
|
+
|
|
553
|
+
const lock = await readLock(installGlobally, cwd);
|
|
554
|
+
const now = new Date().toISOString();
|
|
555
|
+
lock.skills[name] = {
|
|
556
|
+
chainVersion: info.record.version,
|
|
557
|
+
description: info.description,
|
|
558
|
+
installedAt: lock.skills[name]?.installedAt ?? now,
|
|
559
|
+
updatedAt: now,
|
|
560
|
+
};
|
|
561
|
+
await writeLock(lock, installGlobally, cwd);
|
|
562
|
+
|
|
563
|
+
// ── 6. Result ────────────────────────────────────────────────
|
|
564
|
+
if (options.json) {
|
|
565
|
+
formatOutput({ name, chainVersion: info.record.version, agents: written }, true);
|
|
566
|
+
} else {
|
|
567
|
+
installSpinner.stop("Installation complete");
|
|
568
|
+
const resultLines = written.map((p) => `${pc.green("✓")} ${shortenPath(p, cwd)}`);
|
|
569
|
+
p.note(resultLines.join("\n"), pc.green(`Installed ${name}`));
|
|
570
|
+
console.log();
|
|
571
|
+
p.outro(pc.green("Done!") + pc.dim(" Review skills before use; they run with full agent permissions."));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export async function handleSkillsRemove(name: string, options: InstallOptions) {
|
|
576
|
+
const cwd = process.cwd();
|
|
577
|
+
const isGlobal = options.global ?? false;
|
|
578
|
+
const lock = await readLock(isGlobal, cwd);
|
|
579
|
+
|
|
580
|
+
if (!lock.skills[name]) {
|
|
581
|
+
throw new Error(`Skill "${name}" is not installed (not found in lock file)`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const agentIds = options.agent?.length ? options.agent : Object.keys(AGENT_CONFIGS);
|
|
585
|
+
const targets = resolveInstallDirs(agentIds, isGlobal, cwd);
|
|
586
|
+
await removeSkillFiles(name, targets);
|
|
587
|
+
|
|
588
|
+
delete lock.skills[name];
|
|
589
|
+
await writeLock(lock, isGlobal, cwd);
|
|
590
|
+
|
|
591
|
+
if (options.json) {
|
|
592
|
+
formatOutput({ name, removed: true }, true);
|
|
593
|
+
} else {
|
|
594
|
+
console.log(pc.green(`Skill "${name}" removed`));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export async function handleSkillsList(options: InstallOptions) {
|
|
599
|
+
const cwd = process.cwd();
|
|
600
|
+
const isGlobal = options.global ?? false;
|
|
601
|
+
const lock = await readLock(isGlobal, cwd);
|
|
602
|
+
const entries = Object.entries(lock.skills);
|
|
603
|
+
|
|
604
|
+
if (options.json) {
|
|
605
|
+
formatOutput(
|
|
606
|
+
entries.map(([n, e]) => ({
|
|
607
|
+
name: n,
|
|
608
|
+
chainVersion: e.chainVersion,
|
|
609
|
+
description: e.description,
|
|
610
|
+
updatedAt: e.updatedAt,
|
|
611
|
+
})),
|
|
612
|
+
true
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const scope = isGlobal ? "Global" : "Project";
|
|
618
|
+
if (entries.length === 0) {
|
|
619
|
+
console.log(pc.dim(`No ${scope.toLowerCase()} skills installed via naracli.`));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
console.log(`${pc.bold(scope + " Skills")}`);
|
|
624
|
+
console.log();
|
|
625
|
+
|
|
626
|
+
for (const [name] of entries) {
|
|
627
|
+
// Collect all agents that have this skill installed; use the first matching
|
|
628
|
+
// dir as the representative path (canonical), matching nara-skills display.
|
|
629
|
+
const installedAgents: string[] = [];
|
|
630
|
+
let canonicalPath = "";
|
|
631
|
+
for (const [, cfg] of Object.entries(AGENT_CONFIGS)) {
|
|
632
|
+
// Only include agents whose software is currently installed (mirrors nara-skills detectInstalledAgents)
|
|
633
|
+
if (!cfg.detect()) continue;
|
|
634
|
+
const dir = isGlobal ? cfg.globalDir : join(cwd, cfg.projectDir);
|
|
635
|
+
if (existsSync(join(dir, name, "SKILL.md"))) {
|
|
636
|
+
if (!canonicalPath) canonicalPath = shortenPath(join(dir, name), cwd);
|
|
637
|
+
installedAgents.push(cfg.displayName);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (installedAgents.length === 0) {
|
|
642
|
+
console.log(`${pc.cyan(name)} ${pc.dim("(files not found on disk)")}`);
|
|
643
|
+
} else {
|
|
644
|
+
const agentInfo =
|
|
645
|
+
installedAgents.length <= 5
|
|
646
|
+
? installedAgents.join(", ")
|
|
647
|
+
: `${installedAgents.slice(0, 5).join(", ")} +${installedAgents.length - 5} more`;
|
|
648
|
+
console.log(`${pc.cyan(name)} ${pc.dim(canonicalPath)}`);
|
|
649
|
+
console.log(` ${pc.dim("Agents:")} ${agentInfo}`);
|
|
650
|
+
}
|
|
651
|
+
console.log();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export async function handleSkillsCheck(options: InstallOptions) {
|
|
656
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
657
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
658
|
+
const cwd = process.cwd();
|
|
659
|
+
const isGlobal = options.global ?? false;
|
|
660
|
+
const lock = await readLock(isGlobal, cwd);
|
|
661
|
+
const entries = Object.entries(lock.skills);
|
|
662
|
+
|
|
663
|
+
if (entries.length === 0) {
|
|
664
|
+
if (options.json) {
|
|
665
|
+
formatOutput([], true);
|
|
666
|
+
} else {
|
|
667
|
+
console.log(pc.dim("No skills installed."));
|
|
668
|
+
}
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const spinner = p.spinner();
|
|
673
|
+
if (!options.json) spinner.start("Checking for updates...");
|
|
674
|
+
|
|
675
|
+
const results = await Promise.all(
|
|
676
|
+
entries.map(async ([n, local]) => {
|
|
677
|
+
try {
|
|
678
|
+
const record = await getSkillRecord(connection, n);
|
|
679
|
+
return {
|
|
680
|
+
name: n,
|
|
681
|
+
localVersion: local.chainVersion,
|
|
682
|
+
chainVersion: record.version,
|
|
683
|
+
updateAvailable: record.version > local.chainVersion,
|
|
684
|
+
};
|
|
685
|
+
} catch {
|
|
686
|
+
return { name: n, localVersion: local.chainVersion, chainVersion: null, updateAvailable: false };
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
if (!options.json) spinner.stop("Done");
|
|
692
|
+
|
|
693
|
+
if (options.json) {
|
|
694
|
+
formatOutput(results, true);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
console.log("");
|
|
699
|
+
const maxName = Math.max(...results.map((r) => r.name.length), 4);
|
|
700
|
+
for (const r of results) {
|
|
701
|
+
const padded = r.name.padEnd(maxName);
|
|
702
|
+
if (r.chainVersion === null) {
|
|
703
|
+
console.log(` ${pc.cyan(padded)} v${r.localVersion} ${pc.dim("(chain error)")}`);
|
|
704
|
+
} else if (r.updateAvailable) {
|
|
705
|
+
console.log(
|
|
706
|
+
` ${pc.cyan(padded)} v${r.localVersion} → v${r.chainVersion} ${pc.yellow("(update available)")}`
|
|
707
|
+
);
|
|
708
|
+
} else {
|
|
709
|
+
console.log(` ${pc.cyan(padded)} v${r.localVersion} ${pc.dim("(up to date)")}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
console.log("");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export async function handleSkillsUpdate(names: string[], options: InstallOptions) {
|
|
716
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
717
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
718
|
+
const cwd = process.cwd();
|
|
719
|
+
const isGlobal = options.global ?? false;
|
|
720
|
+
const lock = await readLock(isGlobal, cwd);
|
|
721
|
+
|
|
722
|
+
let toUpdate: string[];
|
|
723
|
+
|
|
724
|
+
if (names.length > 0) {
|
|
725
|
+
for (const n of names) {
|
|
726
|
+
if (!lock.skills[n]) throw new Error(`Skill "${n}" is not installed`);
|
|
727
|
+
}
|
|
728
|
+
toUpdate = names;
|
|
729
|
+
} else {
|
|
730
|
+
const spinner = p.spinner();
|
|
731
|
+
if (!options.json) spinner.start("Checking for updates...");
|
|
732
|
+
const checks = await Promise.all(
|
|
733
|
+
Object.keys(lock.skills).map(async (n) => {
|
|
734
|
+
try {
|
|
735
|
+
const record = await getSkillRecord(connection, n);
|
|
736
|
+
return { name: n, hasUpdate: record.version > lock.skills[n]!.chainVersion };
|
|
737
|
+
} catch {
|
|
738
|
+
return { name: n, hasUpdate: false };
|
|
739
|
+
}
|
|
740
|
+
})
|
|
741
|
+
);
|
|
742
|
+
if (!options.json) spinner.stop("Done");
|
|
743
|
+
toUpdate = checks.filter((c) => c.hasUpdate).map((c) => c.name);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (toUpdate.length === 0) {
|
|
747
|
+
if (options.json) {
|
|
748
|
+
formatOutput({ updated: [] }, true);
|
|
749
|
+
} else {
|
|
750
|
+
console.log(pc.green("All skills are up to date."));
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const agentIds = options.agent?.length ? options.agent : Object.keys(AGENT_CONFIGS);
|
|
756
|
+
const targets = resolveInstallDirs(agentIds, isGlobal, cwd);
|
|
757
|
+
const updated: Array<{ name: string; chainVersion: number; agents: string[] }> = [];
|
|
758
|
+
|
|
759
|
+
for (const n of toUpdate) {
|
|
760
|
+
const spinner = p.spinner();
|
|
761
|
+
if (!options.json) spinner.start(`Updating "${n}"...`);
|
|
762
|
+
|
|
763
|
+
const [info, content] = await Promise.all([
|
|
764
|
+
getSkillInfo(connection, n),
|
|
765
|
+
getSkillContent(connection, n),
|
|
766
|
+
]);
|
|
767
|
+
|
|
768
|
+
if (!content) {
|
|
769
|
+
if (!options.json) spinner.stop(pc.yellow(`"${n}" has no content on chain — skipped`));
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const written = await writeSkillFiles(n, content, targets);
|
|
774
|
+
const now = new Date().toISOString();
|
|
775
|
+
lock.skills[n] = {
|
|
776
|
+
chainVersion: info.record.version,
|
|
777
|
+
description: info.description,
|
|
778
|
+
installedAt: lock.skills[n]?.installedAt ?? now,
|
|
779
|
+
updatedAt: now,
|
|
780
|
+
};
|
|
781
|
+
updated.push({ name: n, chainVersion: info.record.version, agents: written });
|
|
782
|
+
|
|
783
|
+
if (!options.json) spinner.stop(pc.green(`"${n}" updated to v${info.record.version}`));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
await writeLock(lock, isGlobal, cwd);
|
|
787
|
+
|
|
788
|
+
if (options.json) {
|
|
789
|
+
formatOutput({ updated }, true);
|
|
790
|
+
} else if (updated.length > 0) {
|
|
791
|
+
console.log(pc.green(`\nUpdated ${updated.length} skill(s)`));
|
|
792
|
+
}
|
|
793
|
+
}
|