mindvest-atlas 0.53.0 → 0.55.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindvest-atlas",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "Atlas CLI \u2014 OAuth login, tool calls, and live alert/flow streaming for the Atlas trading API",
5
5
  "type": "module",
6
6
  "bin": {
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.53.0';
5
+ export const VERSION = '0.55.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';
@@ -57,8 +57,21 @@ export async function atlas(tool, params = {}) {
57
57
  * for pure text. fresh:false keeps ONE conversation for this job (the model
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
+ *
61
+ * model:"" uses whichever model is connected, which is the usual case. Name one
62
+ * to send THIS question somewhere else - a cheap fast one for twenty
63
+ * classifications and a strong one for the judgement call at the end, in the
64
+ * same script:
65
+ *
66
+ * const kind = await ask(`Sector or company?\n${line}`, { model: "claude-haiku-4-5" });
67
+ * const call = await ask("Given all of the above, do we buy?", { model: "claude-opus-4-8" });
68
+ *
69
+ * models() lists what this computer can reach, with their ids and the effort
70
+ * words each one takes. A name it does not have is refused BY NAME rather than
71
+ * quietly answered by the default, because a script that asked for a specific
72
+ * model and silently got another one is worse than one that stops.
60
73
  */
61
- export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600000 } = {}) {
74
+ export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600000, model = "" } = {}) {
62
75
  const url = process.env.ATLAS_ASK_URL;
63
76
  const token = process.env.ATLAS_ASK_TOKEN;
64
77
  if (!url || !token) {
@@ -67,10 +80,14 @@ export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600
67
80
  "Connect a model in Atlas Studio, or do this step in code."
68
81
  );
69
82
  }
83
+ const body = { job: process.env.ATLAS_JOB || "", prompt: String(prompt), tools, fresh };
84
+ // Sent only when named, so a script that does not care keeps whatever is
85
+ // connected rather than pinning itself to today's default.
86
+ if (String(model || "").trim()) body.model = String(model).trim();
70
87
  const res = await fetch(url, {
71
88
  method: "POST",
72
89
  headers: { "Content-Type": "application/json", "X-Atlas-Token": token },
73
- body: JSON.stringify({ job: process.env.ATLAS_JOB || "", prompt: String(prompt), tools, fresh }),
90
+ body: JSON.stringify(body),
74
91
  signal: AbortSignal.timeout(timeoutMs),
75
92
  });
76
93
  const out = await res.json().catch(() => ({}));
@@ -78,6 +95,36 @@ export async function ask(prompt, { tools = true, fresh = false, timeoutMs = 600
78
95
  return out.answer || "";
79
96
  }
80
97
 
98
+ /**
99
+ * Every model this computer can reach, asked of the app itself.
100
+ *
101
+ * Returns rows of {id, label, vendor, ready, effort, effortDefault} - the id to
102
+ * pass to ask({model}), and the effort words THAT model takes, which differ per
103
+ * vendor and are the other thing that gets guessed wrong. An empty list means
104
+ * no model is connected, in which case ask() will say so too.
105
+ *
106
+ * const roster = await models();
107
+ * const cheap = roster.find((m) => m.ready && m.id.includes("haiku"));
108
+ * await ask("Sector or company?", { model: cheap?.id });
109
+ */
110
+ export async function models() {
111
+ const url = process.env.ATLAS_ASK_URL;
112
+ const token = process.env.ATLAS_ASK_TOKEN;
113
+ if (!url || !token) return [];
114
+ try {
115
+ const res = await fetch(url.replace("/ask", "/models"), {
116
+ headers: { "X-Atlas-Token": token },
117
+ signal: AbortSignal.timeout(20000),
118
+ });
119
+ const out = await res.json().catch(() => ({}));
120
+ return (out.models || []).filter((m) => m && m.id);
121
+ } catch {
122
+ // A roster that cannot be read is not a reason to take the job down: the
123
+ // script can still ask the connected model without naming one.
124
+ return [];
125
+ }
126
+ }
127
+
81
128
  /**
82
129
  * Call a tool on one of YOUR connected MCP servers.
83
130
  *
@@ -1502,7 +1502,7 @@ const STARTER_README = (name) => [
1502
1502
  '- `main.py` what runs. Replaced by the model on the first build.',
1503
1503
  '- `.env` your keys. Values stay on this computer and are never read',
1504
1504
  ' back by anything, including the model.',
1505
- '- `atlas_env.py` helpers: secret(), require(), atlas(), ask().',
1505
+ '- `atlas_env.py` helpers: secret(), require(), atlas(), ask(), models().',
1506
1506
  '- `plan.md` a diagram of what happens, written after the first build.',
1507
1507
  '- `dashboard.js` how this job draws itself on the Dashboard tab. Atlas owns',
1508
1508
  ' the page; this is the drawing.',