akm-cli 0.7.0-rc1 → 0.7.0

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 (83) hide show
  1. package/dist/src/cli.js +100 -16
  2. package/dist/src/commands/config-cli.js +42 -0
  3. package/dist/src/commands/history.js +78 -7
  4. package/dist/src/commands/registry-search.js +69 -6
  5. package/dist/src/commands/search.js +30 -3
  6. package/dist/src/commands/show.js +29 -0
  7. package/dist/src/commands/source-add.js +5 -1
  8. package/dist/src/commands/source-manage.js +7 -1
  9. package/dist/src/core/config.js +28 -0
  10. package/dist/src/indexer/db-search.js +1 -0
  11. package/dist/src/indexer/indexer.js +16 -2
  12. package/dist/src/indexer/matchers.js +1 -1
  13. package/dist/src/indexer/search-source.js +4 -2
  14. package/dist/src/integrations/agent/profiles.js +1 -1
  15. package/dist/src/integrations/agent/spawn.js +67 -16
  16. package/dist/src/integrations/github.js +9 -3
  17. package/dist/src/llm/embedders/remote.js +37 -3
  18. package/dist/src/output/cli-hints.js +15 -2
  19. package/dist/src/output/renderers.js +3 -1
  20. package/dist/src/output/shapes.js +8 -1
  21. package/dist/src/output/text.js +156 -3
  22. package/dist/src/registry/build-index.js +5 -4
  23. package/dist/src/registry/providers/static-index.js +3 -1
  24. package/dist/src/setup/setup.js +9 -0
  25. package/dist/src/wiki/wiki.js +54 -6
  26. package/dist/src/workflows/runs.js +37 -3
  27. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +1 -1
  28. package/dist/tests/bench/attribution.test.js +24 -23
  29. package/dist/tests/bench/cleanup.js +31 -0
  30. package/dist/tests/bench/cli.js +366 -31
  31. package/dist/tests/bench/cli.test.js +282 -14
  32. package/dist/tests/bench/corpus.js +3 -0
  33. package/dist/tests/bench/corpus.test.js +10 -10
  34. package/dist/tests/bench/doctor.js +525 -0
  35. package/dist/tests/bench/driver.js +77 -22
  36. package/dist/tests/bench/driver.test.js +142 -1
  37. package/dist/tests/bench/environment.js +233 -0
  38. package/dist/tests/bench/environment.test.js +199 -0
  39. package/dist/tests/bench/evolve.js +67 -0
  40. package/dist/tests/bench/evolve.test.js +12 -4
  41. package/dist/tests/bench/failure-modes.test.js +52 -3
  42. package/dist/tests/bench/feedback-integrity.test.js +3 -2
  43. package/dist/tests/bench/leakage.test.js +105 -2
  44. package/dist/tests/bench/learning-curve.test.js +3 -2
  45. package/dist/tests/bench/metrics.js +102 -26
  46. package/dist/tests/bench/metrics.test.js +10 -4
  47. package/dist/tests/bench/opencode-config.js +194 -0
  48. package/dist/tests/bench/opencode-config.test.js +370 -0
  49. package/dist/tests/bench/report.js +73 -9
  50. package/dist/tests/bench/report.test.js +59 -10
  51. package/dist/tests/bench/run-config.js +355 -0
  52. package/dist/tests/bench/run-config.test.js +298 -0
  53. package/dist/tests/bench/run-curate-test.js +32 -0
  54. package/dist/tests/bench/run-failing-tasks.js +56 -0
  55. package/dist/tests/bench/run-full-bench.js +51 -0
  56. package/dist/tests/bench/run-items36-targeted.js +69 -0
  57. package/dist/tests/bench/run-nano-quick.js +42 -0
  58. package/dist/tests/bench/run-waveg-targeted.js +62 -0
  59. package/dist/tests/bench/runner.js +257 -94
  60. package/dist/tests/bench/tmp.js +90 -0
  61. package/dist/tests/bench/trajectory.js +2 -2
  62. package/dist/tests/bench/verifier.js +6 -1
  63. package/dist/tests/bench/workflow-spec.js +11 -24
  64. package/dist/tests/bench/workflow-spec.test.js +1 -1
  65. package/dist/tests/bench/workflow-trace.js +34 -0
  66. package/dist/tests/cli-errors.test.js +1 -0
  67. package/dist/tests/commands/history.test.js +195 -0
  68. package/dist/tests/config.test.js +25 -0
  69. package/dist/tests/e2e.test.js +23 -2
  70. package/dist/tests/fixtures/stashes/load.js +1 -1
  71. package/dist/tests/fixtures/stashes/load.test.js +11 -2
  72. package/dist/tests/indexer.test.js +12 -1
  73. package/dist/tests/output-baseline.test.js +2 -1
  74. package/dist/tests/output-shapes-unit.test.js +3 -1
  75. package/dist/tests/registry-build-index.test.js +17 -1
  76. package/dist/tests/registry-providers/static-index.test.js +34 -0
  77. package/dist/tests/registry-search.test.js +200 -0
  78. package/dist/tests/remember-frontmatter.test.js +11 -13
  79. package/dist/tests/source-qa-fixes.test.js +18 -0
  80. package/dist/tests/source-registry.test.js +3 -3
  81. package/dist/tests/source-source.test.js +61 -1
  82. package/dist/tests/workflow-qa-fixes.test.js +18 -0
  83. package/package.json +1 -1
@@ -485,6 +485,206 @@ describe("AKM_REGISTRY_URL env var", () => {
485
485
  srv2.close();
486
486
  }
487
487
  });
488
+ // Problem A: env-based override must preserve provider type
489
+ test("provider::url syntax routes to the declared provider type", async () => {
490
+ // Stand up a skills-sh-shaped endpoint
491
+ const skillsSrv = Bun.serve({
492
+ port: 0,
493
+ fetch() {
494
+ return new Response(JSON.stringify({
495
+ skills: [{ id: "org/tools/my-skill", name: "my-skill", installs: 200, source: "org/tools" }],
496
+ }), { headers: { "Content-Type": "application/json" } });
497
+ },
498
+ });
499
+ process.env.AKM_REGISTRY_URL = `skills-sh::http://localhost:${skillsSrv.port}`;
500
+ try {
501
+ const result = await searchRegistry("my-skill");
502
+ // skills-sh provider should have handled this — hits use skills-sh id format
503
+ expect(result.hits.length).toBeGreaterThan(0);
504
+ expect(result.hits[0].id).toBe("skills-sh:org/tools/my-skill");
505
+ expect(result.hits[0].installRef).toBe("github:org/tools");
506
+ expect(result.warnings).toEqual([]);
507
+ }
508
+ finally {
509
+ skillsSrv.stop(true);
510
+ }
511
+ });
512
+ test("bare URL in env var defaults to static-index provider", async () => {
513
+ const srv = serveIndex(FIXTURE_INDEX);
514
+ process.env.AKM_REGISTRY_URL = srv.url;
515
+ try {
516
+ const result = await searchRegistry("openkit");
517
+ expect(result.hits.length).toBeGreaterThan(0);
518
+ // static-index uses the stash id directly
519
+ expect(result.hits[0].id).toBe("npm:@itlackey/openkit");
520
+ }
521
+ finally {
522
+ srv.close();
523
+ }
524
+ });
525
+ test("unknown provider type in env var produces warning, not crash", async () => {
526
+ process.env.AKM_REGISTRY_URL = `no-such-provider::http://127.0.0.1:1/index.json`;
527
+ const result = await searchRegistry("anything");
528
+ expect(result.hits).toEqual([]);
529
+ expect(result.warnings.length).toBe(1);
530
+ expect(result.warnings[0]).toContain("no-such-provider");
531
+ });
532
+ test("mixed provider types in comma-separated env var", async () => {
533
+ const staticSrv = serveIndex({
534
+ version: 3,
535
+ updatedAt: "2026-01-01T00:00:00Z",
536
+ stashes: [
537
+ {
538
+ id: "npm:env-static-stash",
539
+ name: "env-static-stash",
540
+ ref: "env-static-stash",
541
+ source: "npm",
542
+ tags: ["deploy"],
543
+ },
544
+ ],
545
+ });
546
+ const skillsSrv = Bun.serve({
547
+ port: 0,
548
+ fetch() {
549
+ return new Response(JSON.stringify({
550
+ skills: [{ id: "user/tools/env-skill", name: "env-skill", installs: 100, source: "user/tools" }],
551
+ }), { headers: { "Content-Type": "application/json" } });
552
+ },
553
+ });
554
+ process.env.AKM_REGISTRY_URL = `${staticSrv.url},skills-sh::http://localhost:${skillsSrv.port}`;
555
+ try {
556
+ const result = await searchRegistry("env");
557
+ const ids = result.hits.map((h) => h.id);
558
+ expect(ids).toContain("npm:env-static-stash");
559
+ expect(ids).toContain("skills-sh:user/tools/env-skill");
560
+ expect(result.warnings).toEqual([]);
561
+ }
562
+ finally {
563
+ staticSrv.close();
564
+ skillsSrv.stop(true);
565
+ }
566
+ });
567
+ });
568
+ // ── Score normalization (Problem B) ─────────────────────────────────────────
569
+ describe("cross-provider score normalization", () => {
570
+ test("scores from all providers are in [0, 1] after normalization", async () => {
571
+ // static-index raw scores can exceed 1 (e.g. exact name + tag + description).
572
+ // After normalization, all scores in the merged response must be <= 1.
573
+ const srv = serveIndex(FIXTURE_INDEX);
574
+ try {
575
+ const result = await searchRegistry("openkit bun typescript starter", {
576
+ registries: [{ url: srv.url }],
577
+ });
578
+ for (const hit of result.hits) {
579
+ if (hit.score !== undefined) {
580
+ expect(hit.score).toBeGreaterThanOrEqual(0);
581
+ expect(hit.score).toBeLessThanOrEqual(1);
582
+ }
583
+ }
584
+ }
585
+ finally {
586
+ srv.close();
587
+ }
588
+ });
589
+ test("top hit within a provider batch retains score = 1 after normalization", async () => {
590
+ // The highest-scored hit in each provider batch should map to exactly 1.0.
591
+ const srv = serveIndex(FIXTURE_INDEX);
592
+ try {
593
+ const result = await searchRegistry("openkit", {
594
+ registries: [{ url: srv.url }],
595
+ });
596
+ expect(result.hits.length).toBeGreaterThan(0);
597
+ const topScore = result.hits[0].score;
598
+ expect(topScore).toBe(1);
599
+ }
600
+ finally {
601
+ srv.close();
602
+ }
603
+ });
604
+ test("merged multi-provider results are ordered by normalized score", async () => {
605
+ // Provider A: static-index with a moderate-relevance match.
606
+ // Provider B: skills-sh with a high-installs match.
607
+ // After normalization each batch has max=1; the better-matched kit wins.
608
+ const staticSrv = serveIndex({
609
+ version: 3,
610
+ updatedAt: "2026-01-01T00:00:00Z",
611
+ stashes: [
612
+ {
613
+ id: "npm:exact-name-match",
614
+ name: "deploy",
615
+ description: "exact match",
616
+ ref: "exact-name-match",
617
+ source: "npm",
618
+ tags: ["deploy"],
619
+ },
620
+ {
621
+ id: "npm:partial-match",
622
+ name: "deployment-helper",
623
+ description: "partial",
624
+ ref: "partial-match",
625
+ source: "npm",
626
+ tags: [],
627
+ },
628
+ ],
629
+ });
630
+ const skillsSrv = Bun.serve({
631
+ port: 0,
632
+ fetch() {
633
+ return new Response(JSON.stringify({
634
+ skills: [
635
+ { id: "org/deploy-skill", name: "deploy-skill", installs: 1000, source: "org/deploy-skill" },
636
+ { id: "org/other-skill", name: "other-skill", installs: 100, source: "org/other" },
637
+ ],
638
+ }), { headers: { "Content-Type": "application/json" } });
639
+ },
640
+ });
641
+ try {
642
+ const result = await searchRegistry("deploy", {
643
+ registries: [{ url: staticSrv.url }, { url: `http://localhost:${skillsSrv.port}`, provider: "skills-sh" }],
644
+ });
645
+ // All scores in [0, 1]
646
+ for (const hit of result.hits) {
647
+ if (hit.score !== undefined) {
648
+ expect(hit.score).toBeGreaterThanOrEqual(0);
649
+ expect(hit.score).toBeLessThanOrEqual(1);
650
+ }
651
+ }
652
+ // Results should be sorted descending
653
+ for (let i = 1; i < result.hits.length; i++) {
654
+ expect((result.hits[i - 1].score ?? 0) >= (result.hits[i].score ?? 0)).toBe(true);
655
+ }
656
+ }
657
+ finally {
658
+ staticSrv.close();
659
+ skillsSrv.stop(true);
660
+ }
661
+ });
662
+ test("single-hit provider batch normalizes to score 1", async () => {
663
+ const srv = serveIndex({
664
+ version: 3,
665
+ updatedAt: "2026-01-01T00:00:00Z",
666
+ stashes: [
667
+ {
668
+ id: "npm:only-stash",
669
+ name: "only-stash",
670
+ description: "only one",
671
+ ref: "only-stash",
672
+ source: "npm",
673
+ tags: ["unique"],
674
+ },
675
+ ],
676
+ });
677
+ try {
678
+ const result = await searchRegistry("unique", {
679
+ registries: [{ url: srv.url }],
680
+ });
681
+ expect(result.hits.length).toBe(1);
682
+ expect(result.hits[0].score).toBe(1);
683
+ }
684
+ finally {
685
+ srv.close();
686
+ }
687
+ });
488
688
  });
