wogiflow 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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,923 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * flow-lsp.js - LSP Client for Wogi Flow
4
+ *
5
+ * Provides Language Server Protocol integration for:
6
+ * - Type information at cursor position
7
+ * - Diagnostics (errors/warnings)
8
+ * - Go to definition
9
+ * - Completions
10
+ *
11
+ * Used by hybrid mode to get accurate type info instead of guessing.
12
+ */
13
+
14
+ const { spawn, spawnSync } = require('child_process');
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+ const { getConfig, PROJECT_ROOT, colors, success, warn, error } = require('./flow-utils');
18
+
19
+ /**
20
+ * Convert a file path to a proper file:// URI
21
+ * Handles Windows paths correctly (file:///C:/path vs file:///home/user)
22
+ */
23
+ function pathToFileUri(filePath) {
24
+ // Normalize to forward slashes
25
+ let normalized = filePath.replace(/\\/g, '/');
26
+
27
+ // On Windows, paths like C:/foo need an extra slash: file:///C:/foo
28
+ // On Unix, paths like /home/foo just need file:///home/foo
29
+ if (/^[a-zA-Z]:/.test(normalized)) {
30
+ // Windows path with drive letter
31
+ return `file:///${normalized}`;
32
+ }
33
+
34
+ // Unix path - already starts with /
35
+ return `file://${normalized}`;
36
+ }
37
+
38
+ // ─────────────────────────────────────────────────────────────
39
+ // LSP Client Class
40
+ // ─────────────────────────────────────────────────────────────
41
+
42
+ class LSPClient {
43
+ constructor(projectRoot) {
44
+ this.projectRoot = projectRoot;
45
+ this.process = null;
46
+ this.requestId = 0;
47
+ this.pending = new Map();
48
+ this.initialized = false;
49
+ this.buffer = '';
50
+ this.diagnosticsCache = new Map();
51
+ }
52
+
53
+ /**
54
+ * Start the LSP server
55
+ */
56
+ async start() {
57
+ const config = getConfig();
58
+ const serverCommand = config.lsp?.server || 'typescript-language-server';
59
+
60
+ // Find the language server
61
+ const tsserver = this._findServer(serverCommand);
62
+ if (!tsserver) {
63
+ throw new Error(
64
+ `${serverCommand} not found. Install with:\n` +
65
+ ' npm i -g typescript-language-server typescript\n' +
66
+ 'Or for local install:\n' +
67
+ ' npm i -D typescript-language-server typescript'
68
+ );
69
+ }
70
+
71
+ // Spawn the server
72
+ this.process = spawn(tsserver, ['--stdio'], {
73
+ cwd: this.projectRoot,
74
+ env: { ...process.env },
75
+ stdio: ['pipe', 'pipe', 'pipe']
76
+ });
77
+
78
+ // Set up I/O handling
79
+ this._setupIO();
80
+
81
+ // Initialize the connection
82
+ await this._initialize();
83
+
84
+ return this;
85
+ }
86
+
87
+ /**
88
+ * Find the language server executable
89
+ */
90
+ _findServer(serverCommand) {
91
+ // Check common locations
92
+ const locations = [
93
+ serverCommand,
94
+ path.join(this.projectRoot, 'node_modules/.bin', serverCommand),
95
+ path.join(this.projectRoot, 'node_modules/.bin/typescript-language-server'),
96
+ '/usr/local/bin/typescript-language-server'
97
+ ];
98
+
99
+ for (const loc of locations) {
100
+ try {
101
+ const result = spawnSync('which', [loc], { encoding: 'utf-8' });
102
+ if (result.status === 0) {
103
+ return result.stdout.trim() || loc;
104
+ }
105
+ } catch (err) {
106
+ // Try next location
107
+ }
108
+
109
+ // Also try direct execution check
110
+ try {
111
+ const result = spawnSync(loc, ['--version'], { encoding: 'utf-8', timeout: 5000 });
112
+ if (result.status === 0) {
113
+ return loc;
114
+ }
115
+ } catch (err) {
116
+ // Try next location
117
+ }
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Set up stdin/stdout handling for LSP protocol
125
+ */
126
+ _setupIO() {
127
+ this.process.stdout.on('data', (data) => {
128
+ this.buffer += data.toString();
129
+ this._parseMessages();
130
+ });
131
+
132
+ this.process.stderr.on('data', (data) => {
133
+ // Log stderr but don't crash
134
+ if (process.env.DEBUG_LSP) {
135
+ console.error('[LSP stderr]', data.toString());
136
+ }
137
+ });
138
+
139
+ this.process.on('error', (err) => {
140
+ error(`LSP process error: ${err.message}`);
141
+ this.initialized = false;
142
+ });
143
+
144
+ this.process.on('exit', (code) => {
145
+ if (code !== 0 && process.env.DEBUG_LSP) {
146
+ console.error(`[LSP] Process exited with code ${code}`);
147
+ }
148
+ this.initialized = false;
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Parse LSP messages from buffer
154
+ */
155
+ _parseMessages() {
156
+ while (true) {
157
+ // Look for Content-Length header
158
+ const headerMatch = this.buffer.match(/Content-Length: (\d+)\r\n\r\n/);
159
+ if (!headerMatch) break;
160
+
161
+ const contentLength = parseInt(headerMatch[1], 10);
162
+ const headerEnd = headerMatch.index + headerMatch[0].length;
163
+
164
+ // Check if we have the full message
165
+ if (this.buffer.length < headerEnd + contentLength) break;
166
+
167
+ // Extract the message
168
+ const messageStr = this.buffer.slice(headerEnd, headerEnd + contentLength);
169
+ this.buffer = this.buffer.slice(headerEnd + contentLength);
170
+
171
+ try {
172
+ const message = JSON.parse(messageStr);
173
+ this._handleMessage(message);
174
+ } catch (err) {
175
+ if (process.env.DEBUG_LSP) {
176
+ console.error('[LSP] Failed to parse message:', err.message);
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Handle incoming LSP message
184
+ */
185
+ _handleMessage(msg) {
186
+ // Response to a request
187
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
188
+ const { resolve, reject, timeout } = this.pending.get(msg.id);
189
+ clearTimeout(timeout);
190
+ this.pending.delete(msg.id);
191
+
192
+ if (msg.error) {
193
+ reject(new Error(msg.error.message || 'LSP error'));
194
+ } else {
195
+ resolve(msg.result);
196
+ }
197
+ return;
198
+ }
199
+
200
+ // Notification (e.g., publishDiagnostics)
201
+ if (msg.method === 'textDocument/publishDiagnostics') {
202
+ this._handleDiagnostics(msg.params);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Handle diagnostics notification
208
+ */
209
+ _handleDiagnostics(params) {
210
+ const uri = params.uri;
211
+ const diagnostics = params.diagnostics || [];
212
+ this.diagnosticsCache.set(uri, diagnostics);
213
+ }
214
+
215
+ /**
216
+ * Send a request to the LSP server
217
+ */
218
+ _send(method, params) {
219
+ const config = getConfig();
220
+ const timeout = config.lsp?.timeout || 5000;
221
+
222
+ return new Promise((resolve, reject) => {
223
+ const id = ++this.requestId;
224
+
225
+ // Set up timeout
226
+ const timeoutId = setTimeout(() => {
227
+ if (this.pending.has(id)) {
228
+ this.pending.delete(id);
229
+ reject(new Error(`LSP request timeout: ${method}`));
230
+ }
231
+ }, timeout);
232
+
233
+ this.pending.set(id, { resolve, reject, timeout: timeoutId });
234
+
235
+ const message = JSON.stringify({
236
+ jsonrpc: '2.0',
237
+ id,
238
+ method,
239
+ params
240
+ });
241
+
242
+ const header = `Content-Length: ${Buffer.byteLength(message)}\r\n\r\n`;
243
+ this.process.stdin.write(header + message);
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Send a notification (no response expected)
249
+ */
250
+ _notify(method, params) {
251
+ const message = JSON.stringify({
252
+ jsonrpc: '2.0',
253
+ method,
254
+ params
255
+ });
256
+ const header = `Content-Length: ${Buffer.byteLength(message)}\r\n\r\n`;
257
+ this.process.stdin.write(header + message);
258
+ }
259
+
260
+ /**
261
+ * Initialize the LSP connection
262
+ */
263
+ async _initialize() {
264
+ const result = await this._send('initialize', {
265
+ processId: process.pid,
266
+ rootUri: pathToFileUri(this.projectRoot),
267
+ rootPath: this.projectRoot,
268
+ capabilities: {
269
+ textDocument: {
270
+ hover: {
271
+ contentFormat: ['markdown', 'plaintext']
272
+ },
273
+ completion: {
274
+ completionItem: {
275
+ snippetSupport: true,
276
+ documentationFormat: ['markdown', 'plaintext']
277
+ }
278
+ },
279
+ synchronization: {
280
+ didSave: true,
281
+ willSave: false,
282
+ willSaveWaitUntil: false
283
+ },
284
+ publishDiagnostics: {
285
+ relatedInformation: true
286
+ }
287
+ },
288
+ workspace: {
289
+ workspaceFolders: true
290
+ }
291
+ },
292
+ workspaceFolders: [
293
+ { uri: pathToFileUri(this.projectRoot), name: path.basename(this.projectRoot) }
294
+ ]
295
+ });
296
+
297
+ // Send initialized notification
298
+ this._notify('initialized', {});
299
+
300
+ this.initialized = true;
301
+ this.serverCapabilities = result?.capabilities || {};
302
+
303
+ return result;
304
+ }
305
+
306
+ /**
307
+ * Open a document (required before querying)
308
+ */
309
+ async openDocument(filePath) {
310
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(this.projectRoot, filePath);
311
+ const uri = pathToFileUri(absPath);
312
+
313
+ if (!fs.existsSync(absPath)) {
314
+ throw new Error(`File not found: ${absPath}`);
315
+ }
316
+
317
+ const content = fs.readFileSync(absPath, 'utf-8');
318
+ const languageId = this._getLanguageId(absPath);
319
+
320
+ this._notify('textDocument/didOpen', {
321
+ textDocument: {
322
+ uri,
323
+ languageId,
324
+ version: 1,
325
+ text: content
326
+ }
327
+ });
328
+
329
+ // Wait a bit for the server to process
330
+ await new Promise(r => setTimeout(r, 100));
331
+
332
+ return uri;
333
+ }
334
+
335
+ /**
336
+ * Close a document
337
+ */
338
+ closeDocument(uri) {
339
+ this._notify('textDocument/didClose', {
340
+ textDocument: { uri }
341
+ });
342
+ }
343
+
344
+ /**
345
+ * Get language ID from file extension
346
+ */
347
+ _getLanguageId(filePath) {
348
+ const ext = path.extname(filePath).toLowerCase();
349
+ const map = {
350
+ '.ts': 'typescript',
351
+ '.tsx': 'typescriptreact',
352
+ '.js': 'javascript',
353
+ '.jsx': 'javascriptreact',
354
+ '.mjs': 'javascript',
355
+ '.cjs': 'javascript',
356
+ '.json': 'json'
357
+ };
358
+ return map[ext] || 'typescript';
359
+ }
360
+
361
+ // ─────────────────────────────────────────────────────────────
362
+ // Public API
363
+ // ─────────────────────────────────────────────────────────────
364
+
365
+ /**
366
+ * Get hover information at a position
367
+ * @param {string} filePath - Path to file
368
+ * @param {number} line - 0-indexed line number
369
+ * @param {number} character - 0-indexed character position
370
+ */
371
+ async hover(filePath, line, character) {
372
+ const uri = await this.openDocument(filePath);
373
+
374
+ try {
375
+ const result = await this._send('textDocument/hover', {
376
+ textDocument: { uri },
377
+ position: { line, character }
378
+ });
379
+ return result;
380
+ } finally {
381
+ this.closeDocument(uri);
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Get type at a specific position
387
+ * @returns {string|null} Type signature or null if not available
388
+ */
389
+ async getTypeAtPosition(filePath, line, character) {
390
+ const hover = await this.hover(filePath, line, character);
391
+ if (!hover?.contents) return null;
392
+
393
+ // Extract type from hover content
394
+ const content = typeof hover.contents === 'string'
395
+ ? hover.contents
396
+ : hover.contents.value || '';
397
+
398
+ // Parse TypeScript type signature from markdown code block
399
+ const codeBlockMatch = content.match(/```(?:typescript|ts)\n([\s\S]*?)\n```/);
400
+ if (codeBlockMatch) {
401
+ return codeBlockMatch[1].trim();
402
+ }
403
+
404
+ // Try to find type in plain format
405
+ const typeMatch = content.match(/^(\w+):\s*(.+)$/m);
406
+ if (typeMatch) {
407
+ return `${typeMatch[1]}: ${typeMatch[2]}`;
408
+ }
409
+
410
+ return content.trim() || null;
411
+ }
412
+
413
+ /**
414
+ * Get diagnostics for a file
415
+ * @param {string} filePath - Path to file
416
+ * @returns {Array} Array of diagnostic objects
417
+ */
418
+ async getDiagnostics(filePath) {
419
+ const uri = await this.openDocument(filePath);
420
+
421
+ // Wait for diagnostics to be pushed
422
+ await new Promise(r => setTimeout(r, 500));
423
+
424
+ const diagnostics = this.diagnosticsCache.get(uri) || [];
425
+
426
+ this.closeDocument(uri);
427
+
428
+ return diagnostics.map(d => ({
429
+ severity: this._diagnosticSeverity(d.severity),
430
+ message: d.message,
431
+ line: d.range?.start?.line,
432
+ character: d.range?.start?.character,
433
+ source: d.source,
434
+ code: d.code
435
+ }));
436
+ }
437
+
438
+ /**
439
+ * Convert diagnostic severity to string
440
+ */
441
+ _diagnosticSeverity(severity) {
442
+ const map = { 1: 'error', 2: 'warning', 3: 'info', 4: 'hint' };
443
+ return map[severity] || 'unknown';
444
+ }
445
+
446
+ /**
447
+ * Get completions at a position
448
+ * @param {string} filePath - Path to file
449
+ * @param {number} line - 0-indexed line number
450
+ * @param {number} character - 0-indexed character position
451
+ */
452
+ async getCompletions(filePath, line, character) {
453
+ const uri = await this.openDocument(filePath);
454
+
455
+ try {
456
+ const result = await this._send('textDocument/completion', {
457
+ textDocument: { uri },
458
+ position: { line, character }
459
+ });
460
+
461
+ const items = Array.isArray(result) ? result : (result?.items || []);
462
+
463
+ return items.map(item => ({
464
+ label: item.label,
465
+ kind: this._completionKind(item.kind),
466
+ detail: item.detail,
467
+ documentation: typeof item.documentation === 'string'
468
+ ? item.documentation
469
+ : item.documentation?.value
470
+ }));
471
+ } finally {
472
+ this.closeDocument(uri);
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Convert completion kind to string
478
+ */
479
+ _completionKind(kind) {
480
+ const map = {
481
+ 1: 'text', 2: 'method', 3: 'function', 4: 'constructor',
482
+ 5: 'field', 6: 'variable', 7: 'class', 8: 'interface',
483
+ 9: 'module', 10: 'property', 11: 'unit', 12: 'value',
484
+ 13: 'enum', 14: 'keyword', 15: 'snippet', 16: 'color',
485
+ 17: 'file', 18: 'reference', 19: 'folder', 20: 'enumMember',
486
+ 21: 'constant', 22: 'struct', 23: 'event', 24: 'operator',
487
+ 25: 'typeParameter'
488
+ };
489
+ return map[kind] || 'unknown';
490
+ }
491
+
492
+ /**
493
+ * Go to definition
494
+ * @param {string} filePath - Path to file
495
+ * @param {number} line - 0-indexed line number
496
+ * @param {number} character - 0-indexed character position
497
+ */
498
+ async getDefinition(filePath, line, character) {
499
+ const uri = await this.openDocument(filePath);
500
+
501
+ try {
502
+ const result = await this._send('textDocument/definition', {
503
+ textDocument: { uri },
504
+ position: { line, character }
505
+ });
506
+
507
+ const locations = Array.isArray(result) ? result : (result ? [result] : []);
508
+
509
+ return locations.map(loc => ({
510
+ uri: loc.uri || loc.targetUri,
511
+ path: (loc.uri || loc.targetUri)?.replace('file://', ''),
512
+ range: loc.range || loc.targetRange
513
+ }));
514
+ } finally {
515
+ this.closeDocument(uri);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Get document symbols (functions, classes, etc.)
521
+ * @param {string} filePath - Path to file
522
+ */
523
+ async getDocumentSymbols(filePath) {
524
+ const uri = await this.openDocument(filePath);
525
+
526
+ try {
527
+ const result = await this._send('textDocument/documentSymbol', {
528
+ textDocument: { uri }
529
+ });
530
+
531
+ return this._flattenSymbols(result || []);
532
+ } finally {
533
+ this.closeDocument(uri);
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Flatten hierarchical symbols
539
+ */
540
+ _flattenSymbols(symbols, parent = null) {
541
+ const flat = [];
542
+ for (const sym of symbols) {
543
+ flat.push({
544
+ name: sym.name,
545
+ kind: this._symbolKind(sym.kind),
546
+ parent: parent?.name,
547
+ range: sym.range || sym.location?.range,
548
+ selectionRange: sym.selectionRange
549
+ });
550
+ if (sym.children) {
551
+ flat.push(...this._flattenSymbols(sym.children, sym));
552
+ }
553
+ }
554
+ return flat;
555
+ }
556
+
557
+ /**
558
+ * Convert symbol kind to string
559
+ */
560
+ _symbolKind(kind) {
561
+ const map = {
562
+ 1: 'file', 2: 'module', 3: 'namespace', 4: 'package',
563
+ 5: 'class', 6: 'method', 7: 'property', 8: 'field',
564
+ 9: 'constructor', 10: 'enum', 11: 'interface', 12: 'function',
565
+ 13: 'variable', 14: 'constant', 15: 'string', 16: 'number',
566
+ 17: 'boolean', 18: 'array', 19: 'object', 20: 'key',
567
+ 21: 'null', 22: 'enumMember', 23: 'struct', 24: 'event',
568
+ 25: 'operator', 26: 'typeParameter'
569
+ };
570
+ return map[kind] || 'unknown';
571
+ }
572
+
573
+ /**
574
+ * Stop the LSP server
575
+ */
576
+ async stop() {
577
+ if (!this.process) return;
578
+
579
+ try {
580
+ await this._send('shutdown', null);
581
+ this._notify('exit', null);
582
+ } catch (err) {
583
+ // Ignore errors during shutdown
584
+ }
585
+
586
+ // Force kill if still running after 1 second
587
+ setTimeout(() => {
588
+ if (this.process && !this.process.killed) {
589
+ this.process.kill('SIGKILL');
590
+ }
591
+ }, 1000);
592
+
593
+ this.process = null;
594
+ this.initialized = false;
595
+ }
596
+ }
597
+
598
+ // ─────────────────────────────────────────────────────────────
599
+ // Singleton Manager
600
+ // ─────────────────────────────────────────────────────────────
601
+
602
+ let instance = null;
603
+
604
+ /**
605
+ * Get or create LSP client instance
606
+ * @param {string} projectRoot - Project root directory
607
+ */
608
+ async function getLSP(projectRoot = PROJECT_ROOT) {
609
+ const config = getConfig();
610
+
611
+ // Return null if LSP is disabled
612
+ if (!config.lsp?.enabled) {
613
+ return null;
614
+ }
615
+
616
+ // Reuse existing instance if same project
617
+ if (instance && instance.projectRoot === projectRoot && instance.initialized) {
618
+ return instance;
619
+ }
620
+
621
+ // Clean up old instance
622
+ if (instance) {
623
+ await instance.stop();
624
+ }
625
+
626
+ // Create new instance
627
+ try {
628
+ instance = new LSPClient(projectRoot);
629
+ await instance.start();
630
+ return instance;
631
+ } catch (err) {
632
+ warn(`LSP initialization failed: ${err.message}`);
633
+ instance = null;
634
+ return null;
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Check if LSP is available and enabled
640
+ */
641
+ function isLSPEnabled() {
642
+ const config = getConfig();
643
+ return config.lsp?.enabled === true;
644
+ }
645
+
646
+ /**
647
+ * Stop the LSP server
648
+ */
649
+ async function stopLSP() {
650
+ if (instance) {
651
+ await instance.stop();
652
+ instance = null;
653
+ }
654
+ }
655
+
656
+ // ─────────────────────────────────────────────────────────────
657
+ // High-Level Helpers
658
+ // ─────────────────────────────────────────────────────────────
659
+
660
+ /**
661
+ * Get types for multiple positions in a file
662
+ * @param {string} filePath - Path to file
663
+ * @param {Array<{line: number, character: number, name?: string}>} positions
664
+ * @returns {Object} Map of name/position to type
665
+ */
666
+ async function getTypesForPositions(filePath, positions) {
667
+ const lsp = await getLSP();
668
+ if (!lsp) return {};
669
+
670
+ const types = {};
671
+ const uri = await lsp.openDocument(filePath);
672
+
673
+ try {
674
+ for (const pos of positions) {
675
+ try {
676
+ const hover = await lsp._send('textDocument/hover', {
677
+ textDocument: { uri },
678
+ position: { line: pos.line, character: pos.character }
679
+ });
680
+
681
+ if (hover?.contents) {
682
+ const content = typeof hover.contents === 'string'
683
+ ? hover.contents
684
+ : hover.contents.value || '';
685
+
686
+ const key = pos.name || `${pos.line}:${pos.character}`;
687
+ types[key] = extractTypeFromHover(content);
688
+ }
689
+ } catch (err) {
690
+ // Skip individual errors
691
+ }
692
+ }
693
+ } finally {
694
+ lsp.closeDocument(uri);
695
+ }
696
+
697
+ return types;
698
+ }
699
+
700
+ /**
701
+ * Extract type signature from hover content
702
+ */
703
+ function extractTypeFromHover(content) {
704
+ // Try code block first
705
+ const codeMatch = content.match(/```(?:typescript|ts)\n([\s\S]*?)\n```/);
706
+ if (codeMatch) {
707
+ return codeMatch[1].trim();
708
+ }
709
+
710
+ // Try inline code
711
+ const inlineMatch = content.match(/`([^`]+)`/);
712
+ if (inlineMatch) {
713
+ return inlineMatch[1];
714
+ }
715
+
716
+ return content.trim();
717
+ }
718
+
719
+ /**
720
+ * Validate a file and get all errors
721
+ * @param {string} filePath - Path to file
722
+ * @returns {Array} Array of errors
723
+ */
724
+ async function validateFile(filePath) {
725
+ const lsp = await getLSP();
726
+ if (!lsp) return [];
727
+
728
+ return lsp.getDiagnostics(filePath);
729
+ }
730
+
731
+ /**
732
+ * Get function/method signature at cursor
733
+ * @param {string} filePath - Path to file
734
+ * @param {number} line - Line number
735
+ * @param {number} character - Character position
736
+ */
737
+ async function getSignatureAtPosition(filePath, line, character) {
738
+ const lsp = await getLSP();
739
+ if (!lsp) return null;
740
+
741
+ return lsp.getTypeAtPosition(filePath, line, character);
742
+ }
743
+
744
+ // ─────────────────────────────────────────────────────────────
745
+ // CLI Interface
746
+ // ─────────────────────────────────────────────────────────────
747
+
748
+ async function main() {
749
+ const args = process.argv.slice(2);
750
+
751
+ if (args.length === 0 || args[0] === '--help') {
752
+ console.log(`
753
+ ${colors.bold}Wogi Flow LSP Client${colors.reset}
754
+
755
+ Usage: flow-lsp.js <command> [options]
756
+
757
+ Commands:
758
+ hover <file> <line> <char> Get type info at position
759
+ diagnostics <file> Get file diagnostics
760
+ symbols <file> Get document symbols
761
+ definition <file> <l> <c> Go to definition
762
+ test Test LSP connection
763
+
764
+ Examples:
765
+ flow-lsp.js hover src/index.ts 10 5
766
+ flow-lsp.js diagnostics src/index.ts
767
+ flow-lsp.js test
768
+ `);
769
+ return;
770
+ }
771
+
772
+ const command = args[0];
773
+
774
+ try {
775
+ switch (command) {
776
+ case 'hover': {
777
+ const [, file, line, char] = args;
778
+ if (!file || line === undefined || char === undefined) {
779
+ error('Usage: hover <file> <line> <char>');
780
+ process.exit(1);
781
+ }
782
+ const lsp = await getLSP();
783
+ if (!lsp) {
784
+ error('LSP not enabled. Set lsp.enabled: true in config.json');
785
+ process.exit(1);
786
+ }
787
+ const result = await lsp.hover(file, parseInt(line), parseInt(char));
788
+ console.log(JSON.stringify(result, null, 2));
789
+ await stopLSP();
790
+ break;
791
+ }
792
+
793
+ case 'type': {
794
+ const [, file, line, char] = args;
795
+ if (!file || line === undefined || char === undefined) {
796
+ error('Usage: type <file> <line> <char>');
797
+ process.exit(1);
798
+ }
799
+ const lsp = await getLSP();
800
+ if (!lsp) {
801
+ error('LSP not enabled');
802
+ process.exit(1);
803
+ }
804
+ const type = await lsp.getTypeAtPosition(file, parseInt(line), parseInt(char));
805
+ console.log(type || '(no type info)');
806
+ await stopLSP();
807
+ break;
808
+ }
809
+
810
+ case 'diagnostics': {
811
+ const [, file] = args;
812
+ if (!file) {
813
+ error('Usage: diagnostics <file>');
814
+ process.exit(1);
815
+ }
816
+ const lsp = await getLSP();
817
+ if (!lsp) {
818
+ error('LSP not enabled');
819
+ process.exit(1);
820
+ }
821
+ const diags = await lsp.getDiagnostics(file);
822
+ console.log(JSON.stringify(diags, null, 2));
823
+ await stopLSP();
824
+ break;
825
+ }
826
+
827
+ case 'symbols': {
828
+ const [, file] = args;
829
+ if (!file) {
830
+ error('Usage: symbols <file>');
831
+ process.exit(1);
832
+ }
833
+ const lsp = await getLSP();
834
+ if (!lsp) {
835
+ error('LSP not enabled');
836
+ process.exit(1);
837
+ }
838
+ const symbols = await lsp.getDocumentSymbols(file);
839
+ console.log(JSON.stringify(symbols, null, 2));
840
+ await stopLSP();
841
+ break;
842
+ }
843
+
844
+ case 'definition': {
845
+ const [, file, line, char] = args;
846
+ if (!file || line === undefined || char === undefined) {
847
+ error('Usage: definition <file> <line> <char>');
848
+ process.exit(1);
849
+ }
850
+ const lsp = await getLSP();
851
+ if (!lsp) {
852
+ error('LSP not enabled');
853
+ process.exit(1);
854
+ }
855
+ const defs = await lsp.getDefinition(file, parseInt(line), parseInt(char));
856
+ console.log(JSON.stringify(defs, null, 2));
857
+ await stopLSP();
858
+ break;
859
+ }
860
+
861
+ case 'test': {
862
+ console.log(`${colors.cyan}Testing LSP connection...${colors.reset}\n`);
863
+
864
+ const config = getConfig();
865
+ console.log(`LSP enabled: ${config.lsp?.enabled ? 'yes' : 'no'}`);
866
+ console.log(`Server: ${config.lsp?.server || 'typescript-language-server'}`);
867
+ console.log(`Timeout: ${config.lsp?.timeout || 5000}ms\n`);
868
+
869
+ if (!config.lsp?.enabled) {
870
+ warn('LSP is disabled. Enable with: lsp.enabled: true in config.json');
871
+ return;
872
+ }
873
+
874
+ try {
875
+ const lsp = await getLSP();
876
+ if (lsp) {
877
+ success('LSP server started successfully');
878
+ console.log(`Server capabilities: ${Object.keys(lsp.serverCapabilities || {}).length} features`);
879
+ await stopLSP();
880
+ }
881
+ } catch (err) {
882
+ error(`LSP test failed: ${err.message}`);
883
+ process.exit(1);
884
+ }
885
+ break;
886
+ }
887
+
888
+ default:
889
+ error(`Unknown command: ${command}`);
890
+ process.exit(1);
891
+ }
892
+ } catch (err) {
893
+ error(`Error: ${err.message}`);
894
+ await stopLSP();
895
+ process.exit(1);
896
+ }
897
+ }
898
+
899
+ // ─────────────────────────────────────────────────────────────
900
+ // Exports
901
+ // ─────────────────────────────────────────────────────────────
902
+
903
+ module.exports = {
904
+ // Main API
905
+ getLSP,
906
+ stopLSP,
907
+ isLSPEnabled,
908
+
909
+ // High-level helpers
910
+ getTypesForPositions,
911
+ validateFile,
912
+ getSignatureAtPosition,
913
+
914
+ // Low-level access
915
+ LSPClient
916
+ };
917
+
918
+ if (require.main === module) {
919
+ main().catch(e => {
920
+ error(err.message);
921
+ process.exit(1);
922
+ });
923
+ }