atris 2.4.0 → 2.5.1

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/bin/atris.js CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Catch-all for uncaught async errors — prevents silent crashes
4
+ process.on('unhandledRejection', (err) => {
5
+ console.error(`\n✗ Unexpected error: ${err?.message || err}`);
6
+ process.exit(1);
7
+ });
8
+
3
9
  const fs = require('fs');
4
10
  const path = require('path');
5
11
  const { exec, spawnSync } = require('child_process');
6
12
  const readline = require('readline');
7
13
  const os = require('os');
8
- const https = require('https');
9
- const http = require('http');
10
14
  const crypto = require('crypto');
11
15
  const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json');
12
16
 
@@ -21,15 +25,42 @@ try {
21
25
  // Ignore parse errors; fall back to unknown
22
26
  }
23
27
 
24
- const DEFAULT_CLIENT_ID = `AtrisCLI/${CLI_VERSION}`;
25
- const DEFAULT_USER_AGENT = `${DEFAULT_CLIENT_ID} (node ${process.version}; ${os.platform()} ${os.release()} ${os.arch()})`;
26
-
27
28
  // Update check utility
28
29
  const { checkForUpdates, showUpdateNotification, autoUpdate } = require('../utils/update-check');
29
30
 
30
31
  // State detection for smart default
31
32
  const { detectWorkspaceState, loadContext } = require('../lib/state-detection');
32
33
 
34
+ // Journal & config utilities (canonical modules)
35
+ const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/file-ops');
36
+ const { getConfigPath, loadConfig, saveConfig, loadLogSyncState, saveLogSyncState } = require('../utils/config');
37
+
38
+ // Auth & API (canonical modules — eliminates duplicate inline code)
39
+ const {
40
+ decodeJwtClaims, getTokenExpiryEpochSeconds, shouldRefreshToken,
41
+ getCredentialsPath, saveCredentials, loadCredentials, deleteCredentials,
42
+ validateAccessToken: _validateAccessToken,
43
+ refreshAccessToken: _refreshAccessToken,
44
+ performTokenRefresh: _performTokenRefresh,
45
+ ensureValidCredentials: _ensureValidCredentials,
46
+ fetchMyAgents: _fetchMyAgents,
47
+ displayAccountSummary: _displayAccountSummary,
48
+ openBrowser, promptUser,
49
+ } = require('../utils/auth');
50
+ const {
51
+ getApiBaseUrl, getAppBaseUrl, buildApiUrl, httpRequest,
52
+ apiRequestJson, streamProChat, spawnClaudeCodeSession,
53
+ DEFAULT_CLIENT_ID, DEFAULT_USER_AGENT,
54
+ } = require('../utils/api');
55
+
56
+ // Bind DI wrappers (utils/auth uses dependency injection for apiRequestJson)
57
+ const validateAccessToken = (token) => _validateAccessToken(token, apiRequestJson);
58
+ const refreshAccessToken = (rt, p) => _refreshAccessToken(rt, p, apiRequestJson);
59
+ const performTokenRefresh = (creds) => _performTokenRefresh(creds, apiRequestJson);
60
+ const ensureValidCredentials = (opts) => _ensureValidCredentials(apiRequestJson, opts);
61
+ const fetchMyAgents = (token) => _fetchMyAgents(token, apiRequestJson);
62
+ const displayAccountSummary = () => _displayAccountSummary(apiRequestJson);
63
+
33
64
  // Run update check in background (non-blocking)
34
65
  // Skip for 'version' and 'update' commands to avoid redundant messages
35
66
  let updateCheckPromise = null;
