atris 3.1.0 → 3.5.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.
Files changed (54) hide show
  1. package/GETTING_STARTED.md +65 -131
  2. package/README.md +29 -4
  3. package/atris/GETTING_STARTED.md +65 -131
  4. package/atris/PERSONA.md +5 -1
  5. package/atris/atris.md +122 -153
  6. package/atris/skills/aeo/SKILL.md +117 -0
  7. package/atris/skills/atris/SKILL.md +49 -25
  8. package/atris/skills/create-member/SKILL.md +29 -9
  9. package/atris/skills/endgame/SKILL.md +9 -0
  10. package/atris/skills/improve/SKILL.md +2 -2
  11. package/atris/skills/research-search/SKILL.md +167 -0
  12. package/atris/skills/research-search/arxiv_search.py +157 -0
  13. package/atris/skills/research-search/program.md +48 -0
  14. package/atris/skills/research-search/results.tsv +6 -0
  15. package/atris/skills/research-search/scholar_search.py +154 -0
  16. package/atris/skills/tidy/SKILL.md +36 -21
  17. package/atris/team/_template/MEMBER.md +2 -0
  18. package/atris/team/validator/MEMBER.md +35 -1
  19. package/atris.md +118 -178
  20. package/bin/atris.js +37 -6
  21. package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
  22. package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
  23. package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
  24. package/cli/atris_code.py +889 -0
  25. package/cli/runtime_guard.py +693 -0
  26. package/commands/align.js +15 -0
  27. package/commands/app.js +316 -0
  28. package/commands/autopilot.js +948 -42
  29. package/commands/business.js +691 -11
  30. package/commands/computer.js +1979 -43
  31. package/commands/context-sync.js +5 -0
  32. package/commands/experiments.js +1 -1
  33. package/commands/lifecycle.js +12 -0
  34. package/commands/plugin.js +24 -0
  35. package/commands/pull.js +40 -1
  36. package/commands/push.js +44 -0
  37. package/commands/release.js +183 -0
  38. package/commands/research.js +52 -0
  39. package/commands/serve.js +1 -0
  40. package/commands/sync.js +372 -87
  41. package/commands/verify.js +53 -4
  42. package/commands/wiki.js +71 -26
  43. package/lib/file-ops.js +13 -1
  44. package/lib/journal.js +23 -0
  45. package/lib/reward-config.js +24 -0
  46. package/lib/scorecard.js +58 -6
  47. package/lib/sync-telemetry.js +59 -0
  48. package/lib/todo.js +6 -0
  49. package/lib/wiki.js +235 -60
  50. package/package.json +4 -2
  51. package/utils/api.js +19 -0
  52. package/utils/auth.js +25 -1
  53. package/utils/config.js +24 -0
  54. package/utils/update-check.js +16 -0
package/lib/wiki.js CHANGED
@@ -2,6 +2,9 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
4
  const WIKI_ROOT = 'atris/wiki';
5
+ const CONTEXT_ROOT = 'atris/context';
6
+ const PRIVATE_WIKI_ROOT = '.atris/presidio';
7
+ const PRIVATE_CONTEXT_ROOT = `${PRIVATE_WIKI_ROOT}/context`;
5
8
  const LEGACY_WIKI_ROOT = 'wiki';
6
9
  const WIKI_BRIEFS_SUBDIR = 'briefs';
7
10
  const LEGACY_WIKI_BRIEFS_SUBDIR = 'syntheses';
@@ -9,6 +12,18 @@ const WIKI_SUBDIRS = ['people', 'systems', 'concepts', WIKI_BRIEFS_SUBDIR];
9
12
  const WIKI_STATUS_FILE = 'STATUS.md';
10
13
  const WIKI_CONTENT_SUBDIRS = WIKI_SUBDIRS.map((subdir) => path.join(WIKI_ROOT, subdir));
11
14
 
15
+ function getWikiRoot(mode = 'public') {
16
+ return mode === 'private' ? PRIVATE_WIKI_ROOT : WIKI_ROOT;
17
+ }
18
+
19
+ function getContextRoot(mode = 'public') {
20
+ return mode === 'private' ? PRIVATE_CONTEXT_ROOT : CONTEXT_ROOT;
21
+ }
22
+
23
+ function getWikiLinkRoot(mode = 'public') {
24
+ return mode === 'private' ? PRIVATE_WIKI_ROOT : 'atris/wiki';
25
+ }
26
+
12
27
  function today() {
13
28
  return new Date().toISOString().slice(0, 10);
14
29
  }
@@ -17,10 +32,12 @@ function nowTime() {
17
32
  return new Date().toTimeString().slice(0, 5);
18
33
  }
19
34
 
