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.
- package/QUICKSTART.md +24 -0
- package/README.md +85 -33
- package/package.json +23 -14
- package/scripts/postinstall.js +63 -0
- package/src/cli/commands/window.js +66 -0
- package/src/main/agents/base-agent.js +15 -7
- package/src/main/agents/builder.js +211 -0
- package/src/main/agents/index.js +12 -4
- package/src/main/agents/orchestrator.js +40 -0
- package/src/main/agents/producer.js +891 -0
- package/src/main/agents/researcher.js +78 -0
- package/src/main/agents/state-manager.js +134 -2
- package/src/main/agents/trace-writer.js +83 -0
- package/src/main/agents/verifier.js +201 -0
- package/src/main/ai-service.js +673 -66
- package/src/main/index.js +682 -110
- package/src/main/inspect-service.js +24 -1
- package/src/main/python-bridge.js +395 -0
- package/src/main/system-automation.js +934 -133
- package/src/main/ui-automation/core/ui-provider.js +99 -0
- package/src/main/ui-automation/core/uia-host.js +214 -0
- package/src/main/ui-automation/index.js +30 -0
- package/src/main/ui-automation/interactions/element-click.js +6 -6
- package/src/main/ui-automation/interactions/high-level.js +28 -6
- package/src/main/ui-automation/interactions/index.js +21 -0
- package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
- package/src/main/ui-automation/window/index.js +6 -0
- package/src/main/ui-automation/window/manager.js +173 -26
- package/src/main/ui-watcher.js +420 -56
- package/src/main/visual-awareness.js +18 -1
- package/src/native/windows-uia/Program.cs +89 -0
- package/src/native/windows-uia/build.ps1 +24 -0
- package/src/native/windows-uia-dotnet/Program.cs +920 -0
- package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
- package/src/native/windows-uia-dotnet/build.ps1 +24 -0
- package/src/renderer/chat/chat.js +943 -671
- package/src/renderer/chat/index.html +39 -4
- package/src/renderer/chat/preload.js +8 -1
- package/src/renderer/overlay/overlay.js +157 -8
- package/src/renderer/overlay/preload.js +4 -0
- package/src/shared/inspect-types.js +82 -6
- package/ARCHITECTURE.md +0 -411
- package/CONFIGURATION.md +0 -302
- package/CONTRIBUTING.md +0 -225
- package/ELECTRON_README.md +0 -121
- package/PROJECT_STATUS.md +0 -229
- 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 };
|