atris 3.63.0 → 3.65.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.
Files changed (81) hide show
  1. package/README.md +4 -0
  2. package/atris/skills/commerce/SKILL.md +24 -0
  3. package/atris/skills/x-search/SKILL.md +3 -1
  4. package/ax +4 -3
  5. package/bin/atris.js +28 -2
  6. package/cli/atris_code.py +25 -1
  7. package/commands/agent-spawn.js +1 -0
  8. package/commands/auth.js +79 -10
  9. package/commands/autoland.js +11 -1
  10. package/commands/browse.js +2 -2
  11. package/commands/business.js +3 -3
  12. package/commands/buy.js +3 -3
  13. package/commands/deck.js +2 -2
  14. package/commands/engine.js +292 -65
  15. package/commands/feedback.js +2 -2
  16. package/commands/fork.js +2 -2
  17. package/commands/gm.js +2 -1
  18. package/commands/hire.js +189 -0
  19. package/commands/improve.js +142 -8
  20. package/commands/integrations.js +4 -3
  21. package/commands/loops.js +2 -1
  22. package/commands/mail.js +163 -0
  23. package/commands/member.js +47 -9
  24. package/commands/mission.js +4 -3
  25. package/commands/pack-craft.js +2 -2
  26. package/commands/publish.js +2 -2
  27. package/commands/radar.js +3 -2
  28. package/commands/router.js +2 -2
  29. package/commands/stream.js +22 -1
  30. package/commands/task.js +84 -2
  31. package/commands/team.js +355 -67
  32. package/commands/transactions.js +3 -3
  33. package/commands/truth.js +2 -2
  34. package/commands/x-search.js +81 -10
  35. package/commands/youtube.js +2 -1
  36. package/lib/auto-accept-certified.js +83 -9
  37. package/lib/autoland.js +3 -2
  38. package/lib/ax-auto-lane.js +2 -2
  39. package/lib/brief-rules.js +11 -0
  40. package/lib/business-target.js +131 -0
  41. package/lib/chat-log-scan.js +2 -1
  42. package/lib/ci-runner.js +3 -2
  43. package/lib/codex-flight.js +2 -1
  44. package/lib/commerce.js +17 -2
  45. package/lib/deck-history.js +2 -2
  46. package/lib/deck-review.js +3 -3
  47. package/lib/design-api.js +2 -2
  48. package/lib/engine-processes.js +430 -0
  49. package/lib/engine-registry.js +101 -21
  50. package/lib/first-minute.js +2 -2
  51. package/lib/fleet.js +8 -36
  52. package/lib/git-spawn.js +5 -2
  53. package/lib/ignored-cache.js +51 -0
  54. package/lib/known-commands.js +1 -1
  55. package/lib/manifest.js +2 -2
  56. package/lib/map-refs.js +70 -4
  57. package/lib/member-identity.js +56 -0
  58. package/lib/pack-capabilities.js +2 -1
  59. package/lib/permission-grants.js +2 -2
  60. package/lib/pulse.js +5 -5
  61. package/lib/roster-models.js +11 -0
  62. package/lib/roster-runs.js +13 -2
  63. package/lib/roster-stale.js +241 -0
  64. package/lib/runner-command.js +22 -6
  65. package/lib/sandbox-pids.js +113 -0
  66. package/lib/secret-gateway.js +3 -13
  67. package/lib/state-home.js +106 -0
  68. package/lib/task-db.js +4 -3
  69. package/lib/task-prune.js +462 -0
  70. package/lib/team-lineup.js +10 -71
  71. package/lib/team-presence.js +224 -0
  72. package/lib/text-table.js +81 -0
  73. package/lib/x-post-check.js +497 -0
  74. package/package.json +1 -1
  75. package/scripts/det/data/xsearch-answer.json +24 -0
  76. package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
  77. package/scripts/det/xsearch-bench.js +307 -0
  78. package/scripts/det/ytnotes +61 -15
  79. package/scripts/det/ytrail-bench.js +2 -1
  80. package/utils/auth.js +3 -3
  81. package/utils/update-check.js +2 -2
package/README.md CHANGED
@@ -295,10 +295,14 @@ An engine is the intelligence that builds a mission. Every installed headless co
295
295
 
