kronk-cli 0.1.2 → 0.1.3
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 +52 -1
- package/package.json +1 -1
- package/src/boot.js +59 -0
- package/src/client.js +22 -0
- package/src/config.js +3 -0
- package/src/index.js +16 -9
package/README.md
CHANGED
|
@@ -50,6 +50,16 @@ workflow run that produced it:
|
|
|
50
50
|
gh attestation verify kronk-cli-*.tgz --repo BardiaN/kronk-cli
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
The same attestation is attached to every release as a file, so it can be checked without
|
|
54
|
+
GitHub's attestations API in the loop — `<tarball>.sigstore.json` for `gh attestation verify`
|
|
55
|
+
and cosign, `<tarball>.intoto.jsonl` for SLSA tooling:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
gh release download v0.1.3 --repo BardiaN/kronk-cli
|
|
59
|
+
gh attestation verify kronk-cli-0.1.3.tgz --repo BardiaN/kronk-cli \
|
|
60
|
+
--bundle kronk-cli-0.1.3.tgz.sigstore.json
|
|
61
|
+
```
|
|
62
|
+
|
|
53
63
|
npm packages carry the same provenance, shown as a **Provenance** panel on the
|
|
54
64
|
[package page](https://www.npmjs.com/package/kronk-cli), and verifiable locally:
|
|
55
65
|
|
|
@@ -315,6 +325,45 @@ kronk-cli --auto "make the tests pass" # unattended, runs the whole task
|
|
|
315
325
|
|
|
316
326
|
---
|
|
317
327
|
|
|
328
|
+
## Startup: the model is loaded before you type
|
|
329
|
+
|
|
330
|
+
Kronk has no load command. It lists a model in `GET /v1/models` as soon as the
|
|
331
|
+
server starts, but the weights only reach VRAM on the first inference request —
|
|
332
|
+
so on a fresh server the first prompt you type pays a 10–30 s cold load, and
|
|
333
|
+
looks like a hang.
|
|
334
|
+
|
|
335
|
+
`kronk-cli` pays it at boot instead. It asks Kronk what is resident, and if the
|
|
336
|
+
selected model is not, sends the cheapest completion there is — one token, no
|
|
337
|
+
reasoning — to trigger admission:
|
|
338
|
+
|
|
339
|
+
```console
|
|
340
|
+
$ kronk-cli
|
|
341
|
+
loaded unsloth/Qwen3.6-35B-A3B-UD-Q4_K_M/AGENT · 11.4s
|
|
342
|
+
|
|
343
|
+
██ kronk-cli · local agent, no network
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
A model already in the pool is left alone; nothing is sent. If the selected one
|
|
347
|
+
cannot be admitted — it will not fit next to what is already resident — the
|
|
348
|
+
fallback runs the same order the CLI uses when you name nothing: the configured
|
|
349
|
+
default, then the best id Kronk is serving. Each is tried once, and a failure
|
|
350
|
+
says why:
|
|
351
|
+
|
|
352
|
+
```console
|
|
353
|
+
$ kronk-cli -m Qwen3.6-27B
|
|
354
|
+
unsloth/Qwen3.6-27B-Q4_K_M failed to load — 507 /chat/completions — insufficient VRAM
|
|
355
|
+
loaded unsloth/Qwen3.6-35B-A3B-UD-Q4_K_M/AGENT · 2.0s · fallback
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
If nothing loads, the original pick stands and the first turn reports the real
|
|
359
|
+
error. A warm-up is a convenience, not a gate — it never decides whether the CLI
|
|
360
|
+
starts.
|
|
361
|
+
|
|
362
|
+
Skip it with `--no-warm` or `KRONK_WARM=false` and the first prompt pays the load,
|
|
363
|
+
as before.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
318
367
|
## Command-line options
|
|
319
368
|
|
|
320
369
|
| Flag | Default | |
|
|
@@ -323,6 +372,7 @@ kronk-cli --auto "make the tests pass" # unattended, runs the whole task
|
|
|
323
372
|
| `-l`, `--models`, `--list` | — | List the models Kronk is serving, then exit |
|
|
324
373
|
| `--no-context` | off | Skip the startup scan of the working directory |
|
|
325
374
|
| `--no-compact` | off | Never auto-compact; fail when the window fills instead |
|
|
375
|
+
| `--no-warm` | off | Don't preload the model at startup; let the first prompt trigger the load |
|
|
326
376
|
| `--mcp [names]` | off | Attach MCP servers — bare for all, or a comma list |
|
|
327
377
|
| `--mcp-list` | — | Show configured MCP servers and their tools, then exit |
|
|
328
378
|
| `-a`, `--auto` | off | Autonomous: auto-approve tools **and** run until the task is done. Implies `--yes` |
|
|
@@ -391,6 +441,7 @@ The per-turn usage line still prints after each response; this one is the runnin
|
|
|
391
441
|
| `KRONK_SANDBOX_DENY` | — | Extra paths to hide from `bash`, comma or colon separated |
|
|
392
442
|
| `KRONK_DISTILL` | `true` | `false` disables tool-output distillation |
|
|
393
443
|
| `KRONK_DISTILL_AT` | `8000` | Characters of output that trigger distillation |
|
|
444
|
+
| `KRONK_WARM` | `true` | `false` skips the boot-time model preload |
|
|
394
445
|
| `KRONK_AUTO_COMPACT` | `true` | `false` disables automatic compaction |
|
|
395
446
|
| `KRONK_COMPACT_AT` | `0.85` | Fraction of the window that triggers compaction |
|
|
396
447
|
| `NO_COLOR` | — | Any value disables colour |
|
|
@@ -847,7 +898,7 @@ previous prompt prefix — watch `cached` climb in the usage line.
|
|
|
847
898
|
|---|---|
|
|
848
899
|
| `Cannot reach Kronk` | `kronk server start --detach` |
|
|
849
900
|
| `Kronk is running but has no models` | `kronk model pull <id>` |
|
|
850
|
-
| First response takes ~25 s | Cold model load
|
|
901
|
+
| First response takes ~25 s | Cold model load, and you started with `--no-warm`. Drop the flag, or keep the model warm with `--pool-ttl 1h` on the server |
|
|
851
902
|
| Long silence before text | The model is reasoning. `--no-think`, or `/thinking` to watch it |
|
|
852
903
|
| `(model produced no answer)` | Reasoning consumed the whole budget. Raise `KRONK_MAX_TOKENS` or use `--no-think` |
|
|
853
904
|
| `kronk-cli: command not found` after an nvm switch | Re-run `npm link`, or see [Using nvm?](#using-nvm) |
|
package/package.json
CHANGED
package/src/boot.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { config, DEFAULT_MODEL } from './config.js';
|
|
2
|
+
import { listLoaded, warm } from './client.js';
|
|
3
|
+
import { c, spinner } from './ui.js';
|
|
4
|
+
|
|
5
|
+
/** Last resort when neither the flag nor DEFAULT_MODEL is being served. */
|
|
6
|
+
export function pickDefault(ids) {
|
|
7
|
+
const chat = ids.filter((id) => !/embedding|rerank/i.test(id));
|
|
8
|
+
const agent = chat.filter((id) => id.endsWith('/AGENT'));
|
|
9
|
+
const pool = agent.length ? agent : chat;
|
|
10
|
+
return pool.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the chosen model resident before the first prompt.
|
|
15
|
+
*
|
|
16
|
+
* A freshly started Kronk serves model *ids* but holds nothing in VRAM — it
|
|
17
|
+
* admits a model on its first inference request, and there is no endpoint that
|
|
18
|
+
* does it sooner. Left alone, that 10–25 s cold load lands on the first thing
|
|
19
|
+
* you type, looking like a hang. Do it here, where a spinner explains the wait
|
|
20
|
+
* and where a model that will not fit can still fall back to one that will.
|
|
21
|
+
*
|
|
22
|
+
* Never fatal: if nothing warms, the original pick stands and the first turn
|
|
23
|
+
* reports the real error. A warm-up is a convenience, not a gate.
|
|
24
|
+
*
|
|
25
|
+
* Returns the id left in `config.model`.
|
|
26
|
+
*/
|
|
27
|
+
export async function ensureLoaded(ids, log = console.error) {
|
|
28
|
+
const loaded = await listLoaded();
|
|
29
|
+
const resident = new Set((Array.isArray(loaded) ? loaded : []).map((l) => l.id));
|
|
30
|
+
if (resident.has(config.model)) return config.model;
|
|
31
|
+
|
|
32
|
+
const chosen = config.model;
|
|
33
|
+
// chosen → configured default → best guess; each distinct id tried once.
|
|
34
|
+
const chain = [...new Set([
|
|
35
|
+
chosen,
|
|
36
|
+
ids.includes(DEFAULT_MODEL) ? DEFAULT_MODEL : null,
|
|
37
|
+
pickDefault(ids),
|
|
38
|
+
])].filter(Boolean);
|
|
39
|
+
|
|
40
|
+
for (const id of chain) {
|
|
41
|
+
if (resident.has(id)) { config.model = id; return id; }
|
|
42
|
+
const t0 = Date.now();
|
|
43
|
+
const spin = spinner(`loading ${id.split('/').pop()} — first run takes 10-30s`);
|
|
44
|
+
try {
|
|
45
|
+
await warm(id);
|
|
46
|
+
spin.stop();
|
|
47
|
+
const how = `${((Date.now() - t0) / 1000).toFixed(1)}s${id === chosen ? '' : ' · fallback'}`;
|
|
48
|
+
log(c.grey(` loaded ${id} · ${how}`));
|
|
49
|
+
config.model = id;
|
|
50
|
+
return id;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
spin.stop();
|
|
53
|
+
log(c.yellow(` ${id} failed to load — ${e.message.split('\n')[0].slice(0, 160)}`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Nothing would load. Keep the original pick and let the first turn say why.
|
|
57
|
+
config.model = chosen;
|
|
58
|
+
return chosen;
|
|
59
|
+
}
|
package/src/client.js
CHANGED
|
@@ -113,3 +113,25 @@ export async function* streamChat({ model, messages, tools, signal, maxTokens, n
|
|
|
113
113
|
|
|
114
114
|
yield { type: 'done', calls: [...calls.values()], finish };
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load a model into the pool.
|
|
119
|
+
*
|
|
120
|
+
* Kronk has no explicit "load" endpoint — admission happens on the first
|
|
121
|
+
* inference request, so the cheapest possible completion *is* the load
|
|
122
|
+
* command. A 23 GB MoE takes ~10–25 s off disk. The reply is discarded;
|
|
123
|
+
* only whether it succeeded matters.
|
|
124
|
+
*/
|
|
125
|
+
export async function warm(id, signal) {
|
|
126
|
+
const res = await req('/chat/completions', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
signal,
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
model: id,
|
|
131
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
132
|
+
max_completion_tokens: 1,
|
|
133
|
+
enable_thinking: false,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
await res.text();
|
|
137
|
+
}
|
package/src/config.js
CHANGED
|
@@ -23,6 +23,9 @@ export const config = {
|
|
|
23
23
|
maxSteps: Number(process.env.KRONK_MAX_STEPS ?? file.maxSteps ?? Infinity),
|
|
24
24
|
showThinking: (process.env.KRONK_THINKING ?? String(file.showThinking ?? 'true')) !== 'false',
|
|
25
25
|
noThink: (process.env.KRONK_NO_THINK ?? String(file.noThink ?? '')) === '1',
|
|
26
|
+
// Kronk admits a model on its first inference request, not at server start.
|
|
27
|
+
// Pay that cold load at boot rather than on the first typed prompt.
|
|
28
|
+
warm: (process.env.KRONK_WARM ?? String(file.warm ?? 'true')) !== 'false',
|
|
26
29
|
autoCompact: (process.env.KRONK_AUTO_COMPACT ?? String(file.autoCompact ?? 'true')) !== 'false',
|
|
27
30
|
compactAt: Number(process.env.KRONK_COMPACT_AT ?? file.compactAt ?? 0.85),
|
|
28
31
|
// Large tool output is summarized in a throwaway context so the raw text
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { stdin, stdout } from 'node:process';
|
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { config, DEFAULT_MODEL, warnIfInsecure } from './config.js';
|
|
6
6
|
import { listModels, listModelDetails, listLoaded, modelLimits, tokenize } from './client.js';
|
|
7
|
+
import { pickDefault, ensureLoaded } from './boot.js';
|
|
7
8
|
import { runTurn, SYSTEM, SYSTEM_AUTO } from './agent.js';
|
|
8
9
|
import { c, banner, fmtContext, statusLine } from './ui.js';
|
|
9
10
|
import { projectContext } from './context.js';
|
|
@@ -29,6 +30,7 @@ const SHOW_MODELS = flag('--models', '-l', '--list');
|
|
|
29
30
|
const SHOW_MCP = flag('--mcp-list');
|
|
30
31
|
const NO_CONTEXT = flag('--no-context');
|
|
31
32
|
if (flag('--no-compact')) config.autoCompact = false;
|
|
33
|
+
if (flag('--no-warm')) config.warm = false;
|
|
32
34
|
|
|
33
35
|
if (flag('-h', '--help')) {
|
|
34
36
|
console.log(`
|
|
@@ -43,6 +45,7 @@ if (flag('-h', '--help')) {
|
|
|
43
45
|
-l, --models list the models Kronk is serving, then exit
|
|
44
46
|
--no-context skip the startup scan of the working directory
|
|
45
47
|
--no-compact never auto-compact; fail instead when the window fills
|
|
48
|
+
--no-warm don't preload the model; let the first prompt trigger it
|
|
46
49
|
--mcp [names] attach MCP servers; bare for all, or a comma list
|
|
47
50
|
--mcp-list show configured MCP servers and their tools, then exit
|
|
48
51
|
-m, --model <id> model to use; substring is enough, /AGENT profiles win
|
|
@@ -60,6 +63,7 @@ if (flag('-h', '--help')) {
|
|
|
60
63
|
KRONK_MAX_TOKENS output cap per response (default 8192)
|
|
61
64
|
KRONK_MAX_STEPS cap on tool calls per task (default unlimited)
|
|
62
65
|
KRONK_NO_THINK set to 1 to disable reasoning
|
|
66
|
+
KRONK_WARM false to skip the boot-time model preload
|
|
63
67
|
KRONK_AUTO_COMPACT false to disable automatic compaction
|
|
64
68
|
KRONK_COMPACT_AT fraction of the window that triggers it (default 0.85)
|
|
65
69
|
|
|
@@ -96,14 +100,6 @@ let MCP_WANTED = null;
|
|
|
96
100
|
const stepsArg = opt('--steps');
|
|
97
101
|
if (stepsArg) config.maxSteps = /^(0|off|none|inf|unlimited)$/i.test(stepsArg) ? Infinity : Number(stepsArg);
|
|
98
102
|
|
|
99
|
-
/** Last resort when neither the flag nor DEFAULT_MODEL is being served. */
|
|
100
|
-
function pickDefault(ids) {
|
|
101
|
-
const chat = ids.filter((id) => !/embedding|rerank/i.test(id));
|
|
102
|
-
const agent = chat.filter((id) => id.endsWith('/AGENT'));
|
|
103
|
-
const pool = agent.length ? agent : chat;
|
|
104
|
-
return pool.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
103
|
async function boot() {
|
|
108
104
|
let ids;
|
|
109
105
|
try {
|
|
@@ -139,6 +135,8 @@ async function boot() {
|
|
|
139
135
|
}
|
|
140
136
|
if (!config.model) config.model = pickDefault(ids);
|
|
141
137
|
|
|
138
|
+
if (config.warm) await ensureLoaded(ids);
|
|
139
|
+
|
|
142
140
|
const { configured, native } = await modelLimits(config.model);
|
|
143
141
|
config.contextWindow = configured;
|
|
144
142
|
config.nativeContext = native;
|
|
@@ -343,7 +341,16 @@ async function main() {
|
|
|
343
341
|
// Without one, stdin IS the prompt, so wait longer before giving up.
|
|
344
342
|
const piped = await readStdin(inline ? 200 : 10_000);
|
|
345
343
|
const oneShotPrompt = inline && piped ? `${inline}\n\n${piped}` : (inline || piped);
|
|
346
|
-
if (oneShotPrompt) {
|
|
344
|
+
if (oneShotPrompt) {
|
|
345
|
+
// stdin has given us everything it is going to. When it is a pipe the
|
|
346
|
+
// caller never closes — a script, an editor task, a CI step — the read
|
|
347
|
+
// above stays pending and its handle would keep the process alive long
|
|
348
|
+
// after the answer was printed. Let go of it before answering.
|
|
349
|
+
stdin.pause();
|
|
350
|
+
stdin.unref?.();
|
|
351
|
+
await oneShot(oneShotPrompt);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
347
354
|
|
|
348
355
|
const rl = readline.createInterface({ input: stdin, output: stdout, historySize: 500 });
|
|
349
356
|
await boot();
|