@shykaruu/jarvis-brain 0.4.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 +428 -0
- package/bin/jarvis.ts +449 -0
- package/package.json +79 -0
- package/roles/activity-observer.yaml +60 -0
- package/roles/ceo-founder.yaml +144 -0
- package/roles/chief-of-staff.yaml +158 -0
- package/roles/dev-lead.yaml +182 -0
- package/roles/executive-assistant.yaml +77 -0
- package/roles/marketing-director.yaml +168 -0
- package/roles/personal-assistant.yaml +266 -0
- package/roles/research-specialist.yaml +60 -0
- package/roles/specialists/content-writer.yaml +53 -0
- package/roles/specialists/customer-support.yaml +57 -0
- package/roles/specialists/data-analyst.yaml +57 -0
- package/roles/specialists/financial-analyst.yaml +56 -0
- package/roles/specialists/hr-specialist.yaml +55 -0
- package/roles/specialists/legal-advisor.yaml +58 -0
- package/roles/specialists/marketing-strategist.yaml +56 -0
- package/roles/specialists/project-coordinator.yaml +55 -0
- package/roles/specialists/research-analyst.yaml +58 -0
- package/roles/specialists/software-engineer.yaml +57 -0
- package/roles/specialists/system-administrator.yaml +57 -0
- package/roles/system-admin.yaml +76 -0
- package/scripts/ensure-bun.cjs +16 -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 +261 -0
- package/src/actions/browser/session.ts +506 -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 +363 -0
- package/src/actions/tools/builtin.ts +950 -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/documents.ts +169 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +31 -0
- package/src/actions/tools/registry.ts +173 -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 +592 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +309 -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 +301 -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 +417 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +238 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +827 -0
- package/src/cli/uninstall.test.ts +37 -0
- package/src/cli/uninstall.ts +202 -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/dashboard-auth.ts +75 -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 +149 -0
- package/src/comms/voice.test.ts +504 -0
- package/src/comms/voice.ts +341 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +669 -0
- package/src/config/README.md +389 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +183 -0
- package/src/config/loader.ts +148 -0
- package/src/config/types.ts +293 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +667 -0
- package/src/daemon/api-routes.ts +3067 -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/dashboard-auth.test.ts +170 -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/flock.c +7 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1070 -0
- package/src/daemon/llm-settings.test.ts +78 -0
- package/src/daemon/llm-settings.ts +450 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.test.ts +283 -0
- package/src/daemon/pid.ts +224 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +926 -0
- package/src/global.d.ts +4 -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 +407 -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 +400 -0
- package/src/llm/gemini.ts +380 -0
- package/src/llm/groq.ts +406 -0
- package/src/llm/history.ts +147 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/manager.ts +226 -0
- package/src/llm/ollama.ts +316 -0
- package/src/llm/openai.ts +411 -0
- package/src/llm/openrouter.ts +390 -0
- package/src/llm/provider.test.ts +487 -0
- package/src/llm/provider.ts +61 -0
- package/src/llm/test.ts +88 -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 +120 -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 +218 -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 +195 -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/sites/builder-tools.ts +215 -0
- package/src/sites/dev-server-manager.ts +286 -0
- package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
- package/src/sites/fixtures/security-test-site/Makefile +15 -0
- package/src/sites/fixtures/security-test-site/README.md +18 -0
- package/src/sites/fixtures/security-test-site/index.html +12 -0
- package/src/sites/fixtures/security-test-site/index.ts +16 -0
- package/src/sites/fixtures/security-test-site/package.json +13 -0
- package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
- package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
- package/src/sites/git-manager.ts +240 -0
- package/src/sites/github-manager.ts +355 -0
- package/src/sites/index.ts +25 -0
- package/src/sites/project-manager.ts +389 -0
- package/src/sites/proxy.ts +133 -0
- package/src/sites/service.ts +136 -0
- package/src/sites/templates.ts +169 -0
- package/src/sites/types.ts +89 -0
- package/src/user/profile-followup.test.ts +84 -0
- package/src/user/profile-followup.ts +185 -0
- package/src/user/profile.ts +224 -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 +270 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/dashboard-sessions.ts +44 -0
- package/src/vault/documents.ts +130 -0
- package/src/vault/entities.ts +185 -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 +139 -0
- package/src/vault/retrieval.ts +258 -0
- package/src/vault/schema.ts +709 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/user-profile.test.ts +113 -0
- package/src/vault/user-profile.ts +176 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/webapp-template-seeds.ts +116 -0
- package/src/vault/webapp-templates.ts +244 -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-3gr23jt9.js +112614 -0
- package/ui/dist/index-9vmj8127.css +14239 -0
- package/ui/dist/index-hy9pc1gm.js +112873 -0
- package/ui/dist/index-j2ep5d1w.js +112374 -0
- package/ui/dist/index-jt00vjqs.js +112858 -0
- package/ui/dist/index-k9ymx5qb.js +112374 -0
- package/ui/dist/index.html +16 -0
- package/ui/public/audio/pcm-capture-processor.js +11 -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,506 @@
|
|
|
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
|
+
'[role="row"]', '[role="gridcell"]',
|
|
38
|
+
'[onclick]', '[contenteditable="true"]', '[tabindex="0"]',
|
|
39
|
+
'[data-testid]'
|
|
40
|
+
].join(', ');
|
|
41
|
+
document.querySelectorAll(sel).forEach((el) => {
|
|
42
|
+
// Skip duplicates (child of already-captured parent)
|
|
43
|
+
if (seen.has(el)) return;
|
|
44
|
+
seen.add(el);
|
|
45
|
+
|
|
46
|
+
const rect = el.getBoundingClientRect();
|
|
47
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
48
|
+
if (rect.width < 5 || rect.height < 5) return;
|
|
49
|
+
const style = window.getComputedStyle(el);
|
|
50
|
+
if (style.visibility === 'hidden') return;
|
|
51
|
+
if (style.display === 'none') return;
|
|
52
|
+
if (style.opacity === '0') return;
|
|
53
|
+
|
|
54
|
+
const tag = el.tagName.toLowerCase();
|
|
55
|
+
const text = (el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 100);
|
|
56
|
+
const attrs = {};
|
|
57
|
+
for (const a of ['href', 'name', 'placeholder', 'type', 'aria-label', 'title', 'id', 'role', 'data-testid', 'contenteditable']) {
|
|
58
|
+
const v = el.getAttribute(a);
|
|
59
|
+
if (v) attrs[a] = v.slice(0, 200);
|
|
60
|
+
}
|
|
61
|
+
// Capture live value (JS property) for inputs — getAttribute('value') returns the HTML default
|
|
62
|
+
if ('value' in el && el.value) attrs.value = String(el.value).slice(0, 200);
|
|
63
|
+
els.push({
|
|
64
|
+
_el: el,
|
|
65
|
+
tag,
|
|
66
|
+
text,
|
|
67
|
+
attrs,
|
|
68
|
+
x: Math.round(rect.x + rect.width / 2),
|
|
69
|
+
y: Math.round(rect.y + rect.height / 2)
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Assign sequential IDs and store DOM refs for later direct focus
|
|
74
|
+
window.__jarvis_elements = els.map(e => e._el);
|
|
75
|
+
els.forEach((el, i) => { el.id = i + 1; delete el._el; });
|
|
76
|
+
|
|
77
|
+
// Get visible text, clean up whitespace
|
|
78
|
+
let bodyText = document.body.innerText || '';
|
|
79
|
+
bodyText = bodyText.replace(/\\n{3,}/g, '\\n\\n').trim().slice(0, 8000);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
title: document.title,
|
|
83
|
+
url: location.href,
|
|
84
|
+
text: bodyText,
|
|
85
|
+
elements: els
|
|
86
|
+
};
|
|
87
|
+
})()`;
|
|
88
|
+
|
|
89
|
+
export class BrowserController {
|
|
90
|
+
private cdp: CDPClient;
|
|
91
|
+
private port: number;
|
|
92
|
+
private profileDir: string | undefined;
|
|
93
|
+
private _connected = false;
|
|
94
|
+
private runningBrowser: RunningBrowser | null = null;
|
|
95
|
+
// Coordinates stored from last snapshot — not sent to LLM
|
|
96
|
+
private elementCoords = new Map<number, { x: number; y: number }>();
|
|
97
|
+
|
|
98
|
+
constructor(port: number = 9222, profileDir?: string) {
|
|
99
|
+
this.cdp = new CDPClient();
|
|
100
|
+
this.port = port;
|
|
101
|
+
this.profileDir = profileDir;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if Chrome CDP is already reachable on the debug port.
|
|
106
|
+
*/
|
|
107
|
+
async isAvailable(): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(`http://127.0.0.1:${this.port}/json/version`, {
|
|
110
|
+
signal: AbortSignal.timeout(2000),
|
|
111
|
+
});
|
|
112
|
+
return res.ok;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Connect to Chrome. If Chrome isn't running, auto-launches it
|
|
120
|
+
* with CDP enabled and an isolated profile. No user setup required.
|
|
121
|
+
*/
|
|
122
|
+
async connect(): Promise<void> {
|
|
123
|
+
if (this._connected) return;
|
|
124
|
+
|
|
125
|
+
// If Chrome isn't running, launch it automatically
|
|
126
|
+
if (!(await this.isAvailable())) {
|
|
127
|
+
console.log('[BrowserController] Chrome not detected, launching automatically...');
|
|
128
|
+
this.runningBrowser = await launchChrome(this.port, this.profileDir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Discover page targets
|
|
132
|
+
const listRes = await fetch(`http://127.0.0.1:${this.port}/json/list`);
|
|
133
|
+
if (!listRes.ok) {
|
|
134
|
+
throw new Error('Chrome CDP not reachable after launch');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const targets = await listRes.json() as Array<{
|
|
138
|
+
type: string;
|
|
139
|
+
webSocketDebuggerUrl: string;
|
|
140
|
+
}>;
|
|
141
|
+
|
|
142
|
+
let pageTarget = targets.find(t => t.type === 'page');
|
|
143
|
+
|
|
144
|
+
if (!pageTarget) {
|
|
145
|
+
// Create a new tab
|
|
146
|
+
const newRes = await fetch(`http://127.0.0.1:${this.port}/json/new?about:blank`);
|
|
147
|
+
pageTarget = await newRes.json() as any;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!pageTarget?.webSocketDebuggerUrl) {
|
|
151
|
+
throw new Error('No page target found and could not create one');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Connect CDP to the page
|
|
155
|
+
await this.cdp.connect(pageTarget.webSocketDebuggerUrl);
|
|
156
|
+
|
|
157
|
+
// Enable required CDP domains
|
|
158
|
+
await this.cdp.send('Page.enable');
|
|
159
|
+
await this.cdp.send('Runtime.enable');
|
|
160
|
+
await this.cdp.send('DOM.enable');
|
|
161
|
+
|
|
162
|
+
// Inject stealth scripts for all future navigations
|
|
163
|
+
await this.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
164
|
+
source: STEALTH_SCRIPT,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this._connected = true;
|
|
168
|
+
console.log('[BrowserController] Connected to Chrome');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Navigate to a URL and wait for the page to load.
|
|
173
|
+
*/
|
|
174
|
+
async navigate(url: string): Promise<PageSnapshot> {
|
|
175
|
+
await this.ensureConnected();
|
|
176
|
+
|
|
177
|
+
const loadPromise = this.cdp.waitForEvent('Page.loadEventFired', 30000);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await this.cdp.send('Page.navigate', { url });
|
|
181
|
+
} catch (err) {
|
|
182
|
+
// If navigate fails, suppress the dangling loadPromise timeout
|
|
183
|
+
loadPromise.catch(() => {});
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await loadPromise;
|
|
189
|
+
} catch {
|
|
190
|
+
// Page.loadEventFired timeout — page may still be usable (SPAs, slow loads)
|
|
191
|
+
console.warn(`[BrowserController] Page load timeout for ${url}, continuing anyway`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Wait for JS to settle
|
|
195
|
+
await Bun.sleep(800);
|
|
196
|
+
|
|
197
|
+
return this.snapshot();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get a snapshot of the current page: text content + numbered interactive elements.
|
|
202
|
+
*/
|
|
203
|
+
async snapshot(): Promise<PageSnapshot> {
|
|
204
|
+
await this.ensureConnected();
|
|
205
|
+
|
|
206
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
207
|
+
expression: SNAPSHOT_SCRIPT,
|
|
208
|
+
returnByValue: true,
|
|
209
|
+
awaitPromise: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (result.exceptionDetails) {
|
|
213
|
+
throw new Error(`Snapshot failed: ${JSON.stringify(result.exceptionDetails)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const data = result.result.value as PageSnapshot & {
|
|
217
|
+
elements: Array<PageElement & { x: number; y: number }>;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Store coordinates locally, strip from LLM-facing data
|
|
221
|
+
this.elementCoords.clear();
|
|
222
|
+
const cleanElements: PageElement[] = [];
|
|
223
|
+
|
|
224
|
+
for (const el of data.elements) {
|
|
225
|
+
this.elementCoords.set(el.id, { x: el.x, y: el.y });
|
|
226
|
+
cleanElements.push({
|
|
227
|
+
id: el.id,
|
|
228
|
+
tag: el.tag,
|
|
229
|
+
text: el.text,
|
|
230
|
+
attrs: el.attrs,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
title: data.title,
|
|
236
|
+
url: data.url,
|
|
237
|
+
text: data.text,
|
|
238
|
+
elements: cleanElements,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Click an element by its snapshot ID.
|
|
244
|
+
*/
|
|
245
|
+
async click(elementId: number): Promise<string> {
|
|
246
|
+
await this.ensureConnected();
|
|
247
|
+
|
|
248
|
+
const coords = this.elementCoords.get(elementId);
|
|
249
|
+
if (!coords) {
|
|
250
|
+
return `Error: Element [${elementId}] not found. Run browser_snapshot first.`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await this.cdp.send('Input.dispatchMouseEvent', {
|
|
254
|
+
type: 'mousePressed',
|
|
255
|
+
x: coords.x,
|
|
256
|
+
y: coords.y,
|
|
257
|
+
button: 'left',
|
|
258
|
+
clickCount: 1,
|
|
259
|
+
});
|
|
260
|
+
await this.cdp.send('Input.dispatchMouseEvent', {
|
|
261
|
+
type: 'mouseReleased',
|
|
262
|
+
x: coords.x,
|
|
263
|
+
y: coords.y,
|
|
264
|
+
button: 'left',
|
|
265
|
+
clickCount: 1,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Wait for navigation/changes
|
|
269
|
+
await Bun.sleep(1000);
|
|
270
|
+
|
|
271
|
+
return `Clicked element [${elementId}]`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Type text into an input element by its snapshot ID.
|
|
276
|
+
* Optionally press Enter after typing.
|
|
277
|
+
*
|
|
278
|
+
* Uses DOM focus + targeted value clearing instead of coordinate-click + Ctrl+A.
|
|
279
|
+
* This prevents misclicks from wiping the wrong field (e.g., typing subject
|
|
280
|
+
* text into the To field in Gmail's compact compose window).
|
|
281
|
+
*/
|
|
282
|
+
async type(elementId: number, text: string, submit: boolean = false): Promise<string> {
|
|
283
|
+
await this.ensureConnected();
|
|
284
|
+
|
|
285
|
+
const coords = this.elementCoords.get(elementId);
|
|
286
|
+
if (!coords) {
|
|
287
|
+
return `Error: Element [${elementId}] not found. Run browser_snapshot first.`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Focus the element via DOM (more reliable than coordinate click for typing)
|
|
291
|
+
const focusResult = await this.cdp.send('Runtime.evaluate', {
|
|
292
|
+
expression: `(() => {
|
|
293
|
+
const el = window.__jarvis_elements && window.__jarvis_elements[${elementId - 1}];
|
|
294
|
+
if (!el) return 'not_found';
|
|
295
|
+
el.focus();
|
|
296
|
+
// Clear existing content based on element type
|
|
297
|
+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') {
|
|
298
|
+
el.value = '';
|
|
299
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
300
|
+
} else if (el.getAttribute('contenteditable') === 'true' || el.getAttribute('role') === 'textbox') {
|
|
301
|
+
// For contenteditable divs, select all within this element only
|
|
302
|
+
const range = document.createRange();
|
|
303
|
+
range.selectNodeContents(el);
|
|
304
|
+
const sel = window.getSelection();
|
|
305
|
+
sel.removeAllRanges();
|
|
306
|
+
sel.addRange(range);
|
|
307
|
+
// Delete the selection
|
|
308
|
+
document.execCommand('delete', false, null);
|
|
309
|
+
}
|
|
310
|
+
return 'ok';
|
|
311
|
+
})()`,
|
|
312
|
+
returnByValue: true,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const focusStatus = focusResult?.result?.value;
|
|
316
|
+
if (focusStatus === 'not_found') {
|
|
317
|
+
// Fallback: coordinate-based click (element refs may have been lost on navigation)
|
|
318
|
+
const clickResult = await this.click(elementId);
|
|
319
|
+
if (clickResult.startsWith('Error:')) return clickResult;
|
|
320
|
+
await Bun.sleep(200);
|
|
321
|
+
// Use Ctrl+A as fallback clearing (old behavior)
|
|
322
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
323
|
+
type: 'keyDown', key: 'a', code: 'KeyA',
|
|
324
|
+
windowsVirtualKeyCode: 65, nativeVirtualKeyCode: 65, modifiers: 2,
|
|
325
|
+
});
|
|
326
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
327
|
+
type: 'keyUp', key: 'a', code: 'KeyA',
|
|
328
|
+
windowsVirtualKeyCode: 65, nativeVirtualKeyCode: 65, modifiers: 2,
|
|
329
|
+
});
|
|
330
|
+
} else {
|
|
331
|
+
await Bun.sleep(200);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Insert text (like paste — much more reliable than char-by-char)
|
|
335
|
+
await this.cdp.send('Input.insertText', { text });
|
|
336
|
+
|
|
337
|
+
let result = `Typed "${text}" into element [${elementId}]`;
|
|
338
|
+
|
|
339
|
+
if (submit) {
|
|
340
|
+
await Bun.sleep(100);
|
|
341
|
+
await this.pressEnter();
|
|
342
|
+
// Wait for page load after submit
|
|
343
|
+
await Bun.sleep(2000);
|
|
344
|
+
result += ' and pressed Enter';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Press Enter key.
|
|
352
|
+
*/
|
|
353
|
+
async pressEnter(): Promise<void> {
|
|
354
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
355
|
+
type: 'rawKeyDown',
|
|
356
|
+
key: 'Enter',
|
|
357
|
+
code: 'Enter',
|
|
358
|
+
windowsVirtualKeyCode: 13,
|
|
359
|
+
nativeVirtualKeyCode: 13,
|
|
360
|
+
});
|
|
361
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
362
|
+
type: 'char',
|
|
363
|
+
key: 'Enter',
|
|
364
|
+
code: 'Enter',
|
|
365
|
+
windowsVirtualKeyCode: 13,
|
|
366
|
+
nativeVirtualKeyCode: 13,
|
|
367
|
+
});
|
|
368
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
369
|
+
type: 'keyUp',
|
|
370
|
+
key: 'Enter',
|
|
371
|
+
code: 'Enter',
|
|
372
|
+
windowsVirtualKeyCode: 13,
|
|
373
|
+
nativeVirtualKeyCode: 13,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Scroll the page up or down.
|
|
379
|
+
* direction: 'down' or 'up'
|
|
380
|
+
* amount: pixels to scroll (default: one viewport height)
|
|
381
|
+
*/
|
|
382
|
+
async scroll(direction: 'up' | 'down' = 'down', amount?: number): Promise<string> {
|
|
383
|
+
await this.ensureConnected();
|
|
384
|
+
|
|
385
|
+
const viewportHeight = (await this.evaluate('window.innerHeight') as number) || 600;
|
|
386
|
+
const scrollAmount = amount ?? viewportHeight;
|
|
387
|
+
|
|
388
|
+
const pixels = direction === 'down' ? scrollAmount : -scrollAmount;
|
|
389
|
+
|
|
390
|
+
await this.evaluate(`window.scrollBy(0, ${pixels})`);
|
|
391
|
+
await Bun.sleep(500); // Wait for lazy-loaded content
|
|
392
|
+
|
|
393
|
+
return `Scrolled ${direction} by ${scrollAmount}px`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Upload a file to a <input type="file"> element on the page.
|
|
398
|
+
* Uses CDP DOM.setFileInputFiles to bypass the native file picker.
|
|
399
|
+
* If no selector is provided, finds the first visible file input.
|
|
400
|
+
*/
|
|
401
|
+
async uploadFile(filePath: string, selector?: string): Promise<string> {
|
|
402
|
+
await this.ensureConnected();
|
|
403
|
+
|
|
404
|
+
// Resolve the file input element
|
|
405
|
+
const query = selector || 'input[type="file"]';
|
|
406
|
+
const doc = await this.cdp.send('DOM.getDocument');
|
|
407
|
+
const node = await this.cdp.send('DOM.querySelector', {
|
|
408
|
+
nodeId: doc.root.nodeId,
|
|
409
|
+
selector: query,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (!node.nodeId) {
|
|
413
|
+
return `Error: No file input found matching "${query}". Click the upload/attach button first to trigger the file input.`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Set the file on the input element via CDP
|
|
417
|
+
try {
|
|
418
|
+
await this.cdp.send('DOM.setFileInputFiles', {
|
|
419
|
+
files: [filePath],
|
|
420
|
+
nodeId: node.nodeId,
|
|
421
|
+
});
|
|
422
|
+
} catch (err) {
|
|
423
|
+
return `Error setting file: ${err instanceof Error ? err.message : String(err)}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await Bun.sleep(1000); // Wait for the app to process the file
|
|
427
|
+
|
|
428
|
+
return `Uploaded file "${filePath}" to file input`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Take a screenshot and save to a file.
|
|
433
|
+
*/
|
|
434
|
+
async screenshot(filePath: string = '/tmp/jarvis-screenshot.png'): Promise<string> {
|
|
435
|
+
await this.ensureConnected();
|
|
436
|
+
|
|
437
|
+
const result = await this.cdp.send('Page.captureScreenshot', { format: 'png' });
|
|
438
|
+
const buffer = Buffer.from(result.data, 'base64');
|
|
439
|
+
|
|
440
|
+
await Bun.write(filePath, buffer);
|
|
441
|
+
return filePath;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Take a screenshot and return raw base64 data (for vision/LLM).
|
|
446
|
+
*/
|
|
447
|
+
async screenshotBuffer(): Promise<{ base64: string; mimeType: string }> {
|
|
448
|
+
await this.ensureConnected();
|
|
449
|
+
const result = await this.cdp.send('Page.captureScreenshot', { format: 'png' });
|
|
450
|
+
return { base64: result.data, mimeType: 'image/png' };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Evaluate arbitrary JavaScript in the page context.
|
|
455
|
+
*/
|
|
456
|
+
async evaluate(expression: string): Promise<unknown> {
|
|
457
|
+
await this.ensureConnected();
|
|
458
|
+
|
|
459
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
460
|
+
expression,
|
|
461
|
+
returnByValue: true,
|
|
462
|
+
awaitPromise: true,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (result.exceptionDetails) {
|
|
466
|
+
throw new Error(`JS error: ${JSON.stringify(result.exceptionDetails)}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return result.result.value;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Disconnect from Chrome. If we auto-launched Chrome, stop it too.
|
|
474
|
+
*/
|
|
475
|
+
async disconnect(): Promise<void> {
|
|
476
|
+
if (this._connected) {
|
|
477
|
+
await this.cdp.close();
|
|
478
|
+
this._connected = false;
|
|
479
|
+
this.elementCoords.clear();
|
|
480
|
+
console.log('[BrowserController] Disconnected');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Stop the Chrome process we launched (if any)
|
|
484
|
+
if (this.runningBrowser) {
|
|
485
|
+
await stopChrome(this.runningBrowser);
|
|
486
|
+
this.runningBrowser = null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
get connected(): boolean {
|
|
491
|
+
return this._connected;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private async ensureConnected(): Promise<void> {
|
|
495
|
+
if (this._connected && !this.cdp.isOpen) {
|
|
496
|
+
// Connection went stale — reset and reconnect
|
|
497
|
+
console.warn('[BrowserController] CDP connection stale, reconnecting...');
|
|
498
|
+
this._connected = false;
|
|
499
|
+
this.elementCoords.clear();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!this._connected) {
|
|
503
|
+
await this.connect();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
@@ -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';
|