klyro 0.1.41 → 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/agent/runtime.d.ts +5 -0
- package/dist/agent/runtime.js +44 -14
- 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/tools/fs/apply-patch.js +38 -1
- package/dist/tools/shell/shell-exec.js +14 -3
- package/dist/tui/app.d.ts +3 -2
- package/dist/tui/app.js +15 -14
- package/dist/verification/baseline.js +14 -13
- package/dist/verification/classify.js +21 -10
- package/dist/verification/engine.js +57 -10
- package/dist/verification/scoped.js +14 -13
- package/package.json +60 -60
package/dist/agent/runtime.d.ts
CHANGED
|
@@ -191,6 +191,11 @@ export interface RunResult {
|
|
|
191
191
|
}
|
|
192
192
|
/** Convert a registry of tools into ToolDefinitions for the provider. */
|
|
193
193
|
export declare function toolDefinitions(registry: ToolRegistry): ToolDefinition[];
|
|
194
|
+
/** Estimate USD cost of a usage block given the model name. */
|
|
195
|
+
export declare function estimateCost(model: string, usage: {
|
|
196
|
+
input: number;
|
|
197
|
+
output: number;
|
|
198
|
+
}): number;
|
|
194
199
|
/** Run the autonomous loop. */
|
|
195
200
|
export declare function run(opts: RunOptions, deps: RuntimeDeps): Promise<RunResult>;
|
|
196
201
|
export declare function defaultSystemPrompt(ctx: {
|
package/dist/agent/runtime.js
CHANGED
|
@@ -35,6 +35,36 @@ export function toolDefinitions(registry) {
|
|
|
35
35
|
inputSchema: t.function.parameters,
|
|
36
36
|
}));
|
|
37
37
|
}
|
|
38
|
+
// BUG-005: Model-aware cost estimation with sensible defaults.
|
|
39
|
+
// Rates are per-1K tokens (input / output). Local models are $0.
|
|
40
|
+
const MODEL_RATES = [
|
|
41
|
+
{ test: (m) => /gpt-4/i.test(m), input: 0.003, output: 0.015 },
|
|
42
|
+
{ test: (m) => /gpt-3\.5/i.test(m), input: 0.0005, output: 0.0015 },
|
|
43
|
+
{ test: (m) => /claude|anthropic/i.test(m), input: 0.003, output: 0.015 },
|
|
44
|
+
{ test: (m) => /gemini/i.test(m), input: 0.00075, output: 0.003 },
|
|
45
|
+
{ test: (m) => /o1/i.test(m), input: 0.015, output: 0.06 },
|
|
46
|
+
];
|
|
47
|
+
/** Estimate USD cost of a usage block given the model name. */
|
|
48
|
+
export function estimateCost(model, usage) {
|
|
49
|
+
const match = MODEL_RATES.find((r) => r.test(model));
|
|
50
|
+
const { input: inRate, output: outRate } = match ?? { input: 0.003, output: 0.015 };
|
|
51
|
+
return (usage.input / 1000) * inRate + (usage.output / 1000) * outRate;
|
|
52
|
+
}
|
|
53
|
+
// PERF-002: Memoized token counting cache.
|
|
54
|
+
let tokenCache = {
|
|
55
|
+
lastRef: null,
|
|
56
|
+
lastSystem: undefined,
|
|
57
|
+
lastCount: 0,
|
|
58
|
+
};
|
|
59
|
+
/** Memoized totalTokens � only recomputes when transcript ref or system changes. */
|
|
60
|
+
function cachedTotalTokens(system, messages) {
|
|
61
|
+
if (tokenCache.lastRef === messages && tokenCache.lastSystem === system) {
|
|
62
|
+
return tokenCache.lastCount;
|
|
63
|
+
}
|
|
64
|
+
const count = totalTokens(system, messages);
|
|
65
|
+
tokenCache = { lastRef: messages, lastSystem: system, lastCount: count };
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
38
68
|
/** Run the autonomous loop. */
|
|
39
69
|
export async function run(opts, deps) {
|
|
40
70
|
const maxSteps = opts.maxTurns ?? opts.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
@@ -143,12 +173,10 @@ export async function run(opts, deps) {
|
|
|
143
173
|
// 5.2 — stuck detection state
|
|
144
174
|
const callHistory = [];
|
|
145
175
|
const fileEditCounts = new Map();
|
|
146
|
-
let stuckCount = 0;
|
|
147
|
-
let lastSignal;
|
|
148
176
|
outer: while (steps < maxSteps) {
|
|
149
177
|
// 5.1 limits: max-cost, max-time
|
|
150
178
|
if (maxCost !== undefined) {
|
|
151
|
-
const cost = (
|
|
179
|
+
const cost = estimateCost(opts.model, usage);
|
|
152
180
|
if (cost >= maxCost) {
|
|
153
181
|
setPhase('limit');
|
|
154
182
|
await closeTracer();
|
|
@@ -189,10 +217,11 @@ export async function run(opts, deps) {
|
|
|
189
217
|
const BUDGET = { total: 120_000, reservedOutput: 4000 };
|
|
190
218
|
let reqMessages = transcript;
|
|
191
219
|
let reqSystem = systemPrompt;
|
|
192
|
-
if (
|
|
220
|
+
if (cachedTotalTokens(systemPrompt, transcript) > BUDGET.total) {
|
|
193
221
|
const c = compressTranscript(systemPrompt, transcript, BUDGET);
|
|
194
222
|
reqSystem = c.system;
|
|
195
223
|
reqMessages = c.messages;
|
|
224
|
+
tokenCache = { lastRef: null, lastSystem: undefined, lastCount: 0 };
|
|
196
225
|
if (c.dropped > 0)
|
|
197
226
|
emitKlyro({ type: 'context.compacted', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', dropped: c.dropped });
|
|
198
227
|
}
|
|
@@ -279,6 +308,8 @@ export async function run(opts, deps) {
|
|
|
279
308
|
const assistantMsg = { role: 'assistant', content: assistantContent };
|
|
280
309
|
transcript.push(assistantMsg);
|
|
281
310
|
await checkpoint(assistantMsg);
|
|
311
|
+
// Invalidate token cache since transcript changed
|
|
312
|
+
tokenCache = { lastRef: null, lastSystem: undefined, lastCount: 0 };
|
|
282
313
|
// No tool calls → potential completion (Level 8 verify gate)
|
|
283
314
|
if (opts.signal?.aborted) {
|
|
284
315
|
finalText = textBuf;
|
|
@@ -363,7 +394,7 @@ export async function run(opts, deps) {
|
|
|
363
394
|
}
|
|
364
395
|
// 6.4 — classify
|
|
365
396
|
const baseline = await getBaseline(opts.cwd, verifyCmd);
|
|
366
|
-
const isFlaky = await rerunOnce(opts.cwd, cmdToRun,
|
|
397
|
+
const isFlaky = await rerunOnce(opts.cwd, cmdToRun, opts.verify?.timeoutMs ?? 45_000);
|
|
367
398
|
const cls = classifyFailure({ failure: vResult.failure, stdout: vResult.stdout, stderr: vResult.stderr }, baseline, isFlaky);
|
|
368
399
|
if (cls === 'flaky') {
|
|
369
400
|
// rerun succeeded on second try — treat as flaky, don't count as repair
|
|
@@ -596,16 +627,15 @@ export async function run(opts, deps) {
|
|
|
596
627
|
}
|
|
597
628
|
};
|
|
598
629
|
// 3.5 — parallel if all concurrencySafe, sequential otherwise
|
|
599
|
-
//
|
|
630
|
+
// BUG-002: preserve call order — run sequentially even when allSafe to avoid out-of-order transcript
|
|
631
|
+
// Parallel execution previously pushed tool_results out of order via Promise.all
|
|
600
632
|
if (allSafe) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
// So we run Promise.all for execution but checkpoint writes are already serialized via store mutex
|
|
608
|
-
await Promise.all(finalizedCalls.map((c) => runOne(c)));
|
|
633
|
+
for (const call of finalizedCalls) {
|
|
634
|
+
toolCallCount++;
|
|
635
|
+
await runOne(call);
|
|
636
|
+
if (opts.signal?.aborted)
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
609
639
|
}
|
|
610
640
|
else {
|
|
611
641
|
for (const call of finalizedCalls) {
|
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 };
|
|
@@ -6,7 +6,8 @@ import * as path from 'node:path';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { defineTool } from '../types.js';
|
|
8
8
|
import { resolveAndFollowSymlinks } from '../../policy/path-guard.js';
|
|
9
|
-
import { safe } from '../normalize.js';
|
|
9
|
+
import { safe, TOOL_ERROR_CODES } from '../normalize.js';
|
|
10
|
+
import { wasRead } from './read-history.js';
|
|
10
11
|
const InputSchema = z.object({
|
|
11
12
|
patch: z.string().min(1).describe('Unified diff patch text'),
|
|
12
13
|
});
|
|
@@ -29,6 +30,18 @@ export const applyPatchTool = defineTool({
|
|
|
29
30
|
// Flush previous
|
|
30
31
|
if (currentFile && fileContent !== null) {
|
|
31
32
|
const { resolved } = await resolveAndFollowSymlinks(ctx.cwd, currentFile);
|
|
33
|
+
if (fileContent === null)
|
|
34
|
+
throw Object.assign(new Error('fileContent is null — cannot write'), { code: TOOL_ERROR_CODES.INTERNAL });
|
|
35
|
+
try {
|
|
36
|
+
const existing = await fs.readFile(resolved, 'utf-8');
|
|
37
|
+
if (!wasRead(currentFile) && existing.length > 200) {
|
|
38
|
+
throw Object.assign(new Error('File was not read this session and is >200 bytes — re-read before writing (POLICY_DENIED)'), { code: 'POLICY_DENIED' });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
if (e && typeof e === 'object' && e.code === 'POLICY_DENIED')
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
32
45
|
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
33
46
|
await fs.writeFile(resolved, fileContent, 'utf-8');
|
|
34
47
|
patchedFiles.push(currentFile);
|
|
@@ -48,6 +61,18 @@ export const applyPatchTool = defineTool({
|
|
|
48
61
|
if (line.startsWith('*** Add File:')) {
|
|
49
62
|
if (currentFile && fileContent !== null) {
|
|
50
63
|
const { resolved } = await resolveAndFollowSymlinks(ctx.cwd, currentFile);
|
|
64
|
+
if (fileContent === null)
|
|
65
|
+
throw Object.assign(new Error('fileContent is null — cannot write'), { code: TOOL_ERROR_CODES.INTERNAL });
|
|
66
|
+
try {
|
|
67
|
+
const existing = await fs.readFile(resolved, 'utf-8');
|
|
68
|
+
if (!wasRead(currentFile) && existing.length > 200) {
|
|
69
|
+
throw Object.assign(new Error('File was not read this session and is >200 bytes — re-read before writing (POLICY_DENIED)'), { code: 'POLICY_DENIED' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e && typeof e === 'object' && e.code === 'POLICY_DENIED')
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
51
76
|
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
52
77
|
await fs.writeFile(resolved, fileContent, 'utf-8');
|
|
53
78
|
patchedFiles.push(currentFile);
|
|
@@ -67,6 +92,18 @@ export const applyPatchTool = defineTool({
|
|
|
67
92
|
}
|
|
68
93
|
if (currentFile && fileContent !== null) {
|
|
69
94
|
const { resolved } = await resolveAndFollowSymlinks(ctx.cwd, currentFile);
|
|
95
|
+
if (fileContent === null)
|
|
96
|
+
throw Object.assign(new Error('fileContent is null — cannot write'), { code: TOOL_ERROR_CODES.INTERNAL });
|
|
97
|
+
try {
|
|
98
|
+
const existing = await fs.readFile(resolved, 'utf-8');
|
|
99
|
+
if (!wasRead(currentFile) && existing.length > 200) {
|
|
100
|
+
throw Object.assign(new Error('File was not read this session and is >200 bytes — re-read before writing (POLICY_DENIED)'), { code: 'POLICY_DENIED' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
if (e && typeof e === 'object' && e.code === 'POLICY_DENIED')
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
70
107
|
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
71
108
|
await fs.writeFile(resolved, fileContent, 'utf-8');
|
|
72
109
|
patchedFiles.push(currentFile);
|
|
@@ -48,10 +48,21 @@ function filteredEnv(extra) {
|
|
|
48
48
|
out[k] = v;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
//
|
|
51
|
+
// Merge user extra AFTER filtering — but only allowed prefixes, block injection vectors
|
|
52
|
+
if (extra) {
|
|
53
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
54
|
+
if (k === 'NODE_OPTIONS' || k === 'LD_PRELOAD' || k === 'LD_LIBRARY_PATH')
|
|
55
|
+
continue;
|
|
56
|
+
if (k.includes('SECRET') || k.includes('TOKEN') || k === 'ANTHROPIC_API_KEY' || k === 'OPENAI_API_KEY')
|
|
57
|
+
continue;
|
|
58
|
+
if (ALLOWED_ENV_PREFIXES.some((p) => k.startsWith(p)) || k === 'PATH' || k === 'PWD' || k === 'TMPDIR' || k === 'TEMP') {
|
|
59
|
+
out[k] = v;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
52
63
|
out.PATH = process.env.PATH;
|
|
53
|
-
if (
|
|
54
|
-
|
|
64
|
+
if (out.NODE_OPTIONS)
|
|
65
|
+
delete out.NODE_OPTIONS;
|
|
55
66
|
return out;
|
|
56
67
|
}
|
|
57
68
|
// Interactive command detection (3.3)
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Klyro TUI
|
|
3
|
-
* Header 3 rows, guide
|
|
2
|
+
* Klyro TUI - opencode-clean - no clumsy words, correct wrap, markdown, scroll
|
|
3
|
+
* Header 3 rows, guide │ at col2, ● Klyro accent, prose wrapped at word boundaries
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import type { StatusSnapshot } from './status.js';
|
|
@@ -21,6 +21,7 @@ export interface AppProps {
|
|
|
21
21
|
appendDelta: (text: string) => void;
|
|
22
22
|
updateStatus: (s: Partial<StatusSnapshot>) => void;
|
|
23
23
|
updatePlan: (p: PlanStep[]) => void;
|
|
24
|
+
clearTranscript: () => void;
|
|
24
25
|
}) => void;
|
|
25
26
|
version?: string;
|
|
26
27
|
isFullscreen?: boolean;
|
package/dist/tui/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Klyro TUI
|
|
4
|
-
* Header 3 rows, guide
|
|
3
|
+
* Klyro TUI - opencode-clean - no clumsy words, correct wrap, markdown, scroll
|
|
4
|
+
* Header 3 rows, guide │ at col2, ● Klyro accent, prose wrapped at word boundaries
|
|
5
5
|
*/
|
|
6
6
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
7
7
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
@@ -19,7 +19,7 @@ function Header({ cwd, model, version, width }) {
|
|
|
19
19
|
return '';
|
|
20
20
|
} })();
|
|
21
21
|
const showLinks = width >= 120;
|
|
22
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: tokens.colors.accent, children: ["KLYRO v", version] }), showLinks ? _jsx(Text, { color: tokens.colors.dim, children: "\
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: tokens.colors.accent, children: ["KLYRO v", version] }), showLinks ? _jsx(Text, { color: tokens.colors.dim, children: "\u2502 /help /config /clear /exit" }) : null] }), _jsxs(Text, { color: tokens.colors.dim, children: [model, "[200k] \u00B7 API Usage Billing"] }), _jsxs(Text, { color: tokens.colors.dim, children: [cwd, branch ? ` · ${branch}` : ''] })] }));
|
|
23
23
|
}
|
|
24
24
|
function verbForTool(name) {
|
|
25
25
|
if (name === 'read_file')
|
|
@@ -71,7 +71,7 @@ function groupTools(items) {
|
|
|
71
71
|
flush();
|
|
72
72
|
return out;
|
|
73
73
|
}
|
|
74
|
-
// Simple markdown: **bold**
|
|
74
|
+
// Simple markdown: **bold** †’ bold, keep lists/tables, wrap at word boundaries
|
|
75
75
|
function MarkdownText({ text, dim, width }) {
|
|
76
76
|
// Split by **bold** segments
|
|
77
77
|
const parts = [];
|
|
@@ -89,7 +89,7 @@ function MarkdownText({ text, dim, width }) {
|
|
|
89
89
|
parts.push(_jsx(Text, { color: dim ? tokens.colors.dim : undefined, wrap: "wrap", children: text.slice(last) }, `t-${idx++}`));
|
|
90
90
|
if (parts.length === 0)
|
|
91
91
|
return _jsx(Text, { color: dim ? tokens.colors.dim : undefined, wrap: "wrap", children: text });
|
|
92
|
-
// Render as single line with bold segments
|
|
92
|
+
// Render as single line with bold segments — Ink will wrap the parent Box
|
|
93
93
|
return _jsx(Text, { wrap: "wrap", children: parts });
|
|
94
94
|
}
|
|
95
95
|
// Chat scroll state: scrollOffset, pinned (user scrolled away from bottom),
|
|
@@ -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
|
|
@@ -330,9 +331,9 @@ export function App(props) {
|
|
|
330
331
|
const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
|
|
331
332
|
const totalTokens = status.usageInput + status.usageOutput;
|
|
332
333
|
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
|
|
333
|
-
const baseHints = status.status === 'running' ? 'ctrl+c to stop
|
|
334
|
-
const hints = maxOffset > 0 && isFullscreen ? `${baseHints}
|
|
335
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: isFullscreen ? height - 1 : undefined, children: [_jsx(Header, { cwd: props.cwd, model: status.model, version: ver, width: width }), _jsxs(Box, { flexDirection: "row", flexGrow: isFullscreen ? 1 : 0, overflow: isFullscreen ? 'hidden' : undefined, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: isFullscreen ? 'hidden' : undefined, paddingX: 0, children: [grouped.length === 0 ? (_jsx(Text, { color: tokens.colors.dim, children: "Message Klyro
|
|
334
|
+
const baseHints = status.status === 'running' ? 'ctrl+c to stop · enter to queue · ctrl+o expand' : transcript.length === 0 ? 'shift+tab to cycle · †‘†“ for history · / for commands' : 'enter to send · shift+enter newline · @ to attach';
|
|
335
|
+
const hints = maxOffset > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
|
|
336
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: isFullscreen ? height - 1 : undefined, children: [_jsx(Header, { cwd: props.cwd, model: status.model, version: ver, width: width }), _jsxs(Box, { flexDirection: "row", flexGrow: isFullscreen ? 1 : 0, overflow: isFullscreen ? 'hidden' : undefined, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: isFullscreen ? 'hidden' : undefined, paddingX: 0, children: [grouped.length === 0 ? (_jsx(Text, { color: tokens.colors.dim, children: "Message Klyro..." })) : visibleGrouped.map((item) => {
|
|
336
337
|
if (item.verb) {
|
|
337
338
|
const gr = item;
|
|
338
339
|
const isExpanded = expandedGroups.has(gr.id);
|
|
@@ -365,8 +366,8 @@ export function App(props) {
|
|
|
365
366
|
return `Edited ${gr.items.length} files`;
|
|
366
367
|
return `${gr.verb} ${gr.items.length} items`;
|
|
367
368
|
})();
|
|
368
|
-
const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '
|
|
369
|
-
const marker = isExpanded ? '
|
|
369
|
+
const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? 'œ—' : `${gr.totalMs}ms`;
|
|
370
|
+
const marker = isExpanded ? '–¼' : 'œ“';
|
|
370
371
|
const markerColor = gr.status === 'error' ? tokens.colors.err : gr.status === 'running' ? tokens.colors.warn : tokens.colors.ok;
|
|
371
372
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: markerColor, children: [marker, " ", verbLine] }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => {
|
|
372
373
|
let friendly = '';
|
|
@@ -394,11 +395,11 @@ export function App(props) {
|
|
|
394
395
|
return _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { wrap: "wrap", children: it.text })] }, it.id);
|
|
395
396
|
}
|
|
396
397
|
if (it.kind === 'text') {
|
|
397
|
-
// prose
|
|
398
|
+
// prose — render markdown, not raw **, with proper wrap and guide
|
|
398
399
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [g('agentBullet'), " Klyro"] })] }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.dim, children: [" ", g('guide'), " "] }), _jsx(Box, { flexGrow: 1, children: _jsx(MarkdownText, { text: it.text }) })] }) })] }, it.id));
|
|
399
400
|
}
|
|
400
401
|
if (it.kind === 'error')
|
|
401
|
-
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), " \
|
|
402
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), " \u0153\u2014 ", it.message] }) }, it.id);
|
|
402
403
|
if (it.kind === 'policy')
|
|
403
404
|
return null;
|
|
404
405
|
if (it.kind === 'file_changed')
|
|
@@ -406,5 +407,5 @@ export function App(props) {
|
|
|
406
407
|
if (it.kind === 'diff')
|
|
407
408
|
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.soft, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsx(Text, { color: tokens.colors.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.colors.ok : l.kind === 'remove' ? tokens.colors.err : tokens.colors.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] }, it.id));
|
|
408
409
|
return null;
|
|
409
|
-
}), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking..." }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", p.title] })] }, p.id)))] })) : null, queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", q.slice(0, 60), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '
|
|
410
|
+
}), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking..." }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", p.title] })] }, p.id)))] })) : null, queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", q.slice(0, 60), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), pinned && pendingNew > 0 ? (_jsx(Box, { justifyContent: "flex-end", paddingX: 1, marginTop: -1, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ↓ ', pendingNew, ' new ', pendingNew === 1 ? 'message' : 'messages', ' '] }) })) : null, _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), _jsxs(Text, { wrap: "wrap", children: [input || _jsx(Text, { color: tokens.colors.dim, children: "Message Klyro..." }), "|"] })] }), _jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: tokens.colors.dim, children: [baseHints, maxOffset > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct > 0 ? `${ctxPct}% ctx · ` : '', status.status === 'running' ? 'auto mode on ●' : ''] })] })] }));
|
|
410
411
|
}
|
|
@@ -66,17 +66,11 @@ export async function runBaseline(cwd, command, timeoutMs = 90_000) {
|
|
|
66
66
|
return baseline;
|
|
67
67
|
}
|
|
68
68
|
const MAX_BASELINE_BYTES = 256 * 1024;
|
|
69
|
-
function cap(cur, chunk) {
|
|
70
|
-
if (cur.length >= MAX_BASELINE_BYTES)
|
|
71
|
-
return cur;
|
|
72
|
-
const n = cur + chunk;
|
|
73
|
-
return n.length > MAX_BASELINE_BYTES ? n.slice(0, MAX_BASELINE_BYTES) + '\n... [truncated]' : n;
|
|
74
|
-
}
|
|
75
69
|
function runCmd(cwd, command, timeoutMs) {
|
|
76
70
|
return new Promise((resolve) => {
|
|
77
71
|
const child = spawn(command, { cwd, shell: true, env: process.env });
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
const outChunks = [];
|
|
73
|
+
const errChunks = [];
|
|
80
74
|
let done = false;
|
|
81
75
|
const timer = setTimeout(() => {
|
|
82
76
|
if (done)
|
|
@@ -86,24 +80,31 @@ function runCmd(cwd, command, timeoutMs) {
|
|
|
86
80
|
child.kill();
|
|
87
81
|
}
|
|
88
82
|
catch { /* ignore */ }
|
|
89
|
-
|
|
83
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
|
|
84
|
+
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
|
|
85
|
+
resolve({ ok: false, exitCode: -1, stdout: so, stderr: se + '\n[baseline timeout]' });
|
|
90
86
|
}, timeoutMs);
|
|
91
|
-
child.stdout.on('data', (b) => {
|
|
92
|
-
|
|
87
|
+
child.stdout.on('data', (b) => { if (Buffer.concat(outChunks).length < MAX_BASELINE_BYTES)
|
|
88
|
+
outChunks.push(b); });
|
|
89
|
+
child.stderr.on('data', (b) => { if (Buffer.concat(errChunks).length < MAX_BASELINE_BYTES)
|
|
90
|
+
errChunks.push(b); });
|
|
93
91
|
child.on('close', (code) => {
|
|
94
92
|
if (done)
|
|
95
93
|
return;
|
|
96
94
|
done = true;
|
|
97
95
|
clearTimeout(timer);
|
|
98
96
|
const exit = typeof code === 'number' ? code : -1;
|
|
99
|
-
|
|
97
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
|
|
98
|
+
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
|
|
99
|
+
resolve({ ok: exit === 0, exitCode: exit, stdout: so, stderr: se });
|
|
100
100
|
});
|
|
101
101
|
child.on('error', (err) => {
|
|
102
102
|
if (done)
|
|
103
103
|
return;
|
|
104
104
|
done = true;
|
|
105
105
|
clearTimeout(timer);
|
|
106
|
-
|
|
106
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
|
|
107
|
+
resolve({ ok: false, exitCode: -1, stdout: so, stderr: String(err) });
|
|
107
108
|
});
|
|
108
109
|
});
|
|
109
110
|
}
|
|
@@ -7,7 +7,7 @@ import { spawn } from 'node:child_process';
|
|
|
7
7
|
export function classifyFailure(current, baseline, flakyRerunOk) {
|
|
8
8
|
const combined = (current.stderr + '\n' + current.stdout).toLowerCase();
|
|
9
9
|
// env: missing binary, network, permission, no such file, EACCES etc
|
|
10
|
-
if (/enoent|command not found|no such file|network|econn|etimedout|eacces|permission denied|
|
|
10
|
+
if (/enoent|command not found|no such file|network|econn|etimedout|eacces|permission denied|environment variable/.test(combined) && /error/i.test(combined)) {
|
|
11
11
|
// only if not a real test failure but env
|
|
12
12
|
if (!current.failure || current.failure.type === 'unknown')
|
|
13
13
|
return 'env';
|
|
@@ -42,7 +42,7 @@ export async function rerunOnce(cwd, command, timeoutMs = 45_000) {
|
|
|
42
42
|
const t = setTimeout(() => { if (!done) {
|
|
43
43
|
done = true;
|
|
44
44
|
try {
|
|
45
|
-
child.kill();
|
|
45
|
+
child.kill('SIGKILL');
|
|
46
46
|
}
|
|
47
47
|
catch { }
|
|
48
48
|
resolve(false);
|
|
@@ -72,19 +72,30 @@ export async function gatherRepairContext(cwd, failure) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
// hunks: git diff --stat + --unified=2 for changed files
|
|
75
|
-
const hunks = await
|
|
76
|
-
const
|
|
77
|
-
|
|
75
|
+
const hunks = await execCapturePipe(cwd, 'git diff --stat && echo "---" && git diff -U2 2>&1 | head -n 300');
|
|
76
|
+
const blamePath = failure?.files[0]?.path;
|
|
77
|
+
const blame = blamePath && /^[\w./-]+$/.test(blamePath) && !blamePath.includes('..')
|
|
78
|
+
? await execCaptureArgv(cwd, blamePath, ['--no-pager', 'blame', '--'])
|
|
78
79
|
: '';
|
|
79
80
|
return { failingTests, hunks, blame };
|
|
80
81
|
}
|
|
81
|
-
function
|
|
82
|
+
function execCapturePipe(cwd, cmd) {
|
|
82
83
|
return new Promise((resolve) => {
|
|
83
84
|
const child = spawn(cmd, { cwd, shell: true, env: process.env });
|
|
84
|
-
|
|
85
|
-
child.stdout.on('data', (b) => {
|
|
86
|
-
child.stderr.on('data', (b) => {
|
|
87
|
-
child.on('close', () => resolve(
|
|
85
|
+
const chunks = [];
|
|
86
|
+
child.stdout.on('data', (b) => { chunks.push(b); });
|
|
87
|
+
child.stderr.on('data', (b) => { chunks.push(b); });
|
|
88
|
+
child.on('close', () => resolve(Buffer.concat(chunks).toString().slice(0, 4000)));
|
|
89
|
+
child.on('error', () => resolve(''));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function execCaptureArgv(cwd, file, args) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const child = spawn('git', [...args, file], { cwd, shell: false, env: process.env });
|
|
95
|
+
const chunks = [];
|
|
96
|
+
child.stdout.on('data', (b) => { chunks.push(b); });
|
|
97
|
+
child.stderr.on('data', (b) => { chunks.push(b); });
|
|
98
|
+
child.on('close', () => resolve(Buffer.concat(chunks).toString().split('\n').slice(0, 20).join('\n')));
|
|
88
99
|
child.on('error', () => resolve(''));
|
|
89
100
|
});
|
|
90
101
|
}
|
|
@@ -17,41 +17,88 @@ function appendCapped(current, chunk) {
|
|
|
17
17
|
const next = current + chunk;
|
|
18
18
|
return next.length > MAX_VERIFY_BYTES ? next.slice(0, MAX_VERIFY_BYTES) + '\n... [truncated]' : next;
|
|
19
19
|
}
|
|
20
|
+
// SEC-004: denylist for dangerous patterns in verify commands.
|
|
21
|
+
// Reuses the same patterns as shell-exec.ts DANGEROUS_PATTERNS.
|
|
22
|
+
const DANGEROUS_VERIFY_PATTERNS = [
|
|
23
|
+
{ pattern: /rm\s+-rf?\s+\//, reason: 'recursive delete at filesystem root' },
|
|
24
|
+
{ pattern: /rm\s+-rf?\s+\/\/+/, reason: 'recursive delete at filesystem root (//)' },
|
|
25
|
+
{ pattern: /rm\s+-rf?\s+\/\*/, reason: 'recursive delete at filesystem root (/*)' },
|
|
26
|
+
{ pattern: /rm\s+-rf?\s+\.\s*($|[;&|])/, reason: 'recursive delete current directory' },
|
|
27
|
+
{ pattern: /rm\s+-rf?\s+\*\s*($|[;&|])/, reason: 'recursive delete all files via *' },
|
|
28
|
+
{ pattern: /rm\s+-rf?\s+\.\/\*\s*($|[;&|])/, reason: 'recursive delete all files' },
|
|
29
|
+
{ pattern: /rm\s+-rf?\s+~(\/|$)/, reason: 'recursive delete home directory via ~' },
|
|
30
|
+
{ pattern: /rm\s+-rf?\s+\$HOME\b/, reason: 'recursive delete home via $HOME' },
|
|
31
|
+
{ pattern: /rm\s+-rf?\s+\$PWD\b/, reason: 'recursive delete via $PWD' },
|
|
32
|
+
{ pattern: /del\s+\/s\s+\/q\s+[a-z]:\\/i, reason: 'recursive delete on Windows drive root' },
|
|
33
|
+
{ pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, reason: 'fork bomb' },
|
|
34
|
+
{ pattern: /bomb\(\)\s*\{\s*bomb\|bomb/, reason: 'fork bomb variant' },
|
|
35
|
+
{ pattern: />\s*\/dev\/sd[a-z]/, reason: 'overwrite raw block device' },
|
|
36
|
+
{ pattern: /mkfs(\.|\s)/, reason: 'format filesystem' },
|
|
37
|
+
{ pattern: /dd\s+.*of=\/dev\//, reason: 'dd write to device' },
|
|
38
|
+
{ pattern: /chmod\s+-R\s+777\s+\//, reason: 'chmod 777 on root' },
|
|
39
|
+
{ pattern: /curl.*\|\s*(sh|bash|zsh|python|python3|perl|ruby|php)/i, reason: 'curl|sh to unknown host' },
|
|
40
|
+
{ pattern: /wget.*\|\s*(sh|bash|python|perl|ruby)/i, reason: 'wget|sh pipe' },
|
|
41
|
+
{ pattern: /rm\s+-rf\s+--no-preserve-root\s+\//, reason: 'recursive delete --no-preserve-root' },
|
|
42
|
+
{ pattern: /\$\(/, reason: 'command substitution $()' },
|
|
43
|
+
{ pattern: /`[^`]*`/, reason: 'command substitution via backticks' },
|
|
44
|
+
{ pattern: /\|\s*bash\b|\|\s*sh\b/, reason: 'pipe to shell' },
|
|
45
|
+
{ pattern: /;\s*rm\s+-rf/, reason: 'chained rm -rf' },
|
|
46
|
+
{ pattern: /&&\s*rm\s+-rf/, reason: 'chained rm -rf' },
|
|
47
|
+
{ pattern: /\|\|\s*rm\s+-rf/, reason: 'chained rm -rf' },
|
|
48
|
+
];
|
|
20
49
|
export async function verify(opts) {
|
|
50
|
+
// SEC-004: reject dangerous patterns before spawning
|
|
51
|
+
for (const { pattern, reason } of DANGEROUS_VERIFY_PATTERNS) {
|
|
52
|
+
if (pattern.test(opts.command)) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
exitCode: -1,
|
|
56
|
+
stdout: '',
|
|
57
|
+
stderr: `Command blocked: ${reason}`,
|
|
58
|
+
failure: { type: 'runtime', files: [], raw: `Command blocked: ${reason}`, exitCode: -1 },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
21
62
|
const timeout = opts.timeoutMs ?? 5 * 60 * 1000;
|
|
22
63
|
return new Promise((resolve) => {
|
|
23
64
|
const child = spawn(opts.command, { cwd: opts.cwd, shell: true, env: process.env });
|
|
24
|
-
|
|
25
|
-
|
|
65
|
+
const outChunks = [];
|
|
66
|
+
const errChunks = [];
|
|
26
67
|
let done = false;
|
|
27
68
|
const timer = setTimeout(() => {
|
|
28
69
|
if (done)
|
|
29
70
|
return;
|
|
30
71
|
child.kill();
|
|
31
72
|
done = true;
|
|
32
|
-
const
|
|
73
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
|
|
74
|
+
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
|
|
75
|
+
const raw = se + '\n' + so;
|
|
33
76
|
resolve({
|
|
34
77
|
ok: false,
|
|
35
78
|
exitCode: -1,
|
|
36
|
-
stdout,
|
|
37
|
-
stderr:
|
|
79
|
+
stdout: so,
|
|
80
|
+
stderr: se + '\n[verify timeout]',
|
|
38
81
|
failure: { type: 'runtime', files: [], raw, exitCode: -1 },
|
|
39
82
|
});
|
|
40
83
|
}, timeout);
|
|
41
|
-
child.stdout.on('data', (b) => {
|
|
42
|
-
|
|
84
|
+
child.stdout.on('data', (b) => { if (Buffer.concat(outChunks).length < MAX_VERIFY_BYTES)
|
|
85
|
+
outChunks.push(b); });
|
|
86
|
+
child.stderr.on('data', (b) => { if (Buffer.concat(errChunks).length < MAX_VERIFY_BYTES)
|
|
87
|
+
errChunks.push(b); });
|
|
43
88
|
child.on('close', (code) => {
|
|
44
89
|
if (done)
|
|
45
90
|
return;
|
|
46
91
|
done = true;
|
|
47
92
|
clearTimeout(timer);
|
|
93
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
|
|
94
|
+
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
|
|
48
95
|
const exit = typeof code === 'number' ? code : -1;
|
|
49
96
|
if (exit === 0) {
|
|
50
|
-
resolve({ ok: true, exitCode: 0, stdout, stderr });
|
|
97
|
+
resolve({ ok: true, exitCode: 0, stdout: so, stderr: se });
|
|
51
98
|
return;
|
|
52
99
|
}
|
|
53
|
-
const failure = detect(
|
|
54
|
-
resolve({ ok: false, exitCode: exit, stdout, stderr, failure });
|
|
100
|
+
const failure = detect(so, se, exit);
|
|
101
|
+
resolve({ ok: false, exitCode: exit, stdout: so, stderr: se, failure });
|
|
55
102
|
});
|
|
56
103
|
});
|
|
57
104
|
}
|
|
@@ -71,17 +71,11 @@ export function buildScopedCommand(cwd, baseCommand, relatedTests) {
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
73
|
const MAX_SCOPED_BYTES = 256 * 1024;
|
|
74
|
-
function appendCappedScoped(cur, chunk) {
|
|
75
|
-
if (cur.length >= MAX_SCOPED_BYTES)
|
|
76
|
-
return cur;
|
|
77
|
-
const n = cur + chunk;
|
|
78
|
-
return n.length > MAX_SCOPED_BYTES ? n.slice(0, MAX_SCOPED_BYTES) + '\n... [truncated]' : n;
|
|
79
|
-
}
|
|
80
74
|
export async function runScopedVerify(cwd, command, timeoutMs = 45_000) {
|
|
81
75
|
return new Promise((resolve) => {
|
|
82
76
|
const child = spawn(command, { cwd, shell: true, env: process.env });
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
const outChunks = [];
|
|
78
|
+
const errChunks = [];
|
|
85
79
|
let done = false;
|
|
86
80
|
const timer = setTimeout(() => {
|
|
87
81
|
if (done)
|
|
@@ -91,24 +85,31 @@ export async function runScopedVerify(cwd, command, timeoutMs = 45_000) {
|
|
|
91
85
|
child.kill();
|
|
92
86
|
}
|
|
93
87
|
catch { /* ignore */ }
|
|
94
|
-
|
|
88
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
|
|
89
|
+
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
|
|
90
|
+
resolve({ ok: false, exitCode: -1, stdout: so, stderr: se + '\n[scoped timeout]' });
|
|
95
91
|
}, timeoutMs);
|
|
96
|
-
child.stdout.on('data', (b) => {
|
|
97
|
-
|
|
92
|
+
child.stdout.on('data', (b) => { if (Buffer.concat(outChunks).length < MAX_SCOPED_BYTES)
|
|
93
|
+
outChunks.push(b); });
|
|
94
|
+
child.stderr.on('data', (b) => { if (Buffer.concat(errChunks).length < MAX_SCOPED_BYTES)
|
|
95
|
+
errChunks.push(b); });
|
|
98
96
|
child.on('close', (code) => {
|
|
99
97
|
if (done)
|
|
100
98
|
return;
|
|
101
99
|
done = true;
|
|
102
100
|
clearTimeout(timer);
|
|
103
101
|
const exit = typeof code === 'number' ? code : -1;
|
|
104
|
-
|
|
102
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
|
|
103
|
+
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
|
|
104
|
+
resolve({ ok: exit === 0, exitCode: exit, stdout: so, stderr: se });
|
|
105
105
|
});
|
|
106
106
|
child.on('error', (err) => {
|
|
107
107
|
if (done)
|
|
108
108
|
return;
|
|
109
109
|
done = true;
|
|
110
110
|
clearTimeout(timer);
|
|
111
|
-
|
|
111
|
+
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
|
|
112
|
+
resolve({ ok: false, exitCode: -1, stdout: so, stderr: String(err) });
|
|
112
113
|
});
|
|
113
114
|
});
|
|
114
115
|
}
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "klyro",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Klyro
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"klyro": "dist/index.js",
|
|
10
|
-
"ky": "dist/index.js"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"README.md",
|
|
15
|
-
"READ.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"engines": {
|
|
19
|
-
"node": ">=20"
|
|
20
|
-
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsc",
|
|
23
|
-
"start": "node dist/index.js",
|
|
24
|
-
"dev": "tsx src/index.ts",
|
|
25
|
-
"dev:watch": "tsx watch src/index.ts",
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"test": "vitest run",
|
|
28
|
-
"test:watch": "vitest",
|
|
29
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
30
|
-
"pack:dry": "npm pack --dry-run",
|
|
31
|
-
"publish:public": "npm publish --access public"
|
|
32
|
-
},
|
|
33
|
-
"keywords": [
|
|
34
|
-
"llm",
|
|
35
|
-
"ai",
|
|
36
|
-
"cli",
|
|
37
|
-
"openai",
|
|
38
|
-
"anthropic",
|
|
39
|
-
"harness",
|
|
40
|
-
"agent",
|
|
41
|
-
"streaming",
|
|
42
|
-
"klyro"
|
|
43
|
-
],
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"commander": "^12.1.0",
|
|
47
|
-
"ink": "^7.1.1",
|
|
48
|
-
"ink-spinner": "^5.0.0",
|
|
49
|
-
"react": "^19.2.0",
|
|
50
|
-
"zod": "^4.5.4"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@types/node": "^22.9.0",
|
|
54
|
-
"@types/react": "^19.2.18",
|
|
55
|
-
"ink-testing-library": "^4.0.0",
|
|
56
|
-
"tsx": "^4.23.13",
|
|
57
|
-
"typescript": "^5.5.4",
|
|
58
|
-
"vitest": "^4.1.11"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "klyro",
|
|
3
|
+
"version": "0.1.43",
|
|
4
|
+
"description": "Klyro \u2014 autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"klyro": "dist/index.js",
|
|
10
|
+
"ky": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"READ.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"start": "node dist/index.js",
|
|
24
|
+
"dev": "tsx src/index.ts",
|
|
25
|
+
"dev:watch": "tsx watch src/index.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
30
|
+
"pack:dry": "npm pack --dry-run",
|
|
31
|
+
"publish:public": "npm publish --access public"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"llm",
|
|
35
|
+
"ai",
|
|
36
|
+
"cli",
|
|
37
|
+
"openai",
|
|
38
|
+
"anthropic",
|
|
39
|
+
"harness",
|
|
40
|
+
"agent",
|
|
41
|
+
"streaming",
|
|
42
|
+
"klyro"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"commander": "^12.1.0",
|
|
47
|
+
"ink": "^7.1.1",
|
|
48
|
+
"ink-spinner": "^5.0.0",
|
|
49
|
+
"react": "^19.2.0",
|
|
50
|
+
"zod": "^4.5.4"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.9.0",
|
|
54
|
+
"@types/react": "^19.2.18",
|
|
55
|
+
"ink-testing-library": "^4.0.0",
|
|
56
|
+
"tsx": "^4.23.13",
|
|
57
|
+
"typescript": "^5.5.4",
|
|
58
|
+
"vitest": "^4.1.11"
|
|
59
|
+
}
|
|
60
|
+
}
|