atris 3.2.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 (49) hide show
  1. package/GETTING_STARTED.md +65 -131
  2. package/README.md +18 -2
  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/research-search/SKILL.md +167 -0
  11. package/atris/skills/research-search/arxiv_search.py +157 -0
  12. package/atris/skills/research-search/program.md +48 -0
  13. package/atris/skills/research-search/results.tsv +6 -0
  14. package/atris/skills/research-search/scholar_search.py +154 -0
  15. package/atris/skills/tidy/SKILL.md +36 -21
  16. package/atris/team/_template/MEMBER.md +2 -0
  17. package/atris/team/validator/MEMBER.md +35 -1
  18. package/atris.md +118 -178
  19. package/bin/atris.js +30 -5
  20. package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
  21. package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
  22. package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
  23. package/cli/atris_code.py +889 -0
  24. package/cli/runtime_guard.py +693 -0
  25. package/commands/align.js +15 -0
  26. package/commands/app.js +316 -0
  27. package/commands/autopilot.js +390 -7
  28. package/commands/business.js +677 -2
  29. package/commands/computer.js +1979 -43
  30. package/commands/context-sync.js +5 -0
  31. package/commands/lifecycle.js +12 -0
  32. package/commands/plugin.js +24 -0
  33. package/commands/pull.js +40 -1
  34. package/commands/push.js +44 -0
  35. package/commands/serve.js +1 -0
  36. package/commands/sync.js +272 -76
  37. package/commands/verify.js +50 -1
  38. package/commands/wiki.js +27 -2
  39. package/lib/file-ops.js +13 -1
  40. package/lib/journal.js +23 -0
  41. package/lib/scorecard.js +42 -4
  42. package/lib/sync-telemetry.js +59 -0
  43. package/lib/todo.js +6 -0
  44. package/lib/wiki.js +150 -6
  45. package/package.json +2 -1
  46. package/utils/api.js +19 -0
  47. package/utils/auth.js +25 -1
  48. package/utils/config.js +24 -0
  49. package/utils/update-check.js +16 -0
@@ -0,0 +1,59 @@
1
+ // Atris sync telemetry — emit one event per `atris push` / `atris pull`.
2
+ //
3
+ // Why awaited (not fire-and-forget): push.js / pull.js call process.exit(1)
4
+ // on most failure paths. A fire-and-forget POST gets killed mid-flight and
5
+ // the RL loop loses signals on the exact failures it most needs to learn.
6
+ //
7
+ // Best-effort: any error swallowed silently — telemetry never blocks UX.
8
+ // Hard 2s timeout so a flaky control plane can't slow real operations.
9
+
10
+ const { apiRequestJson } = require('../utils/api');
11
+
12
+ let _cachedVersion = null;
13
+ function cliVersion() {
14
+ if (_cachedVersion) return _cachedVersion;
15
+ try {
16
+ _cachedVersion = require('../package.json').version || 'unknown';
17
+ } catch {
18
+ _cachedVersion = 'unknown';
19
+ }
20
+ return _cachedVersion;
21
+ }
22
+
23
+ async function emitSyncEvent(token, businessId, workspaceId, op, outcome, latencyMs, extras = {}) {
24
+ if (!token || !businessId || !workspaceId || !op || !outcome) return false;
25
+ const event = {
26
+ business_id: businessId,
27
+ workspace_id: workspaceId,
28
+ op,
29
+ outcome,
30
+ latency_ms: Math.max(0, Math.round(latencyMs || 0)),
31
+ bytes_transferred: extras.bytes_transferred || 0,
32
+ bytes_changed: extras.bytes_changed || 0,
33
+ files_pushed: extras.files_pushed || 0,
34
+ files_deleted: extras.files_deleted || 0,
35
+ files_unchanged: extras.files_unchanged || 0,
36
+ cli_version: cliVersion(),
37
+ };
38
+ if (extras.error_detail) event.error_detail = String(extras.error_detail).slice(0, 500);
39
+
40
+ try {
41
+ await apiRequestJson('/atris-sync/telemetry', {
42
+ method: 'POST',
43
+ token,
44
+ body: event,
45
+ timeoutMs: 2000,
46
+ retries: 0,
47
+ });
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function startTimer() {
55
+ const t0 = Date.now();
56
+ return () => Date.now() - t0;
57
+ }
58
+
59
+ module.exports = { emitSyncEvent, startTimer };
package/lib/todo.js CHANGED
@@ -17,6 +17,12 @@ function parseTodo(todoPath) {
17
17
  };
18
18
  }
19
19
 
20
+ /**
21
+ * Parse a specific section from TODO.md content into task objects.
22
+ * @param {string} content - Full TODO.md content
23
+ * @param {string} sectionName - Section name to extract (e.g., 'Backlog')
24
+ * @returns {Array} Array of parsed task objects
25
+ */
20
26
  function parseSection(content, sectionName) {
21
27
  const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
28
  const match = content.match(new RegExp(`##\\s+${escaped}\\n([\\s\\S]*?)(?=\\n##|$)`, 'i'));
package/lib/wiki.js CHANGED
@@ -2,7 +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';
5
6
  const PRIVATE_WIKI_ROOT = '.atris/presidio';
7
+ const PRIVATE_CONTEXT_ROOT = `${PRIVATE_WIKI_ROOT}/context`;
6
8
  const LEGACY_WIKI_ROOT = 'wiki';
7
9
  const WIKI_BRIEFS_SUBDIR = 'briefs';
8
10
  const LEGACY_WIKI_BRIEFS_SUBDIR = 'syntheses';
@@ -14,6 +16,10 @@ function getWikiRoot(mode = 'public') {
14
16
  return mode === 'private' ? PRIVATE_WIKI_ROOT : WIKI_ROOT;
15
17
  }
16
18
 
19
+ function getContextRoot(mode = 'public') {
20
+ return mode === 'private' ? PRIVATE_CONTEXT_ROOT : CONTEXT_ROOT;
21
+ }
22
+
17
23
  function getWikiLinkRoot(mode = 'public') {
18
24
  return mode === 'private' ? PRIVATE_WIKI_ROOT : 'atris/wiki';
19
25
  }
@@ -89,6 +95,23 @@ function statusMarkdown() {
89
95
  `;
90
96
  }
91
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
+
92
115
  function ensureFile(filePath, content) {
93
116
  if (!fs.existsSync(filePath)) {
94
117
  fs.writeFileSync(filePath, content, 'utf8');
@@ -185,6 +208,122 @@ function ensureWikiScaffold(projectRoot = process.cwd(), mode = 'public') {
185
208
  return wikiDir;
186
209
  }
187
210
 
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
+
188
327
  function findLocalWikiDir(projectRoot = process.cwd(), slug = null, mode = 'public') {
189
328
  if (mode === 'private') {
190
329
  const privateDir = path.join(projectRoot, PRIVATE_WIKI_ROOT);
@@ -444,7 +583,7 @@ function parseStatusBullets(content) {
444
583
  return bullets;
445
584
  }
446
585
 
447
- function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public') {
586
+ function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public', overrides = {}) {
448
587
  const wikiDir = ensureWikiScaffold(projectRoot, mode);
449
588
  const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
450
589
  const existing = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : '';
@@ -453,9 +592,9 @@ function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public') {
453
592
  const lines = [
454
593
  '# Atris Wiki Status',
455
594
  '',
456
- `- Last ingest: ${bullets.get('Last ingest') || 'never'}`,
457
- `- Last lint: ${bullets.get('Last lint') || 'never'}`,
458
- `- 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()}`}`,
459
598
  `- Health: ${report.health}`,
460
599
  `- Next move: ${report.nextMove}`,
461
600
  '',
@@ -465,7 +604,7 @@ function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public') {
465
604
  return statusPath;
466
605
  }
467
606
 
468
- function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode = 'public') {
607
+ function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode = 'public', kind = 'LOOP') {
469
608
  const wikiDir = ensureWikiScaffold(projectRoot, mode);
470
609
  const logPath = path.join(wikiDir, 'log.md');
471
610
  let content = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '# Atris Wiki Log\n';
@@ -476,7 +615,7 @@ function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode
476
615
  }
