atris 3.66.0 → 3.68.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/commands/auth.js CHANGED
@@ -1,7 +1,8 @@
1
- const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
1
+ const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, loadCredentials, saveCredentials, saveAgentClientCredentials, preserveAgentClientLogin, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
2
2
  const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
3
3
  const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
4
4
  const { hasFlag, readFlag } = require('../lib/arg-parser');
5
+ const agentClient = require('../utils/agent-client');
5
6
  const fs = require('fs');
6
7
  const path = require('path');
7
8
 
@@ -128,7 +129,8 @@ function isAgentAccessToken(token) {
128
129
  function scopedTokenCandidate(credentials = {}) {
129
130
  if (credentials.agent_token) return credentials.agent_token;
130
131
  if (
131
- credentials.source === 'env'
132
+ credentials.source === agentClient.AUTH_TYPE
133
+ || credentials.source === 'env'
132
134
  || credentials.source === 'agent_token_file'
133
135
  || isAgentAccessToken(credentials.token)
134
136
  ) {
@@ -290,11 +292,16 @@ async function ensureBilledCommandAuth(scope, deps = {}) {
290
292
  const scopes = claims?.scopes || credentials.agent_token_scopes || credentials.scopes || [];
291
293
  const expiry = claims?.exp
292
294
  ? claims.exp * 1000
293
- : Date.parse(credentials.agent_token_expires_at || credentials.expires_at);
295
+ : Date.parse(credentials.agent_token_expires_at || credentials.expires_at || credentials.token_expires_at);
294
296
  if (!deps.forceMint && candidate && Array.isArray(scopes) && scopes.includes(wanted) && Number.isFinite(expiry) && expiry > Date.now()) {
295
297
  return { ok: true, token: candidate, minted: false, credentials };
296
298
  }
297
299
 
300
+ if (credentials.source === agentClient.AUTH_TYPE) {
301
+ // An agent client cannot mint itself more scope; the owner grants it.
302
+ return { ok: false, error: `this agent client does not have the ${wanted} scope. ask its owner to create one that does.` };
303
+ }
304
+
298
305
  if (!canMintFromLogin(credentials)) {
299
306
  return { ok: false, error: NO_STORED_JWT_MESSAGE };
300
307
  }
@@ -382,6 +389,26 @@ async function mintAgentToken(args = [], deps = {}) {
382
389
 
383
390
  async function printWhoamiPayload(asJson) {
384
391
  const ensured = await ensureValidCredentials(apiRequestJson);
392
+ if (!ensured.error && ensured.source === 'agent_client') {
393
+ const creds = ensured.credentials || {};
394
+ const payload = {
395
+ ok: true,
396
+ logged_in: true,
397
+ auth_type: agentClient.AUTH_TYPE,
398
+ client_id: creds.client_id,
399
+ scopes: creds.scopes || [],
400
+ token_expires_at: creds.token_expires_at || null,
401
+ };
402
+ if (asJson) {
403
+ console.log(JSON.stringify(payload, null, 2));
404
+ } else {
405
+ console.log('Status: Logged in ✓ (agent client)');
406
+ console.log(`Client ID: ${payload.client_id}`);
407
+ console.log(`Scopes: ${payload.scopes.length ? payload.scopes.join(', ') : 'unknown'}`);
408
+ if (payload.token_expires_at) console.log(`Token renews before: ${payload.token_expires_at}`);
409
+ }
410
+ process.exit(0);
411
+ }
385
412
  if (ensured.error) {
386
413
  if (!asJson && ensured.detail === AGENT_TOKEN_EXPIRED_DETAIL) {
387
414
  console.error('the agent key expired.');
@@ -430,6 +457,144 @@ async function printWhoamiPayload(asJson) {
430
457
  process.exit(0);
431
458
  }
432
459
 
460
+ function flagPresent(args, name) {
461
+ return args.some((arg) => arg === name || String(arg).startsWith(`${name}=`));
462
+ }
463
+
464
+ function envValue(env, name) {
465
+ return typeof env[name] === 'string' ? env[name].trim() : '';
466
+ }
467
+
468
+ // Decide whether this `atris login` is an agent client sign-in. Returns null
469
+ // for every other login path so those stay exactly as they were.
470
+ function agentClientLoginRequest(args = [], env = process.env) {
471
+ if (flagPresent(args, '--client-secret')) {
472
+ return { error: 'do not put the secret on the command line. use --client-secret-file <path> or ATRIS_CLIENT_SECRET.' };
473
+ }
474
+ const usesFlags = flagPresent(args, '--client-id') || flagPresent(args, '--client-secret-file');
475
+ const envId = envValue(env, 'ATRIS_CLIENT_ID');
476
+ if (!usesFlags && (!envId || flagPresent(args, '--token') || wantsAgentToken(args))) return null;
477
+ const clientId = readFlag(args, '--client-id', '') || envId;
478
+ if (!clientId) return { error: '--client-id needs a client id (atc_...)' };
479
+ if (!clientId.startsWith(agentClient.CLIENT_ID_PREFIX)) {
480
+ return { error: `client ids start with ${agentClient.CLIENT_ID_PREFIX}` };
481
+ }
482
+ const flagFile = readFlag(args, '--client-secret-file', '');
483
+ if (flagPresent(args, '--client-secret-file') && !flagFile) {
484
+ return { error: '--client-secret-file needs a path' };
485
+ }
486
+ const secretFile = flagFile || envValue(env, 'ATRIS_CLIENT_SECRET_FILE');
487
+ if (!secretFile && !envValue(env, 'ATRIS_CLIENT_SECRET')) {
488
+ return { error: 'no client secret. pass --client-secret-file <path>, or set ATRIS_CLIENT_SECRET_FILE or ATRIS_CLIENT_SECRET.' };
489
+ }
490
+ return {
491
+ clientId,
492
+ secretFile: secretFile ? path.resolve(secretFile) : null,
493
+ scope: agentClient.scopeList(readFlag(args, '--scope', '') || readFlag(args, '--scopes', '')).join(' ') || null,
494
+ };
495
+ }
496
+
497
+ // Keep the person's own login reachable with `atris switch` after the agent
498
+ // client takes over credentials.json.
499
+ function keepHumanLoginAsProfile(nextClientId) {
500
+ try {
501
+ const stored = JSON.parse(fs.readFileSync(getCredentialsPath(), 'utf8'));
502
+ if (agentClient.isAgentClientCredential(stored)) {
503
+ // A different agent client was signed in: keep it switchable too.
504
+ return stored.client_id === nextClientId ? null : preserveAgentClientLogin();
505
+ }
506
+ if (!stored || !stored.token) return null;
507
+ const name = profileNameFromEmail(stored.email);
508
+ if (!name) return null;
509
+ if (!loadProfile(name)) saveProfile(name, stored);
510
+ return name;
511
+ } catch {
512
+ return null;
513
+ }
514
+ }
515
+
516
+ async function loginAgentClient(request, deps = {}) {
517
+ const env = deps.env || process.env;
518
+ const json = Boolean(deps.json);
519
+ const output = deps.output || console.log;
520
+ const outputError = deps.outputError || console.error;
521
+ const fail = (message, code) => {
522
+ if (json) output(JSON.stringify({ ok: false, logged_in: false, error: code || 'agent_client_login_failed', detail: message }, null, 2));
523
+ else outputError(message);
524
+ return 1;
525
+ };
526
+
527
+ const warnings = [];
528
+ if (request.secretFile) {
529
+ let stat;
530
+ try {
531
+ stat = fs.statSync(request.secretFile);
532
+ } catch (error) {
533
+ return fail(`could not read the client secret file ${request.secretFile}${error.code ? ` (${error.code})` : ''}`, 'secret_missing');
534
+ }
535
+ if (!stat.isFile()) return fail(`${request.secretFile} is not a file`, 'secret_missing');
536
+ if (process.platform !== 'win32' && (stat.mode & 0o077)) {
537
+ warnings.push(`other users on this machine can read ${request.secretFile}. run: chmod 600 ${request.secretFile}`);
538
+ }
539
+ }
540
+
541
+ const pending = {
542
+ auth_type: agentClient.AUTH_TYPE,
543
+ client_id: request.clientId,
544
+ client_secret_file: request.secretFile,
545
+ client_secret_env: request.secretFile ? null : 'ATRIS_CLIENT_SECRET',
546
+ scope: request.scope,
547
+ };
548
+
549
+ let secret;
550
+ let entry;
551
+ try {
552
+ secret = agentClient.readClientSecret(pending, env);
553
+ entry = await agentClient.requestClientToken({ clientId: request.clientId, secret, scope: request.scope }, deps);
554
+ } catch (error) {
555
+ return fail(agentClient.scrub(error && error.message, [secret]), error && error.code);
556
+ }
557
+
558
+ let secretFile = request.secretFile;
559
+ let copiedSecretTo = null;
560
+ if (!secretFile) {
561
+ // The secret came from ATRIS_CLIENT_SECRET. Keep a private copy so later
562
+ // commands can renew without the variable.
563
+ secretFile = agentClient.managedSecretPath(request.clientId);
564
+ agentClient.writeFileAtomic(secretFile, `${secret}\n`);
565
+ copiedSecretTo = secretFile;
566
+ }
567
+
568
+ const keptProfile = keepHumanLoginAsProfile(request.clientId);
569
+ saveAgentClientCredentials({ client_id: request.clientId, client_secret_file: secretFile, scope: request.scope });
570
+ agentClient.writeTokenCache(request.clientId, entry);
571
+
572
+ const scopes = agentClient.scopeList(entry.scope);
573
+ if (envValue(env, 'ATRIS_TOKEN')) {
574
+ warnings.push('ATRIS_TOKEN is set in this shell and takes priority. unset it to use the agent client.');
575
+ }
576
+ if (json) {
577
+ output(JSON.stringify({
578
+ ok: true,
579
+ logged_in: true,
580
+ auth_type: agentClient.AUTH_TYPE,
581
+ client_id: request.clientId,
582
+ scopes,
583
+ token_expires_at: entry.expires_at,
584
+ ...(copiedSecretTo ? { secret_copied_to: copiedSecretTo } : {}),
585
+ warnings,
586
+ }, null, 2));
587
+ return 0;
588
+ }
589
+ output(`signed in as agent client ${request.clientId}`);
590
+ output(`scopes: ${scopes.length ? scopes.join(', ') : 'all granted to this client'}`);
591
+ output('the token renews on its own before it runs out.');
592
+ if (copiedSecretTo) output(`copied the secret from ATRIS_CLIENT_SECRET to ${copiedSecretTo} (only you can read it).`);
593
+ if (keptProfile) output(`the previous login is saved. switch back: atris switch ${keptProfile} --global`);
594
+ warnings.forEach((line) => outputError(line));
595
+ return 0;
596
+ }
597
+
433
598
  async function loginAtris(options = {}) {
434
599
  // Support: atris login --token <token> --force
435
600
  const args = process.argv.slice(3);
@@ -440,6 +605,16 @@ async function loginAtris(options = {}) {
440
605
  const nonInteractive = isNonInteractive(args);
441
606
 
442
607
  try {
608
+ const clientLogin = agentClientLoginRequest(args, process.env);
609
+ if (clientLogin) {
610
+ if (clientLogin.error) {
611
+ if (asJson) console.log(JSON.stringify({ ok: false, logged_in: false, error: clientLogin.error }, null, 2));
612
+ else console.error(clientLogin.error);
613
+ process.exit(1);
614
+ }
615
+ process.exit(await loginAgentClient(clientLogin, { json: asJson }));
616
+ }
617
+
443
618
  const existing = loadCredentials();
444
619
 
445
620
  if (wantsAgentToken(args)) {
@@ -487,12 +662,13 @@ async function loginAtris(options = {}) {
487
662
  email,
488
663
  user_id: userId,
489
664
  provider,
665
+ ...(existing.client_id ? { auth_type: agentClient.AUTH_TYPE, client_id: existing.client_id } : {}),
490
666
  credentials_saved: existing.saved_at || null,
491
667
  next: 'atris whoami --json',
492
668
  }, null, 2));
493
669
  process.exit(0);
494
670
  }
495
- console.log(`Currently signed in as: ${email || userId || 'unknown'}`);
671
+ console.log(`Currently signed in as: ${email || userId || (existing.client_id ? `agent client ${existing.client_id}` : 'unknown')}`);
496
672
  if (provider) console.log(`Provider: ${provider}`);
497
673
  console.log('Next: atris whoami');
498
674
  process.exit(0);
@@ -608,11 +784,17 @@ function logoutAtris() {
608
784
  process.exit(0);
609
785
  }
610
786
 
787
+ if (credentials.from_env) {
788
+ console.log(`This agent client (${credentials.client_id}) comes from ATRIS_CLIENT_ID in your environment.`);
789
+ console.log('Unset ATRIS_CLIENT_ID to sign it out.');
790
+ process.exit(0);
791
+ }
792
+
611
793
  const profiles = listProfiles();
612
794
  const currentName = profileNameFromEmail(credentials?.email);
613
795
 
614
796
  deleteCredentials();
615
- console.log(`✓ Signed out from ${credentials.email || 'current account'}`);
797
+ console.log(`✓ Signed out from ${credentials.email || (credentials.client_id ? `agent client ${credentials.client_id}` : 'current account')}`);
616
798
 
617
799
  // Remind about other profiles
618
800
  const remaining = profiles.filter(p => p !== currentName);
@@ -658,14 +840,14 @@ async function switchAccount() {
658
840
  }
659
841
 
660
842
  const current = loadCredentials();
661
- const currentName = profileNameFromEmail(current?.email);
843
+ const currentName = activeProfileName(current);
662
844
 
663
845
  if (!targetName) {
664
846
  // Interactive: show list and let user pick
665
847
  console.log('Switch account:\n');
666
848
  profiles.forEach((name, i) => {
667
849
  const profile = loadProfile(name);
668
- const email = profile?.email || 'unknown';
850
+ const email = profile ? profileLabel(profile, 'unknown') : 'unknown';
669
851
  const marker = name === currentName ? ' ← active' : '';
670
852
  console.log(` ${i + 1}. ${name}, ${email}${marker}`);
671
853
  });
@@ -709,6 +891,32 @@ async function switchAccount() {
709
891
  return activateProfile(match, currentName, { global: globalFlag });
710
892
  }
711
893
 
894
+ // A profile can be switched to if it has a login token or is a saved agent client.
895
+ function usableProfile(profile) {
896
+ return Boolean(profile && (profile.token || agentClient.isAgentClientCredential(profile)));
897
+ }
898
+
899
+ function profileLabel(profile, name) {
900
+ if (profile?.email) return profile.email;
901
+ if (agentClient.isAgentClientCredential(profile)) return `agent client ${profile.client_id}`;
902
+ return name;
903
+ }
904
+
905
+ function activeProfileName(credentials) {
906
+ if (agentClient.isAgentClientCredential(credentials)) return agentClient.profileNameForClient(credentials.client_id);
907
+ return profileNameFromEmail(credentials?.email);
908
+ }
909
+
910
+ // Overwrite credentials.json with a profile. An agent client login there is
911
+ // saved as a profile first, so its token cache and secret file stay reachable.
912
+ function writeGlobalCredentials(profile) {
913
+ const kept = preserveAgentClientLogin();
914
+ const credentialsPath = getCredentialsPath();
915
+ fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
916
+ try { fs.chmodSync(credentialsPath, 0o600); } catch {}
917
+ return kept;
918
+ }
919
+
712
920
  function activateProfile(name, currentName, { global = false } = {}) {
713
921
  if (name === currentName) {
714
922
  console.log(`Already on "${name}".`);
@@ -716,21 +924,20 @@ function activateProfile(name, currentName, { global = false } = {}) {
716
924
  }
717
925
 
718
926
  const profile = loadProfile(name);
719
- if (!profile || !profile.token) {
927
+ if (!usableProfile(profile)) {
720
928
  console.error(`Profile "${name}" is corrupted. Run "atris login" to fix.`);
721
929
  process.exit(1);
722
930
  }
723
931
 
724
932
  if (global || !getTerminalSessionId()) {
725
933
  // Global switch, write to credentials.json (affects all terminals)
726
- const credentialsPath = getCredentialsPath();
727
- fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
728
- try { fs.chmodSync(credentialsPath, 0o600); } catch {}
729
- console.log(`Switched to ${profile.email || name} (global, all terminals)`);
934
+ const kept = writeGlobalCredentials(profile);
935
+ console.log(`Switched to ${profileLabel(profile, name)} (global, all terminals)`);
936
+ if (kept && kept !== name) console.log(`The agent client login is saved. Switch back: atris switch ${kept} --global`);
730
937
  } else {
731
938
  // Per-terminal switch, write session file (only this terminal)
732
939
  setSessionProfile(name);
733
- console.log(`Switched to ${profile.email || name}`);
940
+ console.log(`Switched to ${profileLabel(profile, name)}`);
734
941
  }
735
942
  }
736
943
 
@@ -958,10 +1165,8 @@ function activateGlobal() {
958
1165
  const name = process.argv[3];
959
1166
  if (!name) { process.exit(1); }
960
1167
  const profile = loadProfile(name);
961
- if (!profile || !profile.token) { process.exit(1); }
962
- const credentialsPath = getCredentialsPath();
963
- fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
964
- try { fs.chmodSync(credentialsPath, 0o600); } catch {}
1168
+ if (!usableProfile(profile)) { process.exit(1); }
1169
+ writeGlobalCredentials(profile);
965
1170
  process.exit(0);
966
1171
  }
967
1172
 
@@ -974,7 +1179,7 @@ function switchSession() {
974
1179
  if (!name) { process.exit(1); }
975
1180
 
976
1181
  const profile = loadProfile(name);
977
- if (!profile || !profile.token) { process.exit(1); }
1182
+ if (!usableProfile(profile)) { process.exit(1); }
978
1183
 
979
1184
  // Accept explicit session ID from the shell wrapper (more reliable than
980
1185
  // detecting it from inside a child process where TTY env vars may be missing).
@@ -996,9 +1201,7 @@ function switchSession() {
996
1201
  setSessionProfile(name);
997
1202
  } else {
998
1203
  // No terminal ID, global fallback
999
- const credentialsPath = getCredentialsPath();
1000
- fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
1001
- try { fs.chmodSync(credentialsPath, 0o600); } catch {}
1204
+ writeGlobalCredentials(profile);
1002
1205
  }
1003
1206
  }
1004
1207
  process.exit(0);
@@ -1087,4 +1290,5 @@ module.exports = {
1087
1290
  persistMintedAgentToken,
1088
1291
  ensureBilledCommandAuth,
1089
1292
  wantsAgentToken,
1293
+ agentClientLoginRequest,
1090
1294
  };
@@ -11,8 +11,10 @@
11
11
  // Backend: backend/routers/agent_auth_router.py (POST /auth/agent/signup).
12
12
 
13
13
  const crypto = require('crypto');
14
+ const fs = require('fs');
14
15
  const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
15
- const { saveCredentials } = require('../utils/auth');
16
+ const { saveCredentials, saveProfile, saveAgentClientCredentials, getCredentialsPath } = require('../utils/auth');
17
+ const agentClient = require('../utils/agent-client');
16
18
  const { argsWantHelp, isHelpToken } = require('../lib/noninteractive');
17
19
 
18
20
  // Mirror the server rule exactly (^[a-z0-9]{3,30}$) so we fail fast and friendly
@@ -45,6 +47,36 @@ function parseHandle(args = []) {
45
47
  return (positional || '').trim().toLowerCase();
46
48
  }
47
49
 
50
+ // Newer servers also hand back an agent client (id + secret) so the agent can
51
+ // renew its own login forever. Store it the way `atris login` does with the
52
+ // secret in ATRIS_CLIENT_SECRET: a private 0600 copy under
53
+ // ~/.atris/agent-clients/, then make the client the active login. The 30-day
54
+ // token just saved stays reachable as a profile named after the handle.
55
+ // Returns { ok: true } or { ok: false, secretFile, clientId }. Never throws,
56
+ // never prints the secret or a token.
57
+ async function adoptSignupClient(client, handle, deps = {}) {
58
+ const clientId = client && typeof client.client_id === 'string' ? client.client_id.trim() : '';
59
+ const secret = client && typeof client.client_secret === 'string' ? client.client_secret.trim() : '';
60
+ if (!clientId.startsWith(agentClient.CLIENT_ID_PREFIX) || !secret) return { ok: false };
61
+
62
+ let secretFile = null;
63
+ try {
64
+ const target = agentClient.managedSecretPath(clientId);
65
+ agentClient.writeFileAtomic(target, `${secret}\n`);
66
+ secretFile = target;
67
+ const entry = await agentClient.requestClientToken({ clientId, secret, scope: null }, deps);
68
+
69
+ // Keep the 30-day login as a profile named after the handle, then switch.
70
+ const fallback = JSON.parse(fs.readFileSync(getCredentialsPath(), 'utf8'));
71
+ saveProfile(handle, fallback);
72
+ saveAgentClientCredentials({ client_id: clientId, client_secret_file: secretFile, scope: null });
73
+ agentClient.writeTokenCache(clientId, entry);
74
+ return { ok: true, clientId };
75
+ } catch (error) {
76
+ return { ok: false, clientId, secretFile, reason: agentClient.scrub(error && error.message, [secret]) };
77
+ }
78
+ }
79
+
48
80
  function printSignupUsage() {
49
81
  console.error('Usage: atris signup <handle>');
50
82
  console.error(' handle: 3–30 characters, lowercase letters and digits only.');
@@ -79,9 +111,20 @@ async function signupCommand(args = []) {
79
111
  const { token, email, user_id: userId } = res.data;
80
112
  const identity = email || `${handle}@atrismail.com`;
81
113
  saveCredentials(token, null, identity, userId || null, 'atrisos');
114
+ const hasClient = Boolean(res.data.client);
115
+ const adopted = hasClient ? await adoptSignupClient(res.data.client, handle) : { ok: false };
82
116
  console.log(`\n✓ You're in: ${identity}`);
117
+ if (adopted.ok) console.log('This agent renews its own login; no human needed.');
83
118
  console.log(' Inert starter account (0 credits): identity is free, capability is earned.');
84
119
  console.log(' Saved to your active profile.');
120
+ if (adopted.ok) {
121
+ console.log(` The 30-day login is kept as a backup. switch to it: atris switch ${handle} --global`);
122
+ } else if (hasClient && adopted.secretFile) {
123
+ console.log(` Could not sign in with the agent client yet${adopted.reason ? ` (${adopted.reason})` : ''}. Using the 30-day login for now.`);
124
+ console.log(` Try again: atris login --client-id ${adopted.clientId} --client-secret-file ${adopted.secretFile}`);
125
+ } else if (hasClient || typeof res.data.client_error === 'string') {
126
+ console.log(' This login lasts 30 days; after that, renewing it will need a fresh signup login.');
127
+ }
85
128
  console.log('\nNext:');
86
129
  console.log(' atris play # claim a starter mission and earn your first proof-backed rep');
87
130
  console.log(' atris xp # see where you stand on the board');
@@ -107,4 +150,4 @@ async function signupCommand(args = []) {
107
150
  return 1;
108
151
  }
109
152
 
110
- module.exports = { signupCommand, parseHandle, HANDLE_RE };
153
+ module.exports = { signupCommand, parseHandle, HANDLE_RE, adoptSignupClient };
@@ -4,7 +4,7 @@ const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'work
4
4
  'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
5
5
  'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
6
6
  'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
7
- 'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
7
+ 'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
8
8
  'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
9
9
 
10
10
  // Damerau-Levenshtein edit distance between two short strings. Counts an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.66.0",
3
+ "version": "3.68.0",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {