akm-cli 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ import { WorkflowSourceCollisionError, WorkflowSourceNameError, WorkflowSourceRe
17
17
  import { compileWorkflowSource } from "../workflows/source-ir/compile.js";
18
18
  import { prepareTaskV3Execution } from "./prepare/prepare.js";
19
19
  import { parseSchedule } from "./schedule.js";
20
- import { assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "./scheduler-binding.js";
20
+ import { assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeArtifactOwner, schedulerNativeBindingId, } from "./scheduler-binding.js";
21
21
  import { parseTaskSource } from "./source/parse-task-source.js";
22
22
  import { projectTaskSourceV4 } from "./source/project-v4.js";
23
23
  import { taskSourceErrorDetail } from "./source-v3.js";
@@ -47,6 +47,7 @@ export async function prepareSchedulerSyncSourceSet(input) {
47
47
  desired: compiled.desired,
48
48
  sourceSnapshot,
49
49
  executableWorkflows: compiled.executableWorkflows,
50
+ failures: compiled.failures,
50
51
  });
51
52
  }
52
53
  export function finalizeSchedulerSyncPlan(input, prepared) {
@@ -108,35 +109,7 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
108
109
  .sort(compareCodePoints);
109
110
  for (const id of removed) {
110
111
  const current = present.get(id);
111
- if (!current?.invocation) {
112
- throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
113
- }
114
- const nativeId = exactInstalledNativeId(id, current, inspection.artifacts);
115
- const artifact = inspection.artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
116
- const priorFingerprint = current.signature ?? artifact?.fingerprint;
117
- if (!artifact || priorFingerprint === undefined) {
118
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
119
- }
120
- const logicalSource = installedLogicalSource(current.invocation, coherentInput);
121
- const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
122
- if (ordinal === undefined) {
123
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
124
- }
125
- operations.push(Object.freeze({
126
- kind: "remove",
127
- id,
128
- nativeId,
129
- expected: freezeRemovalExpectation({
130
- state: "present",
131
- bindingId: id,
132
- nativeId,
133
- logicalSource,
134
- ordinal,
135
- invocation: current.invocation,
136
- fingerprint: priorFingerprint,
137
- }),
138
- ...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
139
- }));
112
+ operations.push(buildSchedulerRemoveOperation(id, current, inspection.artifacts, coherentInput));
140
113
  }
141
114
  return Object.freeze({
142
115
  desired,
@@ -146,6 +119,49 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
146
119
  unchanged: Object.freeze(unchanged),
147
120
  operations: Object.freeze(operations),
148
121
  sourceSnapshot: prepared.sourceSnapshot,
122
+ failures: prepared.failures,
123
+ });
124
+ }
125
+ /**
126
+ * Build the exact removal operation for one installed binding: same
127
+ * exact-native-fingerprint / ordinal-attribution safety checks
128
+ * `finalizeSchedulerSyncPlan`'s remove loop always applied, factored out so
129
+ * `akm task prune` (#851) can build removal operations for entries
130
+ * `belongsToBundle` structurally can't see (unresolvable ownership) without
131
+ * re-deriving — or weakening — this logic. Throws the same `UsageError`s a
132
+ * sync removal would on an inexact match; callers computing prune candidates
133
+ * should only pass entries they've already independently confirmed are safe
134
+ * to remove.
135
+ */
136
+ export function buildSchedulerRemoveOperation(id, current, artifacts, input) {
137
+ if (!current?.invocation) {
138
+ throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
139
+ }
140
+ const nativeId = exactInstalledNativeId(id, current, artifacts);
141
+ const artifact = artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
142
+ const priorFingerprint = current.signature ?? artifact?.fingerprint;
143
+ if (!artifact || priorFingerprint === undefined) {
144
+ throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
145
+ }
146
+ const logicalSource = installedLogicalSource(current.invocation, input);
147
+ const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
148
+ if (ordinal === undefined) {
149
+ throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
150
+ }
151
+ return Object.freeze({
152
+ kind: "remove",
153
+ id,
154
+ nativeId,
155
+ expected: freezeRemovalExpectation({
156
+ state: "present",
157
+ bindingId: id,
158
+ nativeId,
159
+ logicalSource,
160
+ ordinal,
161
+ invocation: current.invocation,
162
+ fingerprint: priorFingerprint,
163
+ }),
164
+ ...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
149
165
  });
150
166
  }
