@cjhyy/code-shell-capability-coding 0.8.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 +69 -0
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/bin/agent-server-stdio.d.ts +1 -0
- package/dist/bin/agent-server-stdio.js +4 -0
- package/dist/capability-runtime.d.ts +22 -0
- package/dist/capability-runtime.js +88 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +55 -0
- package/dist/cc-orchestrator/agent-adapter.js +143 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +20 -0
- package/dist/cc-orchestrator/cc-capability.js +57 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +45 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +251 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +39 -0
- package/dist/cc-orchestrator/codex-session-history.js +249 -0
- package/dist/cc-orchestrator/codex-user-text.d.ts +9 -0
- package/dist/cc-orchestrator/codex-user-text.js +25 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +231 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-driver.js +284 -0
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +36 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +185 -0
- package/dist/cc-orchestrator/index.d.ts +10 -0
- package/dist/cc-orchestrator/index.js +10 -0
- package/dist/cc-orchestrator/related-session-discovery.d.ts +21 -0
- package/dist/cc-orchestrator/related-session-discovery.js +77 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +16 -0
- package/dist/cc-orchestrator/relevance-judge.js +38 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +99 -0
- package/dist/cc-orchestrator/session-discovery.js +295 -0
- package/dist/cc-orchestrator/session-history.d.ts +55 -0
- package/dist/cc-orchestrator/session-history.js +150 -0
- package/dist/external-agents/config.d.ts +3 -0
- package/dist/external-agents/config.js +16 -0
- package/dist/external-agents/types.d.ts +32 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/external-runtimes/claude-code/event-translator.d.ts +54 -0
- package/dist/external-runtimes/claude-code/event-translator.js +201 -0
- package/dist/external-runtimes/claude-code/mcp-config.d.ts +84 -0
- package/dist/external-runtimes/claude-code/mcp-config.js +175 -0
- package/dist/external-runtimes/claude-code/runtime.d.ts +56 -0
- package/dist/external-runtimes/claude-code/runtime.js +168 -0
- package/dist/external-runtimes/codex/app-server-client.d.ts +66 -0
- package/dist/external-runtimes/codex/app-server-client.js +244 -0
- package/dist/external-runtimes/codex/event-translator.d.ts +68 -0
- package/dist/external-runtimes/codex/event-translator.js +187 -0
- package/dist/external-runtimes/codex/index.d.ts +7 -0
- package/dist/external-runtimes/codex/index.js +6 -0
- package/dist/external-runtimes/codex/runtime.d.ts +128 -0
- package/dist/external-runtimes/codex/runtime.js +216 -0
- package/dist/external-runtimes/index.d.ts +37 -0
- package/dist/external-runtimes/index.js +27 -0
- package/dist/external-runtimes/session-factory.d.ts +97 -0
- package/dist/external-runtimes/session-factory.js +110 -0
- package/dist/external-runtimes/shared/mcp-bridge.d.ts +66 -0
- package/dist/external-runtimes/shared/mcp-bridge.js +365 -0
- package/dist/external-runtimes/shared/session-context-store.d.ts +83 -0
- package/dist/external-runtimes/shared/session-context-store.js +114 -0
- package/dist/external-runtimes/shared/spawn-env.d.ts +30 -0
- package/dist/external-runtimes/shared/spawn-env.js +41 -0
- package/dist/git/parse-log.d.ts +14 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +49 -0
- package/dist/git/utils.js +161 -0
- package/dist/git/worktree/crud.d.ts +108 -0
- package/dist/git/worktree/crud.js +415 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +83 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +86 -0
- package/dist/git/worktree/index.d.ts +6 -0
- package/dist/git/worktree/index.js +6 -0
- package/dist/git/worktree/query.d.ts +43 -0
- package/dist/git/worktree/query.js +128 -0
- package/dist/git/worktree/slug.d.ts +12 -0
- package/dist/git/worktree/slug.js +59 -0
- package/dist/git/worktree.d.ts +1 -0
- package/dist/git/worktree.js +5 -0
- package/dist/index.capability.d.ts +7 -0
- package/dist/index.capability.js +190 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.git.d.ts +4 -0
- package/dist/index.git.js +4 -0
- package/dist/index.js +18 -0
- package/dist/index.orchestration.d.ts +7 -0
- package/dist/index.orchestration.js +5 -0
- package/dist/lsp/client.d.ts +41 -0
- package/dist/lsp/client.js +192 -0
- package/dist/lsp/manager.d.ts +53 -0
- package/dist/lsp/manager.js +248 -0
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/lsp/servers.d.ts +16 -0
- package/dist/lsp/servers.js +60 -0
- package/dist/prompt/coding.md +42 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +161 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/tools/apply-patch/LICENSE-codex +201 -0
- package/dist/tools/apply-patch/NOTICE.md +29 -0
- package/dist/tools/apply-patch/applier.d.ts +26 -0
- package/dist/tools/apply-patch/applier.js +306 -0
- package/dist/tools/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tools/apply-patch/backup-targets.js +30 -0
- package/dist/tools/apply-patch/eol.d.ts +4 -0
- package/dist/tools/apply-patch/eol.js +9 -0
- package/dist/tools/apply-patch/index.d.ts +20 -0
- package/dist/tools/apply-patch/index.js +106 -0
- package/dist/tools/apply-patch/parser.d.ts +17 -0
- package/dist/tools/apply-patch/parser.js +209 -0
- package/dist/tools/apply-patch/seek-sequence.d.ts +18 -0
- package/dist/tools/apply-patch/seek-sequence.js +123 -0
- package/dist/tools/apply-patch/types.d.ts +49 -0
- package/dist/tools/apply-patch/types.js +13 -0
- package/dist/tools/brief.d.ts +6 -0
- package/dist/tools/brief.js +41 -0
- package/dist/tools/check-quota.d.ts +14 -0
- package/dist/tools/check-quota.js +38 -0
- package/dist/tools/drive-agent.d.ts +52 -0
- package/dist/tools/drive-agent.js +1184 -0
- package/dist/tools/lsp.d.ts +6 -0
- package/dist/tools/lsp.js +220 -0
- package/dist/tools/notebook-edit.d.ts +6 -0
- package/dist/tools/notebook-edit.js +127 -0
- package/dist/tools/worktree.d.ts +10 -0
- package/dist/tools/worktree.js +363 -0
- package/package.json +60 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP client — JSON-RPC over stdio communication with coding language servers.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
export class LSPClient extends EventEmitter {
|
|
7
|
+
command;
|
|
8
|
+
args;
|
|
9
|
+
cwd;
|
|
10
|
+
process;
|
|
11
|
+
nextId = 1;
|
|
12
|
+
pending = new Map();
|
|
13
|
+
// The LSP framing uses byte-counted Content-Length, so we buffer bytes,
|
|
14
|
+
// not characters. Joining chunks as strings (`data.toString()` + string
|
|
15
|
+
// concat) can split a multibyte UTF-8 codepoint across chunks and de-
|
|
16
|
+
// synchronize against Content-Length. Stay in Buffer-land until we've
|
|
17
|
+
// sliced out exactly the right body length.
|
|
18
|
+
buffer = Buffer.alloc(0);
|
|
19
|
+
contentLength = -1;
|
|
20
|
+
initialized = false;
|
|
21
|
+
constructor(command, args = [], cwd) {
|
|
22
|
+
super();
|
|
23
|
+
this.command = command;
|
|
24
|
+
this.args = args;
|
|
25
|
+
this.cwd = cwd;
|
|
26
|
+
}
|
|
27
|
+
async start() {
|
|
28
|
+
this.process = spawn(this.command, this.args, {
|
|
29
|
+
cwd: this.cwd,
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
});
|
|
32
|
+
this.process.stdout?.on("data", (data) => this.handleData(data));
|
|
33
|
+
this.process.stderr?.on("data", (_data) => {
|
|
34
|
+
// LSP servers often log to stderr — ignore
|
|
35
|
+
});
|
|
36
|
+
this.process.on("exit", (code) => {
|
|
37
|
+
this.emit("exit", code);
|
|
38
|
+
this.rejectAll(new Error(`LSP server exited with code ${code}`));
|
|
39
|
+
});
|
|
40
|
+
// Don't keep the event loop alive solely for the LSP child.
|
|
41
|
+
// Call after listeners are wired so event registration doesn't re-ref.
|
|
42
|
+
this.process.unref();
|
|
43
|
+
}
|
|
44
|
+
async initialize(rootUri) {
|
|
45
|
+
const result = await this.request("initialize", {
|
|
46
|
+
processId: process.pid,
|
|
47
|
+
rootUri,
|
|
48
|
+
capabilities: {
|
|
49
|
+
textDocument: {
|
|
50
|
+
definition: { dynamicRegistration: false },
|
|
51
|
+
references: { dynamicRegistration: false },
|
|
52
|
+
hover: { contentFormat: ["plaintext", "markdown"] },
|
|
53
|
+
documentSymbol: { dynamicRegistration: false },
|
|
54
|
+
completion: { dynamicRegistration: false },
|
|
55
|
+
},
|
|
56
|
+
workspace: {
|
|
57
|
+
symbol: { dynamicRegistration: false },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
await this.notify("initialized", {});
|
|
62
|
+
this.initialized = true;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
async request(method, params) {
|
|
66
|
+
if (!this.process?.stdin)
|
|
67
|
+
throw new Error("LSP server not started");
|
|
68
|
+
const id = this.nextId++;
|
|
69
|
+
const message = { jsonrpc: "2.0", id, method, params };
|
|
70
|
+
const content = JSON.stringify(message);
|
|
71
|
+
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
if (this.pending.has(id)) {
|
|
75
|
+
this.pending.delete(id);
|
|
76
|
+
reject(new Error(`LSP request "${method}" timed out`));
|
|
77
|
+
}
|
|
78
|
+
}, 30000);
|
|
79
|
+
// Don't let the timer keep the event loop alive by itself
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
this.pending.set(id, {
|
|
82
|
+
resolve: (value) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
resolve(value);
|
|
85
|
+
},
|
|
86
|
+
reject: (err) => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
reject(err);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
this.process.stdin.write(header + content);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async notify(method, params) {
|
|
95
|
+
if (!this.process?.stdin)
|
|
96
|
+
throw new Error("LSP server not started");
|
|
97
|
+
const message = { jsonrpc: "2.0", method, params };
|
|
98
|
+
const content = JSON.stringify(message);
|
|
99
|
+
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
|
|
100
|
+
this.process.stdin.write(header + content);
|
|
101
|
+
}
|
|
102
|
+
async shutdown() {
|
|
103
|
+
if (!this.process)
|
|
104
|
+
return;
|
|
105
|
+
try {
|
|
106
|
+
await this.request("shutdown", {});
|
|
107
|
+
await this.notify("exit", {});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Force kill
|
|
111
|
+
}
|
|
112
|
+
this.process.stdout?.removeAllListeners();
|
|
113
|
+
this.process.stderr?.removeAllListeners();
|
|
114
|
+
this.process.removeAllListeners();
|
|
115
|
+
this.process.kill();
|
|
116
|
+
this.process = undefined;
|
|
117
|
+
// Drain pending requests
|
|
118
|
+
for (const { reject } of this.pending.values()) {
|
|
119
|
+
reject(new Error("LSP server shut down"));
|
|
120
|
+
}
|
|
121
|
+
this.pending.clear();
|
|
122
|
+
}
|
|
123
|
+
get isInitialized() {
|
|
124
|
+
return this.initialized;
|
|
125
|
+
}
|
|
126
|
+
get isAlive() {
|
|
127
|
+
return this.process !== undefined && !this.process.killed;
|
|
128
|
+
}
|
|
129
|
+
handleData(data) {
|
|
130
|
+
// Append the new chunk as bytes. Buffer.concat avoids the UTF-8
|
|
131
|
+
// boundary corruption that string concatenation produces — a
|
|
132
|
+
// multibyte codepoint split across two TCP/pipe chunks decodes to
|
|
133
|
+
// U+FFFD, mangles the buffer length count, and breaks framing.
|
|
134
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
135
|
+
while (true) {
|
|
136
|
+
if (this.contentLength < 0) {
|
|
137
|
+
// Search for the header terminator in bytes, not chars.
|
|
138
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
139
|
+
if (headerEnd < 0)
|
|
140
|
+
break;
|
|
141
|
+
// Headers are ASCII per spec, so toString("ascii") is safe and
|
|
142
|
+
// avoids paying for a UTF-8 decode on the framing band.
|
|
143
|
+
const header = this.buffer.slice(0, headerEnd).toString("ascii");
|
|
144
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
145
|
+
if (!match) {
|
|
146
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
this.contentLength = parseInt(match[1], 10);
|
|
150
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
151
|
+
}
|
|
152
|
+
// Content-Length is a BYTE count — compare against the buffer's
|
|
153
|
+
// byte length, not its character length. Pre-fix this compared
|
|
154
|
+
// string lengths; with multibyte responses (e.g. CJK identifiers,
|
|
155
|
+
// emoji in diagnostics) we'd block waiting for bytes that had
|
|
156
|
+
// already arrived.
|
|
157
|
+
if (this.buffer.length < this.contentLength)
|
|
158
|
+
break;
|
|
159
|
+
const bodyBytes = this.buffer.subarray(0, this.contentLength);
|
|
160
|
+
this.buffer = this.buffer.subarray(this.contentLength);
|
|
161
|
+
this.contentLength = -1;
|
|
162
|
+
// Decode the full body in one shot — at this point we have the
|
|
163
|
+
// exact byte count, so the UTF-8 boundary issue is fully gone.
|
|
164
|
+
const content = bodyBytes.toString("utf8");
|
|
165
|
+
try {
|
|
166
|
+
const message = JSON.parse(content);
|
|
167
|
+
if ("id" in message && this.pending.has(message.id)) {
|
|
168
|
+
const { resolve, reject } = this.pending.get(message.id);
|
|
169
|
+
this.pending.delete(message.id);
|
|
170
|
+
if (message.error) {
|
|
171
|
+
reject(new Error(`LSP error: ${message.error.message}`));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
resolve(message.result);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if ("method" in message && !("id" in message)) {
|
|
178
|
+
this.emit("notification", message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Skip malformed messages
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
rejectAll(error) {
|
|
187
|
+
for (const { reject } of this.pending.values()) {
|
|
188
|
+
reject(error);
|
|
189
|
+
}
|
|
190
|
+
this.pending.clear();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Server Manager — manages lifecycle of language server instances.
|
|
3
|
+
* Lazy initialization: servers start only when needed.
|
|
4
|
+
*/
|
|
5
|
+
import { LSPClient } from "./client.js";
|
|
6
|
+
type ServerState = "stopped" | "starting" | "ready" | "error";
|
|
7
|
+
export declare class LSPServerManager {
|
|
8
|
+
private servers;
|
|
9
|
+
private rootUri;
|
|
10
|
+
constructor(cwd: string);
|
|
11
|
+
/**
|
|
12
|
+
* Get or start a language server by name.
|
|
13
|
+
*/
|
|
14
|
+
getClient(serverName: string): Promise<LSPClient | undefined>;
|
|
15
|
+
/**
|
|
16
|
+
* Start a specific language server.
|
|
17
|
+
*/
|
|
18
|
+
private startServer;
|
|
19
|
+
/**
|
|
20
|
+
* Check if any LSP server is connected.
|
|
21
|
+
*/
|
|
22
|
+
isConnected(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* List server statuses.
|
|
25
|
+
*/
|
|
26
|
+
listServers(): Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
language: string;
|
|
29
|
+
state: ServerState;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Shutdown all servers.
|
|
34
|
+
*/
|
|
35
|
+
shutdownAll(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export declare function isCommandAvailable(command: string, env?: NodeJS.ProcessEnv): boolean;
|
|
38
|
+
export declare function initializeLSPManager(cwd: string): LSPServerManager;
|
|
39
|
+
/**
|
|
40
|
+
* Manager for `cwd`, creating it on first use.
|
|
41
|
+
*
|
|
42
|
+
* Lazy by design: spawning language servers eagerly at startup would cost every
|
|
43
|
+
* session a process it may never use. Callers that only want to observe existing
|
|
44
|
+
* state pass `{ create: false }`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getLSPManager(cwd?: string, options?: {
|
|
47
|
+
create?: boolean;
|
|
48
|
+
}): LSPServerManager | undefined;
|
|
49
|
+
/** Shut down and forget one workspace's servers (session/workspace teardown). */
|
|
50
|
+
export declare function shutdownLSPManager(cwd: string): Promise<void>;
|
|
51
|
+
/** Shut down every workspace's servers (process teardown). */
|
|
52
|
+
export declare function shutdownAllLSPManagers(): Promise<void>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Server Manager — manages lifecycle of language server instances.
|
|
3
|
+
* Lazy initialization: servers start only when needed.
|
|
4
|
+
*/
|
|
5
|
+
import { LSPClient } from "./client.js";
|
|
6
|
+
import { BUILTIN_LSP_SERVERS } from "./servers.js";
|
|
7
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
8
|
+
import { delimiter, extname, join } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { rootUriToPath } from "./root-path.js";
|
|
11
|
+
export class LSPServerManager {
|
|
12
|
+
servers = new Map();
|
|
13
|
+
rootUri;
|
|
14
|
+
constructor(cwd) {
|
|
15
|
+
this.rootUri = pathToFileURL(cwd).href;
|
|
16
|
+
// Register all built-in servers
|
|
17
|
+
for (const config of BUILTIN_LSP_SERVERS) {
|
|
18
|
+
this.servers.set(config.name, { config, client: undefined, state: "stopped" });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get or start a language server by name.
|
|
23
|
+
*/
|
|
24
|
+
async getClient(serverName) {
|
|
25
|
+
const managed = this.servers.get(serverName);
|
|
26
|
+
if (!managed)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (managed.state === "ready" && managed.client?.isAlive) {
|
|
29
|
+
return managed.client;
|
|
30
|
+
}
|
|
31
|
+
if (managed.state === "starting") {
|
|
32
|
+
// Wait for it to be ready
|
|
33
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
34
|
+
return managed.client;
|
|
35
|
+
}
|
|
36
|
+
// Try to start
|
|
37
|
+
return this.startServer(serverName);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Start a specific language server.
|
|
41
|
+
*/
|
|
42
|
+
async startServer(name) {
|
|
43
|
+
const managed = this.servers.get(name);
|
|
44
|
+
if (!managed)
|
|
45
|
+
return undefined;
|
|
46
|
+
// Check if command is available
|
|
47
|
+
if (!isCommandAvailable(managed.config.command)) {
|
|
48
|
+
managed.state = "error";
|
|
49
|
+
managed.error = `${managed.config.command} not found. Install: ${managed.config.installHint}`;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
managed.state = "starting";
|
|
53
|
+
try {
|
|
54
|
+
const client = new LSPClient(managed.config.command, managed.config.args, rootUriToPath(this.rootUri));
|
|
55
|
+
await client.start();
|
|
56
|
+
await client.initialize(this.rootUri);
|
|
57
|
+
managed.client = client;
|
|
58
|
+
managed.state = "ready";
|
|
59
|
+
return client;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
managed.state = "error";
|
|
63
|
+
managed.error = err.message;
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if any LSP server is connected.
|
|
69
|
+
*/
|
|
70
|
+
isConnected() {
|
|
71
|
+
for (const server of this.servers.values()) {
|
|
72
|
+
if (server.state === "ready" && server.client?.isAlive)
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* List server statuses.
|
|
79
|
+
*/
|
|
80
|
+
listServers() {
|
|
81
|
+
return [...this.servers.values()].map((s) => ({
|
|
82
|
+
name: s.config.name,
|
|
83
|
+
language: s.config.language,
|
|
84
|
+
state: s.state,
|
|
85
|
+
error: s.error,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Shutdown all servers.
|
|
90
|
+
*/
|
|
91
|
+
async shutdownAll() {
|
|
92
|
+
for (const server of this.servers.values()) {
|
|
93
|
+
if (server.client?.isAlive) {
|
|
94
|
+
try {
|
|
95
|
+
await server.client.shutdown();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Force killed
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
server.state = "stopped";
|
|
102
|
+
server.client = undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function isCommandAvailable(command, env = process.env) {
|
|
107
|
+
const trimmed = command.trim();
|
|
108
|
+
if (!trimmed)
|
|
109
|
+
return false;
|
|
110
|
+
const hasPathSeparator = trimmed.includes("/") || trimmed.includes("\\");
|
|
111
|
+
// A bare command with whitespace is not executable via spawn(command, args)
|
|
112
|
+
// anyway. Reject it up front so a configured command can never be interpreted
|
|
113
|
+
// as shell syntax such as `pylsp; touch /tmp/pwned`.
|
|
114
|
+
if (!hasPathSeparator && /\s/.test(trimmed))
|
|
115
|
+
return false;
|
|
116
|
+
if (hasPathSeparator) {
|
|
117
|
+
return candidateCommandNames(trimmed, env).some(isExecutableFile);
|
|
118
|
+
}
|
|
119
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
120
|
+
if (!dir)
|
|
121
|
+
continue;
|
|
122
|
+
for (const name of candidateCommandNames(trimmed, env)) {
|
|
123
|
+
if (isExecutableFile(join(dir, name)))
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
function candidateCommandNames(command, env) {
|
|
130
|
+
if (process.platform !== "win32" || extname(command))
|
|
131
|
+
return [command];
|
|
132
|
+
const pathext = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
|
|
133
|
+
.split(";")
|
|
134
|
+
.map((extension) => extension.trim())
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
return [command, ...pathext.map((extension) => `${command}${extension}`)];
|
|
137
|
+
}
|
|
138
|
+
function isExecutableFile(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
const st = statSync(filePath);
|
|
141
|
+
if (!st.isFile())
|
|
142
|
+
return false;
|
|
143
|
+
if (process.platform !== "win32") {
|
|
144
|
+
accessSync(filePath, constants.X_OK);
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* One manager per workspace root.
|
|
154
|
+
*
|
|
155
|
+
* This used to be a single process-wide singleton that only existed if some host
|
|
156
|
+
* called `initializeLSPManager()` — and NO host ever did. The `LSP` tool was
|
|
157
|
+
* registered and advertised to the agent, but its first act is
|
|
158
|
+
* `getLSPManager()`, so every invocation returned "LSP is not initialized".
|
|
159
|
+
* The capability was dead on arrival in every product surface.
|
|
160
|
+
*
|
|
161
|
+
* Keying by root also fixes the design problem behind the singleton: an agent
|
|
162
|
+
* can work across several workspaces in one process (worktrees, sub-agents), and
|
|
163
|
+
* a language server is rooted at exactly one directory.
|
|
164
|
+
*/
|
|
165
|
+
const managers = new Map();
|
|
166
|
+
/**
|
|
167
|
+
* Use order, as a monotonic counter rather than a clock.
|
|
168
|
+
*
|
|
169
|
+
* `Date.now()` has millisecond resolution, so several workspaces touched in the
|
|
170
|
+
* same tick would tie and the LRU sort would pick an arbitrary victim — possibly
|
|
171
|
+
* the one being actively used.
|
|
172
|
+
*/
|
|
173
|
+
const useOrder = new Map();
|
|
174
|
+
let useTick = 0;
|
|
175
|
+
/**
|
|
176
|
+
* Most workspaces to keep language servers alive for at once.
|
|
177
|
+
*
|
|
178
|
+
* Per-workspace managers removed the single-cwd bug, but replaced it with a map
|
|
179
|
+
* that only ever grows: a long-lived Desktop session that touches many
|
|
180
|
+
* worktrees would accumulate a language server per root. Evicting the
|
|
181
|
+
* least-recently-used keeps the process bounded — an evicted workspace simply
|
|
182
|
+
* pays a cold start the next time it is used.
|
|
183
|
+
*/
|
|
184
|
+
const MAX_LIVE_WORKSPACES = 4;
|
|
185
|
+
export function initializeLSPManager(cwd) {
|
|
186
|
+
useOrder.set(cwd, ++useTick);
|
|
187
|
+
const existing = managers.get(cwd);
|
|
188
|
+
if (existing)
|
|
189
|
+
return existing;
|
|
190
|
+
const created = new LSPServerManager(cwd);
|
|
191
|
+
managers.set(cwd, created);
|
|
192
|
+
evictLeastRecentlyUsed();
|
|
193
|
+
return created;
|
|
194
|
+
}
|
|
195
|
+
/** Shut down the oldest workspaces once the live set exceeds its cap. */
|
|
196
|
+
function evictLeastRecentlyUsed() {
|
|
197
|
+
if (managers.size <= MAX_LIVE_WORKSPACES)
|
|
198
|
+
return;
|
|
199
|
+
const ordered = [...managers.keys()].sort((left, right) => (useOrder.get(left) ?? 0) - (useOrder.get(right) ?? 0));
|
|
200
|
+
for (const cwd of ordered.slice(0, managers.size - MAX_LIVE_WORKSPACES)) {
|
|
201
|
+
const manager = managers.get(cwd);
|
|
202
|
+
managers.delete(cwd);
|
|
203
|
+
useOrder.delete(cwd);
|
|
204
|
+
// Fire-and-forget: eviction must not make the caller wait on a shutdown
|
|
205
|
+
// handshake with a server it is no longer using.
|
|
206
|
+
void manager?.shutdownAll().catch(() => undefined);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Manager for `cwd`, creating it on first use.
|
|
211
|
+
*
|
|
212
|
+
* Lazy by design: spawning language servers eagerly at startup would cost every
|
|
213
|
+
* session a process it may never use. Callers that only want to observe existing
|
|
214
|
+
* state pass `{ create: false }`.
|
|
215
|
+
*/
|
|
216
|
+
export function getLSPManager(cwd, options = {}) {
|
|
217
|
+
if (cwd === undefined) {
|
|
218
|
+
// Back-compat for callers with no workspace context: return the sole manager
|
|
219
|
+
// when exactly one exists, otherwise nothing (ambiguous).
|
|
220
|
+
return managers.size === 1 ? [...managers.values()][0] : undefined;
|
|
221
|
+
}
|
|
222
|
+
const existing = managers.get(cwd);
|
|
223
|
+
if (existing) {
|
|
224
|
+
// Refresh recency on READ too — a workspace being actively used must not be
|
|
225
|
+
// the LRU victim just because it was created long ago.
|
|
226
|
+
useOrder.set(cwd, ++useTick);
|
|
227
|
+
return existing;
|
|
228
|
+
}
|
|
229
|
+
if (options.create === false)
|
|
230
|
+
return undefined;
|
|
231
|
+
return initializeLSPManager(cwd);
|
|
232
|
+
}
|
|
233
|
+
/** Shut down and forget one workspace's servers (session/workspace teardown). */
|
|
234
|
+
export async function shutdownLSPManager(cwd) {
|
|
235
|
+
const manager = managers.get(cwd);
|
|
236
|
+
if (!manager)
|
|
237
|
+
return;
|
|
238
|
+
managers.delete(cwd);
|
|
239
|
+
useOrder.delete(cwd);
|
|
240
|
+
await manager.shutdownAll();
|
|
241
|
+
}
|
|
242
|
+
/** Shut down every workspace's servers (process teardown). */
|
|
243
|
+
export async function shutdownAllLSPManagers() {
|
|
244
|
+
const all = [...managers.values()];
|
|
245
|
+
managers.clear();
|
|
246
|
+
useOrder.clear();
|
|
247
|
+
await Promise.all(all.map((manager) => manager.shutdownAll()));
|
|
248
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert an LSP `file://` root URI to a platform filesystem path.
|
|
3
|
+
*
|
|
4
|
+
* The LSP manager used `rootUri.replace("file://", "")`, which only strips the
|
|
5
|
+
* first occurrence and leaves percent-encoding intact, and on Windows turns
|
|
6
|
+
* `file:///C:/x` into the invalid `/C:/x`. fileURLToPath does the proper
|
|
7
|
+
* platform conversion (drive letters, percent-decoding, UNC).
|
|
8
|
+
*/
|
|
9
|
+
export declare function rootUriToPath(rootUri: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
/**
|
|
3
|
+
* Convert an LSP `file://` root URI to a platform filesystem path.
|
|
4
|
+
*
|
|
5
|
+
* The LSP manager used `rootUri.replace("file://", "")`, which only strips the
|
|
6
|
+
* first occurrence and leaves percent-encoding intact, and on Windows turns
|
|
7
|
+
* `file:///C:/x` into the invalid `/C:/x`. fileURLToPath does the proper
|
|
8
|
+
* platform conversion (drive letters, percent-decoding, UNC).
|
|
9
|
+
*/
|
|
10
|
+
export function rootUriToPath(rootUri) {
|
|
11
|
+
return fileURLToPath(rootUri);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding capability language server configurations.
|
|
3
|
+
*/
|
|
4
|
+
export interface LSPServerConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
language: string;
|
|
7
|
+
extensions: string[];
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
installHint: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const BUILTIN_LSP_SERVERS: LSPServerConfig[];
|
|
13
|
+
/**
|
|
14
|
+
* Detect which LSP server to use based on file extension.
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectLSPServer(filePath: string): LSPServerConfig | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding capability language server configurations.
|
|
3
|
+
*/
|
|
4
|
+
export const BUILTIN_LSP_SERVERS = [
|
|
5
|
+
{
|
|
6
|
+
name: "typescript",
|
|
7
|
+
language: "typescript",
|
|
8
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
9
|
+
command: "typescript-language-server",
|
|
10
|
+
args: ["--stdio"],
|
|
11
|
+
installHint: "npm install -g typescript-language-server typescript",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "python",
|
|
15
|
+
language: "python",
|
|
16
|
+
extensions: [".py"],
|
|
17
|
+
command: "pylsp",
|
|
18
|
+
args: [],
|
|
19
|
+
installHint: "pip install python-lsp-server",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "go",
|
|
23
|
+
language: "go",
|
|
24
|
+
extensions: [".go"],
|
|
25
|
+
command: "gopls",
|
|
26
|
+
args: ["serve"],
|
|
27
|
+
installHint: "go install golang.org/x/tools/gopls@latest",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "rust",
|
|
31
|
+
language: "rust",
|
|
32
|
+
extensions: [".rs"],
|
|
33
|
+
command: "rust-analyzer",
|
|
34
|
+
args: [],
|
|
35
|
+
installHint: "rustup component add rust-analyzer",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "json",
|
|
39
|
+
language: "json",
|
|
40
|
+
extensions: [".json"],
|
|
41
|
+
command: "vscode-json-languageserver",
|
|
42
|
+
args: ["--stdio"],
|
|
43
|
+
installHint: "npm install -g vscode-langservers-extracted",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "css",
|
|
47
|
+
language: "css",
|
|
48
|
+
extensions: [".css", ".scss", ".less"],
|
|
49
|
+
command: "vscode-css-languageserver",
|
|
50
|
+
args: ["--stdio"],
|
|
51
|
+
installHint: "npm install -g vscode-langservers-extracted",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Detect which LSP server to use based on file extension.
|
|
56
|
+
*/
|
|
57
|
+
export function detectLSPServer(filePath) {
|
|
58
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
59
|
+
return BUILTIN_LSP_SERVERS.find((s) => s.extensions.includes(ext));
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Coding assistant capability
|
|
2
|
+
|
|
3
|
+
The default job in this preset is software engineering in the current working directory.
|
|
4
|
+
|
|
5
|
+
## Reading before writing
|
|
6
|
+
|
|
7
|
+
- Do not propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications.
|
|
8
|
+
- Read code before editing it, prefer modifying existing files, and avoid unrelated refactors.
|
|
9
|
+
|
|
10
|
+
## Investigation has a budget — looking is not acting
|
|
11
|
+
|
|
12
|
+
- Read, search, and investigate freely, but treat read-only exploration as a means to action, not an end in itself. After about 3 read-only tool calls (Read/Grep/Glob) on the same question, you should either (a) make a change, (b) run a command with side effects (Bash, debug log, repro), or (c) ask the user a specific question. Continuing to read more files usually means your current strategy isn't working — change strategy instead.
|
|
13
|
+
- Never re-read a file (or the same line range of a file) you've already read in this conversation. The content is already in your context. If you've re-derived the same conclusion twice from the same code, the answer isn't in static reading — switch to runtime verification.
|
|
14
|
+
- For bug reports where static analysis hasn't pinpointed the cause in 2-3 reads, the right next step is almost always to add a debug log line and ask the user to reproduce, or to inspect runtime state — not to read more files.
|
|
15
|
+
- Don't narrate the same code path twice in your reasoning. If you've already traced "input X → parser Y → handler Z" once and concluded "this looks correct", stop. Do not re-trace it from a different angle hoping for a different answer. Trust your first analysis and move to verification.
|
|
16
|
+
- These rules are enforced at runtime by an investigation guard. A second read of the same target prepends a `<system-reminder>` to that tool's result; a third read of the same target is hard-blocked with an error. Four or more consecutive read-only calls without a side-effecting action also surface a reminder, and several consecutive turns of read-only work with no text update to the user surface another. When you see one of these reminders, treat it as a signal that your current strategy isn't producing new information — switch to a side-effecting verification (Bash, debug log, repro, edit) or ask the user a specific question rather than reading more.
|
|
17
|
+
|
|
18
|
+
## Code quality discipline
|
|
19
|
+
|
|
20
|
+
- Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.
|
|
21
|
+
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.
|
|
22
|
+
- Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is what the task actually requires — no speculative abstractions, but no half-finished implementations either. Three similar lines of code is better than a premature abstraction.
|
|
23
|
+
- Avoid backwards-compatibility hacks like renaming unused \_vars, re-exporting types, adding // removed comments for removed code, etc. If you are certain that something is unused, you can delete it completely.
|
|
24
|
+
|
|
25
|
+
## Security
|
|
26
|
+
|
|
27
|
+
- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it. Prioritize writing safe, secure, and correct code.
|
|
28
|
+
|
|
29
|
+
## Git
|
|
30
|
+
|
|
31
|
+
- When referencing specific functions or pieces of code include the pattern file_path:line_number to allow the user to easily navigate to the source code location.
|
|
32
|
+
- When referencing GitHub issues or pull requests, use the owner/repo#123 format so they render as clickable links.
|
|
33
|
+
|
|
34
|
+
## Opening URLs and local previews
|
|
35
|
+
|
|
36
|
+
- To show the user a web page or a local dev-server preview, print the address in your reply as a bare URL or a markdown link (e.g. `http://localhost:5173` or [预览](http://localhost:5173)) — do NOT wrap it in backticks. The desktop app turns a link in your answer into a clickable link that opens inside its built-in browser panel; backtick-wrapped URLs render as plain code and are not clickable.
|
|
37
|
+
- Do NOT run shell commands to open a URL (`open <url>` on macOS, `xdg-open` on Linux, `start <url>` on Windows). Those launch the user's external system browser and bypass the in-app browser panel. Start the dev server, then just tell the user the address as a link.
|
|
38
|
+
|
|
39
|
+
## Coding tools
|
|
40
|
+
|
|
41
|
+
- Use git/worktree/LSP/notebook tools when they help, but treat them as optional helpers rather than core assumptions.
|
|
42
|
+
- If the user asks for help or feedback, point them to /help and the project repository.
|