atris 3.82.11 → 3.82.12

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.
@@ -0,0 +1,356 @@
1
+ 'use strict';
2
+
3
+ // Atris memory for any agent in any terminal, the parts with no network: the
4
+ // help screen, how each command reads its arguments, how answers are worded,
5
+ // and the rules block a person pastes into an agent's rules file. Kept free of
6
+ // heavy requires so `atris memory --help` and the fast path stay cheap.
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const LIVE_COMMANDS = new Set([
12
+ 'show', 'search', 'read', 'ls', 'remember', 'write', 'rm', 'usage', 'log', 'lesson', 'agents', 'setup',
13
+ ]);
14
+
15
+ function isLiveCommand(word) {
16
+ return LIVE_COMMANDS.has(String(word || ''));
17
+ }
18
+
19
+ // One line per command, shown together by `atris memory` and one at a time by
20
+ // `atris memory <command> --help`.
21
+ const COLUMN = 38;
22
+ const row = (form, ...what) => [` ${form}`.padEnd(COLUMN) + what[0], ...what.slice(1).map((more) => ' '.repeat(COLUMN) + more)].join('\n');
23
+ const USAGE_LINES = {
24
+ show: row('show', 'who they are and what matters now; read it before helping'),
25
+ search: row('search "rent|lease" [--in folder]', 'lines with any of the words, split by |'),
26
+ read: row('read <path> [--from N] [--lines N]', 'numbered lines of one file; --raw prints it as stored'),
27
+ ls: row('ls [folder]', 'what is in a folder; MAP.md says what is where'),
28
+ remember: row('remember "one short line"', 'a lasting fact or preference about the person, when they',
29
+ 'say remember, note, save or don\'t forget [--to now|learning]'),
30
+ write: row('write <path> [--file f]', 'save a page under wiki/ or a file under files/, or pipe it in'),
31
+ rm: row('rm <path>', 'delete a page under wiki/ or a file under files/'),
32
+ usage: row('usage', 'file space used and what the plan gives'),
33
+ log: row('log "what I did" --as <agent>', 'one line in your notebook, team/<agent>/logs/<today>'),
34
+ lesson: row('lesson "text" --before N --after N', 'kept in team/<agent>/lessons.md only when the score', 'went up (--as <agent>)'),
35
+ agents: row('agents [--write AGENTS.md]', 'rules to paste into any agent, plus the MCP setup lines'),
36
+ };
37
+ USAGE_LINES.setup = USAGE_LINES.agents;
38
+
39
+ const LIVE_HELP = [
40
+ 'atris memory: the person\'s own memory, shared by every app and agent they use.',
41
+ 'Every command takes --json. Sign in once with: atris login',
42
+ '',
43
+ 'read',
44
+ USAGE_LINES.show,
45
+ USAGE_LINES.search,
46
+ USAGE_LINES.read,
47
+ USAGE_LINES.ls,
48
+ '',
49
+ 'save',
50
+ USAGE_LINES.remember,
51
+ USAGE_LINES.write,
52
+ USAGE_LINES.rm,
53
+ USAGE_LINES.usage,
54
+ '',
55
+ 'notebook (an agent\'s own; --as defaults to $ATRIS_AGENT)',
56
+ USAGE_LINES.log,
57
+ USAGE_LINES.lesson,
58
+ USAGE_LINES.agents,
59
+ '',
60
+ ];
61
+
62
+ // Flags that take a value, per command. --json, --raw and --help take none.
63
+ const VALUE_FLAGS = {
64
+ search: ['--in'],
65
+ read: ['--from', '--lines'],
66
+ ls: [],
67
+ show: [],
68
+ remember: ['--to', '--section', '--as'],
69
+ write: ['--file'],
70
+ rm: [],
71
+ usage: [],
72
+ log: ['--as'],
73
+ lesson: ['--before', '--after', '--as'],
74
+ agents: ['--write', '--as'],
75
+ setup: ['--write', '--as'],
76
+ };
77
+ const BOOL_FLAGS = { read: ['--raw'] };
78
+ const ALL_VALUE_FLAGS = new Set(Object.values(VALUE_FLAGS).flat());
79
+ const ALL_BOOL_FLAGS = new Set(Object.values(BOOL_FLAGS).flat());
80
+
81
+ // { sub, words, flags, json, help } or { error }. Everything after -- is a
82
+ // word, so a line that starts with a dash still gets through.
83
+ function parseLiveArgs(args = []) {
84
+ const out = { sub: '', words: [], flags: {}, json: false, help: false };
85
+ const words = [];
86
+ const used = [];
87
+ let rest = false;
88
+ for (let i = 0; i < args.length; i += 1) {
89
+ const arg = String(args[i]);
90
+ if (rest || arg === '-' || !arg.startsWith('-')) { words.push(arg); continue; }
91
+ if (arg === '--') { rest = true; continue; }
92
+ if (arg === '--json') { out.json = true; continue; }
93
+ if (arg === '--help' || arg === '-h') { out.help = true; continue; }
94
+ const eq = arg.indexOf('=');
95
+ const name = eq === -1 ? arg : arg.slice(0, eq);
96
+ if (ALL_VALUE_FLAGS.has(name)) {
97
+ const value = eq === -1 ? args[i + 1] : arg.slice(eq + 1);
98
+ if (eq === -1) i += 1;
99
+ if (value === undefined || value === '') return { error: `${name} needs a value` };
100
+ out.flags[name] = String(value);
101
+ } else if (ALL_BOOL_FLAGS.has(name) && eq === -1) {
102
+ out.flags[name] = true;
103
+ } else {
104
+ return { error: `unknown option ${name}. To pass words that start with a dash, put -- before them.` };
105
+ }
106
+ used.push(name);
107
+ }
108
+ out.sub = words.shift() || '';
109
+ out.words = words;
110
+ const allowed = new Set([...(VALUE_FLAGS[out.sub] || []), ...(BOOL_FLAGS[out.sub] || [])]);
111
+ const wrong = used.find((name) => !allowed.has(name));
112
+ if (wrong) return { error: `${wrong} does not work with atris memory ${out.sub}` };
113
+ return out;
114
+ }
115
+
116
+ function positiveInt(value, name) {
117
+ if (value === undefined) return { value: undefined };
118
+ const n = Number(value);
119
+ if (!Number.isInteger(n) || n < 1) return { error: `${name} must be a whole number, 1 or more` };
120
+ return { value: n };
121
+ }
122
+
123
+ function plainNumber(value, name) {
124
+ const n = Number(value);
125
+ if (value === undefined || value === '' || !Number.isFinite(n)) return { error: `${name} needs a number` };
126
+ return { value: n };
127
+ }
128
+
129
+ function size(bytes) {
130
+ const n = Number(bytes) || 0;
131
+ if (n < 1000) return `${n} B`;
132
+ if (n < 1000000) return `${(n / 1000).toFixed(n < 10000 ? 1 : 0)} KB`;
133
+ return `${(n / 1000000).toFixed(1)} MB`;
134
+ }
135
+
136
+ // ---------------------------------------------------------------- show
137
+
138
+ const SHOW_SECTIONS = [
139
+ ['about', 'About them, in their words', 'about.md'],
140
+ ['now', 'What they are working on now', 'now.md'],
141
+ ['learning', 'Their learning', 'wiki/learning.md'],
142
+ ['board', 'Their board: the goal, the deadline, the doors they are knocking on', 'board.md'],
143
+ ];
144
+ const SECTION_MAX_CHARS = 2400;
145
+ const JOURNAL_LINES = 15;
146
+
147
+ // The file without its title line. Empty when only headings are left, which is
148
+ // how a fresh note looks (learning.md starts with three empty headings).
149
+ function noteBody(text) {
150
+ const body = String(text || '').replace(/\r\n/g, '\n').replace(/^# .*\n?/, '').trim();
151
+ const hasWords = body.split('\n').some((line) => line.trim() && !/^#{1,6} /.test(line.trim()));
152
+ return hasWords ? body : '';
153
+ }
154
+
155
+ function clip(body, readPath) {
156
+ if (body.length <= SECTION_MAX_CHARS) return body;
157
+ const cut = body.slice(0, SECTION_MAX_CHARS);
158
+ const atLine = cut.lastIndexOf('\n');
159
+ return `${atLine > 0 ? cut.slice(0, atLine) : cut}\n(more: atris memory read ${readPath})`;
160
+ }
161
+
162
+ function journalLines(text, max = JOURNAL_LINES) {
163
+ return String(text || '').split(/\r?\n/).filter((line) => line.startsWith('- ')).slice(0, max);
164
+ }
165
+
166
+ // The person's notes as one short block for a model, from GET /me/memory.
167
+ function showText(data) {
168
+ const files = (data && data.files) || {};
169
+ const parts = ['# Atris memory', 'The person\'s own notes, shared by every app they use. Use them to fit your help; do not recite them.'];
170
+ let any = false;
171
+ for (const [name, label, readPath] of SHOW_SECTIONS) {
172
+ const body = noteBody(files[name]);
173
+ if (!body) continue;
174
+ any = true;
175
+ // Headings inside a note sit one level under the section title.
176
+ parts.push('', `## ${label} (${readPath})`, clip(body, readPath).replace(/^(#{2,5}) /gm, '#$1 '));
177
+ }
178
+ const recent = journalLines(files.journal);
179
+ if (recent.length) {
180
+ any = true;
181
+ parts.push('', '## Recently they said, newest first', ...recent);
182
+ }
183
+ if (!any) {
184
+ parts.push('', 'Nothing saved yet. When they tell you something lasting about themselves, save it:',
185
+ 'atris memory remember "one short line"');
186
+ return parts.join('\n');
187
+ }
188
+ parts.push('', 'More: atris memory search "word|other word". Save: atris memory remember "one short line".');
189
+ return parts.join('\n');
190
+ }
191
+
192
+ // ---------------------------------------------------------------- other answers
193
+
194
+ function searchLines(query, data) {
195
+ const hits = (data && data.hits) || [];
196
+ const scanned = data && Number.isFinite(Number(data.files)) ? Number(data.files) : null;
197
+ const where = scanned === null ? '' : ` (searched ${scanned} ${scanned === 1 ? 'file' : 'files'})`;
198
+ if (!hits.length) {
199
+ const words = String(query).split('|').map((w) => w.trim()).filter(Boolean).map((w) => `"${w}"`);
200
+ return [`No lines with ${words.join(' or ')}${where}.`];
201
+ }
202
+ const lines = hits.map((hit) => `${hit.path}:${hit.n}: ${hit.text}`);
203
+ lines.push(`${hits.length} ${hits.length === 1 ? 'line' : 'lines'}${where}. Read around one: atris memory read <path> --from <line>`);
204
+ if (data.truncated) lines.push('More lines match than shown. Narrow it with fewer words or --in <folder>.');
205
+ return lines;
206
+ }
207
+
208
+ function readLines(data) {
209
+ const page = (data && data.lines) || [];
210
+ const total = Number(data && data.total) || 0;
211
+ if (!page.length) return [total ? `${data.path} has ${total} lines; none from there.` : `${data.path} is empty.`];
212
+ const width = String(page[page.length - 1].n).length;
213
+ const lines = page.map((line) => `${String(line.n).padStart(Math.max(width, 3))} ${line.text}`);
214
+ if (data.more) {
215
+ const last = page[page.length - 1].n;
216
+ lines.push(`(lines ${page[0].n}-${last} of ${total}. Next: atris memory read ${data.path} --from ${last + 1})`);
217
+ }
218
+ return lines;
219
+ }
220
+
221
+ function lsLines(folder, data) {
222
+ const items = (data && data.items) || [];
223
+ if (!items.length) return [folder ? `Nothing in ${folder} yet.` : 'Your Atris memory folder is empty so far.'];
224
+ const width = Math.max(...items.map((item) => String(item.name).length)) + 2;
225
+ return items.map((item) => {
226
+ const name = String(item.name);
227
+ const note = name.endsWith('/') ? `${item.files} ${item.files === 1 ? 'file' : 'files'}` : size(item.size);
228
+ return `${name.padEnd(width)}${note}`;
229
+ });
230
+ }
231
+
232
+ // ---------------------------------------------------------------- rules block
233
+
234
+ const BEGIN = '<!-- atris-memory:begin -->';
235
+ const END = '<!-- atris-memory:end -->';
236
+
237
+ // What a person pastes into AGENTS.md, CLAUDE.md, .cursorrules or Codex
238
+ // instructions. Plain words, under 15 lines.
239
+ function rulesBlock(agent) {
240
+ const as = agent || '<your-name>';
241
+ return [
242
+ BEGIN,
243
+ '## Atris memory',
244
+ '',
245
+ 'This person keeps one memory in Atris that every app and agent shares. Use it.',
246
+ '- 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"`.',
248
+ '- Open what you find with `atris memory read <path>`. `atris memory ls` shows the folder.',
249
+ '- When they say remember, note, save or don\'t forget, save one short line: `atris memory remember "..."`.',
250
+ '- Save only lasting facts about them, never logs or your own work. Never rewrite their about.md.',
251
+ '- Put what you learn on a topic in a page: `atris memory write wiki/<topic>.md --file <file>`.',
252
+ `- Keep your notebook. After a task: \`atris memory log "what you did" --as ${as}\`.`,
253
+ `- A lesson that raised a score: \`atris memory lesson "..." --before 3 --after 5 --as ${as}\`.`,
254
+ '- Every command takes --json. If one says not logged in, ask them to run `atris login`.',
255
+ END,
256
+ ].join('\n');
257
+ }
258
+
259
+ // The text with exactly one rules block: replaced where it was, else added at
260
+ // the end. Nothing outside the markers changes. Line endings follow the file.
261
+ function upsertBlock(existing, block) {
262
+ const text = String(existing || '');
263
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
264
+ const body = block.replace(/\r?\n/g, eol);
265
+ const spans = [];
266
+ for (let from = 0; ;) {
267
+ const start = text.indexOf(BEGIN, from);
268
+ if (start === -1) break;
269
+ const end = text.indexOf(END, start + BEGIN.length);
270
+ if (end === -1) {
271
+ throw new Error(`found ${BEGIN} with no ${END} after it. Fix that by hand, then run this again.`);
272
+ }
273
+ spans.push([start, end + END.length]);
274
+ from = end + END.length;
275
+ }
276
+ if (!spans.length) {
277
+ if (!text) return { text: `${body}${eol}`, action: 'added' };
278
+ const gap = text.endsWith(eol) ? eol : `${eol}${eol}`;
279
+ return { text: `${text}${gap}${body}${eol}`, action: 'added' };
280
+ }
281
+ let next = text.slice(0, spans[0][0]) + body;
282
+ for (let i = 0; i < spans.length; i += 1) {
283
+ const until = i + 1 < spans.length ? spans[i + 1][0] : text.length;
284
+ let between = text.slice(spans[i][1], until);
285
+ if (i + 1 < spans.length && between.trim() === '') between = ''; // a pasted-twice copy goes, with its gap
286
+ next += between;
287
+ }
288
+ return { text: next, action: next === text ? 'unchanged' : 'replaced' };
289
+ }
290
+
291
+ function writeRulesFile(file, block, cwd = process.cwd()) {
292
+ const abs = path.resolve(cwd, file);
293
+ let existing = '';
294
+ try {
295
+ if (fs.statSync(abs).isDirectory()) throw new Error(`${file} is a folder. Name a rules file, like AGENTS.md.`);
296
+ existing = fs.readFileSync(abs, 'utf8');
297
+ } catch (error) {
298
+ if (error.code !== 'ENOENT') throw error;
299
+ }
300
+ const { text, action } = upsertBlock(existing, block);
301
+ if (action !== 'unchanged') {
302
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
303
+ fs.writeFileSync(abs, text); // in place, so a linked AGENTS.md stays a link
304
+ }
305
+ return { path: abs, action };
306
+ }
307
+
308
+ // Install lines for apps that speak MCP. The address is the same server the
309
+ // CLI talks to, at /mcp (backend docs/MCP.md has the same wording).
310
+ function mcpSetup(apiBase) {
311
+ let origin;
312
+ try { origin = new URL(apiBase).origin; } catch { return null; }
313
+ const url = `${origin}/mcp`;
314
+ const cursor = JSON.stringify({ mcpServers: { atris: { type: 'http', url, headers: { Authorization: 'Bearer atris_YOUR_KEY' } } } });
315
+ return {
316
+ url,
317
+ key: 'atris api-key create memory',
318
+ claude_code: `claude mcp add --transport http atris ${url} --header "Authorization: Bearer atris_YOUR_KEY"`,
319
+ codex: `codex mcp add atris --url ${url} --bearer-token-env-var ATRIS_API_KEY`,
320
+ cursor,
321
+ };
322
+ }
323
+
324
+ function mcpLines(setup) {
325
+ if (!setup) return [];
326
+ return [
327
+ 'Apps that speak MCP get the same memory through the Atris connection.',
328
+ `Make a key first: ${setup.key}, then put it where atris_YOUR_KEY is.`,
329
+ ` Claude Code: ${setup.claude_code}`,
330
+ ` Codex: ${setup.codex}`,
331
+ ' (with ATRIS_API_KEY=atris_YOUR_KEY set where Codex runs)',
332
+ ` Cursor: in ~/.cursor/mcp.json: ${setup.cursor}`,
333
+ ];
334
+ }
335
+
336
+ module.exports = {
337
+ BEGIN,
338
+ END,
339
+ LIVE_HELP,
340
+ USAGE_LINES,
341
+ isLiveCommand,
342
+ lsLines,
343
+ mcpLines,
344
+ mcpSetup,
345
+ noteBody,
346
+ parseLiveArgs,
347
+ plainNumber,
348
+ positiveInt,
349
+ readLines,
350
+ rulesBlock,
351
+ searchLines,
352
+ showText,
353
+ size,
354
+ upsertBlock,
355
+ writeRulesFile,
356
+ };
@@ -80,6 +80,7 @@ function assertSafeWorkspaceRoot(dir, { slug, op } = {}) {
80
80
  }
81
81
 
82
82
  module.exports = {
83
+ dangerousWorkspaceReason,
83
84
  resolveSafeOutputDir,
84
85
  assertSafeWorkspaceRoot,
85
86
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.82.11",
3
+ "version": "3.82.12",
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",