296
296
  The first ready worker leads and the rest are backups. `~/.atris/ROSTER.md` covers every project on the machine; `atris/ROSTER.md` overrides it for one project. A worker that stalls sits out 30 minutes and the next one takes over.
297
297
 
298
+ The roster warns when a worker runs an old model and a newer one in the same family is on this machine, and prints the command that moves it. It also warns when a pinned model has no proof in 30 days. A landed run is proof; so is `verified 2026-09-30` on the line after a quick smoke test.
299
+
298
300
  ```bash
299
301
  atris engine roster # who does what, with each worker's recent record
300
302
  atris engine assign build claude --model "opus 5.5" # change a job's lead
301
303
  atris engine roster --available # tools and models on this machine
304
+ atris team # who really worked in the last 7 days, then quiet and idle
305
+ atris team --record # also save one row per member per day for the learning loop
302
306
  ```
303
307
 
304
308
  Show every installed engine and the current default:
@@ -13,6 +13,30 @@ You can quote and ask. Only the human can pay. Every dollar leaves only after
13
13
  the person acts themselves: a tap in their Link app, a payment page they
14
14
  fill in, or a text from their own phone.
15
15
 
16
+ ## Get a key
17
+
18
+ The person mints a commerce key from their own signed-in CLI. It can ask for
19
+ prices and start their approval. It can never pay.
20
+
21
+ ```bash
22
+ atris login --agent --commerce --commerce-max-usd 150
23
+ # scopes: commerce:quote, transactions:read
24
+ # largest single quote: $150 (1 to 500, default 200)
25
+ # this key can ask for prices and start your approval, it can never pay.
26
+ ```
27
+
28
+ - Same machine: nothing else to do. The key is stored in its own slot
29
+ (`commerce_agent_token` in `~/.atris/credentials.json`), and `atris buy` and
30
+ `atris transactions` use it. Minting an x-search or YouTube key later does not
31
+ replace it.
32
+ - Agent on another machine: add `--print-key` to show the key once. The person
33
+ hands it to the agent as `ATRIS_TOKEN` (CLI) or `ATRIS_API_KEY` (curl below).
34
+ Never paste it into a chat.
35
+ - Narrower key: `--scopes commerce:quote` (quote, approve, status) or
36
+ `--scopes transactions:read` (history only).
37
+ - Quotes over the key's cap are refused, and each key gets 50 quotes a day (UTC). A developer key (`atris api-key`) or the
38
+ person's own login also works, without those per-key limits.
39
+
16
40
  ## The loop
17
41
 
18
42
  1. Quote. `atris buy quote --kind <kind> [fields]`.
@@ -49,7 +49,9 @@ atris x-search person --name "Leah Bonvissuto" --handle leahbon
49
49
  atris x-search --help
50
50
  ```
51
51
 
52
- Add `--json` for the raw API payload. Curl below is the raw API for debugging only.
52
+ Every cited post is looked up on X for free (the public embed endpoint, 5s cap), whatever layout the answer uses. A cited post whose words appear in the answer is checked: its `https://x.com/<handle>/status/<id> (checked)` link prints under that post, or in a `Checked posts:` list when the answer is a table or prose. A quote of 40+ characters that matches no real post says `(could not find this post on X, treat as unverified)`; if X could not be reached it says so and is never called fake. Citations no post uses print as `Other sources:`. The last line is `posts: N checked, N unverified, N unknown`. `--no-check` skips the lookup. Bench it with `atris x-search bench --free` (0 credits) or `--quick` (5 credits).
53
+
54
+ Add `--json` for the raw API payload (plus a `checks` list). Curl below is the raw API for debugging only.
53
55
 
54
56
  ## Bootstrap (ALWAYS Run First)
55
57
 
package/ax CHANGED
@@ -5,6 +5,7 @@ const http = require('http');
5
5
  const https = require('https');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
+ const { stateHome } = require('./lib/state-home');
8
9
  const readline = require('readline');
9
10
  const crypto = require('crypto');
10
11
  const { Readable } = require('stream');
@@ -230,7 +231,7 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
230
231
 
231
232
  const baseDir = fs.existsSync(path.join(cwd, 'atris'))
232
233
  ? path.join(cwd, 'atris', 'runs')
