akm-cli 0.7.4 → 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.
Files changed (37) hide show
  1. package/{CHANGELOG.md → .github/CHANGELOG.md} +33 -0
  2. package/.github/LICENSE +374 -0
  3. package/dist/cli.js +241 -170
  4. package/dist/commands/curate.js +1 -0
  5. package/dist/commands/distill.js +14 -4
  6. package/dist/commands/events.js +10 -1
  7. package/dist/commands/migration-help.js +2 -2
  8. package/dist/commands/propose.js +36 -16
  9. package/dist/commands/reflect.js +40 -14
  10. package/dist/commands/remember.js +1 -1
  11. package/dist/commands/show.js +19 -44
  12. package/dist/commands/vault.js +5 -10
  13. package/dist/core/asset-registry.js +1 -1
  14. package/dist/core/asset-spec.js +1 -1
  15. package/dist/core/config.js +13 -0
  16. package/dist/core/events.js +19 -2
  17. package/dist/indexer/db-search.js +35 -235
  18. package/dist/indexer/db.js +15 -5
  19. package/dist/indexer/ensure-index.js +72 -0
  20. package/dist/indexer/graph-extraction.js +10 -0
  21. package/dist/indexer/indexer.js +38 -22
  22. package/dist/integrations/agent/prompts.js +95 -15
  23. package/dist/integrations/agent/spawn.js +65 -12
  24. package/dist/llm/client.js +40 -2
  25. package/dist/llm/graph-extract.js +2 -4
  26. package/dist/llm/memory-infer.js +7 -4
  27. package/dist/output/cli-hints.js +17 -8
  28. package/dist/output/renderers.js +6 -1
  29. package/dist/output/shapes.js +8 -3
  30. package/dist/output/text.js +18 -19
  31. package/dist/sources/providers/git.js +43 -1
  32. package/dist/workflows/db.js +9 -0
  33. package/dist/workflows/runs.js +25 -8
  34. package/dist/workflows/scope-key.js +76 -0
  35. package/docs/migration/release-notes/0.7.4.md +1 -1
  36. package/docs/migration/release-notes/0.7.5.md +20 -0
  37. 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 (!effectiveName) {
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 metadata = args.note ? JSON.stringify({ note: args.note }) : undefined;
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 current index. Run "akm index" and try again.`);
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: { signal, ...(args.note ? { note: args.note } : {}) },
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 { listWorkflowRuns: listRuns } = await import("./workflows/runs.js");
1165
- const { runs: existingRuns } = listRuns({});
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 the vaults/
1845
- // asset directory. Values are NEVER written to stdout. `vault load` is
1846
- // the only value-emitting path: it parses the vault with dotenv, writes
1847
- // a safely-escaped shell script to a mode-0600 temp file, and emits only
1848
- // `. <temp>; rm -f <temp>` on stdout for `eval`. The shell reads values
1849
- // from the temp file — they never transit through akm's stdout.
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 stashDir = resolveStashDir({ readOnly: true });
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 typeRoot = path.join(stashDir, "vaults");
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
- if (!fs.existsSync(vaultsDir))
1871
- return result;
1872
- const walk = (dir) => {
1873
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1874
- const full = path.join(dir, entry.name);
1875
- if (entry.isDirectory()) {
1876
- walk(full);
1877
- continue;
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
- if (!entry.isFile())
1880
- continue;
1881
- if (entry.name !== ".env" && !entry.name.endsWith(".env"))
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 wasRefMisparsedAsFlagValue(ref, flag, flagValue) {
1894
- const argv = process.argv.slice(2);
1895
- const vaultIndex = argv.indexOf("vault");
1896
- const listIndex = vaultIndex >= 0 ? argv.indexOf("list", vaultIndex + 1) : -1;
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
- if (flagIndex === -1)
1913
- return false;
1914
- // If the same token appeared before the flag, the user explicitly passed it
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 parsedDetail = parseFlagValue(process.argv, "--detail");
1935
- if (parsedDetail !== undefined && ref === parsedDetail && wasRefMisparsedAsFlagValue(ref, "--detail", parsedDetail)) {
1936
- return undefined;
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
- return ref;
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, or list keys (no values) inside one vault" },
1942
- args: {
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, listEntries } = await import("./commands/vault.js");
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: `vault:${name}`, path: absPath });
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: `vault:${name}`, key: realKey, path: absPath });
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: vault:${name}`);
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: `vault:${name}`, key: args.key, removed, path: absPath });
2044
+ output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed, path: absPath });
2027
2045
  });
2028
2046
  },
2029
2047
  });
2030
- const vaultLoadCommand = defineCommand({
2048
+ const vaultPathCommand = defineCommand({
2031
2049
  meta: {
2032
- name: "load",
2033
- description: 'Emit a shell snippet that loads vault values into the current shell. Use: eval "$(akm vault load vault:<name>)". Values are parsed by dotenv, written to a mode-0600 temp file with safe single-quote escaping, then sourced and removed. No values appear on akm\'s stdout, and no shell expansion happens on raw vault content.',
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
- async run({ args }) {
2056
+ run({ args }) {
2039
2057
  return runWithJsonErrors(async () => {
2040
- // This command deliberately bypasses output()/JSON shaping. Its stdout
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: vault:${name}`);
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
- const quotedTmp = `'${tmpPath.replace(/'/g, "'\\''")}'`;
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 vaultShowCommand = defineCommand({
2087
- meta: { name: "show", description: "Show keys (no values) inside a vault — alias for `vault list <ref>`" },
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
- ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
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 { listEntries } = await import("./commands/vault.js");
2094
- const { name, absPath } = resolveVaultPath(args.ref);
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: vault:${name}`);
2085
+ throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
2097
2086
  }
2098
- const entries = listEntries(absPath);
2099
- output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
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). Lists keys + comments only values never returned in structured output.",
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
- show: vaultShowCommand,
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 result = akmEventsList({ since: args.since, type: args.type, ref: args.ref });
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", "show", "create", "set", "unset", "load"]);
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",
@@ -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}`,