akm-cli 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -445,7 +445,8 @@ const showCommand = defineCommand({
445
445
  }
446
446
  }
447
447
  const cliDetail = getOutputMode().detail;
448
- const showDetail = cliDetail === "summary" ? "summary" : undefined;
448
+ const explicitDetail = parseFlagValue(process.argv, "--detail");
449
+ const showDetail = explicitDetail === "brief" ? "brief" : cliDetail === "summary" ? "summary" : undefined;
449
450
  const result = await akmShowUnified({ ref: args.ref, view, detail: showDetail });
450
451
  output("show", result);
451
452
  });
@@ -1535,6 +1536,53 @@ function listVaultsRecursive(listKeysFn) {
1535
1536
  walk(vaultsDir);
1536
1537
  return result;
1537
1538
  }
1539
+ function wasRefMisparsedAsFlagValue(ref, flag, flagValue) {
1540
+ const argv = process.argv.slice(2);
1541
+ const vaultIndex = argv.indexOf("vault");
1542
+ const listIndex = vaultIndex >= 0 ? argv.indexOf("list", vaultIndex + 1) : -1;
1543
+ const tokens = listIndex >= 0 ? argv.slice(listIndex + 1) : argv;
1544
+ let flagIndex = -1;
1545
+ let flagConsumesNextToken = false;
1546
+ for (let i = 0; i < tokens.length; i += 1) {
1547
+ const token = tokens[i];
1548
+ if (token === flag) {
1549
+ flagIndex = i;
1550
+ flagConsumesNextToken = true;
1551
+ break;
1552
+ }
1553
+ if (token === `${flag}=${flagValue}`) {
1554
+ flagIndex = i;
1555
+ break;
1556
+ }
1557
+ }
1558
+ if (flagIndex === -1)
1559
+ return false;
1560
+ // If the same token appeared before the flag, the user explicitly passed it
1561
+ // as the positional ref and it was not consumed by the output flag.
1562
+ if (tokens.slice(0, flagIndex).includes(ref))
1563
+ return false;
1564
+ // Skip past either `--flag value` (2 tokens) or `--flag=value` (1 token)
1565
+ // before checking whether the ref appears elsewhere as a real positional.
1566
+ const TOKENS_AFTER_SPACE_FLAG = 2;
1567
+ const TOKENS_AFTER_EQUALS_FLAG = 1;
1568
+ const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? TOKENS_AFTER_SPACE_FLAG : TOKENS_AFTER_EQUALS_FLAG);
1569
+ if (tokens.slice(firstTokenAfterFlag).includes(ref))
1570
+ return false;
1571
+ return true;
1572
+ }
1573
+ function resolveVaultListRef(ref) {
1574
+ if (ref === undefined)
1575
+ return undefined;
1576
+ const parsedFormat = parseFlagValue(process.argv, "--format");
1577
+ if (parsedFormat !== undefined && ref === parsedFormat && wasRefMisparsedAsFlagValue(ref, "--format", parsedFormat)) {
1578
+ return undefined;
1579
+ }
1580
+ const parsedDetail = parseFlagValue(process.argv, "--detail");
1581
+ if (parsedDetail !== undefined && ref === parsedDetail && wasRefMisparsedAsFlagValue(ref, "--detail", parsedDetail)) {
1582
+ return undefined;
1583
+ }
1584
+ return ref;
1585
+ }
1538
1586
  const vaultListCommand = defineCommand({
1539
1587
  meta: { name: "list", description: "List vaults, or list keys (no values) inside one vault" },
1540
1588
  args: {
@@ -1543,8 +1591,9 @@ const vaultListCommand = defineCommand({
1543
1591
  run({ args }) {
1544
1592
  return runWithJsonErrors(async () => {
1545
1593
  const { listKeys, listEntries } = await import("./commands/vault.js");
1546
- if (args.ref) {
1547
- const { name, absPath } = resolveVaultPath(args.ref);
1594
+ const effectiveRef = resolveVaultListRef(args.ref);
1595
+ if (effectiveRef) {
1596
+ const { name, absPath } = resolveVaultPath(effectiveRef);
1548
1597
  if (!fs.existsSync(absPath)) {
1549
1598
  throw new NotFoundError(`Vault not found: vault:${name}`);
1550
1599
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
- import { resolveStashDir } from "../core/common";
9
+ import { isWithin, resolveStashDir } from "../core/common";
10
10
  import { loadConfig } from "../core/config";
11
11
  import { NotFoundError, UsageError } from "../core/errors";
12
12
  import { akmIndex } from "../indexer/indexer";
@@ -14,6 +14,7 @@ import { removeLockEntry, upsertLockEntry } from "../integrations/lockfile";
14
14
  import { parseRegistryRef } from "../registry/resolve";
15
15
  import { syncFromRef } from "../sources/providers/sync-from-ref";
16
16
  import { ensureWebsiteMirror } from "../sources/providers/website";
17
+ import { listWikis, resolveWikisRoot } from "../wiki/wiki";
17
18
  import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
18
19
  import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add";
19
20
  import { removeStash } from "./source-manage";
@@ -57,6 +58,31 @@ export async function akmListSources(input) {
57
58
  status: { exists: directoryExists(entry.stashRoot) },
58
59
  });
59
60
  }
61
+ if (!kindFilter || kindFilter.includes("filesystem")) {
62
+ const wikisRoot = resolveWikisRoot(stashDir);
63
+ const seenPaths = new Set(sources
64
+ .map((source) => source.path)
65
+ .filter((sourcePath) => typeof sourcePath === "string")
66
+ .map((sourcePath) => path.resolve(sourcePath)));
67
+ for (const wiki of listWikis(stashDir)) {
68
+ // `listWikis()` also includes externally-registered wikis. `akm list`
69
+ // should synthesize source entries here only for stash-owned wiki dirs.
70
+ if (!isWithin(wiki.path, wikisRoot))
71
+ continue;
72
+ const resolvedPath = path.resolve(wiki.path);
73
+ if (seenPaths.has(resolvedPath))
74
+ continue;
75
+ seenPaths.add(resolvedPath);
76
+ sources.push({
77
+ name: wiki.name,
78
+ kind: "filesystem",
79
+ wiki: wiki.name,
80
+ path: wiki.path,
81
+ writable: true,
82
+ status: { exists: directoryExists(wiki.path) },
83
+ });
84
+ }
85
+ }
60
86
  return {
61
87
  schemaVersion: 1,
62
88
  stashDir,
@@ -98,8 +98,8 @@ function resolveRegisteredWikiAssetPath(wikiRoot, wikiName, assetName) {
98
98
  * type-dir resolution if the index has no row. Spec §6.2; no remote provider
99
99
  * fallback.
100
100
  *
101
- * When `detail` is `"summary"`, the response omits content/template/prompt and
102
- * returns only compact metadata (name, type, description, tags, parameters).
101
+ * When `detail` is `"brief"` or `"summary"`, the response omits
102
+ * content/template/prompt and returns compact metadata.
103
103
  */
104
104
  export async function akmShowUnified(input) {
105
105
  const ref = input.ref.trim();
@@ -251,6 +251,9 @@ export async function showLocal(input) {
251
251
  editable,
252
252
  ...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
253
253
  };
254
+ if (input.detail === "brief") {
255
+ return buildBriefResponse(fullResponse, assetPath);
256
+ }
254
257
  if (input.detail === "summary") {
255
258
  return buildSummaryResponse(fullResponse, assetPath);
256
259
  }
@@ -270,6 +273,24 @@ export async function showByRef(ref) {
270
273
  const body = await fs.promises.readFile(entry.filePath, "utf8");
271
274
  return { filePath: entry.filePath, body };
272
275
  }
276
+ /**
277
+ * Build a reduced brief response from a full ShowResponse.
278
+ *
279
+ * Keeps routing/identification fields while omitting content/template/prompt.
280
+ */
281
+ function buildBriefResponse(full, assetPath) {
282
+ const summary = buildSummaryResponse(full, assetPath);
283
+ return {
284
+ type: summary.type,
285
+ name: summary.name,
286
+ path: summary.path,
287
+ ...(summary.description ? { description: summary.description } : {}),
288
+ ...(summary.action ? { action: summary.action } : {}),
289
+ ...(summary.run ? { run: summary.run } : {}),
290
+ ...(summary.origin !== undefined ? { origin: summary.origin } : {}),
291
+ ...(full.editable !== undefined ? { editable: full.editable } : {}),
292
+ };
293
+ }
273
294
  /**
274
295
  * Build a compact summary response from a full ShowResponse.
275
296
  *
@@ -17,7 +17,7 @@ import { defaultRendererRegistry } from "../core/asset-registry";
17
17
  import { deriveCanonicalAssetNameFromStashRoot } from "../core/asset-spec";
18
18
  import { getDbPath } from "../core/paths";
19
19
  import { warn } from "../core/warn";
20
- import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getUtilityScoresByIds, openDatabase, searchFts, searchVec, } from "./db";
20
+ import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getUtilityScoresByIds, openDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "./db";
21
21
  import { getRenderer } from "./file-context";
22
22
  import { generateMetadataFlat, loadStashFile, shouldIndexStashFile } from "./metadata";
23
23
  import { buildSearchText } from "./search-fields";
@@ -115,8 +115,11 @@ export async function searchLocal(input) {
115
115
  }
116
116
  // ── Database search ─────────────────────────────────────────────────────────
117
117
  async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry) {
118
- // Empty query: return all entries
119
- if (!query) {
118
+ const hasSearchableTokens = query.length > 0 && sanitizeFtsQuery(query).length > 0;
119
+ // Empty queries — including ones that sanitize down to no searchable FTS
120
+ // tokens such as "." — should enumerate matching entries instead of
121
+ // returning an empty result set from FTS.
122
+ if (!hasSearchableTokens) {
120
123
  const typeFilter = searchType === "any" ? undefined : searchType;
121
124
  const allEntries = getAllEntries(db, typeFilter);
122
125
  // Deduplicate by file path — multiple entries can share the same file
@@ -255,10 +255,9 @@ const WIKI_INFRA_FILES = new Set(["schema.md", "index.md", "log.md"]);
255
255
  * Apply wiki-specific index exclusions while leaving all other stash files
256
256
  * untouched.
257
257
  *
258
- * - In a normal stash, excludes `wikis/<name>/raw/**` and wiki-root
259
- * `schema.md`, `index.md`, `log.md`.
260
- * - In a wiki-root stash source (`wikiName`), excludes `raw/**` and those same
261
- * root-level infrastructure files.
258
+ * - In a normal stash, excludes wiki-root `schema.md`, `index.md`, `log.md`.
259
+ * - In a wiki-root stash source (`wikiName`), excludes those same root-level
260
+ * infrastructure files.
262
261
  */
263
262
  export function shouldIndexStashFile(stashRoot, file, options) {
264
263
  const relPath = path.relative(stashRoot, file);
@@ -268,8 +267,6 @@ export function shouldIndexStashFile(stashRoot, file, options) {
268
267
  if (segments.length === 0)
269
268
  return true;
270
269
  if (options?.treatStashRootAsWikiRoot) {
271
- if (segments[0] === "raw")
272
- return false;
273
270
  return !(segments.length === 1 && WIKI_INFRA_FILES.has(segments[0]));
274
271
  }
275
272
  const wikisIdx = segments.indexOf("wikis");
@@ -278,8 +275,6 @@ export function shouldIndexStashFile(stashRoot, file, options) {
278
275
  const wikiRelativeSegments = segments.slice(wikisIdx + 2);
279
276
  if (wikiRelativeSegments.length === 0)
280
277
  return true;
281
- if (wikiRelativeSegments[0] === "raw")
282
- return false;
283
278
  return !(wikiRelativeSegments.length === 1 && WIKI_INFRA_FILES.has(wikiRelativeSegments[0]));
284
279
  }
285
280
  /**
package/dist/wiki/wiki.js CHANGED
@@ -99,8 +99,9 @@ export function ensureWikiNameAvailable(stashDir, name) {
99
99
  * Walk a wiki directory and bucket files into pages vs raws.
100
100
  *
101
101
  * "Pages" are any `.md` files under the wiki root EXCEPT `schema.md`,
102
- * `index.md`, `log.md`, or anything under `raw/`. This matches the set the
103
- * agent edits, and the set `akm wiki pages` exposes.
102
+ * `index.md`, or `log.md`. Raw sources are bucketed separately so callers can
103
+ * distinguish authored pages from ingested source material while still
104
+ * surfacing both.
104
105
  *
105
106
  * Returns two mtime signals:
106
107
  * - `lastModifiedMs` — newest across all .md files. Used for the `show` /
@@ -495,15 +496,16 @@ function readPageFrontmatter(absPath) {
495
496
  return out;
496
497
  }
497
498
  /**
498
- * List the pages in a wiki, excluding `schema.md`, `index.md`, `log.md`, and
499
- * anything under `raw/`. Each entry carries its ref (`wiki:<name>/<page>`),
500
- * path, and frontmatter-derived fields for orientation.
499
+ * List the addressable markdown entries in a wiki, excluding only the
500
+ * infrastructure files `schema.md`, `index.md`, and `log.md`. This includes
501
+ * both authored pages and `raw/` sources so `wiki pages` can inventory content
502
+ * written via `akm wiki stash`.
501
503
  */
502
504
  export function listPages(stashDir, name) {
503
505
  const wikiDir = resolveWikiSource(stashDir, name).path;
504
- const { pages } = scanWikiFiles(wikiDir);
506
+ const { pages, raws } = scanWikiFiles(wikiDir);
505
507
  const result = [];
506
- for (const abs of pages) {
508
+ for (const abs of [...pages, ...raws]) {
507
509
  const pageName = pageNameFromPath(wikiDir, abs);
508
510
  const ref = `wiki:${name}/${pageName}`;
509
511
  const fm = readPageFrontmatter(abs);
@@ -540,7 +542,6 @@ export async function searchInWiki(input) {
540
542
  }
541
543
  throw err;
542
544
  }
543
- const rawDir = path.join(wikiDir, RAW_SUBDIR);
544
545
  const filtered = [];
545
546
  for (const hit of response.hits) {
546
547
  // hits can be SourceSearchHit or RegistrySearchResultHit (union); filter
@@ -556,9 +557,6 @@ export async function searchInWiki(input) {
556
557
  const basename = path.basename(stashHit.path);
557
558
  if (WIKI_SPECIAL_FILES.has(basename) && path.dirname(stashHit.path) === wikiDir)
558
559
  continue;
559
- // Exclude anything under raw/
560
- if (isWithin(stashHit.path, rawDir))
561
- continue;
562
560
  filtered.push(stashHit);
563
561
  }
564
562
  return { ...response, hits: filtered, registryHits: undefined };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Kit Manager) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [