copilot-liku-cli 0.0.4 → 0.0.9

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 (47) hide show
  1. package/QUICKSTART.md +24 -0
  2. package/README.md +85 -33
  3. package/package.json +23 -14
  4. package/scripts/postinstall.js +63 -0
  5. package/src/cli/commands/window.js +66 -0
  6. package/src/main/agents/base-agent.js +15 -7
  7. package/src/main/agents/builder.js +211 -0
  8. package/src/main/agents/index.js +12 -4
  9. package/src/main/agents/orchestrator.js +40 -0
  10. package/src/main/agents/producer.js +891 -0
  11. package/src/main/agents/researcher.js +78 -0
  12. package/src/main/agents/state-manager.js +134 -2
  13. package/src/main/agents/trace-writer.js +83 -0
  14. package/src/main/agents/verifier.js +201 -0
  15. package/src/main/ai-service.js +673 -66
  16. package/src/main/index.js +682 -110
  17. package/src/main/inspect-service.js +24 -1
  18. package/src/main/python-bridge.js +395 -0
  19. package/src/main/system-automation.js +934 -133
  20. package/src/main/ui-automation/core/ui-provider.js +99 -0
  21. package/src/main/ui-automation/core/uia-host.js +214 -0
  22. package/src/main/ui-automation/index.js +30 -0
  23. package/src/main/ui-automation/interactions/element-click.js +6 -6
  24. package/src/main/ui-automation/interactions/high-level.js +28 -6
  25. package/src/main/ui-automation/interactions/index.js +21 -0
  26. package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
  27. package/src/main/ui-automation/window/index.js +6 -0
  28. package/src/main/ui-automation/window/manager.js +173 -26
  29. package/src/main/ui-watcher.js +420 -56
  30. package/src/main/visual-awareness.js +18 -1
  31. package/src/native/windows-uia/Program.cs +89 -0
  32. package/src/native/windows-uia/build.ps1 +24 -0
  33. package/src/native/windows-uia-dotnet/Program.cs +920 -0
  34. package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
  35. package/src/native/windows-uia-dotnet/build.ps1 +24 -0
  36. package/src/renderer/chat/chat.js +943 -671
  37. package/src/renderer/chat/index.html +39 -4
  38. package/src/renderer/chat/preload.js +8 -1
  39. package/src/renderer/overlay/overlay.js +157 -8
  40. package/src/renderer/overlay/preload.js +4 -0
  41. package/src/shared/inspect-types.js +82 -6
  42. package/ARCHITECTURE.md +0 -411
  43. package/CONFIGURATION.md +0 -302
  44. package/CONTRIBUTING.md +0 -225
  45. package/ELECTRON_README.md +0 -121
  46. package/PROJECT_STATUS.md +0 -229
  47. package/TESTING.md +0 -274
@@ -304,13 +304,36 @@ async function detectRegions(options = {}) {
304
304
  label: e.Name || e.ClassName || '',
305
305
  role: e.ControlType?.replace('ControlType.', '') || 'element',
306
306
  bounds: e.Bounds,
307
- confidence: e.IsEnabled ? 0.9 : 0.6
307
+ confidence: e.IsEnabled ? 0.9 : 0.6,
308
+ clickPoint: e.ClickablePoint || e.clickPoint || null,
309
+ runtimeId: e.runtimeId || null
308
310
  })),
309
311
  'accessibility'
310
312
  );
311
313
  results.sources.push('accessibility');
312
314
  }
313
315
 
316
+ // OCR-based region detection (when screenshot is available)
317
+ if (options.screenshot) {
318
+ try {
319
+ const ocrResult = await visualAwareness.extractTextFromImage(options.screenshot);
320
+ if (ocrResult && ocrResult.text && !ocrResult.error) {
321
+ // OCR returns text but not individual bounding boxes from Windows OCR
322
+ // Store as a single text-content region covering the screenshot area
323
+ updateRegions([{
324
+ label: 'OCR text content',
325
+ role: 'text',
326
+ bounds: { x: 0, y: 0, width: options.screenshot.width || 0, height: options.screenshot.height || 0 },
327
+ text: ocrResult.text,
328
+ confidence: 0.7
329
+ }], 'ocr');
330
+ results.sources.push('ocr');
331
+ }
332
+ } catch (ocrError) {
333
+ console.warn('[INSPECT] OCR detection skipped:', ocrError.message);
334
+ }
335
+ }
336
+
314
337
  // Update window context
315
338
  await updateWindowContext();
316
339
 
@@ -0,0 +1,395 @@
1
+ /**
2
+ * PythonBridge — JSON-RPC 2.0 client for the MUSE Python server.
3
+ *
4
+ * Spawns `python -m multimodal_gen.server --jsonrpc --verbose` as a child
5
+ * process and communicates via HTTP POST (JSON-RPC 2.0) on localhost.
6
+ *
7
+ * Uses only Node built-in modules (http, child_process, events) — NO npm deps.
8
+ *
9
+ * Singleton access:
10
+ * const bridge = PythonBridge.getShared();
11
+ * await bridge.start();
12
+ * const result = await bridge.call('ping', {});
13
+ */
14
+
15
+ const EventEmitter = require('events');
16
+ const http = require('http');
17
+ const { spawn } = require('child_process');
18
+ const path = require('path');
19
+ const fs = require('fs');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Singleton instance
23
+ // ---------------------------------------------------------------------------
24
+ let _sharedInstance = null;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // PythonBridge
28
+ // ---------------------------------------------------------------------------
29
+
30
+ class PythonBridge extends EventEmitter {
31
+ /**
32
+ * @param {object} options
33
+ * @param {string} [options.pythonPath='python'] Python executable.
34
+ * @param {string} [options.serverHost='127.0.0.1']
35
+ * @param {number} [options.serverPort=8765]
36
+ * @param {string} [options.cwd] Working directory for the child process.
37
+ */
38
+ constructor(options = {}) {
39
+ super();
40
+
41
+ this.pythonPath = options.pythonPath || 'python';
42
+ this.serverHost = options.serverHost || process.env.MUSE_GATEWAY_HOST || '127.0.0.1';
43
+ this.serverPort = options.serverPort || Number(process.env.MUSE_GATEWAY_PORT || 8765);
44
+ this.cwd = options.cwd || path.resolve(__dirname, '..', '..', '..', 'MUSE');
45
+
46
+ /** @type {import('child_process').ChildProcess | null} */
47
+ this._child = null;
48
+
49
+ /** Auto-incrementing JSON-RPC request id */
50
+ this._nextId = 1;
51
+
52
+ /** True while the server child process is running */
53
+ this._running = false;
54
+
55
+ /** True once start() has completed successfully */
56
+ this._ready = false;
57
+
58
+ /** True when we're connected to an externally-managed gateway (e.g. JUCE) */
59
+ this._externalGateway = false;
60
+
61
+ /** Last child-process spawn error (if any) */
62
+ this._lastSpawnError = null;
63
+ }
64
+
65
+ _emitBridgeError(err) {
66
+ if (this.listenerCount('error') > 0) {
67
+ this.emit('error', err);
68
+ } else {
69
+ console.error('[PythonBridge] Unhandled bridge error:', err?.message || err);
70
+ }
71
+ }
72
+
73
+ // ------------------------------------------------------------------
74
+ // Singleton
75
+ // ------------------------------------------------------------------
76
+
77
+ /**
78
+ * Return (or create) a shared singleton PythonBridge instance.
79
+ * All agents should use this to avoid spawning multiple servers.
80
+ *
81
+ * @param {object} [options] Passed to the constructor only on first call.
82
+ * @returns {PythonBridge}
83
+ */
84
+ static getShared(options = {}) {
85
+ if (!_sharedInstance) {
86
+ _sharedInstance = new PythonBridge(options);
87
+ }
88
+ return _sharedInstance;
89
+ }
90
+
91
+ /**
92
+ * Reset the shared instance (for testing or full shutdown).
93
+ */
94
+ static resetShared() {
95
+ if (_sharedInstance) {
96
+ _sharedInstance.stop().catch(() => {});
97
+ _sharedInstance = null;
98
+ }
99
+ }
100
+
101
+ // ------------------------------------------------------------------
102
+ // Lifecycle
103
+ // ------------------------------------------------------------------
104
+
105
+ /**
106
+ * Spawn the Python JSON-RPC server and wait until it responds to `ping`.
107
+ *
108
+ * Polls up to 10 times (500 ms apart) before giving up.
109
+ *
110
+ * @returns {Promise<void>}
111
+ */
112
+ async start() {
113
+ if (this._running && this._ready) {
114
+ return; // Already started
115
+ }
116
+
117
+ // Prefer attaching to an already-running gateway (JUCE auto-start) to avoid port contention.
118
+ // If ping succeeds, we don't spawn a child and we also won't send shutdown on stop().
119
+ try {
120
+ const res = await this._rawCall('ping', {}, 1500);
121
+ if (res && res.status === 'ok') {
122
+ this._ready = true;
123
+ this._running = false;
124
+ this._externalGateway = true;
125
+ this.emit('started', { port: this.serverPort, attempt: 0, external: true });
126
+ return;
127
+ }
128
+ } catch (_err) {
129
+ // No gateway reachable; fall through to spawning.
130
+ }
131
+
132
+ if (!fs.existsSync(this.cwd)) {
133
+ throw new Error(`PythonBridge cwd does not exist: ${this.cwd}`);
134
+ }
135
+
136
+ // Spawn the child process
137
+ const args = ['-m', 'multimodal_gen.server', '--gateway', '--verbose'];
138
+
139
+ this._child = spawn(this.pythonPath, args, {
140
+ cwd: this.cwd,
141
+ stdio: ['ignore', 'pipe', 'pipe'],
142
+ windowsHide: true,
143
+ });
144
+
145
+ this._running = true;
146
+ this._externalGateway = false;
147
+
148
+ // Forward stdout / stderr as events (useful for debugging)
149
+ this._child.stdout.on('data', (data) => {
150
+ const text = data.toString().trim();
151
+ if (text) {
152
+ this.emit('stdout', text);
153
+ }
154
+ });
155
+
156
+ this._child.stderr.on('data', (data) => {
157
+ const text = data.toString().trim();
158
+ if (text) {
159
+ this.emit('stderr', text);
160
+ }
161
+ });
162
+
163
+ this._child.on('error', (err) => {
164
+ this._running = false;
165
+ this._ready = false;
166
+ this._lastSpawnError = err;
167
+ this._emitBridgeError(err);
168
+ });
169
+
170
+ this._child.on('exit', (code, signal) => {
171
+ this._running = false;
172
+ this._ready = false;
173
+ this.emit('stopped', { code, signal });
174
+ });
175
+
176
+ // Wait for server readiness (ping check)
177
+ const maxAttempts = 10;
178
+ const intervalMs = 500;
179
+
180
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
181
+ await _sleep(intervalMs);
182
+
183
+ if (this._lastSpawnError) {
184
+ const spawnErr = this._lastSpawnError;
185
+ this._lastSpawnError = null;
186
+ await this.stop();
187
+ throw new Error(`PythonBridge spawn failed (${this.pythonPath}) in ${this.cwd}: ${spawnErr.message}`);
188
+ }
189
+
190
+ try {
191
+ const res = await this.call('ping', {});
192
+ if (res && res.status === 'ok') {
193
+ this._ready = true;
194
+ this.emit('started', { port: this.serverPort, attempt });
195
+ return;
196
+ }
197
+ } catch (_err) {
198
+ // Server not ready yet — retry
199
+ }
200
+ }
201
+
202
+ // Could not reach server — clean up
203
+ await this.stop();
204
+ throw new Error(
205
+ `PythonBridge: server did not respond to ping after ${maxAttempts} attempts`
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Gracefully stop the server.
211
+ *
212
+ * Sends 'shutdown' RPC first (best-effort), then kills the child.
213
+ *
214
+ * @returns {Promise<void>}
215
+ */
216
+ async stop() {
217
+ if (!this._running && !this._child) {
218
+ return;
219
+ }
220
+
221
+ // Only request shutdown if we own the process.
222
+ if (!this._externalGateway) {
223
+ try {
224
+ await this._rawCall('shutdown', {}, 2000);
225
+ } catch (_err) {
226
+ // Ignore — we'll kill the process anyway
227
+ }
228
+ }
229
+
230
+ // Kill child process
231
+ if (this._child) {
232
+ try {
233
+ this._child.kill('SIGTERM');
234
+ } catch (_err) {
235
+ // Already dead
236
+ }
237
+ this._child = null;
238
+ }
239
+
240
+ this._running = false;
241
+ this._ready = false;
242
+ this._externalGateway = false;
243
+ this.emit('stopped', { reason: 'explicit' });
244
+ }
245
+
246
+ // ------------------------------------------------------------------
247
+ // RPC
248
+ // ------------------------------------------------------------------
249
+
250
+ /**
251
+ * Send a JSON-RPC 2.0 call with automatic retry on connection errors.
252
+ *
253
+ * @param {string} method RPC method name.
254
+ * @param {object} params Named parameters.
255
+ * @param {number} [timeoutMs=30000] Per-attempt timeout.
256
+ * @returns {Promise<any>} The `result` field from the response.
257
+ */
258
+ async call(method, params = {}, timeoutMs = 30000) {
259
+ const maxRetries = 2;
260
+ const retryDelayMs = 500;
261
+ let lastError = null;
262
+
263
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
264
+ try {
265
+ return await this._rawCall(method, params, timeoutMs);
266
+ } catch (err) {
267
+ lastError = err;
268
+
269
+ const isConnectionError =
270
+ err.code === 'ECONNREFUSED' ||
271
+ err.code === 'ECONNRESET' ||
272
+ err.code === 'EPIPE' ||
273
+ err.message.includes('socket hang up');
274
+
275
+ if (isConnectionError && attempt < maxRetries) {
276
+ await _sleep(retryDelayMs);
277
+ continue;
278
+ }
279
+
280
+ throw err;
281
+ }
282
+ }
283
+
284
+ throw lastError;
285
+ }
286
+
287
+ /**
288
+ * Check whether the server is alive (ping succeeds).
289
+ *
290
+ * @returns {Promise<boolean>}
291
+ */
292
+ async isAlive() {
293
+ try {
294
+ const res = await this._rawCall('ping', {}, 3000);
295
+ return res && res.status === 'ok';
296
+ } catch (_err) {
297
+ return false;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Synchronous-style getter: is the child process still running?
303
+ *
304
+ * @returns {boolean}
305
+ */
306
+ get isRunning() {
307
+ return this._running;
308
+ }
309
+
310
+ // ------------------------------------------------------------------
311
+ // Internal
312
+ // ------------------------------------------------------------------
313
+
314
+ /**
315
+ * Low-level JSON-RPC call over HTTP POST.
316
+ *
317
+ * @param {string} method
318
+ * @param {object} params
319
+ * @param {number} timeoutMs
320
+ * @returns {Promise<any>}
321
+ * @private
322
+ */
323
+ _rawCall(method, params, timeoutMs = 30000) {
324
+ const id = this._nextId++;
325
+ const body = JSON.stringify({
326
+ jsonrpc: '2.0',
327
+ method,
328
+ params,
329
+ id,
330
+ });
331
+
332
+ return new Promise((resolve, reject) => {
333
+ const req = http.request(
334
+ {
335
+ hostname: this.serverHost,
336
+ port: this.serverPort,
337
+ path: '/',
338
+ method: 'POST',
339
+ headers: {
340
+ 'Content-Type': 'application/json; charset=utf-8',
341
+ 'Content-Length': Buffer.byteLength(body),
342
+ },
343
+ timeout: timeoutMs,
344
+ },
345
+ (res) => {
346
+ const chunks = [];
347
+ res.on('data', (chunk) => chunks.push(chunk));
348
+ res.on('end', () => {
349
+ try {
350
+ const raw = Buffer.concat(chunks).toString('utf-8');
351
+ const json = JSON.parse(raw);
352
+
353
+ if (json.error) {
354
+ const rpcErr = new Error(
355
+ `JSON-RPC error ${json.error.code}: ${json.error.message}`
356
+ );
357
+ rpcErr.code = json.error.code;
358
+ rpcErr.data = json.error.data;
359
+ reject(rpcErr);
360
+ return;
361
+ }
362
+
363
+ resolve(json.result);
364
+ } catch (parseErr) {
365
+ reject(new Error(`Failed to parse JSON-RPC response: ${parseErr.message}`));
366
+ }
367
+ });
368
+ }
369
+ );
370
+
371
+ req.on('error', (err) => reject(err));
372
+ req.on('timeout', () => {
373
+ req.destroy();
374
+ reject(new Error(`JSON-RPC call '${method}' timed out after ${timeoutMs}ms`));
375
+ });
376
+
377
+ req.write(body);
378
+ req.end();
379
+ });
380
+ }
381
+ }
382
+
383
+ // ---------------------------------------------------------------------------
384
+ // Helpers
385
+ // ---------------------------------------------------------------------------
386
+
387
+ function _sleep(ms) {
388
+ return new Promise((resolve) => setTimeout(resolve, ms));
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // Exports
393
+ // ---------------------------------------------------------------------------
394
+
395
+ module.exports = { PythonBridge };