nocaap 0.0.1 → 0.0.3

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.
package/dist/index.js CHANGED
@@ -1,29 +1,46 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { confirm, input, checkbox } from '@inquirer/prompts';
2
+ import path2, { dirname as dirname$1, join as join$1 } from 'path';
3
+ import { fileURLToPath } from 'url';
4
4
  import upath from 'upath';
5
- import 'path';
6
5
  import fs2 from 'fs-extra';
7
6
  import chalk from 'chalk';
8
7
  import ora from 'ora';
9
8
  import { z } from 'zod';
10
9
  import os from 'os';
10
+ import matter2 from 'gray-matter';
11
+ import { create, insertMultiple, search } from '@orama/orama';
12
+ import { persist, restore } from '@orama/plugin-data-persistence';
13
+ import { confirm, input, checkbox } from '@inquirer/prompts';
14
+ import { readFileSync } from 'fs';
15
+ import { Command } from 'commander';
11
16
  import simpleGit from 'simple-git';
12
- import matter from 'gray-matter';
13
17
  import { exec } from 'child_process';
14
18
  import { promisify } from 'util';
19
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
20
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
21
 
16
- var CONTEXT_DIR = ".context";
17
- var PACKAGES_DIR = "packages";
18
- var CONFIG_FILE = "context.config.json";
19
- var LOCK_FILE = "context.lock";
20
- var INDEX_FILE = "INDEX.md";
22
+ var __defProp = Object.defineProperty;
23
+ var __getOwnPropNames = Object.getOwnPropertyNames;
24
+ var __esm = (fn, res) => function __init() {
25
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
26
+ };
27
+ var __export = (target, all) => {
28
+ for (var name in all)
29
+ __defProp(target, name, { get: all[name], enumerable: true });
30
+ };
31
+ var init_esm_shims = __esm({
32
+ "node_modules/tsup/assets/esm_shims.js"() {
33
+ }
34
+ });
21
35
  function toUnix(filePath) {
22
36
  return upath.toUnix(filePath);
23
37
  }
24
38
  function join(...segments) {
25
39
  return upath.join(...segments);
26
40
  }
41
+ function resolve(...segments) {
42
+ return toUnix(path2.resolve(...segments));
43
+ }
27
44
  function dirname(filePath) {
28
45
  return upath.dirname(filePath);
29
46
  }
@@ -54,6 +71,12 @@ function getPackagePath(projectRoot, alias) {
54
71
  function relative(from, to) {
55
72
  return upath.relative(from, to);
56
73
  }
74
+ function isWithin(parent, child) {
75
+ const resolvedParent = resolve(parent);
76
+ const resolvedChild = resolve(child);
77
+ const normalizedParent = resolvedParent.endsWith("/") ? resolvedParent : `${resolvedParent}/`;
78
+ return resolvedChild === resolvedParent || resolvedChild.startsWith(normalizedParent);
79
+ }
57
80
  async function ensureDir(dirPath) {
58
81
  await fs2.ensureDir(dirPath);
59
82
  }
@@ -65,46 +88,17 @@ async function exists(filePath) {
65
88
  return false;
66
89
  }
67
90
  }
