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.
Files changed (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /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
+ };