mindvest-atlas 0.99.0 → 0.100.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.99.0",
3
+ "version": "0.100.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": {
@@ -27,7 +27,7 @@ import {
27
27
  computerUseAllowed, listComputerSessions, readComputerActions, listComputerVideo, readComputerVideoPart,
28
28
  sessionReplay, venvPython, ambientPython,
29
29
  readSchedule, cronMatches, dueMinutes, scheduleMatches, scheduleDueMinutes, writeNext,
30
- SCRIPTS_DIR, projectsForAlert, readMode, checksWhen, projectPrints,
30
+ SCRIPTS_DIR, projectsForAlert, readMode, checksWhen, checksDue, projectPrints,
31
31
  streamStudioFrames,
32
32
  localBrokers, localBrokerCall,
33
33
  localMemory,
@@ -910,12 +910,12 @@ const REQUEST_TICK_MS = 1000;
910
910
  */
911
911
  export function requestTick(root, timeoutMs, spawn = spawnJob, stop = stopRun, nowMs = Date.now()) {
912
912
  const acted = [];
913
- for (const { dir, op, only, replay, why, assume } of takeRequests(root, nowMs)) {
913
+ for (const { dir, op, only, skip, replay, why, assume } of takeRequests(root, nowMs)) {
914
914
  const slug = path.basename(dir);
915
915
  // Atlas Studio's Run checks / Recheck: the same road a check run from the
916
916
  // web takes, so it runs exactly as the project's own run does.
917
917
  if (op === 'checks') {
918
- const r = startChecks({ job: slug, only, replay, why, assume }, root, spawn);
918
+ const r = startChecks({ job: slug, only, skip, replay, why, assume }, root, spawn);
919
919
  if (r.ok) eprintln(color.dim(`· ${slug} — checks asked for here`));
920
920
  acted.push([slug, 'checks']);
921
921
  continue;
@@ -1054,20 +1054,24 @@ export function checksTick(root, nowMs = Date.now(), spawn = spawnJob) {
1054
1054
  if (busy) { Object.assign(s, prints, { filesAt: 0, dataAt: 0, at: nowMs }); continue; }
1055
1055
  if (prints.files !== s.files) { s.files = prints.files; s.filesAt = nowMs; }
1056
1056
  if (prints.data !== s.data) { s.data = prints.data; s.dataAt = nowMs; }
1057
+ const results = readJsonFile(path.join(dir, 'checks.json'));
1058
+ const rows = Array.isArray(results?.checks) ? results.checks : [];
1059
+ // ON THEIR TIMES: each check's own (atlas.json "checks"."each"), and the
1060
+ // project's for every check without one; one run for all that are due.
1061
+ const due = checksDue(when, rows.map((c) => String(c?.id)), off, s.at / 1000, nowMs / 1000);
1057
1062
  let why = null;
1058
- if (when.schedule && scheduleDueMinutes(when.schedule, s.at / 1000, nowMs / 1000).length) why = 'schedule';
1063
+ if (due) why = 'schedule';
1059
1064
  else if (when.files && s.filesAt && nowMs - s.filesAt >= SETTLE_MS) why = 'files';
1060
1065
  else if (when.data && s.dataAt && nowMs - s.dataAt >= SETTLE_MS) why = 'data';
1061
1066
  s.at = nowMs;
1062
- const results = readJsonFile(path.join(dir, 'checks.json'));
1063
- const rows = Array.isArray(results?.checks) ? results.checks : [];
1064
1067
  // EVERY CHECK PAUSED OR DELETED: nothing to run by itself (atlas_env would
1065
1068
  // skip each one anyway, after fetching its data for nothing).
1066
1069
  if (why && rows.length && rows.every((c) => off.has(String(c?.id)))) { s.filesAt = 0; s.dataAt = 0; continue; }
1067
1070
  if (why) {
1068
1071
  s.filesAt = 0; s.dataAt = 0;
1069
- const r = startChecks({ job: slug, why }, root, spawn);
1070
- if (r.ok) { started.push([slug, why]); eprintln(color.dim(`· ${slug} — checks (${why})`)); }
1072
+ const r = startChecks({ job: slug, why, ...(due ? due : {}) }, root, spawn);
1073
+ const which = due?.only.length ? `: ${due.only.join(', ')}` : '';
1074
+ if (r.ok) { started.push([slug, why]); eprintln(color.dim(`· ${slug} — checks (${why}${which})`)); }
1071
1075
  continue;
1072
1076
  }
1073
1077
  // A FIX THAT NOTHING HAS RUN SINCE: that check again, fresh.
@@ -1228,16 +1232,21 @@ export async function remoteControlCommand(args) {
1228
1232
  /** For the tests: the one road every run takes. */
1229
1233
  export const runJobForTests = (job, root, timeoutMs) => runJob(job, root, timeoutMs);
1230
1234
 
1235
+ const CHECK_ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
1236
+
1231
1237
  /** Start a project's checks.py through the ordinary run (plan check included). */
1232
1238
  export function startChecks(ask, root, spawn = spawnJob) {
1233
1239
  const job = String(ask?.job || '');
1234
1240
  const { dir } = workspace(job, root);
1235
1241
  if (!job || !fs.existsSync(path.join(dir, 'checks.py'))) return { ok: false, error: 'This project has no checks yet.' };
1236
- const asked = (Array.isArray(ask.only) ? ask.only : []).map(String).filter((x) => /^[A-Za-z0-9_-]{1,80}$/.test(x));
1242
+ const asked = (Array.isArray(ask.only) ? ask.only : []).map(String).filter((x) => CHECK_ID_RE.test(x));
1237
1243
  // A paused or deleted check is never run, not even when named.
1238
1244
  const off = checksOff(dir);
1239
1245
  const only = asked.filter((x) => !off.has(x));
1240
1246
  if (asked.length && !only.length) return { ok: false, error: 'That check is paused. Resume it to run it.' };
1247
+ // Not due on this minute: a check whose own time did not come round, in a
1248
+ // run the project's schedule started (atlas_env's wants() says no).
1249
+ const skip = (Array.isArray(ask.skip) ? ask.skip : []).map(String).filter((x) => CHECK_ID_RE.test(x)).slice(0, 200);
1241
1250
  const replay = /^\d{8}-\d{6}$/.test(String(ask.replay || '')) ? String(ask.replay) : '';
1242
1251
  // What started it, for the run's history line (a word, or nothing).
1243
1252
  const why = /^[a-z]{1,20}$/.test(String(ask.why || '')) ? String(ask.why) : '';
@@ -1247,6 +1256,7 @@ export function startChecks(ask, root, spawn = spawnJob) {
1247
1256
  automation_id: job, name: job, source: 'local', file: 'checks.py',
1248
1257
  env: {
1249
1258
  ATLAS_CHECK_ONLY: only.join(','), ATLAS_CHECK_REPLAY: replay, ...(why ? { ATLAS_CHECK_WHY: why } : {}),
1259
+ ...(skip.length && !only.length ? { ATLAS_CHECK_SKIP: skip.join(',') } : {}),
1250
1260
  ...(assume ? { ATLAS_CHECK_ASSUME: assume } : {}),
1251
1261
  },
1252
1262
  }, root, CHECKS_TIMEOUT_MS);
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.99.0';
5
+ export const VERSION = '0.100.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';
@@ -1065,6 +1065,9 @@ class _CheckRun:
1065
1065
  import time as _time
1066
1066
  self.dir = _checks_dir()
1067
1067
  self.only = [x for x in (os.environ.get("ATLAS_CHECK_ONLY") or "").split(",") if x]
1068
+ # A run its project's schedule started, minus the checks that keep a
1069
+ # time of their own which is not now (atlas.json "checks"."each").
1070
+ self.skip = [x for x in (os.environ.get("ATLAS_CHECK_SKIP") or "").split(",") if x]
1068
1071
  self.replay_of = (os.environ.get("ATLAS_CHECK_REPLAY") or "").strip() or None
1069
1072
  self.started = _time.time()
1070
1073
  self.id = _time.strftime("%Y%m%d-%H%M%S", _time.localtime(self.started))
@@ -1321,9 +1324,11 @@ class _CheckRun:
1321
1324
 
1322
1325
  # ── results ──
1323
1326
  def wants(self, check_id):
1324
- """False for a check this run was not asked for (Recheck of one), and
1325
- for one the person paused or deleted."""
1326
- if _check_id(check_id) in self.off:
1327
+ """False for a check this run was not asked for (Recheck of one, or a
1328
+ run on the times of a few checks), for one whose own time is not now
1329
+ in a run on the project's schedule, and for one the person paused or
1330
+ deleted."""
1331
+ if _check_id(check_id) in self.off or check_id in self.skip:
1327
1332
  return False
1328
1333
  return not self.only or check_id in self.only
1329
1334
 
@@ -695,17 +695,53 @@ export function checksWhen(dir) {
695
695
  const m = readManifest(dir);
696
696
  const c = m.checks;
697
697
  const o = c && typeof c === 'object' ? c : {};
698
- const expr = typeof o.schedule === 'string' ? o.schedule.trim() : '';
699
- const cron = expr ? parseCron(expr) : null;
700
698
  const on = Array.isArray(o.on) ? o.on.map(String) : [];
701
699
  // The zone it is read in: the checks' own, else the project's, else UTC like
702
700
  // every schedule (Atlas Studio and the web say it in these words too).
703
- const tz = [o.timezone, m.timezone].find((z) => typeof z === 'string' && z.trim()) || '';
704
- return {
705
- schedule: cron ? { expr, lines: [{ kind: 'cron', cron, text: expr }], tz: String(tz).trim(), error: null, cron } : null,
706
- files: on.includes('files'),
707
- data: on.includes('data'),
701
+ const tz = String([o.timezone, m.timezone].find((z) => typeof z === 'string' && z.trim()) || '').trim();
702
+ const sched = (raw) => {
703
+ const expr = typeof raw === 'string' ? raw.trim() : '';
704
+ const cron = expr ? parseCron(expr) : null;
705
+ return cron ? { expr, lines: [{ kind: 'cron', cron, text: expr }], tz, error: null, cron } : null;
708
706
  };
707
+ // A TIME PER CHECK (owner, 2026-10-16: "for each checks, let this be here"):
708
+ // "each": {"<check id>": "<five-field cron>"}, read in the same zone. A check
709
+ // with no entry (or one that is not a cron) keeps the project's schedule.
710
+ const each = {};
711
+ const e = o.each && typeof o.each === 'object' && !Array.isArray(o.each) ? o.each : {};
712
+ for (const [id, raw] of Object.entries(e).slice(0, 200)) {
713
+ if (!CHECK_ID.test(id)) continue;
714
+ const s = sched(raw);
715
+ if (s) each[id] = s;
716
+ }
717
+ return { schedule: sched(o.schedule), each, files: on.includes('files'), data: on.includes('data') };
718
+ }
719
+
720
+ const CHECK_ID = /^[A-Za-z0-9_-]{1,80}$/;
721
+
722
+ /**
723
+ * WHICH CHECKS ARE DUE between two moments (from, to], or null for none: the
724
+ * checks whose own time ("each") came round, and, when the project's schedule
725
+ * came round, every check without a time of its own. One answer, so two
726
+ * schedules on the same minute start ONE run. `known` is the ids checks.json
727
+ * holds, `off` the paused and deleted.
728
+ * {only: [...]} : just these checks (only their own times came round);
729
+ * {only: [], skip: [...]} : all of them but these (their own time did not).
730
+ * The python runner's checks_due is the same, line for line.
731
+ */
732
+ export function checksDue(when, known, off, fromEpoch, toEpoch) {
733
+ const own = Object.keys(when?.each || {}).filter((id) => !off.has(id)).sort();
734
+ const ownDue = own.filter((id) => scheduleDueMinutes(when.each[id], fromEpoch, toEpoch).length);
735
+ const projectDue = !!(when?.schedule && scheduleDueMinutes(when.schedule, fromEpoch, toEpoch).length);
736
+ if (projectDue) {
737
+ const skip = own.filter((id) => !ownDue.includes(id));
738
+ // Nothing left to run: every check it knows is paused, deleted, or keeps
739
+ // a time of its own that is not now.
740
+ const left = known.filter((id) => !off.has(id) && !skip.includes(id));
741
+ if (known.length && !left.length) return null;
742
+ return { only: [], skip };
743
+ }
744
+ return ownDue.length ? { only: ownDue, skip: [] } : null;
709
745
  }
710
746
 
711
747
  /** What a folder holds, as one string that changes when anything in it does. */
@@ -1456,8 +1492,11 @@ export function readJobTable(job, rel, root) {
1456
1492
  * A PDF, Word or PowerPoint file goes to the web as its bytes, like a picture
1457
1493
  * (owner, 2026-10-15: "pdf file support, word docs ... for everything"): the
1458
1494
  * website draws it (its components/files/DocView.tsx). Read as text it was a
1459
- * screenful of bytes, or refused past the text ceiling. Nothing else is sent
1460
- * as bytes: a .doc or a zip is not drawn there, so it is not carried.
1495
+ * screenful of bytes, or refused past the text ceiling. Since 2026-10-16 the
1496
+ * old Office files (.doc, .ppt, .xls) and Parquet too (owner: "yes for
1497
+ * parquet ... yes for old office files"): the website reads their words or
1498
+ * their first rows itself. Nothing else is sent as bytes: a zip or an
1499
+ * OpenDocument file is not drawn there, so it is not carried.
1461
1500
  */
1462
1501
  export const DOC_TYPES = {
1463
1502
  '.pdf': 'application/pdf',
@@ -1466,6 +1505,10 @@ export const DOC_TYPES = {
1466
1505
  '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1467
1506
  '.pptm': 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
1468
1507
  '.ppsx': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
1508
+ '.doc': 'application/msword',
1509
+ '.ppt': 'application/vnd.ms-powerpoint',
1510
+ '.xls': 'application/vnd.ms-excel',
1511
+ '.parquet': 'application/vnd.apache.parquet',
1469
1512
  };
1470
1513
 
1471
1514
  /** The relay's ceiling for a document: a picture's. Atlas's server holds it too. */
@@ -1478,6 +1521,11 @@ export function docType(name) {
1478
1521
  return dot >= 0 ? (DOC_TYPES[n.slice(dot)] || '') : '';
1479
1522
  }
1480
1523
 
1524
+ /** A Jupyter notebook: JSON text with its outputs' pictures inside. */
1525
+ export function isNotebook(name) {
1526
+ return String(name).toLowerCase().endsWith('.ipynb');
1527
+ }
1528
+
1481
1529
  /** "12.4 MB", the website's words (lib/doc-view.ts sizeWords). */
1482
1530
  function sizeWords(bytes) {
1483
1531
  if (bytes < 1024) return `${bytes} bytes`;
@@ -1499,6 +1547,13 @@ export function readJobFile(job, rel, root) {
1499
1547
  if (size > MAX_DOC_BYTES) return { ok: false, error: `This file is ${sizeWords(size)}, too large to show here.` };
1500
1548
  return { ok: true, mime: doc, b64: fs.readFileSync(target).toString('base64') };
1501
1549
  }
1550
+ // A JUPYTER NOTEBOOK (2026-10-16) is text, but its pictures are inside it:
1551
+ // it may be as large as a document, and past that it is worded the same.
1552
+ if (isNotebook(target)) {
1553
+ const size = fs.statSync(target).size;
1554
+ if (size > MAX_DOC_BYTES) return { ok: false, error: `This file is ${sizeWords(size)}, too large to show here.` };
1555
+ return { ok: true, text: fs.readFileSync(target, 'utf8') };
1556
+ }
1502
1557
  if (fs.statSync(target).size > (mime ? MAX_IMAGE_BYTES : MAX_VIEW_BYTES)) {
1503
1558
  return { ok: false, error: 'That file is too large to show here.' };
1504
1559
  }
@@ -1850,7 +1905,7 @@ export function writeRequest(job, op, root, extra = {}) {
1850
1905
  try {
1851
1906
  fs.mkdirSync(dir, { recursive: true });
1852
1907
  // A checks request says which checks and which run it repeats.
1853
- const more = want === 'checks' ? { only: extra.only, replay: extra.replay, why: extra.why, assume: extra.assume } : {};
1908
+ const more = want === 'checks' ? { only: extra.only, skip: extra.skip, replay: extra.replay, why: extra.why, assume: extra.assume } : {};
1854
1909
  fs.writeFileSync(path.join(dir, `${at}-${want}.json`), JSON.stringify({ op: want, at: at / 1000, ...more }), 'utf8');
1855
1910
  } catch (err) {
1856
1911
  return { ok: false, error: `Could not leave the request: ${err.message}` };
@@ -1894,7 +1949,7 @@ export function takeRequests(root, nowMs = Date.now()) {
1894
1949
  if (Number.isFinite(body.at) && nowMs - body.at * 1000 > REQUEST_TTL_MS) continue;
1895
1950
  // A checks request carries which checks and which run it repeats.
1896
1951
  out.push(op === 'checks'
1897
- ? { dir: path.join(base, slug), op, only: body.only, replay: body.replay, why: body.why, assume: body.assume }
1952
+ ? { dir: path.join(base, slug), op, only: body.only, skip: body.skip, replay: body.replay, why: body.why, assume: body.assume }
1898
1953
  : { dir: path.join(base, slug), op });
1899
1954
  }
1900
1955
  }