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 +1 -1
- package/src/commands/remote-control.js +19 -9
- package/src/config.js +1 -1
- package/src/helpers/atlas_env.py +8 -3
- package/src/remote-control.js +66 -11
package/package.json
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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) =>
|
|
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.
|
|
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';
|
package/src/helpers/atlas_env.py
CHANGED
|
@@ -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
|
|
1325
|
-
for one
|
|
1326
|
-
|
|
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
|
|
package/src/remote-control.js
CHANGED
|
@@ -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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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.
|
|
1460
|
-
*
|
|
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
|
}
|