akm-cli 0.8.0-rc1 → 0.8.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2080,6 +2080,10 @@ function listVaultsRecursive(listKeysFn) {
2080
2080
  const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
2081
2081
  if (!canonical)
2082
2082
  continue;
2083
+ // Skip sensitive vaults: presence of a sibling .sensitive marker file suppresses listing.
2084
+ const markerPath = full.replace(/\.env$/, ".sensitive");
2085
+ if (fs.existsSync(markerPath))
2086
+ continue;
2083
2087
  const { keys } = listKeysFn(full);
2084
2088
  result.push({ ref: makeVaultRef(canonical, source), path: full, keys });
2085
2089
  }
@@ -2102,6 +2106,9 @@ function splitVaultRunTarget(target) {
2102
2106
  if (!key) {
2103
2107
  throw new UsageError("Expected vault run target in the form <ref> or <ref/KEY>.");
2104
2108
  }
2109
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
2110
+ throw new UsageError(`"${key}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
2111
+ }
2105
2112
  const resolved = resolveVaultPath(refPart);
2106
2113
  if (!fs.existsSync(resolved.absPath)) {
2107
2114
  throw new NotFoundError(`Vault not found: ${makeVaultRef(resolved.name, resolved.source)}`);
@@ -2122,48 +2129,63 @@ const vaultCreateCommand = defineCommand({
2122
2129
  meta: { name: "create", description: "Create an empty vault file (no-op if it already exists)" },
2123
2130
  args: {
2124
2131
  name: { type: "positional", description: "Vault name (e.g. prod) — file becomes <name>.env", required: true },
2132
+ sensitive: {
2133
+ type: "boolean",
2134
+ description: "Exclude this vault from vault list output and the search index",
2135
+ default: false,
2136
+ },
2125
2137
  },
2126
2138
  run({ args }) {
2127
2139
  return runWithJsonErrors(async () => {
2128
2140
  const { createVault } = await import("./commands/vault.js");
2129
2141
  const { name, absPath, source } = resolveVaultPath(args.name);
2130
2142
  createVault(absPath);
2131
- output("vault-create", { ref: makeVaultRef(name, source), path: absPath });
2143
+ if (args.sensitive) {
2144
+ const markerPath = absPath.replace(/\.env$/, ".sensitive");
2145
+ if (!fs.existsSync(markerPath)) {
2146
+ fs.writeFileSync(markerPath, "", { mode: 0o600 });
2147
+ }
2148
+ }
2149
+ output("vault-create", { ref: makeVaultRef(name, source) });
2132
2150
  });
2133
2151
  },
2134
2152
  });
2135
2153
  const vaultSetCommand = defineCommand({
2136
2154
  meta: {
2137
2155
  name: "set",
2138
- description: 'Set a key in a vault. Value is written to disk and never echoed back. Accepts KEY=VALUE combined form or separate KEY VALUE args. Optionally attach a comment with --comment "description".',
2156
+ description: 'Set a key in a vault. Value is read from stdin by default (never via argv, avoiding /proc/cmdline exposure). Use --from-env <VAR> to read from an environment variable instead. Optionally attach a comment with --comment "description".',
2139
2157
  },
2140
2158
  args: {
2141
2159
  ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
2142
- key: { type: "positional", description: "Key name (e.g. DB_URL) or KEY=VALUE combined form", required: true },
2143
- value: {
2144
- type: "positional",
2145
- description: "Value to store (omit when using KEY=VALUE combined form)",
2146
- required: false,
2147
- },
2160
+ key: { type: "positional", description: "Key name (e.g. DB_URL)", required: true },
2148
2161
  comment: { type: "string", description: "Optional comment written above the key line", required: false },
2162
+ "from-env": {
2163
+ type: "string",
2164
+ description: "Read value from the named environment variable instead of stdin",
2165
+ },
2149
2166
  },
2150
2167
  run({ args }) {
2151
2168
  return runWithJsonErrors(async () => {
2152
2169
  const { setKey } = await import("./commands/vault.js");
2153
2170
  const { name, absPath, source } = resolveVaultPath(args.ref);
2154
- let realKey;
2171
+ const fromEnv = getHyphenatedArg(args, "from-env");
2155
2172
  let realValue;
2156
- if ((args.value === undefined || args.value === "") && args.key.includes("=")) {
2157
- const eqIdx = args.key.indexOf("=");
2158
- realKey = args.key.slice(0, eqIdx);
2159
- realValue = args.key.slice(eqIdx + 1);
2173
+ if (fromEnv !== undefined) {
2174
+ const envVal = process.env[fromEnv];
2175
+ if (envVal === undefined) {
2176
+ throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
2177
+ }
2178
+ realValue = envVal;
2160
2179
  }
2161
2180
  else {
2162
- realKey = args.key;
2163
- realValue = args.value ?? "";
2181
+ const chunks = [];
2182
+ for await (const chunk of Bun.stdin.stream()) {
2183
+ chunks.push(chunk);
2184
+ }
2185
+ realValue = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
2164
2186
  }
2165
- setKey(absPath, realKey, realValue, args.comment);
2166
- output("vault-set", { ref: makeVaultRef(name, source), key: realKey, path: absPath });
2187
+ setKey(absPath, args.key, realValue, args.comment);
2188
+ output("vault-set", { ref: makeVaultRef(name, source), key: args.key });
2167
2189
  });
2168
2190
  },
2169
2191
  });
@@ -2181,7 +2203,7 @@ const vaultUnsetCommand = defineCommand({
2181
2203
  throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
2182
2204
  }
2183
2205
  const removed = unsetKey(absPath, args.key);
2184
- output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed, path: absPath });
2206
+ output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed });
2185
2207
  });
2186
2208
  },
2187
2209
  });
@@ -2237,6 +2259,15 @@ const vaultRunCommand = defineCommand({
2237
2259
  mergedEnv[envKey] = envValue;
2238
2260
  }
2239
2261
  }
2262
+ // Emit vault access event (keys only, no values) for audit trail.
2263
+ // Best-effort: never block vault run on event write failure.
2264
+ appendEvent({
2265
+ eventType: "vault_access",
2266
+ ref: makeVaultRef(name, source),
2267
+ metadata: {
2268
+ keys: key ? [key] : Object.keys(envValues),
2269
+ },
2270
+ });
2240
2271
  const result = spawnSync(command[0], command.slice(1), {
2241
2272
  stdio: "inherit",
2242
2273
  env: mergedEnv,
@@ -104,6 +104,23 @@ function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
104
104
  if (fs.existsSync(derivedPath))
105
105
  return true;
106
106
  }
107
+ // Knowledge-specific: search subdirectories like knowledge/projects/, knowledge/tools/, etc.
108
+ if (refType === "knowledge") {
109
+ try {
110
+ const knowledgeDir = path.join(root, "knowledge");
111
+ if (fs.existsSync(knowledgeDir) && fs.statSync(knowledgeDir).isDirectory()) {
112
+ const entries = fs.readdirSync(knowledgeDir);
113
+ for (const entry of entries) {
114
+ const subPath = path.join(knowledgeDir, entry, `${refName}.md`);
115
+ if (fs.existsSync(subPath))
116
+ return true;
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ // Ignore errors reading directory
122
+ }
123
+ }
107
124
  // Fallback: the refName may already encode the full stash-relative path
108
125
  // (e.g. knowledge:skills/foo/references/bar where the file lives at
109
126
  // <stash>/skills/foo/references/bar.md, not <stash>/knowledge/skills/...).
@@ -119,6 +136,11 @@ function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
119
136
  /**
120
137
  * Returns an array of {ref, resolvedRelPath} for every local AKM ref in the
121
138
  * body that does not resolve to a real file under any of the provided stash roots.
139
+ *
140
+ * Skips false-positive patterns:
141
+ * - Shell variables: memory:$(cmd) or knowledge:${VAR}
142
+ * - ACP type notation: agent::Type (double colons are C++/ACP syntax)
143
+ * - Incomplete/placeholder refs: slug is single character or "**"
122
144
  */
123
145
  function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
124
146
  const allRoots = [stashRoot, ...extraStashRoots];
@@ -128,6 +150,14 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
128
150
  // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
129
151
  while ((match = re.exec(body)) !== null) {
130
152
  const fullRef = match[1]; // e.g. "workflow:foo" or "local//workflow:foo"
153
+ // Skip shell variables: memory:$(cmd) or knowledge:${VAR}
154
+ if (fullRef.includes("$(") || fullRef.includes("${")) {
155
+ continue;
156
+ }
157
+ // Skip ACP type notation: agent::Type (double colons)
158
+ if (fullRef.includes("::")) {
159
+ continue;
160
+ }
131
161
  // Strip leading "local//" prefix if present
132
162
  let ref = fullRef;
133
163
  if (ref.startsWith("local//")) {
@@ -147,6 +177,10 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
147
177
  if (!refName || refName.startsWith("/") || refName.startsWith("~") || refName.startsWith("http")) {
148
178
  continue;
149
179
  }
180
+ // Skip placeholder/incomplete refs: single character slug or "**"
181
+ if (refName.length <= 1 || refName === "**") {
182
+ continue;
183
+ }
150
184
  const relPath = refToRelPath(refType, refName);
151
185
  if (relPath === null)
152
186
  continue; // type is skipped
@@ -176,6 +176,9 @@ export function buildShellExportScript(vaultPath) {
176
176
  */
177
177
  export function setKey(vaultPath, key, value, comment) {
178
178
  validateKeyName(key);
179
+ if (comment !== undefined && /[\r\n]/.test(comment)) {
180
+ throw new Error("Vault key comment cannot contain newline characters.");
181
+ }
179
182
  ensureParentDir(vaultPath);
180
183
  const existing = fs.existsSync(vaultPath) ? fs.readFileSync(vaultPath, "utf8") : "";
181
184
  const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
@@ -246,7 +249,7 @@ export function unsetKey(vaultPath, key) {
246
249
  let out = kept.join("\n");
247
250
  if (out.length > 0 && !out.endsWith("\n"))
248
251
  out += "\n";
249
- writeFileAtomic(vaultPath, out);
252
+ writeFileAtomic(vaultPath, out, 0o600);
250
253
  return true;
251
254
  }
252
255
  /** Create an empty vault file (does nothing if it already exists). */
@@ -254,7 +257,7 @@ export function createVault(vaultPath) {
254
257
  ensureParentDir(vaultPath);
255
258
  if (fs.existsSync(vaultPath))
256
259
  return;
257
- writeFileAtomic(vaultPath, "");
260
+ writeFileAtomic(vaultPath, "", 0o600);
258
261
  }
259
262
  /**
260
263
  * Characters that are safe in an UNquoted dotenv value AND are not
@@ -303,5 +306,5 @@ function validateKeyName(key) {
303
306
  function ensureParentDir(filePath) {
304
307
  const dir = path.dirname(filePath);
305
308
  if (!fs.existsSync(dir))
306
- fs.mkdirSync(dir, { recursive: true });
309
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
307
310
  }
@@ -67,6 +67,10 @@ function validateName(name) {
67
67
  if (normalized === ".." || normalized.startsWith("../")) {
68
68
  throw new UsageError("Path traversal in asset name.", "MISSING_REQUIRED_ARGUMENT");
69
69
  }
70
+ const segments = normalized.split("/");
71
+ if (segments.some((seg) => seg === "." || seg === "..")) {
72
+ throw new UsageError("Asset name cannot contain relative path segments.", "MISSING_REQUIRED_ARGUMENT");
73
+ }
70
74
  }
71
75
  function normalizeName(name) {
72
76
  return path.posix.normalize(name.replace(/\\/g, "/"));
@@ -40,9 +40,9 @@ export function isAssetType(type) {
40
40
  export function writeFileAtomic(target, content, mode) {
41
41
  const tmp = `${target}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
42
42
  fs.writeFileSync(tmp, content, "utf8");
43
- fs.renameSync(tmp, target);
44
43
  if (mode !== undefined)
45
- fs.chmodSync(target, mode);
44
+ fs.chmodSync(tmp, mode);
45
+ fs.renameSync(tmp, target);
46
46
  }
47
47
  /**
48
48
  * Resolve the stash directory using a three-level fallback chain:
@@ -1175,6 +1175,7 @@ export function getAllEntriesForEmbedding(db) {
1175
1175
  .prepare(`
1176
1176
  SELECT e.id, e.search_text AS searchText, e.entry_key AS entryKey, e.file_path AS filePath FROM entries e
1177
1177
  WHERE NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)
1178
+ AND e.entry_type != 'vault'
1178
1179
  `)
1179
1180
  .all();
1180
1181
  }
@@ -440,6 +440,12 @@ export function shouldIndexStashFile(stashRoot, file, options) {
440
440
  const segments = relPath.split(/[\\/]+/).filter(Boolean);
441
441
  if (segments.length === 0)
442
442
  return true;
443
+ // Skip vault .env files that have a sibling .sensitive marker file.
444
+ if (segments[0] === "vaults" && (file.endsWith(".env") || path.basename(file) === ".env")) {
445
+ const markerPath = file.replace(/\.env$/, ".sensitive");
446
+ if (fs.existsSync(markerPath))
447
+ return false;
448
+ }
443
449
  if (options?.treatStashRootAsWikiRoot) {
444
450
  return !(segments.length === 1 && WIKI_INFRA_FILES.has(segments[0]));
445
451
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.8.0-rc1",
3
+ "version": "0.8.0-rc2",
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": [