akm-cli 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/asset-spec.js +96 -9
- package/dist/cli.js +195 -55
- package/dist/common.js +15 -2
- package/dist/config-cli.js +65 -6
- package/dist/config.js +206 -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 +22 -16
- 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 +52 -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 +33 -3
- package/dist/stash-search.js +70 -402
- package/dist/stash-show.js +24 -5
- 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,18 +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);
|
|
139
|
+
}
|
|
140
|
+
export function deriveCanonicalAssetNameFromStashRoot(assetType, stashRoot, filePath) {
|
|
141
|
+
const relPath = toPosix(path.relative(stashRoot, filePath));
|
|
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").
|
|
149
|
+
const typeRoot = firstSegment === TYPE_DIRS[assetType] ? path.join(stashRoot, firstSegment) : stashRoot;
|
|
150
|
+
return deriveCanonicalAssetName(assetType, typeRoot, filePath);
|
|
67
151
|
}
|
|
68
152
|
export function resolveAssetPathFromName(assetType, typeRoot, name) {
|
|
69
|
-
|
|
153
|
+
const spec = ASSET_SPECS[assetType];
|
|
154
|
+
if (!spec)
|
|
155
|
+
throw new Error(`Unknown asset type: "${assetType}"`);
|
|
156
|
+
return spec.toAssetPath(typeRoot, name);
|
|
70
157
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,19 +3,20 @@ 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 { agentikitShow } from "./stash-show";
|
|
18
|
+
import { agentikitSearch, parseSearchSource } from "./stash-search";
|
|
19
|
+
import { agentikitShowUnified } from "./stash-show";
|
|
19
20
|
import { resolveStashSources } from "./stash-source";
|
|
20
21
|
import { setQuiet, warn } from "./warn";
|
|
21
22
|
// Version: prefer compile-time define, then package.json, then fallback
|
|
@@ -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,23 +776,142 @@ 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
|
});
|
|
764
817
|
const sourcesCommand = defineCommand({
|
|
765
|
-
meta: { name: "sources", description: "
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
818
|
+
meta: { name: "sources", description: "Manage stash sources (local paths and remote providers)" },
|
|
819
|
+
subCommands: {
|
|
820
|
+
list: defineCommand({
|
|
821
|
+
meta: { name: "list", description: "List all stash sources" },
|
|
822
|
+
run() {
|
|
823
|
+
return runWithJsonErrors(() => {
|
|
824
|
+
const config = loadConfig();
|
|
825
|
+
const localSources = resolveStashSources();
|
|
826
|
+
const stashes = config.stashes ?? [];
|
|
827
|
+
// Legacy fallback: show remoteStashSources if no stashes config
|
|
828
|
+
const legacyRemote = !config.stashes ? (config.remoteStashSources ?? []) : [];
|
|
829
|
+
output("sources", {
|
|
830
|
+
localSources,
|
|
831
|
+
stashes,
|
|
832
|
+
...(legacyRemote.length > 0 ? { remoteSources: legacyRemote } : {}),
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
},
|
|
836
|
+
}),
|
|
837
|
+
add: defineCommand({
|
|
838
|
+
meta: { name: "add", description: "Add a stash source (filesystem path or remote URL)" },
|
|
839
|
+
args: {
|
|
840
|
+
target: { type: "positional", description: "Path or URL to add", required: true },
|
|
841
|
+
name: { type: "string", description: "Human-friendly name for the source" },
|
|
842
|
+
provider: { type: "string", description: "Provider type (e.g. openviking). Required for URLs." },
|
|
843
|
+
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
844
|
+
},
|
|
845
|
+
run({ args }) {
|
|
846
|
+
return runWithJsonErrors(() => {
|
|
847
|
+
const config = loadConfig();
|
|
848
|
+
const stashes = [...(config.stashes ?? [])];
|
|
849
|
+
const isUrl = args.target.startsWith("http://") || args.target.startsWith("https://");
|
|
850
|
+
if (isUrl) {
|
|
851
|
+
if (args.target.startsWith("http://")) {
|
|
852
|
+
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
853
|
+
}
|
|
854
|
+
const providerType = args.provider;
|
|
855
|
+
if (!providerType) {
|
|
856
|
+
throw new UsageError("--provider is required for URL sources (e.g. --provider openviking)");
|
|
857
|
+
}
|
|
858
|
+
if (stashes.some((s) => s.url === args.target)) {
|
|
859
|
+
output("sources-add", { stashes, added: false, message: "Source URL already configured" });
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const entry = { type: providerType, url: args.target };
|
|
863
|
+
if (args.name)
|
|
864
|
+
entry.name = args.name;
|
|
865
|
+
if (args.options) {
|
|
866
|
+
try {
|
|
867
|
+
entry.options = JSON.parse(args.options);
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
throw new UsageError("--options must be valid JSON");
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
stashes.push(entry);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// Filesystem path
|
|
877
|
+
const resolvedPath = path.resolve(args.target);
|
|
878
|
+
if (stashes.some((s) => s.path === resolvedPath)) {
|
|
879
|
+
output("sources-add", { stashes, added: false, message: "Source path already configured" });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const entry = { type: "filesystem", path: resolvedPath };
|
|
883
|
+
if (args.name)
|
|
884
|
+
entry.name = args.name;
|
|
885
|
+
stashes.push(entry);
|
|
886
|
+
}
|
|
887
|
+
// Migrate: remove remoteStashSources when moving to stashes
|
|
888
|
+
const { remoteStashSources, ...rest } = config;
|
|
889
|
+
saveConfig({ ...rest, stashes });
|
|
890
|
+
output("sources-add", { stashes, added: true });
|
|
891
|
+
});
|
|
892
|
+
},
|
|
893
|
+
}),
|
|
894
|
+
remove: defineCommand({
|
|
895
|
+
meta: { name: "remove", description: "Remove a stash source by URL, path, or name" },
|
|
896
|
+
args: {
|
|
897
|
+
target: { type: "positional", description: "Source URL, path, or name to remove", required: true },
|
|
898
|
+
},
|
|
899
|
+
run({ args }) {
|
|
900
|
+
return runWithJsonErrors(() => {
|
|
901
|
+
const config = loadConfig();
|
|
902
|
+
const stashes = [...(config.stashes ?? [])];
|
|
903
|
+
const resolvedTarget = args.target.startsWith("http") ? args.target : path.resolve(args.target);
|
|
904
|
+
const idx = stashes.findIndex((s) => s.url === resolvedTarget || s.path === resolvedTarget || s.name === resolvedTarget);
|
|
905
|
+
if (idx === -1) {
|
|
906
|
+
output("sources-remove", { stashes, removed: false, message: "No matching source found" });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const removed = stashes.splice(idx, 1)[0];
|
|
910
|
+
saveConfig({ ...config, stashes });
|
|
911
|
+
output("sources-remove", { stashes, removed: true, entry: removed });
|
|
912
|
+
});
|
|
913
|
+
},
|
|
914
|
+
}),
|
|
773
915
|
},
|
|
774
916
|
});
|
|
775
917
|
const hintsCommand = defineCommand({
|
|
@@ -813,18 +955,12 @@ const main = defineCommand({
|
|
|
813
955
|
hints: hintsCommand,
|
|
814
956
|
},
|
|
815
957
|
});
|
|
816
|
-
const SEARCH_SOURCES = ["local", "registry", "both"];
|
|
817
958
|
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
|
|
818
959
|
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
819
960
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
820
961
|
// so we must replace process.argv with the normalized version before runMain.
|
|
821
962
|
process.argv = normalizeShowArgv(process.argv);
|
|
822
963
|
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
964
|
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
829
965
|
const EXIT_GENERAL = 1;
|
|
830
966
|
const EXIT_USAGE = 2;
|
|
@@ -866,7 +1002,7 @@ function buildHint(message) {
|
|
|
866
1002
|
if (message.includes("remote package fetched but asset not found"))
|
|
867
1003
|
return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
|
|
868
1004
|
if (message.includes("Invalid value for --source"))
|
|
869
|
-
return "Pick one of:
|
|
1005
|
+
return "Pick one of: stash, registry, both.";
|
|
870
1006
|
if (message.includes("Invalid value for --format"))
|
|
871
1007
|
return "Pick one of: json, text, yaml.";
|
|
872
1008
|
if (message.includes("Invalid value for --detail"))
|
|
@@ -975,7 +1111,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
975
1111
|
\`\`\`sh
|
|
976
1112
|
akm search "<query>" # Search for assets
|
|
977
1113
|
akm search "<query>" --type skill # Filter by type
|
|
978
|
-
akm search "<query>" --source both # Search
|
|
1114
|
+
akm search "<query>" --source both # Search stash providers and registries for assets
|
|
979
1115
|
akm show <ref> # View asset details
|
|
980
1116
|
akm add <ref> # Install a kit (npm, GitHub, git, local)
|
|
981
1117
|
akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
|
|
@@ -1003,7 +1139,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
|
|
|
1003
1139
|
\`\`\`sh
|
|
1004
1140
|
akm search "<query>" # Search local stash
|
|
1005
1141
|
akm search "<query>" --type skill # Filter by asset type
|
|
1006
|
-
akm search "<query>" --source both # Search
|
|
1142
|
+
akm search "<query>" --source both # Search all stash providers and registries
|
|
1007
1143
|
akm search "<query>" --source registry # Search registries only
|
|
1008
1144
|
akm search "<query>" --limit 10 # Limit results
|
|
1009
1145
|
akm search "<query>" --detail full # Include scores, paths, timing
|
|
@@ -1011,8 +1147,8 @@ akm search "<query>" --detail full # Include scores, paths, timing
|
|
|
1011
1147
|
|
|
1012
1148
|
| Flag | Values | Default |
|
|
1013
1149
|
| --- | --- | --- |
|
|
1014
|
-
| \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`any\` | \`any\` |
|
|
1015
|
-
| \`--source\` | \`
|
|
1150
|
+
| \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`memory\`, \`any\` | \`any\` |
|
|
1151
|
+
| \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
|
|
1016
1152
|
| \`--limit\` | number | \`20\` |
|
|
1017
1153
|
| \`--format\` | \`json\`, \`text\`, \`yaml\` | \`json\` |
|
|
1018
1154
|
| \`--detail\` | \`brief\`, \`normal\`, \`full\` | \`brief\` |
|
|
@@ -1029,6 +1165,7 @@ akm show agent:architect # Show agent (returns system promp
|
|
|
1029
1165
|
akm show knowledge:guide toc # Table of contents
|
|
1030
1166
|
akm show knowledge:guide section "Auth" # Specific section
|
|
1031
1167
|
akm show knowledge:guide lines 10 30 # Line range
|
|
1168
|
+
akm show viking://resources/my-doc # Show remote OpenViking content
|
|
1032
1169
|
\`\`\`
|
|
1033
1170
|
|
|
1034
1171
|
| Type | Key fields returned |
|
|
@@ -1038,6 +1175,7 @@ akm show knowledge:guide lines 10 30 # Line range
|
|
|
1038
1175
|
| command | \`template\`, \`description\`, \`parameters\` |
|
|
1039
1176
|
| agent | \`prompt\`, \`description\`, \`modelHint\`, \`toolPolicy\` |
|
|
1040
1177
|
| knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
|
|
1178
|
+
| memory | \`content\` (recalled context) |
|
|
1041
1179
|
|
|
1042
1180
|
## Install & Manage Kits
|
|
1043
1181
|
|
|
@@ -1076,6 +1214,8 @@ akm registry add <url> --provider skills-sh # Specify provider type
|
|
|
1076
1214
|
akm registry remove <url-or-name> # Remove a registry
|
|
1077
1215
|
akm registry search "<query>" # Search all registries
|
|
1078
1216
|
akm registry search "<query>" --assets # Include asset-level results
|
|
1217
|
+
akm registry build-index # Build ./index.json
|
|
1218
|
+
akm registry build-index --out dist/index.json # Build to a custom path
|
|
1079
1219
|
\`\`\`
|
|
1080
1220
|
|
|
1081
1221
|
## 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
|
+
}
|