68
- var log = {
69
- /** Info message (blue) */
70
- info: (message) => console.log(chalk.blue("\u2139"), message),
71
- /** Success message (green) */
72
- success: (message) => console.log(chalk.green("\u2714"), message),
73
- /** Warning message (yellow) */
74
- warn: (message) => console.log(chalk.yellow("\u26A0"), message),
75
- /** Error message (red) */
76
- error: (message) => console.log(chalk.red("\u2716"), message),
77
- /** Debug message (gray) - only when NOCAAP_DEBUG=true */
78
- debug: (message) => {
79
- if (process.env.NOCAAP_DEBUG === "true") {
80
- console.log(chalk.gray("\u{1F50D}"), chalk.gray(message));
81
- }
82
- },
83
- /** Plain message */
84
- plain: (message) => console.log(message),
85
- /** Styled title */
86
- title: (message) => console.log(chalk.bold.cyan(`
87
- ${message}
88
- `)),
89
- /** Dim helper text */
90
- dim: (message) => console.log(chalk.dim(message)),
91
- /** Empty line for spacing */
92
- newline: () => console.log(),
93
- /** Horizontal rule */
94
- hr: () => console.log(chalk.dim("\u2500".repeat(50)))
95
- };
96
- var style = {
97
- bold: (text) => chalk.bold(text),
98
- dim: (text) => chalk.dim(text),
99
- italic: (text) => chalk.italic(text),
100
- underline: (text) => chalk.underline(text),
101
- code: (text) => chalk.cyan(`\`${text}\``),
102
- path: (text) => chalk.yellow(text),
103
- url: (text) => chalk.blue.underline(text),
104
- success: (text) => chalk.green(text),
105
- error: (text) => chalk.red(text),
106
- warn: (text) => chalk.yellow(text)
107
- };
91
+ var CONTEXT_DIR, PACKAGES_DIR, CONFIG_FILE, LOCK_FILE, INDEX_FILE;
92
+ var init_paths = __esm({
93
+ "src/utils/paths.ts"() {
94
+ init_esm_shims();
95
+ CONTEXT_DIR = ".context";
96
+ PACKAGES_DIR = "packages";
97
+ CONFIG_FILE = "context.config.json";
98
+ LOCK_FILE = "context.lock";
99
+ INDEX_FILE = "INDEX.md";
100
+ }
101
+ });
108
102
  function createSpinner(text) {
109
103
  const spinner = ora({
110
104
  text,
@@ -144,38 +138,63 @@ function createSpinner(text) {
144
138
  };
145
139
  return instance;
146
140
  }
147
- var gitUrlPattern = /^(git@|https:\/\/|git:\/\/).+/;
148
- var ContextEntrySchema = z.object({
149
- name: z.string().min(1, "Context name is required"),
150
- description: z.string(),
151
- repo: z.string().min(1, "Repository URL is required").refine(
152
- (url) => gitUrlPattern.test(url),
153
- "Must be a valid Git URL (git@, https://, or git://)"
154
- ),
155
- path: z.string().optional(),
156
- tags: z.array(z.string()).optional()
157
- });
158
- var RegistrySchema = z.object({
159
- name: z.string().optional(),
160
- contexts: z.array(ContextEntrySchema),
161
- imports: z.array(z.string().url()).optional()
162
- });
163
- var PackageEntrySchema = z.object({
164
- alias: z.string().min(1, "Alias is required"),
165
- source: z.string().min(1, "Source URL is required"),
166
- path: z.string().optional(),
167
- version: z.string().default("main")
168
- });
169
- var ConfigSchema = z.object({
170
- registryUrl: z.string().url().optional(),
171
- packages: z.array(PackageEntrySchema)
172
- });
173
- var LockEntrySchema = z.object({
174
- commitHash: z.string().min(1, "Commit hash is required"),
175
- sparsePath: z.string(),
176
- updatedAt: z.string().datetime()
141
+ async function withSpinner(text, task, options) {
142
+ const spinner = ora(text).start();
143
+ try {
144
+ const result = await task();
145
+ spinner.succeed(options?.successText || text);
146
+ return result;
147
+ } catch (error) {
148
+ spinner.fail(options?.failText || text);
149
+ throw error;
150
+ }
151
+ }
152
+ var log, style;
153
+ var init_logger = __esm({
154
+ "src/utils/logger.ts"() {
155
+ init_esm_shims();
156
+ log = {
157
+ /** Info message (blue) */
158
+ info: (message) => console.log(chalk.blue("\u2139"), message),
159
+ /** Success message (green) */
160
+ success: (message) => console.log(chalk.green("\u2714"), message),
161
+ /** Warning message (yellow) */
162
+ warn: (message) => console.log(chalk.yellow("\u26A0"), message),
163
+ /** Error message (red) */
164
+ error: (message) => console.log(chalk.red("\u2716"), message),
165
+ /** Debug message (gray) - only when NOCAAP_DEBUG=true */
166
+ debug: (message) => {
167
+ if (process.env.NOCAAP_DEBUG === "true") {
168
+ console.log(chalk.gray("\u{1F50D}"), chalk.gray(message));
169
+ }
170
+ },
171
+ /** Plain message */
172
+ plain: (message) => console.log(message),
173
+ /** Styled title */
174
+ title: (message) => console.log(chalk.bold.cyan(`
175
+ ${message}
176
+ `)),
177
+ /** Dim helper text */
178
+ dim: (message) => console.log(chalk.dim(message)),
179
+ /** Empty line for spacing */
180
+ newline: () => console.log(),
181
+ /** Horizontal rule */
182
+ hr: () => console.log(chalk.dim("\u2500".repeat(50)))
183
+ };
184
+ style = {
185
+ bold: (text) => chalk.bold(text),
186
+ dim: (text) => chalk.dim(text),
187
+ italic: (text) => chalk.italic(text),
188
+ underline: (text) => chalk.underline(text),
189
+ code: (text) => chalk.cyan(`\`${text}\``),
190
+ path: (text) => chalk.yellow(text),
191
+ url: (text) => chalk.blue.underline(text),
192
+ success: (text) => chalk.green(text),
193
+ error: (text) => chalk.red(text),
194
+ warn: (text) => chalk.yellow(text)
195
+ };
196
+ }
177
197
  });
178
- var LockfileSchema = z.record(z.string(), LockEntrySchema);
179
198
  function safeValidate(schema, data) {
180
199
  const result = schema.safeParse(data);
181
200
  return result.success ? { success: true, data: result.data } : { success: false, error: result.error };
@@ -190,8 +209,73 @@ function safeValidateConfig(data) {
190
209
  function safeValidateLockfile(data) {
191
210
  return safeValidate(LockfileSchema, data);
192
211
  }
193
-
194
- // src/core/config.ts
212
+ function safeValidateGlobalConfig(data) {
213
+ return safeValidate(GlobalConfigSchema, data);
214
+ }
215
+ var gitUrlPattern, ContextEntrySchema, RegistrySchema, PackageEntrySchema, SearchSettingsSchema, PushSettingsSchema, IndexSettingsSchema, EmbeddingSettingsSchema, ConfigSchema, LockEntrySchema, LockfileSchema, GlobalConfigSchema;
216
+ var init_schemas = __esm({
217
+ "src/schemas/index.ts"() {
218
+ init_esm_shims();
219
+ gitUrlPattern = /^(git@|https:\/\/|git:\/\/).+/;
220
+ ContextEntrySchema = z.object({
221
+ name: z.string().min(1, "Context name is required"),
222
+ description: z.string(),
223
+ repo: z.string().min(1, "Repository URL is required").refine(
224
+ (url) => gitUrlPattern.test(url),
225
+ "Must be a valid Git URL (git@, https://, or git://)"
226
+ ),
227
+ path: z.string().optional(),
228
+ tags: z.array(z.string()).optional()
229
+ });
230
+ RegistrySchema = z.object({
231
+ name: z.string().optional(),
232
+ contexts: z.array(ContextEntrySchema),
233
+ imports: z.array(z.string().url()).optional()
234
+ });
235
+ PackageEntrySchema = z.object({
236
+ alias: z.string().min(1, "Alias is required"),
237
+ source: z.string().min(1, "Source URL is required"),
238
+ path: z.string().optional(),
239
+ version: z.string().default("main")
240
+ });
241
+ SearchSettingsSchema = z.object({
242
+ fulltextWeight: z.number().min(0).max(1).optional(),
243
+ vectorWeight: z.number().min(0).max(1).optional(),
244
+ rrfK: z.number().int().positive().optional()
245
+ }).optional();
246
+ PushSettingsSchema = z.object({
247
+ baseBranch: z.string().optional()
248
+ }).optional();
249
+ IndexSettingsSchema = z.object({
250
+ semantic: z.boolean().optional(),
251
+ provider: z.enum(["ollama", "openai", "tfjs", "auto"]).optional()
252
+ }).optional();
253
+ EmbeddingSettingsSchema = z.object({
254
+ provider: z.enum(["ollama", "openai", "tfjs", "auto"]).optional(),
255
+ ollamaModel: z.string().optional(),
256
+ ollamaBaseUrl: z.string().url().optional()
257
+ }).optional();
258
+ ConfigSchema = z.object({
259
+ registryUrl: z.string().url().optional(),
260
+ packages: z.array(PackageEntrySchema),
261
+ search: SearchSettingsSchema,
262
+ push: PushSettingsSchema,
263
+ index: IndexSettingsSchema
264
+ });
265
+ LockEntrySchema = z.object({
266
+ commitHash: z.string().min(1, "Commit hash is required"),
267
+ sparsePath: z.string(),
268
+ updatedAt: z.string().datetime()
269
+ });
270
+ LockfileSchema = z.record(z.string(), LockEntrySchema);
271
+ GlobalConfigSchema = z.object({
272
+ defaultRegistry: z.string().url().optional(),
273
+ updatedAt: z.string().datetime().optional(),
274
+ push: PushSettingsSchema,
275
+ embedding: EmbeddingSettingsSchema
276
+ });
277
+ }
278
+ });
195
279
  async function initContextDir(projectRoot) {
196
280
  const contextDir = getContextDir(projectRoot);
197
281
  const packagesDir = getPackagesDir(projectRoot);
@@ -301,15 +385,15 @@ async function getLockEntry(projectRoot, alias) {
301
385
  const lockfile = await readLockfile(projectRoot);
302
386
  return lockfile[alias];
303
387
  }
304
- async function upsertPackage(projectRoot, pkg) {
388
+ async function upsertPackage(projectRoot, pkg2) {
305
389
  const config = await readConfig(projectRoot) ?? { packages: [] };
306
- const existingIndex = config.packages.findIndex((p) => p.alias === pkg.alias);
390
+ const existingIndex = config.packages.findIndex((p) => p.alias === pkg2.alias);
307
391
  if (existingIndex >= 0) {
308
- config.packages[existingIndex] = pkg;
309
- log.debug(`Updated package '${pkg.alias}' in config`);
392
+ config.packages[existingIndex] = pkg2;
393
+ log.debug(`Updated package '${pkg2.alias}' in config`);
310
394
  } else {
311
- config.packages.push(pkg);
312
- log.debug(`Added package '${pkg.alias}' to config`);
395
+ config.packages.push(pkg2);
396
+ log.debug(`Added package '${pkg2.alias}' to config`);
313
397
  }
314
398
  await writeConfig(projectRoot, config);
315
399
  }
@@ -329,8 +413,6 @@ async function getPackage(projectRoot, alias) {
329
413
  const config = await readConfig(projectRoot);
330
414
  return config?.packages.find((p) => p.alias === alias);
331
415
  }
332
- var GITIGNORE_ENTRY = ".context/packages/";
333
- var GITIGNORE_COMMENT = "# nocaap packages (auto-generated)";
334
416
  async function updateGitignore(projectRoot) {
335
417
  const gitignorePath = join(projectRoot, ".gitignore");
336
418
  try {
@@ -357,56 +439,6 @@ ${GITIGNORE_ENTRY}
357
439
  return false;
358
440
  }
359
441
  }
360
- var CURSOR_RULES_CONTENT = `# nocaap Context
361
- This project uses nocaap for organizational context.
362
- Read .context/INDEX.md for available documentation.
363
- `;
364
- async function updateCursorRules(projectRoot) {
365
- const cursorDir = join(projectRoot, ".cursor");
366
- const cursorRulesPath = join(cursorDir, "rules");
367
- const legacyCursorRulesPath = join(projectRoot, ".cursorrules");
368
- try {
369
- for (const rulePath of [cursorRulesPath, legacyCursorRulesPath]) {
370
- if (await exists(rulePath)) {
371
- const content = await fs2.readFile(rulePath, "utf-8");
372
- if (content.includes(".context/INDEX.md")) {
373
- log.debug("Cursor rules already contain nocaap reference");
374
- return false;
375
- }
376
- }
377
- }
378
- if (await exists(cursorDir)) {
379
- if (await exists(cursorRulesPath)) {
380
- const content = await fs2.readFile(cursorRulesPath, "utf-8");
381
- const newContent = content.endsWith("\n") ? content : content + "\n";
382
- await fs2.writeFile(cursorRulesPath, `${newContent}
383
- ${CURSOR_RULES_CONTENT}`);
384
- } else {
385
- await fs2.writeFile(cursorRulesPath, CURSOR_RULES_CONTENT);
386
- }
387
- log.debug("Updated .cursor/rules with nocaap reference");
388
- return true;
389
- }
390
- if (await exists(legacyCursorRulesPath)) {
391
- const content = await fs2.readFile(legacyCursorRulesPath, "utf-8");
392
- const newContent = content.endsWith("\n") ? content : content + "\n";
393
- await fs2.writeFile(legacyCursorRulesPath, `${newContent}
394
- ${CURSOR_RULES_CONTENT}`);
395
- } else {
396
- await fs2.writeFile(legacyCursorRulesPath, CURSOR_RULES_CONTENT);
397
- }
398
- log.debug("Updated .cursorrules with nocaap reference");
399
- return true;
400
- } catch (error) {
401
- log.debug(`Failed to update Cursor rules: ${error}`);
402
- return false;
403
- }
404
- }
405
- var CLAUDE_MD_CONTENT = `
406
- ## Project Context
407
- This project uses nocaap for organizational context.
408
- Read \`.context/INDEX.md\` for standards, guidelines, and documentation.
409
- `;
410
442
  async function updateClaudeMd(projectRoot) {
411
443
  const claudeMdPath = join(projectRoot, "CLAUDE.md");
412
444
  try {
@@ -428,8 +460,34 @@ async function updateClaudeMd(projectRoot) {
428
460
  return false;
429
461
  }
430
462
  }
431
- var NOCAAP_DIR = ".nocaap";
432
- var CONFIG_FILE2 = "config.json";
463
+ async function getSearchSettings(projectRoot) {
464
+ const config = await readConfig(projectRoot);
465
+ return config?.search;
466
+ }
467
+ async function getPushSettings(projectRoot) {
468
+ const config = await readConfig(projectRoot);
469
+ return config?.push;
470
+ }
471
+ async function getIndexSettings(projectRoot) {
472
+ const config = await readConfig(projectRoot);
473
+ return config?.index;
474
+ }
475
+ var GITIGNORE_ENTRY, GITIGNORE_COMMENT, CLAUDE_MD_CONTENT;
476
+ var init_config = __esm({
477
+ "src/core/config.ts"() {
478
+ init_esm_shims();
479
+ init_paths();
480
+ init_schemas();
481
+ init_logger();
482
+ GITIGNORE_ENTRY = ".context/packages/";
483
+ GITIGNORE_COMMENT = "# nocaap packages";
484
+ CLAUDE_MD_CONTENT = `
485
+ ## Project Context
486
+ This project uses nocaap for organizational context.
487
+ Read \`.context/INDEX.md\` for standards, guidelines, and documentation.
488
+ `;
489
+ }
490
+ });
433
491
  function getGlobalConfigDir() {
434
492
  return join(os.homedir(), NOCAAP_DIR);
435
493
  }
@@ -444,8 +502,13 @@ async function getGlobalConfig() {
444
502
  }
445
503
  try {
446
504
  const data = await fs2.readJson(configPath);
505
+ const result = safeValidateGlobalConfig(data);
506
+ if (!result.success) {
507
+ log.debug(`Invalid global config, using defaults: ${result.error.message}`);
508
+ return {};
509
+ }
447
510
  log.debug(`Read global config from ${configPath}`);
448
- return data;
511
+ return result.data;
449
512
  } catch (error) {
450
513
  log.debug(`Failed to read global config: ${error}`);
451
514
  return {};
@@ -465,21 +528,1203 @@ async function getDefaultRegistry() {
465
528
  log.debug(`Using registry from NOCAAP_DEFAULT_REGISTRY env var`);
466
529
  return envRegistry;
467
530
  }
468
- const config = await getGlobalConfig();
469
- return config.defaultRegistry;
470
- }
471
- async function setDefaultRegistry(url) {
472
- const config = await getGlobalConfig();
473
- config.defaultRegistry = url;
474
- await setGlobalConfig(config);
475
- log.debug(`Set default registry to ${url}`);
476
- }
477
- async function clearDefaultRegistry() {
478
- const config = await getGlobalConfig();
479
- delete config.defaultRegistry;
480
- await setGlobalConfig(config);
481
- log.debug("Cleared default registry");
531
+ const config = await getGlobalConfig();
532
+ return config.defaultRegistry;
533
+ }
534
+ async function setDefaultRegistry(url) {
535
+ const config = await getGlobalConfig();
536
+ config.defaultRegistry = url;
537
+ await setGlobalConfig(config);
538
+ log.debug(`Set default registry to ${url}`);
539
+ }
540
+ async function getGlobalPushSettings() {
541
+ const config = await getGlobalConfig();
542
+ return config.push;
543
+ }
544
+ async function getGlobalEmbeddingSettings() {
545
+ const config = await getGlobalConfig();
546
+ return config.embedding;
547
+ }
548
+ var NOCAAP_DIR, CONFIG_FILE2;
549
+ var init_global_config = __esm({
550
+ "src/core/global-config.ts"() {
551
+ init_esm_shims();
552
+ init_paths();
553
+ init_logger();
554
+ init_schemas();
555
+ NOCAAP_DIR = ".nocaap";
556
+ CONFIG_FILE2 = "config.json";
557
+ }
558
+ });
559
+ function parseHeading(line) {
560
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
561
+ if (!match || !match[1] || !match[2]) return null;
562
+ return {
563
+ level: match[1].length,
564
+ text: match[2].trim()
565
+ };
566
+ }
567
+ function splitByH2Sections(body, documentTitle) {
568
+ const lines = body.split("\n");
569
+ const sections = [];
570
+ let currentHeadings = [documentTitle];
571
+ let currentContent = [];
572
+ let h1Seen = false;
573
+ for (const line of lines) {
574
+ const heading = parseHeading(line);
575
+ if (heading) {
576
+ if (heading.level === 1 && !h1Seen) {
577
+ h1Seen = true;
578
+ continue;
579
+ }
580
+ if (heading.level === 2) {
581
+ if (currentContent.length > 0) {
582
+ const content = currentContent.join("\n").trim();
583
+ if (content.length >= MIN_CHUNK_SIZE) {
584
+ sections.push({
585
+ headings: [...currentHeadings],
586
+ content
587
+ });
588
+ }
589
+ }
590
+ currentHeadings = [documentTitle, heading.text];
591
+ currentContent = [];
592
+ continue;
593
+ }
594
+ if (heading.level >= 3 && currentHeadings.length >= 2) {
595
+ currentHeadings = [
596
+ currentHeadings[0],
597
+ currentHeadings[1],
598
+ heading.text
599
+ ];
600
+ }
601
+ }
602
+ currentContent.push(line);
603
+ }
604
+ if (currentContent.length > 0) {
605
+ const content = currentContent.join("\n").trim();
606
+ if (content.length >= MIN_CHUNK_SIZE) {
607
+ sections.push({
608
+ headings: [...currentHeadings],
609
+ content
610
+ });
611
+ }
612
+ }
613
+ if (sections.length === 0 && body.trim().length >= MIN_CHUNK_SIZE) {
614
+ sections.push({
615
+ headings: [documentTitle],
616
+ content: body.trim()
617
+ });
618
+ }
619
+ return sections;
620
+ }
621
+ function splitLargeSection(section) {
622
+ if (section.content.length <= TARGET_CHUNK_SIZE) {
623
+ return [section];
624
+ }
625
+ const paragraphs = section.content.split(/\n\n+/);
626
+ const chunks = [];
627
+ let currentChunk = "";
628
+ for (const para of paragraphs) {
629
+ if (currentChunk.length + para.length + 2 > TARGET_CHUNK_SIZE) {
630
+ if (currentChunk.trim().length >= MIN_CHUNK_SIZE) {
631
+ chunks.push({
632
+ headings: section.headings,
633
+ content: currentChunk.trim()
634
+ });
635
+ }
636
+ currentChunk = para;
637
+ } else {
638
+ currentChunk += (currentChunk ? "\n\n" : "") + para;
639
+ }
640
+ }
641
+ if (currentChunk.trim().length >= MIN_CHUNK_SIZE) {
642
+ chunks.push({
643
+ headings: section.headings,
644
+ content: currentChunk.trim()
645
+ });
646
+ }
647
+ return chunks.length > 0 ? chunks : [section];
648
+ }
649
+ async function chunkFile(filePath, packageAlias, contextDir) {
650
+ const normalizedPath = toUnix(filePath);
651
+ const relativePath = relative(contextDir, normalizedPath);
652
+ log.debug(`Chunking file: ${relativePath}`);
653
+ const fileContent = await fs2.readFile(normalizedPath, "utf-8");
654
+ const { data: frontmatter, content: body } = matter2(fileContent);
655
+ const title = extractTitle2(frontmatter, body, normalizedPath);
656
+ const summary = frontmatter.summary ?? frontmatter.description;
657
+ const type = frontmatter.type;
658
+ const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((t) => typeof t === "string") : [];
659
+ const metadata = { title, summary, type, tags };
660
+ const filename = basename(normalizedPath).toLowerCase();
661
+ const isIndexFile = filename === "readme.md" || filename === "index.md";
662
+ if (isIndexFile) {
663
+ const content = body.trim().slice(0, README_MAX_SIZE);
664
+ return [{
665
+ id: `${relativePath}#0`,
666
+ content,
667
+ path: relativePath,
668
+ package: packageAlias,
669
+ headings: [title],
670
+ metadata: { ...metadata, type: metadata.type ?? "index" }
671
+ }];
672
+ }
673
+ const sections = splitByH2Sections(body, title);
674
+ const allChunks = [];
675
+ for (const section of sections) {
676
+ allChunks.push(...splitLargeSection(section));
677
+ }
678
+ return allChunks.map((chunk, index) => ({
679
+ id: `${relativePath}#${index}`,
680
+ content: chunk.content,
681
+ path: relativePath,
682
+ package: packageAlias,
683
+ headings: chunk.headings,
684
+ metadata
685
+ }));
686
+ }
687
+ function extractTitle2(frontmatter, body, filePath) {
688
+ if (typeof frontmatter.title === "string" && frontmatter.title.trim()) {
689
+ return frontmatter.title.trim();
690
+ }
691
+ const h1Match = body.match(/^#\s+(.+)$/m);
692
+ if (h1Match?.[1]) {
693
+ return h1Match[1].trim();
694
+ }
695
+ const filename = basename(filePath, extname(filePath));
696
+ return filename.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
697
+ }
698
+ async function chunkPackage(packagePath, packageAlias, contextDir) {
699
+ const chunks = [];
700
+ if (!await exists(packagePath)) {
701
+ log.debug(`Package path does not exist: ${packagePath}`);
702
+ return chunks;
703
+ }
704
+ const files = await findMarkdownFiles(packagePath);
705
+ for (const file of files) {
706
+ try {
707
+ const fileChunks = await chunkFile(file, packageAlias, contextDir);
708
+ chunks.push(...fileChunks);
709
+ } catch (error) {
710
+ const message = error instanceof Error ? error.message : "Unknown error";
711
+ log.debug(`Failed to chunk ${file}: ${message}`);
712
+ }
713
+ }
714
+ return chunks;
715
+ }
716
+ async function findMarkdownFiles(dirPath) {
717
+ const results = [];
718
+ const entries = await fs2.readdir(dirPath, { withFileTypes: true });
719
+ for (const entry of entries) {
720
+ const fullPath = join(dirPath, entry.name);
721
+ if (entry.isDirectory()) {
722
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
723
+ continue;
724
+ }
725
+ const subFiles = await findMarkdownFiles(fullPath);
726
+ results.push(...subFiles);
727
+ } else if (entry.isFile()) {
728
+ const ext = extname(entry.name).toLowerCase();
729
+ if (ext === ".md" || ext === ".mdx") {
730
+ results.push(fullPath);
731
+ }
732
+ }
733
+ }
734
+ return results;
735
+ }
736
+ var TARGET_CHUNK_SIZE, MIN_CHUNK_SIZE, README_MAX_SIZE;
737
+ var init_chunker = __esm({
738
+ "src/core/chunker.ts"() {
739
+ init_esm_shims();
740
+ init_paths();
741
+ init_logger();
742
+ TARGET_CHUNK_SIZE = 500;
743
+ MIN_CHUNK_SIZE = 100;
744
+ README_MAX_SIZE = 2e3;
745
+ }
746
+ });
747
+ function getVectorStorePath(projectRoot) {
748
+ return join(getContextDir(projectRoot), VECTOR_DIR);
749
+ }
750
+ var VECTOR_DIR, VECTOR_TABLE, METADATA_FILE, VectorStore;
751
+ var init_vector_store = __esm({
752
+ "src/core/vector-store.ts"() {
753
+ init_esm_shims();
754
+ init_paths();
755
+ init_logger();
756
+ VECTOR_DIR = "vectors.lance";
757
+ VECTOR_TABLE = "chunks";
758
+ METADATA_FILE = "vector-metadata.json";
759
+ VectorStore = class {
760
+ db = null;
761
+ table = null;
762
+ projectRoot;
763
+ constructor(projectRoot) {
764
+ this.projectRoot = projectRoot;
765
+ }
766
+ /**
767
+ * Get the vector store directory path
768
+ */
769
+ getVectorPath() {
770
+ return join(getContextDir(this.projectRoot), VECTOR_DIR);
771
+ }
772
+ /**
773
+ * Get the metadata file path
774
+ */
775
+ getMetadataPath() {
776
+ return join(getContextDir(this.projectRoot), METADATA_FILE);
777
+ }
778
+ /**
779
+ * Check if a vector index exists
780
+ */
781
+ async exists() {
782
+ return exists(this.getVectorPath());
783
+ }
784
+ /**
785
+ * Initialize the vector store connection
786
+ */
787
+ async initialize() {
788
+ const vectorPath = this.getVectorPath();
789
+ if (!await exists(vectorPath)) {
790
+ log.debug("No vector index found");
791
+ return false;
792
+ }
793
+ try {
794
+ const lancedb = await import('@lancedb/lancedb');
795
+ this.db = await lancedb.connect(vectorPath);
796
+ const tableNames = await this.db.tableNames();
797
+ if (tableNames.includes(VECTOR_TABLE)) {
798
+ this.table = await this.db.openTable(VECTOR_TABLE);
799
+ log.debug("Loaded existing vector index");
800
+ return true;
801
+ }
802
+ return false;
803
+ } catch (error) {
804
+ const message = error instanceof Error ? error.message : "Unknown error";
805
+ log.debug(`Failed to initialize vector store: ${message}`);
806
+ return false;
807
+ }
808
+ }
809
+ /**
810
+ * Create a new vector index from chunks
811
+ */
812
+ async createIndex(chunks, metadata) {
813
+ const vectorPath = this.getVectorPath();
814
+ try {
815
+ const lancedb = await import('@lancedb/lancedb');
816
+ if (await exists(vectorPath)) {
817
+ await fs2.remove(vectorPath);
818
+ }
819
+ this.db = await lancedb.connect(vectorPath);
820
+ this.table = await this.db.createTable(VECTOR_TABLE, chunks);
821
+ const storedMetadata = {
822
+ embedding: metadata,
823
+ chunkCount: chunks.length
824
+ };
825
+ await fs2.writeJson(this.getMetadataPath(), storedMetadata, { spaces: 2 });
826
+ log.debug(`Created vector index with ${chunks.length} chunks`);
827
+ } catch (error) {
828
+ const message = error instanceof Error ? error.message : "Unknown error";
829
+ throw new Error(`Failed to create vector index: ${message}`);
830
+ }
831
+ }
832
+ /**
833
+ * Search for similar vectors
834
+ */
835
+ async search(queryVector, limit = 10) {
836
+ if (!this.table) {
837
+ throw new Error("Vector store not initialized");
838
+ }
839
+ try {
840
+ const results = await this.table.vectorSearch(queryVector).limit(limit).toArray();
841
+ return results.map((r) => ({
842
+ id: r.id,
843
+ content: r.content,
844
+ path: r.path,
845
+ package: r.package,
846
+ title: r.title,
847
+ // Convert distance to similarity score (lower distance = higher similarity)
848
+ score: r._distance ? 1 / (1 + r._distance) : 1
849
+ }));
850
+ } catch (error) {
851
+ const message = error instanceof Error ? error.message : "Unknown error";
852
+ throw new Error(`Vector search failed: ${message}`);
853
+ }
854
+ }
855
+ /**
856
+ * Get stored metadata
857
+ */
858
+ async getMetadata() {
859
+ const metadataPath = this.getMetadataPath();
860
+ if (!await exists(metadataPath)) {
861
+ return null;
862
+ }
863
+ try {
864
+ return await fs2.readJson(metadataPath);
865
+ } catch {
866
+ return null;
867
+ }
868
+ }
869
+ };
870
+ }
871
+ });
872
+
873
+ // src/core/embeddings.ts
874
+ function setEmbeddingSettings(settings) {
875
+ embeddingSettings = settings;
876
+ log.debug(`Embedding settings: provider=${settings.provider}, model=${settings.ollamaModel}`);
877
+ }
878
+ function getOllamaBaseUrl() {
879
+ return embeddingSettings?.ollamaBaseUrl ?? "http://localhost:11434";
880
+ }
881
+ function getOllamaModel() {
882
+ return embeddingSettings?.ollamaModel ?? PROVIDER_CONFIG.ollama.model;
883
+ }
884
+ async function detectProvider() {
885
+ if (await isOllamaAvailable()) {
886
+ log.debug("Detected Ollama with embedding model");
887
+ return "ollama";
888
+ }
889
+ if (process.env.OPENAI_API_KEY) {
890
+ log.debug("Detected OpenAI API key");
891
+ return "openai";
892
+ }
893
+ log.debug("Using Transformers.js embeddings (fallback)");
894
+ return "tfjs";
895
+ }
896
+ async function isOllamaAvailable() {
897
+ try {
898
+ const baseUrl = getOllamaBaseUrl();
899
+ const response = await fetch(`${baseUrl}/api/tags`, {
900
+ signal: AbortSignal.timeout(2e3)
901
+ });
902
+ if (!response.ok) return false;
903
+ const data = await response.json();
904
+ const hasEmbedModel = data.models?.some(
905
+ (m) => m.name.includes("nomic-embed") || m.name.includes("mxbai-embed") || m.name.includes("all-minilm")
906
+ );
907
+ return hasEmbedModel ?? false;
908
+ } catch {
909
+ return false;
910
+ }
911
+ }
912
+ async function generateEmbeddings(texts, provider) {
913
+ const resolvedProvider = provider === "auto" ? await detectProvider() : provider;
914
+ log.debug(`Generating ${texts.length} embeddings with ${resolvedProvider}`);
915
+ switch (resolvedProvider) {
916
+ case "ollama":
917
+ return generateOllamaEmbeddings(texts);
918
+ case "openai":
919
+ return generateOpenAIEmbeddings(texts);
920
+ case "tfjs":
921
+ return generateTfjsEmbeddings(texts);
922
+ default:
923
+ throw new Error(`Unknown provider: ${resolvedProvider}`);
924
+ }
925
+ }
926
+ async function generateQueryEmbedding(query, provider) {
927
+ const result = await generateEmbeddings([query], provider);
928
+ return result.vectors[0];
929
+ }
930
+ async function generateOllamaEmbeddings(texts) {
931
+ const config = PROVIDER_CONFIG.ollama;
932
+ const model = getOllamaModel();
933
+ const vectors = [];
934
+ for (let i = 0; i < texts.length; i += config.batchSize) {
935
+ const batch = texts.slice(i, i + config.batchSize);
936
+ try {
937
+ const ollama = await import('ollama');
938
+ const response = await ollama.default.embed({
939
+ model,
940
+ input: batch
941
+ });
942
+ vectors.push(...response.embeddings);
943
+ } catch (error) {
944
+ const message = error instanceof Error ? error.message : "Unknown error";
945
+ throw new Error(`Ollama embedding failed: ${message}`);
946
+ }
947
+ }
948
+ return {
949
+ vectors,
950
+ model,
951
+ dimensions: config.dimensions,
952
+ provider: "ollama"
953
+ };
954
+ }
955
+ async function generateOpenAIEmbeddings(texts) {
956
+ const config = PROVIDER_CONFIG.openai;
957
+ const apiKey = process.env.OPENAI_API_KEY;
958
+ if (!apiKey) {
959
+ throw new Error("OPENAI_API_KEY environment variable is required");
960
+ }
961
+ const vectors = [];
962
+ for (let i = 0; i < texts.length; i += config.batchSize) {
963
+ const batch = texts.slice(i, i + config.batchSize);
964
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
965
+ method: "POST",
966
+ headers: {
967
+ "Authorization": `Bearer ${apiKey}`,
968
+ "Content-Type": "application/json"
969
+ },
970
+ body: JSON.stringify({
971
+ model: config.model,
972
+ input: batch
973
+ })
974
+ });
975
+ if (!response.ok) {
976
+ const error = await response.text();
977
+ throw new Error(`OpenAI embedding failed: ${error}`);
978
+ }
979
+ const data = await response.json();
980
+ vectors.push(...data.data.map((d) => d.embedding));
981
+ }
982
+ return {
983
+ vectors,
984
+ model: config.model,
985
+ dimensions: config.dimensions,
986
+ provider: "openai"
987
+ };
988
+ }
989
+ async function generateTfjsEmbeddings(texts) {
990
+ const config = PROVIDER_CONFIG.tfjs;
991
+ try {
992
+ const { pipeline } = await import('@xenova/transformers');
993
+ const embedder = await pipeline("feature-extraction", config.model);
994
+ const vectors = [];
995
+ for (let i = 0; i < texts.length; i += config.batchSize) {
996
+ const batch = texts.slice(i, i + config.batchSize);
997
+ for (const text of batch) {
998
+ const output = await embedder(text, {
999
+ pooling: "mean",
1000
+ normalize: true
1001
+ });
1002
+ vectors.push(Array.from(output.data));
1003
+ }
1004
+ }
1005
+ return {
1006
+ vectors,
1007
+ model: config.model,
1008
+ dimensions: config.dimensions,
1009
+ provider: "tfjs"
1010
+ };
1011
+ } catch (error) {
1012
+ const message = error instanceof Error ? error.message : "Unknown error";
1013
+ throw new Error(`Transformers.js embedding failed: ${message}`);
1014
+ }
1015
+ }
1016
+ function getProviderConfig(provider) {
1017
+ return PROVIDER_CONFIG[provider];
1018
+ }
1019
+ var embeddingSettings, PROVIDER_CONFIG;
1020
+ var init_embeddings = __esm({
1021
+ "src/core/embeddings.ts"() {
1022
+ init_esm_shims();
1023
+ init_logger();
1024
+ embeddingSettings = null;
1025
+ PROVIDER_CONFIG = {
1026
+ ollama: {
1027
+ model: "nomic-embed-text",
1028
+ dimensions: 768,
1029
+ batchSize: 50
1030
+ },
1031
+ openai: {
1032
+ model: "text-embedding-3-small",
1033
+ dimensions: 1536,
1034
+ batchSize: 100
1035
+ },
1036
+ tfjs: {
1037
+ model: "Xenova/all-MiniLM-L6-v2",
1038
+ dimensions: 384,
1039
+ batchSize: 32
1040
+ }
1041
+ };
1042
+ }
1043
+ });
1044
+
1045
+ // src/core/fusion.ts
1046
+ function reciprocalRankFusion(fulltextResults, vectorResults, options = {}) {
1047
+ const { k = 60, fulltextWeight = 0.4, vectorWeight = 0.6 } = options;
1048
+ const scores = /* @__PURE__ */ new Map();
1049
+ fulltextResults.forEach((doc, rank) => {
1050
+ const rrfScore = fulltextWeight * (1 / (k + rank + 1));
1051
+ const existing = scores.get(doc.id);
1052
+ if (existing) {
1053
+ existing.score += rrfScore;
1054
+ existing.sources.fulltext = rank + 1;
1055
+ } else {
1056
+ scores.set(doc.id, {
1057
+ score: rrfScore,
1058
+ doc,
1059
+ sources: { fulltext: rank + 1 }
1060
+ });
1061
+ }
1062
+ });
1063
+ vectorResults.forEach((doc, rank) => {
1064
+ const rrfScore = vectorWeight * (1 / (k + rank + 1));
1065
+ const existing = scores.get(doc.id);
1066
+ if (existing) {
1067
+ existing.score += rrfScore;
1068
+ existing.sources.vector = rank + 1;
1069
+ } else {
1070
+ scores.set(doc.id, {
1071
+ score: rrfScore,
1072
+ doc,
1073
+ sources: { vector: rank + 1 }
1074
+ });
1075
+ }
1076
+ });
1077
+ return Array.from(scores.values()).sort((a, b) => b.score - a.score).map(({ doc, score, sources }) => ({
1078
+ id: doc.id,
1079
+ content: doc.content,
1080
+ path: doc.path,
1081
+ package: doc.package,
1082
+ title: doc.title,
1083
+ score,
1084
+ sources
1085
+ }));
1086
+ }
1087
+ function normalizeScores(results) {
1088
+ if (results.length === 0) return results;
1089
+ const maxScore = Math.max(...results.map((r) => r.score));
1090
+ if (maxScore === 0) return results;
1091
+ return results.map((r) => ({
1092
+ ...r,
1093
+ score: r.score / maxScore
1094
+ }));
1095
+ }
1096
+ var init_fusion = __esm({
1097
+ "src/core/fusion.ts"() {
1098
+ init_esm_shims();
1099
+ }
1100
+ });
1101
+
1102
+ // src/core/settings.ts
1103
+ async function resolveSettings(projectRoot, cliOverrides) {
1104
+ log.debug("Resolving settings...");
1105
+ const resolved = structuredClone(DEFAULTS);
1106
+ const globalPush = await getGlobalPushSettings();
1107
+ const globalEmbedding = await getGlobalEmbeddingSettings();
1108
+ if (globalPush?.baseBranch) {
1109
+ resolved.push.baseBranch = globalPush.baseBranch;
1110
+ log.debug(`Using global push.baseBranch: ${globalPush.baseBranch}`);
1111
+ }
1112
+ if (globalEmbedding) {
1113
+ if (globalEmbedding.provider) {
1114
+ resolved.embedding.provider = globalEmbedding.provider;
1115
+ }
1116
+ if (globalEmbedding.ollamaModel) {
1117
+ resolved.embedding.ollamaModel = globalEmbedding.ollamaModel;
1118
+ }
1119
+ if (globalEmbedding.ollamaBaseUrl) {
1120
+ resolved.embedding.ollamaBaseUrl = globalEmbedding.ollamaBaseUrl;
1121
+ }
1122
+ log.debug("Applied global embedding settings");
1123
+ }
1124
+ const projectSearch = await getSearchSettings(projectRoot);
1125
+ const projectPush = await getPushSettings(projectRoot);
1126
+ const projectIndex = await getIndexSettings(projectRoot);
1127
+ if (projectSearch) {
1128
+ if (projectSearch.fulltextWeight !== void 0) {
1129
+ resolved.search.fulltextWeight = projectSearch.fulltextWeight;
1130
+ }
1131
+ if (projectSearch.vectorWeight !== void 0) {
1132
+ resolved.search.vectorWeight = projectSearch.vectorWeight;
1133
+ }
1134
+ if (projectSearch.rrfK !== void 0) {
1135
+ resolved.search.rrfK = projectSearch.rrfK;
1136
+ }
1137
+ log.debug("Applied project search settings");
1138
+ }
1139
+ if (projectPush?.baseBranch) {
1140
+ resolved.push.baseBranch = projectPush.baseBranch;
1141
+ log.debug(`Using project push.baseBranch: ${projectPush.baseBranch}`);
1142
+ }
1143
+ if (projectIndex) {
1144
+ if (projectIndex.semantic !== void 0) {
1145
+ resolved.index.semantic = projectIndex.semantic;
1146
+ }
1147
+ if (projectIndex.provider !== void 0) {
1148
+ resolved.index.provider = projectIndex.provider;
1149
+ }
1150
+ log.debug("Applied project index settings");
1151
+ }
1152
+ return resolved;
1153
+ }
1154
+ async function resolveSearchSettings(projectRoot, cliOverrides) {
1155
+ const all = await resolveSettings(projectRoot);
1156
+ return all.search;
1157
+ }
1158
+ async function resolveEmbeddingSettings(projectRoot, cliOverrides) {
1159
+ const all = await resolveSettings(projectRoot);
1160
+ return all.embedding;
1161
+ }
1162
+ async function resolvePushSettings(projectRoot, cliOverrides) {
1163
+ const all = await resolveSettings(projectRoot);
1164
+ return all.push;
1165
+ }
1166
+ var DEFAULTS;
1167
+ var init_settings = __esm({
1168
+ "src/core/settings.ts"() {
1169
+ init_esm_shims();
1170
+ init_global_config();
1171
+ init_config();
1172
+ init_logger();
1173
+ DEFAULTS = {
1174
+ search: {
1175
+ fulltextWeight: 0.4,
1176
+ vectorWeight: 0.6,
1177
+ rrfK: 60
1178
+ },
1179
+ push: {},
1180
+ embedding: {
1181
+ provider: "auto",
1182
+ ollamaModel: "nomic-embed-text",
1183
+ ollamaBaseUrl: "http://localhost:11434"
1184
+ },
1185
+ index: {
1186
+ semantic: false,
1187
+ provider: "auto"
1188
+ }
1189
+ };
1190
+ }
1191
+ });
1192
+ function extractQueryKeywords(query) {
1193
+ return query.toLowerCase().split(/\s+/).filter((word) => word.length > 2 && !STOP_WORDS.has(word));
1194
+ }
1195
+ function getSearchIndexPath(projectRoot) {
1196
+ return join(getContextDir(projectRoot), SEARCH_INDEX_FILE);
1197
+ }
1198
+ async function searchIndexExists(projectRoot) {
1199
+ return exists(getSearchIndexPath(projectRoot));
1200
+ }
1201
+ var SEARCH_INDEX_FILE, DEFAULT_LIMIT, INDEX_VERSION, DEFAULT_SEARCH_SETTINGS, oramaSchema, STOP_WORDS, SearchEngine;
1202
+ var init_search_engine = __esm({
1203
+ "src/core/search-engine.ts"() {
1204
+ init_esm_shims();
1205
+ init_paths();
1206
+ init_logger();
1207
+ init_vector_store();
1208
+ init_embeddings();
1209
+ init_fusion();
1210
+ init_settings();
1211
+ SEARCH_INDEX_FILE = "index.orama.json";
1212
+ DEFAULT_LIMIT = 10;
1213
+ INDEX_VERSION = "1.0.0";
1214
+ DEFAULT_SEARCH_SETTINGS = {
1215
+ fulltextWeight: 0.4,
1216
+ vectorWeight: 0.6,
1217
+ rrfK: 60
1218
+ };
1219
+ oramaSchema = {
1220
+ id: "string",
1221
+ content: "string",
1222
+ path: "string",
1223
+ package: "string",
1224
+ headings: "string[]",
1225
+ title: "string",
1226
+ summary: "string",
1227
+ type: "string",
1228
+ tags: "string[]"
1229
+ };
1230
+ STOP_WORDS = /* @__PURE__ */ new Set([
1231
+ "what",
1232
+ "is",
1233
+ "the",
1234
+ "a",
1235
+ "an",
1236
+ "how",
1237
+ "do",
1238
+ "does",
1239
+ "are",
1240
+ "was",
1241
+ "were",
1242
+ "been",
1243
+ "being",
1244
+ "have",
1245
+ "has",
1246
+ "had",
1247
+ "having",
1248
+ "who",
1249
+ "which",
1250
+ "where",
1251
+ "when",
1252
+ "why",
1253
+ "can",
1254
+ "could",
1255
+ "would",
1256
+ "should",
1257
+ "of",
1258
+ "on",
1259
+ "in",
1260
+ "to",
1261
+ "for",
1262
+ "with",
1263
+ "by",
1264
+ "from",
1265
+ "at",
1266
+ "about"
1267
+ ]);
1268
+ SearchEngine = class {
1269
+ db = null;
1270
+ metadata = null;
1271
+ vectorStore = null;
1272
+ projectRoot = null;
1273
+ embeddingProvider = "auto";
1274
+ searchSettings = DEFAULT_SEARCH_SETTINGS;
1275
+ /**
1276
+ * Check if the search engine has been initialized
1277
+ */
1278
+ isInitialized() {
1279
+ return this.db !== null;
1280
+ }
1281
+ /**
1282
+ * Create a new search index from chunks
1283
+ */
1284
+ async createIndex(chunks) {
1285
+ log.debug(`Creating search index with ${chunks.length} chunks`);
1286
+ this.db = await create({ schema: oramaSchema });
1287
+ const documents = chunks.map((chunk) => ({
1288
+ id: chunk.id,
1289
+ content: chunk.content,
1290
+ path: chunk.path,
1291
+ package: chunk.package,
1292
+ headings: chunk.headings,
1293
+ title: chunk.metadata.title,
1294
+ summary: chunk.metadata.summary ?? "",
1295
+ type: chunk.metadata.type ?? "",
1296
+ tags: chunk.metadata.tags
1297
+ }));
1298
+ await insertMultiple(this.db, documents, 500);
1299
+ const packages = [...new Set(chunks.map((c) => c.package))];
1300
+ this.metadata = {
1301
+ version: INDEX_VERSION,
1302
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1303
+ chunkCount: chunks.length,
1304
+ packages
1305
+ };
1306
+ log.debug(`Search index created with ${chunks.length} documents`);
1307
+ }
1308
+ /**
1309
+ * Save the index to a JSON file
1310
+ */
1311
+ async saveIndex(projectRoot) {
1312
+ if (!this.db || !this.metadata) {
1313
+ throw new Error("No index to save. Call createIndex first.");
1314
+ }
1315
+ const indexPath = join(getContextDir(projectRoot), SEARCH_INDEX_FILE);
1316
+ const data = await persist(this.db, "json");
1317
+ const storedIndex = {
1318
+ metadata: this.metadata,
1319
+ data
1320
+ };
1321
+ await fs2.writeJson(indexPath, storedIndex, { spaces: 2 });
1322
+ log.debug(`Saved search index to ${indexPath}`);
1323
+ }
1324
+ /**
1325
+ * Load an existing index from file
1326
+ */
1327
+ async loadIndex(projectRoot) {
1328
+ const indexPath = join(getContextDir(projectRoot), SEARCH_INDEX_FILE);
1329
+ this.projectRoot = projectRoot;
1330
+ if (!await exists(indexPath)) {
1331
+ log.debug("No existing search index found");
1332
+ return false;
1333
+ }
1334
+ try {
1335
+ const storedIndex = await fs2.readJson(indexPath);
1336
+ this.db = await restore("json", storedIndex.data);
1337
+ this.metadata = storedIndex.metadata;
1338
+ log.debug(`Loaded search index: ${this.metadata.chunkCount} chunks`);
1339
+ await this.initializeVectorStore();
1340
+ try {
1341
+ this.searchSettings = await resolveSearchSettings(projectRoot);
1342
+ log.debug(`Search settings: fulltext=${this.searchSettings.fulltextWeight}, vector=${this.searchSettings.vectorWeight}, k=${this.searchSettings.rrfK}`);
1343
+ } catch {
1344
+ log.debug("Could not resolve search settings, using defaults");
1345
+ this.searchSettings = DEFAULT_SEARCH_SETTINGS;
1346
+ }
1347
+ return true;
1348
+ } catch (error) {
1349
+ const message = error instanceof Error ? error.message : "Unknown error";
1350
+ log.debug(`Failed to load search index: ${message}`);
1351
+ return false;
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Initialize the vector store if available
1356
+ */
1357
+ async initializeVectorStore() {
1358
+ if (!this.projectRoot) return false;
1359
+ try {
1360
+ const store = new VectorStore(this.projectRoot);
1361
+ const initialized = await store.initialize();
1362
+ if (initialized) {
1363
+ this.vectorStore = store;
1364
+ const metadata = await store.getMetadata();
1365
+ if (metadata) {
1366
+ this.embeddingProvider = metadata.embedding.provider;
1367
+ log.debug(`Loaded vector store (${metadata.embedding.model})`);
1368
+ }
1369
+ return true;
1370
+ }
1371
+ this.vectorStore = null;
1372
+ return false;
1373
+ } catch {
1374
+ log.debug("Vector store not available");
1375
+ this.vectorStore = null;
1376
+ return false;
1377
+ }
1378
+ }
1379
+ /**
1380
+ * Check if vector search is available
1381
+ */
1382
+ hasVectorSearch() {
1383
+ return this.vectorStore !== null;
1384
+ }
1385
+ /**
1386
+ * Search the index
1387
+ */
1388
+ async search(options) {
1389
+ if (!this.db) {
1390
+ throw new Error("Search engine not initialized. Call loadIndex or createIndex first.");
1391
+ }
1392
+ const { query, packages, tags, limit = DEFAULT_LIMIT } = options;
1393
+ let where;
1394
+ if (packages?.length || tags?.length) {
1395
+ where = {};
1396
+ if (packages?.length) {
1397
+ where.package = packages;
1398
+ }
1399
+ if (tags?.length) {
1400
+ where.tags = { containsAll: tags };
1401
+ }
1402
+ }
1403
+ const results = await search(this.db, {
1404
+ term: query,
1405
+ properties: ["content", "title", "summary", "headings"],
1406
+ limit,
1407
+ where
1408
+ });
1409
+ return results.hits.map((hit) => ({
1410
+ id: hit.document.id,
1411
+ content: hit.document.content,
1412
+ path: hit.document.path,
1413
+ package: hit.document.package,
1414
+ headings: hit.document.headings,
1415
+ title: hit.document.title,
1416
+ score: hit.score
1417
+ }));
1418
+ }
1419
+ /**
1420
+ * Get index metadata
1421
+ */
1422
+ getMetadata() {
1423
+ return this.metadata;
1424
+ }
1425
+ /**
1426
+ * Get list of indexed packages
1427
+ */
1428
+ getPackages() {
1429
+ return this.metadata?.packages ?? [];
1430
+ }
1431
+ /**
1432
+ * Hybrid search combining fulltext (BM25) and vector (semantic) search
1433
+ */
1434
+ async hybridSearch(options) {
1435
+ const { query, mode = "fulltext", packages, limit = DEFAULT_LIMIT } = options;
1436
+ if (mode === "fulltext") {
1437
+ return this.search({ query, packages, limit });
1438
+ }
1439
+ if (!this.vectorStore) {
1440
+ if (mode === "semantic") {
1441
+ throw new Error('Vector search not available. Run "nocaap index --semantic" first.');
1442
+ }
1443
+ log.debug("Vector store not available, falling back to fulltext search");
1444
+ return this.search({ query, packages, limit });
1445
+ }
1446
+ const queryVector = await generateQueryEmbedding(query, this.embeddingProvider);
1447
+ if (mode === "semantic") {
1448
+ const vectorResults2 = await this.vectorStore.search(queryVector, limit);
1449
+ return vectorResults2.map((r) => ({
1450
+ id: r.id,
1451
+ content: r.content,
1452
+ path: r.path,
1453
+ package: r.package,
1454
+ headings: [],
1455
+ title: r.title,
1456
+ score: r.score,
1457
+ sources: { vector: 1 }
1458
+ }));
1459
+ }
1460
+ const [fulltextResults, vectorResults] = await Promise.all([
1461
+ this.search({ query, packages, limit: limit * 2 }),
1462
+ this.vectorStore.search(queryVector, limit * 2)
1463
+ ]);
1464
+ const ftRanked = fulltextResults.map((r) => ({
1465
+ id: r.id,
1466
+ content: r.content,
1467
+ path: r.path,
1468
+ package: r.package,
1469
+ title: r.title,
1470
+ score: r.score
1471
+ }));
1472
+ const vecRanked = vectorResults.map((r) => ({
1473
+ id: r.id,
1474
+ content: r.content,
1475
+ path: r.path,
1476
+ package: r.package,
1477
+ title: r.title,
1478
+ score: r.score
1479
+ }));
1480
+ const fused = reciprocalRankFusion(ftRanked, vecRanked, {
1481
+ fulltextWeight: this.searchSettings.fulltextWeight,
1482
+ vectorWeight: this.searchSettings.vectorWeight,
1483
+ k: this.searchSettings.rrfK
1484
+ });
1485
+ const queryKeywords = extractQueryKeywords(query);
1486
+ for (const result of fused) {
1487
+ const pathLower = result.path.toLowerCase();
1488
+ let boost = 1;
1489
+ for (const keyword of queryKeywords) {
1490
+ if (pathLower.includes(keyword)) {
1491
+ boost *= 1.15;
1492
+ }
1493
+ }
1494
+ result.score *= boost;
1495
+ }
1496
+ for (const result of fused) {
1497
+ const pathLower = result.path.toLowerCase();
1498
+ if (pathLower.endsWith("readme.md") || pathLower.endsWith("index.md")) {
1499
+ result.score *= 1.25;
1500
+ }
1501
+ }
1502
+ fused.sort((a, b) => b.score - a.score);
1503
+ const normalized = normalizeScores(fused);
1504
+ return normalized.slice(0, limit).map((r) => ({
1505
+ id: r.id,
1506
+ content: r.content,
1507
+ path: r.path,
1508
+ package: r.package,
1509
+ headings: [],
1510
+ title: r.title,
1511
+ score: r.score,
1512
+ sources: r.sources
1513
+ }));
1514
+ }
1515
+ };
1516
+ }
1517
+ });
1518
+
1519
+ // src/commands/index-search.ts
1520
+ async function indexSearchCommand(options = {}) {
1521
+ const { semantic = false, provider = "auto" } = options;
1522
+ const projectRoot = process.cwd();
1523
+ const contextDir = getContextDir(projectRoot);
1524
+ if (!await exists(contextDir)) {
1525
+ throw new Error(
1526
+ "No .context directory found. Run `nocaap setup` or `nocaap add` first."
1527
+ );
1528
+ }
1529
+ const config = await readConfig(projectRoot);
1530
+ if (!config || config.packages.length === 0) {
1531
+ throw new Error(
1532
+ "No packages configured. Run `nocaap setup` or `nocaap add` first."
1533
+ );
1534
+ }
1535
+ if (semantic) {
1536
+ const embeddingSettings2 = await resolveEmbeddingSettings(projectRoot);
1537
+ setEmbeddingSettings(embeddingSettings2);
1538
+ }
1539
+ log.info(`Building search index for ${config.packages.length} package(s)...`);
1540
+ const allChunks = [];
1541
+ let totalFiles = 0;
1542
+ const indexedPackages = [];
1543
+ for (const pkg2 of config.packages) {
1544
+ const packagePath = getPackagePath(projectRoot, pkg2.alias);
1545
+ if (!await exists(packagePath)) {
1546
+ log.warn(`Package directory not found: ${pkg2.alias}`);
1547
+ continue;
1548
+ }
1549
+ const chunks = await withSpinner(
1550
+ `Chunking ${pkg2.alias}...`,
1551
+ async () => chunkPackage(packagePath, pkg2.alias, contextDir),
1552
+ { successText: `Chunked ${pkg2.alias}` }
1553
+ );
1554
+ if (chunks.length > 0) {
1555
+ allChunks.push(...chunks);
1556
+ indexedPackages.push(pkg2.alias);
1557
+ const uniquePaths = new Set(chunks.map((c) => c.path));
1558
+ totalFiles += uniquePaths.size;
1559
+ } else {
1560
+ log.warn(`No content found in package: ${pkg2.alias}`);
1561
+ }
1562
+ }
1563
+ if (allChunks.length === 0) {
1564
+ throw new Error("No content to index. Check your package directories.");
1565
+ }
1566
+ const searchEngine = new SearchEngine();
1567
+ await withSpinner(
1568
+ "Building fulltext index...",
1569
+ async () => {
1570
+ await searchEngine.createIndex(allChunks);
1571
+ await searchEngine.saveIndex(projectRoot);
1572
+ },
1573
+ { successText: "Fulltext index built" }
1574
+ );
1575
+ const indexPath = getSearchIndexPath(projectRoot);
1576
+ const result = {
1577
+ chunkCount: allChunks.length,
1578
+ fileCount: totalFiles,
1579
+ packages: indexedPackages,
1580
+ indexPath
1581
+ };
1582
+ if (semantic) {
1583
+ result.vectorIndexPath = getVectorStorePath(projectRoot);
1584
+ await buildVectorIndex(allChunks, projectRoot, provider, result);
1585
+ }
1586
+ log.success(
1587
+ `Indexed ${allChunks.length} chunks from ${totalFiles} files across ${indexedPackages.length} package(s)`
1588
+ );
1589
+ if (result.embeddingProvider) {
1590
+ log.info(`Semantic search enabled (${result.embeddingProvider}/${result.embeddingModel})`);
1591
+ }
1592
+ return result;
1593
+ }
1594
+ async function buildVectorIndex(chunks, projectRoot, providerOption, result) {
1595
+ const resolvedProvider = providerOption === "auto" ? await withSpinner(
1596
+ "Detecting embedding provider...",
1597
+ async () => detectProvider(),
1598
+ { successText: "Provider detected" }
1599
+ ) : providerOption;
1600
+ const config = getProviderConfig(resolvedProvider);
1601
+ result.embeddingProvider = resolvedProvider;
1602
+ result.embeddingModel = config.model;
1603
+ log.info(`Using ${resolvedProvider} (${config.model}, ${config.dimensions}d)`);
1604
+ const texts = chunks.map((c) => c.content);
1605
+ const embeddingResult = await withSpinner(
1606
+ `Generating embeddings for ${chunks.length} chunks...`,
1607
+ async () => generateEmbeddings(texts, resolvedProvider),
1608
+ { successText: "Embeddings generated" }
1609
+ );
1610
+ const vectorChunks = chunks.map((chunk, i) => ({
1611
+ id: chunk.id,
1612
+ content: chunk.content,
1613
+ path: chunk.path,
1614
+ package: chunk.package,
1615
+ title: chunk.metadata.title,
1616
+ vector: embeddingResult.vectors[i]
1617
+ }));
1618
+ const vectorStore = new VectorStore(projectRoot);
1619
+ await withSpinner(
1620
+ "Creating vector index...",
1621
+ async () => {
1622
+ await vectorStore.createIndex(vectorChunks, {
1623
+ provider: resolvedProvider,
1624
+ model: embeddingResult.model,
1625
+ dimensions: embeddingResult.dimensions,
1626
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1627
+ });
1628
+ },
1629
+ { successText: "Vector index created" }
1630
+ );
1631
+ }
1632
+ var init_index_search = __esm({
1633
+ "src/commands/index-search.ts"() {
1634
+ init_esm_shims();
1635
+ init_config();
1636
+ init_chunker();
1637
+ init_search_engine();
1638
+ init_vector_store();
1639
+ init_embeddings();
1640
+ init_settings();
1641
+ init_paths();
1642
+ init_logger();
1643
+ }
1644
+ });
1645
+
1646
+ // src/commands/wizard/index-wizard.ts
1647
+ var index_wizard_exports = {};
1648
+ __export(index_wizard_exports, {
1649
+ runIndexWizard: () => runIndexWizard
1650
+ });
1651
+ async function runIndexWizard(options) {
1652
+ const { skipPrompts = false } = options;
1653
+ if (!skipPrompts) {
1654
+ const wantIndex = await confirm({
1655
+ message: "Would you like to build a search index now?",
1656
+ default: true
1657
+ });
1658
+ if (!wantIndex) {
1659
+ log.dim("Skipped indexing. Run `nocaap index` later to enable search.");
1660
+ return { indexed: false, semantic: false };
1661
+ }
1662
+ }
1663
+ let useSemantic = false;
1664
+ if (!skipPrompts) {
1665
+ useSemantic = await confirm({
1666
+ message: "Enable semantic search? (understands meaning, not just keywords)",
1667
+ default: false
1668
+ });
1669
+ }
1670
+ let resolvedProvider;
1671
+ if (useSemantic) {
1672
+ resolvedProvider = await withSpinner(
1673
+ "Detecting embedding providers...",
1674
+ async () => detectProvider(),
1675
+ { successText: "Provider detected" }
1676
+ );
1677
+ const config = getProviderConfig(resolvedProvider);
1678
+ log.info(`Using ${style.bold(config.model)} for embeddings`);
1679
+ }
1680
+ log.newline();
1681
+ let result;
1682
+ try {
1683
+ result = await indexSearchCommand({
1684
+ semantic: useSemantic,
1685
+ provider: useSemantic ? resolvedProvider : void 0
1686
+ });
1687
+ } catch (error) {
1688
+ const message = error instanceof Error ? error.message : "Unknown error";
1689
+ log.error(`Failed to build index: ${message}`);
1690
+ return { indexed: false, semantic: false };
1691
+ }
1692
+ return {
1693
+ indexed: true,
1694
+ semantic: useSemantic,
1695
+ chunkCount: result.chunkCount,
1696
+ provider: result.embeddingProvider
1697
+ };
482
1698
  }
1699
+ var init_index_wizard = __esm({
1700
+ "src/commands/wizard/index-wizard.ts"() {
1701
+ init_esm_shims();
1702
+ init_logger();
1703
+ init_index_search();
1704
+ init_embeddings();
1705
+ }
1706
+ });
1707
+
1708
+ // src/index.ts
1709
+ init_esm_shims();
1710
+
1711
+ // src/commands/setup.ts
1712
+ init_esm_shims();
1713
+ init_paths();
1714
+ init_logger();
1715
+ init_config();
1716
+ init_global_config();
1717
+
1718
+ // src/core/registry.ts
1719
+ init_esm_shims();
1720
+ init_schemas();
1721
+ init_logger();
1722
+ init_paths();
1723
+
1724
+ // src/core/git-engine.ts
1725
+ init_esm_shims();
1726
+ init_paths();
1727
+ init_logger();
483
1728
  function createGit(baseDir) {
484
1729
  const options = {
485
1730
  baseDir: baseDir ? toUnix(baseDir) : void 0,
@@ -787,9 +2032,9 @@ function normalizeRegistryUrl(url) {
787
2032
  /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/
788
2033
  );
789
2034
  if (rawGitHubMatch) {
790
- const [, org, repo, branchName, path2] = rawGitHubMatch;
2035
+ const [, org, repo, branchName, path3] = rawGitHubMatch;
791
2036
  gitUrl = `git@github.com:${org}/${repo}.git`;
792
- filePath = path2;
2037
+ filePath = path3;
793
2038
  httpUrl = original;
794
2039
  provider = "github";
795
2040
  branch = branchName ?? null;
@@ -799,10 +2044,10 @@ function normalizeRegistryUrl(url) {
799
2044
  /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/
800
2045
  );
801
2046
  if (blobMatch) {
802
- const [, org, repo, branchName, path2] = blobMatch;
2047
+ const [, org, repo, branchName, path3] = blobMatch;
803
2048
  gitUrl = `git@github.com:${org}/${repo}.git`;
804
- filePath = path2;
805
- httpUrl = `https://raw.githubusercontent.com/${org}/${repo}/${branchName}/${path2}`;
2049
+ filePath = path3;
2050
+ httpUrl = `https://raw.githubusercontent.com/${org}/${repo}/${branchName}/${path3}`;
806
2051
  provider = "github";
807
2052
  branch = branchName ?? null;
808
2053
  return { original, gitUrl, filePath, httpUrl, provider, branch };
@@ -1081,8 +2326,8 @@ async function fetchRegistryWithImports(url, options) {
1081
2326
  return mergeRegistries([registry, ...importedRegistries]);
1082
2327
  }
1083
2328
  function getContextKey(context) {
1084
- const path2 = context.path ?? "";
1085
- return `${context.repo}::${path2}`;
2329
+ const path3 = context.path ?? "";
2330
+ return `${context.repo}::${path3}`;
1086
2331
  }
1087
2332
  function mergeRegistries(registries) {
1088
2333
  const contextMap = /* @__PURE__ */ new Map();
@@ -1101,6 +2346,12 @@ function mergeRegistries(registries) {
1101
2346
  // Don't include imports in merged result (they've been resolved)
1102
2347
  };
1103
2348
  }
2349
+
2350
+ // src/core/indexer.ts
2351
+ init_esm_shims();
2352
+ init_paths();
2353
+ init_config();
2354
+ init_logger();
1104
2355
  var CHARS_PER_TOKEN = 4;
1105
2356
  var TOKEN_BUDGET_WARNING = 8e3;
1106
2357
  var MAX_PREVIEW_LINES = 5;
@@ -1110,7 +2361,7 @@ async function parseDocFile(filePath, basePath) {
1110
2361
  const relativePath = relative(basePath, normalizedPath);
1111
2362
  log.debug(`Parsing doc file: ${relativePath}`);
1112
2363
  const content = await fs2.readFile(normalizedPath, "utf-8");
1113
- const { data: frontmatter, content: body } = matter(content);
2364
+ const { data: frontmatter, content: body } = matter2(content);
1114
2365
  const title = extractTitle(frontmatter, body, normalizedPath);
1115
2366
  const summary = frontmatter.summary ?? frontmatter.description;
1116
2367
  const type = frontmatter.type;
@@ -1188,7 +2439,6 @@ async function findDocFiles(dirPath) {
1188
2439
  }
1189
2440
  async function generateIndex(projectRoot) {
1190
2441
  const contextDir = getContextDir(projectRoot);
1191
- getPackagesDir(projectRoot);
1192
2442
  const warnings = [];
1193
2443
  log.debug(`Generating index for ${projectRoot}`);
1194
2444
  const config = await readConfig(projectRoot);
@@ -1203,17 +2453,17 @@ async function generateIndex(projectRoot) {
1203
2453
  }
1204
2454
  const packageIndexes = [];
1205
2455
  let totalFiles = 0;
1206
- for (const pkg of config.packages) {
1207
- const packagePath = getPackagePath(projectRoot, pkg.alias);
2456
+ for (const pkg2 of config.packages) {
2457
+ const packagePath = getPackagePath(projectRoot, pkg2.alias);
1208
2458
  if (!await exists(packagePath)) {
1209
- log.debug(`Package directory not found: ${pkg.alias}`);
1210
- warnings.push(`Package '${pkg.alias}' directory not found`);
2459
+ log.debug(`Package directory not found: ${pkg2.alias}`);
2460
+ warnings.push(`Package '${pkg2.alias}' directory not found`);
1211
2461
  continue;
1212
2462
  }
1213
2463
  const docFiles = await findDocFiles(packagePath);
1214
2464
  if (docFiles.length === 0) {
1215
- log.debug(`No documentation files found in package: ${pkg.alias}`);
1216
- warnings.push(`No .md/.mdx files found in '${pkg.alias}'`);
2465
+ log.debug(`No documentation files found in package: ${pkg2.alias}`);
2466
+ warnings.push(`No .md/.mdx files found in '${pkg2.alias}'`);
1217
2467
  continue;
1218
2468
  }
1219
2469
  const fileMetas = [];
@@ -1229,7 +2479,7 @@ async function generateIndex(projectRoot) {
1229
2479
  }
1230
2480
  fileMetas.sort((a, b) => a.title.localeCompare(b.title));
1231
2481
  packageIndexes.push({
1232
- alias: pkg.alias,
2482
+ alias: pkg2.alias,
1233
2483
  files: fileMetas
1234
2484
  });
1235
2485
  totalFiles += fileMetas.length;
@@ -1272,18 +2522,18 @@ function generateIndexMarkdown(packages) {
1272
2522
  if (packages.length > 1) {
1273
2523
  lines.push("## Table of Contents");
1274
2524
  lines.push("");
1275
- for (const pkg of packages) {
1276
- const anchorId = pkg.alias.toLowerCase().replace(/[^a-z0-9]+/g, "-");
1277
- lines.push(`- [${pkg.alias}](#${anchorId}) (${pkg.files.length} files)`);
2525
+ for (const pkg2 of packages) {
2526
+ const anchorId = pkg2.alias.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2527
+ lines.push(`- [${pkg2.alias}](#${anchorId}) (${pkg2.files.length} files)`);
1278
2528
  }
1279
2529
  lines.push("");
1280
2530
  lines.push("---");
1281
2531
  lines.push("");
1282
2532
  }
1283
- for (const pkg of packages) {
1284
- lines.push(`## ${pkg.alias} (${pkg.files.length} files)`);
2533
+ for (const pkg2 of packages) {
2534
+ lines.push(`## ${pkg2.alias} (${pkg2.files.length} files)`);
1285
2535
  lines.push("");
1286
- for (const file of pkg.files) {
2536
+ for (const file of pkg2.files) {
1287
2537
  lines.push(`### ${file.title}`);
1288
2538
  lines.push("");
1289
2539
  lines.push(`**Path:** \`${file.relativePath}\``);
@@ -1309,6 +2559,13 @@ async function writeIndex(projectRoot) {
1309
2559
  log.debug(`Wrote INDEX.md to ${indexPath}`);
1310
2560
  return result;
1311
2561
  }
2562
+ async function readIndex(projectRoot) {
2563
+ const indexPath = getIndexPath(projectRoot);
2564
+ if (!await exists(indexPath)) {
2565
+ return null;
2566
+ }
2567
+ return fs2.readFile(indexPath, "utf-8");
2568
+ }
1312
2569
  async function generateIndexWithProgress(projectRoot) {
1313
2570
  log.info("Regenerating INDEX.md...");
1314
2571
  const result = await writeIndex(projectRoot);
@@ -1429,11 +2686,27 @@ async function setupCommand(options) {
1429
2686
  return;
1430
2687
  }
1431
2688
  log.newline();
1432
- const choices = accessibleContexts.map(({ context }) => ({
1433
- name: formatContextChoice(context),
1434
- value: context.name,
1435
- checked: false
1436
- }));
2689
+ const currentConfig = await readConfig(projectRoot);
2690
+ const installedAliases = new Set(
2691
+ currentConfig?.packages.map((p) => p.alias) ?? []
2692
+ );
2693
+ const choices = accessibleContexts.map(({ context }) => {
2694
+ const alias = generateAlias(context);
2695
+ const isInstalled = installedAliases.has(alias);
2696
+ return {
2697
+ name: isInstalled ? `${formatContextChoice(context)} ${style.dim("[already installed]")}` : formatContextChoice(context),
2698
+ value: context.name,
2699
+ checked: false,
2700
+ disabled: isInstalled ? "Already installed" : false
2701
+ };
2702
+ });
2703
+ const installedCount = [...installedAliases].filter(
2704
+ (alias) => accessibleContexts.some(({ context }) => generateAlias(context) === alias)
2705
+ ).length;
2706
+ if (installedCount > 0) {
2707
+ log.info(`${installedCount} package(s) already installed (shown as disabled)`);
2708
+ log.newline();
2709
+ }
1437
2710
  const selectedNames = await checkbox({
1438
2711
  message: "Select contexts to install:",
1439
2712
  choices,
@@ -1449,9 +2722,9 @@ async function setupCommand(options) {
1449
2722
  await initContextDir(projectRoot);
1450
2723
  initSpinner.succeed("Initialized .context/ directory");
1451
2724
  }
1452
- const existingConfig = await readConfig(projectRoot) ?? { packages: [] };
1453
- existingConfig.registryUrl = registryUrl;
1454
- await writeConfig(projectRoot, existingConfig);
2725
+ const configToUpdate = await readConfig(projectRoot) ?? { packages: [] };
2726
+ configToUpdate.registryUrl = registryUrl;
2727
+ await writeConfig(projectRoot, configToUpdate);
1455
2728
  log.newline();
1456
2729
  log.info(`Installing ${selectedNames.length} context(s)...`);
1457
2730
  log.newline();
@@ -1461,6 +2734,10 @@ async function setupCommand(options) {
1461
2734
  const context = accessibleContexts.find((r) => r.context.name === name)?.context;
1462
2735
  if (!context) continue;
1463
2736
  const alias = generateAlias(context);
2737
+ if (installedAliases.has(alias)) {
2738
+ log.dim(`Skipping ${alias} (already installed)`);
2739
+ continue;
2740
+ }
1464
2741
  const spinner = createSpinner(`Installing ${style.bold(context.name)}...`).start();
1465
2742
  try {
1466
2743
  const targetDir = getPackagePath(projectRoot, alias);
@@ -1495,21 +2772,13 @@ async function setupCommand(options) {
1495
2772
  if (successCount > 0) {
1496
2773
  log.newline();
1497
2774
  log.hr();
2775
+ const { runIndexWizard: runIndexWizard2 } = await Promise.resolve().then(() => (init_index_wizard(), index_wizard_exports));
2776
+ await runIndexWizard2({ projectRoot });
2777
+ }
2778
+ if (successCount > 0) {
1498
2779
  log.newline();
1499
- log.info("IDE Integration (optional)");
2780
+ log.hr();
1500
2781
  log.newline();
1501
- const addCursor = await confirm({
1502
- message: "Add nocaap reference to Cursor rules?",
1503
- default: true
1504
- });
1505
- if (addCursor) {
1506
- const updated = await updateCursorRules(projectRoot);
1507
- if (updated) {
1508
- log.success("Added nocaap reference to Cursor rules");
1509
- } else {
1510
- log.dim("Cursor rules already configured");
1511
- }
1512
- }
1513
2782
  const addClaude = await confirm({
1514
2783
  message: "Add nocaap reference to CLAUDE.md?",
1515
2784
  default: true
@@ -1559,15 +2828,19 @@ function generateAlias(context) {
1559
2828
  }
1560
2829
 
1561
2830
  // src/commands/add.ts
2831
+ init_esm_shims();
2832
+ init_paths();
2833
+ init_logger();
2834
+ init_config();
1562
2835
  function extractAliasFromUrl(url) {
1563
2836
  const cleaned = url.replace(/\.git$/, "");
1564
2837
  const segments = cleaned.split(/[/:]/);
1565
2838
  const lastSegment = segments[segments.length - 1];
1566
2839
  return lastSegment?.replace(/[^a-zA-Z0-9-_]/g, "") || "context";
1567
2840
  }
1568
- function deriveAlias(url, path2) {
1569
- if (path2) {
1570
- const leafFolder = path2.split("/").filter(Boolean).pop();
2841
+ function deriveAlias(url, path3) {
2842
+ if (path3) {
2843
+ const leafFolder = path3.split("/").filter(Boolean).pop();
1571
2844
  if (leafFolder) {
1572
2845
  return leafFolder.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
1573
2846
  }
@@ -1649,6 +2922,10 @@ Please check:
1649
2922
  }
1650
2923
 
1651
2924
  // src/commands/update.ts
2925
+ init_esm_shims();
2926
+ init_paths();
2927
+ init_logger();
2928
+ init_config();
1652
2929
  async function updateCommand(alias, options) {
1653
2930
  const projectRoot = process.cwd();
1654
2931
  log.title("Updating context packages");
@@ -1665,8 +2942,8 @@ async function updateCommand(alias, options) {
1665
2942
  log.info(`Updating ${packagesToUpdate.length} package(s)...`);
1666
2943
  log.newline();
1667
2944
  const results = [];
1668
- for (const pkg of packagesToUpdate) {
1669
- const result = await updatePackage(projectRoot, pkg, options);
2945
+ for (const pkg2 of packagesToUpdate) {
2946
+ const result = await updatePackage(projectRoot, pkg2, options);
1670
2947
  results.push(result);
1671
2948
  }
1672
2949
  log.newline();
@@ -1705,41 +2982,41 @@ async function updateCommand(alias, options) {
1705
2982
  throw new Error(`${errors.length} package(s) failed to update`);
1706
2983
  }
1707
2984
  }
1708
- async function updatePackage(projectRoot, pkg, options) {
1709
- const packagePath = getPackagePath(projectRoot, pkg.alias);
1710
- const spinner = createSpinner(`Updating ${style.bold(pkg.alias)}...`).start();
2985
+ async function updatePackage(projectRoot, pkg2, options) {
2986
+ const packagePath = getPackagePath(projectRoot, pkg2.alias);
2987
+ const spinner = createSpinner(`Updating ${style.bold(pkg2.alias)}...`).start();
1711
2988
  try {
1712
2989
  if (!await exists(packagePath)) {
1713
- spinner.warn(`${pkg.alias}: Package directory not found`);
2990
+ spinner.warn(`${pkg2.alias}: Package directory not found`);
1714
2991
  return {
1715
- alias: pkg.alias,
2992
+ alias: pkg2.alias,
1716
2993
  status: "skipped",
1717
2994
  error: "Directory not found"
1718
2995
  };
1719
2996
  }
1720
2997
  if (!await isGitRepo(packagePath)) {
1721
- spinner.warn(`${pkg.alias}: Not a git repository`);
2998
+ spinner.warn(`${pkg2.alias}: Not a git repository`);
1722
2999
  return {
1723
- alias: pkg.alias,
3000
+ alias: pkg2.alias,
1724
3001
  status: "skipped",
1725
3002
  error: "Not a git repository"
1726
3003
  };
1727
3004
  }
1728
3005
  if (await isDirty(packagePath)) {
1729
- spinner.warn(`${pkg.alias}: Has uncommitted changes`);
3006
+ spinner.warn(`${pkg2.alias}: Has uncommitted changes`);
1730
3007
  return {
1731
- alias: pkg.alias,
3008
+ alias: pkg2.alias,
1732
3009
  status: "skipped",
1733
3010
  error: "Uncommitted changes (commit or discard first)"
1734
3011
  };
1735
3012
  }
1736
- const lockEntry = await getLockEntry(projectRoot, pkg.alias);
1737
- if (lockEntry && lockEntry.sparsePath !== (pkg.path || "")) {
1738
- spinner.warn(`${pkg.alias}: Sparse path changed in config`);
1739
- log.dim(` Config: ${pkg.path || "(root)"}`);
3013
+ const lockEntry = await getLockEntry(projectRoot, pkg2.alias);
3014
+ if (lockEntry && lockEntry.sparsePath !== (pkg2.path || "")) {
3015
+ spinner.warn(`${pkg2.alias}: Sparse path changed in config`);
3016
+ log.dim(` Config: ${pkg2.path || "(root)"}`);
1740
3017
  log.dim(` Locked: ${lockEntry.sparsePath || "(root)"}`);
1741
3018
  return {
1742
- alias: pkg.alias,
3019
+ alias: pkg2.alias,
1743
3020
  status: "skipped",
1744
3021
  error: "Sparse path changed (run `nocaap remove` then `nocaap add` to re-clone)"
1745
3022
  };
@@ -1747,31 +3024,31 @@ async function updatePackage(projectRoot, pkg, options) {
1747
3024
  const oldCommit = await getHeadCommit(packagePath);
1748
3025
  const { commitHash: newCommit } = await pull(packagePath);
1749
3026
  if (oldCommit === newCommit && !options.force) {
1750
- spinner.info(`${pkg.alias}: Already up-to-date`);
3027
+ spinner.info(`${pkg2.alias}: Already up-to-date`);
1751
3028
  return {
1752
- alias: pkg.alias,
3029
+ alias: pkg2.alias,
1753
3030
  status: "up-to-date",
1754
3031
  oldCommit,
1755
3032
  newCommit
1756
3033
  };
1757
3034
  }
1758
- await updateLockEntry(projectRoot, pkg.alias, {
3035
+ await updateLockEntry(projectRoot, pkg2.alias, {
1759
3036
  commitHash: newCommit,
1760
- sparsePath: pkg.path || "",
3037
+ sparsePath: pkg2.path || "",
1761
3038
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1762
3039
  });
1763
- spinner.succeed(`${pkg.alias}: Updated`);
3040
+ spinner.succeed(`${pkg2.alias}: Updated`);
1764
3041
  return {
1765
- alias: pkg.alias,
3042
+ alias: pkg2.alias,
1766
3043
  status: "updated",
1767
3044
  oldCommit,
1768
3045
  newCommit
1769
3046
  };
1770
3047
  } catch (error) {
1771
3048
  const message = error instanceof Error ? error.message : "Unknown error";
1772
- spinner.fail(`${pkg.alias}: Failed`);
3049
+ spinner.fail(`${pkg2.alias}: Failed`);
1773
3050
  return {
1774
- alias: pkg.alias,
3051
+ alias: pkg2.alias,
1775
3052
  status: "error",
1776
3053
  error: message
1777
3054
  };
@@ -1779,6 +3056,10 @@ async function updatePackage(projectRoot, pkg, options) {
1779
3056
  }
1780
3057
 
1781
3058
  // src/commands/list.ts
3059
+ init_esm_shims();
3060
+ init_paths();
3061
+ init_logger();
3062
+ init_config();
1782
3063
  async function listCommand() {
1783
3064
  const projectRoot = process.cwd();
1784
3065
  const config = await readConfig(projectRoot);
@@ -1793,10 +3074,10 @@ async function listCommand() {
1793
3074
  log.dim(`Registry: ${config.registryUrl}`);
1794
3075
  log.newline();
1795
3076
  }
1796
- for (const pkg of config.packages) {
1797
- const lock = lockfile[pkg.alias];
3077
+ for (const pkg2 of config.packages) {
3078
+ const lock = lockfile[pkg2.alias];
1798
3079
  const commit = lock?.commitHash?.slice(0, 8) ?? "unknown";
1799
- const packagePath = getPackagePath(projectRoot, pkg.alias);
3080
+ const packagePath = getPackagePath(projectRoot, pkg2.alias);
1800
3081
  let statusIndicator = style.success("\u25CF");
1801
3082
  let statusText = "";
1802
3083
  if (!await exists(packagePath)) {
@@ -1811,12 +3092,12 @@ async function listCommand() {
1811
3092
  } catch {
1812
3093
  }
1813
3094
  }
1814
- log.plain(`${statusIndicator} ${style.bold(pkg.alias)}${statusText}`);
1815
- log.dim(` Source: ${pkg.source}`);
1816
- if (pkg.path) {
1817
- log.dim(` Path: ${pkg.path}`);
3095
+ log.plain(`${statusIndicator} ${style.bold(pkg2.alias)}${statusText}`);
3096
+ log.dim(` Source: ${pkg2.source}`);
3097
+ if (pkg2.path) {
3098
+ log.dim(` Path: ${pkg2.path}`);
1818
3099
  }
1819
- log.dim(` Branch: ${pkg.version || "main"}`);
3100
+ log.dim(` Branch: ${pkg2.version || "main"}`);
1820
3101
  log.dim(` Commit: ${commit}`);
1821
3102
  if (lock?.updatedAt) {
1822
3103
  const updated = new Date(lock.updatedAt).toLocaleDateString();
@@ -1827,20 +3108,26 @@ async function listCommand() {
1827
3108
  log.hr();
1828
3109
  log.dim(`${config.packages.length} package(s) installed`);
1829
3110
  }
3111
+
3112
+ // src/commands/remove.ts
3113
+ init_esm_shims();
3114
+ init_paths();
3115
+ init_logger();
3116
+ init_config();
1830
3117
  async function removeCommand(alias, options) {
1831
3118
  const projectRoot = process.cwd();
1832
3119
  log.title("Removing context package");
1833
- const pkg = await getPackage(projectRoot, alias);
1834
- if (!pkg) {
3120
+ const pkg2 = await getPackage(projectRoot, alias);
3121
+ if (!pkg2) {
1835
3122
  throw new Error(
1836
3123
  `Package '${alias}' not found in configuration.
1837
3124
  Run \`nocaap list\` to see installed packages.`
1838
3125
  );
1839
3126
  }
1840
3127
  log.info(`Package: ${alias}`);
1841
- log.dim(` Source: ${pkg.source}`);
1842
- if (pkg.path) {
1843
- log.dim(` Path: ${pkg.path}`);
3128
+ log.dim(` Source: ${pkg2.source}`);
3129
+ if (pkg2.path) {
3130
+ log.dim(` Path: ${pkg2.path}`);
1844
3131
  }
1845
3132
  log.newline();
1846
3133
  const packagePath = getPackagePath(projectRoot, alias);
@@ -1866,8 +3153,8 @@ Run \`nocaap list\` to see installed packages.`
1866
3153
  const dirSpinner = createSpinner("Removing package directory...").start();
1867
3154
  try {
1868
3155
  if (await exists(packagePath)) {
1869
- const fs8 = await import('fs-extra');
1870
- await fs8.default.remove(packagePath);
3156
+ const fs12 = await import('fs-extra');
3157
+ await fs12.default.remove(packagePath);
1871
3158
  }
1872
3159
  dirSpinner.succeed("Removed package directory");
1873
3160
  } catch (error) {
@@ -1890,92 +3177,290 @@ Run \`nocaap list\` to see installed packages.`
1890
3177
  }
1891
3178
 
1892
3179
  // src/commands/config.ts
3180
+ init_esm_shims();
3181
+ init_logger();
3182
+ init_global_config();
3183
+ init_config();
3184
+ init_settings();
3185
+ init_paths();
3186
+ var CONFIG_KEYS = {
3187
+ "registry": { scope: "global", desc: "Default registry URL" },
3188
+ "push.baseBranch": { scope: "both", desc: "Default PR target branch" },
3189
+ "embedding.provider": { scope: "global", desc: "Embedding provider (ollama|openai|tfjs|auto)" },
3190
+ "embedding.ollamaModel": { scope: "global", desc: "Ollama model name" },
3191
+ "embedding.ollamaBaseUrl": { scope: "global", desc: "Ollama server URL" },
3192
+ "search.fulltextWeight": { scope: "project", desc: "BM25 weight (0-1)" },
3193
+ "search.vectorWeight": { scope: "project", desc: "Vector weight (0-1)" },
3194
+ "search.rrfK": { scope: "project", desc: "RRF smoothing constant" },
3195
+ "index.semantic": { scope: "project", desc: "Enable semantic indexing by default" },
3196
+ "index.provider": { scope: "project", desc: "Index embedding provider" }
3197
+ };
1893
3198
  async function configCommand(key, value, options) {
1894
- if (options.list || !key && !value) {
1895
- await showAllConfig();
3199
+ if (options.list || !key) {
3200
+ await showConfig(options);
1896
3201
  return;
1897
3202
  }
1898
3203
  switch (key) {
1899
- case "registry":
1900
- await handleRegistryConfig(value, options.clear);
3204
+ case "list":
3205
+ await showConfig(options);
1901
3206
  break;
1902
- default:
1903
- log.error(`Unknown config key: ${style.code(key ?? "")}`);
1904
- log.newline();
1905
- log.info("Available keys:");
1906
- log.dim(" registry - Default registry URL for `nocaap setup`");
3207
+ case "get":
3208
+ if (!value) {
3209
+ log.error("Usage: nocaap config get <key>");
3210
+ showAvailableKeys();
3211
+ return;
3212
+ }
3213
+ await getConfigValue(value);
1907
3214
  break;
3215
+ case "set":
3216
+ log.error("Usage: nocaap config set <key> <value>");
3217
+ log.dim("Example: nocaap config set push.baseBranch develop");
3218
+ break;
3219
+ case "unset":
3220
+ if (!value) {
3221
+ log.error("Usage: nocaap config unset <key>");
3222
+ showAvailableKeys();
3223
+ return;
3224
+ }
3225
+ await unsetConfigValue(value, options);
3226
+ break;
3227
+ default:
3228
+ if (value !== void 0) {
3229
+ await setConfigValue(key, value, options);
3230
+ } else {
3231
+ await getConfigValue(key);
3232
+ }
1908
3233
  }
1909
3234
  }
1910
- async function showAllConfig() {
1911
- const config = await getGlobalConfig();
1912
- const configPath = getGlobalConfigPath();
1913
- log.title("Global Configuration");
1914
- log.dim(`Location: ${configPath}`);
3235
+ async function showConfig(options) {
3236
+ const projectRoot = process.cwd();
3237
+ const showGlobal = !options.project;
3238
+ const showProject = !options.global;
3239
+ log.title("Configuration");
1915
3240
  log.newline();
1916
- if (Object.keys(config).length === 0 || !config.defaultRegistry) {
1917
- log.info("No configuration set.");
3241
+ if (showGlobal) {
3242
+ const globalPath = getGlobalConfigPath();
3243
+ log.info(style.bold("Global") + style.dim(` (${globalPath})`));
3244
+ const globalConfig = await getGlobalConfig();
3245
+ if (Object.keys(globalConfig).length === 0) {
3246
+ log.dim(" (empty)");
3247
+ } else {
3248
+ printConfigObject(globalConfig, " ");
3249
+ }
1918
3250
  log.newline();
1919
- log.dim("Set your organization's registry:");
1920
- log.dim(" nocaap config registry <url>");
1921
- return;
1922
- }
1923
- if (config.defaultRegistry) {
1924
- log.info(`${style.bold("registry")}: ${config.defaultRegistry}`);
1925
3251
  }
1926
- if (config.updatedAt) {
3252
+ if (showProject) {
3253
+ const hasProject = await configExists(projectRoot);
3254
+ const projectPath = getConfigPath(projectRoot);
3255
+ log.info(style.bold("Project") + style.dim(` (${projectPath})`));
3256
+ if (!hasProject) {
3257
+ log.dim(" (no project config - run nocaap setup first)");
3258
+ } else {
3259
+ const projectConfig = await readConfig(projectRoot);
3260
+ if (projectConfig) {
3261
+ const { search: search2, push, index } = projectConfig;
3262
+ const settings = { search: search2, push, index };
3263
+ const hasSettings = Object.values(settings).some((v) => v !== void 0);
3264
+ if (hasSettings) {
3265
+ printConfigObject(settings, " ");
3266
+ } else {
3267
+ log.dim(" (no settings configured)");
3268
+ }
3269
+ }
3270
+ }
1927
3271
  log.newline();
1928
- log.dim(`Last updated: ${new Date(config.updatedAt).toLocaleString()}`);
3272
+ }
3273
+ if (showGlobal && showProject) {
3274
+ try {
3275
+ const resolved = await resolveSettings(projectRoot);
3276
+ log.info(style.bold("Effective Settings") + style.dim(" (merged)"));
3277
+ printConfigObject(resolved, " ");
3278
+ } catch {
3279
+ }
1929
3280
  }
1930
3281
  }
1931
- async function handleRegistryConfig(value, clear) {
1932
- if (clear) {
1933
- await clearDefaultRegistry();
1934
- log.success("Default registry cleared.");
3282
+ async function getConfigValue(key) {
3283
+ const projectRoot = process.cwd();
3284
+ if (!CONFIG_KEYS[key] && key !== "registry") {
3285
+ log.error(`Unknown config key: ${style.code(key)}`);
3286
+ showAvailableKeys();
1935
3287
  return;
1936
3288
  }
1937
- if (value) {
1938
- try {
1939
- new URL(value);
1940
- } catch {
1941
- throw new Error(
1942
- `Invalid URL: ${value}
1943
- Please provide a valid URL, e.g.:
1944
- https://raw.githubusercontent.com/your-org/hub/main/nocaap-registry.json`
1945
- );
3289
+ try {
3290
+ const resolved = await resolveSettings(projectRoot);
3291
+ const value = getNestedValue(resolved, key);
3292
+ if (value !== void 0) {
3293
+ log.info(`${key}: ${formatValue(value)}`);
3294
+ } else {
3295
+ log.info(`${key}: ${style.dim("(not set)")}`);
1946
3296
  }
1947
- await setDefaultRegistry(value);
1948
- log.success("Default registry set!");
1949
- log.newline();
1950
- log.info(`Registry: ${style.url(value)}`);
1951
- log.newline();
1952
- log.dim("Now you can run `nocaap setup` without the --registry flag.");
3297
+ } catch {
3298
+ const globalConfig = await getGlobalConfig();
3299
+ const value = getNestedValue(globalConfig, key);
3300
+ if (value !== void 0) {
3301
+ log.info(`${key}: ${formatValue(value)}`);
3302
+ } else {
3303
+ log.info(`${key}: ${style.dim("(not set)")}`);
3304
+ }
3305
+ }
3306
+ }
3307
+ async function setConfigValue(key, value, options) {
3308
+ const projectRoot = process.cwd();
3309
+ const keyInfo = CONFIG_KEYS[key];
3310
+ if (!keyInfo && key !== "registry") {
3311
+ log.error(`Unknown config key: ${style.code(key)}`);
3312
+ showAvailableKeys();
3313
+ return;
3314
+ }
3315
+ const scope = options.project ? "project" : "global";
3316
+ if (keyInfo && scope === "project" && keyInfo.scope === "global") {
3317
+ log.error(`Key '${key}' can only be set globally.`);
3318
+ log.dim("Run without --project flag.");
3319
+ return;
3320
+ }
3321
+ if (keyInfo && scope === "global" && keyInfo.scope === "project") {
3322
+ log.error(`Key '${key}' can only be set at project level.`);
3323
+ log.dim("Use --project flag.");
1953
3324
  return;
1954
3325
  }
1955
- const registry = await getDefaultRegistry();
1956
- if (registry) {
1957
- const envVar = process.env.NOCAAP_DEFAULT_REGISTRY;
1958
- log.info(`Default registry: ${style.url(registry)}`);
1959
- if (envVar) {
1960
- log.dim(" (from NOCAAP_DEFAULT_REGISTRY environment variable)");
3326
+ const parsedValue = parseValue(key, value);
3327
+ if (scope === "global") {
3328
+ await setGlobalValue(key, parsedValue);
3329
+ log.success(`Set ${style.code(key)} = ${formatValue(parsedValue)} (global)`);
3330
+ } else {
3331
+ await setProjectValue(projectRoot, key, parsedValue);
3332
+ log.success(`Set ${style.code(key)} = ${formatValue(parsedValue)} (project)`);
3333
+ }
3334
+ }
3335
+ async function unsetConfigValue(key, options) {
3336
+ const projectRoot = process.cwd();
3337
+ const scope = options.project ? "project" : "global";
3338
+ if (scope === "global") {
3339
+ const config = await getGlobalConfig();
3340
+ deleteNestedValue(config, key);
3341
+ await setGlobalConfig(config);
3342
+ log.success(`Removed ${style.code(key)} from global config`);
3343
+ } else {
3344
+ const config = await readConfig(projectRoot);
3345
+ if (config) {
3346
+ deleteNestedValue(config, key);
3347
+ await writeConfig(projectRoot, config);
3348
+ log.success(`Removed ${style.code(key)} from project config`);
1961
3349
  } else {
1962
- log.dim(" (from global config)");
3350
+ log.warn("No project config found");
1963
3351
  }
3352
+ }
3353
+ }
3354
+ async function setGlobalValue(key, value) {
3355
+ const config = await getGlobalConfig();
3356
+ if (key === "registry") {
3357
+ config.defaultRegistry = value;
1964
3358
  } else {
1965
- log.info("No default registry configured.");
1966
- log.newline();
1967
- log.dim("Set one with:");
1968
- log.dim(" nocaap config registry <url>");
1969
- log.newline();
1970
- log.dim("Example:");
1971
- log.dim(
1972
- " nocaap config registry https://raw.githubusercontent.com/your-org/hub/main/nocaap-registry.json"
1973
- );
3359
+ setNestedValue(config, key, value);
3360
+ }
3361
+ await setGlobalConfig(config);
3362
+ }
3363
+ async function setProjectValue(projectRoot, key, value) {
3364
+ const config = await readConfig(projectRoot);
3365
+ if (!config) {
3366
+ log.error("No project config found. Run `nocaap setup` first.");
3367
+ return;
3368
+ }
3369
+ setNestedValue(config, key, value);
3370
+ await writeConfig(projectRoot, config);
3371
+ }
3372
+ function parseValue(key, value) {
3373
+ if (value === "true") return true;
3374
+ if (value === "false") return false;
3375
+ if (key.includes("Weight") || key === "search.rrfK") {
3376
+ const num = parseFloat(value);
3377
+ if (isNaN(num)) {
3378
+ throw new Error(`Invalid number: ${value}`);
3379
+ }
3380
+ if (key.includes("Weight") && (num < 0 || num > 1)) {
3381
+ throw new Error(`Weight must be between 0 and 1`);
3382
+ }
3383
+ return num;
3384
+ }
3385
+ return value;
3386
+ }
3387
+ function formatValue(value) {
3388
+ if (typeof value === "string") return style.code(value);
3389
+ if (typeof value === "number") return style.code(value.toString());
3390
+ if (typeof value === "boolean") return style.code(value ? "true" : "false");
3391
+ return String(value);
3392
+ }
3393
+ function getNestedValue(obj, path3) {
3394
+ if (path3 === "registry") {
3395
+ return obj.defaultRegistry;
3396
+ }
3397
+ const parts = path3.split(".");
3398
+ let current = obj;
3399
+ for (const part of parts) {
3400
+ if (current && typeof current === "object" && part in current) {
3401
+ current = current[part];
3402
+ } else {
3403
+ return void 0;
3404
+ }
3405
+ }
3406
+ return current;
3407
+ }
3408
+ function setNestedValue(obj, path3, value) {
3409
+ const parts = path3.split(".");
3410
+ let current = obj;
3411
+ for (let i = 0; i < parts.length - 1; i++) {
3412
+ const part = parts[i];
3413
+ if (!(part in current) || typeof current[part] !== "object") {
3414
+ current[part] = {};
3415
+ }
3416
+ current = current[part];
1974
3417
  }
3418
+ const lastPart = parts[parts.length - 1];
3419
+ current[lastPart] = value;
1975
3420
  }
3421
+ function deleteNestedValue(obj, path3) {
3422
+ const parts = path3.split(".");
3423
+ let current = obj;
3424
+ for (let i = 0; i < parts.length - 1; i++) {
3425
+ const part = parts[i];
3426
+ if (!(part in current) || typeof current[part] !== "object") {
3427
+ return;
3428
+ }
3429
+ current = current[part];
3430
+ }
3431
+ const lastPart = parts[parts.length - 1];
3432
+ delete current[lastPart];
3433
+ }
3434
+ function printConfigObject(obj, indent) {
3435
+ for (const [key, value] of Object.entries(obj)) {
3436
+ if (value === void 0) continue;
3437
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
3438
+ log.info(`${indent}${style.bold(key)}:`);
3439
+ printConfigObject(value, indent + " ");
3440
+ } else {
3441
+ log.info(`${indent}${key}: ${formatValue(value)}`);
3442
+ }
3443
+ }
3444
+ }
3445
+ function showAvailableKeys() {
3446
+ log.newline();
3447
+ log.info("Available keys:");
3448
+ for (const [key, info] of Object.entries(CONFIG_KEYS)) {
3449
+ const scopeTag = info.scope === "both" ? "" : ` [${info.scope}]`;
3450
+ log.dim(` ${key}${scopeTag} - ${info.desc}`);
3451
+ }
3452
+ }
3453
+
3454
+ // src/commands/push.ts
3455
+ init_esm_shims();
3456
+ init_paths();
3457
+ init_logger();
3458
+ init_config();
3459
+ init_settings();
1976
3460
 
1977
3461
  // src/utils/providers.ts
1978
- function detectProvider(url) {
3462
+ init_esm_shims();
3463
+ function detectProvider2(url) {
1979
3464
  const normalized = url.toLowerCase();
1980
3465
  if (normalized.includes("github.com") || normalized.includes("github:")) {
1981
3466
  return "github";
@@ -1989,7 +3474,7 @@ function detectProvider(url) {
1989
3474
  return "unknown";
1990
3475
  }
1991
3476
  function parseRepoInfo(url) {
1992
- const provider = detectProvider(url);
3477
+ const provider = detectProvider2(url);
1993
3478
  const cleaned = url.replace(/\.git$/, "");
1994
3479
  const sshMatch = cleaned.match(/git@[^:]+:([^/]+)\/(.+)/);
1995
3480
  if (sshMatch && sshMatch[1] && sshMatch[2]) {
@@ -2013,19 +3498,23 @@ function parseRepoInfo(url) {
2013
3498
  repo: ""
2014
3499
  };
2015
3500
  }
2016
- function buildNewPrUrl(info, branch) {
3501
+ function buildNewPrUrl(info, branch, baseBranch = "main") {
2017
3502
  const { provider, owner, repo } = info;
2018
3503
  switch (provider) {
2019
3504
  case "github":
2020
- return `https://github.com/${owner}/${repo}/compare/main...${branch}?expand=1`;
3505
+ return `https://github.com/${owner}/${repo}/compare/${baseBranch}...${branch}?expand=1`;
2021
3506
  case "gitlab":
2022
- return `https://gitlab.com/${owner}/${repo}/-/merge_requests/new?merge_request[source_branch]=${branch}`;
3507
+ return `https://gitlab.com/${owner}/${repo}/-/merge_requests/new?merge_request[source_branch]=${branch}&merge_request[target_branch]=${baseBranch}`;
2023
3508
  case "bitbucket":
2024
- return `https://bitbucket.org/${owner}/${repo}/pull-requests/new?source=${branch}`;
3509
+ return `https://bitbucket.org/${owner}/${repo}/pull-requests/new?source=${branch}&dest=${baseBranch}`;
2025
3510
  default:
2026
3511
  return `https://github.com/${owner}/${repo}`;
2027
3512
  }
2028
3513
  }
3514
+
3515
+ // src/core/github.ts
3516
+ init_esm_shims();
3517
+ init_logger();
2029
3518
  var execAsync = promisify(exec);
2030
3519
  async function isGhAvailable() {
2031
3520
  try {
@@ -2190,20 +3679,20 @@ async function getAllPackages(projectRoot) {
2190
3679
  if (!config) {
2191
3680
  return [];
2192
3681
  }
2193
- return config.packages.map((pkg) => ({
2194
- alias: pkg.alias,
2195
- source: pkg.source,
2196
- path: pkg.path,
2197
- localCommit: lockfile[pkg.alias]?.commitHash || ""
3682
+ return config.packages.map((pkg2) => ({
3683
+ alias: pkg2.alias,
3684
+ source: pkg2.source,
3685
+ path: pkg2.path,
3686
+ localCommit: lockfile[pkg2.alias]?.commitHash || ""
2198
3687
  }));
2199
3688
  }
2200
3689
  async function selectPackagesToPush(packages) {
2201
3690
  if (packages.length === 0) {
2202
3691
  return [];
2203
3692
  }
2204
- const choices = packages.map((pkg) => ({
2205
- name: `${pkg.alias} (${pkg.source})`,
2206
- value: pkg.alias,
3693
+ const choices = packages.map((pkg2) => ({
3694
+ name: `${pkg2.alias} (${pkg2.source})`,
3695
+ value: pkg2.alias,
2207
3696
  checked: false
2208
3697
  }));
2209
3698
  const selected = await checkbox({
@@ -2213,56 +3702,97 @@ async function selectPackagesToPush(packages) {
2213
3702
  });
2214
3703
  return selected;
2215
3704
  }
2216
- async function pushSinglePackage(projectRoot, pkg, commitMessage) {
2217
- const packagePath = getPackagePath(projectRoot, pkg.alias);
2218
- const branchName = generateBranchName(pkg.alias);
2219
- const repoInfo = parseRepoInfo(pkg.source);
3705
+ async function pushSinglePackage(projectRoot, pkg2, commitMessage) {
3706
+ const packagePath = getPackagePath(projectRoot, pkg2.alias);
3707
+ const branchName = generateBranchName(pkg2.alias);
3708
+ const repoInfo = parseRepoInfo(pkg2.source);
3709
+ const pushSettings = await resolvePushSettings(projectRoot);
3710
+ const baseBranch = pushSettings.baseBranch ?? await getDefaultBranch(pkg2.source);
3711
+ log.debug(`Using base branch: ${baseBranch}`);
2220
3712
  const checkSpinner = createSpinner("Checking upstream...").start();
2221
3713
  try {
2222
- const defaultBranch = await getDefaultBranch(pkg.source);
2223
- const remoteCommit = await getRemoteCommitHash(pkg.source, defaultBranch);
2224
- if (remoteCommit !== pkg.localCommit) {
3714
+ const remoteCommit = await getRemoteCommitHash(pkg2.source, baseBranch);
3715
+ if (remoteCommit !== pkg2.localCommit) {
2225
3716
  checkSpinner.fail("Upstream has diverged");
2226
3717
  return {
2227
- success: false,
2228
- error: `Upstream has changed. Run 'nocaap update ${pkg.alias}' first.`
3718
+ status: "failed",
3719
+ error: `Upstream has changed. Run 'nocaap update ${pkg2.alias}' first.`
2229
3720
  };
2230
3721
  }
2231
3722
  checkSpinner.succeed("Upstream in sync");
2232
3723
  } catch (error) {
2233
3724
  checkSpinner.fail("Failed to check upstream");
2234
3725
  const msg = error instanceof Error ? error.message : "Unknown error";
2235
- return { success: false, error: msg };
3726
+ return { status: "failed", error: msg };
2236
3727
  }
2237
3728
  const cloneSpinner = createSpinner("Cloning upstream...").start();
2238
3729
  let tempDir;
2239
3730
  let cleanup;
2240
3731
  try {
2241
- const defaultBranch = await getDefaultBranch(pkg.source);
2242
- const result = await cloneToTemp(pkg.source, defaultBranch);
3732
+ const result = await cloneToTemp(pkg2.source, baseBranch);
2243
3733
  tempDir = result.tempDir;
2244
3734
  cleanup = result.cleanup;
2245
3735
  cloneSpinner.succeed("Cloned to temp directory");
2246
3736
  } catch (error) {
2247
3737
  cloneSpinner.fail("Clone failed");
2248
3738
  const msg = error instanceof Error ? error.message : "Unknown error";
2249
- return { success: false, error: msg };
3739
+ return { status: "failed", error: msg };
2250
3740
  }
2251
3741
  try {
2252
- const branchSpinner = createSpinner("Creating branch...").start();
2253
- await createBranch(tempDir, branchName);
2254
- branchSpinner.succeed(`Created branch: ${branchName}`);
2255
3742
  const copySpinner = createSpinner("Copying changes...").start();
2256
- const targetPath = pkg.path ? join(tempDir, pkg.path.replace(/^\/+/, "")) : tempDir;
3743
+ const sparsePath = pkg2.path ? toUnix(pkg2.path).replace(/^\/+/, "") : "";
3744
+ const sparseSegments = sparsePath.split("/").filter(Boolean);
3745
+ if (sparseSegments.includes("..")) {
3746
+ throw new Error(
3747
+ `Invalid package path '${pkg2.path}': path traversal segments are not allowed`
3748
+ );
3749
+ }
3750
+ const targetPath = sparsePath ? join(tempDir, sparsePath) : tempDir;
3751
+ if (!isWithin(tempDir, targetPath)) {
3752
+ throw new Error(`Resolved target path escapes temp clone root: ${targetPath}`);
3753
+ }
3754
+ let sourcePath = packagePath;
3755
+ if (sparsePath) {
3756
+ const sparseSubdir = join(packagePath, sparsePath);
3757
+ if (!isWithin(packagePath, sparseSubdir)) {
3758
+ throw new Error(
3759
+ `Resolved source path escapes package directory: ${sparseSubdir}`
3760
+ );
3761
+ }
3762
+ const stat = await fs2.stat(sparseSubdir).catch(() => null);
3763
+ if (stat?.isDirectory()) {
3764
+ sourcePath = sparseSubdir;
3765
+ log.debug(`Package is non-flat, using sparse subdir: ${sparseSubdir}`);
3766
+ }
3767
+ }
3768
+ if (!isWithin(packagePath, sourcePath)) {
3769
+ throw new Error(
3770
+ `Resolved source path escapes package directory: ${sourcePath}`
3771
+ );
3772
+ }
2257
3773
  await ensureDir(targetPath);
2258
- const items = await fs2.readdir(packagePath);
3774
+ if (sparsePath && targetPath !== tempDir) {
3775
+ const existing = await fs2.readdir(targetPath).catch(() => []);
3776
+ for (const item of existing) {
3777
+ await fs2.remove(join(targetPath, item));
3778
+ }
3779
+ }
3780
+ const items = await fs2.readdir(sourcePath);
2259
3781
  for (const item of items) {
2260
3782
  if (item === ".git") continue;
2261
- const srcPath = join(packagePath, item);
3783
+ const srcPath = join(sourcePath, item);
2262
3784
  const destPath = join(targetPath, item);
2263
3785
  await fs2.copy(srcPath, destPath, { overwrite: true });
2264
3786
  }
2265
3787
  copySpinner.succeed("Changes copied");
3788
+ const hasChanges = await isDirty(tempDir);
3789
+ if (!hasChanges) {
3790
+ await cleanup();
3791
+ return { status: "skipped", skipReason: "no_changes" };
3792
+ }
3793
+ const branchSpinner = createSpinner("Creating branch...").start();
3794
+ await createBranch(tempDir, branchName);
3795
+ branchSpinner.succeed(`Created branch: ${branchName}`);
2266
3796
  const commitSpinner = createSpinner("Committing...").start();
2267
3797
  try {
2268
3798
  await commitAll(tempDir, commitMessage);
@@ -2272,7 +3802,7 @@ async function pushSinglePackage(projectRoot, pkg, commitMessage) {
2272
3802
  if (msg.includes("nothing to commit")) {
2273
3803
  commitSpinner.warn("No changes to commit");
2274
3804
  await cleanup();
2275
- return { success: true, error: "No changes detected" };
3805
+ return { status: "skipped", skipReason: "nothing_to_commit" };
2276
3806
  }
2277
3807
  throw error;
2278
3808
  }
@@ -2285,15 +3815,14 @@ async function pushSinglePackage(projectRoot, pkg, commitMessage) {
2285
3815
  throw error;
2286
3816
  }
2287
3817
  const prSpinner = createSpinner("Creating PR...").start();
2288
- const defaultBranch = await getDefaultBranch(pkg.source);
2289
- const manualUrl = buildNewPrUrl(repoInfo, branchName);
3818
+ const manualUrl = buildNewPrUrl(repoInfo, branchName, baseBranch);
2290
3819
  const prResult = await createPr({
2291
3820
  repoDir: tempDir,
2292
3821
  owner: repoInfo.owner,
2293
3822
  repo: repoInfo.repo,
2294
3823
  branch: branchName,
2295
- baseBranch: defaultBranch,
2296
- title: `Update ${pkg.alias} context via nocaap`,
3824
+ baseBranch,
3825
+ title: `Update ${pkg2.alias} context via nocaap`,
2297
3826
  body: `This PR was created automatically by nocaap.
2298
3827
 
2299
3828
  **Commit message:** ${commitMessage}`,
@@ -2306,13 +3835,13 @@ async function pushSinglePackage(projectRoot, pkg, commitMessage) {
2306
3835
  }
2307
3836
  await cleanup();
2308
3837
  return {
2309
- success: true,
3838
+ status: "pushed",
2310
3839
  prUrl: prResult.url || manualUrl
2311
3840
  };
2312
3841
  } catch (error) {
2313
3842
  await cleanup();
2314
3843
  const msg = error instanceof Error ? error.message : "Unknown error";
2315
- return { success: false, error: msg };
3844
+ return { status: "failed", error: msg };
2316
3845
  }
2317
3846
  }
2318
3847
  async function pushCommand(alias, options) {
@@ -2329,8 +3858,8 @@ async function pushCommand(alias, options) {
2329
3858
  packagesToPush = allPackages;
2330
3859
  log.info(`Pushing all ${packagesToPush.length} package(s)...`);
2331
3860
  } else if (alias) {
2332
- const pkg = allPackages.find((p) => p.alias === alias);
2333
- if (!pkg) {
3861
+ const pkg2 = allPackages.find((p) => p.alias === alias);
3862
+ if (!pkg2) {
2334
3863
  log.error(`Package '${alias}' not found in config.`);
2335
3864
  log.dim("Available packages:");
2336
3865
  for (const p of allPackages) {
@@ -2338,7 +3867,7 @@ async function pushCommand(alias, options) {
2338
3867
  }
2339
3868
  return;
2340
3869
  }
2341
- packagesToPush = [pkg];
3870
+ packagesToPush = [pkg2];
2342
3871
  } else {
2343
3872
  log.info("Select packages to push:");
2344
3873
  log.newline();
@@ -2353,22 +3882,25 @@ async function pushCommand(alias, options) {
2353
3882
  const defaultMessage = packagesToPush.length === 1 && packagesToPush[0] ? `Update ${packagesToPush[0].alias} context via nocaap` : "Update context via nocaap";
2354
3883
  const commitMessage = options.message || defaultMessage;
2355
3884
  const results = [];
2356
- for (const pkg of packagesToPush) {
3885
+ for (const pkg2 of packagesToPush) {
2357
3886
  log.hr();
2358
3887
  log.newline();
2359
- log.info(`Pushing ${style.bold(pkg.alias)}...`);
2360
- log.dim(` Source: ${pkg.source}`);
2361
- if (pkg.path) {
2362
- log.dim(` Path: ${pkg.path}`);
3888
+ log.info(`Pushing ${style.bold(pkg2.alias)}...`);
3889
+ log.dim(` Source: ${pkg2.source}`);
3890
+ if (pkg2.path) {
3891
+ log.dim(` Path: ${pkg2.path}`);
2363
3892
  }
2364
3893
  log.newline();
2365
- const result = await pushSinglePackage(projectRoot, pkg, commitMessage);
2366
- results.push({ alias: pkg.alias, ...result });
2367
- if (result.success && result.prUrl) {
3894
+ const result = await pushSinglePackage(projectRoot, pkg2, commitMessage);
3895
+ results.push({ alias: pkg2.alias, ...result });
3896
+ if (result.status === "pushed" && result.prUrl) {
2368
3897
  log.newline();
2369
- log.success(`PR created for ${pkg.alias}:`);
3898
+ log.success(`PR created for ${pkg2.alias}:`);
2370
3899
  log.info(` ${style.url(result.prUrl)}`);
2371
- } else if (result.error) {
3900
+ } else if (result.status === "skipped") {
3901
+ log.newline();
3902
+ log.info(`${pkg2.alias}: No changes to push`);
3903
+ } else if (result.status === "failed") {
2372
3904
  log.newline();
2373
3905
  log.error(`Failed: ${result.error}`);
2374
3906
  }
@@ -2376,11 +3908,12 @@ async function pushCommand(alias, options) {
2376
3908
  log.newline();
2377
3909
  log.hr();
2378
3910
  log.newline();
2379
- const successCount = results.filter((r) => r.success).length;
2380
- const failCount = results.filter((r) => !r.success).length;
2381
- if (successCount > 0) {
2382
- log.success(`${successCount} package(s) pushed successfully.`);
2383
- const withPrs = results.filter((r) => r.success && r.prUrl);
3911
+ const pushed = results.filter((r) => r.status === "pushed");
3912
+ const skipped = results.filter((r) => r.status === "skipped");
3913
+ const failed = results.filter((r) => r.status === "failed");
3914
+ if (pushed.length > 0) {
3915
+ log.success(`${pushed.length} package(s) pushed successfully.`);
3916
+ const withPrs = pushed.filter((r) => r.prUrl);
2384
3917
  if (withPrs.length > 0) {
2385
3918
  log.newline();
2386
3919
  log.info("Pull Requests:");
@@ -2389,20 +3922,361 @@ async function pushCommand(alias, options) {
2389
3922
  }
2390
3923
  }
2391
3924
  }
2392
- if (failCount > 0) {
3925
+ if (skipped.length > 0) {
3926
+ log.newline();
3927
+ log.info(`${skipped.length} package(s) skipped (no changes).`);
3928
+ for (const r of skipped) {
3929
+ log.dim(` ${r.alias}: ${r.skipReason}`);
3930
+ }
3931
+ }
3932
+ if (failed.length > 0) {
2393
3933
  log.newline();
2394
- log.warn(`${failCount} package(s) failed.`);
2395
- for (const r of results.filter((r2) => !r2.success)) {
3934
+ log.warn(`${failed.length} package(s) failed.`);
3935
+ for (const r of failed) {
2396
3936
  log.dim(` ${r.alias}: ${r.error}`);
2397
3937
  }
2398
3938
  }
2399
3939
  }
2400
3940
 
3941
+ // src/commands/serve.ts
3942
+ init_esm_shims();
3943
+
3944
+ // src/core/mcp-server.ts
3945
+ init_esm_shims();
3946
+ init_search_engine();
3947
+ init_config();
3948
+ init_paths();
3949
+ async function createMcpServer(options) {
3950
+ const { projectRoot } = options;
3951
+ const contextDir = getContextDir(projectRoot);
3952
+ const searchEngine = new SearchEngine();
3953
+ const hasIndex = await searchIndexExists(projectRoot);
3954
+ if (hasIndex) {
3955
+ await searchEngine.loadIndex(projectRoot);
3956
+ }
3957
+ const server = new McpServer({
3958
+ name: "nocaap",
3959
+ version: "1.0.0"
3960
+ });
3961
+ server.registerResource(
3962
+ "index",
3963
+ "nocaap://index",
3964
+ {
3965
+ title: "Context Index",
3966
+ description: "Complete index of organizational knowledge with document summaries. Includes team directory, product specs, engineering docs, and company strategy.",
3967
+ mimeType: "text/markdown"
3968
+ },
3969
+ async (uri) => {
3970
+ const indexContent = await readIndex(projectRoot);
3971
+ return {
3972
+ contents: [{
3973
+ uri: uri.href,
3974
+ mimeType: "text/markdown",
3975
+ text: indexContent ?? "No INDEX.md found. Run `nocaap update` to generate."
3976
+ }]
3977
+ };
3978
+ }
3979
+ );
3980
+ server.registerResource(
3981
+ "manifest",
3982
+ "nocaap://manifest",
3983
+ {
3984
+ title: "Package Manifest",
3985
+ description: "Configuration showing installed knowledge packages and their sources.",
3986
+ mimeType: "application/json"
3987
+ },
3988
+ async (uri) => {
3989
+ const config = await readConfig(projectRoot);
3990
+ const manifest = {
3991
+ packages: config?.packages ?? [],
3992
+ searchIndexAvailable: hasIndex,
3993
+ packagesPath: getPackagesDir(projectRoot)
3994
+ };
3995
+ return {
3996
+ contents: [{
3997
+ uri: uri.href,
3998
+ mimeType: "application/json",
3999
+ text: JSON.stringify(manifest, null, 2)
4000
+ }]
4001
+ };
4002
+ }
4003
+ );
4004
+ server.registerTool(
4005
+ "search",
4006
+ {
4007
+ title: "Search Context",
4008
+ description: "Search organizational knowledge including team directory, product documentation, engineering guidelines, company strategy, and project context. Use for questions about people, products, processes, or internal information. Returns ranked results with snippets - use get_document for full content.",
4009
+ inputSchema: {
4010
+ query: z.string().describe("Search query"),
4011
+ mode: z.enum(["fulltext", "semantic", "hybrid"]).optional().describe("Search mode (default: fulltext, or hybrid if vector index exists)"),
4012
+ packages: z.array(z.string()).optional().describe("Filter to specific packages"),
4013
+ tags: z.array(z.string()).optional().describe("Filter by document tags"),
4014
+ limit: z.number().optional().describe("Maximum results (default: 10)")
4015
+ }
4016
+ },
4017
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4018
+ async ({ query, mode, packages, tags, limit }) => {
4019
+ if (!searchEngine.isInitialized()) {
4020
+ return {
4021
+ content: [{
4022
+ type: "text",
4023
+ text: "Search index not available. Run `nocaap index` to build it."
4024
+ }]
4025
+ };
4026
+ }
4027
+ const searchMode = mode ?? (searchEngine.hasVectorSearch() ? "hybrid" : "fulltext");
4028
+ try {
4029
+ const results = await searchEngine.hybridSearch({
4030
+ query,
4031
+ mode: searchMode,
4032
+ packages,
4033
+ limit: limit ?? 10
4034
+ });
4035
+ const formattedResults = results.map((r, i) => ({
4036
+ rank: i + 1,
4037
+ path: r.path,
4038
+ package: r.package,
4039
+ title: r.title,
4040
+ headings: r.headings,
4041
+ score: r.score,
4042
+ sources: r.sources,
4043
+ snippet: r.content.slice(0, 200) + (r.content.length > 200 ? "..." : "")
4044
+ }));
4045
+ return {
4046
+ content: [{
4047
+ type: "text",
4048
+ text: JSON.stringify({
4049
+ mode: searchMode,
4050
+ vectorSearchAvailable: searchEngine.hasVectorSearch(),
4051
+ results: formattedResults
4052
+ }, null, 2)
4053
+ }]
4054
+ };
4055
+ } catch (error) {
4056
+ const message = error instanceof Error ? error.message : "Unknown error";
4057
+ return {
4058
+ content: [{
4059
+ type: "text",
4060
+ text: `Search error: ${message}`
4061
+ }]
4062
+ };
4063
+ }
4064
+ }
4065
+ );
4066
+ server.registerTool(
4067
+ "get_document",
4068
+ {
4069
+ title: "Get Document",
4070
+ description: "Retrieve full documentation by path (from search results). Use after searching to get complete details about team members, products, engineering decisions, or any organizational knowledge.",
4071
+ inputSchema: {
4072
+ path: z.string().describe("Relative path to document (from search results)")
4073
+ }
4074
+ },
4075
+ async ({ path: docPath }) => {
4076
+ const normalizedPath = toUnix(docPath);
4077
+ const fullPath = join(contextDir, normalizedPath);
4078
+ if (!isWithin(contextDir, fullPath)) {
4079
+ return {
4080
+ content: [{
4081
+ type: "text",
4082
+ text: `Error: Path is outside context directory`
4083
+ }]
4084
+ };
4085
+ }
4086
+ if (!await exists(fullPath)) {
4087
+ return {
4088
+ content: [{
4089
+ type: "text",
4090
+ text: `Document not found: ${docPath}`
4091
+ }]
4092
+ };
4093
+ }
4094
+ const content = await fs2.readFile(fullPath, "utf-8");
4095
+ return {
4096
+ content: [{
4097
+ type: "text",
4098
+ text: content
4099
+ }]
4100
+ };
4101
+ }
4102
+ );
4103
+ server.registerTool(
4104
+ "get_section",
4105
+ {
4106
+ title: "Get Section",
4107
+ description: 'Retrieve a specific section from a document by heading. Useful for extracting targeted information like "Key Accomplishments" or "Technical Architecture" without loading the entire document.',
4108
+ inputSchema: {
4109
+ path: z.string().describe("Relative path to document"),
4110
+ heading: z.string().describe("The heading text to find")
4111
+ }
4112
+ },
4113
+ async ({ path: docPath, heading }) => {
4114
+ const normalizedPath = toUnix(docPath);
4115
+ const fullPath = join(contextDir, normalizedPath);
4116
+ if (!isWithin(contextDir, fullPath)) {
4117
+ return {
4118
+ content: [{
4119
+ type: "text",
4120
+ text: `Error: Path is outside context directory`
4121
+ }]
4122
+ };
4123
+ }
4124
+ if (!await exists(fullPath)) {
4125
+ return {
4126
+ content: [{
4127
+ type: "text",
4128
+ text: `Document not found: ${docPath}`
4129
+ }]
4130
+ };
4131
+ }
4132
+ const content = await fs2.readFile(fullPath, "utf-8");
4133
+ const section = extractSection(content, heading);
4134
+ if (!section) {
4135
+ return {
4136
+ content: [{
4137
+ type: "text",
4138
+ text: `Section not found: "${heading}"`
4139
+ }]
4140
+ };
4141
+ }
4142
+ return {
4143
+ content: [{
4144
+ type: "text",
4145
+ text: section
4146
+ }]
4147
+ };
4148
+ }
4149
+ );
4150
+ server.registerTool(
4151
+ "list_contexts",
4152
+ {
4153
+ title: "List Contexts",
4154
+ description: "List available knowledge domains and packages. Shows what organizational context is installed (team info, products, engineering docs, etc.). Use to discover what information is available before searching.",
4155
+ inputSchema: {
4156
+ tags: z.array(z.string()).optional().describe("Filter by tags (not yet implemented)")
4157
+ }
4158
+ },
4159
+ async () => {
4160
+ const config = await readConfig(projectRoot);
4161
+ if (!config || config.packages.length === 0) {
4162
+ return {
4163
+ content: [{
4164
+ type: "text",
4165
+ text: "No context packages installed."
4166
+ }]
4167
+ };
4168
+ }
4169
+ const packages = config.packages.map((pkg2) => ({
4170
+ alias: pkg2.alias,
4171
+ source: pkg2.source,
4172
+ path: pkg2.path ?? "/",
4173
+ version: pkg2.version ?? "main"
4174
+ }));
4175
+ return {
4176
+ content: [{
4177
+ type: "text",
4178
+ text: JSON.stringify(packages, null, 2)
4179
+ }]
4180
+ };
4181
+ }
4182
+ );
4183
+ server.registerTool(
4184
+ "get_overview",
4185
+ {
4186
+ title: "Get Context Overview",
4187
+ description: "Get a structured overview of all available organizational knowledge. Returns package names, document titles, and content summaries. RECOMMENDED: Call this first to understand what context is available before searching.",
4188
+ inputSchema: {}
4189
+ },
4190
+ async () => {
4191
+ const indexContent = await readIndex(projectRoot);
4192
+ if (!indexContent) {
4193
+ return {
4194
+ content: [{
4195
+ type: "text",
4196
+ text: "No context index available. Run `nocaap index` to generate."
4197
+ }]
4198
+ };
4199
+ }
4200
+ return {
4201
+ content: [{
4202
+ type: "text",
4203
+ text: indexContent
4204
+ }]
4205
+ };
4206
+ }
4207
+ );
4208
+ return server;
4209
+ }
4210
+ function extractSection(content, heading) {
4211
+ const lines = content.split("\n");
4212
+ const headingLower = heading.toLowerCase();
4213
+ let capturing = false;
4214
+ let captureLevel = 0;
4215
+ const sectionLines = [];
4216
+ for (const line of lines) {
4217
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
4218
+ if (headingMatch && headingMatch[1] && headingMatch[2]) {
4219
+ const level = headingMatch[1].length;
4220
+ const text = headingMatch[2].trim().toLowerCase();
4221
+ if (capturing) {
4222
+ if (level <= captureLevel) {
4223
+ break;
4224
+ }
4225
+ } else if (text === headingLower) {
4226
+ capturing = true;
4227
+ captureLevel = level;
4228
+ }
4229
+ }
4230
+ if (capturing) {
4231
+ sectionLines.push(line);
4232
+ }
4233
+ }
4234
+ return sectionLines.length > 0 ? sectionLines.join("\n") : null;
4235
+ }
4236
+ async function startMcpServer(options) {
4237
+ const server = await createMcpServer(options);
4238
+ const transport = new StdioServerTransport();
4239
+ await server.connect(transport);
4240
+ }
4241
+
4242
+ // src/commands/serve.ts
4243
+ init_paths();
4244
+ function generateClaudeDesktopConfig() {
4245
+ return {
4246
+ mcpServers: {
4247
+ nocaap: {
4248
+ command: "nocaap",
4249
+ args: ["serve"]
4250
+ }
4251
+ }
4252
+ };
4253
+ }
4254
+ async function serveCommand(options = {}) {
4255
+ const projectRoot = options.root ?? process.cwd();
4256
+ if (options.printConfig) {
4257
+ const config = generateClaudeDesktopConfig();
4258
+ console.log(JSON.stringify(config, null, 2));
4259
+ return;
4260
+ }
4261
+ const contextDir = getContextDir(projectRoot);
4262
+ if (!await exists(contextDir)) {
4263
+ throw new Error(
4264
+ "No .context directory found. Run `nocaap setup` or `nocaap add` first."
4265
+ );
4266
+ }
4267
+ await startMcpServer({ projectRoot });
4268
+ }
4269
+
2401
4270
  // src/index.ts
4271
+ init_index_search();
4272
+ init_logger();
4273
+ var __filename2 = fileURLToPath(import.meta.url);
4274
+ var __dirname2 = dirname$1(__filename2);
4275
+ var pkg = JSON.parse(readFileSync(join$1(__dirname2, "..", "package.json"), "utf-8"));
2402
4276
  var program = new Command();
2403
4277
  program.name("nocaap").description(
2404
4278
  "Normalized Organizational Context-as-a-Package\n\nStandardize your AI agent context across teams.\nRun `nocaap setup` to get started."
2405
- ).version("0.1.0");
4279
+ ).version(pkg.version);
2406
4280
  program.command("setup").description("Interactive setup wizard to configure context packages").option("-r, --registry <url>", "Registry URL to fetch contexts from").action(async (options) => {
2407
4281
  try {
2408
4282
  await setupCommand({ registry: options.registry });
@@ -2452,11 +4326,12 @@ program.command("remove <alias>").alias("rm").description("Remove a context pack
2452
4326
  process.exit(1);
2453
4327
  }
2454
4328
  });
2455
- program.command("config [key] [value]").description("Manage global nocaap configuration").option("-l, --list", "Show all configuration").option("--clear", "Clear the specified config key").action(async (key, value, options) => {
4329
+ program.command("config [key] [value]").description("Manage nocaap configuration").option("-l, --list", "Show all configuration").option("-g, --global", "Use global config scope").option("-p, --project", "Use project config scope").action(async (key, value, options) => {
2456
4330
  try {
2457
4331
  await configCommand(key, value, {
2458
4332
  list: options.list,
2459
- clear: options.clear
4333
+ global: options.global,
4334
+ project: options.project
2460
4335
  });
2461
4336
  } catch (error) {
2462
4337
  const message = error instanceof Error ? error.message : String(error);
@@ -2476,16 +4351,33 @@ program.command("push [alias]").description("Push local changes to upstream as a
2476
4351
  process.exit(1);
2477
4352
  }
2478
4353
  });
2479
- program.command("generate").alias("index").description("Regenerate INDEX.md without updating packages").action(async () => {
4354
+ program.command("index").description("Build INDEX.md and search index for AI agent access").option("--semantic", "Enable semantic search with vector embeddings").option(
4355
+ "--provider <provider>",
4356
+ "Embedding provider: ollama | openai | tfjs | auto",
4357
+ "auto"
4358
+ ).action(async (options) => {
2480
4359
  try {
2481
4360
  const projectRoot = process.cwd();
2482
4361
  await generateIndexWithProgress(projectRoot);
4362
+ await indexSearchCommand({
4363
+ semantic: options.semantic,
4364
+ provider: options.provider
4365
+ });
2483
4366
  } catch (error) {
2484
4367
  const message = error instanceof Error ? error.message : String(error);
2485
4368
  log.error(message);
2486
4369
  process.exit(1);
2487
4370
  }
2488
4371
  });
4372
+ program.command("serve").description("Start the MCP server for AI agent access").option("--print-config", "Print Claude Desktop configuration JSON").option("--root <path>", "Project root directory (default: current directory)").action(async (options) => {
4373
+ try {
4374
+ await serveCommand({ printConfig: options.printConfig, root: options.root });
4375
+ } catch (error) {
4376
+ const message = error instanceof Error ? error.message : String(error);
4377
+ console.error(`Error: ${message}`);
4378
+ process.exit(1);
4379
+ }
4380
+ });
2489
4381
  program.parse();
2490
4382
  //# sourceMappingURL=index.js.map
2491
4383
  //# sourceMappingURL=index.js.map