akm-cli 0.9.17-alpha.4 → 0.9.17-alpha.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 CHANGED
@@ -6,6 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.17-alpha.5] - 2026-09-27
10
+
11
+ `akm show` works again for a memory that has a `.derived.md` child (835 of them
12
+ in one real bundle), and `akm bundle add --provider … --name` holds to the same
13
+ `--name` contract as every other add.
14
+
15
+ ### Fixed
16
+
17
+ - **`akm show` works for a memory that has a `.derived.md` child.** When
18
+ `memories/X.md` and `memories/X.derived.md` both existed, `akm show
19
+ memories/X`, with or without a `#fragment`, failed with
20
+ `RESOURCE_ALREADY_EXISTS` ("multiple physical owners"); `akm curate`
21
+ previewed such a memory from its description alone, and `akm curate --pack`
22
+ left it out. The index gives the derived child its own ref,
23
+ `memories/X.derived`, but the ref lookup also counted `X.derived.md` as a
24
+ file for `memories/X`. The lookup now follows the index: `memories/X` is
25
+ `X.md` and `memories/X.derived` is `X.derived.md`. A derived child whose
26
+ parent file is gone no longer answers for the parent's ref either, so it
27
+ cannot hide a real `X.md` in a lower-priority bundle. `akm lint` and
28
+ `--xref` / `--supersedes` validation still accept a ref to `memories/X`
29
+ when only `X.derived.md` remains. Broken since 0.9.7.
30
+ (`src/core/asset/asset-placement.ts`, `src/commands/lint/base-linter.ts`)
31
+ - **`akm bundle add --provider … --name` keeps the `--name` contract too.**
32
+ Since 0.9.17-alpha.4 an explicit `--name` that is not a legal bundle slug,
33
+ or is taken by another bundle, fails with exit 2, and re-adding a source
34
+ under a different name points at `akm bundle rename`. A declarative add
35
+ (`akm bundle add <target> --provider npm|git|website`) still replaced such
36
+ a name with a derived one and exited 0. It now fails the same way, before
37
+ any write. (`src/commands/sources/source-manage.ts`)
38
+
9
39
  ## [0.9.17-alpha.4] - 2026-09-27
10
40
 
11
41
  Search and curate are rebuilt on measured evidence. On a 221-query suite of real
@@ -640,9 +670,8 @@ config migration, and it lands with fewer lines in `src/` than 0.9.17-alpha.3.
640
670
  documented for `registryId`. The key used to come from the basename of the
641
671
  cache directory the package was unpacked into, which is always `extracted`,
642
672
  so every registry bundle after the first was `extracted-<hash>`. A dotted
643
- or mixed-case name is slugged like a directory name (`Foo.js` → `foo-js`),
644
- and a `--name` that is not a legal bundle slug now falls back to this name
645
- too. Bundles that are already installed keep their current key, including
673
+ or mixed-case name is slugged like a directory name (`Foo.js` → `foo-js`).
674
+ Bundles that are already installed keep their current key, including
646
675
  `extracted`, because every recorded `extracted//…` ref depends on it.
647
676
  - **A one-file change in a large directory no longer costs `akm index` half
648
677
  an hour.** Both full-text tables keyed their per-entry deletes on
@@ -42,6 +42,7 @@ import { isArchivedRelPath } from "../../core/asset/memory-archive.js";
42
42
  import { conceptIdFromTypeName, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
43
43
  import { localDateStamp } from "../../core/common.js";
44
44
  import { containsRedactedContent, REDACTED_CONTENT_MARKER } from "../../core/content-safety.js";
45
+ import { DERIVED_SUFFIX } from "../../core/recognition-util.js";
45
46
  import { findFenceRegions } from "./markdown-insertion.js";
46
47
  // ── Helpers ───────────────────────────────────────────────────────────────────
47
48
  /** Fold physically wrapped prose the same way a YAML plain scalar does. */
@@ -216,10 +217,23 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
216
217
  // record while leaving the live stash untouched.
217
218
  return memoryArchiveHasRef(refType, refName, stashRoots);
218
219
  }
220
+ /**
221
+ * The stash-relative files that satisfy a ref, in preference order: its own
222
+ * placement spellings, then, for a memory, the `<name>.derived.md` child (#882),
223
+ * so an edge to a parent whose plain `.md` is gone still reaches the child it was
224
+ * distilled into. This is lint's reachability rule only; the child owns
225
+ * `memories/<name>.derived`, never `memories/<name>`.
226
+ */
227
+ function refPathCandidates(refType, typeDir, refName) {
228
+ const candidates = assetPathCandidatesForName(refType, typeDir, refName);
229
+ if (refType !== "memory" || refName.endsWith(DERIVED_SUFFIX))
230
+ return candidates;
231
+ return [...candidates, assetPathForName(refType, typeDir, `${refName}${DERIVED_SUFFIX}`)];
232
+ }
219
233
  /**
220
234
  * True when `(refType, refName)` names a memory that prune archived in any
221
235
  * root. Mirrors `resolveRefPathInStash`'s candidate set so a ref that resolved
222
- * through the `.derived.md` twin (#882) still resolves once archived.
236
+ * through the `.derived.md` child (#882) still resolves once archived.
223
237
  */
