klyro 1.0.3 → 1.0.5

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 (48) hide show
  1. package/README.md +29 -0
  2. package/dist/agent/runtime.js +11 -2
  3. package/dist/cli/auth.js +11 -3
  4. package/dist/cli/completion.js +63 -10
  5. package/dist/cli/doctor.js +13 -0
  6. package/dist/cli/repl.js +116 -10
  7. package/dist/cli/run.js +14 -0
  8. package/dist/cli/slash/parser.d.ts +7 -1
  9. package/dist/cli/slash/parser.js +37 -10
  10. package/dist/cli/update.d.ts +8 -4
  11. package/dist/cli/update.js +50 -7
  12. package/dist/context/accounting.d.ts +8 -0
  13. package/dist/context/accounting.js +18 -1
  14. package/dist/context/compaction.d.ts +1 -0
  15. package/dist/context/compaction.js +2 -1
  16. package/dist/index.js +78 -8
  17. package/dist/mcp/config.d.ts +7 -0
  18. package/dist/mcp/config.js +45 -0
  19. package/dist/mcp/registry.js +6 -4
  20. package/dist/mcp/serve.d.ts +23 -0
  21. package/dist/mcp/serve.js +111 -0
  22. package/dist/policy/engine.d.ts +11 -1
  23. package/dist/policy/engine.js +14 -1
  24. package/dist/policy/secret-redactor.js +4 -1
  25. package/dist/shared/error-map.d.ts +19 -0
  26. package/dist/shared/error-map.js +58 -0
  27. package/dist/tools/lsp/diagnostics.d.ts +35 -4
  28. package/dist/tools/lsp/diagnostics.js +88 -9
  29. package/dist/tools/normalize.d.ts +3 -0
  30. package/dist/tools/normalize.js +8 -5
  31. package/dist/tools/search/dependencies.d.ts +2 -2
  32. package/dist/tools/shell/background.d.ts +6 -0
  33. package/dist/tools/shell/background.js +17 -0
  34. package/dist/tools/symbols/find-symbol.d.ts +1 -1
  35. package/dist/tools/symbols/find-symbol.js +8 -6
  36. package/dist/tools/types.d.ts +8 -1
  37. package/dist/tui/app.d.ts +2 -0
  38. package/dist/tui/app.js +330 -52
  39. package/dist/tui/app.test.js +45 -6
  40. package/dist/tui/markdown.js +9 -0
  41. package/dist/tui/mouse.d.ts +26 -1
  42. package/dist/tui/mouse.js +104 -6
  43. package/dist/tui/scroll-flow.test.js +4 -2
  44. package/dist/tui/scroll-model.d.ts +3 -1
  45. package/dist/tui/scroll-model.js +6 -5
  46. package/dist/tui/tokens.d.ts +6 -6
  47. package/dist/tui/tokens.js +9 -6
  48. package/package.json +1 -1
@@ -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;