20
- function protocolMarkdown() {
35
+ function protocolMarkdown(mode = 'public') {
36
+ const wikiRoot = getWikiRoot(mode);
37
+ const wikiLinkRoot = getWikiLinkRoot(mode);
21
38
  return `# Atris Wiki Protocol
22
39
 
23
- This wiki lives in \`${WIKI_ROOT}/\`.
40
+ This wiki lives in \`${wikiRoot}/\`.
24
41
 
25
42
  ## Purpose
26
43
 
@@ -28,20 +45,20 @@ Turn raw project context into a living memory the next agent can pick up cold.
28
45
 
29
46
  ## Shape
30
47
 
31
- - \`${WIKI_ROOT}/wiki.md\` - this protocol
32
- - \`${WIKI_ROOT}/index.md\` - catalog grouped by page type
33
- - \`${WIKI_ROOT}/log.md\` - append-only ingest and lint history
34
- - \`${WIKI_ROOT}/STATUS.md\` - plain-English health summary
35
- - \`${WIKI_ROOT}/people/\` - humans (employees, contacts, stakeholders)
36
- - \`${WIKI_ROOT}/systems/\` - tools, tables, dashboards, services, products
37
- - \`${WIKI_ROOT}/concepts/\` - patterns, frameworks, recurring ideas
38
- - \`${WIKI_ROOT}/${WIKI_BRIEFS_SUBDIR}/\` - multi-page briefs and cross-cutting analysis
48
+ - \`${wikiRoot}/wiki.md\` - this protocol
49
+ - \`${wikiRoot}/index.md\` - catalog grouped by page type
50
+ - \`${wikiRoot}/log.md\` - append-only ingest and lint history
51
+ - \`${wikiRoot}/STATUS.md\` - plain-English health summary
52
+ - \`${wikiRoot}/people/\` - humans (employees, contacts, stakeholders)
53
+ - \`${wikiRoot}/systems/\` - tools, tables, dashboards, services, products
54
+ - \`${wikiRoot}/concepts/\` - patterns, frameworks, recurring ideas
55
+ - \`${wikiRoot}/${WIKI_BRIEFS_SUBDIR}/\` - multi-page briefs and cross-cutting analysis
39
56
 
40
57
  ## Rules
41
58
 
42
59
  - Read the full source before writing.
43
60
  - Merge new facts into existing pages. Do not overwrite history blindly.
44
- - Add cross-references with \`[[atris/wiki/...]]\` links.
61
+ - Add cross-references with \`[[${wikiLinkRoot}/...]]\` links.
45
62
  - Keep \`index.md\`, \`log.md\`, and \`STATUS.md\` in sync with page changes.
46
63
  - If something is unclear or contradictory, say so directly.
47
64
  `;
@@ -78,6 +95,23 @@ function statusMarkdown() {
78
95
  `;
79
96
  }
80
97
 
98
+ function contextMarkdown(mode = 'public') {
99
+ const contextRoot = getContextRoot(mode);
100
+ const wikiRoot = getWikiRoot(mode);
101
+ return `# Context
102
+
103
+ Raw source material lives in \`${contextRoot}/\`.
104
+ Compiled memory belongs in \`${wikiRoot}/\`.
105
+
106
+ ## Rules
107
+
108
+ - Drop source files or source packs here before compiling them into the wiki.
109
+ - Treat source files as immutable evidence. If the source changes, add a new dated copy.
110
+ - Keep compiled summaries, briefs, and durable facts in the wiki instead of this folder.
111
+ - Use \`${contextRoot}/_ingest/\` for staged ingest packs and receipts.
112
+ `;
113
+ }
114
+
81
115
  function ensureFile(filePath, content) {
82
116
  if (!fs.existsSync(filePath)) {
83
117
  fs.writeFileSync(filePath, content, 'utf8');
@@ -155,15 +189,18 @@ function migrateLegacyBriefsDir(wikiDir) {
155
189
  rewriteLegacyWikiReferences(wikiDir);
156
190
  }
157
191
 
158
- function ensureWikiScaffold(projectRoot = process.cwd()) {
159
- const wikiDir = path.join(projectRoot, WIKI_ROOT);
192
+ function ensureWikiScaffold(projectRoot = process.cwd(), mode = 'public') {
193
+ const wikiRoot = getWikiRoot(mode);
194
+ const wikiDir = path.join(projectRoot, wikiRoot);
160
195
  fs.mkdirSync(wikiDir, { recursive: true });
161
- migrateLegacyBriefsDir(wikiDir);
196
+ if (mode === 'public') {
197
+ migrateLegacyBriefsDir(wikiDir);
198
+ }
162
199
  for (const subdir of WIKI_SUBDIRS) {
163
200
  fs.mkdirSync(path.join(wikiDir, subdir), { recursive: true });
164
201
  }
165
202
 
166
- ensureFile(path.join(wikiDir, 'wiki.md'), protocolMarkdown());
203
+ ensureFile(path.join(wikiDir, 'wiki.md'), protocolMarkdown(mode));
167
204
  ensureFile(path.join(wikiDir, 'index.md'), indexMarkdown());
168
205
  ensureFile(path.join(wikiDir, 'log.md'), logMarkdown());
169
206
  ensureFile(path.join(wikiDir, WIKI_STATUS_FILE), statusMarkdown());
@@ -171,7 +208,128 @@ function ensureWikiScaffold(projectRoot = process.cwd()) {
171
208
  return wikiDir;
172
209
  }
173
210
 
174
- function findLocalWikiDir(projectRoot = process.cwd(), slug = null) {
211
+ function ensureContextScaffold(projectRoot = process.cwd(), mode = 'public') {
212
+ const contextRoot = getContextRoot(mode);
213
+ const contextDir = path.join(projectRoot, contextRoot);
214
+ fs.mkdirSync(path.join(contextDir, '_ingest'), { recursive: true });
215
+ ensureFile(path.join(contextDir, 'README.md'), contextMarkdown(mode));
216
+ return contextDir;
217
+ }
218
+
219
+ function slugifyLabel(value) {
220
+ return String(value || 'source')
221
+ .toLowerCase()
222
+ .replace(/[^a-z0-9]+/g, '-')
223
+ .replace(/^-+|-+$/g, '')
224
+ .slice(0, 40) || 'source';
225
+ }
226
+
227
+ function isInsideDirectory(candidate, rootDir) {
228
+ const relative = path.relative(rootDir, candidate);
229
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
230
+ }
231
+
232
+ function walkFiles(dir, output = []) {
233
+ if (!fs.existsSync(dir)) return output;
234
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
235
+ for (const entry of entries) {
236
+ const fullPath = path.join(dir, entry.name);
237
+ if (entry.isDirectory()) {
238
+ walkFiles(fullPath, output);
239
+ } else if (entry.isFile()) {
240
+ output.push(fullPath);
241
+ }
242
+ }
243
+ return output;
244
+ }
245
+
246
+ function stageWikiIngest(projectRoot = process.cwd(), sourceValue, mode = 'public') {
247
+ const contextDir = ensureContextScaffold(projectRoot, mode);
248
+ const ingestDir = path.join(contextDir, '_ingest');
249
+ const raw = String(sourceValue || '').trim();
250
+ const timestamp = `${today()}-${nowTime().replace(':', '')}`;
251
+ const labelSeed = raw ? path.basename(raw) : 'source-pack';
252
+ const packDir = path.join(ingestDir, `${timestamp}-${slugifyLabel(labelSeed)}`);
253
+ fs.mkdirSync(packDir, { recursive: true });
254
+
255
+ const manifest = {
256
+ ingested_at: `${today()} ${nowTime()}`,
257
+ mode,
258
+ source_input: raw,
259
+ pack_path: path.relative(projectRoot, packDir).replace(/\\/g, '/'),
260
+ entries: [],
261
+ };
262
+
263
+ const maybeUrl = /^https?:\/\//i.test(raw);
264
+ const sourcePath = raw ? path.resolve(projectRoot, raw) : null;
265
+
266
+ if (raw && sourcePath && fs.existsSync(sourcePath)) {
267
+ const stat = fs.statSync(sourcePath);
268
+ if (isInsideDirectory(sourcePath, contextDir)) {
269
+ const files = stat.isDirectory() ? walkFiles(sourcePath) : [sourcePath];
270
+ manifest.entries.push({
271
+ kind: stat.isDirectory() ? 'directory' : 'file',
272
+ original: path.relative(projectRoot, sourcePath).replace(/\\/g, '/'),
273
+ staged: path.relative(projectRoot, sourcePath).replace(/\\/g, '/'),
274
+ file_count: files.length,
275
+ files: files.map((filePath) => path.relative(projectRoot, filePath).replace(/\\/g, '/')),
276
+ });
277
+ } else {
278
+ const stagedPath = path.join(packDir, path.basename(sourcePath));
279
+ if (stat.isDirectory()) {
280
+ fs.cpSync(sourcePath, stagedPath, { recursive: true });
281
+ } else {
282
+ fs.copyFileSync(sourcePath, stagedPath);
283
+ }
284
+ const files = stat.isDirectory() ? walkFiles(stagedPath) : [stagedPath];
285
+ manifest.entries.push({
286
+ kind: stat.isDirectory() ? 'directory' : 'file',
287
+ original: path.relative(projectRoot, sourcePath).replace(/\\/g, '/'),
288
+ staged: path.relative(projectRoot, stagedPath).replace(/\\/g, '/'),
289
+ file_count: files.length,
290
+ files: files.map((filePath) => path.relative(projectRoot, filePath).replace(/\\/g, '/')),
291
+ });
292
+ }
293
+ } else if (maybeUrl) {
294
+ const linksPath = path.join(packDir, 'links.txt');
295
+ fs.writeFileSync(linksPath, `${raw}\n`, 'utf8');
296
+ manifest.entries.push({
297
+ kind: 'url',
298
+ original: raw,
299
+ staged: path.relative(projectRoot, linksPath).replace(/\\/g, '/'),
300
+ file_count: 1,
301
+ files: [path.relative(projectRoot, linksPath).replace(/\\/g, '/')],
302
+ });
303
+ } else {
304
+ const requestPath = path.join(packDir, 'request.txt');
305
+ fs.writeFileSync(requestPath, `${raw || '(empty)'}\n`, 'utf8');
306
+ manifest.entries.push({
307
+ kind: 'unresolved',
308
+ original: raw || '(empty)',
309
+ staged: path.relative(projectRoot, requestPath).replace(/\\/g, '/'),
310
+ file_count: 1,
311
+ files: [path.relative(projectRoot, requestPath).replace(/\\/g, '/')],
312
+ });
313
+ }
314
+
315
+ const manifestPath = path.join(packDir, 'manifest.json');
316
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
317
+
318
+ return {
319
+ contextDir: path.relative(projectRoot, contextDir).replace(/\\/g, '/'),
320
+ packPath: path.relative(projectRoot, packDir).replace(/\\/g, '/'),
321
+ manifestPath: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
322
+ promptSource: manifest.entries[0]?.staged || path.relative(projectRoot, packDir).replace(/\\/g, '/'),
323
+ manifest,
324
+ };
325
+ }
326
+
327
+ function findLocalWikiDir(projectRoot = process.cwd(), slug = null, mode = 'public') {
328
+ if (mode === 'private') {
329
+ const privateDir = path.join(projectRoot, PRIVATE_WIKI_ROOT);
330
+ return fs.existsSync(privateDir) ? privateDir : null;
331
+ }
332
+
175
333
  const tries = [
176
334
  path.join(projectRoot, WIKI_ROOT),
177
335
  path.join(projectRoot, LEGACY_WIKI_ROOT),
@@ -190,8 +348,8 @@ function normalizeWikiOnlyPrefix(prefix) {
190
348
  return null;
191
349
  }
192
350
 
193
- function readWikiStatus(projectRoot = process.cwd(), slug = null) {
194
- const wikiDir = findLocalWikiDir(projectRoot, slug);
351
+ function readWikiStatus(projectRoot = process.cwd(), slug = null, mode = 'public') {
352
+ const wikiDir = findLocalWikiDir(projectRoot, slug, mode);
195
353
  if (!wikiDir) return null;
196
354
 
197
355
  const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
@@ -275,8 +433,9 @@ function parseFrontmatter(content) {
275
433
  return frontmatter;
276
434
  }
277
435
 
278
- function readWikiPages(projectRoot = process.cwd()) {
279
- const wikiDir = path.join(projectRoot, WIKI_ROOT);
436
+ function readWikiPages(projectRoot = process.cwd(), mode = 'public') {
437
+ const wikiRoot = getWikiRoot(mode);
438
+ const wikiDir = path.join(projectRoot, wikiRoot);
280
439
  const pages = [];
281
440
 
282
441
  for (const subdir of WIKI_SUBDIRS) {
@@ -302,8 +461,8 @@ function normalizeSourcePath(projectRoot, source) {
302
461
  return path.normalize(path.join(projectRoot, source));
303
462
  }
304
463
 
305
- function findStaleWikiPages(projectRoot = process.cwd()) {
306
- return readWikiPages(projectRoot)
464
+ function findStaleWikiPages(projectRoot = process.cwd(), mode = 'public') {
465
+ return readWikiPages(projectRoot, mode)
307
466
  .map((page) => {
308
467
  const sources = Array.isArray(page.frontmatter.sources) ? page.frontmatter.sources : [];
309
468
  if (sources.length === 0) return null;
@@ -345,13 +504,14 @@ function findStaleWikiPages(projectRoot = process.cwd()) {
345
504
  }
346
505
 
347
506
  function extractWikiLinks(content) {
348
- const matches = content.match(/\[\[(atris\/wiki\/[^\]]+?)\]\]/g) || [];
507
+ const matches = content.match(/\[\[((?:atris\/wiki|\.atris\/presidio)\/[^\]]+?)\]\]/g) || [];
349
508
  return matches.map((match) => match.slice(2, -2));
350
509
  }
351
510
 
352
- function findWikiOrphans(projectRoot = process.cwd()) {
353
- const pages = readWikiPages(projectRoot);
354
- const indexPath = path.join(projectRoot, WIKI_ROOT, 'index.md');
511
+ function findWikiOrphans(projectRoot = process.cwd(), mode = 'public') {
512
+ const wikiRoot = getWikiRoot(mode);
513
+ const pages = readWikiPages(projectRoot, mode);
514
+ const indexPath = path.join(projectRoot, wikiRoot, 'index.md');
355
515
  const indexContent = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '';
356
516
 
357
517
  const inboundLinks = new Map();
@@ -423,8 +583,8 @@ function parseStatusBullets(content) {
423
583
  return bullets;
424
584
  }
425
585
 
426
- function writeWikiStatus(projectRoot = process.cwd(), report) {
427
- const wikiDir = ensureWikiScaffold(projectRoot);
586
+ function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public', overrides = {}) {
587
+ const wikiDir = ensureWikiScaffold(projectRoot, mode);
428
588
  const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
429
589
  const existing = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : '';
430
590
  const bullets = parseStatusBullets(existing);
@@ -432,9 +592,9 @@ function writeWikiStatus(projectRoot = process.cwd(), report) {
432
592
  const lines = [
433
593
  '# Atris Wiki Status',
434
594
  '',
435
- `- Last ingest: ${bullets.get('Last ingest') || 'never'}`,
436
- `- Last lint: ${bullets.get('Last lint') || 'never'}`,
437
- `- Last loop: ${today()} ${nowTime()}`,
595
+ `- Last ingest: ${overrides.lastIngest || bullets.get('Last ingest') || 'never'}`,
596
+ `- Last lint: ${overrides.lastLint || bullets.get('Last lint') || 'never'}`,
597
+ `- Last loop: ${overrides.lastLoop || `${today()} ${nowTime()}`}`,
438
598
  `- Health: ${report.health}`,
439
599
  `- Next move: ${report.nextMove}`,
440
600
  '',
@@ -444,8 +604,8 @@ function writeWikiStatus(projectRoot = process.cwd(), report) {
444
604
  return statusPath;
445
605
  }
446
606
 
447
- function appendWikiLog(projectRoot = process.cwd(), summary, details = []) {
448
- const wikiDir = ensureWikiScaffold(projectRoot);
607
+ function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode = 'public', kind = 'LOOP') {
608
+ const wikiDir = ensureWikiScaffold(projectRoot, mode);
449
609
  const logPath = path.join(wikiDir, 'log.md');
450
610
  let content = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '# Atris Wiki Log\n';
451
611
  const dateHeader = `## ${today()}`;
@@ -455,7 +615,7 @@ function appendWikiLog(projectRoot = process.cwd(), summary, details = []) {
455
615
  }
456
616
 
457
617
  if (!content.endsWith('\n')) content += '\n';
458
- content += `- ${nowTime()} LOOP ${summary}\n`;
618
+ content += `- ${nowTime()} ${kind} ${summary}\n`;
459
619
  for (const detail of details) {
460
620
  content += ` - ${detail}\n`;
461
621
  }
@@ -471,17 +631,20 @@ function formatSourceList(sourceValue) {
471
631
  .join(', ');
472
632
  }
473
633
 
474
- const WIKI_SCHEMA = `The wiki lives in ${WIKI_ROOT}/.
634
+ function buildWikiSchema(mode = 'public') {
635
+ const wikiRoot = getWikiRoot(mode);
636
+ const wikiLinkRoot = getWikiLinkRoot(mode);
637
+ return `The wiki lives in ${wikiRoot}/.
475
638
 
476
639
  Structure:
477
- - ${WIKI_ROOT}/wiki.md - protocol for future agents
478
- - ${WIKI_ROOT}/index.md - catalog grouped by type
479
- - ${WIKI_ROOT}/log.md - append-only activity log
480
- - ${WIKI_ROOT}/STATUS.md - plain-English health summary
481
- - ${WIKI_ROOT}/people/ - one page per human
482
- - ${WIKI_ROOT}/systems/ - one page per tool, table, dashboard, service, or product
483
- - ${WIKI_ROOT}/concepts/ - pattern and framework pages
484
- - ${WIKI_ROOT}/${WIKI_BRIEFS_SUBDIR}/ - cross-cutting briefs referencing 3+ pages
640
+ - ${wikiRoot}/wiki.md - protocol for future agents
641
+ - ${wikiRoot}/index.md - catalog grouped by type
642
+ - ${wikiRoot}/log.md - append-only activity log
643
+ - ${wikiRoot}/STATUS.md - plain-English health summary
644
+ - ${wikiRoot}/people/ - one page per human
645
+ - ${wikiRoot}/systems/ - one page per tool, table, dashboard, service, or product
646
+ - ${wikiRoot}/concepts/ - pattern and framework pages
647
+ - ${wikiRoot}/${WIKI_BRIEFS_SUBDIR}/ - cross-cutting briefs referencing 3+ pages
485
648
 
486
649
  Page format:
487
650
  ---
@@ -497,7 +660,7 @@ tags: [tag1, tag2]
497
660
  # Title
498
661
  Body in markdown.
499
662
  ## Cross-References
500
- - [[atris/wiki/people/related.md]] - why related
663
+ - [[${wikiLinkRoot}/people/related.md]] - why related
501
664
 
502
665
  Rules:
503
666
  - Read every listed source fully before writing
@@ -505,20 +668,23 @@ Rules:
505
668
  - Keep index.md, log.md, and STATUS.md current
506
669
  - Flag contradictions directly instead of smoothing them over
507
670
  - Never modify the raw source documents you ingested`;
671
+ }
508
672
 
509
- function buildIngestPrompt(sourceValue) {
673
+ function buildIngestPrompt(sourceValue, mode = 'public') {
674
+ const wikiRoot = getWikiRoot(mode);
675
+ const wikiLinkRoot = getWikiLinkRoot(mode);
510
676
  return `Atris wiki ingest: ${formatSourceList(sourceValue)}
511
- ${WIKI_SCHEMA}
677
+ ${buildWikiSchema(mode)}
512
678
 
513
679
  Workflow:
514
680
  1. Read every source in: ${sourceValue}
515
- 2. Ensure ${WIKI_ROOT}/ exists with wiki.md, index.md, log.md, STATUS.md, and the 3 page subfolders
681
+ 2. Ensure ${wikiRoot}/ exists with wiki.md, index.md, log.md, STATUS.md, and the 3 page subfolders
516
682
  3. Extract people, systems, and concepts worth preserving
517
- 4. Create or update pages under ${WIKI_ROOT}/, merging with existing facts instead of replacing them
518
- 5. Add cross-references using [[atris/wiki/...]] links
519
- 6. Update ${WIKI_ROOT}/index.md with one-line descriptions of touched pages
520
- 7. Append an INGEST entry to ${WIKI_ROOT}/log.md under today's date
521
- 8. Refresh ${WIKI_ROOT}/STATUS.md in plain English for a non-technical reader
683
+ 4. Create or update pages under ${wikiRoot}/, merging with existing facts instead of replacing them
684
+ 5. Add cross-references using [[${wikiLinkRoot}/...]] links
685
+ 6. Update ${wikiRoot}/index.md with one-line descriptions of touched pages
686
+ 7. Append an INGEST entry to ${wikiRoot}/log.md under today's date
687
+ 8. Refresh ${wikiRoot}/STATUS.md in plain English for a non-technical reader
522
688
 
523
689
  Quality bar:
524
690
  - Ask clarifying questions if the source is ambiguous
@@ -527,18 +693,20 @@ Quality bar:
527
693
  - Leave the wiki sharper than you found it`;
528
694
  }
529
695
 
530
- function buildQueryPrompt(question) {
696
+ function buildQueryPrompt(question, mode = 'public') {
697
+ const wikiRoot = getWikiRoot(mode);
531
698
  return `Atris wiki query: ${question}
532
699
 
533
- Read ${WIKI_ROOT}/index.md first, then the most relevant pages.
534
- Answer from the wiki with direct references to page paths under ${WIKI_ROOT}/.
700
+ Read ${wikiRoot}/index.md first, then the most relevant pages.
701
+ Answer from the wiki with direct references to page paths under ${wikiRoot}/.
535
702
  If the answer reveals a reusable insight, offer to save it as a brief page.`;
536
703
  }
537
704
 
538
- function buildLintPrompt() {
705
+ function buildLintPrompt(mode = 'public') {
706
+ const wikiRoot = getWikiRoot(mode);
539
707
  return `Atris wiki lint pass
540
708
 
541
- Read ${WIKI_ROOT}/index.md, crawl the referenced pages, and inspect the local wiki.
709
+ Read ${wikiRoot}/index.md, crawl the referenced pages, and inspect the local wiki.
542
710
 
543
711
  Checks:
544
712
  1. Every page referenced by index.md exists
@@ -546,8 +714,8 @@ Checks:
546
714
  3. Orphan pages are listed
547
715
  4. Contradictions are called out plainly
548
716
  5. Gaps worth ingesting next are listed concretely
549
- 6. ${WIKI_ROOT}/STATUS.md is rewritten in plain English
550
- 7. ${WIKI_ROOT}/log.md gets a LINT entry under today's date
717
+ 6. ${wikiRoot}/STATUS.md is rewritten in plain English
718
+ 7. ${wikiRoot}/log.md gets a LINT entry under today's date
551
719
 
552
720
  Output:
553
721
  - Clear summary for a non-technical reader
@@ -557,12 +725,18 @@ Output:
557
725
 
558
726
  module.exports = {
559
727
  WIKI_ROOT,
728
+ CONTEXT_ROOT,
729
+ PRIVATE_WIKI_ROOT,
730
+ PRIVATE_CONTEXT_ROOT,
560
731
  LEGACY_WIKI_ROOT,
561
732
  WIKI_SUBDIRS,
562
733
  WIKI_CONTENT_SUBDIRS,
563
- WIKI_SCHEMA,
734
+ WIKI_SCHEMA: buildWikiSchema(),
564
735
  WIKI_STATUS_FILE,
736
+ getWikiRoot,
737
+ getContextRoot,
565
738
  ensureWikiScaffold,
739
+ ensureContextScaffold,
566
740
  findLocalWikiDir,
567
741
  normalizeWikiOnlyPrefix,
568
742
  readWikiStatus,
@@ -570,6 +744,7 @@ module.exports = {
570
744
  findStaleWikiPages,
571
745
  findWikiOrphans,
572
746
  findSuggestedSources,
747
+ stageWikiIngest,
573
748
  writeWikiStatus,
574
749
  appendWikiLog,
575
750
  buildIngestPrompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.1.0",
3
+ "version": "3.5.0",
4
4
  "description": "Atris — an operating system for intelligence. Integrates with any agent.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
+ "cli/",
11
12
  "commands/",
12
13
  "utils/",
13
14
  "lib/",
@@ -32,7 +33,8 @@
32
33
  "atris/skills/"
33
34
  ],
34
35
  "scripts": {
35
- "test": "node --test"
36
+ "test": "node --test",
37
+ "prepare": "cp scripts/pre-commit .git/hooks/pre-commit 2>/dev/null && chmod +x .git/hooks/pre-commit || true"
36
38
  },
37
39
  "keywords": [
38
40
  "atrisdev",
package/utils/api.js CHANGED
@@ -20,22 +20,41 @@ try {
20
20
  const DEFAULT_CLIENT_ID = `AtrisCLI/${CLI_VERSION}`;
21
21
  const DEFAULT_USER_AGENT = `${DEFAULT_CLIENT_ID} (node ${process.version}; ${os.platform()} ${os.release()} ${os.arch()})`;
22
22
 
23
+ /**
24
+ * Get the base URL for the Atris API.
25
+ * @returns {string} API base URL
26
+ */
23
27
  function getApiBaseUrl() {
24
28
  const raw = process.env.ATRIS_API_URL || 'https://api.atris.ai/api';
25
29
  return raw.replace(/\/$/, '');
26
30
  }
27
31
 
32
+ /**
33
+ * Get the base URL for the Atris web app.
34
+ * @returns {string} App base URL
35
+ */
28
36
  function getAppBaseUrl() {
29
37
  const raw = process.env.ATRIS_APP_URL || 'https://atris.ai';
30
38
  return raw.replace(/\/$/, '');
31
39
  }
32
40
 
41
+ /**
42
+ * Build a full API URL from a path.
43
+ * @param {string} pathname - API endpoint path
44
+ * @returns {string} Full API URL
45
+ */
33
46
  function buildApiUrl(pathname) {
34
47
  const base = getApiBaseUrl();
35
48
  const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`;
36
49
  return `${base}${normalizedPath}`;
37
50
  }
38
51
 
52
+ /**
53
+ * Make an HTTP/HTTPS request.
54
+ * @param {string} urlString - Full URL to request
55
+ * @param {Object} options - Request options (method, headers, body, timeoutMs)
56
+ * @returns {Promise<Object>} Response with statusCode, headers, and data
57
+ */
39
58
  function httpRequest(urlString, options) {
40
59
  return new Promise((resolve, reject) => {
41
60
  const parsed = new URL(urlString);
package/utils/auth.js CHANGED
@@ -4,6 +4,10 @@ const fs = require('fs');
4
4
  const { exec } = require('child_process');
5
5
  const readline = require('readline');
6
6
 
7
+ /**
8
+ * Open a URL in the system's default browser.
9
+ * @param {string} url - URL to open
10
+ */
7
11
  function openBrowser(url) {
8
12
  const platform = os.platform();
9
13
  // Sanitize URL to prevent shell injection — only allow valid URL characters
@@ -30,6 +34,11 @@ let sharedRl = null;
30
34
  let inputLines = [];
31
35
  let inputIndex = 0;
32
36
 
37
+ /**
38
+ * Prompt the user for input, handling both TTY and piped input.
39
+ * @param {string} question - Prompt text to display
40
+ * @returns {Promise<string>} User's input
41
+ */
33
42
  function promptUser(question) {
34
43
  // If stdin is not a TTY (piped input), read all lines upfront
35
44
  if (!process.stdin.isTTY && inputLines.length === 0 && !sharedRl) {
@@ -74,7 +83,11 @@ function promptUser(question) {
74
83
 
75
84
  const TOKEN_REFRESH_BUFFER_SECONDS = 300;
76
85
 
77
- // JWT helpers
86
+ /**
87
+ * Decode and parse the claims from a JWT token.
88
+ * @param {string} token - JWT token string
89
+ * @returns {Object|null} Decoded claims or null if invalid
90
+ */
78
91
  function decodeJwtClaims(token) {
79
92
  if (!token || typeof token !== 'string') {
80
93
  return null;
@@ -93,6 +106,11 @@ function decodeJwtClaims(token) {
93
106
  }
94
107
  }
95
108
 
109
+ /**
110
+ * Get the expiration time of a JWT token in epoch seconds.
111
+ * @param {string} token - JWT token string
112
+ * @returns {number|null} Expiry epoch seconds or null if invalid
113
+ */
96
114
  function getTokenExpiryEpochSeconds(token) {
97
115
  const claims = decodeJwtClaims(token);
98
116
  if (!claims || typeof claims.exp !== 'number') {
@@ -101,6 +119,12 @@ function getTokenExpiryEpochSeconds(token) {
101
119
  return claims.exp;
102
120
  }
103
121
 
122
+ /**
123
+ * Check if a JWT token should be refreshed based on expiry.
124
+ * @param {string} token - JWT token string
125
+ * @param {number} [bufferSeconds=300] - Seconds before expiry to trigger refresh
126
+ * @returns {boolean} True if token needs refresh
127
+ */
104
128
  function shouldRefreshToken(token, bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS) {
105
129
  const exp = getTokenExpiryEpochSeconds(token);
106
130
  if (!exp) {
package/utils/config.js CHANGED
@@ -1,11 +1,19 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
+ /**
5
+ * Get the path to atris/.config in the current project.
6
+ * @returns {string} Config file path
7
+ */
4
8
  function getConfigPath() {
5
9
  const targetDir = path.join(process.cwd(), 'atris');
6
10
  return path.join(targetDir, '.config');
7
11
  }
8
12
 
13
+ /**
14
+ * Load config from atris/.config, returning empty object if missing/invalid.
15
+ * @returns {Object} Config object
16
+ */
9
17
  function loadConfig() {
10
18
  const configPath = getConfigPath();
11
19
 
@@ -21,6 +29,10 @@ function loadConfig() {
21
29
  }
22
30
  }
23
31
 
32
+ /**
33
+ * Save config to atris/.config. Exits if atris/ folder doesn't exist.
34
+ * @param {Object} config - Config object to save
35
+ */
24
36
  function saveConfig(config) {
25
37
  const configPath = getConfigPath();
26
38
  const targetDir = path.dirname(configPath);
@@ -33,11 +45,19 @@ function saveConfig(config) {
33
45
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
34
46
  }
35
47
 
48
+ /**
49
+ * Get the path to atris/.log_sync_state.json for tracking sync timestamps.
50
+ * @returns {string} Log sync state file path
51
+ */
36
52
  function getLogSyncStatePath() {
37
53
  const targetDir = path.join(process.cwd(), 'atris');
38
54
  return path.join(targetDir, '.log_sync_state.json');
39
55
  }
40
56
 
57
+ /**
58
+ * Load log sync state, returning empty object if missing/invalid.
59
+ * @returns {Object} Sync state object with last sync timestamps
60
+ */
41
61
  function loadLogSyncState() {
42
62
  const statePath = getLogSyncStatePath();
43
63
  if (!fs.existsSync(statePath)) {
@@ -51,6 +71,10 @@ function loadLogSyncState() {
51
71
  }
52
72
  }
53
73
 
74
+ /**
75
+ * Save log sync state to atris/.log_sync_state.json.
76
+ * @param {Object} state - Sync state object to save
77
+ */
54
78
  function saveLogSyncState(state) {
55
79
  const statePath = getLogSyncStatePath();
56
80
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2));