mindvest-atlas 0.41.0 → 0.43.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.41.0",
3
+ "version": "0.43.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": {
@@ -119,7 +119,10 @@ export function runInProject(job, code, root) {
119
119
  // THE SAME HOLDER FOR EVERY COMMAND in this shell, and no process behind the
120
120
  // claim: each of these exits, and a claim needing a live process would be
121
121
  // gone before the next one ran.
122
- env.ATLAS_COMPUTER_SESSION = shellSession(root);
122
+ // INHERITED FIRST: Atlas hands the chat's own session down, so each
123
+ // conversation and each ask() gets its OWN recording. The shell id is the
124
+ // fallback for a bare terminal Atlas did not start.
125
+ if (!env.ATLAS_COMPUTER_SESSION) env.ATLAS_COMPUTER_SESSION = shellSession(root);
123
126
  env.ATLAS_COMPUTER_PID = '0';
124
127
  env.PYTHONPATH = [dir, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
125
128
  const out = spawnSync(python[0], [...python.slice(1), '-c', code], {
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.41.0';
5
+ export const VERSION = '0.43.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';
@@ -998,6 +998,44 @@ def _primary_index(mons):
998
998
  _SESSION_SCREEN = {"index": None}
999
999
 
1000
1000
 
1001
+ def _screen_note():
1002
+ """Where the chosen screen is remembered, BETWEEN COMMANDS.
1003
+
1004
+ THIS IS WHY CAPTURES CAME BACK FROM SCREEN 1. Working the computer from a
1005
+ shell means one short-lived process per action, and module state does not
1006
+ survive that: use_screen(2) set a number in a process that then exited, and
1007
+ the next command started fresh, found nothing, and fell back to the primary.
1008
+ So a run said "work on screen 2", was told it had, and then every picture it
1009
+ took was of screen 1 while it clicked coordinates measured on screen 2
1010
+ (owner, 2026-09-06: "the capture keeps returning screen-1 content even after
1011
+ switching to screen 2").
1012
+
1013
+ Beside the actions, because it is a fact about this session like they are.
1014
+ """
1015
+ return os.path.join(_session_dir(), "screen.json")
1016
+
1017
+
1018
+ def _remember_screen(index):
1019
+ import json as _json
1020
+ try:
1021
+ with open(_screen_note(), "w", encoding="utf-8") as fh:
1022
+ _json.dump({"index": int(index)}, fh)
1023
+ except (OSError, TypeError, ValueError):
1024
+ # A choice we cannot write down still holds for THIS process, which is
1025
+ # the whole of a script's run.
1026
+ pass
1027
+
1028
+
1029
+ def _recall_screen():
1030
+ import json as _json
1031
+ try:
1032
+ with open(_screen_note(), encoding="utf-8") as fh:
1033
+ want = _json.load(fh).get("index")
1034
+ return int(want) if want else None
1035
+ except Exception:
1036
+ return None
1037
+
1038
+
1001
1039
  def _chosen_screen():
1002
1040
  """Which screen this computer has been told to work on, or None for any."""
1003
1041
  import json as _json
@@ -1026,6 +1064,7 @@ def use_screen(index=None):
1026
1064
  % (want, len(mons), "is" if len(mons) == 1 else "are")
1027
1065
  )
1028
1066
  _SESSION_SCREEN["index"] = want
1067
+ _remember_screen(want)
1029
1068
  _log_action("use_screen", {"monitor": want, "x": found["x"], "y": found["y"],
1030
1069
  "width": found["width"], "height": found["height"]})
1031
1070
  return found
@@ -1036,6 +1075,10 @@ def _current_screen():
1036
1075
  has said. Returns the whole entry, so callers have its bounds."""
1037
1076
  mons = screens()
1038
1077
  want = _SESSION_SCREEN["index"]
1078
+ if want is None:
1079
+ # WHAT THIS SESSION LAST CHOSE, before anything else. A command that did
1080
+ # not itself call use_screen is still part of the run that did.
1081
+ want = _recall_screen()
1039
1082
  if want is None:
1040
1083
  # THE ONE THE PERSON CHOSE, if they chose one. Defaulting to the primary
1041
1084
  # is right on one screen and wrong on three: somebody with a spare
@@ -1146,6 +1189,37 @@ def screen(path=None):
1146
1189
  % (m["x"], m["y"])}
1147
1190
 
1148
1191
 
1192
+ def _screen_changed(path):
1193
+ """True when this picture differs from the one before it.
1194
+
1195
+ Compared by content, not by time: a screen that is still is genuinely
1196
+ unchanged, and telling a reader so saves it opening an image to discover
1197
+ nothing moved. Kept in the session folder, so it works across the separate
1198
+ processes a shell run is made of.
1199
+ """
1200
+ import hashlib
1201
+ if not path:
1202
+ return True
1203
+ try:
1204
+ with open(path, "rb") as fh:
1205
+ now = hashlib.md5(fh.read()).hexdigest()
1206
+ except OSError:
1207
+ return True
1208
+ mark = os.path.join(_session_dir(), "last-screen.txt")
1209
+ was = None
1210
+ try:
1211
+ with open(mark, encoding="utf-8") as fh:
1212
+ was = fh.read().strip()
1213
+ except OSError:
1214
+ pass
1215
+ try:
1216
+ with open(mark, "w", encoding="utf-8") as fh:
1217
+ fh.write(now)
1218
+ except OSError:
1219
+ pass
1220
+ return was != now
1221
+
1222
+
1149
1223
  def do(*steps, **kw):
1150
1224
  """Do several things, then look ONCE. The fast loop.
1151
1225
 
@@ -1198,6 +1272,11 @@ def do(*steps, **kw):
1198
1272
  _time.sleep(settle)
1199
1273
  shot = screen()
1200
1274
  out = {"did": did}
1275
+ # DID ANYTHING ACTUALLY CHANGE? A picture that is byte-for-byte the last one
1276
+ # is a wasted look: the reader spends a turn opening it to find out nothing
1277
+ # happened, which is the slowest possible way to learn that. Said in words
1278
+ # instead, and the picture is still there for when it did.
1279
+ out["changed"] = _screen_changed(shot if isinstance(shot, str) else shot.get("path"))
1201
1280
  # screen() answers a bare path on one monitor and a dict on several. Both
1202
1281
  # travel, because the offset is the thing a reader needs next.
1203
1282
  if isinstance(shot, dict):
@@ -1261,13 +1340,27 @@ def type(text, interval=0.01): # noqa: A001 (the name people expect)
1261
1340
  {"chars": len(str(text))})
1262
1341
 
1263
1342
 
1264
- def key(keys):
1265
- """One key, or several together: key("enter"), key(["mod", "c"]).
1343
+ def key(*keys):
1344
+ """One key, or several together.
1345
+
1346
+ key("enter")
1347
+ key("ctrl", "a") # and key(["ctrl", "a"]) still works
1348
+ key("mod", "c")
1349
+
1350
+ BOTH SHAPES, because everything that calls this assumed the first one and it
1351
+ only accepted the second. `atlas computer-use key ctrl a`, the guidance, and
1352
+ every combination anybody wrote all raised "key() takes 1 positional
1353
+ argument", so no chord worked ANYWHERE while single keys did (owner,
1354
+ 2026-09-06, after a run could not minimise a window with win+down and
1355
+ concluded the CLI only takes one token). A signature that only its author
1356
+ calls correctly is a broken signature.
1266
1357
 
1267
1358
  "mod" is control on Windows and Linux and command on a Mac, so one script
1268
1359
  works on all three. Anything else is passed through as written.
1269
1360
  """
1270
- names = _keys([keys] if isinstance(keys, str) else list(keys))
1361
+ if len(keys) == 1 and not isinstance(keys[0], str):
1362
+ keys = tuple(keys[0])
1363
+ names = _keys(list(keys))
1271
1364
  return _do("key", lambda: (_gui().hotkey(*names) if len(names) > 1 else _gui().press(names[0])),
1272
1365
  {"keys": names})
1273
1366