akm-cli 0.10.261005-beta → 0.10.261007-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.
Files changed (55) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/STABILITY.md +9 -0
  3. package/dist/assets/ui/app.js +91 -0
  4. package/dist/assets/ui/asset.js +67 -0
  5. package/dist/assets/ui/bundles.js +80 -0
  6. package/dist/assets/ui/changes.js +32 -0
  7. package/dist/assets/ui/charts.js +93 -0
  8. package/dist/assets/ui/config.js +69 -0
  9. package/dist/assets/ui/diff-view.js +202 -0
  10. package/dist/assets/ui/diff.js +261 -0
  11. package/dist/assets/ui/index.html +30 -0
  12. package/dist/assets/ui/lib.js +238 -0
  13. package/dist/assets/ui/markdown.js +103 -0
  14. package/dist/assets/ui/overview.js +145 -0
  15. package/dist/assets/ui/proposal-actions.js +273 -0
  16. package/dist/assets/ui/proposals.js +219 -0
  17. package/dist/assets/ui/search.js +62 -0
  18. package/dist/assets/ui/styles.css +1415 -0
  19. package/dist/cli.js +3 -0
  20. package/dist/commands/health/checks.js +5 -3
  21. package/dist/commands/health/egress.js +5 -1
  22. package/dist/commands/health/engine-usage.js +2 -2
  23. package/dist/commands/health/llm-usage.js +2 -2
  24. package/dist/commands/health/windows.js +5 -5
  25. package/dist/commands/health.js +40 -8
  26. package/dist/commands/improve/reflect-noise.js +4 -0
  27. package/dist/commands/improve/reflect.js +14 -5
  28. package/dist/commands/metrics/metrics-cli.js +3 -3
  29. package/dist/commands/proposal/propose-cli.js +5 -0
  30. package/dist/commands/proposal/propose.js +6 -2
  31. package/dist/commands/proposal/repository.js +34 -29
  32. package/dist/commands/read/search.js +2 -1
  33. package/dist/commands/read/show.js +18 -12
  34. package/dist/commands/sources/source-clone.js +123 -11
  35. package/dist/commands/ui/api.js +556 -0
  36. package/dist/commands/ui/git-read.js +101 -0
  37. package/dist/commands/ui/open-browser.js +30 -0
  38. package/dist/commands/ui/proposal-actions.js +179 -0
  39. package/dist/commands/ui/redact.js +88 -0
  40. package/dist/commands/ui/server.js +352 -0
  41. package/dist/commands/ui/ui-cli.js +62 -0
  42. package/dist/core/asset/resolve-ref.js +7 -3
  43. package/dist/indexer/indexer.js +14 -2
  44. package/dist/indexer/search/db-search.js +7 -3
  45. package/dist/output/format-exempt.js +3 -0
  46. package/dist/storage/repositories/improve-runs-repository.js +6 -0
  47. package/dist/storage/repositories/index-db.js +10 -2
  48. package/dist/storage/repositories/metrics-repository.js +17 -0
  49. package/docs/architecture/0.10-process-rules.md +3 -4
  50. package/docs/architecture/improvement.md +2 -1
  51. package/docs/architecture/internals/improve-workflow.md +2 -2
  52. package/docs/reference/README.md +1 -0
  53. package/docs/reference/cli.md +68 -0
  54. package/docs/reference/ui-api.md +576 -0
  55. package/package.json +1 -1
