klyro 1.0.5 → 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.
Files changed (47) hide show
  1. package/dist/agent/custom-agents.d.ts +3 -0
  2. package/dist/agent/custom-agents.js +96 -0
  3. package/dist/agent/orchestrator.d.ts +22 -2
  4. package/dist/agent/orchestrator.js +30 -4
  5. package/dist/agent/runtime.d.ts +5 -0
  6. package/dist/agent/runtime.js +164 -50
  7. package/dist/checkpoints/store.d.ts +9 -0
  8. package/dist/checkpoints/store.js +20 -0
  9. package/dist/cli/completion.js +2 -2
  10. package/dist/cli/config.d.ts +4 -4
  11. package/dist/cli/eval.d.ts +15 -1
  12. package/dist/cli/eval.js +34 -2
  13. package/dist/cli/hooks.d.ts +54 -5
  14. package/dist/cli/hooks.js +85 -6
  15. package/dist/cli/init.d.ts +6 -0
  16. package/dist/cli/init.js +60 -0
  17. package/dist/cli/repl.js +146 -25
  18. package/dist/cli/run.d.ts +7 -1
  19. package/dist/cli/run.js +92 -50
  20. package/dist/cli/slash/custom.d.ts +25 -0
  21. package/dist/cli/slash/custom.js +166 -0
  22. package/dist/cli/slash/parser.d.ts +9 -1
  23. package/dist/cli/slash/parser.js +31 -9
  24. package/dist/context/memory.js +18 -1
  25. package/dist/eval/harness.d.ts +21 -3
  26. package/dist/eval/harness.js +31 -3
  27. package/dist/eval/judge.d.ts +32 -0
  28. package/dist/eval/judge.js +63 -0
  29. package/dist/eval/tasks.js +134 -0
  30. package/dist/index.js +150 -127
  31. package/dist/mcp/client.d.ts +15 -0
  32. package/dist/mcp/client.js +42 -2
  33. package/dist/mcp/config.d.ts +3 -1
  34. package/dist/mcp/config.js +19 -1
  35. package/dist/mcp/registry.d.ts +13 -0
  36. package/dist/mcp/registry.js +41 -1
  37. package/dist/mcp/remote.d.ts +29 -0
  38. package/dist/mcp/remote.js +153 -0
  39. package/dist/policy/approval.d.ts +15 -1
  40. package/dist/policy/approval.js +8 -0
  41. package/dist/providers/endpoints.d.ts +43 -0
  42. package/dist/providers/endpoints.js +104 -0
  43. package/dist/providers.js +13 -10
  44. package/dist/tui/app.js +140 -13
  45. package/dist/tui/app.test.js +24 -0
  46. package/dist/tui/approval.js +53 -1
  47. package/package.json +1 -1
@@ -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>;
@@ -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): Promise<EvalResult>;
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
  }
@@ -1,26 +1,46 @@
1
1
  /**
2
- * Hooks engine (10.2) — preToolUse / postToolUse shell hooks.
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, plus `KLYRO_TOOL_NAME`
14
- * and `KLYRO_TOOL_INPUT_JSON` for the hook's inspection.
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 runHook(hook: Hook, ctx: HookContext): Promise<HookResult>;
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) — preToolUse / postToolUse shell hooks.
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, plus `KLYRO_TOOL_NAME`
14
- * and `KLYRO_TOOL_INPUT_JSON` for the hook's inspection.
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: z.enum(['preToolUse', 'postToolUse']),
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
+ }
@@ -0,0 +1,6 @@
1
+ export interface InitResult {
2
+ created: string[];
3
+ skipped: string[];
4
+ }
5
+ export declare function initProject(cwd: string): Promise<InitResult>;
6
+ export declare function nextStepsText(): string;
@@ -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
+ }