atris 3.74.6 → 3.74.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/atris.js CHANGED
@@ -1967,6 +1967,15 @@ function showWelcomeVisualization() {
1967
1967
  // Silent: the lineup is a hint, not a gate.
1968
1968
  }
1969
1969
 
1970
+ // The four values settle close calls, so every new chat sees them, not
1971
+ // only an agent that reads atris.md to the end (lib/values.js).
1972
+ try {
1973
+ const values = require('../lib/values').valuesForRoot(cwd);
1974
+ values.forEach((value, index) => console.log(row(index ? '' : 'values', `${value.name.padEnd(14)}${value.gist}`)));
1975
+ } catch {
1976
+ // Advisory only; never blocks boot.
1977
+ }
1978
+
1970
1979
  const hasNoTasks = glance.active === 0
1971
1980
  && glance.review === 0
1972
1981
  && glance.reviewCertified === 0;
package/commands/idea.js CHANGED
@@ -31,6 +31,7 @@ function ideaUsage() {
31
31
  ' atris idea retry <n> [--repo <path>] [--verify "<cmd>"]',
32
32
  ' atris idea tick [--repo <path>] [--max-minutes <n>]',
33
33
  ' atris idea on|off [--repo <path>] [--dry-run]',
34
+ ' atris idea login|logout (the Claude login sealed builds use)',
34
35
  ].join('\n');
35
36
  }
36
37
 
@@ -213,6 +214,35 @@ function retryIdea(ref, { repo, verify = '', json, log }) {
213
214
  return 0;
214
215
  }
215
216
 
