atris 3.82.14 → 3.82.15

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.
@@ -52,38 +52,38 @@ const {
52
52
  findAtrisDir,
53
53
  } = require('../lib/business-core');
54
54
 
55
- async function findExistingBusinessBySlug(slug, token) {
55
+ async function findExistingBusinessBySlug(slug, token, { api = apiRequestJson, loadLocal = loadBusinesses } = {}) {
56
56
  if (!slug) return null;
57
57
 
58
- // Local cache first, no network round-trip needed.
59
- const local = loadBusinesses();
60
- if (local[slug]) {
61
- return { id: local[slug].business_id, name: local[slug].name, slug, source: 'local' };
62
- }
63
- for (const v of Object.values(local)) {
64
- if (businessMatchesSlug(v, slug)) {
65
- return { id: v.business_id, name: v.name, slug, source: 'local' };
66
- }
67
- }
58
+ const local = loadLocal();
59
+ const localMatch = local[slug]
60
+ ? { id: local[slug].business_id, name: local[slug].name, slug, source: 'local' }
61
+ : (() => {
62
+ const v = Object.values(local).find((b) => businessMatchesSlug(b, slug));
63
+ return v ? { id: v.business_id, name: v.name, slug, source: 'local' } : null;
64
+ })();
68
65
 
69
- if (!token) return null;
66
+ if (!token) return localMatch;
70
67
 
71
- // Cloud lookup, covers businesses the user is a member of but hasn't added.
72
- const direct = await apiRequestJson(`/business/by-slug/${encodeURIComponent(slug)}`, {
73
- method: 'GET',
74
- token,
75
- });
68
+ // The cloud is the truth. The Mac's list can hold a business that was
69
+ // deleted, and trusting it refused to recreate one that no longer existed.
70
+ const direct = await api(`/business/by-slug/${encodeURIComponent(slug)}`, { method: 'GET', token });
76
71
  if (direct.ok && direct.data && direct.data.id) {
77
72
  return { id: direct.data.id, name: direct.data.name, slug: direct.data.slug || slug, source: 'cloud' };
78
73
  }
79
74
 
80
- const list = await apiRequestJson('/business/', { method: 'GET', token });
75
+ const list = await api('/business/', { method: 'GET', token });
81
76
  if (list.ok && Array.isArray(list.data)) {
82
77
  const match = list.data.find(b => businessMatchesSlug(b, slug));
83
78
  if (match) return { id: match.id, name: match.name, slug: match.slug, source: 'cloud' };
79
+ if (localMatch) {
80
+ console.warn(` Note: this Mac still lists "${slug}", but the cloud has no such business. Run: atris business doctor --fix`);
81
+ }
82
+ return null;
84
83
  }
85
84
 
86
- return null;
85
+ // Cloud unreachable: fall back to the Mac's list rather than risk a duplicate.
86
+ return localMatch;
87
87
  }
88
88
 
89
89
  async function addBusiness(slug) {
@@ -1665,6 +1665,7 @@ async function businessCommand(subcommand, ...args) {
1665
1665
  }
1666
1666
 
1667
1667
  module.exports = {
1668
+ findExistingBusinessBySlug,
1668
1669
  businessCommand,
1669
1670
  businessWorkspaceBase,
1670
1671
  loadBusinesses,
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // atris memory show, search, read, ls, remember, write, rm, usage, log, lesson
3
+ // atris memory show, search, ask, read, ls, remember, write, rm, usage, log, lesson
4
4
  // and agents: the person's Atris memory from any terminal, so any agent can
5
5
  // read it, search it and save to it. One request per command. The login and
6
6
  // network modules load only when a command needs the server.
@@ -13,6 +13,7 @@ const { NOT_LOGGED_IN, oneLine, tokenFrom } = require('../lib/developer-api');
13
13
  const MAX_WRITE_BYTES = 1000000;
14
14
  const STDIN_WAIT_MS = 5000;
15
15
  const REMEMBER_MAX_CHARS = 1000;
16
+ const ASK_MAX_CHARS = 500;
16
17
  const REMEMBER_FILES = ['journal', 'now', 'learning'];
17
18
  const LEARNING_SECTIONS = ['want', 'understand', 'shaky'];
18
19
 
@@ -138,6 +139,20 @@ const COMMANDS = {
138
139
  },
139
140
  },
140
141
 
142
+ ask: {
143
+ check(ctx) {
144
+ const question = ctx.words.join(' ').replace(/\s+/g, ' ').trim();
145
+ if (!question) throw new UsageError('ask a question, like atris memory ask "where does Sam work?"');
146
+ if (question.length > ASK_MAX_CHARS) throw new UsageError('keep the question under 500 characters');
147
+ return { question, folder: ctx.flags['--in'] || '' };
148
+ },
149
+ async run(ctx, { question, folder }) {
150
+ // The server reads for up to 75 seconds; asking twice would count twice.
151
+ const data = await ctx.call(`/memory/ask?q=${enc(question)}${folder ? `&folder=${enc(folder)}` : ''}`, { timeoutMs: 90000, retries: 0 });
152
+ return { data, lines: live.askLines(data) };
153
+ },
154
+ },
155
+
141
156
  read: {
142
157
  check(ctx) {
143
158
  const rel = onePath(ctx, 'file to read');
package/commands/push.js CHANGED
@@ -9,6 +9,17 @@ const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
9
9
  const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
10
10
  const { assertSafeWorkspaceRoot } = require('../lib/workspace-safety');
11
11
 
12
+ // The server's own words for a refused request. It used to be dropped, so a
13
+ // refusal for any reason read as "you can only push to your team/ folder".
14
+ function refusalText(result) {
15
+ if (!result) return '';
16
+ const raw = result.errorMessage || result.error || (result.data && result.data.detail) || '';
17
+ if (!raw) return '';
18
+ if (typeof raw === 'string') return raw === 'Request failed' ? '' : raw;
19
+ if (typeof raw === 'object' && typeof raw.message === 'string') return raw.message;
20
+ try { return JSON.stringify(raw); } catch { return String(raw); }
21
+ }
22
+
12
23
  function resolvePushSourceDir({ slug, argv = process.argv, cwd = process.cwd() }) {
13
24
  const fromIdx = argv.indexOf('--from');
14
25
  if (fromIdx !== -1 && argv[fromIdx + 1]) {
@@ -858,13 +869,15 @@ async function pushAtris() {
858
869
  pushed = landedPaths.size;
859
870
  break;
860
871
  } else {
861
- console.error(`\n Push failed: ${retry.errorMessage || retry.error || retry.status}`);
872
+ console.error(`\n Push failed: ${refusalText(retry) || retry.status}`);
873
+ if (refusalText(result)) console.error(` The first refusal said: ${refusalText(result)}`);
862
874
  await emit('access_denied', { error_detail: `403 retry failed: ${retry.status}` });
863
875
  process.exit(1);
864
876
  }
865
877
  } else {
866
- console.error('\n Access denied: you can only push to your team/ folder.');
867
- await emit('access_denied');
878
+ const reason = refusalText(result);
879
+ console.error(`\n Access denied: ${reason || 'the server refused this push and gave no reason.'}`);
880
+ await emit('access_denied', reason ? { error_detail: reason } : undefined);
868
881
  process.exit(1);
869
882
  }
870
883
  } else if (result.status === 409) {
@@ -1052,6 +1065,7 @@ async function pushAtris() {
1052
1065
  }
1053
1066
 
1054
1067
  module.exports = {
1068
+ refusalText,
1055
1069
  pushAtris,
1056
1070
  buildPushChangePlan,
1057
1071
  buildPushUploadBatches,
@@ -9,7 +9,7 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
 
11
11
  const LIVE_COMMANDS = new Set([
12
- 'show', 'search', 'read', 'ls', 'remember', 'write', 'rm', 'usage', 'log', 'lesson', 'agents', 'setup',
12
+ 'show', 'search', 'ask', 'read', 'ls', 'remember', 'write', 'rm', 'usage', 'log', 'lesson', 'agents', 'setup',
13
13
  ]);
14
14
 
15
15
  function isLiveCommand(word) {
@@ -19,10 +19,11 @@ function isLiveCommand(word) {
19
19
  // One line per command, shown together by `atris memory` and one at a time by
20
20
  // `atris memory <command> --help`.
21
21
  const COLUMN = 38;
22
- const row = (form, ...what) => [` ${form}`.padEnd(COLUMN) + what[0], ...what.slice(1).map((more) => ' '.repeat(COLUMN) + more)].join('\n');
22
+ const row = (form, ...what) => [` ${form} `.padEnd(COLUMN) + what[0], ...what.slice(1).map((more) => ' '.repeat(COLUMN) + more)].join('\n');
23
23
  const USAGE_LINES = {
24
24
  show: row('show', 'who they are and what matters now; read it before helping'),
25
25
  search: row('search "rent|lease" [--in folder]', 'lines with any of the words, split by |'),
26
+ ask: row('ask "a question" [--in folder]', 'answered from their memory, with the lines it rests on'),
26
27
  read: row('read <path> [--from N] [--lines N]', 'numbered lines of one file; --raw prints it as stored'),
27
28
  ls: row('ls [folder]', 'what is in a folder; MAP.md says what is where'),
28
29
  remember: row('remember "one short line"', 'a lasting fact or preference about the person, when they',
@@ -43,6 +44,7 @@ const LIVE_HELP = [
43
44
  'read',
44
45
  USAGE_LINES.show,
45
46
  USAGE_LINES.search,
47
+ USAGE_LINES.ask,
46
48
  USAGE_LINES.read,
47
49
  USAGE_LINES.ls,
48
50
  '',
@@ -62,6 +64,7 @@ const LIVE_HELP = [
62
64
  // Flags that take a value, per command. --json, --raw and --help take none.
63
65
  const VALUE_FLAGS = {
64
66
  search: ['--in'],
67
+ ask: ['--in'],
65
68
  read: ['--from', '--lines'],
66
69
  ls: [],
67
70
  show: [],
@@ -205,6 +208,26 @@ function searchLines(query, data) {
205
208
  return lines;
206
209
  }
207
210
 
211
+ // The answer first, then each line it rests on, then how sure it is. With no
212
+ // answer (the model was busy) the search hits it started from are shown.
213
+ function askLines(data) {
214
+ const sources = (data && data.sources) || [];
215
+ const cite = sources.map((s) => ` ${s.path}:${s.n}: ${s.text}`);
216
+ if (data && data.answer && data.found) {
217
+ return [String(data.answer), ...(cite.length ? ['', 'From:', ...cite] : [])];
218
+ }
219
+ if (data && data.answer) {
220
+ const lines = [String(data.answer)];
221
+ if (cite.length) lines.push('', 'Closest lines:', ...cite);
222
+ lines.push('', 'Not found for certain. Search it yourself: atris memory search "word|other word"');
223
+ return lines;
224
+ }
225
+ const hits = (data && data.search && data.search.hits) || [];
226
+ const lines = [String((data && data.note) || 'No answer right now.')];
227
+ if (hits.length) lines.push('', 'Lines that may hold it:', ...hits.slice(0, 8).map((h) => ` ${h.path}:${h.n}: ${h.text}`));
228
+ return lines;
229
+ }
230
+
208
231
  function readLines(data) {
209
232
  const page = (data && data.lines) || [];
210
233
  const total = Number(data && data.total) || 0;
@@ -244,7 +267,7 @@ function rulesBlock(agent) {
244
267
  '',
245
268
  'This person keeps one memory in Atris that every app and agent shares. Use it.',
246
269
  '- Start each task with `atris memory show`: who they are and what matters now.',
247
- '- Before you ask them anything they may have said before, search: `atris memory search "word|other word"`.',
270
+ '- Before you ask them anything they may have said before, search: `atris memory search "word|other word"`, or ask in plain words: `atris memory ask "..."`.',
248
271
  '- Open what you find with `atris memory read <path>`. `atris memory ls` shows the folder.',
249
272
  '- When they say remember, note, save or don\'t forget, save one short line: `atris memory remember "..."`.',
250
273
  '- Save only lasting facts about them, never logs or your own work. Never rewrite their about.md.',
@@ -338,6 +361,7 @@ module.exports = {
338
361
  END,
339
362
  LIVE_HELP,
340
363
  USAGE_LINES,
364
+ askLines,
341
365
  isLiveCommand,
342
366
  lsLines,
343
367
  mcpLines,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.82.14",
3
+ "version": "3.82.15",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof. project memory your coding agents share across sessions.",
5
5
  "mcpName": "io.github.atrislabs/atris",
6
6
  "main": "bin/atris.js",