@foxden-app/foxclaw 0.3.7 → 0.3.10
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/controller/controller.d.ts +2 -0
- package/dist/controller/controller.js +58 -14
- package/dist/controller/presentation.d.ts +3 -2
- package/dist/controller/presentation.js +20 -6
- package/dist/i18n.d.ts +8 -8
- package/dist/i18n.js +8 -8
- package/dist/main.js +45 -1
- package/package.json +2 -2
- package/skills/foxclaw/SKILL.md +44 -7
- package/skills/foxclaw/agents/openai.yaml +4 -4
- package/skills/npm-publish/SKILL.md +35 -1
|
@@ -249,6 +249,8 @@ export declare class BridgeSessionCore {
|
|
|
249
249
|
private retryTurnAfterAuthRotation;
|
|
250
250
|
private selectNextCodexAuthCandidate;
|
|
251
251
|
private listCodexAuthState;
|
|
252
|
+
private readCodexAuthSwitchLabels;
|
|
253
|
+
private codexAuthSwitchParams;
|
|
252
254
|
private switchCodexAuthAndRestart;
|
|
253
255
|
private buildNativeCollaborationMode;
|
|
254
256
|
private buildCodexUsageStatusLines;
|
|
@@ -3693,7 +3693,8 @@ export class BridgeSessionCore {
|
|
|
3693
3693
|
await this.sendMessage(scopeId, renderAuthListMessage(locale, state, parseWeixinBridgeScope(scopeId) !== null));
|
|
3694
3694
|
return;
|
|
3695
3695
|
}
|
|
3696
|
-
await this.
|
|
3696
|
+
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
3697
|
+
await this.sendMessage(scopeId, t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel)));
|
|
3697
3698
|
await this.switchCodexAuthAndRestart(scopeId, locale, candidate, false);
|
|
3698
3699
|
}
|
|
3699
3700
|
async handleAuthToggleCommand(scopeId, locale, args, disabled) {
|
|
@@ -4190,8 +4191,10 @@ export class BridgeSessionCore {
|
|
|
4190
4191
|
}
|
|
4191
4192
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_recorded'));
|
|
4192
4193
|
this.pendingAuthChoiceLists.delete(localId);
|
|
4194
|
+
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
4195
|
+
const switchingMessage = t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel));
|
|
4193
4196
|
if (record.messageId !== null) {
|
|
4194
|
-
await this.editMessage(event.scopeId, record.messageId,
|
|
4197
|
+
await this.editMessage(event.scopeId, record.messageId, switchingMessage, []);
|
|
4195
4198
|
}
|
|
4196
4199
|
await this.switchCodexAuthAndRestart(event.scopeId, locale, candidate, false);
|
|
4197
4200
|
}
|
|
@@ -4207,16 +4210,17 @@ export class BridgeSessionCore {
|
|
|
4207
4210
|
this.authRotationInProgress = true;
|
|
4208
4211
|
try {
|
|
4209
4212
|
const failedTargets = rotation.retry?.failedAuthTargets ?? this.authRotationFailedTargets;
|
|
4210
|
-
const
|
|
4213
|
+
const selection = await this.selectNextCodexAuthCandidate(failedTargets);
|
|
4211
4214
|
const locale = this.localeForChat(rotation.scopeId);
|
|
4212
|
-
if (!
|
|
4215
|
+
if (!selection) {
|
|
4213
4216
|
await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_no_candidate', {
|
|
4214
4217
|
error: formatShortStatusError(rotation.reason),
|
|
4215
4218
|
}));
|
|
4216
4219
|
return false;
|
|
4217
4220
|
}
|
|
4221
|
+
const { candidate, fromLabel, toLabel } = selection;
|
|
4218
4222
|
await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_switching', {
|
|
4219
|
-
|
|
4223
|
+
...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
|
|
4220
4224
|
error: formatShortStatusError(rotation.reason),
|
|
4221
4225
|
}));
|
|
4222
4226
|
await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true);
|
|
@@ -4290,21 +4294,35 @@ export class BridgeSessionCore {
|
|
|
4290
4294
|
for (let offset = 1; offset <= state.candidates.length; offset += 1) {
|
|
4291
4295
|
const candidate = state.candidates[(currentIndex + offset + state.candidates.length) % state.candidates.length];
|
|
4292
4296
|
if (candidate && !candidate.disabled && !failedTargets.has(candidate.path)) {
|
|
4293
|
-
return candidate;
|
|
4297
|
+
return { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) };
|
|
4294
4298
|
}
|
|
4295
4299
|
}
|
|
4296
|
-
|
|
4300
|
+
const candidate = candidates[0] ?? null;
|
|
4301
|
+
return candidate ? { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) } : null;
|
|
4297
4302
|
}
|
|
4298
4303
|
async listCodexAuthState() {
|
|
4299
4304
|
return listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames());
|
|
4300
4305
|
}
|
|
4306
|
+
async readCodexAuthSwitchLabels(candidate) {
|
|
4307
|
+
const state = await this.listCodexAuthState();
|
|
4308
|
+
return {
|
|
4309
|
+
fromLabel: state.currentLabel,
|
|
4310
|
+
toLabel: await authPathDisplayLabel(candidate.path),
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
codexAuthSwitchParams(locale, fromLabel, toLabel) {
|
|
4314
|
+
return {
|
|
4315
|
+
from: fromLabel ?? t(locale, 'none'),
|
|
4316
|
+
to: toLabel,
|
|
4317
|
+
};
|
|
4318
|
+
}
|
|
4301
4319
|
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic) {
|
|
4302
|
-
await switchCodexAuth(candidate.path);
|
|
4320
|
+
const result = await switchCodexAuth(candidate.path);
|
|
4303
4321
|
this.authRotationFailedTargets.delete(candidate.path);
|
|
4304
4322
|
this.pendingTurnErrors.clear();
|
|
4305
4323
|
this.attachedThreads.clear();
|
|
4306
4324
|
await this.app.restart();
|
|
4307
|
-
const lines = [t(locale, automatic ? 'auth_auto_done' : 'auth_switch_done',
|
|
4325
|
+
const lines = [t(locale, automatic ? 'auth_auto_done' : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
|
|
4308
4326
|
lines.push(...await this.buildCodexUsageStatusLines(locale));
|
|
4309
4327
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
4310
4328
|
}
|
|
@@ -4599,8 +4617,8 @@ export class BridgeSessionCore {
|
|
|
4599
4617
|
};
|
|
4600
4618
|
const text = formatThreadsMessage(locale, threadLikes, binding.threadId, listState.searchTerm, listState);
|
|
4601
4619
|
const keyboard = parseWeixinBridgeScope(scopeId)
|
|
4602
|
-
? buildThreadsKeyboard(locale, threadLikes)
|
|
4603
|
-
: buildThreadListKeyboard(locale, threadLikes, listState);
|
|
4620
|
+
? buildThreadsKeyboard(locale, threadLikes, binding.threadId)
|
|
4621
|
+
: buildThreadListKeyboard(locale, threadLikes, listState, binding.threadId);
|
|
4604
4622
|
await this.editHtmlMessage(scopeId, event.messageId, text, keyboard);
|
|
4605
4623
|
}
|
|
4606
4624
|
let callbackText = t(locale, 'thread_opened');
|
|
@@ -5040,13 +5058,14 @@ export class BridgeSessionCore {
|
|
|
5040
5058
|
threadId: row.threadId,
|
|
5041
5059
|
name: row.name,
|
|
5042
5060
|
preview: row.preview,
|
|
5061
|
+
cwd: row.cwd,
|
|
5043
5062
|
archived: row.archived,
|
|
5044
5063
|
}));
|
|
5045
5064
|
text += `\n\n${formatWeixinThreadsCopyPaste(locale, rows, searchTerm ?? null, offset)}`;
|
|
5046
5065
|
}
|
|
5047
5066
|
const keyboard = parseWeixinBridgeScope(scopeId)
|
|
5048
|
-
? buildThreadsKeyboard(locale, forDisplay)
|
|
5049
|
-
: buildThreadListKeyboard(locale, forDisplay, presentationState);
|
|
5067
|
+
? buildThreadsKeyboard(locale, forDisplay, binding?.threadId ?? null)
|
|
5068
|
+
: buildThreadListKeyboard(locale, forDisplay, presentationState, binding?.threadId ?? null);
|
|
5050
5069
|
if (messageId !== undefined) {
|
|
5051
5070
|
await this.editHtmlMessage(scopeId, messageId, text, keyboard);
|
|
5052
5071
|
return;
|
|
@@ -6913,7 +6932,7 @@ async function listCodexAuthState(disabledNames = new Set()) {
|
|
|
6913
6932
|
authDir,
|
|
6914
6933
|
authPath,
|
|
6915
6934
|
currentTargetPath,
|
|
6916
|
-
currentLabel: currentTargetPath ?
|
|
6935
|
+
currentLabel: currentTargetPath ? await authPathDisplayLabel(currentTargetPath) : null,
|
|
6917
6936
|
candidates,
|
|
6918
6937
|
};
|
|
6919
6938
|
}
|
|
@@ -6946,6 +6965,27 @@ async function resolveCurrentAuthTarget(authDir, authPath) {
|
|
|
6946
6965
|
}
|
|
6947
6966
|
return authPath;
|
|
6948
6967
|
}
|
|
6968
|
+
async function resolveAuthFinalTargetPath(startPath) {
|
|
6969
|
+
let currentPath = startPath;
|
|
6970
|
+
const seen = new Set();
|
|
6971
|
+
for (let depth = 0; depth < 20; depth += 1) {
|
|
6972
|
+
const absolutePath = path.resolve(currentPath);
|
|
6973
|
+
if (seen.has(absolutePath)) {
|
|
6974
|
+
return currentPath;
|
|
6975
|
+
}
|
|
6976
|
+
seen.add(absolutePath);
|
|
6977
|
+
const stat = await fs.lstat(currentPath).catch(() => null);
|
|
6978
|
+
if (!stat?.isSymbolicLink()) {
|
|
6979
|
+
return currentPath;
|
|
6980
|
+
}
|
|
6981
|
+
const target = await fs.readlink(currentPath);
|
|
6982
|
+
currentPath = path.resolve(path.dirname(currentPath), target);
|
|
6983
|
+
}
|
|
6984
|
+
return currentPath;
|
|
6985
|
+
}
|
|
6986
|
+
async function authPathDisplayLabel(authPath) {
|
|
6987
|
+
return path.basename(await resolveAuthFinalTargetPath(authPath));
|
|
6988
|
+
}
|
|
6949
6989
|
async function pointCodexAuthAtTarget(authDir, authPath, targetPath) {
|
|
6950
6990
|
await fs.mkdir(authDir, { recursive: true });
|
|
6951
6991
|
const tempLink = path.join(authDir, `.auth.json.${process.pid}.${Date.now()}.tmp`);
|
|
@@ -6965,6 +7005,10 @@ async function switchCodexAuth(targetPath) {
|
|
|
6965
7005
|
throw new Error(`Auth candidate is no longer available: ${path.basename(targetPath)}`);
|
|
6966
7006
|
}
|
|
6967
7007
|
await pointCodexAuthAtTarget(state.authDir, state.authPath, candidate.path);
|
|
7008
|
+
return {
|
|
7009
|
+
fromLabel: state.currentLabel,
|
|
7010
|
+
toLabel: await authPathDisplayLabel(candidate.path),
|
|
7011
|
+
};
|
|
6968
7012
|
}
|
|
6969
7013
|
function renderAuthListMessage(locale, state, includeWeixinCopyPaste = false) {
|
|
6970
7014
|
const lines = [
|
|
@@ -33,12 +33,12 @@ export interface ThreadListPresentationState {
|
|
|
33
33
|
archived?: boolean;
|
|
34
34
|
}
|
|
35
35
|
export declare function formatThreadsMessage(locale: AppLocale, threads: ThreadLike[], currentThreadId: string | null, searchTerm?: string | null, listState?: ThreadListPresentationState): string;
|
|
36
|
-
export declare function buildThreadsKeyboard(locale: AppLocale, threads: ThreadLike[]): Array<Array<{
|
|
36
|
+
export declare function buildThreadsKeyboard(locale: AppLocale, threads: ThreadLike[], currentThreadId?: string | null): Array<Array<{
|
|
37
37
|
text: string;
|
|
38
38
|
callback_data: string;
|
|
39
39
|
}>>;
|
|
40
40
|
/** Threads keyboard plus Prev/Next and optional clear-filter row (Telegram only). */
|
|
41
|
-
export declare function buildThreadListKeyboard(locale: AppLocale, threads: ThreadLike[], listState: ThreadListPresentationState): Array<Array<{
|
|
41
|
+
export declare function buildThreadListKeyboard(locale: AppLocale, threads: ThreadLike[], listState: ThreadListPresentationState, currentThreadId?: string | null): Array<Array<{
|
|
42
42
|
text: string;
|
|
43
43
|
callback_data: string;
|
|
44
44
|
}>>;
|
|
@@ -51,6 +51,7 @@ export interface WeixinCopyPasteThreadRow {
|
|
|
51
51
|
threadId: string;
|
|
52
52
|
name: string | null;
|
|
53
53
|
preview: string;
|
|
54
|
+
cwd?: string | null;
|
|
54
55
|
archived?: boolean;
|
|
55
56
|
}
|
|
56
57
|
export declare function formatWeixinThreadsCopyPaste(locale: AppLocale, threads: WeixinCopyPasteThreadRow[], searchTerm?: string | null,
|
|
@@ -24,7 +24,7 @@ export function formatThreadsMessage(locale, threads, currentThreadId, searchTer
|
|
|
24
24
|
}));
|
|
25
25
|
}
|
|
26
26
|
if (currentThread) {
|
|
27
|
-
const currentTitle = truncate(
|
|
27
|
+
const currentTitle = truncate(formatThreadDisplayName(locale, currentThread), 40);
|
|
28
28
|
headerLines.push(t(locale, 'threads_current', { title: escapeTelegramHtml(currentTitle) }));
|
|
29
29
|
headerLines.push(escapeTelegramHtml([
|
|
30
30
|
formatCwd(locale, currentThread.cwd),
|
|
@@ -32,13 +32,16 @@ export function formatThreadsMessage(locale, threads, currentThreadId, searchTer
|
|
|
32
32
|
formatStatusLabel(locale, currentThread.status),
|
|
33
33
|
].filter(Boolean).join(' | ')));
|
|
34
34
|
}
|
|
35
|
+
headerLines.push('', ...threads.map((thread, index) => formatThreadListLine(locale, thread, currentThreadId, index)));
|
|
35
36
|
return headerLines.join('\n');
|
|
36
37
|
}
|
|
37
|
-
export function buildThreadsKeyboard(locale, threads) {
|
|
38
|
+
export function buildThreadsKeyboard(locale, threads, currentThreadId = null) {
|
|
38
39
|
return threads.flatMap((thread, index) => {
|
|
39
40
|
const ordinal = typeof thread.index === 'number' ? thread.index : index + 1;
|
|
41
|
+
const selected = currentThreadId === thread.threadId;
|
|
42
|
+
const prefix = selected ? '✅ ' : '';
|
|
40
43
|
const openRow = [{
|
|
41
|
-
text:
|
|
44
|
+
text: `${prefix}${ordinal}. ${truncate(formatThreadDisplayName(locale, thread), 48)}`,
|
|
42
45
|
callback_data: `thread:open:${thread.threadId}`,
|
|
43
46
|
}];
|
|
44
47
|
const actionRow = thread.archived
|
|
@@ -52,9 +55,15 @@ export function buildThreadsKeyboard(locale, threads) {
|
|
|
52
55
|
return [openRow, actionRow];
|
|
53
56
|
});
|
|
54
57
|
}
|
|
58
|
+
function formatThreadListLine(locale, thread, currentThreadId, index) {
|
|
59
|
+
const ordinal = typeof thread.index === 'number' ? thread.index : index + 1;
|
|
60
|
+
const prefix = currentThreadId === thread.threadId ? '✅ ' : '';
|
|
61
|
+
const title = truncate(formatThreadDisplayName(locale, thread), 72);
|
|
62
|
+
return `${prefix}${ordinal}. ${escapeTelegramHtml(title)}`;
|
|
63
|
+
}
|
|
55
64
|
/** Threads keyboard plus Prev/Next and optional clear-filter row (Telegram only). */
|
|
56
|
-
export function buildThreadListKeyboard(locale, threads, listState) {
|
|
57
|
-
const rows = buildThreadsKeyboard(locale, threads);
|
|
65
|
+
export function buildThreadListKeyboard(locale, threads, listState, currentThreadId = null) {
|
|
66
|
+
const rows = buildThreadsKeyboard(locale, threads, currentThreadId);
|
|
58
67
|
rows.push([{ text: t(locale, 'button_new_thread'), callback_data: 'thread:new' }]);
|
|
59
68
|
const navigationRow = [];
|
|
60
69
|
if (listState.hasPreviousPage) {
|
|
@@ -142,7 +151,7 @@ pageOffset = 0) {
|
|
|
142
151
|
for (let i = 0; i < threads.length; i += 1) {
|
|
143
152
|
const index = pageOffset + i + 1;
|
|
144
153
|
const thread = threads[i];
|
|
145
|
-
const title = truncate(
|
|
154
|
+
const title = truncate(formatThreadDisplayName(locale, thread), 40);
|
|
146
155
|
lines.push(`${index}. ${title}`);
|
|
147
156
|
if (thread.archived) {
|
|
148
157
|
lines.push(`/thread_unarchive ${index}`);
|
|
@@ -534,6 +543,11 @@ function formatCwd(locale, cwd) {
|
|
|
534
543
|
const base = path.basename(cwd);
|
|
535
544
|
return base || cwd;
|
|
536
545
|
}
|
|
546
|
+
function formatThreadDisplayName(locale, thread) {
|
|
547
|
+
const folder = compactWhitespace(formatCwd(locale, thread.cwd ?? null));
|
|
548
|
+
const title = compactWhitespace(thread.name || thread.preview || t(locale, 'empty'));
|
|
549
|
+
return `${folder}|${title}`;
|
|
550
|
+
}
|
|
537
551
|
function compactWhitespace(value) {
|
|
538
552
|
return value.replace(/\s+/g, ' ').trim();
|
|
539
553
|
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -133,10 +133,10 @@ declare const MESSAGES: {
|
|
|
133
133
|
readonly auth_choice_expired: "This auth list is no longer active";
|
|
134
134
|
readonly auth_choice_mismatch: "Auth choice does not match this message";
|
|
135
135
|
readonly auth_choice_recorded: "Auth selected";
|
|
136
|
-
readonly auth_switching: "Switching Codex auth
|
|
137
|
-
readonly auth_switch_done: "Codex auth switched
|
|
138
|
-
readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching
|
|
139
|
-
readonly auth_auto_done: "Auto-switched Codex auth
|
|
136
|
+
readonly auth_switching: "Switching Codex auth: {from} -> {to}...";
|
|
137
|
+
readonly auth_switch_done: "Codex auth switched: {from} -> {to}.";
|
|
138
|
+
readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching: {from} -> {to}...";
|
|
139
|
+
readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
|
|
140
140
|
readonly auth_auto_retrying: "Retrying the same request with the new auth...";
|
|
141
141
|
readonly auth_auto_retry_thread_missing: "Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.";
|
|
142
142
|
readonly auth_auto_no_candidate: "Codex auth problem detected ({error}), but no unused auth candidate is available.";
|
|
@@ -683,10 +683,10 @@ declare const MESSAGES: {
|
|
|
683
683
|
readonly auth_choice_expired: "这个 auth 列表已经不再有效";
|
|
684
684
|
readonly auth_choice_mismatch: "这个 auth 选择按钮不属于当前消息";
|
|
685
685
|
readonly auth_choice_recorded: "已选择 auth";
|
|
686
|
-
readonly auth_switching: "正在切换 Codex auth
|
|
687
|
-
readonly auth_switch_done: "Codex auth
|
|
688
|
-
readonly auth_auto_switching: "检测到 Codex auth 问题({error}
|
|
689
|
-
readonly auth_auto_done: "已自动切换 Codex auth
|
|
686
|
+
readonly auth_switching: "正在切换 Codex auth:{from} -> {to}...";
|
|
687
|
+
readonly auth_switch_done: "Codex auth 已切换:{from} -> {to}。";
|
|
688
|
+
readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
|
|
689
|
+
readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
|
|
690
690
|
readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
|
|
691
691
|
readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
|
|
692
692
|
readonly auth_auto_no_candidate: "检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。";
|
package/dist/i18n.js
CHANGED
|
@@ -131,10 +131,10 @@ const MESSAGES = {
|
|
|
131
131
|
auth_choice_expired: 'This auth list is no longer active',
|
|
132
132
|
auth_choice_mismatch: 'Auth choice does not match this message',
|
|
133
133
|
auth_choice_recorded: 'Auth selected',
|
|
134
|
-
auth_switching: 'Switching Codex auth
|
|
135
|
-
auth_switch_done: 'Codex auth switched
|
|
136
|
-
auth_auto_switching: 'Codex auth problem detected ({error}). Switching
|
|
137
|
-
auth_auto_done: 'Auto-switched Codex auth
|
|
134
|
+
auth_switching: 'Switching Codex auth: {from} -> {to}...',
|
|
135
|
+
auth_switch_done: 'Codex auth switched: {from} -> {to}.',
|
|
136
|
+
auth_auto_switching: 'Codex auth problem detected ({error}). Switching: {from} -> {to}...',
|
|
137
|
+
auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
|
|
138
138
|
auth_auto_retrying: 'Retrying the same request with the new auth...',
|
|
139
139
|
auth_auto_retry_thread_missing: 'Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.',
|
|
140
140
|
auth_auto_no_candidate: 'Codex auth problem detected ({error}), but no unused auth candidate is available.',
|
|
@@ -681,10 +681,10 @@ const MESSAGES = {
|
|
|
681
681
|
auth_choice_expired: '这个 auth 列表已经不再有效',
|
|
682
682
|
auth_choice_mismatch: '这个 auth 选择按钮不属于当前消息',
|
|
683
683
|
auth_choice_recorded: '已选择 auth',
|
|
684
|
-
auth_switching: '正在切换 Codex auth
|
|
685
|
-
auth_switch_done: 'Codex auth
|
|
686
|
-
auth_auto_switching: '检测到 Codex auth 问题({error}
|
|
687
|
-
auth_auto_done: '已自动切换 Codex auth
|
|
684
|
+
auth_switching: '正在切换 Codex auth:{from} -> {to}...',
|
|
685
|
+
auth_switch_done: 'Codex auth 已切换:{from} -> {to}。',
|
|
686
|
+
auth_auto_switching: '检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...',
|
|
687
|
+
auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
|
|
688
688
|
auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
|
|
689
689
|
auth_auto_retry_thread_missing: 'Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。',
|
|
690
690
|
auth_auto_no_candidate: '检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。',
|
package/dist/main.js
CHANGED
|
@@ -8,7 +8,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
|
|
9
9
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
10
10
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
11
|
-
const
|
|
11
|
+
const rawCommand = process.argv[2];
|
|
12
|
+
const command = rawCommand || 'serve';
|
|
12
13
|
loadEnv();
|
|
13
14
|
const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
14
15
|
const entryPoint = fileURLToPath(import.meta.url);
|
|
@@ -23,6 +24,14 @@ const PROXY_ENV_KEYS = [
|
|
|
23
24
|
'no_proxy',
|
|
24
25
|
];
|
|
25
26
|
async function main() {
|
|
27
|
+
if (isVersionCommand(command)) {
|
|
28
|
+
console.log(readPackageVersion());
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (isHelpCommand(command)) {
|
|
32
|
+
printUsage();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
26
35
|
if (command === 'init') {
|
|
27
36
|
await initConfig();
|
|
28
37
|
return;
|
|
@@ -73,9 +82,44 @@ async function main() {
|
|
|
73
82
|
await runWeixinLoginCli();
|
|
74
83
|
return;
|
|
75
84
|
}
|
|
85
|
+
if (command !== 'serve') {
|
|
86
|
+
console.error(`Unknown command: ${command}`);
|
|
87
|
+
printUsage();
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
76
90
|
requireNode24(command);
|
|
77
91
|
await runServeCli();
|
|
78
92
|
}
|
|
93
|
+
function isVersionCommand(value) {
|
|
94
|
+
return value === 'version' || value === '--version' || value === '-v' || value === '-V';
|
|
95
|
+
}
|
|
96
|
+
function isHelpCommand(value) {
|
|
97
|
+
return value === 'help' || value === '--help' || value === '-h';
|
|
98
|
+
}
|
|
99
|
+
function readPackageVersion() {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
102
|
+
return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version : '0.0.0';
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return '0.0.0';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function printUsage() {
|
|
109
|
+
console.log(`FoxClaw ${readPackageVersion()}
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
foxclaw [serve]
|
|
113
|
+
foxclaw init
|
|
114
|
+
foxclaw doctor
|
|
115
|
+
foxclaw status
|
|
116
|
+
foxclaw start|restart|stop
|
|
117
|
+
foxclaw install-systemd|uninstall-systemd
|
|
118
|
+
foxclaw install-launchd
|
|
119
|
+
foxclaw weixin-login [account-id]
|
|
120
|
+
foxclaw --version
|
|
121
|
+
foxclaw --help`);
|
|
122
|
+
}
|
|
79
123
|
async function runServeCli() {
|
|
80
124
|
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter },] = await Promise.all([
|
|
81
125
|
import('./channels/bridge_messaging_router.js'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxden-app/foxclaw",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"install-launchd": "node dist/main.js install-launchd",
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
51
|
"lint": "eslint .",
|
|
52
|
-
"test": "node --test --import tsx
|
|
52
|
+
"test": "node --test --import tsx $(find src -name '*.test.ts' -print)",
|
|
53
53
|
"prepack": "npm run build"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: foxclaw
|
|
3
|
-
description: Deploy, configure,
|
|
2
|
+
name: foxclaw
|
|
3
|
+
description: Deploy, configure, validate, develop, and release FoxClaw. Use when Codex needs to clone or update the FoxClaw repo, collect Telegram values, write `.env`, enable launchd/systemd, guide first-message tests, or perform FoxClaw repo wrap-up actions such as Chinese commit messages, push, npm publish, and local install/service update.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# FoxClaw
|
|
@@ -158,10 +158,47 @@ Do this whenever the bridge has been started:
|
|
|
158
158
|
- privacy mode still on
|
|
159
159
|
- bot not admin in group
|
|
160
160
|
|
|
161
|
-
Do not describe the setup as "done" until this smoke test has either passed or been blocked by missing Telegram-side access.
|
|
162
|
-
|
|
163
|
-
##
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
Do not describe the setup as "done" until this smoke test has either passed or been blocked by missing Telegram-side access.
|
|
162
|
+
|
|
163
|
+
## Development Wrap-Up
|
|
164
|
+
|
|
165
|
+
Use this checklist when the user asks for standard closing actions, release wrap-up, local install updates, npm publish, or says things like "收尾动作", "中文 commit msg", "push", "npm publish", or "本地安装更新".
|
|
166
|
+
|
|
167
|
+
1. Inspect scope before staging:
|
|
168
|
+
- `git status -sb`
|
|
169
|
+
- `git diff --stat`
|
|
170
|
+
- `git diff --name-status`
|
|
171
|
+
2. Run the relevant verification with Node 24+. If the system `node` is older, prepend the known Node 24 bin path or use the repo's documented Node 24 shell.
|
|
172
|
+
- Code changes: `npm run typecheck`, `npm run lint`, `npm test`
|
|
173
|
+
- Package/release changes: also run `npm pack --dry-run`
|
|
174
|
+
- Skill-only changes: validate the skill folder, then run `npm pack --dry-run`
|
|
175
|
+
3. Build before any local service restart:
|
|
176
|
+
- `npm run build`
|
|
177
|
+
4. Commit intentionally:
|
|
178
|
+
- Stage only the changed files that belong to the task.
|
|
179
|
+
- Use a Chinese commit message when the user asked in Chinese or explicitly said "中文 commit msg".
|
|
180
|
+
- Never stage unrelated local changes.
|
|
181
|
+
5. Push the current branch after a successful commit:
|
|
182
|
+
- `git push origin <branch>`
|
|
183
|
+
6. Refresh the local install when requested:
|
|
184
|
+
- If the user has a pnpm global FoxClaw install, prefer `pnpm add -g <repo-path>` so the global `foxclaw` points at the local repo.
|
|
185
|
+
- Rebuild before restarting because local linked installs run `dist/main.js`.
|
|
186
|
+
- Refresh systemd with the existing service env path, for example `FOXCLAW_ENV=<existing-env> <node24> dist/main.js install-systemd`. Do not run `install-systemd` from the repo without `FOXCLAW_ENV`, because it may rewrite the service to use the repo `.env`.
|
|
187
|
+
- For macOS launchd, use the launchd install/start path from this skill and verify with `node dist/main.js status`.
|
|
188
|
+
- Verify the running service reports the expected FoxClaw version in `status`.
|
|
189
|
+
- If `doctor` fails only because `DEFAULT_CWD` is missing, report that separately; do not treat it as evidence that the service update failed.
|
|
190
|
+
7. Publish to npm when requested:
|
|
191
|
+
- Prefer GitHub Actions trusted publishing via `.github/workflows/publish.yml`: bump and commit the package version, push `main`, then push a matching `v<version>` tag. The tag version must match `package.json`.
|
|
192
|
+
- Configure npmjs.com trusted publishing for `foxden-app/foxclaw` and workflow `publish.yml`; do not store npm tokens if OIDC trusted publishing is available.
|
|
193
|
+
- Temporary fallback: store an npm automation/bypass-2FA token as the GitHub Actions secret `NPM_TOKEN`. Never print the token or commit it.
|
|
194
|
+
- Check `npm whoami` and `npm view @foxden-app/foxclaw version`.
|
|
195
|
+
- If the target version is already published, bump with `npm version patch --no-git-tag-version` before validation and commit.
|
|
196
|
+
- Manual fallback only: run `BROWSER=true npm publish` in a TTY.
|
|
197
|
+
- If npm returns `ENEEDAUTH`, report that npm publish is blocked by registry login and leave the package unreported as published.
|
|
198
|
+
- Never print npm tokens or `.npmrc` auth values.
|
|
199
|
+
|
|
200
|
+
## Resources
|
|
201
|
+
|
|
202
|
+
- [references/telegram-setup.md](./references/telegram-setup.md): Telegram-side checklist and ID discovery
|
|
166
203
|
- `scripts/bootstrap_host.py`: install and configure the bridge on the current Mac
|
|
167
204
|
- `scripts/bootstrap_remote.py`: run the same bootstrap on another Mac over SSH
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "FoxClaw"
|
|
3
|
-
short_description: "Install,
|
|
4
|
-
default_prompt: "Use $foxclaw to clone
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "FoxClaw"
|
|
3
|
+
short_description: "Install, validate, update, and release FoxClaw bridges"
|
|
4
|
+
default_prompt: "Use $foxclaw to clone or update FoxClaw, configure Telegram values, validate service health, or finish repo changes with commit, push, npm publish, and local install refresh."
|
|
@@ -5,7 +5,7 @@ description: Publish npm packages safely, especially packages that require npm 2
|
|
|
5
5
|
|
|
6
6
|
# NPM Publish
|
|
7
7
|
|
|
8
|
-
Use this skill to publish an npm package from a repo. Prefer the normal no-2FA publish path first. Fall back to npm web-auth only when npm explicitly prompts for it.
|
|
8
|
+
Use this skill to publish an npm package from a repo. Prefer CI trusted publishing when the repository has a publish workflow. Prefer the normal no-2FA publish path first only when CI publishing is not available. Fall back to npm web-auth only when npm explicitly prompts for it.
|
|
9
9
|
|
|
10
10
|
## Release Checklist
|
|
11
11
|
|
|
@@ -43,6 +43,40 @@ Use this skill to publish an npm package from a repo. Prefer the normal no-2FA p
|
|
|
43
43
|
|
|
44
44
|
## Publish Flow
|
|
45
45
|
|
|
46
|
+
### GitHub Actions Trusted Publishing
|
|
47
|
+
|
|
48
|
+
Use this path when the repo has `.github/workflows/publish.yml` and npmjs.com has a trusted publisher configured for the package.
|
|
49
|
+
|
|
50
|
+
1. Verify the package version is not already published:
|
|
51
|
+
```bash
|
|
52
|
+
npm view <package-name> version
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
2. If needed, bump the package version and commit it before tagging:
|
|
56
|
+
```bash
|
|
57
|
+
npm version patch --no-git-tag-version
|
|
58
|
+
git add package.json package-lock.json
|
|
59
|
+
git commit -m "<Chinese release message when appropriate>"
|
|
60
|
+
git push origin <branch>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
3. Publish by pushing a matching version tag:
|
|
64
|
+
```bash
|
|
65
|
+
git tag v<package-version>
|
|
66
|
+
git push origin v<package-version>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
4. Watch the GitHub Actions `Publish` workflow. It must run lint, typecheck, tests, `npm pack --dry-run`, then `npm publish --access public` through trusted publishing.
|
|
70
|
+
|
|
71
|
+
5. Verify the registry after the workflow succeeds:
|
|
72
|
+
```bash
|
|
73
|
+
npm view <package-name> version
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Do not store or print npm tokens when trusted publishing is available. If trusted publishing is not configured on npmjs.com, the workflow can use a GitHub Actions secret named `NPM_TOKEN` as a temporary fallback. Store only automation/bypass-2FA tokens there, never paste tokens in chat or commit them.
|
|
77
|
+
|
|
78
|
+
### Manual Publish
|
|
79
|
+
|
|
46
80
|
1. Start publish in a TTY and disable local browser opening. This works both when npm publishes directly and when it asks for web auth:
|
|
47
81
|
```bash
|
|
48
82
|
BROWSER=true npm publish
|