akm-cli 0.0.16 → 0.0.17

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 CHANGED
@@ -9,6 +9,7 @@ import { ConfigError, NotFoundError, UsageError } from "./errors";
9
9
  import { agentikitIndex } from "./indexer";
10
10
  import { agentikitInit } from "./init";
11
11
  import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
12
+ import { searchRegistry } from "./registry-search";
12
13
  import { checkForUpdate, performUpgrade } from "./self-update";
13
14
  import { agentikitAdd } from "./stash-add";
14
15
  import { agentikitClone } from "./stash-clone";
@@ -101,6 +102,8 @@ function shapeForCommand(command, result, detail) {
101
102
  switch (command) {
102
103
  case "search":
103
104
  return shapeSearchOutput(result, detail);
105
+ case "registry-search":
106
+ return shapeRegistrySearchOutput(result, detail);
104
107
  case "show":
105
108
  return shapeShowOutput(result, detail);
106
109
  default:
@@ -127,6 +130,22 @@ function shapeSearchOutput(result, detail) {
127
130
  ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
128
131
  };
129
132
  }
133
+ function shapeRegistrySearchOutput(result, detail) {
134
+ const hits = Array.isArray(result.hits) ? result.hits : [];
135
+ const assetHits = Array.isArray(result.assetHits) ? result.assetHits : [];
136
+ // Shape kit hits as registry type
137
+ const shapedKitHits = hits.map((hit) => shapeSearchHit({ ...hit, type: "registry" }, detail));
138
+ const shapedAssetHits = assetHits.map((hit) => shapeSearchHit(hit, detail));
139
+ const shaped = {
140
+ hits: shapedKitHits,
141
+ ...(shapedAssetHits.length > 0 ? { assetHits: shapedAssetHits } : {}),
142
+ ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
143
+ };
144
+ if (detail === "full") {
145
+ shaped.query = result.query;
146
+ }
147
+ return shaped;
148
+ }
130
149
  function shapeSearchHit(hit, detail) {
131
150
  // Keep local and registry hit models separate internally so search and
132
151
  // ranking logic can carry source-specific metadata. Normalize the external
@@ -139,6 +158,15 @@ function shapeSearchHit(hit, detail) {
139
158
  return pickFields(hit, ["type", "name", "id", "description", "tags", "action", "curated"]);
140
159
  return hit;
141
160
  }
161
+ if (hit.type === "registry-asset") {
162
+ const brief = withTruncatedDescription(pickFields(hit, ["type", "assetType", "assetName", "description", "kit", "action"]));
163
+ if (detail === "brief")
164
+ return brief;
165
+ if (detail === "normal") {
166
+ return pickFields(hit, ["type", "assetType", "assetName", "description", "kit", "registryName", "action"]);
167
+ }
168
+ return hit;
169
+ }
142
170
  const brief = withTruncatedDescription(pickFields(hit, ["type", "name", "description", "action"]));
143
171
  if (detail === "brief")
144
172
  return brief;
@@ -405,7 +433,7 @@ const searchCommand = defineCommand({
405
433
  query: { type: "positional", description: "Search query (omit to list all assets)", required: false, default: "" },
406
434
  type: {
407
435
  type: "string",
408
- description: "Asset type filter (skill|command|agent|knowledge|script|any). 'tool' is accepted as alias for 'script'.",
436
+ description: "Asset type filter (skill|command|agent|knowledge|script|any).",
409
437
  },
410
438
  limit: { type: "string", description: "Maximum number of results" },
411
439
  source: { type: "string", description: "Search source (local|registry|both)", default: "local" },
@@ -439,7 +467,7 @@ const addCommand = defineCommand({
439
467
  },
440
468
  });
441
469
  const listCommand = defineCommand({
442
- meta: { name: "list", description: "List installed registry packages from config" },
470
+ meta: { name: "list", description: "List installed kits" },
443
471
  async run() {
444
472
  await runWithJsonErrors(async () => {
445
473
  const result = await agentikitList();
@@ -448,7 +476,7 @@ const listCommand = defineCommand({
448
476
  },
449
477
  });
450
478
  const removeCommand = defineCommand({
451
- meta: { name: "remove", description: "Remove an installed registry package by id or ref" },
479
+ meta: { name: "remove", description: "Remove an installed kit by id or ref" },
452
480
  args: {
453
481
  target: { type: "positional", description: "Installed target (id or ref)", required: true },
454
482
  },
@@ -460,7 +488,7 @@ const removeCommand = defineCommand({
460
488
  },
461
489
  });
462
490
  const updateCommand = defineCommand({
463
- meta: { name: "update", description: "Update one or all installed registry packages" },
491
+ meta: { name: "update", description: "Update one or all installed kits" },
464
492
  args: {
465
493
  target: { type: "positional", description: "Installed target (id or ref)", required: false },
466
494
  all: { type: "boolean", description: "Update all installed entries", default: false },
@@ -500,35 +528,33 @@ const showCommand = defineCommand({
500
528
  ref: { type: "positional", description: "Asset ref (type:name)", required: true },
501
529
  format: { type: "string", description: "Output format (json|text|yaml)" },
502
530
  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)" },
531
+ akmView: { type: "string", description: "Internal positional knowledge view mode parser" },
532
+ akmHeading: { type: "string", description: "Internal positional section heading parser" },
533
+ akmStart: { type: "string", description: "Internal positional start-line parser" },
534
+ akmEnd: { type: "string", description: "Internal positional end-line parser" },
509
535
  },
510
536
  async run({ args }) {
511
537
  await runWithJsonErrors(async () => {
512
538
  let view;
513
- if (args.view) {
514
- switch (args.view) {
539
+ if (args.akmView) {
540
+ switch (args.akmView) {
515
541
  case "section":
516
- view = { mode: "section", heading: args.heading ?? "" };
542
+ view = { mode: "section", heading: args.akmHeading ?? "" };
517
543
  break;
518
544
  case "lines":
519
545
  view = {
520
546
  mode: "lines",
521
- start: Number(args.start ?? "1"),
522
- end: args.end ? parseInt(args.end, 10) : Number.MAX_SAFE_INTEGER,
547
+ start: Number(args.akmStart ?? "1"),
548
+ end: args.akmEnd ? parseInt(args.akmEnd, 10) : Number.MAX_SAFE_INTEGER,
523
549
  };
524
550
  break;
525
551
  case "toc":
526
552
  case "frontmatter":
527
553
  case "full":
528
- view = { mode: args.view };
554
+ view = { mode: args.akmView };
529
555
  break;
530
556
  default:
531
- throw new UsageError(`Unknown view mode: ${args.view}. Expected one of: full|toc|frontmatter|section|lines`);
557
+ throw new UsageError(`Unknown view mode: ${args.akmView}. Expected one of: full|toc|frontmatter|section|lines`);
532
558
  }
533
559
  }
534
560
  const result = await agentikitShow({ ref: args.ref, view });
@@ -540,9 +566,6 @@ const configCommand = defineCommand({
540
566
  meta: { name: "config", description: "Show and manage configuration" },
541
567
  args: {
542
568
  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
569
  },
547
570
  subCommands: {
548
571
  path: defineCommand({
@@ -631,30 +654,7 @@ const configCommand = defineCommand({
631
654
  output("config", listConfig(loadConfig()));
632
655
  return;
633
656
  }
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
- }
657
+ output("config", listConfig(loadConfig()));
658
658
  });
659
659
  },
660
660
  });
@@ -664,7 +664,7 @@ const cloneCommand = defineCommand({
664
664
  description: "Clone an asset from any stash source into the working stash or a custom destination",
665
665
  },
666
666
  args: {
667
- ref: { type: "positional", description: "Asset ref (e.g. @installed:pkg/tool:script.sh)", required: true },
667
+ ref: { type: "positional", description: "Asset ref (e.g. npm:@scope/pkg//script:deploy.sh)", required: true },
668
668
  name: { type: "string", description: "New name for the cloned asset" },
669
669
  force: { type: "boolean", description: "Overwrite if asset already exists in working stash", default: false },
670
670
  dest: { type: "string", description: "Destination directory (default: working stash)" },
@@ -681,12 +681,91 @@ const cloneCommand = defineCommand({
681
681
  });
682
682
  },
683
683
  });
684
+ const registryCommand = defineCommand({
685
+ meta: { name: "registry", description: "Manage kit registries" },
686
+ subCommands: {
687
+ list: defineCommand({
688
+ meta: { name: "list", description: "List configured registries" },
689
+ run() {
690
+ return runWithJsonErrors(() => {
691
+ const config = loadConfig();
692
+ const registries = config.registries ?? [];
693
+ output("registry-list", { registries });
694
+ });
695
+ },
696
+ }),
697
+ add: defineCommand({
698
+ meta: { name: "add", description: "Add a registry by URL" },
699
+ args: {
700
+ url: { type: "positional", description: "Registry index URL", required: true },
701
+ name: { type: "string", description: "Human-friendly name for the registry" },
702
+ },
703
+ run({ args }) {
704
+ return runWithJsonErrors(() => {
705
+ if (!args.url.startsWith("http")) {
706
+ throw new UsageError("Registry URL must start with http:// or https://");
707
+ }
708
+ const config = loadConfig();
709
+ const registries = [...(config.registries ?? [])];
710
+ // Deduplicate by URL
711
+ if (registries.some((r) => r.url === args.url)) {
712
+ output("registry-add", { registries, added: false, message: "Registry URL already configured" });
713
+ return;
714
+ }
715
+ const entry = { url: args.url };
716
+ if (args.name)
717
+ entry.name = args.name;
718
+ registries.push(entry);
719
+ saveConfig({ ...config, registries });
720
+ output("registry-add", { registries, added: true });
721
+ });
722
+ },
723
+ }),
724
+ remove: defineCommand({
725
+ meta: { name: "remove", description: "Remove a registry by URL or name" },
726
+ args: {
727
+ target: { type: "positional", description: "Registry URL or name to remove", required: true },
728
+ },
729
+ run({ args }) {
730
+ return runWithJsonErrors(() => {
731
+ const config = loadConfig();
732
+ const registries = [...(config.registries ?? [])];
733
+ const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
734
+ if (idx === -1) {
735
+ output("registry-remove", { registries, removed: false, message: "No matching registry found" });
736
+ return;
737
+ }
738
+ const removed = registries.splice(idx, 1)[0];
739
+ saveConfig({ ...config, registries });
740
+ output("registry-remove", { registries, removed: true, entry: removed });
741
+ });
742
+ },
743
+ }),
744
+ search: defineCommand({
745
+ meta: { name: "search", description: "Search enabled registries for kits" },
746
+ args: {
747
+ query: { type: "positional", description: "Search query", required: true },
748
+ limit: { type: "string", description: "Maximum number of results" },
749
+ assets: { type: "boolean", description: "Include asset-level search results", default: false },
750
+ },
751
+ async run({ args }) {
752
+ await runWithJsonErrors(async () => {
753
+ const limit = args.limit ? parseInt(args.limit, 10) : undefined;
754
+ const result = await searchRegistry(args.query, { limit, includeAssets: args.assets });
755
+ output("registry-search", result);
756
+ });
757
+ },
758
+ }),
759
+ },
760
+ });
684
761
  const sourcesCommand = defineCommand({
685
762
  meta: { name: "sources", description: "List all stash search paths and their status" },
686
763
  run() {
687
764
  return runWithJsonErrors(() => {
765
+ const config = loadConfig();
688
766
  const sources = resolveStashSources();
689
- output("sources", { sources });
767
+ const registries = config.registries ?? [];
768
+ output("sources", { sources, registries });
690
769
  });
691
770
  },
692
771
  });
@@ -713,6 +792,7 @@ const main = defineCommand({
713
792
  show: showCommand,
714
793
  clone: cloneCommand,
715
794
  sources: sourcesCommand,
795
+ registry: registryCommand,
716
796
  config: configCommand,
717
797
  },
718
798
  });
@@ -721,7 +801,7 @@ const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
721
801
  const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
722
802
  // citty reads process.argv directly and does not accept a custom argv array,
723
803
  // so we must replace process.argv with the normalized version before runMain.
724
- process.argv = normalizeShowArgv(normalizeConfigArgv(process.argv));
804
+ process.argv = normalizeShowArgv(process.argv);
725
805
  runMain(main);
726
806
  function parseSearchSource(value) {
727
807
  if (SEARCH_SOURCES.includes(value))
@@ -764,7 +844,7 @@ function buildHint(message) {
764
844
  return "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg`.";
765
845
  if (message.includes("Specify either <target> or --all"))
766
846
  return "Use only one: a positional target or `--all`.";
767
- if (message.includes("No installed registry entry matched target"))
847
+ if (message.includes("No installed kit matched target"))
768
848
  return "Run `akm list` to view installed ids/refs, then retry with one of those values.";
769
849
  if (message.includes("remote package fetched but asset not found"))
770
850
  return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
@@ -783,83 +863,25 @@ function hasConfigSubcommand(args) {
783
863
  const command = Array.isArray(args._) ? args._[0] : undefined;
784
864
  return typeof command === "string" && CONFIG_SUBCOMMAND_SET.has(command);
785
865
  }
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
866
  /**
845
867
  * Normalize argv so positional view-mode arguments after the asset ref
846
- * are rewritten into the flag form that citty can parse.
868
+ * are rewritten into internal flags that citty can parse.
847
869
  *
848
870
  * 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
871
+ * akm show knowledge:guide.md toc → akm show knowledge:guide.md --akmView toc
872
+ * akm show knowledge:guide.md section Auth → akm show knowledge:guide.md --akmView section --akmHeading Auth
873
+ * akm show knowledge:guide.md lines 1 50 → akm show knowledge:guide.md --akmView lines --akmStart 1 --akmEnd 50
852
874
  *
853
- * If --view is already present the argv is returned unchanged (backward compat).
875
+ * Legacy `--view` is intentionally unsupported.
854
876
  * Returns a new array; the input is never modified.
855
877
  */
856
878
  function normalizeShowArgv(argv) {
857
879
  // argv[0]=bun argv[1]=script argv[2]=subcommand argv[3]=ref argv[4..]=rest
858
880
  if (argv[2] !== "show")
859
881
  return argv;
860
- // If --view is already present, pass through unchanged
861
- if (argv.includes("--view"))
862
- return argv;
882
+ if (argv.includes("--view") || argv.includes("--heading") || argv.includes("--start") || argv.includes("--end")) {
883
+ throw new UsageError('Legacy show flags are no longer supported. Use positional syntax like `akm show knowledge:guide toc` or `akm show knowledge:guide section "Auth"`.');
884
+ }
863
885
  // Separate global flags from positional/show-specific args
864
886
  const prefix = argv.slice(0, 3); // [bun, script, show]
865
887
  const rest = argv.slice(3);
@@ -891,21 +913,21 @@ function normalizeShowArgv(argv) {
891
913
  if (!ref || !viewMode || !SHOW_VIEW_MODES.has(viewMode)) {
892
914
  return argv;
893
915
  }
894
- const result = [...prefix, ref, "--view", viewMode];
916
+ const result = [...prefix, ref, "--akmView", viewMode];
895
917
  if (viewMode === "section") {
896
918
  // Next arg is the heading name; pass empty string when missing so the
897
919
  // show handler can produce a clear "section not found" error.
898
920
  const heading = showArgs[2] ?? "";
899
- result.push("--heading", heading);
921
+ result.push("--akmHeading", heading);
900
922
  }
901
923
  else if (viewMode === "lines") {
902
924
  // Next two args are start and end
903
925
  const start = showArgs[2];
904
926
  const end = showArgs[3];
905
927
  if (start)
906
- result.push("--start", start);
928
+ result.push("--akmStart", start);
907
929
  if (end)
908
- result.push("--end", end);
930
+ result.push("--akmEnd", end);
909
931
  }
910
932
  result.push(...globalFlags);
911
933
  return result;
package/dist/common.js CHANGED
@@ -3,13 +3,6 @@ import path from "node:path";
3
3
  import { TYPE_DIRS } from "./asset-spec";
4
4
  import { ConfigError } from "./errors";
5
5
  import { getConfigPath, getDefaultStashDir } from "./paths";
6
- /**
7
- * Normalize an asset type for output purposes.
8
- * "tool" is a transparent alias for "script" -- all output should use "script".
9
- */
10
- export function normalizeAssetType(type) {
11
- return type === "tool" ? "script" : type;
12
- }
13
6
  // ── Constants ───────────────────────────────────────────────────────────────
14
7
  export const IS_WINDOWS = process.platform === "win32";
15
8
  // ── Validators ──────────────────────────────────────────────────────────────
@@ -23,6 +23,8 @@ export function parseConfigValue(key, value) {
23
23
  return { embedding: parseEmbeddingConnectionValue(value) };
24
24
  case "llm":
25
25
  return { llm: parseLlmConnectionValue(value) };
26
+ case "registries":
27
+ return { registries: parseRegistriesValue(value) };
26
28
  case "output.format":
27
29
  return { output: { format: parseOutputFormat(value) } };
28
30
  case "output.detail":
@@ -43,6 +45,8 @@ export function getConfigValue(config, key) {
43
45
  return config.embedding ?? null;
44
46
  case "llm":
45
47
  return config.llm ?? null;
48
+ case "registries":
49
+ return config.registries ?? DEFAULT_CONFIG.registries ?? [];
46
50
  case "output.format":
47
51
  return config.output?.format ?? null;
48
52
  case "output.detail":
@@ -58,6 +62,7 @@ export function setConfigValue(config, key, rawValue) {
58
62
  case "searchPaths":
59
63
  case "embedding":
60
64
  case "llm":
65
+ case "registries":
61
66
  case "output.format":
62
67
  case "output.detail":
63
68
  return mergeConfigValue(config, parseConfigValue(key, rawValue));
@@ -73,6 +78,8 @@ export function unsetConfigValue(config, key) {
73
78
  return { ...config, embedding: undefined };
74
79
  case "llm":
75
80
  return { ...config, llm: undefined };
81
+ case "registries":
82
+ return { ...config, registries: undefined };
76
83
  case "output.format":
77
84
  return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
78
85
  case "output.detail":
@@ -89,6 +96,7 @@ export function listConfig(config) {
89
96
  stashDir: config.stashDir ?? null,
90
97
  embedding: config.embedding ?? null,
91
98
  llm: config.llm ?? null,
99
+ registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
92
100
  };
93
101
  }
94
102
  function mergeConfigValue(config, partial) {
@@ -115,6 +123,36 @@ function parseOutputDetail(value) {
115
123
  return value;
116
124
  throw new UsageError(`Invalid value for output.detail: expected one of brief|normal|full`);
117
125
  }
126
+ function parseRegistriesValue(value) {
127
+ if (value === "null" || value === "")
128
+ return undefined;
129
+ let parsed;
130
+ try {
131
+ parsed = JSON.parse(value);
132
+ }
133
+ catch {
134
+ throw new UsageError(`Invalid value for registries: expected JSON array of {url, name?, enabled?} objects` +
135
+ ` (e.g. '[{"url":"https://example.com/index.json","name":"my-registry"}]')`);
136
+ }
137
+ if (!Array.isArray(parsed)) {
138
+ throw new UsageError(`Invalid value for registries: expected a JSON array`);
139
+ }
140
+ return parsed.map((entry, i) => {
141
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
142
+ throw new UsageError(`Invalid value for registries[${i}]: expected an object with a "url" field`);
143
+ }
144
+ const obj = entry;
145
+ if (typeof obj.url !== "string" || !obj.url) {
146
+ throw new UsageError(`Invalid value for registries[${i}]: "url" is required`);
147
+ }
148
+ const result = { url: obj.url };
149
+ if (typeof obj.name === "string" && obj.name)
150
+ result.name = obj.name;
151
+ if (typeof obj.enabled === "boolean")
152
+ result.enabled = obj.enabled;
153
+ return result;
154
+ });
155
+ }
118
156
  function parseEmbeddingConnectionValue(value) {
119
157
  if (value === "null" || value === "")
120
158
  return undefined;
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "
5
5
  export const DEFAULT_CONFIG = {
6
6
  semanticSearch: true,
7
7
  searchPaths: [],
8
+ registries: [{ url: "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json", name: "official" }],
8
9
  output: {
9
10
  format: "json",
10
11
  detail: "brief",
@@ -113,27 +114,18 @@ function pickKnownKeys(raw) {
113
114
  if (Array.isArray(raw.searchPaths)) {
114
115
  config.searchPaths = raw.searchPaths.filter((d) => typeof d === "string");
115
116
  }
116
- // Backward compat: merge legacy mountedStashDirs into searchPaths
117
- if (Array.isArray(raw.mountedStashDirs)) {
118
- const legacy = raw.mountedStashDirs.filter((d) => typeof d === "string");
119
- const existing = new Set(config.searchPaths);
120
- for (const d of legacy) {
121
- if (!existing.has(d))
122
- config.searchPaths.push(d);
123
- }
124
- }
125
117
  const embedding = parseEmbeddingConfig(raw.embedding);
126
118
  if (embedding)
127
119
  config.embedding = embedding;
128
120
  const llm = parseLlmConfig(raw.llm);
129
121
  if (llm)
130
122
  config.llm = llm;
131
- const registry = parseRegistryConfig(raw.registry);
132
- if (registry)
133
- config.registry = registry;
134
- if (Array.isArray(raw.registryUrls)) {
135
- config.registryUrls = raw.registryUrls.filter((u) => typeof u === "string" && u.startsWith("http"));
136
- }
123
+ const installed = parseInstalledEntries(raw.installed);
124
+ if (installed)
125
+ config.installed = installed;
126
+ const registries = parseRegistriesConfig(raw.registries);
127
+ if (registries)
128
+ config.registries = registries;
137
129
  const output = parseOutputConfig(raw.output);
138
130
  if (output)
139
131
  config.output = output;
@@ -273,23 +265,20 @@ function parseLlmConfig(value) {
273
265
  }
274
266
  return result;
275
267
  }
276
- function parseRegistryConfig(value) {
277
- if (typeof value !== "object" || value === null || Array.isArray(value))
268
+ function parseInstalledEntries(value) {
269
+ if (!Array.isArray(value))
278
270
  return undefined;
279
- const obj = value;
280
- if (!Array.isArray(obj.installed))
281
- return undefined;
282
- const installed = obj.installed
283
- .map((entry) => parseRegistryInstalledEntry(entry))
271
+ const entries = value
272
+ .map((entry) => parseInstalledKitEntry(entry))
284
273
  .filter((entry) => entry !== undefined);
285
- return { installed };
274
+ return entries.length > 0 ? entries : undefined;
286
275
  }
287
- function parseRegistryInstalledEntry(value) {
276
+ function parseInstalledKitEntry(value) {
288
277
  if (typeof value !== "object" || value === null || Array.isArray(value))
289
278
  return undefined;
290
279
  const obj = value;
291
280
  const id = asNonEmptyString(obj.id);
292
- const source = asRegistrySource(obj.source);
281
+ const source = asKitSource(obj.source);
293
282
  const ref = asNonEmptyString(obj.ref);
294
283
  const artifactUrl = asNonEmptyString(obj.artifactUrl);
295
284
  const stashRoot = asNonEmptyString(obj.stashRoot);
@@ -317,8 +306,33 @@ function parseRegistryInstalledEntry(value) {
317
306
  function asNonEmptyString(value) {
318
307
  return typeof value === "string" && value ? value : undefined;
319
308
  }
320
- function asRegistrySource(value) {
309
+ function asKitSource(value) {
321
310
  if (value === "npm" || value === "github" || value === "git" || value === "local")
322
311
  return value;
323
312
  return undefined;
324
313
  }
314
+ function parseRegistriesConfig(value) {
315
+ if (!Array.isArray(value))
316
+ return undefined;
317
+ const entries = value
318
+ .map((entry) => parseRegistryConfigEntry(entry))
319
+ .filter((entry) => entry !== undefined);
320
+ // Return the array even if empty — an explicit empty array means "no registries"
321
+ // which overrides the default. Only return undefined if the field was not an array.
322
+ return entries;
323
+ }
324
+ function parseRegistryConfigEntry(value) {
325
+ if (typeof value !== "object" || value === null || Array.isArray(value))
326
+ return undefined;
327
+ const obj = value;
328
+ const url = asNonEmptyString(obj.url);
329
+ if (!url || !url.startsWith("http"))
330
+ return undefined;
331
+ const entry = { url };
332
+ const name = asNonEmptyString(obj.name);
333
+ if (name)
334
+ entry.name = name;
335
+ if (typeof obj.enabled === "boolean")
336
+ entry.enabled = obj.enabled;
337
+ return entry;
338
+ }