atris 3.65.0 → 3.67.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.
package/README.md CHANGED
@@ -173,6 +173,33 @@ No proof, no AgentXP. `atris task done --proof` records review/RL context, but
173
173
  only human `atris task accept` mints Career XP for the local card or hosted
174
174
  leaderboard.
175
175
 
176
+ ## Agents That Sign In On Their Own
177
+
178
+ `atris login` signs a person in. An agent can sign itself in with an agent
179
+ client, so no one has to hand it a login token.
180
+
181
+ The owner makes the client once. The secret is shown one time, or written to a
182
+ private file with `--secret-file`:
183
+
184
+ ```bash
185
+ atris agent-client create --name nightly-bot --scopes mcp:read,x-search --secret-file ./bot.secret
186
+ atris agent-client list
187
+ atris agent-client rotate <client_id>
188
+ atris agent-client revoke <client_id>
189
+ ```
190
+
191
+ The agent signs in with the id and the secret file. Its short token renews on
192
+ its own; the secret and token are never printed:
193
+
194
+ ```bash
195
+ atris login --client-id atc_... --client-secret-file ./bot.secret
196
+ ```
197
+
198
+ Or set `ATRIS_CLIENT_ID` plus `ATRIS_CLIENT_SECRET_FILE` (or
199
+ `ATRIS_CLIENT_SECRET`) and run `atris login` once. It reads them, no flags
200
+ needed, and opens no browser. After that every command renews the token on
201
+ its own. `ATRIS_TOKEN`, when set, still wins.
202
+
176
203
  ## Business Owners
177
204
 
178
205
  If you want a shared owner for a company, lab, collective, community, artist, team, or project, use the business command instead of raw `atris init`.
@@ -95,7 +95,7 @@ Smoke: `reply with the word OK`, run from `$HOME`, 120s cap, one at a time on a
95
95
  | Cursor | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` (run from the target repo) | `composer-2.5` | 2026-09-23 | 37.5 | `--trust` required for non-interactive. |
96
96
  | Fable | `atris engine fable --timeout 120 --json "<question>"` | `claude-fable-5` | 2026-09-23 | 21.4 | Canonical read-only FABLE ask with receipt; scale `--timeout` to the work. |
97
97
  | Composer | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` | `composer-2.5` via Cursor | 2026-09-23 | 37.5 | Same binary as Cursor. |
98
- | Haiku | `claude -p "<prompt>" --model claude-haiku-4-5 --output-format json --no-session-persistence` | `claude-haiku-4-5` | 2026-09-23 | 19.0 | Fast validation and bounded read-only checks. |
98
+ | Haiku | `claude -p "<prompt>" --model claude-haiku-5-5 --output-format json --no-session-persistence` | `claude-haiku-5-5` | 2026-10-07 | 9.5 | Fast validation and bounded read-only checks. |
99
99
  | Devin (build) | `devin -p --permission-mode dangerous --model swe-2-max --prompt-file <brief>` (run from an isolated worktree) | `swe-2-max` (Free tag in `devin models list`, 2026-09-23) | 2026-09-23 | 14.0 | In `-p` mode the default permission mode rejects shell commands too, so even a read-only audit dies after one step (2026-09-23). Keep the brief outside the worktree. Long runs (5+ min) can print nothing even on success: judge by the worktree diff. `devin cloud` outlives this machine. |
100
100
  | Devin (search) | `devin -p --permission-mode dangerous --model swe-1.7-lightning -- "<prompt>"` | `swe-1.7-lightning` (paid, no Free tag on 2026-09-23) | 2026-09-23 | 13.9 | |
101
101
  | Grok | `grok --always-approve --model grok-4.7 -p "<prompt>"` (run from the target repo) | `grok-4.7` | 2026-09-23 | 11.7 | Unpinned default is now `grok-4.7-build-fast`; `grok models` lists the menu. Uses grok.com login. `--best-of-n <N>` for tricky bounded builds. |
