klyro 0.1.42 → 0.1.43
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/dist/cli/repl.js +149 -20
- package/dist/cli/slash/parser.d.ts +12 -2
- package/dist/cli/slash/parser.js +13 -5
- package/dist/tui/app.d.ts +1 -0
- package/dist/tui/app.js +2 -1
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -50,9 +50,15 @@ export async function startRepl(opts = {}) {
|
|
|
50
50
|
if (providerKind === 'anthropic' && !apiKey) {
|
|
51
51
|
process.stderr.write('klyro: anthropic provider detected but KLYRO_API_KEY is empty — falling back to OpenAI-compatible adapter\n');
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
let currentProvider = effectiveProvider;
|
|
54
|
+
let currentBaseUrl = baseUrl;
|
|
55
|
+
let currentApiKey = apiKey;
|
|
56
|
+
let currentMaxSteps = opts.maxSteps ?? 30;
|
|
57
|
+
let effortLevel = 'medium';
|
|
58
|
+
const buildAdapter = (prov, url, key) => prov === 'anthropic'
|
|
59
|
+
? anthropicAdapter({ baseURL: url, apiKey: key, timeoutMs: 60_000 })
|
|
60
|
+
: httpChatAdapter({ baseURL: url, apiKey: key, timeoutMs: 60_000 });
|
|
61
|
+
let adapter = buildAdapter(currentProvider, currentBaseUrl, currentApiKey);
|
|
56
62
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
57
63
|
const ctxPrefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
58
64
|
// 4.4 KLYRO.md hierarchy
|
|
@@ -133,9 +139,18 @@ export async function startRepl(opts = {}) {
|
|
|
133
139
|
let tuiSessionId;
|
|
134
140
|
if (isAltScreen)
|
|
135
141
|
enterAlt();
|
|
142
|
+
const EFFORT_STEPS = { low: 10, medium: 30, high: 50, max: 100 };
|
|
143
|
+
function queuedClear() {
|
|
144
|
+
if (isMounted && directHooks)
|
|
145
|
+
directHooks.clearTranscript();
|
|
146
|
+
else
|
|
147
|
+
pendingQueue.length = 0;
|
|
148
|
+
if (isMounted && directHooks)
|
|
149
|
+
directHooks.clearTranscript();
|
|
150
|
+
}
|
|
136
151
|
app = render(React.createElement(App, {
|
|
137
152
|
initialModel: model,
|
|
138
|
-
maxSteps:
|
|
153
|
+
maxSteps: currentMaxSteps,
|
|
139
154
|
cwd,
|
|
140
155
|
initialStatus: { status: 'idle' },
|
|
141
156
|
approvalBridge: tuiBridge,
|
|
@@ -190,7 +205,7 @@ export async function startRepl(opts = {}) {
|
|
|
190
205
|
let sessionId;
|
|
191
206
|
if (!isSimpleChat) {
|
|
192
207
|
try {
|
|
193
|
-
const rec = await tuiStore.create({ cwd, task: taskText, config: { model, maxSteps:
|
|
208
|
+
const rec = await tuiStore.create({ cwd, task: taskText, config: { model, maxSteps: currentMaxSteps } });
|
|
194
209
|
sessionId = rec.id;
|
|
195
210
|
tuiSessionId = rec.id;
|
|
196
211
|
// Session info goes to status bar, not transcript (clean like Claude Code)
|
|
@@ -238,7 +253,7 @@ export async function startRepl(opts = {}) {
|
|
|
238
253
|
task: taskText,
|
|
239
254
|
cwd,
|
|
240
255
|
model,
|
|
241
|
-
maxSteps:
|
|
256
|
+
maxSteps: currentMaxSteps,
|
|
242
257
|
signal: ac.signal,
|
|
243
258
|
nonInteractive: opts.nonInteractive ?? false,
|
|
244
259
|
verify: { enabled: true, maxRepairAttempts: 3 },
|
|
@@ -387,21 +402,34 @@ export async function startRepl(opts = {}) {
|
|
|
387
402
|
// Listener cleanup is handled by the waitUntilExit resolver below
|
|
388
403
|
return;
|
|
389
404
|
case 'clear':
|
|
405
|
+
queuedClear();
|
|
390
406
|
queuedAppend({ id: `sep-${Date.now()}`, kind: 'text', text: '--- cleared ---', role: 'assistant' });
|
|
391
407
|
return;
|
|
392
408
|
case 'help': {
|
|
393
409
|
const helpText = [
|
|
394
410
|
'commands:',
|
|
395
|
-
' /clear
|
|
396
|
-
' /
|
|
397
|
-
' /
|
|
398
|
-
' /
|
|
399
|
-
' /
|
|
400
|
-
' /
|
|
401
|
-
' /
|
|
402
|
-
' /
|
|
403
|
-
' /
|
|
404
|
-
|
|
411
|
+
' /clear — clear transcript',
|
|
412
|
+
' /compact [focus] — compact context (clears transcript, keeps marker)',
|
|
413
|
+
' /model [id] — show or switch model mid-session',
|
|
414
|
+
' /provider [name] — show or switch provider (openai|anthropic)',
|
|
415
|
+
' /effort [level] — show or set effort (low|medium|high|max → steps)',
|
|
416
|
+
' /diff — show git diff',
|
|
417
|
+
' /status — show session status',
|
|
418
|
+
' /plan — show current plan/todos',
|
|
419
|
+
' /verify — detect + run verifiers',
|
|
420
|
+
' /project — project scan',
|
|
421
|
+
' /context — context breakdown',
|
|
422
|
+
' /cost — token cost',
|
|
423
|
+
' /jobs — background jobs',
|
|
424
|
+
' /memory — session notes',
|
|
425
|
+
' /undo /rewind — checkpoints',
|
|
426
|
+
' /login /logout — credentials',
|
|
427
|
+
' /init — create KLYRO.md',
|
|
428
|
+
' /config — show config path',
|
|
429
|
+
' /doctor — run diagnostics',
|
|
430
|
+
' /version — show version',
|
|
431
|
+
' /quit (/exit) — exit',
|
|
432
|
+
`provider: ${currentProvider} model: ${model} effort: ${effortLevel} (${currentMaxSteps} steps) cwd: ${cwd}`,
|
|
405
433
|
].join('\n');
|
|
406
434
|
queuedAppend({ id: `help-${Date.now()}`, kind: 'text', text: helpText, role: 'assistant' });
|
|
407
435
|
return;
|
|
@@ -476,7 +504,7 @@ export async function startRepl(opts = {}) {
|
|
|
476
504
|
});
|
|
477
505
|
}
|
|
478
506
|
else {
|
|
479
|
-
queuedAppend({ id: `stat2-${Date.now()}`, kind: 'text', text: `model: ${model} provider: ${
|
|
507
|
+
queuedAppend({ id: `stat2-${Date.now()}`, kind: 'text', text: `model: ${model} provider: ${currentProvider} effort: ${effortLevel} (${currentMaxSteps} steps) cwd: ${cwd}`, role: 'assistant' });
|
|
480
508
|
}
|
|
481
509
|
return;
|
|
482
510
|
}
|
|
@@ -577,14 +605,23 @@ export async function startRepl(opts = {}) {
|
|
|
577
605
|
return;
|
|
578
606
|
}
|
|
579
607
|
case 'compact': {
|
|
580
|
-
|
|
581
|
-
|
|
608
|
+
const focus = cmd.focus?.trim();
|
|
609
|
+
queuedClear();
|
|
610
|
+
queuedAppend({
|
|
611
|
+
id: `compact-${Date.now()}`,
|
|
612
|
+
kind: 'text',
|
|
613
|
+
text: focus ? `Context compacted — transcript cleared (focus: ${focus}). Continuing fresh.` : 'Context compacted — transcript cleared. Continuing fresh.',
|
|
614
|
+
role: 'assistant',
|
|
615
|
+
});
|
|
616
|
+
queuedStatus({ status: 'done' });
|
|
582
617
|
return;
|
|
583
618
|
}
|
|
584
619
|
case 'model': {
|
|
585
620
|
const next = cmd.model?.trim();
|
|
586
621
|
if (!next) {
|
|
587
|
-
|
|
622
|
+
const { MODEL_REGISTRY } = await import('../providers/model-info.js');
|
|
623
|
+
const known = Object.keys(MODEL_REGISTRY).join(', ');
|
|
624
|
+
queuedAppend({ id: `mdl-${Date.now()}`, kind: 'text', text: `current model: ${model}\nknown: ${known}\nusage: /model <id>`, role: 'assistant' });
|
|
588
625
|
}
|
|
589
626
|
else {
|
|
590
627
|
queuedStatus({ model: next });
|
|
@@ -593,6 +630,98 @@ export async function startRepl(opts = {}) {
|
|
|
593
630
|
}
|
|
594
631
|
return;
|
|
595
632
|
}
|
|
633
|
+
case 'provider': {
|
|
634
|
+
const next = cmd.provider?.trim().toLowerCase();
|
|
635
|
+
if (!next) {
|
|
636
|
+
queuedAppend({ id: `prov-${Date.now()}`, kind: 'text', text: `current provider: ${currentProvider}\nbaseURL: ${currentBaseUrl}\nusage: /provider <openai|anthropic>`, role: 'assistant' });
|
|
637
|
+
}
|
|
638
|
+
else if (next !== 'openai' && next !== 'anthropic') {
|
|
639
|
+
queuedAppend({ id: `prov-err-${Date.now()}`, kind: 'error', message: `unknown provider: ${next} (expected openai|anthropic)` });
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
if (next === 'anthropic' && !currentApiKey) {
|
|
643
|
+
queuedAppend({ id: `prov-warn-${Date.now()}`, kind: 'text', text: 'warning: no API key set — anthropic adapter may 401. Set KLYRO_API_KEY or use /login.', role: 'assistant' });
|
|
644
|
+
}
|
|
645
|
+
currentProvider = next;
|
|
646
|
+
adapter = buildAdapter(currentProvider, currentBaseUrl, currentApiKey);
|
|
647
|
+
queuedAppend({ id: `prov2-${Date.now()}`, kind: 'text', text: `provider switched to ${next} (takes effect on next prompt)`, role: 'assistant' });
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
case 'effort': {
|
|
652
|
+
const level = cmd.level?.trim().toLowerCase();
|
|
653
|
+
if (!level) {
|
|
654
|
+
queuedAppend({ id: `eff-${Date.now()}`, kind: 'text', text: `current effort: ${effortLevel} (${currentMaxSteps} steps)\nlevels: low (10) | medium (30) | high (50) | max (100)\nusage: /effort <level>`, role: 'assistant' });
|
|
655
|
+
}
|
|
656
|
+
else if (!EFFORT_STEPS[level]) {
|
|
657
|
+
queuedAppend({ id: `eff-err-${Date.now()}`, kind: 'error', message: `unknown effort: ${level} (expected low|medium|high|max)` });
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
effortLevel = level;
|
|
661
|
+
currentMaxSteps = EFFORT_STEPS[level];
|
|
662
|
+
queuedStatus({ maxSteps: currentMaxSteps });
|
|
663
|
+
queuedAppend({ id: `eff2-${Date.now()}`, kind: 'text', text: `effort set to ${level} (${currentMaxSteps} max steps)`, role: 'assistant' });
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
case 'login': {
|
|
668
|
+
const { runLogin } = await import('./auth.js');
|
|
669
|
+
const code = await runLogin();
|
|
670
|
+
queuedAppend({ id: `login-${Date.now()}`, kind: 'text', text: code === 0 ? 'login saved (0600)' : 'login failed', role: 'assistant' });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
case 'logout': {
|
|
674
|
+
const { runLogout } = await import('./auth.js');
|
|
675
|
+
const code = await runLogout();
|
|
676
|
+
queuedAppend({ id: `logout-${Date.now()}`, kind: 'text', text: code === 0 ? 'logged out' : 'logout failed', role: 'assistant' });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
case 'init': {
|
|
680
|
+
const { writeFileSync, existsSync } = await import('node:fs');
|
|
681
|
+
const { join } = await import('node:path');
|
|
682
|
+
const target = join(cwd, 'KLYRO.md');
|
|
683
|
+
if (existsSync(target)) {
|
|
684
|
+
queuedAppend({ id: `init-${Date.now()}`, kind: 'text', text: `KLYRO.md already exists at ${target}`, role: 'assistant' });
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
try {
|
|
688
|
+
const { runScan } = await import('./scan.js');
|
|
689
|
+
let out = '';
|
|
690
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
691
|
+
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
692
|
+
await runScan({ cwd, json: false });
|
|
693
|
+
process.stdout.write = orig;
|
|
694
|
+
writeFileSync(target, `# KLYRO.md\n\nProject: ${cwd}\n\n## Stack\n\n${out.slice(0, 2000)}\n\n## Conventions\n\n- Prefer smallest change that solves the task.\n- Run verification after edits.\n`);
|
|
695
|
+
queuedAppend({ id: `init2-${Date.now()}`, kind: 'text', text: `created ${target}`, role: 'assistant' });
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
case 'plan': {
|
|
704
|
+
try {
|
|
705
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
706
|
+
const { join } = await import('node:path');
|
|
707
|
+
const todosPath = join(cwd, '.klyro', 'plans', 'todos.json');
|
|
708
|
+
if (!existsSync(todosPath)) {
|
|
709
|
+
queuedAppend({ id: `plan-${Date.now()}`, kind: 'text', text: 'No active plan (no .klyro/plans/todos.json). The agent creates one via todo_write when planning.', role: 'assistant' });
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
const raw = readFileSync(todosPath, 'utf-8').slice(0, 2000);
|
|
713
|
+
queuedAppend({ id: `plan2-${Date.now()}`, kind: 'text', text: `Plan (todos.json):\n${raw}`, role: 'assistant' });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
queuedAppend({ id: `plan-err-${Date.now()}`, kind: 'error', message: `plan failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
case 'prompt': {
|
|
722
|
+
// Regular prompts never reach onSlash — no-op for exhaustiveness.
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
596
725
|
case 'unknown':
|
|
597
726
|
queuedAppend({
|
|
598
727
|
id: `unk-${Date.now()}`,
|
|
@@ -18,9 +18,16 @@ export type SlashCommand = {
|
|
|
18
18
|
kind: 'clear';
|
|
19
19
|
} | {
|
|
20
20
|
kind: 'compact';
|
|
21
|
+
focus?: string;
|
|
21
22
|
} | {
|
|
22
23
|
kind: 'model';
|
|
23
24
|
model: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'provider';
|
|
27
|
+
provider: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'effort';
|
|
30
|
+
level: string;
|
|
24
31
|
} | {
|
|
25
32
|
kind: 'diff';
|
|
26
33
|
} | {
|
|
@@ -56,8 +63,11 @@ export type SlashCommand = {
|
|
|
56
63
|
} | {
|
|
57
64
|
kind: 'context';
|
|
58
65
|
} | {
|
|
59
|
-
kind: '
|
|
60
|
-
|
|
66
|
+
kind: 'login';
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'logout';
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'init';
|
|
61
71
|
} | {
|
|
62
72
|
kind: 'prompt';
|
|
63
73
|
text: string;
|
package/dist/cli/slash/parser.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Anything not starting with "/" is a regular prompt and yields
|
|
15
15
|
* { kind: 'prompt', text }.
|
|
16
16
|
*/
|
|
17
|
-
const KNOWN = ['clear', 'compact', 'model', 'diff', 'undo', 'rewind', 'plan', 'status', 'quit', 'help', 'config', 'doctor', 'version', 'cost', 'thinking', 'memory', 'jobs', 'verify', 'project', 'context', '
|
|
17
|
+
const KNOWN = ['clear', 'compact', 'model', 'm', 'provider', 'effort', 'diff', 'undo', 'rewind', 'plan', 'status', 'quit', 'exit', 'q', 'help', 'config', 'doctor', 'version', 'cost', 'thinking', 'memory', 'jobs', 'verify', 'project', 'context', 'login', 'logout', 'init'];
|
|
18
18
|
export function parse(input) {
|
|
19
19
|
const trimmed = input.trim();
|
|
20
20
|
if (!trimmed.startsWith('/')) {
|
|
@@ -25,7 +25,7 @@ export function parse(input) {
|
|
|
25
25
|
const rest = space === -1 ? '' : trimmed.slice(space + 1).trim();
|
|
26
26
|
switch (name) {
|
|
27
27
|
case 'clear': return { kind: 'clear' };
|
|
28
|
-
case 'compact': return { kind: 'compact' };
|
|
28
|
+
case 'compact': return { kind: 'compact', focus: rest || undefined };
|
|
29
29
|
case 'diff': return { kind: 'diff' };
|
|
30
30
|
case 'undo': return { kind: 'undo' };
|
|
31
31
|
case 'rewind': return { kind: 'rewind' };
|
|
@@ -38,7 +38,9 @@ export function parse(input) {
|
|
|
38
38
|
case 'verify': return { kind: 'verify' };
|
|
39
39
|
case 'project': return { kind: 'project' };
|
|
40
40
|
case 'context': return { kind: 'context' };
|
|
41
|
-
case '
|
|
41
|
+
case 'login': return { kind: 'login' };
|
|
42
|
+
case 'logout': return { kind: 'logout' };
|
|
43
|
+
case 'init': return { kind: 'init' };
|
|
42
44
|
case 'quit':
|
|
43
45
|
case 'exit':
|
|
44
46
|
case 'q': return { kind: 'quit' };
|
|
@@ -47,10 +49,16 @@ export function parse(input) {
|
|
|
47
49
|
case 'config': return { kind: 'config' };
|
|
48
50
|
case 'doctor': return { kind: 'doctor' };
|
|
49
51
|
case 'version': return { kind: 'version' };
|
|
52
|
+
case 'provider':
|
|
53
|
+
case 'p': {
|
|
54
|
+
return { kind: 'provider', provider: rest };
|
|
55
|
+
}
|
|
56
|
+
case 'effort':
|
|
57
|
+
case 'e': {
|
|
58
|
+
return { kind: 'effort', level: rest };
|
|
59
|
+
}
|
|
50
60
|
case 'model':
|
|
51
61
|
case 'm': {
|
|
52
|
-
if (!rest)
|
|
53
|
-
return { kind: 'unknown', raw: trimmed };
|
|
54
62
|
return { kind: 'model', model: rest };
|
|
55
63
|
}
|
|
56
64
|
default: return { kind: 'unknown', raw: trimmed };
|
package/dist/tui/app.d.ts
CHANGED
package/dist/tui/app.js
CHANGED
|
@@ -233,9 +233,10 @@ export function App(props) {
|
|
|
233
233
|
streamingIdRef.current = null; }, [status.status]);
|
|
234
234
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
235
235
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
236
|
+
const clearTranscript = useCallback(() => { streamingIdRef.current = null; setTranscript([]); setPlan([]); }, []);
|
|
236
237
|
const onMountedRef = useRef(props.onMounted);
|
|
237
238
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
238
|
-
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan]);
|
|
239
|
+
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan, clearTranscript]);
|
|
239
240
|
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
240
241
|
n.delete(id);
|
|
241
242
|
else
|
package/package.json
CHANGED