mindvest-atlas 0.23.2 → 0.26.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 +14 -0
- package/src/commands/tools.js +35 -1
- package/src/config.js +1 -1
- package/src/helpers/atlas_env.js +153 -79
- package/src/helpers/atlas_env.py +1208 -1
- package/src/remote-control.js +39 -0
- package/src/sharing.js +52 -6
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
beatRunner,
|
|
23
23
|
setPaused, writeHelpers, askModel, listModels, chatModel, answerPermission,
|
|
24
24
|
createProject, ensureProjectFiles, listChats, readChat, deleteChat, appendChat,
|
|
25
|
+
computerUseAllowed,
|
|
25
26
|
readSchedule, cronMatches, dueMinutes, scheduleMatches, scheduleDueMinutes, writeNext,
|
|
26
27
|
SCRIPTS_DIR, projectsForAlert, readMode,
|
|
27
28
|
} from '../remote-control.js';
|
|
@@ -381,6 +382,17 @@ async function runJob(job, root, timeoutMs) {
|
|
|
381
382
|
ensureProjectFiles(dir);
|
|
382
383
|
|
|
383
384
|
eprintln(color.dim(`▶ ${name}`));
|
|
385
|
+
// MAY THIS RUN USE THE COMPUTER? Read per run, from this machine's own
|
|
386
|
+
// settings, so turning the switch off takes effect at the next job rather
|
|
387
|
+
// than at the next restart. The helper refuses without it and writes the
|
|
388
|
+
// refusal down; nothing here pretends that is a sandbox, only that there is
|
|
389
|
+
// one path that asks first and keeps a record.
|
|
390
|
+
const computerEnv = computerUseAllowed(root)
|
|
391
|
+
? {
|
|
392
|
+
ATLAS_COMPUTER_USE: '1',
|
|
393
|
+
ATLAS_COMPUTER_SESSION: new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19),
|
|
394
|
+
}
|
|
395
|
+
: {};
|
|
384
396
|
const env = {
|
|
385
397
|
...process.env,
|
|
386
398
|
// Secrets first (shared .env, then the job's own), so a job's own env can
|
|
@@ -398,6 +410,8 @@ async function runJob(job, root, timeoutMs) {
|
|
|
398
410
|
// one job's questions stay one conversation and do not blend into another's.
|
|
399
411
|
ATLAS_JOB: path.basename(dir),
|
|
400
412
|
ATLAS_JOB_PAYLOAD: JSON.stringify(job.payload || {}),
|
|
413
|
+
// Last, so a job's own env cannot switch this on for itself.
|
|
414
|
+
...computerEnv,
|
|
401
415
|
};
|
|
402
416
|
const r = await runOnce(argv, dir, env, timeoutMs);
|
|
403
417
|
const finished = Date.now();
|
package/src/commands/tools.js
CHANGED
|
@@ -11,6 +11,30 @@ function firstLine(desc) {
|
|
|
11
11
|
return '';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Commands this CLI has of its OWN. They are not in the account's tool catalog
|
|
15
|
+
// and never will be, so a search that finds nothing there should still say when
|
|
16
|
+
// one of these is what the person meant (owner, 2026-09-05: an agent concluded
|
|
17
|
+
// there was no screenshot tool while `atlas screenshot` was right there).
|
|
18
|
+
const LOCAL_COMMANDS = {
|
|
19
|
+
screenshot: "atlas screenshot --job <project> [--all] a picture of a project's dashboard, reviewed in words",
|
|
20
|
+
share: 'atlas share <state|publish|off|peek|install|updates> hand a project to someone, or take one in',
|
|
21
|
+
alerts: 'atlas alerts watch your alerts as they fire',
|
|
22
|
+
flow: 'atlas flow options flow, live',
|
|
23
|
+
'remote-control': "atlas remote-control run this computer's projects",
|
|
24
|
+
'historical-contract': "atlas historical-contract ... an option's past greeks",
|
|
25
|
+
'update-cli': 'atlas update-cli fetch the newest Atlas',
|
|
26
|
+
whoami: 'atlas whoami who this computer is signed in as',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Commands of this CLI whose name or line contains the search term. */
|
|
30
|
+
function localMatches(term) {
|
|
31
|
+
if (!term) return [];
|
|
32
|
+
return Object.entries(LOCAL_COMMANDS)
|
|
33
|
+
.filter(([name, line]) => name.includes(term) || line.toLowerCase().includes(term))
|
|
34
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
35
|
+
.map(([, line]) => line);
|
|
36
|
+
}
|
|
37
|
+
|
|
14
38
|
export async function toolsCommand(args) {
|
|
15
39
|
const flags = args.flags;
|
|
16
40
|
const catalog = await fetchCatalog({ force: !!flags.refresh });
|
|
@@ -38,7 +62,17 @@ export async function toolsCommand(args) {
|
|
|
38
62
|
for (const r of rows) {
|
|
39
63
|
println(' ' + color.cyan(r.slug.padEnd(width)) + color.dim(r.desc));
|
|
40
64
|
}
|
|
41
|
-
if (!rows.length)
|
|
65
|
+
if (!rows.length) {
|
|
66
|
+
println(color.dim(' (no tools match "' + filter + '")'));
|
|
67
|
+
// A COMMAND is not a tool, and looking for one in the catalog is an easy
|
|
68
|
+
// mistake to make: they are both things you type after `atlas`.
|
|
69
|
+
const local = localMatches(filter);
|
|
70
|
+
if (local.length) {
|
|
71
|
+
println('');
|
|
72
|
+
println(color.dim(' Not a tool, but this CLI can do it:'));
|
|
73
|
+
for (const line of local) println(' ' + line);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
42
76
|
println();
|
|
43
77
|
println(color.dim('Run ') + color.cyan('atlas <tool> --help') + color.dim(' for parameters.'));
|
|
44
78
|
return 0;
|
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.26.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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Secrets for this job. Written by Atlas — edits are overwritten.
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import url_ from "node:url";
|
|
5
5
|
//
|
|
6
6
|
// Values come from the environment Atlas set up before starting this script. Do
|
|
7
7
|
// NOT open .env: this module is the supported way to reach a secret, and the
|
|
@@ -58,7 +58,7 @@ export async function atlas(tool, params = {}) {
|
|
|
58
58
|
* starts up once, later questions can refer to earlier ones); fresh:true is for
|
|
59
59
|
* independent questions that should not pile into one context.
|
|
60
60
|
*/
|
|
61
|
-
export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600000
|
|
61
|
+
export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600000 } = {}) {
|
|
62
62
|
const url = process.env.ATLAS_ASK_URL;
|
|
63
63
|
const token = process.env.ATLAS_ASK_TOKEN;
|
|
64
64
|
if (!url || !token) {
|
|
@@ -70,12 +70,7 @@ export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600
|
|
|
70
70
|
const res = await fetch(url, {
|
|
71
71
|
method: "POST",
|
|
72
72
|
headers: { "Content-Type": "application/json", "X-Atlas-Token": token },
|
|
73
|
-
|
|
74
|
-
// whatever is connected rather than pinning itself to today's default.
|
|
75
|
-
body: JSON.stringify({
|
|
76
|
-
job: process.env.ATLAS_JOB || "", prompt: String(prompt), tools, fresh,
|
|
77
|
-
...(String(model || "").trim() ? { model: String(model).trim() } : {}),
|
|
78
|
-
}),
|
|
73
|
+
body: JSON.stringify({ job: process.env.ATLAS_JOB || "", prompt: String(prompt), tools, fresh }),
|
|
79
74
|
signal: AbortSignal.timeout(timeoutMs),
|
|
80
75
|
});
|
|
81
76
|
const out = await res.json().catch(() => ({}));
|
|
@@ -83,77 +78,6 @@ export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600
|
|
|
83
78
|
return out.answer || "";
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
/**
|
|
87
|
-
* A PICTURE OF THE DASHBOARD, AND WHAT IS WRONG WITH IT.
|
|
88
|
-
*
|
|
89
|
-
* You build a page you never see rendered: the thing that draws it is the Atlas
|
|
90
|
-
* app, not the browser the file opens in, and that gap is where the UI bugs
|
|
91
|
-
* live. Atlas MEASURES the render as well as photographing it and hands back
|
|
92
|
-
* what it found in words, because you cannot see a PNG (owner, 2026-09-04:
|
|
93
|
-
* "can the AI take a screenshot and review it — right now the UI is just
|
|
94
|
-
* guessing").
|
|
95
|
-
*
|
|
96
|
-
* await screenshot(); // computer, dark
|
|
97
|
-
* await screenshot({ phone: true }); // phone, dark
|
|
98
|
-
* await screenshot({ theme: "light" }); // computer, light
|
|
99
|
-
* await screenshot({ phone: true, theme: "light" });
|
|
100
|
-
*
|
|
101
|
-
* All four. The phone is where this is most likely to be broken and the view
|
|
102
|
-
* you are least able to picture.
|
|
103
|
-
*
|
|
104
|
-
* Returns { path, summary, findings, facts }. `summary` is the line to read;
|
|
105
|
-
* `broken` findings are faults, `style` findings are design rules this page is
|
|
106
|
-
* not following. PRINT THE SUMMARY so it lands in the run log, fix what it
|
|
107
|
-
* names, and take another. {} means the app was not reachable.
|
|
108
|
-
*/
|
|
109
|
-
export async function screenshot({ theme = "dark", phone = false, width, height, timeoutMs = 60000 } = {}) {
|
|
110
|
-
const url = process.env.ATLAS_ASK_URL;
|
|
111
|
-
const token = process.env.ATLAS_ASK_TOKEN;
|
|
112
|
-
if (!url || !token) return {};
|
|
113
|
-
const job = path.basename(path.dirname(fileURLToPath(import.meta.url)));
|
|
114
|
-
const body = { job, theme, phone: !!phone };
|
|
115
|
-
if (width) body.width = Number(width);
|
|
116
|
-
if (height) body.height = Number(height);
|
|
117
|
-
try {
|
|
118
|
-
const res = await fetch(url.replace("/ask", "/screenshot"), {
|
|
119
|
-
method: "POST",
|
|
120
|
-
headers: { "Content-Type": "application/json", "X-Atlas-Token": token },
|
|
121
|
-
body: JSON.stringify(body),
|
|
122
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
123
|
-
});
|
|
124
|
-
const out = await res.json().catch(() => ({}));
|
|
125
|
-
if (!res.ok || !out.ok) return {};
|
|
126
|
-
return {
|
|
127
|
-
path: out.path || "", summary: out.summary || "",
|
|
128
|
-
findings: out.findings || [], facts: out.facts || {},
|
|
129
|
-
};
|
|
130
|
-
} catch {
|
|
131
|
-
return {};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Every model this computer can reach.
|
|
137
|
-
*
|
|
138
|
-
* Returns [{ id, label }]. Pass an `id` to ask(..., { model }). An empty list
|
|
139
|
-
* means no model is connected, in which case ask() says so too.
|
|
140
|
-
*/
|
|
141
|
-
export async function models() {
|
|
142
|
-
const url = process.env.ATLAS_ASK_URL;
|
|
143
|
-
const token = process.env.ATLAS_ASK_TOKEN;
|
|
144
|
-
if (!url || !token) return [];
|
|
145
|
-
try {
|
|
146
|
-
const res = await fetch(url.replace("/ask", "/models"), {
|
|
147
|
-
headers: { "X-Atlas-Token": token },
|
|
148
|
-
signal: AbortSignal.timeout(20000),
|
|
149
|
-
});
|
|
150
|
-
const out = await res.json().catch(() => ({}));
|
|
151
|
-
return (out.models || []).filter((m) => m && m.id);
|
|
152
|
-
} catch {
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
81
|
/**
|
|
158
82
|
* Call a tool on one of YOUR connected MCP servers.
|
|
159
83
|
*
|
|
@@ -178,6 +102,156 @@ export async function mcpTools({ server = "", search = "" } = {}) {
|
|
|
178
102
|
return atlas("mcp-tools", { server, search });
|
|
179
103
|
}
|
|
180
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Use this project's dashboard the way a person would.
|
|
107
|
+
*
|
|
108
|
+
* screenshot() showed you what it LOOKS like. This lets you find out whether it
|
|
109
|
+
* WORKS: press the button, type in the box, look again at what happened. A
|
|
110
|
+
* layout fault is visible in a picture; a filter that returns nothing and a form
|
|
111
|
+
* that will not submit are not, and those are most of what is actually broken.
|
|
112
|
+
*
|
|
113
|
+
* await dashboard.open();
|
|
114
|
+
* for (const thing of await dashboard.find("button")) console.log(thing);
|
|
115
|
+
* await dashboard.click(x, y);
|
|
116
|
+
* await dashboard.type("AAPL");
|
|
117
|
+
* console.log((await dashboard.shot()).summary);
|
|
118
|
+
* await dashboard.close();
|
|
119
|
+
*
|
|
120
|
+
* It drives the dashboard and nothing else, so the person's real pointer never
|
|
121
|
+
* moves and they can carry on working while you click. Coordinates are inside
|
|
122
|
+
* the dashboard, from its top left, and they are not the same between shapes:
|
|
123
|
+
* reopening at { phone: true } draws a different layout.
|
|
124
|
+
*/
|
|
125
|
+
async function driveDashboard(op, body = {}) {
|
|
126
|
+
const url = process.env.ATLAS_ASK_URL || "";
|
|
127
|
+
const token = process.env.ATLAS_ASK_TOKEN || "";
|
|
128
|
+
if (!url || !token) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"The dashboard can only be driven from Atlas Remote Control, which is "
|
|
131
|
+
+ "what draws it. Run this project from the app.",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const job = path.basename(path.dirname(url_.fileURLToPath(import.meta.url)));
|
|
135
|
+
const res = await fetch(url.replace("/ask", "/dashboard-drive"), {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json", "X-Atlas-Token": token },
|
|
138
|
+
body: JSON.stringify({ ...body, job, op }),
|
|
139
|
+
});
|
|
140
|
+
const out = await res.json().catch(() => ({}));
|
|
141
|
+
if (!res.ok || out.ok === false) {
|
|
142
|
+
throw new Error(out.error || "The dashboard would not do that.");
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const dashboard = {
|
|
148
|
+
open: (opts = {}) => driveDashboard("open", {
|
|
149
|
+
phone: !!opts.phone,
|
|
150
|
+
theme: opts.theme || "dark",
|
|
151
|
+
width: opts.width,
|
|
152
|
+
height: opts.height,
|
|
153
|
+
}),
|
|
154
|
+
/** Where things are, without reading pixels. This is your own page: ask it. */
|
|
155
|
+
find: async (selector = "button, a, input, select, [role=button]") =>
|
|
156
|
+
(await driveDashboard("read", { selector })).found || [],
|
|
157
|
+
click: (x, y, opts = {}) => driveDashboard("click", {
|
|
158
|
+
x, y, button: opts.button || "left", count: opts.count || 1,
|
|
159
|
+
}),
|
|
160
|
+
doubleClick: (x, y) => driveDashboard("click", { x, y, count: 2 }),
|
|
161
|
+
move: (x, y) => driveDashboard("move", { x, y }),
|
|
162
|
+
scroll: (x, y, dx = 0, dy = -300) => driveDashboard("scroll", { x, y, dx, dy }),
|
|
163
|
+
type: (text) => driveDashboard("type", { text: String(text) }),
|
|
164
|
+
key: (...keys) => driveDashboard("key", { keys: keys.map(String) }),
|
|
165
|
+
/** A picture AND what is wrong with it, because you cannot see a PNG. */
|
|
166
|
+
shot: () => driveDashboard("shot"),
|
|
167
|
+
/** All four views, each measured: computer and phone, dark and light. */
|
|
168
|
+
check: async () => (await driveDashboard("check")).views || [],
|
|
169
|
+
close: () => driveDashboard("close"),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* See the screen, move the mouse, type: the whole computer, not just your page.
|
|
174
|
+
*
|
|
175
|
+
* Runs through the project's Python, which is where the machinery lives
|
|
176
|
+
* (pyautogui and the recording), so the switch, the one-run-at-a-time claim, the
|
|
177
|
+
* action log and standing back when somebody picks up the mouse are all exactly
|
|
178
|
+
* the same rules whichever language the script is written in. There is one
|
|
179
|
+
* implementation, not two that can disagree about what is allowed.
|
|
180
|
+
*
|
|
181
|
+
* await computer.screen(); // look FIRST, always
|
|
182
|
+
* await computer.click(x, y);
|
|
183
|
+
* await computer.type("hello");
|
|
184
|
+
* await computer.note("signing in");
|
|
185
|
+
*
|
|
186
|
+
* Off until the person turns it on, under This Computer in Atlas Remote
|
|
187
|
+
* Control. Read skills/computer-use before using any of it.
|
|
188
|
+
*/
|
|
189
|
+
async function useComputer(op, args = []) {
|
|
190
|
+
const { spawn } = await import("node:child_process");
|
|
191
|
+
const dir = path.dirname(url_.fileURLToPath(import.meta.url));
|
|
192
|
+
const py = process.env.ATLAS_PYTHON || (process.platform === "win32" ? "python" : "python3");
|
|
193
|
+
const code =
|
|
194
|
+
"import json,sys,atlas_env\n"
|
|
195
|
+
+ "op=sys.argv[1]; args=json.loads(sys.argv[2])\n"
|
|
196
|
+
+ "out=getattr(atlas_env,op)(*args)\n"
|
|
197
|
+
+ "sys.stdout.write('\\n__ATLAS__'+json.dumps(out,default=str))\n";
|
|
198
|
+
return await new Promise((done, fail) => {
|
|
199
|
+
const p = spawn(py, ["-c", code, op, JSON.stringify(args)], { cwd: dir });
|
|
200
|
+
let out = "", err = "";
|
|
201
|
+
p.stdout.on("data", (d) => { out += d; });
|
|
202
|
+
p.stderr.on("data", (d) => { err += d; });
|
|
203
|
+
p.on("error", () => fail(new Error(
|
|
204
|
+
"No Python on this computer, and using the computer runs through it. "
|
|
205
|
+
+ "Install Python 3, or use dashboard for work on your own page.",
|
|
206
|
+
)));
|
|
207
|
+
p.on("close", (codeOut) => {
|
|
208
|
+
if (codeOut !== 0) {
|
|
209
|
+
// The last line of a Python traceback is the message the helper wrote,
|
|
210
|
+
// and that message is the one worth showing: it says which switch to
|
|
211
|
+
// turn on or who is holding the mouse.
|
|
212
|
+
const lines = err.trim().split("\n").filter(Boolean);
|
|
213
|
+
const last = lines[lines.length - 1] || "That did not work.";
|
|
214
|
+
return fail(new Error(last.replace(/^\w*Error:\s*/, "")));
|
|
215
|
+
}
|
|
216
|
+
const mark = out.lastIndexOf("__ATLAS__");
|
|
217
|
+
if (mark < 0) return done(null);
|
|
218
|
+
try {
|
|
219
|
+
done(JSON.parse(out.slice(mark + 9)));
|
|
220
|
+
} catch {
|
|
221
|
+
done(null);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const computer = {
|
|
228
|
+
ready: () => useComputer("computer_ready"),
|
|
229
|
+
screen: () => useComputer("screen"),
|
|
230
|
+
region: (x, y, w, h) => useComputer("region", [x, y, w, h]),
|
|
231
|
+
screens: () => useComputer("screens"),
|
|
232
|
+
useScreen: (n) => useComputer("use_screen", [n]),
|
|
233
|
+
click: (x, y) => useComputer("click", [x, y]),
|
|
234
|
+
doubleClick: (x, y) => useComputer("double_click", [x, y]),
|
|
235
|
+
rightClick: (x, y) => useComputer("right_click", [x, y]),
|
|
236
|
+
move: (x, y) => useComputer("move", [x, y]),
|
|
237
|
+
drag: (x1, y1, x2, y2) => useComputer("drag", [x1, y1, x2, y2]),
|
|
238
|
+
scroll: (x, y, dx, dy) => useComputer("scroll", [x, y, dx, dy]),
|
|
239
|
+
type: (text) => useComputer("type", [String(text)]),
|
|
240
|
+
key: (...keys) => useComputer("key", keys.map(String)),
|
|
241
|
+
copy: (text) => useComputer("copy", [String(text)]),
|
|
242
|
+
focusWindow: (title) => useComputer("focus_window", [String(title)]),
|
|
243
|
+
cursor: () => useComputer("get_cursor"),
|
|
244
|
+
/** Say what you are doing. Somebody is watching a strip that shows this. */
|
|
245
|
+
note: (text) => useComputer("note", [String(text)]),
|
|
246
|
+
pauseRecording: (why = "") => useComputer("pause_recording", [String(why)]),
|
|
247
|
+
resumeRecording: () => useComputer("resume_recording"),
|
|
248
|
+
/** After a person used it by hand. Look again before you call this. */
|
|
249
|
+
takeOver: () => useComputer("take_over"),
|
|
250
|
+
release: () => useComputer("release_computer"),
|
|
251
|
+
/** Only when the person asked you to work inside Atlas itself. */
|
|
252
|
+
useAtlasApp: () => useComputer("use_atlas_app"),
|
|
253
|
+
};
|
|
254
|
+
|
|
181
255
|
/**
|
|
182
256
|
* Stop cleanly when a secret has not been filled in yet.
|
|
183
257
|
*
|