@ninjaxtools/slopdex 0.14.0 → 0.15.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.
@@ -154,6 +154,7 @@ Usage: `slopdex <command> [arguments] [options]`. Quote queries and regexes. Boo
154
154
  | `--description-model <name>` | Description model; `gpt-5.6-sol` for OpenAI/Zen and `gpt-5.6-luna` for Go. |
155
155
  | `--reranker-candidates <number>` | With `config reranker openai`, embedding-ranked functions sent to the LLM; range `1`-`100`, default `10`. |
156
156
  | `--ignore-errors` | Silence saved-diagnostic warnings without deleting records. |
157
+ | `--verbose` | Report every external model request on stderr instead of once per call kind/provider/model. Config `"verbose": true` has the same effect. |
157
158
  | `-h`, `--help` | Usage; no refresh. |
158
159
  | `--version` | Package version; exits without refresh or saved-diagnostic warnings. |
159
160
 
@@ -201,7 +202,7 @@ Use `--no-reindex` when the task calls for committed-only results or reuse of an
201
202
  | `index-errors` | `summary` | JSON array |
202
203
  | `status`, update commands, `descriptions` | JSON object | — |
203
204
 
204
- Prefer summary output for compact source review, clusters for duplicate families, and JSON/JSONL for structured processing. Stdout carries results; stderr carries notices and warnings. Cross-search omits sources without emitted matches. Empty output means no findings under the chosen coverage/filters, not proof that no similar code exists.
205
+ Prefer summary output for compact source review, clusters for duplicate families, and JSON/JSONL for structured processing. Stdout carries results; stderr carries notices and warnings. External vector, description, and reranking requests identify their provider and model once per combination, or for every request with `--verbose`. Cross-search omits sources without emitted matches. Empty output means no findings under the chosen coverage/filters, not proof that no similar code exists.
205
206
 
206
207
  With reranking enabled, query summaries display both reranker relevance and embedding similarity. Similarity thresholds filter candidates before reranking; limits apply to the reranked output. LLM candidate documents include descriptions when available and function metadata/source code.
207
208
 
package/README.md CHANGED
@@ -150,6 +150,7 @@ slopdex update-git --force-reindex
150
150
  | `--description-provider <openai\|opencode\|opencode-go>` | Description provider; OpenAI by default. OpenCode values use Zen or Go with `OPENCODE_API_KEY`. |
151
151
  | `--description-model <name>` | Description model; `gpt-5.6-sol` for OpenAI/Zen and `gpt-5.6-luna` for Go, then the persisted model unless overridden. Published OpenCode models use their documented protocol. |
152
152
  | `--ignore-errors` | Silence warnings about saved indexing errors; records remain available. |
153
+ | `--verbose` | Write one stderr notice for every external model call instead of one per call kind/provider/model. |
153
154
  | `-h`, `--help` | Show CLI usage without refreshing. |
154
155
  | `--version` | Print the package version and exit. |
155
156
 
@@ -233,6 +234,8 @@ Ordinary updates preserve an existing file description even when its source chan
233
234
 
234
235
  Tree-sitter extraction, generated descriptions, and document/query vectors are content-addressed in the same SQLite database. Each validated result is committed immediately, independently of the final logical index update. If indexing is interrupted or a later provider call fails, rerunning reuses every completed result whose profile, operation, input, and source context hash still match.
235
236
 
237
+ When Slopdex makes external vector, description, or reranking model calls, stderr identifies the call kind, provider, and model. By default each combination is reported once per process regardless of request count. Pass `--verbose`, or set `"verbose": true` in config, to report every request. Cache hits do not produce notices because they do not call a model.
238
+
236
239
  With complete descriptions, `search` and cross-search average **one-third code similarity + one-third callable-description similarity + one-third file-description similarity**. `search-description` averages callable and file descriptions without code. Cross-repository analysis needs complete descriptions on both sides; otherwise the entire analysis uses code-only scores. Stale file descriptions remain searchable until explicitly reindexed. Thresholds and limits apply to the selected score.
