atris 2.6.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +124 -34
  2. package/atris/CLAUDE.md +5 -1
  3. package/atris/atris.md +4 -0
  4. package/atris/features/README.md +24 -0
  5. package/atris/skills/autopilot/SKILL.md +74 -75
  6. package/atris/skills/endgame/SKILL.md +179 -0
  7. package/atris/skills/flow/SKILL.md +121 -0
  8. package/atris/skills/improve/SKILL.md +84 -0
  9. package/atris/skills/loop/SKILL.md +72 -0
  10. package/atris/skills/wiki/SKILL.md +61 -0
  11. package/atris/team/executor/MEMBER.md +10 -4
  12. package/atris/team/navigator/MEMBER.md +2 -0
  13. package/atris/team/validator/MEMBER.md +8 -5
  14. package/atris.md +33 -0
  15. package/bin/atris.js +210 -41
  16. package/commands/activate.js +28 -2
  17. package/commands/align.js +720 -0
  18. package/commands/auth.js +75 -2
  19. package/commands/autopilot.js +1213 -270
  20. package/commands/browse.js +100 -0
  21. package/commands/business.js +785 -12
  22. package/commands/clean.js +107 -2
  23. package/commands/computer.js +429 -0
  24. package/commands/context-sync.js +78 -8
  25. package/commands/experiments.js +351 -0
  26. package/commands/feedback.js +150 -0
  27. package/commands/fleet.js +395 -0
  28. package/commands/fork.js +127 -0
  29. package/commands/init.js +50 -1
  30. package/commands/learn.js +407 -0
  31. package/commands/lifecycle.js +94 -0
  32. package/commands/loop.js +114 -0
  33. package/commands/publish.js +129 -0
  34. package/commands/pull.js +369 -38
  35. package/commands/push.js +283 -246
  36. package/commands/review.js +149 -0
  37. package/commands/run.js +76 -43
  38. package/commands/serve.js +360 -0
  39. package/commands/setup.js +1 -1
  40. package/commands/soul.js +381 -0
  41. package/commands/status.js +119 -1
  42. package/commands/sync.js +147 -1
  43. package/commands/terminal.js +201 -0
  44. package/commands/wiki.js +376 -0
  45. package/commands/workflow.js +191 -74
  46. package/commands/workspace-clean.js +3 -3
  47. package/lib/endstate.js +259 -0
  48. package/lib/learnings.js +235 -0
  49. package/lib/manifest.js +1 -0
  50. package/lib/todo.js +9 -5
  51. package/lib/wiki.js +578 -0
  52. package/package.json +2 -2
  53. package/utils/api.js +40 -35
  54. package/utils/auth.js +1 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * atris terminal <business> <command...> [--timeout N]
