klyro 1.0.4 → 1.0.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.
- package/README.md +29 -0
- package/dist/agent/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +22 -2
- package/dist/agent/orchestrator.js +30 -4
- package/dist/agent/runtime.d.ts +5 -0
- package/dist/agent/runtime.js +174 -51
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +20 -0
- package/dist/cli/auth.js +11 -3
- package/dist/cli/completion.js +63 -10
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +34 -2
- package/dist/cli/hooks.d.ts +54 -5
- package/dist/cli/hooks.js +85 -6
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/repl.js +261 -34
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +97 -41
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +16 -2
- package/dist/cli/slash/parser.js +67 -18
- package/dist/cli/update.d.ts +8 -4
- package/dist/cli/update.js +50 -7
- package/dist/context/accounting.d.ts +8 -0
- package/dist/context/accounting.js +18 -1
- package/dist/context/compaction.d.ts +1 -0
- package/dist/context/compaction.js +2 -1
- package/dist/context/memory.js +18 -1
- package/dist/eval/harness.d.ts +21 -3
- package/dist/eval/harness.js +31 -3
- package/dist/eval/judge.d.ts +32 -0
- package/dist/eval/judge.js +63 -0
- package/dist/eval/tasks.js +134 -0
- package/dist/index.js +225 -132
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +10 -1
- package/dist/mcp/config.js +64 -1
- package/dist/mcp/registry.d.ts +13 -0
- package/dist/mcp/registry.js +47 -5
- package/dist/mcp/remote.d.ts +29 -0
- package/dist/mcp/remote.js +153 -0
- package/dist/mcp/serve.d.ts +23 -0
- package/dist/mcp/serve.js +111 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +14 -1
- package/dist/policy/secret-redactor.js +4 -1
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +13 -10
- package/dist/shared/error-map.d.ts +19 -0
- package/dist/shared/error-map.js +58 -0
- package/dist/tools/lsp/diagnostics.d.ts +35 -4
- package/dist/tools/lsp/diagnostics.js +88 -9
- package/dist/tools/normalize.d.ts +3 -0
- package/dist/tools/normalize.js +8 -5
- package/dist/tools/search/dependencies.d.ts +2 -2
- package/dist/tools/shell/background.d.ts +6 -0
- package/dist/tools/shell/background.js +17 -0
- package/dist/tools/symbols/find-symbol.d.ts +1 -1
- package/dist/tools/symbols/find-symbol.js +8 -6
- package/dist/tools/types.d.ts +8 -1
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +454 -53
- package/dist/tui/app.test.js +66 -3
- package/dist/tui/approval.js +53 -1
- package/dist/tui/markdown.js +9 -0
- package/dist/tui/mouse.d.ts +26 -1
- package/dist/tui/mouse.js +104 -6
- package/dist/tui/scroll-flow.test.js +3 -1
- package/dist/tui/tokens.d.ts +6 -6
- package/dist/tui/tokens.js +9 -6
- package/package.json +1 -1
|
@@ -154,6 +154,26 @@ export async function listCheckpoints(cwd) {
|
|
|
154
154
|
return [];
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
/** Numbered snapshot list for `/checkpoints` and the `/rewind` menu. */
|
|
158
|
+
export async function listCheckpointInfo(cwd) {
|
|
159
|
+
const ids = await listCheckpoints(cwd);
|
|
160
|
+
const out = [];
|
|
161
|
+
for (let i = ids.length - 1; i >= 0; i--) {
|
|
162
|
+
const id = ids[i];
|
|
163
|
+
let ts = 0;
|
|
164
|
+
let files = 0;
|
|
165
|
+
try {
|
|
166
|
+
const meta = JSON.parse(await fs.readFile(path.join(ckptDir(cwd), id, '.meta.json'), 'utf-8'));
|
|
167
|
+
if (typeof meta.ts === 'number')
|
|
168
|
+
ts = meta.ts;
|
|
169
|
+
if (Array.isArray(meta.files))
|
|
170
|
+
files = meta.files.length;
|
|
171
|
+
}
|
|
172
|
+
catch { /* best-effort */ }
|
|
173
|
+
out.push({ index: ids.length - i, id, ts, files });
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
157
177
|
export async function diff(cwd, id) {
|
|
158
178
|
const ckpts = await listCheckpoints(cwd);
|
|
159
179
|
const target = id ?? ckpts[ckpts.length - 1];
|
package/dist/cli/auth.js
CHANGED
|
@@ -132,13 +132,21 @@ export async function runLogout(provider) {
|
|
|
132
132
|
}
|
|
133
133
|
export function getStoredKey(provider) {
|
|
134
134
|
try {
|
|
135
|
-
//
|
|
136
|
-
//
|
|
135
|
+
// Refuse (don't just warn) when the credentials file is
|
|
136
|
+
// group/other-readable: a key other users can read is compromised by
|
|
137
|
+
// definition. Repair hint included. KLYRO_CREDENTIALS_INSECURE_OK=1
|
|
138
|
+
// preserves the old warn-and-continue behavior for exotic setups.
|
|
137
139
|
if (process.platform !== 'win32') {
|
|
138
140
|
try {
|
|
139
141
|
const st = fsSync.statSync(credPath());
|
|
140
142
|
if ((st.mode & 0o077) !== 0) {
|
|
141
|
-
process.
|
|
143
|
+
if (process.env.KLYRO_CREDENTIALS_INSECURE_OK === '1') {
|
|
144
|
+
process.stderr.write(`warning: credentials file ${credPath()} is group/other-readable (mode ${(st.mode & 0o777).toString(8)}) — run chmod 600 on it\n`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
process.stderr.write(`klyro: refusing to use group/other-readable credentials file ${credPath()} (mode ${(st.mode & 0o777).toString(8)}) — run: chmod 600 ${credPath()} (or set KLYRO_CREDENTIALS_INSECURE_OK=1 to override)\n`);
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
catch { /* missing file → no warning */ }
|
package/dist/cli/completion.js
CHANGED
|
@@ -2,40 +2,93 @@
|
|
|
2
2
|
* 1.2 — klyro completion
|
|
3
3
|
* Generates shell completion scripts for bash/zsh/fish/powershell.
|
|
4
4
|
*/
|
|
5
|
-
const COMMANDS = ['tui', 'run', 'chat', 'config', 'doctor', 'completion', 'update', 'eval', 'session', 'resume', 'help', 'version'];
|
|
5
|
+
const COMMANDS = ['tui', 'run', 'chat', 'config', 'doctor', 'init', 'completion', 'update', 'eval', 'session', 'resume', 'help', 'version', 'scan', 'project', 'mcp', 'hooks', 'agents', 'commit', 'audit', 'benchmark', 'sessions', 'login', 'logout'];
|
|
6
|
+
/** Second-level completion: global flags + per-command flags. */
|
|
7
|
+
const GLOBAL_FLAGS = ['--cwd', '--config', '--debug', '--verbose', '--quiet', '--json', '--yes', '--no-color', '--print', '--output-format', '--no-stream', '--show-thinking', '--tui', '--chat', '--continue', '--resume', '--help', '--version'];
|
|
8
|
+
const COMMAND_FLAGS = {
|
|
9
|
+
run: ['-m', '--model', '--max-steps', '--max-tokens', '--temperature', '--timeout', '--base-url', '--api-key', '--output', '--provider', '--dry-run', '--resume', '--resume-session', '--verify', '--verify-command', '--verify-mode', '--max-repairs', '--persist', '--require-verify', '--agent', '--max-depth', '--bare'],
|
|
10
|
+
chat: ['-s', '--system', '-m', '--model', '-t', '--timeout'],
|
|
11
|
+
eval: ['--output', '--suite', '--filter', '--runs', '--parallel', '--model'],
|
|
12
|
+
tui: ['-m', '--model', '--max-steps'],
|
|
13
|
+
doctor: ['--json'],
|
|
14
|
+
session: ['list', 'show', 'resume', 'fork', 'delete'],
|
|
15
|
+
sessions: ['export', 'import', 'fork', 'delete'],
|
|
16
|
+
mcp: ['list', 'add', 'remove', 'probe', 'serve'],
|
|
17
|
+
config: ['list', 'get', 'set', 'unset', 'path'],
|
|
18
|
+
commit: ['--dry-run', '--message', '--force-secret'],
|
|
19
|
+
completion: ['bash', 'zsh', 'fish', 'powershell'],
|
|
20
|
+
resume: ['-m', '--model', '--max-steps'],
|
|
21
|
+
};
|
|
22
|
+
function flagsFor(cmd) {
|
|
23
|
+
return [...GLOBAL_FLAGS, ...(COMMAND_FLAGS[cmd] ?? [])];
|
|
24
|
+
}
|
|
6
25
|
function bashScript() {
|
|
7
|
-
|
|
26
|
+
const cmdCases = Object.entries(COMMAND_FLAGS)
|
|
27
|
+
.map(([c, fs]) => ` ${c}) opts="${fs.join(' ')}" ;;`)
|
|
28
|
+
.join('\n');
|
|
29
|
+
return `# klyro bash completion (commands + flags)
|
|
8
30
|
_klyro_complete() {
|
|
9
31
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
32
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
10
33
|
local cmds="${COMMANDS.join(' ')}"
|
|
11
|
-
|
|
34
|
+
if [[ $COMP_CWORD -eq 1 ]]; then
|
|
35
|
+
COMPREPLY=( $(compgen -W "$cmds ${GLOBAL_FLAGS.join(' ')}" -- "$cur") )
|
|
36
|
+
return
|
|
37
|
+
fi
|
|
38
|
+
local first="\${COMP_WORDS[1]}"
|
|
39
|
+
local opts=""
|
|
40
|
+
case "$first" in
|
|
41
|
+
${cmdCases}
|
|
42
|
+
*) opts="${GLOBAL_FLAGS.join(' ')}" ;;
|
|
43
|
+
esac
|
|
44
|
+
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
|
|
12
45
|
}
|
|
13
46
|
complete -F _klyro_complete klyro
|
|
14
47
|
complete -F _klyro_complete ky
|
|
15
48
|
`;
|
|
16
49
|
}
|
|
17
50
|
function zshScript() {
|
|
51
|
+
const cmdCases = Object.entries(COMMAND_FLAGS)
|
|
52
|
+
.map(([c, fs]) => ` ${c}) _values 'flags' ${fs.map((f) => `'${f}'`).join(' ')} ;;`)
|
|
53
|
+
.join('\n');
|
|
18
54
|
return `#compdef klyro ky
|
|
19
55
|
_klyro() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
if (( CURRENT == 2 )); then
|
|
57
|
+
_describe 'klyro commands' ${COMMANDS.map((c) => `'${c}'`).join(' ')} ${GLOBAL_FLAGS.map((f) => `'${f}'`).join(' ')}
|
|
58
|
+
return
|
|
59
|
+
fi
|
|
60
|
+
case "$words[2]" in
|
|
61
|
+
${cmdCases}
|
|
62
|
+
*) _values 'flags' ${GLOBAL_FLAGS.map((f) => `'${f}'`).join(' ')} ;;
|
|
63
|
+
esac
|
|
23
64
|
}
|
|
24
65
|
compdef _klyro klyro ky
|
|
25
66
|
`;
|
|
26
67
|
}
|
|
27
68
|
function fishScript() {
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
const flagLines = Object.entries(COMMAND_FLAGS)
|
|
70
|
+
.flatMap(([c, fs]) => fs.map((f) => `complete -c klyro -f -n "__fish_seen_subcommand_from ${c}" -a ${f}`))
|
|
71
|
+
.join('\n');
|
|
72
|
+
return `# klyro fish completion (commands + flags)
|
|
73
|
+
${COMMANDS.map((c) => `complete -c klyro -f -n __fish_use_subcommand -a ${c}`).join('\n')}
|
|
74
|
+
${flagLines}
|
|
30
75
|
complete -c ky -f -a "${COMMANDS.join(' ')}"
|
|
31
76
|
`;
|
|
32
77
|
}
|
|
33
78
|
function powershellScript() {
|
|
34
|
-
return `# klyro powershell completion
|
|
79
|
+
return `# klyro powershell completion (commands + flags)
|
|
35
80
|
Register-ArgumentCompleter -Native -CommandName klyro,ky -ScriptBlock {
|
|
36
81
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
37
82
|
$cmds = @(${COMMANDS.map((c) => `'${c}'`).join(', ')})
|
|
38
|
-
$
|
|
83
|
+
$flags = @(${GLOBAL_FLAGS.map((f) => `'${f}'`).join(', ')})
|
|
84
|
+
$tokens = $commandAst.ToString() -split '\\s+'
|
|
85
|
+
if ($tokens.Count -le 2) { $cands = $cmds + $flags } else {
|
|
86
|
+
switch ($tokens[1]) {
|
|
87
|
+
${Object.entries(COMMAND_FLAGS).map(([c, fs]) => ` '${c}' { $cands = @(${fs.map((f) => `'${f}'`).join(', ')}) }`).join('\n')}
|
|
88
|
+
default { $cands = $flags }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
$cands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
39
92
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
40
93
|
}
|
|
41
94
|
}
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { z } from 'zod';
|
|
|
7
7
|
export declare const ConfigSchema: z.ZodObject<{
|
|
8
8
|
model: z.ZodOptional<z.ZodString>;
|
|
9
9
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
10
|
-
openai: "openai";
|
|
11
10
|
anthropic: "anthropic";
|
|
11
|
+
openai: "openai";
|
|
12
12
|
}>>;
|
|
13
13
|
baseUrl: z.ZodOptional<z.ZodString>;
|
|
14
14
|
apiKey: z.ZodOptional<z.ZodString>;
|
|
@@ -20,8 +20,8 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
20
20
|
providers: z.ZodOptional<z.ZodObject<{
|
|
21
21
|
failover: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
22
22
|
provider: z.ZodEnum<{
|
|
23
|
-
openai: "openai";
|
|
24
23
|
anthropic: "anthropic";
|
|
24
|
+
openai: "openai";
|
|
25
25
|
}>;
|
|
26
26
|
baseURL: z.ZodOptional<z.ZodString>;
|
|
27
27
|
apiKey: z.ZodOptional<z.ZodString>;
|
|
@@ -32,8 +32,8 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
32
32
|
export type KlyroConfig = z.infer<typeof ConfigSchema>;
|
|
33
33
|
export declare const FailoverEntrySchema: z.ZodObject<{
|
|
34
34
|
provider: z.ZodEnum<{
|
|
35
|
-
openai: "openai";
|
|
36
35
|
anthropic: "anthropic";
|
|
36
|
+
openai: "openai";
|
|
37
37
|
}>;
|
|
38
38
|
baseURL: z.ZodOptional<z.ZodString>;
|
|
39
39
|
apiKey: z.ZodOptional<z.ZodString>;
|
|
@@ -43,8 +43,8 @@ export type FailoverEntry = z.infer<typeof FailoverEntrySchema>;
|
|
|
43
43
|
export declare const FailoverConfigSchema: z.ZodObject<{
|
|
44
44
|
failover: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
45
45
|
provider: z.ZodEnum<{
|
|
46
|
-
openai: "openai";
|
|
47
46
|
anthropic: "anthropic";
|
|
47
|
+
openai: "openai";
|
|
48
48
|
}>;
|
|
49
49
|
baseURL: z.ZodOptional<z.ZodString>;
|
|
50
50
|
apiKey: z.ZodOptional<z.ZodString>;
|
package/dist/cli/doctor.js
CHANGED
|
@@ -108,6 +108,18 @@ function checkPlatform() {
|
|
|
108
108
|
const ok = ['win32', 'linux', 'darwin'].includes(process.platform);
|
|
109
109
|
return { name: 'Platform', ok, detail: `${process.platform} ${process.arch} ${ok ? '✓' : '✗ unsupported'}` };
|
|
110
110
|
}
|
|
111
|
+
async function checkSandbox() {
|
|
112
|
+
try {
|
|
113
|
+
const { detectSandbox } = await import('../tools/shell/sandbox.js');
|
|
114
|
+
const st = detectSandbox();
|
|
115
|
+
if (st.active)
|
|
116
|
+
return { name: 'Sandbox', ok: true, detail: `${st.backend} ✓` };
|
|
117
|
+
return { name: 'Sandbox', ok: true, detail: `none — ${st.reason ?? 'policy+path guards only'}` };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { name: 'Sandbox', ok: true, detail: 'none — policy+path guards only' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
111
123
|
async function checkMcp(cwd) {
|
|
112
124
|
try {
|
|
113
125
|
const { loadMcpServers } = await import('../mcp/config.js');
|
|
@@ -167,6 +179,7 @@ export async function runDoctor(opts = {}) {
|
|
|
167
179
|
checks.push(await checkGit());
|
|
168
180
|
checks.push(await checkTools());
|
|
169
181
|
checks.push(checkPlatform());
|
|
182
|
+
checks.push(await checkSandbox());
|
|
170
183
|
const mcpCheck = await checkMcp(cwd);
|
|
171
184
|
const trustCheck = await checkTrust();
|
|
172
185
|
checks.push(mcpCheck);
|
package/dist/cli/eval.d.ts
CHANGED
|
@@ -54,6 +54,10 @@ export interface EvalScenario {
|
|
|
54
54
|
};
|
|
55
55
|
/** Adapter events to script, as [eventKind, ...args] tuples. */
|
|
56
56
|
scripted_events?: Array<Array<unknown[]>>;
|
|
57
|
+
/** Semantic rubric graded by a model judge (needs --judge-model). */
|
|
58
|
+
judge?: {
|
|
59
|
+
rubric: string[];
|
|
60
|
+
};
|
|
57
61
|
expect?: {
|
|
58
62
|
status?: RunResult['status'];
|
|
59
63
|
textContains?: string;
|
|
@@ -70,6 +74,11 @@ export interface EvalResult {
|
|
|
70
74
|
toolCalls: number;
|
|
71
75
|
text: string;
|
|
72
76
|
durationMs: number;
|
|
77
|
+
judge?: {
|
|
78
|
+
pass: boolean;
|
|
79
|
+
notes: string;
|
|
80
|
+
skipped: boolean;
|
|
81
|
+
};
|
|
73
82
|
}
|
|
74
83
|
export interface RunEvalOptions {
|
|
75
84
|
inputPath: string;
|
|
@@ -79,7 +88,12 @@ export interface RunEvalOptions {
|
|
|
79
88
|
runs?: number;
|
|
80
89
|
parallel?: number;
|
|
81
90
|
model?: string;
|
|
91
|
+
/** Live model id for grading `judge.rubric` (env endpoint + key required). */
|
|
92
|
+
judgeModel?: string;
|
|
82
93
|
}
|
|
83
94
|
export declare function runEval(opts: RunEvalOptions): Promise<number>;
|
|
84
95
|
export declare function scriptedAdapterFromSpec(spec: Array<Array<unknown[]>> | undefined): ProviderAdapter;
|
|
85
|
-
export declare function runScenario(sc: EvalScenario
|
|
96
|
+
export declare function runScenario(sc: EvalScenario, judgeOpts?: {
|
|
97
|
+
adapter: ProviderAdapter;
|
|
98
|
+
model: string;
|
|
99
|
+
}): Promise<EvalResult>;
|
package/dist/cli/eval.js
CHANGED
|
@@ -131,10 +131,23 @@ export async function runEval(opts) {
|
|
|
131
131
|
stderr.write('klyro eval: no scenarios in input\n');
|
|
132
132
|
return 2;
|
|
133
133
|
}
|
|
134
|
+
// Live judge adapter for `judge.rubric` (env endpoint + key; mock otherwise).
|
|
135
|
+
let judgeAdapter;
|
|
136
|
+
if (opts.judgeModel) {
|
|
137
|
+
const { httpChatAdapter } = await import('../agent/provider-adapter.js');
|
|
138
|
+
const { getStoredKey } = await import('./auth.js');
|
|
139
|
+
const baseUrl = process.env.KLYRO_BASE_URL;
|
|
140
|
+
const apiKey = process.env.KLYRO_API_KEY ?? getStoredKey('openai') ?? getStoredKey('anthropic');
|
|
141
|
+
if (!baseUrl || !apiKey) {
|
|
142
|
+
stderr.write('klyro eval: --judge-model needs KLYRO_BASE_URL and KLYRO_API_KEY (or a stored key)\n');
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
judgeAdapter = httpChatAdapter({ baseURL: baseUrl, apiKey });
|
|
146
|
+
}
|
|
134
147
|
const results = [];
|
|
135
148
|
for (const sc of scenarios) {
|
|
136
149
|
const start = Date.now();
|
|
137
|
-
const r = await runScenario(sc);
|
|
150
|
+
const r = await runScenario(sc, judgeAdapter && opts.judgeModel ? { adapter: judgeAdapter, model: opts.judgeModel } : undefined);
|
|
138
151
|
r.durationMs = Date.now() - start;
|
|
139
152
|
results.push(r);
|
|
140
153
|
if (opts.output === 'json') {
|
|
@@ -223,7 +236,7 @@ function tupleToEvent(tuple) {
|
|
|
223
236
|
throw new Error(`scriptedAdapterFromSpec: unknown event kind: ${kind}`);
|
|
224
237
|
}
|
|
225
238
|
}
|
|
226
|
-
export async function runScenario(sc) {
|
|
239
|
+
export async function runScenario(sc, judgeOpts) {
|
|
227
240
|
const failures = [];
|
|
228
241
|
const model = sc.model ?? 'mock';
|
|
229
242
|
const adapter = scriptedAdapterFromSpec(sc.scripted_events);
|
|
@@ -265,6 +278,24 @@ export async function runScenario(sc) {
|
|
|
265
278
|
if (exp.toolCallsAtMost !== undefined && result.toolCalls > exp.toolCallsAtMost) {
|
|
266
279
|
failures.push(`toolCalls: expected <= ${exp.toolCallsAtMost}, got ${result.toolCalls}`);
|
|
267
280
|
}
|
|
281
|
+
// Model-graded semantic check (opt-in: needs a live judge adapter).
|
|
282
|
+
let judge;
|
|
283
|
+
if (sc.judge && sc.judge.rubric.length > 0) {
|
|
284
|
+
if (judgeOpts) {
|
|
285
|
+
const { runJudge } = await import('../eval/judge.js');
|
|
286
|
+
const v = await runJudge(judgeOpts.adapter, judgeOpts.model, {
|
|
287
|
+
task: sc.task,
|
|
288
|
+
finalText: result.finalText,
|
|
289
|
+
toolCalls: result.toolCalls,
|
|
290
|
+
}, sc.judge.rubric);
|
|
291
|
+
judge = { pass: v.pass, notes: v.notes, skipped: v.skipped };
|
|
292
|
+
if (!v.pass)
|
|
293
|
+
failures.push(`judge: ${v.notes || 'rubric unmet'}`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
judge = { pass: true, notes: 'no judge adapter — skipped', skipped: true };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
268
299
|
return {
|
|
269
300
|
name: sc.name,
|
|
270
301
|
passed: failures.length === 0,
|
|
@@ -274,5 +305,6 @@ export async function runScenario(sc) {
|
|
|
274
305
|
toolCalls: result.toolCalls,
|
|
275
306
|
text: result.finalText,
|
|
276
307
|
durationMs: 0,
|
|
308
|
+
...(judge ? { judge } : {}),
|
|
277
309
|
};
|
|
278
310
|
}
|
package/dist/cli/hooks.d.ts
CHANGED
|
@@ -1,26 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hooks engine (10.2) —
|
|
2
|
+
* Hooks engine (10.2, v2) — lifecycle + tool shell hooks.
|
|
3
|
+
*
|
|
4
|
+
* Events: `preToolUse` / `postToolUse` (per tool call), `sessionStart`
|
|
5
|
+
* (once per run, may abort it), `sessionEnd` (once per run, best-effort),
|
|
6
|
+
* `stop` (after each agent step).
|
|
3
7
|
*
|
|
4
8
|
* Config files:
|
|
5
9
|
* - project: `<cwd>/.klyro/hooks.json`
|
|
6
10
|
* - global: `~/.klyro/hooks.json` (merged; project wins on name clash)
|
|
7
11
|
*
|
|
8
|
-
* Schema: `{ hooks: Array<{ name, event, command, timeoutMs? }> }`.
|
|
12
|
+
* Schema: `{ hooks: Array<{ name, event, command, matcher?, timeoutMs? }> }`.
|
|
13
|
+
* `matcher` is a regex tested against the tool name — lifecycle events
|
|
14
|
+
* (`sessionStart`/`sessionEnd`/`stop`) always match; tool events without a
|
|
15
|
+
* matcher match every tool.
|
|
9
16
|
*
|
|
10
17
|
* `loadHooks` never throws — a missing file is `[]`, an invalid file is
|
|
11
18
|
* `[]` plus a one-time stderr warning per path. `runHook` spawns the
|
|
12
19
|
* command with `shell: true` (commands are strings, must work on win +
|
|
13
|
-
* posix), a default 30s timeout, a filtered env,
|
|
14
|
-
*
|
|
20
|
+
* posix), a default 30s timeout, a filtered env, `KLYRO_TOOL_NAME` /
|
|
21
|
+
* `KLYRO_TOOL_INPUT_JSON` for inspection, AND the full JSON payload on
|
|
22
|
+
* stdin (`{ event, tool, input, sessionId, ... }`).
|
|
15
23
|
*/
|
|
16
24
|
import { z } from 'zod';
|
|
25
|
+
export declare const HookEventSchema: z.ZodEnum<{
|
|
26
|
+
stop: "stop";
|
|
27
|
+
preToolUse: "preToolUse";
|
|
28
|
+
postToolUse: "postToolUse";
|
|
29
|
+
sessionStart: "sessionStart";
|
|
30
|
+
sessionEnd: "sessionEnd";
|
|
31
|
+
}>;
|
|
32
|
+
export type HookEvent = z.infer<typeof HookEventSchema>;
|
|
17
33
|
export declare const HookSchema: z.ZodObject<{
|
|
18
34
|
name: z.ZodString;
|
|
19
35
|
event: z.ZodEnum<{
|
|
36
|
+
stop: "stop";
|
|
20
37
|
preToolUse: "preToolUse";
|
|
21
38
|
postToolUse: "postToolUse";
|
|
39
|
+
sessionStart: "sessionStart";
|
|
40
|
+
sessionEnd: "sessionEnd";
|
|
22
41
|
}>;
|
|
23
42
|
command: z.ZodString;
|
|
43
|
+
matcher: z.ZodOptional<z.ZodString>;
|
|
24
44
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
25
45
|
}, z.core.$strip>;
|
|
26
46
|
export type Hook = z.infer<typeof HookSchema>;
|
|
@@ -40,8 +60,37 @@ export interface HookContext {
|
|
|
40
60
|
toolName: string;
|
|
41
61
|
input: unknown;
|
|
42
62
|
}
|
|
63
|
+
/** Lifecycle payload delivered on stdin (and merged into env where small). */
|
|
64
|
+
export interface HookPayload {
|
|
65
|
+
event: HookEvent;
|
|
66
|
+
tool?: string;
|
|
67
|
+
input?: unknown;
|
|
68
|
+
sessionId?: string;
|
|
69
|
+
cwd?: string;
|
|
70
|
+
status?: string;
|
|
71
|
+
step?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Select hooks for an event. Tool events honor `matcher` (regex against the
|
|
75
|
+
* tool name; invalid regex never matches); lifecycle events always match.
|
|
76
|
+
* Exported pure for unit tests.
|
|
77
|
+
*/
|
|
78
|
+
export declare function hooksForEvent(hooks: Hook[], event: HookEvent, toolName?: string): Hook[];
|
|
43
79
|
/**
|
|
44
80
|
* Run one hook. Resolves (never rejects) with the exit code + sliced
|
|
45
81
|
* output. `ok` is true only when the process exited 0.
|
|
82
|
+
*
|
|
83
|
+
* `stdinJson` (when given) is written to the child's stdin as JSON —
|
|
84
|
+
* the primary contract (mirrors CC's stdin-JSON hooks); the env vars are
|
|
85
|
+
* kept as a convenience for shell one-liners.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runHook(hook: Hook, ctx: HookContext, stdinJson?: unknown): Promise<HookResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Run all `sessionEnd` hooks for a finished run (best-effort, sequential).
|
|
90
|
+
* Returns hook outputs for logging. Never throws.
|
|
46
91
|
*/
|
|
47
|
-
export declare function
|
|
92
|
+
export declare function runSessionEndHooks(cwd: string, sessionId: string | undefined, status: string): Promise<Array<{
|
|
93
|
+
name: string;
|
|
94
|
+
ok: boolean;
|
|
95
|
+
output: string;
|
|
96
|
+
}>>;
|
package/dist/cli/hooks.js
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hooks engine (10.2) —
|
|
2
|
+
* Hooks engine (10.2, v2) — lifecycle + tool shell hooks.
|
|
3
|
+
*
|
|
4
|
+
* Events: `preToolUse` / `postToolUse` (per tool call), `sessionStart`
|
|
5
|
+
* (once per run, may abort it), `sessionEnd` (once per run, best-effort),
|
|
6
|
+
* `stop` (after each agent step).
|
|
3
7
|
*
|
|
4
8
|
* Config files:
|
|
5
9
|
* - project: `<cwd>/.klyro/hooks.json`
|
|
6
10
|
* - global: `~/.klyro/hooks.json` (merged; project wins on name clash)
|
|
7
11
|
*
|
|
8
|
-
* Schema: `{ hooks: Array<{ name, event, command, timeoutMs? }> }`.
|
|
12
|
+
* Schema: `{ hooks: Array<{ name, event, command, matcher?, timeoutMs? }> }`.
|
|
13
|
+
* `matcher` is a regex tested against the tool name — lifecycle events
|
|
14
|
+
* (`sessionStart`/`sessionEnd`/`stop`) always match; tool events without a
|
|
15
|
+
* matcher match every tool.
|
|
9
16
|
*
|
|
10
17
|
* `loadHooks` never throws — a missing file is `[]`, an invalid file is
|
|
11
18
|
* `[]` plus a one-time stderr warning per path. `runHook` spawns the
|
|
12
19
|
* command with `shell: true` (commands are strings, must work on win +
|
|
13
|
-
* posix), a default 30s timeout, a filtered env,
|
|
14
|
-
*
|
|
20
|
+
* posix), a default 30s timeout, a filtered env, `KLYRO_TOOL_NAME` /
|
|
21
|
+
* `KLYRO_TOOL_INPUT_JSON` for inspection, AND the full JSON payload on
|
|
22
|
+
* stdin (`{ event, tool, input, sessionId, ... }`).
|
|
15
23
|
*/
|
|
16
24
|
import { spawn } from 'node:child_process';
|
|
17
25
|
import * as fs from 'node:fs';
|
|
18
26
|
import * as os from 'node:os';
|
|
19
27
|
import * as path from 'node:path';
|
|
20
28
|
import { z } from 'zod';
|
|
29
|
+
export const HookEventSchema = z.enum(['preToolUse', 'postToolUse', 'sessionStart', 'sessionEnd', 'stop']);
|
|
21
30
|
export const HookSchema = z.object({
|
|
22
31
|
name: z.string().min(1),
|
|
23
|
-
event:
|
|
32
|
+
event: HookEventSchema,
|
|
24
33
|
command: z.string().min(1),
|
|
34
|
+
/** Optional regex matched against the tool name (tool events only). */
|
|
35
|
+
matcher: z.string().min(1).optional(),
|
|
25
36
|
timeoutMs: z.number().int().positive().optional(),
|
|
26
37
|
});
|
|
27
38
|
const HooksFileSchema = z.object({
|
|
@@ -116,11 +127,36 @@ function hookEnv() {
|
|
|
116
127
|
out.PATH = process.env.PATH;
|
|
117
128
|
return out;
|
|
118
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Select hooks for an event. Tool events honor `matcher` (regex against the
|
|
132
|
+
* tool name; invalid regex never matches); lifecycle events always match.
|
|
133
|
+
* Exported pure for unit tests.
|
|
134
|
+
*/
|
|
135
|
+
export function hooksForEvent(hooks, event, toolName) {
|
|
136
|
+
return hooks.filter((h) => {
|
|
137
|
+
if (h.event !== event)
|
|
138
|
+
return false;
|
|
139
|
+
if (h.matcher === undefined)
|
|
140
|
+
return true;
|
|
141
|
+
if (toolName === undefined)
|
|
142
|
+
return true; // lifecycle events have no tool
|
|
143
|
+
try {
|
|
144
|
+
return new RegExp(h.matcher).test(toolName);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
119
151
|
/**
|
|
120
152
|
* Run one hook. Resolves (never rejects) with the exit code + sliced
|
|
121
153
|
* output. `ok` is true only when the process exited 0.
|
|
154
|
+
*
|
|
155
|
+
* `stdinJson` (when given) is written to the child's stdin as JSON —
|
|
156
|
+
* the primary contract (mirrors CC's stdin-JSON hooks); the env vars are
|
|
157
|
+
* kept as a convenience for shell one-liners.
|
|
122
158
|
*/
|
|
123
|
-
export function runHook(hook, ctx) {
|
|
159
|
+
export function runHook(hook, ctx, stdinJson) {
|
|
124
160
|
const timeoutMs = hook.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;
|
|
125
161
|
return new Promise((resolve) => {
|
|
126
162
|
let env;
|
|
@@ -139,6 +175,13 @@ export function runHook(hook, ctx) {
|
|
|
139
175
|
catch {
|
|
140
176
|
env.KLYRO_TOOL_INPUT_JSON = '{}';
|
|
141
177
|
}
|
|
178
|
+
let payload = '';
|
|
179
|
+
try {
|
|
180
|
+
payload = JSON.stringify(stdinJson ?? { event: 'preToolUse', tool: ctx.toolName, input: ctx.input ?? {} });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
payload = '{}';
|
|
184
|
+
}
|
|
142
185
|
let child;
|
|
143
186
|
try {
|
|
144
187
|
child = spawn(hook.command, {
|
|
@@ -147,6 +190,7 @@ export function runHook(hook, ctx) {
|
|
|
147
190
|
windowsHide: true,
|
|
148
191
|
timeout: timeoutMs,
|
|
149
192
|
env,
|
|
193
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
150
194
|
});
|
|
151
195
|
}
|
|
152
196
|
catch (err) {
|
|
@@ -177,5 +221,40 @@ export function runHook(hook, ctx) {
|
|
|
177
221
|
stderr: stderr.slice(0, MAX_HOOK_OUTPUT_CHARS),
|
|
178
222
|
});
|
|
179
223
|
});
|
|
224
|
+
// Deliver the stdin JSON contract, then close so the child never hangs
|
|
225
|
+
// waiting for EOF. Write errors (EPIPE on early exit) are ignored —
|
|
226
|
+
// the exit code below is what matters.
|
|
227
|
+
try {
|
|
228
|
+
if (child.stdin) {
|
|
229
|
+
child.stdin.on('error', () => undefined);
|
|
230
|
+
child.stdin.write(payload);
|
|
231
|
+
child.stdin.end();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
180
235
|
});
|
|
181
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Run all `sessionEnd` hooks for a finished run (best-effort, sequential).
|
|
239
|
+
* Returns hook outputs for logging. Never throws.
|
|
240
|
+
*/
|
|
241
|
+
export async function runSessionEndHooks(cwd, sessionId, status) {
|
|
242
|
+
const out = [];
|
|
243
|
+
let hooks;
|
|
244
|
+
try {
|
|
245
|
+
hooks = hooksForEvent(loadHooks(cwd), 'sessionEnd');
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
for (const h of hooks) {
|
|
251
|
+
try {
|
|
252
|
+
const r = await runHook(h, { toolName: '', input: {} }, { event: 'sessionEnd', sessionId, cwd, status });
|
|
253
|
+
out.push({ name: h.name, ok: r.ok, output: (r.stdout || r.stderr || '').slice(0, 2000) });
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
out.push({ name: h.name, ok: false, output: '' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project bootstrap shared by `klyro init` and the REPL `/init`:
|
|
3
|
+
* scan-seeded KLYRO.md (only when absent) + `.mcp.json` skeleton.
|
|
4
|
+
* Never overwrites user files; returns created vs skipped lists.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
function captureStdout() {
|
|
9
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
10
|
+
let out = '';
|
|
11
|
+
process.stdout.write = ((c) => {
|
|
12
|
+
out += String(c);
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
release: () => {
|
|
17
|
+
process.stdout.write = orig;
|
|
18
|
+
return out;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function initProject(cwd) {
|
|
23
|
+
const created = [];
|
|
24
|
+
const skipped = [];
|
|
25
|
+
const klyroMd = path.join(cwd, 'KLYRO.md');
|
|
26
|
+
if (fs.existsSync(klyroMd)) {
|
|
27
|
+
skipped.push('KLYRO.md (exists)');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const { runScan } = await import('./scan.js');
|
|
31
|
+
const cap = captureStdout();
|
|
32
|
+
try {
|
|
33
|
+
await runScan({ cwd, json: false });
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
// release even if scan throws — partial output still seeds the draft
|
|
37
|
+
}
|
|
38
|
+
const out = cap.release();
|
|
39
|
+
fs.writeFileSync(klyroMd, `# 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`, 'utf-8');
|
|
40
|
+
created.push('KLYRO.md');
|
|
41
|
+
}
|
|
42
|
+
const mcpJson = path.join(cwd, '.mcp.json');
|
|
43
|
+
if (fs.existsSync(mcpJson)) {
|
|
44
|
+
skipped.push('.mcp.json (exists)');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
fs.writeFileSync(mcpJson, JSON.stringify({ mcpServers: {} }, null, 2) + '\n', 'utf-8');
|
|
48
|
+
created.push('.mcp.json');
|
|
49
|
+
}
|
|
50
|
+
return { created, skipped };
|
|
51
|
+
}
|
|
52
|
+
export function nextStepsText() {
|
|
53
|
+
return [
|
|
54
|
+
'Next steps:',
|
|
55
|
+
' klyro login # store provider key once (or set KLYRO_BASE_URL/_API_KEY/_MODEL)',
|
|
56
|
+
' klyro doctor # verify toolchain, provider, sessions, sandbox',
|
|
57
|
+
' klyro agents # list subagents (add yours in .klyro/agents/*.md)',
|
|
58
|
+
' klyro mcp add <n> <cmd># attach a tool server (or https:// URL for remote)',
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|