gitnexus 1.6.8-rc.29 → 1.6.8-rc.30

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.
@@ -85,6 +85,16 @@ const KEY_SPECS = {
85
85
  // built-in convention set, is otherwise invisible to route_map consumers.
86
86
  // Listing it here adds it to the cross-file consumer scan.
87
87
  fetchWrappers: { target: 'fetchWrappers', kind: 'string-array' },
88
+ // Auth token AND dims are intentionally CLI/env-only — no embeddingAuthToken
89
+ // or embeddingDims key here:
90
+ // - the token keeps secrets out of a committed .gitnexusrc;
91
+ // - dims cannot take effect from .gitnexusrc anyway — schema.ts reads
92
+ // GITNEXUS_EMBEDDING_DIMS at module-load (before .gitnexusrc is loaded in
93
+ // analyzeCommandImpl), so a config value would size nothing and silently
94
+ // mismatch the vector column. Use --embedding-dims or GITNEXUS_EMBEDDING_DIMS.
95
+ // (URL/MODEL are safe as config keys: they are read lazily at runtime, not at module-load.)
96
+ embeddingBaseUrl: { target: 'embeddingBaseUrl', kind: 'string' },
97
+ embeddingModel: { target: 'embeddingModel', kind: 'string' },
88
98
  };
89
99
  /** Top-level container key for the nested form; not itself an `AnalyzeOptions` field. */
90
100
  const NESTED_KEY = 'analyze';
@@ -115,6 +115,14 @@ export interface AnalyzeOptions {
115
115
  * outside the built-in convention still produces `route_map` consumers.
116
116
  */
117
117
  fetchWrappers?: string[];
118
+ /** OpenAI-compatible embeddings base URL (incl. /v1). Overrides GITNEXUS_EMBEDDING_URL. */
119
+ embeddingBaseUrl?: string;
120
+ /** Embedding model name. Overrides GITNEXUS_EMBEDDING_MODEL. */
121
+ embeddingModel?: string;
122
+ /** Bearer token for the embeddings endpoint. Overrides GITNEXUS_EMBEDDING_API_KEY. Never logged. */
123
+ embeddingAuthToken?: string;
124
+ /** Embedding vector dimensions (positive integer string). Overrides GITNEXUS_EMBEDDING_DIMS. */
125
+ embeddingDims?: string;
118
126
  }
