atris 1.5.2 → 1.5.4

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/README.md CHANGED
@@ -10,7 +10,7 @@ See [`ENGINEERING_CYCLE.md`](./ENGINEERING_CYCLE.md) for the full vision.
10
10
  atris init → atris brainstorm → atris plan → atris do → atris review → atris launch
11
11
  ```
12
12
 
13
- Each command provides clear instruction prompts for your coding agent (Claude Code, Cursor, etc.).
13
+ Each command provides clear instruction prompts for your coding agent (Cursor, Claude, etc.).
14
14
 
15
15
  ---
16
16
 
@@ -79,7 +79,7 @@ Need a structured prompt for an agent-led ideation session?
79
79
  atris brainstorm
80
80
  ```
81
81
 
82
- Answer a couple quick questions, get a ready-to-send conversation opener for Claude Code (with context + ASCII cue), and optionally log the session summary plus next steps.
82
+ Answer a couple quick questions, get a ready-to-send conversation opener for your coding agent (with context + ASCII cue), and optionally log the session summary plus next steps.
83
83
 
84
84
  ---
85
85
 
@@ -0,0 +1,134 @@
1
+ const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary } = require('../utils/auth');
2
+ const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
3
+
4
+ async function loginAtris() {
5
+
6
+ try {
7
+ console.log('🔐 Login to AtrisOS\n');
8
+
9
+ const existing = loadCredentials();
10
+ if (existing) {
11
+ const label = existing.email || existing.user_id || 'unknown user';
12
+ console.log(`Already logged in as: ${label}`);
13
+ const confirm = await promptUser('Do you want to login again? (y/N): ');
14
+ if (confirm.toLowerCase() !== 'y') {
15
+ console.log('Login cancelled.');
16
+ process.exit(0);
17
+ }
18
+ }
19
+
20
+ console.log('Choose login method:');
21
+ console.log(' 1. Browser OAuth (recommended)');
22
+ console.log(' 2. Paste existing API token');
23
+ console.log(' 3. Cancel');
24
+
25
+ const choice = await promptUser('\nEnter choice (1-3): ');
26
+
27
+ if (choice === '1') {
28
+ const loginUrl = `${getAppBaseUrl()}/auth/cli`;
29
+ console.log('\n🌐 Opening browser for OAuth login…');
30
+ console.log('If it does not open automatically, visit:');
31
+ console.log(loginUrl);
32
+ console.log('\nAfter signing in, copy the CLI code shown in the browser and paste it below.');
33
+ console.log('Codes expire after five minutes.\n');
34
+
35
+ openBrowser(loginUrl);
36
+
37
+ const code = await promptUser('Paste the CLI code here: ');
38
+ if (!code) {
39
+ console.error('✗ Error: Code is required');
40
+ process.exit(1);
41
+ }
42
+
43
+ const exchange = await apiRequestJson('/auth/cli/exchange', {
44
+ method: 'POST',
45
+ body: { code: code.trim() },
46
+ });
47
+
48
+ if (!exchange.ok || !exchange.data) {
49
+ console.error(`✗ Error: ${exchange.error || 'Invalid or expired code'}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ const payload = exchange.data;
54
+ const token = payload.token;
55
+ const refreshToken = payload.refresh_token;
56
+
57
+ if (!token || !refreshToken) {
58
+ console.error('✗ Error: Backend did not return tokens. Please try again.');
59
+ process.exit(1);
60
+ }
61
+
62
+ const email = payload.email || existing?.email || null;
63
+ const userId = payload.user_id || existing?.user_id || null;
64
+ const provider = payload.provider || 'atris';
65
+
66
+ saveCredentials(token, refreshToken, email, userId, provider);
67
+ console.log('\n✓ Successfully logged in!');
68
+ await displayAccountSummary(apiRequestJson);
69
+ console.log('\nYou can now use cloud features with atris commands.');
70
+ process.exit(0);
71
+ } else if (choice === '2') {
72
+ console.log('\n📋 Manual Token Entry');
73
+ console.log('Get your token from: https://app.atris.ai/settings/api\n');
74
+
75
+ const tokenInput = await promptUser('Paste your API token: ');
76
+
77
+ if (!tokenInput) {
78
+ console.error('✗ Error: Token is required');
79
+ process.exit(1);
80
+ }
81
+
82
+ const trimmed = tokenInput.trim();
83
+ saveCredentials(trimmed, null, existing?.email || null, existing?.user_id || null, existing?.provider || 'manual');
84
+ console.log('\nAttempting to validate token…\n');
85
+
86
+ const summary = await displayAccountSummary(apiRequestJson);
87
+ if (summary.error) {
88
+ console.log('\n⚠️ Token saved, but validation failed. You may need to relogin.');
89
+ } else {
90
+ console.log('\n✓ Token validated successfully.');
91
+ }
92
+
93
+ console.log('\nYou can now use cloud features with atris commands.');
94
+ process.exit(0);
95
+ } else {
96
+ console.log('Login cancelled.');
97
+ process.exit(0);
98
+ }
99
+ } catch (error) {
100
+ console.error(`\n✗ Login failed: ${error.message || error}`);
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ function logoutAtris() {
106
+ const credentials = loadCredentials();
107
+
108
+ if (!credentials) {
109
+ console.log('Not currently logged in.');
110
+ process.exit(0);
111
+ }
112
+
113
+ deleteCredentials();
114
+ console.log('✓ Successfully logged out');
115
+ console.log(`✓ Removed credentials from ${getCredentialsPath()}`);
116
+ }
117
+
118
+ async function whoamiAtris() {
119
+ const { apiRequestJson } = require('../utils/api');
120
+
121
+ try {
122
+ const summary = await displayAccountSummary(apiRequestJson);
123
+ if (summary.error) {
124
+ console.log('\nRun "atris login" to authenticate with AtrisOS.');
125
+ process.exit(1);
126
+ }
127
+ process.exit(0);
128
+ } catch (error) {
129
+ console.error(`✗ Failed to fetch account details: ${error.message || error}`);
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ module.exports = { loginAtris, logoutAtris, whoamiAtris };
@@ -0,0 +1,169 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getLogPath, ensureLogDirectory, createLogFile, parseInboxItems, addInboxIdea, removeInboxItemFromContent, recordBrainstormSession } = require('../lib/file-ops');
4
+ const { loadConfig } = require('../utils/config');
5
+ const { loadCredentials } = require('../utils/auth');
6
+ const { apiRequestJson } = require('../utils/api');
7
+
8
+ function brainstormAbortError() {
9
+ const error = new Error('Brainstorm cancelled by user.');
10
+ error.__brainstormAbort = true;
11
+ return error;
12
+ }
13
+
14
+ async function brainstormAtris() {
15
+ // Check for args: atris brainstorm "topic" --outcome "..." --vibe "..." --constraints "..."
16
+ const args = process.argv.slice(3);
17
+ let topicArg = null;
18
+ let outcomeArg = null;
19
+ let vibeArg = null;
20
+ let constraintsArg = null;
21
+
22
+ for (let i = 0; i < args.length; i++) {
23
+ if (args[i] === '--outcome' && args[i + 1]) {
24
+ outcomeArg = args[i + 1];
25
+ i++;
26
+ } else if (args[i] === '--vibe' && args[i + 1]) {
27
+ vibeArg = args[i + 1];
28
+ i++;
29
+ } else if (args[i] === '--constraints' && args[i + 1]) {
30
+ constraintsArg = args[i + 1];
31
+ i++;
32
+ } else if (!args[i].startsWith('--') && !topicArg) {
33
+ topicArg = args[i];
34
+ }
35
+ }
36
+ const targetDir = path.join(process.cwd(), 'atris');
37
+ if (!fs.existsSync(targetDir)) {
38
+ throw new Error('atris/ folder not found. Run "atris init" first.');
39
+ }
40
+
41
+ ensureLogDirectory();
42
+ const { logFile, dateFormatted } = getLogPath();
43
+ if (!fs.existsSync(logFile)) {
44
+ createLogFile(logFile, dateFormatted);
45
+ }
46
+
47
+ // Show current stats for game-like feel
48
+ let todayStats = { completions: 0, inbox: 0 };
49
+ if (fs.existsSync(logFile)) {
50
+ const logContent = fs.readFileSync(logFile, 'utf8');
51
+ const completionMatches = logContent.match(/- \*\*C\d+:/g);
52
+ todayStats.completions = completionMatches ? completionMatches.length : 0;
53
+ const inboxMatch = logContent.match(/## Inbox\n([\s\S]*?)(?=\n##|---)/);
54
+ if (inboxMatch && inboxMatch[1].trim()) {
55
+ const inboxMatches = inboxMatch[1].match(/- \*\*I\d+:/g);
56
+ todayStats.inbox = inboxMatches ? inboxMatches.length : 0;
57
+ }
58
+ }
59
+
60
+ console.log('');
61
+ console.log('┌─────────────────────────────────────────────────────────────┐');
62
+ console.log('│ ATRIS Brainstorm — structured prompt generator │');
63
+ console.log('└─────────────────────────────────────────────────────────────┘');
64
+ console.log('');
65
+ console.log(`📅 Date: ${dateFormatted}`);
66
+ console.log(`📊 Today: ${todayStats.completions} completions | ${todayStats.inbox} inbox items`);
67
+ console.log('💡 Type "exit" at any prompt to cancel.');
68
+ console.log('');
69
+
70
+ // Try to fetch latest journal entry from backend (optional)
71
+ let journalContext = '';
72
+ const config = loadConfig();
73
+ const credentials = loadCredentials();
74
+
75
+ if (config.agent_id && credentials && credentials.token) {
76
+ try {
77
+ console.log('📖 Fetching latest journal entry from AtrisOS...');
78
+ const journalResult = await apiRequestJson(`/agents/${config.agent_id}/journal/today`, {
79
+ method: 'GET',
80
+ token: credentials.token,
81
+ });
82
+
83
+ if (journalResult.ok && journalResult.data?.content) {
84
+ journalContext = journalResult.data.content;
85
+ console.log('✓ Loaded journal entry from backend');
86
+ } else {
87
+ const listResult = await apiRequestJson(`/agents/${config.agent_id}/journal/?limit=1`, {
88
+ method: 'GET',
89
+ token: credentials.token,
90
+ });
91
+
92
+ if (listResult.ok && listResult.data?.entries?.length > 0) {
93
+ journalContext = listResult.data.entries[0].content || '';
94
+ console.log('✓ Loaded latest journal entry from backend');
95
+ }
96
+ }
97
+ } catch (error) {
98
+ console.log('ℹ️ Using local journal file (backend unavailable)');
99
+ }
100
+ console.log('');
101
+ }
102
+
103
+ // Fallback to local log file if no backend context
104
+ if (!journalContext) {
105
+ if (fs.existsSync(logFile)) {
106
+ journalContext = fs.readFileSync(logFile, 'utf8');
107
+ }
108
+ }
109
+
110
+ // Determine topic and inputs
111
+ let selectedInboxItem = null;
112
+ let topicSummary = topicArg || '';
113
+ let userStory = outcomeArg || '';
114
+ let feelingsVibe = vibeArg || '';
115
+ let constraints = constraintsArg || '';
116
+
117
+ // If no args provided, use inbox or prompt user
118
+ if (!topicArg) {
119
+ const initialContent = journalContext || (fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf8') : '');
120
+ let inboxItems = parseInboxItems(initialContent);
121
+
122
+ if (inboxItems.length > 0) {
123
+ // Use first inbox item if available
124
+ selectedInboxItem = inboxItems[0];
125
+ topicSummary = selectedInboxItem.text;
126
+ console.log(`📌 Using inbox item I${selectedInboxItem.id}: ${topicSummary}`);
127
+ } else {
128
+ throw new Error('No topic provided. Usage: atris brainstorm "topic" [--outcome "..."] [--vibe "..."] [--constraints "..."]');
129
+ }
130
+ } else {
131
+ // Add new idea to inbox
132
+ const newId = addInboxIdea(logFile, topicSummary);
133
+ selectedInboxItem = { id: newId, text: topicSummary };
134
+ console.log(`✓ Added I${newId} to today's Inbox.`);
135
+ }
136
+
137
+ const sourceLabel = selectedInboxItem ? `I${selectedInboxItem.id}` : 'Ad-hoc';
138
+
139
+ // Build the brainstorm message (structured for AI to read)
140
+ const brainstormMessage = [
141
+ `I want to brainstorm: ${topicSummary}`,
142
+ '',
143
+ userStory ? `The outcome should be: ${userStory}` : '',
144
+ feelingsVibe ? `Vibe we're going for: ${feelingsVibe}` : '',
145
+ journalContext ? `Recent context from journal:\n${journalContext.substring(0, 500)}${journalContext.length > 500 ? '...' : ''}` : '',
146
+ constraints ? `Constraints: ${constraints}` : '',
147
+ '',
148
+ 'Help me uncover what we need to build. Keep responses short (4-5 sentences), pause for alignment, sketch ASCII when structure helps.',
149
+ ].filter(line => line !== '').join('\n');
150
+
151
+ console.log('');
152
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
153
+ console.log('🧠 BRAINSTORM SESSION READY');
154
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
155
+ console.log('');
156
+ console.log('📝 Instructions for your coding agent:');
157
+ console.log('');
158
+ console.log(brainstormMessage);
159
+ console.log('');
160
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
161
+ console.log('');
162
+ console.log('💡 Your coding agent (in Cursor/IDE) should read the above and');
163
+ console.log(' start brainstorming with you here in the chat interface.');
164
+ console.log(' Keep it conversational, short responses, ASCII diagrams when helpful.');
165
+ console.log('');
166
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
167
+ }
168
+
169
+ module.exports = { brainstormAtris };
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function initAtris() {
5
+ const targetDir = path.join(process.cwd(), 'atris');
6
+ const agentTeamDir = path.join(targetDir, 'agent_team');
7
+ const sourceFile = path.join(__dirname, '..', 'atris.md');
8
+ const targetFile = path.join(targetDir, 'atris.md');
9
+
10
+ if (!fs.existsSync(targetDir)) {
11
+ fs.mkdirSync(targetDir, { recursive: true });
12
+ console.log('✓ Created atris/ folder');
13
+ } else {
14
+ console.log('✓ atris/ folder already exists');
15
+ }
16
+
17
+ if (!fs.existsSync(agentTeamDir)) {
18
+ fs.mkdirSync(agentTeamDir, { recursive: true });
19
+ console.log('✓ Created atris/agent_team/ folder');
20
+ }
21
+
22
+ const gettingStartedFile = path.join(targetDir, 'GETTING_STARTED.md');
23
+ const personaFile = path.join(targetDir, 'PERSONA.md');
24
+ const mapFile = path.join(targetDir, 'MAP.md');
25
+ const taskContextsFile = path.join(targetDir, 'TASK_CONTEXTS.md');
26
+ const navigatorFile = path.join(agentTeamDir, 'navigator.md');
27
+ const executorFile = path.join(agentTeamDir, 'executor.md');
28
+ const validatorFile = path.join(agentTeamDir, 'validator.md');
29
+ const launcherFile = path.join(agentTeamDir, 'launcher.md');
30
+
31
+ const gettingStartedSource = path.join(__dirname, '..', 'GETTING_STARTED.md');
32
+ const personaSource = path.join(__dirname, '..', 'PERSONA.md');
33
+
34
+ if (!fs.existsSync(gettingStartedFile) && fs.existsSync(gettingStartedSource)) {
35
+ fs.copyFileSync(gettingStartedSource, gettingStartedFile);
36
+ console.log('✓ Created GETTING_STARTED.md');
37
+ }
38
+
39
+ if (!fs.existsSync(personaFile) && fs.existsSync(personaSource)) {
40
+ fs.copyFileSync(personaSource, personaFile);
41
+ console.log('✓ Created PERSONA.md');
42
+ }
43
+
44
+ if (!fs.existsSync(mapFile)) {
45
+ 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');
46
+ console.log('✓ Created MAP.md placeholder');
47
+ }
48
+
49
+ if (!fs.existsSync(taskContextsFile)) {
50
+ 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');
51
+ console.log('✓ Created TASK_CONTEXTS.md placeholder');
52
+ }
53
+
54
+ const navigatorSource = path.join(__dirname, '..', 'atris', 'agent_team', 'navigator.md');
55
+ const executorSource = path.join(__dirname, '..', 'atris', 'agent_team', 'executor.md');
56
+ const validatorSource = path.join(__dirname, '..', 'atris', 'agent_team', 'validator.md');
57
+ const launcherSource = path.join(__dirname, '..', 'atris', 'agent_team', 'launcher.md');
58
+
59
+ if (!fs.existsSync(navigatorFile) && fs.existsSync(navigatorSource)) {
60
+ fs.copyFileSync(navigatorSource, navigatorFile);
61
+ console.log('✓ Created agent_team/navigator.md');
62
+ }
63
+
64
+ if (!fs.existsSync(executorFile) && fs.existsSync(executorSource)) {
65
+ fs.copyFileSync(executorSource, executorFile);
66
+ console.log('✓ Created agent_team/executor.md');
67
+ }
68
+
69
+ if (!fs.existsSync(validatorFile) && fs.existsSync(validatorSource)) {
70
+ fs.copyFileSync(validatorSource, validatorFile);
71
+ console.log('✓ Created agent_team/validator.md');
72
+ }
73
+
74
+ if (!fs.existsSync(launcherFile) && fs.existsSync(launcherSource)) {
75
+ fs.copyFileSync(launcherSource, launcherFile);
76
+ console.log('✓ Created agent_team/launcher.md');
77
+ }
78
+
79
+ if (fs.existsSync(sourceFile)) {
80
+ fs.copyFileSync(sourceFile, targetFile);
81
+ console.log('✓ Copied atris.md to atris/ folder');
82
+ console.log('\n✨ ATRIS initialized! Full structure created:');
83
+ console.log(' atris/');
84
+ console.log(' ├── GETTING_STARTED.md (read this first!)');
85
+ console.log(' ├── PERSONA.md (agent personality)');
86
+ console.log(' ├── atris.md (AI agent instructions)');
87
+ console.log(' ├── MAP.md (placeholder)');
88
+ console.log(' ├── TASK_CONTEXTS.md (placeholder)');
89
+ console.log(' └── agent_team/');
90
+ console.log(' ├── navigator.md (placeholder)');
91
+ console.log(' ├── executor.md (placeholder)');
92
+ console.log(' ├── validator.md (placeholder)');
93
+ console.log(' └── launcher.md (placeholder)');
94
+ console.log('\nNext steps:');
95
+ console.log('1. Read atris/GETTING_STARTED.md for the full guide');
96
+ console.log('2. Open atris/atris.md and paste it to your AI agent');
97
+ console.log('3. Your agent will populate all placeholder files in ~10 mins');
98
+ } else {
99
+ console.error('✗ Error: atris.md not found in package');
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ module.exports = { initAtris };
@@ -0,0 +1,261 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/file-ops');
4
+ const { parseJournalSections, mergeSections, reconstructJournal, showLogDiff } = require('../lib/journal');
5
+ const { computeContentHash, isSameTimestamp } = require('../lib/journal');
6
+ const { loadConfig, loadLogSyncState, saveLogSyncState } = require('../utils/config');
7
+ const { ensureValidCredentials } = require('../utils/auth');
8
+ const { apiRequestJson } = require('../utils/api');
9
+ const { promptUser } = require('../utils/auth');
10
+
11
+ async function logSyncAtris() {
12
+ const targetDir = path.join(process.cwd(), 'atris');
13
+
14
+ if (!fs.existsSync(targetDir)) {
15
+ throw new Error('atris/ folder not found. Run "atris init" first.');
16
+ }
17
+
18
+ let dateArg = process.argv[4];
19
+ if (dateArg && dateArg.startsWith('--date=')) {
20
+ dateArg = dateArg.split('=')[1];
21
+ }
22
+
23
+ let { logsDir, yearDir, logFile, dateFormatted } = getLogPath(dateArg);
24
+ if (Number.isNaN(new Date(dateFormatted).getTime())) {
25
+ throw new Error(`Invalid date provided: ${dateArg}`);
26
+ }
27
+
28
+ if (!fs.existsSync(logsDir)) {
29
+ fs.mkdirSync(logsDir, { recursive: true });
30
+ }
31
+ if (!fs.existsSync(yearDir)) {
32
+ fs.mkdirSync(yearDir, { recursive: true });
33
+ }
34
+ if (!fs.existsSync(logFile)) {
35
+ createLogFile(logFile, dateFormatted);
36
+ console.log(`Created local log template for ${dateFormatted}. Fill it in before syncing.`);
37
+ }
38
+
39
+ const localContent = fs.readFileSync(logFile, 'utf8');
40
+ const localHash = computeContentHash(localContent);
41
+
42
+ const config = loadConfig();
43
+ if (!config.agent_id) {
44
+ throw new Error('No agent selected. Run "atris agent" first.');
45
+ }
46
+
47
+ const ensured = await ensureValidCredentials(apiRequestJson);
48
+ if (ensured.error) {
49
+ if (ensured.error === 'not_logged_in') {
50
+ throw new Error('Not logged in. Run "atris login" first.');
51
+ }
52
+ if (ensured.detail && ensured.detail.toLowerCase().includes('enotfound')) {
53
+ throw new Error('Unable to reach Atris API. Check your network connection.');
54
+ }
55
+ throw new Error(ensured.detail || ensured.error || 'Authentication failed');
56
+ }
57
+
58
+ const credentials = ensured.credentials;
59
+ const agentId = config.agent_id;
60
+ const agentLabel = config.agent_name || agentId;
61
+
62
+ console.log(`🔄 Syncing log for ${dateFormatted} with agent "${agentLabel}"`);
63
+
64
+ const syncState = loadLogSyncState();
65
+ const knownRemoteUpdate = syncState[dateFormatted]?.updated_at || null;
66
+ const knownRemoteHash = syncState[dateFormatted]?.hash || null;
67
+
68
+ let remoteExists = false;
69
+ let remoteUpdatedAt = null;
70
+ let remoteContent = null;
71
+ let remoteHash = null;
72
+ const existing = await apiRequestJson(`/agents/${agentId}/journal/${dateFormatted}`, {
73
+ method: 'GET',
74
+ token: credentials.token,
75
+ });
76
+
77
+ if (existing.ok) {
78
+ remoteExists = true;
79
+ remoteUpdatedAt = existing.data?.updated_at || existing.data?.created_at || null;
80
+ remoteContent = typeof existing.data?.content === 'string' ? existing.data.content : null;
81
+ remoteHash = remoteContent ? computeContentHash(remoteContent) : null;
82
+
83
+ if (remoteUpdatedAt) {
84
+ const localStats = fs.statSync(logFile);
85
+ const localModified = localStats.mtime.toISOString();
86
+ const remoteTime = new Date(remoteUpdatedAt).getTime();
87
+ const localTime = new Date(localModified).getTime();
88
+
89
+ const remoteMatchesKnown = (knownRemoteUpdate && isSameTimestamp(remoteUpdatedAt, knownRemoteUpdate))
90
+ || (remoteHash && knownRemoteHash && remoteHash === knownRemoteHash);
91
+
92
+ if (remoteTime > localTime && !remoteMatchesKnown) {
93
+ const normalizedRemote = remoteContent ? remoteContent.replace(/\r\n/g, '\n') : null;
94
+ const normalizedLocal = localContent.replace(/\r\n/g, '\n');
95
+ if (normalizedRemote !== null && normalizedRemote.trim() === normalizedLocal.trim()) {
96
+ const remoteDate = new Date(remoteUpdatedAt);
97
+ if (!Number.isNaN(remoteDate.getTime())) {
98
+ fs.utimesSync(logFile, remoteDate, remoteDate);
99
+ const state = loadLogSyncState();
100
+ state[dateFormatted] = {
101
+ updated_at: remoteUpdatedAt,
102
+ hash: remoteHash || knownRemoteHash || computeContentHash(remoteContent || ''),
103
+ };
104
+ saveLogSyncState(state);
105
+ }
106
+ console.log('✓ Already synced (timestamps aligned with web)');
107
+ return;
108
+ }
109
+
110
+ try {
111
+ const localSections = parseJournalSections(normalizedLocal);
112
+ const remoteSections = parseJournalSections(normalizedRemote || '');
113
+ const { merged, conflicts } = mergeSections(localSections, remoteSections, knownRemoteHash);
114
+
115
+ if (conflicts.length === 0) {
116
+ const mergedContent = reconstructJournal(merged);
117
+ fs.writeFileSync(logFile, mergedContent, 'utf8');
118
+ console.log('✓ Auto-merged web and local changes');
119
+ console.log(` Merged sections: ${Object.keys(merged).filter(k => k !== '__header__').join(', ')}`);
120
+ localContent = mergedContent;
121
+ } else {
122
+ console.log('⚠️ Conflicting changes in same section(s)');
123
+ console.log(` Conflicts: ${conflicts.join(', ')}`);
124
+ console.log(` Remote updated: ${remoteUpdatedAt}`);
125
+ console.log(` Local modified: ${localModified}`);
126
+ console.log(' Type "y" to replace local with web version, or "n" to keep local changes.');
127
+ console.log('');
128
+
129
+ if (typeof remoteContent === 'string') {
130
+ showLogDiff(logFile, remoteContent);
131
+ }
132
+
133
+ const answer = await promptUser('Overwrite local with web version? (y/n): ');
134
+
135
+ if (answer && answer.toLowerCase() === 'y') {
136
+ const pulledContent = existing.data?.content || '';
137
+ fs.writeFileSync(logFile, pulledContent, 'utf8');
138
+ remoteHash = computeContentHash(pulledContent);
139
+ console.log('✓ Local journal updated from web');
140
+ console.log(`🗒️ File: ${path.relative(process.cwd(), logFile)}`);
141
+ if (remoteUpdatedAt) {
142
+ const remoteDate = new Date(remoteUpdatedAt);
143
+ if (!Number.isNaN(remoteDate.getTime())) {
144
+ fs.utimesSync(logFile, remoteDate, remoteDate);
145
+ }
146
+ const state = loadLogSyncState();
147
+ state[dateFormatted] = {
148
+ updated_at: remoteUpdatedAt,
149
+ hash: remoteHash || computeContentHash(pulledContent),
150
+ };
151
+ saveLogSyncState(state);
152
+ }
153
+ return;
154
+ } else {
155
+ console.log('⏩ Keeping local version, will push to web');
156
+ }
157
+ }
158
+ } catch (parseError) {
159
+ console.log('⚠️ Web version is newer than local version');
160
+ console.log(` Remote updated: ${remoteUpdatedAt}`);
161
+ console.log(` Local modified: ${localModified}`);
162
+ 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.');
163
+ console.log('');
164
+
165
+ if (typeof remoteContent === 'string') {
166
+ showLogDiff(logFile, remoteContent);
167
+ }
168
+
169
+ const answer = await promptUser('Overwrite local with web version? (y/n): ');
170
+
171
+ if (answer && answer.toLowerCase() === 'y') {
172
+ const pulledContent = existing.data?.content || '';
173
+ fs.writeFileSync(logFile, pulledContent, 'utf8');
174
+ remoteHash = computeContentHash(pulledContent);
175
+ console.log('✓ Local journal updated from web');
176
+ console.log(`🗒️ File: ${path.relative(process.cwd(), logFile)}`);
177
+ if (remoteUpdatedAt) {
178
+ const remoteDate = new Date(remoteUpdatedAt);
179
+ if (!Number.isNaN(remoteDate.getTime())) {
180
+ fs.utimesSync(logFile, remoteDate, remoteDate);
181
+ }
182
+ const state = loadLogSyncState();
183
+ state[dateFormatted] = {
184
+ updated_at: remoteUpdatedAt,
185
+ hash: remoteHash || computeContentHash(pulledContent),
186
+ };
187
+ saveLogSyncState(state);
188
+ }
189
+ return;
190
+ } else {
191
+ console.log('⏩ Keeping local version, will push to web');
192
+ }
193
+ }
194
+ } else if (remoteTime > localTime && remoteMatchesKnown) {
195
+ console.log('⚠️ Web timestamp ahead due to clock skew (matches last sync); pushing local changes.');
196
+ } else if (remoteTime === localTime) {
197
+ console.log('✓ Already synced (local and web are identical)');
198
+ if (remoteUpdatedAt) {
199
+ const state = loadLogSyncState();
200
+ state[dateFormatted] = {
201
+ updated_at: remoteUpdatedAt,
202
+ hash: remoteHash || knownRemoteHash || computeContentHash(remoteContent || ''),
203
+ };
204
+ saveLogSyncState(state);
205
+ }
206
+ return;
207
+ }
208
+ }
209
+ } else if (!existing.status) {
210
+ throw new Error('Unable to reach Atris API. Check your network connection.');
211
+ } else if (existing.status && existing.status !== 404) {
212
+ throw new Error(existing.error || 'Failed to check existing journal entry');
213
+ }
214
+
215
+ const payload = {
216
+ content: localContent,
217
+ metadata: {
218
+ source: 'cli',
219
+ local_path: `logs/${dateFormatted}.md`,
220
+ },
221
+ };
222
+
223
+ const result = await apiRequestJson(`/agents/${agentId}/journal/${dateFormatted}`, {
224
+ method: 'PUT',
225
+ token: credentials.token,
226
+ body: payload,
227
+ });
228
+
229
+ if (!result.ok) {
230
+ if (!result.status) {
231
+ throw new Error('Unable to reach Atris API. Check your network connection.');
232
+ }
233
+ throw new Error(result.error || 'Failed to sync journal entry');
234
+ }
235
+
236
+ const data = result.data || {};
237
+ const updatedAt = data.updated_at || new Date().toISOString();
238
+
239
+ if (remoteExists) {
240
+ console.log(`✓ Updated journal entry (previous update: ${remoteUpdatedAt || 'unknown'})`);
241
+ } else {
242
+ console.log('✓ Created journal entry in Atris');
243
+ }
244
+
245
+ console.log(`🗒️ Local file: ${path.relative(process.cwd(), logFile)}`);
246
+ console.log(`🕒 Updated at: ${updatedAt}`);
247
+ const updatedDate = new Date(updatedAt);
248
+ if (!Number.isNaN(updatedDate.getTime())) {
249
+ fs.utimesSync(logFile, updatedDate, updatedDate);
250
+ }
251
+ const finalContent = fs.readFileSync(logFile, 'utf8');
252
+ const finalHash = computeContentHash(finalContent);
253
+ const finalState = loadLogSyncState();
254
+ finalState[dateFormatted] = {
255
+ updated_at: updatedAt,
256
+ hash: finalHash,
257
+ };
258
+ saveLogSyncState(finalState);
259
+ }
260
+
261
+ module.exports = { logSyncAtris };