akm-cli 0.9.0-beta.53 → 0.9.0-beta.54

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 (35) hide show
  1. package/dist/cli/clack.js +56 -0
  2. package/dist/cli/confirm.js +1 -1
  3. package/dist/commands/health/html-report.js +33 -10
  4. package/dist/commands/health.js +154 -21
  5. package/dist/commands/improve/outcome-loop.js +18 -16
  6. package/dist/commands/improve/preparation.js +19 -3
  7. package/dist/commands/read/curate.js +4 -4
  8. package/dist/commands/read/search-cli.js +6 -4
  9. package/dist/commands/read/search.js +7 -3
  10. package/dist/commands/read/show.js +3 -5
  11. package/dist/commands/sources/add-cli.js +1 -1
  12. package/dist/commands/sources/init.js +12 -0
  13. package/dist/commands/sources/stash-cli.js +1 -1
  14. package/dist/commands/tasks/default-tasks.js +12 -0
  15. package/dist/core/config/config.js +12 -0
  16. package/dist/core/warn.js +21 -0
  17. package/dist/indexer/db/db.js +6 -0
  18. package/dist/indexer/ensure-index.js +3 -2
  19. package/dist/indexer/index-writer-lock.js +9 -0
  20. package/dist/indexer/indexer.js +16 -4
  21. package/dist/indexer/read-preflight.js +23 -0
  22. package/dist/indexer/walk/walker.js +21 -13
  23. package/dist/integrations/agent/detect.js +9 -0
  24. package/dist/integrations/agent/index.js +1 -1
  25. package/dist/llm/client.js +12 -0
  26. package/dist/llm/embedder.js +26 -2
  27. package/dist/llm/embedders/local.js +7 -1
  28. package/dist/scripts/migrate-storage.js +26 -2
  29. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +5 -1
  30. package/dist/setup/detect.js +9 -0
  31. package/dist/setup/registry-stash-loader.js +12 -0
  32. package/dist/setup/setup.js +1 -1
  33. package/dist/tasks/backends/index.js +9 -0
  34. package/dist/tasks/runner.js +9 -0
  35. package/package.json +2 -2
@@ -28,10 +28,10 @@ import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../co
28
28
  import { appendEvent, readEvents } from "../../core/events.js";
29
29
  import { closeDatabase, computeBodyHash, findEntryIdByRef, openExistingDatabase } from "../../indexer/db/db.js";
30
30
  import { hasGraphData } from "../../indexer/db/graph-db.js";
31
- import { ensureIndex } from "../../indexer/ensure-index.js";
32
31
  import { listRelatedPathsForFile } from "../../indexer/graph/graph-boost.js";
33
32
  import { extractGraphForSingleFile } from "../../indexer/graph/graph-extraction.js";
34
33
  import { lookup } from "../../indexer/indexer.js";
34
+ import { ensurePrimaryIndexForRead, resolveReadSources } from "../../indexer/read-preflight.js";
35
35
  import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../../indexer/search/search-source.js";
36
36
  import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
37
37
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../../indexer/walk/file-context.js";
@@ -147,10 +147,8 @@ export async function akmShowUnified(input) {
147
147
  }
148
148
  }
149
149
  // Auto-index when stale so the index is current before lookup.
150
- const allSources = resolveSourceEntries();
151
- if (allSources.length > 0) {
152
- await ensureIndex(allSources[0].path);
153
- }
150
+ const { primarySource } = resolveReadSources();
151
+ await ensurePrimaryIndexForRead(primarySource);
154
152
  // Try local filesystem (FTS5 index lookup)
155
153
  const result = await showLocal(input);
156
154
  // Scope filter narrows resolution: if --scope was supplied, the asset's
@@ -3,8 +3,8 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import * as p from "@clack/prompts";
7
6
  import { defineCommand } from "citty";
7
+ import * as p from "../../cli/clack.js";
8
8
  import { output, runWithJsonErrors } from "../../cli/shared.js";
9
9
  import { UsageError } from "../../core/errors.js";
10
10
  import { appendEvent } from "../../core/events.js";
@@ -55,7 +55,19 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
55
55
  function isUnderTestRunner() {
56
56
  return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
57
57
  }
