@gmickel/gno 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/package.json +7 -2
- package/src/cli/commands/completion/completion.ts +201 -0
- package/src/cli/commands/completion/index.ts +8 -0
- package/src/cli/commands/completion/scripts.ts +365 -0
- package/src/cli/context.ts +3 -0
- package/src/cli/pager.ts +200 -0
- package/src/cli/program.ts +125 -40
- package/src/config/types.ts +4 -4
- package/src/llm/registry.ts +1 -1
- package/src/pipeline/answer.ts +13 -10
- package/src/pipeline/search.ts +5 -1
package/README.md
CHANGED
|
@@ -295,7 +295,7 @@ graph TD
|
|
|
295
295
|
1. **Query Expansion**: LLM generates lexical variants, semantic rephrases, and a [HyDE](https://arxiv.org/abs/2212.10496) passage
|
|
296
296
|
2. **Parallel Retrieval**: Document-level BM25 + chunk-level vector search on all variants
|
|
297
297
|
3. **Fusion**: RRF with 2× weight for original query, tiered bonus for top ranks
|
|
298
|
-
4. **Reranking**: Qwen3-Reranker scores
|
|
298
|
+
4. **Reranking**: Qwen3-Reranker scores best chunk per document (4K), blended with fusion
|
|
299
299
|
|
|
300
300
|
> **Deep dive**: [How Search Works](https://gno.sh/docs/HOW-SEARCH-WORKS/)
|
|
301
301
|
|
|
@@ -332,14 +332,14 @@ Models auto-download on first use to `~/.cache/gno/models/`.
|
|
|
332
332
|
|
|
333
333
|
### Model Presets
|
|
334
334
|
|
|
335
|
-
| Preset | Disk | Best For
|
|
336
|
-
| :--------- | :----- |
|
|
337
|
-
| `slim` | ~1GB | Fast,
|
|
338
|
-
| `balanced` | ~2GB |
|
|
339
|
-
| `quality` | ~2.5GB | Best answers
|
|
335
|
+
| Preset | Disk | Best For |
|
|
336
|
+
| :--------- | :----- | :--------------------------- |
|
|
337
|
+
| `slim` | ~1GB | Fast, good quality (default) |
|
|
338
|
+
| `balanced` | ~2GB | Slightly larger model |
|
|
339
|
+
| `quality` | ~2.5GB | Best answers |
|
|
340
340
|
|
|
341
341
|
```bash
|
|
342
|
-
gno models use
|
|
342
|
+
gno models use slim
|
|
343
343
|
gno models pull --all # Optional: pre-download models (auto-downloads on first use)
|
|
344
344
|
```
|
|
345
345
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
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",
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
"test:coverage": "bun test --coverage",
|
|
49
49
|
"test:coverage:html": "bun test --coverage --html",
|
|
50
50
|
"test:fixtures": "bun scripts/generate-test-fixtures.ts",
|
|
51
|
+
"evals": "bun scripts/update-eval-scores.ts",
|
|
52
|
+
"eval": "bun --bun evalite",
|
|
53
|
+
"eval:watch": "bun --bun evalite watch",
|
|
51
54
|
"reset": "bun run src/index.ts reset --confirm",
|
|
52
55
|
"docs:verify": "bun run scripts/docs-verify.ts",
|
|
53
56
|
"website:install": "cd website && bundle install",
|
|
@@ -110,6 +113,7 @@
|
|
|
110
113
|
"zod": "^4.3.4"
|
|
111
114
|
},
|
|
112
115
|
"devDependencies": {
|
|
116
|
+
"@ai-sdk/openai": "^3.0.2",
|
|
113
117
|
"@biomejs/biome": "2.3.10",
|
|
114
118
|
"@tailwindcss/cli": "^4.1.18",
|
|
115
119
|
"@types/bun": "latest",
|
|
@@ -126,7 +130,8 @@
|
|
|
126
130
|
"oxlint-tsgolint": "^0.10.1",
|
|
127
131
|
"pdf-lib": "^1.17.1",
|
|
128
132
|
"pptxgenjs": "^4.0.1",
|
|
129
|
-
"ultracite": "7.0.4"
|
|
133
|
+
"ultracite": "7.0.4",
|
|
134
|
+
"vitest": "^4.0.16"
|
|
130
135
|
},
|
|
131
136
|
"peerDependencies": {
|
|
132
137
|
"typescript": "^5"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion commands - output scripts or auto-install.
|
|
3
|
+
*
|
|
4
|
+
* @module src/cli/commands/completion/completion
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// node:fs/promises - no Bun equivalent for mkdir/appendFile/writeFile
|
|
8
|
+
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { CLI_NAME } from "../../../app/constants.js";
|
|
13
|
+
import { CliError } from "../../errors.js";
|
|
14
|
+
import {
|
|
15
|
+
getCompletionScript,
|
|
16
|
+
SUPPORTED_SHELLS,
|
|
17
|
+
type Shell,
|
|
18
|
+
} from "./scripts.js";
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Types
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface OutputOptions {
|
|
25
|
+
shell: Shell;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InstallOptions {
|
|
29
|
+
/** Override shell detection */
|
|
30
|
+
shell?: Shell;
|
|
31
|
+
/** JSON output */
|
|
32
|
+
json?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface InstallResult {
|
|
36
|
+
shell: Shell;
|
|
37
|
+
path: string;
|
|
38
|
+
action: "installed" | "already_installed";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Shell Detection
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detect user's current shell.
|
|
47
|
+
*/
|
|
48
|
+
function detectShell(): Shell | undefined {
|
|
49
|
+
// Check $SHELL env
|
|
50
|
+
const shellEnv = process.env.SHELL || "";
|
|
51
|
+
if (shellEnv.includes("zsh")) return "zsh";
|
|
52
|
+
if (shellEnv.includes("bash")) return "bash";
|
|
53
|
+
if (shellEnv.includes("fish")) return "fish";
|
|
54
|
+
|
|
55
|
+
// Check parent process name on Unix
|
|
56
|
+
// This is less reliable but can help
|
|
57
|
+
const parentName = process.env._ || "";
|
|
58
|
+
if (parentName.includes("zsh")) return "zsh";
|
|
59
|
+
if (parentName.includes("bash")) return "bash";
|
|
60
|
+
if (parentName.includes("fish")) return "fish";
|
|
61
|
+
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the appropriate rc file for a shell.
|
|
67
|
+
*/
|
|
68
|
+
function getShellRcPath(shell: Shell): string {
|
|
69
|
+
const home = homedir();
|
|
70
|
+
switch (shell) {
|
|
71
|
+
case "bash":
|
|
72
|
+
// Prefer .bashrc, but .bash_profile on macOS
|
|
73
|
+
return process.platform === "darwin"
|
|
74
|
+
? join(home, ".bash_profile")
|
|
75
|
+
: join(home, ".bashrc");
|
|
76
|
+
case "zsh":
|
|
77
|
+
return join(home, ".zshrc");
|
|
78
|
+
case "fish":
|
|
79
|
+
return join(home, ".config", "fish", "completions", `${CLI_NAME}.fish`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if completion is already installed.
|
|
85
|
+
*/
|
|
86
|
+
async function isCompletionInstalled(shell: Shell): Promise<boolean> {
|
|
87
|
+
const path = getShellRcPath(shell);
|
|
88
|
+
|
|
89
|
+
// For fish, check if file exists
|
|
90
|
+
if (shell === "fish") {
|
|
91
|
+
return await Bun.file(path).exists();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// For bash/zsh, check for shell-specific patterns
|
|
95
|
+
try {
|
|
96
|
+
const content = await Bun.file(path).text();
|
|
97
|
+
// Generic header comment
|
|
98
|
+
const header = `# ${CLI_NAME}`;
|
|
99
|
+
// Bash function name
|
|
100
|
+
const bashFn = `_${CLI_NAME}_completions`;
|
|
101
|
+
// Zsh-specific patterns (function definition or completion header)
|
|
102
|
+
const zshFn = `_${CLI_NAME}()`;
|
|
103
|
+
|
|
104
|
+
if (shell === "bash") {
|
|
105
|
+
return content.includes(header) || content.includes(bashFn);
|
|
106
|
+
}
|
|
107
|
+
if (shell === "zsh") {
|
|
108
|
+
return content.includes(header) || content.includes(zshFn);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// Commands
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Output completion script to stdout.
|
|
122
|
+
*/
|
|
123
|
+
export function completionOutput(options: OutputOptions): string {
|
|
124
|
+
const { shell } = options;
|
|
125
|
+
|
|
126
|
+
if (!SUPPORTED_SHELLS.includes(shell)) {
|
|
127
|
+
throw new CliError(
|
|
128
|
+
"VALIDATION",
|
|
129
|
+
`Unsupported shell: ${shell}. Supported: ${SUPPORTED_SHELLS.join(", ")}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return getCompletionScript(shell);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Auto-install completion to user's shell config.
|
|
138
|
+
*/
|
|
139
|
+
export async function completionInstall(
|
|
140
|
+
options: InstallOptions
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const { json = false } = options;
|
|
143
|
+
let { shell } = options;
|
|
144
|
+
|
|
145
|
+
// Auto-detect shell if not specified
|
|
146
|
+
if (!shell) {
|
|
147
|
+
shell = detectShell();
|
|
148
|
+
if (!shell) {
|
|
149
|
+
throw new CliError(
|
|
150
|
+
"VALIDATION",
|
|
151
|
+
"Could not detect shell. Please specify: gno completion install --shell <bash|zsh|fish>"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!SUPPORTED_SHELLS.includes(shell)) {
|
|
157
|
+
throw new CliError(
|
|
158
|
+
"VALIDATION",
|
|
159
|
+
`Unsupported shell: ${shell}. Supported: ${SUPPORTED_SHELLS.join(", ")}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if already installed
|
|
164
|
+
const alreadyInstalled = await isCompletionInstalled(shell);
|
|
165
|
+
const rcPath = getShellRcPath(shell);
|
|
166
|
+
const script = getCompletionScript(shell);
|
|
167
|
+
|
|
168
|
+
let result: InstallResult;
|
|
169
|
+
|
|
170
|
+
if (alreadyInstalled) {
|
|
171
|
+
result = { shell, path: rcPath, action: "already_installed" };
|
|
172
|
+
} else {
|
|
173
|
+
// Install completion
|
|
174
|
+
if (shell === "fish") {
|
|
175
|
+
// Fish uses a separate file in completions dir
|
|
176
|
+
const dir = join(homedir(), ".config", "fish", "completions");
|
|
177
|
+
await mkdir(dir, { recursive: true });
|
|
178
|
+
await writeFile(rcPath, script, "utf-8");
|
|
179
|
+
} else {
|
|
180
|
+
// Bash/zsh append to rc file
|
|
181
|
+
const separator = "\n\n";
|
|
182
|
+
await appendFile(rcPath, separator + script, "utf-8");
|
|
183
|
+
}
|
|
184
|
+
result = { shell, path: rcPath, action: "installed" };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Output result
|
|
188
|
+
if (json) {
|
|
189
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
190
|
+
} else {
|
|
191
|
+
if (result.action === "already_installed") {
|
|
192
|
+
process.stdout.write(`Completion already installed for ${shell}.\n`);
|
|
193
|
+
process.stdout.write(`Config: ${result.path}\n`);
|
|
194
|
+
} else {
|
|
195
|
+
process.stdout.write(`Completion installed for ${shell}.\n`);
|
|
196
|
+
process.stdout.write(`Config: ${result.path}\n`);
|
|
197
|
+
process.stdout.write(`\nRestart your shell or run:\n`);
|
|
198
|
+
process.stdout.write(` source ${result.path}\n`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion scripts for bash, zsh, and fish.
|
|
3
|
+
*
|
|
4
|
+
* @module src/cli/commands/completion/scripts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CLI_NAME } from "../../../app/constants.js";
|
|
8
|
+
|
|
9
|
+
export type Shell = "bash" | "zsh" | "fish";
|
|
10
|
+
export const SUPPORTED_SHELLS: Shell[] = ["bash", "zsh", "fish"];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* All gno commands and subcommands for completion.
|
|
14
|
+
*/
|
|
15
|
+
const COMMANDS = [
|
|
16
|
+
"init",
|
|
17
|
+
"index",
|
|
18
|
+
"update",
|
|
19
|
+
"embed",
|
|
20
|
+
"status",
|
|
21
|
+
"doctor",
|
|
22
|
+
"cleanup",
|
|
23
|
+
"reset",
|
|
24
|
+
"search",
|
|
25
|
+
"vsearch",
|
|
26
|
+
"query",
|
|
27
|
+
"ask",
|
|
28
|
+
"get",
|
|
29
|
+
"multi-get",
|
|
30
|
+
"ls",
|
|
31
|
+
"serve",
|
|
32
|
+
"mcp",
|
|
33
|
+
"mcp serve",
|
|
34
|
+
"mcp install",
|
|
35
|
+
"mcp uninstall",
|
|
36
|
+
"mcp status",
|
|
37
|
+
"collection",
|
|
38
|
+
"collection add",
|
|
39
|
+
"collection list",
|
|
40
|
+
"collection remove",
|
|
41
|
+
"collection rename",
|
|
42
|
+
"context",
|
|
43
|
+
"context add",
|
|
44
|
+
"context list",
|
|
45
|
+
"context check",
|
|
46
|
+
"context rm",
|
|
47
|
+
"models",
|
|
48
|
+
"models list",
|
|
49
|
+
"models pull",
|
|
50
|
+
"models clear",
|
|
51
|
+
"models path",
|
|
52
|
+
"models use",
|
|
53
|
+
"skill",
|
|
54
|
+
"skill install",
|
|
55
|
+
"skill uninstall",
|
|
56
|
+
"skill show",
|
|
57
|
+
"skill paths",
|
|
58
|
+
"completion",
|
|
59
|
+
"completion output",
|
|
60
|
+
"completion install",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Global flags available on all commands.
|
|
65
|
+
*/
|
|
66
|
+
const GLOBAL_FLAGS = [
|
|
67
|
+
"--index",
|
|
68
|
+
"--config",
|
|
69
|
+
"--no-color",
|
|
70
|
+
"--no-pager",
|
|
71
|
+
"--verbose",
|
|
72
|
+
"--yes",
|
|
73
|
+
"--quiet",
|
|
74
|
+
"--json",
|
|
75
|
+
"--offline",
|
|
76
|
+
"--help",
|
|
77
|
+
"--version",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate bash completion script.
|
|
82
|
+
*/
|
|
83
|
+
export function generateBashCompletion(): string {
|
|
84
|
+
const commands = COMMANDS.filter((c) => !c.includes(" ")).join(" ");
|
|
85
|
+
const subcommands: Record<string, string> = {};
|
|
86
|
+
|
|
87
|
+
for (const cmd of COMMANDS) {
|
|
88
|
+
if (cmd.includes(" ")) {
|
|
89
|
+
const parts = cmd.split(" ");
|
|
90
|
+
const parent = parts[0];
|
|
91
|
+
const sub = parts[1];
|
|
92
|
+
if (parent && sub) {
|
|
93
|
+
subcommands[parent] = subcommands[parent]
|
|
94
|
+
? `${subcommands[parent]} ${sub}`
|
|
95
|
+
: sub;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const subCases = Object.entries(subcommands)
|
|
101
|
+
.map(
|
|
102
|
+
([parent, subs]) =>
|
|
103
|
+
` ${parent}) COMPREPLY=($(compgen -W "${subs}" -- "\${cur}")) ;;`
|
|
104
|
+
)
|
|
105
|
+
.join("\n");
|
|
106
|
+
|
|
107
|
+
return `# ${CLI_NAME} bash completion
|
|
108
|
+
# Add to ~/.bashrc or ~/.bash_completion
|
|
109
|
+
|
|
110
|
+
_${CLI_NAME}_completions() {
|
|
111
|
+
local cur prev cword
|
|
112
|
+
# Portable: don't rely on _init_completion (requires bash-completion package)
|
|
113
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
114
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
115
|
+
cword="\${COMP_CWORD}"
|
|
116
|
+
|
|
117
|
+
local commands="${commands}"
|
|
118
|
+
local global_flags="${GLOBAL_FLAGS.join(" ")}"
|
|
119
|
+
|
|
120
|
+
# Complete subcommands for parent commands
|
|
121
|
+
case "\${COMP_WORDS[1]}" in
|
|
122
|
+
${subCases}
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
# Complete global flags
|
|
126
|
+
if [[ "\${cur}" == -* ]]; then
|
|
127
|
+
COMPREPLY=($(compgen -W "\${global_flags}" -- "\${cur}"))
|
|
128
|
+
return
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Complete top-level commands
|
|
132
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
133
|
+
COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}"))
|
|
134
|
+
return
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Dynamic collection completion for -c/--collection flag
|
|
138
|
+
if [[ "\${prev}" == "-c" || "\${prev}" == "--collection" ]]; then
|
|
139
|
+
local collections
|
|
140
|
+
collections=$(${CLI_NAME} collection list --json 2>/dev/null | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
|
|
141
|
+
COMPREPLY=($(compgen -W "\${collections}" -- "\${cur}"))
|
|
142
|
+
return
|
|
143
|
+
fi
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
complete -F _${CLI_NAME}_completions ${CLI_NAME}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate zsh completion script.
|
|
152
|
+
*/
|
|
153
|
+
export function generateZshCompletion(): string {
|
|
154
|
+
const topLevel = COMMANDS.filter((c) => !c.includes(" "));
|
|
155
|
+
const subcommands: Record<string, string[]> = {};
|
|
156
|
+
|
|
157
|
+
for (const cmd of COMMANDS) {
|
|
158
|
+
if (cmd.includes(" ")) {
|
|
159
|
+
const parts = cmd.split(" ");
|
|
160
|
+
const parent = parts[0];
|
|
161
|
+
const sub = parts[1];
|
|
162
|
+
if (parent && sub) {
|
|
163
|
+
subcommands[parent] = subcommands[parent] || [];
|
|
164
|
+
subcommands[parent].push(sub);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const subCases = Object.entries(subcommands)
|
|
170
|
+
.map(
|
|
171
|
+
([parent, subs]) =>
|
|
172
|
+
` ${parent})
|
|
173
|
+
_arguments '2:subcommand:(${subs.join(" ")})'
|
|
174
|
+
;;`
|
|
175
|
+
)
|
|
176
|
+
.join("\n");
|
|
177
|
+
|
|
178
|
+
return `# ${CLI_NAME} zsh completion
|
|
179
|
+
# Add to ~/.zshrc or copy to a directory in $fpath
|
|
180
|
+
# If autoloading from fpath, add "#compdef ${CLI_NAME}" as first line
|
|
181
|
+
|
|
182
|
+
_${CLI_NAME}() {
|
|
183
|
+
local -a commands
|
|
184
|
+
commands=(
|
|
185
|
+
${topLevel.map((c) => ` '${c}:${getCommandDescription(c)}'`).join("\n")}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
local -a global_flags
|
|
189
|
+
global_flags=(
|
|
190
|
+
'--index[index name]:name:'
|
|
191
|
+
'--config[config file path]:file:_files'
|
|
192
|
+
'-c[filter by collection]:collection:_${CLI_NAME}_collections'
|
|
193
|
+
'--collection[filter by collection]:collection:_${CLI_NAME}_collections'
|
|
194
|
+
'--no-color[disable colors]'
|
|
195
|
+
'--no-pager[disable paging]'
|
|
196
|
+
'--verbose[verbose logging]'
|
|
197
|
+
'--yes[non-interactive mode]'
|
|
198
|
+
'--quiet[suppress non-essential output]'
|
|
199
|
+
'--json[JSON output]'
|
|
200
|
+
'--offline[offline mode]'
|
|
201
|
+
'--help[show help]'
|
|
202
|
+
'--version[show version]'
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
_arguments -C \\
|
|
206
|
+
"\${global_flags[@]}" \\
|
|
207
|
+
'1:command:->command' \\
|
|
208
|
+
'*::arg:->args'
|
|
209
|
+
|
|
210
|
+
case "$state" in
|
|
211
|
+
command)
|
|
212
|
+
_describe -t commands 'gno commands' commands
|
|
213
|
+
;;
|
|
214
|
+
args)
|
|
215
|
+
# words[1] is the program name in zsh, words[2] is the command
|
|
216
|
+
case "\${words[2]}" in
|
|
217
|
+
${subCases}
|
|
218
|
+
esac
|
|
219
|
+
;;
|
|
220
|
+
esac
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Dynamic collection completion
|
|
224
|
+
_${CLI_NAME}_collections() {
|
|
225
|
+
local -a collections
|
|
226
|
+
collections=(\${(f)"$($CLI_NAME collection list --json 2>/dev/null | grep -o '"name":"[^"]*"' | cut -d'"' -f4)"})
|
|
227
|
+
_describe -t collections 'collections' collections
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Register completion (works when sourced or autoloaded)
|
|
231
|
+
(( $+functions[compdef] )) && compdef _${CLI_NAME} ${CLI_NAME}
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Generate fish completion script.
|
|
237
|
+
*/
|
|
238
|
+
export function generateFishCompletion(): string {
|
|
239
|
+
const topLevel = COMMANDS.filter((c) => !c.includes(" "));
|
|
240
|
+
const subcommands: Record<string, string[]> = {};
|
|
241
|
+
|
|
242
|
+
for (const cmd of COMMANDS) {
|
|
243
|
+
if (cmd.includes(" ")) {
|
|
244
|
+
const parts = cmd.split(" ");
|
|
245
|
+
const parent = parts[0];
|
|
246
|
+
const sub = parts[1];
|
|
247
|
+
if (parent && sub) {
|
|
248
|
+
subcommands[parent] = subcommands[parent] || [];
|
|
249
|
+
subcommands[parent].push(sub);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const topLevelCompletions = topLevel
|
|
255
|
+
.map(
|
|
256
|
+
(c) =>
|
|
257
|
+
`complete -c ${CLI_NAME} -f -n "__fish_use_subcommand" -a "${c}" -d "${getCommandDescription(c)}"`
|
|
258
|
+
)
|
|
259
|
+
.join("\n");
|
|
260
|
+
|
|
261
|
+
const subCompletions = Object.entries(subcommands)
|
|
262
|
+
.flatMap(([parent, subs]) =>
|
|
263
|
+
subs.map(
|
|
264
|
+
(sub) =>
|
|
265
|
+
`complete -c ${CLI_NAME} -f -n "__fish_seen_subcommand_from ${parent}" -a "${sub}" -d "${getCommandDescription(`${parent} ${sub}`)}"`
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
.join("\n");
|
|
269
|
+
|
|
270
|
+
return `# ${CLI_NAME} fish completion
|
|
271
|
+
# Copy to ~/.config/fish/completions/${CLI_NAME}.fish
|
|
272
|
+
|
|
273
|
+
# Disable file completion by default
|
|
274
|
+
complete -c ${CLI_NAME} -f
|
|
275
|
+
|
|
276
|
+
# Global flags
|
|
277
|
+
complete -c ${CLI_NAME} -l index -d "index name" -r
|
|
278
|
+
complete -c ${CLI_NAME} -l config -d "config file path" -r
|
|
279
|
+
complete -c ${CLI_NAME} -l no-color -d "disable colors"
|
|
280
|
+
complete -c ${CLI_NAME} -l verbose -d "verbose logging"
|
|
281
|
+
complete -c ${CLI_NAME} -l yes -d "non-interactive mode"
|
|
282
|
+
complete -c ${CLI_NAME} -l quiet -d "suppress non-essential output"
|
|
283
|
+
complete -c ${CLI_NAME} -l json -d "JSON output"
|
|
284
|
+
complete -c ${CLI_NAME} -l offline -d "offline mode"
|
|
285
|
+
complete -c ${CLI_NAME} -s h -l help -d "show help"
|
|
286
|
+
complete -c ${CLI_NAME} -s V -l version -d "show version"
|
|
287
|
+
|
|
288
|
+
# Top-level commands
|
|
289
|
+
${topLevelCompletions}
|
|
290
|
+
|
|
291
|
+
# Subcommands
|
|
292
|
+
${subCompletions}
|
|
293
|
+
|
|
294
|
+
# Dynamic collection completion for -c/--collection
|
|
295
|
+
complete -c ${CLI_NAME} -s c -l collection -d "filter by collection" -xa "(${CLI_NAME} collection list --json 2>/dev/null | string match -r '"name":"[^"]*"' | string replace -r '"name":"([^"]*)"' '$1')"
|
|
296
|
+
`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get script for a specific shell.
|
|
301
|
+
*/
|
|
302
|
+
export function getCompletionScript(shell: Shell): string {
|
|
303
|
+
switch (shell) {
|
|
304
|
+
case "bash":
|
|
305
|
+
return generateBashCompletion();
|
|
306
|
+
case "zsh":
|
|
307
|
+
return generateZshCompletion();
|
|
308
|
+
case "fish":
|
|
309
|
+
return generateFishCompletion();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get brief description for a command.
|
|
315
|
+
*/
|
|
316
|
+
function getCommandDescription(cmd: string): string {
|
|
317
|
+
const descriptions: Record<string, string> = {
|
|
318
|
+
init: "Initialize GNO configuration",
|
|
319
|
+
index: "Index files from collections",
|
|
320
|
+
update: "Sync files from disk",
|
|
321
|
+
embed: "Generate embeddings",
|
|
322
|
+
status: "Show index status",
|
|
323
|
+
doctor: "Diagnose configuration issues",
|
|
324
|
+
cleanup: "Clean orphaned data",
|
|
325
|
+
reset: "Delete all GNO data",
|
|
326
|
+
search: "BM25 keyword search",
|
|
327
|
+
vsearch: "Vector similarity search",
|
|
328
|
+
query: "Hybrid search with reranking",
|
|
329
|
+
ask: "Query with grounded answer",
|
|
330
|
+
get: "Get document by URI",
|
|
331
|
+
"multi-get": "Get multiple documents",
|
|
332
|
+
ls: "List indexed documents",
|
|
333
|
+
serve: "Start web UI server",
|
|
334
|
+
mcp: "MCP server and configuration",
|
|
335
|
+
"mcp serve": "Start MCP server",
|
|
336
|
+
"mcp install": "Install MCP server to client",
|
|
337
|
+
"mcp uninstall": "Remove MCP server from client",
|
|
338
|
+
"mcp status": "Show MCP installation status",
|
|
339
|
+
collection: "Manage collections",
|
|
340
|
+
"collection add": "Add a collection",
|
|
341
|
+
"collection list": "List collections",
|
|
342
|
+
"collection remove": "Remove a collection",
|
|
343
|
+
"collection rename": "Rename a collection",
|
|
344
|
+
context: "Manage context items",
|
|
345
|
+
"context add": "Add context metadata",
|
|
346
|
+
"context list": "List context items",
|
|
347
|
+
"context check": "Check context configuration",
|
|
348
|
+
"context rm": "Remove context item",
|
|
349
|
+
models: "Manage LLM models",
|
|
350
|
+
"models list": "List available models",
|
|
351
|
+
"models pull": "Download models",
|
|
352
|
+
"models clear": "Clear model cache",
|
|
353
|
+
"models path": "Show model cache path",
|
|
354
|
+
"models use": "Switch active model preset",
|
|
355
|
+
skill: "Manage GNO agent skill",
|
|
356
|
+
"skill install": "Install GNO skill",
|
|
357
|
+
"skill uninstall": "Uninstall GNO skill",
|
|
358
|
+
"skill show": "Preview skill files",
|
|
359
|
+
"skill paths": "Show skill installation paths",
|
|
360
|
+
completion: "Shell completion scripts",
|
|
361
|
+
"completion output": "Output completion script",
|
|
362
|
+
"completion install": "Install shell completions",
|
|
363
|
+
};
|
|
364
|
+
return descriptions[cmd] || cmd;
|
|
365
|
+
}
|
package/src/cli/context.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface GlobalOptions {
|
|
|
21
21
|
quiet: boolean;
|
|
22
22
|
json: boolean;
|
|
23
23
|
offline: boolean;
|
|
24
|
+
noPager: boolean;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -58,6 +59,8 @@ export function parseGlobalOptions(
|
|
|
58
59
|
quiet: Boolean(raw.quiet),
|
|
59
60
|
json: Boolean(raw.json),
|
|
60
61
|
offline: offlineEnabled,
|
|
62
|
+
// Commander: --no-pager => opts().pager === false
|
|
63
|
+
noPager: raw.pager === false,
|
|
61
64
|
};
|
|
62
65
|
}
|
|
63
66
|
|
package/src/cli/pager.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pager utility for long CLI output.
|
|
3
|
+
* Pipes output through less/more when terminal height exceeded.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/pager
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// node:os - no Bun equivalent for platform()
|
|
9
|
+
import { platform } from "node:os";
|
|
10
|
+
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// Types
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface PagerOptions {
|
|
16
|
+
/** Force disable paging (--no-pager flag) */
|
|
17
|
+
noPager?: boolean;
|
|
18
|
+
/** Override terminal height detection */
|
|
19
|
+
terminalHeight?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Pager Detection
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Find available pager command.
|
|
28
|
+
* Priority: $PAGER env → less (with -R for colors) → more
|
|
29
|
+
*/
|
|
30
|
+
function findPager(): string[] | null {
|
|
31
|
+
// Check $PAGER env first (cross-platform)
|
|
32
|
+
const pagerEnv = process.env.PAGER;
|
|
33
|
+
if (pagerEnv) {
|
|
34
|
+
// Split in case user has args like "less -R"
|
|
35
|
+
return pagerEnv.split(/\s+/);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Platform-specific fallbacks
|
|
39
|
+
const isWindows = platform() === "win32";
|
|
40
|
+
|
|
41
|
+
if (isWindows) {
|
|
42
|
+
// Windows: use more.com (basic but universally available)
|
|
43
|
+
return ["more.com"];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Unix: prefer less with -R (preserve ANSI colors)
|
|
47
|
+
return ["less", "-R"];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if paging should be enabled.
|
|
52
|
+
*/
|
|
53
|
+
function shouldPage(options: PagerOptions): boolean {
|
|
54
|
+
// Explicitly disabled
|
|
55
|
+
if (options.noPager) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Not a TTY (piped output)
|
|
60
|
+
if (!process.stdout.isTTY) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get terminal height.
|
|
69
|
+
*/
|
|
70
|
+
function getTerminalHeight(options: PagerOptions): number {
|
|
71
|
+
if (options.terminalHeight !== undefined) {
|
|
72
|
+
return options.terminalHeight;
|
|
73
|
+
}
|
|
74
|
+
return process.stdout.rows || 24; // Default to 24 if unknown
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
// Pager Class
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pager that buffers output and pipes through pager if needed.
|
|
83
|
+
*/
|
|
84
|
+
export class Pager {
|
|
85
|
+
private options: PagerOptions;
|
|
86
|
+
private buffer: string[] = [];
|
|
87
|
+
private terminalHeight: number;
|
|
88
|
+
private pagerProcess: ReturnType<typeof Bun.spawn> | null = null;
|
|
89
|
+
private enabled: boolean;
|
|
90
|
+
|
|
91
|
+
constructor(options: PagerOptions = {}) {
|
|
92
|
+
this.options = options;
|
|
93
|
+
this.terminalHeight = getTerminalHeight(options);
|
|
94
|
+
this.enabled = shouldPage(options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Write a line to the pager buffer.
|
|
99
|
+
*/
|
|
100
|
+
writeLine(line: string): void {
|
|
101
|
+
this.buffer.push(line);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Write multiple lines to the pager buffer.
|
|
106
|
+
*/
|
|
107
|
+
writeLines(lines: string[]): void {
|
|
108
|
+
this.buffer.push(...lines);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Write raw content (may contain newlines).
|
|
113
|
+
*/
|
|
114
|
+
write(content: string): void {
|
|
115
|
+
const lines = content.split("\n");
|
|
116
|
+
// Don't add empty string from trailing newline
|
|
117
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
118
|
+
lines.pop();
|
|
119
|
+
}
|
|
120
|
+
this.buffer.push(...lines);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Flush buffer to stdout, using pager if content exceeds terminal height.
|
|
125
|
+
*/
|
|
126
|
+
async flush(): Promise<void> {
|
|
127
|
+
const content = this.buffer.join("\n");
|
|
128
|
+
const lineCount = this.buffer.length;
|
|
129
|
+
|
|
130
|
+
// If paging disabled or content fits in terminal, write directly
|
|
131
|
+
if (!this.enabled || lineCount <= this.terminalHeight - 1) {
|
|
132
|
+
if (content) {
|
|
133
|
+
process.stdout.write(content + "\n");
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Try to spawn pager
|
|
139
|
+
const pagerCmd = findPager();
|
|
140
|
+
if (!pagerCmd) {
|
|
141
|
+
// No pager available, write directly
|
|
142
|
+
process.stdout.write(content + "\n");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Spawn pager and pipe content
|
|
147
|
+
await this.spawnPager(pagerCmd, content);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Spawn pager process and pipe content.
|
|
152
|
+
*/
|
|
153
|
+
private async spawnPager(pagerCmd: string[], content: string): Promise<void> {
|
|
154
|
+
const [cmd, ...args] = pagerCmd;
|
|
155
|
+
if (!cmd) {
|
|
156
|
+
process.stdout.write(content + "\n");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
162
|
+
stdin: "pipe",
|
|
163
|
+
stdout: "inherit",
|
|
164
|
+
stderr: "inherit",
|
|
165
|
+
});
|
|
166
|
+
this.pagerProcess = proc;
|
|
167
|
+
|
|
168
|
+
// Write content to pager stdin
|
|
169
|
+
if (proc.stdin) {
|
|
170
|
+
proc.stdin.write(content + "\n");
|
|
171
|
+
await proc.stdin.end();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Wait for pager to exit
|
|
175
|
+
await proc.exited;
|
|
176
|
+
this.pagerProcess = null;
|
|
177
|
+
} catch {
|
|
178
|
+
// Spawn failed - fall back to direct output
|
|
179
|
+
process.stdout.write(content + "\n");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
// Helper Function
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Convenience function to page content.
|
|
190
|
+
* @param content Content to potentially page
|
|
191
|
+
* @param options Pager options
|
|
192
|
+
*/
|
|
193
|
+
export async function pageContent(
|
|
194
|
+
content: string,
|
|
195
|
+
options: PagerOptions = {}
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const pager = new Pager(options);
|
|
198
|
+
pager.write(content);
|
|
199
|
+
await pager.flush();
|
|
200
|
+
}
|
package/src/cli/program.ts
CHANGED
|
@@ -110,6 +110,25 @@ function getFormat(
|
|
|
110
110
|
return "terminal";
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Write output with optional paging for terminal format.
|
|
115
|
+
* Paging only applies to terminal format with list-like output.
|
|
116
|
+
*/
|
|
117
|
+
async function writeOutput(
|
|
118
|
+
content: string,
|
|
119
|
+
format: "terminal" | "json" | "files" | "csv" | "md" | "xml"
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const globals = getGlobals();
|
|
122
|
+
|
|
123
|
+
// Only page terminal output when paging enabled
|
|
124
|
+
if (format === "terminal" && !globals.noPager) {
|
|
125
|
+
const { pageContent } = await import("./pager.js");
|
|
126
|
+
await pageContent(content);
|
|
127
|
+
} else {
|
|
128
|
+
process.stdout.write(content + "\n");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
133
|
// Program Factory
|
|
115
134
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -134,7 +153,8 @@ export function createProgram(): Command {
|
|
|
134
153
|
.option("--yes", "non-interactive mode")
|
|
135
154
|
.option("-q, --quiet", "suppress non-essential output")
|
|
136
155
|
.option("--json", "JSON output (for errors and supported commands)")
|
|
137
|
-
.option("--offline", "offline mode (use cached models only)")
|
|
156
|
+
.option("--offline", "offline mode (use cached models only)")
|
|
157
|
+
.option("--no-pager", "disable automatic paging of long output");
|
|
138
158
|
|
|
139
159
|
// Resolve globals ONCE before any command runs (ensures consistency)
|
|
140
160
|
program.hook("preAction", (thisCommand) => {
|
|
@@ -152,6 +172,7 @@ export function createProgram(): Command {
|
|
|
152
172
|
wireMcpCommand(program);
|
|
153
173
|
wireSkillCommands(program);
|
|
154
174
|
wireServeCommand(program);
|
|
175
|
+
wireCompletionCommand(program);
|
|
155
176
|
|
|
156
177
|
// Add docs/support links to help footer
|
|
157
178
|
program.addHelpText(
|
|
@@ -226,17 +247,16 @@ function wireSearchCommands(program: Command): void {
|
|
|
226
247
|
result.error
|
|
227
248
|
);
|
|
228
249
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
);
|
|
250
|
+
const output = formatSearch(result, {
|
|
251
|
+
json: format === "json",
|
|
252
|
+
md: format === "md",
|
|
253
|
+
csv: format === "csv",
|
|
254
|
+
xml: format === "xml",
|
|
255
|
+
files: format === "files",
|
|
256
|
+
full: Boolean(cmdOpts.full),
|
|
257
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
258
|
+
});
|
|
259
|
+
await writeOutput(output, format);
|
|
240
260
|
});
|
|
241
261
|
|
|
242
262
|
// vsearch - Vector similarity search
|
|
@@ -291,17 +311,16 @@ function wireSearchCommands(program: Command): void {
|
|
|
291
311
|
if (!result.success) {
|
|
292
312
|
throw new CliError("RUNTIME", result.error);
|
|
293
313
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
);
|
|
314
|
+
const output = formatVsearch(result, {
|
|
315
|
+
json: format === "json",
|
|
316
|
+
md: format === "md",
|
|
317
|
+
csv: format === "csv",
|
|
318
|
+
xml: format === "xml",
|
|
319
|
+
files: format === "files",
|
|
320
|
+
full: Boolean(cmdOpts.full),
|
|
321
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
322
|
+
});
|
|
323
|
+
await writeOutput(output, format);
|
|
305
324
|
});
|
|
306
325
|
|
|
307
326
|
// query - Hybrid search with expansion and reranking
|
|
@@ -388,13 +407,12 @@ function wireSearchCommands(program: Command): void {
|
|
|
388
407
|
if (!result.success) {
|
|
389
408
|
throw new CliError("RUNTIME", result.error);
|
|
390
409
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
);
|
|
410
|
+
const output = formatQuery(result, {
|
|
411
|
+
format,
|
|
412
|
+
full: Boolean(cmdOpts.full),
|
|
413
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
414
|
+
});
|
|
415
|
+
await writeOutput(output, format);
|
|
398
416
|
});
|
|
399
417
|
|
|
400
418
|
// ask - Human-friendly query with grounded answer
|
|
@@ -464,9 +482,12 @@ function wireSearchCommands(program: Command): void {
|
|
|
464
482
|
if (!result.success) {
|
|
465
483
|
throw new CliError("RUNTIME", result.error);
|
|
466
484
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
485
|
+
const output = formatAsk(result, {
|
|
486
|
+
json: format === "json",
|
|
487
|
+
md: format === "md",
|
|
488
|
+
showSources,
|
|
489
|
+
});
|
|
490
|
+
await writeOutput(output, format);
|
|
470
491
|
});
|
|
471
492
|
}
|
|
472
493
|
|
|
@@ -738,13 +759,12 @@ function wireRetrievalCommands(program: Command): void {
|
|
|
738
759
|
);
|
|
739
760
|
}
|
|
740
761
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
);
|
|
762
|
+
const output = formatLs(result, {
|
|
763
|
+
json: format === "json",
|
|
764
|
+
files: format === "files",
|
|
765
|
+
md: format === "md",
|
|
766
|
+
});
|
|
767
|
+
await writeOutput(output, format);
|
|
748
768
|
}
|
|
749
769
|
);
|
|
750
770
|
}
|
|
@@ -1407,3 +1427,68 @@ function wireServeCommand(program: Command): void {
|
|
|
1407
1427
|
// Server runs until SIGINT/SIGTERM - no output needed here
|
|
1408
1428
|
});
|
|
1409
1429
|
}
|
|
1430
|
+
|
|
1431
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1432
|
+
// Completion Command (shell completions)
|
|
1433
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1434
|
+
|
|
1435
|
+
function wireCompletionCommand(program: Command): void {
|
|
1436
|
+
const completionCmd = program
|
|
1437
|
+
.command("completion")
|
|
1438
|
+
.description("Shell completion scripts");
|
|
1439
|
+
|
|
1440
|
+
// Output completion script
|
|
1441
|
+
completionCmd
|
|
1442
|
+
.command("output <shell>", { isDefault: true })
|
|
1443
|
+
.description("Output completion script for a shell (bash, zsh, fish)")
|
|
1444
|
+
.action(async (shell: string) => {
|
|
1445
|
+
const { completionOutput, SUPPORTED_SHELLS } =
|
|
1446
|
+
await import("./commands/completion/index.js");
|
|
1447
|
+
|
|
1448
|
+
if (!SUPPORTED_SHELLS.includes(shell as "bash" | "zsh" | "fish")) {
|
|
1449
|
+
throw new CliError(
|
|
1450
|
+
"VALIDATION",
|
|
1451
|
+
`Unsupported shell: ${shell}. Supported: ${SUPPORTED_SHELLS.join(", ")}`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const script = completionOutput({
|
|
1456
|
+
shell: shell as "bash" | "zsh" | "fish",
|
|
1457
|
+
});
|
|
1458
|
+
process.stdout.write(script);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
// Install completion to user's shell config
|
|
1462
|
+
completionCmd
|
|
1463
|
+
.command("install")
|
|
1464
|
+
.description("Auto-install completion to shell config")
|
|
1465
|
+
.option(
|
|
1466
|
+
"-s, --shell <shell>",
|
|
1467
|
+
"shell to install for (bash, zsh, fish) - auto-detected if omitted"
|
|
1468
|
+
)
|
|
1469
|
+
.option("--json", "JSON output")
|
|
1470
|
+
.action(async (...args: unknown[]) => {
|
|
1471
|
+
// Last arg is the Command object - use optsWithGlobals for subcommands
|
|
1472
|
+
const cmd = args[args.length - 1] as Command;
|
|
1473
|
+
const cmdOpts = cmd.optsWithGlobals<{ shell?: string; json?: boolean }>();
|
|
1474
|
+
const { shell, json } = cmdOpts;
|
|
1475
|
+
|
|
1476
|
+
const { completionInstall, SUPPORTED_SHELLS } =
|
|
1477
|
+
await import("./commands/completion/index.js");
|
|
1478
|
+
|
|
1479
|
+
if (
|
|
1480
|
+
shell &&
|
|
1481
|
+
!SUPPORTED_SHELLS.includes(shell as "bash" | "zsh" | "fish")
|
|
1482
|
+
) {
|
|
1483
|
+
throw new CliError(
|
|
1484
|
+
"VALIDATION",
|
|
1485
|
+
`Unsupported shell: ${shell}. Supported: ${SUPPORTED_SHELLS.join(", ")}`
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
await completionInstall({
|
|
1490
|
+
shell: shell as "bash" | "zsh" | "fish" | undefined,
|
|
1491
|
+
json: Boolean(json),
|
|
1492
|
+
});
|
|
1493
|
+
});
|
|
1494
|
+
}
|
package/src/config/types.ts
CHANGED
|
@@ -176,7 +176,7 @@ export type ModelPreset = z.infer<typeof ModelPresetSchema>;
|
|
|
176
176
|
export const DEFAULT_MODEL_PRESETS: ModelPreset[] = [
|
|
177
177
|
{
|
|
178
178
|
id: "slim",
|
|
179
|
-
name: "Slim (
|
|
179
|
+
name: "Slim (Default, ~1GB)",
|
|
180
180
|
embed: "hf:gpustack/bge-m3-GGUF/bge-m3-Q4_K_M.gguf",
|
|
181
181
|
rerank:
|
|
182
182
|
"hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",
|
|
@@ -184,11 +184,11 @@ export const DEFAULT_MODEL_PRESETS: ModelPreset[] = [
|
|
|
184
184
|
},
|
|
185
185
|
{
|
|
186
186
|
id: "balanced",
|
|
187
|
-
name: "Balanced (
|
|
187
|
+
name: "Balanced (~2GB)",
|
|
188
188
|
embed: "hf:gpustack/bge-m3-GGUF/bge-m3-Q4_K_M.gguf",
|
|
189
189
|
rerank:
|
|
190
190
|
"hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",
|
|
191
|
-
gen: "hf:
|
|
191
|
+
gen: "hf:bartowski/Qwen2.5-3B-Instruct-GGUF/Qwen2.5-3B-Instruct-Q4_K_M.gguf",
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
id: "quality",
|
|
@@ -202,7 +202,7 @@ export const DEFAULT_MODEL_PRESETS: ModelPreset[] = [
|
|
|
202
202
|
|
|
203
203
|
export const ModelConfigSchema = z.object({
|
|
204
204
|
/** Active preset ID */
|
|
205
|
-
activePreset: z.string().default("
|
|
205
|
+
activePreset: z.string().default("slim"),
|
|
206
206
|
/** Model presets */
|
|
207
207
|
presets: z.array(ModelPresetSchema).default(DEFAULT_MODEL_PRESETS),
|
|
208
208
|
/** Model load timeout in ms */
|
package/src/llm/registry.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { DEFAULT_MODEL_PRESETS } from "../config/types";
|
|
|
19
19
|
*/
|
|
20
20
|
export function getModelConfig(config: Config): ModelConfig {
|
|
21
21
|
return {
|
|
22
|
-
activePreset: config.models?.activePreset ?? "
|
|
22
|
+
activePreset: config.models?.activePreset ?? "slim",
|
|
23
23
|
presets: config.models?.presets?.length
|
|
24
24
|
? config.models.presets
|
|
25
25
|
: DEFAULT_MODEL_PRESETS,
|
package/src/pipeline/answer.ts
CHANGED
|
@@ -13,21 +13,24 @@ import type { Citation, SearchResult } from "./types";
|
|
|
13
13
|
// Constants
|
|
14
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
const ANSWER_PROMPT = `
|
|
16
|
+
const ANSWER_PROMPT = `Answer the question using ONLY the context blocks below. Cite sources with [1], [2], etc.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
4) Do not cite sources you did not use. Do not invent citation numbers.
|
|
18
|
+
Example:
|
|
19
|
+
Q: What is the capital of France?
|
|
20
|
+
Context:
|
|
21
|
+
[1] France is a country in Western Europe. Paris is the capital and largest city.
|
|
22
|
+
[2] The Eiffel Tower, built in 1889, is located in Paris.
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
Answer: Paris is the capital of France [1]. It is home to the Eiffel Tower [2].
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
Q: {query}
|
|
29
|
+
|
|
30
|
+
Context:
|
|
28
31
|
{context}
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
Answer:`;
|
|
31
34
|
|
|
32
35
|
/** Abstention message when LLM cannot ground answer */
|
|
33
36
|
export const ABSTENTION_MESSAGE =
|
package/src/pipeline/search.ts
CHANGED
|
@@ -223,7 +223,11 @@ export async function searchBm25(
|
|
|
223
223
|
|
|
224
224
|
// For --full, fetch full content and build results
|
|
225
225
|
if (options.full) {
|
|
226
|
-
|
|
226
|
+
// Sort by raw BM25 score (smaller = better) before building results
|
|
227
|
+
const sortedEntries = [...bestByDocid.values()].sort(
|
|
228
|
+
(a, b) => a.score - b.score
|
|
229
|
+
);
|
|
230
|
+
for (const { fts, chunk } of sortedEntries) {
|
|
227
231
|
let fullContent: string | undefined;
|
|
228
232
|
if (fts.mirrorHash) {
|
|
229
233
|
const contentResult = await store.getContent(fts.mirrorHash);
|