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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindvest-atlas",
3
- "version": "0.98.0",
3
+ "version": "0.99.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": {
@@ -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 Array.isArray(results?.checks) ? results.checks : []) {
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 only = (Array.isArray(ask.only) ? ask.only : []).map(String).filter((x) => /^[A-Za-z0-9_-]{1,80}$/.test(x));
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.98.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';
@@ -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 = "".join(c if c.isalnum() or c in "-_" else "-" for c in str(check_id))[:80] or "check"
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
 
@@ -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"]} (local time, like Atlas Studio).
692
+ * "30 8 * * 1-5", "on": ["files", "data"], "timezone": "America/New_York"}.
693
693
  */
694
694
  export function checksWhen(dir) {
695
- const c = readManifest(dir).checks;
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: '', error: null, cron } : null,
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
  }