233
- : path.join(os.homedir(), '.atris', 'runs');
234
+ : path.join(stateHome(), '.atris', 'runs');
234
235
  fs.mkdirSync(baseDir, { recursive: true });
235
236
  const logPath = path.join(baseDir, `ax-${kind}-${timestampForFile()}.log`);
236
237
  fs.appendFileSync(logPath, [
@@ -995,7 +996,7 @@ function normalizeMode(mode) {
995
996
  function autoPickLogPath(options = {}) {
996
997
  return options.pickLogPath
997
998
  || process.env.ATRIS_ROUTER_PICKS_PATH
998
- || path.join(os.homedir(), '.atris', 'ax-auto-picks.jsonl');
999
+ || path.join(stateHome(), '.atris', 'ax-auto-picks.jsonl');
999
1000
  }
1000
1001
 
1001
1002
  function autoMessageWords(message) {
@@ -2328,7 +2329,7 @@ function formatTaskPreviewRows(receipt, options = {}) {
2328
2329
  }
2329
2330
 
2330
2331
  function approvalStorePath(options = {}) {
2331
- return options.storePath || process.env.AX_APPROVAL_STORE || path.join(os.homedir(), '.atris', 'ax-approvals.json');
2332
+ return options.storePath || process.env.AX_APPROVAL_STORE || path.join(stateHome(), '.atris', 'ax-approvals.json');
2332
2333
  }
2333
2334
 
2334
2335
  function approvalMaxAgeMs(options = {}) {
package/bin/atris.js CHANGED
@@ -719,6 +719,9 @@ function showHelpAll() {
719
719
  console.log(' topup - Buy credits and print a Stripe checkout URL');
720
720
  console.log(' buy - Quote a purchase or stock order; the person approves before anything is charged');
721
721
  console.log(' transactions - List what you bought, paid in fees, traded, or sent');
722
+ console.log(' hire - Hire an agent in one step: name, job, inbox, allowance, nightly work loop');
723
+ console.log(' hires - Morning check-in: one line per hire (mail, spend, last run)');
724
+ console.log(' mail - Your agents\' own inboxes: list, read, send (not Gmail)');
722
725
  console.log(' design - Extract a site design system, check brand adherence, search brands');
723
726
  console.log(' mcp - Run the atris MCP server (stdio) for Claude Desktop and Cursor');
724
727
  console.log('');
@@ -1011,6 +1014,9 @@ function showAuthHelp(commandName) {
1011
1014
  console.log(' --agent Mint a scoped agent token from the stored login.');
1012
1015
  console.log(' --scopes <list> Comma-separated scopes (default: x-search,youtube).');
1013
1016
  console.log(' --daily-credit-cap <n> Daily credit cap (default: 50).');
1017
+ console.log(' --commerce Commerce key: scopes commerce:quote,transactions:read. Can quote, never pay.');
1018
+ console.log(' --commerce-max-usd <n> Largest single quote a commerce key may ask for, 1 to 500 (default: 200).');
1019
+ console.log(' --print-key Print the minted key once, for an agent on another machine.');
1014
1020
  } else if (commandName === 'switch') {
1015
1021
  console.log(' --global, -g Switch the account for all terminals.');
1016
1022
  }
@@ -1856,9 +1862,15 @@ function showWelcomeVisualization() {
1856
1862
  // Every new chat sees which tool and model does each job, from the same
1857
1863
  // resolver as atris engine roster. A roster that cannot be read drops the
1858
1864
  // line; it never stops the boot.
1865
+ // A roster worker that ends within seven days gets one line, so the job
1866
+ // does not quietly fall to its backup when the date passes.
1859
1867
  try {
1860
- const teamLine = require('../lib/team-lineup').bootTeamLine(cwd, { width: 80 - 11 });
1868
+ const engineView = require('../commands/engine');
1869
+ const jobs = engineView.jobRosterView(cwd);
1870
+ const teamLine = require('../lib/team-lineup').bootTeamLine(cwd, { width: 80 - 11, jobs });
1861
1871
  if (teamLine) console.log(row('team', teamLine));
1872
+ const endsLine = engineView.bootExpiringLine(engineView.expiringWorkers(jobs));
1873
+ if (endsLine) console.log(row('roster', endsLine));
1862
1874
  } catch {
1863
1875
  // Silent: the lineup is a hint, not a gate.
1864
1876
  }
@@ -2375,6 +2387,20 @@ if (command === 'guide') {
2375
2387
  Promise.resolve(require('../commands/buy').buyCommand(process.argv.slice(3)))
2376
2388
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
2377
2389
  .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2390
+ } else if (command === 'hire') {
2391
+ // One-call hire: member + inbox + allowance + overnight loop (POST /business/{id}/hire).
2392
+ Promise.resolve(require('../commands/hire').hireCommand(process.argv.slice(3)))
2393
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2394
+ .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2395
+ } else if (command === 'hires') {
2396
+ Promise.resolve(require('../commands/hire').hiresCommand(process.argv.slice(3)))
2397
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2398
+ .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2399
+ } else if (command === 'mail') {
2400
+ // Agent inboxes (@atrismail.com), not Gmail. Send asks first unless --yes.
2401
+ Promise.resolve(require('../commands/mail').mailCommand(process.argv.slice(3)))
2402
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2403
+ .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2378
2404
  } else if (command === 'transactions') {
2379
2405
  Promise.resolve(require('../commands/transactions').transactionsCommand(process.argv.slice(3)))
2380
2406
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
@@ -2981,7 +3007,7 @@ if (command === 'guide') {
2981
3007
  const subcommand = process.argv[3];
2982
3008
  const args = process.argv.slice(4);
2983
3009
  Promise.resolve(require('../commands/member').memberCommand(subcommand, ...args))
2984
- .then(() => process.exit(subcommand === 'run' ? (process.exitCode || 0) : 0))
3010
+ .then(() => process.exit(process.exitCode || 0))
2985
3011
  .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
2986
3012
  } else if (command === 'app') {
2987
3013
  const subcommand = process.argv[3];
package/cli/atris_code.py CHANGED
@@ -12,11 +12,13 @@ from __future__ import annotations
12
12
 
13
13
  import argparse
14
14
  import asyncio
15
+ import getpass
15
16
  import json
16
17
  import logging
17
18
  import os
18
19
  import subprocess
19
20
  import sys
21
+ import tempfile
20
22
  import time
21
23
  from dataclasses import dataclass
22
24
  from pathlib import Path
@@ -36,7 +38,29 @@ RED = "\033[91m"
36
38
 
37
39
  PACKAGE_ROOT = Path(__file__).resolve().parent.parent
38
40
  TEAM_DIR = PACKAGE_ROOT / "atris" / "team"
39
- STATE_DIR = Path.home() / ".atris"
41
+
42
+
43
+ def _same_dir(a: str, b: str) -> bool:
44
+ try:
45
+ return os.path.samefile(a, b)
46
+ except OSError:
47
+ return False
48
+
49
+
50
+ def _state_home() -> Path:
51
+ """Mirror lib/state-home.js: a test run never writes the real ~/.atris."""
52
+ env, home = os.environ, Path.home()
53
+ under_test = env.get("NODE_TEST_CONTEXT") or env.get("ATRIS_TEST_STATE_DIR")
54
+ if env.get("ATRIS_TEST_REAL_HOME") == "1" or not under_test:
55
+ return home
56
+ real = [os.path.expanduser("~" + getpass.getuser()), env.get("ATRIS_TEST_PROTECTED_HOME", "")]
57
+ if not any(r and _same_dir(str(home), r) for r in real):
58
+ return home
59
+ user = os.getuid() if hasattr(os, "getuid") else getpass.getuser()
60
+ return Path(env.get("ATRIS_TEST_STATE_DIR") or os.path.join(tempfile.gettempdir(), f"atris-test-home-{user}"))
61
+
62
+
63
+ STATE_DIR = _state_home() / ".atris"
40
64
  SESSION_STATE_FILE = STATE_DIR / "computer_sessions.json"
41
65
  AUDIT_LOG_FILE = STATE_DIR / "computer_audit.jsonl"
42
66
  RESUME_MAX_AGE_SECONDS = 6 * 60 * 60
@@ -465,6 +465,7 @@ function agentSpawnStatusCommand(args = [], deps = {}) {
465
465
  }
466
466
 
467
467
  module.exports = {
468
+ ALLOWED_ENGINES,
468
469
  parseSpawnArgs,
469
470
  commandForEngine,
470
471
  createSpawnRequest,
package/commands/auth.js CHANGED
@@ -8,6 +8,13 @@ const path = require('path');
8
8
  const AGENT_TOKEN_PATH = '/auth/agent-token';
9
9
  const DEFAULT_AGENT_SCOPES = ['x-search', 'youtube'];
10
10
  const DEFAULT_DAILY_CREDIT_CAP = 50;
11
+ // Commerce keys can ask for prices and start the owner's approval. They can
12
+ // never pay: every dollar still needs the owner's own tap or text. They live
13
+ // in their own slot so an x-search or YouTube mint never overwrites them.
14
+ const COMMERCE_QUOTE_SCOPE = 'commerce:quote';
15
+ const COMMERCE_SCOPES = [COMMERCE_QUOTE_SCOPE, 'transactions:read'];
16
+ const COMMERCE_MAX_USD_LIMIT = 500;
17
+ const COMMERCE_NEVER_PAYS = 'this key can ask for prices and start your approval, it can never pay.';
11
18
  const NO_STORED_JWT_MESSAGE = 'not signed in. run atris login first.';
12
19
 
13
20
  function firstNonEmptyString(...values) {
@@ -23,8 +30,12 @@ function wantsAgentToken(args = []) {
23
30
  return first === 'agent-token';
24
31
  }
25
32
 
26
- function parseScopeList(raw) {
27
- if (!raw) return [...DEFAULT_AGENT_SCOPES];
33
+ function isCommerceKey(scopes = []) {
34
+ return Array.isArray(scopes) && scopes.some((scope) => COMMERCE_SCOPES.includes(String(scope)));
35
+ }
36
+
37
+ function parseScopeList(raw, fallback = DEFAULT_AGENT_SCOPES) {
38
+ if (!raw) return [...fallback];
28
39
  const scopes = String(raw).split(',').map((part) => part.trim()).filter(Boolean);
29
40
  if (scopes.length === 0) {
30
41
  throw new Error('scopes must list at least one value');
@@ -43,11 +54,29 @@ function parseDailyCreditCap(args) {
43
54
  return value;
44
55
  }
45
56
 
57
+ function parseCommerceMaxUsd(args, scopes) {
58
+ const present = args.some((arg) => arg === '--commerce-max-usd' || String(arg).startsWith('--commerce-max-usd='));
59
+ if (!present) return null;
60
+ const raw = String(readFlag(args, '--commerce-max-usd', '')).replace(/^\$/, '');
61
+ const value = Number(raw);
62
+ if (!Number.isInteger(value) || value < 1 || value > COMMERCE_MAX_USD_LIMIT) {
63
+ throw new Error(`commerce max must be a whole dollar amount from 1 to ${COMMERCE_MAX_USD_LIMIT}`);
64
+ }
65
+ if (!scopes.includes(COMMERCE_QUOTE_SCOPE)) {
66
+ throw new Error('--commerce-max-usd needs the commerce:quote scope (try --commerce)');
67
+ }
68
+ return value;
69
+ }
70
+
46
71
  function parseAgentTokenArgs(args = []) {
72
+ const commerce = hasFlag(args, '--commerce');
73
+ const scopes = parseScopeList(readFlag(args, '--scopes', ''), commerce ? COMMERCE_SCOPES : DEFAULT_AGENT_SCOPES);
47
74
  return {
48
75
  agent: wantsAgentToken(args),
49
- scopes: parseScopeList(readFlag(args, '--scopes', '')),
76
+ scopes,
50
77
  dailyCreditCap: parseDailyCreditCap(args),
78
+ commerceMaxUsd: parseCommerceMaxUsd(args, scopes),
79
+ printKey: hasFlag(args, '--print-key'),
51
80
  json: wantsJson(args),
52
81
  help: hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help',
53
82
  };
@@ -82,7 +111,14 @@ function extractAgentTokenMeta(data, requested, token) {
82
111
  const dailyCreditCap = Number.isFinite(Number(rawCap)) ? Number(rawCap) : requested.dailyCreditCap;
83
112
  const expiresAt = expiryFromValue(payload.expires_at ?? payload.expiry ?? payload.expires ?? payload.exp)
84
113
  || expiryFromValue(getTokenExpiryEpochSeconds(token));
85
- return { scopes, dailyCreditCap, expiresAt };
114
+ const rawMax = payload.commerce_max_quote_usd;
115
+ const commerceMaxUsd = Number.isFinite(Number(rawMax)) && rawMax !== null && rawMax !== undefined
116
+ ? Number(rawMax)
117
+ : (requested.commerceMaxUsd || null);
118
+ const commerceDailyQuotes = Number.isFinite(Number(payload.commerce_daily_quotes)) && payload.commerce_daily_quotes != null
119
+ ? Number(payload.commerce_daily_quotes)
120
+ : null;
121
+ return { scopes, dailyCreditCap, expiresAt, commerceMaxUsd, commerceDailyQuotes };
86
122
  }
87
123
 
88
124
  function isAgentAccessToken(token) {
@@ -116,12 +152,22 @@ function persistMintedAgentToken(credentials, token, extras = {}) {
116
152
  if (isAgentAccessToken(credentials.token)) {
117
153
  throw new Error('Refusing to save a scoped agent token as the login token; keep it under agent_token');
118
154
  }
155
+ const slot = isCommerceKey(extras.scopes)
156
+ ? {
157
+ commerce_agent_token: token,
158
+ commerce_agent_token_scopes: extras.scopes || [],
159
+ commerce_agent_token_expires_at: extras.expiresAt || null,
160
+ commerce_max_quote_usd: extras.commerceMaxUsd || null,
161
+ }
162
+ : {
163
+ agent_token: token,
164
+ agent_token_scopes: extras.scopes || [],
165
+ agent_token_expires_at: extras.expiresAt || null,
166
+ };
119
167
  const next = {
120
168
  ...credentials,
121
169
  refresh_token: extras.refresh_token || credentials.refresh_token || null,
122
- agent_token: token,
123
- agent_token_scopes: extras.scopes || [],
124
- agent_token_expires_at: extras.expiresAt || null,
170
+ ...slot,
125
171
  };
126
172
  if (credentials.source_profile) {
127
173
  saveProfile(credentials.source_profile, next);
@@ -132,10 +178,14 @@ function persistMintedAgentToken(credentials, token, extras = {}) {
132
178
  }
133
179
 
134
180
  function printAgentTokenMint(meta, output) {
135
- output('minted scoped agent token');
181
+ const commerce = isCommerceKey(meta.scopes);
182
+ output(commerce ? 'minted commerce agent key' : 'minted scoped agent token');
136
183
  output(`scopes: ${meta.scopes.join(', ')}`);
184
+ if (meta.commerceMaxUsd) output(`largest single quote: $${meta.commerceMaxUsd}`);
185
+ if (meta.commerceDailyQuotes) output(`quotes per day: ${meta.commerceDailyQuotes}`);
137
186
  output(`daily credit cap: ${meta.dailyCreditCap}`);
138
187
  if (meta.expiresAt) output(`expires: ${meta.expiresAt}`);
188
+ if (commerce) output(COMMERCE_NEVER_PAYS);
139
189
  }
140
190
 
141
191
  function redactSecret(text, secret) {
@@ -175,10 +225,16 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
175
225
  return { ok: false, code: 'not_logged_in', error: NO_STORED_JWT_MESSAGE };
176
226
  }
177
227
 
228
+ const commerceMaxUsd = Number.isInteger(requested.commerceMaxUsd) ? requested.commerceMaxUsd : null;
229
+ if (commerceMaxUsd !== null && !scopes.includes(COMMERCE_QUOTE_SCOPE)) {
230
+ return { ok: false, code: 'invalid_scopes', error: '--commerce-max-usd needs the commerce:quote scope' };
231
+ }
232
+
178
233
  const body = {
179
234
  scopes,
180
235
  daily_credit_cap: dailyCreditCap,
181
236
  };
237
+ if (commerceMaxUsd !== null) body.commerce_max_quote_usd = commerceMaxUsd;
182
238
  let authToken = isAgentAccessToken(accessToken) ? refreshToken : (accessToken || refreshToken);
183
239
  let result = await postAgentToken(api, authToken, body);
184
240
  if (!result.ok && result.status === 401 && refreshToken && authToken !== refreshToken) {
@@ -201,7 +257,7 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
201
257
  return { ok: false, code: 'missing_token', error: 'backend did not return an agent token' };
202
258
  }
203
259
 
204
- const meta = extractAgentTokenMeta(result.data, { scopes, dailyCreditCap }, minted);
260
+ const meta = extractAgentTokenMeta(result.data, { scopes, dailyCreditCap, commerceMaxUsd }, minted);
205
261
  persist(credentials, minted, {
206
262
  ...meta,
207
263
  refresh_token: firstNonEmptyString(result.data && result.data.refresh_token) || refreshToken,
@@ -212,7 +268,7 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
212
268
  ok: true,
213
269
  token: minted,
214
270
  meta,
215
- storedIn: credentials.source_profile ? `profile ${credentials.source_profile}, agent_token` : '~/.atris/credentials.json, agent_token',
271
+ storedIn: `${credentials.source_profile ? `profile ${credentials.source_profile}` : '~/.atris/credentials.json'}, ${isCommerceKey(meta.scopes) ? 'commerce_agent_token' : 'agent_token'}`,
216
272
  };
217
273
  }
218
274
 
@@ -276,6 +332,7 @@ async function mintAgentToken(args = [], deps = {}) {
276
332
  const minted = await mintScopedAgentToken({
277
333
  scopes: options.scopes,
278
334
  dailyCreditCap: options.dailyCreditCap,
335
+ commerceMaxUsd: options.commerceMaxUsd,
279
336
  }, deps);
280
337
 
281
338
  if (!minted.ok) {
@@ -302,12 +359,24 @@ async function mintAgentToken(args = [], deps = {}) {
302
359
  scopes: minted.meta.scopes,
303
360
  daily_credit_cap: minted.meta.dailyCreditCap,
304
361
  expires_at: minted.meta.expiresAt,
362
+ ...(isCommerceKey(minted.meta.scopes) ? {
363
+ commerce_max_quote_usd: minted.meta.commerceMaxUsd,
364
+ commerce_daily_quotes: minted.meta.commerceDailyQuotes,
365
+ can_pay: false,
366
+ note: COMMERCE_NEVER_PAYS,
367
+ } : {}),
368
+ ...(options.printKey ? { key: minted.token } : {}),
305
369
  }, null, 2));
306
370
  return 0;
307
371
  }
308
372
 
309
373
  printAgentTokenMint(minted.meta, output);
310
374
  output(`stored in ${minted.storedIn}`);
375
+ if (options.printKey) {
376
+ // Only on request: an outside agent needs the raw key once.
377
+ output('key (shown once, give it to the agent as ATRIS_TOKEN, do not paste it into chat):');
378
+ output(minted.token);
379
+ }
311
380
  return 0;
312
381
  }
313
382
 
@@ -135,7 +135,16 @@ function runOwnCli(root, cliArgs, timeoutMs = 300000) {
135
135
  // target root or a global `atris` breaks whenever the tick runs a project
136
136
  // that isn't the CLI repo on a machine without a global install (CI, cron).
137
137
  const bin = path.resolve(__dirname, '..', 'bin', 'atris.js');
138
- const result = spawnSync(process.execPath, [bin, ...cliArgs], { cwd: root, encoding: 'utf8', timeout: timeoutMs });
138
+ return spawnCapture([bin, ...cliArgs], { cwd: root, timeoutMs });
139
+ }
140
+
141
+ // spawnSync keeps 1 MB of stdout by default and kills the child past it
142
+ // (ENOBUFS). A certify-and-land sweep over 150+ review rows prints more than
143
+ // that, so the accept step read as unreadable on most ticks. Allow 64 MB.
144
+ const OWN_CLI_MAX_BUFFER = 64 * 1024 * 1024;
145
+
146
+ function spawnCapture(nodeArgs, { cwd, timeoutMs = 300000 } = {}) {
147
+ const result = spawnSync(process.execPath, nodeArgs, { cwd, encoding: 'utf8', timeout: timeoutMs, maxBuffer: OWN_CLI_MAX_BUFFER });
139
148
  return {
140
149
  status: result.status,
141
150
  stdout: String(result.stdout || ''),
@@ -1139,6 +1148,7 @@ module.exports = {
1139
1148
  digestNextMoves,
1140
1149
  acceptSweepTimeoutMs,
1141
1150
  describeAcceptKillCause,
1151
+ spawnCapture,
1142
1152
  digestStoryRows,
1143
1153
  digestTickStatus,
1144
1154
  engineAnswerValidationLine,
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
3
+ const { stateHome } = require('../lib/state-home');
4
4
  const { loadCredentials } = require('../utils/auth');
5
5
  const { apiRequestJson } = require('../utils/api');
6
6
 
@@ -34,7 +34,7 @@ async function browseAtris() {
34
34
  if (!result.ok) {
35
35
  // API failed, try local templates
36
36
  console.log(' API unavailable, checking local templates...');
37
- const localDir = path.join(os.homedir(), '.atris', 'templates');
37
+ const localDir = path.join(stateHome(), '.atris', 'templates');
38
38
  if (!fs.existsSync(localDir)) {
39
39
  console.error(' No templates found locally or from API.');
40
40
  process.exit(1);
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { stateHome } = require('../lib/state-home');
3
4
  const os = require('os');
4
5
  const { loadCredentials } = require('../utils/auth');
5
6
  const { apiRequestJson } = require('../utils/api');
@@ -18,8 +19,7 @@ function businessWorkspaceBase() {
18
19
  }
19
20
 
20
21
  function getBusinessConfigPath() {
21
- const home = require('os').homedir();
22
- const dir = path.join(home, '.atris');
22
+ const dir = path.join(stateHome(), '.atris');
23
23
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
24
24
  return path.join(dir, 'businesses.json');
25
25
  }
@@ -3139,7 +3139,7 @@ async function connectService(connector, ...flags) {
3139
3139
  }
3140
3140
 
3141
3141
  // Create local secrets directory
3142
- const secretsDir = path.join(require('os').homedir(), '.atris', 'secrets', connector);
3142
+ const secretsDir = path.join(stateHome(), '.atris', 'secrets', connector);
3143
3143
  if (!fs.existsSync(secretsDir)) {
3144
3144
  fs.mkdirSync(secretsDir, { recursive: true });
3145
3145
  console.log(`\n Created secrets dir: ${secretsDir}/`);
package/commands/buy.js CHANGED
@@ -12,8 +12,8 @@
12
12
 
13
13
  const { apiRequestJson } = require('../utils/api');
14
14
  const { loadCredentials } = require('../utils/auth');
15
- const { NOT_LOGGED_IN, tokenFrom, oneLine, printResult } = require('../lib/developer-api');
16
- const { parseFlags, formatCents, untilText, errorFrom } = require('../lib/commerce');
15
+ const { NOT_LOGGED_IN, oneLine, printResult } = require('../lib/developer-api');
16
+ const { parseFlags, formatCents, untilText, errorFrom, commerceToken } = require('../lib/commerce');
17
17
 
18
18
  const KINDS = ['ticket', 'shop', 'trade', 'flight'];
19
19
  const NOTHING_CHARGED = 'Nothing is charged until the person approves it themselves.';
@@ -218,7 +218,7 @@ async function buyCommand(args = [], deps = {}) {
218
218
 
219
219
  const load = deps.loadCredentials || loadCredentials;
220
220
  const request = deps.apiRequestJson || apiRequestJson;
221
- const token = tokenFrom(load());
221
+ const token = commerceToken(load(), 'commerce:quote', deps.now ? deps.now() : Date.now());
222
222
  if (!token) return fail(NOT_LOGGED_IN);
223
223
 
224
224
  try {
package/commands/deck.js CHANGED
@@ -15,8 +15,8 @@
15
15
 
16
16
  const fs = require('fs');
17
17
  const https = require('https');
18
- const os = require('os');
19
18
  const path = require('path');
19
+ const { stateHome } = require('../lib/state-home');
20
20
  const { hasFlag } = require('../lib/arg-parser');
21
21
  const { buildDeck, THEMES, notesRequests } = require('../lib/slides-deck');
22
22
  const {
@@ -34,7 +34,7 @@ const BASE = 'api.atris.ai';
34
34
  const PFX = '/api/integrations/google-slides';
35
35
 
36
36
  function token() {
37
- try { return require(path.join(os.homedir(), '.atris/credentials.json')).token; }
37
+ try { return require(path.join(stateHome(), '.atris/credentials.json')).token; }
38
38
  catch { return null; }
39
39
  }
40
40