119
127
  /**
120
128
  * Whether the post-index skill step should run.
@@ -23,8 +23,10 @@ import { warnMissingOptionalGrammars, getOptionalGrammarExtensions } from './opt
23
23
  import { glob } from 'glob';
24
24
  import fs from 'fs/promises';
25
25
  import { cliError } from './cli-message.js';
26
+ import { EMBEDDING_DIMS_ERROR, normalizeEmbeddingDims } from './embedding-dims.js';
26
27
  import { formatElapsed } from './format-elapsed.js';
27
28
  import { isHfDownloadFailure } from '../core/embeddings/hf-env.js';
29
+ import { safeUrl } from '../core/embeddings/http-client.js';
28
30
  import { isLocalEmbeddingRuntimeBlockerMessage } from '../core/embeddings/runtime-support.js';
29
31
  import { warnIfNpm11NpxRisk } from './resolve-invocation.js';
30
32
  // Capture stderr.write at module load BEFORE anything (LadybugDB native
@@ -446,6 +448,10 @@ const ANALYZE_CLI_ENV_KEYS = [
446
448
  'GITNEXUS_EMBEDDING_SUB_BATCH_SIZE',
447
449
  'GITNEXUS_EMBEDDING_DEVICE',
448
450
  'GITNEXUS_ANALYZE_PROGRESS_ACTIVE',
451
+ 'GITNEXUS_EMBEDDING_URL',
452
+ 'GITNEXUS_EMBEDDING_MODEL',
453
+ 'GITNEXUS_EMBEDDING_API_KEY',
454
+ 'GITNEXUS_EMBEDDING_DIMS',
449
455
  ];
450
456
  const snapshotAnalyzeEnv = () => {
451
457
  const snap = {};
@@ -690,6 +696,95 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
690
696
  }
691
697
  process.env.GITNEXUS_EMBEDDING_DEVICE = options.embeddingDevice;
692
698
  }
699
+ // --- Custom HTTP embedding endpoint flags (override GITNEXUS_EMBEDDING_* env vars) ---
700
+ const anyHttpEmbedFlag = options.embeddingBaseUrl !== undefined ||
701
+ options.embeddingModel !== undefined ||
702
+ options.embeddingAuthToken !== undefined ||
703
+ options.embeddingDims !== undefined;
704
+ if (options.embeddingBaseUrl !== undefined) {
705
+ const url = options.embeddingBaseUrl.trim();
706
+ if (url.length === 0) {
707
+ cliError(' --embedding-base-url must not be empty.\n');
708
+ process.exitCode = 1;
709
+ return;
710
+ }
711
+ let parsed;
712
+ try {
713
+ parsed = new URL(url);
714
+ }
715
+ catch {
716
+ cliError(` --embedding-base-url is not a valid URL: "${url}".\n`);
717
+ process.exitCode = 1;
718
+ return;
719
+ }
720
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
721
+ cliError(' --embedding-base-url must use http:// or https://.\n');
722
+ process.exitCode = 1;
723
+ return;
724
+ }
725
+ // http-client strips trailing slashes; store as given (trimmed).
726
+ process.env.GITNEXUS_EMBEDDING_URL = url;
727
+ }
728
+ if (options.embeddingModel !== undefined) {
729
+ const model = options.embeddingModel.trim();
730
+ if (model.length === 0) {
731
+ cliError(' --embedding-model must not be empty.\n');
732
+ process.exitCode = 1;
733
+ return;
734
+ }
735
+ process.env.GITNEXUS_EMBEDDING_MODEL = model;
736
+ }
737
+ if (options.embeddingAuthToken !== undefined) {
738
+ const token = options.embeddingAuthToken.trim();
739
+ if (token.length === 0) {
740
+ cliError(' --embedding-auth-token must not be empty.\n');
741
+ process.exitCode = 1;
742
+ return;
743
+ }
744
+ // Never log the token value.
745
+ process.env.GITNEXUS_EMBEDDING_API_KEY = token;
746
+ }
747
+ // Validate + normalize dims through the same shared helper the preAction
748
+ // hook uses, so the CLI path, this direct/programmatic-call path, schema.ts
749
+ // (parseInt) and http-client (/^\d+$/) all agree on one canonical value.
750
+ if (options.embeddingDims !== undefined) {
751
+ const dims = normalizeEmbeddingDims(options.embeddingDims);
752
+ if (dims === null) {
753
+ cliError(` ${EMBEDDING_DIMS_ERROR}\n`);
754
+ process.exitCode = 1;
755
+ return;
756
+ }
757
+ process.env.GITNEXUS_EMBEDDING_DIMS = dims;
758
+ }
759
+ // Custom-endpoint UX, emitting at most ONE message that reflects THIS run's
760
+ // intent (not ambient env). Order matters — the first matching branch wins:
761
+ // 1. flags given but --embeddings absent: the endpoint won't be used, so
762
+ // say only that (no contradictory "Using…" line).
763
+ // 2. embeddings enabled + a complete endpoint (flags or env): confirm it,
764
+ // masking the URL via safeUrl() since a base URL may carry credentials
765
+ // in userinfo (http://user:pass@host) or a query token (?api_key=…)
766
+ // that must not land in stdout/CI logs. The auth token is never printed.
767
+ // 3. embeddings enabled but only one of URL/MODEL supplied via flags:
768
+ // http-client.isHttpMode() needs BOTH, so warn about the fallback.
769
+ // Gating on embeddingsEnabled also stops the old behaviour of printing
770
+ // "Using custom embedding endpoint" on every analyze run whenever the env
771
+ // vars happened to be set.
772
+ if (anyHttpEmbedFlag && !embeddingsEnabled) {
773
+ console.log(' Note: --embedding-* flags only apply when --embeddings is also passed; ' +
774
+ 'no embeddings will be generated this run.\n');
775
+ }
776
+ else if (embeddingsEnabled &&
777
+ process.env.GITNEXUS_EMBEDDING_URL &&
778
+ process.env.GITNEXUS_EMBEDDING_MODEL) {
779
+ console.log(` Using custom embedding endpoint: ${safeUrl(process.env.GITNEXUS_EMBEDDING_URL)} ` +
780
+ `(model: ${process.env.GITNEXUS_EMBEDDING_MODEL})\n`);
781
+ }
782
+ else if (embeddingsEnabled &&
783
+ anyHttpEmbedFlag &&
784
+ (process.env.GITNEXUS_EMBEDDING_URL || process.env.GITNEXUS_EMBEDDING_MODEL)) {
785
+ console.log(' Note: custom HTTP embeddings require BOTH --embedding-base-url and --embedding-model ' +
786
+ '(or the matching env vars). Falling back to local ONNX embeddings.\n');
787
+ }
693
788
  if (options.repairFts && options.force) {
694
789
  cliError(' Cannot combine `--repair-fts` with `--force`. ' +
695
790
  'Use `--repair-fts` for fast FTS-only repair, or `--force` for a full rebuild.\n');
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Strict positive-integer normalization for the `--embedding-dims` flag /
3
+ * `GITNEXUS_EMBEDDING_DIMS` value.
4
+ *
5
+ * Single source of truth shared by two write paths:
6
+ * 1. the `analyze` `preAction` hook (CLI path) — it must set the env var
7
+ * BEFORE `schema.ts` reads `GITNEXUS_EMBEDDING_DIMS` at module-load time
8
+ * (the static import chain `analyze.ts → run-analyze.ts → schema.ts`
9
+ * bakes `FLOAT[dims]` into the vector-table DDL), and
10
+ * 2. `analyzeCommandImpl` (direct / programmatic-call path, which bypasses
11
+ * the commander hook).
12
+ *
13
+ * Keep this module dependency-free. `index.ts` imports it eagerly, so pulling
14
+ * in anything that transitively loads `schema.ts` (e.g. `analyze.ts`) — or
15
+ * even `cli-message.ts`, which drags in the logger + i18n — would defeat the
16
+ * lazy `import('./analyze.js')` the hook exists to enable. Callers print the
17
+ * error themselves (the hook to stderr, the impl via `cliError`).
18
+ *
19
+ * Trim-then-validate, matching the sibling URL/MODEL/TOKEN flags: surrounding
20
+ * whitespace is tolerated, but the remaining value must be all digits and
21
+ * `> 0`. This rejects scientific notation (`1e3`), hex (`0x10`), fractions
22
+ * (`3.5`), signs (`+5`/`-5`), and trailing junk (`4096x`) so the three
23
+ * downstream readers — `schema.ts` (`parseInt`), `http-client.ts` (`/^\d+$/`),
24
+ * and this helper — all agree on one canonical value. Without it, `1e3` parsed
25
+ * to `FLOAT[1]` at module-load but requested 1000-dim vectors at runtime.
26
+ */
27
+ /** Shared error message so both call sites surface identical wording. */
28
+ export declare const EMBEDDING_DIMS_ERROR = "--embedding-dims must be a positive integer.";
29
+ /**
30
+ * Returns the canonical positive-integer string (e.g. `"007"` → `"7"`), or
31
+ * `null` when the input is not a strict positive integer.
32
+ */
33
+ export declare const normalizeEmbeddingDims: (raw: string) => string | null;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Strict positive-integer normalization for the `--embedding-dims` flag /
3
+ * `GITNEXUS_EMBEDDING_DIMS` value.
4
+ *
5
+ * Single source of truth shared by two write paths:
6
+ * 1. the `analyze` `preAction` hook (CLI path) — it must set the env var
7
+ * BEFORE `schema.ts` reads `GITNEXUS_EMBEDDING_DIMS` at module-load time
8
+ * (the static import chain `analyze.ts → run-analyze.ts → schema.ts`
9
+ * bakes `FLOAT[dims]` into the vector-table DDL), and
10
+ * 2. `analyzeCommandImpl` (direct / programmatic-call path, which bypasses
11
+ * the commander hook).
12
+ *
13
+ * Keep this module dependency-free. `index.ts` imports it eagerly, so pulling
14
+ * in anything that transitively loads `schema.ts` (e.g. `analyze.ts`) — or
15
+ * even `cli-message.ts`, which drags in the logger + i18n — would defeat the
16
+ * lazy `import('./analyze.js')` the hook exists to enable. Callers print the
17
+ * error themselves (the hook to stderr, the impl via `cliError`).
18
+ *
19
+ * Trim-then-validate, matching the sibling URL/MODEL/TOKEN flags: surrounding
20
+ * whitespace is tolerated, but the remaining value must be all digits and
21
+ * `> 0`. This rejects scientific notation (`1e3`), hex (`0x10`), fractions
22
+ * (`3.5`), signs (`+5`/`-5`), and trailing junk (`4096x`) so the three
23
+ * downstream readers — `schema.ts` (`parseInt`), `http-client.ts` (`/^\d+$/`),
24
+ * and this helper — all agree on one canonical value. Without it, `1e3` parsed
25
+ * to `FLOAT[1]` at module-load but requested 1000-dim vectors at runtime.
26
+ */
27
+ /** Shared error message so both call sites surface identical wording. */
28
+ export const EMBEDDING_DIMS_ERROR = '--embedding-dims must be a positive integer.';
29
+ /**
30
+ * Returns the canonical positive-integer string (e.g. `"007"` → `"7"`), or
31
+ * `null` when the input is not a strict positive integer.
32
+ */
33
+ export const normalizeEmbeddingDims = (raw) => {
34
+ const trimmed = raw.trim();
35
+ if (!/^\d+$/.test(trimmed) || parseInt(trimmed, 10) <= 0) {
36
+ return null;
37
+ }
38
+ return String(parseInt(trimmed, 10));
39
+ };
package/dist/cli/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { Command } from 'commander';
5
5
  import { createRequire } from 'node:module';
