@foxden-app/foxclaw 0.3.14 → 0.3.17
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/dist/codex_app/local_usage.d.ts +6 -0
- package/dist/codex_app/local_usage.js +49 -0
- package/dist/controller/controller.d.ts +11 -0
- package/dist/controller/controller.js +158 -15
- package/dist/i18n.d.ts +10 -4
- package/dist/i18n.js +10 -4
- package/dist/main.js +9 -1
- package/dist/update.d.ts +2 -1
- package/dist/update.js +61 -13
- package/docs/user-manual.md +8 -7
- package/docs/zh/user-manual.md +8 -7
- package/package.json +1 -1
|
@@ -14,6 +14,10 @@ export interface CodexLocalUsageStats {
|
|
|
14
14
|
outputSpeed: CodexLocalOutputSpeedStats;
|
|
15
15
|
latestSessionMtimeMs: number | null;
|
|
16
16
|
}
|
|
17
|
+
export interface CodexLocalUsageSnapshot {
|
|
18
|
+
computedAtMs: number;
|
|
19
|
+
stats: CodexLocalUsageStats;
|
|
20
|
+
}
|
|
17
21
|
export interface CodexLocalOutputSpeedStats {
|
|
18
22
|
samples: number;
|
|
19
23
|
outputTokens: number;
|
|
@@ -22,3 +26,5 @@ export interface CodexLocalOutputSpeedStats {
|
|
|
22
26
|
latestSampleAtMs: number | null;
|
|
23
27
|
}
|
|
24
28
|
export declare function readCodexLocalUsageStats(codexHome?: string): Promise<CodexLocalUsageStats>;
|
|
29
|
+
export declare function readCodexLocalUsageSnapshot(snapshotPath: string): Promise<CodexLocalUsageSnapshot | null>;
|
|
30
|
+
export declare function writeCodexLocalUsageSnapshot(snapshotPath: string, snapshot: CodexLocalUsageSnapshot): Promise<void>;
|
|
@@ -37,6 +37,24 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
|
37
37
|
latestSessionMtimeMs,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
export async function readCodexLocalUsageSnapshot(snapshotPath) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(await fs.readFile(snapshotPath, 'utf8'));
|
|
43
|
+
if (!isFiniteNumber(parsed.computedAtMs) || !isCodexLocalUsageStats(parsed.stats)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return { computedAtMs: parsed.computedAtMs, stats: parsed.stats };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function writeCodexLocalUsageSnapshot(snapshotPath, snapshot) {
|
|
53
|
+
await fs.mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
54
|
+
const temporaryPath = `${snapshotPath}.${process.pid}.tmp`;
|
|
55
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(snapshot, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
56
|
+
await fs.rename(temporaryPath, snapshotPath);
|
|
57
|
+
}
|
|
40
58
|
function resolveCodexHome() {
|
|
41
59
|
return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
42
60
|
}
|
|
@@ -128,6 +146,37 @@ function emptyOutputSpeedStats() {
|
|
|
128
146
|
latestSampleAtMs: null,
|
|
129
147
|
};
|
|
130
148
|
}
|
|
149
|
+
function isCodexLocalUsageStats(value) {
|
|
150
|
+
if (!value || typeof value !== 'object') {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const stats = value;
|
|
154
|
+
const totals = stats.totals;
|
|
155
|
+
const speed = stats.outputSpeed;
|
|
156
|
+
return isFiniteNumber(stats.sessionFiles)
|
|
157
|
+
&& isFiniteNumber(stats.sessionsWithUsage)
|
|
158
|
+
&& isFiniteNumber(stats.turns)
|
|
159
|
+
&& isFiniteNumber(stats.usageEvents)
|
|
160
|
+
&& Boolean(totals)
|
|
161
|
+
&& isFiniteNumber(totals?.inputTokens)
|
|
162
|
+
&& isFiniteNumber(totals?.cachedInputTokens)
|
|
163
|
+
&& isFiniteNumber(totals?.outputTokens)
|
|
164
|
+
&& isFiniteNumber(totals?.reasoningOutputTokens)
|
|
165
|
+
&& isFiniteNumber(totals?.totalTokens)
|
|
166
|
+
&& Boolean(speed)
|
|
167
|
+
&& isFiniteNumber(speed?.samples)
|
|
168
|
+
&& isFiniteNumber(speed?.outputTokens)
|
|
169
|
+
&& isFiniteNumber(speed?.seconds)
|
|
170
|
+
&& isNullableFiniteNumber(speed?.latestTokensPerSecond)
|
|
171
|
+
&& isNullableFiniteNumber(speed?.latestSampleAtMs)
|
|
172
|
+
&& isNullableFiniteNumber(stats.latestSessionMtimeMs);
|
|
173
|
+
}
|
|
174
|
+
function isFiniteNumber(value) {
|
|
175
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
176
|
+
}
|
|
177
|
+
function isNullableFiniteNumber(value) {
|
|
178
|
+
return value === null || isFiniteNumber(value);
|
|
179
|
+
}
|
|
131
180
|
function addUsage(totals, usage) {
|
|
132
181
|
const inputTokens = numberField(usage, 'input_tokens', 'inputTokens');
|
|
133
182
|
const outputTokens = numberField(usage, 'output_tokens', 'outputTokens');
|
|
@@ -32,6 +32,10 @@ export declare class BridgeSessionCore {
|
|
|
32
32
|
private authRotationInProgress;
|
|
33
33
|
private authRotationFailedTargets;
|
|
34
34
|
private localUsageCache;
|
|
35
|
+
private localUsageCacheLoaded;
|
|
36
|
+
private localUsageRefresh;
|
|
37
|
+
private authQuotaSnapshots;
|
|
38
|
+
private authQuotaSnapshotsLoaded;
|
|
35
39
|
private lastRemoteControlStatus;
|
|
36
40
|
private pendingThreadRenames;
|
|
37
41
|
private pendingThreadNewCwds;
|
|
@@ -266,6 +270,13 @@ export declare class BridgeSessionCore {
|
|
|
266
270
|
private formatCodexLocalOutputSpeedStatusLines;
|
|
267
271
|
private resolveFastStatusLabel;
|
|
268
272
|
private readCachedCodexLocalUsageStats;
|
|
273
|
+
private refreshCodexLocalUsageIfNeeded;
|
|
274
|
+
private refreshCodexLocalUsageStats;
|
|
275
|
+
private codexLocalUsageSnapshotPath;
|
|
276
|
+
private refreshCurrentCodexAuthQuota;
|
|
277
|
+
private readCodexAuthQuotaSnapshots;
|
|
278
|
+
private writeCodexAuthQuotaSnapshots;
|
|
279
|
+
private codexAuthQuotaSnapshotPath;
|
|
269
280
|
private sendThreadContextSummary;
|
|
270
281
|
private handleModelCommand;
|
|
271
282
|
private handleEffortCommand;
|
|
@@ -11,7 +11,7 @@ import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessag
|
|
|
11
11
|
import { isDefaultTelegramScope, resolveTelegramAddressing } from '../telegram/addressing.js';
|
|
12
12
|
import { BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope, parseWeixinBridgeScope } from '../core/bridge_scope.js';
|
|
13
13
|
import { resolveTelegramRenderRoute } from '../telegram/rendering.js';
|
|
14
|
-
import { readCodexLocalUsageStats } from '../codex_app/local_usage.js';
|
|
14
|
+
import { readCodexLocalUsageSnapshot, readCodexLocalUsageStats, writeCodexLocalUsageSnapshot, } from '../codex_app/local_usage.js';
|
|
15
15
|
import { normalizeTurnActivityEvent, } from './activity.js';
|
|
16
16
|
import { normalizeAccessPreset, resolveAccessMode } from './access.js';
|
|
17
17
|
import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
|
|
@@ -23,7 +23,9 @@ class UserFacingError extends Error {
|
|
|
23
23
|
const OBSERVED_THREAD_POLL_MS = 1500;
|
|
24
24
|
const OBSERVED_CLI_USER_LABEL = 'codex-cli-user';
|
|
25
25
|
const DEFAULT_COLLABORATION_MODE = 'default';
|
|
26
|
-
const
|
|
26
|
+
const CODEX_LOCAL_USAGE_REFRESH_MS = 30 * 60_000;
|
|
27
|
+
const CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME = 'codex-local-usage.json';
|
|
28
|
+
const CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME = 'codex-auth-quota.json';
|
|
27
29
|
const USER_INPUT_SUBMITTED_NOTICE_MS = 90_000;
|
|
28
30
|
const SELF_UPDATE_STATUS_POLL_MS = 1000;
|
|
29
31
|
const PLAN_IMPLEMENTATION_CODING_MESSAGE = 'Implement the plan.';
|
|
@@ -110,6 +112,10 @@ export class BridgeSessionCore {
|
|
|
110
112
|
authRotationInProgress = false;
|
|
111
113
|
authRotationFailedTargets = new Set();
|
|
112
114
|
localUsageCache = null;
|
|
115
|
+
localUsageCacheLoaded = false;
|
|
116
|
+
localUsageRefresh = null;
|
|
117
|
+
authQuotaSnapshots = {};
|
|
118
|
+
authQuotaSnapshotsLoaded = false;
|
|
113
119
|
lastRemoteControlStatus = null;
|
|
114
120
|
pendingThreadRenames = new Map();
|
|
115
121
|
pendingThreadNewCwds = new Map();
|
|
@@ -186,6 +192,9 @@ export class BridgeSessionCore {
|
|
|
186
192
|
await this.app.start();
|
|
187
193
|
await this.restorePendingUserInputs();
|
|
188
194
|
await this.cleanupStaleTurnPreviews();
|
|
195
|
+
void this.refreshCodexLocalUsageIfNeeded().catch((error) => {
|
|
196
|
+
this.logger.warn('codex.local_usage_background_refresh_failed', { error: formatUserError(error) });
|
|
197
|
+
});
|
|
189
198
|
this.updateStatus();
|
|
190
199
|
}
|
|
191
200
|
/** Begin Telegram Bot API long-polling after handlers and Codex are ready. */
|
|
@@ -348,7 +357,11 @@ export class BridgeSessionCore {
|
|
|
348
357
|
const binding = this.store.getBinding(scopeId);
|
|
349
358
|
const settings = this.store.getChatSettings(scopeId);
|
|
350
359
|
const access = this.resolveEffectiveAccess(scopeId, settings);
|
|
351
|
-
const fastStatus = await
|
|
360
|
+
const [fastStatus, codexUsageLines, codexLocalUsageLines] = await Promise.all([
|
|
361
|
+
this.resolveFastStatusLabel(locale, settings),
|
|
362
|
+
this.buildCodexUsageStatusLines(locale),
|
|
363
|
+
this.buildCodexLocalUsageStatusLines(locale),
|
|
364
|
+
]);
|
|
352
365
|
const appServer = this.app.getServerStatus();
|
|
353
366
|
const appServerLabel = appServer.pid && appServer.port
|
|
354
367
|
? `${appServer.running ? t(locale, 'status_app_server_running') : t(locale, 'status_app_server_stale')} pid=${appServer.pid} port=${appServer.port}`
|
|
@@ -376,8 +389,8 @@ export class BridgeSessionCore {
|
|
|
376
389
|
t(locale, 'status_pending_user_inputs', { value: this.store.countPendingUserInputs() }),
|
|
377
390
|
t(locale, 'status_active_turns', { value: this.activeTurns.size }),
|
|
378
391
|
];
|
|
379
|
-
lines.push(...
|
|
380
|
-
lines.push(...
|
|
392
|
+
lines.push(...codexUsageLines);
|
|
393
|
+
lines.push(...codexLocalUsageLines);
|
|
381
394
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
382
395
|
return;
|
|
383
396
|
}
|
|
@@ -3743,6 +3756,7 @@ export class BridgeSessionCore {
|
|
|
3743
3756
|
return;
|
|
3744
3757
|
}
|
|
3745
3758
|
const state = await this.listCodexAuthState();
|
|
3759
|
+
await this.refreshCurrentCodexAuthQuota(state);
|
|
3746
3760
|
const record = {
|
|
3747
3761
|
localId: crypto.randomBytes(8).toString('hex'),
|
|
3748
3762
|
chatId: scopeId,
|
|
@@ -4384,7 +4398,12 @@ export class BridgeSessionCore {
|
|
|
4384
4398
|
return candidate ? { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) } : null;
|
|
4385
4399
|
}
|
|
4386
4400
|
async listCodexAuthState() {
|
|
4387
|
-
|
|
4401
|
+
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames());
|
|
4402
|
+
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4403
|
+
state.candidates.forEach((candidate) => {
|
|
4404
|
+
candidate.quota = snapshots[candidate.name] ?? null;
|
|
4405
|
+
});
|
|
4406
|
+
return state;
|
|
4388
4407
|
}
|
|
4389
4408
|
async readCodexAuthSwitchLabels(candidate) {
|
|
4390
4409
|
const state = await this.listCodexAuthState();
|
|
@@ -4486,7 +4505,7 @@ export class BridgeSessionCore {
|
|
|
4486
4505
|
}
|
|
4487
4506
|
lines.push(t(locale, 'status_codex_usage_window', {
|
|
4488
4507
|
window: formatRateLimitWindowLabel(locale, window, kind),
|
|
4489
|
-
percent:
|
|
4508
|
+
percent: formatRemainingUsagePercent(window.usedPercent),
|
|
4490
4509
|
reset: window.resetsAt
|
|
4491
4510
|
? t(locale, 'status_codex_usage_reset', { value: formatLocalTimestamp(window.resetsAt) })
|
|
4492
4511
|
: '',
|
|
@@ -4505,7 +4524,14 @@ export class BridgeSessionCore {
|
|
|
4505
4524
|
}
|
|
4506
4525
|
async buildCodexLocalUsageStatusLines(locale) {
|
|
4507
4526
|
try {
|
|
4508
|
-
const
|
|
4527
|
+
const snapshot = await this.readCachedCodexLocalUsageStats();
|
|
4528
|
+
void this.refreshCodexLocalUsageIfNeeded(snapshot).catch((error) => {
|
|
4529
|
+
this.logger.warn('codex.local_usage_background_refresh_failed', { error: formatUserError(error) });
|
|
4530
|
+
});
|
|
4531
|
+
if (!snapshot) {
|
|
4532
|
+
return [t(locale, 'status_codex_local_usage_refreshing')];
|
|
4533
|
+
}
|
|
4534
|
+
const stats = snapshot.stats;
|
|
4509
4535
|
if (stats.sessionFiles === 0 || stats.sessionsWithUsage === 0) {
|
|
4510
4536
|
return [];
|
|
4511
4537
|
}
|
|
@@ -4523,6 +4549,9 @@ export class BridgeSessionCore {
|
|
|
4523
4549
|
reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
|
|
4524
4550
|
}),
|
|
4525
4551
|
...this.formatCodexLocalOutputSpeedStatusLines(locale, stats),
|
|
4552
|
+
t(locale, 'status_codex_local_snapshot_at', {
|
|
4553
|
+
value: formatLocalTimestamp(snapshot.computedAtMs / 1000),
|
|
4554
|
+
}),
|
|
4526
4555
|
];
|
|
4527
4556
|
}
|
|
4528
4557
|
catch (error) {
|
|
@@ -4554,13 +4583,87 @@ export class BridgeSessionCore {
|
|
|
4554
4583
|
}
|
|
4555
4584
|
}
|
|
4556
4585
|
async readCachedCodexLocalUsageStats() {
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
return this.localUsageCache.stats;
|
|
4586
|
+
if (this.localUsageCacheLoaded) {
|
|
4587
|
+
return this.localUsageCache;
|
|
4560
4588
|
}
|
|
4589
|
+
this.localUsageCache = await readCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath());
|
|
4590
|
+
this.localUsageCacheLoaded = true;
|
|
4591
|
+
return this.localUsageCache;
|
|
4592
|
+
}
|
|
4593
|
+
async refreshCodexLocalUsageIfNeeded(snapshot) {
|
|
4594
|
+
const current = snapshot === undefined ? await this.readCachedCodexLocalUsageStats() : snapshot;
|
|
4595
|
+
if (current && Date.now() - current.computedAtMs < CODEX_LOCAL_USAGE_REFRESH_MS) {
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
if (this.localUsageRefresh) {
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
this.localUsageRefresh = this.refreshCodexLocalUsageStats().finally(() => {
|
|
4602
|
+
this.localUsageRefresh = null;
|
|
4603
|
+
});
|
|
4604
|
+
await this.localUsageRefresh;
|
|
4605
|
+
}
|
|
4606
|
+
async refreshCodexLocalUsageStats() {
|
|
4561
4607
|
const stats = await readCodexLocalUsageStats();
|
|
4562
|
-
|
|
4563
|
-
|
|
4608
|
+
const snapshot = { computedAtMs: Date.now(), stats };
|
|
4609
|
+
this.localUsageCache = snapshot;
|
|
4610
|
+
this.localUsageCacheLoaded = true;
|
|
4611
|
+
await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
|
|
4612
|
+
}
|
|
4613
|
+
codexLocalUsageSnapshotPath() {
|
|
4614
|
+
return path.join(path.dirname(this.config.statusPath), CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME);
|
|
4615
|
+
}
|
|
4616
|
+
async refreshCurrentCodexAuthQuota(state) {
|
|
4617
|
+
const candidate = state.candidates.find(entry => entry.isCurrent);
|
|
4618
|
+
if (!candidate) {
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
try {
|
|
4622
|
+
const snapshot = selectCodexRateLimitSnapshot(await this.app.readAccountRateLimits());
|
|
4623
|
+
if (!snapshot) {
|
|
4624
|
+
return;
|
|
4625
|
+
}
|
|
4626
|
+
const quota = authQuotaSnapshotFromRateLimit(snapshot);
|
|
4627
|
+
candidate.quota = quota;
|
|
4628
|
+
this.authQuotaSnapshots[candidate.name] = quota;
|
|
4629
|
+
await this.writeCodexAuthQuotaSnapshots();
|
|
4630
|
+
}
|
|
4631
|
+
catch (error) {
|
|
4632
|
+
this.logger.warn('codex.auth_quota_refresh_failed', { error: formatUserError(error) });
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
async readCodexAuthQuotaSnapshots() {
|
|
4636
|
+
if (this.authQuotaSnapshotsLoaded) {
|
|
4637
|
+
return this.authQuotaSnapshots;
|
|
4638
|
+
}
|
|
4639
|
+
this.authQuotaSnapshotsLoaded = true;
|
|
4640
|
+
try {
|
|
4641
|
+
const parsed = JSON.parse(await fs.readFile(this.codexAuthQuotaSnapshotPath(), 'utf8'));
|
|
4642
|
+
if (parsed && typeof parsed === 'object') {
|
|
4643
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
4644
|
+
if (isCodexAuthQuotaSnapshot(value)) {
|
|
4645
|
+
this.authQuotaSnapshots[name] = value;
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
catch {
|
|
4651
|
+
// A missing or invalid historical cache should not block the auth panel.
|
|
4652
|
+
}
|
|
4653
|
+
return this.authQuotaSnapshots;
|
|
4654
|
+
}
|
|
4655
|
+
async writeCodexAuthQuotaSnapshots() {
|
|
4656
|
+
const snapshotPath = this.codexAuthQuotaSnapshotPath();
|
|
4657
|
+
await fs.mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
4658
|
+
const temporaryPath = `${snapshotPath}.${process.pid}.tmp`;
|
|
4659
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(this.authQuotaSnapshots, null, 2)}\n`, {
|
|
4660
|
+
encoding: 'utf8',
|
|
4661
|
+
mode: 0o600,
|
|
4662
|
+
});
|
|
4663
|
+
await fs.rename(temporaryPath, snapshotPath);
|
|
4664
|
+
}
|
|
4665
|
+
codexAuthQuotaSnapshotPath() {
|
|
4666
|
+
return path.join(path.dirname(this.config.statusPath), CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME);
|
|
4564
4667
|
}
|
|
4565
4668
|
async sendThreadContextSummary(scopeId, locale, threadId) {
|
|
4566
4669
|
try {
|
|
@@ -7039,6 +7142,7 @@ async function listCodexAuthState(disabledNames = new Set()) {
|
|
|
7039
7142
|
isCurrent: currentTargetPath === candidatePath,
|
|
7040
7143
|
disabled: disabledNames.has(entry.name),
|
|
7041
7144
|
mtimeMs: stat.mtimeMs,
|
|
7145
|
+
quota: null,
|
|
7042
7146
|
});
|
|
7043
7147
|
}
|
|
7044
7148
|
candidates.sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true }));
|
|
@@ -7138,12 +7242,13 @@ function renderAuthListMessage(locale, state, includeWeixinCopyPaste = false) {
|
|
|
7138
7242
|
return lines.join('\n');
|
|
7139
7243
|
}
|
|
7140
7244
|
lines.push(t(locale, 'auth_candidate_count', { value: state.candidates.length }));
|
|
7245
|
+
lines.push(t(locale, 'auth_quota_legend'));
|
|
7141
7246
|
state.candidates.forEach((candidate, index) => {
|
|
7142
7247
|
const marker = candidate.isCurrent ? ' *' : '';
|
|
7143
7248
|
const status = candidate.disabled
|
|
7144
7249
|
? t(locale, 'auth_candidate_status_disabled')
|
|
7145
7250
|
: t(locale, 'auth_candidate_status_enabled');
|
|
7146
|
-
lines.push(`${index + 1}. ${candidate.name}${marker} [${status}]`);
|
|
7251
|
+
lines.push(`${index + 1}. ${formatAuthQuotaPrefix(candidate.quota)}|${candidate.name}${marker} [${status}]`);
|
|
7147
7252
|
});
|
|
7148
7253
|
if (includeWeixinCopyPaste) {
|
|
7149
7254
|
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...state.candidates.map((_candidate, index) => `/auth use ${index + 1}`), ...state.candidates.map((candidate, index) => candidate.disabled
|
|
@@ -7155,7 +7260,7 @@ function renderAuthListMessage(locale, state, includeWeixinCopyPaste = false) {
|
|
|
7155
7260
|
function authChoiceKeyboard(locale, record) {
|
|
7156
7261
|
const rows = record.candidates.map((candidate, index) => [
|
|
7157
7262
|
{
|
|
7158
|
-
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${candidate.name}${candidate.disabled ? ' · off' : ''}`),
|
|
7263
|
+
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaPrefix(candidate.quota)}|${candidate.name}${candidate.disabled ? ' · off' : ''}`),
|
|
7159
7264
|
callback_data: `auth:${record.localId}:${index}`,
|
|
7160
7265
|
},
|
|
7161
7266
|
{
|
|
@@ -7692,6 +7797,44 @@ function formatUsagePercent(value) {
|
|
|
7692
7797
|
}
|
|
7693
7798
|
return formatCompactNumber(value);
|
|
7694
7799
|
}
|
|
7800
|
+
function formatRemainingUsagePercent(usedPercent) {
|
|
7801
|
+
const remainingPercent = remainingUsagePercent(usedPercent);
|
|
7802
|
+
return remainingPercent === null ? '?' : formatUsagePercent(remainingPercent);
|
|
7803
|
+
}
|
|
7804
|
+
function authQuotaSnapshotFromRateLimit(snapshot) {
|
|
7805
|
+
return {
|
|
7806
|
+
capturedAtMs: Date.now(),
|
|
7807
|
+
primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
|
|
7808
|
+
secondaryRemainingPercent: snapshot.secondary ? remainingUsagePercent(snapshot.secondary.usedPercent) : null,
|
|
7809
|
+
};
|
|
7810
|
+
}
|
|
7811
|
+
function remainingUsagePercent(usedPercent) {
|
|
7812
|
+
if (!Number.isFinite(usedPercent)) {
|
|
7813
|
+
return null;
|
|
7814
|
+
}
|
|
7815
|
+
return Math.max(0, Math.min(100, 100 - usedPercent));
|
|
7816
|
+
}
|
|
7817
|
+
function formatAuthQuotaPrefix(snapshot) {
|
|
7818
|
+
if (!snapshot) {
|
|
7819
|
+
return '--|--';
|
|
7820
|
+
}
|
|
7821
|
+
const primary = snapshot.primaryRemainingPercent === null ? '--' : formatUsagePercent(snapshot.primaryRemainingPercent);
|
|
7822
|
+
const secondary = snapshot.secondaryRemainingPercent === null ? '--' : formatUsagePercent(snapshot.secondaryRemainingPercent);
|
|
7823
|
+
return `${primary}|${secondary}`;
|
|
7824
|
+
}
|
|
7825
|
+
function isCodexAuthQuotaSnapshot(value) {
|
|
7826
|
+
if (!value || typeof value !== 'object') {
|
|
7827
|
+
return false;
|
|
7828
|
+
}
|
|
7829
|
+
const snapshot = value;
|
|
7830
|
+
return typeof snapshot.capturedAtMs === 'number'
|
|
7831
|
+
&& Number.isFinite(snapshot.capturedAtMs)
|
|
7832
|
+
&& isNullableFiniteNumber(snapshot.primaryRemainingPercent)
|
|
7833
|
+
&& isNullableFiniteNumber(snapshot.secondaryRemainingPercent);
|
|
7834
|
+
}
|
|
7835
|
+
function isNullableFiniteNumber(value) {
|
|
7836
|
+
return value === null || (typeof value === 'number' && Number.isFinite(value));
|
|
7837
|
+
}
|
|
7695
7838
|
function formatCompactNumber(value) {
|
|
7696
7839
|
return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, '');
|
|
7697
7840
|
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ declare const MESSAGES: {
|
|
|
90
90
|
readonly status_app_server: "Codex app-server: {value}";
|
|
91
91
|
readonly status_app_server_running: "running";
|
|
92
92
|
readonly status_app_server_stale: "stale";
|
|
93
|
-
readonly status_user_agent: "
|
|
93
|
+
readonly status_user_agent: "Codex/FoxClaw agent: {value}";
|
|
94
94
|
readonly status_current_thread: "Current thread: {value}";
|
|
95
95
|
readonly status_configured_model: "Configured model: {value}";
|
|
96
96
|
readonly status_configured_effort: "Configured effort: {value}";
|
|
@@ -107,12 +107,14 @@ declare const MESSAGES: {
|
|
|
107
107
|
readonly status_codex_account: "Codex account: {value}";
|
|
108
108
|
readonly status_codex_plan: "Codex plan: {value}";
|
|
109
109
|
readonly status_codex_usage_title: "Codex usage ({value}):";
|
|
110
|
-
readonly status_codex_usage_window: "{window}: {percent}%
|
|
110
|
+
readonly status_codex_usage_window: "{window}: {percent}% remaining{reset}";
|
|
111
111
|
readonly status_codex_usage_reset: ", resets {value}";
|
|
112
112
|
readonly status_codex_usage_unavailable: "Codex usage: unavailable ({error})";
|
|
113
113
|
readonly status_codex_local_history: "Codex local history: {sessions} sessions, {turns} turns, {events} usage records";
|
|
114
114
|
readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}";
|
|
115
115
|
readonly status_codex_local_speed: "Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)";
|
|
116
|
+
readonly status_codex_local_snapshot_at: "Codex local stats snapshot: {value}";
|
|
117
|
+
readonly status_codex_local_usage_refreshing: "Codex local history: building snapshot in background";
|
|
116
118
|
readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
|
|
117
119
|
readonly status_codex_credits: "Codex credits: {value}";
|
|
118
120
|
readonly status_codex_limit_reached: "Codex limit: {value}";
|
|
@@ -131,6 +133,7 @@ declare const MESSAGES: {
|
|
|
131
133
|
readonly auth_current: "Current auth: {value}";
|
|
132
134
|
readonly auth_dir: "Auth dir: {value}";
|
|
133
135
|
readonly auth_candidate_count: "Candidates: {value}";
|
|
136
|
+
readonly auth_quota_legend: "Quota remaining: 5h|7d|auth";
|
|
134
137
|
readonly auth_candidate_status_enabled: "enabled";
|
|
135
138
|
readonly auth_candidate_status_disabled: "disabled";
|
|
136
139
|
readonly auth_candidate_enabled: "Enabled auth candidate for auto-rotation: {value}";
|
|
@@ -651,7 +654,7 @@ declare const MESSAGES: {
|
|
|
651
654
|
readonly status_app_server: "Codex app-server:{value}";
|
|
652
655
|
readonly status_app_server_running: "运行中";
|
|
653
656
|
readonly status_app_server_stale: "失效";
|
|
654
|
-
readonly status_user_agent: "
|
|
657
|
+
readonly status_user_agent: "Codex/FoxClaw 标识:{value}";
|
|
655
658
|
readonly status_current_thread: "当前线程:{value}";
|
|
656
659
|
readonly status_configured_model: "已配置模型:{value}";
|
|
657
660
|
readonly status_configured_effort: "已配置推理强度:{value}";
|
|
@@ -668,12 +671,14 @@ declare const MESSAGES: {
|
|
|
668
671
|
readonly status_codex_account: "Codex 账号:{value}";
|
|
669
672
|
readonly status_codex_plan: "Codex 套餐:{value}";
|
|
670
673
|
readonly status_codex_usage_title: "Codex 用量({value}):";
|
|
671
|
-
readonly status_codex_usage_window: "{window}
|
|
674
|
+
readonly status_codex_usage_window: "{window}:剩余 {percent}%{reset}";
|
|
672
675
|
readonly status_codex_usage_reset: ",重置时间 {value}";
|
|
673
676
|
readonly status_codex_usage_unavailable: "Codex 用量:无法获取({error})";
|
|
674
677
|
readonly status_codex_local_history: "Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录";
|
|
675
678
|
readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}";
|
|
676
679
|
readonly status_codex_local_speed: "Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)";
|
|
680
|
+
readonly status_codex_local_snapshot_at: "Codex 本地统计快照:{value}";
|
|
681
|
+
readonly status_codex_local_usage_refreshing: "Codex 本地历史:正在后台生成统计快照";
|
|
677
682
|
readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
|
|
678
683
|
readonly status_codex_credits: "Codex 额度:{value}";
|
|
679
684
|
readonly status_codex_limit_reached: "Codex 限制:{value}";
|
|
@@ -692,6 +697,7 @@ declare const MESSAGES: {
|
|
|
692
697
|
readonly auth_current: "当前 auth:{value}";
|
|
693
698
|
readonly auth_dir: "Auth 目录:{value}";
|
|
694
699
|
readonly auth_candidate_count: "候选数量:{value}";
|
|
700
|
+
readonly auth_quota_legend: "额度剩余:5h|7d|auth";
|
|
695
701
|
readonly auth_candidate_status_enabled: "启用";
|
|
696
702
|
readonly auth_candidate_status_disabled: "禁用";
|
|
697
703
|
readonly auth_candidate_enabled: "已启用 auth 候选自动轮换:{value}";
|
package/dist/i18n.js
CHANGED
|
@@ -88,7 +88,7 @@ const MESSAGES = {
|
|
|
88
88
|
status_app_server: 'Codex app-server: {value}',
|
|
89
89
|
status_app_server_running: 'running',
|
|
90
90
|
status_app_server_stale: 'stale',
|
|
91
|
-
status_user_agent: '
|
|
91
|
+
status_user_agent: 'Codex/FoxClaw agent: {value}',
|
|
92
92
|
status_current_thread: 'Current thread: {value}',
|
|
93
93
|
status_configured_model: 'Configured model: {value}',
|
|
94
94
|
status_configured_effort: 'Configured effort: {value}',
|
|
@@ -105,12 +105,14 @@ const MESSAGES = {
|
|
|
105
105
|
status_codex_account: 'Codex account: {value}',
|
|
106
106
|
status_codex_plan: 'Codex plan: {value}',
|
|
107
107
|
status_codex_usage_title: 'Codex usage ({value}):',
|
|
108
|
-
status_codex_usage_window: '{window}: {percent}%
|
|
108
|
+
status_codex_usage_window: '{window}: {percent}% remaining{reset}',
|
|
109
109
|
status_codex_usage_reset: ', resets {value}',
|
|
110
110
|
status_codex_usage_unavailable: 'Codex usage: unavailable ({error})',
|
|
111
111
|
status_codex_local_history: 'Codex local history: {sessions} sessions, {turns} turns, {events} usage records',
|
|
112
112
|
status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}',
|
|
113
113
|
status_codex_local_speed: 'Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)',
|
|
114
|
+
status_codex_local_snapshot_at: 'Codex local stats snapshot: {value}',
|
|
115
|
+
status_codex_local_usage_refreshing: 'Codex local history: building snapshot in background',
|
|
114
116
|
status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
|
|
115
117
|
status_codex_credits: 'Codex credits: {value}',
|
|
116
118
|
status_codex_limit_reached: 'Codex limit: {value}',
|
|
@@ -129,6 +131,7 @@ const MESSAGES = {
|
|
|
129
131
|
auth_current: 'Current auth: {value}',
|
|
130
132
|
auth_dir: 'Auth dir: {value}',
|
|
131
133
|
auth_candidate_count: 'Candidates: {value}',
|
|
134
|
+
auth_quota_legend: 'Quota remaining: 5h|7d|auth',
|
|
132
135
|
auth_candidate_status_enabled: 'enabled',
|
|
133
136
|
auth_candidate_status_disabled: 'disabled',
|
|
134
137
|
auth_candidate_enabled: 'Enabled auth candidate for auto-rotation: {value}',
|
|
@@ -649,7 +652,7 @@ const MESSAGES = {
|
|
|
649
652
|
status_app_server: 'Codex app-server:{value}',
|
|
650
653
|
status_app_server_running: '运行中',
|
|
651
654
|
status_app_server_stale: '失效',
|
|
652
|
-
status_user_agent: '
|
|
655
|
+
status_user_agent: 'Codex/FoxClaw 标识:{value}',
|
|
653
656
|
status_current_thread: '当前线程:{value}',
|
|
654
657
|
status_configured_model: '已配置模型:{value}',
|
|
655
658
|
status_configured_effort: '已配置推理强度:{value}',
|
|
@@ -666,12 +669,14 @@ const MESSAGES = {
|
|
|
666
669
|
status_codex_account: 'Codex 账号:{value}',
|
|
667
670
|
status_codex_plan: 'Codex 套餐:{value}',
|
|
668
671
|
status_codex_usage_title: 'Codex 用量({value}):',
|
|
669
|
-
status_codex_usage_window: '{window}
|
|
672
|
+
status_codex_usage_window: '{window}:剩余 {percent}%{reset}',
|
|
670
673
|
status_codex_usage_reset: ',重置时间 {value}',
|
|
671
674
|
status_codex_usage_unavailable: 'Codex 用量:无法获取({error})',
|
|
672
675
|
status_codex_local_history: 'Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录',
|
|
673
676
|
status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}',
|
|
674
677
|
status_codex_local_speed: 'Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)',
|
|
678
|
+
status_codex_local_snapshot_at: 'Codex 本地统计快照:{value}',
|
|
679
|
+
status_codex_local_usage_refreshing: 'Codex 本地历史:正在后台生成统计快照',
|
|
675
680
|
status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
|
|
676
681
|
status_codex_credits: 'Codex 额度:{value}',
|
|
677
682
|
status_codex_limit_reached: 'Codex 限制:{value}',
|
|
@@ -690,6 +695,7 @@ const MESSAGES = {
|
|
|
690
695
|
auth_current: '当前 auth:{value}',
|
|
691
696
|
auth_dir: 'Auth 目录:{value}',
|
|
692
697
|
auth_candidate_count: '候选数量:{value}',
|
|
698
|
+
auth_quota_legend: '额度剩余:5h|7d|auth',
|
|
693
699
|
auth_candidate_status_enabled: '启用',
|
|
694
700
|
auth_candidate_status_disabled: '禁用',
|
|
695
701
|
auth_candidate_enabled: '已启用 auth 候选自动轮换:{value}',
|
package/dist/main.js
CHANGED
|
@@ -9,7 +9,7 @@ import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getL
|
|
|
9
9
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
10
10
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
11
11
|
import { refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
|
|
12
|
-
import { createSelfUpdateRuntime, performSelfUpdate } from './update.js';
|
|
12
|
+
import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate } from './update.js';
|
|
13
13
|
const rawCommand = process.argv[2];
|
|
14
14
|
const command = rawCommand || 'serve';
|
|
15
15
|
loadEnv();
|
|
@@ -776,9 +776,17 @@ function stopLaunchd() {
|
|
|
776
776
|
console.log(`Stopped ${plist}`);
|
|
777
777
|
}
|
|
778
778
|
function buildServicePath(nodeDir) {
|
|
779
|
+
const pnpmPath = resolveCommand('pnpm');
|
|
780
|
+
const inferredPnpmHome = inferPnpmHomeFromEntryPoint(entryPoint) || '';
|
|
781
|
+
const configuredPnpmHome = process.env.PNPM_HOME?.trim() || '';
|
|
779
782
|
const parts = [
|
|
780
783
|
path.join(process.env.HOME || '', '.local', 'bin'),
|
|
781
784
|
nodeDir,
|
|
785
|
+
inferredPnpmHome,
|
|
786
|
+
inferredPnpmHome ? path.join(inferredPnpmHome, 'bin') : '',
|
|
787
|
+
configuredPnpmHome,
|
|
788
|
+
configuredPnpmHome ? path.join(configuredPnpmHome, 'bin') : '',
|
|
789
|
+
pnpmPath ? path.dirname(pnpmPath) : '',
|
|
782
790
|
'/usr/local/sbin',
|
|
783
791
|
'/usr/local/bin',
|
|
784
792
|
'/usr/sbin',
|
package/dist/update.d.ts
CHANGED
|
@@ -41,7 +41,8 @@ export interface SelfUpdateOutcome {
|
|
|
41
41
|
error: string | null;
|
|
42
42
|
}
|
|
43
43
|
export declare function selfUpdateStatusPath(statusPath: string): string;
|
|
44
|
-
export declare function
|
|
44
|
+
export declare function inferPnpmHomeFromEntryPoint(entryPoint: string): string | null;
|
|
45
|
+
export declare function resolveSelfUpdateInstaller(entryPoint: string, nodePath?: string, exists?: (target: string) => boolean, env?: NodeJS.ProcessEnv): SelfUpdateInstaller;
|
|
45
46
|
export declare function readSelfUpdateStatus(statusFile: string): SelfUpdateStatus | null;
|
|
46
47
|
export declare function writeSelfUpdateStatus(statusFile: string, status: SelfUpdateStatus): void;
|
|
47
48
|
export declare function createSelfUpdateRuntime(options: CreateSelfUpdateRuntimeOptions): SelfUpdateRuntime;
|
package/dist/update.js
CHANGED
|
@@ -7,21 +7,42 @@ const UPDATE_STATUS_FILENAME = 'self-update.json';
|
|
|
7
7
|
export function selfUpdateStatusPath(statusPath) {
|
|
8
8
|
return path.join(path.dirname(statusPath), UPDATE_STATUS_FILENAME);
|
|
9
9
|
}
|
|
10
|
-
export function
|
|
10
|
+
export function inferPnpmHomeFromEntryPoint(entryPoint) {
|
|
11
11
|
const normalizedEntryPoint = entryPoint.replace(/\\/g, '/');
|
|
12
12
|
const globalMarker = '/global/';
|
|
13
13
|
const globalIndex = normalizedEntryPoint.indexOf(globalMarker);
|
|
14
|
-
if (globalIndex
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (globalIndex <= 0 || !normalizedEntryPoint.includes('/.pnpm/')) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return normalizedEntryPoint.slice(0, globalIndex);
|
|
18
|
+
}
|
|
19
|
+
export function resolveSelfUpdateInstaller(entryPoint, nodePath = process.execPath, exists = fs.existsSync, env = process.env) {
|
|
20
|
+
const pnpmHome = inferPnpmHomeFromEntryPoint(entryPoint);
|
|
21
|
+
if (pnpmHome) {
|
|
22
|
+
const commandName = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
23
|
+
const candidates = executableCandidates(commandName, nodePath, env, [
|
|
24
|
+
path.join(pnpmHome, commandName),
|
|
25
|
+
path.join(pnpmHome, 'bin', commandName),
|
|
26
|
+
env.PNPM_HOME?.trim() ? path.join(env.PNPM_HOME.trim(), commandName) : '',
|
|
27
|
+
env.PNPM_HOME?.trim() ? path.join(env.PNPM_HOME.trim(), 'bin', commandName) : '',
|
|
28
|
+
]);
|
|
29
|
+
const pnpmCommand = candidates.find((candidate) => exists(candidate));
|
|
30
|
+
if (pnpmCommand) {
|
|
31
|
+
return {
|
|
32
|
+
manager: 'pnpm',
|
|
33
|
+
command: pnpmCommand,
|
|
34
|
+
installArgs: ['add', '--global', PACKAGE_SPEC],
|
|
35
|
+
rootArgs: ['root', '--global'],
|
|
36
|
+
};
|
|
19
37
|
}
|
|
38
|
+
const npmCommandName = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
39
|
+
const npmCommand = executableCandidates(npmCommandName, nodePath, env)
|
|
40
|
+
.find((candidate) => exists(candidate)) ?? npmCommandName;
|
|
20
41
|
return {
|
|
21
42
|
manager: 'pnpm',
|
|
22
|
-
command:
|
|
23
|
-
installArgs: ['add', '--global', PACKAGE_SPEC],
|
|
24
|
-
rootArgs: ['root', '--global'],
|
|
43
|
+
command: npmCommand,
|
|
44
|
+
installArgs: ['exec', '--yes', '--package=pnpm@latest', '--', 'pnpm', 'add', '--global', PACKAGE_SPEC],
|
|
45
|
+
rootArgs: ['exec', '--yes', '--package=pnpm@latest', '--', 'pnpm', 'root', '--global'],
|
|
25
46
|
};
|
|
26
47
|
}
|
|
27
48
|
const adjacentNpm = path.join(path.dirname(nodePath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
@@ -117,13 +138,14 @@ export function performSelfUpdate(options) {
|
|
|
117
138
|
const env = options.env ?? process.env;
|
|
118
139
|
let toVersion = null;
|
|
119
140
|
try {
|
|
120
|
-
const installer = resolveSelfUpdateInstaller(options.entryPoint, options.nodePath);
|
|
141
|
+
const installer = resolveSelfUpdateInstaller(options.entryPoint, options.nodePath, fs.existsSync, env);
|
|
142
|
+
const installerEnv = buildInstallerEnv(options.entryPoint, installer, env);
|
|
121
143
|
console.log(`[UPDATE] Installing ${PACKAGE_SPEC} with ${installer.manager}...`);
|
|
122
|
-
runInherited(installer.command, installer.installArgs,
|
|
123
|
-
const updatedEntryPoint = resolveUpdatedEntryPoint(installer,
|
|
144
|
+
runInherited(installer.command, installer.installArgs, installerEnv);
|
|
145
|
+
const updatedEntryPoint = resolveUpdatedEntryPoint(installer, installerEnv);
|
|
124
146
|
toVersion = readInstalledPackageVersion(updatedEntryPoint);
|
|
125
147
|
console.log('[UPDATE] Running checks and restarting the FoxClaw service...');
|
|
126
|
-
runInherited(options.nodePath, [updatedEntryPoint, 'start'],
|
|
148
|
+
runInherited(options.nodePath, [updatedEntryPoint, 'start'], installerEnv);
|
|
127
149
|
completeNotification(options.notificationFile, 'succeeded', toVersion, null);
|
|
128
150
|
console.log(`[OK] FoxClaw updated and restarted: ${options.version} -> ${toVersion}`);
|
|
129
151
|
return {
|
|
@@ -145,6 +167,32 @@ export function performSelfUpdate(options) {
|
|
|
145
167
|
};
|
|
146
168
|
}
|
|
147
169
|
}
|
|
170
|
+
function executableCandidates(commandName, nodePath, env, preferred = []) {
|
|
171
|
+
return [
|
|
172
|
+
...preferred,
|
|
173
|
+
path.join(path.dirname(nodePath), commandName),
|
|
174
|
+
...(env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, commandName)),
|
|
175
|
+
].filter((candidate, index, all) => candidate && all.indexOf(candidate) === index);
|
|
176
|
+
}
|
|
177
|
+
function buildInstallerEnv(entryPoint, installer, env) {
|
|
178
|
+
const pnpmHome = installer.manager === 'pnpm' ? inferPnpmHomeFromEntryPoint(entryPoint) : null;
|
|
179
|
+
if (!pnpmHome) {
|
|
180
|
+
return env;
|
|
181
|
+
}
|
|
182
|
+
const configuredPnpmHome = env.PNPM_HOME?.trim() || pnpmHome;
|
|
183
|
+
const pathEntries = [
|
|
184
|
+
configuredPnpmHome,
|
|
185
|
+
path.join(configuredPnpmHome, 'bin'),
|
|
186
|
+
pnpmHome,
|
|
187
|
+
path.join(pnpmHome, 'bin'),
|
|
188
|
+
...(env.PATH || '').split(path.delimiter).filter(Boolean),
|
|
189
|
+
];
|
|
190
|
+
return {
|
|
191
|
+
...env,
|
|
192
|
+
PNPM_HOME: configuredPnpmHome,
|
|
193
|
+
PATH: pathEntries.filter((entry, index, all) => all.indexOf(entry) === index).join(path.delimiter),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
148
196
|
function runInherited(command, args, env) {
|
|
149
197
|
const result = spawnSync(command, args, { stdio: 'inherit', env });
|
|
150
198
|
if (result.error) {
|
package/docs/user-manual.md
CHANGED
|
@@ -230,7 +230,7 @@ Later commands are sorted by recent usage. Plain text, photos, and files continu
|
|
|
230
230
|
|
|
231
231
|
### 3.2 `/status`, `/account`, `/quota`, `/update`
|
|
232
232
|
|
|
233
|
-
- `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary.
|
|
233
|
+
- `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary. Local session, token, and output-speed metrics use a background-generated historical snapshot instead of scanning large logs during the request.
|
|
234
234
|
- `/account`: current Codex account.
|
|
235
235
|
- `/quota`: Codex usage and quota window.
|
|
236
236
|
- `/update`: upgrade FoxClaw, run checks, and restart the service; it refuses while a turn, approval, or question is active, then reports the result after restart.
|
|
@@ -387,7 +387,7 @@ If the login is cancelled or fails, FoxClaw tries to restore the previous auth t
|
|
|
387
387
|
|
|
388
388
|
### 6.3 The `/auth` Panel
|
|
389
389
|
|
|
390
|
-
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload.
|
|
390
|
+
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. The `5h|7d` numbers before each filename are the last recorded remaining percentages for the two quota windows; the current auth is refreshed when the panel opens, while other candidates are not switched merely to query quota.
|
|
391
391
|
|
|
392
392
|
Approximation:
|
|
393
393
|
|
|
@@ -396,16 +396,17 @@ Codex auth
|
|
|
396
396
|
Current: auth.json_personal
|
|
397
397
|
Auth dir: /home/alice/.codex
|
|
398
398
|
Candidates: 2
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
Quota remaining: 5h|7d|auth
|
|
400
|
+
1. 20|25|auth.json_personal * [enabled]
|
|
401
|
+
2. --|--|auth.json_team [enabled]
|
|
401
402
|
|
|
402
|
-
[✅ auth.json_personal] [✅]
|
|
403
|
-
[🔐 auth.json_team] [✅]
|
|
403
|
+
[✅ 20|25|auth.json_personal] [✅]
|
|
404
|
+
[🔐 --|--|auth.json_team] [✅]
|
|
404
405
|
[🛡️ Access] [🔑 Login]
|
|
405
406
|
[🔄 Reload auth]
|
|
406
407
|
```
|
|
407
408
|
|
|
408
|
-
The right-side `✅` / `⏸️` button shows the current state. Tapping it toggles enabled/disabled, and the refreshed list shows the new state.
|
|
409
|
+
The right-side `✅` / `⏸️` button shows the current state. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. `--|--` means no quota snapshot has been observed for that candidate yet.
|
|
409
410
|
|
|
410
411
|
Equivalent commands:
|
|
411
412
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -230,7 +230,7 @@ TG_ALLOWED_TOPIC_ID=42
|
|
|
230
230
|
|
|
231
231
|
### 3.2 `/status`、`/account`、`/quota`、`/update`
|
|
232
232
|
|
|
233
|
-
- `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex
|
|
233
|
+
- `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex 用量摘要。本地 session/Token/输出速度使用后台生成的历史快照,避免状态查询现场扫描大量日志。
|
|
234
234
|
- `/account`:查看当前 Codex 登录账号。
|
|
235
235
|
- `/quota`:查看 Codex 用量和额度窗口。
|
|
236
236
|
- `/update`:升级 FoxClaw、自检并重启服务;当前有运行中回复、审批或待确认问题时会拒绝执行,重启后会回报结果。
|
|
@@ -387,7 +387,7 @@ cp -L ~/.codex/auth.json ~/.codex/auth.json_personal
|
|
|
387
387
|
|
|
388
388
|
### 6.3 `/auth` 面板
|
|
389
389
|
|
|
390
|
-
`/auth` 会列出候选账号、当前账号和 auth
|
|
390
|
+
`/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth 会在打开面板时刷新,其他候选不会为了查询额度被自动切换。
|
|
391
391
|
|
|
392
392
|
示意:
|
|
393
393
|
|
|
@@ -396,16 +396,17 @@ Codex auth
|
|
|
396
396
|
Current: auth.json_personal
|
|
397
397
|
Auth dir: /home/alice/.codex
|
|
398
398
|
Candidates: 2
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
额度剩余:5h|7d|auth
|
|
400
|
+
1. 20|25|auth.json_personal * [enabled]
|
|
401
|
+
2. --|--|auth.json_team [enabled]
|
|
401
402
|
|
|
402
|
-
[✅ auth.json_personal] [✅]
|
|
403
|
-
[🔐 auth.json_team] [✅]
|
|
403
|
+
[✅ 20|25|auth.json_personal] [✅]
|
|
404
|
+
[🔐 --|--|auth.json_team] [✅]
|
|
404
405
|
[🛡️ Access] [🔑 设备登录]
|
|
405
406
|
[🔄 Reload auth]
|
|
406
407
|
```
|
|
407
408
|
|
|
408
|
-
右侧 `✅` / `⏸️`
|
|
409
|
+
右侧 `✅` / `⏸️` 表示当前状态。点一下会切换启用/禁用,列表刷新后图标会随状态变化。`--|--` 表示该候选还没有额度历史快照。
|
|
409
410
|
|
|
410
411
|
命令等价用法:
|
|
411
412
|
|