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
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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(/\/+$/, '')}
|
|
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
|
|
6
|
+
readonly note: "LSP off via KLYRO_LSP=0";
|
|
7
7
|
} | {
|
|
8
|
-
readonly enabled:
|
|
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:
|
|
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: "
|
|
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 —
|
|
3
|
-
*
|
|
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: '
|
|
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 (
|
|
16
|
-
return { enabled: false, diagnostics: [], note: 'LSP off
|
|
17
|
-
|
|
18
|
-
|
|
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: '
|
|
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 () =>
|
|
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
|
package/dist/tools/normalize.js
CHANGED
|
@@ -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
|
|
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?: "
|
|
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
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
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
|
|
18
|
-
return { name: input.name, hits: [], note: 'find_symbol disabled — use grep/repo_map
|
|
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();
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|