mindvest-atlas 0.23.0 → 0.23.2

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.23.0",
3
+ "version": "0.23.2",
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": {
@@ -195,6 +195,7 @@ async function handleAsk(ask, root) {
195
195
  out = await sharing.publish(ask.job || '', root, {
196
196
  name: ask.name, description: ask.description || '', notes: ask.notes || '',
197
197
  asNew: Boolean(ask.as_new), force: Boolean(ask.force),
198
+ exclude: ask.exclude,
198
199
  });
199
200
  } else if (ask.op === 'unshare') out = await sharing.unpublish(ask.job || '', root);
200
201
  else if (ask.op === 'peek') out = await sharing.peek(ask.share || '', root);
@@ -28,8 +28,14 @@ export async function screenshotCommand(args) {
28
28
  const url = process.env.ATLAS_ASK_URL;
29
29
  const token = process.env.ATLAS_ASK_TOKEN;
30
30
  if (!url || !token) {
31
- eprintln(color.red('Atlas Remote Control is not running, so nothing can draw the page.'));
32
- eprintln(color.dim('Open it and turn Local Mode on, then try again.'));
31
+ // NOT a Local Mode check, whatever it used to say. These variables are
32
+ // handed to a process Atlas starts — a script it runs, or an agent it
33
+ // spawned. Their absence means this command is running somewhere Atlas did
34
+ // not start it, and saying "turn Local Mode on" sent people to a switch
35
+ // that was already on (owner, 2026-09-05).
36
+ eprintln(color.red('This has to run from inside Atlas. Atlas is what draws the page.'));
37
+ eprintln(color.dim('Ask for it in a chat, or call screenshot() from the project\'s script. '
38
+ + 'Run by hand in a terminal there is nothing to photograph.'));
33
39
  return 1;
34
40
  }
35
41
 
@@ -25,6 +25,18 @@
25
25
  import { println } from '../util/ui.js';
26
26
  import * as sharing from '../sharing.js';
27
27
 
28
+ /** `--hold-back a/b.py` or `--hold-back=a,b`, and `--hold-back=` meaning none.
29
+ *
30
+ * undefined leaves the project's own choice alone; an EMPTY list is a real
31
+ * instruction ("share everything") and has to survive the difference. */
32
+ function holdBack(args) {
33
+ const raw = flag(args, 'hold-back', 'hold_back', 'exclude');
34
+ if (raw === undefined) return undefined;
35
+ if (typeof raw === 'boolean') return [];
36
+ const items = Array.isArray(raw) ? raw : String(raw).split(',');
37
+ return items.map((x) => String(x).trim()).filter(Boolean);
38
+ }
39
+
28
40
  function flag(args, ...names) {
29
41
  for (const name of names) {
30
42
  const value = args.flags[name];
@@ -38,6 +50,7 @@ const SUB_HELP = `atlas share <state|publish|off|peek|file|install|updates|fetch
38
50
  state --job <name> is this project shared, and its link
39
51
  publish --job <name> [--notes …] put the current files out as a new version
40
52
  [--as-new] [--force] [--name …] [--description …]
53
+ [--hold-back runs,notes.md] files to leave out (kept in atlas.json)
41
54
  off --job <name> stop sharing; the link stops working
42
55
  peek --link <id:token> download and describe, without installing
43
56
  file --package <id> --path <p> one file's text from a downloaded package
@@ -67,6 +80,7 @@ export async function shareCommand(args) {
67
80
  notes: String(flag(args, 'notes') || ''),
68
81
  asNew: Boolean(flag(args, 'as-new', 'as_new', 'new')),
69
82
  force: Boolean(flag(args, 'force')),
83
+ exclude: holdBack(args),
70
84
  });
71
85
  break;
72
86
  case 'off':
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.23.0';
5
+ export const VERSION = '0.23.2';
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';
@@ -1190,8 +1190,8 @@ export async function askModel(prompt, { job = '', tools = true, fresh = false,
1190
1190
  const token = process.env.ATLAS_ASK_TOKEN;
1191
1191
  if (!url || !token) {
1192
1192
  throw new Error(
1193
- 'No model is connected on this computer. Open Atlas Remote Control, '
1194
- + 'connect a model, and turn Local Mode on.'
1193
+ 'This script is not running through Atlas, so it has no way to reach a model. '
1194
+ + 'Press Run in Atlas, or let its schedule start it.'
1195
1195
  );
1196
1196
  }
1197
1197
  const res = await fetch(url, {
@@ -1240,8 +1240,8 @@ export async function chatModel(prompt, onEvent, { job = '', model = '', timeout
1240
1240
  const token = process.env.ATLAS_ASK_TOKEN;
1241
1241
  if (!url || !token) {
1242
1242
  onEvent({ kind: 'error', message:
1243
- 'No model is connected on this computer. Open Atlas Remote Control, '
1244
- + 'connect a model, and turn Local Mode on.' });
1243
+ 'This is not running through Atlas, so it has no way to reach a model. '
1244
+ + 'Press Run in Atlas, or let its schedule start it.' });
1245
1245
  return;
1246
1246
  }
1247
1247
  try {
package/src/sharing.js CHANGED
@@ -77,8 +77,53 @@ export function writePin(dir, pin) {
77
77
 
78
78
  // ── packing ────────────────────────────────────────────────────────────────
79
79
 
80
- /** Every file that may leave the machine, as POSIX relative paths. */
81
- export function packableFiles(dir) {
80
+ /** Where a project remembers what its author holds back. In `atlas.json`, with
81
+ * the schedule and the run line, because it is a setting OF the project and a
82
+ * person may want to read or change it without opening a screen. */
83
+ export const EXCLUDE_KEY = 'share_exclude';
84
+
85
+ /** Paths this project's author has chosen not to share. Never a secret list:
86
+ * the hard rules (.env, .venv, the dot folders) are not in here and cannot be,
87
+ * because a preference must not be the thing standing between a key and a
88
+ * stranger. */
89
+ export function readExclude(dir) {
90
+ try {
91
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, 'atlas.json'), 'utf8'));
92
+ const items = raw && typeof raw === 'object' ? raw[EXCLUDE_KEY] : null;
93
+ if (!Array.isArray(items)) return [];
94
+ return [...new Set(items.map((x) => String(x).trim().replace(/^\/+|\/+$/g, '')).filter(Boolean))].sort();
95
+ } catch { return []; }
96
+ }
97
+
98
+ /** Remember the choice, so the next publish holds the same things back. */
99
+ export function writeExclude(dir, paths) {
100
+ const file = path.join(dir, 'atlas.json');
101
+ let raw = {};
102
+ try {
103
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
104
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) raw = parsed;
105
+ } catch { raw = {}; }
106
+ const clean = [...new Set((paths || []).map((x) => String(x).trim().replace(/^\/+|\/+$/g, '')).filter(Boolean))].sort();
107
+ if (clean.length) raw[EXCLUDE_KEY] = clean; else delete raw[EXCLUDE_KEY];
108
+ try {
109
+ fs.writeFileSync(file, JSON.stringify(raw, null, 2));
110
+ return true;
111
+ } catch { return false; }
112
+ }
113
+
114
+ /** Is this file one the author left out — itself, or a folder holding it? */
115
+ const heldBack = (rel, exclude) =>
116
+ (exclude || []).some((item) => rel === item || rel.startsWith(`${item}/`));
117
+
118
+ /**
119
+ * Every file that may leave the machine, as POSIX relative paths.
120
+ *
121
+ * `exclude` is the author's own choice on top of the relay filter, and only
122
+ * ever SUBTRACTS. Nothing a person can tick makes a `.env` shareable: the two
123
+ * are different kinds of rule and are kept apart on purpose.
124
+ */
125
+ export function packableFiles(dir, exclude) {
126
+ const held = exclude === undefined ? readExclude(dir) : (exclude || []);
82
127
  const out = [];
83
128
  const walk = (at, prefix) => {
84
129
  let items = [];
@@ -90,6 +135,7 @@ export function packableFiles(dir) {
90
135
  if (item.isDirectory()) { walk(path.join(at, item.name), rel); continue; }
91
136
  if (!item.isFile() || isSecret(item.name)) continue;
92
137
  if (parts.length === 1 && HELPERS.has(item.name)) continue;
138
+ if (heldBack(rel, held)) continue;
93
139
  out.push(rel);
94
140
  }
95
141
  };
@@ -121,12 +167,16 @@ export function requiredKeys(dir, files) {
121
167
  return [...names].filter((n) => !n.startsWith('ATLAS_')).sort();
122
168
  }
123
169
 
124
- export function packProject(job, root, { name, description = '' } = {}) {
170
+ export function packProject(job, root, { name, description = '', exclude } = {}) {
125
171
  const dir = projectDir(job, root);
126
172
  if (!fs.existsSync(dir)) return { ok: false, error: `There is no project called ${job} here.` };
127
- const files = packableFiles(dir);
173
+ const files = packableFiles(dir, exclude);
128
174
  if (!files.length) {
129
- return { ok: false, error: 'This project has nothing in it yet, so there is nothing to share.' };
175
+ return {
176
+ ok: false,
177
+ error: 'Nothing would be shared. Every file in this project is either held back '
178
+ + 'or one Atlas never shares.',
179
+ };
130
180
  }
131
181
  const hashes = {};
132
182
  const entries = [];
@@ -150,8 +200,14 @@ export function packProject(job, root, { name, description = '' } = {}) {
150
200
 
151
201
  // ── publishing ─────────────────────────────────────────────────────────────
152
202
 
153
- export async function publish(job, root, { name, description = '', notes = '', asNew = false, force = false } = {}) {
203
+ export async function publish(job, root, {
204
+ name, description = '', notes = '', asNew = false, force = false, exclude,
205
+ } = {}) {
154
206
  const dir = projectDir(job, root);
207
+ // The choice is REMEMBERED before it is used, so the next publish holds the
208
+ // same things back without being told again — and so a person can read it in
209
+ // atlas.json rather than having to open a screen to find out.
210
+ if (exclude !== undefined && exclude !== null) writeExclude(dir, exclude);
155
211
  const packed = packProject(job, root, { name, description });
156
212
  if (!packed.ok) return packed;
157
213
 
@@ -219,11 +275,31 @@ export async function unpublish(job, root) {
219
275
  return { ok: true };
220
276
  }
221
277
 
278
+ /**
279
+ * Is this project shared, from where, and exactly what would go.
280
+ *
281
+ * The file list comes back with it so the share screen can show what is IN a
282
+ * publish before it happens — and let the author take something out. It is the
283
+ * real packing list, not a guess at one: the same call that packs produces it.
284
+ */
222
285
  export function shareState(job, root) {
223
- const pin = readPin(projectDir(job, root));
224
- if (!pin) return { ok: true, shared: false, mine: true };
225
- return {
286
+ const dir = projectDir(job, root);
287
+ const exclude = readExclude(dir);
288
+ const shareable = packableFiles(dir, []);
289
+ const base = {
226
290
  ok: true,
291
+ shared: false,
292
+ mine: true,
293
+ // Every file that COULD go, whether or not it is going. NOT `files`: that
294
+ // word already means "what is in a package" on the peek side, and one name
295
+ // with two meanings is a bug waiting for a careless reader.
296
+ shareable,
297
+ exclude: exclude.filter((x) => shareable.some((f) => f === x || f.startsWith(`${x}/`))),
298
+ };
299
+ const pin = readPin(dir);
300
+ if (!pin) return base;
301
+ return {
302
+ ...base,
227
303
  shared: Boolean(pin.share),
228
304
  share: pin.share,
229
305
  package_id: pin.package_id,
@@ -313,7 +389,7 @@ export async function peek(shareText, root) {
313
389
  export function peekRead(packageId, filePath, root) {
314
390
  const zipPath = path.join(stagingDir(root, packageId), 'package.zip');
315
391
  if (!fs.existsSync(zipPath)) {
316
- return { ok: false, error: 'Open the link again — this download has gone.' };
392
+ return { ok: false, error: 'Open the link again: this download has gone.' };
317
393
  }
318
394
  try {
319
395
  const found = unzip(fs.readFileSync(zipPath)).find((e) => e.name === filePath);
@@ -370,7 +446,7 @@ export function install(packageId, root, { job } = {}) {
370
446
  const stage = stagingDir(root, packageId);
371
447
  const zipPath = path.join(stage, 'package.zip');
372
448
  if (!fs.existsSync(zipPath)) {
373
- return { ok: false, error: 'Open the link again — this download has gone.' };
449
+ return { ok: false, error: 'Open the link again: this download has gone.' };
374
450
  }
375
451
  let manifest = {};
376
452
  try {