atris 3.2.0 → 3.11.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 +46 -12
- 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 +16 -0
- package/commands/app.js +316 -0
- package/commands/autopilot.js +863 -23
- package/commands/brainstorm.js +7 -5
- package/commands/business.js +677 -2
- package/commands/clean.js +19 -3
- package/commands/computer.js +2022 -43
- package/commands/context-sync.js +5 -0
- package/commands/integrations.js +14 -9
- package/commands/lifecycle.js +12 -0
- package/commands/plugin.js +24 -0
- package/commands/pull.js +86 -11
- package/commands/push.js +153 -9
- 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/commands/workflow.js +24 -9
- package/lib/file-ops.js +13 -1
- package/lib/journal.js +23 -0
- package/lib/manifest.js +3 -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/lib/workspace-safety.js +87 -0
- 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
package/lib/scorecard.js
CHANGED
|
@@ -4,16 +4,31 @@ const { parseTodo } = require('./todo');
|
|
|
4
4
|
|
|
5
5
|
const PRIVATE_MEMORY_ROOT = '.atris/presidio';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Ensure the private memory directory exists.
|
|
9
|
+
* @param {string} atrisDir - Path to the atris directory
|
|
10
|
+
* @returns {string} Path to the private memory directory
|
|
11
|
+
*/
|
|
7
12
|
function ensurePrivateMemoryDir(atrisDir) {
|
|
8
13
|
const privateDir = path.join(path.dirname(atrisDir), PRIVATE_MEMORY_ROOT);
|
|
9
14
|
fs.mkdirSync(privateDir, { recursive: true });
|
|
10
15
|
return privateDir;
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Get the path to the scorecards file.
|
|
20
|
+
* @param {string} atrisDir - Path to the atris directory
|
|
21
|
+
* @returns {string} Path to scorecards.md
|
|
22
|
+
*/
|
|
13
23
|
function getScorecardsPath(atrisDir) {
|
|
14
24
|
return path.join(ensurePrivateMemoryDir(atrisDir), 'scorecards.md');
|
|
15
25
|
}
|
|
16
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Parse a "picked at" timestamp from scorecard data.
|
|
29
|
+
* @param {string} value - Timestamp string in YYYY-MM-DD [HH:MM] format
|
|
30
|
+
* @returns {Date|null} Parsed date or null if invalid
|
|
31
|
+
*/
|
|
17
32
|
function parsePickedAt(value) {
|
|
18
33
|
if (!value) return null;
|
|
19
34
|
const match = String(value).trim().match(/^(\d{4}-\d{2}-\d{2})(?:\s+(\d{2}:\d{2}))?/);
|
|
@@ -24,6 +39,12 @@ function parsePickedAt(value) {
|
|
|
24
39
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Parse a tick timestamp combining date and time label.
|
|
44
|
+
* @param {string} dateStr - Date in YYYY-MM-DD format
|
|
45
|
+
* @param {string} timeLabel - Time label like "2:30 PM"
|
|
46
|
+
* @returns {Date|null} Parsed date or null if invalid
|
|
47
|
+
*/
|
|
27
48
|
function parseTickDate(dateStr, timeLabel) {
|
|
28
49
|
const match = String(timeLabel || '').trim().toLowerCase().match(/^(\d{1,2}):(\d{2})(?:\s*(am|pm))?$/);
|
|
29
50
|
if (!match) return null;
|
|
@@ -40,6 +61,13 @@ function parseTickDate(dateStr, timeLabel) {
|
|
|
40
61
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
41
62
|
}
|
|
42
63
|
|
|
64
|
+
/**
|
|
65
|
+
* List journal log files within a date range.
|
|
66
|
+
* @param {string} atrisDir - Path to the atris directory
|
|
67
|
+
* @param {Date} startDate - Start of date range
|
|
68
|
+
* @param {Date} [endDate] - End of date range (defaults to today)
|
|
69
|
+
* @returns {Array} Array of log file paths
|
|
70
|
+
*/
|
|
43
71
|
function listLogFiles(atrisDir, startDate, endDate = new Date()) {
|
|
44
72
|
const logsDir = path.join(atrisDir, 'logs');
|
|
45
73
|
if (!fs.existsSync(logsDir)) return [];
|
|
@@ -153,16 +181,20 @@ function buildScorecardData(atrisDir, { slug, pickedAt } = {}) {
|
|
|
153
181
|
const todo = parseTodo(todoPath);
|
|
154
182
|
const startAt = parsePickedAt(pickedAt) || new Date();
|
|
155
183
|
const rewardStats = collectRewardStats(atrisDir, pickedAt);
|
|
156
|
-
|
|
184
|
+
// Count shipped tasks from journal completions (tasks get deleted from TODO.md after completion)
|
|
185
|
+
const completedFromTodo = todo.completed.filter(t => t.tag === 'endgame').length;
|
|
157
186
|
const activeEndgame = todo.backlog.filter(t => t.tag === 'endgame').length
|
|
158
187
|
+ todo.inProgress.filter(t => t.tag === 'endgame').length;
|
|
188
|
+
// Fall back to reward tick count if TODO completions were already pruned
|
|
189
|
+
const shipped = completedFromTodo > 0 ? completedFromTodo : rewardStats.totalTicks - rewardStats.haltedTicks;
|
|
190
|
+
const attempted = shipped + activeEndgame + rewardStats.haltedTicks;
|
|
159
191
|
|
|
160
192
|
return {
|
|
161
193
|
slug,
|
|
162
194
|
startDate: startAt.toISOString().slice(0, 10),
|
|
163
195
|
endDate: new Date().toISOString().slice(0, 10),
|
|
164
|
-
tasksShipped:
|
|
165
|
-
tasksAttempted:
|
|
196
|
+
tasksShipped: Math.max(shipped, 0),
|
|
197
|
+
tasksAttempted: Math.max(attempted, shipped),
|
|
166
198
|
wallClockHours: Math.max(0, (Date.now() - startAt.getTime()) / (1000 * 60 * 60)),
|
|
167
199
|
haltRatio: rewardStats.totalTicks > 0 ? rewardStats.haltedTicks / rewardStats.totalTicks : 0,
|
|
168
200
|
totalReward: rewardStats.totalReward,
|
|
@@ -205,6 +237,12 @@ function writeScorecard(atrisDir, data) {
|
|
|
205
237
|
|
|
206
238
|
const scorecardsPath = getScorecardsPath(atrisDir);
|
|
207
239
|
|
|
240
|
+
// Dedupe guard: don't write the same slug twice
|
|
241
|
+
const existing = readScorecards(atrisDir);
|
|
242
|
+
if (existing.some(sc => sc.slug === slug)) {
|
|
243
|
+
return; // already written
|
|
244
|
+
}
|
|
245
|
+
|
|
208
246
|
// Ensure scorecards.md exists
|
|
209
247
|
if (!fs.existsSync(scorecardsPath)) {
|
|
210
248
|
const template = `# scorecards.md — Endgame Results\n\n> Append-only. One line per closed endgame. Records outcome metrics from the horizon.\n\n---\n\n`;
|
|
@@ -263,7 +301,7 @@ function readScorecards(atrisDir) {
|
|
|
263
301
|
const scorecards = [];
|
|
264
302
|
|
|
265
303
|
for (const line of content.split('\n')) {
|
|
266
|
-
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s*—\s*shipped:\s*(\d+)\/(\d+)\s*—\s*wall-clock:\s*(.+?)\s*—\s*halt:\s*(\d+)%\s*—\s*reward:\s*(
|
|
304
|
+
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s*—\s*shipped:\s*(\d+)\/(\d+)\s*—\s*wall-clock:\s*(.+?)\s*—\s*halt:\s*(\d+)%\s*—\s*reward:\s*(-?\d+)\s*—\s*lessons:\s*(\d+)$/);
|
|
267
305
|
if (!match) continue;
|
|
268
306
|
|
|
269
307
|
const [, endDate, slug, shipped, attempted, wallClockStr, haltPercent, reward, lessons] = match;
|
|
@@ -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,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
// Returns a human-readable reason string if `dir` is a path we must never
|
|
5
|
+
// treat as an Atris workspace root (pull/push would walk and mutate it).
|
|
6
|
+
// Returns null if the dir is safe.
|
|
7
|
+
//
|
|
8
|
+
// The real bug this guards against: if outputDir resolves to $HOME, pull's
|
|
9
|
+
// force mirror sweep walks ~/Library, ~/Documents, ~/Downloads, ... and
|
|
10
|
+
// deletes any local file not on cloud. That wipes the user's home dir.
|
|
11
|
+
function dangerousWorkspaceReason(dir) {
|
|
12
|
+
if (!dir) return 'empty path';
|
|
13
|
+
const abs = path.resolve(dir);
|
|
14
|
+
const home = os.homedir();
|
|
15
|
+
const rootParsed = path.parse(abs).root;
|
|
16
|
+
|
|
17
|
+
if (abs === home) return 'your home directory';
|
|
18
|
+
if (abs === rootParsed) return 'the filesystem root';
|
|
19
|
+
if (abs === path.dirname(home)) return `the users root (${path.dirname(home)})`;
|
|
20
|
+
|
|
21
|
+
const systemPaths = [
|
|
22
|
+
'/tmp', '/var', '/etc', '/usr', '/bin', '/sbin', '/opt',
|
|
23
|
+
'/private', '/Library', '/Applications', '/System', '/Volumes',
|
|
24
|
+
'/Users', '/home', '/root',
|
|
25
|
+
];
|
|
26
|
+
for (const p of systemPaths) {
|
|
27
|
+
if (abs === p) return `the system path ${p}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Reserved top-level folders inside the user's home — never workspaces.
|
|
31
|
+
const homeReservedNames = new Set([
|
|
32
|
+
'Library', 'Applications', 'Documents', 'Downloads', 'Desktop',
|
|
33
|
+
'Pictures', 'Music', 'Movies', 'Public', 'Sites', 'Dropbox',
|
|
34
|
+
'OneDrive', 'iCloud Drive',
|
|
35
|
+
]);
|
|
36
|
+
if (path.dirname(abs) === home && homeReservedNames.has(path.basename(abs))) {
|
|
37
|
+
return `a reserved home folder (~/${path.basename(abs)})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Pick a safe fallback directory for a business workspace when the
|
|
44
|
+
// originally-resolved path is dangerous. Prefers cwd/slug, falls back to
|
|
45
|
+
// ~/atris-workspaces/slug if cwd itself is unsafe.
|
|
46
|
+
function safeFallbackDir(slug, cwd) {
|
|
47
|
+
const cwdFallback = path.join(cwd, slug);
|
|
48
|
+
if (!dangerousWorkspaceReason(cwdFallback)) return cwdFallback;
|
|
49
|
+
return path.join(os.homedir(), 'atris-workspaces', slug);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Resolve an outputDir, auto-relocating to a safe fallback if the originally
|
|
53
|
+
// chosen path is dangerous. Non-technical users don't need to know about
|
|
54
|
+
// mkdir/cd — atris picks a safe folder and tells them where it landed.
|
|
55
|
+
function resolveSafeOutputDir(requested, { slug, cwd = process.cwd(), op = 'use' } = {}) {
|
|
56
|
+
const reason = dangerousWorkspaceReason(requested);
|
|
57
|
+
if (!reason) return { dir: path.resolve(requested), relocated: false };
|
|
58
|
+
|
|
59
|
+
const fallback = safeFallbackDir(slug, cwd);
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(` ${requested} is ${reason} — not safe to ${op}.`);
|
|
62
|
+
console.log(` Using ${fallback} instead.`);
|
|
63
|
+
console.log('');
|
|
64
|
+
return { dir: fallback, relocated: true, originalReason: reason };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Hard-refuse variant — used by push where we can't auto-relocate (the
|
|
68
|
+
// source has to be where the user actually has their files).
|
|
69
|
+
function assertSafeWorkspaceRoot(dir, { slug, op } = {}) {
|
|
70
|
+
const reason = dangerousWorkspaceReason(dir);
|
|
71
|
+
if (!reason) return;
|
|
72
|
+
const label = op || 'operate on';
|
|
73
|
+
console.error('');
|
|
74
|
+
console.error(` Refusing to ${label} ${dir} (${reason}).`);
|
|
75
|
+
console.error('');
|
|
76
|
+
console.error(' Atris would walk this folder and sync files inside it.');
|
|
77
|
+
console.error(' Cd into a dedicated workspace folder first.');
|
|
78
|
+
if (slug) console.error(` For example: mkdir -p ~/code/${slug} && cd ~/code/${slug}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
dangerousWorkspaceReason,
|
|
84
|
+
safeFallbackDir,
|
|
85
|
+
resolveSafeOutputDir,
|
|
86
|
+
assertSafeWorkspaceRoot,
|
|
87
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.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));
|