@gmickel/gno 1.39.0 → 1.39.2

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.
@@ -0,0 +1 @@
1
+ 82afff37d4a331933c19bebb9c73972fd6eb665ffd1444bbdf65cfb0220dd692 gno-browser-clipper-v1.39.2.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "1.39.0"
24
+ "version": "1.39.2"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.39.0",
3
+ "version": "1.39.2",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
package/spec/cli.md CHANGED
@@ -1194,34 +1194,36 @@ BM25 keyword search over indexed documents.
1194
1194
  **Synopsis:**
1195
1195
 
1196
1196
  ```bash
1197
- gno search <query> [-n <num>] [--min-score <num>] [-c <collection>] [--since <date>] [--until <date>] [--category <values>] [--author <text>] [--intent <text>] [--exclude <values>] [--tags-all <tags>] [--tags-any <tags>] [--full] [--line-numbers] [--lang <bcp47>] [--json|--files|--csv|--md|--xml]
1197
+ gno search [query] [--query-file <path>] [-n <num>] [--min-score <num>] [-c <collection>] [--since <date>] [--until <date>] [--category <values>] [--author <text>] [--intent <text>] [--exclude <values>] [--tags-all <tags>] [--tags-any <tags>] [--full] [--line-numbers] [--lang <bcp47>] [--json|--files|--csv|--md|--xml]
1198
1198
  ```
1199
1199
 
1200
1200
  **Arguments:**
1201
- | Arg | Type | Description |
1202
- |-----|------|-------------|
1203
- | `<query>` | string | Search query |
1201
+
1202
+ | Arg | Type | Description |
1203
+ | --------- | ------ | -------------------------------------------------------------------- |
1204
+ | `[query]` | string | Search query. Optional when `--query-file` is set. Do not pass both. |
1204
1205
 
1205
1206
  **Options:**
1206
1207
 
1207
- | Option | Type | Default | Description |
1208
- | ----------------------- | -------- | ------------------------- | --------------------------------------------------------------------------------------------- |
1209
- | `-n` | integer | 5 (20 for --json/--files) | Max results |
1210
- | `--min-score` | number | 0 | Minimum score threshold |
1211
- | `-c, --collection` | string | all | Filter to collection |
1212
- | `--since` | string | none | Modified-at lower bound (ISO date/time or relative token) |
1213
- | `--until` | string | none | Modified-at upper bound (ISO date/time or relative token) |
1214
- | `--category` | string | none | Filter to docs with matching category/content type (comma-separated) |
1215
- | `--author` | string | none | Filter to docs where author contains value (case-insensitive) |
1216
- | `--intent` | string | none | Disambiguating context for ambiguous queries; steers snippets without being searched directly |
1217
- | `--exclude` | string | none | Hard-prune docs containing any comma-separated term in title/path/body |
1218
- | `--tags-all` | string | none | Filter to docs with ALL tags (comma-separated) |
1219
- | `--tags-any` | string | none | Filter to docs with ANY tag (comma-separated) |
1220
- | `--project-root` | string[] | cwd | Trusted project root; repeatable, replaces default cwd/repository affinity |
1221
- | `--no-project-affinity` | boolean | false | Disable project-aware soft ranking; invalid with `--project-root` |
1222
- | `--full` | boolean | false | Include full mirror content instead of snippet |
1223
- | `--line-numbers` | boolean | false | Include line numbers in output |
1224
- | `--lang` | string | auto | Language filter/hint (BCP-47) |
1208
+ | Option | Type | Default | Description |
1209
+ | ----------------------- | -------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
1210
+ | `--query-file` | string | none | Read the query from a file; `-` reads stdin so the query never appears on argv. Do not also pass a positional query. |
1211
+ | `-n` | integer | 5 (20 for --json/--files) | Max results |
1212
+ | `--min-score` | number | 0 | Minimum score threshold |
1213
+ | `-c, --collection` | string | all | Filter to collection |
1214
+ | `--since` | string | none | Modified-at lower bound (ISO date/time or relative token) |
1215
+ | `--until` | string | none | Modified-at upper bound (ISO date/time or relative token) |
1216
+ | `--category` | string | none | Filter to docs with matching category/content type (comma-separated) |
1217
+ | `--author` | string | none | Filter to docs where author contains value (case-insensitive) |
1218
+ | `--intent` | string | none | Disambiguating context for ambiguous queries; steers snippets without being searched directly |
1219
+ | `--exclude` | string | none | Hard-prune docs containing any comma-separated term in title/path/body |
1220
+ | `--tags-all` | string | none | Filter to docs with ALL tags (comma-separated) |
1221
+ | `--tags-any` | string | none | Filter to docs with ANY tag (comma-separated) |
1222
+ | `--project-root` | string[] | cwd | Trusted project root; repeatable, replaces default cwd/repository affinity |
1223
+ | `--no-project-affinity` | boolean | false | Disable project-aware soft ranking; invalid with `--project-root` |
1224
+ | `--full` | boolean | false | Include full mirror content instead of snippet |
1225
+ | `--line-numbers` | boolean | false | Include line numbers in output |
1226
+ | `--lang` | string | auto | Language filter/hint (BCP-47) |
1225
1227
 
1226
1228
  **Scoring:**
1227
1229
 
@@ -1336,15 +1338,18 @@ Hybrid search combining BM25 and vector retrieval with optional expansion and re
1336
1338
  **Synopsis:**
1337
1339
 
1338
1340
  ```bash
1339
- gno query <query> [-n <num>] [--min-score <num>] [-c <collection>] [--since <date>] [--until <date>] [--category <values>] [--author <text>] [--intent <text>] [--exclude <values>] [-C <num>] [--tags-all <tags>] [--tags-any <tags>] [--full] [--line-numbers] [--lang <bcp47>] [--no-expand] [--no-rerank] [--graph] [--no-graph] [--query-mode <mode:text>]... [--explain] [--json|--files|--csv|--md|--xml]
1341
+ gno query [query...] [--query-file <path>] [-n <num>] [--min-score <num>] [-c <collection>] [--since <date>] [--until <date>] [--category <values>] [--author <text>] [--intent <text>] [--exclude <values>] [-C <num>] [--tags-all <tags>] [--tags-any <tags>] [--full] [--line-numbers] [--lang <bcp47>] [--no-expand] [--no-rerank] [--graph] [--no-graph] [--query-mode <mode:text>]... [--explain] [--json|--files|--csv|--md|--xml]
1340
1342
  gno query diagnose <query> --target <doc> [-n <num>] [--min-score <num>] [-c <collection>] [--since <date>] [--until <date>] [--category <values>] [--author <text>] [--intent <text>] [--exclude <values>] [-C <num>] [--tags-all <tags>] [--tags-any <tags>] [--lang <bcp47>] [--no-expand] [--no-rerank] [--graph] [--no-graph] [--json]
1341
1343
  ```
1342
1344
 
1345
+ The positional query is optional when `--query-file` is set. Do not pass both. `--query-file` is invalid on `query diagnose`.
1346
+
1343
1347
  **Options:** Same as `gno search`, plus:
1344
1348
 
1345
1349
  **Additional Options:**
1346
1350
  | Option | Type | Description |
1347
1351
  |--------|------|-------------|
1352
+ | `--query-file` | string | Same as `gno search`: read the query from a file (`-` is stdin). Invalid with a positional query and invalid on `query diagnose`. |
1348
1353
  | `--no-expand` | boolean | Disable query expansion |
1349
1354
  | `--no-rerank` | boolean | Disable cross-encoder reranking |
1350
1355
  | `--graph` | boolean | Explicitly enable the default bounded one-hop graph neighbor expansion |
