klyro 1.0.4 → 1.0.5
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 +29 -0
- package/dist/agent/runtime.js +11 -2
- package/dist/cli/auth.js +11 -3
- package/dist/cli/completion.js +63 -10
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/repl.js +116 -10
- package/dist/cli/run.js +14 -0
- package/dist/cli/slash/parser.d.ts +7 -1
- package/dist/cli/slash/parser.js +37 -10
- package/dist/cli/update.d.ts +8 -4
- package/dist/cli/update.js +50 -7
- package/dist/context/accounting.d.ts +8 -0
- package/dist/context/accounting.js +18 -1
- package/dist/context/compaction.d.ts +1 -0
- package/dist/context/compaction.js +2 -1
- package/dist/index.js +78 -8
- package/dist/mcp/config.d.ts +7 -0
- package/dist/mcp/config.js +45 -0
- package/dist/mcp/registry.js +6 -4
- package/dist/mcp/serve.d.ts +23 -0
- package/dist/mcp/serve.js +111 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +14 -1
- package/dist/policy/secret-redactor.js +4 -1
- package/dist/shared/error-map.d.ts +19 -0
- package/dist/shared/error-map.js +58 -0
- package/dist/tools/lsp/diagnostics.d.ts +35 -4
- package/dist/tools/lsp/diagnostics.js +88 -9
- package/dist/tools/normalize.d.ts +3 -0
- package/dist/tools/normalize.js +8 -5
- package/dist/tools/search/dependencies.d.ts +2 -2
- package/dist/tools/shell/background.d.ts +6 -0
- package/dist/tools/shell/background.js +17 -0
- package/dist/tools/symbols/find-symbol.d.ts +1 -1
- package/dist/tools/symbols/find-symbol.js +8 -6
- package/dist/tools/types.d.ts +8 -1
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +324 -50
- package/dist/tui/app.test.js +42 -3
- package/dist/tui/markdown.js +9 -0
- package/dist/tui/mouse.d.ts +26 -1
- package/dist/tui/mouse.js +104 -6
- package/dist/tui/scroll-flow.test.js +3 -1
- package/dist/tui/tokens.d.ts +6 -6
- package/dist/tui/tokens.js +9 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,35 @@ node dist/index.js chat "Explain TypeScript in 2 sentences"
|
|
|
26
26
|
node dist/index.js chat
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## Exit codes
|
|
30
|
+
|
|
31
|
+
| Code | Meaning |
|
|
32
|
+
|---|---|
|
|
33
|
+
| 0 | Success / complete |
|
|
34
|
+
| 1 | Unexpected failure (last-resort handler) |
|
|
35
|
+
| 2 | Usage / config error, policy refusal to commit, unknown command or option |
|
|
36
|
+
| 3 | Config invalid / not found |
|
|
37
|
+
| 4 | Provider error (auth, rate-limit, timeout) |
|
|
38
|
+
| 5 | No final answer from provider |
|
|
39
|
+
| 7 | Stopped: max steps, cost/time limit, or stuck |
|
|
40
|
+
| 8 | Verification failed (or `--require-verify` unsatisfied) |
|
|
41
|
+
| 130 | Aborted (Ctrl+C / Esc×2 / /cancel / SIGINT) |
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
| Variable | Scope |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `KLYRO_BASE_URL`, `KLYRO_API_KEY`, `KLYRO_MODEL`, `KLYRO_PROVIDER` | Provider selection |
|
|
48
|
+
| `KLYRO_CONFIG` / `--config` | Config file override |
|
|
49
|
+
| `KLYRO_YES` / `--yes` | **Commit only** — auto-approves `klyro commit` prompts; nothing else reads it |
|
|
50
|
+
| `KLYRO_NO_UPDATE_CHECK=1` | Disables the 24h update check |
|
|
51
|
+
| `KLYRO_ALLOW_MAIN_PUSH=1` | Per-risk escape for protected-branch push |
|
|
52
|
+
| `KLYRO_CREDENTIALS_INSECURE_OK=1` | Warn (don't refuse) on group-readable credentials |
|
|
53
|
+
| `KLYRO_LSP=0` | Force language tools off |
|
|
54
|
+
| `KLYRO_SYMBOLS=0` | Force `find_symbol` off |
|
|
55
|
+
| `KLYRO_WORKER=0` | Disable subprocess isolation for subagents |
|
|
56
|
+
| `KLYRO_SESSIONS_DIR`, `KLYRO_UPDATE_CACHE`, `KLYRO_CREDENTIALS_FILE` | Relocatable state (tests + power users) |
|
|
57
|
+
|
|
29
58
|
## Documentation
|
|
30
59
|
|
|
31
60
|
| Doc | Purpose |
|
package/dist/agent/runtime.js
CHANGED
|
@@ -23,11 +23,13 @@ import { verify, diagnosticForModel } from '../verification/engine.js';
|
|
|
23
23
|
import { detectVerifyCommand } from '../verification/auto.js';
|
|
24
24
|
import { ensureBaseline, getBaseline } from '../verification/baseline.js';
|
|
25
25
|
import { compressTranscript, totalTokens, calibrateEstimate, transcriptCharLength } from '../context/tokenizer.js';
|
|
26
|
+
import { capForModel } from '../context/accounting.js';
|
|
26
27
|
import { ratesFor, isAnthropicModel } from '../providers/model-info.js';
|
|
27
28
|
import { classifyFailure, rerunOnce, gatherRepairContext, guardRepair } from '../verification/classify.js';
|
|
28
29
|
import { findRelatedTests, buildScopedCommand, runScopedVerify, syntaxCheck, checkImports } from '../verification/scoped.js';
|
|
29
30
|
import { globalBus } from '../events/bus.js';
|
|
30
31
|
import { TraceWriter } from '../trace/writer.js';
|
|
32
|
+
import { killAllJobs } from '../tools/shell/background.js';
|
|
31
33
|
import { loadHooks, runHook } from '../cli/hooks.js';
|
|
32
34
|
/** Normalize either systemPrompt shape into {system, suffix}. */
|
|
33
35
|
export function resolveSystemPrompt(fn, ctx) {
|
|
@@ -267,6 +269,9 @@ export async function run(opts, deps) {
|
|
|
267
269
|
}
|
|
268
270
|
if (opts.signal?.aborted) {
|
|
269
271
|
emit?.({ kind: 'aborted' });
|
|
272
|
+
// Abort cascade (fix: background shells must not outlive the run).
|
|
273
|
+
const killed = killAllJobs();
|
|
274
|
+
emitKlyro({ type: 'abort', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', reason: killed.length > 0 ? `aborted by operator (${killed.length} background job(s) killed)` : 'aborted by operator' });
|
|
270
275
|
if (store && sessionId) {
|
|
271
276
|
try {
|
|
272
277
|
await store.setStatus(sessionId, 'aborted', finalText);
|
|
@@ -297,7 +302,9 @@ export async function run(opts, deps) {
|
|
|
297
302
|
// Budget accounting sees what the model sees (prefix + suffix); the
|
|
298
303
|
// request itself keeps the halves split for cache-friendly adapters.
|
|
299
304
|
const systemForBudget = telemetrySuffix ? `${stableSystem}\n\n${telemetrySuffix}` : stableSystem;
|
|
300
|
-
|
|
305
|
+
// Window-aware ceiling (was a hardcoded 120k that overflowed 8k local
|
|
306
|
+
// models): size the input budget to the model's context window.
|
|
307
|
+
const BUDGET = { total: capForModel(opts.model, 4000), reservedOutput: 4000 };
|
|
301
308
|
let reqMessages = transcript;
|
|
302
309
|
let reqSystem = stableSystem;
|
|
303
310
|
let reqSuffix = telemetrySuffix;
|
|
@@ -530,6 +537,8 @@ export async function run(opts, deps) {
|
|
|
530
537
|
if (opts.signal?.aborted) {
|
|
531
538
|
finalText = textBuf;
|
|
532
539
|
emit?.({ kind: 'aborted' });
|
|
540
|
+
const killed = killAllJobs();
|
|
541
|
+
emitKlyro({ type: 'abort', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', reason: killed.length > 0 ? `aborted by operator (${killed.length} background job(s) killed)` : 'aborted by operator' });
|
|
533
542
|
if (store && sessionId) {
|
|
534
543
|
try {
|
|
535
544
|
await store.setStatus(sessionId, 'aborted', finalText);
|
|
@@ -824,7 +833,7 @@ export async function run(opts, deps) {
|
|
|
824
833
|
// results immediately — gate runs in call order so these stay ordered.
|
|
825
834
|
// Returns true when the call is approved for execution.
|
|
826
835
|
const gateCall = async (call) => {
|
|
827
|
-
const decision = await deps.policy.evaluate({ name: call.name, input: call.input }, { cwd: opts.cwd, nonInteractive: opts.nonInteractive });
|
|
836
|
+
const decision = await deps.policy.evaluate({ name: call.name, input: call.input, permission: deps.registry.get(call.name)?.permission }, { cwd: opts.cwd, nonInteractive: opts.nonInteractive });
|
|
828
837
|
emit?.({ kind: 'policy_decision', id: call.id, name: call.name, action: decision.action, ...(decision.action !== 'allow' ? { reason: decision.reason } : {}) });
|
|
829
838
|
// Mirror to KlyroEvent bus
|
|
830
839
|
if (decision.action === 'allow') {
|
package/dist/cli/auth.js
CHANGED
|
@@ -132,13 +132,21 @@ export async function runLogout(provider) {
|
|
|
132
132
|
}
|
|
133
133
|
export function getStoredKey(provider) {
|
|
134
134
|
try {
|
|
135
|
-
//
|
|
136
|
-
//
|
|
135
|
+
// Refuse (don't just warn) when the credentials file is
|
|
136
|
+
// group/other-readable: a key other users can read is compromised by
|
|
137
|
+
// definition. Repair hint included. KLYRO_CREDENTIALS_INSECURE_OK=1
|
|
138
|
+
// preserves the old warn-and-continue behavior for exotic setups.
|
|
137
139
|
if (process.platform !== 'win32') {
|
|
138
140
|
try {
|
|
139
141
|
const st = fsSync.statSync(credPath());
|
|
140
142
|
if ((st.mode & 0o077) !== 0) {
|
|
141
|
-
process.
|
|
143
|
+
if (process.env.KLYRO_CREDENTIALS_INSECURE_OK === '1') {
|
|
144
|
+
process.stderr.write(`warning: credentials file ${credPath()} is group/other-readable (mode ${(st.mode & 0o777).toString(8)}) — run chmod 600 on it\n`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
process.stderr.write(`klyro: refusing to use group/other-readable credentials file ${credPath()} (mode ${(st.mode & 0o777).toString(8)}) — run: chmod 600 ${credPath()} (or set KLYRO_CREDENTIALS_INSECURE_OK=1 to override)\n`);
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
catch { /* missing file → no warning */ }
|
package/dist/cli/completion.js
CHANGED
|
@@ -2,40 +2,93 @@
|
|
|
2
2
|
* 1.2 — klyro completion
|
|
3
3
|
* Generates shell completion scripts for bash/zsh/fish/powershell.
|
|
4
4
|
*/
|
|
5
|
-
const COMMANDS = ['tui', 'run', 'chat', 'config', 'doctor', 'completion', 'update', 'eval', 'session', 'resume', 'help', 'version'];
|
|
5
|
+
const COMMANDS = ['tui', 'run', 'chat', 'config', 'doctor', 'completion', 'update', 'eval', 'session', 'resume', 'help', 'version', 'scan', 'project', 'mcp', 'hooks', 'agents', 'commit', 'audit', 'benchmark', 'sessions', 'login', 'logout'];
|
|
6
|
+
/** Second-level completion: global flags + per-command flags. */
|
|
7
|
+
const GLOBAL_FLAGS = ['--cwd', '--config', '--debug', '--verbose', '--quiet', '--json', '--yes', '--no-color', '--print', '--output-format', '--no-stream', '--show-thinking', '--tui', '--chat', '--continue', '--resume', '--help', '--version'];
|
|
8
|
+
const COMMAND_FLAGS = {
|
|
9
|
+
run: ['-m', '--model', '--max-steps', '--max-tokens', '--temperature', '--timeout', '--base-url', '--api-key', '--output', '--provider', '--dry-run', '--resume', '--resume-session', '--verify', '--verify-command', '--verify-mode', '--max-repairs', '--persist', '--require-verify', '--agent', '--max-depth'],
|
|
10
|
+
chat: ['-s', '--system', '-m', '--model', '-t', '--timeout'],
|
|
11
|
+
eval: ['--output', '--suite', '--filter', '--runs', '--parallel', '--model'],
|
|
12
|
+
tui: ['-m', '--model', '--max-steps'],
|
|
13
|
+
doctor: ['--json'],
|
|
14
|
+
session: ['list', 'show', 'resume', 'fork', 'delete'],
|
|
15
|
+
sessions: ['export', 'import', 'fork', 'delete'],
|
|
16
|
+
mcp: ['list', 'add', 'remove', 'probe', 'serve'],
|
|
17
|
+
config: ['list', 'get', 'set', 'unset', 'path'],
|
|
18
|
+
commit: ['--dry-run', '--message', '--force-secret'],
|
|
19
|
+
completion: ['bash', 'zsh', 'fish', 'powershell'],
|
|
20
|
+
resume: ['-m', '--model', '--max-steps'],
|
|
21
|
+
};
|
|
22
|
+
function flagsFor(cmd) {
|
|
23
|
+
return [...GLOBAL_FLAGS, ...(COMMAND_FLAGS[cmd] ?? [])];
|
|
24
|
+
}
|
|
6
25
|
function bashScript() {
|
|
7
|
-
|
|
26
|
+
const cmdCases = Object.entries(COMMAND_FLAGS)
|
|
27
|
+
.map(([c, fs]) => ` ${c}) opts="${fs.join(' ')}" ;;`)
|
|
28
|
+
.join('\n');
|
|
29
|
+
return `# klyro bash completion (commands + flags)
|
|
8
30
|
_klyro_complete() {
|
|
9
31
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
32
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
10
33
|
local cmds="${COMMANDS.join(' ')}"
|
|
11
|
-
|
|
34
|
+
if [[ $COMP_CWORD -eq 1 ]]; then
|
|
35
|
+
COMPREPLY=( $(compgen -W "$cmds ${GLOBAL_FLAGS.join(' ')}" -- "$cur") )
|
|
36
|
+
return
|
|
37
|
+
fi
|
|
38
|
+
local first="\${COMP_WORDS[1]}"
|
|
39
|
+
local opts=""
|
|
40
|
+
case "$first" in
|
|
41
|
+
${cmdCases}
|
|
42
|
+
*) opts="${GLOBAL_FLAGS.join(' ')}" ;;
|
|
43
|
+
esac
|
|
44
|
+
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
|
|
12
45
|
}
|
|
13
46
|
complete -F _klyro_complete klyro
|
|
14
47
|
complete -F _klyro_complete ky
|
|
15
48
|
`;
|
|
16
49
|
}
|
|
17
50
|
function zshScript() {
|
|
51
|
+
const cmdCases = Object.entries(COMMAND_FLAGS)
|
|
52
|
+
.map(([c, fs]) => ` ${c}) _values 'flags' ${fs.map((f) => `'${f}'`).join(' ')} ;;`)
|
|
53
|
+
.join('\n');
|
|
18
54
|
return `#compdef klyro ky
|
|
19
55
|
_klyro() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
if (( CURRENT == 2 )); then
|
|
57
|
+
_describe 'klyro commands' ${COMMANDS.map((c) => `'${c}'`).join(' ')} ${GLOBAL_FLAGS.map((f) => `'${f}'`).join(' ')}
|
|
58
|
+
return
|
|
59
|
+
fi
|
|
60
|
+
case "$words[2]" in
|
|
61
|
+
${cmdCases}
|
|
62
|
+
*) _values 'flags' ${GLOBAL_FLAGS.map((f) => `'${f}'`).join(' ')} ;;
|
|
63
|
+
esac
|
|
23
64
|
}
|
|
24
65
|
compdef _klyro klyro ky
|
|
25
66
|
`;
|
|
26
67
|
}
|
|
27
68
|
function fishScript() {
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
const flagLines = Object.entries(COMMAND_FLAGS)
|
|
70
|
+
.flatMap(([c, fs]) => fs.map((f) => `complete -c klyro -f -n "__fish_seen_subcommand_from ${c}" -a ${f}`))
|
|
71
|
+
.join('\n');
|
|
72
|
+
return `# klyro fish completion (commands + flags)
|
|
73
|
+
${COMMANDS.map((c) => `complete -c klyro -f -n __fish_use_subcommand -a ${c}`).join('\n')}
|
|
74
|
+
${flagLines}
|
|
30
75
|
complete -c ky -f -a "${COMMANDS.join(' ')}"
|
|
31
76
|
`;
|
|
32
77
|
}
|
|
33
78
|
function powershellScript() {
|
|
34
|
-
return `# klyro powershell completion
|
|
79
|
+
return `# klyro powershell completion (commands + flags)
|
|
35
80
|
Register-ArgumentCompleter -Native -CommandName klyro,ky -ScriptBlock {
|
|
36
81
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
37
82
|
$cmds = @(${COMMANDS.map((c) => `'${c}'`).join(', ')})
|
|
38
|
-
$
|
|
83
|
+
$flags = @(${GLOBAL_FLAGS.map((f) => `'${f}'`).join(', ')})
|
|
84
|
+
$tokens = $commandAst.ToString() -split '\\s+'
|
|
85
|
+
if ($tokens.Count -le 2) { $cands = $cmds + $flags } else {
|
|
86
|
+
switch ($tokens[1]) {
|
|
87
|
+
${Object.entries(COMMAND_FLAGS).map(([c, fs]) => ` '${c}' { $cands = @(${fs.map((f) => `'${f}'`).join(', ')}) }`).join('\n')}
|
|
88
|
+
default { $cands = $flags }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
$cands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
39
92
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
40
93
|
}
|
|
41
94
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -108,6 +108,18 @@ function checkPlatform() {
|
|
|
108
108
|
const ok = ['win32', 'linux', 'darwin'].includes(process.platform);
|
|
109
109
|
return { name: 'Platform', ok, detail: `${process.platform} ${process.arch} ${ok ? '✓' : '✗ unsupported'}` };
|
|
110
110
|
}
|
|
111
|
+
async function checkSandbox() {
|
|
112
|
+
try {
|
|
113
|
+
const { detectSandbox } = await import('../tools/shell/sandbox.js');
|
|
114
|
+
const st = detectSandbox();
|
|
115
|
+
if (st.active)
|
|
116
|
+
return { name: 'Sandbox', ok: true, detail: `${st.backend} ✓` };
|
|
117
|
+
return { name: 'Sandbox', ok: true, detail: `none — ${st.reason ?? 'policy+path guards only'}` };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { name: 'Sandbox', ok: true, detail: 'none — policy+path guards only' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
111
123
|
async function checkMcp(cwd) {
|
|
112
124
|
try {
|
|
113
125
|
const { loadMcpServers } = await import('../mcp/config.js');
|
|
@@ -167,6 +179,7 @@ export async function runDoctor(opts = {}) {
|
|
|
167
179
|
checks.push(await checkGit());
|
|
168
180
|
checks.push(await checkTools());
|
|
169
181
|
checks.push(checkPlatform());
|
|
182
|
+
checks.push(await checkSandbox());
|
|
170
183
|
const mcpCheck = await checkMcp(cwd);
|
|
171
184
|
const trustCheck = await checkTrust();
|
|
172
185
|
checks.push(mcpCheck);
|
package/dist/cli/repl.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parseUnifiedDiff } from '../tui/diff-parser.js';
|
|
|
25
25
|
import { parse } from './slash/parser.js';
|
|
26
26
|
import { resolveProvider, providerHelp, lastProviderError } from '../providers.js';
|
|
27
27
|
import { readVersion } from '../version.js';
|
|
28
|
-
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, createReadWrapper } from '../tui/mouse.js';
|
|
28
|
+
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, PASTE_ENABLE, PASTE_DISABLE, PasteFilter, createReadWrapper } from '../tui/mouse.js';
|
|
29
29
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
30
30
|
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
31
31
|
import { buildSystemPrompt, parseImageInput } from '../context/system-prompt.js';
|
|
@@ -291,6 +291,8 @@ export async function startRepl(opts = {}) {
|
|
|
291
291
|
let lastStatus = null;
|
|
292
292
|
const pendingQueue = [];
|
|
293
293
|
let isMounted = false;
|
|
294
|
+
// Live vim input mode for the TUI (toggled by /vim, persisted to config).
|
|
295
|
+
let vimLive = 'insert';
|
|
294
296
|
let directHooks;
|
|
295
297
|
// Plain-text mirror for exit replay (scroll.md §1.2: session survives in
|
|
296
298
|
// native scrollback after the alt screen is torn down). Cap 300 lines.
|
|
@@ -393,6 +395,7 @@ export async function startRepl(opts = {}) {
|
|
|
393
395
|
// IMPORTANT: Ink 7 reads stdin via 'readable' + stdin.read() (paused mode),
|
|
394
396
|
// never 'data' events — so the tap wraps read(), not emit().
|
|
395
397
|
const mouseFilter = new MouseFilter();
|
|
398
|
+
const pasteFilter = new PasteFilter();
|
|
396
399
|
const origStdinRead = process.stdin.read.bind(process.stdin);
|
|
397
400
|
const origStdinEmit = process.stdin.emit.bind(process.stdin);
|
|
398
401
|
let mouseTapInstalled = false;
|
|
@@ -405,18 +408,36 @@ export async function startRepl(opts = {}) {
|
|
|
405
408
|
catch { /* ignore */ }
|
|
406
409
|
}
|
|
407
410
|
}
|
|
411
|
+
// Bracketed paste: bulk-insert into the input buffer instead of
|
|
412
|
+
// char-at-a-time typing (no autocomplete/history churn per char).
|
|
413
|
+
function dispatchPastes(pastes) {
|
|
414
|
+
for (const p of pastes) {
|
|
415
|
+
try {
|
|
416
|
+
if (isMounted && directHooks)
|
|
417
|
+
directHooks.pasteText(p);
|
|
418
|
+
}
|
|
419
|
+
catch { /* ignore */ }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
408
422
|
function installMouseTap() {
|
|
409
423
|
if (!isAltScreen || mouseTapInstalled)
|
|
410
424
|
return;
|
|
411
425
|
mouseTapInstalled = true;
|
|
426
|
+
// Bracketed paste on: the terminal wraps pastes in ESC[200~ … ESC[201~.
|
|
427
|
+
try {
|
|
428
|
+
process.stdout.write(PASTE_ENABLE);
|
|
429
|
+
}
|
|
430
|
+
catch { /* ignore */ }
|
|
412
431
|
const stdinAny = process.stdin;
|
|
413
432
|
// Primary path: Ink's paused-mode read loop.
|
|
414
|
-
stdinAny.read = createReadWrapper(origStdinRead, mouseFilter, (d) => dispatchWheels([d]));
|
|
433
|
+
stdinAny.read = createReadWrapper(origStdinRead, mouseFilter, (d) => dispatchWheels([d]), { onPaste: (p) => dispatchPastes([p]), pasteFilter });
|
|
415
434
|
// Fallback path: flowing mode ('data' events), e.g. if any library
|
|
416
435
|
// resumes the stream. Same split, same dispatch.
|
|
417
436
|
stdinAny.emit = (...a) => {
|
|
418
437
|
if (a[0] === 'data' && Buffer.isBuffer(a[1])) {
|
|
419
|
-
const
|
|
438
|
+
const psplit = pasteFilter.push(a[1]);
|
|
439
|
+
dispatchPastes(psplit.pastes);
|
|
440
|
+
const split = mouseFilter.push(psplit.kept);
|
|
420
441
|
dispatchWheels(split.wheels);
|
|
421
442
|
if (split.kept.length === 0)
|
|
422
443
|
return false;
|
|
@@ -430,6 +451,11 @@ export async function startRepl(opts = {}) {
|
|
|
430
451
|
return;
|
|
431
452
|
mouseTapInstalled = false;
|
|
432
453
|
mouseFilter.reset();
|
|
454
|
+
pasteFilter.reset();
|
|
455
|
+
try {
|
|
456
|
+
process.stdout.write(PASTE_DISABLE);
|
|
457
|
+
}
|
|
458
|
+
catch { /* ignore */ }
|
|
433
459
|
const stdinAny = process.stdin;
|
|
434
460
|
stdinAny.read = origStdinRead;
|
|
435
461
|
stdinAny.emit = origStdinEmit;
|
|
@@ -457,7 +483,9 @@ export async function startRepl(opts = {}) {
|
|
|
457
483
|
if (!isAltScreen)
|
|
458
484
|
return;
|
|
459
485
|
const sink = (...args) => {
|
|
460
|
-
|
|
486
|
+
// Scrub secrets before the line reaches the ring buffer or disk —
|
|
487
|
+
// stray provider/tool logs must never persist credentials.
|
|
488
|
+
const line = redact(`[${new Date().toISOString()}] ${args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}`);
|
|
461
489
|
consoleRing.push(line);
|
|
462
490
|
if (consoleRing.length > 200)
|
|
463
491
|
consoleRing.splice(0, consoleRing.length - 200);
|
|
@@ -700,6 +728,13 @@ export async function startRepl(opts = {}) {
|
|
|
700
728
|
onMounted: (hooks) => {
|
|
701
729
|
directHooks = hooks;
|
|
702
730
|
isMounted = true;
|
|
731
|
+
// Apply a live vim mode toggled before mount.
|
|
732
|
+
if (vimLive !== 'insert') {
|
|
733
|
+
try {
|
|
734
|
+
hooks.setVimMode(vimLive);
|
|
735
|
+
}
|
|
736
|
+
catch { /* ignore */ }
|
|
737
|
+
}
|
|
703
738
|
for (const ev of pendingQueue) {
|
|
704
739
|
if (ev.kind === 'status')
|
|
705
740
|
hooks.updateStatus(ev.patch);
|
|
@@ -1050,7 +1085,14 @@ export async function startRepl(opts = {}) {
|
|
|
1050
1085
|
return;
|
|
1051
1086
|
}
|
|
1052
1087
|
case 'memory': {
|
|
1053
|
-
|
|
1088
|
+
const { loadMemory } = await import('../context/memory.js');
|
|
1089
|
+
const notes = (await loadMemory(cwd)).trim();
|
|
1090
|
+
queuedAppend({
|
|
1091
|
+
id: `mem-${Date.now()}`,
|
|
1092
|
+
kind: 'text',
|
|
1093
|
+
text: notes ? `Memory (.klyro/memory/session-notes.md):\n${notes.slice(0, 4000)}` : 'Memory is empty — the agent records durable notes here via memory_write.',
|
|
1094
|
+
role: 'assistant',
|
|
1095
|
+
});
|
|
1054
1096
|
return;
|
|
1055
1097
|
}
|
|
1056
1098
|
case 'jobs': {
|
|
@@ -1613,8 +1655,13 @@ export async function startRepl(opts = {}) {
|
|
|
1613
1655
|
case 'cancel': {
|
|
1614
1656
|
ac.abort();
|
|
1615
1657
|
ac = new AbortController();
|
|
1658
|
+
// Immediate abort cascade: background shells must not outlive the
|
|
1659
|
+
// cancelled run (the runtime abort path also kills them, but the
|
|
1660
|
+
// controller swap above means we do it here for immediacy).
|
|
1661
|
+
const { killAllJobs } = await import('../tools/shell/background.js');
|
|
1662
|
+
const killed = killAllJobs();
|
|
1616
1663
|
queuedStatus({ status: 'aborted' });
|
|
1617
|
-
queuedAppend({ id: `cancel-${Date.now()}`, kind: 'text', text: 'cancelled current operation', role: 'assistant' });
|
|
1664
|
+
queuedAppend({ id: `cancel-${Date.now()}`, kind: 'text', text: killed.length > 0 ? `cancelled current operation (${killed.length} background job(s) killed)` : 'cancelled current operation', role: 'assistant' });
|
|
1618
1665
|
return;
|
|
1619
1666
|
}
|
|
1620
1667
|
case 'shell': {
|
|
@@ -2468,13 +2515,72 @@ export async function startRepl(opts = {}) {
|
|
|
2468
2515
|
}
|
|
2469
2516
|
return;
|
|
2470
2517
|
}
|
|
2471
|
-
case 'keymap':
|
|
2472
|
-
|
|
2518
|
+
case 'keymap': {
|
|
2519
|
+
// Real binding table (mirrors src/tui/app.tsx useInput + mouse.ts).
|
|
2520
|
+
// A stored `klyro.keymap` override is shown, not applied — the TUI
|
|
2521
|
+
// has one compiled keymap; the value is kept for future remapping.
|
|
2522
|
+
const { runConfig } = await import('./config.js');
|
|
2523
|
+
const capture = async (args) => {
|
|
2524
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
2525
|
+
let out = '';
|
|
2526
|
+
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
2527
|
+
try {
|
|
2528
|
+
await runConfig(args);
|
|
2529
|
+
}
|
|
2530
|
+
finally {
|
|
2531
|
+
process.stdout.write = orig;
|
|
2532
|
+
}
|
|
2533
|
+
return out;
|
|
2534
|
+
};
|
|
2535
|
+
const stored = cmd.name?.trim();
|
|
2536
|
+
if (stored) {
|
|
2537
|
+
await capture(['set', 'klyro.keymap', stored]);
|
|
2538
|
+
queuedAppend({ id: `keymap2-${Date.now()}`, kind: 'text', text: `keymap note saved: ${stored} (display-only — the TUI uses its compiled keymap below)`, role: 'assistant' });
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
queuedAppend({
|
|
2542
|
+
id: `keymap-${Date.now()}`,
|
|
2543
|
+
kind: 'text',
|
|
2544
|
+
role: 'assistant',
|
|
2545
|
+
text: [
|
|
2546
|
+
'Keymap (TUI compiled bindings):',
|
|
2547
|
+
' Enter send · Shift+Enter newline · Tab complete slash · Esc drop queued / Esc×2 cancel run',
|
|
2548
|
+
' Ctrl+C cancel (1st) / quit (2nd) · Ctrl+O expand last tool group · Ctrl+G jump bottom',
|
|
2549
|
+
' PgUp/PgDn or Ctrl+U/Ctrl+D half-page · Ctrl+Home/End top/bottom · Home/End jump · Space jump to unread',
|
|
2550
|
+
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑/↓ history (with text) else scroll · wheel ±3 lines',
|
|
2551
|
+
' Shift+drag selects · /vim toggles vim input mode · /keymap <note> saves a display note',
|
|
2552
|
+
].join('\n'),
|
|
2553
|
+
});
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
case 'vim': {
|
|
2557
|
+
// Live vim input mode (not a config stub): toggles the TUI between
|
|
2558
|
+
// insert and normal mode via directHooks; persists the choice too.
|
|
2559
|
+
const want = cmd.state?.trim().toLowerCase();
|
|
2560
|
+
const next = want === 'on' || want === 'normal' ? 'normal' : want === 'off' || want === 'insert' ? 'insert' : vimLive === 'insert' ? 'normal' : 'insert';
|
|
2561
|
+
vimLive = next;
|
|
2562
|
+
try {
|
|
2563
|
+
const { runConfig } = await import('./config.js');
|
|
2564
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
2565
|
+
process.stdout.write = (() => true);
|
|
2566
|
+
try {
|
|
2567
|
+
await runConfig(['set', 'klyro.vim', next]);
|
|
2568
|
+
}
|
|
2569
|
+
finally {
|
|
2570
|
+
process.stdout.write = orig;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
catch { /* persist best-effort */ }
|
|
2574
|
+
if (isMounted && directHooks)
|
|
2575
|
+
directHooks.setVimMode(next);
|
|
2576
|
+
queuedAppend({ id: `vim-${Date.now()}`, kind: 'text', text: `vim mode: ${next}${next === 'normal' ? ' (h/l move · i/a insert · x delete · 0/$ ends · j/k scroll)' : ''}`, role: 'assistant' });
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2473
2579
|
case 'theme':
|
|
2474
2580
|
case 'statusline':
|
|
2475
2581
|
case 'output-style': {
|
|
2476
|
-
const key = cmd.kind === '
|
|
2477
|
-
const val = (cmd.kind === '
|
|
2582
|
+
const key = cmd.kind === 'theme' ? 'klyro.theme' : cmd.kind === 'statusline' ? 'klyro.statusline' : 'klyro.outputStyle';
|
|
2583
|
+
const val = (cmd.kind === 'theme' ? cmd.name : cmd.kind === 'statusline' ? cmd.format : cmd.style)?.trim();
|
|
2478
2584
|
const { runConfig } = await import('./config.js');
|
|
2479
2585
|
const capture = async (args) => {
|
|
2480
2586
|
const orig = process.stdout.write.bind(process.stdout);
|
package/dist/cli/run.js
CHANGED
|
@@ -403,6 +403,20 @@ export async function runOnce(opts) {
|
|
|
403
403
|
}
|
|
404
404
|
if (output === 'human')
|
|
405
405
|
stdout.write('\n');
|
|
406
|
+
// Final cost line: headless runs previously surfaced cost only through
|
|
407
|
+
// [budget] warnings. Human/silent → stderr one-liner; json → additive
|
|
408
|
+
// cost_usd/usage fields on the final object (purely additive, no shape break).
|
|
409
|
+
{
|
|
410
|
+
const { estimateCost } = await import('../providers/model-info.js');
|
|
411
|
+
const cost = estimateCost(opts.model, result.usage.input, result.usage.output);
|
|
412
|
+
const costLine = `$${cost.toFixed(4)} · ${result.usage.input} in / ${result.usage.output} out${result.usage.estimated ? ' (estimated)' : ''}`;
|
|
413
|
+
if (output === 'json') {
|
|
414
|
+
stdout.write(JSON.stringify({ kind: 'cost', cost_usd: cost, usage: result.usage }) + '\n');
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
stderr.write(`klyro: cost ${costLine}\n`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
406
420
|
if (result.status === 'max_steps') {
|
|
407
421
|
if (output === 'json')
|
|
408
422
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
@@ -316,5 +316,11 @@ export interface CommandDef {
|
|
|
316
316
|
}
|
|
317
317
|
/** Curated canonical commands with one-line hints — drives /help, /commands and TUI autocomplete. */
|
|
318
318
|
export declare const COMMAND_DEFS: CommandDef[];
|
|
319
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* Fuzzy score for TUI autocomplete: subsequence match with bonuses for
|
|
321
|
+
* prefix (+100), word-boundary (+30), and consecutive (+15) matches, minus
|
|
322
|
+
* a gap penalty. Returns -Infinity when `query` is not a subsequence.
|
|
323
|
+
*/
|
|
324
|
+
export declare function fuzzyScore(name: string, query: string): number;
|
|
325
|
+
/** Fuzzy-match command names for TUI autocomplete — top `limit` (default 6). */
|
|
320
326
|
export declare function suggestCommands(prefix: string, limit?: number): CommandDef[];
|
package/dist/cli/slash/parser.js
CHANGED
|
@@ -293,18 +293,45 @@ export const COMMAND_DEFS = [
|
|
|
293
293
|
{ name: 'deps', hint: 'dependencies' },
|
|
294
294
|
{ name: 'install', hint: 'install deps' },
|
|
295
295
|
];
|
|
296
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* Fuzzy score for TUI autocomplete: subsequence match with bonuses for
|
|
298
|
+
* prefix (+100), word-boundary (+30), and consecutive (+15) matches, minus
|
|
299
|
+
* a gap penalty. Returns -Infinity when `query` is not a subsequence.
|
|
300
|
+
*/
|
|
301
|
+
export function fuzzyScore(name, query) {
|
|
302
|
+
const n = name.toLowerCase();
|
|
303
|
+
const q = query.toLowerCase();
|
|
304
|
+
if (!q)
|
|
305
|
+
return 0;
|
|
306
|
+
let score = 0;
|
|
307
|
+
let ni = 0;
|
|
308
|
+
let lastHit = -2;
|
|
309
|
+
for (let qi = 0; qi < q.length; qi++) {
|
|
310
|
+
const found = n.indexOf(q[qi], ni);
|
|
311
|
+
if (found === -1)
|
|
312
|
+
return -Infinity;
|
|
313
|
+
if (qi === 0 && found === 0)
|
|
314
|
+
score += 100; // prefix
|
|
315
|
+
if (found > 0 && (n[found - 1] === '-' || n[found - 1] === '_'))
|
|
316
|
+
score += 30; // word boundary
|
|
317
|
+
if (found === lastHit + 1)
|
|
318
|
+
score += 15; // consecutive
|
|
319
|
+
else
|
|
320
|
+
score -= (found - ni); // gap penalty
|
|
321
|
+
lastHit = found;
|
|
322
|
+
ni = found + 1;
|
|
323
|
+
}
|
|
324
|
+
score -= (n.length - q.length); // prefer shorter names
|
|
325
|
+
return score;
|
|
326
|
+
}
|
|
327
|
+
/** Fuzzy-match command names for TUI autocomplete — top `limit` (default 6). */
|
|
297
328
|
export function suggestCommands(prefix, limit = 6) {
|
|
298
329
|
const p = prefix.toLowerCase().replace(/^\//, '');
|
|
299
330
|
if (!p)
|
|
300
331
|
return COMMAND_DEFS.slice(0, limit);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
else if (d.name.includes(p))
|
|
307
|
-
contains.push(d);
|
|
308
|
-
}
|
|
309
|
-
return [...starts, ...contains].slice(0, limit);
|
|
332
|
+
return COMMAND_DEFS.map((d) => ({ d, s: fuzzyScore(d.name, p) + fuzzyScore(d.hint, p) * 0.25 }))
|
|
333
|
+
.filter((x) => x.s > -Infinity)
|
|
334
|
+
.sort((a, b) => b.s - a.s)
|
|
335
|
+
.slice(0, limit)
|
|
336
|
+
.map((x) => x.d);
|
|
310
337
|
}
|
package/dist/cli/update.d.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* klyro update — check registry for newer version, cached 24h.
|
|
3
3
|
* Env KLYRO_NO_UPDATE_CHECK=1 disables.
|
|
4
4
|
*
|
|
5
|
-
* Integrity: before recommending `npm i`, we verify the tarball
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Integrity: before recommending `npm i`, we verify the tarball against
|
|
6
|
+
* BOTH the registry's SRI digest (sha512/sha256) AND the legacy sha1
|
|
7
|
+
* `dist.shasum` when present — a tampered CDN or MITM registry response
|
|
8
|
+
* must forge two independent digests to push a malicious binary.
|
|
9
|
+
* Downgrade protection: a registry `latest` that is not strictly newer
|
|
10
|
+
* than the running version (semver) is never recommended.
|
|
9
11
|
*/
|
|
12
|
+
/** Minimal semver compare for `x.y.z[-prerelease]`; null when unparseable. */
|
|
13
|
+
export declare function compareSemver(a: string, b: string): number | null;
|
|
10
14
|
export declare function checkForUpdate(current: string): Promise<string | null>;
|
|
11
15
|
export declare function runUpdate(): Promise<number>;
|