flowseeker 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.env.example +7 -0
  2. package/CHANGELOG.md +143 -108
  3. package/README.md +288 -221
  4. package/dist/chat/nativeChatParticipant.js +1 -1
  5. package/dist/cli/flowCommand.js +175 -0
  6. package/dist/cli/main.js +1810 -0
  7. package/dist/cli/mcpServer.js +7 -1
  8. package/dist/cli/runEvaluation.js +281 -2
  9. package/dist/config/defaultConfig.js +11 -1
  10. package/dist/config/env.js +118 -0
  11. package/dist/config/loadConfig.js +18 -1
  12. package/dist/config/loadConfigFromPath.js +3 -1
  13. package/dist/eval/accuracyV2.js +483 -0
  14. package/dist/eval/goldenTask.js +192 -0
  15. package/dist/extension.js +23 -0
  16. package/dist/framework/laravel.js +177 -0
  17. package/dist/gateway/embeddingProviders.js +852 -0
  18. package/dist/index/cacheStore.js +43 -0
  19. package/dist/index/configRouteDiscoveryProbe.js +288 -0
  20. package/dist/index/embeddingIndex.js +193 -0
  21. package/dist/index/graphIndex.js +460 -0
  22. package/dist/index/indexWatcher.js +86 -0
  23. package/dist/index/semanticChunkIndex.js +388 -0
  24. package/dist/index/structuredExtractor.js +303 -12
  25. package/dist/index/treeSitterExtractor.js +264 -0
  26. package/dist/index/vectorStore.js +41 -0
  27. package/dist/index/workspaceIndex.js +901 -26
  28. package/dist/mcp/mcpTools.js +1678 -2
  29. package/dist/pipeline/contextBlueprint.js +15 -2
  30. package/dist/pipeline/contextPack.js +3 -3
  31. package/dist/pipeline/deterministicReranker.js +358 -0
  32. package/dist/pipeline/evaluationMetrics.js +14 -2
  33. package/dist/pipeline/fileGroups.js +7 -1
  34. package/dist/pipeline/fileScanner.js +209 -12
  35. package/dist/pipeline/fusionTrace.js +149 -0
  36. package/dist/pipeline/llmReranker.js +151 -0
  37. package/dist/pipeline/nodeScan.js +102 -12
  38. package/dist/pipeline/ranker.js +875 -16
  39. package/dist/pipeline/retrievalFusion.js +41 -0
  40. package/dist/pipeline/roleRefinement.js +62 -0
  41. package/dist/pipeline/runHeadless.js +60 -5
  42. package/dist/pipeline/runPipeline.js +2 -2
  43. package/dist/pipeline/solvePacket.js +656 -43
  44. package/dist/pipeline/subsystem.js +21 -0
  45. package/dist/pipeline/taskUnderstanding.js +2 -2
  46. package/dist/ui/chatViewProvider.js +1 -1
  47. package/docs/demo-screenshot-checklist.md +86 -0
  48. package/docs/marketplace-copy.md +92 -0
  49. package/docs/mcp-onboarding.md +191 -0
  50. package/package.json +633 -561
