klyro 0.1.61 → 0.1.63
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/READ.md +68 -0
- package/dist/agent/anthropic-adapter.d.ts +30 -9
- package/dist/agent/anthropic-adapter.js +101 -48
- package/dist/agent/provider-adapter.js +61 -38
- package/dist/agent/runtime.d.ts +3 -1
- package/dist/agent/runtime.js +227 -25
- package/dist/cli/config.d.ts +21 -0
- package/dist/cli/config.js +31 -0
- package/dist/cli/repl.js +34 -13
- package/dist/cli/run.js +9 -0
- package/dist/index.js +1 -43
- package/dist/persistence/store.d.ts +1 -1
- package/dist/policy/approval.d.ts +27 -6
- package/dist/policy/approval.js +36 -8
- package/dist/policy/engine.d.ts +13 -0
- package/dist/policy/engine.js +28 -0
- package/dist/policy/patterns.d.ts +16 -0
- package/dist/policy/patterns.js +26 -0
- package/dist/tui/app.js +3 -1
- package/dist/tui/app.test.js +10 -0
- package/dist/tui/approval.d.ts +1 -1
- package/dist/tui/approval.js +2 -2
- package/dist/version.d.ts +8 -0
- package/dist/version.js +47 -0
- package/package.json +2 -2
package/dist/policy/approval.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as readline from 'node:readline/promises';
|
|
9
9
|
import { stdin as input, stdout as output } from 'node:process';
|
|
10
|
+
import { patternForCall } from './patterns.js';
|
|
10
11
|
/** Default prompt backed by readline on stdin/stdout. */
|
|
11
12
|
export class StdinApprovalPrompt {
|
|
12
13
|
async ask(req) {
|
|
@@ -34,21 +35,48 @@ export class DenyAllApprovalPrompt {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* Pattern approval cache — ask-once-per-pattern (Claude-Code-like).
|
|
39
|
+
*
|
|
40
|
+
* Wraps any inner prompt (TUI modal, stdin). The first `ask` for a pattern
|
|
41
|
+
* delegates to the user; `always` records the pattern for the session,
|
|
42
|
+
* `always-persist` additionally calls `onPersist` (the CLI wires this to
|
|
43
|
+
* append the pattern to ~/.klyro/settings.json and the live engine).
|
|
44
|
+
* Re-prompts never happen for a recorded pattern within the session.
|
|
45
|
+
*
|
|
46
|
+
* The pattern comes from `req.pattern` (pre-derived by the runtime via
|
|
47
|
+
* `patternForCall`) or is derived from `req.input` here as a fallback.
|
|
39
48
|
*/
|
|
40
|
-
export class
|
|
49
|
+
export class PatternApprovalCache {
|
|
41
50
|
inner;
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
opts;
|
|
52
|
+
session = new Set();
|
|
53
|
+
constructor(inner = new DenyAllApprovalPrompt(), opts = {}) {
|
|
44
54
|
this.inner = inner;
|
|
55
|
+
this.opts = opts;
|
|
45
56
|
}
|
|
46
57
|
async ask(req) {
|
|
47
|
-
|
|
58
|
+
const key = req.pattern ?? patternFromRequest(req);
|
|
59
|
+
if (key && this.session.has(key))
|
|
48
60
|
return 'allow';
|
|
49
61
|
const choice = await this.inner.ask(req);
|
|
50
|
-
if (choice === 'always')
|
|
51
|
-
this.
|
|
62
|
+
if ((choice === 'always' || choice === 'always-persist') && key) {
|
|
63
|
+
this.session.add(key);
|
|
64
|
+
}
|
|
65
|
+
if (choice === 'always-persist' && key) {
|
|
66
|
+
// Best-effort: the current call is already approved via the session
|
|
67
|
+
// set above — a persist failure must not retro-deny it.
|
|
68
|
+
try {
|
|
69
|
+
await this.opts.onPersist?.(key);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* ignore */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
52
75
|
return choice;
|
|
53
76
|
}
|
|
54
77
|
}
|
|
78
|
+
function patternFromRequest(req) {
|
|
79
|
+
if (!req.input)
|
|
80
|
+
return undefined;
|
|
81
|
+
return patternForCall(req.toolName, req.input);
|
|
82
|
+
}
|
package/dist/policy/engine.d.ts
CHANGED
|
@@ -67,6 +67,19 @@ export declare class PolicyEngine {
|
|
|
67
67
|
nonInteractive: boolean;
|
|
68
68
|
}): Promise<Decision>;
|
|
69
69
|
private evaluateGlobRules;
|
|
70
|
+
/**
|
|
71
|
+
* Session rule management — appends a glob rule (`tool(glob)` grammar)
|
|
72
|
+
* if not already present. Used for persisted "always allow" patterns
|
|
73
|
+
* (loaded at startup) and live additions (approval `always→settings`).
|
|
74
|
+
*/
|
|
75
|
+
addAllow(rule: string): void;
|
|
76
|
+
addDeny(rule: string): void;
|
|
77
|
+
addAsk(rule: string): void;
|
|
78
|
+
applyRules(rules: {
|
|
79
|
+
allow?: string[];
|
|
80
|
+
deny?: string[];
|
|
81
|
+
ask?: string[];
|
|
82
|
+
}): void;
|
|
70
83
|
}
|
|
71
84
|
/** Builtin set of rules. Order matters: first match wins. */
|
|
72
85
|
export declare function builtinRules(): PolicyRule[];
|
package/dist/policy/engine.js
CHANGED
|
@@ -116,6 +116,34 @@ export class PolicyEngine {
|
|
|
116
116
|
// Precedence: deny → allow → ask
|
|
117
117
|
return check(this.config.deny, 'deny') ?? check(this.config.allow, 'allow') ?? check(this.config.ask, 'ask') ?? null;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Session rule management — appends a glob rule (`tool(glob)` grammar)
|
|
121
|
+
* if not already present. Used for persisted "always allow" patterns
|
|
122
|
+
* (loaded at startup) and live additions (approval `always→settings`).
|
|
123
|
+
*/
|
|
124
|
+
addAllow(rule) {
|
|
125
|
+
const list = (this.config.allow ??= []);
|
|
126
|
+
if (!list.includes(rule))
|
|
127
|
+
list.push(rule);
|
|
128
|
+
}
|
|
129
|
+
addDeny(rule) {
|
|
130
|
+
const list = (this.config.deny ??= []);
|
|
131
|
+
if (!list.includes(rule))
|
|
132
|
+
list.push(rule);
|
|
133
|
+
}
|
|
134
|
+
addAsk(rule) {
|
|
135
|
+
const list = (this.config.ask ??= []);
|
|
136
|
+
if (!list.includes(rule))
|
|
137
|
+
list.push(rule);
|
|
138
|
+
}
|
|
139
|
+
applyRules(rules) {
|
|
140
|
+
for (const r of rules.allow ?? [])
|
|
141
|
+
this.addAllow(r);
|
|
142
|
+
for (const r of rules.deny ?? [])
|
|
143
|
+
this.addDeny(r);
|
|
144
|
+
for (const r of rules.ask ?? [])
|
|
145
|
+
this.addAsk(r);
|
|
146
|
+
}
|
|
119
147
|
}
|
|
120
148
|
/** Builtin set of rules. Order matters: first match wins. */
|
|
121
149
|
export function builtinRules() {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval patterns — the unit of "always allow this pattern".
|
|
3
|
+
*
|
|
4
|
+
* A pattern uses the same `tool(glob)` grammar the policy engine matches
|
|
5
|
+
* (`matchesGlobRule` in engine.ts), e.g. `shell_exec(npm *)`,
|
|
6
|
+
* `write_file(src/index.ts)`. Derivation is deterministic: the same call
|
|
7
|
+
* shape always yields the same pattern, so session caching can key on exact
|
|
8
|
+
* pattern equality and persisted patterns re-match across sessions.
|
|
9
|
+
*
|
|
10
|
+
* Scoping (Claude-Code-like):
|
|
11
|
+
* - shell_exec / run_verify → first command word + ` *`
|
|
12
|
+
* (`npm test` and `npm run build` share `shell_exec(npm *)`)
|
|
13
|
+
* - file tools with a path → the exact path (safest: no wildcards)
|
|
14
|
+
* - everything else → the bare tool name (whole-tool session allow)
|
|
15
|
+
*/
|
|
16
|
+
export declare function patternForCall(name: string, input: Record<string, unknown>): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval patterns — the unit of "always allow this pattern".
|
|
3
|
+
*
|
|
4
|
+
* A pattern uses the same `tool(glob)` grammar the policy engine matches
|
|
5
|
+
* (`matchesGlobRule` in engine.ts), e.g. `shell_exec(npm *)`,
|
|
6
|
+
* `write_file(src/index.ts)`. Derivation is deterministic: the same call
|
|
7
|
+
* shape always yields the same pattern, so session caching can key on exact
|
|
8
|
+
* pattern equality and persisted patterns re-match across sessions.
|
|
9
|
+
*
|
|
10
|
+
* Scoping (Claude-Code-like):
|
|
11
|
+
* - shell_exec / run_verify → first command word + ` *`
|
|
12
|
+
* (`npm test` and `npm run build` share `shell_exec(npm *)`)
|
|
13
|
+
* - file tools with a path → the exact path (safest: no wildcards)
|
|
14
|
+
* - everything else → the bare tool name (whole-tool session allow)
|
|
15
|
+
*/
|
|
16
|
+
export function patternForCall(name, input) {
|
|
17
|
+
if ((name === 'shell_exec' || name === 'run_verify') && typeof input.command === 'string') {
|
|
18
|
+
const first = input.command.trim().split(/\s+/, 1)[0] ?? '';
|
|
19
|
+
if (first)
|
|
20
|
+
return `${name}(${first} *)`;
|
|
21
|
+
}
|
|
22
|
+
if (typeof input.path === 'string' && input.path.length > 0) {
|
|
23
|
+
return `${name}(${input.path})`;
|
|
24
|
+
}
|
|
25
|
+
return name;
|
|
26
|
+
}
|
package/dist/tui/app.js
CHANGED
|
@@ -13,6 +13,7 @@ import { tokens, g } from './tokens.js';
|
|
|
13
13
|
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, } from './scroll-model.js';
|
|
14
14
|
import { buildIndex, itemAtRow, MeasureCache } from './measure.js';
|
|
15
15
|
import { renderMarkdownLines } from './markdown.js';
|
|
16
|
+
import { readVersion } from '../version.js';
|
|
16
17
|
import { getTranscriptCommand, } from './transcript-commands.js';
|
|
17
18
|
import { estimateCost, getModelInfo } from '../providers/model-info.js';
|
|
18
19
|
let _id = 0;
|
|
@@ -674,7 +675,8 @@ export function App(props) {
|
|
|
674
675
|
setHistIdx(null);
|
|
675
676
|
}
|
|
676
677
|
});
|
|
677
|
-
|
|
678
|
+
// Single source of truth: package.json via version.ts — never hardcoded.
|
|
679
|
+
const ver = props.version ?? readVersion();
|
|
678
680
|
const rule = g('rule').repeat(Math.max(10, width - 2));
|
|
679
681
|
// Single cost/context source: model-aware registry rates + window.
|
|
680
682
|
const cost = estimateCost(status.model, status.usageInput, status.usageOutput);
|
package/dist/tui/app.test.js
CHANGED
|
@@ -565,6 +565,16 @@ describe('App', () => {
|
|
|
565
565
|
const top = await waitForMatch(lastFrame, /⇅ 0\/\d+/);
|
|
566
566
|
expect(top).toMatch(/MSG-00-tag/);
|
|
567
567
|
});
|
|
568
|
+
it('header shows the passed version, defaulting to package.json (never stale)', async () => {
|
|
569
|
+
const { readVersion } = await import('../version.js');
|
|
570
|
+
const { lastFrame: f1 } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, version: "9.9.9-test" }));
|
|
571
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
572
|
+
expect(f1() ?? '').toContain('9.9.9-test');
|
|
573
|
+
const { lastFrame: f2 } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
|
|
574
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
575
|
+
expect(f2() ?? '').toContain(readVersion());
|
|
576
|
+
expect(f2() ?? '').not.toContain('0.1.27');
|
|
577
|
+
});
|
|
568
578
|
it('Shift+Up / Shift+Down scroll by one line', async () => {
|
|
569
579
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
570
580
|
await new Promise((r) => setTimeout(r, 50));
|
package/dist/tui/approval.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* a Promise.
|
|
14
14
|
* 3. The App polls getPending() on every render to show a modal.
|
|
15
15
|
* 4. The App's useInput handler sees a pending prompt and routes
|
|
16
|
-
* y/n
|
|
16
|
+
* y/a/A/n to the resolver (A = always-persist to settings).
|
|
17
17
|
*
|
|
18
18
|
* Non-TTY mode (no Ink mounted) keeps using StdinApprovalPrompt; this
|
|
19
19
|
* module is only used when the TUI is active.
|
package/dist/tui/approval.js
CHANGED
|
@@ -14,7 +14,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
14
14
|
* a Promise.
|
|
15
15
|
* 3. The App polls getPending() on every render to show a modal.
|
|
16
16
|
* 4. The App's useInput handler sees a pending prompt and routes
|
|
17
|
-
* y/n
|
|
17
|
+
* y/a/A/n to the resolver (A = always-persist to settings).
|
|
18
18
|
*
|
|
19
19
|
* Non-TTY mode (no Ink mounted) keeps using StdinApprovalPrompt; this
|
|
20
20
|
* module is only used when the TUI is active.
|
|
@@ -85,7 +85,7 @@ export function ApprovalModal({ bridge }) {
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
if (c === 'A') {
|
|
88
|
-
bridge.resolve('always');
|
|
88
|
+
bridge.resolve('always-persist');
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
if (c === 'd' || c === 'n') {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the Klyro version (read from package.json).
|
|
3
|
+
*
|
|
4
|
+
* Every surface — `klyro --version`, the TUI header, `/version` — reads
|
|
5
|
+
* through here, so a version bump in package.json shows up everywhere with
|
|
6
|
+
* no hardcoded fallbacks rotting behind.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readVersion(): string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the Klyro version (read from package.json).
|
|
3
|
+
*
|
|
4
|
+
* Every surface — `klyro --version`, the TUI header, `/version` — reads
|
|
5
|
+
* through here, so a version bump in package.json shows up everywhere with
|
|
6
|
+
* no hardcoded fallbacks rotting behind.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
export function readVersion() {
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const candidates = [
|
|
14
|
+
resolve(here, '..', 'package.json'),
|
|
15
|
+
resolve(here, '../..', 'package.json'),
|
|
16
|
+
resolve(here, '../../package.json'),
|
|
17
|
+
];
|
|
18
|
+
for (const pkgPath of candidates) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(pkgPath, 'utf8');
|
|
21
|
+
const pkg = JSON.parse(raw);
|
|
22
|
+
if (typeof pkg.version === 'string' && pkg.version.length > 0)
|
|
23
|
+
return pkg.version;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const e = err;
|
|
27
|
+
// Only ignore missing file; surface JSON parse errors
|
|
28
|
+
if (e.code === 'ENOENT')
|
|
29
|
+
continue;
|
|
30
|
+
// Malformed JSON — warn but don't crash version output
|
|
31
|
+
process.stderr.write(`klyro: warning: malformed ${pkgPath}: ${e.message}\n`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Fallback: try require-style resolution
|
|
36
|
+
try {
|
|
37
|
+
const pkgPath = resolve(process.cwd(), 'package.json');
|
|
38
|
+
const raw = readFileSync(pkgPath, 'utf8');
|
|
39
|
+
const pkg = JSON.parse(raw);
|
|
40
|
+
if (pkg.name === 'klyro' && typeof pkg.version === 'string')
|
|
41
|
+
return pkg.version;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
return '0.0.0';
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klyro",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Klyro
|
|
3
|
+
"version": "0.1.63",
|
|
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",
|