@foxden-app/foxclaw 0.3.18 → 0.3.19
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/.env.example +5 -2
- package/README.md +14 -4
- package/README_EN.md +14 -4
- package/dist/auth/mirror.d.ts +25 -0
- package/dist/auth/mirror.js +199 -0
- package/dist/codex_app/client.d.ts +2 -1
- package/dist/codex_app/client.js +10 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +18 -1
- package/dist/controller/controller.d.ts +13 -1
- package/dist/controller/controller.js +95 -25
- package/dist/core/bridge_scope.d.ts +5 -2
- package/dist/core/bridge_scope.js +7 -3
- package/dist/main.js +144 -9
- package/dist/store/database.d.ts +4 -2
- package/dist/store/database.js +42 -6
- package/dist/telegram/addressing.d.ts +1 -0
- package/dist/telegram/addressing.js +3 -0
- package/dist/telegram/gateway.d.ts +4 -1
- package/dist/telegram/gateway.js +23 -5
- package/dist/types.d.ts +7 -0
- package/dist/update.d.ts +5 -0
- package/dist/update.js +93 -5
- package/docs/agent-assisted-install.md +4 -4
- package/docs/install-for-beginners.md +3 -3
- package/docs/troubleshooting.md +1 -1
- package/docs/user-manual.md +10 -10
- package/docs/zh/agent-assisted-install.md +4 -4
- package/docs/zh/foxclaw-skill.md +1 -1
- package/docs/zh/install-for-beginners.md +3 -3
- package/docs/zh/troubleshooting.md +1 -1
- package/docs/zh/user-manual.md +10 -10
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +24 -19
- package/skills/foxclaw/references/telegram-setup.md +5 -4
- package/skills/foxclaw/scripts/bootstrap_host.py +11 -8
- package/skills/foxclaw/scripts/bootstrap_remote.py +8 -4
package/dist/store/database.js
CHANGED
|
@@ -8,10 +8,16 @@ export class BridgeStore {
|
|
|
8
8
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
9
9
|
this.db = new DatabaseSync(dbPath);
|
|
10
10
|
this.db.exec(`
|
|
11
|
-
CREATE TABLE IF NOT EXISTS telegram_offsets (
|
|
12
|
-
bot_key TEXT PRIMARY KEY,
|
|
13
|
-
update_id INTEGER NOT NULL
|
|
14
|
-
);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS telegram_offsets (
|
|
12
|
+
bot_key TEXT PRIMARY KEY,
|
|
13
|
+
update_id INTEGER NOT NULL
|
|
14
|
+
);
|
|
15
|
+
CREATE TABLE IF NOT EXISTS telegram_private_scopes (
|
|
16
|
+
bot_id TEXT PRIMARY KEY,
|
|
17
|
+
scope_id TEXT NOT NULL,
|
|
18
|
+
chat_id TEXT NOT NULL,
|
|
19
|
+
updated_at INTEGER NOT NULL
|
|
20
|
+
);
|
|
15
21
|
CREATE TABLE IF NOT EXISTS chat_bindings (
|
|
16
22
|
chat_id TEXT PRIMARY KEY,
|
|
17
23
|
thread_id TEXT NOT NULL,
|
|
@@ -110,6 +116,13 @@ export class BridgeStore {
|
|
|
110
116
|
disabled INTEGER NOT NULL DEFAULT 0,
|
|
111
117
|
updated_at INTEGER NOT NULL
|
|
112
118
|
);
|
|
119
|
+
CREATE TABLE IF NOT EXISTS codex_auth_candidate_runtime (
|
|
120
|
+
runtime_id TEXT NOT NULL,
|
|
121
|
+
name TEXT NOT NULL,
|
|
122
|
+
disabled INTEGER NOT NULL DEFAULT 0,
|
|
123
|
+
updated_at INTEGER NOT NULL,
|
|
124
|
+
PRIMARY KEY (runtime_id, name)
|
|
125
|
+
);
|
|
113
126
|
`);
|
|
114
127
|
this.ensureColumn('thread_cache', 'name', 'TEXT');
|
|
115
128
|
this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
|
|
@@ -136,6 +149,17 @@ export class BridgeStore {
|
|
|
136
149
|
ON CONFLICT(bot_key) DO UPDATE SET update_id = excluded.update_id
|
|
137
150
|
`).run(botKey, updateId);
|
|
138
151
|
}
|
|
152
|
+
rememberTelegramPrivateScope(botId, scopeId, chatId) {
|
|
153
|
+
this.db.prepare(`
|
|
154
|
+
INSERT INTO telegram_private_scopes (bot_id, scope_id, chat_id, updated_at)
|
|
155
|
+
VALUES (?, ?, ?, ?)
|
|
156
|
+
ON CONFLICT(bot_id) DO UPDATE SET scope_id = excluded.scope_id, chat_id = excluded.chat_id, updated_at = excluded.updated_at
|
|
157
|
+
`).run(botId, scopeId, chatId, Date.now());
|
|
158
|
+
}
|
|
159
|
+
getTelegramPrivateChatId(botId) {
|
|
160
|
+
const row = this.db.prepare('SELECT chat_id FROM telegram_private_scopes WHERE bot_id = ?').get(botId);
|
|
161
|
+
return row ? String(row.chat_id) : null;
|
|
162
|
+
}
|
|
139
163
|
getBinding(chatId) {
|
|
140
164
|
const row = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(chatId);
|
|
141
165
|
if (!row)
|
|
@@ -462,11 +486,23 @@ export class BridgeStore {
|
|
|
462
486
|
ON CONFLICT(scope_id) DO UPDATE SET context_token = excluded.context_token, updated_at = excluded.updated_at
|
|
463
487
|
`).run(scopeId, contextToken, Date.now());
|
|
464
488
|
}
|
|
465
|
-
listDisabledCodexAuthCandidateNames() {
|
|
489
|
+
listDisabledCodexAuthCandidateNames(runtimeId = 'default') {
|
|
490
|
+
if (runtimeId !== 'default') {
|
|
491
|
+
const runtimeRows = this.db.prepare('SELECT name FROM codex_auth_candidate_runtime WHERE runtime_id = ? AND disabled = 1').all(runtimeId);
|
|
492
|
+
return new Set(runtimeRows.map(row => String(row.name)));
|
|
493
|
+
}
|
|
466
494
|
const rows = this.db.prepare('SELECT name FROM codex_auth_candidates WHERE disabled = 1').all();
|
|
467
495
|
return new Set(rows.map(row => String(row.name)));
|
|
468
496
|
}
|
|
469
|
-
setCodexAuthCandidateDisabled(name, disabled) {
|
|
497
|
+
setCodexAuthCandidateDisabled(name, disabled, runtimeId = 'default') {
|
|
498
|
+
if (runtimeId !== 'default') {
|
|
499
|
+
this.db.prepare(`
|
|
500
|
+
INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, updated_at)
|
|
501
|
+
VALUES (?, ?, ?, ?)
|
|
502
|
+
ON CONFLICT(runtime_id, name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
503
|
+
`).run(runtimeId, name, disabled ? 1 : 0, Date.now());
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
470
506
|
this.db.prepare(`
|
|
471
507
|
INSERT INTO codex_auth_candidates (name, disabled, updated_at)
|
|
472
508
|
VALUES (?, ?, ?)
|
|
@@ -27,6 +27,7 @@ interface DefaultScopeParams {
|
|
|
27
27
|
allowedChatId: string | null;
|
|
28
28
|
allowedTopicId: number | null;
|
|
29
29
|
topicId: number | null;
|
|
30
|
+
requireExplicitGroupAddressing?: boolean;
|
|
30
31
|
}
|
|
31
32
|
export declare function resolveTelegramAddressing(params: ResolveTelegramAddressingParams): TelegramAddressingDecision;
|
|
32
33
|
export declare function isDefaultTelegramScope(params: DefaultScopeParams): boolean;
|
|
@@ -34,12 +34,15 @@ export declare class TelegramGateway extends EventEmitter {
|
|
|
34
34
|
private readonly pollIntervalMs;
|
|
35
35
|
private readonly store;
|
|
36
36
|
private readonly logger;
|
|
37
|
+
private readonly namespacedScopes;
|
|
37
38
|
private running;
|
|
38
39
|
private botKey;
|
|
39
40
|
private botUsername;
|
|
40
41
|
private botUserId;
|
|
41
|
-
constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger);
|
|
42
|
+
constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger, namespacedScopes?: boolean);
|
|
42
43
|
get username(): string | null;
|
|
44
|
+
get identity(): string | null;
|
|
45
|
+
initializeIdentity(): Promise<string>;
|
|
43
46
|
start(): Promise<void>;
|
|
44
47
|
stop(): void;
|
|
45
48
|
sendMessage(chatId: string, text: string, inlineKeyboard?: Array<Array<{
|
package/dist/telegram/gateway.js
CHANGED
|
@@ -11,11 +11,12 @@ export class TelegramGateway extends EventEmitter {
|
|
|
11
11
|
pollIntervalMs;
|
|
12
12
|
store;
|
|
13
13
|
logger;
|
|
14
|
+
namespacedScopes;
|
|
14
15
|
running = false;
|
|
15
16
|
botKey;
|
|
16
17
|
botUsername = null;
|
|
17
18
|
botUserId = null;
|
|
18
|
-
constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger) {
|
|
19
|
+
constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger, namespacedScopes = false) {
|
|
19
20
|
super();
|
|
20
21
|
this.botToken = botToken;
|
|
21
22
|
this.allowedUserId = allowedUserId;
|
|
@@ -23,16 +24,26 @@ export class TelegramGateway extends EventEmitter {
|
|
|
23
24
|
this.pollIntervalMs = pollIntervalMs;
|
|
24
25
|
this.store = store;
|
|
25
26
|
this.logger = logger;
|
|
27
|
+
this.namespacedScopes = namespacedScopes;
|
|
26
28
|
this.botKey = `telegram:${crypto.createHash('sha256').update(this.botToken).digest('hex').slice(0, 8)}`;
|
|
27
29
|
}
|
|
28
30
|
get username() {
|
|
29
31
|
return this.botUsername;
|
|
30
32
|
}
|
|
33
|
+
get identity() {
|
|
34
|
+
return this.botUserId === null ? null : `bot${this.botUserId}`;
|
|
35
|
+
}
|
|
36
|
+
async initializeIdentity() {
|
|
37
|
+
await this.resolveBotIdentity(true);
|
|
38
|
+
return this.identity;
|
|
39
|
+
}
|
|
31
40
|
async start() {
|
|
32
41
|
if (this.running)
|
|
33
42
|
return;
|
|
34
43
|
this.running = true;
|
|
35
|
-
|
|
44
|
+
if (this.botUserId === null) {
|
|
45
|
+
await this.resolveBotIdentity(this.namespacedScopes);
|
|
46
|
+
}
|
|
36
47
|
await this.registerCommands();
|
|
37
48
|
void this.pollLoop();
|
|
38
49
|
}
|
|
@@ -134,12 +145,16 @@ export class TelegramGateway extends EventEmitter {
|
|
|
134
145
|
async downloadResolvedFile(remoteFilePath, destinationPath) {
|
|
135
146
|
return downloadTelegramFile(this.botToken, remoteFilePath, destinationPath);
|
|
136
147
|
}
|
|
137
|
-
async resolveBotIdentity() {
|
|
148
|
+
async resolveBotIdentity(required = false) {
|
|
138
149
|
const result = await callTelegramApi(this.botToken, 'getMe', {});
|
|
139
150
|
if (result.ok && result.result) {
|
|
140
151
|
this.botKey = `telegram:bot${result.result.id}`;
|
|
141
152
|
this.botUserId = result.result.id;
|
|
142
153
|
this.botUsername = result.result.username ?? null;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (required) {
|
|
157
|
+
throw new Error(result.description || 'Failed to resolve Telegram bot identity');
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
160
|
async registerCommands() {
|
|
@@ -187,7 +202,7 @@ export class TelegramGateway extends EventEmitter {
|
|
|
187
202
|
const attachments = extractAttachments(update.message);
|
|
188
203
|
const text = update.message.text ?? update.message.caption ?? '';
|
|
189
204
|
const topicId = update.message.message_thread_id ?? null;
|
|
190
|
-
const scopeId = toTelegramBridgeScopeId(createTelegramScopeId(String(update.message.chat.id), topicId));
|
|
205
|
+
const scopeId = toTelegramBridgeScopeId(createTelegramScopeId(String(update.message.chat.id), topicId), this.namespacedScopes ? this.identity : null);
|
|
191
206
|
const entities = update.message.text ? (update.message.entities ?? []) : (update.message.caption_entities ?? []);
|
|
192
207
|
const replyToBot = this.botUserId !== null && update.message.reply_to_message?.from?.id === this.botUserId;
|
|
193
208
|
if (text || attachments.length > 0) {
|
|
@@ -204,6 +219,9 @@ export class TelegramGateway extends EventEmitter {
|
|
|
204
219
|
replyToBot,
|
|
205
220
|
...(update.message.from.language_code ? { languageCode: update.message.from.language_code } : {}),
|
|
206
221
|
});
|
|
222
|
+
if (update.message.chat.type === 'private' && this.identity) {
|
|
223
|
+
this.store.rememberTelegramPrivateScope(this.identity, scopeId, String(update.message.chat.id));
|
|
224
|
+
}
|
|
207
225
|
return;
|
|
208
226
|
}
|
|
209
227
|
}
|
|
@@ -216,7 +234,7 @@ export class TelegramGateway extends EventEmitter {
|
|
|
216
234
|
this.emit('callback', {
|
|
217
235
|
chatId: String(update.callback_query.message.chat.id),
|
|
218
236
|
topicId,
|
|
219
|
-
scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId)),
|
|
237
|
+
scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId), this.namespacedScopes ? this.identity : null),
|
|
220
238
|
userId: String(update.callback_query.from.id),
|
|
221
239
|
data: update.callback_query.data,
|
|
222
240
|
callbackQueryId: update.callback_query.id,
|
package/dist/types.d.ts
CHANGED
|
@@ -340,4 +340,11 @@ export interface RuntimeStatus {
|
|
|
340
340
|
telegram: boolean;
|
|
341
341
|
weixin: boolean;
|
|
342
342
|
};
|
|
343
|
+
bots?: Array<{
|
|
344
|
+
id: string;
|
|
345
|
+
username: string | null;
|
|
346
|
+
connected: boolean;
|
|
347
|
+
activeTurns: number;
|
|
348
|
+
codexAppServer?: RuntimeStatus['codexAppServer'];
|
|
349
|
+
}>;
|
|
343
350
|
}
|
package/dist/update.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface SelfUpdateStatus {
|
|
|
6
6
|
locale: AppLocale;
|
|
7
7
|
fromVersion: string;
|
|
8
8
|
toVersion: string | null;
|
|
9
|
+
codexUpdate?: string | null;
|
|
9
10
|
error: string | null;
|
|
10
11
|
updatedAt: string;
|
|
11
12
|
}
|
|
@@ -19,6 +20,7 @@ export interface SelfUpdateInstaller {
|
|
|
19
20
|
command: string;
|
|
20
21
|
installArgs: string[];
|
|
21
22
|
rootArgs: string[];
|
|
23
|
+
pnpmHome?: string;
|
|
22
24
|
}
|
|
23
25
|
interface CreateSelfUpdateRuntimeOptions {
|
|
24
26
|
entryPoint: string;
|
|
@@ -26,12 +28,14 @@ interface CreateSelfUpdateRuntimeOptions {
|
|
|
26
28
|
version: string;
|
|
27
29
|
statusPath: string;
|
|
28
30
|
logPath: string;
|
|
31
|
+
codexCliBin?: string;
|
|
29
32
|
}
|
|
30
33
|
interface PerformSelfUpdateOptions {
|
|
31
34
|
entryPoint: string;
|
|
32
35
|
nodePath: string;
|
|
33
36
|
version: string;
|
|
34
37
|
notificationFile?: string;
|
|
38
|
+
codexCliBin?: string;
|
|
35
39
|
env?: NodeJS.ProcessEnv;
|
|
36
40
|
}
|
|
37
41
|
export interface SelfUpdateOutcome {
|
|
@@ -43,6 +47,7 @@ export interface SelfUpdateOutcome {
|
|
|
43
47
|
export declare function selfUpdateStatusPath(statusPath: string): string;
|
|
44
48
|
export declare function inferPnpmHomeFromEntryPoint(entryPoint: string): string | null;
|
|
45
49
|
export declare function resolveSelfUpdateInstaller(entryPoint: string, nodePath?: string, exists?: (target: string) => boolean, env?: NodeJS.ProcessEnv): SelfUpdateInstaller;
|
|
50
|
+
export declare function resolveCodexUpdateInstaller(codexCliBin: string, nodePath?: string, exists?: (target: string) => boolean, env?: NodeJS.ProcessEnv, realpath?: (target: string) => string, readText?: (target: string) => string): SelfUpdateInstaller | null;
|
|
46
51
|
export declare function readSelfUpdateStatus(statusFile: string): SelfUpdateStatus | null;
|
|
47
52
|
export declare function writeSelfUpdateStatus(statusFile: string, status: SelfUpdateStatus): void;
|
|
48
53
|
export declare function createSelfUpdateRuntime(options: CreateSelfUpdateRuntimeOptions): SelfUpdateRuntime;
|
package/dist/update.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
5
|
const PACKAGE_SPEC = '@foxden-app/foxclaw@latest';
|
|
6
|
+
const CODEX_PACKAGE_SPEC = '@openai/codex@latest';
|
|
6
7
|
const UPDATE_STATUS_FILENAME = 'self-update.json';
|
|
7
8
|
export function selfUpdateStatusPath(statusPath) {
|
|
8
9
|
return path.join(path.dirname(statusPath), UPDATE_STATUS_FILENAME);
|
|
@@ -33,6 +34,7 @@ export function resolveSelfUpdateInstaller(entryPoint, nodePath = process.execPa
|
|
|
33
34
|
command: pnpmCommand,
|
|
34
35
|
installArgs: ['add', '--global', PACKAGE_SPEC],
|
|
35
36
|
rootArgs: ['root', '--global'],
|
|
37
|
+
pnpmHome,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
const npmCommandName = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
@@ -43,6 +45,7 @@ export function resolveSelfUpdateInstaller(entryPoint, nodePath = process.execPa
|
|
|
43
45
|
command: npmCommand,
|
|
44
46
|
installArgs: ['exec', '--yes', '--package=pnpm@latest', '--', 'pnpm', 'add', '--global', PACKAGE_SPEC],
|
|
45
47
|
rootArgs: ['exec', '--yes', '--package=pnpm@latest', '--', 'pnpm', 'root', '--global'],
|
|
48
|
+
pnpmHome,
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
const adjacentNpm = path.join(path.dirname(nodePath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
@@ -53,6 +56,65 @@ export function resolveSelfUpdateInstaller(entryPoint, nodePath = process.execPa
|
|
|
53
56
|
rootArgs: ['root', '--global'],
|
|
54
57
|
};
|
|
55
58
|
}
|
|
59
|
+
export function resolveCodexUpdateInstaller(codexCliBin, nodePath = process.execPath, exists = fs.existsSync, env = process.env, realpath = fs.realpathSync, readText = (target) => fs.readFileSync(target, 'utf8')) {
|
|
60
|
+
const resolved = resolveManagedCodexEntryPoint(codexCliBin, realpath, readText);
|
|
61
|
+
if (!resolved)
|
|
62
|
+
return null;
|
|
63
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
64
|
+
const pnpmManaged = normalized.includes('/global/') && normalized.includes('/.pnpm/@openai+codex@');
|
|
65
|
+
const npmManaged = normalized.includes('/lib/node_modules/@openai/codex/')
|
|
66
|
+
|| normalized.includes('/npm/node_modules/@openai/codex/');
|
|
67
|
+
if (!pnpmManaged && !npmManaged) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const installer = resolveSelfUpdateInstaller(resolved, nodePath, exists, env);
|
|
71
|
+
return {
|
|
72
|
+
...installer,
|
|
73
|
+
installArgs: installer.installArgs.map((argument) => (argument === PACKAGE_SPEC ? CODEX_PACKAGE_SPEC : argument)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function resolveManagedCodexEntryPoint(codexCliBin, realpath, readText) {
|
|
77
|
+
const pending = [codexCliBin];
|
|
78
|
+
const visited = new Set();
|
|
79
|
+
while (pending.length > 0 && visited.size < 4) {
|
|
80
|
+
const candidate = pending.shift();
|
|
81
|
+
let resolved = candidate;
|
|
82
|
+
try {
|
|
83
|
+
resolved = realpath(candidate);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Wrapper inspection below can still reveal its managed target.
|
|
87
|
+
}
|
|
88
|
+
if (visited.has(resolved))
|
|
89
|
+
continue;
|
|
90
|
+
visited.add(resolved);
|
|
91
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
92
|
+
if (isManagedCodexPackagePath(normalized)) {
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
95
|
+
let contents = '';
|
|
96
|
+
try {
|
|
97
|
+
contents = readText(resolved);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const embeddedPackageRoot = contents.match(/\/[^\s"']*\/global\/[^\s"']*\/\.pnpm\/@openai\+codex@[^\s"']*\/node_modules\/@openai\/codex/)?.[0];
|
|
103
|
+
if (embeddedPackageRoot) {
|
|
104
|
+
return path.join(embeddedPackageRoot, 'bin', 'codex.js');
|
|
105
|
+
}
|
|
106
|
+
const wrappedExecutable = contents.match(/\bexec\s+"([^"]*codex[^"]*)"/)?.[1];
|
|
107
|
+
if (wrappedExecutable) {
|
|
108
|
+
pending.push(wrappedExecutable);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function isManagedCodexPackagePath(normalized) {
|
|
114
|
+
return (normalized.includes('/global/') && normalized.includes('/.pnpm/@openai+codex@'))
|
|
115
|
+
|| normalized.includes('/lib/node_modules/@openai/codex/')
|
|
116
|
+
|| normalized.includes('/npm/node_modules/@openai/codex/');
|
|
117
|
+
}
|
|
56
118
|
export function readSelfUpdateStatus(statusFile) {
|
|
57
119
|
try {
|
|
58
120
|
const parsed = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
@@ -69,6 +131,7 @@ export function readSelfUpdateStatus(statusFile) {
|
|
|
69
131
|
locale: parsed.locale,
|
|
70
132
|
fromVersion: parsed.fromVersion,
|
|
71
133
|
toVersion: typeof parsed.toVersion === 'string' ? parsed.toVersion : null,
|
|
134
|
+
...(typeof parsed.codexUpdate === 'string' ? { codexUpdate: parsed.codexUpdate } : {}),
|
|
72
135
|
error: typeof parsed.error === 'string' ? parsed.error : null,
|
|
73
136
|
updatedAt: parsed.updatedAt,
|
|
74
137
|
};
|
|
@@ -97,6 +160,7 @@ export function createSelfUpdateRuntime(options) {
|
|
|
97
160
|
locale,
|
|
98
161
|
fromVersion: options.version,
|
|
99
162
|
toVersion: null,
|
|
163
|
+
codexUpdate: null,
|
|
100
164
|
error: null,
|
|
101
165
|
updatedAt: new Date().toISOString(),
|
|
102
166
|
});
|
|
@@ -106,7 +170,7 @@ export function createSelfUpdateRuntime(options) {
|
|
|
106
170
|
const child = spawn(options.nodePath, [options.entryPoint, 'update', '--notification-file', statusFile], {
|
|
107
171
|
detached: true,
|
|
108
172
|
stdio: ['ignore', logFd, logFd],
|
|
109
|
-
env: process.env,
|
|
173
|
+
env: options.codexCliBin ? { ...process.env, CODEX_CLI_BIN: options.codexCliBin } : process.env,
|
|
110
174
|
});
|
|
111
175
|
child.unref();
|
|
112
176
|
}
|
|
@@ -117,6 +181,7 @@ export function createSelfUpdateRuntime(options) {
|
|
|
117
181
|
locale,
|
|
118
182
|
fromVersion: options.version,
|
|
119
183
|
toVersion: null,
|
|
184
|
+
codexUpdate: null,
|
|
120
185
|
error: formatError(error),
|
|
121
186
|
updatedAt: new Date().toISOString(),
|
|
122
187
|
});
|
|
@@ -137,7 +202,10 @@ export function createSelfUpdateRuntime(options) {
|
|
|
137
202
|
export function performSelfUpdate(options) {
|
|
138
203
|
const env = options.env ?? process.env;
|
|
139
204
|
let toVersion = null;
|
|
205
|
+
let codexUpdate = null;
|
|
140
206
|
try {
|
|
207
|
+
codexUpdate = updateManagedCodexCli(options.codexCliBin ?? env.CODEX_CLI_BIN ?? '', options.nodePath, env);
|
|
208
|
+
console.log(`[UPDATE] ${codexUpdate}`);
|
|
141
209
|
const installer = resolveSelfUpdateInstaller(options.entryPoint, options.nodePath, fs.existsSync, env);
|
|
142
210
|
const installerEnv = buildInstallerEnv(options.entryPoint, installer, env);
|
|
143
211
|
console.log(`[UPDATE] Installing ${PACKAGE_SPEC} with ${installer.manager}...`);
|
|
@@ -146,7 +214,7 @@ export function performSelfUpdate(options) {
|
|
|
146
214
|
toVersion = readInstalledPackageVersion(updatedEntryPoint);
|
|
147
215
|
console.log('[UPDATE] Running checks and restarting the FoxClaw service...');
|
|
148
216
|
runInherited(options.nodePath, [updatedEntryPoint, 'start'], installerEnv);
|
|
149
|
-
completeNotification(options.notificationFile, 'succeeded', toVersion, null);
|
|
217
|
+
completeNotification(options.notificationFile, 'succeeded', toVersion, codexUpdate, null);
|
|
150
218
|
console.log(`[OK] FoxClaw updated and restarted: ${options.version} -> ${toVersion}`);
|
|
151
219
|
return {
|
|
152
220
|
ok: true,
|
|
@@ -157,7 +225,7 @@ export function performSelfUpdate(options) {
|
|
|
157
225
|
}
|
|
158
226
|
catch (error) {
|
|
159
227
|
const message = formatError(error);
|
|
160
|
-
completeNotification(options.notificationFile, 'failed', toVersion, message);
|
|
228
|
+
completeNotification(options.notificationFile, 'failed', toVersion, codexUpdate, message);
|
|
161
229
|
console.error(`[FAIL] FoxClaw update failed: ${message}`);
|
|
162
230
|
return {
|
|
163
231
|
ok: false,
|
|
@@ -167,6 +235,23 @@ export function performSelfUpdate(options) {
|
|
|
167
235
|
};
|
|
168
236
|
}
|
|
169
237
|
}
|
|
238
|
+
function updateManagedCodexCli(codexCliBin, nodePath, env) {
|
|
239
|
+
if (!codexCliBin) {
|
|
240
|
+
return 'Codex CLI update skipped: CODEX_CLI_BIN is not configured.';
|
|
241
|
+
}
|
|
242
|
+
const installer = resolveCodexUpdateInstaller(codexCliBin, nodePath, fs.existsSync, env);
|
|
243
|
+
if (!installer) {
|
|
244
|
+
return 'Codex CLI update skipped: configured installation is not a recognized global npm/pnpm package.';
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const installerEnv = buildInstallerEnv(fs.realpathSync(codexCliBin), installer, env);
|
|
248
|
+
runInherited(installer.command, installer.installArgs, installerEnv);
|
|
249
|
+
return `Codex CLI updated with ${installer.manager}.`;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
return `Codex CLI update failed without blocking FoxClaw update: ${formatError(error)}`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
170
255
|
function executableCandidates(commandName, nodePath, env, preferred = []) {
|
|
171
256
|
return [
|
|
172
257
|
...preferred,
|
|
@@ -175,7 +260,9 @@ function executableCandidates(commandName, nodePath, env, preferred = []) {
|
|
|
175
260
|
].filter((candidate, index, all) => candidate && all.indexOf(candidate) === index);
|
|
176
261
|
}
|
|
177
262
|
function buildInstallerEnv(entryPoint, installer, env) {
|
|
178
|
-
const pnpmHome = installer.manager === 'pnpm'
|
|
263
|
+
const pnpmHome = installer.manager === 'pnpm'
|
|
264
|
+
? installer.pnpmHome ?? inferPnpmHomeFromEntryPoint(entryPoint)
|
|
265
|
+
: null;
|
|
179
266
|
if (!pnpmHome) {
|
|
180
267
|
return env;
|
|
181
268
|
}
|
|
@@ -230,7 +317,7 @@ function readInstalledPackageVersion(updatedEntryPoint) {
|
|
|
230
317
|
return 'unknown';
|
|
231
318
|
}
|
|
232
319
|
}
|
|
233
|
-
function completeNotification(notificationFile, state, toVersion, error) {
|
|
320
|
+
function completeNotification(notificationFile, state, toVersion, codexUpdate, error) {
|
|
234
321
|
if (!notificationFile) {
|
|
235
322
|
return;
|
|
236
323
|
}
|
|
@@ -242,6 +329,7 @@ function completeNotification(notificationFile, state, toVersion, error) {
|
|
|
242
329
|
...pending,
|
|
243
330
|
state,
|
|
244
331
|
toVersion,
|
|
332
|
+
codexUpdate,
|
|
245
333
|
error,
|
|
246
334
|
updatedAt: new Date().toISOString(),
|
|
247
335
|
});
|
|
@@ -8,7 +8,7 @@ This works well with Codex, OpenClaw, QwenPaw, Hermes, OpenCode, Kimi CLI, or an
|
|
|
8
8
|
|
|
9
9
|
Prepare these values before asking the agent:
|
|
10
10
|
|
|
11
|
-
- `
|
|
11
|
+
- `TG_BOT_TOKENS`: one or more comma-separated Telegram bot tokens from `@BotFather`
|
|
12
12
|
- `TG_ALLOWED_USER_ID`: your numeric Telegram user id
|
|
13
13
|
- `DEFAULT_CWD`: the folder where Codex should work
|
|
14
14
|
|
|
@@ -33,7 +33,7 @@ Published package:
|
|
|
33
33
|
Use private Telegram chat first. Do not configure group/topic mode unless I explicitly provide TG_ALLOWED_CHAT_ID or TG_ALLOWED_TOPIC_ID.
|
|
34
34
|
|
|
35
35
|
Here are the required values:
|
|
36
|
-
|
|
36
|
+
TG_BOT_TOKENS=<paste one token, or comma-separated tokens here>
|
|
37
37
|
TG_ALLOWED_USER_ID=<paste numeric Telegram user id here>
|
|
38
38
|
DEFAULT_CWD=<paste absolute working directory here>
|
|
39
39
|
|
|
@@ -49,13 +49,13 @@ Tasks:
|
|
|
49
49
|
9. Verify the final state:
|
|
50
50
|
- foxclaw.service is active/enabled on Linux
|
|
51
51
|
- foxclaw status works
|
|
52
|
-
10. Report the commands used, the final status, and the log command I should use if something stops working. Redact
|
|
52
|
+
10. Report the commands used, the final status, and the log command I should use if something stops working. Redact TG_BOT_TOKENS and never print the full token or full .env content.
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
## Safety Notes
|
|
56
56
|
|
|
57
57
|
- Do not paste bot tokens into public issue trackers or public chat logs.
|
|
58
58
|
- Do not commit `.env`.
|
|
59
|
-
- When reporting results, redact `TG_BOT_TOKEN`.
|
|
59
|
+
- When reporting results, redact `TG_BOT_TOKENS` and legacy `TG_BOT_TOKEN`.
|
|
60
60
|
- Do not use `/` or your whole home directory as `DEFAULT_CWD` for a first install.
|
|
61
61
|
- Use `foxclaw start` for normal service startup. Use foreground `foxclaw serve` only when troubleshooting.
|
|
@@ -143,7 +143,7 @@ nano ~/.foxclaw/.env
|
|
|
143
143
|
For a first private-chat install, fill only the important values:
|
|
144
144
|
|
|
145
145
|
```dotenv
|
|
146
|
-
|
|
146
|
+
TG_BOT_TOKENS=123456789:replace_with_your_bot_token
|
|
147
147
|
TG_ALLOWED_USER_ID=123456789
|
|
148
148
|
TG_ALLOWED_CHAT_ID=
|
|
149
149
|
TG_ALLOWED_TOPIC_ID=
|
|
@@ -176,7 +176,7 @@ You want to see:
|
|
|
176
176
|
```text
|
|
177
177
|
[OK] node >= 24
|
|
178
178
|
[OK] codex cli available
|
|
179
|
-
[OK] telegram bot token configured
|
|
179
|
+
[OK] telegram bot token(s) configured
|
|
180
180
|
[OK] telegram allowed user configured
|
|
181
181
|
[OK] default cwd exists
|
|
182
182
|
```
|
|
@@ -280,7 +280,7 @@ Update FoxClaw later:
|
|
|
280
280
|
foxclaw update
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
-
You can also send `/update` in an authorized Telegram chat. When no
|
|
283
|
+
You can also send `/update` in an authorized Telegram chat. When every bot is idle with no approval or question active, it attempts to update an npm/pnpm-managed Codex CLI, upgrades FoxClaw, checks, restarts the service, and reports the result after restart.
|
|
284
284
|
|
|
285
285
|
## Next Step
|
|
286
286
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -20,7 +20,7 @@ journalctl --user -u foxclaw.service -f
|
|
|
20
20
|
| --- | --- | --- |
|
|
21
21
|
| `[FAIL] node >= 24` | Your current shell is using an older Node.js. | Install or activate Node.js 24+ by any method, then rerun `foxclaw doctor`. If the service uses old Node, reinstall it from a Node 24+ shell with `foxclaw start`. |
|
|
22
22
|
| `[FAIL] codex cli available` | The `codex` command is not in PATH. | Install Codex CLI or fix PATH, then confirm `codex --version` works. |
|
|
23
|
-
| `[FAIL] telegram bot token configured` | `TG_BOT_TOKEN` is
|
|
23
|
+
| `[FAIL] telegram bot token(s) configured` | Neither `TG_BOT_TOKENS` nor legacy `TG_BOT_TOKEN` is present in `.env`. | Put one or more comma-separated `@BotFather` tokens in `TG_BOT_TOKENS`. |
|
|
24
24
|
| `[FAIL] telegram allowed user configured` | `TG_ALLOWED_USER_ID` is missing from `.env`. | Get your numeric id from `@userinfobot` and add it to `.env`. |
|
|
25
25
|
| `[FAIL] default cwd exists` | `DEFAULT_CWD` points to a folder that does not exist. | Create the folder or change `DEFAULT_CWD` to an existing absolute path. |
|
|
26
26
|
|
package/docs/user-manual.md
CHANGED
|
@@ -100,7 +100,7 @@ Both install the same published npm package. Use one global package manager cons
|
|
|
100
100
|
|
|
101
101
|
### 1.6 Fill In The Config
|
|
102
102
|
|
|
103
|
-
`foxclaw init` creates the default config file at `~/.foxclaw/.env` and prompts for
|
|
103
|
+
`foxclaw init` creates the default config file at `~/.foxclaw/.env` and prompts for one or more comma-separated Telegram bot tokens, your numeric Telegram user id, and the default workspace. If the current shell has proxy variables such as `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY`, it also asks whether to save them into the FoxClaw config. When `HTTP_PROXY` or `HTTPS_PROXY` is configured, FoxClaw passes it to systemd/launchd explicitly and enables Node's env proxy support. Press Enter on any field to skip it, then edit manually if needed:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
$EDITOR ~/.foxclaw/.env
|
|
@@ -109,7 +109,7 @@ $EDITOR ~/.foxclaw/.env
|
|
|
109
109
|
Minimum private-chat config:
|
|
110
110
|
|
|
111
111
|
```dotenv
|
|
112
|
-
|
|
112
|
+
TG_BOT_TOKENS=123456789:replace_with_your_bot_token
|
|
113
113
|
TG_ALLOWED_USER_ID=123456789
|
|
114
114
|
TG_ALLOWED_CHAT_ID=
|
|
115
115
|
TG_ALLOWED_TOPIC_ID=
|
|
@@ -120,7 +120,7 @@ DEFAULT_SANDBOX_MODE=workspace-write
|
|
|
120
120
|
|
|
121
121
|
Fields:
|
|
122
122
|
|
|
123
|
-
- `
|
|
123
|
+
- `TG_BOT_TOKENS`: one or more `@BotFather` tokens separated by commas. The legacy single-bot `TG_BOT_TOKEN` setting remains compatible.
|
|
124
124
|
- `TG_ALLOWED_USER_ID`: your numeric Telegram user id.
|
|
125
125
|
- `TG_ALLOWED_CHAT_ID`: leave empty for the first private-chat setup.
|
|
126
126
|
- `TG_ALLOWED_TOPIC_ID`: leave empty unless binding a Telegram topic.
|
|
@@ -136,7 +136,7 @@ foxclaw start
|
|
|
136
136
|
foxclaw status
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
For later upgrades, run `foxclaw update`. It uses the npm or pnpm installation method currently managing FoxClaw,
|
|
139
|
+
For later upgrades, run `foxclaw update`. It uses the npm or pnpm installation method currently managing FoxClaw, attempts to update a globally npm/pnpm-managed Codex CLI, runs checks, and restarts the background service.
|
|
140
140
|
|
|
141
141
|
Linux service logs:
|
|
142
142
|
|
|
@@ -233,7 +233,7 @@ Later commands are sorted by recent usage. Plain text, photos, and files continu
|
|
|
233
233
|
- `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary. Local session, token, and visible-reply-throughput metrics use a background-generated historical snapshot instead of scanning large logs during the request; throughput is computed end-to-end for completed turns, excluding reasoning tokens while including waiting and tool execution time.
|
|
234
234
|
- `/account`: current Codex account.
|
|
235
235
|
- `/quota`: Codex usage and quota window.
|
|
236
|
-
- `/update`: upgrade FoxClaw, run checks, and restart the service; it refuses while a turn, approval, or question
|
|
236
|
+
- `/update`: upgrade FoxClaw, attempt to update an npm/pnpm-managed Codex CLI, run checks, and restart the service; it refuses while any bot runtime has a turn, approval, or question active, then reports the result after restart.
|
|
237
237
|
|
|
238
238
|
### 3.3 `/config`, `/requirements`, `/provider`
|
|
239
239
|
|
|
@@ -335,11 +335,11 @@ Watch mode mirrors live turn progress and approval requests. The watching chat i
|
|
|
335
335
|
|
|
336
336
|
## 6. Codex Login And Auth Rotation
|
|
337
337
|
|
|
338
|
-
This is a key FoxClaw feature. Codex auth is usually stored at `~/.codex/auth.json`. FoxClaw stores multiple accounts as candidate files and switches which candidate the active `auth.json` points to.
|
|
338
|
+
This is a key FoxClaw feature. Codex auth is usually stored at `~/.codex/auth.json`. FoxClaw stores multiple accounts as candidate files and switches which candidate the active `auth.json` points to. In `TG_BOT_TOKENS` mode, each bot has an isolated Codex home, app-server, and current candidate, so bots can run and switch accounts independently; validated login/refresh credentials are safely mirrored between bot homes.
|
|
339
339
|
|
|
340
340
|
### 6.1 File Format
|
|
341
341
|
|
|
342
|
-
|
|
342
|
+
In single-bot compatibility mode, candidate files live in the Codex auth directory, usually `~/.codex/`. If `CODEX_AUTH_DIR` is set, FoxClaw uses that directory. Multi-bot mode treats that directory as its candidate source and stores isolated bot copies under `~/.foxclaw/codex/telegram/bot<id>/home/`.
|
|
343
343
|
|
|
344
344
|
Recommended layout:
|
|
345
345
|
|
|
@@ -357,7 +357,7 @@ FoxClaw recognizes candidate names in these forms:
|
|
|
357
357
|
- `auth.json.<name>`
|
|
358
358
|
- `auth.json-<name>`
|
|
359
359
|
|
|
360
|
-
`auth.json` is what Codex currently uses. When switching accounts, FoxClaw points `auth.json` at one candidate. Candidate contents are Codex-generated JSON
|
|
360
|
+
`auth.json` is what Codex currently uses. When switching accounts, FoxClaw points `auth.json` at one candidate. Candidate contents are Codex-generated JSON and should not be hand-written. In multi-bot mode, FoxClaw mirrors a candidate only when its account identity matches and its refresh timestamp is newer, preventing a same-name candidate from overwriting a different account.
|
|
361
361
|
|
|
362
362
|
If you already have a working `auth.json`, you can save it as a candidate:
|
|
363
363
|
|
|
@@ -416,7 +416,7 @@ Equivalent commands:
|
|
|
416
416
|
- `/auth disable <n>`: skip candidate n during auto-rotation.
|
|
417
417
|
- `/auth reload` or `/auth_reload`: restart app-server and reload the current `auth.json`.
|
|
418
418
|
|
|
419
|
-
If active turns, pending approvals, pending user inputs, or MCP elicitations
|
|
419
|
+
If the requesting bot runtime has active turns, pending approvals, pending user inputs, or MCP elicitations, FoxClaw refuses manual auth switching to avoid changing accounts mid-request; another idle bot is unaffected.
|
|
420
420
|
|
|
421
421
|
### 6.4 How Auto-Rotation Works
|
|
422
422
|
|
|
@@ -452,7 +452,7 @@ auth.json_backup # backup account, enable or disable as needed
|
|
|
452
452
|
|
|
453
453
|
## 8. Safety
|
|
454
454
|
|
|
455
|
-
- Do not share `TG_BOT_TOKEN`, `~/.codex/auth.json*`, or `.env`.
|
|
455
|
+
- Do not share `TG_BOT_TOKENS`, `TG_BOT_TOKEN`, `~/.codex/auth.json*`, or `.env`.
|
|
456
456
|
- Do not use `/`, `/home`, `/Users`, or your whole home directory as the first `DEFAULT_CWD`.
|
|
457
457
|
- When unsure, use `/permissions read-only` or select `Read-only` in `/setup`.
|
|
458
458
|
- Group mode still only accepts `TG_ALLOWED_USER_ID`, but use trusted groups.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
先准备这几个值:
|
|
10
10
|
|
|
11
|
-
- `
|
|
11
|
+
- `TG_BOT_TOKENS`:从 `@BotFather` 拿到的一个或多个 Telegram bot token,多项用逗号分隔
|
|
12
12
|
- `TG_ALLOWED_USER_ID`:你的 Telegram 数字用户 ID
|
|
13
13
|
- `DEFAULT_CWD`:希望 Codex 默认工作的目录
|
|
14
14
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
先使用 Telegram 私聊模式。除非我明确提供 TG_ALLOWED_CHAT_ID 或 TG_ALLOWED_TOPIC_ID,否则不要配置群组/话题模式。
|
|
33
33
|
|
|
34
34
|
必需配置:
|
|
35
|
-
|
|
35
|
+
TG_BOT_TOKENS=<粘贴一个 token,或多个逗号分隔 token>
|
|
36
36
|
TG_ALLOWED_USER_ID=<把 Telegram 数字用户 ID 粘贴在这里>
|
|
37
37
|
DEFAULT_CWD=<把绝对工作目录粘贴在这里>
|
|
38
38
|
|
|
@@ -48,13 +48,13 @@ DEFAULT_CWD=<把绝对工作目录粘贴在这里>
|
|
|
48
48
|
9. 验证最终状态:
|
|
49
49
|
- Linux 上 foxclaw.service 处于 active/enabled
|
|
50
50
|
- foxclaw status 可以正常输出
|
|
51
|
-
10. 汇报执行过的命令、最终状态和后续看日志的命令。请隐藏
|
|
51
|
+
10. 汇报执行过的命令、最终状态和后续看日志的命令。请隐藏 TG_BOT_TOKENS,不要打印完整 token 或完整 .env。
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## 安全注意事项
|
|
55
55
|
|
|
56
56
|
- 不要把 bot token 粘贴到公开 issue、公开聊天或代码仓库。
|
|
57
57
|
- 不要提交 `.env`。
|
|
58
|
-
- 汇报结果时隐藏 `TG_BOT_TOKEN`。
|
|
58
|
+
- 汇报结果时隐藏 `TG_BOT_TOKENS` 和兼容变量 `TG_BOT_TOKEN`。
|
|
59
59
|
- 第一次安装不要把 `/`、整个 `/Users`、整个 `/home` 或完整 home 目录设为 `DEFAULT_CWD`。
|
|
60
60
|
- 日常启动用 `foxclaw start`;只有排障时才用前台模式 `foxclaw serve`。
|
package/docs/zh/foxclaw-skill.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
## 基本流程
|
|
12
12
|
|
|
13
|
-
1. 准备 `
|
|
13
|
+
1. 准备 `TG_BOT_TOKENS`(一个或多个逗号分隔的 bot token)、`TG_ALLOWED_USER_ID` 和 `DEFAULT_CWD`。
|
|
14
14
|
2. 让 Codex 使用 `skills/foxclaw`。
|
|
15
15
|
3. 如果是远程机器,提供 SSH 目标。
|
|
16
16
|
4. 让 Codex 执行安装、写配置、跑 `foxclaw doctor`。
|