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/atris/AGENTS.md +1 -2
- package/atris/atris.md +40 -6
- package/atris/features/_templates/changelog.md.template +11 -0
- package/atris/skills/clawhub/chief-of-staff/SKILL.md +65 -0
- package/atris/skills/clawhub/member-runtime/SKILL.md +112 -0
- package/atris/skills/create-app/SKILL.md +278 -0
- package/atris/skills/drive/SKILL.md +20 -0
- package/atris/skills/github/SKILL.md +207 -0
- package/atris/skills/youtube/SKILL.md +207 -0
- package/atris/team/brainstormer/MEMBER.md +23 -0
- package/atris/team/executor/MEMBER.md +29 -0
- package/atris/team/launcher/MEMBER.md +24 -0
- package/atris/team/navigator/MEMBER.md +28 -11
- package/atris/team/navigator/journal/2026-02-23.md +6 -0
- package/atris/team/researcher/MEMBER.md +21 -0
- package/atris/team/validator/MEMBER.md +29 -8
- package/bin/atris.js +241 -1776
- package/commands/analytics.js +2 -2
- package/commands/brainstorm.js +1 -2
- package/commands/member.js +194 -8
- package/commands/status.js +128 -143
- package/commands/sync.js +4 -4
- package/commands/workflow.js +36 -33
- package/lib/state-detection.js +15 -4
- package/lib/todo.js +184 -0
- package/package.json +1 -1
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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
60
|
-
|
|
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
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
127
|
+
let files;
|
|
128
|
+
try { files = fs.readdirSync(dir); } catch { return; }
|
|
128
129
|
for (const file of files) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
//
|
|
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 {
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
702
|
+
require('../commands/version').showVersion();
|
|
682
703
|
} else if (command === 'login') {
|
|
683
|
-
|
|
704
|
+
require('../commands/auth').loginAtris();
|
|
684
705
|
} else if (command === 'logout') {
|
|
685
|
-
|
|
706
|
+
require('../commands/auth').logoutAtris();
|
|
686
707
|
} else if (command === 'whoami') {
|
|
687
|
-
|
|
708
|
+
require('../commands/auth').whoamiAtris();
|
|
688
709
|
} else if (command === 'switch') {
|
|
689
|
-
|
|
710
|
+
require('../commands/auth').switchAccount();
|
|
690
711
|
} else if (command === 'accounts') {
|
|
691
|
-
|
|
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
|
-
|
|
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 =
|
|
745
|
+
promise = require('../commands/autopilot').autopilotFromTodo(options);
|
|
725
746
|
} else if (description) {
|
|
726
|
-
promise =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
821
|
+
require('../commands/clean').cleanAtris({ dryRun });
|
|
801
822
|
} else if (command === 'verify') {
|
|
802
823
|
const taskId = process.argv[3] || null;
|
|
803
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1184
|
-
|
|
1185
|
-
const { logFile, dateFormatted } = getLogPath();
|
|
951
|
+
// Check if logged in
|
|
952
|
+
const credentials = loadCredentials();
|
|
1186
953
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
969
|
+
if (!result.ok) {
|
|
970
|
+
console.error(`✗ Error: ${result.error || 'Failed to fetch agents'}`);
|
|
971
|
+
process.exit(1);
|
|
972
|
+
}
|
|
1205
973
|
|
|
1206
|
-
|
|
1207
|
-
const input = line.trim();
|
|
974
|
+
const agents = result.data?.my_agents || [];
|
|
1208
975
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1229
|
-
ensureLogDirectory();
|
|
1230
|
-
const { logFile, dateFormatted } = getLogPath();
|
|
1006
|
+
const selection = parseInt(answer, 10);
|
|
1231
1007
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1243
|
-
console.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
|
-
|
|
1033
|
+
console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
|
|
1034
|
+
process.exit(1);
|
|
1251
1035
|
}
|
|
1252
1036
|
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
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
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
//
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
-
}
|