akm-cli 0.10.261002-beta → 0.10.261004-beta

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,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.10.261004] - 2026-10-13
10
+
11
+ ### Fixed
12
+
13
+ - **`akm feedback` fails when it cannot record the feedback** (also in 0.9.32). When `state.db` was read-only to the
14
+ process, as it is inside an agent sandbox that only allows writes in the workspace, the feedback row was silently
15
+ dropped and the command still printed `"ok": true`. It now fails with a `DATA_DIR_NOT_WRITABLE` config error (exit
16
+ 78) and records nothing. Add akm's data directory to the sandbox's writable paths.
17
+
18
+ ## [0.10.261003] - 2026-10-12
19
+
20
+ ### Fixed
21
+
22
+ - **`akm upgrade` now keeps the OpenCode 2 plugin (`akm-opencode-v2`) current.** Three things stopped it:
23
+ `--next` refused with "pins akm-cli ... older than the ... that @latest pins" because npm's `latest` for
24
+ `akm-opencode-v2` is a retired daily alpha whose 8-digit pin sorts above every monthly build (and the lockstep
25
+ reported that as "could not read the akm-cli pin"); `--next` now follows the `next` tag and no longer compares it with
26
+ `@latest`, for OpenCode 1 as well. The refresh trashed the cache and deferred whenever an OpenCode process ran, which
27
+ on a host with a standing `opencode serve` was always. It now runs OpenCode 2's own `opencode plugin update <spec>`,
28
+ which installs the new build beside the old one for the running service to pick up at its next restart, so it is
29
+ neither trashed nor deferred. OpenCode 1 is unchanged.
30
+
31
+ - **`akm upgrade --next` no longer says the Claude Code and Codex plugins have "no prerelease channel" and work "with
32
+ any 0.9.x akm".** akm-plugins now has a `next` branch for prereleases, and the plugins require akm 0.10.261001 or
33
+ newer. The `--next` note now names the ref the `akm-plugins` marketplace follows (read from `claude plugin marketplace
34
+ list` and Codex's `config.toml`) and, when it is the stable default branch, how to switch to `next`. `akm upgrade`
35
+ still only updates the ref the marketplace follows.
36
+
9
37
  ## [0.10.261002] - 2026-10-12
10
38
 
11
39
  ### Fixed
@@ -13,17 +13,17 @@ import { parseFrontmatter, parseFrontmatterBlock, spliceFrontmatterLine } from "
13
13
  import { conceptIdFromTypeName, parseRefInput, typeNameFromConceptId } from "../core/asset/resolve-ref.js";
14
14
  import { isWithin, resolveStashDir, safeRealpath, writeFileAtomic } from "../core/common.js";
15
15
  import { loadConfig } from "../core/config/config.js";
16
- import { NotFoundError, UsageError } from "../core/errors.js";
16
+ import { ConfigError, NotFoundError, UsageError } from "../core/errors.js";
17
17
  import { appendEvent } from "../core/events.js";
18
18
  import { resolveMutationTarget } from "../core/mutation-target.js";
19
19
  import { isPathAbsent } from "../core/path-access.js";
20
20
  import { getDbPath } from "../core/paths.js";
21
- import { withStateDb } from "../core/state-db.js";
21
+ import { getStateDbPath, withStateDb } from "../core/state-db.js";
22
22
  import { warn } from "../core/warn.js";
23
23
  import { withWriteTargetMutation } from "../core/write-source.js";
24
24
  import { recomputeUtilityScores } from "../indexer/indexer.js";
25
25
  import { resolveSourceEntries } from "../indexer/search/search-source.js";
26
- import { insertUsageEvent, resolveUsageEventSource } from "../indexer/usage/usage-events.js";
26
+ import { insertUsageEventStrict, resolveUsageEventSource } from "../indexer/usage/usage-events.js";
27
27
  import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
28
28
  import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
29
29
  import { findEntryIdByRef, getEntryFilePathById, getItemRefById, } from "../storage/repositories/index-entries-repository.js";
@@ -161,20 +161,28 @@ const UTILITY_REVIEW_THRESHOLD = 0.5;
161
161
  * boosting its own picks. This is by design and must not be removed. The
162
162
  * utility score is read by improve and metrics only; its update is best-effort
163
163
  * and its failure never fails the feedback record or changes the ranking effect.
164
+ * A failed usage-event write fails the command (DATA_DIR_NOT_WRITABLE).
164
165
  */
165
166
  function recordFeedbackUsage(indexDb, entryId, durableEntryRef, signal, metadataStr) {
166
167
  let utilityResult;
167
168
  let rankingSkippedReason;
168
169
  const eventSource = resolveUsageEventSource();
169
170
  withStateDb((stateDb) => {
170
- insertUsageEvent(stateDb, {
171
- event_type: "feedback",
172
- entry_ref: durableEntryRef,
173
- entry_id: entryId,
174
- signal,
175
- metadata: metadataStr,
176
- source: eventSource,
177
- });
171
+ // The usage event IS the feedback record, so a failed write fails the command
172
+ // instead of reporting ok while nothing was stored.
173
+ try {
174
+ insertUsageEventStrict(stateDb, {
175
+ event_type: "feedback",
176
+ entry_ref: durableEntryRef,
177
+ entry_id: entryId,
178
+ signal,
179
+ metadata: metadataStr,
180
+ source: eventSource,
181
+ });
182
+ }
183
+ catch (err) {
184
+ throw new ConfigError(`Feedback was not recorded: could not write ${getStateDbPath()} (${err instanceof Error ? err.message : String(err)}).`, "DATA_DIR_NOT_WRITABLE");
185
+ }
178
186
  if (eventSource !== "user") {
179
187
  rankingSkippedReason =
180
188
  `feedback source is "${eventSource}", not "user" — it does not count toward search ranking, which counts ` +
@@ -88,22 +88,26 @@ function detectClaudeCode() {
88
88
  return { entry: skipped("claude-code", marketplaces.missing ? "claude is not on PATH" : marketplaces.error) };
89
89
  }
90
90
  const list = parseJson(marketplaces.stdout);
91
- if (!Array.isArray(list) || !list.some((m) => m?.name === MARKETPLACE)) {
91
+ const marketplace = Array.isArray(list) ? list.find((m) => m?.name === MARKETPLACE) : undefined;
92
+ if (!marketplace)
92
93
  return { entry: skipped("claude-code", `the ${MARKETPLACE} marketplace is not configured`) };
93
- }
94
94
  const plugin = claudePluginVersion();
95
95
  if ("error" in plugin)
96
96
  return { entry: skipped("claude-code", plugin.error.error) };
97
97
  if (plugin.version === undefined)
98
98
  return { entry: skipped("claude-code", "the akm plugin is not installed") };
99
- return { version: plugin.version };
99
+ return { version: plugin.version, ref: typeof marketplace.ref === "string" ? marketplace.ref : undefined };
100
100
  }
101
- function upgradeClaudeCode(dryRun) {
101
+ function upgradeClaudeCode(dryRun, next) {
102
102
  const detected = detectClaudeCode();
103
103
  if ("entry" in detected)
104
104
  return detected.entry;
105
+ const entry = refreshClaudeCode(dryRun, detected.version);
106
+ return next ? withChannelNote(entry, detected.ref, "re-add the marketplace as `itlackey/akm-plugins@next`") : entry;
107
+ }
108
+ function refreshClaudeCode(dryRun, from) {
105
109
  if (dryRun)
106
- return unknownRefresh("claude-code", detected.version);
110
+ return unknownRefresh("claude-code", from);
107
111
  const marketplace = runCommand("claude", ["plugin", "marketplace", "update", MARKETPLACE], REFRESH_TIMEOUT_MS);
108
112
  if (!marketplace.ok)
109
113
  return failed("claude-code", marketplace.error);
@@ -111,7 +115,7 @@ function upgradeClaudeCode(dryRun) {
111
115
  if (!update.ok)
112
116
  return failed("claude-code", update.error);
113
117
  const after = claudePluginVersion();
114
- return refreshed("claude-code", detected.version, "version" in after ? after.version : undefined);
118
+ return refreshed("claude-code", from, "version" in after ? after.version : undefined);
115
119
  }
116
120
  // ── Codex ───────────────────────────────────────────────────────────────────
117
121
  function codexPluginVersion() {
@@ -122,21 +126,39 @@ function codexPluginVersion() {
122
126
  const plugin = parsed?.installed?.find((p) => p?.pluginId === PLUGIN_ID);
123
127
  return { version: plugin ? String(plugin.version ?? "") : undefined };
124
128
  }
125
- function upgradeCodex(dryRun) {
129
+ /** The ref `[marketplaces.akm-plugins]` follows in Codex's config.toml; undefined when it names none (the default branch). */
130
+ function codexMarketplaceRef() {
131
+ try {
132
+ const home = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
133
+ const config = fs.readFileSync(path.join(home, "config.toml"), "utf8");
134
+ const section = config.split(/^\[/m).find((part) => part.startsWith(`marketplaces.${MARKETPLACE}]`));
135
+ return section?.match(/^ref\s*=\s*"([^"]+)"/m)?.[1];
136
+ }
137
+ catch {
138
+ return undefined;
139
+ }
140
+ }
141
+ function upgradeCodex(dryRun, next) {
126
142
  const before = codexPluginVersion();
127
143
  if ("error" in before)
128
144
  return skipped("codex", before.error.missing ? "codex is not on PATH" : before.error.error);
129
145
  if (before.version === undefined) {
130
146
  return skipped("codex", `the akm plugin is not installed from the ${MARKETPLACE} marketplace`);
131
147
  }
148
+ const entry = refreshCodex(dryRun, before.version);
149
+ return next
150
+ ? withChannelNote(entry, codexMarketplaceRef(), "run `codex plugin marketplace add itlackey/akm-plugins --ref next`")
151
+ : entry;
152
+ }
153
+ function refreshCodex(dryRun, from) {
132
154
  if (dryRun)
133
- return unknownRefresh("codex", before.version);
155
+ return unknownRefresh("codex", from);
134
156
  // Also refreshes the installed plugin's cache, so no re-add is needed.
135
157
  const upgrade = runCommand("codex", ["plugin", "marketplace", "upgrade", MARKETPLACE], REFRESH_TIMEOUT_MS);
136
158
  if (!upgrade.ok)
137
159
  return failed("codex", upgrade.error);
138
160
  const after = codexPluginVersion();
139
- return refreshed("codex", before.version, "version" in after ? after.version : undefined);
161
+ return refreshed("codex", from, "version" in after ? after.version : undefined);
140
162
  }
141
163
  // ── OpenCode ────────────────────────────────────────────────────────────────
142
164
  const nextSpec = (flavor) => `${flavor.pkg}@next`;
@@ -225,39 +247,6 @@ export function lookupOpenCodeLatest(tag, flavor) {
225
247
  const akmCli = parsed["dependencies.akm-cli"];
226
248
  return { version: parsed.version, akmCli: typeof akmCli === "string" ? akmCli : undefined };
227
249
  }
228
- /**
229
- * `<pkg>@next`, which must exist and pin an akm-cli no older than the one
230
- * `@latest` pins: a prerelease channel that trails the stable one is not a target.
231
- *
232
- * The two builds are compared by their akm-cli pins, never by their own
233
- * versions. akm-plugins' stable versions are `<akm_version><yyyymmddhhmm>`
234
- * concatenated into the PATCH (`0.9.27202610072331`), while prereleases are
235
- * `<akm_version>.<ts>` (`0.9.28-alpha.8.202610081938`), so by semver every
236
- * prerelease sorts below the stable build and the comparison means nothing
237
- * (#1089). For the same reason plugin versions are only ever tested for
238
- * equality (is the cache already this build?), never ordered.
239
- */
240
- export function lookupOpenCodeNext(flavor) {
241
- const next = lookupOpenCodeLatest("next", flavor);
242
- if ("error" in next)
243
- return next;
244
- const latest = lookupOpenCodeLatest("latest", flavor);
245
- if ("error" in latest)
246
- return { error: `could not compare with ${flavor.pkg}@latest: ${latest.error}` };
247
- // No pin on @next: returned as is, and the lockstep holds the CLI and says why.
248
- if (!next.akmCli)
249
- return next;
250
- if (!latest.akmCli) {
251
- return { error: `could not compare with ${flavor.pkg}@latest: it declares no akm-cli dependency` };
252
- }
253
- if (semverOrder(next.akmCli, latest.akmCli) < 0) {
254
- return {
255
- error: `${nextSpec(flavor)} (${next.version}) pins akm-cli ${next.akmCli}, older than the ` +
256
- `${latest.akmCli} that ${flavor.pkg}@latest (${latest.version}) pins`,
257
- };
258
- }
259
- return next;
260
- }
261
250
  const CONTAINER_CGROUP = /docker|containerd|podman|libpod|lxc/;
262
251
  /**
263
252
  * Whether the process could use this host's OpenCode cache. A process in another mount namespace (a container's
@@ -338,20 +327,24 @@ function upgradeOpenCode(dryRun, target) {
338
327
  if (cache.version === latest.version)
339
328
  return { harness, outcome: "current", from: cache.version, to: latest.version };
340
329
  const base = { harness, from: cache.version, to: latest.version };
341
- const running = openCodeRunning();
342
- if (running !== false) {
343
- return {
344
- ...base,
345
- outcome: "deferred",
346
- message: running === true
347
- ? "OpenCode is running; the plugin cache is replaced by a later `akm upgrade` once it has exited"
348
- : "could not tell whether OpenCode is running; the plugin cache is replaced by a later `akm upgrade`",
349
- };
330
+ // OpenCode 1's refresh replaces the cache the running OpenCode is using, so it waits for the exit.
331
+ // OpenCode 2's `plugin update` installs beside the old build, which a running service keeps using until it restarts.
332
+ if (flavor.replacesCache) {
333
+ const running = openCodeRunning();
334
+ if (running !== false) {
335
+ return {
336
+ ...base,
337
+ outcome: "deferred",
338
+ message: running === true
339
+ ? "OpenCode is running; the plugin cache is replaced by a later `akm upgrade` once it has exited"
340
+ : "could not tell whether OpenCode is running; the plugin cache is replaced by a later `akm upgrade`",
341
+ };
342
+ }
350
343
  }
351
344
  if (dryRun)
352
345
  return { ...base, outcome: "pending" };
353
- // OpenCode must be runnable before the cache goes: it does the prefetch, and only the major
354
- // that owns this plugin's cache can re-create it. akm never installs or replaces the binary.
346
+ // OpenCode must be runnable before anything changes: only the major that owns this plugin's cache can
347
+ // refresh it. akm never installs or replaces the binary.
355
348
  // OpenCode 2 may sit beside an OpenCode 1 `opencode` as `opencode2`; OpenCode 1 is only ever `opencode`.
356
349
  const bin = flavor.major === 2 ? resolveOpencodeBin() : OPENCODE_SDK_SERVER_BIN;
357
350
  const detected = detectOpencodeMajor(bin);
@@ -362,43 +355,55 @@ function upgradeOpenCode(dryRun, target) {
362
355
  // refresh needs a command only OpenCode 2 has, so it must be sure.
363
356
  if (major === undefined ? flavor.major === 2 : major !== flavor.major) {
364
357
  return skipped(harness, `\`${bin}\` on PATH is ${major === undefined ? "of an unknown version" : `OpenCode ${major}`}, ` +
365
- `not OpenCode ${flavor.major}, so it cannot re-create the ${flavor.pkg} cache; the cache is left alone`);
366
- }
367
- try {
368
- moveToTrash(cache.dir);
358
+ `not OpenCode ${flavor.major}, so it cannot refresh the ${flavor.pkg} cache; the cache is left alone`);
369
359
  }
370
- catch (error) {
371
- return failed(harness, `could not move ${cache.dir} to the trash: ${error instanceof Error ? error.message : String(error)}`);
360
+ if (flavor.replacesCache) {
361
+ try {
362
+ moveToTrash(cache.dir);
363
+ }
364
+ catch (error) {
365
+ return failed(harness, `could not move ${cache.dir} to the trash: ${error instanceof Error ? error.message : String(error)}`);
366
+ }
372
367
  }
368
+ const trashed = flavor.replacesCache ? " (the old cache is in the trash)" : "";
373
369
  // From a temp dir, so no project's opencode.json is read.
374
370
  const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-opencode-prefetch-"));
375
371
  try {
376
- const prefetch = runCommand(bin, flavor.prefetch(target.spec), PREFETCH_TIMEOUT_MS, { cwd: workDir });
377
- if (!prefetch.ok)
378
- return failed(harness, `${prefetch.error} (the old cache is in the trash)`);
372
+ let refresh = runCommand(bin, flavor.refresh(target.spec), PREFETCH_TIMEOUT_MS, { cwd: workDir });
373
+ // OpenCode 2 starts its background service for this command and answers "Plugin is not configured" until
374
+ // that service has loaded the plugins, so the first command after the service was down fails. Ask again.
375
+ for (let retry = 0; !refresh.ok && flavor.major === 2 && retry < 2 && /not configured/.test(refresh.error); retry++) {
376
+ refresh = runCommand(bin, flavor.refresh(target.spec), PREFETCH_TIMEOUT_MS, { cwd: workDir });
377
+ }
378
+ if (!refresh.ok)
379
+ return failed(harness, `${refresh.error}${trashed}`);
379
380
  }
380
381
  finally {
381
382
  fs.rmSync(workDir, { recursive: true, force: true });
382
383
  }
383
384
  const after = detectOpenCodeCache(tag, flavor);
384
385
  if (!after)
385
- return failed(harness, "opencode did not re-create the plugin cache (the old cache is in the trash)");
386
+ return failed(harness, `opencode did not re-create the plugin cache${trashed}`);
386
387
  return { ...base, outcome: "updated", to: after.version ?? latest.version };
387
388
  }
388
389
  // ── Orchestration ───────────────────────────────────────────────────────────
389
- const NO_PRERELEASE_CHANNEL = "--next: no prerelease channel; the akm-plugins marketplace is followed as usual and works with any 0.9.x akm";
390
- /** The note `--next` adds to a harness whose plugin has no prerelease channel. */
391
- function withNextNote(entry) {
390
+ /**
391
+ * The note `--next` adds to a marketplace harness. The channel is the ref the marketplace follows (`next` is the
392
+ * prerelease branch, the default branch is stable); akm only updates that ref and cannot switch it.
393
+ */
394
+ function withChannelNote(entry, ref, howToSwitch) {
392
395
  if (entry.outcome === "skipped")
393
396
  return entry;
394
- return { ...entry, message: entry.message ? `${entry.message}; ${NO_PRERELEASE_CHANNEL}` : NO_PRERELEASE_CHANNEL };
397
+ const note = ref === "next"
398
+ ? `--next: the ${MARKETPLACE} marketplace follows ref next, the prerelease channel`
399
+ : `--next: the ${MARKETPLACE} marketplace follows ${ref ? `ref ${ref}` : "the default branch (stable)"}, ` +
400
+ `not the prerelease channel; akm upgrade cannot switch it, ${howToSwitch} to follow prereleases`;
401
+ return { ...entry, message: entry.message ? `${entry.message}; ${note}` : note };
395
402
  }
396
403
  export function upgradePlugins(opts) {
397
- const claude = upgradeClaudeCode(opts.dryRun);
398
- const codex = upgradeCodex(opts.dryRun);
399
404
  return [
400
- opts.next ? withNextNote(claude) : claude,
401
- opts.next ? withNextNote(codex) : codex,
405
+ upgradeClaudeCode(opts.dryRun, opts.next),
406
+ upgradeCodex(opts.dryRun, opts.next),
402
407
  ...opts.openCode.map((target) => upgradeOpenCode(opts.dryRun, target)),
403
408
  ];
404
409
  }
@@ -500,7 +505,7 @@ function resolveOpenCodeTarget(next, flavor, configured) {
500
505
  return leftAlone(otherSpecNote(flavor, spec));
501
506
  const tag = followNext ? "next" : "latest";
502
507
  const cache = detectOpenCodeCache(tag, flavor);
503
- const latest = cache ? (followNext ? lookupOpenCodeNext(flavor) : lookupOpenCodeLatest("latest", flavor)) : undefined;
508
+ const latest = cache ? lookupOpenCodeLatest(tag, flavor) : undefined;
504
509
  return { flavor, cache, latest, tag, spec, notFollowingNext: next && !followNext };
505
510
  }
506
511
  /**
@@ -10,6 +10,7 @@ const CONFIG_HINTS = {
10
10
  STASH_DIR_NOT_A_DIRECTORY: "The configured default bundle path exists but isn't a directory. Update it to point at a folder.",
11
11
  STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update the default bundle path.",
12
12
  DATA_DIR_UNREADABLE: "The data directory is not readable by the user running akm. Check its owner and mode, or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns.",
13
+ DATA_DIR_NOT_WRITABLE: "Nothing was recorded. Make the akm data directory writable for this process (for an agent sandbox, add it to the writable paths), or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user can write, then retry.",
13
14
  INDEX_SCHEMA_INCOMPATIBLE: "Run `akm index --full` to rebuild the derived index from the currently materialized sources.",
14
15
  LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
15
16
  TEST_ISOLATION_MISSING: "Under bun test, when AKM_BUNDLE_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
@@ -55,14 +55,18 @@ export function ensureUsageEventsSchema(db) {
55
55
  */
56
56
  export function insertUsageEvent(db, event) {
57
57
  try {
58
- db.prepare(`INSERT INTO usage_events (event_type, query, entry_id, entry_ref, signal, metadata, source)
59
- VALUES (?, ?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null, event.source ?? "unknown");
58
+ insertUsageEventStrict(db, event);
60
59
  }
61
60
  catch (error) {
62
61
  rethrowIfTestIsolationError(error);
63
62
  /* fire-and-forget: silently ignore errors */
64
63
  }
65
64
  }
65
+ /** Insert a usage event and let a failed write throw, for callers whose record is the event itself. */
66
+ export function insertUsageEventStrict(db, event) {
67
+ db.prepare(`INSERT INTO usage_events (event_type, query, entry_id, entry_ref, signal, metadata, source)
68
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null, event.source ?? "unknown");
69
+ }
66
70
  // ── Query ────────────────────────────────────────────────────────────────────
67
71
  /**
68
72
  * User feedback counts for many assets at once, keyed by durable `entry_ref`.
@@ -48,7 +48,8 @@ export const OPENCODE_V1 = {
48
48
  cacheFolder: (spec) => path.join("packages", spec),
49
49
  nodeModulesDir: (folder) => path.join(folder, "node_modules"),
50
50
  // Any OpenCode command that resolves the config installs the plugin again.
51
- prefetch: () => ["debug", "config"],
51
+ refresh: () => ["debug", "config"],
52
+ replacesCache: true,
52
53
  };
53
54
  export const OPENCODE_V2 = {
54
55
  harness: "opencode-v2",
@@ -63,6 +64,8 @@ export const OPENCODE_V2 = {
63
64
  const build = newestBuildDir(folder);
64
65
  return build ? path.join(build, "node_modules") : undefined;
65
66
  },
66
- // `debug config` does not resolve plugins in OpenCode 2; `plugin add` of a configured spec only reinstalls it.
67
- prefetch: (spec) => ["plugin", "add", spec],
67
+ // `plugin update <spec>` (the spec as the config names it) installs the new build into a fresh
68
+ // `<epoch-ms>` folder and leaves the old ones; the running service stays on the old build until it restarts.
69
+ refresh: (spec) => ["plugin", "update", spec],
70
+ replacesCache: false,
68
71
  };
@@ -25658,6 +25658,7 @@ var CONFIG_HINTS = {
25658
25658
  STASH_DIR_NOT_A_DIRECTORY: "The configured default bundle path exists but isn't a directory. Update it to point at a folder.",
25659
25659
  STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update the default bundle path.",
25660
25660
  DATA_DIR_UNREADABLE: "The data directory is not readable by the user running akm. Check its owner and mode, or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns.",
25661
+ DATA_DIR_NOT_WRITABLE: "Nothing was recorded. Make the akm data directory writable for this process (for an agent sandbox, add it to the writable paths), or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user can write, then retry.",
25661
25662
  INDEX_SCHEMA_INCOMPATIBLE: "Run `akm index --full` to rebuild the derived index from the currently materialized sources.",
25662
25663
  LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
25663
25664
  TEST_ISOLATION_MISSING: "Under bun test, when AKM_BUNDLE_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
@@ -24986,6 +24986,7 @@ var CONFIG_HINTS = {
24986
24986
  STASH_DIR_NOT_A_DIRECTORY: "The configured default bundle path exists but isn't a directory. Update it to point at a folder.",
24987
24987
  STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update the default bundle path.",
24988
24988
  DATA_DIR_UNREADABLE: "The data directory is not readable by the user running akm. Check its owner and mode, or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns.",
24989
+ DATA_DIR_NOT_WRITABLE: "Nothing was recorded. Make the akm data directory writable for this process (for an agent sandbox, add it to the writable paths), or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user can write, then retry.",
24989
24990
  INDEX_SCHEMA_INCOMPATIBLE: "Run `akm index --full` to rebuild the derived index from the currently materialized sources.",
24990
24991
  LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
24991
24992
  TEST_ISOLATION_MISSING: "Under bun test, when AKM_BUNDLE_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
@@ -155,10 +155,12 @@ Other breaking changes:
155
155
 
156
156
  - `akm-opencode` is for OpenCode 1. `akm-opencode-v2` is for OpenCode 2. Each
157
157
  pins one exact `akm-cli`, and `akm upgrade` holds the CLI to the pin.
158
- - OpenCode caches the plugin spec it resolved and does not re-check it. A new
158
+ - OpenCode 1 caches the plugin spec it resolved and does not re-check it. A new
159
159
  build is picked up only after the plugin's cache folder is cleared. `akm
160
160
  upgrade` does this for `@latest` (or `@next`) when no OpenCode process runs.
161
- A spec pinned in your OpenCode config is left alone.
161
+ For OpenCode 2 it runs `opencode plugin update` instead, even while OpenCode
162
+ runs; the new build loads at the next restart. A spec pinned in your OpenCode
163
+ config is left alone.
162
164
  - OpenCode 1 asks before reading or writing outside the project directory.
163
165
  OpenCode 2 does not unless its config says `ask`, so akm's rejection of a
164
166
  permission request only happens there once you add the rule. akm changes no
@@ -407,12 +407,16 @@ run. For OpenCode 2, run `opencode plugin add akm-opencode-v2`.
407
407
  it holds the CLI to the older pin. It never moves the CLI backwards. If it
408
408
  cannot read the pin it leaves the CLI alone and exits 1. `akm health` warns
409
409
  when a plugin's bundled `akm-cli` differs from the running CLI.
410
- - **OpenCode caches the plugin spec it resolved, and never re-checks it.** A new
411
- plugin build is not picked up until its cache folder is cleared. `akm upgrade`
412
- does this for `@latest` (and `@next` with `--next`) when no OpenCode process
413
- is running: it moves the folder to the trash, then re-fetches (`opencode debug
414
- config` for OpenCode 1, `opencode plugin add akm-opencode-v2` for OpenCode 2).
415
- If your config pins another spec, such as `akm-opencode@0.9.30`, `akm upgrade`
410
+ - **OpenCode 1 caches the plugin spec it resolved, and never re-checks it.** A
411
+ new plugin build is not picked up until its cache folder is cleared. `akm
412
+ upgrade` does this for `@latest` (and `@next` with `--next`) when no OpenCode
413
+ process is running: it moves the folder to the trash, then re-fetches
414
+ (`opencode debug config`). **OpenCode 2** has an updater: `akm upgrade` runs
415
+ `opencode plugin update akm-opencode-v2` (with the spec your config names)
416
+ even while OpenCode runs; the running service keeps the old build until it
417
+ restarts. (`opencode plugin list` and `check` print "No plugins found" until
418
+ the service has loaded plugins after a restart, and `plugin update` leaves the
419
+ old build folders behind.) If your config pins another spec, such as `akm-opencode@0.9.30`, `akm upgrade`
416
420
  leaves it alone: change the spec, quit OpenCode, move that cache folder aside
417
421
  and start OpenCode again.
418
422
  - **OpenCode 2 permission defaults.** akm rejects every OpenCode permission
@@ -430,7 +434,12 @@ run. For OpenCode 2, run `opencode plugin add akm-opencode-v2`.
430
434
  driven as OpenCode 2 with a warning.
431
435
  - **Claude Code and Codex plugins** come from the `akm-plugins` marketplace and
432
436
  are updated by `akm upgrade`. Use the 0.10 plugin set with 0.10 (it passes
433
- `--detail agent`; see itlackey/akm-plugins#133).
437
+ `--detail agent`; see itlackey/akm-plugins#133); the plugins require akm
438
+ 0.10.261001 or newer. The marketplace's default branch is stable and its
439
+ `next` branch carries prereleases: add it as `itlackey/akm-plugins@next` in
440
+ Claude Code, or `codex plugin marketplace add itlackey/akm-plugins --ref next`
441
+ in Codex. `akm upgrade` updates whichever ref the marketplace follows and
442
+ cannot switch it.
434
443
  - **Other callers.** Anything of yours that passes `--shape agent` (benchmark
435
444
  runners, hooks) needs `--detail agent` and akm 0.10 together. akm-eval's
436
445
  retrieval, skillret and longmemeval benchmarks are the known ones
@@ -1341,7 +1341,7 @@ stopping the CLI upgrade.
1341
1341
  | Claude Code | `claude` is on `PATH`, the `akm-plugins` marketplace is configured and `akm@akm-plugins` is installed | `claude plugin marketplace update akm-plugins`, then `claude plugin update akm@akm-plugins` |
1342
1342
  | Codex | `codex` is on `PATH` and `akm@akm-plugins` is installed from the `akm-plugins` marketplace | `codex plugin marketplace upgrade akm-plugins` (this also refreshes the installed plugin cache) |
1343
1343
  | OpenCode 1 | `akm-opencode` is cached at `$XDG_CACHE_HOME/opencode/packages/akm-opencode@latest` (default `~/.cache/opencode/...`) and `opencode` is on `PATH` | If npm's `akm-opencode@latest` differs from the cached version and no OpenCode process is running: the cache directory is moved to the trash (never deleted), then `opencode debug config` is run from a temporary directory to fetch the new version. While OpenCode runs the update is `deferred` to a later `akm upgrade`. |
1344
- | OpenCode 2 | `akm-opencode-v2` is named under `"plugins"` in the global OpenCode config (`opencode.json` or `.jsonc`) and cached at `$XDG_CACHE_HOME/opencode/npm/akm-opencode-v2@latest/<build>/`, and the `opencode` on `PATH` is OpenCode 2 | Same rule as OpenCode 1, with `akm-opencode-v2`: the cache folder is moved to the trash, then `opencode plugin add akm-opencode-v2` is run from a temporary directory (for a spec the config already names it only re-installs the cache; it does not touch the config). Listed only on a host that has the plugin, so an OpenCode 1 host reports exactly what it did before. |
1344
+ | OpenCode 2 | `akm-opencode-v2` is named under `"plugins"` in the global OpenCode config (`opencode.json` or `.jsonc`) and cached at `$XDG_CACHE_HOME/opencode/npm/akm-opencode-v2@latest/<build>/`, and the `opencode` on `PATH` is OpenCode 2 | If npm's `akm-opencode-v2@latest` differs from the cached version (the newest `<build>` folder): `opencode plugin update <spec>` is run from a temporary directory, with the spec as the config names it. OpenCode 2 installs the new build into a fresh `<build>` folder and the running service keeps the old one until it restarts, so nothing is moved to the trash and the update is not deferred while OpenCode runs. Listed only on a host that has the plugin, so an OpenCode 1 host reports exactly what it did before. |
1345
1345
 
1346
1346
  The two OpenCode plugins never touch each other: `akm-opencode` is only looked
1347
1347
  for in OpenCode 1's cache and `plugin` key, `akm-opencode-v2` only in OpenCode
@@ -1349,11 +1349,17 @@ for in OpenCode 1's cache and `plugin` key, `akm-opencode-v2` only in OpenCode
1349
1349
  --version` and skips the plugin when the `opencode` on `PATH` is the other
1350
1350
  major, since only the matching major can re-create that cache; akm never
1351
1351
  installs or replaces `opencode`. A spec that appears only under the legacy
1352
- `plugin` key for OpenCode 2 is skipped with a note (`opencode plugin add`
1353
- would write a `plugins` entry). Which OpenCode major akm itself drives is
1352
+ `plugin` key for OpenCode 2 is skipped with a note (move it to `plugins`). Which OpenCode major akm itself drives is
1354
1353
  [detected from the engine's binary](configuration.md#engines), independent of this
1355
1354
  step.
1356
1355
 
1356
+ OpenCode 2 has its own updater, `opencode plugin check` and `opencode plugin update`,
1357
+ which akm uses for `akm-opencode-v2`. Two things to know when you run them by
1358
+ hand: `plugin list` and `plugin check` print "No plugins found" until the
1359
+ OpenCode service has loaded its plugins after a restart, and `plugin update`
1360
+ leaves the old `<build>` folders under the cache folder (the newest one is the
1361
+ one in use).
1362
+
1357
1363
  Claude Code does not update third-party marketplaces on its own (the Claude
1358
1364
  desktop app turns plugin updates off), and OpenCode never re-checks a cached
1359
1365
  plugin, so neither moves without this step. Codex refreshes its git
@@ -1414,9 +1420,8 @@ carries `channel: "next"` (`"latest"` otherwise); no other field changes.
1414
1420
  With that line in the global OpenCode config (`~/.config/opencode/opencode.json`
1415
1421
  or `.jsonc`, or the file named by `OPENCODE_CONFIG`), lockstep and the cache
1416
1422
  refresh use `akm-opencode@next` (its version and its `akm-cli` pin) instead of
1417
- `@latest`. If `akm-opencode@next` is missing, pins an older `akm-cli` than
1418
- `@latest` does, or its `akm-cli` pin is unreadable, the CLI is held where it is and the OpenCode entry
1419
- is `failed`, exactly as in the stable lockstep above.
1423
+ `@latest`. If `akm-opencode@next` is missing or its `akm-cli` pin is unreadable,
1424
+ the CLI is held where it is and the OpenCode entry is `failed`, exactly as in the stable lockstep above.
1420
1425
 
1421
1426
  OpenCode 2's `akm-opencode-v2` follows the same rule with its own spec:
1422
1427
  `{ "plugins": ["akm-opencode-v2@next"] }`, cached under
@@ -1425,9 +1430,15 @@ carries `channel: "next"` (`"latest"` otherwise); no other field changes.
1425
1430
  Without that line the OpenCode entry is `skipped`, its message names the line
1426
1431
  to add, and lockstep stays against the `@latest` pin, so the CLI does not go
1427
1432
  past what OpenCode will run.
1428
- - **Claude Code and Codex.** Unchanged: their plugin comes from the `akm-plugins`
1429
- git marketplace, which has no prerelease channel and accepts any 0.9.x akm,
1430
- prereleases included. Their entries carry a note saying so.
1433
+ - **Claude Code and Codex.** Their plugin comes from the `akm-plugins` git
1434
+ marketplace, and the channel is the ref the marketplace follows: the default
1435
+ branch is stable, the `next` branch carries prereleases. `akm upgrade` updates
1436
+ whatever ref the marketplace follows and cannot switch it, with or without
1437
+ `--next`. To follow prereleases, add the marketplace as `itlackey/akm-plugins@next`
1438
+ in Claude Code (`extraKnownMarketplaces.akm-plugins.source.ref = "next"` in
1439
+ `settings.json`), or run `codex plugin marketplace add itlackey/akm-plugins --ref next`
1440
+ in Codex. Under `--next` their entries carry a note naming the ref followed.
1441
+ The plugins need akm 0.10.261001 or newer; any newer akm passes.
1431
1442
  - `--check --next` reports all of this and changes nothing.
1432
1443
 
1433
1444
  #### Containers
@@ -79,6 +79,12 @@ mode and owner, and the uid you are running as. They do **not** report an empty
79
79
  index. `akm health` stays runnable in that state and reports it as a failing
80
80
  `state-db-readable` check, so it remains the command to reach for.
81
81
 
82
+ If akm can read this directory but **cannot write** it — an agent sandbox that
83
+ only allows writes inside the workspace, for instance — `akm feedback` fails with
84
+ a `DATA_DIR_NOT_WRITABLE` config error (exit 78) and records nothing, instead of
85
+ reporting success for feedback it could not store. Add the data directory to the
86
+ sandbox's writable paths.
87
+
82
88
  > **0.9.1 note.** A pre-release build briefly chmodded this directory to `0700`
83
89
  > and the databases to `0600` on every open. That was reverted: it silently
84
90
  > changed the permissions of directories akm did not create, which broke installs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.10.261002-beta",
3
+ "version": "0.10.261004-beta",
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": [