mindvest-atlas 0.96.1 → 0.98.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/access.js +113 -0
- package/src/cli.js +15 -1
- package/src/commands/install.js +147 -0
- package/src/commands/remote-control.js +253 -25
- package/src/commands/run.js +10 -1
- package/src/config.js +1 -1
- package/src/helpers/atlas_env.js +84 -7
- package/src/helpers/atlas_env.py +772 -7
- package/src/remote-control.js +220 -10
- package/src/sharing.js +34 -0
- package/src/table.js +218 -0
- package/src/util/help.js +3 -0
package/package.json
CHANGED
package/src/access.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// May this account use Atlas on this computer right now?
|
|
2
|
+
//
|
|
3
|
+
// OWNER, 2026-10-14: "every user must have at least pro plan. first time user
|
|
4
|
+
// can use studio free for 1 week ... it does not matter if the user never used
|
|
5
|
+
// atlas tool at all. we still have web control relay network etc." and, of the
|
|
6
|
+
// command line: "not just command line, if they don't pay, they do not see",
|
|
7
|
+
// "no runs ever runs and no new orders".
|
|
8
|
+
//
|
|
9
|
+
// The server decides (POST /api/computer/access): a paid plan, or the one free
|
|
10
|
+
// week, which it starts the first time an account uses a computer and keeps
|
|
11
|
+
// against the account (in its own settings), never here. This file only asks, keeps
|
|
12
|
+
// the last answer, and lets a computer that cannot reach Atlas carry on until
|
|
13
|
+
// the `good_until` the server gave it (two days after a paid plan was last
|
|
14
|
+
// seen, the end of the free week otherwise).
|
|
15
|
+
//
|
|
16
|
+
// What it gates is decided by its callers: STARTING a run. Nothing here stops
|
|
17
|
+
// a run already going, and nothing closes, cancels or watches less.
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { configDir } from './config.js';
|
|
21
|
+
import { requestJson, AuthError } from './api/client.js';
|
|
22
|
+
|
|
23
|
+
export const ACCESS_PATH = '/api/computer/access';
|
|
24
|
+
export const SUBSCRIBE_URL = 'https://mind-vest.io/atlas/subscribe';
|
|
25
|
+
const FILE = 'access.json';
|
|
26
|
+
// Asked at most this often while the answer is yes; a no is asked again sooner,
|
|
27
|
+
// so somebody who has just paid is not kept waiting.
|
|
28
|
+
export const FRESH_MS = 10 * 60_000;
|
|
29
|
+
export const FRESH_NO_MS = 60_000;
|
|
30
|
+
|
|
31
|
+
export const LOCKED_LINE =
|
|
32
|
+
`Your free week has ended. Pick a plan in your account to run things on this computer again: ${SUBSCRIBE_URL}`;
|
|
33
|
+
|
|
34
|
+
function file() {
|
|
35
|
+
return path.join(configDir(), FILE);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readAccess() {
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(fs.readFileSync(file(), 'utf8'));
|
|
41
|
+
return raw && typeof raw === 'object' ? raw : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeAccess(rec) {
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
50
|
+
fs.writeFileSync(file(), JSON.stringify(rec, null, 2));
|
|
51
|
+
} catch { /* the answer still holds for this process */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The server's answer, reduced to the fields this file trusts. */
|
|
55
|
+
export function fromServer(data, now = Date.now()) {
|
|
56
|
+
const d = data && typeof data === 'object' ? data : {};
|
|
57
|
+
const state = ['paid', 'trial', 'ended'].includes(d.state) ? d.state : 'unknown';
|
|
58
|
+
const good = Date.parse(d.good_until || '');
|
|
59
|
+
return {
|
|
60
|
+
allowed: d.allowed === true,
|
|
61
|
+
state,
|
|
62
|
+
trial_ends_at: typeof d.trial_ends_at === 'string' ? d.trial_ends_at : null,
|
|
63
|
+
days_left: Number.isFinite(d.days_left) ? d.days_left : null,
|
|
64
|
+
good_until: Number.isFinite(good) ? new Date(good).toISOString() : null,
|
|
65
|
+
checked_at: new Date(now).toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Pure: what a kept answer means at `now`. A yes holds until its good_until;
|
|
71
|
+
* past that it is a no until the server is asked again. Nothing kept is a no,
|
|
72
|
+
* with its own sentence: the first answer needs the internet.
|
|
73
|
+
*/
|
|
74
|
+
export function verdict(rec, now = Date.now()) {
|
|
75
|
+
if (!rec) {
|
|
76
|
+
return { allowed: false, state: 'unknown',
|
|
77
|
+
message: 'Connect this computer to the internet once so Atlas can check your plan.' };
|
|
78
|
+
}
|
|
79
|
+
const until = Date.parse(rec.good_until || '');
|
|
80
|
+
const allowed = rec.allowed === true && Number.isFinite(until) && now < until;
|
|
81
|
+
return { ...rec, allowed, message: allowed ? null : LOCKED_LINE };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function fresh(rec, now) {
|
|
85
|
+
const at = Date.parse(rec?.checked_at || '');
|
|
86
|
+
if (!Number.isFinite(at)) return false;
|
|
87
|
+
return now - at < (rec.allowed ? FRESH_MS : FRESH_NO_MS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ask (or reuse a recent answer). `ask` is for tests.
|
|
92
|
+
*
|
|
93
|
+
* A server that does not know the question (404: this CLI is newer than the
|
|
94
|
+
* server) is a yes: a release must never lock everybody out before the server
|
|
95
|
+
* that decides has gone live. Any other failure falls back to the kept answer.
|
|
96
|
+
*/
|
|
97
|
+
export async function computerAccess({ now = Date.now(), ask = requestJson, force = false } = {}) {
|
|
98
|
+
const kept = readAccess();
|
|
99
|
+
if (!force && kept && fresh(kept, now)) return verdict(kept, now);
|
|
100
|
+
try {
|
|
101
|
+
// Nothing about this computer is sent: only who is asking (the sign-in).
|
|
102
|
+
const data = await ask(ACCESS_PATH, { method: 'POST', body: {} });
|
|
103
|
+
const rec = fromServer(data, now);
|
|
104
|
+
writeAccess(rec);
|
|
105
|
+
return verdict(rec, now);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err && err.status === 404) return { allowed: true, state: 'unknown', message: null };
|
|
108
|
+
if (err instanceof AuthError && !verdict(kept, now).allowed) {
|
|
109
|
+
return { allowed: false, state: 'unknown', message: 'Sign in first: atlas login' };
|
|
110
|
+
}
|
|
111
|
+
return verdict(kept, now);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,8 @@ import { remoteControlCommand } from './commands/remote-control.js';
|
|
|
18
18
|
import { updateCliCommand } from './commands/update-cli.js';
|
|
19
19
|
import { screenshotCommand } from './commands/screenshot.js';
|
|
20
20
|
import { runCommand } from './commands/run.js';
|
|
21
|
+
import { computerAccess } from './access.js';
|
|
22
|
+
import { installCommand } from './commands/install.js';
|
|
21
23
|
import { computerUseCommand } from './commands/computer-use.js';
|
|
22
24
|
import { dashboardCommand } from './commands/dashboard.js';
|
|
23
25
|
import { chatSearchCommand } from './commands/chat-search.js';
|
|
@@ -119,8 +121,20 @@ export async function main(rawArgs) {
|
|
|
119
121
|
// RUN SOMETHING INSIDE A PROJECT, which had no entry point at all: an
|
|
120
122
|
// agent that had correctly worked out atlas_env only exists in project
|
|
121
123
|
// code then had nowhere to try it (owner, 2026-09-05).
|
|
122
|
-
|
|
124
|
+
// LIBRARIES FOR THIS COMPUTER'S PROJECTS (owner, 2026-10-14), one shared
|
|
125
|
+
// environment under the Atlas folder; Studio's Libraries tab uses it too.
|
|
126
|
+
case 'install':
|
|
127
|
+
return await installCommand(withoutCommand(args));
|
|
128
|
+
case 'run': {
|
|
129
|
+
// A run on this computer needs a plan (or the free week): asked
|
|
130
|
+
// before anything starts, never in the middle of one.
|
|
131
|
+
const access = await computerAccess();
|
|
132
|
+
if (!access.allowed) {
|
|
133
|
+
eprintln(color.yellow(access.message));
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
123
136
|
return runCommand(withoutCommand(args));
|
|
137
|
+
}
|
|
124
138
|
// SEEING AND USING THIS COMPUTER, from a shell. It existed only in the
|
|
125
139
|
// Python package, and Atlas Studio bundles THIS one, so the app
|
|
126
140
|
// answered "Unknown command or tool: computer-use" on the very machine
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// `atlas install`: Python libraries for this computer's projects, installed on
|
|
2
|
+
// request (owner, 2026-10-14: "let there be local atlas cli (install - this)
|
|
3
|
+
// which downloads, but users can also install this too").
|
|
4
|
+
//
|
|
5
|
+
// One shared environment under the Atlas folder (`.libraries`), which every
|
|
6
|
+
// project without its own venv runs with (remote-control.js venvPython). It is
|
|
7
|
+
// made the first time something is installed. `uv` is used when it is on this
|
|
8
|
+
// computer (much faster), pip otherwise. Atlas Studio's Libraries tab drives
|
|
9
|
+
// this same command with --json, so there is one way to install.
|
|
10
|
+
//
|
|
11
|
+
// atlas install <name>[==version] ... install (or bring up to date)
|
|
12
|
+
// atlas install --update <name> ... the newest version
|
|
13
|
+
// atlas install --remove <name> ... take it out
|
|
14
|
+
// atlas install --list [--latest] what is installed (and what is newer)
|
|
15
|
+
// --json one JSON line, for a program
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import { color, println, eprintln } from '../util/ui.js';
|
|
19
|
+
import { ambientPython, envPython, librariesVenv, whichSync } from '../remote-control.js';
|
|
20
|
+
|
|
21
|
+
/** A package as pip spells it, with nothing that could be read as an option. */
|
|
22
|
+
export const PACKAGE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*(\[[A-Za-z0-9,._-]+\])?((==|>=|<=|~=)[A-Za-z0-9.*+!_-]+)?$/;
|
|
23
|
+
|
|
24
|
+
export function packageName(spec) {
|
|
25
|
+
return String(spec).replace(/\[.*$/, '').replace(/(==|>=|<=|~=).*$/, '').toLowerCase().replace(/_/g, '-');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MINUTES = 60_000;
|
|
29
|
+
|
|
30
|
+
function run(cmd, args, timeoutMs) {
|
|
31
|
+
const out = spawnSync(cmd, args, { encoding: 'utf8', timeout: timeoutMs, windowsHide: true, maxBuffer: 32 * 1024 * 1024 });
|
|
32
|
+
return { code: out.status ?? 1, stdout: out.stdout || '', stderr: out.stderr || '', error: out.error };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The shared environment, made if it is not there yet. */
|
|
36
|
+
export function ensureEnv(root) {
|
|
37
|
+
const dir = librariesVenv(root);
|
|
38
|
+
const py = envPython(dir);
|
|
39
|
+
if (fs.existsSync(py)) return { ok: true, python: py, dir, made: false };
|
|
40
|
+
const uv = whichSync('uv');
|
|
41
|
+
const made = uv
|
|
42
|
+
? run(uv, ['venv', dir], 5 * MINUTES)
|
|
43
|
+
: run(ambientPython()[0], [...ambientPython().slice(1), '-m', 'venv', dir], 5 * MINUTES);
|
|
44
|
+
if (made.code !== 0 || !fs.existsSync(py)) {
|
|
45
|
+
return { ok: false, error: `Could not make the libraries folder: ${(made.stderr || made.error?.message || '').trim().split('\n').pop() || 'Python is not on this computer.'}` };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, python: py, dir, made: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pipArgs(py, verb, extra) {
|
|
51
|
+
const uv = whichSync('uv');
|
|
52
|
+
if (uv) return [uv, ['pip', verb, '--python', py, ...extra]];
|
|
53
|
+
if (verb === 'uninstall') extra = ['-y', ...extra];
|
|
54
|
+
return [py, ['-m', 'pip', verb, '--disable-pip-version-check', ...extra]];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What is installed: [{name, version}]. */
|
|
58
|
+
export function installed(root) {
|
|
59
|
+
const py = envPython(librariesVenv(root));
|
|
60
|
+
if (!fs.existsSync(py)) return [];
|
|
61
|
+
const [cmd, args] = pipArgs(py, 'list', ['--format', 'json']);
|
|
62
|
+
const out = run(cmd, args, 2 * MINUTES);
|
|
63
|
+
try {
|
|
64
|
+
const rows = JSON.parse(out.stdout || '[]');
|
|
65
|
+
return rows.map((r) => ({ name: String(r.name), version: String(r.version) }));
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The newest version and one line about a package, from PyPI (null when unknown). */
|
|
72
|
+
export async function latest(name, fetchImpl = fetch) {
|
|
73
|
+
try {
|
|
74
|
+
const r = await fetchImpl(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`, { signal: AbortSignal.timeout(15_000) });
|
|
75
|
+
if (!r.ok) return null;
|
|
76
|
+
const d = await r.json();
|
|
77
|
+
return { version: String(d?.info?.version || ''), summary: String(d?.info?.summary || '').slice(0, 200) };
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function installCommand(args) {
|
|
84
|
+
// A switch followed by a name takes the name as its value (`--remove pandas`):
|
|
85
|
+
// the switch is on, and the name is one of the names.
|
|
86
|
+
const flags = { ...(args.flags || {}) };
|
|
87
|
+
const names = (args.positional || args._ || []).map(String).filter(Boolean);
|
|
88
|
+
for (const k of ['remove', 'update', 'list', 'latest', 'json']) {
|
|
89
|
+
const v = flags[k];
|
|
90
|
+
for (const one of Array.isArray(v) ? v : [v]) if (typeof one === 'string' && one) names.push(one);
|
|
91
|
+
if (v !== undefined && v !== false) flags[k] = true;
|
|
92
|
+
}
|
|
93
|
+
const json = !!flags.json;
|
|
94
|
+
const say = (obj, human) => {
|
|
95
|
+
if (json) println(JSON.stringify(obj));
|
|
96
|
+
else human();
|
|
97
|
+
return obj.ok ? 0 : 1;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (flags.list) {
|
|
101
|
+
const list = installed();
|
|
102
|
+
if (flags.latest) {
|
|
103
|
+
for (const p of list) {
|
|
104
|
+
const l = await latest(p.name);
|
|
105
|
+
if (l?.version) p.latest = l.version;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return say({ ok: true, installed: list }, () => {
|
|
109
|
+
if (!list.length) return println(color.dim('Nothing installed yet. `atlas install <name>`'));
|
|
110
|
+
for (const p of list) {
|
|
111
|
+
const newer = p.latest && p.latest !== p.version ? color.yellow(` ${p.latest} is out`) : '';
|
|
112
|
+
println(`${p.name} ${color.dim(p.version)}${newer}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!names.length) {
|
|
118
|
+
eprintln(color.red('Which library? `atlas install pandas`'));
|
|
119
|
+
eprintln(color.dim(' --update <name> the newest version'));
|
|
120
|
+
eprintln(color.dim(' --remove <name> take it out'));
|
|
121
|
+
eprintln(color.dim(' --list what is installed'));
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
const bad = names.find((n) => !PACKAGE_RE.test(n));
|
|
125
|
+
if (bad) return say({ ok: false, error: `"${bad}" is not a library name.` }, () => eprintln(color.red(`"${bad}" is not a library name.`)));
|
|
126
|
+
|
|
127
|
+
const env = ensureEnv();
|
|
128
|
+
if (!env.ok) return say(env, () => eprintln(color.red(env.error)));
|
|
129
|
+
|
|
130
|
+
const verb = flags.remove ? 'uninstall' : 'install';
|
|
131
|
+
const extra = flags.remove ? names : [...(flags.update ? ['--upgrade'] : []), ...names];
|
|
132
|
+
if (!json) eprintln(color.dim(`● ${flags.remove ? 'Removing' : 'Installing'} ${names.join(', ')}…`));
|
|
133
|
+
const [cmd, cmdArgs] = pipArgs(env.python, verb, extra);
|
|
134
|
+
const out = run(cmd, cmdArgs, 20 * MINUTES);
|
|
135
|
+
if (out.code !== 0) {
|
|
136
|
+
const why = (out.stderr || out.stdout || out.error?.message || '').trim().split('\n').filter(Boolean).pop() || 'it did not finish';
|
|
137
|
+
return say({ ok: false, error: `Could not ${flags.remove ? 'remove' : 'install'} ${names.join(', ')}: ${why}` },
|
|
138
|
+
() => eprintln(color.red(`✗ ${why}`)));
|
|
139
|
+
}
|
|
140
|
+
const now = installed();
|
|
141
|
+
const wanted = new Set(names.map(packageName));
|
|
142
|
+
const these = now.filter((p) => wanted.has(packageName(p.name)));
|
|
143
|
+
return say({ ok: true, installed: these, removed: flags.remove ? names.map(packageName) : undefined }, () => {
|
|
144
|
+
if (flags.remove) println(color.green(`✓ removed ${names.join(', ')}`));
|
|
145
|
+
else for (const p of these) println(color.green(`✓ ${p.name} ${p.version}`));
|
|
146
|
+
});
|
|
147
|
+
}
|