@@ -1357,7 +1362,7 @@ gno query diagnose <query> --target <doc> [-n <num>] [--min-score <num>] [-c <co
1357
1362
  | `--target` | ref | Required for `query diagnose`; target document to diagnose |
1358
1363
 
1359
1364
  `query diagnose` accepts the same `--project-root` and
1360
- `--no-project-affinity` controls as `query`.
1365
+ `--no-project-affinity` controls as `query`. It rejects `--query-file`.
1361
1366
 
1362
1367
  **Compatibility / Migration:**
1363
1368
 
@@ -46,6 +46,7 @@ import {
46
46
  parseOptionalFloat,
47
47
  parsePositiveInt,
48
48
  } from "./options";
49
+ import { resolveCliQueryText } from "./query-text";
49
50
 
50
51
  // ─────────────────────────────────────────────────────────────────────────────
51
52
  // Global State (set by preAction hook)
@@ -625,8 +626,12 @@ function wireTraceCommands(program: Command): void {
625
626
  function wireSearchCommands(program: Command): void {
626
627
  // search - BM25 keyword search
627
628
  program
628
- .command("search <query>")
629
+ .command("search [query]")
629
630
  .description("BM25 keyword search")
631
+ .option(
632
+ "--query-file <path>",
633
+ "read query from file; use - for stdin (keeps the query off argv)"
634
+ )
630
635
  .option("-n, --limit <num>", "max results")
631
636
  .option("--min-score <num>", "minimum score threshold")
632
637
  .option("-c, --collection <name>", "filter by collection")
@@ -662,95 +667,104 @@ function wireSearchCommands(program: Command): void {
662
667
  .option("--csv", "CSV output")
663
668
  .option("--xml", "XML output")
664
669
  .option("--files", "file paths only")
665
- .action(async (queryText: string, cmdOpts: Record<string, unknown>) => {
666
- const format = getFormat(cmdOpts);
667
- assertFormatSupported(CMD.search, format);
668
- const globals = getGlobals();
669
-
670
- // Validate empty query
671
- if (!queryText.trim()) {
672
- throw new CliError("VALIDATION", "Query cannot be empty");
673
- }
670
+ .action(
671
+ async (
672
+ queryText: string | undefined,
673
+ cmdOpts: Record<string, unknown>
674
+ ) => {
675
+ const format = getFormat(cmdOpts);
676
+ assertFormatSupported(CMD.search, format);
677
+ const globals = getGlobals();
678
+ queryText = await resolveCliQueryText(queryText, cmdOpts.queryFile);
674
679
 
675
- // Validate minScore range
676
- const minScore = parseOptionalFloat("min-score", cmdOpts.minScore);
677
- if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
678
- throw new CliError("VALIDATION", "--min-score must be between 0 and 1");
679
- }
680
+ // Validate empty query
681
+ if (!queryText.trim()) {
682
+ throw new CliError("VALIDATION", "Query cannot be empty");
683
+ }
680
684
 
681
- // Parse and validate tag filters
682
- let tagsAll: string[] | undefined;
683
- let tagsAny: string[] | undefined;
684
- try {
685
- tagsAll = cmdOpts.tagsAll
686
- ? parseAndValidateTagFilter(cmdOpts.tagsAll as string)
687
- : undefined;
688
- tagsAny = cmdOpts.tagsAny
689
- ? parseAndValidateTagFilter(cmdOpts.tagsAny as string)
690
- : undefined;
691
- } catch (error) {
692
- throw new CliError(
693
- "VALIDATION",
694
- error instanceof Error ? error.message : "Invalid tag filter"
695
- );
696
- }
685
+ // Validate minScore range
686
+ const minScore = parseOptionalFloat("min-score", cmdOpts.minScore);
687
+ if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
688
+ throw new CliError(
689
+ "VALIDATION",
690
+ "--min-score must be between 0 and 1"
691
+ );
692
+ }
697
693
 
698
- const limit = cmdOpts.limit
699
- ? parsePositiveInt("limit", cmdOpts.limit)
700
- : cmdOpts.verify
701
- ? 5
702
- : getDefaultLimit(format);
703
- const categories = parseCsvValues(cmdOpts.category);
704
- const exclude = parseCsvValues(cmdOpts.exclude);
705
- const projectAffinity = parseCliProjectAffinityOptions(cmdOpts);
694
+ // Parse and validate tag filters
695
+ let tagsAll: string[] | undefined;
696
+ let tagsAny: string[] | undefined;
697
+ try {
698
+ tagsAll = cmdOpts.tagsAll
699
+ ? parseAndValidateTagFilter(cmdOpts.tagsAll as string)
700
+ : undefined;
701
+ tagsAny = cmdOpts.tagsAny
702
+ ? parseAndValidateTagFilter(cmdOpts.tagsAny as string)
703
+ : undefined;
704
+ } catch (error) {
705
+ throw new CliError(
706
+ "VALIDATION",
707
+ error instanceof Error ? error.message : "Invalid tag filter"
708
+ );
709
+ }
706
710
 
707
- const { search, formatSearch } = await import("./commands/search");
708
- const result = await search(queryText, {
709
- configPath: globals.config,
710
- indexName: globals.index,
711
- limit,
712
- minScore,
713
- collection: cmdOpts.collection as string | undefined,
714
- lang: cmdOpts.lang as string | undefined,
715
- since: cmdOpts.since as string | undefined,
716
- until: cmdOpts.until as string | undefined,
717
- categories,
718
- author: cmdOpts.author as string | undefined,
719
- intent: cmdOpts.intent as string | undefined,
720
- exclude,
721
- tagsAll,
722
- tagsAny,
723
- ...projectAffinity,
724
- full: Boolean(cmdOpts.full),
725
- lineNumbers: Boolean(cmdOpts.lineNumbers),
726
- json: format === "json",
727
- md: format === "md",
728
- csv: format === "csv",
729
- xml: format === "xml",
730
- files: format === "files",
731
- });
711
+ const limit = cmdOpts.limit
712
+ ? parsePositiveInt("limit", cmdOpts.limit)
713
+ : cmdOpts.verify
714
+ ? 5
715
+ : getDefaultLimit(format);
716
+ const categories = parseCsvValues(cmdOpts.category);
717
+ const exclude = parseCsvValues(cmdOpts.exclude);
718
+ const projectAffinity = parseCliProjectAffinityOptions(cmdOpts);
719
+
720
+ const { search, formatSearch } = await import("./commands/search");
721
+ const result = await search(queryText, {
722
+ configPath: globals.config,
723
+ indexName: globals.index,
724
+ limit,
725
+ minScore,
726
+ collection: cmdOpts.collection as string | undefined,
727
+ lang: cmdOpts.lang as string | undefined,
728
+ since: cmdOpts.since as string | undefined,
729
+ until: cmdOpts.until as string | undefined,
730
+ categories,
731
+ author: cmdOpts.author as string | undefined,
732
+ intent: cmdOpts.intent as string | undefined,
733
+ exclude,
734
+ tagsAll,
735
+ tagsAny,
736
+ ...projectAffinity,
737
+ full: Boolean(cmdOpts.full),
738
+ lineNumbers: Boolean(cmdOpts.lineNumbers),
739
+ json: format === "json",
740
+ md: format === "md",
741
+ csv: format === "csv",
742
+ xml: format === "xml",
743
+ files: format === "files",
744
+ });
732
745
 
733
- // Check success before printing - stdout is for successful outputs only
734
- if (!result.success) {
735
- // Map validation errors to exit code 1
736
- throw new CliError(
737
- result.isValidation ? "VALIDATION" : "RUNTIME",
738
- result.error
739
- );
746
+ // Check success before printing - stdout is for successful outputs only
747
+ if (!result.success) {
748
+ // Map validation errors to exit code 1
749
+ throw new CliError(
750
+ result.isValidation ? "VALIDATION" : "RUNTIME",
751
+ result.error
752
+ );
753
+ }
754
+ const output = formatSearch(result, {
755
+ json: format === "json",
756
+ md: format === "md",
757
+ csv: format === "csv",
758
+ xml: format === "xml",
759
+ files: format === "files",
760
+ full: Boolean(cmdOpts.full),
761
+ lineNumbers: Boolean(cmdOpts.lineNumbers),
762
+ terminalLinks: await resolveTerminalLinkPolicy(format),
763
+ });
764
+ await writeOutput(output, format);
765
+ writeRetrievalTraceReceipt(result.metadata);
740
766
  }
741
- const output = formatSearch(result, {
742
- json: format === "json",
743
- md: format === "md",
744
- csv: format === "csv",
745
- xml: format === "xml",
746
- files: format === "files",
747
- full: Boolean(cmdOpts.full),
748
- lineNumbers: Boolean(cmdOpts.lineNumbers),
749
- terminalLinks: await resolveTerminalLinkPolicy(format),
750
- });
751
- await writeOutput(output, format);
752
- writeRetrievalTraceReceipt(result.metadata);
753
- });
767
+ );
754
768
 
755
769
  // vsearch - Vector similarity search
756
770
  program
@@ -876,8 +890,12 @@ function wireSearchCommands(program: Command): void {
876
890
 
877
891
  // query - Hybrid search with expansion and reranking
878
892
  program
879
- .command("query <query...>")
893
+ .command("query [query...]")
880
894
  .description("Hybrid search with expansion and reranking")
895
+ .option(
896
+ "--query-file <path>",
897
+ "read query from file; use - for stdin (keeps the query off argv)"
898
+ )
881
899
  .option("-n, --limit <num>", "max results")
882
900
  .option("--min-score <num>", "minimum score threshold")
883
901
  .option("-c, --collection <name>", "filter by collection")
@@ -931,103 +949,158 @@ function wireSearchCommands(program: Command): void {
931
949
  .option("--csv", "CSV output")
932
950
  .option("--xml", "XML output")
933
951
  .option("--files", "file paths only")
934
- .action(async (queryParts: string[], cmdOpts: Record<string, unknown>) => {
935
- const format = getFormat(cmdOpts);
936
- const isDiagnose =
937
- queryParts[0] === "diagnose" && Boolean(cmdOpts.target);
938
- assertFormatSupported(isDiagnose ? CMD.queryDiagnose : CMD.query, format);
939
- const diagnoseFormat = format === "json" ? "json" : "terminal";
940
- const globals = getGlobals();
941
- let queryText = isDiagnose
942
- ? queryParts.slice(1).join(" ")
943
- : queryParts.join(" ");
952
+ .action(
953
+ async (
954
+ queryParts: string[] | undefined,
955
+ cmdOpts: Record<string, unknown>
956
+ ) => {
957
+ const parts = queryParts ?? [];
958
+ const format = getFormat(cmdOpts);
959
+ const isDiagnose = parts[0] === "diagnose" && Boolean(cmdOpts.target);
960
+ assertFormatSupported(
961
+ isDiagnose ? CMD.queryDiagnose : CMD.query,
962
+ format
963
+ );
964
+ const diagnoseFormat = format === "json" ? "json" : "terminal";
965
+ const globals = getGlobals();
966
+ if (isDiagnose && cmdOpts.queryFile) {
967
+ throw new CliError(
968
+ "VALIDATION",
969
+ "query diagnose does not accept --query-file"
970
+ );
971
+ }
972
+ let queryText = isDiagnose
973
+ ? parts.slice(1).join(" ")
974
+ : await resolveCliQueryText(parts.join(" "), cmdOpts.queryFile);
944
975
 
945
- // Validate empty query
946
- if (!queryText.trim()) {
947
- throw new CliError("VALIDATION", "Query cannot be empty");
948
- }
976
+ // Validate empty query
977
+ if (!queryText.trim()) {
978
+ throw new CliError("VALIDATION", "Query cannot be empty");
979
+ }
949
980
 
950
- // Validate minScore range
951
- const minScore = parseOptionalFloat("min-score", cmdOpts.minScore);
952
- if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
953
- throw new CliError("VALIDATION", "--min-score must be between 0 and 1");
954
- }
981
+ // Validate minScore range
982
+ const minScore = parseOptionalFloat("min-score", cmdOpts.minScore);
983
+ if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
984
+ throw new CliError(
985
+ "VALIDATION",
986
+ "--min-score must be between 0 and 1"
987
+ );
988
+ }
955
989
 
956
- // Parse optional structured query modes
957
- let queryModes: import("../pipeline/types").QueryModeInput[] | undefined;
958
- if (Array.isArray(cmdOpts.queryMode) && cmdOpts.queryMode.length > 0) {
959
- const { parseQueryModeSpecs } = await import("../pipeline/query-modes");
960
- const parsed = parseQueryModeSpecs(cmdOpts.queryMode as string[]);
961
- if (!parsed.ok) {
962
- throw new CliError("VALIDATION", parsed.error.message);
990
+ // Parse optional structured query modes
991
+ let queryModes:
992
+ | import("../pipeline/types").QueryModeInput[]
993
+ | undefined;
994
+ if (Array.isArray(cmdOpts.queryMode) && cmdOpts.queryMode.length > 0) {
995
+ const { parseQueryModeSpecs } =
996
+ await import("../pipeline/query-modes");
997
+ const parsed = parseQueryModeSpecs(cmdOpts.queryMode as string[]);
998
+ if (!parsed.ok) {
999
+ throw new CliError("VALIDATION", parsed.error.message);
1000
+ }
1001
+ queryModes = parsed.value;
963
1002
  }
964
- queryModes = parsed.value;
965
- }
966
1003
 
967
- const { normalizeStructuredQueryInput } =
968
- await import("../core/structured-query");
969
- const normalizedInput = normalizeStructuredQueryInput(
970
- queryText,
971
- queryModes ?? []
972
- );
973
- if (!normalizedInput.ok) {
974
- throw new CliError("VALIDATION", normalizedInput.error.message);
975
- }
976
- queryText = normalizedInput.value.query;
977
- queryModes =
978
- normalizedInput.value.queryModes.length > 0
979
- ? normalizedInput.value.queryModes
980
- : undefined;
1004
+ const { normalizeStructuredQueryInput } =
1005
+ await import("../core/structured-query");
1006
+ const normalizedInput = normalizeStructuredQueryInput(
1007
+ queryText,
1008
+ queryModes ?? []
1009
+ );
1010
+ if (!normalizedInput.ok) {
1011
+ throw new CliError("VALIDATION", normalizedInput.error.message);
1012
+ }
1013
+ queryText = normalizedInput.value.query;
1014
+ queryModes =
1015
+ normalizedInput.value.queryModes.length > 0
1016
+ ? normalizedInput.value.queryModes
1017
+ : undefined;
981
1018
 
982
- // Parse and validate tag filters
983
- let tagsAll: string[] | undefined;
984
- let tagsAny: string[] | undefined;
985
- try {
986
- tagsAll = cmdOpts.tagsAll
987
- ? parseAndValidateTagFilter(cmdOpts.tagsAll as string)
988
- : undefined;
989
- tagsAny = cmdOpts.tagsAny
990
- ? parseAndValidateTagFilter(cmdOpts.tagsAny as string)
1019
+ // Parse and validate tag filters
1020
+ let tagsAll: string[] | undefined;
1021
+ let tagsAny: string[] | undefined;
1022
+ try {
1023
+ tagsAll = cmdOpts.tagsAll
1024
+ ? parseAndValidateTagFilter(cmdOpts.tagsAll as string)
1025
+ : undefined;
1026
+ tagsAny = cmdOpts.tagsAny
1027
+ ? parseAndValidateTagFilter(cmdOpts.tagsAny as string)
1028
+ : undefined;
1029
+ } catch (error) {
1030
+ throw new CliError(
1031
+ "VALIDATION",
1032
+ error instanceof Error ? error.message : "Invalid tag filter"
1033
+ );
1034
+ }
1035
+
1036
+ const limit = cmdOpts.limit
1037
+ ? parsePositiveInt("limit", cmdOpts.limit)
1038
+ : getDefaultLimit(format);
1039
+ const { loadConfig } = await import("../config");
1040
+ const { getActivePreset } = await import("../llm/registry");
1041
+ const configResult = await loadConfig(globals.config);
1042
+ const activePresetId = configResult.ok
1043
+ ? getActivePreset(configResult.value).id
1044
+ : "slim-tuned";
1045
+ const candidateLimit = cmdOpts.candidateLimit
1046
+ ? parsePositiveInt("candidate-limit", cmdOpts.candidateLimit)
991
1047
  : undefined;
992
- } catch (error) {
993
- throw new CliError(
994
- "VALIDATION",
995
- error instanceof Error ? error.message : "Invalid tag filter"
996
- );
997
- }
1048
+ const categories = parseCsvValues(cmdOpts.category);
1049
+ const exclude = parseCsvValues(cmdOpts.exclude);
1050
+ const projectAffinity = parseCliProjectAffinityOptions(cmdOpts);
1051
+
1052
+ const depthPolicy = resolveDepthPolicy({
1053
+ presetId: activePresetId,
1054
+ fast: Boolean(cmdOpts.fast),
1055
+ thorough: Boolean(cmdOpts.thorough),
1056
+ expand: cmdOpts.expand === false ? false : undefined,
1057
+ rerank: cmdOpts.rerank === false ? false : undefined,
1058
+ candidateLimit,
1059
+ });
998
1060
 
999
- const limit = cmdOpts.limit
1000
- ? parsePositiveInt("limit", cmdOpts.limit)
1001
- : getDefaultLimit(format);
1002
- const { loadConfig } = await import("../config");
1003
- const { getActivePreset } = await import("../llm/registry");
1004
- const configResult = await loadConfig(globals.config);
1005
- const activePresetId = configResult.ok
1006
- ? getActivePreset(configResult.value).id
1007
- : "slim-tuned";
1008
- const candidateLimit = cmdOpts.candidateLimit
1009
- ? parsePositiveInt("candidate-limit", cmdOpts.candidateLimit)
1010
- : undefined;
1011
- const categories = parseCsvValues(cmdOpts.category);
1012
- const exclude = parseCsvValues(cmdOpts.exclude);
1013
- const projectAffinity = parseCliProjectAffinityOptions(cmdOpts);
1061
+ if (isDiagnose) {
1062
+ const { queryDiagnose, formatQueryDiagnose } =
1063
+ await import("./commands/query");
1064
+ const result = await queryDiagnose(queryText, {
1065
+ configPath: globals.config,
1066
+ indexName: globals.index,
1067
+ target: cmdOpts.target as string,
1068
+ limit,
1069
+ minScore,
1070
+ collection: cmdOpts.collection as string | undefined,
1071
+ lang: cmdOpts.lang as string | undefined,
1072
+ since: cmdOpts.since as string | undefined,
1073
+ until: cmdOpts.until as string | undefined,
1074
+ categories,
1075
+ author: cmdOpts.author as string | undefined,
1076
+ intent: cmdOpts.intent as string | undefined,
1077
+ exclude,
1078
+ tagsAll,
1079
+ tagsAny,
1080
+ ...projectAffinity,
1081
+ noExpand: depthPolicy.noExpand,
1082
+ noRerank: depthPolicy.noRerank,
1083
+ graph: cmdOpts.graph !== false,
1084
+ noGraph: Boolean(cmdOpts.fast) || cmdOpts.graph === false,
1085
+ candidateLimit: depthPolicy.candidateLimit,
1086
+ queryModes,
1087
+ json: diagnoseFormat === "json",
1088
+ });
1014
1089
 
1015
- const depthPolicy = resolveDepthPolicy({
1016
- presetId: activePresetId,
1017
- fast: Boolean(cmdOpts.fast),
1018
- thorough: Boolean(cmdOpts.thorough),
1019
- expand: cmdOpts.expand === false ? false : undefined,
1020
- rerank: cmdOpts.rerank === false ? false : undefined,
1021
- candidateLimit,
1022
- });
1090
+ if (!result.success) {
1091
+ throw new CliError("RUNTIME", result.error);
1092
+ }
1093
+ await writeOutput(
1094
+ formatQueryDiagnose(result, { format: diagnoseFormat }),
1095
+ diagnoseFormat
1096
+ );
1097
+ return;
1098
+ }
1023
1099
 
1024
- if (isDiagnose) {
1025
- const { queryDiagnose, formatQueryDiagnose } =
1026
- await import("./commands/query");
1027
- const result = await queryDiagnose(queryText, {
1100
+ const { query, formatQuery } = await import("./commands/query");
1101
+ const result = await query(queryText, {
1028
1102
  configPath: globals.config,
1029
1103
  indexName: globals.index,
1030
- target: cmdOpts.target as string,
1031
1104
  limit,
1032
1105
  minScore,
1033
1106
  collection: cmdOpts.collection as string | undefined,
@@ -1041,70 +1114,35 @@ function wireSearchCommands(program: Command): void {
1041
1114
  tagsAll,
1042
1115
  tagsAny,
1043
1116
  ...projectAffinity,
1117
+ full: Boolean(cmdOpts.full),
1118
+ lineNumbers: Boolean(cmdOpts.lineNumbers),
1044
1119
  noExpand: depthPolicy.noExpand,
1045
1120
  noRerank: depthPolicy.noRerank,
1046
1121
  graph: cmdOpts.graph !== false,
1047
1122
  noGraph: Boolean(cmdOpts.fast) || cmdOpts.graph === false,
1048
1123
  candidateLimit: depthPolicy.candidateLimit,
1049
1124
  queryModes,
1050
- json: diagnoseFormat === "json",
1125
+ explain: Boolean(cmdOpts.explain),
1126
+ json: format === "json",
1127
+ md: format === "md",
1128
+ csv: format === "csv",
1129
+ xml: format === "xml",
1130
+ files: format === "files",
1051
1131
  });
1052
1132
 
1053
1133
  if (!result.success) {
1054
1134
  throw new CliError("RUNTIME", result.error);
1055
1135
  }
1056
- await writeOutput(
1057
- formatQueryDiagnose(result, { format: diagnoseFormat }),
1058
- diagnoseFormat
1059
- );
1060
- return;
1061
- }
1062
-
1063
- const { query, formatQuery } = await import("./commands/query");
1064
- const result = await query(queryText, {
1065
- configPath: globals.config,
1066
- indexName: globals.index,
1067
- limit,
1068
- minScore,
1069
- collection: cmdOpts.collection as string | undefined,
1070
- lang: cmdOpts.lang as string | undefined,
1071
- since: cmdOpts.since as string | undefined,
1072
- until: cmdOpts.until as string | undefined,
1073
- categories,
1074
- author: cmdOpts.author as string | undefined,
1075
- intent: cmdOpts.intent as string | undefined,
1076
- exclude,
1077
- tagsAll,
1078
- tagsAny,
1079
- ...projectAffinity,
1080
- full: Boolean(cmdOpts.full),
1081
- lineNumbers: Boolean(cmdOpts.lineNumbers),
1082
- noExpand: depthPolicy.noExpand,
1083
- noRerank: depthPolicy.noRerank,
1084
- graph: cmdOpts.graph !== false,
1085
- noGraph: Boolean(cmdOpts.fast) || cmdOpts.graph === false,
1086
- candidateLimit: depthPolicy.candidateLimit,
1087
- queryModes,
1088
- explain: Boolean(cmdOpts.explain),
1089
- json: format === "json",
1090
- md: format === "md",
1091
- csv: format === "csv",
1092
- xml: format === "xml",
1093
- files: format === "files",
1094
- });
1095
-
1096
- if (!result.success) {
1097
- throw new CliError("RUNTIME", result.error);
1136
+ const output = formatQuery(result, {
1137
+ format,
1138
+ full: Boolean(cmdOpts.full),
1139
+ lineNumbers: Boolean(cmdOpts.lineNumbers),
1140
+ terminalLinks: await resolveTerminalLinkPolicy(format),
1141
+ });
1142
+ await writeOutput(output, format);
1143
+ writeRetrievalTraceReceipt(result.metadata);
1098
1144
  }
1099
- const output = formatQuery(result, {
1100
- format,
1101
- full: Boolean(cmdOpts.full),
1102
- lineNumbers: Boolean(cmdOpts.lineNumbers),
1103
- terminalLinks: await resolveTerminalLinkPolicy(format),
1104
- });
1105
- await writeOutput(output, format);
1106
- writeRetrievalTraceReceipt(result.metadata);
1107
- });
1145
+ );
1108
1146
 
1109
1147
  // bench - Retrieval benchmark fixture runner
1110
1148
  program
@@ -3201,7 +3239,9 @@ function wireSkillCommands(program: Command): void {
3201
3239
  | "hermes"
3202
3240
  | "all",
3203
3241
  force: Boolean(cmdOpts.force),
3204
- json: Boolean(cmdOpts.json),
3242
+ // The root program also defines a global --json and may consume the
3243
+ // flag; leave undefined here so the runner falls back to globals.
3244
+ json: cmdOpts.json === true ? true : undefined,
3205
3245
  });
3206
3246
  });
3207
3247
 
@@ -3252,7 +3292,7 @@ function wireSkillCommands(program: Command): void {
3252
3292
  | "openclaw"
3253
3293
  | "hermes"
3254
3294
  | "all",
3255
- json: Boolean(cmdOpts.json),
3295
+ json: cmdOpts.json === true ? true : undefined,
3256
3296
  });
3257
3297
  });
3258
3298
 
@@ -3316,7 +3356,7 @@ function wireSkillCommands(program: Command): void {
3316
3356
  | "openclaw"
3317
3357
  | "hermes"
3318
3358
  | "all",
3319
- json: Boolean(cmdOpts.json),
3359
+ json: cmdOpts.json === true ? true : undefined,
3320
3360
  });
3321
3361
  });
