mindvest-atlas 0.96.1 → 0.97.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 +13 -2
- package/src/config.js +1 -1
- package/src/helpers/atlas_env.js +84 -7
- package/src/helpers/atlas_env.py +116 -7
- package/src/remote-control.js +68 -2
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
takeActionCommands, writeActionReply, actionArgv, runAction, actionHandler, claimAsk, REPLIES_DIR, COMMANDS_DIR,
|
|
22
22
|
writeCommand,
|
|
23
23
|
beatRunner,
|
|
24
|
-
setPaused, writeHelpers, askModel, listModels, chatModel, answerPermission,
|
|
24
|
+
setPaused, writeHelpers, askModel, listModels, chatModel, answerPermission, stopChat, chatState,
|
|
25
25
|
createProject, ensureProjectFiles, listChats, readChat, deleteChat, appendChat,
|
|
26
26
|
computerUseAllowed, listComputerSessions, readComputerActions, listComputerVideo, readComputerVideoPart,
|
|
27
27
|
sessionReplay, venvPython, ambientPython,
|
|
@@ -431,7 +431,7 @@ async function handleAsk(ask, root) {
|
|
|
431
431
|
};
|
|
432
432
|
const modelUsed = ask.model || '';
|
|
433
433
|
if (job && chatId) appendChat(job, chatId, 'user', ask.prompt || '', root, undefined, modelUsed);
|
|
434
|
-
void chatModel(ask.prompt || '', send, { job, model: ask.model || '', chatId })
|
|
434
|
+
void chatModel(ask.prompt || '', send, { job, model: ask.model || '', chatId, instead: ask.instead === true })
|
|
435
435
|
.catch((err) => send({ kind: 'error', message: String(err?.message || err) }))
|
|
436
436
|
.finally(async () => {
|
|
437
437
|
if (job && chatId) appendChat(job, chatId, 'assistant', said.join(''), root, undefined, modelUsed);
|
|
@@ -442,6 +442,17 @@ async function handleAsk(ask, root) {
|
|
|
442
442
|
});
|
|
443
443
|
return undefined;
|
|
444
444
|
}
|
|
445
|
+
if (ask.op === 'chat_stop') {
|
|
446
|
+
// Stop, pressed on the web or said to the voice there: its own ask, like
|
|
447
|
+
// an answer to a question, because the chat stream is one-way.
|
|
448
|
+
void stopChat(ask.job || '', ask.chat_id || '', { computer: ask.computer === true, ahead: ask.ahead === true }).then((r) => answer(ask.request_id, r));
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
if (ask.op === 'chat_state') {
|
|
452
|
+
// Is a run going in that conversation: asked by a page that did not start it.
|
|
453
|
+
void chatState(ask.job || '', ask.chat_id || '').then((r) => answer(ask.request_id, r));
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
445
456
|
if (ask.op === 'permission') {
|
|
446
457
|
// The person's answer to a question the model asked mid-turn. Its own ask,
|
|
447
458
|
// not part of the chat stream: the stream is one-way. `permission_id` is
|
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.97.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
|
@@ -2,16 +2,93 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import url_ from "node:url";
|
|
5
|
+
import os_ from "node:os";
|
|
6
|
+
import childProcess_ from "node:child_process";
|
|
5
7
|
//
|
|
6
8
|
// Values come from the environment Atlas set up before starting this script. Do
|
|
7
9
|
// NOT open .env: this module is the supported way to reach a secret, and the
|
|
8
10
|
// file itself is not something a script or a model needs to read.
|
|
9
11
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* What this project's own .env, and the shared one above it, say NOW.
|
|
14
|
+
*
|
|
15
|
+
* A script Atlas starts has every key in its environment already. Two cases
|
|
16
|
+
* did not, and both were told a key "is not filled in" while it sat in the
|
|
17
|
+
* file: a listener that had been running since before the key was typed in,
|
|
18
|
+
* and a script run by hand, by its owner or by a model working in the project.
|
|
19
|
+
* The file is read HERE, in the one module that is the supported way to reach
|
|
20
|
+
* a secret. Nothing is printed and nothing is kept.
|
|
21
|
+
*/
|
|
22
|
+
function fileSecrets() {
|
|
23
|
+
const out = {};
|
|
24
|
+
const here = path.dirname(url_.fileURLToPath(import.meta.url));
|
|
25
|
+
let root = process.env.ATLAS_LIBRARY_DIR || process.env.ATLAS_LOCAL_DIR || path.dirname(path.dirname(here));
|
|
26
|
+
if (root.startsWith("~")) root = path.join(os_.homedir(), root.slice(1));
|
|
27
|
+
// The shared file first, then the project's own on top: the runner's order.
|
|
28
|
+
for (const file of [path.join(root, ".env"), path.join(here, ".env")]) {
|
|
29
|
+
let text;
|
|
30
|
+
try { text = fs.readFileSync(file, "utf8"); } catch { continue; }
|
|
31
|
+
for (const raw of text.split("\n")) {
|
|
32
|
+
const line = raw.trim();
|
|
33
|
+
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
34
|
+
const at = line.indexOf("=");
|
|
35
|
+
const k = line.slice(0, at).trim();
|
|
36
|
+
let v = line.slice(at + 1).trim();
|
|
37
|
+
if (v.length >= 2 && v[0] === v[v.length - 1] && (v[0] === String.fromCharCode(34) || v[0] === String.fromCharCode(39))) v = v.slice(1, -1);
|
|
38
|
+
if (k) out[k] = v;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The value of one secret, for USE — never for printing or logging. From the
|
|
45
|
+
* environment Atlas started this script with; when it is not there, from the
|
|
46
|
+
* project's .env as it stands now. */
|
|
47
|
+
export const secret = (name, fallback) => process.env[name] || fileSecrets()[name] || fallback;
|
|
12
48
|
|
|
13
49
|
/** Whether a secret has been filled in. True/false only, never the value. */
|
|
14
|
-
export const hasSecret = (name) => Boolean(
|
|
50
|
+
export const hasSecret = (name) => Boolean(secret(name));
|
|
51
|
+
|
|
52
|
+
/** This computer's own Atlas sign-in, as the atlas CLI keeps it. */
|
|
53
|
+
function signedIn() {
|
|
54
|
+
const dir = process.env.ATLAS_CONFIG_DIR
|
|
55
|
+
|| (process.env.XDG_CONFIG_HOME ? path.join(process.env.XDG_CONFIG_HOME, "atlas") : path.join(os_.homedir(), ".config", "atlas"));
|
|
56
|
+
try {
|
|
57
|
+
const found = JSON.parse(fs.readFileSync(path.join(dir, "credentials.json"), "utf8"));
|
|
58
|
+
return found && typeof found === "object" ? found : {};
|
|
59
|
+
} catch { return {}; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The bearer this script calls Atlas with. Never print it or pass it on.
|
|
64
|
+
*
|
|
65
|
+
* Started by Atlas, a script is handed it in its environment. RUN BY HAND (by
|
|
66
|
+
* its owner in a terminal, or by a model working in the project) it had none,
|
|
67
|
+
* and every call failed with "no Atlas token" although the person was signed
|
|
68
|
+
* in on this very computer. It now uses that sign-in: the file the atlas CLI
|
|
69
|
+
* itself keeps, readable only by this computer's own user, which is the same
|
|
70
|
+
* person a script started by Atlas acts as.
|
|
71
|
+
*
|
|
72
|
+
* What this does NOT do, on purpose: no token is written into the project,
|
|
73
|
+
* none is printed, none is copied anywhere. Somebody on another computer, or
|
|
74
|
+
* another user of this one, has no such file and is refused as before. A
|
|
75
|
+
* sign-in that has run out is renewed by the CLI itself (its output thrown
|
|
76
|
+
* away), never here.
|
|
77
|
+
*/
|
|
78
|
+
function atlasToken() {
|
|
79
|
+
if (process.env.ATLAS_TOKEN) return process.env.ATLAS_TOKEN;
|
|
80
|
+
const usable = (found) => (found.access_token && (!found.expires_at || Date.now() < Number(found.expires_at) - 60000)
|
|
81
|
+
? String(found.access_token) : "");
|
|
82
|
+
const token = usable(signedIn());
|
|
83
|
+
if (token) return token;
|
|
84
|
+
try {
|
|
85
|
+
childProcess_.execFileSync("atlas", ["whoami"], { stdio: "ignore", timeout: 30000, shell: process.platform === "win32" });
|
|
86
|
+
} catch { /* not signed in, or no atlas on this computer */ }
|
|
87
|
+
return usable(signedIn());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const NO_TOKEN = "This job has no Atlas token. Is Local Mode signed in? "
|
|
91
|
+
+ "(Run by hand, a script uses the sign-in on this computer: run `atlas login` once.)";
|
|
15
92
|
|
|
16
93
|
/**
|
|
17
94
|
* Call an Atlas tool over the REST API. Returns the parsed JSON.
|
|
@@ -28,8 +105,8 @@ export const hasSecret = (name) => Boolean(process.env[name]);
|
|
|
28
105
|
*/
|
|
29
106
|
export async function atlas(tool, params = {}) {
|
|
30
107
|
const base = (process.env.ATLAS_BASE_URL || "https://atlasmcp.finmanagerai.com").replace(/\/+$/, "");
|
|
31
|
-
const token =
|
|
32
|
-
if (!token) throw new Error(
|
|
108
|
+
const token = atlasToken();
|
|
109
|
+
if (!token) throw new Error(NO_TOKEN);
|
|
33
110
|
const res = await fetch(`${base}/api/v1/tools/${tool}`, {
|
|
34
111
|
method: "POST",
|
|
35
112
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
@@ -232,8 +309,8 @@ function alertMatches(event, match) {
|
|
|
232
309
|
*/
|
|
233
310
|
export async function* alerts({ match = null, timeoutMs = null, reconnect = true } = {}) {
|
|
234
311
|
const base = (process.env.ATLAS_BASE_URL || "https://atlasmcp.finmanagerai.com").replace(/\/+$/, "");
|
|
235
|
-
const token =
|
|
236
|
-
if (!token) throw new Error(
|
|
312
|
+
const token = atlasToken();
|
|
313
|
+
if (!token) throw new Error(NO_TOKEN);
|
|
237
314
|
const deadline = timeoutMs == null ? null : Date.now() + Number(timeoutMs);
|
|
238
315
|
let attempt = 0;
|
|
239
316
|
while (deadline == null || Date.now() < deadline) {
|
package/src/helpers/atlas_env.py
CHANGED
|
@@ -8,14 +8,123 @@ import os
|
|
|
8
8
|
import sys
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def _file_secrets():
|
|
12
|
+
"""What this project's own .env, and the shared one above it, say NOW.
|
|
13
|
+
|
|
14
|
+
A script Atlas starts has every key in its environment already. Two cases
|
|
15
|
+
did not, and both were told a key "is not filled in" while it sat in the
|
|
16
|
+
file: a listener that had been running since before the key was typed in
|
|
17
|
+
(its environment is the one it started with), and a script run by hand, by
|
|
18
|
+
its owner or by a model working in the project. So one script could say yes
|
|
19
|
+
and another no about the same key at the same moment.
|
|
20
|
+
|
|
21
|
+
The file is read HERE, in the one module that is the supported way to reach
|
|
22
|
+
a secret. Nothing is printed, nothing is kept, and a script or a model
|
|
23
|
+
still has no reason to open .env itself.
|
|
24
|
+
"""
|
|
25
|
+
out = {}
|
|
26
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
root = (os.environ.get("ATLAS_LIBRARY_DIR") or os.environ.get("ATLAS_LOCAL_DIR")
|
|
28
|
+
or os.path.dirname(os.path.dirname(here)))
|
|
29
|
+
quotes = chr(34) + chr(39)
|
|
30
|
+
# The shared file first, then the project's own on top: the runner's order.
|
|
31
|
+
for path in (os.path.join(os.path.expanduser(root), ".env"), os.path.join(here, ".env")):
|
|
32
|
+
try:
|
|
33
|
+
with open(path, encoding="utf-8") as f:
|
|
34
|
+
text = f.read()
|
|
35
|
+
except Exception:
|
|
36
|
+
continue
|
|
37
|
+
for raw in text.split("\n"):
|
|
38
|
+
line = raw.strip()
|
|
39
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
40
|
+
continue
|
|
41
|
+
k, v = line.split("=", 1)
|
|
42
|
+
k, v = k.strip(), v.strip()
|
|
43
|
+
if len(v) >= 2 and v[0] == v[-1] and v[0] in quotes:
|
|
44
|
+
v = v[1:-1]
|
|
45
|
+
if k:
|
|
46
|
+
out[k] = v
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
11
50
|
def secret(name, default=None):
|
|
12
|
-
"""The value of one secret, for USE — never for printing or logging.
|
|
13
|
-
|
|
51
|
+
"""The value of one secret, for USE — never for printing or logging.
|
|
52
|
+
|
|
53
|
+
From the environment Atlas started this script with; when it is not there,
|
|
54
|
+
from the project's .env as it stands now (see _file_secrets).
|
|
55
|
+
"""
|
|
56
|
+
v = os.environ.get(name)
|
|
57
|
+
if v:
|
|
58
|
+
return v
|
|
59
|
+
v = _file_secrets().get(name)
|
|
60
|
+
return v if v else default
|
|
14
61
|
|
|
15
62
|
|
|
16
63
|
def has_secret(name):
|
|
17
64
|
"""Whether a secret has been filled in. True/False only, never the value."""
|
|
18
|
-
return bool(
|
|
65
|
+
return bool(secret(name))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _signed_in():
|
|
69
|
+
"""This computer's own Atlas sign-in, as the atlas CLI keeps it."""
|
|
70
|
+
import json as _json
|
|
71
|
+
base = os.environ.get("ATLAS_CONFIG_DIR") or (
|
|
72
|
+
os.path.join(os.environ["XDG_CONFIG_HOME"], "atlas") if os.environ.get("XDG_CONFIG_HOME")
|
|
73
|
+
else os.path.join(os.path.expanduser("~"), ".config", "atlas"))
|
|
74
|
+
try:
|
|
75
|
+
with open(os.path.join(base, "credentials.json"), encoding="utf-8") as f:
|
|
76
|
+
found = _json.load(f)
|
|
77
|
+
return found if isinstance(found, dict) else {}
|
|
78
|
+
except Exception:
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _atlas_token():
|
|
83
|
+
"""The bearer this script calls Atlas with. Never print it or pass it on.
|
|
84
|
+
|
|
85
|
+
Started by Atlas, a script is handed it in its environment. RUN BY HAND
|
|
86
|
+
(by its owner in a terminal, or by a model working in the project) it had
|
|
87
|
+
none, and every call failed with "no Atlas token" although the person was
|
|
88
|
+
signed in on this very computer. It now uses that sign-in: the file the
|
|
89
|
+
atlas CLI itself keeps, readable only by this computer's own user, which is
|
|
90
|
+
the same person a script started by Atlas acts as.
|
|
91
|
+
|
|
92
|
+
What this does NOT do, on purpose: no token is written into the project,
|
|
93
|
+
none is printed, none is copied anywhere. Somebody on another computer, or
|
|
94
|
+
another user of this one, has no such file and is refused as before. A
|
|
95
|
+
sign-in that has run out is renewed by the CLI itself (its output thrown
|
|
96
|
+
away), never here.
|
|
97
|
+
"""
|
|
98
|
+
token = os.environ.get("ATLAS_TOKEN", "")
|
|
99
|
+
if token:
|
|
100
|
+
return token
|
|
101
|
+
import time as _time
|
|
102
|
+
|
|
103
|
+
def usable(found):
|
|
104
|
+
got = found.get("access_token") or ""
|
|
105
|
+
ends = found.get("expires_at")
|
|
106
|
+
try:
|
|
107
|
+
still = (not ends) or _time.time() * 1000 < float(ends) - 60000
|
|
108
|
+
except Exception:
|
|
109
|
+
still = False
|
|
110
|
+
return str(got) if got and still else ""
|
|
111
|
+
|
|
112
|
+
token = usable(_signed_in())
|
|
113
|
+
if token:
|
|
114
|
+
return token
|
|
115
|
+
try:
|
|
116
|
+
import shutil as _shutil
|
|
117
|
+
import subprocess as _sp
|
|
118
|
+
cli = _shutil.which("atlas")
|
|
119
|
+
if cli:
|
|
120
|
+
_sp.run([cli, "whoami"], stdout=_sp.DEVNULL, stderr=_sp.DEVNULL, stdin=_sp.DEVNULL, timeout=30)
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
return usable(_signed_in())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
_NO_TOKEN = ("This job has no Atlas token. Is Local Mode signed in? "
|
|
127
|
+
"(Run by hand, a script uses the sign-in on this computer: run `atlas login` once.)")
|
|
19
128
|
|
|
20
129
|
|
|
21
130
|
#: Tools that read a lot of options flow: minutes on a busy name, not seconds.
|
|
@@ -50,9 +159,9 @@ def atlas(tool, **params):
|
|
|
50
159
|
slug = slug[4:] if slug.startswith("get_") else slug
|
|
51
160
|
wait = params.pop("_timeout", None) or (600 if slug in _SLOW_TOOLS else 120)
|
|
52
161
|
base = os.environ.get("ATLAS_BASE_URL", "https://atlasmcp.finmanagerai.com").rstrip("/")
|
|
53
|
-
token =
|
|
162
|
+
token = _atlas_token()
|
|
54
163
|
if not token:
|
|
55
|
-
raise RuntimeError(
|
|
164
|
+
raise RuntimeError(_NO_TOKEN)
|
|
56
165
|
req = urllib.request.Request(
|
|
57
166
|
"%s/api/v1/tools/%s" % (base, tool),
|
|
58
167
|
data=_json.dumps(params).encode("utf-8"),
|
|
@@ -370,9 +479,9 @@ def alerts(match=None, timeout=None, reconnect=True):
|
|
|
370
479
|
import urllib.request
|
|
371
480
|
|
|
372
481
|
base = os.environ.get("ATLAS_BASE_URL", "https://atlasmcp.finmanagerai.com").rstrip("/")
|
|
373
|
-
token =
|
|
482
|
+
token = _atlas_token()
|
|
374
483
|
if not token:
|
|
375
|
-
raise RuntimeError(
|
|
484
|
+
raise RuntimeError(_NO_TOKEN)
|
|
376
485
|
deadline = None if timeout is None else _time.time() + float(timeout)
|
|
377
486
|
attempt = 0
|
|
378
487
|
while True:
|
package/src/remote-control.js
CHANGED
|
@@ -2101,7 +2101,7 @@ export async function studioVoice(name, args, allowed, root, opts = {}) {
|
|
|
2101
2101
|
* running, an error — so the person watching on the web sees the build happen
|
|
2102
2102
|
* rather than staring at nothing for two minutes and assuming it hung.
|
|
2103
2103
|
*/
|
|
2104
|
-
export async function chatModel(prompt, onEvent, { job = '', model = '', chatId = '', timeoutMs = 1800000 } = {}) {
|
|
2104
|
+
export async function chatModel(prompt, onEvent, { job = '', model = '', chatId = '', instead = false, timeoutMs = 1800000 } = {}) {
|
|
2105
2105
|
const url = process.env.ATLAS_ASK_URL;
|
|
2106
2106
|
const token = process.env.ATLAS_ASK_TOKEN;
|
|
2107
2107
|
if (!url || !token) {
|
|
@@ -2117,7 +2117,10 @@ export async function chatModel(prompt, onEvent, { job = '', model = '', chatId
|
|
|
2117
2117
|
// chat_id: which conversation this is, so Studio gives each web chat its
|
|
2118
2118
|
// own model session and a Studio chat carried on here reaches the session
|
|
2119
2119
|
// already open in the app (0.87.0).
|
|
2120
|
-
|
|
2120
|
+
// instead: said to a run that is working, to change what it is doing.
|
|
2121
|
+
// Studio ends the turn in progress and this one takes its place (0.97.0).
|
|
2122
|
+
// Left out unless asked for, so an ordinary message is what it always was.
|
|
2123
|
+
body: JSON.stringify({ job, prompt: String(prompt), model, chat_id: chatId, ...(instead ? { instead: true } : {}) }),
|
|
2121
2124
|
signal: AbortSignal.timeout(timeoutMs),
|
|
2122
2125
|
});
|
|
2123
2126
|
if (!res.ok || !res.body) {
|
|
@@ -2148,6 +2151,69 @@ export async function chatModel(prompt, onEvent, { job = '', model = '', chatId
|
|
|
2148
2151
|
}
|
|
2149
2152
|
}
|
|
2150
2153
|
|
|
2154
|
+
/**
|
|
2155
|
+
* Stop the turn a conversation has in flight on THIS computer (0.97.0).
|
|
2156
|
+
*
|
|
2157
|
+
* Stop pressed on the web used to end only the browser's own stream: nothing
|
|
2158
|
+
* told this computer, so the model carried on, mouse and keyboard included,
|
|
2159
|
+
* and the next message waited behind it. A closed stream cannot mean stop (a
|
|
2160
|
+
* shut laptop looks the same, and a build must outlive that), so Stop is its
|
|
2161
|
+
* own ask, and this passes it to the bridge. `stopped` false when nothing of
|
|
2162
|
+
* that conversation was running.
|
|
2163
|
+
*/
|
|
2164
|
+
export async function stopChat(job, chatId, { computer = false, ahead = false, timeoutMs = 15000 } = {}) {
|
|
2165
|
+
const url = process.env.ATLAS_ASK_URL;
|
|
2166
|
+
const token = process.env.ATLAS_ASK_TOKEN;
|
|
2167
|
+
if (!url || !token) return { ok: false, error: 'No model is connected on this computer.' };
|
|
2168
|
+
try {
|
|
2169
|
+
const res = await fetch(url.replace('/ask', '/chat/stop'), {
|
|
2170
|
+
method: 'POST',
|
|
2171
|
+
headers: { 'Content-Type': 'application/json', 'X-Atlas-Token': token },
|
|
2172
|
+
// computer: let go of the mouse and keyboard too (Stop on the live picture).
|
|
2173
|
+
// ahead: the page's own message is still waiting behind another run, and
|
|
2174
|
+
// it is that run that is to end (the message then takes its turn).
|
|
2175
|
+
body: JSON.stringify({ job: String(job || ''), chat_id: String(chatId || ''), ...(computer ? { computer: true } : {}), ...(ahead ? { ahead: true } : {}) }),
|
|
2176
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
2177
|
+
});
|
|
2178
|
+
const out = await res.json().catch(() => ({}));
|
|
2179
|
+
// An Atlas Studio from before this existed answers the address as a chat
|
|
2180
|
+
// with nothing said (400) or as not found: said plainly, so the web can
|
|
2181
|
+
// tell the person the stop did not reach the model.
|
|
2182
|
+
if (!res.ok || !out.ok) return { ok: false, error: 'Atlas Studio on this computer is out of date and cannot be stopped from here. Open it and let it update.' };
|
|
2183
|
+
return { ok: true, stopped: out.stopped === true };
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
return { ok: false, error: String(err?.message || err) };
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
/**
|
|
2190
|
+
* Is a run going in this conversation on THIS computer (0.97.0)?
|
|
2191
|
+
*
|
|
2192
|
+
* A web page that did not start the run (another tab, a phone, the app
|
|
2193
|
+
* itself), or that started it and was then reloaded, had no way to know one
|
|
2194
|
+
* was going, so it showed no Stop. It asks, and Atlas Studio answers yes or
|
|
2195
|
+
* no, and whether the page's own message is still waiting behind it.
|
|
2196
|
+
*/
|
|
2197
|
+
export async function chatState(job, chatId, { timeoutMs = 8000 } = {}) {
|
|
2198
|
+
const url = process.env.ATLAS_ASK_URL;
|
|
2199
|
+
const token = process.env.ATLAS_ASK_TOKEN;
|
|
2200
|
+
if (!url || !token) return { ok: false, error: 'No model is connected on this computer.' };
|
|
2201
|
+
try {
|
|
2202
|
+
const res = await fetch(url.replace('/ask', '/chat/state'), {
|
|
2203
|
+
method: 'POST',
|
|
2204
|
+
headers: { 'Content-Type': 'application/json', 'X-Atlas-Token': token },
|
|
2205
|
+
body: JSON.stringify({ job: String(job || ''), chat_id: String(chatId || '') }),
|
|
2206
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
2207
|
+
});
|
|
2208
|
+
const out = await res.json().catch(() => ({}));
|
|
2209
|
+
// An Atlas Studio from before this existed cannot say.
|
|
2210
|
+
if (!res.ok || !out.ok) return { ok: false, error: 'Atlas Studio on this computer is out of date and cannot say what is running. Open it and let it update.' };
|
|
2211
|
+
return { ok: true, running: out.running === true, waiting: out.waiting === true };
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
return { ok: false, error: String(err?.message || err) };
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2151
2217
|
/**
|
|
2152
2218
|
* Hand the model the person's answer to a question it asked mid-turn.
|
|
2153
2219
|
*
|