atris 3.12.1 → 3.14.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.
@@ -5,6 +5,7 @@
5
5
  * atris computer --cloud — Open CLOUD workspace mode
6
6
  * atris computer wake — Start the computer
7
7
  * atris computer sleep — Stop (files persist)
8
+ * atris computer card — Show the local computer card
8
9
  * atris computer run <command> — Run bash on EC2 (no LLM)
9
10
  * atris computer grep <pattern> — Search files on EC2
10
11
  * atris computer ls [path] — List files
@@ -52,8 +53,40 @@ const KNOWN_CHAT_COMMANDS = new Set([
52
53
  '/start',
53
54
  '/status',
54
55
  '/worker',
56
+ '/workflow',
55
57
  ]);
56
58
 
59
+ const CODEOPS_WORKFLOW_PROMPT = `
60
+ ## Atris CodeOps Workflow
61
+
62
+ You are running inside Atris CodeOps with full computer permissions (permission_mode=bypassPermissions).
63
+ Use those permissions to inspect, edit, test, commit, push, and open PRs when the task calls for it.
64
+
65
+ Do not behave like an open-ended chat.
66
+ Every coding or repo operation must follow the scientific workflow:
67
+ OBSERVE -> HYPOTHESIS -> PLAN -> ACTION -> VALIDATION -> EVIDENCE -> NEXT STATE.
68
+
69
+ For a new coding request, first show a concise PLAN with Files, Checks, Risk, and Merge policy.
70
+ If the user has not clearly approved execution, ask for approval before editing.
71
+ If the user explicitly says to execute, proceed after the concise plan.
72
+
73
+ After work, always report:
74
+ - edited_files
75
+ - commands_run
76
+ - validation_result
77
+ - evidence
78
+ - pr_url if any
79
+ - pr_state
80
+ - merge_state
81
+ - next_task
82
+
83
+ Use one of these next states:
84
+ planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.
85
+
86
+ Never hide failures.
87
+ A blocked check or missing permission is evidence, not success.
88
+ `.trim();
89
+
57
90
  function color(code, value) {
58
91
  if (process.env.NO_COLOR || !process.stdout.isTTY) return String(value);
59
92
  return `\x1b[${code}m${value}\x1b[0m`;
@@ -165,6 +198,7 @@ function printCloudHelp() {
165
198
  console.log(' /start Show the beginner flow again');
166
199
  console.log(' /help Show this menu');
167
200
  console.log(' /status Show cloud computer status');
201
+ console.log(' /workflow Show the CodeOps workflow contract');
168
202
  console.log(' /files [path] List files in the workspace');
169
203
  console.log(' /run <cmd> Run shell without the model');
170
204
  console.log(' /audit [n] Show recent runs, output, and charges');
@@ -201,6 +235,48 @@ function printCloudStartPanel(ctx, worker, model, billingLabel, authSummary = nu
201
235
  console.log(ui.dim('Plain English goes to Atris. Slash commands control the computer.'));
202
236
  }
203
237
 
238
+ function appendSystemPrompt(basePrompt, extraPrompt) {
239
+ if (!extraPrompt) return basePrompt || null;
240
+ if (basePrompt && basePrompt.includes('## Atris CodeOps Workflow')) return basePrompt;
241
+ if (!basePrompt) return extraPrompt;
242
+ return `${String(basePrompt).trim()}\n\n${extraPrompt}`;
243
+ }
244
+
245
+ function codeOpsCloudOptions(options = {}) {
246
+ return {
247
+ ...options,
248
+ worker: options.worker || 'claude',
249
+ mode: 'codeops',
250
+ systemPrompt: appendSystemPrompt(options.systemPrompt, CODEOPS_WORKFLOW_PROMPT),
251
+ };
252
+ }
253
+
254
+ function printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary = null) {
255
+ console.log('');
256
+ console.log(ui.bold('Atris CodeOps Computer'));
257
+ console.log(`${ctx.businessName} ${ui.dim('/workspace persists, full permissions enabled')}`);
258
+ console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
259
+ console.log(`Billing: ${billingLabel}`);
260
+ if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
261
+ console.log(`${ui.green('Workflow locked')} ${ui.dim('observe -> plan -> act -> validate -> evidence -> next')}`);
262
+ console.log('');
263
+ console.log(ui.bold('Start here'));
264
+ console.log(' Type a coding goal in plain English.');
265
+ console.log(' CodeOps will plan first, then execute after approval or explicit proceed language.');
266
+ console.log(' Use /workflow to see the contract, /run for shell, /audit for run history, /exit to leave.');
267
+ console.log('');
268
+ }
269
+
270
+ function printCodeOpsWorkflowContract() {
271
+ console.log('');
272
+ console.log(ui.bold('CodeOps workflow'));
273
+ console.log(' observe -> hypothesis -> plan -> action -> validation -> evidence -> next state');
274
+ console.log('');
275
+ console.log(' Required final evidence: edited_files, commands_run, validation_result, pr_url, pr_state, merge_state, next_task.');
276
+ console.log(' Allowed states: planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.');
277
+ console.log(' Full permissions stay on; the workflow contract controls how the computer uses them.');
278
+ }
279
+
204
280
  function buildLocalBridgeSystemPrompt(sessionId, localRoot, allowBash) {
205
281
  const endpoint = `/api/cli/sessions/${sessionId}/file-op`;
206
282
  const bashLine = allowBash
@@ -630,8 +706,8 @@ async function runLocalBridgeOp(token, sessionId, op, timeoutSeconds = 30) {
630
706
  return data;
631
707
  }
632
708
 
633
- function readBusinessBinding() {
634
- const bindingPath = path.join(process.cwd(), '.atris', 'business.json');
709
+ function readBusinessBinding(cwd = process.cwd()) {
710
+ const bindingPath = path.join(cwd, '.atris', 'business.json');
635
711
  if (!fs.existsSync(bindingPath)) return null;
636
712
  try {
637
713
  return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
@@ -640,6 +716,160 @@ function readBusinessBinding() {
640
716
  }
641
717
  }
642
718
 
719
+ function readPackageMeta(cwd = process.cwd()) {
720
+ const packagePath = path.join(cwd, 'package.json');
721
+ if (!fs.existsSync(packagePath)) return null;
722
+ try {
723
+ return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
724
+ } catch {
725
+ return null;
726
+ }
727
+ }
728
+
729
+ function relIfExists(cwd, target) {
730
+ return fs.existsSync(path.join(cwd, target)) ? target : null;
731
+ }
732
+
733
+ function detectValidationCommand(cwd = process.cwd(), pkg = null) {
734
+ const meta = pkg || readPackageMeta(cwd);
735
+ const testScript = meta?.scripts?.test;
736
+ if (testScript && !/no test specified/i.test(testScript)) return 'npm test';
737
+ if (fs.existsSync(path.join(cwd, 'pytest.ini')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) return 'pytest';
738
+ if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return 'cargo test';
739
+ if (fs.existsSync(path.join(cwd, 'go.mod'))) return 'go test ./...';
740
+ return 'none detected';
741
+ }
742
+
743
+ function detectComputerType(cwd = process.cwd(), pkg = null, binding = null) {
744
+ if (binding?.computer_type) return binding.computer_type;
745
+ if (binding?.workspace_type) return binding.workspace_type;
746
+ const meta = pkg || readPackageMeta(cwd);
747
+ if (meta?.bin || fs.existsSync(path.join(cwd, 'bin')) || fs.existsSync(path.join(cwd, 'commands'))) return 'codeops';
748
+ if (fs.existsSync(path.join(cwd, 'docs')) || fs.existsSync(path.join(cwd, 'atris', 'wiki'))) return 'research';
749
+ return 'workspace';
750
+ }
751
+
752
+ function buildComputerCard(cwd = process.cwd()) {
753
+ const binding = readBusinessBinding(cwd);
754
+ const pkg = readPackageMeta(cwd);
755
+ const folderName = path.basename(cwd);
756
+ const ownerName = binding?.name || pkg?.name || folderName;
757
+ const ownerType = binding ? 'business' : 'project';
758
+ const computerName = binding?.computer_name || binding?.workspace_name || `${ownerName} computer`;
759
+ const computerType = detectComputerType(cwd, pkg, binding);
760
+ const memory = [
761
+ relIfExists(cwd, 'atris/MAP.md'),
762
+ relIfExists(cwd, 'atris/TODO.md'),
763
+ relIfExists(cwd, 'atris/wiki'),
764
+ relIfExists(cwd, 'atris/logs'),
765
+ ].filter(Boolean);
766
+ const artifacts = [
767
+ fs.existsSync(path.join(cwd, 'atris')) ? 'atris/reports/' : null,
768
+ relIfExists(cwd, '.atris/receipts'),
769
+ ].filter(Boolean);
770
+
771
+ return {
772
+ ownerName,
773
+ ownerType,
774
+ computerName,
775
+ computerType,
776
+ workspace: cwd,
777
+ loop: 'plan -> do -> review',
778
+ memory,
779
+ validation: detectValidationCommand(cwd, pkg),
780
+ proof: binding ? 'atris computer proof' : 'atris proof run',
781
+ visual: 'atris visualize "<prompt>"',
782
+ artifacts,
783
+ generatedAt: new Date().toISOString(),
784
+ };
785
+ }
786
+
787
+ function renderList(items) {
788
+ return items.length ? items.join(', ') : 'none detected';
789
+ }
790
+
791
+ function renderComputerCard(card) {
792
+ return [
793
+ 'Atris Computer Card',
794
+ '',
795
+ ` Owner: ${card.ownerName} (${card.ownerType})`,
796
+ ` Computer: ${card.computerName}`,
797
+ ` Type: ${card.computerType}`,
798
+ ` Workspace: ${card.workspace}`,
799
+ ` Loop: ${card.loop}`,
800
+ ` Memory: ${renderList(card.memory)}`,
801
+ ` Validate: ${card.validation}`,
802
+ ` Proof: ${card.proof}`,
803
+ ` Visual: ${card.visual}`,
804
+ ` Artifacts: ${renderList(card.artifacts)}`,
805
+ ].join('\n');
806
+ }
807
+
808
+ function renderComputerCardMarkdown(card) {
809
+ return [
810
+ '# Atris Computer Card',
811
+ '',
812
+ `Generated: ${card.generatedAt}`,
813
+ '',
814
+ `- Owner: ${card.ownerName} (${card.ownerType})`,
815
+ `- Computer: ${card.computerName}`,
816
+ `- Type: ${card.computerType}`,
817
+ `- Workspace: ${card.workspace}`,
818
+ `- Loop: ${card.loop}`,
819
+ `- Memory: ${renderList(card.memory)}`,
820
+ `- Validate: ${card.validation}`,
821
+ `- Proof: ${card.proof}`,
822
+ `- Visual: ${card.visual}`,
823
+ `- Artifacts: ${renderList(card.artifacts)}`,
824
+ '',
825
+ ].join('\n');
826
+ }
827
+
828
+ function parseComputerCardArgs(args = []) {
829
+ const options = { write: false, out: null, help: false };
830
+ for (let i = 0; i < args.length; i++) {
831
+ const arg = args[i];
832
+ if (arg === '--help' || arg === '-h') options.help = true;
833
+ else if (arg === '--write') options.write = true;
834
+ else if (arg === '--out' && args[i + 1]) options.out = args[++i];
835
+ else if (arg.startsWith('--out=')) options.out = arg.slice('--out='.length);
836
+ }
837
+ return options;
838
+ }
839
+
840
+ function defaultComputerCardPath(cwd = process.cwd()) {
841
+ if (fs.existsSync(path.join(cwd, 'atris'))) {
842
+ return path.join(cwd, 'atris', 'reports', 'computer-card.md');
843
+ }
844
+ return path.join(cwd, 'computer-card.md');
845
+ }
846
+
847
+ function computerCard(args = [], cwd = process.cwd()) {
848
+ const options = parseComputerCardArgs(args);
849
+ if (options.help) {
850
+ console.log('Usage: atris computer card [--write] [--out <path>]');
851
+ console.log('');
852
+ console.log('Show the local owner/computer card for this workspace.');
853
+ return null;
854
+ }
855
+
856
+ const card = buildComputerCard(cwd);
857
+ console.log(renderComputerCard(card));
858
+
859
+ if (options.write || options.out) {
860
+ const outputPath = options.out
861
+ ? (path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out))
862
+ : defaultComputerCardPath(cwd);
863
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
864
+ fs.writeFileSync(outputPath, renderComputerCardMarkdown(card), 'utf8');
865
+ console.log('');
866
+ console.log(`Wrote ${path.relative(cwd, outputPath) || outputPath}`);
867
+ return outputPath;
868
+ }
869
+
870
+ return card;
871
+ }
872
+
643
873
  async function resolveBusinessContext(token) {
644
874
  const binding = readBusinessBinding();
645
875
  if (!binding) return null;
@@ -1386,6 +1616,8 @@ async function computerExec(token, prompt, ctx = null, options = {}) {
1386
1616
  workspace_id: ctx.workspaceId,
1387
1617
  ...(options.worker ? { worker: options.worker } : {}),
1388
1618
  ...(options.model ? { model: options.model } : {}),
1619
+ ...(options.systemPrompt ? { system_prompt: options.systemPrompt } : {}),
1620
+ ...(options.allowedTools ? { allowed_tools: options.allowedTools } : {}),
1389
1621
  },
1390
1622
  timeoutMs: 40000,
1391
1623
  });
@@ -1742,6 +1974,10 @@ async function computerChat(token, ctx, initialOptions = {}) {
1742
1974
  return;
1743
1975
  }
1744
1976
 
1977
+ const isCodeOps = initialOptions.mode === 'codeops' || ctx.slug === 'atris-codeops';
1978
+ const chatSystemPrompt = isCodeOps
1979
+ ? appendSystemPrompt(initialOptions.systemPrompt, CODEOPS_WORKFLOW_PROMPT)
1980
+ : initialOptions.systemPrompt;
1745
1981
  let sessionId = `biz-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
1746
1982
  printCloudWordmark();
1747
1983
  const selection = await chooseCloudLane(token, ctx, initialOptions);
@@ -1758,12 +1994,16 @@ async function computerChat(token, ctx, initialOptions = {}) {
1758
1994
  return;
1759
1995
  }
1760
1996
 
1761
- printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
1997
+ if (isCodeOps) {
1998
+ printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
1999
+ } else {
2000
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
2001
+ }
1762
2002
 
1763
2003
  const rl = readline.createInterface({
1764
2004
  input: process.stdin,
1765
2005
  output: process.stdout,
1766
- prompt: 'cloud> ',
2006
+ prompt: isCodeOps ? 'codeops> ' : 'cloud> ',
1767
2007
  });
1768
2008
 
1769
2009
  rl.prompt();
@@ -1782,7 +2022,11 @@ async function computerChat(token, ctx, initialOptions = {}) {
1782
2022
  if (line === '/start') {
1783
2023
  billingLabel = await describeBillingMode(token, ctx, worker);
1784
2024
  authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1785
- printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
2025
+ if (isCodeOps) {
2026
+ printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
2027
+ } else {
2028
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
2029
+ }
1786
2030
  rl.prompt();
1787
2031
  continue;
1788
2032
  }
@@ -1796,6 +2040,11 @@ async function computerChat(token, ctx, initialOptions = {}) {
1796
2040
  rl.prompt();
1797
2041
  continue;
1798
2042
  }
2043
+ if (line === '/workflow') {
2044
+ printCodeOpsWorkflowContract();
2045
+ rl.prompt();
2046
+ continue;
2047
+ }
1799
2048
  if (line === '/pwd') {
1800
2049
  await computerRun(token, 'pwd', ctx);
1801
2050
  rl.prompt();
@@ -1889,7 +2138,12 @@ async function computerChat(token, ctx, initialOptions = {}) {
1889
2138
  }
1890
2139
  }
1891
2140
 
1892
- sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, { worker, model });
2141
+ sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, {
2142
+ worker,
2143
+ model,
2144
+ systemPrompt: chatSystemPrompt,
2145
+ allowedTools: initialOptions.allowedTools,
2146
+ });
1893
2147
  rl.prompt();
1894
2148
  }
1895
2149
  } catch (error) {
@@ -2298,6 +2552,11 @@ async function runComputer() {
2298
2552
  return;
2299
2553
  }
2300
2554
 
2555
+ if (sub === 'card') {
2556
+ computerCard(args.slice(1));
2557
+ return;
2558
+ }
2559
+
2301
2560
  if (sub === 'claude' || sub === 'codex') {
2302
2561
  computerLocalLegacy(args);
2303
2562
  return;
@@ -2306,6 +2565,15 @@ async function runComputer() {
2306
2565
  if (sub === '--help') {
2307
2566
  console.log('Usage: atris computer [mode|command]');
2308
2567
  console.log('');
2568
+ console.log('Atris computers are persistent AI workspaces for scoped jobs.');
2569
+ console.log('');
2570
+ console.log(' Owner = User | Business');
2571
+ console.log(' Owner has many Computers');
2572
+ console.log(' Computer = workspace + files + tools + secrets + memory + agents + validation');
2573
+ console.log('');
2574
+ console.log('Common types: codeops, research, CRM, reporting, recruiting, event ops, support, business ops.');
2575
+ console.log('A business can be a company, lab, collective, community, artist, team, or project.');
2576
+ console.log('');
2309
2577
  console.log('First use:');
2310
2578
  console.log(' cd ~/arena/atris-business/<business>');
2311
2579
  console.log(' atris computer');
@@ -2313,12 +2581,13 @@ async function runComputer() {
2313
2581
  console.log('');
2314
2582
  console.log('Modes:');
2315
2583
  console.log(' (default) Choose CLOUD vs LOCAL when both are available');
2584
+ console.log(' card Show the local owner/computer card, no login required');
2316
2585
  console.log(' local Open LOCAL Atris mode; cloud brain edits this folder');
2317
2586
  console.log(' proof Run the local-edit + cloud-isolation + audit proof');
2318
2587
  console.log(' local-byo Open LOCAL BYO Claude mode; Anthropic tokens, no cloud audit');
2319
2588
  console.log(' --cloud Open CLOUD workspace mode in the bound business workspace');
2320
2589
  console.log(' cloud Open CLOUD workspace mode in the bound business workspace');
2321
- console.log(' codeops Open Atris CodeOps cloud workspace if your account has access');
2590
+ console.log(' codeops Open Atris CodeOps workflow computer if your account has access');
2322
2591
  console.log(' --worker Cloud worker override: claude | openai');
2323
2592
  console.log(' --model Cloud model override');
2324
2593
  console.log(' claude|codex Legacy local console backends');
@@ -2344,6 +2613,8 @@ async function runComputer() {
2344
2613
  console.log('');
2345
2614
  console.log('Examples:');
2346
2615
  console.log(' atris computer');
2616
+ console.log(' atris computer card --write');
2617
+ console.log(' atris business init "My Lab" # shared owner + first/default computer');
2347
2618
  console.log(' atris computer proof');
2348
2619
  console.log(' atris computer local');
2349
2620
  console.log(' atris computer codex');
@@ -2351,6 +2622,9 @@ async function runComputer() {
2351
2622
  console.log(' atris computer --cloud --worker openai --model gpt-5.4');
2352
2623
  console.log(' atris computer cloud');
2353
2624
  console.log(' atris computer codeops');
2625
+ console.log(' atris computer codeops status');
2626
+ console.log(' atris computer codeops run "pwd"');
2627
+ console.log(' atris computer codeops exec "Plan a safe repo fix"');
2354
2628
  console.log(' atris computer status');
2355
2629
  console.log(' atris computer wake');
2356
2630
  console.log(' atris computer run "ls -la /workspace"');
@@ -2370,7 +2644,44 @@ async function runComputer() {
2370
2644
  console.error('Ask an Atris CodeOps admin to add you to the atris-codeops business.');
2371
2645
  return;
2372
2646
  }
2373
- await computerChat(token, codeopsCtx, { worker: 'claude', ...cloudOptions });
2647
+ const codeopsOptions = codeOpsCloudOptions(cloudOptions);
2648
+ const codeopsSub = args[1];
2649
+ const codeopsRest = args.slice(2).join(' ');
2650
+ if (!codeopsSub || codeopsSub === 'chat') {
2651
+ await computerChat(token, codeopsCtx, codeopsOptions);
2652
+ return;
2653
+ }
2654
+ switch (codeopsSub) {
2655
+ case '--help':
2656
+ case 'help':
2657
+ console.log('Usage: atris computer codeops [chat|status|wake|sleep|run|grep|ls|cat|exec|audit|workflow]');
2658
+ console.log('');
2659
+ console.log('Examples:');
2660
+ console.log(' atris computer codeops');
2661
+ console.log(' atris computer codeops status');
2662
+ console.log(' atris computer codeops run "pwd && git status --short"');
2663
+ console.log(' atris computer codeops exec "Plan the smallest safe fix, then wait"');
2664
+ return;
2665
+ case 'status': return computerStatus(token, codeopsCtx);
2666
+ case 'wake': return computerWake(token, codeopsCtx);
2667
+ case 'sleep': return computerSleep(token, codeopsCtx);
2668
+ case 'run': return computerRun(token, codeopsRest, codeopsCtx);
2669
+ case 'grep': return computerGrep(token, codeopsRest, codeopsCtx);
2670
+ case 'ls': return computerLs(token, codeopsRest || undefined, codeopsCtx);
2671
+ case 'cat': return computerCat(token, codeopsRest, codeopsCtx);
2672
+ case 'exec': return computerExec(token, codeopsRest, codeopsCtx, codeopsOptions);
2673
+ case 'audit': {
2674
+ const limit = codeopsRest ? Number.parseInt(codeopsRest, 10) : 10;
2675
+ return computerAudit(token, codeopsCtx, Number.isFinite(limit) ? limit : 10);
2676
+ }
2677
+ case 'workflow':
2678
+ printCodeOpsWorkflowContract();
2679
+ return;
2680
+ default:
2681
+ console.error(`Unknown CodeOps subcommand: ${codeopsSub}`);
2682
+ console.log('Run: atris computer codeops help');
2683
+ return;
2684
+ }
2374
2685
  return;
2375
2686
  }
2376
2687
 
@@ -2383,6 +2694,7 @@ async function runComputer() {
2383
2694
 
2384
2695
  switch (sub) {
2385
2696
  case 'chat': return computerChat(token, ctx, cloudOptions);
2697
+ case 'card': return computerCard(args.slice(1));
2386
2698
  case 'proof': return computerProof(token, ctx, cloudOptions);
2387
2699
  case 'status': return computerStatus(token, ctx);
2388
2700
  case 'wake': return computerWake(token, ctx);
@@ -2405,4 +2717,9 @@ async function runComputer() {
2405
2717
  }
2406
2718
  }
2407
2719
 
2408
- module.exports = { runComputer };
2720
+ module.exports = {
2721
+ runComputer,
2722
+ buildComputerCard,
2723
+ renderComputerCard,
2724
+ renderComputerCardMarkdown,
2725
+ };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Errors command for Atris CLI — admin dashboard over atris_error_events.
3
+ *
4
+ * Usage:
5
+ * atris errors List errors from last 24h, grouped by signature
6
+ * atris errors --hours 72 Widen the window (max 720h / 30d)
7
+ * atris errors --limit 1000 Raise the raw-event cap for grouping
8
+ * atris errors show <id> Full detail (stack trace, message) for one event
9
+ *
10
+ * Requires admin role on the user row.
11
+ */
12
+
13
+ const { loadCredentials } = require('../utils/auth');
14
+ const { apiRequestJson } = require('../utils/api');
15
+
16
+ function getToken() {
17
+ const creds = loadCredentials();
18
+ if (!creds || !creds.token) {
19
+ console.error('Not logged in. Run: atris login');
20
+ process.exit(1);
21
+ }
22
+ return creds.token;
23
+ }
24
+
25
+ function extractFlag(args, ...names) {
26
+ const remaining = [];
27
+ let value = null;
28
+ for (let i = 0; i < args.length; i++) {
29
+ const a = args[i];
30
+ const eq = a.indexOf('=');
31
+ const key = eq >= 0 ? a.slice(0, eq) : a;
32
+ if (names.includes(key)) {
33
+ value = eq >= 0 ? a.slice(eq + 1) : args[++i];
34
+ } else {
35
+ remaining.push(a);
36
+ }
37
+ }
38
+ return [value, remaining];
39
+ }
40
+
41
+ async function listErrors(args) {
42
+ const [hoursArg, r1] = extractFlag(args, '--hours', '-H');
43
+ const [limitArg, r2] = extractFlag(r1, '--limit', '-L');
44
+ const hours = hoursArg ? parseInt(hoursArg, 10) : 24;
45
+ const limit = limitArg ? parseInt(limitArg, 10) : 500;
46
+
47
+ const token = getToken();
48
+ const result = await apiRequestJson(`/errors?hours=${hours}&limit=${limit}`, {
49
+ method: 'GET',
50
+ token,
51
+ });
52
+
53
+ if (!result.ok) {
54
+ console.error(`Error: ${result.error || 'Failed to fetch errors'}`);
55
+ process.exit(1);
56
+ }
57
+
58
+ const data = result.data || {};
59
+ const groups = data.groups || [];
60
+
61
+ if (groups.length === 0) {
62
+ console.log(`No errors in the last ${hours}h. Clean.`);
63
+ return;
64
+ }
65
+
66
+ console.log(
67
+ `Errors — last ${data.window_hours}h — ` +
68
+ `${data.total_events} events across ${data.unique_signatures} signatures\n`,
69
+ );
70
+
71
+ groups.forEach((g, idx) => {
72
+ const s = g.sample || {};
73
+ const shortId = (s.id || '').substring(0, 8);
74
+ const last = s.created_at ? s.created_at.substring(0, 16).replace('T', ' ') : '';
75
+ const status = s.status_code ? ` [${s.status_code}]` : '';
76
+ const msg = (s.message || '').replace(/\s+/g, ' ').substring(0, 140);
77
+
78
+ console.log(`${idx + 1}. x${g.count} ${g.signature}${status}`);
79
+ if (last) console.log(` last: ${last} UTC latest id: ${shortId}`);
80
+ if (msg) console.log(` "${msg}"`);
81
+ console.log('');
82
+ });
83
+
84
+ console.log(
85
+ `Run \`atris errors show <id>\` for full stack trace of a specific event.`,
86
+ );
87
+ }
88
+
89
+ async function showError(errorId) {
90
+ if (!errorId) {
91
+ console.error('Usage: atris errors show <id>');
92
+ console.error('(id must be a full UUID — get one from `atris errors` output)');
93
+ process.exit(1);
94
+ }
95
+
96
+ const token = getToken();
97
+ const result = await apiRequestJson(`/errors/${encodeURIComponent(errorId)}`, {
98
+ method: 'GET',
99
+ token,
100
+ });
101
+
102
+ if (!result.ok) {
103
+ console.error(`Error: ${result.error || 'Failed to fetch error'}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ const e = result.data || {};
108
+ console.log(`Error ${e.id || errorId}`);
109
+ console.log(` type: ${e.error_type || '?'}`);
110
+ console.log(` method: ${e.request_method || '?'}`);
111
+ console.log(` path: ${e.request_path || '?'}`);
112
+ console.log(` status: ${e.status_code || '?'}`);
113
+ console.log(` when: ${e.created_at || '?'}`);
114
+ console.log(` source: ${e.source || '?'}`);
115
+ if (e.user_id) console.log(` user: ${e.user_id}`);
116
+ console.log('');
117
+ console.log('Message:');
118
+ console.log(' ' + (e.message || '(none)').split('\n').join('\n '));
119
+ console.log('');
120
+ if (e.stack_trace) {
121
+ console.log('Stack trace:');
122
+ console.log(' ' + e.stack_trace.split('\n').join('\n '));
123
+ }
124
+ }
125
+
126
+ function printHelp() {
127
+ console.log('');
128
+ console.log('Usage:');
129
+ console.log(' atris errors List errors from last 24h, grouped');
130
+ console.log(' atris errors --hours 72 Widen the window (max 720h / 30d)');
131
+ console.log(' atris errors --limit 1000 Raise the raw-event cap');
132
+ console.log(' atris errors show <full-uuid> Full detail for one event');
133
+ console.log('');
134
+ console.log('Admin role required.');
135
+ console.log('');
136
+ }
137
+
138
+ async function errorsCommand() {
139
+ const args = process.argv.slice(3);
140
+ const sub = args[0];
141
+
142
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
143
+ printHelp();
144
+ return;
145
+ }
146
+
147
+ if (sub === 'show') {
148
+ await showError(args[1]);
149
+ return;
150
+ }
151
+
152
+ await listErrors(args);
153
+ }
154
+
155
+ module.exports = { errorsCommand };