@@ -119,7 +119,7 @@ Headless dispatch permissions (verified 2026-08-11): `codex exec`, `grok`, and `
119
119
 
120
120
  | Engine | Flag | Alternates on the live menu |
121
121
  |--------|------|-----------------------------|
122
- | Claude | `--model <id>` | `claude-opus-5-5`, `claude-haiku-4-5` |
122
+ | Claude | `--model <id>` | `claude-opus-5-5`, `claude-haiku-5-5` |
123
123
  | Devin | `--model <id>` | `swe-2-max`, `swe-2-high`, `swe-2-medium` (Free); `claude-opus-5-5-high`, `gpt-6-sol` (paid) |
124
124
  | Cursor | `--model <id>` | `composer-2.5`, `grok-4.7-xhigh`, `claude-opus-5-thinking-high`; `--list-models` |
125
125
  | Grok | `--model <id>` | `grok-4.7`, `grok-4.7-build-fast` (default), `grok-4.6`, `grok-4.5`; `grok models` |
package/bin/atris.js CHANGED
@@ -632,6 +632,7 @@ function showHelpAll() {
632
632
  console.log(' rsi - Read the Dream-RSI attempt ledger (trees, attempts, policy, dreams)');
633
633
  console.log(` autoland - Approve the policy once; ${require('../lib/autoland').certifiedWorkLandsPhrase(process.cwd())}, you keep irreversible calls`);
634
634
  console.log(' engine - engine registry, answer validation, dispatch flights, and live progress');
635
+ console.log(' link - What two things could become together: one line, why, a first thing to make, who makes it');
635
636
  console.log(' ci - run github actions jobs locally with runs-on: atris');
636
637
  console.log(' router - inspect ax lane outcomes and promote gated reflex overrides');
637
638
  console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
@@ -716,6 +717,7 @@ function showHelpAll() {
716
717
  console.log(' balance - Show credit balance in dollars');
717
718
  console.log(' usage - Show developer API usage');
718
719
  console.log(' api-key - Create, list, rotate, or revoke a developer API key');
720
+ console.log(' agent-client - Let an agent sign itself in: create, list, revoke, rotate (secret shown once)');
719
721
  console.log(' topup - Buy credits and print a Stripe checkout URL');
720
722
  console.log(' buy - Quote a purchase or stock order; the person approves before anything is charged');
721
723
  console.log(' transactions - List what you bought, paid in fees, traded, or sent');
@@ -752,6 +754,7 @@ function showHelpAll() {
752
754
  console.log(' member list - Show all team members');
753
755
  console.log(' member activate <n> - Activate a member (link skills, show context)');
754
756
  console.log(' member upgrade <n> - Convert flat file to directory format');
757
+ console.log(' member result <n> - Newest run of a member: run record, receipt, artifacts, result number');
755
758
  console.log('');
756
759
  console.log('Plugin:');
757
760
  console.log(' plugin build - Package skills as .plugin for Cowork');
@@ -981,7 +984,7 @@ function showReleaseHelp() {
981
984
 
982
985
  function showAuthHelp(commandName) {
983
986
  const usage = {
984
- login: 'Usage: atris login [--token <token>] [--force] [--agent]',
987
+ login: 'Usage: atris login [--token <token>] [--force] [--agent] [--client-id <atc_...> --client-secret-file <path>]',
985
988
  logout: 'Usage: atris logout',
986
989
  whoami: 'Usage: atris whoami [--json]',
987
990
  switch: 'Usage: atris switch [account] [--global]',
@@ -995,6 +998,8 @@ function showAuthHelp(commandName) {
995
998
  if (commandName === 'login') {
996
999
  console.log(' Sign in with browser OAuth or a pasted API token.');
997
1000
  console.log(' --agent mints a scoped agent token from stored credentials. No browser.');
1001
+ console.log(' --client-id signs an agent in by itself with an agent client (see atris agent-client).');
1002
+ console.log(' The token renews on its own; the secret is never printed.');
998
1003
  } else if (commandName === 'logout') {
999
1004
  console.log(' Sign out of the current Atris account.');
1000
1005
  } else if (commandName === 'whoami') {
@@ -1017,6 +1022,10 @@ function showAuthHelp(commandName) {
1017
1022
  console.log(' --commerce Commerce key: scopes commerce:quote,transactions:read. Can quote, never pay.');
1018
1023
  console.log(' --commerce-max-usd <n> Largest single quote a commerce key may ask for, 1 to 500 (default: 200).');
1019
1024
  console.log(' --print-key Print the minted key once, for an agent on another machine.');
1025
+ console.log(' --client-id <atc_...> Sign in as an agent client (no browser, no person).');
1026
+ console.log(' --client-secret-file <p> File holding the agent client secret (chmod 600).');
1027
+ console.log(' --scope "a b" Ask for fewer scopes than the client has.');
1028
+ console.log(' Or set ATRIS_CLIENT_ID plus ATRIS_CLIENT_SECRET_FILE or ATRIS_CLIENT_SECRET, then run atris login once.');
1020
1029
  } else if (commandName === 'switch') {
1021
1030
  console.log(' --global, -g Switch the account for all terminals.');
1022
1031
  }
@@ -1776,9 +1785,28 @@ function showWelcomeVisualization() {
1776
1785
  // Document health is advisory and must never prevent startup.
1777
1786
  }
1778
1787
 
1788
+ // What the background cleanup did lately, in its own plain words: the
1789
+ // newest line the hourly autoland tick or a project job left in the
1790
+ // journal, plus agent branches parked for a person.
1791
+ try {
1792
+ const lately = require('../lib/cleanup-summary').latestCleanupLine(cwd);
1793
+ if (lately) console.log(row('lately', lately));
1794
+ } catch {
1795
+ // Advisory only; never blocks boot.
1796
+ }
1797
+
1798
+ // When the hourly autoland tick is alive it lands, salvages, and clears
1799
+ // finished branches and old agent copies itself, so boot must not hand the
1800
+ // owner that chore ("39 finished pieces to put away"). Only a dead or
1801
+ // missing heartbeat brings the tidy counts and `land --reap` back.
1802
+ let autolandAlive = false;
1803
+ try { autolandAlive = require('../lib/autoland').heartbeatIsLive(cwd); } catch { autolandAlive = false; }
1804
+
1779
1805
  // landSummary is expensive (git board classification) - compute once per boot.
1780
1806
  let landInfo = null;
1781
- try { landInfo = require('../commands/land').landSummary(cwd); } catch (err) { landInfo = null; }
1807
+ if (!autolandAlive) {
1808
+ try { landInfo = require('../commands/land').landSummary(cwd); } catch (err) { landInfo = null; }
1809
+ }
1782
1810
  let rotInfo = null;
1783
1811
  try {
1784
1812
  const { parseLessons } = require('../lib/memory-view');
@@ -1792,7 +1820,7 @@ function showWelcomeVisualization() {
1792
1820
  worktreeDir = path.join(path.dirname(resolved), '.agent-worktrees', path.basename(resolved));
1793
1821
  }
1794
1822
  let worktrees = 0;
1795
- if (fs.existsSync(worktreeDir)) {
1823
+ if (!autolandAlive && fs.existsSync(worktreeDir)) {
1796
1824
  worktrees = fs.readdirSync(worktreeDir, { withFileTypes: true })
1797
1825
  .filter((entry) => entry.isDirectory()).length;
1798
1826
  }
@@ -2374,6 +2402,11 @@ if (command === 'guide') {
2374
2402
  Promise.resolve(require('../commands/usage').usageCommand(process.argv.slice(3)))
2375
2403
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
2376
2404
  .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2405
+ } else if (command === 'agent-client') {
2406
+ // Owner side of agent self-login: create, list, revoke, rotate agent clients.
2407
+ Promise.resolve(require('../commands/agent-client').agentClientCommand(process.argv.slice(3)))
2408
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2409
+ .catch((err) => { console.error(require('../utils/agent-client').scrub(err && err.message, [])); process.exit(1); });
2377
2410
  } else if (command === 'api-key') {
2378
2411
  Promise.resolve(require('../commands/api-key').apiKeyCommand(process.argv.slice(3)))
2379
2412
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
@@ -3171,6 +3204,11 @@ if (command === 'guide') {
3171
3204
  Promise.resolve(require('../commands/engine').engineCommand(engineArgs))
3172
3205
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
3173
3206
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
3207
+ } else if (command === 'link') {
3208
+ // Link: ask the roster's engine what two things could become together.
3209
+ Promise.resolve(require('../commands/link').linkCommand(process.argv.slice(3)))
3210
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
3211
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
3174
3212
  } else if (command === 'playbook') {
3175
3213
  Promise.resolve()
3176
3214
  .then(() => require('../commands/playbook').playbookCommand(process.argv.slice(3)))
@@ -0,0 +1,266 @@
1
+ 'use strict';
2
+
3
+ // Owner side of agent self-login. A person creates an agent client once and
4
+ // hands its id and secret to the agent; the agent then signs itself in with
5
+ // `atris login --client-id ... --client-secret-file ...`.
6
+ //
7
+ // The secret is shown exactly once, at create or rotate, or written to a
8
+ // private file with --secret-file. It never goes to stderr or error text.
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { apiRequestJson } = require('../utils/api');
13
+ const { ensureValidCredentials, readActiveCredentials } = require('../utils/auth');
14
+ const agentClient = require('../utils/agent-client');
15
+ const { readFlag } = require('../lib/arg-parser');
16
+ const { wantsJson, wantsHelp, oneLine } = require('../lib/developer-api');
17
+
18
+ const BASE_PATH = '/auth/agent-clients';
19
+ const ACTIONS = new Set(['create', 'list', 'revoke', 'rotate']);
20
+ const VALUE_FLAGS = ['--name', '--scopes', '--daily-credit-cap', '--ttl', '--secret-file'];
21
+ const SHOWN_ONCE = 'store this secret now, it will not be shown again:';
22
+ const AGENT_CLIENT_CANNOT_MANAGE = 'agent clients are managed with your own login, not an agent client. run: atris switch <you> or atris login --force';
23
+
24
+ function showAgentClientHelp(log = console.log) {
25
+ log('usage: atris agent-client create --name <name> --scopes <a,b> [--daily-credit-cap N] [--ttl SECONDS] [--secret-file <path>]');
26
+ log(' atris agent-client list');
27
+ log(' atris agent-client revoke <client_id>');
28
+ log(' atris agent-client rotate <client_id> [--secret-file <path>]');
29
+ log('');
30
+ log('an agent client lets an agent sign itself in, with no person in the loop.');
31
+ log('create and rotate show the secret once. --secret-file writes it to a private file instead.');
32
+ log('the agent then runs: atris login --client-id <atc_...> --client-secret-file <path>');
33
+ log('needs your own login. add --json for machine output.');
34
+ }
35
+
36
+ function positiveInt(raw, label) {
37
+ const value = Number(raw);
38
+ if (!Number.isInteger(value) || value <= 0) return { error: `${label} must be a positive whole number` };
39
+ return { value };
40
+ }
41
+
42
+ function parseAgentClientArgs(args = []) {
43
+ const positionals = [];
44
+ for (let i = 0; i < args.length; i += 1) {
45
+ const arg = String(args[i]);
46
+ if (arg === '--json') continue;
47
+ const valueFlag = VALUE_FLAGS.find((flag) => arg === flag || arg.startsWith(`${flag}=`));
48
+ if (valueFlag) {
49
+ if (arg === valueFlag) {
50
+ const next = args[i + 1];
51
+ if (next == null || String(next).startsWith('--')) return { error: `${valueFlag} needs a value` };
52
+ i += 1;
53
+ }
54
+ continue;
55
+ }
56
+ if (arg.startsWith('--')) return { error: `unknown agent-client option: ${arg}` };
57
+ positionals.push(arg);
58
+ }
59
+
60
+ const parsed = {
61
+ action: positionals[0] || '',
62
+ clientId: positionals[1] || '',
63
+ name: readFlag(args, '--name', ''),
64
+ scopes: agentClient.scopeList(readFlag(args, '--scopes', '')),
65
+ secretFile: readFlag(args, '--secret-file', ''),
66
+ dailyCreditCap: null,
67
+ ttlSeconds: null,
68
+ };
69
+ const cap = readFlag(args, '--daily-credit-cap', '');
70
+ if (cap) {
71
+ const checked = positiveInt(cap, '--daily-credit-cap');
72
+ if (checked.error) return checked;
73
+ parsed.dailyCreditCap = checked.value;
74
+ }
75
+ const ttl = readFlag(args, '--ttl', '');
76
+ if (ttl) {
77
+ const checked = positiveInt(ttl, '--ttl');
78
+ if (checked.error) return checked;
79
+ parsed.ttlSeconds = checked.value;
80
+ }
81
+
82
+ if (!ACTIONS.has(parsed.action)) return { error: 'usage: atris agent-client create|list|revoke|rotate' };
83
+ if (parsed.action === 'create') {
84
+ if (!parsed.name) return { error: 'create needs --name <name>' };
85
+ if (parsed.scopes.length === 0) return { error: 'create needs --scopes <a,b>' };
86
+ }
87
+ if ((parsed.action === 'revoke' || parsed.action === 'rotate') && !parsed.clientId) {
88
+ return { error: `usage: atris agent-client ${parsed.action} <client_id>` };
89
+ }
90
+ if (parsed.clientId && !parsed.clientId.startsWith(agentClient.CLIENT_ID_PREFIX)) {
91
+ return { error: `client ids start with ${agentClient.CLIENT_ID_PREFIX}` };
92
+ }
93
+ if (parsed.secretFile && parsed.action !== 'create' && parsed.action !== 'rotate') {
94
+ return { error: '--secret-file only works with create and rotate' };
95
+ }
96
+ return parsed;
97
+ }
98
+
99
+ // Check before calling the backend: a secret that cannot be saved is lost.
100
+ function checkSecretFileTarget(filePath) {
101
+ const resolved = path.resolve(filePath);
102
+ try {
103
+ if (fs.statSync(resolved).isDirectory()) return { error: `${resolved} is a folder, not a file` };
104
+ } catch (error) {
105
+ if (error.code !== 'ENOENT') return { error: `cannot use ${resolved} (${error.code})` };
106
+ }
107
+ const dir = path.dirname(resolved);
108
+ try {
109
+ fs.accessSync(dir, fs.constants.W_OK);
110
+ } catch {
111
+ return { error: `cannot write to ${dir}` };
112
+ }
113
+ return { path: resolved };
114
+ }
115
+
116
+ function clientsFrom(data) {
117
+ if (Array.isArray(data)) return data;
118
+ if (!data || typeof data !== 'object') return [];
119
+ for (const key of ['clients', 'agent_clients', 'items', 'data']) {
120
+ if (Array.isArray(data[key])) return data[key];
121
+ }
122
+ return [];
123
+ }
124
+
125
+ function scopesText(scopes) {
126
+ const list = agentClient.scopeList(scopes);
127
+ return list.length ? list.join(', ') : 'none';
128
+ }
129
+
130
+ function listText(data) {
131
+ const clients = clientsFrom(data);
132
+ if (clients.length === 0) return 'no agent clients yet. make one: atris agent-client create --name <name> --scopes <a,b>';
133
+ return clients.map((client) => {
134
+ const parts = [client.client_id || client.id || '?', client.name || 'unnamed', `scopes: ${scopesText(client.scopes)}`];
135
+ if (client.revoked_at) parts.push('revoked');
136
+ else if (client.last_used_at) parts.push(`last used ${client.last_used_at}`);
137
+ return parts.join(' ');
138
+ }).join('\n');
139
+ }
140
+
141
+ // Everything the backend sent, minus the secret.
142
+ function withoutSecret(data) {
143
+ const { client_secret, ...rest } = data && typeof data === 'object' ? data : {};
144
+ return rest;
145
+ }
146
+
147
+ async function agentClientCommand(args = [], deps = {}) {
148
+ const log = deps.log || console.log;
149
+ const err = deps.err || console.error;
150
+ if (args.length === 0 || wantsHelp(args)) {
151
+ showAgentClientHelp(log);
152
+ return args.length === 0 ? 1 : 0;
153
+ }
154
+
155
+ const json = wantsJson(args);
156
+ const fail = (message, extra = {}) => {
157
+ if (json) log(JSON.stringify({ ok: false, error: message, ...extra }));
158
+ else err(message);
159
+ return 1;
160
+ };
161
+
162
+ const parsed = parseAgentClientArgs(args);
163
+ if (parsed.error) return fail(parsed.error);
164
+
165
+ let secretTarget = null;
166
+ if (parsed.secretFile) {
167
+ secretTarget = checkSecretFileTarget(parsed.secretFile);
168
+ if (secretTarget.error) return fail(secretTarget.error);
169
+ }
170
+
171
+ const readActive = deps.readActiveCredentials || readActiveCredentials;
172
+ if (agentClient.isAgentClientCredential(readActive())) {
173
+ return fail(AGENT_CLIENT_CANNOT_MANAGE);
174
+ }
175
+
176
+ const request = deps.apiRequestJson || apiRequestJson;
177
+ const ensure = deps.ensureValidCredentials || ((opts) => ensureValidCredentials(request, opts));
178
+ const ensured = await ensure();
179
+ if (!ensured || ensured.error || !ensured.credentials || !ensured.credentials.token) {
180
+ return fail('not logged in. run: atris login');
181
+ }
182
+ if (ensured.source === agentClient.AUTH_TYPE || agentClient.isAgentClientCredential(ensured.credentials)) {
183
+ return fail(AGENT_CLIENT_CANNOT_MANAGE);
184
+ }
185
+ const token = ensured.credentials.token;
186
+
187
+ let result;
188
+ try {
189
+ if (parsed.action === 'list') {
190
+ result = await request(BASE_PATH, { method: 'GET', token });
191
+ } else if (parsed.action === 'create') {
192
+ const body = { name: parsed.name, scopes: parsed.scopes };
193
+ if (parsed.dailyCreditCap != null) body.daily_credit_cap = parsed.dailyCreditCap;
194
+ if (parsed.ttlSeconds != null) body.token_ttl_seconds = parsed.ttlSeconds;
195
+ result = await request(BASE_PATH, { method: 'POST', token, body, retries: 0 });
196
+ } else if (parsed.action === 'revoke') {
197
+ result = await request(`${BASE_PATH}/${encodeURIComponent(parsed.clientId)}`, { method: 'DELETE', token });
198
+ } else {
199
+ result = await request(`${BASE_PATH}/${encodeURIComponent(parsed.clientId)}/rotate-secret`, {
200
+ method: 'POST',
201
+ token,
202
+ retries: 0,
203
+ });
204
+ }
205
+ } catch (error) {
206
+ return fail(agentClient.scrub(error && error.message, [token]));
207
+ }
208
+
209
+ if (!result || !result.ok) {
210
+ const status = result && result.status;
211
+ const secret = result && result.data && result.data.client_secret;
212
+ const message = status === 404 && parsed.action !== 'revoke' && parsed.action !== 'rotate'
213
+ ? 'agent clients are not live on the server yet. try again after the backend update.'
214
+ : agentClient.scrub(oneLine(result && (result.data || result.error)), [token, secret]);
215
+ return fail(message, { status: status || 0 });
216
+ }
217
+
218
+ const data = result.data && typeof result.data === 'object' ? result.data : {};
219
+
220
+ if (parsed.action === 'list') {
221
+ if (json) log(JSON.stringify({ ok: true, clients: clientsFrom(data).map(withoutSecret) }));
222
+ else log(listText(data));
223
+ return 0;
224
+ }
225
+
226
+ if (parsed.action === 'revoke') {
227
+ if (json) log(JSON.stringify({ ok: true, client_id: parsed.clientId, revoked: true, ...withoutSecret(data) }));
228
+ else log(`revoked ${parsed.clientId}. its tokens stop working now.`);
229
+ return 0;
230
+ }
231
+
232
+ // create or rotate: the one moment the secret exists outside the server.
233
+ const secret = typeof data.client_secret === 'string' ? data.client_secret : '';
234
+ const clientId = data.client_id || parsed.clientId;
235
+ if (!secret) return fail('the server did not return a client secret');
236
+
237
+ if (secretTarget) {
238
+ try {
239
+ agentClient.writeFileAtomic(secretTarget.path, `${secret}\n`);
240
+ } catch (error) {
241
+ return fail(`could not write ${secretTarget.path}${error.code ? ` (${error.code})` : ''}. run atris agent-client rotate ${clientId} --secret-file <other path> to get a new secret.`);
242
+ }
243
+ }
244
+
245
+ if (json) {
246
+ const payload = { ok: true, ...withoutSecret(data), client_id: clientId };
247
+ if (secretTarget) payload.secret_file = secretTarget.path;
248
+ else payload.client_secret = secret;
249
+ log(JSON.stringify(payload));
250
+ return 0;
251
+ }
252
+
253
+ log(parsed.action === 'create' ? `created agent client ${data.name || parsed.name}` : `new secret for ${clientId}. the old one stops working.`);
254
+ log(`client id: ${clientId}`);
255
+ if (data.scopes) log(`scopes: ${scopesText(data.scopes)}`);
256
+ if (secretTarget) {
257
+ log(`secret saved to ${secretTarget.path} (only you can read it). it will not be shown again.`);
258
+ } else {
259
+ log(SHOWN_ONCE);
260
+ log(secret);
261
+ }
262
+ log(`agent sign-in: atris login --client-id ${clientId} --client-secret-file ${secretTarget ? secretTarget.path : '<file holding the secret>'}`);
263
+ return 0;
264
+ }
265
+
266
+ module.exports = { agentClientCommand, parseAgentClientArgs };