klyro 0.1.4 → 0.1.6

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.
Files changed (78) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +1 -1
  2. package/dist/agent/anthropic-adapter.js +22 -20
  3. package/dist/agent/provider-adapter.js +30 -7
  4. package/dist/agent/registry.js +24 -7
  5. package/dist/agent/retry.js +54 -9
  6. package/dist/agent/runtime.d.ts +45 -1
  7. package/dist/agent/runtime.js +236 -23
  8. package/dist/chat.d.ts +3 -3
  9. package/dist/chat.js +137 -39
  10. package/dist/cli/auth.d.ts +9 -0
  11. package/dist/cli/auth.js +84 -0
  12. package/dist/cli/completion.d.ts +6 -0
  13. package/dist/cli/completion.js +66 -0
  14. package/dist/cli/config.d.ts +44 -0
  15. package/dist/cli/config.js +428 -0
  16. package/dist/cli/doctor.d.ts +8 -0
  17. package/dist/cli/doctor.js +136 -0
  18. package/dist/cli/errors.d.ts +5 -0
  19. package/dist/cli/errors.js +37 -0
  20. package/dist/cli/markdown.d.ts +9 -0
  21. package/dist/cli/markdown.js +77 -0
  22. package/dist/cli/repl.js +113 -5
  23. package/dist/cli/run.d.ts +9 -0
  24. package/dist/cli/run.js +97 -2
  25. package/dist/cli/slash/parser.d.ts +10 -0
  26. package/dist/cli/slash/parser.js +6 -1
  27. package/dist/cli/trace.d.ts +7 -0
  28. package/dist/cli/trace.js +63 -0
  29. package/dist/cli/update.d.ts +6 -0
  30. package/dist/cli/update.js +70 -0
  31. package/dist/context/system-prompt.d.ts +14 -0
  32. package/dist/context/system-prompt.js +47 -0
  33. package/dist/events/bus.d.ts +16 -0
  34. package/dist/events/bus.js +28 -0
  35. package/dist/events/catalog.d.ts +136 -0
  36. package/dist/events/catalog.js +5 -0
  37. package/dist/index.js +335 -22
  38. package/dist/persistence/session.d.ts +12 -0
  39. package/dist/persistence/session.js +45 -0
  40. package/dist/policy/engine.d.ts +11 -1
  41. package/dist/policy/engine.js +85 -2
  42. package/dist/providers/model-info.d.ts +18 -0
  43. package/dist/providers/model-info.js +18 -0
  44. package/dist/providers.js +3 -2
  45. package/dist/renderers/json.d.ts +11 -0
  46. package/dist/renderers/json.js +14 -0
  47. package/dist/renderers/terminal.d.ts +9 -0
  48. package/dist/renderers/terminal.js +41 -0
  49. package/dist/repl.js +52 -15
  50. package/dist/shared/errors.d.ts +18 -0
  51. package/dist/shared/errors.js +33 -0
  52. package/dist/shared/index.d.ts +2 -0
  53. package/dist/shared/index.js +2 -0
  54. package/dist/shared/types.d.ts +19 -0
  55. package/dist/shared/types.js +11 -0
  56. package/dist/tools/fs/read-file.d.ts +1 -31
  57. package/dist/tools/fs/read-file.js +45 -17
  58. package/dist/tools/fs/read-history.d.ts +3 -0
  59. package/dist/tools/fs/read-history.js +13 -0
  60. package/dist/tools/fs/write-file.d.ts +1 -4
  61. package/dist/tools/fs/write-file.js +35 -1
  62. package/dist/tools/normalize.d.ts +1 -1
  63. package/dist/tools/shell/shell-exec.d.ts +1 -26
  64. package/dist/tools/shell/shell-exec.js +121 -17
  65. package/dist/tools/types.d.ts +25 -0
  66. package/dist/trace/writer.d.ts +13 -0
  67. package/dist/trace/writer.js +53 -0
  68. package/dist/tui/app.d.ts +1 -15
  69. package/dist/tui/app.js +203 -22
  70. package/dist/tui/approval.js +25 -4
  71. package/dist/tui/approval.test.js +1 -1
  72. package/dist/tui/snapshot.test.js +2 -2
  73. package/dist/tui/status.js +5 -1
  74. package/dist/util/log.d.ts +12 -0
  75. package/dist/util/log.js +75 -0
  76. package/dist/verification/auto.d.ts +8 -0
  77. package/dist/verification/auto.js +52 -0
  78. package/package.json +3 -2
