atris 3.2.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +65 -131
- package/README.md +18 -2
- package/atris/GETTING_STARTED.md +65 -131
- package/atris/PERSONA.md +5 -1
- package/atris/atris.md +122 -153
- package/atris/skills/aeo/SKILL.md +117 -0
- package/atris/skills/atris/SKILL.md +49 -25
- package/atris/skills/create-member/SKILL.md +29 -9
- package/atris/skills/endgame/SKILL.md +9 -0
- package/atris/skills/research-search/SKILL.md +167 -0
- package/atris/skills/research-search/arxiv_search.py +157 -0
- package/atris/skills/research-search/program.md +48 -0
- package/atris/skills/research-search/results.tsv +6 -0
- package/atris/skills/research-search/scholar_search.py +154 -0
- package/atris/skills/tidy/SKILL.md +36 -21
- package/atris/team/_template/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +35 -1
- package/atris.md +118 -178
- package/bin/atris.js +30 -5
- package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
- package/cli/atris_code.py +889 -0
- package/cli/runtime_guard.py +693 -0
- package/commands/align.js +15 -0
- package/commands/app.js +316 -0
- package/commands/autopilot.js +390 -7
- package/commands/business.js +677 -2
- package/commands/computer.js +1979 -43
- package/commands/context-sync.js +5 -0
- package/commands/lifecycle.js +12 -0
- package/commands/plugin.js +24 -0
- package/commands/pull.js +40 -1
- package/commands/push.js +44 -0
- package/commands/serve.js +1 -0
- package/commands/sync.js +272 -76
- package/commands/verify.js +50 -1
- package/commands/wiki.js +27 -2
- package/lib/file-ops.js +13 -1
- package/lib/journal.js +23 -0
- package/lib/scorecard.js +42 -4
- package/lib/sync-telemetry.js +59 -0
- package/lib/todo.js +6 -0
- package/lib/wiki.js +150 -6
- package/package.json +2 -1
- package/utils/api.js +19 -0
- package/utils/auth.js +25 -1
- package/utils/config.js +24 -0
- package/utils/update-check.js +16 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Atris sync telemetry — emit one event per `atris push` / `atris pull`.
|
|
2
|
+
//
|
|
3
|
+
// Why awaited (not fire-and-forget): push.js / pull.js call process.exit(1)
|
|
4
|
+
// on most failure paths. A fire-and-forget POST gets killed mid-flight and
|
|
5
|
+
// the RL loop loses signals on the exact failures it most needs to learn.
|
|
6
|
+
//
|
|
7
|
+
// Best-effort: any error swallowed silently — telemetry never blocks UX.
|
|
8
|
+
// Hard 2s timeout so a flaky control plane can't slow real operations.
|
|
9
|
+
|
|
10
|
+
const { apiRequestJson } = require('../utils/api');
|
|
11
|
+
|
|
12
|
+
let _cachedVersion = null;
|
|
13
|
+
function cliVersion() {
|
|
14
|
+
if (_cachedVersion) return _cachedVersion;
|
|
15
|
+
try {
|
|
16
|
+
_cachedVersion = require('../package.json').version || 'unknown';
|
|
17
|
+
} catch {
|
|
18
|
+
_cachedVersion = 'unknown';
|
|
19
|
+
}
|
|
20
|
+
return _cachedVersion;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function emitSyncEvent(token, businessId, workspaceId, op, outcome, latencyMs, extras = {}) {
|
|
24
|
+
if (!token || !businessId || !workspaceId || !op || !outcome) return false;
|
|
25
|
+
const event = {
|
|
26
|
+
business_id: businessId,
|
|
27
|
+
workspace_id: workspaceId,
|
|
28
|
+
op,
|
|
29
|
+
outcome,
|
|
30
|
+
latency_ms: Math.max(0, Math.round(latencyMs || 0)),
|
|
31
|
+
bytes_transferred: extras.bytes_transferred || 0,
|
|
32
|
+
bytes_changed: extras.bytes_changed || 0,
|
|
33
|
+
files_pushed: extras.files_pushed || 0,
|
|
34
|
+
files_deleted: extras.files_deleted || 0,
|
|
35
|
+
files_unchanged: extras.files_unchanged || 0,
|
|
36
|
+
cli_version: cliVersion(),
|
|
37
|
+
};
|
|
38
|
+
if (extras.error_detail) event.error_detail = String(extras.error_detail).slice(0, 500);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await apiRequestJson('/atris-sync/telemetry', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
token,
|
|
44
|
+
body: event,
|
|
45
|
+
timeoutMs: 2000,
|
|
46
|
+
retries: 0,
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function startTimer() {
|
|
55
|
+
const t0 = Date.now();
|
|
56
|
+
return () => Date.now() - t0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { emitSyncEvent, startTimer };
|
package/lib/todo.js
CHANGED
|
@@ -17,6 +17,12 @@ function parseTodo(todoPath) {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parse a specific section from TODO.md content into task objects.
|
|
22
|
+
* @param {string} content - Full TODO.md content
|
|
23
|
+
* @param {string} sectionName - Section name to extract (e.g., 'Backlog')
|
|
24
|
+
* @returns {Array} Array of parsed task objects
|
|
25
|
+
*/
|
|
20
26
|
function parseSection(content, sectionName) {
|
|
21
27
|
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
28
|
const match = content.match(new RegExp(`##\\s+${escaped}\\n([\\s\\S]*?)(?=\\n##|$)`, 'i'));
|
package/lib/wiki.js
CHANGED
|
@@ -2,7 +2,9 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const WIKI_ROOT = 'atris/wiki';
|
|
5
|
+
const CONTEXT_ROOT = 'atris/context';
|
|
5
6
|
const PRIVATE_WIKI_ROOT = '.atris/presidio';
|
|
7
|
+
const PRIVATE_CONTEXT_ROOT = `${PRIVATE_WIKI_ROOT}/context`;
|
|
6
8
|
const LEGACY_WIKI_ROOT = 'wiki';
|
|
7
9
|
const WIKI_BRIEFS_SUBDIR = 'briefs';
|
|
8
10
|
const LEGACY_WIKI_BRIEFS_SUBDIR = 'syntheses';
|
|
@@ -14,6 +16,10 @@ function getWikiRoot(mode = 'public') {
|
|
|
14
16
|
return mode === 'private' ? PRIVATE_WIKI_ROOT : WIKI_ROOT;
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
function getContextRoot(mode = 'public') {
|
|
20
|
+
return mode === 'private' ? PRIVATE_CONTEXT_ROOT : CONTEXT_ROOT;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
function getWikiLinkRoot(mode = 'public') {
|
|
18
24
|
return mode === 'private' ? PRIVATE_WIKI_ROOT : 'atris/wiki';
|
|
19
25
|
}
|
|
@@ -89,6 +95,23 @@ function statusMarkdown() {
|
|
|
89
95
|
`;
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
function contextMarkdown(mode = 'public') {
|
|
99
|
+
const contextRoot = getContextRoot(mode);
|
|
100
|
+
const wikiRoot = getWikiRoot(mode);
|
|
101
|
+
return `# Context
|
|
102
|
+
|
|
103
|
+
Raw source material lives in \`${contextRoot}/\`.
|
|
104
|
+
Compiled memory belongs in \`${wikiRoot}/\`.
|
|
105
|
+
|
|
106
|
+
## Rules
|
|
107
|
+
|
|
108
|
+
- Drop source files or source packs here before compiling them into the wiki.
|
|
109
|
+
- Treat source files as immutable evidence. If the source changes, add a new dated copy.
|
|
110
|
+
- Keep compiled summaries, briefs, and durable facts in the wiki instead of this folder.
|
|
111
|
+
- Use \`${contextRoot}/_ingest/\` for staged ingest packs and receipts.
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
function ensureFile(filePath, content) {
|
|
93
116
|
if (!fs.existsSync(filePath)) {
|
|
94
117
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
@@ -185,6 +208,122 @@ function ensureWikiScaffold(projectRoot = process.cwd(), mode = 'public') {
|
|
|
185
208
|
return wikiDir;
|
|
186
209
|
}
|
|
187
210
|
|
|
211
|
+
function ensureContextScaffold(projectRoot = process.cwd(), mode = 'public') {
|
|
212
|
+
const contextRoot = getContextRoot(mode);
|
|
213
|
+
const contextDir = path.join(projectRoot, contextRoot);
|
|
214
|
+
fs.mkdirSync(path.join(contextDir, '_ingest'), { recursive: true });
|
|
215
|
+
ensureFile(path.join(contextDir, 'README.md'), contextMarkdown(mode));
|
|
216
|
+
return contextDir;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function slugifyLabel(value) {
|
|
220
|
+
return String(value || 'source')
|
|
221
|
+
.toLowerCase()
|
|
222
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
223
|
+
.replace(/^-+|-+$/g, '')
|
|
224
|
+
.slice(0, 40) || 'source';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isInsideDirectory(candidate, rootDir) {
|
|
228
|
+
const relative = path.relative(rootDir, candidate);
|
|
229
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function walkFiles(dir, output = []) {
|
|
233
|
+
if (!fs.existsSync(dir)) return output;
|
|
234
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
const fullPath = path.join(dir, entry.name);
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
walkFiles(fullPath, output);
|
|
239
|
+
} else if (entry.isFile()) {
|
|
240
|
+
output.push(fullPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return output;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function stageWikiIngest(projectRoot = process.cwd(), sourceValue, mode = 'public') {
|
|
247
|
+
const contextDir = ensureContextScaffold(projectRoot, mode);
|
|
248
|
+
const ingestDir = path.join(contextDir, '_ingest');
|
|
249
|
+
const raw = String(sourceValue || '').trim();
|
|
250
|
+
const timestamp = `${today()}-${nowTime().replace(':', '')}`;
|
|
251
|
+
const labelSeed = raw ? path.basename(raw) : 'source-pack';
|
|
252
|
+
const packDir = path.join(ingestDir, `${timestamp}-${slugifyLabel(labelSeed)}`);
|
|
253
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
254
|
+
|
|
255
|
+
const manifest = {
|
|
256
|
+
ingested_at: `${today()} ${nowTime()}`,
|
|
257
|
+
mode,
|
|
258
|
+
source_input: raw,
|
|
259
|
+
pack_path: path.relative(projectRoot, packDir).replace(/\\/g, '/'),
|
|
260
|
+
entries: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const maybeUrl = /^https?:\/\//i.test(raw);
|
|
264
|
+
const sourcePath = raw ? path.resolve(projectRoot, raw) : null;
|
|
265
|
+
|
|
266
|
+
if (raw && sourcePath && fs.existsSync(sourcePath)) {
|
|
267
|
+
const stat = fs.statSync(sourcePath);
|
|
268
|
+
if (isInsideDirectory(sourcePath, contextDir)) {
|
|
269
|
+
const files = stat.isDirectory() ? walkFiles(sourcePath) : [sourcePath];
|
|
270
|
+
manifest.entries.push({
|
|
271
|
+
kind: stat.isDirectory() ? 'directory' : 'file',
|
|
272
|
+
original: path.relative(projectRoot, sourcePath).replace(/\\/g, '/'),
|
|
273
|
+
staged: path.relative(projectRoot, sourcePath).replace(/\\/g, '/'),
|
|
274
|
+
file_count: files.length,
|
|
275
|
+
files: files.map((filePath) => path.relative(projectRoot, filePath).replace(/\\/g, '/')),
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
const stagedPath = path.join(packDir, path.basename(sourcePath));
|
|
279
|
+
if (stat.isDirectory()) {
|
|
280
|
+
fs.cpSync(sourcePath, stagedPath, { recursive: true });
|
|
281
|
+
} else {
|
|
282
|
+
fs.copyFileSync(sourcePath, stagedPath);
|
|
283
|
+
}
|
|
284
|
+
const files = stat.isDirectory() ? walkFiles(stagedPath) : [stagedPath];
|
|
285
|
+
manifest.entries.push({
|
|
286
|
+
kind: stat.isDirectory() ? 'directory' : 'file',
|
|
287
|
+
original: path.relative(projectRoot, sourcePath).replace(/\\/g, '/'),
|
|
288
|
+
staged: path.relative(projectRoot, stagedPath).replace(/\\/g, '/'),
|
|
289
|
+
file_count: files.length,
|
|
290
|
+
files: files.map((filePath) => path.relative(projectRoot, filePath).replace(/\\/g, '/')),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
} else if (maybeUrl) {
|
|
294
|
+
const linksPath = path.join(packDir, 'links.txt');
|
|
295
|
+
fs.writeFileSync(linksPath, `${raw}\n`, 'utf8');
|
|
296
|
+
manifest.entries.push({
|
|
297
|
+
kind: 'url',
|
|
298
|
+
original: raw,
|
|
299
|
+
staged: path.relative(projectRoot, linksPath).replace(/\\/g, '/'),
|
|
300
|
+
file_count: 1,
|
|
301
|
+
files: [path.relative(projectRoot, linksPath).replace(/\\/g, '/')],
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
const requestPath = path.join(packDir, 'request.txt');
|
|
305
|
+
fs.writeFileSync(requestPath, `${raw || '(empty)'}\n`, 'utf8');
|
|
306
|
+
manifest.entries.push({
|
|
307
|
+
kind: 'unresolved',
|
|
308
|
+
original: raw || '(empty)',
|
|
309
|
+
staged: path.relative(projectRoot, requestPath).replace(/\\/g, '/'),
|
|
310
|
+
file_count: 1,
|
|
311
|
+
files: [path.relative(projectRoot, requestPath).replace(/\\/g, '/')],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const manifestPath = path.join(packDir, 'manifest.json');
|
|
316
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
contextDir: path.relative(projectRoot, contextDir).replace(/\\/g, '/'),
|
|
320
|
+
packPath: path.relative(projectRoot, packDir).replace(/\\/g, '/'),
|
|
321
|
+
manifestPath: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
|
|
322
|
+
promptSource: manifest.entries[0]?.staged || path.relative(projectRoot, packDir).replace(/\\/g, '/'),
|
|
323
|
+
manifest,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
188
327
|
function findLocalWikiDir(projectRoot = process.cwd(), slug = null, mode = 'public') {
|
|
189
328
|
if (mode === 'private') {
|
|
190
329
|
const privateDir = path.join(projectRoot, PRIVATE_WIKI_ROOT);
|
|
@@ -444,7 +583,7 @@ function parseStatusBullets(content) {
|
|
|
444
583
|
return bullets;
|
|
445
584
|
}
|
|
446
585
|
|
|
447
|
-
function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public') {
|
|
586
|
+
function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public', overrides = {}) {
|
|
448
587
|
const wikiDir = ensureWikiScaffold(projectRoot, mode);
|
|
449
588
|
const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
|
|
450
589
|
const existing = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : '';
|
|
@@ -453,9 +592,9 @@ function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public') {
|
|
|
453
592
|
const lines = [
|
|
454
593
|
'# Atris Wiki Status',
|
|
455
594
|
'',
|
|
456
|
-
`- Last ingest: ${bullets.get('Last ingest') || 'never'}`,
|
|
457
|
-
`- Last lint: ${bullets.get('Last lint') || 'never'}`,
|
|
458
|
-
`- Last loop: ${today()} ${nowTime()}`,
|
|
595
|
+
`- Last ingest: ${overrides.lastIngest || bullets.get('Last ingest') || 'never'}`,
|
|
596
|
+
`- Last lint: ${overrides.lastLint || bullets.get('Last lint') || 'never'}`,
|
|
597
|
+
`- Last loop: ${overrides.lastLoop || `${today()} ${nowTime()}`}`,
|
|
459
598
|
`- Health: ${report.health}`,
|
|
460
599
|
`- Next move: ${report.nextMove}`,
|
|
461
600
|
'',
|
|
@@ -465,7 +604,7 @@ function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public') {
|
|
|
465
604
|
return statusPath;
|
|
466
605
|
}
|
|
467
606
|
|
|
468
|
-
function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode = 'public') {
|
|
607
|
+
function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode = 'public', kind = 'LOOP') {
|
|
469
608
|
const wikiDir = ensureWikiScaffold(projectRoot, mode);
|
|
470
609
|
const logPath = path.join(wikiDir, 'log.md');
|
|
471
610
|
let content = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '# Atris Wiki Log\n';
|
|
@@ -476,7 +615,7 @@ function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode
|
|
|
476
615
|
}
|
|
477
616
|
|
|
478
617
|
if (!content.endsWith('\n')) content += '\n';
|
|
479
|
-
content += `- ${nowTime()}
|
|
618
|
+
content += `- ${nowTime()} ${kind} ${summary}\n`;
|
|
480
619
|
for (const detail of details) {
|
|
481
620
|
content += ` - ${detail}\n`;
|
|
482
621
|
}
|
|
@@ -586,14 +725,18 @@ Output:
|
|
|
586
725
|
|
|
587
726
|
module.exports = {
|
|
588
727
|
WIKI_ROOT,
|
|
728
|
+
CONTEXT_ROOT,
|
|
589
729
|
PRIVATE_WIKI_ROOT,
|
|
730
|
+
PRIVATE_CONTEXT_ROOT,
|
|
590
731
|
LEGACY_WIKI_ROOT,
|
|
591
732
|
WIKI_SUBDIRS,
|
|
592
733
|
WIKI_CONTENT_SUBDIRS,
|
|
593
734
|
WIKI_SCHEMA: buildWikiSchema(),
|
|
594
735
|
WIKI_STATUS_FILE,
|
|
595
736
|
getWikiRoot,
|
|
737
|
+
getContextRoot,
|
|
596
738
|
ensureWikiScaffold,
|
|
739
|
+
ensureContextScaffold,
|
|
597
740
|
findLocalWikiDir,
|
|
598
741
|
normalizeWikiOnlyPrefix,
|
|
599
742
|
readWikiStatus,
|
|
@@ -601,6 +744,7 @@ module.exports = {
|
|
|
601
744
|
findStaleWikiPages,
|
|
602
745
|
findWikiOrphans,
|
|
603
746
|
findSuggestedSources,
|
|
747
|
+
stageWikiIngest,
|
|
604
748
|
writeWikiStatus,
|
|
605
749
|
appendWikiLog,
|
|
606
750
|
buildIngestPrompt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Atris — an operating system for intelligence. Integrates with any agent.",
|
|
5
5
|
"main": "bin/atris.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
|
+
"cli/",
|
|
11
12
|
"commands/",
|
|
12
13
|
"utils/",
|
|
13
14
|
"lib/",
|
package/utils/api.js
CHANGED
|
@@ -20,22 +20,41 @@ try {
|
|
|
20
20
|
const DEFAULT_CLIENT_ID = `AtrisCLI/${CLI_VERSION}`;
|
|
21
21
|
const DEFAULT_USER_AGENT = `${DEFAULT_CLIENT_ID} (node ${process.version}; ${os.platform()} ${os.release()} ${os.arch()})`;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Get the base URL for the Atris API.
|
|
25
|
+
* @returns {string} API base URL
|
|
26
|
+
*/
|
|
23
27
|
function getApiBaseUrl() {
|
|
24
28
|
const raw = process.env.ATRIS_API_URL || 'https://api.atris.ai/api';
|
|
25
29
|
return raw.replace(/\/$/, '');
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Get the base URL for the Atris web app.
|
|
34
|
+
* @returns {string} App base URL
|
|
35
|
+
*/
|
|
28
36
|
function getAppBaseUrl() {
|
|
29
37
|
const raw = process.env.ATRIS_APP_URL || 'https://atris.ai';
|
|
30
38
|
return raw.replace(/\/$/, '');
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Build a full API URL from a path.
|
|
43
|
+
* @param {string} pathname - API endpoint path
|
|
44
|
+
* @returns {string} Full API URL
|
|
45
|
+
*/
|
|
33
46
|
function buildApiUrl(pathname) {
|
|
34
47
|
const base = getApiBaseUrl();
|
|
35
48
|
const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
36
49
|
return `${base}${normalizedPath}`;
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Make an HTTP/HTTPS request.
|
|
54
|
+
* @param {string} urlString - Full URL to request
|
|
55
|
+
* @param {Object} options - Request options (method, headers, body, timeoutMs)
|
|
56
|
+
* @returns {Promise<Object>} Response with statusCode, headers, and data
|
|
57
|
+
*/
|
|
39
58
|
function httpRequest(urlString, options) {
|
|
40
59
|
return new Promise((resolve, reject) => {
|
|
41
60
|
const parsed = new URL(urlString);
|
package/utils/auth.js
CHANGED
|
@@ -4,6 +4,10 @@ const fs = require('fs');
|
|
|
4
4
|
const { exec } = require('child_process');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Open a URL in the system's default browser.
|
|
9
|
+
* @param {string} url - URL to open
|
|
10
|
+
*/
|
|
7
11
|
function openBrowser(url) {
|
|
8
12
|
const platform = os.platform();
|
|
9
13
|
// Sanitize URL to prevent shell injection — only allow valid URL characters
|
|
@@ -30,6 +34,11 @@ let sharedRl = null;
|
|
|
30
34
|
let inputLines = [];
|
|
31
35
|
let inputIndex = 0;
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Prompt the user for input, handling both TTY and piped input.
|
|
39
|
+
* @param {string} question - Prompt text to display
|
|
40
|
+
* @returns {Promise<string>} User's input
|
|
41
|
+
*/
|
|
33
42
|
function promptUser(question) {
|
|
34
43
|
// If stdin is not a TTY (piped input), read all lines upfront
|
|
35
44
|
if (!process.stdin.isTTY && inputLines.length === 0 && !sharedRl) {
|
|
@@ -74,7 +83,11 @@ function promptUser(question) {
|
|
|
74
83
|
|
|
75
84
|
const TOKEN_REFRESH_BUFFER_SECONDS = 300;
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Decode and parse the claims from a JWT token.
|
|
88
|
+
* @param {string} token - JWT token string
|
|
89
|
+
* @returns {Object|null} Decoded claims or null if invalid
|
|
90
|
+
*/
|
|
78
91
|
function decodeJwtClaims(token) {
|
|
79
92
|
if (!token || typeof token !== 'string') {
|
|
80
93
|
return null;
|
|
@@ -93,6 +106,11 @@ function decodeJwtClaims(token) {
|
|
|
93
106
|
}
|
|
94
107
|
}
|
|
95
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Get the expiration time of a JWT token in epoch seconds.
|
|
111
|
+
* @param {string} token - JWT token string
|
|
112
|
+
* @returns {number|null} Expiry epoch seconds or null if invalid
|
|
113
|
+
*/
|
|
96
114
|
function getTokenExpiryEpochSeconds(token) {
|
|
97
115
|
const claims = decodeJwtClaims(token);
|
|
98
116
|
if (!claims || typeof claims.exp !== 'number') {
|
|
@@ -101,6 +119,12 @@ function getTokenExpiryEpochSeconds(token) {
|
|
|
101
119
|
return claims.exp;
|
|
102
120
|
}
|
|
103
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Check if a JWT token should be refreshed based on expiry.
|
|
124
|
+
* @param {string} token - JWT token string
|
|
125
|
+
* @param {number} [bufferSeconds=300] - Seconds before expiry to trigger refresh
|
|
126
|
+
* @returns {boolean} True if token needs refresh
|
|
127
|
+
*/
|
|
104
128
|
function shouldRefreshToken(token, bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS) {
|
|
105
129
|
const exp = getTokenExpiryEpochSeconds(token);
|
|
106
130
|
if (!exp) {
|
package/utils/config.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Get the path to atris/.config in the current project.
|
|
6
|
+
* @returns {string} Config file path
|
|
7
|
+
*/
|
|
4
8
|
function getConfigPath() {
|
|
5
9
|
const targetDir = path.join(process.cwd(), 'atris');
|
|
6
10
|
return path.join(targetDir, '.config');
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Load config from atris/.config, returning empty object if missing/invalid.
|
|
15
|
+
* @returns {Object} Config object
|
|
16
|
+
*/
|
|
9
17
|
function loadConfig() {
|
|
10
18
|
const configPath = getConfigPath();
|
|
11
19
|
|
|
@@ -21,6 +29,10 @@ function loadConfig() {
|
|
|
21
29
|
}
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Save config to atris/.config. Exits if atris/ folder doesn't exist.
|
|
34
|
+
* @param {Object} config - Config object to save
|
|
35
|
+
*/
|
|
24
36
|
function saveConfig(config) {
|
|
25
37
|
const configPath = getConfigPath();
|
|
26
38
|
const targetDir = path.dirname(configPath);
|
|
@@ -33,11 +45,19 @@ function saveConfig(config) {
|
|
|
33
45
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Get the path to atris/.log_sync_state.json for tracking sync timestamps.
|
|
50
|
+
* @returns {string} Log sync state file path
|
|
51
|
+
*/
|
|
36
52
|
function getLogSyncStatePath() {
|
|
37
53
|
const targetDir = path.join(process.cwd(), 'atris');
|
|
38
54
|
return path.join(targetDir, '.log_sync_state.json');
|
|
39
55
|
}
|
|
40
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Load log sync state, returning empty object if missing/invalid.
|
|
59
|
+
* @returns {Object} Sync state object with last sync timestamps
|
|
60
|
+
*/
|
|
41
61
|
function loadLogSyncState() {
|
|
42
62
|
const statePath = getLogSyncStatePath();
|
|
43
63
|
if (!fs.existsSync(statePath)) {
|
|
@@ -51,6 +71,10 @@ function loadLogSyncState() {
|
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Save log sync state to atris/.log_sync_state.json.
|
|
76
|
+
* @param {Object} state - Sync state object to save
|
|
77
|
+
*/
|
|
54
78
|
function saveLogSyncState(state) {
|
|
55
79
|
const statePath = getLogSyncStatePath();
|
|
56
80
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
package/utils/update-check.js
CHANGED
|
@@ -8,6 +8,10 @@ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
|
8
8
|
const ATRIS_DIR = path.join(os.homedir(), '.atris');
|
|
9
9
|
const CACHE_FILE = path.join(ATRIS_DIR, '.update-check');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Get the currently installed CLI version from package.json.
|
|
13
|
+
* @returns {string|null} Installed version string, or null on error
|
|
14
|
+
*/
|
|
11
15
|
function getInstalledVersion() {
|
|
12
16
|
try {
|
|
13
17
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
@@ -18,6 +22,10 @@ function getInstalledVersion() {
|
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Load cached update check data from ~/.atris/.update-check.
|
|
27
|
+
* @returns {{lastCheck: Date|null, latestVersion: string|null}} Cache data
|
|
28
|
+
*/
|
|
21
29
|
function getCacheData() {
|
|
22
30
|
try {
|
|
23
31
|
if (fs.existsSync(CACHE_FILE)) {
|
|
@@ -33,6 +41,10 @@ function getCacheData() {
|
|
|
33
41
|
return { lastCheck: null, latestVersion: null };
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Save update check result to ~/.atris/.update-check cache file.
|
|
46
|
+
* @param {string} latestVersion - The latest version from npm
|
|
47
|
+
*/
|
|
36
48
|
function saveCacheData(latestVersion) {
|
|
37
49
|
try {
|
|
38
50
|
// Ensure ~/.atris/ exists
|
|
@@ -49,6 +61,10 @@ function saveCacheData(latestVersion) {
|
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Fetch the latest version of atris from npm registry.
|
|
66
|
+
* @returns {Promise<string>} Latest version string
|
|
67
|
+
*/
|
|
52
68
|
function checkNpmVersion() {
|
|
53
69
|
return new Promise((resolve, reject) => {
|
|
54
70
|
const options = {
|