mono-pilot 0.2.9 → 0.2.12

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.
Files changed (158) hide show
  1. package/README.md +270 -7
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +60 -35
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +94 -50
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -1,16 +1,25 @@
1
- import { existsSync } from "node:fs";
2
- import { readdir, readFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, dirname, resolve } from "node:path";
5
3
  import process from "node:process";
6
4
  import { fileURLToPath, pathToFileURL } from "node:url";
7
5
  import { buildRuntimeEnvelope, createModeStateData, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, ASK_MODE_SWITCH_REMINDER, PLAN_MODE_STILL_ACTIVE_REMINDER, } from "./mode-runtime.js";
8
- import { createRpcRequestId, extractStringHeaders, isRecord, isServerEnabled, parseMcpConfig, postJsonRpcRequest, resolveMcpConfigPath, toNonEmptyString, MCP_PROTOCOL_VERSION, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, } from "../utils/mcp-client.js";
6
+ import { getBriefReflectionReminder } from "../brief/reflection.js";
7
+ import { createRpcRequestId, postJsonRpcRequest, MCP_PROTOCOL_VERSION, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, } from "../mcp/protocol.js";
8
+ import { extractStringHeaders, isRecord, isServerEnabled, loadMcpConfig, toNonEmptyString, } from "../mcp/config.js";
9
+ import { discoverRules } from "../rules/discovery.js";
9
10
  const PLAN_MODE_REMINDER_PATH = fileURLToPath(new URL("../../tools/plan-mode-reminder.md", import.meta.url));
10
11
  const ASK_MODE_REMINDER_PATH = fileURLToPath(new URL("../../tools/ask-mode-reminder.md", import.meta.url));
11
- const RULES_RELATIVE_DIR = join(".pi", "rules");
12
12
  const MCP_INSTRUCTIONS_DESCRIPTION = "Instructions provided by MCP servers to help use them properly";
13
13
  const USER_QUERY_RENDER_PATCH_FLAG = "__monoPilotUserQueryRenderPatched__";
