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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindvest-atlas",
3
- "version": "0.31.0",
3
+ "version": "0.35.0",
4
4
  "description": "Atlas CLI \u2014 OAuth login, tool calls, and live alert/flow streaming for the Atlas trading API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- function session(root) {
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 = session(root);
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(session());
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) !== 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;
@@ -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.
@@ -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.31.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';
@@ -518,7 +518,27 @@ _COMPUTER_OFF = (
518
518
 
519
519
 
520
520
  def _computer_on():
521
- return os.environ.get("ATLAS_COMPUTER_USE", "") not in ("", "0", "false", "no")
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
- w, h = _gui().size()
888
- # No mss: we can only honestly describe the one pyautogui can see.
889
- return [{"index": 1, "x": 0, "y": 0, "width": int(w), "height": int(h), "primary": True}]
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
- want = _primary_index(mons)
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
- _HUMAN_NUDGE_PX = 20
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():