mindvest-atlas 0.8.0 → 0.9.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 +31 -6
- package/src/config.js +1 -1
- package/src/remote-control.js +62 -2
package/package.json
CHANGED
|
@@ -17,9 +17,10 @@ import { color, eprintln } from '../util/ui.js';
|
|
|
17
17
|
import {
|
|
18
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
|
+
setPaused, writeHelpers, askModel, listModels,
|
|
21
21
|
} from '../remote-control.js';
|
|
22
22
|
import fs from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
23
24
|
import path from 'node:path';
|
|
24
25
|
|
|
25
26
|
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d+Z$/, 'Z');
|
|
@@ -104,18 +105,39 @@ async function handleAsk(ask, root) {
|
|
|
104
105
|
}
|
|
105
106
|
if (ask.op === 'runs') return answer(ask.request_id, { ok: true, runs: listRuns(root) });
|
|
106
107
|
if (ask.op === 'read') return answer(ask.request_id, readJobFile(ask.job || '', ask.path || '', root));
|
|
108
|
+
if (ask.op === 'models') {
|
|
109
|
+
void listModels().then((m) => answer(ask.request_id, { ok: true, ...m }));
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
if (ask.op === 'build') {
|
|
113
|
+
// THE POINT OF ALL OF THIS. Words in, working script out: the owner says
|
|
114
|
+
// what they want in one sentence and their own model writes it, runs it,
|
|
115
|
+
// and fixes it here. No terminal, no questions asked back, no code shown
|
|
116
|
+
// unless they go looking. Deliberately NOT awaited — the model writes AND
|
|
117
|
+
// runs, which takes minutes, and the stream must keep serving everyone else.
|
|
118
|
+
void askModel(ask.prompt || '', { job: ask.job || '' })
|
|
119
|
+
.then((a) => answer(ask.request_id, { ok: true, answer: a }))
|
|
120
|
+
.catch((err) => answer(ask.request_id, { ok: false, error: String(err.message || err) }));
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
107
123
|
return answer(ask.request_id, { ok: false, error: 'Unknown request.' });
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
/**
|
|
111
127
|
* How many projects may run at once.
|
|
112
128
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
129
|
+
* Scaled to the machine rather than fixed at a timid number: these are mostly
|
|
130
|
+
* IO-bound jobs waiting on an API, so a modern laptop can carry far more than
|
|
131
|
+
* four, and holding a job back on an idle machine helps nobody. The floor of 4
|
|
132
|
+
* covers a single-core VM; the ceiling of 16 leaves the computer usable for the
|
|
133
|
+
* person sitting at it, which is the actual constraint — this is their machine,
|
|
134
|
+
* not a build server.
|
|
135
|
+
*
|
|
136
|
+
* This bounds RUNS, not threads. A single run is free to use every core it can
|
|
137
|
+
* (see the build prompt): one run per project is about not racing two copies of
|
|
138
|
+
* the same script over one folder, never about how hard that script may work.
|
|
117
139
|
*/
|
|
118
|
-
const MAX_PARALLEL = 4;
|
|
140
|
+
const MAX_PARALLEL = Math.max(4, Math.min(16, os.cpus().length || 4));
|
|
119
141
|
let active = 0;
|
|
120
142
|
const waiting = [];
|
|
121
143
|
|
|
@@ -196,6 +218,9 @@ async function runJob(job, root, timeoutMs) {
|
|
|
196
218
|
ATLAS_TOKEN: await jobToken(),
|
|
197
219
|
...(job.env && typeof job.env === 'object'
|
|
198
220
|
? Object.fromEntries(Object.entries(job.env).map(([k, v]) => [String(k), String(v)])) : {}),
|
|
221
|
+
// Which job is asking. The ask bridge keys the model's session on this, so
|
|
222
|
+
// one job's questions stay one conversation and do not blend into another's.
|
|
223
|
+
ATLAS_JOB: path.basename(dir),
|
|
199
224
|
ATLAS_JOB_PAYLOAD: JSON.stringify(job.payload || {}),
|
|
200
225
|
};
|
|
201
226
|
const r = await runOnce(argv, dir, env, timeoutMs);
|
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.8.
|
|
5
|
+
export const VERSION = '0.8.1';
|
|
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/remote-control.js
CHANGED
|
@@ -28,8 +28,15 @@ export const DEFAULT_ROOT = '~/AtlasStudio';
|
|
|
28
28
|
* so nothing has to be declared anywhere.
|
|
29
29
|
*/
|
|
30
30
|
export const ENTRIES = ['main.py', 'main.js', 'main.mjs'];
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Hard ceiling on one job, so a hung script cannot hold a slot forever.
|
|
33
|
+
*
|
|
34
|
+
* Half an hour, not ten minutes: real work — a chain walk, a wide scan, a model
|
|
35
|
+
* call in the middle — routinely runs past ten, and killing an honest job
|
|
36
|
+
* because the limit was set for a toy one is the worse failure. A script that is
|
|
37
|
+
* genuinely stuck is still stopped.
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_TIMEOUT_MS = 1_800_000;
|
|
33
40
|
|
|
34
41
|
export function workspaceRoot(override) {
|
|
35
42
|
const raw = override || process.env.ATLAS_LOCAL_DIR
|
|
@@ -481,3 +488,56 @@ export function listRuns(root, limit = 50) {
|
|
|
481
488
|
runs.sort((a, b) => b.started_at - a.started_at);
|
|
482
489
|
return runs.slice(0, limit);
|
|
483
490
|
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Hand a plain-English instruction to the model on THIS computer.
|
|
494
|
+
*
|
|
495
|
+
* This is how "watch SPY and buy the dip" becomes a working script without
|
|
496
|
+
* anyone opening a terminal: the words go straight to the owner's own model
|
|
497
|
+
* over ACP, which writes the script into the job's folder, runs it, reads its
|
|
498
|
+
* own errors and fixes them until it works.
|
|
499
|
+
*
|
|
500
|
+
* Returns the model's account of what it did. Throws if no model is connected.
|
|
501
|
+
*/
|
|
502
|
+
export async function askModel(prompt, { job = '', tools = true, fresh = false, timeoutMs = 900000 } = {}) {
|
|
503
|
+
const url = process.env.ATLAS_ASK_URL;
|
|
504
|
+
const token = process.env.ATLAS_ASK_TOKEN;
|
|
505
|
+
if (!url || !token) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
'No model is connected on this computer. Open Atlas Remote Control, '
|
|
508
|
+
+ 'connect a model, and turn Local Mode on.'
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const res = await fetch(url, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: { 'Content-Type': 'application/json', 'X-Atlas-Token': token },
|
|
514
|
+
body: JSON.stringify({ job, prompt: String(prompt), tools, fresh }),
|
|
515
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
516
|
+
});
|
|
517
|
+
const out = await res.json().catch(() => ({}));
|
|
518
|
+
if (!res.ok || !out.ok) throw new Error(out.error || `The model could not answer (${res.status}).`);
|
|
519
|
+
return out.answer || '';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Every model this computer can build with, and which one it uses by default.
|
|
524
|
+
*
|
|
525
|
+
* Names only. Returns an empty catalog rather than throwing when no model is
|
|
526
|
+
* connected, because "none yet" is a state the UI shows, not an error.
|
|
527
|
+
*/
|
|
528
|
+
export async function listModels(timeoutMs = 15000) {
|
|
529
|
+
const url = process.env.ATLAS_ASK_URL;
|
|
530
|
+
const token = process.env.ATLAS_ASK_TOKEN;
|
|
531
|
+
const empty = { models: [], default: null, agent: null };
|
|
532
|
+
if (!url || !token) return empty;
|
|
533
|
+
try {
|
|
534
|
+
const res = await fetch(url.replace('/ask', '/models'), {
|
|
535
|
+
headers: { 'X-Atlas-Token': token },
|
|
536
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
537
|
+
});
|
|
538
|
+
if (!res.ok) return empty;
|
|
539
|
+
return await res.json();
|
|
540
|
+
} catch {
|
|
541
|
+
return empty;
|
|
542
|
+
}
|
|
543
|
+
}
|