@@ -56,13 +56,34 @@ export function ApprovalModal({ bridge }) {
56
56
  useEffect(() => {
57
57
  return bridge.subscribe((p) => setPending(p?.req ?? null));
58
58
  }, [bridge]);
59
+ const [explain, setExplain] = useState(false);
59
60
  useInput((inputStr, key) => {
60
61
  if (!pending)
61
62
  return;
62
63
  const c = inputStr.toLowerCase();
63
- if (c === 'y' || c === 'a' || c === 'd' || c === 'n') {
64
- const choice = (c === 'y' || c === 'a') ? (c === 'a' ? 'always' : 'allow') : 'deny';
65
- bridge.resolve(choice);
64
+ if (c === 'y') {
65
+ bridge.resolve('allow');
66
+ return;
67
+ }
68
+ if (c === 'a') {
69
+ bridge.resolve('always');
70
+ return;
71
+ }
72
+ if (c === 'A') {
73
+ bridge.resolve('always');
74
+ return;
75
+ }
76
+ if (c === 'd' || c === 'n') {
77
+ bridge.resolve('deny');
78
+ return;
79
+ }
80
+ if (c === 'e') {
81
+ // Edit: for MVP, treat as deny with edit hint — model will be told to re-read and retry
82
+ bridge.resolve('deny');
83
+ return;
84
+ }
85
+ if (c === '?') {
86
+ setExplain((v) => !v);
66
87
  return;
67
88
  }
68
89
  if (key.return) {
@@ -72,5 +93,5 @@ export function ApprovalModal({ bridge }) {
72
93
  });
73
94
  if (!pending)
74
95
  return null;
75
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 1, marginY: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u26A0 approval needed \u2014 ", pending.toolName] }), _jsxs(Text, { color: "gray", children: [" reason: ", pending.reason] }), pending.summary ? _jsxs(Text, { children: [" ", pending.summary] }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "[y] allow" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[a] always allow" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "red", children: "[d] deny" }), _jsx(Text, { color: "gray", children: " (Enter = deny)" })] })] }));
96
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 1, marginY: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u26A0 approval needed \u2014 ", pending.toolName] }), _jsxs(Text, { color: "gray", children: [" reason: ", pending.reason] }), pending.summary ? _jsxs(Text, { children: [" ", pending.summary] }) : null, explain ? _jsxs(Text, { color: "cyan", children: [" Explain: This tool will ", pending.toolName, " with the shown args. [y] once, [a] session, [A] always\u2192settings, [n] deny, [e] edit, [?] toggle help."] }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "[y] once" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[a] session" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[A] always" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "red", children: "[n] deny" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "yellow", children: "[e] edit" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[?] explain" }), _jsx(Text, { color: "gray", children: " (Enter = deny)" })] })] }));
76
97
  }