224
238
  function memoryArchiveHasRef(refType, refName, stashRoots) {
225
239
  if (refType !== "memory")
@@ -227,7 +241,7 @@ function memoryArchiveHasRef(refType, refName, stashRoots) {
227
241
  const typeDir = stashDirFor(refType);
228
242
  if (typeDir === undefined)
229
243
  return false;
230
- const candidates = assetPathCandidatesForName(refType, typeDir, refName);
244
+ const candidates = refPathCandidates(refType, typeDir, refName);
231
245
  for (const root of stashRoots) {
232
246
  for (const candidate of candidates) {
233
247
  if (isArchivedRelPath(candidate, root))
@@ -241,8 +255,8 @@ function memoryArchiveHasRef(refType, refName, stashRoots) {
241
255
  * the same reachability rules (in the same order) as
242
256
  * {@link refExistsInAnyStash}, which delegates here. Returns the absolute path
243
257
  * of the file that makes the ref "exist" — for a multi-file skill directory
244
- * that is its `SKILL.md` primary, for a `memory` ref its `.derived.md` twin
245
- * when the plain `.md` is absent (#882, see `assetPathCandidatesForName`) —
258
+ * that is its `SKILL.md` primary, for a `memory` ref its `.derived.md` child
259
+ * when the plain `.md` is absent (#882, see {@link refPathCandidates}) —
246
260
  * or `null` when the ref does not resolve in this root.
247
261
  *
248
262
  * Extracted for SPEC-5 (`--supersedes` demotion): write commands need the
@@ -253,7 +267,7 @@ function memoryArchiveHasRef(refType, refName, stashRoots) {
253
267
  */
254
268
  export function resolveRefPathInStash(relPath, refType, refName, root) {
255
269
  const typeDir = stashDirFor(refType);
256
- const candidates = typeDir === undefined ? [relPath] : assetPathCandidatesForName(refType, typeDir, refName);
270
+ const candidates = typeDir === undefined ? [relPath] : refPathCandidates(refType, typeDir, refName);
257
271
  for (const candidate of candidates) {
258
272
  const absPath = path.join(root, candidate);
259
273
  if (fs.existsSync(absPath))
@@ -93,12 +93,9 @@ export function bundleKeyForUrl(config, url) {
93
93
  * (path/url) — the shared {@link deriveBundleId} rule (D-R5), made unique against
94
94
  * the currently-configured bundle keys.
95
95
  *
96
- * This helper stays forgiving (no `--name` contract enforcement): it is also
97
- * used by `akm source add` (`source-manage.ts`'s `addStash`), which predates
98
- * and is not in scope for the D6 `--name` contract. A caller that DOES need
99
- * the D6 contract (an illegal or already-taken explicit name failing loudly)
100
- * validates with {@link validateExplicitBundleName} itself before calling in,
101
- * as `source-add.ts`'s local/website/registry add paths do.
96
+ * Every add path validates an explicit `--name` with
97
+ * {@link validateExplicitBundleName} before calling in (an illegal or taken
98
+ * name fails loudly), so a `preferredName` that reaches here is returned as is.
102
99
  */
103
100
  export function nextBundleKey(bundles, preferredName, seedLocator) {
104
101
  return deriveBundleId(preferredName, seedLocator, new Set(Object.keys(bundles)));
@@ -3,6 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import path from "node:path";
5
5
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
6
+ import { validateExplicitBundleName } from "../../core/bundle-id.js";
6
7
  import { isRemoteUrl } from "../../core/common.js";
7
8
  import { bundleEntryToSourceEntry, bundlesToSourceEntries, getSources, mutateConfig } from "../../core/config/config.js";
8
9
  import { ConfigError, UsageError } from "../../core/errors.js";
@@ -51,7 +52,10 @@ export function addStash(opts) {
51
52
  const bundles = { ...(config.bundles ?? {}) };
52
53
  let key;
53
54
  if (useDescriptorPath) {
54
- if (bundleKeyForUrl(config, target)) {
55
+ const existingKey = bundleKeyForUrl(config, target);
56
+ if (name !== undefined)
57
+ validateExplicitBundleName(bundles, name, existingKey);
58
+ if (existingKey) {
55
59
  const already = targetIsUrl ? "Source URL already configured" : "Source already configured";
56
60
  result = { sources: getSources(config), added: false, message: already };
57
61
  return config;
@@ -69,7 +73,10 @@ export function addStash(opts) {
69
73
  }
70
74
  else {
71
75
  const resolvedPath = path.resolve(target);
72
- if (bundleKeyForPath(config, resolvedPath)) {
76
+ const existingKey = bundleKeyForPath(config, resolvedPath);
77
+ if (name !== undefined)
78
+ validateExplicitBundleName(bundles, name, existingKey);
79
+ if (existingKey) {
73
80
  result = { sources: getSources(config), added: false, message: "Source path already configured" };
74
81
  return config;
75
82
  }
@@ -26,7 +26,7 @@
26
26
  import fs from "node:fs";
27
27
  import path from "node:path";
28
28
  import { toPosix } from "../common.js";
29
- import { DERIVED_SUFFIX, SCRIPT_EXTENSIONS, WORKFLOW_EXTENSIONS } from "../recognition-util.js";
29
+ import { SCRIPT_EXTENSIONS, WORKFLOW_EXTENSIONS } from "../recognition-util.js";
30
30
  const workflowSpec = {
31
31
  isRelevantFile: (fileName) => WORKFLOW_EXTENSIONS.includes(path.extname(fileName).toLowerCase()),
32
32
  toCanonicalName: (typeRoot, filePath) => {
@@ -245,21 +245,12 @@ export function assetPathForName(assetType, typeRoot, name) {
245
245
  * "default" alias is genuinely dual-owned: both `<dir>/.env` and
246
246
  * `<dir>/default.env` derive the same canonical name (`toCanonicalName`
247
247
  * above), so a physical-owner lookup must consider both without reading
248
- * either file. `memory` has a second, analogous duality (#882): a ref to
249
- * `<name>` may own either `<name>.md` or the LLM-inferred `<name>.derived.md`
250
- * twin — `.derived` is a provenance marker on the SAME identity, not part of
251
- * the name (see `resolveParentRef`/`isDerivedMemory` in
252
- * `commands/improve/memory/derived-ref.ts`, and the belief-edge identity
253
- * channel's own `memory:<name>.derived` refs). The plain `.md` file wins when
254
- * both exist, so it stays `primary` — first in the returned list — and every
255
- * caller here already prefers the first candidate that exists on disk. Every
256
- * other placement type has exactly one inverse spelling.
248
+ * either file. Every other placement type has exactly one inverse spelling —
249
+ * including `memory`: `<name>.derived.md` is a separate item that owns
250
+ * `<name>.derived`, never a second spelling of `<name>`.
257
251
  */
258
252
  export function assetPathCandidatesForName(assetType, typeRoot, name) {
259
253
  const primary = assetPathForName(assetType, typeRoot, name);
260
- if (assetType === "memory" && !name.endsWith(DERIVED_SUFFIX)) {
261
- return [primary, assetPathForName(assetType, typeRoot, `${name}${DERIVED_SUFFIX}`)];
262
- }
263
254
  if (assetType !== "env")
264
255
  return [primary];
265
256
  const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
@@ -30545,7 +30545,6 @@ var SCRIPT_EXTENSIONS = new Set([
30545
30545
  ".kts"
30546
30546
  ]);
30547
30547
  var WORKFLOW_EXTENSIONS = [".md", ".yml"];
30548
- var DERIVED_SUFFIX = ".derived";
30549
30548
  var KNOWN_TYPES = [
30550
30549
  "skill",
30551
30550
  "command",
@@ -30703,9 +30702,6 @@ function assetPathForName(assetType, typeRoot, name) {
30703
30702
  }
30704
30703
  function assetPathCandidatesForName(assetType, typeRoot, name) {
30705
30704
  const primary = assetPathForName(assetType, typeRoot, name);
30706
- if (assetType === "memory" && !name.endsWith(DERIVED_SUFFIX)) {
30707
- return [primary, assetPathForName(assetType, typeRoot, `${name}${DERIVED_SUFFIX}`)];
30708
- }
30709
30705
  if (assetType !== "env")
30710
30706
  return [primary];
30711
30707
  const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
@@ -29873,7 +29873,6 @@ var SCRIPT_EXTENSIONS = new Set([
29873
29873
  ".kts"
29874
29874
  ]);
29875
29875
  var WORKFLOW_EXTENSIONS = [".md", ".yml"];
29876
- var DERIVED_SUFFIX = ".derived";
29877
29876
  var KNOWN_TYPES = [
29878
29877
  "skill",
29879
29878
  "command",
@@ -30031,9 +30030,6 @@ function assetPathForName(assetType, typeRoot, name) {
30031
30030
  }
30032
30031
  function assetPathCandidatesForName(assetType, typeRoot, name) {
30033
30032
  const primary = assetPathForName(assetType, typeRoot, name);
30034
- if (assetType === "memory" && !name.endsWith(DERIVED_SUFFIX)) {
30035
- return [primary, assetPathForName(assetType, typeRoot, `${name}${DERIVED_SUFFIX}`)];
30036
- }
30037
30033
  if (assetType !== "env")
30038
30034
  return [primary];
30039
30035
  const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.17-alpha.4",
3
+ "version": "0.9.17-alpha.5",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
6
6
  "keywords": [