klyro 1.0.5 → 1.0.7

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/README.md +13 -0
  2. package/dist/agent/custom-agents.d.ts +3 -0
  3. package/dist/agent/custom-agents.js +96 -0
  4. package/dist/agent/orchestrator.d.ts +26 -0
  5. package/dist/agent/orchestrator.js +41 -4
  6. package/dist/agent/runtime.d.ts +15 -0
  7. package/dist/agent/runtime.js +232 -61
  8. package/dist/chat.d.ts +10 -0
  9. package/dist/chat.js +39 -7
  10. package/dist/checkpoints/store.d.ts +11 -0
  11. package/dist/checkpoints/store.js +32 -0
  12. package/dist/cli/auth.d.ts +10 -3
  13. package/dist/cli/auth.js +43 -5
  14. package/dist/cli/completion.js +2 -2
  15. package/dist/cli/config.d.ts +4 -4
  16. package/dist/cli/doctor.js +0 -1
  17. package/dist/cli/eval.d.ts +15 -1
  18. package/dist/cli/eval.js +43 -5
  19. package/dist/cli/hooks.d.ts +74 -5
  20. package/dist/cli/hooks.js +118 -7
  21. package/dist/cli/init.d.ts +6 -0
  22. package/dist/cli/init.js +60 -0
  23. package/dist/cli/keychain.d.ts +10 -0
  24. package/dist/cli/keychain.js +86 -0
  25. package/dist/cli/repl.js +188 -30
  26. package/dist/cli/run.d.ts +7 -1
  27. package/dist/cli/run.js +92 -50
  28. package/dist/cli/setup.js +3 -2
  29. package/dist/cli/slash/custom.d.ts +25 -0
  30. package/dist/cli/slash/custom.js +166 -0
  31. package/dist/cli/slash/parser.d.ts +9 -1
  32. package/dist/cli/slash/parser.js +34 -9
  33. package/dist/cli/update.d.ts +3 -1
  34. package/dist/cli/update.js +16 -1
  35. package/dist/context/accounting.d.ts +6 -0
  36. package/dist/context/accounting.js +8 -2
  37. package/dist/context/compaction.d.ts +2 -1
  38. package/dist/context/compaction.js +39 -12
  39. package/dist/context/memory.d.ts +11 -0
  40. package/dist/context/memory.js +59 -4
  41. package/dist/eval/harness.d.ts +40 -5
  42. package/dist/eval/harness.js +103 -10
  43. package/dist/eval/judge.d.ts +32 -0
  44. package/dist/eval/judge.js +63 -0
  45. package/dist/eval/tasks.js +134 -0
  46. package/dist/index.js +239 -130
  47. package/dist/mcp/auth.d.ts +85 -0
  48. package/dist/mcp/auth.js +249 -0
  49. package/dist/mcp/client.d.ts +15 -0
  50. package/dist/mcp/client.js +42 -2
  51. package/dist/mcp/config.d.ts +31 -1
  52. package/dist/mcp/config.js +84 -1
  53. package/dist/mcp/registry.d.ts +19 -0
  54. package/dist/mcp/registry.js +118 -2
  55. package/dist/mcp/remote.d.ts +36 -0
  56. package/dist/mcp/remote.js +207 -0
  57. package/dist/mcp/sse.d.ts +42 -0
  58. package/dist/mcp/sse.js +310 -0
  59. package/dist/persistence/audit.d.ts +15 -3
  60. package/dist/persistence/audit.js +84 -13
  61. package/dist/persistence/store.d.ts +9 -0
  62. package/dist/persistence/store.js +17 -0
  63. package/dist/policy/approval.d.ts +15 -1
  64. package/dist/policy/approval.js +8 -0
  65. package/dist/policy/engine.js +9 -0
  66. package/dist/providers/endpoints.d.ts +43 -0
  67. package/dist/providers/endpoints.js +104 -0
  68. package/dist/providers.js +17 -14
  69. package/dist/tools/shell/shell-exec.d.ts +13 -0
  70. package/dist/tools/shell/shell-exec.js +64 -2
  71. package/dist/tui/app.js +172 -15
  72. package/dist/tui/app.test.js +27 -2
  73. package/dist/tui/approval.js +55 -1
  74. package/dist/tui/scroll-model.d.ts +2 -2
  75. package/dist/tui/scroll-model.js +9 -3
  76. package/dist/tui/tokens.d.ts +8 -11
  77. package/dist/tui/tokens.js +18 -11
  78. package/package.json +1 -1
