@usejarvis/brain 0.1.0
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/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Controller — High-level browser automation
|
|
3
|
+
*
|
|
4
|
+
* Wraps CDPClient with user-friendly operations:
|
|
5
|
+
* navigate, snapshot (interactive elements with IDs), click, type, screenshot.
|
|
6
|
+
*
|
|
7
|
+
* The snapshot approach: each interactive element gets a numeric [id].
|
|
8
|
+
* The LLM sees these IDs and references them in click/type commands.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { CDPClient } from './cdp.ts';
|
|
12
|
+
import { STEALTH_SCRIPT } from './stealth.ts';
|
|
13
|
+
import { launchChrome, stopChrome, type RunningBrowser } from './chrome-launcher.ts';
|
|
14
|
+
|
|
15
|
+
export type PageElement = {
|
|
16
|
+
id: number;
|
|
17
|
+
tag: string;
|
|
18
|
+
text: string;
|
|
19
|
+
attrs: Record<string, string>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type PageSnapshot = {
|
|
23
|
+
title: string;
|
|
24
|
+
url: string;
|
|
25
|
+
text: string;
|
|
26
|
+
elements: PageElement[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// JS function injected into the page to extract interactive elements
|
|
30
|
+
const SNAPSHOT_SCRIPT = `(() => {
|
|
31
|
+
const els = [];
|
|
32
|
+
const seen = new WeakSet();
|
|
33
|
+
const sel = [
|
|
34
|
+
'a', 'button', 'input', 'select', 'textarea', 'summary',
|
|
35
|
+
'[role="button"]', '[role="link"]', '[role="tab"]', '[role="textbox"]',
|
|
36
|
+
'[role="combobox"]', '[role="menuitem"]', '[role="option"]',
|
|
37
|
+
'[onclick]', '[contenteditable="true"]', '[tabindex="0"]',
|
|
38
|
+
'[data-testid]'
|
|
39
|
+
].join(', ');
|
|
40
|
+
document.querySelectorAll(sel).forEach((el) => {
|
|
41
|
+
// Skip duplicates (child of already-captured parent)
|
|
42
|
+
if (seen.has(el)) return;
|
|
43
|
+
seen.add(el);
|
|
44
|
+
|
|
45
|
+
const rect = el.getBoundingClientRect();
|
|
46
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
47
|
+
if (rect.width < 5 || rect.height < 5) return;
|
|
48
|
+
const style = window.getComputedStyle(el);
|
|
49
|
+
if (style.visibility === 'hidden') return;
|
|
50
|
+
if (style.display === 'none') return;
|
|
51
|
+
if (style.opacity === '0') return;
|
|
52
|
+
|
|
53
|
+
const tag = el.tagName.toLowerCase();
|
|
54
|
+
const text = (el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 100);
|
|
55
|
+
const attrs = {};
|
|
56
|
+
for (const a of ['href', 'name', 'placeholder', 'type', 'value', 'aria-label', 'title', 'id', 'role', 'data-testid', 'contenteditable']) {
|
|
57
|
+
const v = el.getAttribute(a);
|
|
58
|
+
if (v) attrs[a] = v.slice(0, 200);
|
|
59
|
+
}
|
|
60
|
+
els.push({
|
|
61
|
+
tag,
|
|
62
|
+
text,
|
|
63
|
+
attrs,
|
|
64
|
+
x: Math.round(rect.x + rect.width / 2),
|
|
65
|
+
y: Math.round(rect.y + rect.height / 2)
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Assign sequential IDs
|
|
70
|
+
els.forEach((el, i) => { el.id = i + 1; });
|
|
71
|
+
|
|
72
|
+
// Get visible text, clean up whitespace
|
|
73
|
+
let bodyText = document.body.innerText || '';
|
|
74
|
+
bodyText = bodyText.replace(/\\n{3,}/g, '\\n\\n').trim().slice(0, 8000);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
title: document.title,
|
|
78
|
+
url: location.href,
|
|
79
|
+
text: bodyText,
|
|
80
|
+
elements: els
|
|
81
|
+
};
|
|
82
|
+
})()`;
|
|
83
|
+
|
|
84
|
+
export class BrowserController {
|
|
85
|
+
private cdp: CDPClient;
|
|
86
|
+
private port: number;
|
|
87
|
+
private profileDir: string | undefined;
|
|
88
|
+
private _connected = false;
|
|
89
|
+
private runningBrowser: RunningBrowser | null = null;
|
|
90
|
+
// Coordinates stored from last snapshot — not sent to LLM
|
|
91
|
+
private elementCoords = new Map<number, { x: number; y: number }>();
|
|
92
|
+
|
|
93
|
+
constructor(port: number = 9222, profileDir?: string) {
|
|
94
|
+
this.cdp = new CDPClient();
|
|
95
|
+
this.port = port;
|
|
96
|
+
this.profileDir = profileDir;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if Chrome CDP is already reachable on the debug port.
|
|
101
|
+
*/
|
|
102
|
+
async isAvailable(): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`http://127.0.0.1:${this.port}/json/version`, {
|
|
105
|
+
signal: AbortSignal.timeout(2000),
|
|
106
|
+
});
|
|
107
|
+
return res.ok;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Connect to Chrome. If Chrome isn't running, auto-launches it
|
|
115
|
+
* with CDP enabled and an isolated profile. No user setup required.
|
|
116
|
+
*/
|
|
117
|
+
async connect(): Promise<void> {
|
|
118
|
+
if (this._connected) return;
|
|
119
|
+
|
|
120
|
+
// If Chrome isn't running, launch it automatically
|
|
121
|
+
if (!(await this.isAvailable())) {
|
|
122
|
+
console.log('[BrowserController] Chrome not detected, launching automatically...');
|
|
123
|
+
this.runningBrowser = await launchChrome(this.port, this.profileDir);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Discover page targets
|
|
127
|
+
const listRes = await fetch(`http://127.0.0.1:${this.port}/json/list`);
|
|
128
|
+
if (!listRes.ok) {
|
|
129
|
+
throw new Error('Chrome CDP not reachable after launch');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const targets = await listRes.json() as Array<{
|
|
133
|
+
type: string;
|
|
134
|
+
webSocketDebuggerUrl: string;
|
|
135
|
+
}>;
|
|
136
|
+
|
|
137
|
+
let pageTarget = targets.find(t => t.type === 'page');
|
|
138
|
+
|
|
139
|
+
if (!pageTarget) {
|
|
140
|
+
// Create a new tab
|
|
141
|
+
const newRes = await fetch(`http://127.0.0.1:${this.port}/json/new?about:blank`);
|
|
142
|
+
pageTarget = await newRes.json() as any;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!pageTarget?.webSocketDebuggerUrl) {
|
|
146
|
+
throw new Error('No page target found and could not create one');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Connect CDP to the page
|
|
150
|
+
await this.cdp.connect(pageTarget.webSocketDebuggerUrl);
|
|
151
|
+
|
|
152
|
+
// Enable required CDP domains
|
|
153
|
+
await this.cdp.send('Page.enable');
|
|
154
|
+
await this.cdp.send('Runtime.enable');
|
|
155
|
+
await this.cdp.send('DOM.enable');
|
|
156
|
+
|
|
157
|
+
// Inject stealth scripts for all future navigations
|
|
158
|
+
await this.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
159
|
+
source: STEALTH_SCRIPT,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this._connected = true;
|
|
163
|
+
console.log('[BrowserController] Connected to Chrome');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Navigate to a URL and wait for the page to load.
|
|
168
|
+
*/
|
|
169
|
+
async navigate(url: string): Promise<PageSnapshot> {
|
|
170
|
+
await this.ensureConnected();
|
|
171
|
+
|
|
172
|
+
const loadPromise = this.cdp.waitForEvent('Page.loadEventFired', 30000);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await this.cdp.send('Page.navigate', { url });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// If navigate fails, suppress the dangling loadPromise timeout
|
|
178
|
+
loadPromise.catch(() => {});
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await loadPromise;
|
|
184
|
+
} catch {
|
|
185
|
+
// Page.loadEventFired timeout — page may still be usable (SPAs, slow loads)
|
|
186
|
+
console.warn(`[BrowserController] Page load timeout for ${url}, continuing anyway`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Wait for JS to settle
|
|
190
|
+
await Bun.sleep(800);
|
|
191
|
+
|
|
192
|
+
return this.snapshot();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get a snapshot of the current page: text content + numbered interactive elements.
|
|
197
|
+
*/
|
|
198
|
+
async snapshot(): Promise<PageSnapshot> {
|
|
199
|
+
await this.ensureConnected();
|
|
200
|
+
|
|
201
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
202
|
+
expression: SNAPSHOT_SCRIPT,
|
|
203
|
+
returnByValue: true,
|
|
204
|
+
awaitPromise: true,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (result.exceptionDetails) {
|
|
208
|
+
throw new Error(`Snapshot failed: ${JSON.stringify(result.exceptionDetails)}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const data = result.result.value as PageSnapshot & {
|
|
212
|
+
elements: Array<PageElement & { x: number; y: number }>;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Store coordinates locally, strip from LLM-facing data
|
|
216
|
+
this.elementCoords.clear();
|
|
217
|
+
const cleanElements: PageElement[] = [];
|
|
218
|
+
|
|
219
|
+
for (const el of data.elements) {
|
|
220
|
+
this.elementCoords.set(el.id, { x: el.x, y: el.y });
|
|
221
|
+
cleanElements.push({
|
|
222
|
+
id: el.id,
|
|
223
|
+
tag: el.tag,
|
|
224
|
+
text: el.text,
|
|
225
|
+
attrs: el.attrs,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
title: data.title,
|
|
231
|
+
url: data.url,
|
|
232
|
+
text: data.text,
|
|
233
|
+
elements: cleanElements,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Click an element by its snapshot ID.
|
|
239
|
+
*/
|
|
240
|
+
async click(elementId: number): Promise<string> {
|
|
241
|
+
await this.ensureConnected();
|
|
242
|
+
|
|
243
|
+
const coords = this.elementCoords.get(elementId);
|
|
244
|
+
if (!coords) {
|
|
245
|
+
return `Error: Element [${elementId}] not found. Run browser_snapshot first.`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await this.cdp.send('Input.dispatchMouseEvent', {
|
|
249
|
+
type: 'mousePressed',
|
|
250
|
+
x: coords.x,
|
|
251
|
+
y: coords.y,
|
|
252
|
+
button: 'left',
|
|
253
|
+
clickCount: 1,
|
|
254
|
+
});
|
|
255
|
+
await this.cdp.send('Input.dispatchMouseEvent', {
|
|
256
|
+
type: 'mouseReleased',
|
|
257
|
+
x: coords.x,
|
|
258
|
+
y: coords.y,
|
|
259
|
+
button: 'left',
|
|
260
|
+
clickCount: 1,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Wait for navigation/changes
|
|
264
|
+
await Bun.sleep(1000);
|
|
265
|
+
|
|
266
|
+
return `Clicked element [${elementId}]`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Type text into an input element by its snapshot ID.
|
|
271
|
+
* Optionally press Enter after typing.
|
|
272
|
+
*/
|
|
273
|
+
async type(elementId: number, text: string, submit: boolean = false): Promise<string> {
|
|
274
|
+
await this.ensureConnected();
|
|
275
|
+
|
|
276
|
+
// Click to focus the element
|
|
277
|
+
const clickResult = await this.click(elementId);
|
|
278
|
+
if (clickResult.startsWith('Error:')) return clickResult;
|
|
279
|
+
|
|
280
|
+
await Bun.sleep(200);
|
|
281
|
+
|
|
282
|
+
// Clear existing content
|
|
283
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
284
|
+
type: 'keyDown',
|
|
285
|
+
key: 'a',
|
|
286
|
+
code: 'KeyA',
|
|
287
|
+
windowsVirtualKeyCode: 65,
|
|
288
|
+
nativeVirtualKeyCode: 65,
|
|
289
|
+
modifiers: 2, // Ctrl
|
|
290
|
+
});
|
|
291
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
292
|
+
type: 'keyUp',
|
|
293
|
+
key: 'a',
|
|
294
|
+
code: 'KeyA',
|
|
295
|
+
windowsVirtualKeyCode: 65,
|
|
296
|
+
nativeVirtualKeyCode: 65,
|
|
297
|
+
modifiers: 2,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Insert text (like paste — much more reliable than char-by-char)
|
|
301
|
+
await this.cdp.send('Input.insertText', { text });
|
|
302
|
+
|
|
303
|
+
let result = `Typed "${text}" into element [${elementId}]`;
|
|
304
|
+
|
|
305
|
+
if (submit) {
|
|
306
|
+
await Bun.sleep(100);
|
|
307
|
+
await this.pressEnter();
|
|
308
|
+
// Wait for page load after submit
|
|
309
|
+
await Bun.sleep(2000);
|
|
310
|
+
result += ' and pressed Enter';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Press Enter key.
|
|
318
|
+
*/
|
|
319
|
+
async pressEnter(): Promise<void> {
|
|
320
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
321
|
+
type: 'rawKeyDown',
|
|
322
|
+
key: 'Enter',
|
|
323
|
+
code: 'Enter',
|
|
324
|
+
windowsVirtualKeyCode: 13,
|
|
325
|
+
nativeVirtualKeyCode: 13,
|
|
326
|
+
});
|
|
327
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
328
|
+
type: 'char',
|
|
329
|
+
key: 'Enter',
|
|
330
|
+
code: 'Enter',
|
|
331
|
+
windowsVirtualKeyCode: 13,
|
|
332
|
+
nativeVirtualKeyCode: 13,
|
|
333
|
+
});
|
|
334
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
335
|
+
type: 'keyUp',
|
|
336
|
+
key: 'Enter',
|
|
337
|
+
code: 'Enter',
|
|
338
|
+
windowsVirtualKeyCode: 13,
|
|
339
|
+
nativeVirtualKeyCode: 13,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Scroll the page up or down.
|
|
345
|
+
* direction: 'down' or 'up'
|
|
346
|
+
* amount: pixels to scroll (default: one viewport height)
|
|
347
|
+
*/
|
|
348
|
+
async scroll(direction: 'up' | 'down' = 'down', amount?: number): Promise<string> {
|
|
349
|
+
await this.ensureConnected();
|
|
350
|
+
|
|
351
|
+
const viewportHeight = (await this.evaluate('window.innerHeight') as number) || 600;
|
|
352
|
+
const scrollAmount = amount ?? viewportHeight;
|
|
353
|
+
|
|
354
|
+
const pixels = direction === 'down' ? scrollAmount : -scrollAmount;
|
|
355
|
+
|
|
356
|
+
await this.evaluate(`window.scrollBy(0, ${pixels})`);
|
|
357
|
+
await Bun.sleep(500); // Wait for lazy-loaded content
|
|
358
|
+
|
|
359
|
+
return `Scrolled ${direction} by ${scrollAmount}px`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Take a screenshot and save to a file.
|
|
364
|
+
*/
|
|
365
|
+
async screenshot(filePath: string = '/tmp/jarvis-screenshot.png'): Promise<string> {
|
|
366
|
+
await this.ensureConnected();
|
|
367
|
+
|
|
368
|
+
const result = await this.cdp.send('Page.captureScreenshot', { format: 'png' });
|
|
369
|
+
const buffer = Buffer.from(result.data, 'base64');
|
|
370
|
+
|
|
371
|
+
await Bun.write(filePath, buffer);
|
|
372
|
+
return filePath;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Take a screenshot and return raw base64 data (for vision/LLM).
|
|
377
|
+
*/
|
|
378
|
+
async screenshotBuffer(): Promise<{ base64: string; mimeType: string }> {
|
|
379
|
+
await this.ensureConnected();
|
|
380
|
+
const result = await this.cdp.send('Page.captureScreenshot', { format: 'png' });
|
|
381
|
+
return { base64: result.data, mimeType: 'image/png' };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Evaluate arbitrary JavaScript in the page context.
|
|
386
|
+
*/
|
|
387
|
+
async evaluate(expression: string): Promise<unknown> {
|
|
388
|
+
await this.ensureConnected();
|
|
389
|
+
|
|
390
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
391
|
+
expression,
|
|
392
|
+
returnByValue: true,
|
|
393
|
+
awaitPromise: true,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (result.exceptionDetails) {
|
|
397
|
+
throw new Error(`JS error: ${JSON.stringify(result.exceptionDetails)}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return result.result.value;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Disconnect from Chrome. If we auto-launched Chrome, stop it too.
|
|
405
|
+
*/
|
|
406
|
+
async disconnect(): Promise<void> {
|
|
407
|
+
if (this._connected) {
|
|
408
|
+
await this.cdp.close();
|
|
409
|
+
this._connected = false;
|
|
410
|
+
this.elementCoords.clear();
|
|
411
|
+
console.log('[BrowserController] Disconnected');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Stop the Chrome process we launched (if any)
|
|
415
|
+
if (this.runningBrowser) {
|
|
416
|
+
await stopChrome(this.runningBrowser);
|
|
417
|
+
this.runningBrowser = null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
get connected(): boolean {
|
|
422
|
+
return this._connected;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private async ensureConnected(): Promise<void> {
|
|
426
|
+
if (this._connected && !this.cdp.isOpen) {
|
|
427
|
+
// Connection went stale — reset and reconnect
|
|
428
|
+
console.warn('[BrowserController] CDP connection stale, reconnecting...');
|
|
429
|
+
this._connected = false;
|
|
430
|
+
this.elementCoords.clear();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!this._connected) {
|
|
434
|
+
await this.connect();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Stealth — Anti-detection scripts
|
|
3
|
+
*
|
|
4
|
+
* Injected into every new document via Page.addScriptToEvaluateOnNewDocument
|
|
5
|
+
* to hide automation fingerprints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const STEALTH_SCRIPT = `
|
|
9
|
+
// Hide webdriver flag
|
|
10
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
11
|
+
|
|
12
|
+
// Fake plugins array (real browsers have plugins)
|
|
13
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
14
|
+
get: () => {
|
|
15
|
+
const plugins = [
|
|
16
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
|
|
17
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
|
|
18
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin' },
|
|
19
|
+
];
|
|
20
|
+
plugins.length = 3;
|
|
21
|
+
return plugins;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Fake languages
|
|
26
|
+
Object.defineProperty(navigator, 'languages', {
|
|
27
|
+
get: () => ['en-US', 'en']
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Remove automation-related properties from window
|
|
31
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
|
|
32
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
|
|
33
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
|
|
34
|
+
|
|
35
|
+
// Fix chrome.runtime to look like a real browser
|
|
36
|
+
if (!window.chrome) window.chrome = {};
|
|
37
|
+
if (!window.chrome.runtime) window.chrome.runtime = {};
|
|
38
|
+
|
|
39
|
+
// Fix permissions query
|
|
40
|
+
const originalQuery = window.navigator.permissions?.query;
|
|
41
|
+
if (originalQuery) {
|
|
42
|
+
window.navigator.permissions.query = (parameters) => {
|
|
43
|
+
if (parameters.name === 'notifications') {
|
|
44
|
+
return Promise.resolve({ state: Notification.permission });
|
|
45
|
+
}
|
|
46
|
+
return originalQuery(parameters);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// App Control exports
|
|
2
|
+
export { getAppController } from './app-control/interface.ts';
|
|
3
|
+
export type { AppController, WindowInfo, UIElement } from './app-control/interface.ts';
|
|
4
|
+
export { LinuxAppController } from './app-control/linux.ts';
|
|
5
|
+
export { WindowsAppController } from './app-control/windows.ts';
|
|
6
|
+
export { MacAppController } from './app-control/macos.ts';
|
|
7
|
+
|
|
8
|
+
// Browser exports
|
|
9
|
+
export { CDPClient as CDPBrowser } from './browser/cdp.ts';
|
|
10
|
+
export type { PageElement as BrowserTab } from './browser/session.ts';
|
|
11
|
+
export { BrowserController as BrowserSession } from './browser/session.ts';
|
|
12
|
+
|
|
13
|
+
// Terminal exports
|
|
14
|
+
export { TerminalExecutor } from './terminal/executor.ts';
|
|
15
|
+
export type { CommandResult, ExecuteOptions } from './terminal/executor.ts';
|
|
16
|
+
export { WSLBridge } from './terminal/wsl-bridge.ts';
|
|
17
|
+
|
|
18
|
+
// Tools exports
|
|
19
|
+
export { ToolRegistry } from './tools/registry.ts';
|
|
20
|
+
export type { ToolDefinition, ToolParameter } from './tools/registry.ts';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawn, type Subprocess } from 'bun';
|
|
2
|
+
|
|
3
|
+
export type CommandResult = {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
duration: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ExecuteOptions = {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class TerminalExecutor {
|
|
17
|
+
private shell: string;
|
|
18
|
+
private defaultTimeout: number;
|
|
19
|
+
|
|
20
|
+
constructor(opts?: { shell?: string; timeout?: number }) {
|
|
21
|
+
this.shell = opts?.shell ?? TerminalExecutor.detectShell();
|
|
22
|
+
this.defaultTimeout = opts?.timeout ?? 30000;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async execute(command: string, opts?: ExecuteOptions): Promise<CommandResult> {
|
|
26
|
+
const startTime = Date.now();
|
|
27
|
+
const timeout = opts?.timeout ?? this.defaultTimeout;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const proc = spawn({
|
|
31
|
+
cmd: [this.shell, '-c', command],
|
|
32
|
+
cwd: opts?.cwd,
|
|
33
|
+
env: { ...process.env, ...opts?.env },
|
|
34
|
+
stdout: 'pipe',
|
|
35
|
+
stderr: 'pipe',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const timeoutId = setTimeout(() => {
|
|
39
|
+
proc.kill();
|
|
40
|
+
}, timeout);
|
|
41
|
+
|
|
42
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
43
|
+
this.readStream(proc.stdout),
|
|
44
|
+
this.readStream(proc.stderr),
|
|
45
|
+
proc.exited,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
|
|
50
|
+
const duration = Date.now() - startTime;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
stdout: stdout.toString('utf-8'),
|
|
54
|
+
stderr: stderr.toString('utf-8'),
|
|
55
|
+
exitCode: exitCode ?? 0,
|
|
56
|
+
duration,
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const duration = Date.now() - startTime;
|
|
60
|
+
|
|
61
|
+
if (duration >= timeout) {
|
|
62
|
+
throw new Error(`Command timed out after ${timeout}ms: ${command}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(`Command execution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async *stream(command: string, opts?: { cwd?: string; env?: Record<string, string> }): AsyncIterable<string> {
|
|
70
|
+
const proc = spawn({
|
|
71
|
+
cmd: [this.shell, '-c', command],
|
|
72
|
+
cwd: opts?.cwd,
|
|
73
|
+
env: { ...process.env, ...opts?.env },
|
|
74
|
+
stdout: 'pipe',
|
|
75
|
+
stderr: 'pipe',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const decoder = new TextDecoder();
|
|
79
|
+
|
|
80
|
+
if (!proc.stdout) {
|
|
81
|
+
throw new Error('Failed to get stdout stream');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const reader = proc.stdout.getReader();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
while (true) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
|
|
90
|
+
if (done) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (value) {
|
|
95
|
+
yield decoder.decode(value, { stream: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
reader.releaseLock();
|
|
100
|
+
proc.kill();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static detectShell(): string {
|
|
105
|
+
const platform = process.platform;
|
|
106
|
+
|
|
107
|
+
if (platform === 'win32') {
|
|
108
|
+
return process.env.COMSPEC ?? 'powershell.exe';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (process.env.SHELL) {
|
|
112
|
+
return process.env.SHELL;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return '/bin/bash';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getShell(): string {
|
|
119
|
+
return this.shell;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async readStream(stream: ReadableStream<Uint8Array> | null): Promise<Buffer> {
|
|
123
|
+
if (!stream) {
|
|
124
|
+
return Buffer.from('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const reader = stream.getReader();
|
|
128
|
+
const chunks: Uint8Array[] = [];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
while (true) {
|
|
132
|
+
const { done, value } = await reader.read();
|
|
133
|
+
|
|
134
|
+
if (done) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (value) {
|
|
139
|
+
chunks.push(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
reader.releaseLock();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
147
|
+
const result = new Uint8Array(totalLength);
|
|
148
|
+
|
|
149
|
+
let offset = 0;
|
|
150
|
+
for (const chunk of chunks) {
|
|
151
|
+
result.set(chunk, offset);
|
|
152
|
+
offset += chunk.length;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Buffer.from(result);
|
|
156
|
+
}
|
|
157
|
+
}
|