fixo-cli 1.0.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.
Files changed (303) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +530 -0
  3. package/dist/agent/agent-client.d.ts +108 -0
  4. package/dist/agent/agent-client.d.ts.map +1 -0
  5. package/dist/agent/agent-client.js +1247 -0
  6. package/dist/agent/agent-client.js.map +1 -0
  7. package/dist/agent/agent-pool.d.ts +20 -0
  8. package/dist/agent/agent-pool.d.ts.map +1 -0
  9. package/dist/agent/agent-pool.js +217 -0
  10. package/dist/agent/agent-pool.js.map +1 -0
  11. package/dist/agent/background-awareness.d.ts +55 -0
  12. package/dist/agent/background-awareness.d.ts.map +1 -0
  13. package/dist/agent/background-awareness.js +104 -0
  14. package/dist/agent/background-awareness.js.map +1 -0
  15. package/dist/agent/command-parser.d.ts +33 -0
  16. package/dist/agent/command-parser.d.ts.map +1 -0
  17. package/dist/agent/command-parser.js +120 -0
  18. package/dist/agent/command-parser.js.map +1 -0
  19. package/dist/agent/context-budget.d.ts +91 -0
  20. package/dist/agent/context-budget.d.ts.map +1 -0
  21. package/dist/agent/context-budget.js +219 -0
  22. package/dist/agent/context-budget.js.map +1 -0
  23. package/dist/agent/conversation.d.ts +190 -0
  24. package/dist/agent/conversation.d.ts.map +1 -0
  25. package/dist/agent/conversation.js +547 -0
  26. package/dist/agent/conversation.js.map +1 -0
  27. package/dist/agent/hooks.d.ts +72 -0
  28. package/dist/agent/hooks.d.ts.map +1 -0
  29. package/dist/agent/hooks.js +214 -0
  30. package/dist/agent/hooks.js.map +1 -0
  31. package/dist/agent/mcp-bridge.d.ts +13 -0
  32. package/dist/agent/mcp-bridge.d.ts.map +1 -0
  33. package/dist/agent/mcp-bridge.js +86 -0
  34. package/dist/agent/mcp-bridge.js.map +1 -0
  35. package/dist/agent/mcp-client.d.ts +24 -0
  36. package/dist/agent/mcp-client.d.ts.map +1 -0
  37. package/dist/agent/mcp-client.js +146 -0
  38. package/dist/agent/mcp-client.js.map +1 -0
  39. package/dist/agent/mcp-manager.d.ts +13 -0
  40. package/dist/agent/mcp-manager.d.ts.map +1 -0
  41. package/dist/agent/mcp-manager.js +84 -0
  42. package/dist/agent/mcp-manager.js.map +1 -0
  43. package/dist/agent/mcp-registry.d.ts +45 -0
  44. package/dist/agent/mcp-registry.d.ts.map +1 -0
  45. package/dist/agent/mcp-registry.js +98 -0
  46. package/dist/agent/mcp-registry.js.map +1 -0
  47. package/dist/agent/orchestrator.d.ts +14 -0
  48. package/dist/agent/orchestrator.d.ts.map +1 -0
  49. package/dist/agent/orchestrator.js +118 -0
  50. package/dist/agent/orchestrator.js.map +1 -0
  51. package/dist/agent/parser-adapter.d.ts +120 -0
  52. package/dist/agent/parser-adapter.d.ts.map +1 -0
  53. package/dist/agent/parser-adapter.js +265 -0
  54. package/dist/agent/parser-adapter.js.map +1 -0
  55. package/dist/agent/parsers/imports.d.ts +11 -0
  56. package/dist/agent/parsers/imports.d.ts.map +1 -0
  57. package/dist/agent/parsers/imports.js +94 -0
  58. package/dist/agent/parsers/imports.js.map +1 -0
  59. package/dist/agent/parsers/shell.d.ts +23 -0
  60. package/dist/agent/parsers/shell.d.ts.map +1 -0
  61. package/dist/agent/parsers/shell.js +200 -0
  62. package/dist/agent/parsers/shell.js.map +1 -0
  63. package/dist/agent/parsers/symbols.d.ts +17 -0
  64. package/dist/agent/parsers/symbols.d.ts.map +1 -0
  65. package/dist/agent/parsers/symbols.js +103 -0
  66. package/dist/agent/parsers/symbols.js.map +1 -0
  67. package/dist/agent/permissions.d.ts +65 -0
  68. package/dist/agent/permissions.d.ts.map +1 -0
  69. package/dist/agent/permissions.js +219 -0
  70. package/dist/agent/permissions.js.map +1 -0
  71. package/dist/agent/predictive-gate.d.ts +69 -0
  72. package/dist/agent/predictive-gate.d.ts.map +1 -0
  73. package/dist/agent/predictive-gate.js +128 -0
  74. package/dist/agent/predictive-gate.js.map +1 -0
  75. package/dist/agent/provider-cooldown.d.ts +144 -0
  76. package/dist/agent/provider-cooldown.d.ts.map +1 -0
  77. package/dist/agent/provider-cooldown.js +300 -0
  78. package/dist/agent/provider-cooldown.js.map +1 -0
  79. package/dist/agent/providers-manager.d.ts +109 -0
  80. package/dist/agent/providers-manager.d.ts.map +1 -0
  81. package/dist/agent/providers-manager.js +464 -0
  82. package/dist/agent/providers-manager.js.map +1 -0
  83. package/dist/agent/repo-map.d.ts +6 -0
  84. package/dist/agent/repo-map.d.ts.map +1 -0
  85. package/dist/agent/repo-map.js +221 -0
  86. package/dist/agent/repo-map.js.map +1 -0
  87. package/dist/agent/retry.d.ts +103 -0
  88. package/dist/agent/retry.d.ts.map +1 -0
  89. package/dist/agent/retry.js +276 -0
  90. package/dist/agent/retry.js.map +1 -0
  91. package/dist/agent/search/index.d.ts +61 -0
  92. package/dist/agent/search/index.d.ts.map +1 -0
  93. package/dist/agent/search/index.js +314 -0
  94. package/dist/agent/search/index.js.map +1 -0
  95. package/dist/agent/single-agent.d.ts +76 -0
  96. package/dist/agent/single-agent.d.ts.map +1 -0
  97. package/dist/agent/single-agent.js +697 -0
  98. package/dist/agent/single-agent.js.map +1 -0
  99. package/dist/agent/skills.d.ts +22 -0
  100. package/dist/agent/skills.d.ts.map +1 -0
  101. package/dist/agent/skills.js +139 -0
  102. package/dist/agent/skills.js.map +1 -0
  103. package/dist/agent/stream-glue.d.ts +85 -0
  104. package/dist/agent/stream-glue.d.ts.map +1 -0
  105. package/dist/agent/stream-glue.js +120 -0
  106. package/dist/agent/stream-glue.js.map +1 -0
  107. package/dist/agent/subagent.d.ts +72 -0
  108. package/dist/agent/subagent.d.ts.map +1 -0
  109. package/dist/agent/subagent.js +193 -0
  110. package/dist/agent/subagent.js.map +1 -0
  111. package/dist/agent/telemetry.d.ts +192 -0
  112. package/dist/agent/telemetry.d.ts.map +1 -0
  113. package/dist/agent/telemetry.js +400 -0
  114. package/dist/agent/telemetry.js.map +1 -0
  115. package/dist/agent/tokenizer.d.ts +42 -0
  116. package/dist/agent/tokenizer.d.ts.map +1 -0
  117. package/dist/agent/tokenizer.js +107 -0
  118. package/dist/agent/tokenizer.js.map +1 -0
  119. package/dist/agent/tool-executor.d.ts +289 -0
  120. package/dist/agent/tool-executor.d.ts.map +1 -0
  121. package/dist/agent/tool-executor.js +2519 -0
  122. package/dist/agent/tool-executor.js.map +1 -0
  123. package/dist/agent/web-impl.d.ts +2 -0
  124. package/dist/agent/web-impl.d.ts.map +1 -0
  125. package/dist/agent/web-impl.js +34 -0
  126. package/dist/agent/web-impl.js.map +1 -0
  127. package/dist/agent/web.d.ts +8 -0
  128. package/dist/agent/web.d.ts.map +1 -0
  129. package/dist/agent/web.js +8 -0
  130. package/dist/agent/web.js.map +1 -0
  131. package/dist/agent/worker-agent.d.ts +27 -0
  132. package/dist/agent/worker-agent.d.ts.map +1 -0
  133. package/dist/agent/worker-agent.js +503 -0
  134. package/dist/agent/worker-agent.js.map +1 -0
  135. package/dist/config.d.ts +162 -0
  136. package/dist/config.d.ts.map +1 -0
  137. package/dist/config.js +138 -0
  138. package/dist/config.js.map +1 -0
  139. package/dist/context/fixo-md-watcher.d.ts +42 -0
  140. package/dist/context/fixo-md-watcher.d.ts.map +1 -0
  141. package/dist/context/fixo-md-watcher.js +126 -0
  142. package/dist/context/fixo-md-watcher.js.map +1 -0
  143. package/dist/context/fixo-md.d.ts +50 -0
  144. package/dist/context/fixo-md.d.ts.map +1 -0
  145. package/dist/context/fixo-md.js +118 -0
  146. package/dist/context/fixo-md.js.map +1 -0
  147. package/dist/context/todo.d.ts +65 -0
  148. package/dist/context/todo.d.ts.map +1 -0
  149. package/dist/context/todo.js +194 -0
  150. package/dist/context/todo.js.map +1 -0
  151. package/dist/git/git-manager.d.ts +33 -0
  152. package/dist/git/git-manager.d.ts.map +1 -0
  153. package/dist/git/git-manager.js +293 -0
  154. package/dist/git/git-manager.js.map +1 -0
  155. package/dist/git/git-ops.d.ts +10 -0
  156. package/dist/git/git-ops.d.ts.map +1 -0
  157. package/dist/git/git-ops.js +131 -0
  158. package/dist/git/git-ops.js.map +1 -0
  159. package/dist/index.d.ts +3 -0
  160. package/dist/index.d.ts.map +1 -0
  161. package/dist/index.js +352 -0
  162. package/dist/index.js.map +1 -0
  163. package/dist/indexer.d.ts +30 -0
  164. package/dist/indexer.d.ts.map +1 -0
  165. package/dist/indexer.js +273 -0
  166. package/dist/indexer.js.map +1 -0
  167. package/dist/lsp/lsp-client.d.ts +24 -0
  168. package/dist/lsp/lsp-client.d.ts.map +1 -0
  169. package/dist/lsp/lsp-client.js +205 -0
  170. package/dist/lsp/lsp-client.js.map +1 -0
  171. package/dist/lsp/lsp-manager.d.ts +17 -0
  172. package/dist/lsp/lsp-manager.d.ts.map +1 -0
  173. package/dist/lsp/lsp-manager.js +154 -0
  174. package/dist/lsp/lsp-manager.js.map +1 -0
  175. package/dist/lsp/lsp-pre-save.d.ts +137 -0
  176. package/dist/lsp/lsp-pre-save.d.ts.map +1 -0
  177. package/dist/lsp/lsp-pre-save.js +245 -0
  178. package/dist/lsp/lsp-pre-save.js.map +1 -0
  179. package/dist/lsp/syntax-fallback.d.ts +83 -0
  180. package/dist/lsp/syntax-fallback.d.ts.map +1 -0
  181. package/dist/lsp/syntax-fallback.js +275 -0
  182. package/dist/lsp/syntax-fallback.js.map +1 -0
  183. package/dist/model-outcomes.d.ts +12 -0
  184. package/dist/model-outcomes.d.ts.map +1 -0
  185. package/dist/model-outcomes.js +46 -0
  186. package/dist/model-outcomes.js.map +1 -0
  187. package/dist/planner.d.ts +32 -0
  188. package/dist/planner.d.ts.map +1 -0
  189. package/dist/planner.js +163 -0
  190. package/dist/planner.js.map +1 -0
  191. package/dist/project-memory.d.ts +29 -0
  192. package/dist/project-memory.d.ts.map +1 -0
  193. package/dist/project-memory.js +349 -0
  194. package/dist/project-memory.js.map +1 -0
  195. package/dist/review.d.ts +2 -0
  196. package/dist/review.d.ts.map +1 -0
  197. package/dist/review.js +61 -0
  198. package/dist/review.js.map +1 -0
  199. package/dist/runtime/background-jobs.d.ts +97 -0
  200. package/dist/runtime/background-jobs.d.ts.map +1 -0
  201. package/dist/runtime/background-jobs.js +331 -0
  202. package/dist/runtime/background-jobs.js.map +1 -0
  203. package/dist/runtime/credential-vault.d.ts +124 -0
  204. package/dist/runtime/credential-vault.d.ts.map +1 -0
  205. package/dist/runtime/credential-vault.js +184 -0
  206. package/dist/runtime/credential-vault.js.map +1 -0
  207. package/dist/runtime/loop-trap.d.ts +197 -0
  208. package/dist/runtime/loop-trap.d.ts.map +1 -0
  209. package/dist/runtime/loop-trap.js +420 -0
  210. package/dist/runtime/loop-trap.js.map +1 -0
  211. package/dist/runtime/policy.d.ts +15 -0
  212. package/dist/runtime/policy.d.ts.map +1 -0
  213. package/dist/runtime/policy.js +60 -0
  214. package/dist/runtime/policy.js.map +1 -0
  215. package/dist/runtime/redaction.d.ts +66 -0
  216. package/dist/runtime/redaction.d.ts.map +1 -0
  217. package/dist/runtime/redaction.js +155 -0
  218. package/dist/runtime/redaction.js.map +1 -0
  219. package/dist/runtime/session-snapshots.d.ts +76 -0
  220. package/dist/runtime/session-snapshots.d.ts.map +1 -0
  221. package/dist/runtime/session-snapshots.js +166 -0
  222. package/dist/runtime/session-snapshots.js.map +1 -0
  223. package/dist/runtime/staging.d.ts +205 -0
  224. package/dist/runtime/staging.d.ts.map +1 -0
  225. package/dist/runtime/staging.js +526 -0
  226. package/dist/runtime/staging.js.map +1 -0
  227. package/dist/runtime/task-session.d.ts +95 -0
  228. package/dist/runtime/task-session.d.ts.map +1 -0
  229. package/dist/runtime/task-session.js +263 -0
  230. package/dist/runtime/task-session.js.map +1 -0
  231. package/dist/runtime/worktree.d.ts +55 -0
  232. package/dist/runtime/worktree.d.ts.map +1 -0
  233. package/dist/runtime/worktree.js +175 -0
  234. package/dist/runtime/worktree.js.map +1 -0
  235. package/dist/setup-wizard.d.ts +8 -0
  236. package/dist/setup-wizard.d.ts.map +1 -0
  237. package/dist/setup-wizard.js +73 -0
  238. package/dist/setup-wizard.js.map +1 -0
  239. package/dist/shared/content.d.ts +43 -0
  240. package/dist/shared/content.d.ts.map +1 -0
  241. package/dist/shared/content.js +61 -0
  242. package/dist/shared/content.js.map +1 -0
  243. package/dist/shared/types.d.ts +217 -0
  244. package/dist/shared/types.d.ts.map +1 -0
  245. package/dist/shared/types.js +3 -0
  246. package/dist/shared/types.js.map +1 -0
  247. package/dist/test-runner.d.ts +5 -0
  248. package/dist/test-runner.d.ts.map +1 -0
  249. package/dist/test-runner.js +42 -0
  250. package/dist/test-runner.js.map +1 -0
  251. package/dist/types.d.ts +85 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +2 -0
  254. package/dist/types.js.map +1 -0
  255. package/dist/ui/ascii.d.ts +23 -0
  256. package/dist/ui/ascii.d.ts.map +1 -0
  257. package/dist/ui/ascii.js +45 -0
  258. package/dist/ui/ascii.js.map +1 -0
  259. package/dist/ui/colors.d.ts +111 -0
  260. package/dist/ui/colors.d.ts.map +1 -0
  261. package/dist/ui/colors.js +166 -0
  262. package/dist/ui/colors.js.map +1 -0
  263. package/dist/ui/image-attach.d.ts +27 -0
  264. package/dist/ui/image-attach.d.ts.map +1 -0
  265. package/dist/ui/image-attach.js +100 -0
  266. package/dist/ui/image-attach.js.map +1 -0
  267. package/dist/ui/index.d.ts +18 -0
  268. package/dist/ui/index.d.ts.map +1 -0
  269. package/dist/ui/index.js +18 -0
  270. package/dist/ui/index.js.map +1 -0
  271. package/dist/ui/markdown-stream.d.ts +91 -0
  272. package/dist/ui/markdown-stream.d.ts.map +1 -0
  273. package/dist/ui/markdown-stream.js +524 -0
  274. package/dist/ui/markdown-stream.js.map +1 -0
  275. package/dist/ui/plan-renderer.d.ts +36 -0
  276. package/dist/ui/plan-renderer.d.ts.map +1 -0
  277. package/dist/ui/plan-renderer.js +79 -0
  278. package/dist/ui/plan-renderer.js.map +1 -0
  279. package/dist/ui/prompt.d.ts +11 -0
  280. package/dist/ui/prompt.d.ts.map +1 -0
  281. package/dist/ui/prompt.js +1960 -0
  282. package/dist/ui/prompt.js.map +1 -0
  283. package/dist/ui/render-primitives.d.ts +117 -0
  284. package/dist/ui/render-primitives.d.ts.map +1 -0
  285. package/dist/ui/render-primitives.js +322 -0
  286. package/dist/ui/render-primitives.js.map +1 -0
  287. package/dist/ui/render.d.ts +133 -0
  288. package/dist/ui/render.d.ts.map +1 -0
  289. package/dist/ui/render.js +547 -0
  290. package/dist/ui/render.js.map +1 -0
  291. package/dist/ui/session-header.d.ts +30 -0
  292. package/dist/ui/session-header.d.ts.map +1 -0
  293. package/dist/ui/session-header.js +74 -0
  294. package/dist/ui/session-header.js.map +1 -0
  295. package/dist/workspace-guard.d.ts +68 -0
  296. package/dist/workspace-guard.d.ts.map +1 -0
  297. package/dist/workspace-guard.js +168 -0
  298. package/dist/workspace-guard.js.map +1 -0
  299. package/dist/workspace-lock.d.ts +27 -0
  300. package/dist/workspace-lock.d.ts.map +1 -0
  301. package/dist/workspace-lock.js +95 -0
  302. package/dist/workspace-lock.js.map +1 -0
  303. package/package.json +63 -0
