screenhand 0.1.1 → 0.3.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/README.md +193 -109
- package/bin/darwin-arm64/macos-bridge +0 -0
- package/dist/mcp-desktop.js +5876 -0
- package/dist/scripts/codex-monitor-daemon.js +335 -0
- package/dist/scripts/export-help-center.js +112 -0
- package/dist/scripts/marketing-loop.js +117 -0
- package/dist/scripts/observer-daemon.js +288 -0
- package/dist/scripts/orchestrator-daemon.js +399 -0
- package/dist/scripts/supervisor-daemon.js +272 -0
- package/dist/scripts/threads-campaign.js +208 -0
- package/dist/scripts/worker-daemon.js +228 -0
- package/dist/src/agent/cli.js +82 -0
- package/dist/src/agent/loop.js +274 -0
- package/dist/src/community/fetcher.js +109 -0
- package/dist/src/community/index.js +6 -0
- package/dist/src/community/publisher.js +191 -0
- package/dist/src/community/remote-api.js +121 -0
- package/dist/src/community/types.js +3 -0
- package/dist/src/community/validator.js +95 -0
- package/{src/config.ts → dist/src/config.js} +5 -10
- package/dist/src/context-tracker.js +489 -0
- package/{src/index.ts → dist/src/index.js} +32 -52
- package/dist/src/ingestion/coverage-auditor.js +233 -0
- package/dist/src/ingestion/doc-parser.js +164 -0
- package/dist/src/ingestion/index.js +8 -0
- package/dist/src/ingestion/menu-scanner.js +152 -0
- package/dist/src/ingestion/reference-merger.js +186 -0
- package/dist/src/ingestion/shortcut-extractor.js +180 -0
- package/dist/src/ingestion/tutorial-extractor.js +170 -0
- package/dist/src/ingestion/types.js +3 -0
- package/dist/src/jobs/manager.js +305 -0
- package/dist/src/jobs/runner.js +806 -0
- package/dist/src/jobs/store.js +102 -0
- package/dist/src/jobs/types.js +30 -0
- package/dist/src/jobs/worker.js +97 -0
- package/dist/src/learning/engine.js +356 -0
- package/dist/src/learning/index.js +9 -0
- package/dist/src/learning/locator-policy.js +120 -0
- package/dist/src/learning/pattern-policy.js +89 -0
- package/dist/src/learning/recovery-policy.js +116 -0
- package/dist/src/learning/sensor-policy.js +115 -0
- package/dist/src/learning/timing-model.js +204 -0
- package/dist/src/learning/topology-policy.js +90 -0
- package/dist/src/learning/types.js +9 -0
- package/dist/src/logging/timeline-logger.js +48 -0
- package/dist/src/mcp/mcp-stdio-server.js +464 -0
- package/dist/src/mcp/server.js +363 -0
- package/dist/src/mcp-entry.js +60 -0
- package/dist/src/memory/playbook-seeds.js +200 -0
- package/dist/src/memory/recall.js +222 -0
- package/dist/src/memory/research.js +104 -0
- package/dist/src/memory/seeds.js +101 -0
- package/dist/src/memory/service.js +446 -0
- package/dist/src/memory/session.js +169 -0
- package/dist/src/memory/store.js +451 -0
- package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
- package/dist/src/monitor/codex-monitor.js +382 -0
- package/dist/src/monitor/task-queue.js +97 -0
- package/dist/src/monitor/types.js +62 -0
- package/dist/src/native/bridge-client.js +412 -0
- package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
- package/dist/src/observer/state.js +199 -0
- package/dist/src/observer/types.js +43 -0
- package/dist/src/orchestrator/state.js +68 -0
- package/dist/src/orchestrator/types.js +22 -0
- package/dist/src/perception/ax-source.js +162 -0
- package/dist/src/perception/cdp-source.js +162 -0
- package/dist/src/perception/coordinator.js +771 -0
- package/dist/src/perception/frame-differ.js +287 -0
- package/dist/src/perception/index.js +22 -0
- package/dist/src/perception/manager.js +199 -0
- package/dist/src/perception/types.js +47 -0
- package/dist/src/perception/vision-source.js +399 -0
- package/dist/src/planner/deterministic.js +298 -0
- package/dist/src/planner/executor.js +870 -0
- package/dist/src/planner/goal-store.js +92 -0
- package/dist/src/planner/index.js +21 -0
- package/dist/src/planner/planner.js +520 -0
- package/dist/src/planner/tool-registry.js +71 -0
- package/dist/src/planner/types.js +22 -0
- package/dist/src/platform/explorer.js +213 -0
- package/dist/src/platform/help-center-markdown.js +527 -0
- package/dist/src/platform/learner.js +257 -0
- package/dist/src/playbook/engine.js +486 -0
- package/dist/src/playbook/index.js +20 -0
- package/dist/src/playbook/mcp-recorder.js +204 -0
- package/dist/src/playbook/recorder.js +536 -0
- package/dist/src/playbook/runner.js +408 -0
- package/dist/src/playbook/store.js +312 -0
- package/dist/src/playbook/types.js +17 -0
- package/dist/src/recovery/detectors.js +156 -0
- package/dist/src/recovery/engine.js +327 -0
- package/dist/src/recovery/index.js +20 -0
- package/dist/src/recovery/strategies.js +274 -0
- package/dist/src/recovery/types.js +20 -0
- package/dist/src/runtime/accessibility-adapter.js +430 -0
- package/dist/src/runtime/app-adapter.js +64 -0
- package/dist/src/runtime/applescript-adapter.js +305 -0
- package/dist/src/runtime/ax-role-map.js +96 -0
- package/dist/src/runtime/browser-adapter.js +52 -0
- package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
- package/dist/src/runtime/composite-adapter.js +221 -0
- package/dist/src/runtime/execution-contract.js +159 -0
- package/dist/src/runtime/executor.js +286 -0
- package/dist/src/runtime/locator-cache.js +50 -0
- package/dist/src/runtime/planning-loop.js +63 -0
- package/dist/src/runtime/service.js +432 -0
- package/dist/src/runtime/session-manager.js +63 -0
- package/dist/src/runtime/state-observer.js +121 -0
- package/dist/src/runtime/vision-adapter.js +225 -0
- package/dist/src/state/app-map-types.js +72 -0
- package/dist/src/state/app-map.js +1974 -0
- package/dist/src/state/entity-tracker.js +108 -0
- package/dist/src/state/fusion.js +96 -0
- package/dist/src/state/index.js +21 -0
- package/dist/src/state/ladder-generator.js +236 -0
- package/dist/src/state/persistence.js +156 -0
- package/dist/src/state/types.js +17 -0
- package/dist/src/state/world-model.js +1456 -0
- package/dist/src/supervisor/locks.js +186 -0
- package/dist/src/supervisor/supervisor.js +403 -0
- package/dist/src/supervisor/types.js +30 -0
- package/dist/src/test-mcp-protocol.js +154 -0
- package/dist/src/types.js +17 -0
- package/dist/src/util/atomic-write.js +133 -0
- package/dist/src/util/sanitize.js +146 -0
- package/dist-app-maps/com.figma.Desktop.json +959 -0
- package/dist-app-maps/com.hnc.Discord.json +1146 -0
- package/dist-app-maps/notion.id.json +2831 -0
- package/dist-playbooks/canva-screenhand-carousel.json +445 -0
- package/dist-playbooks/codex-desktop.json +76 -0
- package/dist-playbooks/competitor-research-stack.json +122 -0
- package/dist-playbooks/davinci-color-grade.json +153 -0
- package/dist-playbooks/davinci-edit-timeline.json +162 -0
- package/dist-playbooks/davinci-render.json +114 -0
- package/dist-playbooks/devto.json +52 -0
- package/dist-playbooks/discord.json +41 -0
- package/dist-playbooks/google-flow-create-project.json +59 -0
- package/dist-playbooks/google-flow-edit-image.json +90 -0
- package/dist-playbooks/google-flow-edit-video.json +90 -0
- package/dist-playbooks/google-flow-generate-image.json +68 -0
- package/dist-playbooks/google-flow-generate-video.json +191 -0
- package/dist-playbooks/google-flow-open-project.json +48 -0
- package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
- package/dist-playbooks/google-flow-search-assets.json +64 -0
- package/dist-playbooks/instagram.json +57 -0
- package/dist-playbooks/linkedin.json +52 -0
- package/dist-playbooks/n8n.json +43 -0
- package/dist-playbooks/reddit.json +52 -0
- package/dist-playbooks/threads.json +59 -0
- package/dist-playbooks/x-twitter.json +59 -0
- package/dist-playbooks/youtube.json +59 -0
- package/dist-references/canva.json +646 -0
- package/dist-references/codex-desktop.json +305 -0
- package/dist-references/davinci-resolve-keyboard.json +594 -0
- package/dist-references/davinci-resolve-menu-map.json +1139 -0
- package/dist-references/davinci-resolve-menus-batch1.json +116 -0
- package/dist-references/davinci-resolve-menus-batch2.json +372 -0
- package/dist-references/davinci-resolve-menus-batch3.json +330 -0
- package/dist-references/davinci-resolve-menus-batch4.json +297 -0
- package/dist-references/davinci-resolve-shortcuts.json +333 -0
- package/dist-references/devto.json +317 -0
- package/dist-references/discord.json +549 -0
- package/dist-references/figma.json +1186 -0
- package/dist-references/finder.json +146 -0
- package/dist-references/google-ads-transparency.json +95 -0
- package/dist-references/google-flow.json +649 -0
- package/dist-references/instagram.json +341 -0
- package/dist-references/linkedin.json +324 -0
- package/dist-references/meta-ad-library.json +86 -0
- package/dist-references/n8n.json +387 -0
- package/dist-references/notes.json +27 -0
- package/dist-references/notion.json +163 -0
- package/dist-references/reddit.json +341 -0
- package/dist-references/threads.json +337 -0
- package/dist-references/x-twitter.json +403 -0
- package/dist-references/youtube.json +373 -0
- package/native/macos-bridge/Package.swift +1 -0
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
- package/native/macos-bridge/Sources/AppManagement.swift +212 -2
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
- package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
- package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
- package/native/macos-bridge/Sources/main.swift +169 -16
- package/native/windows-bridge/Program.cs +5 -0
- package/native/windows-bridge/ScreenCapture.cs +124 -0
- package/package.json +29 -4
- package/scripts/postinstall.cjs +127 -0
- package/.claude/commands/automate.md +0 -28
- package/.claude/commands/debug-ui.md +0 -19
- package/.claude/commands/screenshot.md +0 -15
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.mcp.json +0 -8
- package/DESKTOP_MCP_GUIDE.md +0 -92
- package/SECURITY.md +0 -44
- package/docs/architecture.md +0 -47
- package/install-skills.sh +0 -19
- package/mcp-bridge.ts +0 -271
- package/mcp-desktop.ts +0 -1221
- package/playbooks/instagram.json +0 -41
- package/playbooks/instagram_v2.json +0 -201
- package/playbooks/x_v1.json +0 -211
- package/scripts/devpost-live-loop.mjs +0 -421
- package/src/logging/timeline-logger.ts +0 -55
- package/src/mcp/server.ts +0 -449
- package/src/memory/recall.ts +0 -191
- package/src/memory/research.ts +0 -146
- package/src/memory/seeds.ts +0 -123
- package/src/memory/session.ts +0 -201
- package/src/memory/store.ts +0 -434
- package/src/memory/types.ts +0 -69
- package/src/native/bridge-client.ts +0 -239
- package/src/runtime/accessibility-adapter.ts +0 -487
- package/src/runtime/app-adapter.ts +0 -169
- package/src/runtime/applescript-adapter.ts +0 -376
- package/src/runtime/ax-role-map.ts +0 -102
- package/src/runtime/browser-adapter.ts +0 -129
- package/src/runtime/cdp-chrome-adapter.ts +0 -676
- package/src/runtime/composite-adapter.ts +0 -274
- package/src/runtime/executor.ts +0 -396
- package/src/runtime/planning-loop.ts +0 -81
- package/src/runtime/service.ts +0 -448
- package/src/runtime/session-manager.ts +0 -50
- package/src/runtime/state-observer.ts +0 -136
- package/src/runtime/vision-adapter.ts +0 -297
- package/src/types.ts +0 -297
- package/tests/bridge-client.test.ts +0 -176
- package/tests/browser-stealth.test.ts +0 -210
- package/tests/composite-adapter.test.ts +0 -64
- package/tests/mcp-server.test.ts +0 -151
- package/tests/memory-recall.test.ts +0 -339
- package/tests/memory-research.test.ts +0 -159
- package/tests/memory-seeds.test.ts +0 -120
- package/tests/memory-store.test.ts +0 -392
- package/tests/types.test.ts +0 -92
- package/tsconfig.check.json +0 -17
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -8
- /package/{playbooks → dist-references}/devpost.json +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
import { TaskQueue } from "./task-queue.js";
|
|
18
|
+
import { DEFAULT_MONITOR_CONFIG } from "./types.js";
|
|
19
|
+
export class CodexMonitor {
|
|
20
|
+
bridge;
|
|
21
|
+
terminals = new Map();
|
|
22
|
+
pollTimer = null;
|
|
23
|
+
config;
|
|
24
|
+
running = false;
|
|
25
|
+
assignTimers = new Map();
|
|
26
|
+
queue;
|
|
27
|
+
/** Callback when a terminal status changes */
|
|
28
|
+
onStatusChange;
|
|
29
|
+
/** Callback when a task is auto-assigned */
|
|
30
|
+
onTaskAssigned;
|
|
31
|
+
/** Callback for log messages */
|
|
32
|
+
onLog;
|
|
33
|
+
constructor(bridge, config = {}) {
|
|
34
|
+
this.bridge = bridge;
|
|
35
|
+
this.config = { ...DEFAULT_MONITOR_CONFIG, ...config };
|
|
36
|
+
this.queue = new TaskQueue();
|
|
37
|
+
}
|
|
38
|
+
log(msg) {
|
|
39
|
+
if (this.onLog)
|
|
40
|
+
this.onLog(msg);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Start monitoring a VS Code terminal.
|
|
44
|
+
* Finds VS Code by PID, identifies terminal panels, begins polling.
|
|
45
|
+
*/
|
|
46
|
+
async addTerminal(opts) {
|
|
47
|
+
const id = "term_" + opts.vscodePid + "_" + Date.now().toString(36);
|
|
48
|
+
const state = {
|
|
49
|
+
id,
|
|
50
|
+
vscodePid: opts.vscodePid,
|
|
51
|
+
...(opts.windowId != null ? { windowId: opts.windowId } : {}),
|
|
52
|
+
terminalLabel: opts.label ?? "Terminal",
|
|
53
|
+
status: "unknown",
|
|
54
|
+
lastOutput: "",
|
|
55
|
+
lastTask: null,
|
|
56
|
+
startedAt: new Date().toISOString(),
|
|
57
|
+
lastPollAt: new Date().toISOString(),
|
|
58
|
+
tasksCompleted: 0,
|
|
59
|
+
taskHistory: [],
|
|
60
|
+
};
|
|
61
|
+
this.terminals.set(id, state);
|
|
62
|
+
this.log(`Added terminal ${id} (pid=${opts.vscodePid})`);
|
|
63
|
+
// Do an initial poll
|
|
64
|
+
await this.pollTerminal(state);
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
/** Remove a terminal from monitoring */
|
|
68
|
+
removeTerminal(terminalId) {
|
|
69
|
+
const timer = this.assignTimers.get(terminalId);
|
|
70
|
+
if (timer) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
this.assignTimers.delete(terminalId);
|
|
73
|
+
}
|
|
74
|
+
return this.terminals.delete(terminalId);
|
|
75
|
+
}
|
|
76
|
+
/** Start the polling loop */
|
|
77
|
+
start() {
|
|
78
|
+
if (this.running)
|
|
79
|
+
return;
|
|
80
|
+
this.running = true;
|
|
81
|
+
this.pollTimer = setInterval(async () => {
|
|
82
|
+
if (!this.running)
|
|
83
|
+
return;
|
|
84
|
+
for (const terminal of this.terminals.values()) {
|
|
85
|
+
try {
|
|
86
|
+
await this.pollTerminal(terminal);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
this.log(`Poll error for ${terminal.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, this.config.pollIntervalMs);
|
|
93
|
+
this.log(`Monitor started (poll every ${this.config.pollIntervalMs}ms)`);
|
|
94
|
+
}
|
|
95
|
+
/** Stop the polling loop */
|
|
96
|
+
stop() {
|
|
97
|
+
this.running = false;
|
|
98
|
+
if (this.pollTimer) {
|
|
99
|
+
clearInterval(this.pollTimer);
|
|
100
|
+
this.pollTimer = null;
|
|
101
|
+
}
|
|
102
|
+
for (const timer of this.assignTimers.values()) {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
}
|
|
105
|
+
this.assignTimers.clear();
|
|
106
|
+
this.log("Monitor stopped");
|
|
107
|
+
}
|
|
108
|
+
/** Get all monitored terminals */
|
|
109
|
+
getTerminals() {
|
|
110
|
+
return [...this.terminals.values()];
|
|
111
|
+
}
|
|
112
|
+
/** Get a specific terminal */
|
|
113
|
+
getTerminal(id) {
|
|
114
|
+
return this.terminals.get(id);
|
|
115
|
+
}
|
|
116
|
+
get isRunning() {
|
|
117
|
+
return this.running;
|
|
118
|
+
}
|
|
119
|
+
// ── Core polling logic ──
|
|
120
|
+
async pollTerminal(terminal) {
|
|
121
|
+
terminal.lastPollAt = new Date().toISOString();
|
|
122
|
+
// Strategy: use OCR on the VS Code window to read terminal content.
|
|
123
|
+
// This is more reliable than AX tree for terminal text content.
|
|
124
|
+
const output = await this.readTerminalContent(terminal);
|
|
125
|
+
if (output === null)
|
|
126
|
+
return; // couldn't read
|
|
127
|
+
const oldStatus = terminal.status;
|
|
128
|
+
terminal.lastOutput = output;
|
|
129
|
+
// Detect status from the last few lines of output
|
|
130
|
+
const lastLines = output.split("\n").slice(-15).join("\n");
|
|
131
|
+
terminal.status = this.detectStatus(lastLines);
|
|
132
|
+
// Status transition handling
|
|
133
|
+
if (oldStatus !== terminal.status) {
|
|
134
|
+
this.log(`Terminal ${terminal.id}: ${oldStatus} -> ${terminal.status}`);
|
|
135
|
+
if (this.onStatusChange) {
|
|
136
|
+
this.onStatusChange(terminal, oldStatus);
|
|
137
|
+
}
|
|
138
|
+
// If terminal just went idle, handle task completion + auto-assign
|
|
139
|
+
if (terminal.status === "idle" && (oldStatus === "running" || oldStatus === "unknown")) {
|
|
140
|
+
this.handleTerminalIdle(terminal);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Read terminal content via screenshot + OCR of the VS Code window.
|
|
146
|
+
*/
|
|
147
|
+
async readTerminalContent(terminal) {
|
|
148
|
+
try {
|
|
149
|
+
let shotPath;
|
|
150
|
+
if (terminal.windowId) {
|
|
151
|
+
// Capture specific window
|
|
152
|
+
const shot = await this.bridge.call("cg.captureWindow", {
|
|
153
|
+
windowId: terminal.windowId,
|
|
154
|
+
});
|
|
155
|
+
shotPath = shot.path;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Try to find VS Code window
|
|
159
|
+
const wins = await this.bridge.call("app.windows");
|
|
160
|
+
const vscodeWin = wins.find((w) => w.pid === terminal.vscodePid || w.bundleId === "com.microsoft.VSCode");
|
|
161
|
+
if (!vscodeWin) {
|
|
162
|
+
this.log(`VS Code window not found for pid=${terminal.vscodePid}`);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
terminal.windowId = vscodeWin.windowId;
|
|
166
|
+
const shot = await this.bridge.call("cg.captureWindow", {
|
|
167
|
+
windowId: vscodeWin.windowId,
|
|
168
|
+
});
|
|
169
|
+
shotPath = shot.path;
|
|
170
|
+
}
|
|
171
|
+
// OCR the screenshot
|
|
172
|
+
const ocr = await this.bridge.call("vision.ocr", {
|
|
173
|
+
imagePath: shotPath,
|
|
174
|
+
});
|
|
175
|
+
return ocr.text;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
this.log(`OCR failed for ${terminal.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
+
// Fallback: try AX tree to read terminal content
|
|
180
|
+
return this.readTerminalViaAX(terminal);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Fallback: read terminal content from accessibility tree.
|
|
185
|
+
*/
|
|
186
|
+
async readTerminalViaAX(terminal) {
|
|
187
|
+
try {
|
|
188
|
+
const tree = await this.bridge.call("ax.getElementTree", {
|
|
189
|
+
pid: terminal.vscodePid,
|
|
190
|
+
maxDepth: 6,
|
|
191
|
+
});
|
|
192
|
+
// Find terminal text areas in the AX tree
|
|
193
|
+
const terminalText = this.extractTerminalText(tree);
|
|
194
|
+
return terminalText;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Recursively search AX tree for terminal content.
|
|
202
|
+
* VS Code terminals usually show up as AXTextArea or AXGroup with role "terminal".
|
|
203
|
+
*/
|
|
204
|
+
extractTerminalText(node, depth = 0) {
|
|
205
|
+
if (depth > 8)
|
|
206
|
+
return null;
|
|
207
|
+
const role = (node.role || "").toLowerCase();
|
|
208
|
+
const title = (node.title || "").toLowerCase();
|
|
209
|
+
const desc = (node.description || "").toLowerCase();
|
|
210
|
+
// Look for terminal-like elements
|
|
211
|
+
const isTerminal = role.includes("terminal") ||
|
|
212
|
+
title.includes("terminal") ||
|
|
213
|
+
desc.includes("terminal") ||
|
|
214
|
+
(role === "textarea" && (title.includes("terminal") || desc.includes("terminal")));
|
|
215
|
+
if (isTerminal && node.value) {
|
|
216
|
+
return node.value;
|
|
217
|
+
}
|
|
218
|
+
if (node.children) {
|
|
219
|
+
for (const child of node.children) {
|
|
220
|
+
const found = this.extractTerminalText(child, depth + 1);
|
|
221
|
+
if (found)
|
|
222
|
+
return found;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Detect Codex status from terminal output text.
|
|
229
|
+
*/
|
|
230
|
+
detectStatus(text) {
|
|
231
|
+
const lower = text.toLowerCase();
|
|
232
|
+
// Check error patterns first (highest priority)
|
|
233
|
+
for (const pattern of this.config.errorPatterns) {
|
|
234
|
+
if (lower.includes(pattern.toLowerCase())) {
|
|
235
|
+
// Only if it appears in the last few lines
|
|
236
|
+
const lastLines = text.split("\n").slice(-5).join("\n").toLowerCase();
|
|
237
|
+
if (lastLines.includes(pattern.toLowerCase())) {
|
|
238
|
+
return "error";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Check idle patterns (prompt waiting for input)
|
|
243
|
+
const lastLines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
244
|
+
const lastLine = lastLines[lastLines.length - 1] ?? "";
|
|
245
|
+
const lastLineTrimmed = lastLine.trim();
|
|
246
|
+
for (const pattern of this.config.idlePatterns) {
|
|
247
|
+
if (lastLineTrimmed.includes(pattern) || lastLineTrimmed.endsWith(pattern.trim())) {
|
|
248
|
+
return "idle";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Check running patterns
|
|
252
|
+
for (const pattern of this.config.runningPatterns) {
|
|
253
|
+
if (lower.includes(pattern.toLowerCase())) {
|
|
254
|
+
return "running";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// If we can read content but can't determine status
|
|
258
|
+
return "unknown";
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Handle a terminal going idle — complete current task and maybe assign next.
|
|
262
|
+
*/
|
|
263
|
+
handleTerminalIdle(terminal) {
|
|
264
|
+
// Record task completion if there was an active task
|
|
265
|
+
if (terminal.lastTask) {
|
|
266
|
+
const completed = {
|
|
267
|
+
task: terminal.lastTask,
|
|
268
|
+
startedAt: terminal.taskHistory.length > 0
|
|
269
|
+
? terminal.taskHistory[terminal.taskHistory.length - 1]?.completedAt ?? terminal.startedAt
|
|
270
|
+
: terminal.startedAt,
|
|
271
|
+
completedAt: new Date().toISOString(),
|
|
272
|
+
output: terminal.lastOutput.split("\n").slice(-20).join("\n"),
|
|
273
|
+
};
|
|
274
|
+
terminal.taskHistory.push(completed);
|
|
275
|
+
terminal.tasksCompleted++;
|
|
276
|
+
terminal.lastTask = null;
|
|
277
|
+
// Complete the task in the queue
|
|
278
|
+
const runningTask = this.queue.all().find((t) => t.status === "running" && t.terminalId === terminal.id);
|
|
279
|
+
if (runningTask) {
|
|
280
|
+
this.queue.complete(runningTask.id, completed.output);
|
|
281
|
+
}
|
|
282
|
+
this.log(`Terminal ${terminal.id}: task completed (${terminal.tasksCompleted} total)`);
|
|
283
|
+
}
|
|
284
|
+
// Auto-assign next task if enabled
|
|
285
|
+
if (this.config.autoAssign) {
|
|
286
|
+
// Clear any existing assign timer
|
|
287
|
+
const existingTimer = this.assignTimers.get(terminal.id);
|
|
288
|
+
if (existingTimer)
|
|
289
|
+
clearTimeout(existingTimer);
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
this.assignTimers.delete(terminal.id);
|
|
292
|
+
this.tryAssignTask(terminal);
|
|
293
|
+
}, this.config.assignDelayMs);
|
|
294
|
+
this.assignTimers.set(terminal.id, timer);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Try to assign the next queued task to a terminal.
|
|
299
|
+
*/
|
|
300
|
+
async tryAssignTask(terminal) {
|
|
301
|
+
// Only assign if still idle
|
|
302
|
+
if (terminal.status !== "idle")
|
|
303
|
+
return false;
|
|
304
|
+
const task = this.queue.next(terminal.id);
|
|
305
|
+
if (!task) {
|
|
306
|
+
this.log(`No queued tasks for terminal ${terminal.id}`);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
this.queue.assign(task.id, terminal.id);
|
|
310
|
+
terminal.lastTask = task.prompt;
|
|
311
|
+
this.log(`Assigning task "${task.prompt.slice(0, 50)}" to terminal ${terminal.id}`);
|
|
312
|
+
if (this.onTaskAssigned) {
|
|
313
|
+
this.onTaskAssigned(terminal.id, task);
|
|
314
|
+
}
|
|
315
|
+
// Type the task into the terminal
|
|
316
|
+
try {
|
|
317
|
+
await this.typeIntoTerminal(terminal, task.prompt);
|
|
318
|
+
this.queue.markRunning(task.id);
|
|
319
|
+
terminal.status = "running";
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
this.log(`Failed to type task: ${err instanceof Error ? err.message : String(err)}`);
|
|
324
|
+
this.queue.fail(task.id, String(err));
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Type a command into the terminal by focusing VS Code and using keyboard input.
|
|
330
|
+
*/
|
|
331
|
+
async typeIntoTerminal(terminal, text) {
|
|
332
|
+
// 1. Focus VS Code
|
|
333
|
+
await this.bridge.call("app.focus", { bundleId: "com.microsoft.VSCode" });
|
|
334
|
+
await sleep(300);
|
|
335
|
+
// 2. If we know the terminal label, try to focus that specific terminal pane
|
|
336
|
+
// Use accessibility to find and click the terminal
|
|
337
|
+
try {
|
|
338
|
+
await this.bridge.call("ax.findElement", {
|
|
339
|
+
pid: terminal.vscodePid,
|
|
340
|
+
title: terminal.terminalLabel,
|
|
341
|
+
exact: false,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Terminal pane might already be focused, continue
|
|
346
|
+
}
|
|
347
|
+
// 3. Type the command
|
|
348
|
+
await this.bridge.call("cg.typeText", { text });
|
|
349
|
+
await sleep(100);
|
|
350
|
+
// 4. Press Enter to execute
|
|
351
|
+
await this.bridge.call("cg.keyCombo", { keys: ["enter"] });
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Manually assign a task to a terminal (bypasses queue).
|
|
355
|
+
*/
|
|
356
|
+
async assignDirect(terminalId, prompt) {
|
|
357
|
+
const terminal = this.terminals.get(terminalId);
|
|
358
|
+
if (!terminal)
|
|
359
|
+
return false;
|
|
360
|
+
terminal.lastTask = prompt;
|
|
361
|
+
try {
|
|
362
|
+
await this.typeIntoTerminal(terminal, prompt);
|
|
363
|
+
terminal.status = "running";
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/** Update monitor config */
|
|
371
|
+
updateConfig(config) {
|
|
372
|
+
this.config = { ...this.config, ...config };
|
|
373
|
+
// Restart polling if interval changed
|
|
374
|
+
if (config.pollIntervalMs && this.running) {
|
|
375
|
+
this.stop();
|
|
376
|
+
this.start();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function sleep(ms) {
|
|
381
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
382
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
export class TaskQueue {
|
|
18
|
+
tasks = [];
|
|
19
|
+
/** Add a task to the queue */
|
|
20
|
+
enqueue(prompt, options = {}) {
|
|
21
|
+
const task = {
|
|
22
|
+
id: "task_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
23
|
+
prompt,
|
|
24
|
+
priority: options.priority ?? 10,
|
|
25
|
+
terminalId: options.terminalId ?? null,
|
|
26
|
+
status: "queued",
|
|
27
|
+
createdAt: new Date().toISOString(),
|
|
28
|
+
assignedAt: null,
|
|
29
|
+
completedAt: null,
|
|
30
|
+
result: null,
|
|
31
|
+
};
|
|
32
|
+
this.tasks.push(task);
|
|
33
|
+
this.tasks.sort((a, b) => a.priority - b.priority);
|
|
34
|
+
return task;
|
|
35
|
+
}
|
|
36
|
+
/** Get the next queued task for a specific terminal (or any terminal) */
|
|
37
|
+
next(terminalId) {
|
|
38
|
+
// First try tasks assigned to this specific terminal
|
|
39
|
+
const specific = this.tasks.find((t) => t.status === "queued" && t.terminalId === terminalId);
|
|
40
|
+
if (specific)
|
|
41
|
+
return specific;
|
|
42
|
+
// Then try unassigned tasks
|
|
43
|
+
const any = this.tasks.find((t) => t.status === "queued" && t.terminalId === null);
|
|
44
|
+
return any ?? null;
|
|
45
|
+
}
|
|
46
|
+
/** Mark a task as assigned */
|
|
47
|
+
assign(taskId, terminalId) {
|
|
48
|
+
const task = this.tasks.find((t) => t.id === taskId);
|
|
49
|
+
if (task) {
|
|
50
|
+
task.status = "assigned";
|
|
51
|
+
task.terminalId = terminalId;
|
|
52
|
+
task.assignedAt = new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Mark a task as running */
|
|
56
|
+
markRunning(taskId) {
|
|
57
|
+
const task = this.tasks.find((t) => t.id === taskId);
|
|
58
|
+
if (task)
|
|
59
|
+
task.status = "running";
|
|
60
|
+
}
|
|
61
|
+
/** Mark a task as completed */
|
|
62
|
+
complete(taskId, result) {
|
|
63
|
+
const task = this.tasks.find((t) => t.id === taskId);
|
|
64
|
+
if (task) {
|
|
65
|
+
task.status = "completed";
|
|
66
|
+
task.completedAt = new Date().toISOString();
|
|
67
|
+
task.result = result;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Mark a task as failed */
|
|
71
|
+
fail(taskId, result) {
|
|
72
|
+
const task = this.tasks.find((t) => t.id === taskId);
|
|
73
|
+
if (task) {
|
|
74
|
+
task.status = "failed";
|
|
75
|
+
task.completedAt = new Date().toISOString();
|
|
76
|
+
task.result = result;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Get all tasks */
|
|
80
|
+
all() {
|
|
81
|
+
return [...this.tasks];
|
|
82
|
+
}
|
|
83
|
+
/** Get queued tasks count */
|
|
84
|
+
queuedCount() {
|
|
85
|
+
return this.tasks.filter((t) => t.status === "queued").length;
|
|
86
|
+
}
|
|
87
|
+
/** Remove completed/failed tasks older than given ms */
|
|
88
|
+
cleanup(olderThanMs = 3600000) {
|
|
89
|
+
const cutoff = Date.now() - olderThanMs;
|
|
90
|
+
const before = this.tasks.length;
|
|
91
|
+
this.tasks = this.tasks.filter((t) => t.status === "queued" ||
|
|
92
|
+
t.status === "assigned" ||
|
|
93
|
+
t.status === "running" ||
|
|
94
|
+
(t.completedAt && new Date(t.completedAt).getTime() > cutoff));
|
|
95
|
+
return before - this.tasks.length;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
export const DEFAULT_MONITOR_CONFIG = {
|
|
18
|
+
pollIntervalMs: 3000,
|
|
19
|
+
runningPatterns: [
|
|
20
|
+
"Thinking",
|
|
21
|
+
"Working",
|
|
22
|
+
"Generating",
|
|
23
|
+
"Analyzing",
|
|
24
|
+
"Reading",
|
|
25
|
+
"Writing",
|
|
26
|
+
"Searching",
|
|
27
|
+
"Running",
|
|
28
|
+
"Executing",
|
|
29
|
+
"...",
|
|
30
|
+
"spinning",
|
|
31
|
+
"in progress",
|
|
32
|
+
],
|
|
33
|
+
idlePatterns: [
|
|
34
|
+
"codex>",
|
|
35
|
+
"Codex>",
|
|
36
|
+
"> ",
|
|
37
|
+
"$ ",
|
|
38
|
+
"Done",
|
|
39
|
+
"Complete",
|
|
40
|
+
"Finished",
|
|
41
|
+
"Task completed",
|
|
42
|
+
"All done",
|
|
43
|
+
"ready",
|
|
44
|
+
"waiting for input",
|
|
45
|
+
"What would you like",
|
|
46
|
+
"How can I help",
|
|
47
|
+
],
|
|
48
|
+
errorPatterns: [
|
|
49
|
+
"Error:",
|
|
50
|
+
"error:",
|
|
51
|
+
"FAILED",
|
|
52
|
+
"failed",
|
|
53
|
+
"Exception",
|
|
54
|
+
"Traceback",
|
|
55
|
+
"panic:",
|
|
56
|
+
"FATAL",
|
|
57
|
+
"Cannot",
|
|
58
|
+
"could not",
|
|
59
|
+
],
|
|
60
|
+
autoAssign: true,
|
|
61
|
+
assignDelayMs: 2000,
|
|
62
|
+
};
|