@@ -0,0 +1,576 @@
1
+ # `akm ui` API
2
+
3
+ The JSON API behind [`akm ui`](cli.md#ui). The page that `akm ui` serves uses only this API, and everything it
4
+ returns comes from the function the matching CLI command renders, so it never disagrees with the CLI. The API is
5
+ read-only apart from three proposal decisions: [accept](#post-apiproposalsidaccept), [reject](#post-apiproposalsidreject)
6
+ and [accept several](#post-apiproposalsaccept).
7
+
8
+ **Experimental** (see [STABILITY.md](../../STABILITY.md)). The shapes below are what 0.10 ships; they may change in any
9
+ release, and nothing outside the page should script against them. Use the CLI's `--format json` for that.
10
+
11
+ ## Conventions
12
+
13
+ - Base URL: the one `akm ui` prints, `http://<host>:<port>/`. Every path below is under it.
14
+ - **Token.** Every `/api/` request must carry the launch token, as the `X-Akm-Ui-Token` header or a `token` query
15
+ parameter. A missing or wrong token is a `401`. The page receives the token in its URL (`/?token=...`), keeps it in
16
+ `sessionStorage`, drops it from the address bar and sends the header. Static files (`/`, `/app.js`, `/styles.css`) are
17
+ served without a token and embed no data.
18
+ - **Host.** The `Host` header must be the address the server is bound to (`127.0.0.1:<port>` by default); anything else is
19
+ a `403`. A server bound to a wildcard address (`--host 0.0.0.0`) also accepts IP-literal hosts on its port.
20
+ - **Methods.** `GET` (and `HEAD`) only, except the three `POST` routes under
21
+ [Deciding proposals](#deciding-proposals-the-only-writes). Anything else is a `405` (after the token check).
22
+ - **Envelope.** Every response is JSON, `Cache-Control: no-store`:
23
+
24
+ ```json
25
+ { "ok": true, "data": { } }
26
+ { "ok": false, "error": "Missing or invalid session token.", "code": "UNAUTHORIZED" }
27
+ ```
28
+
29
+ Errors raised by akm keep their stable `code` (`PROPOSAL_NOT_FOUND`, `ASSET_NOT_FOUND`, `INVALID_FLAG_VALUE`, ...).
30
+ Status codes: `400` usage error (bad parameter), `401` token, `403` host or origin, `404` not found, `405` method,
31
+ `409` conflict (writes only), `413` body too large, `415` content type, `503` transient (another akm process holds a
32
+ lock; retry), `500` anything else. Codes the server itself raises: `UNAUTHORIZED`, `HOST_NOT_ALLOWED`,
33
+ `ORIGIN_NOT_ALLOWED`, `METHOD_NOT_ALLOWED`, `UNSUPPORTED_MEDIA_TYPE`, `BODY_TOO_LARGE`, `INVALID_JSON_ARGUMENT`,
34
+ `NOT_FOUND`, `INTERNAL_ERROR`.
35
+ - **Redaction.** Config values under secret-like keys (`apiKey`, `token`, `password`, `secret`, `credential`,
36
+ `authorization`, anything under `env` or `secrets`) are `"[redacted]"`; symbolic references (`$VAR`, `secret://name`)
37
+ are kept. Credentials inside any URL in any response (userinfo, `token=`, `api_key=`, `sig=`, ...) are masked. Env and
38
+ secret assets are never read; see `/api/asset`.
39
+ - **Timestamps** are ISO 8601 UTC unless noted. Counts that cannot be computed are `null`, never `NaN`.
40
+ - **Read-only, except the three proposal writes.** Every `GET` opens `state.db` and `index.db` read-only and never
41
+ creates or migrates them: a missing or behind-on-migrations `state.db` makes the sections that need it empty (and
42
+ `health` report it as a failing check), not an error. Searching and showing record no usage events and build no index.
43
+ Browsing writes nothing to a bundle, config or database.
44
+
45
+ The limit parameters (`limit`) are positive integers capped at 200.
46
+
47
+ ## `GET /api/meta`
48
+
49
+ The running server and where it reads from.
50
+
51
+ ```json
52
+ {
53
+ "ok": true,
54
+ "data": {
55
+ "version": "0.10.261005",
56
+ "runtime": "bun",
57
+ "stashDir": "/home/me/akm",
58
+ "dataDir": "/home/me/.local/share/akm",
59
+ "configPath": "/home/me/.config/akm/config.json",
60
+ "stateDbPath": "/home/me/.local/share/akm/state.db",
61
+ "indexDbPath": "/home/me/.local/share/akm/index.db",
62
+ "startedAt": "2026-10-16T15:41:15.716Z",
63
+ "host": "127.0.0.1",
64
+ "port": 39829
65
+ }
66
+ }
67
+ ```
68
+
69
+ `runtime` is `bun` or `node`. `stashDir` is `null` when no bundle is configured yet.
70
+
71
+ ## `GET /api/overview`
72
+
73
+ Everything on the overview page in one call. Query parameters:
74
+
75
+ | Parameter | Default | Meaning |
76
+ | --- | --- | --- |
77
+ | `since` | `24h` | Health window start (ISO, date, epoch ms, or `24h` / `7d`). A duration also turns on the prior-window comparison, as `akm health --report --since` does. |
78
+ | `metricsSince` | `30d` | Metrics window start, as `akm metrics --since`. |
79
+ | `probe` | off | `1` probes engine reachability and checks for a newer akm release (network). Off by default so loading the page makes no outbound calls. |
80
+
81
+ ```json
82
+ {
83
+ "ok": true,
84
+ "data": {
85
+ "health": {
86
+ "schemaVersion": 3,
87
+ "ok": true,
88
+ "status": "pass",
89
+ "since": "2026-10-15T15:41:20.499Z",
90
+ "hardChecks": [{ "name": "state-db-schema", "kind": "deterministic", "status": "pass", "confidence": "high", "message": "...", "evidence": {} }],
91
+ "advisories": [],
92
+ "metrics": { "taskFailRate": 0, "agentFailureRate": 0, "stuckActiveRuns": 0, "llmUsage": {} },
93
+ "improve": {},
94
+ "runs": [],
95
+ "windows": [{ "name": "current", "since": "...", "until": "...", "runs": 1, "improve": {}, "metrics": {} }, { "name": "prior" }],
96
+ "deltas": {},
97
+ "report": {
98
+ "window": "24h",
99
+ "compare": "24h",
100
+ "pendingProposals": [{ "ref": "stash//lessons/rg", "source": "reflect", "createdAt": "..." }],
101
+ "acceptRateBySource": []
102
+ }
103
+ },
104
+ "metrics": { "schemaVersion": 1, "window": {}, "usage": {}, "feedback": {}, "utility": {}, "llm": {}, "proposals": {}, "notes": [] },
105
+ "improveRuns": [
106
+ {
107
+ "id": "run-1",
108
+ "startedAt": "2026-10-16T15:42:09.487Z",
109
+ "completedAt": "2026-10-16T15:43:09.487Z",
110
+ "ok": true,
111
+ "durationMs": 60000,
112
+ "strategy": "default",
113
+ "scope": { "mode": "all" },
114
+ "modelCalls": {
115
+ "total": 5,
116
+ "failures": 1,
117
+ "byProcess": { "reflect": { "calls": 3, "failures": 1 }, "triage.judgment": { "calls": 2, "failures": 0 } }
118
+ },
119
+ "preRunSync": { "committed": false, "pushed": false, "skipped": true, "reason": "no remote" },
120
+ "sync": { "committed": true, "pushed": false, "skipped": false },
121
+ "triage": { "promoted": 2, "rejected": 1, "deferred": 0, "failed": 0, "skippedByCap": 0 }
122
+ }
123
+ ],
124
+ "usageByDay": [
125
+ { "date": "2026-10-15", "search": 0, "curate": 0, "show": 0, "feedbackPositive": 0, "feedbackNegative": 0 },
126
+ { "date": "2026-10-16", "search": 1, "curate": 0, "show": 1, "feedbackPositive": 1, "feedbackNegative": 1 }
127
+ ]
128
+ }
129
+ }
130
+ ```
131
+
132
+ - `health` is what `akm health --report --group-by run` returns (`--no-probe` unless `probe=1`), minus the write: the
133
+ `state-db-round-trip` check reports itself skipped. See [CLI: health](cli.md#health) for the check list and
134
+ [`--format html`](cli.md#health) for how the report renders it. `windows`, `deltas` and `report.compare` are absent
135
+ when `since` is an absolute date.
136
+ - `metrics` is what `akm metrics` returns (without `rows`); see [CLI: metrics](cli.md#metrics).
137
+ - `improveRuns` are the last 10 real (non-dry-run) improve runs, newest first. `preRunSync`, `sync` and `triage` are `null`
138
+ when the run did not record them; `modelCalls` is empty for runs from before the usage report existed.
139
+ - `usageByDay` is the last 14 UTC days, oldest first, user-attributed events only (the same rule as ranking and
140
+ `akm metrics`). Zero-filled.
141
+ - A section that fails is `null` (`[]` for the two lists) and named in a top-level `errors` object
142
+ (`{ "errors": { "metrics": "..." } }`); the others still load. `errors` is absent when nothing failed.
143
+
144
+ ## `GET /api/bundles`
145
+
146
+ One card per configured bundle, plus the index state. Git status is read from the repository as it is: nothing is
147
+ fetched, so `ahead`/`behind` are measured against the remote-tracking branch as the last `akm sync` left it.
148
+
149
+ ```json
150
+ {
151
+ "ok": true,
152
+ "data": {
153
+ "defaultBundle": "stash",
154
+ "defaultWriteTarget": null,
155
+ "bundles": [
156
+ {
157
+ "name": "stash",
158
+ "kind": "filesystem",
159
+ "default": true,
160
+ "source": { "kind": "path", "locator": "/home/me/akm" },
161
+ "path": "/home/me/akm",
162
+ "writable": true,
163
+ "version": null,
164
+ "exists": true,
165
+ "itemCount": 14,
166
+ "byType": { "fact": 12, "knowledge": 2 },
167
+ "git": {
168
+ "branch": "main",
169
+ "upstream": "origin/main",
170
+ "ahead": 1,
171
+ "behind": 0,
172
+ "dirtyPaths": 0,
173
+ "lastCommit": { "sha": "7591a50789d4cddc", "time": "2026-10-16T10:46:41-05:00", "subject": "akm save 2026-10-16 10:00:00" }
174
+ }
175
+ }
176
+ ],
177
+ "index": {
178
+ "entryCount": 14,
179
+ "byType": { "fact": 12, "knowledge": 2 },
180
+ "lastBuiltAt": "2026-10-16T15:34:00.512Z",
181
+ "hasEmbeddings": false,
182
+ "embeddingCount": 0,
183
+ "embeddingCoverage": 0,
184
+ "semanticSearch": { "mode": "off", "status": "disabled" }
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ - `kind` is `filesystem`, `git`, `npm` or `website`. `source` describes where the bundle comes from; `path` is its local
191
+ root (`null` if it has none).
192
+ - `git` is `null` unless the bundle's root is itself a git repository. `upstream`, `ahead` and `behind` are `null` without an
193
+ upstream; `branch` is `null` on a detached HEAD; `lastCommit` is `null` for a repository with no commits. `dirtyPaths`
194
+ counts paths with uncommitted changes.
195
+ - `index` mirrors `akm info`'s `indexStats` and `semanticSearch`. `embeddingCoverage` is embeddings over entries
196
+ (`null` for an empty or missing index). `links`, `unavailable` and `unreadable` appear only when `akm info` reports them.
197
+
198
+ ## `GET /api/config`
199
+
200
+ The effective configuration, as `akm config list` shows it, with where each value comes from.
201
+
202
+ ```json
203
+ {
204
+ "ok": true,
205
+ "data": {
206
+ "path": "/home/me/.config/akm/config.json",
207
+ "exists": true,
208
+ "config": {
209
+ "semanticSearchMode": "off",
210
+ "engines": { "default": { "kind": "llm", "endpoint": "https://llm.example.test/v1/chat/completions?token=%5Bredacted%5D", "model": "chat-model" } },
211
+ "defaultBundle": "stash"
212
+ },
213
+ "entries": [
214
+ { "path": "semanticSearchMode", "value": "off", "source": "file" },
215
+ { "path": "output.format", "value": "json", "source": "default" }
216
+ ],
217
+ "engines": [{ "name": "default", "kind": "llm", "endpoint": "https://llm.example.test/v1/chat/completions?token=%5Bredacted%5D", "model": "chat-model" }],
218
+ "env": [{ "name": "AKM_BUNDLE_DIR", "value": "/home/me/akm" }]
219
+ }
220
+ }
221
+ ```
222
+
223
+ - `config` is the redacted effective config tree. `entries` is the same data flattened to one row per leaf (arrays are
224
+ leaves) with `source`: `default` (no config file sets it), `file` (this install's `config.json`),
225
+ `extends:<ref>` (a config it extends), or `null` when the key cannot be traced (a key containing a `.`).
226
+ - `engines` is `config.engines` as a list. Reachability is a health probe, not config: use `/api/overview?probe=1`.
227
+ - `env` lists the `AKM_*` environment variables set for the server, which override paths and credentials. A
228
+ secret-like name (`...KEY`, `...TOKEN`, ...) has `value: "[redacted]"`.
229
+
230
+ ## `GET /api/proposals`
231
+
232
+ The proposal queue, as `akm proposal list` reads it. Query: `status=pending` (default) or `recent`, `limit` (default 100).
233
+
234
+ - `pending`: the pending queue, oldest-waiting first.
235
+ - `recent`: accepted, rejected and reverted proposals, most recently decided first.
236
+
237
+ ```json
238
+ {
239
+ "ok": true,
240
+ "data": {
241
+ "status": "pending",
242
+ "totalCount": 3,
243
+ "bySource": { "consolidate-pair": 2, "reflect": 1 },
244
+ "proposals": [
245
+ {
246
+ "id": "d6bb7f94-69aa-48cf-9084-5dba2fa1afb3",
247
+ "source": "consolidate-pair",
248
+ "ref": "stash//knowledge/old-note",
249
+ "status": "pending",
250
+ "createdAt": "2026-10-16T15:46:41.896Z",
251
+ "updatedAt": "2026-10-16T15:46:41.896Z",
252
+ "waitingSince": "2026-10-16T15:46:41.896Z",
253
+ "confidence": null,
254
+ "gateDecision": { "outcome": "deferred", "reason": "judgment-deferred", "decidedAt": "..." },
255
+ "review": null,
256
+ "isRetire": true,
257
+ "retirement": {
258
+ "retiredRef": "stash//knowledge/old-note",
259
+ "successorRef": "stash//knowledge/new-note",
260
+ "reason": "duplicate",
261
+ "judgeLabel": "duplicate",
262
+ "judgeReason": "Same procedure, newer wording.",
263
+ "cosine": 0.93,
264
+ "onlyInRetired": [],
265
+ "onlyInSuccessor": ["Adds the rollback step."],
266
+ "continuityRisk": null
267
+ }
268
+ }
269
+ ]
270
+ }
271
+ }
272
+ ```
273
+
274
+ - `totalCount` and `bySource` count the whole selection; `proposals` holds the first `limit`.
275
+ - `waitingSince` is the last reopen, else creation: the age to show. `gateDecision` is the drain's or quality gate's
276
+ verdict (`outcome`: `staged`, `auto-rejected`, `deferred`, ...; `reason` is a stable token). `review` is
277
+ `{ outcome, reason?, decidedAt }` on a decided proposal.
278
+ - `retirement` is `null` unless the proposal retires an asset (`isRetire`). `continuityRisk` is
279
+ `{ failingQueries, unverifiedQueries?, ranks }` when the continuity check flagged the pair, else `null`.
280
+ - Refs are durable `bundle//conceptId` refs.
281
+
282
+ ## `GET /api/proposals/:id`
283
+
284
+ One proposal, as `akm proposal show` plus `akm proposal diff`. `:id` is a proposal id, or an unambiguous prefix of a
285
+ pending one, or an asset ref (URL-encode it). Unknown: `404` `PROPOSAL_NOT_FOUND`.
286
+
287
+ ```json
288
+ {
289
+ "ok": true,
290
+ "data": {
291
+ "proposal": {
292
+ "id": "d6bb7f94-69aa-48cf-9084-5dba2fa1afb3",
293
+ "source": "reflect",
294
+ "ref": "stash//lessons/rg",
295
+ "status": "pending",
296
+ "createdAt": "2026-10-16T15:46:41.896Z",
297
+ "updatedAt": "2026-10-16T15:46:41.896Z",
298
+ "waitingSince": "2026-10-16T15:46:41.896Z",
299
+ "confidence": null,
300
+ "gateDecision": null,
301
+ "review": null,
302
+ "isRetire": false,
303
+ "retirement": null,
304
+ "sourceRun": "run-1",
305
+ "proposedTarget": { "source": "stash", "root": "/home/me/akm" },
306
+ "payload": { "frontmatter": { "description": "Use ripgrep" } },
307
+ "changes": [{ "op": "create", "path": "lessons/rg.md" }]
308
+ },
309
+ "validation": {
310
+ "ok": true,
311
+ "findings": [{ "kind": "invalid-description", "message": "...", "severity": "warn" }]
312
+ },
313
+ "diff": {
314
+ "unified": "--- /dev/null\n+++ stash//lessons/rg (proposed, new asset)\n@@ 0,0 1,9 @@\n+---\n...",
315
+ "isNew": true,
316
+ "targetPath": "/home/me/akm/lessons/rg.md",
317
+ "before": null,
318
+ "after": "---\ndescription: Use ripgrep\n---\n\nPrefer rg over grep.\n",
319
+ "truncated": false,
320
+ "redacted": false
321
+ },
322
+ "diffError": null
323
+ }
324
+ }
325
+ ```
326
+
327
+ - `proposal` carries the full metadata (everything `akm proposal show` prints) except content: the proposed text is in
328
+ `diff.unified` and `diff.after`, `payload.content` is omitted, and `changes` lists `{ op, path }` only. The prior content kept for
329
+ `akm proposal revert` is never returned. The summary fields from the list are repeated, and `retirement` is the full
330
+ record (with content hashes) on a retire proposal.
331
+ - `diff.before` and `diff.after` are the two texts the unified diff compares, read the same way (same target
332
+ resolution as accept): the asset as it is now (`null` when the proposal creates it) and the content accept would
333
+ write. The page lines them up itself, because the unified text pairs lines by position and so shows one inserted
334
+ line as a rewrite of everything after it. Each is cut at 512 K characters; when either was, `truncated` is `true` and
335
+ the page shows `unified` instead. For a retire proposal both are `null` (the file leaves; nothing is proposed). For an
336
+ env or secret target both are `null`, `redacted` is `true` and `unified` is a one-line notice: their content is never
337
+ returned.
338
+ - `diff` is `null` and `diffError` says why when the target cannot be resolved (for example the bundle is no longer
339
+ configured). For a retire proposal `unified` shows the file leaving and `retirement` has the successor.
340
+ - `validation` is the proposal validator's result; it does not run the accept preflight `akm proposal show` adds.
341
+
342
+ ## Deciding proposals (the only writes)
343
+
344
+ Three `POST` routes accept or reject proposals. Each calls the function the CLI command calls (`akmProposalAccept`,
345
+ `akmProposalReject`, `akmProposalAcceptMany`) with the arguments the CLI passes, so a decision made here is recorded
346
+ exactly like `akm proposal accept` / `reject`: the same state.db rows and events, the same asset write and the same git
347
+ commit. No other path accepts anything but `GET`.
348
+
349
+ A write is refused unless **all** of these hold, checked in this order before the body is read:
350
+
351
+ | Requirement | Otherwise |
352
+ | --- | --- |
353
+ | The launch token in the `X-Akm-Ui-Token` **header**. A `?token=` query parameter does not count for writes. | `401 UNAUTHORIZED` |
354
+ | An `Origin` header equal to the server's own origin, `http://<host>:<port>` (the validated `Host`). A missing, `null`, other-host, other-port or `https` origin is refused, so another web page cannot make the browser send one. | `403 ORIGIN_NOT_ALLOWED` |
355
+ | `Content-Type: application/json` (a `charset` parameter is fine). | `415 UNSUPPORTED_MEDIA_TYPE` |
356
+ | A body of at most 65536 bytes (64 KB). | `413 BODY_TOO_LARGE` |
357
+ | A body that is valid JSON. | `400 INVALID_JSON_ARGUMENT` |
358
+
359
+ The `Host` guard applies as for every request (`403 HOST_NOT_ALLOWED`). The `X-Akm-Ui-Token` and `Origin` requirements are
360
+ the reason the page's own `fetch` works and a cross-site form post or script does not.
361
+
362
+ Failures from the decision itself keep the usual envelope. Codes and statuses:
363
+
364
+ | Status | `code` | When |
365
+ | --- | --- | --- |
366
+ | `400` | `MISSING_REQUIRED_ARGUMENT`, `INVALID_JSON_ARGUMENT`, `INVALID_FLAG_VALUE`, `INVALID_PROPOSAL`, ... | Bad input: no or empty reason, an `ids` list outside 1 to 200, a non-object body, a proposal that fails validation. |
367
+ | `404` | `PROPOSAL_NOT_FOUND` | No proposal has that id (or an ambiguous or unknown prefix). |
368
+ | `409` | `PROPOSAL_STALE` | The asset changed (or was created) after the proposal was made, so accepting would overwrite newer content. Nothing is written. |
369
+ | `409` | `PROPOSAL_NOT_PENDING` | The proposal was already accepted, rejected or reverted. |
370
+ | `409` | `ASSET_MUTATION_LEASE_HELD` | Another akm process (an `improve` run, the CLI) or request holds the write lease. Retry in a moment; the request does not wait. |
371
+ | `500` | `INTERNAL_ERROR`, other akm codes | Anything else. |
372
+
373
+ ### `POST /api/proposals/:id/accept`
374
+
375
+ Accept one pending proposal. `:id` is a proposal id or unambiguous prefix. The body is `{}` (or empty).
376
+
377
+ ```json
378
+ {
379
+ "ok": true,
380
+ "data": {
381
+ "schemaVersion": 1,
382
+ "ok": true,
383
+ "id": "d6bb7f94-69aa-48cf-9084-5dba2fa1afb3",
384
+ "ref": "stash//lessons/rg",
385
+ "assetPath": "/home/me/akm/lessons/rg.md",
386
+ "proposal": { "id": "d6bb7f94-...", "status": "accepted", "ref": "stash//lessons/rg", "...": "summary fields as in GET /api/proposals" },
387
+ "commit": { "sha": "9f1c...", "time": "2026-10-16T16:02:11-05:00", "subject": "akm accept: reflect d6bb7f94 stash//lessons/rg", "repo": "/home/me/akm" }
388
+ }
389
+ }
390
+ ```
391
+
392
+ - `data` is `akmProposalAccept`'s result, with `proposal` reduced to the list summary (the prior content kept for
393
+ `akm proposal revert` is never returned) and `commit` added.
394
+ - `commit` is the commit the accept just made in the bundle's repository, or `null` when the bundle is not a git
395
+ repository (or the commit did not happen; a failed commit only warns, as in the CLI).
396
+
397
+ ### `POST /api/proposals/:id/reject`
398
+
399
+ Reject one pending proposal. The body is `{"reason": "..."}`: required, a string, trimmed, 1 to 2000 characters, else
400
+ `400 MISSING_REQUIRED_ARGUMENT` (or `INVALID_FLAG_VALUE` when too long). The asset and the repository are untouched.
401
+
402
+ ```json
403
+ {
404
+ "ok": true,
405
+ "data": {
406
+ "schemaVersion": 1,
407
+ "ok": true,
408
+ "id": "d6bb7f94-69aa-48cf-9084-5dba2fa1afb3",
409
+ "ref": "stash//lessons/rg",
410
+ "reason": "Duplicates lessons/ripgrep.",
411
+ "proposal": { "id": "d6bb7f94-...", "status": "rejected", "review": { "outcome": "rejected", "reason": "Duplicates lessons/ripgrep." } }
412
+ }
413
+ }
414
+ ```
415
+
416
+ A rejection can be undone with `akm proposal reopen`.
417
+
418
+ ### `POST /api/proposals/accept`
419
+
420
+ Accept several pending proposals and commit them **once** (`akmProposalAcceptMany`, the call behind
421
+ `akm proposal accept <id> <id> ...`). The body is `{"ids": ["<id>", ...]}`: 1 to 200 non-empty strings; repeats are
422
+ dropped. An id that fails is reported and the rest carry on; what was accepted is committed either way. There is no
423
+ batch reject.
424
+
425
+ ```json
426
+ {
427
+ "ok": true,
428
+ "data": {
429
+ "accepted": 3,
430
+ "failed": 1,
431
+ "results": [
432
+ { "schemaVersion": 1, "ok": true, "id": "...", "ref": "stash//lessons/a", "assetPath": "/home/me/akm/lessons/a.md", "proposal": { "status": "accepted" } },
433
+ { "schemaVersion": 1, "ok": false, "id": "...", "error": "Proposal target changed after proposal ... was created; refusing to overwrite newer content.", "code": "PROPOSAL_STALE" }
434
+ ],
435
+ "commit": { "sha": "...", "time": "...", "subject": "akm proposal accept: 3 accepted (reflect 3)", "repo": "/home/me/akm" }
436
+ }
437
+ }
438
+ ```
439
+
440
+ - `results` follows the order of `ids`. A failed entry has `error` (akm's message) and a `code`: `PROPOSAL_STALE`,
441
+ `PROPOSAL_NOT_PENDING`, `PROPOSAL_NOT_FOUND`, `ASSET_MUTATION_LEASE_HELD`, or `ACCEPT_FAILED` for anything else. The
442
+ response is `200` even when some ids failed; a held lease fails the whole request with `409`.
443
+ - `commit` is the single commit for the batch (`null` when nothing was accepted or the bundle is not a git repository).
444
+
445
+ ## `GET /api/changes`
446
+
447
+ Recent commits of the write-target stash (`defaultWriteTarget`, else the default bundle), from its git log. Query:
448
+ `limit` (default 50).
449
+
450
+ ```json
451
+ {
452
+ "ok": true,
453
+ "data": {
454
+ "bundle": "stash",
455
+ "path": "/home/me/akm",
456
+ "git": true,
457
+ "commits": [
458
+ {
459
+ "sha": "7591a50789d4cddc89ab53ef9bdb2dfc9863444",
460
+ "shortSha": "7591a507",
461
+ "time": "2026-10-16T10:46:41-05:00",
462
+ "author": "akm-test",
463
+ "subject": "akm save 2026-10-16 10:00:00",
464
+ "origin": "sync",
465
+ "paths": ["knowledge/more.md"],
466
+ "pathCount": 1
467
+ }
468
+ ]
469
+ }
470
+ }
471
+ ```
472
+
473
+ - `git` is `false` and `commits` empty when the stash is not a git repository.
474
+ - `origin` is derived from the subject, which is how akm names its own commits:
475
+
476
+ | `origin` | Subject |
477
+ | --- | --- |
478
+ | `improve-sync` | `akm improve ...` (the pre-run sync and the end-of-run auto-sync) |
479
+ | `accept` | `akm accept: ...`, `akm proposal ...` (including `akm proposal drain: N accepted ...`) |
480
+ | `remember` | contains `remember` |
481
+ | `sync` | `akm save ...`, `akm sync ...` |
482
+ | `other` | anything else, such as a hand-made commit |
483
+
484
+ - `paths` lists at most 50 changed paths; `pathCount` is the real number. `time` is the commit's own committer date with
485
+ its offset.
486
+
487
+ ## `GET /api/search`
488
+
489
+ `akm search`, with the same results for the same inputs. Query:
490
+
491
+ | Parameter | Meaning |
492
+ | --- | --- |
493
+ | `q` | The query. Empty lists everything, as `akm search` with no query does; a trailing-slash prefix (`memories/projecta/`) enumerates a subtree. |
494
+ | `type` | Asset type filter (`knowledge`, `skill`, `memory`, ...), exact match. |
495
+ | `bundle` | Narrow to one configured bundle (`akm search --from <name>`). Unknown names are a `400`. |
496
+ | `limit` | Maximum hits. Default 20. |
497
+
498
+ `data` is the search function's own response (what `akm search --format json` shapes down for the terminal), unshaped:
499
+
500
+ ```json
501
+ {
502
+ "ok": true,
503
+ "data": {
504
+ "schemaVersion": 1,
505
+ "bundleDir": "/home/me/akm",
506
+ "source": "local",
507
+ "hits": [
508
+ {
509
+ "type": "knowledge",
510
+ "name": "deploy",
511
+ "path": "/home/me/akm/knowledge/deploy.md",
512
+ "ref": "knowledge/deploy",
513
+ "origin": "stash",
514
+ "editable": true,
515
+ "description": "Deploy guide",
516
+ "tags": ["deploy"],
517
+ "size": "small",
518
+ "action": "akm show knowledge/deploy -> read reference material",
519
+ "score": 0.016393,
520
+ "whyMatched": ["lexical rank 1"],
521
+ "estimatedTokens": 24,
522
+ "quality": "generated"
523
+ }
524
+ ],
525
+ "searchMode": "keyword",
526
+ "tip": "...",
527
+ "warnings": ["..."],
528
+ "timing": { "totalMs": 4, "rankMs": 1, "embedMs": 1 }
529
+ }
530
+ }
531
+ ```
532
+
533
+ `tip` and `warnings` appear when there is something to say (an empty index, a stale one). Pass a hit's `ref` to
534
+ `/api/asset`. Registry results are never included, and no search is counted as demand.
535
+
536
+ ## `GET /api/asset`
537
+
538
+ `akm show`, plus feedback. Query: `ref` (required), `[bundle//]conceptId[#fragment]` as `akm show` takes it.
539
+
540
+ ```json
541
+ {
542
+ "ok": true,
543
+ "data": {
544
+ "redacted": false,
545
+ "durableRef": "stash//knowledge/guide",
546
+ "asset": {
547
+ "type": "knowledge",
548
+ "name": "guide",
549
+ "path": "/home/me/akm/knowledge/guide.md",
550
+ "ref": "knowledge/guide",
551
+ "origin": "stash",
552
+ "editable": true,
553
+ "action": "Reference material - read the content below. ...",
554
+ "content": "---\ndescription: Deploy guide\n---\n# Deploy guide\n...",
555
+ "links": { "outgoing": { "xref": { "total": 1, "refs": ["knowledge/notes"] } } }
556
+ },
557
+ "feedback": {
558
+ "counts": { "positive": 1, "negative": 1 },
559
+ "history": [
560
+ { "at": "2026-10-16T15:46:41.000Z", "signal": "negative", "source": "user" },
561
+ { "at": "2026-10-16T15:46:41.000Z", "signal": "positive", "source": "user", "reason": "clear", "tags": ["code:x"] }
562
+ ]
563
+ }
564
+ }
565
+ }
566
+ ```
567
+
568
+ - `asset` is the show response in full (frontmatter fields such as `description`, `tags`, `toolPolicy`, the
569
+ `content`, workflow `steps`, and so on depending on the type), and `links` groups declared links by kind
570
+ (`outgoing`, `incoming`, `unresolved`). A missing ref is a `404` `ASSET_NOT_FOUND`.
571
+ - `feedback.counts` are all-time totals by signal across every source; `history` is the latest 50 events, newest
572
+ first, with the feedback's `reason` and `tags` when it had them. Both are empty for an asset with no feedback or when
573
+ `state.db` is unavailable. `durableRef` is the key feedback rows use, `null` when the asset is not indexed.
574
+ - **Env and secret assets are never read.** A ref of type `env` or `secret` returns only
575
+ `{ "redacted": true, "type": "env", "ref": "env/prod", "note": "..." }`, with no `asset`, key names or values.
576
+ Use `akm env list` for key names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.10.261005-beta",
3
+ "version": "0.10.261007-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": [