akm-cli 0.0.0 → 0.0.16

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/cli.js ADDED
@@ -0,0 +1,912 @@
1
+ #!/usr/bin/env bun
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { defineCommand, runMain } from "citty";
5
+ import { resolveStashDir } from "./common";
6
+ import { getConfigPath, loadConfig, saveConfig } from "./config";
7
+ import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
8
+ import { ConfigError, NotFoundError, UsageError } from "./errors";
9
+ import { agentikitIndex } from "./indexer";
10
+ import { agentikitInit } from "./init";
11
+ import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
12
+ import { checkForUpdate, performUpgrade } from "./self-update";
13
+ import { agentikitAdd } from "./stash-add";
14
+ import { agentikitClone } from "./stash-clone";
15
+ import { agentikitList, agentikitRemove, agentikitUpdate } from "./stash-registry";
16
+ import { agentikitSearch } from "./stash-search";
17
+ import { agentikitShow } from "./stash-show";
18
+ import { resolveStashSources } from "./stash-source";
19
+ import { setQuiet, warn } from "./warn";
20
+ // Version: prefer compile-time define, then package.json, then fallback
21
+ const pkgVersion = (() => {
22
+ // Injected at compile time via `bun build --define`
23
+ if (typeof AKM_VERSION !== "undefined")
24
+ return AKM_VERSION;
25
+ try {
26
+ const pkgPath = path.resolve(import.meta.dir ?? __dirname, "../package.json");
27
+ if (fs.existsSync(pkgPath)) {
28
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
29
+ if (typeof pkg.version === "string")
30
+ return pkg.version;
31
+ }
32
+ }
33
+ catch {
34
+ // swallow — running as compiled binary without package.json
35
+ }
36
+ return "0.0.0-dev";
37
+ })();
38
+ const OUTPUT_FORMATS = ["json", "yaml", "text"];
39
+ const DETAIL_LEVELS = ["brief", "normal", "full"];
40
+ const BRIEF_DESCRIPTION_LIMIT = 160;
41
+ function hasBunYAML(b) {
42
+ // biome-ignore lint/suspicious/noExplicitAny: type guard for runtime feature detection
43
+ return typeof b.YAML?.stringify === "function";
44
+ }
45
+ /** Try Bun.YAML.stringify; fall back to JSON if the API is unavailable */
46
+ function yamlStringify(obj) {
47
+ if (hasBunYAML(Bun)) {
48
+ return Bun.YAML.stringify(obj);
49
+ }
50
+ warn("YAML output not available, using JSON");
51
+ return JSON.stringify(obj, null, 2);
52
+ }
53
+ function parseOutputFormat(value) {
54
+ if (!value)
55
+ return undefined;
56
+ if (OUTPUT_FORMATS.includes(value))
57
+ return value;
58
+ throw new UsageError(`Invalid value for --format: ${value}. Expected one of: ${OUTPUT_FORMATS.join("|")}`);
59
+ }
60
+ function parseDetailLevel(value) {
61
+ if (!value)
62
+ return undefined;
63
+ if (DETAIL_LEVELS.includes(value))
64
+ return value;
65
+ throw new UsageError(`Invalid value for --detail: ${value}. Expected one of: ${DETAIL_LEVELS.join("|")}`);
66
+ }
67
+ function parseFlagValue(flag) {
68
+ for (let i = 0; i < process.argv.length; i++) {
69
+ const arg = process.argv[i];
70
+ if (arg === flag)
71
+ return process.argv[i + 1];
72
+ if (arg.startsWith(`${flag}=`))
73
+ return arg.slice(flag.length + 1);
74
+ }
75
+ return undefined;
76
+ }
77
+ function resolveOutputMode() {
78
+ const config = loadConfig();
79
+ const format = parseOutputFormat(parseFlagValue("--format")) ?? config.output?.format ?? "json";
80
+ const detail = parseDetailLevel(parseFlagValue("--detail")) ?? config.output?.detail ?? "brief";
81
+ return { format, detail };
82
+ }
83
+ function output(command, result) {
84
+ const mode = resolveOutputMode();
85
+ const shaped = shapeForCommand(command, result, mode.detail);
86
+ switch (mode.format) {
87
+ case "json":
88
+ console.log(JSON.stringify(shaped, null, 2));
89
+ return;
90
+ case "yaml":
91
+ console.log(yamlStringify(shaped));
92
+ return;
93
+ case "text": {
94
+ const plain = formatPlain(command, shaped, mode.detail);
95
+ console.log(plain ?? JSON.stringify(shaped, null, 2));
96
+ return;
97
+ }
98
+ }
99
+ }
100
+ function shapeForCommand(command, result, detail) {
101
+ switch (command) {
102
+ case "search":
103
+ return shapeSearchOutput(result, detail);
104
+ case "show":
105
+ return shapeShowOutput(result, detail);
106
+ default:
107
+ return result;
108
+ }
109
+ }
110
+ function shapeSearchOutput(result, detail) {
111
+ const hits = Array.isArray(result.hits) ? result.hits : [];
112
+ const shapedHits = hits.map((hit) => shapeSearchHit(hit, detail));
113
+ if (detail === "full") {
114
+ return {
115
+ schemaVersion: result.schemaVersion,
116
+ stashDir: result.stashDir,
117
+ source: result.source,
118
+ hits: shapedHits,
119
+ ...(result.tip ? { tip: result.tip } : {}),
120
+ ...(result.warnings ? { warnings: result.warnings } : {}),
121
+ ...(result.timing ? { timing: result.timing } : {}),
122
+ };
123
+ }
124
+ return {
125
+ hits: shapedHits,
126
+ ...(result.tip ? { tip: result.tip } : {}),
127
+ ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
128
+ };
129
+ }
130
+ function shapeSearchHit(hit, detail) {
131
+ // Keep local and registry hit models separate internally so search and
132
+ // ranking logic can carry source-specific metadata. Normalize the external
133
+ // contract here so default CLI output stays compact and consistent.
134
+ if (hit.type === "registry") {
135
+ const brief = withTruncatedDescription(pickFields(hit, ["type", "name", "id", "description", "action", "curated"]));
136
+ if (detail === "brief")
137
+ return brief;
138
+ if (detail === "normal")
139
+ return pickFields(hit, ["type", "name", "id", "description", "tags", "action", "curated"]);
140
+ return hit;
141
+ }
142
+ const brief = withTruncatedDescription(pickFields(hit, ["type", "name", "description", "action"]));
143
+ if (detail === "brief")
144
+ return brief;
145
+ if (detail === "normal") {
146
+ return pickFields(hit, ["type", "name", "ref", "origin", "description", "tags", "size", "action", "run"]);
147
+ }
148
+ return hit;
149
+ }
150
+ function withTruncatedDescription(hit) {
151
+ if (typeof hit.description !== "string")
152
+ return hit;
153
+ return {
154
+ ...hit,
155
+ description: truncateDescription(hit.description, BRIEF_DESCRIPTION_LIMIT),
156
+ };
157
+ }
158
+ function truncateDescription(description, limit) {
159
+ const normalized = description.replace(/\s+/g, " ").trim();
160
+ if (normalized.length <= limit)
161
+ return normalized;
162
+ const truncated = normalized.slice(0, limit - 1);
163
+ const lastSpace = truncated.lastIndexOf(" ");
164
+ const safe = lastSpace >= Math.floor(limit * 0.6) ? truncated.slice(0, lastSpace) : truncated;
165
+ return `${safe.trimEnd()}...`;
166
+ }
167
+ function shapeShowOutput(result, detail) {
168
+ const base = pickFields(result, [
169
+ "type",
170
+ "name",
171
+ "origin",
172
+ "action",
173
+ "description",
174
+ "content",
175
+ "template",
176
+ "prompt",
177
+ "toolPolicy",
178
+ "modelHint",
179
+ "agent",
180
+ "parameters",
181
+ "run",
182
+ "setup",
183
+ "cwd",
184
+ ]);
185
+ if (detail !== "full") {
186
+ return base;
187
+ }
188
+ return {
189
+ schemaVersion: 1,
190
+ ...base,
191
+ ...pickFields(result, ["path", "editable", "editHint"]),
192
+ };
193
+ }
194
+ function pickFields(source, fields) {
195
+ const result = {};
196
+ for (const field of fields) {
197
+ if (source[field] !== undefined) {
198
+ result[field] = source[field];
199
+ }
200
+ }
201
+ return result;
202
+ }
203
+ /**
204
+ * Return a plain-text string for commands that are better as short messages,
205
+ * or null to fall through to YAML output.
206
+ */
207
+ function formatPlain(command, result, detail) {
208
+ const r = result;
209
+ switch (command) {
210
+ case "init": {
211
+ let out = `Stash initialized at ${r.stashDir ?? "unknown"}`;
212
+ if (r.configPath)
213
+ out += `\nConfig saved to ${r.configPath}`;
214
+ return out;
215
+ }
216
+ case "index": {
217
+ return `Indexed ${r.totalEntries ?? 0} entries from ${r.directoriesScanned ?? 0} directories (mode: ${r.mode ?? "unknown"})`;
218
+ }
219
+ case "show": {
220
+ const lines = [];
221
+ if (r.type || r.name) {
222
+ lines.push(`# ${String(r.type ?? "asset")}: ${String(r.name ?? "unknown")}`);
223
+ }
224
+ if (r.origin !== undefined)
225
+ lines.push(`# origin: ${String(r.origin)}`);
226
+ if (r.action)
227
+ lines.push(`# ${String(r.action)}`);
228
+ if (r.description)
229
+ lines.push(`description: ${String(r.description)}`);
230
+ if (r.agent)
231
+ lines.push(`agent: ${String(r.agent)}`);
232
+ if (Array.isArray(r.parameters) && r.parameters.length > 0)
233
+ lines.push(`parameters: ${r.parameters.join(", ")}`);
234
+ if (r.modelHint != null)
235
+ lines.push(`modelHint: ${String(r.modelHint)}`);
236
+ if (r.toolPolicy != null)
237
+ lines.push(`toolPolicy: ${JSON.stringify(r.toolPolicy)}`);
238
+ if (r.run)
239
+ lines.push(`run: ${String(r.run)}`);
240
+ if (r.setup)
241
+ lines.push(`setup: ${String(r.setup)}`);
242
+ if (r.cwd)
243
+ lines.push(`cwd: ${String(r.cwd)}`);
244
+ if (detail === "full") {
245
+ if (r.path)
246
+ lines.push(`path: ${String(r.path)}`);
247
+ if (r.editable !== undefined)
248
+ lines.push(`editable: ${String(r.editable)}`);
249
+ if (r.editHint)
250
+ lines.push(`editHint: ${String(r.editHint)}`);
251
+ if (r.schemaVersion !== undefined)
252
+ lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
253
+ }
254
+ const payloads = [r.content, r.template, r.prompt].filter((value) => value != null).map(String);
255
+ if (payloads.length > 0) {
256
+ if (lines.length > 0)
257
+ lines.push("");
258
+ lines.push(...payloads);
259
+ }
260
+ return lines.length > 0 ? lines.join("\n") : null;
261
+ }
262
+ case "search": {
263
+ return formatSearchPlain(r, detail);
264
+ }
265
+ case "add": {
266
+ const index = r.index;
267
+ const scanned = index?.directoriesScanned ?? 0;
268
+ const total = index?.totalEntries ?? 0;
269
+ return `Installed ${r.ref} (${scanned} directories scanned, ${total} total assets indexed)`;
270
+ }
271
+ case "remove": {
272
+ const target = r.target ?? r.ref ?? "";
273
+ const ok = r.ok !== false ? "OK" : "FAILED";
274
+ return `remove: ${target} ${ok}`;
275
+ }
276
+ case "update": {
277
+ const processed = r.processed;
278
+ if (!processed?.length)
279
+ return `update: nothing to update`;
280
+ const lines = processed.map((item) => {
281
+ const changed = item.changed;
282
+ const installed = item.installed;
283
+ const previous = item.previous;
284
+ if (changed?.any) {
285
+ const prev = previous?.resolvedVersion ?? "unknown";
286
+ const next = installed?.resolvedVersion ?? "unknown";
287
+ return `update: ${item.id} v${prev} → v${next}`;
288
+ }
289
+ return `update: ${item.id} (unchanged)`;
290
+ });
291
+ return lines.join("\n");
292
+ }
293
+ case "upgrade": {
294
+ if (r.upgraded === true) {
295
+ return `akm upgraded: v${r.currentVersion} → v${r.newVersion}`;
296
+ }
297
+ if (r.updateAvailable === true) {
298
+ return `akm v${r.currentVersion} → v${r.latestVersion} available (run 'akm upgrade' to install)`;
299
+ }
300
+ if (r.updateAvailable === false && r.latestVersion) {
301
+ return `akm v${r.currentVersion} is already the latest version`;
302
+ }
303
+ if (r.message)
304
+ return String(r.message);
305
+ return null;
306
+ }
307
+ case "clone": {
308
+ const dst = r.destination?.path ?? "unknown";
309
+ const remote = r.remoteFetched ? " (fetched from remote)" : "";
310
+ const over = r.overwritten ? " (overwritten)" : "";
311
+ return `Cloned${remote} → ${dst}${over}`;
312
+ }
313
+ default:
314
+ return null; // fall through to YAML
315
+ }
316
+ }
317
+ function formatSearchPlain(r, detail) {
318
+ const hits = r.hits ?? [];
319
+ if (hits.length === 0) {
320
+ return r.tip ? String(r.tip) : "No results found.";
321
+ }
322
+ const lines = [];
323
+ for (const hit of hits) {
324
+ const type = hit.type ?? "unknown";
325
+ const name = hit.name ?? "unnamed";
326
+ const score = hit.score != null ? ` (score: ${hit.score})` : "";
327
+ const desc = hit.description ? ` ${hit.description}` : "";
328
+ lines.push(`${type}: ${name}${score}`);
329
+ if (desc)
330
+ lines.push(desc);
331
+ if (hit.id)
332
+ lines.push(` id: ${String(hit.id)}`);
333
+ if (hit.ref)
334
+ lines.push(` ref: ${String(hit.ref)}`);
335
+ if (hit.origin !== undefined)
336
+ lines.push(` origin: ${String(hit.origin)}`);
337
+ if (hit.size)
338
+ lines.push(` size: ${String(hit.size)}`);
339
+ if (hit.action)
340
+ lines.push(` action: ${String(hit.action)}`);
341
+ if (hit.run)
342
+ lines.push(` run: ${String(hit.run)}`);
343
+ if (Array.isArray(hit.tags) && hit.tags.length > 0)
344
+ lines.push(` tags: ${hit.tags.join(", ")}`);
345
+ if (hit.curated !== undefined)
346
+ lines.push(` curated: ${String(hit.curated)}`);
347
+ if (detail === "full") {
348
+ if (hit.path)
349
+ lines.push(` path: ${String(hit.path)}`);
350
+ if (hit.editable != null)
351
+ lines.push(` editable: ${String(hit.editable)}`);
352
+ if (hit.editHint)
353
+ lines.push(` editHint: ${String(hit.editHint)}`);
354
+ const whyMatched = hit.whyMatched;
355
+ if (whyMatched && whyMatched.length > 0) {
356
+ lines.push(` whyMatched: ${whyMatched.join(", ")}`);
357
+ }
358
+ }
359
+ lines.push(""); // blank line between hits
360
+ }
361
+ if (detail === "full" && r.timing) {
362
+ const timing = r.timing;
363
+ const parts = [];
364
+ if (timing.totalMs != null)
365
+ parts.push(`total: ${timing.totalMs}ms`);
366
+ if (timing.rankMs != null)
367
+ parts.push(`rank: ${timing.rankMs}ms`);
368
+ if (timing.embedMs != null)
369
+ parts.push(`embed: ${timing.embedMs}ms`);
370
+ if (parts.length > 0)
371
+ lines.push(`timing: ${parts.join(", ")}`);
372
+ }
373
+ return lines.join("\n").trimEnd();
374
+ }
375
+ const initCommand = defineCommand({
376
+ meta: {
377
+ name: "init",
378
+ description: "Initialize Agent-i-Kit's working stash directory and persist stashDir in config",
379
+ },
380
+ args: {
381
+ dir: { type: "string", description: "Custom stash directory path (default: ~/akm)" },
382
+ },
383
+ async run({ args }) {
384
+ await runWithJsonErrors(async () => {
385
+ const result = await agentikitInit({ dir: args.dir });
386
+ output("init", result);
387
+ });
388
+ },
389
+ });
390
+ const indexCommand = defineCommand({
391
+ meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
392
+ args: {
393
+ full: { type: "boolean", description: "Force full reindex", default: false },
394
+ },
395
+ async run({ args }) {
396
+ await runWithJsonErrors(async () => {
397
+ const result = await agentikitIndex({ full: args.full });
398
+ output("index", result);
399
+ });
400
+ },
401
+ });
402
+ const searchCommand = defineCommand({
403
+ meta: { name: "search", description: "Search the stash" },
404
+ args: {
405
+ query: { type: "positional", description: "Search query (omit to list all assets)", required: false, default: "" },
406
+ type: {
407
+ type: "string",
408
+ description: "Asset type filter (skill|command|agent|knowledge|script|any). 'tool' is accepted as alias for 'script'.",
409
+ },
410
+ limit: { type: "string", description: "Maximum number of results" },
411
+ source: { type: "string", description: "Search source (local|registry|both)", default: "local" },
412
+ format: { type: "string", description: "Output format (json|text|yaml)" },
413
+ detail: { type: "string", description: "Detail level (brief|normal|full)" },
414
+ },
415
+ async run({ args }) {
416
+ await runWithJsonErrors(async () => {
417
+ const type = args.type;
418
+ const limit = args.limit ? parseInt(args.limit, 10) : undefined;
419
+ const source = parseSearchSource(args.source);
420
+ const result = await agentikitSearch({ query: args.query, type, limit, source });
421
+ output("search", result);
422
+ });
423
+ },
424
+ });
425
+ const addCommand = defineCommand({
426
+ meta: { name: "add", description: "Install a kit from npm, GitHub, any git host, or a local directory" },
427
+ args: {
428
+ ref: {
429
+ type: "positional",
430
+ description: "Registry ref (npm package, owner/repo, git URL, or local directory)",
431
+ required: true,
432
+ },
433
+ },
434
+ async run({ args }) {
435
+ await runWithJsonErrors(async () => {
436
+ const result = await agentikitAdd({ ref: args.ref });
437
+ output("add", result);
438
+ });
439
+ },
440
+ });
441
+ const listCommand = defineCommand({
442
+ meta: { name: "list", description: "List installed registry packages from config" },
443
+ async run() {
444
+ await runWithJsonErrors(async () => {
445
+ const result = await agentikitList();
446
+ output("list", result);
447
+ });
448
+ },
449
+ });
450
+ const removeCommand = defineCommand({
451
+ meta: { name: "remove", description: "Remove an installed registry package by id or ref" },
452
+ args: {
453
+ target: { type: "positional", description: "Installed target (id or ref)", required: true },
454
+ },
455
+ async run({ args }) {
456
+ await runWithJsonErrors(async () => {
457
+ const result = await agentikitRemove({ target: args.target });
458
+ output("remove", result);
459
+ });
460
+ },
461
+ });
462
+ const updateCommand = defineCommand({
463
+ meta: { name: "update", description: "Update one or all installed registry packages" },
464
+ args: {
465
+ target: { type: "positional", description: "Installed target (id or ref)", required: false },
466
+ all: { type: "boolean", description: "Update all installed entries", default: false },
467
+ force: { type: "boolean", description: "Force fresh download even if version is unchanged", default: false },
468
+ },
469
+ async run({ args }) {
470
+ await runWithJsonErrors(async () => {
471
+ const result = await agentikitUpdate({ target: args.target, all: args.all, force: args.force });
472
+ output("update", result);
473
+ });
474
+ },
475
+ });
476
+ const upgradeCommand = defineCommand({
477
+ meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
478
+ args: {
479
+ check: { type: "boolean", description: "Check for updates without installing", default: false },
480
+ force: { type: "boolean", description: "Force upgrade even if on latest", default: false },
481
+ },
482
+ async run({ args }) {
483
+ await runWithJsonErrors(async () => {
484
+ const check = await checkForUpdate(pkgVersion);
485
+ if (args.check) {
486
+ output("upgrade", check);
487
+ return;
488
+ }
489
+ const result = await performUpgrade(check, { force: args.force });
490
+ output("upgrade", result);
491
+ });
492
+ },
493
+ });
494
+ const showCommand = defineCommand({
495
+ meta: {
496
+ name: "show",
497
+ description: "Show a stash asset by ref (e.g. akm show knowledge:guide.md toc, akm show knowledge:guide.md section 'Auth')",
498
+ },
499
+ args: {
500
+ ref: { type: "positional", description: "Asset ref (type:name)", required: true },
501
+ format: { type: "string", description: "Output format (json|text|yaml)" },
502
+ detail: { type: "string", description: "Detail level (brief|normal|full)" },
503
+ // These flags are kept for backward compatibility (--view toc still works)
504
+ // but the preferred syntax is positional: akm show ref toc
505
+ view: { type: "string", description: "Knowledge view mode (full|toc|frontmatter|section|lines)" },
506
+ heading: { type: "string", description: "Section heading (for section view)" },
507
+ start: { type: "string", description: "Start line (for lines view)" },
508
+ end: { type: "string", description: "End line (for lines view)" },
509
+ },
510
+ async run({ args }) {
511
+ await runWithJsonErrors(async () => {
512
+ let view;
513
+ if (args.view) {
514
+ switch (args.view) {
515
+ case "section":
516
+ view = { mode: "section", heading: args.heading ?? "" };
517
+ break;
518
+ case "lines":
519
+ view = {
520
+ mode: "lines",
521
+ start: Number(args.start ?? "1"),
522
+ end: args.end ? parseInt(args.end, 10) : Number.MAX_SAFE_INTEGER,
523
+ };
524
+ break;
525
+ case "toc":
526
+ case "frontmatter":
527
+ case "full":
528
+ view = { mode: args.view };
529
+ break;
530
+ default:
531
+ throw new UsageError(`Unknown view mode: ${args.view}. Expected one of: full|toc|frontmatter|section|lines`);
532
+ }
533
+ }
534
+ const result = await agentikitShow({ ref: args.ref, view });
535
+ output("show", result);
536
+ });
537
+ },
538
+ });
539
+ const configCommand = defineCommand({
540
+ meta: { name: "config", description: "Show and manage configuration" },
541
+ args: {
542
+ list: { type: "boolean", description: "List current configuration", default: false },
543
+ get: { type: "string", description: "Get a configuration value by key" },
544
+ unset: { type: "string", description: "Unset an optional configuration key or whole embedding/llm section" },
545
+ set: { type: "string", description: "Back-compat alias for updating a key (key=value format)" },
546
+ },
547
+ subCommands: {
548
+ path: defineCommand({
549
+ meta: { name: "path", description: "Show paths to config, stash, cache, and index" },
550
+ args: {
551
+ all: { type: "boolean", description: "Show all paths (config, stash, cache, index)", default: false },
552
+ },
553
+ run({ args }) {
554
+ return runWithJsonErrors(() => {
555
+ const configPath = getConfigPath();
556
+ if (args.all) {
557
+ let stashDir;
558
+ try {
559
+ stashDir = resolveStashDir({ readOnly: true });
560
+ }
561
+ catch {
562
+ stashDir = `${getDefaultStashDir()} (not initialized)`;
563
+ }
564
+ const cacheDir = getCacheDir();
565
+ const result = {
566
+ config: configPath,
567
+ stash: stashDir,
568
+ cache: cacheDir,
569
+ index: getDbPath(),
570
+ };
571
+ output("config", result);
572
+ }
573
+ else {
574
+ console.log(configPath);
575
+ }
576
+ });
577
+ },
578
+ }),
579
+ list: defineCommand({
580
+ meta: { name: "list", description: "List current configuration" },
581
+ run() {
582
+ return runWithJsonErrors(() => {
583
+ output("config", listConfig(loadConfig()));
584
+ });
585
+ },
586
+ }),
587
+ get: defineCommand({
588
+ meta: { name: "get", description: "Get a configuration value by key" },
589
+ args: {
590
+ key: { type: "positional", required: true, description: "Config key (for example: embedding, stashDir)" },
591
+ },
592
+ run({ args }) {
593
+ return runWithJsonErrors(() => {
594
+ output("config", getConfigValue(loadConfig(), args.key));
595
+ });
596
+ },
597
+ }),
598
+ set: defineCommand({
599
+ meta: { name: "set", description: "Set a configuration value by key" },
600
+ args: {
601
+ key: { type: "positional", required: true, description: "Config key (for example: embedding, llm)" },
602
+ value: { type: "positional", required: true, description: "Config value" },
603
+ },
604
+ run({ args }) {
605
+ return runWithJsonErrors(() => {
606
+ const updated = setConfigValue(loadConfig(), args.key, args.value);
607
+ saveConfig(updated);
608
+ output("config", listConfig(updated));
609
+ });
610
+ },
611
+ }),
612
+ unset: defineCommand({
613
+ meta: { name: "unset", description: "Unset an optional configuration key or whole embedding/llm section" },
614
+ args: {
615
+ key: { type: "positional", required: true, description: "Config key to unset" },
616
+ },
617
+ run({ args }) {
618
+ return runWithJsonErrors(() => {
619
+ const updated = unsetConfigValue(loadConfig(), args.key);
620
+ saveConfig(updated);
621
+ output("config", listConfig(updated));
622
+ });
623
+ },
624
+ }),
625
+ },
626
+ run({ args }) {
627
+ return runWithJsonErrors(() => {
628
+ if (hasConfigSubcommand(args))
629
+ return;
630
+ if (args.list) {
631
+ output("config", listConfig(loadConfig()));
632
+ return;
633
+ }
634
+ if (args.get) {
635
+ output("config", getConfigValue(loadConfig(), args.get));
636
+ return;
637
+ }
638
+ if (args.unset) {
639
+ const updated = unsetConfigValue(loadConfig(), args.unset);
640
+ saveConfig(updated);
641
+ output("config", listConfig(updated));
642
+ return;
643
+ }
644
+ if (args.set) {
645
+ const eqIndex = args.set.indexOf("=");
646
+ if (eqIndex === -1) {
647
+ throw new UsageError("--set expects key=value format");
648
+ }
649
+ const key = args.set.slice(0, eqIndex);
650
+ const value = args.set.slice(eqIndex + 1);
651
+ const config = setConfigValue(loadConfig(), key, value);
652
+ saveConfig(config);
653
+ output("config", listConfig(config));
654
+ }
655
+ else {
656
+ output("config", listConfig(loadConfig()));
657
+ }
658
+ });
659
+ },
660
+ });
661
+ const cloneCommand = defineCommand({
662
+ meta: {
663
+ name: "clone",
664
+ description: "Clone an asset from any stash source into the working stash or a custom destination",
665
+ },
666
+ args: {
667
+ ref: { type: "positional", description: "Asset ref (e.g. @installed:pkg/tool:script.sh)", required: true },
668
+ name: { type: "string", description: "New name for the cloned asset" },
669
+ force: { type: "boolean", description: "Overwrite if asset already exists in working stash", default: false },
670
+ dest: { type: "string", description: "Destination directory (default: working stash)" },
671
+ },
672
+ async run({ args }) {
673
+ await runWithJsonErrors(async () => {
674
+ const result = await agentikitClone({
675
+ sourceRef: args.ref,
676
+ newName: args.name,
677
+ force: args.force,
678
+ dest: args.dest,
679
+ });
680
+ output("clone", result);
681
+ });
682
+ },
683
+ });
684
+ const sourcesCommand = defineCommand({
685
+ meta: { name: "sources", description: "List all stash search paths and their status" },
686
+ run() {
687
+ return runWithJsonErrors(() => {
688
+ const sources = resolveStashSources();
689
+ output("sources", { sources });
690
+ });
691
+ },
692
+ });
693
+ const main = defineCommand({
694
+ meta: {
695
+ name: "akm",
696
+ version: pkgVersion,
697
+ description: "CLI tool to search, open, and manage assets from Agent-i-Kit stash.",
698
+ },
699
+ args: {
700
+ format: { type: "string", description: "Output format (json|text|yaml)" },
701
+ detail: { type: "string", description: "Detail level (brief|normal|full)" },
702
+ quiet: { type: "boolean", alias: "q", description: "Suppress stderr warnings", default: false },
703
+ },
704
+ subCommands: {
705
+ init: initCommand,
706
+ index: indexCommand,
707
+ add: addCommand,
708
+ list: listCommand,
709
+ remove: removeCommand,
710
+ update: updateCommand,
711
+ upgrade: upgradeCommand,
712
+ search: searchCommand,
713
+ show: showCommand,
714
+ clone: cloneCommand,
715
+ sources: sourcesCommand,
716
+ config: configCommand,
717
+ },
718
+ });
719
+ const SEARCH_SOURCES = ["local", "registry", "both"];
720
+ const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
721
+ const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
722
+ // citty reads process.argv directly and does not accept a custom argv array,
723
+ // so we must replace process.argv with the normalized version before runMain.
724
+ process.argv = normalizeShowArgv(normalizeConfigArgv(process.argv));
725
+ runMain(main);
726
+ function parseSearchSource(value) {
727
+ if (SEARCH_SOURCES.includes(value))
728
+ return value;
729
+ throw new UsageError(`Invalid value for --source: ${value}. Expected one of: ${SEARCH_SOURCES.join("|")}`);
730
+ }
731
+ // ── Exit codes ──────────────────────────────────────────────────────────────
732
+ const EXIT_GENERAL = 1;
733
+ const EXIT_USAGE = 2;
734
+ const EXIT_CONFIG = 78;
735
+ function classifyExitCode(error) {
736
+ if (error instanceof UsageError)
737
+ return EXIT_USAGE;
738
+ if (error instanceof ConfigError)
739
+ return EXIT_CONFIG;
740
+ if (error instanceof NotFoundError)
741
+ return EXIT_GENERAL;
742
+ return EXIT_GENERAL;
743
+ }
744
+ async function runWithJsonErrors(fn) {
745
+ try {
746
+ // Apply --quiet flag early so warnings inside the command are suppressed
747
+ if (process.argv.includes("--quiet") || process.argv.includes("-q")) {
748
+ setQuiet(true);
749
+ }
750
+ await fn();
751
+ }
752
+ catch (error) {
753
+ const message = error instanceof Error ? error.message : String(error);
754
+ const hint = buildHint(message);
755
+ const exitCode = classifyExitCode(error);
756
+ console.error(JSON.stringify({ ok: false, error: message, hint }, null, 2));
757
+ process.exit(exitCode);
758
+ }
759
+ }
760
+ function buildHint(message) {
761
+ if (message.includes("No stash directory found"))
762
+ return "Run `akm init` to create the default stash, or set stashDir in your config.";
763
+ if (message.includes("Either <target> or --all is required"))
764
+ return "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg`.";
765
+ if (message.includes("Specify either <target> or --all"))
766
+ return "Use only one: a positional target or `--all`.";
767
+ if (message.includes("No installed registry entry matched target"))
768
+ return "Run `akm list` to view installed ids/refs, then retry with one of those values.";
769
+ if (message.includes("remote package fetched but asset not found"))
770
+ return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
771
+ if (message.includes("Invalid value for --source"))
772
+ return "Pick one of: local, registry, both.";
773
+ if (message.includes("Invalid value for --format"))
774
+ return "Pick one of: json, text, yaml.";
775
+ if (message.includes("Invalid value for --detail"))
776
+ return "Pick one of: brief, normal, full.";
777
+ if (message.includes("expected JSON object with endpoint and model")) {
778
+ return 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.';
779
+ }
780
+ return undefined;
781
+ }
782
+ function hasConfigSubcommand(args) {
783
+ const command = Array.isArray(args._) ? args._[0] : undefined;
784
+ return typeof command === "string" && CONFIG_SUBCOMMAND_SET.has(command);
785
+ }
786
+ /**
787
+ * Normalize argv before citty parses it so git-style config forms like
788
+ * `akm config llm.maxTokens 512` and `akm config --get llm.maxTokens`
789
+ * are normalized into the existing config subcommands.
790
+ *
791
+ * Returns a new array; the input is never modified.
792
+ */
793
+ function normalizeConfigArgv(argv) {
794
+ // Global flags should not be treated as config subcommand arguments.
795
+ // We strip them from the analysis portion, normalize, then re-append them.
796
+ const globalFlags = [];
797
+ const configArgs = [];
798
+ for (let i = 3; i < argv.length; i++) {
799
+ const arg = argv[i];
800
+ if (arg === "--quiet" || arg === "-q") {
801
+ globalFlags.push(arg);
802
+ continue;
803
+ }
804
+ if (arg.startsWith("--format=") || arg.startsWith("--detail=")) {
805
+ globalFlags.push(arg);
806
+ continue;
807
+ }
808
+ if (arg === "--format" || arg === "--detail") {
809
+ globalFlags.push(arg);
810
+ if (argv[i + 1] !== undefined) {
811
+ globalFlags.push(argv[i + 1]);
812
+ i++;
813
+ }
814
+ continue;
815
+ }
816
+ configArgs.push(arg);
817
+ }
818
+ const [command, argAfterCommand, argAfterKey, ...rest] = [argv[2], ...configArgs];
819
+ if (command !== "config")
820
+ return argv;
821
+ if (!argAfterCommand)
822
+ return argv;
823
+ const prefix = argv.slice(0, 3);
824
+ const buildResult = (...newArgs) => [...prefix, ...newArgs, ...globalFlags];
825
+ if (argAfterCommand === "--list") {
826
+ return buildResult("list");
827
+ }
828
+ if (argAfterCommand === "--get" && argAfterKey) {
829
+ return buildResult("get", argAfterKey, ...rest);
830
+ }
831
+ if (argAfterCommand === "--unset" && argAfterKey) {
832
+ return buildResult("unset", argAfterKey, ...rest);
833
+ }
834
+ if (argAfterCommand.startsWith("-"))
835
+ return argv;
836
+ if (CONFIG_SUBCOMMAND_SET.has(argAfterCommand))
837
+ return argv;
838
+ // A single arg after `config` behaves like `git config <key>` and reads the value.
839
+ if (argAfterKey === undefined) {
840
+ return buildResult("get", argAfterCommand);
841
+ }
842
+ return buildResult("set", argAfterCommand, argAfterKey, ...rest);
843
+ }
844
+ /**
845
+ * Normalize argv so positional view-mode arguments after the asset ref
846
+ * are rewritten into the flag form that citty can parse.
847
+ *
848
+ * Converts:
849
+ * akm show knowledge:guide.md toc → akm show knowledge:guide.md --view toc
850
+ * akm show knowledge:guide.md section Auth → akm show knowledge:guide.md --view section --heading Auth
851
+ * akm show knowledge:guide.md lines 1 50 → akm show knowledge:guide.md --view lines --start 1 --end 50
852
+ *
853
+ * If --view is already present the argv is returned unchanged (backward compat).
854
+ * Returns a new array; the input is never modified.
855
+ */
856
+ function normalizeShowArgv(argv) {
857
+ // argv[0]=bun argv[1]=script argv[2]=subcommand argv[3]=ref argv[4..]=rest
858
+ if (argv[2] !== "show")
859
+ return argv;
860
+ // If --view is already present, pass through unchanged
861
+ if (argv.includes("--view"))
862
+ return argv;
863
+ // Separate global flags from positional/show-specific args
864
+ const prefix = argv.slice(0, 3); // [bun, script, show]
865
+ const rest = argv.slice(3);
866
+ const globalFlags = [];
867
+ const showArgs = [];
868
+ for (let i = 0; i < rest.length; i++) {
869
+ const arg = rest[i];
870
+ if (arg === "--quiet" || arg === "-q") {
871
+ globalFlags.push(arg);
872
+ continue;
873
+ }
874
+ if (arg.startsWith("--format=") || arg.startsWith("--detail=")) {
875
+ globalFlags.push(arg);
876
+ continue;
877
+ }
878
+ if (arg === "--format" || arg === "--detail") {
879
+ globalFlags.push(arg);
880
+ if (rest[i + 1] !== undefined) {
881
+ globalFlags.push(rest[i + 1]);
882
+ i++;
883
+ }
884
+ continue;
885
+ }
886
+ showArgs.push(arg);
887
+ }
888
+ // showArgs[0] = ref, showArgs[1] = potential view mode, showArgs[2..] = view params
889
+ const ref = showArgs[0];
890
+ const viewMode = showArgs[1];
891
+ if (!ref || !viewMode || !SHOW_VIEW_MODES.has(viewMode)) {
892
+ return argv;
893
+ }
894
+ const result = [...prefix, ref, "--view", viewMode];
895
+ if (viewMode === "section") {
896
+ // Next arg is the heading name; pass empty string when missing so the
897
+ // show handler can produce a clear "section not found" error.
898
+ const heading = showArgs[2] ?? "";
899
+ result.push("--heading", heading);
900
+ }
901
+ else if (viewMode === "lines") {
902
+ // Next two args are start and end
903
+ const start = showArgs[2];
904
+ const end = showArgs[3];
905
+ if (start)
906
+ result.push("--start", start);
907
+ if (end)
908
+ result.push("--end", end);
909
+ }
910
+ result.push(...globalFlags);
911
+ return result;
912
+ }