atris 3.1.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 +29 -4
- 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/improve/SKILL.md +2 -2
- 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 +37 -6
- 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 +948 -42
- package/commands/business.js +691 -11
- package/commands/computer.js +1979 -43
- package/commands/context-sync.js +5 -0
- package/commands/experiments.js +1 -1
- 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/release.js +183 -0
- package/commands/research.js +52 -0
- package/commands/serve.js +1 -0
- package/commands/sync.js +372 -87
- package/commands/verify.js +53 -4
- package/commands/wiki.js +71 -26
- package/lib/file-ops.js +13 -1
- package/lib/journal.js +23 -0
- package/lib/reward-config.js +24 -0
- package/lib/scorecard.js +58 -6
- package/lib/sync-telemetry.js +59 -0
- package/lib/todo.js +6 -0
- package/lib/wiki.js +235 -60
- package/package.json +4 -2
- 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/wiki.js
CHANGED
|
@@ -2,6 +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';
|
|
6
|
+
const PRIVATE_WIKI_ROOT = '.atris/presidio';
|
|
7
|
+
const PRIVATE_CONTEXT_ROOT = `${PRIVATE_WIKI_ROOT}/context`;
|
|
5
8
|
const LEGACY_WIKI_ROOT = 'wiki';
|
|
6
9
|
const WIKI_BRIEFS_SUBDIR = 'briefs';
|
|
7
10
|
const LEGACY_WIKI_BRIEFS_SUBDIR = 'syntheses';
|
|
@@ -9,6 +12,18 @@ const WIKI_SUBDIRS = ['people', 'systems', 'concepts', WIKI_BRIEFS_SUBDIR];
|
|
|
9
12
|
const WIKI_STATUS_FILE = 'STATUS.md';
|
|
10
13
|
const WIKI_CONTENT_SUBDIRS = WIKI_SUBDIRS.map((subdir) => path.join(WIKI_ROOT, subdir));
|
|
11
14
|
|
|
15
|
+
function getWikiRoot(mode = 'public') {
|
|
16
|
+
return mode === 'private' ? PRIVATE_WIKI_ROOT : WIKI_ROOT;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getContextRoot(mode = 'public') {
|
|
20
|
+
return mode === 'private' ? PRIVATE_CONTEXT_ROOT : CONTEXT_ROOT;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getWikiLinkRoot(mode = 'public') {
|
|
24
|
+
return mode === 'private' ? PRIVATE_WIKI_ROOT : 'atris/wiki';
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
function today() {
|
|
13
28
|
return new Date().toISOString().slice(0, 10);
|
|
14
29
|
}
|
|
@@ -17,10 +32,12 @@ function nowTime() {
|
|
|
17
32
|
return new Date().toTimeString().slice(0, 5);
|
|
18
33
|
}
|
|
19
34
|
|
|
20
|
-
function protocolMarkdown() {
|
|
35
|
+
function protocolMarkdown(mode = 'public') {
|
|
36
|
+
const wikiRoot = getWikiRoot(mode);
|
|
37
|
+
const wikiLinkRoot = getWikiLinkRoot(mode);
|
|
21
38
|
return `# Atris Wiki Protocol
|
|
22
39
|
|
|
23
|
-
This wiki lives in \`${
|
|
40
|
+
This wiki lives in \`${wikiRoot}/\`.
|
|
24
41
|
|
|
25
42
|
## Purpose
|
|
26
43
|
|
|
@@ -28,20 +45,20 @@ Turn raw project context into a living memory the next agent can pick up cold.
|
|
|
28
45
|
|
|
29
46
|
## Shape
|
|
30
47
|
|
|
31
|
-
- \`${
|
|
32
|
-
- \`${
|
|
33
|
-
- \`${
|
|
34
|
-
- \`${
|
|
35
|
-
- \`${
|
|
36
|
-
- \`${
|
|
37
|
-
- \`${
|
|
38
|
-
- \`${
|
|
48
|
+
- \`${wikiRoot}/wiki.md\` - this protocol
|
|
49
|
+
- \`${wikiRoot}/index.md\` - catalog grouped by page type
|
|
50
|
+
- \`${wikiRoot}/log.md\` - append-only ingest and lint history
|
|
51
|
+
- \`${wikiRoot}/STATUS.md\` - plain-English health summary
|
|
52
|
+
- \`${wikiRoot}/people/\` - humans (employees, contacts, stakeholders)
|
|
53
|
+
- \`${wikiRoot}/systems/\` - tools, tables, dashboards, services, products
|
|
54
|
+
- \`${wikiRoot}/concepts/\` - patterns, frameworks, recurring ideas
|
|
55
|
+
- \`${wikiRoot}/${WIKI_BRIEFS_SUBDIR}/\` - multi-page briefs and cross-cutting analysis
|
|
39
56
|
|
|
40
57
|
## Rules
|
|
41
58
|
|
|
42
59
|
- Read the full source before writing.
|
|
43
60
|
- Merge new facts into existing pages. Do not overwrite history blindly.
|
|
44
|
-
- Add cross-references with \`[[
|
|
61
|
+
- Add cross-references with \`[[${wikiLinkRoot}/...]]\` links.
|
|
45
62
|
- Keep \`index.md\`, \`log.md\`, and \`STATUS.md\` in sync with page changes.
|
|
46
63
|
- If something is unclear or contradictory, say so directly.
|
|
47
64
|
`;
|
|
@@ -78,6 +95,23 @@ function statusMarkdown() {
|
|
|
78
95
|
`;
|
|
79
96
|
}
|
|
80
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
|
+
|
|
81
115
|
function ensureFile(filePath, content) {
|
|
82
116
|
if (!fs.existsSync(filePath)) {
|
|
83
117
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
@@ -155,15 +189,18 @@ function migrateLegacyBriefsDir(wikiDir) {
|
|
|
155
189
|
rewriteLegacyWikiReferences(wikiDir);
|
|
156
190
|
}
|
|
157
191
|
|
|
158
|
-
function ensureWikiScaffold(projectRoot = process.cwd()) {
|
|
159
|
-
const
|
|
192
|
+
function ensureWikiScaffold(projectRoot = process.cwd(), mode = 'public') {
|
|
193
|
+
const wikiRoot = getWikiRoot(mode);
|
|
194
|
+
const wikiDir = path.join(projectRoot, wikiRoot);
|
|
160
195
|
fs.mkdirSync(wikiDir, { recursive: true });
|
|
161
|
-
|
|
196
|
+
if (mode === 'public') {
|
|
197
|
+
migrateLegacyBriefsDir(wikiDir);
|
|
198
|
+
}
|
|
162
199
|
for (const subdir of WIKI_SUBDIRS) {
|
|
163
200
|
fs.mkdirSync(path.join(wikiDir, subdir), { recursive: true });
|
|
164
201
|
}
|
|
165
202
|
|
|
166
|
-
ensureFile(path.join(wikiDir, 'wiki.md'), protocolMarkdown());
|
|
203
|
+
ensureFile(path.join(wikiDir, 'wiki.md'), protocolMarkdown(mode));
|
|
167
204
|
ensureFile(path.join(wikiDir, 'index.md'), indexMarkdown());
|
|
168
205
|
ensureFile(path.join(wikiDir, 'log.md'), logMarkdown());
|
|
169
206
|
ensureFile(path.join(wikiDir, WIKI_STATUS_FILE), statusMarkdown());
|
|
@@ -171,7 +208,128 @@ function ensureWikiScaffold(projectRoot = process.cwd()) {
|
|
|
171
208
|
return wikiDir;
|
|
172
209
|
}
|
|
173
210
|
|
|
174
|
-
function
|
|
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
|
+
|
|
327
|
+
function findLocalWikiDir(projectRoot = process.cwd(), slug = null, mode = 'public') {
|
|
328
|
+
if (mode === 'private') {
|
|
329
|
+
const privateDir = path.join(projectRoot, PRIVATE_WIKI_ROOT);
|
|
330
|
+
return fs.existsSync(privateDir) ? privateDir : null;
|
|
331
|
+
}
|
|
332
|
+
|
|
175
333
|
const tries = [
|
|
176
334
|
path.join(projectRoot, WIKI_ROOT),
|
|
177
335
|
path.join(projectRoot, LEGACY_WIKI_ROOT),
|
|
@@ -190,8 +348,8 @@ function normalizeWikiOnlyPrefix(prefix) {
|
|
|
190
348
|
return null;
|
|
191
349
|
}
|
|
192
350
|
|
|
193
|
-
function readWikiStatus(projectRoot = process.cwd(), slug = null) {
|
|
194
|
-
const wikiDir = findLocalWikiDir(projectRoot, slug);
|
|
351
|
+
function readWikiStatus(projectRoot = process.cwd(), slug = null, mode = 'public') {
|
|
352
|
+
const wikiDir = findLocalWikiDir(projectRoot, slug, mode);
|
|
195
353
|
if (!wikiDir) return null;
|
|
196
354
|
|
|
197
355
|
const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
|
|
@@ -275,8 +433,9 @@ function parseFrontmatter(content) {
|
|
|
275
433
|
return frontmatter;
|
|
276
434
|
}
|
|
277
435
|
|
|
278
|
-
function readWikiPages(projectRoot = process.cwd()) {
|
|
279
|
-
const
|
|
436
|
+
function readWikiPages(projectRoot = process.cwd(), mode = 'public') {
|
|
437
|
+
const wikiRoot = getWikiRoot(mode);
|
|
438
|
+
const wikiDir = path.join(projectRoot, wikiRoot);
|
|
280
439
|
const pages = [];
|
|
281
440
|
|
|
282
441
|
for (const subdir of WIKI_SUBDIRS) {
|
|
@@ -302,8 +461,8 @@ function normalizeSourcePath(projectRoot, source) {
|
|
|
302
461
|
return path.normalize(path.join(projectRoot, source));
|
|
303
462
|
}
|
|
304
463
|
|
|
305
|
-
function findStaleWikiPages(projectRoot = process.cwd()) {
|
|
306
|
-
return readWikiPages(projectRoot)
|
|
464
|
+
function findStaleWikiPages(projectRoot = process.cwd(), mode = 'public') {
|
|
465
|
+
return readWikiPages(projectRoot, mode)
|
|
307
466
|
.map((page) => {
|
|
308
467
|
const sources = Array.isArray(page.frontmatter.sources) ? page.frontmatter.sources : [];
|
|
309
468
|
if (sources.length === 0) return null;
|
|
@@ -345,13 +504,14 @@ function findStaleWikiPages(projectRoot = process.cwd()) {
|
|
|
345
504
|
}
|
|
346
505
|
|
|
347
506
|
function extractWikiLinks(content) {
|
|
348
|
-
const matches = content.match(/\[\[(atris\/wiki\/[^\]]+?)\]\]/g) || [];
|
|
507
|
+
const matches = content.match(/\[\[((?:atris\/wiki|\.atris\/presidio)\/[^\]]+?)\]\]/g) || [];
|
|
349
508
|
return matches.map((match) => match.slice(2, -2));
|
|
350
509
|
}
|
|
351
510
|
|
|
352
|
-
function findWikiOrphans(projectRoot = process.cwd()) {
|
|
353
|
-
const
|
|
354
|
-
const
|
|
511
|
+
function findWikiOrphans(projectRoot = process.cwd(), mode = 'public') {
|
|
512
|
+
const wikiRoot = getWikiRoot(mode);
|
|
513
|
+
const pages = readWikiPages(projectRoot, mode);
|
|
514
|
+
const indexPath = path.join(projectRoot, wikiRoot, 'index.md');
|
|
355
515
|
const indexContent = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '';
|
|
356
516
|
|
|
357
517
|
const inboundLinks = new Map();
|
|
@@ -423,8 +583,8 @@ function parseStatusBullets(content) {
|
|
|
423
583
|
return bullets;
|
|
424
584
|
}
|
|
425
585
|
|
|
426
|
-
function writeWikiStatus(projectRoot = process.cwd(), report) {
|
|
427
|
-
const wikiDir = ensureWikiScaffold(projectRoot);
|
|
586
|
+
function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public', overrides = {}) {
|
|
587
|
+
const wikiDir = ensureWikiScaffold(projectRoot, mode);
|
|
428
588
|
const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
|
|
429
589
|
const existing = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : '';
|
|
430
590
|
const bullets = parseStatusBullets(existing);
|
|
@@ -432,9 +592,9 @@ function writeWikiStatus(projectRoot = process.cwd(), report) {
|
|
|
432
592
|
const lines = [
|
|
433
593
|
'# Atris Wiki Status',
|
|
434
594
|
'',
|
|
435
|
-
`- Last ingest: ${bullets.get('Last ingest') || 'never'}`,
|
|
436
|
-
`- Last lint: ${bullets.get('Last lint') || 'never'}`,
|
|
437
|
-
`- 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()}`}`,
|
|
438
598
|
`- Health: ${report.health}`,
|
|
439
599
|
`- Next move: ${report.nextMove}`,
|
|
440
600
|
'',
|
|
@@ -444,8 +604,8 @@ function writeWikiStatus(projectRoot = process.cwd(), report) {
|
|
|
444
604
|
return statusPath;
|
|
445
605
|
}
|
|
446
606
|
|
|
447
|
-
function appendWikiLog(projectRoot = process.cwd(), summary, details = []) {
|
|
448
|
-
const wikiDir = ensureWikiScaffold(projectRoot);
|
|
607
|
+
function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode = 'public', kind = 'LOOP') {
|
|
608
|
+
const wikiDir = ensureWikiScaffold(projectRoot, mode);
|
|
449
609
|
const logPath = path.join(wikiDir, 'log.md');
|
|
450
610
|
let content = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '# Atris Wiki Log\n';
|
|
451
611
|
const dateHeader = `## ${today()}`;
|
|
@@ -455,7 +615,7 @@ function appendWikiLog(projectRoot = process.cwd(), summary, details = []) {
|
|
|
455
615
|
}
|
|
456
616
|
|
|
457
617
|
if (!content.endsWith('\n')) content += '\n';
|
|
458
|
-
content += `- ${nowTime()}
|
|
618
|
+
content += `- ${nowTime()} ${kind} ${summary}\n`;
|
|
459
619
|
for (const detail of details) {
|
|
460
620
|
content += ` - ${detail}\n`;
|
|
461
621
|
}
|
|
@@ -471,17 +631,20 @@ function formatSourceList(sourceValue) {
|
|
|
471
631
|
.join(', ');
|
|
472
632
|
}
|
|
473
633
|
|
|
474
|
-
|
|
634
|
+
function buildWikiSchema(mode = 'public') {
|
|
635
|
+
const wikiRoot = getWikiRoot(mode);
|
|
636
|
+
const wikiLinkRoot = getWikiLinkRoot(mode);
|
|
637
|
+
return `The wiki lives in ${wikiRoot}/.
|
|
475
638
|
|
|
476
639
|
Structure:
|
|
477
|
-
- ${
|
|
478
|
-
- ${
|
|
479
|
-
- ${
|
|
480
|
-
- ${
|
|
481
|
-
- ${
|
|
482
|
-
- ${
|
|
483
|
-
- ${
|
|
484
|
-
- ${
|
|
640
|
+
- ${wikiRoot}/wiki.md - protocol for future agents
|
|
641
|
+
- ${wikiRoot}/index.md - catalog grouped by type
|
|
642
|
+
- ${wikiRoot}/log.md - append-only activity log
|
|
643
|
+
- ${wikiRoot}/STATUS.md - plain-English health summary
|
|
644
|
+
- ${wikiRoot}/people/ - one page per human
|
|
645
|
+
- ${wikiRoot}/systems/ - one page per tool, table, dashboard, service, or product
|
|
646
|
+
- ${wikiRoot}/concepts/ - pattern and framework pages
|
|
647
|
+
- ${wikiRoot}/${WIKI_BRIEFS_SUBDIR}/ - cross-cutting briefs referencing 3+ pages
|
|
485
648
|
|
|
486
649
|
Page format:
|
|
487
650
|
---
|
|
@@ -497,7 +660,7 @@ tags: [tag1, tag2]
|
|
|
497
660
|
# Title
|
|
498
661
|
Body in markdown.
|
|
499
662
|
## Cross-References
|
|
500
|
-
- [[
|
|
663
|
+
- [[${wikiLinkRoot}/people/related.md]] - why related
|
|
501
664
|
|
|
502
665
|
Rules:
|
|
503
666
|
- Read every listed source fully before writing
|
|
@@ -505,20 +668,23 @@ Rules:
|
|
|
505
668
|
- Keep index.md, log.md, and STATUS.md current
|
|
506
669
|
- Flag contradictions directly instead of smoothing them over
|
|
507
670
|
- Never modify the raw source documents you ingested`;
|
|
671
|
+
}
|
|
508
672
|
|
|
509
|
-
function buildIngestPrompt(sourceValue) {
|
|
673
|
+
function buildIngestPrompt(sourceValue, mode = 'public') {
|
|
674
|
+
const wikiRoot = getWikiRoot(mode);
|
|
675
|
+
const wikiLinkRoot = getWikiLinkRoot(mode);
|
|
510
676
|
return `Atris wiki ingest: ${formatSourceList(sourceValue)}
|
|
511
|
-
${
|
|
677
|
+
${buildWikiSchema(mode)}
|
|
512
678
|
|
|
513
679
|
Workflow:
|
|
514
680
|
1. Read every source in: ${sourceValue}
|
|
515
|
-
2. Ensure ${
|
|
681
|
+
2. Ensure ${wikiRoot}/ exists with wiki.md, index.md, log.md, STATUS.md, and the 3 page subfolders
|
|
516
682
|
3. Extract people, systems, and concepts worth preserving
|
|
517
|
-
4. Create or update pages under ${
|
|
518
|
-
5. Add cross-references using [[
|
|
519
|
-
6. Update ${
|
|
520
|
-
7. Append an INGEST entry to ${
|
|
521
|
-
8. Refresh ${
|
|
683
|
+
4. Create or update pages under ${wikiRoot}/, merging with existing facts instead of replacing them
|
|
684
|
+
5. Add cross-references using [[${wikiLinkRoot}/...]] links
|
|
685
|
+
6. Update ${wikiRoot}/index.md with one-line descriptions of touched pages
|
|
686
|
+
7. Append an INGEST entry to ${wikiRoot}/log.md under today's date
|
|
687
|
+
8. Refresh ${wikiRoot}/STATUS.md in plain English for a non-technical reader
|
|
522
688
|
|
|
523
689
|
Quality bar:
|
|
524
690
|
- Ask clarifying questions if the source is ambiguous
|
|
@@ -527,18 +693,20 @@ Quality bar:
|
|
|
527
693
|
- Leave the wiki sharper than you found it`;
|
|
528
694
|
}
|
|
529
695
|
|
|
530
|
-
function buildQueryPrompt(question) {
|
|
696
|
+
function buildQueryPrompt(question, mode = 'public') {
|
|
697
|
+
const wikiRoot = getWikiRoot(mode);
|
|
531
698
|
return `Atris wiki query: ${question}
|
|
532
699
|
|
|
533
|
-
Read ${
|
|
534
|
-
Answer from the wiki with direct references to page paths under ${
|
|
700
|
+
Read ${wikiRoot}/index.md first, then the most relevant pages.
|
|
701
|
+
Answer from the wiki with direct references to page paths under ${wikiRoot}/.
|
|
535
702
|
If the answer reveals a reusable insight, offer to save it as a brief page.`;
|
|
536
703
|
}
|
|
537
704
|
|
|
538
|
-
function buildLintPrompt() {
|
|
705
|
+
function buildLintPrompt(mode = 'public') {
|
|
706
|
+
const wikiRoot = getWikiRoot(mode);
|
|
539
707
|
return `Atris wiki lint pass
|
|
540
708
|
|
|
541
|
-
Read ${
|
|
709
|
+
Read ${wikiRoot}/index.md, crawl the referenced pages, and inspect the local wiki.
|
|
542
710
|
|
|
543
711
|
Checks:
|
|
544
712
|
1. Every page referenced by index.md exists
|
|
@@ -546,8 +714,8 @@ Checks:
|
|
|
546
714
|
3. Orphan pages are listed
|
|
547
715
|
4. Contradictions are called out plainly
|
|
548
716
|
5. Gaps worth ingesting next are listed concretely
|
|
549
|
-
6. ${
|
|
550
|
-
7. ${
|
|
717
|
+
6. ${wikiRoot}/STATUS.md is rewritten in plain English
|
|
718
|
+
7. ${wikiRoot}/log.md gets a LINT entry under today's date
|
|
551
719
|
|
|
552
720
|
Output:
|
|
553
721
|
- Clear summary for a non-technical reader
|
|
@@ -557,12 +725,18 @@ Output:
|
|
|
557
725
|
|
|
558
726
|
module.exports = {
|
|
559
727
|
WIKI_ROOT,
|
|
728
|
+
CONTEXT_ROOT,
|
|
729
|
+
PRIVATE_WIKI_ROOT,
|
|
730
|
+
PRIVATE_CONTEXT_ROOT,
|
|
560
731
|
LEGACY_WIKI_ROOT,
|
|
561
732
|
WIKI_SUBDIRS,
|
|
562
733
|
WIKI_CONTENT_SUBDIRS,
|
|
563
|
-
WIKI_SCHEMA,
|
|
734
|
+
WIKI_SCHEMA: buildWikiSchema(),
|
|
564
735
|
WIKI_STATUS_FILE,
|
|
736
|
+
getWikiRoot,
|
|
737
|
+
getContextRoot,
|
|
565
738
|
ensureWikiScaffold,
|
|
739
|
+
ensureContextScaffold,
|
|
566
740
|
findLocalWikiDir,
|
|
567
741
|
normalizeWikiOnlyPrefix,
|
|
568
742
|
readWikiStatus,
|
|
@@ -570,6 +744,7 @@ module.exports = {
|
|
|
570
744
|
findStaleWikiPages,
|
|
571
745
|
findWikiOrphans,
|
|
572
746
|
findSuggestedSources,
|
|
747
|
+
stageWikiIngest,
|
|
573
748
|
writeWikiStatus,
|
|
574
749
|
appendWikiLog,
|
|
575
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/",
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
"atris/skills/"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
|
-
"test": "node --test"
|
|
36
|
+
"test": "node --test",
|
|
37
|
+
"prepare": "cp scripts/pre-commit .git/hooks/pre-commit 2>/dev/null && chmod +x .git/hooks/pre-commit || true"
|
|
36
38
|
},
|
|
37
39
|
"keywords": [
|
|
38
40
|
"atrisdev",
|
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));
|