atris 3.14.0 → 3.15.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/README.md CHANGED
@@ -157,6 +157,7 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
157
157
  - `atris task` keeps a local SQLite task plane for agents while `atris/TODO.md` remains the readable project board
158
158
  - `atris experiments` runs small test packs in `atris/experiments/`
159
159
  - `atris pull` and `atris push` sync cloud workspaces and journals
160
+ - `atris live` keeps a business brain fresh by checking/fixing the workspace, pushing local state, pulling cloud state, and pushing again after local changes go quiet
160
161
 
161
162
  ## Verifiable Feedback Loop
162
163
 
package/bin/atris.js CHANGED
@@ -270,6 +270,7 @@ function showHelp() {
270
270
  console.log('Sync:');
271
271
  console.log(' pull - Pull journals + member data from cloud');
272
272
  console.log(' push - Push workspace files to cloud');
273
+ console.log(' live - Keep a business brain fresh (doctor, pull, watch, push)');
273
274
  console.log(' clean-workspace <slug> - Analyze & remove junk files from a workspace (alias: cw)');
274
275
  console.log('');
275
276
  console.log('GitHub for Context:');
@@ -444,8 +445,8 @@ const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('
444
445
  // Check if this is a known command or natural language input
445
446
  const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
446
447
  'activate', '_activate', 'agent', 'chat', 'console', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
447
- 'clean', 'verify', 'search', 'skill', 'member', 'app', 'learn', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
448
- 'ingest', 'query', 'lint', 'loop', 'task',
448
+ 'clean', 'verify', 'search', 'skill', 'member', 'app', 'learn', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
449
+ 'ingest', 'query', 'lint', 'loop', 'task', 'aeo',
449
450
  'gmail', 'calendar', 'twitter', 'slack', 'integrations', 'setup', 'clean-workspace', 'cw',
450
451
  'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet'];
451
452
 
@@ -796,6 +797,11 @@ if (command === 'init') {
796
797
  Promise.resolve(require('../commands/task').run(process.argv.slice(3)))
797
798
  .then(() => process.exit(0))
798
799
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
800
+ } else if (command === 'aeo') {
801
+ // AEO: AI Engine Optimization — credit-metered citation drafting against the customer workspace.
802
+ Promise.resolve(require('../commands/aeo').run(process.argv.slice(3)))
803
+ .then(() => process.exit(0))
804
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
799
805
  } else if (command === 'agent') {
800
806
  agentAtris().then(() => process.exit(0)).catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
801
807
  } else if (command === 'log') {
@@ -1104,6 +1110,10 @@ if (command === 'init') {
1104
1110
  require('../commands/push').pushAtris()
1105
1111
  .then(() => process.exit(0))
1106
1112
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1113
+ } else if (command === 'live') {
1114
+ require('../commands/live').liveCommand()
1115
+ .then(() => process.exit(0))
1116
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1107
1117
  } else if (command === 'align') {
1108
1118
  require('../commands/align').alignAtris()
1109
1119
  .then(() => process.exit(0))
@@ -0,0 +1,197 @@
1
+ /**
2
+ * atris aeo — AI Engine Optimization commands
3
+ *
4
+ * atris aeo init # create entity-graph skeleton in workspace
5
+ * atris aeo draft "<topic>" [opts] # generate citation-optimized article (credit-metered)
6
+ *
7
+ * Hits the backend endpoints registered under:
8
+ * POST /api/business/{id}/workspaces/{ws}/aeo/init
9
+ * POST /api/business/{id}/workspaces/{ws}/aeo/draft
10
+ *
11
+ * Business resolution mirrors `atris terminal`: explicit --workspace slug,
12
+ * else cwd .atris/business.json. The endpoint itself takes care of running
13
+ * Claude Sonnet 4.6 with the 10 AEO rules and writing to /workspace/atris/aeo/drafts/.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { loadCredentials } = require('../utils/auth');
19
+ const { apiRequestJson } = require('../utils/api');
20
+ const { loadBusinesses, saveBusinesses } = require('./business');
21
+
22
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
23
+
24
+ async function ensureAwake(token, businessId, maxWaitSec = 90) {
25
+ const status = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token });
26
+ if (status.ok && status.data && status.data.status === 'running' && status.data.endpoint) return true;
27
+ process.stdout.write(' Waking EC2 computer... ');
28
+ await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token });
29
+ const start = Date.now();
30
+ while (Date.now() - start < maxWaitSec * 1000) {
31
+ await sleep(3000);
32
+ const s = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token });
33
+ if (s.ok && s.data && s.data.status === 'running' && s.data.endpoint) {
34
+ console.log(`awake (${Math.floor((Date.now() - start) / 1000)}s)`);
35
+ return true;
36
+ }
37
+ }
38
+ console.log('timeout');
39
+ return false;
40
+ }
41
+
42
+ async function resolveBusiness(token, slug) {
43
+ const businesses = loadBusinesses();
44
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
45
+ if (list.ok) {
46
+ const match = (list.data || []).find(
47
+ (b) => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase()
48
+ );
49
+ if (!match) return null;
50
+ businesses[slug] = {
51
+ business_id: match.id,
52
+ workspace_id: match.workspace_id,
53
+ name: match.name,
54
+ slug: match.slug,
55
+ added_at: new Date().toISOString(),
56
+ };
57
+ saveBusinesses(businesses);
58
+ return { businessId: match.id, workspaceId: match.workspace_id, businessName: match.name };
59
+ }
60
+ if (businesses[slug]) {
61
+ return {
62
+ businessId: businesses[slug].business_id,
63
+ workspaceId: businesses[slug].workspace_id,
64
+ businessName: businesses[slug].name || slug,
65
+ };
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function pickSlug(args) {
71
+ const wsIdx = args.findIndex((a) => a === '--workspace' || a === '-w');
72
+ if (wsIdx !== -1 && args[wsIdx + 1]) {
73
+ const slug = args[wsIdx + 1];
74
+ args.splice(wsIdx, 2);
75
+ return slug;
76
+ }
77
+ const bizFile = path.join(process.cwd(), '.atris', 'business.json');
78
+ if (fs.existsSync(bizFile)) {
79
+ try { return JSON.parse(fs.readFileSync(bizFile, 'utf8')).slug; } catch { /* ignore */ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function printHelp() {
85
+ console.log('Usage:');
86
+ console.log(' atris aeo init [--workspace <slug>]');
87
+ console.log(' atris aeo draft "<topic>" [--workspace <slug>] [--queries q1,q2] [--slug X] [--url URL]');
88
+ console.log('');
89
+ console.log('Examples:');
90
+ console.log(' atris aeo init');
91
+ console.log(' atris aeo draft "what is pallet" --queries "what is pallet,best freight platform"');
92
+ console.log(' atris aeo draft "how does atris work" --workspace doordash --slug atris-overview');
93
+ }
94
+
95
+ async function aeoInit(args) {
96
+ const slug = pickSlug(args);
97
+ if (!slug) {
98
+ console.error('Cannot determine business. Pass --workspace <slug> or run from a workspace.');
99
+ process.exit(1);
100
+ }
101
+ const creds = loadCredentials();
102
+ if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
103
+
104
+ const biz = await resolveBusiness(creds.token, slug);
105
+ if (!biz) { console.error(`Business "${slug}" not found.`); process.exit(1); }
106
+ if (!biz.workspaceId) { console.error(`Business "${slug}" has no workspace.`); process.exit(1); }
107
+
108
+ const awake = await ensureAwake(creds.token, biz.businessId);
109
+ if (!awake) { console.error(' EC2 computer did not become ready in time.'); process.exit(1); }
110
+
111
+ const result = await apiRequestJson(
112
+ `/business/${biz.businessId}/workspaces/${biz.workspaceId}/aeo/init`,
113
+ { method: 'POST', token: creds.token, body: {}, timeoutMs: 60000 }
114
+ );
115
+ if (!result.ok) {
116
+ console.error(`✗ aeo init failed: ${result.errorMessage || result.error || result.status}`);
117
+ process.exit(1);
118
+ }
119
+ const data = result.data || {};
120
+ const created = data.created || [];
121
+ const skipped = data.skipped || [];
122
+ console.log(`✓ AEO entity graph @ ${data.dir}`);
123
+ if (created.length) console.log(` created: ${created.map((p) => p.split('/').pop()).join(', ')}`);
124
+ if (skipped.length) console.log(` existed: ${skipped.map((p) => p.split('/').pop()).join(', ')}`);
125
+ }
126
+
127
+ async function aeoDraft(args) {
128
+ // Pull --slug, --url, --queries, --workspace; remainder is the topic.
129
+ const opts = {};
130
+ for (const k of ['slug', 'url', 'queries']) {
131
+ const i = args.findIndex((a) => a === `--${k}`);
132
+ if (i !== -1 && args[i + 1]) {
133
+ opts[k] = args[i + 1];
134
+ args.splice(i, 2);
135
+ }
136
+ }
137
+ const slug = pickSlug(args);
138
+ const topic = args.join(' ').trim();
139
+ if (!topic) {
140
+ console.error('Missing topic. Usage: atris aeo draft "<topic>"');
141
+ process.exit(1);
142
+ }
143
+ if (!slug) {
144
+ console.error('Cannot determine business. Pass --workspace <slug> or run from a workspace.');
145
+ process.exit(1);
146
+ }
147
+ const creds = loadCredentials();
148
+ if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
149
+
150
+ const biz = await resolveBusiness(creds.token, slug);
151
+ if (!biz) { console.error(`Business "${slug}" not found.`); process.exit(1); }
152
+ if (!biz.workspaceId) { console.error(`Business "${slug}" has no workspace.`); process.exit(1); }
153
+
154
+ const awake = await ensureAwake(creds.token, biz.businessId);
155
+ if (!awake) { console.error(' EC2 computer did not become ready in time.'); process.exit(1); }
156
+
157
+ const body = { topic };
158
+ if (opts.slug) body.slug = opts.slug;
159
+ if (opts.url) body.target_url = opts.url;
160
+ if (opts.queries) body.target_queries = opts.queries.split(',').map((s) => s.trim()).filter(Boolean);
161
+
162
+ process.stdout.write(`Drafting "${topic}" for ${biz.businessName}... `);
163
+ const t0 = Date.now();
164
+ const result = await apiRequestJson(
165
+ `/business/${biz.businessId}/workspaces/${biz.workspaceId}/aeo/draft`,
166
+ { method: 'POST', token: creds.token, body, timeoutMs: 180000 }
167
+ );
168
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
169
+ if (!result.ok) {
170
+ console.log('failed');
171
+ console.error(`✗ aeo draft failed (${result.status}): ${result.errorMessage || result.error}`);
172
+ process.exit(1);
173
+ }
174
+ const data = result.data || {};
175
+ console.log(`done (${elapsed}s)`);
176
+ console.log('');
177
+ console.log(` path: ${data.path}`);
178
+ console.log(` self-score: ${data.self_score ?? '?'}/10`);
179
+ console.log(` credits: ${data.credits_charged ?? '?'}`);
180
+ console.log(` tokens: in=${data.tokens?.input ?? '?'} out=${data.tokens?.output ?? '?'}`);
181
+ console.log(` entity graph: entities=${data.entity_graph?.has_entities ? 'y' : 'n'} defs=${data.entity_graph?.has_definitions ? 'y' : 'n'} stats=${data.entity_graph?.has_stats ? 'y' : 'n'}`);
182
+ if (data.overlay_active) console.log(` overlay: active (${data.overlay_lines} lines)`);
183
+ if (data.hint) console.log(` hint: ${data.hint}`);
184
+ }
185
+
186
+ async function run(args = []) {
187
+ const sub = args[0];
188
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') return printHelp();
189
+ const rest = args.slice(1);
190
+ if (sub === 'init') return aeoInit(rest);
191
+ if (sub === 'draft') return aeoDraft(rest);
192
+ console.error(`Unknown aeo subcommand: ${sub}`);
193
+ printHelp();
194
+ process.exit(1);
195
+ }
196
+
197
+ module.exports = { run };
@@ -0,0 +1,289 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+
5
+ const DEFAULT_INTERVAL_SEC = 120;
6
+ const DEFAULT_DEBOUNCE_SEC = 30;
7
+ const DEFAULT_TIMEOUT_SEC = 600;
8
+
9
+ const IGNORED_DIRS = new Set(['.git', 'node_modules', 'dist', 'build', 'release', '.next', '__pycache__']);
10
+ const IGNORED_FILES = new Set(['.DS_Store']);
11
+
12
+ function parseNumberFlag(args, name, fallback) {
13
+ const eq = args.find((arg) => arg.startsWith(`--${name}=`));
14
+ if (eq) {
15
+ const value = Number(eq.slice(name.length + 3));
16
+ return Number.isFinite(value) && value > 0 ? value : fallback;
17
+ }
18
+ const idx = args.indexOf(`--${name}`);
19
+ if (idx !== -1 && args[idx + 1]) {
20
+ const value = Number(args[idx + 1]);
21
+ return Number.isFinite(value) && value > 0 ? value : fallback;
22
+ }
23
+ return fallback;
24
+ }
25
+
26
+ function parseStringFlag(args, name) {
27
+ const eq = args.find((arg) => arg.startsWith(`--${name}=`));
28
+ if (eq) return eq.slice(name.length + 3);
29
+ const idx = args.indexOf(`--${name}`);
30
+ if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('-')) return args[idx + 1];
31
+ return null;
32
+ }
33
+
34
+ function firstPositionalArg(args) {
35
+ const flagsWithValues = new Set(['--interval', '--debounce', '--timeout', '--only', '--root']);
36
+ for (let i = 0; i < args.length; i++) {
37
+ const arg = args[i];
38
+ if (flagsWithValues.has(arg)) {
39
+ i += 1;
40
+ continue;
41
+ }
42
+ if (arg.startsWith('-')) continue;
43
+ return arg;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function readBusinessSlugFromCwd(cwd) {
49
+ const file = path.join(cwd, '.atris', 'business.json');
50
+ if (!fs.existsSync(file)) return null;
51
+ try {
52
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
53
+ return data.slug || data.canonical_slug || data.name || null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function parseLiveOptions(args, cwd = process.cwd()) {
60
+ const first = firstPositionalArg(args);
61
+ return {
62
+ slug: first || readBusinessSlugFromCwd(cwd),
63
+ once: args.includes('--once'),
64
+ dryRun: args.includes('--dry-run'),
65
+ noDoctor: args.includes('--no-doctor'),
66
+ noPush: args.includes('--no-push'),
67
+ intervalSec: parseNumberFlag(args, 'interval', DEFAULT_INTERVAL_SEC),
68
+ debounceSec: parseNumberFlag(args, 'debounce', DEFAULT_DEBOUNCE_SEC),
69
+ timeoutSec: parseNumberFlag(args, 'timeout', DEFAULT_TIMEOUT_SEC),
70
+ only: parseStringFlag(args, 'only'),
71
+ root: parseStringFlag(args, 'root') || path.dirname(cwd),
72
+ cwd,
73
+ };
74
+ }
75
+
76
+ function printLiveHelp() {
77
+ console.log('Usage: atris live [business] [options]');
78
+ console.log('');
79
+ console.log('Keeps a business brain fresh: doctor, pull, watch local changes, push after quiet, and periodically pull.');
80
+ console.log('');
81
+ console.log('Examples:');
82
+ console.log(' atris live atris-labs');
83
+ console.log(' atris live --once');
84
+ console.log(' atris live atris-labs --dry-run');
85
+ console.log(' atris live atris-labs --interval=120 --debounce=30');
86
+ console.log('');
87
+ console.log('Options:');
88
+ console.log(' --once Run one freshness cycle and exit');
89
+ console.log(' --dry-run Print the plan without running pull/push');
90
+ console.log(' --interval <sec> Seconds between cloud pulls (default: 120)');
91
+ console.log(' --debounce <sec> Quiet seconds before pushing local changes (default: 30)');
92
+ console.log(' --timeout <sec> Pull timeout passed through to atris pull (default: 600)');
93
+ console.log(' --only <prefix> Limit pull/push to a path prefix');
94
+ console.log(' --no-doctor Skip business doctor --fix');
95
+ console.log(' --no-push Pull/watch only; never push');
96
+ }
97
+
98
+ function shouldIgnore(relativePath) {
99
+ if (!relativePath) return true;
100
+ const parts = relativePath.split(path.sep);
101
+ if (parts.some((part) => IGNORED_DIRS.has(part))) return true;
102
+ if (IGNORED_FILES.has(path.basename(relativePath))) return true;
103
+ if (relativePath.startsWith(path.join('.atris', 'state'))) return true;
104
+ return false;
105
+ }
106
+
107
+ function collectSnapshot(root) {
108
+ const snapshot = new Map();
109
+
110
+ function walk(dir) {
111
+ let entries;
112
+ try {
113
+ entries = fs.readdirSync(dir, { withFileTypes: true });
114
+ } catch {
115
+ return;
116
+ }
117
+
118
+ for (const entry of entries) {
119
+ const full = path.join(dir, entry.name);
120
+ const rel = path.relative(root, full);
121
+ if (shouldIgnore(rel)) continue;
122
+ if (entry.isDirectory()) {
123
+ walk(full);
124
+ } else if (entry.isFile()) {
125
+ try {
126
+ const stat = fs.statSync(full);
127
+ snapshot.set(rel, `${stat.size}:${Math.floor(stat.mtimeMs)}`);
128
+ } catch {
129
+ // Files can disappear while the operator is saving.
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ walk(root);
136
+ return snapshot;
137
+ }
138
+
139
+ function snapshotsDiffer(a, b) {
140
+ if (a.size !== b.size) return true;
141
+ for (const [key, value] of a.entries()) {
142
+ if (b.get(key) !== value) return true;
143
+ }
144
+ return false;
145
+ }
146
+
147
+ function commandLine(argv) {
148
+ return ['atris', ...argv].join(' ');
149
+ }
150
+
151
+ function runCli(argv, options) {
152
+ if (options.dryRun) {
153
+ console.log(` dry-run: ${commandLine(argv)}`);
154
+ return Promise.resolve({ status: 0 });
155
+ }
156
+
157
+ return new Promise((resolve, reject) => {
158
+ const child = spawn(process.execPath, [path.join(__dirname, '..', 'bin', 'atris.js'), ...argv], {
159
+ cwd: options.cwd,
160
+ stdio: 'inherit',
161
+ env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1' },
162
+ });
163
+ child.on('error', reject);
164
+ child.on('exit', (status) => {
165
+ if (status === 0) resolve({ status });
166
+ else reject(new Error(`${commandLine(argv)} exited ${status}`));
167
+ });
168
+ });
169
+ }
170
+
171
+ function buildPullArgs(options) {
172
+ const args = ['pull', options.slug, '--timeout', String(options.timeoutSec)];
173
+ if (options.only) args.push('--only', options.only);
174
+ return args;
175
+ }
176
+
177
+ function buildPushArgs(options) {
178
+ const args = ['push', options.slug, '--from', options.cwd];
179
+ if (options.only) args.push('--only', options.only);
180
+ return args;
181
+ }
182
+
183
+ async function runFreshnessCycle(options, reason) {
184
+ console.log('');
185
+ console.log(`atris live: ${reason}`);
186
+ if (!options.noDoctor) {
187
+ await runCli(['business', 'doctor', '--fix', '--root', options.root], options);
188
+ }
189
+ if (!options.noPush) {
190
+ await runCli(buildPushArgs(options), options);
191
+ }
192
+ await runCli(buildPullArgs(options), options);
193
+ }
194
+
195
+ async function liveCommand(args = process.argv.slice(3)) {
196
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
197
+ printLiveHelp();
198
+ return;
199
+ }
200
+
201
+ const options = parseLiveOptions(args);
202
+ if (!options.slug) {
203
+ console.error('Usage: atris live [business]');
204
+ console.error('Run inside a business workspace or pass a business slug.');
205
+ process.exit(1);
206
+ }
207
+
208
+ console.log('');
209
+ console.log(`Atris Live: ${options.slug}`);
210
+ console.log(` workspace: ${options.cwd}`);
211
+ console.log(` interval: ${options.intervalSec}s`);
212
+ console.log(` debounce: ${options.debounceSec}s`);
213
+ console.log(` push: ${options.noPush ? 'off' : 'on'}`);
214
+ if (options.only) console.log(` only: ${options.only}`);
215
+
216
+ if (options.dryRun) {
217
+ await runFreshnessCycle(options, 'planned startup cycle');
218
+ if (!options.once) console.log(` dry-run: would watch ${options.cwd} and sync every ${options.intervalSec}s`);
219
+ return;
220
+ }
221
+
222
+ await runFreshnessCycle(options, 'startup freshness cycle');
223
+ if (options.once) return;
224
+
225
+ console.log('');
226
+ console.log('Brain fresh. Watching for local changes. Press Ctrl+C to stop.');
227
+
228
+ let lastSnapshot = collectSnapshot(options.cwd);
229
+ let pendingPush = false;
230
+ let quietTicks = 0;
231
+ let running = false;
232
+
233
+ async function guarded(label, fn) {
234
+ if (running) return;
235
+ running = true;
236
+ try {
237
+ await fn();
238
+ lastSnapshot = collectSnapshot(options.cwd);
239
+ pendingPush = false;
240
+ quietTicks = 0;
241
+ } catch (err) {
242
+ console.error(`\natris live paused after ${label}: ${err.message || err}`);
243
+ console.error('Fix the issue, then restart `atris live`.');
244
+ process.exit(1);
245
+ } finally {
246
+ running = false;
247
+ }
248
+ }
249
+
250
+ setInterval(() => {
251
+ if (running) return;
252
+ const current = collectSnapshot(options.cwd);
253
+ if (snapshotsDiffer(lastSnapshot, current)) {
254
+ pendingPush = true;
255
+ quietTicks = 0;
256
+ lastSnapshot = current;
257
+ process.stdout.write('\rLocal brain changed. Waiting for quiet before push... ');
258
+ return;
259
+ }
260
+ if (pendingPush) {
261
+ quietTicks += 1;
262
+ if (quietTicks >= options.debounceSec) {
263
+ void guarded('push', async () => {
264
+ console.log('\nLocal brain quiet. Pushing fresh state...');
265
+ if (!options.noPush) await runCli(buildPushArgs(options), options);
266
+ });
267
+ }
268
+ }
269
+ }, 1000);
270
+
271
+ setInterval(() => {
272
+ if (pendingPush) {
273
+ process.stdout.write('\rLocal changes pending; skipping cloud pull until local brain is pushed... ');
274
+ return;
275
+ }
276
+ void guarded('periodic pull', async () => {
277
+ console.log('\nChecking cloud for fresher brain...');
278
+ await runCli(buildPullArgs(options), options);
279
+ });
280
+ }, options.intervalSec * 1000);
281
+ }
282
+
283
+ module.exports = {
284
+ collectSnapshot,
285
+ liveCommand,
286
+ parseLiveOptions,
287
+ shouldIgnore,
288
+ snapshotsDiffer,
289
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "description": "Atris — an operating system for intelligence. Integrates with any agent.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {