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.
Files changed (80) hide show
  1. package/README.md +29 -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 +22 -2
  5. package/dist/agent/orchestrator.js +30 -4
  6. package/dist/agent/runtime.d.ts +5 -0
  7. package/dist/agent/runtime.js +174 -51
  8. package/dist/checkpoints/store.d.ts +9 -0
  9. package/dist/checkpoints/store.js +20 -0
  10. package/dist/cli/auth.js +11 -3
  11. package/dist/cli/completion.js +63 -10
  12. package/dist/cli/config.d.ts +4 -4
  13. package/dist/cli/doctor.js +13 -0
  14. package/dist/cli/eval.d.ts +15 -1
  15. package/dist/cli/eval.js +34 -2
  16. package/dist/cli/hooks.d.ts +54 -5
  17. package/dist/cli/hooks.js +85 -6
  18. package/dist/cli/init.d.ts +6 -0
  19. package/dist/cli/init.js +60 -0
  20. package/dist/cli/repl.js +261 -34
  21. package/dist/cli/run.d.ts +7 -1
  22. package/dist/cli/run.js +97 -41
  23. package/dist/cli/slash/custom.d.ts +25 -0
  24. package/dist/cli/slash/custom.js +166 -0
  25. package/dist/cli/slash/parser.d.ts +16 -2
  26. package/dist/cli/slash/parser.js +67 -18
  27. package/dist/cli/update.d.ts +8 -4
  28. package/dist/cli/update.js +50 -7
  29. package/dist/context/accounting.d.ts +8 -0
  30. package/dist/context/accounting.js +18 -1
  31. package/dist/context/compaction.d.ts +1 -0
  32. package/dist/context/compaction.js +2 -1
  33. package/dist/context/memory.js +18 -1
  34. package/dist/eval/harness.d.ts +21 -3
  35. package/dist/eval/harness.js +31 -3
  36. package/dist/eval/judge.d.ts +32 -0
  37. package/dist/eval/judge.js +63 -0
  38. package/dist/eval/tasks.js +134 -0
  39. package/dist/index.js +225 -132
  40. package/dist/mcp/client.d.ts +15 -0
  41. package/dist/mcp/client.js +42 -2
  42. package/dist/mcp/config.d.ts +10 -1
  43. package/dist/mcp/config.js +64 -1
  44. package/dist/mcp/registry.d.ts +13 -0
  45. package/dist/mcp/registry.js +47 -5
  46. package/dist/mcp/remote.d.ts +29 -0
  47. package/dist/mcp/remote.js +153 -0
  48. package/dist/mcp/serve.d.ts +23 -0
  49. package/dist/mcp/serve.js +111 -0
  50. package/dist/policy/approval.d.ts +15 -1
  51. package/dist/policy/approval.js +8 -0
  52. package/dist/policy/engine.d.ts +11 -1
  53. package/dist/policy/engine.js +14 -1
  54. package/dist/policy/secret-redactor.js +4 -1
  55. package/dist/providers/endpoints.d.ts +43 -0
  56. package/dist/providers/endpoints.js +104 -0
  57. package/dist/providers.js +13 -10
  58. package/dist/shared/error-map.d.ts +19 -0
  59. package/dist/shared/error-map.js +58 -0
  60. package/dist/tools/lsp/diagnostics.d.ts +35 -4
  61. package/dist/tools/lsp/diagnostics.js +88 -9
  62. package/dist/tools/normalize.d.ts +3 -0
  63. package/dist/tools/normalize.js +8 -5
  64. package/dist/tools/search/dependencies.d.ts +2 -2
  65. package/dist/tools/shell/background.d.ts +6 -0
  66. package/dist/tools/shell/background.js +17 -0
  67. package/dist/tools/symbols/find-symbol.d.ts +1 -1
  68. package/dist/tools/symbols/find-symbol.js +8 -6
  69. package/dist/tools/types.d.ts +8 -1
  70. package/dist/tui/app.d.ts +2 -0
  71. package/dist/tui/app.js +454 -53
  72. package/dist/tui/app.test.js +66 -3
  73. package/dist/tui/approval.js +53 -1
  74. package/dist/tui/markdown.js +9 -0
  75. package/dist/tui/mouse.d.ts +26 -1
  76. package/dist/tui/mouse.js +104 -6
  77. package/dist/tui/scroll-flow.test.js +3 -1
  78. package/dist/tui/tokens.d.ts +6 -6
  79. package/dist/tui/tokens.js +9 -6
  80. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Single provider registration point.
3
+ *
4
+ * Everything a new provider needs lives in ONE entry here:
5
+ * - default base URL + model (hosted) or probe URL (local)
6
+ * - wire protocol kind (OpenAI-compatible vs Anthropic)
7
+ * - key env vars for resolution
8
+ *
9
+ * Consumers:
10
+ * - `src/providers.ts` local-probe list + hosted defaults derive from this
11
+ * - `src/providers/model-info.ts` pricing stays model-keyed (unchanged)
12
+ * - `registerEndpoint()` allows programmatic/enterprise providers
13
+ */
14
+ export type ProviderKind = 'openai-compatible' | 'anthropic';
15
+ export interface ProviderEndpoint {
16
+ /** Stable id, e.g. 'openai', 'ollama'. Lowercase + dashes. */
17
+ id: string;
18
+ /** Human label for doctor/help output. */
19
+ label: string;
20
+ /** Wire protocol for adapter selection. */
21
+ kind: ProviderKind;
22
+ /** Default base URL (hosted) or probe URL (local). */
23
+ baseURL: string;
24
+ /** Model used when nothing else specifies one. */
25
+ defaultModel: string;
26
+ /** Local servers only: probed in order with `GET <base><probePath>`. */
27
+ probePath?: string;
28
+ /** Env vars tried (in order) for this provider's key. */
29
+ apiKeyEnv?: string[];
30
+ }
31
+ /** Register (or override) a provider endpoint programmatically. */
32
+ export declare function registerEndpoint(def: ProviderEndpoint): void;
33
+ /** All endpoints: builtins with custom overrides applied. */
34
+ export declare function listEndpoints(): ProviderEndpoint[];
35
+ /** Find one endpoint by id (custom wins). */
36
+ export declare function getEndpoint(id: string): ProviderEndpoint | undefined;
37
+ /** Local-probe candidates in order (entries with probePath). */
38
+ export declare function localProbeEndpoints(): ProviderEndpoint[];
39
+ /**
40
+ * Guess the provider id from a base URL (host matching). Returns undefined
41
+ * for unknown hosts — callers treat that as generic OpenAI-compatible.
42
+ */
43
+ export declare function inferProviderId(baseURL: string): string | undefined;
@@ -0,0 +1,104 @@
1
+ const BUILTINS = [
2
+ {
3
+ id: 'openai',
4
+ label: 'OpenAI',
5
+ kind: 'openai-compatible',
6
+ baseURL: 'https://api.openai.com/v1',
7
+ defaultModel: 'gpt-4o-mini',
8
+ apiKeyEnv: ['KLYRO_API_KEY', 'OPENAI_API_KEY'],
9
+ },
10
+ {
11
+ id: 'anthropic',
12
+ label: 'Anthropic',
13
+ kind: 'anthropic',
14
+ baseURL: 'https://api.anthropic.com/v1',
15
+ defaultModel: 'claude-3-5-sonnet-20240620',
16
+ apiKeyEnv: ['KLYRO_API_KEY', 'ANTHROPIC_API_KEY'],
17
+ },
18
+ {
19
+ id: 'ollama',
20
+ label: 'Ollama',
21
+ kind: 'openai-compatible',
22
+ baseURL: 'http://localhost:11434/v1',
23
+ defaultModel: 'llama3.2',
24
+ probePath: '/models',
25
+ },
26
+ {
27
+ id: 'lmstudio',
28
+ label: 'LM Studio',
29
+ kind: 'openai-compatible',
30
+ baseURL: 'http://localhost:1234/v1',
31
+ defaultModel: 'local-model',
32
+ probePath: '/models',
33
+ },
34
+ {
35
+ id: 'vllm',
36
+ label: 'vLLM',
37
+ kind: 'openai-compatible',
38
+ baseURL: 'http://localhost:8000/v1',
39
+ defaultModel: 'meta-llama/Llama-3-8B-Instruct',
40
+ probePath: '/models',
41
+ },
42
+ {
43
+ id: 'llamacpp',
44
+ label: 'llama.cpp',
45
+ kind: 'openai-compatible',
46
+ baseURL: 'http://localhost:8080/v1',
47
+ defaultModel: 'local-model',
48
+ probePath: '/models',
49
+ },
50
+ ];
51
+ const custom = new Map();
52
+ function validateEndpoint(def) {
53
+ if (!/^[a-z0-9-]{1,32}$/.test(def.id))
54
+ throw new Error(`invalid provider id "${def.id}"`);
55
+ if (!def.baseURL || !def.defaultModel)
56
+ throw new Error(`provider "${def.id}" needs baseURL + defaultModel`);
57
+ }
58
+ /** Register (or override) a provider endpoint programmatically. */
59
+ export function registerEndpoint(def) {
60
+ validateEndpoint(def);
61
+ custom.set(def.id, def);
62
+ }
63
+ /** All endpoints: builtins with custom overrides applied. */
64
+ export function listEndpoints() {
65
+ const byId = new Map();
66
+ for (const d of BUILTINS)
67
+ byId.set(d.id, d);
68
+ for (const [id, d] of custom)
69
+ byId.set(id, d);
70
+ return [...byId.values()];
71
+ }
72
+ /** Find one endpoint by id (custom wins). */
73
+ export function getEndpoint(id) {
74
+ return custom.get(id) ?? BUILTINS.find((d) => d.id === id);
75
+ }
76
+ /** Local-probe candidates in order (entries with probePath). */
77
+ export function localProbeEndpoints() {
78
+ return listEndpoints().filter((d) => d.probePath !== undefined);
79
+ }
80
+ /**
81
+ * Guess the provider id from a base URL (host matching). Returns undefined
82
+ * for unknown hosts — callers treat that as generic OpenAI-compatible.
83
+ */
84
+ export function inferProviderId(baseURL) {
85
+ let host = '';
86
+ try {
87
+ host = new URL(baseURL).hostname.toLowerCase();
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ if (host === 'api.openai.com')
93
+ return 'openai';
94
+ if (host === 'api.anthropic.com')
95
+ return 'anthropic';
96
+ for (const d of listEndpoints()) {
97
+ try {
98
+ if (new URL(d.baseURL).hostname.toLowerCase() === host && d.id !== 'openai' && d.id !== 'anthropic')
99
+ return d.id;
100
+ }
101
+ catch { /* ignore */ }
102
+ }
103
+ return undefined;
104
+ }
package/dist/providers.js CHANGED
@@ -12,12 +12,15 @@
12
12
  * 4. Fall back to api.openai.com with a clear error if no key
13
13
  */
14
14
  import { assertSafeBaseURL, normalizeBaseURL } from './chat.js';
15
- const LOCAL_ENDPOINTS = [
16
- { name: 'Ollama', baseURL: 'http://localhost:11434/v1', defaultModel: 'llama3.2' },
17
- { name: 'LM Studio', baseURL: 'http://localhost:1234/v1', defaultModel: 'local-model' },
18
- { name: 'vLLM', baseURL: 'http://localhost:8000/v1', defaultModel: 'meta-llama/Llama-3-8B-Instruct' },
19
- { name: 'llama.cpp', baseURL: 'http://localhost:8080/v1', defaultModel: 'local-model' },
20
- ];
15
+ import { localProbeEndpoints } from './providers/endpoints.js';
16
+ /**
17
+ * Local probe candidates, derived from the single provider registry
18
+ * (`providers/endpoints.ts`) — adding a local server there automatically
19
+ * extends probing here.
20
+ */
21
+ function localProbeList() {
22
+ return localProbeEndpoints().map((d) => ({ name: d.label, baseURL: d.baseURL, defaultModel: d.defaultModel, probePath: d.probePath ?? '/models' }));
23
+ }
21
24
  /**
22
25
  * Resolve which provider to use. Does not throw; returns `null` if nothing
23
26
  * is reachable.
@@ -147,8 +150,8 @@ export async function resolveProvider() {
147
150
  };
148
151
  }
149
152
  // Probe local endpoints
150
- for (const ep of LOCAL_ENDPOINTS) {
151
- if (await probeLocal(ep.baseURL)) {
153
+ for (const ep of localProbeList()) {
154
+ if (await probeLocal(ep.baseURL, ep.probePath)) {
152
155
  return {
153
156
  baseURL: ep.baseURL,
154
157
  apiKey: '',
@@ -160,13 +163,13 @@ export async function resolveProvider() {
160
163
  return null;
161
164
  }
162
165
  /** Hit a cheap endpoint to check if a local server is up. */
163
- async function probeLocal(baseURL) {
166
+ async function probeLocal(baseURL, probePath = '/models') {
164
167
  const ctrl = new AbortController();
165
168
  const t = setTimeout(() => ctrl.abort(), 600);
166
169
  try {
167
170
  // Try the OpenAI-style /models endpoint; some servers (Ollama before load)
168
171
  // answer /, others only /v1/models.
169
- const res = await fetch(`${baseURL.replace(/\/+$/, '')}/models`, {
172
+ const res = await fetch(`${baseURL.replace(/\/+$/, '')}${probePath}`, {
170
173
  method: 'GET',
171
174
  signal: ctrl.signal,
172
175
  });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Single mapper unifying the three error dialects:
3
+ * - ToolErrorCode (tools/normalize.ts — per-tool failures)
4
+ * - FailureClass (verification/classify.ts — verify repair classes)
5
+ * → KlyroErrorCode (shared/errors.ts — harness-wide + exit codes)
6
+ *
7
+ * Type-only imports keep this module cycle-free: it is safe to import
8
+ * from tools/, verification/, and cli/ alike.
9
+ */
10
+ import { EXIT_CODE, type KlyroErrorCode } from './errors.js';
11
+ import type { FailureClass } from '../verification/classify.js';
12
+ export { EXIT_CODE };
13
+ export type { KlyroErrorCode };
14
+ /** Map any tool-layer error code to its harness-wide KlyroErrorCode. */
15
+ export declare function toolErrorToKlyroCode(code: string): KlyroErrorCode;
16
+ /** Map a verification failure class to its harness-wide code. */
17
+ export declare function failureClassToKlyroCode(cls: FailureClass): KlyroErrorCode;
18
+ /** Exit code for any mapped error: single entry point for CLI mapping. */
19
+ export declare function exitCodeFor(code: KlyroErrorCode): number;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Single mapper unifying the three error dialects:
3
+ * - ToolErrorCode (tools/normalize.ts — per-tool failures)
4
+ * - FailureClass (verification/classify.ts — verify repair classes)
5
+ * → KlyroErrorCode (shared/errors.ts — harness-wide + exit codes)
6
+ *
7
+ * Type-only imports keep this module cycle-free: it is safe to import
8
+ * from tools/, verification/, and cli/ alike.
9
+ */
10
+ import { EXIT_CODE } from './errors.js';
11
+ export { EXIT_CODE };
12
+ /** Map any tool-layer error code to its harness-wide KlyroErrorCode. */
13
+ export function toolErrorToKlyroCode(code) {
14
+ switch (code) {
15
+ case 'NOT_FOUND':
16
+ case 'COMMAND_NOT_FOUND':
17
+ case 'MATCH_NOT_FOUND':
18
+ case 'UNKNOWN_TOOL':
19
+ return 'TOOL_NOT_FOUND';
20
+ case 'PERMISSION_DENIED':
21
+ case 'COMMAND_DENIED':
22
+ case 'POLICY_DENIED':
23
+ return 'TOOL_DENIED';
24
+ case 'PATH_ESCAPE':
25
+ return 'PATH_ESCAPE';
26
+ case 'TIMEOUT':
27
+ return 'PROVIDER_TIMEOUT';
28
+ case 'INVALID_INPUT':
29
+ case 'INVALID_PATCH':
30
+ case 'HUNK_MISMATCH':
31
+ case 'MATCH_AMBIGUOUS':
32
+ return 'CONFIG_INVALID';
33
+ case 'ABORTED':
34
+ case 'STALE':
35
+ case 'EXIT_NONZERO':
36
+ case 'IO_ERROR':
37
+ case 'INTERNAL':
38
+ default:
39
+ return 'UNKNOWN';
40
+ }
41
+ }
42
+ /** Map a verification failure class to its harness-wide code. */
43
+ export function failureClassToKlyroCode(cls) {
44
+ switch (cls) {
45
+ case 'introduced':
46
+ return 'VERIFY_FAILED';
47
+ case 'pre_existing':
48
+ case 'flaky':
49
+ // Not the agent's fault — surfaced as advisory, not a hard failure.
50
+ return 'UNKNOWN';
51
+ case 'env':
52
+ return 'CONFIG_INVALID';
53
+ }
54
+ }
55
+ /** Exit code for any mapped error: single entry point for CLI mapping. */
56
+ export function exitCodeFor(code) {
57
+ return EXIT_CODE[code] ?? 1;
58
+ }
@@ -3,10 +3,20 @@ export declare const lspDiagnosticsTool: import("../types.js").Tool<{
3
3
  }, {
4
4
  readonly enabled: false;
5
5
  readonly diagnostics: readonly [];
6
- readonly note: "LSP off — use /lsp to enable";
6
+ readonly note: "LSP off via KLYRO_LSP=0";
7
7
  } | {
8
- readonly enabled: true;
8
+ readonly enabled: false;
9
9
  readonly diagnostics: readonly [];
10
+ readonly note: "no TypeScript compiler found (node_modules/.bin/tsc missing)";
11
+ } | {
12
+ readonly enabled: true;
13
+ readonly diagnostics: {
14
+ file: string;
15
+ line: number;
16
+ col: number;
17
+ code: string;
18
+ message: string;
19
+ }[];
10
20
  readonly note?: undefined;
11
21
  }>;
12
22
  export declare const lspGotoDefinitionTool: import("../types.js").Tool<{
@@ -14,7 +24,28 @@ export declare const lspGotoDefinitionTool: import("../types.js").Tool<{
14
24
  line: number;
15
25
  character?: number | undefined;
16
26
  }, {
17
- readonly enabled: boolean;
27
+ readonly enabled: false;
28
+ readonly location: null;
29
+ readonly note: "LSP off via KLYRO_LSP=0";
30
+ readonly candidates?: undefined;
31
+ } | {
32
+ readonly enabled: true;
33
+ readonly location: null;
34
+ readonly note: `unreadable file: ${string}`;
35
+ readonly candidates?: undefined;
36
+ } | {
37
+ readonly enabled: true;
18
38
  readonly location: null;
19
- readonly note: "stub";
39
+ readonly note: "no identifier under cursor";
40
+ readonly candidates?: undefined;
41
+ } | {
42
+ readonly enabled: true;
43
+ readonly location: {
44
+ file: string;
45
+ line: number;
46
+ kind: string;
47
+ name: string;
48
+ } | null;
49
+ readonly candidates: number;
50
+ readonly note?: undefined;
20
51
  }>;
@@ -1,28 +1,107 @@
1
1
  /**
2
- * 7.5 — LSP bridge stub (off by default, /lsp to enable)
3
- * Provides diagnostics / goto_definition when LS available, otherwise no-op.
2
+ * 7.5 — Language intelligence without an LSP client dependency.
3
+ *
4
+ * `lsp_diagnostics` runs the TypeScript compiler (`tsc --noEmit`) when the
5
+ * project has one and filters the output to the requested path (or returns
6
+ * a capped full-project list). Other languages report "unsupported".
7
+ * `lsp_goto_definition` resolves the identifier under (line, character) to
8
+ * its definition via the regex repo-map symbol index. Set KLYRO_LSP=0 to
9
+ * force both tools off.
4
10
  */
11
+ import { spawnSync } from 'node:child_process';
12
+ import * as fs from 'node:fs/promises';
13
+ import * as path from 'node:path';
5
14
  import { z } from 'zod';
6
15
  import { defineTool } from '../types.js';
7
16
  import { safe } from '../normalize.js';
17
+ import { resolveWithinCwd } from '../../policy/path-guard.js';
18
+ import { buildRepoMap } from '../../context/repo-map.js';
19
+ const OFF_NOTE = 'set KLYRO_LSP=1 to re-enable (default on)';
20
+ function isOff() {
21
+ return process.env.KLYRO_LSP === '0';
22
+ }
8
23
  export const lspDiagnosticsTool = defineTool({
9
24
  name: 'lsp_diagnostics',
10
- description: 'LSP diagnostics (stub — off unless KLYRO_LSP=1)',
25
+ description: 'TypeScript diagnostics for a file (tsc --noEmit, filtered to path)',
11
26
  inputSchema: z.object({ path: z.string().optional() }),
12
27
  permission: 'read',
13
28
  isConcurrencySafe: true,
14
29
  execute: async (input, ctx) => safe(async () => {
15
- if (process.env.KLYRO_LSP !== '1')
16
- return { enabled: false, diagnostics: [], note: 'LSP off — use /lsp to enable' };
17
- // Real would spawn language server; stub returns empty
18
- return { enabled: true, diagnostics: [] };
30
+ if (isOff())
31
+ return { enabled: false, diagnostics: [], note: 'LSP off via KLYRO_LSP=0' };
32
+ const target = input.path ? resolveWithinCwd(ctx.cwd, input.path).resolved : null;
33
+ // Prefer the project's own tsc; fall back to npx tsc on PATH.
34
+ const tscArgs = ['--noEmit', '--pretty', 'false'];
35
+ let out = '';
36
+ try {
37
+ const local = path.join(ctx.cwd, 'node_modules', '.bin', process.platform === 'win32' ? 'tsc.cmd' : 'tsc');
38
+ const res = spawnSync(local, tscArgs, { cwd: ctx.cwd, timeout: 90_000, encoding: 'utf-8', maxBuffer: 4 * 1024 * 1024 });
39
+ out = (res.stdout ?? '') + (res.stderr ?? '');
40
+ }
41
+ catch {
42
+ return { enabled: false, diagnostics: [], note: 'no TypeScript compiler found (node_modules/.bin/tsc missing)' };
43
+ }
44
+ const diags = [];
45
+ for (const ln of out.split('\n')) {
46
+ const m = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s*(.*)$/.exec(ln.trim());
47
+ if (!m)
48
+ continue;
49
+ const file = m[1].replace(/\\/g, '/');
50
+ if (target && path.relative(ctx.cwd, path.resolve(ctx.cwd, file)) !== path.relative(ctx.cwd, target))
51
+ continue;
52
+ diags.push({ file, line: Number(m[2]), col: Number(m[3]), code: m[5], message: m[6].slice(0, 300) });
53
+ if (diags.length >= 50)
54
+ break;
55
+ }
56
+ return { enabled: true, diagnostics: diags };
19
57
  }),
20
58
  });
21
59
  export const lspGotoDefinitionTool = defineTool({
22
60
  name: 'lsp_goto_definition',
23
- description: 'LSP goto_definition (stub)',
61
+ description: 'Go to definition: identifier under (line, character) resolved via the repo symbol index',
24
62
  inputSchema: z.object({ path: z.string().min(1), line: z.number().int().min(1), character: z.number().int().min(0).optional() }),
25
63
  permission: 'read',
26
64
  isConcurrencySafe: true,
27
- execute: async (input) => safe(async () => ({ enabled: process.env.KLYRO_LSP === '1', location: null, note: 'stub' })),
65
+ execute: async (input, ctx) => safe(async () => {
66
+ if (isOff())
67
+ return { enabled: false, location: null, note: 'LSP off via KLYRO_LSP=0' };
68
+ const { resolved } = resolveWithinCwd(ctx.cwd, input.path);
69
+ let content;
70
+ try {
71
+ content = await fs.readFile(resolved, 'utf-8');
72
+ }
73
+ catch {
74
+ return { enabled: true, location: null, note: `unreadable file: ${input.path}` };
75
+ }
76
+ const lines = content.split('\n');
77
+ const lineText = lines[input.line - 1] ?? '';
78
+ const col = Math.min(input.character ?? 0, lineText.length);
79
+ const wordAt = (() => {
80
+ const isWord = (c) => /[A-Za-z0-9_$]/.test(c);
81
+ let s = col;
82
+ let e = col;
83
+ while (s > 0 && isWord(lineText[s - 1]))
84
+ s--;
85
+ while (e < lineText.length && isWord(lineText[e]))
86
+ e++;
87
+ const w = lineText.slice(s, e);
88
+ return w || null;
89
+ })();
90
+ if (!wordAt)
91
+ return { enabled: true, location: null, note: 'no identifier under cursor' };
92
+ const files = await buildRepoMap({ cwd: ctx.cwd, maxFiles: 300 });
93
+ const hits = [];
94
+ for (const f of files) {
95
+ for (const s of f.symbols) {
96
+ if (s.name === wordAt)
97
+ hits.push({ file: f.path, line: s.line, kind: s.kind, name: s.name });
98
+ }
99
+ }
100
+ // Prefer a definition outside the requesting file (import target),
101
+ // else the local one.
102
+ const rel = path.relative(ctx.cwd, resolved);
103
+ const other = hits.filter((h) => h.file !== rel);
104
+ const best = other[0] ?? hits[0] ?? null;
105
+ return { enabled: true, location: best, candidates: hits.length };
106
+ }),
28
107
  });
@@ -4,6 +4,7 @@
4
4
  * errors without each tool hand-coding the shape.
5
5
  */
6
6
  import type { ToolResult } from './types.js';
7
+ import type { KlyroErrorCode } from '../shared/errors.js';
7
8
  /** Standard tool error codes. Tools may add their own. */
8
9
  export declare const TOOL_ERROR_CODES: {
9
10
  readonly NOT_FOUND: "NOT_FOUND";
@@ -23,6 +24,8 @@ export declare function toToolError(err: unknown, fallbackCode?: ToolErrorCode):
23
24
  code: ToolErrorCode;
24
25
  message: string;
25
26
  details?: unknown;
27
+ /** Harness-wide code from the central mapper (shared/error-map.ts). */
28
+ klyroCode: KlyroErrorCode;
26
29
  };
27
30
  /** Convenience: wrap a function in try/catch and return a ToolResult.
28
31
  * If the function returns a ToolResult (already-shaped), it is passed through
@@ -3,6 +3,7 @@
3
3
  * Centralizing this means tools can `throw` and the harness sees structured
4
4
  * errors without each tool hand-coding the shape.
5
5
  */
6
+ import { toolErrorToKlyroCode } from '../shared/error-map.js';
6
7
  /** Standard tool error codes. Tools may add their own. */
7
8
  export const TOOL_ERROR_CODES = {
8
9
  NOT_FOUND: 'NOT_FOUND',
@@ -19,26 +20,28 @@ export const TOOL_ERROR_CODES = {
19
20
  };
20
21
  export function toToolError(err, fallbackCode = TOOL_ERROR_CODES.INTERNAL) {
21
22
  if (err === null || err === undefined) {
22
- return { code: fallbackCode, message: 'Unknown error' };
23
+ return { code: fallbackCode, message: 'Unknown error', klyroCode: toolErrorToKlyroCode(fallbackCode) };
23
24
  }
24
25
  if (err instanceof Error) {
25
26
  // If the error already declares a tool-level `code` (e.g. PathGuardError),
26
27
  // honor it instead of mapping through Node errno.
27
28
  const maybeCode = err.code;
28
29
  if (typeof maybeCode === 'string' && isToolErrorCode(maybeCode)) {
29
- return { code: maybeCode, message: err.message };
30
+ return { code: maybeCode, message: err.message, klyroCode: toolErrorToKlyroCode(maybeCode) };
30
31
  }
31
32
  if (typeof maybeCode === 'string') {
32
33
  // Looks like a Node errno (e.g. 'ENOENT'); map it.
34
+ const code = mapNodeErrorCode(maybeCode);
33
35
  return {
34
- code: mapNodeErrorCode(maybeCode),
36
+ code,
35
37
  message: err.message,
36
38
  details: { syscall: err.syscall, path: err.path },
39
+ klyroCode: toolErrorToKlyroCode(code),
37
40
  };
38
41
  }
39
- return { code: fallbackCode, message: err.message, details: err.stack };
42
+ return { code: fallbackCode, message: err.message, details: err.stack, klyroCode: toolErrorToKlyroCode(fallbackCode) };
40
43
  }
41
- return { code: fallbackCode, message: String(err) };
44
+ return { code: fallbackCode, message: String(err), klyroCode: toolErrorToKlyroCode(fallbackCode) };
42
45
  }
43
46
  const CUSTOM_TOOL_CODES = new Set([
44
47
  'MATCH_NOT_FOUND',
@@ -9,8 +9,8 @@ import { z } from 'zod';
9
9
  declare const InputSchema: z.ZodObject<{
10
10
  cwd: z.ZodOptional<z.ZodString>;
11
11
  ecosystem: z.ZodOptional<z.ZodEnum<{
12
- auto: "auto";
13
12
  npm: "npm";
13
+ auto: "auto";
14
14
  python: "python";
15
15
  go: "go";
16
16
  rust: "rust";
@@ -29,7 +29,7 @@ export interface DependenciesOutput {
29
29
  }
30
30
  export declare const dependenciesTool: import("../types.js").Tool<{
31
31
  cwd?: string | undefined;
32
- ecosystem?: "auto" | "npm" | "python" | "go" | "rust" | undefined;
32
+ ecosystem?: "npm" | "auto" | "python" | "go" | "rust" | undefined;
33
33
  maxResults?: number | undefined;
34
34
  }, {
35
35
  dependencies: never[];
@@ -9,6 +9,12 @@ export declare function startBackground(command: string, cwd: string): string;
9
9
  export declare function startBackgroundArgv(argv: string[], cwd: string): string;
10
10
  export declare function getOutput(id: string, filter?: string): string;
11
11
  export declare function killJob(id: string): void;
12
+ /**
13
+ * Abort cascade: kill every tracked background job. Called from the
14
+ * runtime abort path and the REPL `/cancel` handler so a cancelled run
15
+ * never leaks runaway shells. Returns the killed job ids for audit.
16
+ */
17
+ export declare function killAllJobs(): string[];
12
18
  export declare function listJobs(): Array<{
13
19
  id: string;
14
20
  command: string;
@@ -90,6 +90,23 @@ export function killJob(id) {
90
90
  catch { /* ignore */ }
91
91
  jobs.delete(id);
92
92
  }
93
+ /**
94
+ * Abort cascade: kill every tracked background job. Called from the
95
+ * runtime abort path and the REPL `/cancel` handler so a cancelled run
96
+ * never leaks runaway shells. Returns the killed job ids for audit.
97
+ */
98
+ export function killAllJobs() {
99
+ const killed = [];
100
+ for (const job of [...jobs.values()]) {
101
+ try {
102
+ job.proc.kill('SIGKILL');
103
+ }
104
+ catch { /* ignore */ }
105
+ jobs.delete(job.id);
106
+ killed.push(job.id);
107
+ }
108
+ return killed;
109
+ }
93
110
  export function listJobs() {
94
111
  return [...jobs.values()].map((j) => ({ id: j.id, command: j.command, cwd: j.cwd, running: j.proc.exitCode === null }));
95
112
  }
@@ -4,7 +4,7 @@ export declare const findSymbolTool: import("../types.js").Tool<{
4
4
  }, {
5
5
  readonly name: string;
6
6
  readonly hits: readonly [];
7
- readonly note: "find_symbol disabled — use grep/repo_map (decision 7.4: ripgrep baseline 4.2s vs tree-sitter 5.8s, no gain)";
7
+ readonly note: "find_symbol disabled via KLYRO_SYMBOLS=0 — use grep/repo_map";
8
8
  } | {
9
9
  readonly name: string;
10
10
  readonly hits: {
@@ -1,7 +1,9 @@
1
1
  /**
2
- * 7.4 — find_symbol (optional, eval-gated) — stub using regex repo-map
3
- * Real tree-sitter would be <5s/100k LOC, but ripgrep baseline wins on locate suite, so shipped disabled.
4
- * Enable with KLYRO_SYMBOLS=1 for experiment; decision recorded in docs/decisions/7.4-symbols.md
2
+ * 7.4 — find_symbol via the regex repo-map (no tree-sitter dependency).
3
+ * Previously gated behind KLYRO_SYMBOLS=1; enabled by default now — the
4
+ * regex symbol index is genuinely useful for name lookup even though
5
+ * ripgrep wins on raw locate speed. Set KLYRO_SYMBOLS=0 to force the
6
+ * disabled path (kept for the eval comparison).
5
7
  */
6
8
  import { z } from 'zod';
7
9
  import { defineTool } from '../types.js';
@@ -9,13 +11,13 @@ import { safe } from '../normalize.js';
9
11
  import { buildRepoMap } from '../../context/repo-map.js';
10
12
  export const findSymbolTool = defineTool({
11
13
  name: 'find_symbol',
12
- description: 'Find symbol by name (regex, disabled unless KLYRO_SYMBOLS=1 — ripgrep wins on locate suite)',
14
+ description: 'Find a code symbol (function/class/interface/const) by name across the repo',
13
15
  inputSchema: z.object({ name: z.string().min(1), kind: z.string().optional() }),
14
16
  permission: 'read',
15
17
  isConcurrencySafe: true,
16
18
  execute: async (input, ctx) => safe(async () => {
17
- if (process.env.KLYRO_SYMBOLS !== '1') {
18
- return { name: input.name, hits: [], note: 'find_symbol disabled — use grep/repo_map (decision 7.4: ripgrep baseline 4.2s vs tree-sitter 5.8s, no gain)' };
19
+ if (process.env.KLYRO_SYMBOLS === '0') {
20
+ return { name: input.name, hits: [], note: 'find_symbol disabled via KLYRO_SYMBOLS=0 — use grep/repo_map' };
19
21
  }
20
22
  const files = await buildRepoMap({ cwd: ctx.cwd, maxFiles: 200 });
21
23
  const q = input.name.toLowerCase();
@@ -8,6 +8,7 @@
8
8
  * 4. Normalize errors into { code, message, details? }
9
9
  */
10
10
  import type { z } from 'zod';
11
+ import type { KlyroErrorCode } from '../shared/errors.js';
11
12
  /** Standard tool result — either a typed value or a structured error. */
12
13
  export type ToolResult<T> = {
13
14
  ok: true;
@@ -18,6 +19,7 @@ export type ToolResult<T> = {
18
19
  code: string;
19
20
  message: string;
20
21
  details?: unknown;
22
+ klyroCode?: KlyroErrorCode;
21
23
  };
22
24
  };
23
25
  /**
@@ -78,7 +80,12 @@ export interface Tool<TInput, TOutput> {
78
80
  description: string;
79
81
  /** Zod schema for runtime validation. */
80
82
  inputSchema: z.ZodType<TInput>;
81
- /** Permission class: read | edit | execute | admin */
83
+ /**
84
+ * Permission class: read | edit | execute | admin.
85
+ * Consumed by the runtime → policy path: the runtime passes this into
86
+ * `PolicyEngine.evaluate`, and `execute`/`admin` tools with no explicit
87
+ * allow rule fall through to ask (interactive) / deny (headless).
88
+ */
82
89
  permission?: 'read' | 'edit' | 'execute' | 'admin';
83
90
  /** True if tool is safe to run in parallel with others */
84
91
  isConcurrencySafe?: boolean;
package/dist/tui/app.d.ts CHANGED
@@ -31,6 +31,8 @@ export interface AppProps {
31
31
  updateTool: (idCall: string, patch: ToolResultPatch) => void;
32
32
  appendThinkingDelta: (text: string) => void;
33
33
  clearThinking: () => void;
34
+ pasteText: (text: string) => void;
35
+ setVimMode: (mode: 'insert' | 'normal') => void;
34
36
  }) => void;
35
37
  version?: string;
36
38
  isFullscreen?: boolean;