@alphafox/cli 0.3.7 → 0.3.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.
@@ -8,6 +8,8 @@ import type { ProfileConfig } from "../config/profiles";
8
8
  import { type StoredTokens } from "../keychain/store";
9
9
  /** Refresh when access token expires within this window. */
10
10
  export declare const ACCESS_TOKEN_REFRESH_SKEW_MS = 60000;
11
+ /** Drop a stale inter-process refresh lock after this long. */
12
+ export declare const REFRESH_LOCK_STALE_MS = 30000;
11
13
  export type RefreshOutcome = {
12
14
  readonly status: "refreshed";
13
15
  readonly tokens: StoredTokens;
@@ -40,5 +42,6 @@ export declare function refreshStoredTokensOrNull(profile: ProfileConfig, env?:
40
42
  readonly now?: number;
41
43
  readonly force?: boolean;
42
44
  }): Promise<StoredTokens | null>;
45
+ export declare function refreshLockFilePath(profile: string, env?: NodeJS.ProcessEnv): string;
43
46
  /** Test helper: clear in-flight map between cases. */
44
47
  export declare function clearRefreshInflightForTests(): void;
@@ -6,15 +6,21 @@
6
6
  * Outcomes are explicit: callers must not treat a failed refresh as a healthy session.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.ACCESS_TOKEN_REFRESH_SKEW_MS = void 0;
9
+ exports.REFRESH_LOCK_STALE_MS = exports.ACCESS_TOKEN_REFRESH_SKEW_MS = void 0;
10
10
  exports.accessTokenNeedsRefresh = accessTokenNeedsRefresh;
11
11
  exports.refreshStoredTokens = refreshStoredTokens;
12
12
  exports.refreshStoredTokensOrNull = refreshStoredTokensOrNull;
13
+ exports.refreshLockFilePath = refreshLockFilePath;
13
14
  exports.clearRefreshInflightForTests = clearRefreshInflightForTests;
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ const node_os_1 = require("node:os");
14
18
  const version_1 = require("../version");
15
19
  const store_1 = require("../keychain/store");
16
20
  /** Refresh when access token expires within this window. */
17
21
  exports.ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
22
+ /** Drop a stale inter-process refresh lock after this long. */
23
+ exports.REFRESH_LOCK_STALE_MS = 30_000;
18
24
  /** In-flight refresh promises so concurrent API calls share one rotation. */
19
25
  const inflightByProfile = new Map();
20
26
  function accessTokenNeedsRefresh(tokens, now = Date.now()) {
@@ -36,20 +42,38 @@ async function refreshStoredTokens(profile, env = process.env, fetchImpl = fetch
36
42
  tokens: null,
37
43
  };
38
44
  }
39
- if (!options.force &&
40
- !accessTokenNeedsRefresh(existing, options.now ?? Date.now())) {
45
+ const now = options.now ?? Date.now();
46
+ if (!options.force && !accessTokenNeedsRefresh(existing, now)) {
41
47
  return { status: "unchanged", tokens: existing };
42
48
  }
43
- const key = profile.name;
44
- const pending = inflightByProfile.get(key);
45
- if (pending) {
46
- return pending;
47
- }
48
- const work = performRefresh(profile, existing, env, fetchImpl).finally(() => {
49
- inflightByProfile.delete(key);
49
+ return withRefreshLock(profile.name, env, async () => {
50
+ const latest = (0, store_1.loadTokens)(profile.name, env) ?? existing;
51
+ if (!latest?.refreshToken?.trim()) {
52
+ return {
53
+ status: "no_session",
54
+ reason: "no_refresh_token",
55
+ tokens: null,
56
+ };
57
+ }
58
+ const someoneElseRefreshed = latest.refreshToken !== existing.refreshToken ||
59
+ latest.expiresAt > existing.expiresAt;
60
+ if (someoneElseRefreshed && !accessTokenNeedsRefresh(latest, now)) {
61
+ return { status: "unchanged", tokens: latest };
62
+ }
63
+ if (!options.force && !accessTokenNeedsRefresh(latest, now)) {
64
+ return { status: "unchanged", tokens: latest };
65
+ }
66
+ const key = profile.name;
67
+ const pending = inflightByProfile.get(key);
68
+ if (pending) {
69
+ return pending;
70
+ }
71
+ const work = performRefresh(profile, latest, env, fetchImpl).finally(() => {
72
+ inflightByProfile.delete(key);
73
+ });
74
+ inflightByProfile.set(key, work);
75
+ return work;
50
76
  });
51
- inflightByProfile.set(key, work);
52
- return work;
53
77
  }
54
78
  /**
55
79
  * Convenience for callers that only need tokens on successful refresh/unchanged.
@@ -62,6 +86,68 @@ async function refreshStoredTokensOrNull(profile, env = process.env, fetchImpl =
62
86
  }
63
87
  return null;
64
88
  }
89
+ function refreshLockFilePath(profile, env = process.env) {
90
+ const base = env.ALPHAFOX_KEYCHAIN_DIR?.trim() ||
91
+ (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "alphafox", "keychain");
92
+ return (0, node_path_1.join)(base, `${profile}.refresh.lock`);
93
+ }
94
+ async function withRefreshLock(profile, env, work) {
95
+ const path = refreshLockFilePath(profile, env);
96
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
97
+ const started = Date.now();
98
+ while (true) {
99
+ try {
100
+ const fd = (0, node_fs_1.openSync)(path, node_fs_1.constants.O_CREAT | node_fs_1.constants.O_EXCL | node_fs_1.constants.O_WRONLY);
101
+ try {
102
+ (0, node_fs_1.writeFileSync)(fd, `${process.pid}\n${Date.now()}\n`);
103
+ }
104
+ finally {
105
+ (0, node_fs_1.closeSync)(fd);
106
+ }
107
+ try {
108
+ return await work();
109
+ }
110
+ finally {
111
+ try {
112
+ (0, node_fs_1.unlinkSync)(path);
113
+ }
114
+ catch {
115
+ // another process stole a stale lock
116
+ }
117
+ }
118
+ }
119
+ catch (err) {
120
+ const code = err.code;
121
+ if (code !== "EEXIST") {
122
+ throw err;
123
+ }
124
+ try {
125
+ if (Date.now() - (0, node_fs_1.statSync)(path).mtimeMs > exports.REFRESH_LOCK_STALE_MS) {
126
+ (0, node_fs_1.unlinkSync)(path);
127
+ continue;
128
+ }
129
+ }
130
+ catch {
131
+ // lock disappeared; retry acquire
132
+ }
133
+ if (Date.now() - started > exports.REFRESH_LOCK_STALE_MS + 5_000) {
134
+ try {
135
+ (0, node_fs_1.unlinkSync)(path);
136
+ }
137
+ catch {
138
+ // raced
139
+ }
140
+ continue;
141
+ }
142
+ await sleep(50);
143
+ }
144
+ }
145
+ }
146
+ function sleep(ms) {
147
+ return new Promise((resolve) => {
148
+ setTimeout(resolve, ms);
149
+ });
150
+ }
65
151
  async function performRefresh(profile, existing, env, fetchImpl) {
66
152
  const origin = profile.apiBaseUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
67
153
  const url = `${origin}/api/auth/oauth/token`;
@@ -9,6 +9,7 @@ const allowlist_1 = require("../catalog/allowlist");
9
9
  const profiles_1 = require("../config/profiles");
10
10
  const envelope_1 = require("../envelope");
11
11
  const browser_login_1 = require("../auth/browser-login");
12
+ const refresh_1 = require("../auth/refresh");
12
13
  const client_1 = require("../http/client");
13
14
  const store_1 = require("../keychain/store");
14
15
  const confirmation_1 = require("../safety/confirmation");
@@ -351,15 +352,30 @@ async function cmdAuth(args, flags, env) {
351
352
  });
352
353
  if (sub === "status") {
353
354
  const verify = args.includes("--verify");
354
- const tokens = (0, store_1.loadTokens)(profile.name, env);
355
+ let tokens = (0, store_1.loadTokens)(profile.name, env);
355
356
  if (!tokens) {
356
357
  (0, envelope_1.writeSuccess)({
357
358
  authenticated: false,
359
+ session: "none",
358
360
  profile: profile.name,
359
361
  verified: false,
362
+ refresh: "no_session",
363
+ accessTokenExpired: null,
364
+ hasRefreshToken: false,
360
365
  }, { format: flags.format, jq: flags.jq });
361
366
  return 0;
362
367
  }
368
+ let refresh = "skipped";
369
+ if ((0, refresh_1.accessTokenNeedsRefresh)(tokens)) {
370
+ const outcome = await (0, refresh_1.refreshStoredTokens)(profile, env);
371
+ refresh = outcome.status;
372
+ if (outcome.status === "refreshed" || outcome.status === "unchanged") {
373
+ tokens = outcome.tokens;
374
+ }
375
+ else {
376
+ tokens = (0, store_1.loadTokens)(profile.name, env) ?? tokens;
377
+ }
378
+ }
363
379
  let verified = null;
364
380
  let whoami = null;
365
381
  if (verify) {
@@ -370,9 +386,18 @@ async function cmdAuth(args, flags, env) {
370
386
  }, env);
371
387
  verified = res.status >= 200 && res.status < 300;
372
388
  whoami = verified ? res.json : { status: res.status, body: res.json };
389
+ tokens = (0, store_1.loadTokens)(profile.name, env) ?? tokens;
373
390
  }
391
+ const accessTokenExpired = tokens.expiresAt <= Date.now();
392
+ const hasRefreshToken = Boolean(tokens.refreshToken?.trim());
393
+ const session = !accessTokenExpired
394
+ ? "active"
395
+ : refresh === "failed"
396
+ ? "refresh_failed"
397
+ : "expired";
374
398
  (0, envelope_1.writeSuccess)({
375
- authenticated: true,
399
+ authenticated: session === "active",
400
+ session,
376
401
  profile: profile.name,
377
402
  environment: tokens.environment,
378
403
  issuer: tokens.issuer,
@@ -381,6 +406,9 @@ async function cmdAuth(args, flags, env) {
381
406
  scopes: tokens.scopes,
382
407
  accessTokenFingerprint: (0, store_1.tokenFingerprint)(tokens.accessToken),
383
408
  expiresAt: tokens.expiresAt,
409
+ accessTokenExpired,
410
+ hasRefreshToken,
411
+ refresh,
384
412
  verified,
385
413
  whoami,
386
414
  }, { format: flags.format, jq: flags.jq });
@@ -1,141 +1,141 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@alphafox/cli",
4
- "packageVersion": "0.3.7",
4
+ "packageVersion": "0.3.8",
5
5
  "contractVersion": "2026-08-13",
6
- "bundleHash": "e12e1922538778707e60e931a247ecebb3e028cefeba295f4ca299d8d82d661b",
6
+ "bundleHash": "cd05f4eb6f9e1a7c9188ac5770196698c6f2c9ff6aaebfbfbaba3581ef08d226",
7
7
  "skills": [
8
8
  {
9
9
  "name": "alphafox",
10
- "version": "0.3.7",
10
+ "version": "0.3.8",
11
11
  "files": [
12
12
  {
13
13
  "path": "SKILL.md",
14
- "sha256": "2cd53898008075e68afd8ce955b63cf61f2971ef28f6c13c41853b1c5de967c8",
14
+ "sha256": "e8eadf96f36f6aa124b36d15a912465e839389e5228dbf9dfd9ad2f1ec86f65a",
15
15
  "size": 3553
16
16
  }
17
17
  ],
18
- "hash": "d687ecf8cc366e26dcb535bf31a30faceb037f021c818fa24997d144a22d3c20"
18
+ "hash": "fb41a55fcd363231cb69e30e4c0fe7b1ad2e17b12e2bb90cfda7d6e9ccd2f02c"
19
19
  },
20
20
  {
21
21
  "name": "alphafox-account",
22
- "version": "0.3.7",
22
+ "version": "0.3.8",
23
23
  "files": [
24
24
  {
25
25
  "path": "SKILL.md",
26
- "sha256": "fc5675dc44494227679994b761ca2e014d36acc207c68e3fb6d833912a7644ee",
26
+ "sha256": "9925573bf644b998ea683df7a20276a835d33cb83b0548ee0d1c43429ad99582",
27
27
  "size": 783
28
28
  }
29
29
  ],
30
- "hash": "bc1352e9d352f313f69f5257ba351137778c3ee1d8714d2d80fc39c0824c5cec"
30
+ "hash": "934bcd76d1bea875324673ffcc3d1c52e3f4065f776c4958ffc230cf911bac21"
31
31
  },
32
32
  {
33
33
  "name": "alphafox-admin",
34
- "version": "0.3.7",
34
+ "version": "0.3.8",
35
35
  "files": [
36
36
  {
37
37
  "path": "SKILL.md",
38
- "sha256": "6594e6cbfd319a41ae3abf9bf4f8239bb42bc25e6eec295999fe39cd694b2b8b",
38
+ "sha256": "e9ae0b433cead54e9c284f0ea8859dd9e96123f842788938319404b96bc085e7",
39
39
  "size": 787
40
40
  }
41
41
  ],
42
- "hash": "d6c5573444a020f376d51a73d44f39da9de8ea20bfd160f838b0de01bbb5e798"
42
+ "hash": "1346e798f18582c16cfbb887667272406f170e775d3027131d3d88e1f1a656e3"
43
43
  },
44
44
  {
45
45
  "name": "alphafox-auth",
46
- "version": "0.3.7",
46
+ "version": "0.3.8",
47
47
  "files": [
48
48
  {
49
49
  "path": "SKILL.md",
50
- "sha256": "b7b2694cbcf6e784d89943950435cb34454309cfbe07e383cc028a435dfec77d",
51
- "size": 1919
50
+ "sha256": "8ec9f081d9a1d40606ac7c18cba5dae9f1b6144ddc0079005b16af54be8f2f46",
51
+ "size": 2370
52
52
  }
53
53
  ],
54
- "hash": "aa7e2a39058660c4c803ed6e1bed94da0559aa943998336d6f0c83d8930a1d9d"
54
+ "hash": "854910a6792aa6d0a8b9cd796afb41753a34cef79df95c5b50de432f03a4e67a"
55
55
  },
56
56
  {
57
57
  "name": "alphafox-engine-backtest",
58
- "version": "0.3.7",
58
+ "version": "0.3.8",
59
59
  "files": [
60
60
  {
61
61
  "path": "SKILL.md",
62
- "sha256": "e09e938c9dd90d0be156b2d8bd83ba854c4d1a190a1b4aa575ca67db70184ebc",
62
+ "sha256": "e5f37b8541c510a27f84cbf9eea7524efe0b8f9c60c4faf17521b6cac298f92b",
63
63
  "size": 7584
64
64
  }
65
65
  ],
66
- "hash": "91de2131ffde0b9fef16221d5589a8c430ae48ddfc4f2dc781d510b9d4c90532"
66
+ "hash": "efb614f145763f514c33d02e112657bd42868cc746f13dbcb3cceaa0ecb770e4"
67
67
  },
68
68
  {
69
69
  "name": "alphafox-exchange",
70
- "version": "0.3.7",
70
+ "version": "0.3.8",
71
71
  "files": [
72
72
  {
73
73
  "path": "SKILL.md",
74
- "sha256": "53cba327c5507266b4dca7193fad67ba33c0c7bb745be41fd194425b9ef5928e",
74
+ "sha256": "118141754916741f4f8b449cf307a89cdfa11f234302726ba4d6b58e396327b5",
75
75
  "size": 743
76
76
  }
77
77
  ],
78
- "hash": "910f451fc7939926cfbeee27ee72f2aae86fcdd78f343e9f02c30df105ac6480"
78
+ "hash": "3933f12a2d8c55db465d4a11cf1f7d40f90fff81b3931f398e2fe3bcaf064d62"
79
79
  },
80
80
  {
81
81
  "name": "alphafox-market",
82
- "version": "0.3.7",
82
+ "version": "0.3.8",
83
83
  "files": [
84
84
  {
85
85
  "path": "SKILL.md",
86
- "sha256": "6589628478dc9c98c20479a2ffdded4014ce71b9c488dd386a606fef25a37d26",
86
+ "sha256": "0e77dcc697fe60f742e1075986685db5018d9d659a4d8f44f9cc343117c1c74b",
87
87
  "size": 3078
88
88
  }
89
89
  ],
90
- "hash": "84d243b7c53c519cba34c8c487ba23eae60bde8440c4dcfff06e20efd63c07fa"
90
+ "hash": "710c45c6124583d3fa9c9584f67bbc665bad35c9e6d1294c7416f493b8a6b184"
91
91
  },
92
92
  {
93
93
  "name": "alphafox-notification",
94
- "version": "0.3.7",
94
+ "version": "0.3.8",
95
95
  "files": [
96
96
  {
97
97
  "path": "SKILL.md",
98
- "sha256": "8b985ff7f33696eb11427147cd021d32654f525eea54f8098f368e8750a59381",
98
+ "sha256": "712aa6a4de8090bb7ee57373d35dd95c62bad7a24e443747afa4f11fb6e79b68",
99
99
  "size": 698
100
100
  }
101
101
  ],
102
- "hash": "1f316dc3fe251699b3f90102cbc16cb4fbd3a3c5c07b923297c075fe2e73a4b3"
102
+ "hash": "5f2feef0e17912a593bafae0961f983e3f781d12800e4808c136004897321ab2"
103
103
  },
104
104
  {
105
105
  "name": "alphafox-shared",
106
- "version": "0.3.7",
106
+ "version": "0.3.8",
107
107
  "files": [
108
108
  {
109
109
  "path": "SKILL.md",
110
- "sha256": "3aac1ab46a759a2c6748516936f886ec028e680bff7deb3ff1abc51af99ed18a",
111
- "size": 5163
110
+ "sha256": "c6bab01f9c8369f9c2a29eebf7b46462887f14d3bec7ad00e2a159d395346508",
111
+ "size": 5385
112
112
  }
113
113
  ],
114
- "hash": "72a7255ab23ff7c1adf41b1ff2e22834d1b58ee9e2ccea990c64f9277bf8b0a3"
114
+ "hash": "2b78fe080877af19b659964a8c965720e5ab731dd157ced10d2ee598b8c574a0"
115
115
  },
116
116
  {
117
117
  "name": "alphafox-strategy",
118
- "version": "0.3.7",
118
+ "version": "0.3.8",
119
119
  "files": [
120
120
  {
121
121
  "path": "SKILL.md",
122
- "sha256": "1ef2a00dd1c74922a3d0248041813609fc58701e7857bb069cbed511b0c46168",
122
+ "sha256": "03f1d1c6d048d7b0a23d725c6585d1d019445579be9779dc93095ebafb3fcf5a",
123
123
  "size": 1826
124
124
  }
125
125
  ],
126
- "hash": "44c2bf74ff053977f3a8631115ebf868af7cff33856e0c6a4fe55080049c1ae1"
126
+ "hash": "ebf375c6eeaf13c971a3090d05beda7037f253325ee2236c35b26ad540922827"
127
127
  },
128
128
  {
129
129
  "name": "alphafox-trading",
130
- "version": "0.3.7",
130
+ "version": "0.3.8",
131
131
  "files": [
132
132
  {
133
133
  "path": "SKILL.md",
134
- "sha256": "db065fcc272e66d091cfe7469317d7911946f96094f475f64d34d201c392424c",
134
+ "sha256": "4b597f23ff6c8ce384e8fd56fab73c6128d373741285bac6df0b061e7ee50242",
135
135
  "size": 3122
136
136
  }
137
137
  ],
138
- "hash": "af36dc51e47ca91cb4a2f5ca78e03e17f003ee47b5354cef42947f401fbe48c0"
138
+ "hash": "d7e818482ec3ba9aeab50af72e72f990288f0e2e6714e1227cba441db05fa75a"
139
139
  }
140
140
  ]
141
141
  }
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare const CLI_NAME = "alphafox";
2
2
  export declare const CLI_PACKAGE = "@alphafox/cli";
3
- export declare const CLI_VERSION = "0.3.7";
3
+ export declare const CLI_VERSION = "0.3.8";
4
4
  export { CATALOG_VERSION as CLI_CONTRACT_VERSION } from "./catalog/operations";
package/dist/version.js CHANGED
@@ -3,6 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CLI_CONTRACT_VERSION = exports.CLI_VERSION = exports.CLI_PACKAGE = exports.CLI_NAME = void 0;
4
4
  exports.CLI_NAME = "alphafox";
5
5
  exports.CLI_PACKAGE = "@alphafox/cli";
6
- exports.CLI_VERSION = "0.3.7";
6
+ exports.CLI_VERSION = "0.3.8";
7
7
  var operations_1 = require("./catalog/operations");
8
8
  Object.defineProperty(exports, "CLI_CONTRACT_VERSION", { enumerable: true, get: function () { return operations_1.CATALOG_VERSION; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphafox/cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "AlphaFox CLI — Agent/Human entry for the public Application API.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-account
3
3
  description: Account, wallet, and subscription read paths.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Account / wallet
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-admin
3
3
  description: Admin-only operations reusing Web role authorization.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Admin
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox
3
3
  description: AlphaFox CLI entry router. Use for any AlphaFox request — install, update, login, whoami, 回测, engine backtest, strategy definitions, create/list/start/stop a running strategy (trader), ticker/标的 resolve (美股 or crypto), market data, exchange connectors, wallet, subscriptions, notifications, or admin. If a CLI command prints `[alphafox] update available`, ask the user「检测到新的版本,是否需要我帮你升级?」and only then run `alphafox update --format json --no-input`. Start here, then open the routed domain skill. Do not guess alphafox-engine-backtest vs alphafox-strategy vs alphafox-trading from memory.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # AlphaFox
@@ -36,7 +36,7 @@ If several rows apply, load **all** of them (typical: `alphafox-shared` + `alpha
36
36
  The CLI may print this on **stderr** at most once every 24 hours:
37
37
 
38
38
  ```text
39
- [alphafox] update available: 0.3.6 -> 0.3.7. After the user confirms, run: alphafox update --format json --no-input,
39
+ [alphafox] update available: 0.3.7 -> 0.3.8. After the user confirms, run: alphafox update --format json --no-input,
40
40
  ```
41
41
 
42
42
  If you see that notice (or `updateAvailable: true` from `alphafox update --check`):
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-shared
3
3
  description: Shared AlphaFox CLI rules for Agents — auth, profiles, envelopes, risk gates, and public operationIds only.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # AlphaFox shared Agent contract
@@ -67,9 +67,10 @@ alphafox auth login --no-wait --format json --no-input
67
67
  # show verification_uri / user_code to the human, then:
68
68
  alphafox auth login --device-code <device_code> --format json --no-input
69
69
  alphafox auth status --verify --format json --no-input
70
- alphafox whoami --format json --no-input
71
70
  ```
72
71
 
72
+ Access tokens last ~10 minutes; the CLI refreshes them. After idle, run **one** `auth status --verify` — not `whoami` in parallel. `session: active` means logged in. A past `expiresAt` is not logout. Re-login only when `session` is `none` or `refresh_failed`.
73
+
73
74
  Local browser: `alphafox auth login --browser --format json --no-input` (loopback 127.0.0.1). If the browser cannot open, copy `authorizeUrl` from the error; do not invent a Device Flow retry unless the operator is headless.
74
75
 
75
76
  Wrong environment / missing permission / missing `--yes`: stop. Do not retry with a different profile.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-auth
3
3
  description: Login, status, logout, whoami, and environment isolation for AlphaFox CLI.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Auth Skill
@@ -19,23 +19,26 @@ Always `--format json --no-input`. Never `--token`.
19
19
  1. `alphafox auth login --no-wait --format json --no-input`
20
20
  2. Present `verification_uri` / `user_code` to the human.
21
21
  3. After approval: `alphafox auth login --device-code <device_code> --format json --no-input`
22
- 4. `alphafox whoami --format json --no-input` / `alphafox auth status --verify --format json --no-input`
22
+ 4. `alphafox auth status --verify --format json --no-input` (do not also run `whoami` in parallel)
23
23
 
24
24
  ### Browser loopback (human, local machine)
25
25
 
26
26
  1. `alphafox auth login --browser --format json --no-input`
27
27
  2. CLI binds `127.0.0.1` and opens the system browser. Do not copy codes or verifiers.
28
- 3. After the localhost callback, `alphafox whoami` / `alphafox auth status --verify`
28
+ 3. After the localhost callback, `alphafox auth status --verify --format json --no-input`
29
29
  4. If the browser cannot open, the error includes a copyable `authorizeUrl`. Do not retry as Device Flow unless the human is headless.
30
30
 
31
31
  ### Status / logout
32
32
 
33
- - `alphafox auth status --verify --format json --no-input`
33
+ - `alphafox auth status --verify --format json --no-input` is enough. Do **not** also run `whoami` in parallel — concurrent refresh can kill the session.
34
+ - Access tokens last ~10 minutes. The CLI refreshes them automatically. A past `expiresAt` is **not** logout.
35
+ - Logged in: `session` is `active` (or `authenticated: true` after status). Re-login only when `session` is `none` or `refresh_failed`.
34
36
  - `alphafox auth logout --format json --no-input` (server revoke + local keychain clear). If `remoteRevoke` is `failed`, local tokens are still cleared but exit is non-zero — do not claim a full logout.
35
37
 
36
38
  ### Recovery
37
39
 
38
- - `401` / `expired_token`: re-run Device Flow or browser login. Do not reuse a token from another profile.
40
+ - `session: refresh_failed` / refresh grant `invalid_grant`: re-run Device Flow or browser login. Do not reuse a token from another profile.
41
+ - Do not treat a short idle or an expired access token as a missing login.
39
42
  - Cross-env: production tokens are rejected on staging/local. Switch `--profile` only with explicit operator intent.
40
43
 
41
44
  ## Safety
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-engine-backtest
3
3
  description: Local Engine WASM backtest (alphafox engine-backtest run|sweep) vs catalog experiment CRUD.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Engine Backtest
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-exchange
3
3
  description: Exchange connectors list and connection management via Public API.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Exchange connectors
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-market
3
3
  description: Market data and ticker resolution for US equity perps, RWAs, and crypto on the same perp catalog. Use when the user names 美股, NVDA, AAPL, BTC, or any 标的. Keep the operator's asset class via symbolMetadata — do not rewrite NVDA into a crypto coin.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Market
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-notification
3
3
  description: Notification channels and subscriptions.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Notification
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-strategy
3
3
  description: Strategy definitions — list types (grid, dca, copy, …) and validate config. Creating a running strategy is creating a trader; use alphafox-trading for that.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Strategy definitions
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: alphafox-trading
3
3
  description: Running strategies (traders) — create, list, start, and stop. A trader is a live or paper strategy instance (grid, dca, copy, …), not a person.
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  ---
6
6
 
7
7
  # Running strategies (traders)