decorated-pi 0.7.1 → 0.7.3

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.
@@ -2,9 +2,9 @@
2
2
  * LSP Server Config — language detection, server commands, workspace roots.
3
3
  */
4
4
  import { existsSync, readdirSync, type Dirent } from "node:fs";
5
- import { spawnSync } from "node:child_process";
6
5
  import { dirname, extname, isAbsolute, join, resolve } from "node:path";
7
6
  import type { DependencyStatus } from "../../hooks/skeleton.js";
7
+ import { getDependencyPath, resolveDependency } from "../../settings.js";
8
8
 
9
9
  // ─── File extension → language mapping ────────────────────────────────────
10
10
 
@@ -97,6 +97,16 @@ export function listSupportedLanguages(): string[] {
97
97
  return Object.keys(LANGUAGE_SERVERS).sort();
98
98
  }
99
99
 
100
+ /** Unique binary names used by builtin LSP servers. Used by the
101
+ * /dp-settings Dependencies UI to know which binaries are configurable. */
102
+ export function listLspBinaryNames(): string[] {
103
+ const seen = new Set<string>();
104
+ for (const lang of Object.keys(LANGUAGE_SERVERS)) {
105
+ seen.add(LANGUAGE_SERVERS[lang].command);
106
+ }
107
+ return [...seen].sort();
108
+ }
109
+
100
110
  export function getServerConfig(
101
111
  language: string,
102
112
  cwd = process.cwd(),
@@ -104,6 +114,12 @@ export function getServerConfig(
104
114
  const base = LANGUAGE_SERVERS[language];
105
115
  if (!base) return undefined;
106
116
 
117
+ const override = getDependencyPath(base.command);
118
+ if (override) {
119
+ const resolvedOverride = resolveDependency(base.command, { extendPath: [] });
120
+ return { ...base, command: resolvedOverride ?? override, is_project_local: false };
121
+ }
122
+
107
123
  const resolved = resolveLocalBinary(base.command, cwd);
108
124
  return { ...base, command: resolved.command, is_project_local: resolved.is_project_local };
109
125
  }
@@ -147,14 +163,16 @@ export function collectLspDependencyStatuses(cwd: string): DependencyStatus[] {
147
163
  const statuses: DependencyStatus[] = [];
148
164
  const seen = new Set<string>();
149
165
  for (const language of listSupportedLanguages()) {
150
- const cfg = getServerConfig(language, cwd);
151
- if (!cfg || seen.has(cfg.command)) continue;
152
- seen.add(cfg.command);
166
+ const base = LANGUAGE_SERVERS[language];
167
+ if (!base || seen.has(base.command)) continue;
168
+ seen.add(base.command);
169
+ const path = resolveLspBinary(base.command, cwd);
153
170
  statuses.push({
154
171
  module: "lsp",
155
- label: cfg.command,
156
- state: commandExists(cfg.command) ? "ok" : "missing",
157
- detail: cfg.install_hint,
172
+ label: base.command,
173
+ state: path ? "ok" : "missing",
174
+ detail: base.install_hint,
175
+ path: path ?? undefined,
158
176
  });
159
177
  }
160
178
  return statuses;
@@ -174,14 +192,12 @@ export function findWorkspaceRoot(
174
192
 
175
193
  // ─── Internal helpers ─────────────────────────────────────────────────────
176
194
 
177
- function commandExists(command: string): boolean {
178
- if (isAbsolute(command) || command.includes("/") || command.includes("\\")) {
179
- return existsSync(command);
180
- }
181
- const result = process.platform === "win32"
182
- ? spawnSync("where", [command], { encoding: "utf-8" })
183
- : spawnSync(process.env.SHELL || "sh", ["-lc", `command -v '${command.replace(/'/g, `'"'"'`)}'`], { encoding: "utf-8" });
184
- return result.status === 0;
195
+ export function lspDependencyExtendPath(cwd = process.cwd()): string[] {
196
+ return [...ancestorDirs(cwd)].map((dir) => join(dir, "node_modules", ".bin"));
197
+ }
198
+
199
+ export function resolveLspBinary(command: string, cwd = process.cwd()): string | null {
200
+ return resolveDependency(command, { extendPath: lspDependencyExtendPath(cwd) });
185
201
  }
186
202
 
187
203
  function resolveLocalBinary(
@@ -69,7 +69,6 @@ function severityLabel(s: number): string {
69
69
  // ─── Register tools ───────────────────────────────────────────────────────
70
70
 
71
71
  export function registerLspTools(pi: ExtensionAPI, manager: LspServerManager) {
72
-
73
72
  // ── lsp_diagnostics ────────────────────────────────────────────────────
74
73
  pi.registerTool({
75
74
  name: "lsp_diagnostics",
@@ -14,7 +14,7 @@ export const CODEGRAPH_BUILTIN: Omit<McpServerConfig, "source"> = {
14
14
  name: "codegraph",
15
15
  command: "codegraph",
16
16
  args: ["serve", "--mcp"],
17
- enabled: false,
17
+ enabled: true,
18
18
  description:
19
19
  "Local code knowledge graph (colbymchenry/codegraph). Enable via /mcp.",
20
20
  canUseInProject: (cwd: string) => fs.existsSync(path.join(cwd, ".codegraph")),
@@ -16,7 +16,7 @@ export class McpConnection {
16
16
  client: Client;
17
17
  transport: StreamableHTTPClientTransport | SSEClientTransport | StdioClientTransport | undefined;
18
18
  tools: McpToolSpec[] = [];
19
- private connected = false;
19
+ connected = false;
20
20
 
21
21
  source: string = "unknown";
22
22
 
@@ -15,11 +15,11 @@
15
15
  import * as fs from "node:fs";
16
16
  import * as os from "node:os";
17
17
  import * as path from "node:path";
18
- import { spawnSync } from "node:child_process";
19
- import { isModuleEnabled } from "../../settings.js";
18
+ import { isModuleEnabled, resolveDependency } from "../../settings.js";
20
19
  import type { DependencyStatus } from "../../hooks/skeleton.js";
21
20
  import { BUILTIN_MCP_SERVERS } from "./builtin/index.js";
22
21
 
22
+
23
23
  export { BUILTIN_MCP_SERVERS } from "./builtin/index.js";
24
24
 
25
25
  export interface McpServerConfig {
@@ -33,6 +33,8 @@ export interface McpServerConfig {
33
33
  source: "builtin" | "global" | "project";
34
34
  /** Optional predicate: return false if this server cannot be used in the given project. */
35
35
  canUseInProject?: (cwd: string) => boolean;
36
+ /** Binary/config key before dependency path resolution. */
37
+ dependencyName?: string;
36
38
  }
37
39
 
38
40
  function globalMcpJsonPath(): string {
@@ -217,33 +219,50 @@ export function resolveMcpConfigs(cwd: string): McpServerConfig[] {
217
219
  }
218
220
  }
219
221
 
220
- return [...byName.values()].filter((s) => s.url || s.command);
222
+ return [...byName.values()]
223
+ .filter((s) => s.url || s.command)
224
+ .map((s) => {
225
+ // Apply user-configured binary path override (dependencies[cmd].path)
226
+ // last, so it wins over builtin/global/project configs. Only applies
227
+ // to servers that use a command (not URL-based servers).
228
+ if (!s.command) return s;
229
+ const dependencyName = s.command;
230
+ const resolved = resolveMcpBinary(dependencyName);
231
+ return { ...s, dependencyName, command: resolved ?? s.command };
232
+ });
233
+ }
234
+
235
+ /** Unique binary names used by builtin MCP servers that have a `command`
236
+ * (URL-based servers are excluded — they don't need a binary). */
237
+ export function listMcpBinaryNames(): string[] {
238
+ const seen = new Set<string>();
239
+ for (const s of BUILTIN_MCP_SERVERS) {
240
+ if (s.command) seen.add(s.command);
241
+ }
242
+ return [...seen].sort();
221
243
  }
222
244
 
223
245
  export function collectMcpDependencyStatuses(cwd: string): DependencyStatus[] {
224
246
  const seen = new Set<string>();
225
247
  const statuses: DependencyStatus[] = [];
226
248
  for (const cfg of resolveMcpConfigs(cwd)) {
227
- if (!cfg.enabled || !cfg.command || seen.has(cfg.command)) continue;
228
- seen.add(cfg.command);
249
+ const depName = cfg.dependencyName ?? cfg.command;
250
+ if (!cfg.enabled || !cfg.command || !depName || seen.has(depName)) continue;
251
+ seen.add(depName);
252
+ const resolved = resolveMcpBinary(depName);
229
253
  statuses.push({
230
254
  module: `mcp:${cfg.name}`,
231
- label: cfg.command,
232
- state: commandExists(cfg.command) ? "ok" : "missing",
255
+ label: depName,
256
+ state: resolved ? "ok" : "missing",
233
257
  detail: `Install the MCP server command for \"${cfg.name}\" or update its config.`,
258
+ path: resolved ?? undefined,
234
259
  });
235
260
  }
236
261
  return statuses;
237
262
  }
238
263
 
239
- function commandExists(command: string): boolean {
240
- if (path.isAbsolute(command) || command.includes("/") || command.includes("\\")) {
241
- return fs.existsSync(command);
242
- }
243
- const result = process.platform === "win32"
244
- ? spawnSync("where", [command], { encoding: "utf-8" })
245
- : spawnSync(process.env.SHELL || "sh", ["-lc", `command -v '${command.replace(/'/g, `'"'"'`)}'`], { encoding: "utf-8" });
246
- return result.status === 0;
264
+ export function resolveMcpBinary(command: string): string | null {
265
+ return resolveDependency(command);
247
266
  }
248
267
 
249
268
  function readMcpJsonSafe(filePath: string): Record<string, any> | null {
@@ -1521,3 +1521,13 @@ function truncate(s: string, maxLen = 60): string {
1521
1521
  if (firstLine.length <= maxLen) return firstLine;
1522
1522
  return firstLine.slice(0, maxLen - 3) + "...";
1523
1523
  }
1524
+
1525
+ // Test exports
1526
+ export const __patchCoreTest = {
1527
+ charOffsetToLine,
1528
+ detectTabWidth,
1529
+ normalizeIndentForFuzzy,
1530
+ truncate,
1531
+ collapseSequentialReplacements,
1532
+ generateReplacementDiff,
1533
+ };