@dyyz1993/pi-coding-agent 0.74.24 → 0.74.27
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/CHANGELOG.md +9 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/session-manager.d.ts +5 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +8 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +310 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +446 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface ServerMetricsSnapshot {
|
|
4
|
+
name: string;
|
|
5
|
+
pid: number | undefined;
|
|
6
|
+
fileTypes: string[];
|
|
7
|
+
state: "started" | "ready" | "stopped" | "error";
|
|
8
|
+
startupDurationMs: number | undefined;
|
|
9
|
+
memoryRssKb: number | undefined;
|
|
10
|
+
requestCount: number;
|
|
11
|
+
notifyCount: number;
|
|
12
|
+
lastActivityAt: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ServerMetricsState {
|
|
16
|
+
name: string;
|
|
17
|
+
pid: number | undefined;
|
|
18
|
+
fileTypes: string[];
|
|
19
|
+
state: "started" | "ready" | "stopped" | "error";
|
|
20
|
+
startedAt: number | undefined;
|
|
21
|
+
readyAt: number | undefined;
|
|
22
|
+
stoppedAt: number | undefined;
|
|
23
|
+
requestCount: number;
|
|
24
|
+
notifyCount: number;
|
|
25
|
+
lastActivityAt: number | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ServerMetricsCollector {
|
|
29
|
+
onStarting(name: string, fileTypes: string[]): void;
|
|
30
|
+
onReady(name: string, pid: number | undefined): void;
|
|
31
|
+
onStop(name: string): void;
|
|
32
|
+
onError(name: string): void;
|
|
33
|
+
onRequest(name: string): void;
|
|
34
|
+
onNotify(name: string): void;
|
|
35
|
+
snapshot(): ServerMetricsSnapshot[];
|
|
36
|
+
summary(): string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createServerMetricsCollector(): ServerMetricsCollector {
|
|
40
|
+
const servers = new Map<string, ServerMetricsState>();
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
onStarting(name: string, fileTypes: string[]): void {
|
|
44
|
+
servers.set(name, {
|
|
45
|
+
name,
|
|
46
|
+
pid: undefined,
|
|
47
|
+
fileTypes,
|
|
48
|
+
state: "started",
|
|
49
|
+
startedAt: Date.now(),
|
|
50
|
+
readyAt: undefined,
|
|
51
|
+
stoppedAt: undefined,
|
|
52
|
+
requestCount: 0,
|
|
53
|
+
notifyCount: 0,
|
|
54
|
+
lastActivityAt: undefined,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
onReady(name: string, pid: number | undefined): void {
|
|
59
|
+
const entry = servers.get(name);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
entry.state = "ready";
|
|
64
|
+
entry.pid = pid;
|
|
65
|
+
entry.readyAt = Date.now();
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
onStop(name: string): void {
|
|
69
|
+
const entry = servers.get(name);
|
|
70
|
+
if (!entry) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
entry.state = "stopped";
|
|
74
|
+
entry.stoppedAt = Date.now();
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
onError(name: string): void {
|
|
78
|
+
const entry = servers.get(name);
|
|
79
|
+
if (!entry) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
entry.state = "error";
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
onRequest(name: string): void {
|
|
86
|
+
const entry = servers.get(name);
|
|
87
|
+
if (!entry) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
entry.requestCount += 1;
|
|
91
|
+
entry.lastActivityAt = Date.now();
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
onNotify(name: string): void {
|
|
95
|
+
const entry = servers.get(name);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
entry.notifyCount += 1;
|
|
100
|
+
entry.lastActivityAt = Date.now();
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
snapshot(): ServerMetricsSnapshot[] {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
return [...servers.values()].map((entry) => ({
|
|
106
|
+
name: entry.name,
|
|
107
|
+
pid: entry.pid,
|
|
108
|
+
fileTypes: entry.fileTypes,
|
|
109
|
+
state: entry.state,
|
|
110
|
+
startupDurationMs:
|
|
111
|
+
entry.readyAt && entry.startedAt ? entry.readyAt - entry.startedAt : undefined,
|
|
112
|
+
memoryRssKb: entry.pid ? getProcessMemoryKb(entry.pid) : undefined,
|
|
113
|
+
requestCount: entry.requestCount,
|
|
114
|
+
notifyCount: entry.notifyCount,
|
|
115
|
+
lastActivityAt: entry.lastActivityAt,
|
|
116
|
+
}));
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
summary(): string {
|
|
120
|
+
const snapshots = this.snapshot();
|
|
121
|
+
if (snapshots.length === 0) {
|
|
122
|
+
return "[lsp-metrics] No servers configured.";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const lines: string[] = ["[lsp-metrics] Session summary:", ""];
|
|
126
|
+
|
|
127
|
+
for (const snap of snapshots) {
|
|
128
|
+
const types = snap.fileTypes.length > 0 ? snap.fileTypes.join(", ") : "*";
|
|
129
|
+
const startup = snap.startupDurationMs !== undefined ? `${snap.startupDurationMs}ms` : "n/a";
|
|
130
|
+
const mem = snap.memoryRssKb !== undefined ? `${Math.round(snap.memoryRssKb / 1024)}MB` : "n/a";
|
|
131
|
+
const totalOps = snap.requestCount + snap.notifyCount;
|
|
132
|
+
const idleTime = snap.lastActivityAt ? `${Math.round((Date.now() - snap.lastActivityAt) / 1000)}s ago` : "never used";
|
|
133
|
+
const pid = snap.pid ?? "n/a";
|
|
134
|
+
|
|
135
|
+
lines.push(` ${snap.name} [${types}]`);
|
|
136
|
+
lines.push(` state: ${snap.state} pid: ${pid} startup: ${startup} memory: ${mem}`);
|
|
137
|
+
lines.push(` requests: ${snap.requestCount} notifications: ${snap.notifyCount} total: ${totalOps} last: ${idleTime}`);
|
|
138
|
+
lines.push("");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const totalMemory = snapshots.reduce((sum, s) => sum + (s.memoryRssKb ?? 0), 0);
|
|
142
|
+
const totalOps = snapshots.reduce((sum, s) => sum + s.requestCount + s.notifyCount, 0);
|
|
143
|
+
const unusedServers = snapshots.filter((s) => s.requestCount + s.notifyCount === 0);
|
|
144
|
+
|
|
145
|
+
lines.push(` Total: ${snapshots.length} servers, ${Math.round(totalMemory / 1024)}MB RSS, ${totalOps} ops`);
|
|
146
|
+
if (unusedServers.length > 0) {
|
|
147
|
+
lines.push(
|
|
148
|
+
` Unused: ${unusedServers.map((s) => s.name).join(", ")} (${unusedServers.length}/${snapshots.length} servers received zero requests)`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getProcessMemoryKb(pid: number): number | undefined {
|
|
158
|
+
try {
|
|
159
|
+
if (process.platform === "darwin") {
|
|
160
|
+
const output = execSync(`ps -p ${pid} -o rss=`, { timeout: 2000, encoding: "utf8" }).trim();
|
|
161
|
+
const kb = Number.parseInt(output, 10);
|
|
162
|
+
return Number.isFinite(kb) ? kb : undefined;
|
|
163
|
+
}
|
|
164
|
+
if (process.platform === "linux") {
|
|
165
|
+
const stat = execSync(`cat /proc/${pid}/status 2>/dev/null | grep VmRSS`, {
|
|
166
|
+
timeout: 2000,
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
}).trim();
|
|
169
|
+
const match = /(\d+)\s*kB/i.exec(stat);
|
|
170
|
+
return match ? Number.parseInt(match[1], 10) : undefined;
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { readFile as fsReadFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
5
|
+
import type { ExtensionAPI } from "@dyyz1993/pi-coding-agent";
|
|
6
|
+
import type { LspRuntimeRegistry } from "../client/registry.js";
|
|
7
|
+
import type { ResolvedLspConfig } from "../config/resolver.js";
|
|
8
|
+
import { waitForPushDiagnostics } from "../utils/diagnostics-wait.js";
|
|
9
|
+
import { extractPullDiagnostics, languageIdFromPath } from "../utils/lsp-helpers.js";
|
|
10
|
+
|
|
11
|
+
export interface LspToolRouter {
|
|
12
|
+
register(pi: ExtensionAPI): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LspToolRouterOptions {
|
|
16
|
+
cwd?: string;
|
|
17
|
+
getResolvedConfig: () => ResolvedLspConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LspActionSchema = Type.Union([
|
|
21
|
+
Type.Literal("diagnostics"),
|
|
22
|
+
Type.Literal("definition"),
|
|
23
|
+
Type.Literal("references"),
|
|
24
|
+
Type.Literal("hover"),
|
|
25
|
+
Type.Literal("symbols"),
|
|
26
|
+
Type.Literal("rename"),
|
|
27
|
+
Type.Literal("status"),
|
|
28
|
+
Type.Literal("reload"),
|
|
29
|
+
Type.Literal("full_check"),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const LspToolSchema = Type.Object({
|
|
33
|
+
action: LspActionSchema,
|
|
34
|
+
path: Type.Optional(Type.String({ description: "File path (relative or absolute) for document-scoped actions" })),
|
|
35
|
+
line: Type.Optional(Type.Number({ description: "Zero-based line for position-based actions" })),
|
|
36
|
+
character: Type.Optional(Type.Number({ description: "Zero-based character for position-based actions" })),
|
|
37
|
+
newName: Type.Optional(Type.String({ description: "New symbol name for rename action" })),
|
|
38
|
+
query: Type.Optional(Type.String({ description: "Workspace query for symbols action" })),
|
|
39
|
+
includeDeclaration: Type.Optional(
|
|
40
|
+
Type.Boolean({ description: "Whether references action should include declaration locations" }),
|
|
41
|
+
),
|
|
42
|
+
directory: Type.Optional(Type.String({ description: "Directory to scan for full_check action (defaults to cwd)" })),
|
|
43
|
+
maxFiles: Type.Optional(Type.Number({ description: "Maximum files to check in full_check (default 50)" })),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
type LspToolParams = Static<typeof LspToolSchema>;
|
|
47
|
+
|
|
48
|
+
interface LspToolDetails {
|
|
49
|
+
action: LspToolParams["action"];
|
|
50
|
+
payload?: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createLspToolRouter(runtime: LspRuntimeRegistry, options: LspToolRouterOptions): LspToolRouter {
|
|
54
|
+
const cwd = options.cwd ?? process.cwd();
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
register(pi: ExtensionAPI): void {
|
|
58
|
+
pi.registerTool({
|
|
59
|
+
name: "lsp",
|
|
60
|
+
label: "LSP",
|
|
61
|
+
description:
|
|
62
|
+
"Run LSP actions (diagnostics, definition, references, hover, symbols, rename, status, reload, full_check)",
|
|
63
|
+
parameters: LspToolSchema,
|
|
64
|
+
execute: async (_toolCallId: string, params: LspToolParams) => {
|
|
65
|
+
const details = await executeAction(runtime, params, cwd, options.getResolvedConfig);
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: renderDetails(details) }],
|
|
68
|
+
details,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
pi.registerTool({
|
|
74
|
+
name: "lsp_health",
|
|
75
|
+
label: "LSP Health",
|
|
76
|
+
description: "Backward-compatible health status shortcut for the LSP extension package",
|
|
77
|
+
parameters: Type.Object({}),
|
|
78
|
+
execute: async () => {
|
|
79
|
+
const details = await executeAction(runtime, { action: "status" }, cwd, options.getResolvedConfig);
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: renderDetails(details) }],
|
|
82
|
+
details,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function executeAction(
|
|
91
|
+
runtime: LspRuntimeRegistry,
|
|
92
|
+
params: LspToolParams,
|
|
93
|
+
cwd: string,
|
|
94
|
+
getResolvedConfig: () => ResolvedLspConfig,
|
|
95
|
+
): Promise<LspToolDetails> {
|
|
96
|
+
switch (params.action) {
|
|
97
|
+
case "status": {
|
|
98
|
+
const status = runtime.getStatus();
|
|
99
|
+
return { action: "status", payload: status };
|
|
100
|
+
}
|
|
101
|
+
case "reload": {
|
|
102
|
+
await runtime.reload(getResolvedConfig());
|
|
103
|
+
return { action: "reload", payload: runtime.getStatus() };
|
|
104
|
+
}
|
|
105
|
+
case "diagnostics": {
|
|
106
|
+
if (params.path) {
|
|
107
|
+
const uri = toFileUri(params.path, cwd);
|
|
108
|
+
const fullPath = resolve(cwd, params.path);
|
|
109
|
+
try {
|
|
110
|
+
const content = await fsReadFile(fullPath, "utf8");
|
|
111
|
+
runtime.notify(
|
|
112
|
+
"textDocument/didOpen",
|
|
113
|
+
{
|
|
114
|
+
textDocument: {
|
|
115
|
+
uri,
|
|
116
|
+
languageId: languageIdFromPath(params.path),
|
|
117
|
+
version: Date.now(),
|
|
118
|
+
text: content,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{ path: params.path },
|
|
122
|
+
);
|
|
123
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
124
|
+
} catch (openError) {
|
|
125
|
+
return { action: "diagnostics", payload: { error: String(openError), hint: "didOpen failed" } };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let diagnostics = runtime.getPublishedDiagnostics(params.path);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const results = await runtime.requestAll(
|
|
132
|
+
"textDocument/diagnostic",
|
|
133
|
+
{ textDocument: { uri } },
|
|
134
|
+
{ path: params.path, timeoutMs: 8000 },
|
|
135
|
+
);
|
|
136
|
+
for (const result of results) {
|
|
137
|
+
if (!result) continue;
|
|
138
|
+
const pulled = extractPullDiagnostics(result);
|
|
139
|
+
if (pulled.length > 0) {
|
|
140
|
+
diagnostics = diagnostics.concat(pulled);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.debug("[lsp] pull diagnostics failed:", err instanceof Error ? err.message : err);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
action: "diagnostics",
|
|
149
|
+
payload: diagnostics.length > 0 ? diagnostics : { uri, hint: "no diagnostics" },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return { action: "diagnostics", payload: runtime.getPublishedDiagnostics() };
|
|
153
|
+
}
|
|
154
|
+
case "hover": {
|
|
155
|
+
const position = requirePosition(params, cwd, "hover");
|
|
156
|
+
const payload = await runtime.request(
|
|
157
|
+
"textDocument/hover",
|
|
158
|
+
{
|
|
159
|
+
textDocument: { uri: position.uri },
|
|
160
|
+
position: { line: position.line, character: position.character },
|
|
161
|
+
},
|
|
162
|
+
{ path: position.path },
|
|
163
|
+
);
|
|
164
|
+
return { action: "hover", payload };
|
|
165
|
+
}
|
|
166
|
+
case "definition": {
|
|
167
|
+
const position = requirePosition(params, cwd, "definition");
|
|
168
|
+
const payload = await runtime.request(
|
|
169
|
+
"textDocument/definition",
|
|
170
|
+
{
|
|
171
|
+
textDocument: { uri: position.uri },
|
|
172
|
+
position: { line: position.line, character: position.character },
|
|
173
|
+
},
|
|
174
|
+
{ path: position.path },
|
|
175
|
+
);
|
|
176
|
+
return { action: "definition", payload };
|
|
177
|
+
}
|
|
178
|
+
case "references": {
|
|
179
|
+
const position = requirePosition(params, cwd, "references");
|
|
180
|
+
const payload = await runtime.request(
|
|
181
|
+
"textDocument/references",
|
|
182
|
+
{
|
|
183
|
+
textDocument: { uri: position.uri },
|
|
184
|
+
position: { line: position.line, character: position.character },
|
|
185
|
+
context: {
|
|
186
|
+
includeDeclaration: params.includeDeclaration ?? false,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{ path: position.path },
|
|
190
|
+
);
|
|
191
|
+
return { action: "references", payload };
|
|
192
|
+
}
|
|
193
|
+
case "symbols": {
|
|
194
|
+
if (params.query) {
|
|
195
|
+
const payload = await runtime.request("workspace/symbol", {
|
|
196
|
+
query: params.query,
|
|
197
|
+
});
|
|
198
|
+
return { action: "symbols", payload };
|
|
199
|
+
}
|
|
200
|
+
if (!params.path) {
|
|
201
|
+
throw new Error("symbols action requires either query or path.");
|
|
202
|
+
}
|
|
203
|
+
const uri = toFileUri(params.path, cwd);
|
|
204
|
+
const payload = await runtime.request(
|
|
205
|
+
"textDocument/documentSymbol",
|
|
206
|
+
{
|
|
207
|
+
textDocument: { uri },
|
|
208
|
+
},
|
|
209
|
+
{ path: params.path },
|
|
210
|
+
);
|
|
211
|
+
return { action: "symbols", payload };
|
|
212
|
+
}
|
|
213
|
+
case "rename": {
|
|
214
|
+
if (!params.newName) {
|
|
215
|
+
throw new Error("rename action requires newName.");
|
|
216
|
+
}
|
|
217
|
+
const position = requirePosition(params, cwd, "rename");
|
|
218
|
+
const payload = await runtime.request(
|
|
219
|
+
"textDocument/rename",
|
|
220
|
+
{
|
|
221
|
+
textDocument: { uri: position.uri },
|
|
222
|
+
position: { line: position.line, character: position.character },
|
|
223
|
+
newName: params.newName,
|
|
224
|
+
},
|
|
225
|
+
{ path: position.path },
|
|
226
|
+
);
|
|
227
|
+
return { action: "rename", payload };
|
|
228
|
+
}
|
|
229
|
+
case "full_check": {
|
|
230
|
+
const scanDir = params.directory ? resolve(cwd, params.directory) : cwd;
|
|
231
|
+
const maxFiles = params.maxFiles ?? 50;
|
|
232
|
+
const files = await collectSourceFiles(scanDir, maxFiles);
|
|
233
|
+
if (files.length === 0) {
|
|
234
|
+
return { action: "full_check", payload: { hint: "no source files found", directory: scanDir } };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const results: Array<{ filePath: string; errorCount: number; warningCount: number; errors: Array<{ line: number; message: string }> }> = [];
|
|
238
|
+
let totalErrors = 0;
|
|
239
|
+
let totalWarnings = 0;
|
|
240
|
+
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
try {
|
|
243
|
+
const relPath = relative(cwd, file);
|
|
244
|
+
const uri = pathToFileURL(file).href;
|
|
245
|
+
const content = await fsReadFile(file, "utf8");
|
|
246
|
+
|
|
247
|
+
runtime.notify(
|
|
248
|
+
"textDocument/didOpen",
|
|
249
|
+
{
|
|
250
|
+
textDocument: {
|
|
251
|
+
uri,
|
|
252
|
+
languageId: languageIdFromPath(file),
|
|
253
|
+
version: Date.now(),
|
|
254
|
+
text: content,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{ path: relPath },
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
await waitForPushDiagnostics(runtime, relPath);
|
|
261
|
+
|
|
262
|
+
let diagnostics = runtime.getPublishedDiagnostics(relPath);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const pullResults = await runtime.requestAll(
|
|
266
|
+
"textDocument/diagnostic",
|
|
267
|
+
{ textDocument: { uri } },
|
|
268
|
+
{ path: relPath, timeoutMs: 5000 },
|
|
269
|
+
);
|
|
270
|
+
for (const result of pullResults) {
|
|
271
|
+
if (!result) continue;
|
|
272
|
+
const pulled = extractPullDiagnostics(result);
|
|
273
|
+
if (pulled.length > 0) {
|
|
274
|
+
diagnostics = diagnostics.concat(pulled);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// pull diagnostics optional
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const errors = diagnostics.filter((d) => d.severity === 1);
|
|
282
|
+
const warnings = diagnostics.filter((d) => d.severity === 2);
|
|
283
|
+
totalErrors += errors.length;
|
|
284
|
+
totalWarnings += warnings.length;
|
|
285
|
+
|
|
286
|
+
if (errors.length > 0) {
|
|
287
|
+
results.push({
|
|
288
|
+
filePath: relPath,
|
|
289
|
+
errorCount: errors.length,
|
|
290
|
+
warningCount: warnings.length,
|
|
291
|
+
errors: errors.slice(0, 5).map((d) => ({
|
|
292
|
+
line: d.range.start.line + 1,
|
|
293
|
+
message: d.message,
|
|
294
|
+
})),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
action: "full_check",
|
|
304
|
+
payload: {
|
|
305
|
+
scannedFiles: files.length,
|
|
306
|
+
totalErrors,
|
|
307
|
+
totalWarnings,
|
|
308
|
+
filesWithErrors: results,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function requirePosition(
|
|
316
|
+
params: LspToolParams,
|
|
317
|
+
cwd: string,
|
|
318
|
+
action: "hover" | "definition" | "references" | "rename",
|
|
319
|
+
): { path: string; uri: string; line: number; character: number } {
|
|
320
|
+
if (!params.path) {
|
|
321
|
+
throw new Error(`${action} action requires path.`);
|
|
322
|
+
}
|
|
323
|
+
if (typeof params.line !== "number" || typeof params.character !== "number") {
|
|
324
|
+
throw new Error(`${action} action requires line and character.`);
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
path: params.path,
|
|
328
|
+
uri: toFileUri(params.path, cwd),
|
|
329
|
+
line: params.line,
|
|
330
|
+
character: params.character,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function toFileUri(filePath: string, cwd: string): string {
|
|
335
|
+
return pathToFileURL(resolve(cwd, filePath)).href;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const MAX_RENDERED_DETAILS_CHARS = 40_000;
|
|
339
|
+
|
|
340
|
+
function renderDetails(details: LspToolDetails): string {
|
|
341
|
+
const header = `LSP action: ${details.action}`;
|
|
342
|
+
if (details.payload === undefined) {
|
|
343
|
+
return header;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const renderedPayload = safeJsonStringify(details.payload, MAX_RENDERED_DETAILS_CHARS);
|
|
347
|
+
if (!renderedPayload) {
|
|
348
|
+
return header;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return `${header}\n${renderedPayload}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function safeJsonStringify(payload: unknown, maxChars: number): string {
|
|
355
|
+
let rendered: string;
|
|
356
|
+
try {
|
|
357
|
+
rendered = JSON.stringify(payload, null, 2) ?? "";
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
360
|
+
rendered = `"<unserializable payload: ${message}>"`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (rendered.length <= maxChars) {
|
|
364
|
+
return rendered;
|
|
365
|
+
}
|
|
366
|
+
return `${rendered.slice(0, maxChars)}\n... (truncated at ${maxChars} chars)`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs", ".go"]);
|
|
370
|
+
const SKIP_SCAN_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", "coverage"]);
|
|
371
|
+
|
|
372
|
+
async function collectSourceFiles(dir: string, maxFiles: number): Promise<string[]> {
|
|
373
|
+
const files: string[] = [];
|
|
374
|
+
|
|
375
|
+
async function walk(currentDir: string, depth: number): Promise<void> {
|
|
376
|
+
if (files.length >= maxFiles || depth > 8) return;
|
|
377
|
+
|
|
378
|
+
let entries;
|
|
379
|
+
try {
|
|
380
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
381
|
+
} catch {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const entry of entries) {
|
|
386
|
+
if (files.length >= maxFiles) return;
|
|
387
|
+
|
|
388
|
+
if (entry.isDirectory()) {
|
|
389
|
+
if (SKIP_SCAN_DIRS.has(entry.name)) continue;
|
|
390
|
+
await walk(join(currentDir, entry.name), depth + 1);
|
|
391
|
+
} else if (entry.isFile()) {
|
|
392
|
+
const ext = extname(entry.name).toLowerCase();
|
|
393
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
394
|
+
files.push(join(currentDir, entry.name));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
await walk(dir, 0);
|
|
401
|
+
return files;
|
|
402
|
+
}
|