@mrtrinhvn/ag-kit 1.1.9 → 1.3.1
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/package.json +2 -2
- package/template/.agent/.version +1 -0
- package/template/.agent/agents/orchestrator.md +0 -2
- package/template/.agent/knowledge/ag-kit-elite.md +22 -0
- package/template/.agent/knowledge/model-switching-vfs.md +16 -16
- package/template/.agent/scripts/port_utils.sh +69 -30
- package/template/.agent/scripts/receptionist_down.sh +24 -16
- package/template/.agent/scripts/receptionist_up.sh +124 -21
- package/template/.agent/scripts/repomap.py +126 -0
- package/template/.agent/skills/ag-kit-core/SKILL.md +68 -0
- package/template/.agent/skills/codeact-executor/SKILL.md +41 -0
- package/template/.agent/skills/intelligent-routing/SKILL.md +38 -0
- package/template/.agent/skills/knowledge-management/SKILL.md +1 -1
- package/template/.agent/skills/lazy-gravity/SKILL.md +14 -11
- package/template/.agent/skills/llm-routing-quirks/SKILL.md +17 -6
- package/template/.agent/skills/remoat-integration/SKILL.md +37 -13
- package/template/.agent/skills/semantic-search/SKILL.md +24 -0
- package/template/.agent/skills/telegram-agentic-gateway/SKILL.md +77 -38
- package/template/.agent/skills/telegram-agentic-gateway/templates/CdpService.ts.template +514 -0
- package/template/.agent/skills/telegram-agentic-gateway/templates/ResponseMonitor.ts.template +102 -0
- package/template/.agent/skills/telegram-agentic-gateway/templates/start.sh.template +12 -0
- package/template/GEMINI.md +27 -18
- package/template/scripts/ag_deck/index.html +58 -0
- package/template/scripts/ag_heartbeat.js +46 -0
- package/template/scripts/ag_hud.js +87 -155
- package/template/scripts/ag_phantom.sh +50 -0
- package/template/scripts/ag_portal_bridge.js +139 -114
- package/template/start.sh +0 -0
- package/template/stop_bot.sh +0 -0
- package/template/.agent/.shared/ui-ux-pro-max/data/charts.csv +0 -26
- package/template/.agent/.shared/ui-ux-pro-max/data/colors.csv +0 -97
- package/template/.agent/.shared/ui-ux-pro-max/data/icons.csv +0 -101
- package/template/.agent/.shared/ui-ux-pro-max/data/landing.csv +0 -31
- package/template/.agent/.shared/ui-ux-pro-max/data/products.csv +0 -97
- package/template/.agent/.shared/ui-ux-pro-max/data/prompts.csv +0 -24
- package/template/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +0 -45
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +0 -54
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
- package/template/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +0 -50
- package/template/.agent/.shared/ui-ux-pro-max/data/styles.csv +0 -59
- package/template/.agent/.shared/ui-ux-pro-max/data/typography.csv +0 -58
- package/template/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +0 -101
- package/template/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
- package/template/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +0 -31
- package/template/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
- package/template/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
- package/template/.agent/.shared/ui-ux-pro-max/scripts/core.py +0 -258
- package/template/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +0 -1067
- package/template/.agent/.shared/ui-ux-pro-max/scripts/search.py +0 -106
- package/template/.agent/ARCHITECTURE.md +0 -288
- package/template/.agent/knowledge/orchestrator_v3_protocol.md +0 -60
- package/template/.agent/knowledge/self_healing_logs.md +0 -22
- package/template/.agent/knowledge/tele-agentic-standard.md +0 -30
- package/template/.agent/mcp_config.json +0 -24
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import * as http from 'http';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
import { config } from '../config/env.js';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
|
|
8
|
+
const SELECTORS = {
|
|
9
|
+
CHAT_INPUT: 'div[role="textbox"][aria-label="Chat input"], div[role="textbox"]:not(.xterm-helper-textarea)',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class CdpService extends EventEmitter {
|
|
13
|
+
private ws: WebSocket | null = null;
|
|
14
|
+
private connected = false;
|
|
15
|
+
private contexts: { id: number; name: string; url: string }[] = [];
|
|
16
|
+
private pendingCalls = new Map<number, { resolve: Function; reject: Function; timer: NodeJS.Timeout }>();
|
|
17
|
+
private idCounter = 1;
|
|
18
|
+
private callTimeout = 30000;
|
|
19
|
+
|
|
20
|
+
// ─── Discovery ──────────────────────────────────────────────────────
|
|
21
|
+
private async getJson(url: string): Promise<any[]> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const req = http.get(url, (res) => {
|
|
24
|
+
let data = '';
|
|
25
|
+
res.on('data', c => data += c);
|
|
26
|
+
res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } });
|
|
27
|
+
});
|
|
28
|
+
req.on('error', reject);
|
|
29
|
+
req.setTimeout(3000, () => { req.destroy(); reject(new Error('Timeout')); });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async discoverTarget(): Promise<string> {
|
|
34
|
+
const port = config.idePort;
|
|
35
|
+
|
|
36
|
+
// We trust the port defined in .env (updated by port_utils.sh)
|
|
37
|
+
try {
|
|
38
|
+
const pages = await this.getJson(`http://127.0.0.1:${port}/json/list`);
|
|
39
|
+
// Filter by project name if specified
|
|
40
|
+
const projectMatch = (p: any) => {
|
|
41
|
+
if (!config.projectName) return true;
|
|
42
|
+
const title = (p.title || '').toLowerCase();
|
|
43
|
+
const url = (p.url || '').toLowerCase();
|
|
44
|
+
const lowName = config.projectName.toLowerCase();
|
|
45
|
+
return title.includes(lowName) || url.includes(lowName);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const sortedPages = pages.sort((a, b) => {
|
|
49
|
+
const aUrl = (a.url || '').toLowerCase();
|
|
50
|
+
const bUrl = (b.url || '').toLowerCase();
|
|
51
|
+
const isAWorkbench = aUrl.includes('workbench.html');
|
|
52
|
+
const isBWorkbench = bUrl.includes('workbench.html');
|
|
53
|
+
if (isAWorkbench && !isBWorkbench) return -1;
|
|
54
|
+
if (!isAWorkbench && isBWorkbench) return 1;
|
|
55
|
+
return 0;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const target = sortedPages.find((p: any) =>
|
|
59
|
+
p.webSocketDebuggerUrl &&
|
|
60
|
+
(p.url?.includes('workbench') || p.title?.includes('Antigravity') || p.title?.includes('Code')) &&
|
|
61
|
+
projectMatch(p)
|
|
62
|
+
);
|
|
63
|
+
if (target) {
|
|
64
|
+
console.log(`[CDP] 🔍 Found IDE for "${config.projectName}" on port ${port}: "${target.title}"`);
|
|
65
|
+
return target.webSocketDebuggerUrl;
|
|
66
|
+
}
|
|
67
|
+
} catch { /* connection failed */ }
|
|
68
|
+
throw new Error(`No IDE found for project "${config.projectName}" on port ${port}. Please run ./start.sh to sync.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Connection ─────────────────────────────────────────────────────
|
|
72
|
+
async connect(): Promise<void> {
|
|
73
|
+
const wsUrl = await this.discoverTarget();
|
|
74
|
+
this.ws = new WebSocket(wsUrl);
|
|
75
|
+
|
|
76
|
+
await new Promise<void>((resolve, reject) => {
|
|
77
|
+
this.ws!.on('open', () => { this.connected = true; resolve(); });
|
|
78
|
+
this.ws!.on('error', reject);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.ws.on('message', (msg: WebSocket.Data) => {
|
|
82
|
+
try {
|
|
83
|
+
const data = JSON.parse(msg.toString());
|
|
84
|
+
// Handle pending call responses
|
|
85
|
+
if (data.id !== undefined && this.pendingCalls.has(data.id)) {
|
|
86
|
+
const { resolve, reject, timer } = this.pendingCalls.get(data.id)!;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
this.pendingCalls.delete(data.id);
|
|
89
|
+
if (data.error) reject(new Error(data.error.message)); else resolve(data.result);
|
|
90
|
+
}
|
|
91
|
+
// Track execution contexts
|
|
92
|
+
if (data.method === 'Runtime.executionContextCreated') {
|
|
93
|
+
this.contexts.push(data.params.context);
|
|
94
|
+
}
|
|
95
|
+
if (data.method === 'Runtime.executionContextDestroyed') {
|
|
96
|
+
this.contexts = this.contexts.filter(c => c.id !== data.params.executionContextId);
|
|
97
|
+
}
|
|
98
|
+
} catch { /* ignore parse error */ }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.ws.on('close', () => {
|
|
102
|
+
this.connected = false;
|
|
103
|
+
this.ws = null;
|
|
104
|
+
console.log('[CDP] ⚡ Disconnected from IDE');
|
|
105
|
+
this.emit('disconnected');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Get execution contexts
|
|
109
|
+
await this.call('Runtime.enable', {});
|
|
110
|
+
console.log('[CDP] ✅ Connected to IDE');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async disconnect(): Promise<void> {
|
|
114
|
+
if (this.ws) { this.ws.close(); this.ws = null; }
|
|
115
|
+
this.connected = false;
|
|
116
|
+
this.contexts = [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getContexts(): { id: number; name: string; url: string }[] { return this.contexts; }
|
|
120
|
+
|
|
121
|
+
isConnected(): boolean { return this.connected; }
|
|
122
|
+
|
|
123
|
+
// ─── CDP Call ────────────────────────────────────────────────────────
|
|
124
|
+
async call(method: string, params: any = {}): Promise<any> {
|
|
125
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
126
|
+
throw new Error('CDP not connected');
|
|
127
|
+
}
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const id = this.idCounter++;
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
this.pendingCalls.delete(id);
|
|
132
|
+
reject(new Error(`Timeout: ${method}`));
|
|
133
|
+
}, this.callTimeout);
|
|
134
|
+
this.pendingCalls.set(id, { resolve, reject, timer });
|
|
135
|
+
this.ws!.send(JSON.stringify({ id, method, params }));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Evaluate a script on any available execution context (e.g. child webviews)
|
|
141
|
+
*/
|
|
142
|
+
async evaluateOnAnyContext(expression: string): Promise<any> {
|
|
143
|
+
for (const ctx of this.contexts) {
|
|
144
|
+
try {
|
|
145
|
+
const res = await this.call('Runtime.evaluate', {
|
|
146
|
+
expression, returnByValue: true, contextId: ctx.id,
|
|
147
|
+
});
|
|
148
|
+
if (res?.result?.value !== undefined && res.result.value !== null) {
|
|
149
|
+
return res.result.value;
|
|
150
|
+
}
|
|
151
|
+
} catch { /* next context */ }
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Prompt Injection ───────────────────────────────────────────────
|
|
157
|
+
async injectPrompt(text: string): Promise<boolean> {
|
|
158
|
+
// 1. Focus chat input
|
|
159
|
+
const focused = await this.focusChatInput();
|
|
160
|
+
if (!focused) throw new Error('Cannot find chat input in IDE');
|
|
161
|
+
|
|
162
|
+
// 2. Clear existing text (Ctrl+A → Backspace)
|
|
163
|
+
await this.clearInput();
|
|
164
|
+
|
|
165
|
+
// 3. Type the prompt
|
|
166
|
+
await this.call('Input.insertText', { text });
|
|
167
|
+
await this.sleep(100);
|
|
168
|
+
|
|
169
|
+
// 4. Press Enter to submit
|
|
170
|
+
await this.pressEnter();
|
|
171
|
+
console.log(`[CDP] 📨 Injected prompt (${text.length} chars)`);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async focusChatInput(): Promise<boolean> {
|
|
176
|
+
const script = `(() => {
|
|
177
|
+
const editors = Array.from(document.querySelectorAll('${SELECTORS.CHAT_INPUT}'));
|
|
178
|
+
const visible = editors.filter(el => el.offsetParent !== null);
|
|
179
|
+
const editor = visible[visible.length - 1];
|
|
180
|
+
if (!editor) return false;
|
|
181
|
+
editor.focus();
|
|
182
|
+
return true;
|
|
183
|
+
})()`;
|
|
184
|
+
|
|
185
|
+
for (const ctx of this.contexts) {
|
|
186
|
+
try {
|
|
187
|
+
const res = await this.call('Runtime.evaluate', {
|
|
188
|
+
expression: script, returnByValue: true, contextId: ctx.id,
|
|
189
|
+
});
|
|
190
|
+
if (res?.result?.value === true) return true;
|
|
191
|
+
} catch { /* next context */ }
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async clearInput(): Promise<void> {
|
|
197
|
+
const mod = process.platform === 'darwin' ? 4 : 2; // Meta / Ctrl
|
|
198
|
+
await this.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'a', code: 'KeyA', modifiers: mod, windowsVirtualKeyCode: 65 });
|
|
199
|
+
await this.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'a', code: 'KeyA', modifiers: mod, windowsVirtualKeyCode: 65 });
|
|
200
|
+
await this.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 });
|
|
201
|
+
await this.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 });
|
|
202
|
+
await this.sleep(50);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async pressEnter(): Promise<void> {
|
|
206
|
+
await this.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 });
|
|
207
|
+
await this.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Response Extraction ────────────────────────────────────────────
|
|
211
|
+
async extractLastResponse(): Promise<string> {
|
|
212
|
+
// 1. NEURAL ECHO FAST-TRACK (Highest priority if Agent actively dictates response)
|
|
213
|
+
try {
|
|
214
|
+
if (fs.existsSync('.last_response.json')) {
|
|
215
|
+
const lastResp = JSON.parse(fs.readFileSync('.last_response.json', 'utf8'));
|
|
216
|
+
if (Date.now() - new Date(lastResp.timestamp).getTime() < 120000) {
|
|
217
|
+
return lastResp.text;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (e) { }
|
|
221
|
+
|
|
222
|
+
// 2. DOM EXTRACTION FALLBACK (Precise evaluation across contexts)
|
|
223
|
+
const script = `(() => {
|
|
224
|
+
function queryAllShadows(selector, root = document) {
|
|
225
|
+
let results = [];
|
|
226
|
+
if (root.querySelectorAll) results.push(...root.querySelectorAll(selector));
|
|
227
|
+
let walker = document.createTreeWalker(root, 1, null, false);
|
|
228
|
+
while (walker.nextNode()) {
|
|
229
|
+
if (walker.currentNode.shadowRoot) {
|
|
230
|
+
results.push(...queryAllShadows(selector, walker.currentNode.shadowRoot));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return results;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Find all assistant responses (often rendered in markdown-body or generic message containers)
|
|
237
|
+
const containers = queryAllShadows('.markdown-body, [class*="message-container"], [class*="chat-message"], .select-text');
|
|
238
|
+
|
|
239
|
+
const assistantMsgs = containers.filter(el => {
|
|
240
|
+
if (!el || !el.classList) return false;
|
|
241
|
+
|
|
242
|
+
// Skip elements that are visually hidden (like collapsed thought blocks)
|
|
243
|
+
let curr = el;
|
|
244
|
+
while(curr) {
|
|
245
|
+
if (curr.classList && (curr.classList.contains('opacity-0') || curr.classList.contains('max-h-0'))) return false;
|
|
246
|
+
curr = curr.parentNode || curr.host;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Exclude the outer application shell to avoid grabbing the input box or header
|
|
250
|
+
if (el.classList.contains('chat-app') || el.id === 'chat-container' || ['main', 'body'].includes(el.tagName?.toLowerCase())) return false;
|
|
251
|
+
|
|
252
|
+
const html = el.innerHTML.toLowerCase();
|
|
253
|
+
// Skip user messages and UI elements
|
|
254
|
+
if (html.includes('assistant') || html.includes('bot') || html.includes('response') || el.classList.contains('markdown-body') || el.classList.contains('markdown') || el.classList.contains('select-text')) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
function cleanTextNode(node) {
|
|
261
|
+
const clone = node.cloneNode(true);
|
|
262
|
+
// Remove <details> and <summary> which contain thoughts
|
|
263
|
+
const details = clone.querySelectorAll('details, summary, .thought, .thought-block, style, script');
|
|
264
|
+
details.forEach(d => d.remove());
|
|
265
|
+
|
|
266
|
+
// Remove known UI artifacts and collapsed thought blocks
|
|
267
|
+
const uis = clone.querySelectorAll('.ask-anything, .model-selector, [class*="placeholder"], [class*="status"], .opacity-0, .max-h-0, [class*="max-h-0"]');
|
|
268
|
+
uis.forEach(u => u.remove());
|
|
269
|
+
|
|
270
|
+
let text = (clone.innerText || clone.textContent || '').trim();
|
|
271
|
+
|
|
272
|
+
// Clean up leaked agent state headers and thought phase tags that slip through
|
|
273
|
+
text = text.replace(/🧠\\s*(CEO Orchestrator|Antigravity|Agent|Thought):?/gi, '');
|
|
274
|
+
text = text.replace(/⚡\\s*Planning/gi, '');
|
|
275
|
+
text = text.replace(/\\d+\\s+files?\\s+with\\s+changes/gi, '');
|
|
276
|
+
text = text.replace(/Prioritizing Tool Specificity[\\s\\S]*?before proceeding\\./gi, '');
|
|
277
|
+
text = text.replace(/^(Đang xử lý|Generating|Thinking|Running).*$/gim, '');
|
|
278
|
+
|
|
279
|
+
return text.trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (assistantMsgs.length > 0) {
|
|
283
|
+
return cleanTextNode(assistantMsgs[assistantMsgs.length - 1]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Secondary fallback if specific assistant wrappers aren't matched
|
|
287
|
+
const allMsgs = queryAllShadows('[class*="message"], [class*="chat"]');
|
|
288
|
+
const filteredMsgs = allMsgs.filter(el => {
|
|
289
|
+
if (!el || !el.classList) return false;
|
|
290
|
+
return !el.classList.contains('chat-app') && el.id !== 'chat-container';
|
|
291
|
+
});
|
|
292
|
+
if (filteredMsgs.length > 0) {
|
|
293
|
+
return cleanTextNode(filteredMsgs[filteredMsgs.length - 1]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return '';
|
|
297
|
+
})()`;
|
|
298
|
+
|
|
299
|
+
for (let i = this.contexts.length - 1; i >= 0; i--) {
|
|
300
|
+
const ctx = this.contexts[i];
|
|
301
|
+
try {
|
|
302
|
+
const res = await this.call('Runtime.evaluate', {
|
|
303
|
+
expression: script, returnByValue: true, contextId: ctx.id,
|
|
304
|
+
});
|
|
305
|
+
if (res?.result?.value) {
|
|
306
|
+
let text = res.result.value.trim();
|
|
307
|
+
// Filter out obvious whole-UI dumps if they accidentally catch the header
|
|
308
|
+
if (!text.includes('Explored') && text.length > 10) {
|
|
309
|
+
return text;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch { /* ignore context error */ }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 3. NATIVE AXTREE EXTRACTION (Absolute last resort, heavily stripped)
|
|
316
|
+
try {
|
|
317
|
+
const tree = await this.call('Accessibility.getFullAXTree');
|
|
318
|
+
const nodes = tree?.nodes || [];
|
|
319
|
+
const textNodes = nodes.filter((n: any) =>
|
|
320
|
+
['StaticText', 'InlineTextBox', 'paragraph'].includes(n.role?.value) && n.name?.value?.trim()
|
|
321
|
+
).map((n: any) => n.name.value.trim());
|
|
322
|
+
|
|
323
|
+
let promptIndex = -1;
|
|
324
|
+
for (let i = textNodes.length - 1; i >= 0; i--) {
|
|
325
|
+
if (/^\[TG-Topic-\d+\]/.test(textNodes[i])) {
|
|
326
|
+
promptIndex = i;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (promptIndex !== -1) {
|
|
332
|
+
let lines: string[] = [];
|
|
333
|
+
for (let i = promptIndex + 1; i < textNodes.length; i++) {
|
|
334
|
+
let txt = textNodes[i];
|
|
335
|
+
let txtLower = txt.toLowerCase();
|
|
336
|
+
|
|
337
|
+
if (/ask anything/i.test(txtLower)) break;
|
|
338
|
+
if (/@ to mention/i.test(txtLower)) break;
|
|
339
|
+
if (/\/ for workflows/i.test(txtLower)) break;
|
|
340
|
+
if (/search the workspace/i.test(txtLower)) break;
|
|
341
|
+
if (/^\[TG-Topic-\d+\]/i.test(txt)) break;
|
|
342
|
+
|
|
343
|
+
if (/press desired key combination/i.test(txtLower)) continue;
|
|
344
|
+
|
|
345
|
+
// Skip Tool Events & Execution Status
|
|
346
|
+
const skipExactList = ['content_copy', 'always run', 'cancel', 'reject all', 'accept all', 'preview', 'edit', 'analyzed', 'generating..', 'planning'];
|
|
347
|
+
if (skipExactList.includes(txtLower.trim())) continue;
|
|
348
|
+
|
|
349
|
+
if (/^\d+\s*files?\s*with\s*changes/i.test(txtLower)) continue;
|
|
350
|
+
if (/^[+-]\d+$/.test(txt.trim())) continue;
|
|
351
|
+
if (/(Gemini|Claude|GPT|Ollama).*?(Pro|High|Sonnet|Opus|Flash|Tool|Agent)/i.test(txt)) continue;
|
|
352
|
+
if (/(CEO Orchestrator|Thought for \d+s|Antigravity)/i.test(txt)) continue;
|
|
353
|
+
|
|
354
|
+
// Skip Tool Usage strings (Explored X pages, Ran command, Edited X files, etc.)
|
|
355
|
+
if (/^(explored|ran command|ran background command|checked command status|edited|evaluating|analyzing|prioritizing|refining|formulating) /i.test(txtLower)) continue;
|
|
356
|
+
if (/^\d+\s+(file|command|search|page|browser)s?/i.test(txtLower)) continue;
|
|
357
|
+
if (txtLower === 'explored' || txtLower === 'exploring' || txtLower === 'ran command' || txtLower === 'exit code 0') continue;
|
|
358
|
+
if (txt === '🧠' || txt === '⚡' || txt.includes('🧠') || txt.includes('⚡')) continue;
|
|
359
|
+
|
|
360
|
+
if (txt.length > 1 && !lines.includes(txt)) {
|
|
361
|
+
lines.push(txt);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (lines.length > 0) return lines.join('\n');
|
|
365
|
+
}
|
|
366
|
+
} catch (e) { }
|
|
367
|
+
|
|
368
|
+
return '';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Extract the last user message from the IDE chat panel.
|
|
373
|
+
*/
|
|
374
|
+
async extractLastUserMessage(): Promise<string> {
|
|
375
|
+
const script = `(() => {
|
|
376
|
+
function queryAllShadows(selector, root = document) {
|
|
377
|
+
let results = [];
|
|
378
|
+
if (root.querySelectorAll) results.push(...root.querySelectorAll(selector));
|
|
379
|
+
let walker = document.createTreeWalker(root, 1, null, false);
|
|
380
|
+
while (walker.nextNode()) {
|
|
381
|
+
if (walker.currentNode.shadowRoot) {
|
|
382
|
+
results.push(...queryAllShadows(selector, walker.currentNode.shadowRoot));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return results;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const containers = queryAllShadows('[class*="message-container"], [class*="chat-message"]');
|
|
389
|
+
const userMsgs = containers.filter(el => {
|
|
390
|
+
const html = el.innerHTML.toLowerCase();
|
|
391
|
+
return !html.includes('assistant') && !html.includes('response') && !html.includes('bot');
|
|
392
|
+
});
|
|
393
|
+
if (userMsgs.length === 0) return '';
|
|
394
|
+
const last = userMsgs[userMsgs.length - 1];
|
|
395
|
+
return last.innerText || last.textContent || '';
|
|
396
|
+
})()`;
|
|
397
|
+
|
|
398
|
+
for (const ctx of this.contexts) {
|
|
399
|
+
if (ctx.url && ctx.url.includes('workbench.html')) {
|
|
400
|
+
try {
|
|
401
|
+
const res = await this.call('Runtime.evaluate', {
|
|
402
|
+
expression: script, returnByValue: true, contextId: ctx.id,
|
|
403
|
+
});
|
|
404
|
+
if (res?.result?.value) return res.result.value.trim();
|
|
405
|
+
} catch { /* next context */ }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Check if AI is still generating (spinner/stop button present)
|
|
413
|
+
*/
|
|
414
|
+
async isGenerating(): Promise<boolean> {
|
|
415
|
+
try {
|
|
416
|
+
const tree = await this.call('Accessibility.getFullAXTree');
|
|
417
|
+
const nodes = tree?.nodes || [];
|
|
418
|
+
|
|
419
|
+
const isGen = nodes.some((n: any) => {
|
|
420
|
+
const role = n.role?.value;
|
|
421
|
+
const name = (n.name?.value || '').trim().toLowerCase();
|
|
422
|
+
if (!name) return false;
|
|
423
|
+
|
|
424
|
+
// Check for Cancel/Stop button
|
|
425
|
+
if (role === 'button' && (name === 'cancel' || name.includes('stop generation') || name === 'stop')) return true;
|
|
426
|
+
|
|
427
|
+
// Exact strings that indicate loading state in the UI (not user text)
|
|
428
|
+
const exactLoadingStates = [
|
|
429
|
+
'generating...', 'thinking...', 'analyzing...', 'working...',
|
|
430
|
+
'evaluating...', 'planning...', 'running...', 'searching...'
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
if (role === 'StaticText' || role === 'InlineTextBox') {
|
|
434
|
+
if (exactLoadingStates.includes(name)) return true;
|
|
435
|
+
if (name.startsWith('running command:')) return true;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (isGen) return true;
|
|
441
|
+
} catch { /* ignore */ }
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Extract the real-time agent status text (e.g. "Running command...", "Searching web...")
|
|
447
|
+
*/
|
|
448
|
+
async extractAgentStatus(): Promise<string> {
|
|
449
|
+
try {
|
|
450
|
+
const tree = await this.call('Accessibility.getFullAXTree');
|
|
451
|
+
const nodes = tree?.nodes || [];
|
|
452
|
+
|
|
453
|
+
for (const n of nodes) {
|
|
454
|
+
if (n.role?.value !== 'StaticText' && n.role?.value !== 'InlineTextBox') continue;
|
|
455
|
+
const text = n.name?.value?.trim() || '';
|
|
456
|
+
if (text.length > 3 && text.length < 150) {
|
|
457
|
+
const lower = text.toLowerCase();
|
|
458
|
+
|
|
459
|
+
// Must exactly end with "..." to be a status indicator, or be a specific tool execution
|
|
460
|
+
if (lower.endsWith('...')) {
|
|
461
|
+
if (
|
|
462
|
+
lower.startsWith('thought') ||
|
|
463
|
+
lower.startsWith('thinking') ||
|
|
464
|
+
lower.startsWith('running') ||
|
|
465
|
+
lower.startsWith('analyzing') ||
|
|
466
|
+
lower.startsWith('reading') ||
|
|
467
|
+
lower.startsWith('viewing') ||
|
|
468
|
+
lower.startsWith('searching') ||
|
|
469
|
+
lower.startsWith('executing') ||
|
|
470
|
+
lower.startsWith('writing') ||
|
|
471
|
+
lower.startsWith('listing') ||
|
|
472
|
+
lower.startsWith('generating') ||
|
|
473
|
+
lower.startsWith('evaluating') ||
|
|
474
|
+
lower.startsWith('exploring') ||
|
|
475
|
+
lower.startsWith('considering') ||
|
|
476
|
+
lower.startsWith('planning')
|
|
477
|
+
) {
|
|
478
|
+
return text;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (lower.match(/^\\d+\\s+files?\\s+with\\s+changes/i)) return text;
|
|
482
|
+
if (lower.startsWith('ran command') || lower.startsWith('explored')) return text;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Generic fallback
|
|
487
|
+
if (await this.isGenerating()) {
|
|
488
|
+
return 'Đang xử lý (Processing)...';
|
|
489
|
+
}
|
|
490
|
+
} catch { /* ignore */ }
|
|
491
|
+
|
|
492
|
+
return '';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Capture a screenshot of the current page
|
|
497
|
+
*/
|
|
498
|
+
async captureScreenshot(): Promise<Buffer | null> {
|
|
499
|
+
try {
|
|
500
|
+
const res = await this.call('Page.captureScreenshot', { format: 'png' });
|
|
501
|
+
if (res && res.data) {
|
|
502
|
+
return Buffer.from(res.data, 'base64');
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
console.error('[CDP] ❌ Screenshot failed:', err);
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private sleep(ms: number): Promise<void> {
|
|
511
|
+
return new Promise(r => setTimeout(r, ms));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CdpService } from './CdpService.js';
|
|
2
|
+
|
|
3
|
+
export interface MonitorResult {
|
|
4
|
+
response: string;
|
|
5
|
+
completed: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ResponseMonitor {
|
|
10
|
+
private cdp: CdpService;
|
|
11
|
+
private pollIntervalMs = 2000;
|
|
12
|
+
private maxWaitMs = 86400000; // 24 hours (Infinite)
|
|
13
|
+
|
|
14
|
+
constructor(cdp: CdpService) {
|
|
15
|
+
this.cdp = cdp;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wait for AI to finish generating and return the full response.
|
|
20
|
+
* Calls onProgress for each intermediate state.
|
|
21
|
+
* Calls onStatus for thinking/status updates.
|
|
22
|
+
*/
|
|
23
|
+
async waitForResponse(
|
|
24
|
+
onProgress?: (partial: string) => void,
|
|
25
|
+
onStatus?: (status: string) => void
|
|
26
|
+
): Promise<MonitorResult> {
|
|
27
|
+
const startTime = Date.now();
|
|
28
|
+
let lastResponse = '';
|
|
29
|
+
let staleCount = 0;
|
|
30
|
+
let statusUpdateCount = 0;
|
|
31
|
+
|
|
32
|
+
// Initial wait
|
|
33
|
+
let lastKnownStatus = 'Khởi tạo tác vụ... (Initializing)';
|
|
34
|
+
if (onStatus) onStatus(lastKnownStatus);
|
|
35
|
+
await this.sleep(1500);
|
|
36
|
+
|
|
37
|
+
while (Date.now() - startTime < this.maxWaitMs) {
|
|
38
|
+
const generating = await this.cdp.isGenerating();
|
|
39
|
+
const currentResponse = await this.cdp.extractLastResponse();
|
|
40
|
+
|
|
41
|
+
// Update status every cycle, the callback will debounce it
|
|
42
|
+
if (onStatus) {
|
|
43
|
+
const liveStatus = await this.cdp.extractAgentStatus();
|
|
44
|
+
if (liveStatus && !liveStatus.includes('Đang xử lý')) {
|
|
45
|
+
lastKnownStatus = liveStatus;
|
|
46
|
+
}
|
|
47
|
+
// Luôn gửi trạng thái sống động cuối cùng (VD: Thought for 3s, Running command...)
|
|
48
|
+
onStatus(lastKnownStatus);
|
|
49
|
+
}
|
|
50
|
+
statusUpdateCount++;
|
|
51
|
+
|
|
52
|
+
// Detect new content
|
|
53
|
+
if (currentResponse && currentResponse !== lastResponse) {
|
|
54
|
+
lastResponse = currentResponse;
|
|
55
|
+
staleCount = 0;
|
|
56
|
+
if (onProgress) onProgress(this.truncate(currentResponse, 4000));
|
|
57
|
+
} else {
|
|
58
|
+
staleCount++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// AI finished: not generating + response stable for 3 polls
|
|
62
|
+
if (!generating && lastResponse && staleCount >= 2) {
|
|
63
|
+
return { response: lastResponse, completed: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Timeout safety: if stuck generating but no new text for 60 polls (120 seconds)
|
|
67
|
+
if (generating && staleCount >= 60 && lastResponse && lastResponse.length > 20) {
|
|
68
|
+
return { response: lastResponse, completed: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If NOT generating but stuck with some UI artifact that wouldn't parse well, timeout sooner
|
|
72
|
+
if (!generating && staleCount >= 10 && lastResponse) {
|
|
73
|
+
return { response: lastResponse, completed: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If NOT generating and still NO response after 15 polls (30 seconds), it probably failed or returned empty
|
|
77
|
+
if (!generating && staleCount >= 15 && !lastResponse) {
|
|
78
|
+
return {
|
|
79
|
+
response: "❌ Phản hồi đã hoàn tất nhưng bot không trích xuất được nội dung (Rỗng).",
|
|
80
|
+
completed: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await this.sleep(this.pollIntervalMs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
response: lastResponse || '⏰ Quá thời gian chờ (15 phút). Đặc vụ có thể đang chạy một quá trình quá tải hoặc IDE đã mất kết nối.',
|
|
89
|
+
completed: false,
|
|
90
|
+
error: 'Timeout waiting for IDE response',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private truncate(text: string, maxLen: number): string {
|
|
95
|
+
if (text.length <= maxLen) return text;
|
|
96
|
+
return text.substring(0, maxLen) + '\n\n... (truncated)';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private sleep(ms: number): Promise<void> {
|
|
100
|
+
return new Promise(r => setTimeout(r, ms));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 🏢 CEOgravity - Startup Orchestrator
|
|
3
|
+
# Golden Combo Edition (V3)
|
|
4
|
+
|
|
5
|
+
# 1. Load config
|
|
6
|
+
if [ -f .env ]; then
|
|
7
|
+
source .env
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# 2. Khởi động Giao diện & Agent (Agent-Defined)
|
|
11
|
+
echo "🚀 Activating CEOgravity Golden Portal..."
|
|
12
|
+
bash .agent/scripts/receptionist_up.sh
|