3
+ *
4
+ * Run a shell command directly on a business EC2 workspace via the warm runner.
5
+ * This is the load-bearing primitive for fast bulk ops — one bash call beats
6
+ * hundreds of rate-limited individual file API calls.
7
+ *
8
+ * SAFETY:
9
+ * - Auto-wakes the EC2 computer (the rule: never operate on cache)
10
+ * - Refuses commands longer than 10000 chars (matches backend limit)
11
+ * - Caps timeout at 120s (matches backend limit)
12
+ * - Prints stdout, stderr, and exit_code so the caller knows what happened
13
+ *
14
+ * USAGE:
15
+ * atris terminal pallet "ls /workspace/atris/"
16
+ * atris terminal pallet "find /workspace -name '*.md' | wc -l" --timeout 60
17
+ * atris terminal "rm -rf /workspace/cruft" # auto-detects business from .atris/business.json
18
+ *
19
+ * Discovered the /terminal endpoint during overnight pallet cleanup — bulk
20
+ * deleting 401 files via individual /file DELETE calls hit the rate limit
21
+ * after request 60 and would have taken hours. One `rm -rf` via /terminal
22
+ * finished in 1 second.
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { loadCredentials } = require('../utils/auth');
28
+ const { apiRequestJson } = require('../utils/api');
29
+ const { loadBusinesses, saveBusinesses } = require('./business');
30
+
31
+ function sleep(ms) {
32
+ return new Promise((r) => setTimeout(r, ms));
33
+ }
34
+
35
+ async function ensureAwake(token, businessId, maxWaitSec = 90) {
36
+ const status = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token });
37
+ if (status.ok && status.data && status.data.status === 'running' && status.data.endpoint) {
38
+ return true;
39
+ }
40
+ process.stdout.write(' Waking EC2 computer... ');
41
+ await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token });
42
+ const start = Date.now();
43
+ while (Date.now() - start < maxWaitSec * 1000) {
44
+ await sleep(3000);
45
+ const s = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token });
46
+ if (s.ok && s.data && s.data.status === 'running' && s.data.endpoint) {
47
+ const elapsed = Math.floor((Date.now() - start) / 1000);
48
+ console.log(`awake (${elapsed}s)`);
49
+ return true;
50
+ }
51
+ }
52
+ console.log('timeout');
53
+ return false;
54
+ }
55
+
56
+ async function resolveBusiness(token, slug) {
57
+ const businesses = loadBusinesses();
58
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
59
+ if (list.ok) {
60
+ const match = (list.data || []).find(
61
+ (b) => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase()
62
+ );
63
+ if (!match) return null;
64
+ businesses[slug] = {
65
+ business_id: match.id,
66
+ workspace_id: match.workspace_id,
67
+ name: match.name,
68
+ slug: match.slug,
69
+ added_at: new Date().toISOString(),
70
+ };
71
+ saveBusinesses(businesses);
72
+ return { businessId: match.id, workspaceId: match.workspace_id, businessName: match.name };
73
+ }
74
+ if (businesses[slug]) {
75
+ return {
76
+ businessId: businesses[slug].business_id,
77
+ workspaceId: businesses[slug].workspace_id,
78
+ businessName: businesses[slug].name || slug,
79
+ };
80
+ }
81
+ return null;
82
+ }
83
+
84
+ async function terminalAtris() {
85
+ // Parse args. Three forms:
86
+ // atris terminal <business> <command...>
87
+ // atris terminal <command...> (auto-detect business)
88
+ // atris terminal --help
89
+ const args = process.argv.slice(3);
90
+
91
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
92
+ console.log('Usage: atris terminal [business] <command> [--timeout N]');
93
+ console.log('');
94
+ console.log(' atris terminal pallet "ls /workspace/atris/"');
95
+ console.log(' atris terminal "find /workspace -name \\"*.md\\"" # auto-detect business');
96
+ console.log(' atris terminal pallet "rm -rf /workspace/cruft" --timeout 30');
97
+ console.log('');
98
+ console.log(' --timeout N seconds to wait for the command (default 30, max 120)');
99
+ process.exit(0);
100
+ }
101
+
102
+ // Parse --timeout
103
+ let timeoutSec = 30;
104
+ const tIdx = args.indexOf('--timeout');
105
+ if (tIdx !== -1 && args[tIdx + 1]) {
106
+ const parsed = parseInt(args[tIdx + 1], 10);
107
+ if (!isNaN(parsed)) timeoutSec = Math.min(120, Math.max(1, parsed));
108
+ args.splice(tIdx, 2);
109
+ }
110
+
111
+ // Try to detect: if the first arg looks like a known slug (no quotes, no spaces),
112
+ // treat it as the business and the rest as the command.
113
+ // Otherwise auto-detect from .atris/business.json.
114
+ let slug = null;
115
+ let command = null;
116
+
117
+ const bizFile = path.join(process.cwd(), '.atris', 'business.json');
118
+ const cwdSlug = (() => {
119
+ if (!fs.existsSync(bizFile)) return null;
120
+ try { return JSON.parse(fs.readFileSync(bizFile, 'utf8')).slug; } catch { return null; }
121
+ })();
122
+
123
+ // If first arg is a single word with no shell metacharacters, it might be a slug
124
+ const firstLooksLikeSlug = args[0] && /^[a-z0-9-]+$/i.test(args[0]) && !args[0].includes(' ');
125
+
126
+ if (firstLooksLikeSlug && args.length > 1) {
127
+ slug = args[0];
128
+ command = args.slice(1).join(' ');
129
+ } else if (cwdSlug) {
130
+ slug = cwdSlug;
131
+ command = args.join(' ');
132
+ } else if (firstLooksLikeSlug && args.length === 1) {
133
+ // First (and only) arg is a slug-shaped word — could be the slug with no command
134
+ console.error('Missing command. Usage: atris terminal <business> <command>');
135
+ process.exit(1);
136
+ } else {
137
+ console.error('Cannot determine business. Run from inside a workspace, or pass slug as first arg.');
138
+ process.exit(1);
139
+ }
140
+
141
+ if (!command || command.length === 0) {
142
+ console.error('Missing command. Usage: atris terminal <business> <command>');
143
+ process.exit(1);
144
+ }
145
+ if (command.length > 10000) {
146
+ console.error(`Command too long (${command.length} chars, max 10000)`);
147
+ process.exit(1);
148
+ }
149
+
150
+ const creds = loadCredentials();
151
+ if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
152
+
153
+ const biz = await resolveBusiness(creds.token, slug);
154
+ if (!biz) { console.error(`Business "${slug}" not found.`); process.exit(1); }
155
+ if (!biz.workspaceId) { console.error(`Business "${slug}" has no workspace.`); process.exit(1); }
156
+
157
+ // Auto-wake (the rule)
158
+ const awake = await ensureAwake(creds.token, biz.businessId);
159
+ if (!awake) {
160
+ console.error(' EC2 computer did not become ready in time. Aborting.');
161
+ process.exit(1);
162
+ }
163
+
164
+ // Execute the command
165
+ const result = await apiRequestJson(
166
+ `/business/${biz.businessId}/workspaces/${biz.workspaceId}/terminal`,
167
+ {
168
+ method: 'POST',
169
+ token: creds.token,
170
+ body: { command, timeout: timeoutSec },
171
+ timeoutMs: (timeoutSec + 10) * 1000,
172
+ }
173
+ );
174
+
175
+ if (!result.ok) {
176
+ console.error(`\n✗ Terminal call failed: ${result.errorMessage || result.error || result.status}`);
177
+ process.exit(1);
178
+ }
179
+
180
+ const body = result.data || {};
181
+ const stdout = body.stdout || '';
182
+ const stderr = body.stderr || '';
183
+ const exitCode = body.exit_code !== undefined ? body.exit_code : '?';
184
+ const timedOut = body.timed_out === true;
185
+
186
+ if (stdout) process.stdout.write(stdout);
187
+ if (stderr) {
188
+ if (stdout && !stdout.endsWith('\n')) process.stdout.write('\n');
189
+ process.stderr.write(stderr);
190
+ if (!stderr.endsWith('\n')) process.stderr.write('\n');
191
+ }
192
+
193
+ if (timedOut) {
194
+ console.error(`\n[timed out after ${timeoutSec}s]`);
195
+ }
196
+
197
+ // Exit with the same code as the remote command (so shell pipelines work)
198
+ process.exit(typeof exitCode === 'number' ? exitCode : 0);
199
+ }
200
+
201
+ module.exports = { terminalAtris };
@@ -0,0 +1,376 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadCredentials } = require('../utils/auth');
4
+ const { apiRequestJson } = require('../utils/api');
5
+ const { loadBusinesses, saveBusinesses } = require('./business');
6
+ const {
7
+ WIKI_ROOT,
8
+ ensureWikiScaffold,
9
+ findLocalWikiDir,
10
+ buildIngestPrompt,
11
+ buildQueryPrompt,
12
+ buildLintPrompt,
13
+ } = require('../lib/wiki');
14
+
15
+ function autoDetectSlug() {
16
+ const bizFile = path.join(process.cwd(), '.atris', 'business.json');
17
+ if (!fs.existsSync(bizFile)) return null;
18
+ try {
19
+ return JSON.parse(fs.readFileSync(bizFile, 'utf8')).slug || null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function parseCloudArgs(args) {
26
+ if (args.length >= 2 && /^[a-z][a-z0-9-]*$/i.test(args[0])) {
27
+ return [args[0], args.slice(1).join(' ')];
28
+ }
29
+ return [autoDetectSlug(), args.join(' ')];
30
+ }
31
+
32
+ function parseModeArgs(args) {
33
+ const cloud = args.includes('--cloud');
34
+ return {
35
+ mode: cloud ? 'cloud' : 'local',
36
+ args: args.filter((arg) => arg !== '--cloud' && arg !== '--local'),
37
+ };
38
+ }
39
+
40
+ function requireCreds() {
41
+ const creds = loadCredentials();
42
+ if (!creds?.token) {
43
+ console.error('Not logged in. Run: atris login');
44
+ process.exit(1);
45
+ }
46
+ return creds;
47
+ }
48
+
49
+ async function resolveBusiness(slug, token) {
50
+ if (!slug) {
51
+ console.error('No business specified. Pass <slug> or run from a workspace folder.');
52
+ process.exit(1);
53
+ }
54
+
55
+ const cache = loadBusinesses();
56
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
57
+ if (list.ok) {
58
+ const match = (list.data || []).find((business) => business.slug === slug || business.name?.toLowerCase() === slug.toLowerCase());
59
+ if (!match) {
60
+ console.error(`Business "${slug}" not found.`);
61
+ process.exit(1);
62
+ }
63
+ cache[slug] = {
64
+ business_id: match.id,
65
+ workspace_id: match.workspace_id,
66
+ name: match.name,
67
+ slug: match.slug,
68
+ added_at: new Date().toISOString(),
69
+ };
70
+ saveBusinesses(cache);
71
+ return match;
72
+ }
73
+
74
+ if (cache[slug]) {
75
+ return {
76
+ id: cache[slug].business_id,
77
+ workspace_id: cache[slug].workspace_id,
78
+ name: cache[slug].name || slug,
79
+ };
80
+ }
81
+
82
+ console.error(`Failed to reach API and no cached business for "${slug}".`);
83
+ process.exit(1);
84
+ }
85
+
86
+ async function runChat(business, prompt, token) {
87
+ const start = await apiRequestJson(`/business/${business.id}/chat`, {
88
+ method: 'POST',
89
+ token,
90
+ body: { message: prompt, workspace_id: business.workspace_id },
91
+ });
92
+
93
+ if (!start.ok) {
94
+ console.error(`Chat failed: ${start.error || start.status}`);
95
+ process.exit(1);
96
+ }
97
+
98
+ const executionId = start.data?.execution_id;
99
+ if (!executionId) {
100
+ console.error('No execution_id from server.');
101
+ process.exit(1);
102
+ }
103
+
104
+ let fromIndex = 0;
105
+ let errors = 0;
106
+ while (true) {
107
+ await new Promise((resolve) => setTimeout(resolve, 1500));
108
+ const events = await apiRequestJson(
109
+ `/business/${business.id}/chat/events?execution_id=${executionId}&workspace_id=${business.workspace_id}&from_index=${fromIndex}`,
110
+ { method: 'GET', token, timeoutMs: 60000 }
111
+ );
112
+
113
+ if (!events.ok) {
114
+ if (++errors >= 5) {
115
+ console.error('\nLost connection to AI computer.');
116
+ return;
117
+ }
118
+ continue;
119
+ }
120
+
121
+ errors = 0;
122
+ let done = false;
123
+ for (const event of (events.data?.events || [])) {
124
+ fromIndex++;
125
+ if (event.type === 'assistant_text' && event.content) {
126
+ process.stdout.write(event.content);
127
+ } else if (event.type === 'tool_use' && event.tool) {
128
+ const argument = event.input?.file_path || event.input?.path || event.input?.pattern || event.input?.command || '';
129
+ if (argument) {
130
+ console.log(`\n [${event.tool}] ${String(argument).slice(0, 100)}`);
131
+ }
132
+ } else if (event.type === 'complete' || event.type === 'error') {
133
+ done = true;
134
+ break;
135
+ }
136
+ }
137
+
138
+ if (done || ['completed', 'error'].includes(events.data?.status)) {
139
+ console.log('');
140
+ return;
141
+ }
142
+ }
143
+ }
144
+
145
+ function printLocalPrompt(title, prompt, details = []) {
146
+ console.log('');
147
+ console.log(title);
148
+ console.log(`Target: ${WIKI_ROOT}`);
149
+ details.forEach((detail) => console.log(detail));
150
+ console.log('');
151
+ console.log('Prompt for the current coding agent:');
152
+ console.log('');
153
+ console.log(prompt);
154
+ console.log('');
155
+ }
156
+
157
+ async function wikiIngest(mode, slug, sourceValue) {
158
+ if (!sourceValue) {
159
+ console.error('Usage: atris wiki ingest [business] <path>');
160
+ process.exit(1);
161
+ }
162
+
163
+ if (mode === 'local') {
164
+ const wikiDir = ensureWikiScaffold();
165
+ printLocalPrompt('Local wiki ingest', buildIngestPrompt(sourceValue), [
166
+ `Wiki dir: ${wikiDir}`,
167
+ `Sources: ${sourceValue}`,
168
+ ]);
169
+ return;
170
+ }
171
+
172
+ const creds = requireCreds();
173
+ const business = await resolveBusiness(slug, creds.token);
174
+ console.log(`\nIngesting ${sourceValue} into ${business.name}...\n`);
175
+ await runChat(business, buildIngestPrompt(sourceValue), creds.token);
176
+ console.log('\nDone. Run `atris pull --only wiki` to sync atris/wiki locally.');
177
+ }
178
+
179
+ async function wikiQuery(mode, slug, question) {
180
+ if (!question) {
181
+ console.error('Usage: atris wiki query [business] "question"');
182
+ process.exit(1);
183
+ }
184
+
185
+ if (mode === 'local') {
186
+ const wikiDir = findLocalWikiDir(process.cwd(), slug);
187
+ if (!wikiDir) {
188
+ console.error('No local atris/wiki found. Run: atris ingest <path>');
189
+ process.exit(1);
190
+ }
191
+ printLocalPrompt('Local wiki query', buildQueryPrompt(question), [
192
+ `Wiki dir: ${wikiDir}`,
193
+ `Question: ${question}`,
194
+ ]);
195
+ return;
196
+ }
197
+
198
+ const creds = requireCreds();
199
+ const business = await resolveBusiness(slug, creds.token);
200
+ await runChat(business, buildQueryPrompt(question), creds.token);
201
+ }
202
+
203
+ async function wikiLint(mode, slug) {
204
+ if (mode === 'local') {
205
+ const wikiDir = findLocalWikiDir(process.cwd(), slug);
206
+ if (!wikiDir) {
207
+ console.error('No local atris/wiki found. Run: atris ingest <path>');
208
+ process.exit(1);
209
+ }
210
+ printLocalPrompt('Local wiki lint', buildLintPrompt(), [`Wiki dir: ${wikiDir}`]);
211
+ return;
212
+ }
213
+
214
+ const creds = requireCreds();
215
+ const business = await resolveBusiness(slug, creds.token);
216
+ console.log(`\nLinting ${business.name} wiki...\n`);
217
+ await runChat(business, buildLintPrompt(), creds.token);
218
+ }
219
+
220
+ function wikiSearch(slug, query) {
221
+ if (!query) {
222
+ console.error('Usage: atris wiki search [business] <term>');
223
+ process.exit(1);
224
+ }
225
+
226
+ const wikiDir = findLocalWikiDir(process.cwd(), slug);
227
+ if (!wikiDir) {
228
+ console.error(`No local wiki found. Run: atris pull ${slug || ''} --only wiki`);
229
+ process.exit(1);
230
+ }
231
+
232
+ const indexPath = path.join(wikiDir, 'index.md');
233
+ if (!fs.existsSync(indexPath)) {
234
+ console.error('No wiki index found. Run an ingest first.');
235
+ process.exit(1);
236
+ }
237
+
238
+ const lowered = query.toLowerCase();
239
+ const matches = fs.readFileSync(indexPath, 'utf8')
240
+ .split('\n')
241
+ .filter((line) => line.trim().startsWith('-') && line.toLowerCase().includes(lowered));
242
+
243
+ if (matches.length === 0) {
244
+ console.log(`No matches for "${query}".`);
245
+ return;
246
+ }
247
+
248
+ console.log(`\n${matches.length} match${matches.length === 1 ? '' : 'es'}:\n`);
249
+ matches.forEach((match) => console.log(match));
250
+ console.log('');
251
+ }
252
+
253
+ function wikiLog(slug, limit) {
254
+ const wikiDir = findLocalWikiDir(process.cwd(), slug);
255
+ if (!wikiDir) {
256
+ console.error(`No local wiki found. Run: atris pull ${slug || ''} --only wiki`);
257
+ process.exit(1);
258
+ }
259
+
260
+ const logPath = path.join(wikiDir, 'log.md');
261
+ if (!fs.existsSync(logPath)) {
262
+ console.log('No wiki log yet.');
263
+ return;
264
+ }
265
+
266
+ const lines = fs.readFileSync(logPath, 'utf8').split('\n');
267
+ const entries = lines.filter((line) => /^## \d{4}-\d{2}-\d{2}/.test(line) || /^- \d{1,2}:\d{2}/.test(line) || /^ - /.test(line));
268
+ if (entries.length === 0) {
269
+ console.log('No wiki log entries yet.');
270
+ return;
271
+ }
272
+
273
+ const output = [];
274
+ let bullets = 0;
275
+ let firstIndex = entries.length;
276
+ for (let index = entries.length - 1; index >= 0; index--) {
277
+ if (bullets >= limit && /^- /.test(entries[index])) break;
278
+ output.unshift(entries[index]);
279
+ firstIndex = index;
280
+ if (/^- /.test(entries[index])) bullets++;
281
+ }
282
+
283
+ if (output.length > 0 && !/^## /.test(output[0])) {
284
+ for (let index = firstIndex - 1; index >= 0; index--) {
285
+ if (/^## /.test(entries[index])) {
286
+ output.unshift(entries[index]);
287
+ break;
288
+ }
289
+ }
290
+ }
291
+
292
+ console.log('');
293
+ output.forEach((line) => console.log(line));
294
+ console.log('');
295
+ }
296
+
297
+ async function wikiCommand(subcommand, ...args) {
298
+ const { mode, args: cleanArgs } = parseModeArgs(args);
299
+
300
+ switch (subcommand) {
301
+ case 'ingest': {
302
+ const [slug, sourceValue] = mode === 'cloud' ? parseCloudArgs(cleanArgs) : [null, cleanArgs.join(' ')];
303
+ await wikiIngest(mode, slug, sourceValue);
304
+ break;
305
+ }
306
+ case 'query': {
307
+ const [slug, question] = mode === 'cloud' ? parseCloudArgs(cleanArgs) : [null, cleanArgs.join(' ')];
308
+ await wikiQuery(mode, slug, question);
309
+ break;
310
+ }
311
+ case 'lint': {
312
+ const slug = mode === 'cloud' ? (cleanArgs[0] || autoDetectSlug()) : null;
313
+ await wikiLint(mode, slug);
314
+ break;
315
+ }
316
+ case 'search': {
317
+ const [slug, query] = parseCloudArgs(cleanArgs);
318
+ wikiSearch(slug, query);
319
+ break;
320
+ }
321
+ case 'log': {
322
+ let slug;
323
+ let limit;
324
+ if (cleanArgs.length === 0) {
325
+ slug = autoDetectSlug();
326
+ limit = 20;
327
+ } else if (cleanArgs.length === 1) {
328
+ if (/^\d+$/.test(cleanArgs[0])) {
329
+ slug = autoDetectSlug();
330
+ limit = parseInt(cleanArgs[0], 10);
331
+ } else {
332
+ slug = cleanArgs[0];
333
+ limit = 20;
334
+ }
335
+ } else {
336
+ slug = cleanArgs[0];
337
+ limit = parseInt(cleanArgs[1], 10) || 20;
338
+ }
339
+ wikiLog(slug, limit);
340
+ break;
341
+ }
342
+ case 'loop': {
343
+ if (mode === 'cloud') {
344
+ console.error('Cloud loop is not implemented yet. Run local `atris loop` first.');
345
+ process.exit(1);
346
+ }
347
+ const { loopAtris } = require('./loop');
348
+ await loopAtris(cleanArgs);
349
+ break;
350
+ }
351
+ default:
352
+ console.log('Usage: atris wiki <ingest|query|lint|search|log|loop> [business] [args]');
353
+ console.log('');
354
+ console.log(' ingest <path> Local-first ingest into atris/wiki/');
355
+ console.log(' query "question" Local-first query against atris/wiki/');
356
+ console.log(' lint Local-first lint for atris/wiki/');
357
+ console.log(' search [business] <term> Search local atris/wiki/index.md');
358
+ console.log(' log [business] [N] Show recent atris/wiki/log.md entries');
359
+ console.log(' loop Run local wiki upkeep analysis and refresh STATUS/log');
360
+ console.log('');
361
+ console.log('Flags:');
362
+ console.log(' --cloud Route ingest/query/lint to the cloud workspace');
363
+ console.log(' --local Be explicit about local mode');
364
+ console.log('');
365
+ console.log('Business is auto-detected from .atris/business.json for cloud mode if omitted.');
366
+ }
367
+ }
368
+
369
+ module.exports = {
370
+ wikiCommand,
371
+ wikiIngest,
372
+ wikiQuery,
373
+ wikiLint,
374
+ wikiSearch,
375
+ wikiLog,
376
+ };