package/README.md CHANGED
@@ -55,6 +55,19 @@ node dist/index.js chat
55
55
  | `KLYRO_WORKER=0` | Disable subprocess isolation for subagents |
56
56
  | `KLYRO_SESSIONS_DIR`, `KLYRO_UPDATE_CACHE`, `KLYRO_CREDENTIALS_FILE` | Relocatable state (tests + power users) |
57
57
 
58
+ ## New in recent releases
59
+
60
+ - `klyro run --bare` — deterministic runs: skips MCP, hooks, memory/KLYRO.md/context, persistence
61
+ - `klyro mcp trust <name>` / `mcp prompts [server]` / `mcp add <name> <https-url>` — remote MCP + prompt trust
62
+ - `klyro agents lint` — validate `.klyro/agents/*.md` (ids, tool names)
63
+ - `klyro init` — scan-seeded `KLYRO.md` + `.mcp.json` (never overwrites)
64
+ - `klyro update --apply` — opt-in self-apply of the verified update
65
+ - `klyro eval --judge-model <id>` — model-graded rubric scoring
66
+ - Hooks: `matcher` scoping, stdin JSON, `sessionStart`/`sessionEnd`/`stop` events, JSON verdicts
67
+ - Custom agents (`.klyro/agents/*.md`), custom commands (`.klyro/commands/*.md`), vim mode (`/vim`), `@`-file completion
68
+ - Credentials prefer the OS keychain (macOS Keychain, Linux libsecret), 0600 file fallback
69
+ - Headless JSON ends with exactly one stable `kind:result` envelope (parse the LAST line)
70
+
58
71
  ## Documentation
59
72
 
60
73
  | Doc | Purpose |
