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 +52 -3
- package/dist/commands/installed-stashes.js +27 -1
- package/dist/commands/show.js +23 -2
- package/dist/indexer/db-search.js +6 -3
- package/dist/indexer/metadata.js +3 -8
- package/dist/wiki/wiki.js +9 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -445,7 +445,8 @@ const showCommand = defineCommand({
|
|
|
445
445
|
}
|
|
446
446
|
}
|
|
447
447
|
const cliDetail = getOutputMode().detail;
|
|
448
|
-
const
|
|
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
|
-
|
|
1547
|
-
|
|
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,
|
package/dist/commands/show.js
CHANGED
|
@@ -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
|
|
102
|
-
* returns
|
|
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
|
-
|
|
119
|
-
|
|
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
|
package/dist/indexer/metadata.js
CHANGED
|
@@ -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 `
|
|
259
|
-
*
|
|
260
|
-
*
|
|
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
|
|
103
|
-
*
|
|
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
|
|
499
|
-
*
|
|
500
|
-
*
|
|
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.
|
|
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": [
|