mindvest-atlas 0.6.2 → 0.7.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.6.2",
3
+ "version": "0.7.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": {
@@ -15,8 +15,9 @@ import { requestJson } from '../api/client.js';
15
15
  import { runStream } from '../util/stream-loop.js';
16
16
  import { color, eprintln } from '../util/ui.js';
17
17
  import {
18
- DEFAULT_TIMEOUT_MS, claimRun, computerName, jobEnv, listJobFiles, listRuns, machineId,
18
+ DEFAULT_TIMEOUT_MS, claimRun, computerName, isPaused, jobEnv, listJobFiles, listRuns, machineId,
19
19
  osLabel, readJobFile, releaseRun, staleness, summarize, tail, workspace, workspaceRoot,
20
+ setPaused, writeHelpers,
20
21
  } from '../remote-control.js';
21
22
  import fs from 'node:fs';
22
23
  import path from 'node:path';
@@ -97,6 +98,10 @@ async function answer(requestId, payload) {
97
98
  async function handleAsk(ask, root) {
98
99
  if (!ask?.request_id) return;
99
100
  if (ask.op === 'list') return answer(ask.request_id, { ok: true, jobs: listJobFiles(root) });
101
+ if (ask.op === 'pause') {
102
+ const { dir } = workspace(ask.job || '', root);
103
+ return answer(ask.request_id, { ok: true, paused: setPaused(dir, Boolean(ask.paused)) });
104
+ }
100
105
  if (ask.op === 'runs') return answer(ask.request_id, { ok: true, runs: listRuns(root) });
101
106
  if (ask.op === 'read') return answer(ask.request_id, readJobFile(ask.job || '', ask.path || '', root));
102
107
  return answer(ask.request_id, { ok: false, error: 'Unknown request.' });
@@ -113,6 +118,13 @@ async function runJob(job, root, timeoutMs) {
113
118
  }
114
119
 
115
120
  const { dir, entry, argv } = workspace(job.automation_id, root);
121
+ // Paused is the owner saying "not now". A scheduled run that fires anyway
122
+ // would make the switch meaningless; a MANUAL run is the owner asking
123
+ // directly, so it goes through.
124
+ if (isPaused(dir) && job.source !== 'manual') {
125
+ eprintln(color.dim(`· ${name} — paused`));
126
+ return undefined;
127
+ }
116
128
  if (!fs.existsSync(entry)) {
117
129
  // NOT a failure. The schedule fired and this computer took the job; there
118
130
  // was simply nothing to carry out yet. Its owner never sees the code, so
@@ -128,6 +140,10 @@ async function runJob(job, root, timeoutMs) {
128
140
  return undefined;
129
141
  }
130
142
 
143
+ // Refreshed every run, so a job always has the supported way to reach a
144
+ // secret and never a reason to open .env itself.
145
+ writeHelpers(dir);
146
+
131
147
  eprintln(color.dim(`▶ ${name}`));
132
148
  const env = {
133
149
  ...process.env,
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.6.2';
5
+ export const VERSION = '0.7.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';
@@ -168,6 +168,70 @@ export function readEnvFile(file) {
168
168
  * Shared first so a key is typed once; the job's own last so a job that wants
169
169
  * its own value gets it. Neither is required.
170
170
  */
171
+ /**
172
+ * Written into every job folder before it runs, in both languages.
173
+ *
174
+ * The job imports this instead of opening .env — so the script the model writes
175
+ * has no reason to touch the file, and a model debugging that script has nothing
176
+ * to learn by trying. Regenerated on every run, so editing or deleting it
177
+ * changes nothing.
178
+ */
179
+ export function writeHelpers(dir) {
180
+ const py = `"""Secrets for this job. Written by Atlas — edits are overwritten.
181
+
182
+ Values come from the environment Atlas set up before starting this script. Do
183
+ NOT open .env: this module is the supported way to reach a secret, and the file
184
+ itself is not something a script or a model needs to read.
185
+ """
186
+ import os
187
+ import sys
188
+
189
+
190
+ def secret(name, default=None):
191
+ """The value of one secret, for USE — never for printing or logging."""
192
+ return os.environ.get(name, default)
193
+
194
+
195
+ def has_secret(name):
196
+ """Whether a secret has been filled in. True/False only, never the value."""
197
+ return bool(os.environ.get(name))
198
+
199
+
200
+ def require(*names):
201
+ """Stop cleanly when a secret has not been filled in yet."""
202
+ missing = [n for n in names if not os.environ.get(n)]
203
+ if missing:
204
+ print("Missing: %s. Add the value in Atlas Remote Control, then run again."
205
+ % ", ".join(missing))
206
+ sys.exit(2)
207
+ `;
208
+ const js = `// Secrets for this job. Written by Atlas — edits are overwritten.
209
+ //
210
+ // Values come from the environment Atlas set up before starting this script. Do
211
+ // NOT open .env: this module is the supported way to reach a secret, and the
212
+ // file itself is not something a script or a model needs to read.
213
+
214
+ /** The value of one secret, for USE — never for printing or logging. */
215
+ export const secret = (name, fallback) => process.env[name] ?? fallback;
216
+
217
+ /** Whether a secret has been filled in. True/false only, never the value. */
218
+ export const hasSecret = (name) => Boolean(process.env[name]);
219
+
220
+ /** Stop cleanly when a secret has not been filled in yet. */
221
+ export function require_(...names) {
222
+ const missing = names.filter((n) => !process.env[n]);
223
+ if (missing.length) {
224
+ console.log(\`Missing: \${missing.join(", ")}. Add the value in Atlas Remote Control, then run again.\`);
225
+ process.exit(2);
226
+ }
227
+ }
228
+ `;
229
+ try {
230
+ fs.writeFileSync(path.join(dir, 'atlas_env.py'), py);
231
+ fs.writeFileSync(path.join(dir, 'atlas_env.js'), js);
232
+ } catch { /* a helper is a courtesy, never a failure */ }
233
+ }
234
+
171
235
  export function jobEnv(dir, root) {
172
236
  return { ...readEnvFile(path.join(workspaceRoot(root), '.env')), ...readEnvFile(path.join(dir, '.env')) };
173
237
  }
@@ -267,7 +331,7 @@ export function listJobFiles(root) {
267
331
  }
268
332
  };
269
333
  walk(path.join(base, job), '');
270
- return { job, files };
334
+ return { job, files, paused: isPaused(path.join(base, job)) };
271
335
  });
272
336
  }
273
337
 
@@ -308,6 +372,31 @@ export function readJobFile(job, rel, root) {
308
372
  * long), which is why the writer stamps both.
309
373
  */
310
374
  export const RUNNING_MARKER = '.running';
375
+ /**
376
+ * Marks a project the owner has switched off.
377
+ *
378
+ * Local, like everything else about a project: pausing is a fact about this
379
+ * computer's copy, and a file is the only form of it that survives the runner
380
+ * restarting and is visible in the folder to anyone wondering why nothing has
381
+ * happened.
382
+ */
383
+ export const PAUSED_MARKER = '.paused';
384
+
385
+ export const isPaused = (dir) => fs.existsSync(path.join(dir, PAUSED_MARKER));
386
+
387
+ /** Switch one project off or back on. Returns the state it ended in. */
388
+ export function setPaused(dir, paused) {
389
+ const m = path.join(dir, PAUSED_MARKER);
390
+ try {
391
+ if (paused) {
392
+ fs.mkdirSync(dir, { recursive: true });
393
+ fs.writeFileSync(m, `${Date.now() / 1000}\n`);
394
+ } else {
395
+ fs.rmSync(m, { force: true });
396
+ }
397
+ } catch { /* best-effort */ }
398
+ return isPaused(dir);
399
+ }
311
400
 
312
401
  export const runningMarker = (dir) => path.join(dir, 'runs', RUNNING_MARKER);
313
402