akm-cli 0.0.21 → 0.0.23
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 +8 -5
- package/dist/asset-spec.js +91 -10
- package/dist/cli.js +172 -57
- package/dist/common.js +15 -2
- package/dist/config-cli.js +55 -6
- package/dist/config.js +118 -22
- package/dist/create-provider-registry.js +18 -0
- package/dist/db.js +156 -53
- package/dist/embedder.js +36 -18
- package/dist/errors.js +6 -0
- package/dist/file-context.js +18 -19
- package/dist/frontmatter.js +19 -3
- package/dist/indexer.js +126 -89
- package/dist/{stash-registry.js → installed-kits.js} +16 -24
- package/dist/kit-include.js +108 -0
- package/dist/local-search.js +429 -0
- package/dist/lockfile.js +47 -5
- package/dist/matchers.js +6 -0
- package/dist/metadata.js +20 -10
- package/dist/paths.js +4 -0
- package/dist/providers/skills-sh.js +3 -2
- package/dist/providers/static-index.js +4 -9
- package/dist/registry-build-index.js +356 -0
- package/dist/registry-factory.js +19 -0
- package/dist/registry-install.js +114 -109
- package/dist/registry-resolve.js +44 -9
- package/dist/registry-search.js +14 -9
- package/dist/renderers.js +23 -7
- package/dist/ripgrep-install.js +9 -4
- package/dist/self-update.js +31 -4
- package/dist/stash-add.js +75 -6
- package/dist/stash-clone.js +1 -1
- package/dist/stash-provider-factory.js +37 -0
- package/dist/stash-provider.js +1 -0
- package/dist/stash-providers/filesystem.js +42 -0
- package/dist/stash-providers/index.js +9 -0
- package/dist/stash-providers/openviking.js +337 -0
- package/dist/stash-resolve.js +4 -4
- package/dist/stash-search.js +70 -401
- package/dist/stash-show.js +24 -5
- package/dist/stash-source-manage.js +82 -0
- package/dist/stash-source.js +19 -11
- package/dist/walker.js +15 -10
- package/dist/warn.js +7 -0
- package/package.json +1 -1
- package/dist/provider-registry.js +0 -8
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
9
|
A package manager for AI agent capabilities -- scripts, skills, commands,
|
|
10
|
-
agents, and
|
|
11
|
-
shell commands.
|
|
10
|
+
agents, knowledge, and memories -- that works with any AI coding assistant that
|
|
11
|
+
can run shell commands.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -42,7 +42,7 @@ Any model that can run shell commands can use `akm`. Add this to your
|
|
|
42
42
|
## Resources & Capabilities
|
|
43
43
|
|
|
44
44
|
You have access to a searchable library of scripts, skills, commands, agents,
|
|
45
|
-
and
|
|
45
|
+
knowledge, and memories via the `akm` CLI. Use `akm -h` for details.
|
|
46
46
|
~~~
|
|
47
47
|
|
|
48
48
|
No plugins, SDKs, or integration code required. Platform-specific plugins
|
|
@@ -89,13 +89,16 @@ Registries are indexes of available kits. The official
|
|
|
89
89
|
```sh
|
|
90
90
|
akm registry search "code review" # Search registries
|
|
91
91
|
akm registry add https://example.com/registry/index.json --name team # Add a registry
|
|
92
|
+
akm sources add http://host:1933 --provider openviking \
|
|
93
|
+
--options '{"apiKey":"key"}' # Add an OpenViking stash source
|
|
92
94
|
akm registry list # List configured registries
|
|
95
|
+
akm show viking://resources/my-doc # Fetch remote content from OpenViking
|
|
93
96
|
```
|
|
94
97
|
|
|
95
98
|
Private access is supported through:
|
|
96
99
|
- **GitHub tokens** -- Set `GITHUB_TOKEN` to access private GitHub repos when installing kits
|
|
97
|
-
- **Provider options** --
|
|
98
|
-
- **Pluggable providers** --
|
|
100
|
+
- **Provider options** -- `--options` flag accepts JSON for provider-specific configuration (API keys, custom headers)
|
|
101
|
+
- **Pluggable providers** -- Built-in registry providers include `static-index` and `skills-sh`; stash providers include `filesystem` and `openviking`; custom providers can implement their own authentication
|
|
99
102
|
|
|
100
103
|
See the [Registry docs](docs/registry.md) for hosting your own registry and
|
|
101
104
|
the v2 index format.
|
package/dist/asset-spec.js
CHANGED
|
@@ -37,7 +37,7 @@ const scriptSpec = {
|
|
|
37
37
|
toCanonicalName: (typeRoot, filePath) => toPosix(path.relative(typeRoot, filePath)),
|
|
38
38
|
toAssetPath: (typeRoot, name) => path.join(typeRoot, name),
|
|
39
39
|
};
|
|
40
|
-
|
|
40
|
+
const ASSET_SPECS_INTERNAL = {
|
|
41
41
|
skill: {
|
|
42
42
|
stashDir: "skills",
|
|
43
43
|
isRelevantFile: (fileName) => fileName === "SKILL.md",
|
|
@@ -53,24 +53,105 @@ export const ASSET_SPECS = {
|
|
|
53
53
|
agent: { stashDir: "agents", ...markdownSpec },
|
|
54
54
|
knowledge: { stashDir: "knowledge", ...markdownSpec },
|
|
55
55
|
script: { stashDir: "scripts", ...scriptSpec },
|
|
56
|
+
memory: { stashDir: "memories", ...markdownSpec },
|
|
56
57
|
};
|
|
57
|
-
export const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
|
|
59
|
+
/**
|
|
60
|
+
* Deferred hooks set by `local-search.ts` at module init time to avoid a
|
|
61
|
+
* circular dependency (asset-spec → local-search → asset-spec).
|
|
62
|
+
*
|
|
63
|
+
* When `registerAssetType` is called with a spec that includes `rendererName`
|
|
64
|
+
* or `actionBuilder`, these hooks are invoked automatically so callers only
|
|
65
|
+
* need a single `registerAssetType(type, spec)` call to fully register a new
|
|
66
|
+
* asset type — no separate `registerTypeRenderer`/`registerActionBuilder` calls
|
|
67
|
+
* are required.
|
|
68
|
+
*/
|
|
69
|
+
let _registerTypeRenderer;
|
|
70
|
+
let _registerActionBuilder;
|
|
71
|
+
/**
|
|
72
|
+
* Called once by `local-search.ts` during module initialization to wire in the
|
|
73
|
+
* renderer and action-builder registration hooks.
|
|
74
|
+
*
|
|
75
|
+
* @internal — not part of the public extension API; use `registerAssetType` instead.
|
|
76
|
+
*/
|
|
77
|
+
export function _setAssetTypeHooks(rendererHook, actionBuilderHook) {
|
|
78
|
+
_registerTypeRenderer = rendererHook;
|
|
79
|
+
_registerActionBuilder = actionBuilderHook;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register a custom asset type with the Agentikit asset system.
|
|
83
|
+
*
|
|
84
|
+
* ## Full extension registration API
|
|
85
|
+
*
|
|
86
|
+
* Providing `rendererName` and/or `actionBuilder` in the spec automatically
|
|
87
|
+
* registers the renderer and action builder so that search results and `show`
|
|
88
|
+
* output work out of the box without additional calls.
|
|
89
|
+
*
|
|
90
|
+
* ### Minimal registration (filesystem layout only)
|
|
91
|
+
* ```ts
|
|
92
|
+
* registerAssetType("widget", {
|
|
93
|
+
* stashDir: "widgets",
|
|
94
|
+
* isRelevantFile: (f) => f.endsWith(".widget"),
|
|
95
|
+
* toCanonicalName: (root, fp) => path.basename(fp, ".widget"),
|
|
96
|
+
* toAssetPath: (root, name) => path.join(root, `${name}.widget`),
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* ### Full registration (filesystem + renderer + action)
|
|
101
|
+
* ```ts
|
|
102
|
+
* registerAssetType("widget", {
|
|
103
|
+
* stashDir: "widgets",
|
|
104
|
+
* isRelevantFile: (f) => f.endsWith(".widget"),
|
|
105
|
+
* toCanonicalName: (root, fp) => path.basename(fp, ".widget"),
|
|
106
|
+
* toAssetPath: (root, name) => path.join(root, `${name}.widget`),
|
|
107
|
+
* rendererName: "widget-md", // registered via registerRenderer() separately
|
|
108
|
+
* actionBuilder: (ref) => `akm show ${ref} -> use widget`,
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* If `rendererName` or `actionBuilder` is provided but the hooks have not yet
|
|
113
|
+
* been wired (i.e. `local-search.ts` has not been imported), the values are
|
|
114
|
+
* stored in the spec and will take effect once the hooks are set.
|
|
115
|
+
*/
|
|
116
|
+
export function registerAssetType(type, spec) {
|
|
117
|
+
ASSET_SPECS_INTERNAL[type] = spec;
|
|
118
|
+
TYPE_DIRS[type] = spec.stashDir;
|
|
119
|
+
ASSET_TYPES = getAssetTypes();
|
|
120
|
+
// Auto-register renderer and action builder if provided in spec
|
|
121
|
+
if (spec.rendererName && _registerTypeRenderer) {
|
|
122
|
+
_registerTypeRenderer(type, spec.rendererName);
|
|
123
|
+
}
|
|
124
|
+
if (spec.actionBuilder && _registerActionBuilder) {
|
|
125
|
+
_registerActionBuilder(type, spec.actionBuilder);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function getAssetTypes() {
|
|
129
|
+
return Object.keys(ASSET_SPECS_INTERNAL);
|
|
130
|
+
}
|
|
131
|
+
/** Warning: mutable `let` — stale if captured before `registerAssetType()` calls. Prefer `getAssetTypes()`. */
|
|
132
|
+
export let ASSET_TYPES = getAssetTypes();
|
|
133
|
+
export const TYPE_DIRS = Object.fromEntries(Object.entries(ASSET_SPECS_INTERNAL).map(([type, spec]) => [type, spec.stashDir]));
|
|
62
134
|
export function isRelevantAssetFile(assetType, fileName) {
|
|
63
|
-
return ASSET_SPECS[assetType]
|
|
135
|
+
return ASSET_SPECS[assetType]?.isRelevantFile(fileName) ?? false;
|
|
64
136
|
}
|
|
65
137
|
export function deriveCanonicalAssetName(assetType, typeRoot, filePath) {
|
|
66
|
-
return ASSET_SPECS[assetType]
|
|
138
|
+
return ASSET_SPECS[assetType]?.toCanonicalName(typeRoot, filePath);
|
|
67
139
|
}
|
|
68
140
|
export function deriveCanonicalAssetNameFromStashRoot(assetType, stashRoot, filePath) {
|
|
69
141
|
const relPath = toPosix(path.relative(stashRoot, filePath));
|
|
70
|
-
const
|
|
142
|
+
const segments = relPath.split("/").filter(Boolean);
|
|
143
|
+
const firstSegment = segments[0];
|
|
144
|
+
// When the first segment matches the canonical type dir (e.g. "agents"),
|
|
145
|
+
// use it as the type root so canonical names are relative to it.
|
|
146
|
+
// Otherwise fall back to stashRoot — this preserves the full relative path
|
|
147
|
+
// as the canonical name, which is correct for installed kits that live
|
|
148
|
+
// under custom directories (e.g. "tools/agents/svelte-file-editor").
|
|
71
149
|
const typeRoot = firstSegment === TYPE_DIRS[assetType] ? path.join(stashRoot, firstSegment) : stashRoot;
|
|
72
150
|
return deriveCanonicalAssetName(assetType, typeRoot, filePath);
|
|
73
151
|
}
|
|
74
152
|
export function resolveAssetPathFromName(assetType, typeRoot, name) {
|
|
75
|
-
|
|
153
|
+
const spec = ASSET_SPECS[assetType];
|
|
154
|
+
if (!spec)
|
|
155
|
+
throw new Error(`Unknown asset type: "${assetType}"`);
|
|
156
|
+
return spec.toAssetPath(typeRoot, name);
|
|
76
157
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,20 +3,21 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
5
|
import { resolveStashDir } from "./common";
|
|
6
|
-
import { getConfigPath, loadConfig, saveConfig } from "./config";
|
|
6
|
+
import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
|
|
7
7
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
|
|
8
8
|
import { ConfigError, NotFoundError, UsageError } from "./errors";
|
|
9
9
|
import { agentikitIndex } from "./indexer";
|
|
10
10
|
import { agentikitInit } from "./init";
|
|
11
|
+
import { agentikitList, agentikitRemove, agentikitUpdate } from "./installed-kits";
|
|
11
12
|
import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
|
|
13
|
+
import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
|
|
12
14
|
import { searchRegistry } from "./registry-search";
|
|
13
15
|
import { checkForUpdate, performUpgrade } from "./self-update";
|
|
14
16
|
import { agentikitAdd } from "./stash-add";
|
|
15
17
|
import { agentikitClone } from "./stash-clone";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { resolveStashSources } from "./stash-source";
|
|
18
|
+
import { agentikitSearch, parseSearchSource } from "./stash-search";
|
|
19
|
+
import { agentikitShowUnified } from "./stash-show";
|
|
20
|
+
import { addStashSource, listStashSources, removeStashSource } from "./stash-source-manage";
|
|
20
21
|
import { setQuiet, warn } from "./warn";
|
|
21
22
|
// Version: prefer compile-time define, then package.json, then fallback
|
|
22
23
|
const pkgVersion = (() => {
|
|
@@ -38,7 +39,7 @@ const pkgVersion = (() => {
|
|
|
38
39
|
})();
|
|
39
40
|
const OUTPUT_FORMATS = ["json", "yaml", "text"];
|
|
40
41
|
const DETAIL_LEVELS = ["brief", "normal", "full"];
|
|
41
|
-
const
|
|
42
|
+
const NORMAL_DESCRIPTION_LIMIT = 250;
|
|
42
43
|
function hasBunYAML(b) {
|
|
43
44
|
// biome-ignore lint/suspicious/noExplicitAny: type guard for runtime feature detection
|
|
44
45
|
return typeof b.YAML?.stringify === "function";
|
|
@@ -135,7 +136,8 @@ function shapeRegistrySearchOutput(result, detail) {
|
|
|
135
136
|
const assetHits = Array.isArray(result.assetHits) ? result.assetHits : [];
|
|
136
137
|
// Shape kit hits as registry type
|
|
137
138
|
const shapedKitHits = hits.map((hit) => shapeSearchHit({ ...hit, type: "registry" }, detail));
|
|
138
|
-
|
|
139
|
+
// Shape asset hits by detail level
|
|
140
|
+
const shapedAssetHits = assetHits.map((hit) => shapeAssetHit(hit, detail));
|
|
139
141
|
const shaped = {
|
|
140
142
|
hits: shapedKitHits,
|
|
141
143
|
...(shapedAssetHits.length > 0 ? { assetHits: shapedAssetHits } : {}),
|
|
@@ -146,42 +148,35 @@ function shapeRegistrySearchOutput(result, detail) {
|
|
|
146
148
|
}
|
|
147
149
|
return shaped;
|
|
148
150
|
}
|
|
151
|
+
function shapeAssetHit(hit, detail) {
|
|
152
|
+
if (detail === "brief")
|
|
153
|
+
return pickFields(hit, ["assetName", "assetType", "action"]);
|
|
154
|
+
if (detail === "normal") {
|
|
155
|
+
return capDescription(pickFields(hit, ["assetName", "assetType", "description", "kit", "action"]), NORMAL_DESCRIPTION_LIMIT);
|
|
156
|
+
}
|
|
157
|
+
return hit;
|
|
158
|
+
}
|
|
149
159
|
function shapeSearchHit(hit, detail) {
|
|
150
|
-
// Keep local and registry hit models separate internally so search and
|
|
151
|
-
// ranking logic can carry source-specific metadata. Normalize the external
|
|
152
|
-
// contract here so default CLI output stays compact and consistent.
|
|
153
160
|
if (hit.type === "registry") {
|
|
154
|
-
const brief = withTruncatedDescription(pickFields(hit, ["type", "name", "id", "description", "action", "curated"]));
|
|
155
161
|
if (detail === "brief")
|
|
156
|
-
return
|
|
157
|
-
if (detail === "normal")
|
|
158
|
-
return pickFields(hit, ["type", "name", "id", "description", "tags", "action", "curated"]);
|
|
159
|
-
return hit;
|
|
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;
|
|
162
|
+
return pickFields(hit, ["name", "action"]);
|
|
165
163
|
if (detail === "normal") {
|
|
166
|
-
return pickFields(hit, ["
|
|
164
|
+
return capDescription(pickFields(hit, ["name", "description", "action", "curated"]), NORMAL_DESCRIPTION_LIMIT);
|
|
167
165
|
}
|
|
168
166
|
return hit;
|
|
169
167
|
}
|
|
170
|
-
|
|
168
|
+
// Stash hit (local or remote)
|
|
171
169
|
if (detail === "brief")
|
|
172
|
-
return
|
|
170
|
+
return pickFields(hit, ["type", "name", "action"]);
|
|
173
171
|
if (detail === "normal") {
|
|
174
|
-
return pickFields(hit, ["type", "name", "
|
|
172
|
+
return capDescription(pickFields(hit, ["type", "name", "description", "action", "score"]), NORMAL_DESCRIPTION_LIMIT);
|
|
175
173
|
}
|
|
176
174
|
return hit;
|
|
177
175
|
}
|
|
178
|
-
function
|
|
176
|
+
function capDescription(hit, limit) {
|
|
179
177
|
if (typeof hit.description !== "string")
|
|
180
178
|
return hit;
|
|
181
|
-
return {
|
|
182
|
-
...hit,
|
|
183
|
-
description: truncateDescription(hit.description, BRIEF_DESCRIPTION_LIMIT),
|
|
184
|
-
};
|
|
179
|
+
return { ...hit, description: truncateDescription(hit.description, limit) };
|
|
185
180
|
}
|
|
186
181
|
function truncateDescription(description, limit) {
|
|
187
182
|
const normalized = description.replace(/\s+/g, " ").trim();
|
|
@@ -400,6 +395,13 @@ function formatSearchPlain(r, detail) {
|
|
|
400
395
|
}
|
|
401
396
|
return lines.join("\n").trimEnd();
|
|
402
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Naming Conventions:
|
|
400
|
+
* - stash-* : Operations on the user's local asset store (stash-show, stash-add, stash-clone)
|
|
401
|
+
* - stash-provider-* : Runtime data source providers (filesystem, openviking)
|
|
402
|
+
* - registry-* : Kit discovery from remote registries (npm, GitHub)
|
|
403
|
+
* - installed-kits : Management of kits already installed locally
|
|
404
|
+
*/
|
|
403
405
|
const initCommand = defineCommand({
|
|
404
406
|
meta: {
|
|
405
407
|
name: "init",
|
|
@@ -433,17 +435,21 @@ const searchCommand = defineCommand({
|
|
|
433
435
|
query: { type: "positional", description: "Search query (omit to list all assets)", required: false, default: "" },
|
|
434
436
|
type: {
|
|
435
437
|
type: "string",
|
|
436
|
-
description: "Asset type filter (skill
|
|
438
|
+
description: "Asset type filter (e.g. skill, command, agent, knowledge, script, memory, or any).",
|
|
437
439
|
},
|
|
438
440
|
limit: { type: "string", description: "Maximum number of results" },
|
|
439
|
-
source: { type: "string", description: "Search source (
|
|
441
|
+
source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
|
|
440
442
|
format: { type: "string", description: "Output format (json|text|yaml)" },
|
|
441
443
|
detail: { type: "string", description: "Detail level (brief|normal|full)" },
|
|
442
444
|
},
|
|
443
445
|
async run({ args }) {
|
|
444
446
|
await runWithJsonErrors(async () => {
|
|
445
447
|
const type = args.type;
|
|
446
|
-
const
|
|
448
|
+
const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
|
|
449
|
+
if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
|
|
450
|
+
throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
|
|
451
|
+
}
|
|
452
|
+
const limit = limitRaw;
|
|
447
453
|
const source = parseSearchSource(args.source);
|
|
448
454
|
const result = await agentikitSearch({ query: args.query, type, limit, source });
|
|
449
455
|
output("search", result);
|
|
@@ -506,6 +512,11 @@ const upgradeCommand = defineCommand({
|
|
|
506
512
|
args: {
|
|
507
513
|
check: { type: "boolean", description: "Check for updates without installing", default: false },
|
|
508
514
|
force: { type: "boolean", description: "Force upgrade even if on latest", default: false },
|
|
515
|
+
skipChecksum: {
|
|
516
|
+
type: "boolean",
|
|
517
|
+
description: "Skip checksum verification (not recommended)",
|
|
518
|
+
default: false,
|
|
519
|
+
},
|
|
509
520
|
},
|
|
510
521
|
async run({ args }) {
|
|
511
522
|
await runWithJsonErrors(async () => {
|
|
@@ -514,7 +525,7 @@ const upgradeCommand = defineCommand({
|
|
|
514
525
|
output("upgrade", check);
|
|
515
526
|
return;
|
|
516
527
|
}
|
|
517
|
-
const result = await performUpgrade(check, { force: args.force });
|
|
528
|
+
const result = await performUpgrade(check, { force: args.force, skipChecksum: args.skipChecksum });
|
|
518
529
|
output("upgrade", result);
|
|
519
530
|
});
|
|
520
531
|
},
|
|
@@ -557,7 +568,7 @@ const showCommand = defineCommand({
|
|
|
557
568
|
throw new UsageError(`Unknown view mode: ${args.akmView}. Expected one of: full|toc|frontmatter|section|lines`);
|
|
558
569
|
}
|
|
559
570
|
}
|
|
560
|
-
const result = await
|
|
571
|
+
const result = await agentikitShowUnified({ ref: args.ref, view });
|
|
561
572
|
output("show", result);
|
|
562
573
|
});
|
|
563
574
|
},
|
|
@@ -689,7 +700,7 @@ const registryCommand = defineCommand({
|
|
|
689
700
|
run() {
|
|
690
701
|
return runWithJsonErrors(() => {
|
|
691
702
|
const config = loadConfig();
|
|
692
|
-
const registries = config.registries ??
|
|
703
|
+
const registries = config.registries ?? DEFAULT_CONFIG.registries;
|
|
693
704
|
output("registry-list", { registries });
|
|
694
705
|
});
|
|
695
706
|
},
|
|
@@ -700,12 +711,16 @@ const registryCommand = defineCommand({
|
|
|
700
711
|
url: { type: "positional", description: "Registry index URL", required: true },
|
|
701
712
|
name: { type: "string", description: "Human-friendly name for the registry" },
|
|
702
713
|
provider: { type: "string", description: "Provider type (e.g. static-index, skills-sh)" },
|
|
714
|
+
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
703
715
|
},
|
|
704
716
|
run({ args }) {
|
|
705
717
|
return runWithJsonErrors(() => {
|
|
706
718
|
if (!args.url.startsWith("http")) {
|
|
707
719
|
throw new UsageError("Registry URL must start with http:// or https://");
|
|
708
720
|
}
|
|
721
|
+
if (args.url.startsWith("http://")) {
|
|
722
|
+
warn("Warning: registry URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
723
|
+
}
|
|
709
724
|
const config = loadConfig();
|
|
710
725
|
const registries = [...(config.registries ?? [])];
|
|
711
726
|
// Deduplicate by URL
|
|
@@ -718,6 +733,14 @@ const registryCommand = defineCommand({
|
|
|
718
733
|
entry.name = args.name;
|
|
719
734
|
if (args.provider)
|
|
720
735
|
entry.provider = args.provider;
|
|
736
|
+
if (args.options) {
|
|
737
|
+
try {
|
|
738
|
+
entry.options = JSON.parse(args.options);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
throw new UsageError("--options must be valid JSON");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
721
744
|
registries.push(entry);
|
|
722
745
|
saveConfig({ ...config, registries });
|
|
723
746
|
output("registry-add", { registries, added: true });
|
|
@@ -753,24 +776,117 @@ const registryCommand = defineCommand({
|
|
|
753
776
|
},
|
|
754
777
|
async run({ args }) {
|
|
755
778
|
await runWithJsonErrors(async () => {
|
|
756
|
-
const
|
|
757
|
-
|
|
779
|
+
const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
|
|
780
|
+
if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
|
|
781
|
+
throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
|
|
782
|
+
}
|
|
783
|
+
const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
|
|
758
784
|
output("registry-search", result);
|
|
759
785
|
});
|
|
760
786
|
},
|
|
761
787
|
}),
|
|
788
|
+
"build-index": defineCommand({
|
|
789
|
+
meta: { name: "build-index", description: "Build a v2 registry index from discovery and manual entries" },
|
|
790
|
+
args: {
|
|
791
|
+
out: { type: "string", description: "Output path for the generated index", default: "index.json" },
|
|
792
|
+
manual: { type: "string", description: "Manual entries JSON file", default: "manual-entries.json" },
|
|
793
|
+
npmRegistry: { type: "string", description: "Override npm registry base URL" },
|
|
794
|
+
githubApi: { type: "string", description: "Override GitHub API base URL" },
|
|
795
|
+
},
|
|
796
|
+
async run({ args }) {
|
|
797
|
+
await runWithJsonErrors(async () => {
|
|
798
|
+
const result = await buildRegistryIndex({
|
|
799
|
+
manualEntriesPath: args.manual,
|
|
800
|
+
npmRegistryBase: args.npmRegistry,
|
|
801
|
+
githubApiBase: args.githubApi,
|
|
802
|
+
});
|
|
803
|
+
const outPath = writeRegistryIndex(result.index, args.out);
|
|
804
|
+
output("registry-build-index", {
|
|
805
|
+
outPath,
|
|
806
|
+
version: result.index.version,
|
|
807
|
+
updatedAt: result.index.updatedAt,
|
|
808
|
+
totalKits: result.counts.total,
|
|
809
|
+
counts: result.counts,
|
|
810
|
+
manualEntriesPath: result.paths.manualEntriesPath,
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
}),
|
|
762
815
|
},
|
|
763
816
|
});
|
|
817
|
+
/**
|
|
818
|
+
* Shared subcommand definitions for stash source management.
|
|
819
|
+
* Used by both `akm stash` (preferred) and `akm sources` (legacy alias).
|
|
820
|
+
*/
|
|
821
|
+
function buildSourceSubCommands(outputPrefix) {
|
|
822
|
+
return {
|
|
823
|
+
list: defineCommand({
|
|
824
|
+
meta: { name: "list", description: "List all stash sources" },
|
|
825
|
+
run() {
|
|
826
|
+
return runWithJsonErrors(() => {
|
|
827
|
+
output(`${outputPrefix}`, listStashSources());
|
|
828
|
+
});
|
|
829
|
+
},
|
|
830
|
+
}),
|
|
831
|
+
add: defineCommand({
|
|
832
|
+
meta: { name: "add", description: "Add a stash source (filesystem path or remote URL)" },
|
|
833
|
+
args: {
|
|
834
|
+
target: { type: "positional", description: "Path or URL to add", required: true },
|
|
835
|
+
name: { type: "string", description: "Human-friendly name for the source" },
|
|
836
|
+
provider: { type: "string", description: "Provider type (e.g. openviking). Required for URLs." },
|
|
837
|
+
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
838
|
+
},
|
|
839
|
+
run({ args }) {
|
|
840
|
+
return runWithJsonErrors(() => {
|
|
841
|
+
if (args.target.startsWith("http://")) {
|
|
842
|
+
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
843
|
+
}
|
|
844
|
+
let parsedOptions;
|
|
845
|
+
if (args.options) {
|
|
846
|
+
try {
|
|
847
|
+
const parsed = JSON.parse(args.options);
|
|
848
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
849
|
+
throw new UsageError("--options must be a JSON object");
|
|
850
|
+
}
|
|
851
|
+
parsedOptions = parsed;
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
if (err instanceof UsageError)
|
|
855
|
+
throw err;
|
|
856
|
+
throw new UsageError("--options must be valid JSON");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const result = addStashSource({
|
|
860
|
+
target: args.target,
|
|
861
|
+
name: args.name,
|
|
862
|
+
providerType: args.provider,
|
|
863
|
+
options: parsedOptions,
|
|
864
|
+
});
|
|
865
|
+
output(`${outputPrefix}-add`, result);
|
|
866
|
+
});
|
|
867
|
+
},
|
|
868
|
+
}),
|
|
869
|
+
remove: defineCommand({
|
|
870
|
+
meta: { name: "remove", description: "Remove a stash source by URL, path, or name" },
|
|
871
|
+
args: {
|
|
872
|
+
target: { type: "positional", description: "Source URL, path, or name to remove", required: true },
|
|
873
|
+
},
|
|
874
|
+
run({ args }) {
|
|
875
|
+
return runWithJsonErrors(() => {
|
|
876
|
+
const result = removeStashSource(args.target);
|
|
877
|
+
output(`${outputPrefix}-remove`, result);
|
|
878
|
+
});
|
|
879
|
+
},
|
|
880
|
+
}),
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const stashCommand = defineCommand({
|
|
884
|
+
meta: { name: "stash", description: "Manage stash sources (local paths and remote providers)" },
|
|
885
|
+
subCommands: buildSourceSubCommands("stash"),
|
|
886
|
+
});
|
|
764
887
|
const sourcesCommand = defineCommand({
|
|
765
|
-
meta: { name: "sources", description: "
|
|
766
|
-
|
|
767
|
-
return runWithJsonErrors(() => {
|
|
768
|
-
const config = loadConfig();
|
|
769
|
-
const sources = resolveStashSources();
|
|
770
|
-
const registries = config.registries ?? [];
|
|
771
|
-
output("sources", { sources, registries });
|
|
772
|
-
});
|
|
773
|
-
},
|
|
888
|
+
meta: { name: "sources", description: "Manage stash sources (alias for 'akm stash')" },
|
|
889
|
+
subCommands: buildSourceSubCommands("sources"),
|
|
774
890
|
});
|
|
775
891
|
const hintsCommand = defineCommand({
|
|
776
892
|
meta: {
|
|
@@ -807,24 +923,19 @@ const main = defineCommand({
|
|
|
807
923
|
search: searchCommand,
|
|
808
924
|
show: showCommand,
|
|
809
925
|
clone: cloneCommand,
|
|
926
|
+
stash: stashCommand,
|
|
810
927
|
sources: sourcesCommand,
|
|
811
928
|
registry: registryCommand,
|
|
812
929
|
config: configCommand,
|
|
813
930
|
hints: hintsCommand,
|
|
814
931
|
},
|
|
815
932
|
});
|
|
816
|
-
const SEARCH_SOURCES = ["local", "registry", "both"];
|
|
817
933
|
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
|
|
818
934
|
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
819
935
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
820
936
|
// so we must replace process.argv with the normalized version before runMain.
|
|
821
937
|
process.argv = normalizeShowArgv(process.argv);
|
|
822
938
|
runMain(main);
|
|
823
|
-
function parseSearchSource(value) {
|
|
824
|
-
if (SEARCH_SOURCES.includes(value))
|
|
825
|
-
return value;
|
|
826
|
-
throw new UsageError(`Invalid value for --source: ${value}. Expected one of: ${SEARCH_SOURCES.join("|")}`);
|
|
827
|
-
}
|
|
828
939
|
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
829
940
|
const EXIT_GENERAL = 1;
|
|
830
941
|
const EXIT_USAGE = 2;
|
|
@@ -866,7 +977,7 @@ function buildHint(message) {
|
|
|
866
977
|
if (message.includes("remote package fetched but asset not found"))
|
|
867
978
|
return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
|
|
868
979
|
if (message.includes("Invalid value for --source"))
|
|
869
|
-
return "Pick one of:
|
|
980
|
+
return "Pick one of: stash, registry, both.";
|
|
870
981
|
if (message.includes("Invalid value for --format"))
|
|
871
982
|
return "Pick one of: json, text, yaml.";
|
|
872
983
|
if (message.includes("Invalid value for --detail"))
|
|
@@ -975,7 +1086,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
975
1086
|
\`\`\`sh
|
|
976
1087
|
akm search "<query>" # Search for assets
|
|
977
1088
|
akm search "<query>" --type skill # Filter by type
|
|
978
|
-
akm search "<query>" --source both # Search
|
|
1089
|
+
akm search "<query>" --source both # Search stash providers and registries for assets
|
|
979
1090
|
akm show <ref> # View asset details
|
|
980
1091
|
akm add <ref> # Install a kit (npm, GitHub, git, local)
|
|
981
1092
|
akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
|
|
@@ -1003,7 +1114,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
1003
1114
|
\`\`\`sh
|
|
1004
1115
|
akm search "<query>" # Search local stash
|
|
1005
1116
|
akm search "<query>" --type skill # Filter by asset type
|
|
1006
|
-
akm search "<query>" --source both # Search
|
|
1117
|
+
akm search "<query>" --source both # Search all stash providers and registries
|
|
1007
1118
|
akm search "<query>" --source registry # Search registries only
|
|
1008
1119
|
akm search "<query>" --limit 10 # Limit results
|
|
1009
1120
|
akm search "<query>" --detail full # Include scores, paths, timing
|
|
@@ -1011,8 +1122,8 @@ akm search "<query>" --detail full # Include scores, paths, timing
|
|
|
1011
1122
|
|
|
1012
1123
|
| Flag | Values | Default |
|
|
1013
1124
|
| --- | --- | --- |
|
|
1014
|
-
| \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`any\` | \`any\` |
|
|
1015
|
-
| \`--source\` | \`
|
|
1125
|
+
| \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`memory\`, \`any\` | \`any\` |
|
|
1126
|
+
| \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
|
|
1016
1127
|
| \`--limit\` | number | \`20\` |
|
|
1017
1128
|
| \`--format\` | \`json\`, \`text\`, \`yaml\` | \`json\` |
|
|
1018
1129
|
| \`--detail\` | \`brief\`, \`normal\`, \`full\` | \`brief\` |
|
|
@@ -1029,6 +1140,7 @@ akm show agent:architect # Show agent (returns system promp
|
|
|
1029
1140
|
akm show knowledge:guide toc # Table of contents
|
|
1030
1141
|
akm show knowledge:guide section "Auth" # Specific section
|
|
1031
1142
|
akm show knowledge:guide lines 10 30 # Line range
|
|
1143
|
+
akm show viking://resources/my-doc # Show remote OpenViking content
|
|
1032
1144
|
\`\`\`
|
|
1033
1145
|
|
|
1034
1146
|
| Type | Key fields returned |
|
|
@@ -1038,6 +1150,7 @@ akm show knowledge:guide lines 10 30 # Line range
|
|
|
1038
1150
|
| command | \`template\`, \`description\`, \`parameters\` |
|
|
1039
1151
|
| agent | \`prompt\`, \`description\`, \`modelHint\`, \`toolPolicy\` |
|
|
1040
1152
|
| knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
|
|
1153
|
+
| memory | \`content\` (recalled context) |
|
|
1041
1154
|
|
|
1042
1155
|
## Install & Manage Kits
|
|
1043
1156
|
|
|
@@ -1076,6 +1189,8 @@ akm registry add <url> --provider skills-sh # Specify provider type
|
|
|
1076
1189
|
akm registry remove <url-or-name> # Remove a registry
|
|
1077
1190
|
akm registry search "<query>" # Search all registries
|
|
1078
1191
|
akm registry search "<query>" --assets # Include asset-level results
|
|
1192
|
+
akm registry build-index # Build ./index.json
|
|
1193
|
+
akm registry build-index --out dist/index.json # Build to a custom path
|
|
1079
1194
|
\`\`\`
|
|
1080
1195
|
|
|
1081
1196
|
## Configuration
|
package/dist/common.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getConfigPath, getDefaultStashDir } from "./paths";
|
|
|
7
7
|
export const IS_WINDOWS = process.platform === "win32";
|
|
8
8
|
// ── Validators ──────────────────────────────────────────────────────────────
|
|
9
9
|
export function isAssetType(type) {
|
|
10
|
-
return type
|
|
10
|
+
return Object.hasOwn(TYPE_DIRS, type);
|
|
11
11
|
}
|
|
12
12
|
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
13
13
|
/**
|
|
@@ -16,6 +16,11 @@ export function isAssetType(type) {
|
|
|
16
16
|
* 2. stashDir field in config.json
|
|
17
17
|
* 3. Platform default (~/akm or ~/Documents/akm on Windows)
|
|
18
18
|
*
|
|
19
|
+
* WARNING: May write to config file as a side effect when AKM_STASH_DIR is set.
|
|
20
|
+
* Specifically, when AKM_STASH_DIR is set and `options.readOnly` is not true,
|
|
21
|
+
* this function calls `persistStashDirToConfig()` which writes the resolved
|
|
22
|
+
* path into config.json on disk.
|
|
23
|
+
*
|
|
19
24
|
* Throws if no valid stash directory is found.
|
|
20
25
|
*/
|
|
21
26
|
export function resolveStashDir(options) {
|
|
@@ -82,6 +87,11 @@ function readStashDirFromConfig() {
|
|
|
82
87
|
/**
|
|
83
88
|
* Persist stashDir to config.json if not already set, so users can
|
|
84
89
|
* transition away from relying on the AKM_STASH_DIR env var.
|
|
90
|
+
*
|
|
91
|
+
* WARNING: This function writes to disk (config.json). It is called as a side
|
|
92
|
+
* effect of `resolveStashDir()` when AKM_STASH_DIR is set and `readOnly` is
|
|
93
|
+
* not true. Callers that must not touch the filesystem should pass
|
|
94
|
+
* `{ readOnly: true }` to `resolveStashDir()`.
|
|
85
95
|
*/
|
|
86
96
|
function persistStashDirToConfig(stashDir) {
|
|
87
97
|
try {
|
|
@@ -101,7 +111,7 @@ function persistStashDirToConfig(stashDir) {
|
|
|
101
111
|
raw.stashDir = stashDir;
|
|
102
112
|
const dir = path.dirname(configPath);
|
|
103
113
|
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
-
const tmpPath = `${configPath}.tmp.${process.pid}`;
|
|
114
|
+
const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
105
115
|
fs.writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
106
116
|
fs.renameSync(tmpPath, configPath);
|
|
107
117
|
}
|
|
@@ -196,3 +206,6 @@ function parseRetryAfter(response) {
|
|
|
196
206
|
const seconds = parseInt(header, 10);
|
|
197
207
|
return Number.isNaN(seconds) ? undefined : seconds * 1000;
|
|
198
208
|
}
|
|
209
|
+
export function toErrorMessage(error) {
|
|
210
|
+
return error instanceof Error ? error.message : String(error);
|
|
211
|
+
}
|