151
167
  function exactInstalledNativeId(logicalId, current, artifacts) {
@@ -190,10 +206,17 @@ export function assertSchedulerNativeArtifactOwnership(desired, installed) {
190
206
  const wanted = desiredByKey.get(key);
191
207
  if (!wanted)
192
208
  continue;
193
- if (artifact.nativeId !== schedulerBindingNativeId(wanted) ||
194
- artifact.bindingId !== wanted.id ||
195
- artifact.invocation === undefined ||
196
- !sameInvocation(artifact.invocation, wanted.invocation)) {
209
+ // Re-derive ownership from the artifact's own invocation content (not the
210
+ // caller-supplied `bindingId` label) so a proven owner whose invocation
211
+ // no longer matches the desired shape is an UPDATE, not a refusal — that
212
+ // reconciliation happens below in finalizeSchedulerSyncPlan. An artifact
213
+ // whose invocation content does not actually prove it belongs to
214
+ // `wanted` (unproven, malformed, or a different logical owner) is still
215
+ // a genuine collision.
216
+ const provenBindingId = artifact.invocation !== undefined
217
+ ? schedulerNativeArtifactOwner(artifact.nativeId, artifact.invocation)?.logicalId
218
+ : undefined;
219
+ if (artifact.nativeId !== schedulerBindingNativeId(wanted) || provenBindingId !== wanted.id) {
197
220
  throw nativeArtifactCollision(desiredArtifact(wanted), artifact);
198
221
  }
199
222
  }
@@ -252,12 +275,17 @@ async function compileDesiredSourceSet(input, collector) {
252
275
  const failures = [];
253
276
  await compileTaskSources(input, collector, bindings, failures);
254
277
  await compileWorkflowSources(input, collector, bindings, executableWorkflows, failures);
255
- if (failures.length > 0) {
256
- throw new UsageError(`Scheduler sync rejected the desired source set before mutation:\n${failures.map((failure) => `- ${failure}`).join("\n")}`, "TASK_SOURCE_INVALID");
257
- }
278
+ // Degrade, don't reject (#867): one source that fails to parse/prepare no
279
+ // longer poisons the whole desired set — it is dropped from `desired` and
280
+ // reported here instead, so every OTHER task/workflow still reconciles.
281
+ // Genuinely cross-cutting integrity violations (duplicate ids, native
282
+ // artifact ownership conflicts, an incoherent backend inspection) are
283
+ // asserted separately in `finalizeSchedulerSyncPlan` and still hard-fail
284
+ // the whole sync — this only relaxes the per-source parse/prepare gate.
258
285
  return Object.freeze({
259
286
  desired: Object.freeze(bindings),
260
287
  executableWorkflows: Object.freeze(executableWorkflows.sort((left, right) => compareCodePoints(left.ref, right.ref))),
288
+ failures: Object.freeze(failures.sort((left, right) => compareCodePoints(left.path, right.path))),
261
289
  });
262
290
  }
263
291
  async function compileTaskSources(input, collector, out, failures) {
@@ -269,6 +297,7 @@ async function compileTaskSources(input, collector, out, failures) {
269
297
  const relative = guarded.relativePath;
270
298
  const conceptId = relative.slice(0, -4);
271
299
  const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
300
+ const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
272
301
  try {
273
302
  const physicalIdentity = guarded.physicalIdentity;
274
303
  const priorOwner = physicalOwners.get(physicalIdentity);
@@ -356,7 +385,7 @@ async function compileTaskSources(input, collector, out, failures) {
356
385
  }
357
386
  }
358
387
  catch (cause) {
359
- failures.push(taskFailure(sourcePath, cause));
388
+ failures.push(taskFailure(sourcePath, qualifiedRefForFailure, cause));
360
389
  }
361
390
  }
362
391
  }
@@ -365,6 +394,8 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
365
394
  return;
366
395
  const lookups = enumerateWorkflowLookups(input, collector, failures);
367
396
  for (const [canonicalName, sources] of lookups) {
397
+ const failurePath = sources[0]?.sourcePath ?? canonicalName;
398
+ const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
368
399
  try {
369
400
  if (sources.length > 1) {
370
401
  throw new WorkflowSourceCollisionError(input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName, sources.map((source) => source.relativePath));
@@ -427,7 +458,7 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
427
458
  }
428
459
  }
429
460
  catch (cause) {
430
- failures.push(errorMessage(cause));
461
+ failures.push(workflowFailure(failurePath, failureRef, cause));
431
462
  }
432
463
  }
433
464
  }
@@ -456,7 +487,7 @@ function enumerateWorkflowLookups(input, collector, failures) {
456
487
  const stem = authoredName.slice(0, -extension.length).toLowerCase();
457
488
  const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
458
489
  if (nestedSuffix) {
459
- failures.push(errorMessage(new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
490
+ failures.push(workflowFailure(sourcePath, undefined, new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
460
491
  continue;
461
492
  }
462
493
  const canonicalName = canonicalizeWorkflowName(authoredName);
@@ -529,9 +560,13 @@ function assertUniqueInstalledIds(installed) {
529
560
  seen.add(binding.id);
530
561
  }
531
562
  }
532
- function taskFailure(file, cause) {
563
+ function taskFailure(file, ref, cause) {
533
564
  const detail = taskSourceErrorDetail(cause);
534
- return detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
565
+ const reason = detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
566
+ return Object.freeze({ path: file, ref, reason });
567
+ }
568
+ function workflowFailure(file, ref, cause) {
569
+ return Object.freeze({ path: file, ...(ref ? { ref } : {}), reason: errorMessage(cause) });
535
570
  }
536
571
  function errorMessage(cause) {
537
572
  return cause instanceof Error ? cause.message : String(cause);
@@ -61,6 +61,27 @@ const SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
61
61
  function shellStableV2Executable(executable) {
62
62
  return executable === "akm" || executable.includes("/");
63
63
  }
64
+ /**
65
+ * `env NAME=value... cmd args...` is env(1) itself resolving and exec'ing
66
+ * `cmd` via its own PATH search — that lookup happens inside env's execvp()
67
+ * regardless of whether env was launched by direct execve (v2) or by a host
68
+ * shell (v3 `run:`). The shell-vs-argv divergence `shellStableV2Executable`
69
+ * guards against (bare names shadowed by shell aliases/builtins/functions)
70
+ * therefore does not apply to whatever env ultimately invokes, so skip past
71
+ * a leading `env` and its `NAME=value` assignments to find the real target.
72
+ * Returns the original tokens, unchanged, when there is no such target
73
+ * (e.g. `env` with nothing after its assignments).
74
+ */
75
+ function skipEnvAssignmentPrefix(tokens) {
76
+ if (tokens[0] !== "env")
77
+ return { tokens, envWrapped: false };
78
+ let index = 1;
79
+ while (index < tokens.length && SHELL_ASSIGNMENT_WORD.test(tokens[index]))
80
+ index += 1;
81
+ if (index >= tokens.length)
82
+ return { tokens, envWrapped: false };
83
+ return { tokens: tokens.slice(index), envWrapped: true };
84
+ }
64
85
  const KNOWN_PROMPT_REF_FAMILIES = new Set([
65
86
  "agents",
66
87
  "commands",
@@ -81,15 +102,6 @@ function hash(bytes) {
81
102
  return crypto.createHash("sha256").update(bytes).digest("hex");
82
103
  }
83
104
  function base(input) {
84
- const inspectionIdentity = input.inspectionIdentity
85
- ? Object.freeze({
86
- file: Object.freeze({ ...input.inspectionIdentity.file }),
87
- root: Object.freeze({ ...input.inspectionIdentity.root }),
88
- ...(input.inspectionIdentity.bundleRoot
89
- ? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) }
90
- : {}),
91
- })
92
- : undefined;
93
105
  return {
94
106
  filePath: input.filePath,
95
107
  before: Buffer.from(input.bytes),
@@ -98,7 +110,6 @@ function base(input) {
98
110
  writable: input.writable,
99
111
  ...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
100
112
  ...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
101
- ...(inspectionIdentity ? { inspectionIdentity } : {}),
102
113
  };
103
114
  }
104
115
  function blocked(input, reason, detail) {
@@ -348,8 +359,9 @@ function migratedObject(data) {
348
359
  if (tokens.length === 0 || tokens.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
349
360
  return "shell-operators-change-v2-literal-argv-semantics";
350
361
  }
351
- const executable = tokens[0];
352
- if (SHELL_ASSIGNMENT_WORD.test(executable) || !shellStableV2Executable(executable)) {
362
+ const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens);
363
+ const executable = targetTokens[0];
364
+ if (SHELL_ASSIGNMENT_WORD.test(executable) || (!envWrapped && !shellStableV2Executable(executable))) {
353
365
  return "shell-command-resolution-changes-v2-literal-argv-semantics";
354
366
  }
355
367
  output.run = tokens.join(" ");
@@ -76,15 +76,6 @@ function causeMessage(cause) {
76
76
  return cause instanceof Error ? cause.message : String(cause);
77
77
  }
78
78
  function base(input) {
79
- const inspectionIdentity = input.inspectionIdentity
80
- ? Object.freeze({
81
- file: Object.freeze({ ...input.inspectionIdentity.file }),
82
- root: Object.freeze({ ...input.inspectionIdentity.root }),
83
- ...(input.inspectionIdentity.bundleRoot
84
- ? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) }
85
- : {}),
86
- })
87
- : undefined;
88
79
  return {
89
80
  filePath: input.filePath,
90
81
  before: Buffer.from(input.bytes),
@@ -93,7 +84,6 @@ function base(input) {
93
84
  writable: input.writable,
94
85
  ...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
95
86
  ...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
96
- ...(inspectionIdentity ? { inspectionIdentity } : {}),
97
87
  };
98
88
  }
99
89
  function blocked(input, reason, detail) {
@@ -2423,9 +2423,10 @@ shell commands. It manages on-disk task definitions under
2423
2423
  (cron / launchd / schtasks). Task source v4 YAML (`version: 4`) is the only
2424
2424
  executable source contract this release accepts; `akm task add` writes v4 —
2425
2425
  see the canonical [Tasks reference](tasks.md). The
2426
- group is `add | run | explain | sync | doctor | history` — there is no `list`
2427
- or `remove`; use `akm search --type task` / `akm show tasks/<id>` to inspect,
2428
- and edit the file + `akm task sync` to change or remove a schedule.
2426
+ group is `add | run | explain | sync | doctor | history | prune` — there is
2427
+ no `list` or `remove`; use `akm search --type task` / `akm show tasks/<id>`
2428
+ to inspect, and edit the file + `akm task sync` to change or remove a
2429
+ schedule.
2429
2430
 
2430
2431
  ```sh
2431
2432
  akm search --type task # List tasks (cross-bundle)
@@ -2439,8 +2440,12 @@ akm task run <id> # Execute now (what the scheduler ca
2439
2440
  akm task explain <ref> # Read-only: declared inputs, target, schedule — spawns nothing
2440
2441
  akm task history [--id <id>] [--limit <n>] # Recent runs from state.db
2441
2442
  akm task sync # Reconcile on-disk YAML with scheduler
2443
+ akm task sync --dry-run # Preview the reconcile — zero scheduler writes
2442
2444
  akm task sync --rebind # Also capture the current installed runtime
2443
2445
  akm task doctor # Report scheduler backend + paths
2446
+ akm task prune # Preview orphaned scheduler entries — zero writes
2447
+ akm task prune --yes # Remove every currently-computed orphan
2448
+ akm task prune --id ghost,stale --yes # Remove only the named orphan ids
2444
2449
  ```
2445
2450
 
2446
2451
  `task add` also accepts `--disabled` (register but leave off in the OS
@@ -2466,6 +2471,23 @@ schedule-binding) and run `akm task sync`. To remove one, delete its file
2466
2471
  (`<bundle>/tasks/<id>.yml`) and run `akm task sync` — sync uninstalls the
2467
2472
  orphaned scheduler entry.
2468
2473
 
2474
+ `akm task sync --dry-run` prints the planned adds/updates/removes (removals
2475
+ carry their owning bundle) without touching the scheduler — zero writes.
2476
+ Exits non-zero when removals are pending, so it can gate a CI/health check
2477
+ on "sync would change something."
2478
+
2479
+ `akm task prune` reclaims installed scheduler entries that `sync` can never
2480
+ clean up on its own: entries whose own `--scheduler-context` descriptor no
2481
+ longer resolves to a live bundle (a corrupt/missing descriptor, or the
2482
+ bundle directory it pointed at is gone). It never touches an entry that
2483
+ still resolves to a live bundle — that's `sync`'s job. Like `sync
2484
+ --dry-run`, the default is a dry-run preview (zero scheduler writes) that
2485
+ exits non-zero when there are candidates to remove; `--yes` executes the
2486
+ printed plan, and `--id <id1,id2,...>` narrows a `--yes` run (or a preview)
2487
+ to specific binding ids — naming an id that isn't a current orphan
2488
+ candidate (not installed, or it still resolves to a live bundle) is
2489
+ refused with a usage error and removes nothing.
2490
+
2469
2491
  Scheduler activation captures the installed akm runtime. Ordinary `task sync`
2470
2492
  reconciles definitions, schedules, and enabled state while preserving that
2471
2493
  runtime binding. Use `task sync --rebind` only after intentionally moving or
@@ -7,12 +7,33 @@ directory. Project `.akm/config.json` files are not merged.
7
7
 
8
8
  ## Version 0.9
9
9
 
10
- A present configuration file must set `configVersion` to exactly `"0.9.0"`.
11
- Missing, older, newer, numeric, and malformed versions are rejected by ordinary
12
- commands without rewriting the file. Pre-0.9 config and database layouts are
13
- not runtime inputs and are not migrated by `akm upgrade`. Configure the current
14
- schema directly. The standalone migrator exists only for explicit task
15
- migration: task v2 to task v3, then task v3 to task source v4, in one pass.
10
+ A present configuration file must set `configVersion` to a version this
11
+ binary knows: the current `"0.9.0"`, or a known older version it can
12
+ auto-upgrade in memory (see "Version read shim" below). Missing, newer,
13
+ numeric, and any other unrecognized version are rejected by ordinary
14
+ commands without rewriting the file — an older binary never guesses at a
15
+ newer, unknown shape. Pre-0.9 config and database layouts are not runtime
16
+ inputs and are not migrated by `akm upgrade`. Configure the current schema
17
+ directly. The standalone migrator exists only for explicit task migration:
18
+ task v2 to task v3, then task v3 to task source v4, in one pass.
19
+
20
+ ### Version read shim
21
+
22
+ Like the task-source v2/v3 auto-shim (`akm migrate apply`'s in-memory
23
+ counterpart, documented under Migration below), a known older `configVersion`
24
+ is converted to the current shape in memory on load — with a one-line stderr
25
+ deprecation warning — rather than hard-failing every command. Nothing is
26
+ written back to disk by the shim itself; the very next config-mutating
27
+ command (`akm config set`, etc.) persists the upgrade for free, since every
28
+ config write already forces `configVersion` to the current value, which
29
+ silences the warning. A `configVersion` this binary does not recognize at
30
+ all — including anything newer than current — still fails closed with
31
+ `UNSUPPORTED_CONFIG_VERSION`.
32
+
33
+ As of this writing `"0.9.0"` is the only `configVersion` akm has ever
34
+ shipped, so there is no real older shape for the shim to convert yet; the
35
+ mechanism (`src/core/config/config-version-shim.ts`) is established ahead of
36
+ the first bump that will need it, per #863.
16
37
 
17
38
  ```jsonc
18
39
  {
@@ -389,6 +389,16 @@ for full before/after examples and recovery guidance.
389
389
  - Disable a binding by editing the source and syncing: set that schedule
390
390
  entry's own `enabled: false`.
391
391
  - Delete the `.yml` source and sync to remove its derived binding(s).
392
+ - `akm task sync --dry-run` previews the reconcile (adds/updates/removes,
393
+ removals annotated with their owning bundle) without writing to the
394
+ scheduler; exits non-zero when removals are pending.
395
+ - `akm task prune` removes installed scheduler entries `sync` cannot reach
396
+ because their own descriptor no longer resolves to a live bundle
397
+ (corrupt/missing `--scheduler-context`, or the owning bundle directory is
398
+ gone). It never touches an entry that still resolves to a live bundle.
399
+ Defaults to a dry-run preview (zero writes); `--yes` executes it; `--id
400
+ <id1,id2,...>` scopes to specific ids and refuses any id that isn't a
401
+ current orphan candidate.
392
402
  - Use `akm task sync --rebind` only when deliberately changing the captured
393
403
  AKM runtime, then verify with `akm task doctor`.
394
404
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
6
6
  "keywords": [