@dsh-stack/lsp 0.0.0-stage → 0.1.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/README.md +40 -2
- package/bin/lsp.mjs +173 -0
- package/lib/index.d.ts +38 -0
- package/lib/index.js +56 -0
- package/lib/settings.d.ts +72 -0
- package/lib/settings.js +69 -0
- package/package.json +57 -4
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lsp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness (`dsh`) plugin: owns the LSP capability on the web profile.
|
|
4
|
+
|
|
5
|
+
The harness ships the LSP service definition (`ctx.lsp`), a generic stdio
|
|
6
|
+
provider (`dsh-lsp-stdio`), and a model-facing `lsp` tool (`dsh-tool-lsp`), but
|
|
7
|
+
composes none of them by default. This plugin mounts the trio and feeds the
|
|
8
|
+
stdio provider the server table from the `lsp` settings section, so the
|
|
9
|
+
agent's `goToDefinition` / `findReferences` / `goToImplementation` / `hover`
|
|
10
|
+
queries work out of the box.
|
|
11
|
+
|
|
12
|
+
## Settings
|
|
13
|
+
|
|
14
|
+
`lsp:` section of `settings.yaml`:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
lsp:
|
|
18
|
+
servers:
|
|
19
|
+
typescript:
|
|
20
|
+
command: typescript-language-server
|
|
21
|
+
extensionToLanguage: { ".ts": typescript, ".tsx": typescriptreact }
|
|
22
|
+
args: [--stdio]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Server entries follow `LspLocalServerConfig` from `@deepseek-ai/dsh-lsp-stdio`.
|
|
26
|
+
Changes apply on the next boot (mounts are boot-time, not hot-reloaded).
|
|
27
|
+
|
|
28
|
+
## CLI
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
dsh lsp list
|
|
32
|
+
dsh lsp servers add <id> <command> [--ext=.ts=typescript] [--args=a b c]
|
|
33
|
+
dsh lsp servers remove <id>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Layout
|
|
37
|
+
|
|
38
|
+
- `src/settings.ts` — the `lsp` settings namespace and server entry schema.
|
|
39
|
+
- `src/index.ts` — plugin: settings section + mounting the LSP trio.
|
|
40
|
+
- `bin/lsp.mjs` — the `dsh lsp` CLI.
|
|
41
|
+
- `check-plugin.mjs` — boot-verify harness (`npm test`).
|
package/bin/lsp.mjs
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// jscpd:ignore-start -- per-package CLI entrypoint boilerplate (arg parsing, help text), duplicated by design across sibling CLI packages
|
|
3
|
+
/**
|
|
4
|
+
* The `dsh lsp` command: the owner surface over the lsp server table. It
|
|
5
|
+
* reads/writes the `lsp` section of `settings.yaml` under the same agent
|
|
6
|
+
* home the harness boots (DSH_HOME); the plugin mounts those servers on the
|
|
7
|
+
* next boot.
|
|
8
|
+
*
|
|
9
|
+
* Verbs:
|
|
10
|
+
* list — installed LSP servers (id, command, extensions)
|
|
11
|
+
* servers add <id> <command> — add a stdio server (--ext=.ts=typescript, --args=...)
|
|
12
|
+
* servers remove <id> — delete a server entry
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
const home = resolve(process.env.DSH_HOME ?? join(homedir(), ".agents"));
|
|
20
|
+
const settingsPath = join(home, "settings.yaml");
|
|
21
|
+
|
|
22
|
+
/** readSection implementation. */
|
|
23
|
+
function* readSection(text, section) {
|
|
24
|
+
let inSection = false;
|
|
25
|
+
for (const line of text.split("\n")) {
|
|
26
|
+
if (new RegExp(`^${section}\\s*:`).test(line)) {
|
|
27
|
+
inSection = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (inSection && !/^\s/.test(line)) break;
|
|
31
|
+
if (!inSection) continue;
|
|
32
|
+
const match = line.match(/^\s+([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
33
|
+
if (match) {
|
|
34
|
+
yield [match[1], match[2].trim().replace(/^(['"])(.*)\1$/, "$2")];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** parseJsonValue implementation. */
|
|
40
|
+
function parseJsonValue(raw) {
|
|
41
|
+
if (raw === "" || raw === "null") return null;
|
|
42
|
+
if (raw[0] === "{" || raw[0] === "[") {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
} catch {
|
|
46
|
+
/* fall through */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (raw === "true") return true;
|
|
50
|
+
if (raw === "false") return false;
|
|
51
|
+
const num = Number(raw);
|
|
52
|
+
if (raw !== "" && Number.isFinite(num)) return num;
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** readServers implementation. */
|
|
57
|
+
async function readServers() {
|
|
58
|
+
try {
|
|
59
|
+
const text = await readFile(settingsPath, "utf8");
|
|
60
|
+
const section = {};
|
|
61
|
+
for (const [key, value] of readSection(text, "lsp")) {
|
|
62
|
+
section[key] = parseJsonValue(value);
|
|
63
|
+
}
|
|
64
|
+
return section;
|
|
65
|
+
} catch {
|
|
66
|
+
/* no document yet */
|
|
67
|
+
}
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** writeServers implementation. */
|
|
72
|
+
async function writeServers(servers) {
|
|
73
|
+
let text = "";
|
|
74
|
+
try {
|
|
75
|
+
text = await readFile(settingsPath, "utf8");
|
|
76
|
+
} catch {
|
|
77
|
+
/* new file */
|
|
78
|
+
}
|
|
79
|
+
const pattern = /^lsp[^\n]*\n(?:[ \t][^\n]*\n)*/m;
|
|
80
|
+
const section = `lsp:\n servers: ${JSON.stringify(servers.servers ?? {})}\n`;
|
|
81
|
+
const without = text.replace(pattern, "");
|
|
82
|
+
const rest = without.replace(/\n{3,}/g, "\n\n").trim();
|
|
83
|
+
const out = `${rest}${rest.endsWith("\n") ? "" : "\n"}${section}`;
|
|
84
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
85
|
+
await writeFile(settingsPath, out, "utf8");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** printHelp implementation. */
|
|
89
|
+
function printHelp() {
|
|
90
|
+
process.stdout.write(`usage: dsh lsp <verb> [args]
|
|
91
|
+
|
|
92
|
+
verbs:
|
|
93
|
+
list
|
|
94
|
+
servers add <id> <command> [--ext=.ts=typescript] [--args=a b c]
|
|
95
|
+
servers remove <id>
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** main implementation. */
|
|
100
|
+
async function main() {
|
|
101
|
+
const verb = process.argv[2];
|
|
102
|
+
const argv = process.argv.slice(3);
|
|
103
|
+
|
|
104
|
+
if (verb === "list") {
|
|
105
|
+
const servers = await readServers();
|
|
106
|
+
const entries = Object.entries(servers.servers ?? {});
|
|
107
|
+
if (entries.length === 0) {
|
|
108
|
+
process.stdout.write("no LSP servers configured\n");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const [id, cfg] of entries) {
|
|
112
|
+
const ext = Object.keys(cfg.extensionToLanguage ?? {}).join(",") || "—";
|
|
113
|
+
process.stdout.write(`${id} ${cfg.command} ${ext}\n`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (verb === "servers" && argv[0] === "add") {
|
|
119
|
+
const id = argv[1];
|
|
120
|
+
const command = argv[2];
|
|
121
|
+
if (!id || !command) {
|
|
122
|
+
process.stderr.write("dsh lsp servers add: missing id or command\n");
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const extensionToLanguage = {};
|
|
127
|
+
const args = [];
|
|
128
|
+
for (const arg of argv.slice(3)) {
|
|
129
|
+
if (arg.startsWith("--ext=")) {
|
|
130
|
+
const spec = arg.slice(6);
|
|
131
|
+
const [ext, lang] = spec.split("=");
|
|
132
|
+
if (ext && lang) extensionToLanguage[ext] = lang;
|
|
133
|
+
} else if (arg.startsWith("--args=")) {
|
|
134
|
+
args.push(...arg.slice(7).split(" ").filter(Boolean));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const servers = await readServers();
|
|
138
|
+
const table = servers.servers ?? {};
|
|
139
|
+
table[id] = { command, extensionToLanguage, ...(args.length > 0 ? { args } : {}) };
|
|
140
|
+
await writeServers({ ...servers, servers: table });
|
|
141
|
+
process.stdout.write(`added LSP server ${id} -> ${command}\n`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (verb === "servers" && argv[0] === "remove") {
|
|
146
|
+
const id = argv[1];
|
|
147
|
+
if (!id) {
|
|
148
|
+
process.stderr.write("dsh lsp servers remove: missing id\n");
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const servers = await readServers();
|
|
153
|
+
const table = servers.servers ?? {};
|
|
154
|
+
if (table[id] === undefined) {
|
|
155
|
+
process.stderr.write(`dsh lsp servers remove: no server "${id}"\n`);
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
delete table[id];
|
|
160
|
+
await writeServers({ ...servers, servers: table });
|
|
161
|
+
process.stdout.write(`removed LSP server ${id}\n`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
printHelp();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((err) => {
|
|
169
|
+
process.stderr.write(`dsh lsp: ${err.message}\n`);
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// jscpd:ignore-end
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lsp`: owns the LSP capability on the web profile. The harness ships the
|
|
3
|
+
* LSP service definition (`ctx.lsp`), a generic stdio provider
|
|
4
|
+
* (`dsh-lsp-stdio`), and a model-facing `lsp` tool (`dsh-tool-lsp`) — but
|
|
5
|
+
* composes none of them by default, and its LSP operation set is a closed
|
|
6
|
+
* compile-time union (no formatting: that is `formatters`' job). This
|
|
7
|
+
* plugin mounts the trio through `ctx.plugin` and feeds the stdio provider
|
|
8
|
+
* the server table this plugin's own Config carries, so the agent's
|
|
9
|
+
* `goToDefinition`/`findReferences`/`goToImplementation`/`hover` queries work
|
|
10
|
+
* out of the box. The `dsh lsp` CLI (bin/lsp.mjs) manages the table; changes
|
|
11
|
+
* apply on the next boot (mounts are boot-time, not hot-reloaded).
|
|
12
|
+
* @module lsp
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
15
|
+
import { LspConfig } from "./settings.js";
|
|
16
|
+
export type * from "./settings.js";
|
|
17
|
+
export declare const name = "lsp";
|
|
18
|
+
export declare const inject: string[];
|
|
19
|
+
/** Re-exported bare, not as `z<LspConfig>` — see `settings.ts`. */
|
|
20
|
+
export declare const Config: import("@deepseek-ai/schemastery").default<import("./settings.js").LspServerInput, LspConfig, "plain">;
|
|
21
|
+
/**
|
|
22
|
+
* Mount the LSP service definition, the stdio provider (only when at least one
|
|
23
|
+
* server is configured — `lsp-stdio` refuses an empty table), and the
|
|
24
|
+
* model-facing tool.
|
|
25
|
+
*
|
|
26
|
+
* Since 0.2.0 there is no separate settings section to read: this plugin's
|
|
27
|
+
* `Config` is the `lsp` form the settings service projects, under this entry's
|
|
28
|
+
* own id as its namespace, so `config.servers` is the one server table — entry
|
|
29
|
+
* values and profile edits alike arrive through it. It is a live reference the
|
|
30
|
+
* Loader commits settings writes into, so it is read through `installedServers`
|
|
31
|
+
* at mount rather than snapshotted. The mounts stay boot-time, so a table edited
|
|
32
|
+
* after boot takes effect on the next boot, the same contract the `dsh lsp` CLI
|
|
33
|
+
* documents.
|
|
34
|
+
*
|
|
35
|
+
* @param ctx - the plugin context.
|
|
36
|
+
* @param config - the plugin's live configuration, carrying the LSP server table.
|
|
37
|
+
*/
|
|
38
|
+
export declare function apply(ctx: Context, config: LspConfig): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lsp`: owns the LSP capability on the web profile. The harness ships the
|
|
3
|
+
* LSP service definition (`ctx.lsp`), a generic stdio provider
|
|
4
|
+
* (`dsh-lsp-stdio`), and a model-facing `lsp` tool (`dsh-tool-lsp`) — but
|
|
5
|
+
* composes none of them by default, and its LSP operation set is a closed
|
|
6
|
+
* compile-time union (no formatting: that is `formatters`' job). This
|
|
7
|
+
* plugin mounts the trio through `ctx.plugin` and feeds the stdio provider
|
|
8
|
+
* the server table this plugin's own Config carries, so the agent's
|
|
9
|
+
* `goToDefinition`/`findReferences`/`goToImplementation`/`hover` queries work
|
|
10
|
+
* out of the box. The `dsh lsp` CLI (bin/lsp.mjs) manages the table; changes
|
|
11
|
+
* apply on the next boot (mounts are boot-time, not hot-reloaded).
|
|
12
|
+
* @module lsp
|
|
13
|
+
*/
|
|
14
|
+
import { declareCustomSettingsPage } from "@dsh-stack/plugin-kit";
|
|
15
|
+
import Lsp from "@deepseek-ai/dsh-lsp";
|
|
16
|
+
import * as LspStdio from "@deepseek-ai/dsh-lsp-stdio";
|
|
17
|
+
import * as ToolLsp from "@deepseek-ai/dsh-tool-lsp";
|
|
18
|
+
import { installedServers, LspConfig } from "./settings.js";
|
|
19
|
+
export const name = "lsp";
|
|
20
|
+
export const inject = [];
|
|
21
|
+
/** Re-exported bare, not as `z<LspConfig>` — see `settings.ts`. */
|
|
22
|
+
export const Config = LspConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Mount the LSP service definition, the stdio provider (only when at least one
|
|
25
|
+
* server is configured — `lsp-stdio` refuses an empty table), and the
|
|
26
|
+
* model-facing tool.
|
|
27
|
+
*
|
|
28
|
+
* Since 0.2.0 there is no separate settings section to read: this plugin's
|
|
29
|
+
* `Config` is the `lsp` form the settings service projects, under this entry's
|
|
30
|
+
* own id as its namespace, so `config.servers` is the one server table — entry
|
|
31
|
+
* values and profile edits alike arrive through it. It is a live reference the
|
|
32
|
+
* Loader commits settings writes into, so it is read through `installedServers`
|
|
33
|
+
* at mount rather than snapshotted. The mounts stay boot-time, so a table edited
|
|
34
|
+
* after boot takes effect on the next boot, the same contract the `dsh lsp` CLI
|
|
35
|
+
* documents.
|
|
36
|
+
*
|
|
37
|
+
* @param ctx - the plugin context.
|
|
38
|
+
* @param config - the plugin's live configuration, carrying the LSP server table.
|
|
39
|
+
*/
|
|
40
|
+
export function apply(ctx, config) {
|
|
41
|
+
declareCustomSettingsPage(ctx);
|
|
42
|
+
ctx.inject(["settings"], async () => {
|
|
43
|
+
const servers = installedServers(config);
|
|
44
|
+
if (ctx.get("lsp") === undefined) {
|
|
45
|
+
await ctx.plugin(Lsp);
|
|
46
|
+
}
|
|
47
|
+
if (Object.keys(servers).length > 0) {
|
|
48
|
+
await ctx.plugin(LspStdio, { servers });
|
|
49
|
+
await ctx.plugin(ToolLsp, {});
|
|
50
|
+
ctx.logger.info(`lsp: mounted ${Object.keys(servers).length} LSP server(s)`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
ctx.logger.warn("lsp: no LSP servers configured — run `dsh lsp servers add <id> <command>`");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lsp settings: the `lsp` entry owns the LSP server table. The plugin mounts
|
|
3
|
+
* the harness LSP capability (service definition, the stdio provider with this
|
|
4
|
+
* table, and the model-facing `lsp` tool) on the web profile, so
|
|
5
|
+
* `goToDefinition`/`findReferences`/`goToImplementation`/`hover` work for the
|
|
6
|
+
* agent without touching the pristine harness.
|
|
7
|
+
*
|
|
8
|
+
* Since 0.2.0 there is no second section schema to register: this plugin's own
|
|
9
|
+
* `Config` is the form the settings service projects, and its namespace is this
|
|
10
|
+
* entry's own id. So one schema, `LspConfig`, carries both roles. Which of its
|
|
11
|
+
* fields that form shows is decided by volatility: the server table is the
|
|
12
|
+
* user's choice — which language servers exist and how to launch them — so it is
|
|
13
|
+
* declared `.volatile()`, and it is the whole of the table rather than just the
|
|
14
|
+
* list of ids. The settings service only recurses into `object` schemas when it
|
|
15
|
+
* projects a form, so a volatile marker nested inside the table's entries would
|
|
16
|
+
* leave `servers` itself unprojected; marking the dictionary is what makes the
|
|
17
|
+
* form exist, and it is what lets the user edit a server's command rather than
|
|
18
|
+
* only add and remove ids.
|
|
19
|
+
* @module lsp/settings
|
|
20
|
+
*/
|
|
21
|
+
import type { Volatile } from "@deepseek-ai/cordis";
|
|
22
|
+
import z from "@deepseek-ai/schemastery";
|
|
23
|
+
import type { LspLocalServerConfig } from "@deepseek-ai/dsh-lsp-stdio";
|
|
24
|
+
/** Namespace of the LSP server table — this plugin's own Loader entry id. */
|
|
25
|
+
export declare const NS: import("@dsh-stack/plugin-kit").SettingsNamespace;
|
|
26
|
+
/** One stdio language server the plugin mounts through `dsh-lsp-stdio`. */
|
|
27
|
+
export type LspServerEntry = LspLocalServerConfig;
|
|
28
|
+
export declare const LspServerEntry: z<LspServerEntry>;
|
|
29
|
+
/** The plugin's configuration — and, since 0.2.0, its own settings form. */
|
|
30
|
+
export interface LspConfig {
|
|
31
|
+
/**
|
|
32
|
+
* Lowercase leading-dot extension → installed stdio server. Volatile because
|
|
33
|
+
* the answer is machine-local: which language servers exist, under what ids and
|
|
34
|
+
* with what commands, differs per workstation, so no deployment can pin one
|
|
35
|
+
* correct table and the user owns it. It is volatile as one whole-dictionary
|
|
36
|
+
* node — not a marker per table entry — because the settings service only
|
|
37
|
+
* recurses into `object` schemas when it projects a form, and because a form
|
|
38
|
+
* write can only address a path lying under a volatile node: a reference per
|
|
39
|
+
* entry could be neither declared nor edited, and marking only the ids would
|
|
40
|
+
* leave the command a client cannot correct.
|
|
41
|
+
*/
|
|
42
|
+
servers: Volatile<Record<string, LspServerEntry>>;
|
|
43
|
+
}
|
|
44
|
+
/** What the server-table schema accepts before validation. */
|
|
45
|
+
export type LspServerInput = {
|
|
46
|
+
servers: Record<string, LspServerEntry>;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Not annotated `z<LspConfig>`: a volatile field's output type is `Volatile<T>`,
|
|
50
|
+
* which `z<T>`'s invariant position cannot hold. The three-parameter schema
|
|
51
|
+
* type is the annotation that can, and it is required rather than cosmetic —
|
|
52
|
+
* left to inference, the nested `Volatile` inside this dictionary has no name
|
|
53
|
+
* the declaration emitter can write (`TS2742`), and `themes`/`formatters` get
|
|
54
|
+
* away with a bare `z.object({...})` only because their volatile fields are
|
|
55
|
+
* scalar. `harness`'s own `llm-pi-ai` uses the same bare shape and builds with
|
|
56
|
+
* a different declaration emitter.
|
|
57
|
+
*/
|
|
58
|
+
export declare const LspConfig: z<LspServerInput, LspConfig, "plain">;
|
|
59
|
+
/**
|
|
60
|
+
* The servers currently installed for this entry, in the mutable shape
|
|
61
|
+
* `dsh-lsp-stdio` declares.
|
|
62
|
+
*
|
|
63
|
+
* Reads the volatile field at call time, so a settings write is visible to the
|
|
64
|
+
* next read rather than to a value captured at boot. `args` is copied because a
|
|
65
|
+
* volatile snapshot is deeply frozen and the stdio provider's own Config declares
|
|
66
|
+
* a mutable `string[]` there; the plain properties and index signatures need no
|
|
67
|
+
* such copy, as TypeScript does not compare them for `readonly`.
|
|
68
|
+
*
|
|
69
|
+
* @param config - the plugin's projected Config.
|
|
70
|
+
* @returns the installed server table, empty when none is configured.
|
|
71
|
+
*/
|
|
72
|
+
export declare function installedServers(config: LspConfig | undefined): Record<string, LspServerEntry>;
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lsp settings: the `lsp` entry owns the LSP server table. The plugin mounts
|
|
3
|
+
* the harness LSP capability (service definition, the stdio provider with this
|
|
4
|
+
* table, and the model-facing `lsp` tool) on the web profile, so
|
|
5
|
+
* `goToDefinition`/`findReferences`/`goToImplementation`/`hover` work for the
|
|
6
|
+
* agent without touching the pristine harness.
|
|
7
|
+
*
|
|
8
|
+
* Since 0.2.0 there is no second section schema to register: this plugin's own
|
|
9
|
+
* `Config` is the form the settings service projects, and its namespace is this
|
|
10
|
+
* entry's own id. So one schema, `LspConfig`, carries both roles. Which of its
|
|
11
|
+
* fields that form shows is decided by volatility: the server table is the
|
|
12
|
+
* user's choice — which language servers exist and how to launch them — so it is
|
|
13
|
+
* declared `.volatile()`, and it is the whole of the table rather than just the
|
|
14
|
+
* list of ids. The settings service only recurses into `object` schemas when it
|
|
15
|
+
* projects a form, so a volatile marker nested inside the table's entries would
|
|
16
|
+
* leave `servers` itself unprojected; marking the dictionary is what makes the
|
|
17
|
+
* form exist, and it is what lets the user edit a server's command rather than
|
|
18
|
+
* only add and remove ids.
|
|
19
|
+
* @module lsp/settings
|
|
20
|
+
*/
|
|
21
|
+
import z from "@deepseek-ai/schemastery";
|
|
22
|
+
import { settingsNamespace } from "@dsh-stack/plugin-kit";
|
|
23
|
+
/** Namespace of the LSP server table — this plugin's own Loader entry id. */
|
|
24
|
+
export const NS = settingsNamespace("lsp");
|
|
25
|
+
export const LspServerEntry = z.object({
|
|
26
|
+
command: z.string().required(),
|
|
27
|
+
extensionToLanguage: z.dict(String).required(),
|
|
28
|
+
args: z.array(String).default([]),
|
|
29
|
+
env: z.dict(String).default({}),
|
|
30
|
+
initializationOptions: z.any().default(null),
|
|
31
|
+
configuration: z.any().default(null),
|
|
32
|
+
maxMessageBytes: z.number().default(16_000_000),
|
|
33
|
+
maxStderrBytes: z.number().default(1_000_000),
|
|
34
|
+
maxDocumentBytes: z.number().default(4_000_000),
|
|
35
|
+
shutdownTimeoutMs: z.number().default(5_000),
|
|
36
|
+
killGraceMs: z.number().default(2_000),
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Not annotated `z<LspConfig>`: a volatile field's output type is `Volatile<T>`,
|
|
40
|
+
* which `z<T>`'s invariant position cannot hold. The three-parameter schema
|
|
41
|
+
* type is the annotation that can, and it is required rather than cosmetic —
|
|
42
|
+
* left to inference, the nested `Volatile` inside this dictionary has no name
|
|
43
|
+
* the declaration emitter can write (`TS2742`), and `themes`/`formatters` get
|
|
44
|
+
* away with a bare `z.object({...})` only because their volatile fields are
|
|
45
|
+
* scalar. `harness`'s own `llm-pi-ai` uses the same bare shape and builds with
|
|
46
|
+
* a different declaration emitter.
|
|
47
|
+
*/
|
|
48
|
+
export const LspConfig = z.object({
|
|
49
|
+
servers: z.dict(LspServerEntry).default({}).volatile(),
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* The servers currently installed for this entry, in the mutable shape
|
|
53
|
+
* `dsh-lsp-stdio` declares.
|
|
54
|
+
*
|
|
55
|
+
* Reads the volatile field at call time, so a settings write is visible to the
|
|
56
|
+
* next read rather than to a value captured at boot. `args` is copied because a
|
|
57
|
+
* volatile snapshot is deeply frozen and the stdio provider's own Config declares
|
|
58
|
+
* a mutable `string[]` there; the plain properties and index signatures need no
|
|
59
|
+
* such copy, as TypeScript does not compare them for `readonly`.
|
|
60
|
+
*
|
|
61
|
+
* @param config - the plugin's projected Config.
|
|
62
|
+
* @returns the installed server table, empty when none is configured.
|
|
63
|
+
*/
|
|
64
|
+
export function installedServers(config) {
|
|
65
|
+
return Object.fromEntries(Object.entries(config?.servers?.get() ?? {}).map(([id, entry]) => [
|
|
66
|
+
id,
|
|
67
|
+
{ ...entry, args: [...(entry.args ?? [])] },
|
|
68
|
+
]));
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-stack/lsp",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"description": "LSP server configuration + dsh lsp CLI for the DeepSeek Harness LSP capability seam",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./src/*": "./src/*",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"bin",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "node check-plugin.mjs"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
28
|
+
"@deepseek-ai/dsh-lsp": "0.2.0-rc.2",
|
|
29
|
+
"@deepseek-ai/dsh-lsp-stdio": "0.2.0-rc.2",
|
|
30
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.2",
|
|
31
|
+
"@deepseek-ai/dsh-tool-lsp": "0.2.0-rc.2",
|
|
32
|
+
"@deepseek-ai/schemastery": "^3.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@dsh-stack/plugin-kit": "0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.9.0",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
|
+
"@deepseek-ai/dsh-lsp": "0.2.0-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-lsp-stdio": "0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-tool-lsp": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/schemastery": "^3.0.0"
|
|
46
|
+
},
|
|
47
|
+
"stack": {
|
|
48
|
+
"kind": "plugin",
|
|
49
|
+
"id": "stack.dev.lsp"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/marius-patrik/dsh-stack.git",
|
|
57
|
+
"directory": "plugins/lsp"
|
|
58
|
+
}
|
|
59
|
+
}
|