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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindvest-atlas",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Atlas CLI — OAuth login, tool calls, and live alert/flow streaming for the Atlas trading API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- function buildBody(tool, flags) {
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.0';
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>] [--json] [--refresh] List available tools');
20
+ println(' tools [--search <q>] [--refresh] List available tools');
21
21
  println(' <tool> --help Show a tool\'s parameters');
22
- println(' <tool> [--param value] [--json] [--out <dir>] Call a 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('Tip: pass a full JSON body with ') + c.cyan(`atlas ${slug} --json '{"...":...}'`));
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
  }