mindvest-atlas 0.55.0 → 0.56.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.55.0",
3
+ "version": "0.56.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.55.0';
5
+ export const VERSION = '0.56.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';
@@ -1615,6 +1615,48 @@ const STARTER_TRADES = JSON.stringify({
1615
1615
  * project. Nothing here is ever overwritten — only what is MISSING is written,
1616
1616
  * because everything in this list is the agent's to change once it exists.
1617
1617
  */
1618
+ /**
1619
+ * WHAT A SHARED COPY DOES NOT CARRY. A starter like the rest: written when
1620
+ * absent, never touched afterwards, so a project that predates it gets one and
1621
+ * an author who has edited theirs keeps every word.
1622
+ *
1623
+ * The three defaults are the author's OWN output, not the project - the
1624
+ * runner's logs, the trades this job recorded, and the numbers on their
1625
+ * dashboard. Sharing those hands somebody else's record to whoever imports it,
1626
+ * and reads on their Performance page as if it were theirs. Nothing breaks by
1627
+ * holding them back: ensureProjectFiles writes a fresh trades.json and
1628
+ * dashboard.json into the imported copy before its first run.
1629
+ *
1630
+ * They are held back VISIBLY - the share dialog shows them unticked with the
1631
+ * reason - so an author who does mean to publish their record can say so.
1632
+ */
1633
+ const STARTER_SHAREIGNORE = `# What this project keeps to itself
1634
+
1635
+ Anything listed here is held back when the project is shared. One rule per line.
1636
+ A \`#\` is a comment, a list marker is fine, and a plain name matches that file or
1637
+ anything under it. A trailing slash means a folder: \`runs/\` holds back
1638
+ everything inside it, however deep. A \`*\` is a glob, matched against the whole
1639
+ path and against the file's own name, so \`*.csv\` catches one three folders down.
1640
+
1641
+ These three are here from the start because they are YOURS, not the project's:
1642
+ your logs and your numbers. Delete a line to share it anyway.
1643
+
1644
+ - runs/ every past run, and what the runner wrote while working
1645
+ - trades.json the trades this job recorded. Your record, not theirs
1646
+ - dashboard.json what your last run put on the dashboard
1647
+
1648
+ Add to it as you build - a downloaded file, a scratch folder, an export somebody
1649
+ handed you:
1650
+
1651
+ # - add/
1652
+ # - data/*.csv
1653
+ # - scratch.py
1654
+
1655
+ It can only ever hold things BACK. Nothing written here makes a \`.env\`
1656
+ shareable: secrets never travel whatever this file says, so this is for what is
1657
+ merely private, not for what is dangerous.
1658
+ `;
1659
+
1618
1660
  export function starterFiles(slug) {
1619
1661
  return [
1620
1662
  ['README.md', STARTER_README(slug)],
@@ -1626,6 +1668,7 @@ export function starterFiles(slug) {
1626
1668
  ['dashboard.json', STARTER_DASHBOARD_JSON(slug)],
1627
1669
  ['atlas.json', STARTER_MANIFEST],
1628
1670
  ['trades.json', STARTER_TRADES],
1671
+ ['shareignore.md', STARTER_SHAREIGNORE],
1629
1672
  ['.env', '# KEY=value - one per line. Values stay on this computer.\n'],
1630
1673
  ];
1631
1674
  }
package/src/sharing.js CHANGED
@@ -115,6 +115,93 @@ export function writeExclude(dir, paths) {
115
115
  const heldBack = (rel, exclude) =>
116
116
  (exclude || []).some((item) => rel === item || rel.startsWith(`${item}/`));
117
117
 
118
+ /** The file a project uses to say what it keeps to itself. */
119
+ export const SHAREIGNORE = 'shareignore.md';
120
+
121
+ /**
122
+ * Patterns this project holds back on its own account. Mirror of
123
+ * `sharing.read_shareignore` — the same file is read by whichever CLI happens
124
+ * to publish, and a project that held something back under one and shipped it
125
+ * under the other is the worst kind of difference.
126
+ *
127
+ * A MARKDOWN FILE WITH PROSE IN IT, because that is what it is called and what
128
+ * an agent writes. So the reader has to tell a rule from a sentence, and the
129
+ * test is that A RULE IS A PATH: no spaces in it. Everything else on the line
130
+ * is a description.
131
+ *
132
+ * - runs/ every past run -> runs
133
+ * trades.json -> trades.json
134
+ * `my report.csv` -> my report.csv
135
+ * Anything listed here is held back. -> prose, not a rule
136
+ */
137
+ export function readShareignore(dir) {
138
+ let raw = '';
139
+ try { raw = fs.readFileSync(path.join(dir, SHAREIGNORE), 'utf8'); } catch { return []; }
140
+ const out = new Set();
141
+ for (const line of raw.split(/\r?\n/)) {
142
+ let item = line.trim();
143
+ // A heading or a commented-out rule. Both are the author talking, not
144
+ // asking for something.
145
+ if (!item || item.startsWith('#')) continue;
146
+ if (/^[-*+] /.test(item)) item = item.slice(2).trim();
147
+ // A description follows the rule after a COLUMN of space (or a tab, or a
148
+ // `#`), never after the single space that holds a sentence together.
149
+ item = item.split(/\s{2,}|\t|\s+#/)[0].trim();
150
+ const quoted = item.startsWith('`') && item.replace(/[.,]+$/, '').endsWith('`');
151
+ item = item.replace(/^`+|`+$/g, '').trim().replace(/\/+$/, '').trim();
152
+ if (!item) continue;
153
+ // THE TEST THAT KEEPS PROSE OUT. A sentence has spaces in it and a path
154
+ // does not, so anything still carrying one is a line the author wrote to be
155
+ // read, unless they said otherwise with backticks.
156
+ if (item.includes(' ') && !quoted) continue;
157
+ out.add(item);
158
+ }
159
+ return [...out].sort();
160
+ }
161
+
162
+ /**
163
+ * Does this project's own shareignore hold this file back?
164
+ *
165
+ * A plain path matches itself and anything under it; anything with a `*` in it
166
+ * is matched as a glob, against the whole path AND against the file's own name,
167
+ * so `*.csv` catches one three folders down without anybody having to write
168
+ * `**\/*.csv`.
169
+ */
170
+ export function ignoredHere(rel, patterns) {
171
+ const name = rel.split('/').pop();
172
+ for (const item of patterns || []) {
173
+ if (/[*?[]/.test(item)) {
174
+ // The three glob characters, and nothing else treated as special: a
175
+ // pattern is a file name, so a dot or a dash in it means itself.
176
+ const rx = new RegExp(`^${item.replace(/[.+^${}()|\\]/g, '\\$&')
177
+ .replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]')
178
+ .replace(/\[([^\]]+)\]/g, '[$1]')}$`);
179
+ if (rx.test(rel) || rx.test(name)) return true;
180
+ } else if (rel === item || rel.startsWith(`${item}/`)) {
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+
187
+ /**
188
+ * What this project's own shareignore is holding back, as real paths, so the
189
+ * share dialog can show them ALREADY UNTICKED with the reason. A file simply
190
+ * missing from the list looks like a file Atlas lost; one shown as auto-ignored
191
+ * is a decision somebody can see and disagree with.
192
+ */
193
+ export function heldBackHere(dir) {
194
+ const patterns = readShareignore(dir);
195
+ if (!patterns.length) return [];
196
+ // packableFiles already applies the relay filter and the secret rule; asking
197
+ // it for everything the author has NOT hand-excluded and then keeping what
198
+ // this file catches means the two lists can never disagree about what a file
199
+ // even is.
200
+ return packableFiles(dir, [], { applyIgnore: false })
201
+ .filter((rel) => ignoredHere(rel, patterns))
202
+ .sort();
203
+ }
204
+
118
205
  /**
119
206
  * Every file that may leave the machine, as POSIX relative paths.
120
207
  *
@@ -122,8 +209,17 @@ const heldBack = (rel, exclude) =>
122
209
  * ever SUBTRACTS. Nothing a person can tick makes a `.env` shareable: the two
123
210
  * are different kinds of rule and are kept apart on purpose.
124
211
  */
125
- export function packableFiles(dir, exclude) {
212
+ export function packableFiles(dir, exclude, { applyIgnore = true } = {}) {
126
213
  const held = exclude === undefined ? readExclude(dir) : (exclude || []);
214
+ // WHAT THE PROJECT ITSELF HOLDS BACK, on top of the author's own choice. Read
215
+ // here rather than passed in, because it is the project's rule and not a
216
+ // caller's: every path that packs a folder gets it without asking.
217
+ //
218
+ // `applyIgnore: false` is for the ONE caller that needs the list as it would
219
+ // be without this file, so it can say what the file caught. It walks the same
220
+ // folder the same way rather than a second walk that could disagree about
221
+ // what a file even is.
222
+ const ignored = applyIgnore ? readShareignore(dir) : [];
127
223
  const out = [];
128
224
  const walk = (at, prefix) => {
129
225
  let items = [];
@@ -136,6 +232,7 @@ export function packableFiles(dir, exclude) {
136
232
  if (!item.isFile() || isSecret(item.name)) continue;
137
233
  if (parts.length === 1 && HELPERS.has(item.name)) continue;
138
234
  if (heldBack(rel, held)) continue;
235
+ if (ignoredHere(rel, ignored)) continue;
139
236
  out.push(rel);
140
237
  }
141
238
  };
@@ -327,11 +424,21 @@ export async function unpublish(job, root) {
327
424
  export function shareState(job, root) {
328
425
  const dir = projectDir(job, root);
329
426
  const exclude = readExclude(dir);
330
- const shareable = packableFiles(dir, []);
427
+ const packing = packableFiles(dir, []);
428
+ // SHOWN, THOUGH THEY ARE NOT GOING. A file the project's own shareignore
429
+ // holds back is already out of `packing`, and a file that simply is not in
430
+ // the list looks like one Atlas lost. So they are put back into what the
431
+ // dialog SEES, and named separately as the reason — a decision somebody can
432
+ // read and disagree with, rather than an absence.
433
+ const auto = heldBackHere(dir);
434
+ const shareable = [...new Set([...packing, ...auto])].sort();
331
435
  const base = {
332
436
  ok: true,
333
437
  shared: false,
334
438
  mine: true,
439
+ // Held back by the project itself, not by a tick. The way to change one is
440
+ // to edit shareignore.md, which is the single place that says so.
441
+ auto_ignored: auto,
335
442
  // Every file that COULD go, whether or not it is going. NOT `files`: that
336
443
  // word already means "what is in a package" on the peek side, and one name
337
444
  // with two meanings is a bug waiting for a careless reader.