@@ -0,0 +1,3 @@
1
+ import type { AgentDefinition } from './orchestrator.js';
2
+ /** Load custom agents: global first, project wins on id clash. Never throws. */
3
+ export declare function loadCustomAgents(cwd: string): AgentDefinition[];
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Custom subagents from markdown files:
3
+ * `<cwd>/.klyro/agents/*.md` (project) + `~/.klyro/agents/*.md` (global).
4
+ * Project wins on id clash (including overriding a builtin).
5
+ *
6
+ * Frontmatter fields: name (default: filename), description, tools
7
+ * (comma/list — omitted inherits parent tools), model, readonly,
8
+ * canSpawn, maxSteps, maxTokens, maxCost, maxTimeMs, allowedPaths.
9
+ * The markdown body becomes specialist instructions (`prompt`) prepended
10
+ * to the delegated task. Unknown tool names are NOT rejected here —
11
+ * `resolveCapabilities` drops them with reasons at spawn time.
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import * as os from 'node:os';
15
+ import * as path from 'node:path';
16
+ import { parseFrontmatter, parseList, parseBool, parseInt_ } from '../cli/slash/custom.js';
17
+ const AGENT_ID_RE = /^[A-Za-z0-9_-]{1,32}$/;
18
+ function readAgentFile(file, source) {
19
+ let raw;
20
+ try {
21
+ raw = fs.readFileSync(file, 'utf-8');
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ const { data, body } = parseFrontmatter(raw);
27
+ const fallback = path.basename(file, path.extname(file));
28
+ const id = (data['name'] || fallback).toLowerCase();
29
+ if (!AGENT_ID_RE.test(id))
30
+ return null;
31
+ const description = data['description'] || `Custom agent ${id}`;
32
+ const def = { id, description };
33
+ const tools = parseList(data['tools']);
34
+ if (tools.length > 0)
35
+ def.allowedTools = tools;
36
+ if (data['model'])
37
+ def.model = data['model'];
38
+ if (data['readonly'] !== undefined && data['readonly'] !== '')
39
+ def.readonly = parseBool(data['readonly'], false);
40
+ if (data['canSpawn'] !== undefined && data['canSpawn'] !== '')
41
+ def.canSpawn = parseBool(data['canSpawn'], false);
42
+ const maxSteps = parseInt_(data['maxsteps']);
43
+ if (maxSteps !== undefined)
44
+ def.maxSteps = maxSteps;
45
+ const maxTokens = parseInt_(data['maxtokens']);
46
+ if (maxTokens !== undefined)
47
+ def.maxTokens = maxTokens;
48
+ const maxCost = data['maxcost'] !== undefined && data['maxcost'] !== '' ? Number(data['maxcost']) : undefined;
49
+ if (maxCost !== undefined && Number.isFinite(maxCost) && maxCost > 0)
50
+ def.maxCost = maxCost;
51
+ const maxTimeMs = parseInt_(data['maxtimems']);
52
+ if (maxTimeMs !== undefined)
53
+ def.maxTimeMs = maxTimeMs;
54
+ const paths = parseList(data['allowedpaths']);
55
+ if (paths.length > 0)
56
+ def.allowedPaths = paths;
57
+ const prompt = body.trim();
58
+ if (prompt)
59
+ def.prompt = prompt;
60
+ def.source = source;
61
+ return def;
62
+ }
63
+ function listAgentFiles(dir) {
64
+ let entries;
65
+ try {
66
+ entries = fs.readdirSync(dir, { withFileTypes: true });
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ return entries
72
+ .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md'))
73
+ .map((e) => path.join(dir, e.name))
74
+ .sort();
75
+ }
76
+ /** Load custom agents: global first, project wins on id clash. Never throws. */
77
+ export function loadCustomAgents(cwd) {
78
+ const byId = new Map();
79
+ try {
80
+ const home = os.homedir() || process.cwd();
81
+ for (const f of listAgentFiles(path.join(home, '.klyro', 'agents'))) {
82
+ const d = readAgentFile(f, 'global');
83
+ if (d)
84
+ byId.set(d.id, d);
85
+ }
86
+ for (const f of listAgentFiles(path.join(cwd, '.klyro', 'agents'))) {
87
+ const d = readAgentFile(f, 'project');
88
+ if (d)
89
+ byId.set(d.id, d);
90
+ }
91
+ }
92
+ catch {
93
+ return [...byId.values()];
94
+ }
95
+ return [...byId.values()];
96
+ }
@@ -43,9 +43,21 @@ export interface AgentDefinition {
43
43
  * spawn time (`undefined` = no additional constraint).
44
44
  */
45
45
  allowedPaths?: string[];
46
+ /**
47
+ * Specialist instructions (from `.klyro/agents/*.md` body or programmatic
48
+ * defs). Prepended to the delegated task at spawn time.
49
+ */
50
+ prompt?: string;
51
+ /** Where the definition came from (builtins omit this = 'builtin'). */
52
+ source?: 'builtin' | 'project' | 'global';
46
53
  }
47
54
  /** Default agents a model can delegate to. */
48
55
  export declare const BUILTIN_AGENTS: readonly AgentDefinition[];
56
+ /**
57
+ * Layer specialist instructions into a child's system prompt (not the user
58
+ * task). Pure — unit-tested directly. No-op when the def has no prompt.
59
+ */
60
+ export declare function layerSpecialistPrompt(base: RuntimeDeps['systemPrompt'], def: Pick<AgentDefinition, 'id' | 'prompt'>): RuntimeDeps['systemPrompt'];
49
61
  /** Compact summary returned to the parent — the child's transcript stays separate. */
50
62
  export interface ChildSummary {
51
63
  taskId: string;
@@ -158,7 +170,20 @@ export interface OrchestratorOpts {
158
170
  * always isolate. Defaults to false.
159
171
  */
160
172
  isTui?: boolean;
173
+ /**
174
+ * Working directory used to discover custom agents
175
+ * (`.klyro/agents/*.md`). Defaults to `process.cwd()`.
176
+ */
177
+ cwd?: string;
161
178
  }
179
+ /**
180
+ * All agents: builtins plus custom `.klyro/agents/*.md` definitions.
181
+ * Custom ids win on clash (including overriding a builtin) — the override
182
+ * is surfaced via `source`. No instance needed; used by CLI + spawn paths.
183
+ */
184
+ export declare function listAllAgents(cwd?: string): AgentDefinition[];
185
+ /** Find one agent by id across builtins + custom files. */
186
+ export declare function findAgent(id: string, cwd?: string): AgentDefinition | undefined;
162
187
  /**
163
188
  * Build a `subtask.progress` note for one finished tool call.
164
189
  * Pure — unit-tested directly (see agent-tools.test.ts).
@@ -182,6 +207,7 @@ export declare class AgentOrchestrator {
182
207
  readonly taskManager: TaskManager;
183
208
  readonly workerSpawner: WorkerSpawner;
184
209
  readonly isTui: boolean;
210
+ private readonly customCwd;
185
211
  /** Per-task spawn metadata: capability drops + worktree placement. */
186
212
  private readonly taskMeta;
187
213
  /** Finished summaries not yet drained via `drainCompletions`. */
@@ -19,6 +19,7 @@ import { TaskManager } from './task-manager.js';
19
19
  import { WorkerSpawner } from './worker-spawner.js';
20
20
  import { resolveCapabilities, DEFAULT_WRITE_TOOLS, DEFAULT_SPAWN_TOOLS, DEFAULT_DENIED_TOOLS, } from './capabilities.js';
21
21
  import { forkChild, workerEntryPath } from './child-worker.js';
22
+ import { loadCustomAgents } from './custom-agents.js';
22
23
  import { resolveAndFollowSymlinks } from '../policy/path-guard.js';
23
24
  import { ensureGitRepo, createWorktree, mergeWorktree, removeWorktree, deleteBranch, } from './worktree-manager.js';
24
25
  /** Concurrency budgets enforced in `spawnAgent` (CONCURRENCY_LIMIT on exceed). */
@@ -70,6 +71,39 @@ export const BUILTIN_AGENTS = [
70
71
  allowedTools: ['read_file', 'list_directory', 'glob', 'grep', 'search_files', 'repo_map', 'recent_files'],
71
72
  },
72
73
  ];
74
+ /**
75
+ * Layer specialist instructions into a child's system prompt (not the user
76
+ * task). Pure — unit-tested directly. No-op when the def has no prompt.
77
+ */
78
+ export function layerSpecialistPrompt(base, def) {
79
+ if (!def.prompt)
80
+ return base;
81
+ return (ctx) => {
82
+ const r = base(ctx);
83
+ const block = `\n\n<specialist id="${def.id}">\n${def.prompt}\n</specialist>`;
84
+ return typeof r === 'string' ? r + block : { ...r, system: r.system + block };
85
+ };
86
+ }
87
+ /**
88
+ * All agents: builtins plus custom `.klyro/agents/*.md` definitions.
89
+ * Custom ids win on clash (including overriding a builtin) — the override
90
+ * is surfaced via `source`. No instance needed; used by CLI + spawn paths.
91
+ */
92
+ export function listAllAgents(cwd) {
93
+ const byId = new Map();
94
+ for (const d of BUILTIN_AGENTS)
95
+ byId.set(d.id, { ...d, source: 'builtin' });
96
+ try {
97
+ for (const d of loadCustomAgents(cwd ?? process.cwd()))
98
+ byId.set(d.id, d);
99
+ }
100
+ catch { /* custom agents are best-effort */ }
101
+ return [...byId.values()];
102
+ }
103
+ /** Find one agent by id across builtins + custom files. */
104
+ export function findAgent(id, cwd) {
105
+ return listAllAgents(cwd).find((a) => a.id === id);
106
+ }
73
107
  /** Map a runtime `RunResult.status` to a task status. */
74
108
  function mapResultStatus(status) {
75
109
  switch (status) {
@@ -132,6 +166,7 @@ export class AgentOrchestrator {
132
166
  taskManager;
133
167
  workerSpawner;
134
168
  isTui;
169
+ customCwd;
135
170
  /** Per-task spawn metadata: capability drops + worktree placement. */
136
171
  taskMeta = new Map();
137
172
  /** Finished summaries not yet drained via `drainCompletions`. */
@@ -142,12 +177,13 @@ export class AgentOrchestrator {
142
177
  this.taskManager = opts.taskManager ?? new TaskManager({ sessionId: opts.sessionId });
143
178
  this.workerSpawner = opts.workerSpawner ?? new WorkerSpawner();
144
179
  this.isTui = opts.isTui ?? false;
180
+ this.customCwd = opts.cwd;
145
181
  }
146
182
  listAgents() {
147
- return [...BUILTIN_AGENTS];
183
+ return listAllAgents(this.customCwd);
148
184
  }
149
185
  getAgent(id) {
150
- return BUILTIN_AGENTS.find((a) => a.id === id);
186
+ return listAllAgents(this.customCwd).find((a) => a.id === id);
151
187
  }
152
188
  /** Build the bridge the parent's runtime hands to tools. */
153
189
  bridgeFor(parent) {
@@ -345,7 +381,8 @@ export class AgentOrchestrator {
345
381
  }
346
382
  this.taskMeta.set(record.id, worktree ? { def, dropped: resolved.dropped, worktree, repoCwd } : { def, dropped: resolved.dropped });
347
383
  const childRegistry = new ScopedRegistry(this.deps.registry, resolved.allowed);
348
- const childDeps = { ...this.deps, registry: childRegistry };
384
+ const childPromptFn = layerSpecialistPrompt(this.deps.systemPrompt, def);
385
+ const childDeps = { ...this.deps, registry: childRegistry, systemPrompt: childPromptFn };
349
386
  const childRef = {
350
387
  taskId: record.id,
351
388
  ...(parent.taskId !== undefined ? { parentTaskId: parent.taskId } : {}),
@@ -405,7 +442,7 @@ export class AgentOrchestrator {
405
442
  let childOutcome;
406
443
  try {
407
444
  if (useProcessIsolation) {
408
- const sysPrompt = resolveSystemPrompt(this.deps.systemPrompt, { cwd: childCwd });
445
+ const sysPrompt = resolveSystemPrompt(childPromptFn, { cwd: childCwd });
409
446
  // Splice the volatile telemetry suffix into the stable prefix so the
410
447
  // child's provider sees one system string. Telemetry is best-effort
411
448
  // inside the child (it re-emits); the goal here is parity, not
@@ -85,6 +85,11 @@ export interface RunOptions {
85
85
  temperature?: number;
86
86
  signal?: AbortSignal;
87
87
  nonInteractive: boolean;
88
+ /**
89
+ * Bare mode: skip all hooks (load + sessionStart/stop). The caller is
90
+ * responsible for skipping MCP/persistence/context (see runOnce `bare`).
91
+ */
92
+ bare?: boolean;
88
93
  /**
89
94
  * Optional pre-existing transcript to seed the conversation. When set,
90
95
  * the runtime skips the initial `[{role:'user', content:[text(task)]}]`
@@ -126,6 +131,16 @@ export interface RunOptions {
126
131
  store?: import('../persistence/store.js').SessionStore;
127
132
  sessionId?: string;
128
133
  };
134
+ /**
135
+ * Level 10 — tamper-evident audit. When an AuditLog is provided, the
136
+ * runtime writes policy decisions and tool completions into the chained
137
+ * audit stream (complements, does not replace, persistence). Defaults off
138
+ * so callers opt in; `klyro` CLI enables it when a sessions dir exists.
139
+ */
140
+ audit?: {
141
+ log?: import('../persistence/audit.js').AuditLog;
142
+ sessionId?: string;
143
+ };
129
144
  /**
130
145
  * Orchestration context (P0). Present for any agent that is itself managed
131
146
  * by an AgentOrchestrator — so a child knows who its parent is, how deep the