mindvest-atlas 0.1.0 → 0.1.3
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/call.js +81 -1
- package/src/config.js +1 -1
- package/src/util/help.js +17 -3
package/package.json
CHANGED
package/src/commands/call.js
CHANGED
|
@@ -10,7 +10,48 @@ import { color, println, eprintln, prettyJson, redactBase64 } from '../util/ui.j
|
|
|
10
10
|
|
|
11
11
|
const RESERVED = new Set(['help', 'h', 'json', 'out', 'save-charts', 'raw', 'base-url', 'version', 'v']);
|
|
12
12
|
|
|
13
|
+
// Detect a chart payload in a tool response — top level, MCP structuredContent,
|
|
14
|
+
// a `data` block, or a per-image `data`/inline entry (heatmaps put chart_family
|
|
15
|
+
// inline in images[]). Returns the renderable node, or null.
|
|
16
|
+
function chartNodeOf(o) {
|
|
17
|
+
if (!o || typeof o !== 'object' || Array.isArray(o)) return null;
|
|
18
|
+
if (Array.isArray(o.charts) && o.charts.length) return o;
|
|
19
|
+
if (typeof o.chart_family === 'string' && o.chart_family) return o;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function maybeWriteChartSink(data) {
|
|
23
|
+
const sink = process.env.ATLAS_CHART_SINK;
|
|
24
|
+
if (!sink || !data || typeof data !== 'object') return;
|
|
25
|
+
let chart =
|
|
26
|
+
chartNodeOf(data) ||
|
|
27
|
+
chartNodeOf(data.structuredContent) ||
|
|
28
|
+
chartNodeOf(data.structured_content) ||
|
|
29
|
+
chartNodeOf(data.data);
|
|
30
|
+
if (!chart && Array.isArray(data.images)) {
|
|
31
|
+
const cs = data.images
|
|
32
|
+
.map((e) => (e && typeof e === 'object' ? chartNodeOf(e) || chartNodeOf(e.data) : null))
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
if (cs.length) chart = { charts: cs };
|
|
35
|
+
}
|
|
36
|
+
if (!chart) return;
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(sink), { recursive: true });
|
|
39
|
+
// APPEND one JSON line per chart (JSONL) — never overwrite. Two chart tool
|
|
40
|
+
// calls in the SAME turn each land their own line, so the Studio watcher
|
|
41
|
+
// captures BOTH; an overwrite lost the first when the second raced in
|
|
42
|
+
// before the watcher read it (the "multi-chart-per-turn never rendered"
|
|
43
|
+
// bug). The host reads the file whole and fires each new line.
|
|
44
|
+
fs.appendFileSync(sink, JSON.stringify({ at: Date.now(), payload: chart }) + '\n');
|
|
45
|
+
} catch {
|
|
46
|
+
/* UI-only side channel — never fail the command */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Coerce one flag value to the schema type. A value that arrived as an array
|
|
51
|
+
// (a repeated flag, e.g. `--tag a --tag b`) maps element-wise — so array params
|
|
52
|
+
// need no JSON. Unknown/string types still accept an inline JSON array/object.
|
|
13
53
|
function coerce(raw, type) {
|
|
54
|
+
if (Array.isArray(raw)) return raw.map((x) => coerce(x, type === 'array' ? 'string' : type));
|
|
14
55
|
if (raw === true || raw === false) return raw;
|
|
15
56
|
const s = String(raw);
|
|
16
57
|
if (type === 'integer') {
|
|
@@ -31,9 +72,36 @@ function coerce(raw, type) {
|
|
|
31
72
|
return s;
|
|
32
73
|
}
|
|
33
74
|
|
|
75
|
+
// A nested value (from a dotted flag like `--updates.is-active true`) carries no
|
|
76
|
+
// schema type, so infer the obvious one: booleans, plain numbers, inline JSON,
|
|
77
|
+
// else the string verbatim. This lets a flag express a nested body with no JSON
|
|
78
|
+
// at all; `--json '{…}'` stays for exact control over an ambiguous value.
|
|
79
|
+
function looseScalar(raw) {
|
|
80
|
+
if (raw === true || raw === false) return raw;
|
|
81
|
+
if (Array.isArray(raw)) return raw.map(looseScalar);
|
|
82
|
+
const s = String(raw);
|
|
83
|
+
if (/^(true|false)$/i.test(s)) return /^true$/i.test(s);
|
|
84
|
+
if (/^-?\d+$/.test(s)) return parseInt(s, 10);
|
|
85
|
+
if (/^-?(?:\d+\.\d*|\.\d+)$/.test(s)) return Number(s);
|
|
86
|
+
if (s.startsWith('[') || s.startsWith('{')) { try { return JSON.parse(s); } catch { return s; } }
|
|
87
|
+
return s;
|
|
88
|
+
}
|
|
89
|
+
|
|
34
90
|
const norm = (s) => s.replace(/-/g, '_');
|
|
35
91
|
|
|
36
|
-
|
|
92
|
+
// Set a value at a dotted path, creating intermediate objects as needed. Each
|
|
93
|
+
// segment is hyphen-normalised, so `--bracket.stop-loss` → bracket.stop_loss.
|
|
94
|
+
function setPath(obj, segs, value) {
|
|
95
|
+
let cur = obj;
|
|
96
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
97
|
+
const k = segs[i];
|
|
98
|
+
if (typeof cur[k] !== 'object' || cur[k] === null || Array.isArray(cur[k])) cur[k] = {};
|
|
99
|
+
cur = cur[k];
|
|
100
|
+
}
|
|
101
|
+
cur[segs[segs.length - 1]] = value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildBody(tool, flags) {
|
|
37
105
|
const params = tool.parameters || {};
|
|
38
106
|
const byNorm = {};
|
|
39
107
|
for (const p of Object.keys(params)) byNorm[norm(p)] = p;
|
|
@@ -41,6 +109,12 @@ function buildBody(tool, flags) {
|
|
|
41
109
|
const body = {};
|
|
42
110
|
for (const [key, val] of Object.entries(flags)) {
|
|
43
111
|
if (RESERVED.has(key)) continue;
|
|
112
|
+
// Dotted flag → nested object: `--updates.is-active true` builds
|
|
113
|
+
// { updates: { is_active: true } } so even nested bodies need no JSON.
|
|
114
|
+
if (key.includes('.')) {
|
|
115
|
+
setPath(body, key.split('.').map(norm), looseScalar(val));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
44
118
|
const pname = params[key] ? key : byNorm[norm(key)];
|
|
45
119
|
if (pname) {
|
|
46
120
|
body[pname] = coerce(val, (params[pname].type) || 'string');
|
|
@@ -140,6 +214,12 @@ export async function callCommand(toolSlug, args) {
|
|
|
140
214
|
return 1;
|
|
141
215
|
}
|
|
142
216
|
|
|
217
|
+
// UI side-channel: when a host (Atlas Studio) sets ATLAS_CHART_SINK, write the
|
|
218
|
+
// chart payload there so the app renders the interactive widget — for EVERY
|
|
219
|
+
// caller and model, whether this CLI runs directly or behind a wrapper shim.
|
|
220
|
+
// Purely additive; never alters stdout or fails the command.
|
|
221
|
+
maybeWriteChartSink(data);
|
|
222
|
+
|
|
143
223
|
// Save chart images if requested.
|
|
144
224
|
const outDir = (typeof flags.out === 'string' && flags.out) || (typeof flags['save-charts'] === 'string' && flags['save-charts']);
|
|
145
225
|
if (outDir) {
|
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.1.
|
|
5
|
+
export const VERSION = '0.1.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/util/help.js
CHANGED
|
@@ -17,9 +17,10 @@ export function printRootHelp() {
|
|
|
17
17
|
println(' whoami Show the signed-in account');
|
|
18
18
|
println();
|
|
19
19
|
println(c.bold('TOOLS'));
|
|
20
|
-
println(' tools [--search <q>] [--
|
|
20
|
+
println(' tools [--search <q>] [--refresh] List available tools');
|
|
21
21
|
println(' <tool> --help Show a tool\'s parameters');
|
|
22
|
-
println(' <tool>
|
|
22
|
+
println(' <tool> --param value [--out <dir>] Call a tool — pass params as flags');
|
|
23
|
+
println(' <tool> --a.b value Nested value via a dotted flag');
|
|
23
24
|
println();
|
|
24
25
|
println(c.bold('STREAMS'));
|
|
25
26
|
println(' alerts [--symbol SPY] [--name greek_exposure] Live alert feed (SSE)');
|
|
@@ -41,6 +42,7 @@ export function printRootHelp() {
|
|
|
41
42
|
println(c.dim(' atlas tools'));
|
|
42
43
|
println(c.dim(' atlas stock-quote --symbol SPY'));
|
|
43
44
|
println(c.dim(' atlas options-chain --symbol SPY --expiration 2026-06-20'));
|
|
45
|
+
println(c.dim(' atlas update-alert --alert-id <id> --updates.is-active true # nested via a dotted flag'));
|
|
44
46
|
println(c.dim(' atlas alerts --symbol SPY --name greek_exposure --save-charts ./charts'));
|
|
45
47
|
println(c.dim(' atlas flow stream --symbol NVDA'));
|
|
46
48
|
}
|
|
@@ -101,9 +103,21 @@ export function printToolHelp(name, tool) {
|
|
|
101
103
|
if (argDescs[p]) println(' ' + ' '.repeat(flagWidth) + c.dim(argDescs[p]));
|
|
102
104
|
}
|
|
103
105
|
println();
|
|
106
|
+
|
|
107
|
+
// A concrete, copy-pasteable EXAMPLE in flag form — what to model, NOT a
|
|
108
|
+
// JSON body. Prefer required params; otherwise show the first couple.
|
|
109
|
+
const entries = Object.entries(params);
|
|
110
|
+
const required = entries.filter(([, m]) => m.required);
|
|
111
|
+
const chosen = (required.length ? required : entries).slice(0, 3);
|
|
112
|
+
const example = chosen.map(([p, m]) => `${flagForm(p)} <${m.type || 'string'}>`).join(' ');
|
|
113
|
+
println(c.bold('EXAMPLE'));
|
|
114
|
+
println(' ' + c.cyan(`atlas ${slug}${example ? ' ' + example : ''}`));
|
|
115
|
+
println();
|
|
104
116
|
} else {
|
|
105
117
|
println(c.dim('This tool takes no parameters.'));
|
|
106
118
|
println();
|
|
107
119
|
}
|
|
108
|
-
println(c.dim('
|
|
120
|
+
println(c.dim('Pass each parameter as a flag (above). A nested value uses a dotted flag, e.g. '));
|
|
121
|
+
println(' ' + c.cyan(`atlas ${slug} --updates.is-active true`) + c.dim(' (→ {"updates":{"is_active":true}})'));
|
|
122
|
+
println(c.dim('Advanced: ') + c.cyan(`--json '{…}'`) + c.dim(' sends a full body verbatim.'));
|
|
109
123
|
}
|