6
6
  import { createLazyAction, createLbugLazyAction } from './lazy-action.js';
7
+ import { EMBEDDING_DIMS_ERROR, normalizeEmbeddingDims } from './embedding-dims.js';
7
8
  import { registerGroupCommands } from './group.js';
8
9
  import { localizeCliHelp } from './help-i18n.js';
9
10
  import { t } from './i18n/index.js';
@@ -24,6 +25,14 @@ program
24
25
  .description('Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors')
25
26
  .option('-f, --force', 'Apply the changes (default is a dry-run preview)')
26
27
  .action(createLazyAction(() => import('./uninstall.js'), 'uninstallCommand'));
28
+ // Baseline of GITNEXUS_EMBEDDING_DIMS captured by the analyze preAction hook
29
+ // before it overwrites the var, so the postAction hook can restore it. The
30
+ // analyzeCommand env snapshot is taken AFTER this hook runs, so it cannot undo
31
+ // the hook's write on its own — without this restore a CLI --embedding-dims
32
+ // would leak into a later in-process program.parseAsync (tests / long-running
33
+ // hosts). Single-shot CLI exits the process, making the restore a no-op there.
34
+ let dimsEnvBaseline;
35
+ let dimsEnvCaptured = false;
27
36
  program
28
37
  .command('analyze [path]')
29
38
  .description('Index a repository (full analysis)')
@@ -63,7 +72,53 @@ program
63
72
  .option('--embedding-batch-size <n>', 'Number of nodes per embedding batch')
64
73
  .option('--embedding-sub-batch-size <n>', 'Number of chunks per embedding model call')
65
74
  .option('--embedding-device <device>', 'Embedding device: auto, cpu, dml, cuda, or wasm')
75
+ .option('--embedding-base-url <url>', 'OpenAI-compatible embeddings base URL including the /v1 suffix ' +
76
+ '(e.g. http://10.219.32.29:11434/v1 for Ollama). Overrides GITNEXUS_EMBEDDING_URL.')
77
+ .option('--embedding-model <model>', 'Embedding model name (e.g. qwen3-embedding:8b). Overrides GITNEXUS_EMBEDDING_MODEL.')
78
+ .option('--embedding-auth-token <token>', 'Bearer token for the embeddings endpoint (omit for unauthenticated servers like Ollama). ' +
79
+ 'Overrides GITNEXUS_EMBEDDING_API_KEY.')
80
+ .option('--embedding-dims <number>', 'Embedding vector dimensions (positive integer; e.g. 4096 for Qwen3-Embedding-8B). ' +
81
+ 'Must match what the index was built with. Overrides GITNEXUS_EMBEDDING_DIMS.')
66
82
  .addHelpText('after', () => t('help.analyze.environment'))