14
+ const USER_MESSAGE_MODULE_PATH = fileURLToPath(import.meta.url);
15
+ const CURRENT_DATE_TIME_REMINDER_PREFIX = "Current date and time:";
16
+ const DEV_RUNTIME_TOOL_EFFICIENCY_REMINDER = `<system_reminder>
17
+ Detected development runtime (extension loaded from src/*.ts with runtime transpilation).
18
+ Always keep the current task goal first; only focus on tool-usage efficiency when the task is blocked or clear inefficiency appears.
19
+ When inefficiency is directly related to the current task, tool changes are allowed, but must be incremental: add observability/visibility first, then apply the smallest necessary logic adjustment.
20
+ Keep changes scoped, avoid unrelated refactors, and keep behavior verifiable; run npm run check after edits (and run npm run build when src changes).
21
+ If user intent or change boundaries are unclear, ask before modifying.
22
+ </system_reminder>`;
14
23
  function extractUserQueryForTuiDisplay(text) {
15
24
  const startIndex = text.indexOf("<user_query>");
16
25
  if (startIndex === -1)
@@ -52,6 +61,61 @@ function normalizeServerLabel(value) {
52
61
  return undefined;
53
62
  return normalized;
54
63
  }
64
+ function escapeXml(value) {
65
+ return value
66
+ .replace(/&/g, "&amp;")
67
+ .replace(/</g, "&lt;")
68
+ .replace(/>/g, "&gt;")
69
+ .replace(/\"/g, "&quot;")
70
+ .replace(/'/g, "&apos;");
71
+ }
72
+ function buildPlanFileReminder(planFilePath) {
73
+ const escapedPath = escapeXml(planFilePath);
74
+ return [
75
+ "<system_reminder>",
76
+ `Plan file path: ${escapedPath}`,
77
+ "While Plan mode is active, create or update this plan file as needed during collaboration with the user.",
78
+ "You may modify this plan file in Plan mode; do not modify other files unless the mode changes.",
79
+ "</system_reminder>",
80
+ ].join("\n");
81
+ }
82
+ function appendPlanFileReminder(reminder, snapshot) {
83
+ if (snapshot.activeMode !== "plan") {
84
+ return reminder;
85
+ }
86
+ const planFilePath = snapshot.planFilePath?.trim();
87
+ if (!planFilePath) {
88
+ return reminder;
89
+ }
90
+ const planFileReminder = buildPlanFileReminder(planFilePath);
91
+ if (!reminder) {
92
+ return planFileReminder;
93
+ }
94
+ return `${reminder}\n\n${planFileReminder}`;
95
+ }
96
+ function isDevRuntimeModulePath(modulePath) {
97
+ const normalized = modulePath.replace(/\\/g, "/");
98
+ return normalized.includes("/src/") && normalized.endsWith(".ts");
99
+ }
100
+ function appendDevRuntimeToolEfficiencyReminder(reminder) {
101
+ if (!isDevRuntimeModulePath(USER_MESSAGE_MODULE_PATH)) {
102
+ return reminder;
103
+ }
104
+ if (!reminder) {
105
+ return DEV_RUNTIME_TOOL_EFFICIENCY_REMINDER;
106
+ }
107
+ return `${reminder}\n\n${DEV_RUNTIME_TOOL_EFFICIENCY_REMINDER}`;
108
+ }
109
+ function buildCurrentDateTimeReminder(now = new Date()) {
110
+ return `<system_reminder>\n${CURRENT_DATE_TIME_REMINDER_PREFIX} ${now.toString()}\n</system_reminder>`;
111
+ }
112
+ function appendCurrentDateTimeReminder(reminder, now = new Date()) {
113
+ const dateTimeReminder = buildCurrentDateTimeReminder(now);
114
+ if (!reminder) {
115
+ return dateTimeReminder;
116
+ }
117
+ return `${reminder}\n\n${dateTimeReminder}`;
118
+ }
55
119
  async function fetchServerInstructions(serverUrl, serverHeaders, serverName) {
56
120
  try {
57
121
  const initializeResponse = await postJsonRpcRequest({
@@ -85,17 +149,16 @@ async function fetchServerInstructions(serverUrl, serverHeaders, serverName) {
85
149
  }
86
150
  }
87
151
  async function buildMcpInstructionsEnvelope(workspaceCwd) {
88
- const configPath = resolveMcpConfigPath(workspaceCwd);
89
- if (!configPath)
90
- return undefined;
91
- let servers;
152
+ let config;
92
153
  try {
93
- servers = await parseMcpConfig(configPath);
154
+ config = await loadMcpConfig(workspaceCwd);
94
155
  }
95
156
  catch {
96
157
  return undefined;
97
158
  }
98
- const serverEntries = Object.entries(servers)
159
+ if (!config)
160
+ return undefined;
161
+ const serverEntries = Object.entries(config.servers)
99
162
  .filter(([, config]) => isServerEnabled(config))
100
163
  .map(([name, config]) => {
101
164
  const serverName = normalizeServerLabel(name);
@@ -138,49 +201,26 @@ async function buildMcpInstructionsEnvelope(workspaceCwd) {
138
201
  return lines.join("\n");
139
202
  }
140
203
  async function buildRulesEnvelope(workspaceCwd) {
141
- const rulesDirPath = resolve(workspaceCwd, RULES_RELATIVE_DIR);
142
- const userRulesDirPath = resolve(homedir(), RULES_RELATIVE_DIR);
143
- const loadRulesFromDir = async (dirPath) => {
144
- if (!existsSync(dirPath))
145
- return new Map();
146
- let directoryEntries;
204
+ const { userRules, projectRules } = await discoverRules(workspaceCwd);
205
+ const allRulePaths = [...userRules, ...projectRules];
206
+ if (allRulePaths.length === 0)
207
+ return undefined;
208
+ const ruleEntries = [];
209
+ for (const filePath of allRulePaths) {
147
210
  try {
148
- directoryEntries = await readdir(dirPath, { withFileTypes: true, encoding: "utf8" });
211
+ const content = await readFile(filePath, "utf-8");
212
+ const normalized = content.trim();
213
+ if (normalized.length > 0) {
214
+ ruleEntries.push({ filename: basename(filePath), content: normalized });
215
+ }
149
216
  }
150
217
  catch {
151
- return new Map();
218
+ // Ignore unreadable rule files.
152
219
  }
153
- const ruleFileNames = directoryEntries
154
- .filter((entry) => entry.isFile() && entry.name.endsWith(".rule.txt"))
155
- .map((entry) => entry.name)
156
- .sort((a, b) => a.localeCompare(b));
157
- const rules = new Map();
158
- for (const ruleFileName of ruleFileNames) {
159
- const ruleFilePath = resolve(dirPath, ruleFileName);
160
- try {
161
- const content = await readFile(ruleFilePath, "utf-8");
162
- const normalized = content.trim();
163
- if (normalized.length > 0) {
164
- rules.set(ruleFileName, normalized);
165
- }
166
- }
167
- catch {
168
- // Ignore unreadable rule files.
169
- }
170
- }
171
- return rules;
172
- };
173
- const userRules = await loadRulesFromDir(userRulesDirPath);
174
- const workspaceRules = await loadRulesFromDir(rulesDirPath);
175
- const mergedRules = new Map(userRules);
176
- for (const [fileName, content] of workspaceRules) {
177
- mergedRules.set(fileName, content);
178
220
  }
179
- if (mergedRules.size === 0)
221
+ if (ruleEntries.length === 0)
180
222
  return undefined;
181
- const rules = Array.from(mergedRules.entries())
182
- .sort(([a], [b]) => a.localeCompare(b))
183
- .map(([, content]) => content);
223
+ const rules = ruleEntries.sort((a, b) => a.filename.localeCompare(b.filename)).map((e) => e.content);
184
224
  const lines = ["<rules>"];
185
225
  for (const rule of rules) {
186
226
  lines.push("<user_rule>");
@@ -281,12 +321,16 @@ export default function runtimeEnvelopeExtension(pi) {
281
321
  getRulesEnvelope(),
282
322
  ]);
283
323
  const { reminder, changed, snapshot } = modeRuntimeStore.consumeReminder(planEntryReminder, askEntryReminder);
324
+ const reminderWithPlanFile = appendPlanFileReminder(reminder, snapshot);
325
+ const reminderWithDevToolEfficiency = appendDevRuntimeToolEfficiencyReminder(reminderWithPlanFile);
326
+ const reminderWithCurrentDateTime = appendCurrentDateTimeReminder(reminderWithDevToolEfficiency);
284
327
  if (changed) {
285
328
  pi.appendEntry(MODE_STATE_ENTRY_TYPE, createModeStateData(snapshot));
286
329
  }
330
+ const briefReminder = getBriefReflectionReminder();
287
331
  return {
288
332
  action: "transform",
289
- text: buildRuntimeEnvelope(event.text, reminder, mcpInstructions, rulesEnvelope),
333
+ text: buildRuntimeEnvelope(event.text, reminderWithCurrentDateTime, mcpInstructions, rulesEnvelope, briefReminder),
290
334
  images: event.images,
291
335
  };
292
336
  });
@@ -0,0 +1,235 @@
1
+ import { Bus, BusEvent, Filesystem, Log, LspState, withTimeout } from "./runtime.js";
2
+ import path from "node:path";
3
+ import { pathToFileURL, fileURLToPath } from "node:url";
4
+ import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/lib/node/main.js";
5
+ const LANGUAGE_EXTENSIONS = {
6
+ ".abap": "abap",
7
+ ".bat": "bat",
8
+ ".bib": "bibtex",
9
+ ".bibtex": "bibtex",
10
+ ".clj": "clojure",
11
+ ".cljs": "clojure",
12
+ ".cljc": "clojure",
13
+ ".edn": "clojure",
14
+ ".coffee": "coffeescript",
15
+ ".c": "c",
16
+ ".cpp": "cpp",
17
+ ".cxx": "cpp",
18
+ ".cc": "cpp",
19
+ ".c++": "cpp",
20
+ ".cs": "csharp",
21
+ ".css": "css",
22
+ ".d": "d",
23
+ ".pas": "pascal",
24
+ ".pascal": "pascal",
25
+ ".diff": "diff",
26
+ ".patch": "diff",
27
+ ".dart": "dart",
28
+ ".dockerfile": "dockerfile",
29
+ ".ex": "elixir",
30
+ ".exs": "elixir",
31
+ ".erl": "erlang",
32
+ ".hrl": "erlang",
33
+ ".fs": "fsharp",
34
+ ".fsi": "fsharp",
35
+ ".fsx": "fsharp",
36
+ ".fsscript": "fsharp",
37
+ ".go": "go",
38
+ ".groovy": "groovy",
39
+ ".hs": "haskell",
40
+ ".lhs": "haskell",
41
+ ".html": "html",
42
+ ".htm": "html",
43
+ ".ini": "ini",
44
+ ".java": "java",
45
+ ".jl": "julia",
46
+ ".js": "javascript",
47
+ ".kt": "kotlin",
48
+ ".kts": "kotlin",
49
+ ".jsx": "javascriptreact",
50
+ ".json": "json",
51
+ ".tex": "latex",
52
+ ".latex": "latex",
53
+ ".less": "less",
54
+ ".lua": "lua",
55
+ ".makefile": "makefile",
56
+ "makefile": "makefile",
57
+ ".md": "markdown",
58
+ ".markdown": "markdown",
59
+ ".m": "objective-c",
60
+ ".mm": "objective-cpp",
61
+ ".pl": "perl",
62
+ ".pm": "perl",
63
+ ".php": "php",
64
+ ".ps1": "powershell",
65
+ ".psm1": "powershell",
66
+ ".py": "python",
67
+ ".r": "r",
68
+ ".rb": "ruby",
69
+ ".rake": "ruby",
70
+ ".rs": "rust",
71
+ ".scss": "scss",
72
+ ".sass": "sass",
73
+ ".scala": "scala",
74
+ ".sh": "shellscript",
75
+ ".bash": "shellscript",
76
+ ".zsh": "shellscript",
77
+ ".ksh": "shellscript",
78
+ ".sql": "sql",
79
+ ".svelte": "svelte",
80
+ ".swift": "swift",
81
+ ".ts": "typescript",
82
+ ".tsx": "typescriptreact",
83
+ ".mts": "typescript",
84
+ ".cts": "typescript",
85
+ ".xml": "xml",
86
+ ".yaml": "yaml",
87
+ ".yml": "yaml",
88
+ ".mjs": "javascript",
89
+ ".cjs": "javascript",
90
+ ".vue": "vue",
91
+ ".zig": "zig",
92
+ ".ml": "ocaml",
93
+ ".mli": "ocaml",
94
+ ".tf": "terraform",
95
+ ".tfvars": "terraform-vars",
96
+ ".nix": "nix",
97
+ };
98
+ const DIAGNOSTICS_DEBOUNCE_MS = 150;
99
+ export var LSPClient;
100
+ (function (LSPClient) {
101
+ const log = Log.create({ service: "lsp.client" });
102
+ LSPClient.Event = {
103
+ Diagnostics: BusEvent.define("lsp.client.diagnostics", null),
104
+ };
105
+ async function create(input) {
106
+ const l = log.clone().tag("serverID", input.serverID);
107
+ l.info("starting client");
108
+ const connection = createMessageConnection(new StreamMessageReader(input.server.process.stdout), new StreamMessageWriter(input.server.process.stdin));
109
+ const diagnostics = new Map();
110
+ connection.onNotification("textDocument/publishDiagnostics", (params) => {
111
+ const filePath = Filesystem.normalizePath(fileURLToPath(params.uri));
112
+ const existed = diagnostics.has(filePath);
113
+ diagnostics.set(filePath, params.diagnostics);
114
+ // TypeScript sends empty diagnostics on first open; skip until we have a prior entry
115
+ if (!existed && input.serverID === "typescript")
116
+ return;
117
+ Bus.publish(LSPClient.Event.Diagnostics, { path: filePath, serverID: input.serverID });
118
+ });
119
+ connection.onRequest("window/workDoneProgress/create", () => null);
120
+ connection.onRequest("workspace/configuration", async () => [input.server.initialization ?? {}]);
121
+ connection.onRequest("client/registerCapability", async () => { });
122
+ connection.onRequest("client/unregisterCapability", async () => { });
123
+ connection.onRequest("workspace/workspaceFolders", async () => [
124
+ { name: "workspace", uri: pathToFileURL(input.root).href },
125
+ ]);
126
+ connection.listen();
127
+ l.info("sending initialize");
128
+ await withTimeout(connection.sendRequest("initialize", {
129
+ rootUri: pathToFileURL(input.root).href,
130
+ processId: input.server.process.pid,
131
+ workspaceFolders: [{ name: "workspace", uri: pathToFileURL(input.root).href }],
132
+ initializationOptions: { ...input.server.initialization },
133
+ capabilities: {
134
+ window: { workDoneProgress: true },
135
+ workspace: {
136
+ configuration: true,
137
+ didChangeWatchedFiles: { dynamicRegistration: true },
138
+ },
139
+ textDocument: {
140
+ synchronization: { didOpen: true, didChange: true },
141
+ publishDiagnostics: { versionSupport: true },
142
+ },
143
+ },
144
+ }), 45_000).catch((err) => {
145
+ const e = new Error(`LSP initialize failed: ${input.serverID}`);
146
+ e.cause = err;
147
+ throw e;
148
+ });
149
+ await connection.sendNotification("initialized", {});
150
+ if (input.server.initialization) {
151
+ await connection.sendNotification("workspace/didChangeConfiguration", {
152
+ settings: input.server.initialization,
153
+ });
154
+ }
155
+ const files = {};
156
+ const result = {
157
+ root: input.root,
158
+ get serverID() {
159
+ return input.serverID;
160
+ },
161
+ get connection() {
162
+ return connection;
163
+ },
164
+ notify: {
165
+ async open(inp) {
166
+ const p = path.isAbsolute(inp.path)
167
+ ? inp.path
168
+ : path.resolve(LspState.directory, inp.path);
169
+ const text = await Filesystem.readText(p);
170
+ const ext = path.extname(p);
171
+ const languageId = LANGUAGE_EXTENSIONS[ext] ?? "plaintext";
172
+ const uri = pathToFileURL(p).href;
173
+ const version = files[p];
174
+ if (version !== undefined) {
175
+ await connection.sendNotification("workspace/didChangeWatchedFiles", {
176
+ changes: [{ uri, type: 2 }],
177
+ });
178
+ const next = version + 1;
179
+ files[p] = next;
180
+ await connection.sendNotification("textDocument/didChange", {
181
+ textDocument: { uri, version: next },
182
+ contentChanges: [{ text }],
183
+ });
184
+ return;
185
+ }
186
+ await connection.sendNotification("workspace/didChangeWatchedFiles", {
187
+ changes: [{ uri, type: 1 }],
188
+ });
189
+ diagnostics.delete(p);
190
+ await connection.sendNotification("textDocument/didOpen", {
191
+ textDocument: { uri, languageId, version: 0, text },
192
+ });
193
+ files[p] = 0;
194
+ },
195
+ },
196
+ get diagnostics() {
197
+ return diagnostics;
198
+ },
199
+ async waitForDiagnostics(inp) {
200
+ const normalized = Filesystem.normalizePath(path.isAbsolute(inp.path)
201
+ ? inp.path
202
+ : path.resolve(LspState.directory, inp.path));
203
+ let unsub;
204
+ let timer;
205
+ return withTimeout(new Promise((resolve) => {
206
+ unsub = Bus.subscribe(LSPClient.Event.Diagnostics, (event) => {
207
+ if (event.properties.path === normalized &&
208
+ event.properties.serverID === result.serverID) {
209
+ if (timer)
210
+ clearTimeout(timer);
211
+ timer = setTimeout(() => {
212
+ unsub?.();
213
+ resolve();
214
+ }, DIAGNOSTICS_DEBOUNCE_MS);
215
+ }
216
+ });
217
+ }), 3000)
218
+ .catch(() => { })
219
+ .finally(() => {
220
+ if (timer)
221
+ clearTimeout(timer);
222
+ unsub?.();
223
+ });
224
+ },
225
+ async shutdown() {
226
+ connection.end();
227
+ connection.dispose();
228
+ input.server.process.kill();
229
+ },
230
+ };
231
+ l.info("initialized");
232
+ return result;
233
+ }
234
+ LSPClient.create = create;
235
+ })(LSPClient || (LSPClient = {}));
@@ -0,0 +1,165 @@
1
+ import { Log, LspState } from "./runtime.js";
2
+ import { LSPClient } from "./client.js";
3
+ import { LSPServer } from "./server.js";
4
+ import path from "node:path";
5
+ export var LSP;
6
+ (function (LSP) {
7
+ const log = Log.create({ service: "lsp" });
8
+ let _state;
9
+ function getState() {
10
+ if (!_state) {
11
+ _state = {
12
+ clients: [],
13
+ servers: {
14
+ typescript: LSPServer.Typescript,
15
+ pyright: LSPServer.Pyright,
16
+ gopls: LSPServer.Gopls,
17
+ rust: LSPServer.RustAnalyzer,
18
+ sourcekit: LSPServer.SourcekitLsp,
19
+ clangd: LSPServer.Clangd,
20
+ },
21
+ broken: new Set(),
22
+ spawning: new Map(),
23
+ };
24
+ }
25
+ return _state;
26
+ }
27
+ function init(directory) {
28
+ LspState.directory = directory;
29
+ log.info("LSP initialized", { directory });
30
+ }
31
+ LSP.init = init;
32
+ async function shutdown() {
33
+ if (!_state)
34
+ return;
35
+ await Promise.all(_state.clients.map((c) => c.shutdown()));
36
+ _state = undefined;
37
+ }
38
+ LSP.shutdown = shutdown;
39
+ async function getClients(file) {
40
+ const s = getState();
41
+ const ext = path.parse(file).ext || file;
42
+ const result = [];
43
+ async function schedule(server, root, key) {
44
+ const handle = await server
45
+ .spawn(root)
46
+ .then((v) => {
47
+ if (!v)
48
+ s.broken.add(key);
49
+ return v;
50
+ })
51
+ .catch((err) => {
52
+ s.broken.add(key);
53
+ log.error(`failed to spawn ${server.id}`, { error: String(err) });
54
+ return undefined;
55
+ });
56
+ if (!handle)
57
+ return undefined;
58
+ const client = await LSPClient.create({
59
+ serverID: server.id,
60
+ server: handle,
61
+ root,
62
+ }).catch((err) => {
63
+ s.broken.add(key);
64
+ handle.process.kill();
65
+ log.error(`failed to init ${server.id}`, { error: String(err) });
66
+ return undefined;
67
+ });
68
+ if (!client) {
69
+ handle.process.kill();
70
+ return undefined;
71
+ }
72
+ // Race condition guard: another concurrent call may have added the same client
73
+ const existing = s.clients.find((x) => x.root === root && x.serverID === server.id);
74
+ if (existing) {
75
+ handle.process.kill();
76
+ return existing;
77
+ }
78
+ s.clients.push(client);
79
+ return client;
80
+ }
81
+ for (const server of Object.values(s.servers)) {
82
+ if (server.extensions.length && !server.extensions.includes(ext))
83
+ continue;
84
+ const root = await server.root(file);
85
+ if (!root)
86
+ continue;
87
+ const key = root + server.id;
88
+ if (s.broken.has(key))
89
+ continue;
90
+ const match = s.clients.find((x) => x.root === root && x.serverID === server.id);
91
+ if (match) {
92
+ result.push(match);
93
+ continue;
94
+ }
95
+ const inflight = s.spawning.get(key);
96
+ if (inflight) {
97
+ const c = await inflight;
98
+ if (c)
99
+ result.push(c);
100
+ continue;
101
+ }
102
+ const task = schedule(server, root, key);
103
+ s.spawning.set(key, task);
104
+ task.finally(() => {
105
+ if (s.spawning.get(key) === task)
106
+ s.spawning.delete(key);
107
+ });
108
+ const client = await task;
109
+ if (client)
110
+ result.push(client);
111
+ }
112
+ return result;
113
+ }
114
+ async function touchFile(file, wait) {
115
+ const clients = await getClients(file);
116
+ await Promise.all(clients.map(async (client) => {
117
+ const pending = wait
118
+ ? client.waitForDiagnostics({ path: file })
119
+ : Promise.resolve();
120
+ await client.notify.open({ path: file });
121
+ return pending;
122
+ })).catch((err) => log.error("failed to touch file", { err: String(err), file }));
123
+ }
124
+ LSP.touchFile = touchFile;
125
+ async function diagnostics() {
126
+ const s = getState();
127
+ const results = {};
128
+ for (const client of s.clients) {
129
+ for (const [p, diags] of client.diagnostics.entries()) {
130
+ results[p] = [...(results[p] ?? []), ...diags];
131
+ }
132
+ }
133
+ return results;
134
+ }
135
+ LSP.diagnostics = diagnostics;
136
+ async function workspaceSymbol(query) {
137
+ const s = getState();
138
+ // Filter to meaningful symbol kinds: Class(5), Method(6), Enum(10), Interface(11), Function(12)
139
+ const meaningful = new Set([5, 6, 10, 11, 12, 13, 14, 23]);
140
+ const tasks = s.clients.map((c) => c.connection
141
+ .sendRequest("workspace/symbol", { query })
142
+ .then((r) => (r ?? [])
143
+ .filter((x) => meaningful.has(x.kind))
144
+ .slice(0, 10))
145
+ .catch(() => []));
146
+ return (await Promise.all(tasks)).flat();
147
+ }
148
+ LSP.workspaceSymbol = workspaceSymbol;
149
+ let Diagnostic;
150
+ (function (Diagnostic) {
151
+ const severityLabel = {
152
+ 1: "ERROR",
153
+ 2: "WARN",
154
+ 3: "INFO",
155
+ 4: "HINT",
156
+ };
157
+ function pretty(d) {
158
+ const sev = severityLabel[d.severity ?? 1] ?? "ERROR";
159
+ const line = d.range.start.line + 1;
160
+ const col = d.range.start.character + 1;
161
+ return `${sev} [${line}:${col}] ${d.message}`;
162
+ }
163
+ Diagnostic.pretty = pretty;
164
+ })(Diagnostic = LSP.Diagnostic || (LSP.Diagnostic = {}));
165
+ })(LSP || (LSP = {}));
@@ -0,0 +1,67 @@
1
+ import { EventEmitter } from "node:events";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ export const Log = {
5
+ create: (_meta) => ({
6
+ info: (_msg, _data) => { },
7
+ error: (msg, data) => console.error(`[lsp] ${msg}`, data ?? ""),
8
+ clone() {
9
+ return this;
10
+ },
11
+ tag(_k, _v) {
12
+ return this;
13
+ },
14
+ }),
15
+ };
16
+ let dir = process.cwd();
17
+ export const LspState = {
18
+ get directory() {
19
+ return dir;
20
+ },
21
+ set directory(d) {
22
+ dir = d;
23
+ },
24
+ };
25
+ const exists = (p) => fs.access(p).then(() => true).catch(() => false);
26
+ async function* filesUp(opts) {
27
+ let current = opts.start;
28
+ while (true) {
29
+ for (const target of opts.targets) {
30
+ const p = path.join(current, target);
31
+ if (await exists(p))
32
+ yield p;
33
+ }
34
+ if (current === opts.stop || !current.startsWith(opts.stop))
35
+ break;
36
+ const parent = path.dirname(current);
37
+ if (parent === current)
38
+ break;
39
+ current = parent;
40
+ }
41
+ }
42
+ export const Filesystem = {
43
+ readText: (p) => fs.readFile(p, "utf8"),
44
+ exists,
45
+ normalizePath: (p) => path.normalize(p),
46
+ up: filesUp,
47
+ };
48
+ export function withTimeout(promise, ms) {
49
+ return Promise.race([
50
+ promise,
51
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)),
52
+ ]);
53
+ }
54
+ const emitter = new EventEmitter();
55
+ emitter.setMaxListeners(200);
56
+ export const BusEvent = {
57
+ define: (type, _schema) => ({ type }),
58
+ };
59
+ export const Bus = {
60
+ publish: (event, props) => {
61
+ emitter.emit(event.type, { properties: props });
62
+ },
63
+ subscribe: (event, handler) => {
64
+ emitter.on(event.type, handler);
65
+ return () => emitter.off(event.type, handler);
66
+ },
67
+ };