mindvest-atlas 0.98.0 → 0.99.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 +68 -3
- package/src/config.js +1 -1
- package/src/helpers/atlas_env.py +40 -2
- package/src/remote-control.js +48 -3
package/package.json
CHANGED
|
@@ -283,6 +283,8 @@ async function handleAsk(ask, root) {
|
|
|
283
283
|
// results are read like any file (op 'read' on checks.json and checks/).
|
|
284
284
|
if (ask.op === 'checks') return answer(ask.request_id, startChecks({ ...ask, why: 'web' }, root));
|
|
285
285
|
if (ask.op === 'checks_auto') return answer(ask.request_id, setCheckAuto(ask, root));
|
|
286
|
+
// Pause, resume or delete one check (owner, 2026-10-15), in checks/settings.json.
|
|
287
|
+
if (ask.op === 'checks_state') return answer(ask.request_id, setCheckState(ask, root));
|
|
286
288
|
// ── Sharing a project ────────────────────────────────────────────────
|
|
287
289
|
// Not awaited: a pack, an upload and a download are seconds to minutes on a
|
|
288
290
|
// home connection, and the ask loop has the run queue, the schedule and every
|
|
@@ -1045,6 +1047,7 @@ export function checksTick(root, nowMs = Date.now(), spawn = spawnJob) {
|
|
|
1045
1047
|
if (!fs.existsSync(path.join(dir, 'checks.py'))) continue;
|
|
1046
1048
|
const when = checksWhen(dir);
|
|
1047
1049
|
const prints = projectPrints(dir);
|
|
1050
|
+
const off = checksOff(dir);
|
|
1048
1051
|
let s = checksSeen.get(slug);
|
|
1049
1052
|
if (!s) { checksSeen.set(slug, { at: nowMs, ...prints, filesAt: 0, dataAt: 0, fixed: new Set() }); continue; }
|
|
1050
1053
|
const busy = running.has(`${slug}:checks`) || starting.has(`${slug}:checks`) || nowMs - (checksEndedAt.get(slug) ?? 0) < 30_000;
|
|
@@ -1056,6 +1059,11 @@ export function checksTick(root, nowMs = Date.now(), spawn = spawnJob) {
|
|
|
1056
1059
|
else if (when.files && s.filesAt && nowMs - s.filesAt >= SETTLE_MS) why = 'files';
|
|
1057
1060
|
else if (when.data && s.dataAt && nowMs - s.dataAt >= SETTLE_MS) why = 'data';
|
|
1058
1061
|
s.at = nowMs;
|
|
1062
|
+
const results = readJsonFile(path.join(dir, 'checks.json'));
|
|
1063
|
+
const rows = Array.isArray(results?.checks) ? results.checks : [];
|
|
1064
|
+
// EVERY CHECK PAUSED OR DELETED: nothing to run by itself (atlas_env would
|
|
1065
|
+
// skip each one anyway, after fetching its data for nothing).
|
|
1066
|
+
if (why && rows.length && rows.every((c) => off.has(String(c?.id)))) { s.filesAt = 0; s.dataAt = 0; continue; }
|
|
1059
1067
|
if (why) {
|
|
1060
1068
|
s.filesAt = 0; s.dataAt = 0;
|
|
1061
1069
|
const r = startChecks({ job: slug, why }, root, spawn);
|
|
@@ -1063,9 +1071,9 @@ export function checksTick(root, nowMs = Date.now(), spawn = spawnJob) {
|
|
|
1063
1071
|
continue;
|
|
1064
1072
|
}
|
|
1065
1073
|
// A FIX THAT NOTHING HAS RUN SINCE: that check again, fresh.
|
|
1066
|
-
const results = readJsonFile(path.join(dir, 'checks.json'));
|
|
1067
1074
|
const fixes = readJsonFile(path.join(dir, 'checks', 'resolutions.json'));
|
|
1068
|
-
for (const c of
|
|
1075
|
+
for (const c of rows) {
|
|
1076
|
+
if (off.has(String(c?.id))) continue;
|
|
1069
1077
|
const f = fixes && fixes[c?.id];
|
|
1070
1078
|
if (!f || f.ok === false || c.status === 'pass' || s.fixed.has(`${c.id}@${f.at}`)) continue;
|
|
1071
1079
|
if (Date.parse(f.at) <= Date.parse(c.ran_at || '')) continue;
|
|
@@ -1225,7 +1233,11 @@ export function startChecks(ask, root, spawn = spawnJob) {
|
|
|
1225
1233
|
const job = String(ask?.job || '');
|
|
1226
1234
|
const { dir } = workspace(job, root);
|
|
1227
1235
|
if (!job || !fs.existsSync(path.join(dir, 'checks.py'))) return { ok: false, error: 'This project has no checks yet.' };
|
|
1228
|
-
const
|
|
1236
|
+
const asked = (Array.isArray(ask.only) ? ask.only : []).map(String).filter((x) => /^[A-Za-z0-9_-]{1,80}$/.test(x));
|
|
1237
|
+
// A paused or deleted check is never run, not even when named.
|
|
1238
|
+
const off = checksOff(dir);
|
|
1239
|
+
const only = asked.filter((x) => !off.has(x));
|
|
1240
|
+
if (asked.length && !only.length) return { ok: false, error: 'That check is paused. Resume it to run it.' };
|
|
1229
1241
|
const replay = /^\d{8}-\d{6}$/.test(String(ask.replay || '')) ? String(ask.replay) : '';
|
|
1230
1242
|
// What started it, for the run's history line (a word, or nothing).
|
|
1231
1243
|
const why = /^[a-z]{1,20}$/.test(String(ask.why || '')) ? String(ask.why) : '';
|
|
@@ -1264,6 +1276,59 @@ export function cleanAssume(raw) {
|
|
|
1264
1276
|
return Object.keys(out).length ? JSON.stringify(out) : '';
|
|
1265
1277
|
}
|
|
1266
1278
|
|
|
1279
|
+
/** The checks the person paused or deleted (checks/settings.json), by id. */
|
|
1280
|
+
export function checksOff(dir) {
|
|
1281
|
+
const d = readJsonFile(path.join(dir, 'checks', 'settings.json')) || {};
|
|
1282
|
+
const paused = d.paused && typeof d.paused === 'object' ? d.paused : {};
|
|
1283
|
+
const deleted = d.deleted && typeof d.deleted === 'object' ? d.deleted : {};
|
|
1284
|
+
return new Set([...Object.keys(paused).filter((k) => paused[k]), ...Object.keys(deleted)]);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* PAUSE, RESUME OR DELETE ONE CHECK (owner, 2026-10-15: "I can pause and
|
|
1289
|
+
* delete checks ... a button to pause checks so it never runs"), from the web
|
|
1290
|
+
* or a phone. checks/settings.json, which Atlas Studio writes the same way
|
|
1291
|
+
* and atlas_env reads at every run: a paused check never runs; a deleted one
|
|
1292
|
+
* also leaves the results and is not written back.
|
|
1293
|
+
*/
|
|
1294
|
+
export function setCheckState(ask, root) {
|
|
1295
|
+
const id = String(ask?.id || '');
|
|
1296
|
+
const action = String(ask?.action || '');
|
|
1297
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(id)) return { ok: false, error: 'That is not a check.' };
|
|
1298
|
+
if (!['pause', 'resume', 'delete'].includes(action)) return { ok: false, error: 'Pause, resume or delete.' };
|
|
1299
|
+
const { dir } = workspace(String(ask?.job || ''), root);
|
|
1300
|
+
if (!fs.existsSync(dir)) return { ok: false, error: 'There is no such project on this computer.' };
|
|
1301
|
+
const file = path.join(dir, 'checks', 'settings.json');
|
|
1302
|
+
const d = readJsonFile(file) || {};
|
|
1303
|
+
const paused = d.paused && typeof d.paused === 'object' ? d.paused : {};
|
|
1304
|
+
const deleted = d.deleted && typeof d.deleted === 'object' ? d.deleted : {};
|
|
1305
|
+
if (action === 'pause') paused[id] = true;
|
|
1306
|
+
else delete paused[id];
|
|
1307
|
+
if (action === 'delete') {
|
|
1308
|
+
deleted[id] = new Date().toISOString();
|
|
1309
|
+
if (d.auto_resolve && typeof d.auto_resolve === 'object') delete d.auto_resolve[id];
|
|
1310
|
+
}
|
|
1311
|
+
d.paused = paused;
|
|
1312
|
+
d.deleted = deleted;
|
|
1313
|
+
try {
|
|
1314
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1315
|
+
fs.writeFileSync(`${file}.tmp`, JSON.stringify(d, null, 2));
|
|
1316
|
+
fs.renameSync(`${file}.tmp`, file);
|
|
1317
|
+
} catch {
|
|
1318
|
+
return { ok: false, error: 'That could not be saved.' };
|
|
1319
|
+
}
|
|
1320
|
+
if (action === 'delete') {
|
|
1321
|
+
// Its row leaves the results; its history stays (the person's record).
|
|
1322
|
+
const res = path.join(dir, 'checks.json');
|
|
1323
|
+
const r = readJsonFile(res);
|
|
1324
|
+
if (r && Array.isArray(r.checks) && r.checks.some((c) => c?.id === id)) {
|
|
1325
|
+
r.checks = r.checks.filter((c) => c?.id !== id);
|
|
1326
|
+
try { fs.writeFileSync(`${res}.tmp`, JSON.stringify(r, null, 2)); fs.renameSync(`${res}.tmp`, res); } catch { /* the settings already hide it */ }
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return { ok: true };
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1267
1332
|
/** One check's auto-resolve switch, in checks/settings.json (Atlas Studio reads it too). */
|
|
1268
1333
|
export function setCheckAuto(ask, root) {
|
|
1269
1334
|
const id = String(ask?.id || '');
|
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.99.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
|
@@ -1030,6 +1030,23 @@ def _read_json(path, default):
|
|
|
1030
1030
|
return default
|
|
1031
1031
|
|
|
1032
1032
|
|
|
1033
|
+
def _check_id(value):
|
|
1034
|
+
return "".join(c if c.isalnum() or c in "-_" else "-" for c in str(value))[:80] or "check"
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _checks_off():
|
|
1038
|
+
"""THE CHECKS THE PERSON PAUSED OR DELETED (owner, 2026-10-15: "a button to
|
|
1039
|
+
pause checks so it never runs"). Kept in checks/settings.json beside the
|
|
1040
|
+
auto-fix switches, by Atlas Studio, the web (through the runner) and
|
|
1041
|
+
restore_check(). A paused or deleted check never runs: wants() says no and
|
|
1042
|
+
result() keeps nothing, whatever started the run (its schedule, a change,
|
|
1043
|
+
Run all, an agent building)."""
|
|
1044
|
+
d = _read_json(os.path.join(_checks_dir(), "checks", "settings.json"), {})
|
|
1045
|
+
paused = d.get("paused") if isinstance(d.get("paused"), dict) else {}
|
|
1046
|
+
deleted = d.get("deleted") if isinstance(d.get("deleted"), dict) else {}
|
|
1047
|
+
return {str(k) for k, v in paused.items() if v} | {str(k) for k in deleted}
|
|
1048
|
+
|
|
1049
|
+
|
|
1033
1050
|
def _library_versions():
|
|
1034
1051
|
try:
|
|
1035
1052
|
from importlib import metadata as _md
|
|
@@ -1069,6 +1086,8 @@ class _CheckRun:
|
|
|
1069
1086
|
# per check): what assume() handed out, per check, in order.
|
|
1070
1087
|
self._assumed = {}
|
|
1071
1088
|
self._mine_cache = None
|
|
1089
|
+
# Paused or deleted by the person: never run, never recorded.
|
|
1090
|
+
self.off = _checks_off()
|
|
1072
1091
|
self._watch_the_end()
|
|
1073
1092
|
# THE ONE MODEL THAT RUNS THESE CHECKS (owner, 2026-10-15: "what model
|
|
1074
1093
|
# performed the test"): the project's atlas.json "checks": {"model"},
|
|
@@ -1302,7 +1321,10 @@ class _CheckRun:
|
|
|
1302
1321
|
|
|
1303
1322
|
# ── results ──
|
|
1304
1323
|
def wants(self, check_id):
|
|
1305
|
-
"""False for a check this run was not asked for (Recheck of one)
|
|
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
|
+
return False
|
|
1306
1328
|
return not self.only or check_id in self.only
|
|
1307
1329
|
|
|
1308
1330
|
def result(self, check_id, status, title, summary="", fixed=None, report=None,
|
|
@@ -1316,7 +1338,11 @@ class _CheckRun:
|
|
|
1316
1338
|
agent is asked when the person presses Resolve."""
|
|
1317
1339
|
import time as _time
|
|
1318
1340
|
status = status if status in _CHECK_STATUSES else "warn"
|
|
1319
|
-
cid =
|
|
1341
|
+
cid = _check_id(check_id)
|
|
1342
|
+
# Paused or deleted: nothing of it is kept, even from a checks.py that
|
|
1343
|
+
# did not ask wants() first.
|
|
1344
|
+
if cid in self.off:
|
|
1345
|
+
return
|
|
1320
1346
|
report_rel = None
|
|
1321
1347
|
if report:
|
|
1322
1348
|
text = str(report)
|
|
@@ -1583,6 +1609,18 @@ def checks():
|
|
|
1583
1609
|
return _CheckRun()
|
|
1584
1610
|
|
|
1585
1611
|
|
|
1612
|
+
def restore_check(check_id):
|
|
1613
|
+
"""Bring back a check the person deleted (or paused), ONLY when they ask
|
|
1614
|
+
for it back: it runs again from the next run."""
|
|
1615
|
+
cid = _check_id(check_id)
|
|
1616
|
+
path = os.path.join(_checks_dir(), "checks", "settings.json")
|
|
1617
|
+
data = _read_json(path, {})
|
|
1618
|
+
for key in ("paused", "deleted"):
|
|
1619
|
+
if isinstance(data.get(key), dict):
|
|
1620
|
+
data[key].pop(cid, None)
|
|
1621
|
+
_write_json(path, data)
|
|
1622
|
+
|
|
1623
|
+
|
|
1586
1624
|
def progress(step="", done=None, total=None, message="", **extra):
|
|
1587
1625
|
"""Say where you are, so the dashboard can draw it.
|
|
1588
1626
|
|
package/src/remote-control.js
CHANGED
|
@@ -689,16 +689,20 @@ export function checksCommand(dir) {
|
|
|
689
689
|
|
|
690
690
|
/**
|
|
691
691
|
* When a project's checks run by themselves: atlas.json "checks": {"schedule":
|
|
692
|
-
* "30 8 * * 1-5", "on": ["files", "data"]
|
|
692
|
+
* "30 8 * * 1-5", "on": ["files", "data"], "timezone": "America/New_York"}.
|
|
693
693
|
*/
|
|
694
694
|
export function checksWhen(dir) {
|
|
695
|
-
const
|
|
695
|
+
const m = readManifest(dir);
|
|
696
|
+
const c = m.checks;
|
|
696
697
|
const o = c && typeof c === 'object' ? c : {};
|
|
697
698
|
const expr = typeof o.schedule === 'string' ? o.schedule.trim() : '';
|
|
698
699
|
const cron = expr ? parseCron(expr) : null;
|
|
699
700
|
const on = Array.isArray(o.on) ? o.on.map(String) : [];
|
|
701
|
+
// The zone it is read in: the checks' own, else the project's, else UTC like
|
|
702
|
+
// 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()) || '';
|
|
700
704
|
return {
|
|
701
|
-
schedule: cron ? { expr, lines: [{ kind: 'cron', cron, text: expr }], tz:
|
|
705
|
+
schedule: cron ? { expr, lines: [{ kind: 'cron', cron, text: expr }], tz: String(tz).trim(), error: null, cron } : null,
|
|
702
706
|
files: on.includes('files'),
|
|
703
707
|
data: on.includes('data'),
|
|
704
708
|
};
|
|
@@ -1448,12 +1452,53 @@ export function readJobTable(job, rel, root) {
|
|
|
1448
1452
|
}
|
|
1449
1453
|
}
|
|
1450
1454
|
|
|
1455
|
+
/**
|
|
1456
|
+
* A PDF, Word or PowerPoint file goes to the web as its bytes, like a picture
|
|
1457
|
+
* (owner, 2026-10-15: "pdf file support, word docs ... for everything"): the
|
|
1458
|
+
* 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.
|
|
1461
|
+
*/
|
|
1462
|
+
export const DOC_TYPES = {
|
|
1463
|
+
'.pdf': 'application/pdf',
|
|
1464
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1465
|
+
'.docm': 'application/vnd.ms-word.document.macroEnabled.12',
|
|
1466
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
1467
|
+
'.pptm': 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
|
1468
|
+
'.ppsx': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
/** The relay's ceiling for a document: a picture's. Atlas's server holds it too. */
|
|
1472
|
+
export const MAX_DOC_BYTES = MAX_IMAGE_BYTES;
|
|
1473
|
+
|
|
1474
|
+
/** The content type for a document's name, or '' if it is not one. */
|
|
1475
|
+
export function docType(name) {
|
|
1476
|
+
const n = String(name).toLowerCase();
|
|
1477
|
+
const dot = n.lastIndexOf('.');
|
|
1478
|
+
return dot >= 0 ? (DOC_TYPES[n.slice(dot)] || '') : '';
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/** "12.4 MB", the website's words (lib/doc-view.ts sizeWords). */
|
|
1482
|
+
function sizeWords(bytes) {
|
|
1483
|
+
if (bytes < 1024) return `${bytes} bytes`;
|
|
1484
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
1485
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1451
1488
|
export function readJobFile(job, rel, root) {
|
|
1452
1489
|
const where = jobTarget(job, rel, root);
|
|
1453
1490
|
if (!where.ok) return where;
|
|
1454
1491
|
const { target } = where;
|
|
1455
1492
|
const mime = imageType(path.basename(target));
|
|
1493
|
+
const doc = docType(path.basename(target));
|
|
1456
1494
|
try {
|
|
1495
|
+
if (doc) {
|
|
1496
|
+
const size = fs.statSync(target).size;
|
|
1497
|
+
// Worded as the website's viewer words it, which tells it apart from a
|
|
1498
|
+
// computer from before (that one refused a document as too long a text).
|
|
1499
|
+
if (size > MAX_DOC_BYTES) return { ok: false, error: `This file is ${sizeWords(size)}, too large to show here.` };
|
|
1500
|
+
return { ok: true, mime: doc, b64: fs.readFileSync(target).toString('base64') };
|
|
1501
|
+
}
|
|
1457
1502
|
if (fs.statSync(target).size > (mime ? MAX_IMAGE_BYTES : MAX_VIEW_BYTES)) {
|
|
1458
1503
|
return { ok: false, error: 'That file is too large to show here.' };
|
|
1459
1504
|
}
|