489
689
  // ── Provenance tagging ──────────────────────────────────────────────────────
490
690
  describe("provenance tagging", () => {
@@ -67,6 +67,13 @@ describe("zero-flag remember", () => {
67
67
  const content = fs.readFileSync(json.path, "utf8");
68
68
  expect(content.startsWith("---")).toBe(false);
69
69
  });
70
+ test("reads stdin when --format json is present", () => {
71
+ const { result } = runCli(["remember", "--name", "from-stdin", "--format", "json"], { input: "stdin body" });
72
+ expect(result.status).toBe(0);
73
+ const json = JSON.parse(result.stdout);
74
+ expect(fs.readFileSync(json.path, "utf8")).toContain("stdin body");
75
+ expect(fs.readFileSync(json.path, "utf8")).not.toContain("\njson");
76
+ });
70
77
  });
71
78
  // ── CLI args (Mode 1) ────────────────────────────────────────────────────────
72
79
  describe("remember --tag", () => {
@@ -227,21 +234,12 @@ describe("remember --auto", () => {
227
234
  expect(parsed.data.observed_at).toBe("2026-01-15");
228
235
  void result; // suppress unused variable warning
229
236
  });
230
- test("--auto without any tags from heuristics or CLI rejects with missing-fields error", () => {
237
+ test("--auto without any tags from heuristics or CLI still writes the memory", () => {
231
238
  // Plain text body — no code block, no URL. Heuristics won't derive any tags.
232
239
  const { result } = runCli(["remember", "Plain text note without any tags derivable", "--auto"]);
233
- // If no heuristic tags are derived and no CLI tags provided, should fail
234
- // (The auto heuristic only adds "code" for code blocks, none for plain text)
235
- // This tests that the guard fires correctly
236
- if (result.status !== 0) {
237
- const json = JSON.parse(result.stderr);
238
- expect(json.error).toContain("tags");
239
- }
240
- else {
241
- // If some future heuristic adds tags for plain text, just verify the file is written
242
- const json = JSON.parse(result.stdout);
243
- expect(fs.existsSync(json.path)).toBe(true);
244
- }
240
+ expect(result.status).toBe(0);
241
+ const json = JSON.parse(result.stdout);
242
+ expect(fs.existsSync(json.path)).toBe(true);
245
243
  });
246
244
  test("--auto + explicit --tag satisfies required-field check", () => {
247
245
  const body = "No special content here";
@@ -14,7 +14,9 @@ import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { akmListSources, akmUpdate } from "../src/commands/installed-stashes";
16
16
  import { akmAdd } from "../src/commands/source-add";
17
+ import { addStash } from "../src/commands/source-manage";
17
18
  import { loadConfig, saveConfig } from "../src/core/config";
19
+ import { ConfigError } from "../src/core/errors";
18
20
  const createdTmpDirs = [];
19
21
  function createTmpDir(prefix = "akm-qa-") {
20
22
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
@@ -115,6 +117,22 @@ describe("issue #9: --name flag persisted for filesystem sources", () => {
115
117
  expect(typeof added?.name).toBe("string");
116
118
  });
117
119
  });
120
+ describe("manual QA add validation", () => {
121
+ test("akmAdd rejects writable installs for npm refs before syncing", async () => {
122
+ saveConfig({ semanticSearchMode: "off" });
123
+ await expect(akmAdd({ ref: "npm:left-pad", writable: true })).rejects.toThrow(ConfigError);
124
+ });
125
+ test("addStash rejects openviking providers before persisting config", () => {
126
+ saveConfig({ semanticSearchMode: "off" });
127
+ expect(() => addStash({ target: "https://example.com", providerType: "openviking" })).toThrow(ConfigError);
128
+ expect(loadConfig().sources).toBeUndefined();
129
+ });
130
+ test("addStash rejects writable website sources before persisting config", () => {
131
+ saveConfig({ semanticSearchMode: "off" });
132
+ expect(() => addStash({ target: "https://example.com", providerType: "website", writable: true })).toThrow(ConfigError);
133
+ expect(loadConfig().sources).toBeUndefined();
134
+ });
135
+ });
118
136
  // ── Issue #10: filesystem kind = "filesystem" in list output ──────────────
119
137
  describe("issue #10: filesystem kind in list output", () => {
120
138
  test("filesystem source has kind='filesystem' in akmListSources", async () => {
@@ -155,9 +155,9 @@ describe("akmListSources", () => {
155
155
  semanticSearchMode: "off",
156
156
  installed: [
157
157
  {
158
- id: "github:owner/repo",
159
- source: "github",
160
- ref: "github:owner/repo",
158
+ id: "git:https://github.com/owner/repo",
159
+ source: "git",
160
+ ref: "git:https://github.com/owner/repo.git",
161
161
  artifactUrl: "https://github.com/owner/repo.git",
162
162
  stashRoot,
163
163
  cacheDir,
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { saveConfig } from "../src/core/config";
6
- import { findSourceForPath, getPrimarySource, isEditable, resolveAllStashDirs, resolveSourceEntries, } from "../src/indexer/search-source";
6
+ import { ensureSourceCaches, findSourceForPath, getPrimarySource, isEditable, resolveAllStashDirs, resolveSourceEntries, } from "../src/indexer/search-source";
7
7
  const originalStashDir = process.env.AKM_STASH_DIR;
8
8
  const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
9
9
  let testConfigDir = "";
@@ -219,3 +219,63 @@ describe("isEditable", () => {
219
219
  expect(isEditable("/some/random/path/file.sh")).toBe(true);
220
220
  });
221
221
  });
222
+ // ── ensureSourceCaches ────────────────────────────────────────────────────────
223
+ describe("ensureSourceCaches", () => {
224
+ test("completes without error when sources[] is empty", async () => {
225
+ const config = { semanticSearchMode: "off", sources: [] };
226
+ await expect(ensureSourceCaches(config)).resolves.toBeUndefined();
227
+ });
228
+ test("completes without error when sources[] has filesystem entries (no sync needed)", async () => {
229
+ const config = {
230
+ semanticSearchMode: "off",
231
+ sources: [{ type: "filesystem", path: stashDir }],
232
+ };
233
+ await expect(ensureSourceCaches(config)).resolves.toBeUndefined();
234
+ });
235
+ test("reads from sources[] not stashes[] — git entries in sources[] are processed", async () => {
236
+ // A config where sources[] has a git entry and stashes is undefined.
237
+ // We can't run a real git mirror in unit tests, but we verify that the
238
+ // function does NOT throw even when the git URL is unreachable (it warns).
239
+ const config = {
240
+ semanticSearchMode: "off",
241
+ sources: [
242
+ {
243
+ type: "git",
244
+ url: "https://github.com/example/nonexistent-repo.git",
245
+ name: "test-git",
246
+ },
247
+ ],
248
+ };
249
+ // Should resolve (not reject) — failures are warn-only
250
+ await expect(ensureSourceCaches(config)).resolves.toBeUndefined();
251
+ });
252
+ test("reads from sources[] not stashes[] — website entries in sources[] are processed", async () => {
253
+ // A config where sources[] has a website entry and stashes is undefined.
254
+ // The mirror will fail (unreachable host) but the function warns, not throws.
255
+ const config = {
256
+ semanticSearchMode: "off",
257
+ sources: [
258
+ {
259
+ type: "website",
260
+ url: "https://example.invalid/docs",
261
+ name: "test-website",
262
+ },
263
+ ],
264
+ };
265
+ await expect(ensureSourceCaches(config)).resolves.toBeUndefined();
266
+ });
267
+ test("stashes[] entries are still processed for one-release backwards compat", async () => {
268
+ // stashes[] is deprecated but still accepted in the runtime shape for one release.
269
+ const config = {
270
+ semanticSearchMode: "off",
271
+ stashes: [
272
+ {
273
+ type: "git",
274
+ url: "https://github.com/example/nonexistent-repo.git",
275
+ name: "legacy-git",
276
+ },
277
+ ],
278
+ };
279
+ await expect(ensureSourceCaches(config)).resolves.toBeUndefined();
280
+ });
281
+ });
@@ -282,6 +282,24 @@ describe("workflow status with workflow ref", () => {
282
282
  const err = JSON.parse(status.stderr);
283
283
  expect(err.error).toContain("No workflow runs found");
284
284
  });
285
+ test("next with an unknown run id returns WORKFLOW_NOT_FOUND", () => {
286
+ const env = createWorkflowEnv();
287
+ setupWorkflow(env);
288
+ const next = runCli(["workflow", "next", "bogus-run-id"], env);
289
+ expect(next.status).toBe(1);
290
+ const err = JSON.parse(next.stderr);
291
+ expect(err.code).toBe("WORKFLOW_NOT_FOUND");
292
+ expect(err.hint).toContain("akm workflow list --active");
293
+ });
294
+ test("status with an unknown run id returns WORKFLOW_NOT_FOUND", () => {
295
+ const env = createWorkflowEnv();
296
+ setupWorkflow(env);
297
+ const status = runCli(["workflow", "status", "bogus-run-id"], env);
298
+ expect(status.status).toBe(1);
299
+ const err = JSON.parse(status.stderr);
300
+ expect(err.code).toBe("WORKFLOW_NOT_FOUND");
301
+ expect(err.hint).toContain("akm workflow list --active");
302
+ });
285
303
  });
286
304
  // ---------------------------------------------------------------------------
287
305
  // 7. workflow create name validation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.7.0-rc1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Kit Manager) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [