screenhand 0.2.0 → 0.3.1
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 +165 -446
- package/bin/darwin-arm64/macos-bridge +0 -0
- package/dist/mcp-desktop.js +3615 -400
- 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/threads-campaign.js +208 -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/dist/src/context-tracker.js +489 -0
- 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 +82 -14
- package/dist/src/jobs/runner.js +138 -15
- 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 +4 -1
- package/dist/src/memory/playbook-seeds.js +200 -0
- package/dist/src/memory/recall.js +60 -8
- package/dist/src/memory/service.js +30 -5
- package/dist/src/memory/store.js +34 -5
- package/dist/src/native/bridge-client.js +253 -31
- 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 +296 -11
- package/dist/src/playbook/mcp-recorder.js +204 -0
- package/dist/src/playbook/recorder.js +3 -2
- package/dist/src/playbook/runner.js +1 -1
- package/dist/src/playbook/store.js +139 -10
- 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 +55 -18
- package/dist/src/runtime/applescript-adapter.js +8 -2
- package/dist/src/runtime/cdp-chrome-adapter.js +1 -1
- package/dist/src/runtime/executor.js +23 -3
- package/dist/src/runtime/locator-cache.js +24 -2
- package/dist/src/runtime/service.js +59 -15
- package/dist/src/runtime/session-manager.js +4 -1
- package/dist/src/runtime/vision-adapter.js +2 -1
- 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/util/atomic-write.js +19 -4
- 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/devpost.json +186 -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 +22 -0
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +482 -0
- package/native/macos-bridge/Sources/AppManagement.swift +339 -0
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +537 -0
- package/native/macos-bridge/Sources/ObserverBridge.swift +120 -0
- package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
- package/native/macos-bridge/Sources/VisionBridge.swift +238 -0
- package/native/macos-bridge/Sources/main.swift +498 -0
- package/native/windows-bridge/AppManagement.cs +234 -0
- package/native/windows-bridge/InputBridge.cs +436 -0
- package/native/windows-bridge/Program.cs +270 -0
- package/native/windows-bridge/ScreenCapture.cs +453 -0
- package/native/windows-bridge/UIAutomationBridge.cs +571 -0
- package/native/windows-bridge/WindowsBridge.csproj +17 -0
- package/package.json +12 -1
- package/scripts/postinstall.cjs +127 -0
- package/dist/.audit-log.jsonl +0 -55
- package/dist/.screenhand/memory/.lock +0 -1
- package/dist/.screenhand/memory/actions.jsonl +0 -85
- package/dist/.screenhand/memory/errors.jsonl +0 -5
- package/dist/.screenhand/memory/errors.jsonl.bak +0 -4
- package/dist/.screenhand/memory/state.json +0 -35
- package/dist/.screenhand/memory/state.json.bak +0 -35
- package/dist/.screenhand/memory/strategies.jsonl +0 -12
- package/dist/agent/cli.js +0 -73
- package/dist/agent/loop.js +0 -258
- package/dist/config.js +0 -9
- package/dist/index.js +0 -56
- package/dist/logging/timeline-logger.js +0 -29
- package/dist/mcp/mcp-stdio-server.js +0 -448
- package/dist/mcp/server.js +0 -347
- package/dist/mcp-entry.js +0 -59
- package/dist/memory/recall.js +0 -160
- package/dist/memory/research.js +0 -98
- package/dist/memory/seeds.js +0 -89
- package/dist/memory/session.js +0 -161
- package/dist/memory/store.js +0 -391
- package/dist/memory/types.js +0 -4
- package/dist/monitor/codex-monitor.js +0 -377
- package/dist/monitor/task-queue.js +0 -84
- package/dist/monitor/types.js +0 -49
- package/dist/native/bridge-client.js +0 -174
- package/dist/native/macos-bridge-client.js +0 -5
- package/dist/npm-publish-helper.js +0 -117
- package/dist/npm-token-cdp.js +0 -113
- package/dist/npm-token-create.js +0 -135
- package/dist/npm-token-finish.js +0 -126
- package/dist/playbook/engine.js +0 -193
- package/dist/playbook/index.js +0 -4
- package/dist/playbook/recorder.js +0 -519
- package/dist/playbook/runner.js +0 -392
- package/dist/playbook/store.js +0 -166
- package/dist/playbook/types.js +0 -4
- package/dist/runtime/accessibility-adapter.js +0 -377
- package/dist/runtime/app-adapter.js +0 -48
- package/dist/runtime/applescript-adapter.js +0 -283
- package/dist/runtime/ax-role-map.js +0 -80
- package/dist/runtime/browser-adapter.js +0 -36
- package/dist/runtime/cdp-chrome-adapter.js +0 -505
- package/dist/runtime/composite-adapter.js +0 -205
- package/dist/runtime/executor.js +0 -250
- package/dist/runtime/locator-cache.js +0 -12
- package/dist/runtime/planning-loop.js +0 -47
- package/dist/runtime/service.js +0 -372
- package/dist/runtime/session-manager.js +0 -28
- package/dist/runtime/state-observer.js +0 -105
- package/dist/runtime/vision-adapter.js +0 -208
- package/dist/test-mcp-protocol.js +0 -138
- package/dist/types.js +0 -1
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex Monitor — watches VS Code terminals for AI agent activity
|
|
3
|
-
*
|
|
4
|
-
* Uses the native bridge to:
|
|
5
|
-
* 1. Find VS Code windows + terminal panels via accessibility tree
|
|
6
|
-
* 2. Poll terminal content via OCR (screenshot + vision.ocr)
|
|
7
|
-
* 3. Detect agent status (running/idle/error) via pattern matching
|
|
8
|
-
* 4. Auto-assign queued tasks when a terminal goes idle
|
|
9
|
-
*
|
|
10
|
-
* The monitor runs as a background polling loop, similar to PlaybookRecorder.
|
|
11
|
-
*/
|
|
12
|
-
import { TaskQueue } from "./task-queue.js";
|
|
13
|
-
import { DEFAULT_MONITOR_CONFIG } from "./types.js";
|
|
14
|
-
export class CodexMonitor {
|
|
15
|
-
bridge;
|
|
16
|
-
terminals = new Map();
|
|
17
|
-
pollTimer = null;
|
|
18
|
-
config;
|
|
19
|
-
running = false;
|
|
20
|
-
assignTimers = new Map();
|
|
21
|
-
queue;
|
|
22
|
-
/** Callback when a terminal status changes */
|
|
23
|
-
onStatusChange;
|
|
24
|
-
/** Callback when a task is auto-assigned */
|
|
25
|
-
onTaskAssigned;
|
|
26
|
-
/** Callback for log messages */
|
|
27
|
-
onLog;
|
|
28
|
-
constructor(bridge, config = {}) {
|
|
29
|
-
this.bridge = bridge;
|
|
30
|
-
this.config = { ...DEFAULT_MONITOR_CONFIG, ...config };
|
|
31
|
-
this.queue = new TaskQueue();
|
|
32
|
-
}
|
|
33
|
-
log(msg) {
|
|
34
|
-
if (this.onLog)
|
|
35
|
-
this.onLog(msg);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Start monitoring a VS Code terminal.
|
|
39
|
-
* Finds VS Code by PID, identifies terminal panels, begins polling.
|
|
40
|
-
*/
|
|
41
|
-
async addTerminal(opts) {
|
|
42
|
-
const id = "term_" + opts.vscodePid + "_" + Date.now().toString(36);
|
|
43
|
-
const state = {
|
|
44
|
-
id,
|
|
45
|
-
vscodePid: opts.vscodePid,
|
|
46
|
-
...(opts.windowId != null ? { windowId: opts.windowId } : {}),
|
|
47
|
-
terminalLabel: opts.label ?? "Terminal",
|
|
48
|
-
status: "unknown",
|
|
49
|
-
lastOutput: "",
|
|
50
|
-
lastTask: null,
|
|
51
|
-
startedAt: new Date().toISOString(),
|
|
52
|
-
lastPollAt: new Date().toISOString(),
|
|
53
|
-
tasksCompleted: 0,
|
|
54
|
-
taskHistory: [],
|
|
55
|
-
};
|
|
56
|
-
this.terminals.set(id, state);
|
|
57
|
-
this.log(`Added terminal ${id} (pid=${opts.vscodePid})`);
|
|
58
|
-
// Do an initial poll
|
|
59
|
-
await this.pollTerminal(state);
|
|
60
|
-
return state;
|
|
61
|
-
}
|
|
62
|
-
/** Remove a terminal from monitoring */
|
|
63
|
-
removeTerminal(terminalId) {
|
|
64
|
-
const timer = this.assignTimers.get(terminalId);
|
|
65
|
-
if (timer) {
|
|
66
|
-
clearTimeout(timer);
|
|
67
|
-
this.assignTimers.delete(terminalId);
|
|
68
|
-
}
|
|
69
|
-
return this.terminals.delete(terminalId);
|
|
70
|
-
}
|
|
71
|
-
/** Start the polling loop */
|
|
72
|
-
start() {
|
|
73
|
-
if (this.running)
|
|
74
|
-
return;
|
|
75
|
-
this.running = true;
|
|
76
|
-
this.pollTimer = setInterval(async () => {
|
|
77
|
-
if (!this.running)
|
|
78
|
-
return;
|
|
79
|
-
for (const terminal of this.terminals.values()) {
|
|
80
|
-
try {
|
|
81
|
-
await this.pollTerminal(terminal);
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
this.log(`Poll error for ${terminal.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}, this.config.pollIntervalMs);
|
|
88
|
-
this.log(`Monitor started (poll every ${this.config.pollIntervalMs}ms)`);
|
|
89
|
-
}
|
|
90
|
-
/** Stop the polling loop */
|
|
91
|
-
stop() {
|
|
92
|
-
this.running = false;
|
|
93
|
-
if (this.pollTimer) {
|
|
94
|
-
clearInterval(this.pollTimer);
|
|
95
|
-
this.pollTimer = null;
|
|
96
|
-
}
|
|
97
|
-
for (const timer of this.assignTimers.values()) {
|
|
98
|
-
clearTimeout(timer);
|
|
99
|
-
}
|
|
100
|
-
this.assignTimers.clear();
|
|
101
|
-
this.log("Monitor stopped");
|
|
102
|
-
}
|
|
103
|
-
/** Get all monitored terminals */
|
|
104
|
-
getTerminals() {
|
|
105
|
-
return [...this.terminals.values()];
|
|
106
|
-
}
|
|
107
|
-
/** Get a specific terminal */
|
|
108
|
-
getTerminal(id) {
|
|
109
|
-
return this.terminals.get(id);
|
|
110
|
-
}
|
|
111
|
-
get isRunning() {
|
|
112
|
-
return this.running;
|
|
113
|
-
}
|
|
114
|
-
// ── Core polling logic ──
|
|
115
|
-
async pollTerminal(terminal) {
|
|
116
|
-
terminal.lastPollAt = new Date().toISOString();
|
|
117
|
-
// Strategy: use OCR on the VS Code window to read terminal content.
|
|
118
|
-
// This is more reliable than AX tree for terminal text content.
|
|
119
|
-
const output = await this.readTerminalContent(terminal);
|
|
120
|
-
if (output === null)
|
|
121
|
-
return; // couldn't read
|
|
122
|
-
const oldStatus = terminal.status;
|
|
123
|
-
terminal.lastOutput = output;
|
|
124
|
-
// Detect status from the last few lines of output
|
|
125
|
-
const lastLines = output.split("\n").slice(-15).join("\n");
|
|
126
|
-
terminal.status = this.detectStatus(lastLines);
|
|
127
|
-
// Status transition handling
|
|
128
|
-
if (oldStatus !== terminal.status) {
|
|
129
|
-
this.log(`Terminal ${terminal.id}: ${oldStatus} -> ${terminal.status}`);
|
|
130
|
-
if (this.onStatusChange) {
|
|
131
|
-
this.onStatusChange(terminal, oldStatus);
|
|
132
|
-
}
|
|
133
|
-
// If terminal just went idle, handle task completion + auto-assign
|
|
134
|
-
if (terminal.status === "idle" && (oldStatus === "running" || oldStatus === "unknown")) {
|
|
135
|
-
this.handleTerminalIdle(terminal);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Read terminal content via screenshot + OCR of the VS Code window.
|
|
141
|
-
*/
|
|
142
|
-
async readTerminalContent(terminal) {
|
|
143
|
-
try {
|
|
144
|
-
let shotPath;
|
|
145
|
-
if (terminal.windowId) {
|
|
146
|
-
// Capture specific window
|
|
147
|
-
const shot = await this.bridge.call("cg.captureWindow", {
|
|
148
|
-
windowId: terminal.windowId,
|
|
149
|
-
});
|
|
150
|
-
shotPath = shot.path;
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
// Try to find VS Code window
|
|
154
|
-
const wins = await this.bridge.call("app.windows");
|
|
155
|
-
const vscodeWin = wins.find((w) => w.pid === terminal.vscodePid || w.bundleId === "com.microsoft.VSCode");
|
|
156
|
-
if (!vscodeWin) {
|
|
157
|
-
this.log(`VS Code window not found for pid=${terminal.vscodePid}`);
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
terminal.windowId = vscodeWin.windowId;
|
|
161
|
-
const shot = await this.bridge.call("cg.captureWindow", {
|
|
162
|
-
windowId: vscodeWin.windowId,
|
|
163
|
-
});
|
|
164
|
-
shotPath = shot.path;
|
|
165
|
-
}
|
|
166
|
-
// OCR the screenshot
|
|
167
|
-
const ocr = await this.bridge.call("vision.ocr", {
|
|
168
|
-
imagePath: shotPath,
|
|
169
|
-
});
|
|
170
|
-
return ocr.text;
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
this.log(`OCR failed for ${terminal.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
174
|
-
// Fallback: try AX tree to read terminal content
|
|
175
|
-
return this.readTerminalViaAX(terminal);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Fallback: read terminal content from accessibility tree.
|
|
180
|
-
*/
|
|
181
|
-
async readTerminalViaAX(terminal) {
|
|
182
|
-
try {
|
|
183
|
-
const tree = await this.bridge.call("ax.getElementTree", {
|
|
184
|
-
pid: terminal.vscodePid,
|
|
185
|
-
maxDepth: 6,
|
|
186
|
-
});
|
|
187
|
-
// Find terminal text areas in the AX tree
|
|
188
|
-
const terminalText = this.extractTerminalText(tree);
|
|
189
|
-
return terminalText;
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Recursively search AX tree for terminal content.
|
|
197
|
-
* VS Code terminals usually show up as AXTextArea or AXGroup with role "terminal".
|
|
198
|
-
*/
|
|
199
|
-
extractTerminalText(node, depth = 0) {
|
|
200
|
-
if (depth > 8)
|
|
201
|
-
return null;
|
|
202
|
-
const role = (node.role || "").toLowerCase();
|
|
203
|
-
const title = (node.title || "").toLowerCase();
|
|
204
|
-
const desc = (node.description || "").toLowerCase();
|
|
205
|
-
// Look for terminal-like elements
|
|
206
|
-
const isTerminal = role.includes("terminal") ||
|
|
207
|
-
title.includes("terminal") ||
|
|
208
|
-
desc.includes("terminal") ||
|
|
209
|
-
(role === "textarea" && (title.includes("terminal") || desc.includes("terminal")));
|
|
210
|
-
if (isTerminal && node.value) {
|
|
211
|
-
return node.value;
|
|
212
|
-
}
|
|
213
|
-
if (node.children) {
|
|
214
|
-
for (const child of node.children) {
|
|
215
|
-
const found = this.extractTerminalText(child, depth + 1);
|
|
216
|
-
if (found)
|
|
217
|
-
return found;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Detect Codex status from terminal output text.
|
|
224
|
-
*/
|
|
225
|
-
detectStatus(text) {
|
|
226
|
-
const lower = text.toLowerCase();
|
|
227
|
-
// Check error patterns first (highest priority)
|
|
228
|
-
for (const pattern of this.config.errorPatterns) {
|
|
229
|
-
if (lower.includes(pattern.toLowerCase())) {
|
|
230
|
-
// Only if it appears in the last few lines
|
|
231
|
-
const lastLines = text.split("\n").slice(-5).join("\n").toLowerCase();
|
|
232
|
-
if (lastLines.includes(pattern.toLowerCase())) {
|
|
233
|
-
return "error";
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// Check idle patterns (prompt waiting for input)
|
|
238
|
-
const lastLines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
239
|
-
const lastLine = lastLines[lastLines.length - 1] ?? "";
|
|
240
|
-
const lastLineTrimmed = lastLine.trim();
|
|
241
|
-
for (const pattern of this.config.idlePatterns) {
|
|
242
|
-
if (lastLineTrimmed.includes(pattern) || lastLineTrimmed.endsWith(pattern.trim())) {
|
|
243
|
-
return "idle";
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
// Check running patterns
|
|
247
|
-
for (const pattern of this.config.runningPatterns) {
|
|
248
|
-
if (lower.includes(pattern.toLowerCase())) {
|
|
249
|
-
return "running";
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
// If we can read content but can't determine status
|
|
253
|
-
return "unknown";
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Handle a terminal going idle — complete current task and maybe assign next.
|
|
257
|
-
*/
|
|
258
|
-
handleTerminalIdle(terminal) {
|
|
259
|
-
// Record task completion if there was an active task
|
|
260
|
-
if (terminal.lastTask) {
|
|
261
|
-
const completed = {
|
|
262
|
-
task: terminal.lastTask,
|
|
263
|
-
startedAt: terminal.taskHistory.length > 0
|
|
264
|
-
? terminal.taskHistory[terminal.taskHistory.length - 1]?.completedAt ?? terminal.startedAt
|
|
265
|
-
: terminal.startedAt,
|
|
266
|
-
completedAt: new Date().toISOString(),
|
|
267
|
-
output: terminal.lastOutput.split("\n").slice(-20).join("\n"),
|
|
268
|
-
};
|
|
269
|
-
terminal.taskHistory.push(completed);
|
|
270
|
-
terminal.tasksCompleted++;
|
|
271
|
-
terminal.lastTask = null;
|
|
272
|
-
// Complete the task in the queue
|
|
273
|
-
const runningTask = this.queue.all().find((t) => t.status === "running" && t.terminalId === terminal.id);
|
|
274
|
-
if (runningTask) {
|
|
275
|
-
this.queue.complete(runningTask.id, completed.output);
|
|
276
|
-
}
|
|
277
|
-
this.log(`Terminal ${terminal.id}: task completed (${terminal.tasksCompleted} total)`);
|
|
278
|
-
}
|
|
279
|
-
// Auto-assign next task if enabled
|
|
280
|
-
if (this.config.autoAssign) {
|
|
281
|
-
// Clear any existing assign timer
|
|
282
|
-
const existingTimer = this.assignTimers.get(terminal.id);
|
|
283
|
-
if (existingTimer)
|
|
284
|
-
clearTimeout(existingTimer);
|
|
285
|
-
const timer = setTimeout(() => {
|
|
286
|
-
this.assignTimers.delete(terminal.id);
|
|
287
|
-
this.tryAssignTask(terminal);
|
|
288
|
-
}, this.config.assignDelayMs);
|
|
289
|
-
this.assignTimers.set(terminal.id, timer);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Try to assign the next queued task to a terminal.
|
|
294
|
-
*/
|
|
295
|
-
async tryAssignTask(terminal) {
|
|
296
|
-
// Only assign if still idle
|
|
297
|
-
if (terminal.status !== "idle")
|
|
298
|
-
return false;
|
|
299
|
-
const task = this.queue.next(terminal.id);
|
|
300
|
-
if (!task) {
|
|
301
|
-
this.log(`No queued tasks for terminal ${terminal.id}`);
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
this.queue.assign(task.id, terminal.id);
|
|
305
|
-
terminal.lastTask = task.prompt;
|
|
306
|
-
this.log(`Assigning task "${task.prompt.slice(0, 50)}" to terminal ${terminal.id}`);
|
|
307
|
-
if (this.onTaskAssigned) {
|
|
308
|
-
this.onTaskAssigned(terminal.id, task);
|
|
309
|
-
}
|
|
310
|
-
// Type the task into the terminal
|
|
311
|
-
try {
|
|
312
|
-
await this.typeIntoTerminal(terminal, task.prompt);
|
|
313
|
-
this.queue.markRunning(task.id);
|
|
314
|
-
terminal.status = "running";
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
catch (err) {
|
|
318
|
-
this.log(`Failed to type task: ${err instanceof Error ? err.message : String(err)}`);
|
|
319
|
-
this.queue.fail(task.id, String(err));
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Type a command into the terminal by focusing VS Code and using keyboard input.
|
|
325
|
-
*/
|
|
326
|
-
async typeIntoTerminal(terminal, text) {
|
|
327
|
-
// 1. Focus VS Code
|
|
328
|
-
await this.bridge.call("app.focus", { bundleId: "com.microsoft.VSCode" });
|
|
329
|
-
await sleep(300);
|
|
330
|
-
// 2. If we know the terminal label, try to focus that specific terminal pane
|
|
331
|
-
// Use accessibility to find and click the terminal
|
|
332
|
-
try {
|
|
333
|
-
await this.bridge.call("ax.findElement", {
|
|
334
|
-
pid: terminal.vscodePid,
|
|
335
|
-
title: terminal.terminalLabel,
|
|
336
|
-
exact: false,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
// Terminal pane might already be focused, continue
|
|
341
|
-
}
|
|
342
|
-
// 3. Type the command
|
|
343
|
-
await this.bridge.call("cg.typeText", { text });
|
|
344
|
-
await sleep(100);
|
|
345
|
-
// 4. Press Enter to execute
|
|
346
|
-
await this.bridge.call("cg.keyCombo", { keys: ["enter"] });
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Manually assign a task to a terminal (bypasses queue).
|
|
350
|
-
*/
|
|
351
|
-
async assignDirect(terminalId, prompt) {
|
|
352
|
-
const terminal = this.terminals.get(terminalId);
|
|
353
|
-
if (!terminal)
|
|
354
|
-
return false;
|
|
355
|
-
terminal.lastTask = prompt;
|
|
356
|
-
try {
|
|
357
|
-
await this.typeIntoTerminal(terminal, prompt);
|
|
358
|
-
terminal.status = "running";
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
/** Update monitor config */
|
|
366
|
-
updateConfig(config) {
|
|
367
|
-
this.config = { ...this.config, ...config };
|
|
368
|
-
// Restart polling if interval changed
|
|
369
|
-
if (config.pollIntervalMs && this.running) {
|
|
370
|
-
this.stop();
|
|
371
|
-
this.start();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
function sleep(ms) {
|
|
376
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
377
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task Queue — manages tasks to be assigned to Codex terminals
|
|
3
|
-
*/
|
|
4
|
-
export class TaskQueue {
|
|
5
|
-
tasks = [];
|
|
6
|
-
/** Add a task to the queue */
|
|
7
|
-
enqueue(prompt, options = {}) {
|
|
8
|
-
const task = {
|
|
9
|
-
id: "task_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
10
|
-
prompt,
|
|
11
|
-
priority: options.priority ?? 10,
|
|
12
|
-
terminalId: options.terminalId ?? null,
|
|
13
|
-
status: "queued",
|
|
14
|
-
createdAt: new Date().toISOString(),
|
|
15
|
-
assignedAt: null,
|
|
16
|
-
completedAt: null,
|
|
17
|
-
result: null,
|
|
18
|
-
};
|
|
19
|
-
this.tasks.push(task);
|
|
20
|
-
this.tasks.sort((a, b) => a.priority - b.priority);
|
|
21
|
-
return task;
|
|
22
|
-
}
|
|
23
|
-
/** Get the next queued task for a specific terminal (or any terminal) */
|
|
24
|
-
next(terminalId) {
|
|
25
|
-
// First try tasks assigned to this specific terminal
|
|
26
|
-
const specific = this.tasks.find((t) => t.status === "queued" && t.terminalId === terminalId);
|
|
27
|
-
if (specific)
|
|
28
|
-
return specific;
|
|
29
|
-
// Then try unassigned tasks
|
|
30
|
-
const any = this.tasks.find((t) => t.status === "queued" && t.terminalId === null);
|
|
31
|
-
return any ?? null;
|
|
32
|
-
}
|
|
33
|
-
/** Mark a task as assigned */
|
|
34
|
-
assign(taskId, terminalId) {
|
|
35
|
-
const task = this.tasks.find((t) => t.id === taskId);
|
|
36
|
-
if (task) {
|
|
37
|
-
task.status = "assigned";
|
|
38
|
-
task.terminalId = terminalId;
|
|
39
|
-
task.assignedAt = new Date().toISOString();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/** Mark a task as running */
|
|
43
|
-
markRunning(taskId) {
|
|
44
|
-
const task = this.tasks.find((t) => t.id === taskId);
|
|
45
|
-
if (task)
|
|
46
|
-
task.status = "running";
|
|
47
|
-
}
|
|
48
|
-
/** Mark a task as completed */
|
|
49
|
-
complete(taskId, result) {
|
|
50
|
-
const task = this.tasks.find((t) => t.id === taskId);
|
|
51
|
-
if (task) {
|
|
52
|
-
task.status = "completed";
|
|
53
|
-
task.completedAt = new Date().toISOString();
|
|
54
|
-
task.result = result;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/** Mark a task as failed */
|
|
58
|
-
fail(taskId, result) {
|
|
59
|
-
const task = this.tasks.find((t) => t.id === taskId);
|
|
60
|
-
if (task) {
|
|
61
|
-
task.status = "failed";
|
|
62
|
-
task.completedAt = new Date().toISOString();
|
|
63
|
-
task.result = result;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Get all tasks */
|
|
67
|
-
all() {
|
|
68
|
-
return [...this.tasks];
|
|
69
|
-
}
|
|
70
|
-
/** Get queued tasks count */
|
|
71
|
-
queuedCount() {
|
|
72
|
-
return this.tasks.filter((t) => t.status === "queued").length;
|
|
73
|
-
}
|
|
74
|
-
/** Remove completed/failed tasks older than given ms */
|
|
75
|
-
cleanup(olderThanMs = 3600000) {
|
|
76
|
-
const cutoff = Date.now() - olderThanMs;
|
|
77
|
-
const before = this.tasks.length;
|
|
78
|
-
this.tasks = this.tasks.filter((t) => t.status === "queued" ||
|
|
79
|
-
t.status === "assigned" ||
|
|
80
|
-
t.status === "running" ||
|
|
81
|
-
(t.completedAt && new Date(t.completedAt).getTime() > cutoff));
|
|
82
|
-
return before - this.tasks.length;
|
|
83
|
-
}
|
|
84
|
-
}
|
package/dist/monitor/types.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex Monitor types — track AI coding agents in VS Code terminals
|
|
3
|
-
*/
|
|
4
|
-
export const DEFAULT_MONITOR_CONFIG = {
|
|
5
|
-
pollIntervalMs: 3000,
|
|
6
|
-
runningPatterns: [
|
|
7
|
-
"Thinking",
|
|
8
|
-
"Working",
|
|
9
|
-
"Generating",
|
|
10
|
-
"Analyzing",
|
|
11
|
-
"Reading",
|
|
12
|
-
"Writing",
|
|
13
|
-
"Searching",
|
|
14
|
-
"Running",
|
|
15
|
-
"Executing",
|
|
16
|
-
"...",
|
|
17
|
-
"spinning",
|
|
18
|
-
"in progress",
|
|
19
|
-
],
|
|
20
|
-
idlePatterns: [
|
|
21
|
-
"codex>",
|
|
22
|
-
"Codex>",
|
|
23
|
-
"> ",
|
|
24
|
-
"$ ",
|
|
25
|
-
"Done",
|
|
26
|
-
"Complete",
|
|
27
|
-
"Finished",
|
|
28
|
-
"Task completed",
|
|
29
|
-
"All done",
|
|
30
|
-
"ready",
|
|
31
|
-
"waiting for input",
|
|
32
|
-
"What would you like",
|
|
33
|
-
"How can I help",
|
|
34
|
-
],
|
|
35
|
-
errorPatterns: [
|
|
36
|
-
"Error:",
|
|
37
|
-
"error:",
|
|
38
|
-
"FAILED",
|
|
39
|
-
"failed",
|
|
40
|
-
"Exception",
|
|
41
|
-
"Traceback",
|
|
42
|
-
"panic:",
|
|
43
|
-
"FATAL",
|
|
44
|
-
"Cannot",
|
|
45
|
-
"could not",
|
|
46
|
-
],
|
|
47
|
-
autoAssign: true,
|
|
48
|
-
assignDelayMs: 2000,
|
|
49
|
-
};
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { createInterface } from "node:readline";
|
|
5
|
-
/**
|
|
6
|
-
* Per-method timeout overrides (ms).
|
|
7
|
-
* Methods not listed here use the default 10s timeout.
|
|
8
|
-
*/
|
|
9
|
-
const METHOD_TIMEOUTS = {
|
|
10
|
-
"app.launch": 30_000,
|
|
11
|
-
"cg.captureScreen": 15_000,
|
|
12
|
-
"cg.captureWindow": 15_000,
|
|
13
|
-
"vision.ocr": 20_000,
|
|
14
|
-
"vision.findText": 20_000,
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Resolves the correct native bridge binary path for the current platform.
|
|
18
|
-
*/
|
|
19
|
-
function defaultBinaryPath() {
|
|
20
|
-
// import.meta.dirname is Node 20+; for Node 18 derive from import.meta.url
|
|
21
|
-
const base = import.meta.dirname ?? path.dirname(new URL(import.meta.url).pathname);
|
|
22
|
-
if (process.platform === "win32") {
|
|
23
|
-
return path.resolve(base, "../../native/windows-bridge/bin/Release/net8.0-windows/windows-bridge.exe");
|
|
24
|
-
}
|
|
25
|
-
// macOS (default)
|
|
26
|
-
return path.resolve(base, "../../native/macos-bridge/.build/release/macos-bridge");
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Platform-aware native bridge client.
|
|
30
|
-
* Spawns the correct bridge binary (macOS Swift or Windows C#) based on the OS,
|
|
31
|
-
* communicating via the same JSON-RPC-over-stdio protocol.
|
|
32
|
-
*
|
|
33
|
-
* Drop-in replacement for the original MacOSBridgeClient.
|
|
34
|
-
*/
|
|
35
|
-
export class BridgeClient extends EventEmitter {
|
|
36
|
-
process = null;
|
|
37
|
-
nextId = 1;
|
|
38
|
-
pending = new Map();
|
|
39
|
-
binaryPath;
|
|
40
|
-
restarting = false;
|
|
41
|
-
started = false;
|
|
42
|
-
constructor(binaryPath) {
|
|
43
|
-
super();
|
|
44
|
-
this.binaryPath = binaryPath ?? defaultBinaryPath();
|
|
45
|
-
}
|
|
46
|
-
async start() {
|
|
47
|
-
if (this.started)
|
|
48
|
-
return;
|
|
49
|
-
await this.spawn();
|
|
50
|
-
this.started = true;
|
|
51
|
-
}
|
|
52
|
-
async stop() {
|
|
53
|
-
this.started = false;
|
|
54
|
-
if (this.process) {
|
|
55
|
-
this.process.kill();
|
|
56
|
-
this.process = null;
|
|
57
|
-
}
|
|
58
|
-
// Reject all pending
|
|
59
|
-
for (const [id, pending] of this.pending) {
|
|
60
|
-
clearTimeout(pending.timer);
|
|
61
|
-
pending.reject(new Error("Bridge stopped"));
|
|
62
|
-
this.pending.delete(id);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
async call(method, params, timeoutMs) {
|
|
66
|
-
const effectiveTimeout = timeoutMs ?? METHOD_TIMEOUTS[method] ?? 10_000;
|
|
67
|
-
if (!this.process || this.process.exitCode !== null) {
|
|
68
|
-
await this.restart();
|
|
69
|
-
}
|
|
70
|
-
const id = this.nextId++;
|
|
71
|
-
const request = { id, method };
|
|
72
|
-
if (params) {
|
|
73
|
-
request.params = params;
|
|
74
|
-
}
|
|
75
|
-
return new Promise((resolve, reject) => {
|
|
76
|
-
const timer = setTimeout(() => {
|
|
77
|
-
this.pending.delete(id);
|
|
78
|
-
reject(new Error(`Bridge call "${method}" timed out after ${effectiveTimeout}ms`));
|
|
79
|
-
}, effectiveTimeout);
|
|
80
|
-
this.pending.set(id, {
|
|
81
|
-
resolve: resolve,
|
|
82
|
-
reject,
|
|
83
|
-
timer,
|
|
84
|
-
});
|
|
85
|
-
const line = JSON.stringify(request) + "\n";
|
|
86
|
-
this.process.stdin.write(line);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
async ping() {
|
|
90
|
-
return this.call("ping");
|
|
91
|
-
}
|
|
92
|
-
async checkPermissions() {
|
|
93
|
-
return this.call("check_permissions");
|
|
94
|
-
}
|
|
95
|
-
async spawn() {
|
|
96
|
-
const child = spawn(this.binaryPath, [], {
|
|
97
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
98
|
-
});
|
|
99
|
-
child.on("error", (err) => {
|
|
100
|
-
this.emit("error", err);
|
|
101
|
-
if (this.started) {
|
|
102
|
-
this.restart().catch(() => { });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
child.on("exit", (code) => {
|
|
106
|
-
this.emit("exit", code);
|
|
107
|
-
if (this.started && !this.restarting) {
|
|
108
|
-
this.restart().catch(() => { });
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
// Parse stdout line by line
|
|
112
|
-
const rl = createInterface({ input: child.stdout });
|
|
113
|
-
rl.on("line", (line) => {
|
|
114
|
-
this.handleLine(line);
|
|
115
|
-
});
|
|
116
|
-
// Log stderr
|
|
117
|
-
child.stderr?.on("data", (data) => {
|
|
118
|
-
this.emit("stderr", data.toString());
|
|
119
|
-
});
|
|
120
|
-
this.process = child;
|
|
121
|
-
}
|
|
122
|
-
handleLine(line) {
|
|
123
|
-
let response;
|
|
124
|
-
try {
|
|
125
|
-
response = JSON.parse(line);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return; // Ignore malformed lines
|
|
129
|
-
}
|
|
130
|
-
// Event (streaming notification from observer)
|
|
131
|
-
if (response.event) {
|
|
132
|
-
this.emit("ax-event", response.event);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
// Response to a pending request
|
|
136
|
-
const pending = this.pending.get(response.id);
|
|
137
|
-
if (!pending)
|
|
138
|
-
return;
|
|
139
|
-
this.pending.delete(response.id);
|
|
140
|
-
clearTimeout(pending.timer);
|
|
141
|
-
if (response.error) {
|
|
142
|
-
pending.reject(new Error(response.error.message));
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
pending.resolve(response.result);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async restart() {
|
|
149
|
-
if (this.restarting)
|
|
150
|
-
return;
|
|
151
|
-
this.restarting = true;
|
|
152
|
-
// Reject all pending requests
|
|
153
|
-
for (const [id, pending] of this.pending) {
|
|
154
|
-
clearTimeout(pending.timer);
|
|
155
|
-
pending.reject(new Error("Bridge process crashed, restarting"));
|
|
156
|
-
this.pending.delete(id);
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
if (this.process) {
|
|
160
|
-
this.process.kill();
|
|
161
|
-
this.process = null;
|
|
162
|
-
}
|
|
163
|
-
await this.spawn();
|
|
164
|
-
this.emit("restart");
|
|
165
|
-
}
|
|
166
|
-
finally {
|
|
167
|
-
this.restarting = false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* @deprecated Use BridgeClient instead. This alias exists for backward compatibility.
|
|
173
|
-
*/
|
|
174
|
-
export const MacOSBridgeClient = BridgeClient;
|