@@ -0,0 +1,1810 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const child_process_1 = require("child_process");
38
+ const fsSync = __importStar(require("fs"));
39
+ const fs = __importStar(require("fs/promises"));
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const module_1 = require("module");
43
+ const loadConfigFromPath_1 = require("../config/loadConfigFromPath");
44
+ const embeddingProviders_1 = require("../gateway/embeddingProviders");
45
+ const mcpTools_1 = require("../mcp/mcpTools");
46
+ const LOCAL_MODEL_PROFILE = {
47
+ modelId: "sentence-transformers/all-MiniLM-L6-v2",
48
+ modelSlug: "all-MiniLM-L6-v2",
49
+ modelAlias: "minilm",
50
+ dimensions: 384,
51
+ runtimePackage: "@huggingface/transformers",
52
+ schemaVersion: 1
53
+ };
54
+ void main().catch((error) => {
55
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
56
+ console.error(message);
57
+ process.exitCode = 1;
58
+ });
59
+ async function main() {
60
+ const args = process.argv.slice(2);
61
+ const command = args[0];
62
+ if (!command || command === "--help" || command === "-h" || command === "help") {
63
+ printHelp();
64
+ return;
65
+ }
66
+ if (command === "--version" || command === "-v") {
67
+ await printVersion();
68
+ return;
69
+ }
70
+ if (command === "mcp") {
71
+ await runMcpServer(args.slice(1));
72
+ return;
73
+ }
74
+ if (command === "claude") {
75
+ printClaudeSetup();
76
+ return;
77
+ }
78
+ if (command === "semantic") {
79
+ await runSemanticCommand(args.slice(1));
80
+ return;
81
+ }
82
+ if (command === "doctor") {
83
+ await runDoctorCommand(args.slice(1));
84
+ return;
85
+ }
86
+ if (command === "setup") {
87
+ await runSetupCommand(args.slice(1));
88
+ return;
89
+ }
90
+ if (command === "benchmark") {
91
+ await runBenchmarkCommand(args.slice(1));
92
+ return;
93
+ }
94
+ if (command === "guide" || command === "auto" || command === "files" || command === "retrieve") {
95
+ await runFlowCommand(parseFlowArgs(command, args.slice(1)));
96
+ return;
97
+ }
98
+ throw new Error(`Unknown command: ${command}`);
99
+ }
100
+ async function runSemanticCommand(args) {
101
+ const subcommand = args[0] && !args[0].startsWith("-") ? args[0] : "check";
102
+ const rest = subcommand === args[0] ? args.slice(1) : args;
103
+ if (subcommand === "local") {
104
+ await runLocalCommand(args.slice(1));
105
+ return;
106
+ }
107
+ const { workspace, testProvider, preset } = parseSemanticArgs(rest);
108
+ if (subcommand === "init") {
109
+ await initSemanticEnv(workspace, preset);
110
+ return;
111
+ }
112
+ if (subcommand === "check") {
113
+ await checkSemanticSetup(workspace, testProvider);
114
+ return;
115
+ }
116
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
117
+ printSemanticHelp();
118
+ return;
119
+ }
120
+ throw new Error(`Unknown semantic command: ${subcommand}`);
121
+ }
122
+ function parseSemanticArgs(args) {
123
+ let workspace = process.cwd();
124
+ let testProvider = false;
125
+ let preset = "";
126
+ for (let i = 0; i < args.length; i++) {
127
+ const arg = args[i];
128
+ const next = args[i + 1];
129
+ if (arg === "--help" || arg === "-h") {
130
+ printSemanticHelp();
131
+ process.exit(0);
132
+ }
133
+ if ((arg === "--workspace" || arg === "-w") && next) {
134
+ workspace = path.resolve(next);
135
+ i++;
136
+ continue;
137
+ }
138
+ if (arg === "--test") {
139
+ testProvider = true;
140
+ continue;
141
+ }
142
+ if (arg === "--jina") {
143
+ preset = "jina";
144
+ continue;
145
+ }
146
+ if (arg === "--openai-compatible") {
147
+ preset = "openaiCompatible";
148
+ continue;
149
+ }
150
+ if (arg === "--local") {
151
+ preset = "local";
152
+ continue;
153
+ }
154
+ throw new Error(`Unknown semantic option: ${arg}`);
155
+ }
156
+ return { workspace, testProvider, preset };
157
+ }
158
+ async function initSemanticEnv(workspace, preset) {
159
+ const resolved = path.resolve(workspace);
160
+ const configPath = path.join(resolved, ".flowseeker", "config.json");
161
+ const envPath = path.join(resolved, ".env");
162
+ const effectivePreset = preset || "jina";
163
+ // Build semantic config based on preset
164
+ let semanticConfig;
165
+ if (effectivePreset === "local") {
166
+ semanticConfig = {
167
+ semanticEnabled: true,
168
+ semanticProvider: "local",
169
+ semanticModel: "local-minilm-v1",
170
+ semanticBaseUrl: "",
171
+ semanticApiKeyEnv: "",
172
+ semanticCache: true,
173
+ semanticMaxFilesPerRun: 400,
174
+ semanticTimeoutMs: 120000
175
+ };
176
+ }
177
+ else if (effectivePreset === "openaiCompatible") {
178
+ semanticConfig = {
179
+ semanticEnabled: true,
180
+ semanticProvider: "openaiCompatible",
181
+ semanticBaseUrl: "",
182
+ semanticModel: "",
183
+ semanticApiKeyEnv: "FLOWSEEKER_EMBEDDING_API_KEY",
184
+ semanticCache: true,
185
+ semanticMaxFilesPerRun: 800,
186
+ semanticTimeoutMs: 60000
187
+ };
188
+ }
189
+ else {
190
+ // jina (default)
191
+ semanticConfig = {
192
+ semanticEnabled: true,
193
+ semanticProvider: "openaiCompatible",
194
+ semanticBaseUrl: "https://api.jina.ai/v1",
195
+ semanticModel: "jina-code-embeddings-0.5b",
196
+ semanticApiKeyEnv: "FLOWSEEKER_EMBEDDING_API_KEY",
197
+ semanticCache: true,
198
+ semanticMaxFilesPerRun: 800,
199
+ semanticTimeoutMs: 60000
200
+ };
201
+ }
202
+ // Merge into existing .flowseeker/config.json
203
+ let existingConfig = {};
204
+ try {
205
+ existingConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
206
+ }
207
+ catch {
208
+ // create below
209
+ }
210
+ const existingIndex = existingConfig.index || {};
211
+ existingConfig.index = { ...existingIndex, ...semanticConfig };
212
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
213
+ await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2) + "\n", "utf8");
214
+ process.stdout.write(`Semantic config written: ${configPath}\n`);
215
+ process.stdout.write(`Preset: ${effectivePreset}\n`);
216
+ // For hosted presets, add API key placeholder to .env
217
+ if (effectivePreset !== "local") {
218
+ const keyEnv = semanticConfig.semanticApiKeyEnv;
219
+ let existingEnv = "";
220
+ try {
221
+ existingEnv = await fs.readFile(envPath, "utf8");
222
+ }
223
+ catch { /* create below */ }
224
+ if (!new RegExp(`^${escapeRegExp(keyEnv)}=`, "m").test(existingEnv)) {
225
+ const block = [
226
+ existingEnv.trim() ? "\n" : "",
227
+ "# FlowSeeker semantic retrieval API key",
228
+ `${keyEnv}=`,
229
+ ""
230
+ ].join("\n");
231
+ await fs.writeFile(envPath, `${existingEnv}${block}`, "utf8");
232
+ process.stdout.write(`API key placeholder added: ${envPath} (${keyEnv})\n`);
233
+ }
234
+ else {
235
+ process.stdout.write(`API key env already present: ${envPath} (${keyEnv})\n`);
236
+ }
237
+ }
238
+ // Next steps
239
+ const nextSteps = effectivePreset === "local"
240
+ ? ["Local semantic config is enabled. Runtime and model remain explicit setup steps.", "Run: flowseeker semantic local setup", "Then run: flowseeker semantic local download"]
241
+ : effectivePreset === "openaiCompatible"
242
+ ? ["This preset is provider-neutral and incomplete until configured.", "Before --test, edit .flowseeker/config.json:", " semanticBaseUrl", " semanticModel", " semanticApiKeyEnv", "The model must match the provider/base URL.", "The API key still belongs in .env.", "Run: flowseeker semantic check to confirm configuration."]
243
+ : [" 1. Fill FLOWSEEKER_EMBEDDING_API_KEY in .env", " 2. Run: flowseeker semantic check --test"];
244
+ process.stdout.write(nextSteps.join("\n") + "\n");
245
+ }
246
+ async function checkSemanticSetup(workspace, testProvider) {
247
+ const resolved = path.resolve(workspace);
248
+ const configPath = path.join(resolved, ".flowseeker", "config.json");
249
+ const envPath = path.join(resolved, ".env");
250
+ const config = await (0, loadConfigFromPath_1.loadConfigFromPath)(resolved);
251
+ const apiKeyEnv = config.index.semanticApiKeyEnv;
252
+ const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;
253
+ const provider = (0, embeddingProviders_1.createConfiguredEmbeddingProvider)({ ...config.index, workspacePath: resolved });
254
+ const isLocal = config.index.semanticProvider === "local";
255
+ const lines = [
256
+ "FlowSeeker semantic status",
257
+ "",
258
+ `Workspace: ${resolved}`,
259
+ `Enabled: ${config.index.semanticEnabled ? "yes" : "no"}`,
260
+ `Provider: ${config.index.semanticProvider || "(none)"}`,
261
+ `Model: ${config.index.semanticModel || "(empty)"}`,
262
+ `Base URL: ${config.index.semanticBaseUrl || "(provider default)"}`,
263
+ `API key env: ${apiKeyEnv || "(none)"}`,
264
+ `API key: ${apiKey ? "***configured***" : (isLocal ? "not required" : "missing")}`,
265
+ `Cache: ${config.index.semanticCache ? "enabled" : "disabled"}`,
266
+ `Max files/run: ${config.index.semanticMaxFilesPerRun}`,
267
+ `Timeout: ${config.index.semanticTimeoutMs}ms`,
268
+ `Config file: ${configPath}${await fileExists(configPath) ? "" : " (not found)"}`,
269
+ `Env file: ${envPath}${await fileExists(envPath) ? "" : " (not found)"}`
270
+ ];
271
+ if (!config.index.semanticEnabled) {
272
+ lines.push("", "Result: disabled", "Next action: Run flowseeker semantic init --jina (recommended) or --openai-compatible to enable hosted semantic retrieval.");
273
+ process.stdout.write(`${lines.join("\n")}\n`);
274
+ return;
275
+ }
276
+ if (isLocal) {
277
+ const readiness = (0, embeddingProviders_1.getLocalProviderReadiness)(resolved, {
278
+ semanticModel: config.index.semanticModel,
279
+ semanticLocalModelPath: config.index.semanticLocalModelPath,
280
+ semanticDimensions: config.index.semanticDimensions
281
+ });
282
+ const statusMessages = {
283
+ local_unavailable_runtime_missing: { note: "Local embedding runtime (@huggingface/transformers) is not installed in this project.", nextAction: readiness.nextAction },
284
+ local_unavailable_model_missing: { note: "Local embedding model is not downloaded.", nextAction: readiness.nextAction },
285
+ local_unavailable_model_invalid: { note: "Local embedding model metadata is invalid.", nextAction: readiness.nextAction },
286
+ local_ready: { note: "Local embedding provider is ready.", nextAction: readiness.nextAction },
287
+ bundled_package_ready: { note: "Built-in local model package is installed and ready.", nextAction: readiness.nextAction },
288
+ bundled_package_missing: { note: "Optional local model package (flowseeker-local) is not installed.", nextAction: readiness.nextAction },
289
+ bundled_package_invalid: { note: "Built-in local model package is installed but invalid.", nextAction: readiness.nextAction }
290
+ };
291
+ const msg = statusMessages[readiness.localStatus] || statusMessages.local_unavailable_runtime_missing;
292
+ const isReady = readiness.localStatus === "local_ready" || readiness.localStatus === "bundled_package_ready";
293
+ lines.push("", `Result: ${readiness.localStatus}`, `Note: ${msg.note}`, `Next action: ${msg.nextAction}`);
294
+ if (testProvider && !isReady) {
295
+ lines.push("Provider test: skipped (local unavailable).");
296
+ process.exitCode = 1;
297
+ }
298
+ if (testProvider && isReady) {
299
+ const localProvider = (0, embeddingProviders_1.createConfiguredEmbeddingProvider)({ ...config.index, workspacePath: resolved });
300
+ if (!localProvider) {
301
+ lines.push("Provider test: local provider not available despite local_ready.");
302
+ process.exitCode = 1;
303
+ }
304
+ else {
305
+ try {
306
+ const [vector] = await localProvider.embedTexts(["flowseeker semantic provider check"]);
307
+ lines.push("", "Provider test: passed", `Vector: ${vector.length} dimensions`, `All finite: ${vector.every(v => isFinite(v))}`, "Next action: Local semantic retrieval is ready.");
308
+ }
309
+ catch (error) {
310
+ const message = error instanceof Error ? error.message : String(error);
311
+ lines.push("", "Provider test failed: " + message, "Next action: Check your local runtime installation and model cache.");
312
+ process.exitCode = 1;
313
+ }
314
+ }
315
+ }
316
+ process.stdout.write(`${lines.join("\n")}\n`);
317
+ return;
318
+ }
319
+ // Provider-neutral openaiCompatible must have a model configured
320
+ if (config.index.semanticProvider === "openaiCompatible" && !config.index.semanticModel) {
321
+ lines.push("", "Result: incomplete_config", "Next action: Set semanticModel in .flowseeker/config.json to the embedding model exposed by your provider. Set semanticBaseUrl if your provider needs a custom endpoint. Keep the key in .env.");
322
+ process.stdout.write(`${lines.join("\n")}\n`);
323
+ process.exitCode = 1;
324
+ return;
325
+ }
326
+ if (!provider) {
327
+ lines.push("", "Result: skipped_no_key", `Next action: Set ${apiKeyEnv}=<your embedding API key> in .env.`);
328
+ process.stdout.write(`${lines.join("\n")}\n`);
329
+ process.exitCode = 1;
330
+ return;
331
+ }
332
+ if (!testProvider) {
333
+ lines.push("", "Result: configured", "Next action: Run flowseeker semantic check --test to verify the key with one embedding request.");
334
+ process.stdout.write(`${lines.join("\n")}\n`);
335
+ return;
336
+ }
337
+ try {
338
+ const [vector] = await provider.embedTexts(["flowseeker semantic provider check"]);
339
+ lines.push("", "Result: active", `Provider test: passed (${vector.length} dimensions)`, "Next action: Semantic retrieval is ready. Your MCP/CLI scans will include semantic evidence.");
340
+ }
341
+ catch (error) {
342
+ const message = error instanceof Error ? error.message : String(error);
343
+ lines.push("", "Result: error", `Provider test failed: ${message}`, "Next action: Check your API key, base URL, and network access.");
344
+ process.exitCode = 1;
345
+ }
346
+ process.stdout.write(`${lines.join("\n")}\n`);
347
+ }
348
+ async function fileExists(filePath) {
349
+ try {
350
+ await fs.access(filePath);
351
+ return true;
352
+ }
353
+ catch {
354
+ return false;
355
+ }
356
+ }
357
+ // --- Doctor ---
358
+ async function runDoctorCommand(args) {
359
+ let workspace = process.cwd();
360
+ let useJson = false;
361
+ let checkMcp = false;
362
+ for (let i = 0; i < args.length; i++) {
363
+ const arg = args[i];
364
+ const next = args[i + 1];
365
+ if (arg === "--help" || arg === "-h") {
366
+ process.stderr.write("Usage: flowseeker doctor [--workspace <path>] [--json] [--mcp]\n");
367
+ return;
368
+ }
369
+ if ((arg === "--workspace" || arg === "-w") && next) {
370
+ workspace = path.resolve(next);
371
+ i++;
372
+ continue;
373
+ }
374
+ if (arg === "--json") {
375
+ useJson = true;
376
+ continue;
377
+ }
378
+ if (arg === "--mcp") {
379
+ checkMcp = true;
380
+ continue;
381
+ }
382
+ throw new Error(`Unknown doctor option: ${arg}`);
383
+ }
384
+ const resolved = path.resolve(workspace);
385
+ const configPath = path.join(resolved, ".flowseeker", "config.json");
386
+ const cachePath = path.join(resolved, ".flowseeker", "cache");
387
+ const envPath = path.join(resolved, ".env");
388
+ const pkg = JSON.parse(await fs.readFile(path.resolve(__dirname, "../../package.json"), "utf8"));
389
+ const config = await (0, loadConfigFromPath_1.loadConfigFromPath)(resolved);
390
+ const nextActions = [];
391
+ // Semantic status
392
+ const semEnabled = config.index.semanticEnabled;
393
+ const semProvider = config.index.semanticProvider || "none";
394
+ const semModel = config.index.semanticModel || "(empty)";
395
+ const semBaseUrl = config.index.semanticBaseUrl || "(provider default)";
396
+ const semKeyEnv = config.index.semanticApiKeyEnv || "(none)";
397
+ const semKeyOk = semKeyEnv ? !!process.env[semKeyEnv] : false;
398
+ const semCache = config.index.semanticCache;
399
+ const semMaxFiles = config.index.semanticMaxFilesPerRun;
400
+ const semTimeout = config.index.semanticTimeoutMs;
401
+ let semResult = "disabled";
402
+ if (!semEnabled)
403
+ semResult = "disabled";
404
+ else if (semProvider === "local") {
405
+ const localReadiness = (0, embeddingProviders_1.getLocalProviderReadiness)(resolved, {
406
+ semanticModel: config.index.semanticModel || "local-minilm-v1",
407
+ semanticLocalModelPath: config.index.semanticLocalModelPath,
408
+ semanticDimensions: config.index.semanticDimensions
409
+ });
410
+ semResult = localReadiness.localStatus;
411
+ }
412
+ else if (!semKeyOk)
413
+ semResult = "skipped_no_key";
414
+ else if (semProvider === "openaiCompatible" && !config.index.semanticModel)
415
+ semResult = "incomplete_config";
416
+ else
417
+ semResult = "configured";
418
+ if (semResult === "disabled")
419
+ nextActions.push("Semantic retrieval is optional. Run flowseeker semantic init --openai-compatible for hosted or flowseeker semantic init --local for local no-key path.");
420
+ else if (semResult === "skipped_no_key")
421
+ nextActions.push(`Set ${semKeyEnv} in .env and re-run flowseeker semantic check --test`);
422
+ else if (semResult === "local_unavailable" || semResult === "local_unavailable_runtime_missing")
423
+ nextActions.push("Run flowseeker semantic local setup, then flowseeker semantic local download.");
424
+ else if (semResult === "bundled_package_missing")
425
+ nextActions.push("Install the ready local model package: npm i -g flowseeker flowseeker-local. Then run flowseeker semantic check --test.");
426
+ else if (semResult === "bundled_package_invalid")
427
+ nextActions.push("The flowseeker-local package is installed but invalid. Reinstall: npm i -g flowseeker flowseeker-local.");
428
+ else if (semResult === "bundled_package_ready") { } // no action needed, ready
429
+ else if (semResult === "incomplete_config")
430
+ nextActions.push("Set semanticModel in .flowseeker/config.json to the embedding model exposed by your provider. Set semanticBaseUrl if your provider needs a custom endpoint.");
431
+ // Config/env presence
432
+ const configExists = await fileExists(configPath);
433
+ const envExists = await fileExists(envPath);
434
+ if (!configExists)
435
+ nextActions.push("No .flowseeker/config.json found. Deterministic/no-embedding retrieval is the default and requires no setup.");
436
+ // Cache
437
+ let cacheSummary = "not found";
438
+ if (await fileExists(cachePath)) {
439
+ try {
440
+ const cacheFiles = await fs.readdir(cachePath);
441
+ cacheSummary = `${cacheFiles.length} file(s)`;
442
+ if (cacheFiles.length === 0)
443
+ nextActions.push("Cache directory is empty. Run a scan to populate the index.");
444
+ }
445
+ catch {
446
+ cacheSummary = "unreadable";
447
+ }
448
+ }
449
+ // MCP check
450
+ let mcpStatus = { checked: false };
451
+ if (checkMcp) {
452
+ try {
453
+ const serverPath = path.join(__dirname, "mcpServer.js");
454
+ const child = (0, child_process_1.spawn)(process.execPath, [serverPath], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
455
+ const stdoutChunks = [];
456
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
457
+ // Send initialize
458
+ const initMsg = JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "flowseeker-doctor", version: "0.0.0" } } }) + "\n";
459
+ child.stdin?.write(initMsg);
460
+ // Send tools/list
461
+ const toolsMsg = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }) + "\n";
462
+ child.stdin?.write(toolsMsg);
463
+ // Send prompts/list
464
+ const promptsMsg = JSON.stringify({ jsonrpc: "2.0", id: 2, method: "prompts/list", params: {} }) + "\n";
465
+ child.stdin?.write(promptsMsg);
466
+ // Wait briefly then kill
467
+ await new Promise(r => setTimeout(r, 2000));
468
+ child.kill();
469
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
470
+ const hasRetrieve = stdout.includes("flowseeker_retrieve");
471
+ const hasGuide = stdout.includes("flowseeker_guide");
472
+ const hasAuto = stdout.includes("flowseeker_auto");
473
+ const hasFiles = stdout.includes("flowseeker_files");
474
+ const hasValidatePlan = stdout.includes("flowseeker_validate_plan");
475
+ const hasContext = stdout.includes("flowseeker_context");
476
+ const hasInspect = stdout.includes("flowseeker_inspect");
477
+ const hasExpand = stdout.includes("flowseeker_expand");
478
+ const hasStatus = stdout.includes("flowseeker_status");
479
+ const hasIndex = stdout.includes("flowseeker_index");
480
+ const hasFeedback = stdout.includes("flowseeker_feedback");
481
+ mcpStatus = {
482
+ checked: true,
483
+ initializeOk: stdout.includes('"id":0'),
484
+ toolsIncludesRetrieve: hasRetrieve,
485
+ toolsIncludesGuide: hasGuide,
486
+ toolsIncludesAuto: hasAuto,
487
+ toolsIncludesFiles: hasFiles,
488
+ toolsIncludesValidatePlan: hasValidatePlan,
489
+ toolsIncludesContext: hasContext,
490
+ toolsIncludesInspect: hasInspect,
491
+ toolsIncludesExpand: hasExpand,
492
+ toolsIncludesStatus: hasStatus,
493
+ toolsIncludesIndex: hasIndex,
494
+ toolsIncludesFeedback: hasFeedback,
495
+ promptsIncludeRetrieve: stdout.includes('"retrieve"'),
496
+ promptsIncludeGuide: stdout.includes('"guide"'),
497
+ promptsIncludeAuto: stdout.includes('"auto"'),
498
+ promptsIncludeFiles: stdout.includes('"files"'),
499
+ promptsIncludeValidatePlan: stdout.includes('"validate-plan"'),
500
+ promptsIncludeContext: stdout.includes('"context"'),
501
+ };
502
+ if (!hasRetrieve)
503
+ nextActions.push("MCP tools/list missing flowseeker_retrieve. Check MCP server startup.");
504
+ }
505
+ catch (e) {
506
+ mcpStatus = { checked: false, error: e.message };
507
+ nextActions.push("MCP self-check failed. Verify Node.js and the FlowSeeker installation.");
508
+ }
509
+ }
510
+ // Deterministic/no-embedding is default and always healthy
511
+ const deterministicStatus = "ok";
512
+ const isWarningResult = semResult === "skipped_no_key" || semResult === "local_unavailable" || semResult === "local_unavailable_runtime_missing" || semResult === "local_unavailable_model_missing" || semResult === "incomplete_config" || semResult === "bundled_package_missing" || semResult === "bundled_package_invalid";
513
+ const overall = isWarningResult ? "warn"
514
+ : (nextActions.filter(a => !a.includes("optional") && !a.includes("deterministic") && !a.includes("default") && !a.includes("No .flowseeker")).length > 0 ? "warn" : "ok");
515
+ const doctor = {
516
+ status: overall,
517
+ version: pkg.version ?? "unknown",
518
+ node: process.version,
519
+ platform: `${os.platform()} ${os.arch()}`,
520
+ workspace: resolved,
521
+ deterministic: { status: deterministicStatus, retrieval: "available", note: "No embedding required. Code-structure analysis is first-class." },
522
+ semantic: {
523
+ enabled: semEnabled, provider: semProvider, model: semModel, baseUrl: semBaseUrl,
524
+ keyEnv: semKeyEnv, keyConfigured: semKeyOk, cache: semCache, maxFilesPerRun: semMaxFiles,
525
+ timeoutMs: semTimeout, result: semResult
526
+ },
527
+ mcp: mcpStatus,
528
+ config: { path: configPath, exists: configExists },
529
+ env: { path: envPath, exists: envExists },
530
+ cache: { path: cachePath, summary: cacheSummary },
531
+ nextActions
532
+ };
533
+ if (useJson) {
534
+ process.stdout.write(JSON.stringify(doctor, null, 2) + "\n");
535
+ }
536
+ else {
537
+ const lines = [
538
+ "FlowSeeker Doctor",
539
+ "",
540
+ `Version: ${doctor.version} Node: ${doctor.node} Platform: ${doctor.platform}`,
541
+ `Workspace: ${doctor.workspace}`,
542
+ "",
543
+ "Deterministic:",
544
+ " Retrieval: available (default, first-class)",
545
+ " No embedding, API key, or model required.",
546
+ "",
547
+ "Semantic:",
548
+ ` Enabled: ${semEnabled} Provider: ${semProvider} Model: ${semModel}`,
549
+ ` Base URL: ${semBaseUrl} Key env: ${semKeyEnv} Key configured: ${semKeyOk}`,
550
+ ` Cache: ${semCache} Max files/run: ${semMaxFiles} Timeout: ${semTimeout}ms`,
551
+ ` Result: ${semResult}`,
552
+ "",
553
+ `Config: ${configPath} (${configExists ? "exists" : "missing"})`,
554
+ `Env: ${envPath} (${envExists ? "exists" : "missing"})`,
555
+ `Cache: ${cachePath} (${cacheSummary})`,
556
+ ""
557
+ ];
558
+ if (checkMcp) {
559
+ const mcp = mcpStatus;
560
+ lines.push("MCP:", "");
561
+ lines.push(" Self-check: " + (mcp.checked ? (mcp.initializeOk ? "pass" : "init failed") : "not run"));
562
+ if (mcp.checked) {
563
+ const toolIcons = (name) => mcp[name] ? "present" : "MISSING";
564
+ lines.push(" Tools: retrieve=" + toolIcons("toolsIncludesRetrieve") + " guide=" + toolIcons("toolsIncludesGuide") + " auto=" + toolIcons("toolsIncludesAuto") + " files=" + toolIcons("toolsIncludesFiles") + " validate_plan=" + toolIcons("toolsIncludesValidatePlan") + " context=" + toolIcons("toolsIncludesContext") + " inspect=" + toolIcons("toolsIncludesInspect") + " expand=" + toolIcons("toolsIncludesExpand") + " status=" + toolIcons("toolsIncludesStatus") + " index=" + toolIcons("toolsIncludesIndex") + " feedback=" + toolIcons("toolsIncludesFeedback"));
565
+ lines.push(" Prompts: retrieve=" + toolIcons("promptsIncludeRetrieve") + " guide=" + toolIcons("promptsIncludeGuide") + " auto=" + toolIcons("promptsIncludeAuto") + " files=" + toolIcons("promptsIncludeFiles") + " validate-plan=" + toolIcons("promptsIncludeValidatePlan") + " context=" + toolIcons("promptsIncludeContext"));
566
+ }
567
+ lines.push(" Server command: flowseeker");
568
+ lines.push(" Server args: mcp --workspace " + resolved + "");
569
+ lines.push("");
570
+ lines.push("Config candidates:");
571
+ const configCandidates = [
572
+ { path: path.join(resolved, ".claude", "mcp.json"), label: "Claude Code", method: "external CLI command: claude mcp add" },
573
+ { path: path.join(resolved, ".codex", "mcp.json"), label: "Codex CLI", method: "external CLI command: codex mcp add" },
574
+ { path: path.join(resolved, ".cursor", "mcp.json"), label: "Cursor", method: "file merge (flowseeker setup cursor --yes)" },
575
+ { path: path.join(resolved, ".vscode", "mcp.json"), label: "VS Code", method: "file merge (flowseeker setup vscode --yes)" },
576
+ ];
577
+ let anyConfig = false;
578
+ for (const cc of configCandidates) {
579
+ const exists = await fileExists(cc.path);
580
+ if (exists)
581
+ anyConfig = true;
582
+ lines.push(" " + cc.label + ": " + (exists ? cc.path + " (exists)" : cc.method));
583
+ }
584
+ // Local package status
585
+ const localInfo = (0, embeddingProviders_1.detectBundledLocalPackage)(resolved);
586
+ lines.push("");
587
+ lines.push("Local model package: " + (localInfo.available ? (localInfo.valid ? "installed and ready" : "installed but invalid") : "not installed (optional: npm i -g flowseeker-local)"));
588
+ lines.push("");
589
+ if (!anyConfig) {
590
+ nextActions.push("No MCP config found. Run: flowseeker setup claude --no-exec to preview, then flowseeker setup claude --yes to apply.");
591
+ }
592
+ if (!localInfo.available) {
593
+ nextActions.push("Optional: npm i -g flowseeker-local for local embeddings (no API key needed)");
594
+ }
595
+ }
596
+ if (nextActions.length > 0) {
597
+ lines.push("Next actions:");
598
+ nextActions.forEach(a => lines.push(` - ${a}`));
599
+ lines.push("");
600
+ }
601
+ lines.push(`Status: ${overall}`);
602
+ process.stdout.write(lines.join("\n") + "\n");
603
+ }
604
+ }
605
+ const UTF8_BOM = "";
606
+ function stripBom(text) {
607
+ return text.startsWith(UTF8_BOM) ? text.slice(1) : text;
608
+ }
609
+ async function readJsonSafely(filePath) {
610
+ if (!(await fileExists(filePath)))
611
+ return { data: {} };
612
+ let raw;
613
+ try {
614
+ raw = await fs.readFile(filePath, "utf8");
615
+ }
616
+ catch {
617
+ return { data: {}, error: "cannot read file" };
618
+ }
619
+ const cleaned = stripBom(raw);
620
+ try {
621
+ const parsed = JSON.parse(cleaned);
622
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
623
+ return { data: {}, error: "root is not a JSON object" };
624
+ return { data: parsed };
625
+ }
626
+ catch {
627
+ return { data: {}, error: "invalid JSON" };
628
+ }
629
+ }
630
+ async function writeMergedConfig(filePath, dir, key, entry) {
631
+ const safe = await readJsonSafely(filePath);
632
+ if (safe.error)
633
+ return { written: false, error: safe.error };
634
+ const existing = safe.data[key];
635
+ if (existing === null)
636
+ return { written: false, error: `"${key}" is null (expected object)` };
637
+ if (existing !== undefined && (typeof existing !== "object" || Array.isArray(existing)))
638
+ return { written: false, error: `"${key}" is not an object` };
639
+ const servers = existing || {};
640
+ safe.data[key] = { ...servers, flowseeker: entry };
641
+ await fs.mkdir(dir, { recursive: true });
642
+ await fs.writeFile(filePath, JSON.stringify(safe.data, null, 2) + "\n", "utf8");
643
+ return { written: true };
644
+ }
645
+ async function runSetupCommand(args) {
646
+ let workspace = process.cwd();
647
+ let dryRun = true;
648
+ let force = false;
649
+ let noExec = false;
650
+ const agents = [];
651
+ // Subcommand-style: flowseeker setup claude, flowseeker setup codex --yes, etc.
652
+ if (args[0] && !args[0].startsWith("-") && ["claude", "codex", "cursor", "vscode"].includes(args[0])) {
653
+ agents.push(args[0]);
654
+ args = args.slice(1); // shift for remaining flag parsing
655
+ }
656
+ for (let i = 0; i < args.length; i++) {
657
+ const arg = args[i];
658
+ const next = args[i + 1];
659
+ if (arg === "--help" || arg === "-h") {
660
+ process.stdout.write("Usage: flowseeker setup [claude|codex|cursor|vscode] [--workspace <path>] [--dry-run] [--force] [--yes] [--no-exec]\n\n");
661
+ process.stdout.write("One-command MCP setup for AI coding agents.\n\n");
662
+ process.stdout.write("Agents:\n");
663
+ process.stdout.write(" claude Setup Claude Code MCP (prints external CLI command)\n");
664
+ process.stdout.write(" codex Setup Codex CLI MCP (prints external CLI command)\n");
665
+ process.stdout.write(" cursor Setup Cursor MCP (writes .cursor/mcp.json)\n");
666
+ process.stdout.write(" vscode Setup VS Code MCP (writes .vscode/mcp.json)\n\n");
667
+ process.stdout.write("Flags:\n");
668
+ process.stdout.write(" --dry-run Preview config only (default)\n");
669
+ process.stdout.write(" --no-exec Same as --dry-run; preview without writing files\n");
670
+ process.stdout.write(" --yes / --force Write config files\n");
671
+ process.stdout.write(" -w, --workspace Target workspace (default: current directory)\n\n");
672
+ process.stdout.write("Examples:\n");
673
+ process.stdout.write(" flowseeker setup claude --no-exec # preview Claude MCP setup\n");
674
+ process.stdout.write(" flowseeker setup claude --yes # print Claude MCP add command\n");
675
+ process.stdout.write(" flowseeker setup vscode --yes # write VS Code MCP config\n");
676
+ process.stdout.write(" flowseeker doctor --mcp # verify MCP setup\n");
677
+ return;
678
+ }
679
+ if ((arg === "--workspace" || arg === "-w") && next) {
680
+ workspace = path.resolve(next);
681
+ i++;
682
+ continue;
683
+ }
684
+ if (arg === "--dry-run" || arg === "--no-exec") {
685
+ dryRun = true;
686
+ noExec = true;
687
+ continue;
688
+ }
689
+ if (arg === "--force" || arg === "--yes") {
690
+ force = true;
691
+ dryRun = false;
692
+ continue;
693
+ }
694
+ if (arg === "--agent" && next) {
695
+ agents.push(...next.split(",").map(s => s.trim()).filter(Boolean));
696
+ i++;
697
+ continue;
698
+ }
699
+ throw new Error(`Unknown setup option: ${arg}`);
700
+ }
701
+ if (agents.length === 0)
702
+ agents.push("claude", "codex", "cursor", "vscode");
703
+ const resolved = path.resolve(workspace);
704
+ const planned = [];
705
+ const nextActions = [];
706
+ const failures = [];
707
+ const agentDetails = [];
708
+ for (const agent of agents) {
709
+ const detail = { target: agent };
710
+ if (agent === "claude" || agent === "codex") {
711
+ const cmd = `${agent} mcp add flowseeker -- flowseeker mcp --workspace ${resolved}`;
712
+ detail.config = "external CLI-managed config (" + agent + " mcp add)";
713
+ detail.cmd = "flowseeker";
714
+ detail.args = "mcp --workspace " + resolved;
715
+ detail.apply = cmd;
716
+ detail.method = "external CLI command";
717
+ if (dryRun) {
718
+ planned.push(`[${agent}] Copy-paste into terminal: ${cmd}`);
719
+ }
720
+ else {
721
+ planned.push(`[${agent}] Copy-paste into terminal: ${cmd}`);
722
+ nextActions.push(`Run: ${cmd}`);
723
+ }
724
+ }
725
+ else if (agent === "cursor" || agent === "vscode") {
726
+ const dir = agent === "cursor" ? path.join(resolved, ".cursor") : path.join(resolved, ".vscode");
727
+ const filePath = path.join(dir, "mcp.json");
728
+ const key = agent === "cursor" ? "mcpServers" : "servers";
729
+ const entry = { command: "flowseeker", args: ["mcp", "--workspace", resolved] };
730
+ detail.config = filePath;
731
+ detail.configKey = key;
732
+ detail.cmd = "flowseeker";
733
+ detail.args = "mcp --workspace " + resolved;
734
+ detail.apply = "flowseeker setup " + agent + " --yes";
735
+ detail.method = "file merge";
736
+ if (dryRun) {
737
+ planned.push(`Would merge FlowSeeker into ${filePath} [key: ${key}]`);
738
+ }
739
+ else if (force) {
740
+ const result = await writeMergedConfig(filePath, dir, key, entry);
741
+ if (result.written) {
742
+ planned.push(`Merged FlowSeeker into ${filePath} [key: ${key}]`);
743
+ nextActions.push("Verify with: flowseeker doctor --mcp");
744
+ }
745
+ else {
746
+ failures.push(`[${agent}] ${filePath}: ${result.error}`);
747
+ nextActions.push(`Fix ${filePath} manually before re-running setup. File was NOT modified.`);
748
+ }
749
+ }
750
+ }
751
+ else {
752
+ failures.push(`[${agent}] Unknown agent. Supported: claude, codex, cursor, vscode`);
753
+ }
754
+ if (detail)
755
+ agentDetails.push(detail);
756
+ }
757
+ const hasExternalCli = agentDetails.some(d => d.method === "external CLI command");
758
+ const hasFileWrite = agentDetails.some(d => d.method === "file merge");
759
+ const mode = dryRun ? "dry-run" : "apply";
760
+ const lines = [
761
+ "FlowSeeker setup",
762
+ `Workspace: ${resolved}`,
763
+ `Mode: ${mode}`,
764
+ ""
765
+ ];
766
+ // Show local package status
767
+ const localInfo = (0, embeddingProviders_1.detectBundledLocalPackage)(resolved);
768
+ if (localInfo.available) {
769
+ lines.push("Local model package: " + (localInfo.valid ? "flowseeker-local installed and ready" : "installed but invalid"));
770
+ }
771
+ else {
772
+ lines.push("Local model package: not installed (optional)");
773
+ lines.push(" Install: npm i -g flowseeker-local (no API key needed)");
774
+ }
775
+ lines.push("");
776
+ // Per-agent schema
777
+ for (const d of agentDetails) {
778
+ lines.push("---");
779
+ lines.push("Target: " + d.target);
780
+ lines.push("Config: " + d.config);
781
+ if (d.configKey)
782
+ lines.push("Config key: " + d.configKey);
783
+ lines.push("Server command: " + d.cmd);
784
+ lines.push("Server args: " + d.args);
785
+ if (dryRun)
786
+ lines.push("Apply: " + d.apply);
787
+ lines.push("Verify: flowseeker doctor --mcp");
788
+ lines.push("");
789
+ }
790
+ if (agentDetails.length > 1)
791
+ lines.push("");
792
+ // Honest copy per method
793
+ lines.push("Next actions:");
794
+ if (dryRun) {
795
+ if (hasExternalCli)
796
+ lines.push(" - External CLI targets: run the command shown above to register FlowSeeker.");
797
+ if (hasFileWrite)
798
+ lines.push(" - File-merge targets: re-run with --yes to write/merge config files.");
799
+ }
800
+ else {
801
+ if (hasExternalCli)
802
+ lines.push(" - External CLI targets: run the Apply command shown above. FlowSeeker does not write config files for these targets.");
803
+ if (hasFileWrite)
804
+ lines.push(" - File-merge targets: config written. Run: flowseeker doctor --mcp to verify.");
805
+ if (!hasFileWrite && hasExternalCli)
806
+ lines.push(" - After running the external CLI command, verify with: flowseeker doctor --mcp");
807
+ }
808
+ if (nextActions.length > 0 && !dryRun) {
809
+ nextActions.forEach(a => lines.push(` - ${a}`));
810
+ }
811
+ lines.push("");
812
+ lines.push("");
813
+ process.stdout.write(lines.join("\n"));
814
+ if (failures.length > 0)
815
+ process.exitCode = 1;
816
+ }
817
+ // --- Setup end ---
818
+ function detectPackageManager(workspacePath, override) {
819
+ if (override)
820
+ return override;
821
+ const check = (file) => { try {
822
+ fsSync.accessSync(path.join(workspacePath, file));
823
+ return true;
824
+ }
825
+ catch {
826
+ return false;
827
+ } };
828
+ if (check("pnpm-lock.yaml"))
829
+ return "pnpm";
830
+ if (check("yarn.lock"))
831
+ return "yarn";
832
+ if (check("bun.lockb") || check("bun.lock"))
833
+ return "bun";
834
+ return "npm";
835
+ }
836
+ function pmInstallCommand(pm, pkg) {
837
+ if (pm === "pnpm")
838
+ return `pnpm add ${pkg}`;
839
+ if (pm === "yarn")
840
+ return `yarn add ${pkg}`;
841
+ if (pm === "bun")
842
+ return `bun add ${pkg}`;
843
+ return `npm install ${pkg}`;
844
+ }
845
+ function pmUninstallCommand(pm, pkg) {
846
+ if (pm === "pnpm")
847
+ return `pnpm remove ${pkg}`;
848
+ if (pm === "yarn")
849
+ return `yarn remove ${pkg}`;
850
+ if (pm === "bun")
851
+ return `bun remove ${pkg}`;
852
+ return `npm uninstall ${pkg}`;
853
+ }
854
+ function pmInstallArgs(pm, pkg) {
855
+ if (pm === "pnpm")
856
+ return ["add", pkg];
857
+ if (pm === "yarn")
858
+ return ["add", pkg];
859
+ if (pm === "bun")
860
+ return ["add", pkg];
861
+ return ["install", pkg];
862
+ }
863
+ function pmUninstallArgs(pm, pkg) {
864
+ if (pm === "pnpm")
865
+ return ["remove", pkg];
866
+ if (pm === "yarn")
867
+ return ["remove", pkg];
868
+ if (pm === "bun")
869
+ return ["remove", pkg];
870
+ return ["uninstall", pkg];
871
+ }
872
+ function buildLaunchPlan(pm, pkg, workspacePath) {
873
+ const displayCommand = pmInstallCommand(pm, pkg);
874
+ const pmArgs = pmInstallArgs(pm, pkg);
875
+ if (os.platform() === "win32") {
876
+ const cmdLine = [pm, ...pmArgs].join(" ");
877
+ return { displayCommand, pm, pmArgs, executable: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", cmdLine], cwd: workspacePath };
878
+ }
879
+ return { displayCommand, pm, pmArgs, executable: pm, args: pmArgs, cwd: workspacePath };
880
+ }
881
+ function buildProbePlan(pm, workspacePath) {
882
+ const displayCommand = pm + " --version";
883
+ const pmArgs = ["--version"];
884
+ if (os.platform() === "win32") {
885
+ const cmdLine = pm + " --version";
886
+ return { displayCommand, pm, pmArgs, executable: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", cmdLine], cwd: workspacePath };
887
+ }
888
+ return { displayCommand, pm, pmArgs, executable: pm, args: pmArgs, cwd: workspacePath };
889
+ }
890
+ async function runLauncher(plan) {
891
+ return new Promise((resolve) => {
892
+ const child = (0, child_process_1.spawn)(plan.executable, plan.args, { cwd: plan.cwd, stdio: "inherit", windowsHide: true });
893
+ child.on("error", (err) => {
894
+ process.stderr.write("Setup failed: could not start " + plan.executable + " - " + err.message + "\n");
895
+ process.stderr.write("Is " + plan.pm + " installed and available on PATH?\n");
896
+ process.exitCode = 1;
897
+ resolve(true);
898
+ });
899
+ child.on("exit", (code) => {
900
+ if (code !== 0) {
901
+ process.stderr.write("Command exited with code " + (code ?? "null") + "\n");
902
+ process.exitCode = code ?? 1;
903
+ resolve(true);
904
+ }
905
+ else {
906
+ resolve(false);
907
+ }
908
+ });
909
+ });
910
+ }
911
+ async function downloadArtifacts(workspacePath, modelId, modelDir, offline) {
912
+ const workspaceRequire = (0, module_1.createRequire)(path.join(workspacePath, "package.json"));
913
+ const transformers = workspaceRequire("@huggingface/transformers");
914
+ const pipeline = transformers.pipeline || transformers.default?.pipeline;
915
+ if (typeof pipeline !== "function")
916
+ throw new Error("Local runtime does not export pipeline.");
917
+ const pipelineOpts = { cache_dir: modelDir };
918
+ if (offline)
919
+ pipelineOpts.local_files_only = true;
920
+ const extractor = await pipeline("feature-extraction", modelId, pipelineOpts);
921
+ const callFn = typeof extractor === "function" ? extractor : extractor?.__call__;
922
+ if (typeof callFn !== "function")
923
+ throw new Error("Pipeline extractor is not callable.");
924
+ // Run a tiny probe to force artifact materialization
925
+ await callFn("flowseeker model download probe");
926
+ // Discover artifact files recursively under modelDir
927
+ const artifacts = {};
928
+ await collectArtifacts(modelDir, modelDir, artifacts);
929
+ if (Object.keys(artifacts).length === 0)
930
+ throw new Error("No artifact files were created under " + modelDir);
931
+ return artifacts;
932
+ }
933
+ async function collectArtifacts(baseDir, currentDir, out) {
934
+ let entries;
935
+ try {
936
+ entries = fsSync.readdirSync(currentDir, { withFileTypes: true });
937
+ }
938
+ catch {
939
+ return;
940
+ }
941
+ for (const entry of entries) {
942
+ const full = path.join(currentDir, entry.name);
943
+ if (entry.isDirectory()) {
944
+ await collectArtifacts(baseDir, full, out);
945
+ }
946
+ else if (entry.isFile()) {
947
+ const rel = path.relative(baseDir, full).replace(/\\/g, "/");
948
+ if (rel && rel !== "manifest.json")
949
+ out[rel] = rel;
950
+ }
951
+ }
952
+ }
953
+ // --- Local semantic subcommands ---
954
+ // --- Local semantic subcommands ---
955
+ async function runLocalCommand(args) {
956
+ // --help as first/only arg prints general help; recognized subcommands handle their own --help
957
+ const knownSubs = ["status", "setup", "download", "remove", "evidence"];
958
+ const hasHelp = args.some(arg => arg === "--help" || arg === "-h");
959
+ if (hasHelp && !(args[0] && knownSubs.includes(args[0]))) {
960
+ process.stdout.write("Usage: flowseeker semantic local [status|setup|download|remove|evidence] [-w <path>]\n\n");
961
+ process.stdout.write("Ready local package path (no API key, single install):\n");
962
+ process.stdout.write(" npm i -g flowseeker flowseeker-local\n");
963
+ process.stdout.write(" flowseeker semantic check --test\n\n");
964
+ process.stdout.write("Advanced project-level setup (explicit steps):\n");
965
+ process.stdout.write(" 1. flowseeker semantic local setup --yes # install runtime in project\n");
966
+ process.stdout.write(" 2. flowseeker semantic local download --yes # fetch/cache model files\n");
967
+ process.stdout.write(" 3. flowseeker semantic check --test # verify local embeddings\n\n");
968
+ process.stdout.write("Commands:\n");
969
+ process.stdout.write(" status Show local provider readiness (runtime, model, result)\n");
970
+ process.stdout.write(" setup Install @huggingface/transformers in your project (use --yes)\n");
971
+ process.stdout.write(" download Fetch/cache local model files (use --yes, --offline for pre-cached)\n");
972
+ process.stdout.write(" remove Remove local model cache from project\n");
973
+ process.stdout.write(" evidence Structured local readiness report (no install/download by default)\n\n");
974
+ process.stdout.write("Flags:\n");
975
+ process.stdout.write(" --yes Confirm action (required for setup/download/remove)\n");
976
+ process.stdout.write(" --no-exec Preview the execution plan without installing/downloading\n");
977
+ process.stdout.write(" --offline Pre-cached model only, no network (download only)\n");
978
+ process.stdout.write(" --probe Test launcher without installing (setup only)\n");
979
+ process.stdout.write(" --manager Override package manager: npm|pnpm|yarn|bun (setup only)\n");
980
+ process.stdout.write(" --print-command Print install command only (setup only)\n");
981
+ process.stdout.write(" --force Overwrite existing manifest (download/remove)\n");
982
+ process.stdout.write(" --dry-run Preview only, no changes (default)\n\n");
983
+ process.stdout.write("Local runtime/model is not bundled with FlowSeeker.\n");
984
+ process.stdout.write("Setup installs runtime only. Download fetches/caches model files.\n");
985
+ process.stdout.write("No API key or hosted provider is needed for local mode.\n");
986
+ return;
987
+ }
988
+ const sub = args[0] && !args[0].startsWith("-") ? args[0] : "status";
989
+ let workspace = process.cwd();
990
+ let dryRun = true;
991
+ let yes = false;
992
+ let force = false;
993
+ let printCommand = false;
994
+ let modelAlias = "";
995
+ let manager = "";
996
+ let noExec = false;
997
+ let probe = false;
998
+ let offline = false;
999
+ for (let i = 1; i < args.length; i++) {
1000
+ const arg = args[i];
1001
+ const next = args[i + 1];
1002
+ if (arg === "--help" || arg === "-h")
1003
+ continue;
1004
+ if ((arg === "--workspace" || arg === "-w") && next) {
1005
+ workspace = path.resolve(next);
1006
+ i++;
1007
+ continue;
1008
+ }
1009
+ if (arg === "--dry-run") {
1010
+ dryRun = true;
1011
+ continue;
1012
+ }
1013
+ if (arg === "--yes") {
1014
+ yes = true;
1015
+ dryRun = false;
1016
+ continue;
1017
+ }
1018
+ if (arg === "--force") {
1019
+ force = true;
1020
+ continue;
1021
+ }
1022
+ if (arg === "--print-command") {
1023
+ printCommand = true;
1024
+ continue;
1025
+ }
1026
+ if ((arg === "--model" || arg === "-m") && next) {
1027
+ modelAlias = next;
1028
+ i++;
1029
+ continue;
1030
+ }
1031
+ if ((arg === "--manager") && next) {
1032
+ manager = next;
1033
+ i++;
1034
+ continue;
1035
+ }
1036
+ if (arg === "--no-exec") {
1037
+ noExec = true;
1038
+ continue;
1039
+ }
1040
+ if (arg === "--probe") {
1041
+ probe = true;
1042
+ continue;
1043
+ }
1044
+ if (arg === "--offline") {
1045
+ offline = true;
1046
+ continue;
1047
+ }
1048
+ if (arg === "--json") {
1049
+ continue;
1050
+ }
1051
+ if ((arg === "--report" || arg === "-r") && next) {
1052
+ i++;
1053
+ continue;
1054
+ }
1055
+ throw new Error(`Unknown local option: ${arg}`);
1056
+ }
1057
+ const resolved = path.resolve(workspace);
1058
+ const configPath = path.join(resolved, ".flowseeker", "config.json");
1059
+ const modelBasePath = path.join(resolved, ".flowseeker", "models");
1060
+ const modelSlug = modelAlias || LOCAL_MODEL_PROFILE.modelSlug;
1061
+ // Reject unsafe model aliases
1062
+ if (modelAlias && (modelAlias.includes("..") || modelAlias.includes("/") || modelAlias.includes("\\") || modelAlias.length > 200)) {
1063
+ process.stderr.write("Unsafe model alias: " + modelAlias + "\n");
1064
+ process.exitCode = 1;
1065
+ return;
1066
+ }
1067
+ const modelDir = path.join(modelBasePath, modelSlug);
1068
+ const manifestPath = path.join(modelDir, "manifest.json");
1069
+ const configExists = await fileExists(configPath);
1070
+ let provider = "none";
1071
+ if (configExists) {
1072
+ try {
1073
+ const c = JSON.parse(await fs.readFile(configPath, "utf8"));
1074
+ const idx = c.index || {};
1075
+ provider = String(idx.semanticProvider || "none");
1076
+ }
1077
+ catch { /* unreadable */ }
1078
+ }
1079
+ if (sub === "status") {
1080
+ let localModelPath = "";
1081
+ let localDimensions = 0;
1082
+ if (configExists && provider === "local") {
1083
+ try {
1084
+ const c = JSON.parse(await fs.readFile(configPath, "utf8"));
1085
+ const idx = c.index || {};
1086
+ localModelPath = String(idx.semanticLocalModelPath || "");
1087
+ localDimensions = Number(idx.semanticDimensions || 0);
1088
+ }
1089
+ catch { /* unreadable */ }
1090
+ }
1091
+ // If manifest exists, use its model path
1092
+ if (await fileExists(manifestPath)) {
1093
+ try {
1094
+ const m = JSON.parse(await fs.readFile(manifestPath, "utf8"));
1095
+ localModelPath = String(m.modelPath || localModelPath);
1096
+ localDimensions = Number(m.dimensions || localDimensions);
1097
+ }
1098
+ catch { /* unreadable */ }
1099
+ }
1100
+ const readiness = (0, embeddingProviders_1.getLocalProviderReadiness)(resolved, {
1101
+ semanticModel: provider !== "local" ? "" : "local-minilm-v1",
1102
+ semanticLocalModelPath: localModelPath,
1103
+ semanticDimensions: localDimensions
1104
+ });
1105
+ const manifestStatus = await fileExists(manifestPath) ? "exists" : "not found";
1106
+ const lines = ["FlowSeeker local embedding status", "", `Workspace: ${resolved}`, `Config: ${configExists ? "exists" : "not found"}`, `Provider: ${provider}`, `Runtime: ${readiness.runtimeStatus}`, `Model path: ${localModelPath || modelBasePath}`, `Manifest: ${manifestStatus}`, `Models: none downloaded`, "", `Result: ${readiness.localStatus}`, `Next action: ${readiness.nextAction}`];
1107
+ process.stdout.write(lines.join("\n") + "\n");
1108
+ }
1109
+ else if (sub === "setup") {
1110
+ // Validate manager
1111
+ const validManagers = ["npm", "pnpm", "yarn", "bun"];
1112
+ if (manager && !validManagers.includes(manager)) {
1113
+ process.stderr.write("Invalid manager: " + manager + ". Use: " + validManagers.join(", ") + "\n");
1114
+ process.exitCode = 1;
1115
+ return;
1116
+ }
1117
+ const pm = detectPackageManager(resolved, manager);
1118
+ const installCmd = pmInstallCommand(pm, "@huggingface/transformers");
1119
+ const uninstallCmd = pmUninstallCommand(pm, "@huggingface/transformers");
1120
+ const runtimeAvailable = (0, embeddingProviders_1.detectLocalRuntime)(resolved);
1121
+ if (printCommand) {
1122
+ process.stdout.write(installCmd + "\n");
1123
+ }
1124
+ else if (yes) {
1125
+ if (probe) {
1126
+ const plan = buildProbePlan(pm, resolved);
1127
+ if (noExec) {
1128
+ process.stdout.write("NO-EXEC PROBE: Would run in " + resolved + ":\n");
1129
+ process.stdout.write(" Command: " + plan.displayCommand + "\n");
1130
+ process.stdout.write(" Launcher: " + plan.executable + "\n");
1131
+ process.stdout.write(" Launcher args: " + JSON.stringify(plan.args) + "\n");
1132
+ process.stdout.write("Execution skipped (--no-exec).\n");
1133
+ }
1134
+ else {
1135
+ process.stdout.write("Probing launcher: " + plan.executable + " " + plan.args.join(" ") + "\n");
1136
+ const probeFailed = await runLauncher(plan);
1137
+ if (!probeFailed) {
1138
+ process.stdout.write("Launcher probe: OK\n");
1139
+ process.stdout.write("Next action: Run flowseeker semantic local setup --yes to install the runtime.\n");
1140
+ }
1141
+ }
1142
+ }
1143
+ else {
1144
+ const plan = buildLaunchPlan(pm, "@huggingface/transformers", resolved);
1145
+ if (noExec) {
1146
+ process.stdout.write("NO-EXEC: Would run in " + resolved + ":\n");
1147
+ process.stdout.write(" Command: " + plan.displayCommand + "\n");
1148
+ process.stdout.write(" Launcher: " + plan.executable + "\n");
1149
+ process.stdout.write(" Launcher args: " + JSON.stringify(plan.args) + "\n");
1150
+ process.stdout.write(" CWD: " + plan.cwd + "\n");
1151
+ process.stdout.write("Execution skipped (--no-exec).\n");
1152
+ process.stdout.write("Next action: Run flowseeker semantic local status to check runtime readiness.\n");
1153
+ }
1154
+ else {
1155
+ process.stdout.write("Installing local embedding runtime...\n");
1156
+ process.stdout.write("Package manager: " + pm + "\n");
1157
+ process.stdout.write("Command: " + plan.displayCommand + "\n");
1158
+ const installFailed = await runLauncher(plan);
1159
+ if (!installFailed) {
1160
+ process.stdout.write("Setup complete. Next action: Run flowseeker semantic local status.\n");
1161
+ }
1162
+ }
1163
+ }
1164
+ }
1165
+ else {
1166
+ process.stdout.write("Local embedding runtime setup plan:\n\n");
1167
+ process.stdout.write("The runtime (@huggingface/transformers) will be installed in YOUR project.\n");
1168
+ process.stdout.write("FlowSeeker itself stays lean.\n\n");
1169
+ process.stdout.write("Package manager: " + pm + "\n");
1170
+ process.stdout.write("Install command: " + installCmd + "\n");
1171
+ process.stdout.write("Rollback command: " + uninstallCmd + "\n\n");
1172
+ process.stdout.write("Runtime status: " + (runtimeAvailable ? "available" : "not installed") + "\n\n");
1173
+ process.stdout.write("Use --print-command for machine-readable output.\n");
1174
+ process.stdout.write("Use --manager npm|pnpm|yarn|bun to override auto-detection.\n");
1175
+ process.stdout.write("Use --yes to install the runtime in this project.\n");
1176
+ process.stdout.write("Use --yes --no-exec to preview the execution plan.\n");
1177
+ process.stdout.write("Use --yes --probe to test the launcher (safe, no install).\n");
1178
+ process.stdout.write("setup does NOT download the model. Run semantic local download after setup.\n");
1179
+ }
1180
+ }
1181
+ else if (sub === "download") {
1182
+ const runtimeAvailable = (0, embeddingProviders_1.detectLocalRuntime)(resolved);
1183
+ // Dry-run: always useful, even without runtime
1184
+ if (dryRun) {
1185
+ process.stdout.write("DRY-RUN download plan:\n");
1186
+ process.stdout.write(" Model: " + LOCAL_MODEL_PROFILE.modelId + "\n");
1187
+ process.stdout.write(" Cache dir: " + modelDir + "\n");
1188
+ process.stdout.write(" Manifest path: " + manifestPath + "\n");
1189
+ process.stdout.write(" Runtime: " + (runtimeAvailable ? "available" : "not installed") + "\n");
1190
+ process.stdout.write(" Creates no files.\n");
1191
+ process.stdout.write(" Default: allows fetching/caching model files via project runtime.\n");
1192
+ process.stdout.write(" Use --offline for pre-cached model only (no network).\n");
1193
+ if (!runtimeAvailable)
1194
+ process.stdout.write(" Next action: Run flowseeker semantic local setup to install runtime.\n");
1195
+ else
1196
+ process.stdout.write(" Next action: Use --yes to download the model to " + modelDir + ".\n");
1197
+ return;
1198
+ }
1199
+ if (!runtimeAvailable && !noExec) {
1200
+ process.stdout.write("Local runtime not found. Run flowseeker semantic local setup first.\n");
1201
+ process.exitCode = 1;
1202
+ return;
1203
+ }
1204
+ if (yes) {
1205
+ if (noExec) {
1206
+ process.stdout.write("NO-EXEC: Would download model to " + modelDir + "\n");
1207
+ process.stdout.write(" Model: " + LOCAL_MODEL_PROFILE.modelId + "\n");
1208
+ process.stdout.write(" Runtime: " + (runtimeAvailable ? "available" : "not installed") + "\n");
1209
+ process.stdout.write(" Offline: " + (offline ? "yes (no network)" : "no (may fetch/cache)") + "\n");
1210
+ process.stdout.write("Execution skipped (--no-exec).\n");
1211
+ if (!runtimeAvailable)
1212
+ process.stdout.write("Next action: Run flowseeker semantic local setup first.\n");
1213
+ return;
1214
+ }
1215
+ const manifestExists = await fileExists(manifestPath);
1216
+ if (manifestExists && !force) {
1217
+ process.stdout.write("Model manifest already exists: " + manifestPath + "\nUse --force to overwrite.\n");
1218
+ return;
1219
+ }
1220
+ try {
1221
+ const artifacts = await downloadArtifacts(resolved, LOCAL_MODEL_PROFILE.modelId, modelDir, offline);
1222
+ const manifest = {
1223
+ schemaVersion: LOCAL_MODEL_PROFILE.schemaVersion,
1224
+ provider: "local",
1225
+ modelId: LOCAL_MODEL_PROFILE.modelId,
1226
+ modelSlug: LOCAL_MODEL_PROFILE.modelSlug,
1227
+ dimensions: LOCAL_MODEL_PROFILE.dimensions,
1228
+ createdAt: new Date().toISOString(),
1229
+ source: "huggingface",
1230
+ status: "ready",
1231
+ runtimePackage: LOCAL_MODEL_PROFILE.runtimePackage,
1232
+ artifacts
1233
+ };
1234
+ await fs.mkdir(modelDir, { recursive: true });
1235
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
1236
+ process.stdout.write("Model downloaded: " + modelDir + (offline ? " (offline)" : "") + "\n");
1237
+ process.stdout.write("Manifest written: " + manifestPath + " (status: ready)\n");
1238
+ // Update config
1239
+ if (configExists) {
1240
+ try {
1241
+ const c = JSON.parse(await fs.readFile(configPath, "utf8"));
1242
+ const idx = c.index || {};
1243
+ idx.semanticProvider = "local";
1244
+ idx.semanticLocalModelPath = modelDir;
1245
+ idx.semanticDimensions = LOCAL_MODEL_PROFILE.dimensions;
1246
+ idx.semanticLocalModelId = LOCAL_MODEL_PROFILE.modelId;
1247
+ c.index = idx;
1248
+ await fs.writeFile(configPath, JSON.stringify(c, null, 2) + "\n", "utf8");
1249
+ process.stdout.write("Config updated: " + configPath + "\n");
1250
+ }
1251
+ catch { /* config write non-fatal */ }
1252
+ }
1253
+ process.stdout.write("Next action: Run flowseeker semantic check --test to verify local embeddings.\n");
1254
+ }
1255
+ catch (err) {
1256
+ // Clean up on failure — don't leave partial state
1257
+ try {
1258
+ await fs.rm(modelDir, { recursive: true, force: true });
1259
+ }
1260
+ catch { /* best effort */ }
1261
+ process.stderr.write("Download failed: " + (err instanceof Error ? err.message : String(err)) + "\n");
1262
+ process.stderr.write("No model artifacts were saved. Re-run with --force to retry.\n");
1263
+ process.exitCode = 1;
1264
+ }
1265
+ }
1266
+ else {
1267
+ process.stdout.write("Local model download plan:\n\n");
1268
+ process.stdout.write("Model: " + LOCAL_MODEL_PROFILE.modelId + " (" + LOCAL_MODEL_PROFILE.dimensions + " dimensions)\n");
1269
+ process.stdout.write("Cache: " + modelDir + "\n\n");
1270
+ process.stdout.write("Default: allows fetching/caching model files via project runtime.\n");
1271
+ process.stdout.write("Use --offline for pre-cached model only (no network).\n");
1272
+ process.stdout.write("Use --dry-run to preview. Use --yes to download.\n");
1273
+ process.stdout.write("Use --yes --no-exec to preview the download plan.\n");
1274
+ }
1275
+ }
1276
+ else if (sub === "remove") {
1277
+ // Path safety: modelDir must be inside modelBasePath
1278
+ const resolvedBase = path.resolve(modelBasePath);
1279
+ const resolvedDir = path.resolve(modelDir);
1280
+ if (!resolvedDir.startsWith(resolvedBase + path.sep) && resolvedDir !== resolvedBase) {
1281
+ process.stderr.write("Path safety: model dir " + resolvedDir + " is outside " + resolvedBase + "\n");
1282
+ process.exitCode = 1;
1283
+ return;
1284
+ }
1285
+ const manifestExists = await fileExists(manifestPath);
1286
+ if (!manifestExists) {
1287
+ process.stdout.write("No model manifest found at " + manifestPath + "\nNothing to remove.\n");
1288
+ return;
1289
+ }
1290
+ if (dryRun) {
1291
+ process.stdout.write("DRY-RUN: Would remove " + modelDir + "\n");
1292
+ process.stdout.write("Use --yes to confirm.\n");
1293
+ }
1294
+ else if (yes) {
1295
+ try {
1296
+ await fs.rm(modelDir, { recursive: true, force: true });
1297
+ }
1298
+ catch { /* already gone */ }
1299
+ process.stdout.write("Removed: " + modelDir + "\n");
1300
+ // Clear model config fields
1301
+ if (configExists) {
1302
+ try {
1303
+ const c = JSON.parse(await fs.readFile(configPath, "utf8"));
1304
+ const idx = c.index || {};
1305
+ delete idx.semanticLocalModelPath;
1306
+ delete idx.semanticDimensions;
1307
+ delete idx.semanticLocalModelId;
1308
+ c.index = idx;
1309
+ await fs.writeFile(configPath, JSON.stringify(c, null, 2) + "\n", "utf8");
1310
+ process.stdout.write("Config cleaned: removed semanticLocalModelPath, semanticDimensions, semanticLocalModelId\n");
1311
+ }
1312
+ catch { /* config write failed */ }
1313
+ }
1314
+ }
1315
+ else {
1316
+ process.stdout.write("Local model remove plan:\n\n");
1317
+ process.stdout.write("Would remove: " + modelDir + "\n");
1318
+ process.stdout.write("Use --dry-run to preview. Use --yes to confirm.\n");
1319
+ }
1320
+ }
1321
+ else if (sub === "evidence") {
1322
+ // --help should print help even without other flags
1323
+ if (args.some(arg => arg === "--help" || arg === "-h")) {
1324
+ process.stdout.write("Usage: flowseeker semantic local evidence [--json] [--report <path>] [--workspace <path>]\n\n");
1325
+ process.stdout.write("Produce a structured local embedding readiness/evidence report.\n");
1326
+ process.stdout.write("Report-only by default: no install, no download, no network, no hosted fallback.\n\n");
1327
+ process.stdout.write("Options:\n");
1328
+ process.stdout.write(" --json Print JSON report to stdout\n");
1329
+ process.stdout.write(" --report <path> Write JSON report to file\n");
1330
+ process.stdout.write(" -w, --workspace <path> Target workspace (default: current directory)\n\n");
1331
+ process.stdout.write("Setup and download are explicit separate commands:\n");
1332
+ process.stdout.write(" flowseeker semantic local setup --yes\n");
1333
+ process.stdout.write(" flowseeker semantic local download --yes\n");
1334
+ return;
1335
+ }
1336
+ let useJson = false;
1337
+ let reportPath = "";
1338
+ for (let i = 1; i < args.length; i++) {
1339
+ if (args[i] === "--json") {
1340
+ useJson = true;
1341
+ }
1342
+ else if ((args[i] === "--report" || args[i] === "-r") && args[i + 1]) {
1343
+ reportPath = args[++i];
1344
+ }
1345
+ else if (args[i] === "--workspace" || args[i] === "-w") {
1346
+ i++;
1347
+ }
1348
+ }
1349
+ const modelPath = await readConfigModelPath(configPath);
1350
+ const effectiveModelPath = modelPath || modelDir;
1351
+ const readiness = (0, embeddingProviders_1.getLocalProviderReadiness)(resolved, {
1352
+ semanticModel: "local-minilm-v1",
1353
+ semanticLocalModelPath: effectiveModelPath,
1354
+ semanticDimensions: 384
1355
+ });
1356
+ const statusMap = {
1357
+ "local_unavailable_runtime_missing": "runtime_missing",
1358
+ "local_unavailable_model_missing": "model_missing",
1359
+ "local_unavailable_model_invalid": "model_invalid",
1360
+ "local_ready": "pass",
1361
+ "bundled_package_ready": "pass",
1362
+ "bundled_package_missing": "model_missing",
1363
+ "bundled_package_invalid": "model_invalid"
1364
+ };
1365
+ const classification = statusMap[readiness.localStatus] || "unknown_error";
1366
+ const evidence = {
1367
+ timestamp: new Date().toISOString(),
1368
+ workspace: resolved,
1369
+ runtimeStatus: readiness.runtimeStatus,
1370
+ modelStatus: readiness.modelStatus,
1371
+ readinessStatus: readiness.localStatus,
1372
+ providerName: "local",
1373
+ modelId: "",
1374
+ dimensions: 0,
1375
+ vectorCount: 0,
1376
+ allFinite: false,
1377
+ embedLatencyMs: 0,
1378
+ classification,
1379
+ nextAction: readiness.nextAction,
1380
+ allowInstall: false,
1381
+ allowDownload: false
1382
+ };
1383
+ const isReady = readiness.localStatus === "local_ready" || readiness.localStatus === "bundled_package_ready";
1384
+ if (isReady && (effectiveModelPath || readiness.localStatus === "bundled_package_ready")) {
1385
+ let modelSrcPath = effectiveModelPath;
1386
+ if (readiness.localStatus === "bundled_package_ready") {
1387
+ // Get model info from bundled package
1388
+ const bundled = (0, embeddingProviders_1.detectBundledLocalPackage)(resolved);
1389
+ if (bundled.valid && bundled.packagePath) {
1390
+ evidence.modelId = bundled.modelId || evidence.modelId;
1391
+ evidence.dimensions = bundled.dimensions || evidence.dimensions;
1392
+ modelSrcPath = path.dirname(bundled.manifestPath || bundled.packagePath);
1393
+ }
1394
+ }
1395
+ evidence.modelId = LOCAL_MODEL_PROFILE.modelId;
1396
+ evidence.dimensions = LOCAL_MODEL_PROFILE.dimensions;
1397
+ try {
1398
+ const mfPath = path.join(effectiveModelPath, "manifest.json");
1399
+ if (await fileExists(mfPath)) {
1400
+ const mf = JSON.parse(await fs.readFile(mfPath, "utf8"));
1401
+ if (mf.modelId)
1402
+ evidence.modelId = mf.modelId;
1403
+ if (mf.dimensions)
1404
+ evidence.dimensions = Number(mf.dimensions);
1405
+ }
1406
+ }
1407
+ catch { /* use defaults */ }
1408
+ const provider = (0, embeddingProviders_1.createConfiguredEmbeddingProvider)({
1409
+ semanticProvider: "local", semanticBaseUrl: "", semanticApiKeyEnv: "",
1410
+ semanticModel: String(evidence.modelId), semanticTimeoutMs: 60000,
1411
+ semanticLocalModelPath: effectiveModelPath, semanticDimensions: Number(evidence.dimensions),
1412
+ workspacePath: resolved
1413
+ });
1414
+ if (provider) {
1415
+ const t0 = Date.now();
1416
+ try {
1417
+ const vectors = await provider.embedTexts(["flowseeker semantic local evidence"]);
1418
+ evidence.embedLatencyMs = Date.now() - t0;
1419
+ evidence.vectorCount = vectors.length;
1420
+ evidence.allFinite = vectors.length > 0 && vectors.every((v) => v.every((x) => isFinite(x)));
1421
+ evidence.classification = "pass";
1422
+ evidence.nextAction = "Local embedding provider is ready.";
1423
+ }
1424
+ catch (err) {
1425
+ evidence.embedLatencyMs = Date.now() - t0;
1426
+ const msg = err instanceof Error ? err.message : String(err);
1427
+ evidence.classification = msg.indexOf("dimension") >= 0 || msg.indexOf("non-numeric") >= 0 ? "embedding_shape_invalid"
1428
+ : msg.indexOf("pipeline") >= 0 || msg.indexOf("export") >= 0 ? "runtime_api_mismatch" : "unknown_error";
1429
+ evidence.nextAction = msg;
1430
+ }
1431
+ }
1432
+ }
1433
+ // Write report file if requested (before stdout to keep JSON clean)
1434
+ if (reportPath) {
1435
+ const absReport = path.resolve(reportPath);
1436
+ await fs.mkdir(path.dirname(absReport), { recursive: true });
1437
+ await fs.writeFile(absReport, JSON.stringify(evidence, null, 2) + "\n", "utf8");
1438
+ }
1439
+ if (useJson) {
1440
+ // --json: valid JSON only to stdout, no trailing text
1441
+ process.stdout.write(JSON.stringify(evidence, null, 2) + "\n");
1442
+ }
1443
+ else if (reportPath) {
1444
+ // --report without --json: also print JSON to stdout plus confirmation
1445
+ process.stdout.write(JSON.stringify(evidence, null, 2) + "\n");
1446
+ process.stdout.write("Report written: " + path.resolve(reportPath) + "\n");
1447
+ }
1448
+ else {
1449
+ const lines = [
1450
+ "Local embedding evidence report", "",
1451
+ "Workspace: " + evidence.workspace,
1452
+ "Runtime: " + evidence.runtimeStatus,
1453
+ "Model: " + evidence.modelStatus,
1454
+ "Readiness: " + evidence.readinessStatus,
1455
+ "Classification: " + evidence.classification,
1456
+ "Next action: " + evidence.nextAction
1457
+ ];
1458
+ if (evidence.modelId)
1459
+ lines.push("Model ID: " + evidence.modelId);
1460
+ if (Number(evidence.dimensions) > 0)
1461
+ lines.push("Dimensions: " + evidence.dimensions);
1462
+ if (Number(evidence.vectorCount) > 0)
1463
+ lines.push("Vectors: " + evidence.vectorCount + " (all finite: " + evidence.allFinite + ")");
1464
+ if (Number(evidence.embedLatencyMs) > 0)
1465
+ lines.push("Latency: " + evidence.embedLatencyMs + "ms");
1466
+ process.stdout.write(lines.join("\n") + "\n");
1467
+ }
1468
+ }
1469
+ else {
1470
+ throw new Error(`Unknown local subcommand: ${sub}`);
1471
+ }
1472
+ }
1473
+ async function readConfigModelPath(configPath) {
1474
+ try {
1475
+ const c = JSON.parse(await fs.readFile(configPath, "utf8"));
1476
+ const idx = c.index || {};
1477
+ return String(idx.semanticLocalModelPath || "");
1478
+ }
1479
+ catch {
1480
+ return "";
1481
+ }
1482
+ }
1483
+ // --- End local ---
1484
+ // --- Benchmark ---
1485
+ async function runBenchmarkCommand(args) {
1486
+ let workspace = process.cwd();
1487
+ let useJson = false;
1488
+ for (let i = 0; i < args.length; i++) {
1489
+ const arg = args[i];
1490
+ const next = args[i + 1];
1491
+ if (arg === "--help" || arg === "-h") {
1492
+ process.stderr.write("Usage: flowseeker benchmark [--workspace <path>] [--json]\n");
1493
+ return;
1494
+ }
1495
+ if ((arg === "--workspace" || arg === "-w") && next) {
1496
+ workspace = path.resolve(next);
1497
+ i++;
1498
+ continue;
1499
+ }
1500
+ if (arg === "--json") {
1501
+ useJson = true;
1502
+ continue;
1503
+ }
1504
+ throw new Error(`Unknown benchmark option: ${arg}`);
1505
+ }
1506
+ const resolved = path.resolve(workspace);
1507
+ const config = await (0, loadConfigFromPath_1.loadConfigFromPath)(resolved);
1508
+ const localReadiness = (0, embeddingProviders_1.getLocalProviderReadiness)(resolved, {
1509
+ semanticModel: config.index.semanticModel,
1510
+ semanticLocalModelPath: config.index.semanticLocalModelPath,
1511
+ semanticDimensions: config.index.semanticDimensions
1512
+ });
1513
+ const hostedStatus = determineHostedStatus(config.index);
1514
+ const modes = [
1515
+ {
1516
+ mode: "deterministic_no_embedding",
1517
+ status: "first_class",
1518
+ qualityMeasured: true,
1519
+ performanceMeasured: true,
1520
+ reasonWhenSkipped: "",
1521
+ filesScanned: "smoke gate passing, publishable claims verified",
1522
+ runtimeMs: 0,
1523
+ regressionGate: "pass",
1524
+ nextAction: "No action needed. Deterministic retrieval is the default and fully functional."
1525
+ },
1526
+ {
1527
+ mode: "hosted_semantic",
1528
+ status: hostedStatus.status,
1529
+ qualityMeasured: false,
1530
+ performanceMeasured: false,
1531
+ reasonWhenSkipped: hostedStatus.reasonWhenSkipped,
1532
+ filesScanned: 0,
1533
+ runtimeMs: 0,
1534
+ regressionGate: "pass",
1535
+ nextAction: hostedStatus.nextAction
1536
+ },
1537
+ {
1538
+ mode: "local_provider",
1539
+ status: localReadiness.localStatus,
1540
+ qualityMeasured: false,
1541
+ performanceMeasured: true,
1542
+ reasonWhenSkipped: localReadiness.reasonWhenSkipped,
1543
+ filesScanned: 0,
1544
+ runtimeMs: 0,
1545
+ regressionGate: localReadiness.localStatus === "local_unavailable_runtime_missing" || localReadiness.localStatus.indexOf("ready") >= 0 ? "pass" : "warn",
1546
+ nextAction: localReadiness.nextAction
1547
+ }
1548
+ ];
1549
+ if (useJson) {
1550
+ process.stdout.write(JSON.stringify({ workspace: resolved, modes }, null, 2) + "\n");
1551
+ }
1552
+ else {
1553
+ const lines = ["FlowSeeker benchmark report", "", `Workspace: ${resolved}`];
1554
+ for (const m of modes) {
1555
+ lines.push("", `Mode: ${m.mode}`, `Status: ${m.status}`, `Quality measured: ${m.qualityMeasured}`, `Performance measured: ${m.performanceMeasured}`, `Regression gate: ${m.regressionGate}`, `Next action: ${m.nextAction}`);
1556
+ if (m.reasonWhenSkipped)
1557
+ lines.push(`Reason: ${m.reasonWhenSkipped}`);
1558
+ }
1559
+ process.stdout.write(lines.join("\n") + "\n");
1560
+ }
1561
+ }
1562
+ function determineHostedStatus(idx) {
1563
+ if (!idx.semanticEnabled)
1564
+ return { status: "disabled", reasonWhenSkipped: "Semantic retrieval is disabled. Run flowseeker semantic init --jina or --openai-compatible.", nextAction: "Run flowseeker semantic init --jina or --openai-compatible" };
1565
+ if (idx.semanticProvider === "local")
1566
+ return { status: "not_active", reasonWhenSkipped: "Hosted semantic is not active when provider is local.", nextAction: "" };
1567
+ if (idx.semanticProvider === "openaiCompatible" && !idx.semanticModel)
1568
+ return { status: "incomplete_config", reasonWhenSkipped: "semanticModel is empty. Set it to the embedding model exposed by your provider in .flowseeker/config.json.", nextAction: "Edit .flowseeker/config.json: set semanticModel to your provider's embedding model." };
1569
+ if (!process.env[idx.semanticApiKeyEnv])
1570
+ return { status: "skipped_no_key", reasonWhenSkipped: idx.semanticApiKeyEnv + " is missing or empty.", nextAction: "Set " + idx.semanticApiKeyEnv + " in .env and re-run flowseeker semantic check --test." };
1571
+ return { status: "configured", reasonWhenSkipped: "", nextAction: "Run flowseeker semantic check --test to verify hosted embeddings." };
1572
+ }
1573
+ async function runFlowCommand(options) {
1574
+ const input = {
1575
+ task: options.task,
1576
+ workspace: options.workspace,
1577
+ maxFiles: options.maxFiles,
1578
+ limit: options.limit
1579
+ };
1580
+ const result = await (0, mcpTools_1.handleToolCall)(modeToToolName(options.mode), input, options.workspace);
1581
+ const text = result.content.map((item) => item.text).join("\n\n");
1582
+ if (options.outFile) {
1583
+ await fs.mkdir(path.dirname(path.resolve(options.outFile)), { recursive: true });
1584
+ await fs.writeFile(options.outFile, text, "utf8");
1585
+ }
1586
+ else {
1587
+ process.stdout.write(`${text}\n`);
1588
+ }
1589
+ if (result.isError) {
1590
+ process.exitCode = 1;
1591
+ }
1592
+ }
1593
+ async function runMcpServer(args) {
1594
+ const serverPath = path.join(__dirname, "mcpServer.js");
1595
+ await new Promise((resolve, reject) => {
1596
+ const child = (0, child_process_1.spawn)(process.execPath, [serverPath, ...args], {
1597
+ stdio: "inherit",
1598
+ windowsHide: true
1599
+ });
1600
+ child.on("error", reject);
1601
+ child.on("exit", (code, signal) => {
1602
+ if (signal) {
1603
+ process.kill(process.pid, signal);
1604
+ return;
1605
+ }
1606
+ process.exitCode = code ?? 0;
1607
+ resolve();
1608
+ });
1609
+ });
1610
+ }
1611
+ function parseFlowArgs(mode, args) {
1612
+ let workspace = process.cwd();
1613
+ let maxFiles;
1614
+ let limit;
1615
+ let outFile;
1616
+ const taskParts = [];
1617
+ for (let i = 0; i < args.length; i++) {
1618
+ const arg = args[i];
1619
+ const next = args[i + 1];
1620
+ if (arg === "--help" || arg === "-h") {
1621
+ printCommandHelp(mode);
1622
+ process.exit(0);
1623
+ }
1624
+ if ((arg === "--workspace" || arg === "-w") && next) {
1625
+ workspace = path.resolve(next);
1626
+ i++;
1627
+ continue;
1628
+ }
1629
+ if (arg === "--max-files" && next) {
1630
+ maxFiles = parsePositiveInt(next, "--max-files");
1631
+ i++;
1632
+ continue;
1633
+ }
1634
+ if ((arg === "--limit" || arg === "-n") && next) {
1635
+ limit = parsePositiveInt(next, "--limit");
1636
+ i++;
1637
+ continue;
1638
+ }
1639
+ if ((arg === "--out" || arg === "-o") && next) {
1640
+ outFile = next;
1641
+ i++;
1642
+ continue;
1643
+ }
1644
+ if (arg.startsWith("-")) {
1645
+ throw new Error(`Unknown option: ${arg}`);
1646
+ }
1647
+ taskParts.push(arg);
1648
+ }
1649
+ const task = taskParts.join(" ").trim();
1650
+ if (!task) {
1651
+ printCommandHelp(mode);
1652
+ throw new Error("Task is required.");
1653
+ }
1654
+ return { mode, workspace, task, maxFiles, limit, outFile };
1655
+ }
1656
+ function modeToToolName(mode) {
1657
+ switch (mode) {
1658
+ case "auto":
1659
+ return "flowseeker_auto";
1660
+ case "files":
1661
+ return "flowseeker_files";
1662
+ case "retrieve":
1663
+ return "flowseeker_retrieve";
1664
+ case "guide":
1665
+ default:
1666
+ return "flowseeker_guide";
1667
+ }
1668
+ }
1669
+ function parsePositiveInt(value, option) {
1670
+ const parsed = Number(value);
1671
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1672
+ throw new Error(`${option} must be a positive integer.`);
1673
+ }
1674
+ return parsed;
1675
+ }
1676
+ async function printVersion() {
1677
+ const packageJsonPath = path.resolve(__dirname, "../../package.json");
1678
+ const raw = await fs.readFile(packageJsonPath, "utf8");
1679
+ const parsed = JSON.parse(raw);
1680
+ process.stdout.write(`${parsed.version ?? "unknown"}\n`);
1681
+ }
1682
+ function printHelp() {
1683
+ const lines = [
1684
+ "FlowSeeker",
1685
+ "",
1686
+ "Usage:",
1687
+ " flowseeker guide \"Fix checkout webhook retry\"",
1688
+ " flowseeker auto \"Add CSV export with tests\"",
1689
+ " flowseeker files \"Find auth middleware\"",
1690
+ " flowseeker retrieve \"Debug login session expiry\"",
1691
+ " flowseeker mcp --workspace .",
1692
+ " flowseeker semantic init",
1693
+ " flowseeker semantic check --test",
1694
+ "",
1695
+ "Commands:",
1696
+ " guide Build an agent guidance prompt from retrieved context.",
1697
+ " auto Build a reviewable implementation-plan prompt.",
1698
+ " files Print top ranked files only.",
1699
+ " retrieve Print the full Solve Packet.",
1700
+ " mcp Start the FlowSeeker MCP stdio server.",
1701
+ " semantic Init or check optional semantic embedding retrieval.",
1702
+ " doctor Diagnose install, config, semantic, and MCP status.",
1703
+ " setup Configure FlowSeeker MCP for Claude, Codex, Cursor, VS Code.",
1704
+ " benchmark Report retrieval mode readiness (offline, no key needed).",
1705
+ " claude Print Claude Code MCP setup commands.",
1706
+ "",
1707
+ "Options for guide/auto/files/retrieve:",
1708
+ " -w, --workspace <path> Workspace root. Default: current directory.",
1709
+ " -o, --out <file> Write output to a file instead of stdout.",
1710
+ " -n, --limit <count> File limit for files. Default: 15.",
1711
+ " --max-files <count> Scan cap for retrieve.",
1712
+ "",
1713
+ "Aliases:",
1714
+ " fs-guide, fs-auto, fs-files, fs-retrieve, fs-mcp",
1715
+ ""
1716
+ ];
1717
+ process.stdout.write(lines.join("\n") + "\n");
1718
+ }
1719
+ function printSemanticHelp() {
1720
+ const lines = [
1721
+ "FlowSeeker semantic retrieval",
1722
+ "",
1723
+ "Usage:",
1724
+ " flowseeker semantic init --jina",
1725
+ " flowseeker semantic init --openai-compatible",
1726
+ " flowseeker semantic init --local",
1727
+ " flowseeker semantic check",
1728
+ " flowseeker semantic check --test",
1729
+ " flowseeker semantic local status",
1730
+ " flowseeker semantic local setup",
1731
+ " flowseeker semantic local download",
1732
+ "",
1733
+ "Init presets:",
1734
+ " --jina Hosted Jina embeddings preset (recommended quick start)",
1735
+ " --openai-compatible Generic OpenAI-compatible path; configure model/base URL before testing",
1736
+ " --local Local embeddings via explicit project-level runtime setup",
1737
+ "",
1738
+ "Local commands:",
1739
+ " semantic local status Show local provider readiness (runtime, model, status)",
1740
+ " semantic local setup Install @huggingface/transformers in your project",
1741
+ " semantic local download Fetch/cache local model files",
1742
+ "",
1743
+ "Ready local package path (no API key, single install):",
1744
+ " npm i -g flowseeker flowseeker-local",
1745
+ " flowseeker semantic check --test",
1746
+ "",
1747
+ "Note: Local runtime/model is not bundled with FlowSeeker.",
1748
+ "Setup installs runtime only. Download fetches/caches model files.",
1749
+ "No API key or hosted provider is needed for local mode.",
1750
+ "Run `flowseeker semantic local --help` for the full local command reference.",
1751
+ "",
1752
+ "Options:",
1753
+ " -w, --workspace <path> Workspace root. Default: current directory.",
1754
+ " --test Call the embedding provider once to verify key/model/base URL.",
1755
+ "",
1756
+ "Config (stored in .flowseeker/config.json):",
1757
+ " semanticProvider e.g. openaiCompatible",
1758
+ " semanticBaseUrl Provider endpoint (https://api.jina.ai/v1 or your own)",
1759
+ " semanticModel Embedding model name exposed by your provider",
1760
+ " semanticApiKeyEnv Env var holding the API key (FLOWSEEKER_EMBEDDING_API_KEY)",
1761
+ "",
1762
+ "Secret (stored in .env only):",
1763
+ " FLOWSEEKER_EMBEDDING_API_KEY=<your key>",
1764
+ "",
1765
+ "The model must match the provider and base URL.",
1766
+ "",
1767
+ "Fast setup:",
1768
+ " flowseeker semantic init --jina",
1769
+ " # fill FLOWSEEKER_EMBEDDING_API_KEY in .env",
1770
+ " flowseeker semantic check --test",
1771
+ ""
1772
+ ];
1773
+ process.stdout.write(lines.join("\n") + "\n");
1774
+ }
1775
+ function escapeRegExp(value) {
1776
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1777
+ }
1778
+ function printCommandHelp(mode) {
1779
+ const lines = [
1780
+ `Usage: flowseeker ${mode} [options] "task description"`,
1781
+ "",
1782
+ "Options:",
1783
+ " -w, --workspace <path> Workspace root. Default: current directory.",
1784
+ " -o, --out <file> Write output to a file instead of stdout.",
1785
+ " -n, --limit <count> File limit for files. Default: 15.",
1786
+ " --max-files <count> Scan cap for retrieve.",
1787
+ ""
1788
+ ];
1789
+ console.error(lines.join("\n"));
1790
+ }
1791
+ function printClaudeSetup() {
1792
+ const lines = [
1793
+ "Claude Code setup",
1794
+ "",
1795
+ "1. Install globally:",
1796
+ " npm i -g flowseeker",
1797
+ "",
1798
+ "2. Add FlowSeeker as an MCP server from your target repo:",
1799
+ " claude mcp add flowseeker -- flowseeker mcp --workspace .",
1800
+ "",
1801
+ "3. In Claude Code, ask it to use FlowSeeker:",
1802
+ " Use FlowSeeker to guide: Fix checkout webhook retry",
1803
+ "",
1804
+ "Direct prompt workflow without MCP:",
1805
+ " flowseeker auto \"Fix checkout webhook retry\" --out .flowseeker/agent-prompt.md",
1806
+ ""
1807
+ ];
1808
+ process.stdout.write(lines.join("\n"));
1809
+ }
1810
+ //# sourceMappingURL=main.js.map