@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,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop Controller — Windows Desktop Automation via Sidecar
|
|
3
|
+
*
|
|
4
|
+
* TCP JSON-RPC client that communicates with the desktop-bridge.exe sidecar.
|
|
5
|
+
* Mirrors the BrowserController pattern: lazy connection, auto-launch,
|
|
6
|
+
* snapshot with numbered element IDs, element cache for click/type by ID.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createConnection, type Socket } from 'node:net';
|
|
10
|
+
import { writeFileSync } from 'node:fs';
|
|
11
|
+
import type { AppController, WindowInfo, UIElement } from './interface.ts';
|
|
12
|
+
import { launchSidecar, stopSidecar, isSidecarRunning, type RunningSidecar } from './sidecar-launcher.ts';
|
|
13
|
+
|
|
14
|
+
export type DesktopSnapshot = {
|
|
15
|
+
window: { pid: number; title: string; className: string };
|
|
16
|
+
elements: FlatElement[];
|
|
17
|
+
totalElements: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type FlatElement = {
|
|
21
|
+
id: number;
|
|
22
|
+
role: string;
|
|
23
|
+
name: string;
|
|
24
|
+
value: string | null;
|
|
25
|
+
depth: number;
|
|
26
|
+
isEnabled: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_PORT = 9224;
|
|
30
|
+
const MAX_SNAPSHOT_ELEMENTS = 60;
|
|
31
|
+
|
|
32
|
+
export class DesktopController implements AppController {
|
|
33
|
+
private port: number;
|
|
34
|
+
private host: string = 'localhost';
|
|
35
|
+
private socket: Socket | null = null;
|
|
36
|
+
private _connected = false;
|
|
37
|
+
private runningSidecar: RunningSidecar | null = null;
|
|
38
|
+
private nextRequestId = 1;
|
|
39
|
+
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
40
|
+
private buffer = '';
|
|
41
|
+
|
|
42
|
+
// Element cache from last snapshot (like BrowserController.elementCoords)
|
|
43
|
+
private elementCache = new Map<number, UIElement>();
|
|
44
|
+
private lastSnapshotWindow: { pid: number; title: string } | null = null;
|
|
45
|
+
|
|
46
|
+
constructor(port: number = DEFAULT_PORT) {
|
|
47
|
+
this.port = port;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Connection lifecycle ---
|
|
51
|
+
|
|
52
|
+
async connect(): Promise<void> {
|
|
53
|
+
if (this._connected) return;
|
|
54
|
+
|
|
55
|
+
// Check if sidecar is already running
|
|
56
|
+
if (!(await isSidecarRunning(this.port))) {
|
|
57
|
+
console.log('[DesktopController] Sidecar not running, launching...');
|
|
58
|
+
this.runningSidecar = await launchSidecar(this.port);
|
|
59
|
+
this.host = this.runningSidecar.host;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Open TCP socket
|
|
63
|
+
await this.openSocket();
|
|
64
|
+
this._connected = true;
|
|
65
|
+
console.log(`[DesktopController] Connected to sidecar on ${this.host}:${this.port}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async disconnect(): Promise<void> {
|
|
69
|
+
if (this.socket) {
|
|
70
|
+
this.socket.destroy();
|
|
71
|
+
this.socket = null;
|
|
72
|
+
}
|
|
73
|
+
this._connected = false;
|
|
74
|
+
this.elementCache.clear();
|
|
75
|
+
this.pending.clear();
|
|
76
|
+
this.buffer = '';
|
|
77
|
+
|
|
78
|
+
if (this.runningSidecar) {
|
|
79
|
+
await stopSidecar(this.runningSidecar);
|
|
80
|
+
this.runningSidecar = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log('[DesktopController] Disconnected');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get connected(): boolean {
|
|
87
|
+
return this._connected;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- AppController interface ---
|
|
91
|
+
|
|
92
|
+
async getActiveWindow(): Promise<WindowInfo> {
|
|
93
|
+
await this.ensureConnected();
|
|
94
|
+
const result = await this.send('getActiveWindow') as any;
|
|
95
|
+
if (!result) throw new Error('No active window found');
|
|
96
|
+
return this.toWindowInfo(result);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async listWindows(): Promise<WindowInfo[]> {
|
|
100
|
+
await this.ensureConnected();
|
|
101
|
+
const result = await this.send('listWindows') as any[];
|
|
102
|
+
return result.map((w) => this.toWindowInfo(w));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async getWindowTree(pid: number): Promise<UIElement[]> {
|
|
106
|
+
await this.ensureConnected();
|
|
107
|
+
const result = await this.send('getWindowTree', { pid, depth: 5 }) as any;
|
|
108
|
+
return this.parseElements(result.elements || []);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async clickElement(element: UIElement): Promise<void> {
|
|
112
|
+
await this.ensureConnected();
|
|
113
|
+
const id = parseInt(element.id, 10);
|
|
114
|
+
await this.send('clickElement', { elementId: id });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async typeText(text: string): Promise<void> {
|
|
118
|
+
await this.ensureConnected();
|
|
119
|
+
await this.send('typeText', { text });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async pressKeys(keys: string[]): Promise<void> {
|
|
123
|
+
await this.ensureConnected();
|
|
124
|
+
await this.send('pressKeys', { keys });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async captureScreen(): Promise<Buffer> {
|
|
128
|
+
await this.ensureConnected();
|
|
129
|
+
const base64 = await this.send('captureScreen') as string;
|
|
130
|
+
return Buffer.from(base64, 'base64');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async captureWindow(pid: number): Promise<Buffer> {
|
|
134
|
+
await this.ensureConnected();
|
|
135
|
+
const base64 = await this.send('captureWindow', { pid }) as string;
|
|
136
|
+
return Buffer.from(base64, 'base64');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async focusWindow(pid: number): Promise<void> {
|
|
140
|
+
await this.ensureConnected();
|
|
141
|
+
await this.send('focusWindow', { pid });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Extended methods (snapshot-based, like BrowserController) ---
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get a snapshot of a window's UI elements with sequential [id]s.
|
|
148
|
+
* If no PID given, snapshots the active window.
|
|
149
|
+
*/
|
|
150
|
+
async snapshot(pid?: number): Promise<DesktopSnapshot> {
|
|
151
|
+
await this.ensureConnected();
|
|
152
|
+
|
|
153
|
+
// Get target window
|
|
154
|
+
let targetPid = pid;
|
|
155
|
+
if (!targetPid) {
|
|
156
|
+
const active = await this.send('getActiveWindow') as any;
|
|
157
|
+
if (!active) throw new Error('No active window');
|
|
158
|
+
targetPid = active.pid;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Get UI tree
|
|
162
|
+
const result = await this.send('getWindowTree', { pid: targetPid, depth: 5 }) as any;
|
|
163
|
+
|
|
164
|
+
// Flatten tree into sequential IDs
|
|
165
|
+
this.elementCache.clear();
|
|
166
|
+
const flatElements: FlatElement[] = [];
|
|
167
|
+
this.flattenTree(result.elements || [], 0, flatElements);
|
|
168
|
+
|
|
169
|
+
this.lastSnapshotWindow = {
|
|
170
|
+
pid: targetPid!,
|
|
171
|
+
title: result.window?.title || '',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
window: result.window || { pid: targetPid, title: '', className: '' },
|
|
176
|
+
elements: flatElements.slice(0, MAX_SNAPSHOT_ELEMENTS),
|
|
177
|
+
totalElements: flatElements.length,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Click a UI element by its snapshot [id].
|
|
183
|
+
*/
|
|
184
|
+
async clickById(elementId: number): Promise<string> {
|
|
185
|
+
await this.ensureConnected();
|
|
186
|
+
|
|
187
|
+
const element = this.elementCache.get(elementId);
|
|
188
|
+
if (!element) {
|
|
189
|
+
return `Error: Element [${elementId}] not found. Run desktop_snapshot first.`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await this.send('clickElement', { elementId });
|
|
193
|
+
|
|
194
|
+
const label = element.name ? `"${element.name}"` : element.role;
|
|
195
|
+
return `Clicked [${element.role}] ${label} (id: ${elementId})`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Type text, optionally clicking an element first.
|
|
200
|
+
*/
|
|
201
|
+
async typeById(elementId: number | undefined, text: string): Promise<string> {
|
|
202
|
+
await this.ensureConnected();
|
|
203
|
+
|
|
204
|
+
if (elementId !== undefined) {
|
|
205
|
+
const clickResult = await this.clickById(elementId);
|
|
206
|
+
if (clickResult.startsWith('Error:')) return clickResult;
|
|
207
|
+
// Brief pause after click
|
|
208
|
+
await Bun.sleep(200);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await this.send('typeText', { text });
|
|
212
|
+
|
|
213
|
+
const targetStr = elementId !== undefined ? ` into element [${elementId}]` : '';
|
|
214
|
+
return `Typed "${text}"${targetStr}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Launch a Windows application.
|
|
219
|
+
*/
|
|
220
|
+
async launchApp(executable: string, args?: string): Promise<object> {
|
|
221
|
+
await this.ensureConnected();
|
|
222
|
+
return await this.send('launchApp', { executable, args: args || '' }) as object;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Close a window by PID.
|
|
227
|
+
*/
|
|
228
|
+
async closeWindow(pid: number): Promise<void> {
|
|
229
|
+
await this.ensureConnected();
|
|
230
|
+
await this.send('closeWindow', { pid });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Drag one element onto another.
|
|
235
|
+
*/
|
|
236
|
+
async dragElement(fromId: UIElement | number, toId: UIElement | number): Promise<void> {
|
|
237
|
+
await this.ensureConnected();
|
|
238
|
+
|
|
239
|
+
const fromNum = typeof fromId === 'number' ? fromId : parseInt(fromId.id, 10);
|
|
240
|
+
const toNum = typeof toId === 'number' ? toId : parseInt(toId.id, 10);
|
|
241
|
+
|
|
242
|
+
await this.send('dragElement', { fromId: fromNum, toId: toNum });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Take a desktop screenshot and save to file.
|
|
247
|
+
*/
|
|
248
|
+
async screenshotToFile(pid?: number, filePath: string = '/tmp/jarvis-desktop-screenshot.png'): Promise<string> {
|
|
249
|
+
const buffer = pid ? await this.captureWindow(pid) : await this.captureScreen();
|
|
250
|
+
writeFileSync(filePath, buffer);
|
|
251
|
+
return filePath;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Take a desktop screenshot and return raw base64 data (for vision/LLM).
|
|
256
|
+
*/
|
|
257
|
+
async screenshotBase64(pid?: number): Promise<{ base64: string; mimeType: string }> {
|
|
258
|
+
await this.ensureConnected();
|
|
259
|
+
const method = pid ? 'captureWindow' : 'captureScreen';
|
|
260
|
+
const base64 = await this.send(method, pid ? { pid } : {}) as string;
|
|
261
|
+
return { base64, mimeType: 'image/png' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- Private helpers ---
|
|
265
|
+
|
|
266
|
+
private async ensureConnected(): Promise<void> {
|
|
267
|
+
if (this._connected && this.socket && !this.socket.destroyed) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Connection went stale
|
|
272
|
+
if (this._connected) {
|
|
273
|
+
console.warn('[DesktopController] Connection stale, reconnecting...');
|
|
274
|
+
this._connected = false;
|
|
275
|
+
this.elementCache.clear();
|
|
276
|
+
this.pending.clear();
|
|
277
|
+
this.buffer = '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await this.connect();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private openSocket(): Promise<void> {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
const socket = createConnection({ host: this.host, port: this.port }, () => {
|
|
286
|
+
this.socket = socket;
|
|
287
|
+
resolve();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
socket.setEncoding('utf-8');
|
|
291
|
+
|
|
292
|
+
socket.on('data', (chunk: string) => {
|
|
293
|
+
this.buffer += chunk;
|
|
294
|
+
this.processBuffer();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
socket.on('error', (err) => {
|
|
298
|
+
if (!this._connected) {
|
|
299
|
+
reject(err);
|
|
300
|
+
} else {
|
|
301
|
+
console.error('[DesktopController] Socket error:', err.message);
|
|
302
|
+
this._connected = false;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
socket.on('close', () => {
|
|
307
|
+
this._connected = false;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Timeout for initial connection
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
if (!this._connected && !this.socket) {
|
|
313
|
+
socket.destroy();
|
|
314
|
+
reject(new Error(`Failed to connect to sidecar on ${this.host}:${this.port}`));
|
|
315
|
+
}
|
|
316
|
+
}, 5000);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private processBuffer(): void {
|
|
321
|
+
const lines = this.buffer.split('\n');
|
|
322
|
+
// Keep incomplete last line in buffer
|
|
323
|
+
this.buffer = lines.pop() || '';
|
|
324
|
+
|
|
325
|
+
for (let line of lines) {
|
|
326
|
+
line = line.trim();
|
|
327
|
+
if (!line) continue;
|
|
328
|
+
|
|
329
|
+
// Strip UTF-8 BOM if present (C# can emit it)
|
|
330
|
+
if (line.charCodeAt(0) === 0xFEFF) {
|
|
331
|
+
line = line.slice(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const response = JSON.parse(line);
|
|
336
|
+
const id = response.id;
|
|
337
|
+
const p = this.pending.get(id);
|
|
338
|
+
if (p) {
|
|
339
|
+
this.pending.delete(id);
|
|
340
|
+
if (response.error) {
|
|
341
|
+
p.reject(new Error(response.error.message || 'Unknown error'));
|
|
342
|
+
} else {
|
|
343
|
+
p.resolve(response.result);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
console.warn('[DesktopController] Invalid JSON from sidecar:', line.slice(0, 100));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private send(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
if (!this.socket || this.socket.destroyed) {
|
|
355
|
+
reject(new Error('Not connected to sidecar'));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const id = this.nextRequestId++;
|
|
360
|
+
const request = JSON.stringify({
|
|
361
|
+
jsonrpc: '2.0',
|
|
362
|
+
method,
|
|
363
|
+
params: params || {},
|
|
364
|
+
id,
|
|
365
|
+
}) + '\n';
|
|
366
|
+
|
|
367
|
+
this.pending.set(id, { resolve, reject });
|
|
368
|
+
|
|
369
|
+
this.socket.write(request, (err) => {
|
|
370
|
+
if (err) {
|
|
371
|
+
this.pending.delete(id);
|
|
372
|
+
reject(err);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Timeout per request
|
|
377
|
+
setTimeout(() => {
|
|
378
|
+
if (this.pending.has(id)) {
|
|
379
|
+
this.pending.delete(id);
|
|
380
|
+
reject(new Error(`Timeout waiting for response to ${method} (id: ${id})`));
|
|
381
|
+
}
|
|
382
|
+
}, 30_000);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private flattenTree(elements: any[], depth: number, result: FlatElement[]): void {
|
|
387
|
+
for (const el of elements) {
|
|
388
|
+
const id = el.id;
|
|
389
|
+
const uiElement: UIElement = {
|
|
390
|
+
id: String(id),
|
|
391
|
+
role: el.role || '',
|
|
392
|
+
name: el.name || '',
|
|
393
|
+
value: el.value || null,
|
|
394
|
+
bounds: el.bounds || { x: 0, y: 0, width: 0, height: 0 },
|
|
395
|
+
children: [],
|
|
396
|
+
properties: el.properties || {},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
this.elementCache.set(id, uiElement);
|
|
400
|
+
|
|
401
|
+
result.push({
|
|
402
|
+
id,
|
|
403
|
+
role: el.role || '',
|
|
404
|
+
name: el.name || '',
|
|
405
|
+
value: el.value || null,
|
|
406
|
+
depth,
|
|
407
|
+
isEnabled: el.isEnabled !== false,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Recurse into children
|
|
411
|
+
if (el.children && el.children.length > 0) {
|
|
412
|
+
this.flattenTree(el.children, depth + 1, result);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private toWindowInfo(raw: any): WindowInfo {
|
|
418
|
+
return {
|
|
419
|
+
pid: raw.pid || 0,
|
|
420
|
+
title: raw.title || '',
|
|
421
|
+
className: raw.className || '',
|
|
422
|
+
bounds: raw.bounds || { x: 0, y: 0, width: 0, height: 0 },
|
|
423
|
+
focused: raw.focused || false,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private parseElements(raw: any[]): UIElement[] {
|
|
428
|
+
return raw.map((el) => ({
|
|
429
|
+
id: String(el.id),
|
|
430
|
+
role: el.role || '',
|
|
431
|
+
name: el.name || '',
|
|
432
|
+
value: el.value || null,
|
|
433
|
+
bounds: el.bounds || { x: 0, y: 0, width: 0, height: 0 },
|
|
434
|
+
children: el.children ? this.parseElements(el.children) : [],
|
|
435
|
+
properties: el.properties || {},
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type WindowInfo = {
|
|
2
|
+
pid: number;
|
|
3
|
+
title: string;
|
|
4
|
+
className: string;
|
|
5
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
6
|
+
focused: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type UIElement = {
|
|
10
|
+
id: string;
|
|
11
|
+
role: string;
|
|
12
|
+
name: string;
|
|
13
|
+
value: string | null;
|
|
14
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
15
|
+
children: UIElement[];
|
|
16
|
+
properties: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface AppController {
|
|
20
|
+
getActiveWindow(): Promise<WindowInfo>;
|
|
21
|
+
getWindowTree(pid: number): Promise<UIElement[]>;
|
|
22
|
+
listWindows(): Promise<WindowInfo[]>;
|
|
23
|
+
|
|
24
|
+
clickElement(element: UIElement): Promise<void>;
|
|
25
|
+
typeText(text: string): Promise<void>;
|
|
26
|
+
pressKeys(keys: string[]): Promise<void>;
|
|
27
|
+
|
|
28
|
+
captureScreen(): Promise<Buffer>;
|
|
29
|
+
captureWindow(pid: number): Promise<Buffer>;
|
|
30
|
+
|
|
31
|
+
focusWindow(pid: number): Promise<void>;
|
|
32
|
+
|
|
33
|
+
// Optional extended operations
|
|
34
|
+
launchApp?(executable: string, args?: string): Promise<object>;
|
|
35
|
+
closeWindow?(pid: number): Promise<void>;
|
|
36
|
+
dragElement?(from: UIElement, to: UIElement): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getAppController(): AppController {
|
|
40
|
+
const platform = process.platform;
|
|
41
|
+
|
|
42
|
+
switch (platform) {
|
|
43
|
+
case 'linux': {
|
|
44
|
+
// In WSL2, use DesktopController to control Windows desktop via sidecar
|
|
45
|
+
const { WSLBridge } = require('../terminal/wsl-bridge.ts');
|
|
46
|
+
if (WSLBridge.isWSL()) {
|
|
47
|
+
const { DesktopController } = require('./desktop-controller.ts');
|
|
48
|
+
return new DesktopController();
|
|
49
|
+
}
|
|
50
|
+
const { LinuxAppController } = require('./linux.ts');
|
|
51
|
+
return new LinuxAppController();
|
|
52
|
+
}
|
|
53
|
+
case 'win32': {
|
|
54
|
+
const { WindowsAppController } = require('./windows.ts');
|
|
55
|
+
return new WindowsAppController();
|
|
56
|
+
}
|
|
57
|
+
case 'darwin': {
|
|
58
|
+
const { MacAppController } = require('./macos.ts');
|
|
59
|
+
return new MacAppController();
|
|
60
|
+
}
|
|
61
|
+
default:
|
|
62
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
63
|
+
}
|
|
64
|
+
}
|