akm-cli 0.5.0-rc2 → 0.5.0-rc3

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
@@ -651,7 +651,7 @@ function formatSearchPlain(r, detail) {
651
651
  function formatWikiListPlain(r) {
652
652
  const wikis = Array.isArray(r.wikis) ? r.wikis : [];
653
653
  if (wikis.length === 0)
654
- return "No wikis. Create one with `akm wiki create <name>`.";
654
+ return "No wikis. Create one with `akm wiki create <name>` or register one with `akm wiki register <name> <path-or-repo>`.";
655
655
  const lines = ["NAME\tPAGES\tRAWS\tLAST-MODIFIED"];
656
656
  for (const w of wikis) {
657
657
  const name = typeof w.name === "string" ? w.name : "?";
@@ -1205,11 +1205,19 @@ const addCommand = defineCommand({
1205
1205
  if (shouldWarnOnPlainHttp(ref)) {
1206
1206
  warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
1207
1207
  }
1208
- const websiteOptions = {};
1209
- if (args["max-pages"])
1210
- websiteOptions.maxPages = args["max-pages"];
1211
- if (args["max-depth"])
1212
- websiteOptions.maxDepth = args["max-depth"];
1208
+ const websiteOptions = buildWebsiteOptions(args);
1209
+ if (args.type === "wiki") {
1210
+ const { registerWikiSource } = await import("./stash-add");
1211
+ const result = await registerWikiSource({
1212
+ ref,
1213
+ name: args.name,
1214
+ options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
1215
+ trustThisInstall: args.trust,
1216
+ writable: args.writable,
1217
+ });
1218
+ output("add", result);
1219
+ return;
1220
+ }
1213
1221
  const result = await akmAdd({
1214
1222
  ref,
1215
1223
  name: args.name,
@@ -1222,6 +1230,14 @@ const addCommand = defineCommand({
1222
1230
  });
1223
1231
  },
1224
1232
  });
1233
+ function buildWebsiteOptions(args) {
1234
+ const websiteOptions = {};
1235
+ if (typeof args["max-pages"] === "string" && args["max-pages"].length > 0)
1236
+ websiteOptions.maxPages = args["max-pages"];
1237
+ if (typeof args["max-depth"] === "string" && args["max-depth"].length > 0)
1238
+ websiteOptions.maxDepth = args["max-depth"];
1239
+ return websiteOptions;
1240
+ }
1225
1241
  const VALID_SOURCE_KINDS = new Set(["local", "managed", "remote"]);
1226
1242
  function parseKindFilter(raw) {
1227
1243
  if (!raw)
@@ -2464,6 +2480,41 @@ const wikiCreateCommand = defineCommand({
2464
2480
  });
2465
2481
  },
2466
2482
  });
2483
+ const wikiRegisterCommand = defineCommand({
2484
+ meta: {
2485
+ name: "register",
2486
+ description: "Register an existing directory or repo as a first-class wiki without copying or mutating it",
2487
+ },
2488
+ args: {
2489
+ name: { type: "positional", description: "Wiki name (lowercase, digits, hyphens)", required: true },
2490
+ ref: { type: "positional", description: "Path or repo ref for the external wiki source", required: true },
2491
+ writable: {
2492
+ type: "boolean",
2493
+ description: "Mark a git-backed source as writable so changes can be pushed back",
2494
+ default: false,
2495
+ },
2496
+ trust: {
2497
+ type: "boolean",
2498
+ description: "Bypass install-audit blocking for this registration only",
2499
+ default: false,
2500
+ },
2501
+ "max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
2502
+ "max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
2503
+ },
2504
+ run({ args }) {
2505
+ return runWithJsonErrors(async () => {
2506
+ const { registerWikiSource } = await import("./stash-add");
2507
+ const result = await registerWikiSource({
2508
+ ref: args.ref.trim(),
2509
+ name: args.name,
2510
+ options: Object.keys(buildWebsiteOptions(args)).length > 0 ? buildWebsiteOptions(args) : undefined,
2511
+ trustThisInstall: args.trust,
2512
+ writable: args.writable,
2513
+ });
2514
+ output("wiki-register", result);
2515
+ });
2516
+ },
2517
+ });
2467
2518
  const wikiListCommand = defineCommand({
2468
2519
  meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
2469
2520
  run() {
@@ -2549,12 +2600,9 @@ const wikiSearchCommand = defineCommand({
2549
2600
  },
2550
2601
  run({ args }) {
2551
2602
  return runWithJsonErrors(async () => {
2552
- const { searchInWiki } = await import("./wiki.js");
2603
+ const { resolveWikiSource, searchInWiki } = await import("./wiki.js");
2553
2604
  const stashDir = resolveStashDir();
2554
- const wikiDir = path.join(stashDir, "wikis", args.name);
2555
- if (!fs.existsSync(wikiDir)) {
2556
- throw new NotFoundError(`Wiki not found: ${args.name}`);
2557
- }
2605
+ resolveWikiSource(stashDir, args.name);
2558
2606
  const parsedLimit = args.limit ? Number(args.limit) : undefined;
2559
2607
  const limit = typeof parsedLimit === "number" && Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : undefined;
2560
2608
  const response = await searchInWiki({ stashDir, wikiName: args.name, query: args.query, limit });
@@ -2633,6 +2681,7 @@ const wikiCommand = defineCommand({
2633
2681
  },
2634
2682
  subCommands: {
2635
2683
  create: wikiCreateCommand,
2684
+ register: wikiRegisterCommand,
2636
2685
  list: wikiListCommand,
2637
2686
  show: wikiShowCommand,
2638
2687
  remove: wikiRemoveCommand,
@@ -2974,14 +3023,15 @@ ranking can learn from actual usage.
2974
3023
 
2975
3024
  ## Wikis
2976
3025
 
2977
- Multi-wiki knowledge bases (Karpathy-style). Each wiki is a directory at
2978
- \`<stashDir>/wikis/<name>/\` with \`schema.md\`, \`index.md\`, \`log.md\`, \`raw/\`,
2979
- and agent-authored pages. akm owns lifecycle + raw-slug + lint + index
2980
- regeneration; page edits use your native Read/Write/Edit tools.
3026
+ Multi-wiki knowledge bases (Karpathy-style). A stash-owned wiki lives at
3027
+ \`<stashDir>/wikis/<name>/\`; external directories or repos can also be registered
3028
+ as first-class wikis. akm owns lifecycle + raw-slug + lint + index regeneration
3029
+ for stash-owned wikis; page edits use your native Read/Write/Edit tools.
2981
3030
 
2982
3031
  \`\`\`sh
2983
3032
  akm wiki list # List wikis (name, pages, raws, last-modified)
2984
3033
  akm wiki create research # Scaffold a new wiki
3034
+ akm wiki register ics-docs ~/code/ics-documentation # Register an external wiki
2985
3035
  akm wiki show research # Path, description, counts, last 3 log entries
2986
3036
  akm wiki pages research # Page refs + descriptions (excludes schema/index/log/raw)
2987
3037
  akm wiki search research "attention" # Scoped search (equivalent to --type wiki --wiki research)
@@ -393,7 +393,7 @@ async function tryVecScores(db, query, k, config) {
393
393
  }
394
394
  // ── Substring fallback (no index) ───────────────────────────────────────────
395
395
  async function substringSearch(query, searchType, limit, stashDir, sources, config) {
396
- const assets = await indexAssets(stashDir, searchType);
396
+ const assets = await indexAssets(stashDir, searchType, sources);
397
397
  const matched = assets.filter((asset) => !query || buildSearchText(asset.entry).includes(query));
398
398
  if (!query) {
399
399
  const sorted = matched.sort(compareAssets);
@@ -568,7 +568,12 @@ function readFileSize(filePath) {
568
568
  return undefined;
569
569
  }
570
570
  }
571
- async function indexAssets(stashDir, type) {
571
+ async function indexAssets(stashDir, type, sources) {
572
+ const resolvedStashDir = realpathOrResolve(stashDir);
573
+ const source = sources?.find((entry) => realpathOrResolve(entry.path) === resolvedStashDir);
574
+ if (source?.wikiName) {
575
+ return indexWikiRootAssets(stashDir, source.wikiName, type);
576
+ }
572
577
  const assets = [];
573
578
  const filterType = type === "any" ? undefined : type;
574
579
  const fileContexts = walkStashFlat(stashDir);
@@ -626,6 +631,29 @@ async function indexAssets(stashDir, type) {
626
631
  }
627
632
  return assets;
628
633
  }
634
+ async function indexWikiRootAssets(wikiRoot, wikiName, type) {
635
+ if (type !== "any" && type !== "wiki")
636
+ return [];
637
+ const assets = [];
638
+ for (const ctx of walkStashFlat(wikiRoot)) {
639
+ if (ctx.ext !== ".md")
640
+ continue;
641
+ if (!shouldIndexStashFile(wikiRoot, ctx.absPath, { treatStashRootAsWikiRoot: true }))
642
+ continue;
643
+ const relNoExt = ctx.relPath.replace(/\.md$/, "");
644
+ assets.push({
645
+ entry: {
646
+ name: `${wikiName}/${relNoExt}`,
647
+ type: "wiki",
648
+ filename: ctx.fileName,
649
+ description: ctx.frontmatter()?.description,
650
+ source: "frontmatter",
651
+ },
652
+ path: ctx.absPath,
653
+ });
654
+ }
655
+ return assets;
656
+ }
629
657
  function compareAssets(a, b) {
630
658
  if (a.entry.type !== b.entry.type)
631
659
  return a.entry.type.localeCompare(b.entry.type);
@@ -659,3 +687,11 @@ function deduplicateAssetsByPath(assets) {
659
687
  return true;
660
688
  });
661
689
  }
690
+ function realpathOrResolve(targetPath) {
691
+ try {
692
+ return fs.realpathSync(targetPath);
693
+ }
694
+ catch {
695
+ return path.resolve(targetPath);
696
+ }
697
+ }
package/dist/stash-add.js CHANGED
@@ -9,7 +9,7 @@ import { detectStashRoot, installRegistryRef, upsertInstalledRegistryEntry } fro
9
9
  import { parseRegistryRef } from "./registry-resolve";
10
10
  import { ensureWebsiteMirror, validateWebsiteInputUrl } from "./stash-providers/website";
11
11
  import { warn } from "./warn";
12
- import { validateWikiName } from "./wiki";
12
+ import { ensureWikiNameAvailable, validateWikiName } from "./wiki";
13
13
  const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
14
14
  export async function akmAdd(input) {
15
15
  const ref = input.ref.trim();
@@ -47,6 +47,20 @@ export async function akmAdd(input) {
47
47
  }
48
48
  return addRegistryKit(ref, stashDir, input.trustThisInstall, input.writable, wikiName);
49
49
  }
50
+ export async function registerWikiSource(input) {
51
+ const stashDir = resolveStashDir();
52
+ const name = input.name ?? deriveWikiNameFromRef(input.ref);
53
+ validateWikiName(name);
54
+ ensureWikiNameAvailable(stashDir, name);
55
+ return akmAdd({
56
+ ref: input.ref,
57
+ name,
58
+ overrideType: "wiki",
59
+ options: input.options,
60
+ trustThisInstall: input.trustThisInstall,
61
+ writable: input.writable,
62
+ });
63
+ }
50
64
  /**
51
65
  * Add a local directory as a filesystem stash source.
52
66
  * Creates a stashes[] entry instead of an installed[] entry.
@@ -22,11 +22,7 @@ import "./stash-providers/index";
22
22
  * `/`, e.g. `wiki:research`.
23
23
  */
24
24
  async function showWikiRoot(stashDir, wikiName) {
25
- const { showWiki, resolveWikiDir } = await import("./wiki.js");
26
- const wikiDir = resolveWikiDir(stashDir, wikiName);
27
- if (!fs.existsSync(wikiDir)) {
28
- throw new NotFoundError(`Wiki not found: ${wikiName}. Run \`akm wiki create ${wikiName}\` to create it.`);
29
- }
25
+ const { showWiki } = await import("./wiki.js");
30
26
  const result = showWiki(stashDir, wikiName);
31
27
  // Shape the WikiShowResult into a ShowResponse-compatible object.
32
28
  // The payload mirrors what `akm wiki show <name>` returns.
@@ -43,6 +39,44 @@ async function showWikiRoot(stashDir, wikiName) {
43
39
  recentLog: result.recentLog,
44
40
  };
45
41
  }
42
+ async function showWikiRootForSource(stashDir, source, wikiName) {
43
+ const { showWikiAtPath } = await import("./wiki.js");
44
+ if (source.wikiName === wikiName) {
45
+ const result = showWikiAtPath(wikiName, source.path);
46
+ return {
47
+ type: "wiki",
48
+ name: result.ref,
49
+ path: result.path,
50
+ ...(result.description ? { description: result.description } : {}),
51
+ origin: null,
52
+ editable: false,
53
+ pages: result.pages,
54
+ raws: result.raws,
55
+ ...(result.lastModified ? { lastModified: result.lastModified } : {}),
56
+ recentLog: result.recentLog,
57
+ };
58
+ }
59
+ return showWikiRoot(stashDir, wikiName);
60
+ }
61
+ function resolveRegisteredWikiAssetPath(wikiRoot, wikiName, assetName) {
62
+ const pageName = assetName === wikiName ? "" : assetName.slice(wikiName.length + 1);
63
+ if (!pageName) {
64
+ throw new NotFoundError(`Wiki page not found: wiki:${assetName}`);
65
+ }
66
+ const candidate = path.resolve(wikiRoot, `${pageName}.md`);
67
+ const resolvedRoot = fs.realpathSync(wikiRoot);
68
+ if (!candidate.startsWith(resolvedRoot + path.sep)) {
69
+ throw new UsageError("Ref resolves outside the stash root.");
70
+ }
71
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
72
+ throw new NotFoundError(`Stash asset not found for ref: wiki:${assetName}`);
73
+ }
74
+ const realTarget = fs.realpathSync(candidate);
75
+ if (!realTarget.startsWith(resolvedRoot + path.sep)) {
76
+ throw new UsageError("Ref resolves outside the stash root.");
77
+ }
78
+ return realTarget;
79
+ }
46
80
  /**
47
81
  * Unified show: tries local FTS5 index first, then remote providers.
48
82
  *
@@ -63,7 +97,7 @@ export async function akmShowUnified(input) {
63
97
  let lastError;
64
98
  for (const source of searchSources) {
65
99
  try {
66
- return await showWikiRoot(source.path, parsed.name);
100
+ return await showWikiRootForSource(allSources[0]?.path ?? source.path, source, parsed.name);
67
101
  }
68
102
  catch (err) {
69
103
  if (!(err instanceof NotFoundError))
@@ -145,8 +179,19 @@ export async function showLocal(input) {
145
179
  const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
146
180
  const allStashDirs = searchSources.map((s) => s.path);
147
181
  let assetPath;
182
+ const matchedSource = parsed.type === "wiki" ? searchSources.find((source) => parsed.name.startsWith(`${source.wikiName}/`)) : undefined;
148
183
  let lastError;
184
+ if (parsed.type === "wiki" && matchedSource?.wikiName) {
185
+ try {
186
+ assetPath = resolveRegisteredWikiAssetPath(matchedSource.path, matchedSource.wikiName, parsed.name);
187
+ }
188
+ catch (err) {
189
+ lastError = err instanceof Error ? err : new Error(String(err));
190
+ }
191
+ }
149
192
  for (const dir of allStashDirs) {
193
+ if (assetPath)
194
+ break;
150
195
  try {
151
196
  assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
152
197
  break;
@@ -165,14 +210,17 @@ export async function showLocal(input) {
165
210
  new NotFoundError(`Stash asset not found for ref: ${displayType}:${parsed.name}. ` +
166
211
  "Check the name with `akm search` or verify the asset exists in your stash."));
167
212
  }
168
- const source = findSourceForPath(assetPath, allSources);
213
+ const source = matchedSource ?? findSourceForPath(assetPath, allSources);
169
214
  const sourceStashDir = source?.path ?? allStashDirs[0];
170
215
  if (!sourceStashDir) {
171
216
  throw new UsageError(`Could not determine stash root for asset: ${displayType}:${parsed.name}. ` +
172
217
  "Run `akm init` to create the stash directory, or check `akm stash list` for configured paths.");
173
218
  }
174
219
  const fileCtx = buildFileContext(sourceStashDir, assetPath);
175
- const match = await runMatchers(fileCtx);
220
+ const forcedWikiMatch = parsed.type === "wiki" && source?.wikiName && parsed.name.startsWith(`${source.wikiName}/`)
221
+ ? { type: "wiki", specificity: 20, renderer: "wiki-md", meta: {} }
222
+ : undefined;
223
+ const match = forcedWikiMatch ?? (await runMatchers(fileCtx));
176
224
  if (!match) {
177
225
  throw new UsageError(`Could not display asset "${displayType}:${parsed.name}" — unsupported file type or unrecognized layout`);
178
226
  }
package/dist/wiki.js CHANGED
@@ -16,8 +16,10 @@ import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import { parse as yamlParse } from "yaml";
18
18
  import { isWithin } from "./common";
19
+ import { loadUserConfig, saveConfig } from "./config";
19
20
  import { NotFoundError, UsageError } from "./errors";
20
21
  import { parseFrontmatter, parseFrontmatterBlock } from "./frontmatter";
22
+ import { resolveStashSources } from "./search-source";
21
23
  import { akmSearch } from "./stash-search";
22
24
  import { buildIndexMd, buildLogMd, buildSchemaMd } from "./templates/wiki-templates";
23
25
  // ── Constants ───────────────────────────────────────────────────────────────
@@ -58,6 +60,41 @@ export function extractWikiNameFromRef(ref) {
58
60
  const match = ref.match(/^wiki:([a-z0-9][a-z0-9-]*)(?:\/|$)/);
59
61
  return match?.[1];
60
62
  }
63
+ function wikiNotFoundMessage(name) {
64
+ return `Wiki not found: ${name}. Run \`akm wiki create ${name}\` to create it or \`akm wiki register ${name} <path-or-repo>\` to register an external wiki.`;
65
+ }
66
+ function registeredWikiSources(stashDir) {
67
+ return resolveStashSources(stashDir)
68
+ .filter((source) => typeof source.wikiName === "string")
69
+ .map((source) => ({
70
+ name: source.wikiName,
71
+ path: source.path,
72
+ mode: "external",
73
+ source,
74
+ }));
75
+ }
76
+ export function resolveWikiSource(stashDir, name) {
77
+ validateWikiName(name);
78
+ const wikiDir = resolveWikiDir(stashDir, name);
79
+ if (fs.existsSync(wikiDir)) {
80
+ return { name, path: wikiDir, mode: "stash" };
81
+ }
82
+ const external = registeredWikiSources(stashDir).find((source) => source.name === name);
83
+ if (external)
84
+ return external;
85
+ throw new NotFoundError(wikiNotFoundMessage(name));
86
+ }
87
+ export function ensureWikiNameAvailable(stashDir, name) {
88
+ validateWikiName(name);
89
+ const wikiDir = resolveWikiDir(stashDir, name);
90
+ if (fs.existsSync(wikiDir)) {
91
+ throw new UsageError(`Wiki already exists: ${name}.`);
92
+ }
93
+ const external = registeredWikiSources(stashDir).find((source) => source.name === name);
94
+ if (external) {
95
+ throw new UsageError(`Wiki already registered: ${name}.`);
96
+ }
97
+ }
61
98
  /**
62
99
  * Walk a wiki directory and bucket files into pages vs raws.
63
100
  *
@@ -158,25 +195,20 @@ function toIsoDate(ms) {
158
195
  */
159
196
  export function listWikis(stashDir) {
160
197
  const wikisRoot = resolveWikisRoot(stashDir);
161
- if (!fs.existsSync(wikisRoot))
162
- return [];
163
- let entries;
164
- try {
165
- entries = fs.readdirSync(wikisRoot, { withFileTypes: true });
166
- }
167
- catch {
168
- return [];
198
+ const summaries = new Map();
199
+ let entries = [];
200
+ if (fs.existsSync(wikisRoot)) {
201
+ try {
202
+ entries = fs.readdirSync(wikisRoot, { withFileTypes: true });
203
+ }
204
+ catch {
205
+ entries = [];
206
+ }
169
207
  }
170
- const summaries = [];
171
- for (const entry of entries) {
172
- if (!entry.isDirectory())
173
- continue;
174
- if (!WIKI_NAME_RE.test(entry.name))
175
- continue;
176
- const dir = path.join(wikisRoot, entry.name);
208
+ const summarize = (name, dir) => {
177
209
  const buckets = scanWikiFiles(dir);
178
210
  const summary = {
179
- name: entry.name,
211
+ name,
180
212
  path: dir,
181
213
  pages: buckets.pages.length,
182
214
  raws: buckets.raws.length,
@@ -186,10 +218,21 @@ export function listWikis(stashDir) {
186
218
  summary.description = description;
187
219
  if (buckets.lastModifiedMs !== undefined)
188
220
  summary.lastModified = toIsoDate(buckets.lastModifiedMs);
189
- summaries.push(summary);
221
+ summaries.set(name, summary);
222
+ };
223
+ for (const entry of entries) {
224
+ if (!entry.isDirectory())
225
+ continue;
226
+ if (!WIKI_NAME_RE.test(entry.name))
227
+ continue;
228
+ summarize(entry.name, path.join(wikisRoot, entry.name));
229
+ }
230
+ for (const source of registeredWikiSources(stashDir)) {
231
+ if (summaries.has(source.name))
232
+ continue;
233
+ summarize(source.name, source.path);
190
234
  }
191
- summaries.sort((a, b) => a.name.localeCompare(b.name));
192
- return summaries;
235
+ return Array.from(summaries.values()).sort((a, b) => a.name.localeCompare(b.name));
193
236
  }
194
237
  // ── Show ────────────────────────────────────────────────────────────────────
195
238
  /**
@@ -238,11 +281,7 @@ function readRecentLog(wikiDir, limit = 3) {
238
281
  // Newest-first convention: the top `limit` `##` blocks are the most recent.
239
282
  return sections.slice(0, limit);
240
283
  }
241
- export function showWiki(stashDir, name) {
242
- const wikiDir = resolveWikiDir(stashDir, name);
243
- if (!fs.existsSync(wikiDir)) {
244
- throw new NotFoundError(`Wiki not found: ${name}. Run \`akm wiki create ${name}\` to create it.`);
245
- }
284
+ export function showWikiAtPath(name, wikiDir) {
246
285
  const buckets = scanWikiFiles(wikiDir);
247
286
  const result = {
248
287
  name,
@@ -259,8 +298,15 @@ export function showWiki(stashDir, name) {
259
298
  result.lastModified = toIsoDate(buckets.lastModifiedMs);
260
299
  return result;
261
300
  }
301
+ export function showWiki(stashDir, name) {
302
+ return showWikiAtPath(name, resolveWikiSource(stashDir, name).path);
303
+ }
262
304
  // ── Create ──────────────────────────────────────────────────────────────────
263
305
  export function createWiki(stashDir, name) {
306
+ const existing = registeredWikiSources(stashDir).find((source) => source.name === name);
307
+ if (existing) {
308
+ throw new UsageError(`Wiki already registered: ${name}.`);
309
+ }
264
310
  const wikiDir = resolveWikiDir(stashDir, name);
265
311
  fs.mkdirSync(wikiDir, { recursive: true });
266
312
  const files = [
@@ -307,7 +353,25 @@ export function createWiki(stashDir, name) {
307
353
  * ignore that (e.g. idempotent cleanup) by catching.
308
354
  */
309
355
  export function removeWiki(stashDir, name, options = {}) {
310
- const wikiDir = resolveWikiDir(stashDir, name);
356
+ const resolved = resolveWikiSource(stashDir, name);
357
+ const wikiDir = resolved.path;
358
+ if (resolved.mode === "external") {
359
+ const config = loadUserConfig();
360
+ const stashes = (config.stashes ?? []).filter((entry) => entry.wikiName !== name);
361
+ const installed = (config.installed ?? []).filter((entry) => entry.wikiName !== name);
362
+ saveConfig({
363
+ ...config,
364
+ stashes: stashes.length > 0 ? stashes : undefined,
365
+ installed: installed.length > 0 ? installed : undefined,
366
+ });
367
+ return {
368
+ name,
369
+ path: wikiDir,
370
+ removed: [],
371
+ preservedRaw: false,
372
+ unregistered: true,
373
+ };
374
+ }
311
375
  if (!fs.existsSync(wikiDir)) {
312
376
  throw new NotFoundError(`Wiki not found: ${name}.`);
313
377
  }
@@ -420,10 +484,7 @@ function readPageFrontmatter(absPath) {
420
484
  * path, and frontmatter-derived fields for orientation.
421
485
  */
422
486
  export function listPages(stashDir, name) {
423
- const wikiDir = resolveWikiDir(stashDir, name);
424
- if (!fs.existsSync(wikiDir)) {
425
- throw new NotFoundError(`Wiki not found: ${name}.`);
426
- }
487
+ const wikiDir = resolveWikiSource(stashDir, name).path;
427
488
  const { pages } = scanWikiFiles(wikiDir);
428
489
  const result = [];
429
490
  for (const abs of pages) {
@@ -447,13 +508,22 @@ export function listPages(stashDir, name) {
447
508
  */
448
509
  export async function searchInWiki(input) {
449
510
  validateWikiName(input.wikiName);
450
- const wikiDir = resolveWikiDir(input.stashDir, input.wikiName);
451
511
  const response = await akmSearch({
452
512
  query: input.query,
453
513
  type: "wiki",
454
514
  limit: input.limit,
455
515
  source: "stash",
456
516
  });
517
+ let wikiDir;
518
+ try {
519
+ wikiDir = resolveWikiSource(input.stashDir, input.wikiName).path;
520
+ }
521
+ catch (err) {
522
+ if (err instanceof NotFoundError) {
523
+ return { ...response, hits: [], registryHits: undefined };
524
+ }
525
+ throw err;
526
+ }
457
527
  const rawDir = path.join(wikiDir, RAW_SUBDIR);
458
528
  const filtered = [];
459
529
  for (const hit of response.hits) {
@@ -564,10 +634,7 @@ function ensureTrailingNewline(value) {
564
634
  * job (see `akm wiki ingest <name>` for the workflow).
565
635
  */
566
636
  export function stashRaw(input) {
567
- const wikiDir = resolveWikiDir(input.stashDir, input.wikiName);
568
- if (!fs.existsSync(wikiDir)) {
569
- throw new NotFoundError(`Wiki not found: ${input.wikiName}. Run \`akm wiki create ${input.wikiName}\` first.`);
570
- }
637
+ const wikiDir = resolveWikiSource(input.stashDir, input.wikiName).path;
571
638
  const rawDir = path.join(wikiDir, RAW_SUBDIR);
572
639
  fs.mkdirSync(rawDir, { recursive: true });
573
640
  const baseSlug = slugifyForWiki(input.preferredName ?? deriveQueryFromSource(input.content) ?? "source");
@@ -598,10 +665,7 @@ export function stashRaw(input) {
598
665
  * - `stale-index`: `index.md` mtime is older than the newest page mtime
599
666
  */
600
667
  export function lintWiki(stashDir, name) {
601
- const wikiDir = resolveWikiDir(stashDir, name);
602
- if (!fs.existsSync(wikiDir)) {
603
- throw new NotFoundError(`Wiki not found: ${name}.`);
604
- }
668
+ const wikiDir = resolveWikiSource(stashDir, name).path;
605
669
  const pages = listPages(stashDir, name);
606
670
  const { raws, pagesLastModifiedMs } = scanWikiFiles(wikiDir);
607
671
  const pageRefs = new Set(pages.map((p) => p.ref));
@@ -822,10 +886,7 @@ export function regenerateAllWikiIndexes(stashDir) {
822
886
  * a verb here and in the printer stays colocated.
823
887
  */
824
888
  export function buildIngestWorkflow(stashDir, name) {
825
- const wikiDir = resolveWikiDir(stashDir, name);
826
- if (!fs.existsSync(wikiDir)) {
827
- throw new NotFoundError(`Wiki not found: ${name}. Run \`akm wiki create ${name}\` first.`);
828
- }
889
+ const wikiDir = resolveWikiSource(stashDir, name).path;
829
890
  const schemaPath = path.join(wikiDir, SCHEMA_MD);
830
891
  const workflow = `# Ingest workflow for wiki:${name}
831
892
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.5.0-rc2",
3
+ "version": "0.5.0-rc3",
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": [