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 +28 -0
- package/dist/commands/feedback-cli.js +19 -11
- package/dist/commands/sources/plugin-upgrade.js +77 -72
- package/dist/core/errors.js +1 -0
- package/dist/indexer/usage/usage-events.js +6 -2
- package/dist/integrations/harnesses/opencode/plugin-layout.js +6 -3
- package/dist/scripts/akm-migrate-node.js +1 -0
- package/dist/scripts/akm-migrate.js +1 -0
- package/docs/migration/release-notes/0.10.0.md +4 -2
- package/docs/migration/v0.9-to-v0.10.md +16 -7
- package/docs/reference/cli.md +20 -9
- package/docs/reference/data-and-telemetry.md +6 -0
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
: "
|
|
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
|
|
354
|
-
//
|
|
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
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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,
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
|
401
|
-
opts.next
|
|
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 ?
|
|
508
|
+
const latest = cache ? lookupOpenCodeLatest(tag, flavor) : undefined;
|
|
504
509
|
return { flavor, cache, latest, tag, spec, notFollowingNext: next && !followNext };
|
|
505
510
|
}
|
|
506
511
|
/**
|
package/dist/core/errors.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
// `
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
411
|
-
plugin build is not picked up until its cache folder is cleared. `akm
|
|
412
|
-
does this for `@latest` (and `@next` with `--next`) when no OpenCode
|
|
413
|
-
is running: it moves the folder to the trash, then re-fetches
|
|
414
|
-
config`
|
|
415
|
-
|
|
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
|
package/docs/reference/cli.md
CHANGED
|
@@ -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 |
|
|
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 (`
|
|
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
|
|
1418
|
-
|
|
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.**
|
|
1429
|
-
|
|
1430
|
-
|
|
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.
|
|
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": [
|