akm-cli 0.7.3 → 0.7.5
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/{CHANGELOG.md → .github/CHANGELOG.md} +35 -0
- package/.github/LICENSE +374 -0
- package/dist/cli.js +241 -170
- package/dist/commands/curate.js +1 -0
- package/dist/commands/distill.js +14 -4
- package/dist/commands/events.js +10 -1
- package/dist/commands/migration-help.js +2 -2
- package/dist/commands/propose.js +36 -16
- package/dist/commands/reflect.js +40 -14
- package/dist/commands/remember.js +1 -1
- package/dist/commands/show.js +19 -44
- package/dist/commands/vault.js +5 -10
- package/dist/core/asset-registry.js +1 -1
- package/dist/core/asset-spec.js +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/events.js +19 -2
- package/dist/indexer/db-search.js +35 -235
- package/dist/indexer/db.js +15 -5
- package/dist/indexer/ensure-index.js +72 -0
- package/dist/indexer/graph-extraction.js +10 -0
- package/dist/indexer/indexer.js +38 -22
- package/dist/integrations/agent/prompts.js +95 -15
- package/dist/integrations/agent/spawn.js +65 -12
- package/dist/llm/client.js +40 -2
- package/dist/llm/graph-extract.js +2 -4
- package/dist/llm/memory-infer.js +7 -4
- package/dist/output/cli-hints.js +17 -8
- package/dist/output/renderers.js +6 -1
- package/dist/output/shapes.js +8 -3
- package/dist/output/text.js +18 -19
- package/dist/sources/providers/git.js +43 -1
- package/dist/workflows/db.js +9 -0
- package/dist/workflows/runs.js +25 -8
- package/dist/workflows/scope-key.js +76 -0
- package/docs/migration/release-notes/0.7.3.md +16 -0
- package/docs/migration/release-notes/0.7.4.md +17 -0
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
@@ -34,6 +35,7 @@ import { getCacheDir, getDbPath, getDefaultStashDir } from "./core/paths";
|
|
|
34
35
|
import { setQuiet, setVerbose, warn } from "./core/warn";
|
|
35
36
|
import { resolveWriteTarget, writeAssetToSource } from "./core/write-source";
|
|
36
37
|
import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "./indexer/db";
|
|
38
|
+
import { ensureIndex } from "./indexer/ensure-index";
|
|
37
39
|
import { akmIndex } from "./indexer/indexer";
|
|
38
40
|
import { resolveSourceEntries } from "./indexer/search-source";
|
|
39
41
|
import { insertUsageEvent } from "./indexer/usage-events";
|
|
@@ -725,7 +727,7 @@ const saveCommand = defineCommand({
|
|
|
725
727
|
? undefined
|
|
726
728
|
: args.name;
|
|
727
729
|
let writable;
|
|
728
|
-
if (
|
|
730
|
+
if (effectiveName === undefined) {
|
|
729
731
|
// Primary stash — honour the root-level writable flag from config.
|
|
730
732
|
const cfg = loadConfig();
|
|
731
733
|
writable = cfg.writable === true ? true : undefined;
|
|
@@ -939,6 +941,30 @@ const registryCommand = defineCommand({
|
|
|
939
941
|
}),
|
|
940
942
|
},
|
|
941
943
|
});
|
|
944
|
+
const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
|
|
945
|
+
const MAX_FEEDBACK_TAGS = 10;
|
|
946
|
+
function validateFeedbackTags(raw) {
|
|
947
|
+
const seen = new Set();
|
|
948
|
+
const out = [];
|
|
949
|
+
for (const tag of raw) {
|
|
950
|
+
const parts = tag.split(":");
|
|
951
|
+
if (parts.length < 2 || parts[0] === "" || parts.slice(1).join("") === "") {
|
|
952
|
+
throw new UsageError(`Invalid tag "${tag}". Tags must be in key:value format where key matches [a-z_][a-z0-9_]* and value is non-empty.`, "INVALID_FLAG_VALUE");
|
|
953
|
+
}
|
|
954
|
+
const key = parts[0];
|
|
955
|
+
if (!TAG_KEY_RE.test(key)) {
|
|
956
|
+
throw new UsageError(`Invalid tag key "${key}" in "${tag}". Key must match [a-z_][a-z0-9_]*.`, "INVALID_FLAG_VALUE");
|
|
957
|
+
}
|
|
958
|
+
if (seen.has(tag))
|
|
959
|
+
continue;
|
|
960
|
+
seen.add(tag);
|
|
961
|
+
out.push(tag);
|
|
962
|
+
}
|
|
963
|
+
if (out.length > MAX_FEEDBACK_TAGS) {
|
|
964
|
+
throw new UsageError(`Too many tags: ${out.length}. Maximum is ${MAX_FEEDBACK_TAGS}.`, "INVALID_FLAG_VALUE");
|
|
965
|
+
}
|
|
966
|
+
return out;
|
|
967
|
+
}
|
|
942
968
|
const feedbackCommand = defineCommand({
|
|
943
969
|
meta: {
|
|
944
970
|
name: "feedback",
|
|
@@ -963,9 +989,13 @@ const feedbackCommand = defineCommand({
|
|
|
963
989
|
default: false,
|
|
964
990
|
},
|
|
965
991
|
note: { type: "string", description: "Optional note to attach to the feedback" },
|
|
992
|
+
tag: {
|
|
993
|
+
type: "string",
|
|
994
|
+
description: "Tag to attach to the feedback (repeatable, e.g. --tag slice:train --tag team:platform)",
|
|
995
|
+
},
|
|
966
996
|
},
|
|
967
997
|
run({ args }) {
|
|
968
|
-
return runWithJsonErrors(() => {
|
|
998
|
+
return runWithJsonErrors(async () => {
|
|
969
999
|
const ref = (args.ref ?? "").trim();
|
|
970
1000
|
if (!ref) {
|
|
971
1001
|
throw new UsageError("Asset ref is required. Usage: akm feedback <ref> --positive|--negative", "MISSING_REQUIRED_ARGUMENT", "Pass a ref like `skill:deploy` and either --positive or --negative.");
|
|
@@ -978,12 +1008,25 @@ const feedbackCommand = defineCommand({
|
|
|
978
1008
|
throw new UsageError("Specify --positive or --negative.");
|
|
979
1009
|
}
|
|
980
1010
|
const signal = args.positive ? "positive" : "negative";
|
|
981
|
-
const
|
|
1011
|
+
const rawTags = parseAllFlagValues("--tag");
|
|
1012
|
+
const validatedTags = validateFeedbackTags(rawTags);
|
|
1013
|
+
const metadataObj = {
|
|
1014
|
+
signal,
|
|
1015
|
+
...(args.note ? { note: args.note } : {}),
|
|
1016
|
+
...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
|
|
1017
|
+
};
|
|
1018
|
+
const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
|
|
1019
|
+
// Auto-index when stale so the index is current before recording feedback.
|
|
1020
|
+
const sources = resolveSourceEntries();
|
|
1021
|
+
if (sources.length > 0) {
|
|
1022
|
+
await ensureIndex(sources[0].path);
|
|
1023
|
+
}
|
|
982
1024
|
const db = openExistingDatabase();
|
|
983
1025
|
try {
|
|
984
1026
|
const entryId = findEntryIdByRef(db, ref);
|
|
985
1027
|
if (entryId === undefined) {
|
|
986
|
-
throw new UsageError(`Ref "${ref}" is not in the
|
|
1028
|
+
throw new UsageError(`Ref "${ref}" is not in the index. ` +
|
|
1029
|
+
"Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
|
|
987
1030
|
}
|
|
988
1031
|
// Persist the feedback signal into usage_events. For positive signals,
|
|
989
1032
|
// the EMA utility score is updated immediately on the next read path.
|
|
@@ -995,7 +1038,7 @@ const feedbackCommand = defineCommand({
|
|
|
995
1038
|
entry_ref: ref,
|
|
996
1039
|
entry_id: entryId,
|
|
997
1040
|
signal,
|
|
998
|
-
metadata,
|
|
1041
|
+
metadata: metadataStr,
|
|
999
1042
|
});
|
|
1000
1043
|
}
|
|
1001
1044
|
finally {
|
|
@@ -1004,9 +1047,9 @@ const feedbackCommand = defineCommand({
|
|
|
1004
1047
|
appendEvent({
|
|
1005
1048
|
eventType: "feedback",
|
|
1006
1049
|
ref,
|
|
1007
|
-
metadata:
|
|
1050
|
+
metadata: metadataObj,
|
|
1008
1051
|
});
|
|
1009
|
-
output("feedback", { ok: true, ref, signal, note: args.note ?? null });
|
|
1052
|
+
output("feedback", { ok: true, ref, signal, note: args.note ?? null, tags: validatedTags });
|
|
1010
1053
|
});
|
|
1011
1054
|
},
|
|
1012
1055
|
});
|
|
@@ -1132,7 +1175,7 @@ async function writeMarkdownAsset(options) {
|
|
|
1132
1175
|
const workflowStartCommand = defineCommand({
|
|
1133
1176
|
meta: {
|
|
1134
1177
|
name: "start",
|
|
1135
|
-
description: "Start a new workflow run",
|
|
1178
|
+
description: "Start a new workflow run in the current working scope",
|
|
1136
1179
|
},
|
|
1137
1180
|
args: {
|
|
1138
1181
|
ref: { type: "positional", description: "Workflow ref (workflow:<name>)", required: true },
|
|
@@ -1148,7 +1191,7 @@ const workflowStartCommand = defineCommand({
|
|
|
1148
1191
|
const workflowNextCommand = defineCommand({
|
|
1149
1192
|
meta: {
|
|
1150
1193
|
name: "next",
|
|
1151
|
-
description: "Show the next actionable workflow step, auto-starting a run when passed a workflow ref",
|
|
1194
|
+
description: "Show the next actionable workflow step in the current scope, auto-starting a run when passed a workflow ref",
|
|
1152
1195
|
},
|
|
1153
1196
|
args: {
|
|
1154
1197
|
target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
|
|
@@ -1161,9 +1204,8 @@ const workflowNextCommand = defineCommand({
|
|
|
1161
1204
|
// run-id shape), short-circuit with a structured WORKFLOW_NOT_FOUND
|
|
1162
1205
|
// error before parseAssetRef gets to throw an unhelpful ref-parse error.
|
|
1163
1206
|
if (looksLikeWorkflowRunId(args.target)) {
|
|
1164
|
-
const {
|
|
1165
|
-
|
|
1166
|
-
if (!existingRuns.some((r) => r.id === args.target)) {
|
|
1207
|
+
const { hasWorkflowRun } = await import("./workflows/runs.js");
|
|
1208
|
+
if (!hasWorkflowRun(args.target)) {
|
|
1167
1209
|
throw new NotFoundError(`Workflow run "${args.target}" not found.`, "WORKFLOW_NOT_FOUND", "Run `akm workflow list --active` to see runs.");
|
|
1168
1210
|
}
|
|
1169
1211
|
}
|
|
@@ -1222,7 +1264,7 @@ const workflowCompleteCommand = defineCommand({
|
|
|
1222
1264
|
const workflowStatusCommand = defineCommand({
|
|
1223
1265
|
meta: {
|
|
1224
1266
|
name: "status",
|
|
1225
|
-
description: "Show full workflow run state for review or resume",
|
|
1267
|
+
description: "Show full workflow run state for review or resume; workflow refs resolve within the current scope",
|
|
1226
1268
|
},
|
|
1227
1269
|
args: {
|
|
1228
1270
|
target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
|
|
@@ -1261,7 +1303,7 @@ const workflowStatusCommand = defineCommand({
|
|
|
1261
1303
|
const workflowListCommand = defineCommand({
|
|
1262
1304
|
meta: {
|
|
1263
1305
|
name: "list",
|
|
1264
|
-
description: "List workflow runs",
|
|
1306
|
+
description: "List workflow runs in the current working scope",
|
|
1265
1307
|
},
|
|
1266
1308
|
args: {
|
|
1267
1309
|
ref: { type: "string", description: "Filter to one workflow ref" },
|
|
@@ -1841,21 +1883,36 @@ const disableCommand = defineCommand({
|
|
|
1841
1883
|
});
|
|
1842
1884
|
// ── vault ───────────────────────────────────────────────────────────────────
|
|
1843
1885
|
//
|
|
1844
|
-
// `akm vault` manages secrets stored in `.env` files under
|
|
1845
|
-
//
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1886
|
+
// `akm vault` manages secrets stored in `.env` files under each stash's
|
|
1887
|
+
// vaults/ directory. Values are NEVER written to stdout or structured output.
|
|
1888
|
+
function parseVaultRef(ref) {
|
|
1889
|
+
return parseAssetRef(ref.includes(":") ? ref : `vault:${ref}`);
|
|
1890
|
+
}
|
|
1891
|
+
function findVaultSource(origin) {
|
|
1892
|
+
const sources = resolveSourceEntries(undefined, loadConfig());
|
|
1893
|
+
if (sources.length === 0) {
|
|
1894
|
+
throw new UsageError("No stashes configured. Run `akm init` to create your working stash.");
|
|
1895
|
+
}
|
|
1896
|
+
if (!origin || origin === "local")
|
|
1897
|
+
return sources[0];
|
|
1898
|
+
const named = sources.find((source) => source.registryId === origin);
|
|
1899
|
+
if (!named) {
|
|
1900
|
+
throw new NotFoundError(`Source not found for origin: ${origin}`);
|
|
1901
|
+
}
|
|
1902
|
+
return named;
|
|
1903
|
+
}
|
|
1904
|
+
function makeVaultRef(name, source) {
|
|
1905
|
+
return source?.registryId ? `${source.registryId}//vault:${name}` : `vault:${name}`;
|
|
1906
|
+
}
|
|
1850
1907
|
function resolveVaultPath(ref) {
|
|
1851
|
-
const
|
|
1852
|
-
const parsed = parseAssetRef(ref.includes(":") ? ref : `vault:${ref}`);
|
|
1908
|
+
const parsed = parseVaultRef(ref);
|
|
1853
1909
|
if (parsed.type !== "vault") {
|
|
1854
1910
|
throw new UsageError(`Expected a vault ref (vault:<name>); got "${ref}".`);
|
|
1855
1911
|
}
|
|
1856
|
-
const
|
|
1912
|
+
const source = findVaultSource(parsed.origin);
|
|
1913
|
+
const typeRoot = path.join(source.path, "vaults");
|
|
1857
1914
|
const absPath = resolveAssetPathFromName("vault", typeRoot, parsed.name);
|
|
1858
|
-
return { name: parsed.name, absPath };
|
|
1915
|
+
return { name: parsed.name, absPath, source, parsedRef: parsed };
|
|
1859
1916
|
}
|
|
1860
1917
|
/**
|
|
1861
1918
|
* Walk `vaults/` recursively and return one entry per `.env` file, using the
|
|
@@ -1864,97 +1921,58 @@ function resolveVaultPath(ref) {
|
|
|
1864
1921
|
* `vault:team/prod`, `vaults/team/.env` → `vault:team/default`).
|
|
1865
1922
|
*/
|
|
1866
1923
|
function listVaultsRecursive(listKeysFn) {
|
|
1867
|
-
const stashDir = resolveStashDir({ readOnly: true });
|
|
1868
|
-
const vaultsDir = path.join(stashDir, "vaults");
|
|
1869
1924
|
const result = [];
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1925
|
+
for (const source of resolveSourceEntries(undefined, loadConfig())) {
|
|
1926
|
+
const vaultsDir = path.join(source.path, "vaults");
|
|
1927
|
+
if (!fs.existsSync(vaultsDir))
|
|
1928
|
+
continue;
|
|
1929
|
+
const walk = (dir) => {
|
|
1930
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1931
|
+
const full = path.join(dir, entry.name);
|
|
1932
|
+
if (entry.isDirectory()) {
|
|
1933
|
+
walk(full);
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
if (!entry.isFile())
|
|
1937
|
+
continue;
|
|
1938
|
+
if (entry.name !== ".env" && !entry.name.endsWith(".env"))
|
|
1939
|
+
continue;
|
|
1940
|
+
const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
|
|
1941
|
+
if (!canonical)
|
|
1942
|
+
continue;
|
|
1943
|
+
const { keys } = listKeysFn(full);
|
|
1944
|
+
result.push({ ref: makeVaultRef(canonical, source), path: full, keys });
|
|
1878
1945
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
continue;
|
|
1883
|
-
const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
|
|
1884
|
-
if (!canonical)
|
|
1885
|
-
continue;
|
|
1886
|
-
const { keys } = listKeysFn(full);
|
|
1887
|
-
result.push({ ref: `vault:${canonical}`, path: full, keyCount: keys.length });
|
|
1888
|
-
}
|
|
1889
|
-
};
|
|
1890
|
-
walk(vaultsDir);
|
|
1946
|
+
};
|
|
1947
|
+
walk(vaultsDir);
|
|
1948
|
+
}
|
|
1891
1949
|
return result;
|
|
1892
1950
|
}
|
|
1893
|
-
function
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const tokens = listIndex >= 0 ? argv.slice(listIndex + 1) : argv;
|
|
1898
|
-
let flagIndex = -1;
|
|
1899
|
-
let flagConsumesNextToken = false;
|
|
1900
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
1901
|
-
const token = tokens[i];
|
|
1902
|
-
if (token === flag) {
|
|
1903
|
-
flagIndex = i;
|
|
1904
|
-
flagConsumesNextToken = true;
|
|
1905
|
-
break;
|
|
1906
|
-
}
|
|
1907
|
-
if (token === `${flag}=${flagValue}`) {
|
|
1908
|
-
flagIndex = i;
|
|
1909
|
-
break;
|
|
1910
|
-
}
|
|
1951
|
+
function splitVaultRunTarget(target) {
|
|
1952
|
+
const full = resolveVaultPath(target);
|
|
1953
|
+
if (fs.existsSync(full.absPath)) {
|
|
1954
|
+
return { ref: makeVaultRef(full.name, full.source) };
|
|
1911
1955
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
// as the positional ref and it was not consumed by the output flag.
|
|
1916
|
-
if (tokens.slice(0, flagIndex).includes(ref))
|
|
1917
|
-
return false;
|
|
1918
|
-
// Skip past either `--flag value` (2 tokens) or `--flag=value` (1 token)
|
|
1919
|
-
// before checking whether the ref appears elsewhere as a real positional.
|
|
1920
|
-
const TOKENS_AFTER_SPACE_FLAG = 2;
|
|
1921
|
-
const TOKENS_AFTER_EQUALS_FLAG = 1;
|
|
1922
|
-
const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? TOKENS_AFTER_SPACE_FLAG : TOKENS_AFTER_EQUALS_FLAG);
|
|
1923
|
-
if (tokens.slice(firstTokenAfterFlag).includes(ref))
|
|
1924
|
-
return false;
|
|
1925
|
-
return true;
|
|
1926
|
-
}
|
|
1927
|
-
function resolveVaultListRef(ref) {
|
|
1928
|
-
if (ref === undefined)
|
|
1929
|
-
return undefined;
|
|
1930
|
-
const parsedFormat = parseFlagValue(process.argv, "--format");
|
|
1931
|
-
if (parsedFormat !== undefined && ref === parsedFormat && wasRefMisparsedAsFlagValue(ref, "--format", parsedFormat)) {
|
|
1932
|
-
return undefined;
|
|
1956
|
+
const slashIndex = target.lastIndexOf("/");
|
|
1957
|
+
if (slashIndex <= 0) {
|
|
1958
|
+
throw new NotFoundError(`Vault not found: ${target.includes(":") ? target : `vault:${target}`}`);
|
|
1933
1959
|
}
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1960
|
+
const refPart = target.slice(0, slashIndex);
|
|
1961
|
+
const key = target.slice(slashIndex + 1).trim();
|
|
1962
|
+
if (!key) {
|
|
1963
|
+
throw new UsageError("Expected vault run target in the form <ref> or <ref/KEY>.");
|
|
1937
1964
|
}
|
|
1938
|
-
|
|
1965
|
+
const resolved = resolveVaultPath(refPart);
|
|
1966
|
+
if (!fs.existsSync(resolved.absPath)) {
|
|
1967
|
+
throw new NotFoundError(`Vault not found: ${makeVaultRef(resolved.name, resolved.source)}`);
|
|
1968
|
+
}
|
|
1969
|
+
return { ref: makeVaultRef(resolved.name, resolved.source), key };
|
|
1939
1970
|
}
|
|
1940
1971
|
const vaultListCommand = defineCommand({
|
|
1941
|
-
meta: { name: "list", description: "List vaults
|
|
1942
|
-
|
|
1943
|
-
ref: { type: "positional", description: "Optional vault ref (e.g. vault:prod or just prod)", required: false },
|
|
1944
|
-
},
|
|
1945
|
-
run({ args }) {
|
|
1972
|
+
meta: { name: "list", description: "List all vaults across all stashes with their available key names (no values)" },
|
|
1973
|
+
run() {
|
|
1946
1974
|
return runWithJsonErrors(async () => {
|
|
1947
|
-
const { listKeys
|
|
1948
|
-
const effectiveRef = resolveVaultListRef(args.ref);
|
|
1949
|
-
if (effectiveRef) {
|
|
1950
|
-
const { name, absPath } = resolveVaultPath(effectiveRef);
|
|
1951
|
-
if (!fs.existsSync(absPath)) {
|
|
1952
|
-
throw new NotFoundError(`Vault not found: vault:${name}`);
|
|
1953
|
-
}
|
|
1954
|
-
const entries = listEntries(absPath);
|
|
1955
|
-
output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
|
|
1956
|
-
return;
|
|
1957
|
-
}
|
|
1975
|
+
const { listKeys } = await import("./commands/vault.js");
|
|
1958
1976
|
const vaults = listVaultsRecursive(listKeys);
|
|
1959
1977
|
output("vault-list", { vaults });
|
|
1960
1978
|
});
|
|
@@ -1968,9 +1986,9 @@ const vaultCreateCommand = defineCommand({
|
|
|
1968
1986
|
run({ args }) {
|
|
1969
1987
|
return runWithJsonErrors(async () => {
|
|
1970
1988
|
const { createVault } = await import("./commands/vault.js");
|
|
1971
|
-
const { name, absPath } = resolveVaultPath(args.name);
|
|
1989
|
+
const { name, absPath, source } = resolveVaultPath(args.name);
|
|
1972
1990
|
createVault(absPath);
|
|
1973
|
-
output("vault-create", { ref:
|
|
1991
|
+
output("vault-create", { ref: makeVaultRef(name, source), path: absPath });
|
|
1974
1992
|
});
|
|
1975
1993
|
},
|
|
1976
1994
|
});
|
|
@@ -1992,7 +2010,7 @@ const vaultSetCommand = defineCommand({
|
|
|
1992
2010
|
run({ args }) {
|
|
1993
2011
|
return runWithJsonErrors(async () => {
|
|
1994
2012
|
const { setKey } = await import("./commands/vault.js");
|
|
1995
|
-
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2013
|
+
const { name, absPath, source } = resolveVaultPath(args.ref);
|
|
1996
2014
|
let realKey;
|
|
1997
2015
|
let realValue;
|
|
1998
2016
|
if ((args.value === undefined || args.value === "") && args.key.includes("=")) {
|
|
@@ -2005,7 +2023,7 @@ const vaultSetCommand = defineCommand({
|
|
|
2005
2023
|
realValue = args.value ?? "";
|
|
2006
2024
|
}
|
|
2007
2025
|
setKey(absPath, realKey, realValue, args.comment);
|
|
2008
|
-
output("vault-set", { ref:
|
|
2026
|
+
output("vault-set", { ref: makeVaultRef(name, source), key: realKey, path: absPath });
|
|
2009
2027
|
});
|
|
2010
2028
|
},
|
|
2011
2029
|
});
|
|
@@ -2018,100 +2036,89 @@ const vaultUnsetCommand = defineCommand({
|
|
|
2018
2036
|
run({ args }) {
|
|
2019
2037
|
return runWithJsonErrors(async () => {
|
|
2020
2038
|
const { unsetKey } = await import("./commands/vault.js");
|
|
2021
|
-
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2039
|
+
const { name, absPath, source } = resolveVaultPath(args.ref);
|
|
2022
2040
|
if (!fs.existsSync(absPath)) {
|
|
2023
|
-
throw new NotFoundError(`Vault not found:
|
|
2041
|
+
throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
|
|
2024
2042
|
}
|
|
2025
2043
|
const removed = unsetKey(absPath, args.key);
|
|
2026
|
-
output("vault-unset", { ref:
|
|
2044
|
+
output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed, path: absPath });
|
|
2027
2045
|
});
|
|
2028
2046
|
},
|
|
2029
2047
|
});
|
|
2030
|
-
const
|
|
2048
|
+
const vaultPathCommand = defineCommand({
|
|
2031
2049
|
meta: {
|
|
2032
|
-
name: "
|
|
2033
|
-
description: '
|
|
2050
|
+
name: "path",
|
|
2051
|
+
description: 'Print the absolute vault file path so you can load it directly, e.g. `source "$(akm vault path vault:prod)"`.',
|
|
2034
2052
|
},
|
|
2035
2053
|
args: {
|
|
2036
2054
|
ref: { type: "positional", description: "Vault ref", required: true },
|
|
2037
2055
|
},
|
|
2038
|
-
|
|
2056
|
+
run({ args }) {
|
|
2039
2057
|
return runWithJsonErrors(async () => {
|
|
2040
|
-
|
|
2041
|
-
// is a shell snippet intended for `eval`, not structured output.
|
|
2042
|
-
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2058
|
+
const { name, absPath, source } = resolveVaultPath(args.ref);
|
|
2043
2059
|
if (!fs.existsSync(absPath)) {
|
|
2044
|
-
throw new NotFoundError(`Vault not found:
|
|
2045
|
-
}
|
|
2046
|
-
const { buildShellExportScript } = await import("./commands/vault.js");
|
|
2047
|
-
const crypto = await import("node:crypto");
|
|
2048
|
-
const os = await import("node:os");
|
|
2049
|
-
// Parse via dotenv (no expansion, no code execution) and build a
|
|
2050
|
-
// script of literal `export KEY='value'` lines with `'\''` escaping.
|
|
2051
|
-
// Sourcing this is safe even if the raw vault file contained shell
|
|
2052
|
-
// metacharacters like $, backticks, or $(...).
|
|
2053
|
-
const script = buildShellExportScript(absPath);
|
|
2054
|
-
// Write to a mode-0600 temp file the shell can source.
|
|
2055
|
-
//
|
|
2056
|
-
// INTENTIONAL: this site uses `os.tmpdir()` (i.e. `/tmp` on Unix)
|
|
2057
|
-
// rather than `${getCacheDir()}/vault/`. The temp file is written
|
|
2058
|
-
// mode-0600, sourced by the parent shell via `eval`, and immediately
|
|
2059
|
-
// `rm -f`'d on the same line of the emitted snippet. `/tmp` is the
|
|
2060
|
-
// conventional location for short-lived shell-eval scratch files and
|
|
2061
|
-
// benefits from tmp-cleanup-on-reboot semantics, which operators
|
|
2062
|
-
// expect for ephemeral secret material. Moving to `~/.cache/akm/`
|
|
2063
|
-
// would surprise those operators and also persist the file across
|
|
2064
|
-
// reboots if the eval is interrupted before the inline `rm -f` runs.
|
|
2065
|
-
// The bench/registry-build rationale (#276/#284) — orphan dirs
|
|
2066
|
-
// accumulating under `/tmp` from long-running builds — does not
|
|
2067
|
-
// apply here: the file is single-shot, a few hundred bytes, and
|
|
2068
|
-
// removed by the same shell command that sources it.
|
|
2069
|
-
// Regression test: tests/vault-load-error.test.ts verifies the
|
|
2070
|
-
// emitted snippet contains both `. <path>` and `rm -f <path>`.
|
|
2071
|
-
const tmpPath = path.join(os.tmpdir(), `akm-vault-${crypto.randomBytes(12).toString("hex")}.sh`);
|
|
2072
|
-
fs.writeFileSync(tmpPath, script, { mode: 0o600, encoding: "utf8" });
|
|
2073
|
-
try {
|
|
2074
|
-
fs.chmodSync(tmpPath, 0o600);
|
|
2075
|
-
}
|
|
2076
|
-
catch {
|
|
2077
|
-
/* best-effort on platforms without chmod */
|
|
2060
|
+
throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
|
|
2078
2061
|
}
|
|
2079
|
-
|
|
2080
|
-
// Emit: source the temp file, then remove it — values reach bash only
|
|
2081
|
-
// via the temp file (mode 0600), never via akm's stdout.
|
|
2082
|
-
process.stdout.write(`. ${quotedTmp}; rm -f ${quotedTmp}\n`);
|
|
2062
|
+
process.stdout.write(`${absPath}\n`);
|
|
2083
2063
|
});
|
|
2084
2064
|
},
|
|
2085
2065
|
});
|
|
2086
|
-
const
|
|
2087
|
-
meta: {
|
|
2066
|
+
const vaultRunCommand = defineCommand({
|
|
2067
|
+
meta: {
|
|
2068
|
+
name: "run",
|
|
2069
|
+
description: "Run a command with env injected from a vault or a single vault key: `akm vault run <ref[/KEY]> -- <command>`",
|
|
2070
|
+
},
|
|
2088
2071
|
args: {
|
|
2089
|
-
|
|
2072
|
+
target: { type: "positional", description: "Vault ref or ref/key target", required: true },
|
|
2090
2073
|
},
|
|
2091
2074
|
run({ args }) {
|
|
2092
2075
|
return runWithJsonErrors(async () => {
|
|
2093
|
-
const
|
|
2094
|
-
|
|
2076
|
+
const dashIndex = process.argv.indexOf("--");
|
|
2077
|
+
if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
|
|
2078
|
+
throw new UsageError("Missing command. Usage: akm vault run <ref[/KEY]> -- <command>");
|
|
2079
|
+
}
|
|
2080
|
+
const command = process.argv.slice(dashIndex + 1);
|
|
2081
|
+
const { loadEnv } = await import("./commands/vault.js");
|
|
2082
|
+
const { ref, key } = splitVaultRunTarget(args.target);
|
|
2083
|
+
const { name, absPath, source } = resolveVaultPath(ref);
|
|
2095
2084
|
if (!fs.existsSync(absPath)) {
|
|
2096
|
-
throw new NotFoundError(`Vault not found:
|
|
2085
|
+
throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
|
|
2097
2086
|
}
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2087
|
+
const envValues = loadEnv(absPath);
|
|
2088
|
+
const mergedEnv = { ...process.env };
|
|
2089
|
+
if (key) {
|
|
2090
|
+
if (!(key in envValues)) {
|
|
2091
|
+
throw new NotFoundError(`Key not found in ${makeVaultRef(name, source)}: ${key}`);
|
|
2092
|
+
}
|
|
2093
|
+
mergedEnv[key] = envValues[key];
|
|
2094
|
+
}
|
|
2095
|
+
else {
|
|
2096
|
+
for (const [envKey, envValue] of Object.entries(envValues)) {
|
|
2097
|
+
mergedEnv[envKey] = envValue;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
2101
|
+
stdio: "inherit",
|
|
2102
|
+
env: mergedEnv,
|
|
2103
|
+
});
|
|
2104
|
+
if (result.error)
|
|
2105
|
+
throw result.error;
|
|
2106
|
+
process.exit(result.status ?? 0);
|
|
2100
2107
|
});
|
|
2101
2108
|
},
|
|
2102
2109
|
});
|
|
2103
2110
|
const vaultCommand = defineCommand({
|
|
2104
2111
|
meta: {
|
|
2105
2112
|
name: "vault",
|
|
2106
|
-
description: "Manage secret vaults (.env files).
|
|
2113
|
+
description: "Manage secret vaults (.env files). Keys are visible, values stay on disk and never appear in structured output.",
|
|
2107
2114
|
},
|
|
2108
2115
|
subCommands: {
|
|
2109
2116
|
list: vaultListCommand,
|
|
2110
|
-
|
|
2117
|
+
path: vaultPathCommand,
|
|
2118
|
+
run: vaultRunCommand,
|
|
2111
2119
|
create: vaultCreateCommand,
|
|
2112
2120
|
set: vaultSetCommand,
|
|
2113
2121
|
unset: vaultUnsetCommand,
|
|
2114
|
-
load: vaultLoadCommand,
|
|
2115
2122
|
},
|
|
2116
2123
|
run({ args }) {
|
|
2117
2124
|
return runWithJsonErrors(async () => {
|
|
@@ -2374,10 +2381,26 @@ const eventsListCommand = defineCommand({
|
|
|
2374
2381
|
},
|
|
2375
2382
|
type: { type: "string", description: "Filter by event type (add, remove, remember, feedback, ...)" },
|
|
2376
2383
|
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
2384
|
+
"exclude-tags": {
|
|
2385
|
+
type: "string",
|
|
2386
|
+
description: "Exclude events matching these tags (repeatable)",
|
|
2387
|
+
},
|
|
2388
|
+
"include-tags": {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
description: "Only include events with ALL these tags (repeatable)",
|
|
2391
|
+
},
|
|
2377
2392
|
},
|
|
2378
2393
|
run({ args }) {
|
|
2379
2394
|
return runWithJsonErrors(() => {
|
|
2380
|
-
const
|
|
2395
|
+
const excludeTags = parseAllFlagValues("--exclude-tags");
|
|
2396
|
+
const includeTags = parseAllFlagValues("--include-tags");
|
|
2397
|
+
const result = akmEventsList({
|
|
2398
|
+
since: args.since,
|
|
2399
|
+
type: args.type,
|
|
2400
|
+
ref: args.ref,
|
|
2401
|
+
...(excludeTags.length > 0 ? { excludeTags } : {}),
|
|
2402
|
+
...(includeTags.length > 0 ? { includeTags } : {}),
|
|
2403
|
+
});
|
|
2381
2404
|
output("events-list", result);
|
|
2382
2405
|
});
|
|
2383
2406
|
},
|
|
@@ -2394,6 +2417,14 @@ const eventsTailCommand = defineCommand({
|
|
|
2394
2417
|
"interval-ms": { type: "string", description: "Polling interval in ms (default: 75)" },
|
|
2395
2418
|
"max-duration-ms": { type: "string", description: "Stop after this many ms (default: never)" },
|
|
2396
2419
|
"max-events": { type: "string", description: "Stop after observing this many events" },
|
|
2420
|
+
"exclude-tags": {
|
|
2421
|
+
type: "string",
|
|
2422
|
+
description: "Exclude events matching these tags (repeatable)",
|
|
2423
|
+
},
|
|
2424
|
+
"include-tags": {
|
|
2425
|
+
type: "string",
|
|
2426
|
+
description: "Only include events with ALL these tags (repeatable)",
|
|
2427
|
+
},
|
|
2397
2428
|
},
|
|
2398
2429
|
async run({ args }) {
|
|
2399
2430
|
await runWithJsonErrors(async () => {
|
|
@@ -2406,6 +2437,8 @@ const eventsTailCommand = defineCommand({
|
|
|
2406
2437
|
// also rendered through the standard output() pipeline so JSON
|
|
2407
2438
|
// consumers always get the canonical envelope.
|
|
2408
2439
|
const stream = mode.format === "text" || mode.format === "jsonl";
|
|
2440
|
+
const excludeTags = parseAllFlagValues("--exclude-tags");
|
|
2441
|
+
const includeTags = parseAllFlagValues("--include-tags");
|
|
2409
2442
|
const result = await akmEventsTail({
|
|
2410
2443
|
since: args.since,
|
|
2411
2444
|
type: args.type,
|
|
@@ -2413,6 +2446,8 @@ const eventsTailCommand = defineCommand({
|
|
|
2413
2446
|
intervalMs,
|
|
2414
2447
|
maxDurationMs,
|
|
2415
2448
|
maxEvents,
|
|
2449
|
+
...(excludeTags.length > 0 ? { excludeTags } : {}),
|
|
2450
|
+
...(includeTags.length > 0 ? { includeTags } : {}),
|
|
2416
2451
|
onEvent: stream
|
|
2417
2452
|
? (event) => {
|
|
2418
2453
|
if (mode.format === "jsonl") {
|
|
@@ -2575,6 +2610,14 @@ const distillCommand = defineCommand({
|
|
|
2575
2610
|
type: "string",
|
|
2576
2611
|
description: "Comma-separated asset refs whose feedback events MUST be filtered out before the LLM input is built. Falls back to AKM_DISTILL_EXCLUDE_FEEDBACK_FROM when omitted.",
|
|
2577
2612
|
},
|
|
2613
|
+
"exclude-tags": {
|
|
2614
|
+
type: "string",
|
|
2615
|
+
description: "Exclude feedback events matching these tags (repeatable, e.g. --exclude-tags slice:eval)",
|
|
2616
|
+
},
|
|
2617
|
+
"include-tags": {
|
|
2618
|
+
type: "string",
|
|
2619
|
+
description: "Only include feedback events with ALL these tags (repeatable)",
|
|
2620
|
+
},
|
|
2578
2621
|
},
|
|
2579
2622
|
async run({ args }) {
|
|
2580
2623
|
await runWithJsonErrors(async () => {
|
|
@@ -2583,10 +2626,38 @@ const distillCommand = defineCommand({
|
|
|
2583
2626
|
// CLI flag takes precedence over the env var when both are present.
|
|
2584
2627
|
const excludeRaw = excludeFlag ?? excludeEnv;
|
|
2585
2628
|
const excludeFeedbackFromRefs = parseExcludeFeedbackFromRefs(excludeRaw);
|
|
2629
|
+
const excludeTagsRaw = parseAllFlagValues("--exclude-tags");
|
|
2630
|
+
const excludeTagsEnv = process.env.AKM_DISTILL_EXCLUDE_TAGS;
|
|
2631
|
+
const excludeTags = [
|
|
2632
|
+
...new Set([
|
|
2633
|
+
...excludeTagsRaw,
|
|
2634
|
+
...(excludeTagsEnv
|
|
2635
|
+
? excludeTagsEnv
|
|
2636
|
+
.split(",")
|
|
2637
|
+
.map((s) => s.trim())
|
|
2638
|
+
.filter(Boolean)
|
|
2639
|
+
: []),
|
|
2640
|
+
]),
|
|
2641
|
+
];
|
|
2642
|
+
const includeTagsRaw = parseAllFlagValues("--include-tags");
|
|
2643
|
+
const includeTagsEnv = process.env.AKM_DISTILL_INCLUDE_TAGS;
|
|
2644
|
+
const includeTags = [
|
|
2645
|
+
...new Set([
|
|
2646
|
+
...includeTagsRaw,
|
|
2647
|
+
...(includeTagsEnv
|
|
2648
|
+
? includeTagsEnv
|
|
2649
|
+
.split(",")
|
|
2650
|
+
.map((s) => s.trim())
|
|
2651
|
+
.filter(Boolean)
|
|
2652
|
+
: []),
|
|
2653
|
+
]),
|
|
2654
|
+
];
|
|
2586
2655
|
const result = await akmDistill({
|
|
2587
2656
|
ref: args.ref,
|
|
2588
2657
|
sourceRun: getHyphenatedArg(args, "source-run"),
|
|
2589
2658
|
...(excludeFeedbackFromRefs.length > 0 ? { excludeFeedbackFromRefs } : {}),
|
|
2659
|
+
...(excludeTags.length > 0 ? { excludeTags } : {}),
|
|
2660
|
+
...(includeTags.length > 0 ? { includeTags } : {}),
|
|
2590
2661
|
});
|
|
2591
2662
|
output("distill", result);
|
|
2592
2663
|
});
|
|
@@ -2751,7 +2822,7 @@ const main = defineCommand({
|
|
|
2751
2822
|
},
|
|
2752
2823
|
});
|
|
2753
2824
|
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
|
|
2754
|
-
const VAULT_SUBCOMMAND_SET = new Set(["list", "
|
|
2825
|
+
const VAULT_SUBCOMMAND_SET = new Set(["list", "path", "run", "create", "set", "unset"]);
|
|
2755
2826
|
const WIKI_SUBCOMMAND_SET = new Set([
|
|
2756
2827
|
"create",
|
|
2757
2828
|
"register",
|
package/dist/commands/curate.js
CHANGED
|
@@ -135,6 +135,7 @@ async function enrichCuratedStashHit(query, hit) {
|
|
|
135
135
|
ref: hit.ref,
|
|
136
136
|
...(description ? { description } : {}),
|
|
137
137
|
...(preview ? { preview } : {}),
|
|
138
|
+
...(shown?.keys?.length ? { keys: shown.keys } : {}),
|
|
138
139
|
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
139
140
|
...(shown?.run ? { run: shown.run } : {}),
|
|
140
141
|
followUp: `akm show ${hit.ref}`,
|