@@ -0,0 +1,154 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { LspClient, getLanguageId } from './lsp-client.js';
4
+ function findBinaryInPath(binaryName) {
5
+ const pathEnv = process.env.PATH || '';
6
+ const delimiter = process.platform === 'win32' ? ';' : ':';
7
+ const dirs = pathEnv.split(delimiter);
8
+ for (const dir of dirs) {
9
+ const baseNames = [binaryName];
10
+ if (process.platform === 'win32') {
11
+ baseNames.push(`${binaryName}.cmd`, `${binaryName}.bat`, `${binaryName}.exe`);
12
+ }
13
+ for (const name of baseNames) {
14
+ const fullPath = path.join(dir, name);
15
+ try {
16
+ if (fs.existsSync(fullPath)) {
17
+ const stat = fs.statSync(fullPath);
18
+ if (process.platform === 'win32' || (stat.mode & 0o111) !== 0) {
19
+ return fullPath;
20
+ }
21
+ }
22
+ }
23
+ catch {
24
+ // Ignore files we cannot access
25
+ }
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ export class LspManager {
31
+ workspaceRoot;
32
+ clients = new Map();
33
+ failedSearches = new Set();
34
+ constructor(workspaceRoot) {
35
+ this.workspaceRoot = workspaceRoot;
36
+ process.on('exit', () => this.killAllSync());
37
+ process.on('SIGINT', () => {
38
+ this.killAllSync();
39
+ process.exit(130);
40
+ });
41
+ }
42
+ killAllSync() {
43
+ for (const client of this.clients.values()) {
44
+ client.killSync();
45
+ }
46
+ }
47
+ async getOrStartClient(filePath) {
48
+ const ext = path.extname(filePath).toLowerCase();
49
+ let binaryName = '';
50
+ let args = [];
51
+ const languageId = getLanguageId(filePath);
52
+ if (!languageId)
53
+ return null;
54
+ if (languageId === 'typescript' || languageId === 'javascript') {
55
+ binaryName = 'typescript-language-server';
56
+ args = ['--stdio'];
57
+ }
58
+ else if (languageId === 'python') {
59
+ binaryName = 'pyright-langserver';
60
+ args = ['--stdio'];
61
+ }
62
+ else if (languageId === 'go') {
63
+ binaryName = 'gopls';
64
+ args = [];
65
+ }
66
+ else if (languageId === 'rust') {
67
+ binaryName = 'rust-analyzer';
68
+ args = [];
69
+ }
70
+ else {
71
+ return null;
72
+ }
73
+ if (this.clients.has(languageId)) {
74
+ return this.clients.get(languageId);
75
+ }
76
+ if (this.failedSearches.has(binaryName)) {
77
+ return null;
78
+ }
79
+ let binaryPath = findBinaryInPath(binaryName);
80
+ // If JS/TS and not found, we no longer forcefully globally install.
81
+ // Let it fail gracefully and inform the user.
82
+ if (!binaryPath && (binaryName === 'typescript-language-server')) {
83
+ console.warn(`\n[LSP Manager] JS/TS language server not found on PATH. Run 'npm install -g typescript-language-server typescript' to enable LSP features.`);
84
+ }
85
+ if (!binaryPath) {
86
+ this.failedSearches.add(binaryName);
87
+ console.warn(`\n[LSP Manager] Warning: '${binaryName}' not found on PATH. LSP features for '${ext}' files are disabled.`);
88
+ return null;
89
+ }
90
+ const client = new LspClient(binaryPath, args, this.workspaceRoot);
91
+ try {
92
+ await client.start();
93
+ this.clients.set(languageId, client);
94
+ return client;
95
+ }
96
+ catch (err) {
97
+ console.error(`[LSP Manager] Failed to start LSP client for ${binaryName}:`, err);
98
+ this.failedSearches.add(binaryName);
99
+ return null;
100
+ }
101
+ }
102
+ syncFileFromDisk(filePath, client) {
103
+ try {
104
+ if (fs.existsSync(filePath)) {
105
+ const content = fs.readFileSync(filePath, 'utf-8');
106
+ client.syncFile(filePath, content);
107
+ }
108
+ }
109
+ catch (err) {
110
+ console.error(`[LSP Manager] Failed to read and sync file: ${filePath}`, err.message);
111
+ }
112
+ }
113
+ async getClientAndSync(filePath) {
114
+ const client = await this.getOrStartClient(filePath);
115
+ if (client) {
116
+ this.syncFileFromDisk(filePath, client);
117
+ }
118
+ return client;
119
+ }
120
+ async gotoDefinition(filePath, line, character) {
121
+ const client = await this.getClientAndSync(filePath);
122
+ if (!client)
123
+ return null;
124
+ return client.gotoDefinition(filePath, line, character);
125
+ }
126
+ async findReferences(filePath, line, character) {
127
+ const client = await this.getClientAndSync(filePath);
128
+ if (!client)
129
+ return null;
130
+ return client.findReferences(filePath, line, character);
131
+ }
132
+ async hover(filePath, line, character) {
133
+ const client = await this.getClientAndSync(filePath);
134
+ if (!client)
135
+ return null;
136
+ return client.hover(filePath, line, character);
137
+ }
138
+ async getDiagnostics(filePath) {
139
+ const client = await this.getClientAndSync(filePath);
140
+ if (!client)
141
+ return [];
142
+ // Give LSP a short tick (100ms) to publish diagnostics if this was the first open/sync
143
+ await new Promise((resolve) => setTimeout(resolve, 100));
144
+ return client.getDiagnostics(filePath);
145
+ }
146
+ async stopAll() {
147
+ for (const client of this.clients.values()) {
148
+ await client.stop();
149
+ }
150
+ this.clients.clear();
151
+ this.failedSearches.clear();
152
+ }
153
+ }
154
+ //# sourceMappingURL=lsp-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lsp-manager.js","sourceRoot":"","sources":["../../src/lsp/lsp-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Live Pre-Save LSP Compilation Check — Pillar 3 of the Phase 2
3
+ * safety refactor. The problem: an LLM can produce syntactically
4
+ * valid TypeScript that is semantically broken (undefined
5
+ * references, wrong import paths, type errors). A workspace
6
+ * corrupted by a "looks-fine" write is worse than a workspace
7
+ * that crashes loudly.
8
+ *
9
+ * The fix: after the new content is staged, query the local
10
+ * language server for diagnostics. If the user-configured mode
11
+ * is `block` and any errors are present, refuse the commit and
12
+ * roll the staging back so the user keeps the original file. If
13
+ * the mode is `warn`, log the diagnostics but commit anyway. If
14
+ * the mode is `off`, the gate is a no-op.
15
+ *
16
+ * The gate is constructed lazily on first use and cached in a
17
+ * module-level singleton. The diagnostics call is bounded by a
18
+ * 500ms hard timeout so the staging path never blocks the agent
19
+ * loop indefinitely waiting for a slow or hung language server.
20
+ *
21
+ * The gate is fully decoupled from `LspManager` via a
22
+ * `LspDiagnosticsProvider` function — production code supplies
23
+ * one that wraps `LspManager.getDiagnostics`; tests supply a
24
+ * mock that returns canned data.
25
+ */
26
+ import type { StagedWrite } from '../runtime/staging.js';
27
+ /** Pre-save gate severity. Mirrors `LspPreSaveMode` in `config.ts`. */
28
+ export type LspPreSaveMode = 'off' | 'warn' | 'block' | 'sandbox-mock';
29
+ /** Severity, normalised from the LSP `DiagnosticSeverity` int. */
30
+ export type LspDiagnosticSeverity = 'error' | 'warning' | 'info' | 'hint';
31
+ /** A single diagnostic, normalised. */
32
+ export interface LspDiagnostic {
33
+ readonly severity: LspDiagnosticSeverity;
34
+ readonly message: string;
35
+ readonly line: number;
36
+ readonly column: number;
37
+ readonly source: string;
38
+ readonly code: string | number | null;
39
+ }
40
+ /** Result of a single pre-save check. */
41
+ export type LspPreSaveResult = {
42
+ readonly state: 'ok';
43
+ } | {
44
+ readonly state: 'no-language-server';
45
+ } | {
46
+ readonly state: 'diagnostics';
47
+ readonly diagnostics: ReadonlyArray<LspDiagnostic>;
48
+ /** Convenience: the count of `error`-severity items. */
49
+ readonly errorCount: number;
50
+ };
51
+ /** Function that returns diagnostics for a file (or empty). */
52
+ export type LspDiagnosticsProvider = (filePath: string) => Promise<LspDiagnostic[]>;
53
+ /** Options for {@link LspPreSaveGate}. */
54
+ export interface LspPreSaveGateOptions {
55
+ /** The mode. Defaults to `'off'`. */
56
+ mode?: LspPreSaveMode;
57
+ /** The diagnostics provider (defaults to a noop). */
58
+ provider?: LspDiagnosticsProvider;
59
+ /**
60
+ * Predicate that reports whether a real language server is
61
+ * installed for the given file path. Used by `sandbox-mock`
62
+ * mode to distinguish "LSP returned 0 diagnostics" (real pass)
63
+ * from "no LSP installed, unvalidated write" (must be
64
+ * blocked in sandbox-mock mode). Defaults to `true` — i.e.
65
+ * the gate assumes an LSP is available unless the caller
66
+ * overrides this.
67
+ */
68
+ hasLanguageServer?: (filePath: string) => boolean;
69
+ /** Maximum time (ms) to wait for diagnostics. Defaults to 500. */
70
+ timeoutMs?: number;
71
+ /**
72
+ * Optional callback invoked with the gate's decision before the
73
+ * caller (the staging manager) acts on it. Useful for telemetry
74
+ * and for warning-mode logging. Errors thrown here are caught
75
+ * and swallowed — they never block the write.
76
+ */
77
+ onResult?: (result: LspPreSaveResult, entry: StagedWrite) => void;
78
+ }
79
+ /** Map the LSP `DiagnosticSeverity` int to our normalised severity. */
80
+ export declare function normaliseSeverity(int: unknown): LspDiagnosticSeverity;
81
+ /** Normalise a raw LSP `Diagnostic` payload into our typed shape. */
82
+ export declare function normaliseDiagnostic(raw: unknown): LspDiagnostic;
83
+ export declare class LspPreSaveGate {
84
+ readonly mode: LspPreSaveMode;
85
+ readonly provider: LspDiagnosticsProvider;
86
+ readonly timeoutMs: number;
87
+ readonly onResult: ((r: LspPreSaveResult, e: StagedWrite) => void) | undefined;
88
+ readonly hasLanguageServer: (filePath: string) => boolean;
89
+ constructor(options?: LspPreSaveGateOptions);
90
+ /**
91
+ * Run the gate against a staged write. Returns one of:
92
+ *
93
+ * - `ok` — the file passes the gate.
94
+ * - `no-language-server`— the provider returned no diagnostics and
95
+ * we have no LSP installed; treat as a
96
+ * pass-through so users without a language
97
+ * server aren't blocked.
98
+ * - `diagnostics` — there are diagnostics; the caller
99
+ * decides what to do based on the gate's
100
+ * mode and on the `errorCount`.
101
+ */
102
+ check(entry: StagedWrite): Promise<LspPreSaveResult>;
103
+ /**
104
+ * Throw an {@link LspPreSaveBlockedError} if the result should
105
+ * be a hard block. The caller (the staging pipeline) uses this
106
+ * as the pre-commit hook. In `warn` mode this always returns
107
+ * without throwing. In `block` mode it throws when there is at
108
+ * least one `error`-severity diagnostic. In `sandbox-mock` mode
109
+ * (Pillar 5 / Protection 3) it throws when the result was
110
+ * `no-language-server` — the gate refuses to allow unvalidated
111
+ * writes when no LSP is available, forcing the operator to
112
+ * install a real language server or downgrade to `off`.
113
+ */
114
+ enforce(result: LspPreSaveResult, entry: StagedWrite): void;
115
+ private notify;
116
+ private withTimeout;
117
+ }
118
+ /**
119
+ * Thrown by {@link LspPreSaveGate.enforce} when the gate's mode is
120
+ * `block` and the file has at least one `error`-severity
121
+ * diagnostic. Caught by {@link AtomicStagingManager.commit} and
122
+ * surfaced as a {@link PreCommitHookRejectedError}.
123
+ */
124
+ export declare class LspPreSaveBlockedError extends Error {
125
+ readonly targetPath: string;
126
+ readonly diagnostics: ReadonlyArray<LspDiagnostic>;
127
+ constructor(targetPath: string, diagnostics: ReadonlyArray<LspDiagnostic>);
128
+ }
129
+ /**
130
+ * Build a {@link LspDiagnosticsProvider} from an
131
+ * {@link LspManager}-shaped object. Decouples the gate from the
132
+ * concrete `LspManager` class so tests can inject a mock.
133
+ */
134
+ export declare function makeLspProvider(lspManager: {
135
+ getClientAndSync(filePath: string): Promise<unknown>;
136
+ }): LspDiagnosticsProvider;
137
+ //# sourceMappingURL=lsp-pre-save.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lsp-pre-save.d.ts","sourceRoot":"","sources":["../../src/lsp/lsp-pre-save.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Live Pre-Save LSP Compilation Check — Pillar 3 of the Phase 2
3
+ * safety refactor. The problem: an LLM can produce syntactically
4
+ * valid TypeScript that is semantically broken (undefined
5
+ * references, wrong import paths, type errors). A workspace
6
+ * corrupted by a "looks-fine" write is worse than a workspace
7
+ * that crashes loudly.
8
+ *
9
+ * The fix: after the new content is staged, query the local
10
+ * language server for diagnostics. If the user-configured mode
11
+ * is `block` and any errors are present, refuse the commit and
12
+ * roll the staging back so the user keeps the original file. If
13
+ * the mode is `warn`, log the diagnostics but commit anyway. If
14
+ * the mode is `off`, the gate is a no-op.
15
+ *
16
+ * The gate is constructed lazily on first use and cached in a
17
+ * module-level singleton. The diagnostics call is bounded by a
18
+ * 500ms hard timeout so the staging path never blocks the agent
19
+ * loop indefinitely waiting for a slow or hung language server.
20
+ *
21
+ * The gate is fully decoupled from `LspManager` via a
22
+ * `LspDiagnosticsProvider` function — production code supplies
23
+ * one that wraps `LspManager.getDiagnostics`; tests supply a
24
+ * mock that returns canned data.
25
+ */
26
+ // ---------------------------------------------------------------------------
27
+ // Helpers
28
+ // ---------------------------------------------------------------------------
29
+ /** Map the LSP `DiagnosticSeverity` int to our normalised severity. */
30
+ export function normaliseSeverity(int) {
31
+ switch (int) {
32
+ case 1:
33
+ return 'error';
34
+ case 2:
35
+ return 'warning';
36
+ case 3:
37
+ return 'info';
38
+ case 4:
39
+ return 'hint';
40
+ default:
41
+ return 'info';
42
+ }
43
+ }
44
+ /** Normalise a raw LSP `Diagnostic` payload into our typed shape. */
45
+ export function normaliseDiagnostic(raw) {
46
+ if (!raw || typeof raw !== 'object') {
47
+ return {
48
+ severity: 'info',
49
+ message: String(raw),
50
+ line: 0,
51
+ column: 0,
52
+ source: '',
53
+ code: null,
54
+ };
55
+ }
56
+ const r = raw;
57
+ const start = r.range?.start;
58
+ return {
59
+ severity: normaliseSeverity(r.severity),
60
+ message: typeof r.message === 'string' ? r.message : String(r.message ?? ''),
61
+ line: typeof start?.line === 'number' ? start.line : 0,
62
+ column: typeof start?.character === 'number' ? start.character : 0,
63
+ source: typeof r.source === 'string' ? r.source : '',
64
+ code: typeof r.code === 'string' || typeof r.code === 'number' ? r.code : null,
65
+ };
66
+ }
67
+ const noopProvider = async () => [];
68
+ // ---------------------------------------------------------------------------
69
+ // LspPreSaveGate
70
+ // ---------------------------------------------------------------------------
71
+ export class LspPreSaveGate {
72
+ mode;
73
+ provider;
74
+ timeoutMs;
75
+ onResult;
76
+ hasLanguageServer;
77
+ constructor(options = {}) {
78
+ this.mode = options.mode ?? 'off';
79
+ this.provider = options.provider ?? noopProvider;
80
+ this.timeoutMs = options.timeoutMs ?? 500;
81
+ this.onResult = options.onResult;
82
+ this.hasLanguageServer = options.hasLanguageServer ?? (() => true);
83
+ }
84
+ /**
85
+ * Run the gate against a staged write. Returns one of:
86
+ *
87
+ * - `ok` — the file passes the gate.
88
+ * - `no-language-server`— the provider returned no diagnostics and
89
+ * we have no LSP installed; treat as a
90
+ * pass-through so users without a language
91
+ * server aren't blocked.
92
+ * - `diagnostics` — there are diagnostics; the caller
93
+ * decides what to do based on the gate's
94
+ * mode and on the `errorCount`.
95
+ */
96
+ async check(entry) {
97
+ if (this.mode === 'off') {
98
+ return { state: 'ok' };
99
+ }
100
+ let raw;
101
+ try {
102
+ raw = await this.withTimeout(this.provider(entry.targetPath), this.timeoutMs);
103
+ }
104
+ catch {
105
+ // Provider threw or timed out — treat as a pass-through so a
106
+ // hung language server does not block the agent loop.
107
+ return { state: 'no-language-server' };
108
+ }
109
+ if (raw.length === 0) {
110
+ // Provider returned zero diagnostics. Distinguish
111
+ // between "LSP installed, file is clean" (→ `ok`) and
112
+ // "no LSP installed, write is unvalidated" (→
113
+ // `no-language-server`). The latter is what the
114
+ // sandbox-mock mode rejects.
115
+ if (!this.hasLanguageServer(entry.targetPath)) {
116
+ const result = { state: 'no-language-server' };
117
+ this.notify(result, entry);
118
+ return result;
119
+ }
120
+ const result = { state: 'ok' };
121
+ this.notify(result, entry);
122
+ return result;
123
+ }
124
+ const errorCount = raw.filter((d) => d.severity === 'error').length;
125
+ const result = {
126
+ state: 'diagnostics',
127
+ diagnostics: raw,
128
+ errorCount,
129
+ };
130
+ this.notify(result, entry);
131
+ return result;
132
+ }
133
+ /**
134
+ * Throw an {@link LspPreSaveBlockedError} if the result should
135
+ * be a hard block. The caller (the staging pipeline) uses this
136
+ * as the pre-commit hook. In `warn` mode this always returns
137
+ * without throwing. In `block` mode it throws when there is at
138
+ * least one `error`-severity diagnostic. In `sandbox-mock` mode
139
+ * (Pillar 5 / Protection 3) it throws when the result was
140
+ * `no-language-server` — the gate refuses to allow unvalidated
141
+ * writes when no LSP is available, forcing the operator to
142
+ * install a real language server or downgrade to `off`.
143
+ */
144
+ enforce(result, entry) {
145
+ if (this.mode === 'off')
146
+ return;
147
+ if (this.mode === 'warn')
148
+ return;
149
+ if (this.mode === 'sandbox-mock') {
150
+ // Pillar 5 / Protection 3 — refuse to commit a write
151
+ // when the gate has no language server backing it. The
152
+ // operator must explicitly opt out by setting the mode
153
+ // to `off` (or by installing a real LSP).
154
+ if (result.state === 'no-language-server') {
155
+ throw new LspPreSaveBlockedError(entry.targetPath, [
156
+ {
157
+ severity: 'error',
158
+ message: 'sandbox-mock: no language server is available to validate this write. ' +
159
+ 'Install typescript-language-server / pyright etc., or set ' +
160
+ 'preferences.safety.lspPreSave to "off" to disable pre-save validation.',
161
+ line: 0,
162
+ column: 0,
163
+ source: 'fixo-safety',
164
+ code: 'SANDBOX_MOCK_NO_LSP',
165
+ },
166
+ ]);
167
+ }
168
+ return;
169
+ }
170
+ // `block` mode — fail on any error-severity diagnostic.
171
+ if (result.state !== 'diagnostics')
172
+ return;
173
+ if (result.errorCount === 0)
174
+ return;
175
+ throw new LspPreSaveBlockedError(entry.targetPath, result.diagnostics);
176
+ }
177
+ notify(result, entry) {
178
+ if (!this.onResult)
179
+ return;
180
+ try {
181
+ this.onResult(result, entry);
182
+ }
183
+ catch {
184
+ // Swallow observer errors — they are best-effort.
185
+ }
186
+ }
187
+ withTimeout(p, ms) {
188
+ return new Promise((resolve, reject) => {
189
+ const timer = setTimeout(() => reject(new Error('LSP gate timeout')), ms);
190
+ p.then((v) => {
191
+ clearTimeout(timer);
192
+ resolve(v);
193
+ }, (err) => {
194
+ clearTimeout(timer);
195
+ reject(err);
196
+ });
197
+ });
198
+ }
199
+ }
200
+ // ---------------------------------------------------------------------------
201
+ // Errors
202
+ // ---------------------------------------------------------------------------
203
+ /**
204
+ * Thrown by {@link LspPreSaveGate.enforce} when the gate's mode is
205
+ * `block` and the file has at least one `error`-severity
206
+ * diagnostic. Caught by {@link AtomicStagingManager.commit} and
207
+ * surfaced as a {@link PreCommitHookRejectedError}.
208
+ */
209
+ export class LspPreSaveBlockedError extends Error {
210
+ targetPath;
211
+ diagnostics;
212
+ constructor(targetPath, diagnostics) {
213
+ const summary = diagnostics
214
+ .filter((d) => d.severity === 'error')
215
+ .slice(0, 3)
216
+ .map((d) => `${d.line + 1}:${d.column + 1} ${d.message}`)
217
+ .join('; ');
218
+ super(`LSP pre-save blocked: ${diagnostics.filter((d) => d.severity === 'error').length} ` +
219
+ `error(s) in ${targetPath}${summary ? ` — ${summary}` : ''}`);
220
+ this.name = 'LspPreSaveBlockedError';
221
+ this.targetPath = targetPath;
222
+ this.diagnostics = diagnostics;
223
+ }
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Adapter — wire LspManager into the gate
227
+ // ---------------------------------------------------------------------------
228
+ /**
229
+ * Build a {@link LspDiagnosticsProvider} from an
230
+ * {@link LspManager}-shaped object. Decouples the gate from the
231
+ * concrete `LspManager` class so tests can inject a mock.
232
+ */
233
+ export function makeLspProvider(lspManager) {
234
+ return async (filePath) => {
235
+ const client = (await lspManager.getClientAndSync(filePath));
236
+ if (!client)
237
+ return [];
238
+ // Give the LSP a short tick to publish (matches LspManager's
239
+ // existing behaviour for the very first open).
240
+ await new Promise((resolve) => setTimeout(resolve, 100));
241
+ const raw = client.getDiagnostics(filePath);
242
+ return Array.isArray(raw) ? raw.map((d) => normaliseDiagnostic(d)) : [];
243
+ };
244
+ }
245
+ //# sourceMappingURL=lsp-pre-save.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lsp-pre-save.js","sourceRoot":"","sources":["../../src/lsp/lsp-pre-save.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * syntax-fallback.ts — Pure-JS brace/paren/bracket balance check
3
+ * used as a fallback when no real language server is on the PATH.
4
+ *
5
+ * Why this exists
6
+ * ---------------
7
+ * The LspPreSaveGate calls into `LspManager` to ask the local
8
+ * language server for diagnostics. On a freshly-installed system
9
+ * there is no `tsserver` / `gopls` / `rust-analyzer` on the PATH —
10
+ * the user has a working FixO CLI but no editor infrastructure.
11
+ * In that case the pre-save gate would silently fall through and
12
+ * commit syntactically broken edits.
13
+ *
14
+ * This module provides a *very* cheap structural sanity check
15
+ * (brace/paren/bracket/quote balance) that runs in microseconds
16
+ * and catches the most common form of "LLM forgot a closing
17
+ * brace" corruption. It is intentionally not a real parser — it
18
+ * is a smoke detector, not a smoke alarm. If the syntax check
19
+ * reports `ok`, the real LSP may still surface semantic errors;
20
+ * if it reports `unbalanced`, the file is almost certainly
21
+ * broken and we should refuse the write.
22
+ *
23
+ * The check is env-gated behind `FIXO_LSP_FALLBACK=syntax-only`
24
+ * so a developer who *does* have a language server is never
25
+ * bothered by it.
26
+ */
27
+ export type SyntaxHealthVerdict = {
28
+ readonly state: 'ok';
29
+ } | {
30
+ readonly state: 'unbalanced';
31
+ /** The first unclosed delimiter, in document order. */
32
+ readonly opener: '{' | '(' | '[';
33
+ /** 1-based line where the imbalance was detected. */
34
+ readonly line: number;
35
+ } | {
36
+ readonly state: 'unterminated-string';
37
+ /** 1-based line where the runaway string starts. */
38
+ readonly line: number;
39
+ } | {
40
+ readonly state: 'unterminated-comment';
41
+ /** 1-based line where the block comment starts. */
42
+ readonly line: number;
43
+ };
44
+ /**
45
+ * Run the structural sanity check on a source string. Pure,
46
+ * sync, and allocation-light. The output is stable so tests can
47
+ * pin the verdict exactly.
48
+ */
49
+ export declare function syntaxHealthCheck(source: string): SyntaxHealthVerdict;
50
+ /**
51
+ * Format a {@link SyntaxHealthVerdict} as a single-line summary
52
+ * suitable for the LLM's `tool_result`.
53
+ */
54
+ export declare function formatSyntaxVerdict(verdict: SyntaxHealthVerdict): string;
55
+ export interface LspSanityResult {
56
+ /** True when at least one common language server is on the PATH,
57
+ * or when the syntax-fallback mode is explicitly enabled. */
58
+ ok: boolean;
59
+ /** Human-readable reason. Empty when `ok`. */
60
+ reason: string;
61
+ /** The language servers we looked for. */
62
+ checked: string[];
63
+ /** The ones we actually found on the PATH. */
64
+ found: string[];
65
+ /** Whether `FIXO_LSP_FALLBACK=syntax-only` is set. */
66
+ syntaxOnly: boolean;
67
+ }
68
+ /**
69
+ * Synchronous PATH check for common language servers. Returns a
70
+ * {@link LspSanityResult} the boot code can use to decide whether
71
+ * to warn the user. Pure — no side effects, no I/O beyond
72
+ * `which`-style PATH scanning.
73
+ *
74
+ * The check is intentionally permissive: a missing language server
75
+ * is a `reason` to warn, not a hard failure. The user can always
76
+ * install one later. The hard fail mode is when neither a
77
+ * language server nor `FIXO_LSP_FALLBACK=syntax-only` is present
78
+ * AND the user has configured `lspPreSave: 'block'` — in that
79
+ * case the pre-save gate would block all writes, which is a much
80
+ * worse experience than a boot-time warning.
81
+ */
82
+ export declare function checkLspSanity(env?: NodeJS.ProcessEnv): LspSanityResult;
83
+ //# sourceMappingURL=syntax-fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syntax-fallback.d.ts","sourceRoot":"","sources":["../../src/lsp/syntax-fallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GACxB;IACE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACjC,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GACD;IACE,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC;IACtC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GACD;IACE,QAAQ,CAAC,KAAK,EAAE,sBAAsB,CAAC;IACvC,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB,CA8IrE;AAcD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAWxE;AAMD,MAAM,WAAW,eAAe;IAC9B;kEAC8D;IAC9D,EAAE,EAAE,OAAO,CAAC;IACZ,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,sDAAsD;IACtD,UAAU,EAAE,OAAO,CAAC;CACrB;AAeD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,eAAe,CA4CpF"}