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 +49 -18
- package/dist/commands/lint/base-linter.js +34 -0
- package/dist/commands/vault.js +6 -3
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/common.js +2 -2
- package/dist/indexer/db.js +1 -0
- package/dist/indexer/metadata.js +6 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
2171
|
+
const fromEnv = getHyphenatedArg(args, "from-env");
|
|
2155
2172
|
let realValue;
|
|
2156
|
-
if (
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
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
|
-
|
|
2163
|
-
|
|
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,
|
|
2166
|
-
output("vault-set", { ref: makeVaultRef(name, source), key:
|
|
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
|
|
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
|
package/dist/commands/vault.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/asset-ref.js
CHANGED
|
@@ -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, "/"));
|
package/dist/core/common.js
CHANGED
|
@@ -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(
|
|
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:
|
package/dist/indexer/db.js
CHANGED
|
@@ -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
|
}
|
package/dist/indexer/metadata.js
CHANGED
|
@@ -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-
|
|
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": [
|