@vascend/talos 0.1.0 → 0.1.2
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/README.md +7 -3
- package/dist/backend/asset-events.js +9 -9
- package/dist/core/agent.js +3 -3
- package/dist/core/cli-args.js +50 -50
- package/dist/core/cli-installation.d.ts +4 -0
- package/dist/core/cli-installation.js +55 -0
- package/dist/core/compaction.js +26 -26
- package/dist/core/config.js +101 -101
- package/dist/core/execution-guidance.js +21 -21
- package/dist/core/package-update.d.ts +3 -1
- package/dist/core/package-update.js +19 -6
- package/dist/core/self-update.d.ts +1 -1
- package/dist/core/signed-release.d.ts +33 -0
- package/dist/core/signed-release.js +123 -0
- package/dist/core/update-check.d.ts +1 -1
- package/dist/core/update-check.js +15 -14
- package/dist/core/vibe-modes.js +13 -13
- package/dist/distribution/install.d.ts +1 -0
- package/dist/distribution/install.js +31 -0
- package/dist/install.mjs +271 -0
- package/dist/node-runtime.json +13 -0
- package/dist/providers/oauth-callback-server.js +32 -32
- package/dist/tools/capabilities.js +30 -30
- package/dist/tools/computer-capture.js +50 -50
- package/dist/tools/program.js +82 -82
- package/dist/tui/i18n-catalog.js +16 -0
- package/dist/tui/quick-help.js +31 -31
- package/dist/tui/repl.js +5 -5
- package/dist/tui/update-prompt.js +1 -0
- package/dist/vascend/code-events.js +9 -9
- package/dist/vascend/completeness.js +31 -31
- package/dist/vascend/context-fabric.js +16 -16
- package/dist/vascend/contract-memory.js +30 -30
- package/dist/vascend/council.js +82 -82
- package/dist/vascend/decision-promotion.js +23 -23
- package/dist/vascend/granular-kingdom.js +17 -17
- package/dist/vascend/granular-launcher.js +2 -2
- package/dist/vascend/kingdom-kpi.js +19 -19
- package/dist/vascend/kingdom-run.js +6 -6
- package/dist/vascend/kingdom.js +56 -56
- package/dist/vascend/mandate.js +21 -21
- package/dist/vascend/objective-revision.js +34 -34
- package/dist/vascend/patch-plan.js +22 -22
- package/dist/vascend/requirements.js +29 -29
- package/dist/vascend/reviewer.js +26 -26
- package/dist/vascend/room-memory.js +25 -25
- package/dist/vascend/runtime.js +44 -44
- package/dist/vascend/self-improve.js +39 -39
- package/dist/vascend/test-failure-memory.js +9 -9
- package/dist/vascend/vision.js +18 -18
- package/dist/vendor/computer-use/README.md +7 -7
- package/dist/vendor/computer-use/computer-desktop.js +211 -211
- package/dist/vendor/computer-use/computer-observe.js +315 -315
- package/dist/vendor/computer-use/computer-scripts.js +1230 -1230
- package/dist/vendor/computer-use/computer-tools.js +2136 -2136
- package/dist/vendor/computer-use/package.json +1 -1
- package/dist/vendor/computer-use/rac/atomic-input.js +91 -91
- package/dist/vendor/computer-use/rac/grammatica.js +101 -101
- package/dist/vendor/computer-use/rac/parser.js +185 -185
- package/dist/vendor/computer-use/rac/protocol.js +136 -136
- package/install.ps1 +374 -30
- package/install.sh +356 -43
- package/package.json +2 -2
- package/dist/core/kingdoms-mode-state.d.ts +0 -24
- package/dist/core/kingdoms-mode-state.js +0 -139
- package/dist/text-sanitize.js +0 -28
|
@@ -1,315 +1,315 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Layer di OSSERVAZIONE di Computer Use: funzioni pure che normalizzano,
|
|
4
|
-
// filtrano, compattano e confrontano quello che il desktop restituisce. Nessuna
|
|
5
|
-
// I/O, nessuna politica: si testa senza toccare Windows.
|
|
6
|
-
//
|
|
7
|
-
// Il criterio guida e' il costo di contesto. Il tree UI Automation di una app
|
|
8
|
-
// reale produce centinaia di nodi: consegnarli tutti, con tutti i campi vuoti,
|
|
9
|
-
// bruciava il turno prima di agire. Qui si filtra per intento e si emette solo
|
|
10
|
-
// cio' che serve a decidere il passo successivo.
|
|
11
|
-
|
|
12
|
-
const crypto = require('crypto');
|
|
13
|
-
|
|
14
|
-
const INTERACTIVE_TYPES = Object.freeze(['Edit', 'Button', 'CheckBox', 'RadioButton', 'ComboBox',
|
|
15
|
-
'ListItem', 'MenuItem', 'TabItem', 'TreeItem', 'Hyperlink', 'Slider', 'Spinner', 'SplitButton',
|
|
16
|
-
'Document', 'Menu', 'List', 'Tree', 'Tab']);
|
|
17
|
-
|
|
18
|
-
function integer(value, min, max, fallback = 0) {
|
|
19
|
-
const number = Math.trunc(Number(value));
|
|
20
|
-
return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : fallback;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function cleanBounds(value) {
|
|
24
|
-
const bounds = value && typeof value === 'object' ? value : {};
|
|
25
|
-
const x = Number(bounds.x); const y = Number(bounds.y);
|
|
26
|
-
const width = Number(bounds.width); const height = Number(bounds.height);
|
|
27
|
-
if (![x, y, width, height].every(Number.isFinite) || width < 1 || height < 1) return null;
|
|
28
|
-
return { x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function cleanWindow(value) {
|
|
32
|
-
const bounds = cleanBounds(value && value.bounds);
|
|
33
|
-
const windowId = String(value && value.windowId || '');
|
|
34
|
-
if (!bounds || !/^\d+$/.test(windowId)) return null;
|
|
35
|
-
return {
|
|
36
|
-
windowId,
|
|
37
|
-
processId: integer(value.processId, 0, 2147483647),
|
|
38
|
-
process: String(value.process || '').slice(0, 120),
|
|
39
|
-
title: String(value.title || '').slice(0, 300),
|
|
40
|
-
bounds,
|
|
41
|
-
visible: !(value && value.visible === false),
|
|
42
|
-
minimized: !!(value && value.minimized === true),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function cleanProcess(value) {
|
|
47
|
-
const processId = integer(value && value.processId, 0, 2147483647);
|
|
48
|
-
const process = String(value && value.process || '').trim().slice(0, 120);
|
|
49
|
-
if (!processId || !process) return null;
|
|
50
|
-
const windowId = /^\d+$/.test(String(value && value.windowId || ''))
|
|
51
|
-
? String(value.windowId) : '';
|
|
52
|
-
return {
|
|
53
|
-
processId,
|
|
54
|
-
process,
|
|
55
|
-
title: String(value && value.title || '').slice(0, 300),
|
|
56
|
-
windowId,
|
|
57
|
-
hasWindow: value && typeof value.hasWindow === 'boolean' ? value.hasWindow : !!windowId,
|
|
58
|
-
bounds: cleanBounds(value && value.bounds),
|
|
59
|
-
visible: !(value && value.visible === false),
|
|
60
|
-
minimized: !!(value && value.minimized === true),
|
|
61
|
-
responding: !(value && value.responding === false),
|
|
62
|
-
// Campo strettamente host-side: serve per app.getFileIcon e viene rimosso
|
|
63
|
-
// da ogni evento/risposta pubblica prima di lasciare il modulo dei tool.
|
|
64
|
-
_executablePath: String(value && (value._executablePath || value.executablePath) || '').slice(0, 1024),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function cleanElement(value, index, scope = '') {
|
|
69
|
-
const bounds = cleanBounds(value && value.bounds);
|
|
70
|
-
const row = {
|
|
71
|
-
name: String(value && value.name || '').slice(0, 180),
|
|
72
|
-
automationId: String(value && value.automationId || '').slice(0, 120),
|
|
73
|
-
type: String(value && value.type || 'Control').slice(0, 80),
|
|
74
|
-
className: String(value && value.className || '').slice(0, 100),
|
|
75
|
-
processId: integer(value && value.processId, 0, 2147483647),
|
|
76
|
-
enabled: value && value.enabled !== false,
|
|
77
|
-
focusable: value && value.focusable === true,
|
|
78
|
-
isPassword: value && value.isPassword === true,
|
|
79
|
-
value: String(value && value.value || '').slice(0, 500),
|
|
80
|
-
toggle: String(value && value.toggle || '').slice(0, 40),
|
|
81
|
-
selected: typeof (value && value.selected) === 'boolean' ? value.selected : null,
|
|
82
|
-
expanded: String(value && value.expanded || '').slice(0, 40),
|
|
83
|
-
actions: Array.isArray(value && value.actions)
|
|
84
|
-
? value.actions.map((item) => String(item || '').slice(0, 20)).filter(Boolean).slice(0, 6)
|
|
85
|
-
: [],
|
|
86
|
-
bounds,
|
|
87
|
-
};
|
|
88
|
-
if (!bounds && !row.automationId && !row.name) return null;
|
|
89
|
-
const identity = [String(scope || ''), row.processId, row.automationId,
|
|
90
|
-
row.type, row.className, row.name, index].join('|');
|
|
91
|
-
row.targetId = 'ui-' + crypto.createHash('sha256').update(identity).digest('hex').slice(0, 10);
|
|
92
|
-
return row;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Selettore consegnato al layer nativo: identifica il controllo nel tree VIVO,
|
|
96
|
-
// non una coordinata congelata.
|
|
97
|
-
function selectorFor(element) {
|
|
98
|
-
const bounds = element && element.bounds ? element.bounds : null;
|
|
99
|
-
return {
|
|
100
|
-
automationId: String(element && element.automationId || ''),
|
|
101
|
-
name: String(element && element.name || ''),
|
|
102
|
-
type: String(element && element.type || ''),
|
|
103
|
-
className: String(element && element.className || ''),
|
|
104
|
-
centerX: bounds ? Math.round(bounds.x + bounds.width / 2) : 0,
|
|
105
|
-
centerY: bounds ? Math.round(bounds.y + bounds.height / 2) : 0,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function pointerFor(element) {
|
|
110
|
-
const bounds = element && element.bounds ? cleanBounds(element.bounds) : null;
|
|
111
|
-
if (!bounds) return null;
|
|
112
|
-
return { x: Math.round(bounds.x + bounds.width / 2), y: Math.round(bounds.y + bounds.height / 2) };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function normalizeQueryText(value) {
|
|
116
|
-
return String(value || '')
|
|
117
|
-
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
118
|
-
.toLocaleLowerCase().replace(/\.exe\b/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function targetSearchScore(query, fields) {
|
|
122
|
-
const needle = normalizeQueryText(query);
|
|
123
|
-
if (!needle) return 1;
|
|
124
|
-
const tokens = needle.split(/\s+/).filter(Boolean);
|
|
125
|
-
let best = 0;
|
|
126
|
-
for (const field of fields.flatMap((value) => Array.isArray(value) ? value : [value])) {
|
|
127
|
-
const haystack = normalizeQueryText(field);
|
|
128
|
-
if (!haystack) continue;
|
|
129
|
-
if (haystack === needle) best = Math.max(best, 1000);
|
|
130
|
-
else if (haystack.startsWith(needle)) best = Math.max(best, 700);
|
|
131
|
-
else if (haystack.includes(needle)) best = Math.max(best, 500);
|
|
132
|
-
if (tokens.length && tokens.every((token) => haystack.includes(token))) {
|
|
133
|
-
best = Math.max(best, 300 + tokens.length * 20);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return best;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function searchedTargets(rows, query, limit, fields) {
|
|
140
|
-
return (Array.isArray(rows) ? rows : [])
|
|
141
|
-
.map((row, index) => ({ row, index, score: targetSearchScore(query, fields(row)) }))
|
|
142
|
-
.filter((entry) => entry.score > 0)
|
|
143
|
-
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
144
|
-
.slice(0, limit)
|
|
145
|
-
.map((entry) => entry.row);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Selezione dei controlli per intento: query testuale, tipi richiesti, solo
|
|
149
|
-
// interattivi. Restituisce anche quanti ne sono stati esclusi, perche' un
|
|
150
|
-
// troncamento silenzioso e' peggio di un elenco corto e dichiarato.
|
|
151
|
-
function filterElements(rows, options = {}) {
|
|
152
|
-
const all = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
153
|
-
const query = String(options.query || '').trim();
|
|
154
|
-
const types = (Array.isArray(options.types) ? options.types : [])
|
|
155
|
-
.map((item) => String(item || '').trim().toLocaleLowerCase()).filter(Boolean);
|
|
156
|
-
const interactiveOnly = options.interactiveOnly === true;
|
|
157
|
-
const limit = integer(options.limit, 1, 320, 60);
|
|
158
|
-
const typed = types.length
|
|
159
|
-
? all.filter((row) => types.includes(String(row.type || '').toLocaleLowerCase()))
|
|
160
|
-
: all;
|
|
161
|
-
const scoped = interactiveOnly
|
|
162
|
-
? typed.filter((row) => INTERACTIVE_TYPES.includes(row.type)
|
|
163
|
-
|| (Array.isArray(row.actions) && row.actions.length > 0))
|
|
164
|
-
: typed;
|
|
165
|
-
const matched = query
|
|
166
|
-
? searchedTargets(scoped, query, scoped.length, (row) => [row.name, row.value, row.automationId, row.type])
|
|
167
|
-
: scoped;
|
|
168
|
-
return {
|
|
169
|
-
rows: matched.slice(0, limit),
|
|
170
|
-
total: all.length,
|
|
171
|
-
matched: matched.length,
|
|
172
|
-
truncated: Math.max(0, matched.length - limit),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Vista compatta per il modello: fuori i campi vuoti, fuori gli identificativi
|
|
177
|
-
// interni (automationId, className, processId) che non servono a decidere.
|
|
178
|
-
// Il target_id resta l'unico riferimento stabile con cui agire.
|
|
179
|
-
function compactElement(row) {
|
|
180
|
-
const compact = { targetId: row.targetId, type: row.type };
|
|
181
|
-
if (row.name) compact.name = row.name;
|
|
182
|
-
if (row.value) compact.value = row.value;
|
|
183
|
-
if (row.toggle) compact.toggle = row.toggle;
|
|
184
|
-
if (row.selected === true) compact.selected = true;
|
|
185
|
-
if (row.expanded) compact.expanded = row.expanded;
|
|
186
|
-
if (row.enabled === false) compact.disabled = true;
|
|
187
|
-
if (row.isPassword) compact.password = true;
|
|
188
|
-
if (Array.isArray(row.actions) && row.actions.length) compact.can = row.actions;
|
|
189
|
-
if (row.bounds) compact.bounds = [row.bounds.x, row.bounds.y, row.bounds.width, row.bounds.height];
|
|
190
|
-
return compact;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function similarity(expected, current) {
|
|
194
|
-
if (!expected || !current) return -1;
|
|
195
|
-
if (expected.processId && current.processId && expected.processId !== current.processId) return -1;
|
|
196
|
-
if (expected.automationId && expected.automationId !== current.automationId) return -1;
|
|
197
|
-
if (!expected.automationId && expected.name && expected.name !== current.name) return -1;
|
|
198
|
-
let score = 0;
|
|
199
|
-
if (expected.automationId && expected.automationId === current.automationId) score += 8;
|
|
200
|
-
if (expected.name && expected.name === current.name) score += 5;
|
|
201
|
-
if (expected.type === current.type) score += 3;
|
|
202
|
-
if (expected.className && expected.className === current.className) score += 2;
|
|
203
|
-
if (!expected.automationId && !expected.name && expected.bounds && current.bounds) {
|
|
204
|
-
const dx = Math.abs((expected.bounds.x + expected.bounds.width / 2) - (current.bounds.x + current.bounds.width / 2));
|
|
205
|
-
const dy = Math.abs((expected.bounds.y + expected.bounds.height / 2) - (current.bounds.y + current.bounds.height / 2));
|
|
206
|
-
if (dx + dy <= 48) score += 4;
|
|
207
|
-
}
|
|
208
|
-
return score;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function elementSignature(row) {
|
|
212
|
-
return [row.automationId, row.name, row.type, row.className].join('|');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Delta fra due letture del tree: e' la verifica che il modello chiederebbe
|
|
216
|
-
// comunque dopo ogni azione, ma costa una riga invece di un intero inventario.
|
|
217
|
-
function diffElements(before, after, limit = 12) {
|
|
218
|
-
const previous = new Map((Array.isArray(before) ? before : []).map((row) => [elementSignature(row), row]));
|
|
219
|
-
const current = new Map((Array.isArray(after) ? after : []).map((row) => [elementSignature(row), row]));
|
|
220
|
-
const changes = [];
|
|
221
|
-
for (const [signature, row] of current) {
|
|
222
|
-
const old = previous.get(signature);
|
|
223
|
-
if (!old) {
|
|
224
|
-
changes.push({ change: 'appeared', type: row.type, ...(row.name ? { name: row.name } : {}),
|
|
225
|
-
...(row.value ? { value: row.value } : {}) });
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
const fields = {};
|
|
229
|
-
if (old.value !== row.value) fields.value = row.value;
|
|
230
|
-
if (old.toggle !== row.toggle) fields.toggle = row.toggle;
|
|
231
|
-
if (old.selected !== row.selected && typeof row.selected === 'boolean') fields.selected = row.selected;
|
|
232
|
-
if (old.expanded !== row.expanded) fields.expanded = row.expanded;
|
|
233
|
-
if (old.enabled !== row.enabled) fields.enabled = row.enabled;
|
|
234
|
-
if (Object.keys(fields).length) {
|
|
235
|
-
changes.push({ change: 'updated', type: row.type, ...(row.name ? { name: row.name } : {}), ...fields });
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
for (const [signature, row] of previous) {
|
|
239
|
-
if (current.has(signature)) continue;
|
|
240
|
-
changes.push({ change: 'disappeared', type: row.type, ...(row.name ? { name: row.name } : {}) });
|
|
241
|
-
}
|
|
242
|
-
const ranked = changes.sort((a, b) => {
|
|
243
|
-
const weight = (change) => change === 'updated' ? 0 : change === 'appeared' ? 1 : 2;
|
|
244
|
-
return weight(a.change) - weight(b.change);
|
|
245
|
-
});
|
|
246
|
-
return { changes: ranked.slice(0, limit), changeCount: ranked.length };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function describeChanges(delta) {
|
|
250
|
-
const count = Number(delta && delta.changeCount) || 0;
|
|
251
|
-
if (!count) return 'nessun cambiamento rilevato nella finestra';
|
|
252
|
-
const first = delta.changes[0];
|
|
253
|
-
const label = first.change === 'appeared' ? 'e comparso' : first.change === 'disappeared' ? 'e scomparso' : 'e cambiato';
|
|
254
|
-
const subject = `${first.name || first.type}`.slice(0, 60);
|
|
255
|
-
return count === 1 ? `${subject} ${label}` : `${subject} ${label}, ${count - 1} altri cambiamenti`;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Envelope unico: ogni tool dice com'e' andata (status), in una riga (summary) e
|
|
259
|
-
// che cosa ha senso fare adesso (nextActions). Un errore senza il passo
|
|
260
|
-
// successivo costringe il modello a indovinare.
|
|
261
|
-
function ok(summary, data = {}, nextActions = []) {
|
|
262
|
-
return {
|
|
263
|
-
ok: true,
|
|
264
|
-
status: 'success',
|
|
265
|
-
summary: String(summary || '').slice(0, 240),
|
|
266
|
-
...data,
|
|
267
|
-
...(nextActions.length ? { nextActions: nextActions.slice(0, 4) } : {}),
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function warn(summary, data = {}, nextActions = []) {
|
|
272
|
-
return { ...ok(summary, data, nextActions), status: 'warning' };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function fail(code, error, nextActions = [], data = {}) {
|
|
276
|
-
return {
|
|
277
|
-
ok: false,
|
|
278
|
-
status: 'error',
|
|
279
|
-
code: String(code || 'ERROR'),
|
|
280
|
-
error: String(error || 'Operazione non riuscita.').slice(0, 400),
|
|
281
|
-
...data,
|
|
282
|
-
...(nextActions.length ? { nextActions: nextActions.slice(0, 4) } : {}),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Scorciatoie del menu Start: e' l'elenco che rende possibile "apri Spotify"
|
|
287
|
-
// anche quando l'app non e' in esecuzione. Riceve nomi di file, non processi.
|
|
288
|
-
function startMenuMatches(entries, query, limit = 6) {
|
|
289
|
-
const rows = (Array.isArray(entries) ? entries : []).filter((entry) => entry && entry.name && entry.path);
|
|
290
|
-
return searchedTargets(rows, query, integer(limit, 1, 20, 6), (row) => [row.name]);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
module.exports = {
|
|
294
|
-
INTERACTIVE_TYPES,
|
|
295
|
-
integer,
|
|
296
|
-
cleanBounds,
|
|
297
|
-
cleanWindow,
|
|
298
|
-
cleanProcess,
|
|
299
|
-
cleanElement,
|
|
300
|
-
selectorFor,
|
|
301
|
-
pointerFor,
|
|
302
|
-
normalizeQueryText,
|
|
303
|
-
targetSearchScore,
|
|
304
|
-
searchedTargets,
|
|
305
|
-
filterElements,
|
|
306
|
-
compactElement,
|
|
307
|
-
similarity,
|
|
308
|
-
elementSignature,
|
|
309
|
-
diffElements,
|
|
310
|
-
describeChanges,
|
|
311
|
-
startMenuMatches,
|
|
312
|
-
ok,
|
|
313
|
-
warn,
|
|
314
|
-
fail,
|
|
315
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Layer di OSSERVAZIONE di Computer Use: funzioni pure che normalizzano,
|
|
4
|
+
// filtrano, compattano e confrontano quello che il desktop restituisce. Nessuna
|
|
5
|
+
// I/O, nessuna politica: si testa senza toccare Windows.
|
|
6
|
+
//
|
|
7
|
+
// Il criterio guida e' il costo di contesto. Il tree UI Automation di una app
|
|
8
|
+
// reale produce centinaia di nodi: consegnarli tutti, con tutti i campi vuoti,
|
|
9
|
+
// bruciava il turno prima di agire. Qui si filtra per intento e si emette solo
|
|
10
|
+
// cio' che serve a decidere il passo successivo.
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
const INTERACTIVE_TYPES = Object.freeze(['Edit', 'Button', 'CheckBox', 'RadioButton', 'ComboBox',
|
|
15
|
+
'ListItem', 'MenuItem', 'TabItem', 'TreeItem', 'Hyperlink', 'Slider', 'Spinner', 'SplitButton',
|
|
16
|
+
'Document', 'Menu', 'List', 'Tree', 'Tab']);
|
|
17
|
+
|
|
18
|
+
function integer(value, min, max, fallback = 0) {
|
|
19
|
+
const number = Math.trunc(Number(value));
|
|
20
|
+
return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanBounds(value) {
|
|
24
|
+
const bounds = value && typeof value === 'object' ? value : {};
|
|
25
|
+
const x = Number(bounds.x); const y = Number(bounds.y);
|
|
26
|
+
const width = Number(bounds.width); const height = Number(bounds.height);
|
|
27
|
+
if (![x, y, width, height].every(Number.isFinite) || width < 1 || height < 1) return null;
|
|
28
|
+
return { x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanWindow(value) {
|
|
32
|
+
const bounds = cleanBounds(value && value.bounds);
|
|
33
|
+
const windowId = String(value && value.windowId || '');
|
|
34
|
+
if (!bounds || !/^\d+$/.test(windowId)) return null;
|
|
35
|
+
return {
|
|
36
|
+
windowId,
|
|
37
|
+
processId: integer(value.processId, 0, 2147483647),
|
|
38
|
+
process: String(value.process || '').slice(0, 120),
|
|
39
|
+
title: String(value.title || '').slice(0, 300),
|
|
40
|
+
bounds,
|
|
41
|
+
visible: !(value && value.visible === false),
|
|
42
|
+
minimized: !!(value && value.minimized === true),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cleanProcess(value) {
|
|
47
|
+
const processId = integer(value && value.processId, 0, 2147483647);
|
|
48
|
+
const process = String(value && value.process || '').trim().slice(0, 120);
|
|
49
|
+
if (!processId || !process) return null;
|
|
50
|
+
const windowId = /^\d+$/.test(String(value && value.windowId || ''))
|
|
51
|
+
? String(value.windowId) : '';
|
|
52
|
+
return {
|
|
53
|
+
processId,
|
|
54
|
+
process,
|
|
55
|
+
title: String(value && value.title || '').slice(0, 300),
|
|
56
|
+
windowId,
|
|
57
|
+
hasWindow: value && typeof value.hasWindow === 'boolean' ? value.hasWindow : !!windowId,
|
|
58
|
+
bounds: cleanBounds(value && value.bounds),
|
|
59
|
+
visible: !(value && value.visible === false),
|
|
60
|
+
minimized: !!(value && value.minimized === true),
|
|
61
|
+
responding: !(value && value.responding === false),
|
|
62
|
+
// Campo strettamente host-side: serve per app.getFileIcon e viene rimosso
|
|
63
|
+
// da ogni evento/risposta pubblica prima di lasciare il modulo dei tool.
|
|
64
|
+
_executablePath: String(value && (value._executablePath || value.executablePath) || '').slice(0, 1024),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cleanElement(value, index, scope = '') {
|
|
69
|
+
const bounds = cleanBounds(value && value.bounds);
|
|
70
|
+
const row = {
|
|
71
|
+
name: String(value && value.name || '').slice(0, 180),
|
|
72
|
+
automationId: String(value && value.automationId || '').slice(0, 120),
|
|
73
|
+
type: String(value && value.type || 'Control').slice(0, 80),
|
|
74
|
+
className: String(value && value.className || '').slice(0, 100),
|
|
75
|
+
processId: integer(value && value.processId, 0, 2147483647),
|
|
76
|
+
enabled: value && value.enabled !== false,
|
|
77
|
+
focusable: value && value.focusable === true,
|
|
78
|
+
isPassword: value && value.isPassword === true,
|
|
79
|
+
value: String(value && value.value || '').slice(0, 500),
|
|
80
|
+
toggle: String(value && value.toggle || '').slice(0, 40),
|
|
81
|
+
selected: typeof (value && value.selected) === 'boolean' ? value.selected : null,
|
|
82
|
+
expanded: String(value && value.expanded || '').slice(0, 40),
|
|
83
|
+
actions: Array.isArray(value && value.actions)
|
|
84
|
+
? value.actions.map((item) => String(item || '').slice(0, 20)).filter(Boolean).slice(0, 6)
|
|
85
|
+
: [],
|
|
86
|
+
bounds,
|
|
87
|
+
};
|
|
88
|
+
if (!bounds && !row.automationId && !row.name) return null;
|
|
89
|
+
const identity = [String(scope || ''), row.processId, row.automationId,
|
|
90
|
+
row.type, row.className, row.name, index].join('|');
|
|
91
|
+
row.targetId = 'ui-' + crypto.createHash('sha256').update(identity).digest('hex').slice(0, 10);
|
|
92
|
+
return row;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Selettore consegnato al layer nativo: identifica il controllo nel tree VIVO,
|
|
96
|
+
// non una coordinata congelata.
|
|
97
|
+
function selectorFor(element) {
|
|
98
|
+
const bounds = element && element.bounds ? element.bounds : null;
|
|
99
|
+
return {
|
|
100
|
+
automationId: String(element && element.automationId || ''),
|
|
101
|
+
name: String(element && element.name || ''),
|
|
102
|
+
type: String(element && element.type || ''),
|
|
103
|
+
className: String(element && element.className || ''),
|
|
104
|
+
centerX: bounds ? Math.round(bounds.x + bounds.width / 2) : 0,
|
|
105
|
+
centerY: bounds ? Math.round(bounds.y + bounds.height / 2) : 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pointerFor(element) {
|
|
110
|
+
const bounds = element && element.bounds ? cleanBounds(element.bounds) : null;
|
|
111
|
+
if (!bounds) return null;
|
|
112
|
+
return { x: Math.round(bounds.x + bounds.width / 2), y: Math.round(bounds.y + bounds.height / 2) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeQueryText(value) {
|
|
116
|
+
return String(value || '')
|
|
117
|
+
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
118
|
+
.toLocaleLowerCase().replace(/\.exe\b/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function targetSearchScore(query, fields) {
|
|
122
|
+
const needle = normalizeQueryText(query);
|
|
123
|
+
if (!needle) return 1;
|
|
124
|
+
const tokens = needle.split(/\s+/).filter(Boolean);
|
|
125
|
+
let best = 0;
|
|
126
|
+
for (const field of fields.flatMap((value) => Array.isArray(value) ? value : [value])) {
|
|
127
|
+
const haystack = normalizeQueryText(field);
|
|
128
|
+
if (!haystack) continue;
|
|
129
|
+
if (haystack === needle) best = Math.max(best, 1000);
|
|
130
|
+
else if (haystack.startsWith(needle)) best = Math.max(best, 700);
|
|
131
|
+
else if (haystack.includes(needle)) best = Math.max(best, 500);
|
|
132
|
+
if (tokens.length && tokens.every((token) => haystack.includes(token))) {
|
|
133
|
+
best = Math.max(best, 300 + tokens.length * 20);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return best;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function searchedTargets(rows, query, limit, fields) {
|
|
140
|
+
return (Array.isArray(rows) ? rows : [])
|
|
141
|
+
.map((row, index) => ({ row, index, score: targetSearchScore(query, fields(row)) }))
|
|
142
|
+
.filter((entry) => entry.score > 0)
|
|
143
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
144
|
+
.slice(0, limit)
|
|
145
|
+
.map((entry) => entry.row);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Selezione dei controlli per intento: query testuale, tipi richiesti, solo
|
|
149
|
+
// interattivi. Restituisce anche quanti ne sono stati esclusi, perche' un
|
|
150
|
+
// troncamento silenzioso e' peggio di un elenco corto e dichiarato.
|
|
151
|
+
function filterElements(rows, options = {}) {
|
|
152
|
+
const all = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
153
|
+
const query = String(options.query || '').trim();
|
|
154
|
+
const types = (Array.isArray(options.types) ? options.types : [])
|
|
155
|
+
.map((item) => String(item || '').trim().toLocaleLowerCase()).filter(Boolean);
|
|
156
|
+
const interactiveOnly = options.interactiveOnly === true;
|
|
157
|
+
const limit = integer(options.limit, 1, 320, 60);
|
|
158
|
+
const typed = types.length
|
|
159
|
+
? all.filter((row) => types.includes(String(row.type || '').toLocaleLowerCase()))
|
|
160
|
+
: all;
|
|
161
|
+
const scoped = interactiveOnly
|
|
162
|
+
? typed.filter((row) => INTERACTIVE_TYPES.includes(row.type)
|
|
163
|
+
|| (Array.isArray(row.actions) && row.actions.length > 0))
|
|
164
|
+
: typed;
|
|
165
|
+
const matched = query
|
|
166
|
+
? searchedTargets(scoped, query, scoped.length, (row) => [row.name, row.value, row.automationId, row.type])
|
|
167
|
+
: scoped;
|
|
168
|
+
return {
|
|
169
|
+
rows: matched.slice(0, limit),
|
|
170
|
+
total: all.length,
|
|
171
|
+
matched: matched.length,
|
|
172
|
+
truncated: Math.max(0, matched.length - limit),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Vista compatta per il modello: fuori i campi vuoti, fuori gli identificativi
|
|
177
|
+
// interni (automationId, className, processId) che non servono a decidere.
|
|
178
|
+
// Il target_id resta l'unico riferimento stabile con cui agire.
|
|
179
|
+
function compactElement(row) {
|
|
180
|
+
const compact = { targetId: row.targetId, type: row.type };
|
|
181
|
+
if (row.name) compact.name = row.name;
|
|
182
|
+
if (row.value) compact.value = row.value;
|
|
183
|
+
if (row.toggle) compact.toggle = row.toggle;
|
|
184
|
+
if (row.selected === true) compact.selected = true;
|
|
185
|
+
if (row.expanded) compact.expanded = row.expanded;
|
|
186
|
+
if (row.enabled === false) compact.disabled = true;
|
|
187
|
+
if (row.isPassword) compact.password = true;
|
|
188
|
+
if (Array.isArray(row.actions) && row.actions.length) compact.can = row.actions;
|
|
189
|
+
if (row.bounds) compact.bounds = [row.bounds.x, row.bounds.y, row.bounds.width, row.bounds.height];
|
|
190
|
+
return compact;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function similarity(expected, current) {
|
|
194
|
+
if (!expected || !current) return -1;
|
|
195
|
+
if (expected.processId && current.processId && expected.processId !== current.processId) return -1;
|
|
196
|
+
if (expected.automationId && expected.automationId !== current.automationId) return -1;
|
|
197
|
+
if (!expected.automationId && expected.name && expected.name !== current.name) return -1;
|
|
198
|
+
let score = 0;
|
|
199
|
+
if (expected.automationId && expected.automationId === current.automationId) score += 8;
|
|
200
|
+
if (expected.name && expected.name === current.name) score += 5;
|
|
201
|
+
if (expected.type === current.type) score += 3;
|
|
202
|
+
if (expected.className && expected.className === current.className) score += 2;
|
|
203
|
+
if (!expected.automationId && !expected.name && expected.bounds && current.bounds) {
|
|
204
|
+
const dx = Math.abs((expected.bounds.x + expected.bounds.width / 2) - (current.bounds.x + current.bounds.width / 2));
|
|
205
|
+
const dy = Math.abs((expected.bounds.y + expected.bounds.height / 2) - (current.bounds.y + current.bounds.height / 2));
|
|
206
|
+
if (dx + dy <= 48) score += 4;
|
|
207
|
+
}
|
|
208
|
+
return score;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function elementSignature(row) {
|
|
212
|
+
return [row.automationId, row.name, row.type, row.className].join('|');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Delta fra due letture del tree: e' la verifica che il modello chiederebbe
|
|
216
|
+
// comunque dopo ogni azione, ma costa una riga invece di un intero inventario.
|
|
217
|
+
function diffElements(before, after, limit = 12) {
|
|
218
|
+
const previous = new Map((Array.isArray(before) ? before : []).map((row) => [elementSignature(row), row]));
|
|
219
|
+
const current = new Map((Array.isArray(after) ? after : []).map((row) => [elementSignature(row), row]));
|
|
220
|
+
const changes = [];
|
|
221
|
+
for (const [signature, row] of current) {
|
|
222
|
+
const old = previous.get(signature);
|
|
223
|
+
if (!old) {
|
|
224
|
+
changes.push({ change: 'appeared', type: row.type, ...(row.name ? { name: row.name } : {}),
|
|
225
|
+
...(row.value ? { value: row.value } : {}) });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const fields = {};
|
|
229
|
+
if (old.value !== row.value) fields.value = row.value;
|
|
230
|
+
if (old.toggle !== row.toggle) fields.toggle = row.toggle;
|
|
231
|
+
if (old.selected !== row.selected && typeof row.selected === 'boolean') fields.selected = row.selected;
|
|
232
|
+
if (old.expanded !== row.expanded) fields.expanded = row.expanded;
|
|
233
|
+
if (old.enabled !== row.enabled) fields.enabled = row.enabled;
|
|
234
|
+
if (Object.keys(fields).length) {
|
|
235
|
+
changes.push({ change: 'updated', type: row.type, ...(row.name ? { name: row.name } : {}), ...fields });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const [signature, row] of previous) {
|
|
239
|
+
if (current.has(signature)) continue;
|
|
240
|
+
changes.push({ change: 'disappeared', type: row.type, ...(row.name ? { name: row.name } : {}) });
|
|
241
|
+
}
|
|
242
|
+
const ranked = changes.sort((a, b) => {
|
|
243
|
+
const weight = (change) => change === 'updated' ? 0 : change === 'appeared' ? 1 : 2;
|
|
244
|
+
return weight(a.change) - weight(b.change);
|
|
245
|
+
});
|
|
246
|
+
return { changes: ranked.slice(0, limit), changeCount: ranked.length };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function describeChanges(delta) {
|
|
250
|
+
const count = Number(delta && delta.changeCount) || 0;
|
|
251
|
+
if (!count) return 'nessun cambiamento rilevato nella finestra';
|
|
252
|
+
const first = delta.changes[0];
|
|
253
|
+
const label = first.change === 'appeared' ? 'e comparso' : first.change === 'disappeared' ? 'e scomparso' : 'e cambiato';
|
|
254
|
+
const subject = `${first.name || first.type}`.slice(0, 60);
|
|
255
|
+
return count === 1 ? `${subject} ${label}` : `${subject} ${label}, ${count - 1} altri cambiamenti`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Envelope unico: ogni tool dice com'e' andata (status), in una riga (summary) e
|
|
259
|
+
// che cosa ha senso fare adesso (nextActions). Un errore senza il passo
|
|
260
|
+
// successivo costringe il modello a indovinare.
|
|
261
|
+
function ok(summary, data = {}, nextActions = []) {
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
status: 'success',
|
|
265
|
+
summary: String(summary || '').slice(0, 240),
|
|
266
|
+
...data,
|
|
267
|
+
...(nextActions.length ? { nextActions: nextActions.slice(0, 4) } : {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function warn(summary, data = {}, nextActions = []) {
|
|
272
|
+
return { ...ok(summary, data, nextActions), status: 'warning' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function fail(code, error, nextActions = [], data = {}) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
status: 'error',
|
|
279
|
+
code: String(code || 'ERROR'),
|
|
280
|
+
error: String(error || 'Operazione non riuscita.').slice(0, 400),
|
|
281
|
+
...data,
|
|
282
|
+
...(nextActions.length ? { nextActions: nextActions.slice(0, 4) } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Scorciatoie del menu Start: e' l'elenco che rende possibile "apri Spotify"
|
|
287
|
+
// anche quando l'app non e' in esecuzione. Riceve nomi di file, non processi.
|
|
288
|
+
function startMenuMatches(entries, query, limit = 6) {
|
|
289
|
+
const rows = (Array.isArray(entries) ? entries : []).filter((entry) => entry && entry.name && entry.path);
|
|
290
|
+
return searchedTargets(rows, query, integer(limit, 1, 20, 6), (row) => [row.name]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
INTERACTIVE_TYPES,
|
|
295
|
+
integer,
|
|
296
|
+
cleanBounds,
|
|
297
|
+
cleanWindow,
|
|
298
|
+
cleanProcess,
|
|
299
|
+
cleanElement,
|
|
300
|
+
selectorFor,
|
|
301
|
+
pointerFor,
|
|
302
|
+
normalizeQueryText,
|
|
303
|
+
targetSearchScore,
|
|
304
|
+
searchedTargets,
|
|
305
|
+
filterElements,
|
|
306
|
+
compactElement,
|
|
307
|
+
similarity,
|
|
308
|
+
elementSignature,
|
|
309
|
+
diffElements,
|
|
310
|
+
describeChanges,
|
|
311
|
+
startMenuMatches,
|
|
312
|
+
ok,
|
|
313
|
+
warn,
|
|
314
|
+
fail,
|
|
315
|
+
};
|