@shubhamcodess/whatcmd 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shubhamcodess
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,221 @@
1
- # Temporary Holding Version
1
+ # whatcmd
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Type plain English at your shell prompt, get a real command back.**
4
+
5
+ whatcmd runs a small language model **on your own machine** through [Ollama](https://ollama.com). No API keys, no accounts, and your requests never leave your computer.
6
+
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+ ![Node](https://img.shields.io/badge/node-%3E%3D21-brightgreen)
9
+ ![Status](https://img.shields.io/badge/status-0.1.0%20pre--release-orange)
10
+
11
+ ```text
12
+ $ /w find files bigger than 100MB ← press Enter
13
+ $ find . -type f -size +100M ← your line is replaced; press Enter again to run it
14
+ ```
15
+
16
+ ## Why
17
+
18
+ Most of the time you know what you want to do but not the exact flags. whatcmd turns the request into one command, puts it on your prompt, and lets you decide.
19
+
20
+ - **It never runs anything by itself.** The command appears on your prompt. You read it, edit it, or press Enter.
21
+ - **It knows your system.** It detects your OS, distro, shell, package manager and installed tools, so it writes BSD flags on macOS (`sed -i ''`, `stat -f`) and avoids tools you don't have.
22
+ - **It reads your docs.** If your request names an installed program (`jq`, `ffmpeg`, `lsof`, …) or a package, it reads that tool's man page or `--help` and gives the relevant lines to the model.
23
+ - **It stays quiet when it should.** "tell me a joke" or "what is the capital of France" produce nothing.
24
+ - **It warns before you hurt yourself.** `rm -r`, `dd`, `mkfs`, `curl | sh`, `git push --force`, `chmod 777` and placeholders like `/path/to/…` are flagged on your prompt.
25
+
26
+ ## Install
27
+
28
+ Requirements: Node.js 21 or newer, macOS or Linux, and bash or zsh for the shortcut (any shell can use `whatcmd <request>`).
29
+
30
+ Choose one of the two options. Both install the same `whatcmd` command.
31
+
32
+ ### Option 1: npm
33
+
34
+ ```bash
35
+ npm install -g @shubhamcodess/whatcmd
36
+ whatcmd init
37
+ ```
38
+
39
+ ### Option 2: release tarball
40
+
41
+ Every release attaches the installable package to its GitHub release. Download it from the [releases page](https://github.com/shubhamcodess/cmdkit/releases), then install the file:
42
+
43
+ ```bash
44
+ npm install -g ./shubhamcodess-whatcmd-0.1.0.tgz
45
+ whatcmd init
46
+ ```
47
+
48
+ Replace `0.1.0` with the version you downloaded. The tarball is the same file CI builds and tests, so it works without npm access.
49
+
50
+ ### Then: first-time setup
51
+
52
+ `whatcmd init` walks you through setup:
53
+
54
+ 1. Detects your system and saves it to your profile.
55
+ 2. Checks Ollama. If it's missing, it shows the install command for your OS and offers to run it. If it's installed but stopped, it offers to start it.
56
+ 3. Lets you pick a model (recommended ones, any model you already have, or any name from the [Ollama library](https://ollama.com/library)) and downloads it.
57
+ 4. Runs a test request and shows how fast it is.
58
+ 5. Adds the `/w` shortcut to your `~/.zshrc` (or `~/.bashrc`).
59
+
60
+ Open a new terminal and type `/w <request>`.
61
+
62
+ ### Development: clone and install from source
63
+
64
+ ```bash
65
+ git clone https://github.com/shubhamcodess/cmdkit whatcmd
66
+ cd whatcmd
67
+ npm ci # installs dependencies and builds dist/
68
+ npm test # 30 unit tests
69
+ npm install -g . # installs the whatcmd command from this checkout
70
+ ```
71
+
72
+ To change the code and see it immediately, run `npm run build` after each edit, or use `npm link` instead of `npm install -g .` and remember to `npm unlink -g @shubhamcodess/whatcmd` when you're done. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.
73
+
74
+ ## Use
75
+
76
+ | You type | What happens |
77
+ | --- | --- |
78
+ | `/w <request>` then Enter | Your line is replaced by the command. Press Enter again to run it. |
79
+ | `whatcmd <request>` | Prints the command and asks "Run it? [y/N]". |
80
+ | `whatcmd --raw -- <request>` | Prints only the command, for scripts. Exit code `2` if the request isn't a shell task. |
81
+
82
+ Quotes are not needed, even with `'`, `>` or `*` in the request: the shortcut reads your line before the shell parses it.
83
+
84
+ ```bash
85
+ whatcmd "what's using port 8080"
86
+ whatcmd --raw -- list the 5 biggest folders in this directory
87
+ ```
88
+
89
+ ## Commands
90
+
91
+ ```text
92
+ whatcmd init first-time setup
93
+ whatcmd doctor check Ollama, model, speed and shortcut; offers fixes
94
+ whatcmd use <model> switch model (downloads it if needed)
95
+ whatcmd models list downloaded models
96
+ whatcmd config show settings
97
+ whatcmd config set <key> <value> | get <key> | unset <key> | path | reset
98
+ whatcmd profile [--refresh] show (or re-detect) the system info given to the model
99
+ whatcmd shell bash|zsh print the shell hook, to add it yourself
100
+ whatcmd uninstall remove the shortcut from your shell config
101
+ ```
102
+
103
+ Flags: `--debug` (grounding, prompts and timings), `--model=NAME` (one request with another model), `--yes` (non-interactive `init` and `doctor`), `--raw` (command only).
104
+
105
+ ## Settings
106
+
107
+ Stored in `~/.config/whatcmd/config.json`. Run `whatcmd config` to see the effective values.
108
+
109
+ | Key | Default | Meaning |
110
+ | --- | --- | --- |
111
+ | `model` | `qwen2.5-coder:1.5b` | Any Ollama model. |
112
+ | `host` | `http://127.0.0.1:11434` | Ollama server. Also reads `OLLAMA_HOST`. |
113
+ | `prefix` | `/w` | What you type before a request, e.g. `config set prefix ,`. |
114
+ | `grounding` | `true` | Read local docs of the tools you mention. |
115
+ | `notes` | *(empty)* | Extra instructions, e.g. `"prefer ripgrep over grep"`. |
116
+ | `keepAlive` | `2h` | How long Ollama keeps the model loaded (`-1` = forever). |
117
+ | `temperature` | `0` | Keep at `0` for repeatable answers. |
118
+ | `timeoutMs` | `30000` | Time limit for one model call. |
119
+
120
+ Environment variables override the file: `WHATCMD_MODEL`, `WHATCMD_HOST`, `WHATCMD_CONFIG_DIR`, `WHATCMD_CACHE_DIR`. `NO_COLOR` turns off colours.
121
+
122
+ ## Measurements
123
+
124
+ All numbers below come from the benchmark and eval runs described in [EVALS.md](EVALS.md). Hardware: Apple M4 Pro, 14 cores, 48 GB RAM, macOS 15.6.1, Node 22.16, Ollama 0.40.14, zsh 5.9. The final benchmark ran on commit `47e6b47`.
125
+
126
+ **Accuracy: 500-case benchmark, config A (`qwen2.5-coder:1.5b`, grounding on)**
127
+
128
+ | Area | Result | Target |
129
+ | --- | --- | --- |
130
+ | Overall | **92.7%** (454 / 490 scored) | — |
131
+ | Generation (plain-English → command) | **84.7%** (127 / 150) | 85% (one case short) |
132
+ | Refusal (non-shell requests produce nothing) | **98.8%** (79 / 80) | 93.8% |
133
+ | Grounding, invented flags on fake tools | 85% (17 / 20) | — |
134
+ | Grounding, real installed tools | 90% (27 / 30) | — |
135
+ | Risk warnings (model + deterministic) | 95% (38 / 40) | — |
136
+ | Missing tools (named tool not installed) | 100% (30 / 30) | — |
137
+ | Gate, risk, config and CLI unit-style checks | 100% (60 / 60, 30 / 30, 20 / 20) | — |
138
+ | Shell hook, zsh (10 scenarios) | 10 / 10 | — |
139
+
140
+ **Other sizes, same run**
141
+
142
+ | Config | Model | Grounding | Result |
143
+ | --- | --- | --- | --- |
144
+ | A | qwen2.5-coder:1.5b (default) | on | 92.7% overall, all 500 cases |
145
+ | B | qwen2.5-coder:1.5b | off | 77.7% of model cases (grounding cases cannot pass without docs) |
146
+ | C | qwen2.5-coder:0.5b | on | 68% of model cases; generation 66% |
147
+
148
+ **Eval (`npm run eval`, 83 requests, same model and commit)**
149
+
150
+ - Valid requests answered correctly: **35 / 48**.
151
+ - Non-shell requests blocked: **34 / 35**.
152
+ - Latency p50 105 ms, p90 218 ms, max 645 ms.
153
+
154
+ **Speed (config A, benchmark run)**
155
+
156
+ | Measure | Value |
157
+ | --- | --- |
158
+ | Model call, p50 / p90 / p99 | 121 ms / 422 ms / 739 ms |
159
+ | Whole command end-to-end (`whatcmd --raw`), p50 / p90 | 217 ms / 330 ms |
160
+ | First request after the model unloads | about 0.8 s |
161
+ | Repeatability: 50 random cases run 3 times | 50 / 50 gave identical output |
162
+
163
+ Numbers vary with hardware, model, and whether the model is already loaded.
164
+
165
+ ## How it works
166
+
167
+ ```text
168
+ request ─► grounding ─► local model ─► gate ─► your prompt line
169
+ man / --help system profile NONE? not a command?
170
+ package db + examples not installed? → retry once
171
+ risky? → warning
172
+ ```
173
+
174
+ 1. **Profile.** OS, distro, shell, package manager, GNU/BSD/BusyBox userland, and which common tools are installed. Detected once and stored.
175
+ 2. **Grounding.** Words in your request that name an installed program or system package pull in that tool's usage and matching option lines. The man page is read first, `--help` is the fallback, and dangerous programs are never run with `--help`. Results are cached per binary version.
176
+ 3. **Generation.** The model gets the profile, a few examples and the docs. It must answer with one command or `NONE`.
177
+ 4. **Gate.** Output is dropped when it is `NONE`, doesn't parse as shell, is chat text, repeats the request, copies an example, or loops.
178
+ 5. **Retries (at most 3 model calls).**
179
+ - **Second chance:** a request that is clearly about the computer (files, git, ports, docker, paths…) gets one more attempt with a prompt that has no `NONE` option. Arithmetic, text and prompt-injection answers are still filtered out.
180
+ - **Fix-up:** if the command uses a program that isn't installed, GNU-only flags on macOS, or repeats the request, the model gets one specific hint. For example: "on macOS use `vm_stat` instead of `free`".
181
+ - **Auto-fix:** on macOS, `sed -i 's/…'` becomes `sed -i '' 's/…'`.
182
+
183
+ ## Known limits
184
+
185
+ - **Small models make mistakes.** The eval and the benchmark both show wrong flags, wrong tools and dropped filters. Read every command before you press Enter.
186
+ - **Generation is just under target.** 84.7% against an 85% goal on the 500-case benchmark. Measured, not fixed.
187
+ - **Only zsh was verified end to end.** The bash shortcut needs bash 4 or newer. macOS ships bash 3.2, so on a Mac use zsh (the default) or `brew install bash`. `whatcmd init` detects the old bash and says so. The bash hook scenarios were skipped in the benchmark.
188
+ - **Benchmarks ran on one machine** (Apple Silicon, macOS 15.6.1, Ollama 0.40.14). Linux is supported by design but not covered by the benchmark.
189
+ - **Slow first `--help` calls** can time out, and then that tool gets no docs for that request. Later calls work. The benchmark's kubectl case hit this once.
190
+ - **Math and text requests** can still come back as `bc` or `echo` commands in some cases.
191
+ - **Conflicts:** the shortcut rebinds Enter in bash (via `bind`) and wraps `accept-line` in zsh. It may conflict with tools that do the same, such as ble.sh.
192
+ - **No shortcut yet** for fish or Windows shells. Use `whatcmd <request>`.
193
+ - **Homebrew packages** are not looked up in the package database (too slow per request). Installed commands are still read via man page or `--help`.
194
+
195
+ ## Documentation
196
+
197
+ | File | What it covers |
198
+ | --- | --- |
199
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | How to set up, test and send a change. |
200
+ | [TESTING.md](TESTING.md) | Unit tests, what they cover, and how to run them. |
201
+ | [EVALS.md](EVALS.md) | The eval and the 500-case benchmark: design, results, and how to reproduce. |
202
+ | [benchmarks/README.md](benchmarks/README.md) | The raw benchmark files: cases, harness, results per run. |
203
+ | [CHANGELOG.md](CHANGELOG.md) | What changed in each release. |
204
+ | [SECURITY.md](SECURITY.md) | Supported versions and how to report a vulnerability. |
205
+ | [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | Expected behaviour in this project. |
206
+ | [LICENSE](LICENSE) | MIT. |
207
+
208
+ ## Development
209
+
210
+ ```bash
211
+ npm ci
212
+ npm test # build + 30 unit tests
213
+ npm run eval # end-to-end eval against your local Ollama
214
+ npm run eval -- --model=qwen2.5-coder:0.5b --no-grounding
215
+ ```
216
+
217
+ See [CONTRIBUTING.md](CONTRIBUTING.md) before you open a pull request.
218
+
219
+ ## License
220
+
221
+ MIT. See [LICENSE](LICENSE).
package/dist/cli.js ADDED
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { ConfigError, DEFAULTS, SETTABLE, configPath, loadConfig, readStored, saveStored, setKey, unsetKey, writeStored, } from "./config.js";
5
+ import { OllamaError } from "./ollama.js";
6
+ import { bold, canPrompt, confirm, cyan, dim, err, red, yellow } from "./ui.js";
7
+ const VERSION = (() => {
8
+ try {
9
+ return JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
10
+ }
11
+ catch {
12
+ return "0.0.0";
13
+ }
14
+ })();
15
+ const HELP = `${bold("whatcmd")} — plain English in, shell command out. Runs locally on Ollama.
16
+
17
+ ${bold("Usage")}
18
+ /w <request> at any bash/zsh prompt (after \`whatcmd init\`): the command
19
+ replaces your line; press Enter again to run it
20
+ whatcmd <request> print the command and offer to run it
21
+ whatcmd init first-time setup: checks Ollama, picks a model, adds /w
22
+ whatcmd doctor check everything works (Ollama, model, speed, shell shortcut)
23
+ whatcmd use <model> switch model (downloads it if needed)
24
+ whatcmd models list downloaded Ollama models
25
+ whatcmd config show settings
26
+ whatcmd config set <key> <value> | get <key> | unset <key> | path | reset
27
+ whatcmd profile [--refresh] show (or re-detect) the system info given to the model
28
+ whatcmd shell bash|zsh print the shell hook (to add it yourself)
29
+ whatcmd uninstall remove the /w shortcut from your shell config
30
+
31
+ ${bold("Options")}
32
+ --raw print only the command (for scripts and the shell hook)
33
+ --debug show grounding, prompts and timings
34
+ --yes, -y accept defaults without asking (doctor/init)
35
+ --model=NAME use this model for one request (or with doctor/init: switch to it)
36
+ -- everything after this is the request
37
+
38
+ ${bold("Environment")}
39
+ WHATCMD_MODEL, WHATCMD_HOST (or OLLAMA_HOST), WHATCMD_CONFIG_DIR, NO_COLOR
40
+
41
+ ${bold("Examples")}
42
+ whatcmd find files bigger than 100MB
43
+ whatcmd "what's using port 8080"
44
+ whatcmd config set model qwen2.5-coder:0.5b
45
+ whatcmd config set notes "prefer ripgrep over grep"
46
+ `;
47
+ function parseArgs(argv) {
48
+ const flags = new Set();
49
+ const values = {};
50
+ const rest = [];
51
+ let literal = false;
52
+ for (const a of argv) {
53
+ if (literal)
54
+ rest.push(a);
55
+ else if (a === "--")
56
+ literal = true;
57
+ else if (/^--[a-z-]+=/.test(a)) {
58
+ const i = a.indexOf("=");
59
+ values[a.slice(2, i)] = a.slice(i + 1);
60
+ }
61
+ else if (/^(--[a-z-]+|-[hvy])$/.test(a))
62
+ flags.add(a);
63
+ else
64
+ rest.push(a);
65
+ }
66
+ return { flags, values, rest, literal };
67
+ }
68
+ /** Subcommands only match their exact shape, so "whatcmd init a git repo" is still a request. */
69
+ function subcommand(rest) {
70
+ const [a, b] = rest;
71
+ const n = rest.length;
72
+ if (["init", "doctor", "models", "profile", "uninstall"].includes(a) && n === 1)
73
+ return a;
74
+ if (a === "config" && (n === 1 || ["list", "get", "set", "unset", "path", "reset"].includes(b)))
75
+ return a;
76
+ if (a === "use" && n === 2)
77
+ return a;
78
+ if (a === "shell" && n === 2 && ["bash", "zsh"].includes(b))
79
+ return a;
80
+ return null;
81
+ }
82
+ function explainOllamaError(e, model) {
83
+ if (e.code === "unreachable")
84
+ return `${e.message}. Is Ollama running? Run: whatcmd doctor`;
85
+ if (e.code === "model_missing")
86
+ return `${e.message}. Run: whatcmd use ${model}`;
87
+ return e.message;
88
+ }
89
+ async function runRequest(ask, opts) {
90
+ const { generate } = await import("./generate.js");
91
+ const cfg = loadConfig();
92
+ if (opts.model)
93
+ cfg.model = opts.model;
94
+ let res;
95
+ try {
96
+ res = await generate(ask, cfg);
97
+ }
98
+ catch (e) {
99
+ if (e instanceof OllamaError) {
100
+ err(`whatcmd: ${explainOllamaError(e, cfg.model)}`);
101
+ return 1;
102
+ }
103
+ throw e;
104
+ }
105
+ if (opts.debug) {
106
+ err(dim(`model: ${cfg.model} timings: ${JSON.stringify(res.debug.timings)}`));
107
+ for (const n of res.debug.groundNotes)
108
+ err(dim(`grounding: ${n}`));
109
+ for (const r of res.debug.refs)
110
+ err(dim(`grounding [${r.name} via ${r.source}]:\n${r.text}`));
111
+ if (res.debug.retries.length)
112
+ err(dim(`retries: ${res.debug.retries.join(", ")}`));
113
+ err(dim(`raw outputs: ${JSON.stringify(res.debug.outputs)}`));
114
+ }
115
+ if (res.kind === "rejected") {
116
+ const why = res.reason === "empty" ? "empty request" : "not a shell task, nothing generated";
117
+ err(opts.raw ? `whatcmd: ${why}` : yellow(`whatcmd: ${why}`));
118
+ return 2;
119
+ }
120
+ if (opts.raw) {
121
+ process.stdout.write(res.command + "\n");
122
+ for (const w of res.warnings)
123
+ err(`whatcmd: ⚠ ${w}`);
124
+ return 0;
125
+ }
126
+ process.stdout.write(bold(cyan(res.command, process.stdout), process.stdout) + "\n");
127
+ for (const w of res.warnings)
128
+ err(yellow(`⚠ ${w}`));
129
+ if (!canPrompt() || !process.stdout.isTTY)
130
+ return 0;
131
+ if (!(await confirm("Run it?", false)))
132
+ return 0;
133
+ const shell = process.env.SHELL || "/bin/sh";
134
+ return spawnSync(shell, ["-c", res.command], { stdio: "inherit" }).status ?? 1;
135
+ }
136
+ async function configCommand(rest) {
137
+ const [, action = "list", key, ...valueParts] = rest;
138
+ const value = valueParts.join(" ");
139
+ if (action === "path") {
140
+ process.stdout.write(configPath() + "\n");
141
+ return 0;
142
+ }
143
+ if (action === "list") {
144
+ const cfg = loadConfig();
145
+ const stored = readStored();
146
+ err(dim(`# ${configPath()}`));
147
+ for (const k of Object.keys(SETTABLE)) {
148
+ const src = process.env[`WHATCMD_${k.toUpperCase()}`] ? "env" : k in stored ? "" : "default";
149
+ process.stdout.write(`${k} = ${JSON.stringify(cfg[k])}${src ? dim(` (${src})`, process.stdout) : ""}\n`);
150
+ }
151
+ return 0;
152
+ }
153
+ if (action === "get") {
154
+ if (!key || !SETTABLE[key])
155
+ throw new ConfigError(`Usage: whatcmd config get <${Object.keys(SETTABLE).join("|")}>`);
156
+ process.stdout.write(String(loadConfig()[key]) + "\n");
157
+ return 0;
158
+ }
159
+ if (action === "set") {
160
+ if (!key || !valueParts.length)
161
+ throw new ConfigError("Usage: whatcmd config set <key> <value>");
162
+ setKey(key, value);
163
+ if (key === "prefix")
164
+ (await import("./shell.js")).refreshHookFiles(value);
165
+ err(`${key} = ${JSON.stringify(loadConfig()[key])}`);
166
+ if (key === "prefix")
167
+ err(dim("Open a new terminal for the new prefix to take effect."));
168
+ if (key === "model") {
169
+ const { listModels, hasModel } = await import("./ollama.js");
170
+ try {
171
+ if (!hasModel(await listModels(loadConfig().host), value))
172
+ err(yellow(`${value} is not downloaded yet. Run: whatcmd use ${value}`));
173
+ }
174
+ catch { }
175
+ }
176
+ return 0;
177
+ }
178
+ if (action === "unset") {
179
+ if (!key)
180
+ throw new ConfigError("Usage: whatcmd config unset <key>");
181
+ unsetKey(key);
182
+ err(`${key} reset to default (${JSON.stringify(DEFAULTS[key])})`);
183
+ return 0;
184
+ }
185
+ if (action === "reset") {
186
+ const { profile } = readStored();
187
+ writeStored(profile ? { profile } : {});
188
+ err("Settings reset to defaults (system profile kept).");
189
+ return 0;
190
+ }
191
+ return 1;
192
+ }
193
+ async function main(argv) {
194
+ const { flags, values, rest, literal } = parseArgs(argv);
195
+ if (flags.has("--version") || flags.has("-v")) {
196
+ process.stdout.write(VERSION + "\n");
197
+ return 0;
198
+ }
199
+ const sub = literal ? null : subcommand(rest);
200
+ if (!sub && (flags.has("--help") || flags.has("-h") || rest.length === 0)) {
201
+ process.stdout.write(HELP);
202
+ return rest.length === 0 && !flags.has("--help") && !flags.has("-h") ? 1 : 0;
203
+ }
204
+ const yes = flags.has("--yes") || flags.has("-y");
205
+ switch (sub) {
206
+ case "init":
207
+ case "doctor": {
208
+ const d = await import("./doctor.js");
209
+ const opts = { yes, model: values.model, refreshProfile: flags.has("--refresh") };
210
+ return (await (sub === "init" ? d.init(opts) : d.doctor(opts))) ? 0 : 1;
211
+ }
212
+ case "config":
213
+ return configCommand(rest);
214
+ case "use": {
215
+ const model = rest[1];
216
+ const { listModels, hasModel } = await import("./ollama.js");
217
+ const cfg = loadConfig();
218
+ if (!hasModel(await listModels(cfg.host), model)) {
219
+ if (!yes && canPrompt() && !(await confirm(`${model} is not downloaded. Download it now?`, true)))
220
+ return 1;
221
+ await (await import("./doctor.js")).downloadModel(cfg.host, model);
222
+ }
223
+ saveStored({ model });
224
+ err(`Now using ${cyan(model)}`);
225
+ return 0;
226
+ }
227
+ case "models": {
228
+ const { listModels, sameModel } = await import("./ollama.js");
229
+ const { formatBytes } = await import("./ui.js");
230
+ const cfg = loadConfig();
231
+ const models = await listModels(cfg.host);
232
+ if (!models.length)
233
+ err("No models downloaded. Run: whatcmd doctor");
234
+ for (const m of models)
235
+ process.stdout.write(`${sameModel(m.name, cfg.model) ? "*" : " "} ${m.name.padEnd(28)} ${formatBytes(m.size)}\n`);
236
+ return 0;
237
+ }
238
+ case "profile": {
239
+ const { detectProfile, profilePrompt } = await import("./env.js");
240
+ let profile = loadConfig().profile;
241
+ if (!profile || flags.has("--refresh")) {
242
+ profile = detectProfile();
243
+ saveStored({ profile });
244
+ }
245
+ process.stdout.write(profilePrompt(profile) + "\n");
246
+ err(dim(`detected ${profile.detectedAt}; re-detect with: whatcmd profile --refresh`));
247
+ return 0;
248
+ }
249
+ case "shell": {
250
+ const { hookScript } = await import("./shell.js");
251
+ process.stdout.write(hookScript(rest[1], loadConfig().prefix));
252
+ return 0;
253
+ }
254
+ case "uninstall": {
255
+ const { uninstallHook } = await import("./shell.js");
256
+ const { detectProfile } = await import("./env.js");
257
+ const osName = (loadConfig().profile ?? detectProfile()).os;
258
+ const removed = ["bash", "zsh"].filter((s) => uninstallHook(s, osName));
259
+ err(removed.length ? `Removed the shortcut from your ${removed.join(" and ")} config.` : "No shortcut was installed.");
260
+ err(dim("To remove whatcmd itself: npm uninstall -g whatcmd"));
261
+ return 0;
262
+ }
263
+ }
264
+ return runRequest(rest.join(" "), { raw: flags.has("--raw"), debug: flags.has("--debug"), model: values.model });
265
+ }
266
+ main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
267
+ if (e instanceof ConfigError)
268
+ err(`whatcmd: ${e.message}`);
269
+ else if (e instanceof OllamaError)
270
+ err(`whatcmd: ${explainOllamaError(e, loadConfig().model)}`);
271
+ else
272
+ err(red(`whatcmd: ${e?.stack ?? e}`));
273
+ process.exit(1);
274
+ });
package/dist/config.js ADDED
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export const DEFAULT_MODEL = "qwen2.5-coder:1.5b";
5
+ export const DEFAULTS = {
6
+ model: DEFAULT_MODEL,
7
+ host: "http://127.0.0.1:11434",
8
+ temperature: 0,
9
+ keepAlive: "2h",
10
+ timeoutMs: 30000,
11
+ prefix: "/w",
12
+ grounding: true,
13
+ notes: "",
14
+ };
15
+ export const SETTABLE = {
16
+ model: { type: "string", help: "Ollama model to use" },
17
+ host: { type: "string", help: "Ollama server URL" },
18
+ temperature: { type: "number", help: "0 = deterministic (recommended)" },
19
+ keepAlive: { type: "string", help: 'keep model loaded: "2h", "30m", "-1" = forever' },
20
+ timeoutMs: { type: "number", help: "max time for one model call" },
21
+ prefix: { type: "string", help: "shell trigger, default /w" },
22
+ grounding: { type: "boolean", help: "use local docs of tools you mention" },
23
+ notes: { type: "string", help: "extra instructions added to every prompt" },
24
+ };
25
+ export function configDir() {
26
+ return (process.env.WHATCMD_CONFIG_DIR ||
27
+ path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "whatcmd"));
28
+ }
29
+ export function cacheDir() {
30
+ return (process.env.WHATCMD_CACHE_DIR ||
31
+ path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "whatcmd"));
32
+ }
33
+ export const configPath = () => path.join(configDir(), "config.json");
34
+ /** Accepts the same forms as OLLAMA_HOST: "host", "host:port", ":port", "http(s)://host[:port]". */
35
+ export function normalizeHost(raw) {
36
+ let s = raw.trim().replace(/\/+$/, "");
37
+ const hadScheme = /^https?:\/\//i.test(s);
38
+ if (!hadScheme)
39
+ s = "http://" + (s.startsWith(":") ? "127.0.0.1" + s : s);
40
+ const u = new URL(s);
41
+ if (u.hostname === "0.0.0.0" || u.hostname === "")
42
+ u.hostname = "127.0.0.1";
43
+ if (!hadScheme && !u.port)
44
+ u.port = "11434";
45
+ return u.toString().replace(/\/+$/, "");
46
+ }
47
+ export function readStored() {
48
+ try {
49
+ return JSON.parse(fs.readFileSync(configPath(), "utf8"));
50
+ }
51
+ catch {
52
+ return {};
53
+ }
54
+ }
55
+ export function writeStored(next) {
56
+ fs.mkdirSync(configDir(), { recursive: true });
57
+ fs.writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
58
+ }
59
+ export function saveStored(patch) {
60
+ writeStored({ ...readStored(), ...patch });
61
+ }
62
+ /** Effective config: defaults < config file < environment variables. */
63
+ export function loadConfig() {
64
+ const stored = readStored();
65
+ const c = { ...DEFAULTS, ...stored };
66
+ if (!stored.host && process.env.OLLAMA_HOST)
67
+ c.host = process.env.OLLAMA_HOST;
68
+ if (process.env.WHATCMD_HOST)
69
+ c.host = process.env.WHATCMD_HOST;
70
+ if (process.env.WHATCMD_MODEL)
71
+ c.model = process.env.WHATCMD_MODEL;
72
+ try {
73
+ c.host = normalizeHost(c.host);
74
+ }
75
+ catch {
76
+ c.host = DEFAULTS.host;
77
+ }
78
+ return c;
79
+ }
80
+ export class ConfigError extends Error {
81
+ }
82
+ export function coerce(key, raw) {
83
+ const spec = SETTABLE[key];
84
+ if (!spec)
85
+ throw new ConfigError(`Unknown key "${key}". Keys: ${Object.keys(SETTABLE).join(", ")}`);
86
+ if (spec.type === "number") {
87
+ const n = Number(raw);
88
+ if (!Number.isFinite(n))
89
+ throw new ConfigError(`${key} must be a number`);
90
+ if (key === "temperature" && (n < 0 || n > 2))
91
+ throw new ConfigError("temperature must be between 0 and 2");
92
+ if (key === "timeoutMs" && n < 500)
93
+ throw new ConfigError("timeoutMs must be at least 500");
94
+ return n;
95
+ }
96
+ if (spec.type === "boolean") {
97
+ if (/^(true|on|yes|1)$/i.test(raw))
98
+ return true;
99
+ if (/^(false|off|no|0)$/i.test(raw))
100
+ return false;
101
+ throw new ConfigError(`${key} must be true or false`);
102
+ }
103
+ if (key === "host")
104
+ return normalizeHost(raw);
105
+ if (key === "prefix" && !/^[^\s'"\\`$]+$/.test(raw))
106
+ throw new ConfigError("prefix must be one word without spaces, quotes, $ or backslashes");
107
+ if (key === "model" && !raw.trim())
108
+ throw new ConfigError("model cannot be empty");
109
+ return raw;
110
+ }
111
+ export function setKey(key, raw) {
112
+ saveStored({ [key]: coerce(key, raw) });
113
+ }
114
+ export function unsetKey(key) {
115
+ if (!SETTABLE[key])
116
+ throw new ConfigError(`Unknown key "${key}"`);
117
+ const s = readStored();
118
+ delete s[key];
119
+ writeStored(s);
120
+ }