@@ -47,7 +47,7 @@ describe('ApprovalModal', () => {
47
47
  expect(out).toContain('shell_exec');
48
48
  expect(out).toContain('destructive');
49
49
  expect(out).toContain('rm -rf /');
50
- expect(out).toContain('allow');
50
+ expect(out).toContain('once');
51
51
  expect(out).toContain('deny');
52
52
  bridge.resolve('deny');
53
53
  await promise;
@@ -22,8 +22,8 @@ describe('App visual snapshot', () => {
22
22
  expect(frame).toContain('gpt-4o-mini');
23
23
  // Status line should show
24
24
  expect(frame).toMatch(/idle/i);
25
- // Input prompt should be visible
26
- expect(frame).toContain('>');
25
+ // Input prompt should be visible (klyro › per 1.4)
26
+ expect(frame).toMatch(/klyro|›/);
27
27
  });
28
28
  it('renders a transcript with assistant text', () => {
29
29
  const { lastFrame } = render(_jsx(App, { ...DEFAULT_PROPS, initialModel: "gpt-4o-mini", maxSteps: 30, initialStatus: { status: 'idle', model: 'gpt-4o-mini', step: 0, maxSteps: 30, usageInput: 0, usageOutput: 0, repairs: 0 }, initialTranscript: [
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text, Box } from 'ink';
3
+ import { estimateCost } from '../providers/model-info.js';
3
4
  export function StatusLine({ snapshot }) {
4
5
  const { model, step, maxSteps, usageInput, usageOutput, repairs, status } = snapshot;
5
6
  const statusColor = status === 'running' ? 'cyan' :
@@ -9,5 +10,8 @@ export function StatusLine({ snapshot }) {
9
10
  'gray';
10
11
  const usageKbIn = (usageInput / 1024).toFixed(1);
11
12
  const usageKbOut = (usageOutput / 1024).toFixed(1);
12
- return (_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "gray", children: "model " }), _jsx(Text, { bold: true, children: model }), _jsx(Text, { color: "gray", children: " step " }), _jsx(Text, { bold: true, children: step }), _jsxs(Text, { color: "gray", children: ["/", maxSteps] }), _jsx(Text, { color: "gray", children: " repairs " }), _jsx(Text, { bold: true, children: repairs })] }), _jsxs(Text, { children: [_jsx(Text, { color: "gray", children: "tokens " }), _jsxs(Text, { color: "cyan", children: [usageKbIn, "K"] }), _jsx(Text, { color: "gray", children: " in / " }), _jsxs(Text, { color: "cyan", children: [usageKbOut, "K"] }), _jsx(Text, { color: "gray", children: " out" })] }), _jsx(Text, { children: _jsxs(Text, { color: statusColor, children: ["\u25CF ", status] }) })] }));
13
+ const cost = estimateCost(model, usageInput, usageOutput);
14
+ const costStr = cost > 0 ? `$${cost.toFixed(3)}` : `$0.000`;
15
+ const ctxPct = Math.min(99, Math.round(((usageInput + usageOutput) / 128000) * 100));
16
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "gray", children: "model " }), _jsx(Text, { bold: true, children: model }), _jsx(Text, { color: "gray", children: " step " }), _jsx(Text, { bold: true, children: step }), _jsxs(Text, { color: "gray", children: ["/", maxSteps] }), _jsx(Text, { color: "gray", children: " repairs " }), _jsx(Text, { bold: true, children: repairs })] }), _jsxs(Text, { children: [_jsx(Text, { color: "gray", children: "tokens " }), _jsxs(Text, { color: "cyan", children: [usageKbIn, "K"] }), _jsx(Text, { color: "gray", children: " in / " }), _jsxs(Text, { color: "cyan", children: [usageKbOut, "K"] }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: "green", children: costStr }), _jsxs(Text, { color: "gray", children: [" \u00B7 ", ctxPct, "% ctx"] })] }), _jsx(Text, { children: _jsxs(Text, { color: statusColor, children: ["\u25CF ", status] }) })] }));
13
17
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 1.5 — Logging (pino JSON to ~/.klyro/logs/klyro-YYYY-MM-DD.log, 14-day rotation, secret redaction)
3
+ * Minimal implementation: JSONL append, redaction via secret-redactor, --debug pretty tee.
4
+ */
5
+ export interface LogEntry {
6
+ ts: string;
7
+ level: 'debug' | 'info' | 'warn' | 'error';
8
+ msg: string;
9
+ data?: unknown;
10
+ }
11
+ export declare function log(entry: LogEntry): Promise<void>;
12
+ export declare function logSync(entry: LogEntry): void;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 1.5 — Logging (pino JSON to ~/.klyro/logs/klyro-YYYY-MM-DD.log, 14-day rotation, secret redaction)
3
+ * Minimal implementation: JSONL append, redaction via secret-redactor, --debug pretty tee.
4
+ */
5
+ import * as fs from 'node:fs/promises';
6
+ import * as fsSync from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import { redact } from '../policy/secret-redactor.js';
10
+ function getLogDir() {
11
+ if (process.env.KLYRO_LOG_DIR)
12
+ return process.env.KLYRO_LOG_DIR;
13
+ const home = os.homedir() || process.cwd();
14
+ return path.join(home, '.klyro', 'logs');
15
+ }
16
+ function getLogFile() {
17
+ const dir = getLogDir();
18
+ const date = new Date().toISOString().slice(0, 10);
19
+ return path.join(dir, `klyro-${date}.log`);
20
+ }
21
+ async function rotateLogs() {
22
+ const dir = getLogDir();
23
+ try {
24
+ const files = await fs.readdir(dir);
25
+ const cutoff = Date.now() - 14 * 24 * 60 * 60 * 1000;
26
+ for (const f of files) {
27
+ if (!f.startsWith('klyro-') || !f.endsWith('.log'))
28
+ continue;
29
+ const fp = path.join(dir, f);
30
+ try {
31
+ const st = await fs.stat(fp);
32
+ if (st.mtimeMs < cutoff)
33
+ await fs.unlink(fp);
34
+ }
35
+ catch { /* ignore */ }
36
+ }
37
+ }
38
+ catch { /* ignore dir not exists */ }
39
+ }
40
+ let _rotateDone = false;
41
+ export async function log(entry) {
42
+ if (!_rotateDone) {
43
+ _rotateDone = true;
44
+ rotateLogs().catch(() => undefined);
45
+ }
46
+ const redactedMsg = redact(entry.msg);
47
+ const redactedData = entry.data ? JSON.parse(redact(JSON.stringify(entry.data))) : undefined;
48
+ const line = JSON.stringify({ ...entry, msg: redactedMsg, data: redactedData }) + '\n';
49
+ const file = getLogFile();
50
+ try {
51
+ await fs.mkdir(path.dirname(file), { recursive: true });
52
+ await fs.appendFile(file, line, 'utf-8');
53
+ }
54
+ catch { /* ignore */ }
55
+ if (process.env.KLYRO_LOG_LEVEL === 'debug' || process.env.DEBUG) {
56
+ const pretty = `[${entry.level}] ${entry.msg}${entry.data ? ' ' + JSON.stringify(entry.data) : ''}`;
57
+ if (entry.level === 'error')
58
+ process.stderr.write(pretty + '\n');
59
+ else
60
+ process.stdout.write(pretty + '\n');
61
+ }
62
+ }
63
+ export function logSync(entry) {
64
+ const redactedMsg = redact(entry.msg);
65
+ const line = JSON.stringify({ ...entry, msg: redactedMsg, ts: entry.ts }) + '\n';
66
+ const file = getLogFile();
67
+ try {
68
+ fsSync.mkdirSync(path.dirname(file), { recursive: true });
69
+ fsSync.appendFileSync(file, line, 'utf-8');
70
+ }
71
+ catch { /* ignore */ }
72
+ if (process.env.KLYRO_LOG_LEVEL === 'debug') {
73
+ process.stderr.write(`[${entry.level}] ${redactedMsg}\n`);
74
+ }
75
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Level 8 — auto-detect verification command from project.
3
+ * Picks the most relevant check for the repo so the harness
4
+ * can verify without explicit user config.
5
+ */
6
+ export declare function detectVerifyCommand(cwd: string): string | null;
7
+ /** Human-readable reason why a command was chosen. */
8
+ export declare function describeVerifyCommand(cmd: string | null): string;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Level 8 — auto-detect verification command from project.
3
+ * Picks the most relevant check for the repo so the harness
4
+ * can verify without explicit user config.
5
+ */
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ export function detectVerifyCommand(cwd) {
9
+ const pkgPath = path.join(cwd, 'package.json');
10
+ if (fs.existsSync(pkgPath)) {
11
+ try {
12
+ const raw = fs.readFileSync(pkgPath, 'utf-8');
13
+ const json = JSON.parse(raw);
14
+ // Prefer test if available — it's the strongest signal
15
+ if (json.scripts?.test) {
16
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
17
+ return 'pnpm test';
18
+ if (fs.existsSync(path.join(cwd, 'yarn.lock')))
19
+ return 'yarn test';
20
+ if (fs.existsSync(path.join(cwd, 'bun.lockb')))
21
+ return 'bun test';
22
+ return 'npm test';
23
+ }
24
+ if (json.scripts?.build)
25
+ return 'npm run build';
26
+ }
27
+ catch {
28
+ // ignore parse errors
29
+ }
30
+ if (fs.existsSync(path.join(cwd, 'tsconfig.json')))
31
+ return 'npx tsc --noEmit';
32
+ }
33
+ if (fs.existsSync(path.join(cwd, 'tsconfig.json')))
34
+ return 'npx tsc --noEmit';
35
+ if (fs.existsSync(path.join(cwd, 'Makefile')) || fs.existsSync(path.join(cwd, 'makefile')))
36
+ return 'make test';
37
+ if (fs.existsSync(path.join(cwd, 'pyproject.toml')) || fs.existsSync(path.join(cwd, 'requirements.txt')))
38
+ return 'pytest';
39
+ if (fs.existsSync(path.join(cwd, 'go.mod')))
40
+ return 'go test ./...';
41
+ return null;
42
+ }
43
+ /** Human-readable reason why a command was chosen. */
44
+ export function describeVerifyCommand(cmd) {
45
+ if (!cmd)
46
+ return 'no verification command auto-detected';
47
+ if (cmd.includes('test'))
48
+ return `auto-detected: ${cmd}`;
49
+ if (cmd.includes('tsc'))
50
+ return `auto-detected: ${cmd} (typecheck)`;
51
+ return `auto-detected: ${cmd}`;
52
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {
9
- "klyro": "dist/index.js"
9
+ "klyro": "dist/index.js",
10
+ "ky": "dist/index.js"
10
11
  },
11
12
  "files": [
12
13
  "dist",