akm-cli 0.3.0 → 0.4.0

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/matchers.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - `extensionMatcher` (3) -- classifies any file by extension alone.
9
9
  * Ensures every known file type is discoverable regardless of directory.
10
- * - `directoryMatcher` (10) -- boosts specificity when the first ancestor
10
+ * - `directoryMatcher` (10) -- boosts specificity when an ancestor
11
11
  * directory matches a known type name (e.g. `scripts/`, `agents/`).
12
12
  * - `parentDirHintMatcher` (15) -- boosts specificity based on the
13
13
  * immediate parent directory name.
@@ -43,31 +43,34 @@ export function extensionMatcher(ctx) {
43
43
  }
44
44
  // ── directoryMatcher (specificity: 10) ──────────────────────────────────────
45
45
  /**
46
- * Directory-based matcher that boosts specificity when the first ancestor
46
+ * Directory-based matcher that boosts specificity when an ancestor
47
47
  * directory segment from the stash root matches a known type name.
48
+ *
49
+ * The first matching type-like ancestor wins. This preserves intuitive
50
+ * behavior for nested kit layouts such as `agent-stash/agents/blog/foo.md`
51
+ * while still honoring earlier type roots like `commands/agents/foo.md`.
48
52
  */
49
53
  export function directoryMatcher(ctx) {
50
- const topDir = ctx.ancestorDirs[0];
51
- if (!topDir)
52
- return null;
53
54
  const ext = ctx.ext;
54
- if (topDir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
55
- return { type: "script", specificity: 10, renderer: "script-source" };
56
- }
57
- if (topDir === "skills" && ctx.fileName === "SKILL.md") {
58
- return { type: "skill", specificity: 10, renderer: "skill-md" };
59
- }
60
- if (topDir === "commands" && ext === ".md") {
61
- return { type: "command", specificity: 10, renderer: "command-md" };
62
- }
63
- if (topDir === "agents" && ext === ".md") {
64
- return { type: "agent", specificity: 10, renderer: "agent-md" };
65
- }
66
- if (topDir === "knowledge" && ext === ".md") {
67
- return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
68
- }
69
- if (topDir === "memories" && ext === ".md") {
70
- return { type: "memory", specificity: 10, renderer: "memory-md" };
55
+ for (const dir of ctx.ancestorDirs) {
56
+ if (dir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
57
+ return { type: "script", specificity: 10, renderer: "script-source" };
58
+ }
59
+ if (dir === "skills" && ctx.fileName === "SKILL.md") {
60
+ return { type: "skill", specificity: 10, renderer: "skill-md" };
61
+ }
62
+ if (dir === "commands" && ext === ".md") {
63
+ return { type: "command", specificity: 10, renderer: "command-md" };
64
+ }
65
+ if (dir === "agents" && ext === ".md") {
66
+ return { type: "agent", specificity: 10, renderer: "agent-md" };
67
+ }
68
+ if (dir === "knowledge" && ext === ".md") {
69
+ return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
70
+ }
71
+ if (dir === "memories" && ext === ".md") {
72
+ return { type: "memory", specificity: 10, renderer: "memory-md" };
73
+ }
71
74
  }
72
75
  return null;
73
76
  }
@@ -4,7 +4,8 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { TYPE_DIRS } from "./asset-spec";
6
6
  import { fetchWithRetry, isWithin } from "./common";
7
- import { loadConfig, saveConfig } from "./config";
7
+ import { loadConfig, loadUserConfig, saveConfig } from "./config";
8
+ import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
8
9
  import { copyIncludedPaths, findNearestIncludeConfig } from "./kit-include";
9
10
  import { getRegistryCacheDir as _getRegistryCacheDir } from "./paths";
10
11
  import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "./registry-resolve";
@@ -12,13 +13,20 @@ import { warn } from "./warn";
12
13
  const REGISTRY_STASH_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
13
14
  export async function installRegistryRef(ref, options) {
14
15
  const parsed = parseRegistryRef(ref);
16
+ const config = loadConfig();
15
17
  if (parsed.source === "local") {
16
- return installLocalRegistryRef(parsed, options);
18
+ return installLocalRegistryRef(parsed, config, options);
17
19
  }
18
20
  if (parsed.source === "git") {
19
- return installGitRegistryRef(parsed, options);
21
+ return installGitRegistryRef(parsed, config, options);
20
22
  }
21
23
  const resolved = await resolveRegistryArtifact(parsed);
24
+ const registryLabels = deriveRegistryLabels({
25
+ source: resolved.source,
26
+ ref: resolved.ref,
27
+ artifactUrl: resolved.artifactUrl,
28
+ });
29
+ enforceRegistryInstallPolicy(registryLabels, config, ref);
22
30
  const installedAt = (options?.now ?? new Date()).toISOString();
23
31
  const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
24
32
  const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id, resolved.resolvedVersion ?? resolved.resolvedRevision);
@@ -30,6 +38,7 @@ export async function installRegistryRef(ref, options) {
30
38
  const cachedStashRoot = detectStashRoot(extractedDir);
31
39
  if (cachedStashRoot) {
32
40
  const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
41
+ const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
33
42
  return {
34
43
  id: resolved.id,
35
44
  source: resolved.source,
@@ -42,6 +51,7 @@ export async function installRegistryRef(ref, options) {
42
51
  extractedDir,
43
52
  stashRoot: cachedStashRoot,
44
53
  integrity,
54
+ audit,
45
55
  };
46
56
  }
47
57
  }
@@ -54,11 +64,13 @@ export async function installRegistryRef(ref, options) {
54
64
  let provisionalKitRoot;
55
65
  let installRoot;
56
66
  let stashRoot;
67
+ let audit;
57
68
  try {
58
69
  await downloadArchive(resolved.artifactUrl, archivePath);
59
70
  verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
60
71
  integrity = await computeFileHash(archivePath);
61
72
  extractTarGzSecure(archivePath, extractedDir);
73
+ audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
62
74
  provisionalKitRoot = detectStashRoot(extractedDir);
63
75
  installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
64
76
  stashRoot = detectStashRoot(installRoot);
@@ -86,11 +98,18 @@ export async function installRegistryRef(ref, options) {
86
98
  extractedDir,
87
99
  stashRoot,
88
100
  integrity,
101
+ audit,
89
102
  };
90
103
  }
91
- async function installLocalRegistryRef(parsed, options) {
104
+ async function installLocalRegistryRef(parsed, config, options) {
92
105
  const resolved = await resolveRegistryArtifact(parsed);
93
106
  const installedAt = (options?.now ?? new Date()).toISOString();
107
+ const registryLabels = deriveRegistryLabels({
108
+ source: resolved.source,
109
+ ref: resolved.ref,
110
+ artifactUrl: resolved.artifactUrl,
111
+ });
112
+ const audit = runInstallAuditOrThrow(parsed.sourcePath, resolved.source, resolved.ref, registryLabels, config);
94
113
  // For local directories, detect the stash root within the source path.
95
114
  // If no nested stash is found, the source path itself is used.
96
115
  const stashRoot = detectStashRoot(parsed.sourcePath);
@@ -105,10 +124,18 @@ async function installLocalRegistryRef(parsed, options) {
105
124
  cacheDir: parsed.sourcePath,
106
125
  extractedDir: parsed.sourcePath,
107
126
  stashRoot,
127
+ audit,
108
128
  };
109
129
  }
110
- async function installGitRegistryRef(parsed, options) {
130
+ async function installGitRegistryRef(parsed, config, options) {
111
131
  const resolved = await resolveRegistryArtifact(parsed);
132
+ const registryLabels = deriveRegistryLabels({
133
+ source: resolved.source,
134
+ ref: resolved.ref,
135
+ artifactUrl: resolved.artifactUrl,
136
+ gitUrl: parsed.url,
137
+ });
138
+ enforceRegistryInstallPolicy(registryLabels, config, parsed.ref);
112
139
  const installedAt = (options?.now ?? new Date()).toISOString();
113
140
  const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
114
141
  const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, resolved.resolvedRevision);
@@ -121,6 +148,7 @@ async function installGitRegistryRef(parsed, options) {
121
148
  const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
122
149
  const stashRoot = detectStashRoot(installRoot);
123
150
  if (stashRoot) {
151
+ const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
124
152
  return {
125
153
  id: resolved.id,
126
154
  source: resolved.source,
@@ -132,6 +160,7 @@ async function installGitRegistryRef(parsed, options) {
132
160
  cacheDir,
133
161
  extractedDir,
134
162
  stashRoot,
163
+ audit,
135
164
  };
136
165
  }
137
166
  }
@@ -147,6 +176,7 @@ async function installGitRegistryRef(parsed, options) {
147
176
  let provisionalKitRoot;
148
177
  let installRoot;
149
178
  let stashRoot;
179
+ let audit;
150
180
  try {
151
181
  const cloneArgs = ["clone", "--depth", "1"];
152
182
  if (parsed.requestedRef) {
@@ -163,6 +193,7 @@ async function installGitRegistryRef(parsed, options) {
163
193
  copyDirectoryContents(cloneDir, extractedDir);
164
194
  // Clean up the clone dir
165
195
  fs.rmSync(cloneDir, { recursive: true, force: true });
196
+ audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
166
197
  provisionalKitRoot = detectStashRoot(extractedDir);
167
198
  installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
168
199
  stashRoot = detectStashRoot(installRoot);
@@ -189,10 +220,11 @@ async function installGitRegistryRef(parsed, options) {
189
220
  cacheDir,
190
221
  extractedDir,
191
222
  stashRoot,
223
+ audit,
192
224
  };
193
225
  }
194
226
  export function upsertInstalledRegistryEntry(entry) {
195
- const current = loadConfig();
227
+ const current = loadUserConfig();
196
228
  const currentInstalled = current.installed ?? [];
197
229
  const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id);
198
230
  const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)];
@@ -204,7 +236,7 @@ export function upsertInstalledRegistryEntry(entry) {
204
236
  return nextConfig;
205
237
  }
206
238
  export function removeInstalledRegistryEntry(id) {
207
- const current = loadConfig();
239
+ const current = loadUserConfig();
208
240
  const currentInstalled = current.installed ?? [];
209
241
  const nextInstalled = currentInstalled.filter((item) => item.id !== id);
210
242
  const nextConfig = {
@@ -462,3 +494,10 @@ async function computeFileHash(filePath) {
462
494
  const hash = createHash("sha256").update(data).digest("hex");
463
495
  return `sha256:${hash}`;
464
496
  }
497
+ function runInstallAuditOrThrow(rootDir, source, ref, registryLabels, config) {
498
+ const audit = auditInstallCandidate({ rootDir, source, ref, registryLabels, config });
499
+ if (audit.blocked) {
500
+ throw new Error(formatInstallAuditFailure(ref, audit));
501
+ }
502
+ return audit;
503
+ }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { resolveStashDir } from "./common";
4
4
  import { loadConfig } from "./config";
5
5
  import { ensureGitMirror, getCachePaths, parseGitRepoUrl } from "./stash-providers/git";
6
+ import { ensureWebsiteMirror, getCachePaths as getWebsiteCachePaths } from "./stash-providers/website";
6
7
  import { warn } from "./warn";
7
8
  // ── Resolution ──────────────────────────────────────────────────────────────
8
9
  /**
@@ -54,6 +55,19 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
54
55
  }
55
56
  }
56
57
  }
58
+ // Website stash entries: resolve cache directory so the indexer can walk
59
+ // the scraped markdown snapshots.
60
+ for (const entry of config.stashes ?? []) {
61
+ if (entry.type === "website" && entry.url && entry.enabled !== false) {
62
+ try {
63
+ const cachePaths = getWebsiteCachePaths(entry.url);
64
+ addSource(cachePaths.stashDir, entry.name ?? entry.url);
65
+ }
66
+ catch (err) {
67
+ warn(`Warning: failed to resolve website stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
68
+ }
69
+ }
70
+ }
57
71
  // Installed kits (registry and local)
58
72
  for (const entry of config.installed ?? []) {
59
73
  addSource(entry.stashRoot, entry.id);
@@ -153,11 +167,12 @@ function isValidDirectory(dir) {
153
167
  // ── Git stash cache integration ──────────────────────────────────────────────
154
168
  const GIT_STASH_TYPES = new Set(["context-hub", "github", "git"]);
155
169
  /**
156
- * Ensure all git stash mirrors are refreshed so their cache directories
157
- * exist on disk. Must be called (async) before `resolveStashSources()` so
158
- * the content directories pass the `isValidDirectory()` check.
170
+ * Ensure all cache-backed stash providers are refreshed so their cache
171
+ * directories exist on disk. Must be called (async) before
172
+ * `resolveStashSources()` so the content directories pass the
173
+ * `isValidDirectory()` check.
159
174
  */
160
- export async function ensureGitCaches(config) {
175
+ export async function ensureStashCaches(config) {
161
176
  const cfg = config ?? loadConfig();
162
177
  for (const entry of cfg.stashes ?? []) {
163
178
  if (!GIT_STASH_TYPES.has(entry.type) || !entry.url || entry.enabled === false)
@@ -171,6 +186,18 @@ export async function ensureGitCaches(config) {
171
186
  warn(`Warning: failed to refresh git mirror for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
172
187
  }
173
188
  }
189
+ for (const entry of cfg.stashes ?? []) {
190
+ if (entry.type !== "website" || !entry.url || entry.enabled === false)
191
+ continue;
192
+ try {
193
+ await ensureWebsiteMirror(entry, { requireStashDir: true });
194
+ }
195
+ catch (err) {
196
+ warn(`Warning: failed to refresh website stash for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
197
+ }
198
+ }
174
199
  }
175
- /** @deprecated Use ensureGitCaches instead. */
176
- export const ensureContextHubCaches = ensureGitCaches;
200
+ /** @deprecated Use ensureStashCaches instead. */
201
+ export const ensureGitCaches = ensureStashCaches;
202
+ /** @deprecated Use ensureStashCaches instead. */
203
+ export const ensureContextHubCaches = ensureStashCaches;
package/dist/setup.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import path from "node:path";
9
9
  import * as p from "@clack/prompts";
10
10
  import { isHttpUrl } from "./common";
11
- import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
11
+ import { DEFAULT_CONFIG, getConfigPath, loadUserConfig, saveConfig } from "./config";
12
12
  import { closeDatabase, isVecAvailable, openDatabase } from "./db";
13
13
  import { detectAgentPlatforms, detectOllama, detectOpenViking } from "./detect";
14
14
  import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "./embedder";
@@ -568,7 +568,7 @@ async function stepAgentPlatforms(current) {
568
568
  // ── Main Wizard ─────────────────────────────────────────────────────────────
569
569
  export async function runSetupWizard() {
570
570
  p.intro("akm setup");
571
- const current = loadConfig();
571
+ const current = loadUserConfig();
572
572
  const configPath = getConfigPath();
573
573
  // Step 1: Stash directory
574
574
  p.log.step("Step 1: Stash Directory");
package/dist/stash-add.js CHANGED
@@ -1,18 +1,22 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { resolveStashDir } from "./common";
4
- import { loadConfig, saveConfig } from "./config";
3
+ import { isHttpUrl, resolveStashDir } from "./common";
4
+ import { loadConfig, loadUserConfig, saveConfig } from "./config";
5
5
  import { UsageError } from "./errors";
6
6
  import { akmIndex } from "./indexer";
7
7
  import { upsertLockEntry } from "./lockfile";
8
8
  import { detectStashRoot, installRegistryRef, upsertInstalledRegistryEntry } from "./registry-install";
9
9
  import { parseRegistryRef } from "./registry-resolve";
10
+ import { ensureWebsiteMirror, validateWebsiteInputUrl } from "./stash-providers/website";
10
11
  export async function akmAdd(input) {
11
12
  const ref = input.ref.trim();
12
13
  if (!ref)
13
14
  throw new UsageError("Install ref or local directory is required. " +
14
15
  "Examples: `akm add @scope/kit`, `akm add github:owner/repo`, `akm add ./local/path`");
15
16
  const stashDir = resolveStashDir();
17
+ if (shouldAddAsWebsiteUrl(ref)) {
18
+ return addWebsiteStashSource(ref, stashDir, input.name, input.options);
19
+ }
16
20
  // Detect local directory refs and route them to stashes[] instead of installed[]
17
21
  try {
18
22
  const parsed = parseRegistryRef(ref);
@@ -32,7 +36,7 @@ export async function akmAdd(input) {
32
36
  async function addLocalStashSource(ref, sourcePath, stashDir) {
33
37
  const stashRoot = detectStashRoot(sourcePath);
34
38
  const resolvedPath = path.resolve(stashRoot);
35
- const config = loadConfig();
39
+ const config = loadUserConfig();
36
40
  // Check for duplicates in stashes[]
37
41
  const stashes = [...(config.stashes ?? [])];
38
42
  const existing = stashes.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
@@ -69,6 +73,50 @@ async function addLocalStashSource(ref, sourcePath, stashDir) {
69
73
  },
70
74
  };
71
75
  }
76
+ async function addWebsiteStashSource(ref, stashDir, name, options) {
77
+ const normalizedUrl = validateWebsiteInputUrl(ref);
78
+ const config = loadUserConfig();
79
+ const stashes = [...(config.stashes ?? [])];
80
+ let entry = stashes.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
81
+ if (!entry) {
82
+ entry = {
83
+ type: "website",
84
+ url: normalizedUrl,
85
+ name: name ?? toWebsiteName(normalizedUrl),
86
+ ...(options && Object.keys(options).length > 0 ? { options } : {}),
87
+ };
88
+ stashes.push(entry);
89
+ saveConfig({ ...config, stashes });
90
+ }
91
+ else if (options && Object.keys(options).length > 0) {
92
+ entry.options = { ...entry.options, ...options };
93
+ saveConfig({ ...config, stashes });
94
+ }
95
+ const cachePaths = await ensureWebsiteMirror(entry, { requireStashDir: true });
96
+ const index = await akmIndex({ stashDir });
97
+ const updatedConfig = loadConfig();
98
+ return {
99
+ schemaVersion: 1,
100
+ stashDir,
101
+ ref,
102
+ stashSource: {
103
+ type: "website",
104
+ url: normalizedUrl,
105
+ name: entry.name,
106
+ stashRoot: cachePaths.stashDir,
107
+ },
108
+ config: {
109
+ stashCount: updatedConfig.stashes?.length ?? 0,
110
+ installedKitCount: updatedConfig.installed?.length ?? 0,
111
+ },
112
+ index: {
113
+ mode: index.mode,
114
+ totalEntries: index.totalEntries,
115
+ directoriesScanned: index.directoriesScanned,
116
+ directoriesSkipped: index.directoriesSkipped,
117
+ },
118
+ };
119
+ }
72
120
  /**
73
121
  * Install a kit from a registry (npm, github, git).
74
122
  */
@@ -119,6 +167,7 @@ async function addRegistryKit(ref, stashDir) {
119
167
  cacheDir: installed.cacheDir,
120
168
  extractedDir: installed.extractedDir,
121
169
  installedAt: installed.installedAt,
170
+ audit: installed.audit,
122
171
  },
123
172
  config: {
124
173
  stashCount: config.stashes?.length ?? 0,
@@ -139,3 +188,26 @@ function toReadableId(resolvedPath) {
139
188
  }
140
189
  return resolvedPath;
141
190
  }
191
+ // Keep this list limited to widely-used git hosts for the non-breaking
192
+ // "repo-like URL" fast-path; everything else continues to default to website snapshots.
193
+ const KNOWN_GIT_HOSTS = new Set(["github.com", "gitlab.com", "bitbucket.org", "codeberg.org", "git.sr.ht"]);
194
+ export function shouldAddAsWebsiteUrl(ref) {
195
+ return isHttpUrl(ref) && !isLikelyGitRepositoryUrl(ref);
196
+ }
197
+ function isLikelyGitRepositoryUrl(ref) {
198
+ try {
199
+ const parsed = new URL(ref);
200
+ return KNOWN_GIT_HOSTS.has(parsed.hostname.toLowerCase()) || parsed.pathname.endsWith(".git");
201
+ }
202
+ catch {
203
+ return false;
204
+ }
205
+ }
206
+ function toWebsiteName(siteUrl) {
207
+ try {
208
+ return new URL(siteUrl).hostname;
209
+ }
210
+ catch {
211
+ return siteUrl;
212
+ }
213
+ }
@@ -8,3 +8,4 @@
8
8
  import "./filesystem";
9
9
  import "./git";
10
10
  import "./openviking";
11
+ import "./website";