akm-cli 0.2.2 → 0.3.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.
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DEFAULT_LOCAL_MODEL } from "./embedder";
4
+ import { getCacheDir, getSemanticStatusPath } from "./paths";
5
+ export function deriveSemanticProviderFingerprint(embedding) {
6
+ if (embedding?.endpoint) {
7
+ return `remote:${embedding.endpoint}|${embedding.model}|${embedding.dimension ?? "default"}`;
8
+ }
9
+ return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}`;
10
+ }
11
+ export function readSemanticStatus() {
12
+ try {
13
+ const raw = JSON.parse(fs.readFileSync(getSemanticStatusPath(), "utf8"));
14
+ if ((raw.status === "pending" ||
15
+ raw.status === "ready-js" ||
16
+ raw.status === "ready-vec" ||
17
+ raw.status === "blocked") &&
18
+ typeof raw.providerFingerprint === "string" &&
19
+ typeof raw.lastCheckedAt === "string") {
20
+ const status = {
21
+ status: raw.status,
22
+ providerFingerprint: raw.providerFingerprint,
23
+ lastCheckedAt: raw.lastCheckedAt,
24
+ };
25
+ if (typeof raw.reason === "string")
26
+ status.reason = raw.reason;
27
+ if (typeof raw.message === "string")
28
+ status.message = raw.message;
29
+ if (typeof raw.entryCount === "number")
30
+ status.entryCount = raw.entryCount;
31
+ if (typeof raw.embeddingCount === "number")
32
+ status.embeddingCount = raw.embeddingCount;
33
+ return status;
34
+ }
35
+ }
36
+ catch {
37
+ // ignore corrupt or missing semantic status
38
+ }
39
+ return undefined;
40
+ }
41
+ export function writeSemanticStatus(status) {
42
+ const dir = getCacheDir();
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ const filePath = getSemanticStatusPath();
45
+ const tmpPath = path.join(dir, `semantic-status.json.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`);
46
+ fs.writeFileSync(tmpPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
47
+ try {
48
+ fs.renameSync(tmpPath, filePath);
49
+ }
50
+ catch (err) {
51
+ try {
52
+ fs.unlinkSync(tmpPath);
53
+ }
54
+ catch {
55
+ /* ignore cleanup failure */
56
+ }
57
+ throw err;
58
+ }
59
+ }
60
+ export function clearSemanticStatus() {
61
+ try {
62
+ fs.unlinkSync(getSemanticStatusPath());
63
+ }
64
+ catch {
65
+ // ignore missing file
66
+ }
67
+ }
68
+ /** How long a "blocked" status is retained before the system retries. 24 hours. */
69
+ export const BLOCKED_TTL_MS = 24 * 60 * 60 * 1000;
70
+ export function getEffectiveSemanticStatus(config, status = readSemanticStatus()) {
71
+ if (config.semanticSearchMode === "off")
72
+ return "disabled";
73
+ if (!status)
74
+ return "pending";
75
+ const fingerprint = deriveSemanticProviderFingerprint(config.embedding);
76
+ if (status.providerFingerprint !== fingerprint)
77
+ return "pending";
78
+ // Auto-recovery: if blocked status is older than BLOCKED_TTL_MS, treat as pending
79
+ // so the next index run will re-attempt semantic setup.
80
+ if (status.status === "blocked") {
81
+ const checkedAt = new Date(status.lastCheckedAt).getTime();
82
+ if (Number.isNaN(checkedAt) || Date.now() - checkedAt > BLOCKED_TTL_MS) {
83
+ return "pending";
84
+ }
85
+ }
86
+ return status.status;
87
+ }
88
+ export function isSemanticRuntimeReady(status) {
89
+ return status === "ready-js" || status === "ready-vec";
90
+ }
91
+ export function classifySemanticFailure(message) {
92
+ const lower = message.toLowerCase();
93
+ if (lower.includes("401") || lower.includes("403") || lower.includes("auth") || lower.includes("unauthorized")) {
94
+ return "remote-auth";
95
+ }
96
+ if (lower.includes("429") || lower.includes("rate limit") || lower.includes("quota")) {
97
+ return "remote-rate-limit";
98
+ }
99
+ if (lower.includes("eacces") || lower.includes("permission denied")) {
100
+ return "permission-denied";
101
+ }
102
+ // Native library / linker errors must be checked before the generic ONNX
103
+ // match because Alpine/musl linker errors often contain "onnxruntime" in
104
+ // the library path (e.g. onnxruntime_binding.node).
105
+ if (lower.includes("shared library") ||
106
+ lower.includes("glibc") ||
107
+ lower.includes("musl") ||
108
+ lower.includes("libc.so")) {
109
+ return "native-lib-missing";
110
+ }
111
+ if (lower.includes("onnx") || lower.includes("onnxruntime")) {
112
+ return "onnx-runtime-failed";
113
+ }
114
+ if (lower.includes("404") || lower.includes("model not found") || lower.includes("bad request")) {
115
+ return "remote-model";
116
+ }
117
+ if (lower.includes("transformers") || lower.includes("missing-package")) {
118
+ return "missing-package";
119
+ }
120
+ if (lower.includes("download")) {
121
+ return "local-model-download";
122
+ }
123
+ if (lower.includes("dimension mismatch")) {
124
+ return "dimension-mismatch";
125
+ }
126
+ if (lower.includes("db") || lower.includes("sqlite") || lower.includes("cache dir")) {
127
+ return "db-open";
128
+ }
129
+ if (lower.includes("timeout") ||
130
+ lower.includes("unreachable") ||
131
+ lower.includes("refused") ||
132
+ lower.includes("network") ||
133
+ lower.includes("fetch")) {
134
+ return "remote-network";
135
+ }
136
+ return "unknown";
137
+ }
package/dist/setup.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * registry selection, stash sources, and agent platform discovery.
6
6
  * Collects all choices and writes config once at the end.
7
7
  */
8
+ import path from "node:path";
8
9
  import * as p from "@clack/prompts";
9
10
  import { isHttpUrl } from "./common";
10
11
  import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
@@ -14,6 +15,7 @@ import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailabl
14
15
  import { akmIndex } from "./indexer";
15
16
  import { akmInit } from "./init";
16
17
  import { getDefaultStashDir } from "./paths";
18
+ import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus } from "./semantic-status";
17
19
  // ── Constants ───────────────────────────────────────────────────────────────
18
20
  /** Recommended GitHub repositories shown during setup. */
19
21
  const RECOMMENDED_GITHUB_REPOS = [
@@ -78,6 +80,22 @@ async function promptOrBack(fn) {
78
80
  return null;
79
81
  return result;
80
82
  }
83
+ /**
84
+ * Quick connectivity check. Returns true if we can reach a public
85
+ * endpoint within 3 seconds, false otherwise. Used to skip network-
86
+ * dependent setup steps gracefully when offline.
87
+ *
88
+ * @internal Exported for testing only.
89
+ */
90
+ export async function isOnline() {
91
+ try {
92
+ await fetch("https://dns.google", { signal: AbortSignal.timeout(3000) });
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
81
99
  function isRemoteEmbeddingConfig(embedding) {
82
100
  return isHttpUrl(embedding?.endpoint);
83
101
  }
@@ -99,10 +117,10 @@ export function describeSemanticSearchAssets(embedding) {
99
117
  export async function stepSemanticSearch(current, embedding) {
100
118
  const enabled = await prompt(() => p.confirm({
101
119
  message: "Enable semantic search?",
102
- initialValue: current.semanticSearch,
120
+ initialValue: current.semanticSearchMode !== "off",
103
121
  }));
104
122
  if (!enabled) {
105
- return { enabled: false, prepareAssets: false };
123
+ return { mode: "off", prepareAssets: false };
106
124
  }
107
125
  p.note(describeSemanticSearchAssets(embedding).join("\n"), "Semantic Search Assets");
108
126
  const prepareAssets = await prompt(() => p.confirm({
@@ -111,7 +129,7 @@ export async function stepSemanticSearch(current, embedding) {
111
129
  : "Download and verify semantic-search assets now?",
112
130
  initialValue: true,
113
131
  }));
114
- return { enabled: true, prepareAssets };
132
+ return { mode: "auto", prepareAssets };
115
133
  }
116
134
  async function prepareSemanticSearchAssets(config) {
117
135
  const remote = isRemoteEmbeddingConfig(config.embedding);
@@ -121,7 +139,9 @@ async function prepareSemanticSearchAssets(config) {
121
139
  const spin = p.spinner();
122
140
  spin.start("Installing @huggingface/transformers...");
123
141
  try {
142
+ const pkgRoot = path.resolve(import.meta.dir, "..");
124
143
  const proc = Bun.spawn(["bun", "add", "@huggingface/transformers"], {
144
+ cwd: pkgRoot,
125
145
  stdout: "pipe",
126
146
  stderr: "pipe",
127
147
  });
@@ -138,7 +158,7 @@ async function prepareSemanticSearchAssets(config) {
138
158
  p.log.warn(`Automatic install failed: ${msg}\n` +
139
159
  "Install it manually with: bun add @huggingface/transformers\n" +
140
160
  "Then re-run `akm setup` or `akm index --full --verbose`.");
141
- return false;
161
+ return { ok: false, reason: "missing-package", message: `Automatic install failed: ${msg}` };
142
162
  }
143
163
  }
144
164
  }
@@ -151,16 +171,18 @@ async function prepareSemanticSearchAssets(config) {
151
171
  spin.stop("Semantic-search assets could not be prepared.");
152
172
  if (result.reason === "remote-unreachable") {
153
173
  p.log.warn("The remote embedding endpoint is not reachable. Check your endpoint and credentials, then retry `akm index --full --verbose`.");
174
+ return { ok: false, reason: "remote-network", message: "The remote embedding endpoint is not reachable." };
154
175
  }
155
176
  else if (result.reason === "missing-package") {
156
177
  p.log.warn("@huggingface/transformers is not installed. Install it with: bun add @huggingface/transformers\n" +
157
178
  "Then re-run `akm setup` or `akm index --full --verbose`.");
179
+ return { ok: false, reason: "missing-package", message: "@huggingface/transformers is not installed." };
158
180
  }
159
181
  else {
160
182
  p.log.warn(`The local embedding model could not be downloaded: ${result.message}\n` +
161
183
  "Retry `akm index --full --verbose` after confirming local model downloads are permitted.");
184
+ return { ok: false, reason: "local-model-download", message: result.message };
162
185
  }
163
- return false;
164
186
  }
165
187
  spin.stop(remote ? "Remote embedding endpoint is ready." : "Local embedding model downloaded and ready.");
166
188
  let db;
@@ -182,7 +204,7 @@ async function prepareSemanticSearchAssets(config) {
182
204
  if (db)
183
205
  closeDatabase(db);
184
206
  }
185
- return true;
207
+ return { ok: true };
186
208
  }
187
209
  // ── Steps ───────────────────────────────────────────────────────────────────
188
210
  async function stepStashDir(current) {
@@ -250,11 +272,20 @@ async function stepOllama(current) {
250
272
  embedding = current.embedding;
251
273
  }
252
274
  else if (embChoice !== "local") {
253
- // Ask for dimension — different models produce different sizes
275
+ // Ask for dimension — different models produce different sizes.
276
+ // Common dimensions: nomic-embed-text=768, mxbai-embed-large=1024,
277
+ // all-minilm/bge-small=384. Default based on selected model.
278
+ const knownDims = {
279
+ nomic: 768,
280
+ mxbai: 1024,
281
+ minilm: 384,
282
+ bge: 384,
283
+ };
284
+ const guessedDim = Object.entries(knownDims).find(([k]) => embChoice.includes(k))?.[1] ?? 384;
254
285
  const dimChoice = await prompt(() => p.text({
255
- message: "Embedding dimension (must match your index; 384 is common for MiniLM/nomic):",
256
- placeholder: "384",
257
- defaultValue: "384",
286
+ message: `Embedding dimension for ${embChoice}:`,
287
+ placeholder: String(guessedDim),
288
+ defaultValue: String(guessedDim),
258
289
  validate: (v) => {
259
290
  const n = Number(v);
260
291
  if (!Number.isInteger(n) || n <= 0)
@@ -542,12 +573,18 @@ export async function runSetupWizard() {
542
573
  // Step 1: Stash directory
543
574
  p.log.step("Step 1: Stash Directory");
544
575
  const stashDir = await stepStashDir(current);
576
+ // Quick connectivity check — skip network-dependent steps when offline
577
+ const online = await isOnline();
578
+ if (!online) {
579
+ p.log.warn("No network connectivity detected. Skipping Ollama detection and remote embedding checks.\n" +
580
+ "Local-only setup will continue. Re-run `akm setup` when online for full configuration.");
581
+ }
545
582
  // Step 2: Ollama / Embedding / LLM
546
583
  p.log.step("Step 2: Embedding & LLM");
547
- const { embedding, llm } = await stepOllama(current);
584
+ const { embedding, llm } = online ? await stepOllama(current) : { embedding: current.embedding, llm: current.llm };
548
585
  // Step 3: Semantic search assets
549
586
  p.log.step("Step 3: Semantic Search");
550
- const semanticSearch = await stepSemanticSearch(current, embedding);
587
+ const semanticSearchMode = await stepSemanticSearch(current, embedding);
551
588
  // Step 4: Registries
552
589
  p.log.step("Step 4: Registries");
553
590
  const registries = await stepRegistries(current);
@@ -573,7 +610,7 @@ export async function runSetupWizard() {
573
610
  registries,
574
611
  stashes: allStashes.length > 0 ? allStashes : undefined,
575
612
  // Preserve existing fields
576
- semanticSearch: semanticSearch.enabled,
613
+ semanticSearchMode: semanticSearchMode.mode,
577
614
  installed: current.installed,
578
615
  output: current.output,
579
616
  };
@@ -583,7 +620,7 @@ export async function runSetupWizard() {
583
620
  `Stash directory: ${stashDir}`,
584
621
  `Embedding: ${embedding ? `${embedding.provider ?? "remote"} / ${embedding.model}` : "built-in local"}`,
585
622
  `LLM: ${llm ? `${llm.provider ?? "remote"} / ${llm.model}` : "disabled"}`,
586
- `Semantic search: ${semanticSearch.enabled ? "enabled" : "disabled"}`,
623
+ `Semantic search: ${semanticSearchMode.mode}`,
587
624
  `Registries: ${effectiveRegistries.filter((r) => r.enabled !== false).length} enabled`,
588
625
  `Stash sources: ${allStashes.length}`,
589
626
  ].join("\n"), "Configuration Summary");
@@ -597,18 +634,39 @@ export async function runSetupWizard() {
597
634
  saveConfig(newConfig);
598
635
  // Initialize stash directory
599
636
  await akmInit({ dir: stashDir });
600
- if (semanticSearch.enabled) {
601
- if (semanticSearch.prepareAssets) {
637
+ if (semanticSearchMode.mode === "off") {
638
+ clearSemanticStatus();
639
+ }
640
+ if (semanticSearchMode.mode === "auto") {
641
+ if (semanticSearchMode.prepareAssets) {
602
642
  const ready = await prepareSemanticSearchAssets(newConfig);
603
- if (!ready) {
604
- // Asset preparation failed: disable semantic search and persist the update.
605
- newConfig.semanticSearch = false;
606
- saveConfig(newConfig);
607
- p.log.warn("Semantic search has been disabled in the saved configuration. Re-run `akm setup` or `akm index --full --verbose` once the issue is resolved.");
643
+ if (!ready.ok) {
644
+ writeSemanticStatus({
645
+ status: "blocked",
646
+ reason: ready.reason,
647
+ message: ready.message,
648
+ providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
649
+ lastCheckedAt: new Date().toISOString(),
650
+ });
651
+ p.log.warn("Semantic search remains set to auto, but is currently blocked. Re-run `akm index --full --verbose` once the issue is resolved.");
652
+ }
653
+ else {
654
+ writeSemanticStatus({
655
+ status: "pending",
656
+ message: "Semantic prerequisites verified. Building the index to finish activation.",
657
+ providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
658
+ lastCheckedAt: new Date().toISOString(),
659
+ });
608
660
  }
609
661
  }
610
662
  else {
611
- p.log.info("Semantic search will be enabled, but asset preparation was skipped. Run `akm index --full --verbose` later to verify it.");
663
+ writeSemanticStatus({
664
+ status: "pending",
665
+ message: "Semantic search is enabled, but asset preparation was skipped.",
666
+ providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
667
+ lastCheckedAt: new Date().toISOString(),
668
+ });
669
+ p.log.info("Semantic search is set to auto, but asset preparation was skipped. Run `akm index --full --verbose` later to verify it.");
612
670
  }
613
671
  }
614
672
  // Build search index
@@ -618,7 +676,7 @@ export async function runSetupWizard() {
618
676
  try {
619
677
  const indexResult = await akmIndex({ stashDir });
620
678
  spin.stop(`Indexed ${indexResult.totalEntries} assets.`);
621
- if (newConfig.semanticSearch) {
679
+ if (newConfig.semanticSearchMode === "auto") {
622
680
  if (indexResult.verification.ok) {
623
681
  p.log.success(indexResult.verification.message);
624
682
  }
@@ -633,6 +691,15 @@ export async function runSetupWizard() {
633
691
  catch (err) {
634
692
  spin.stop("Indexing failed — you can run `akm index` manually later.");
635
693
  p.log.warn(String(err));
694
+ if (newConfig.semanticSearchMode === "auto") {
695
+ writeSemanticStatus({
696
+ status: "blocked",
697
+ reason: "index-failed",
698
+ message: String(err),
699
+ providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
700
+ lastCheckedAt: new Date().toISOString(),
701
+ });
702
+ }
636
703
  }
637
704
  // API key reminder
638
705
  if (embedding?.apiKey === undefined && embedding?.provider !== "ollama") {
package/dist/stash-add.js CHANGED
@@ -7,24 +7,6 @@ import { akmIndex } from "./indexer";
7
7
  import { upsertLockEntry } from "./lockfile";
8
8
  import { detectStashRoot, installRegistryRef, upsertInstalledRegistryEntry } from "./registry-install";
9
9
  import { parseRegistryRef } from "./registry-resolve";
10
- export async function akmKitAdd(input) {
11
- const ref = input.ref.trim();
12
- if (!ref)
13
- throw new UsageError("Registry ref is required. " + "Examples: `akm kit add @scope/kit`, `akm kit add github:owner/repo`");
14
- const stashDir = resolveStashDir();
15
- try {
16
- const parsed = parseRegistryRef(ref);
17
- if (parsed.source === "local") {
18
- throw new UsageError(`Local directories should be added as stashes, not kits. Use \`akm stash add ${ref}\` instead.`);
19
- }
20
- }
21
- catch (err) {
22
- if (err instanceof UsageError)
23
- throw err;
24
- // Not a local ref — fall through to registry install
25
- }
26
- return addRegistryKit(ref, stashDir);
27
- }
28
10
  export async function akmAdd(input) {
29
11
  const ref = input.ref.trim();
30
12
  if (!ref)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.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": [
@@ -44,6 +44,7 @@
44
44
  "check": "bun run lint && bunx tsc --noEmit && bun test ./tests",
45
45
  "check:changed": "bun test tests/output-baseline.test.ts tests/e2e.test.ts tests/stash-search.test.ts && bun run lint && bunx tsc --noEmit",
46
46
  "test": "bun test ./tests",
47
+ "release:check": "./tests/release-check.sh",
47
48
  "lint": "bunx biome check src/ tests/",
48
49
  "lint:fix": "bunx biome check --write src/ tests/",
49
50
  "format": "bunx biome format --write src/ tests/",
@@ -60,6 +61,7 @@
60
61
  "typescript": "^5.9.3"
61
62
  },
62
63
  "optionalDependencies": {
64
+ "@huggingface/transformers": "^3.8.1",
63
65
  "sqlite-vec": "0.1.7-alpha.2"
64
66
  },
65
67
  "engines": {
@@ -68,7 +70,6 @@
68
70
  "dependencies": {
69
71
  "@clack/prompts": "^1.1.0",
70
72
  "citty": "^0.2.1",
71
- "@huggingface/transformers": "^3.8.1",
72
73
  "yaml": "^2.8.2"
73
74
  }
74
75
  }