akm-cli 0.3.1 → 0.4.1

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.
@@ -0,0 +1,324 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { filterNonEmptyStrings } from "./common";
4
+ const DEFAULT_INSTALL_AUDIT_CONFIG = {
5
+ enabled: true,
6
+ blockOnCritical: true,
7
+ blockUnlistedRegistries: false,
8
+ registryAllowlist: [],
9
+ };
10
+ const MAX_SCANNED_FILE_BYTES = 256 * 1024;
11
+ const LIFECYCLE_SCRIPT_NAMES = new Set([
12
+ "preinstall",
13
+ "install",
14
+ "postinstall",
15
+ "prepublish",
16
+ "prepublishOnly",
17
+ "prepare",
18
+ ]);
19
+ const TEXT_FILE_EXTENSIONS = new Set([
20
+ ".cjs",
21
+ ".cts",
22
+ ".js",
23
+ ".json",
24
+ ".jsonc",
25
+ ".jsx",
26
+ ".mjs",
27
+ ".md",
28
+ ".ps1",
29
+ ".py",
30
+ ".rb",
31
+ ".sh",
32
+ ".toml",
33
+ ".ts",
34
+ ".tsx",
35
+ ".txt",
36
+ ".yaml",
37
+ ".yml",
38
+ ]);
39
+ const CONTENT_RULES = [
40
+ {
41
+ id: "prompt-ignore-previous-instructions",
42
+ severity: "high",
43
+ category: "prompt-injection",
44
+ message: "Contains instructions to ignore prior prompts or instructions.",
45
+ pattern: /\b(ignore|disregard|forget)\b[^.\n]{0,100}\b(previous|prior|earlier)\b[^.\n]{0,100}\b(instructions?|prompts?|messages?)\b/i,
46
+ },
47
+ {
48
+ id: "prompt-reveal-hidden-secrets",
49
+ severity: "critical",
50
+ category: "prompt-injection",
51
+ message: "Contains instructions to reveal hidden prompts or secrets.",
52
+ pattern: /\b(reveal|print|dump|show|exfiltrat(?:e|ion))\b[^.\n]{0,120}\b(system prompt|hidden instructions?|developer message|api key|token|secret|password)\b/i,
53
+ },
54
+ {
55
+ id: "prompt-bypass-guardrails",
56
+ severity: "high",
57
+ category: "prompt-injection",
58
+ message: "Contains instructions to bypass safety or security controls.",
59
+ pattern: /\b(bypass|disable|ignore)\b[^.\n]{0,100}\b(safety|security|guardrails|restrictions|policies)\b/i,
60
+ },
61
+ {
62
+ id: "remote-shell-pipe",
63
+ severity: "critical",
64
+ category: "malicious-code",
65
+ message: "Downloads remote content and pipes it directly into a shell.",
66
+ pattern: /\b(curl|wget)\b[^\n|]{0,200}\|\s*(sh|bash|zsh)\b/i,
67
+ },
68
+ {
69
+ id: "powershell-download-exec",
70
+ severity: "critical",
71
+ category: "malicious-code",
72
+ message: "Downloads remote content and executes it in PowerShell.",
73
+ pattern: /\b(Invoke-WebRequest|iwr|curl)\b[^\n|]{0,200}\|\s*(iex|Invoke-Expression)\b/i,
74
+ },
75
+ {
76
+ id: "powershell-encoded-command",
77
+ severity: "critical",
78
+ category: "malicious-code",
79
+ message: "Uses an encoded PowerShell command.",
80
+ pattern: /\bpowershell(?:\.exe)?\b[^\n]{0,120}\s-(?:enc|encodedcommand)\b/i,
81
+ },
82
+ {
83
+ id: "credential-exfiltration-language",
84
+ severity: "high",
85
+ category: "malicious-code",
86
+ message: "Contains language associated with credential or secret exfiltration.",
87
+ pattern: /\b(exfiltrat(?:e|ion)|harvest|steal)\b[^.\n]{0,120}\b(credentials?|tokens?|secrets?|ssh keys?|passwords?|cookies?)\b/i,
88
+ },
89
+ ];
90
+ export function resolveInstallAuditConfig(config) {
91
+ const installAudit = config?.security?.installAudit;
92
+ const allowlist = filterNonEmptyStrings(installAudit?.registryAllowlist) ??
93
+ filterNonEmptyStrings(installAudit?.registryWhitelist) ??
94
+ [];
95
+ return {
96
+ enabled: installAudit?.enabled ?? DEFAULT_INSTALL_AUDIT_CONFIG.enabled,
97
+ blockOnCritical: installAudit?.blockOnCritical ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockOnCritical,
98
+ blockUnlistedRegistries: installAudit?.blockUnlistedRegistries ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockUnlistedRegistries,
99
+ registryAllowlist: allowlist.map((entry) => entry.trim().toLowerCase()),
100
+ };
101
+ }
102
+ export function enforceRegistryInstallPolicy(registryLabels, config, ref) {
103
+ const resolved = resolveInstallAuditConfig(config);
104
+ if (!resolved.blockUnlistedRegistries)
105
+ return;
106
+ if (resolved.registryAllowlist.length === 0) {
107
+ throw new Error(`Install blocked for ${ref}: no registries are allowlisted. Configure security.installAudit.registryAllowlist or disable security.installAudit.blockUnlistedRegistries.`);
108
+ }
109
+ const matched = registryLabels.some((label) => resolved.registryAllowlist.includes(label.toLowerCase()));
110
+ if (matched)
111
+ return;
112
+ throw new Error(`Install blocked for ${ref}: registry is not allowlisted. Allowed: ${resolved.registryAllowlist.join(", ")}. Seen: ${registryLabels.join(", ")}.`);
113
+ }
114
+ export function auditInstallCandidate(input) {
115
+ const resolved = resolveInstallAuditConfig(input.config);
116
+ if (!resolved.enabled) {
117
+ return {
118
+ enabled: false,
119
+ passed: true,
120
+ blocked: false,
121
+ registryLabels: [...input.registryLabels],
122
+ findings: [],
123
+ scannedFiles: 0,
124
+ scannedBytes: 0,
125
+ summary: buildSummary([]),
126
+ };
127
+ }
128
+ const findings = [];
129
+ const counters = { scannedFiles: 0, scannedBytes: 0 };
130
+ scanDirectory(input.rootDir, input.rootDir, findings, counters);
131
+ const summary = buildSummary(findings);
132
+ const blocked = resolved.blockOnCritical && summary.critical > 0;
133
+ return {
134
+ enabled: true,
135
+ passed: findings.length === 0,
136
+ blocked,
137
+ registryLabels: [...input.registryLabels],
138
+ findings,
139
+ scannedFiles: counters.scannedFiles,
140
+ scannedBytes: counters.scannedBytes,
141
+ summary,
142
+ };
143
+ }
144
+ export function formatInstallAuditFailure(ref, report) {
145
+ const lines = [`Security audit failed for ${ref}.`, formatInstallAuditSummary(report)];
146
+ for (const finding of report.findings.slice(0, 5)) {
147
+ lines.push(`- [${finding.severity}] ${finding.message}${finding.file ? ` (${finding.file})` : ""}`);
148
+ }
149
+ if (report.findings.length > 5) {
150
+ lines.push(`- ${report.findings.length - 5} more finding(s) omitted`);
151
+ }
152
+ lines.push("Disable blocking with `security.installAudit.blockOnCritical = false`, or disable audits with `security.installAudit.enabled = false`.");
153
+ return lines.join("\n");
154
+ }
155
+ export function formatInstallAuditSummary(report) {
156
+ if (!report.enabled)
157
+ return "Audit: disabled";
158
+ const severitySummary = [];
159
+ if (report.summary.critical > 0)
160
+ severitySummary.push(`${report.summary.critical} critical`);
161
+ if (report.summary.high > 0)
162
+ severitySummary.push(`${report.summary.high} high`);
163
+ if (report.summary.moderate > 0)
164
+ severitySummary.push(`${report.summary.moderate} moderate`);
165
+ if (report.summary.low > 0)
166
+ severitySummary.push(`${report.summary.low} low`);
167
+ const detail = severitySummary.length > 0 ? severitySummary.join(", ") : "no findings";
168
+ return `Audit: ${report.blocked ? "blocked" : report.passed ? "passed" : "warnings"} (${detail}; scanned ${report.scannedFiles} file${report.scannedFiles === 1 ? "" : "s"})`;
169
+ }
170
+ export function deriveRegistryLabels(input) {
171
+ const labels = new Set();
172
+ labels.add(input.source);
173
+ if (input.source === "github")
174
+ labels.add("github.com");
175
+ if (input.source === "npm")
176
+ labels.add("npm");
177
+ addUrlLabels(labels, input.artifactUrl);
178
+ addUrlLabels(labels, input.gitUrl);
179
+ if (input.source === "github" && input.ref.startsWith("github:")) {
180
+ labels.add("github");
181
+ }
182
+ return [...labels];
183
+ }
184
+ function scanDirectory(dir, rootDir, findings, counters) {
185
+ let entries;
186
+ try {
187
+ entries = fs.readdirSync(dir, { withFileTypes: true });
188
+ }
189
+ catch {
190
+ return;
191
+ }
192
+ for (const entry of entries) {
193
+ if (entry.name === ".git" || entry.name === "node_modules")
194
+ continue;
195
+ const fullPath = path.join(dir, entry.name);
196
+ if (entry.isDirectory()) {
197
+ scanDirectory(fullPath, rootDir, findings, counters);
198
+ continue;
199
+ }
200
+ if (!entry.isFile())
201
+ continue;
202
+ scanFile(fullPath, rootDir, findings, counters);
203
+ }
204
+ }
205
+ function scanFile(filePath, rootDir, findings, counters) {
206
+ const ext = path.extname(filePath).toLowerCase();
207
+ const basename = path.basename(filePath).toLowerCase();
208
+ if (basename !== "package.json" && !TEXT_FILE_EXTENSIONS.has(ext))
209
+ return;
210
+ let fileSize;
211
+ try {
212
+ fileSize = fs.statSync(filePath).size;
213
+ }
214
+ catch {
215
+ return;
216
+ }
217
+ const readSize = Math.min(fileSize, MAX_SCANNED_FILE_BYTES);
218
+ const buf = Buffer.alloc(readSize);
219
+ let bytesRead;
220
+ try {
221
+ const fd = fs.openSync(filePath, "r");
222
+ try {
223
+ bytesRead = fs.readSync(fd, buf, 0, readSize, 0);
224
+ }
225
+ finally {
226
+ fs.closeSync(fd);
227
+ }
228
+ }
229
+ catch {
230
+ return;
231
+ }
232
+ if (bytesRead === 0)
233
+ return;
234
+ const bytes = buf.subarray(0, bytesRead);
235
+ if (bytes.includes(0))
236
+ return;
237
+ counters.scannedFiles += 1;
238
+ counters.scannedBytes += bytesRead;
239
+ const content = bytes.toString("utf8");
240
+ const relativePath = path.relative(rootDir, filePath) || path.basename(filePath);
241
+ const genericContent = basename === "package.json" ? stripPackageJsonScripts(content) : content;
242
+ for (const rule of CONTENT_RULES) {
243
+ const match = genericContent.match(rule.pattern);
244
+ if (!match)
245
+ continue;
246
+ findings.push({
247
+ id: rule.id,
248
+ severity: rule.severity,
249
+ category: rule.category,
250
+ message: rule.message,
251
+ file: relativePath,
252
+ snippet: clipSnippet(match[0]),
253
+ });
254
+ }
255
+ if (basename === "package.json") {
256
+ scanPackageJson(content, relativePath, findings);
257
+ }
258
+ }
259
+ function stripPackageJsonScripts(content) {
260
+ let parsed;
261
+ try {
262
+ parsed = JSON.parse(content);
263
+ }
264
+ catch {
265
+ return content;
266
+ }
267
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
268
+ return content;
269
+ const packageJson = { ...parsed };
270
+ delete packageJson.scripts;
271
+ return JSON.stringify(packageJson, null, 2);
272
+ }
273
+ function scanPackageJson(content, relativePath, findings) {
274
+ let parsed;
275
+ try {
276
+ parsed = JSON.parse(content);
277
+ }
278
+ catch {
279
+ return;
280
+ }
281
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
282
+ return;
283
+ const scripts = parsed.scripts;
284
+ if (typeof scripts !== "object" || scripts === null || Array.isArray(scripts))
285
+ return;
286
+ for (const [name, command] of Object.entries(scripts)) {
287
+ if (!LIFECYCLE_SCRIPT_NAMES.has(name) || typeof command !== "string")
288
+ continue;
289
+ for (const rule of CONTENT_RULES) {
290
+ if (!rule.pattern.test(command))
291
+ continue;
292
+ findings.push({
293
+ id: `lifecycle-${name}-${rule.id}`,
294
+ severity: rule.severity,
295
+ category: "install-script",
296
+ message: `Lifecycle script "${name}" is suspicious: ${rule.message.toLowerCase()}`,
297
+ file: relativePath,
298
+ snippet: clipSnippet(command),
299
+ });
300
+ }
301
+ }
302
+ }
303
+ function clipSnippet(value) {
304
+ const normalized = value.replace(/\s+/g, " ").trim();
305
+ return normalized.length <= 140 ? normalized : `${normalized.slice(0, 137)}...`;
306
+ }
307
+ function buildSummary(findings) {
308
+ const summary = { low: 0, moderate: 0, high: 0, critical: 0, total: findings.length };
309
+ for (const finding of findings) {
310
+ summary[finding.severity] += 1;
311
+ }
312
+ return summary;
313
+ }
314
+ function addUrlLabels(labels, rawUrl) {
315
+ if (!rawUrl)
316
+ return;
317
+ try {
318
+ const parsed = new URL(rawUrl);
319
+ labels.add(parsed.hostname.toLowerCase());
320
+ }
321
+ catch {
322
+ // Ignore non-URL refs (for example git@host:path)
323
+ }
324
+ }
@@ -247,8 +247,8 @@ function tryResolveInstalledTarget(installed, target) {
247
247
  return undefined;
248
248
  }
249
249
  function toInstalledEntry(status) {
250
- // KitInstallStatus extends InstalledKitEntry; omit the extra extractedDir field.
251
- const { extractedDir: _extractedDir, ...base } = status;
250
+ // KitInstallStatus extends InstalledKitEntry; omit transient install-only fields.
251
+ const { extractedDir: _extractedDir, audit: _audit, ...base } = status;
252
252
  return base;
253
253
  }
254
254
  function toInstallStatus(status) {
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
+ }
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,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { isHttpUrl, resolveStashDir } from "./common";
4
- import { loadConfig, saveConfig } from "./config";
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";
@@ -36,7 +36,7 @@ export async function akmAdd(input) {
36
36
  async function addLocalStashSource(ref, sourcePath, stashDir) {
37
37
  const stashRoot = detectStashRoot(sourcePath);
38
38
  const resolvedPath = path.resolve(stashRoot);
39
- const config = loadConfig();
39
+ const config = loadUserConfig();
40
40
  // Check for duplicates in stashes[]
41
41
  const stashes = [...(config.stashes ?? [])];
42
42
  const existing = stashes.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
@@ -75,7 +75,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir) {
75
75
  }
76
76
  async function addWebsiteStashSource(ref, stashDir, name, options) {
77
77
  const normalizedUrl = validateWebsiteInputUrl(ref);
78
- const config = loadConfig();
78
+ const config = loadUserConfig();
79
79
  const stashes = [...(config.stashes ?? [])];
80
80
  let entry = stashes.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
81
81
  if (!entry) {
@@ -167,6 +167,7 @@ async function addRegistryKit(ref, stashDir) {
167
167
  cacheDir: installed.cacheDir,
168
168
  extractedDir: installed.extractedDir,
169
169
  installedAt: installed.installedAt,
170
+ audit: installed.audit,
170
171
  },
171
172
  config: {
172
173
  stashCount: config.stashes?.length ?? 0,
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { loadConfig, saveConfig } from "./config";
2
+ import { loadConfig, loadUserConfig, saveConfig } from "./config";
3
3
  import { UsageError } from "./errors";
4
4
  import { resolveStashSources } from "./search-source";
5
5
  // ── Operations ──────────────────────────────────────────────────────────────
@@ -12,7 +12,7 @@ import { resolveStashSources } from "./search-source";
12
12
  */
13
13
  export function addStash(opts) {
14
14
  const { target, name, providerType, options: providerOptions } = opts;
15
- const config = loadConfig();
15
+ const config = loadUserConfig();
16
16
  const stashes = [...(config.stashes ?? [])];
17
17
  const isUrl = target.startsWith("http://") || target.startsWith("https://");
18
18
  let entry;
@@ -49,7 +49,7 @@ export function addStash(opts) {
49
49
  * Match priority: URL > path > name (most specific first).
50
50
  */
51
51
  export function removeStash(target) {
52
- const config = loadConfig();
52
+ const config = loadUserConfig();
53
53
  const stashes = [...(config.stashes ?? [])];
54
54
  const isUrl = target.startsWith("http://") || target.startsWith("https://");
55
55
  const resolvedPath = !isUrl ? path.resolve(target) : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Kit Manager) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [