mindvest-atlas 0.91.0 → 0.93.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/README.md +1 -0
- package/package.json +1 -1
- package/src/commands/computer-use.js +202 -52
- package/src/config.js +1 -1
- package/src/helpers/atlas_env.js +72 -0
- package/src/helpers/atlas_env.py +495 -319
- package/src/remote-control.js +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ atlas options-chain --symbol SPY --expiration 2026-06-20
|
|
|
30
30
|
atlas historical-contract --symbol NVDA --expiry 2026-07-13 --strike 205 --side call --from 2026-07-06 --to 2026-07-10
|
|
31
31
|
atlas alerts --symbol SPY --name greek_exposure --save-charts ./charts
|
|
32
32
|
atlas flow stream --symbol NVDA
|
|
33
|
+
atlas phone-notify --title "Backtest done" --body "SPY: 61% win" --key backtest # your own phone
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
## Layout
|
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// a project's script would call, inside the project, so the switch, the claim,
|
|
23
23
|
// the action log and standing back for a person are identically the same rules.
|
|
24
24
|
import { spawn, spawnSync } from 'node:child_process';
|
|
25
|
+
import net from 'node:net';
|
|
25
26
|
import crypto from 'node:crypto';
|
|
26
27
|
import fs from 'node:fs';
|
|
27
28
|
import path from 'node:path';
|
|
@@ -59,14 +60,14 @@ const HELP = `atlas computer-use <command>
|
|
|
59
60
|
press "<name>" press one of them by name or id (e7), found live [--right] [--double] [--wait <s>]
|
|
60
61
|
type-into "<name>" "<text>" [--enter] fill a field by its name and read it back
|
|
61
62
|
wait-for "<name>" [--gone] [--timeout <s>] until a control or text appears (or goes)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
shortcut <name> [n] a move by its name, the right keys for this computer: zoom_out, back,
|
|
64
|
+
next_tab, next_field, open_list, top, bottom, copy ... (shortcuts lists them)
|
|
65
|
+
shortcuts every named move and its keys here
|
|
66
|
+
fill-form <v> <v>... a whole form from the keyboard, Tab between: text, "pick:Canada" for a
|
|
67
|
+
drop-down, "key:space" for a tick box, "-" to leave a field [--submit] [--keep]
|
|
68
|
+
pick "<option>" choose in the drop-down that has the focus, by name [--open]
|
|
69
|
+
read-text the words in what is in front, as text (select all, copy)
|
|
70
|
+
open-link "<words>" follow a link or press a button on a page by its words, no aiming
|
|
70
71
|
take-over carry on after a person used it by hand
|
|
71
72
|
stop give the computer back
|
|
72
73
|
sessions this project's recordings, with their ids
|
|
@@ -100,14 +101,16 @@ export const OPS = {
|
|
|
100
101
|
cursor: ['get_cursor', 0],
|
|
101
102
|
changed: ['changed', 0],
|
|
102
103
|
code: ['run_code', 1],
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
'
|
|
110
|
-
|
|
104
|
+
// ── THE KEYS EVERY APP ALREADY ANSWERS TO (2026-10-04) ────────────────
|
|
105
|
+
// No separate browser verbs any more: one way of using a computer. A named
|
|
106
|
+
// move needs no aiming and knows this computer's keys (a Mac's are not
|
|
107
|
+
// Windows's), and a whole form is one call.
|
|
108
|
+
shortcut: ['shortcut', -1],
|
|
109
|
+
shortcuts: ['shortcuts', 0],
|
|
110
|
+
'fill-form': ['fill_form', -1],
|
|
111
|
+
pick: ['pick', -1],
|
|
112
|
+
'read-text': ['read_text', 0],
|
|
113
|
+
'open-link': ['open_link', -1],
|
|
111
114
|
'take-over': ['take_over', 0],
|
|
112
115
|
ready: ['computer_ready', 0],
|
|
113
116
|
// WAIT ESPECIALLY. A sequence without it cannot pause for a page, which is
|
|
@@ -160,6 +163,21 @@ export const OPS = {
|
|
|
160
163
|
'type-into': ['type_into', -1],
|
|
161
164
|
'wait-for': ['wait_for', -1],
|
|
162
165
|
};
|
|
166
|
+
// Verbs whose words are ONE value inside a do "..." step.
|
|
167
|
+
const PHRASES = ['type', 'note', 'find', 'code', 'controls', 'press', 'wait-for', 'pick', 'open-link'];
|
|
168
|
+
|
|
169
|
+
/** A form's values as fill_form takes them: "-" leaves a field alone; the
|
|
170
|
+
* rest are the words as given ("pick:Canada" and "key:space" are read by the
|
|
171
|
+
* helper itself). */
|
|
172
|
+
export function formValues(list) {
|
|
173
|
+
return (list || []).map((v) => String(v).trim()).map((v) => (v === '-' ? null : v));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** read-text for a person or a model: the words, then how many there were. */
|
|
177
|
+
function readTextLines(out) {
|
|
178
|
+
return `${out.text}\n(${out.chars} characters${out.cut ? ', cut short' : ''})`;
|
|
179
|
+
}
|
|
180
|
+
|
|
163
181
|
// Which values are numbers. Passing "840" as a string to click() would move the
|
|
164
182
|
// pointer nowhere and report success. THE SAME SET THE PYTHON CLI USES: a verb
|
|
165
183
|
// that takes numbers in one runtime and strings in the other is a verb nobody
|
|
@@ -221,14 +239,22 @@ export function askServer(job, op, args, root) {
|
|
|
221
239
|
}
|
|
222
240
|
// A synchronous ask, because this command has nothing else to do until the
|
|
223
241
|
// answer comes and the caller is a plain exit code.
|
|
242
|
+
//
|
|
243
|
+
// THE NEWLINES BELOW ARE WRITTEN \\n ON PURPOSE. This is a script inside a
|
|
244
|
+
// template string: a plain \n there becomes a real line break in the middle
|
|
245
|
+
// of a quoted string, the child fails to parse, and this returned null for
|
|
246
|
+
// every command, every time, from 2026-09-06 to 2026-10-04. So this runtime
|
|
247
|
+
// (the one Atlas Studio bundles) never once used the live process, paid for
|
|
248
|
+
// a fresh Python per command, and deleted the note on the way out. A test
|
|
249
|
+
// now talks to a real listener through this function.
|
|
224
250
|
const body = JSON.stringify({ token, op, args: args || [], kwargs: {} });
|
|
225
251
|
const out = spawnSync(process.execPath, ['-e', `
|
|
226
252
|
const net = require('node:net');
|
|
227
253
|
const s = net.connect(${port}, '127.0.0.1');
|
|
228
254
|
let got = '';
|
|
229
255
|
s.setTimeout(120000, () => { s.destroy(); process.exit(3); });
|
|
230
|
-
s.on('connect', () => s.write(process.argv[1] + '
|
|
231
|
-
s.on('data', (d) => { got += d; if (got.includes('
|
|
256
|
+
s.on('connect', () => s.write(process.argv[1] + '\\n'));
|
|
257
|
+
s.on('data', (d) => { got += d; if (got.includes('\\n')) { process.stdout.write(got); s.end(); } });
|
|
232
258
|
s.on('error', () => process.exit(3));
|
|
233
259
|
`, body], { encoding: 'utf8' });
|
|
234
260
|
if (out.status !== 0 || !out.stdout) {
|
|
@@ -243,6 +269,97 @@ export function askServer(job, op, args, root) {
|
|
|
243
269
|
}
|
|
244
270
|
}
|
|
245
271
|
|
|
272
|
+
/**
|
|
273
|
+
* The same ask WITHOUT a second process, for a command that can wait for its
|
|
274
|
+
* answer: this process talks to the live one itself. askServer() starts a node
|
|
275
|
+
* child for every ask, because it must answer synchronously; that child is a
|
|
276
|
+
* tenth of a second on a fast machine and more on a slow one, per command.
|
|
277
|
+
* Resolves to the answer, or null when nobody is listening (and then the note
|
|
278
|
+
* that pointed nowhere is removed, as askServer does).
|
|
279
|
+
*/
|
|
280
|
+
export function askLive(job, op, args, root) {
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
let note;
|
|
283
|
+
let port;
|
|
284
|
+
let token;
|
|
285
|
+
try {
|
|
286
|
+
const { dir } = workspace(job || '', root);
|
|
287
|
+
const session = process.env.ATLAS_COMPUTER_SESSION || shellSession(root);
|
|
288
|
+
note = path.join(dir, '.computer-use', session, 'serve.json');
|
|
289
|
+
const n = JSON.parse(fs.readFileSync(note, 'utf8'));
|
|
290
|
+
port = Number(n.port);
|
|
291
|
+
token = String(n.token);
|
|
292
|
+
} catch {
|
|
293
|
+
resolve(null);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
let got = '';
|
|
297
|
+
let done = false;
|
|
298
|
+
const finish = (answer) => {
|
|
299
|
+
if (done) return;
|
|
300
|
+
done = true;
|
|
301
|
+
if (answer === null) { try { fs.rmSync(note, { force: true }); } catch { /* it may be gone */ } }
|
|
302
|
+
resolve(answer);
|
|
303
|
+
};
|
|
304
|
+
const s = net.connect(port, '127.0.0.1');
|
|
305
|
+
s.setTimeout(120000, () => { s.destroy(); finish(null); });
|
|
306
|
+
s.on('connect', () => s.write(JSON.stringify({ token, op, args: args || [], kwargs: {} }) + '\n'));
|
|
307
|
+
s.on('data', (d) => {
|
|
308
|
+
got += d;
|
|
309
|
+
if (!got.includes('\n')) return;
|
|
310
|
+
s.end();
|
|
311
|
+
try { finish(JSON.parse(got.trim().split('\n').pop())); } catch { finish(null); }
|
|
312
|
+
});
|
|
313
|
+
s.on('error', () => finish(null));
|
|
314
|
+
s.on('close', () => finish(null));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The interpreter in its no-console form, where one exists (Windows:
|
|
319
|
+
* pythonw.exe). Only for a process whose output nobody reads. */
|
|
320
|
+
export function windowless(python, platform = process.platform) {
|
|
321
|
+
if (platform !== 'win32' || !python.length) return python;
|
|
322
|
+
const exe = python[0];
|
|
323
|
+
for (const [stem, quiet] of [['python.exe', 'pythonw.exe'], ['py.exe', 'pyw.exe']]) {
|
|
324
|
+
if (exe.toLowerCase().endsWith(stem)) {
|
|
325
|
+
const other = exe.slice(0, -stem.length) + quiet;
|
|
326
|
+
if (fs.existsSync(other)) return [other, ...python.slice(1)];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return python;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Leave one process up for this session. Best effort, always: a machine where
|
|
334
|
+
* this cannot run spawns per action as before. It ends itself after a quarter
|
|
335
|
+
* of an hour idle, and `stop` ends it at once.
|
|
336
|
+
*/
|
|
337
|
+
export function startServer(job, root) {
|
|
338
|
+
try {
|
|
339
|
+
if (askServer(job, 'computer_ready', [], root)) return true; // already up
|
|
340
|
+
const { dir } = workspace(job || '', root);
|
|
341
|
+
const venv = venvPython(dir);
|
|
342
|
+
const python = windowless(venv ? [String(venv)] : [...ambientPython()]);
|
|
343
|
+
const env = { ...process.env, ...jobEnv(dir), ATLAS_JOB: path.basename(dir) };
|
|
344
|
+
if (!env.ATLAS_ASK_URL || !env.ATLAS_ASK_TOKEN) {
|
|
345
|
+
const at = askAddress(root);
|
|
346
|
+
if (at) { env.ATLAS_ASK_URL = at.url; env.ATLAS_ASK_TOKEN = at.token; }
|
|
347
|
+
}
|
|
348
|
+
if (!env.ATLAS_COMPUTER_SESSION) env.ATLAS_COMPUTER_SESSION = shellSession(root);
|
|
349
|
+
if (!env.ATLAS_COMPUTER_PID) env.ATLAS_COMPUTER_PID = '0';
|
|
350
|
+
env.PYTHONPATH = [dir, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
|
|
351
|
+
// Before it comes up: it imports pyautogui at the start on purpose.
|
|
352
|
+
ensureDeps(python, dir, env);
|
|
353
|
+
const child = spawn(python[0], [...python.slice(1), '-c', 'import atlas_env; atlas_env.serve()'],
|
|
354
|
+
{ cwd: dir, env, stdio: 'ignore', detached: true, windowsHide: true });
|
|
355
|
+
child.on('error', () => { /* slower, not broken */ });
|
|
356
|
+
child.unref();
|
|
357
|
+
return true;
|
|
358
|
+
} catch {
|
|
359
|
+
return false; // slower, not broken
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
246
363
|
/** Run one atlas_env call inside the project, and hand back what it said. */
|
|
247
364
|
/** What using the computer needs, and what it is called when you import it. */
|
|
248
365
|
const NEEDED = [['pyautogui', 'pyautogui'], ['mss', 'mss'], ['Pillow', 'PIL']];
|
|
@@ -478,6 +595,13 @@ export function computerUseCommand(args) {
|
|
|
478
595
|
+ "e.note('took the computer')\n"
|
|
479
596
|
+ 'print(json.dumps(e.computer_ready(), default=str))\n';
|
|
480
597
|
const out = runInProject(job, code);
|
|
598
|
+
// AND LEAVE ONE PROCESS UP for the rest of the session. Everything after
|
|
599
|
+
// this is a message to it rather than a Python start and pyautogui's
|
|
600
|
+
// imports, which on Windows is most of a second for an action that takes
|
|
601
|
+
// twenty milliseconds. The Python CLI has done this since September; this
|
|
602
|
+
// one, the one Atlas Studio bundles, never did, so every command in a chat
|
|
603
|
+
// paid for a fresh start while the guide said it would not (2026-10-04).
|
|
604
|
+
if (out === 0) startServer(job);
|
|
481
605
|
if (out === 0) {
|
|
482
606
|
println('');
|
|
483
607
|
println(color.dim('Watching opened in Atlas Studio. Now use it:'));
|
|
@@ -572,18 +696,31 @@ export function computerUseCommand(args) {
|
|
|
572
696
|
const [name] = OPS[verb];
|
|
573
697
|
// A quoted phrase is one value: `type "hello there"` means two words
|
|
574
698
|
// typed, not a word typed twice.
|
|
575
|
-
let vals = (
|
|
576
|
-
'controls', 'press', 'wait-for'].includes(verb))
|
|
699
|
+
let vals = (PHRASES.includes(verb))
|
|
577
700
|
? (bits.length > 1 ? [bits.slice(1).join(' ')] : [])
|
|
578
701
|
: bits.slice(1).map((v) => (NUMERIC.has(verb) ? Number(v) : v));
|
|
579
702
|
// A name can be several words, so the text comes after a "|":
|
|
580
703
|
// "type-into Last name | Smith". Without one, the first word is the name.
|
|
581
704
|
if (verb === 'type-into') vals = splitTarget(bits.slice(1).join(' '));
|
|
705
|
+
// "shortcut zoom_out 2": the name, then how many times.
|
|
706
|
+
if (verb === 'shortcut') vals = bits.length > 2 ? [bits[1], Number(bits[2])] : bits.slice(1);
|
|
707
|
+
// "fill-form Ada | Lovelace | pick:Canada": one value per field.
|
|
708
|
+
if (verb === 'fill-form') vals = [formValues(bits.slice(1).join(' ').split('|'))];
|
|
582
709
|
steps.push([name, ...vals]);
|
|
583
710
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
711
|
+
// THE LIVE PROCESS FIRST, like every other command: a run of steps was the
|
|
712
|
+
// one thing that always paid for a fresh Python, which is most of what a
|
|
713
|
+
// short run costs.
|
|
714
|
+
return askLive(job, 'do', steps).then((live) => {
|
|
715
|
+
if (live) {
|
|
716
|
+
if (live.ok) { println(JSON.stringify(live.out, null, 2)); return 0; }
|
|
717
|
+
eprintln(color.red(String(live.error || 'That did not work.')));
|
|
718
|
+
return 1;
|
|
719
|
+
}
|
|
720
|
+
const code = 'import atlas_env as e, json\n'
|
|
721
|
+
+ `print(json.dumps(e.do(*[tuple(s) for s in json.loads(${JSON.stringify(JSON.stringify(steps))})]), default=str, indent=2))\n`;
|
|
722
|
+
return runInProject(job, code);
|
|
723
|
+
});
|
|
587
724
|
}
|
|
588
725
|
|
|
589
726
|
if (!OPS[sub]) {
|
|
@@ -604,22 +741,34 @@ export function computerUseCommand(args) {
|
|
|
604
741
|
eprintln(color.red(`${sub} takes ${count} value${count === 1 ? '' : 's'}.`));
|
|
605
742
|
return 1;
|
|
606
743
|
}
|
|
607
|
-
if (count === -1 && !rest.length && sub !== '
|
|
744
|
+
if (count === -1 && !rest.length && sub !== 'controls') {
|
|
608
745
|
eprintln(color.red(`${sub} needs at least one.`));
|
|
609
746
|
return 1;
|
|
610
747
|
}
|
|
611
748
|
let values = rest.map((v) => (NUMERIC.has(sub) ? Number(v) : v));
|
|
612
749
|
// A block of code, a sentence to find, an address: the words are one value.
|
|
613
|
-
if (['code', 'note', 'find', 'type', '
|
|
750
|
+
if (['code', 'note', 'find', 'type', 'open-link'].includes(sub) && rest.length > 1) {
|
|
614
751
|
values = [rest.join(' ')];
|
|
615
752
|
}
|
|
753
|
+
if (sub === 'shortcut') {
|
|
754
|
+
// The name, then how many times: `shortcut zoom_out 2`.
|
|
755
|
+
values = rest.length > 1 ? [rest[0], Number(rest[1])] : [rest[0]];
|
|
756
|
+
if (rest.length > 2 || (rest.length > 1 && !Number.isFinite(values[1]))) {
|
|
757
|
+
eprintln(color.red('shortcut takes a name and, if you like, how many times: shortcut zoom_out 2'));
|
|
758
|
+
return 1;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// One value per field, as given; then whether to press Enter, and whether to
|
|
762
|
+
// replace what each field held.
|
|
763
|
+
if (sub === 'fill-form') values = [formValues(rest), Boolean(flags.submit), !flags.keep];
|
|
764
|
+
if (sub === 'pick') values = [rest.join(' '), Boolean(flags.open)];
|
|
616
765
|
if (sub === 'press' || sub === 'wait-for' || sub === 'controls') {
|
|
617
766
|
values = rest.length ? [rest.join(' ')] : [];
|
|
618
767
|
if (sub === 'press') {
|
|
619
768
|
values.push(right ? 'right' : 'left', twice ? 2 : 1);
|
|
620
769
|
// How long it may wait for the control to appear, stop being grey, or
|
|
621
770
|
// come out from under something (default 5s).
|
|
622
|
-
if (flags.wait !== undefined) values.push(null,
|
|
771
|
+
if (flags.wait !== undefined) values.push(null, null, Number(flags.wait));
|
|
623
772
|
}
|
|
624
773
|
if (sub === 'wait-for') {
|
|
625
774
|
values.push(gone, flags.timeout !== undefined ? Number(flags.timeout) : 10);
|
|
@@ -633,32 +782,33 @@ export function computerUseCommand(args) {
|
|
|
633
782
|
values = [rest[0], rest.slice(1).join(' '), enter];
|
|
634
783
|
if (flags.wait !== undefined) values.push(true, null, Number(flags.wait));
|
|
635
784
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
785
|
+
// THE LIVE PROCESS FIRST: same function, same rules, without a python start
|
|
786
|
+
// (and without a second node either: this command can wait for its answer).
|
|
787
|
+
return askLive(job, name, values).then((answer) => {
|
|
788
|
+
if (answer) {
|
|
789
|
+
if (answer.ok) {
|
|
790
|
+
// THE LIST AS LINES: one control per line is what gets read, and the
|
|
791
|
+
// same list as JSON is five times the words to read it in.
|
|
792
|
+
if (sub === 'controls' && answer.out && answer.out.summary) println(answer.out.summary);
|
|
793
|
+
else if (sub === 'read-text' && answer.out && typeof answer.out.text === 'string') println(readTextLines(answer.out));
|
|
794
|
+
else if (sub === 'shortcuts' && answer.out) println(Object.entries(answer.out).map(([k, v]) => `${k.padEnd(18)} ${v}`).join('\n'));
|
|
795
|
+
else println(JSON.stringify(answer.out, null, 2));
|
|
796
|
+
return 0;
|
|
797
|
+
}
|
|
798
|
+
eprintln(color.red(String(answer.error || 'That did not work.')));
|
|
639
799
|
return 1;
|
|
640
800
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
return 1;
|
|
655
|
-
}
|
|
656
|
-
// JSON rather than pasting: the text may hold quotes, and a snippet built by
|
|
657
|
-
// pasting is a snippet that breaks on somebody's password.
|
|
658
|
-
const code = 'import atlas_env as e, json\n'
|
|
659
|
-
+ `out = e.${name}(*json.loads(${JSON.stringify(JSON.stringify(values))}))\n`
|
|
660
|
-
+ (sub === 'controls'
|
|
661
|
-
? 'print(out["summary"] if isinstance(out, dict) and out.get("summary") else json.dumps(out, default=str))\n'
|
|
662
|
-
: 'print(json.dumps(out, default=str))\n');
|
|
663
|
-
return runInProject(job, code);
|
|
801
|
+
// JSON rather than pasting: the text may hold quotes, and a snippet built by
|
|
802
|
+
// pasting is a snippet that breaks on somebody's password.
|
|
803
|
+
const code = 'import atlas_env as e, json\n'
|
|
804
|
+
+ `out = e.${name}(*json.loads(${JSON.stringify(JSON.stringify(values))}))\n`
|
|
805
|
+
+ (sub === 'controls'
|
|
806
|
+
? 'print(out["summary"] if isinstance(out, dict) and out.get("summary") else json.dumps(out, default=str))\n'
|
|
807
|
+
: sub === 'read-text'
|
|
808
|
+
? 'print(out["text"]); print("(%d characters%s)" % (out["chars"], ", cut short" if out.get("cut") else ""))\n'
|
|
809
|
+
: sub === 'shortcuts'
|
|
810
|
+
? 'print("\\n".join("%-18s %s" % kv for kv in out.items()))\n'
|
|
811
|
+
: 'print(json.dumps(out, default=str))\n');
|
|
812
|
+
return runInProject(job, code);
|
|
813
|
+
});
|
|
664
814
|
}
|
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.93.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.js
CHANGED
|
@@ -329,6 +329,62 @@ export async function mcpTools({ server = "", search = "" } = {}) {
|
|
|
329
329
|
return atlas("mcp-tools", { server, search });
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Tell the owner's PHONE (the MindVest Atlas app). A fixed layout you fill in.
|
|
334
|
+
*
|
|
335
|
+
* A title (cut at 40 characters) and up to two short lines; plain words. The
|
|
336
|
+
* same `key` again REPLACES what is on the phone instead of adding to it.
|
|
337
|
+
* `progress` (0 to 1) draws a bar. `button` puts ONE button on it (16
|
|
338
|
+
* characters): pressing it runs this project again, by hand, with the press in
|
|
339
|
+
* the payload (see phonePress()). `done: true` finishes a card that is up. At
|
|
340
|
+
* most 30 new ones an hour, so say what matters, not every step.
|
|
341
|
+
*
|
|
342
|
+
* await notify("Backtest done", { body: "SPY: 61% win\n214 trades", key: "backtest" });
|
|
343
|
+
* await notify("Backtest running", { key: "backtest", progress: 0.4 });
|
|
344
|
+
* await notify("Backtest done", { key: "backtest", button: "Run on QQQ" });
|
|
345
|
+
*
|
|
346
|
+
* Returns the tool's answer. `sent` is 0 when the owner switched these off
|
|
347
|
+
* (Profile, Manage notifications, Studio); that is their choice, not an error.
|
|
348
|
+
*/
|
|
349
|
+
export async function notify(title, { body = "", key = "", progress = null, button = "", done = false } = {}) {
|
|
350
|
+
const params = { title, body: body || "" };
|
|
351
|
+
if (key) params.key = key;
|
|
352
|
+
if (progress !== null && progress !== undefined) params.progress = progress;
|
|
353
|
+
if (button) {
|
|
354
|
+
params.button = button;
|
|
355
|
+
// The project the button starts: this one.
|
|
356
|
+
params.project = process.env.ATLAS_JOB || "";
|
|
357
|
+
}
|
|
358
|
+
if (done) params.done = true;
|
|
359
|
+
return atlas("phone-notify", params);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The button press this is about, or null.
|
|
364
|
+
*
|
|
365
|
+
* When a press STARTED this run it is in the payload: {key, button, title,
|
|
366
|
+
* pressed_at}. A script that was already running when the button was pressed
|
|
367
|
+
* (a listener, a long job) is not started twice; it passes the `key` it sent
|
|
368
|
+
* the button with and is told whether it has been pressed.
|
|
369
|
+
*
|
|
370
|
+
* const press = await phonePress(); // did a button start this run?
|
|
371
|
+
* if (await phonePress("backtest")) stop(); // still running: pressed yet?
|
|
372
|
+
*/
|
|
373
|
+
export async function phonePress(key = "") {
|
|
374
|
+
let started = null;
|
|
375
|
+
try {
|
|
376
|
+
started = JSON.parse(process.env.ATLAS_JOB_PAYLOAD || "{}").phone;
|
|
377
|
+
} catch {
|
|
378
|
+
started = null;
|
|
379
|
+
}
|
|
380
|
+
if (started && typeof started === "object" && (!key || started.key === key)) return started;
|
|
381
|
+
if (!key) return null;
|
|
382
|
+
const answer = await atlas("phone-button", { key });
|
|
383
|
+
return answer && answer.pressed
|
|
384
|
+
? { key, button: answer.button || "", pressed_at: answer.pressed_at }
|
|
385
|
+
: null;
|
|
386
|
+
}
|
|
387
|
+
|
|
332
388
|
/**
|
|
333
389
|
* Use this project's dashboard the way a person would.
|
|
334
390
|
*
|
|
@@ -508,6 +564,22 @@ export const computer = {
|
|
|
508
564
|
press: (target, opts = {}) => useComputer("press", [String(target)], opts),
|
|
509
565
|
typeInto: (target, text, opts = {}) => useComputer("type_into", [String(target), String(text)], opts),
|
|
510
566
|
waitFor: (target, opts = {}) => useComputer("wait_for", [String(target)], opts),
|
|
567
|
+
/**
|
|
568
|
+
* THE KEYS EVERY APP ALREADY ANSWERS TO. A keystroke needs no aiming, and
|
|
569
|
+
* these know this computer's keys (a Mac's are not Windows's), in any browser.
|
|
570
|
+
* await computer.shortcut("zoom_out", 2); // also back, next_tab, top, bottom, open_list...
|
|
571
|
+
* await computer.fillForm(["Ada", "Lovelace", { pick: "Canada" }, null, { key: "space" }], { submit: true });
|
|
572
|
+
* const page = await computer.readText(); // { text, chars, cut }
|
|
573
|
+
* await computer.openLink("Pricing"); // a link or a button, by its words
|
|
574
|
+
* fillForm starts where the focus is (press or click the first field), puts
|
|
575
|
+
* one value in each field with Tab between, and looks once at the end.
|
|
576
|
+
*/
|
|
577
|
+
shortcut: (name, times = 1) => useComputer("shortcut", [String(name), times]),
|
|
578
|
+
shortcuts: () => useComputer("shortcuts"),
|
|
579
|
+
fillForm: (values, opts = {}) => useComputer("fill_form", [values || []], opts),
|
|
580
|
+
pick: (option, opts = {}) => useComputer("pick", [String(option)], opts),
|
|
581
|
+
readText: (opts = {}) => useComputer("read_text", [], opts),
|
|
582
|
+
openLink: (words, opts = {}) => useComputer("open_link", [String(words)], opts),
|
|
511
583
|
click: (x, y) => useComputer("click", [x, y]),
|
|
512
584
|
doubleClick: (x, y) => useComputer("double_click", [x, y]),
|
|
513
585
|
rightClick: (x, y) => useComputer("right_click", [x, y]),
|