237
240
 
238
241
  Text output labels combined scores. JSON exposes `codeSimilarity`, `descriptionSimilarity`, `fileDescriptionSimilarity`, and cross-search scoring mode/weights. Compare runs only with matching scoring mode, weights, embedding and description-generator profiles, threshold, and source/candidate filters.
@@ -265,6 +268,7 @@ Optional file: `<root>/.slopdex/config.json`. Example using Jina embeddings, Ope
265
268
  "rerankerModel": "gpt-5.6-luna",
266
269
  "rerankerCandidates": 10,
267
270
  "descriptionProvider": "opencode-go",
271
+ "verbose": true,
268
272
  "exclude": ["**/fixtures/**"]
269
273
  }
270
274
  ```
@@ -284,6 +288,7 @@ Optional file: `<root>/.slopdex/config.json`. Example using Jina embeddings, Ope
284
288
  | `exclude` | Additional repository-relative exclusion globs. |
285
289
  | `maxFileSize` | Maximum source-file size in bytes; positive integer, default `1048576`. |
286
290
  | `embeddingBatchSize` | Embedding inputs per batch; positive integer, default `32`. |
291
+ | `verbose` | When true, report every external model request on stderr; false/unset reports each call kind/provider/model once per process. |
287
292
 
288
293
  Keep keys in the environment (`OPENAI_API_KEY`, `JINA_API_KEY`, `COHERE_API_KEY`, `OPENCODE_API_KEY`). Reranker settings do not change the stored index and do not require a rebuild. Changing the embedding profile requires rebuilding with `--force-reindex`.
289
294
 
package/dist/cli.js CHANGED
@@ -1745,6 +1745,20 @@ function reconcileFunctions(current, previous) {
1745
1745
  import { createOpenAI } from "@ai-sdk/openai";
1746
1746
  import { APICallError, generateText } from "ai";
1747
1747
  import { randomUUID } from "node:crypto";
1748
+
1749
+ // src/model-call-notice.ts
1750
+ var reportedCalls = /* @__PURE__ */ new Set();
1751
+ function reportModelCall(kind, profile, verbose = false) {
1752
+ const key = JSON.stringify([kind, profile.provider, profile.model]);
1753
+ if (!verbose && reportedCalls.has(key)) return;
1754
+ reportedCalls.add(key);
1755
+ process.stderr.write(
1756
+ `slopdex: notice: external model call: kind=${kind} provider=${JSON.stringify(profile.provider)} model=${JSON.stringify(profile.model)}
1757
+ `
1758
+ );
1759
+ }
1760
+
1761
+ // src/descriptions/openai.ts
1748
1762
  var DESCRIPTION_PROVIDER_NAMES = ["openai", "opencode", "opencode-go"];
1749
1763
  function isDescriptionProviderName(value) {
1750
1764
  return DESCRIPTION_PROVIDER_NAMES.includes(value);
@@ -1769,6 +1783,7 @@ var OpenAIDescriptionProvider = class {
1769
1783
  #apiKeyName;
1770
1784
  #baseUrl;
1771
1785
  #openCode;
1786
+ #verbose;
1772
1787
  constructor(options = {}) {
1773
1788
  const provider = options.provider ?? "openai";
1774
1789
  const defaults = PROVIDERS[provider];
@@ -1776,6 +1791,7 @@ var OpenAIDescriptionProvider = class {
1776
1791
  this.#apiKey = options.apiKey ?? process.env[this.#apiKeyName] ?? "";
1777
1792
  this.#baseUrl = (options.baseUrl ?? defaults.baseUrl).replace(/\/$/, "");
1778
1793
  this.#openCode = provider !== "openai";
1794
+ this.#verbose = options.verbose ?? false;
1779
1795
  this.profile = {
1780
1796
  provider,
1781
1797
  model: options.model ?? defaults.model,
@@ -1825,6 +1841,7 @@ var OpenAIDescriptionProvider = class {
1825
1841
  throwIfAborted(options?.signal);
1826
1842
  if (!this.#apiKey) throw new CodeIndexError(`${this.#apiKeyName} is required to generate descriptions.`);
1827
1843
  const { model, responses } = await this.#languageModel(headers);
1844
+ reportModelCall("descriptions", this.profile, this.#verbose);
1828
1845
  let text;
1829
1846
  try {
1830
1847
  ({ text } = await generateText({
@@ -1951,7 +1968,8 @@ var CodeIndex = class {
1951
1968
  this.#database = new IndexDatabase(this.indexPath, this.rootDir, profile, options.readOnly ?? false);
1952
1969
  const storedDescriptionProfile = this.#database.descriptionProfile();
1953
1970
  this.descriptionProvider = options.descriptionProvider ?? new OpenAIDescriptionProvider({
1954
- ...storedDescriptionProfile && isDescriptionProviderName(storedDescriptionProfile.provider) ? { provider: storedDescriptionProfile.provider, model: storedDescriptionProfile.model } : {}
1971
+ ...storedDescriptionProfile && isDescriptionProviderName(storedDescriptionProfile.provider) ? { provider: storedDescriptionProfile.provider, model: storedDescriptionProfile.model } : {},
1972
+ ...options.verbose ? { verbose: true } : {}
1955
1973
  });
1956
1974
  this.#policy = new SourcePolicy(options.include, options.exclude);
1957
1975
  this.#maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
@@ -2946,6 +2964,7 @@ var JinaEmbeddingProvider = class {
2946
2964
  profile;
2947
2965
  #apiKey;
2948
2966
  #baseUrl;
2967
+ #verbose;
2949
2968
  constructor(options = {}) {
2950
2969
  this.#apiKey = options.apiKey ?? process.env.JINA_API_KEY ?? "";
2951
2970
  if (!this.#apiKey) throw new Error("JINA_API_KEY is required.");
@@ -2958,11 +2977,13 @@ var JinaEmbeddingProvider = class {
2958
2977
  dimensions,
2959
2978
  strategyVersion: "callable-v2:code-query-passage"
2960
2979
  };
2980
+ this.#verbose = options.verbose ?? false;
2961
2981
  const url = (options.baseUrl ?? "https://api.jina.ai/v1/embeddings").replace(/\/$/, "");
2962
2982
  this.#baseUrl = url.endsWith("/embeddings") ? url.slice(0, -"/embeddings".length) : url;
2963
2983
  }
2964
2984
  async embedDocuments(inputs, options) {
2965
2985
  if (inputs.length === 0) return [];
2986
+ reportModelCall("vectors", this.profile, this.#verbose);
2966
2987
  return requestEmbeddings(
2967
2988
  embedMany({
2968
2989
  model: this.#model("code.passage"),
@@ -2975,6 +2996,7 @@ var JinaEmbeddingProvider = class {
2975
2996
  );
2976
2997
  }
2977
2998
  async embedQuery(input, options) {
2999
+ reportModelCall("vectors", this.profile, this.#verbose);
2978
3000
  return (await requestEmbeddings(
2979
3001
  embed({
2980
3002
  model: this.#model("code.query"),
@@ -3019,6 +3041,7 @@ function truncateInput(input) {
3019
3041
  var OpenAIEmbeddingProvider = class {
3020
3042
  profile;
3021
3043
  #model;
3044
+ #verbose;
3022
3045
  constructor(options = {}) {
3023
3046
  const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
3024
3047
  if (!apiKey) throw new Error("OPENAI_API_KEY is required.");
@@ -3026,6 +3049,7 @@ var OpenAIEmbeddingProvider = class {
3026
3049
  const dimensions = options.dimensions ?? 3072;
3027
3050
  if (!Number.isInteger(dimensions) || dimensions < 1) throw new Error("dimensions must be a positive integer.");
3028
3051
  this.profile = { provider: "openai", model, dimensions, strategyVersion: "callable-v2" };
3052
+ this.#verbose = options.verbose ?? false;
3029
3053
  this.#model = createOpenAI3({
3030
3054
  apiKey,
3031
3055
  baseURL: (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")
@@ -3033,6 +3057,7 @@ var OpenAIEmbeddingProvider = class {
3033
3057
  }
3034
3058
  async embedDocuments(inputs, options) {
3035
3059
  if (inputs.length === 0) return [];
3060
+ reportModelCall("vectors", this.profile, this.#verbose);
3036
3061
  return requestEmbeddings(
3037
3062
  embedMany2({
3038
3063
  model: this.#model,
@@ -3045,6 +3070,7 @@ var OpenAIEmbeddingProvider = class {
3045
3070
  );
3046
3071
  }
3047
3072
  async embedQuery(input, options) {
3073
+ reportModelCall("vectors", this.profile, this.#verbose);
3048
3074
  return (await requestEmbeddings(
3049
3075
  embed2({
3050
3076
  model: this.#model,
@@ -3144,12 +3170,14 @@ var HostedReranker = class {
3144
3170
  #apiKey;
3145
3171
  #url;
3146
3172
  #returnDocuments;
3173
+ #verbose;
3147
3174
  constructor(options, settings) {
3148
3175
  this.#apiKey = options.apiKey ?? process.env[settings.apiKeyName] ?? "";
3149
3176
  if (!this.#apiKey) throw new Error(`${settings.apiKeyName} is required.`);
3150
3177
  const model = options.model ?? settings.defaultModel;
3151
3178
  if (!model.trim()) throw new Error("reranker model must not be empty.");
3152
3179
  this.profile = { provider: settings.provider, model };
3180
+ this.#verbose = options.verbose ?? false;
3153
3181
  const url = (options.baseUrl ?? settings.defaultUrl).replace(/\/$/, "");
3154
3182
  this.#url = url.endsWith("/rerank") ? url : `${url}/rerank`;
3155
3183
  this.#returnDocuments = settings.returnDocuments;
@@ -3159,6 +3187,7 @@ var HostedReranker = class {
3159
3187
  const limit = options.limit ?? documents.length;
3160
3188
  assertPositiveInteger(limit, "rerank limit");
3161
3189
  throwIfAborted(options.signal);
3190
+ reportModelCall("reranking", this.profile, this.#verbose);
3162
3191
  let response;
3163
3192
  try {
3164
3193
  response = await fetch(this.#url, {
@@ -3248,9 +3277,11 @@ var OpenAILLMReranker = class {
3248
3277
  maximumCandidateCount = MAX_CANDIDATES;
3249
3278
  #apiKey;
3250
3279
  #baseUrl;
3280
+ #verbose;
3251
3281
  constructor(options = {}) {
3252
3282
  this.#apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
3253
3283
  this.#baseUrl = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
3284
+ this.#verbose = options.verbose ?? false;
3254
3285
  this.candidateCount = options.candidateCount ?? 10;
3255
3286
  assertPositiveInteger(this.candidateCount, "reranker candidate count");
3256
3287
  if (this.candidateCount > MAX_CANDIDATES) {
@@ -3302,6 +3333,7 @@ var OpenAILLMReranker = class {
3302
3333
  document: tokens.length <= candidateTokenLimit ? document : tokenizer2.decode(tokens.slice(0, candidateTokenLimit))
3303
3334
  };
3304
3335
  });
3336
+ reportModelCall("reranking", this.profile, this.#verbose);
3305
3337
  ({ output } = await generateText2({
3306
3338
  model: createOpenAI4({ apiKey: this.#apiKey, baseURL: this.#baseUrl }).responses(this.profile.model),
3307
3339
  prompt: JSON.stringify({
@@ -3532,6 +3564,7 @@ var parsed = (() => {
3532
3564
  "no-reindex": { type: "boolean", default: false },
3533
3565
  callables: { type: "boolean", default: false },
3534
3566
  "ignore-errors": { type: "boolean", default: false },
3567
+ verbose: { type: "boolean", default: false },
3535
3568
  version: { type: "boolean", default: false },
3536
3569
  help: { type: "boolean", short: "h", default: false }
3537
3570
  }
@@ -3545,7 +3578,7 @@ var parsed = (() => {
3545
3578
  var [command, ...positionals] = parsed.positionals;
3546
3579
  var descriptionsAction = command === "descriptions" ? positionals[0] : void 0;
3547
3580
  if (parsed.values.version) {
3548
- const version = true ? "0.14.0" : JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
3581
+ const version = true ? "0.15.0" : JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
3549
3582
  process.stdout.write(`${version}
3550
3583
  `);
3551
3584
  process.exit(0);
@@ -3620,7 +3653,8 @@ async function main() {
3620
3653
  ...config.include ? { include: config.include } : {},
3621
3654
  ...config.exclude ? { exclude: config.exclude } : {},
3622
3655
  ...config.maxFileSize ? { maxFileSize: config.maxFileSize } : {},
3623
- ...config.embeddingBatchSize ? { embeddingBatchSize: config.embeddingBatchSize } : {}
3656
+ ...config.embeddingBatchSize ? { embeddingBatchSize: config.embeddingBatchSize } : {},
3657
+ ...config.verbose ? { verbose: true } : {}
3624
3658
  };
3625
3659
  const updateTarget = command === "update-git" ? parsed.values.target ?? "HEAD" : "HEAD";
3626
3660
  const updateStats = await ensureIndexUpdated(
@@ -3708,6 +3742,7 @@ async function runCrossSearch(source, sourceOptions, provider) {
3708
3742
  ...targetConfig?.exclude ? { exclude: targetConfig.exclude } : {},
3709
3743
  ...targetConfig?.maxFileSize ? { maxFileSize: targetConfig.maxFileSize } : {},
3710
3744
  ...targetConfig?.embeddingBatchSize ? { embeddingBatchSize: targetConfig.embeddingBatchSize } : {},
3745
+ ...targetConfig?.verbose ? { verbose: true } : {},
3711
3746
  ...targetDescriptionProvider ? { descriptionProvider: targetDescriptionProvider } : {}
3712
3747
  } : void 0;
3713
3748
  if (targetOptions) {
@@ -3807,7 +3842,8 @@ async function initializeIndex(options, indexPath, label, target, noReindex, des
3807
3842
  indexPath,
3808
3843
  ...descriptionProfile && !options.descriptionProvider ? { descriptionProvider: new OpenAIDescriptionProvider({
3809
3844
  provider: descriptionProfile.provider,
3810
- model: descriptionProfile.model
3845
+ model: descriptionProfile.model,
3846
+ ...options.verbose ? { verbose: true } : {}
3811
3847
  }) } : {}
3812
3848
  });
3813
3849
  try {
@@ -3888,6 +3924,9 @@ function commandLineConfig(config) {
3888
3924
  if (config.rerankingEnabled !== void 0 && typeof config.rerankingEnabled !== "boolean") {
3889
3925
  throw new CodeIndexError("rerankingEnabled must be a boolean.");
3890
3926
  }
3927
+ if (config.verbose !== void 0 && typeof config.verbose !== "boolean") {
3928
+ throw new CodeIndexError("verbose must be a boolean.");
3929
+ }
3891
3930
  if (config.rerankingEnabled === true) {
3892
3931
  if (config.rerankerProvider !== "cohere" && config.rerankerProvider !== "jina" && config.rerankerProvider !== "openai") {
3893
3932
  throw new CodeIndexError(`Unsupported reranker provider: ${String(config.rerankerProvider)}`);
@@ -3908,14 +3947,16 @@ function commandLineConfig(config) {
3908
3947
  ...parsed.values.model ? { model: parsed.values.model } : {},
3909
3948
  ...descriptionProviderValue ? { descriptionProvider: descriptionProviderValue } : {},
3910
3949
  ...parsed.values["description-model"] ? { descriptionModel: parsed.values["description-model"] } : {},
3911
- ...dimensions ? { dimensions } : {}
3950
+ ...dimensions ? { dimensions } : {},
3951
+ ...parsed.values.verbose ? { verbose: true } : {}
3912
3952
  };
3913
3953
  }
3914
3954
  function createDescriptionProvider(config) {
3915
3955
  if (!config.descriptionProvider && !config.descriptionModel) return void 0;
3916
3956
  return new OpenAIDescriptionProvider({
3917
3957
  ...config.descriptionProvider ? { provider: config.descriptionProvider } : {},
3918
- ...config.descriptionModel ? { model: config.descriptionModel } : {}
3958
+ ...config.descriptionModel ? { model: config.descriptionModel } : {},
3959
+ ...config.verbose ? { verbose: true } : {}
3919
3960
  });
3920
3961
  }
3921
3962
  function descriptionRefresh(config) {
@@ -4096,26 +4137,35 @@ function createProvider(config) {
4096
4137
  if ((config.provider ?? "openai") === "jina") {
4097
4138
  return new JinaEmbeddingProvider({
4098
4139
  ...config.model ? { model: config.model } : {},
4099
- ...config.dimensions ? { dimensions: config.dimensions } : {}
4140
+ ...config.dimensions ? { dimensions: config.dimensions } : {},
4141
+ ...config.verbose ? { verbose: true } : {}
4100
4142
  });
4101
4143
  }
4102
4144
  return new OpenAIEmbeddingProvider({
4103
4145
  ...config.model ? { model: config.model } : {},
4104
- ...config.dimensions ? { dimensions: config.dimensions } : {}
4146
+ ...config.dimensions ? { dimensions: config.dimensions } : {},
4147
+ ...config.verbose ? { verbose: true } : {}
4105
4148
  });
4106
4149
  }
4107
4150
  function createReranker(config) {
4108
4151
  if (config.rerankingEnabled !== true) return void 0;
4109
4152
  if (config.rerankerProvider === "cohere") {
4110
- return new CohereReranker(config.rerankerModel ? { model: config.rerankerModel } : {});
4153
+ return new CohereReranker({
4154
+ ...config.rerankerModel ? { model: config.rerankerModel } : {},
4155
+ ...config.verbose ? { verbose: true } : {}
4156
+ });
4111
4157
  }
4112
4158
  if (config.rerankerProvider === "jina") {
4113
- return new JinaReranker(config.rerankerModel ? { model: config.rerankerModel } : {});
4159
+ return new JinaReranker({
4160
+ ...config.rerankerModel ? { model: config.rerankerModel } : {},
4161
+ ...config.verbose ? { verbose: true } : {}
4162
+ });
4114
4163
  }
4115
4164
  if (config.rerankerProvider === "openai") {
4116
4165
  return new OpenAILLMReranker({
4117
4166
  ...config.rerankerModel ? { model: config.rerankerModel } : {},
4118
- ...config.rerankerCandidates ? { candidateCount: config.rerankerCandidates } : {}
4167
+ ...config.rerankerCandidates ? { candidateCount: config.rerankerCandidates } : {},
4168
+ ...config.verbose ? { verbose: true } : {}
4119
4169
  });
4120
4170
  }
4121
4171
  throw new CodeIndexError("rerankerProvider is required when rerankingEnabled is true.");
@@ -4376,6 +4426,7 @@ Options:
4376
4426
  --no-reindex Skip worktree overlays or reuse a non-Git index
4377
4427
  --callables With reindex-files, also regenerate callable descriptions
4378
4428
  --ignore-errors Silence warnings about persisted indexing errors
4429
+ --verbose Log every external model call instead of one per kind/model
4379
4430
  --limit <number> Search result limit
4380
4431
  --threshold <number|range> Show similarities at/above a value or within a range
4381
4432
  --format <json|summary|clusters> Output format (default: summary; cross-search: clusters)