@@ -39,12 +70,16 @@ if (!skipUpdateCheck && (!process.argv[2] || (process.argv[2] && !['version', 'u
39
70
  .then((updateInfo) => {
40
71
  // Show notification if update available (after command completes)
41
72
  if (updateInfo) {
42
- // Auto-update in background, fall back to notification if it fails
43
- setTimeout(() => {
44
- if (!autoUpdate(updateInfo)) {
45
- showUpdateNotification(updateInfo);
46
- }
47
- }, 100);
73
+ // Notify only never auto-update mid-session (opt-in via ATRIS_AUTO_UPDATE=1)
74
+ if (process.env.ATRIS_AUTO_UPDATE === '1') {
75
+ setTimeout(() => {
76
+ if (!autoUpdate(updateInfo)) {
77
+ showUpdateNotification(updateInfo);
78
+ }
79
+ }, 100);
80
+ } else {
81
+ showUpdateNotification(updateInfo);
82
+ }
48
83
  }
49
84
  return updateInfo;
50
85
  })
@@ -56,52 +91,17 @@ if (!skipUpdateCheck && (!process.argv[2] || (process.argv[2] && !['version', 'u
56
91
 
57
92
  const command = process.argv[2];
58
93
 
59
- // Auto-sync skills on every command (fast just file diffs, no network)
60
- try {
61
- const { syncSkills } = require('../commands/sync');
62
- const skillsUpdated = syncSkills({ silent: true });
63
- if (skillsUpdated > 0) {
64
- console.log(`⬆️ ${skillsUpdated} skill${skillsUpdated > 1 ? 's' : ''} updated`);
65
- }
66
- } catch (e) {
67
- // Non-critical
68
- }
69
-
70
- const TOKEN_REFRESH_BUFFER_SECONDS = 300; // Refresh ~5 minutes before expiry
71
-
72
- function decodeJwtClaims(token) {
73
- if (!token || typeof token !== 'string') {
74
- return null;
75
- }
76
- const parts = token.split('.');
77
- if (parts.length < 2) {
78
- return null;
79
- }
94
+ // Auto-sync skills only for commands that modify workspace state
95
+ if (['init', 'update', 'sync', 'upgrade'].includes(command)) {
80
96
  try {
81
- const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');
82
- const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');
83
- const decoded = Buffer.from(padded, 'base64').toString('utf8');
84
- return JSON.parse(decoded);
85
- } catch {
86
- return null;
87
- }
88
- }
89
-
90
- function getTokenExpiryEpochSeconds(token) {
91
- const claims = decodeJwtClaims(token);
92
- if (!claims || typeof claims.exp !== 'number') {
93
- return null;
94
- }
95
- return claims.exp;
96
- }
97
-
98
- function shouldRefreshToken(token, bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS) {
99
- const exp = getTokenExpiryEpochSeconds(token);
100
- if (!exp) {
101
- return false;
97
+ const { syncSkills } = require('../commands/sync');
98
+ const skillsUpdated = syncSkills({ silent: true });
99
+ if (skillsUpdated > 0) {
100
+ console.log(`⬆️ ${skillsUpdated} skill${skillsUpdated > 1 ? 's' : ''} updated`);
101
+ }
102
+ } catch (e) {
103
+ // Non-critical
102
104
  }
103
- const nowSeconds = Math.floor(Date.now() / 1000);
104
- return exp <= nowSeconds + bufferSeconds;
105
105
  }
106
106
 
107
107
  function searchJournal(keyword) {
@@ -124,25 +124,28 @@ function searchJournal(keyword) {
124
124
 
125
125
  // Recursively find all .md files in logs directory
126
126
  function walkDir(dir) {
127
- const files = fs.readdirSync(dir);
127
+ let files;
128
+ try { files = fs.readdirSync(dir); } catch { return; }
128
129
  for (const file of files) {
129
- const filePath = path.join(dir, file);
130
- const stat = fs.statSync(filePath);
131
- if (stat.isDirectory()) {
132
- walkDir(filePath);
133
- } else if (file.endsWith('.md')) {
134
- const content = fs.readFileSync(filePath, 'utf8');
135
- const lines = content.split('\n');
136
- lines.forEach((line, idx) => {
137
- if (line.toLowerCase().includes(keywordLower)) {
138
- results.push({
139
- file: path.relative(process.cwd(), filePath),
140
- line: idx + 1,
141
- content: line.trim()
142
- });
143
- }
144
- });
145
- }
130
+ try {
131
+ const filePath = path.join(dir, file);
132
+ const stat = fs.statSync(filePath);
133
+ if (stat.isDirectory()) {
134
+ walkDir(filePath);
135
+ } else if (file.endsWith('.md')) {
136
+ const content = fs.readFileSync(filePath, 'utf8');
137
+ const lines = content.split('\n');
138
+ lines.forEach((line, idx) => {
139
+ if (line.toLowerCase().includes(keywordLower)) {
140
+ results.push({
141
+ file: path.relative(process.cwd(), filePath),
142
+ line: idx + 1,
143
+ content: line.trim()
144
+ });
145
+ }
146
+ });
147
+ }
148
+ } catch { /* skip unreadable files */ }
146
149
  }
147
150
  }
148
151
 
@@ -160,6 +163,31 @@ function searchJournal(keyword) {
160
163
  }
161
164
  }
162
165
 
166
+ function consoleCmd() {
167
+ const workspace = process.cwd();
168
+ const daemonScript = path.join(workspace, 'cli', 'atrisd.sh');
169
+
170
+ if (!fs.existsSync(daemonScript)) {
171
+ console.error('✗ Missing cli/atrisd.sh in this workspace.');
172
+ console.error(' Run this from your project root, or add cli/atrisd.sh first.');
173
+ process.exit(1);
174
+ }
175
+
176
+ const args = process.argv.slice(3);
177
+ const result = spawnSync('bash', [daemonScript, ...args], {
178
+ cwd: workspace,
179
+ stdio: 'inherit',
180
+ env: process.env,
181
+ });
182
+
183
+ if (result.error) {
184
+ console.error(`✗ Failed to start console: ${result.error.message}`);
185
+ process.exit(1);
186
+ }
187
+
188
+ process.exit(result.status ?? 0);
189
+ }
190
+
163
191
  function showHelp() {
164
192
  console.log('');
165
193
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -203,6 +231,7 @@ function showHelp() {
203
231
  console.log(' next - Auto-advance to next step');
204
232
  console.log('');
205
233
  console.log('Cloud & agents:');
234
+ console.log(' console - Start/attach always-on coding console (tmux daemon)');
206
235
  console.log(' agent - Select which Atris agent to use');
207
236
  console.log(' chat - Chat with the selected Atris agent');
208
237
  console.log(' login - Authenticate (use --token <t> for non-interactive)');
@@ -326,30 +355,20 @@ if (command === 'help' || command === '--help' || command === '-h') {
326
355
  process.exit(0);
327
356
  }
328
357
 
329
- // Command handlers - must load BEFORE interactiveEntry() is called (TDZ issue)
358
+ // Core command handlers loaded eagerly (used by interactiveEntry default path)
330
359
  const { initAtris: initCmd } = require('../commands/init');
331
360
  const { syncAtris: syncCmd } = require('../commands/sync');
332
361
  const { logAtris: logCmd } = require('../commands/log');
333
- const { logSyncAtris: logSyncCmd } = require('../commands/log-sync');
334
- const { loginAtris: loginCmd, logoutAtris: logoutCmd, whoamiAtris: whoamiCmd, switchAccount: switchCmd, listAccountsCmd: accountsCmd } = require('../commands/auth');
335
- const { showVersion: versionCmd } = require('../commands/version');
336
- const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('../commands/workflow');
337
- const { visualizeAtris: visualizeCmd } = require('../commands/visualize');
338
- const { brainstormAtris: brainstormCmd } = require('../commands/brainstorm');
339
- const { autopilotAtris: autopilotCmd, autopilotFromTodo: autopilotFromTodoCmd } = require('../commands/autopilot');
340
362
  const { activateAtris: activateCmd } = require('../commands/activate');
341
363
  const { statusAtris: statusCmd } = require('../commands/status');
342
- const { analyticsAtris: analyticsCmd } = require('../commands/analytics');
343
- const { cleanAtris: cleanCmd } = require('../commands/clean');
344
- const { verifyAtris: verifyCmd } = require('../commands/verify');
345
- const { skillCommand: skillCmd } = require('../commands/skill');
346
- const { memberCommand: memberCmd } = require('../commands/member');
347
- const { pluginCommand: pluginCmd } = require('../commands/plugin');
364
+ const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('../commands/workflow');
365
+
366
+ // All other commands are lazy-loaded inline (require() only when invoked)
348
367
 
349
368
  // Check if this is a known command or natural language input
350
369
  const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'plan', 'do', 'review',
351
- 'activate', 'agent', 'chat', 'login', 'logout', 'whoami', 'switch', 'accounts', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
352
- 'clean', 'verify', 'search', 'skill', 'member', 'plugin',
370
+ 'activate', 'agent', 'chat', 'console', 'login', 'logout', 'whoami', 'switch', 'accounts', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
371
+ 'clean', 'verify', 'search', 'skill', 'member', 'plugin', 'sync',
353
372
  'gmail', 'calendar', 'twitter', 'slack', 'integrations'];
354
373
 
355
374
  // Check if command is an atris.md spec file - triggers welcome visualization
@@ -651,11 +670,11 @@ if (command === 'init') {
651
670
  process.exit(1);
652
671
  });
653
672
  } else if (command === 'agent') {
654
- agentAtris();
673
+ agentAtris().then(() => process.exit(0)).catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
655
674
  } else if (command === 'log') {
656
675
  const subcommand = process.argv[3];
657
676
  if (subcommand === 'sync') {
658
- logSyncCmd()
677
+ require('../commands/log-sync').logSyncAtris()
659
678
  .then(() => process.exit(0))
660
679
  .catch((error) => {
661
680
  console.error(`✗ Log sync failed: ${error.message || error}`);
@@ -666,10 +685,10 @@ if (command === 'init') {
666
685
  }
667
686
  } else if (command === 'activate') {
668
687
  activateCmd();
669
- } else if (command === 'update') {
688
+ } else if (command === 'update' || command === 'sync') {
670
689
  syncCmd();
671
690
  } else if (command === 'upgrade') {
672
- upgradeAtris();
691
+ upgradeAtris().then(() => process.exit(0)).catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
673
692
  } else if (command === 'chat') {
674
693
  chatAtris()
675
694
  .then(() => process.exit(0))
@@ -677,23 +696,25 @@ if (command === 'init') {
677
696
  console.error(`✗ Chat failed: ${error.message || error}`);
678
697
  process.exit(1);
679
698
  });
699
+ } else if (command === 'console') {
700
+ consoleCmd();
680
701
  } else if (command === 'version') {
681
- versionCmd();
702
+ require('../commands/version').showVersion();
682
703
  } else if (command === 'login') {
683
- loginCmd();
704
+ require('../commands/auth').loginAtris();
684
705
  } else if (command === 'logout') {
685
- logoutCmd();
706
+ require('../commands/auth').logoutAtris();
686
707
  } else if (command === 'whoami') {
687
- whoamiCmd();
708
+ require('../commands/auth').whoamiAtris();
688
709
  } else if (command === 'switch') {
689
- switchCmd();
710
+ require('../commands/auth').switchAccount();
690
711
  } else if (command === 'accounts') {
691
- accountsCmd();
712
+ require('../commands/auth').listAccountsCmd();
692
713
  } else if (command === 'visualize') {
693
714
  console.log('ℹ️ "atris visualize" is a legacy helper. Visualization is now built into "atris plan".');
694
715
  console.log(' Prefer: atris plan');
695
716
  console.log('');
696
- visualizeCmd();
717
+ require('../commands/visualize').visualizeAtris();
697
718
  } else if (command === 'autopilot') {
698
719
  const args = process.argv.slice(3);
699
720
  if (args.includes('--help') || args.includes('-h')) {
@@ -721,9 +742,9 @@ if (command === 'init') {
721
742
 
722
743
  let promise;
723
744
  if (fromTodo) {
724
- promise = autopilotFromTodoCmd(options);
745
+ promise = require('../commands/autopilot').autopilotFromTodo(options);
725
746
  } else if (description) {
726
- promise = autopilotCmd(description, options);
747
+ promise = require('../commands/autopilot').autopilotAtris(description, options);
727
748
  } else {
728
749
  console.log('Usage: atris autopilot "description" [--bug] [--verbose] [--iterations=N]');
729
750
  console.log(' atris autopilot --from-todo');
@@ -739,7 +760,7 @@ if (command === 'init') {
739
760
  process.exit(1);
740
761
  });
741
762
  } else if (command === 'brainstorm') {
742
- brainstormCmd()
763
+ require('../commands/brainstorm').brainstormAtris()
743
764
  .then(() => process.exit(0))
744
765
  .catch((error) => {
745
766
  console.error(`✗ Brainstorm failed: ${error.message || error}`);
@@ -794,13 +815,13 @@ if (command === 'init') {
794
815
  const isQuick = process.argv.includes('--quick') || process.argv.includes('-q');
795
816
  statusCmd(isQuick);
796
817
  } else if (command === 'analytics') {
797
- analyticsCmd();
818
+ require('../commands/analytics').analyticsAtris();
798
819
  } else if (command === 'clean') {
799
820
  const dryRun = process.argv.includes('--dry-run') || process.argv.includes('-n');
800
- cleanCmd({ dryRun });
821
+ require('../commands/clean').cleanAtris({ dryRun });
801
822
  } else if (command === 'verify') {
802
823
  const taskId = process.argv[3] || null;
803
- verifyCmd(taskId);
824
+ require('../commands/verify').verifyAtris(taskId);
804
825
  } else if (command === 'search') {
805
826
  const keyword = process.argv.slice(3).join(' ');
806
827
  searchJournal(keyword);
@@ -810,241 +831,51 @@ if (command === 'init') {
810
831
  const args = process.argv.slice(4);
811
832
  gmailCommand(subcommand, ...args)
812
833
  .then(() => process.exit(0))
813
- .catch((err) => { console.error(err.message); process.exit(1); });
834
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
814
835
  } else if (command === 'calendar') {
815
836
  const { calendarCommand } = require('../commands/integrations');
816
837
  const subcommand = process.argv[3];
817
838
  const args = process.argv.slice(4);
818
839
  calendarCommand(subcommand, ...args)
819
840
  .then(() => process.exit(0))
820
- .catch((err) => { console.error(err.message); process.exit(1); });
841
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
821
842
  } else if (command === 'twitter') {
822
843
  const { twitterCommand } = require('../commands/integrations');
823
844
  const subcommand = process.argv[3];
824
845
  const args = process.argv.slice(4);
825
846
  twitterCommand(subcommand, ...args)
826
847
  .then(() => process.exit(0))
827
- .catch((err) => { console.error(err.message); process.exit(1); });
848
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
828
849
  } else if (command === 'slack') {
829
850
  const { slackCommand } = require('../commands/integrations');
830
851
  const subcommand = process.argv[3];
831
852
  const args = process.argv.slice(4);
832
853
  slackCommand(subcommand, ...args)
833
854
  .then(() => process.exit(0))
834
- .catch((err) => { console.error(err.message); process.exit(1); });
855
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
835
856
  } else if (command === 'integrations') {
836
857
  const { integrationsStatus } = require('../commands/integrations');
837
858
  integrationsStatus()
838
859
  .then(() => process.exit(0))
839
- .catch((err) => { console.error(err.message); process.exit(1); });
860
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
840
861
  } else if (command === 'skill') {
841
862
  const subcommand = process.argv[3];
842
863
  const args = process.argv.slice(4);
843
- skillCmd(subcommand, ...args);
864
+ require('../commands/skill').skillCommand(subcommand, ...args);
844
865
  } else if (command === 'member') {
845
866
  const subcommand = process.argv[3];
846
867
  const args = process.argv.slice(4);
847
- memberCmd(subcommand, ...args);
868
+ require('../commands/member').memberCommand(subcommand, ...args);
848
869
  } else if (command === 'plugin') {
849
870
  const subcommand = process.argv[3] || 'build';
850
871
  const args = process.argv.slice(4);
851
- pluginCmd(subcommand, ...args);
872
+ require('../commands/plugin').pluginCommand(subcommand, ...args);
852
873
  } else {
853
874
  console.log(`Unknown command: ${command}`);
854
875
  console.log('Run "atris help" to see available commands');
855
876
  process.exit(1);
856
877
  }
857
878
 
858
- // NOTE: initAtris, syncAtris, logAtris, appendLog, logSyncAtris, showTodayLog, showRecentLogs
859
- // are legacy inline implementations. Routing now uses require('../commands/...') instead.
860
- // The journal utilities (getLogPath, ensureLogDirectory, createLogFile) are still used by
861
- // top-level code at lines ~393 and ~2553 via hoisting — do not remove without migrating those.
862
- function initAtris() {
863
- const targetDir = path.join(process.cwd(), 'atris');
864
- const teamDir = path.join(targetDir, 'team');
865
- const sourceFile = path.join(__dirname, '..', 'atris.md');
866
- const targetFile = path.join(targetDir, 'atris.md');
867
-
868
- // Create atris/ folder structure
869
- if (!fs.existsSync(targetDir)) {
870
- fs.mkdirSync(targetDir, { recursive: true });
871
- console.log('✓ Created atris/ folder');
872
- } else {
873
- console.log('✓ atris/ folder already exists');
874
- }
875
-
876
- // Create team/ subfolder
877
- if (!fs.existsSync(teamDir)) {
878
- fs.mkdirSync(teamDir, { recursive: true });
879
- console.log('✓ Created atris/team/ folder');
880
- }
881
-
882
- // Create policies/ subfolder
883
- const policiesDir = path.join(targetDir, 'policies');
884
- if (!fs.existsSync(policiesDir)) {
885
- fs.mkdirSync(policiesDir, { recursive: true });
886
- console.log('✓ Created atris/policies/ folder');
887
- }
888
-
889
- // Create placeholder files
890
- const gettingStartedFile = path.join(targetDir, 'GETTING_STARTED.md');
891
- const personaFile = path.join(targetDir, 'PERSONA.md');
892
- const mapFile = path.join(targetDir, 'MAP.md');
893
- const taskContextsFile = path.join(targetDir, 'TASK_CONTEXTS.md');
894
- const navigatorFile = path.join(teamDir, 'navigator.md');
895
- const executorFile = path.join(teamDir, 'executor.md');
896
- const validatorFile = path.join(teamDir, 'validator.md');
897
- const launcherFile = path.join(teamDir, 'launcher.md');
898
-
899
- const gettingStartedSource = path.join(__dirname, '..', 'GETTING_STARTED.md');
900
- const personaSource = path.join(__dirname, '..', 'PERSONA.md');
901
-
902
- // Copy GETTING_STARTED.md
903
- if (!fs.existsSync(gettingStartedFile) && fs.existsSync(gettingStartedSource)) {
904
- fs.copyFileSync(gettingStartedSource, gettingStartedFile);
905
- console.log('✓ Created GETTING_STARTED.md');
906
- }
907
-
908
- // Copy PERSONA.md
909
- if (!fs.existsSync(personaFile) && fs.existsSync(personaSource)) {
910
- fs.copyFileSync(personaSource, personaFile);
911
- console.log('✓ Created PERSONA.md');
912
- }
913
-
914
- if (!fs.existsSync(mapFile)) {
915
- fs.writeFileSync(mapFile, '# MAP.md\n\n> Generated by your AI agent after reading atris.md\n\nRun your AI agent with atris.md to populate this file.\n');
916
- console.log('✓ Created MAP.md placeholder');
917
- }
918
-
919
- if (!fs.existsSync(taskContextsFile)) {
920
- fs.writeFileSync(taskContextsFile, '# TASK_CONTEXTS.md\n\n> Generated by your AI agent after reading atris.md\n\nRun your AI agent with atris.md to populate this file.\n');
921
- console.log('✓ Created TASK_CONTEXTS.md placeholder');
922
- }
923
-
924
- // Copy agent templates from package (MEMBER.md directory format)
925
- const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer', 'researcher'];
926
- members.forEach(name => {
927
- const sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
928
- const memberDir = path.join(teamDir, name);
929
- const targetFile = path.join(memberDir, 'MEMBER.md');
930
- const legacyFile = path.join(teamDir, `${name}.md`);
931
-
932
- if (fs.existsSync(targetFile) || fs.existsSync(legacyFile)) return;
933
-
934
- if (fs.existsSync(sourceFile)) {
935
- fs.mkdirSync(memberDir, { recursive: true });
936
- fs.copyFileSync(sourceFile, targetFile);
937
- console.log(`✓ Created team/${name}/MEMBER.md`);
938
- }
939
- });
940
-
941
- // Copy policies from package
942
- const antislopSource = path.join(__dirname, '..', 'atris', 'policies', 'ANTISLOP.md');
943
- const antislopFile = path.join(policiesDir, 'ANTISLOP.md');
944
- if (!fs.existsSync(antislopFile) && fs.existsSync(antislopSource)) {
945
- fs.copyFileSync(antislopSource, antislopFile);
946
- console.log('✓ Created policies/ANTISLOP.md');
947
- }
948
-
949
- // Copy atris.md to the folder
950
- if (fs.existsSync(sourceFile)) {
951
- fs.copyFileSync(sourceFile, targetFile);
952
- console.log('✓ Copied atris.md to atris/ folder');
953
- console.log('\nAtris initialized. Structure created:');
954
- console.log(' atris/');
955
- console.log(' ├── GETTING_STARTED.md (read this first!)');
956
- console.log(' ├── PERSONA.md (agent personality)');
957
- console.log(' ├── atris.md (AI agent instructions)');
958
- console.log(' ├── MAP.md (placeholder)');
959
- console.log(' ├── TASK_CONTEXTS.md (placeholder)');
960
- console.log(' ├── team/');
961
- console.log(' │ ├── navigator.md');
962
- console.log(' │ ├── executor.md');
963
- console.log(' │ ├── validator.md');
964
- console.log(' │ └── launcher.md');
965
- console.log(' └── policies/');
966
- console.log(' └── ANTISLOP.md (output quality checklist)');
967
- console.log('\nNext steps:');
968
- console.log('1. Read atris/GETTING_STARTED.md for the full guide');
969
- console.log('2. Open atris/atris.md and paste it to your AI agent');
970
- console.log('3. Your agent will populate all placeholder files in ~10 mins');
971
- } else {
972
- console.error('✗ Error: atris.md not found in package');
973
- process.exit(1);
974
- }
975
- }
976
-
977
- function syncAtris() {
978
- const targetDir = path.join(process.cwd(), 'atris');
979
- const teamDir = path.join(targetDir, 'team');
980
-
981
- // Check if atris/ folder exists
982
- if (!fs.existsSync(targetDir)) {
983
- console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
984
- process.exit(1);
985
- }
986
-
987
- // Ensure team folder exists
988
- if (!fs.existsSync(teamDir)) {
989
- fs.mkdirSync(teamDir, { recursive: true });
990
- }
991
-
992
- // Ensure policies folder exists
993
- const policiesDir = path.join(targetDir, 'policies');
994
- if (!fs.existsSync(policiesDir)) {
995
- fs.mkdirSync(policiesDir, { recursive: true });
996
- console.log('✓ Created atris/policies/ folder');
997
- }
998
-
999
- // Files to sync
1000
- const filesToSync = [
1001
- { source: 'atris.md', target: 'atris.md' },
1002
- { source: 'atrisDev.md', target: 'atrisDev.md' },
1003
- { source: 'PERSONA.md', target: 'PERSONA.md' },
1004
- { source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
1005
- { source: 'atris/team/navigator/MEMBER.md', target: 'team/navigator/MEMBER.md' },
1006
- { source: 'atris/team/executor/MEMBER.md', target: 'team/executor/MEMBER.md' },
1007
- { source: 'atris/team/validator/MEMBER.md', target: 'team/validator/MEMBER.md' },
1008
- { source: 'atris/team/launcher/MEMBER.md', target: 'team/launcher/MEMBER.md' },
1009
- { source: 'atris/team/brainstormer/MEMBER.md', target: 'team/brainstormer/MEMBER.md' },
1010
- { source: 'atris/team/researcher/MEMBER.md', target: 'team/researcher/MEMBER.md' },
1011
- { source: 'atris/policies/ANTISLOP.md', target: 'policies/ANTISLOP.md' }
1012
- ];
1013
-
1014
- let updated = 0;
1015
- let skipped = 0;
1016
-
1017
- filesToSync.forEach(({ source, target }) => {
1018
- const sourceFile = path.join(__dirname, '..', source);
1019
- const targetFile = path.join(targetDir, target);
1020
-
1021
- if (!fs.existsSync(sourceFile)) {
1022
- console.log(`⚠ Skipping ${source} (not found in package)`);
1023
- return;
1024
- }
1025
-
1026
- const currentContent = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, 'utf8') : '';
1027
- const newContent = fs.readFileSync(sourceFile, 'utf8');
1028
-
1029
- if (currentContent === newContent) {
1030
- skipped++;
1031
- return;
1032
- }
1033
-
1034
- fs.mkdirSync(path.dirname(targetFile), { recursive: true });
1035
- fs.copyFileSync(sourceFile, targetFile);
1036
- console.log(`✓ Updated ${target}`);
1037
- updated++;
1038
- });
1039
-
1040
- if (updated === 0) {
1041
- console.log('✓ Already up to date');
1042
- } else {
1043
- console.log(`\n✓ Updated ${updated} file(s), ${skipped} unchanged`);
1044
- console.log('\nRun your AI agent again to use the latest specs and agent templates.');
1045
- }
1046
- }
1047
-
1048
879
  async function upgradeAtris() {
1049
880
  console.log('');
1050
881
  console.log('┌─────────────────────────────────────────────────────────────┐');
@@ -1094,84 +925,21 @@ async function upgradeAtris() {
1094
925
  }
1095
926
  }
1096
927
 
1097
- // ============================================
1098
- // Log System
1099
- // ============================================
1100
-
1101
- function getLogPath(dateStr) {
1102
- const targetDir = path.join(process.cwd(), 'atris');
1103
- const date = dateStr ? new Date(dateStr) : new Date();
1104
- const year = date.getFullYear();
1105
- const month = String(date.getMonth() + 1).padStart(2, '0');
1106
- const day = String(date.getDate()).padStart(2, '0');
1107
- const dateFormatted = `${year}-${month}-${day}`; // YYYY-MM-DD in local time
1108
-
1109
- const logsDir = path.join(targetDir, 'logs');
1110
- const yearDir = path.join(logsDir, year.toString());
1111
- const logFile = path.join(yearDir, `${dateFormatted}.md`);
1112
-
1113
- return { logsDir, yearDir, logFile, dateFormatted };
1114
- }
1115
-
1116
- function ensureLogDirectory() {
1117
- const { logsDir, yearDir } = getLogPath();
1118
-
1119
- if (!fs.existsSync(logsDir)) {
1120
- fs.mkdirSync(logsDir, { recursive: true });
1121
- }
1122
-
1123
- if (!fs.existsSync(yearDir)) {
1124
- fs.mkdirSync(yearDir, { recursive: true });
1125
- }
1126
- }
1127
-
1128
- function createLogFile(logFile, dateFormatted) {
1129
- let carryInProgress = '';
1130
- let carryBacklog = '';
1131
- let carryInbox = '';
1132
-
928
+ function showVersion() {
1133
929
  try {
1134
- const [y, m, d] = String(dateFormatted).split('-').map(Number);
1135
- if (Number.isFinite(y) && Number.isFinite(m) && Number.isFinite(d)) {
1136
- const prev = new Date(y, m - 1, d);
1137
- prev.setDate(prev.getDate() - 1);
1138
-
1139
- const prevYear = prev.getFullYear();
1140
- const prevMonth = String(prev.getMonth() + 1).padStart(2, '0');
1141
- const prevDay = String(prev.getDate()).padStart(2, '0');
1142
- const prevDateFormatted = `${prevYear}-${prevMonth}-${prevDay}`;
1143
- const prevLogFile = path.join(process.cwd(), 'atris', 'logs', prevYear.toString(), `${prevDateFormatted}.md`);
1144
-
1145
- if (fs.existsSync(prevLogFile)) {
1146
- const prevContent = fs.readFileSync(prevLogFile, 'utf8');
1147
-
1148
- const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1149
- const sectionBody = (headingLine) => {
1150
- const regex = new RegExp(
1151
- `## ${escapeRegExp(headingLine)}\\n([\\s\\S]*?)(?=\\n---|\\n## |$)`
1152
- );
1153
- const match = prevContent.match(regex);
1154
- return match ? match[1].trim() : '';
1155
- };
1156
-
1157
- carryInProgress = sectionBody('In Progress 🔄');
1158
- carryBacklog = sectionBody('Backlog');
1159
- carryInbox = sectionBody('Inbox');
1160
- }
1161
- }
1162
- } catch {
1163
- // Best-effort carry-forward; never block journal creation.
930
+ const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
931
+ console.log(`atris v${packageJson.version}`);
932
+ } catch (error) {
933
+ console.error('✗ Error: Could not read package.json');
934
+ process.exit(1);
1164
935
  }
1165
-
1166
- const inProgressBody = carryInProgress ? `${carryInProgress}\n\n` : '';
1167
- const backlogBody = carryBacklog ? `${carryBacklog}\n\n` : '';
1168
- const inboxBody = carryInbox ? `${carryInbox}\n\n` : '';
1169
-
1170
- const initialContent = `# Log — ${dateFormatted}\n\n## Completed ✅\n\n---\n\n## In Progress 🔄\n\n${inProgressBody}---\n\n## Backlog\n\n${backlogBody}---\n\n## Notes\n\n---\n\n## Inbox\n\n${inboxBody}\n`;
1171
- fs.writeFileSync(logFile, initialContent);
1172
936
  }
1173
937
 
1174
- function logAtris() {
938
+ // ============================================
939
+ // Agent Selection
940
+ // ============================================
941
+
942
+ async function agentAtris() {
1175
943
  const targetDir = path.join(process.cwd(), 'atris');
1176
944
 
1177
945
  // Check if atris/ folder exists
@@ -1180,1291 +948,138 @@ function logAtris() {
1180
948
  process.exit(1);
1181
949
  }
1182
950
 
1183
- // Ensure log directory exists
1184
- ensureLogDirectory();
1185
- const { logFile, dateFormatted } = getLogPath();
951
+ // Check if logged in
952
+ const credentials = loadCredentials();
1186
953
 
1187
- // Create log file if doesn't exist
1188
- if (!fs.existsSync(logFile)) {
1189
- createLogFile(logFile, dateFormatted);
954
+ if (!credentials || !credentials.token) {
955
+ console.error('✗ Error: Not logged in. Run "atris login" first.');
956
+ process.exit(1);
1190
957
  }
1191
958
 
1192
- // Start interactive logging session
1193
- console.log(`┌─────────────────────────────────────────────────────────┐`);
1194
- console.log(`│ Daily Log — ${dateFormatted} [type "exit" to quit] │`);
1195
- console.log(`└─────────────────────────────────────────────────────────┘`);
1196
- console.log('');
959
+ console.log('🔍 Fetching your agents...\n');
1197
960
 
1198
- const rl = readline.createInterface({
1199
- input: process.stdin,
1200
- output: process.stdout,
1201
- prompt: '> '
961
+ // Fetch agents from backend
962
+ const result = await apiRequestJson('/agent/my-agents', {
963
+ method: 'GET',
964
+ headers: {
965
+ 'Authorization': `Bearer ${credentials.token}`,
966
+ },
1202
967
  });
1203
968
 
1204
- rl.prompt();
969
+ if (!result.ok) {
970
+ console.error(`✗ Error: ${result.error || 'Failed to fetch agents'}`);
971
+ process.exit(1);
972
+ }
1205
973
 
1206
- rl.on('line', (line) => {
1207
- const input = line.trim();
974
+ const agents = result.data?.my_agents || [];
1208
975
 
1209
- if (input.toLowerCase() === 'exit') {
1210
- console.log('\n✓ Log saved');
1211
- rl.close();
1212
- process.exit(0);
1213
- }
976
+ if (agents.length === 0) {
977
+ console.log('No agents found. Create one at https://atris.ai');
978
+ process.exit(0);
979
+ }
1214
980
 
1215
- if (input) {
1216
- const entry = `- ${input}\n`;
1217
- fs.appendFileSync(logFile, entry);
981
+ // Show current selection
982
+ const config = loadConfig();
983
+ if (config.agent_id) {
984
+ const current = agents.find(a => a.id === config.agent_id);
985
+ if (current) {
986
+ console.log(`Current agent: ${current.name}\n`);
1218
987
  }
988
+ }
1219
989
 
1220
- rl.prompt();
990
+ // Display agents
991
+ console.log('Available agents:');
992
+ agents.forEach((agent, index) => {
993
+ console.log(` ${index + 1}. ${agent.name}`);
1221
994
  });
1222
995
 
1223
- rl.on('close', () => {
996
+ console.log('');
997
+
998
+ // Prompt for selection
999
+ const answer = await promptUser('Select agent number (or press Enter to cancel): ');
1000
+
1001
+ if (!answer) {
1002
+ console.log('Cancelled.');
1224
1003
  process.exit(0);
1225
- });
1226
- }
1004
+ }
1227
1005
 
1228
- function appendLog(message) {
1229
- ensureLogDirectory();
1230
- const { logFile, dateFormatted } = getLogPath();
1006
+ const selection = parseInt(answer, 10);
1231
1007
 
1232
- // Create log file if doesn't exist
1233
- if (!fs.existsSync(logFile)) {
1234
- createLogFile(logFile, dateFormatted);
1235
- console.log(`✓ Created log for ${dateFormatted}`);
1008
+ if (isNaN(selection) || selection < 1 || selection > agents.length) {
1009
+ console.error('✗ Invalid selection');
1010
+ process.exit(1);
1236
1011
  }
1237
1012
 
1238
- // Append message with timestamp
1239
- const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
1240
- const entry = `**${timestamp}** — ${message}\n\n`;
1013
+ const selectedAgent = agents[selection - 1];
1014
+
1015
+ // Save to config
1016
+ config.agent_id = selectedAgent.id;
1017
+ config.agent_name = selectedAgent.name;
1018
+ saveConfig(config);
1241
1019
 
1242
- fs.appendFileSync(logFile, entry);
1243
- console.log(`✓ Added to ${dateFormatted} log`);
1020
+ console.log(`\n✓ Selected agent: ${selectedAgent.name}`);
1021
+ console.log(`✓ Config saved to atris/.config`);
1022
+ console.log(`\nYou can now use "atris chat" to talk with this agent.`);
1244
1023
  }
1245
1024
 
1246
- async function logSyncAtris() {
1247
- const targetDir = path.join(process.cwd(), 'atris');
1248
1025
 
1026
+ async function chatAtris() {
1027
+ // Get message from command line args
1028
+ const message = process.argv.slice(3).join(' ').trim();
1029
+
1030
+ // Check atris/ exists
1031
+ const targetDir = path.join(process.cwd(), 'atris');
1249
1032
  if (!fs.existsSync(targetDir)) {
1250
- throw new Error('atris/ folder not found. Run "atris init" first.');
1033
+ console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
1034
+ process.exit(1);
1251
1035
  }
1252
1036
 
1253
- // Determine date (today by default, allow optional 4th arg or --date=)
1254
- let dateArg = process.argv[4];
1255
- if (dateArg && dateArg.startsWith('--date=')) {
1256
- dateArg = dateArg.split('=')[1];
1037
+ // Check agent selected
1038
+ const config = loadConfig();
1039
+ if (!config.agent_id) {
1040
+ console.error('✗ Error: No agent selected. Run "atris agent" first.');
1041
+ process.exit(1);
1257
1042
  }
1258
1043
 
1259
- let { logsDir, yearDir, logFile, dateFormatted } = getLogPath(dateArg);
1260
- if (Number.isNaN(new Date(dateFormatted).getTime())) {
1261
- throw new Error(`Invalid date provided: ${dateArg}`);
1044
+ // Check credentials
1045
+ const credentials = loadCredentials();
1046
+ if (!credentials || !credentials.token) {
1047
+ console.error('✗ Error: Not logged in. Run "atris login" first.');
1048
+ process.exit(1);
1262
1049
  }
1263
1050
 
1264
- // Ensure log directory and file exist
1265
- if (!fs.existsSync(logsDir)) {
1266
- fs.mkdirSync(logsDir, { recursive: true });
1267
- }
1268
- if (!fs.existsSync(yearDir)) {
1269
- fs.mkdirSync(yearDir, { recursive: true });
1270
- }
1271
- if (!fs.existsSync(logFile)) {
1272
- createLogFile(logFile, dateFormatted);
1273
- console.log(`Created local log template for ${dateFormatted}. Fill it in before syncing.`);
1051
+ // If message provided, one-shot mode
1052
+ if (message) {
1053
+ await chatOnce(config, credentials, message);
1054
+ return;
1274
1055
  }
1275
1056
 
1276
- let localContent = fs.readFileSync(logFile, 'utf8');
1277
- const localHash = computeContentHash(localContent);
1278
-
1279
- // Ensure agent selected
1280
- const config = loadConfig();
1281
- if (!config.agent_id) {
1282
- throw new Error('No agent selected. Run "atris agent" first.');
1283
- }
1057
+ // Otherwise, interactive mode
1058
+ await chatInteractive(config, credentials);
1059
+ }
1284
1060
 
1285
- // Ensure credentials
1286
- const ensured = await ensureValidCredentials();
1287
- if (ensured.error) {
1288
- if (ensured.error === 'not_logged_in') {
1289
- throw new Error('Not logged in. Run "atris login" first.');
1290
- }
1291
- if (ensured.detail && ensured.detail.toLowerCase().includes('enotfound')) {
1292
- throw new Error('Unable to reach Atris API. Check your network connection.');
1293
- }
1294
- throw new Error(ensured.detail || ensured.error || 'Authentication failed');
1295
- }
1061
+ async function chatOnce(config, credentials, message) {
1062
+ console.log(`\nAgent: ${config.agent_name || config.agent_id}`);
1063
+ console.log('');
1296
1064
 
1297
- const credentials = ensured.credentials;
1298
1065
  const agentId = config.agent_id;
1299
- const agentLabel = config.agent_name || agentId;
1300
-
1301
- console.log(`🔄 Syncing log for ${dateFormatted} with agent "${agentLabel}"`);
1302
-
1303
- // Check existing remote entry (best effort)
1304
- const syncState = loadLogSyncState();
1305
- const knownRemoteUpdate = syncState[dateFormatted]?.updated_at || null;
1306
- const knownRemoteHash = syncState[dateFormatted]?.hash || null;
1066
+ const apiUrl = getApiBaseUrl().replace(/\/api$/, '');
1067
+ const endpoint = `${apiUrl}/api/agent/${agentId}/pro-chat`;
1307
1068
 
1308
- let remoteExists = false;
1309
- let remoteUpdatedAt = null;
1310
- let remoteContent = null;
1311
- let remoteHash = null;
1312
- const existing = await apiRequestJson(`/agents/${agentId}/journal/${dateFormatted}`, {
1313
- method: 'GET',
1314
- token: credentials.token,
1069
+ const body = JSON.stringify({
1070
+ message: message,
1071
+ stream: true,
1072
+ memory_enabled: true,
1315
1073
  });
1316
1074
 
1317
- if (existing.ok) {
1318
- remoteExists = true;
1319
- remoteUpdatedAt = existing.data?.updated_at || existing.data?.created_at || null;
1320
- remoteContent = typeof existing.data?.content === 'string' ? existing.data.content : null;
1321
- remoteHash = remoteContent ? computeContentHash(remoteContent) : null;
1322
-
1323
- // Bidirectional sync: check if remote is newer
1324
- if (remoteUpdatedAt) {
1325
- const localStats = fs.statSync(logFile);
1326
- const localModified = localStats.mtime.toISOString();
1327
- const remoteTime = new Date(remoteUpdatedAt).getTime();
1328
- const localTime = new Date(localModified).getTime();
1329
-
1330
- const remoteMatchesKnown = (knownRemoteUpdate && isSameTimestamp(remoteUpdatedAt, knownRemoteUpdate))
1331
- || (remoteHash && knownRemoteHash && remoteHash === knownRemoteHash);
1332
-
1333
- if (remoteTime > localTime && !remoteMatchesKnown) {
1334
- const normalizedRemote = remoteContent ? remoteContent.replace(/\r\n/g, '\n') : null;
1335
- const normalizedLocal = localContent.replace(/\r\n/g, '\n');
1336
- if (normalizedRemote !== null && normalizedRemote.trim() === normalizedLocal.trim()) {
1337
- const remoteDate = new Date(remoteUpdatedAt);
1338
- if (!Number.isNaN(remoteDate.getTime())) {
1339
- fs.utimesSync(logFile, remoteDate, remoteDate);
1340
- const state = loadLogSyncState();
1341
- state[dateFormatted] = {
1342
- updated_at: remoteUpdatedAt,
1343
- hash: remoteHash || knownRemoteHash || computeContentHash(remoteContent || ''),
1344
- };
1345
- saveLogSyncState(state);
1346
- }
1347
- console.log('✓ Already synced (timestamps aligned with web)');
1348
- return;
1349
- }
1350
-
1351
- // Try section-based merge
1352
- try {
1353
- const localSections = parseJournalSections(normalizedLocal);
1354
- const remoteSections = parseJournalSections(normalizedRemote || '');
1355
- const { merged, conflicts } = mergeSections(localSections, remoteSections, knownRemoteHash);
1356
-
1357
- if (conflicts.length === 0) {
1358
- // Clean merge - auto-merge and continue
1359
- const mergedContent = reconstructJournal(merged);
1360
- fs.writeFileSync(logFile, mergedContent, 'utf8');
1361
- console.log('✓ Auto-merged web and local changes');
1362
- console.log(` Merged sections: ${Object.keys(merged).filter(k => k !== '__header__').join(', ')}`);
1363
- // Update local content for push
1364
- localContent = mergedContent;
1365
- } else {
1366
- // Conflicts detected - prompt user
1367
- console.log('⚠️ Conflicting changes in same section(s)');
1368
- console.log(` Conflicts: ${conflicts.join(', ')}`);
1369
- console.log(` Remote updated: ${remoteUpdatedAt}`);
1370
- console.log(` Local modified: ${localModified}`);
1371
- console.log(' Type "y" to replace local with web version, or "n" to keep local changes.');
1372
- console.log('');
1373
-
1374
- if (typeof remoteContent === 'string') {
1375
- showLogDiff(logFile, remoteContent);
1376
- }
1377
-
1378
- const answer = await promptUser('Overwrite local with web version? (y/n): ');
1379
-
1380
- if (answer && answer.toLowerCase() === 'y') {
1381
- // Pull remote content
1382
- const pulledContent = existing.data?.content || '';
1383
- fs.writeFileSync(logFile, pulledContent, 'utf8');
1384
- remoteHash = computeContentHash(pulledContent);
1385
- console.log('✓ Local journal updated from web');
1386
- console.log(`🗒️ File: ${path.relative(process.cwd(), logFile)}`);
1387
- if (remoteUpdatedAt) {
1388
- const remoteDate = new Date(remoteUpdatedAt);
1389
- if (!Number.isNaN(remoteDate.getTime())) {
1390
- fs.utimesSync(logFile, remoteDate, remoteDate);
1391
- }
1392
- const state = loadLogSyncState();
1393
- state[dateFormatted] = {
1394
- updated_at: remoteUpdatedAt,
1395
- hash: remoteHash || computeContentHash(pulledContent),
1396
- };
1397
- saveLogSyncState(state);
1398
- }
1399
- return;
1400
- } else {
1401
- console.log('⏩ Keeping local version, will push to web');
1402
- }
1403
- }
1404
- } catch (parseError) {
1405
- // Fallback to old prompt behavior if parsing fails
1406
- console.log('⚠️ Web version is newer than local version');
1407
- console.log(` Remote updated: ${remoteUpdatedAt}`);
1408
- console.log(` Local modified: ${localModified}`);
1409
- console.log(' Type "y" to replace your local file with the web version, or "n" to keep local changes and push them to the web.');
1410
- console.log('');
1411
-
1412
- if (typeof remoteContent === 'string') {
1413
- showLogDiff(logFile, remoteContent);
1414
- }
1415
-
1416
- const answer = await promptUser('Overwrite local with web version? (y/n): ');
1417
-
1418
- if (answer && answer.toLowerCase() === 'y') {
1419
- // Pull remote content
1420
- const pulledContent = existing.data?.content || '';
1421
- fs.writeFileSync(logFile, pulledContent, 'utf8');
1422
- remoteHash = computeContentHash(pulledContent);
1423
- console.log('✓ Local journal updated from web');
1424
- console.log(`🗒️ File: ${path.relative(process.cwd(), logFile)}`);
1425
- if (remoteUpdatedAt) {
1426
- const remoteDate = new Date(remoteUpdatedAt);
1427
- if (!Number.isNaN(remoteDate.getTime())) {
1428
- fs.utimesSync(logFile, remoteDate, remoteDate);
1429
- }
1430
- const state = loadLogSyncState();
1431
- state[dateFormatted] = {
1432
- updated_at: remoteUpdatedAt,
1433
- hash: remoteHash || computeContentHash(pulledContent),
1434
- };
1435
- saveLogSyncState(state);
1436
- }
1437
- return;
1438
- } else {
1439
- console.log('⏩ Keeping local version, will push to web');
1440
- }
1441
- }
1442
- } else if (remoteTime > localTime && remoteMatchesKnown) {
1443
- console.log('⚠️ Web timestamp ahead due to clock skew (matches last sync); pushing local changes.');
1444
- } else if (remoteTime === localTime) {
1445
- console.log('✓ Already synced (local and web are identical)');
1446
- if (remoteUpdatedAt) {
1447
- const state = loadLogSyncState();
1448
- state[dateFormatted] = {
1449
- updated_at: remoteUpdatedAt,
1450
- hash: remoteHash || knownRemoteHash || computeContentHash(remoteContent || ''),
1451
- };
1452
- saveLogSyncState(state);
1453
- }
1454
- return;
1455
- }
1456
- }
1457
- } else if (!existing.status) {
1458
- throw new Error('Unable to reach Atris API. Check your network connection.');
1459
- } else if (existing.status && existing.status !== 404) {
1460
- throw new Error(existing.error || 'Failed to check existing journal entry');
1461
- }
1462
-
1463
- const payload = {
1464
- content: localContent,
1465
- metadata: {
1466
- source: 'cli',
1467
- local_path: `logs/${dateFormatted}.md`,
1468
- },
1469
- };
1470
-
1471
- const result = await apiRequestJson(`/agents/${agentId}/journal/${dateFormatted}`, {
1472
- method: 'PUT',
1473
- token: credentials.token,
1474
- body: payload,
1475
- });
1476
-
1477
- if (!result.ok) {
1478
- if (!result.status) {
1479
- throw new Error('Unable to reach Atris API. Check your network connection.');
1480
- }
1481
- throw new Error(result.error || 'Failed to sync journal entry');
1482
- }
1483
-
1484
- const data = result.data || {};
1485
- const updatedAt = data.updated_at || new Date().toISOString();
1486
-
1487
- if (remoteExists) {
1488
- console.log(`✓ Updated journal entry (previous update: ${remoteUpdatedAt || 'unknown'})`);
1489
- } else {
1490
- console.log('✓ Created journal entry in Atris');
1491
- }
1492
-
1493
- console.log(`🗒️ Local file: ${path.relative(process.cwd(), logFile)}`);
1494
- console.log(`🕒 Updated at: ${updatedAt}`);
1495
- const updatedDate = new Date(updatedAt);
1496
- if (!Number.isNaN(updatedDate.getTime())) {
1497
- fs.utimesSync(logFile, updatedDate, updatedDate);
1498
- }
1499
- const finalContent = fs.readFileSync(logFile, 'utf8');
1500
- const finalHash = computeContentHash(finalContent);
1501
- const finalState = loadLogSyncState();
1502
- finalState[dateFormatted] = {
1503
- updated_at: updatedAt,
1504
- hash: finalHash,
1505
- };
1506
- saveLogSyncState(finalState);
1507
- }
1508
-
1509
- function showTodayLog() {
1510
- const { logFile, dateFormatted } = getLogPath();
1511
-
1512
- if (!fs.existsSync(logFile)) {
1513
- console.log(`No log for today (${dateFormatted})`);
1514
- console.log('\nCreate one with: atris log "your message"');
1515
- process.exit(0);
1516
- }
1517
-
1518
- const content = fs.readFileSync(logFile, 'utf8');
1519
- console.log(content);
1520
- }
1521
-
1522
- function showRecentLogs() {
1523
- const { logsDir, yearDir } = getLogPath();
1524
-
1525
- if (!fs.existsSync(logsDir) || !fs.existsSync(yearDir)) {
1526
- console.log('No logs found');
1527
- console.log('\nCreate one with: atris log "your message"');
1528
- process.exit(0);
1529
- }
1530
-
1531
- // Get all log files in current year directory
1532
- const files = fs.readdirSync(yearDir)
1533
- .filter(f => f.endsWith('.md'))
1534
- .sort()
1535
- .reverse()
1536
- .slice(0, 3); // Last 3 days
1537
-
1538
- if (files.length === 0) {
1539
- console.log('No logs found');
1540
- process.exit(0);
1541
- }
1542
-
1543
- console.log(`\n📋 Last ${files.length} day(s) of logs:\n`);
1544
- console.log('='.repeat(60) + '\n');
1545
-
1546
- files.reverse().forEach((file, index) => {
1547
- const filePath = path.join(yearDir, file);
1548
- const content = fs.readFileSync(filePath, 'utf8');
1549
- console.log(content);
1550
-
1551
- // Add separator between days
1552
- if (index < files.length - 1) {
1553
- console.log('─'.repeat(60) + '\n');
1554
- }
1555
- });
1556
- }
1557
-
1558
- // ============================================
1559
- // Authentication & Credentials Management
1560
- // ============================================
1561
-
1562
- function getCredentialsPath() {
1563
- const homeDir = os.homedir();
1564
- const atrisDir = path.join(homeDir, '.atris');
1565
-
1566
- // Create .atris directory if it doesn't exist
1567
- if (!fs.existsSync(atrisDir)) {
1568
- fs.mkdirSync(atrisDir, { recursive: true });
1569
- }
1570
-
1571
- return path.join(atrisDir, 'credentials.json');
1572
- }
1573
-
1574
- function saveCredentials(token, refreshToken, email, userId, provider) {
1575
- const credentialsPath = getCredentialsPath();
1576
- const credentials = {
1577
- token,
1578
- refresh_token: refreshToken || null,
1579
- email: email || null,
1580
- user_id: userId || null,
1581
- provider: provider || null,
1582
- saved_at: new Date().toISOString()
1583
- };
1584
-
1585
- fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
1586
- }
1587
-
1588
- function loadCredentials() {
1589
- const credentialsPath = getCredentialsPath();
1590
-
1591
- if (!fs.existsSync(credentialsPath)) {
1592
- return null;
1593
- }
1594
-
1595
- try {
1596
- const data = fs.readFileSync(credentialsPath, 'utf8');
1597
- const parsed = JSON.parse(data);
1598
- if (!parsed.provider) {
1599
- parsed.provider = null;
1600
- }
1601
- if (!parsed.saved_at && parsed.created_at) {
1602
- parsed.saved_at = parsed.created_at;
1603
- }
1604
- return parsed;
1605
- } catch (error) {
1606
- return null;
1607
- }
1608
- }
1609
-
1610
- function deleteCredentials() {
1611
- const credentialsPath = getCredentialsPath();
1612
-
1613
- if (fs.existsSync(credentialsPath)) {
1614
- fs.unlinkSync(credentialsPath);
1615
- }
1616
- }
1617
-
1618
- function getApiBaseUrl() {
1619
- const raw = process.env.ATRIS_API_URL || 'https://api.atris.ai/api';
1620
- return raw.replace(/\/$/, '');
1621
- }
1622
-
1623
- function getAppBaseUrl() {
1624
- const raw = process.env.ATRIS_APP_URL || 'https://atris.ai';
1625
- return raw.replace(/\/$/, '');
1626
- }
1627
-
1628
- function buildApiUrl(pathname) {
1629
- const base = getApiBaseUrl();
1630
- const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`;
1631
- return `${base}${normalizedPath}`;
1632
- }
1633
-
1634
- async function apiRequestJson(pathname, options = {}) {
1635
- const url = buildApiUrl(pathname);
1636
- const headers = { ...(options.headers || {}) };
1637
- if (options.token) {
1638
- headers.Authorization = `Bearer ${options.token}`;
1639
- }
1640
- if (!headers['User-Agent'] && !headers['user-agent']) {
1641
- headers['User-Agent'] = DEFAULT_USER_AGENT;
1642
- }
1643
- if (!headers['X-Atris-Client']) {
1644
- headers['X-Atris-Client'] = DEFAULT_CLIENT_ID;
1645
- }
1646
-
1647
- let bodyPayload;
1648
- if (options.body !== undefined && options.body !== null) {
1649
- if (typeof options.body === 'string' || Buffer.isBuffer(options.body)) {
1650
- bodyPayload = options.body;
1651
- } else {
1652
- bodyPayload = JSON.stringify(options.body);
1653
- if (!headers['Content-Type']) {
1654
- headers['Content-Type'] = 'application/json';
1655
- }
1656
- }
1657
- }
1658
-
1659
- try {
1660
- const result = await httpRequest(url, {
1661
- method: options.method || 'GET',
1662
- headers,
1663
- body: bodyPayload,
1664
- });
1665
-
1666
- const text = result.body.toString('utf8');
1667
- let data = null;
1668
- if (text) {
1669
- try {
1670
- data = JSON.parse(text);
1671
- } catch {
1672
- data = null;
1673
- }
1674
- }
1675
-
1676
- const ok = result.status >= 200 && result.status < 300;
1677
- const errorMessage = !ok
1678
- ? (data && typeof data === 'object' && (data.detail || data.error || data.message)) || text || 'Request failed'
1679
- : undefined;
1680
-
1681
- return {
1682
- ok,
1683
- status: result.status,
1684
- data,
1685
- text,
1686
- error: errorMessage,
1687
- };
1688
- } catch (error) {
1689
- return {
1690
- ok: false,
1691
- status: 0,
1692
- data: null,
1693
- text: '',
1694
- error: error.message || 'Network error',
1695
- };
1696
- }
1697
- }
1698
-
1699
- function httpRequest(urlString, options) {
1700
- return new Promise((resolve, reject) => {
1701
- const parsed = new URL(urlString);
1702
- const isHttps = parsed.protocol === 'https:';
1703
- const transport = isHttps ? https : http;
1704
-
1705
- const requestOptions = {
1706
- method: options.method || 'GET',
1707
- hostname: parsed.hostname,
1708
- port: parsed.port || (isHttps ? 443 : 80),
1709
- path: `${parsed.pathname}${parsed.search}`,
1710
- headers: { ...(options.headers || {}) },
1711
- };
1712
-
1713
- const req = transport.request(requestOptions, (res) => {
1714
- const chunks = [];
1715
- res.on('data', (chunk) => chunks.push(chunk));
1716
- res.on('end', () => {
1717
- resolve({
1718
- status: res.statusCode || 0,
1719
- headers: res.headers,
1720
- body: Buffer.concat(chunks),
1721
- });
1722
- });
1723
- });
1724
-
1725
- req.on('error', reject);
1726
-
1727
- if (options.body) {
1728
- if (!req.hasHeader('Content-Length')) {
1729
- req.setHeader('Content-Length', Buffer.byteLength(options.body));
1730
- }
1731
- req.write(options.body);
1732
- }
1733
-
1734
- req.end();
1735
- });
1736
- }
1737
-
1738
- async function validateAccessToken(token) {
1739
- if (!token) {
1740
- return { ok: false, status: 0, error: 'Missing token' };
1741
- }
1742
- return apiRequestJson('/auth/validate', {
1743
- method: 'POST',
1744
- body: { token },
1745
- token,
1746
- });
1747
- }
1748
-
1749
- async function refreshAccessToken(refreshToken, provider) {
1750
- if (!refreshToken) {
1751
- return { ok: false, status: 0, error: 'Missing refresh token' };
1752
- }
1753
- const body = { refresh_token: refreshToken };
1754
- if (provider) {
1755
- body.provider = provider;
1756
- }
1757
- return apiRequestJson('/auth/refresh', {
1758
- method: 'POST',
1759
- body,
1760
- });
1761
- }
1762
-
1763
- async function performTokenRefresh(credentials, sourceLabel = 'refreshed') {
1764
- if (!credentials || !credentials.refresh_token) {
1765
- return { ok: false, error: 'missing_refresh_token' };
1766
- }
1767
-
1768
- const refreshed = await refreshAccessToken(credentials.refresh_token, credentials.provider);
1769
- if (!refreshed.ok) {
1770
- return { ok: false, error: refreshed.error || 'Refresh request failed' };
1771
- }
1772
-
1773
- const accessToken = refreshed.data?.access_token;
1774
- if (!accessToken) {
1775
- return { ok: false, error: 'No access token returned by refresh API' };
1776
- }
1777
-
1778
- const newRefreshToken = refreshed.data?.refresh_token || credentials.refresh_token;
1779
- const refreshUser = refreshed.data?.user || null;
1780
- const provider = refreshed.data?.provider || credentials.provider;
1781
- const email = refreshUser?.email || credentials.email;
1782
- const userId = refreshUser?.id || credentials.user_id;
1783
-
1784
- saveCredentials(accessToken, newRefreshToken, email, userId, provider);
1785
- let latestCreds = loadCredentials();
1786
-
1787
- const validation = await validateAccessToken(accessToken);
1788
- let finalUser = refreshUser;
1789
-
1790
- if (validation.ok && validation.data?.valid) {
1791
- finalUser = validation.data.user || refreshUser || null;
1792
- const updatedEmail = finalUser?.email || latestCreds?.email || email;
1793
- const updatedProvider = finalUser?.provider || latestCreds?.provider || provider;
1794
- const updatedUserId = finalUser?.id || latestCreds?.user_id || userId;
1795
-
1796
- if (
1797
- !latestCreds ||
1798
- updatedEmail !== latestCreds.email ||
1799
- updatedProvider !== latestCreds.provider ||
1800
- updatedUserId !== latestCreds.user_id
1801
- ) {
1802
- saveCredentials(accessToken, newRefreshToken, updatedEmail, updatedUserId, updatedProvider);
1803
- latestCreds = loadCredentials();
1804
- }
1805
- }
1806
-
1807
- return {
1808
- ok: true,
1809
- payload: {
1810
- credentials: latestCreds || loadCredentials(),
1811
- user: finalUser,
1812
- source: sourceLabel,
1813
- },
1814
- };
1815
- }
1816
-
1817
- async function ensureValidCredentials(options = {}) {
1818
- let credentials = loadCredentials();
1819
- if (!credentials || !credentials.token) {
1820
- return { error: 'not_logged_in' };
1821
- }
1822
-
1823
- if (credentials.refresh_token && shouldRefreshToken(credentials.token)) {
1824
- const proactive = await performTokenRefresh(credentials, 'proactive_refresh');
1825
- if (proactive.ok) {
1826
- return proactive.payload;
1827
- }
1828
- credentials = loadCredentials() || credentials;
1829
- }
1830
-
1831
- const validation = await validateAccessToken(credentials.token);
1832
- if (validation.ok && validation.data?.valid) {
1833
- const user = validation.data.user || null;
1834
- const updatedEmail = user?.email || credentials.email;
1835
- const updatedProvider = user?.provider || credentials.provider;
1836
- const updatedUserId = user?.id || credentials.user_id;
1837
-
1838
- if (
1839
- updatedEmail !== credentials.email ||
1840
- updatedProvider !== credentials.provider ||
1841
- updatedUserId !== credentials.user_id
1842
- ) {
1843
- saveCredentials(
1844
- credentials.token,
1845
- credentials.refresh_token,
1846
- updatedEmail,
1847
- updatedUserId,
1848
- updatedProvider
1849
- );
1850
- }
1851
-
1852
- return {
1853
- credentials: loadCredentials(),
1854
- user,
1855
- source: 'access_token',
1856
- };
1857
- }
1858
-
1859
- if (!credentials.refresh_token) {
1860
- return { error: 'token_invalid', detail: validation.error || 'Token expired' };
1861
- }
1862
-
1863
- const refreshed = await performTokenRefresh(credentials, 'refreshed');
1864
- if (!refreshed.ok) {
1865
- return { error: 'refresh_failed', detail: refreshed.error };
1866
- }
1867
-
1868
- return refreshed.payload;
1869
- }
1870
-
1871
- async function fetchMyAgents(token) {
1872
- if (!token) {
1873
- return null;
1874
- }
1875
-
1876
- const response = await apiRequestJson('/agent/my-agents', {
1877
- method: 'GET',
1878
- token,
1879
- });
1880
-
1881
- if (!response.ok) {
1882
- if (response.status === 404) {
1883
- return null;
1884
- }
1885
- throw new Error(response.error || 'Failed to fetch agents');
1886
- }
1887
-
1888
- return response.data;
1889
- }
1890
-
1891
- async function displayAccountSummary() {
1892
- const ensured = await ensureValidCredentials();
1893
-
1894
- if (ensured.error) {
1895
- console.log('Status: Not logged in');
1896
- if (ensured.detail) {
1897
- console.log(`Reason: ${ensured.detail}`);
1898
- }
1899
- return { error: ensured.error, detail: ensured.detail };
1900
- }
1901
-
1902
- const { credentials, user } = ensured;
1903
- const email = user?.email || credentials.email || 'unknown';
1904
- const userId = user?.id || credentials.user_id || 'unknown';
1905
- const provider = user?.provider || credentials.provider || 'unknown';
1906
- const savedAt = credentials.saved_at || 'unknown';
1907
-
1908
- console.log('Status: Logged in ✓');
1909
- console.log(`Email: ${email}`);
1910
- console.log(`User ID: ${userId}`);
1911
- console.log(`Provider: ${provider}`);
1912
- console.log(`Credentials saved: ${savedAt}`);
1913
- console.log(`Credential file: ${getCredentialsPath()}`);
1914
-
1915
- try {
1916
- const agentsResponse = await fetchMyAgents(credentials.token);
1917
- if (agentsResponse && agentsResponse.my_agents) {
1918
- const agents = agentsResponse.my_agents;
1919
- const total = agentsResponse.total ?? agents.length;
1920
- console.log(`Agents: ${total}`);
1921
- agents.slice(0, 5).forEach((agent) => {
1922
- const name = agent.name || agent.id || 'Unnamed agent';
1923
- console.log(` • ${name}`);
1924
- });
1925
- if (total > 5) {
1926
- console.log(` …and ${total - 5} more`);
1927
- }
1928
- }
1929
- } catch (error) {
1930
- console.log(`Agents: Unable to load (${error.message})`);
1931
- }
1932
-
1933
- return { credentials, user };
1934
- }
1935
-
1936
- function openBrowser(url) {
1937
- const platform = os.platform();
1938
- let command;
1939
-
1940
- if (platform === 'darwin') {
1941
- command = `open "${url}"`;
1942
- } else if (platform === 'win32') {
1943
- command = `start "${url}"`;
1944
- } else {
1945
- command = `xdg-open "${url}"`;
1946
- }
1947
-
1948
- exec(command, (error) => {
1949
- if (error) {
1950
- console.log(`\nCouldn't open browser automatically. Please visit:\n${url}`);
1951
- }
1952
- });
1953
- }
1954
-
1955
- function promptUser(question) {
1956
- const rl = readline.createInterface({
1957
- input: process.stdin,
1958
- output: process.stdout
1959
- });
1960
-
1961
- return new Promise((resolve) => {
1962
- rl.question(question, (answer) => {
1963
- rl.close();
1964
- resolve(answer.trim());
1965
- });
1966
- });
1967
- }
1968
-
1969
- async function loginAtris() {
1970
- try {
1971
- console.log('🔐 Login to AtrisOS\n');
1972
-
1973
- const existing = loadCredentials();
1974
- if (existing) {
1975
- const label = existing.email || existing.user_id || 'unknown user';
1976
- console.log(`Already logged in as: ${label}`);
1977
- const confirm = await promptUser('Do you want to login again? (y/N): ');
1978
- if (confirm.toLowerCase() !== 'y') {
1979
- console.log('Login cancelled.');
1980
- process.exit(0);
1981
- }
1982
- }
1983
-
1984
- console.log('Choose login method:');
1985
- console.log(' 1. Browser OAuth (recommended)');
1986
- console.log(' 2. Paste existing API token');
1987
- console.log(' 3. Cancel');
1988
-
1989
- const choice = await promptUser('\nEnter choice (1-3): ');
1990
-
1991
- if (choice === '1') {
1992
- const loginUrl = `${getAppBaseUrl()}/auth/cli`;
1993
- console.log('\n🌐 Opening browser for OAuth login…');
1994
- console.log('If it does not open automatically, visit:');
1995
- console.log(loginUrl);
1996
- console.log('\nAfter signing in, copy the CLI code shown in the browser and paste it below.');
1997
- console.log('Codes expire after five minutes.\n');
1998
-
1999
- openBrowser(loginUrl);
2000
-
2001
- const code = await promptUser('Paste the CLI code here: ');
2002
- if (!code) {
2003
- console.error('✗ Error: Code is required');
2004
- process.exit(1);
2005
- }
2006
-
2007
- const exchange = await apiRequestJson('/auth/cli/exchange', {
2008
- method: 'POST',
2009
- body: { code: code.trim() },
2010
- });
2011
-
2012
- if (!exchange.ok || !exchange.data) {
2013
- console.error(`✗ Error: ${exchange.error || 'Invalid or expired code'}`);
2014
- process.exit(1);
2015
- }
2016
-
2017
- const payload = exchange.data;
2018
- const token = payload.token;
2019
- const refreshToken = payload.refresh_token;
2020
-
2021
- if (!token || !refreshToken) {
2022
- console.error('✗ Error: Backend did not return tokens. Please try again.');
2023
- process.exit(1);
2024
- }
2025
-
2026
- const email = payload.email || existing?.email || null;
2027
- const userId = payload.user_id || existing?.user_id || null;
2028
- const provider = payload.provider || 'atris';
2029
-
2030
- saveCredentials(token, refreshToken, email, userId, provider);
2031
- console.log('\n✓ Successfully logged in!');
2032
- await displayAccountSummary();
2033
- console.log('\nYou can now use cloud features with atris commands.');
2034
- process.exit(0);
2035
- } else if (choice === '2') {
2036
- console.log('\n📋 Manual Token Entry');
2037
- console.log('Get your token from: https://atris.ai/auth/cli\n');
2038
-
2039
- const tokenInput = await promptUser('Paste your API token: ');
2040
-
2041
- if (!tokenInput) {
2042
- console.error('✗ Error: Token is required');
2043
- process.exit(1);
2044
- }
2045
-
2046
- const trimmed = tokenInput.trim();
2047
- saveCredentials(trimmed, null, existing?.email || null, existing?.user_id || null, existing?.provider || 'manual');
2048
- console.log('\nAttempting to validate token…\n');
2049
-
2050
- const summary = await displayAccountSummary();
2051
- if (summary.error) {
2052
- console.log('\n⚠️ Token saved, but validation failed. You may need to relogin.');
2053
- } else {
2054
- console.log('\n✓ Token validated successfully.');
2055
- }
2056
-
2057
- console.log('\nYou can now use cloud features with atris commands.');
2058
- process.exit(0);
2059
- } else {
2060
- console.log('Login cancelled.');
2061
- process.exit(0);
2062
- }
2063
- } catch (error) {
2064
- console.error(`\n✗ Login failed: ${error.message || error}`);
2065
- process.exit(1);
2066
- }
2067
- }
2068
-
2069
- function logoutAtris() {
2070
- const credentials = loadCredentials();
2071
-
2072
- if (!credentials) {
2073
- console.log('Not currently logged in.');
2074
- process.exit(0);
2075
- }
2076
-
2077
- deleteCredentials();
2078
- console.log('✓ Successfully logged out');
2079
- console.log(`✓ Removed credentials from ${getCredentialsPath()}`);
2080
- }
2081
-
2082
- async function whoamiAtris() {
2083
- try {
2084
- const summary = await displayAccountSummary();
2085
- if (summary.error) {
2086
- console.log('\nRun "atris login" to authenticate with AtrisOS.');
2087
- process.exit(1);
2088
- }
2089
- process.exit(0);
2090
- } catch (error) {
2091
- console.error(`✗ Failed to fetch account details: ${error.message || error}`);
2092
- process.exit(1);
2093
- }
2094
- }
2095
-
2096
- function showVersion() {
2097
- try {
2098
- const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
2099
- console.log(`atris v${packageJson.version}`);
2100
- } catch (error) {
2101
- console.error('✗ Error: Could not read package.json');
2102
- process.exit(1);
2103
- }
2104
- }
2105
-
2106
- // ============================================
2107
- // Config Management
2108
- // ============================================
2109
-
2110
- function getConfigPath() {
2111
- const targetDir = path.join(process.cwd(), 'atris');
2112
- return path.join(targetDir, '.config');
2113
- }
2114
-
2115
- function loadConfig() {
2116
- const configPath = getConfigPath();
2117
-
2118
- if (!fs.existsSync(configPath)) {
2119
- return {};
2120
- }
2121
-
2122
- try {
2123
- const data = fs.readFileSync(configPath, 'utf8');
2124
- return JSON.parse(data);
2125
- } catch (error) {
2126
- return {};
2127
- }
2128
- }
2129
-
2130
- function saveConfig(config) {
2131
- const configPath = getConfigPath();
2132
- const targetDir = path.dirname(configPath);
2133
-
2134
- if (!fs.existsSync(targetDir)) {
2135
- console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
2136
- process.exit(1);
2137
- }
2138
-
2139
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
2140
- }
2141
-
2142
- function getLogSyncStatePath() {
2143
- const targetDir = path.join(process.cwd(), 'atris');
2144
- return path.join(targetDir, '.log_sync_state.json');
2145
- }
2146
-
2147
- function loadLogSyncState() {
2148
- const statePath = getLogSyncStatePath();
2149
- if (!fs.existsSync(statePath)) {
2150
- return {};
2151
- }
2152
-
2153
- try {
2154
- return JSON.parse(fs.readFileSync(statePath, 'utf8'));
2155
- } catch {
2156
- return {};
2157
- }
2158
- }
2159
-
2160
- function saveLogSyncState(state) {
2161
- const statePath = getLogSyncStatePath();
2162
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
2163
- }
2164
-
2165
- function isSameTimestamp(a, b) {
2166
- if (!a || !b) return false;
2167
- const ta = new Date(a).getTime();
2168
- const tb = new Date(b).getTime();
2169
- if (Number.isNaN(ta) || Number.isNaN(tb)) return false;
2170
- return Math.abs(ta - tb) < 5;
2171
- }
2172
-
2173
- function computeContentHash(content) {
2174
- if (typeof content !== 'string') {
2175
- return null;
2176
- }
2177
-
2178
- const normalized = content.replace(/\r\n/g, '\n');
2179
- return crypto.createHash('sha256').update(normalized).digest('hex');
2180
- }
2181
-
2182
- function parseJournalSections(content) {
2183
- const sections = {};
2184
- const lines = content.split('\n');
2185
- let currentSection = '__header__';
2186
- let currentContent = [];
2187
-
2188
- for (const line of lines) {
2189
- if (line.startsWith('## ')) {
2190
- // Save previous section
2191
- if (currentContent.length > 0 || currentSection === '__header__') {
2192
- sections[currentSection] = currentContent.join('\n');
2193
- }
2194
- // Start new section
2195
- currentSection = line.substring(3).trim();
2196
- currentContent = [line];
2197
- } else {
2198
- currentContent.push(line);
2199
- }
2200
- }
2201
-
2202
- // Save last section
2203
- if (currentContent.length > 0) {
2204
- sections[currentSection] = currentContent.join('\n');
2205
- }
2206
-
2207
- return sections;
2208
- }
2209
-
2210
- function mergeSections(localSections, remoteSections, knownRemoteHash) {
2211
- const merged = {};
2212
- const conflicts = [];
2213
-
2214
- // Get all unique section names
2215
- const allSections = new Set([...Object.keys(localSections), ...Object.keys(remoteSections)]);
2216
-
2217
- for (const section of allSections) {
2218
- const localContent = localSections[section] || '';
2219
- const remoteContent = remoteSections[section] || '';
2220
-
2221
- if (localContent === remoteContent) {
2222
- // Same content, use either
2223
- merged[section] = localContent;
2224
- } else if (!remoteContent) {
2225
- // Only in local, keep local
2226
- merged[section] = localContent;
2227
- } else if (!localContent) {
2228
- // Only in remote, keep remote
2229
- merged[section] = remoteContent;
2230
- } else {
2231
- // Both exist but differ - check if remote matches known state
2232
- const remoteHash = computeContentHash(remoteContent);
2233
- if (knownRemoteHash && remoteHash === knownRemoteHash) {
2234
- // Remote hasn't changed since last sync, prefer local
2235
- merged[section] = localContent;
2236
- } else {
2237
- // Real conflict - mark for user review
2238
- conflicts.push(section);
2239
- merged[section] = localContent; // Default to local
2240
- }
2241
- }
2242
- }
2243
-
2244
- return { merged, conflicts };
2245
- }
2246
-
2247
- function reconstructJournal(sections) {
2248
- const parts = [];
2249
-
2250
- // Header first
2251
- if (sections['__header__']) {
2252
- parts.push(sections['__header__']);
2253
- }
2254
-
2255
- // Then all other sections in order (preserve original order where possible)
2256
- const sectionOrder = ['Completed ✅', 'In Progress 🔄', 'Backlog', 'Notes', 'Inbox', 'Timestamps', 'Lessons Learned'];
2257
-
2258
- for (const section of sectionOrder) {
2259
- if (sections[section]) {
2260
- parts.push(sections[section]);
2261
- }
2262
- }
2263
-
2264
- // Add any remaining sections not in the standard order
2265
- for (const [section, content] of Object.entries(sections)) {
2266
- if (section !== '__header__' && !sectionOrder.includes(section)) {
2267
- parts.push(content);
2268
- }
2269
- }
2270
-
2271
- return parts.join('\n');
2272
- }
2273
-
2274
- function showLogDiff(localPath, remoteContent) {
2275
- let tmpDir;
2276
- try {
2277
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-diff-'));
2278
- const remotePath = path.join(tmpDir, 'remote.md');
2279
- fs.writeFileSync(remotePath, remoteContent, 'utf8');
2280
-
2281
- const diffCommands = [
2282
- { cmd: 'git', args: ['--no-pager', 'diff', '--no-index', '--color=always', '--', localPath, remotePath] },
2283
- { cmd: 'diff', args: ['-u', localPath, remotePath] },
2284
- ];
2285
-
2286
- let shown = false;
2287
- for (const { cmd, args } of diffCommands) {
2288
- const result = spawnSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
2289
- if (result.error || result.status === 127) {
2290
- continue;
2291
- }
2292
-
2293
- const output = `${result.stdout || ''}${result.stderr || ''}`.trimEnd();
2294
- if (output) {
2295
- console.log('─────────────────────────────────────────────────────────────');
2296
- console.log('Diff (web -> local):');
2297
- process.stdout.write(output.endsWith('\n') ? output : `${output}\n`);
2298
- console.log('─────────────────────────────────────────────────────────────');
2299
- shown = true;
2300
- break;
2301
- }
2302
- }
2303
-
2304
- if (!shown) {
2305
- console.log('─────────────────────────────────────────────────────────────');
2306
- console.log('Diff: (no textual diff available; files may be identical or differ only in whitespace)');
2307
- console.log('─────────────────────────────────────────────────────────────');
2308
- }
2309
- } catch (error) {
2310
- console.log('─────────────────────────────────────────────────────────────');
2311
- console.log(`Unable to show diff automatically (${error.message || error}).`);
2312
- console.log('─────────────────────────────────────────────────────────────');
2313
- } finally {
2314
- if (tmpDir) {
2315
- try {
2316
- fs.rmSync(tmpDir, { recursive: true, force: true });
2317
- } catch (_) {
2318
- // ignore cleanup errors
2319
- }
2320
- }
2321
- }
2322
- }
2323
- // ============================================
2324
- // Agent Selection
2325
- // ============================================
2326
-
2327
- async function agentAtris() {
2328
- const targetDir = path.join(process.cwd(), 'atris');
2329
-
2330
- // Check if atris/ folder exists
2331
- if (!fs.existsSync(targetDir)) {
2332
- console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
2333
- process.exit(1);
2334
- }
2335
-
2336
- // Check if logged in
2337
- const credentials = loadCredentials();
2338
-
2339
- if (!credentials || !credentials.token) {
2340
- console.error('✗ Error: Not logged in. Run "atris login" first.');
2341
- process.exit(1);
2342
- }
2343
-
2344
- console.log('🔍 Fetching your agents...\n');
2345
-
2346
- // Fetch agents from backend
2347
- const result = await apiRequestJson('/agent/my-agents', {
2348
- method: 'GET',
2349
- headers: {
2350
- 'Authorization': `Bearer ${credentials.token}`,
2351
- },
2352
- });
2353
-
2354
- if (!result.ok) {
2355
- console.error(`✗ Error: ${result.error || 'Failed to fetch agents'}`);
2356
- process.exit(1);
2357
- }
2358
-
2359
- const agents = result.data?.my_agents || [];
2360
-
2361
- if (agents.length === 0) {
2362
- console.log('No agents found. Create one at https://atris.ai');
2363
- process.exit(0);
2364
- }
2365
-
2366
- // Show current selection
2367
- const config = loadConfig();
2368
- if (config.agent_id) {
2369
- const current = agents.find(a => a.id === config.agent_id);
2370
- if (current) {
2371
- console.log(`Current agent: ${current.name}\n`);
2372
- }
2373
- }
2374
-
2375
- // Display agents
2376
- console.log('Available agents:');
2377
- agents.forEach((agent, index) => {
2378
- console.log(` ${index + 1}. ${agent.name}`);
2379
- });
2380
-
2381
- console.log('');
2382
-
2383
- // Prompt for selection
2384
- const answer = await promptUser('Select agent number (or press Enter to cancel): ');
2385
-
2386
- if (!answer) {
2387
- console.log('Cancelled.');
2388
- process.exit(0);
2389
- }
2390
-
2391
- const selection = parseInt(answer, 10);
2392
-
2393
- if (isNaN(selection) || selection < 1 || selection > agents.length) {
2394
- console.error('✗ Invalid selection');
2395
- process.exit(1);
2396
- }
2397
-
2398
- const selectedAgent = agents[selection - 1];
2399
-
2400
- // Save to config
2401
- config.agent_id = selectedAgent.id;
2402
- config.agent_name = selectedAgent.name;
2403
- saveConfig(config);
2404
-
2405
- console.log(`\n✓ Selected agent: ${selectedAgent.name}`);
2406
- console.log(`✓ Config saved to atris/.config`);
2407
- console.log(`\nYou can now use "atris chat" to talk with this agent.`);
2408
- }
2409
-
2410
-
2411
- async function chatAtris() {
2412
- // Get message from command line args
2413
- const message = process.argv.slice(3).join(' ').trim();
2414
-
2415
- // Check atris/ exists
2416
- const targetDir = path.join(process.cwd(), 'atris');
2417
- if (!fs.existsSync(targetDir)) {
2418
- console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
2419
- process.exit(1);
2420
- }
2421
-
2422
- // Check agent selected
2423
- const config = loadConfig();
2424
- if (!config.agent_id) {
2425
- console.error('✗ Error: No agent selected. Run "atris agent" first.');
2426
- process.exit(1);
2427
- }
2428
-
2429
- // Check credentials
2430
- const credentials = loadCredentials();
2431
- if (!credentials || !credentials.token) {
2432
- console.error('✗ Error: Not logged in. Run "atris login" first.');
2433
- process.exit(1);
2434
- }
2435
-
2436
- // If message provided, one-shot mode
2437
- if (message) {
2438
- await chatOnce(config, credentials, message);
2439
- return;
2440
- }
2441
-
2442
- // Otherwise, interactive mode
2443
- await chatInteractive(config, credentials);
2444
- }
2445
-
2446
- async function chatOnce(config, credentials, message) {
2447
- console.log(`\nAgent: ${config.agent_name || config.agent_id}`);
2448
- console.log('');
2449
-
2450
- const agentId = config.agent_id;
2451
- const apiUrl = getApiBaseUrl().replace(/\/api$/, '');
2452
- const endpoint = `${apiUrl}/api/agent/${agentId}/pro-chat`;
2453
-
2454
- const body = JSON.stringify({
2455
- message: message,
2456
- stream: true,
2457
- memory_enabled: true,
2458
- });
2459
-
2460
- try {
2461
- await streamProChat(endpoint, credentials.token, body);
2462
- console.log('\n\n✓ Complete\n');
2463
- } catch (error) {
2464
- console.error(`\n✗ Error: ${error.message || error}`);
2465
- process.exit(1);
2466
- }
2467
- }
1075
+ try {
1076
+ await streamProChat(endpoint, credentials.token, body);
1077
+ console.log('\n\n✓ Complete\n');
1078
+ } catch (error) {
1079
+ console.error(`\n✗ Error: ${error.message || error}`);
1080
+ process.exit(1);
1081
+ }
1082
+ }
2468
1083
 
2469
1084
  async function chatInteractive(config, credentials) {
2470
1085
  return new Promise((resolve) => {
@@ -2678,153 +1293,3 @@ async function atrisDevEntry(userInput = null) {
2678
1293
  console.log('');
2679
1294
  }
2680
1295
 
2681
- function spawnClaudeCodeSession(url, token, body) {
2682
- return new Promise((resolve, reject) => {
2683
- const parsed = new URL(url);
2684
- const isHttps = parsed.protocol === 'https:';
2685
- const transport = isHttps ? https : http;
2686
-
2687
- const requestOptions = {
2688
- method: 'POST',
2689
- hostname: parsed.hostname,
2690
- port: parsed.port || (isHttps ? 443 : 80),
2691
- path: parsed.pathname,
2692
- headers: {
2693
- 'Authorization': `Bearer ${token}`,
2694
- 'Content-Type': 'application/json',
2695
- 'Content-Length': Buffer.byteLength(body),
2696
- },
2697
- };
2698
-
2699
- const req = transport.request(requestOptions, (res) => {
2700
- if (res.statusCode !== 200) {
2701
- const chunks = [];
2702
- res.on('data', (chunk) => chunks.push(chunk));
2703
- res.on('end', () => {
2704
- const text = Buffer.concat(chunks).toString();
2705
- reject(new Error(`HTTP ${res.statusCode}: ${text}`));
2706
- });
2707
- return;
2708
- }
2709
-
2710
- const chunks = [];
2711
- res.on('data', (chunk) => chunks.push(chunk));
2712
- res.on('end', () => {
2713
- try {
2714
- const response = JSON.parse(Buffer.concat(chunks).toString());
2715
- // Session spawned - could return session ID, URL, etc
2716
- resolve(response);
2717
- } catch (e) {
2718
- resolve({ status: 'session_initiated' });
2719
- }
2720
- });
2721
-
2722
- res.on('error', (err) => {
2723
- reject(err);
2724
- });
2725
- });
2726
-
2727
- req.on('error', (err) => {
2728
- reject(err);
2729
- });
2730
-
2731
- req.write(body);
2732
- req.end();
2733
- });
2734
- }
2735
-
2736
- function streamProChat(url, token, body, showTools = false) {
2737
- return new Promise((resolve, reject) => {
2738
- const parsed = new URL(url);
2739
- const isHttps = parsed.protocol === 'https:';
2740
- const transport = isHttps ? https : http;
2741
-
2742
- const requestOptions = {
2743
- method: 'POST',
2744
- hostname: parsed.hostname,
2745
- port: parsed.port || (isHttps ? 443 : 80),
2746
- path: parsed.pathname,
2747
- headers: {
2748
- 'Authorization': `Bearer ${token}`,
2749
- 'Content-Type': 'application/json',
2750
- 'Content-Length': Buffer.byteLength(body),
2751
- 'Accept': 'text/event-stream',
2752
- },
2753
- };
2754
-
2755
- const req = transport.request(requestOptions, (res) => {
2756
- if (res.statusCode !== 200) {
2757
- const chunks = [];
2758
- res.on('data', (chunk) => chunks.push(chunk));
2759
- res.on('end', () => {
2760
- const text = Buffer.concat(chunks).toString();
2761
- reject(new Error(`HTTP ${res.statusCode}: ${text}`));
2762
- });
2763
- return;
2764
- }
2765
-
2766
- let buffer = '';
2767
-
2768
- res.on('data', (chunk) => {
2769
- buffer += chunk.toString();
2770
- const lines = buffer.split('\n');
2771
- buffer = lines.pop() || '';
2772
-
2773
- for (const line of lines) {
2774
- if (line.startsWith('data: ')) {
2775
- const data = line.slice(6).trim();
2776
- if (!data || data === '[DONE]') continue;
2777
-
2778
- try {
2779
- const msg = JSON.parse(data);
2780
-
2781
- // Handle different message types from Claude SDK
2782
- if (msg.type === 'system_init' && showTools) {
2783
- console.log(`[System] Tools available: ${msg.tools?.join(', ') || 'none'}`);
2784
- } else if (msg.type === 'assistant') {
2785
- // Display assistant text response
2786
- if (msg.content && Array.isArray(msg.content)) {
2787
- for (const block of msg.content) {
2788
- if (block.type === 'text') {
2789
- process.stdout.write(block.text);
2790
- }
2791
- }
2792
- }
2793
- } else if (msg.type === 'tool_use' && showTools) {
2794
- console.log(`\n[⚙️ Executing: ${msg.tool_name}]`);
2795
- } else if (msg.type === 'tool_result' && showTools) {
2796
- const preview = msg.content?.substring(0, 100) || '';
2797
- console.log(`[✓ Result]: ${preview}${msg.content?.length > 100 ? '...' : ''}`);
2798
- } else if (msg.type === 'result') {
2799
- // Final result
2800
- if (msg.result) {
2801
- process.stdout.write(msg.result);
2802
- }
2803
- } else if (msg.chunk) {
2804
- // Legacy chunk format
2805
- process.stdout.write(msg.chunk);
2806
- }
2807
- } catch (e) {
2808
- // Ignore parse errors
2809
- }
2810
- }
2811
- }
2812
- });
2813
-
2814
- res.on('end', () => {
2815
- resolve();
2816
- });
2817
-
2818
- res.on('error', (err) => {
2819
- reject(err);
2820
- });
2821
- });
2822
-
2823
- req.on('error', (err) => {
2824
- reject(err);
2825
- });
2826
-
2827
- req.write(body);
2828
- req.end();
2829
- });
2830
- }