copilot-liku-cli 0.0.8 → 0.0.10
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 +25 -0
- package/package.json +1 -1
- package/src/cli/commands/doctor.js +509 -0
- package/src/cli/liku.js +2 -1
- package/src/main/agents/index.js +6 -1
- package/src/main/agents/orchestrator.js +27 -0
- package/src/main/agents/trace-writer.js +83 -0
- package/src/main/ai-service.js +366 -45
- package/src/main/index.js +2 -0
- package/src/main/system-automation.js +85 -2
- package/src/main/ui-watcher.js +22 -1
- package/src/renderer/chat/chat.js +30 -2
- package/src/renderer/chat/index.html +37 -0
package/QUICKSTART.md
CHANGED
|
@@ -60,6 +60,22 @@ npm run test:ui
|
|
|
60
60
|
This order gives clearer pass/fail signals by validating runtime health first,
|
|
61
61
|
then shortcut routing, then module-level UI automation.
|
|
62
62
|
|
|
63
|
+
### Targeting sanity check
|
|
64
|
+
|
|
65
|
+
Before running keyboard-driven automation (especially browser tab operations), verify what Liku considers the active window:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
liku doctor
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This prints the resolved package root/version (to confirm local vs global) and the current active window (title/process).
|
|
72
|
+
|
|
73
|
+
For deterministic, machine-readable output (recommended for smaller models / automation), use:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
liku doctor --json
|
|
77
|
+
```
|
|
78
|
+
|
|
63
79
|
`smoke:shortcuts` intentionally validates chat visibility via direct in-app
|
|
64
80
|
toggle and validates keyboard routing on overlay with target gating.
|
|
65
81
|
|
|
@@ -138,6 +154,15 @@ Right-click the tray icon to see:
|
|
|
138
154
|
|
|
139
155
|
## Common Tasks
|
|
140
156
|
|
|
157
|
+
### Browser actions (Edge/Chrome)
|
|
158
|
+
|
|
159
|
+
When automating browsers, be explicit about **targeting**:
|
|
160
|
+
1. Ensure the correct browser window is active (bring it to front / focus it)
|
|
161
|
+
2. Ensure the correct tab is active (click the tab title, or use \`ctrl+1..9\`)
|
|
162
|
+
3. Then perform the action (e.g., close tab with \`ctrl+w\`)
|
|
163
|
+
|
|
164
|
+
If you skip steps 1–2 and the overlay/chat has focus, keyboard shortcuts may close the overlay instead of affecting the browser.
|
|
165
|
+
|
|
141
166
|
### Selecting a Screen Element
|
|
142
167
|
```
|
|
143
168
|
1. Press Ctrl+Alt+Space to open chat
|
package/package.json
CHANGED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor command - Minimal diagnostics for targeting reliability
|
|
3
|
+
* @module cli/commands/doctor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { success, error, info, highlight, dim } = require('../util/output');
|
|
8
|
+
|
|
9
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../../..');
|
|
10
|
+
const UI_MODULE = path.resolve(__dirname, '../../main/ui-automation');
|
|
11
|
+
|
|
12
|
+
const DOCTOR_SCHEMA_VERSION = 'doctor.v1';
|
|
13
|
+
|
|
14
|
+
function safeJsonStringify(value) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value, null, 2);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function withConsoleSilenced(enabled, fn) {
|
|
23
|
+
if (!enabled) {
|
|
24
|
+
return fn();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const original = {
|
|
28
|
+
log: console.log,
|
|
29
|
+
info: console.info,
|
|
30
|
+
warn: console.warn,
|
|
31
|
+
error: console.error,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
console.log = () => {};
|
|
35
|
+
console.info = () => {};
|
|
36
|
+
console.warn = () => {};
|
|
37
|
+
console.error = () => {};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
return await fn();
|
|
41
|
+
} finally {
|
|
42
|
+
console.log = original.log;
|
|
43
|
+
console.info = original.info;
|
|
44
|
+
console.warn = original.warn;
|
|
45
|
+
console.error = original.error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeText(text) {
|
|
50
|
+
return String(text || '').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeForMatch(text) {
|
|
54
|
+
return normalizeText(text).toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeForLooseMatch(text) {
|
|
58
|
+
return normalizeForMatch(text)
|
|
59
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
60
|
+
.replace(/\s+/g, ' ')
|
|
61
|
+
.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function includesCI(haystack, needle) {
|
|
65
|
+
if (!haystack || !needle) return false;
|
|
66
|
+
// Loose match to tolerate punctuation differences (e.g., "Microsoft? Edge Beta")
|
|
67
|
+
return normalizeForLooseMatch(haystack).includes(normalizeForLooseMatch(needle));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractQuotedStrings(text) {
|
|
71
|
+
const out = [];
|
|
72
|
+
const str = normalizeText(text);
|
|
73
|
+
const re = /"([^"]+)"|'([^']+)'/g;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = re.exec(str)) !== null) {
|
|
76
|
+
const val = m[1] || m[2];
|
|
77
|
+
if (val) out.push(val);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseRequestHints(requestText) {
|
|
83
|
+
const text = normalizeText(requestText);
|
|
84
|
+
const lower = normalizeForMatch(text);
|
|
85
|
+
|
|
86
|
+
// Extract common patterns
|
|
87
|
+
const tabTitleMatch = /\btab\s+(?:titled|named|called)\s+(?:"([^"]+)"|'([^']+)'|([^,.;\n\r]+))/i.exec(text);
|
|
88
|
+
const tabTitle = tabTitleMatch ? normalizeText(tabTitleMatch[1] || tabTitleMatch[2] || tabTitleMatch[3]) : null;
|
|
89
|
+
|
|
90
|
+
const inWindowMatch = /\b(?:in|within)\s+([^\n\r]+?)\s+window\b/i.exec(text);
|
|
91
|
+
const windowHint = inWindowMatch ? normalizeText(inWindowMatch[1]) : null;
|
|
92
|
+
|
|
93
|
+
// Heuristic: infer app family
|
|
94
|
+
const appHints = {
|
|
95
|
+
isBrowser: /\b(edge|chrome|browser|msedge)\b/i.test(text),
|
|
96
|
+
isEditor: /\b(vs\s*code|visual\s*studio\s*code|code\s*-\s*insiders|editor)\b/i.test(text),
|
|
97
|
+
isTerminal: /\b(terminal|powershell|cmd\.exe|command\s+prompt|windows\s+terminal)\b/i.test(text),
|
|
98
|
+
isExplorer: /\b(file\s+explorer|explorer\.exe)\b/i.test(text),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Infer intent
|
|
102
|
+
const intent = (() => {
|
|
103
|
+
if (/\bclose\b/.test(lower) && /\btab\b/.test(lower)) return 'close_tab';
|
|
104
|
+
if (/\bclose\b/.test(lower) && /\bwindow\b/.test(lower)) return 'close_window';
|
|
105
|
+
if (/\bclick\b/.test(lower)) return 'click';
|
|
106
|
+
if (/\btype\b/.test(lower) || /\benter\b/.test(lower)) return 'type';
|
|
107
|
+
if (/\bscroll\b/.test(lower)) return 'scroll';
|
|
108
|
+
if (/\bdrag\b/.test(lower)) return 'drag';
|
|
109
|
+
if (/\bfind\b/.test(lower) || /\blocate\b/.test(lower)) return 'find';
|
|
110
|
+
if (/\bfocus\b/.test(lower) || /\bactivate\b/.test(lower) || /\bbring\b/.test(lower)) return 'focus';
|
|
111
|
+
return 'unknown';
|
|
112
|
+
})();
|
|
113
|
+
|
|
114
|
+
const quoted = extractQuotedStrings(text);
|
|
115
|
+
|
|
116
|
+
// Potential element text is often quoted, but avoid using the tab title as element text.
|
|
117
|
+
const elementTextCandidates = quoted.filter(q => q && q !== tabTitle);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
raw: text,
|
|
121
|
+
intent,
|
|
122
|
+
windowHint,
|
|
123
|
+
tabTitle,
|
|
124
|
+
appHints,
|
|
125
|
+
elementTextCandidates,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scoreWindowCandidate(win, hints) {
|
|
130
|
+
let score = 0;
|
|
131
|
+
const reasons = [];
|
|
132
|
+
|
|
133
|
+
const title = win?.title || '';
|
|
134
|
+
const proc = win?.processName || '';
|
|
135
|
+
|
|
136
|
+
if (hints.windowHint && includesCI(title, hints.windowHint)) {
|
|
137
|
+
score += 60;
|
|
138
|
+
reasons.push('title matches windowHint');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (hints.appHints?.isBrowser && (includesCI(proc, 'msedge') || includesCI(title, 'edge') || includesCI(proc, 'chrome') || includesCI(title, 'chrome'))) {
|
|
142
|
+
score += 35;
|
|
143
|
+
reasons.push('looks like browser');
|
|
144
|
+
}
|
|
145
|
+
if (hints.appHints?.isEditor && (includesCI(title, 'visual studio code') || includesCI(title, 'code - insiders') || includesCI(proc, 'Code') || includesCI(proc, 'Code - Insiders'))) {
|
|
146
|
+
score += 35;
|
|
147
|
+
reasons.push('looks like editor');
|
|
148
|
+
}
|
|
149
|
+
if (hints.appHints?.isTerminal && (includesCI(title, 'terminal') || includesCI(proc, 'WindowsTerminal') || includesCI(proc, 'pwsh') || includesCI(proc, 'cmd'))) {
|
|
150
|
+
score += 30;
|
|
151
|
+
reasons.push('looks like terminal');
|
|
152
|
+
}
|
|
153
|
+
if (hints.appHints?.isExplorer && (includesCI(proc, 'explorer') || includesCI(title, 'file explorer'))) {
|
|
154
|
+
score += 30;
|
|
155
|
+
reasons.push('looks like explorer');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Prefer non-empty titled windows
|
|
159
|
+
if (normalizeText(title).length > 0) {
|
|
160
|
+
score += 3;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { score, reasons };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildSuggestedPlan(hints, activeWindow, rankedCandidates) {
|
|
167
|
+
const top = rankedCandidates?.[0]?.window || null;
|
|
168
|
+
const target = top || activeWindow || null;
|
|
169
|
+
const plan = [];
|
|
170
|
+
|
|
171
|
+
const targetTitleForFilter = target?.title ? String(target.title) : null;
|
|
172
|
+
|
|
173
|
+
const targetSelector = (() => {
|
|
174
|
+
if (!target) return null;
|
|
175
|
+
if (typeof target.hwnd === 'number' && Number.isFinite(target.hwnd)) {
|
|
176
|
+
return { by: 'hwnd', value: target.hwnd };
|
|
177
|
+
}
|
|
178
|
+
if (target.title) {
|
|
179
|
+
return { by: 'title', value: target.title };
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
})();
|
|
183
|
+
|
|
184
|
+
// State machine-ish scaffold. Keep it deterministic and CLI-driven.
|
|
185
|
+
plan.push({
|
|
186
|
+
state: 'VERIFY_ACTIVE_WINDOW',
|
|
187
|
+
goal: 'Confirm which window will receive input',
|
|
188
|
+
command: 'liku window --active',
|
|
189
|
+
verification: 'Active window title/process match the intended target',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (targetSelector && hints.intent !== 'unknown') {
|
|
193
|
+
const frontCmd = targetSelector.by === 'hwnd'
|
|
194
|
+
? `liku window --front --hwnd ${targetSelector.value}`
|
|
195
|
+
: `liku window --front "${String(targetSelector.value).replace(/"/g, '\\"')}"`;
|
|
196
|
+
|
|
197
|
+
plan.unshift({
|
|
198
|
+
state: 'FOCUS_TARGET_WINDOW',
|
|
199
|
+
goal: 'Bring the intended target window to the foreground',
|
|
200
|
+
command: frontCmd,
|
|
201
|
+
verification: 'Window is foreground and becomes active',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Tab targeting for browsers is always a separate step.
|
|
206
|
+
if (hints.intent === 'close_tab' && hints.tabTitle) {
|
|
207
|
+
const windowFilter = targetTitleForFilter ? ` --window "${targetTitleForFilter.replace(/"/g, '\\"')}"` : '';
|
|
208
|
+
plan.push({
|
|
209
|
+
state: 'ACTIVATE_TARGET_TAB',
|
|
210
|
+
goal: `Make the tab active: "${hints.tabTitle}"`,
|
|
211
|
+
command: `liku click "${String(hints.tabTitle).replace(/"/g, '\\"')}" --type TabItem${windowFilter}`,
|
|
212
|
+
verification: 'The tab becomes active (visually highlighted)',
|
|
213
|
+
notes: 'If UIA cannot see browser tabs, fall back to ctrl+1..9 or ctrl+tab cycling with waits.',
|
|
214
|
+
});
|
|
215
|
+
plan.push({
|
|
216
|
+
state: 'EXECUTE_ACTION',
|
|
217
|
+
goal: 'Close the active tab',
|
|
218
|
+
command: 'liku keys ctrl+w',
|
|
219
|
+
verification: 'Tab disappears; previous tab becomes active',
|
|
220
|
+
});
|
|
221
|
+
return { target, plan };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (hints.intent === 'close_window') {
|
|
225
|
+
plan.push({
|
|
226
|
+
state: 'EXECUTE_ACTION',
|
|
227
|
+
goal: 'Close the active window',
|
|
228
|
+
command: 'liku keys alt+f4',
|
|
229
|
+
verification: 'Window closes and focus changes',
|
|
230
|
+
notes: 'Prefer alt+f4 for closing windows; ctrl+shift+w is app-specific and can close the wrong thing.',
|
|
231
|
+
});
|
|
232
|
+
return { target, plan };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (hints.intent === 'click') {
|
|
236
|
+
const elementText = hints.elementTextCandidates?.[0] || null;
|
|
237
|
+
if (elementText) {
|
|
238
|
+
const windowFilter = targetTitleForFilter ? ` --window "${targetTitleForFilter.replace(/"/g, '\\"')}"` : '';
|
|
239
|
+
plan.push({
|
|
240
|
+
state: 'EXECUTE_ACTION',
|
|
241
|
+
goal: `Click element: "${elementText}"`,
|
|
242
|
+
command: `liku click "${String(elementText).replace(/"/g, '\\"')}"${windowFilter}`,
|
|
243
|
+
verification: 'Expected UI response occurs (button press, navigation, etc.)',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return { target, plan };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Generic fallback: ensure focus + suggest next step.
|
|
250
|
+
plan.push({
|
|
251
|
+
state: 'NEXT',
|
|
252
|
+
goal: 'If the target is not correct, refine the window hint and retry',
|
|
253
|
+
command: 'liku window # list windows',
|
|
254
|
+
verification: 'You can identify the intended window title/process',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return { target, plan };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function mermaidForPlan(plan) {
|
|
261
|
+
if (!Array.isArray(plan) || plan.length === 0) return null;
|
|
262
|
+
const ids = plan.map(p => p.state);
|
|
263
|
+
const edges = [];
|
|
264
|
+
for (let i = 0; i < ids.length - 1; i++) {
|
|
265
|
+
edges.push(`${ids[i]} --> ${ids[i + 1]}`);
|
|
266
|
+
}
|
|
267
|
+
return `stateDiagram-v2\n ${edges.join('\n ')}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildChecks({ uiaError, activeWindow, windows, requestText, requestHints, requestAnalysis }) {
|
|
271
|
+
const checks = [];
|
|
272
|
+
const push = (id, status, message, details = null) => {
|
|
273
|
+
checks.push({ id, status, message, details });
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
push(
|
|
277
|
+
'uia.available',
|
|
278
|
+
uiaError ? 'fail' : 'pass',
|
|
279
|
+
uiaError ? 'UI Automation unavailable or errored' : 'UI Automation available',
|
|
280
|
+
uiaError ? { error: uiaError } : null
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
push(
|
|
284
|
+
'ui.activeWindow.present',
|
|
285
|
+
activeWindow ? 'pass' : 'warn',
|
|
286
|
+
activeWindow ? 'Active window detected' : 'Active window missing',
|
|
287
|
+
activeWindow ? { title: activeWindow.title, processName: activeWindow.processName, hwnd: activeWindow.hwnd } : null
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
push(
|
|
291
|
+
'ui.windows.enumerated',
|
|
292
|
+
Array.isArray(windows) && windows.length > 0 ? 'pass' : 'warn',
|
|
293
|
+
Array.isArray(windows) && windows.length > 0 ? `Enumerated ${windows.length} windows` : 'No windows enumerated',
|
|
294
|
+
Array.isArray(windows) ? { count: windows.length } : { count: 0 }
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (requestText) {
|
|
298
|
+
push(
|
|
299
|
+
'request.parsed',
|
|
300
|
+
requestHints ? 'pass' : 'fail',
|
|
301
|
+
requestHints ? 'Request parsed into hints' : 'Request parsing failed',
|
|
302
|
+
requestHints || null
|
|
303
|
+
);
|
|
304
|
+
push(
|
|
305
|
+
'request.plan.generated',
|
|
306
|
+
requestAnalysis?.plan?.length ? 'pass' : 'warn',
|
|
307
|
+
requestAnalysis?.plan?.length ? `Generated ${requestAnalysis.plan.length} plan steps` : 'No plan steps generated',
|
|
308
|
+
requestAnalysis?.plan?.length ? { steps: requestAnalysis.plan.map(s => s.state) } : null
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return checks;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function summarizeChecks(checks) {
|
|
316
|
+
const summary = { pass: 0, warn: 0, fail: 0 };
|
|
317
|
+
for (const c of checks) {
|
|
318
|
+
if (c.status === 'pass') summary.pass += 1;
|
|
319
|
+
else if (c.status === 'warn') summary.warn += 1;
|
|
320
|
+
else if (c.status === 'fail') summary.fail += 1;
|
|
321
|
+
}
|
|
322
|
+
return summary;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function run(args, options) {
|
|
326
|
+
// Load package metadata from the resolved project root (this is the key signal
|
|
327
|
+
// for "am I running the local install or some other copy?")
|
|
328
|
+
let pkg;
|
|
329
|
+
try {
|
|
330
|
+
pkg = require(path.join(PROJECT_ROOT, 'package.json'));
|
|
331
|
+
} catch (e) {
|
|
332
|
+
if (!options.quiet) {
|
|
333
|
+
error(`Failed to load package.json from ${PROJECT_ROOT}: ${e.message}`);
|
|
334
|
+
}
|
|
335
|
+
return { success: false, error: 'Could not load package metadata', projectRoot: PROJECT_ROOT };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const generatedAt = new Date().toISOString();
|
|
339
|
+
|
|
340
|
+
const envInfo = {
|
|
341
|
+
name: pkg.name,
|
|
342
|
+
version: pkg.version,
|
|
343
|
+
projectRoot: PROJECT_ROOT,
|
|
344
|
+
cwd: process.cwd(),
|
|
345
|
+
node: process.version,
|
|
346
|
+
platform: process.platform,
|
|
347
|
+
arch: process.arch,
|
|
348
|
+
execPath: process.execPath,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const requestText = args.length > 0 ? args.join(' ') : null;
|
|
352
|
+
const requestHints = requestText ? parseRequestHints(requestText) : null;
|
|
353
|
+
|
|
354
|
+
// UIA / active window + other state
|
|
355
|
+
let activeWindow = null;
|
|
356
|
+
let windows = [];
|
|
357
|
+
let mouse = null;
|
|
358
|
+
let uiaError = null;
|
|
359
|
+
await withConsoleSilenced(Boolean(options.json), async () => {
|
|
360
|
+
try {
|
|
361
|
+
// Lazy load so doctor still works even if UIA deps are missing
|
|
362
|
+
// (we'll just report that in output)
|
|
363
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
364
|
+
const ui = require(UI_MODULE);
|
|
365
|
+
activeWindow = await ui.getActiveWindow();
|
|
366
|
+
mouse = await ui.getMousePosition();
|
|
367
|
+
|
|
368
|
+
// Keep window lists bounded by default.
|
|
369
|
+
const maxWindows = options.all ? Number.MAX_SAFE_INTEGER : (options.windows ? parseInt(options.windows, 10) : 15);
|
|
370
|
+
const allWindows = await ui.findWindows({});
|
|
371
|
+
windows = Array.isArray(allWindows) ? allWindows.slice(0, maxWindows) : [];
|
|
372
|
+
|
|
373
|
+
if (!activeWindow) {
|
|
374
|
+
uiaError = 'No active window detected';
|
|
375
|
+
}
|
|
376
|
+
} catch (e) {
|
|
377
|
+
uiaError = e.message;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Candidate targeting analysis (optional)
|
|
382
|
+
let requestAnalysis = null;
|
|
383
|
+
if (requestHints) {
|
|
384
|
+
const candidates = (Array.isArray(windows) ? windows : []).map(w => {
|
|
385
|
+
const { score, reasons } = scoreWindowCandidate(w, requestHints);
|
|
386
|
+
return { score, reasons, window: w };
|
|
387
|
+
}).sort((a, b) => b.score - a.score);
|
|
388
|
+
|
|
389
|
+
const { target, plan } = buildSuggestedPlan(requestHints, activeWindow, candidates);
|
|
390
|
+
requestAnalysis = {
|
|
391
|
+
request: requestHints,
|
|
392
|
+
target,
|
|
393
|
+
candidates: candidates.slice(0, 8).map(c => ({ score: c.score, reasons: c.reasons, window: c.window })),
|
|
394
|
+
plan,
|
|
395
|
+
mermaid: options.flow ? mermaidForPlan(plan) : null,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const checks = buildChecks({ uiaError, activeWindow, windows, requestText, requestHints, requestAnalysis });
|
|
400
|
+
const checksSummary = summarizeChecks(checks);
|
|
401
|
+
const ok = checksSummary.fail === 0;
|
|
402
|
+
|
|
403
|
+
const report = {
|
|
404
|
+
schemaVersion: DOCTOR_SCHEMA_VERSION,
|
|
405
|
+
generatedAt,
|
|
406
|
+
ok,
|
|
407
|
+
checks,
|
|
408
|
+
checksSummary,
|
|
409
|
+
env: envInfo,
|
|
410
|
+
request: requestText ? { text: requestText, hints: requestHints } : null,
|
|
411
|
+
uiState: {
|
|
412
|
+
activeWindow,
|
|
413
|
+
windows,
|
|
414
|
+
mouse,
|
|
415
|
+
uiaError: uiaError || null,
|
|
416
|
+
},
|
|
417
|
+
targeting: requestAnalysis ? {
|
|
418
|
+
selectedWindow: requestAnalysis.target || null,
|
|
419
|
+
candidates: requestAnalysis.candidates || [],
|
|
420
|
+
} : null,
|
|
421
|
+
plan: requestAnalysis ? {
|
|
422
|
+
steps: requestAnalysis.plan || [],
|
|
423
|
+
mermaid: requestAnalysis.mermaid || null,
|
|
424
|
+
} : null,
|
|
425
|
+
next: {
|
|
426
|
+
commands: (
|
|
427
|
+
requestAnalysis?.plan?.length
|
|
428
|
+
? requestAnalysis.plan.map(s => s.command).filter(Boolean)
|
|
429
|
+
: ['liku window --active', 'liku window']
|
|
430
|
+
),
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (options.json) {
|
|
435
|
+
// Caller wants machine-readable output
|
|
436
|
+
return report;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!options.quiet) {
|
|
440
|
+
console.log(`\n${highlight('Liku Diagnostics (doctor)')}\n`);
|
|
441
|
+
|
|
442
|
+
console.log(`${highlight('Package:')} ${envInfo.name} v${envInfo.version}`);
|
|
443
|
+
console.log(`${highlight('Resolved root:')} ${envInfo.projectRoot}`);
|
|
444
|
+
console.log(`${highlight('Node:')} ${envInfo.node} (${envInfo.platform}/${envInfo.arch})`);
|
|
445
|
+
console.log(`${highlight('CWD:')} ${envInfo.cwd}`);
|
|
446
|
+
|
|
447
|
+
console.log(`${highlight('Schema:')} ${DOCTOR_SCHEMA_VERSION}`);
|
|
448
|
+
console.log(`${highlight('OK:')} ${ok ? 'true' : 'false'} ${dim(`(pass=${checksSummary.pass} warn=${checksSummary.warn} fail=${checksSummary.fail})`)}`);
|
|
449
|
+
|
|
450
|
+
console.log(`\n${highlight('Active window:')}`);
|
|
451
|
+
if (activeWindow) {
|
|
452
|
+
const bounds = activeWindow.bounds || { x: '?', y: '?', width: '?', height: '?' };
|
|
453
|
+
console.log(` Title: ${activeWindow.title || dim('(unknown)')}`);
|
|
454
|
+
console.log(` Process: ${activeWindow.processName || dim('(unknown)')}`);
|
|
455
|
+
console.log(` Class: ${activeWindow.className || dim('(unknown)')}`);
|
|
456
|
+
console.log(` Handle: ${activeWindow.hwnd ?? dim('(unknown)')}`);
|
|
457
|
+
console.log(` Bounds: ${bounds.x},${bounds.y} ${bounds.width}x${bounds.height}`);
|
|
458
|
+
} else {
|
|
459
|
+
error(`Could not read active window (${uiaError || 'unknown error'})`);
|
|
460
|
+
info('Tip: try running `liku window --active` to confirm UI Automation is working.');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (mouse) {
|
|
464
|
+
console.log(`\n${highlight('Mouse:')} ${mouse.x},${mouse.y}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (Array.isArray(windows) && windows.length > 0) {
|
|
468
|
+
console.log(`\n${highlight(`Top windows (${windows.length}${options.all ? '' : ' shown'}):`)}`);
|
|
469
|
+
windows.slice(0, 10).forEach((w, idx) => {
|
|
470
|
+
const title = w.title || '(untitled)';
|
|
471
|
+
const proc = w.processName || '-';
|
|
472
|
+
const hwnd = w.hwnd ?? '?';
|
|
473
|
+
console.log(` ${idx + 1}. [${hwnd}] ${title} ${dim('—')} ${proc}`);
|
|
474
|
+
});
|
|
475
|
+
if (windows.length > 10) {
|
|
476
|
+
console.log(dim(' (Use --windows <n> or --all with --json for more)'));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Helpful next-step hints for browser operations
|
|
481
|
+
console.log(`\n${highlight('Targeting tips:')}`);
|
|
482
|
+
console.log(` - Before sending keys, ensure the intended app is active.`);
|
|
483
|
+
console.log(` - For browsers: activate the correct tab first, then use ${highlight('ctrl+w')} to close the active tab.`);
|
|
484
|
+
|
|
485
|
+
if (requestAnalysis?.plan?.length) {
|
|
486
|
+
console.log(`\n${highlight('Suggested plan:')}`);
|
|
487
|
+
requestAnalysis.plan.forEach((step, i) => {
|
|
488
|
+
console.log(` ${i + 1}. ${highlight(step.state)}: ${step.command}`);
|
|
489
|
+
});
|
|
490
|
+
if (options.flow && requestAnalysis.mermaid) {
|
|
491
|
+
console.log(`\n${highlight('Flow (Mermaid):')}\n${requestAnalysis.mermaid}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// For debugging copy/paste
|
|
496
|
+
if (options.debug) {
|
|
497
|
+
const json = safeJsonStringify(report);
|
|
498
|
+
if (json) {
|
|
499
|
+
console.log(`\n${highlight('Raw JSON:')}\n${json}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (ok) success('Doctor check OK');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return report;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
module.exports = { run };
|
package/src/cli/liku.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* liku - Copilot-Liku CLI
|
|
4
4
|
*
|
|
@@ -36,6 +36,7 @@ const pkg = require(path.join(PROJECT_ROOT, 'package.json'));
|
|
|
36
36
|
// Command registry
|
|
37
37
|
const COMMANDS = {
|
|
38
38
|
start: { desc: 'Start the Electron agent with overlay', file: 'start' },
|
|
39
|
+
doctor: { desc: 'Diagnostics: version, environment, active window', file: 'doctor' },
|
|
39
40
|
click: { desc: 'Click element by text or coordinates', file: 'click', args: '<text|x,y>' },
|
|
40
41
|
find: { desc: 'Find UI elements matching criteria', file: 'find', args: '<text>' },
|
|
41
42
|
type: { desc: 'Type text at current cursor position', file: 'type', args: '<text>' },
|
package/src/main/agents/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const { VerifierAgent } = require('./verifier');
|
|
|
18
18
|
const { ProducerAgent } = require('./producer');
|
|
19
19
|
const { ResearcherAgent } = require('./researcher');
|
|
20
20
|
const { AgentStateManager } = require('./state-manager');
|
|
21
|
+
const { TraceWriter } = require('./trace-writer');
|
|
21
22
|
|
|
22
23
|
module.exports = {
|
|
23
24
|
AgentOrchestrator,
|
|
@@ -27,6 +28,7 @@ module.exports = {
|
|
|
27
28
|
ProducerAgent,
|
|
28
29
|
ResearcherAgent,
|
|
29
30
|
AgentStateManager,
|
|
31
|
+
TraceWriter,
|
|
30
32
|
|
|
31
33
|
// Factory function for creating configured orchestrator
|
|
32
34
|
createAgentSystem: (aiService, options = {}) => {
|
|
@@ -47,8 +49,11 @@ module.exports = {
|
|
|
47
49
|
modelMetadata
|
|
48
50
|
});
|
|
49
51
|
|
|
52
|
+
// Attach persistent flight recorder
|
|
53
|
+
const traceWriter = new TraceWriter(orchestrator);
|
|
54
|
+
|
|
50
55
|
// Return object with both orchestrator and stateManager
|
|
51
|
-
return { orchestrator, stateManager };
|
|
56
|
+
return { orchestrator, stateManager, traceWriter };
|
|
52
57
|
},
|
|
53
58
|
|
|
54
59
|
// Recovery function for checkpoint restoration
|
|
@@ -181,6 +181,33 @@ class AgentOrchestrator extends EventEmitter {
|
|
|
181
181
|
|
|
182
182
|
// ===== Handoff Management =====
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Execute multiple agents in parallel (e.g., Builder + Researcher)
|
|
186
|
+
* Returns array of results in the same order as the roles array.
|
|
187
|
+
*/
|
|
188
|
+
async executeParallel(roles, context, message) {
|
|
189
|
+
const agents = roles.map(role => {
|
|
190
|
+
const agent = this.agents.get(role);
|
|
191
|
+
if (!agent) throw new Error(`Agent not found for parallel execution: ${role}`);
|
|
192
|
+
return { role, agent };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.emit('parallel:start', { roles, message });
|
|
196
|
+
|
|
197
|
+
const task = { description: message, context };
|
|
198
|
+
const results = await Promise.all(
|
|
199
|
+
agents.map(({ role, agent }) => {
|
|
200
|
+
this.stateManager.updateAgentActivity(agent.id);
|
|
201
|
+
return agent.process(task, context).catch(err => ({
|
|
202
|
+
success: false, error: err.message, role
|
|
203
|
+
}));
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
this.emit('parallel:complete', { roles, results: results.map((r, i) => ({ role: roles[i], success: r.success })) });
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
184
211
|
async executeHandoff(fromAgent, targetRole, context, message) {
|
|
185
212
|
const targetAgent = this.agents.get(targetRole);
|
|
186
213
|
|