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/README.md +161 -16
- package/dist/index.js +2258 -366
- package/dist/index.js.map +1 -1
- package/package.json +36 -4
package/dist/index.js
CHANGED
|
@@ -1,29 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
17
|
-
var
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
var
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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,
|
|
388
|
+
async function upsertPackage(projectRoot, pkg2) {
|
|
305
389
|
const config = await readConfig(projectRoot) ?? { packages: [] };
|
|
306
|
-
const existingIndex = config.packages.findIndex((p) => p.alias ===
|
|
390
|
+
const existingIndex = config.packages.findIndex((p) => p.alias === pkg2.alias);
|
|
307
391
|
if (existingIndex >= 0) {
|
|
308
|
-
config.packages[existingIndex] =
|
|
309
|
-
log.debug(`Updated package '${
|
|
392
|
+
config.packages[existingIndex] = pkg2;
|
|
393
|
+
log.debug(`Updated package '${pkg2.alias}' in config`);
|
|
310
394
|
} else {
|
|
311
|
-
config.packages.push(
|
|
312
|
-
log.debug(`Added package '${
|
|
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
|
-
|
|
432
|
-
|
|
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
|
|
478
|
-
const config = await getGlobalConfig();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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,
|
|
2035
|
+
const [, org, repo, branchName, path3] = rawGitHubMatch;
|
|
791
2036
|
gitUrl = `git@github.com:${org}/${repo}.git`;
|
|
792
|
-
filePath =
|
|
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,
|
|
2047
|
+
const [, org, repo, branchName, path3] = blobMatch;
|
|
803
2048
|
gitUrl = `git@github.com:${org}/${repo}.git`;
|
|
804
|
-
filePath =
|
|
805
|
-
httpUrl = `https://raw.githubusercontent.com/${org}/${repo}/${branchName}/${
|
|
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
|
|
1085
|
-
return `${context.repo}::${
|
|
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 } =
|
|
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
|
|
1207
|
-
const packagePath = getPackagePath(projectRoot,
|
|
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: ${
|
|
1210
|
-
warnings.push(`Package '${
|
|
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: ${
|
|
1216
|
-
warnings.push(`No .md/.mdx files found in '${
|
|
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:
|
|
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
|
|
1276
|
-
const anchorId =
|
|
1277
|
-
lines.push(`- [${
|
|
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
|
|
1284
|
-
lines.push(`## ${
|
|
2533
|
+
for (const pkg2 of packages) {
|
|
2534
|
+
lines.push(`## ${pkg2.alias} (${pkg2.files.length} files)`);
|
|
1285
2535
|
lines.push("");
|
|
1286
|
-
for (const file of
|
|
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
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
|
1453
|
-
|
|
1454
|
-
await writeConfig(projectRoot,
|
|
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.
|
|
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,
|
|
1569
|
-
if (
|
|
1570
|
-
const leafFolder =
|
|
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
|
|
1669
|
-
const result = await updatePackage(projectRoot,
|
|
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,
|
|
1709
|
-
const packagePath = getPackagePath(projectRoot,
|
|
1710
|
-
const spinner = createSpinner(`Updating ${style.bold(
|
|
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(`${
|
|
2990
|
+
spinner.warn(`${pkg2.alias}: Package directory not found`);
|
|
1714
2991
|
return {
|
|
1715
|
-
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(`${
|
|
2998
|
+
spinner.warn(`${pkg2.alias}: Not a git repository`);
|
|
1722
2999
|
return {
|
|
1723
|
-
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(`${
|
|
3006
|
+
spinner.warn(`${pkg2.alias}: Has uncommitted changes`);
|
|
1730
3007
|
return {
|
|
1731
|
-
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,
|
|
1737
|
-
if (lockEntry && lockEntry.sparsePath !== (
|
|
1738
|
-
spinner.warn(`${
|
|
1739
|
-
log.dim(` Config: ${
|
|
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:
|
|
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(`${
|
|
3027
|
+
spinner.info(`${pkg2.alias}: Already up-to-date`);
|
|
1751
3028
|
return {
|
|
1752
|
-
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,
|
|
3035
|
+
await updateLockEntry(projectRoot, pkg2.alias, {
|
|
1759
3036
|
commitHash: newCommit,
|
|
1760
|
-
sparsePath:
|
|
3037
|
+
sparsePath: pkg2.path || "",
|
|
1761
3038
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1762
3039
|
});
|
|
1763
|
-
spinner.succeed(`${
|
|
3040
|
+
spinner.succeed(`${pkg2.alias}: Updated`);
|
|
1764
3041
|
return {
|
|
1765
|
-
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(`${
|
|
3049
|
+
spinner.fail(`${pkg2.alias}: Failed`);
|
|
1773
3050
|
return {
|
|
1774
|
-
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
|
|
1797
|
-
const lock = lockfile[
|
|
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,
|
|
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(
|
|
1815
|
-
log.dim(` Source: ${
|
|
1816
|
-
if (
|
|
1817
|
-
log.dim(` 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: ${
|
|
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
|
|
1834
|
-
if (!
|
|
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: ${
|
|
1842
|
-
if (
|
|
1843
|
-
log.dim(` 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
|
|
1870
|
-
await
|
|
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
|
|
1895
|
-
await
|
|
3199
|
+
if (options.list || !key) {
|
|
3200
|
+
await showConfig(options);
|
|
1896
3201
|
return;
|
|
1897
3202
|
}
|
|
1898
3203
|
switch (key) {
|
|
1899
|
-
case "
|
|
1900
|
-
await
|
|
3204
|
+
case "list":
|
|
3205
|
+
await showConfig(options);
|
|
1901
3206
|
break;
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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
|
|
1911
|
-
const
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
log.
|
|
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 (
|
|
1917
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
log.
|
|
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
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
|
1956
|
-
if (
|
|
1957
|
-
|
|
1958
|
-
log.
|
|
1959
|
-
|
|
1960
|
-
|
|
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.
|
|
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
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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((
|
|
2194
|
-
alias:
|
|
2195
|
-
source:
|
|
2196
|
-
path:
|
|
2197
|
-
localCommit: lockfile[
|
|
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((
|
|
2205
|
-
name: `${
|
|
2206
|
-
value:
|
|
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,
|
|
2217
|
-
const packagePath = getPackagePath(projectRoot,
|
|
2218
|
-
const branchName = generateBranchName(
|
|
2219
|
-
const repoInfo = parseRepoInfo(
|
|
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
|
|
2223
|
-
|
|
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
|
-
|
|
2228
|
-
error: `Upstream has changed. Run 'nocaap update ${
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
|
2296
|
-
title: `Update ${
|
|
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
|
-
|
|
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 {
|
|
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
|
|
2333
|
-
if (!
|
|
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 = [
|
|
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
|
|
3885
|
+
for (const pkg2 of packagesToPush) {
|
|
2357
3886
|
log.hr();
|
|
2358
3887
|
log.newline();
|
|
2359
|
-
log.info(`Pushing ${style.bold(
|
|
2360
|
-
log.dim(` Source: ${
|
|
2361
|
-
if (
|
|
2362
|
-
log.dim(` 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,
|
|
2366
|
-
results.push({ alias:
|
|
2367
|
-
if (result.
|
|
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 ${
|
|
3898
|
+
log.success(`PR created for ${pkg2.alias}:`);
|
|
2370
3899
|
log.info(` ${style.url(result.prUrl)}`);
|
|
2371
|
-
} else if (result.
|
|
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
|
|
2380
|
-
const
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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 (
|
|
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(`${
|
|
2395
|
-
for (const r of
|
|
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(
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|