83
+ .hook('preAction', (thisCommand) => {
84
+ // ONLY GITNEXUS_EMBEDDING_DIMS must be set here: schema.ts reads it at
85
+ // module-load time during the lazy import('./analyze.js') below (via the
86
+ // static chain analyze.ts → run-analyze.ts → schema.ts), so deferring to
87
+ // analyzeCommandImpl would be too late. URL / MODEL / API_KEY are read
88
+ // lazily at runtime (readConfig), so analyzeCommandImpl is their sole
89
+ // setter — keeping them out of this hook means they fall under the impl's
90
+ // env snapshot/restore and don't leak across in-process invocations.
91
+ const dimsOpt = thisCommand.opts()['embeddingDims'];
92
+ if (dimsOpt !== undefined) {
93
+ // Validate + normalize BEFORE writing the env var: schema.ts throws on a
94
+ // bad value at module-load, which — on the synchronous program.parse()
95
+ // path, before the analyze fatal-handlers are installed — would surface
96
+ // as a raw unhandled rejection instead of this friendly message.
97
+ const dims = normalizeEmbeddingDims(String(dimsOpt));
98
+ if (dims === null) {
99
+ process.stderr.write(`\n ${EMBEDDING_DIMS_ERROR}\n\n`);
100
+ process.exit(1);
101
+ }
102
+ dimsEnvBaseline = process.env.GITNEXUS_EMBEDDING_DIMS;
103
+ dimsEnvCaptured = true;
104
+ process.env.GITNEXUS_EMBEDDING_DIMS = dims;
105
+ }
106
+ })
107
+ .hook('postAction', () => {
108
+ // Restore the pre-hook GITNEXUS_EMBEDDING_DIMS so a CLI override doesn't
109
+ // persist into a later program.parseAsync in the same process. (Fires on a
110
+ // microtask after a successful parse; the crash path never reaches here,
111
+ // but the hook validates dims before writing, so there's nothing to undo.)
112
+ if (!dimsEnvCaptured)
113
+ return;
114
+ dimsEnvCaptured = false;
115
+ if (dimsEnvBaseline === undefined) {
116
+ delete process.env.GITNEXUS_EMBEDDING_DIMS;
117
+ }
118
+ else {
119
+ process.env.GITNEXUS_EMBEDDING_DIMS = dimsEnvBaseline;
120
+ }
121
+ })
67
122
  .action(createLbugLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
68
123
  program
69
124
  .command('index [path...]')
@@ -19,6 +19,13 @@ export declare const isHttpMode: () => boolean;
19
19
  * if HTTP mode is not active or no explicit dimensions are set.
20
20
  */
21
21
  export declare const getHttpDimensions: () => number | undefined;
22
+ /**
23
+ * Return a safe representation of a URL for logs and error messages.
24
+ * Strips query string (may contain tokens) and userinfo (may contain
25
+ * credentials), keeping protocol + host + path. Exported so the CLI's
26
+ * custom-endpoint confirmation can mask the same way.
27
+ */
28
+ export declare const safeUrl: (url: string) => string;
22
29
  /**
23
30
  * Embed texts via the HTTP backend, splitting into batches.
24
31
  * Reads config from env vars on every call.
@@ -56,10 +56,12 @@ export const isHttpMode = () => readConfig() !== null;
56
56
  */
57
57
  export const getHttpDimensions = () => readConfig()?.dimensions;
58
58
  /**
59
- * Return a safe representation of a URL for error messages.
60
- * Strips query string (may contain tokens) and userinfo.
59
+ * Return a safe representation of a URL for logs and error messages.
60
+ * Strips query string (may contain tokens) and userinfo (may contain
61
+ * credentials), keeping protocol + host + path. Exported so the CLI's
62
+ * custom-endpoint confirmation can mask the same way.
61
63
  */
62
- const safeUrl = (url) => {
64
+ export const safeUrl = (url) => {
63
65
  try {
64
66
  const u = new URL(url);
65
67
  return `${u.protocol}//${u.host}${u.pathname}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.8-rc.29",
3
+ "version": "1.6.8-rc.30",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",