477
616
 
478
617
  if (!content.endsWith('\n')) content += '\n';
479
- content += `- ${nowTime()} LOOP ${summary}\n`;
618
+ content += `- ${nowTime()} ${kind} ${summary}\n`;
480
619
  for (const detail of details) {
481
620
  content += ` - ${detail}\n`;
482
621
  }
@@ -586,14 +725,18 @@ Output:
586
725
 
587
726
  module.exports = {
588
727
  WIKI_ROOT,
728
+ CONTEXT_ROOT,
589
729
  PRIVATE_WIKI_ROOT,
730
+ PRIVATE_CONTEXT_ROOT,
590
731
  LEGACY_WIKI_ROOT,
591
732
  WIKI_SUBDIRS,
592
733
  WIKI_CONTENT_SUBDIRS,
593
734
  WIKI_SCHEMA: buildWikiSchema(),
594
735
  WIKI_STATUS_FILE,
595
736
  getWikiRoot,
737
+ getContextRoot,
596
738
  ensureWikiScaffold,
739
+ ensureContextScaffold,
597
740
  findLocalWikiDir,
598
741
  normalizeWikiOnlyPrefix,
599
742
  readWikiStatus,
@@ -601,6 +744,7 @@ module.exports = {
601
744
  findStaleWikiPages,
602
745
  findWikiOrphans,
603
746
  findSuggestedSources,
747
+ stageWikiIngest,
604
748
  writeWikiStatus,
605
749
  appendWikiLog,
606
750
  buildIngestPrompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.2.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/",
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));
@@ -8,6 +8,10 @@ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
8
8
  const ATRIS_DIR = path.join(os.homedir(), '.atris');
9
9
  const CACHE_FILE = path.join(ATRIS_DIR, '.update-check');
10
10
 
11
+ /**
12
+ * Get the currently installed CLI version from package.json.
13
+ * @returns {string|null} Installed version string, or null on error
14
+ */
11
15
  function getInstalledVersion() {
12
16
  try {
13
17
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
@@ -18,6 +22,10 @@ function getInstalledVersion() {
18
22
  }
19
23
  }
20
24
 
25
+ /**
26
+ * Load cached update check data from ~/.atris/.update-check.
27
+ * @returns {{lastCheck: Date|null, latestVersion: string|null}} Cache data
28
+ */
21
29
  function getCacheData() {
22
30
  try {
23
31
  if (fs.existsSync(CACHE_FILE)) {
@@ -33,6 +41,10 @@ function getCacheData() {
33
41
  return { lastCheck: null, latestVersion: null };
34
42
  }
35
43
 
44
+ /**
45
+ * Save update check result to ~/.atris/.update-check cache file.
46
+ * @param {string} latestVersion - The latest version from npm
47
+ */
36
48
  function saveCacheData(latestVersion) {
37
49
  try {
38
50
  // Ensure ~/.atris/ exists
@@ -49,6 +61,10 @@ function saveCacheData(latestVersion) {
49
61
  }
50
62
  }
51
63
 
64
+ /**
65
+ * Fetch the latest version of atris from npm registry.
66
+ * @returns {Promise<string>} Latest version string
67
+ */
52
68
  function checkNpmVersion() {
53
69
  return new Promise((resolve, reject) => {
54
70
  const options = {