mindvest-atlas 0.7.0 → 0.8.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/api/client.js +7 -0
- package/src/commands/remote-control.js +50 -4
- package/src/config.js +1 -1
package/package.json
CHANGED
package/src/api/client.js
CHANGED
|
@@ -41,6 +41,13 @@ async function refreshAndStore(creds) {
|
|
|
41
41
|
return updated;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** The bearer a job uses to call the Atlas API directly. Exported so the runner
|
|
45
|
+
* can put it in a child's environment; empty when not signed in, because a
|
|
46
|
+
* missing token is a script's problem to report, not the runner's to crash on. */
|
|
47
|
+
export async function jobToken() {
|
|
48
|
+
try { return await currentAccessToken(); } catch { return ''; }
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
async function currentAccessToken() {
|
|
45
52
|
const env = envToken();
|
|
46
53
|
if (env) return env;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// a schedule that quietly broke, which is the failure the record exists to make
|
|
11
11
|
// visible.
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
-
import { API } from '../config.js';
|
|
14
|
-
import { requestJson } from '../api/client.js';
|
|
13
|
+
import { API, baseUrl } from '../config.js';
|
|
14
|
+
import { requestJson, jobToken } 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 {
|
|
@@ -107,6 +107,44 @@ async function handleAsk(ask, root) {
|
|
|
107
107
|
return answer(ask.request_id, { ok: false, error: 'Unknown request.' });
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* How many projects may run at once.
|
|
112
|
+
*
|
|
113
|
+
* Different projects are genuinely independent — own folder, own .env, own
|
|
114
|
+
* single run — so serialising them would make a five-minute job delay a
|
|
115
|
+
* five-second one for no reason. Capped so a burst of due schedules cannot
|
|
116
|
+
* fork-bomb a laptop.
|
|
117
|
+
*/
|
|
118
|
+
const MAX_PARALLEL = 4;
|
|
119
|
+
let active = 0;
|
|
120
|
+
const waiting = [];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run a job WITHOUT blocking the stream.
|
|
124
|
+
*
|
|
125
|
+
* Awaiting it inline was a real bug: a ten-minute script held the SSE loop, so
|
|
126
|
+
* no other job started, the keepalive stopped, and the web's questions about
|
|
127
|
+
* this computer went unanswered until it finished. The loop's job is to listen;
|
|
128
|
+
* the work belongs beside it.
|
|
129
|
+
*
|
|
130
|
+
* One run per PROJECT is still enforced by the marker inside runJob, so this
|
|
131
|
+
* parallelism is across projects only.
|
|
132
|
+
*/
|
|
133
|
+
function spawnJob(job, root, timeoutMs) {
|
|
134
|
+
const start = () => {
|
|
135
|
+
active += 1;
|
|
136
|
+
void runJob(job, root, timeoutMs)
|
|
137
|
+
.catch((err) => eprintln(color.red(`✗ ${err.message}`)))
|
|
138
|
+
.finally(() => {
|
|
139
|
+
active -= 1;
|
|
140
|
+
const next = waiting.shift();
|
|
141
|
+
if (next) next();
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
if (active < MAX_PARALLEL) start();
|
|
145
|
+
else waiting.push(start);
|
|
146
|
+
}
|
|
147
|
+
|
|
110
148
|
async function runJob(job, root, timeoutMs) {
|
|
111
149
|
const name = job.name || job.automation_id || 'job';
|
|
112
150
|
const started = Date.now();
|
|
@@ -150,6 +188,12 @@ async function runJob(job, root, timeoutMs) {
|
|
|
150
188
|
// Secrets first (shared .env, then the job's own), so a job's own env can
|
|
151
189
|
// deliberately override a shared key.
|
|
152
190
|
...jobEnv(dir, root),
|
|
191
|
+
// The job's own credentials for the Atlas API, so a script calls it directly
|
|
192
|
+
// instead of shelling out to the CLI once per ticker. The token is the SAME
|
|
193
|
+
// one this runner is signed in with — the job acts as its owner, which is
|
|
194
|
+
// the only identity it could sensibly have.
|
|
195
|
+
ATLAS_BASE_URL: baseUrl(),
|
|
196
|
+
ATLAS_TOKEN: await jobToken(),
|
|
153
197
|
...(job.env && typeof job.env === 'object'
|
|
154
198
|
? Object.fromEntries(Object.entries(job.env).map(([k, v]) => [String(k), String(v)])) : {}),
|
|
155
199
|
ATLAS_JOB_PAYLOAD: JSON.stringify(job.payload || {}),
|
|
@@ -205,12 +249,14 @@ export async function remoteControlCommand(args) {
|
|
|
205
249
|
}
|
|
206
250
|
const jobs = Array.isArray(pending) ? pending : (pending?.jobs || []);
|
|
207
251
|
if (jobs.length) eprintln(color.dim(`● ${jobs.length} job(s) queued while away`));
|
|
208
|
-
for (const job of jobs)
|
|
252
|
+
for (const job of jobs) spawnJob(job, root, timeoutMs);
|
|
209
253
|
},
|
|
210
254
|
onEvent: async ({ event, payload }) => {
|
|
211
255
|
if (event === 'job' || event === 'message' || event === 'run') {
|
|
212
|
-
|
|
256
|
+
spawnJob(payload, root, timeoutMs);
|
|
213
257
|
} else if (event === 'ask') {
|
|
258
|
+
// Questions ARE awaited: they are fast, and answering them in order
|
|
259
|
+
// keeps a reply matched to the request that asked for it.
|
|
214
260
|
await handleAsk(payload, root);
|
|
215
261
|
}
|
|
216
262
|
},
|
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.8.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';
|