@cruxy/cli 0.19.0 → 0.20.0
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/dist/cli/commands/run.js +12 -1
- package/dist/cli/session-factory.js +12 -0
- package/dist/config/schema.d.ts +127 -14
- package/dist/config/schema.js +39 -0
- package/dist/errors/constructors.d.ts +23 -0
- package/dist/errors/constructors.js +67 -0
- package/dist/errors/types.d.ts +10 -0
- package/dist/errors/types.js +17 -0
- package/dist/lsp/client.d.ts +25 -0
- package/dist/lsp/client.js +43 -0
- package/dist/lsp/index.d.ts +8 -0
- package/dist/lsp/index.js +8 -0
- package/dist/lsp/pool.d.ts +48 -0
- package/dist/lsp/pool.js +132 -0
- package/dist/lsp/registry.d.ts +38 -0
- package/dist/lsp/registry.js +133 -0
- package/dist/lsp/server.d.ts +48 -0
- package/dist/lsp/server.js +264 -0
- package/dist/lsp/service.d.ts +44 -0
- package/dist/lsp/service.js +76 -0
- package/dist/lsp/tools/common.d.ts +23 -0
- package/dist/lsp/tools/common.js +75 -0
- package/dist/lsp/tools/find-definition.d.ts +23 -0
- package/dist/lsp/tools/find-definition.js +41 -0
- package/dist/lsp/tools/find-references.d.ts +23 -0
- package/dist/lsp/tools/find-references.js +41 -0
- package/dist/lsp/tools/get-diagnostics.d.ts +17 -0
- package/dist/lsp/tools/get-diagnostics.js +43 -0
- package/dist/lsp/tools/hover.d.ts +23 -0
- package/dist/lsp/tools/hover.js +38 -0
- package/dist/lsp/tools/index.d.ts +4 -0
- package/dist/lsp/tools/index.js +4 -0
- package/dist/lsp/transport.d.ts +48 -0
- package/dist/lsp/transport.js +264 -0
- package/dist/lsp/types.d.ts +107 -0
- package/dist/lsp/types.js +1 -0
- package/dist/tools/file/grep-files.d.ts +2 -2
- package/package.json +1 -1
package/dist/cli/commands/run.js
CHANGED
|
@@ -11,6 +11,7 @@ import { buildHooksService } from "../../hooks/index.js";
|
|
|
11
11
|
import { runInteractive } from "../repl.js";
|
|
12
12
|
import { buildAgentSession } from "../session-factory.js";
|
|
13
13
|
import { apiKeyEnvVar, maybeRunOnboarding } from "../onboard.js";
|
|
14
|
+
import { resetLspServices } from "../../lsp/index.js";
|
|
14
15
|
export function runCommand() {
|
|
15
16
|
return new Command("run")
|
|
16
17
|
.description("run a task once, or start an interactive session")
|
|
@@ -92,7 +93,14 @@ export function runCommand() {
|
|
|
92
93
|
});
|
|
93
94
|
const session = buildAgentSession(config, apiKey, process.cwd(), Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox, hooksService.runner);
|
|
94
95
|
if (interactive) {
|
|
95
|
-
|
|
96
|
+
try {
|
|
97
|
+
await runInteractive(session, undefined, renderer, checkpoints, hooksService.commands);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
// LSP (C.12): gracefully shut down any language servers spawned
|
|
101
|
+
// during the session (the process-exit kill-tree is the fail-safe).
|
|
102
|
+
await resetLspServices();
|
|
103
|
+
}
|
|
96
104
|
return;
|
|
97
105
|
}
|
|
98
106
|
checkpoints?.beginRun(prompt);
|
|
@@ -110,6 +118,9 @@ export function runCommand() {
|
|
|
110
118
|
}
|
|
111
119
|
finally {
|
|
112
120
|
renderer.close();
|
|
121
|
+
// LSP (C.12): gracefully shut down any language servers spawned during
|
|
122
|
+
// the run (the process-exit kill-tree is the fail-safe for a hard kill).
|
|
123
|
+
await resetLspServices();
|
|
113
124
|
}
|
|
114
125
|
// End-of-run usage summary (C.22): honest tokens + per-tier breakdown +
|
|
115
126
|
// cost (only when priced). Printed after the live region is torn down.
|
|
@@ -9,6 +9,7 @@ import { Session, } from "../agent/index.js";
|
|
|
9
9
|
import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
|
|
10
10
|
import { routerForConfig } from "../routing/index.js";
|
|
11
11
|
import { MemoryService, rememberTool } from "../memory/index.js";
|
|
12
|
+
import { findDefinitionTool, findReferencesTool, getDiagnosticsTool, hoverTool, } from "../lsp/index.js";
|
|
12
13
|
import { appendRun } from "../usage/index.js";
|
|
13
14
|
import { SubagentOrchestrator, makeSpawnSubagentTool, } from "../subagent/index.js";
|
|
14
15
|
/**
|
|
@@ -136,6 +137,17 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
136
137
|
logger.warn(`memory: excluded ${e.scope} entry ${e.id} — ${e.message}`);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
140
|
+
// Per-language LSP (C.12): register the four read-only introspection tools
|
|
141
|
+
// only when enabled. They spawn and manage EXTERNAL language-server processes,
|
|
142
|
+
// so — like the sandbox — the feature is opt-in; when off, none is registered
|
|
143
|
+
// and no server ever spawns. Read-only (no approval), so they bypass the U.3
|
|
144
|
+
// gate like search_codebase and are available to subagents and plan proposals.
|
|
145
|
+
if (config.lsp.enabled) {
|
|
146
|
+
execRegistry.register(findDefinitionTool);
|
|
147
|
+
execRegistry.register(findReferencesTool);
|
|
148
|
+
execRegistry.register(getDiagnosticsTool);
|
|
149
|
+
execRegistry.register(hoverTool);
|
|
150
|
+
}
|
|
139
151
|
// One io shared by every prompt in the session (plan approval, the U.3 gate,
|
|
140
152
|
// and any gate inside a subagent), so they all coordinate with the same live
|
|
141
153
|
// region. The full wrapper stack around an ApprovalService is factored here
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -236,8 +236,8 @@ export declare const TestConfigSchema: z.ZodObject<{
|
|
|
236
236
|
captureBytes: number;
|
|
237
237
|
command?: string | undefined;
|
|
238
238
|
}, {
|
|
239
|
-
maxIterations?: number | undefined;
|
|
240
239
|
command?: string | undefined;
|
|
240
|
+
maxIterations?: number | undefined;
|
|
241
241
|
captureBytes?: number | undefined;
|
|
242
242
|
}>;
|
|
243
243
|
/**
|
|
@@ -264,9 +264,9 @@ export declare const SubagentConfigSchema: z.ZodObject<{
|
|
|
264
264
|
maxIterations: number;
|
|
265
265
|
timeoutMs?: number | undefined;
|
|
266
266
|
}, {
|
|
267
|
+
timeoutMs?: number | undefined;
|
|
267
268
|
maxTokens?: number | undefined;
|
|
268
269
|
maxIterations?: number | undefined;
|
|
269
|
-
timeoutMs?: number | undefined;
|
|
270
270
|
}>>;
|
|
271
271
|
}, "strict", z.ZodTypeAny, {
|
|
272
272
|
maxDepth: number;
|
|
@@ -278,9 +278,9 @@ export declare const SubagentConfigSchema: z.ZodObject<{
|
|
|
278
278
|
}, {
|
|
279
279
|
maxDepth?: number | undefined;
|
|
280
280
|
defaultBudget?: {
|
|
281
|
+
timeoutMs?: number | undefined;
|
|
281
282
|
maxTokens?: number | undefined;
|
|
282
283
|
maxIterations?: number | undefined;
|
|
283
|
-
timeoutMs?: number | undefined;
|
|
284
284
|
} | undefined;
|
|
285
285
|
}>;
|
|
286
286
|
/**
|
|
@@ -408,6 +408,59 @@ export declare const MemoryConfigSchema: z.ZodObject<{
|
|
|
408
408
|
maxRecallTokens?: number | undefined;
|
|
409
409
|
}>;
|
|
410
410
|
export type MemoryConfig = z.infer<typeof MemoryConfigSchema>;
|
|
411
|
+
/**
|
|
412
|
+
* Per-language LSP integration (C.12): read-only, symbol-aware tools
|
|
413
|
+
* (find_definition, find_references, get_diagnostics, hover) backed by a
|
|
414
|
+
* managed pool of language servers. OFF by default — like the sandbox and
|
|
415
|
+
* hooks, it spawns and manages EXTERNAL server processes, a real execution
|
|
416
|
+
* surface the user opts into explicitly. When on, a missing server binary is
|
|
417
|
+
* an actionable, coded failure (CRUXY_E_LSP_SERVER_NOT_FOUND), never a silent
|
|
418
|
+
* empty result. Servers spawn lazily per language, are reused for the session,
|
|
419
|
+
* and are shut down cleanly (with a process-exit kill-tree backstop).
|
|
420
|
+
*/
|
|
421
|
+
export declare const LspConfigSchema: z.ZodObject<{
|
|
422
|
+
/** Master switch. When false, the four LSP tools are not registered and no
|
|
423
|
+
* server ever spawns (the feature stays fully inert). */
|
|
424
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
425
|
+
/**
|
|
426
|
+
* Per-language command overrides, keyed by language id (e.g. `typescript`,
|
|
427
|
+
* `python`, `go`, `rust`). The value is the full command line
|
|
428
|
+
* (`"gopls -remote=auto"`); it is split on whitespace. Anything omitted
|
|
429
|
+
* falls back to the built-in default table (see `lsp/registry.ts`).
|
|
430
|
+
*/
|
|
431
|
+
servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
432
|
+
/** Kill and fail (CRUXY_E_LSP_TIMEOUT) if the `initialize` handshake does
|
|
433
|
+
* not complete within this many ms. */
|
|
434
|
+
startupTimeout: z.ZodDefault<z.ZodNumber>;
|
|
435
|
+
/** Fail a single query (CRUXY_E_LSP_TIMEOUT) if the server does not respond
|
|
436
|
+
* within this many ms — the server is kept alive, only the request errors. */
|
|
437
|
+
requestTimeout: z.ZodDefault<z.ZodNumber>;
|
|
438
|
+
/** Maximum concurrently-running language servers; acquiring past the cap
|
|
439
|
+
* evicts the idle-oldest server first. */
|
|
440
|
+
maxServers: z.ZodDefault<z.ZodNumber>;
|
|
441
|
+
/** Shut a server down after it has been idle this many ms. */
|
|
442
|
+
idleTimeout: z.ZodDefault<z.ZodNumber>;
|
|
443
|
+
/** Cap on locations/diagnostics returned by a single tool call; the rest are
|
|
444
|
+
* summarized as an "N more" note (bounded output). */
|
|
445
|
+
maxResults: z.ZodDefault<z.ZodNumber>;
|
|
446
|
+
}, "strict", z.ZodTypeAny, {
|
|
447
|
+
startupTimeout: number;
|
|
448
|
+
requestTimeout: number;
|
|
449
|
+
enabled: boolean;
|
|
450
|
+
servers: Record<string, string>;
|
|
451
|
+
maxServers: number;
|
|
452
|
+
idleTimeout: number;
|
|
453
|
+
maxResults: number;
|
|
454
|
+
}, {
|
|
455
|
+
startupTimeout?: number | undefined;
|
|
456
|
+
requestTimeout?: number | undefined;
|
|
457
|
+
enabled?: boolean | undefined;
|
|
458
|
+
servers?: Record<string, string> | undefined;
|
|
459
|
+
maxServers?: number | undefined;
|
|
460
|
+
idleTimeout?: number | undefined;
|
|
461
|
+
maxResults?: number | undefined;
|
|
462
|
+
}>;
|
|
463
|
+
export type LspConfig = z.infer<typeof LspConfigSchema>;
|
|
411
464
|
/** A per-tier price, in the user's own currency, PER MILLION TOKENS (C.22). */
|
|
412
465
|
export declare const TierPriceSchema: z.ZodObject<{
|
|
413
466
|
/** Price per 1,000,000 input tokens. */
|
|
@@ -547,12 +600,12 @@ export declare const McpServerSchema: z.ZodObject<{
|
|
|
547
600
|
url: z.ZodOptional<z.ZodString>;
|
|
548
601
|
}, "strict", z.ZodTypeAny, {
|
|
549
602
|
command?: string | undefined;
|
|
550
|
-
args?: string[] | undefined;
|
|
551
603
|
url?: string | undefined;
|
|
604
|
+
args?: string[] | undefined;
|
|
552
605
|
}, {
|
|
553
606
|
command?: string | undefined;
|
|
554
|
-
args?: string[] | undefined;
|
|
555
607
|
url?: string | undefined;
|
|
608
|
+
args?: string[] | undefined;
|
|
556
609
|
}>;
|
|
557
610
|
export declare const CruxyConfigSchema: z.ZodObject<{
|
|
558
611
|
model: z.ZodDefault<z.ZodObject<{
|
|
@@ -742,6 +795,48 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
742
795
|
overlapLines?: number | undefined;
|
|
743
796
|
} | undefined;
|
|
744
797
|
}>>;
|
|
798
|
+
lsp: z.ZodDefault<z.ZodObject<{
|
|
799
|
+
/** Master switch. When false, the four LSP tools are not registered and no
|
|
800
|
+
* server ever spawns (the feature stays fully inert). */
|
|
801
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
802
|
+
/**
|
|
803
|
+
* Per-language command overrides, keyed by language id (e.g. `typescript`,
|
|
804
|
+
* `python`, `go`, `rust`). The value is the full command line
|
|
805
|
+
* (`"gopls -remote=auto"`); it is split on whitespace. Anything omitted
|
|
806
|
+
* falls back to the built-in default table (see `lsp/registry.ts`).
|
|
807
|
+
*/
|
|
808
|
+
servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
809
|
+
/** Kill and fail (CRUXY_E_LSP_TIMEOUT) if the `initialize` handshake does
|
|
810
|
+
* not complete within this many ms. */
|
|
811
|
+
startupTimeout: z.ZodDefault<z.ZodNumber>;
|
|
812
|
+
/** Fail a single query (CRUXY_E_LSP_TIMEOUT) if the server does not respond
|
|
813
|
+
* within this many ms — the server is kept alive, only the request errors. */
|
|
814
|
+
requestTimeout: z.ZodDefault<z.ZodNumber>;
|
|
815
|
+
/** Maximum concurrently-running language servers; acquiring past the cap
|
|
816
|
+
* evicts the idle-oldest server first. */
|
|
817
|
+
maxServers: z.ZodDefault<z.ZodNumber>;
|
|
818
|
+
/** Shut a server down after it has been idle this many ms. */
|
|
819
|
+
idleTimeout: z.ZodDefault<z.ZodNumber>;
|
|
820
|
+
/** Cap on locations/diagnostics returned by a single tool call; the rest are
|
|
821
|
+
* summarized as an "N more" note (bounded output). */
|
|
822
|
+
maxResults: z.ZodDefault<z.ZodNumber>;
|
|
823
|
+
}, "strict", z.ZodTypeAny, {
|
|
824
|
+
startupTimeout: number;
|
|
825
|
+
requestTimeout: number;
|
|
826
|
+
enabled: boolean;
|
|
827
|
+
servers: Record<string, string>;
|
|
828
|
+
maxServers: number;
|
|
829
|
+
idleTimeout: number;
|
|
830
|
+
maxResults: number;
|
|
831
|
+
}, {
|
|
832
|
+
startupTimeout?: number | undefined;
|
|
833
|
+
requestTimeout?: number | undefined;
|
|
834
|
+
enabled?: boolean | undefined;
|
|
835
|
+
servers?: Record<string, string> | undefined;
|
|
836
|
+
maxServers?: number | undefined;
|
|
837
|
+
idleTimeout?: number | undefined;
|
|
838
|
+
maxResults?: number | undefined;
|
|
839
|
+
}>>;
|
|
745
840
|
checkpoint: z.ZodDefault<z.ZodObject<{
|
|
746
841
|
/** Auto-checkpoint before a run's first mutation (and enable `cruxy rollback`). */
|
|
747
842
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -773,9 +868,9 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
773
868
|
maxIterations: number;
|
|
774
869
|
timeoutMs?: number | undefined;
|
|
775
870
|
}, {
|
|
871
|
+
timeoutMs?: number | undefined;
|
|
776
872
|
maxTokens?: number | undefined;
|
|
777
873
|
maxIterations?: number | undefined;
|
|
778
|
-
timeoutMs?: number | undefined;
|
|
779
874
|
}>>;
|
|
780
875
|
}, "strict", z.ZodTypeAny, {
|
|
781
876
|
maxDepth: number;
|
|
@@ -787,9 +882,9 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
787
882
|
}, {
|
|
788
883
|
maxDepth?: number | undefined;
|
|
789
884
|
defaultBudget?: {
|
|
885
|
+
timeoutMs?: number | undefined;
|
|
790
886
|
maxTokens?: number | undefined;
|
|
791
887
|
maxIterations?: number | undefined;
|
|
792
|
-
timeoutMs?: number | undefined;
|
|
793
888
|
} | undefined;
|
|
794
889
|
}>>;
|
|
795
890
|
test: z.ZodDefault<z.ZodObject<{
|
|
@@ -804,8 +899,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
804
899
|
captureBytes: number;
|
|
805
900
|
command?: string | undefined;
|
|
806
901
|
}, {
|
|
807
|
-
maxIterations?: number | undefined;
|
|
808
902
|
command?: string | undefined;
|
|
903
|
+
maxIterations?: number | undefined;
|
|
809
904
|
captureBytes?: number | undefined;
|
|
810
905
|
}>>;
|
|
811
906
|
sandbox: z.ZodDefault<z.ZodObject<{
|
|
@@ -1015,12 +1110,12 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1015
1110
|
url: z.ZodOptional<z.ZodString>;
|
|
1016
1111
|
}, "strict", z.ZodTypeAny, {
|
|
1017
1112
|
command?: string | undefined;
|
|
1018
|
-
args?: string[] | undefined;
|
|
1019
1113
|
url?: string | undefined;
|
|
1114
|
+
args?: string[] | undefined;
|
|
1020
1115
|
}, {
|
|
1021
1116
|
command?: string | undefined;
|
|
1022
|
-
args?: string[] | undefined;
|
|
1023
1117
|
url?: string | undefined;
|
|
1118
|
+
args?: string[] | undefined;
|
|
1024
1119
|
}>>>;
|
|
1025
1120
|
logLevel: z.ZodDefault<z.ZodEnum<["debug", "info", "warn", "error", "silent"]>>;
|
|
1026
1121
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -1120,6 +1215,15 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1120
1215
|
overlapLines: number;
|
|
1121
1216
|
};
|
|
1122
1217
|
};
|
|
1218
|
+
lsp: {
|
|
1219
|
+
startupTimeout: number;
|
|
1220
|
+
requestTimeout: number;
|
|
1221
|
+
enabled: boolean;
|
|
1222
|
+
servers: Record<string, string>;
|
|
1223
|
+
maxServers: number;
|
|
1224
|
+
idleTimeout: number;
|
|
1225
|
+
maxResults: number;
|
|
1226
|
+
};
|
|
1123
1227
|
test: {
|
|
1124
1228
|
maxIterations: number;
|
|
1125
1229
|
captureBytes: number;
|
|
@@ -1135,8 +1239,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1135
1239
|
};
|
|
1136
1240
|
mcpServers: Record<string, {
|
|
1137
1241
|
command?: string | undefined;
|
|
1138
|
-
args?: string[] | undefined;
|
|
1139
1242
|
url?: string | undefined;
|
|
1243
|
+
args?: string[] | undefined;
|
|
1140
1244
|
}>;
|
|
1141
1245
|
logLevel: "debug" | "info" | "warn" | "error" | "silent";
|
|
1142
1246
|
}, {
|
|
@@ -1186,9 +1290,9 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1186
1290
|
subagent?: {
|
|
1187
1291
|
maxDepth?: number | undefined;
|
|
1188
1292
|
defaultBudget?: {
|
|
1293
|
+
timeoutMs?: number | undefined;
|
|
1189
1294
|
maxTokens?: number | undefined;
|
|
1190
1295
|
maxIterations?: number | undefined;
|
|
1191
|
-
timeoutMs?: number | undefined;
|
|
1192
1296
|
} | undefined;
|
|
1193
1297
|
} | undefined;
|
|
1194
1298
|
model?: {
|
|
@@ -1236,9 +1340,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1236
1340
|
overlapLines?: number | undefined;
|
|
1237
1341
|
} | undefined;
|
|
1238
1342
|
} | undefined;
|
|
1343
|
+
lsp?: {
|
|
1344
|
+
startupTimeout?: number | undefined;
|
|
1345
|
+
requestTimeout?: number | undefined;
|
|
1346
|
+
enabled?: boolean | undefined;
|
|
1347
|
+
servers?: Record<string, string> | undefined;
|
|
1348
|
+
maxServers?: number | undefined;
|
|
1349
|
+
idleTimeout?: number | undefined;
|
|
1350
|
+
maxResults?: number | undefined;
|
|
1351
|
+
} | undefined;
|
|
1239
1352
|
test?: {
|
|
1240
|
-
maxIterations?: number | undefined;
|
|
1241
1353
|
command?: string | undefined;
|
|
1354
|
+
maxIterations?: number | undefined;
|
|
1242
1355
|
captureBytes?: number | undefined;
|
|
1243
1356
|
} | undefined;
|
|
1244
1357
|
hooks?: {
|
|
@@ -1251,8 +1364,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1251
1364
|
} | undefined;
|
|
1252
1365
|
mcpServers?: Record<string, {
|
|
1253
1366
|
command?: string | undefined;
|
|
1254
|
-
args?: string[] | undefined;
|
|
1255
1367
|
url?: string | undefined;
|
|
1368
|
+
args?: string[] | undefined;
|
|
1256
1369
|
}> | undefined;
|
|
1257
1370
|
logLevel?: "debug" | "info" | "warn" | "error" | "silent" | undefined;
|
|
1258
1371
|
}>;
|
package/dist/config/schema.js
CHANGED
|
@@ -290,6 +290,44 @@ export const MemoryConfigSchema = z
|
|
|
290
290
|
maxRecallTokens: z.number().int().positive().default(1000),
|
|
291
291
|
})
|
|
292
292
|
.strict();
|
|
293
|
+
/**
|
|
294
|
+
* Per-language LSP integration (C.12): read-only, symbol-aware tools
|
|
295
|
+
* (find_definition, find_references, get_diagnostics, hover) backed by a
|
|
296
|
+
* managed pool of language servers. OFF by default — like the sandbox and
|
|
297
|
+
* hooks, it spawns and manages EXTERNAL server processes, a real execution
|
|
298
|
+
* surface the user opts into explicitly. When on, a missing server binary is
|
|
299
|
+
* an actionable, coded failure (CRUXY_E_LSP_SERVER_NOT_FOUND), never a silent
|
|
300
|
+
* empty result. Servers spawn lazily per language, are reused for the session,
|
|
301
|
+
* and are shut down cleanly (with a process-exit kill-tree backstop).
|
|
302
|
+
*/
|
|
303
|
+
export const LspConfigSchema = z
|
|
304
|
+
.object({
|
|
305
|
+
/** Master switch. When false, the four LSP tools are not registered and no
|
|
306
|
+
* server ever spawns (the feature stays fully inert). */
|
|
307
|
+
enabled: z.boolean().default(false),
|
|
308
|
+
/**
|
|
309
|
+
* Per-language command overrides, keyed by language id (e.g. `typescript`,
|
|
310
|
+
* `python`, `go`, `rust`). The value is the full command line
|
|
311
|
+
* (`"gopls -remote=auto"`); it is split on whitespace. Anything omitted
|
|
312
|
+
* falls back to the built-in default table (see `lsp/registry.ts`).
|
|
313
|
+
*/
|
|
314
|
+
servers: z.record(z.string(), z.string().min(1)).default({}),
|
|
315
|
+
/** Kill and fail (CRUXY_E_LSP_TIMEOUT) if the `initialize` handshake does
|
|
316
|
+
* not complete within this many ms. */
|
|
317
|
+
startupTimeout: z.number().int().positive().default(15000),
|
|
318
|
+
/** Fail a single query (CRUXY_E_LSP_TIMEOUT) if the server does not respond
|
|
319
|
+
* within this many ms — the server is kept alive, only the request errors. */
|
|
320
|
+
requestTimeout: z.number().int().positive().default(10000),
|
|
321
|
+
/** Maximum concurrently-running language servers; acquiring past the cap
|
|
322
|
+
* evicts the idle-oldest server first. */
|
|
323
|
+
maxServers: z.number().int().positive().default(4),
|
|
324
|
+
/** Shut a server down after it has been idle this many ms. */
|
|
325
|
+
idleTimeout: z.number().int().positive().default(300000),
|
|
326
|
+
/** Cap on locations/diagnostics returned by a single tool call; the rest are
|
|
327
|
+
* summarized as an "N more" note (bounded output). */
|
|
328
|
+
maxResults: z.number().int().positive().default(100),
|
|
329
|
+
})
|
|
330
|
+
.strict();
|
|
293
331
|
/** A per-tier price, in the user's own currency, PER MILLION TOKENS (C.22). */
|
|
294
332
|
export const TierPriceSchema = z
|
|
295
333
|
.object({
|
|
@@ -346,6 +384,7 @@ export const CruxyConfigSchema = z
|
|
|
346
384
|
context: ContextConfigSchema.default({}),
|
|
347
385
|
approval: ApprovalConfigSchema.default({}),
|
|
348
386
|
index: IndexConfigSchema.default({}),
|
|
387
|
+
lsp: LspConfigSchema.default({}),
|
|
349
388
|
checkpoint: CheckpointConfigSchema.default({}),
|
|
350
389
|
subagent: SubagentConfigSchema.default({}),
|
|
351
390
|
test: TestConfigSchema.default({}),
|
|
@@ -157,6 +157,29 @@ export declare function memoryInvalid(detail: string): CruxyError;
|
|
|
157
157
|
* is your own local usage history, so deleting it loses nothing but history.
|
|
158
158
|
*/
|
|
159
159
|
export declare function usageRead(path: string, reason: string, underlying?: unknown): CruxyError;
|
|
160
|
+
/**
|
|
161
|
+
* No language server is available for a query: either no server is configured
|
|
162
|
+
* for the file's language, or the configured/default binary is not installed.
|
|
163
|
+
* THE CORE HONESTY RULE for C.12: this is a coded, actionable failure — it must
|
|
164
|
+
* NEVER collapse into an empty result, because "no server" would then read as
|
|
165
|
+
* "no references found". `installHint` carries the concrete next step.
|
|
166
|
+
*/
|
|
167
|
+
export declare function lspServerNotFound(language: string, reason: "no-spec" | "binary-missing", detail?: {
|
|
168
|
+
command?: string;
|
|
169
|
+
installHint?: string;
|
|
170
|
+
}): CruxyError;
|
|
171
|
+
/**
|
|
172
|
+
* A language server's `initialize` handshake or a single request exceeded its
|
|
173
|
+
* timeout. The process is killed (startup) or the request rejected (per-request)
|
|
174
|
+
* — cruxy never hangs waiting on an unresponsive server.
|
|
175
|
+
*/
|
|
176
|
+
export declare function lspTimeout(language: string, phase: "startup" | "request", timeoutMs: number): CruxyError;
|
|
177
|
+
/**
|
|
178
|
+
* A language server crashed (its process exited unexpectedly) and could not be
|
|
179
|
+
* recovered — the single automatic restart also died. Distinct from a server
|
|
180
|
+
* that answered with zero results (that is an ordinary, non-error outcome).
|
|
181
|
+
*/
|
|
182
|
+
export declare function lspCrashed(language: string, detail?: string): CruxyError;
|
|
160
183
|
export declare function internal(underlying?: unknown): CruxyError;
|
|
161
184
|
/**
|
|
162
185
|
* Map a known provider/transport error (from `@cruxy/sdk`) to a typed
|
|
@@ -666,6 +666,73 @@ export function usageRead(path, reason, underlying) {
|
|
|
666
666
|
meta: { path },
|
|
667
667
|
});
|
|
668
668
|
}
|
|
669
|
+
// ── per-language LSP (exit 15) — C.12 ─────────────────────────────────────────
|
|
670
|
+
/**
|
|
671
|
+
* No language server is available for a query: either no server is configured
|
|
672
|
+
* for the file's language, or the configured/default binary is not installed.
|
|
673
|
+
* THE CORE HONESTY RULE for C.12: this is a coded, actionable failure — it must
|
|
674
|
+
* NEVER collapse into an empty result, because "no server" would then read as
|
|
675
|
+
* "no references found". `installHint` carries the concrete next step.
|
|
676
|
+
*/
|
|
677
|
+
export function lspServerNotFound(language, reason, detail) {
|
|
678
|
+
const noSpec = reason === "no-spec";
|
|
679
|
+
return new CruxyError({
|
|
680
|
+
code: ErrorCode.LspServerNotFound,
|
|
681
|
+
title: noSpec
|
|
682
|
+
? `no language server configured for "${language}"`
|
|
683
|
+
: `the "${language}" language server (${detail?.command ?? "?"}) is not installed`,
|
|
684
|
+
cause: noSpec
|
|
685
|
+
? "this file's language has no entry in the default server table or `lsp.servers`"
|
|
686
|
+
: `\`${detail?.command ?? language}\` was not found on PATH`,
|
|
687
|
+
nextSteps: noSpec
|
|
688
|
+
? [
|
|
689
|
+
`set a server command, e.g. \`cruxy config set lsp.servers.${language} "<command> --stdio"\``,
|
|
690
|
+
]
|
|
691
|
+
: [
|
|
692
|
+
detail?.installHint ?? `install the ${language} language server`,
|
|
693
|
+
`or point cruxy at an installed one: \`cruxy config set lsp.servers.${language} "<command>"\``,
|
|
694
|
+
],
|
|
695
|
+
meta: { language, reason, command: detail?.command },
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* A language server's `initialize` handshake or a single request exceeded its
|
|
700
|
+
* timeout. The process is killed (startup) or the request rejected (per-request)
|
|
701
|
+
* — cruxy never hangs waiting on an unresponsive server.
|
|
702
|
+
*/
|
|
703
|
+
export function lspTimeout(language, phase, timeoutMs) {
|
|
704
|
+
return new CruxyError({
|
|
705
|
+
code: ErrorCode.LspTimeout,
|
|
706
|
+
title: phase === "startup"
|
|
707
|
+
? `the "${language}" language server did not start within ${timeoutMs}ms`
|
|
708
|
+
: `the "${language}" language server did not respond within ${timeoutMs}ms`,
|
|
709
|
+
cause: phase === "startup"
|
|
710
|
+
? "the initialize handshake timed out; the process was killed"
|
|
711
|
+
: "the request timed out; the server was left running for later calls",
|
|
712
|
+
nextSteps: [
|
|
713
|
+
`raise the bound if the server is just slow to start: \`cruxy config set lsp.${phase === "startup" ? "startupTimeout" : "requestTimeout"} ${timeoutMs * 2}\``,
|
|
714
|
+
"re-run with --verbose to see the server's stderr",
|
|
715
|
+
],
|
|
716
|
+
meta: { language, phase, timeoutMs },
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* A language server crashed (its process exited unexpectedly) and could not be
|
|
721
|
+
* recovered — the single automatic restart also died. Distinct from a server
|
|
722
|
+
* that answered with zero results (that is an ordinary, non-error outcome).
|
|
723
|
+
*/
|
|
724
|
+
export function lspCrashed(language, detail) {
|
|
725
|
+
return new CruxyError({
|
|
726
|
+
code: ErrorCode.LspCrashed,
|
|
727
|
+
title: `the "${language}" language server crashed`,
|
|
728
|
+
cause: detail ?? "the server process exited unexpectedly and did not recover",
|
|
729
|
+
nextSteps: [
|
|
730
|
+
"re-run with --verbose to see the server's stderr",
|
|
731
|
+
`verify the server runs standalone (the command in \`lsp.servers.${language}\` or the default)`,
|
|
732
|
+
],
|
|
733
|
+
meta: { language },
|
|
734
|
+
});
|
|
735
|
+
}
|
|
669
736
|
// ── internal (exit 1) ─────────────────────────────────────────────────────────
|
|
670
737
|
export function internal(underlying) {
|
|
671
738
|
return new CruxyError({
|
package/dist/errors/types.d.ts
CHANGED
|
@@ -83,6 +83,16 @@ export declare const ErrorCode: {
|
|
|
83
83
|
/** The local usage store is corrupt/unreadable — the read is SKIPPED and this
|
|
84
84
|
* is surfaced; never fatal to a run (usage display is best-effort). */
|
|
85
85
|
readonly UsageRead: "CRUXY_E_USAGE_READ";
|
|
86
|
+
/** A language server binary is not installed / not on PATH (or no server is
|
|
87
|
+
* configured for the file's language). Actionable, NEVER a silent empty
|
|
88
|
+
* result — "no server" must not read as "no references found". */
|
|
89
|
+
readonly LspServerNotFound: "CRUXY_E_LSP_SERVER_NOT_FOUND";
|
|
90
|
+
/** A server's `initialize` handshake or a single request exceeded its timeout
|
|
91
|
+
* — the process was killed / the request errored, never left to hang. */
|
|
92
|
+
readonly LspTimeout: "CRUXY_E_LSP_TIMEOUT";
|
|
93
|
+
/** A language server crashed (and, where applicable, a single restart also
|
|
94
|
+
* failed). Distinct from "server returned no results". */
|
|
95
|
+
readonly LspCrashed: "CRUXY_E_LSP_CRASHED";
|
|
86
96
|
};
|
|
87
97
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
88
98
|
/** The process exit code for an error code (defaults to 1 for safety). */
|
package/dist/errors/types.js
CHANGED
|
@@ -99,6 +99,17 @@ export const ErrorCode = {
|
|
|
99
99
|
/** The local usage store is corrupt/unreadable — the read is SKIPPED and this
|
|
100
100
|
* is surfaced; never fatal to a run (usage display is best-effort). */
|
|
101
101
|
UsageRead: "CRUXY_E_USAGE_READ",
|
|
102
|
+
// per-language LSP (exit 15) — C.12
|
|
103
|
+
/** A language server binary is not installed / not on PATH (or no server is
|
|
104
|
+
* configured for the file's language). Actionable, NEVER a silent empty
|
|
105
|
+
* result — "no server" must not read as "no references found". */
|
|
106
|
+
LspServerNotFound: "CRUXY_E_LSP_SERVER_NOT_FOUND",
|
|
107
|
+
/** A server's `initialize` handshake or a single request exceeded its timeout
|
|
108
|
+
* — the process was killed / the request errored, never left to hang. */
|
|
109
|
+
LspTimeout: "CRUXY_E_LSP_TIMEOUT",
|
|
110
|
+
/** A language server crashed (and, where applicable, a single restart also
|
|
111
|
+
* failed). Distinct from "server returned no results". */
|
|
112
|
+
LspCrashed: "CRUXY_E_LSP_CRASHED",
|
|
102
113
|
};
|
|
103
114
|
/**
|
|
104
115
|
* Category exit codes. Distinct per category so a caller (CI, a script) can
|
|
@@ -170,6 +181,12 @@ const EXIT_CODES = {
|
|
|
170
181
|
// fix (delete the file); it shares the usage exit code and is never fatal to a
|
|
171
182
|
// run — the aggregation just skips it.
|
|
172
183
|
[ErrorCode.UsageRead]: 2,
|
|
184
|
+
// Per-language LSP (C.12). A missing server / timeout / crash surfaces inside
|
|
185
|
+
// a tool result (the agent reads and reroutes) and only exits the process if
|
|
186
|
+
// thrown directly; grouped for a greppable exit code.
|
|
187
|
+
[ErrorCode.LspServerNotFound]: 15,
|
|
188
|
+
[ErrorCode.LspTimeout]: 15,
|
|
189
|
+
[ErrorCode.LspCrashed]: 15,
|
|
173
190
|
};
|
|
174
191
|
/** The process exit code for an error code (defaults to 1 for safety). */
|
|
175
192
|
export function exitCodeFor(code) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LspPool } from "./pool.js";
|
|
2
|
+
import type { LspDiagnostic, LspHover, LspLocation } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Typed, normalized LSP queries over a {@link LspPool} (C.12). Its only real
|
|
5
|
+
* job beyond delegating is the availability discipline: it resolves a file's
|
|
6
|
+
* language and acquires a server, letting the pool's coded errors (no server /
|
|
7
|
+
* timeout / crash) propagate — while a server's genuine `[]`/`null` answer flows
|
|
8
|
+
* back untouched. That keeps "couldn't ask" and "asked, nothing found" distinct
|
|
9
|
+
* all the way up to the tool boundary. Callers pass ABSOLUTE, in-root paths
|
|
10
|
+
* (validated by the tool via `resolveInRoot`); the server relativizes results.
|
|
11
|
+
*/
|
|
12
|
+
export declare class LspClient {
|
|
13
|
+
private readonly pool;
|
|
14
|
+
constructor(pool: LspPool);
|
|
15
|
+
definition(file: string, line: number, col: number): Promise<LspLocation[]>;
|
|
16
|
+
references(file: string, line: number, col: number): Promise<LspLocation[]>;
|
|
17
|
+
hover(file: string, line: number, col: number): Promise<LspHover | null>;
|
|
18
|
+
diagnostics(file: string): Promise<LspDiagnostic[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the file's language and hand back a live server. An unmapped
|
|
21
|
+
* extension is a coded, actionable failure (`no-spec`) — NOT an empty result,
|
|
22
|
+
* so "unknown language" can never read as "no references found".
|
|
23
|
+
*/
|
|
24
|
+
private serverFor;
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { lspServerNotFound } from "../errors/index.js";
|
|
3
|
+
import { languageForFile } from "./registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* Typed, normalized LSP queries over a {@link LspPool} (C.12). Its only real
|
|
6
|
+
* job beyond delegating is the availability discipline: it resolves a file's
|
|
7
|
+
* language and acquires a server, letting the pool's coded errors (no server /
|
|
8
|
+
* timeout / crash) propagate — while a server's genuine `[]`/`null` answer flows
|
|
9
|
+
* back untouched. That keeps "couldn't ask" and "asked, nothing found" distinct
|
|
10
|
+
* all the way up to the tool boundary. Callers pass ABSOLUTE, in-root paths
|
|
11
|
+
* (validated by the tool via `resolveInRoot`); the server relativizes results.
|
|
12
|
+
*/
|
|
13
|
+
export class LspClient {
|
|
14
|
+
pool;
|
|
15
|
+
constructor(pool) {
|
|
16
|
+
this.pool = pool;
|
|
17
|
+
}
|
|
18
|
+
async definition(file, line, col) {
|
|
19
|
+
return (await this.serverFor(file)).definition(file, line, col);
|
|
20
|
+
}
|
|
21
|
+
async references(file, line, col) {
|
|
22
|
+
return (await this.serverFor(file)).references(file, line, col);
|
|
23
|
+
}
|
|
24
|
+
async hover(file, line, col) {
|
|
25
|
+
return (await this.serverFor(file)).hover(file, line, col);
|
|
26
|
+
}
|
|
27
|
+
async diagnostics(file) {
|
|
28
|
+
return (await this.serverFor(file)).diagnostics(file);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the file's language and hand back a live server. An unmapped
|
|
32
|
+
* extension is a coded, actionable failure (`no-spec`) — NOT an empty result,
|
|
33
|
+
* so "unknown language" can never read as "no references found".
|
|
34
|
+
*/
|
|
35
|
+
serverFor(file) {
|
|
36
|
+
const language = languageForFile(file);
|
|
37
|
+
if (!language) {
|
|
38
|
+
const ext = path.extname(file) || path.basename(file);
|
|
39
|
+
throw lspServerNotFound(ext, "no-spec");
|
|
40
|
+
}
|
|
41
|
+
return this.pool.acquire(language);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { DEFAULT_SPECS, EXT_TO_LANGUAGE, LspRegistry, binaryOnPath, languageForFile, } from "./registry.js";
|
|
3
|
+
export { StdioTransport, TransportTimeoutError, killTree, } from "./transport.js";
|
|
4
|
+
export { Server, type ServerTimeouts } from "./server.js";
|
|
5
|
+
export { LspPool, type PoolOptions, type PoolDeps } from "./pool.js";
|
|
6
|
+
export { LspClient } from "./client.js";
|
|
7
|
+
export { getLspService, resetLspServices, type LspService, type LspServiceDeps, } from "./service.js";
|
|
8
|
+
export { findDefinitionTool, findReferencesTool, getDiagnosticsTool, hoverTool, } from "./tools/index.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { DEFAULT_SPECS, EXT_TO_LANGUAGE, LspRegistry, binaryOnPath, languageForFile, } from "./registry.js";
|
|
3
|
+
export { StdioTransport, TransportTimeoutError, killTree, } from "./transport.js";
|
|
4
|
+
export { Server } from "./server.js";
|
|
5
|
+
export { LspPool } from "./pool.js";
|
|
6
|
+
export { LspClient } from "./client.js";
|
|
7
|
+
export { getLspService, resetLspServices, } from "./service.js";
|
|
8
|
+
export { findDefinitionTool, findReferencesTool, getDiagnosticsTool, hoverTool, } from "./tools/index.js";
|