@bndynet/ragbox 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -4,7 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = require("node:fs");
7
9
  const promises_1 = __importDefault(require("node:fs/promises"));
10
+ const node_http_1 = __importDefault(require("node:http"));
8
11
  const node_path_1 = __importDefault(require("node:path"));
9
12
  const commander_1 = require("commander");
10
13
  const config_file_1 = require("./config-file");
@@ -24,6 +27,12 @@ function parseConcurrency(value) {
24
27
  }
25
28
  return parsed;
26
29
  }
30
+ function parsePageIndexRunner(value) {
31
+ if (value === "auto" || value === "single" || value === "batch") {
32
+ return value;
33
+ }
34
+ throw new Error("--pageindex-runner must be one of: auto, single, batch");
35
+ }
27
36
  function parseNonNegativeInteger(value, optionName) {
28
37
  const parsed = Number.parseInt(value, 10);
29
38
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -40,6 +49,9 @@ function parseRetryDelayMs(value) {
40
49
  function parseDebounceMs(value) {
41
50
  return parseNonNegativeInteger(value, "--debounce-ms");
42
51
  }
52
+ function parseStopTimeoutMs(value) {
53
+ return parseNonNegativeInteger(value, "--timeout-ms");
54
+ }
43
55
  function parseServePort(value) {
44
56
  const parsed = Number.parseInt(value, 10);
45
57
  if (!Number.isFinite(parsed) || parsed < 0 || parsed > 65535) {
@@ -76,6 +88,32 @@ function logProgress(event) {
76
88
  break;
77
89
  }
78
90
  }
91
+ function firstLine(value) {
92
+ return value.split(/\r?\n/, 1)[0] ?? value;
93
+ }
94
+ function printIndexProgress(event) {
95
+ switch (event.type) {
96
+ case "scan":
97
+ console.error(`[ragbox] scan complete output=${event.outputDir} total=${event.total} toIndex=${event.toIndex} unchanged=${event.unchanged} deleted=${event.deleted}`);
98
+ break;
99
+ case "index-start":
100
+ console.error(`[ragbox] indexing ${event.index}/${event.total} ${event.path}`);
101
+ break;
102
+ case "index-done":
103
+ console.error(`[ragbox] indexed ${event.index}/${event.total} ${event.path}`);
104
+ if (event.summary) {
105
+ console.error(`[ragbox] summary ${event.path}: ${event.summary}`);
106
+ }
107
+ break;
108
+ case "index-failed":
109
+ console.error(`[ragbox] failed ${event.index}/${event.total} ${event.path}: ${firstLine(event.error)}`);
110
+ break;
111
+ case "write":
112
+ console.error(`[ragbox] wrote manifest=${event.manifestPath}`);
113
+ console.error(`[ragbox] wrote rootTree=${event.rootTreePath}`);
114
+ break;
115
+ }
116
+ }
79
117
  function addLlmOptions(command) {
80
118
  return command
81
119
  .option("--api-key <key>", "OpenAI-compatible API key")
@@ -218,6 +256,7 @@ function buildOptions(configOptions, commandOptions, progress = logProgress) {
218
256
  cliPath: commandOptions.pageindexCli,
219
257
  model: commandOptions.model,
220
258
  outputDir: commandOptions.outputDir,
259
+ pageIndexRunner: commandOptions.pageindexRunner,
221
260
  pythonPath: commandOptions.pageindexPython,
222
261
  watchDebounceMs: commandOptions.debounceMs,
223
262
  watchHealthFile: commandOptions.healthFile,
@@ -245,6 +284,94 @@ function parseSourceNames(source) {
245
284
  .map((name) => name.trim())
246
285
  .filter(Boolean);
247
286
  }
287
+ function resolveServeProbeHost(host) {
288
+ if (host === "0.0.0.0") {
289
+ return "127.0.0.1";
290
+ }
291
+ if (host === "::") {
292
+ return "::1";
293
+ }
294
+ return host;
295
+ }
296
+ function serveHealthUrl(host, port) {
297
+ const urlHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
298
+ return `http://${urlHost}:${port}/health`;
299
+ }
300
+ function isServeHealthResult(value) {
301
+ if (typeof value !== "object" || value === null) {
302
+ return false;
303
+ }
304
+ const health = value;
305
+ return health.version === 1
306
+ && typeof health.ok === "boolean"
307
+ && (health.status === "ready" || health.status === "degraded" || health.status === "error")
308
+ && typeof health.indexes === "object"
309
+ && health.indexes !== null;
310
+ }
311
+ async function checkServeHealth(configPath) {
312
+ const { config } = await (0, config_file_1.readRagboxConfig)(configPath);
313
+ const serveConfig = (0, config_file_1.resolveRagboxServeConfig)({ config });
314
+ const host = resolveServeProbeHost(serveConfig.host);
315
+ const port = serveConfig.port;
316
+ const url = serveHealthUrl(host, port);
317
+ return await new Promise((resolve) => {
318
+ const request = node_http_1.default.get(url, { timeout: 2000 }, (response) => {
319
+ let raw = "";
320
+ response.setEncoding("utf8");
321
+ response.on("data", (chunk) => {
322
+ raw += chunk;
323
+ });
324
+ response.on("end", () => {
325
+ let health;
326
+ let error;
327
+ if (raw.trim()) {
328
+ try {
329
+ const parsed = JSON.parse(raw);
330
+ if (isServeHealthResult(parsed)) {
331
+ health = parsed;
332
+ }
333
+ else {
334
+ error = "Response was not a ragbox health payload.";
335
+ }
336
+ }
337
+ catch (parseError) {
338
+ error = parseError instanceof Error ? parseError.message : String(parseError);
339
+ }
340
+ }
341
+ else {
342
+ error = "Response body was empty.";
343
+ }
344
+ const statusCode = response.statusCode ?? 0;
345
+ resolve({
346
+ version: 1,
347
+ url,
348
+ host,
349
+ port,
350
+ ok: Boolean(health?.ok) && statusCode >= 200 && statusCode < 300,
351
+ reachable: true,
352
+ status: health?.status,
353
+ statusCode,
354
+ health,
355
+ error
356
+ });
357
+ });
358
+ });
359
+ request.on("timeout", () => {
360
+ request.destroy(new Error("Timed out after 2000ms."));
361
+ });
362
+ request.on("error", (error) => {
363
+ resolve({
364
+ version: 1,
365
+ url,
366
+ host,
367
+ port,
368
+ ok: false,
369
+ reachable: false,
370
+ error: error.message
371
+ });
372
+ });
373
+ });
374
+ }
248
375
  function requireFolder(folder, commandName) {
249
376
  if (!folder) {
250
377
  throw new Error(`Missing folder. Pass a folder argument or configure a source rootDir before running ragbox ${commandName}.`);
@@ -433,7 +560,7 @@ async function loadStartTargets(command, commandOptions, folder) {
433
560
  }
434
561
  ];
435
562
  }
436
- async function buildStatusOutput(targets) {
563
+ async function buildStatusOutput(targets, configPath) {
437
564
  const statusTargets = [];
438
565
  for (const target of targets) {
439
566
  const validation = await (0, sdk_1.validateIndex)(target.target);
@@ -446,13 +573,40 @@ async function buildStatusOutput(targets) {
446
573
  warnings: validation.warnings
447
574
  });
448
575
  }
576
+ const serve = await checkServeHealth(configPath);
449
577
  return {
450
578
  version: 1,
451
579
  command: "status",
452
- ok: statusTargets.every((target) => target.ok),
453
- targets: statusTargets
580
+ ok: statusTargets.every((target) => target.ok) && serve.ok,
581
+ targets: statusTargets,
582
+ serve
454
583
  };
455
584
  }
585
+ function serveHealthSummary(serve) {
586
+ if (!serve.reachable) {
587
+ return `HTTP server is not reachable at ${serve.url}: ${serve.error ?? "connection failed"}`;
588
+ }
589
+ if (!serve.health) {
590
+ return `HTTP server responded at ${serve.url}, but health could not be parsed: ${serve.error ?? "invalid response"}`;
591
+ }
592
+ const indexes = serve.health.indexes;
593
+ return `HTTP server ${serve.health.status} at ${serve.url}; indexes ready=${indexes.ready}/${indexes.total}.`;
594
+ }
595
+ function printServeHealthOutput(serve) {
596
+ console.log(`${serve.ok ? "ok" : "error"} serve ${serve.url}`);
597
+ console.log(` reachable=${serve.reachable}`);
598
+ if (serve.statusCode !== undefined) {
599
+ console.log(` http=${serve.statusCode}`);
600
+ }
601
+ if (serve.health) {
602
+ const indexes = serve.health.indexes;
603
+ console.log(` status=${serve.health.status}`);
604
+ console.log(` indexes=${indexes.total} ready=${indexes.ready} failed=${indexes.failed}`);
605
+ }
606
+ if (serve.error) {
607
+ console.log(` error ${serve.error}`);
608
+ }
609
+ }
456
610
  function printStatusOutput(status) {
457
611
  for (const target of status.targets) {
458
612
  const label = target.source ? `${target.source} ${target.target}` : target.target;
@@ -470,6 +624,9 @@ function printStatusOutput(status) {
470
624
  console.log(` warning ${warning.code}: ${warning.message}`);
471
625
  }
472
626
  }
627
+ if (status.serve) {
628
+ printServeHealthOutput(status.serve);
629
+ }
473
630
  }
474
631
  function startTargetLabel(target) {
475
632
  return target.source ? `${target.source} ${target.rootDir}` : target.rootDir;
@@ -517,6 +674,177 @@ async function closeStartHandles(watchHandles, serveHandle) {
517
674
  ...(serveHandle ? [serveHandle.close()] : [])
518
675
  ]);
519
676
  }
677
+ const BACKGROUND_CHILD_ENV = "RAGBOX_BACKGROUND_CHILD";
678
+ function stripBackgroundStartArgs(args) {
679
+ const stripped = [];
680
+ let afterTerminator = false;
681
+ for (let index = 0; index < args.length; index += 1) {
682
+ const arg = args[index];
683
+ if (afterTerminator) {
684
+ stripped.push(arg);
685
+ continue;
686
+ }
687
+ if (arg === "--") {
688
+ afterTerminator = true;
689
+ stripped.push(arg);
690
+ continue;
691
+ }
692
+ if (arg === "--background" || arg.startsWith("--background=")) {
693
+ continue;
694
+ }
695
+ if (arg === "--pid-file" || arg === "--log-file") {
696
+ index += 1;
697
+ continue;
698
+ }
699
+ if (arg === "--no-pid-file" || arg.startsWith("--pid-file=") || arg.startsWith("--log-file=")) {
700
+ continue;
701
+ }
702
+ stripped.push(arg);
703
+ }
704
+ return stripped;
705
+ }
706
+ async function writeBackgroundPidFile(pidFile, pid) {
707
+ if (!pidFile) {
708
+ return undefined;
709
+ }
710
+ const resolvedPidFile = node_path_1.default.resolve(pidFile);
711
+ await promises_1.default.mkdir(node_path_1.default.dirname(resolvedPidFile), { recursive: true });
712
+ await promises_1.default.writeFile(resolvedPidFile, `${pid}\n`, "utf8");
713
+ return resolvedPidFile;
714
+ }
715
+ async function launchBackgroundStart(commandOptions) {
716
+ const cliScript = process.argv[1];
717
+ if (!cliScript) {
718
+ throw new Error("Cannot start ragbox in the background because the CLI script path is unavailable.");
719
+ }
720
+ const logFile = node_path_1.default.resolve(commandOptions.logFile ?? "ragbox.log");
721
+ await promises_1.default.mkdir(node_path_1.default.dirname(logFile), { recursive: true });
722
+ const logFd = (0, node_fs_1.openSync)(logFile, "a");
723
+ const childArgs = [cliScript, ...stripBackgroundStartArgs(process.argv.slice(2))];
724
+ const child = (0, node_child_process_1.spawn)(process.execPath, childArgs, {
725
+ cwd: process.cwd(),
726
+ detached: true,
727
+ env: {
728
+ ...process.env,
729
+ [BACKGROUND_CHILD_ENV]: "1"
730
+ },
731
+ stdio: ["ignore", logFd, logFd]
732
+ });
733
+ try {
734
+ if (!child.pid) {
735
+ throw new Error("Failed to start background ragbox process.");
736
+ }
737
+ const pidFile = await writeBackgroundPidFile(commandOptions.pidFile === false ? undefined : commandOptions.pidFile ?? "ragbox.pid", child.pid);
738
+ child.unref();
739
+ return {
740
+ pid: child.pid,
741
+ logFile,
742
+ pidFile
743
+ };
744
+ }
745
+ catch (error) {
746
+ child.kill("SIGTERM");
747
+ throw error;
748
+ }
749
+ finally {
750
+ (0, node_fs_1.closeSync)(logFd);
751
+ }
752
+ }
753
+ function printBackgroundStartResult(result, jsonl) {
754
+ if (jsonl) {
755
+ writeStartJsonLine("start-background", result);
756
+ return;
757
+ }
758
+ console.log(`Started ragbox in the background (pid=${result.pid})`);
759
+ console.log(`log=${result.logFile}`);
760
+ if (result.pidFile) {
761
+ console.log(`pidFile=${result.pidFile}`);
762
+ }
763
+ }
764
+ function sleep(ms) {
765
+ return new Promise((resolve) => setTimeout(resolve, ms));
766
+ }
767
+ function isNoSuchProcessError(error) {
768
+ return error.code === "ESRCH";
769
+ }
770
+ async function readStopPidFile(pidFile) {
771
+ let raw;
772
+ try {
773
+ raw = await promises_1.default.readFile(pidFile, "utf8");
774
+ }
775
+ catch (error) {
776
+ if (error.code === "ENOENT") {
777
+ throw new Error(`No ragbox pid file found: ${pidFile}. Run ragbox start --background first, or pass --pid-file.`);
778
+ }
779
+ throw error;
780
+ }
781
+ const trimmed = raw.trim();
782
+ const pid = Number.parseInt(trimmed, 10);
783
+ if (!trimmed || !Number.isInteger(pid) || pid <= 0 || String(pid) !== trimmed) {
784
+ throw new Error(`Invalid ragbox pid file: ${pidFile}`);
785
+ }
786
+ return pid;
787
+ }
788
+ function isProcessRunning(pid) {
789
+ try {
790
+ process.kill(pid, 0);
791
+ return true;
792
+ }
793
+ catch (error) {
794
+ if (isNoSuchProcessError(error)) {
795
+ return false;
796
+ }
797
+ throw error;
798
+ }
799
+ }
800
+ async function waitForProcessExit(pid, timeoutMs) {
801
+ const startedAt = Date.now();
802
+ do {
803
+ if (!isProcessRunning(pid)) {
804
+ return true;
805
+ }
806
+ await sleep(50);
807
+ } while (Date.now() - startedAt < timeoutMs);
808
+ return !isProcessRunning(pid);
809
+ }
810
+ async function runStopAction(commandOptions) {
811
+ const pidFile = node_path_1.default.resolve(commandOptions.pidFile ?? "ragbox.pid");
812
+ const pid = await readStopPidFile(pidFile);
813
+ const signal = commandOptions.force ? "SIGKILL" : "SIGTERM";
814
+ const timeoutMs = commandOptions.timeoutMs ?? 5000;
815
+ let stale = false;
816
+ try {
817
+ process.kill(pid, signal);
818
+ }
819
+ catch (error) {
820
+ if (!isNoSuchProcessError(error)) {
821
+ throw error;
822
+ }
823
+ stale = true;
824
+ }
825
+ const stopped = stale || await waitForProcessExit(pid, timeoutMs);
826
+ if (!stopped) {
827
+ throw new Error(`ragbox process ${pid} did not stop within ${timeoutMs}ms; pid file left in place: ${pidFile}`);
828
+ }
829
+ await promises_1.default.rm(pidFile, { force: true });
830
+ return {
831
+ version: 1,
832
+ command: "stop",
833
+ pid,
834
+ pidFile,
835
+ signal,
836
+ stopped,
837
+ stale,
838
+ removedPidFile: true
839
+ };
840
+ }
841
+ function printStopResult(result) {
842
+ if (result.stale) {
843
+ console.log(`Removed stale ragbox pid file ${result.pidFile} (pid=${result.pid})`);
844
+ return;
845
+ }
846
+ console.log(`Stopped ragbox (pid=${result.pid}, signal=${result.signal})`);
847
+ }
520
848
  function isPathLikeCommand(value) {
521
849
  return node_path_1.default.isAbsolute(value) || value.startsWith(".") || value.includes("/") || value.includes("\\");
522
850
  }
@@ -576,12 +904,20 @@ async function buildDoctorOutput(command, commandOptions, target) {
576
904
  ok: Boolean(runtime.apiKey),
577
905
  message: runtime.apiKey ? "LLM API key is configured." : "OPENAI_API_KEY or llm.apiKey is not configured."
578
906
  });
579
- const status = await buildStatusOutput(targets);
907
+ const status = await buildStatusOutput(targets, globalOptions.config);
908
+ const indexStatusOk = status.targets.every((target) => target.ok);
580
909
  checks.push({
581
910
  name: "index-status",
582
- ok: status.ok,
911
+ ok: indexStatusOk,
583
912
  message: status.targets.length > 0 ? `Checked ${status.targets.length} index target(s).` : "No index target was checked."
584
913
  });
914
+ if (status.serve) {
915
+ checks.push({
916
+ name: "serve-health",
917
+ ok: status.serve.ok,
918
+ message: serveHealthSummary(status.serve)
919
+ });
920
+ }
585
921
  return {
586
922
  version: 1,
587
923
  command: "doctor",
@@ -718,11 +1054,6 @@ async function runStartAction(folder, commandOptions, command) {
718
1054
  });
719
1055
  watchHandles.push(handle);
720
1056
  }
721
- const readyResults = await Promise.all(watchHandles.map((handle) => handle.ready));
722
- const failedReady = readyResults.find((ready) => !ready.ok);
723
- if (failedReady && !failedReady.ok) {
724
- throw new Error(`Initial index failed: ${failedReady.error}`);
725
- }
726
1057
  const sourceNames = targets.map((target) => target.source).filter((source) => Boolean(source));
727
1058
  const singleTarget = targets.length === 1 ? targets[0] : undefined;
728
1059
  serveHandle = await (0, serve_1.startServe)({
@@ -747,6 +1078,12 @@ async function runStartAction(folder, commandOptions, command) {
747
1078
  else {
748
1079
  console.log(`Serving ragbox at ${serveHandle.url}`);
749
1080
  }
1081
+ const readyResults = await Promise.all(watchHandles.map((handle) => handle.ready));
1082
+ const failedReady = readyResults.find((ready) => !ready.ok);
1083
+ if (failedReady && !failedReady.ok) {
1084
+ throw new Error(`Initial index failed: ${failedReady.error}`);
1085
+ }
1086
+ await reloadServe();
750
1087
  await new Promise((resolve) => {
751
1088
  let closing = false;
752
1089
  const stop = () => {
@@ -828,11 +1165,15 @@ async function main() {
828
1165
  .option("--pageindex-cli <path>", "PageIndex script path")
829
1166
  .option("-o, --output-dir <folder>", "folder for ragbox index files")
830
1167
  .option("--pageindex-python <path>", "Python executable used to run PageIndex")
1168
+ .option("--pageindex-runner <mode>", "PageIndex runner mode: auto, single, or batch", parsePageIndexRunner)
831
1169
  .option("--json", "print a stable JSON result")))
832
1170
  .action(async (folder, commandOptions, command) => {
833
1171
  const loaded = await loadCommandConfig(command, commandOptions);
834
1172
  const indexFolderPath = requireFolder(folder ?? loaded.rootDir, "index");
835
- const result = await (0, indexer_1.indexFolder)(indexFolderPath, buildOptions(loaded.options, commandOptions));
1173
+ if (!commandOptions.json) {
1174
+ console.error(`[ragbox] indexing ${indexFolderPath}`);
1175
+ }
1176
+ const result = await (0, indexer_1.indexFolder)(indexFolderPath, buildOptions(loaded.options, commandOptions, commandOptions.json ? logProgress : printIndexProgress));
836
1177
  if (commandOptions.json) {
837
1178
  writeJson(indexJsonOutput(result));
838
1179
  return;
@@ -873,8 +1214,9 @@ async function main() {
873
1214
  .option("--all-sources", "check every configured source")
874
1215
  .option("--json", "print a stable JSON result"))
875
1216
  .action(async (target, commandOptions, command) => {
1217
+ const globalOptions = getGlobalOptions(command);
876
1218
  const targets = await loadDiagnosticTargets(command, commandOptions, target, true);
877
- const status = await buildStatusOutput(targets);
1219
+ const status = await buildStatusOutput(targets, globalOptions.config);
878
1220
  if (commandOptions.json) {
879
1221
  writeJson(status);
880
1222
  return;
@@ -921,15 +1263,20 @@ async function main() {
921
1263
  .argument("[folder]", "folder to index, watch, and serve")
922
1264
  .option("--all-sources", "start every configured source")
923
1265
  .option("--auth-token <token>", "bearer token required for non-health endpoints")
1266
+ .option("--background", "run start as a detached background process")
924
1267
  .option("-c, --concurrency <number>", "PageIndex concurrency", parseConcurrency)
925
1268
  .option("--pageindex-cli <path>", "PageIndex script path")
926
1269
  .option("-o, --output-dir <folder>", "folder for ragbox index files")
927
1270
  .option("--pageindex-python <path>", "Python executable used to run PageIndex")
1271
+ .option("--pageindex-runner <mode>", "PageIndex runner mode: auto, single, or batch", parsePageIndexRunner)
928
1272
  .option("--debounce-ms <ms>", "watch change debounce in milliseconds", parseDebounceMs)
929
1273
  .option("--health-file <path>", "write a watch health JSON file")
930
- .option("--host <host>", "host to bind", process.env.RAGBOX_SERVE_HOST ?? "127.0.0.1")
1274
+ .option("--host <host>", "host to bind")
931
1275
  .option("--jsonl", "print stable JSON Lines start, watch, and index progress events")
1276
+ .option("--log-file <path>", "background stdout/stderr log file; defaults to ./ragbox.log")
932
1277
  .option("--lock-file <path>", "create an exclusive lock file while start is running")
1278
+ .option("--pid-file <path>", "background process id file; defaults to ./ragbox.pid")
1279
+ .option("--no-pid-file", "do not write a background process id file")
933
1280
  .option("--port <number>", "port to bind", parseServePort)
934
1281
  .option("--retry-attempts <number>", "retry failed watch index runs", parseRetryAttempts)
935
1282
  .option("--retry-delay-ms <ms>", "delay between watch retries in milliseconds", parseRetryDelayMs)
@@ -937,14 +1284,34 @@ async function main() {
937
1284
  .option("--staging-output-dir <folder>", "staging directory used with --staging")
938
1285
  .option("--webhook <url>", "POST watch events to a webhook URL")))
939
1286
  .action(async (folder, commandOptions, command) => {
1287
+ if (commandOptions.background && process.env[BACKGROUND_CHILD_ENV] !== "1") {
1288
+ const result = await launchBackgroundStart(commandOptions);
1289
+ printBackgroundStartResult(result, commandOptions.jsonl);
1290
+ return;
1291
+ }
940
1292
  await runStartAction(folder, commandOptions, command);
941
1293
  });
1294
+ program
1295
+ .command("stop")
1296
+ .description("stop a background ragbox start process")
1297
+ .option("--pid-file <path>", "pid file written by ragbox start --background", "ragbox.pid")
1298
+ .option("--force", "send SIGKILL instead of SIGTERM")
1299
+ .option("--timeout-ms <ms>", "time to wait for the process to exit", parseStopTimeoutMs)
1300
+ .option("--json", "print a stable JSON result")
1301
+ .action(async (commandOptions) => {
1302
+ const result = await runStopAction(commandOptions);
1303
+ if (commandOptions.json) {
1304
+ writeJson(result);
1305
+ return;
1306
+ }
1307
+ printStopResult(result);
1308
+ });
942
1309
  addProjectOptions(addLlmOptions(program
943
1310
  .command("serve")
944
1311
  .argument("[target]", "docs folder or ragbox output directory")
945
1312
  .option("--all-sources", "serve every configured source by default")
946
1313
  .option("--auth-token <token>", "bearer token required for non-health endpoints")
947
- .option("--host <host>", "host to bind", process.env.RAGBOX_SERVE_HOST ?? "127.0.0.1")
1314
+ .option("--host <host>", "host to bind")
948
1315
  .option("--port <number>", "port to bind", parseServePort)))
949
1316
  .action(async (target, commandOptions, command) => {
950
1317
  const globalOptions = getGlobalOptions(command);
@@ -986,6 +1353,7 @@ async function main() {
986
1353
  .option("--pageindex-cli <path>", "PageIndex script path")
987
1354
  .option("-o, --output-dir <folder>", "folder for ragbox index files")
988
1355
  .option("--pageindex-python <path>", "Python executable used to run PageIndex")
1356
+ .option("--pageindex-runner <mode>", "PageIndex runner mode: auto, single, or batch", parsePageIndexRunner)
989
1357
  .option("--debounce-ms <ms>", "watch change debounce in milliseconds", parseDebounceMs)
990
1358
  .option("--health-file <path>", "write a watch health JSON file")
991
1359
  .option("--jsonl", "print stable JSON Lines watch and index progress events")
@@ -1,11 +1,14 @@
1
- import { PageIndexOptions } from "./folder-index/types";
1
+ import { PageIndexOptions, PageIndexRunner } from "./folder-index/types";
2
2
  export declare const RAGBOX_CONFIG_FILE = "ragbox.config.json";
3
+ export declare const DEFAULT_SERVE_HOST = "127.0.0.1";
4
+ export declare const DEFAULT_SERVE_PORT = 8787;
3
5
  export type RagboxPageIndexConfig = {
4
6
  cli?: string;
5
7
  concurrency?: number;
6
8
  extraArgs?: string[];
7
9
  outputArg?: string;
8
10
  python?: string;
11
+ runner?: PageIndexRunner;
9
12
  };
10
13
  export type RagboxLlmConfig = {
11
14
  apiKey?: string;
@@ -17,6 +20,11 @@ export type RagboxIndexConfig = {
17
20
  include?: string[];
18
21
  outputDir?: string;
19
22
  };
23
+ export type RagboxServeConfig = {
24
+ authToken?: string;
25
+ host?: string;
26
+ port?: number;
27
+ };
20
28
  export type RagboxConfigSource = RagboxIndexConfig & {
21
29
  index?: RagboxIndexConfig;
22
30
  llm?: RagboxLlmConfig;
@@ -29,6 +37,7 @@ export type RagboxConfig = {
29
37
  index?: RagboxIndexConfig;
30
38
  llm?: RagboxLlmConfig;
31
39
  pageIndex?: RagboxPageIndexConfig;
40
+ serve?: RagboxServeConfig;
32
41
  sources?: Record<string, RagboxConfigSource>;
33
42
  };
34
43
  export type ResolvedRagboxConfig = {
@@ -57,6 +66,13 @@ export type WritePageIndexSetupConfigOptions = {
57
66
  cwd?: string;
58
67
  pythonPath?: string;
59
68
  };
69
+ export type ResolveRagboxServeConfigOptions = RagboxServeConfig & {
70
+ config?: RagboxConfig;
71
+ env?: NodeJS.ProcessEnv;
72
+ };
73
+ export type ResolvedRagboxServeConfig = Required<Pick<RagboxServeConfig, "host" | "port">> & Pick<RagboxServeConfig, "authToken">;
74
+ export declare function parseRagboxServePort(value: unknown, source: string): number | undefined;
75
+ export declare function resolveRagboxServeConfig(options?: ResolveRagboxServeConfigOptions): ResolvedRagboxServeConfig;
60
76
  export declare function createDefaultRagboxConfig(options?: Pick<WriteDefaultRagboxConfigOptions, "docsDir" | "outputDir">): RagboxConfig;
61
77
  export declare function listRagboxConfigSourceNames(config: RagboxConfig | undefined): string[];
62
78
  export declare function writeDefaultRagboxConfig(options?: WriteDefaultRagboxConfigOptions): Promise<string>;
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.RAGBOX_CONFIG_FILE = void 0;
6
+ exports.DEFAULT_SERVE_PORT = exports.DEFAULT_SERVE_HOST = exports.RAGBOX_CONFIG_FILE = void 0;
7
+ exports.parseRagboxServePort = parseRagboxServePort;
8
+ exports.resolveRagboxServeConfig = resolveRagboxServeConfig;
7
9
  exports.createDefaultRagboxConfig = createDefaultRagboxConfig;
8
10
  exports.listRagboxConfigSourceNames = listRagboxConfigSourceNames;
9
11
  exports.writeDefaultRagboxConfig = writeDefaultRagboxConfig;
@@ -13,8 +15,14 @@ exports.resolveRagboxConfig = resolveRagboxConfig;
13
15
  const promises_1 = __importDefault(require("node:fs/promises"));
14
16
  const node_path_1 = __importDefault(require("node:path"));
15
17
  exports.RAGBOX_CONFIG_FILE = "ragbox.config.json";
18
+ exports.DEFAULT_SERVE_HOST = "127.0.0.1";
19
+ exports.DEFAULT_SERVE_PORT = 8787;
16
20
  const DEFAULT_INCLUDE = ["**/*.md", "**/*.mdx"];
17
21
  const DEFAULT_EXCLUDE = ["node_modules/**", ".git/**", ".pageindex/**", "dist/**", "build/**"];
22
+ const DEFAULT_API_KEY_PLACEHOLDER = "YOUR_OPENAI_API_KEY";
23
+ const DEFAULT_SERVE_TOKEN_PLACEHOLDER = "YOUR_RAGBOX_SERVE_TOKEN";
24
+ const API_KEY_PLACEHOLDERS = new Set([DEFAULT_API_KEY_PLACEHOLDER, "sk-..."]);
25
+ const SERVE_TOKEN_PLACEHOLDERS = new Set([DEFAULT_SERVE_TOKEN_PLACEHOLDER, "<token>"]);
18
26
  async function pathExists(filePath) {
19
27
  try {
20
28
  await promises_1.default.access(filePath);
@@ -94,12 +102,14 @@ function pageIndexConfigToOptions(configDir, config) {
94
102
  concurrency: config.concurrency,
95
103
  extraArgs: config.extraArgs,
96
104
  outputArg: config.outputArg,
105
+ pageIndexRunner: config.runner,
97
106
  pythonPath: resolveConfigCommandPath(configDir, config.python)
98
107
  };
99
108
  }
100
109
  function llmConfigToOptions(config) {
110
+ const apiKey = config.apiKey?.trim();
101
111
  return {
102
- apiKey: config.apiKey,
112
+ apiKey: apiKey && !API_KEY_PLACEHOLDERS.has(apiKey) ? config.apiKey : undefined,
103
113
  baseUrl: config.baseUrl,
104
114
  model: config.model
105
115
  };
@@ -111,17 +121,52 @@ function indexConfigToOptions(configDir, config) {
111
121
  outputDir: resolveConfigRelativePath(configDir, config.outputDir)
112
122
  };
113
123
  }
124
+ function parseRagboxServePort(value, source) {
125
+ if (value === undefined) {
126
+ return undefined;
127
+ }
128
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number.parseInt(value, 10) : NaN;
129
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
130
+ throw new Error(`Invalid ${source}: ${String(value)}`);
131
+ }
132
+ return parsed;
133
+ }
134
+ function resolveRagboxServeToken(value) {
135
+ const authToken = value?.trim();
136
+ return authToken && !SERVE_TOKEN_PLACEHOLDERS.has(authToken) ? authToken : undefined;
137
+ }
138
+ function resolveRagboxServeConfig(options = {}) {
139
+ const env = options.env ?? process.env;
140
+ return {
141
+ authToken: resolveRagboxServeToken(options.authToken)
142
+ ?? resolveRagboxServeToken(options.config?.serve?.authToken)
143
+ ?? resolveRagboxServeToken(env.RAGBOX_SERVE_TOKEN),
144
+ host: options.host ?? options.config?.serve?.host ?? env.RAGBOX_SERVE_HOST ?? exports.DEFAULT_SERVE_HOST,
145
+ port: options.port
146
+ ?? parseRagboxServePort(options.config?.serve?.port, "ragbox config serve.port")
147
+ ?? parseRagboxServePort(env.RAGBOX_SERVE_PORT, "RAGBOX_SERVE_PORT")
148
+ ?? exports.DEFAULT_SERVE_PORT
149
+ };
150
+ }
114
151
  function createDefaultRagboxConfig(options = {}) {
115
152
  const docsDir = options.docsDir ?? "./docs";
116
153
  const outputDir = options.outputDir ?? "./.ragbox-index";
117
154
  return {
118
155
  version: 1,
119
156
  pageIndex: {
120
- cli: "/path/to/PageIndex/run_pageindex.py"
157
+ cli: "/path/to/PageIndex/run_pageindex.py",
158
+ concurrency: 1,
159
+ runner: "auto"
121
160
  },
122
161
  llm: {
123
162
  baseUrl: "https://api.openai.com/v1",
124
- model: "gpt-4o-mini"
163
+ model: "gpt-4o-mini",
164
+ apiKey: DEFAULT_API_KEY_PLACEHOLDER
165
+ },
166
+ serve: {
167
+ authToken: DEFAULT_SERVE_TOKEN_PLACEHOLDER,
168
+ host: exports.DEFAULT_SERVE_HOST,
169
+ port: exports.DEFAULT_SERVE_PORT
125
170
  },
126
171
  docs: {
127
172
  rootDir: docsDir,