3322
3362
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resolve a search/query string from a positional argument or --query-file.
3
+ * `--query-file -` reads stdin so callers can keep the query off argv.
4
+ *
5
+ * @module src/cli/query-text
6
+ */
7
+
8
+ import { CliError } from "./errors";
9
+
10
+ const readQueryFile = async (path: string): Promise<string> => {
11
+ if (path.includes("\0")) {
12
+ throw new CliError("VALIDATION", "--query-file path is invalid");
13
+ }
14
+ try {
15
+ const raw =
16
+ path === "-" ? await Bun.stdin.text() : await Bun.file(path).text();
17
+ return raw.replace(/\s+$/u, "");
18
+ } catch (error) {
19
+ throw new CliError(
20
+ "VALIDATION",
21
+ error instanceof Error
22
+ ? `Failed to read --query-file: ${error.message}`
23
+ : "Failed to read --query-file"
24
+ );
25
+ }
26
+ };
27
+
28
+ export const resolveCliQueryText = async (
29
+ positional: string | undefined,
30
+ queryFile: unknown
31
+ ): Promise<string> => {
32
+ const file = typeof queryFile === "string" ? queryFile : "";
33
+ const pos = positional ?? "";
34
+ if (file !== "" && pos.trim() !== "") {
35
+ throw new CliError("VALIDATION", "Pass a query or --query-file, not both");
36
+ }
37
+ if (file !== "") {
38
+ return await readQueryFile(file);
39
+ }
40
+ return pos;
41
+ };
@@ -1 +0,0 @@
1
- b3b2f2609cfe5c821eb9d22d76832b6ad329afa7496c0792e0dd4f5bd0f74fbc gno-browser-clipper-v1.39.0.zip