atris 3.63.0 → 3.64.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 (65) hide show
  1. package/atris/skills/commerce/SKILL.md +24 -0
  2. package/atris/skills/x-search/SKILL.md +3 -1
  3. package/ax +4 -3
  4. package/bin/atris.js +11 -2
  5. package/cli/atris_code.py +25 -1
  6. package/commands/auth.js +79 -10
  7. package/commands/autoland.js +11 -1
  8. package/commands/browse.js +2 -2
  9. package/commands/business.js +3 -3
  10. package/commands/buy.js +3 -3
  11. package/commands/deck.js +2 -2
  12. package/commands/engine.js +122 -13
  13. package/commands/feedback.js +2 -2
  14. package/commands/fork.js +2 -2
  15. package/commands/gm.js +2 -1
  16. package/commands/improve.js +142 -8
  17. package/commands/integrations.js +4 -3
  18. package/commands/loops.js +2 -1
  19. package/commands/member.js +47 -9
  20. package/commands/mission.js +1 -1
  21. package/commands/pack-craft.js +2 -2
  22. package/commands/publish.js +2 -2
  23. package/commands/radar.js +3 -2
  24. package/commands/router.js +2 -2
  25. package/commands/task.js +84 -2
  26. package/commands/transactions.js +3 -3
  27. package/commands/truth.js +2 -2
  28. package/commands/x-search.js +81 -10
  29. package/commands/youtube.js +2 -1
  30. package/lib/autoland.js +3 -2
  31. package/lib/ax-auto-lane.js +2 -2
  32. package/lib/brief-rules.js +11 -0
  33. package/lib/chat-log-scan.js +2 -1
  34. package/lib/ci-runner.js +3 -2
  35. package/lib/codex-flight.js +2 -1
  36. package/lib/commerce.js +17 -2
  37. package/lib/deck-history.js +2 -2
  38. package/lib/deck-review.js +3 -3
  39. package/lib/design-api.js +2 -2
  40. package/lib/engine-registry.js +85 -18
  41. package/lib/first-minute.js +2 -2
  42. package/lib/fleet.js +8 -36
  43. package/lib/git-spawn.js +5 -2
  44. package/lib/ignored-cache.js +51 -0
  45. package/lib/manifest.js +2 -2
  46. package/lib/map-refs.js +70 -4
  47. package/lib/pack-capabilities.js +2 -1
  48. package/lib/permission-grants.js +2 -2
  49. package/lib/pulse.js +5 -5
  50. package/lib/roster-runs.js +13 -2
  51. package/lib/runner-command.js +22 -6
  52. package/lib/sandbox-pids.js +113 -0
  53. package/lib/secret-gateway.js +3 -13
  54. package/lib/state-home.js +106 -0
  55. package/lib/task-db.js +4 -3
  56. package/lib/task-prune.js +462 -0
  57. package/lib/x-post-check.js +497 -0
  58. package/package.json +1 -1
  59. package/scripts/det/data/xsearch-answer.json +24 -0
  60. package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
  61. package/scripts/det/xsearch-bench.js +307 -0
  62. package/scripts/det/ytnotes +61 -15
  63. package/scripts/det/ytrail-bench.js +2 -1
  64. package/utils/auth.js +3 -3
  65. package/utils/update-check.js +2 -2
@@ -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
@@ -1011,6 +1011,9 @@ function showAuthHelp(commandName) {
1011
1011
  console.log(' --agent Mint a scoped agent token from the stored login.');
1012
1012
  console.log(' --scopes <list> Comma-separated scopes (default: x-search,youtube).');
1013
1013
  console.log(' --daily-credit-cap <n> Daily credit cap (default: 50).');
1014
+ console.log(' --commerce Commerce key: scopes commerce:quote,transactions:read. Can quote, never pay.');
1015
+ console.log(' --commerce-max-usd <n> Largest single quote a commerce key may ask for, 1 to 500 (default: 200).');
1016
+ console.log(' --print-key Print the minted key once, for an agent on another machine.');
1014
1017
  } else if (commandName === 'switch') {
1015
1018
  console.log(' --global, -g Switch the account for all terminals.');
1016
1019
  }
@@ -1856,9 +1859,15 @@ function showWelcomeVisualization() {
1856
1859
  // Every new chat sees which tool and model does each job, from the same
1857
1860
  // resolver as atris engine roster. A roster that cannot be read drops the
1858
1861
  // line; it never stops the boot.
1862
+ // A roster worker that ends within seven days gets one line, so the job
1863
+ // does not quietly fall to its backup when the date passes.
1859
1864
  try {
1860
- const teamLine = require('../lib/team-lineup').bootTeamLine(cwd, { width: 80 - 11 });
1865
+ const engineView = require('../commands/engine');
1866
+ const jobs = engineView.jobRosterView(cwd);
1867
+ const teamLine = require('../lib/team-lineup').bootTeamLine(cwd, { width: 80 - 11, jobs });
1861
1868
  if (teamLine) console.log(row('team', teamLine));
1869
+ const endsLine = engineView.bootExpiringLine(engineView.expiringWorkers(jobs));
1870
+ if (endsLine) console.log(row('roster', endsLine));
1862
1871
  } catch {
1863
1872
  // Silent: the lineup is a hint, not a gate.
1864
1873
  }
@@ -2981,7 +2990,7 @@ if (command === 'guide') {
2981
2990
  const subcommand = process.argv[3];
2982
2991
  const args = process.argv.slice(4);
2983
2992
  Promise.resolve(require('../commands/member').memberCommand(subcommand, ...args))
2984
- .then(() => process.exit(subcommand === 'run' ? (process.exitCode || 0) : 0))
2993
+ .then(() => process.exit(process.exitCode || 0))
2985
2994
  .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
2986
2995
  } else if (command === 'app') {
2987
2996
  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
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