mindvest-atlas 0.67.0 → 0.68.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.67.0",
3
+ "version": "0.68.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/cli.js CHANGED
@@ -12,6 +12,7 @@ import { whoamiCommand } from './commands/whoami.js';
12
12
  import { toolsCommand } from './commands/tools.js';
13
13
  import { modelsCommand, askCommand } from './commands/models.js';
14
14
  import { alertsCommand } from './commands/alerts.js';
15
+ import { signalsCommand } from './commands/signals.js';
15
16
  import { flowCommand } from './commands/flow.js';
16
17
  import { remoteControlCommand } from './commands/remote-control.js';
17
18
  import { updateCliCommand } from './commands/update-cli.js';
@@ -88,6 +89,10 @@ export async function main(rawArgs) {
88
89
  case 'models': return await modelsCommand(withoutCommand(args));
89
90
  case 'ask': return await askCommand(withoutCommand(args));
90
91
  case 'alerts': return await alertsCommand(withoutCommand(args));
92
+ // Plays: groups, the board, a group's record, and closing one down.
93
+ case 'signals':
94
+ case 'plays':
95
+ return await signalsCommand(withoutCommand(args));
91
96
  case 'flow': return await flowCommand(withoutCommand(args));
92
97
  // Long-lived: hold the job stream and run this computer's jobs.
93
98
  // Walk the whole loop and say which hop fails.
@@ -12,7 +12,8 @@ import path from 'node:path';
12
12
  import { API } from '../config.js';
13
13
  import { runStream } from '../util/stream-loop.js';
14
14
  import { formatAlert } from '../util/format.js';
15
- import { color, println, eprintln } from '../util/ui.js';
15
+ import { color, println, eprintln, prettyJson } from '../util/ui.js';
16
+ import { requestJson } from '../api/client.js';
16
17
  import * as background from '../util/background.js';
17
18
 
18
19
  // Names the slot, the claim file and the log, and it is the command's own name
@@ -25,8 +26,66 @@ function sanitizeFilename(name) {
25
26
  return String(name || '').replace(/[^A-Za-z0-9._-]/g, '_') || 'alert.png';
26
27
  }
27
28
 
29
+ // ── list / delete ────────────────────────────────────────────────────────────
30
+ const callTool = (name, body) => requestJson(API.tool(name), { method: 'POST', body: body || {} });
31
+
32
+ async function listAlerts(flags) {
33
+ const data = await callTool('List-Alerts', {
34
+ ...(flags.all ? { active_only: false } : {}),
35
+ ...(flags.symbol ? { symbol: String(flags.symbol).toUpperCase() } : {}),
36
+ });
37
+ if (flags.json) { println(prettyJson(data)); return 0; }
38
+ const list = (data && (data.alerts || data.data)) || (Array.isArray(data) ? data : []);
39
+ if (!list.length) { println(color.dim('No alerts.')); return 0; }
40
+ for (const a of list) {
41
+ const state = a.is_active === false ? color.dim('off') : color.green('on');
42
+ println(`${state} ${color.bold(a.name || '(unnamed)')} ${color.dim([a.symbol, a.alert_type].filter(Boolean).join(' · '))}`);
43
+ println(color.dim(` ${a.id}`));
44
+ }
45
+ println('');
46
+ println(color.dim('Switch one off: atlas alerts delete --id <id>'));
47
+ println(color.dim('Delete for good: atlas alerts delete --id <id> --purge'));
48
+ return 0;
49
+ }
50
+
51
+ async function deleteAlerts(args) {
52
+ const flags = args.flags;
53
+ // --id may repeat; extra positionals after the verb are ids too, so
54
+ // `atlas alerts delete a b c` does what it looks like it does.
55
+ const fromFlags = flags.id === undefined ? [] : (Array.isArray(flags.id) ? flags.id : [flags.id]);
56
+ const ids = [...fromFlags, ...(args._ || []).slice(1)]
57
+ .map((x) => String(x || '').trim())
58
+ .filter((x) => x && x !== 'true');
59
+ if (!ids.length) {
60
+ eprintln(color.red('Which alert? --id <id> (repeat for several), or list them: atlas alerts list'));
61
+ return 1;
62
+ }
63
+ const purge = !!flags.purge;
64
+ const data = await callTool('Delete-Alert', {
65
+ alert_ids: ids, purge,
66
+ });
67
+ if (data && data.error) { eprintln(color.red(String(data.error))); return 1; }
68
+ if (flags.json) { println(prettyJson(data)); return 0; }
69
+ println(data.message || (purge ? 'Deleted.' : 'Switched off.'));
70
+ // A purge that kept some says why, and the message already names them; this
71
+ // points at the way through rather than leaving the user to guess.
72
+ if (data.kept && Object.keys(data.kept).length) {
73
+ println(color.dim('Repoint those setups, or drop --purge to just switch the alert off.'));
74
+ }
75
+ return 0;
76
+ }
77
+
28
78
  export async function alertsCommand(args) {
29
79
  const flags = args.flags;
80
+ // MANAGING alerts, not watching them. A bare `atlas alerts` has meant "open
81
+ // the live feed" since it shipped, so the stream stays the default and the
82
+ // two management verbs are positionals in front of it. Without these the
83
+ // only way to clear out a spoiled alert was to know the tool slug by heart
84
+ // (owner, 2026-09-12: "it's hard to manage spoiled alerts without a
85
+ // something to delete them").
86
+ const verb = String((args._ && args._[0]) || '').toLowerCase();
87
+ if (verb === 'list' || verb === 'ls') return await listAlerts(flags);
88
+ if (verb === 'delete' || verb === 'rm' || verb === 'off') return await deleteAlerts(args);
30
89
  if (flags.stop) return background.stop(SLOT);
31
90
  if (flags.detach) return background.detach(SLOT);
32
91
  const holder = background.holdUntilStopped(SLOT);
@@ -0,0 +1,201 @@
1
+ // `atlas signals …` — the plays surface as real commands.
2
+ //
3
+ // Owner, 2026-09-12: *"CLI need update if not the actions and doc will not see
4
+ // it — it needs commands."* Every tool has always been callable as
5
+ // `atlas <slug> --flag value`, so the plays tools worked; what did not work was
6
+ // FINDING them. `atlas --help` listed no plays at all, so an agent (or a
7
+ // person) reading the CLI concluded there was no way to see a group's record or
8
+ // to close one down.
9
+ //
10
+ // These are thin: each subcommand is one tool call with the flags spelled the
11
+ // way a person would say them, and a printed answer rather than a JSON dump.
12
+ // `--json` gives the raw body for scripts.
13
+ import { API } from '../config.js';
14
+ import { requestJson } from '../api/client.js';
15
+ import { color, println, eprintln, prettyJson } from '../util/ui.js';
16
+
17
+ const HELP = `${color.bold('atlas signals')} — plays: your groups, the board, and what they have made
18
+
19
+ ${color.bold('USAGE')}
20
+ atlas signals <command> [options]
21
+
22
+ ${color.bold('COMMANDS')}
23
+ groups The groups you can post to, and the ones you follow
24
+ plays [--board community|private] The plays themselves [--days N] [--limit N]
25
+ show --id <play> One play, as a card
26
+ take --id <play> [--contracts N] Place a play in YOUR account [--account <id>]
27
+ performance --group <id> [--mine] Closed trades and the win rate for a group
28
+ delete --group <id> Delete a group you own
29
+ [--stop-subscribers] …stopping the subscriptions first
30
+ discover Public groups other people run
31
+ follow --group <id> [--off] Follow a public group, or stop
32
+ schema What a play is made of — read before posting
33
+
34
+ ${color.bold('EXAMPLES')}
35
+ atlas signals groups
36
+ atlas signals plays --board community --days 1
37
+ atlas signals performance --group <id>
38
+ atlas signals delete --group <id> --stop-subscribers
39
+ `;
40
+
41
+ const callTool = (name, body) => requestJson(API.tool(name), { method: 'POST', body: body || {} });
42
+
43
+ // A tool answers with its own {success:false,error} rather than an HTTP error,
44
+ // so every subcommand checks the body, not just the status.
45
+ function failed(data) {
46
+ return data && typeof data === 'object' && data.success === false;
47
+ }
48
+ function printFailure(data) {
49
+ eprintln(color.red(String(data.error || 'That did not work.')));
50
+ if (data.how_to_proceed) eprintln(color.dim(String(data.how_to_proceed)));
51
+ return 1;
52
+ }
53
+
54
+ const num = (v) => (v === undefined || v === null || v === '' ? null : Number(v));
55
+ const money = (n) => (n === null || n === undefined || Number.isNaN(Number(n))
56
+ ? '—'
57
+ : `${Number(n) < 0 ? '-' : ''}$${Math.abs(Number(n)).toLocaleString('en-US', { maximumFractionDigits: 2 })}`);
58
+
59
+ function rows(list) {
60
+ return Array.isArray(list) ? list : (list && Array.isArray(list.signals) ? list.signals : []);
61
+ }
62
+
63
+ export async function signalsCommand(args) {
64
+ const flags = args.flags || {};
65
+ const sub = String(args._[0] || 'groups').toLowerCase();
66
+ if (flags.help || sub === 'help') { println(HELP); return 0; }
67
+ const json = !!flags.json;
68
+ const groupId = flags.group || flags['group-id'] || flags.id;
69
+
70
+ switch (sub) {
71
+ case 'groups': {
72
+ const data = await callTool('Signal-Groups', {});
73
+ if (failed(data)) return printFailure(data);
74
+ if (json) { println(prettyJson(data)); return 0; }
75
+ const list = data.groups || data.data || [];
76
+ if (!list.length) { println(color.dim('No groups yet. The community board is always there: atlas signals plays --board community')); return 0; }
77
+ for (const g of list) {
78
+ const bits = [g.role, g.visibility, g.signal_count != null ? `${g.signal_count} plays` : null].filter(Boolean);
79
+ println(`${color.bold(g.name || '(unnamed)')} ${color.dim(bits.join(' · '))}`);
80
+ println(color.dim(` ${g.id}`));
81
+ }
82
+ return 0;
83
+ }
84
+ case 'plays':
85
+ case 'list': {
86
+ const body = {};
87
+ if (flags.board) body.board = String(flags.board);
88
+ if (flags.group) body.group_id = String(flags.group);
89
+ if (flags.days !== undefined) body.days = num(flags.days);
90
+ if (flags.limit !== undefined) body.limit = num(flags.limit);
91
+ const data = await callTool('Signal-List', body);
92
+ if (failed(data)) return printFailure(data);
93
+ if (json) { println(prettyJson(data)); return 0; }
94
+ const list = rows(data.plays || data.signals || data);
95
+ if (!list.length) { println(color.dim('Nothing on that board in that window.')); return 0; }
96
+ for (const s of list) {
97
+ const p = s.payload || s;
98
+ const what = [p.side, p.symbol, p.strike ? `$${p.strike}` : null, (p.option_type || '').toUpperCase() || null,
99
+ p.expiration_date || p.expiry || null].filter(Boolean).join(' ');
100
+ println(`${color.bold(what || '(play)')} ${color.dim(s.status || '')}`);
101
+ println(color.dim(` ${s.id}${s.posted_at ? ' ' + s.posted_at : ''}`));
102
+ }
103
+ return 0;
104
+ }
105
+ case 'show': {
106
+ const id = flags.id || flags.play || flags.signal;
107
+ if (!id) { eprintln(color.red('Which play? --id <play-id>')); return 1; }
108
+ const data = await callTool('Signal-Show', { signal_id: String(id) });
109
+ if (failed(data)) return printFailure(data);
110
+ println(prettyJson(data));
111
+ return 0;
112
+ }
113
+ case 'take': {
114
+ const id = flags.id || flags.play || flags.signal;
115
+ if (!id) { eprintln(color.red('Which play? --id <play-id>')); return 1; }
116
+ const body = { signal_id: String(id) };
117
+ if (flags.contracts !== undefined) body.contracts = num(flags.contracts);
118
+ if (flags.account) body.account_id = String(flags.account);
119
+ const data = await callTool('Signal-Take', body);
120
+ if (failed(data)) return printFailure(data);
121
+ println(json ? prettyJson(data) : (data.message || 'Taken.'));
122
+ return 0;
123
+ }
124
+ case 'performance':
125
+ case 'perf': {
126
+ if (!groupId) { eprintln(color.red('Which group? --group <id> (atlas signals groups)')); return 1; }
127
+ const data = await callTool('Signal-Group-Performance', {
128
+ group_id: String(groupId), mine: !!flags.mine,
129
+ });
130
+ if (failed(data)) return printFailure(data);
131
+ if (json) { println(prettyJson(data)); return 0; }
132
+ const t = data.totals || {};
133
+ const name = data.display_name || data.name || 'this group';
134
+ const whose = data.whose === 'mine' ? 'your own trades' : "the author's record";
135
+ println(`${color.bold(name)} ${color.dim('— ' + whose)}`);
136
+ println(` Closed trades ${(data.rows || []).length}`);
137
+ println(` Win rate ${t.win_rate ?? 0}% ${color.dim(`(${t.win_count ?? 0} up, ${t.loss_count ?? 0} down)`)}`);
138
+ println(` Total ${money(t.total_pnl)} ${color.dim(`${t.total_pnl_pct ?? 0}%`)}`);
139
+ if (data.viewer_has_rows && data.whose !== 'mine') {
140
+ println(color.dim(' You have taken plays here too: add --mine for your own numbers.'));
141
+ }
142
+ return 0;
143
+ }
144
+ case 'delete':
145
+ case 'rm': {
146
+ if (!groupId) { eprintln(color.red('Which group? --group <id> (atlas signals groups)')); return 1; }
147
+ const stop = !!(flags['stop-subscribers'] || flags.stop);
148
+ const data = await callTool('Signal-Group-Delete', {
149
+ group_id: String(groupId), stop_subscribers: stop,
150
+ });
151
+ if (failed(data)) {
152
+ // THE REFUSAL WITH AN ANSWER. People are subscribed: say how many, say
153
+ // what stopping them costs them, and print the line that does it —
154
+ // rather than an error that names a screen this person is not looking at.
155
+ if (data.code === 'has_subscribers') {
156
+ const n = Number(data.subscriber_count) || 1;
157
+ eprintln(color.yellow(`${n} ${n === 1 ? 'person is' : 'people are'} subscribed to this group.`));
158
+ eprintln(color.dim('Deleting stops their billing at once, so there is no next payment, and they'
159
+ + ' lose the rest of the month they paid for. Anything they already traded is theirs.'));
160
+ eprintln('');
161
+ eprintln(' ' + color.cyan(`atlas signals delete --group ${groupId} --stop-subscribers`));
162
+ return 1;
163
+ }
164
+ return printFailure(data);
165
+ }
166
+ println(json ? prettyJson(data) : (data.message || 'Group deleted.'));
167
+ return 0;
168
+ }
169
+ case 'discover': {
170
+ const data = await callTool('Signal-Discover', {});
171
+ if (failed(data)) return printFailure(data);
172
+ if (json) { println(prettyJson(data)); return 0; }
173
+ const list = data.groups || [];
174
+ if (!list.length) { println(color.dim('No public groups yet.')); return 0; }
175
+ for (const g of list) {
176
+ println(`${color.bold(g.name || '(unnamed)')} ${color.dim([g.owner_label, `${g.follower_count ?? 0} following`].filter(Boolean).join(' · '))}`);
177
+ println(color.dim(` ${g.id}`));
178
+ }
179
+ return 0;
180
+ }
181
+ case 'follow': {
182
+ if (!groupId) { eprintln(color.red('Which group? --group <id>')); return 1; }
183
+ const data = await callTool('Signal-Follow', {
184
+ group_id: String(groupId), follow: flags.off ? false : flags.follow !== false,
185
+ });
186
+ if (failed(data)) return printFailure(data);
187
+ println(json ? prettyJson(data) : (data.message || 'Done.'));
188
+ return 0;
189
+ }
190
+ case 'schema': {
191
+ const data = await callTool('Signal-Schema', {});
192
+ if (failed(data)) return printFailure(data);
193
+ println(typeof data === 'string' ? data : prettyJson(data));
194
+ return 0;
195
+ }
196
+ default:
197
+ eprintln(color.red(`Unknown: atlas signals ${sub}`));
198
+ println(HELP);
199
+ return 1;
200
+ }
201
+ }
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.67.0';
5
+ export const VERSION = '0.68.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/util/help.js CHANGED
@@ -27,7 +27,17 @@ export function printRootHelp() {
27
27
  println(' --strike 205 --side call --from 2026-07-06 [--to 2026-07-10] for ONE option contract');
28
28
  println(' [--no-greeks] [--json]');
29
29
  println();
30
+ println(c.bold('PLAYS (SIGNALS)'));
31
+ println(' signals groups Groups you post to, and the ones you follow');
32
+ println(' signals plays [--board community|private] The plays [--days N] [--limit N]');
33
+ println(' signals performance --group <id> [--mine] A group\'s closed trades and win rate');
34
+ println(' signals delete --group <id> [--stop-subscribers] Delete a group (stops subscriptions first)');
35
+ println(' signals take --id <play> [--contracts N] Place a play in your account');
36
+ println(' signals <discover|follow|show|schema> ' + c.dim('atlas signals help for all of it'));
37
+ println();
30
38
  println(c.bold('STREAMS'));
39
+ println(' alerts list [--all] [--symbol SPY] Your alerts, with their ids');
40
+ println(' alerts delete --id <id> [--id <id2>] [--purge] Switch alerts off, or delete for good');
31
41
  println(' alerts [--symbol SPY] [--name greek_exposure] Live alert feed (SSE)');
32
42
  println(' [--save-charts <dir>] [--no-chart] [--json]');
33
43
  println(' [--detach] [--stop] Stream in the background, or stop it');
@@ -59,6 +69,8 @@ export function printRootHelp() {
59
69
  println(c.dim(' atlas workflow-create --name "FOMC day" --run-once-at "2026-09-17 14:00" --tools Stock-Quote --instruction "…" # once, at specific dates/times; --trigger-source manual = Run now only'));
60
70
  println(c.dim(' atlas alerts --symbol SPY --name greek_exposure --save-charts ./charts'));
61
71
  println(c.dim(' atlas flow stream --symbol NVDA'));
72
+ println(c.dim(' atlas signals performance --group <id>'));
73
+ println(c.dim(' atlas signals delete --group <id> --stop-subscribers'));
62
74
  }
63
75
 
64
76
  // Parse the `Args:` block of a tool docstring into { paramName: description }.