217
+ // One command for the login a sealed build needs: Claude's own setup-token
218
+ // runs on the person's terminal, the token is picked off the screen (or
219
+ // pasted when it cannot be), proven with a sign-in from a blank home, then
220
+ // saved where only the owner can read it. Nothing is saved when it fails.
221
+ function ideaLogin({ log = console.log, deps = {} } = {}) {
222
+ const login = { ...require('../lib/engine-login'), ...deps };
223
+ const tty = deps.isTTY !== undefined ? deps.isTTY : Boolean(process.stdin.isTTY && process.stdout.isTTY);
224
+ if (!tty) {
225
+ log('run this in your own terminal: atris idea login');
226
+ return 2;
227
+ }
228
+ log('a browser window opens for Claude. approve it there; there is nothing to copy.');
229
+ let token = login.findToken(login.recordSetupToken().text);
230
+ if (!token && login.askToken) token = String(login.askToken() || '').trim();
231
+ if (!token) {
232
+ log('no login came back from claude setup-token. nothing saved.');
233
+ return 1;
234
+ }
235
+ const check = login.checkToken(token);
236
+ if (!check.ok) {
237
+ log(`that login did not sign in${check.detail ? `: ${check.detail}` : ''}. nothing saved.`);
238
+ return 1;
239
+ }
240
+ login.saveToken(token);
241
+ log('done. sealed builds now sign in to Claude on your subscription.');
242
+ log('undo: atris idea logout');
243
+ return 0;
244
+ }
245
+
216
246
  function ideaAtris(args = process.argv.slice(3), { cwd = process.cwd(), log = console.log, keepDeps = {} } = {}) {
217
247
  const list = Array.isArray(args) ? args : [];
218
248
  if (argsWantHelp(list)) {
@@ -224,6 +254,12 @@ function ideaAtris(args = process.argv.slice(3), { cwd = process.cwd(), log = co
224
254
  const json = flags.json === true;
225
255
  const verb = words[0];
226
256
 
257
+ if (verb === 'login' && words.length === 1) return ideaLogin({ log, deps: keepDeps.login });
258
+ if (verb === 'logout' && words.length === 1) {
259
+ const { forgetToken } = require('../lib/engine-login');
260
+ log(forgetToken() ? 'sealed builds no longer have a Claude login.' : 'there was no saved login.');
261
+ return 0;
262
+ }
227
263
  if (verb === 'retry' && words.length <= 2) return retryIdea(words[1], { repo, verify: flags.verify || '', json, log });
228
264
  if ((verb === 'on' || verb === 'off') && words.length === 1) {
229
265
  return switchTimer(verb === 'on', { repo, dryRun: flags.dryRun === true, json, log, keepDeps });
package/commands/zen.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { abandonedDaysFromEnv, checkZen, sideCopyLines } = require('../lib/zen');
3
+ const { abandonedDaysFromEnv, checkZen, nextSaveIn, sideCopyLines } = require('../lib/zen');
4
4
  const { applyZen } = require('../lib/zen-apply');
5
5
  const { workspaceZen } = require('../lib/zen-workspace');
6
6
  const { fleetFile, fleetRepos, fleetZen, printFleet } = require('../lib/zen-fleet');
@@ -97,7 +97,9 @@ function printCheck(result, workspace) {
97
97
  console.log(' run atris zen to make it zen');
98
98
  }
99
99
  if (result.worktrees_with_work.length) {
100
- for (const text of sideCopyLines(result.worktrees_with_work, abandonedDaysFromEnv())) console.log(` ${text}`);
100
+ const days = abandonedDaysFromEnv();
101
+ const wait = nextSaveIn(result.worktrees_with_work, days);
102
+ for (const text of sideCopyLines(result.worktrees_with_work, days, wait)) console.log(` ${text}`);
101
103
  }
102
104
  const held = result.worktrees_locked?.length || 0;
103
105
  if (held) {
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ // The one Claude login a sealed build may use. A sealed build hides the real
4
+ // home, and the normal Claude login lives in the macOS keychain and refreshes
5
+ // itself, so a build cannot borrow it. `atris idea login` runs Claude's own
6
+ // `claude setup-token` (a long-lived login on the same subscription), keeps the
7
+ // token in a file only the owner can read, and the sealed build passes it in.
8
+
9
+ const fs = require('node:fs');
10
+ const os = require('node:os');
11
+ const path = require('node:path');
12
+ const { spawnSync } = require('node:child_process');
13
+ const { atrisHome } = require('./state-home');
14
+
15
+ const TOKEN_PATTERN = /sk-ant-oat\d{2}-[A-Za-z0-9_-]{20,}/g;
16
+
17
+ function tokenFile(env = process.env) {
18
+ return path.join(atrisHome(env), 'secrets', 'claude-oauth-token');
19
+ }
20
+
21
+ // The token from a recording of `claude setup-token`, colors and cursor moves
22
+ // stripped. The last one wins, since the screen may redraw.
23
+ function findToken(text) {
24
+ const plain = String(text || '').replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '').replace(/\x1b[\]P][^\x07\x1b]*(\x07|\x1b\\)/g, '');
25
+ const hits = plain.match(TOKEN_PATTERN);
26
+ return hits ? hits[hits.length - 1] : '';
27
+ }
28
+
29
+ function savedToken(env = process.env) {
30
+ try {
31
+ return fs.readFileSync(tokenFile(env), 'utf8').trim();
32
+ } catch {
33
+ return '';
34
+ }
35
+ }
36
+
37
+ function saveToken(token, env = process.env) {
38
+ const file = tokenFile(env);
39
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
40
+ fs.chmodSync(path.dirname(file), 0o700);
41
+ fs.writeFileSync(file, `${token}\n`, { mode: 0o600 });
42
+ fs.chmodSync(file, 0o600);
43
+ return file;
44
+ }
45
+
46
+ function forgetToken(env = process.env) {
47
+ const file = tokenFile(env);
48
+ if (!fs.existsSync(file)) return false;
49
+ fs.rmSync(file, { force: true });
50
+ return true;
51
+ }
52
+
53
+ // Runs `claude setup-token` on the person's own terminal while `script`
54
+ // records what it prints, then reads the recording and deletes it.
55
+ function recordSetupToken({ spawn = spawnSync, platform = process.platform } = {}) {
56
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-login-'));
57
+ fs.chmodSync(dir, 0o700);
58
+ const log = path.join(dir, 'screen');
59
+ try {
60
+ const args = platform === 'darwin'
61
+ ? ['-q', log, 'claude', 'setup-token']
62
+ : ['-q', '-c', 'claude setup-token', log];
63
+ const result = spawn('script', args, { stdio: 'inherit' });
64
+ const text = fs.existsSync(log) ? fs.readFileSync(log, 'utf8') : '';
65
+ return { status: result.status, text };
66
+ } finally {
67
+ fs.rmSync(dir, { recursive: true, force: true });
68
+ }
69
+ }
70
+
71
+ // When the token cannot be read off the screen, the person pastes it once,
72
+ // without it showing on screen.
73
+ function askToken({ spawn = spawnSync } = {}) {
74
+ const script = 'printf "paste the token claude printed (it stays hidden): " >&2; stty -echo; read -r t; stty echo; printf "\\n" >&2; printf %s "$t"';
75
+ const result = spawn('sh', ['-c', script], { stdio: ['inherit', 'pipe', 'inherit'], encoding: 'utf8' });
76
+ return String(result.stdout || '').trim();
77
+ }
78
+
79
+ // Signs in the way a sealed build does: a blank home, no keychain, only the
80
+ // token. Returns { ok, detail }.
81
+ function checkToken(token, { spawn = spawnSync } = {}) {
82
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-login-check-'));
83
+ try {
84
+ const result = spawn('claude', ['-p', 'Reply with the single word ok.', '--model', 'haiku',
85
+ '--settings', JSON.stringify({ disableAllHooks: true }), '--strict-mcp-config', '--tools', ''], {
86
+ cwd: home,
87
+ encoding: 'utf8',
88
+ timeout: 120000,
89
+ env: {
90
+ PATH: process.env.PATH || '',
91
+ HOME: home,
92
+ CLAUDE_CONFIG_DIR: path.join(home, '.claude'),
93
+ CLAUDE_CODE_OAUTH_TOKEN: token,
94
+ ANTHROPIC_API_KEY: '',
95
+ },
96
+ });
97
+ const out = `${result.stdout || ''}${result.stderr || ''}`.trim();
98
+ const ok = result.status === 0 && /\bok\b/i.test(result.stdout || '');
99
+ return { ok, detail: ok ? '' : (out.split('\n').pop() || `exit ${result.status}`).slice(0, 200) };
100
+ } finally {
101
+ fs.rmSync(home, { recursive: true, force: true });
102
+ }
103
+ }
104
+
105
+ module.exports = { findToken, savedToken, saveToken, forgetToken, recordSetupToken, askToken, checkToken };
package/lib/fleet.js CHANGED
@@ -1907,7 +1907,10 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
1907
1907
  // owner policy: claude work runs on the subscription login, never API
1908
1908
  // billing, so the api key is stripped even when the parent env has one.
1909
1909
  ANTHROPIC_API_KEY: '',
1910
- CLAUDE_CODE_OAUTH_TOKEN: selectedSecret(['claude'], 'CLAUDE_CODE_OAUTH_TOKEN'),
1910
+ // `atris idea login` saves this one login for sealed builds; a token
1911
+ // already in the environment wins.
1912
+ CLAUDE_CODE_OAUTH_TOKEN: selectedSecret(['claude'], 'CLAUDE_CODE_OAUTH_TOKEN')
1913
+ || (selectedEngine === 'claude' ? require('./engine-login').savedToken() : ''),
1911
1914
  CURSOR_API_KEY: selectedSecret(['cursor'], 'CURSOR_API_KEY'),
1912
1915
  DEVIN_API_KEY: selectedSecret(['devin'], 'DEVIN_API_KEY'),
1913
1916
  XAI_API_KEY: selectedSecret(['grok'], 'XAI_API_KEY'),