mindvest-atlas 0.31.0 → 0.35.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/package.json +1 -1
- package/src/commands/computer-use.js +8 -4
- package/src/commands/run.js +7 -0
- package/src/commands/tools.js +2 -0
- package/src/config.js +1 -1
- package/src/helpers/atlas_env.py +149 -9
package/package.json
CHANGED
|
@@ -87,7 +87,11 @@ const OPS = {
|
|
|
87
87
|
const NUMERIC = new Set(['use-screen', 'click', 'double-click', 'right-click',
|
|
88
88
|
'middle-click', 'triple-click', 'move', 'drag', 'scroll']);
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
/** This shell's holding id, made once and remembered. Shared with `atlas run`
|
|
91
|
+
* so everything typed at one prompt lands in ONE session: the log reads as a
|
|
92
|
+
* session rather than a pile of one-action folders, and the claim names
|
|
93
|
+
* something the watch view can find. */
|
|
94
|
+
export function shellSession(root) {
|
|
91
95
|
const file = path.join(workspaceRoot(root), SESSION_FILE);
|
|
92
96
|
try {
|
|
93
97
|
const got = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -115,7 +119,7 @@ function runInProject(job, code, root) {
|
|
|
115
119
|
// THE SAME HOLDER FOR EVERY COMMAND in this shell, and no process behind the
|
|
116
120
|
// claim: each of these exits, and a claim needing a live process would be
|
|
117
121
|
// gone before the next one ran.
|
|
118
|
-
env.ATLAS_COMPUTER_SESSION =
|
|
122
|
+
env.ATLAS_COMPUTER_SESSION = shellSession(root);
|
|
119
123
|
env.ATLAS_COMPUTER_PID = '0';
|
|
120
124
|
env.PYTHONPATH = [dir, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
|
|
121
125
|
const out = spawnSync(python[0], [...python.slice(1), '-c', code], {
|
|
@@ -155,7 +159,7 @@ export function computerUseCommand(args) {
|
|
|
155
159
|
|
|
156
160
|
if (sub === 'stop') {
|
|
157
161
|
// Given back by NAME, so one shell cannot release another's hold.
|
|
158
|
-
const released = releaseComputerClaim(
|
|
162
|
+
const released = releaseComputerClaim(shellSession());
|
|
159
163
|
println(color.dim(released ? 'Gave the computer back.' : 'It was not yours to give back.'));
|
|
160
164
|
return 0;
|
|
161
165
|
}
|
|
@@ -168,7 +172,7 @@ export function computerUseCommand(args) {
|
|
|
168
172
|
return 2;
|
|
169
173
|
}
|
|
170
174
|
const holder = computerClaim();
|
|
171
|
-
if (holder && String(holder.session) !==
|
|
175
|
+
if (holder && String(holder.session) !== shellSession()) {
|
|
172
176
|
eprintln(color.red(`${holder.job || 'Something'} is using the computer right now.`));
|
|
173
177
|
eprintln(color.dim(' There is one mouse, so it is one at a time.'));
|
|
174
178
|
return 3;
|
package/src/commands/run.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
workspace, venvPython, ambientPython, jobEnv, ensureProjectFiles,
|
|
28
28
|
} from '../remote-control.js';
|
|
29
29
|
import { color, println, eprintln } from '../util/ui.js';
|
|
30
|
+
import { shellSession } from './computer-use.js';
|
|
30
31
|
|
|
31
32
|
export function runCommand(args) {
|
|
32
33
|
const flags = args.flags || {};
|
|
@@ -92,6 +93,12 @@ export function runCommand(args) {
|
|
|
92
93
|
+ 'if you need those.'));
|
|
93
94
|
}
|
|
94
95
|
if (!env.ATLAS_JOB) env.ATLAS_JOB = path.basename(dir);
|
|
96
|
+
// ONE SESSION FOR THE WHOLE SHELL, the same one `atlas computer-use` uses.
|
|
97
|
+
// Without it every command became its own session of one action, and the
|
|
98
|
+
// claim named a session no folder matched, so the watch view showed "Nothing
|
|
99
|
+
// yet" beside a screen plainly being worked on.
|
|
100
|
+
if (!env.ATLAS_COMPUTER_SESSION) env.ATLAS_COMPUTER_SESSION = shellSession();
|
|
101
|
+
if (!env.ATLAS_COMPUTER_PID) env.ATLAS_COMPUTER_PID = '0';
|
|
95
102
|
// PYTHONPATH as well as the working directory. Running with -c does not put
|
|
96
103
|
// the script's folder on the path the way running a file does, and `import
|
|
97
104
|
// atlas_env` is the entire reason somebody is here.
|
package/src/commands/tools.js
CHANGED
|
@@ -25,6 +25,8 @@ const LOCAL_COMMANDS = {
|
|
|
25
25
|
'update-cli': 'atlas update-cli fetch the newest Atlas',
|
|
26
26
|
whoami: 'atlas whoami who this computer is signed in as',
|
|
27
27
|
run: 'atlas run --job <project> [--code "..."] run a project, or one snippet, INSIDE it',
|
|
28
|
+
'computer-use': 'atlas computer-use start|screen|click|type|stop see and use this computer',
|
|
29
|
+
computer: 'atlas computer-use start|screen|click|type|stop see and use this computer',
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
// ── THE THIRD PLACE, which nothing here used to mention ────────────────────
|
package/src/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
|
|
5
|
-
export const VERSION = '0.
|
|
5
|
+
export const VERSION = '0.35.0';
|
|
6
6
|
export const DEFAULT_BASE_URL = 'https://atlasmcp.finmanagerai.com';
|
|
7
7
|
export const DEFAULT_SCOPE = 'atlas broker';
|
|
8
8
|
export const CLIENT_NAME = 'Atlas CLI';
|
package/src/helpers/atlas_env.py
CHANGED
|
@@ -518,7 +518,27 @@ _COMPUTER_OFF = (
|
|
|
518
518
|
|
|
519
519
|
|
|
520
520
|
def _computer_on():
|
|
521
|
-
|
|
521
|
+
"""Is this computer allowed to be used, whoever is asking.
|
|
522
|
+
|
|
523
|
+
THE SWITCH IS THE FILE. The environment variable is a fast path the runner
|
|
524
|
+
sets when it starts a job, and reading ONLY that was wrong: a command typed
|
|
525
|
+
in a shell sets no such variable, so every one of them was refused with
|
|
526
|
+
"switched off" while the switch was plainly on, and the refusal was written
|
|
527
|
+
into the log where it read as a fault (owner, 2026-09-05).
|
|
528
|
+
|
|
529
|
+
The variable still wins when it is there, so a run started with computer use
|
|
530
|
+
withheld stays withheld whatever the file says afterwards.
|
|
531
|
+
"""
|
|
532
|
+
said = os.environ.get("ATLAS_COMPUTER_USE", "")
|
|
533
|
+
if said:
|
|
534
|
+
return said not in ("0", "false", "no")
|
|
535
|
+
import json as _json
|
|
536
|
+
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
537
|
+
try:
|
|
538
|
+
with open(os.path.join(root, ".studio", "settings.json"), encoding="utf-8") as fh:
|
|
539
|
+
return bool(_json.load(fh).get("computer_use"))
|
|
540
|
+
except Exception:
|
|
541
|
+
return False
|
|
522
542
|
|
|
523
543
|
|
|
524
544
|
# ── ONE AT A TIME ───────────────────────────────────────────────────────────
|
|
@@ -763,6 +783,32 @@ def _gui():
|
|
|
763
783
|
"turn Computer Use off and on and it will fetch what is missing. "
|
|
764
784
|
"From a script's own environment: pip install pyautogui."
|
|
765
785
|
)
|
|
786
|
+
# ── WHY CLICKS MISSED ───────────────────────────────────────────────
|
|
787
|
+
#
|
|
788
|
+
# Windows scales most displays (125% is the default on a lot of hardware),
|
|
789
|
+
# and a process that has not said otherwise is lied to about the screen:
|
|
790
|
+
# pyautogui PHOTOGRAPHS in physical pixels, 1920x1080, and CLICKS in scaled
|
|
791
|
+
# logical ones, 1536x864. So a coordinate read honestly off a screenshot
|
|
792
|
+
# lands at four fifths of the way to the target, every time, up and to the
|
|
793
|
+
# left. Nothing errors. The click just quietly hits the wrong thing, and an
|
|
794
|
+
# agent reading the next screenshot sees its press did nothing and tries
|
|
795
|
+
# again slightly differently (owner, 2026-09-05: "why is it struggling to
|
|
796
|
+
# click on buttons?").
|
|
797
|
+
#
|
|
798
|
+
# Declaring the process DPI-aware puts both in the same space, so what you
|
|
799
|
+
# measure is what you press. Per-monitor first, because two screens can be
|
|
800
|
+
# scaled differently; the older call is the fallback on Windows 8 and before.
|
|
801
|
+
if os.name == "nt":
|
|
802
|
+
try:
|
|
803
|
+
import ctypes
|
|
804
|
+
try:
|
|
805
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
806
|
+
except Exception:
|
|
807
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
808
|
+
except Exception:
|
|
809
|
+
# Not fatal: it means coordinates may be scaled, which is what was
|
|
810
|
+
# happening before anyone noticed.
|
|
811
|
+
pass
|
|
766
812
|
_p.FAILSAFE = True
|
|
767
813
|
_p.PAUSE = 0.02
|
|
768
814
|
return _p
|
|
@@ -884,9 +930,46 @@ def screens():
|
|
|
884
930
|
"primary": m["left"] == 0 and m["top"] == 0})
|
|
885
931
|
return out
|
|
886
932
|
except ImportError:
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
933
|
+
pass
|
|
934
|
+
# WITHOUT mss, WINDOWS CAN STILL COUNT ITS OWN MONITORS. Reporting one
|
|
935
|
+
# screen on a two-screen machine is not a smaller answer, it is a wrong one:
|
|
936
|
+
# the picker offers a choice that does not exist and use_screen(2) refuses a
|
|
937
|
+
# monitor that is plainly there (owner, 2026-09-05: "I have 2 screens
|
|
938
|
+
# actually, it should see 2 monitors"). EnumDisplayMonitors is in the
|
|
939
|
+
# standard library, so this needs nothing installed.
|
|
940
|
+
if os.name == "nt":
|
|
941
|
+
try:
|
|
942
|
+
import ctypes
|
|
943
|
+
from ctypes import wintypes
|
|
944
|
+
found = []
|
|
945
|
+
|
|
946
|
+
class RECT(ctypes.Structure):
|
|
947
|
+
_fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long),
|
|
948
|
+
("right", ctypes.c_long), ("bottom", ctypes.c_long)]
|
|
949
|
+
|
|
950
|
+
proto = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong,
|
|
951
|
+
ctypes.POINTER(RECT), ctypes.c_double)
|
|
952
|
+
|
|
953
|
+
def each(_h, _dc, rect, _p):
|
|
954
|
+
r = rect.contents
|
|
955
|
+
found.append((r.left, r.top, r.right - r.left, r.bottom - r.top))
|
|
956
|
+
return 1
|
|
957
|
+
|
|
958
|
+
ctypes.windll.user32.EnumDisplayMonitors(0, 0, proto(each), 0)
|
|
959
|
+
if found:
|
|
960
|
+
# In the order Windows reports them, with the one at the origin
|
|
961
|
+
# named primary: the same convention mss uses, so a number means
|
|
962
|
+
# the same monitor whichever path answered.
|
|
963
|
+
found.sort(key=lambda m: (m[1], m[0]))
|
|
964
|
+
return [{"index": i, "x": x, "y": y, "width": w, "height": h,
|
|
965
|
+
"primary": x == 0 and y == 0}
|
|
966
|
+
for i, (x, y, w, h) in enumerate(found, start=1)]
|
|
967
|
+
except Exception:
|
|
968
|
+
pass
|
|
969
|
+
w, h = _gui().size()
|
|
970
|
+
# One screen is all anything here can see. Said as the answer it is, and the
|
|
971
|
+
# guidance says mss is what widens it.
|
|
972
|
+
return [{"index": 1, "x": 0, "y": 0, "width": int(w), "height": int(h), "primary": True}]
|
|
890
973
|
|
|
891
974
|
|
|
892
975
|
def _primary_index(mons):
|
|
@@ -910,6 +993,18 @@ def _primary_index(mons):
|
|
|
910
993
|
_SESSION_SCREEN = {"index": None}
|
|
911
994
|
|
|
912
995
|
|
|
996
|
+
def _chosen_screen():
|
|
997
|
+
"""Which screen this computer has been told to work on, or None for any."""
|
|
998
|
+
import json as _json
|
|
999
|
+
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
1000
|
+
try:
|
|
1001
|
+
with open(os.path.join(root, ".studio", "settings.json"), encoding="utf-8") as fh:
|
|
1002
|
+
want = _json.load(fh).get("computer_screen")
|
|
1003
|
+
return int(want) if want else None
|
|
1004
|
+
except Exception:
|
|
1005
|
+
return None
|
|
1006
|
+
|
|
1007
|
+
|
|
913
1008
|
def use_screen(index=None):
|
|
914
1009
|
"""Work on this monitor, and only this one, for the rest of the run.
|
|
915
1010
|
|
|
@@ -937,7 +1032,13 @@ def _current_screen():
|
|
|
937
1032
|
mons = screens()
|
|
938
1033
|
want = _SESSION_SCREEN["index"]
|
|
939
1034
|
if want is None:
|
|
940
|
-
|
|
1035
|
+
# THE ONE THE PERSON CHOSE, if they chose one. Defaulting to the primary
|
|
1036
|
+
# is right on one screen and wrong on three: somebody with a spare
|
|
1037
|
+
# monitor for this wants work to happen there, and having to say
|
|
1038
|
+
# use_screen() at the top of every run is a setting written out by hand
|
|
1039
|
+
# every time (owner, 2026-09-05: "how do I select the screen instead of
|
|
1040
|
+
# defaulting to screen one").
|
|
1041
|
+
want = _chosen_screen() or _primary_index(mons)
|
|
941
1042
|
_SESSION_SCREEN["index"] = want
|
|
942
1043
|
return next((m for m in mons if m["index"] == want), mons[0] if mons else
|
|
943
1044
|
{"index": 1, "x": 0, "y": 0, "width": 0, "height": 0, "primary": True})
|
|
@@ -1185,24 +1286,63 @@ def _in_the_atlas_window():
|
|
|
1185
1286
|
return any(name in front for name in _ATLAS_WINDOWS) if front else False
|
|
1186
1287
|
|
|
1187
1288
|
#: How far the pointer can drift before it was somebody's hand.
|
|
1188
|
-
|
|
1289
|
+
#:
|
|
1290
|
+
#: Was 20, which is inside the distance an application moves the cursor by
|
|
1291
|
+
#: itself: a dialog opening, a control taking focus, a menu snapping shut. That
|
|
1292
|
+
#: made a run stand back for nobody (owner, 2026-09-05: "moving the mouse should
|
|
1293
|
+
#: not pause this"). A person reaching for a mouse moves it further than this in
|
|
1294
|
+
#: the first tenth of a second.
|
|
1295
|
+
_HUMAN_NUDGE_PX = 120
|
|
1296
|
+
#: And a position we recorded a while ago says nothing about who moved it since.
|
|
1297
|
+
#: Only a fresh one is evidence.
|
|
1298
|
+
_POINTER_FRESH_S = 20
|
|
1189
1299
|
#: Where we last left it, so the next action can tell.
|
|
1190
|
-
_POINTER = {"at": None}
|
|
1300
|
+
_POINTER = {"at": None, "when": 0}
|
|
1191
1301
|
|
|
1192
1302
|
|
|
1193
1303
|
def _note_pointer():
|
|
1304
|
+
import time as _time
|
|
1194
1305
|
try:
|
|
1195
1306
|
pos = _gui().position()
|
|
1196
1307
|
_POINTER["at"] = (int(pos[0]), int(pos[1]))
|
|
1308
|
+
_POINTER["when"] = _time.time()
|
|
1197
1309
|
except Exception:
|
|
1198
1310
|
_POINTER["at"] = None
|
|
1199
1311
|
|
|
1200
1312
|
|
|
1313
|
+
def _pause_on_touch():
|
|
1314
|
+
"""Should a run stand back when the pointer moves on its own?
|
|
1315
|
+
|
|
1316
|
+
OFF BY DEFAULT (owner, 2026-09-05: "no need of mouse move for the human, I
|
|
1317
|
+
can still operate this but no need"). It was asked for, built, tried, and
|
|
1318
|
+
got in the way: a person can work alongside a run perfectly well, and the
|
|
1319
|
+
cost of guessing wrong is a run that stops for nobody and needs a deliberate
|
|
1320
|
+
take_over() to continue. The failsafe is still there for the case that
|
|
1321
|
+
actually matters, which is wanting it to stop NOW: slam the pointer into a
|
|
1322
|
+
corner and pyautogui aborts.
|
|
1323
|
+
"""
|
|
1324
|
+
import json as _json
|
|
1325
|
+
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
1326
|
+
try:
|
|
1327
|
+
with open(os.path.join(root, ".studio", "settings.json"), encoding="utf-8") as fh:
|
|
1328
|
+
return bool(_json.load(fh).get("computer_pause_on_touch"))
|
|
1329
|
+
except Exception:
|
|
1330
|
+
return False
|
|
1331
|
+
|
|
1332
|
+
|
|
1201
1333
|
def _human_took_over():
|
|
1202
1334
|
"""Has somebody moved the pointer since we last touched it?"""
|
|
1335
|
+
import time as _time
|
|
1336
|
+
if not _pause_on_touch():
|
|
1337
|
+
return False
|
|
1203
1338
|
was = _POINTER["at"]
|
|
1204
1339
|
if was is None:
|
|
1205
1340
|
return False
|
|
1341
|
+
# A position from minutes ago is not evidence of anything. Between two
|
|
1342
|
+
# commands typed at a prompt the pointer belongs to whoever is at the desk,
|
|
1343
|
+
# and that is not somebody interrupting a run.
|
|
1344
|
+
if _time.time() - float(_POINTER.get("when") or 0) > _POINTER_FRESH_S:
|
|
1345
|
+
return False
|
|
1206
1346
|
try:
|
|
1207
1347
|
pos = _gui().position()
|
|
1208
1348
|
except Exception:
|
|
@@ -1279,14 +1419,14 @@ def _grab_frame(width=960, quality=42, monitor=1):
|
|
|
1279
1419
|
|
|
1280
1420
|
|
|
1281
1421
|
def _watched_screen():
|
|
1282
|
-
"""The screen this run chose, read from what it did."""
|
|
1422
|
+
"""The screen this run chose, read from what it did, or the chosen default."""
|
|
1283
1423
|
for row in reversed(_read_own_actions()):
|
|
1284
1424
|
if row.get("action") == "use_screen" and row.get("monitor"):
|
|
1285
1425
|
try:
|
|
1286
1426
|
return int(row["monitor"])
|
|
1287
1427
|
except (TypeError, ValueError):
|
|
1288
1428
|
return 1
|
|
1289
|
-
return 1
|
|
1429
|
+
return _chosen_screen() or 1
|
|
1290
1430
|
|
|
1291
1431
|
|
|
1292
1432
|
def _read_own_actions():
|