58
+ // ── Test seam ────────────────────────────────────────────────────────────────
59
+ // Swap-and-restore override. Inert in production; only tests call the setter.
60
+ let akmInitOverride;
61
+ /** TEST-ONLY. Swap the implementation of `akmInit`; pass undefined to restore. */
62
+ export function _setAkmInitForTests(fake) {
63
+ akmInitOverride = fake;
64
+ }
58
65
  export async function akmInit(options) {
66
+ if (akmInitOverride)
67
+ return akmInitOverride(options);
68
+ return akmInitReal(options);
69
+ }
70
+ async function akmInitReal(options) {
59
71
  const dirExplicitlyProvided = options?.dir != null;
60
72
  const setDefault = options?.setDefault === true;
61
73
  const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
@@ -25,8 +25,8 @@
25
25
  * SIGINT/SIGTERM handlers in a try/finally — left byte-for-byte untouched.
26
26
  */
27
27
  import path from "node:path";
28
- import * as p from "@clack/prompts";
29
28
  import { defineCommand } from "citty";
29
+ import * as p from "../../cli/clack.js";
30
30
  import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
31
31
  import { assertFlatAssetName } from "../../core/asset/asset-create.js";
32
32
  import { isHttpUrl } from "../../core/common.js";
@@ -76,12 +76,19 @@ const DEFAULT_DEPS = {
76
76
  list: akmTasksList,
77
77
  add: akmTasksAdd,
78
78
  };
79
+ let defaultTasksOverrides;
80
+ /** TEST-ONLY. Swap the CI/server/register functions; pass undefined to restore. */
81
+ export function _setDefaultTasksForTests(fakes) {
82
+ defaultTasksOverrides = fakes;
83
+ }
79
84
  /**
80
85
  * Decide whether `akm setup` is running in a CI environment, where it must
81
86
  * register NO scheduled tasks. Mirrors the common `CI=true` convention used by
82
87
  * GitHub Actions, GitLab CI, CircleCI, etc.
83
88
  */
84
89
  export function isCiEnvironment(env = process.env) {
90
+ if (defaultTasksOverrides?.isCiEnvironment)
91
+ return defaultTasksOverrides.isCiEnvironment(env);
85
92
  const ci = env.CI;
86
93
  if (ci === undefined || ci === null)
87
94
  return false;
@@ -95,6 +102,8 @@ export function isCiEnvironment(env = process.env) {
95
102
  * Used as the default when setup is non-interactive (no TTY / --yes / CI).
96
103
  */
97
104
  export function detectServerDefault() {
105
+ if (defaultTasksOverrides?.detectServerDefault)
106
+ return defaultTasksOverrides.detectServerDefault();
98
107
  if (os.platform() !== "linux")
99
108
  return false;
100
109
  // A laptop exposes a battery under /sys/class/power_supply/BAT*. Absence of
@@ -121,6 +130,9 @@ export function detectServerDefault() {
121
130
  * never re-disable a user-enabled task).
122
131
  */
123
132
  export async function registerDefaultTasks(options = {}) {
133
+ if (defaultTasksOverrides?.registerDefaultTasks) {
134
+ return defaultTasksOverrides.registerDefaultTasks(options);
135
+ }
124
136
  if (isCiEnvironment()) {
125
137
  return { skipped: true, reason: "ci", created: [], existing: [], toggled: [] };
126
138
  }
@@ -278,7 +278,19 @@ export function loadConfig() {
278
278
  warnIfProjectConfigPresent(process.cwd());
279
279
  return loadUserConfig();
280
280
  }
281
+ let saveConfigOverride;
282
+ /** TEST-ONLY. Swap the implementation of `saveConfig`; pass undefined to restore. */
283
+ export function _setSaveConfigForTests(fake) {
284
+ saveConfigOverride = fake;
285
+ }
281
286
  export function saveConfig(config) {
287
+ if (saveConfigOverride) {
288
+ saveConfigOverride(config);
289
+ return;
290
+ }
291
+ saveConfigReal(config);
292
+ }
293
+ function saveConfigReal(config) {
282
294
  cachedConfig = undefined;
283
295
  const configPath = getConfigPath();
284
296
  const dir = path.dirname(configPath);
package/dist/core/warn.js CHANGED
@@ -17,6 +17,11 @@ import path from "node:path";
17
17
  let quiet = false;
18
18
  let verbose = false;
19
19
  let logFilePath;
20
+ let sinkOverride;
21
+ /** TEST-ONLY. Swap the output sink; pass undefined to restore real output. */
22
+ export function _setWarnSinkForTests(fake) {
23
+ sinkOverride = fake;
24
+ }
20
25
  export function setQuiet(value) {
21
26
  quiet = value;
22
27
  }
@@ -96,6 +101,10 @@ function appendToLogFile(level, args) {
96
101
  * Use for progress counters and status lines (replaces console.error used for progress).
97
102
  */
98
103
  export function info(...args) {
104
+ if (sinkOverride) {
105
+ sinkOverride("info", args);
106
+ return;
107
+ }
99
108
  appendToLogFile("INFO", args);
100
109
  if (!quiet) {
101
110
  console.warn(...args);
@@ -107,6 +116,10 @@ export function info(...args) {
107
116
  * Drop-in replacement for console.warn() across the codebase.
108
117
  */
109
118
  export function warn(...args) {
119
+ if (sinkOverride) {
120
+ sinkOverride("warn", args);
121
+ return;
122
+ }
110
123
  appendToLogFile("WARN", args);
111
124
  if (!quiet) {
112
125
  console.warn(...args);
@@ -118,6 +131,10 @@ export function warn(...args) {
118
131
  * Drop-in replacement for console.error() used for diagnostic failures.
119
132
  */
120
133
  export function error(...args) {
134
+ if (sinkOverride) {
135
+ sinkOverride("error", args);
136
+ return;
137
+ }
121
138
  appendToLogFile("ERROR", args);
122
139
  if (!quiet) {
123
140
  console.error(...args);
@@ -129,6 +146,10 @@ export function error(...args) {
129
146
  * default verbosity (e.g. registry-content workflow validation errors).
130
147
  */
131
148
  export function warnVerbose(...args) {
149
+ if (sinkOverride) {
150
+ sinkOverride("warnVerbose", args);
151
+ return;
152
+ }
132
153
  if (isVerbose()) {
133
154
  warn(...args);
134
155
  }
@@ -1633,6 +1633,11 @@ function bareRef(ref) {
1633
1633
  * entry_ref populated (see logCurateEvent), so curation is a real retrieval
1634
1634
  * signal here. Legacy summary-only curate rows with a NULL entry_ref simply
1635
1635
  * contribute nothing.
1636
+ *
1637
+ * Machine-sourced events (`source` = 'improve' or 'task') are EXCLUDED: this
1638
+ * count feeds salience/ranking, and pipeline probe traffic counting as demand
1639
+ * creates a self-reinforcing loop (meta-review 05 DRIFT-6). NULL sources
1640
+ * (pre-column rows) count as user demand.
1636
1641
  */
1637
1642
  export function getRetrievalCounts(db, refs) {
1638
1643
  if (refs.length === 0)
@@ -1671,6 +1676,7 @@ export function getRetrievalCounts(db, refs) {
1671
1676
  FROM usage_events
1672
1677
  WHERE event_type IN ('search','show','curate')
1673
1678
  AND entry_ref IS NOT NULL
1679
+ AND (source IS NULL OR source NOT IN ('improve','task'))
1674
1680
  AND CASE
1675
1681
  WHEN instr(entry_ref, '//') > 0
1676
1682
  THEN substr(entry_ref, instr(entry_ref, '//') + 2)
@@ -185,7 +185,7 @@ async function runInlineReindex(stashDir) {
185
185
  }
186
186
  catch (error) {
187
187
  warn("Auto-index failed, proceeding with existing index:", error instanceof Error ? error.message : String(error));
188
- return true;
188
+ return false;
189
189
  }
190
190
  }
191
191
  /**
@@ -200,7 +200,8 @@ async function runInlineReindex(stashDir) {
200
200
  * trigger and waits for it. Use this for callers like `improve` whose
201
201
  * planning logic depends on a current `entries` table in the same process.
202
202
  *
203
- * Returns `true` if an index run was attempted.
203
+ * Returns `true` only when an inline index run succeeds.
204
+ * A rebuild attempt that fails (throws) resolves to `false`.
204
205
  */
205
206
  export async function ensureIndex(stashDir, options = {}) {
206
207
  if (options.mode === "blocking") {
@@ -7,6 +7,7 @@ import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "
7
7
  import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
8
8
  const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
9
9
  const INDEX_WRITER_WAIT_MS = 100;
10
+ const DEFAULT_INDEX_WRITER_MAX_WAIT_MS = 10 * 60 * 1000;
10
11
  const heldLocks = new Map();
11
12
  function buildPayload(purpose, pid = process.pid) {
12
13
  return JSON.stringify({
@@ -49,6 +50,8 @@ function retainHeldLock(lockPath) {
49
50
  export async function acquireIndexWriterLease(options) {
50
51
  const mode = options.mode ?? "wait";
51
52
  const lockPath = getIndexWriterLockPath();
53
+ const startedAt = Date.now();
54
+ const maxWaitMs = options.maxWaitMs ?? DEFAULT_INDEX_WRITER_MAX_WAIT_MS;
52
55
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
53
56
  if (heldLocks.has(lockPath)) {
54
57
  return retainHeldLock(lockPath);
@@ -68,6 +71,12 @@ export async function acquireIndexWriterLease(options) {
68
71
  }
69
72
  if (mode === "try")
70
73
  return undefined;
74
+ // Held by another live process. Time out only *after* a real acquisition
75
+ // attempt, so a caller with maxWaitMs:0 still gets one chance at a free lock
76
+ // instead of throwing before it ever tries.
77
+ if (maxWaitMs >= 0 && Date.now() - startedAt >= maxWaitMs) {
78
+ throw new Error(`timed out waiting for index writer lease for ${options.purpose}`);
79
+ }
71
80
  await delay(INDEX_WRITER_WAIT_MS);
72
81
  }
73
82
  }
@@ -118,7 +118,7 @@ async function runWalkPhase(ctx) {
118
118
  ctx.timing.tWalkEnd = Date.now();
119
119
  throwIfAborted(signal);
120
120
  // LLM enrichment for directories that need it
121
- await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, true, reEnrich);
121
+ await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, reEnrich);
122
122
  onProgress({
123
123
  phase: "llm",
124
124
  message: resolveIndexPassLLM("enrichment", config)
@@ -226,7 +226,19 @@ function runCleanPass(db, dryRun) {
226
226
  };
227
227
  }
228
228
  // ── Indexer ──────────────────────────────────────────────────────────────────
229
+ // ── Test seam ────────────────────────────────────────────────────────────────
230
+ // Swap-and-restore override. Inert in production; only tests call the setter.
231
+ let akmIndexOverride;
232
+ /** TEST-ONLY. Swap the implementation of `akmIndex`; pass undefined to restore. */
233
+ export function _setAkmIndexForTests(fake) {
234
+ akmIndexOverride = fake;
235
+ }
229
236
  export async function akmIndex(options) {
237
+ if (akmIndexOverride)
238
+ return akmIndexOverride(options);
239
+ return akmIndexReal(options);
240
+ }
241
+ async function akmIndexReal(options) {
230
242
  return withIndexWriterLease({ purpose: "akm-index", signal: options?.signal }, async () => {
231
243
  const stashDir = options?.stashDir || resolveStashDir();
232
244
  const onProgress = options?.onProgress ?? (() => { });
@@ -640,7 +652,7 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
640
652
  insertTransaction();
641
653
  return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
642
654
  }
643
- async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, _enrich = false, reEnrich = false) {
655
+ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, reEnrich = false) {
644
656
  // Resolve per-pass LLM config via the unified shim. Returns undefined when
645
657
  // either no `akm.llm` is configured or the user opted this pass out via
646
658
  // `index.enrichment.llm = false`. (#208)
@@ -977,7 +989,7 @@ function resolveIndexedFiles(dirPath, files, stash) {
977
989
  for (const entry of stash.entries) {
978
990
  const entryPath = entry.filename
979
991
  ? path.join(dirPath, entry.filename)
980
- : matchEntryToFile(entry.name, fileBasenameMap, files);
992
+ : matchEntryToFile(entry.name, fileBasenameMap);
981
993
  if (entryPath)
982
994
  resolved.add(entryPath);
983
995
  }
@@ -1096,7 +1108,7 @@ export function buildFileBasenameMap(files) {
1096
1108
  * try matching the last segment
1097
1109
  * 3. No implicit file fallback: ambiguous legacy entries are skipped
1098
1110
  */
1099
- export function matchEntryToFile(entryName, fileMap, _files) {
1111
+ export function matchEntryToFile(entryName, fileMap) {
1100
1112
  // Exact match on entry name
1101
1113
  const exact = fileMap.get(entryName);
1102
1114
  if (exact)
@@ -0,0 +1,23 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { ensureIndex } from "./ensure-index.js";
5
+ import { resolveSourceEntries } from "./search/search-source.js";
6
+ /** Resolve the active read sources using the same resolution rules as search/show. */
7
+ export function resolveReadSources(overrideStashDir, existingConfig) {
8
+ const sources = resolveSourceEntries(overrideStashDir, existingConfig);
9
+ return { sources, primarySource: sources[0] };
10
+ }
11
+ /** Ensure the primary source index is readable for reads, when a primary exists. */
12
+ export async function ensurePrimaryIndexForRead(primarySource) {
13
+ if (!primarySource?.path)
14
+ return false;
15
+ return ensureIndex(primarySource.path);
16
+ }
17
+ /**
18
+ * Convenience helper for callers that only need to ensure a read index from a
19
+ * configured stash path and default config.
20
+ */
21
+ export async function ensurePrimaryIndexFromConfig(overrideStashDir, existingConfig) {
22
+ return ensurePrimaryIndexForRead(resolveReadSources(overrideStashDir, existingConfig).primarySource);
23
+ }
@@ -148,20 +148,28 @@ function isInsideGitRepo(dir) {
148
148
  * read (e.g. permission errors).
149
149
  */
150
150
  export function* walkMarkdownFiles(root) {
151
- let entries;
152
- try {
153
- entries = fs.readdirSync(root, { withFileTypes: true });
154
- }
155
- catch {
156
- return;
157
- }
158
- for (const entry of entries) {
159
- const full = path.join(root, entry.name);
160
- if (entry.isDirectory()) {
161
- yield* walkMarkdownFiles(full);
151
+ const stack = [root];
152
+ while (stack.length > 0) {
153
+ const current = stack.pop();
154
+ if (!current)
155
+ continue;
156
+ let entries;
157
+ try {
158
+ entries = fs.readdirSync(current, { withFileTypes: true });
162
159
  }
163
- else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
164
- yield full;
160
+ catch {
161
+ continue;
162
+ }
163
+ for (const entry of entries) {
164
+ const full = path.join(current, entry.name);
165
+ if (entry.isSymbolicLink())
166
+ continue;
167
+ if (entry.isDirectory()) {
168
+ stack.push(full);
169
+ }
170
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
171
+ yield full;
172
+ }
165
173
  }
166
174
  }
167
175
  }
@@ -55,6 +55,11 @@ export function defaultWhich(bin, envSource = process.env) {
55
55
  }
56
56
  return undefined;
57
57
  }
58
+ let detectOverrides;
59
+ /** TEST-ONLY. Swap the detection implementations; pass undefined to restore. */
60
+ export function _setAgentDetectForTests(fakes) {
61
+ detectOverrides = fakes;
62
+ }
58
63
  /**
59
64
  * Probe every resolvable agent profile (built-ins plus user overrides)
60
65
  * for an installed CLI.
@@ -64,6 +69,8 @@ export function defaultWhich(bin, envSource = process.env) {
64
69
  * @param whichFn Binary lookup. Tests should inject a stub.
65
70
  */
66
71
  export function detectAgentCliProfiles(agent, whichFn = defaultWhich) {
72
+ if (detectOverrides?.detectAgentCliProfiles)
73
+ return detectOverrides.detectAgentCliProfiles(agent, whichFn);
67
74
  const profiles = listResolvedAgentProfiles(agent);
68
75
  return profiles.map((profile) => probeProfile(profile, whichFn));
69
76
  }
@@ -87,6 +94,8 @@ function probeProfile(profile, whichFn) {
87
94
  * writing `agent.default`.
88
95
  */
89
96
  export function pickDefaultAgentProfile(results, existingDefault) {
97
+ if (detectOverrides?.pickDefaultAgentProfile)
98
+ return detectOverrides.pickDefaultAgentProfile(results, existingDefault);
90
99
  if (existingDefault) {
91
100
  const match = results.find((r) => r.name === existingDefault && r.available);
92
101
  if (match)
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  export { getCommandBuilder } from "./builders.js";
5
5
  export { DEFAULT_AGENT_TIMEOUT_MS, listAgentProfileNames, listResolvedAgentProfiles, requireAgentProfile, resolveAgentProfile, resolveDefaultProfileName, resolveProfileFromConfig, } from "./config.js";
6
- export { defaultWhich, detectAgentCliProfiles, pickDefaultAgentProfile } from "./detect.js";
6
+ export { _setAgentDetectForTests, defaultWhich, detectAgentCliProfiles, pickDefaultAgentProfile } from "./detect.js";
7
7
  export { listBuiltinModelAliases, resolveModel } from "./model-aliases.js";
8
8
  export { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles.js";
9
9
  export { buildProposePrompt, buildReflectPrompt, buildSchemaRepairPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "./prompts.js";
@@ -165,7 +165,19 @@ function isRetryable(err) {
165
165
  }
166
166
  return false;
167
167
  }
168
+ // ── Test seam ────────────────────────────────────────────────────────────────
169
+ // Swap-and-restore override. Inert in production; only tests call the setter.
170
+ let chatCompletionOverride;
171
+ /** TEST-ONLY. Swap the implementation of `chatCompletion`; pass undefined to restore. */
172
+ export function _setChatCompletionForTests(fake) {
173
+ chatCompletionOverride = fake;
174
+ }
168
175
  export async function chatCompletion(config, messages, options) {
176
+ if (chatCompletionOverride)
177
+ return chatCompletionOverride(config, messages, options);
178
+ return chatCompletionReal(config, messages, options);
179
+ }
180
+ async function chatCompletionReal(config, messages, options) {
169
181
  const effectiveTimeoutMs = options?.timeoutMs ?? config.timeoutMs ?? 120_000;
170
182
  const started = Date.now();
171
183
  try {
@@ -3,11 +3,26 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { embedCacheKey, getCachedEmbedding, setCachedEmbedding } from "./embedders/cache.js";
5
5
  import { DETERMINISTIC_EMBED_MODEL_ID, deterministicEmbed, isDeterministicEmbedEnabled, } from "./embedders/deterministic.js";
6
- import { DEFAULT_LOCAL_MODEL, isTransformersAvailable, LocalEmbedder } from "./embedders/local.js";
6
+ import { DEFAULT_LOCAL_MODEL, isTransformersAvailable as isTransformersAvailableReal, LocalEmbedder, } from "./embedders/local.js";
7
7
  import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote.js";
8
8
  // ── Re-exports (public API) ─────────────────────────────────────────────────
9
9
  export { clearEmbeddingCache } from "./embedders/cache.js";
10
- export { DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "./embedders/local.js";
10
+ export { _setTransformersLoaderForTests, DEFAULT_LOCAL_MODEL } from "./embedders/local.js";
11
+ let embedderOverrides;
12
+ /** TEST-ONLY. Swap embedder implementations; pass undefined to restore. */
13
+ export function _setEmbedderForTests(fakes) {
14
+ embedderOverrides = fakes;
15
+ }
16
+ /**
17
+ * Check whether the @huggingface/transformers package is importable.
18
+ * Delegating wrapper around `./embedders/local`'s probe so tests can swap it
19
+ * via {@link _setEmbedderForTests}.
20
+ */
21
+ export function isTransformersAvailable() {
22
+ if (embedderOverrides?.isTransformersAvailable)
23
+ return embedderOverrides.isTransformersAvailable();
24
+ return isTransformersAvailableReal();
25
+ }
11
26
  // ── Singleton local embedder ────────────────────────────────────────────────
12
27
  // `_localEmbedder` is an intentional module-level singleton but constructed
13
28
  // lazily on first use. The underlying @huggingface/transformers pipeline is
@@ -40,6 +55,8 @@ export function resetLocalEmbedder() {
40
55
  * and embedding config. Repeated identical queries return the cached vector.
41
56
  */
42
57
  export async function embed(text, embeddingConfig, signal) {
58
+ if (embedderOverrides?.embed)
59
+ return embedderOverrides.embed(text, embeddingConfig, signal);
43
60
  // Deterministic mode (env-gated, test/bench only): model-free, stable.
44
61
  if (isDeterministicEmbedEnabled()) {
45
62
  return deterministicEmbed(text);
@@ -61,6 +78,8 @@ export async function embed(text, embeddingConfig, signal) {
61
78
  * which processes texts in chunks of 32 for genuine batched inference.
62
79
  */
63
80
  export async function embedBatch(texts, embeddingConfig, signal) {
81
+ if (embedderOverrides?.embedBatch)
82
+ return embedderOverrides.embedBatch(texts, embeddingConfig, signal);
64
83
  if (texts.length === 0)
65
84
  return [];
66
85
  // Deterministic mode (env-gated, test/bench only): model-free, stable.
@@ -104,6 +123,8 @@ export { cosineSimilarity } from "./embedders/types.js";
104
123
  * - No config: use `DEFAULT_LOCAL_MODEL` (the shared singleton model).
105
124
  */
106
125
  export function resolveEmbeddingModelId(embeddingConfig) {
126
+ if (embedderOverrides?.resolveEmbeddingModelId)
127
+ return embedderOverrides.resolveEmbeddingModelId(embeddingConfig);
107
128
  if (isDeterministicEmbedEnabled())
108
129
  return DETERMINISTIC_EMBED_MODEL_ID;
109
130
  if (!embeddingConfig)
@@ -117,6 +138,9 @@ export function resolveEmbeddingModelId(embeddingConfig) {
117
138
  * Check whether embedding is available with a detailed reason on failure.
118
139
  */
119
140
  export async function checkEmbeddingAvailability(embeddingConfig) {
141
+ if (embedderOverrides?.checkEmbeddingAvailability) {
142
+ return embedderOverrides.checkEmbeddingAvailability(embeddingConfig);
143
+ }
120
144
  // Deterministic mode (env-gated): always available — no model, no network.
121
145
  if (isDeterministicEmbedEnabled()) {
122
146
  return { available: true };
@@ -28,6 +28,12 @@ function isBatchTensor(v) {
28
28
  Array.isArray(v.dims) &&
29
29
  v.dims.length >= 2);
30
30
  }
31
+ const realTransformersLoader = () => import("@huggingface/transformers");
32
+ let transformersLoader = realTransformersLoader;
33
+ /** TEST-ONLY. Swap the transformers module loader; pass undefined to restore. */
34
+ export function _setTransformersLoaderForTests(fake) {
35
+ transformersLoader = fake ?? realTransformersLoader;
36
+ }
31
37
  const LOCAL_EMBEDDER_DTYPE = "fp32";
32
38
  const LOCAL_EMBEDDER_FALLBACK_DTYPE = "auto";
33
39
  /**
@@ -180,7 +186,7 @@ export class LocalEmbedder {
180
186
  }
181
187
  let pipeline;
182
188
  try {
183
- const mod = await import("@huggingface/transformers");
189
+ const mod = await transformersLoader();
184
190
  pipeline = mod.pipeline;
185
191
  }
186
192
  catch (importError) {
@@ -7595,23 +7595,35 @@ function appendToLogFile(level, args) {
7595
7595
  }
7596
7596
  }
7597
7597
  function warn(...args) {
7598
+ if (sinkOverride) {
7599
+ sinkOverride("warn", args);
7600
+ return;
7601
+ }
7598
7602
  appendToLogFile("WARN", args);
7599
7603
  if (!quiet) {
7600
7604
  console.warn(...args);
7601
7605
  }
7602
7606
  }
7603
7607
  function error(...args) {
7608
+ if (sinkOverride) {
7609
+ sinkOverride("error", args);
7610
+ return;
7611
+ }
7604
7612
  appendToLogFile("ERROR", args);
7605
7613
  if (!quiet) {
7606
7614
  console.error(...args);
7607
7615
  }
7608
7616
  }
7609
7617
  function warnVerbose(...args) {
7618
+ if (sinkOverride) {
7619
+ sinkOverride("warnVerbose", args);
7620
+ return;
7621
+ }
7610
7622
  if (isVerbose()) {
7611
7623
  warn(...args);
7612
7624
  }
7613
7625
  }
7614
- var quiet = false, verbose = false, logFilePath;
7626
+ var quiet = false, verbose = false, logFilePath, sinkOverride;
7615
7627
  var init_warn = () => {};
7616
7628
 
7617
7629
  // src/indexer/walk/file-context.ts
@@ -16451,6 +16463,7 @@ __export(exports_config, {
16451
16463
  getIndexPassConfig: () => getIndexPassConfig,
16452
16464
  getEffectiveRegistries: () => getEffectiveRegistries,
16453
16465
  getDefaultLlmConfig: () => getDefaultLlmConfig,
16466
+ _setSaveConfigForTests: () => _setSaveConfigForTests,
16454
16467
  VALID_HARNESS_IDS: () => VALID_HARNESS_IDS,
16455
16468
  FEEDBACK_FAILURE_MODES: () => FEEDBACK_FAILURE_MODES,
16456
16469
  DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE: () => DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE,
@@ -16593,7 +16606,17 @@ function loadConfig() {
16593
16606
  warnIfProjectConfigPresent(process.cwd());
16594
16607
  return loadUserConfig();
16595
16608
  }
16609
+ function _setSaveConfigForTests(fake) {
16610
+ saveConfigOverride = fake;
16611
+ }
16596
16612
  function saveConfig(config) {
16613
+ if (saveConfigOverride) {
16614
+ saveConfigOverride(config);
16615
+ return;
16616
+ }
16617
+ saveConfigReal(config);
16618
+ }
16619
+ function saveConfigReal(config) {
16597
16620
  cachedConfig = undefined;
16598
16621
  const configPath = getConfigPath();
16599
16622
  const dir = path11.dirname(configPath);
@@ -16762,7 +16785,7 @@ function isFile(filePath) {
16762
16785
  return false;
16763
16786
  }
16764
16787
  }
16765
- var FEEDBACK_FAILURE_MODES, DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE = 4, GRAPH_EXTRACTION_CHARS_PER_BODY = 1500, DEFAULT_CONFIG, PROJECT_CONFIG_RELATIVE_PATH, cachedConfig, INDEX_RESERVED_KEYS, PROJECT_CONFIG_DEPRECATION_WARNED;
16788
+ var FEEDBACK_FAILURE_MODES, DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE = 4, GRAPH_EXTRACTION_CHARS_PER_BODY = 1500, DEFAULT_CONFIG, PROJECT_CONFIG_RELATIVE_PATH, cachedConfig, saveConfigOverride, INDEX_RESERVED_KEYS, PROJECT_CONFIG_DEPRECATION_WARNED;
16766
16789
  var init_config2 = __esm(() => {
16767
16790
  init_errors();
16768
16791
  init_config_io();
@@ -17920,6 +17943,7 @@ function getRetrievalCounts(db, refs) {
17920
17943
  FROM usage_events
17921
17944
  WHERE event_type IN ('search','show','curate')
17922
17945
  AND entry_ref IS NOT NULL
17946
+ AND (source IS NULL OR source NOT IN ('improve','task'))
17923
17947
  AND CASE
17924
17948
  WHEN instr(entry_ref, '//') > 0
17925
17949
  THEN substr(entry_ref, instr(entry_ref, '//') + 2)
@@ -53,12 +53,16 @@ function appendToLogFile(level, args) {
53
53
  }
54
54
  }
55
55
  function warn(...args) {
56
+ if (sinkOverride) {
57
+ sinkOverride("warn", args);
58
+ return;
59
+ }
56
60
  appendToLogFile("WARN", args);
57
61
  if (!quiet) {
58
62
  console.warn(...args);
59
63
  }
60
64
  }
61
- var quiet = false, logFilePath;
65
+ var quiet = false, logFilePath, sinkOverride;
62
66
  var init_warn = () => {};
63
67
 
64
68
  // node_modules/dotenv/lib/main.js
@@ -14,6 +14,11 @@ import { defaultWhich } from "../integrations/agent/detect.js";
14
14
  import { SESSION_LOG_HARNESSES } from "../integrations/harnesses/index.js";
15
15
  import { spawn } from "../runtime.js";
16
16
  import { detectHarnessConfigs } from "./harness-config-import.js";
17
+ let detectOverrides;
18
+ /** TEST-ONLY. Swap the network/host probes; pass undefined to restore. */
19
+ export function _setDetectForTests(fakes) {
20
+ detectOverrides = fakes;
21
+ }
17
22
  // ── Ollama Detection ────────────────────────────────────────────────────────
18
23
  const OLLAMA_BASE = "http://localhost:11434";
19
24
  /**
@@ -23,6 +28,8 @@ const OLLAMA_BASE = "http://localhost:11434";
23
28
  * via subprocess. Returns available models sorted alphabetically.
24
29
  */
25
30
  export async function detectOllama() {
31
+ if (detectOverrides?.detectOllama)
32
+ return detectOverrides.detectOllama();
26
33
  const result = { available: false, models: [], endpoint: OLLAMA_BASE };
27
34
  // Try HTTP API first
28
35
  try {
@@ -120,6 +127,8 @@ const AGENT_PLATFORMS = SESSION_LOG_HARNESSES.filter((h) => h.setupDetectionDir)
120
127
  * Supports both HOME (Unix) and USERPROFILE (Windows).
121
128
  */
122
129
  export function detectAgentPlatforms() {
130
+ if (detectOverrides?.detectAgentPlatforms)
131
+ return detectOverrides.detectAgentPlatforms();
123
132
  const home = process.env.HOME?.trim() || process.env.USERPROFILE?.trim();
124
133
  if (!home)
125
134
  return [];